Bug 故事:两个 DAC 通道并联,我改了 5 个地方才修好(改了5个地方才修好)
2026/7/7 5:33:23 网站建设 项目流程

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故事+排查方法论

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询