简介:这份面向51单片机初学者与课程设计者的红外遥控计算器程序资料,围绕TC9012红外遥控器解码与计算器逻辑实现,帮助读者理解外部中断捕获、红外码值处理、键值扫描、数据运算、结果显示及系统与中断初始化等环节,并附电路图与遥控编码说明,适合单片机入门练习与小型项目参考。压缩包内共1个doc文件,约245KB,集中整理程序源码与相关图示,便于对照阅读和移植。目前已有588人学习下载。读者可从中获取完整的程序框架、全局变量与函数划分思路、红外接收与数码管显示配合方式,以及调试计算器输入输出流程的参考;对遥控器按键映射、数值输入与运算符处理、结果缓存及数码管动态刷新之间的关系,也能看到可直接借鉴的组织方式,用于课程作业、实验复现或二次修改。
1. 从"板子上按键不够用"说起
做单片机课设最容易被卡住的往往不是算法,而是人机交互那一层:板子上独立按键只剩三四个,矩阵键盘又要占掉一整组 IO,于是"拿电视遥控器当键盘"就成了一个很自然的选择。这套基于 51 单片机的红外遥控计算器,用一只 TC9012 遥控器替代键盘,P3.2 接一体化红外接收头,8 位数码管出结果,加减乘除四则运算全部在一颗 40 脚的单片机上跑完,整套程序不到四百行。它适合两类人:一类是要交课设或毕设、需要一个现场能演示、原理还能讲得清的小系统;另一类是想把外部中断、定时器方式 2、红外协议解码、数码管动态扫描这几块基础真正串起来练一遍的人。下面按需求、设计、实现、验收的顺序,把这份程序和配套电路图拆开讲透。
2. 系统总体设计:一条从 38kHz 载波到数码管的信号链路
拿到这个题目第一件事不是写代码,而是把信号从哪来、到哪去画清楚。整条链路只有五个环节:遥控器按键 → 红外发射管调制 38kHz 载波 → 一体化接收头解调输出电平 → 单片机外部中断捕获 + 定时器计时 → 协议解析成键值 → 数码管显示。链路里任何一环的时序没对齐,后面全是玄学调试,所以这一章先把硬件连接和软件分层定死。
2.1 硬件链路与引脚分配
接收头用的是常见的一体化红外接收器件,三个引脚:电源、地、输出。输出脚直接怼到 P3.2,也就是 51 的 INT0 引脚,这一点很关键——红外解码本质是测"两次下降沿之间的时间",必须走外部中断,用轮询查电平的做法在 0.56ms 这个量级上根本来不及。接收头输出是反相的:有 38kHz 载波时输出低电平,没有载波时输出高电平,所以一个完整的引导码在示波器上看是"低 9ms、高 4.5ms"这么一段。
显示部分的引脚分配是整个电路里最需要对照图纸核实的。从display()函数的位操作反推,P0 口被拆成了三段用:低 4 位送段码,中间 3 位送位选,最高位 P0.7 保留给别的用途。这种"一条口线当两条用"的设计在 51 系统里很常见,因为 P0 口本来就是复用总线口,能省则省。
| 引脚 | 连接对象 | 作用 | 备注 |
|---|---|---|---|
| P3.2 / INT0 | 红外接收头 OUT | 下降沿触发外部中断 0 | 必须接这个脚,否则没法用 INT0 |
| P0.0 ~ P0.3 | 段码输入 | 送 0~9 的 BCD 码 | 外接七段译码,或直接驱动共阴段 |
| P0.4 ~ P0.6 | 位选译码器 A/B/C | 选择 8 位数码管中的一位 | 常见配 3-8 译码器 |
| P0.7 | 保留 | 原程序里用P0 & 0x80保住 | 实际用途看电路图 |
| XTAL1/XTAL2 | 晶振 | 提供机器周期 | 12MHz 或 11.0592MHz 都行,见 2.3 |
| RST | 复位电路 | 上电复位 | 10μF + 10kΩ 的经典组合 |
提示:这张表是从代码反推出来的,不同版本的电路图位分配可能完全不一样。动手焊之前一定拿万用表把段码线和位选线对一遍,接反了显示出来是乱码,很容易误判成程序问题。
2.2 软件分层:五个函数各管一段
程序结构是很典型的"主循环 + 中断服务"模型。中断里只做最紧急的事——记时间;主循环里做所有慢活——解析、判键、运算、显示。这种分工在 51 上不是可选项而是必须项,因为中断服务函数里但凡多几条指令,下一次下降沿就可能漏掉。
void main(void) { INTInit(); // 先配中断:定时器0 方式2 + 外部中断0 下降沿 P0 = 0x00; // 上电先把显示口清干净,避免上电瞬间乱亮 while (1) { keyscan(); // 红外解码 + 键值映射 DataHandle(); // 按键语义 -> 操作数缓冲区 DisplayHandle();// 缓冲区 -> 数码管 } }这段主循环只有三个调用,但它定义了整份程序的数据流方向:keyscan()负责把电平变成键值,DataHandle()负责把键值变成"第几个操作数的第几位",DisplayHandle()负责把数组变成段码。三个函数之间通过全局变量传递状态,这在教学项目里是合理的,因为 51 的栈空间小,层层传参反而容易出问题。
各函数的职责边界我一般会这么划分:INTInit()只碰 TMOD、TH0、TL0、ET0、TR0、IT0、EX0、EA 这八个寄存器,多一行都不写;Ircordpro()只负责把irdata[33]这个原始时间数组翻译成 4 个字节,不判断键值含义;keyscan()里做键值到key变量的映射;DataOper()只做数学,不碰显示;DisplayHandle()只读全局数组,不修改任何状态。边界一乱,后面加一个功能就得改五个地方。
2.3 时间基准核算:为什么定时器必须用方式 2
红外解码的精度全靠时间基准,这个基准由定时器 0 的工作方式决定。原程序里TMOD = 0x02,低四位 0010 表示定时器 0 工作在方式 2,也就是 8 位自动重装载模式。这个选择不是随便挑的:方式 2 下 TH0 存重装值、TL0 存计数值,TL0 溢出时硬件自动把 TH0 的值重新装进 TL0,全程不需要软件干预。如果换成方式 1(16 位),每次进中断都得手动重写初值,这几条指令的误差会直接叠加到时间测量上,几十个微秒的偏差在红外解码里就是致命的。
时间刻度算起来很简单:
T_tick = 256 × (12 / fosc)12MHz 晶振下,一个机器周期是 1μs,方式 2 计满 256 个数溢出一次,所以一个 tick 就是 256μs。11.0592MHz 下机器周期约 1.085μs,一个 tick 约 277.8μs。程序里TH0 = 0x00; TL0 = 0x00;意味着从 0 开始数到 255 溢出,正好是满量程 256 个机器周期,这样irtime变量里的每一个数就代表一个固定的时间片,换算起来最省事。
接下来所有阈值的计算都建立在这个 tick 上。红外协议里一个逻辑位的周期是固定的:逻辑 0 是 0.56ms 载波加 0.56ms 空档,下降沿到下降沿约 1.12ms;逻辑 1 是 0.56ms 载波加 1.69ms 空档,约 2.25ms。用 256μs 的刻度去量,逻辑 0 约 4.4 个 tick,逻辑 1 约 8.8 个 tick,门限取在中间 6~7 都能分得开,原程序里cord > 7判为 1 就是这么来的。换成 11.0592MHz,两个值变成 4.0 和 8.1,门限取 6 更稳一些。这也是为什么不能直接抄别人代码里的数字——晶振一换,所有阈值都得重算。
3. 红外解码:33 个时间值怎么还原成 4 个字节
解码这块是整个项目里最容易翻车的地方,因为它没有中间结果可看,错一位后面全错,表现出来就是"按了半天没反应"。要把它讲清楚,得先明白 TC9012 一帧数据到底长什么样:一个引导码,后面跟 4 个字节共 32 位,低位在前,四个字节依次是用户码、用户码反码、数据码、数据码反码。反码的作用就是给接收端一次校验机会——如果收到的用户码和它的反码对不上,说明这一帧被干扰了,直接丢掉。
3.1 外部中断加定时器:两次下降沿之间就是一个时间片
采样逻辑藏在两个中断服务函数里,加起来不到二十行,但信息密度很高。
void tim0_isr(void) interrupt 1 using 1 { irtime++; // 每个 tick 加一,这就是"秒表" } void ex0_isr(void) interrupt 0 using 0 { if (fir) // 已经见过第一个下降沿 { if (irtime < 49 && irtime >= 31) mir = 0; // 引导码窗口,命中则从头记录 irdata[mir] = irtime; // 把这段间隔存进数组 irtime = 0; // 秒表归零,准备量下一段 mir++; if (mir == 33) // 引导码 + 32 位,正好 33 段 { irok = 1; // 通知主循环:一帧收完了 mir = 0; fir = 0; // 重新等待下一帧的引导码 } } else { irtime = 0; // 第一个下降沿只负责启动秒表 fir = 1; } }irdata[33]这个数组的大小是有讲究的:下标 0 存的是引导码那一段的间隔,1 到 32 存的是 32 个数据位各自的间隔。所以后面解析的时候从irdata[1]开始读,一次读 32 个,正好凑成 4 个字节。这个"33"如果写成 32,最后一位会丢;写成 34,会多读一个垃圾值。
irtime用的是uchar类型,最大 255 个 tick,按 256μs 算能表示 65ms,对红外这种毫秒级的信号绰绰有余。用uint反而浪费——51 的 16 位加法比 8 位慢一倍,中断里能省则省。
注意:
irok、mir、irtime、fir这四个变量在中断和主循环里都会被访问,必须声明成volatile或者至少保证编译器不会优化掉。Keil C51 在默认优化级别下对这类变量处理还算保守,但换编译器就可能出问题,这种坑很难查。
3.2 引导码窗口和位门限:两个必须实测标定的数字
原程序里irtime < 49 && irtime >= 31这一行是引导码的识别窗口,对应 12MHz 下 7.9ms 到 12.5ms 的间隔。为什么要用窗口而不是精确值?因为遥控器发射管的载波频率有离散性,接收头的解调延时也有几微秒的抖动,写死一个值必然有一批遥控器过不了。窗口的作用是"容错",但窗口开多大得自己算。
引导码在接收头输出端是一段 9ms 低电平加 4.5ms 高电平,两次下降沿之间的间隔取决于你从哪个沿开始算。稳妥的做法是:先写一个只打印irdata[]的调试版本,从串口把 33 个原始值全部打出来,看实际落在什么范围,再定窗口。我一般会让窗口下限比实测最小值再低 20%,上限比最大值高 20%,这样换几只同型号遥控器都能兼容。
位门限cord > 7同理。按 12MHz 算是 1.79ms,落在 1.12ms 和 2.25ms 正中间偏上;按 11.0592MHz 算是 1.94ms,稍微偏高了点,遇到发射功率弱的遥控器可能把逻辑 1 误判成 0。这种错误的表征很有意思:按"1"结果出来是"3",按"5"出来是"4",因为错的是某一位。遇到这种"按键错位"的现象,八成不是键值表写错了,而是位门限偏了。
3.3 四字节移位还原:低位在前,别搞反了
拿到 32 个时间值之后,剩下的就是纯计算:每个值大于门限记 1,小于记 0,八个一组拼成一个字节。
void Ircordpro(void) { unsigned char i, j, k = 1; // k 从 1 开始,跳过 irdata[0] 的引导码 unsigned char cord, value; for (i = 0; i < 4; i++) // 4 个字节 { value = 0; for (j = 1; j <= 8; j++) // 每个字节 8 位,低位先到 { cord = irdata[k]; if (cord > 7) value |= 0x80; // 逻辑1:先把最高位置 1 if (j < 8) value >>= 1; // 再右移,为下一位腾位置 k++; } IRcord[i] = value; } /* 校验:用户码和它的反码必须互补,数据码同理 */ if ((IRcord[0] == (unsigned char)~IRcord[1]) && (IRcord[2] == (unsigned char)~IRcord[3])) { X = IRcord[2]; // 校验通过才把数据码送出去 } }这段"先置最高位再右移"的写法初看别扭,其实是被协议逼出来的:TC9012 发送时低位在前,第一个到达的位是 D0,而我们要拼的字节是 D7 在高位。所以每来一位就先假设它放在最高位,然后整体右移一次,等八位收完,最先到的 D0 刚好被挤到最低位,顺序自动就正了。如果反过来写成左移,收到的字节位序会完全颠倒,键值表就得整个重写一遍。
校验那一行是全文最值得盯的地方。原程序写的是if((IRcord[0]=~IRcord[1])&&(IRcord[2]=~IRcord[3])),用的是赋值号而不是相等比较。赋值表达式的值等于被赋的值,只要~IRcord[1]不是 0,条件就成立——也就是说这个校验几乎永远通过,形同虚设,同时还把IRcord[0]的内容改掉了。这是个典型的血泪坑,编译不报错、运行不崩溃,只有在遥控器受到干扰时才暴露,而且表现出来是"偶尔按一下算出个莫名其妙的结果"。
4. 按键语义与数据流:从键值到两个操作数缓冲区
解码出来的是 4 个字节,其中只有IRcord[2]有用,它代表按下的是哪个键。到这一步,红外的事就结束了,接下来完全是普通的单片机逻辑:怎么把一串按键组织成"第一个数、运算符、第二个数、等号"这样的序列。这部分的设计好坏,直接决定了这个计算器是"能演示"还是"能演示且经得起追问"。
4.1 键值映射表:一张表定死所有按键
TC9012 每个按键对应的数据码是固定的,从原程序的 switch 分支里可以直接读出这张表:
| 遥控器按键 | IRcord[2] | 程序内 key | 语义 | 附加动作 |
|---|---|---|---|---|
| 0 | 0x08 | 0 | 数字 0 | flag = 1 |
| 1 | 0x01 | 1 | 数字 1 | flag = 1 |
| 2 | 0x02 | 2 | 数字 2 | flag = 1 |
| 3 | 0x03 | 3 | 数字 3 | flag = 1 |
| 4 | 0x05 | 4 | 数字 4 | flag = 1 |
| 5 | 0x06 | 5 | 数字 5 | flag = 1 |
| 6 | 0x07 | 6 | 数字 6 | flag = 1 |
| 7 | 0x09 | 7 | 数字 7 | flag = 1 |
| 8 | 0x0A | 8 | 数字 8 | flag = 1 |
| 9 | 0x0B | 9 | 数字 9 | flag = 1 |
| + | 0x10 | 10 | 加 | flag1 = 1,显示清 0 |
| − | 0x1A | 11 | 减 | flag1 = 1,显示清 0 |
| × | 0x15 | 14 | 乘 | flag1 = 1,显示清 0 |
| ÷ | 0x19 | 15 | 除 | flag1 = 1,显示清 0 |
| = | 0x11 | 13 | 等于 | flag2 = 1,立即调 DataOper |
| C | 0x16 | 12 | 清零 | 调 Init 全清 |
这张表的价值在于它是唯一需要对着遥控器实测的部分。0x08 到 0x0B 这几个值是连着的,0x10、0x11、0x15、0x16、0x19、0x1A 是散的,这正是 TC9012 的编码特点——数据码不是按键盘物理顺序排的。所以换一只不同型号的遥控器,这张表必须整张重测,不能靠猜。
三个标志位的分工是:flag表示"刚刚来了一个新的数字键,待入队";flag1表示"已经进入第二个操作数的输入阶段";flag2表示"运算已经完成,进入结果展示态"。三个标志配合px1、px2两个下标,构成了一个极简的状态机。
4.2 两个操作数缓冲区:为什么用数组而不是整数
一个自然的想法是直接把输入的数字存成int,来一位就x = x * 10 + key。原程序没那么做,而是用了DATA1[8]、DATA2[8]、RESUIT[8]三个数组,每位数字单独存。这么做有两个实际好处:一是显示方便,DisplayHandle()直接按下标取数就能刷屏,不用做除法取位;二是天然支持"退格",虽然这份程序没实现,但结构上留了口子。
void DataHandle(void) { /* 第一个操作数:还没按运算符,且刚来了数字键 */ if ((key < 10) && (flag == 1) && (flag1 == 0)) { if (px1 < 8) { DATA1[px1] = key; px1++; } // 加下标保护,最多 8 位 flag = 0; } /* 第二个操作数:已经按过运算符 */ else if ((key < 10) && (flag == 1) && (flag1 == 1)) { if (px2 < 8) { DATA2[px2] = key; px2++; } flag = 0; } /* 运算符:只在刚按下的这一轮处理一次 */ else if (flag1 == 1) { switch (key) { case 10: f1 = 1; f2 = 0; f3 = 0; f4 = 0; break; // + case 11: f1 = 0; f2 = 1; f3 = 0; f4 = 0; break; // - case 14: f1 = 0; f2 = 0; f3 = 1; f4 = 0; break; // × case 15: f1 = 0; f2 = 0; f3 = 0; f4 = 1; break; // ÷ } } }flag用完立刻清零这一手是必须的,否则主循环一秒钟转几万圈,同一个数字会被塞进缓冲区几万次,px1瞬间冲到 8 以上。原程序里没有对px1、px2做上限判断,这是我在改这份代码时第一个加的东西——数组只有 8 个元素,写越界了在 51 上不会报错,但会把相邻的全局变量踩掉,表现出来是"输到第 9 位突然死机",非常难查。
四个运算标志f1~f4是互斥的,每次设一个必须把另外三个清掉。原程序在 case 里做全了,但如果后来加一个"取余"运算符忘了清,就会出现f1和f5同时为 1、DataOper()里 if-else 链走错分支的情况。
4.3 十进制合成与位分离:运算部分最容易埋雷
到DataOper()这里,两个数组要合成两个整数,算完再把结果的每一位拆回数组。原程序的写法能跑,但有几处需要加固。
void DataOper(void) { uchar i, j; lint k; x1 = 0; x2 = 0; y = 0; // 关键:每次运算前必须清零 for (i = 0; i < px1; i++) // 用 px1 而不是 px1-1 做上界 { k = DATA1[i]; for (j = 0; j < (px1 - i - 1); j++) k *= 10; x1 += k; } for (i = 0; i < px2; i++) { k = DATA2[i]; for (j = 0; j < (px2 - i - 1); j++) k *= 10; x2 += k; } if (f1) y = x1 + x2; else if (f2) y = (x1 >= x2) ? (x1 - x2) : 0; // 先挡负数,无符号数减出负数会变成极大值 else if (f3) y = x1 * x2; else if (f4) y = (x2 == 0) ? 0 : (x1 / x2); // 先挡除零,否则 51 会跑飞 else y = x1; RESUIT[0] = (uchar)(y / 10000000); RESUIT[1] = (uchar)((y % 10000000) / 1000000); RESUIT[2] = (uchar)((y % 1000000) / 100000); RESUIT[3] = (uchar)((y % 100000) / 10000); RESUIT[4] = (uchar)((y % 10000) / 1000); RESUIT[5] = (uchar)((y % 1000) / 100); RESUIT[6] = (uchar)((y % 100) / 10); RESUIT[7] = (uchar)(y % 10); px3 = 7; // 默认从最低位开始显示 for (i = 0; i < 7; i++) // 找第一个非零位,最多退到第 7 位 { if (RESUIT[i]) { px3 = i; break; } } }三处加固对应的都是真实会踩的坑。第一处清零:x1、x2、y都是全局变量,第一次按等号算完不清,第二次按等号会在上次的结果上继续加,表现出来是"算 2+3 得 5,再按一次等号变 10"。第二处循环上界:原程序写的是i < (px1-1),当px1为 0 时,px1-1在uchar下回绕成 255,循环会跑 255 次并把DATA1[]越界读穿,直接按等号就死机。第三处找首位:原程序用while(!RESUIT[m]) m++;,当结果是 0 时八位全是 0,m会一直加到 8 然后越界,DisplayHandle()里拿一个越界的px3去刷屏,显示的就是乱码。
还有两个没写进代码但答辩一定会被问到的点:一是溢出,y是unsigned long,最大到 42 亿,但八位数码管只能显示到 99999999,两个八位数相乘轻松超过这个范围,超出部分会被高位截断,显示一个错误但看起来正常的结果;二是负数,当前设计用无符号数,减法结果为负时只能显示 0。这两点不必非得实现,但心里要有数,被问到能答上来。
5. 常见翻车点排查:五处最容易在演示现场暴露的问题
这份程序能跑通不难,难的是连续按十分钟不出错。下面五条是我在改这类代码时反复遇到的,每一条都有明确的表征,照着现象去查比盲改快得多。
5.1 校验位写成赋值号,遥控器被干扰也能算出一个数
现象:正常按没问题,但只要旁边有人开日光灯或者手机靠近,就会突然跳出一个莫名其妙的结果,而且这个结果往往还挺"合理",不会明显乱码。
原因:if ((IRcord[0] = ~IRcord[1]) && ...)用的是赋值。赋值表达式的值是被赋的那个值,只要~IRcord[1]不为 0 条件就成立,校验永远通过。被干扰的帧本该丢弃,结果被当成有效数据送进了IRcord[2],于是算出一个不属于任何按键的值。
解决:改成==比较,并且两个字节都要显式强转成unsigned char再取反,避免整型提升带来意外。改完后再测,受干扰时表现为"按了没反应",这才是正确行为——丢帧比算错好得多。
5.2 全局变量没清零,第二次按等号结果翻倍
现象:算 2+3 得 5,再按一次等号变成 10,再按一次变 15。或者算完一次之后,下一次无论输什么都带着上次的影子。
原因:x1、x2、y是全局变量,DataOper()开头没有清零,x1 += k是在旧值上累加。同理RESUIT[]没清,上一次的高位会残留下来。
解决:DataOper()第一行就把三个数清零,RESUIT[]在写之前用循环清一遍(虽然每一位都会被重新赋值,但清一遍更保险)。这类"状态残留"问题的通用解法是:凡是参与运算的全局变量,在运算函数入口处一律先归零,不要指望别的地方帮你清。
5.3 结果为 0 或全前导零,数组越界导致显示乱码
现象:算 5-5,数码管不显示 0,而是显示一串乱七八糟的段码,或者干脆全灭。
原因:while(!RESUIT[m]) m++;在结果为 0 时找不到非零位,m会一路加到 8、9、10,然后DisplayHandle()里for(i=7;i>=px3;i--)拿越界的下标去访问数组,读到的是内存里的随机值。
解决:改成有界循环,最多找到第 7 位就停;如果八位全零,px3保持 7,只显示一个 0。这个改法同时解决了"前导零"的问题——比如结果是 007,px3会定位到第 5 位,从第 5 位开始显示,前面两个 0 自然就不显示了。
5.4 除零和负数没有防护,一按就死机
现象:输入 8 ÷ 0 =,数码管直接卡住不动,所有按键失效,必须断电重启。
原因:51 没有硬件除法异常,除以 0 会得到一个未定义的结果,后续的位分离运算再拿这个结果去做除法和取模,可能触发长时间循环或者把栈跑飞。减法为负时,unsigned long会把负数解释成一个接近 42 亿的大数,八位数码管截断后显示一个看似正常的错误值。
解决:在DataOper()里先判x2 == 0直接返回 0,减法先判x1 >= x2再减。这一条看着简单,但很多人写的时候想不起来,直到演示现场翻车。
5.5 遥控器连发码没处理,长按一次算好几次
现象:按住"5"不放,数码管上连续出现好几个 5,直接把缓冲区填满。按住"+"不放,运算标志被反复触发。
原因:TC9012 在按住不放时会以 108ms 的周期发送连发码,连发码只有引导码加一个结束位,长度和正常帧不一样。原程序的mir == 33判定在连发码上凑不满 33 段,会一直卡在半截状态,直到下一次正常帧到来才恢复——这期间irdata[]里的内容已经被搅乱了。更糟的情况是连发码刚好凑够长度,被当成一次有效按键。
解决:在ex0_isr()里加一个"上一帧时间戳",如果两次有效帧的间隔小于某个阈值(比如 150ms),判定为连发,直接丢弃。或者更简单——在keyscan()里记录key的上一次值和按下时刻,同一个键在 200ms 内重复到达就忽略。后者更好实现,也不用改中断。
6. 验收与答辩:拿一张测试表证明它真的能算
代码改完只是开始,真正决定成绩的是能不能在十分钟的演示里稳定跑完所有功能。我的习惯是提前准备一张测试表,从简单到复杂逐项打勾,任何一项不过就往回退一步查,而不是临场凭感觉按。
| 序号 | 测试项 | 操作 | 预期结果 | 判定要点 |
|---|---|---|---|---|
| 1 | 上电初始态 | 通电不按键 | 最右位显示 0 | 不能有残影或全亮 |
| 2 | 单位数输入 | 依次按 1 2 3 | 显示 123 | 数字不能反序 |
| 3 | 加法 | 12 + 34 = | 46 | 必须按等号才出结果 |
| 4 | 减法 | 50 − 12 = | 38 | 借位正确 |
| 5 | 乘法 | 25 × 4 = | 100 | 结果中间位的 0 要显示 |
| 6 | 除法 | 100 ÷ 7 = | 14 | 整除截断,不保留小数 |
| 7 | 结果为 0 | 5 − 5 = | 0 | 不能乱码,不能死机 |
| 8 | 前导零 | 3 + 4 = | 7 | 不能显示 00000007 |
| 9 | 除零 | 8 ÷ 0 = | 0 或报错提示 | 不能卡死 |
| 10 | 清零 | 任意状态按 C | 显示 0 | 所有缓冲区清空 |
| 11 | 连续运算 | 1 + 2 = 之后再按 = | 保持 3 | 不能累加 |
| 12 | 连发抑制 | 按住 5 超过 1 秒 | 只进一位 | 不能连续填满 |
| 13 | 抗干扰 | 演示时开关日光灯 | 无跳变 | 允许丢帧,不允许错算 |
| 14 | 距离测试 | 距离 3 米、偏 30 度按键 | 正常响应 | 记录最大可用距离 |
这张表里第 7、8、9、11、12 项是最容易被忽略但最容易被老师抓到的,因为它们正好对应"边界情况处理"这一评分点。演示的时候我一般主动把这几个点走一遍,比被动等着被问要好。
答辩环节通常还有一个固定问题:这条信号链路上哪一步最容易出错。答"引导码识别"和"位门限判定"都可以,但要把理由说清楚——这两个阈值都依赖晶振频率,晶振一换就必须重算,而 12MHz 和 11.0592MHz 算出来的数差得不多,靠肉眼看不出来,只能靠实测标定。能主动说出这一层,说明不是抄的代码,是算过的。
还有个细节容易被问:为什么irdata是 33 个而不是 32 个。答"下标 0 存引导码,1 到 32 存 32 个数据位"就行,然后可以顺手补一句k从 1 开始就是为了跳过引导码。这种小问题答得干脆,会显得整个数据流在脑子里是通的。
从那以后我每次做带协议解码的项目,都强制走一遍"先打原始数据、再定阈值、最后写解析"的三步,绝不跳过第一步直接抄现成的门限值。上面这张测试表也是被坑出来的——第一次演示就是栽在"5 减 5"上,全屏乱码,台下笑成一片。希望这份拆解和这张表,能帮你把这一步提前走完。
本文还有配套的精品资源,点击获取