Продолжая цикл публикаций по микроконтроллерам на ядре 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 приведена на рисунке.

Функциональная схема модуля I2C
Функциональная схема модуля I2C

Каждый модуль 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.

Временная диаграмма сигналов SCL и SDA
Временная диаграмма сигналов 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 согласно следующей таблице:

I2Cx_MDS

I2Cx_BUF_EN

Режим работы

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

Разряд I2Cx_CR2

Название

Описание

0

I2Cx_STA

Генерация состояния START

1

I2Cx_STO

Генерация состояния STOP

2

I2Cx_AA

Генерация состояния ACK

3

I2Cx_CMD_TC

Разрешение генерации состояний при установке битов PSTA, PSTO, PAA

4

I2Cx_STA_LCK

Разблокировка записи в разряды I2Cx_STA и I2Cx_PSTA

5

I2Cx_STO_LCK

Разблокировка записи в разряды I2Cx_STO и I2Cx_PSTO

6

I2Cx_AA_LCK

Разблокировка записи в разряды I2Cx_AA и I2Cx_PAA

24

I2Cx_PSTA

Генерация состояния START после завершения текущей операции

25

I2Cx_PSTO

Генерация состояния STOP после завершения текущей операции

26

I2Cx_PAA

Генерация состояния 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)

На следующем рисунке показан алгоритм работы модуля в данном режиме.

Алгоритм работы модуля I2C в режиме Buffer mode - Master
Алгоритм работы модуля I2C в режиме Buffer mode - 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, что может привести к приему лишних данных.

Для ведущего устройства можно рекомендовать следующий алгоритм отправки данных:

  1. Дать команду STA с одновременной записью 1 в поле I2Cx_CR2.I2Cx_BUF_CNT.

  2. В регистр I2Cx_DAT записать адрес ведомого устройства, сдвинутый на 1 разряд влево. Младший бит адреса должен быть нулевым. Таким образом будет сформировано состояние SLA+W.

  3. Ожидать установку флага TXF, т.е. готовность буфера принять новые данные.

  4. В поле I2Cx_CR2.I2Cx_BUF_CNT указать число отправляемых байт (1-4), а также опционально установить бит I2Cx_PSTO или I2Cx_PSTA (вместе с битом I2Cx_CMD_TC), если после отправки требуется автоматически сгенерировать состояние STOP или RSTART соответственно.

  5. Записать новые данные в регистр I2Cx_DAT (после чего начинается фактическая отправка).

  6. Если требуется дальнейшая передача данных (более 4-х байт), повторить пункты 3-5.

  7. При "ручном" методе генерации состояния STOP ожидать активацию флага TSCF (фактическое завершение передачи) и только после этого дать команду STO.

  8. При автоматической генерации состояния STOP перед отправкой следующего кадра (очередной команды STA) необходимо ожидать активацию флага STOPF.

Кроме того, возможно потребуется программный сброс флага TXF перед пунктом 4 в случае отправки нескольких кадров с данными.

Для ведущего устройства можно рекомендовать следующий алгоритм приема данных:

  1. Дать команду STA с одновременной записью 1 в поле I2Cx_CR2.I2Cx_BUF_CNT.

  2. В регистр I2Cx_DAT записать адрес ведомого устройства, сдвинутый на 1 разряд влево. Младший бит адреса должен быть установлен. Таким образом будет сформировано состояние SLA+R.

  3. Ожидать установку флага SADRF, т.е. завершение формирования состояния SLA+R.

  4. В поле I2Cx_CR2.I2Cx_BUF_CNT указать число ожидаемых байт (1-4), а также опционально установить бит I2Cx_PAA и(или) I2Cx_PSTO (вместе с битом I2Cx_CMD_TC), если после приема последнего байта требуется автоматически сгенерировать подтверждение и(или) состояние STOP соответственно.

  5. Ожидать установку флага RXF, т.е. готовность данных в буфере.

  6. Прочитать данные из регистра I2Cx_DAT.

  7. Если требуется дальнейший прием данных (более 4-х байт), повторить пункты 4-6.

  8. При "ручном" методе генерации состояния STOP дать команду STO.

  9. При автоматической генерации состояния STOP перед отправкой следующего кадра (очередной команды STA) необходимо ожидать активацию флага STOPF.

Ведомое устройство (Slave)

Модуль переходит в режим ведомого устройства (slave) после установки одного из битов включения механизма детектирования адреса I2Cx_SADR_EN или I2Cx_SADR2_EN. Алгоритм работы модуля показан на следующем рисунке.

Алгоритм работы модуля I2C в режиме Buffer mode - Slave
Алгоритм работы модуля I2C в режиме Buffer mode - Slave

При получении кадра 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, после чего в программе можно прочитать и проанализировать данные.

Для ведомого устройства можно рекомендовать следующий алгоритм работы:

  1. Установить адрес или адреса (при необходимости, и маску) ведомого устройства и переключиться в режим slave.

  2. Опционально: ожидать срабатывание флага SADRF (очевидно, после получения кадра SLA+W с командой) для детектирования факта обращения к устройству и подготовки к выполнению запроса.

  3. Ожидать срабатывание флага RXF, после чего прочитать данные из регистра I2Cx_DAT, количество принятых байт взять из поля I2Cx_CR2.I2Cx_BUF_CNT.

  4. Проанализировать данные. Если требуется дальнейший прием данных, перейти на предыдущий пункт.

  5. Если согласно полученному запросу требуется отправить данные в ответ, количество байт указать в поле I2Cx_CR2.I2Cx_BUF_CNT, затем поместить их в регистр I2Cx_DAT. Это нужно сделать не дожидаясь активации флага SADRF в следующем кадре SLA+R.

  6. Если требуется дальнейшая отправка данных, ожидать срабатывание флага TXF и перейти на предыдущий пункт.

  7. Опционально: ожидать срабатывание флагов STOPF или RSTRF.

