STM32 零基础可移植教程 16:ADC + DMA 连续采样,为什么不用 CPU 一直搬数据

STM32 零基础可移植教程 16:ADC + DMA 连续采样,为什么不用 CPU 一直搬数据

上一篇我们做了 ADC 多通道扫描。

流程大概是:

bash 复制代码
启动 ADC

等待第 1 个通道转换完成

读 CH0

等待第 2 个通道转换完成

读 CH1

等待第 3 个通道转换完成

读 CH2

这个方式很适合入门,因为每一步都看得见。

但工程里如果 ADC 采样频率高一点,通道多一点,CPU 就会很累。

比如你要一直采三路电压:

bash 复制代码
CH0、CH1、CH2、CH0、CH1、CH2、CH0、CH1、CH2...

如果每个数据都让 CPU 去等、去拿、去放进数组,那 CPU 就像站在快递柜旁边,来一个包裹拿一个包裹。

DMA 的作用就是:

bash 复制代码
ADC 转换完一个数据后,DMA 自动把数据搬到内存数组

CPU 不用每个点都盯着

这一篇我们继续沿用第 15 篇的硬件思路:

bash 复制代码
三路 PWM 输出不同占空比

三根杜邦线接回三路 ADC 输入

ADC 扫描三路输入

DMA 自动把结果搬进数组

主循环只负责算平均值和串口打印

这篇只解决一个明确目标:

bash 复制代码
用 ADC + DMA Circular 模式连续采集三路 PWM 平均电压

先不做复杂滤波,不做多任务,不做 FreeRTOS,也不把 DMA 和串口 DMA 混在一起。

先把 ADC + DMA 这条链路跑通。

本篇目标

最终现象:

bash 复制代码
PWM0 = 25%

PWM1 = 50%

PWM2 = 75%


ADC + DMA 后台连续采样

串口持续输出三路平均电压

串口大概会看到:

bash 复制代码
ADC DMA circular test

PWM0=25%, PWM1=50%, PWM2=75%

DMA avg(32 scans): CH0=825 mV, CH1=1650 mV, CH2=2475 mV

DMA avg(32 scans): CH0=824 mV, CH1=1648 mV, CH2=2473 mV

数值不需要完全一样。

只要大致满足:

bash 复制代码
CH0 < CH1 < CH2

并且接近 25%、50%、75% 对应的平均电压,就说明方向是对的。

本篇用到的外设:

bash 复制代码
TIM PWM Output

ADC Regular Scan

DMA Circular

USART printf

准备工作

你需要准备:

|

项目

|

说明

|

| --- | --- |

|

STM32 开发板

|

任意带 ADC、DMA、PWM 的 STM32 都可以

|

|

下载器

|

ST-LINK/V2 或板载 ST-LINK

|

|

杜邦线

|

3 根,用来把三路 PWM 接回三路 ADC

|

|

串口工具

|

用来看 DMA 采样结果

|

|

原理图

|

确认 PWM 输出脚和 ADC 输入脚

|

本篇建议直接复制第 15 篇工程。

因为第 15 篇已经有:

bash 复制代码
三路 PWM 输出

三路 ADC 输入

串口 printf

Rank 1/2/3 扫描顺序

这篇只是在 ADC 后面加 DMA。

硬件连接仍然是:

bash 复制代码
PWM_OUT0 -> ADC_IN0

PWM_OUT1 -> ADC_IN1

PWM_OUT2 -> ADC_IN2

再次提醒:

bash 复制代码
PWM 输出脚和 ADC 输入脚不是同一个脚

PWM 负责造信号,ADC 负责读信号。

DMA 到底帮我们做了什么

不用 DMA 时,CPU 要做很多重复动作:

bash 复制代码
ADC 转换完成了吗?

好了,CPU 读一次 ADC 数据寄存器

放到变量里


ADC 又转换完成了吗?

好了,CPU 再读一次

再放到变量里

这个过程本身不难,但是很占 CPU 注意力。

用了 DMA 后,流程变成:

bash 复制代码
ADC 转换完成一个数据

DMA 自动从 ADC 数据寄存器搬到内存数组

搬完一个,数组下标自动往后走

搬满一半,通知 CPU

搬满一整圈,再通知 CPU

CPU 不需要每个采样点都去等。

它只需要在合适的时候处理一块已经采好的数据。

可以把 DMA 理解成一个"后台搬运工":

bash 复制代码
ADC 负责生产数据

DMA 负责搬数据

内存数组负责存数据

CPU 负责处理一整块数据

先看懂 DMA 数组里数据怎么排

这是 ADC 多通道 DMA 最容易把新手绕晕的地方。

我们配置 3 个 ADC 通道:

bash 复制代码
Rank 1 -> CH0

