
ADC модуль это основа любого электронного измерения. Основа любого DMM. Всё что за корпусом микроконтроллера это аналоговый мир. ADC это портал который позволяет аналоговым сигналам вкатиться в мир цифры.
Постановка задачи
Научиться записывать c PA3 (ADC1_IN3) на STM32F407VE 12-битные семплы в потоковом режиме на частоте дискретизации равной 8kHz. Для точной установки частоты дискретизации использовать аппаратный таймер TIM2 . Использовать встроенный в микроконтроллер ADC. Использовать ADC1. Использовать DMA. Записанные семплы складировать в FIFO. В случае переполнения FIFO выдавать ошибку. При этом не использовать частые прерывания. Не нарушать непрерывность записи семплов. Все эксперименты производить на отладочной плате JZ-F407VET6.
Как отлаживаться?
На плате есть свободные GPIO пины, можно использовать DAC, двухлучевой осциллограф и логический анализатор. Можно просматривать последний принятый семпл утилитой J-Scope через J-link программатор.
Реализация:
Фаза 1: Настройка GPIO
Сначала выберем пины. Учитывая схемотехнику электронной платы JZ-F407VET6 есть только такие варианты.

Я выбрал пин PA3. Этот пин надо сконфигурировать на аналоговую функцию.

Подтяжку к питанию отключить. PinMux выставить в ноль.

Фаза 2: Настройка аппаратного таймера
Для ЦОС алгоритмов важно извлекать ADC семплы с точным и стабильным периодом. То есть надо задавать частоту дискретизации. Аппаратный таймер для ADC выступает в роли метронома для пианиста, чтобы не выбиваться из ритма. ADC1 можно запускать таймерами 1, 2, 3, 4, 5 или 8
Для этого таймера я выбрал 32 битный TIM2 (0x40000000). Задал частоту счета 8k Hz, направление счета вверх. Настроил таймер на режим мастера и настроил генерировать событие (сигнал) при переполнении таймера (Update). Для отладки можно ещё включить прерывания и в обработчике по переполнению таймера переключать состояние какого-н GPIO. Это позволит убедиться, что частота в самом деле установилась как надо.
Таймер будет генерировать импульсы с точной частотой, запуская синхронные АЦП преобразования.
17:18-->timer_diag +----+-----+----------+-----+------------+-------+-------+---------+---------+--------+ |Num | En | busFreq | bit | periodReg | psc | dir |period,s | freq,Hz |busName | +----+-----+----------+-----+------------+-------+-------+---------+---------+--------+ | 2 | On | 84.0M | 32 | 656 | 16 | Up | 0.125m | 8.003k | APB1 | | 3 | On | 84.0M | 16 | 1000 | 84 | Up | 1.000m | 1.000k | APB1 | | 4 | On | 84.0M | 16 | 10500 | 8 | Up | 1.000m | 1.000k | APB1 | | 5 | On | 84.0M | 32 | 4294967295 | 65535 | Up | 3.351M | 0.298u | APB1 | | 8 | On | 168.0M | 16 | 10500 | 16 | Up | 1.000m | 1.000k | APB2 | +----+-----+----------+-----+------------+-------+-------+---------+---------+--------+ 17:30-->
Проверка логическим анализатором показала, что таймер в самом деле переполняется на частоте 8kHz.

