基于STM32的数据采集系统设计:从ADC采样到串口协议全解析
简介:本资源是一套面向电子信息、物联网、自动化等专业本科生的高分单片机课程设计与毕业设计实践方案,聚焦基于STM32的数据采集系统开发,解决传感器信号采集、AD转换、实时显示与数据存储等典型嵌入式应用问题。压缩包共194个文件,含43个头文件(.h)定义外设接口与结构体,42个C源文件(.c)实现ADC采样、定时器控制、LCD显示及串口通信等核心功能,辅以编译中间文件(.o/.d/.crf)、工程配置(.uvproj/.uvopt)、烧录镜像(.hex)及自动化脚本(.bat),完整覆盖Keil MDK开发全流程,包体大小为4.71MB。已有133人下载学习,资源源自答辩得分95分的校级优秀项目,提供可直接运行的源码、详细教程文档及模块化工程结构,便于理解底层驱动逻辑、快速复现系统功能或在此基础上扩展温湿度、压力等多通道采集需求。 每年到毕业季和课设提交期,“基于STM32的数据采集系统”这类题目就会在各个技术社区刷屏。找我咨询这个题目的人特别多,但大部分人的处境都差不多:要么从网上下了一堆打包好的源码却跑不通,要么跑通了但答辩时被老师问得哑口无言。这篇文章我想把我做一个完整数据采集系统的思路、器件选型逻辑、代码框架、调试踩坑过程,以及最后怎么把项目资料整理得像模像样,一次性讲清楚。适合正在做这个题目的在校学生,也适合刚入门STM32想系统做一个小项目的开发者参考。
1. 设计一个数据采集系统,第一步不是写代码而是定需求
1.1 从项目名称反推这个系统要干什么
“STM32单片机数据采集系统”听起来很宽泛,实际上不管题目怎么描述,核心需求就那么几件事:采集多路模拟信号,经过MCU处理后,把数据上传到上位机进行显示或存储。
我在做这个项目时,最早犯的错误就是一上来就翻数据手册、找例程,结果半个月过去了还在看ADC的寄存器。后来冷静下来,花了一天时间把需求拆清楚,整个项目的开发效率反而高了很多。
拆需求时我习惯问自己五个问题:
- 采集几路信号?是电压、电流、温度还是其他物理量?
- 信号的幅度范围是多少?是否需要信号调理电路?
- 要求的采样率是多少?每秒采10次还是每秒采10万次,这决定了用不用DMA、用不用定时器触发。
- 数据传到哪?PC上位机、手机APP还是云端?
- 供电方式是什么?是USB供电还是独立电源供电?
这些问题看着很简单,但直接把系统架构定死了。以我现在手上这套系统为例:采集两路0-3.3V的模拟电压信号(其中一路是温度传感器经调理后的电压),加上一路通过SPI接口读取的数字温湿度传感器数据,采样率定在每秒1000个点,传输方式用串口连PC上位机。这套需求定位是入门级但功能完整的课设/毕设项目,不追求极致性能,但五脏俱全。
1.2 器件选型:主控、传感器、通信模块怎么搭配
先把选型思路讲清楚,这部分决定了项目后面做得顺不顺利。
主控芯片:我选的是STM32F103C8T6。这颗芯片几乎是国内开发者最熟悉的MCU,价格便宜、资料多、教程多,遇到问题随便一搜就有解决方案。虽然现在STM32G4、H7系列性能更强,但对这个项目来说F103完全够用,而且用最经典的芯片把系统做透,反而比盲目上高性能芯片更能学到东西。
传感器:我的系统设计了两类输入信号。一路是电位器输出的0-3.3V模拟电压,用来模拟真实的传感器输出信号,方便演示和调试;另一路是LM35温度传感器,输出电压与温度呈线性关系,10mV/℃,0℃时输出0V。选择LM35而不用DS18B20,就是为了同时覆盖模拟信号采集和数字信号采集两种形态。如果你要用真正的工业传感器,比如PT100热电阻或4-20mA电流环变送器,就需要在选型阶段额外考虑信号调理电路的设计,这个我在后面硬件部分单独说。
通信模块:最稳妥的方案就是USB转串口,也就是CH340或CP2102芯片的小板子,直接连到STM32的USART1引脚。不推荐选用蓝牙或WiFi模块作为主通信方式,因为无线传输的调试难度会成倍增加,而且答辩现场很容易出现连接不稳定的尴尬情况。
1.3 采样率与分辨率:先算清楚再动手
很多初学者不关心采样率,觉得ADC配置好能出数就行。但数据采集系统的核心指标就是采样率和分辨率,这两个参数决定了系统的应用边界。
STM32F103的ADC是12位的,也就是量程0-3.3V被分成4096份,理论分辨率是3.3V除以4096,约为0.8mV。这个分辨率对于大部分课设场景足够了。如果算上放大器噪声和参考电压波动,实际有效位数大概在10-11位左右,这个后面调试时候会验证到。
采样率方面,F103的ADC最高可以跑到1MHz采样时钟,但注意这是指ADC转换本身,实际系统采样率受限于信号调理电路的带宽、DMA传输速度以及MCU处理能力。我的系统定在每秒1000个点,每个点同时采集两路ADC通道,对F103来说非常轻松,CPU占用率很低,可以腾出时间处理通信和显示。这里我建议初学者设计时先算清楚这个账,不要盲目追求高采样率,否则后期会陷入各种瓶颈。
注意:如果要做FFT频谱分析或电机电流波形捕获这类应用,采样率的要求会严格很多。这时建议直接用定时器触发ADC采样,而不是用轮询或延时方式,后者会产生严重的采样时间抖动,导致频域计算结果失真。定时器触发这部分在软件章节详细展开。
2. 硬件电路里最容易丢分的地方:信号调理与电源设计
2.1 模拟前端:传感器信号进ADC之前的必经之路
不少人做数据采集系统时,直接把传感器输出线接到STM32的ADC引脚上,这在小信号场景下是致命的。以LM35为例,它在室温25℃时输出电压只有250mV,而STM32的ADC量程是0-3.3V,如果用满量程测量,250mV只占ADC编码的不到1/13,温控精度会非常有限,而且信号极易被噪声淹没。
因此我加了一级同相放大电路,把LM35的输出放大10倍,这样室温下信号变成2.5V左右,能充分利用ADC的量程。运放选择了LM358,虽然它带宽不高、噪声也不算最低,但便宜、单电源供电、在低速采集场景下完全够用。需要注意的是,同相放大电路的增益由电阻比值决定,也就是1加上Rf除以Rin。我用了10kΩ和1kΩ的电阻组合,增益为1加上10k除以1k,等于11倍,实际输出稍微比10倍高一点,但正好留出了余量。
LC或RC滤波电路也不能省。在每个ADC输入引脚前放一个100Ω电阻和100nF电容组成RC低通滤波器,截止频率约为1除以(2π乘以100乘以1e-7),大约16kHz,可以有效抑制高频干扰。这个设计看似简单,但能明显降低ADC采样值的跳动幅度,后面调试章节我会展示实测对比数据。
2.2 电源与参考电压:ADC跳动一半是供电惹的祸
这个坑我踩得非常深。最初我直接用USB线给开发板供电,ADC采集的数值跳动范围达到了十几个LSB,也就是多达十几级的波动,用信号发生器输入标准的1V电压,读到的数值却一直在变化,根本无法使用。排查了很久后才锁定问题根源——USB供电的5V经过LDO降到3.3V后,纹波很大,而这个3.3V直接作为ADC参考电压,ADC基准本身不稳,转换结果自然就跟着乱跳。
解决办法:
- 将模拟电路和数字电路分开供电,至少要在电路板上将模拟地AGND和数字地DGND单点连接,避免数字开关噪声串入模拟回路。
- 在STM32的VREF+引脚(F103C8T6没有独立VREF+引脚,它与VDDA在内部相连)处加一个10uF钽电容和100nF陶瓷电容并联去耦,这两个电容要尽量靠近MCU的电源引脚放置。
- 如果追求更高精度,可以外接一个REF3030或TL431基准电压源,把参考电压做稳。虽然F103不能直接通过外部电压驱动内部ADC参考源,但可以通过外部基准芯片接到VDDA上来间接改善。
我在这套系统中,经过上述处理后ADC跳动从十几个LSB降到了正负2个LSB以内,效果非常显著。这部分经验值得所有做数据采集的人记下来。
2.3 硬件连接检查清单
画原理图和焊接前一定要检查这些点,我每次做板子都会过一遍,至少帮我避免一半以上的低级错误:
- 每个电源引脚旁都有100nF去耦电容,引脚较远的再加一个10uF电容。
- 复位引脚外部接10kΩ上拉电阻和100nF电容,而不是悬空。
- BOOT0引脚通过10kΩ电阻下拉到地,确保从主Flash启动。
- 芯片的VDDA引脚必须接3.3V并加滤波电容,而不是悬空。
- ADC输入信号电压不能超过VDDA,否则内部保护二极管会导通,轻则采集不准,重则损坏引脚。
- SWD调试接口的SWDIO和SWCLK两个引脚不要复用做其他功能,否则程序烧录到一半可能连不上调试器。
3. STM32软件框架:ADC多通道扫描与DMA的配合方式
3.1 标准外设库、HAL库还是LL库
软件部分第一个选择就是开发库。很多人纠结这个问题,我的建议很简单:如果老师没有特殊要求,用HAL库+STM32CubeMX初始化。理由有三:生成代码快、配置外设时不容易漏掉时钟树设置、网上HAL库的教程数量已经超过了标准外设库。
但HAL库也有让人头疼的地方,比如初始化代码量大、回调机制不够直观、调试时看着层层封装有点懵。所以我自己的代码风格是:CubeMX生成初始化代码,然后在main函数或专门的任务函数里写业务逻辑,核心的ADC采集和串口接收使用HAL库API,但中断处理部分自己写逻辑。
3.2 ADC多通道扫描+DMA循环采样的配置思路
先解释一下为什么必须要用DMA。如果不用DMA,每采集一个ADC通道的数据,CPU都要在ADC转换完成后去读数据寄存器,这段时间里CPU不能做其他事。而使用DMA后,ADC转换完成会自动触发DMA搬运,把转换结果从ADC的数据寄存器搬到内存数组里,全程不需要CPU干预。对于多路循环扫描来说,DMA的循环模式还能自动循环填充缓冲区,CPU只需要定期去读缓冲区的内容即可。
在CubeMX中的关键配置如下:
- ADC设为Scan Conversion Mode,也就是扫描模式,Enable。
- Continuous Conversion Mode设为Enable,连续转换。
- Number Of Conversion配置为2,因为我有两路模拟输入。
- 在Rank下拉菜单中依次配置两个通道,比如Rank 1选IN0,Rank 2选IN1,采样周期Sampling Time选择239.5 Cycles。为什么选这么长的采样周期?因为输入信号源阻抗较大时,ADC内部的采样电容需要足够时间充放电,采样时间太短会导致精度下降。
- 打开ADC的DMA请求,Mode设为Circular循环模式,Data Width设为Half Word。注意ADC1的数据寄存器是16位的,而STM32的DMA传输宽度要匹配好,否则数据会错乱。
然后在代码中定义两个变量即可:
uint16_t adc_buffer[2] = {0, 0}; HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buffer, 2);启动DMA后,adc_buffer[0]永远是通道0的转换结果,adc_buffer[1]永远是通道1的结果,前提是配置DMA时数据长度要刚好等于通道数,且ADC和DMA都工作正常。这里有个常见错误:如果你的DMA配置成Normal模式,一轮采集完成后DMA就停止了,后续ADC数据不会自动搬运,导致缓冲区长期停留在第一次采样的结果。我之前在这个问题上卡了整整一个下午。
3.3 定时器触发采样:为什么需要精确等间隔
连续扫描+DMA循环模式已经能自动采集数据了,为什么还需要定时器触发?这里面有一个关键概念叫等间隔采样。
普通的连续转换模式,ADC会等上一次转换结束后立刻开始下一次转换,两次转换之间的时间间隔完全取决于硬件状态,不是严格等间隔的。虽然大部分时候差别不大,但如果你对采集的数据做FFT、滤波或者计算频率特征,非等间隔采样会在频谱上引入不该有的噪声成分。
定时器触发采样的原理很好理解:定时器产生一个固定频率的更新事件,这个事件直接通过硬件信号连接到ADC的触发输入端,ADC在这个信号的上升沿开始一次转换,于是每次采样的间隔就由定时器的精准时钟决定,与软件执行时间完全无关。
具体配置方法:在CubeMX中选一个定时器,比如TIM2,配置为PWM Generation或Update Event模式,频率设为1000Hz。然后在ADC配置里,Trigger Source选择TIM2 Trigger Out。这样硬件会完成时间同步,不需要在代码里去操作定时器。初始化时先启动定时器,再启动ADC的DMA采集:
HAL_TIM_Base_Start(&htim2); HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buffer, 2);这里顺序很重要。必须先启动定时器,否则ADC触发信号一直不会来,DMA缓冲区就一直不会被更新。我调试的时候曾经把两行顺序写反,结果采集到的数据永远是上一个周期的旧数据,找了好久才意识到是触发时序的问题。
4. 串口数据链路:协议设计、空闲中断与环形缓冲
4.1 数据帧格式怎么定
采集到的数据最终要发给上位机显示或存储,串口是最简单可靠的通道。但如果你直接把原始数据裸发出去,接收端根本不知道哪两个字节是一帧,遇到偶尔的字节丢失就可能整帧错乱。所以自定义一个帧协议是必须的。
我的协议设计如下:
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧头 | 2字节 | 固定为0xAA 0x55,用于识别帧起始 |
| 命令字 | 1字节 | 0x01表示实时数据帧,0x02表示历史数据查询 |
| 长度 | 1字节 | 有效数据字节数 |
| 数据 | N字节 | ADC通道值、传感器值等 |
| 校验 | 2字节 | CRC16,对整个数据段做校验 |
实际发送时,我只发送帧头、命令字、长度、数据和校验。接收端收到0xAA 0x55后,开始按帧格式解析,读取长度字段,等长度字段指定的字节数都收齐后,计算CRC并与收到的校验值比对。比对通过就处理数据,比对失败就丢弃这帧重新搜索帧头。
CRC16的实现可以自己写查表法,也可以直接用XMODEM的CRC16算法。我的建议是直接移植一个成熟的查表实现,因为单字节计算法在中断里执行时间太长,有可能影响下个字节的接收。
4.2 HAL库串口空闲中断的用法
传统接收串口数据的方式是一个字节一个字节地触发中断,每个字节进一次中断,CPU开销大不说,处理帧还要自己判断数据什么时候结束。STM32的串口空闲中断可以很好地解决这个问题——它在一串连续数据结束后触发一次中断,意味着你可以在空闲中断里把一整帧数据取走。
在HAL库中,可以使用HAL_UARTEx_ReceiveToIdle_DMA()函数启用空闲中断+DMA接收模式。这个函数会在DMA接收期间检测总线空闲状态,当发现空闲时,调用空闲回调函数HAL_UARTEx_RxEventCallback。回调函数的参数里包含了本次实际接收到的数据长度,因为DMA缓冲区的固定长度往往比分帧实际长度长,而字段中的Size变量就是实际DMA搬运的字节数。
我的配置是把DMA接收缓冲区设为256字节,上位机每50ms发送一次查询请求,每次收到约60个字节的一帧数据。当数据到达后,串口空闲中断立刻触发回调函数,我在回调里解析协议,解析完一帧完整的命令后置一个标志位,主循环检测到这个标志位后再去执行相应的动作。这样架构清晰,中断里只做最快的事,耗时操作全部放到主循环。
4.3 环形缓冲区:让接收和处理解耦
如果你用过直接数组缓存串口数据的方式,一定遇到过这样的情况:数据来的速度比主循环处理速度快,后到的数据把先到的数据覆盖掉了,导致粘包、缺包。环形缓冲区就是解决这个问题的标准方法。
简单说,环形缓冲区就是一个带有读指针和写指针的数组。写指针由串口接收逻辑(中断或DMA)推进,读指针由主循环的数据处理逻辑推进。当写指针追上读指针时说明缓冲区满;当读指针追上写指针时说明缓冲区为空。关键在于,读写指针的推进操作互不干扰,接收方不会因为处理方慢而丢数据,处理方也不会因为接收方快而读出旧数据。
实际实现时要注意判断指针相等:初始化时读写指针都指向0,后续只有完全相等才表示空或满,这个判断用减法比用相等更安全。我见过不少人在这个地方写错,导致缓冲区明明没数据却一直在处理垃圾数据。
5. 实测与排查:ADC跳动、DMA错位、串口丢帧的完整处理过程
5.1 第一个问题:ADC采样值为什么一直在跳
现象:用电位器输入一个稳定的1.5V电压,万用表测量纹丝不动,但串口上报的ADC值却在1800到1950之间大幅波动。
排查链路:
第一步,确认信号源是否真的稳定。我用万用表测量了电位器输出端,确认电压稳定在1.5V,排除了外部信号源的问题。
第二步,检查ADC配置。采样时间从239.5Cycles改到最长,问题依旧,说明不是采样时间不足。
第三步,怀疑参考电压。用示波器测量VDDA引脚,发现上面叠加了大约200mV的噪声纹波。问题找到了,不是ADC本身坏了,而是参考电压不干净。
解决方案:在VDDA与地之间加一个10uF电容和100nF陶瓷电容,同时把模拟部分和数字部分在PCB上做好单点接地。处理后重新测试,ADC值波动范围从±75降至±3。这对一个12位ADC来说,已经是相当不错的结果了。
这个问题的教训是:数据采集系统里,ADC只是一个转换器,它不会平白无故准确,需要硬件电路配合才能发挥性能。出了跳动问题先量参考电压,这是最快的排查路径。
5.2 第二个问题:DMA缓冲区里的数据为什么会错位
现象:两路ADC通道输入不同的电压,理论上缓冲区[0]对应通道0,[1]对应通道1。但实际读取时,发现两个通道的数据偶尔会发生交换,表现为通道0的数据突然变成了通道1的值,然后下一轮又恢复正常。
排查链路:
一开始我怀疑是DMA配置有问题,仔细检查后确认传输宽度、地址长度都对得上ADC通道数。后来想到,可能是ADC的扫描顺序和DMA缓冲区索引之间的关系没搞明白:当ADC完成通道0的转换后,DMA搬运它的结果到adc_buffer[0],然后ADC开始通道1的转换,转换完成后,DMA搬运到adc_buffer[1]。这个流程本身没问题,问题出在该读取缓冲区数据的时刻,ADC可能刚好正在进行下一轮转换。如果我在ADC正在转换通道0的时候去读adc_buffer[0],读到的是上一轮数据;如果这个时刻刚好在DMA搬运的间隙,缓冲区中某些通道的数据就是旧值,看起来就会错位。
解决方案:在主循环里读取数据时,先暂停ADC的DMA传输(HAL_ADC_DMAStop),此时ADC仍在转换但不会有新数据写入缓冲区,读取缓冲区内容后再重新启动DMA。或者更优雅的办法是开启DMA传输完成中断,在每次整轮传输完成时拷贝缓冲区内容到安全区域。这里我推荐后者,因为频繁停启DMA会丢失时间基准。
5.3 第三个问题:串口丢帧与中断优先级设置
现象:串口助手显示的数据帧偶尔会缺少末尾几个字节,或者出现一帧数据被分割成两部分接收的情况。
排查链路:串口波特率115200,每个字节传输时间约86.8微秒,数据帧总共60字节,总耗时约5.2毫秒。我怀疑某个中断处理时间过长,导致串口接收数据期间没有及时取走DMA缓冲区的数据,新来的数据把旧数据覆盖了。
查看代码后发现,我在串口中断回调里做了一件特别耗时的事:把接收到的数据复制到一个较大数组里时,使用了memcpy处理了整段缓冲区,而且这段代码是在中断上下文内执行的。对于60字节的数据量,memcpy本身不算慢,但我在中断里还做了协议解析和命令执行,这就大大超过了串口接收的时间窗口。
修复方案:
- 中断回调函数里只做标志位置位和必要的数据搬移,绝对不做协议解析。
- 把协议解析放到主循环处理,读取到标志位后再去解析数据。
- 检查中断优先级,将串口全局中断优先级设为当前最高优先级(数字最小),保证接收不被其他中断抢占。
加上这几个修改后,串口丢帧问题彻底消失。这里要特别强调:中断服务函数应该越短越好,这条规则几乎所有入门教程都说了,但真正遇到问题的时候才会深刻理解。
6. 项目资料整理与答辩展示:源码之外要掌握的东西
6.1 代码注释与文档:决定项目能打多少分
很多同学的技术水平不差,但项目资料一塌糊涂,代码没有注释,文档没有目录,老师拿到手里都不想翻。我的经验是:评分的差距往往不在功能实现上,而在文档规范和表达逻辑上。
整理项目资料时至少包含这几个文件:
- README.md:概述整个项目,包括系统截图、功能列表和环境依赖。让人一眼看出项目做的是什么。
- 系统框图:用Visio或draw.io画清楚传感器到MCU到上位机的数据流向,附上硬件引脚连接说明。
- 硬件设计文档:原理图、PCB布局图、关键器件选型理由、信号调理电路的计算过程。
- 软件设计文档:模块划分、关键函数说明、数据帧协议定义、流程图。
代码注释方面,每个.c源文件顶部写清楚模块功能、作者、日期、修改记录。每个函数上方用两三行注释说明输入参数、返回值和功能。关键算法处至少写清思路,不用堆砌注释,但核心逻辑必须有解释。
6.2 上位机演示与波形展示
一个能呈现数据的上位机界面,比十页文字说明都管用。最简单的方案是用Processing或Python的pyqtgraph库写一个串口画图程序,实时读取串口数据并显示波形。这样答辩时,评委能看到实时变化的曲线,比空口说“我的采集系统工作正常”有说服力得多。
如果时间和精力不允许,也可以用串口助手把数据记录到CSV文件,再导入Excel画图。虽然不如实时波形炫酷,但至少能展示数据的完整性和准确性。我当时用Python写了一个40行的实时绘图程序,效果非常好,具体代码网上有大量现成例子,稍微改改就能用。
6.3 老师必问的几个问题,务必提前准备
答辩时老师一般不会问过于刁钻的问题,但有几个方向是必问的:
- 你的ADC采样率为什么这么定?——要能从理论依据出发,说清这个采样率能满足奈奎斯特采样定理,且高于信号最高频率的2倍,最好留2-5倍余量。
- 用了DMA有什么好处?——要答出CPU解放、批量搬运、与ADC硬件协同等几个关键点。
- 如果采集到的数据跳变很厉害,有哪些原因?——这个问题我在第5部分已经详细讲过了,能回答出参考电压不稳、信号源阻抗过大、采样时间不足、数字噪声耦合这四点就能拿到高分。
- 为什么选这个传感器?它的输出范围是多少?——要能快速报出传感器的量程、灵敏度、输出范围和供电电压。
这些问题的共同点是:考察你是否真的理解了系统设计过程,而不是只会烧录代码。把本文前面几节的原理吃透,答辩就不是问题。
最后说一个我在整个项目中体会最深的事:数据采集系统看起来只是“读几个ADC值再发给串口”,但真正做下来,硬件、软件、通信协议、调试工具、上位机、文档组织,每个环节都不能掉链子。如果你正卡在某个环节,不要急着怀疑代码,先从硬件供电和信号质量查起,再回到软件逻辑。硬件基础稳了,软件才有意义。
本文还有配套的精品资源,点击获取
