STM32F103实战:ADC采集+LCD显示+串口上报完整教程
2026/9/9 2:40:10 网站建设 项目流程

简介:STM32F103 ADC采集电压数据并显示于LCD、同时通过串口发送的完整工程,面向嵌入式入门者与STM32开发者。资源以Keil MDK工程形式组织,压缩包共88个文件、大小仅241KB,包含38个C源文件和37个头文件,涵盖外设驱动、主程序、启动文件、hex烧录文件、工程配置及说明文档,结构清晰,便于直接查阅和修改。工程围绕STM32F103ZET6的12位ADC展开,演示多通道电压采样流程,LCD驱动显示实时结果,UART负责将数据发送至电脑或其他设备,代码按HARDWARE、SYSTEM等模块划分,可快速定位液晶屏驱动、ADC初始化、串口发送等关键代码,并可直接移植到其他F1系列板卡。已有12064人学习下载,适合想理解ADC转换、串口通信协议和液晶屏驱动配合使用的读者,拿到手后可根据实际引脚和需求调整采样通道、显示格式或波特率,缩短开发调试时间。 我把这个项目完整地做下来之后,最大的感受是:它看着简单,其实把嵌入式里最常用的几条链路都打通了。ADC采集、LCD显示、串口上报,这三样东西单独拿出来都不难,但合在一起,就特别适合作为STM32F103入门的里程碑项目。你做完这一个,等于把模拟信号处理、显示驱动、通信协议这三大块都过了一遍,后面不管做仪表、传感器节点、数据采集卡,底子都是这个套路。

这个项目本身做什么,一句话就能说清楚:通过STM32F103的片内ADC读取外部电压,把数值实时显示在LCD液晶屏上,同时通过串口把电压数据发到电脑端,用串口调试助手观察和记录数据。对新手来说,它可以帮你彻底搞懂“模拟信号进,数字处理,再输出到人和机器”的完整数据流。对老手来说,它可以作为很多复杂仪器项目的最小原型,往上面加传感器、加按键菜单、加CAN或Modbus都很方便。

1. 项目整体设计与思路拆解

1.1 三块核心功能的分工与协作

先把这个项目的“骨架”拆开看。它本质上是一条完整的数据链路:物理量 -> 电压 -> 数字量 -> 处理结果 -> 展示与上报。

第一块是ADC采集。STM32F103内部有一个12位逐次逼近型ADC,最高可以采样到14MHz的ADC时钟。它能测量的电压范围是0到参考电压,一般就是0到3.3V。所以我们要做的就是接入被测电压,让单片机把它转换成0到4095之间的整数值。这里有个很关键的物理限制:STM32F103的ADC引脚耐压不能超过VDDA,如果你要测的电压超过3.3V,必须用电阻分压或者运放调理电路。很多新手第一次烧板子,就是因为直接把5V或者12V怼进PA0-PA7引脚了。

第二块是LCD显示。市面上的小屏方案很多,有0.96寸OLED、1.8寸TFT、3.2寸电阻屏、带字库的12864等等。这个项目里我用的是比较经典的SPI接口TFT屏,驱动芯片是ST7735,分辨率为128x160。选它有几个原因:SPI接口只占用几根IO口,代码写起来比并口屏简单,而且这颗芯片的资料和驱动代码非常多,遇到问题很容易搜到。显示内容也很明确:电压数值,配上“Voltage:”这样的提示文字,有条件的也可以显示中文,但中文字模需要额外取模,这点后面我会展开讲。

第三块是串口发送。串口是STM32和PC通信最简单的方式,只需要一根USB转TTL线,接到PA9(USART1_TX)和PA10(USART1_RX)上,再把波特率、数据位、停止位配置一致,就能互相收发数据。在这个项目里,串口起一个“数据上报”的作用,把ADC采集到并换算好的电压值以文本形式发出去,电脑上用串口调试助手看到的就是一行行清晰可读的数据。将来如果接个ESP8266模块,同样的数据就能通过WiFi传到云端,思路是完全一样的。

1.2 开发环境与工具选型:为什么选标准库

