ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F030+DS18B20多点温度采集:时序、ROM搜索与CRC校验避坑指南

STM32F030+DS18B20多点温度采集:时序、ROM搜索与CRC校验避坑指南

简介:这是一套基于STM32F030的多点温度采集系统完整源码,适合物联网与嵌入式开发者参考,用于实现远程环境温度监测与云平台数据上报。系统采用DHT20传感器采集温度,通过移远BC260Y-CN模块以NB-IoT技术连接OneNet平台,并加入OLED本地显示功能。压缩包共24个文件,包含10个C源码文件、10个头文件、2个Markdown文档及其他配置文件,整体仅40KB,结构紧凑易读。已有39人下载学习。源码涵盖了硬件初始化、外设驱动、通信协议及系统调度等模块,可直接移植或二次开发,对理解NB-IoT组网、MQTT参数配置和嵌入式软件分层设计均有帮助。

1. STM32F030 做多点温度采集:为什么这个组合能打,坑也最多

一台冷链箱要盯 8 个测温点,某个货架要在 3 米线缆上挂 6 路温度传感器,成本压到几十块,还得能在量产时复制——这种需求一出现,STM32F030 加单总线温度传感器就是大多数工程师的第一反应。F030 是入门级 M0 内核芯片,主频 48MHz,价格便宜到可以批量铺;单总线协议只需要一根 GPIO 就能串起几十个节点,每个节点还有唯一 64 位 ID,天然适合多点采集。但真把这套组合跑起来,你会发现坑全在代码细节里:微秒级时序容差、ROM 搜索算法、CRC 校验、上拉电阻选型,任何一个没处理好都会让数据变成 85℃ 或直接搜不到设备。这篇笔记把从零到跑的完整路径按可复现的代码讲清楚,适合预算有限、又不想踩一遍时序坑的开发者。

2. 先选型:单总线多点还是 ADC 多通道,F030 的资源卡点在哪

2.1 DS18B20 单总线:一条线串起 8 路,把成本压到最低

多点温度采集最常见的两条路:一条是单总线数字传感器,另一条是 NTC 热敏电阻走 ADC 多通道。先聊单总线,因为它最贴合标题里的「多点」两个字。

单总线的物理拓扑很简单:主机一个 GPIO,一根数据线,一个 4.7k 左右的上拉电阻到 3.3V,所有传感器并联在这根线上。每个传感器内部有唯一 64 位 ROM 编码,主机通过搜索命令把线上的设备一个个枚举出来,之后用匹配命令点名读取。这套机制决定了它天然适合多点,不需要每个点单独占用一个 ADC 通道,IO 资源只花一个引脚。

单总线温度传感器常见的型号精度标称在 ±0.5℃(-10℃ 到 +85℃ 范围内),分辨率可以通过配置寄存器在 9 到 12 位之间切换,默认 12 位,也就是 0.0625℃ 每 LSB。12 位分辨率下完成一次转换的典型时间是 750ms,这个速度对温度巡检完全够用,但如果你要跟踪快速温变,它就不合适了。

成本是另一个优势。不算 PCB,传感器本身单价压到几块钱以内,8 路也就一杯咖啡钱。对比 NTC 加运放加精密电阻的方案,省下来的不只是物料,还有每个通道的标定工时。单总线方案还有个隐性好处:传感器坏了不连累主机,替换后新器件的 ROM 编码变了,搜索逻辑重新枚举即可。

2.2 NTC 多通道:F030 的 ADC 没有硬件校准,VREFINT 才是准头

另一条路是 NTC 热敏电阻配 F030 内置 ADC。F030 的 ADC 是 12 位,多通道采集时逐通道扫描,理论上一路一个 IO 就能测一路温度,采样速度比单总线快几个数量级。但这里有个绕不开的卡点:F030 这类 M0 芯片的 ADC 没有硬件校准电路,供电电压直接决定采样结果的绝对精度,而板子上 3.3V 往往不是精确的 3.3V。

F030 出厂时在信息块里存了一个内部参考电压的 ADC 采样值,地址在 0x1FFFF7BA。这个值是芯片在 3.3V、30℃ 条件下校准得到的,12 位 ADC 对它采样的结果会落在某个固定值附近。实际使用时读出当前 VREFINT 的 ADC 值,用「3.3 × 校准值 ÷ 当前采样值」就能反推出真实供电电压,再用这个真实电压去换算 NTC 分压结果。很多开发者不知道这一步,直接拿 3.3V 去算 NTC 阻值,测出来的温度偏出好几度还找不到原因。

