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

Пролог
Как то раз мне принесли электронную плату без отладочного UART. Я привык отлаживать прошивки через UART Shell и это стало проблемой. Хотел было отлаживаться через Segger RTT, но отсутствие прав админа на РС не позволяли подключатся к сокету на локальном PC.
Зато на плате были просто GPIO пины на PLD вилке. И тут, то я понял, что надо запускать программный UART. Я стал писать программный UART. Пока я писал прием байта через программный UART задался вопросом.
Можно ли на STM32 реализовать однобитный ADC через DMA? Причем без использования аппаратного ADC. Надо сделать так, чтобы GPIO пин читался со строго определенным микросекундным периодом по команде аппаратного таймера и сохранял значения прочитанных логических уровней в массив uint8_t. При этом во время чтения N однобитных семплов должно произойти не более 2х аппаратных прерываний: половина чтения и полное чтение. Это необходимо, чтобы не нагружать основную программу в main. Как может выглядеть код такого однобитного ADC на основе HAL SDK от STm?
Как может работать однобитный ADC?
Мы включаем аппаратный таймер. Генерируем прерывания c частотой скажем 76800 Hz. В обработчике прерываний по таймеру считываем логический уровень на GPIO и помещаем его в RAM массив. В обработчике прерываний по таймеру можно также уменьшить счетчик уже принятых семплов для последующей автоматической приостановки аппаратного таймера. Здорово? Забудьте про это! Основная прошивка зависнет и процессор будет только лишь вызывать прерывания по переполнению аппаратного таймера для выхватывания семплов.
В связи с этим возникает задача.
Постановка задачи
Есть микроконтроллер STM32F407ZG. Есть GPIO пин PA1. Надо при возникновении на PA1 отрицательного перепада напряжения включить импровизированный однобитный ADC. То есть в режиме DMA записывать регистр GPIOx_IDR в массив типа uint16, расположенный в RAM памяти. Для определенности частота дискретизации выборки значений должна быть равна 76800 Hz (78,8 kHz). Надо управлять DMA каналом через master timer. Запись должна быть однократная. То есть после чтения массива DMA должен остановиться. Прерывание должно быть только одно: по окончанию заполнения массива. При написании кода использовать HAL SDK от STm.
Реализация
У микроконтроллера STM32 есть DMA. DMA умеет работать в режиме перемещения данных от периферии к RAM памяти. В качестве периферии будет выступать регистры GPIO портов.
Фаза 1: Настройка GPIO
Первым делом надо настроить GPIO.
1) Надо подать тактирование на GPIO. В моем случае это GPIOA
2) Определить на вход нужные нам пины. В моем случае это PA1.
3) Установить скорость пина на максимум.
4) Установить подтяжку напряжения. В моем случае Pull Up.
5) Отключить альтернативную функцию на GPIO пине. Установить значение ноль.
6) Подключить к пину внешние прерывания по отрицательному перепаду напряжения. GPIO_MODE_IT_FALLING
Надо сразу запомнить, что в STM32 значения пинов расположены именно в регистре GPIOx_IDR. Именно регистр GPIOx_IDR мы и будем опрашивать по DMA.

Фаза 2: Настройка аппаратного таймера
Аппаратный таймер для GPIO выступает в роли метронома для пианиста, чтобы не выбиваться из ритма. Именно аппаратный таймер дает команду на считывание логического уровня на GPIO.
Прежде всего надо понять какой номер аппаратного таймера мы выберем для тактирования DMA. Отталкиваться надо от таблицы каналов DMA (Table 42, Table 43). Мы можем выбрать таймеры с номерами 1, 2, 3, 4, 5, 6, 7, 8. Я решил выбрать TIM8.

