ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 ADC+DMA+串口组合:高效数据采集与传输的嵌入式解决方案

STM32 ADC+DMA+串口组合:高效数据采集与传输的嵌入式解决方案 简介STM32 ADCDMA串口资源面向嵌入式开发者和STM32入门者演示如何利用ADC多通道采样、DMA无CPU干预传输和USART串口输出解决连续采集与实时传输场景下的效率问题。压缩包为RAR格式共43个文件约490KB主要包含C源文件、H头文件、Keil工程配置、hex烧录文件及链接映射文件等适用于STM32F10x系列可直接在MDK环境下编译调试。已有494人学习下载。资源内含基于标准外设库的完整工程代码按HARDWARE、USER、SYSTEM等模块组织涵盖ADC初始化、DMA流配置、串口波特率与中断设置并结合LED状态指示和延时函数便于理解外设联动逻辑。通过这套模板读者可快速搭建自己的数据采集与发送链路边看工程边验证适合需要对照例程做二次开发或课程设计的开发人员。1. 整体设计思路为什么是ADCDMA串口这个组合先聊点实在的。STM32做模拟量采集最简单粗暴的办法就是主循环里轮询ADC转换转换完了把数据往串口一丢。但只要你真正上手做过一次就会立刻发现这条路根本走不通主循环被ADC堵死CPU占用率飙升系统稍微加点别的任务就开始卡顿串口数据还经常丢帧。这个时候ADCDMA串口这套组合就成了嵌入式开发里的标准答案。这三者为什么必须绑在一起用我拆开说。ADC负责把模拟电压变成数字量是数据源DMA负责把ADC转换完的数据从外设寄存器直接搬运到内存全程不需要CPU参与串口负责把内存里的数据发出去让电脑端的串口调试助手能看到波形或者数值。整个流程里CPU只在最开始配置一下之后就可以去干别的事了数据流转完全由硬件自动完成。这个设计理念说白了就是“让硬件干硬件该干的事”跟流水线作业一个道理每一站只需要做自己的事中间不需要人来干预。这个方案适合谁来参考刚入门STM32没多久、准备做数据采集项目的同学或者已经在项目里被ADC轮询折腾得焦头烂额的工程师。这套组合能解决的问题很简单却很致命如何在不高频占用CPU的情况下稳定高效地把模拟信号采集并传输到上位机。不管你是要做传感器数据采集、电机电流监测还是音频采样这套底层架构都能直接套用甚至可以说它是STM32嵌入式开发的“基建工程”。我当时接手这个项目的时候需要的采样率并不算高1kHz左右的ADC采样但要求CPU能同时处理UI刷新和按键响应所以精准命中“ADCDMA串口”这个组合。后面所有代码我都基于这个需求展开尽量把参数和逻辑讲透方便你直接抄作业再按需修改。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引脚PA9TX和PA10RX。这个选择的考量是PA0/PA1是ADC1的常规模拟输入脚不和其他外设复用开发板上有排针引出来方便接电位器或者传感器信号。时钟这块很多人容易忽略。ADC的采样频率是挂在APB2总线上的默认系统时钟72MHz时APB2是72MHz但ADC本身有一个分频器最大工作频率不能超过14MHz。我配置的是6分频也就是72/612MHz刚好卡在安全线以内。这个参数直接影响后面的采样周期和转换时间计算必须要先定下来。注意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 实操代码标准库方式的ADCDMA配置下面直接上干货这是我验证过可以跑通的完整初始化代码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和DMAadc_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降低信号源阻抗只有第一个通道有数据第二个为0DMA内存地址未自增或通道数量配置错误检查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. 项目扩展思路和建议这套ADCDMA串口的架构调试稳定之后我建议你可以往几个方向继续扩展。第一个方向是多路采集当前代码支持2路稍微改改就能扩展到6路甚至更多DMA缓冲区大小和ADC通道数同步修改就行。第二个方向是数据协议升级把裸数据加上帧头、帧尾、CRC校验甚至封装成JSON格式这样上位机解析就更轻松。第三个方向是高吞吐量场景比如音频采样这时可能要把数据存到SD卡或者通过USB高速传输这套DMA的思路依然适用只是外设换了一下。在做扩展的时候你始终要把握住这套方案的底层逻辑ADC产生数据DMA负责搬运串口负责传输CPU只做调度和决策。每个环节都要保证不成为性能瓶颈这个原则在嵌入式开发里放之四海而皆准。我自己在这套方案上踩过最大的一次坑是DMA缓冲区和ADC扫描顺序不匹配导致两个通道的数据互换却毫无察觉最后是在数据对比时才发现。排查了很久最后发现是ADC_RegularChannelConfig里面通道顺序设置和DMA配置的顺序不对。这类问题用调试器看内存值是最快的建议你在做类似调试的时候第一个动作先看看DMA缓冲区里的原始数据是否合理再往下排查。这条经验说白了就是“先看数据本身再查链路配置”过来人的建议确实值钱。本文还有配套的精品资源点击获取
返回列表