" В цифровой электронике все — это либо АЦП либо ЦАП.
Аудио кодеки, CAN, RS485, ethernet‑трансиверы, датчики.
Тот же GPIO можно рассматривать, как однобитный ЦАПик. "

Предыстория

Ранее я уже писал как смастерить многобитный ЦАП на основе PWM и RC фильтра. Это текст Управление напряжением PWM сигналом (PWM+RC=DAC). Сегодня же предлагаю рассмотреть однобитный ЦАП и его возможные приложения в технике.

В одном проекте потребовалось работать с электронной платой без аппаратного UART на PLD вилке. Зато там есть достаточное количество GPIO пинов. Хотел отлаживаться через Segger RTT, но без прав админа не подключиться к сокету. Выход - делать программный UART на GPIO пинах. Отсюда вопрос: как выставить на пине поезд из импульсов для имитации UART пакета? При этом надо выдерживать микросекундные интервалы! Сначала подумал было использовать SPI, однако увидел, что на интересующие пины даже аппаратный SPI схемотехнически не выведен.

Затем я подумал запустить аппаратный таймер и в его обработчике прерываний выставлять бинарные семплы на GPIO для имитации UART кадра. Забудьте про это! Такие частые прерывания просто заглушат основную программу в функции main.

Отсюда постановка задачи.

Постановка задачи.
Реализовать на STM32F407ZG однобитный ЦАПик. Назвать его BinDac. У меня в программе есть SRAM массив uint8 из нулей и единиц (семплов), которые надо отправить "на улицу". Есть частота дискретизации задаваемая частотой переполнения аппаратного таймера1 (c частотой, скажем, 9600 Hz). Частоту надо устанавливать с минимальной погрешностью. Надо выдать на GPIO pin PA4 этот поезд из нулей и единиц. Причем сделать это так, чтобы при отправке в ARM Cortex-M процессоре было не более 2х DMA2 прерываний: половина перемещения и окончание перемещения. После окончания отправки массива следует остановить передачу по DMA2 (Stream 5, Channel 6). Код написать с использованием фирменного STm HAL SDK.
Не использовать аппаратный DAC (DAC_OUT1) на STM32, ибо по ТЗ целевой GPIO пин должен произвольно меняться и совершенно не известно куда он выпадет в следующей конфигурации программного компонента BinDac.

Реализация

Классическое решение это делать ЦАП через сцепку GPIO+TIM+DMA. В широкую клетку механизм изложен в официальном открытом документе AN4666 от вендора STm. В своем же тексте я лишь дам развернутый комментарий тому как это понять перевести на русский и реализовать в конкретном случае для STM32F407ZG.

Фаза 1: Подготовка данных (циклограммы)
Прежде всего надо подготовить данные, которые мы хотим отобразить на GPIO. Выделить и заполнить массив uint32. Задать длину этого массива. В массиве должны храниться биты GPIO пина, которые мы и будем отображать на проводе. По сути это наша циклограмма. Это может быть какой-то сигнал, структура пакета, адрес - всё что угодно. При заполнении надо учитывать ту частоту дискретизации, с которой Вы будете всё это отображать. Тут надо учитывать, что это массив для регистра GPIOx_BSRR (Не для GPIOx_ODR).

Скрытый текст
2.26h-->bin_dac_show_sample