我这次用的是标准外设库V3.5(STM32F10x_StdPeriph_Lib),而不是STM32CubeMX生成的HAL库。选标准库,不是因为HAL库不好,而是对这个项目来说,标准库有它独特的优势:代码量更少、调用关系更直接,你会清楚地看到“初始化一个ADC到底需要配置哪些寄存器”。如果你刚接触STM32,用标准库手写一遍ADC初始化,再去理解HAL库封装的那些函数,你会觉得一切都是顺理成章的。如果你上来就用HAL库,当然也能跑起来,但很多底层细节会被包装得严严实实,出了问题反而不容易定位。

开发工具我用的是Keil MDK5。工程建立之后,记得在C/C++选项卡里定义STM32F10X_MD和USE_STDPERIPH_DEVICE,这样编译器和标准库才能正确匹配你要用的型号(我是用STM32F103C8T6,属于中等容量)。编译器版本建议Versatile Compiler V5.06,兼容性最好,V6编译器对这个老库支持多少有些小坑。

下载调试方面,ST-Link V2是最省钱也最稳定的选择。串口调试助手我用的是XCOM,见字面和界面直观。USB转TTL模块的核心芯片是CH340,如果电脑不识别设备,优先去官网装对应驱动。同样功能的还有FTDI芯片的模块,FTDI的驱动兼容性更好,但CH340胜在便宜,两者用起来没有本质区别。

1.3 最小系统板与硬件基础

我用的是一块ST-Link集成的STM32F103C8T6最小系统板,也就是俗称的“蓝板”。如果你用的是裸板,就需要自己搭一个最小系统:主要包含5个部分——电源(3.3V供电加若干去耦电容)、8MHz主晶振(两个20pF左右的负载电容)、复位电路(RESET引脚上拉10k电阻加一个按钮)、BOOT0和BOOT1引脚的上下拉设置(一般BOOT0接10k下拉到地,选择从Flash启动)、以及SWD下载接口(PA13 SWDIO和PA14 SWCLK,再加上GND和3.3V,四根线就行)。

很多人觉得最小系统板是“买了直接用”,懒得管这背后的原理。但我的体会是,花半小时把最小系统原理图看一遍,能省下后面好几个小时的排查时间。比如系统偶尔复位,你首先会怀疑供电和复位引脚的上拉电阻,再怀疑晶振有没有起振,而不是死活查不到原因。做ADC项目还有一个容易忽略的点:给MCU供电的电源质量直接决定ADC的稳定度。如果你用的是电脑USB口供电,尽量别在同一个USB口上挂一堆耗电的外设,否则ADC数值会跟着波动,看起来像“污染”了一样。

2. 核心细节解析与实操要点

2.1 ADC采集原理与关键参数计算

STM32F103的ADC是一个12位逐次逼近型(SAR)ADC。它的工作原理可以这样理解:内部一个比较器加上一堆精密的电阻网络(R-2R梯形网络),先从最高位开始猜测输入电压对应的二进制值,然后逐位逼近,最终在12个周期内接近真实的模拟值。如果你将来去翻STM32中文参考手册的ADC章节,你会发现里面最常念叨的参数就是采样时间、转换周期、对齐方式,这些正好都是代码里需要配置的东西。

首先看时钟。STM32F103的系统时钟经过APB2预分频之后得到PCLK2,默认72MHz,ADC的时钟最大允许14MHz,所以ADC预分频器至少得分频到6分频(72/6=12MHz)。配置语句是RCC_ADCCLKConfig(RCC_PCLK2_Div6),这是ADC稳定工作的前提。

然后是采样时间。参考手册里有一张表,列出了可选的采样周期:1.5周期、7.5周期、13.5周期、28.5周期、41.5周期、55.5周期、71.5周期、239.5周期。采样周期越长,采样保持电容充电越充分,测量结果越准确,但转换速度越慢。一次完整的转换需要“采样周期 + 12.5个周期”(12周期是逐次逼近时间加0.5周期对齐)。以我配置的55.5周期为例,总转换周期数是55.5 + 12.5 = 68个周期,在12MHz ADC时钟下,转换时间是68 / 12MHz ≈ 5.67微秒。这个速度对显示电压值来说绰绰有余。

