Подразумевается, что читатель уже имеет некоторое представление о шине i2c. Например, такие вещи как подключение устройств к шине, что устройства должны иметь уникальные адреса, что устройств может быть не более 127, что нужны подтягивающие резисторы, что посылка начинается с состояния старт, а заканчивается состоянием стоп. Но немного не хватает практики в части визуального контроля и понимания процессов. В этой статье я постараюсь лишить читателя этого самого практического пробела в знаниях.
Итак, рассмотрим поведение шины I2C при работе с микроконтроллером stm32 с использованием библиотеки HAL. На шине I2C1 подсоединена микросхема EEPROM 24LC21.
Согласно описанию на микросхему ее адрес на шине – 1010ххх(7бит), где ХХХ – не имеет значения, хоть ноль, хоть единицы. Следом за адресом устройства идет 8й бит R/W̅. Этот бит говорит slave-устройству, что нужно будет делать дальше – принимать или отдавать данные. Если лог 0, то slave-устройство будет принимать данные (write), то есть запись в slave-устройство. Если лог 1, то slave-устройство будет отправлять данные (read), то есть чтение из slave-устройства. Когда slave-устройство видит на шине свой адрес, то на 9м тактовом импульсе (на линии SCL) оно прижимает линию SDAв ноль, сообщая master-устройству о своем присутствии на линии. Если мастер выдал адрес, но устройства с таким адресом на шине не обнаружилось, то на 9м тактовом импульсе линия SDAбудет в единице. Это девятый бит называют битом подтверждения, acknowledge bit (А, АСК). Каждый обмен сообщениями обрамляется стартовым и стоповым битом. Стартовый бит это состояние когда при высоком состоянии линии SCL линия SDA прижимается к нулю. Стоповый бит это состояние когда при высоком состоянии линии SCL линия SDA прижимается к единице.
Выписка из описания на микросхему

Напишем простую программу чтения из микросхемы трех байт с адреса 5. Для этого нужно сперва записать в микросхему число 5. Это число внутри микросхемы поместится в указатель адреса. А затем считать 3 байта.
В качестве адреса HAL-овские функции просят адрес устройства (взятый из описания) предварительно сдвинуть влево. Об этом указано в описании функций библиотеки HAL. «User manual. Description of STM32F7 HAL and low-layer drivers. UM1905» https://www.st.com/resource/en/user_manual/DM00189702-.pdf (стр. 493) «DevAddress: Target device address: The device 7 bits address value in datasheet must be shifted to the left before calling the interface». Поэтому к 7битному адресу просто добавляем ноль.
txData[0] = 5; // 1
while (1)
{
HAL_I2C_Master_Transmit(&hi2c1, 0B10100000, txData, 1, 100);
HAL_I2C_Master_Receive (&hi2c1, 0B10100000, rxData, 3, 100);
HAL_Delay(1000);
}
В результате работы получаем такую осциллограмму на шине i2c. Левая часть это работы функции HAL_I2C_Master_Transmit,правая – HAL_I2C_Master_Receive.

Рассмотрим их поближе.
-
HAL_I2C_Master_Transmit

Посылка начинается со стартового бита. Затем идут 7 бит адреса (1010000), затем бит r/w (лог 0) (его формирует функция HAL_I2C_Master_Transmit). Затем девятым битом микросхема подтверждает свое присутствие, прижимая SDA к нулю. Мастер, понимая, что устройство отвечает, продолжает посылку, отправляя в шину число 5 (0b00000101). Снова девятым битом устройство прижимает линию SDA к нулю. Так как в функции мы отправляли всего одно число (4й аргумент), то посылка завершается стоповым битом.
-
HAL_I2C_Master_Receive