I,[BinDac],N:1,TIM1,FS:9600 Hz,TxSz:100 Sam,TxMem:0x20005004,OutPad:PA4,Dma:[DMA2_Stream5,CH_6,[GPIOtx]],BIN_DAC1,
I,[BinDac],Spin:0,Init:On,TxSz:96 Sam,TxMem:0x20005004,
I,[BinAdc],StartCnt:7
+-----+----------+------------+-----------------------------------------+
| Num |    TS    |   Sam      |                   BIN                   |
+-----+----------+------------+-----------------------------------------+
|   0 |    0.000 | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|   1 |  13.020u | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|   2 |  26.041u | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|   3 |  39.062u | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|   4 |  52.083u | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|   5 |  65.104u | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|   6 |  78.125u | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|   7 |  91.145u | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|   8 |   0.104m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|   9 |   0.117m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  10 |   0.130m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  11 |   0.143m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  12 |   0.156m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  13 |   0.169m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  14 |   0.182m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  15 |   0.195m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  16 |   0.208m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  17 |   0.221m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  18 |   0.234m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  19 |   0.247m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  20 |   0.260m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  21 |   0.273m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  22 |   0.286m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  23 |   0.299m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  24 |   0.312m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  25 |   0.325m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  26 |   0.338m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  27 |   0.351m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  28 |   0.364m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  29 |   0.377m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  30 |   0.390m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  31 |   0.403m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  32 |   0.416m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  33 |   0.429m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  34 |   0.442m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  35 |   0.455m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  36 |   0.468m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  37 |   0.481m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  38 |   0.494m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  39 |   0.507m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  40 |   0.520m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  41 |   0.533m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  42 |   0.546m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  43 |   0.559m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  44 |   0.572m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  45 |   0.585m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  46 |   0.598m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  47 |   0.611m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  48 |   0.625m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  49 |   0.638m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  50 |   0.651m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  51 |   0.664m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  52 |   0.677m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  53 |   0.690m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  54 |   0.703m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  55 |   0.716m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  56 |   0.729m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  57 |   0.742m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  58 |   0.755m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  59 |   0.768m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  60 |   0.781m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  61 |   0.794m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  62 |   0.807m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  63 |   0.820m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  64 |   0.833m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  65 |   0.846m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  66 |   0.859m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  67 |   0.872m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  68 |   0.885m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  69 |   0.898m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  70 |   0.911m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  71 |   0.924m | 0x00100000 | 0000_0000|0001_0000|0000_0000|0000_0000 |
|  72 |   0.937m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  73 |   0.950m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  74 |   0.963m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  75 |   0.976m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  76 |   0.989m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  77 |   1.002m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  78 |   1.015m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  79 |   1.028m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  80 |   1.041m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  81 |   1.054m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  82 |   1.067m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  83 |   1.080m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  84 |   1.093m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  85 |   1.106m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  86 |   1.119m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  87 |   1.132m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  88 |   1.145m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  89 |   1.158m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  90 |   1.171m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  91 |   1.184m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  92 |   1.197m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  93 |   1.210m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  94 |   1.223m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
|  95 |   1.236m | 0x00000010 | 0000_0000|0000_0000|0000_0000|0001_0000 |
+-----+----------+------------+-----------------------------------------+


Фаза 2: Настройка GPIO
GPIO это то, что связывает программу внутри микроконтроллера со всем оставшимся миром. Именно благодаря GPIO программа может воздействовать на окружающий физический мир и менять реальность. Чтобы напряжение отобразилось на пине надо прописать 1 в регистре GPIOx_ODR. Чтобы установить ноль вольт надо прописать 0 в соответствующем бите GPIOx_ODR.

Забудьте про это! По крайней мере в случае управления одним пином. Если мы будет писать в регистр GPIOx_ODR мы случайно поменяем логическое состояние соседних пинов. Надо писать в регистр GPIOx_BSRR. Регистр BSRR позволяет атомарно менять конкретный пин, не затрагивая остальные биты порта. Это критично, потому что DMA пишет 32-битное слово в GPIOA->BSRR целиком; при использовании ODR запись нуля сбросила бы все пины порта.

При подготовке циклограммы надо это учитывать. В циклограмме будет 1 для установки бита и смещенный на 16 бит один для сброса бита. Поэтому для управления одним пином по DMA надо, чтобы семплы были 32 битные! Такова архитектура периферии STM32.

1) Подать тактирование на GPIO.
2) Настроить на выход те пины GPIO порта которыми мы собираемся управлять по циклограмме из массива. В моем случае PA4.
3) Установить подтяжку напряжения на пин. В моем случае это Pull Up.
4) Установить режим работы пина на выход.
5) Установить самую высокую скорость пина.
6) Отключить альтернативную функцию пина.
7) Установить режим Push Pull

Вот на этом наборе GPIO регистров у меня гарантированно работал обозначенный механизм.

