Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F407 · уровень продвинутый · 11 мин чтения
Открыть в интерактивном справочнике

17а. Микроконтроллер как ведомое устройство: I²C и SPI

Обычно микроконтроллер — ведущий: он опрашивает датчики. Но часто он сам становится «датчиком» для другого: плата расширения, модуль в стойке, сопроцессор при Linux-плате. Тогда такт задаёт чужой ведущий, и главный вопрос меняется — не «как отправить», а «успею ли я ответить».

В разделе — ведомое устройство I²C с картой регистров (как у настоящих датчиков) и подсчёт времени на ответ для SPI. Логика карты регистров (regmap.c) — одна и та же в прошивке и в модели шины на компьютере; обработчик прерываний для F407 собран с заголовками CMSIS. Правила блоков — по справочному руководству F4. Файлы — _verify/slave17a/, сверяет _verify/slave17a_check.py.

Как это называется в документации

Оригинал Перевод
slave transmitter / slave receiver ведомый передатчик / приёмник
clock stretching растягивание такта ведомым
NOSTRETCH (clock stretching disable) запрет растягивания такта
ADDR, STOPF, AF (slave events) адрес совпал, STOP, ведущий ответил NACK
register map, auto-increment pointer карта регистров, указатель с автоинкрементом
overrun / underrun (OVR, NOSTRETCH = 1) потерянный / повторённый байт без растягивания
dummy byte пустой байт, чтобы дать ведомому время

17а.1 Как выглядит ведомый снаружи: карта регистров

Ведущему удобнее всего, когда ведомый похож на обычный датчик: у него есть регистры, первый байт после адреса — номер регистра, дальше указатель растёт сам. Наша карта из 16 байт:

Адрес Регистр Доступ
0x00 WHO_AM_I = 0x5A чтение
0x01 STATUS: число принятых записей чтение
0x02 CTRL чтение и запись
0x04…0x05 SETPOINT, 16 бит, младший байт первым чтение и запись
0x08…0x0B COUNTER, 32 бита чтение

Транзакции ведущего — те же, что для любого датчика:

Два правила, без которых ведомый выдаёт «рваные» значения:

Проверка логики в модели: запись 0x0F, 0xAA, 0xBB, 0xCC кладёт 0xAA в 0x0F, дальше указатель по кругу уходит в 0x00 и 0x01 — оба «только чтение», два байта отброшены (WHO_AM_I остался 0x5A); до STOP регистр 0x0F ещё старый (0x00), после — 0xAA.

Карта из шестнадцати регистров ведомого по адресам от 0x00 до 0x0F: WHO_AM_I равен 0x5A и STATUS только на чтение, CTRL на чтение и запись, SETPOINT на два байта младшим вперёд, COUNTER на четыре байта только на чтение, остальные свободны; ниже транзакции записи и чтения и два правила — запись в тень до STOP, чтение из снимка, иначе счётчик читается как 0x0001FFFF вместо 0x0000FFFF


17а.2 Пять событий блока I²C

Блок I²C сам ловит свой адрес, сам ставит ACK и сам держит линию, если программа не успела. Программе остаются события (флаги в SR1/SR2):

Событие Когда Как сбросить
ADDR адрес совпал; TRA в SR2 — направление прочитать SR1, затем SR2
RXNE принят байт прочитать DR
TXE нужен следующий байт на передачу записать DR
AF ведущий ответил NACK — дочитал записать 0 в бит AF
STOPF ведущий закончил транзакцию прочитать SR1, затем записать CR1

Обработчик для F407 — перевод флагов в вызовы логики:

void I2C1_EV_IRQHandler(void)
{
    uint32_t sr1 = I2C1->SR1;
    if (sr1 & I2C_SR1_ADDR) {
        uint32_t sr2 = I2C1->SR2;                                          /* SR1, затем SR2 — ADDR сброшен */
        int read = (sr2 & I2C_SR2_TRA) != 0;
        rm_on_addr(&rm, read);
        if (read)
            I2C1->DR = rm_on_tx(&rm);                                      /* первый байт — сразу, пока SCL держится */
        return;
    }
    if (sr1 & I2C_SR1_RXNE)
        rm_on_rx(&rm, (uint8_t)I2C1->DR);
    if ((sr1 & I2C_SR1_TXE) && !(sr1 & I2C_SR1_AF))
        I2C1->DR = rm_on_tx(&rm);
    if (sr1 & I2C_SR1_STOPF) {
        I2C1->CR1 |= I2C_CR1_ACK;                                          /* SR1 уже прочитан; запись CR1 сбрасывает STOPF */
        rm_on_stop(&rm);
    }
}

Инициализация ведомого и обработчик ошибок — из того же файла:

void i2c_slave_init(void)
{
    rm_init(&rm);
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    GPIOB->MODER = (GPIOB->MODER & ~(0xFu << 12)) | (0xAu << 12);        /* PB6, PB7 — альтернативная функция */
    GPIOB->OTYPER |= (1u << 6) | (1u << 7);                                /* открытый сток */
    GPIOB->AFR[0] = (GPIOB->AFR[0] & ~(0xFFu << 24)) | (0x44u << 24);      /* AF4 = I2C1 */
    I2C1->CR2 = 42;                                                        /* частота APB1, МГц */
    I2C1->OAR1 = 0x42u << 1;                                               /* 7-битный адрес 0x42 */
    I2C1->CR2 |= I2C_CR2_ITEVTEN | I2C_CR2_ITBUFEN | I2C_CR2_ITERREN;
    I2C1->CR1 = I2C_CR1_PE;
    I2C1->CR1 |= I2C_CR1_ACK;                                              /* ACK ставится только при PE = 1 */
    NVIC_EnableIRQ(I2C1_EV_IRQn);
    NVIC_EnableIRQ(I2C1_ER_IRQn);
}

void I2C1_ER_IRQHandler(void)
{
    if (I2C1->SR1 & I2C_SR1_AF) {                                          /* ведущий ответил NACK — чтение закончено */
        I2C1->SR1 = ~I2C_SR1_AF;
        rm_on_nack(&rm);
    }
}

Сборка arm-none-eabi-gcc -Os: инициализация и оба обработчика — 268 байт, логика карты регистров — 260.

Лишний байт при чтении. Блок просит следующий байт (TXE), когда предыдущий только начал уходить, — ведомый не знает, что ведущий на нём остановится. Поэтому в DR всегда оказывается на один байт больше, чем прочитано, и указатель убегает вперёд. По AF его возвращают на единицу — иначе следующее «продолжение чтения» пропустит регистр.


17а.3 Растягивание такта: шина ждёт программу

Пока ADDR не сброшен, блок держит SCL низким — ведущий ждёт. Так же, если к концу следующего байта программа не прочитала DR (или не записала его при передаче), блок ставит BTF и держит SCL. Запас, значит, — один байт с ACK: 9 битов, 90 мкс на 100 кГц и 22,5 мкс на 400 кГц.

Модель: ведущий пишет уставку 2500 (адрес, указатель 0x04, два байта) и читает счётчик 0x12345678 (указатель 0x08, повторный START, 4 байта). Процессор отвечает на каждое событие через L мкс:

Шина L Запись из них держал ведомый Чтение держал
100 кГц 2 мкс 382 мкс 2 664 мкс 4
100 кГц 100 мкс 500 мкс 120 890 мкс 230
400 кГц 2 мкс 97 мкс 2 169 мкс 4
400 кГц 30 мкс 140 мкс 45 252,5 мкс 87,5
400 кГц 100 мкс 350 мкс 255 672,5 мкс 507,5

Данные верны во всех строках: уставка 2500, счётчик 0x12345678. Цена медленного ведомого — время шины: на 400 кГц при L = 100 мкс запись идёт 350 мкс вместо 95. Пока L меньше 22,5 мкс, держит шину только событие ADDR (по L на каждый адрес).


17а.4 NOSTRETCH: шина не ждёт — данные портятся

Не всякий ведущий поддерживает растянутый такт. Тогда ведомому ставят NOSTRETCH = 1, и опоздание превращается в ошибку OVR:

Та же модель без растягивания:

Шина L Уставка после записи Счётчик прочитан как
100 кГц 2…30 мкс 2500 — верно 0x345678FF
100 кГц 100 мкс 777 (0x0309) 0x7878FFFF
400 кГц 2…10 мкс 2500 — верно 0x345678FF
400 кГц 30 мкс 777 (0x0309) 0x7878FFFF
400 кГц 100 мкс 1000 — старая 0xFFFFFFFF

Запись уставки на 400 кГц при реакции 30 мкс: с растягиванием шина ждёт обработчик, без растягивания байт 0xC4 теряется

Как получилось 777: байт 0xC4 потерян, 0x09 записался по указателю 0x04 — в младший байт; старший остался 0x03. 0x0309 = 777 вместо 2500 — ни одного сигнала об ошибке ведущему.

Счётчик портится даже при L = 2 мкс: первый байт чтения должен уйти сразу после ACK адреса, а обработчик ADDR ещё не успел записать DR — уходит то, что там лежало (0xFF), а все байты сдвигаются. Без растягивания первый байт кладут в DR заранее, до прихода адреса, а остальные — через DMA.


17а.5 SPI-ведомый: ответ нужен раньше вопроса

У SPI растягивания нет вовсе: такт идёт, пока ведущий его даёт, а биты ответа выдвигаются одновременно с битами команды. Руководство требует, чтобы DR ведомого был готов до первого фронта такта; частота такта для ведомого — не выше fPCLK/2.

Если ведущий шлёт команду и сразу ждёт ответ, у ведомого есть только полпериода SCK между последним битом команды и первым битом ответа. С одним пустым байтом — ещё восемь периодов. В тактах ядра 168 МГц:

SCK Байт Без пустого байта С одним пустым байтом
1 МГц 8000 нс 84 такта 1428 тактов
4 МГц 2000 нс 21 такт 357 тактов
10 МГц 800 нс 8,4 такта 142,8 такта
21 МГц 381 нс 4 такта 68 тактов
42 МГц 190 нс 2 такта 34 такта

