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

22е. Драйверы индикаторов TM1637 и MAX7219: свой двухпроводный протокол, SPI-слова, яркость, каскад матриц 8×8

В разделе 22.3 четыре разряда индикатора зажигал по очереди сам микроконтроллер: двенадцать выводов или два 74HC595, прерывание таймера 1 кГц, транзисторы на общих выводах разрядов. Готовые модули избавляют от всего этого: на плате стоит микросхема-драйвер, которая сама держит развёртку, ток и яркость, а программа лишь присылает ей, что показывать. Самые ходовые — TM1637 (четыре цифры на двух проводах) и MAX7219 (восемь цифр или матрица 8×8 на SPI, модули соединяются в цепочку). У первого свой протокол, похожий на I²C, но не I²C; у второго — простые 16-битные слова, зато в цепочке легко перепутать, кому какое слово досталось. Ниже — оба протокола, драйверы на Си для STM32F103, таблица цифр, цепочка из четырёх матриц и бегущая строка.

Опоры: DocID13587 Rev 17 (STM32F103x8/xB): PA4 — SPI1_NSS, PA5 — SPI1_SCK, PA7 — SPI1_MOSI, с. 29; PB6 и PB7 — FT, с. 32. Описаний TM1637 и MAX7219 в проекте нет: команды, регистры и порядок бит взяты из документации производителей и оговорены в подписи в конце. Все числа ниже — прогон драйвера _verify/leddrv22g/leddrv22g.c на компьютере (gcc 12.2.0) со своей моделью обеих микросхем на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3.

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

Оригинал Перевод
LED driver, display driver драйвер светодиодного индикатора
grid, digit разряд
segment сегмент
start / stop condition старт / стоп посылки
acknowledge (ACK) подтверждение приёма
data command, address command команда режима, команда адреса
display control, pulse width управление экраном, ширина импульса (яркость)
open drain открытый сток
shift register сдвиговый регистр
LOAD (CS) защёлка слова, выбор микросхемы
daisy chain, cascade цепочка, каскад
no-op пустое слово
decode mode, code B режим декодера, шрифт цифр B
intensity яркость
scan limit число разрядов развёртки
shutdown, display test выключение, проверка экрана

Вверху — посылка TM1637 из одного байта 0x40: старт — DIO падает при CLK = 1, восемь бит b0…b7 младшим вперёд, DIO меняется при CLK = 0, чип читает бит по фронту CLK, на девятом такте чип прижимает DIO к нулю — ACK, затем стоп — DIO растёт при CLK = 1; полпериода CLK 5 мкс, «12:34» тремя посылками 40, C0 и четыре байта, 8F — 740 мкс. Посередине — подключение к STM32F103: TM1637 на PB6 (CLK) и PB7 (DIO) с открытым стоком и подтяжкой, четыре MAX7219 цепочкой на SPI1: SCK — PA5, DIN — PA7, LOAD — PA4, выход DOUT каждого модуля на вход DIN следующего; SPI 4,5 МГц, в посылке четыре слова, первым уходит слово дальнего модуля №3, пустое слово с адресом 0x00, кадр 32×8 — 8 посылок, 113,8 мкс. Внизу — слово MAX7219: D15–D12 не важны, D11–D8 адрес регистра, D7–D0 данные, пример 0x0A0F — яркость 15


22е.1 Зачем драйвер индикатора

Сравним способы показать четыре цифры:

Способ Выводов МК Развёртка Ток разрядов
Прямо от выводов (22.3) 12 (8 сегментов и 4 разряда) прерывание таймера, 1 кГц транзисторы на разрядах (22.6)
Два 74HC595 (22.1) 3 (SPI и защёлка) прерывание таймера, 1 кГц транзисторы или ключи
TM1637 2 сама микросхема сама микросхема
MAX7219 3 (SPI) сама микросхема сама микросхема, ток задаёт резистор

Главное — не число выводов, а то, что развёртка уходит из программы. Драйвер индикатора сам перебирает разряды и держит ток сегментов, а микроконтроллер пишет в него только при смене картинки. Часы, показывающие минуты, обращаются к TM1637 раз в минуту: одна запись «12:34» занимает 740 мкс, и больше процессор об индикаторе не думает. Прерывание таймера на 1 кГц из раздела 22.3 и тени от недогашенного разряда остаются в прошлом.

