Микроконтроллеры STM32: работа с внешним EEPROM
28 февраля 2018
Одна из проблем с микроконтроллерами STM32 заключается в том, что большинство из них не имеют встроенного EEPROM. Исключением являются только микроконтроллеры серий STM32L0 и STM32L1 с ультра низким энергопотреблением. Это довольно странно после работы с AVR, где EEPROM есть у всех микроконтроллеров. Существует несколько решений, но в рамках этой заметки мы рассмотрим самое очевидное — использование внешнего EEPROM на примере чипа с I2C-интерфейсом 24LC64.
Дополнение: Альтернативное решение заключается в том, чтобы воспользоваться встроенной backup memory микроконтроллеров STM32. Подробности по этой теме вы найдете в посте Микроконтроллеры STM32: использование встроенных RTC.
Цифра 64 в названии говорит о том, что устройство имеет 64 килобит памяти, или 8 Кбайт. Есть аналогичные чипы от разных производителей и с разными объемами памяти — я видел от 128 байт (например, M24C01 от компании ST) до 256 Кбайт (AT24CM02 производства Atmel). В плане распиновки и интерфейса все они абсолютно взаимозаменяемы. Далее я буду говорить о 24LC64, производимом компанией Microchip, так как сам использовал именно его.
Распиновка 24LC64 выглядит так (даташит [PDF]):
VSS и VCC, понятно, представляют собой минус и плюс питания, а SDA и SCL — это I2C шина. Устройство имеет I2C-адрес 0b1010zyx, где значения x, y и z определяются тем, к чему подключены пины A0, A1 и A2. Если пин подключен к земле, соответствующий бит адреса равен нулю, если же к плюсу — то единице. Таким образом, устройство может иметь адрес от 0x50 до 0x57. Наконец, пин WP — это write protection. Если пин подключен к земле, разрешено как чтение, так и запись. Если же пин подключен к плюсу, устройство доступно только на чтение.
Создаем новый проект в STM32CubeMX для вашей отладочной платы. Лично я все также использую плату Nucleo-F411RE, но если вы используете другую, отличия в проекте должны быть минимальными. В Peripherals включаем устройство I2C1, выбрав в выпадающем списке «I2C» вместо «Disable». Также мы будем передавать что-то компьютеру, поэтому включаем устройство USART2, как делали это в заметке Микроконтроллеры STM32: обмен данными по UART. Чтобы плату можно было использовать с Arduino-шилдами, несущими какие-то I2C-устройства, пины I2C1 нужно переназначить на PB9 и PB8 (по умолчанию будут назначены другие: PB7 и PB6). В итоге должна получиться такая картинка:
Затем генерируем код и подправляем Makefile, как обычно. Я лично просто взял Makefile из исходников к заметке про UART и дописал недостающие файлы в C_SOURCES, а именно:
$(FIRMWARE)/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_i2c_ex.c \
Для подключения 24LC64 к плате Nucleo я воспользовался Proto Shield от Arduino. Пришлось прорезать в нем отверстие Dremel’ем, чтобы иметь доступ к кнопкам на отладочной плате. В итоге у меня получился вот такой сэндвич:
Кто-то из читателей мог обратить внимание на то, что в I2C-шине должны использоваться резисторы, подтягивающие SCL и SDA к плюсу, но в этом проекте мы их не используем. Дело в том, что для моей платы и использованного в ней микроконтроллера STM32CubeMX автоматически включает встроенные подтягивающие резисторы на соответствующих пинах.
Убедиться в этом несложно, посмотрев код процедуры HAL_I2C_MspInit в файле ./Src/stm32f4xx_hal_msp.c:
/**I2C1 GPIO Configuration
PB8 ------> I2C1_SCL
PB9 ------> I2C1_SDA
*/
GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// ...
Правда, не так очевидно, почему процедура HAL_I2C_MspInit вообще откуда-то вызывается. Ответ можно найти в файле stm32f4xx_hal_i2c.c, где эта процедура объявляется с атрибутом __weak
и вызывается из процедуры HAL_I2C_Init. Атрибут __weak
работает таким образом, что при сборке не-weak процедура из кода нашего проекта подменяет собой weak-процедуру из HAL, за счет чего она и будет вызвана.
Заметьте, однако, что встроенные подтягивающие резисторы доступны не во всех микроконтроллерах STM32. Насколько мне известно, для серии STM32F1 это работать не будет, и придется все-таки использовать внешние подтягивающие резисторы.
Наконец, рассмотрим основной код прошивки:
const char wmsg[] = "Some data";
char rmsg[sizeof(wmsg)];
// HAL expects address to be shifted one bit to the left
uint16_t devAddr = (0x50 << 1);
uint16_t memAddr = 0x0100;
HAL_StatusTypeDef status;
// Hint: try to comment this line
HAL_I2C_Mem_Write(&hi2c1, devAddr, memAddr, I2C_MEMADD_SIZE_16BIT,
(uint8_t*)wmsg, sizeof(wmsg), HAL_MAX_DELAY);
for(;;) { // wait...
status = HAL_I2C_IsDeviceReady(&hi2c1, devAddr, 1,
HAL_MAX_DELAY);
if(status == HAL_OK)
break;
}
HAL_I2C_Mem_Read(&hi2c1, devAddr, memAddr, I2C_MEMADD_SIZE_16BIT,
(uint8_t*)rmsg, sizeof(rmsg), HAL_MAX_DELAY);
if(memcmp(rmsg, wmsg, sizeof(rmsg)) == 0) {
const char result[] = "Test passed!\r\n";
HAL_UART_Transmit(&huart2, (uint8_t*)result, sizeof(result)-1,
HAL_MAX_DELAY);
} else {
const char result[] = "Test failed :(\r\n";
HAL_UART_Transmit(&huart2, (uint8_t*)result, sizeof(result)-1,
HAL_MAX_DELAY);
}
}
Для работы с внешней памятью в HAL предусмотрены специальные процедуры HAL_I2C_Mem_Read и HAL_I2C_Mem_Write. Заметьте, что эти процедуры работают с I2C адресами, сдвинутыми на 1 бит влево. Связано это с тем, что в протоколе I2C семибитный адрес устройства и бит операции (чтение или запись) передаются в одном байте. Использование «сдвинутых» адресов позволяет выполнять чуть меньше ассемблерных инструкций, что нередко бывает важно в разработке встраиваемых систем. Еще стоит обратить внимание на то, что перед чтением с устройства мы должны дождаться его готовности с помощью процедуры HAL_I2C_IsDeviceReady. Наконец, здесь я забил на коды возврата большинства использованных процедур, чего в боевом коде, пожалуй, делать не стоит.
Fun fact! Вдумчивый читатель, конечно же, обратил внимание на тот факт, что адрес памяти имеет тип uint16_t
. Спрашивается, как можно адресовать им более 64 Кбайт памяти, например, те же 256 Кбайт у AT24CM02? Само собой разумеется, никак. Чипы, имеющие более 64 Кбайт памяти, начинают использовать для адресации младшие биты I2С-адреса устройства. То есть, с точки зрения нашего кода, они будут выглядеть, как 2 или более отдельных I2C-устройства. Соответствующие пины, определяющие адрес устройства, при этом являются NC, то есть, ни к чему не подключаются.
При работе с EEPROM нужно учитывать еще пару важных моментов:
- 24LC64 и его родственники хранят данные в страницах по 32 байта. За один вызов HAL_I2C_Mem_Write вы можете записать только одну страницу, причем нужно учитывать не только размер данных, но и их выравнивание. Если требутеся записать больше одной страницы, процедуру нужно вызывать в цикле. На чтение, насколько я смог выяснить, таких ограничений нет — читать можно сколько угодно и по любым смещениям;
- Запись в EEPROM может быть прервана в любой момент (сел аккумулятор, пользователь выдернул кабель питания, …). Поэтому в боевом коде стоит хранить вместе с данными их контрольную сумму. В случае, если будет записана только часть данных, это позволит обнаружить проблему и использовать хотя бы параметры по умолчанию, а не какой-то мусор.
Полную версию исходников к этой заметке вы найдете на GitHub. В качестве небольшого домашнего задания можете модифицировать прошивку так, чтобы она дампила все содержимое EEPROM и передавала его по UART. Код форматирования бинарных данных в стиле того, как это делает hexdump, можно взять из поста Перехват сетевого трафика при помощи библиотеки libpcap (процедура print_data_hex). В итоге должен получиться приятный такой отладочный инструмент — простенький, но со вкусом.
Дополнение: Вас также могут заинтересовать посты Работа с SPI на примере флеш-памяти AT45DB161E, Учимся работать с SDHC/SDXC-картами по SPI и Прошиваем ПЗУ с ультрафиолетовым стиранием.
Метки: STM32, Электроника.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.