Rank 2 -> CH1

Rank 3 -> CH2

ADC 每扫描一轮,会产生 3 个数据:

bash 复制代码
CH0, CH1, CH2

DMA 会按转换顺序把它们放进数组。

所以数组不是这样排的:

bash 复制代码
CH0, CH0, CH0, CH0...

CH1, CH1, CH1, CH1...

CH2, CH2, CH2, CH2...

而是这样排的:

bash 复制代码
buffer[0] = 第 1 轮 CH0

buffer[1] = 第 1 轮 CH1

buffer[2] = 第 1 轮 CH2


buffer[3] = 第 2 轮 CH0

buffer[4] = 第 2 轮 CH1

buffer[5] = 第 2 轮 CH2


buffer[6] = 第 3 轮 CH0

buffer[7] = 第 3 轮 CH1

buffer[8] = 第 3 轮 CH2

也就是:

bash 复制代码
CH0, CH1, CH2, CH0, CH1, CH2, CH0, CH1, CH2...

代码里算平均值时,就要按这个规律取:

bash 复制代码
buffer[scan * 
3
 + 
0
] -> CH0

buffer[scan * 
3
 + 
1
] -> CH1

buffer[scan * 
3
 + 
2
] -> CH2

这里的 0/1/2 对应的是 ADC Rank 顺序,不是天然对应 ADC 通道号。

为什么要用 Circular 模式

DMA 常见有两种模式:

|

模式

|

含义

|

本篇是否使用

|

| --- | --- | --- |

|

Normal

|

搬满指定长度后停止

|

暂不使用

|

|

Circular

|

搬满后从数组开头继续搬

|

使用

|

我们希望 ADC 一直采样,所以选:

bash 复制代码
DMA Circular

这样 DMA buffer 会像一个环:

bash 复制代码
前半段填满 -> 回调通知 CPU

后半段填满 -> 回调通知 CPU

再回到前半段继续填

本篇代码把数组分成两半:

bash 复制代码
前半段:32 轮扫描,每轮 3 个通道,共 96 个数据

后半段:32 轮扫描,每轮 3 个通道,共 96 个数据

总数组长度是:

bash 复制代码
3 通道 x 32 轮 x 2 半 = 192 个数据

当 DMA 搬满前 96 个数据时,会进入:

bash 复制代码
HAL_ADC_ConvHalfCpltCallback()

当 DMA 搬满后 96 个数据时,会进入:

bash 复制代码
HAL_ADC_ConvCpltCallback()

我们的回调里不算平均值,只做一件事:

bash 复制代码
告诉主循环:有一半数据准备好了

真正的计算和 printf 放在主循环里做。

这是一个好习惯。

中断和回调里尽量少干活,主循环里再处理业务。

CubeMX 配置步骤

1. 复制第 15 篇工程

建议复制第 15 篇工程,改名为:

bash 复制代码
16_adc_dma

如果你重新建工程,也可以按前面的流程走:

  1. 选择芯片型号;

  2. SYS -> Debug 设置为 Serial Wire

  3. 配好 USART,用于 printf;

  4. 配好三路 PWM;

  5. 配好三路 ADC;

  6. 再加 DMA。

2. 保留三路 PWM 输出

这部分和第 15 篇一样。

本篇代码默认三路 PWM 来自:

bash 复制代码
TIM3_CH1

TIM3_CH2

TIM3_CH3

如果你的开发板实际能用的是:

bash 复制代码
PB5 / TIM3_CH2

PB0 / TIM3_CH3

PB1 / TIM3_CH4

那就配置 TIM3 的 CH2、CH3、CH4。

代码里后面把通道宏改成:

bash 复制代码
#define APP_TEST_PWM_CH0 TIM_CHANNEL_2

#define APP_TEST_PWM_CH1 TIM_CHANNEL_3

#define APP_TEST_PWM_CH2 TIM_CHANNEL_4

PWM 推荐先保持:

bash 复制代码
频率:1 kHz

占空比:代码里设置成 25%、50%、75%

3. 配置 ADC 三通道扫描

ADC 还是配置 3 个 Regular Channel。

示例:

bash 复制代码
Rank 1 -> ADC_CHANNEL_1

Rank 2 -> ADC_CHANNEL_2

Rank 3 -> ADC_CHANNEL_3

对应接线:

bash 复制代码
PWM_OUT0 -> PA1 / ADC1_IN1

PWM_OUT1 -> PA2 / ADC1_IN2

PWM_OUT2 -> PA3 / ADC1_IN3

ADC 参数推荐:

|

配置项

|

推荐值

|

说明

|

| --- | --- | --- |

|

Scan Conversion Mode

|

Enable

|

必须开,多通道扫描

|

|