Дополнительные функции

Управление линией SCL для Slave

В режиме Buffer mode в процессе обработки запросов ведомому может потребоваться дополнительное время на формирование ответа, например, на получение данных, которые затребовал ведущий. В этом случае согласно спецификации интерфейса I2C ведомое устройство может задержать тактирование от ведущего путем удержания линии SCL в состоянии низкого уровня. Функция удержания линии SCL по-умолчанию включена и работает в следующих случая:

  • в режиме приема буфер уже заполнен, но программа еще не прочитала из него данные;

  • в режиме передачи буфер уже опустошен, но программа еще не записала в него новые данные.

Для выключения данной функции нужно установить бит I2Cx_CR0.I2Cx_SCLS_DIS.

Таймер таймаута

В состав модулей I2Cx входит 8-разрядный таймер таймаута TMO. Таймер тактируется сигналом CK_I2Cx_TMO (см. п. Тактирование). На рисунке показана функциональная схема таймера.

Таймер таймаута модуля I2C
Таймер таймаута модуля I2C

В зависимости от значения поля 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, приведена на следующем рисунке.

Схема формирования флагов событий модуля I2C
Схема формирования флагов событий модуля I2C

Флаги событий собраны в регистре I2Cx_STA, а биты разрешения прерываний — в регистре I2Cx_INT. Перечень событий и соответствующих флагов прерываний приведен в следующей таблице.

Разряд I2Cx_STA

Флаг события

Бит прерывания

Название события

Описание

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.

Для включения генерации прерывания необходимо:

  1. Выбрать событие (или события) в регистре I2Cx_INT.

  2. Разрешить прерывание самого модуля установкой бита I2Cx_INT.I2Cx_IEA.

  3. Разрешить прерывание IRQ от модуля в контроллере прерываний NVIC в регистре CPU_ISER.

Тестирование

Аппаратная часть

Целью тестирования является проверка работы МК в роли ведущего и ведомого устройства шины I2C. В роли ведущего будет применяться МК MG32F02A064AD48, в роли ведомого — (1) "эталонный" Slave — часы реального времени DS3231 и (2) МК MG32F02A032AT20. Схема подключения микроконтроллеров показана на следующем рисунке.

Схема подключения МК для тестирования модулей I2C
Схема подключения МК для тестирования модулей I2C

Все устройства запитываются от стабилизатора напряжением 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):

Временная диаграмма запроса времени с часов DS3231
Временная диаграмма запроса времени с часов DS3231

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

Теперь посмотрим, что происходит в момент обрыва соединения с ведомым:

Временная диаграмма запроса времени с часов DS3231
Временная диаграмма запроса времени с часов DS3231

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

Временная диаграмма запроса времени с часов DS3231
Временная диаграмма запроса времени с часов DS3231

Время ожидания составило установленную 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):

Временная диаграмма запроса 9 байт с МК
Временная диаграмма запроса 9 байт с МК

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

Временная диаграмма запроса 9 байт с МК
Временная диаграмма запроса 9 байт с МК

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

Второй раз прерывание сработало по флагу TXF после кадра SLA+R и подтверждения ACK в момент начала передачи первого байта от ведомого:

Временная диаграмма запроса 9 байт с МК
Временная диаграмма запроса 9 байт с МК

В этот момент первые 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

Теперь посмотрим временные диаграммы:

Временная диаграмма отправки 5 байт
Временная диаграмма отправки 5 байт

Первый раз прерывание сработало по флагу 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.

Комментарии (7)


  1. LAutour
    26.10.2022 21:16

    Если искать на efind.ru MG32F02 — там как-то очень не густо.


    1. slog2
      26.10.2022 22:28

      А где сейчас густо? И чтобы не левак подсунули.


    1. Gemerus
      27.10.2022 03:22
      +2

      MG32F02A064AD48 сейчас есть в наличии в РФ в количестве 3.8К штук. Но, тем не менее, пока что они выглядят бессмысленно сложными для освоения, пока есть возможность достать STM32 или GD32. Есть еще AT32, там очень хорошая цена на F4 серию. В общем, этих подобий STM32 сейчас целый зоопарк.


      1. progchip666
        27.10.2022 08:06

        Уважаемый, а в какой среде рекомендуете код на GD32 отлаживать. CUBE я так понимаю для этого не годится?


      1. Dima_Sharihin
        27.10.2022 08:24

        Artery вызывает тоску после мегавина. Последний кажется реально более толково спроектирован даже по сравнению с STMами.


    1. reug Автор
      27.10.2022 14:17

      По информации от ЭФО, контроллеры серий MG32F02 и MG32F103 (Cortex-M3) доступны для заказа и поставляются в Россию, популярные позиции всегда есть на складе.


      1. reug Автор
        27.10.2022 14:20

        В следующем году должны появиться МК Megawin на ядре Cortex-M3 с flash до 512 кбайт и CAN, информации по объему ОЗУ пока нет.