这里有个实际经验:如果被测电压信号的内阻很高,比如直接从一个100k电位器的滑动端取电压,采样保持电容有可能在一个周期内充不满,导致测量结果偏低。碰到这种情况,最有效的解决办法不是换滤波器,而是把采样周期加长。很多人在采样周期上踩坑,就是不明白“采样时间”和“转换时间”的区别。

ADC初始化时还要注意对齐方式,我用的右对齐(ADC_DataAlign_Right),这样12位有效数据存在寄存器的低12位,读出来直接就是0到4095。如果用左对齐,虽然最高位对齐,但低4位是无效的0,换算电压时要多移4位,容易把人搞晕。

接下来说电压换算。STM32F103的参考电压默认是VREF+引脚电压,在大多数开发板上就是3.3V。所以测量值ADC_VALUE对应的电压计算公式是:

实际电压 = ADC_VALUE * 3.3 / 4095

注意这里的分母,有的教科书按4096算,有的按4095算,看ADC的满量程定义。STM32参考手册上明确写着,满量程对应的数字值是4095,也就是2的12次方减1。所以我习惯用4095来换算,这样0x0FFF对应3.3V,物理意义更准确。不过说实话,不管用哪个作为分母,误差都不到0.1%,除非你是用来做精密仪器,否则都可以接受。

代码里这一步用浮点数算,3.3f,然后再sprintf格式化为字符串,保留两位小数,方便串口解析。

2.2 数据滤波与读数稳定

如果直接把ADC的原始值打印到串口上,你会发现最后一位经常跳来跳去,这不是程序写错了,而是真实的模拟信号本来就含有噪声,加上ADC本身存在量化误差和内部噪声,读数轻微波动太正常了。对于一些对稳定性要求高的场景,比如测电池电压、测温度传感器电压,抖动会让人很焦虑。所以数据滤波是这个项目中几乎必然要做的一步。

最简单的做法是滑动平均滤波:连续读8次或16次,求平均。注意滑动平均和普通平均的区别,普通平均一次算完就结束,滑动平均是每次读取新值后,剔除旧值、加入新值再平均,适合连续采样的场景。在STM32上实现起来很直观,读8次求和再除以8即可。为什么取8次?不是因为8吉利,而是因为用移位操作就可以实现除以8,编译效率高,而且8次平均对工频干扰的抑制效果在一般场合够用了。

如果你面对的是周期性很强的干扰,比如50Hz工频干扰,平均窗口取20ms的整数倍时间内的采样次数会更有效,这涉及到ADC采样率和窗口长度的匹配,属于进阶玩法。初学者不用太纠结,先做8次平均,发现效果不好再去调整窗口长度。

还有一种中值滤波适合对付偶发的尖峰脉冲,比如静电干扰导致读数瞬间跳到最大值。实现思路是把连续5次结果存到一个数组里,排序后取中间的那一个。把中值滤波和滑动平均结合使用,抗干扰效果会更好,但代价是延迟变大。在实际项目中,我一般先把数据采进来,观察上位机波形,再决定用什么滤波算法。别一上来就整复杂的卡尔曼滤波,那对这个项目来说属于杀鸡用牛刀。

2.3 LCD显示实现与防极化那些事

这个项目里LCD显示是最直观的部分,但也是最容易翻车的部分之一。我用的ST7735TFT屏是SPI接口,接线方式一般是:VCC接3.3V、GND接地、SCLK接SPI时钟线、MOSI接SPI数据线、CS接片选、DC(有时也叫RS或A0)接命令数据选择引脚、RST接复位引脚、BLK(或LED)接背光控制。因为STM32F103自带硬件SPI,可以直接用SPI1来驱动,接口引脚是PA5(SCK)、PA7(MOSI)、PA4(CS),DC和RST用普通GPIO控制即可。如果你想省IO,也可以把SPI1的引脚映射到PB3、PB4、PB5等位置,但出于兼容性考虑,我建议优先使用默认引脚。