Фаза 3: Настройка Analog-to-digital converter (ADC)
Внутри STM32F407VE заложено три 12 битных SAR-ADC. То есть значения которые мы будем получать будут варьироваться от 0 до 4095. Частота дискретизации может достигать 2.4 мега семплов в секунду. Я выбрал ADC1. ADC1 имеет номер прерывания 18. В распоряжении 16 каналов. Я выбрал канал номер три (PA3), разрешение 12 бит. Для настройки ADC надо подать тактирование на ADC подсистему. Режим непрерывного преобразования (continuous conversion) мне не нужен. Так как в этом режме преобразования происходят без паузы. Режим сканирования (Scan mode) мне пока тоже не нужен, так как я собираюсь читать напряжение только с одного единственного пина (PA3). Выбираю выравнивание значащих битов вправо (Right alignment), чтобы можно было визуализировать семплы в UART без лихних преобразований. Ключевой момент в том, чтобы выбрать событие для начала преобразования. Этим событием я выбрал переполнение таймера 2 (0110: Timer 2 TRGO event). Указываю, что надо работать в режиме DMA. Надо указать время накопления заряда на внутреннем конденсаторе в периодах тактирования.
static bool adc_compose_init(const AdcConfig_t* const Config, ADC_InitTypeDef* const pInit) { bool res = false; if(pInit) { pInit->ScanConvMode = DISABLE; pInit->ContinuousConvMode = DISABLE; pInit->NbrOfConversion = 1; pInit->DMAContinuousRequests = ENABLE; pInit->DataAlign = ADC_DATAALIGN_RIGHT; pInit->DiscontinuousConvMode = DISABLE; pInit->ExternalTrigConv = AdcExternalTriggerSourceToExternalTrigConv(Config->trigger_source); pInit->Resolution = AdcResolution2code(Config->resolution); pInit->ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; pInit->NbrOfDiscConversion = 1; pInit->ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISINGFALLING; pInit->EOCSelection = ADC_EOC_SEQ_CONV; res = true; } return res; }
Фаза 4: Настройка DMA
DMA — Сердце потоковой записи. Должно перекачивать данные (uint16_t) от ADC1 в память. Режим работы - peripheral to memory. DMA должно работать в сircular mode (режим цирка), то есть снова и снова переписывать in-place один и тот же массив, на каждой итерации увеличивать память, выравнивать запись по 16 бит. Сами семплы всегда извлекать из одного и того же регистра ADC1.DR.
#ifndef DMA_CHANNEL_CONFIG_ADC_H #define DMA_CHANNEL_CONFIG_ADC_H #ifdef __cplusplus extern "C" { #endif #include "dma_channel_types.h" #include "adc_config.h" bool CallBackHalfAdc1(void); bool CallBackDoneAdc1(void); /* Table 43. DMA2 request mapping */ #define DMA_CHANNEL_ADC1 \ { \ .num = DMA_CHANNEL_NUM_ADC1, \ .DmaPad = {.dma_num = 2, .channel = 0}, \ .name = "adc1", \ .dir = DMA_MCAL_DIR_PERIPH_TO_MEMORY, \ .CallBackHalf = CallBackHalfAdc1, \ .CallBackDone = CallBackDoneAdc1, \ .mem_inc = DMA_INC_ON, \ .per_inc = DMA_INC_OFF, \ .valid = true, \ .aligment_mem = DMA_ALIGNMENT_WORD, \ .aligment_per = DMA_ALIGNMENT_WORD, \ .mode = DMA_MODE_CIRCULAR, \ .interrupt_on = true, \ .priority = DMA_PRIOR_MED, \ .base_addr_source = (uint32_t) &(ADC1->DR), \ .base_addr_destination = (uint32_t) Adc1RxSamples, \ .move_size = (uint32_t) ADC1_RX_SAMPLE_CNT, \ .block_size = (uint32_t) DMA_MEMCPY_SIZE, \ .block_count = 1, \ .fifo = DMA_FIFO_OFF, \ .memory_burst = DMA_BURST_SINGLE, \ .periph_burst = DMA_BURST_SINGLE, \ }, #define DMA_CHANNEL_ADC \ DMA_CHANNEL_ADC1 #ifdef __cplusplus } #endif #endif /* DMA_CHANNEL_CONFIG_ADC_H */
C ADC1 может работать только DMA2, причем только каналы 0 и 4. Я выбрал канал ноль.

DMA надо настроить так, чтобы оно писало в циклическом режиме один и тот же массив. Данные следует выгребать в прерываниях по половинном заполнении и при полоном заполнении. Под выгребанием семплов подразумевается перекачка семплов из массива в очередь ADC1.RxFiFo.

