STM32F103实战:ADC采集+LCD显示+串口上报完整教程

STM32F103实战:ADC采集+LCD显示+串口上报完整教程 简介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线接到PA9USART1_TX和PA10USART1_RX上再把波特率、数据位、停止位配置一致就能互相收发数据。在这个项目里串口起一个“数据上报”的作用把ADC采集到并换算好的电压值以文本形式发出去电脑上用串口调试助手看到的就是一行行清晰可读的数据。将来如果接个ESP8266模块同样的数据就能通过WiFi传到云端思路是完全一样的。1.2 开发环境与工具选型为什么选标准库我这次用的是标准外设库V3.5STM32F10x_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位逐次逼近型SARADC。它的工作原理可以这样理解内部一个比较器加上一堆精密的电阻网络R-2R梯形网络先从最高位开始猜测输入电压对应的二进制值然后逐位逼近最终在12个周期内接近真实的模拟值。如果你将来去翻STM32中文参考手册的ADC章节你会发现里面最常念叨的参数就是采样时间、转换周期、对齐方式这些正好都是代码里需要配置的东西。首先看时钟。STM32F103的系统时钟经过APB2预分频之后得到PCLK2默认72MHzADC的时钟最大允许14MHz所以ADC预分频器至少得分频到6分频72/612MHz。配置语句是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来驱动接口引脚是PA5SCK、PA7MOSI、PA4CSDC和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默认引脚是PA9TX和PA10RX。把PA9接到USB转TTL模块的RXD上PA10接到TXD上注意是交叉连接然后让两块板的GND连在一起。不共地的话串口通信会非常不稳定数据乱码是轻的严重时根本收不到数据。波特率我选的是115200。选择依据很简单数据显示量不大9600也能跑但115200的接收端解码容错性更好而且大部分USB转TTL和串口工具都默认支持。芯片内部波特率是通过USARTDIV分频计算出来的BRR寄存器值 72MHz / (16 * BaudRate)当波特率配置为115200时分频值不是整数会有约0.08%的误差这个误差远小于串口允许的容差范围实际使用完全没有问题。发送的数据格式我设计成一行文本V:3.30V或者更完整一点ADC2879 V2.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_PPADC值会受影响因为它不再是纯粹从引脚读模拟电压而是可能受到其他外设的电平影响。读取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接到STM32F103RXDPA9USART1_TXTXDPA10USART1_RXGNDGND关于被测电压我用的是一个10k电位器三个引脚分别接3.3V、GND、中间滑动端接PA1。这样旋转电位器就能得到0~3.3V可调的输入电压方便验证显示和串口数据是否正确。你要注意这句话容易产生误导实际电阻分压不是把电位器一半的电压直接接到PA1那么简单而是在PA1和GND之间再并联一个10k电阻让PA1对地有稳定的分压路径否则ADC输入悬空时读数会乱跳。调试步骤我按照从易到难的顺序来先烧一个最简单的LED闪烁程序确认开发板和下载链路没问题。这一步看起来很傻但能避免后面出了问题搅在一起。再烧串口回环测试程序把电脑发来的数据原样返回如果串口调试助手能看到回显说明串口链路没问题。然后再把ADC程序烧进去先不接LCD直接把电压打印到串口观察数据是否随电位器变化。最后才接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 0CPOL0、CPHA0或者Mode 3CPOL1、CPHA1配置反了它也会花屏或者白屏。固定位置出现乱码那多半是显示缓冲区地址算错了。比如128x160分辨率一行像素占的字节数不是128而是240因为底层驱动芯片可能是按240x320标准设计的写坐标时没有做地址映射转换显示位置就会错位。这个只能去查驱动芯片的数据手册和初始化时设置的显示偏移量。还有一个容易被人忽视的“偏压”问题。如果你用的是裸液晶屏模块或者12864V0引脚外部的对比度调节电位器阻值不合适会对显示效果和液晶寿命有影响。如果屏幕出现对比度失衡、残影试着微调V0电压正常情况下屏面残余影像会慢慢消退。这就是前面说的防极化和直流偏置问题的具体体现。4.3 串口没数据/乱码的排查串口问题在项目里几乎每个人都会遇到一次。没有数据的时候先别急着改代码用表查硬件TX和RX是不是接反了PA9接RXDPA10接TXD反了的话啥也收不到。两边共地了吗没有共地通信基本废了。波特率一致吗电脑端和MCU端相差一个0都不行。USB转TTL模块的驱动装上了吗打开设备管理器确认出现的是COM口而不是其他未知设备。有没有被其他程序占用串口比如烧录软件和串口助手同时开着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做三点校准。当你想提高采样速度时可以改用ADCDMA的方式连续采样多个通道而不用等待CPU介入。当你想做成产品时可以把串口数据改成Modbus RTU或者自定义二进制协议接进上位机软件。这一步一步加进来的东西才是从“写完这个项目”到“吃透这个项目”之间的路。本文还有配套的精品资源点击获取