关于LCD屏的“极化避免”,这可能是很多人的知识盲区。液晶显示的工作原理是液晶分子在电场作用下发生偏转,而问题在于液晶材料本身对直流电场非常敏感,长时间施加直流电压会导致液晶分子发生不可逆的电化学反应,造成显示残留、对比度下降甚至永久损坏。所以正规的LCD驱动IC在设计时都会采用交流驱动方式,也就是不断翻转液晶两端的电压极性,让平均电压为零,这种现象叫“液晶的极化避免”或者“防直流驱动”。

对使用模块的开发板来说,驱动IC已经内置了极性反转机制,平时不太需要关心。真正需要注意的是这几点:第一,尽量不要让LCD长时间显示完全静止的高对比度画面,比如整屏全黑或全白,这对液晶寿命不友好,必要时可以让程序定期做一下清屏或亮度调整;第二,有的LCD模块上的V0或VOUT引脚是接液晶偏压的,如果屏幕对比度不对或者出现残影,可以微调这个脚的电压;第三,显示驱动代码初始化顺序必须正确,先复位,再延迟,然后发一堆初始化寄存器命令,顺序乱了屏幕不是花屏就是白屏。

再说中文显示。如果你用的屏没有内置中文字库,要显示中文,需要用PCtoLCD2002或者Image2Lcd之类的取模软件,把汉字转成16x16像素的点阵数组,一个汉字占32字节。这个从“需要包含GB2312字库表”的角度来说,比西文字符麻烦不少。所以我这个项目先显示英文和数字,关键数据显示用英文,简单直接。后面如果想加中文菜单,再专门写一个字模文件。

背光亮度控制也是个加分项。如果你的屏有BLK引脚,可以用一个PWM信号去控制背光亮度,实现类似手机屏幕调光的效果。如果只是简单控制亮和灭,用一个GPIO输出高电平拉亮也可以,但长期用最大背光,屏幕发热和老化速度会快一些。所以我建议BLK引脚接一个限流电阻,比如100欧,防止电流过大烧掉背光LED。

2.4 串口发送的规划与细节

串口这块其实是最“透”的部分,但很多人还是在通信格式上栽跟头。先说引脚:我用的是USART1,默认引脚是PA9(TX)和PA10(RX)。把PA9接到USB转TTL模块的RXD上,PA10接到TXD上,注意是交叉连接,然后让两块板的GND连在一起。不共地的话,串口通信会非常不稳定,数据乱码是轻的,严重时根本收不到数据。

波特率我选的是115200。选择依据很简单:数据显示量不大,9600也能跑,但115200的接收端解码容错性更好,而且大部分USB转TTL和串口工具都默认支持。芯片内部波特率是通过USARTDIV分频计算出来的:BRR寄存器值 = 72MHz / (16 * BaudRate),当波特率配置为115200时,分频值不是整数,会有约0.08%的误差,这个误差远小于串口允许的容差范围,实际使用完全没有问题。

发送的数据格式我设计成一行文本:

V:3.30V

或者更完整一点:

ADC=2879 V=2.32V

为什么不用纯数字?因为有了字段名,以后数据多了容易解析,用示波器抓包也一清二楚。如果你将来和上位机软件配合,建议末尾加回车换行(\r\n),上位机按行读取会非常舒服。要是想省流量,可以只发数字,但人眼看起来就不太直观了。

串口初始化的几个关键参数:8位数据位、1位停止位、无校验、无硬件流控。这些是标准配置,串口调试助手那边也要选成一致。要注意的是,如果PA9和PA10你同时用来做别的功能,比如接按键或者LED,那串口初始化可能会打架。调试期间最好让PA9、PA10保持专用,坏消息是ST-Link也占用了SWD口,好消息是SWD口和USART1不冲突,所以可以同时保留下载和串口功能,这点很方便。

3. 实操过程与核心代码实现

3.1 完整流程与初始化顺序

整个程序的执行流程可以归纳为:初始化时钟和GPIO -> 初始化ADC -> 初始化USART1 -> 初始化LCD -> 进入主循环,主循环里做这些事:读ADC值 -> 数字滤波 -> 换算成电压 -> 同时送LCD显示和串口发送 -> 延时或定时等待。