NTC 本身的换算也不省心。常见做法是 NTC 与固定电阻串联分压,ADC 采到分压点电压后,先算出 NTC 当前阻值,再套 B 值公式换算温度:

1/T = 1/T0 + (1/B) * ln(R/R0)

T0 是 25℃,R0 是 NTC 在 25℃ 下的标称阻值,B 值由器件手册给出。公式不复杂,但 B 值本身有离散性,NTC 的一致性也一般,想做到 ±0.5℃ 以内就得逐点标定,量产时这是不小的工作量。

2.3 选型对比:几路、多远、多快,三个问题定方案

两条路各有边界,选型时先问三个问题:几路、多远、多快。

维度单总线方案NTC + ADC 方案
路数扩展一根线可挂数十个,不受 ADC 通道限制受 ADC 通道数限制,通道多了要加模拟开关
线缆长度3 米以内稳妥,超过要降速率或加强上拉线缆阻值会影响分压比,长线要四线制或差分
采样速度12 位分辨率单次转换约 750msADC 采样毫秒级,可做同步快速采集
精度一致性器件出厂校准,互换性好依赖 B 值精度和逐点标定
单路成本每路一个数字传感器,价格略高NTC 加电阻很便宜,但标定工时贵

我一般会这样判断:8 路以内、线不长、对采样速度没要求、想快速交付,选单总线;需要同步采集多个点的温升曲线,或者要 10ms 级采样周期,老老实实上 NTC 加 ADC,把 VREFINT 校准和标定流程做扎实。这篇笔记后面的代码和踩坑全部围绕单总线方案展开,因为它的坑更隐蔽,也更容易在代码层面讲透。

3. 单总线驱动代码:从微秒延时到 ROM 搜索,参数全解

3.1 48MHz 主频下的微秒延时:先校准再谈时序

单总线协议对时序的要求是微秒级的,所有读写操作都建立在精确延时上。F030 主频 48MHz,一个空循环周期大约 20 多纳秒,但 M0 内核没有 M3/M4 的 DWT 周期计数器,SysTick 又往往被系统计时占用,最省事的做法就是裸循环延时。

// 简易微秒延时:系数依赖主频和编译优化等级,必须实测校准 static void delay_us(uint32_t us) { // 48MHz + O2 优化下的经验系数,换成 O0 或换编译器要重新标定 uint32_t cnt = us * 12; while (cnt--) { __NOP(); } }

这段代码里唯一的魔法数字就是 12。不同优化等级下,O0 生成的代码比 O2 慢不少,同样的延时循环实际耗时差出 30% 以上。我见过有人把 F103 的延时函数直接搬过来用,F103 主频 72MHz,F030 主频 48MHz,一样的循环次数实际时间差了 1.5 倍,单总线时序全乱,读回来的数据全是 0xFF。

标定方法很简单:让 GPIO 翻转,用逻辑分析仪或示波器量一下实际高电平时间,对比期望值调系数。比如写一个延时 100us 的循环,示波器量出来只有 80us,系数就乘 1.25。这个步骤省不得,属于「玄学参数」里最影响成败的一个。

3.2 复位、写时序与读时序:窗口参数和它们的容差

单总线的时序分三块:复位、写位、读位。先看复位,主机拉低 480us 以上释放,传感器在释放后 60 到 240us 内拉低总线作为存在脉冲。

// 单总线复位:返回 1 表示检测到设备,0 表示无设备或总线异常 uint8_t ow_reset(void) { uint8_t presence; GPIOB->BRR = (1 << 0); // 拉低数据线,PB0 delay_us(480); // 复位脉冲,必须大于 480us GPIOB->BSRR = (1 << 0); // 释放总线 delay_us(70); // 等待存在脉冲窗口 presence = (GPIOB->IDR >> 0) & 1; delay_us(410); // 补足复位时序剩余时间 return presence == 0; // 低电平表示设备存在 }

复位脉冲 480us 只要求下限,短了不行,长了没事。存在脉冲的采样点放在释放后 70us 左右,处在 60 到 240us 窗口的前段,给慢速设备留足余量。补足 410us 这个动作容易被忽略,但它保证两次操作之间总线处于正常的空闲高电平状态。

