模数转换核心概念
模拟信号与数字信号
模拟信号:时间和幅度均连续变化的信号,可直接反映物理量(声音、温度、光强等)的自然变化,理论上有无限多取值,波形平滑连续。
数字信号:时间和幅度均离散的信号,仅用有限个离散数值(如二进制"0""1")表示信息,波形表现为高低电平组成的脉冲或方波。

转换必要性:计算机、MCU等数字系统仅能处理离散数字信号,需通过模数转换器(ADC)将模拟信号转换为数字信号,才能实现对物理量的采集与处理。
模数转换器(ADC)定义
ADC是一种电子元件,核心功能是将连续的模拟电压信号(如电位器、光敏电阻输出电压)转换为离散的数字量,转换过程需遵循"取样-量化-编码"三步流程,转换精度与速度是核心性能指标。
ADC转换原理(取样-量化-编码)
取样(Sampling)
对连续变化的模拟信号,按固定时间间隔抽取瞬时值,将时间上连续的信号变为时间上离散的脉冲信号。

量化(Quantization)编码(Encoding)
将取样得到的离散电压值,映射为某个固定最小单位(量化单位△)的整数倍,实现幅度上的离散化。
将量化后的整数倍数值,转换为二进制(或其他进制)代码,作为ADC的最终输出。12位ADC输出范围为0~4095(二进制0000 0000 0000~1111 1111 1111),8位ADC输出范围为0~255。

ADC核心性能指标
分辨率
ADC能区分的最小模拟电压变化量,通常用二进制位数表示(8位、10位、12位、16位、24位),位数越高,分辨率越高,量化误差越小。
例:8位ADC分辨率=3.3V/255≈12.94mV,12位ADC分辨率≈0.805mV,后者对微小电压变化更敏感。
转换精度
实际转换结果与理想值的偏差,包含量化误差、偏移误差、增益误差等,通常用LSB(最低有效位)表示,如±1LSB。
转换速率
ADC完成一次转换的时间,单位为μs或kHz(转换频率),转换速率越高,越适合采集快速变化的信号(如音频信号)。
参考电压(VREF)
ADC转换的电压基准,决定输入模拟电压的量程(通常为0~VREF+),VREF精度直接影响ADC转换精度,需选用稳定的电压源。
STM32 ADC外设
核心特性

时钟配置
STM32 ADC挂载于APB2总线,APB2时钟频率最高84MHz,ADC时钟(ADCCLK)需通过预分频器分频得到,分频系数可选2、4、6、8,确保ADCCLK≤36MHz(最大值)。例:预分频系数4时,ADCCLK=84MHz/4=21MHz。

STM32 ADC实操案例
案例1:电位器ADC采集(PA5引脚,单次/连续转换)