Continuous Conversion Mode

|

Enable

|

必须连续转换,配合 DMA 后台不断采样

|

|

Discontinuous Conversion Mode

|

Disable

|

新手先不要开

|

|

External Trigger Conversion Source

|

Regular Conversion launched by software

|

软件启动一次,后面连续转换

|

|

Data Alignment

|

Right alignment

|

常用右对齐

|

|

Number Of Conversion

|

3

|

一轮扫描 3 个通道

|

|

DMA Continuous Requests

|

Enable

|

如果你的 CubeMX 有这个选项,打开

|

Sampling Time 可以先选长一点:

bash 复制代码
55.5 Cycles

或者:

bash 复制代码
71.5 Cycles

采样速度不是本篇重点,先让结果稳定。

4. 给 ADC 添加 DMA

进入 ADC 的 DMA Settings。

添加一个 DMA 请求。

对于 STM32F103 这类芯片,ADC1 常见对应:

bash 复制代码
DMA1 Channel1

不同芯片系列名字可能不一样,以 CubeMX 显示为准。

DMA 参数推荐:

|

配置项

|

推荐值

|

说明

|

| --- | --- | --- |

|

Direction

|

Peripheral to Memory

|

ADC 数据寄存器搬到内存

|

|

Mode

|

Circular

|

搬满后循环继续

|

|

Peripheral Increment

|

Disable

|

ADC 数据寄存器地址不变

|

|

Memory Increment

|

Enable

|

内存数组地址要往后走

|

|

Peripheral Data Width

|

Half Word

|

ADC 12 位数据,用 16 位存

|

|

Memory Data Width

|

Half Word

|

数组用 uint16_t

|

|

Priority

|

Low / Medium

|

入门先用默认或中等

|

5. 打开 DMA 中断

DMA 要触发半满和全满回调,需要 DMA 中断。

在 CubeMX 里确认:

bash 复制代码
DMA interrupt 已启用

对于 STM32F103,可能看到:

bash 复制代码
DMA1 Channel1 global interrupt

并且生成的 stm32f1xx_it.c 里应该有类似:

bash 复制代码
void DMA1_Channel1_IRQHandler(void)
{

  HAL_DMA_IRQHandler(&hdma_adc1);

}

如果没有这个中断函数,DMA 可能能搬数据,但半满/全满回调进不来。

6. 确认 MX_DMA_Init 的位置

CubeMX 通常会在 main.c 里生成:

bash 复制代码
MX_GPIO_Init();

MX_DMA_Init();

MX_ADC1_Init();

MX_TIM3_Init();

MX_USART1_UART_Init();

注意:

bash 复制代码
MX_DMA_Init() 一般要在 MX_ADC1_Init() 前面

因为 ADC 初始化时会把 ADC 句柄和 DMA 句柄关联起来。

如果顺序不对,后面 HAL_ADC_Start_DMA() 可能出问题。

Keil 工程生成和编译

打开 Keil 后,先编译 CubeMX 生成的工程:

bash 复制代码
Build / F7

确认没有错误:

bash 复制代码
0 Error(s)

然后添加本篇代码文件。

本篇新增 4 个文件:

bash 复制代码
Core/Inc/app_adc_dma.h

Core/Src/app_adc_dma.c

Core/Inc/app_test_pwm_multi.h

Core/Src/app_test_pwm_multi.c

app_test_pwm_multi.h/.c 和第 15 篇一样,用来输出三路 PWM。

app_adc_dma.h/.c 是本篇重点,用来启动 ADC DMA、处理半缓冲和全缓冲数据。

如果你手动新建 .c 文件,记得在 Keil 工程树里添加:

bash 复制代码
Core/Src/app_adc_dma.c

Core/Src/app_test_pwm_multi.c

完整代码

1. 新建 Core/Inc/app_adc_dma.h

bash 复制代码
#ifndef APP_ADC_DMA_H

#define APP_ADC_DMA_H


#include "main.h"

#include <stdint.h>


#define APP_ADC_DMA_CHANNEL_COUNT       3u

#define APP_ADC_DMA_SCAN_COUNT_PER_BLOCK 32u


typedef
struct{

    
uint32_t
 raw[APP_ADC_DMA_CHANNEL_COUNT];

    
uint32_t
 voltage_mv[APP_ADC_DMA_CHANNEL_COUNT];

    
uint32_t
 scan_count;

} App_ADCDMA_Result;


void App_ADCDMA_Init(void)
;

HAL_StatusTypeDef App_ADCDMA_Start(void)
;

void App_ADCDMA_Stop(void)
;

uint8_t App_ADCDMA_GetResult(App_ADCDMA_Result *result)
;


#endif

这里有两个宏要看懂:

bash 复制代码
#define APP_ADC_DMA_CHANNEL_COUNT        3u

#define APP_ADC_DMA_SCAN_COUNT_PER_BLOCK 32u

意思是:

bash 复制代码
每轮扫描 3 个通道

每半个 DMA buffer 里放 32 轮扫描

所以每半块数据长度是:

bash 复制代码
3 x 32 = 96 个 ADC 数据

2. 新建 Core/Src/app_adc_dma.c

bash 复制代码
#include "app_adc_dma.h"


#ifndef APP_ADC_DMA_HANDLE

#define APP_ADC_DMA_HANDLE hadc1

#endif


#ifndef APP_ADC_DMA_VREF_MV

#define APP_ADC_DMA_VREF_MV 3300u

#endif


#ifndef APP_ADC_DMA_MAX_RAW

#define APP_ADC_DMA_MAX_RAW 4095u

#endif


#ifndef APP_ADC_DMA_ENABLE_CALIBRATION

#define APP_ADC_DMA_ENABLE_CALIBRATION 0u

#endif


#define APP_ADC_DMA_BLOCK_LENGTH \    (APP_ADC_DMA_CHANNEL_COUNT * APP_ADC_DMA_SCAN_COUNT_PER_BLOCK)


#define APP_ADC_DMA_BUFFER_LENGTH \    (APP_ADC_DMA_BLOCK_LENGTH * 2u)


extern
 ADC_HandleTypeDef APP_ADC_DMA_HANDLE;


static
uint16_t
 s_adc_dma_buffer[APP_ADC_DMA_BUFFER_LENGTH];

static
volatile
uint8_t
 s_half_ready = 
0u
;

static
volatile
uint8_t
 s_full_ready = 
0u
;


static uint32_t App_ADCDMA_RawToVoltageMv(uint32_t raw)
{

    
return
 (raw * APP_ADC_DMA_VREF_MV) / APP_ADC_DMA_MAX_RAW;

}


static void App_ADCDMA_CalcBlock(uint32_t start_index, App_ADCDMA_Result *result)
{

    
uint64_t
 sum[APP_ADC_DMA_CHANNEL_COUNT] = {
0u
};

    
uint32_t
 scan;

    
uint8_t
 ch;


    
for
 (scan = 
0u
; scan < APP_ADC_DMA_SCAN_COUNT_PER_BLOCK; scan++)

    {

        
for
 (ch = 
0u
; ch < APP_ADC_DMA_CHANNEL_COUNT; ch++)

        {

            sum[ch] += s_adc_dma_buffer[start_index +

                                        (scan * APP_ADC_DMA_CHANNEL_COUNT) +

                                        ch];

        }

    }


    
for
 (ch = 
0u
; ch < APP_ADC_DMA_CHANNEL_COUNT; ch++)

    {

        result->raw[ch] = (
uint32_t
)(sum[ch] / APP_ADC_DMA_SCAN_COUNT_PER_BLOCK);

        result->voltage_mv[ch] = App_ADCDMA_RawToVoltageMv(result->raw[ch]);

    }


    result->scan_count = APP_ADC_DMA_SCAN_COUNT_PER_BLOCK;

}


void App_ADCDMA_Init(void)
{

    s_half_ready = 
0u
;

    s_full_ready = 
0u
;


#if APP_ADC_DMA_ENABLE_CALIBRATION

    (
void
)HAL_ADCEx_Calibration_Start(&APP_ADC_DMA_HANDLE);

#endif

}


HAL_StatusTypeDef App_ADCDMA_Start(void)
{

    s_half_ready = 
0u
;

    s_full_ready = 
0u
;


    
return
 HAL_ADC_Start_DMA(&APP_ADC_DMA_HANDLE,

                             (
uint32_t
 *)s_adc_dma_buffer,

                             APP_ADC_DMA_BUFFER_LENGTH);

}


void App_ADCDMA_Stop(void)
{

    (
void
)HAL_ADC_Stop_DMA(&APP_ADC_DMA_HANDLE);

}


uint8_t App_ADCDMA_GetResult(App_ADCDMA_Result *result)
{

    
uint32_t
 start_index;

    
uint8_t
 has_block = 
0u
;


    
if
 (result == 
0
)

    {

        
return
0u
;

    }


    __disable_irq();

    
if
 (s_half_ready != 
0u
)

    {

        s_half_ready = 
0u
;

        start_index = 
0u
;

        has_block = 
1u
;

    }

    
else
if
 (s_full_ready != 
0u
)

    {

        s_full_ready = 
0u
;

        start_index = APP_ADC_DMA_BLOCK_LENGTH;

        has_block = 
1u
;

    }

    
else

    {

        start_index = 
0u
;

    }

    __enable_irq();


    
if
 (has_block == 
0u
)

    {

        
return
0u
;

    }


    App_ADCDMA_CalcBlock(start_index, result);


    
return
1u
;

}