Фаза 5: Организация FIFO
DMA в прерывании запускает две функции. Одна стартует при половинной записи массива, вторая при полной записи. Внутри этих прерываний надо успеть переложить семплы для обработки в приложении, пока не стартанет второй обработчик прерываний. Поэтому надо аккуратно извлечь семплы из массива приемника и переложить из в RxFiFo.
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { AdcHandle_t* Node = AdcHalHandle2Handle(hadc); if(Node) { Node->conv_done = true; Node->conv_done_cnt++; uint32_t i = 0 ; for(i=Node->RxSamplesCnt/2;i<Node->RxSamplesCnt;i++){ i_status ret = iqueue_enqueue(&Node->iQueue, &Node->RxSamples[i]); iqueue_ret_res(ret); } } } void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { AdcHandle_t* Node = AdcHalHandle2Handle(hadc); if(Node) { Node->half_cplt_done = true; Node->half_cplt_done_cnt++; uint32_t i = 0; for(i=0;i<Node->RxSamplesCnt/2;i++){ i_status ret = iqueue_enqueue(&Node->iQueue, &Node->RxSamples[i]); iqueue_ret_res(ret); } } }
Реализация FIFO это отдельная большая задача. Однако я выбрал готовую реализацию FIFO c open source репозитория разработчики-барсуки. Этот программный компонент называется iQueue.
i_status iqueue_init(iqueue_t* _queue, uint32_t _max_elements, size_t _element_size, void* _storage); i_status iqueue_enqueue(iqueue_t* _queue, void* _element); i_status iqueue_dequeue(iqueue_t* _queue, void* _element); i_status iqueue_size(iqueue_t* _queue, size_t* _size);
На время заполнения FiFo надо отключить DMA прерывания (активировать критическую секцию). Без этого может произойти одновременный доступ на изменение переменных FIFO в обработчике прерываний (push) и в функции main (pull). Это приведет к непредсказуемым значениям внутри FIFO.
Фаза 6: Обработка семплов в приложении
Понятное дело, что семплы мы записывали для, того чтобы их как-то обрабатывать. Обработка может быть весьма затратной в плане вычислительных ресурсов. В самом простом случае семплы можно просто отобразить в консоль или на экран. Как приложение получит семплы? Приложение просто будет извлекать семплы из очереди iQueue при помощи функции iqueue_dequeue (pull).
float AdcSample12ToVoltageVef3_3(const uint32_t sample) { float voltage_v = 0.0f; voltage_v = (3.3f *( (float) sample))/((float)ADC_MAX_VAL_12BIT); return voltage_v; } bool adc_proc_one(uint8_t num) { bool res = false; log_level_t ll = log_level_get(LG_ADC); AdcHandle_t* Node = AdcGetNode(num); if(Node) { i_status ret; size_t size = 0; ret = iqueue_size(&Node->iQueue, &size); if(I_OK==ret) { uint32_t i = 0; for(i=0; i<size; i++) { uint16_t sample = 0; ret = iqueue_dequeue(&Node->iQueue, (void*) &sample); if(I_OK == ret) { if(LOG_LEVEL_DEBUG == ll) { cli_printf("\rCode:%4u,%5.3f V", sample, AdcSample12ToVoltageVef3_3(sample)); } } } } } return res; }
Фаза 7: Отображение семплов в DAC
Вот мы научились получать какие-то 12-битные семплы. Где гарантия, что это в самом деле отсчеты напряжения, а не какие-то случайные значения в поломанной программе?
Можно отобразить записанные семплы обратно на улицу при помощи модуля DAC. Затем на двухлучевом осциллографе отобразить записываемый сигнал с отображаемым сигналом. Если есть корреляция, то значит связка ADC+DAC работает корректно. Внутри STM32F407VE заложено два канала 12-битного DAC модуля (базовый адрес DAC=0x40007400). На PCB JZ-F407VET6 эти пины выходят на P4.6 P4.3. Это прямо на удобную PLD вилку.
Pin Function | GPIO | LQFP100 | PinMux | Connector | Dir | Source |
DAC_OUT1 | PA4 | 29 | 0 | P4.6 | out | FromADC |
DAC_OUT2 | PA5 | 30 | 0 | P4.3 | out | FromDDS |
При этом, чтобы что-то отображать надо что-то записывать. Поэтому второй канал DAC я запрограммировал генерировать программный синус сигнал, а первый канал DAC я настроил просто отображать принятые ADC семплы. Получилось полностью аналоговое эхо. Чтобы успевать высвобождать очередь TxFIFO, отображение семплов происходит внутри обработчика прерываний по таймеру 2.