重点提一下初始化顺序。我的习惯是先初始化RCC时钟和必要的GPIO,比如ADC输入引脚、串口引脚、LCD引脚。然后配置系统时钟为72MHz,这一步一般由SystemInit()函数自动完成。接着再初始化各个外设模块,顺序无所谓,只要不互相依赖就行。但ADC在正式转换之前一定要做一次自校准,标准库里对应的函数是ADC_ResetCalibration()和ADC_StartCalibration()。不清零校准状态直接转换,有时候会测出偏差几百个数值的错误结果,这个问题排查起来非常隐蔽。

主循环里的延时,我建议用一个SysTick或定时器去控制发送频率,比如每300毫秒发送一次,这样上位机看到的数据不会刷得飞快看不到。如果你直接空转不加延时,while循环每秒会跑上千次,串口会被数据淹没,LCD也会频繁刷新闪眼睛。模块化代码是另外一个重要习惯。我把ADC读取、LCD显示、串口发送都封装成独立函数,这样任何一个模块出问题,我可以单独测试,而不用整段代码来回改。新手写项目最喜欢把所有逻辑塞在main里,最后调试起来想哭。

3.2 关键代码配置与讲解

下面这段是ADC初始化的核心部分,基于标准库V3.5,我加了非常详细的注释。

static void ADC_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; /* 开启GPIOA和ADC1的时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); /* ADC时钟 = PCLK2 / 6 = 72MHz / 6 = 12MHz,不超过14MHz上限 */ RCC_ADCCLKConfig(RCC_PCLK2_Div6); /* PA1配置为模拟输入,切记不要配置成推挽输出或者浮空输入 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); /* ADC1独立工作模式,单次转换,软件触发,右对齐,通道数1 */ ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); /* 配置转换通道:1号通道,转换顺序为第1个,采样时间55.5周期 */ ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5); /* 使能ADC1,并执行自校准 */ ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); }

注意GPIO的模式必须是GPIO_Mode_AIN(模拟输入),这也是很多人容易写错的地方。还有一个容易忽略的地方:如果你在前面把该引脚配置成了复用推挽(GPIO_Mode_AF_PP),ADC值会受影响,因为它不再是纯粹从引脚读模拟电压,而是可能受到其他外设的电平影响。

读取ADC的函数如下:

uint16_t ADC_Read(void) { /* 软件触发一次转换 */ ADC_SoftwareStartConvCmd(ADC1, ENABLE); /* 等待转换结束标志位EOC置位 */ while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); /* 返回转换结果,因为右对齐,直接返回即可 */ return ADC_GetConversionValue(ADC1); }

滤波函数用8次平均:

uint16_t ADC_GetAverage(void) { uint8_t i; uint32_t sum = 0; for (i = 0; i < 8; i++) { sum += ADC_Read(); } return (uint16_t)(sum / 8); }

为什么把滤波封装成单独的函数?因为将来如果要换中值滤波或者卡尔曼滤波,你只需要改这一个函数,主循环里完全不用动。调用ADC_GetAverage()拿到滤波后的值,换算电压并显示,代码如下:

char lcd_buf[16]; uint16_t adc_value = ADC_GetAverage(); float voltage = (float)adc_value * 3.3f / 4095.0f; sprintf(lcd_buf, "ADC=%d V=%.2f", adc_value, voltage); LCD_ShowString(0, 0, lcd_buf); sprintf(lcd_buf, "V=%.2f\r\n", voltage); UART_SendString(USART1, lcd_buf);

注意LCD_ShowString和UART_SendString都需要你自己实现。UART_SendString的思路非常直接:一个字符一个字符往发送数据寄存器塞,塞之前检查发送数据寄存器为空(TXE标志),等待前一个字节发送完成再发下一个,避免覆盖:

void UART_SendString(USART_TypeDef *USARTx, char *str) { while (*str) { while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET); USART_SendData(USARTx, (uint16_t)*str); str++; } while (USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET); }