void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{

    
if
 (hadc == &APP_ADC_DMA_HANDLE)

    {

        s_half_ready = 
1u
;

    }

}


void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{

    
if
 (hadc == &APP_ADC_DMA_HANDLE)

    {

        s_full_ready = 
1u
;

    }

}

这段代码里有几个重点。

第一,DMA buffer 是 uint16_t

bash 复制代码
static
 
uint16_t
 s_adc_dma_buffer[APP_ADC_DMA_BUFFER_LENGTH];

因为 ADC 是 12 位结果,用 16 位存比较合适。

第二,启动 DMA 时传入整个 buffer:

bash 复制代码
HAL_ADC_Start_DMA(&APP_ADC_DMA_HANDLE,

                  (
uint32_t
 *)s_adc_dma_buffer,

                  APP_ADC_DMA_BUFFER_LENGTH);

APP_ADC_DMA_BUFFER_LENGTH 是转换次数,不是字节数。

第三,回调里只置标志:

bash 复制代码
s_half_ready = 
1u
;

s_full_ready = 
1u
;

不要在回调里 printf()

也不要在回调里做很长的平均计算。

第四,App_ADCDMA_GetResult() 里用了:

bash 复制代码
__disable_irq();

...

__enable_irq();

这里是在保护 s_half_readys_full_ready 这两个标志。

因为它们会在 DMA 中断回调里被修改,也会在主循环里被读取和清零。

这个点前面"临界区"番外已经讲过,这里就是一个真实应用场景。

3. 新建 Core/Inc/app_test_pwm_multi.h

这个文件和第 15 篇一致,用来输出三路 PWM。

bash 复制代码
#ifndef APP_TEST_PWM_MULTI_H

#define APP_TEST_PWM_MULTI_H


#include "main.h"

#include <stdint.h>


#define APP_TEST_PWM_COUNT 3u


void App_TestPWMMulti_Init(void)
;

HAL_StatusTypeDef App_TestPWMMulti_Start(void)
;

void App_TestPWMMulti_Stop(void)
;

void App_TestPWMMulti_SetFrequency(uint32_t frequency_hz)
;

void App_TestPWMMulti_SetDutyPermille(uint8_t index, uint16_t duty_permille)
;

void App_TestPWMMulti_SetAllDuty(uint16_t duty0_permille,                                 uint16_t duty1_permille,                                 uint16_t duty2_permille)
;


#endif

4. 新建 Core/Src/app_test_pwm_multi.c

bash 复制代码
#include "app_test_pwm_multi.h"


#ifndef APP_TEST_PWM_MULTI_HANDLE

#define APP_TEST_PWM_MULTI_HANDLE htim3

#endif


#ifndef APP_TEST_PWM_CH0

#define APP_TEST_PWM_CH0 TIM_CHANNEL_1

#endif


#ifndef APP_TEST_PWM_CH1

#define APP_TEST_PWM_CH1 TIM_CHANNEL_2

#endif


#ifndef APP_TEST_PWM_CH2

#define APP_TEST_PWM_CH2 TIM_CHANNEL_3

#endif


#ifndef APP_TEST_PWM_MULTI_COUNTER_CLK_HZ

#define APP_TEST_PWM_MULTI_COUNTER_CLK_HZ 1000000u

#endif


#ifndef APP_TEST_PWM_MULTI_DEFAULT_FREQ_HZ

#define APP_TEST_PWM_MULTI_DEFAULT_FREQ_HZ 1000u

#endif


#ifndef APP_TEST_PWM_MULTI_MAX_ARR

#define APP_TEST_PWM_MULTI_MAX_ARR 0xFFFFu

#endif


extern
 TIM_HandleTypeDef APP_TEST_PWM_MULTI_HANDLE;


static
const
uint32_t
 s_pwm_channel[APP_TEST_PWM_COUNT] =

{

    APP_TEST_PWM_CH0,

    APP_TEST_PWM_CH1,

    APP_TEST_PWM_CH2

};


static
uint32_t
 s_period_counts = 
1000u
;

static
uint16_t
 s_duty_permille[APP_TEST_PWM_COUNT] = {
250u
, 
500u
, 
750u
};


void App_TestPWMMulti_Init(void)
{

    App_TestPWMMulti_SetFrequency(APP_TEST_PWM_MULTI_DEFAULT_FREQ_HZ);

    App_TestPWMMulti_SetAllDuty(
250u
, 
500u
, 
750u
);

}