写时序和读时序的窗口更窄。写 1 和写 0 的区别在于拉低持续时间:写 0 要拉低 60 到 120us,写 1 只需拉低 1 到 15us 后释放。

// 写一位:bit=1 时短拉低,bit=0 时长拉低 void ow_write_bit(uint8_t bit) { if (bit) { GPIOB->BRR = (1 << 0); delay_us(6); // 写 1:拉低 6us,远小于 15us 上限 GPIOB->BSRR = (1 << 0); delay_us(64); // 释放余量 } else { GPIOB->BRR = (1 << 0); delay_us(60); // 写 0:拉低 60us,进入有效窗口 GPIOB->BSRR = (1 << 0); delay_us(10); // 释放余量 } } // 读一位:主机拉低 2us 后释放,在 15us 内采样总线电平 uint8_t ow_read_bit(void) { uint8_t bit; GPIOB->BRR = (1 << 0); delay_us(2); // 启动读时隙 GPIOB->BSRR = (1 << 0); delay_us(8); // 释放后等待电平稳定 bit = (GPIOB->IDR >> 0) & 1; // 采样点:约 10us 处 delay_us(50); // 补足读时隙总时长 return bit; }

写时序里最关键的是写 1 的拉低时间不能超过 15us,否则设备会当成写 0。我习惯把写 1 的拉低压到 6us 再释放,留出充足余量。读时序的采样点放在释放后 8us,也就是总时长约 10us 处,这在 15us 采样窗口的中前段。读时序还有个隐形坑:如果主机拉低时间太长,释放后总线恢复高电平需要时间,采到的永远是低电平。

提示:执行这些微秒级时序时必须关中断。SysTick、UART 中断随时可能打断时序,一旦在写 0 的 60us 窗口中间被插一脚,整个字节就废了。时序操作前后用__disable_irq()和__enable_irq()包起来。

3.3 ROM 搜索算法:一条线上找出所有传感器的唯一 ID

多点采集的第一步是把线上所有设备的 64 位 ROM 编码找出来。单总线协议里有一条 SEARCH ROM 命令(0xF0),主机逐位发送读时隙,设备会返回两位:一位是自己的 ROM 位值,另一位是该位的反码。两位合起来有三种情况:01 表示该位为 0,10 表示该位为 1,00 表示线上存在多个设备且该位发生冲突,需要主机决定选哪条分支。

// 搜索总线上所有设备 ROM,存入 rom_list,返回找到的数量 uint8_t ds18b20_search_rom(uint8_t (*rom_list)[8], uint8_t max_devices) { uint8_t rom[8] = {0}; uint8_t last_discrepancy = 0; // 上次搜索中最后一位冲突位置 uint8_t last_device = 0; // 是否已搜索完最后一台设备 uint8_t found = 0; while (!last_device && found < max_devices) { uint8_t discrepancy = 0; // 本次搜索最后一位冲突位置 if (!ow_reset()) break; ow_write_byte(0xF0); // SEARCH ROM 命令 for (uint8_t byte_pos = 0; byte_pos < 8; byte_pos++) { uint8_t byte = 0; for (uint8_t bit_pos = 0; bit_pos < 8; bit_pos++) { uint8_t bit_index = byte_pos * 8 + bit_pos; uint8_t b1 = ow_read_bit(); // 设备 ROM 位值 uint8_t b2 = ow_read_bit(); // 该位反码 if (b1 == 1 && b2 == 1) { return found; // 总线异常,无设备 } uint8_t dir; if (b1 == 0 && b2 == 0) { // 冲突位:按搜索规则选择方向 if (bit_index < last_discrepancy) { dir = (rom[byte_pos] >> bit_pos) & 1; } else if (bit_index == last_discrepancy) { dir = 1; // 上次分歧位置这次走 1 } else { dir = 0; discrepancy = bit_index; // 记录新的分歧位置 } } else { dir = b1; // 该位唯一确定 } byte |= (dir << bit_pos); ow_write_bit(dir); // 把选择写回总线 } rom[byte_pos] = byte; } if (discrepancy == 0) { last_device = 1; // 没有新冲突,搜索结束 } else { last_discrepancy = discrepancy; // 为下一轮搜索更新位置 } // ROM 最后一位是 CRC 校验码,验证通过才算有效 if (crc8(rom, 7) == rom[7]) { for (uint8_t i = 0; i < 8; i++) { rom_list[found][i] = rom[i]; } found++; } } return found; }