Без пустого байта остаётся от 84 тактов на 1 МГц до 2 на 42 МГц — а за это время надо войти в прерывание, прочитать команду, выбрать ответ и записать DR. Уже с нескольких мегагерц это невозможно. Отсюда устройство SPI-ведомых:

Флага «не успел записать» у SPI на F4 нет (UDR — только в режиме I2S): ведомый, не успевший с DR, просто отдаст не те биты, и узнать об этом может только ведущий — по контрольной сумме.


17а.6 Грабли

Уставка получила странное значение без всяких ошибок. NOSTRETCH = 1 и медленный обработчик: пропал байт, остальные записались не туда. Разрешить растягивание или принимать через DMA.

Первый прочитанный байт — 0xFF или «прошлый». Без растягивания DR не был заполнен к первому байту. Класть его заранее.

Каждое второе чтение пропускает регистр. Не возвращён указатель по AF: лишний байт, запрошенный TXE, «съел» регистр.

Многобайтное значение иногда «рвётся». Нет снимка при чтении или тени при записи.

Ведомый не отвечает на адрес. ACK в CR1 аппаратно сбрасывается при PE = 0: записанный до включения блока, он пропадёт. Ставить после PE = 1.

ADDR не сбрасывается. Прочитан только SR1 — нужно ещё SR2. И наоборот: лишнее чтение SR2 без проверки ADDR сбрасывает ADDR, который пришёл между двумя чтениями.

SPI-ведомый отдаёт мусор на высокой частоте. Ответ в том же байте, что и команда, физически невозможен — нужен пустой байт или DMA с готовым ответом.


17а.7 Итоги

Полные исходники

i2c_slave_f4.c — обработчики прерываний ведомого I²C на STM32F4.

/* Раздел 17а: ведомый I²C на STM32F4 (I2C1, PB6 = SCL, PB7 = SDA, адрес 0x42) — обработчики прерываний.
 * Порядок сброса флагов — по RM0090: ADDR — чтение SR1, затем SR2; STOPF — чтение SR1, затем запись CR1;
 * AF — запись 0 в бит AF. Сама логика — в regmap.c, одинаковая для прошивки и модели на компьютере. */
#include "stm32f4xx.h"
#include "regmap.h"

static regmap_t rm;

void i2c_slave_init(void)
{
    rm_init(&rm);
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    GPIOB->MODER = (GPIOB->MODER & ~(0xFu << 12)) | (0xAu << 12);        /* PB6, PB7 — альтернативная функция */
    GPIOB->OTYPER |= (1u << 6) | (1u << 7);                                /* открытый сток */
    GPIOB->AFR[0] = (GPIOB->AFR[0] & ~(0xFFu << 24)) | (0x44u << 24);      /* AF4 = I2C1 */
    I2C1->CR2 = 42;                                                        /* частота APB1, МГц */
    I2C1->OAR1 = 0x42u << 1;                                               /* 7-битный адрес 0x42 */
    I2C1->CR2 |= I2C_CR2_ITEVTEN | I2C_CR2_ITBUFEN | I2C_CR2_ITERREN;
    I2C1->CR1 = I2C_CR1_PE;
    I2C1->CR1 |= I2C_CR1_ACK;                                              /* ACK ставится только при PE = 1 */
    NVIC_EnableIRQ(I2C1_EV_IRQn);
    NVIC_EnableIRQ(I2C1_ER_IRQn);
}

void I2C1_EV_IRQHandler(void)
{
    uint32_t sr1 = I2C1->SR1;
    if (sr1 & I2C_SR1_ADDR) {
        uint32_t sr2 = I2C1->SR2;                                          /* SR1, затем SR2 — ADDR сброшен */
        int read = (sr2 & I2C_SR2_TRA) != 0;
        rm_on_addr(&rm, read);
        if (read)
            I2C1->DR = rm_on_tx(&rm);                                      /* первый байт — сразу, пока SCL держится */
        return;
    }
    if (sr1 & I2C_SR1_RXNE)
        rm_on_rx(&rm, (uint8_t)I2C1->DR);
    if ((sr1 & I2C_SR1_TXE) && !(sr1 & I2C_SR1_AF))
        I2C1->DR = rm_on_tx(&rm);
    if (sr1 & I2C_SR1_STOPF) {
        I2C1->CR1 |= I2C_CR1_ACK;                                          /* SR1 уже прочитан; запись CR1 сбрасывает STOPF */
        rm_on_stop(&rm);
    }
}

void I2C1_ER_IRQHandler(void)
{
    if (I2C1->SR1 & I2C_SR1_AF) {                                          /* ведущий ответил NACK — чтение закончено */
        I2C1->SR1 = ~I2C_SR1_AF;
        rm_on_nack(&rm);
    }
}

uint16_t app_setpoint(void)
{
    return rm_setpoint(&rm);
}

Дальше: раздел 18 «SPI» — интерфейс, устроенный куда проще и работающий на порядок быстрее.

Открыть в интерактивном справочнике