1) Подать тактирование на аппаратный таймер.
2) Отключить прерывания по таймеру. Это важно. Иначе обработчики прерываний заблокируют всю остальную прошивку. Особенность однобитного ADC как раз в том, что частоты дискретизации могут быть весьма и весьма высокими.
3) Определить таймер в режим мастера
4) Установить счет в верх
5) Установить период таймера. В моем случае это 76.8 kHz.
6) Определить пределитель аппаратного таймера
7) Определить событие по которому аппаратный таймер подает сигнал. В моем случае надо ставить событие переполнения таймера (update event).
8) В регистре TIMx_DIER активировать бит 8 UDE: Update DMA request enable.
Регистры таймера 8 могут иметь вот такие значения
-->timer_diag_raw_reg 8 I,[Timer],Base:0x40010400,Cnt:20 +-----+-------------+-------+------------+------------+-------------------------------------------+ | N | Name |offset | Addr | ValHex | ValBin | +-----+-------------+-------+------------+------------+-------------------------------------------+ | 1 | TIMx_CR1 | 0x000 | 0x40010400 | 0x00000081 | 0b0000_0000|0000_0000|0000_0000|1000_0001 | | 2 | TIMx_CR2 | 0x004 | 0x40010404 | 0x00000020 | 0b0000_0000|0000_0000|0000_0000|0010_0000 | | 3 | TIMx_SMCR | 0x008 | 0x40010408 | 0x00000080 | 0b0000_0000|0000_0000|0000_0000|1000_0000 | | 4 | TIMx_DIER | 0x00c | 0x4001040c | 0x00000101 | 0b0000_0000|0000_0000|0000_0001|0000_0001 | | 5 | TIMx_SR | 0x010 | 0x40010410 | 0x0000001f | 0b0000_0000|0000_0000|0000_0000|0001_1111 | | 6 | TIMx_EGR | 0x014 | 0x40010414 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 7 | TIMx_CCMR1 | 0x018 | 0x40010418 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 8 | TIMx_CCMR2 | 0x01c | 0x4001041c | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 9 | TIMx_CCER | 0x020 | 0x40010420 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 10 | TIMx_CNT | 0x024 | 0x40010424 | 0x0000019f | 0b0000_0000|0000_0000|0000_0001|1001_1111 | | 11 | TIMx_PSC | 0x028 | 0x40010428 | 0x00000003 | 0b0000_0000|0000_0000|0000_0000|0000_0011 | | 12 | TIMx_ARR | 0x02c | 0x4001042c | 0x00000221 | 0b0000_0000|0000_0000|0000_0010|0010_0001 | | 13 | TIMx_RCR | 0x030 | 0x40010430 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 14 | TIMx_CCR1 | 0x034 | 0x40010434 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 15 | TIMx_CCR2 | 0x038 | 0x40010438 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 16 | TIMx_CCR3 | 0x03c | 0x4001043c | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 17 | TIMx_CCR4 | 0x040 | 0x40010440 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 18 | TIMx_BDTR | 0x044 | 0x40010444 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 19 | TIMx_DCR | 0x048 | 0x40010448 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 20 | TIMx_DMAR | 0x04c | 0x4001044c | 0x00000081 | 0b0000_0000|0000_0000|0000_0000|1000_0001 | +-----+-------------+-------+------------+------------+-------------------------------------------+
Таймер будет генерировать импульсы с точной частотой, запуская синхронные GPIO считывания.
Фаза 3: Настройка DMA канала для опроса GPIO
DMA — Сердце потоковой записи. Будет перекачивать (uint16_t) данные от GPIOA в RAM память.
1) Выбрать DMA канал, который можно триггерить событием по переполнению таймера 8. В нашем случае это DMA2 Stream1 Channel7.
2) Подать тактирование на DMA.
3) Указать, что данные берутся из регистра GPIOx->IDR (Input Data Register). Для порта А получается адрес 0x40020010.