搜索算法的核心逻辑在冲突位处理上。第一次搜索时 last_discrepancy 为 0,所有冲突位都走 0 分支,算法会找到 ROM 编码最小的设备。下一轮搜索时,在上一轮最后一个未走 1 的冲突位置改走 1,就能找到编码次小的设备。如此反复,直到某次搜索全程没有出现新的冲突位,说明所有设备都枚举完了。

这里有个容易翻车的地方:冲突位的方向选择必须严格按照「小于上次分歧位沿用旧值,等于上次分歧位走 1,大于上次分歧位走 0」的规则。某开发者把小于和等于的情况合并处理,导致搜索到第 3 个设备时陷入死循环,每次枚举出来的 ROM 都是同一个。另外,CRC 校验不能省,总线上如果有干扰,错误的 ROM 会被存进列表,后续匹配命令永远找不到对应设备。

4. 读温度与上报:CRC 校验、负温度、UART 输出

4.1 启动转换与读暂存器:命令序列别搞反

ROM 搜索只解决「线上有谁」的问题,真正读温度还要走两条命令:0x44 启动温度转换,0xBE 读取暂存器。命令顺序不能反,先转换再读取,转换命令发出后必须等转换完成。

// 读取指定 ROM 设备温度,rom 为 NULL 时跳过匹配命令(单设备快速读) float ds18b20_read_temp(uint8_t *rom) { uint8_t data[9] = {0}; int16_t raw; if (!ow_reset()) return -100.0f; if (rom) { ow_write_byte(0x55); // MATCH ROM,指定设备 for (uint8_t i = 0; i < 8; i++) ow_write_byte(rom[i]); } else { ow_write_byte(0xCC); // SKIP ROM,广播给所有设备 } ow_write_byte(0x44); // 启动温度转换 // 等待转换完成:12 位分辨率典型 750ms delay_ms(750); if (!ow_reset()) return -100.0f; if (rom) { ow_write_byte(0x55); for (uint8_t i = 0; i < 8; i++) ow_write_byte(rom[i]); } else { ow_write_byte(0xCC); } ow_write_byte(0xBE); // 读取暂存器 for (uint8_t i = 0; i < 9; i++) { data[i] = ow_read_byte(); // 读 9 字节:温度 + 告警 + CRC } if (crc8(data, 8) != data[8]) return -200.0f; // CRC 失败哨兵值 raw = (data[1] << 8) | data[0]; // 温度寄存器低 2 字节 return raw * 0.0625f; // 12 位分辨率每 LSB 0.0625℃ }

这段代码里有两个返回值约定:-100.0f 表示总线复位失败,-200.0f 表示 CRC 校验失败。DS18B20 的测温范围是 -55℃ 到 +125℃,-100 和 -200 都在真实量程之外,不会和正常数据混淆。用这种「哨兵值」做错误标记比返回结构体或错误码更直观,上层轮询代码直接判断执行范围即可。

转换等待时间有个优化空间:12 位分辨率下器件手册给出的典型转换时间是 750ms,但这是上限,实际往往更快。对单设备读温度,固定延时最简单;多设备巡检时,固定延时会让每路都等满 750ms,8 路就是 6 秒,后面第 6 章会讲怎么用广播转换把这个时间压下来。

4.2 CRC-8 检查:判错不重读,等于白做

单总线协议里每个设备的 ROM 编码和暂存器数据都带 CRC 校验。CRC 校验不是可选功能,长线环境下一个干扰脉冲就可能让暂存器里的温度数据跳变几位,没有校验你根本不知道数据是错的。

// CRC-8 按位计算:多项式 x^8 + x^5 + x^4 + 1,即 0x31 uint8_t crc8(uint8_t *data, uint8_t len) { uint8_t crc = 0; for (uint8_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x80) { crc = (crc << 1) ^ 0x31; } else { crc <<= 1; } } } return crc; }

按位计算的 CRC 在单总线上足够快,8 字节数据算一遍也就几十微秒。多项式固定是 0x31,初值 0,这个参数不能随意改,否则和器件内部的 CRC 生成器对不上。如果追求速度,也可以改成查表法,256 字节的表在 F030 的 64KB Flash 里毫无压力。

