stm32-external-eeprom/

Одна из проблем с микроконтроллерами 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] ):

Распиновка чипа 24LC64

VSS и VCC, понятно, представляют собой минус и плюс питания, а SDA и SCL — это I2C шина. Устройство имеет I2C-адрес 0b1010zyx, где значения x, y и z определяются тем, к чему подключены пины A0, A1 и A2. Если пин подключен к земле, соответствующий бит адреса равен нулю, если же к плюсу — то единице. Таким образом, устройство может иметь адрес от 0 x 50 до 0 x 57. Наконец, пин WP — это write protection. Если пин подключен к земле, разрешено как чтение, так и запись. Если же пин подключен к плюсу, устройство доступно только на чтение.

Создаем новый проект в STM32CubeMX для вашей отладочной платы. Лично я все также использую плату Nucleo-F411RE, но если вы используете другую, отличия в проекте должны быть минимальными. В Peripherals включаем устройство I2C1, выбрав в выпадающем списке «I2C» вместо «Disable». Также мы будем передавать что-то компьютеру, поэтому включаем устройство USART2, как делали это в заметке Микроконтроллеры STM32: обмен данными по UART . Чтобы плату можно было использовать с Arduino-шилдами, несущими какие-то I2C-устройства, пины I2C1 нужно переназначить на PB9 и PB8 (по умолчанию будут назначены другие: PB7 и PB6). В итоге должна получиться такая картинка:

Настройка I2C в STM32CubeMX

Затем генерируем код и подправляем Makefile, как обычно. Я лично просто взял Makefile из исходников к заметке про UART и дописал недостающие файлы в C_SOURCES, а именно:

$(FIRMWARE)/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_i2c.c
$(FIRMWARE)/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_i2c_ex.c

Для подключения 24LC64 к плате Nucleo я воспользовался Proto Shield от Arduino. Пришлось прорезать в нем отверстие Dremel’ем, чтобы иметь доступ к кнопкам на отладочной плате. В итоге у меня получился вот такой сэндвич:

Подключение внешнего EEPROM к плате Nucleo

Кто-то из читателей мог обратить внимание на то, что в 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 это работать не будет, и придется все-таки использовать внешние подтягивающие резисторы.

Наконец, рассмотрим основной код прошивки:

void init ( ) {
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 и Прошиваем ПЗУ с ультрафиолетовым стиранием .

EnglishRussianUkrainian