-->gpio_reg_map 0
I,[GPIO],Port,Ok
I,[GPIO],GPIO0,Base:0x40020000
I,[GPIO],Base:0x40020000,Cnt:10
+-----+--------------------+-------+------+------------+------------+-------------------------------------------+
|  N  |        Name        |offset | size |    Addr    |   ValHex   |                  ValBin                   |
+-----+--------------------+-------+------+------------+------------+-------------------------------------------+
|   1 |        GPIOx_MODER | 0x000 |    0 | 0x40020000 | 0xa828a1a0 | 0b1010_1000|0010_1000|1010_0001|1010_0000 |
|   2 |       GPIOx_OTYPER | 0x004 |    4 | 0x40020004 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|   3 |      GPIOx_OSPEEDR | 0x008 |    4 | 0x40020008 | 0x0c3cf3f0 | 0b0000_1100|0011_1100|1111_0011|1111_0000 |
|   4 |        GPIOx_PUPDR | 0x00c |    4 | 0x4002000c | 0x64100146 | 0b0110_0100|0001_0000|0000_0001|0100_0110 |
|   5 |          GPIOx_IDR | 0x010 |    4 | 0x40020010 | 0x00005c1e | 0b0000_0000|0000_0000|0101_1100|0001_1110 |
|   6 |          GPIOx_ODR | 0x014 |    4 | 0x40020014 | 0x000006df | 0b0000_0000|0000_0000|0000_0110|1101_1111 |
|   7 |         GPIOx_BSRR | 0x018 |    4 | 0x40020018 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|   8 |         GPIOx_LCKR | 0x01c |    4 | 0x4002001c | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|   9 |         GPIOx_AFRL | 0x020 |    4 | 0x40020020 | 0x22007700 | 0b0010_0010|0000_0000|0111_0111|0000_0000 |
|  10 |         GPIOx_AFRH | 0x024 |    4 | 0x40020024 | 0x00000770 | 0b0000_0000|0000_0000|0000_0111|0111_0000 |
+-----+--------------------+-------+------+------------+------------+-------------------------------------------+
I,[GPIO],Regs,Ok
-->

Фаза 3: Настройка DMA канала для опроса GPIO
DMA это по сути сопроцессор, который умеет только перекопировать значения из одной ячейки памяти в другую ячейку памяти. DMA необходимы для освобождения CPU от рутинной работы.

1) Выбрать экземпляр DMA. Это самый неочевидный этап. На STM32 два DMA контроллера: DMA1 DMA2. Логика проста. Если мы хотим управлять GPIOA, то надо выбирать такой DMA, который примыкает к шине AHB1

Шиной AHB1 может управлять только DMA2 (базовый адрем 0x40026400).

Более явно это показано на схеме 6 (Figure 6. Multi-AHB matrix)

2) Подать тактирование на DMA2.
3) Выбрать поток DMA2. Я выбираю поток 5. Так как не нем есть аппаратный таймер1, который может триггерить DMA2.
4) Выбрать канал потока5 DMA2. Я выбираю канал 6, так как не нём есть аппаратный таймер1, который может триггерить DMA2.

5) Назначить приоритет DMA2 потока. Ставлю наивысший приоритет.
6) Корректно определить размер DMA семпла. Важный момент. Буду писать по 32 бит. Это как раз надо для работы с широким 32-битным регистром GPIOх_BSRR. Работа через регистр GPIOх_BSRR необходима, чтобы не затрагивать соседние пины GPIO порта. В случае использования регистра GPIOx_ODR можно было бы использовать 16 битный размер слова.
7) Сконфигурировать DMA2 в режим перемещения от
памяти к периферии (memory-to-peripheral).
8) Задать начальный адрес из которого следует извлекать семплы. Это начальный RAM адреc начала циклограммы со значениями для регистра BSRR.
*9) Задать адрес назначения. Это физический адрес GPIO в который следует прописывать данные. Это адрес регистра GPIOx_BSRR (смещение 0x18). В моем случае это регистр GPIOA_BSRR (0x40020018).
9) Задать одиночный режим перемещения. В режиме Normal после передачи счетчика байт DMA автоматически отключается.
10) Назначить количество семплов, которые надо передать. Тут надо указать количество семплов в циклограмме.
11) Активировать прерывания по DMA. Прерывание на половину отправки и прерывание на окончание отправки.
12) Определить код обработчиков прерывании по DMA потоку.

Вот на этом наборе DMA2 Stream5 регистров у меня гарантировано работал механизм.

