Продолжая цикл публикаций по микроконтроллерам на ядре Cortex-M0 компании Megawin (см. предыдущие статьи 1, 2, 3, 4, 5 и 6), сегодня рассмотрим модуль интерфейса I2C.
Функциональные возможности модуля I2C
Микроконтроллеры серии MG32F02 включают один (MG32F02A032) или два (остальные МК) модуля интерфейса I2C. Модули имеют следующие функциональные особенности:
работа в режиме ведущего (master) или ведомого (slave);
частота шины до 1 МГц;
детальная настройка параметров сигнала SCL в режиме ведущего устройства;
удержание сигнала SCL в состоянии низкого уровня в режиме ведомого устройства;
схема активной подтяжки для линий SCL и SDA;
поддержка до двух адресов в режиме ведомого, а также детектирования адреса по маске;
поддержка адреса общего вызова;
поддержка режима нескольких ведущих (multi-master);
детектирование "нештатных" ситуаций на шине (некорректный NACK, переполнение буфера, потеря приоритета);
низкоуровневый (Byte mode) и высокоуровневый (Buffer mode) режимы работы;
буфер приема и передачи размером 4 байта;
поддержка DMA;
встроенный таймер таймаута, определения таймаута шины SMBus;
пробуждение модуля из состояния STOP.
Функциональная схема модулей I2C0 и I2C1 приведена на рисунке.

Каждый модуль I2Cx включает следующие основные узлы:
схему тактирования,
схему детектирования состояния пробуждения,
блок контроля линии SCL,
блок контроля линии SDA,
блок управления буфером Buffer Control,
блок управления режимами Master/Slave,
блок детектирования ошибок Error Detector,
таймер таймаута,
схему управления флагами событий и формирования сигнала прерывания INT_I2Cx.
Тактирование
Модуль I2Cx тактируется сигналом CK_I2Cx, источник которого выбирается в поле I2Cx_CLK.I2Cx_CK_SEL
из числа следующих:
сигнал CK_I2Cx_PR с выхода подсистемы тактирования МК, который, в свою очередь, определяется битом
CSC_CKS1.CSC_I2Cx_CKS
из сигналов CK_APB (0) или CK_AHB (1);выходной сигнал таймера TM00_TRGO.
Сигнал CK_I2Cx поступает на предделитель частоты Prescaler, на выходе которого формируется сигнал CK_I2Cx_PSC. Коэффициент предделителя из непрерывного диапазона 2-16 определяется в 4-битном поле I2Cx_CLK.I2Cx_CK_PSC
значениями от 1 до 15 соответственно. Далее сигнал поступает на делитель частоты DIV, коэффициент которого задается в поле I2Cx_CLK.I2Cx_CK_DIV
из ряда 2, 4, 8, ..., 128. На выходе делителя формируется основной тактовый сигнал модуля CK_I2Cx_INT. С другого выхода делителя с фиксированным коэффициентом 64 формируется сигнал CK_I2Cx_DIV64. Тактовый сигнал таймера таймаута CK_I2Cx_TMO в зависимости от значения поля I2Cx_CLK.I2Cx_TMO_CKS
может формироваться из сигнала CK_I2Cx_DIV64 или общесистемного сигнала CK_UT.
Частота внутреннего сигнала тактирования модуля CK_I2Cx_INT определяется выражением
FINT = F(CK_I2Cx_INT) = F(CK_I2Cx) / [ (PSC + 1)·DIV ],
где F(CK_I2Cx) частота сигнала CK_I2Cx, PSC — значение поля I2Cx_CLK.I2Cx_CK_PSC
, DIV — значение поля I2Cx_CLK.I2Cx_CK_DIV
.
На следующем рисунке показана временная диаграмма сигналов SCL и SDA.

