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

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.

Камера на DCMI: восемь линий данных, PIXCLK, HSYNC и VSYNC идут от камеры OV9655 в DCMI, который собирает четыре байта в слово и держит до четырёх слов в FIFO; по запросу DCMI поток 1 DMA2 на канале 1 переносит слова из регистра DR по 0x5005 0028 в SDRAM; кадр 640×480 RGB565 лежит там тремя частями по 51 200 слов с 0xC010 0000, 0xC013 2000 и 0xC016 4000, указатели M0AR и M1AR чередуются, и свободный получает часть через одну; настройка камеры — отдельно, по I²C


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 Грабли


22б.8 Итоги

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

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.

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