Выбор между ними простой. TM1637 — дешёвые модули на 4 или 6 цифр, часто с двоеточием для часов; два провода, которые можно взять у любых свободных выводов. MAX7219 — восемь разрядов по восемь сегментов, то есть 64 светодиода на микросхему: восемь цифр или матрица 8×8. Модули матриц соединяются в цепочку, и четыре штуки дают экран 32×8 точек на тех же трёх проводах SPI.


22е.2 TM1637: протокол

Две линии — CLK и DIO. Обе с открытым стоком: и микроконтроллер, и чип только прижимают линию к нулю, единицу даёт подтяжка к питанию (раздел 8.1). Посылка похожа на I²C, но адреса устройства нет, а биты идут младшим вперёд:

Первый байт посылки — команда:

Команда Код Что делает
режим данных 0x40 запись, адрес растёт после каждого байта
0x44 запись в один и тот же адрес
адрес 0xC0 + разряд 0…5 дальше в той же посылке — байты сегментов
экран 0x80 выключен
0x88 | яркость 0…7 включён, яркость 8 ступеней

Вывод четырёх цифр — три посылки: режим, адрес с данными, экран.

uint32_t tm_show(const tm_port_t *p, const uint8_t seg[4], uint8_t bright)
{
    const uint8_t mode = TM_CMD_AUTO;
    const uint8_t data[5] = {TM_CMD_ADDR | 0u, seg[0], seg[1], seg[2], seg[3]};
    uint32_t nack = tm_write(p, &mode, 1);  /* три посылки: режим, адрес с данными, экран */
    nack += tm_write(p, data, 5);
    return nack + tm_bright(p, 1, bright);
}

Модель в стенде принимает биты с ног и собирает посылки. «12:34» с полной яркостью — 40 | C0 06 DB 4F 66 | 8F: семь байт, все с ACK, при полупериоде такта 5 мкс — 740 мкс. Одна цифра в режиме фиксированного адреса — 44 | C3 6F: девятка в разряд 3, 335 мкс, остальные разряды не тронуты. Яркость меняется одной посылкой: 88 … 8F — ступени 0…7, 87 гасит экран, не стирая цифр.


22е.3 TM1637: код для F103 и таблица цифр

CLK — PB6, DIO — PB7, выходы с открытым стоком (MODE = 10, CNF = 01, в CRL — 0x66000000). Записать 1 — значит отпустить линию, и её поднимет подтяжка; а читать уровень через IDR можно и у выхода — так драйвер получает ACK, не переключая вывод на вход:

static void tm_clk(uint8_t l) { GPIOB->BSRR = l ? (1u << 6) : (1u << (6 + 16)); }
static void tm_dio(uint8_t l) { GPIOB->BSRR = l ? (1u << 7) : (1u << (7 + 16)); }
static uint8_t tm_dio_in(void) { return (uint8_t)((GPIOB->IDR >> 7) & 1u); }

Байт с ответом — ногодрыгом, такт 10 мкс:

static uint32_t tm_byte(const tm_port_t *p, uint8_t v)
{
    for (uint32_t i = 0; i < 8u; i++) {
        p->clk(0);
        p->dio((uint8_t)((v >> i) & 1u));   /* младший бит первым; DIO меняется при CLK = 0 */
        p->delay_us(TM_HALF_US);
        p->clk(1);                          /* чип читает бит по фронту CLK */
        p->delay_us(TM_HALF_US);
    }
    p->clk(0);
    p->dio(1);                              /* отпустить DIO: девятый такт — ответ чипа */
    p->delay_us(TM_HALF_US);
    p->clk(1);
    p->delay_us(TM_HALF_US);
    uint32_t nack = p->dio_in();            /* ACK: чип держит DIO в нуле */
    p->clk(0);                              /* по спаду девятого такта чип отпускает DIO */
    p->delay_us(TM_HALF_US);
    return nack;
}

Если модуль питается от 5 В и подтяжки на нём — к 5 В, это не страшно: PB6 и PB7 у F103 допускают 5 В (FT, с. 32), и открытый сток с такой подтяжкой им не вредит. Полупериод 5 мкс выбран медленным и надёжным; предельная частота CLK — в документации производителя, здесь она не проверялась.