Для программной реализации генерации синуса пришлось написать отдельный программный компонент DDS (Direct Digital Synthesis). Реализация DDS - это отдельная большая задача. Его задача - обсчитывать семплы для генерации sin функции. Хороший DDS может генерировать все виды сигналов: PWM, SAW, Fence, Chirp, DTMF, BPSK и прочие.
float calc_sin_sample(uint64_t time_us, float frequency, float phase_ms, float des_amplitude, float in_offset) { float lineVal = 0.0f; float argument = 0.0f; float amplitude = 0.0f; float amplitude_scaled = 0.0f; float cur_time_ms = ((float)time_us) / 1000.0f; lineVal = ((cur_time_ms + phase_ms) / 1000.0f) * frequency; argument = 2.0f * M_PI * lineVal; amplitude = (float)sinf((float)argument); amplitude_scaled = (des_amplitude * amplitude) + in_offset; return amplitude_scaled; }
Как можно заметить, сигналы совпадают по частоте, амплитуде и форме. Значит связка ADC, DMA, RxFIFO,TxFIFO DAC работает корректно. Отличие по фазе пропорционально размеру TxFIFO.

Идеи проектов на ADC в STM32
Можно сделать тестер пальчиковых батареек типа AAA или AA. Померять значение напряжения, сравнить больше ли, чем 1,5V и, если больше, то зеленым светодиодом показать, что батарейка работает. Если измеренное напряжение меньше 1.5V, то включить красный светодиод.
Можно сделать мультиметр, измерять напряжения, сопротивления через делитель напряжения. Подключить аналоговый термопары.
Если подключить экран, то можно сделать осциллограф.
Между ADC и DAC можно реализовать какую-н цифровую фильтрацию сигнала. Сделать эхо эффект.
При помощи ADC делают непрерывное слежение за уровнем заряда батарей в BMS платах.
Результат
Удалось научиться читать приложенное к пину напряжение при помощи встроенного в STM32 модуля ADC. Как видите, чтобы просто прочитать напряжение при помощи ADC Вам надо настроить GPIO, ADC, DMA, TIMER. Добавить надёжную абстрактную структуру данных FIFO. Запустить UART, чтобы можно было всё это отлаживать через CLI. Плюс еще запустить DAC и реализовать программный компонент DDS, чтобы отлаживаться в режиме аналогового эха.
Такова специфика программирования микроконтроллеров. Собираешься делать одно, а по ходу работы выясняется, что надо ещё целая куча всего другого.
Отлаженный непрерывный приём ADC семплов открывает дорогу к полноценной DSP обработке принятых АЦП отсчетов.
Словарь
Сокращение | Расшифровка |
ADC | Analog-to-digital converter |
DMM | Digital Multi Meter |
DDS | Direct Digital Synthesis |
DMA | Direct memory access |
DAC | Digital-to-analog converter |
FIFO | first in, first out |
Ссылки
Название | URL |
АЦП преобразования в указанные моменты времени на STM32 @DIVON | |
Звуковая карта USB на STM32. Часть 2: Используем встроенный АЦП | |
Обзор учебно-тренировочной платы JZ-F407VET6 (или электронная парта) | |
Бинарь Demo прошивки для JZ-F407VET6 | https://github.com/aabzel/Artifacts/tree/main/jz_f407vet6_adc_dma_dac_gcc_m |
Реализация очереди IQUEUE | https://github.com/devcoons/iso15765-canbus/blob/master/lib/lib_iqueue.h |
STM32 ADC and DAC with DMA | |
Successive-approximation ADC | |
STM32F429: аналого-цифровые преобразователи (АЦП) | https://microsin.net/programming/arm/stm32f429-analog-to-digital-converters.html |
STM32 ADC (АЦП) и DMA. Обзор, настройка и пример проекта. | https://microtechnics.ru/stm32-adc-aczp-i-dma-obzor-nastrojka-i-primer-proekta/ |
Пуск I2S Трансивера на Artery [часть 2] (DMA, FSM, PipeLine) |
Вопросы
Зачем нужно выравнивание ADC семплов влево? Ведь это потом неудобно анализировать при печати.
Почему разработчики микроконтроллеров не делают режим DMA peripheral to peripheral?
ADC отсчёты всегда положительные 0.....4095. Как на лету отсеивать постоянную составляющую сигнала для последующей ЦОС обработки?
Как на STM32 DAC отобразить отрицательное значение из переменной в int16_t? Ведь DAC выставляет только напряжения от 0V.....до 3.3V.