HAL_StatusTypeDef App_TestPWMMulti_Start(void)
{

    HAL_StatusTypeDef status;

    
uint8_t
 i;


    
for
 (i = 
0u
; i < APP_TEST_PWM_COUNT; i++)

    {

        status = HAL_TIM_PWM_Start(&APP_TEST_PWM_MULTI_HANDLE, s_pwm_channel[i]);

        
if
 (status != HAL_OK)

        {

            
return
 status;

        }

    }


    
return
 HAL_OK;

}


void App_TestPWMMulti_Stop(void)
{

    
uint8_t
 i;


    
for
 (i = 
0u
; i < APP_TEST_PWM_COUNT; i++)

    {

        HAL_TIM_PWM_Stop(&APP_TEST_PWM_MULTI_HANDLE, s_pwm_channel[i]);

    }

}


void App_TestPWMMulti_SetFrequency(uint32_t frequency_hz)
{

    
uint32_t
 arr;


    
if
 (frequency_hz == 
0u
)

    {

        App_TestPWMMulti_Stop();

        
return
;

    }


    s_period_counts = APP_TEST_PWM_MULTI_COUNTER_CLK_HZ / frequency_hz;

    
if
 (s_period_counts < 
2u
)

    {

        s_period_counts = 
2u
;

    }


    arr = s_period_counts - 
1u
;

    
if
 (arr > APP_TEST_PWM_MULTI_MAX_ARR)

    {

        arr = APP_TEST_PWM_MULTI_MAX_ARR;

        s_period_counts = arr + 
1u
;

    }


    __HAL_TIM_SET_AUTORELOAD(&APP_TEST_PWM_MULTI_HANDLE, arr);

    App_TestPWMMulti_SetAllDuty(s_duty_permille[
0
], s_duty_permille[
1
], s_duty_permille[
2
]);

    __HAL_TIM_SET_COUNTER(&APP_TEST_PWM_MULTI_HANDLE, 
0u
);

    __HAL_TIM_GENERATE_EVENT(&APP_TEST_PWM_MULTI_HANDLE, TIM_EVENTSOURCE_UPDATE);

}


void App_TestPWMMulti_SetDutyPermille(uint8_t index, uint16_t duty_permille)
{

    
uint32_t
 ccr;


    
if
 (index >= APP_TEST_PWM_COUNT)

    {

        
return
;

    }


    
if
 (duty_permille > 
1000u
)

    {

        duty_permille = 
1000u
;

    }


    s_duty_permille[index] = duty_permille;

    ccr = (s_period_counts * duty_permille) / 
1000u
;


    __HAL_TIM_SET_COMPARE(&APP_TEST_PWM_MULTI_HANDLE, s_pwm_channel[index], ccr);

}


void App_TestPWMMulti_SetAllDuty(uint16_t duty0_permille,                                 uint16_t duty1_permille,                                 uint16_t duty2_permille)
{

    App_TestPWMMulti_SetDutyPermille(
0u
, duty0_permille);

    App_TestPWMMulti_SetDutyPermille(
1u
, duty1_permille);

    App_TestPWMMulti_SetDutyPermille(
2u
, duty2_permille);

}

main.c 调用方式

1. 添加头文件

main.c 顶部添加:

bash 复制代码
/* USER CODE BEGIN Includes */

#include "app_adc_dma.h"

#include "app_test_pwm_multi.h"

#include <stdio.h>

/* USER CODE END Includes */

2. 初始化后启动 PWM 和 ADC DMA

确认 main() 里有这些 CubeMX 初始化函数:

bash 复制代码
MX_GPIO_Init();

MX_DMA_Init();

MX_ADC1_Init();

MX_TIM3_Init();

MX_USART1_UART_Init();

然后在 USER CODE BEGIN 2 中添加:

bash 复制代码
/* USER CODE BEGIN 2 */

App_TestPWMMulti_Init();

App_TestPWMMulti_Start();


App_ADCDMA_Init();

if
 (App_ADCDMA_Start() != HAL_OK)

{

    
printf
(
"ADC DMA start failed\r\n"
);

}


printf
(
"\r\nADC DMA circular test\r\n"
);

printf
(
"PWM0=25%%, PWM1=50%%, PWM2=75%%\r\n"
);

/* USER CODE END 2 */

顺序建议是:

bash 复制代码
先启动 PWM

再启动 ADC DMA

这样 ADC 开始采样时,输入端已经有 PWM 信号。

3. while 循环里处理 DMA 数据

bash 复制代码
/* USER CODE BEGIN WHILE */

while
 (
1
)