这里最后一行等待TC(发送完成)标志是很多教程里会漏掉的。其实TXE表示数据已经从你的代码搬到了移位寄存器,但数据还没从移位寄存器完全发送出去。如果你紧接着改波特率或者关闭串口,之前的最后一个字节可能被吞掉。加上等待TC,确保所有数据完整上了线。

3.3 硬件接线与现场调试步骤

接线表我整理成这样,照着插就行:

LCD模块引脚接到STM32F103说明
VCC3.3V模块供电
GNDGND共地
SCL/SCKPA5SPI时钟
MOSI/SDAPA7SPI数据输出
CSPA4片选,低有效
DC/RSPA2数据/命令选择
RSTPA3复位,默认拉高
BLK/LED3.3V串100欧电阻背光电源

串口部分:

USB转TTL接到STM32F103
RXDPA9(USART1_TX)
TXDPA10(USART1_RX)
GNDGND

关于被测电压,我用的是一个10k电位器,三个引脚分别接3.3V、GND、中间滑动端接PA1。这样旋转电位器就能得到0~3.3V可调的输入电压,方便验证显示和串口数据是否正确。你要注意,这句话容易产生误导,实际电阻分压不是把电位器一半的电压直接接到PA1那么简单,而是在PA1和GND之间再并联一个10k电阻,让PA1对地有稳定的分压路径,否则ADC输入悬空时读数会乱跳。

调试步骤,我按照从易到难的顺序来:

  1. 先烧一个最简单的LED闪烁程序,确认开发板和下载链路没问题。这一步看起来很傻,但能避免后面出了问题搅在一起。
  2. 再烧串口回环测试程序,把电脑发来的数据原样返回,如果串口调试助手能看到回显,说明串口链路没问题。
  3. 然后再把ADC程序烧进去,先不接LCD,直接把电压打印到串口,观察数据是否随电位器变化。
  4. 最后才接LCD,把显示函数加上去。这样你每增加一个模块,都知道问题的可能范围。

实际调试时,我习惯把串口调试助手的界面分成两个窗口,一个看原始数据显示窗口,一个看刷新率,方便判断程序是在死循环还是真的在跑。如果你看到数据一直不变,先怀疑是否进入了某个while等待循环,比如ADC的校准等待或者串口的TXE等待,这类死等在程序运行不稳时非常常见。

4. 踩坑实录:ADC、LCD、串口的常见问题排查

4.1 ADC读数飘了怎么办

ADC读数漂移是最让人头疼的问题之一。如果你发现数据不是稳定地停留在某个值,而是一直在上下跳,优先按这个顺序排查:

现象可能原因处理办法
读数整体偏高/偏低参考电压不是精确3.3V用万用表量VREF,按实际电压换算
读数持续波动信号源内阻过大增大采样周期到239.5周期
读数有规律跳变采样间隔与工频干扰同步调整采样频率或用滑动平均滤波
读数偶尔突变电源噪声或接地不稳检查USB口供电质量,共地是否可靠
通道间相互影响多个通道连续转换但没加延迟改用逐次采样模式或加延时

有一个细节容易被忽略:如果你在PA1引脚上接了很长很细的杜邦线,这根线相当于一根天线,会吸收附近的电磁干扰。ADC采集时,信号源的内阻越高,这种天线效应越明显。解决方法是尽量缩短接线距离,或者用双绞线把信号线和GND线绞在一起。我实测下来,把一个10k电位器的滑动端直接经过20cm的杜邦线接到PA1,读数比贴近MCU引脚时跳动范围大了大概30%,所以走线问题不能轻视。

4.2 LCD显示异常怎么查

LCD的故障现象通常非常整齐:白屏、花屏、固定位置出现乱码,每种现象对应的问题也不太一样。

白屏,先查供电子模块供电,再用示波器看CS信号SPI时钟信号是否正常。很多时候白屏是因为初始化命令没有正确发出,比如DC引脚极性反了,或者复位时序不对。TFT屏的复位是高电平有效还是低电平有效,模块Datasheet上都会写,留意一下即可。

