1. 只烧 HEX 跑仿真的调试困境,以及 COFF 到底带来了什么
数码管不亮、串口收不到数据、按键按下去毫无反应——在 Proteus 里做 AVR 仿真的人,几乎都经历过这类"玄学故障"。遇到问题,绝大多数人的第一反应是返回 ICCAVR 改代码:把延时调大一点、把端口方向寄存器再确认一遍、把初始化顺序换个位置,然后编译、回到 Proteus、重新加载文件、再跑一次。改一轮十几分钟,问题还在,人也快崩了。
根子在于:你加载进 Proteus 的是一个.hex文件,而 HEX 里面只有机器码和地址,没有符号名、没有行号、没有变量类型。Proteus 看到它就是一堆字节,它没法告诉你"卡在while(!(UCSRA & 0x20))这一行",只能说"仿真还在跑"。你能做的只有看现象——而现象和代码之间隔着一层黑箱。
Proteus 与 ICCAVR 的联合调试要解决的正是这层黑箱。核心思路一句话就能说清:让 ICCAVR 在编译时额外产出一个带调试信息的 COFF 文件(扩展名.cof),把这个.cof而不是.hex挂到 Proteus 的 MCU 元件上,Proteus 的仿真内核就会读出符号表和行号映射,从而支持源码级断点、单步执行、变量观察、内存与寄存器查看。你不是在"猜",而是在像用真实仿真器一样调试。
这套流程特别适合三类人:一是还在读书、手头没有实物开发板的学生;二是硬件还没打样、想先把逻辑跑通的工程师;三是接手了别人 ICCAVR 老工程、代码看不懂只能靠现象反推的人。它对 AVR 系列(ATmega16、ATmega32、ATmega128 这些 ICCAVR 时代的主力)尤其友好,因为这些芯片的仿真是 Proteus 做得最成熟的一档。
1.1 HEX 与 COFF 的本质差别,决定了你能看到什么
打个比方,.hex好比一份只印了菜名的菜单,而.cof是带食材清单、克数和操作步骤的完整菜谱。菜单能让你知道"有这道菜",菜谱才能让你知道"哪一步放多了盐"。
具体来说,COFF(Common Object File Format)里除了最终机器码,还额外存了这些东西:
| 信息类型 | 在 HEX 里 | 在 COFF 里 | 对调试的意义 |
|---|---|---|---|
| 机器码 | 有 | 有 | 都能跑 |
| 函数符号名 | 无 | 有 | 能在delay_ms里下断点 |
| 源文件路径与行号 | 无 | 有 | 能显示.c源码而不是反汇编 |
| 变量名、类型、作用域 | 无 | 有 | Watch 窗口能按名字看值 |
| 全局/静态变量地址 | 无 | 有 | 能直接看数组元素 |
所以判断"我这套联合调试到底通没通",有个特别简单的土办法:仿真暂停后,Proteus 弹出来的窗口里显示的是 C 源码,还是清一色的汇编指令。是 C 源码,说明 COFF 加载成功;是汇编,说明它退化成了纯机器码调试。
1.2 ICCAVR 编译链路上调试信息是在哪一步丢掉的
ICCAVR 的编译流程是"编译器iccavr出目标文件 → 链接器ilink出可执行文件"。调试信息必须一路保留到最后一步,中间任何一环被关掉,COFF 就变成空壳。
常见的丢失原因有三个。第一,工程选项里只勾了输出 HEX,链接器干脆不生成 COFF,或者生成了一个不含调试段的 COFF。第二,优化等级开得太高,编译器把变量塞进寄存器、把语句重排、把没用的函数整个删掉,行号映射变得名存实亡——文件还在,但对不上号。第三,用了别人给的库文件(.a),库编译时没带调试信息,你在库里下的断点全部落空。
提示:判断 COFF 是不是"空壳",可以直接看编译输出目录的文件大小。同样一个工程,带完整调试信息的
.cof通常比.hex大好几倍。如果两者体积差不多,说明调试信息八成没进去。
1.3 联合调试的完整链路长什么样
把整条链路串起来看,其实只有四个环节:
- 在 ICCAVR 里配置工程,产出带调试信息的
.cof; - 在 Proteus 里把 MCU 元件的 Program File 指向
.cof; - 让两边的时钟频率保持一致;
- 保持源码目录不动,让 Proteus 按 COFF 里记录的路径找到
.c文件。
四个环节里,前三个是显性的,第四个最容易被忽略——它恰恰是"编译明明带了调试信息,Proteus 却还是给我看汇编"的头号元凶。后面会单独讲。
2. ICCAVR 这边需要动的几处工程配置
很多人以为联合调试是 Proteus 单方面的事,把文件拖进去就行。实际上,调试信息是 ICCAVR 生成的,Proteus 只是消费者。源头上没配置好,Proteus 再怎么折腾也没用。这一节把 ICCAVR 侧需要改的地方逐个交代清楚。
2.1 器件选择与工程类型:别用向导默认值直接开干
ICCAVR 启动时会问你是新建工程还是用 Application Builder 向导。调试阶段建议直接建普通工程,因为向导生成的工程会顺带塞进一堆启动代码和库配置,排查问题时多一层干扰。
新建工程后在Project → Options里,Target标签下要确认两件事:
- Device选对具体型号。选
ATmega16和选ATmega32,寄存器地址和中断向量表都不一样,选错了轻则外设不工作,重则程序直接跑飞。 - Target Type选 Application(可执行程序),不要选 Static Library。库文件不会生成 COFF。
器件型号必须和 Proteus 里放的元件完全对应。我见过有人在 ICCAVR 里选 ATmega16,Proteus 里放的是 ATmega32,程序烧进去能跑,但一访问PORTC就出错——因为两个型号的端口映射不同。这种问题在纯 HEX 调试下几乎查不出来,只有源码级调试才容易发现。
2.2 打开 COFF 输出与调试信息开关
在Project → Options的Compiler/Output相关标签里,找到输出格式和调试信息相关的选项:
- 输出格式:选择生成 COFF(很多版本标记为 "COFF" 或 "Both"),确保编译后目录里能同时看到
.hex和.cof; - 调试信息(Debug Information):勾上。这个开关决定编译器是否把符号表和行号信息写进目标文件;
- 优化等级(Optimization):调成 0 或关闭。
不同小版本的菜单名称会有细微差别(有的把这几项放在Linker标签下),但你要找的东西就这三类:输出 COFF、保留调试信息、关闭优化。配好之后编译一次,去输出目录确认.cof文件确实新增了。
2.3 优化等级为什么和源码级调试天然打架
这是联合调试里最需要理解的一处原理。编译器的优化做的事包括:把频繁访问的变量从内存搬到寄存器、把连续几条语句合并、把永远不会执行到的分支删掉、把函数内联展开。
对最终运行结果来说,这些优化是好事。但对调试器来说,它们会破坏"源码行"和"机器指令"之间的一一对应关系。举几个实际会碰到的现象:
- 你给一个局部变量下断点,调试器提示"该符号不存在"——因为它已经被优化进寄存器,内存里根本没有这个变量;
- 你在
for循环第一行设断点,单步一次直接跳到循环外,中间几行被编译器合并掉了; - 单步时源码光标来回跳,因为编译器把不同分支的代码重排到了一起;
- 一个函数被内联后,在这个函数里下的断点永远不命中。
关闭优化之后,上面这些现象基本都会消失。代价是代码体积变大、跑得慢一点——但仿真是个离线环境,慢一点完全可以接受。所以我的习惯是:只要还在调试,优化就一直是 0;等逻辑全通了,要评估代码体积时再开高等级编译一次。
注意:如果一定要在高优化下调试,把关键变量加
volatile修饰,能保住它在内存里的位置。但行号错乱问题没法靠volatile解决。
2.4 时钟频率这个数字,同时决定三处行为
时钟频率是联合调试里最容易被低估的一个参数,因为它同时影响着三个地方:
第一个地方是CPU 实际执行速度。Proteus 里的 MCU 元件有一个 Clock Frequency 属性,它决定了每秒钟仿真多少个时钟周期,进而决定延时的长短。
第二个地方是代码里的时序计算。如果你用的是软件延时循环,比如:
void delay_ms(unsigned int ms) { unsigned int i; while (ms--) for (i = 0; i < 1000; i++); }这个函数的实际延时完全取决于主频。8MHz 下它可能是 1ms,1MHz 下就变成 8ms 了。
第三个地方是串口波特率。异步串口的波特率是靠分频寄存器算出来的,计算公式里必然带时钟频率。主频对不上,波特率全错,虚拟终端里收到的就是一堆乱码。
ICCAVR 工程里设置时钟有几个地方:Application Builder 生成的工程会写成F_CPU宏;手工建的工程可以直接在源码顶部#define F_CPU 8000000UL,或者在Compiler标签的 Define 栏里加进去。不管你写在哪里,这个值必须和 Proteus 里 MCU 的 Clock Frequency 属性一一对应。我个人习惯是在源码开头显式写一行#define F_CPU 8000000UL,注释里再把 Proteus 属性值也写一遍,半年后回来看代码也不会忘。
3. Proteus 侧的挂载方式与那个总被忽略的路径问题
ICCAVR 把 COFF 生成好,接下来是让 Proteus 认它。这一步操作很简单,但有几个细节决定了你是"能用"还是"只能凑合看现象"。
3.1 Program File 填 .cof,而不是 .hex
双击 Proteus 里的 MCU 元件,打开属性对话框,找到Program File一栏。点击右侧的文件夹图标,选中 ICCAVR 输出目录下的.cof文件。如果你的目录里同时有.hex和.cof,请一定选.cof。
这一步是整个联合调试的开关。选了.hex,Proteus 只知道机器码,调试菜单里的源码窗口、变量观察全部是灰的或者空的;选了.cof,源码级调试能力才会被激活。
如果你在属性对话框里翻遍了也没找到 Program File,多半是元件选错了——你放的可能是个普通的逻辑器件,而不是 MCU 模型。只有原理图库里以微控制器命名的元件(比如 ATmega16、ATmega32、AT89C51 这类)才有这一栏。
3.2 Clock Frequency 与熔丝位,别让两边对不上
在同一属性对话框里,还有一个Clock Frequency字段。这里填的是赫兹数,8MHz 就填8000000,别填8,也别填8M。填错单位是最常见的低级错误,表现是仿真跑得慢得离谱或者快得看不清。
更深一层的问题是 AVR 的时钟源选择。ATmega 系列有内部 RC 振荡器、外部晶振、外部时钟等多种时钟源,靠**熔丝位(Fuse)**选择。Proteus 里可以设置这个熔丝状态。如果代码假设用 8MHz 外部晶振,而 Proteus 里熔丝位配的是内部 1MHz RC,那么:
- 所有软件延时会乘以 8 倍;
- 串口波特率全部算错;
- 定时器中断的周期也变成预期的 8 倍。
这类问题在实物上表现为"烧进去就是不按预期跑",在仿真里则表现为"时序完全不对但程序逻辑没毛病"。遇到时序类怪问题,第一件事就是把熔丝位和 Clock Frequency 一起核对一遍。
3.3 源码路径失效:为什么 COFF 加载对了却还是显示汇编
这是新手最容易卡住的一环,也是我觉得最值得单拎出来讲的。
COFF 里记录源文件位置时,存的是编译那一刻的路径——如果是绝对路径,就原样存进去;如果是相对路径,是相对编译工作目录的。Proteus 在调试时,会拿着这个路径去硬盘上找对应的.c文件。找到了,就显示源码;找不到,就退回显示反汇编。
于是下面这些操作都会让源码显示失效:
- 编译完之后,把工程目录整体挪到别的位置;
- 把
.c文件单独剪出来给别人; - 从别人那里拷了一份工程和
.cof,但源码路径还在对方电脑上; - ICCAVR 工程里用了相对路径,但编译时的工作目录变了。
排查方法很简单:在 Proteus 里暂停仿真,如果源码窗口里全是汇编,就回到 ICCAVR在原始路径下重新编译一次,再重新加载.cof。重编译会让 COFF 里的路径刷新成当前路径,问题一般就解决了。
提示:养成"工程目录建好就不再移动"的习惯。我的做法是在一个固定的工作盘符下建
avr_projects/项目名/这样的结构,源码、ICCAVR 工程文件、Proteus 原理图全放一起。这样 COFF 里的相对路径永远有效,跨电脑拷整个文件夹也能直接开工。
3.4 把调试观察窗口搭起来
源码能显示之后,下一步是把观察工具打开。Proteus 的调试菜单和数据窗口大致有这几类:
- 源码窗口:显示
.c源码,左侧行号栏可以点出断点; - Watch Window(变量观察):按变量名添加要监视的变量,实时显示值;
- Memory 窗口:按地址看内存内容,适合查数组、缓冲区、堆栈;
- 寄存器 / SFR 视图:直接看 I/O 寄存器的位状态,查端口和定时器配置特别快。
搭建顺序建议是:先把关键全局变量丢进 Watch,再把定时器、串口相关的寄存器加到 SFR 视图,最后在几个关键函数入口下断点。这样即使程序跑飞,你也能从寄存器状态反推出它跑到哪一步了。
需要提醒的是,Watch 窗口能看的变量必须没有被优化掉。如果加了变量名进去显示"未找到符号",先回头检查 ICCAVR 的优化等级是不是还开着。
4. 用动态扫描数码管做一次完整的联合调试演练
前面讲的都是配置,这一节拿一个具体例子把调试流程走一遍。选数码管动态扫描,是因为它同时涉及主循环、定时器中断、端口输出和数组访问,几乎能把联合调试的所有能力都用上。如果你的目标是超声波测距、ADC 采集这类项目,调试思路是一样的,只是把断点位置换一换。
4.1 被调试的代码长什么样
下面是一段能在 ICCAVR 下编译、在 Proteus 里跑的 ATmega16 动态扫描代码:
#include <iom16v.h> #include <macros.h> #define F_CPU 8000000UL unsigned char code seg_table[10] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; volatile unsigned char disp_buf[4] = {1, 2, 3, 4}; volatile unsigned char digit = 0; volatile unsigned int tick = 0; #pragma interrupt_handler timer0_ovf_isr:iv_TIMER0_OVF void timer0_ovf_isr(void) { PORTC = 0x00; PORTD = seg_table[disp_buf[digit]]; PORTC = (1 << digit); digit++; if (digit >= 4) digit = 0; tick++; TCNT0 = 0x06; } void main(void) { DDRA = 0xFF; DDRC = 0xFF; DDRD = 0xFF; TCCR0 = 0x03; TIMSK = 0x01; TCNT0 = 0x06; SEI(); while (1) { if (tick >= 100) { tick = 0; disp_buf[0]++; if (disp_buf[0] > 9) disp_buf[0] = 0; } } }几点说明。ICCAVR 的中断服务函数靠#pragma interrupt_handler绑定向量名,后面跟的iv_TIMER0_OVF是符号化的向量名,新版 ICCAVR 支持这种写法,老版本要改成数字向量号。如果 pragma 写错或者漏写,中断永远不会被调用,编译却完全不报错——这是 ICCAVR 的一个经典陷阱。另外SEI()来自<macros.h>,注意 ICCAVR 里是大写。
4.2 断点该往哪儿放
代码跑起来之后,先别急着单步。断点要放在"信息密度最高"的位置。对这段代码,我会这样布点:
main函数第一行:确认程序真的从这里开始跑。跳不进main,说明复位向量或熔丝位有问题,先解决这个再谈别的;while(1)循环体内部:这是主循环的"心跳点",能确认主循环在转;- 中断服务函数的开头:确认定时器中断有没有被触发。如果它一直不命中,问题就在定时器配置或者 pragma 绑定上;
TCCR0 = 0x03;之后一行:确认定时器寄存器确实写进去了。
我一般先用一个断点确认"程序能到 main",再用一个断点确认"能进中断"。这两个点通了,等于把程序的两条主干道验证完毕,剩下的问题就都是局部逻辑了。
4.3 变量、数组和 volatile 的那点事
打开 Watch 窗口,把disp_buf、digit、tick三个变量加进去,让仿真跑起来暂停,看它们的值。
这里有一个关键点:中断和主循环共用的变量必须加volatile。上面代码里disp_buf、digit、tick都加了。原因不复杂——不加volatile,且优化开着的时候,编译器可能认为主循环里根本没有代码会改这些变量,于是把它们的读取优化掉,导致主循环读到的永远是初始值。加了volatile,每次访问都强制从内存读,行为才符合预期。
还有个观察技巧:disp_buf是个 4 元素数组,Watch 窗口里通常可以展开成disp_buf[0]到disp_buf[3]分别看。调试数码管显示乱跳的问题时,直接盯这四个值,一眼就能看出是"数据源不对"还是"扫描逻辑不对"——如果是数据源问题,disp_buf里就是错的;如果disp_buf正确而显示不对,问题一定出在扫描或段码表上。
4.4 在中断里打断点,以及那个绕不开的实时性问题
在中断服务函数里下断点很有用,但有两个坑要提前知道。
第一个坑是断点触发频率。定时器中断如果每 1ms 触发一次,你把断点放在 ISR 开头,仿真会在极短的时间内疯狂命中,界面根本停不下来。解决办法是把断点放到 ISR 内部某个特定条件下,比如if (digit >= 4)那一行——虽然 Proteus 对条件断点的支持程度随版本而异,你也可以先用单次命中的断点确认一次 ISR 逻辑,然后删掉继续。
第二个坑是程序被暂停时中断不再累积。仿真暂停期间定时器不会继续计数,所以你不会因为暂停而错过中断,但恢复运行后的第一个中断时间点会受影响。做严格时序测量的时候要注意这一点。
测量时序更靠谱的办法是用虚拟示波器。把通道接到数码管的位选引脚上,看方波周期是不是 4ms(对应 4 位数码管、每位 1ms)。如果周期明显偏大,说明定时器初值算错了,或者时钟频率对不上。示波器比断点更适合看"周期"和"占空比"这类连续量,断点更适合看"执行到哪"和"变量是多少",两者配合使用效率最高。
5. 联合调试里最常翻车的几类问题与排查路径
配置走通了不代表一路顺风。下面这几类问题几乎每个用 ICCAVR 加 Proteus 的人都会碰到,我把排查链路按"先看什么、再看什么"的顺序整理出来,照着走基本能定位。
5.1 断点下了但一直不停
先按这个顺序排查。
第一步,确认程序真的跑起来了。用虚拟示波器接到某个一直在翻转的引脚上(比如上面代码里的PORTC位选),看不到方波就说明程序压根没在跑,跟断点没关系。
第二步,检查复位电路。Proteus 里 MCU 的复位引脚如果被拉低,程序就停在复位状态。很多人在原理图里手动加了个复位按键却忘了接上拉电阻,结果复位引脚一直是低电平,程序永远起不来。
第三步,检查熔丝位和时钟源。前面讲过,熔丝选错时钟源,程序可能跑得极慢,看起来像"卡死"。
第四步,确认下断点的那个位置真的会被执行到。你在一个if分支里下断点,而这个分支的条件永远不成立,那当然不停。
5.2 断点能停,但变量值明显不对
这种情况八成是优化导致的。典型表现:变量显示 0 或者一个固定的垃圾值,单步时值不更新,或者变量压根提示找不到符号。
处理办法就是回到 ICCAVR,把优化等级关掉,重新编译,重新加载.cof。这一套下来大部分问题会消失。
如果关了优化还是不对,那就要怀疑变量作用域。Watch 窗口只能看全局变量和当前作用域内的局部变量。局部变量在函数返回后就失效了,你把它加进 Watch,函数一退出它就变成无效值,这不是 bug,是正常的生命周期。
5.3 仿真跑起来像卡死,其实是速度设置
Proteus 的动画模式会影响体感。默认的动画设置下,仿真界面的刷新是有限制的,如果程序里有个忙等循环,界面看上去就像无响应。
在System → Set Animation Options里,可以调整帧率和实时模式。做时序验证的时候,把它设成尽量接近真实时间的模式;只是看逻辑跑通不通的时候,可以让它跑得快一点。这个设置不影响仿真内核的正确性,只影响界面刷新和"看起来快不快",但要清楚这一点,别把界面卡顿误判成程序死循环。
判断是不是真卡死,看仿真时间计数器。如果时间在往前走,说明 CPU 在跑,只是界面刷新慢;如果时间停住了,才是真的停下来了。
5.4 串口、ADC、EEPROM 在仿真里和实物表现不一样
Proteus 的外设模型是理想化的,跟实物必有差异。这几类差异最常见:
| 外设 | 仿真与实物的差异 | 应对方式 |
|---|---|---|
| 串口 | 波特率误差在仿真里不存在,实物上会有偏差 | 仿真里通了不代表实物一定通,实物上要留误差余量 |
| ADC | 仿真输入是理想电压,实物有噪声和参考电压漂移 | 用激励源注入带波动的信号测试算法鲁棒性 |
| EEPROM | 仿真里写入次数无限,实物有擦写寿命 | 调试阶段无所谓,逻辑验证完再考虑寿命 |
| 时钟 | 仿真是理想方波,实物有起振时间和抖动 | 涉及时钟切换的代码要单独在实物上验证 |
对串口调试,有两个实用技巧。一是把虚拟终端接到 UART 引脚上,直接看收到的字节,比看 LED 闪烁直观得多;二是把波特率降下来(比如 9600 甚至 4800),这样即使时钟频率有点偏差也不容易出错。
5.5 编译零报错,仿真就是纹丝不动
先确认三件事:.cof是不是真的挂上去了、时钟频率是不是填了、程序是不是从main开始跑。
如果这三件都没问题,那问题大概率在初始化顺序上。AVR 上电后端口默认是输入状态(高阻),先把DDR配成输出再往PORT写值,顺序反了就会出现"代码没错、端口不动"。这类问题在源码级调试下非常好定位:单步走到PORTD = xxx;那行,暂停,去 SFR 视图看PORTD寄存器的值变没变——变了说明软件侧没问题,没变说明被后续代码覆盖了或者端口没配成输出。
另一个隐患是看门狗。如果代码里开了看门狗却没有及时喂狗,程序会不断复位,表现就是"跑几下就重启"。在 Proteus 里可以在 MCU 属性中启用或禁用看门狗模型,配合源码级断点判断是不是它在捣乱。
6. 把调试效率再往上提的几个实操习惯
配置和排查都通了之后,剩下的是"怎么调得更快"。这几年用下来,下面几个习惯帮我省下的时间最多。
6.1 用激励源注入数据,别靠手改代码
调试传感器相关的项目时,很多人靠改代码里的变量来模拟输入,改一次编译一次,效率极低。Proteus 提供了激励源(Stimulus)机制,可以对某个引脚或节点施加预定义的波形、模拟电压或数字序列。
用一个模拟电压激励源接到 ADC 输入引脚上,就可以在仿真运行过程中动态改变输入电压,实时观察采集结果和显示结果。测超声波测距、光敏电阻这类项目时,这个方法能省掉无数次重新编译。你甚至可以把激励源设成阶梯波,自动遍历整个输入范围,一眼就能看出算法在哪个区间出错。
做这类调试时有个细节要注意:激励源的注入点要放在传感器模型和 MCU 引脚之间。如果你把传感器模型去掉了却忘了接激励源,那个引脚会悬空,AVR 读到的是不确定值,会误判成代码 bug。
6.2 把 printf 重定向到虚拟终端,等于给仿真加了个日志
ICCAVR 支持终端 I/O 重定向,可以把printf的输出挂到串口上。这样在 Proteus 里放一个 Virtual Terminal 接到 UART 引脚,代码里想打什么日志就打什么。
#include <stdio.h> void main(void) { unsigned int adc_val = 0; /* 串口初始化,8MHz 下 9600 波特率,U2X 关闭 */ UBRRH = 0; UBRRL = 51; UCSRB = (1 << TXEN) | (1 << RXEN); UCSRC = (1 << URSEL) | (1 << UCSZ1) | (1 << UCSZ0); while (1) { adc_val++; printf("adc = %u\r\n", adc_val); } }这段代码里UBRRL = 51是按 8MHz、9600 波特率、异步正常模式算出来的,公式是UBRR = F_CPU / (16 * 波特率) - 1,代入就是8000000 / (16 * 9600) - 1 ≈ 51.08,取整 51。你把这个数字改了但没改时钟,终端里就是乱码。
日志法的好处是能看到"时间序列"——变量的变化过程一目了然,这是断点做不到的。断点给你的是某一时刻的快照,日志给你的是连续记录。两者结合,一个看细节,一个看趋势。
6.3 拆出最小可复现工程
遇到一个查了很久都查不出来的问题,与其在大工程里继续翻,不如新起一个最小工程:只保留出问题的那一小段逻辑,其他全部砍掉。
这么做有三个好处。第一,干扰项少了,问题更容易暴露;第二,编译速度快,改一次几秒钟;第三,如果最小工程里问题消失了,那说明问题出在被你砍掉的那部分代码的交互上,排查范围立刻缩小。
我处理过一个数码管"某一位偶尔不亮"的问题,在大工程里查了半天没头绪,最后拆成一个只有定时器中断加位选输出的最小工程,两分钟就看出是中断里清位选的顺序不对——先清零再送段码,中间有个短暂的"位选已开、段码未定"的窗口。这种问题只有在干净环境里才看得清。
6.4 目录、版本和备份,别让路径问题反复咬你
前面反复提到源码路径失效的问题,根子在于工程目录乱。我的习惯是一套固定的结构:
avr_projects/ dsm_display/ src/ 源码 .c 和 .h icc/ ICCAVR 工程文件与输出 proteus/ Proteus 原理图与 .cof 副本 notes.md 调试记录关键点是编译输出目录和 Proteus 原理图放在同一个父目录下,这样 COFF 里的相对路径在任何一台机器上都有效。另外每次改完一个能跑通的版本,把整个src目录复制一份存起来,命名为src_ok_日期。调试过程中最怕的就是"改着改着把一个能跑的版本改坏了,又回不去",有了这个习惯就不慌了。
再补一句关于notes.md的。调试时踩过的坑、试出来的寄存器配置、算出来的定时器初值,随手记进去。我当时觉得"这么简单的东西肯定记得住",一个月后重启同类项目时,全靠这份笔记才没重新踩一遍。这份记录的价值,比代码本身还高。
如果你现在手上正好有一个用 ICCAVR 写的 AVR 老工程,跑在 Proteus 里只能看现象,那不妨按第 2 节和第 3 节的顺序,把 COFF 输出和路径这两件事先理顺。一旦源码窗口能正常显示,你会发现原本要靠猜的问题,很多都能在两三次单步之内定位。