解码模数转换器(ADC)

模数转换核心概念

模拟信号与数字信号

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

数字信号:时间和幅度均离散的信号,仅用有限个离散数值(如二进制"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函数是否重定向。
相关推荐
Zeku2 小时前
Linux驱动学习笔记:SPI子系统中的内核线程初始化
stm32·freertos·linux驱动开发·linux应用开发
1379号监听员_3 小时前
stm32平衡车
stm32·单片机·嵌入式硬件
兆龙电子单片机设计3 小时前
【STM32项目开源】STM32单片机智能台灯控制系统-机智云
stm32·单片机·嵌入式硬件·物联网·开源·毕业设计
云山工作室3 小时前
基于STM32单片机的智能鱼缸(论文+源码)
stm32·单片机·嵌入式硬件
雾削木5 小时前
STM32 HAL库 BMP280气压计读取
linux·stm32·单片机·嵌入式硬件
Y1rong5 小时前
STM32之ADC
stm32·单片机·嵌入式硬件
蓬荜生灰5 小时前
STM32(8)-- 自己创建库函数
stm32·单片机·嵌入式硬件
yuan199976 小时前
STM32F103CBT6驱动AW9523B实现呼吸灯实例
stm32·单片机·嵌入式硬件
Zeku6 小时前
Linux驱动学习笔记:spi-imx.c收发消息的核心流程
stm32·freertos·linux驱动开发·linux应用开发