В модуле имеется возможность настраивать интервалы tHIGH и tLOW через поля I2Cx_HT
и I2Cx_LT
регистра I2Cx_CR1
соответственно. Длительности состояния высокого уровня tHIGH и состояния низкого уровня tLOW сигнала SCL определяются выражениями
tHIGH = (1 + HT)·TINT,
tLOW = (1 + LT)·TINT,
где HT и LT — значения полей I2Cx_HT
и I2Cx_LT
соответственно, а TINT = 1 / FINT — период сигнала CK_I2Cx_INT.
В режиме мастера интервал tVD:DAT между задним фронтом сигнала SCL и моментом изменения состояния сигнала SDA составляет 2·TINT. Далее интервал до переднего фронта сигнала SCL составляет (LT-1)·TINT. Период сигнала SCL, формируемого мастером, будет определяться выражением
TSCL = tHIGH + tLOW = (2 + HT + LT)·TINT.
Итоговая номинальная частота шины I2C будет определяться выражением
FSCL = 1 / TSCL = FINT / (2 + HT + LT) = F(CK_I2Cx) / [ (PSC + 1)·DIV·(2 + HT + LT) ].
Параметры HT и LT имеют значения по-умолчанию 5 и 4 соответственно. Минимальными "рабочими" значениями параметров являются 2 и 2.
Режимы работы
Общие сведения
Модуль I2Cx включается установкой бита I2Cx_CR0.I2Cx_EN
. С точки зрения логики взаимодействия аппаратуры и программы в модулях I2Cx реализованы три основных режима работы:
I2C Byte mode — низкоуровневый (программно-аппаратный) режим с необходимостью реализации программного контроля приема и передачи на основе конечного автомата,
I2C Buffer mode — высокоуровневый (аппаратный режим),
Monitor (Buffer mode) — режим мониторинга (сниффер шины).
В первых двух режимах (I2C) модуль может играть роль ведущего (Master) или ведомого (Slave) устройства на шине I2C. Режим работы определяется битами I2Cx_MDS
и I2Cx_BUF_EN
регистра I2Cx_CR0
согласно следующей таблице:
|
| Режим работы |
---|---|---|
0 (I2C) | 0 (Disable) | I2C Byte mode (по-умолчанию) |
0 (I2C) | 1 (Enable) | I2C Buffer mode |
1 (Monitor) | 0 (Disable) | --- |
1 (Monitor) | 1 (Enable) | Monitor |
При дальнейшем рассмотрении работы модуля будем исходить из того, что читатель знаком с общим принципом функционирования шины I2C. Для изучения этого вопроса можно обратиться к официальной спецификации и ее переводу на русский язык:
Далее будем использовать следующие обозначения:
Обозначение | Описание |
---|---|
START | Состояние шины "Старт" (Start) |
STOP | Состояние шины "Стоп" (Stop) |
RSTART | Состояние шины "Повторный старт" (Repeated Start) |
SLA | Адресный кадр, с которого начинается обмен данными (Slave Address) |
SLA+W | Адресный кадр, в котором указывается, что ведущий далее будет передавать данные (SLA + Write) |
SLA+R | Адресный кадр, в котором указывается, что ведущий далее будет принимать данные (SLA + Read) |
ACK | Состояние шины "Подтверждено" (Acknowledgment) |
NACK | Состояние шины "Не подтверждено" (No Acknowledgment) |
Адресация
С точки зрения роли устройства на шине I2C в модулях I2Cx реализованы режимы "ведущее устройство" (Master) и "ведомое устройство" (Slave). По-умолчанию включен режим ведущего устройства. Режим ведомого включается при разрешении детектирования адреса.
В режиме ведомого устройства могут быть заданы один или два собственных адреса, на которые будет отвечать модуль. Основной адрес ведомого задается в регистре I2Cx_SADR
в битах 1-7, т.е. если записывать байт, то бит 0 можно оставить равным 0. Дополнительный адрес задается в битах 9-15 аналогично. Детектирование основного адреса разрешается установкой бита I2Cx_SADR_EN
, а дополнительного — установкой бита I2Cx_SADR2_EN
в регистре I2Cx_CR0
. Установка хотя бы одного из этих битов переводит модуль в режим ведомого устройства. В режиме пониженного энергопотребления STOP, если один из этих битов установлен, при детектировании на шине кадра обращения к устройству модуль активирует флаг пробуждения WUPF.
Для основного адреса также можно задействовать маску I2Cx_MASK.I2Cx_SA_MSK
(биты 1-7 регистра), расширяющую диапазон адресов, на которые будет отвечать модуль: в адресе будут проверяться только те разряды, которые установлены в 1 в маске. По-умолчанию, в маске установлено значение 0x7F, т.е. строгое соответствие заданному в поле I2Cx_SADR
адресу. Кроме того, модуль будет отвечать на нулевой адрес (общий вызов), если установлен бит I2Cx_CR0.I2Cx_GC_EN
(по-умолчанию сброшен).
Буфер данных
В модуле I2Cx программно доступен 8-разрядный сдвиговый регистр I2Cx_SBUF
, выполняющий функцию буфера данных нижнего уровня. Программа же обычно взаимодействует с 32-разрядным регистром I2Cx_DAT
, выполняющим функцию буфера верхнего уровня с возможностью 8-, 16- и 32-битного доступа. Буфер тесно связан с полем I2Cx_CR2.I2Cx_BUF_CNT
, в котором в режиме Buffer mode задается фактическое число (1-4) принимаемых или передаваемых байт (в режиме Master) или сохраняется число уже принятых байт (в режиме Slave). Последовательность байт при приеме или передаче — от младшего к старшему.
В режиме Buffer mode при передаче данных разрядность операции записи в регистр I2Cx_DAT
должна соответствовать объему передаваемых данных: для записи одного байта нужно использовать байтовую пересылку (STRB
) в младший байт регистра, для двух байт — двухбайтовую (STRH
), для 3-4 байт — пересылку слова (STR
).
Команды управления состоянием
Для управления состоянием шины I2C в регистре I2Cx_CR2
имеются биты, установка которых приводит к генерации нового состояния непосредственно в момент их установки или по завершению текущей операции приема или передачи данных. Биты команд приведены в следующей таблице.
Разряд | Название | Описание |
---|---|---|
0 |
| Генерация состояния START |
1 |
| Генерация состояния STOP |
2 |
| Генерация состояния ACK |
3 |
| Разрешение генерации состояний при установке битов PSTA, PSTO, PAA |
4 |
| Разблокировка записи в разряды |
5 |
| Разблокировка записи в разряды |
6 |
| Разблокировка записи в разряды |
24 |
| Генерация состояния START после завершения текущей операции |
25 |
| Генерация состояния STOP после завершения текущей операции |
26 |
| Генерация состояния ACK после завершения текущей операции |
Команда STA (START) формируется при установке бита I2Cx_STA
вместе с битом разблокировки I2Cx_STA_LCK
и приводит в режиме Master к генерации состояния START, если шина была свободна. Если шина была занята, ожидается состояние STOP на шине, после чего генерируется состояние START. Если модуль уже находится в режиме ведущего после предыдущей команды STA в процессе приема или передачи, генерируется состояние RSTART.
Команда STO (STOP) формируется при установке бита I2Cx_STO
вместе с битом разблокировки I2Cx_STO_LCK
и приводит в режиме Master к генерации состояния STOP. Если одновременно с командой STO дается команда STA, то после генерации состояния STOP генерируется состояние START. В режиме Slave команда STO может использоваться для выхода из состояния какой-либо ошибки на шине.
Команда AA (ACK) формируется при установке бита I2Cx_AA
вместе с битом разблокировки I2Cx_AA_LCK
и приводит к генерации состояния ACK (низкий уровень сигнала SDA в течение передачи 9-го импульса сигнала SCL в текущем кадре) в следующих случаях:
подтверждение адреса при совпадении в кадре SLA в режиме ведомого устройства,
подтверждение принятия байта в режиме приема ведущим устройством,
подтверждение принятия байта в режиме приема ведомым устройством.
Если устанавливается бит I2Cx_AA_LCK
при сброшенном бите I2Cx_AA
, генерируется состояние NACK (остается высокий уровень сигнала SDA в течение передачи 9-го импульса сигнала SCL в текущем кадре) в следующих случаях:
принят байт в режиме приема ведущим устройством,
принят байт в режиме приема ведомым устройством.
Команды STO и AA предназначены для применения, в первую очередь, в программно-аппаратном режиме (Byte mode). В аппаратном режиме (Buffer mode) большинство действий выполняется автоматически и в программе лишь требуется заранее установить поведение модуля с помощью команд PSTA, PSTO и PAA. Для их выполнения также должен быть установлен бит I2Cx_CMD_TC
.
Команда PSTA формируется при установке бита I2Cx_PSTA
вместе с битом I2Cx_STA_LCK
и приводит в режиме Master к генерации состояния RSTART после завершения запланированной операции приема или передачи данных.
Команда PSTO формируется при установке бита I2Cx_PSTO
вместе с битом I2Cx_STO_LCK
и приводит в режиме Master к генерации состояния STOP после завершения запланированной операции приема или передачи данных.
Команда PAA формируется при установке бита I2Cx_PAA
вместе с битом I2Cx_STO_LCK
и приводит к формирования состояния подтверждения после завершения запланированной операции приема или передачи данных. Если бит I2Cx_PAA
сброшен, а бит I2Cx_STO_LCK
установлен, после завершения операции подтверждение генерироваться не будет.
Программно-аппаратный режим (Byte mode)
В программно-аппаратном режиме Byte mode передача и прием каждого кадра шины I2C разбивается на несколько этапов, характеризующихся тем или иным состоянием шины и модуля. Завершение каждого этапа и переход в новое состояние представляет собой событие. Программа должна реагировать на каждое новое событие: формировать команды, считывать или записывать данные. Все возможные состояния пронумерованы и представлены в таблице. Отметим, что разработчики МК серии MG32F02 взяли схему кодирования состояний модуля I2C, применяемую в 8-разрядных МК на ядре C8051 (например, NXP, Silicon Labs) и МК серии ATmega.
Код | Описание | MT | MR | SR | ST |
---|---|---|---|---|---|
0x00 | Ошибка шины | v | v | v | v |
0x08 | Сформировано состояние START | v | v | ||
0x10 | Сформировано состояние RSTART | v | v | ||
0x18 | Передан кадр SLA+W и получено подтверждение (ACK) | v | |||
0x20 | Передан кадр SLA+W, подтверждение не получено (NACK) | v | |||
0x28 | Передан байт с данными и получено подтверждение (ACK) | v | |||
0x30 | Передан байт с данными, подтверждение не получено (NACK) | v | |||
0x38 | Потеря приоритета при передаче кадра SLA+R/W или данных | v | v | ||
0x40 | Передан кадр SLA+R и получено подтверждение (ACK) | v | |||
0x48 | Передан кадр SLA+R, подтверждение не получено (NACK) | v | |||
0x50 | Принят байт с данными и сформировано состояние ACK | v | |||
0x58 | Принят байт с данными и сформировано состояние NACK | v | |||
0x60 | Принят кадр SLA+W с собственным адресом и сформировано состояние ACK | v | |||
0x68 | Принят кадр SLA+W с собственным адресом, потерян приоритет, сформировано состояние ACK | v | |||
0x70 | Принят общий вызов и сформировано состояние ACK | v | |||
0x78 | Принят общий вызов, потерян приоритет, сформировано состояние ACK | v | |||
0x80 | Устройство уже адресовано, принят байт с данными и сформировано состояние ACK | v | |||
0x88 | Устройство уже адресовано, принят байт с данными и сформировано состояние NACK | v | |||
0x90 | Общий вызов, принят байт с данными и сформировано состояние ACK | v | |||
0x98 | Общий вызов, принят байт с данными и сформировано состояние NACK | v | |||
0xA0 | Устройство адресовано, обнаружено состояние START или RSTART | v | |||
0xA8 | Принят кадр SLA+R с собственным адресом и сформировано состояние ACK | v | |||
0xB0 | Принят кадр SLA+R с собственным адресом, потерян приоритет, сформировано состояние ACK | v | |||
0xB8 | Передан байт с данными и получено подтверждение (ACK) | v | |||
0xC0 | Передан байт с данными, подтверждение не получено (NACK) | v | |||
0xC8 | Передан последний байт с данными и получено подтверждение (ACK) | v | |||
0xF8 | Состояние шины STOP или шина свободна | v | v | v | v |
В колонках MT, MR, SR и ST указана применимость состояния в режимах "ведущий передает", "ведущий принимает", "ведомый принимает" и "ведомый передает" соответственно. Код последнего события (состояния) всегда отображается в поле I2Cx_STA2.I2Cx_EVENT
, а появление нового события приводит к установке флага EVENTF и возможному прерыванию. В приеме и передаче данных в этом режиме используется только однобайтный буфер I2Cx_SBUF
. Для формирования состояний на шине применяются вышеописанные команды STA, STO и AA.
В документации User Guide приводятся подробные блок-схемы алгоритмов работы модуля и таблицы переходов между состояниями для режимов: "ведущий передает", "ведущий принимает", "ведомый передает", "ведомый принимает" и "ведомый принимает общий вызов". Оптимальной является реализация рассматриваемых алгоритмов на основе прерываний и конечного автомата.
Аппаратный режим (Buffer mode)
Общие сведения
Аппаратный режим (Buffer mode) позволяет максимально автоматизировать работу с модулем I2C и свести к минимуму программный код. Регистр I2Cx_DAT
выполняет в этом режиме функцию 4-байтного буфера, а синхронизация действий с программой достигается с помощью основных флагов RXF и TXF. В данном режиме при приеме и передаче данных используется также 32-разрядный промежуточный буфер (Shadow Buffer). С буфером связан счетчик I2Cx_ACNT
, который увеличивается на 1 в следующих случаях:
в режиме приема данных — при передаче каждого байта из сдвигового регистра Shift Buffer в промежуточный буфер,
в режиме передачи данных — при передаче каждого байта из промежуточного буфера в сдвиговый регистр.
Ведущее устройство (Master)
На следующем рисунке показан алгоритм работы модуля в данном режиме.