1:17--> dma_channel_reg_map 2 5
 -->dma_channel_reg_map 2 5

W,[DmaChanneDMA2_Stream5,CH_6,, Regs
I,[DmaChannel],Base:0x40026488,Cnt:6
+-----+--------------------+-------+------+------------+------------+-------------------------------------------+
|  N  |        Name        |offset | size |    Addr    |   ValHex   |                  ValBin                   |
+-----+--------------------+-------+------+------------+------------+-------------------------------------------+
|   1 |           DMA_SxCR | 0x000 |    0 | 0x40026488 | 0x0c035446 | 0b0000_1100|0000_0011|0101_0100|0100_0110 
|   2 |         DMA_SxNDTR | 0x004 |    4 | 0x4002648c | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 
|   3 |          DMA_SxPAR | 0x008 |    4 | 0x40026490 | 0x40020018 | 0b0100_0000|0000_0010|0000_0000|0001_1000 
|   4 |         DMA_SxM0AR | 0x00c |    4 | 0x40026494 | 0x20005004 | 0b0010_0000|0000_0000|0101_0000|0000_0100 
|   5 |         DMA_SxM1AR | 0x010 |    4 | 0x40026498 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 
|   6 |          DMA_SxFCR | 0x014 |    4 | 0x4002649c | 0x00000020 | 0b0000_0000|0000_0000|0000_0000|0010_0000 
+-----+--------------------+-------+------+------------+------------+-------------------------------------------+

-->dma_channel_diag_custom
+---------------------+-----+---+---+----------+-----+-----+-----+-----+--------+--------+
| DMAx_Stream_y_Ch_z  | En  |TxI|Hal|   Dir    |mDat |pDat |mInc |pInc |  mode  |  Prio  |
+---------------------+-----+---+---+----------+-----+-----+-----+-----+--------+--------+
| DMA2_Stream_1_Ch_7  | Off | 0 | 0 | Per->mem | 16  | 16  |  on | off | Normal | VerHi  |
| DMA2_Stream_5_Ch_6  | Off | 0 | 0 | Mem->per | 32  | 32  |  on | off | Normal | VerHi  |
+---------------------+-----+---+---+----------+-----+-----+-----+-----+--------+--------+

Фаза 4: Настройка аппаратного таймера
Таймер нужен для того, чтобы тактировать DMA. При каждом переполнении таймера DMA делает перемещение одного семпла в GPIO.

1) Первым делом надо понять, какой выбрать аппаратный таймер для тактирования DMA2. На первый взгляд, судя по тому, что у нас есть на шине APB2 можно выбрать 1, 8, 9, 10 или 11.

Однако при более внимательном рассмотрении выясняется, что сигнал о переполнении аппаратного счетчика могут посылать только таймер1 и таймер8. В связи с этим я выбираю таймер1.

2) Подать тактирование на аппаратный таймер1.
3) Прописать предделитель аппаратного таймера1 (TIMx_PSC).
4) Определить частоту переполнений аппаратного таймера1 (TIMx_ARR).
*5) Отключить прерывания по аппаратному таймеру1. Это важно. Ради этого всё и задумано. Прерывания по аппаратному таймеру совершенно не должны беспокоить центральный процессор и основную программу внутри функции main.
6) Назначить направление счета вверх
7) Назначить функцию мастера
8) Назначить событие для управление DMA2. В нашем случае это TRG_UPDATE. То есть генерировать событие при переполнении счетчика таймера.
9) Отключить slave режим и все настройки slave режима таймера.

Вот на этом наборе регистров таймера 1 у меня гарантировано работал механизм.

1:42-->timer_diag_raw_reg 1