花屏,大概率是SPI速率太高或数据线被干扰了。STM32的SPI1挂在APB2总线上,时钟最高可达36MHz,但杜邦线连的屏幕根本跑不了这么快。我一般会把SPI分频配置成8分频或者16分频,宁可慢一点也要稳定。另外检查SPI模式,ST7735一般是SPI Mode 0(CPOL=0、CPHA=0)或者Mode 3(CPOL=1、CPHA=1),配置反了它也会花屏或者白屏。

固定位置出现乱码,那多半是显示缓冲区地址算错了。比如128x160分辨率,一行像素占的字节数不是128而是240(因为底层驱动芯片可能是按240x320标准设计的),写坐标时没有做地址映射转换,显示位置就会错位。这个只能去查驱动芯片的数据手册和初始化时设置的显示偏移量。

还有一个容易被人忽视的“偏压”问题。如果你用的是裸液晶屏模块或者12864,V0引脚外部的对比度调节电位器阻值不合适,会对显示效果和液晶寿命有影响。如果屏幕出现对比度失衡、残影,试着微调V0电压,正常情况下屏面残余影像会慢慢消退。这就是前面说的防极化和直流偏置问题的具体体现。

4.3 串口没数据/乱码的排查

串口问题在项目里几乎每个人都会遇到一次。没有数据的时候,先别急着改代码,用表查硬件:

  1. TX和RX是不是接反了?PA9接RXD,PA10接TXD,反了的话啥也收不到。
  2. 两边共地了吗?没有共地,通信基本废了。
  3. 波特率一致吗?电脑端和MCU端相差一个0都不行。
  4. USB转TTL模块的驱动装上了吗?打开设备管理器,确认出现的是COM口而不是其他未知设备。
  5. 有没有被其他程序占用串口?比如烧录软件和串口助手同时开着,COM口会被占用导致打不开。

串口乱码的原因多半是波特率配置不对,或者是发送的数据本身就不是ASCII文本。如果你发送的是数值的十六进制表示,比如0x53,在串口助手里直接以文本模式看就是乱码。解决方法是确认发送端发送的是字符串,也就是数字要先经过sprintf转成字符,再逐字节发送。另一个冷门但常见的原因是晶振频率不对:如果最小系统板上用的不是8MHz晶振,而是12MHz或者16MHz晶振,而SystemInit里按8MHz计算时钟,波特率就会发生明显偏移,从而产生乱码。这种问题普通用户很难发现,但一旦遇到,几乎无解,只能换回标准8MHz晶振或者改系统时钟配置。

关于CH340驱动,补充一句:某些精简版驱动安装后,设备管理器显示的是“USB-SERIAL CH340”而不是“USB Serial Port”,这不算错误。但如果你插入模块后完全没有反应,可以先换一根USB线试试,很多USB转TTL模块用劣质数据线会被识别得极不稳定,这是我在实际中踩过的坑。

最后的几点个人经验

这个项目做完之后,我反而觉得它的难点不在代码,而在“串数据链路”的思维。你要能清楚地知道:ADC引脚进来的电压信号,经过采样保持、逐次逼近、寄存器读取,变成了数字量;数字量通过换算变成具有物理意义的电压值;电压值再被格式化成字符串,送到LCD的显存里变成像素,送到串口变成比特流。任何一环出了问题,都要能在脑子里快速定位。

如果让我给这个项目排一个优先级,我会建议先跑通串口打印,再加LCD显示。先看到一个可以观测的数据,再考虑显示美化。调试的时候,你甚至可以把串口当成“数据哨兵”,把所有关键变量都打出来观察,比盯着屏幕猜省力多了。

最后分享一个扩展方向:当你想提高采样精度时,可以使用外部参考电压芯片(比如REF3030)替代默认的3.3V参考,或者用外部ADC做三点校准。当你想提高采样速度时,可以改用ADC+DMA的方式连续采样多个通道而不用等待CPU介入。当你想做成产品时,可以把串口数据改成Modbus RTU或者自定义二进制协议,接进上位机软件。这一步一步加进来的东西,才是从“写完这个项目”到“吃透这个项目”之间的路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询