Таблица цифр. Бит 0 — сегмент a, бит 6 — g, бит 7 — точка или двоеточие: 3F 06 5B 4F 66 6D 7D 07 7F 6F — та же таблица, что в разделе 22.3. На часовых модулях двоеточие обычно подключено к биту 7 второго разряда, поэтому «12:34» — это 06, 5B | 0x80 = DB, 4F, 66; проверьте по своему модулю. Модель рисует, что получилось на экране, — «12:34» и все десять цифр тремя посылками по четыре:

     _   _
  |  _|: _| |_|
  | |_ : _|   |

 _       _   _
| |   |  _|  _|
|_|   | |_   _|
     _   _   _
|_| |_  |_    |
  |  _| |_|   |
 _   _
|_| |_|
|_|  _|

Стенд проверил и три ошибки. Биты старшим вперёд, как привычно по SPI и I²C: чип получает 02 | 03 B6 BE E0 FE | F1 — ни одной команды из таблицы, экран не меняется. Модуль не подключён: все 7 байт без ACK — драйвер возвращает 7, и программа это видит. DIO — обычный выход вместо открытого стока: в единице МК тянет линию вверх, а чип на время ACK — вниз; модель насчитала 70 мкс такой борьбы выходов на каждую запись «12:34» — по 10 мкс на байт.


22е.4 MAX7219: слово, регистры и инициализация

У MAX7219 внутри 16-битный сдвиговый регистр. Слово уходит старшим битом вперёд: D15–D12 не важны, D11–D8 — адрес регистра, D7–D0 — данные. По фронту LOAD (у модулей — вывод CS) чип исполняет слово, которое лежит в регистре.

Адрес Регистр Значение в драйвере
0x00 пустое слово (no-op) для цепочки, 22е.5
0x01…0x08 разряды 0–7 (строки матрицы) байт — восемь точек строки
0x09 режим декодера 0x00 — без декодера
0x0A яркость 0…15
0x0B число разрядов развёртки 0x07 — все восемь
0x0C выключение 0x01 — рабочий режим
0x0F проверка экрана 0x00 — выключена

Для матрицы декодер выключают: с ним чип рисует по байту цифру своего шрифта B, а не восемь точек. Для семисегментных разрядов декодер удобен — байт 0…9 сразу становится цифрой; порядок сегментов в битах у MAX7219 свой, не как в таблице 22.3, — по документации производителя.

Подключение — SPI1 ведущим (раздел 18): SCK — PA5, MOSI — PA7 на вход DIN модуля, LOAD — PA4 обычным выводом. 16 бит (DFF = 1), CPOL = 0, CPHA = 0, делитель 16: 72 / 16 = 4,5 МГц. MISO не нужен — выход DOUT последнего модуля никуда не подключают, и PA5, PA7 без допуска 5 В (с. 29) ничего от модулей на 5 В не получают. Перед фронтом LOAD драйвер ждёт, пока SPI досдвинет последний бит, — как с защёлкой 74HC595 (разделы 22.1 и 18.11):

static void max_cs(uint8_t l)
{
    if (l)
        while (SPI1->SR & SPI_SR_BSY)       /* последний бит должен дойти до чипа */
            ;
    GPIOA->BSRR = l ? (1u << 4) : (1u << (4 + 16));
}

Инициализация задаёт все регистры явно, не полагаясь на их состояние после включения, и стирает экран, пока он ещё выключен:

void max_init(const max_port_t *p, uint8_t intensity)
{
    static const uint8_t blank[MAX_N * 8u];
    max_all(p, MAX_TEST, 0x00u);            /* выключить проверку экрана */
    max_all(p, MAX_SCANLIMIT, 0x07u);       /* развёртка всех 8 строк */
    max_all(p, MAX_DECODE, 0x00u);          /* без декодера: байт — это точки */
    max_all(p, MAX_INTENSITY, intensity & 0x0Fu);
    max_frame(p, blank);                    /* стереть, пока экран выключен */
    max_all(p, MAX_SHUTDOWN, 0x01u);        /* рабочий режим */
}

На цепочке из четырёх модулей это 13 посылок по 4 слова — 52 слова, 832 бита, 184,9 мкс на SPI 4,5 МГц. Первые посылки — 0F00 0F00 0F00 0F00, 0B07 0B07 0B07 0B07, 0900 0900 0900 0900, 0A01 0A01 0A01 0A01.


22е.5 Каскад четырёх модулей: кому какое слово

Выход DOUT модуля соединён со входом DIN следующего, LOAD у всех общий. Цепочка — это четыре 16-битных регистра подряд: каждое новое слово входит в ближний модуль №0 и выталкивает его прежнее слово в №1, то — в №2 и так далее. По фронту LOAD каждый чип исполняет то, что в нём оказалось. Отсюда два правила:

Чтобы сказать что-то одному модулю, остальным шлют пустое слово с адресом 0x00:

void max_one(const max_port_t *p, uint32_t k, uint8_t reg, uint8_t val)
{
    p->cs(0);
    for (uint32_t i = MAX_N; i-- > 0u;)     /* первым уходит слово дальнего модуля */
        p->spi16(i == k ? (uint16_t)((reg << 8) | val) : (uint16_t)(MAX_NOOP << 8));
    p->cs(1);
}

Яркость 15 одному модулю №2 — слова 0000 0A0F 0000 0000; в модели яркость сменилась только у №2. А если послать одно слово 0A0F, будто модуль один, оно попадёт в ближний №0, а модули №1–№3 по фронту LOAD повторят то, что лежало в регистрах от прошлой посылки, — здесь 0A01. В модели яркость 15 получил №0, а не тот, кому предназначалась. Если прошлой посылкой был кадр, дальние модули перерисуют чужие строки.

Развёртка. Регистр 0x0B говорит, сколько разрядов перебирать. Ошибка 0x03 вместо 0x07 — и модель показывает только верхние четыре строки «STM32»:

..####.#####.#...#.#####..###...
.#.......#...##.##....#..#...#..
.#.......#...#.#.#...#.......#..
..###....#...#.#.#....#.....#...
................................
................................
................................
................................

22е.6 Бегущая строка, яркость и ток

Кадр держим в памяти как 32 столбца по байту: бит 0 — верхняя точка. Шрифт 5×7 тоже по столбцам, на знак 6 столбцов — 5 точек и пустой. Вывод кадра — 8 посылок, по одной на строку; в каждой четыре слова с адресом строки 0x01…0x08, первым — модуль №3. Левый столбец модуля — бит 7 (так в модели; у сборок модулей ориентация бывает разной, и тогда порядок бит или модулей разворачивают):

void max_frame(const max_port_t *p, const uint8_t fb[MAX_N * 8u])
{
    for (uint32_t r = 0; r < 8u; r++) {
        p->cs(0);
        for (uint32_t k = MAX_N; k-- > 0u;) {
            uint8_t row = 0;
            for (uint32_t c = 0; c < 8u; c++)
                if (fb[k * 8u + c] & (1u << r))
                    row |= (uint8_t)(0x80u >> c);   /* левый столбец модуля — бит 7 */
            p->spi16((uint16_t)(((MAX_DIGIT0 + r) << 8) | row));
        }
        p->cs(1);
    }
}

«STM32» — 30 столбцов из 32. Первая строка уходит словами 0138 015F 01F4 013D, и модель показывает:

..####.#####.#...#.#####..###...
.#.......#...##.##....#..#...#..
.#.......#...#.#.#...#.......#..
..###....#...#.#.#....#.....#...
.....#...#...#...#.....#...#....
.....#...#...#...#.#...#..#.....
.####....#...#...#..###..#####..
................................

Бегущая строка — тот же кадр со сдвигом: текст рисуется с позиции x0, и каждый шаг x0 уменьшается на 1.

void txt_render(uint8_t fb[MAX_N * 8u], const char *s, int32_t x0)
{
    int32_t w = (int32_t)txt_width(s);
    for (int32_t x = 0; x < (int32_t)(MAX_N * 8u); x++) {
        int32_t t = x - x0;                 /* столбец текста под столбцом x экрана */
        fb[x] = (t >= 0 && t < w && t % 6 < 5) ? glyph(s[t / 6])[t % 6] : 0u;
    }
}

STM32F103 шириной 54 столбца въезжает справа и уходит влево за 86 шагов (32 + 54). Кадр — 512 бит, 113,8 мкс на SPI 4,5 МГц; весь проход — 2752 слова и 9784,9 мкс передачи. С шагом 50 мс проход длится 4,3 с, и на SPI из них уходит меньше 0,3 %. Кадр на шаге, где под экраном F103:

#####...#....###..#####.........
#......##...#...#....#..........
#.......#...#..##...#...........
####....#...#.#.#....#..........
#.......#...##..#.....#.........
#.......#...#...#.#...#.........
#......###...###...###..........
................................