我一般会在 CRC 校验失败后连续重读 3 次,3 次都失败才判定通道异常。单总线在电机启停、继电器吸合这类干扰场景下,偶发单次失败很正常,重读能过滤掉大部分毛刺。但注意重读之间要有至少 10ms 间隔,让总线充分恢复,连续快速重读往往连续失败。

4.3 负温度与格式化输出:int16_t 补码直接乘分辨率就对了

温度寄存器是 16 位有符号补码格式,12 位分辨率下低 4 位是小数部分。处理负温度时最容易出问题的是用了无符号类型承接,导致 -10℃ 变成 65526 然后乘出荒谬结果。

// 格式化输出一路温度,经 UART1 发送 void report_temp(uint8_t channel, uint8_t *rom) { float temp = ds18b20_read_temp(rom); char buf[48]; if (temp > 125.0f || temp < -55.0f) { // 哨兵值范围之外,说明是 CRC 失败或总线错误 sprintf(buf, "CH%02d ERR\r\n", channel); } else { sprintf(buf, "CH%02d %6.2f C\r\n", channel, temp); } // 逐字符发送,避免占用大缓冲 for (char *p = buf; *p; p++) { while (!(USART1->ISR & (1 << 7))); // 等待 TXE 空 USART1->TDR = *p; } }

温度转换的核心就一行:先拼成int16_t再乘 0.0625f。负温度在补码下乘完直接就是负数,不需要额外判断符号位。sprintf输出的浮点位宽用%6.2f,固定两位小数,串口工具解析也方便。换行用\r\n而不是\n,很多串口助手只认前者。

格式化成字符串后逐字符发送,而不是一次性printf,是为了避开 F030 上printf重定向到 UART 时可能出现的缓冲问题。USART1 在 115200 波特率下发送几十个字符只需几毫秒,不会拖慢巡检周期。

5. 多点采集避坑记录:上拉电阻、寄生供电与 85℃ 假数据

5.1 第二路以后全是 85℃:上拉电阻和驱动能力的问题

现象:单设备测试正常,挂上 4 个传感器后,有些通道读回 85℃,有些通道 CRC 校验失败,随机分布但集中在离主机远的设备。

原因:DS18B20 上电后温度寄存器默认值就是 85℃,读回这个值说明设备压根没响应,主机读到了空总线上的 0xFF,CRC 也自然对不上。总线上一堆设备并联后等效电容变大,上拉电阻驱动力不足,远端的设备在读写时隙里电平没有充分翻转。

解决:把上拉电阻从 10k 换成 4.7k,3 米以上线缆再降到 2.2k。F030 的 GPIO 输出驱动能力有限,必要时把速率等级配置成最高。上拉电阻换来换去是这门手艺里最常见的「玄学」操作,实际就是 RC 常数问题,找到示波器上边沿不再缓变的值就对了。

5.2 线一长就 CRC 报错:对线缆电容的敏感度远超预期

现象:1 米内一切正常,线延长到 5 米后读温度开始间歇性失败,时间集中在温度转换完成后的读取阶段。

原因:长线的分布电容让读写时隙的波形边沿变缓,尤其是读时序,主机在 15us 窗口内采样时,总线电平可能还没落到有效的逻辑低。另一个因素是分支线缆的反射,拓扑从星型变成多分支后更严重。

解决:把拓扑改成手拉手的菊花链,避免长分支;给主机侧的数据线串联一个 100Ω 电阻抑制反射;读时序的采样点从 8us 往后挪 2us,避开最陡峭的振铃区。线缆超过 8 米还想要可靠,就得降低分辨率到 9 位或加总线驱动器,不能靠调软参硬扛。

5.3 搜索 ROM 时算法卡死:总线被从机拉死的恢复技巧

现象:上电后第一次搜索 ROM 正常,执行几十次复位后某次搜索返回 0,再次复位也检测不到设备,必须断电重启。

原因:某台设备在复位脉冲来临时没有正确释放总线,或者设备内部状态机被异常时序打乱,一直占用总线。常见诱因是搜索过程中有中断插入,把读时隙拉长到 70us 以上,设备误判成复位信号。