{

/* USER CODE END WHILE */


/* USER CODE BEGIN 3 */

  App_ADCDMA_Result adc;


if
 (App_ADCDMA_GetResult(&adc) != 
0u
)

  {

      
printf
(
"DMA avg(%lu scans): CH0=%lu mV, CH1=%lu mV, CH2=%lu mV\r\n"
,

             adc.scan_count,

             adc.voltage_mv[
0
],

             adc.voltage_mv[
1
],

             adc.voltage_mv[
2
]);

  }


  HAL_Delay(
10
);

/* USER CODE END 3 */

}

这里的 HAL_Delay(10) 不是用来控制采样。

ADC 和 DMA 已经在后台采样了。

这个延时只是避免主循环空转太快。

编译、下载和验证

代码加完后:

  1. Keil 编译;

  2. 下载程序;

  3. 打开串口助手;

  4. 复位开发板;

  5. 观察输出。

串口参数仍然是:

bash 复制代码
115200

8 数据位

1 停止位

无校验

正常输出类似:

bash 复制代码
ADC DMA circular test

PWM0=25%, PWM1=50%, PWM2=75%

DMA avg(32 scans): CH0=826 mV, CH1=1649 mV, CH2=2470 mV

DMA avg(32 scans): CH0=823 mV, CH1=1651 mV, CH2=2475 mV

如果你想验证三路是否真的对应,可以改:

bash 复制代码
App_TestPWMMulti_SetAllDuty(
100u
, 
400u
, 
900u
);

正常应该看到:

bash 复制代码
CH0 变小

CH1 中等

CH2 变大

移植到其他板子的修改点

|

要改的地方

|

为什么要改

|

在哪里改

|

| --- | --- | --- |

|

PWM 输出引脚

|

不同板子引出的 PWM 脚不同

|

CubeMX TIM PWM 通道

|

|

PWM 通道宏

|

代码默认 TIM3 CH1/CH2/CH3

| app_test_pwm_multi.c |

|

PWM 定时器实例

|

可能不是 htim3

| APP_TEST_PWM_MULTI_HANDLE |

|

ADC 输入通道

|

不同板子的模拟输入脚不同

|

CubeMX ADC Channel

|

|

ADC Rank 顺序

|

决定 DMA buffer 里的数据顺序

|

CubeMX ADC Regular Rank

|

|

ADC 句柄

|

可能不是 hadc1

| APP_ADC_DMA_HANDLE |

|

DMA 通道/请求

|

不同芯片 ADC 对应 DMA 不同

|

CubeMX DMA Settings

|

|

DMA 模式

|

本篇必须 Circular

|

CubeMX DMA Mode

|

|

DMA 数据宽度

|

ADC 12 位建议 Half Word

|

CubeMX DMA Data Width

|

|

参考电压

|

VDDA 不一定刚好 3.3V

| APP_ADC_DMA_VREF_MV |

|

ADC 分辨率

|

不是所有系列都固定 12 位

| APP_ADC_DMA_MAX_RAW |

如果你的三路 PWM 是:

bash 复制代码
PB5 / TIM3_CH2

PB0 / TIM3_CH3

PB1 / TIM3_CH4

那么把 app_test_pwm_multi.c 里的通道宏改成:

bash 复制代码
#define APP_TEST_PWM_CH0 TIM_CHANNEL_2

#define APP_TEST_PWM_CH1 TIM_CHANNEL_3

#define APP_TEST_PWM_CH2 TIM_CHANNEL_4

ADC 输入仍然建议另选:

bash 复制代码
PA1 / ADC1_IN1

PA2 / ADC1_IN2

PA3 / ADC1_IN3

然后用杜邦线连接:

bash 复制代码
PB5 -> PA1

PB0 -> PA2

PB1 -> PA3

常见问题排查

1. HAL_ADC_Start_DMA() 返回错误

优先检查:

|

检查项

|

说明

|

| --- | --- |

|

ADC 是否开启 DMA

|

CubeMX ADC DMA Settings 里是否添加 DMA

|

|

DMA 是否初始化

| MX_DMA_Init()

是否生成并调用

|

|

初始化顺序

| MX_DMA_Init()

是否在 MX_ADC1_Init() 前面

|

|

ADC 句柄是否正确

|

代码默认 hadc1

|

|

DMA 句柄是否关联

| adc.c

里是否有 __HAL_LINKDMA()

|

2. 串口只打印开头,不打印平均值

这通常说明 DMA 回调没有进来。

检查:

  • DMA Mode 是否是 Circular;

  • DMA 中断是否开启;

  • DMA1_Channel1_IRQHandler() 是否存在;

  • 中断函数里是否调用了 HAL_DMA_IRQHandler(&hdma_adc1)

  • ADC 是否 Continuous Conversion Enable;

  • 如果有 DMA Continuous Requests 选项,是否 Enable。

3. 三路数值顺序不对

这大概率是 Rank 顺序、接线顺序、PWM 通道顺序没对上。

