22б. Камера по DCMI: кадр в память через DMA
Раздел 22а выводит кадр из памяти на стекло. Здесь обратный путь: кадр приходит с камеры и ложится в память. Камера выдаёт байты по такту пикселей, строку за строкой, и не ждёт: пропустил байт — кадр испорчен. Принимает их блок DCMI, а в память кладёт DMA. Раздел о том, какие у DCMI сигналы и режимы, сколько памяти занимает кадр и что куда помещается, какой поток идёт по шине и как DMA со счётчиком на 65535 передач принимает кадр в 153 600 слов.
Опоры: справочное руководство RM0090 (F405/F407 и F427/F429, глава 15 —
DCMI, глава 10 — DMA, раздел 2.3 — память) и RM0385 для F746 (глава 17 —
DCMI, раздел 2.3). Алгоритм деления кадра — функции HAL_DCMI_Start_DMA и
DCMI_DMAXferCplt из HAL F4 (пак Keil 2.17.1). Числа плат — из пакета
поддержки плат (BSP): STM324x9I-EVAL (камера OV2640), STM32746G-Discovery
(OV9655) и пример Keil для MCBSTM32F400 (OVM7690). Расчёты — скриптом на ПК,
код по регистрам собран armclang для F407 и F746. DCMI QEMU не моделирует —
на камере это не прогонялось.
Как это называется в документации
Оригинал Перевод DCMI (digital camera interface) параллельный вход для камеры PIXCLK такт пикселей: камера выставляет данные по нему HSYNC, VSYNC синхроимпульсы строки и кадра snapshot mode снимок: один кадр, потом захват выключается сам continuous grab mode непрерывный захват, кадр за кадром crop кадрирование: из кадра берётся окно embedded synchronization синхронизация кодами внутри потока данных overrun переполнение: FIFO не успели освободить double buffer mode двойной буфер DMA: два указателя памяти по очереди current target (CT) бит «куда DMA пишет сейчас»
22б.1 Где есть DCMI и как подключают камеру
DCMI есть не у всех: по заголовкам CMSIS из паков — у F407/F417, F427/F429, F437/F439, F446, F469/F479 и у F745/F746, F750/F756, F765/F767/F769, F777/F779. У F401, F405/F415, F410, F411, F412 и F413/F423, а из F7 — у F722/F723 и F730/F732/F733 его нет. Модуль камеры — готовая плата с датчиком, объективом и контроллером; DCMI только принимает: RM0090 прямо пишет, что масштабирование и прочая обработка — забота камеры.
Проводов два набора. Параллельная шина — от камеры к DCMI: данные D0–D13 (8, 10, 12 или 14 бит), PIXCLK, HSYNC и VSYNC. Всего 11, 13, 15 или 17 входов; DCMI всегда ведомый, такт задаёт камера. Вторая шина — I²C: по ней программа настраивает камеру — разрешение, формат пикселя, частоту PIXCLK. Настройка — длинная таблица «регистр — значение» из документации на датчик: у BSP для OV9655 в режиме VGA это 147 пар, у OV2640 — 237.
22б.2 Сигналы, полярности и упаковка в слова
Камера меняет данные по одному фронту PIXCLK, DCMI защёлкивает их по другому — какому, задаёт бит PCKPOL. HSYNC и VSYNC делят поток на строки и кадры. Полярности VSPOL и HSPOL в RM0090 определены так: это уровень на выводе, когда данные недействительны, — то есть уровень гашения, а не «уровень активной строки». Всё, что приходит, пока HSYNC или VSYNC в этом уровне, DCMI отбрасывает.
Так ставят BSP и пример Keil:
| Плата и камера | VSPOL | HSPOL | PCKPOL |
|---|---|---|---|
| STM324x9I-EVAL, OV2640 | низкий | низкий | фронт |
| STM32746G-Discovery, OV9655 | высокий | низкий | фронт |
| MCBSTM32F400, OVM7690 | высокий | низкий | фронт |
Для OV2640 и OV9655 BSP ставят VSPOL по-разному: бит зависит от камеры и её настройки, и его берут под свою камеру, а не из чужого примера.
Вместо HSYNC и VSYNC камера может вставлять в поток коды синхронизации вида 0xFF 00 00 XY (бит ESS). Это работает только на 8-битной шине, и тогда значения 0x00 и 0xFF в данных запрещены.
Упаковка. На 8-битной шине DCMI собирает слово за четыре такта PIXCLK, первый байт — в младшие разряды; на 10, 12 и 14 битах — за два такта, по отсчёту в каждую половину слова. Только когда слово готово, DCMI просит DMA его забрать. Точка RGB565 — два такта, в слове две точки.
Предел частоты — не шина, а тактирование: период PIXCLK должен быть больше 2,5 периода HCLK. При HCLK 168 МГц это PIXCLK меньше 67,2 МГц, при 216 МГц — меньше 86,4 МГц; сам поток RM0090 и RM0385 ограничивают 54 МБ/с.
22б.3 Снимок и непрерывный захват
Снимок (CM = 1). После установки CAPTURE DCMI ждёт начала кадра, принимает ровно один кадр и сам снимает CAPTURE. Если по дороге случилось переполнение, кадр потерян, CAPTURE тоже снят — поэтому после снимка смотрят флаг OVR.
Непрерывный захват (CM = 0). Приём начинается со следующего VSYNC и идёт кадр за кадром, пока CAPTURE не снимут; снятие вступает в силу в конце текущего кадра. Буфер и то, линейный он или по кругу, задаёт DMA, а не DCMI. Поле FCRC прореживает кадры: все, каждый второй или один из четырёх.
Прерываний пять: конец строки (LINE), конец кадра или окна (FRAME), переполнение (OVR), VSYNC и ошибка кодов синхронизации (ERR). FIFO у DCMI на четыре слова, и защиты от переполнения у него нет: если DMA не забрал слово вовремя, FIFO сбрасывается, и DCMI ждёт следующего кадра.
22б.4 Окно CROP, JPEG и прореживание у F7
Окно. DCMI_CWSTRT задаёт начало окна: VST — номер строки, HOFFCNT — сколько тактов PIXCLK пропустить в строке. DCMI_CWSIZE — размер: VLINE — строк, CAPCNT — тактов PIXCLK, оба «минус один». На точку RGB565 тактов два, и это главная ловушка. Ширина окна в тактах должна складываться в целые слова: в описании окна RM0090 пишет, что два младших бита CAPCNT обнуляются, а в описании регистра — что CAPCNT «минус один». Противоречия не будет, если CAPCNT + 1 кратно четырём, как у BSP ниже: 959 + 1 = 960.
У STM32746G-Discovery экран 480×272, а OV9655 такого режима не умеет. BSP
включает камеру в VGA и вырезает окно: VST 104, HOFFCNT 80, CAPCNT 959 —
то есть 480 × 2 − 1, — VLINE 271. В регистрах это CWSTRT 0x0068 0050 и
CWSIZE 0x010F 03BF. Окно — 261 120 байт, 0xFF00 слов, как у GetSize в BSP
для 480×272: одна передача DMA. По вертикали окно по центру. По
горизонтали BSP передаёт половину разницы ширин в точках, а поле считает
такты: 80 тактов — это 40 точек. Окно начнётся с 40-й точки, справа
останется 120; для середины нужен HOFFCNT 160.
JPEG (бит JPEG). Камера сама сжимает кадр, VSYNC отмечает начало и конец картинки, HSYNC служит признаком «данные действительны». Окна и кодов синхронизации в этом режиме нет. Размер кадра заранее неизвестен: буфер берут с запасом, а сколько пришло, считают по NDTR — он убывает на каждой передаче. Если байтов в конце не хватило до слова, DCMI дополняет его нулями. Для несжатых форматов предел — 2048 × 2048 точек, для JPEG ограничения нет.
Прореживание у F7. У F746 в DCMI_CR есть поля, которых нет у F4. BSM берёт каждый второй байт, один из четырёх или два из четырёх, OEBS выбирает, с какого начать; BSM работает только на 8-битной шине. Из YCbCr 4:2:2 так остаётся одна яркость Y — кадр вдвое меньше. LSM и OELS берут каждую вторую строку.
22б.5 Сколько памяти и какой поток
Память, куда может писать DMA одним куском. У F407 это SRAM1 и SRAM2 — 128 КБ: 64 КБ памяти CCM в матрицу шин не заведены (раздел 9). У F429 к ним добавлена SRAM3 — 192 КБ. У F746 — 320 КБ: DTCM, SRAM1 и SRAM2; в DTCM DMA попадает через ведомую шину ядра AHBS (раздел 11б).
| Кадр | Байт | Слов DMA | F407, 128 КБ | F429, 192 КБ | F746, 320 КБ |
|---|---|---|---|---|---|
| 160×120 RGB565 | 38 400 | 9 600 | да | да | да |
| 320×240 RGB565 | 153 600 | 38 400 | нет | да | да |
| 480×272 RGB565 | 261 120 | 65 280 | нет | нет | да |
| 640×480 RGB565 | 614 400 | 153 600 | нет | нет | нет |
| 640×480 Y8 | 307 200 | 76 800 | нет | нет | да |
Числа слов совпадают с GetSize в BSP: 0x2580, 0x9600, 0xFF00 и
0x25800. Кадр 320×240 у F407 не помещается ни во что. Пример Keil для
MCBSTM32F400 обходится без буфера: DMA с выключенным приращением адреса
пишет слова DCMI прямо в регистр данных дисплея 8080 по 0x6C00 0002
(разделы 22а и 41б). Через FIFO потока каждое слово уходит двумя
16-битными записями — две точки. VGA в 16 битах не помещается никуда,
кроме SDRAM: у 746G-Discovery это 8 МБ с 0xC000 0000.
Поток. Пусть камера выдаёт PIXCLK 24 МГц на 8-битной шине — это пример, частоту задаёт настройка камеры. Внутри строки это 24 МБ/с, или шесть миллионов слов в секунду: одно слово раз в 28 тактов HCLK при 168 МГц и раз в 36 тактов при 216 МГц. Запас — FIFO: четыре слова — это 16 тактов PIXCLK, 0,667 мкс, или 112 тактов HCLK 168 МГц. Столько DMA может ждать шину, пока её держат другие: LTDC, DMA2D, процессор. На предельных 54 МБ/с запас — 0,296 мкс. Поэтому BSP ставит потоку камеры высокий приоритет.
Чистое время данных кадра при 24 МГц: 320×240 RGB565 — 6,4 мс, 640×480 — 25,6 мс. Даже без гашения VGA при такой частоте — не больше 39 кадров в секунду.
22б.6 DMA: 65535 слов и двойной буфер
Запрос DCMI заведён на DMA2, канал 1, поток 1 или 7 (RM0090, таблица 43; у RM0385 так же). Направление — периферия → память, обе ширины — слово, приращение адреса только со стороны памяти. BSP обеих плат берут поток 1 в циклическом режиме и прямом режиме FIFO.
Счётчик NDTR 16-битный: не больше 65535 передач за одну настройку (раздел
9). Кадр 640×480 RGB565 — 153 600 слов. HAL обходит это двойным буфером:
если длина больше 0xFFFF, HAL_DCMI_Start_DMA делит её пополам, пока часть
не станет не больше 0xFFFF, и запускает поток с двумя указателями M0AR и
M1AR. В двойном буфере DMA по концу каждой части сам меняет указатель
и продолжает, а обработчик DCMI_DMAXferCplt переставляет свободный
указатель на две части вперёд. Модель этого обработчика на ПК:
| Кадр | Слов | Частей | По слов | Потеряно |
|---|---|---|---|---|
| 640×480 RGB565 | 153 600 | 4 | 38 400 | 0 |
| 640×480 Y8 | 76 800 | 2 | 38 400 | 0 |
| 800×600 RGB565 | 240 000 | 4 | 60 000 | 0 |
| окно 330×399 RGB565 | 65 835 | 2 | 32 917 | 1 |
Для VGA модель проходит части 0, 1, 2, 3, 0, 1, 2, 3 — два кадра подряд, как надо. Но деление — целочисленное: если длина не делится на число частей, остаток HAL просто теряет, и DMA закончит «кадр» раньше, чем кончится кадр камеры.
По регистрам можно делить кадр на любое число частей, лишь бы нацело.
Функции camera_start и обработчик прерывания файла примера делят VGA на
три части по 51 200 слов:
#define CAM_FB 0xC0100000u /* с 0xC000 0000 — буфер LTDC платы */
#define VGA_WORDS FRAME_WORDS(640u, 480u, 2u)
#define PARTS 3u
#define PART (VGA_WORDS / PARTS)
_Static_assert(VGA_WORDS == 153600u && VGA_WORDS % PARTS == 0u && PART <= 65535u,
"кадр делится на части без остатка, часть — не больше 65535 слов");
static volatile uint32_t next_part;
void camera_start(void)
{
dcmi_clocks();
next_part = 2u;
dma_dcmi(CAM_FB, CAM_FB + 4u * PART, PART, DMA_SxCR_DBM | DMA_SxCR_TCIE);
NVIC_EnableIRQ(DMA2_Stream1_IRQn);
dcmi_on(CR_POL); /* CM = 0: кадр за кадром */
DCMI->CR |= DCMI_CR_CAPTURE;
}
void DMA2_Stream1_IRQHandler(void) /* часть дописана: свободному указателю — часть через одну */
{
uint32_t a = CAM_FB + 4u * PART * next_part;
DMA2->LIFCR = DMA_LIFCR_CTCIF1;
if (DMA2_Stream1->CR & DMA_SxCR_CT)
DMA2_Stream1->M0AR = a; /* DMA пишет по M1 — свободен M0 */
else
DMA2_Stream1->M1AR = a;
next_part = (next_part + 1u) % PARTS;
}
Писать M0AR можно, только пока CT = 1, а M1AR — пока CT = 0; иначе RM0090 обещает флаг ошибки TEIF и выключение потока. Отсюда проверка CT. Части по модели идут 0, 1, 2, 0, 1, 2 — нечётное число частей не мешает: кадры просто начинаются попеременно с M0 и с M1. armclang для F746 делает из выбора указателя одну условную команду без перехода:
LDR r3,[r2,#0x20] ; DMA2_Stream1->CR, r2 — адрес DMA2->LIFCR
MOV r0,#0x32000 ; часть: 51 200 слов по 4 байта
LSLS r3,r3,#12 ; бит 19 (CT) — в знак
MOV r3,#0x2c ; смещение M0AR от LIFCR
IT PL
MOVPL r3,#0x30 ; CT = 0 — M1AR
MOVT r12,#0xc010 ; CAM_FB = 0xC010 0000
MLA r0,r1,r0,r12 ; CAM_FB + next_part × 0x32000
STR r0,[r3,r2]
На смену указателя у обработчика — время целой части: 51 200 слов при
PIXCLK 24 МГц — 8,533 мс. Кадр лежит с 0xC010 0000 по 0xC019 6000: начало
SDRAM у 746G-Discovery занято буфером LTDC (LCD_FB_START_ADDRESS
0xC000 0000; 480×272 в 32 битах — 0x7F800 байт).
Для F407 в том же файле — снимок 160×120 во внутреннюю SRAM одной
передачей, функция camera_snapshot. В ней видно, во что складываются
поля: NDTR 0x2580, SxCR 0x0202 5400 (канал 1, высокий приоритет, слова,
приращение памяти), DCMI_CR 0x40A2 (ENABLE, VSPOL, PCKPOL, CM).
22б.7 Грабли
- Полярность VSYNC «наоборот». VSPOL и HSPOL — уровень, когда данные недействительны. Перепутали — DCMI принимает гашение вместо кадра: флаг FRAME приходит, а в буфере мусор или сдвиг.
- HOFFCNT в точках вместо тактов. Поле считает такты PIXCLK, у RGB565 их два на точку. BSP 746G-Discovery передаёт 80 — окно по документу начинается с 40-й точки, а не по центру.
- Кадр больше 65535 слов в одну передачу. NDTR 16-битный: VGA RGB565 — 153 600 слов, только частями через двойной буфер.
- Длина не делится на части. HAL делит пополам нацело и остаток
отбрасывает: у окна 330×399 это одно слово на кадр. Размер окна
подбирают так, чтобы число слов делилось, или делят сами, как в
camera_start. - Запись M0AR или M1AR не вовремя. Трогать можно только тот указатель, по которому DMA сейчас не пишет: смотреть CT. Иначе TEIF и остановка потока.
- Переполнение FIFO. Четыре слова — 0,667 мкс при 24 МГц. Долгие пачки других мастеров в ту же SDRAM или низкий приоритет потока — и флаг OVR, кадр потерян. После снимка OVR проверять обязательно. В непрерывном захвате DMA о сбросе FIFO не знает и продолжит счёт с середины части — по OVR поток DMA перезапускают.
- Буфер в CCM у F4. CCM в матрицу шин не заведена, DMA туда не пишет (раздел 9). У F407 из 192 КБ ОЗУ камере доступны 128.
- Обращение к регистрам DCMI не словом. RM требует 32-битный доступ, байт или полуслово дают ошибку шины.
- Адрес камеры на I²C. BSP передаёт 0x60: HAL принимает адрес уже сдвинутым влево, это семибитный 0x30. Драйвер Keil для OVM7690 пишет семибитный 0x21.
- Кэш у F7. Буфер в SRAM1 или SRAM2 F746 кэшируется, DTCM — нет:
перед чтением кадра процессором строки буфера сбрасывают
SCB_InvalidateDCache_by_Addr(раздел 11б). SDRAM по 0xC000 0000 по умолчанию — Device, без кэша (раздел 41б).
22б.8 Итоги
- DCMI — у F407/F417, F42x/F43x, F446, F469/F479 и у F745 и старше; у F401, F405, F410–F413 его нет. Камеру настраивают по I²C, DCMI только принимает.
- Сигналы: D0–D13, PIXCLK, HSYNC, VSYNC — 11…17 входов; VSPOL/HSPOL — уровень гашения; период PIXCLK больше 2,5 периода HCLK: до 67,2 МГц при 168 МГц.
- 8 бит — слово за 4 такта PIXCLK, FIFO на 4 слова без защиты; при 24 МГц запас DMA — 0,667 мкс.
- Кадр: 320×240 RGB565 — 153 600 байт, в 128 КБ F407 не помещается; 640×480 — 614 400 байт, только в SDRAM.
- Окно считается в тактах PIXCLK: у RGB565 ширину и отступ умножать на 2; 480×272 из VGA — 0xFF00 слов.
- NDTR до 65535: VGA — 153 600 слов, HAL делит на 4 части по 38 400, по регистрам — на 3 по 51 200; длина обязана делиться нацело.
Полные исходники
dcmi22b.c — DCMI по регистрам: снимок на F407 и непрерывный захват на F746.
/* 22б: DCMI по регистрам — снимок QQVGA во внутреннюю SRAM на F407
и непрерывный VGA в SDRAM на F746 с двойным буфером DMA.
Собирается armclang: -DSTM32F407xx и -DSTM32F746xx.
Выводы (GPIO, AF13) и настройка камеры по I²C — вне этого файла. */
#include <stddef.h>
#include <stdint.h>
/* поля DCMI и DMA — RM0090, 15.8 и 10.5; у RM0385 для F7 они те же */
_Static_assert(DCMI_BASE == 0x50050000u && offsetof(DCMI_TypeDef, DR) == 0x28u, "DCMI на AHB2, DR по 0x50050028");
_Static_assert(DCMI_CR_CAPTURE == (1u << 0) && DCMI_CR_CM == (1u << 1) && DCMI_CR_CROP == (1u << 2)
&& DCMI_CR_JPEG == (1u << 3), "CAPTURE, CM, CROP, JPEG — биты 0…3");
_Static_assert(DCMI_CR_PCKPOL == (1u << 5) && DCMI_CR_HSPOL == (1u << 6) && DCMI_CR_VSPOL == (1u << 7),
"полярности PCKPOL, HSPOL, VSPOL — биты 5, 6, 7");
_Static_assert(DCMI_CR_ENABLE == (1u << 14) && DCMI_CWSTRT_VST_Pos == 16u && DCMI_CWSIZE_VLINE_Pos == 16u,
"ENABLE — бит 14; строки окна — в старшей половине CWSTRT и CWSIZE");
_Static_assert(DMA_SxCR_CHSEL_Pos == 25u && DMA_SxCR_DBM == (1u << 18) && DMA_SxCR_CT == (1u << 19),
"канал в битах 27:25, DBM — бит 18, CT — бит 19");
#define FRAME_WORDS(w, h, bytes_per_px) ((w) * (h) * (bytes_per_px) / 4u)
/* VSYNC активен высоким, HSYNC — низким, захват по фронту PIXCLK:
так у STM32746G-Discovery (OV9655) и у MCBSTM32F400 (OVM7690) */
#define CR_POL (DCMI_CR_PCKPOL | DCMI_CR_VSPOL)
static void dcmi_clocks(void) /* такты — раньше любой записи в DMA2 и DCMI */
{
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
RCC->AHB2ENR |= RCC_AHB2ENR_DCMIEN;
(void)RCC->AHB2ENR;
}
static void dcmi_on(uint32_t cr)
{
DCMI->CR = cr; /* всё настроить до ENABLE */
DCMI->CR = cr | DCMI_CR_ENABLE;
}
/* DMA2, поток 1, канал 1 — запрос DCMI (RM0090, таблица 43) */
static void dma_dcmi(uint32_t m0, uint32_t m1, uint32_t items, uint32_t extra)
{
DMA_Stream_TypeDef *s = DMA2_Stream1;
s->CR = 0;
while (s->CR & DMA_SxCR_EN) {
}
DMA2->LIFCR = DMA_LIFCR_CTCIF1 | DMA_LIFCR_CHTIF1 | DMA_LIFCR_CTEIF1 | DMA_LIFCR_CDMEIF1 | DMA_LIFCR_CFEIF1;
s->PAR = (uint32_t)&DCMI->DR;
s->M0AR = m0;
s->M1AR = m1;
s->NDTR = items; /* слов, не больше 65535 */
s->FCR = 0; /* прямой режим, как в BSP */
s->CR = (1u << DMA_SxCR_CHSEL_Pos) | DMA_SxCR_PL_1 | DMA_SxCR_MSIZE_1 | DMA_SxCR_PSIZE_1
| DMA_SxCR_MINC | extra; /* DIR = 00: периферия → память, слова */
s->CR |= DMA_SxCR_EN;
}
#if defined(STM32F407xx)
/* ── снимок 160×120 RGB565: 38 400 байт — во внутреннюю SRAM, одной передачей ── */
#define QQVGA_WORDS FRAME_WORDS(160u, 120u, 2u)
_Static_assert(QQVGA_WORDS == 9600u && QQVGA_WORDS <= 65535u, "QQVGA — 9600 слов, одна передача DMA");
static uint32_t qqvga[QQVGA_WORDS];
int camera_snapshot(void) /* 0 — кадр в qqvga, -1 — переполнение FIFO */
{
dcmi_clocks();
DCMI->ICR = DCMI_ICR_FRAME_ISC | DCMI_ICR_OVR_ISC;
dma_dcmi((uint32_t)qqvga, 0u, QQVGA_WORDS, 0u);
dcmi_on(CR_POL | DCMI_CR_CM); /* CM = 1: один кадр */
DCMI->CR |= DCMI_CR_CAPTURE; /* ждёт начала кадра */
while (DCMI->CR & DCMI_CR_CAPTURE) {
} /* в конце кадра бит снимается сам */
return (DCMI->RISR & DCMI_RIS_OVR_RIS) ? -1 : 0;
}
#elif defined(STM32F746xx)
/* ── непрерывно 640×480 RGB565: 614 400 байт — только в SDRAM (0xC000 0000, 8 МБ) ── */
#define CAM_FB 0xC0100000u /* с 0xC000 0000 — буфер LTDC платы */
#define VGA_WORDS FRAME_WORDS(640u, 480u, 2u)
#define PARTS 3u
#define PART (VGA_WORDS / PARTS)
_Static_assert(VGA_WORDS == 153600u && VGA_WORDS % PARTS == 0u && PART <= 65535u,
"кадр делится на части без остатка, часть — не больше 65535 слов");
static volatile uint32_t next_part;
void camera_start(void)
{
dcmi_clocks();
next_part = 2u;
dma_dcmi(CAM_FB, CAM_FB + 4u * PART, PART, DMA_SxCR_DBM | DMA_SxCR_TCIE);
NVIC_EnableIRQ(DMA2_Stream1_IRQn);
dcmi_on(CR_POL); /* CM = 0: кадр за кадром */
DCMI->CR |= DCMI_CR_CAPTURE;
}
void DMA2_Stream1_IRQHandler(void) /* часть дописана: свободному указателю — часть через одну */
{
uint32_t a = CAM_FB + 4u * PART * next_part;
DMA2->LIFCR = DMA_LIFCR_CTCIF1;
if (DMA2_Stream1->CR & DMA_SxCR_CT)
DMA2_Stream1->M0AR = a; /* DMA пишет по M1 — свободен M0 */
else
DMA2_Stream1->M1AR = a;
next_part = (next_part + 1u) % PARTS;
}
#endif
Дальше: раздел 22в «Сегментный ЖКИ: стекло, мультиплекс и контроллер LCD» — индикатор, который почти не берёт тока: мультиплекс, bias и встроенный контроллер LCD.