104.001,+1806,193,I,[Timer],Base:0x40010000,Cnt:20
+-----+--------------------+-------+------+------------+------------+-------------------------------------------+
|  N  |        Name        |offset | size |    Addr    |   ValHex   |                  ValBin                   |
+-----+--------------------+-------+------+------------+------------+-------------------------------------------+
|   1 |           TIMx_CR1 | 0x000 |    0 | 0x40010000 | 0x00000081 | 0b0000_0000|0000_0000|0000_0000|1000_0001 |
|   2 |           TIMx_CR2 | 0x004 |    4 | 0x40010004 | 0x00000020 | 0b0000_0000|0000_0000|0000_0000|0010_0000 |
|   3 |          TIMx_SMCR | 0x008 |    4 | 0x40010008 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|   4 |          TIMx_DIER | 0x00c |    4 | 0x4001000c | 0x00000101 | 0b0000_0000|0000_0000|0000_0001|0000_0001 |
|   5 |            TIMx_SR | 0x010 |    4 | 0x40010010 | 0x0000001e | 0b0000_0000|0000_0000|0000_0000|0001_1110 |
|   6 |           TIMx_EGR | 0x014 |    4 | 0x40010014 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|   7 |         TIMx_CCMR1 | 0x018 |    4 | 0x40010018 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|   8 |         TIMx_CCMR2 | 0x01c |    4 | 0x4001001c | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|   9 |          TIMx_CCER | 0x020 |    4 | 0x40010020 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|  10 |           TIMx_CNT | 0x024 |    4 | 0x40010024 | 0x0000077f | 0b0000_0000|0000_0000|0000_0111|0111_1111 |
|  11 |           TIMx_PSC | 0x028 |    4 | 0x40010028 | 0x00000003 | 0b0000_0000|0000_0000|0000_0000|0000_0011 |
|  12 |           TIMx_ARR | 0x02c |    4 | 0x4001002c | 0x00001116 | 0b0000_0000|0000_0000|0001_0001|0001_0110 |
|  13 |           TIMx_RCR | 0x030 |    4 | 0x40010030 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|  14 |          TIMx_CCR1 | 0x034 |    4 | 0x40010034 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|  15 |          TIMx_CCR2 | 0x038 |    4 | 0x40010038 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|  16 |          TIMx_CCR3 | 0x03c |    4 | 0x4001003c | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|  17 |          TIMx_CCR4 | 0x040 |    4 | 0x40010040 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|  18 |          TIMx_BDTR | 0x044 |    4 | 0x40010044 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|  19 |           TIMx_DCR | 0x048 |    4 | 0x40010048 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 |
|  20 |          TIMx_DMAR | 0x04c |    4 | 0x4001004c | 0x00000081 | 0b0000_0000|0000_0000|0000_0000|1000_0001 |
+-----+--------------------+-------+------+------------+------------+-------------------------------------------+

Отладка

Что ж посмотрим, как это работает в натуре. Я собрал прототип. Соединил GPIO пин PA4 и UART2_RX (PA3).

Затем сгенерировал циклограммой UART пакет c байтом 0x55 и отправил его в bin_dac. Как можно заметить аппаратный UART2_RX в самом деле принял UART-совместимый пакет. Этот эксперимент явственно доказывает, что мой программный однобитный ЦАП в самом деле работает.

Недостатки двоичного ЦАП
1) Надо много памяти. По сути чтобы аккуратно выставить 1 бит на GPIO надо подготовить одно 32-битное значение для регистра GPIOx_BSRR. Учитывая длинную последовательность сложного сигнала это может просто превысить все мыслимые размеры RAM памяти. Типичный бинарный ЛЧМ импульс на 2500 семплов может запросто занять 10k Byte RAM.

Достоинства двоичного ЦАП
1) Центральный процессор совершенно не нагружается от управления GPIO. Всё происходит аппаратно. Даже DMA прерывания можно отключить. Механизм продолжит работыть автоматически.
2) Можно задавать любую частоту димкретицации
3) Можно одновременно синхронно управлять 16 пинами.