Ведущее устройство начинает работу с формирования на шине адресного кадра SLA. Для этого дается команда STA с одновременным указанием в поле I2Cx_CR2.I2Cx_BUF_CNT
числа передаваемых байт N от 1 до 4. Далее в регистр I2Cx_DAT
записываются передаваемые данные. Первым по порядку байтом должен быть адрес ведомого устройства, к которому происходит обращение, сдвинутый на 1 разряд влево. Если младший бит байта сброшен, формируется кадр SLA+W, если установлен — кадр SLA+R. Если было указано N>1, то в случае ответа ведомого состоянием ACK в кадре SLA+W ведущий продолжает передачу данных в этом же кадре. В любом случае данные передаются только если было получено подтверждение на предыдущий байт. По завершению передачи устанавливается флаг TXF.
При указании в поле I2Cx_BUF_CNT
числа передаваемых байт нужно одновременно сформировать команду PSTO или PSTA, чтобы по завершению передачи модуль сгенерировал состояние STOP или RSTART соответственно.
Если был сформирован кадр SLA+R, после подтверждения адреса со стороны ведомого устройства модуль переходит в режим приема данных. В поле I2Cx_BUF_CNT
нужно указать число принимаемых байт N от 1 до 4. После передачи N байт из промежуточного буфера в регистр I2Cx_DAT
активируется флаг RXF.
В завершающей операции приема данных следует также сформировать команду PSTO или PSTA. В этом случае после получения последнего байта модуль генерирует состояние NACK, сигнализируя тем самым ведомому устройству о завершении операции. Прием всех предыдущих байтов модулем подтверждается автоматически. Если требуется подтвердить и последний байт, вместе с командой PSTO (или PSTA) необходимо также дать команду PAA.
Если в программе перед приемом данных требуется сбросить флаг RXF, то необходимо выполнить "фиктивное" чтение регистра I2Cx_DAT
, а не сбрасывать флаг явно записью 1 в регистр I2Cx_STA
, что может привести к приему лишних данных.
Для ведущего устройства можно рекомендовать следующий алгоритм отправки данных:
Дать команду
STA
с одновременной записью 1 в полеI2Cx_CR2.I2Cx_BUF_CNT
.В регистр
I2Cx_DAT
записать адрес ведомого устройства, сдвинутый на 1 разряд влево. Младший бит адреса должен быть нулевым. Таким образом будет сформировано состояние SLA+W.Ожидать установку флага TXF, т.е. готовность буфера принять новые данные.
В поле
I2Cx_CR2.I2Cx_BUF_CNT
указать число отправляемых байт (1-4), а также опционально установить битI2Cx_PSTO
илиI2Cx_PSTA
(вместе с битомI2Cx_CMD_TC
), если после отправки требуется автоматически сгенерировать состояние STOP или RSTART соответственно.Записать новые данные в регистр
I2Cx_DAT
(после чего начинается фактическая отправка).Если требуется дальнейшая передача данных (более 4-х байт), повторить пункты 3-5.
При "ручном" методе генерации состояния STOP ожидать активацию флага TSCF (фактическое завершение передачи) и только после этого дать команду
STO
.При автоматической генерации состояния STOP перед отправкой следующего кадра (очередной команды
STA
) необходимо ожидать активацию флага STOPF.
Кроме того, возможно потребуется программный сброс флага TXF перед пунктом 4 в случае отправки нескольких кадров с данными.
Для ведущего устройства можно рекомендовать следующий алгоритм приема данных:
Дать команду
STA
с одновременной записью 1 в полеI2Cx_CR2.I2Cx_BUF_CNT
.В регистр
I2Cx_DAT
записать адрес ведомого устройства, сдвинутый на 1 разряд влево. Младший бит адреса должен быть установлен. Таким образом будет сформировано состояние SLA+R.Ожидать установку флага SADRF, т.е. завершение формирования состояния SLA+R.
В поле
I2Cx_CR2.I2Cx_BUF_CNT
указать число ожидаемых байт (1-4), а также опционально установить битI2Cx_PAA
и(или)I2Cx_PSTO
(вместе с битомI2Cx_CMD_TC
), если после приема последнего байта требуется автоматически сгенерировать подтверждение и(или) состояние STOP соответственно.Ожидать установку флага RXF, т.е. готовность данных в буфере.
Прочитать данные из регистра
I2Cx_DAT
.Если требуется дальнейший прием данных (более 4-х байт), повторить пункты 4-6.
При "ручном" методе генерации состояния STOP дать команду
STO
.При автоматической генерации состояния STOP перед отправкой следующего кадра (очередной команды
STA
) необходимо ожидать активацию флага STOPF.
Ведомое устройство (Slave)
Модуль переходит в режим ведомого устройства (slave) после установки одного из битов включения механизма детектирования адреса I2Cx_SADR_EN
или I2Cx_SADR2_EN
. Алгоритм работы модуля показан на следующем рисунке.

При получении кадра SLA+R с подходящим адресом модуль подтверждает кадр и активирует флаг SADRF. Далее происходит передача данных из буфера I2Cx_DAT
(режим Slave Transmitter) в промежуточный буфер. Счетчик в поле I2Cx_CR2.I2Cx_ACNT
отображает число фактически отправленных байт из промежуточного буфера в сдвиговый регистр.
Если число N, указанное в поле I2Cx_CR2.I2Cx_BUF_CNT
, было в интервале от 1 до 4, после передачи N байт в промежуточный буфер на отправку активируется флаг TXF. В этот момент в программе в регистр I2Cx_DAT
нужно записать новые данные. После записи в регистр I2Cx_DAT
счетчик I2Cx_ACNT
обнуляется. Если новые данные не были записаны, будут отправляться существующие данные последовательно с нулевого по (N-1)-й байт. Данные в принципе будут передаваться на шину пока ведущее устройство генерирует тактовые импульсы сигнала SCL не зависимо от значения I2Cx_BUF_CNT
до тех пор, пока на шине не будет cгенерировано состояние STOP или RSTART. Если значения I2Cx_BUF_CNT
равно 0, флаг TXF сработает после отправки 8-го байта, при этом соответствия отправляемых данных каким-либо байтам буфера не гарантируется.
После получения и подтверждения кадра SLA+W также активируется флаг SADRF и затем начинается процесс приема данных. Каждый принятый байт из сдвигового регистра помещается в промежуточный буфер. Четырехбайтный промежуточный буфер заполняется начиная с младшего байта. Количество байт, записанных в этот буфер, доступно в поле I2Cx_CR2.I2Cx_ACNT
. После записи 4-го байта или при определении на шине состояния STOP или RSTART выполняются следующие действия:
принятые данные копируются в регистр
I2Cx_DAT
,число принятых байт записывается в поле
I2Cx_CR2.I2Cx_BUF_CNT
,значение поля
I2Cx_CR2.I2Cx_ACNT
обнуляется,активируется флаг RXF, после чего в программе можно прочитать и проанализировать данные.
Для ведомого устройства можно рекомендовать следующий алгоритм работы:
Установить адрес или адреса (при необходимости, и маску) ведомого устройства и переключиться в режим slave.
Опционально: ожидать срабатывание флага SADRF (очевидно, после получения кадра SLA+W с командой) для детектирования факта обращения к устройству и подготовки к выполнению запроса.
Ожидать срабатывание флага RXF, после чего прочитать данные из регистра
I2Cx_DAT
, количество принятых байт взять из поляI2Cx_CR2.I2Cx_BUF_CNT
.Проанализировать данные. Если требуется дальнейший прием данных, перейти на предыдущий пункт.
Если согласно полученному запросу требуется отправить данные в ответ, количество байт указать в поле
I2Cx_CR2.I2Cx_BUF_CNT
, затем поместить их в регистрI2Cx_DAT
. Это нужно сделать не дожидаясь активации флага SADRF в следующем кадре SLA+R.Если требуется дальнейшая отправка данных, ожидать срабатывание флага TXF и перейти на предыдущий пункт.
Опционально: ожидать срабатывание флагов STOPF или RSTRF.
Дополнительные функции
Управление линией SCL для Slave
В режиме Buffer mode в процессе обработки запросов ведомому может потребоваться дополнительное время на формирование ответа, например, на получение данных, которые затребовал ведущий. В этом случае согласно спецификации интерфейса I2C ведомое устройство может задержать тактирование от ведущего путем удержания линии SCL в состоянии низкого уровня. Функция удержания линии SCL по-умолчанию включена и работает в следующих случая:
в режиме приема буфер уже заполнен, но программа еще не прочитала из него данные;
в режиме передачи буфер уже опустошен, но программа еще не записала в него новые данные.
Для выключения данной функции нужно установить бит I2Cx_CR0.I2Cx_SCLS_DIS
.
Таймер таймаута
В состав модулей I2Cx входит 8-разрядный таймер таймаута TMO. Таймер тактируется сигналом CK_I2Cx_TMO (см. п. Тактирование). На рисунке показана функциональная схема таймера.

