当前位置: 首页 > news >正文

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

http://www.jsqmd.com/news/1139295/

相关文章:

  • 《动手学深度学习》PyTorch 版 vs MXNet 原版:5 大核心章节代码对比分析
  • 玩转控件:封装个带图片的Label控件
  • ARP(地址解析协议)与NDP(邻居发现协议)
  • Unity手游逆向:Assembly-CSharp.dll加密识别与内存转储实战指南
  • LLM嵌入运动规划内核:语义约束自动转数学表达
  • 计算机毕业设计之jsp汽车养护网站的设计与实现
  • 2026企业级Agent产品推荐,三大选型标准与5款客服Agent横评
  • 从花屏到丢帧:视频设备晶振选型 6 大致命误区,90% 研发都踩过
  • 【从零构建分布式在线评测系统】二、Judge0服务器上的心跳程序
  • OPA191 非反相放大器 PCB 布局:3 项关键优化将噪声降低 20dB(附对比图)
  • Windows Server 2008 R2 IIS 低权限 WebShell 提权实战
  • ArcGIS Pro 3.6.3学习版安装全解析:环境、许可与排错
  • Vue导航守卫学习
  • 煤炭井下通信链路隐患排查,GN-W10A 测试设备适配矿山复杂网络环境
  • 为什么说Notepad--是你跨平台文本编辑的最佳选择?
  • 如何创建openEuler新语言翻译团队?完整申请与审批流程指南
  • CryptoJS v4.0.0 模块化升级与实战:JavaScript加密库核心用法解析
  • 【Springboot毕设全套源码+文档】基于springboot蔬菜超市系统的设计与实现(丰富项目+远程调试+讲解+定制)
  • 秋冬服装营销缺创意?先知大模型重塑内容生产力
  • 2026最新微信小程序怎么做自己的店铺全流程
  • AI合规工具:AI合规审计工具的选型与使用指南
  • TLS证书撤销机制深度解析:从CRL、OCSP到Nginx实战配置
  • AI-Shoujo HF Patch完整安装与配置指南:从模组整合到创作平台搭建
  • AutoGLM安卓端侧部署实战:从ADB到本地大模型运行
  • 城市污水管网监测的主要内容
  • 在后台服务中使用 Scoped 服务,为什么总是报错?
  • 从闭源平台的数据困局,到搭建真正属于自己的 AI 知识库
  • CTF Web入门:从HTTP响应包中寻找Flag的侦察思维与实战方法
  • 影刀RPA新手教程:界面布局完全指南——指令区、画布区、属性区深度解析与使用技巧
  • MD5加密Java代码?别找了,就这30行直接抄