Bug 故事:两个 DAC 通道并联,我改了 5 个地方才修好(改了5个地方才修好)
tags: 嵌入式,调试,DAC,SPI,多通道,Bug故事 category: 嵌入式 type: original series: debug-troubleshoot
Bug 故事:两个 DAC 通道并联,我改了 5 个地方才修好(改了5个地方才修好)
一句话: 为了输出更大的电流,把 DAC 的两个通道并联使用。写完通道 A 之后忘记同步写通道 B,导致只有一半电流输出。这个 bug 藏在 5 个代码位置里——初始化、设定值、递增、递减、协议解析——花了三个小时排完。
系列文章:《嵌入式调试排错》 — 编译错误、看门狗、DAC踩坑、协议对齐——真实Bug故事+排查方法论
📄 一个宏搞定 📄 全网独家解决方案 ✅ 改了5个地方才修好 📄 15条指令找出7处错误
背景:为什么要并联
电路里需要驱动一个负载,最大 600mA。但单片 DAC 每个通道只能输出 300mA。
硬件同事的办法:把两个 DAC 通道的输出引脚焊在一起,让它们并联供电。CH3 出 300mA,CH4 也出 300mA,合起来 600mA。
原理没问题,但固件里埋了一个坑。
坑:写完通道 A,忘了通道 B
这个 DAC 是通过 SPI 总线控制的。设定电流值时,写一个寄存器就完事:
// 单通道模式(300mA 量程) void DAC_SetCurrent(uint8_t u8Channel, uint16_t u16Code) { SPI_CS_Low(); SPI_Write(u8Channel, u16Code); // 通道 A SPI_CS_High(); }当 CH3 和 CH4 并联使用时,你脑子里知道"它们是同一个东西"。所以写代码的时候自然就写成:
// 并联模式 — 初始版本(有 bug) void SOA_SetCurrent(uint16_t u16Code) { SPI_CS_Low(); SPI_Write(CH3, u16Code); // 写完通道 A // 忘了写通道 B! SPI_CS_High(); }结果:只有通道 A 在输出电流,通道 B 还在输出上次的值。负载只拿到一半电流。
这个问题本身不难发现——如果代码只有一个地方写电流值的话。
为什么修了 5 个地方
一个真实的嵌入式项目里,设定电流值不是只有一个入口的。我翻了一遍代码,发现电流值可以从这几个地方被改变:
| 位置 | 场景 |
|---|---|
| 1 | 初始化— 上电默认值 |
| 2 | 直接设定— 串口发命令"设电流 = XXX" |
| 3 | 递增— 串口发命令"电流 +10" |
| 4 | 递减— 串口发命令"电流 -10" |
| 5 | 协议批量写入— 上位机一口气写多个参数 |
这 5 个位置,每个都只写了通道 A,没写通道 B。
修法是一样的——每个位置加一行写通道 B:
// 修好之后 SPI_Write(CH3, u16Code); // 通道 A SPI_Write(CH4, u16Code); // 通道 B — 一个字节都不能少修完第 1 个位置,用串口直接设定,电流对了。修完第 2 个,递增也对。修到第 5 个——上位机批量写入——才发现还有一个遗漏。
5 个位置全修完,三个小时过去了。
教训:怎么避免这种 bug
1. 封装,不要撒胡椒
如果一开始就把"并联写入"封装成一个函数:
void DAC_WriteDualChannel(uint16_t u16Code) { SPI_CS_Low(); SPI_Write(CH3, u16Code); SPI_Write(CH4, u16Code); SPI_CS_High(); }然后所有地方只调这个函数——bug 根本不存在。
撒胡椒式地到处写 SPI_Write,出了问题只能在每一处补。
2. 不要靠脑子记硬件细节
"CH3 和 CH4 是并联的"——这个信息只存在脑子里,不在代码里。过三个月换个同事维护,他看到SPI_Write(CH3, code)会自然认为"这就够了"。
用宏定义把这个关系显式化:
#define DAC_CH_SOA_A CH3 #define DAC_CH_SOA_B CH4 #define DAC_SOA_IS_DUAL 1 // 标记:这个通道是并联的3. 并联通道应该强制同步
最彻底的修法:在 DAC 驱动的初始化时,标记哪些通道是并联的。写任意一个时,自动同步另一个。这样上层代码完全不需要知道并联这件事:
// 驱动层 — 自动同步并联通道 void DAC_WriteChannel(uint8_t u8Ch, uint16_t u16Code) { SPI_CS_Low(); SPI_Write(u8Ch, u16Code); // 查同步表:这通道有没有并联的兄弟? uint8_t u8Partner = g_au8SyncPartner[u8Ch]; if (u8Partner != 0xFF) { SPI_Write(u8Partner, u16Code); // 自动同步 } SPI_CS_High(); }硬件细节封装在驱动层,上层代码完全不用关心。
总结
| 教训 | 一句话 |
|---|---|
| 封装 | "并联写入"应该是一个函数,不是到处撒的 SPI_Write |
| 显式化 | 硬件细节写在宏里,不要只存脑子里 |
| 同步机制 | 并联关系应该在驱动层自动处理,上层无知 |
这个 bug 不是技术多难,而是一个硬件细节泄露到了 5 个代码位置。封装做好的话,它根本不会发生。
实测对比:封装前: 5处代码各自写SPI_Write | 封装后: 1个函数统一处理,bug永久消灭
有用的话点个收藏,下次调试直接用。有问题欢迎评论区交流,看到了都会回。
下一篇:《一个宏搞定》——编译错误、看门狗、DAC踩坑、协议对齐——真实Bug故事+排查方法论