解决:每次复位失败后不要立即重试,把复位脉冲拉低时间从 480us 加到 640us,给设备更长的恢复窗口;连续失败 3 次后延时 50ms 再整体重来。搜索函数里对 b1、b2 同 1 的情况直接返回而不是继续死循环,这个保护必须有,否则总线故障时系统卡在搜索里出不来。

5.4 上电就报 85℃:传感器默认值不能当故障

现象:工装刚上电,尚未执行任何转换命令,第一路温度就上报 85℃。

原因:DS18B20 上电后暂存器复位,温度寄存器默认加载 0x0550,换算出来就是 85℃。这是正常现象,不是传感器坏了,也不是接线问题。很多开发者第一次遇到会误判成硬件故障,折腾半天。

解决:上电后延时 500ms 再发起第一次转换,确保器件内部电源稳定;如果读到的温度恰好是 85℃ 且连续两次一致,主动触发一次转换并等 750ms 重新读取。把 85℃ 当作非法值过滤掉也行,但要注意冬天室温如果恰好到 85℃ 那是传感器真的坏了,判断逻辑要结合连续读数和 CRC。

5.5 中断把时序拉断:M0 没有 DWT,也不能靠 HAL_Delay

现象:系统里加了 UART 打印后,单总线读取开始偶尔失败,打印越频繁失败越明显,裸机跑单总线反而一切正常。

原因:串口中断在读写时隙中间抢占执行,把 60us 的写 0 时序拉成 90us,设备直接识别成复位信号。M0 内核没有 M3/M4 的 DWT 计数器,很多人想用周期计数做精确延时也做不了,只能靠关中断。

解决:所有单总线位级操作放在__disable_irq()和__enable_irq()之间,整个字节读写期间关中断。关中断会导致串口接收丢字节,但温度采集场景对丢一两个串口字符不敏感,丢一个温度字节才是大问题。如果系统里还有对实时性要求高的中断,就把单总线访问改成原子操作,只保护每一位的读写,而不是整个字节。

6. 巡检提速与验证手法:广播转换、等待完成与逻辑分析仪

多路巡检最直观但最蠢的做法是挨个设备发转换命令、每个都等 750ms。8 路就是 6 秒一轮,系统响应慢到没法用。正确的做法是广播一条转换命令,让线上所有传感器并行转换,等一轮转换时间后,再逐个匹配 ROM 读取数据。

// 广播转换:所有传感器并行工作,一轮读取只等一次 750ms void read_all_temps(uint8_t (*rom_list)[8], uint8_t count) { // 广播启动转换,所有设备同时开始 ow_reset(); ow_write_byte(0xCC); // SKIP ROM ow_write_byte(0x44); // CONVERT T // 转换期间总线被拉低,轮询等待所有设备完成 while (1) { // 读一位:设备转换完成后释放总线,主机读到 1 if (ow_read_bit()) break; } // 逐个读取 for (uint8_t i = 0; i < count; i++) { ow_reset(); ow_write_byte(0x55); // MATCH ROM for (uint8_t j = 0; j < 8; j++) { ow_write_byte(rom_list[i][j]); } ow_write_byte(0xBE); // READ SCRATCHPAD // 后续 9 字节读取与 CRC 校验同前 float temp = read_temp_from_scratchpad(); report_temp(i + 1, rom_list[i], temp); } }

广播转换的精髓在第二条命令:转换期间 DS18B20 会把总线拉低,转换完成后释放总线。主机持续发读时隙,读到高电平就说明所有设备都干完活了,不用傻等 750ms。这个技巧把多路读取时间从 N 乘以 750ms 压到 750ms 加逐路读取的几百微秒,8 路巡检一轮不到 1 秒。

验证工作比写代码更值得花时间。新板回来,我先用逻辑分析仪抓三段波形:复位后存在脉冲的位置和宽度、写 1 时隙释放后总线恢复高电平的斜率、读时隙采样点落在窗口的哪个位置。这三段波形没问题,上层的搜索和读取基本不会出幺蛾子。逻辑分析仪采样率至少 20MHz,便宜的逻辑分析仪够用。

每次改板子回来,我都会先看一遍这三段波形再跑程序,这个习惯救过我很多次。波形对了,代码不动;波形变了,优先检查上拉电阻和线缆。时序这类问题,逻辑分析仪上一个周期就看明白了,比盲调参数快得多。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进