模数转换核心概念
模拟信号与数字信号
模拟信号:时间和幅度均连续变化的信号,可直接反映物理量(声音、温度、光强等)的自然变化,理论上有无限多取值,波形平滑连续。
数字信号:时间和幅度均离散的信号,仅用有限个离散数值(如二进制“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)用于输出数据。
代码
/* 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 = 20999SysTick->VAL = 0; // 清除当前计数值SysTick->CTRL = 1; // 启动SysTickwhile ((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_TXGPIO_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; // 引脚速率100MHzGPIO_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和PA10GPIO_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_IRQnNVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级:0(最高)NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级:0NVIC_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; // 引脚:PA5GPIO_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; // 禁用DMAADC_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占用。
代码
/* 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数据寄存器DRDMA_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时无影响)/********************************************************** DMA 突发传输配置说明(基于 RM0090 手册)* 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_Stream0DMA_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=21MHzADC_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,优先级1ADC_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 个数据,新数据加入后剔除最早数据 | 实时性较好,内存占用固定 | 对周期性干扰抑制效果有限 | 实时性要求较高的动态系统 |
| 卡尔曼滤波 | 基于状态方程和观测模型的递归最优估计 | 动态性能优异,适用于非线性、多噪声场景 | 算法复杂,需建立精确数学模型 | 高精度动态系统(如运动控制、导航) |
| 限幅滤波 | 设定阈值,仅保留与前次差值在阈值内的数据 | 快速剔除异常值,计算量极小 | 阈值设置依赖经验,无法处理连续异常 | 传感器偶尔跳变的场景(如光照传感器) |
摇杆模块可以采用前后级实现(前级:死区+限幅滤波 后级:滑动平均滤波)
死区指的是系统对信号的不进行响应的范围,需要对死区进行判断,防止误触,提高可靠性。

/* 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函数是否重定向。