Приложения двоичного ЦАП
1) Можно отправлять сложные полифонические мелодии на пьезокерамический звуко излучатель. Можно формировать бинарные ЛЧМ зондирующие импульсы для пьезо-излучателей в гидроакустике. Учитывая то, что можно разом управлять 16ю GPIO пинами, то можно делать формирование фазированной звуковой решетки. Так можно задавать направление в котором станет распространятся звуковая волна.
2) Программно генерировать пакеты цифровых интерфейсов: IR-пультов, DCF77, UART, SPI, CAN, 1-Wire и прочее.
3) Можно реализовать железное мигание LED-ом, которое будет работать даже, если прошивка в main зависнет или свалится в Hard Fault. Можно активировать PWM даже на тех GPIO пинах, на которых просто нет PWM-совместимых пинов. То есть нет выхода компаратора от какого либо аппаратного таймера.
4) Можно генерировать микросекундные импульсы с минимальной погрешностью. Например для управления фотовспышкой. Или для управления программируемым артилерийстким взрывателем.
5) Можно генерировать одно битный дельта-сигма-подобный / pulse-density сигнал для управления LEDами. Делать плавное управление яркостью подсветки.
6) Можно генерировать Class -D аудио. DSD/SACD — это буквально 1-битный поток на высокой частоте (2.8/5.6 МГц). BinDac — это ровно оно, только на GPIO вместо специализированного модулятора.
7) Можно имитировать PDM микрофоны.
8) Можно генерировать модуляции, в которых не участвует амплитуда: FSK/GMSK/PSK/OOK.
9) Можно генерировать ортогональные шумоподобные последовательности для CDMA систем связи.

Итоги
Удалось научиться делать программный бинарный ЦАП на основе STM32. Это открывает дорогу для целого калейдоскопа всяческих приложений на основе обыкновенного микроконтроллера. Начиная от программной имитации интерфейсов (UART, 1-Wire, NEC и прочее), заканчивая управлением многоканальными массивами гидро-акустических фазированных решёток.

Словарь

Акроним

Расшифровка

GPIO

General-Purpose Input/Output

BSRR

Bit Set/Reset Register

SDK

Software Development Kit

DAC

Digital-to-Analog Converter

ЦАП

цифро-аналоговый преобразователь

STM32

ST Microelectronics 32-bit

ODR

output data register

UART

Universal Asynchronous Receiver-Transmitter

DMA

Direct memory access

AHB

Advanced High-performance Bus

RAM

Random Access Memory

Источники

Название

URL

SoftSerial: Однобитный ADC на STM32 (или самодельный логический анализатор)

https://habr.com/ru/articles/1083584/

STM32: аббревиатуры и термины

https://microsin.net/programming/arm/stm32-terms-and-abbreviations.html?ysclid=mu9ncis91o979782233

Управление напряжением PWM сигналом (PWM+RC=DAC)

https://habr.com/ru/articles/821951/

AN4666 Application note Parallel synchronous transmission using GPIO and DMA

https://www.st.com/resource/en/application_note/an4666-parallel-synchronous-transmission-using-gpio-and-dma-stmicroelectronics.pdf

Обзор отладочной платы JL_32F4XX с STM32F4XX

https://stm32-base.org/boards/STM32F407ZGT6-STM32F4XX.html

“Сигма дельта” или как сделать хорошую звуковую карту из STM32F401 @Sdima1357

https://habr.com/ru/articles/662141/


Вопросы:

1) Как в режиме GPIO +DMA + TIMER управлять только одним конкретным GPIO пином ? Например PA4. Дело в том, что соседние пины по схемотехнике тоже сконфигурированы на выход и их менять нельзя. Надо использовать регистр BSRR .

2) Можно ли в STM32 Ethernet-овский DMA + аппаратный таймер использовать для чтения/записи GPIO?

Комментарии (4)


  1. nerudo
    21.09.2026 10:23

    Для столь амбициозной задачи не хватает результатов исследований: а какой же итоговый джиттер получился.


    1. aabzel Автор
      21.09.2026 10:23

      Для столь амбициозной задачи не хватает результатов исследований

      Да что тут амбициозного?
      Просто аппаратная мигалка светодиодом.


  1. Dozer88
    21.09.2026 10:23

    Была мысль управлять кучей шаговиков для ЧПУ таким способом. Джиттер в несколько (десятков) тактов тут не страшен. А вот с фазированными решетками в гидроакустике это уже будет проблемой


    1. aabzel Автор
      21.09.2026 10:23

      Была мысль управлять кучей шаговиков для ЧПУ таким способом. Джиттер в несколько (десятков) тактов тут не страшен.

      Есть у меня отдельная методичка про то как управлять шаговым двигателем.

      Теория управления шаговым двигателем (или как вертеть PTZ камеру)
      https://habr.com/ru/articles/709500/

      + Про это есть публикация
      Asynchronous Control System for Stepper Motor With an Incremental Encoder Feedback.