Яркость. Обе микросхемы гасят светодиоды импульсами: у TM1637 8 ступеней в команде экрана, у MAX7219 — 16 ступеней в регистре 0x0A. Шаги неравномерны для глаза: разница между нижними ступенями заметнее, чем между верхними, как с ШИМ светодиода (раздел 82.2).

Ток. У MAX7219 пиковый ток сегмента задаёт один резистор RSET между выводом ISET и питанием; таблица «ток — сопротивление» есть в документации производителя, здесь она не проверялась. Ток разряда в каждый момент — до восьми сегментов сразу. Пример с допущением 20 мА на сегмент: модуль с полностью зажжённой матрицей берёт до 160 мА, четыре модуля — 640 мА от 5 В. Этот ток считают до того, как подключить цепочку к выводу 5 В отладочной платы, а рисунок «всё горит» на полной яркости проверяют до того, как он станет заставкой.


22е.7 Грабли

На TM1637 ничего не появляется, хотя посылки идут. Биты уходят старшим вперёд: чип получает 02, 03, F1 вместо 40, C0, 8F. TM1637 — младшим вперёд.

Драйвер считает, что ACK нет, при живом модуле. DIO настроен входом без подтяжки или выходом push-pull. Нужен открытый сток и подтяжка; уровень читают из IDR того же вывода.

DIO — push-pull, «но работает». На каждом ACK выходы МК и чипа тянут линию в разные стороны: 70 мкс на запись «12:34». Открытый сток.

Цифры с чужими сегментами или без двоеточия. Разводка модуля: двоеточие — бит 7 одного из разрядов, у шестиразрядных модулей порядок разрядов бывает перепутан. Проверить, зажигая по одному сегменту.

Команду получил не тот модуль цепочки. В посылке меньше слов, чем модулей: слово ушло в ближний модуль, дальние повторили старые слова. Ровно четыре слова под одним LOAD, лишним — 0x00.

Картинка перевёрнута или модули переставлены. Ориентация матриц у сборок разная: развернуть порядок бит в строке или порядок модулей.

Видна только часть строк. Регистр 0x0B не 0x07.

На матрице цифры шрифта вместо точек. Включён декодер 0x09; для матрицы — 0x00.

Последний модуль ловит мусор. LOAD поднят, пока SPI ещё сдвигает последнее слово: ждать сброса BSY.

Матрица мигает или не стартует от 3,3 В. Модуль MAX7219 на 5 В может не принять единицу 3,3 В: по документации производителя порог входа при 5 В — 3,5 В. Буфер с порогом под 3,3 В на линиях SPI.

Плата перезагружается, когда горит всё. Ток матриц: до 160 мА на модуль при 20 мА на сегмент. Отдельное питание или меньшая яркость.


22е.8 Итоги

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

leddrv22g.h — порты, команды TM1637, регистры MAX7219, шрифт.

/* leddrv22g.h — раздел 22е: драйверы индикаторов TM1637 и MAX7219 без регистров.
 * Ноги и SPI — через функции порта: в прошивке fw22g.c это регистры GPIO и SPI1 STM32F103,
 * в стенде host22g.c — функции, которые кормят модель устройств model22g.py. */
#ifndef LEDDRV22G_H
#define LEDDRV22G_H
#include <stdint.h>

/* ── TM1637: два провода, свой протокол, открытый сток ────────────────── */
typedef struct {
    void (*clk)(uint8_t level);         /* 1 — отпустить линию (подтяжка), 0 — прижать к земле */
    void (*dio)(uint8_t level);
    uint8_t (*dio_in)(void);            /* уровень на линии DIO */
    void (*delay_us)(uint32_t us);
} tm_port_t;

#define TM_HALF_US   5u                 /* полпериода CLK: такт 10 мкс */
#define TM_CMD_AUTO  0x40u              /* запись, адрес растёт сам */
#define TM_CMD_FIXED 0x44u              /* запись в один адрес */
#define TM_CMD_ADDR  0xC0u              /* | адрес разряда 0…5 */
#define TM_CMD_DISP  0x80u              /* | 0x08 — экран включён, | 0…7 — яркость */
#define TM_SEG_COLON 0x80u              /* бит 7 разряда: точка или двоеточие часового модуля */

extern const uint8_t tm_digit[10];      /* сегменты цифр, бит 0 — a … бит 6 — g */
uint32_t tm_write(const tm_port_t *p, const uint8_t *b, uint32_t n);     /* -> сколько байтов без ACK */
uint32_t tm_show(const tm_port_t *p, const uint8_t seg[4], uint8_t bright);
uint32_t tm_put(const tm_port_t *p, uint8_t pos, uint8_t seg);
uint32_t tm_bright(const tm_port_t *p, uint8_t on, uint8_t bright);

