
简介面向STM32嵌入式开发者与单片机初学者这是一套基于STM32驱动3.5寸LCD实时绘制波形图的完整工程资源。资源围绕GPIO引脚控制、LCD初始化时序、ADC模拟信号采集、数据处理与动态刷新等关键环节提供可直接移植的源码实现与工程配置覆盖从底层驱动到上层绘制的完整链路。压缩包内含136个文件以H/C源码和工程配置文件为主同时包含编译链接生成的O、D、CRF中间文件及HEX、AXF固件输出整体大小3.08MB便于对照分析开发和调试过程。波形绘制采用缓冲区逐点或逐行传输并引入DMA减轻CPU负担适合需要落地实时波形显示、学习屏幕驱动或数据可视化方法的开发者。该资源已有4522人学习代码结构清晰、可拓展性强稍作调整即可适配不同尺寸LCD也可在此基础上加入触控或更复杂的图形界面。 前阵子调一块传感器板子输出信号时好时坏我想用示波器看一眼波形结果示波器被隔壁组借走了。手头只剩一块带LCD屏的STM32开发板我当时就想板子上明明有屏为什么不能让它直接把ADC采样到的波形画出来于是花了两个晚上把“STM32 LCD实时绘制波形图”这套方案跑通了中间踩了不少坑有几个问题在教程和例程里几乎没人提。这篇文章就把完整的实现思路和排障过程复盘一遍给想在LCD上做波形显示、又不打算一直插着上位机的朋友一条能直接“抄作业”的路线。这篇内容适合这几类人一是做传感器采集、需要快速验证信号趋势的二是做手持式仪表或离线监测设备的三是学习STM32的ADC、DMA、SPI和简单图形算法想找个综合项目练手的。看完你至少能搞清楚三件事采样链路怎么搭才不会卡死、波形怎么映射到屏幕坐标、以及那些让人想砸电脑的显示问题到底出在哪。1. 先决策波形放在LCD上是刚需但别把它当示波器用1.1 什么场景真正需要LCD波形图我见过不少项目数据采集部分做得挺完善结果调试的时候只能对着串口助手看一堆十六进制数或者用OLED屏刷新几个数字。看数值不是不行但信号是缓变、抖动还是周期性异常靠肉眼看数字效率非常低。LCD实时绘制波形图的核心价值是把“数据到底好不好”变成“一眼就能看出来”的事。最典型的场景有三个手持式检测设备现场没有电脑必须本地直接看波形形状产线快速检验操作员不需要懂协议只看波形包络是否正常就能判断嵌入式系统自检开机后跑一段测试序列把关键节点信号画在屏上方便定位故障。我在这个项目里遇到的也是同样的问题传感器输出的是几十赫兹的缓变信号我需要确认它的包络、有没有异常的尖峰而不是精确计算频率和幅值。这种情况下LCD波形图比任何数值列表都好用得多。1.2 方案边界哪些波形不建议硬塞给LCD在动手之前必须先把预期拉到一个合理水平。LCD波形图不是示波器它有几个硬边界第一是刷新率限制。普通的SPI接口TFT屏整屏刷新也就每秒二十帧左右如果还要做清屏、画网格、显示文字实际有效刷新率会更低。这意味着输入信号的频率不能太高否则你看到的不是波形是一团乱麻。第二是存储空间限制。以STM32F103为例SRAM也就20KB到48KBADC采样缓存不可能开得很大采样点数受限分辨率自然受限。第三是波形特征问题。如果你想抓毛刺、抓瞬态跳变、抓高频干扰LCD方案不但无能为力还会给你错误的信心。展示出来的平滑波形很可能是因为采样率不够而丢失了细节。用我自己的标准来划分10kHz以上的连续信号或者需要ns级分辨率的瞬态信号老老实实上真正的示波器。LCD波形图最适合的是观察“轮廓”和“趋势”的信号比如温湿度变化、电池放电曲线、低频传感器输出、PWM占空比调整结果这些场景。想清楚这个边界后面做方案选型时就不会走弯路。2. 下板子前的硬件规划SPI屏、ADC和DMA怎么搭不返工2.1 屏幕选型为什么SPI接口TFT成了默认答案ST7789、ILI9341这类SPI接口TFT屏几乎成了这类项目的默认选择。原因很直接引脚少、布线简单、驱动资料多、价格便宜。四线SPI加上CS、DC、RST、背光总共七八根信号线接在一个20Pin左右的排针上刚刚好不管是飞线还是画PCB都非常舒服。相比之下并口屏虽然写入速度更快但8位数据线加上控制线就要占用十几个GPIO在引脚资源紧张的板子上很难安排。如果你的需求是刷视频级别的全屏动画并口确实有优势但只是画波形的话SPI的带宽完全够用。我在这个项目中用的是240x320分辨率的ILI9341屏RGB565格式一个像素占16bit。实际测试中SPI时钟跑到18MHz显示一屏波形并刷新网格耗时可控制在十几毫秒级别肉眼看起来就是“实时”的。要提醒一句SPI时钟能不能跑高和你的接线长度以及杜邦线质量关系很大线太长或者太乱高速下很容易出现花屏。2.2 引脚分配里最容易忽视的三个细节很多人在原理图阶段只关心功能能不能通不太在意引脚分配结果后面调试时处处被动。这里说三个我交过学费的细节第一个是DC引脚也就是数据/命令选择脚。SPI本身没有这个信号必须用GPIO控制。我建议把DC引脚单独分配到某个普通GPIO并且把它的输出速度配置为高速。因为每传输一个字节都要先拉高或拉低DC如果GPIO翻转速度太慢会拖低整条SPI链路的吞吐量尤其在高SPI时钟频率下很明显。第二个是背光控制。很多人直接用一个高电平把背光点亮就完事了这样也行但很不灵活。更好的做法是把背光接到支持PWM输出的引脚上通过调节占空比控制亮度。白天强光下可以把亮度拉高夜间调低同时还能省电。如果你的面板对亮度有要求这一步在原理图阶段就要想好别等软件写完了才发现引脚不支持定时器复用。第三个容易翻车的地方是共用SPI总线。如果LCD和其他设备共用同一条SPI务必保证各自的CS片选逻辑正确DC信号不能和CS混在一起。我刚开始图省事把DC脚和另一个外设的CS脚共用了结果屏幕一刷新就干扰那个外设查了半天才发现是片选竞争。2.3 ADC、DMA和时钟数据从哪来、怎么搬、走多快STM32的ADC采集配置有一定套路但参数细节非常影响效果。首先ADC时钟不能直接拿系统时钟用必须经过分频保证ADC时钟在允许范围内。以F103为例ADC时钟最高约14MHz我配置在12MHz左右单次12位转换大概需要14个ADC时钟周期换算下来差不多1us完成一次转换这个速度对低频信号绰绰有余。其次启用扫描模式还是单通道模式如果只是采集一个通道的波形最简单的做法是配置单通道连续转换模式然后靠DMA把结果持续搬进内存。DMA选择循环模式传输方向设置为外设到内存外设地址固定为ADC数据寄存器地址内存地址指向一个缓冲区数组。这样配置之后ADC会自动不停地转DMA会自动不停地把结果搬到数组里CPU完全不用管采样这件事只负责在需要的时候取数据来画图。外部晶振这一块也顺便说一下。F103通常用8MHz晶振它的负载电容选择是有讲究的晶振规格书会给出一个负载电容CL实际配置的两个谐振电容C1、C2需要满足CL约等于C1与C2串联后再加上引脚寄生电容。经验值上CL为18pF到20pF左右的8MHz晶振配18到22pF的谐振电容通常能正常起振。如果你发现系统时钟不稳定、串口乱码、外设行为诡异先查晶振电容和起振波形别一上来就怀疑程序。3. 采样链路的核心环形缓冲让ADC不拖累显示3.1 为什么裸奔轮询ADC容易翻车新手写波形显示最容易犯的错误就是在主循环里直接读ADC启动转换、等转换完成、读结果、存数组、然后画屏。这个过程看起来顺理成章但在高采样率下会有一个严重问题——CPU被迫在ADC转换期间一直空等什么也不能干。打个比方这就像你一边数入场人数一边还要负责维护现场秩序每来一个人你都要停下来等他登记完才继续干活。如果人流小问题不大一旦人流上来办事效率立刻崩盘。显示波形时如果采样加绘制全塞在主循环里屏幕刷新就会一卡一卡波形跟着一顿一顿根本谈不上“实时”。更合理的做法是“分工”ADC负责采集数据DMA负责搬运数据CPU只负责把数据画到屏幕上。三者并行工作采样过程不消耗CPUCPU可以专心处理画图逻辑。这也是为什么这一节标题说“环形缓冲才是这套系统的心脏”——没有它后面画图写得再漂亮也白搭。3.2 环形缓冲与DMA中断配合的完整思路要实现“边采集边显示”最实用的方式是DMA缓冲区加中断标志配合。我使用的方案是设置一个512字节即256个16位ADC样本的DMA缓冲区启用DMA的半传输完成中断和全传输完成中断。DMA在传输完前半段时触发一次中断CPU去处理前半段256个样本与此同时DMA继续向后半段写入新数据当后半段写完触发第二次中断时CPU又能处理后半段数据。如此循环就相当于一个“双段缓冲”CPU和DMA交替工作互不干扰。关键代码逻辑可以覆盖为这样#define BUF_SIZE 512 uint16_t adc_buf[BUF_SIZE]; volatile uint8_t buf_half 0; void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_HT1)) { // 半传输完成 DMA_ClearITPendingBit(DMA1_IT_HT1); buf_half 0; // 前半段就绪 } if (DMA_GetITStatus(DMA1_IT_TC1)) { // 全传输完成 DMA_ClearITPendingBit(DMA1_IT_TC1); buf_half 1; // 后半段就绪 } }主循环里判断buf_half的值就知道该处理哪个半区。需要注意的是主循环处理数据和DMA写入数据之间存在竞争关系可能出现主循环还在处理前半段的最后一两个点DMA已经开始往那个区域写新数据了。工程上通常容忍这种轻微竞态因为ADC是连续数据偶尔丢一两个点不影响波形显示。但如果你对数据完整性要求极高就得用调整指针的方式精确切分或者把缓冲区做成真正的环形队列用读写索引来管理。对于大多数显示场景上面这种“半区切换”已经足够了。它比真正的环形队列省内存逻辑也直观很多。3.3 采样率、缓存深度与波形周期的关系采样率不是越高越好过高会让CPU在中断里疲于搬运数据过低则波形会严重失真。这里有一个比较实用的计算思路。先算采样率ADC单次转换时间约14个ADC时钟周期在12MHz ADC时钟下理论采样率约为857kHz。但实际上DMA搬运、系统中断、绘制任务都会消耗资源工程上把这个ADC用在几百kHz以内是稳妥的。对于LCD波形显示我们根本不需要这么高的采样率。再看显示需求。屏幕一行有240个像素也就是说一条完整的居中波形至少需要240个采样点。假设屏幕每秒刷新20帧那就需要240乘以20等于4800个点每秒这个采样率跟ADC的能力相比微不足道。真正限制我们的是如果你用4096点缓存去存储一段波形那这一段波形能覆盖的时间就是4096除以采样率。采样率越高单屏显示的时间窗越短看到的波形细节越多但整体趋势越少采样率越低时间窗越长但细节丢失。在调试时我的经验是先把采样率设得较低比如5kHz看整体包络确认信号大致形状后再逐步提高采样率去看细节。另外在显示前加一道简单的一阶低通或滑动平均能有效减少LCD上看到的高频毛刺对判断信号趋势很有帮助。这一点在参数调试图里尤为好用——你不会希望屏幕上全是噪声峰而把真实的信号轮廓淹没了。4. 画波形的算法细节坐标换算、自动缩放与不闪屏刷新4.1 从ADC原始值到屏幕坐标的换算ADC采回来的数据是数值屏幕显示需要坐标。这个换算看似简单但有两个容易出错的地方。第一个是Y轴方向。大多数人习惯数学坐标系里Y轴向上但LCD屏幕的Y坐标原点在左上角向下递增。也就是说电压越高显示时Y值应该越小否则波形是上下颠倒的。转换公式可以这样写int16_t y LCD_HEIGHT - (int16_t)((long)(value - vmin) * LCD_HEIGHT / (vmax - vmin));这里vmin和vmax是你期望显示的电压范围如果你的信号在0到3.3V之间波动可以设置vmin0vmax409512位ADC满量程。这样value是0时y等于LCD_HEIGHT屏幕底部value是4095时y等于0屏幕顶部符合直觉电压高在上面。第二个容易忽略的是溢出问题。公式里有乘法value减去vmin后乘以LCD_HEIGHT如果直接用uint16_t做乘法很容易在临界处溢出得到的结果会莫名其妙地跳到屏幕另一头。我习惯把所有中间量提升到int32或者long再计算稳妥很多。X坐标的处理相对简单一般用采样点序号作为X然后等比例映射到屏幕宽度。比如缓存里有240个点那就正好一个点对应一个像素极其直观。如果点数超过屏幕宽度可以抽点显示如果点数不足可以插值或只显示原有位置。4.2 垂直方向的自动缩放与抗噪固定Y轴范围写起来简单但实际使用时非常别扭。信号幅值只有几百mV时你在0到3.3V的满量程里看它几乎就是屏幕中间一条不怎么动的直线。用上位机软件看波形时很多人习惯手动拖Y轴但是LCD上没鼠标所以必须做自动缩放。我的实现方案是每次从缓存里找出这一段数据的最大值和最小值然后根据这两个值动态计算vmin和vmax再留出10%左右的边距让波形不会贴在屏幕边缘。这样一来不管信号是大幅摆动还是微小波动屏幕上都能看到明显起伏。自动缩放有一个必须处理好的坑异常毛刺。如果原始数据里有一个特别离谱的尖峰最大值会被拉得极高其他正常波形被压成一条扁线画面反而没法看。解决思路是在求最大值和最小值之前先对数据做一次简单的去极值处理或者使用百分位截断——把排序后的1%最大值和1%最小值去掉再映射Y轴。这个方法在实测中效果好得多。还有一种折中方案固定基础量程然后用自动缩放只调整一个微调系数让波形在量程内保持可见但不过度缩放。这对大多数传感器调试场景够用而且实现简单、不容易出bug。4.3 滚动刷新与局部更新让画面不闪波形显示如果采用“整屏清屏再重画”的办法你会看到屏幕闪烁非常严重尤其SPI屏写全屏整体较慢。我记得第一次跑通波形时整个屏幕像老式电影一样狂闪眼睛没看几分钟就受不了。不闪屏的做法是局部更新。具体来说上一帧每个X坐标有一个Y值这一帧也有一个Y值对比两者差异只在变化的位置重画先清掉旧的像素点再画新的像素点。因为波形在时间轴上连续相邻两帧之间的差异其实很小局部更新的数据量比全屏刷新少一个数量级。如果内存允许可以开辟一块与屏幕波形区域大小一致的显存缓冲区先把要显示的内容全部画进缓冲区然后用一次DMA把整块缓冲区推送到LCD。这种“离屏渲染”的方式能彻底解决闪烁因为屏幕内容不会分两次显示。F103的SRAM有限全屏240x320的RGB565缓冲区要150KB放不下但只给波形区域开一个240x200的缓冲区也差不多96KB依然偏大。所以实际项目里更多用局部重画只在文字或网格等静态元素上用“脏矩形”策略做局部刷新。实测下来配合适当的延时屏幕上波形平滑滚动肉眼看不出闪烁。5. 踩坑实录五个差点劝退项目的问题和对应解法5.1 波形全是毛刺先别怀疑代码去查电源和地我第一版程序跑起来后屏幕上全是密密麻麻的毛刺完全看不出信号轮廓。那时候我第一反应是采样代码写错了反复查了很久的ADC配置结果毫无进展。直到我用万用表量了一下板子供电才发现电源纹波大得离谱传感器信号在这种电源下早就被污染了。这类问题在嵌入式开发里太常见了模拟信号对电源噪声非常敏感而数字电路的开关噪声又会通过地线耦合进ADC。基本对策有几个模拟地和数字地在PCB上单点连接不要在传感器信号线上走长线ADC的参考电压如果可配置的话尽量用独立的基准源。对于飞线调试的场景至少要做到传感器地和板子地短而粗地连接电源端加足够的去耦电容。我后来养成了一个习惯遇到波形毛刺先做两个实验。一是把ADC输入直接接到地的参考电压上如果采集出来的数据依然有大量跳变那问题不在外部信号而在电源或ADC配置。二是把传感器断开短接输入波形理论上应该是平的。哪个环节不平就从哪个环节往下查效率比东猜西猜高很多。5.2 波形断断续续DMA中断标志的细节另一个让我头疼的问题是波形显示不是连续的一条线而是一段一段地跳。这个问题卡了我大半个晚上现象非常诡异波形大部分时间正常但每隔一段时间会有一个明显的断口像被什么东西切了一刀。排查后确认问题出在DMA中断处理上。半传输完成标志和全传输完成标志在中断服务函数里清除后CPU还没来得及处理某个半区的数据DMA已经再次写入了该半区导致数据被覆盖。虽然理论上这种竞态很难出现但在SPI刷新屏幕占用了较长CPU时间的情况下确实会触发。解决办法有几条路一是缩短DMA半区操作的时间比如把画屏操作拆分成更小的批次避免长时间关中断二是增大缓冲区让CPU有更多时间处理数据三是为每个半区增加一个“已处理”标志主循环只在标志有效时才去读取对应半区的数据。如果用的是带Cache的M系列内核比如某些M7芯片还要在读取DMA写入的数据前做Cache一致性失效操作否则会读到陈旧数据这个细节在标准库里不会教但踩一次就记住了。5.3 调试器连不上从接线到调试认证的排查做这个项目过程中我还遇到过一个更“劝退”的问题程序写完后下一板重新上电ST-Link突然报错“No STM32 Target Found!”一度以为芯片坏了。排查顺序很重要不要一上来就怀疑芯片。先检查3.3V供电是否正常再测量RESET引脚电平是否被外部电容异常拉低然后检查SWDIO和SWCLK的接线是否正确。很多时候所谓“连不上”其实就是杜邦线松了或者供电不够导致芯片没有正常工作。还有一种情况是芯片内部开启了调试认证保护。较新的STM32系列支持Debug Authentication功能如果代码里意外使能了调试保护调试器就无法正常连接到内核。遇到这种情况普通ST-Link会一直报错解决办法只能是通过专用方式解除保护或使用带相应授权的调试工具。从操作角度讲我建议在项目初期就把调试接口的硬件复位和接线检查步骤固化下来不要等烧完代码才发现连不上——那种感觉真的很绝望。5.4 中文和字符显示乱码取模与编码双双把关画波形通常要在屏上显示“波形”、“电压”、“时间”这类中文标签这一步也坑了不少人。最常见的问题有两种一是取模方向不对字模数据是按行扫描还是按列扫描没搞清楚显示出来的汉字就是镜像的或者东倒西歪二是编码不对代码文件保存的是GBK字模而编译器按UTF-8解析字符串导致内存里查表索引对不上显示出乱码。我的建议是把字模取模方式固定为“行列式扫描高位在前”并在取模软件里统一导出。显示函数按字节顺序一行一行地推送到LCD不要在中途改变扫描方向。对于编码问题最简单的方法是把字模索引和字符串编码全部统一成GB2312或者UTF-8中的一种并且在工程配置里固定下来。不要用“看起来差不多”的方式拼字符串容易在改版时不知不觉引入混编码。调试时有一个技巧很有用单独写一个测试函数一个字符一个字符地打印逐步缩小乱码范围能快速定位是取模方向错还是索引错。这个技巧帮我节省了大量时间。5.5 画面出现残影LCD极化现象的规避如果你长时间让LCD显示同一个画面尤其是那些基本不动的静态文字和网格线时间长了会发现屏幕上有隐隐的残影像“印”上去了一样。这就是所谓的LCD极化现象液晶分子长时间处于同一偏置状态导致响应变慢换画面时残影需要很久才能消退。规避方法有两个思路。一个是定期反转驱动极性比如每隔一段时间把某一小块区域的数据整体取反再取反回来让液晶分子经历一次完整的翻转过程。另一个思路是避免长时间静止画面对波形图来说因为波形本身在不断变化极化现象还好但屏幕上的标题、时间戳这些元素容易成为“重灾区”。所以在设计显示界面时尽量让静止元素和动态区域分开并对标题栏这些区域做周期性的刷新或小范围滚动。网上还有一种说法是“用波浪线滚屏来防残影”实际对波形显示来说只要波形在移动本身就能起到防止极化的作用。真正要做的是设置一个软件看门狗如果设备长时间处于静止状态就定期自动刷新整个屏幕或者做一次反色闪烁预防残影产生。最后分享一点个人体会这套“STM32LCD波形图”做完之后我最大的感觉是它把嵌入式系统里几块核心能力全都串了起来——ADC采样、DMA搬运、中断管理、SPI通信、图形坐标算法、显示刷新策略。相比单独点个灯、读个传感器这个过程能学到的东西多得多。如果让我给正在做类似项目的人一个最实用的建议那就是调显示的时候先在代码里生成一个已知的标准正弦波用于测试用这个虚拟信号去验证显示链路不要一上来就接真实传感器。等屏幕上能画出干净平滑的正弦波再接入真实信号做缩放和抗噪处理两者分开排查会轻松非常多。后续如果还想继续扩展可以在这个骨架上加按键切换通道、触控十字光标、波形峰值自动打点等功能甚至可以引入FFT做频谱显示。基础架构稳定了往上加东西都是水到渠成的事。本文还有配套的精品资源点击获取