4) Указать что данные надо копировать в RAM память.
5) Определить смешение по 16 бит. Это как раз разрядность GPIO на STM32.
6) Определить направление передачи данных от периферии к памяти
7) отключить увеличение адреса периферии
8) Включить увеличение адреса памяти
9) Определить количество семплов которые мы будем считывать за один прогон
10) Определить одиночный режим работы DMA. То есть запись с последующей остановкой DMA.
11) Активировать прерывани на событие окончания передачи по DMA (Transfer-Complete).
12) Активировать DMA прерывания в настройках NVIC.
Регистры DMA2 Stream1 могут иметь вот такой вид
-->dma_channel_reg_map 2 1 W,[DmaChannel]DMA2_Stream1,CH_0,, Regs I,[DmaChannel],Base:0x40026428,Cnt:6 +-----+--------------+-------+------------+------------+-------------------------------------------+ | N | Name |offset | Addr | ValHex | ValBin | +-----+--------------+-------+------------+------------+-------------------------------------------+ | 1 | DMA_SxCR | 0x000 | 0x40026428 | 0x0e032c06 | 0b0000_1110|0000_0011|0010_1100|0000_0110 | | 2 | DMA_SxNDTR | 0x004 | 0x4002642c | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 3 | DMA_SxPAR | 0x008 | 0x40026430 | 0x40020010 | 0b0100_0000|0000_0010|0000_0000|0001_0000 | | 4 | DMA_SxM0AR | 0x00c | 0x40026434 | 0x20004d8c | 0b0010_0000|0000_0000|0100_1101|1000_1100 | | 5 | DMA_SxM1AR | 0x010 | 0x40026438 | 0x00000000 | 0b0000_0000|0000_0000|0000_0000|0000_0000 | | 6 | DMA_SxFCR | 0x014 | 0x4002643c | 0x00000020 | 0b0000_0000|0000_0000|0000_0000|0010_0000 | +-----+--------------+-------+------------+------------+-------------------------------------------+
Фаза 4: Настройка внешних прерываний.
Для старта нашего однобитного ADC надо как-то дать ему сигнал. Это можно произвести через внешнее прерывание.
1) Выбрать пин для внешнего прерывания. В моем случае это тот же самый пин, который надо опрашивать: PA1.
2) Определить событие перепада. В моем случае это отрицательный перепад.
3) Определить обработчик внешнего прерывания по отрицательному перепаду напряжения на пине.
Недостатки однобитного ADC
1) Захватываются лишние биты. Происходит перерасход памяти. Если Вас интересует конкретный пин, то придется потом выделять этот бит из массива прочитанных слов.
Достоинства однобитного ADC
1) Центральный процессор не нагружается от потокового считывания GPIO. Всё происходит аппаратно в фоновом режиме для программы в main.
Приложения однобитного ADC
1) Вы можете сделать программный UART без существенного проседания производительности прошивки. Есть целая гора плат, где по разным причинам на разъёмы не выведен аппаратный UART. Захватывать поток однобитных семплов, а затем анализировать его. Делать синтаксический разбор байтов.
2) Логический анализатор. Вы можете захватывать поток цифровых данных с 16тибитной шины данных. Следить за параллельными шинами адресов и данных.
3) Черные ящики. Блоки телеметрии. Эти логические уровни можно писать в SD карту и потом анализировать переменные.
4) Диктофоны. Можно одним DMA и одним GPIO портом опрашивать до 16 PDM микрофонов. Записывать в файл, пропускать через DeltaSigma модулятор и получать PCM сигналы. Учитывая, что GPIO обычно много, то можно к одному микроконтроллеру подключать десятки и десятки цифровых MEMS PDM микрофонов.
5) Мультиметр. На основе прочитанного дампа можно вычислить частоту сигнала, скважность.
7) Можно захватывать для анализа прочие программные протоколы: 1-wire, NEC и т. д.
Отладка
Что ж, попробуем проверить как работает этот однобитный ADC. Для экспериментов я взял плату JL_32F4XX.

Джампером соединил выход UART2_TX на PA1. Активировал включение ADC по отрицательному перепаду на PA1. Отправил байт 0x55. Затем отправил команду для печати прочитанных по DMA семплов.