/* ── MAX7219: 16-битное слово по SPI, цепочка модулей ─────────────────── */
typedef struct {
    void (*cs)(uint8_t level);          /* LOAD: по фронту каждый чип берёт своё слово */
    void (*spi16)(uint16_t w);          /* слово старшим битом вперёд, вернуться — когда ушло */
} max_port_t;

#define MAX_N         4u                /* модулей 8×8 в цепочке: 32×8 точек */
#define MAX_NOOP      0x00u
#define MAX_DIGIT0    0x01u             /* 0x01…0x08 — разряды (строки матрицы) */
#define MAX_DECODE    0x09u
#define MAX_INTENSITY 0x0Au
#define MAX_SCANLIMIT 0x0Bu
#define MAX_SHUTDOWN  0x0Cu
#define MAX_TEST      0x0Fu

void max_all(const max_port_t *p, uint8_t reg, uint8_t val);                 /* одно и то же — всем */
void max_one(const max_port_t *p, uint32_t k, uint8_t reg, uint8_t val);     /* только модулю k */
void max_init(const max_port_t *p, uint8_t intensity);
void max_frame(const max_port_t *p, const uint8_t fb[MAX_N * 8u]);          /* 32 столбца, бит 0 — верх */

/* ── бегущая строка: шрифт 5×7 по столбцам ────────────────────────────── */
uint32_t txt_width(const char *s);                                          /* 6 столбцов на знак */
void txt_render(uint8_t fb[MAX_N * 8u], const char *s, int32_t x0);        /* текст с x0 столбца экрана */

#endif

leddrv22g.c — драйверы без регистров: общая часть прошивки и стенда.

/* leddrv22g.c — раздел 22е: TM1637 ногодрыгом и MAX7219 по SPI, бегущая строка (без регистров). */
#include "leddrv22g.h"

/* ══ TM1637 ══════════════════════════════════════════════════════════════ */
const uint8_t tm_digit[10] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};

static void tm_start(const tm_port_t *p)
{
    p->dio(1);
    p->clk(1);
    p->delay_us(TM_HALF_US);
    p->dio(0);                              /* старт: DIO падает при CLK = 1 */
    p->delay_us(TM_HALF_US);
}

static void tm_stop(const tm_port_t *p)
{
    p->clk(0);
    p->dio(0);
    p->delay_us(TM_HALF_US);
    p->clk(1);
    p->delay_us(TM_HALF_US);
    p->dio(1);                              /* стоп: DIO растёт при CLK = 1 */
    p->delay_us(TM_HALF_US);
}

static uint32_t tm_byte(const tm_port_t *p, uint8_t v)
{
    for (uint32_t i = 0; i < 8u; i++) {
        p->clk(0);
        p->dio((uint8_t)((v >> i) & 1u));   /* младший бит первым; DIO меняется при CLK = 0 */
        p->delay_us(TM_HALF_US);
        p->clk(1);                          /* чип читает бит по фронту CLK */
        p->delay_us(TM_HALF_US);
    }
    p->clk(0);
    p->dio(1);                              /* отпустить DIO: девятый такт — ответ чипа */
    p->delay_us(TM_HALF_US);
    p->clk(1);
    p->delay_us(TM_HALF_US);
    uint32_t nack = p->dio_in();            /* ACK: чип держит DIO в нуле */
    p->clk(0);                              /* по спаду девятого такта чип отпускает DIO */
    p->delay_us(TM_HALF_US);
    return nack;
}

uint32_t tm_write(const tm_port_t *p, const uint8_t *b, uint32_t n)
{
    uint32_t nack = 0;
    tm_start(p);
    for (uint32_t i = 0; i < n; i++)
        nack += tm_byte(p, b[i]);
    tm_stop(p);
    return nack;
}

uint32_t tm_show(const tm_port_t *p, const uint8_t seg[4], uint8_t bright)
{
    const uint8_t mode = TM_CMD_AUTO;
    const uint8_t data[5] = {TM_CMD_ADDR | 0u, seg[0], seg[1], seg[2], seg[3]};
    uint32_t nack = tm_write(p, &mode, 1);  /* три посылки: режим, адрес с данными, экран */
    nack += tm_write(p, data, 5);
    return nack + tm_bright(p, 1, bright);
}

