简介:面向嵌入式学习者与STM32入门开发者的一份ADC采样综合例程,围绕STM32F103ZET6展开模拟电压采集、LCD液晶显示与UART串口上传的完整链路,适合用来理解12位ADC多通道采样、LCD背光与段码驱动、串口起始位/数据位/停止位及校验位设置等典型知识点,也可作为课程实验与课程设计的前期验证参考。压缩包共88个文件,约241KB,以C源码和头文件为主体(37个.c、38个.h),并包含启动汇编、Keil工程文件、HEX固件、说明文档及调试配置,目录按HARDWARE、SYSTEM、USER、STM32F10x_FWLib等模块组织,便于按外设拆分阅读。已有12086人学习下载。读者可据此获得可直接编译运行的到手例程,对照ADC采样设置、LCD初始化、UART参数配置与发送函数实现,快速搭建实验环境并迁移到自己的采集显示项目;还可借助分模块驱动、USMART调试组件和工程调试文件,排查编译、下载与显示异常。
1. 一块板子、一个电位器,把 ADC 采集电压这条链路跑通
一块最小系统板、一个十块钱的电位器、一块 1602 字符屏,三样东西接起来,就是嵌入式里最经典的一条小信号闭环:ADC 采集电压、数值实时显示在液晶屏上、同一份数据再通过串口发给电脑。这套东西的工程价值不在"点亮",而在于它是所有模拟量采集项目的骨架——电子秤、温控、电池监测、电流互感器采样,后面挂什么传感器,无非是换前端调理电路而已。
真正卡人的地方从来不是能不能跑起来。小数点后第三位在那儿来回跳、串口助手里偶尔冒半帧乱码、换一块板子重烧就全错位——这些才是血泪经验堆出来的部分。下面的内容按数据流顺序推:从分压电阻、ADC 通道配置、DMA 搬运、滤波换算,一路讲到液晶屏刷新的节奏和串口帧格式,代码都是能直接粘进工程编译的版本,参数为什么这么取也一并写清楚,方便你按自己的量程改。
2. 系统拆解:一条从分压电阻到串口帧的数据链
先把整条链路在脑子里跑一遍,再动手接线,后面调 bug 的效率会高很多。模拟电压进来,经过分压网络落到 ADC 引脚的合法范围内,ADC1 的规则通道按固定采样时间把模拟量量化成 12 位数字,DMA 把这个结果从数据寄存器搬到内存数组,主循环里做滤波和换算,最后分两路出去:一路格式化字符串刷新液晶显示,一路用printf通过串口发给上位机。整条链路是单向的,没有中断回调,也没有复杂的任务调度,主循环一个大while就够,这也是它适合做入门骨架的原因。
2.1 硬件链路与引脚分配
被测电压能不能直接进 ADC,先看量程。STM32F103 的 ADC 参考电压通常就是 3.3V 的 VDDA,输入超过这个值会打满甚至损伤引脚,所以只要被测信号可能超过 3.3V,就必须先分压。常见的做法是两只等值电阻串起来做 1:1 分压,中点接 ADC,这样量程翻倍到 0~6.6V,换算时乘个 2 就行,电阻取 20kΩ 和 20kΩ 也顺便把源阻抗压在了 ADC 能接受的范围里。
| 功能 | 外设 | 引脚 | 说明 |
|---|---|---|---|
| 被测电压输入 | ADC1_IN0 | PA0 | 配置为模拟输入,接分压中点 |
| 串口发送 | USART1_TX | PA9 | 115200-8-N-1 |
| 串口接收 | USART1_RX | PA10 | 预留接收上位机命令 |
| 液晶数据线 | GPIOB | PB0~PB3 | 4 线模式,对应 D4~D7 |
| 液晶控制线 | GPIOB | PB4~PB6 | RS / RW / EN |
分压中点最好并一只 0.1μF 电容到地,和 ADC 的采样保持电容一起构成一个低通,能把高频干扰滤掉一大半。这一步省了,后面滤波代码怎么加都会觉得数据"发飘"。另外 VDDA 和 VSSA 之间的去耦电容别省,参考电压抖一点,满量程 4095 的换算结果就跟着抖。
液晶屏这边,字符型 1602 和 SPI 接口的 TFT 都有人用。1602 便宜、时序简单、四线模式只占 7 个 IO,用来显示一行电压足够了;TFT 能画曲线但驱动代码量大好几倍。下面的代码以 1602 为主线,换成 TFT 只需要把LCD_ShowString换成你屏的字符串函数,主循环结构不动。
2.2 工程结构与初始化顺序
工程按功能分目录,别把所有.c堆在一个文件夹里,后期加通道加协议会乱成一锅粥。我一般按这个结构放:
Project/ ├── Start/ 启动文件、system_stm32f10x.c ├── Library/ 标准外设库源码 ├── Hardware/ │ ├── adc.c / adc.h ADC + DMA 采样与滤波 │ ├── lcd1602.c / .h 液晶驱动 │ └── usart.c / usart.h 串口初始化与重定向 ├── User/ │ ├── main.c 业务主循环 │ └── stm32f10x_it.c 中断服务函数 └── Output/ 编译产物初始化的顺序有讲究,不能随便挪。先开时钟,因为所有外设寄存器写之前时钟必须使能;再配 GPIO,模拟输入引脚这一步尤其别漏;然后是串口和液晶,这两个属于"输出通道",早点初始化好在后面出错时能打日志;最后才是 ADC 和 DMA,并且 ADC 的校准必须放在ADC_Cmd使能之后、启动转换之前。顺序写反了最典型的症状就是第一次读出来的值永远是 0 或者满量程。
int main(void) { /* 1. 系统时钟:72MHz,ADC 分频后 12MHz,不超 14MHz 上限 */ SystemInit(); /* 2. 输出通道先就位,后面出问题能立刻打印出来 */ USART1_Init(115200); LCD1602_Init(); /* 3. 采样通道最后初始化,内部自带校准流程 */ ADC_DMA_Init(); printf("adc demo start\r\n"); while (1) { /* 业务循环见第 4 章 */ } }SystemInit已经在启动文件里被调用过,这里显式写一遍是为了让读者知道时钟是在哪一步定的。串口放在液晶前面,是因为液晶初始化里如果有延时死等,串口还能先吐一行启动日志,方便判断程序到底跑到哪了——这个习惯在后面调 DMA 不搬运的时候救过我好几次。
3. ADC 采集模块:单通道连续采样加 DMA 搬运
ADC 这块是整个项目里最容易写出"能跑但数据不能信"的部分。核心就三件事:时钟别超、采样时间给够、数据搬运别和主循环打架。前两件决定单次转换的精度,第三件决定你读到的是不是一个完整快照。
3.1 时钟、采样时间与分辨率怎么定
STM32F103 的 ADC 时钟由 PCLK2 分频得到,手册给的硬上限是 14MHz。系统跑 72MHz 时,分频系数取 6 得到 12MHz,是常用配置;取 4 就是 18MHz,超了,转换结果会莫名其妙地偏差几个 LSB,这种问题查起来非常折磨人,因为它不是完全错,只是不准。
采样时间决定采样保持电容充电够不够。转换总时间等于采样周期加 12.5 个固定周期,采样时间选 55.5 周期时,一次转换约(55.5+12.5)/12MHz ≈ 5.67μs,理论采样率接近 176kHz,对电压监测这种场景富余得离谱。源阻抗越大,采样时间越要给长:分压电阻用 20kΩ 这个量级,55.5 周期是稳妥选择;要是前端挂了 100kΩ 以上的电阻,建议直接上 239.5 周期,宁可慢也别不准。
分辨率固定 12 位,右对齐,0~4095对应0~VDDA。换算时用 4095 还是 4096 是个老话题:4095 是能实际读到的最大值,用它做分母在满量程附近更贴合;4096 是理想量化级数。这个项目的误差主要来自参考电压和分压电阻精度,两者差的那点可以忽略,我用 4095。
3.2 ADC + DMA 初始化代码逐段讲
不开 DMA 也能跑,主循环里轮询ADC_GetConversionValue就行。但只要液晶刷新和串口打印占掉一点时间,采样间隔就不固定了,你想做固定采样率的时间序列根本没戏。开 DMA 循环模式之后,ADC 自己转自己的,缓冲区里永远是最新的 32 个点,主循环随时取。
/* adc.c —— ADC1 通道 0 配 DMA1 通道 1 循环搬运 */ #define ADC_BUF_LEN 32 volatile uint16_t g_adc_buf[ADC_BUF_LEN]; void ADC_DMA_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); /* PA0 必须是模拟输入,配上拉或浮空都会把分压点拉偏 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); /* DMA:外设地址固定为 ADC1->DR,内存地址自增,半字宽度,循环模式 */ DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)g_adc_buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = ADC_BUF_LEN; 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); DMA_Cmd(DMA1_Channel1, ENABLE); /* ADC:独立模式、单通道、连续转换、软件触发 */ ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); /* 55.5 周期采样,匹配 20k 量级的源阻抗 */ ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); /* 校准必须在 ADC 使能之后、启动转换之前做 */ ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); }几个参数值得单独拎出来说。DMA_Mode_Circular是循环模式的关键,缓冲区写满 32 个点之后自动回到开头继续写,不需要主循环干预;换成DMA_Mode_Normal,32 个点写完就停了,读到的永远是同一段旧数据,现象是电压值卡死不动。ADC_ContinuousConvMode打开后 ADC 转换结束立刻启动下一次,配合 DMA 就是一台不停运行的采样机。ADC_NbrOfChannel设 1 表示规则组只排了一个通道,后面要做多通道,这里改成通道数、打开ScanConvMode、缓冲区改成二维或者按槽位排布。
用 HAL 库的话,对应的是HAL_ADC_Start_DMA配合HAL_ADC_Start和HAL_ADCEx_Calibration_Start,配置项一一对应,DMA_CIRCULAR这个模式名都不带改的。校准别偷懒,跳过校准那个while循环,读数会整体偏个十几 mV,而且是温漂性质的偏,你很难判断是硬件问题还是软件问题,最后变成黑匣子。
3.3 从原始码值到电压值:换算与两级滤波
拿到 32 个原始码值以后,别直接除以 32 求平均就完事。ADC 噪声不全是高斯分布,偶尔会冒一两个离群点,均值滤波对这种尖峰几乎没抵抗力,一个坏点就能把结果拽偏好几个 LSB。工程上常见做法是中值加滑动平均两级处理:先排序取中间值干掉离群点,再对一批中间值取平均压低随机噪声。
/* 对 g_adc_buf 做一次中值 + 均值,返回滤波后的原始码值 */ uint16_t ADC_GetFilteredValue(void) { uint16_t tmp[ADC_BUF_LEN]; uint16_t sum = 0; uint8_t i, j; /* 拷贝一份再排序,避免长时间占用 DMA 正在写的缓冲区 */ for (i = 0; i < ADC_BUF_LEN; i++) tmp[i] = g_adc_buf[i]; /* 冒泡排序,32 个点足够快,别在这上面过度优化 */ for (i = 0; i < ADC_BUF_LEN - 1; i++) for (j = 0; j < ADC_BUF_LEN - 1 - i; j++) if (tmp[j] > tmp[j + 1]) { uint16_t t = tmp[j]; tmp[j] = tmp[j + 1]; tmp[j + 1] = t; } /* 掐头去尾各 25%,剩下的取平均,兼顾去尖峰和平滑 */ for (i = ADC_BUF_LEN / 4; i < ADC_BUF_LEN * 3 / 4; i++) sum += tmp[i]; return sum / (ADC_BUF_LEN / 2); } /* 码值换算成被测端电压,分压比 0.5 所以乘 2 */ float ADC_ToVoltage(uint16_t raw) { return (float)raw * 3.3f / 4095.0f * 2.0f; }ADC_ToVoltage里三个浮点常量都带f后缀,是为了避免默认的 double 参与运算。F103 没有硬件 FPU,double 运算走软件模拟,一次转换多花几十微秒,在主循环里高频调用能明显感觉到液晶刷新变卡。这个细节在 72MHz 的 M3 上会有血泪教训,尤其是你后面想加 FFT 或者 PID 的时候。
掐头去尾的比例可以调。25% 是对付偶发尖峰,如果你前端干扰特别大,可以放大到 30%;如果信号本身变化很快(比如测的是 PWM 输出),这个滤波就太重了,会明显滞后,那就换成指数滑动平均v = v*0.9 + new*0.1,牺牲一点平滑度换响应速度。
4. 显示与串口:两个输出口的刷新节奏
数据算出来之后要分两路走。液晶屏是人看的,刷新太快没必要还会闪;串口是机器看的,要考虑上位机怎么解析、会不会粘包。这两路的节奏完全不一样,混在一起写是新手最常见的结构问题。
4.1 液晶刷新的策略:只改变化的字符
1602 的刷新有个坑:整屏清空再重写,会看到明显的白闪。正确做法是定位光标、覆盖写同样长度的字符串,让不变的字符保持原样。更省事的做法是先在内存里拼好一帧字符串,跟上一帧比一下,一样就不刷。
/* 只在字符串变化时才写屏,避免高频刷新导致的闪烁 */ void LCD_ShowVoltage(float volt) { static char last[17] = {0}; char buf[17]; sprintf(buf, "V=%.3fV ", volt); /* 尾部补空格,覆盖上一次的残留 */ if (strcmp(buf, last) != 0) { LCD1602_SetCursor(0, 0); LCD1602_WriteString(buf); strcpy(last, buf); } }尾部那几个空格不是凑数,是必须的。上一次显示V=3.300V,这次是V=2.500V,长度一样还好;一旦数值变成V=12.345V又回落到V=3.300V,不补空格末尾就会留一截旧字符,屏幕上出现V=3.300V5V这种鬼东西。同理,%.3f的小数位数要跟你实际有效精度匹配:12 位 ADC 加 1:1 分压,理论分辨率是6.6/4095 ≈ 1.6mV,显示三位小数是合理的,显示五位就是自欺欺人。
刷新频率上,主循环里每 100ms 调一次LCD_ShowVoltage足够了。液晶屏本身响应时间是毫秒级,刷太快人眼也看不出差别,反而把主循环时间占满。用SysTick或者简单的计数分频都好,别用delay_ms死等——那个延时函数会阻塞整个循环,串口发送也跟着卡。
4.2 串口重定向与帧格式设计
printf能用是标准库重定向的功劳,本质是把fputc换成往 USART 数据寄存器写字节。MDK 工程里要记得勾上 MicroLIB,否则标准库的printf会拖进一大堆用不到的东西,甚至直接卡死。
/* 重定向 fputc,让 printf 直接走 USART1 */ int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, (uint8_t)ch); return ch; } /* 串口初始化,115200-8-N-1,开接收中断备用 */ void USART1_Init(uint32_t baud) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); /* TX 复用推挽,RX 浮空输入,这两个模式配反了就是只能发不能收 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 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 = baud; 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); USART_Cmd(USART1, ENABLE); }发送格式建议一开始就带标识,别只吐一个裸浮点数。文本帧调试方便,二进制帧省带宽、好解析。这个项目数据量小,文本帧更合适:
/* 主循环里的发送节奏:100ms 一帧,带序号和时间戳占位 */ static uint16_t seq = 0; printf("$VD,%u,%.3f\r\n", seq++, volt);$VD是帧头,%u是自增序号,%.3f是电压值,\r\n是帧尾。上位机按\r\n切分,按逗号取值,序号用来判断有没有丢帧。这个格式看起来土,但胜在串口助手直接能看、Python 脚本两行就能解析,比一上来就搞自定义二进制协议靠谱得多。
while (1) { uint16_t raw = ADC_GetFilteredValue(); float volt = ADC_ToVoltage(raw); if (++tick >= 100) { /* tick 由 SysTick 每 1ms 自增 */ tick = 0; LCD_ShowVoltage(volt); printf("$VD,%u,%.3f\r\n", seq++, volt); } }主循环结构就这么点东西。tick用 SysTick 中断自增,主循环只做判断不阻塞,采样、显示、发送三条线互不干扰。
5. 避坑排查:五个把电压显示成玄学的现场
5.1 末位一直在跳,小数点后第三位按不住
现象:电压值稳定在某个数附近,但第三位每隔几十毫秒换一次,万用表测同一个点却是稳的。
原因:一是没做滤波,单次转换的噪声直接上了屏;二是参考电压不稳,VDDA 去耦电容缺失或者走线太长;三是分压中点没并电容,把环境干扰全引进来了。
解决:先把第 3.3 节的中值加均值滤波挂上,一般能压住 80% 的跳动;再检查 VDDA 到地那两只 10μF 加 100nF 电容有没有焊;最后在分压中点对地并 100nF。三步做完还跳,那就是前端信号源本身在动,跟代码无关。
5.2 串口助手收到的第一帧是乱码
现象:上电后第一行显示乱码,后面的数据全正常。
原因:程序先printf后初始化串口,或者串口初始化完成后第一个字节还没发完,MCU 就开始下一轮配置,导致起始位被拉长;还有一种情况是上位机波特率跟代码里写的差一点,115200 对 9600 当然是全乱。
解决:把printf挪到USART1_Init之后,初始化里加一句while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);等发送完成。乱码如果持续存在,先怀疑波特率,用示波器量一下 TX 上一个位的宽度,反推实际波特率,比盲猜快得多。
5.3 ADC 读数恒为 4095 或者恒为 0
现象:不管怎么转电位器,读出来永远是满量程或者 0。
原因:4095 通常是引脚被拉到了 VDDA,多半是 GPIO 配成了上拉输入而不是模拟输入;0 则是通道号写错、分压电阻没焊、或者 DMA 根本没启动,缓冲区里全是初值。
解决:先查GPIO_Mode_AIN有没有写对,这一条占了一半以上;再确认ADC_RegularChannelConfig里的通道号和实际引脚对得上,PA0 对应ADC_Channel_0,别拿 PB0 的通道号往 PA0 上套;最后在ADC_DMA_Init末尾打断点,看DMA_Cmd是不是真的执行了。
5.4 液晶第一行显示方块或者乱码
现象:上电后屏上出现一排黑方块,或者显示的内容全是乱码字符。
原因:1602 上电后内部复位需要一段时间,初始化延时给不够;四线模式下第一次写0x03的时序要发三次,少发一次就进不了 4 位模式;另外 RW 引脚如果一直拉低写、忙标志没判,后续指令会被吞掉。
解决:上电先delay_ms(50)再开始初始化,别心疼这 50ms;四线模式的初始化序列严格按0x33 → 0x32 → 0x28 → 0x0C → 0x06 → 0x01走,顺序错一步就是黑屏;如果 RW 接地省 IO,每条指令后补一个几微秒的短延时替代忙检测。
5.5 显示值偶发跳变到明显不合理的数
现象:99% 的时间显示正常,偶尔蹦一个 0.000V 或者 6.500V,一闪而过。
原因:这是典型的 DMA 读写竞争。主循环拷贝g_adc_buf的时候,DMA 正好写到了同一个槽位,拷出来的半新半旧;或者滤波函数里直接对g_adc_buf原地排序,把 DMA 正在写的目标区改乱了。
解决:先把缓冲区声明成volatile,让编译器别做过度优化;再按 3.3 节那样先整体拷贝到局部数组再处理,拷贝时间远小于一次 DMA 搬运动作,实际撞上的概率极低;要求更严的话上双缓冲,DMA 写 A 区的时候主循环读 B 区,靠传输完成中断切换。
6. 二次开发:从单通道电压表扩成带阈值报警的监测节点
这套代码真正的价值在于骨架好改。想扩成四通道,改动集中在三处:ADC_InitStructure.ADC_ScanConvMode改成ENABLE,ADC_NbrOfChannel改成 4,缓冲区从一维扩成按通道排布的数组,每个通道占固定槽位。规则组的转换顺序由ADC_RegularChannelConfig的 rank 参数决定,第几个 rank 就落在第几个槽位,对应关系写死之后,取g_adc_multi[ch]就行,不用再算偏移。
/* 四通道循环采样:rank1~rank4 依次对应 ch0/ch1/ch4/ch5 */ uint16_t g_adc_multi[4]; ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 3, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 4, ADC_SampleTime_55Cycles5);阈值报警加在换算之后、显示之前:每个通道配一个上限和下限,超了就点亮对应 LED、蜂鸣器短鸣,同时在串口帧里多带一个状态位,比如$VD,12,3.301,1,0,0,0末尾四个 0/1 就是四路状态。上位机只要判断这个位,不用自己再算一遍阈值,逻辑收敛在设备端更省事。阈值本身建议放 Flash 的最后一页,上电读一次,配合串口接收命令能在线改,比每次重新烧录友好得多。
协议要升级的话,把文本帧换成0xAA 0x55 + 长度 + 命令字 + 数据 + 校验和的二进制帧。校验和用累加取低字节就够,别上 CRC16,这个数据量下纯属给自己找麻烦。切换的时候注意上位机要做兼容,我一般是两条协议并存一段时间,用帧头区分,确认新链路上位机解析稳定了再关掉旧的。
最后是校准。上线前拿个可调电源,分别在 0.5V、1.65V、3.0V 三个点各读十次取平均,跟万用表值对比,把三个点的偏差记下来。如果偏差是固定值,做一次线性补偿就够了;如果偏差随电压变化,那就是分压电阻精度问题,老老实实换 1% 电阻。从那以后我每次做完这类采集项目,都会强制走一遍这三点比对,不跑完不敢往设备上装——很多时候代码没问题,错的是那两只五毛钱的电阻。希望帮到你。
本文还有配套的精品资源,点击获取