Посылка начитается со стартового бита. Затем идут 7 бит адреса (1010000), затем бит R/W (лог 1) (его формирует функция HAL_I2C_Master_Receive). Затем девятым битом микросхема подтверждает свое присутствие, прижимая SDA к нулю. Мастер, понимая, что устройство отвечает, продолжает генерировать тактовые импульсы на линии SCL. Но теперь линией SDA будет управлять slave-устройство, то есть в нашем случае микросхема EEPROM. Именно она формирует данные (в нашем случае нули, так как в микросхему действительно записаны нули). Теперь master их считывает и выставляет девятый бит в ноль или единицу. Так как в функции HAL_I2C_Master_Receive мы указали считать три байта (4й аргумент функции), то после получения третьего байта, master выставляет 9й бит в лог 1, тем самым сообщая slave-устройству что больше от него не требуется выдавать данные, и заканчивает посылку выставляя стоповый бит. Таким образом, функция Receive (прием) сперва фактически делает обращение (запись) к устройству, а затем уже чтение из устройства.
А давайте посмотрим что будет если указать неверный адрес
HAL_I2C_Master_Transmit(&hi2c1, 0B10010000, txData, 1, 100);
HAL_I2C_Master_Receive (&hi2c1, 0B10110000, rxData, 3, 100);

Рассмотрим поближе
-
HAL_I2C_Master_Transmit

-
HAL_I2C_Master_Receive

Как видим в обоих случаях указан неверный адрес и slave-устройство не откликнулось, не прижало линию SDA к нулю на 9м бите. И в обоих случаях текущая посылка прерывается мастером выдачей стопового бита.
Теперь давайте считаем какие-нибудь данные отличные от нуля.Но для этого их надо туда записать.
Для записи данных пишем такой код.
void erase_eeprom (){
uint8_t txData[9] ={0};
for (uint8_t j=0; j<8; j++){
for (uint8_t i=0; i<8; i++) {txData[i+1] = txData[0] + i;}
HAL_I2C_Master_Transmit(&hi2c1, 0xA0, txData, 9, 100);
HAL_Delay(11);
txData[0] += 8;
}
}
Данный код запишет в ячейки памяти число равное адресу этой ячейки. Пишем постранично, режим 4.2.PageWrite (см. описание на микросхему), по 8 байт за один раз. После того как отправили 8 байт данных в микросхему, они там размещаются во внутреннем буфере и после стопового бита происходит процесс записи непосредственно в ячейки памяти. Процесс записи это «долгий» процесс. Занимает 10 мс (см. описание на микросхему), поэтому делаем паузу между записями. Отправили 8 байт, подождали 11 мс. Отправили еще 8 байт, снова ждем 11 мс. И так 8 раз.

На картинке 9й «импульс» это уже чтение из первой части статьи (см Рисунок 1).
Раскроем покрупнее первую посылку записи.

1 – обращение к slave-устройству с адресом 1010000 в режиме записи (бит R/W̅ в лог. 0).
2 – установка адреса 0 (с адреса 0 будет далее писать 8 байт)
3 – запись в текущий адрес числа 0
… после занесения числа внутренний указатель адреса увеличивается на единицу, поэтому следующая запись будет по адресу 1
4 – запись в текущий адрес (1) числа 1
...
10 – запись в текущий адрес (7) числа 7
Раскроем покрупнее запись числа 5 (шаг 8 на рисунке 8)

Посмотрим на запись числа 20 в ячейку 20 – (третий "импульс" на рисунке 7)



1 – обращение к slave-устройству с адресом 1010000 в режиме записи (бит R/W̅ в лог. 0).
2 – установка адреса 16 (с адреса 16 будет далее писать 8 байт)
7 – запись в текущий адрес (20) числа 20
Теперь в EEPROM сохранили данные. Самое время их считать.

На осциллограмме результат работы функции HAL_I2C_Master_Receive (&hi2c1, 0B10100000, rxData, 3, 100);
Видим как устанавливается адрес slave-устройства 1010000, бит R/W̅ устанавливается в лог.1. И затем идут три числа считанные из EEPROM – 5, 6, 7.
На этом пока всё. Также планирую изучить (и написать статью) работу с шиной i2c в режиме прерываний и DMA.
Буду рад критике.
Автор: 5erG0