硬件:STM32F4xx、电位器(接PA5,对应ADC1/2_IN5)、串口(USART1)用于输出数据。
代码
c
/* Includes ------------------------------------------------------------------*/
#include "stm32f4xx.h"
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
/* 重定向fputc函数,实现printf通过USART1输出 */
int fputc(int ch, FILE *f)
{
/* 等待USART1发送数据寄存器为空(TXE位为1) */
while( USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET );
/* 发送字符:ch为要发送的ASCII码,USART_SendData无返回值 */
USART_SendData(USART1, (uint16_t)ch);
return ch; /* 返回发送的字符,符合fputc函数规范 */
}
/* Private variables ---------------------------------------------------------*/
uint16_t adc_val = 0; // 存储ADC转换结果(12位,范围0~4095)
/* Private function prototypes -----------------------------------------------*/
void delay_us(u32 nus); // 微秒延时
void delay_ms(u32 nms); // 毫秒延时
static void PC_Config(u32 baud); // USART1初始化(串口配置)
void PA5_Config(void); // PA5引脚及ADC1初始化
/**
* @brief 微秒延时函数
* @param nus: 待延时时间,单位微秒(μs)
* @retval None
* @note Systick时钟源为21MHz(AHB时钟84MHz,默认8分频,此处已调整为21MHz)
* 重载值计算:nus*21 - 1(每个时钟周期1/21μs,nus需21*nus个时钟周期)
*/
void delay_us(u32 nus)
{
SysTick->CTRL = 0; // 关闭SysTick定时器
SysTick->LOAD = nus * 21 - 1; // 设置重载寄存器值,控制延时时间
SysTick->VAL = 0; // 清除当前计数值,避免残留影响
SysTick->CTRL = 1; // 启动SysTick,使用处理器时钟源
/* 等待COUNTFLAG位(bit16)置1,标识延时结束 */
while ((SysTick->CTRL & 0x00010000) == 0);
SysTick->CTRL = 0; // 关闭SysTick,结束延时
}
/**
* @brief 毫秒延时函数
* @param nms: 待延时时间,单位毫秒(ms)
* @retval None
* @note 基于SysTick实现,存在微小误差,适合普通延时场景
* 1ms需21000个时钟周期(21MHz时钟源)
*/
void delay_ms(u32 nms)
{
while(nms--)
{
SysTick->CTRL = 0; // 关闭SysTick定时器
SysTick->LOAD = 21 * 1000 - 1; // 重载值=21MHz*1ms -1 = 20999
SysTick->VAL = 0; // 清除当前计数值
SysTick->CTRL = 1; // 启动SysTick
while ((SysTick->CTRL & 0x00010000) == 0); // 等待延时结束
SysTick->CTRL = 0; // 关闭SysTick
}
}
/**
* @brief USART1初始化函数(配置串口参数,用于数据输出)
* @param baud: 串口波特率(如115200、9600等)
* @retval None
* @note 引脚映射:PA9(TX)、PA10(RX),复用为USART1功能
*/
static void PC_Config(u32 baud)
{
USART_InitTypeDef USART_InitStructure; // USART配置结构体
NVIC_InitTypeDef NVIC_InitStructure; // NVIC中断配置结构体
GPIO_InitTypeDef GPIO_InitStructure; // GPIO配置结构体
/* 1. 使能GPIOA和USART1时钟 */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); // GPIOA时钟(AHB1总线)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // USART1时钟(APB2总线)
/* 2. 配置GPIO引脚复用功能(PA9=USART1_TX,PA10=USART1_RX) */
GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1); // PA9复用为USART1_TX
GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1); // PA10复用为USART1_RX
/* 3. 配置GPIO引脚参数 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; // 复用模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; // 引脚速率100MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 推挽输出(TX引脚)
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 上拉电阻(RX引脚防干扰)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; // PA9和PA10
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIOA
/* 4. 配置USART1参数 */
USART_InitStructure.USART_BaudRate = baud; // 波特率,由参数传入
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 数据位:8位
USART_InitStructure.USART_StopBits = USART_StopBits_1; // 停止位:1位
USART_InitStructure.USART_Parity = USART_Parity_No; // 校验位:无校验
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 模式:收发全双工
USART_Init(USART1, &USART_InitStructure); // 初始化USART1
/* 5. 配置USART1中断(接收中断) */
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; // 中断通道:USART1_IRQn
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级:0(最高)
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级:0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能中断通道
NVIC_Init(&NVIC_InitStructure); // 初始化NVIC
/* 6. 使能USART1接收中断(接收到数据触发中断) */
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
/* 7. 等待发送寄存器为空,清空初始状态 */
while( USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET );
/* 8. 清除接收中断挂起位,避免初始中断误触发 */
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
/* 9. 使能USART1外设 */
USART_Cmd(USART1, ENABLE);
}
/**
* @brief PA5引脚及ADC1初始化(电位器采集通道配置)
* @param None
* @retval None
* @note PA5配置为模拟输入,ADC1独立模式,12位分辨率,连续转换模式
*/
void PA5_Config(void)
{
ADC_InitTypeDef ADC_InitStructure; // ADC配置结构体
ADC_CommonInitTypeDef ADC_CommonInitStructure; // ADC公共配置结构体
GPIO_InitTypeDef GPIO_InitStructure; // GPIO配置结构体
/* 1. 使能时钟(GPIOA、ADC1) */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); // GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // ADC1时钟(APB2总线)
/* 2. 配置PA5为模拟输入 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; // 引脚:PA5
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN; // 模式:模拟输入
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; // 无上下拉(模拟输入默认配置)
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化GPIOA
/* 3. 配置ADC公共参数(多ADC共用,此处仅用ADC1,独立模式) */
ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent; // 模式:独立模式(单ADC工作)
ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4; // 预分频:4分频(84MHz/4=21MHz)
ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled; // 禁用DMA
ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles; // 两次采样间隔5个时钟周期
ADC_CommonInit(&ADC_CommonInitStructure); // 初始化ADC公共配置
/* 4. 配置ADC1核心参数 */
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; // 分辨率:12位(0~4095)
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 禁用扫描模式(单通道采集)
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 使能连续转换模式(一次触发持续转换)
ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None; // 禁用外部触发(软件触发)
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1; // 外部触发源(禁用时无影响,保留默认)
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据对齐:右对齐(高位补0,便于计算)
ADC_InitStructure.ADC_NbrOfConversion = 1; // 转换通道数量:1个(仅PA5对应通道5)
ADC_Init(ADC1, &ADC_InitStructure); // 初始化ADC1
/* 5. 配置ADC1规则通道(通道5,优先级1,采样时间3个时钟周期) */
ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_3Cycles);
// 参数说明:ADCx=ADC1,Channel=ADC_Channel_5(PA5对应通道),Rank=1(转换优先级1),SampleTime=3Cycles(采样时间)
/* 6. 使能ADC1外设 */
ADC_Cmd(ADC1, ENABLE);
/* 7. 软件触发ADC1规则转换(连续模式下一次触发即可持续转换) */
ADC_SoftwareStartConv(ADC1);
}
/**
* @brief USART1中断服务函数(接收中断处理)
* @param None
* @retval None
* @note 接收到数据后原样回发,用于测试串口通信
*/
void USART1_IRQHandler(void)
{
uint8_t data = 0;
/* 判断是否为USART1接收中断(RXNE位置1) */
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
data = (uint8_t)USART_ReceiveData(USART1); // 读取接收数据(8位)
USART_SendData(USART1, data); // 回发接收的数据
}
}
/**
* @brief 主函数(程序入口)
* @param None
* @retval None
* @note 初始化外设后,循环采集ADC数据,通过串口输出
*/
int main(void)
{
/* 1. NVIC优先级分组(分组4:抢占优先级4位,子优先级0位,范围0~15) */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
/* 2. 硬件初始化(串口115200波特率,PA5及ADC1) */
PC_Config(115200);
PA5_Config();
/* 无限循环,持续采集并输出数据 */
while (1)
{
/* 等待ADC转换结束(EOC位置1,标识一次转换完成) */
while( ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET );
/* 读取ADC转换结果(12位数据,范围0~4095) */
adc_val = ADC_GetConversionValue(ADC1);
/* 串口输出ADC值,格式:adc val = XXXX */
printf("adc val = %d\r\n", adc_val);
delay_ms(500); // 延时500ms,控制输出频率
}
}
代码关键说明
- 模拟输入引脚配置:必须设为
GPIO_Mode_AN(模拟输入),且无上下拉,避免影响模拟信号采集。 - ADC时钟:预分频系数需确保ADCCLK≤36MHz,本案例APB2时钟84MHz,分频4后为21MHz,符合要求。
- 连续转换模式:开启后ADC一次触发持续转换,无需重复软件触发,适合连续采集场景,节省CPU资源。
- 数据计算:12位ADC采集值(0~4095)转换为实际电压公式:V=adc_val×3.3V/4095。
案例2:PS2摇杆模块ADC采集(DMA模式)