В зависимости от значения поля I2Cx_TMOUT.I2Cx_TMO_MDS
таймер работает в следующих режимах:
0 — ожидание низкого уровня на линии SCL (по-умолчанию),
1 — ожидание высокого уровня на линиях SCL и SDA,
2 — таймер общего назначения.
Для включения таймера необходимо установить бит I2Cx_TMOUT.I2Cx_TMO_EN
. Для применения таймера в режиме общего назначения необязательно включать модуль I2Cx. Период счета таймера задается в поле I2Cx_TMOUT.I2Cx_TMO_CNT
. После завершения полного периода (переполнения) активируется флаг TMOUTF. Если установлен бит I2Cx_TMOUT.I2Cx_TMO_CTL
, то при этом происходит сброс всего модуля I2Cx. После срабатывания таймера TMO с настройкой на сброс перед началом каких-либо следующих действий необходимо сбросить флаг TMOUTF, иначе модуль не сможет корректно работать. Автоматический сброс модуля I2Cx при установленном бите I2Cx_TMOUT.I2Cx_TMO_CTL
не приводит к сбросу этого флага.
События и прерывания
Схема формирования общих (вторичных) флагов событий BUFF, STPSTRF и ERRF, генерирующих прерывание модуля INT_I2Cx, приведена на следующем рисунке.

Флаги событий собраны в регистре I2Cx_STA
, а биты разрешения прерываний — в регистре I2Cx_INT
. Перечень событий и соответствующих флагов прерываний приведен в следующей таблице.
Разряд | Флаг события | Бит прерывания | Название события | Описание |
---|---|---|---|---|
0 | BUSYF | - | I2C control busy | Модуль занят выполнением операции |
1 | EVENTF | EVENT_IE | Event code change | Изменилось состояние модуля |
2 | BUFF | BUF_IE | Buffer mode event | Общий флаг событий в буфере |
3 | ERRF | ERR_IE | Error detect | Общий флаг ошибок |
4 | TMOUTF | TMOUT_IE | Timeout detect | Сработал таймер таймаута |
5 | WUPF | WUP_IE | STOP mode wakeup by I2C event | Событие пробуждения в режиме питания STOP при получении адресного кадра в режиме Slave |
6 | RXF | BUF_IE | Receive data register not empty | Принятые данные готовы к чтению (Buffer mode) |
7 | TXF | BUF_IE | Transmit data register empty | Буфер передачи готов к записи новых данных (Buffer mode) |
8 | RSTRF | BUF_IE | Repeat Start asserted | Обнаружено состояние шины RSTART |
9 | STOPF | BUF_IE | Stop detection | Обнаружено состояние шины STOP |
10 | CNTF | - | BUF_CNT register empty | Счетчик BUF_CNT в значении 0 |
11 | ERRCF | - | I2C error | Не принято подтверждение в режиме Master в кадре SLA или при передаче данных |
12 | SADRF | BUF_IE | Slave address asserted or match detect | В режиме Slave — получен кадр SLA с подходящим адресом, в режиме Master — ведомый подтвердил адрес в кадре SLA+R |
13 | SLAF | - | Slave mode detect | Включен режим ведомого |
14 | MSTF | - | Master mode detection | Включен режим ведущего |
15 | RWF | - | Read or write transfer direction | Состояние 8-го бита в кадре SLA: 0 - кадр SLA+W, 1 - кадр SLA+R |
16 | TSCF | - | Shadow Buffer Transfer complete | В режиме передачи байт из промежуточного буфера скопирован в сдвиговый регистр, в режиме приема — из сдвигового регистра в промежуточный буфер |
17 | STPSTRF | STPSTR_IE | STOP or START detect | Обнаружено состояние STOP или START |
18 | TXRF | - | Slave mode transmit data register remained status | Флаг активен, если при отправке из-за ошибки в буфере остались непереданные данные |
19 | ROVRF | ERR_IE | Data buffer RX overrun | Переполнение буфера приема (Buffer mode, задержка SCL отключена) |
20 | TOVRF | ERR_IE | Data buffer TX Overrun | Буфер передачи не содержит данных (Buffer mode, задержка SCL отключена) |
21 | NACKF | ERR_IE | Invalid NoACK received Error | Обнаружено состояние шины NACK |
22 | ALOSF | ERR_IE | Arbitration lost error | Потеря приоритета |
23 | BERRF | ERR_IE | Bus error | Ошибка шины |
В режиме Byte mode для работы ПО необходимо включить прерывание EVENT_IE по флагу EVENTF. В режиме Buffer mode для работы ПО достаточно включить прерывание BUF_IE по флагу BUFF. Если требуется анализ ошибок, необходимо также включить прерывание ERR_IE по общему флагу ERRF. Прерывание TMOUT_IE генерируется только при включении в работу модуля таймера таймаута TMO.
В процессе тестирования обнаружена недокументированная функция: в старшем байте регистра I2Cx_STA
отображается код состояния модуля I2Cx_EVENT
, в том числе, в режиме Buffer mode.
Для включения генерации прерывания необходимо:
Выбрать событие (или события) в регистре
I2Cx_INT
.Разрешить прерывание самого модуля установкой бита
I2Cx_INT.I2Cx_IEA
.Разрешить прерывание IRQ от модуля в контроллере прерываний NVIC в регистре
CPU_ISER
.
Тестирование
Аппаратная часть
Целью тестирования является проверка работы МК в роли ведущего и ведомого устройства шины I2C. В роли ведущего будет применяться МК MG32F02A064AD48, в роли ведомого — (1) "эталонный" Slave — часы реального времени DS3231 и (2) МК MG32F02A032AT20. Схема подключения микроконтроллеров показана на следующем рисунке.