uint32_t tm_put(const tm_port_t *p, uint8_t pos, uint8_t seg)
{
    const uint8_t mode = TM_CMD_FIXED;
    const uint8_t data[2] = {(uint8_t)(TM_CMD_ADDR | (pos & 7u)), seg};
    uint32_t nack = tm_write(p, &mode, 1);
    return nack + tm_write(p, data, 2);
}

uint32_t tm_bright(const tm_port_t *p, uint8_t on, uint8_t bright)
{
    const uint8_t c = (uint8_t)(TM_CMD_DISP | (on ? 0x08u : 0u) | (bright & 7u));
    return tm_write(p, &c, 1);
}

/* ══ MAX7219 ═════════════════════════════════════════════════════════════ */
void max_all(const max_port_t *p, uint8_t reg, uint8_t val)
{
    p->cs(0);
    for (uint32_t k = 0; k < MAX_N; k++)
        p->spi16((uint16_t)((reg << 8) | val));
    p->cs(1);                               /* фронт LOAD: каждый чип исполняет своё слово */
}

void max_one(const max_port_t *p, uint32_t k, uint8_t reg, uint8_t val)
{
    p->cs(0);
    for (uint32_t i = MAX_N; i-- > 0u;)     /* первым уходит слово дальнего модуля */
        p->spi16(i == k ? (uint16_t)((reg << 8) | val) : (uint16_t)(MAX_NOOP << 8));
    p->cs(1);
}

void max_init(const max_port_t *p, uint8_t intensity)
{
    static const uint8_t blank[MAX_N * 8u];
    max_all(p, MAX_TEST, 0x00u);            /* выключить проверку экрана */
    max_all(p, MAX_SCANLIMIT, 0x07u);       /* развёртка всех 8 строк */
    max_all(p, MAX_DECODE, 0x00u);          /* без декодера: байт — это точки */
    max_all(p, MAX_INTENSITY, intensity & 0x0Fu);
    max_frame(p, blank);                    /* стереть, пока экран выключен */
    max_all(p, MAX_SHUTDOWN, 0x01u);        /* рабочий режим */
}

void max_frame(const max_port_t *p, const uint8_t fb[MAX_N * 8u])
{
    for (uint32_t r = 0; r < 8u; r++) {
        p->cs(0);
        for (uint32_t k = MAX_N; k-- > 0u;) {
            uint8_t row = 0;
            for (uint32_t c = 0; c < 8u; c++)
                if (fb[k * 8u + c] & (1u << r))
                    row |= (uint8_t)(0x80u >> c);   /* левый столбец модуля — бит 7 */
            p->spi16((uint16_t)(((MAX_DIGIT0 + r) << 8) | row));
        }
        p->cs(1);
    }
}

/* ══ шрифт 5×7 по столбцам: бит 0 — верхняя точка ═══════════════════════ */
static const char font_ch[] = " 0123FMST";
static const uint8_t font[][5] = {
    {0x00, 0x00, 0x00, 0x00, 0x00},     /* пробел */
    {0x3E, 0x51, 0x49, 0x45, 0x3E},     /* 0 */
    {0x00, 0x42, 0x7F, 0x40, 0x00},     /* 1 */
    {0x42, 0x61, 0x51, 0x49, 0x46},     /* 2 */
    {0x21, 0x41, 0x45, 0x4B, 0x31},     /* 3 */
    {0x7F, 0x09, 0x09, 0x09, 0x01},     /* F */
    {0x7F, 0x02, 0x0C, 0x02, 0x7F},     /* M */
    {0x46, 0x49, 0x49, 0x49, 0x31},     /* S */
    {0x01, 0x01, 0x7F, 0x01, 0x01},     /* T */
};

static const uint8_t *glyph(char ch)
{
    for (uint32_t i = 0; font_ch[i]; i++)
        if (font_ch[i] == ch)
            return font[i];
    return font[0];                         /* нет в шрифте — пробел */
}

uint32_t txt_width(const char *s)
{
    uint32_t n = 0;
    while (s[n])
        n++;
    return n * 6u;                          /* 5 столбцов знака и 1 пустой */
}

