基于STM32的电压电流采集与LCD波形显示系统设计
简介这套基于STM32的电压电流采集与LCD波形显示系统源自2014年全国大学生电子设计竞赛D题面向电子工程与嵌入式开发初学者、课程设计与毕业设计人群提供从模拟信号采样、参数测量到时域波形显示、频域谱线分析的一体化参考方案。压缩包共包含308个文件以C语言与头文件.c/.h、汇编启动文件、Keil工程文件.uvprojx/.uvoptx为主辅以备份文件、hex烧录固件、Python脚本及Keil辅助批处理整体大小仅1.01MB结构紧凑便于下载与移植。目前已有73人学习使用适合需要借鉴电赛思路或快速搭建STM32测量显示系统的开发者。资源内含完整的硬件架构设计与数据采集流程工程代码覆盖定时器、LCD驱动、Flash存储以及ADC采集等关键模块并涉及快速傅里叶算法与数字陷波滤波器的实现可帮助读者深入理解信号处理在MCU上的落地方法。同时附带Keil工程配置文件与清除脚本导入后即可对照实物调试。1. 这个系统解决了什么问题整体方案怎么拆做电源调试的时候最烦的就是手边没有个能看到连续波形的设备。万用表只能看到一个跳动的数字示波器倒是不错但也不是每人桌面都放得下一台。两年前我做一个电池充放电的项目需要同时盯住电压和电流的变化趋势判断有没有异常的毛刺或者塌陷。当时手里正好有块闲置的STM32F103最小系统板加上一块SPI接口的TFT LCD屏于是就有了这个基于STM32的电压电流采集与LCD波形显示系统。先说清楚它能干什么通过单片机自带的ADC模块采集外部电压和电流信号经过前端分压、跟随和放大电路把信号调理到适合ADC采样的范围内再用定时器触发采样配合DMA自动把结果搬进内存最后在LCD上把数据画成动态波形。整个过程不需要外接独立的ADC芯片也不需要昂贵的显示屏成本控制在几十块钱以内。它适合三类人来参考一类是正在做电源、电子负载、电池检测相关项目的嵌入式工程师一类是拿STM32做课设、毕设的学生——这个题目能覆盖ADC、DMA、定时器、LCD显示和数据处理好几块知识点答辩时很好讲还有一类就是纯粹想搞个桌面小示波器玩玩的电子爱好者。整个系统的信息流是这样的外部信号先进调理电路变成0到3.3V以内的电压送给STM32的ADC引脚ADC按照固定频率采样采样结果通过DMA通道自动写入内存数组CPU并不需要逐点参与。完成一段数据的采集后CPU把数组做一次软件滤波和标度换算还原成真实的电压值然后驱动LCD把这段数据绘制成波形。采样和显示在时间上是交替进行的只要配合得当既能保证波形连续又不至于拖慢刷新速度。后面我把软硬件的每个环节展开细讲包括一些常规教程里不会提到的坑。2. 硬件电路的关键参数怎么算整个系统里最容易被忽略的其实不是STM32本身——它的ADC外设已经在芯片内部了只要你给它正确的时钟、触发源和采样时间就行。真正的难点在前端信号调理电路这里决定你能测多高的电压、多宽的电流范围以及测量精度能达到什么水平。2.1 电压采集这一路的分压设计STM32的ADC输入范围是0到3.3V部分型号支持到3.6V但一般按3.3V设计超过这个范围轻则读数饱和重则直接烧掉引脚。所以测电压的第一步就是分压。我常用的方案是电阻分压加电压跟随器。分压部分选用高精密电阻比如0.1%精度的金属膜电阻上臂100K下臂10K这样分压比是11:1也就是说输入端最大可以承受3.3乘以11约36V的电压覆盖常见的24V电源调试绰绰有余。如果你觉得自己可能会测到更高的电压可以把上臂换成200K量程翻倍但代价是输入阻抗变高后级跟随器的输入偏置电流会造成额外的失调误差。选电阻阻值的时候有个经验值串联总阻值尽量在100K到1M之间。阻值太小待测电路会被你这路测量影响尤其测的是高内阻信号源时分压效应会明显拉低真实电压阻值太大热噪声和跟随器偏置电流引起的误差又会上升。分压之后的信号不能直接进单片机。因为分压网络的输出阻抗在10K左右而STM32的内部采样电容只有几皮法建立时间有限。要是直接接采样结果会受信号源内阻影响尤其在快速连续采样时读数会偏低。正确做法是分压输出后面加一级运放电压跟随器常见选型是LM358、TL072或者SGM8531。LM358便宜量又足缺点是带宽低但对几十KHz以内的信号完全够用TL072噪声低适合做微弱信号。运放输出阻抗几乎为零驱动ADC采样电容绰绰有余。2.2 电流采集这一路采样电阻加运放放大电流测量比电压多一点门道。主流方案是在电流回路上串联一个阻值很小的采样电阻测它两端的压降再用欧姆定律算出电流。采样电阻的选型有三条约束阻值要小阻值太小压降难以检测阻值精确度和温漂要好最大功耗要留足够余量。我常用10毫欧、1%精度、2W封装的锰铜电阻。假设工作电流2A那么采样电压是20mV这个数值直接在3.3V的12位ADC上看分辨率大约只有0.8mV/LSB换算成电流就是80mA/LSB太粗糙了。所以要加一级运放把20mV放大50倍变成1V送进ADC。这时候分辨率变成约1.6mV/LSB对应电流约1.6mA / LSB基本够用。运放放大电路用的是同相放大结构放大倍数由两个电阻决定增益等于1 Rf / Rg。如果Rf取49K、Rg取1K放大倍数就是50倍。这里注意运放的输出电压要低于其供电电压减去压降轨到轨运放稍微好一些所以如果放大后的信号要接近3.3V运放供电建议直接用3.3V或者5V正输入端的共模范围也要检查。还有一个细节采样电阻放在负载的低端还是高端会直接影响结果。低端采样实现简单但负载的底线不再等于系统地线对于一些同时需要接地测量的仪器来说会有干扰高端采样共模电压高对运放的要求也更高。我建议初学从低端采样开始做。2.3 LCD模块的选型与接线实践LCD屏幕这块如果你从头开始做优先选SPI接口的TFT屏比如常见的1.3寸、1.8寸、2.4寸分辨率从240×240到320×240都有。SPI屏的好处是占用引脚少只需要CS、SCLK、MOSI、DC、RST和背光六根线软件模拟时序或者用硬件SPI外设都很方便。并口屏比如正点原子常用的FSMC接法写入速度快但要占用十几个IO而且MCU必须支持FSMC或者你用IO模拟时序工程量大很多。对于波形显示这种刷新频率不算极端苛刻的应用SPI屏用硬件SPI跑个36MHz到54MHz时钟完全够用了。接线方面注意SPI屏的MISO引脚大部分是不用的所以屏幕上标注的SDA脚就是主机MOSI输出。有些模块的背光控制是共阳还是共阴手册上通常有标注。我遇到过一次背光控制极性搞反结果一上电屏幕白屏好半天才排查出来。另外SPI屏的片选脚CS要单独控制不能图省事直接拉低不然多设备挂同一条SPI总线时会冲突。初始化时序尤其要注意很多SPI屏的初始化命令序列比较长不同驱动ICST7735、ILI9341、ST7789之间不通用直接套用别的型号的驱动代码常常会在点亮之后出现颜色错乱或者花屏。建议先跑通厂商提供的例程确认显示正常后再裁剪代码不要自己从头去啃数据手册的初始化序列。3. 软件实现从ADC采样到LCD波形绘制的全流程这部分是系统的灵魂。硬件链路搭好了软件要负责把信号稳定、流畅地“搬”到LCD上。我采用的技术路径是定时器触发ADC → DMA搬运 → 主循环显示。这条路径最大的好处是把CPU从逐点采样的繁重劳动里解放出来。3.1 定时器触发ADC采样与DMA搬运的配合先解释为什么不用普通的CPU轮询读取ADC。假设你要以1KHz的采样率采集用轮询的话CPU每1ms就要进一次中断去启动转换、等转换结束、读数据频繁打断其他工作。如果采样率再往上提到10KHz甚至更高CPU基本什么都别干了。所以正确做法是用硬件来批量搬运数据。配置思路如下选择一个定时器我喜欢用TIM2利用它的更新事件作为ADC的触发源。TIM2的预分频和自动重载值决定了触发频率。比如系统时钟72MHz预分频设71那么计数时钟就是1MHz再设置自动重载值为999溢出周期就是1ms也就是1KHz。ADC工作在扫描模式这里只用一个通道所以扫描模式影响不大外部触发源选择TIM2的TRGO事件。每次触发到来时ADC自动启动一次采样和转换转换完成后如果使能了DMA请求DMA控制器就会把结果搬运到内存数组里。DMA的配置有几个关键点数据宽度设为半字16位因为ADC的结果是12位方向设为外设到内存内存地址自增外设地址固定传输长度设为采样点数目——比如一次显示波形要320个点那就传320个。传输模式用循环模式这样DMA搬完320个数之后会自动重新从头开始源源不断地把最新数据填进来。我实测在36MHz的APB2时钟下跑满ADC的1Msps采样率配合DMA循环搬运CPU只需要偶尔来处理一下数据负载非常低。代码上用标准库或者HAL库其实都行。下面是我在标准库环境下的核心配置片段// ADC 和 DMA 初始化片段 ADC_InitTypeDef adcInit; DMA_InitTypeDef dmaInit; // 配置 DMA 通道1数据从 ADC1 DR 寄存器搬运到 buffer DMA_DeInit(DMA1_Channel1); dmaInit.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; dmaInit.DMA_MemoryBaseAddr (uint32_t)adc_buffer; dmaInit.DMA_DIR DMA_DIR_PeripheralSRC; dmaInit.DMA_BufferSize SAMPLE_NUM; dmaInit.DMA_PeripheralInc DMA_PeripheralInc_Disable; dmaInit.DMA_MemoryInc DMA_MemoryInc_Enable; dmaInit.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dmaInit.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dmaInit.DMA_Mode DMA_Mode_Circular; dmaInit.DMA_Priority DMA_Priority_High; dmaInit.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, dmaInit); DMA_Cmd(DMA1_Channel1, ENABLE); // 启用 ADC 的 DMA 请求 ADC_DMACmd(ADC1, ENABLE);配置完成后你不写一行采集代码adc_buffer里的数据也在一直自动更新。这个模式的好处在于它的实时性和确定性都是由硬件保证的不会因为主程序里跑了一个耗时函数就导致采样断档。3.2 数据的软件滤波与标度换算DMA搬回来的原始ADC值是0~4095之间的整数它本身不能直接显示成“12.34V”必须经过两步处理滤波去毛刺然后做标度换算。滤波方面我对每个通道做了移动平均和滑动限幅两层保护。移动平均窗口选取16点——为什么是16因为它正好是2的幂次用位运算右移4位代替除法能省下不少CPU时间。在STM32F103这类Cortex-M3内核上整数除法在特定编译器版本下并不算非常贵但高频调用量大了积少成多位运算总是更保险。滑动限幅的逻辑是当前采样值与上一个有效值之间的差值如果超过一个预设阈值就认为是尖峰毛刺直接丢弃当前值。这个预设阈值的选取取决于你信号的最大上升斜率比如在测PWM驱动信号时电压跳变就在几微秒内如果限幅阈值设太小反而会把真实信号当成毛刺滤掉。标度换算看起来简单实际上有个容易犯迷糊的点不要在主循环里频繁做浮点除法。ADC原始值是整数你要把它映射到实际的电压值公式是实际电压 原始值 × (参考电压 / 4096) × 分压比。一个更好的做法是把分压比和参考电压合并成一个浮点系数每次采样完成后用这个系数做一次乘法然后缓存到浮点变量里。考虑到STM32没有硬件浮点单元F103系列浮点运算要调用软件浮点库花费的周期比整数运算多很多。如果你对速度敏感可以改用定点运算——把系数放大1000倍变成整数计算完再除1000。我实测下来在波形刷新这种场景下直接上浮点乘法的开销完全可以接受不需要刻意优化成定点。3.3 波形显示算法与LCD刷屏策略LCD上画波形本质上是一个把ADC采样值转换成屏幕坐标并绘制折线的问题。假设屏幕分辨率是240×240波形区域可以定义在y30像素到y230像素之间留出顶部区域显示数值底部区域显示时间轴刻度。坐标映射公式是屏幕Y坐标 基准线 实际电压 × 每伏像素数。基准线对应0V往上为正往下为负。这里的“每伏像素数”要结合屏幕高度和满量程电压来确定。满量程36V屏幕可用高度200像素那么每伏约5.5像素分辨率低一点。如果切换量程比如加继电器控制分压电阻这个值也要对应改变。画线时使用LCD驱动库里的画线函数比如LCD_DrawLine从上一个点画到当前点连起来就是波形。但我劝你不要用那种画一笔就刷一屏的写法——速度慢得没法看。标准做法是准备好一帧波形的点坐标数组先集中计算所有点的Y坐标然后调用画线函数逐段画完。画完之后不要立刻清屏而是用背景色把上一帧的波形完整画一遍相当于擦除再画新一帧这样闪烁感会小很多。整个刷屏动作在SPI屏上大概需要几毫秒到十几毫秒取决于分辨率、SPI时钟和刷屏算法。我这里贴一个简化版的波形绘制代码展示坐标转换与刷屏的核心思想void Waveform_Display(void) { uint16_t i; uint16_t x, y, y_prev; uint16_t raw; // 先用背景色擦除上一帧波形 LCD_SetColor(BG_COLOR); for (i 1; i SAMPLE_NUM; i) { x START_X i; // 为简化一像素对应一个采样点 y WAVE_BASE_Y - (uint16_t)((float)prev_buffer[i] * SCALE_Y); y_prev WAVE_BASE_Y - (uint16_t)((float)prev_buffer[i - 1] * SCALE_Y); LCD_DrawLine(x - 1, y_prev, x, y, BG_COLOR); } // 绘制当前帧 LCD_SetColor(WAVE_COLOR); for (i 1; i SAMPLE_NUM; i) { raw adc_buffer[i]; x START_X i; y WAVE_BASE_Y - (uint16_t)((float)raw * SCALE_Y); y_prev WAVE_BASE_Y - (uint16_t)((float)prev_buffer[i - 1] * SCALE_Y); LCD_DrawLine(x - 1, y_prev, x, y, WAVE_COLOR); } // 更新上一帧缓冲 memcpy(prev_buffer, adc_buffer, sizeof(prev_buffer)); }这里SCALE_Y就是把原始ADC值和屏幕高度关联起来的缩放系数实际计算时等于“波形区域高度像素”除以“满量程对应的ADC码值”。坐标算出来之后我还会加一个越界钳制防止波形跑出显示区域比如电压瞬间过冲时屏幕不会花掉。绘制完成后还可以顺手在顶部区域更新电压和电流的数字显示用LCD_ShowString或者字库函数打印。4. 从调试现场总结的典型问题和排查技巧做这个系统时我踩了不少坑下面挑几个印象最深的写出来它们也是后台留言里提问频率最高的问题。4.1 采样值跳动很大先查地线和电源纹波如果你发现LCD上显示的波形毛刺非常多比如明明是一个平稳的直流电压画出来的线却像锯齿一样那多半不是算法问题而是硬件布线或者供电问题。检查顺序是这样先用万用表量一下STM32的VDDA引脚看看是不是稳定在3.3V。很多最小系统板的模拟供电和数字供电是共用一颗LDO的当板子上的继电器、屏幕背光一动作电源就会瞬间跌落ADC的结果就会有周期性波动。我当时的解决方案是在VDDA引脚处增加一颗10uF钽电容加一颗0.1uF陶瓷电容并联滤波并且让ADC的参考电压脚VREF如果独立引出的话也要加同样的滤波电容。另一个常见因素是地线处理。采样电阻和分压电阻的地线如果和LCD、电机等大电流器件共用回路地线上的压降会叠加到采样信号上。解决办法是采用单点接地所有模拟地线先集中到一个点再从那个点单独接到电源地。对洞洞板焊接的项目来说就是把采样电阻、分压电阻的下端都焊到同一个焊盘上。4.2 波形刷新卡顿问题往往出在刷屏逻辑波形刷新卡顿的第一大元凶是使用OLED或者SPI屏时没有开启缓冲、边算边画边清屏。像上面说的用“擦旧画新”的方式比“全屏清空重画”要快很多。第二个元凶是SPI通信速率太低了。有些屏模块板上自带的稳压芯片功耗有限如果不额外供电SPI时钟跑快了会花屏于是很多人在例程里把SPI时钟降到几个MHz但这个问题其实可以通过单独给屏幕的背光和逻辑供电解决。实测下来STM32F103的硬件SPI外设跑到18MHz甚至36MHz时屏幕的响应速度很理想波形刷新率也能从十来帧提升到三四十帧。还要注意一点不要在主循环里连续调用大块的延时函数比如常说的delay_ms(50)否则波形会一顿一顿的。如果确实要控制刷新率用一个非阻塞的计数变量来计时每次循环判断时间是否到达再刷新。4.3 电压档位不准需要做两点校准ADC本身是12位的理论分辨率是满量程除以4096但真实的参考电压VREF未必恰好是3.3V。很多板子用AMS1117-3.3V输出的实际电压可能在3.28V到3.32V之间如果你直接按3.3V去计算最终显示出的电压值会差出百分之零点几到百分之一。校准分两步。第一步是高精度参考校准用一台四位半万用表测量板子上的VREF实际电压把这个值写进代码里的参考电压变量。第二步是增益校准在输入端加一个已知的标准电压比如用精密可调电源输出10V记录显示的测量值然后计算修正系数。把修正系数保存在外部EEPROM或者内部Flash里下次上电自动读取这样换板子、换电阻之后也能保证精度。我这边用电阻分压加软件修正后的误差实测可以控制在0.5%以内对大部分测量应用足够了。4.4 调试时容易踩的经典误区几个典型误区一是JTAG引脚被占用导致无法下载程序。STM32的PA13、PA14、PA15和PB3、PB4这些引脚默认复用为SWD/JTAG功能如果你在这些引脚上接了其他外设第一次能烧录程序之后再次下载时就可能报“no target found”或者“target connection failed”。解决办法是在代码初始化时把这些引脚的复用功能重新配置为GPIO并使用SWD模式只用PA13、PA14两根线连接调试器。如果你完全用不到下载调试也可以直接禁掉JTAG复用。二是晶振电路布局。晶振和两个负载电容的距离越短越好不要把它们放得太远否则起振频率会偏移基础就是那个经典的计算公式CL (C1 * C2) / (C1 C2) CstrayCstray一般取3到5pF别忽略杂散电容。三是虚拟串口安装不上表现为设备管理器里出现黄色感叹号这个问题大多不是硬件坏了而是驱动版本和系统不匹配试着换一个版本的串口驱动或者改插一个USB端口重新识别一遍。5. 这个系统后续可以怎么扩展基础的电压电流采集与LCD波形显示系统完成后扩展空间非常大。如果你手头正好有USART串口可以把采样数据通过串口发送到上位机做成一个低成本数据记录仪。搭配蓝牙模块或者ESP8266还能改成无线传输电池放电曲线在手机上看。我认识的做电源的工程师里有人用类似的方案做简易电子负载的上位机监控界面成本比买成品低了不是一点半点。如果觉得SPI屏刷新频率不够高可以考虑换用内置ILI9488等驱动IC的大尺寸并口屏并配合FSMC总线使用刷新速度能提升数倍。对采样要求更高的话可以把外部触发定时器的频率提高比如用TIM2触发ADC连续采样16K、32K个点给波形加入时间轴缩放功能让它变成一个小型的简易示波器看几十千赫兹以下的中低频信号毫无压力。对电流测量来说更换更高精度的运放和采样电阻加一点滤波电容也能把纹波“洗”得很干净。从算法层面讲如果你处理的不是简单的直流或者低频信号而是想进一步计算信号的频率、有效值、功率后续完全可以基于当前采集到的波形数据加上FFT分析。STM32F103的型号里带DSP指令的型号不多但做点数不大的FFT还是可行的。做FFT的时候要注意采样频率与分析频率范围的匹配采样定理是最基本的前提不是所有信号都能一起处理。这些扩展方向其实都依赖于这个系统打下的基础一套稳定的采样链路、一个高效的DMA搬运机制、一个流畅的显示刷新框架。把这几个基础模块做好后面加什么功能都只是往框架里填东西的事。最后分享一点个人体会这种从零搭起来的小系统最大的价值不在于最后的成品有多漂亮而在于中间调试的那几天你会把芯片手册、数据手册、原理图前前后后翻好几遍很多平时写业务代码时碰不到的底层细节都是在这样的项目里真正弄明白的。如果你也是一个人闷头调建议每次改动硬件或者软件之前在纸上记一笔“做了什么、现象是什么、怀疑哪一步”排查问题时效率会高非常多。本文还有配套的精品资源点击获取
