Embedded HandbookПриложения
Все разделы › X. Проектирование, сети и IoT
STM32F103, STM32F407 · уровень базовый · 9 мин чтения
Открыть в интерактивном справочнике

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.

Адресный байт на проводе: 0x29 как есть — чужой адрес 0x14 и NACK; 0x29 << 1 = 0x52 — адрес 0x29 и ACK

/* 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:

Подтяжки. Обе линии 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).

RGB565: 5 бит красного, 6 зелёного, 5 синего — и красный F800 в памяти как 00 F8


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 Итоги

Дальше: раздел 84 «Прерывания и время на прототипе».

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