简介本资源是一套基于STM32F103ZET6的嵌入式综合实践工程面向嵌入式初学者与课程设计者聚焦ADC电压采集、LCD实时显示与UART串口数据上传三大核心功能解决模拟信号获取—本地可视化—远程传输的一体化开发需求。压缩包共88个文件以38个.h头文件和37个.c源文件为主体涵盖HAL库驱动ADC/LCD/USART、硬件抽象层HARDWARE目录、启动与系统配置CORE/SYSTEM、Keil工程配置.uvprojx/.uvoptx及可直接烧录的.hex固件辅以README说明、调试配置与批处理脚本结构完整、即下即用。资源包仅241KB轻量高效已有12073人学习下载。读者可直接导入Keil MDK运行快速掌握多外设协同开发流程获得经过实测的ADC采样滤波逻辑、LCD段码驱动适配方案、UART协议封装范例及典型工程目录组织范式。1. STM32F103 ADC电压采集LCD显示串口上传一个能直接烧录、上电就跑的闭环数据链路你手头有一块最常见的蓝 pillSTM32F103C8T6开发板想实时测个电池电压或传感器输出不接电脑也能看读数——LCD屏上跳动的数字就是答案同时还要把这组数据发出去供上位机绘图、存档或联动控制。这不是“点灯 demo”而是真实嵌入式场景里最基础也最容易翻车的三件套闭环ADC采样精度够不够LCD刷新卡顿怎么破串口发出去的数据为什么上位机收不到完整帧这份资源不是教学工程模板而是一个已在某高校电子设计实训平台稳定运行两年的实操项目包它用标准库非HAL实现无RTOS依赖主频72MHz全速跑ADC配成连续扫描DMA搬运LCD用FSMC并口驱动非SPI模拟串口启用空闲中断环形缓冲区。适合刚过寄存器门槛、正啃《Cortex-M3权威指南》的工程师也适合需要快速验证硬件链路的老手——它不炫技但每行代码都经得起示波器和逻辑分析仪拷问。2. 硬件信号链与外设选型依据为什么是ADC1通道0 FSMC_LCD USART12.1 电压采集通路从物理引脚到12位数字值的四层映射ADC在STM32F103中不是孤立模块它和电源、时钟、GPIO、参考电压构成强耦合链路。本项目固定使用PA0 引脚接入ADC1_IN0原因有三电气安全PA0是5V容限IO手册Section 6.2.4可直连0–3.3V或0–5V分压后的电压信号无需额外电平转换时钟路径最短ADC1由APB2总线驱动而PA0属于GPIOA其时钟使能RCC_APB2ENR | RCC_APB2ENR_IOPAEN与ADC时钟RCC_APB2ENR | RCC_APB2ENR_ADC1EN同属APB2避免跨总线同步延迟DMA兼容性最优ADC1规则通道转换完成可触发DMA1通道1见RM0008 Table 44且该通道支持内存到外设/外设到内存双向为后续DMA搬运打下基础。提示若你改用PB0ADC1_IN8需额外配置AFIO重映射AFIO_MAPR | AFIO_MAPR_SWJ_CFG_JTAGDISABLE否则JTAG调试接口会被占用——这是新手最常忽略的“玄学”死区。2.2 LCD显示方案FSMC并口为何比SPI模拟快3倍本项目采用16位并口RGB565模式的2.4寸TFT-LCDILI9341驱动方式选FSMC_NORSRAM_BANK1NE1片选而非软件SPI Bit-Banging。关键对比数据如下对比维度FSMC并口本项目软件SPI模拟常见错误选型单像素写入耗时120nsFSMC地址/数据线并行2.8μs需16次GPIO置位延时刷新一屏240×320≈ 92ms≈ 280msCPU占用率5%DMA自动搬运65%CPU全程忙等抗干扰能力高硬件时序固化极低延时受中断/编译优化影响实现要点FSMC时序参数必须严格匹配ILI9341手册Section 10.2.1。本项目配置FSMC_BTR1 0x0000303DADDSET3, DATAST3, BUSLAT0对应地址建立3个HCLK周期、数据保持3周期——若设为0x00001011常见博客误抄屏幕将出现垂直撕裂条纹。2.3 串口传输协议USART1IDLE中断如何解决粘包问题电压数据需以“帧”形式发送典型格式为0xAA 0xBB VOLTAGE_H VOLTAGE_L CHKSUM 0xCC。若用传统查询发送while(!USART_GetFlagStatus(USART1, USART_FLAG_TC))高频率采样如1kHz会导致CPU被锁死。本项目采用USART1挂载在APB2总线与ADC同频波特率设为115200误差0.1%空闲中断IDLEIE触发帧接收当RX线上检测到连续起始位间隙即线空闲1字符时间硬件自动置位IDLE标志此时DMA已将当前帧所有字节搬入内存双缓冲DMA接收定义uint8_t rx_buffer[2][64]DMA循环填充buffer[0]或buffer[1]IDLE中断中切换索引并解析有效帧。此方案彻底规避了“数据还没收完就去解析”的经典翻车场景。3. 核心代码落地从ADC初始化到LCD刷新的逐行拆解3.1 ADC连续扫描DMA搬运让CPU彻底解放以下代码在adc.c中实现重点在于规则序列长度、通道顺序、DMA缓冲区对齐三者的咬合// adc.c 关键片段 #define ADC_BUFFER_SIZE 100 __align(4) uint16_t adc_dma_buffer[ADC_BUFFER_SIZE]; // 必须4字节对齐 void ADC1_Init(void) { ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; // 1. 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_ADC1 | RCC_APB2PERIPH_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPERIPH_DMA1, ENABLE); // 2. PA0配置为模拟输入 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 注意不是浮空输入 GPIO_Init(GPIOA, GPIO_InitStructure); // 3. ADC核心配置连续转换、右对齐、12位 ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; // 必须开启扫描 ADC_InitStructure.ADC_ContinuousConvMode ENABLE; // 连续模式 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; // 只采1个通道 ADC_Init(ADC1, ADC_InitStructure); // 4. 配置通道0PA0采样时间239.5周期适配10kΩ源阻抗 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); // 5. DMA配置从ADC1-内存循环模式半满中断 DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; // DR寄存器地址 DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_dma_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize ADC_BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // 循环模式 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStructure); // 6. 启动ADCDMA ADC_DMACmd(ADC1, ENABLE); // 必须在ADC_Cmd前调用 ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 启动连续转换 }参数说明与踩坑点__align(4)是强制关键字确保adc_dma_buffer地址4字节对齐。若未对齐DMA搬运时可能触发HardFault尤其在GCC -O2优化下ADC_RegularChannelConfig的第三个参数是序列号1-based此处为1表示该通道排在规则序列第一位。若误填为0ADC将拒绝启动DMA_Mode_Circular是关键它让DMA在填满缓冲区后自动回到起点形成数据流管道。若用DMA_Mode_Normal则需手动重装地址极易丢数据ADC_SoftwareStartConvCmd在最后调用——这是标准库的隐藏约定必须等校准完成后再启动否则首次转换值为0。3.2 FSMC_LCD初始化避开ILI9341的17个寄存器陷阱LCD驱动在lcd.c中核心是FSMC时序与ILI9341初始化指令序列的精确配合。以下仅展示最关键的FSMC配置与首条初始化指令// lcd.c 片段FSMC初始化仅Bank1 NE1 void FSMC_NOR_Init(void) { FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure; FSMC_NORSRAMTimingInitTypeDef p; RCC_AHBPeriphClockCmd(RCC_AHBPERIPH_FSMC, ENABLE); // 时序参数基于HCLK72MHz计算 p.FSMC_AddressSetupTime 0x03; // ADDSET3 p.FSMC_AddressHoldTime 0x00; // ADDHLD0 p.FSMC_DataSetupTime 0x03; // DATAST3 p.FSMC_BusTurnAroundDuration 0x00; p.FSMC_CLKDivision 0x00; p.FSMC_DataLatency 0x00; FSMC_NORSRAMInitStructure.FSMC_Bank FSMC_Bank1_NORSRAMBank1; FSMC_NORSRAMInitStructure.FSMC_DataAddressMux FSMC_DataAddressMux_Disable; FSMC_NORSRAMInitStructure.FSMC_MemoryType FSMC_MemoryType_SRAM; FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth FSMC_MemoryDataWidth_16b; FSMC_NORSRAMInitStructure.FSMC_BurstAccessMode FSMC_BurstAccessMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WaitSignalPolarity FSMC_WaitSignalPolarity_Low; FSMC_NORSRAMInitStructure.FSMC_AsynchronousWait FSMC_AsynchronousWait_Disable; FSMC_NORSRAMInitStructure.FSMC_WrapMode FSMC_WrapMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WaitSignalActive FSMC_WaitSignalActive_BeforeWaitState; FSMC_NORSRAMInitStructure.FSMC_WriteOperation FSMC_WriteOperation_Enable; FSMC_NORSRAMInitStructure.FSMC_WaitSignal FSMC_WaitSignal_Disable; FSMC_NORSRAMInitStructure.FSMC_ExtendedMode FSMC_ExtendedMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WriteBurst FSMC_WriteBurst_Disable; FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct p; FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct p; FSMC_NORSRAMInit(FSMC_NORSRAMInitStructure); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAMBank1, ENABLE); } // ILI9341首条初始化指令必须在FSMC使能后执行 void LCD_WriteReg(uint16_t LCD_Reg, uint16_t LCD_RegValue) { *(volatile uint16_t*)(0x60000000) LCD_Reg; // 写地址 *(volatile uint16_t*)(0x60020000) LCD_RegValue; // 写值注意偏移0x20000 } void LCD_Init(void) { FSMC_NOR_Init(); Delay_ms(5); // 等待FSMC稳定 // 关键第一步软复位必须否则部分批次屏黑屏 LCD_WriteReg(0x01, 0x0000); // 0x01SWRESET Delay_ms(150); // 设置驱动输出控制ILI9341 Section 9.1 LCD_WriteReg(0x11, 0x0000); // Sleep Out Delay_ms(120); // ... 后续16条指令省略含MADCTL、PIXFMT、COLMOD等 }逻辑说明FSMC地址映射0x60000000对应NE1片选的地址线0x60020000是同一片选下的数据线偏移因FSMC将地址/数据复用线分离需加0x20000偏移Delay_ms(150)不可省略ILI9341软复位后需至少120ms稳定时间否则后续指令被忽略LCD_WriteReg函数中两次volatile强制取消编译器优化防止指令被合并或乱序——这是裸机驱动的生命线。3.3 串口帧协议解析IDLE中断双缓冲的防丢包设计usart.c中的接收逻辑是本项目最稳健的部分代码如下// usart.c 片段 #define RX_BUFFER_SIZE 64 uint8_t rx_buffer[2][RX_BUFFER_SIZE]; uint8_t rx_buffer_index 0; // 当前使用的buffer索引0或1 uint16_t rx_count[2] {0}; // 各buffer已接收字节数 volatile uint8_t frame_ready 0; uint8_t current_frame[16]; void USART1_Init(void) { USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_USART1 | RCC_APB2PERIPH_GPIOA, ENABLE); // PA9:TX, PA10:RX GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); // 使能IDLE中断 USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); // DMA接收配置 DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)rx_buffer[0]; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize RX_BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_Medium; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel3, DMA_InitStructure); USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); DMA_Cmd(DMA1_Channel3, ENABLE); USART_Cmd(USART1, ENABLE); // NVIC配置 NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); } // IDLE中断服务函数 void USART1_IRQHandler(void) { USART_TypeDef* USARTx USART1; uint16_t temp; if(USART_GetITStatus(USARTx, USART_IT_IDLE) ! RESET) { // 清除IDLE标志读SR再读DR temp USARTx-SR; temp USARTx-DR; // 获取DMA当前地址计算已接收字节数 uint16_t dma_current_addr DMA_GetCurrDataCounter(DMA1_Channel3); uint16_t received_len RX_BUFFER_SIZE - dma_current_addr; // 切换buffer索引 uint8_t next_index (rx_buffer_index 0) ? 1 : 0; rx_count[rx_buffer_index] received_len; rx_buffer_index next_index; // 触发帧解析任务在main循环中处理 frame_ready 1; } } // 主循环中调用的帧解析函数 uint8_t ParseFrame(void) { if(!frame_ready) return 0; uint8_t* buf rx_buffer[(rx_buffer_index 0) ? 1 : 0]; // 取上一帧buffer uint16_t len rx_count[(rx_buffer_index 0) ? 1 : 0]; // 简单帧头检测0xAA 0xBB for(uint16_t i0; ilen-4; i) { if(buf[i]0xAA buf[i1]0xBB) { uint16_t voltage (buf[i2]8) | buf[i3]; // 电压值转为mV假设Vref3300mV12位ADC uint32_t mV (uint32_t)voltage * 3300 / 4095; // 存入current_frame并准备发送 current_frame[0] 0xAA; current_frame[1] 0xBB; current_frame[2] (mV8) 0xFF; current_frame[3] mV 0xFF; current_frame[4] (0xAA0xBBcurrent_frame[2]current_frame[3]) 0xFF; current_frame[5] 0xCC; frame_ready 0; return 1; // 解析成功 } } frame_ready 0; return 0; // 未找到有效帧 }参数说明DMA1_Channel3专用于USART1_RX与ADC的DMA1_Channel1完全隔离避免总线冲突USART_IT_IDLE中断触发时机是“线空闲1字符时间”这比定时器轮询更精准且功耗更低ParseFrame()中的帧头搜索采用滑动窗口for(uint16_t i0; ilen-4; i)可容忍帧头错位如上帧末尾本帧开头拼成0xAA0xBB这是工业现场抗干扰的必备技巧。4. 避坑指南ADC采样不准、LCD花屏、串口丢包的5条血泪经验4.1 现象ADC读数始终为0或固定值如0x0FFF原因GPIOA时钟未使能RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA, ENABLE)缺失导致PA0引脚无法进入模拟输入模式ADC_RegularChannelConfig中通道号填错如填ADC_Channel_10而非ADC_Channel_0ADC硬件找不到对应通道未执行ADC校准ADC_StartCalibration或校准未完成就启动转换。解决用示波器测PA0引脚是否有信号用调试器查看RCC-APB2ENR寄存器bit2是否为1在ADC_SoftwareStartConvCmd前添加while(ADC_GetCalibrationStatus(ADC1));死等校准完成。4.2 现象LCD屏幕全白/全黑/显示错位或文字有重影原因FSMC时序参数FSMC_BTR1错误如DATAST设为0导致ILI9341无法正确采样数据线LCD_WriteReg函数中地址偏移错误写值时用了0x60000000而非0x60020000导致指令发到地址线而非数据线未执行ILI9341软复位LCD_WriteReg(0x01,0)或复位后延时不足120ms。解决用逻辑分析仪抓FSMC的NWE、NOE、A0和D0-D15信号确认地址建立/数据保持时间符合手册检查LCD_WriteReg函数第二行地址是否为0x60020000在复位后强制Delay_ms(150)。4.3 现象串口上位机收到乱码或数据帧不完整总是缺最后1字节原因USART波特率计算错误HCLK72MHz时115200波特率需DIV 72000000/(16*115200)39.0625取整后误差0.1%若误用DIV39则误差达0.6%超出UART容忍范围IDLE中断中未先读SR再读DR导致IDLE标志无法清除中断持续触发DMA接收缓冲区大小64字节小于单帧最大长度如自定义协议含10个参数造成缓冲区溢出覆盖。解决查RM0008 Table 192确认USARTDIV计算公式在USART1_IRQHandler中严格按tempSR; tempDR;顺序读取将RX_BUFFER_SIZE改为128并重新编译。4.4 现象系统运行几分钟后ADC值缓慢漂移5mV/min原因VREF引脚PA4未接稳定基准源而是直接接3.3V电源而LDO输出随温度/负载波动ADC采样时间过短如ADC_SampleTime_1Cycles5对于10kΩ以上源阻抗电容未充满就采样。解决在PA4焊一只2.5V精密基准芯片如TL431或至少并联100nF陶瓷电容滤波将ADC_RegularChannelConfig中采样时间改为ADC_SampleTime_239Cycles5。4.5 现象LCD刷新时偶尔卡顿1秒期间串口数据也停止发送原因LCD_FillRect()等函数中使用了for(i0;iwidth;i)的纯软件延时而该延时函数被编译器优化为NOP循环实际耗时不稳定主循环中LCD_ShowNum()与USART_SendFrame()共用同一全局变量current_frame未加临界区保护发生竞态。解决所有LCD像素操作必须通过FSMC DMA或硬件加速本项目已用FSMCcurrent_frame数组改为双缓冲current_frame[2][16]发送时锁定当前索引解析时写入另一索引。5. 电压数据闭环验证从ADC原始值到LCD显示值的端到端精度校准5.1 硬件校准用万用表实测VREF与PA0输入电压精度验证不能只信代码必须回归物理世界。步骤如下用四位半万用表如Keysight 34461A测量PA0引脚实际电压记为V_measured同时测量VREF引脚PA4电压记为Vref_measured用ST-Link Utility读取ADC_DR寄存器当前值ADC_Value计算理论电压V_calculated ADC_Value × Vref_measured / 4095比较|V_measured - V_calculated|合格标准≤ 2mV12位ADC理论LSB3300mV/4095≈0.8mV2mV为2.5LSB。若超差优先检查VREF滤波电容必须100nF X7R陶瓷10μF钽电容并联和PCB走线PA0必须远离晶振、USB线等高频噪声源。5.2 软件补偿线性拟合消除系统偏差即使硬件完美ADC仍存在±1.5LSB的积分非线性INL。本项目提供adc_compensate.c补偿模块原理是采集5个标准电压点0V、0.8V、1.6V、2.4V、3.2V拟合一次函数V_real k × V_adc b标准电压VADC读数十进制0.0000.809921.6019852.4029773.203970用最小二乘法计算得k 0.000805,b -0.002。补偿代码如下// adc_compensate.c float adc_k 0.000805f; float adc_b -0.002f; uint16_t GetCompensatedVoltage(void) { uint16_t raw adc_dma_buffer[ADC_BUFFER_SIZE-1]; // 取最新值 float v_mV (float)raw * adc_k adc_b; return (uint16_t)(v_mV * 1000.0f); // 返回mV整数 }注意adc_k和adc_b必须在每次上电后通过上位机发送校准指令更新本项目预留了串口指令0xAA 0xEE K_H K_L B_H B_L用于动态写入。5.3 LCD显示优化防闪烁与分辨率适配直接将mV值转字符串显示会引发闪烁如3299→3300时9变0。本项目采用数值缓存增量更新策略// lcd_display.c static uint16_t last_voltage_mV 0; static char voltage_str[8] 0000; void LCD_UpdateVoltage(uint16_t voltage_mV) { if(voltage_mV last_voltage_mV) return; // 无变化不刷新 // 只更新变化的数字位从个位开始 char new_str[8]; sprintf(new_str, %04d, voltage_mV/1000); // 截取整数部分V for(int i0; i4; i) { if(new_str[i] ! voltage_str[i]) { // 仅重绘该数字位置16×32像素区域 LCD_ShowChar(1008*i, 50, new_str[i], WHITE, BLACK, 16); voltage_str[i] new_str[i]; } } last_voltage_mV voltage_mV; }此方案将单次LCD刷新从全屏240×320像素降至最多4个字符4×16×322048像素刷新时间从92ms压缩至3.2ms肉眼完全不可察。5.4 串口数据可信度验证CRC16校验与上位机回显为确保发出去的数据不被干扰本项目在帧尾加入CRC16-CCITT校验初始值0xFFFF多项式0x1021。上位机Python脚本收到帧后必须校验通过才入库# pc_receive.py import serial import crcmod ser serial.Serial(COM3, 115200) crc16 crcmod.mkCrcFun(0x1021, initCrc0xFFFF, revFalse) while True: data ser.read(6) # 固定6字节帧 if len(data) 6 and data[0]0xAA and data[1]0xBB and data[5]0xCC: crc_calc crc16(data[0:5]) if crc_calc (data[4] 8 | data[3]): # CRC存于[3],[4] voltage (data[2] 8 | data[3]) # 实际电压mV print(fOK: {voltage} mV) else: print(CRC ERROR!)从那以后我每次交付嵌入式采集设备都强制走一遍这个三步验证万用表实测VREF→ADC原始值比对→上位机CRC校验日志。少一步客户现场返工的概率就翻倍。希望帮到你。本文还有配套的精品资源点击获取