解码模数转换器(ADC)

news/2026/1/25 22:08:42/文章来源:https://www.cnblogs.com/YouEmbedded/p/19497397

模数转换核心概念

模拟信号与数字信号

模拟信号:时间和幅度均连续变化的信号,可直接反映物理量(声音、温度、光强等)的自然变化,理论上有无限多取值,波形平滑连续。

数字信号:时间和幅度均离散的信号,仅用有限个离散数值(如二进制“0”“1”)表示信息,波形表现为高低电平组成的脉冲或方波。

image

转换必要性:计算机、MCU等数字系统仅能处理离散数字信号,需通过模数转换器(ADC)将模拟信号转换为数字信号,才能实现对物理量的采集与处理。

模数转换器(ADC)定义

ADC是一种电子元件,核心功能是将连续的模拟电压信号(如电位器、光敏电阻输出电压)转换为离散的数字量,转换过程需遵循“取样-量化-编码”三步流程,转换精度与速度是核心性能指标。

ADC转换原理(取样-量化-编码)

取样(Sampling)

对连续变化的模拟信号,按固定时间间隔抽取瞬时值,将时间上连续的信号变为时间上离散的脉冲信号。

image

量化(Quantization)编码(Encoding)

将取样得到的离散电压值,映射为某个固定最小单位(量化单位△)的整数倍,实现幅度上的离散化。

将量化后的整数倍数值,转换为二进制(或其他进制)代码,作为ADC的最终输出。12位ADC输出范围为0~4095(二进制0000 0000 0000~1111 1111 1111),8位ADC输出范围为0~255。

image

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外设

核心特性

3

时钟配置

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

4

STM32 ADC实操案例

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

5

硬件: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模式)

6

硬件: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 个数据,新数据加入后剔除最早数据 实时性较好,内存占用固定 对周期性干扰抑制效果有限 实时性要求较高的动态系统
卡尔曼滤波 基于状态方程和观测模型的递归最优估计 动态性能优异,适用于非线性、多噪声场景 算法复杂,需建立精确数学模型 高精度动态系统(如运动控制、导航)
限幅滤波 设定阈值,仅保留与前次差值在阈值内的数据 快速剔除异常值,计算量极小 阈值设置依赖经验,无法处理连续异常 传感器偶尔跳变的场景(如光照传感器)

摇杆模块可以采用前后级实现(前级:死区+限幅滤波 后级:滑动平均滤波)
死区指的是系统对信号的不进行响应的范围,需要对死区进行判断,防止误触,提高可靠性。

image