硬件:PS2双轴摇杆模块(X轴接PA6/ADC1_IN6,Y轴接PA4/ADC1_IN4,Z轴为按键),使用DMA传输ADC数据,减少CPU占用。
代码
c
/* Includes ------------------------------------------------------------------*/
#include "stm32f4xx.h"
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
/* 存储摇杆X、Y轴ADC值(全局变量,DMA直接写入) */
uint16_t pos[2] = {0}; // pos[0]=X轴值,pos[1]=Y轴值
/**
* @brief PS2摇杆模块初始化(ADC1+DMA配置)
* @param None
* @retval None
* @note ADC1扫描模式+连续转换,DMA循环传输,无需CPU干预数据读取
*/
void PS2_Config(void)
{
ADC_InitTypeDef ADC_InitStructure; // ADC配置结构体
ADC_CommonInitTypeDef ADC_CommonInitStructure; // ADC公共配置结构体
GPIO_InitTypeDef GPIO_InitStructure; // GPIO配置结构体
DMA_InitTypeDef DMA_InitStructure; // DMA配置结构体
/* 1. 使能时钟(GPIOA、GPIOB、ADC1、DMA2) */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); // DMA2时钟(AHB1总线)
/* 2. 配置DMA2_Stream0(ADC1数据传输) */
DMA_DeInit(DMA2_Stream0); // 复位DMA2_Stream0,清除默认配置
DMA_InitStructure.DMA_Channel = DMA_Channel_0; // 通道:DMA_Channel_0(ADC1对应通道)
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR); // 外设地址:ADC1数据寄存器DR
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)pos; // 内存地址:pos数组(存储X、Y轴数据)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; // 方向:外设到内存(ADC→RAM)
DMA_InitStructure.DMA_BufferSize = 2; // 缓冲区大小:2个数据(X、Y轴各1个)
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增(固定DR寄存器)
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增(依次存pos[0]、pos[1])
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设数据宽度:半字(16位,ADC12位数据)
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 内存数据宽度:半字(16位)
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 模式:循环模式(持续传输,覆盖旧数据)
DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 优先级:高(避免数据丢失)
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; // 缓冲区 (FIFO)禁用FIFO模式(直接传输)
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull; // FIFO阈值(禁用FIFO时无影响)
/*********************************************************
* 1. 突发大小定义:MBURST/PBURST 配置的是"节拍数"而非字节数,1个节拍的字节数由 MSIZE/PSIZE 决定:
* - MSIZE/PSIZE=00(8位):1节拍=1字节;01(16位):1节拍=2字节;10(32位):1节拍=4字节
* - 突发类型:00=单次传输 | 01=4节拍 | 10=8节拍 | 11=16节拍
* 2. 核心约束:
* - 突发传输不可分割:AHB总线会锁定DMA授权,保证数据一致性
* - 仅指针递增模式(MINC/PINC=1)允许配置突发:若MINC=0则MBURST必须清00,PINC=0则PBURST必须清00
* - 地址对齐:突发块内所有传输需按数据宽度对齐(8位无要求,16位2字节对齐,32位4字节对齐)
* - 边界限制:突发传输不可跨越1KB地址边界,否则触发AHB错误且无寄存器上报
* - 直接模式(DMDIS=0)下强制为单次传输,MBURST/PBURST由硬件配置
* 3. NDT(传输项数)限制:需满足PSIZE与MSIZE的倍数要求(8位→16位需2的倍数,8位→32位需4的倍数等)
* 4. 本配置为单次传输(Single):每个DMA请求仅触发1个节拍的传输,无突发
********************************************************/
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; // 存储器端口:单次传输(1个节拍)
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; // 外设端口:单次传输(1个节拍)
DMA_Init(DMA2_Stream0, &DMA_InitStructure); // 初始化DMA2_Stream0
DMA_Cmd(DMA2_Stream0, ENABLE); // 使能DMA2_Stream0
/* 3. 配置GPIO(摇杆X、Y轴模拟输入,Z轴数字输入) */
// X轴(PA6)、Y轴(PA4):模拟输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN; // 模拟输入
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; // 无上下拉
GPIO_Init(GPIOA, &GPIO_InitStructure);
// Z轴(PB7):数字输入(按键)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; // 输入模式
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 上拉(按键未按下时为高电平)
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* 4. 配置ADC公共参数 */
ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式
ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4; // 预分频4,ADCCLK=21MHz
ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_1; // DMA访问模式1(支持多通道传输)
ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles; // 两次采样间隔5周期
ADC_CommonInit(&ADC_CommonInitStructure);
/* 5. 配置ADC1参数(扫描+连续转换,适配双通道) */
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; // 12位分辨率
ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 使能扫描模式(多通道采集)
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 使能连续转换
ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None; // 禁用外部触发
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1; // 触发源(默认)
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐
ADC_InitStructure.ADC_NbrOfConversion = 2; // 转换通道数:2个(X、Y轴)
ADC_Init(ADC1, &ADC_InitStructure);
/* 6. 配置ADC1规则通道(X轴IN6优先级1,Y轴IN4优先级2) */
ADC_RegularChannelConfig(ADC1, ADC_Channel_6, 1, ADC_SampleTime_3Cycles); // X轴:通道6,优先级1
ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 2, ADC_SampleTime_3Cycles); // Y轴:通道4,优先级2
/* 7. 使能ADC DMA功能 */
ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE); // 最后一次转换后触发DMA请求
ADC_DMACmd(ADC1, ENABLE); // 使能ADC1 DMA功能
/* 8. 使能ADC1并启动转换 */
ADC_Cmd(ADC1, ENABLE);
ADC_SoftwareStartConv(ADC1); // 软件触发转换
}
/**
* @brief 主函数
* @param None
* @retval None
* @note DMA自动传输ADC数据,主函数仅需读取pos数组并输出
*/
int main(void)
{
/* 1. NVIC优先级分组 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
/* 2. 硬件初始化(串口、PS2摇杆) */
PC_Config(115200); // 串口初始化(115200波特率)
PS2_Config(); // PS2摇杆及ADC+DMA初始化
/* 无限循环,读取并输出摇杆数据 */
while (1)
{
/* 输出X、Y轴ADC值,DMA已自动更新pos数组 */
printf("x=%d,y=%d\r\n", pos[0], pos[1]);
delay_ms(500); // 延时500ms,控制输出频率
}
}
DMA模式优势与关键配置
优势:ADC转换完成后,数据通过DMA直接传输到内存数组,无需CPU中断或轮询读取,降低CPU占用率,适合多通道、高频采集场景。
关键配置:
- DMA通道:ADC1对应DMA2_Stream0,通道0;
- 传输模式:循环模式(
DMA_Mode_Circular),持续覆盖旧数据,无需重复配置; - 内存递增:开启(
DMA_MemoryInc_Enable),依次存储多通道数据; - ADC扫描模式:开启(
ADC_ScanConvMode = ENABLE),支持多通道循环采集。
ADC数据滤波算法
摇杆、光敏电阻等传感器采集的ADC数据易受干扰,需通过滤波算法平滑数据,常用算法如下:
| 滤波算法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 均值滤波 | 连续采集 N 次数据,取算术平均值 | 算法简单,计算量小,平滑噪声效果好 | 对快速变化信号响应滞后,动态特性差 | 缓慢变化信号(如温度、液位) |
| 中值滤波 | 连续采集 N 次数据,取中间值 | 有效抑制脉冲干扰(如突发噪声) | 对快速变化信号响应慢,数据量较小时效果差 | 含脉冲噪声的场景(如压力传感器) |
| 加权平均滤波 | 对不同时刻采样值赋予不同权重后求平均 | 可调节响应速度,兼顾平滑与动态性 | 权重系数需经验调试,计算量略大 | 需平衡噪声抑制与响应速度的场景 |
| 滑动平均滤波 | 保留最近 N 个数据,新数据加入后剔除最早数据 | 实时性较好,内存占用固定 | 对周期性干扰抑制效果有限 | 实时性要求较高的动态系统 |
| 卡尔曼滤波 | 基于状态方程和观测模型的递归最优估计 | 动态性能优异,适用于非线性、多噪声场景 | 算法复杂,需建立精确数学模型 | 高精度动态系统(如运动控制、导航) |
| 限幅滤波 | 设定阈值,仅保留与前次差值在阈值内的数据 | 快速剔除异常值,计算量极小 | 阈值设置依赖经验,无法处理连续异常 | 传感器偶尔跳变的场景(如光照传感器) |
摇杆模块可以采用前后级实现(前级:死区+限幅滤波 后级:滑动平均滤波)
死区指的是系统对信号的不进行响应的范围,需要对死区进行判断,防止误触,提高可靠性。

