Маленькая EEPROM платка. |
Сегодня попробуем подключить внешнюю EEPROM к Maple Mini.
Когда-то давно для экспериментов с AVR были куплены несколько микросхем 24C08WP EEPROM памяти. Maple Mini содержит два независимых I2C интерфейса. В своей задаче мы будем использовать первый I2C.
Цель будет следующей. Запишем во внешнюю память любое число от 1 до 10. Затем считаем его обратно и мигнем встроенным светодиодом тем количеством, которое было получено из памяти. Прерывания I2C интерфейса использовать не будем.
Что нам потребуется.
Во-первых почитать вики про I2C и общие принципы работы. Во-вторых, скачать StdPeriph_Lib (или скачайте с оф. сайта), где есть библиотека-драйвер для I2C с подробнейшими комментариями. В-третьих, не забыть подключить резисторы на SDA, SCL линии. Если на линиях они уже есть, дополнительно подключать не стоит - при параллельном соединении сопротивление уменьшается.Желательно использовать 24C08WP с рабочим напряжением от 2.5 до 5.5 вольт или найти похожую, которая будет поддерживать рабочее напряжение МК 3.3 вольта. Для того, чтобы сразу на прямую подключать к МК.
Для I2C1 будем использовать 15 - SDA и 16 - SCL выводы Maple Mini. На плате они подписаны I2C1.
Выглядит все примерно так... |
Как записать/прочитать данные из внешней EEPROM.
Адрес памяти в I2C шине зависит от вывода E2.Этот вывод влияет на значение бита №3 (счет начинается с 0) в байте адреса микросхемы. Если подключен к земле, тогда бит будет сброшен, если к Vcc - установлен. Вывод E2 подключил к земле, поэтому байт адреса будет равен 0xA0.
Адрес микросхемы состоит из 7 битов. Последний 8-ой бит - это бит чтения/записи (R/W).
Нас интересует запись и чтение только одного байта. В мануале все прекрасно расписано.
Для записи нужно послать следующую последовательность байтов и битов.
Dev select - адрес памяти в шине и равен A0.
R/W - бит сброшен, что означает запись.
Byte address - адрес в памяти, где будет записан байт.
Data in - записываемый байт.
ACK - байт подтверждения приема байта - его отправляет микросхема памяти.
Для чтения нужно отправить адрес считываемого байта, сгенерировать повторный СТАРТ и принять байт.
Dev select - адрес памяти в шине и равен A0.
R/W - бит сброшен, что означает запись.
R/W - бит установлен, что означает чтение (прием данных).
Byte address - адрес в памяти, который будет считан.
Data out - считываемый байт.
ACK - байт подтверждения приема байта - его отправляет микросхема памяти, после повторного СТАРТ (второй start), его отправляет МК. После последнего принятого байта, чтобы закончить передачу, его не нужно отсылать.
MSB и LSB.
Старший и младший значащий бит соответственно - это по сути название первого и последнего бита. Например, один байт состоит из 8 битов. Если эти 8 битов записать на бумажке - 10101010, то старшим значащим битом будет самая левая единичка, а младшим - самый правый нолик. В контексте I2C, MSB - старший значащий бит который будет передан первый, а LSB - младший значащий бит который будет передан последним.Режимы I2C.
Бывает 4 основных режима (modes): slave receiver - ведомый принимает данные; slave transmitter - ведомый шлет данные; master receiver - ведущий принимает данные; master transmitter - ведущий шлет данные.По умолчанию активирован slave mode. I2C интерфейс автоматически переходит из slave в master mode, когда МК генерирует START состояние. Так же режим автоматически меняется из master в slave mode, когда теряется арбитраж или после активации STOP состояния. Это позволяет работать нескольким master'ам на одной линии.
Я сэкономил бы огромное количество времени, если бы знал что разработку следует начинать не только с datasheet, но и вместе с errata. Errata - такой документ, написанный производителем, в котором описаны известные аппаратные баги микроконтроллера и их решение.
Переназначение выводов I2C1.
За переназначение выходов I2C1 отвечает AFIO->MAPR бит. Если он сброшен тогда переназначения не будет и будут использоваться PB6 - (16 вывод платы) и PB7 (15) выводы. Если бит установлен, тогда будут использоваться PB8 и PB9 выводы. На плате Maple Mini выводы PB8 и PB9, не выведены, но вывод PB8 используется кнопкой, поэтому переназначать, без переделок, нет смысла.Переназначение выводов I2C2 в МК не предусмотрено.
Включаем I2C интерфейс.
После того как подключили внешнюю EEPROM, переходим к написанию программы. Для настройки I2C я использовал StdPeriph библиотеку. Алгоритм следующий:- Включить тактирование для необходимых интерфейсов и настроить GPIO:
- Включить тактирование для AFIO;
- Включить тактирование для I2C интерфейса;
- Настроить remapping - какие линии внешних портов будут использоваться для SDA/SCL;
- Включить тактирование для выбранного GPIO, где будут SDA/SCL линии;
- Настроить выбранные в 3 шаге GPIO линии как альтернативный выход с открытым стоком и максимальной выходной частотой в 50 МГц;
- Настроить работу I2C интерфейса (StdPeriph).
- Настроить частоту SCL линии;
- Включить режим чистого I2C, без SMBus;
- Рассчитать и установить скважность.
- Заполнить адрес МК.
- Активировать подтверждение ACK битом. Отсылает ACK бит после каждого успешно принятого байта.
- Настроить длину адреса = 7 бит на шине I2C;
Напишем функцию которая подготовит I2C к работе.
void I2CMainConfig(uint16_t clockSpeed) {
I2C_InitTypeDef I2CInitConfig;
/* --== Настраиваем выходы I2C интерфейса: ==-- */
//включаем тактирование для AFIO
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
//включаем тактирование для I2C1. Используем APB1
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
//настроим AFIO не переназначать выходы I2C1 интерфейса.
//PB6 = SCL, PB7 = SDA
AFIO->MAPR &=~ AFIO_MAPR_I2C1_REMAP;
//включаем тактирование для GPIO B
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
//устанавливаем PB6 и PB7 as Alternate function open drain
GPIOB->CRL |= GPIO_CRL_CNF6_0;
GPIOB->CRL |= GPIO_CRL_CNF6_1;
GPIOB->CRL |= GPIO_CRL_CNF7_0;
GPIOB->CRL |= GPIO_CRL_CNF7_1;
//устанавливаем макс. частоту = 50 MHz
GPIOB->CRL |= GPIO_CRL_MODE6_1;
GPIOB->CRL |= GPIO_CRL_MODE6_0;
GPIOB->CRL |= GPIO_CRL_MODE7_1;
GPIOB->CRL |= GPIO_CRL_MODE7_0;
/* --== Настраиваем I2C интерфейс: ==-- */
I2C_DeInit(I2C1);
I2CInitConfig.I2C_ClockSpeed = clockSpeed * 1000;
I2CInitConfig.I2C_Mode = I2C_Mode_I2C;
I2CInitConfig.I2C_DutyCycle = I2C_DutyCycle_16_9;
I2CInitConfig.I2C_OwnAddress1 = 10;
I2CInitConfig.I2C_Ack = I2C_Ack_Enable;
I2CInitConfig.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_Init(I2C1, &I2CInitConfig);
}
I2C_InitTypeDef I2CInitConfig;
/* --== Настраиваем выходы I2C интерфейса: ==-- */
//включаем тактирование для AFIO
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
//включаем тактирование для I2C1. Используем APB1
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
//настроим AFIO не переназначать выходы I2C1 интерфейса.
//PB6 = SCL, PB7 = SDA
AFIO->MAPR &=~ AFIO_MAPR_I2C1_REMAP;
//включаем тактирование для GPIO B
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
//устанавливаем PB6 и PB7 as Alternate function open drain
GPIOB->CRL |= GPIO_CRL_CNF6_0;
GPIOB->CRL |= GPIO_CRL_CNF6_1;
GPIOB->CRL |= GPIO_CRL_CNF7_0;
GPIOB->CRL |= GPIO_CRL_CNF7_1;
//устанавливаем макс. частоту = 50 MHz
GPIOB->CRL |= GPIO_CRL_MODE6_1;
GPIOB->CRL |= GPIO_CRL_MODE6_0;
GPIOB->CRL |= GPIO_CRL_MODE7_1;
GPIOB->CRL |= GPIO_CRL_MODE7_0;
/* --== Настраиваем I2C интерфейс: ==-- */
I2C_DeInit(I2C1);
I2CInitConfig.I2C_ClockSpeed = clockSpeed * 1000;
I2CInitConfig.I2C_Mode = I2C_Mode_I2C;
I2CInitConfig.I2C_DutyCycle = I2C_DutyCycle_16_9;
I2CInitConfig.I2C_OwnAddress1 = 10;
I2CInitConfig.I2C_Ack = I2C_Ack_Enable;
I2CInitConfig.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_Init(I2C1, &I2CInitConfig);
}
I2C1 - это библиотечная структура всех регистров первого I2C интерфейса. Используй эту структуру всегда, когда хочешь как-либо взаимодействовать с интерфейсом.
Для настройки I2C интерфейса используется структура I2CInitConfig.
I2C_Init() - настраивает I2C1 интерфейс используя значения I2CInitConfig структуры.
Рассчитываем значения регистров.
Частота APB1 используется для тактирования I2C. Она настраивается битами RCC_CFGR->PPRE1, которые содержат значение делителя системной частоты (72 МГц). Обычно после SystemInit(); частота APB1 равна 36 МГц и не должна превышать это значение. Она обозначается как FPCLK1. Значение периода обозначают как TPCLK1 = 1 / FPCLK1.
Значение CR2->FREQ в регистре указывается в МГц. Это значит, что его значение выставляют равным значению FPCLK1 / 1000 000.
Есть два стандарта скорости I2C: Standard Mode = 100 кГц и Fast Mode = 400 кГц в SCL линии.
I2C_CCR->DUTY битом выставляется скважность сигнала для Fast Mode. Она может быть либо 2 к 1 либо 16 к 9. Значение скважности для Standard Mode равен 1 к 1. Этот параметр настраивает соотношение низкого TLOW и высокого уровня THIGHT сигнала в SCL линии.
T - период, t1 - высокий уровень, t2 - низкий уровень. |
В Standard Mode, значение CCR = FPCLK1 / 2 * FSCL.
TLOW + THIGHT = CCR * TPCLK1 + CCR * TPCLK1;
1 / FSCL = 2 * CCR * TPCLK1;
1 / FSCL = 2 * CCR / FPCLK1;
CCR = FPCLK1 / 2 * FSCL;
В Fast Mode, если скважность 2 к 1, то CCR = FPCLK1 / 3 * FSCL.
Если скважность 16 к 9, тогда CCR = FPCLK1 / 25 * FSCL.Значение TRISE для Standard Mode рассчитывается: (1000 ns / TPCLK1) + 1.
Для Fast Mode: (300 ns / TPCLK1) + 1.
Fast Mode включается установкой бита I2C_CCR->F/W.
Способы взаимодействия с I2C интерфейсом.
Когда отправлен/получен байт данных/адреса или произошла ошибка или изменилось состояние линии, то все это отображается в I2C->SR1 и I2C->SR2 регистрах. Они называются регистры статуса. Помимо отображения в регистрах статуса, будут вызваны события/прерывания, если установлены ITBUFEN, ITEVTEN, ITERREN биты. Любое такое изменение называется событие.В StdPeriph библиотеке предлагают 3 способа взаимодействия с I2C.
Первый способ - Basic state monitoring. В библиотеке прописаны константы событий (см. секцию I2C_Events в stm32f10x_i2c.h). И разработчик предлагает использовать I2C_CheckEvent() функцию, которая сравнивает регистры статуса с ожидаемым событием, которое указывается в параметрах функции. В случае, если ожидаемое событие совпало, то она вернет 1. Этот способ лучше всего подходит для ознакомления. Будем его использовать.
Второй способ - Advanced state monitoring. Необходимо использовать I2C_GetLastEvent() функцию, которая просто возвращает конкатенацию регистров статуса. Удобно тем, что можно самостоятельно сравнивать отдельные биты статуса или сравнивать со своими константами событий. Этот способ более гибок чем первый.
Третий способ - Flag-based state monitoring. Используется I2C_GetFlagStatus() функция, которая возвращает текущий статус флаг, а не событие.
Первый способ будем реализовывать следующим образом: даем интерфейсу задачу и ждем событие, которое подтвердит выполнение поставленной задачи.
Отсылаем данные.
Напишем функцию, которая будет отсылать адрес и данные используя Basic state monitoring способ.Алгоритм следующий:
- Генерируем СТАРТ состояние на шине. Ждем выполнения.
- Отправляем Адрес slave устройства и указываем что будем отправлять данные. Ждем выполнения.
- Отправляем байты данных. Ждем выполнения.
- Генерируем СТОП или СТАРТ, если необходимо сгенерировать повторный СТАРТ.
void sendViaI2cNI(uint8_t address, uint8_t *requestData, uint8_t requestLength, uint8_t callReStartNotStopFlag) {
//генерируем СТАРТ состояние
I2C_GenerateSTART(I2C1, ENABLE);
//ждем когда СТАРТ состояние будет сгенерировано
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
//отправляем адрес slave устройства
I2C_Send7bitAddress(I2C1, address, I2C_Direction_Transmitter);
//ждем когда адрес будет отправлен
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
//отправляем данные
for (uint16_t i = 0; i < requestLength; i++) {
I2C_SendData(I2C1, requestData[i]);
//ждем когда байт данных будет отправлен
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
}
if (callReStartNotStopFlag) {
I2C_GenerateSTART(I2C1, ENABLE);
} else {
I2C_GenerateSTOP(I2C1, ENABLE);
}
}
//генерируем СТАРТ состояние
I2C_GenerateSTART(I2C1, ENABLE);
//ждем когда СТАРТ состояние будет сгенерировано
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
//отправляем адрес slave устройства
I2C_Send7bitAddress(I2C1, address, I2C_Direction_Transmitter);
//ждем когда адрес будет отправлен
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
//отправляем данные
for (uint16_t i = 0; i < requestLength; i++) {
I2C_SendData(I2C1, requestData[i]);
//ждем когда байт данных будет отправлен
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
}
if (callReStartNotStopFlag) {
I2C_GenerateSTART(I2C1, ENABLE);
} else {
I2C_GenerateSTOP(I2C1, ENABLE);
}
}
Параметры функции: address - адрес slave устройства; requestData - массив отправляемых байтов; requestLength - длина массива requestData; callReStartNotStopFlag - если 1, тогда в конце генерируем пов. СТАРТ вместо СТОП.
Принимаем данные.
Теперь напишем функцию принимающую данные. В начале следует отправить запрос (например, запрос с адресом считываемого байта во внешней памяти), генерируем пов. СТАРТ и только потом принимаем данные. Алгоритм:- Включаем подтверждение принятых байтов. Отправляем запрос на запрашиваемые данные используя предыдущую функцию. В конце отправляем повторный СТАРТ. Ждем выполнения.
- Отправляем адрес и указываем что будем принимать данные. Ждем выполнения.
- Принимаем байты данных. Ждем выполнения.
- Отменяем подтверждение приема последнего принятого байта с помощью ACK бита.
- И принимаем последний байт.
- Генерируем СТОП состояние.
void receiveViaI2cNI(uint8_t address, uint8_t *requestData, uint8_t requestLength, uint8_t *responseData, uint8_t responseLength) {
//возобновляем подтверждение ACK
I2C_AcknowledgeConfig(I2C1, ENABLE);
//Отправляем запрос
sendViaI2cNI(address, requestData, requestLength, 1);
//Ждем выполнения, когда будет сгенерирован повторный СТАРТ
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
//Отправляем адрес и указываем что будем принимать данные
I2C_Send7bitAddress(I2C1, address, I2C_Direction_Receiver);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
//Принимаем побайтово данные
for (uint16_t i = 0; i < responseLength - 1; i++) {
//Сначала ждем когда будет принят байт
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED));
//Считываем его
responseData[i] = I2C_ReceiveData(I2C1);
}
//Отключаем подтверждение ACK для последнего байта
I2C_AcknowledgeConfig(I2C1, DISABLE);
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED));
responseData[responseLength - 1] = I2C_ReceiveData(I2C1);
//Генерируем СТОП
I2C_GenerateSTOP(I2C1, ENABLE);
}
//возобновляем подтверждение ACK
I2C_AcknowledgeConfig(I2C1, ENABLE);
//Отправляем запрос
sendViaI2cNI(address, requestData, requestLength, 1);
//Ждем выполнения, когда будет сгенерирован повторный СТАРТ
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
//Отправляем адрес и указываем что будем принимать данные
I2C_Send7bitAddress(I2C1, address, I2C_Direction_Receiver);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
//Принимаем побайтово данные
for (uint16_t i = 0; i < responseLength - 1; i++) {
//Сначала ждем когда будет принят байт
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED));
//Считываем его
responseData[i] = I2C_ReceiveData(I2C1);
}
//Отключаем подтверждение ACK для последнего байта
I2C_AcknowledgeConfig(I2C1, DISABLE);
while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED));
responseData[responseLength - 1] = I2C_ReceiveData(I2C1);
//Генерируем СТОП
I2C_GenerateSTOP(I2C1, ENABLE);
}
Параметры функции: address - адрес slave устройства; requestData - массив отправляемых байтов; requestLength - длина массива requestData, responseData - массив принятых байтов; responseLength - длина массива responseData.
Эксперимент №1.
Когда у нас есть написанные функции инициализации и приема/передачи данных, нужно их опробовать.Алгоритм:
- Инициализация.
- Мигнем светодиодом один раз. Это будет означать что инициализация прошла успешно.
- Записываем число во внешнюю память.
- Ждем 5 мс.
- Считываем число из внешней памяти.
- Мигаем встроенным светодиодом тем количеством раз, которое было получено из памяти.
#include <stdio.h>
#include <stm32f10x.h>
//tact in one ms
uint32_t tactsInOneMs = 1;
//delay in ms
void delay(uint16_t waitInMs) {
uint32_t countTacts = waitInMs * tactsInOneMs;
for(; countTacts != 0; countTacts--) {
}
}
/*
...
Здесь должны быть функции I2CMainConfig, sendViaI2cNI, receiveViaI2cNI...
...
*/
int main(void) {
uint8_t requestData[32], requestByteAddress[1], responseData[32];
uint8_t receivedByte = 0;
SystemInit();
tactsInOneMs = SystemCoreClock / 10000;
I2CMainConfig(100);//скорость 100 кГц
//настраиваем LED линию (PB1)
GPIOB->CRL &=~(GPIO_CRL_CNF1_0);
GPIOB->CRL &=~(GPIO_CRL_CNF1_1);
GPIOB->CRL |= (GPIO_CRL_MODE1_1);
GPIOB->CRL |= (GPIO_CRL_MODE1_0);
//мигнем один раз, как конец инициализации
GPIOB->ODR |= GPIO_ODR_ODR1;
delay(1000);
GPIOB->ODR &=~ GPIO_ODR_ODR1;
delay(1000);
//готовим запрос для записи байта
requestData[0] = 0x01;//адрес байта в памяти
requestData[1] = 0x06;//значение байта
//записываем байт во внешнюю память
sendViaI2cNI(0xA0, requestData, 2, 0);
//ждем 5 мс, чтобы внешняя память успела записать новое значение
delay(5);
//считываем байт из памяти
requestByteAddress [0] = 0x01;// адрес считываемого байт в памяти
receiveViaI2cNI(0xA0, requestByteAddress, 1, responseData, 1);
receivedByte = responseData[0];
//мигаем встроенным светодиодом
for(uint8_t i = 0; i < receivedByte; i++){
GPIOB->ODR |= GPIO_ODR_ODR1;
delay(500);
GPIOB->ODR &=~ GPIO_ODR_ODR1;
delay(500);
}
while (1) {
}
}
#ifdef USE_FULL_ASSERT
void assert_failed(uint8_t* file, uint32_t line) {
}
#endif
#include <stm32f10x.h>
//tact in one ms
uint32_t tactsInOneMs = 1;
//delay in ms
void delay(uint16_t waitInMs) {
uint32_t countTacts = waitInMs * tactsInOneMs;
for(; countTacts != 0; countTacts--) {
}
}
/*
...
Здесь должны быть функции I2CMainConfig, sendViaI2cNI, receiveViaI2cNI...
...
*/
int main(void) {
uint8_t requestData[32], requestByteAddress[1], responseData[32];
uint8_t receivedByte = 0;
SystemInit();
tactsInOneMs = SystemCoreClock / 10000;
I2CMainConfig(100);//скорость 100 кГц
//настраиваем LED линию (PB1)
GPIOB->CRL &=~(GPIO_CRL_CNF1_0);
GPIOB->CRL &=~(GPIO_CRL_CNF1_1);
GPIOB->CRL |= (GPIO_CRL_MODE1_1);
GPIOB->CRL |= (GPIO_CRL_MODE1_0);
//мигнем один раз, как конец инициализации
GPIOB->ODR |= GPIO_ODR_ODR1;
delay(1000);
GPIOB->ODR &=~ GPIO_ODR_ODR1;
delay(1000);
//готовим запрос для записи байта
requestData[0] = 0x01;//адрес байта в памяти
requestData[1] = 0x06;//значение байта
//записываем байт во внешнюю память
sendViaI2cNI(0xA0, requestData, 2, 0);
//ждем 5 мс, чтобы внешняя память успела записать новое значение
delay(5);
//считываем байт из памяти
requestByteAddress [0] = 0x01;// адрес считываемого байт в памяти
receiveViaI2cNI(0xA0, requestByteAddress, 1, responseData, 1);
receivedByte = responseData[0];
//мигаем встроенным светодиодом
for(uint8_t i = 0; i < receivedByte; i++){
GPIOB->ODR |= GPIO_ODR_ODR1;
delay(500);
GPIOB->ODR &=~ GPIO_ODR_ODR1;
delay(500);
}
while (1) {
}
}
#ifdef USE_FULL_ASSERT
void assert_failed(uint8_t* file, uint32_t line) {
}
#endif
Если определить макрос USE_FULL_ASSERT в stm32f10x_conf.h (по умолчанию не определен), то необходимо реализовать функцию assert_failed(). Она вызывается когда макрос assert_param возвращает ошибку. В параметрах этой функции передается имя файла и строка в которой произошла ошибка. Если макрос не определен, тогда assert_param возвращает 0 в случае неудачи. По умолчанию он не определен, поэтому функцию можно не реализовывать, но для учебной цели о ней стоило упомянуть.
Макрос assert_param(expr) используется для проверки параметров функций StdPeriph библиотеки, где expr - это выражение проверки параметра.
Заливаем прошивку. Светодиод мигнет один долгий раз после инициализации, а потом мигнет 6 "быстрых" раз. Нажимаем reset на плате и повторно убеждаемся что все работает.
Комментариев нет:
Отправить комментарий