83. I²C и SPI на прототипе: датчик и дисплей
На прототипе к плате подключают датчик по I²C, термометр по 1-Wire и цветной дисплей по SPI — и на каждом шаге есть ловушка, которой нет в учебной схеме. Сами шины на STM32 разобраны в разделах 17 (I²C) и 18 (SPI). Здесь — то, что всплывает при переносе прототипа: адреса, подтяжки, 1-Wire без отдельного блока, память и порядок байтов дисплея, и ошибки в типовых примерах.
Переносимый код собран armclang, сканер шины — с настоящими
заголовками HAL, тест и пример с выходом за массив выполнены на
компьютере, выход за массив поймал AddressSanitizer.
Как это называется в документации
Оригинал Перевод controller / target (master / slave) ведущий / ведомый 7-bit address, reserved addresses семибитный адрес, зарезервированные адреса bus scan сканирование шины pull-up, rise time подтяжка, время нарастания фронта bus capacitance ёмкость шины 1-Wire, ROM code, family code 1-Wire, адрес устройства, код семейства presence pulse импульс присутствия half-duplex полудуплекс chip select (CS) выбор устройства framebuffer кадровый буфер RGB565 цвет в 16 битах: 5 + 6 + 5 buffer overflow / underflow выход за конец / за начало массива
83.1 I²C: адрес, который «не отвечает»
Адресов меньше, чем кажется. Семь бит дают 128 адресов, но 0x00–0x07 и 0x78–0x7F зарезервированы стандартом — устройствам остаются 112, от 0x08 до 0x77. Типовой сканер перебирает 8…126 и заодно стучится в семь зарезервированных адресов. Правильный перебор — до 0x77.
Семь бит или восемь. В документации датчика адрес семибитный,
например 0x29. Библиотека Wire в Arduino принимает его как есть. HAL
на STM32 ждёт адрес уже сдвинутым на бит влево — 0x52; в исходнике
HAL об этом так и написано. Передали 0x29 — получили «датчик не
отвечает» при исправной схеме. Сам сдвиг и бит направления разобраны в
разделе 17.2.
/* found — найденные семибитные адреса; вернёт их число */
int i2c_scan(uint8_t *found, int max)
{
int n = 0;
for (uint8_t a = 0x08; a <= 0x77 && n < max; a++)
if (HAL_I2C_IsDeviceReady(&hi2c1, (uint16_t)(a << 1), 1, 10) == HAL_OK)
found[n++] = a;
return n;
}
Три ошибки из типовых примеров для датчика на Wire:
Wire.begin()вызывают один раз вsetup(), до первой передачи, а не внутри цикла послеbeginTransmission;- ожидание ответа
while (Wire.available() == 0);вечно, если датчик не ответил.requestFromсама возвращает число принятых байт — его и проверяют; - показание расстояния переводят в номер светодиода
map(dist, 0, 200, 0, 8)и берут элемент массива из восьми. При расстоянии 200mapдаёт 8 — индекс за концом массива.
Подтяжки. Обе линии I²C поднимают резисторы (раздел 17.1). Их считают по времени нарастания фронта: примерно 0,85·R·C. При ёмкости шины 100 пФ:
| Подтяжка | Фронт | 100 кГц (до 1000 нс) | 400 кГц (до 300 нс) |
|---|---|---|---|
| 4,7 кОм | 398 нс | ✓ | ✗ |
| 2,2 кОм | 186 нс | ✓ | ✓ |
Для 400 кГц при 100 пФ подтяжка должна быть не больше 3,5 кОм, а снизу её ограничивает ток, которым вывод прижимает линию: при 3 мА и 3,3 В — не меньше 967 Ом. У каждого готового модуля свои 4,7 кОм, и на общей шине они включаются параллельно: три модуля — 1,57 кОм, ещё можно; пять — 940 Ом, уже меньше допустимого. Лишние подтяжки на модулях снимают.
83.2 1-Wire: термометр на одном проводе
Шина 1-Wire обходится одной линией данных (плюс земля): ведущий прижимает её к земле импульсами разной длины, подтяжка поднимает. Каждое устройство несёт заводской 64-битный адрес: байт кода семейства, 48 бит серийного номера и байт CRC. CRC считается из остальных байтов, так что разных адресов 2⁵⁶ ≈ 7,2·10¹⁶, а внутри одного семейства — 2⁴⁸ ≈ 2,8·10¹⁴.
| Действие | Низкий уровень |
|---|---|
| сброс | не меньше 480 мкс, затем устройство отвечает импульсом присутствия |
| запись 0 | около 60 мкс |
| запись 1 или чтение | не больше 15 мкс |
CRC адреса — восьмой байт. Его проверяют всегда: провод длинный, данные идут медленно, помеха вероятна. Удобное свойство: CRC всех восьми байт верного адреса равна нулю.
/* ── 1-Wire: CRC-8 с полиномом x^8 + x^5 + x^4 + 1, младшим битом вперёд ── */
uint8_t crc8_1wire(const uint8_t *p, size_t n)
{
uint8_t crc = 0;
while (n--) {
uint8_t b = *p++;
for (int i = 0; i < 8; i++) {
uint8_t mix = (uint8_t)((crc ^ b) & 1);
crc >>= 1;
if (mix)
crc ^= 0x8C;
b >>= 1;
}
}
return crc;
}
Контрольное значение этого полинома для строки «123456789» — A1; тест
его подтвердил, как и то, что один сбитый бит адреса даёт ненулевую CRC.
1-Wire на STM32 без отсчёта микросекунд. Отдельного блока 1-Wire у STM32 нет. Можно дёргать вывод по таймеру, но проще приспособить UART в полудуплексе: вывод TX — открытый сток с подтяжкой, приём и передача на одном проводе. Длительность низкого уровня задаёт сам байт:
| Что нужно | Скорость | Байт | Низкий уровень |
|---|---|---|---|
| сброс | 9600 бод | 0xF0 |
старт + 4 нуля = 520,8 мкс |
| запись 0 | 115 200 бод | 0x00 |
9 бит = 78,1 мкс |
| запись 1 или чтение | 115 200 бод | 0xFF |
только старт = 8,68 мкс |
Эхо приходит обратно в приёмник: если датчик прижал линию — импульсом присутствия после сброса или нулём при чтении, — принятый байт отличается от отправленного.
/* Сброс: 0xF0 на 9600 бод. Датчик отвечает, прижимая линию, — эхо не 0xF0. */
int ow_reset(void)
{
USART2->BRR = PCLK1 / 9600u;
uint8_t echo = ow_xfer(0xF0);
USART2->BRR = PCLK1 / 115200u;
return echo != 0xF0; /* 1 — на шине кто-то есть */
}
/* Бит: 0x00 записывает ноль, 0xFF — единицу или читает бит от датчика */
int ow_bit(int bit)
{
return ow_xfer(bit ? 0xFF : 0x00) == 0xFF;
}
Полудуплекс у USART F1 включается битом HDSEL. USART2 сидит на шине
36 МГц: делители 3750 и 312, вторая скорость выходит 115 385 бод —
ошибка 0,16 %, для 1-Wire с большим запасом (раздел 16).
83.3 SPI и дисплей
Сама шина — раздел 18. На прототипе с дисплеем важны три вещи.
Подтяжка на выводе выбора. Часто пишут, что SPI подтяжки не нужны. Для линий такта и данных — да, а вывод выбора устройства (CS) подтягивают к питанию: пока микроконтроллер в сбросе или загружается, его выводы висят, и дисплей или флеш-память может принять мусор как команду. С подтяжкой устройство до начала программы не выбрано.
Память. Кадр 240×240 в 16-битном цвете — 115 200 байт. У F103C8 всего 20 КБ ОЗУ: не помещается даже кадр 128×160 (40 960 байт). Рисуют кусками — строку или прямоугольник за раз, задавая дисплею окно. У F407 кадр 240×240 в ОЗУ помещается.
Время. Полный кадр 240×240 по SPI на 18 МГц (предел SPI у F103, раздел 18.3) — 51,2 мс, на 9 МГц — 102,4 мс. На макетных проводах выше 8–10 МГц обычно не работает (раздел 18), поэтому на прототипе анимация тормозит, а на плате — нет.
Цвет и порядок байтов. Дисплеи принимают цвет в 16 битах: 5 бит красного, 6 зелёного, 5 синего.
/* ── цвет для дисплея: 5 бит красного, 6 зелёного, 5 синего ── */
uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b)
{
return (uint16_t)(((r & 0xF8u) << 8) | ((g & 0xFCu) << 3) | (b >> 3));
}
Белый — FFFF, красный — F800, зелёный — 07E0, синий — 001F.
Дисплей ждёт старший байт первым, а в памяти Cortex-M красный F800
лежит как 00 F8 — младшим вперёд (раздел 67). Массив uint16_t,
отправленный по SPI побайтно или через DMA байтами, придёт с
переставленными байтами, и цвета «поплывут». Лечат 16-битным режимом
SPI или перестановкой байтов — на Cortex-M это одна команда rev16
(раздел 78.2).
83.4 Число на экране: две ошибки с буферами
Буфер без места под ноль. В типовом примере показание АЦП выводят
через массив char[4]. Но 1023 — это четыре знака, плюс завершающий
ноль — пять байт. Проверено: в char[4] число 1023 превращается в
«102», в char[5] — выводится целиком; −12 в char[3] становится
«−1». Буфер считают по самому длинному значению со знаком и нулём, а
snprintf возвращает длину, которая была нужна, — по ней видно обрезку:
int fmt_value(char *buf, size_t n, int v)
{
return snprintf(buf, n, "%d", v);
}
На AVR с 2 КБ ОЗУ к этому добавляется класс String, который берёт
память из кучи на каждое преобразование, — дробление кучи (раздел 72).
Статический буфер и snprintf надёжнее.
Сдвиг истории за начало массива. График температуры хранит 120 последних значений и на каждом замере сдвигает их на одно место:
for (int a = 119; a >= 0; --a) /* при a = 0 читает history[-1] */
history[a] = history[a - 1];
Последний шаг цикла читает элемент перед массивом. На Arduino это
тихо портит соседнюю переменную, на компьютере AddressSanitizer
останавливает программу с диагнозом stack-buffer-underflow. С условием
a >= 1 программа работает и кладёт новые значения в начало: 22, 21,
20. А лучше вовсе не сдвигать 120 чисел на каждом замере: кольцевой
буфер с индексом записи, как у скользящего среднего (раздел 81.4), делает
то же за одну запись.
83.5 Грабли
Датчик на I²C «не отвечает» после переноса на STM32. В HAL передан семибитный адрес без сдвига.
Сканер находит «устройства» на странных адресах. Перебирались зарезервированные адреса 0x78–0x7E.
Программа виснет на опросе датчика. Бесконечное ожидание
Wire.available() без проверки ответа.
Шина I²C работает на 100 кГц и сбоит на 400. Подтяжка 4,7 кОм слишком слабая для такой ёмкости.
С пятью модулями шина перестала работать. Их подтяжки включились параллельно — меньше допустимого сопротивления.
Термометр иногда показывает бессмыслицу. Не проверяется CRC.
При включении дисплей показывает мусор. Вывод CS без подтяжки.
Цвета на дисплее перепутаны. Порядок байтов 16-битного цвета.
Кадровый буфер не собирается для F103. 115 200 байт при 20 КБ ОЗУ — рисуют окнами.
Показание 1023 выводится как 102. Буфер без места под ноль.
Рядом с графиком портится посторонняя переменная. Сдвиг истории читает за началом массива.
83.6 Итоги
- Адресов I²C — 112, с 0x08 по 0x77; HAL ждёт адрес сдвинутым влево,
Wire— нет. - Подтяжку считают по фронту 0,85·R·C: 4,7 кОм — для 100 кГц, для 400 кГц при 100 пФ — не больше 3,5 кОм; параллельные подтяжки модулей складываются.
- 1-Wire: один провод, 64-битный адрес с CRC; на STM32 удобно через USART в полудуплексе — сброс байтом 0xF0 на 9600, биты на 115 200.
- Вывод CS подтягивают к питанию. Кадр 240×240 не помещается в F103C8; по SPI он идёт 51,2 мс на 18 МГц. Цвет RGB565 уходит старшим байтом вперёд — не так, как лежит в памяти.
- Буфер под текст — по самому длинному значению плюс ноль; сдвиг
массива с условием
a >= 0читает за его начало.
Дальше: раздел 84 «Прерывания и время на прототипе».