Все устройства запитываются от стабилизатора напряжением 3.3 В программатора J-Link/ST-Link. Для удобства переключения линий SWD-интерфейса между МК установлен переключатель S1. Для линий интерфейса I2C SCL и SDA установлены внешние резисторы подтяжки R3 и R4. При подключении модуля DS3231 МК U2 отключается от шины I2C перемычками (на схеме не показаны), а при работе U2 в качестве ведомого — отключается модуль DS3231, поскольку МК эмулирует его работу. Каждый МК также подключается к ПК через интерфейс UART (в обоих случаях используется модуль МК URT0).
Проектные файлы
В тестировании принимают участие одновременно два "подопытных" МК, поэтому в файлах конфигурации проекта есть изменения. Действия по настройке путей к библиотечным файлам от вендора перенесены в отдельную функцию setup_paths()
в файле premake5.lua
, поскольку пути зависят от типа МК. Для МК MG32F02A032 аргументом функции нужно указать строку "MG32F02A032"
, для всех остальных МК семейства — строку "MG32F02A128"
. В файле определены следующие цели сборки:
svr32
— базовая часть (supervisor) для МК MG32F02A032,svr64
— базовая часть для МК MG32F02A064,app
— прикладная часть (application) для ведущего устройства (MG32F02A064),slave
— прикладная часть для ведомого устройства (MG32F02A032),clock
— прикладная часть для создания часов на базе LED-дисплея на м/с TM1637 (MG32F02A064), в статье не рассматривается.
В каталог проекта также добавлены соответствующие скрипты shell для сборки, запуска и просмотра листинга дизассемблера. Библиотечные файлы драйверов внешних микросхем, подключаемых далее к МК, выделены в отдельный подкаталог ic
.
Параметры и действия, связанные с конфигурацией выводов МК, вынесены в отдельный заголовочный файл hwcf.h
. На данный момент заданы два набора параметров. В качестве примера приводим набор для MG32F02A032:
#ifdef HWCF_A032
#define HW_CLK_AHB 12000000 // MHz
#define HW_LED1_CRH0 PB_CR2_h0 // control register
#define HW_LED1_SCH0 PB_SC_h0 // set-clear register
#define HW_LED1_MASK (1 << 2) // bit mask
#define HW_LED2_CRH0 PB_CR3_h0 // control register
#define HW_LED2_SCH0 PB_SC_h0 // set-clear register
#define HW_LED2_MASK (1 << 3) // bit mask
// Настройка выводов URT0:
#define HW_URT0_SETTX RH(PC_CR0_h0) = (0xA << 12) | 2
#define HW_URT0_SETRX RH(PC_CR1_h0) = (0xA << 12) | (1 << 5) | 3
// Настройка выводов I2C0:
#define HW_I2C0_SETSCL RH(PB_CR10_h0) = (2 << 12) | (1 << 5) | 1
#define HW_I2C0_SETSDA RH(PB_CR11_h0) = (2 << 12) | (1 << 5) | 1
#endif // HWCF_A032
Настройки выводов светодиодов сделаны через константы. Настройки выводов интерфейсов UART и I2C заданы в виде полной операции конфигурации портов.
Напомню, что весь исходный код и сопутствующие файлы доступны в репозитории GitHub.
Библиотека для работы с модулем I2C
Общие функции
Для удобства тестирования разработана библиотека модуля I2C с минимальным набором функций (файлы src/i2c.h
и src/i2c.c
). Библиотека поддерживает только аппаратный режим Buffer mode. Каждая функция получает первым аргументом идентификатор конкретного модуля I2Cx в виде базового адреса его регистров:
#define I2C0_id I2C0_Base
#define I2C1_id I2C1_Base
Такой подход оказался очень эффективным, поскольку "заставил" компилятор gcc применять инструкции обращения к памяти с указанием константного смещения вида str r1, [r0, #16]
(здесь r0
— первый аргумент, который содержит базовый адрес), вместо того, чтобы для каждого обращения к регистру выделять в секции кода еще 4 байта для его адреса.
Работа с модулем начинается с функции инициализации i2c_init()
:
/// Инициализация модуля I2C (включение тактирования)
void i2c_init(uint32_t id) {
RH(CSC_KEY_h0) = 0xA217; // unlock access to CSC regs
#ifdef MG32F02A032
RB(CSC_APB0_b1) |= CSC_APB0_I2C0_EN_enable_b1;
#endif
#ifdef MG32F02A128
RB(CSC_APB0_b1) |= (id & 0x00010000) ? CSC_APB0_I2C1_EN_enable_b1 : CSC_APB0_I2C0_EN_enable_b1;
#endif
RH(CSC_KEY_h0) = 0; // lock access to CSC regs
// Настройка тактирования
RH(id+( I2C0_CLK_h0 -I2C0_Base)) =
I2C_CLK_TMO_CKS_div64_h0 | // CK_TMO: F(CK_PSC)/64 = 37500 Hz
((5 -1) << I2C_CLK_CK_PSC_shift_h0) | // CK_PSC: 12 MHz /5 = 2400 kHz
I2C_CLK_CK_DIV_div4_h0 | // CK_I2Cx_INT: 600 kHz => F(SCL) = 100 kHz
I2C_CLK_CK_SEL_proc_h0; // I2Cx_CK_SEL: APB, 12 MHz
// Тайминг режима master
RH(I2C0_CR1_h0) = 0x0202; // (2+HT+LT) = 6
}
Здесь в зависимости от типа МК включается общее тактирование единственного модуля I2C0 или одного из двух доступных I2C0 или I2C1 в зависимости от id
. Далее производится настройка тактирования внутри модуля. В данном примере показаны настройки на частоту шины 100 кГц. Расчет следующий: суммарный коэффициент деления k = F(CK_I2Cx) / FSCL = 12 МГц / 100 кГц = 120. При HT=2 и LT=2 получаем
(PSC + 1)·DIV = k/ (2 + HT + LT) = 20.
Чтобы частота сигнала CK_I2Cx_TMO была как можно выше, выбираем PSC=4 (коэффициент предделителя 4+1=5) и DIV=4. Таким образом, получаем частоту сигнала I2Cx_CK_PSC 2400 кГц, сигнала I2Cx_CK_INT — 600 кГц, сигнала I2Cx_CK_TMO — 37.5 кГц.
Режим работы модуля устанавливается с помощью функции i2c_setup_mode()
:
/// Установка режима работы модуля I2C по формату регистра CR0 (устанавливает I2Cx_EN)
inline
void i2c_setup_mode(uint32_t id, uint32_t mode) {
RW(id+( I2C0_CR0_w -I2C0_Base)) = mode | I2C_CR0_EN_enable_w; // включаем модуль
}
Для настройки прерывания имеется функция i2c_setup_int()
:
/// Включение прерывания INT_I2Cx по флагам, указанным в flags согласно формату I2Cx_INT
void i2c_setup_int(uint32_t id, uint32_t flags) {
RW(id+( I2C0_INT_w -I2C0_Base)) = flags | I2C_INT_IEA_enable_w; // включаем прерывания в модуле
// включаем прерывание в модуле NVIC:
RW(CPU_ISER_w) = 1 << ((id & 0x00010000) ? 29 : 28); // SETENA
}
В функциях блокирующего опроса состояния модуля используется таймер таймаута, запуск которого с установкой интервала времени ~ 1 мс осуществляется функцией i2c_setup_tmout()
:
/// Настройка таймера таймаута, режим работы mode определяется по формату младшего байта регистра I2Cx_TMOUT.
inline
void i2c_setup_tmout(uint32_t id, uint8_t mode) {
RH(id+( I2C0_TMOUT_h0 -I2C0_Base)) =
(38 << I2C_TMOUT_TMO_CNT_shift_h0) | // период счета ~1 мс для F(CK_TMO)=37.5 кГц
mode |
// I2C_TMOUT_TMO_MDS_scl_low_h0 |
// I2C_TMOUT_TMO_MDS_scl_sda_high_h0 |
I2C_TMOUT_TMO_CTL_enable_h0 |
I2C_TMOUT_TMO_EN_enable_h0;
}
Проверка срабатывания таймера осуществляется функцией i2c_get_tmout()
:
/// Возвращает 1, если таймаут, иначе 0.
inline
uint32_t i2c_get_tmout(uint32_t id) {
return (RB(id+( I2C0_STA_b0 -I2C0_Base)) & I2C_STA_TMOUTF_happened_b0) != 0;
}
Все биты регистра статуса возвращает функция i2c_get_status()
:
/// Возвращает I2Cx_STA
inline
uint32_t i2c_get_status(uint32_t id) {
return RW(id+( I2C0_STA_w -I2C0_Base));
}
Следующие две функции i2c_wait_start()
и i2c_wait_stop()
предназначены для ожидания состояний RSTART и STOP:
/// Ожидает состояние REPEAT START
void i2c_wait_start(uint32_t id) {
i2c_setup_tmout(id,0);
while (! (RW(id+( I2C0_STA_w -I2C0_Base)) & (I2C_STA_RSTRF_happened_w | I2C_STA_TMOUTF_happened_w) )) ;
}
/// Ожидает состояние STOP
void i2c_wait_stop(uint32_t id) {
i2c_setup_tmout(id,0);
while (! (RW(id+( I2C0_STA_w -I2C0_Base)) & (I2C_STA_STOPF_happened_w | I2C_STA_TMOUTF_happened_w) )) ;
}
Функции для ведущего устройства (Master)
В предлагаемых примерах работа ведущего устройства реализована на основе блокирующих функций с заданным временем ожидания без применения прерывания. Функция i2c_master_startw()
генерирует кадр SLA+W и ожидает готовность модуля (перед отправкой данных или завершения кадра):
/// Генерирует состояние START + WRITE с указанным адресом (младший бит должен быть 0).
void i2c_master_startw(uint32_t id, uint8_t addr) {
RW(id+( I2C0_CR2_w -I2C0_Base)) =
(1 << 8) | // BUF_CNT
I2C_CR2_STA_LCK_un_locked_w | I2C_CR2_STA_mask_w; // STA
RB(id+( I2C0_DAT_b0 -I2C0_Base)) = addr;
i2c_setup_tmout(id,0);
while (! (RW(id+( I2C0_STA_w -I2C0_Base)) & (I2C_STA_TXF_happened_w | I2C_STA_TMOUTF_happened_w) )) ;
}
Функция i2c_master_startr()
генерирует кадр SLA+R и ожидает его завершение:
/// Генерирует состояние START + READ с указанным адресом (младший бит должен быть 0).
void i2c_master_startr(uint32_t id, uint8_t addr) {
RW(id+( I2C0_CR2_w -I2C0_Base)) =
(1 << 8) | // BUF_CNT
I2C_CR2_STA_LCK_un_locked_w | I2C_CR2_STA_mask_w; // STA
RB(id+( I2C0_DAT_b0 -I2C0_Base)) = addr | 0x01;
i2c_setup_tmout(id,0);
while (! (RW(id+( I2C0_STA_w -I2C0_Base)) & (I2C_STA_SADRF_happened_w | I2C_STA_TMOUTF_happened_w) )) ;
}
Функция i2c_master_send()
помещает в буфер передачи данные data
длиной len
от 1 до 4 байт и ожидает их передачу в промежуточный буфер:
/// Режим master: передача в режиме Buffer len байт (1-4) из data.
/// Генерирует состояние STOP, если указана опция I2C_STOP.
/// Блокирующая функция с таймаутом: ожидает флаг TXF.
void i2c_master_send(uint32_t id, uint32_t opts, uint8_t len, uint32_t data) {
RW(id+( I2C0_STA_w -I2C0_Base)) |= I2C_STA_TXF_mask_w; // сбрасываем TXF, иначе финальная проверка в конце функции может сработать сразу после старта
RW(id+( I2C0_CR2_w -I2C0_Base)) = opts | (len << 8); // BUF_CNT
RW(id+( I2C0_DAT_w -I2C0_Base)) = data;
i2c_setup_tmout(id,0);
while (! (RW(id+( I2C0_STA_w -I2C0_Base)) & (I2C_STA_TXF_happened_w | I2C_STA_TMOUTF_happened_w) )) ;
}
Функция i2c_master_recv()
ожидает прием данных в количестве len
байт и возвращает содержимое 32-битного буфера:
/// Режим master: прием в режиме Buffer len байт (1-4).
/// Генерирует ACK, если указана опция I2C_ACK.
/// Генерирует состояние STOP, если указана опция I2C_STOP.
/// Блокирующая функция с таймаутом: ожидает флаг RXF.
uint32_t i2c_master_recv(uint32_t id, uint32_t opts, uint8_t len) {
RW(id+( I2C0_CR2_w -I2C0_Base)) = opts | (len << 8); // BUF_CNT
i2c_setup_tmout(id,0);
while (! (RW(id+( I2C0_STA_w -I2C0_Base)) & (I2C_STA_RXF_happened_w | I2C_STA_TMOUTF_happened_w) )) ;
return RW(id+( I2C0_DAT_w -I2C0_Base));
}
Указанные две функции принимают вторым аргументом opts
опции в виде двоичных флагов в соответствии с форматом регистра I2Cx_CR2
. Используемые опции включают команды и собраны в перечислении:
/// Опции по формату регистра I2Cx_CR2
enum I2C_Options {
I2C_NOOPTS = 0,
I2C_NACK = I2C_CR2_CMD_TC_enable_w | I2C_CR2_AA_LCK_un_locked_w,
I2C_ACK = I2C_CR2_CMD_TC_enable_w | I2C_CR2_AA_LCK_un_locked_w | I2C_CR2_PAA_mask_w,
I2C_STOP = I2C_CR2_CMD_TC_enable_w | I2C_CR2_STO_LCK_un_locked_w | I2C_CR2_PSTO_mask_w,
I2C_START = I2C_CR2_CMD_TC_enable_w | I2C_CR2_STA_LCK_un_locked_w | I2C_CR2_PSTA_mask_w
};
Функции для ведомого устройства (Slave)
Работа ведомого устройства реализована на основе неблокирующих функций с применением простейшего конечного автомата и прерывания модуля I2Cx.
Функция i2c_write()
записывает данные от 1 до 4 байт в буфер для отправки:
/// Запись данных в буфер отправки (1-4 байта)
inline
void i2c_write(uint32_t id, uint32_t data, uint8_t len) {
RB(id+( I2C0_CR2_b1 -I2C0_Base)) = (len & I2C_CR2_BUF_CNT_mask_b1); // BUF_CNT
RW(id+( I2C0_DAT_w -I2C0_Base)) = data;
}
Функция i2c_read()
считывает данные (4 байта) из буфера после приема:
/// Чтение буфера приема (4 байта)
inline
uint32_t i2c_read(uint32_t id) {
return RW(id+( I2C0_DAT_w -I2C0_Base));
}
Для передачи данных объемом более 4 байт можно использовать функцию i2c_writebuf()
:
/// Запись данных из буфера программы в буфер отправки.
/// На входе: *p - текущее положение в буфере (указывает на следующий байт после отправленного).
/// На выходе: *p - новое значение, увеличенное на число отправленных байт.
/// Возвращает оставшееся число байт.
uint32_t i2c_writebuf(uint32_t id, const void* buf, uint32_t* p, uint32_t len) {
uint8_t m;
int32_t n;
n = len - *p;
if (n > 0) {
m = n < 4 ? n : 4;
RB(id+( I2C0_CR2_b1 -I2C0_Base)) = (m & I2C_CR2_BUF_CNT_mask_b1); // BUF_CNT
switch (m) {
case 1: RB(id+( I2C0_DAT_b0 -I2C0_Base)) = *((uint8_t*)buf + *p); break;
case 2: RH(id+( I2C0_DAT_h0 -I2C0_Base)) = *(uint16_t*)((uint8_t*)buf + *p); break;
default:
RW(id+( I2C0_DAT_w -I2C0_Base)) = *(uint32_t*)((uint8_t*)buf + *p); break;
}
*p += m;
}
return len-*p;
}
Функция за один вызов может отправить не более 4 байт и предназначена для работы в подпрограмме обработки прерывания. Функция вызывается несколько раз до тех пор, пока весь буфер длиной len
байт не будет передан на отправку. На входе также указывается адрес буфера buf
, из которого нужно передать данные и адрес переменной p
, хранящей текущую позицию в буфере. Содержимое буфера и значение len
не должны изменяться пока все данные не будут отправлены. Перед первым вызовом по адресу p
должен быть записан 0. Функция возвращается число оставшихся неотправленных байт.
Аналогично для приема данных объемом более 4 байт можно использовать функцию i2c_readbuf()
:
/// Чтение принятых данных и их запись в указанный буфер.
/// На входе: *p - текущее положение в буфере (указывает на следующий байт после записанного).
/// На выходе: *p - новое значение, увеличенное на число считанных байт.
/// Возвращает фактическое число считанных байт.
uint8_t i2c_readbuf(uint32_t id, void* buf, uint32_t* p) {
uint8_t n = RB(id+( I2C0_CR2_b1 -I2C0_Base)) & I2C_CR2_BUF_CNT_mask_b1; // BUF_CNT
*(uint32_t*)((uint8_t*)buf + *p) = RW(id+( I2C0_DAT_w -I2C0_Base));
*p += n;
return n;
}
Функция за один вызов может принять не более 4 байт. Функция вызывается каждый раз после срабатывания флага RXF до тех пор, пока не будет принят весь объем данных. На входе также указывается адрес буфера buf
, в который нужно записывать данные и адрес переменной p
, хранящей текущую позицию в буфере. Перед первым вызовом по адресу p
должен быть записан 0. Функция возвращается число байт, принятых за один вызов.
Библиотека для работы с часами DS3231
Функции для работы с м/с DS3231 помещены в файл ic/ds3231.c
. В файле ic/ds3231.h
объявлены идентификатор модуля DS3231_PORT
и адрес ведомого DS3231_ADDR
:
#define DS3231_PORT I2C0_id // I2C module base address
#define DS3231_ADDR 0xD0 // 0b1101000X, X - direction: 0 - write, 1 - read.
Для удобства проверки и обработки таймаута созданы макросы:
#define TMOUT_CHECK if (i2c_get_tmout(DS3231_PORT)) return;
#define TMOUT_CHECK2 if (i2c_get_tmout(DS3231_PORT)) goto failure;
В файле также объявлено перечисление DS3231_Registers
с адресами регистров DS3231 и прототипы функций для работы с часами. Функция ds3231_read()
выполняет запрос значения одного регистра reg
(все регистры 8-разрядные):
uint8_t ds3231_read(uint8_t reg) {
uint32_t d;
i2c_master_startw(DS3231_PORT, DS3231_ADDR); TMOUT_CHECK2
i2c_master_send(DS3231_PORT, I2C_START, 1, reg); TMOUT_CHECK2
i2c_wait_start(DS3231_PORT); TMOUT_CHECK2
i2c_master_startr(DS3231_PORT, DS3231_ADDR); TMOUT_CHECK2
d=i2c_master_recv(DS3231_PORT, I2C_STOP, 1); TMOUT_CHECK2
i2c_wait_stop(DS3231_PORT); TMOUT_CHECK2
return d;
failure:
return 0;
Вначале формируется кадр SLA+W с одним байтом-адресом, затем отдельно передается байт с номером запрашиваемого регистра и автоматическим формированием RSTART по окончанию передачи. Перед дальнейшими действиями ожидается переход шины в это состояние. Далее формируется кадр SLA+R, в котором запрашивается 1 байт данных, после чего формируется состояние STOP и ожидается его фактическая генерация. На каждом шаге выполняется проверка таймаута.
Альтернативным методом чтения регистров DS3231 является мультибайтовый запрос (функция ds3231_read_multi()
), в котором ведущий указывает номер первого регистра, а затем выполняет чтение данных первого и последующих по порядку регистров до тех пор, пока не сформирует состояние NACK:
uint32_t ds3231_read_multi(uint8_t first_reg, uint8_t len) {
uint32_t d;
i2c_master_startw(DS3231_PORT, DS3231_ADDR); TMOUT_CHECK2
i2c_master_send(DS3231_PORT, I2C_START, 1, first_reg); TMOUT_CHECK2
i2c_wait_start(DS3231_PORT); TMOUT_CHECK2
i2c_master_startr(DS3231_PORT, DS3231_ADDR); TMOUT_CHECK2
d=i2c_master_recv(DS3231_PORT, I2C_STOP, len & 0x07); TMOUT_CHECK2
i2c_wait_stop(DS3231_PORT); TMOUT_CHECK2
return d;
failure:
return 0;
}
В данной функции максимальное число регистров модуля DS3231, которые можно прочитать, ограничено числом 4 по размеру буфера приема (4 байта). Состояние NACK модуль I2C сгенерирует автоматически после приема указанного числа байт len
перед формированием состояния STOP. Аналогично реализованы два вида функций записи данных в регистры DS3231:
void ds3231_write(uint8_t reg, uint8_t val) {
i2c_master_startw(DS3231_PORT, DS3231_ADDR); TMOUT_CHECK
i2c_master_send(DS3231_PORT, I2C_STOP, 2, ((uint32_t)val << 8) | reg ); TMOUT_CHECK
i2c_wait_stop(DS3231_PORT); TMOUT_CHECK
}
void ds3231_write_multi(uint8_t first_reg, uint8_t len, uint32_t vals) {
i2c_master_startw(DS3231_PORT, DS3231_ADDR); TMOUT_CHECK
i2c_master_send(DS3231_PORT, I2C_NOOPTS, 1, first_reg); TMOUT_CHECK
i2c_master_send(DS3231_PORT, I2C_STOP, len & 0x07, vals); TMOUT_CHECK
i2c_wait_stop(DS3231_PORT); TMOUT_CHECK
}
Запись реализуется проще, поскольку все данные можно передать одним пакетом после кадра SLA+W. Также реализованы две функции "верхнего" уровня для установки и запроса времени в двоично-десятичном формате (BCD) 0xHHMMSS:
/// Установка времени в формате BCD: 0xHHMMSS
void clock_set_bcd(uint32_t t) {
ds3231_write(REG_SEC, t);
ds3231_write(REG_MIN, t >> 8);
ds3231_write(REG_HOUR,t >> 16);
}
/// Считывание времени в формате BCD: 0xHHMMSS
uint32_t clock_get_bcd() {
uint8_t d[4];
d[0]=ds3231_read(REG_SEC);
d[1]=ds3231_read(REG_MIN);
d[2]=ds3231_read(REG_HOUR);
d[3]=0;
return *(uint32_t*)d;
}
Здесь для наглядности приведены версии функций с побайтовыми запросами, которые компилируются при определении макроса DS3231_ONEBYTE_MODE
. Если макрос не определен, компилируются мультибайтовые версии этих двух функций.
Реализация ведущего устройства
Перейдем к реализации ведущего устройства в аппаратном режиме (Buffer mode). В качестве ведомого будем использовать модуль часов на основе м/с DS3231. Тестовые функции ведущего помещены в файл src/i2c_test.c
, а вызываются они из прикладной части в функции app()
.
Перед началом работы вызывается функция настройки модуля i2c_test_master_setup()
:
void i2c_test_master_setup() {
i2c_init(DS3231_PORT);
HW_I2C0_SETSCL;
HW_I2C0_SETSDA;
// Настройка режима работы
i2c_setup_mode(DS3231_PORT,
I2C_CR0_PDRV_SEL_1t_w |
I2C_CR0_BUF_EN_enable_w | // Режим работы через буфер
I2C_CR0_MDS_i2c_w // I2C : Single/Multi-Master/ Slave mode
);
}
В ней происходит настройка тактирования, установка выводов SCL и SDA, а затем задается режим работы модуля I2C. Далее вызывается одна из трех функций, выполняющих запросы:
i2c_test_master_w1r_ds3231()
— запрос времени с DS3231;i2c_test_master_wN()
— установка произвольного регистра в ведомом;i2c_test_master_w1r()
— запрос произвольного количества байт в ведомом.
Рассмотрим первую функцию:
void i2c_test_master_w1r_ds3231() {
uint32_t d;
while (1) {
d=clock_get_bcd();
if (i2c_get_tmout(I2C_PORT)) {
d=i2c_get_status(I2C_PORT);
debug32hex('S',d); i2c_print_status(d);
led2_flash();
i2c_clr_status(I2C_PORT, I2C_STA_TMOUTF_mask_w);
}
else {
debug32hex('T',d);
}
delay_ms(1000);
}
}
В цикле примерно 1 раз в секунду опрашиваются регистры, содержащие секунды, минуты и часы. Если не происходит срабатывание таймера таймаута, полученное 24-битное число в формате 0xHHMMSS выводится в терминал. Если таймер срабатывает, в терминал выводится статус модуля I2C со всеми флагами в многострочном текстовом формате (по 8 флагов в строку). Для этого используется отладочная функция разработанной библиотеки i2c_print_status()
, которая компилируется только при установленном макросе I2C_DEBUG
. После срабатывания таймера флаг TMOUTF в программе сбрасывается. Для контроля также включается светодиод D2.
Подключаем часы DS3231 и смотрим результат работы в терминале. Через несколько секунд разрываем соединение с ведомым, затем вновь его восстанавливаем:
Hello
T 00180353
T 00180354
T 00180355
T 00180356
S F8000010
----- ----- ----- ----- ----- ----- ----- -----
----- ----- ----- ----- ----- ----- ----- -----
----- ----- ----- TMOUT ----- ----- ----- -----
S F8000010
----- ----- ----- ----- ----- ----- ----- -----
----- ----- ----- ----- ----- ----- ----- -----
----- ----- ----- TMOUT ----- ----- ----- -----
T 00180359
T 00180400
T 00180401
Видно, что ведущий работает и получает корректное значение всех трех байт времени. Также работает таймер TMO при нарушении связи: видно, что в слове статуса устанавливается флаг таймаута (бит 4), а старший байт отображает состояние модуля 0xF8 (недокументированная функция). С помощью цифрового анализатора Saleae Logic посмотрим временную диаграмму работы шины (канал 0 (белый) — сигнал SDA, канал 1 (коричневый) — сигнал SCL):

Запрос начинается с кадра SLA+W, адрес подтверждается (ACK), затем отправляется номер регистра секунд (0x00). Далее, после состояния RSTART (зеленый кружок) формируется кадр SLA+R, адрес подтверждается (ACK) и далее происходит считывание трех байт с подтверждением первых двух со стороны ведущего. Последний байт не подтверждается и ведущий генерирует состояние STOP (красный кружок).
Теперь посмотрим, что происходит в момент обрыва соединения с ведомым:

В результате обрыва ведущий не получил ACK в кадре SLA+W, прервал передачу данных и перешел в режим ожидания. Уменьшим масштаб и посмотрим длительность состояния низкого уровня на линии SCL:

Время ожидания составило установленную 1 мс, после чего модуль "сбросился" и перешел в начальное состояние. Тест пройден успешно.
Реализация ведомого устройства
Общая функция
Перейдем к реализации ведомого устройства на МК MG32F02A032. Модуль I2C также будет работать в аппаратном режиме. Для ведомого устройства удобно использовать прерывания, поскольку события на шине I2C для него всегда будут асинхронными. В проекте сконфигурирована цель slave
с отдельным главным файлом прикладной части app_slave.c
, в котором собраны все функции тестирования и обработчики прерывания. В файле определены идентификатор порта I2C_PORT
и глобальные переменные, отвечающие за буфер:
#define I2C_PORT I2C0_id
#define BUFLEN 16
/// Буфер данных слэйва
uint8_t buf[BUFLEN]; ///< данные буфера
uint32_t bufp; ///< указатель буфера
uint32_t bufn; ///< размер данных на отправку
Указатель bufp
содержит текущую позицию в буфере buf
для операций чтения или записи и должен быть обнулен перед началом работы. Рассмотрим основную функцию i2c_test_slave()
:
/// Общая настройка режима слэйва
void i2c_test_slave() {
uint32_t i;
for (i=0; i< BUFLEN; i++) buf[i]=((i+1)<< 4) | (i+1); // инициализация буфера
// Настройка тактирования:
i2c_init(I2C_PORT);
// Настройка выводов I2C:
HW_I2C0_SETSCL; HW_I2C0_SETSDA;
// Настройка режима работы
i2c_setup_mode(I2C_PORT,
I2C_CR0_PDRV_SEL_1t_w |
I2C_CR0_BUF_EN_enable_w | // Режим работы через буфер
I2C_CR0_MDS_i2c_w | // I2C : Single/Multi-Master/ Slave mode
I2C_CR0_SADR_EN_enable_b0 // Включаем детектор адреса слэйва
);
// Адрес слэйва:
RB(I2C0_SADR_b0) = DS3231_ADDR; // установка адреса
RB(I2C0_MASK_b0) = 0xFE; // настройка маски
// Отключаем задержку сигнала SCL от слэйва
RB(I2C0_CR0_b1) |= I2C_CR0_SCLS_DIS_disable_b1;
// Устанавливаем обработчик прерываний:
SVC2(SVC_HANDLER_SET,28,i2c_hdl_w1rN);
// Включаем прерывания в модуле:
RW(I2C0_INT_w) =
I2C_INT_BUF_IE_enable_w | // flags: RXF, TXF, RSTRF, STOPF, SADRF
I2C_INT_IEA_enable_w;
// Включаем прерывание в модуле NVIC:
RW(CPU_ISER_w) = (1 << 28); // SETENA 28
while (1) {
// Проверка ACNT:
if (RB(I2C0_CR2_b2) & 0x07) led2_on(); else led2_off();
}
}
В начале функции инициализируется буфер значениями 0x11, 0x22, и т.д. Далее настраивается тактирование, назначаются выводы модуля и устанавливается собственный адрес устройства, такой же, как и у часов DS3231. Затем устанавливается обработчик прерывания и настраивается прерывание по флагу BUFF, включающее все события (кроме ошибок), которые могут произойти в режиме Buffer mode. С этого момента функция ведомого устройства будет целиком определяться обработчиком прерывания.
Передача данных по запросу ведущего
Начнем с обработчика i2c_hdl_w1rN()
, реализующего отправку произвольного числа байт из своего буфера. Протокол обмена следующий: ведущий отправляет пакет SLA+W с одним байтом, в котором указывает число запрашиваемых байт. Ведомый после получения кадра SLA+R начинает отправку запрошенных данных. Ведущий подтверждает прием каждого байта, кроме последнего, после чего формирует состояние STOP. Рассмотрим код функции:
/// Обработчик прерывания I2C0
void i2c_hdl_w1rN() {
uint32_t d; // флаги
uint32_t n;
led1_on();
d=i2c_get_status(I2C_PORT);
if (d & I2C_STA_SADRF_mask_w) {
if (d & I2C_STA_RWF_read_w) {
// Мастер читает
}
else {
// Мастер пишет
}
}
if (d & I2C_STA_TXF_mask_w) {
//if (bufp==bufn) led2_on();
if (bufn > bufp) {i2c_writebuf(I2C_PORT,buf,&bufp,bufn); led1_on();}
}
if (d & I2C_STA_RXF_mask_w) {
n=i2c_read(I2C_PORT) & 0xFF;
bufn = (n <= BUFLEN) ? n : BUFLEN;
bufp=0;
i2c_writebuf(I2C_PORT,buf,&bufp,bufn); // Подготавливаем данные для отправки (копируем в буфер максимум байт (4))
}
if (d & I2C_STA_STOPF_mask_w) {
}
if (d & I2C_STA_RSTRF_mask_w) {
}
led1_off();
i2c_clr_status(I2C_PORT, 0x00ffffff);
}
Светодиодные выводы МК будем использовать для отладки и подключим к цифровому анализатору вместе с сигналами SCL и SDA. Сигнал со светодиода D1 будет показывать момент возникновения прерывания, а сигнал с D2 — срабатывание некоторых флагов. В зависимости от установленных флагов в функции выполняются те или иные действия. Одновременно могут быть установлены несколько флагов.
При установленном флаге SADRF далее определяется тип кадра SLA (чтение или запись) и выполняются какие-либо прикладные действия (показано в качестве шаблона). При установленном флаге RXF считывается младший байт буфера приема в переменную n
— запрашиваемое число байт, которое затем проходит формальную проверку на непревышение BUFLEN
. Далее в буфер на отправку передается первая порция данных через функцию i2c_writebuf()
. При установленном флаге TXF вновь вызывается функция i2c_writebuf()
для передачи оставшейся порции данных. В качестве шаблона также показан код для определения установки флагов STOPF и RSTRF. В конце функции сигнал D1 возвращается в 0 и сбрасываются все флаги.
На стороне ведущего устройства в файле i2c_test.c
для данного теста будем использовать функцию i2c_test_master_w1r()
, в которой раз в секунду запрашивается заданное число байт (9) с помощью вспомогательной функции i2c_test_master_req()
. Полученные данные помещаются в глобальный буфер (аналогично ведомому) и после приема последнего байта выдаются в терминал через отладочную функцию debugbuf()
. Состояние таймаута проверяется аналогично функции i2c_test_master_w1r_ds3231()
. Код этих двух функций читатель может посмотреть в репозитории.
Подключаем вместо DS3231 МК MG32F02A032 и смотрим результат в терминале ведущего:
11 22 33 44 55 66 77 88 99
11 22 33 44 55 66 77 88 99
11 22 33 44 55 66 77 88 99
Теперь посмотрим временные диаграммы (канал 2 (красный) — сигнал D1, канал 3 (желтый) — сигнал D2):

В функции i2c_test_slave()
после разрешения прерываний мы оставили циклический опрос счетчика I2Cx_ACNT
: когда его значение становится отлично от нуля, сигнал D2 переходит в состояние высокого уровня. Проанализируем диаграммы. Прерывание сработало всего три раза: первый раз по флагу RXF после фиксирования состояния RSTART, что показывает та же диаграмма в большем масштабе:

В этот момент мы подготовили данные для отправки, не дожидаясь следующего прерывания. В противном случае ведущий получил бы некорректные данные (читатель может это проверить самостоятельно). Сразу после получения первого байта со значением 0x09 и подтверждением ACK значение ACNT стало равно 1, но после прочтения вернулось в 0.
Второй раз прерывание сработало по флагу TXF после кадра SLA+R и подтверждения ACK в момент начала передачи первого байта от ведомого:

В этот момент первые 4 байта уже были переданы в промежуточный буфер для отправки и основной буфер I2Cx_DAT
освободился для новых данных, что привело к установке флага. Теперь мы записали в этот буфер следующие 4 байта. После фактической передачи одного байта (0x11) в сдвиговый регистр значение счетчика ACNT изменилось на 1 и дальше увеличивалось с отправкой каждого последующего байта.
Третий раз прерывание сработало также по флагу TXF после передачи очередных 4 байт в промежуточный буфер. Значение счетчика сбросилось в 0. Мы записали в буфер последний, 9-й байт. Следующие 4 байта 0x55-0x88 были отправлены из промежуточного буфера сразу. После этого, когда счетчик ACNT вновь сбросился, последний байт 0x99 был также скопирован в промежуточный буфер и отправлен.
Прием данных от ведущего
Теперь рассмотрим обработчик i2c_hdl_wN()
со следующим алгоритмом: slave-устройство принимает один пакет с данными и подтверждает каждый байт до тех пор, пока ведомый их передает, после чего дамп буфера выводится в терминал:
/// Обработчик прерывания I2C0
void i2c_hdl_wN() {
uint32_t d;
led1_on();
d=i2c_get_status(I2C_PORT);
if (d & I2C_STA_SADRF_mask_w) {
if (d & I2C_STA_RWF_read_w) { // Master reads
}
else { // Master writes
bufp=0;
}
}
if (d & I2C_STA_RXF_mask_w) {
i2c_readbuf(I2C_PORT,buf,&bufp);
}
if (d & I2C_STA_STOPF_mask_w) {
debugbuf(buf,bufp);
}
led1_off();
i2c_clr_status(I2C_PORT, 0x00ffffff);
}
На стороне ведущего устройства в файле i2c_test.c
для данного теста будем использовать функцию i2c_test_master_wN()
, в которой раз в секунду ведущий передает сначала 1 байт с 0x01, затем последовательность из 4 байт 0xA1, 0xB2, 0xC3, 0xD4.
Смотрим результат в терминале ведомого:
01 A1 B2 C3 D4
01 A1 B2 C3 D4
01 A1 B2 C3 D4
Теперь посмотрим временные диаграммы:

Первый раз прерывание сработало по флагу SADRF после получения кадра SLA+W до генерации подтверждения ACK. После приема первого байта из сдвигового регистра счетчик ACNT принял значение 1. Второй раз прерывание сработало по флагу RXF после приема первых 4 байт, данные из буфера были прочитаны, после чего счетчик ACNT сбросился. После приема последнего байта счетчик ACNT вновь принял значение 1. Третий раз прерывание сработало по двум флагам STOPF и RXF. Мы прочитали из буфера последний байт и стали выполнять длительную процедуру по выводу буфера в терминал, на что потратили примерно 1,4 мс (диаграмма с таким масштабом не показана). Поэтому сигналы D1 и D2 сразу не вернулись в состояние 0.
Заключение
Мы экспериментально проверили основные сценарии взаимодействия ведущего и ведомого устройств в максимально автоматизированном аппаратном режиме работы модулей I2C. Для более специфических задач можно использовать низкоуровневый режим Byte mode. Модуль показал полную работоспособность и высокую функциональность одновременно с относительной простой использования. Предложенные библиотеки могут быть взяты за основу для разработки собственных прошивок МК. За рамками статьи остались следующие вопросы, которые читатель может изучить по User Guide:
управление активной подтяжкой для линий SCL и SDA (16.12.1),
пробуждение модуля из состояния STOP (16.12.3),
обработка ошибок (16.13),
применение DMA (16.14),
режим Monitor.