void txt_render(uint8_t fb[MAX_N * 8u], const char *s, int32_t x0)
{
    int32_t w = (int32_t)txt_width(s);
    for (int32_t x = 0; x < (int32_t)(MAX_N * 8u); x++) {
        int32_t t = x - x0;                 /* столбец текста под столбцом x экрана */
        fb[x] = (t >= 0 && t < w && t % 6 < 5) ? glyph(s[t / 6])[t % 6] : 0u;
    }
}

fw22g.c — прошивка STM32F103: TM1637 на PB6, PB7 и четыре MAX7219 на SPI1.

/* fw22g.c — раздел 22е: TM1637 и цепочка из четырёх MAX7219 от STM32F103 (BluePill), 72 МГц.
 * TM1637: CLK — PB6, DIO — PB7, выходы с открытым стоком, подтяжка к питанию модуля.
 * MAX7219: SPI1 ведущим, SCK — PA5, MOSI (DIN) — PA7, LOAD (CS) — PA4 обычным выводом; 16 бит, 4,5 МГц.
 * Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); задержки — по счётчику тактов DWT. */
#include "stm32f10x.h"
#include "leddrv22g.h"

#define CPU_MHZ 72u

static void delay_us(uint32_t us)
{
    uint32_t t0 = DWT->CYCCNT;
    while (DWT->CYCCNT - t0 < us * CPU_MHZ)
        ;
}

/* ── TM1637 на PB6, PB7 ───────────────────────────────────────────────── */
static void tm_clk(uint8_t l) { GPIOB->BSRR = l ? (1u << 6) : (1u << (6 + 16)); }
static void tm_dio(uint8_t l) { GPIOB->BSRR = l ? (1u << 7) : (1u << (7 + 16)); }
static uint8_t tm_dio_in(void) { return (uint8_t)((GPIOB->IDR >> 7) & 1u); }

static const tm_port_t tm = {tm_clk, tm_dio, tm_dio_in, delay_us};

/* ── MAX7219 на SPI1 ──────────────────────────────────────────────────── */
static void max_cs(uint8_t l)
{
    if (l)
        while (SPI1->SR & SPI_SR_BSY)       /* последний бит должен дойти до чипа */
            ;
    GPIOA->BSRR = l ? (1u << 4) : (1u << (4 + 16));
}

static void max_spi16(uint16_t w)
{
    while (!(SPI1->SR & SPI_SR_TXE))
        ;
    SPI1->DR = w;
    while (!(SPI1->SR & SPI_SR_RXNE))
        ;
    (void)SPI1->DR;                         /* MISO не подключён: читаем, чтобы снять RXNE */
}

static const max_port_t mx = {max_cs, max_spi16};

static void port_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_SPI1EN;
    GPIOB->BSRR = (1u << 6) | (1u << 7);    /* обе линии отпущены */
    /* PB6, PB7: выход с открытым стоком 2 МГц (MODE = 10, CNF = 01) */
    GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0x66000000u;
    GPIOA->BSRR = 1u << 4;                  /* LOAD в единице */
    /* PA4 — выход 2 МГц; PA5, PA7 — альтернативная функция 50 МГц; PA6 — вход, как после сброса */
    GPIOA->CRL = (GPIOA->CRL & 0x0000FFFFu) | 0xB4B20000u;
    /* ведущий, NSS программный, 16 бит, CPOL = 0, CPHA = 0, делитель 16: 72 / 16 = 4,5 МГц */
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_DFF | SPI_CR1_BR_0 | SPI_CR1_BR_1;
    SPI1->CR1 |= SPI_CR1_SPE;
}

int main(void)
{
    static const char text[] = "STM32F103";
    const uint8_t clock[4] = {tm_digit[1], (uint8_t)(tm_digit[2] | TM_SEG_COLON), tm_digit[3], tm_digit[4]};
    uint8_t fb[MAX_N * 8u];
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;     /* счётчик тактов DWT для задержек */
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    port_init();
    (void)tm_show(&tm, clock, 7u);                      /* «12:34», полная яркость */
    max_init(&mx, 1u);
    for (;;) {
        int32_t w = (int32_t)txt_width(text);
        for (int32_t x0 = (int32_t)(MAX_N * 8u); x0 > -w; x0--) {   /* въезжает справа, уходит влево */
            txt_render(fb, text, x0);
            max_frame(&mx, fb);
            delay_us(50000u);                           /* шаг 50 мс */
        }
    }
}

Дальше: раздел 22ж «ИК-пульт и протокол NEC: приёмник VS1838/TSOP, захват таймера, декодер на прерываниях, повтор и шум».

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