c
/* Includes ------------------------------------------------------------------*/
#include "stm32f4xx.h"
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
#include <math.h>
/* 常量定义 ------------------------------------------------------------------*/
#define ADC_RESOLUTION 4096 // 12位ADC分辨率
#define DEAD_ZONE_RANGE 100 // 死区范围 (±50)
#define MIDDLE_VALUE 2048 // ADC中间值
#define CLAMP_THRESHOLD 100 // 限幅阈值 (最大允许变化)
#define MOVING_AVG_SIZE 5 // 滑动平均窗口大小
/* 存储原始和滤波后的摇杆数据 */
uint16_t pos[2] = {0}; // DMA写入的原始数据
uint16_t filtered_pos[2] = {0}; // 滤波后的数据
/* 滑动平均滤波相关结构体 */
typedef struct {
uint16_t buffer[MOVING_AVG_SIZE]; // 数据缓冲区
uint8_t index; // 当前索引
uint16_t sum; // 数据总和
uint8_t count; // 有效数据计数
} MovingAverage_t;
/* X轴和Y轴的滑动平均滤波器 */
MovingAverage_t avg_filter_x = {0};
MovingAverage_t avg_filter_y = {0};
/* 前一次滤波后的值(用于限幅滤波) */
uint16_t last_filtered_x = MIDDLE_VALUE;
uint16_t last_filtered_y = MIDDLE_VALUE;
/**
* @brief 死区滤波
* @param raw_value: 原始ADC值
* @param center: 中心值
* @param dead_zone: 死区范围
* @retval 死区滤波后的值
*/
uint16_t DeadZone_Filter(uint16_t raw_value, uint16_t center, uint16_t dead_zone)
{
if (abs((int)raw_value - (int)center) <= dead_zone / 2) {
return center; // 在死区内,返回中心值
}
return raw_value; // 超出死区,返回原始值
}
/**
* @brief 限幅滤波
* @param current_value: 当前值
* @param last_value: 上一次的值
* @param threshold: 最大允许变化量
* @retval 限幅滤波后的值
*/
uint16_t Clamp_Filter(uint16_t current_value, uint16_t last_value, uint16_t threshold)
{
int16_t diff = (int16_t)current_value - (int16_t)last_value;
if (diff > threshold) {
return last_value + threshold; // 正向变化过大,限制增幅
} else if (diff < -threshold) {
return last_value - threshold; // 负向变化过大,限制减幅
}
return current_value; // 变化在允许范围内
}
/**
* @brief 初始化滑动平均滤波器
* @param filter: 滤波器结构体指针
* @retval None
*/
void MovingAverage_Init(MovingAverage_t* filter)
{
memset(filter->buffer, 0, sizeof(filter->buffer));
filter->index = 0;
filter->sum = 0;
filter->count = 0;
}
/**
* @brief 滑动平均滤波
* @param filter: 滤波器结构体指针
* @param new_value: 新采样值
* @retval 滤波后的平均值
*/
uint16_t MovingAverage_Filter(MovingAverage_t* filter, uint16_t new_value)
{
/* 更新总和:减去最旧值,加上最新值 */
if (filter->count >= MOVING_AVG_SIZE) {
filter->sum -= filter->buffer[filter->index];
} else {
filter->count++;
}
/* 存储新值到缓冲区 */
filter->buffer[filter->index] = new_value;
filter->sum += new_value;
/* 更新索引 */
filter->index = (filter->index + 1) % MOVING_AVG_SIZE;
/* 计算并返回平均值 */
return filter->sum / filter->count;
}
/**
* @brief 两级滤波处理(死区+限幅 → 滑动平均)
* @param raw_x: 原始X轴值
* @param raw_y: 原始Y轴值
* @retval None
*/
void DualStage_Filter(uint16_t raw_x, uint16_t raw_y)
{
static uint8_t filter_initialized = 0;
/* 首次调用时初始化滤波器 */
if (!filter_initialized) {
MovingAverage_Init(&avg_filter_x);
MovingAverage_Init(&avg_filter_y);
filter_initialized = 1;
}
/* 前级滤波:死区滤波 + 限幅滤波 */
uint16_t stage1_x = DeadZone_Filter(raw_x, MIDDLE_VALUE, DEAD_ZONE_RANGE);
uint16_t stage1_y = DeadZone_Filter(raw_y, MIDDLE_VALUE, DEAD_ZONE_RANGE);
stage1_x = Clamp_Filter(stage1_x, last_filtered_x, CLAMP_THRESHOLD);
stage1_y = Clamp_Filter(stage1_y, last_filtered_y, CLAMP_THRESHOLD);
/* 更新上一次的值(用于下一次限幅滤波) */
last_filtered_x = stage1_x;
last_filtered_y = stage1_y;
/* 后级滤波:滑动平均滤波 */
filtered_pos[0] = MovingAverage_Filter(&avg_filter_x, stage1_x);
filtered_pos[1] = MovingAverage_Filter(&avg_filter_y, stage1_y);
}
/**
* @brief PS2摇杆模块初始化(ADC1+DMA配置)
* @param None
* @retval None
*/
void PS2_Config(void)
{
ADC_InitTypeDef ADC_InitStructure;
ADC_CommonInitTypeDef ADC_CommonInitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
DMA_InitTypeDef DMA_InitStructure;
/* 1. 使能时钟 */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
/* 2. 配置DMA2_Stream0 */
DMA_DeInit(DMA2_Stream0);
DMA_InitStructure.DMA_Channel = DMA_Channel_0;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR);
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)pos;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
DMA_InitStructure.DMA_BufferSize = 2;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA2_Stream0, &DMA_InitStructure);
DMA_Cmd(DMA2_Stream0, ENABLE);
/* 3. 配置GPIO */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* 4. 配置ADC公共参数 */
ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4;
ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_1;
ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
ADC_CommonInit(&ADC_CommonInitStructure);
/* 5. 配置ADC1参数 */
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
ADC_InitStructure.ADC_ScanConvMode = ENABLE;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfConversion = 2;
ADC_Init(ADC1, &ADC_InitStructure);
/* 6. 配置ADC1规则通道 */
ADC_RegularChannelConfig(ADC1, ADC_Channel_6, 1, ADC_SampleTime_3Cycles);
ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 2, ADC_SampleTime_3Cycles);
/* 7. 使能ADC DMA功能 */
ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE);
ADC_DMACmd(ADC1, ENABLE);
/* 8. 使能ADC1并启动转换 */
ADC_Cmd(ADC1, ENABLE);
ADC_SoftwareStartConv(ADC1);
}
/**
* @brief 主函数
* @param None
* @retval None
*/
int main(void)
{
/* 1. NVIC优先级分组 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
/* 2. 硬件初始化 */
PC_Config(115200); // 串口初始化(假设已实现)
PS2_Config(); // PS2摇杆初始化
printf("PS2 Joystick with Dual-Stage Filtering\r\n");
printf("Dead Zone: ±%d, Clamp Threshold: %d, Moving Avg Size: %d\r\n",
DEAD_ZONE_RANGE/2, CLAMP_THRESHOLD, MOVING_AVG_SIZE);
/* 3. 主循环 */
while (1)
{
/* 应用两级滤波 */
DualStage_Filter(pos[0], pos[1]);
/* 输出原始和滤波后的数据(便于对比) */
printf("Raw: x=%4d, y=%4d | Filtered: x=%4d, y=%4d\r\n",
pos[0], pos[1], filtered_pos[0], filtered_pos[1]);
delay_ms(10); // 减少延迟,提高采样率(可选)
}
}
光敏电阻ADC采集与数模转换(DAC)
光敏电阻工作原理
光敏电阻是半导体器件,阻值随光照强度增大而减小(光电导效应)。通过串联固定电阻组成分压电路,将阻值变化转换为电压变化,再通过ADC采集。
分压电路公式:Vout = VCC × R固定 / (R光敏 + R固定),光照越强,R光敏越小,Vout越大。
数模转换(DAC)
DAC与ADC相反,将数字量转换为模拟电压,可用于控制LED亮度、电机转速等。STM32内置DAC外设,12位分辨率,输出范围0~VREF。
ADC采集光敏电阻电压后,可通过DAC输出对应模拟电压,实现"光强→数字量→模拟量"的转换闭环。

常见问题与排查
- ADC数据不变:检查引脚配置是否为模拟输入,时钟是否使能,通道是否配置正确;
- 数据波动大:增加采样时间(如
ADC_SampleTime_28Cycles),添加滤波算法,检查硬件接线是否接触不良; - DMA无数据:检查DMA通道、数据流配置是否正确,ADC DMA功能是否使能,缓冲区地址是否正确;
- 串口无输出:检查波特率、数据位、停止位是否匹配,引脚复用是否正确,fputc函数是否重定向。