то же только текстом
Скрытый текст
-->tr sw_uart_rx_55 ; bin_adc_show_sample cmd_unit_test_run() argc 1 102.572,+10454,228,I,[TEST],key [sw_uart_rx_55] 102.572,+0,229,W,[TEST],KeyWord:sw_uart_rx_55,NoNum 102.573,+1,230,I,[TEST],test_sw_uart_rx_55() ********* test,OkTest,61/74,[Name:[sw_uart_rx_55],Fun:0x803aa65],FailCnt:0 102.875,+302,231,I,[TEST],TestDuration:303 ms=0.303 s=0.0050499998 min 102.875,+0,232,I,[TEST],Report 102.876,+1,233,I,[TEST],ok:1/74 102.877,+1,234,I,[TEST],failed:0 102.877,+0,235,I,[TEST],All 1 tests passed! 1:42--> 102.897,+20,236,I,[BinAdc],TIM8,FS:76800 Hz,Mem:0x20004d8c,MemSZ:104,N:1,InPad:[PA1],Dma:[DMA2_Stream1,CH_7,[GPIO_READ]],BIN_ADC1 102.897,+0,237,I,[BinAdc],start_cnt:6,busy_cnt:9,Spin:0,Init:On, 102.898,+1,238,I,[BinAdc],StartCnt:6 +-----+----------+--------+---------------------+ | Num | TS | Sam | BIN | +-----+----------+--------+---------------------+ | 0 | 0.000 | 0x5e18 | 0101_1110_0001_1000 | | 1 | 13.020u | 0x5c18 | 0101_1100_0001_1000 | | 2 | 26.041u | 0x5e18 | 0101_1110_0001_1000 | | 3 | 39.062u | 0x5e18 | 0101_1110_0001_1000 | | 4 | 52.083u | 0x5c18 | 0101_1100_0001_1000 | | 5 | 65.104u | 0x5c1e | 0101_1100_0001_1110 | | 6 | 78.125u | 0x5e1e | 0101_1110_0001_1110 | | 7 | 91.145u | 0x5c1e | 0101_1100_0001_1110 | | 8 | 0.104m | 0x5c1e | 0101_1100_0001_1110 | | 9 | 0.117m | 0x5e1e | 0101_1110_0001_1110 | | 10 | 0.130m | 0x5c1e | 0101_1100_0001_1110 | | 11 | 0.143m | 0x5c1e | 0101_1100_0001_1110 | | 12 | 0.156m | 0x5e1e | 0101_1110_0001_1110 | | 13 | 0.169m | 0x5e18 | 0101_1110_0001_1000 | | 14 | 0.182m | 0x5e18 | 0101_1110_0001_1000 | | 15 | 0.195m | 0x5e18 | 0101_1110_0001_1000 | | 16 | 0.208m | 0x5e18 | 0101_1110_0001_1000 | | 17 | 0.221m | 0x5e18 | 0101_1110_0001_1000 | | 18 | 0.234m | 0x5e18 | 0101_1110_0001_1000 | | 19 | 0.247m | 0x5e18 | 0101_1110_0001_1000 | | 20 | 0.260m | 0x5e18 | 0101_1110_0001_1000 | | 21 | 0.273m | 0x5e1e | 0101_1110_0001_1110 | | 22 | 0.286m | 0x5e1e | 0101_1110_0001_1110 | | 23 | 0.299m | 0x5e1e | 0101_1110_0001_1110 | | 24 | 0.312m | 0x5e1e | 0101_1110_0001_1110 | | 25 | 0.325m | 0x5e1e | 0101_1110_0001_1110 | | 26 | 0.338m | 0x5c1e | 0101_1100_0001_1110 | | 27 | 0.351m | 0x5e1e | 0101_1110_0001_1110 | | 28 | 0.364m | 0x5e1e | 0101_1110_0001_1110 | | 29 | 0.377m | 0x5c18 | 0101_1100_0001_1000 | | 30 | 0.390m | 0x5e18 | 0101_1110_0001_1000 | | 31 | 0.403m | 0x5e18 | 0101_1110_0001_1000 | | 32 | 0.416m | 0x5c18 | 0101_1100_0001_1000 | | 33 | 0.429m | 0x5e18 | 0101_1110_0001_1000 | | 34 | 0.442m | 0x5e18 | 0101_1110_0001_1000 | | 35 | 0.455m | 0x5e18 | 0101_1110_0001_1000 | | 36 | 0.468m | 0x5e18 | 0101_1110_0001_1000 | | 37 | 0.481m | 0x5e1e | 0101_1110_0001_1110 | | 38 | 0.494m | 0x5c1e | 0101_1100_0001_1110 | | 39 | 0.507m | 0x5c1e | 0101_1100_0001_1110 | | 40 | 0.520m | 0x5e1e | 0101_1110_0001_1110 | | 41 | 0.533m | 0x5e1e | 0101_1110_0001_1110 | | 42 | 0.546m | 0x5e1e | 0101_1110_0001_1110 | | 43 | 0.559m | 0x5e1e | 0101_1110_0001_1110 | | 44 | 0.572m | 0x5c1e | 0101_1100_0001_1110 | | 45 | 0.585m | 0x5c18 | 0101_1100_0001_1000 | | 46 | 0.598m | 0x5e18 | 0101_1110_0001_1000 | | 47 | 0.611m | 0x5e18 | 0101_1110_0001_1000 | | 48 | 0.625m | 0x5c18 | 0101_1100_0001_1000 | | 49 | 0.638m | 0x5e18 | 0101_1110_0001_1000 | | 50 | 0.651m | 0x5e18 | 0101_1110_0001_1000 | | 51 | 0.664m | 0x5c18 | 0101_1100_0001_1000 | | 52 | 0.677m | 0x5e18 | 0101_1110_0001_1000 | | 53 | 0.690m | 0x5c1e | 0101_1100_0001_1110 | | 54 | 0.703m | 0x5c1e | 0101_1100_0001_1110 | | 55 | 0.716m | 0x5e1e | 0101_1110_0001_1110 | | 56 | 0.729m | 0x5e1e | 0101_1110_0001_1110 | | 57 | 0.742m | 0x5c1e | 0101_1100_0001_1110 | | 58 | 0.755m | 0x5e1e | 0101_1110_0001_1110 | | 59 | 0.768m | 0x5e1e | 0101_1110_0001_1110 | | 60 | 0.781m | 0x5c1e | 0101_1100_0001_1110 | | 61 | 0.794m | 0x5e18 | 0101_1110_0001_1000 | | 62 | 0.807m | 0x5e18 | 0101_1110_0001_1000 | | 63 | 0.820m | 0x5e18 | 0101_1110_0001_1000 | | 64 | 0.833m | 0x5e18 | 0101_1110_0001_1000 | | 65 | 0.846m | 0x5c18 | 0101_1100_0001_1000 | | 66 | 0.859m | 0x5e18 | 0101_1110_0001_1000 | | 67 | 0.872m | 0x5e18 | 0101_1110_0001_1000 | | 68 | 0.885m | 0x5e18 | 0101_1110_0001_1000 | | 69 | 0.898m | 0x5e1e | 0101_1110_0001_1110 | | 70 | 0.911m | 0x5e1e | 0101_1110_0001_1110 | | 71 | 0.924m | 0x5c1e | 0101_1100_0001_1110 | | 72 | 0.937m | 0x5e1e | 0101_1110_0001_1110 | | 73 | 0.950m | 0x5e1e | 0101_1110_0001_1110 | | 74 | 0.963m | 0x5c1e | 0101_1100_0001_1110 | | 75 | 0.976m | 0x5e1e | 0101_1110_0001_1110 | | 76 | 0.989m | 0x5e1e | 0101_1110_0001_1110 | | 77 | 1.002m | 0x5c1e | 0101_1100_0001_1110 | | 78 | 1.015m | 0x5c1e | 0101_1100_0001_1110 | | 79 | 1.028m | 0x5e1e | 0101_1110_0001_1110 | | 80 | 1.041m | 0x5c1e | 0101_1100_0001_1110 | | 81 | 1.054m | 0x5e1e | 0101_1110_0001_1110 | | 82 | 1.067m | 0x5e1e | 0101_1110_0001_1110 | | 83 | 1.080m | 0x5e1e | 0101_1110_0001_1110 | | 84 | 1.093m | 0x5e1e | 0101_1110_0001_1110 | | 85 | 1.106m | 0x5e1e | 0101_1110_0001_1110 | | 86 | 1.119m | 0x5e1e | 0101_1110_0001_1110 | | 87 | 1.132m | 0x5c1e | 0101_1100_0001_1110 | | 88 | 1.145m | 0x5e1e | 0101_1110_0001_1110 | | 89 | 1.158m | 0x5e1e | 0101_1110_0001_1110 | | 90 | 1.171m | 0x5e1e | 0101_1110_0001_1110 | | 91 | 1.184m | 0x5e1e | 0101_1110_0001_1110 | | 92 | 1.197m | 0x5c1e | 0101_1100_0001_1110 | | 93 | 1.210m | 0x5e1e | 0101_1110_0001_1110 | | 94 | 1.223m | 0x5e1e | 0101_1110_0001_1110 | | 95 | 1.236m | 0x5e1e | 0101_1110_0001_1110 | | 96 | 1.250m | 0x5e1e | 0101_1110_0001_1110 | | 97 | 1.263m | 0x5e1e | 0101_1110_0001_1110 | | 98 | 1.276m | 0x5c1e | 0101_1100_0001_1110 | | 99 | 1.289m | 0x5e1e | 0101_1110_0001_1110 | | 100 | 1.302m | 0x5e1e | 0101_1110_0001_1110 | | 101 | 1.315m | 0x5e1e | 0101_1110_0001_1110 | | 102 | 1.328m | 0x5c1e | 0101_1100_0001_1110 | | 103 | 1.341m | 0x5e1e | 0101_1110_0001_1110 | +-----+----------+--------+---------------------+ 1:43-->
Как можно заметить (по первому биту в колонке BIN), ADC прочитал UART сигнал 0 10101010 11. То есть байт 0x55. Это явственно доказывает, что наш однобитный ADC в самом деле работает.
Итоги
Удалось научиться запускать однобитный ADC на основе сцепки GPIO, TIMER и DMA. Это позволяет разгрузить ядро микроконтроллера от сбора данных.
Такова специфика программирования микроконтроллеров. Собираешься делать одно (программный UART), а по ходу работы выясняется, что надо ещё целая куча всего другого (тот же однобитный ADC).
Данная технология открывает дорогу для создания многочисленных приложений на микроконтроллере, начиная с логических анализаторов, программных UART-ов, заканчивая многоканальными цифровыми фонографами.
Словарь
Акроним |
Расшифровка |
GPIO |
General-Purpose Input/Output |
ADC |
Analog-to-digital converter |
STM32 |
ST Microelectronics 32-bit |
UART |
Universal Asynchronous Receiver-Transmitter |
DMA |
Direct memory access |
RAM |
Random Access Memory |
Источники
Название |
URL |
Потоковая запись ADC семплов на STM32 |
|
STM32F4XX |
|
Demo прошивка однобитного ADC |
https://github.com/aabzel/Artifacts/tree/main/jl_32f4xx_bin_adc_gcc_m |
“Сигма дельта” или как сделать хорошую звуковую карту из STM32F401 @Sdima1357 |
|
AN4666 Application note |
Вопросы:
1) Почему разработчики микроконтроллеров не делают режим DMA peripheral to peripheral?
ivanstor
В электронике все — это либо АЦП либо ЦАП.
А как же пины земли и питания? :-)
aabzel Автор
Это уже не электроника, а электротехника.