/* 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输出对应模拟电压,实现“光强→数字量→模拟量”的转换闭环。

image

常见问题与排查

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1215932.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

解码STM32 看门狗、低功耗与RTC外设

看门狗外设的原理与应用 概述 随着单片机在工业控制、智能设备等领域广泛应用,系统稳定性成为关键。电磁干扰、电压波动等外部因素可能导致程序“跑飞”,即程序执行失控,表现为数据丢失、寄存器值异常、程序指针指向…

M3U8链接健康检查:结构解析+分片验证+监控告警配置

全面解析 M3U8 链接失效的检测与调试方法,涵盖 HTTP 状态码验证、文件结构校验、播放测试、加密流解密及 TS 分片验证。结合 FFmpeg、curl、streamlink 等命令行工具与 Python 自动化脚本,实现批量检测与分钟级监控。…

Struts2_S2-048漏洞复现:原理详解+环境搭建+渗透实践(CVE-2017-9791) - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

AI时代本质的思考

目标&#xff08;Goal&#xff09;&#xff1a;我到底想解决什么&#xff1f; 约束&#xff08;Constraint&#xff09;&#xff1a;不能碰什么&#xff1f; 成功标准&#xff08;Success&#xff09;&#xff1a;做到什么算赢 这个问题非常前沿&#xff0c;而且你这个“人路由…

2025年YOLO算法案例应用领域应用趋势

2025年&#xff0c;基于YOLO系列算法&#xff08;涵盖YOLOv5至YOLO26&#xff09;的案例呈现多领域渗透、技术迭代与场景定制化的特点&#xff0c;覆盖智能交通、工业质检、农业、公共安全、军事、海洋监测、智能家居等多个垂直领域。以下按月份时间线梳理典型案例&#xff0c;…

【SEO黑科技】关键词都埋好了,流量还是差?揭秘图片里的“隐形权重”,用 AI 翻译多拿 30% 搜索流量!

Python 亚马逊SEO 图片索引 A9算法 跨境电商流量 GoogleShopping 图片翻译摘要在跨境电商的精细化运营中&#xff0c;大家都在卷 Listing 的标题和各种埋词。但你可能忽略了一个巨大的流量入口&#xff1a;图片内部的文字。随着亚马逊 A9 算法和 Google Lens 的进化&#xff0c…

论文笔记(一百零六)RynnVLA-002: A Unified Vision-Language-Action and World Model - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

【服装卖家专享】尺码表全是中文怎么破?揭秘 AI 如何智能重构“表格图片”,降低 50% 的尺码退货率!

Python 服装电商 尺码表翻译 表格识别 降低退货 自动化工具 Shein模式摘要在跨境电商的服装、鞋帽类目中&#xff0c;“因尺码不符退货” 造成的损失往往占到总利润的 20% 以上。很多时候&#xff0c;并非产品尺码不准&#xff0c;而是卖家直接使用了厂家提供的 中文尺码表图片…

得物月付额度可以回收提现使用吗

得物月付额度仅限在得物APP内消费,不支持提现、转账或线下使用,且仅对符合条件的用户开放,系统会根据综合评估给出专属额度。以下是具体使用范围与限制: 核心使用范围全品类商品覆盖:可用于购买得物平台自营及部分…

【读书笔记】《跑外卖》

《跑外卖&#xff1a;一个女骑手的世界》读书笔记 一、作者背景与写作缘起 1.1 作者简介 姓名&#xff1a;王婉&#xff08;婉婉&#xff09;出生地&#xff1a;山东某县城童年记忆&#xff1a;北京庙的传说——据说站在庙上能望见北京城&#xff0c;但她多次尝试从未看到过…

冥想第一千七百七十四天(1774)

1.今天周日&#xff0c;然后今天早上先去跑了8公里今天明显感觉到自己的跑步水平退步得很很大自己的最大然后降低到了56&#xff0c;觉着还是有点累的&#xff0c;然后今天没有做太多事情&#xff0c;今天就和朋友一块儿玩了&#xff0c;玩游戏只周末玩&#xff0c;晚上又带着溪…

冥想第一千七百七十二天(1772)

1.今天周五了&#xff0c;项目上也非常忙&#xff0c;然后下了班本来是想着昨天跑步了&#xff0c;然后但是今天昨天没有时间&#xff0c;然后今天就跑了&#xff0c;感觉最近退步了退步的还是很多的不过这也感觉很正常&#xff0c;人总会有高潮和低谷。 2.感谢感谢父母&#x…

冥想第一千七百七十三天(1773)

今天是周六&#xff0c;然后今天是休息日和朋友约着&#xff0c;然后下午一块儿去了&#xff0c;那个参加了跑团的年会&#xff0c;今年的点比较背一个奖品也没抽中&#xff0c;不过孩子玩的倒是挺开心的&#xff0c;晚上到家都快10:00了吧 2.感谢父母&#xff0c;感谢朋友&…

大数据领域:数据清洗推动企业数字化转型

大数据领域&#xff1a;数据清洗推动企业数字化转型关键词&#xff1a;数据清洗、数据质量、企业数字化转型、大数据处理、数据治理、数据价值、数据生命周期摘要&#xff1a;在企业数字化转型的浪潮中&#xff0c;“数据"被称为新时代的"石油”。但未经处理的原始数…

费雪的管理层访谈技巧:洞察公司文化

费雪的管理层访谈技巧&#xff1a;洞察公司文化关键词&#xff1a;费雪、管理层访谈技巧、洞察、公司文化、投资分析摘要&#xff1a;本文聚焦于费雪所提出的管理层访谈技巧&#xff0c;并深入探讨如何通过这些技巧洞察公司文化。公司文化对企业的长期发展和业绩表现有着至关重…

Agent设计哲学:简洁、可靠、可控——构建可信赖智能系统的三大支柱与落地框架

Agent设计哲学:简洁、可靠、可控 副标题:构建可信赖智能系统的三大支柱与落地框架 作者:光子AI 出版社:AI智能体时代虚拟出版社 创作时间:2026-01-17 面对智能Agent时应有的审慎与敬畏——追逐智能能力的同时,不忘回到问题本质,用简洁对抗冗余,用可靠筑牢底线,用可控…

Agentic AI:从技术架构到商业落地:构建自主、协作、可信的下一代智能系统

Agentic AI:从技术架构到商业落地:构建自主、协作、可信的下一代智能系统 作者:光子AI 出版社:AI智能体时代虚拟出版社 创作时间:2026-01-18 前言 当ChatGPT以惊人的自然语言理解能力掀起生成式AI风暴时,整个行业都在欢呼一个新时代的到来。然而,作为这场变革的深度参与…

UF_Modl.h

UF_MODL.h 和 UF_MODL_legacy.h 函数分类介绍 一、uf_modl.h 函数分类 1. 特征管理UF_MODL_ask_feature_sign:获取特征符号(布尔操作类型) UF_MODL_ask_immediate_children:获取建模即时子项偏好设置 UF_MODL_set_…

Hadoop 助力大数据领域的精准营销

Hadoop 助力大数据领域的精准营销 关键词&#xff1a;Hadoop、大数据、精准营销、分布式计算、用户画像、数据挖掘、商业智能 摘要&#xff1a;在“酒香也怕巷子深”的数字时代&#xff0c;企业如何从海量数据中精准找到目标用户&#xff1f;Hadoop作为大数据领域的“基建狂魔”…

大数据领域分布式存储的多租户支持方案

大数据领域分布式存储的多租户支持方案 关键词:分布式存储、多租户架构、资源隔离、性能优化、成本管理、QoS保障、容器化技术 摘要:本文深入探讨大数据领域分布式存储系统中多租户支持的核心技术与实现方案。通过分析多租户架构的技术挑战,提出包含资源隔离、性能保障、安全…