STM32 ADC/DAC原理与实战:从模拟信号处理到嵌入式系统设计

发布时间:2026/8/28 13:47:41
STM32 ADC/DAC原理与实战:从模拟信号处理到嵌入式系统设计 1. 项目概述从“数字”到“现实”的桥梁在嵌入式系统开发中我们常常会听到一个说法嵌入式系统是连接数字世界与物理世界的桥梁。这句话听起来有点抽象但当你真正开始处理传感器数据或者驱动一个电机时就会立刻明白它的含义。我们写的代码、处理的变量都是纯粹的数字信号0和1。但现实世界是连续的、模拟的比如温度的变化、声音的波动、光线的强弱。如何让我们的单片机“感知”这些连续变化的物理量又如何让它“控制”一个模拟设备这就是模拟通道组件——模数转换器ADC和数模转换器DAC——要解决的核心问题。我接触过很多初学者他们能熟练地操作GPIO点灯、用UART打印“Hello World”但一到ADC采样读回来的数据就跳得厉害或者想用DAC输出一个正弦波结果波形畸变得不成样子。这往往是因为只关注了库函数的调用而忽略了模拟电路本身的特点和ADC/DAC的工作原理。本章内容我们就以STM32F10x系列这款经典且应用广泛的MCU为例深入聊聊模拟通道的“原理”与“应用”把这座“桥梁”的构造、通行规则以及可能遇到的“坑”都讲清楚。无论你是正在学习相关课程的学生还是工作中需要快速上手的工程师理解这些内容都能让你在应对模拟信号时更加得心应手。2. 模拟通道核心组件原理深度拆解2.1 ADC将连续世界“数字化”的守门人ADC全称模数转换器它的任务是把一个连续变化的模拟电压信号转换成单片机可以处理的离散数字值。这个过程就像用一把有刻度的尺子去测量一段连续的长度尺子的最小刻度决定了测量的精度。对于STM32F10x系列其内置的ADC是12位逐次逼近型SARADC。我们来拆解这几个关键词12位这意味着ADC的输出数字值范围是0到40952^12 - 1。它对应一个参考电压范围通常是0V到VDDA模拟电源电压通常与VDD相连如3.3V。那么ADC能分辨的最小电压变化即分辨率就是 VDDA / 4096。当VDDA3.3V时分辨率约为0.8mV。这个值理论上是ADC能“感知”到的最小电压差。逐次逼近型SAR这是其工作原理。它内部有一个数模转换器DAC和一个比较器。转换开始时SAR逻辑先设定数字输出的最高位为1即假设输入电压是满量程的一半用内部的DAC产生对应的模拟电压与输入电压在比较器中比较。如果输入电压更高则保留该位为1否则清零。然后依次对次高位、第三位……重复此过程直到最低位。就像玩“猜数字”游戏每次猜中间值根据“大了”或“小了”的反馈缩小范围最终锁定目标值。这种原理决定了SAR ADC在精度、速度和功耗之间有一个很好的平衡非常适合嵌入式应用。注意分辨率不等于精度。精度还受到参考电压稳定性、内部噪声、线性度、积分非线性INL、微分非线性DNL等多种因素影响。一个12位ADC的实际有效位数ENOB可能只有10位或11位。因此在设计高精度测量电路时不能只看ADC的位数。2.2 DAC让数字命令产生“模拟效果”的执行者DAC的作用与ADC相反它接收一个数字代码输出一个对应的模拟电压。STM32F10x的DAC也是12位的这意味着输入数字值0-4095对应输出0V到参考电压Vref通常连接VDDA的模拟电压。其常见结构是电阻串型或R-2R梯形网络型。以电阻串型为例内部有一串精密电阻通过数字代码控制开关从电阻串的不同抽头取出电压。它的特点是线性度好但需要大量的电阻和开关。DAC的输出通常需要一个缓冲运放来驱动外部负载防止负载直接影响分压精度。理解DAC的一个关键点是它的建立时间。当你改变输入数字值时输出电压不会瞬间跳变到新值而是需要一段时间来稳定。这个时间就是建立时间。如果你用DAC生成高频信号比如音频建立时间必须足够短否则会导致波形失真。2.3 关键外围电路原理图上的“配角”性能上的“主角”ADC和DAC的性能很大程度上取决于其外围电路设计。这里有几个必须关注的要点参考电压源这是ADC/DAC的“基准尺”。如果这把“尺子”本身就不准或者晃动测量结果肯定不准。VDDA必须干净、稳定。通常的做法是使用独立的LDO为模拟部分供电并通过磁珠或0欧电阻与数字电源VDD隔离。在VDDA和VSSA模拟地之间必须紧贴芯片引脚放置一个10uF的钽电容或电解电容并联一个0.1uF的陶瓷电容用于低频和高频去耦。模拟输入信号调理传感器直接输出的信号往往不适合直接送入ADC。可能需要滤波使用RC低通滤波器滤除高频噪声。截止频率需要根据信号频率和采样率来设计遵循奈奎斯特采样定理。放大/衰减使用运放电路将信号电压调整到ADC的量程范围内如0-3.3V以充分利用ADC的分辨率。保护使用钳位二极管或电阻限流防止过高或过低的电压损坏ADC输入引脚虽然STM32的ADC引脚通常有一定耐压能力但超出范围会影响转换精度甚至损坏。接地与布局模拟地和数字地必须在一点连接通常是在电源入口处形成“星型接地”避免数字噪声通过地线串扰到模拟部分。在PCB布局上模拟部分应远离时钟、高速数字信号线等噪声源。3. STM32F10x ADC/DAC 配置与编程实战理解了原理我们来看如何在STM32F10x上实际操作。这里我会基于标准外设库StdPeriph Lib来讲解因为其寄存器操作清晰有助于理解本质。HAL库的思路是类似的只是API封装程度更高。3.1 ADC单通道单次转换模式配置这是最简单的模式适用于不频繁的采样比如每分钟读取一次温度传感器。// 1. 初始化结构体与时钟 ADC_InitTypeDef ADC_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 设置ADC时钟不能超过14MHz // 2. 配置ADC引脚为模拟输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; // 假设使用PA1 GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入模式关键 GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 配置ADC参数 ADC_InitStructure.ADC_Mode ADC_Mode_Independent; // 独立模式 ADC_InitStructure.ADC_ScanConvMode DISABLE; // 单通道禁用扫描 ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 单次转换 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; // 软件触发 ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; // 数据右对齐 ADC_InitStructure.ADC_NbrOfChannel 1; // 转换通道数为1 ADC_Init(ADC1, ADC_InitStructure); // 4. 配置通道规则序列规则组 ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5); // 通道1采样时间55.5周期 // 5. 使能ADC ADC_Cmd(ADC1, ENABLE); // 6. 校准非常重要 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); // 7. 启动转换并读取 ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 等待转换结束 uint16_t adc_value ADC_GetConversionValue(ADC1); // 读取12位结果关键参数解析ADC_SampleTime_55Cycles5采样时间。ADC内部有一个采样保持电容需要时间对输入信号充电到稳定。信号源阻抗越高需要的采样时间越长。55.5个周期在ADC时钟为12MHz时约为4.6us。如果输入信号变化快或阻抗大采样时间不足会导致误差。校准ADC内部存在偏移和增益误差上电后必须进行校准这是保证精度的重要一步绝不能省略。3.2 ADC多通道DMA连续转换模式配置对于需要实时、高速采集多路信号的应用如音频、振动分析单次转换效率太低。我们需要使用扫描模式、连续转换模式并配合DMA将数据直接搬运到内存解放CPU。// 假设采集PA1(Ch1), PA2(Ch2)两路信号用DMA存到数组ADC_ConvertedValue[2] uint16_t ADC_ConvertedValue[2]; // 1. DMA配置 DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(ADC1-DR); // 外设地址ADC数据寄存器 DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)ADC_ConvertedValue; // 内存地址 DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; // 方向外设-内存 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; // 16位 DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // 循环模式自动重装 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); // 2. ADC配置与之前类似关键区别如下 ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; // 启用扫描模式 ADC_InitStructure.ADC_ContinuousConvMode ENABLE; // 启用连续转换 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 2; // 2个通道 ADC_Init(ADC1, ADC_InitStructure); // 3. 配置规则通道序列 ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5); // 序列1Ch1 ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 2, ADC_SampleTime_55Cycles5); // 序列2Ch2 // 4. 使能ADC的DMA请求 ADC_DMACmd(ADC1, ENABLE); // 5. 使能ADC校准同上 ADC_Cmd(ADC1, ENABLE); // ... 校准代码 ... // 6. 启动转换 ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 此后ADC会连续、自动地按Ch1-Ch2的顺序转换并通过DMA将结果循环存入ADC_ConvertedValue数组。 // 主循环中直接读取数组即可获得最新数据。实操心得 在DMA循环模式下DMA_BufferSize设置的大小决定了DMA完成一次“循环”的传输次数。例如设为2则DMA在搬运完第2个数据对应Ch2后会重置指针从内存起始地址开始下一轮搬运。你的应用程序需要处理好数据同步问题避免在DMA搬运过程中读取到半新半旧的数据。一种常见方法是使用双缓冲区Ping-Pong Buffer。3.3 DAC输出波形实战我们让DAC输出一个周期性的锯齿波。// 1. 初始化GPIO和DAC GPIO_InitTypeDef GPIO_InitStructure; DAC_InitTypeDef DAC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA4 对应 DAC通道1 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输出模式 GPIO_Init(GPIOA, GPIO_InitStructure); // 2. 配置DAC DAC_InitStructure.DAC_Trigger DAC_Trigger_None; // 不使用硬件触发软件触发 DAC_InitStructure.DAC_WaveGeneration DAC_WaveGeneration_None; // 不产生内置波形 DAC_InitStructure.DAC_OutputBuffer DAC_OutputBuffer_Enable; // 使能输出缓冲驱动能力更强 DAC_Init(DAC_Channel_1, DAC_InitStructure); // 3. 使能DAC通道 DAC_Cmd(DAC_Channel_1, ENABLE); // 4. 在主循环或定时器中断中更新输出值产生锯齿波 uint16_t dac_val 0; while(1) { DAC_SetChannel1Data(DAC_Align_12b_R, dac_val); // 设置12位右对齐数据 dac_val; if(dac_val 4096) { dac_val 0; } // 通过延时或定时器控制更新速率从而控制锯齿波频率 Delay_ms(1); // 粗略延时实际应用应用定时器 }注意事项DAC_OutputBuffer使能后DAC内部输出运放工作可以提供更强的电流输出能力最大约20mA但会引入一定的偏移误差。如果对绝对精度要求极高且负载阻抗很大100kΩ可以考虑禁用缓冲但驱动能力会变弱。生成复杂波形如正弦波时通常预先计算好一个周期的样点值查表法在定时器中断中依次输出这样可以保证波形的时序精确。4. 数据滤波与软件处理技巧ADC采回来的原始数据往往是充满噪声的。硬件滤波是基础软件滤波则是提升数据质量的最后一道防线。4.1 常用软件滤波算法对比与实现限幅滤波消除脉冲噪声#define A 10 // 最大允许偏差 uint16_t Filter_Limit(uint16_t new_value, uint16_t old_value) { if((new_value - old_value A) || (old_value - new_value A)) return old_value; else return new_value; }适用场景克服偶然出现的尖峰脉冲干扰。缺点是无法抑制周期性干扰平滑度差。中位值滤波uint16_t Filter_Median(uint16_t *buf, uint8_t len) { // 对buf排序冒泡即可返回中间值 // len 通常取奇数如3,5,7 sort_buffer(buf, len); return buf[len/2]; }适用场景对温度、液位等变化缓慢的参数能有效滤除偶然的波动。对快速变化的参数不适用。算术平均滤波uint16_t Filter_Average(uint16_t *buf, uint8_t len) { uint32_t sum 0; for(uint8_t i0; ilen; i) sum buf[i]; return (uint16_t)(sum / len); }适用场景抑制随机噪声平滑度高。缺点是灵敏度低对突发干扰抑制能力弱且需要缓存一组数据。滑动平均滤波#define FILTER_LEN 10 uint16_t filter_buf[FILTER_LEN]; uint8_t filter_index 0; uint32_t filter_sum 0; uint16_t Filter_MovingAverage(uint16_t new_value) { filter_sum - filter_buf[filter_index]; // 减去最旧的值 filter_sum new_value; // 加上最新的值 filter_buf[filter_index] new_value; // 更新缓冲区 filter_index (filter_index 1) % FILTER_LEN; // 索引循环 return (uint16_t)(filter_sum / FILTER_LEN); }适用场景实时性要求高、需要连续平滑数据的场合如波形显示。这是我最常用的一种滤波方式效率高效果也不错。一阶滞后滤波低通数字滤波器#define ALPHA 0.3f // 滤波系数0ALPHA1越小越平滑响应越慢 float filtered_val 0; float Filter_FirstOrderLag(float new_value) { filtered_val ALPHA * new_value (1 - ALPHA) * filtered_val; return filtered_val; }适用场景适用于高频周期性噪声的滤除模拟了硬件RC低通滤波器的效果。计算量小但相位滞后。选择建议对于慢变信号温度中位值滑动平均组合拳效果很好。对于有一定速度的信号如速度、压力滑动平均或一阶滞后滤波是首选。可以先在PC上用Matlab或Python模拟算法效果再移植到MCU。4.2 数据标定从ADC值到物理量ADC读到的值是0-4095我们需要将其转换为有意义的物理量如电压、温度、压力等。这需要标定。两点标定法线性传感器 假设传感器输出与物理量成线性关系物理量 K * ADC_Value B我们已知两个标定点在物理量P1时ADC值为AD1在物理量P2时ADC值为AD2则可以计算K (P2 - P1) / (AD2 - AD1) B P1 - K * AD1在程序中每次采样后计算Physical_Value K * ADC_ConvertedValue B;实操心得对于精度要求高的场合标定点应尽量靠近测量范围的两端以减少线性误差。标定系数K和B可以存储在MCU的Flash中如EEPROM模拟区域。如果传感器存在零漂可以定期进行单点零点校准如在已知零点时读取ADC值AD_ZERO然后计算物理量时使用(ADC_Value - AD_ZERO)代替原始值。5. 典型问题排查与性能优化实录在实际项目中模拟信号处理部分总是问题的高发区。下面是我总结的一些常见“坑”及其解决方法。5.1 ADC采样值不稳定、跳动大这是最普遍的问题。可能的原因和排查步骤像侦探破案检查电源和参考电压这是首要怀疑对象。用示波器测量VDDA和VSSA之间的电压看是否平稳纹波是否过大应小于50mV。检查去耦电容是否焊接良好容值是否正确。检查模拟输入信号本身将ADC输入引脚断开直接测量信号源看其是否稳定。传感器供电不稳、信号线引入干扰都会导致源信号不稳。优化采样时间如果信号源阻抗较大如10kΩADC内部的采样保持电容充电不足。增加ADC_SampleTime如从13.5周期增加到239.5周期会有立竿见影的效果。计算公式大致为T_sampling (采样周期数 12.5) / f_ADCCLK。确保采样时间远大于信号源阻抗与采样开关等效电阻构成的RC电路的时间常数。检查PCB布局和接地模拟信号走线是否远离数字噪声源时钟线、PWM线、高速数据线模拟地和数字地是否单点连接模拟部分是否被数字地线环绕不合理的布局会引入难以消除的噪声。软件滤波在硬件条件无法改变时合理的软件滤波是最后的保障。根据信号特性选择合适的滤波算法和参数。5.2 DAC输出波形有台阶、毛刺或失真台阶感量化噪声这是数字信号的本质决定的分辨率12位有限。如果需要更平滑的波形可以在DAC输出后加一个模拟低通滤波器一阶或二阶RC滤除高频量化噪声。使用过采样技术。例如以4倍于目标更新率的频率更新DAC并配合模拟滤波可以有效提高等效分辨率。毛刺Glitch在DAC输入数字码变化时由于内部开关动作不同步可能会产生瞬间的尖峰脉冲。STM32的DAC内置了消毛刺电路但输出缓冲使能时效果更好。对于极敏感的应用可以在DAC输出后加一个采样保持电路或简单的RC滤波。波形失真建立时间不足如果你用软件循环更新DAC生成高频波形可能因为代码执行时间抖动导致更新间隔不一致。务必使用定时器触发DAC更新配置DAC_Trigger为定时器触发源这是保证波形时序精确、频率稳定的唯一正确方法。让硬件定时器自动控制更新节奏CPU仅负责准备下一个数据点通常用DMA配合更佳。5.3 多通道ADC采样时通道间串扰当扫描采样多个通道时有时会发现一个通道的电压值会影响另一个通道的读数。这通常是因为通道切换后ADC内部采样电容上的电荷没有完全释放导致的。解决方法在ADC初始化序列中适当增加通道切换后的延迟。STM32的ADC本身在切换通道时会插入一定的“稳定时间”但如果信号源阻抗很大这个时间可能不够。可以在软件上在读取一个通道后、启动下一个通道转换前插入微秒级的延时for(i0;i100;i) __NOP();。在硬件上可以在每个ADC输入引脚对地接一个小的电容如10pF~100pF作为电荷的泄放路径。但注意这个电容会和信号源阻抗构成低通滤波器影响高频信号。最根本的方法是降低信号源阻抗或者在信号进入ADC之前先用一个运放做电压跟随器进行缓冲隔离。5.4 性能优化清单当系统对ADC/DAC性能要求较高时可以对照以下清单进行优化优化目标硬件措施软件措施提高精度1. 使用高精度、低温漂的基准电压源如REF系列芯片2. 为模拟部分提供独立、干净的LDO电源3. 优化PCB布局严格分区模拟/数字部分4. 信号调理电路使用高精度、低噪声运放1.每次上电后执行ADC校准2. 多次采样取平均过采样3. 实施软件滤波算法如滑动平均4. 定期进行系统标定补偿温漂提高速度1. 提高ADC时钟在允许范围内如14MHz2. 选择低阻抗信号源或加缓冲1. 使用DMA传输减少CPU中断开销2. 选择更短的采样时间需平衡精度3. 将ADC配置为单次模式仅在需要时触发降低功耗1. 在不使用时关闭ADC/DAC模块的时钟和电源如果支持2. 使用高阻抗传感器以降低驱动电流1. 采用间歇采样模式而非连续转换2. 降低ADC时钟频率3. 在低功耗模式下通过定时器或外部事件唤醒ADC进行采样处理模拟信号一半是软件功夫一半是硬件功夫。很多时候软件上的“神奇滤波”只是在弥补硬件设计的不足。我的经验是在项目初期就重视电源、参考电压和PCB布局这部分多花一天时间后期调试可能就能省下一周。当你拿到一个跳动剧烈的ADC数值时第一步永远是拿起示波器去看电源、看信号源而不是一头扎进代码里调整滤波参数。理解了噪声从哪里来才能知道该到哪里去解决它。模拟通道是嵌入式系统中最能体现“系统级”思维的部分它要求开发者同时具备数字逻辑和模拟电路的知识虽然挑战不小但一旦掌握解决问题的成就感也是最大的。

相关新闻