按这个顺序查:

  1. 看 PWM_OUT0/1/2 实际是哪几个引脚;

  2. 看三根杜邦线接到哪几个 ADC 输入;

  3. 看 CubeMX ADC Rank 1/2/3;

  4. APP_TEST_PWM_CH0/1/2

  5. App_TestPWMMulti_SetAllDuty() 三个参数。

记住:

bash 复制代码
DMA buffer 里的第 0、1、2 个数据,对应的是 Rank 1、Rank 2、Rank 3

不是你心里以为的 PA1、PA2、PA3。

4. 编译提示回调函数重复定义

如果你的工程里别的文件已经写了:

bash 复制代码
HAL_ADC_ConvHalfCpltCallback()

HAL_ADC_ConvCpltCallback()

那再把本篇代码直接加进去,就会重复定义。

解决方法不是删掉其中一个功能,而是合并回调。

例如在已有回调里加判断:

bash 复制代码
if
 (hadc == &hadc1)

{

    
/* 调用 ADC DMA 相关处理 */

}

一个工程里同名 HAL 回调函数只能定义一次。

5. 数值跳动比较明显

可以尝试:

  • 增大 APP_ADC_DMA_SCAN_COUNT_PER_BLOCK,比如从 32 改成 64;

  • 提高 PWM 频率;

  • 增加 ADC Sampling Time;

  • 确认 PWM 输出和 ADC 输入接线牢靠;

  • 后续接真实模拟传感器时,前端加 RC 滤波。

6. 程序卡在 Error_Handler

常见原因:

  • CubeMX 配置的 ADC/DMA 不完整;

  • DMA 初始化顺序不对;

  • ADC 校准 API 不适配当前芯片;

  • PWM 或 ADC 引脚冲突;

  • HAL_ADC_Start_DMA() 返回错误后你在代码里进入了错误处理。

排查时可以先把错误打印出来,不要一上来就怀疑 DMA 原理。

7. DMA 能用,但 CPU 读取的数据偶尔怪

本篇用了半缓冲和全缓冲回调,就是为了避免 CPU 正在处理的那一半数据被 DMA 继续写。

如果你自己改成直接随便读整个 buffer,就可能读到 DMA 正在更新的数据。

入门阶段建议先保留这种结构:

bash 复制代码
半满 -> 处理前半段

全满 -> 处理后半段

等这个模式跑通了,再考虑更复杂的数据处理。

本篇小结

这一篇我们完成了 ADC + DMA 连续采样。

你现在应该知道:

  • ADC 负责转换模拟量;

  • DMA 负责把 ADC 数据自动搬到内存数组;

  • Circular 模式适合连续采样;

  • 多通道 ADC DMA 的数组顺序是 CH0, CH1, CH2, CH0, CH1, CH2...

  • 半满回调和全满回调适合通知主循环处理一块数据;

  • 回调里不要做复杂计算和 printf()

  • 主循环和中断共享标志时,可以用很短的临界区保护。

到这里,ADC 这条线已经从:

bash 复制代码
单通道轮询

走到了:

bash 复制代码
多通道扫描 + DMA 连续搬运

下一篇我们切到串口 DMA:

STM32 USART + DMA + IDLE:串口不定长接收怎么做。

前面我们已经做过串口单字节中断和一行命令,下一篇会把串口接收升级成更接近工程项目的写法。

相关推荐
星夜夏空991 小时前
FreeRTOS学习(11)——信号量
单片机·学习
大阳1231 小时前
ARM6.(时钟设置,EPIT定时器)
单片机·嵌入式硬件·gpt·arm·时钟·imx6ull·epit
抓虾爪1 小时前
STM32F407VGT6一站式配齐丨粤科源兴ST分销商,同系列F4/F7/H7均可配套
stm32·单片机·嵌入式硬件
foundbug9992 小时前
STM32 简单语音识别实现方案(开灯关灯拨打电话)
stm32·嵌入式硬件·语音识别
项目題供诗2 小时前
STM32-输入捕获模式测频率&PWMI模式测频率占空比(十五)
stm32·单片机·嵌入式硬件
济6172 小时前
ROS开发专栏---基于图像视觉的目标追踪实验--适配Ubuntu 22.04
嵌入式硬件·嵌入式·ros2·机器人开发·机器人方向
济6173 小时前
ROS开发专栏---视觉图像数据的获取实验--适配Ubuntu 22.04
嵌入式硬件·嵌入式·ros2·机器人开发·机器人方向
kebidaixu3 小时前
SPI四种模式
stm32
俊基科技3 小时前
矿用对讲降噪改造实战|A-68 语音模块解决井下高噪回音、啸叫难题
嵌入式硬件·语音识别·ai降噪·回声消除·矿用通信·语音 dsp