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 бита | чтение |
Транзакции ведущего — те же, что для любого датчика:
- запись:
Sадрес+W · указатель · данные… ·P; - чтение:
Sадрес+W · указатель ·Srадрес+R · данные… (последний — NACK) ·P.
Два правила, без которых ведомый выдаёт «рваные» значения:
- Запись — в теневую копию, в рабочую — по STOP. Уставка из двух байт пишется двумя событиями; если прошивка заглянет между ними, увидит новый младший байт со старым старшим. Поэтому байты копятся в тени и переносятся целиком по STOP.
- Чтение — из снимка на момент адреса. Счётчик 0x0000FFFF, который перешагнул в 0x00010000 посреди чтения: без снимка ведущий получил бы 0x0001FFFF — старшие байты от нового значения, младшие от старого. Со снимком — ровно 0x0000FFFF.
Проверка логики в модели: запись 0x0F, 0xAA, 0xBB, 0xCC кладёт 0xAA в 0x0F,
дальше указатель по кругу уходит в 0x00 и 0x01 — оба «только чтение», два байта
отброшены (WHO_AM_I остался 0x5A); до STOP регистр 0x0F ещё старый (0x00), после — 0xAA.
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:
- приём: не прочитали
DRк концу следующего байта — новый байт потерян; - передача: не записали
DRвовремя — тот же байт уходит дважды.
Та же модель без растягивания:
| Шина | 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 |
Как получилось 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-ведомых:
- пустой байт (или несколько) между командой и ответом — протокол даёт время;
- ответ заранее: ведомый заранее кладёт в DMA-буфер «текущее состояние», ведущий читает его сразу, без команды;
- NSS как рамка: спад NSS — прерывание «начало транзакции», подъём — «конец»; разбор команды — после рамки, не посреди неё.
Флага «не успел записать» у 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 Итоги
- Ведомый как датчик: первый байт — указатель, дальше автоинкремент; запись — в тень до STOP, чтение — из снимка. Без этого 0x0000FFFF читается как 0x0001FFFF.
- Пять событий I²C: ADDR (SR1 → SR2), RXNE, TXE, AF, STOPF (SR1 → запись CR1).
- С растягиванием данные верны при любой медлительности, платит время шины: на 400 кГц при реакции 100 мкс запись — 350 мкс вместо 95.
- Без растягивания запас — 9 битов (22,5 мкс на 400 кГц); опоздал — байт потерян или повторён: уставка 2500 превращается в 777 молча.
- SPI-ведомый не может ответить в том же байте: без пустого байта на 10 МГц остаётся 8 тактов, с пустым — 143.
Полные исходники
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» — интерфейс, устроенный куда проще и работающий на порядок быстрее.