简介:STM32 ADC+DMA+串口资源面向嵌入式开发者和STM32入门者,演示如何利用ADC多通道采样、DMA无CPU干预传输和USART串口输出,解决连续采集与实时传输场景下的效率问题。压缩包为RAR格式,共43个文件,约490KB,主要包含C源文件、H头文件、Keil工程配置、hex烧录文件及链接映射文件等,适用于STM32F10x系列,可直接在MDK环境下编译调试。已有494人学习下载。资源内含基于标准外设库的完整工程,代码按HARDWARE、USER、SYSTEM等模块组织,涵盖ADC初始化、DMA流配置、串口波特率与中断设置,并结合LED状态指示和延时函数,便于理解外设联动逻辑。通过这套模板,读者可快速搭建自己的数据采集与发送链路,边看工程边验证,适合需要对照例程做二次开发或课程设计的开发人员。
1. 整体设计思路:为什么是ADC+DMA+串口这个组合
先聊点实在的。STM32做模拟量采集,最简单粗暴的办法就是主循环里轮询ADC转换,转换完了把数据往串口一丢。但只要你真正上手做过一次,就会立刻发现这条路根本走不通,主循环被ADC堵死,CPU占用率飙升,系统稍微加点别的任务就开始卡顿,串口数据还经常丢帧。这个时候,ADC+DMA+串口这套组合就成了嵌入式开发里的标准答案。
这三者为什么必须绑在一起用?我拆开说。ADC负责把模拟电压变成数字量,是数据源;DMA负责把ADC转换完的数据从外设寄存器直接搬运到内存,全程不需要CPU参与;串口负责把内存里的数据发出去,让电脑端的串口调试助手能看到波形或者数值。整个流程里,CPU只在最开始配置一下,之后就可以去干别的事了,数据流转完全由硬件自动完成。这个设计理念说白了就是“让硬件干硬件该干的事”,跟流水线作业一个道理,每一站只需要做自己的事,中间不需要人来干预。
这个方案适合谁来参考?刚入门STM32没多久、准备做数据采集项目的同学,或者已经在项目里被ADC轮询折腾得焦头烂额的工程师。这套组合能解决的问题很简单却很致命:如何在不高频占用CPU的情况下,稳定高效地把模拟信号采集并传输到上位机。不管你是要做传感器数据采集、电机电流监测还是音频采样,这套底层架构都能直接套用,甚至可以说它是STM32嵌入式开发的“基建工程”。
我当时接手这个项目的时候,需要的采样率并不算高,1kHz左右的ADC采样,但要求CPU能同时处理UI刷新和按键响应,所以精准命中“ADC+DMA+串口”这个组合。后面所有代码我都基于这个需求展开,尽量把参数和逻辑讲透,方便你直接抄作业再按需修改。
2. 硬件环境与工具选型:先把地基打牢
2.1 核心器件和开发环境
我用的主控是STM32F103C8T6,也就是大家常说的“蓝丸”核心板。这块板子便宜、资料多、引脚够用,非常适合作为ADC采集项目的载体。你如果有其他的STM32型号也没关系,只要带ADC外设和DMA控制器就能用,代码层面的差异主要集中在引脚映射和时钟配置上。
开发环境我用的是标准库加Keil MDK 5的组合,配合ST-Link下载器。虽然现在ST官方主推HAL库加STM32CubeMX,但标准库的代码结构更直观,寄存器级别的操作逻辑清楚,对理解DMA的工作机制其实更有帮助。如果你习惯用HAL库,我在后面章节也会标注对应的HAL接口,方便你对照移植。
串口硬件方面,我用的是板载的CH340 USB转串口芯片,直接用USB线连接电脑。这里必须提醒一句,如果你用的是其他USB转串口模块,一定要装好对应的驱动(对应CH340或FTDI驱动),不然设备管理器里会显示“无法识别的设备”,到时候查半天还以为是代码问题。
2.2 引脚分配和时钟规划
ADC采集我选择了PA0和PA1两个引脚,对应ADC1的通道0和通道1。串口用的是USART1,引脚PA9(TX)和PA10(RX)。这个选择的考量是:PA0/PA1是ADC1的常规模拟输入脚,不和其他外设复用,开发板上有排针引出来,方便接电位器或者传感器信号。
时钟这块很多人容易忽略。ADC的采样频率是挂在APB2总线上的,默认系统时钟72MHz时,APB2是72MHz,但ADC本身有一个分频器,最大工作频率不能超过14MHz。我配置的是6分频,也就是72/6=12MHz,刚好卡在安全线以内。这个参数直接影响后面的采样周期和转换时间计算,必须要先定下来。
注意:ADC的时钟不是越快越好,太快会让采样电容充电时间不够,导致采样值偏小或者跳动;太慢又会降低最大采样速率。12MHz是一个比较均衡的取值。
3. ADC采样核心细节:从单通道到多通道的进阶之路
3.1 采样周期、转换时间与数据对齐
很多初学者第一次配ADC都会被“采样周期”这个概念搞晕。简单来说,ADC内部是一个逐次逼近型的比较器,每次转换都需要一定的时间来完成电荷再分配和数据逼近。STM32F103的ADC转换时间公式是:
转换时间 = (采样周期 + 12.5个周期) / ADC时钟频率
我在配置里把采样周期设为239.5个周期,那单次转换时间就是(239.5 + 12.5) / 12MHz ≈ 21μs。这个速度换算下来大概每秒能转4.7万次,对传感器数据采集来说绰绰有余。你会问为什么设置这么大的采样周期?因为我的信号源是电位器分压,输出阻抗比较高,采样周期太短会导致外部信号源来不及给内部采样电容充电,读数就偏了。如果你的信号源输出阻抗低(比如运放缓冲后的信号),可以把这个参数往下调,用更快的采样率。
数据对齐方面,ADC的转换结果寄存器是12位有效位,硬件上可以设置左对齐或者右对齐。我统一用右对齐,这样直接读寄存器得到的数值就是0~4095的实际采样值,后续换算电压或者百分比都不用移位,逻辑比较直观。
3.2 多通道顺序扫描的DMA配置
当你有两个通道以上的数据要采集时,就得开启ADC的扫描模式,并配合DMA把多个通道的结果按顺序搬到内存数组里。这里有个关键点:DMA每次触发搬运的数据宽度必须和ADC的数据寄存器宽度一致。我配置的是半字(Half Word)模式,对应16位宽度,刚好能容纳ADC转换结果的12位有效数据。
DMA的内存地址要定义一个数组,比如:
uint16_t adc_buf[2] = {0};两个通道的数据会依次填充到这个数组的第0和第1个元素中。这里有个坑:DMA的循环模式(Circular Mode)必须开启,不然缓冲区满了之后DMA会停止搬运,ADC扫描的数据就会覆盖前一次结果。开启循环模式后,DMA会自动回卷到数组头继续填充,形成环形缓冲,CPU随时可以读取最新的一批数据。
这里再延伸一个知识点:DMA连续请求(Continuous Requests)这个参数。在ADC的配置里,如果关闭连续转换模式(即单次扫描模式),那么每次软件触发后,ADC只扫描一轮就停止,等待下一次触发;如果开启了连续请求,ADC会持续不断地扫描并触发DMA搬运。我实际测试下来,对于连续采集场景,必须把连续请求打开,否则数据更新速率完全跟不上需求。
提示:如果你只想在固定时刻采集一次,可以关掉连续请求,改为定时器触发或外部中断触发,这种模式在“按需采集”的场景下更省电,也更精准。
3.3 实操代码:标准库方式的ADC+DMA配置
下面直接上干货,这是我验证过可以跑通的完整初始化代码:
void ADC_DMA_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; DMA_InitTypeDef DMA_InitStructure; ADC_InitTypeDef ADC_InitStructure; // 1. 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 2. 配置PA0、PA1为模拟输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); // 3. 配置DMA:数据从ADC1的数据寄存器搬到adc_buf数组 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buf; 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; 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); // 4. 配置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; // 两个通道 ADC_Init(ADC1, &ADC_InitStructure); // 5. 设置通道采样顺序和采样时间 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5); // 6. 使能ADC和DMA ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); // 7. 校准 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); // 8. 开始软件触发 ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 9. 启动DMA搬运 DMA_Cmd(DMA1_Channel1, ENABLE); }初始化完成之后,主循环里你根本不需要再去操作ADC和DMA,adc_buf这个数组里的数据会自动保持在最新状态。读取的时候直接:
uint16_t ch0_value = adc_buf[0]; uint16_t ch1_value = adc_buf[1];非常简单,但效率极高。CPU全程只做了读内存这件事,之前担心的数据覆盖、转换卡顿的问题全部由硬件解决了。
4. 串口数据交互与上位机对接
4.1 串口调试助手的数据格式设计
数据采集到了,下一步就是怎么发到电脑上。最简单的方案是直接把裸数据通过USART发出去,用串口调试助手查看。但这里有个效率问题:如果用标准库的printf重定向到USART,每次发送都需要CPU逐字节写入,当你的发送频率达到1kHz以上时,CPU又会被拖累。
更好的做法是让串口也走DMA,或者至少用DMA发送。我在这套方案里使用了DMA发送,具体操作是把要发送的数据先拼装到一个发送缓冲区里,然后启动一次DMA传输,CPU继续干别的活,发送完成后DMA会触发中断通知你。
发送的数据格式我这里用的是每个通道4字节十六进制数,共8字节一包,加上帧头和校验,总共12字节。帧头用0xAA 0x55,校验用简单的累加和,这样在上位机上做解析就很方便。
4.2 串口DMA发送:别再傻傻等待上一次发完
网络热搜词里有个问题问得很经典:“DMA串口发送需要等待上一轮数据发送完吗?”答案是:如果开启了DMA的传输完成中断,并在中断标志位里判断,是不需要手动等待的。但如果你开了循环发送或者高频发送,就必须注意DMA缓冲区的覆盖问题。
我实际测试下来,最稳妥的做法是设置一个发送忙标志,在DMA传输完成中断里把这个标志清零,每次发送前检查一下这个标志。如果上一次还没发完,就把新数据缓存到一个待发送队列,等传输完成后再发送。这个设计和TCP协议的滑动窗口思路类似,保证发送方不会覆盖接收方还没来得及读走的数据。
4.3 串口接收不上来的排查经验
很多人会遇到串口调试助手完全收不到数据的情况。按照我的排查顺序,一般都能快速定位问题:
- 先看设备管理器里有没有识别到COM口,如果识别不到,检查CH340驱动和USB线,这个排在第一位,因为硬件都没通,后面都白搭。
- 用示波器或者万用表量USART1的TX引脚(PA9),发送的时候有没有电平跳变。没跳变说明代码没有执行到发送逻辑,检查配置和发送函数。
- 检查波特率是否和上位机设置一致,这是最常见的低级错误,但发生频率远超想象。
- 如果数据是乱码,检查时钟配置是否正确。USART的波特率寄存器是根据APB2时钟计算的,一旦时钟树配错,波特率会偏到没谱。
注意:STM32虚拟串口(Virtual COM Port)经常会在电脑上出现黄色感叹号,这多半是驱动版本不兼容。解决方法是到ST官网下载最新驱动,或者用驱动精灵更新一下,问题就能解决。
5. 常见问题与排查技巧实录
5.1 典型Bug速查表
本章节直奔主题,我整理了做这个项目过程中遇到的最典型问题和对应的解决方案:
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| ADC采样值全是4095或0 | 对应引脚未配置为模拟输入 | 检查GPIO模式是否为GPIO_Mode_AIN |
| 采样值持续跳变、噪声大 | 采样周期过短或信号源阻抗过高 | 增大ADC_SampleTime,降低信号源阻抗 |
| 只有第一个通道有数据,第二个为0 | DMA内存地址未自增或通道数量配置错误 | 检查DMA_MemoryInc是否使能,ADC_NbrOfChannel是否为2 |
| 串口收到的是乱码 | 波特率计算错误或时钟树配置异常 | 检查RCC时钟配置,对照数据手册重新计算USARTDIV |
| 数据全部正确但上位机显示断线 | USB转串口芯片供电不足或线材质量差 | 更换优质USB线,或使用外部5V供电 |
| 程序下载时报“No STM32 Target Found” | 调试器连接异常或芯片加密 | 检查ST-Link接线,按住复位键尝试下载,必要时用ST-Link Utility解锁 |
5.2 数据滤波与校准心得
ADC的裸数据直接输出到一个实际的工业项目里肯定是不行,外部干扰和电源噪声会让数据看起来很“毛”。我做过多种滤波方案的对比:算术平均滤波、中值滤波、一阶低通滤波。简单粗暴地说,前两种只适合变化缓慢的信号,比如温度、湿度;对于动态响应要求高的场景,一阶低通滤波更好用。
C语言实现一阶低通滤波只需要一行代码:
filtered_value = (uint16_t)((float)filtered_value * 0.9f + (float)raw_value * 0.1f);滤波系数0.1决定了跟随速度和滤波强度的平衡。系数越小,数据越平滑,但响应越迟钝;系数越大,响应越快,但噪声抑制差。这个参数需要根据你的信号特性去调,没有一劳永逸的值。
关于校准,很多外部ADC芯片(如HX711、ADS1232这类24位ADC)都推荐做三点校准,因为实际器件的增益和零点偏移都不是理想值。STM32内部12位ADC虽然没有那么高精度,但也存在出厂误差,建议在项目正式投用之前,用标准电压源测几组数据,做一下线性校准,把校准系数存在Flash里,这样最终的测量精度能提升不少。
5.3 不定长串口数据的接收方案
最后再分享一个进阶技巧。如果你做的项目不是单纯发数据,而是需要接收上位机的指令,那么就会遇到不定长数据的接收问题。很多人的第一反应是“收到什么就处理什么”,但串口数据是流式的,一次指令可能分多次到达,处理起来很容易出问题。
推荐的做法是“DMA接收 + 空闲中断”组合方案。DMA负责把数据连续搬到接收缓冲区,空闲中断(IDLE)则在你收完一帧数据后触发一次,告诉你“这一轮数据已经收完了”。整个方案的核心逻辑是:DMA保证数据不丢,空闲中断保证你知道什么时候数据帧结束。这个方案在网上相关讨论中已经很成熟,实现起来代码量也不大,非常推荐你研究一下。
6. 项目扩展思路和建议
这套ADC+DMA+串口的架构调试稳定之后,我建议你可以往几个方向继续扩展。第一个方向是多路采集,当前代码支持2路,稍微改改就能扩展到6路甚至更多,DMA缓冲区大小和ADC通道数同步修改就行。第二个方向是数据协议升级,把裸数据加上帧头、帧尾、CRC校验,甚至封装成JSON格式,这样上位机解析就更轻松。第三个方向是高吞吐量场景,比如音频采样,这时可能要把数据存到SD卡或者通过USB高速传输,这套DMA的思路依然适用,只是外设换了一下。
在做扩展的时候,你始终要把握住这套方案的底层逻辑:ADC产生数据,DMA负责搬运,串口负责传输,CPU只做调度和决策。每个环节都要保证不成为性能瓶颈,这个原则在嵌入式开发里放之四海而皆准。
我自己在这套方案上踩过最大的一次坑,是DMA缓冲区和ADC扫描顺序不匹配,导致两个通道的数据互换却毫无察觉,最后是在数据对比时才发现。排查了很久,最后发现是ADC_RegularChannelConfig里面通道顺序设置和DMA配置的顺序不对。这类问题用调试器看内存值是最快的,建议你在做类似调试的时候,第一个动作先看看DMA缓冲区里的原始数据是否合理,再往下排查。这条经验,说白了就是“先看数据本身,再查链路配置”,过来人的建议,确实值钱。
本文还有配套的精品资源,点击获取