В последнее время все чаще натыкаюсь на негативные отзывы о шине I2C
у STM32
, мол работа с ней это танцы с бубном и тд.
За последний месяц мне удалось запустить две микросхемы, работающие по I2C
и ни каких танцев, только вдумчивое чтение даташита.
Модуль I2C у STM32 обладает следующими особенностями:
По умолчанию модуль находится в режиме Slave
, но он автоматически переключается в режим Master
после генерации состояния START
.
Принципиальное отличие между Master и Slave, в том, что Master
генерирует тактовый сигнал и всегда инициирует передачу данных и заканчивает её
. Slave
же, откликается на свой адрес и широковещательный
, при чем отклик на широковещательный адрес можно отключить. Также Slave
генерирует состояние
ACK
, но его тоже можно отключить.
Такое подробное разъяснение необходимо потому, что в обоих режимах устройство может выступать как передатчиком, так и приемником.
Ниже показана структура модуля I2C.
Регистр управления I2C_CR1:
SWRST (Software reset) - единица в этом бите сбрасывает значение всех регистров модуля в дефолтное стояние, может использоваться для сброса при возникновении ошибки.
ALERT (SMBus alert) - установка единицы в этот бит разрешает генерировать сигнал alert в режиме SMBus .
PEC (Packet error checking) - управление этим битом производится программно, но он может быть сброшен аппаратно когда передается PEC, START, STOP или PE=0. Единица в этом бите разрешает передачу CRC .
POS (Acknowledge/PEC Position (for data reception)) - состояние этого бита определяет положение ACK /PEC в двух байтовой конфигурации в режиме Master.
ACK (Acknowledge enable) - единица в этом бите разрешает отправлять ACK /NACK после приема байта адреса или данных.
STOP (Stop generation) - установка единицы в этот бит генерирует сигнал STOP в режиме Master.
START (Start generation) - установка единицы в этот бит генерирует состояние START в режиме Master,
NOSTRETCH (Clock stretching disable (Slave mode)) - если на обработку данных требуется время Slave может остановить передачу мастера, прижав линию SCL к земле, Master будет ждать и не будет ни чего слать, пока линия не будет отпущена. Ноль в этом бите прижимает SCL к земле.
ENGC (General call enable) - если в этом бите установлена единица, модуль отвечает ACK ом на широковещательный адрес 0х00.
ENPEC (PEC enable) - установка единицы в этот бит включает аппаратный подсчет CRC .
ENARP (ARP enable) - установка единицы в этот бит включает ARP .
SMBTYPE (SMBus type) - если в этом бите установлен ноль модуль работает в режиме Slave, если единица в режиме Master.
SMBUS (SMBus mode) - если в этом бите установлен ноль модуль работает в режиме I2C , если единица SMBus .
PE (Peripheral enable) - единица в этом бите включает модуль.
Регистр управления I2C_CR2:
LAST (DMA last transfer) - единица в этом бите разрешает DMA генерировать сигнал окончания передачи EOT (End of Transfer).
DMAEN (DMA requests enable) - единица в этом бите разрешает делать запрос к DMA при установке флагов TxE или RxNE .
ITBUFEN (Buffer interrupt enable) - если этот бит сброшен, разрешены все прерывания, кроме прерываний по приему и передаче.
ITEVTEN (Event interrupt enable) - единица в этом бите разрешает прерывания по событию.
ITERREN (Error interrupt enable) - единица в этом бите разрешает прерывания при возникновении ошибок.
FREQ (Peripheral clock frequency) - в это битовое битовое поле необходимо записать частоту тактирования модуля, она может принимать значение от 2 до 50.
Регистр I2C_OAR1:
ADDMODE (Addressing mode) - этот бит определяет размер адреса Slave, ноль соответствует размеру адреса 7 бит, единица - 10 бит.
ADD (Interface address) - старшие биты адреса, в случае если адрес 10-битный.
ADD (Interface address) - адрес устройства.
ADD0 (Interface address) - младший бит адреса, в случае если адрес 10-битный..
Регистр I2C_OAR2:
ADD2 - альтернативный адрес на который будет отзываться Slave.
ENDUAL (Dual addressing mode enable) - единица в этом бите разрешает Slave отзываться на альтернативный адрес в 7-битном режиме.
I2C_DR - регистр данных, для отправки данных пишем в регистр DR , для приёма читаем его же.
Регистр статуса I2C_SR1:
SMBALERT (SMBus alert) - возникает в случае alert в шине SMBus .
TIMEOUT (Timeout or Tlow error) - возникает если линия SCL прижата к земле. Для master 10mS, для slave 25mS.
PECERR (PEC Error in reception) - возникает при ошибке PEC при приеме.
OVR (Overrun/Underrun) - возникает при переполнении данных.
AF (Acknowledge failure) - устанавливается при получении сигнала NACK . Для сброса нужно записать 0.
ARLO (Arbitration lost (master mode)) - устанавливается при потере арбитража. Для сброса нужно записать 0.
BERR (Bus error) - ошибка шины. Устанавливается в случае возникновения сигнала START или STOP в неправильный момент.
TxE (Data register empty (transmitters)) - устанавливается при опустошении регистра DR, а точнее когда данные из него были перемещены в сдвиговый регистр.
RxNE (Data register not empty (receivers)) - устанавливается при приеме байта данных, кроме адреса.
STOPF (Stop detection (slave mode)) - при работе в режиме slave устанавливается при обнаружении сигнала STOP , если перед этим был сигнал ACK. Для сброса необходимо прочитать SR1 и произвести запись в CR1 .
ADD10 (10-bit header sent (Master mode)) - устанавливается при отправке первого байта 10-битного адреса.
BTF (Byte transfer finished) - флаг устанавливается по окончании приема/передачи байта, работает только при NOSTRETCH равном нулю.
ADDR (Address sent (master mode)/matched (slave mode)) - в режиме master устанавливается после передачи адреса, в режиме slave устанавливается при совпадении адреса. Для сброса нужно прочитать регистр SR1, а затем SR2.
SB (Start bit (Master mode)) - устанавливается при возникновении сигнала START. Для сброса флага необходимо прочитать SR1 и записать данные в регистр DR .
Регистр статуса I2C_SR2:
PEC (Packet error checking register) - в это битовое поле записывается контрольная сумма кадра.
DUALF (Dual flag (Slave mode)) - ноль в этом бите говорит о том, что адрес который принял Slave соответствует OAR1 , иначе OAR2 .
SMBHOST (SMBus host header (Slave mode)) - устанавливается, когда принят заголовок SMBus Host .
SMBDEFAULT
(SMBus device default address (Slave mode)) - устанавливается, если принят адрес по умолчанию
для SMBus
-устройства.
GENCALL (General call address (Slave mode)) - устанавливается, если принят широковещательный адрес в режиме ведомого.
TRA (Transmitter/receiver) - единица в этом бите говорит о том, что модуль работает как передатчик, иначе приемник.
BUSY (Bus busy) - флаг занятости.
MSL (Master/slave) - единица в этом бите говорит о том, что модуль работает в режиме Master, иначе Slave.
Регистр управления частотой I2C_CCR:
F/S (I2C master mode selection) - при установке единицы в этот бит модуль работает в режиме FAST , иначе STANDART .
DUTY (Fm mode duty cycle) - этот бит задает скважность сигнала SCL в режиме FAST . Если установлен ноль tlow/thigh = 2, иначе tlow/thigh = 16/9.
CCR (Clock control register in Fm/Sm mode (Master mode)) - при работе в режиме Master задает тактовую частоту линии SCL.
Sm mode or SMBus
:
Thigh = CCR * TPCLK1
Tlow = CCR * TPCLK1
Fm mode
:
If DUTY = 0:
Thigh = CCR * TPCLK1
Tlow = 2 * CCR * TPCLK1
If DUTY = 1: (to reach 400 kHz)
Thigh = 9 * CCR * TPCLK1
Tlow = 16 * CCR * TPCLK1
Получаем для режима SM
следующее:
CCR * TPCLK1 + CCR * TPCLK1 = 10 000ns
CCR = 10 000/(2* TPCLK1)
Регистр I2C_TRISE:
TRISE
- определяет время нарастания фронта. Рассчитывается по формуле (Tr max/TPCLK1)+1
,
где Tr max
для SM
составляет 1000nS
, а для FM 300nS
,
а TPCLK1
- период который рассчитывается как 1/F
(APB1).
Регистр управления фильтрами I2C_FLTR:
ANOFF (Analog noise filter OFF) - ноль в этом бите включает аналоговый фильтр.
DNF (Digital noise filter) - битовое поле для настройки цифрового фильтра. За подробностями нужно обратиться к документации.
Инициализация модуля из рабочего проекта.
void I2C2_Init(void)
{
/*
SDL -> PB10
SDA -> PB11
RST -> PE15
*/
//включаем тактирование портов и модуля I2C
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN | RCC_AHB1ENR_GPIOEEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;
//альтернативная ф-ция, выход с открытым стоком, 2 MHz
GPIOB->AFR |= (0x04<<2*4);
GPIOB->AFR |= (0x04<<3*4);
GPIOB->MODER |= GPIO_MODER_MODER10_1;
GPIOB->OTYPER |= GPIO_OTYPER_OT_10;
GPIOB->OSPEEDR &= ~GPIO_OSPEEDER_OSPEEDR10;
GPIOB->MODER |= GPIO_MODER_MODER11_1;
GPIOB->OTYPER |= GPIO_OTYPER_OT_11;
GPIOB->OSPEEDR &= ~GPIO_OSPEEDER_OSPEEDR11;
//PE15 двухтактный выход 50MHz
GPIOE->MODER |= GPIO_MODER_MODER15_0;
GPIOE->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR15;
AU_RST_HIGH
//настраиваем модуль в режим I2C
I2C2->CR1 &= ~2C_CR1_SMBUS;
//указываем частоту тактирования модуля
I2C2->CR2 &= ~I2C_CR2_FREQ;
I2C2->CR2 |= 42; // Fclk1=168/4=42MHz
//конфигурируем I2C, standart mode, 100 KHz duty cycle 1/2
I2C2->CCR &= ~(I2C_CCR_FS | I2C_CCR_DUTY);
//задаем частоту работы модуля SCL по формуле 10 000nS/(2* TPCLK1)
I2C2->CCR |= 208; //10 000ns/48ns = 208
//Standart_Mode = 1000nS, Fast_Mode = 300nS, 1/42MHz = 24nS
I2C2->TRISE = 42; //(1000nS/24nS)+1
//включаем модуль
I2C2->CR1 |= I2C_CR1_PE;
}
void I2C_Write(uint8_t reg_addr, uint8_t data)
{
//стартуем
I2C2->CR1 |= I2C_CR1_START;
while(!(I2C2->SR1 & I2C_SR1_SB)){};
(void) I2C2->SR1;
//передаем адрес устройства
I2C2->DR = I2C_ADDRESS(ADDR,I2C_MODE_WRITE);
while(!(I2C2->SR1 & I2C_SR1_ADDR)){};
(void) I2C2->SR1;
(void) I2C2->SR2;
//передаем адрес регистра
I2C2->DR = reg_addr;
while(!(I2C2->SR1 & I2C_SR1_TXE)){};
//пишем данные
I2C2->DR = data;
while(!(I2C2->SR1 & I2C_SR1_BTF)){};
I2C2->CR1 |= I2C_CR1_STOP;
}
uint8_t I2C_Read(uint8_t reg_addr)
{
uint8_t data;
//стартуем
I2C2->CR1 |= I2C_CR1_START;
while(!(I2C2->SR1 & I2C_SR1_SB)){};
(void) I2C2->SR1;
//передаем адрес устройства
I2C2->DR = I2C_ADDRESS(ADR,I2C_MODE_WRITE);
while(!(I2C2->SR1 & I2C_SR1_ADDR)){};
(void) I2C2->SR1;
(void) I2C2->SR2;
//передаем адрес регистра
I2C2->DR = reg_addr;
while(!(I2C2->SR1 & I2C_SR1_TXE)){};
I2C2->CR1 |= I2C_CR1_STOP;
//рестарт!!!
I2C2->CR1 |= I2C_CR1_START;
while(!(I2C2->SR1 & I2C_SR1_SB)){};
(void) I2C2->SR1;
//передаем адрес устройства, но теперь для чтения
I2C2->DR = I2C_ADDRESS(ADR,I2C_MODE_READ);
while(!(I2C2->SR1 & I2C_SR1_ADDR)){};
(void) I2C2->SR1;
(void) I2C2->SR2;
//читаем
I2C2->CR1 &= ~I2C_CR1_ACK;
while(!(I2C2->SR1 & I2C_SR1_RXNE)){};
data = I2C2->DR;
I2C2->CR1 |= I2C_CR1_STOP;
return data;
}
Кто-то любит пирожки, а кто-то - нет.
Интерфейс i2c широко распространён и используется. В stm32f4 модулей, реализующих данный протокол, аж целых три штуки.
Естественно, с полной поддержкой всего этого дела.
Работа с модулем, в целом, такая же, как и в других контроллерах: даёшь ему команды, он их выполняет и отчитывается о результате:
Я> Шли START.
S> Ок, послал.
Я> Круто, шли адрес теперь. Вот такой: 0xXX.
S> Ок, послал. Мне сказали, что ACK. Давай дальше.
Я> Жив ещё, хорошо. Вот тебе номер регистра: 0xYY, - шли.
S> Послал, получил ACK.
Я> Шли ему теперь данные, вот тебе байт: 0xZZ.
S> Послал, он согласен на большее: ACK.
Я> Фиг ему, а не ещё. Шли STOP.
S> Okay.
И всё примерно в таком духе.
В данном контроллере выводы i2c раскиданы по портам таким образом:
PB6: I2C1_SCL
PB7: I2C1_SDA
PB8: I2C1_SCL
PB9: I2C1_SDA
PB10: I2C2_SCL
PB11: I2C2_SDA
PA8: I2C3_SCL
PC9: I2C3_SDA
Вообще, распиновку периферии удобно смотреть в на 59 странице.
Что удивительно, но для работы с i2c нужны все его регистры, благо их немного:
I2C_CR1
- команды модулю для отправки команд/состояний и выбор режимов работы;
I2C_CR2
- настройка DMA и указание рабочей частоты модуля (2-42 МГц);
I2C_OAR1
- настройка адреса устройства (для slave), размер адреса (7 или 10 бит);
I2C_OAR2
- настройка адреса устройства (если адресов два);
I2C_DR
- регистр данных;
I2C_SR1
- регистр состояния модуля;
I2C_SR2
- регистр статуса (slave, должен читаться, если установлен флаги ADDR или STOPF в SR1);
I2C_CCR
- настройка скорости интерфейса;
I2C_TRISE
- настройка таймингов фронтов.
Впрочем, половина из них типа «записать и забыть».
На плате STM32F4-Discovery уже есть I2C устройство, с коим можно попрактиковаться: CS43L22 , аудиоЦАП. Он подключён к выводам PB6/PB9. Главное, не забыть подать высокий уровень на вывод PD4 (там сидит ~RESET), иначе ЦАП не станет отвечать.
Порядок настройки примерно таков:
1
. Разрешить тактирование портов и самого модуля.
Нам нужны выводы PB6/PB9, потому надо установить бит 1 (GPIOBEN) в регистре RCC_AHB1ENR, чтоб порт завёлся.
И установить бит 21 (I2C1EN) в регистре RCC_APB1ENR, чтоб включить модуль I2C. Для второго и третьего модуля номера битов 22 и 23 соответственно.
2
. Дальше настраиваются выводы: выход Oped Drain (GPIO->OTYPER), режим альтернативной функции (GPIO->MODER), и номер альтренативной функции (GPIO->AFR).
По желанию можно настроить подтяжку (GPIO->PUPDR), если её нет на плате (а подтяжка к питанию обеих линий необходима в любом виде). Номер для I2C всегда один и тот же: 4. Приятно, что для каждого типа периферии заведён отдельный номер.
3
. Указывается текущая частота тактирования периферии Fpclk1 (выраженная в МГц) в регистре CR2. Я так понял, это нужно для расчёта разных таймингов протокола.
Кстати, она должна быть не менее двух для обычного режима и не менее четырёх для быстрого. А если нужна полная скорость в 400 кГц, то она ещё и должна делиться на 10 (10, 20, 30, 40 МГц).
Максимально разрешённая частота тактирования: 42 МГц.
4
. Настраивается скорость интерфейса в регистре CCR, выбирается режим (обычный/быстрый).
Cмысл таков: Tsck = CCR * 2 * Tpckl1, т.е. период SCK пропорционален CCR (для быстрого режима всё несколько хитрее, но в RM расписано).
5
. Настраивается максимальное время нарастания фронта в регистре TRISE. Для стандартного режима это время 1 мкс. В регистр надо записать количество тактов шины, укладывающихся в это время, плюс один:
если такт Tpclk1 длится 125 нс, то записываем (1000 нс / 125 нс) + 1 = 8 + 1 = 9.
6
. По желанию разрешается генерация сигналов прерывания (ошибки, состояние и данных);
7
. Модуль включается: флаг PE в регистре CR1 переводится в 1.
Дальше модуль работает уже как надо. Надо только реализовать правильный порядок команд и проверки результатов. Например, запись регистра:
1
. Сначала нужно отправить START, установив флаг с таким именем в регистре CR1. Если всё ок, то спустя некоторое время выставится флаг SB в регистре SR1.
Хочу заметить один момент, - если нет подтяжки на линии (и они в 0), то этот флаг можно не дождаться вовсе.
2
. Если флаг-таки дождались, то отправляем адрес. Для семибитного адреса просто записываем его в DR прям в таком виде, как он будет на линии (7 бит адреса + бит направления). Для десятибитного более сложный алгоритм.
Если устройство ответит на адрес ACK"ом, то в регистре SR1 появится флаг ADDR. Если нет, то флаг AF (Acknowledge failure).
Если ADDR появился, надо прочитать регистр SR2. Можно ничего там и не смотреть, просто последовательное чтение SR1 и SR2 сбрасывает этот флаг. А пока флаг установлен, SCL удерживается мастером в низком состоянии, что полезно, если надо попросить удалённое устройство подождать с отправкой данных.
Если всё ок, то дальше модуль перейдёт в режим приёма или передачи данных в зависимости от младшего бита отправленного адреса. Для записи он должен быть нулём, для чтения - единицей.
но мы рассматриваем запись, потому примем, что там был ноль.
3
. Дальше отправляем адрес регистра, который нас интересует. Точно так же, записав его в DR. После передачи выставится флаг TXE (буфер передачи пуст) и BTF (передача завершена).
4
. Дальше идут данные, которые можно отправлять, пока устройство отвечает ACK. Если ответом будет NACK, то эти флаги не установятся.
5
. По завершении передачи (или в случае непредвиденного состояния) отправляем STOP: устанавливается одноимённый флаг в регистре CR1.
При чтении всё то же самое. Меняется только после записи адреса регистра.
Вместо записи данных идёт повторная отправка START (повторный старт) и отправка адреса с установленным младшим битом (признак чтения).
Модуль будет ждать данных от устройства. Чтобы поощрать его к отправке следующих байт, надо перед приёмом установить флаг ACK в CR1 (чтобы после приёма модуль посылал этот самый ACK).
Как надоест, флаг снимаем, устройство увидит NACK и замолчит. После чего шлём STOP обычным порядком и радуемся принятым данным.
Вот то же самое в виде кода:
// Инициализация модуля
void i2c_Init(void)
{
uint32_t Clock = 16000000UL; // Частота тактирования модуля (system_stm32f4xx.c не используется)
uint32_t Speed = 100000UL; // 100 кГц
// Включить тактирование порта GPIOB
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
// Настроим выводы PB6, PB9
// Open drain!
GPIOB->OTYPER |= GPIO_OTYPER_OT_6 | GPIO_OTYPER_OT_9;
// Подтяжка внешняя, потому тут не настраивается!
// если надо, см. регистр GPIOB->PUPDR
// Номер альтернативной функции
GPIOB->AFR &= ~(0x0FUL << (6 * 4)); // 6 очистим
GPIOB->AFR |= (0x04UL << (6 * 4)); // В 6 запишем 4
GPIOB->AFR &= ~(0x0FUL << ((9 - 8) * 4)); // 9 очистим
GPIOB->AFR |= (0x04UL << ((9 - 8) * 4)); // В 9 запишем 4
// Режим: альтернативная функция
GPIOB->MODER &= ~((0x03UL << (6 * 2)) | (0x03UL << (9 * 2))); // 6, 9 очистим
GPIOB->MODER |= ((0x02UL << (6 * 2)) | (0x02UL << (9 * 2))); // В 6, 9 запишем 2
// Включить тактирование модуля I2C1
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
// На данный момент I2C должен быть выключен
// Сбросим всё (SWRST == 1, сброс)
I2C1->CR1 = I2C_CR1_SWRST;
// PE == 0, это главное
I2C1->CR1 = 0;
// Считаем, что запущены от RC (16 МГц)
// Предделителей в системе тактирования нет (все 1)
// По-хорошему, надо бы вычислять это вс из
// реальной частоты тактирования модуля
I2C1->CR2 = Clock / 1000000UL; // 16 МГц
// Настраиваем частоту
{
// Tclk = (1 / Fperiph);
// Thigh = Tclk * CCR;
// Tlow = Thigh;
// Fi2c = 1 / CCR * 2;
// CCR = Fperiph / (Fi2c * 2);
uint16_t Value = (uint16_t)(Clock / (Speed * 2));
// Минимальное значение: 4
if(Value < 4) Value = 4;
I2C1->CCR = Value;
}
// Задаём предельное время фронта
// В стандартном режиме это время 1000 нс
// Просто прибавляем к частоте, выраженной в МГц единицу (см. RM стр. 604).
I2C1->TRISE = (Clock / 1000000UL) + 1;
// Включим модуль
I2C1->CR1 |= (I2C_CR1_PE);
// Теперь можно что-нибудь делать
}
// Отправить байт
bool i2c_SendByte(uint8_t Address, uint8_t Register, uint8_t Data)
{
if(!i2c_SendStart()) return false;
// Адрес микросхемы
if(!i2c_SendAddress(Address)) return i2c_SendStop();
// Адрес регистра
if(!i2c_SendData(Register)) return i2c_SendStop();
// Данные
if(!i2c_SendData(Data)) return i2c_SendStop();
// Стоп!
i2c_SendStop();
return true;
}
// Получить байт
bool i2c_ReceiveByte(uint8_t Address, uint8_t Register, uint8_t * Data)
{
if(!i2c_SendStart()) return false;
// Адрес микросхемы
if(!i2c_SendAddress(Address)) return i2c_SendStop();
// Адрес регистра
if(!i2c_SendData(Register)) return i2c_SendStop();
// Повторный старт
if(!i2c_SendStart()) return false;
// Адрес микросхемы (чтение)
if(!i2c_SendAddress(Address | 1)) return i2c_SendStop();
// Получим байт
if(!i2c_ReceiveData(Data)) return i2c_SendStop();
// Стоп!
i2c_SendStop();
return true;
}
Использование:
{
uint8_t ID = 0;
i2c_Init();
// Считаем, что PD4 выставлен в высокий уровень и ЦАП работает (это надо сделать как-нибудь)
// Отправка байта в устройство с адресом 0x94, в регистр 0x00 со значением 0x00.
i2c_SendByte(0x94, 0x00, 0x00);
// Приём байта из устройства с адресом 0x94 из регистра 0x01 (ID) в переменную buffer
i2c_ReceiveByte(0x94, 0x01, &ID);
}
Конечно, кроме как в учебном примере так делать нельзя. Ожидание окончания действия слишком уж долгое для такого быстрого контроллера.
Шина I2C существует уже достаточно давно: ее в 1980х создала компания Philips для низкоскоростных устройств. В настоящий момент она достаточно широко применяется, и, скорей всего, дома у вас есть хоть одно устройство с данной шиной. Название шины расшифровывается как Inter-Integrated Circuit. Хардварным модулем I2C в настоящее время обладает большинство микроконтроллеров, в некоторых их и вовсе несколько, как у тех же STM32 (в серии F4 есть целых три модуля I2C).
Шина представляет собой 2 линии, одна из которых данные (SDA), другая - синхросигнал (SCL), обе линии изначально притянуты к питанию. Следует отметить, что четкого указания какое именно должно быть напряжение нет, но чаще всего используется +5В и +3.3В. Устройства в линии не одноранговые, как в CAN, поэтому всегда должно быть Master-устройство. Допускается наличие нескольких Master-устройств, но это все же гораздо реже, чем один Master и ворох Slave устройств.
Передача данных инициируется мастером, который отправляет в шину адрес необходимого устройства, тактирование осуществляется так же мастером. Но, при этом, Slave-устройство имеет возможность «придержать» линию тактирования, как бы сообщая Master-устройству, что не успевает принять или отправить данные, что порой бывает очень полезно. Наибольшее распространение получили в текущий вариант реализации I2C с частотой шины 100 kHz (Standard mode) и 400 kHz (Fast mode).
Существует реализация I2C версии 2.0, которая позволяет достичь гораздо больших скоростей, в 2-3 Мбит/с, но они пока что весьма редкие. Так же у линии есть ограничение по емкости в 400 пФ. Обычно в даташитах для датчиков и прочих I2C устройств указывается их емкость, так что приблизительно можно вычислить «влезет» ли еще один датчик или нет.
В микроконтроллерах очень часто есть внутренняя подтяжка на выводах, что в свободном состоянии даст необходимые +3.3В (или +5В) на линии, но этой подтяжки абсолютно не хватит на нормальную линию. Поэтому всегда стоит делать внешнюю подтяжку и SCL и SDA к питанию резисторами в 4.7кОм..2кОм.
Отдельно стоит отметить то, что обычно линию I2C не рекомендуют делать длинной, да и чаще всего она встречается на печатных платах для обмена между некими цифровыми устройствами, гораздо реже I2C пускают по проводам (но не стоит думать, что это редкость, и то и другое вполне нормально). Если у вас возникла надобность сделать длинную линию I2C, да еще на 400 кГц, то стоит уменьшить сопротивление резисторов подтяжки. 1 кОм - вполне приемлемое значение для линии длиной чуть более метра и с несколькими устройствами на ней. Только не забывайте, что уменьшая сопротивление резисторов, вы увеличиваете ток в линии, что при переизбытке может привести к повреждению устройств.
С программной точки зрения обмен по шине I2C выглядит следующим образом: Master отправляет стартовую последовательность START (при высоком уровне SCL к нулю притягивается SDA), затем отправляет адрес с бит-флагом, указывающим режим чтения или записи, причем в следующим формате:
Если бит режима равен нулю, то это значит, что Master будет записывать информацию в Slave устройство, единица - чтение из Slave. Если взглянуть на это с другой стороны, то каждое I2C устройство предоставляет два «виртуальных» устройства, исходя из чего получается, что если весь байт адреса (т.е. исконные 7 бит + бит режима) четный, то это адрес записи, если нечетный - адрес чтения. Исходя из этого появляется ограничение на количество устройств в шине: 127.
После получения адреса Slave устройство должно сообщить мастеру о принятии адреса, что подтвердит сам факт существования Slave устройства с таким адресом на линии. Подтверждение - это специальный 9й бит, который равен нулю, если адрес совпал и готовы работать, и единице, если не совпал. Это сигналы ACK и NACK соответственно. Так же, ACK используется при последующим приеме и передаче данных. Если мастер записывает в слейв, то слейв должен каждый байт подтверждать сигналом ACK. Если слейв отправляет данные мастеру, то мастер должен на все байты отвечать ACK, кроме последнего - это будет сигналом, что больше отправлять данные не требуется.
В конце всей передачи Master должен отправить завершающую последовательность STOP, которая заключается в поднятии линии SDA до высокого уровня при поднятой линии SCL.
Таким образом, стандартный «пакет» выглядит следующим образом:
Теперь можно перейти к рассмотрению работы с данной шиной на микроконтроллере STM32. Сразу стоит заметить, что во всех сериях данный модуль приблизительно одинаковый, за исключением регистра фильтра в старших сериях (например, STM32F407), поэтому единожды написанный код сможет работать и далее.
Для начала следует включить тактирование модуля I2C, что, впрочем, необходимо и для всей периферии. Так же необходимо включить и настроить пины в режим альтернативной функции. Чтобы посмотреть на какой шине что находится, необходимо обратиться к даташиту, в раздел Device Overview (для F4, это, например, страница 18) (рис.1). Из изображения видно, что I2C находятся на шине APB1. Следующий шаг - включение и настройка GPIO, все, что необходимо: режим альтернативной функции (по даташиту I2C относится к AF4), тип OpenDrain, а подтяжка должна быть внешняя. «Скорость» пинов для 100кГц можно выбрать Low (2 MHz), а для 400 кГц ST рекомендуют выбирать уже Medium или Fast (от 10 MHz). И, наконец, можно настроить I2C. Показывать регистры не имеет смысла, они есть все в reference manual, все, что нужно для стандартного случая будет ниже. До включения непосредственно модуля I2C следует в регистр CR2 записать текущее значение частоты той шины, на которой сидит модуль I2C, в данном случае это частота шины APB1. В рамках даташита это значение называется PCLK.
В разных контроллерах количество и именование шин разное. Так, в серии F4xx есть и APB1 и APB2, и переменная PCLK будет соответственно нумероваться - PCLK1 и PCLK2. Чтобы посмотреть или высчитать конкретную частоту тактирования шины можно воспользоваться приложением CubeMX, которое загружается с официального сайта ST Microelectronics.
Рис. 1 - Схема периферии контроллера STM32F407.
В регистре CR2 так же включаются прерывания от данного модуля. Под этим понимается то, что будет ли модуль сообщать в NVIC о том, что что-то произошло, либо же просто поставит нужные флаги в статусном регистре. Стоит заметить, что в статусном регистре всегда будут ставиться событийные флаги, что логично. В первую очередь интересны прерывания ITEVTEN и ITERREN, прерывания событий и ошибок соответственно. Можно обойтись вполне и только событиями, как наиболее общим.
I2C1->CR2 |= 48; // Peripheral frequency 24MHz I2C1->CR2 |= I2C_CR2_ITEVTEN; // Enable events
Регистр CCR отвечает за тактирование самой шины наружу, поэтому сюда необходимо внести значение, которое рассчитывается по формуле PCLK/I2C_SPEED. Например, мы хотим шину на 400 кГц завести, внутренняя шина APB1 тактируется 48 МГц, соответственно в CCR запишем значение, равное 48*106/4*105 = 120. Так же в данном регистре необходимо указать режим работы Slow/Fast, это последний, 16й бит.
I2C1->CCR &= ~I2C_CCR_CCR; I2C1->CCR |= 120; I2C1->CCR |= I2C_CCR_FS; // FastMode, 400 kHz
Регистр TRISE отвечает за фронты сигналов на SDA и SCL, сюда необходимо внести значение с небольшим запасом. Можно и без запаса, главное не меньше - ничего не заработает. Вносимое значение рассчитывается так: TRISE = RISE/tPCLK. tPCLK = 1/PCLK. Константа RISE - это максимальное время нарастания сигнала, по спецификации это 1000 нс для Slow Mode и 300 нс для Fast mode. tPCLK - это просто период, получается стандартно по формуле 1/F. Так как у нас Fast Mode, то значение в TRISE необходимо следующее: 3.000*10-7/2.083*10-8 = 14.4, и т.к. необходим запас, то округляем в большую сторону, т.е. 15.
Данный показатель важен, но не настолько, как сбившаяся частота тактирования. Я по ошибке посчитал константу TRISE в Fast Mode по формуле для Slow Mode и все работает. Но все же лучше делать правильно, по спецификации шины. Найти ее можно по поисковой фразе “i2c specification”. Да-да, она на английском языке.
I2C1->TRISE = 24;
После того, как данные действия будут выполнены, можно включать модуль и прерывания в модуле NVIC (если нужны).
I2C1->CR1 |= I2C_CR1_PE; // Enable I2C block NVIC_EnableIRQ(I2C1_EV_IRQn); NVIC_SetPriority(I2C1_EV_IRQn, 1);
После настройки модуля, есть два варианта как вы будете с ним работать. Вариант первый - поллингом, т.е. вы будете ждать появления флага в цикле while (и контроллер будет занят только этим, что в большинстве случаев плохо), либо вариант второй - на прерываниях. Я рассмотрю второй вариант, так как он предпочтительный, а если понять логику состояний по прерываниям, то перейти на поллинг не является проблемой.
Итак, первое что необходимо сделать - добавить в код обработчик прерываний событий от модуля I2C. Функция должна называться определенным образом, и ее название можно взять из startup-файла. Для модуля I2C1 функция называется I2C1_EV_IRQHandler. Поэтому в необходимый.c файл добавляем такую функцию:
Void I2C1_EV_IRQHandler(void) { }
Именование данного метода, понятное дело, можно изменять. В startup-файле в ассемблерном коде просто создаются метки, которые не требуют обязательного наличия данной функции. Компилятор языка Си найдя в исходном файле функцию, например ту же I2C1_EV_IRQHandler, выделить под нее точно такое же имя. Когда уже будет происходить окончательная сборка, все это сведется воедино и вместо имени будет присутствовать переход в нужную позицию в коде. Поэтому можно изменять название метки как заблагорассудится (по правилам именования функций, конечно), хотя и не рекомендуется - другие разработчики могут просто не понять, обработчик прерываний это или нет.
Если вы пишете на языке С++, не забудьте «обернуть» обработчик прерываний в блок extern “C” { … }, так как компилятор С++ изменяет имя функции по своим правилам во время компиляции (туда вносится информация о параметрах и возвращаемом значении, например), поэтому сборщик потом не свяжет написанный обработчик и метку в startup файле.
Для написания обработчика прерываний можно обратиться напрямую к документации, reference manual, там есть достаточно подробные схемы для разных режимов работы. Для начала возьмем отправку slave-устройству данных:
Например, мы хотим просто отправить 1 байт данных устройству и прекратить передачу. Для этого нам потребуется только состояния EV5, EV6 и EV8. Где-то в коде программы у нас была глобальная переменная data типа uint8_t, которую мы проинициализировали каким-то значением и хотим передать slave устройству. Инициацию передачи, как мы уже знаем, делает последовательность START:
I2C1->CR1 |= I2C_CR1_START;
Данную строку можно поставить по ходу программы там, когда нужно начинать передачу. Например, после того, как инициализировали переменную data. Дальше уже будет код внутри функции-обработчика прерываний. Для начала необходимо в отдельные переменные сохранить значение статусов:
Volatile uint32_t sr1 = I2C1->SR1, sr2 = I2C1->SR2;
После отправки стартовой последовательности произойдет прерывание с событием EV5. В данном случае в статусном регистре должен выставиться бит SB. Если данный бит выставлен, нам необходимо отправить адрес с битом режима чтения или записи. Для упрощения можно сделать так:
<<1) | mode)
Теперь можно написать обработчик состояния EV5:
If(sr1 & I2C_SR1_SB) { I2C1->DR = I2C_ADDRESS(0x14,I2C_MODE_READ); }
Когда адрес отправится и slave-устройство ответит последовательностью ACK, то произойдет событие EV6 и одновременно EV8: установится флаг ADDR и TXE. А рамках Master-режима, ADDR означает, что адрес отправлен и воспринят slave-устройством, а TXE означает, что буфер свободен для внесения данных для последующей передачи. Флаг ADDR сбросится сам, как только мы прочитаем SR1 и SR2 (необходимо их оба прочитать), а флаг TXE обработаем отдельным блоком кода. Так что, по факту, обрабатывать необходимо только EV5 и EV8, EV6 только информирует о наличии нужного slave на линии. В обработчике TXE все, что нужно - это передавать данные. Так как передавать мы хотим только 1 байт, то сразу же отправим и последовательность STOP:
If(sr1 & I2C_SR1_TXE) { I2C1->DR = data; I2C1->CR1 |= I2C_CR1_STOP; }
Таким образом, заполнив переменную data и дав команду формирования последовательности START, вся работа будет идти в прерываниях, а контроллер тем временем будет занят другой полезной работой большую часть времени (т.е. другая работа кроме, собственно, обработчика прерываний).
Если данных требуется отправить больше 1 байта, то изменения в коде минимальны. Теперь вместо uint8_t data создадим такие глобальные переменные:
Uint8_t iter; uint8_t data = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Так как глобальные переменные целого типа по умолчанию равны нулю, то явно инициализировать переменную iter не требуется. Изменения в обработчике же вообще минимальные - требуется переписать блок обработки события EV8:
If(sr1 & I2C_SR1_TXE) { if(iter < 10) { I2C1->DR = data; } else { I2C1->CR1 |= I2C_CR1_STOP; } }
Таким образом, мы получили вот такую функцию-обработчик:
#define I2C_MODE_READ 1 #define I2C_MODE_WRITE 0 #define I2C_ADDRESS(addr, mode) ((addr<<1) | mode) uint8_t iter; uint8_t data = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; void I2C1_EV_IRQHandler(void) { volatile uint32_t sr1 = module ->SR1, sr2 = module ->SR2; if(sr1 & I2C_SR1_SB) { module ->DR = I2C_ADDRESS(0x14,I2C_MODE_READ); } if(sr1 & I2C_SR1_TXE) { if(iter < 10) { I2C1->DR = data; } else { I2C1->CR1 |= I2C_CR1_STOP; } } }
Код можно модифицировать и далее, создав, например, контекст модуля, сделать одну функцию-обработчик, которая будет на вход принимать только контекст и делать необходимые действия с необходимым модулем и так далее. Например, можно сделать такую функцию:
Void I2C_handler(I2C_TypeDef* module, uint8_t addr, uint8_t data) { volatile uint32_t sr1 = module ->SR1, sr2 = module ->SR2; if(sr1 & I2C_SR1_SB) { module ->DR = I2C_ADDRESS(addr,I2C_MODE_READ); } if(sr1 & I2C_SR1_TXE) { module ->DR = data; module ->CR1 |= I2C_CR1_STOP; } }
Это позволит одну и ту же функцию использовать сразу в двух модулях. Например, мы можем ее вставить вот так:
Void I2C1_EV_IRQHandler(void) { I2C_handler(I2C1, 0x14, 0x10); } void I2C1_EV_IRQHandler(void) { I2C_handler(I2C1, 0x27, 0xFF); }
Еще раз напомню, что адрес устройства, который мы видим в документации - это биты из того байта, который передается модулем, а бит 0 - это режим. Так, указав выше в аргументах адрес 0x14 и режим передачи данных, я получу на передачу байт 0x29. Так как в макросе проверки нет, стоит не забывать, что передавать в него можно максимум адрес 0x7F, иначе получите чехарду.
Для режима чтения все похоже, как можно видеть из диаграммы:
Для обработки нам нужны состояния EV5, EV6, EV7, EV7_1. Статус EV6 по-прежнему сбросится сам после чтения регистров SR1 и SR2, а статус EV7_1 соответствует последнему необходимому байту. Т.е. когда мы приняли предпоследний байт, мы должны отключить отправку сообщения ACK слейву, чтобы следующий байт уже был последним. Итак, возьмем наш предыдущий код и просто внесем в него дополнительный обработчик такого вида, чтобы принять 10 байт данных:
If(sr1 & I2C_SR1_RXNE) { if(rx_iter == 8) { I2C1->CR1 &= ~I2C_CR1_ACK; } else if (rx_iter == 9) { I2C1-> < 10) { rx_data = I2C1->DR; } }
При этом должны быть глобальные переменные:
Uint8_t rx_iter; uint8_t rx_data;
Таким образом, получили вот такой обработчик прерываний для модуля I2C1:
#define I2C_MODE_READ 1 #define I2C_MODE_WRITE 0 #define I2C_ADDRESS(addr, mode) ((addr<<1) | mode) uint8_t iter; uint8_t data = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; uint8_t rx_iter; uint8_t rx_data; uint8_t i2c_mode; void I2C1_EV_IRQHandler(void) { volatile uint32_t sr1 = I2C1->SR1, sr2 = I2C1->SR2; if(sr1 & I2C_SR1_SB) { I2C1->DR = I2C_ADDRESS(0x14,i2c_mode); } if(sr1 & I2C_SR1_TXE) { if(iter < 10) { I2C1->DR = data; } else { I2C1->CR1 |= I2C_CR1_STOP; } } if(sr1 & I2C_SR1_RXNE) { if(rx_iter == 8) { I2C1->CR1 &= ~I2C_CR1_ACK; } else if (rx_iter == 9) { I2C1->CR1 |= I2C_CR1_ACK; } if(rx_iter < 10) { rx_data = I2C1->DR; } }
Данных выше хватает, в общем-то, для очень многих случаев, и, поняв логику написания кода выше, можно по документации будет расширить код необходимым. И еще одно - часто для того, чтобы что-то прочитать из slave-устройства, необходимо в него что-то записать.
Post Views: 318
Для использования в дальнейшем понадобилось связать, используя I2C микроконтроллер STM32 с экраном 2004. Не найдя аналогичного решения в сети, публикую здесь. Данный рецепт подойдёт также для экранов 1602. Далее под катом. (Осторожно, картинки).
Игрушечная касса, купленная сыну, оказалось с дефектом, и работала через раз. Появилась идея переделать её внутренности, и момент выбора микроконтроллера совпал с публикацией статьи про STM32 . Немного прикинув и сравнив цены: STM32+LCD2004+I2C = ArduinoMega (причина была в том, что нужно было реализовать клавиатуру, динамик, устройство ввода штрих-кода и экран, поэтому каждый вывод микроконтроллера на счету) я выбрал первый набор.
Были сделаны покупки, и наступило время ожидания. Для прошивки купил ещё USB-USART переходник.
Что и где покупалось.
Первым опишу подключение. Странно, описывая использование STM32 мало где рисуют схемы, в основном код, сам догадайся, что и как подключить.
Подключение изображу на фотографии (по клику - крупнее).
Данное подключение актуально для STM32F103C8. Для других плат МК проверьте пины подключения I2C1 по даташиту.
USART переходник в USB. Тут понятно. Далее - USART подключаем к STM32 к выведенному около разъема miniUSB USART1. TX к RX и соответственно RX к TX. У меня на USART есть вывод 3v3, я от него и запитал МК. Землю я подключил отдельно, для удобного её отключения во время переключения режимов прошивки и работы. К экрану я припаял I2C (так же на ebay есть экраны с припаянными I2C). Питание для I2C и экрана берётся от 3v3 МК или 5В от USART. Ниже написал про настройку контраста при различном напряжении питании. Далее: SCL от I2C подключается к PB6, SDA от I2C к PB7. Притягивать SCL и SDA к питанию при использовании одного данного устройства нет необходимости.
Первыми граблями был USART. Его я использовал для отладки, в приведённом здесь коде строки работы с ним закомментированы. Но с ним проблему так и не решил. Такое впечатление, что нет синхронизации между компьютером и микроконтроллером до посылки первого символа. Причем если использовать код из примера - то МК прекрасно дублирует получаемый текст, а сам писать не может. Я добился наиболее приемлемого для отладки вывода строк, добавив Delay(500) после каждого символа.
Потом попытался реализовать работу с I2C. Взял код из примера , обратил внимание на комментарии про подвисание МК, проанализировав исходники, увидел что как и автору комментариев, мне необходим сдвиг адреса устройства влево:
//http://microtechnics.ru/stm32-ispolzovanie-i2c/#comment-8109
I2C_Send7bitAddress(I2Cx, slaveAddress<<1, transmissionDirection);
Вставил код и попробовал запустить. Программа повисала на моменте ожидания освобождения шины:
While(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
Тут грабли в адресе I2C устройства. Судя из описания продавца, у меня был адрес 0x20. Вот тут я и потерял 15 минут впустую, но вчитавшись в описание разных моделей I2C переходников, ссылку на которое привёл в своей статье , обратил внимание на последнюю модель и попробовал поменять адрес на 0x27. Всё заработало. Вывод такой: если у Вас на переходнике запаяны A0 A1 A2 - адрес 0x20, не запаяны - 0x27.
Сравните:
![]() |
![]() |
При 5В питания контраст нужно немного уменьшить. А при 3.3В поставить на максимум, на настройке от 5В ничего не видно. Результат представлен на первой картинке в посте. Мой оказался без русского языка, я это увидел, пролистав символы. Попробовал нарисовать кляксу, не зная, что максимум можно определить 8 своих символов, написал для кляксы 12. Подобрал похожие из китайских, вроде получилось.
Код представлен на гихабе, так как для достижения результата переписал библиотеку от Ардуины.
Кто-то любит пирожки, а кто-то - нет.
Интерфейс i2c широко распространён и используется. В stm32f4 модулей, реализующих данный протокол, аж целых три штуки.
Естественно, с полной поддержкой всего этого дела.
Работа с модулем, в целом, такая же, как и в других контроллерах: даёшь ему команды, он их выполняет и отчитывается о результате:
Я> Шли START.
S> Ок, послал.
Я> Круто, шли адрес теперь. Вот такой: 0xXX.
S> Ок, послал. Мне сказали, что ACK. Давай дальше.
Я> Жив ещё, хорошо. Вот тебе номер регистра: 0xYY, - шли.
S> Послал, получил ACK.
Я> Шли ему теперь данные, вот тебе байт: 0xZZ.
S> Послал, он согласен на большее: ACK.
Я> Фиг ему, а не ещё. Шли STOP.
S> Okay.
И всё примерно в таком духе.
В данном контроллере выводы i2c раскиданы по портам таким образом:
PB6: I2C1_SCL
PB7: I2C1_SDA
PB8: I2C1_SCL
PB9: I2C1_SDA
PB10: I2C2_SCL
PB11: I2C2_SDA
PA8: I2C3_SCL
PC9: I2C3_SDA
Вообще, распиновку периферии удобно смотреть в на 59 странице.
Что удивительно, но для работы с i2c нужны все его регистры, благо их немного:
I2C_CR1
- команды модулю для отправки команд/состояний и выбор режимов работы;
I2C_CR2
- настройка DMA и указание рабочей частоты модуля (2-42 МГц);
I2C_OAR1
- настройка адреса устройства (для slave), размер адреса (7 или 10 бит);
I2C_OAR2
- настройка адреса устройства (если адресов два);
I2C_DR
- регистр данных;
I2C_SR1
- регистр состояния модуля;
I2C_SR2
- регистр статуса (slave, должен читаться, если установлен флаги ADDR или STOPF в SR1);
I2C_CCR
- настройка скорости интерфейса;
I2C_TRISE
- настройка таймингов фронтов.
Впрочем, половина из них типа «записать и забыть».
На плате STM32F4-Discovery уже есть I2C устройство, с коим можно попрактиковаться: CS43L22 , аудиоЦАП. Он подключён к выводам PB6/PB9. Главное, не забыть подать высокий уровень на вывод PD4 (там сидит ~RESET), иначе ЦАП не станет отвечать.
Порядок настройки примерно таков:
1
. Разрешить тактирование портов и самого модуля.
Нам нужны выводы PB6/PB9, потому надо установить бит 1 (GPIOBEN) в регистре RCC_AHB1ENR, чтоб порт завёлся.
И установить бит 21 (I2C1EN) в регистре RCC_APB1ENR, чтоб включить модуль I2C. Для второго и третьего модуля номера битов 22 и 23 соответственно.
2
. Дальше настраиваются выводы: выход Oped Drain (GPIO->OTYPER), режим альтернативной функции (GPIO->MODER), и номер альтренативной функции (GPIO->AFR).
По желанию можно настроить подтяжку (GPIO->PUPDR), если её нет на плате (а подтяжка к питанию обеих линий необходима в любом виде). Номер для I2C всегда один и тот же: 4. Приятно, что для каждого типа периферии заведён отдельный номер.
3
. Указывается текущая частота тактирования периферии Fpclk1 (выраженная в МГц) в регистре CR2. Я так понял, это нужно для расчёта разных таймингов протокола.
Кстати, она должна быть не менее двух для обычного режима и не менее четырёх для быстрого. А если нужна полная скорость в 400 кГц, то она ещё и должна делиться на 10 (10, 20, 30, 40 МГц).
Максимально разрешённая частота тактирования: 42 МГц.
4
. Настраивается скорость интерфейса в регистре CCR, выбирается режим (обычный/быстрый).
Cмысл таков: Tsck = CCR * 2 * Tpckl1, т.е. период SCK пропорционален CCR (для быстрого режима всё несколько хитрее, но в RM расписано).
5
. Настраивается максимальное время нарастания фронта в регистре TRISE. Для стандартного режима это время 1 мкс. В регистр надо записать количество тактов шины, укладывающихся в это время, плюс один:
если такт Tpclk1 длится 125 нс, то записываем (1000 нс / 125 нс) + 1 = 8 + 1 = 9.
6
. По желанию разрешается генерация сигналов прерывания (ошибки, состояние и данных);
7
. Модуль включается: флаг PE в регистре CR1 переводится в 1.
Дальше модуль работает уже как надо. Надо только реализовать правильный порядок команд и проверки результатов. Например, запись регистра:
1
. Сначала нужно отправить START, установив флаг с таким именем в регистре CR1. Если всё ок, то спустя некоторое время выставится флаг SB в регистре SR1.
Хочу заметить один момент, - если нет подтяжки на линии (и они в 0), то этот флаг можно не дождаться вовсе.
2
. Если флаг-таки дождались, то отправляем адрес. Для семибитного адреса просто записываем его в DR прям в таком виде, как он будет на линии (7 бит адреса + бит направления). Для десятибитного более сложный алгоритм.
Если устройство ответит на адрес ACK"ом, то в регистре SR1 появится флаг ADDR. Если нет, то флаг AF (Acknowledge failure).
Если ADDR появился, надо прочитать регистр SR2. Можно ничего там и не смотреть, просто последовательное чтение SR1 и SR2 сбрасывает этот флаг. А пока флаг установлен, SCL удерживается мастером в низком состоянии, что полезно, если надо попросить удалённое устройство подождать с отправкой данных.
Если всё ок, то дальше модуль перейдёт в режим приёма или передачи данных в зависимости от младшего бита отправленного адреса. Для записи он должен быть нулём, для чтения - единицей.
но мы рассматриваем запись, потому примем, что там был ноль.
3
. Дальше отправляем адрес регистра, который нас интересует. Точно так же, записав его в DR. После передачи выставится флаг TXE (буфер передачи пуст) и BTF (передача завершена).
4
. Дальше идут данные, которые можно отправлять, пока устройство отвечает ACK. Если ответом будет NACK, то эти флаги не установятся.
5
. По завершении передачи (или в случае непредвиденного состояния) отправляем STOP: устанавливается одноимённый флаг в регистре CR1.
При чтении всё то же самое. Меняется только после записи адреса регистра.
Вместо записи данных идёт повторная отправка START (повторный старт) и отправка адреса с установленным младшим битом (признак чтения).
Модуль будет ждать данных от устройства. Чтобы поощрать его к отправке следующих байт, надо перед приёмом установить флаг ACK в CR1 (чтобы после приёма модуль посылал этот самый ACK).
Как надоест, флаг снимаем, устройство увидит NACK и замолчит. После чего шлём STOP обычным порядком и радуемся принятым данным.
Вот то же самое в виде кода:
// Инициализация модуля
void i2c_Init(void)
{
uint32_t Clock = 16000000UL; // Частота тактирования модуля (system_stm32f4xx.c не используется)
uint32_t Speed = 100000UL; // 100 кГц
// Включить тактирование порта GPIOB
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
// Настроим выводы PB6, PB9
// Open drain!
GPIOB->OTYPER |= GPIO_OTYPER_OT_6 | GPIO_OTYPER_OT_9;
// Подтяжка внешняя, потому тут не настраивается!
// если надо, см. регистр GPIOB->PUPDR
// Номер альтернативной функции
GPIOB->AFR &= ~(0x0FUL << (6 * 4)); // 6 очистим
GPIOB->AFR |= (0x04UL << (6 * 4)); // В 6 запишем 4
GPIOB->AFR &= ~(0x0FUL << ((9 - 8) * 4)); // 9 очистим
GPIOB->AFR |= (0x04UL << ((9 - 8) * 4)); // В 9 запишем 4
// Режим: альтернативная функция
GPIOB->MODER &= ~((0x03UL << (6 * 2)) | (0x03UL << (9 * 2))); // 6, 9 очистим
GPIOB->MODER |= ((0x02UL << (6 * 2)) | (0x02UL << (9 * 2))); // В 6, 9 запишем 2
// Включить тактирование модуля I2C1
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
// На данный момент I2C должен быть выключен
// Сбросим всё (SWRST == 1, сброс)
I2C1->CR1 = I2C_CR1_SWRST;
// PE == 0, это главное
I2C1->CR1 = 0;
// Считаем, что запущены от RC (16 МГц)
// Предделителей в системе тактирования нет (все 1)
// По-хорошему, надо бы вычислять это вс из
// реальной частоты тактирования модуля
I2C1->CR2 = Clock / 1000000UL; // 16 МГц
// Настраиваем частоту
{
// Tclk = (1 / Fperiph);
// Thigh = Tclk * CCR;
// Tlow = Thigh;
// Fi2c = 1 / CCR * 2;
// CCR = Fperiph / (Fi2c * 2);
uint16_t Value = (uint16_t)(Clock / (Speed * 2));
// Минимальное значение: 4
if(Value < 4) Value = 4;
I2C1->CCR = Value;
}
// Задаём предельное время фронта
// В стандартном режиме это время 1000 нс
// Просто прибавляем к частоте, выраженной в МГц единицу (см. RM стр. 604).
I2C1->TRISE = (Clock / 1000000UL) + 1;
// Включим модуль
I2C1->CR1 |= (I2C_CR1_PE);
// Теперь можно что-нибудь делать
}
// Отправить байт
bool i2c_SendByte(uint8_t Address, uint8_t Register, uint8_t Data)
{
if(!i2c_SendStart()) return false;
// Адрес микросхемы
if(!i2c_SendAddress(Address)) return i2c_SendStop();
// Адрес регистра
if(!i2c_SendData(Register)) return i2c_SendStop();
// Данные
if(!i2c_SendData(Data)) return i2c_SendStop();
// Стоп!
i2c_SendStop();
return true;
}
// Получить байт
bool i2c_ReceiveByte(uint8_t Address, uint8_t Register, uint8_t * Data)
{
if(!i2c_SendStart()) return false;
// Адрес микросхемы
if(!i2c_SendAddress(Address)) return i2c_SendStop();
// Адрес регистра
if(!i2c_SendData(Register)) return i2c_SendStop();
// Повторный старт
if(!i2c_SendStart()) return false;
// Адрес микросхемы (чтение)
if(!i2c_SendAddress(Address | 1)) return i2c_SendStop();
// Получим байт
if(!i2c_ReceiveData(Data)) return i2c_SendStop();
// Стоп!
i2c_SendStop();
return true;
}
Использование:
{
uint8_t ID = 0;
i2c_Init();
// Считаем, что PD4 выставлен в высокий уровень и ЦАП работает (это надо сделать как-нибудь)
// Отправка байта в устройство с адресом 0x94, в регистр 0x00 со значением 0x00.
i2c_SendByte(0x94, 0x00, 0x00);
// Приём байта из устройства с адресом 0x94 из регистра 0x01 (ID) в переменную buffer
i2c_ReceiveByte(0x94, 0x01, &ID);
}
Конечно, кроме как в учебном примере так делать нельзя. Ожидание окончания действия слишком уж долгое для такого быстрого контроллера.