惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
GbyAI
GbyAI
Google DeepMind News
Google DeepMind News
Jina AI
Jina AI
B
Blog
aimingoo的专栏
aimingoo的专栏
酷 壳 – CoolShell
酷 壳 – CoolShell
T
The Blog of Author Tim Ferriss
Last Week in AI
Last Week in AI
月光博客
月光博客
H
Help Net Security
V
Visual Studio Blog
量子位
A
About on SuperTechFans
博客园 - Franky
人人都是产品经理
人人都是产品经理
N
Netflix TechBlog - Medium
云风的 BLOG
云风的 BLOG
雷峰网
雷峰网
Martin Fowler
Martin Fowler
Microsoft Security Blog
Microsoft Security Blog
博客园 - 叶小钗
P
Proofpoint News Feed
MongoDB | Blog
MongoDB | Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Потоковая запись ADC семплов на STM32
aabzel · 2026-04-26 · via Все публикации подряд на Хабре

ADC модуль это основа любого электронного измерения. Основа любого DMM. Всё что за корпусом микроконтроллера это аналоговый мир. ADC это портал который позволяет аналоговым сигналам переходить в мир цифры.

Постановка задачи
Научиться записывать c PA3 (ADC1_IN3) на STM32F407VE 12-битные семплы в потоковом режиме на частоте дискретизации равной 8kHz. Для точной установки частоты дискретизации использовать аппаратный таймер TIM2 . Использовать встроенный в микроконтроллер ADC. Использовать ADC1. Использовать DMA. Записанные семплы складировать в FIFO. В случае переполнения FIFO выдавать ошибку. При этом не использовать частые прерывания. Не нарушать непрерывность записи семплов. Все эксперименты производить на отладочной плате JZ-F407VET6.

Как отлаживаться?
На плате есть свободные GPIO пины, можно использовать DAC, осциллограф и логический анализатор.

Реализация:

Фаза 1: Настройка GPIO

Сначала выберем пины. Учитывая схемотехнику JZ-F407VET6 есть только такие варианты.

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

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

Фаза 2: Настройка таймера

Для ЦОС алгоритмов важно извлекать ADC семплы с точным и стабильным периодом. То есть надо задавать частоту дискретизации. Аппаратный таймер для ADC выступает в роли камертона для пианиста, чтобы не выбиваться из ритма. ADC можно запускать таймерами 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), чтобы можно было визуализировать семплы без преобразований. Ключевой момент в том, чтобы выбрать событие для начала преобразования. Этим событием я выбрал переполнение таймера 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 — Сердце потоковой записи. Должно перекачивать данные от ADC в память. Peripheral To Memory . DMA должно работать в режиме цирка (Circular mode), на каждой итерации увеличивать память, выравнивать запись по 16 бит.

#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. Я выбрал канал ноль.

 Table 43. DMA2 request mapping

Table 43. DMA2 request mapping

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 в обработчике прерываний и в функции main. Это приведет к непредсказуемым значениям внутри FIFO.

Фаза 6: Обработка семплов в приложении

Понятное дело, что семплы мы записывали для того чтобы их как-то обрабатывать. Обработка может быть весьма затратной в плане вычислительных ресурсов. В самом простом случае семплы можно просто отобразить в консоль или на экран. Как приложение получит семплы? Приложение просто будет извлекать семплы из очереди iQueue при помощи функции iqueue_dequeue.

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
pin

PinMux

Connector

Dir

DAC_OUT1

PA4

29

0

P4.6

out

DAC_OUT2

PA5

30

0

P4.3

out

При этом чтобы что-то отображать надо что-то записывать. Поэтому второй канал 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

1--Можно сделать тестер пальчиковых батареек типа AAA или AA. Померять значение напряжения, сравнить больше ли чем 1,5V и, если больше, то зеленым светодиодом показать что батарейка работает. Если измеренное напряжение меньше 1.5V, то включить красный светодиод.

2--Можно сделать мультиметр, измерять напряжения, сопротивления через делитель напряжения. Подключить аналоговый термопары.

3--Если подключить экран то можно сделать осциллограф.

4--Между ADC и DAC можно реализовать какую-н цифровую фильтрацию сигнала. Сделать эхо эффект.

5--При помощи ADC делают непрерывное слежение за уровнем заряда батарей в BMS платах.

Результат

Удалось научиться читать приложенное к пину напряжение при помощи встроенного в STM32 модуля ADC. Как видите, чтобы просто прочитать напряжение при помощи ADC Вам надо настроить GPIO, ADC, DMA, TIMER. Добавить надёжную абстрактную структуру данных FIFO. Запустить UART, чтобы можно было всё это отлаживать через CLI. Плюс еще запустить DAC и реализовать программный компонент DDS, чтобы отлаживаться в режиме аналогового эха.

Такова специфика программирования микроконтроллеров. Собираешься делать одно, а по ходу работы выясняется, что надо ещё целая куча всего другого.

Словарь

Сокращение

Расшифровка

ADC

Analog-to-digital converter

DDS

Direct Digital Synthesis

DMA

Direct memory access

DAC

Digital-to-analog converter

FIFO

first in, first out

Ссылки

Название

URL

АЦП преобразования в указанные моменты времени на STM32 @DIVON

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

Звуковая карта USB на STM32. Часть 2: Используем встроенный АЦП

@dmitriyrudnev

https://habr.com/ru/companies/ruvds/articles/694640/

Обзор учебно-тренировочной платы JZ-F407VET6 (или электронная парта)

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

Реализация очереди IQUEUE

https://github.com/devcoons/iso15765-canbus/blob/master/lib/lib_iqueue.h

STM32 ADC and DAC with DMA

https://stm32world.com/wiki/STM32\_ADC\_and\_DAC\_with\_DMA

Successive-approximation ADC

https://en.wikipedia.org/wiki/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)

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

Вопросы

--Зачем нужно выравнивание ADC семплов влево? Ведь это потом неудобно анализировать при печати.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

100%да1

0%нет0

Проголосовал 1 пользователь. Воздержавшихся нет.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

100%да1

0%нет0

Проголосовал 1 пользователь. Воздержавшихся нет.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

100%да1

0%нет0

Проголосовал 1 пользователь. Воздержавшихся нет.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

100%да1

0%нет0

Проголосовал 1 пользователь. Воздержавшихся нет.