☰
51单片机软件延时与查询式I/O:从1Hz方波到A/D转换的时序实践
2026/10/28 2:37:24 网站建设 项目流程

简介:这份资源是襄樊学院物理与电子信息工程学院编写的《嵌入式系统》实验指导书,面向电子信息、自动化等专业学生及嵌入式初学者,用于支撑课程实验与自学练习。文档共6个实验,从Keil C51汇编语言入门起步,涵盖十六进制与十进制转换、顺序结构编程等内容,每个实验均给出实验目的、设备、原理环境、内容、步骤及报告格式要求,并明确预习、实验与总结三阶段任务。压缩包内为1个doc文件,约965KB,可直接打印或电子阅读。已有338人学习下载。读者可据此掌握Keil C51工程建立、源文件编译、软件模拟与硬件仿真调试的完整流程,理解ZY15MCU12BD实验箱与PC串口联机方法,并按规范撰写含程序清单、调试分析与心得的实验报告,适合作为课堂配套与期末复习的实操参考。

1. 汇编延时与端口翻转:1Hz 方波到底是怎么“算”出来的

很多人第一次在单片机实验箱上看到 P1.0 的 LED 以 1Hz 闪烁时,会下意识觉得这是硬件定时器的功劳。但翻开这份实验指导书会发现,实验 1 压根没碰定时器,它用的是最原始的办法:让 CPU 一条一条地空转指令,用指令周期“数”出 0.5 秒。这个反直觉的结论值得先摆出来——在 12MHz 晶振下,一个机器周期是 1 微秒,所谓“延时 0.5 秒”,本质是把 500000 个机器周期用三层循环凑出来。搞懂这件事,你才真正理解嵌入式里“软件延时”这个玄学词背后的确定性,也才能明白为什么后面几个实验的显示刷新、键盘消抖都绕不开它。这份包含 6 个实验的指导书,从 Keil C51 建工程到 A/D 转换,串起来的正是“用软件精确控制硬件时序”这条主线。

2. 软件延时:三层循环如何凑出 0.5 秒

2.1 从晶振到机器周期的换算链

实验 1 在 Keil 里选芯片时要求把晶振频率改成 12MHz,这个动作不是走流程。MCS-51 的机器周期固定为 12 个时钟周期,12MHz 晶振下机器周期就是 1μs,绝大多数指令(除乘除法)都在 1 或 2 个机器周期内完成。指导书里那句“每条指令的执行时间可以从教材的附表中查出”,说的就是查指令的机器周期数。于是延时问题被转化成一个纯算术问题:要让 P1.0 保持 0.5 秒不变,就得让循环体消耗约 500000 个机器周期。

指导书给出的延时骨架是这样的:

; 12MHz 晶振,机器周期 1us AGAIN: CPL P1.0 ; 翻转 P1.0,1 个机器周期 MOV R0, #10 ; 外层计数 LOOP1: MOV R1, #100 ; 中层计数 LOOP2: MOV R2, #250 ; 内层计数 DJNZ R2, $ ; 内层自减,2 个机器周期 DJNZ R1, LOOP2 ; 中层自减 DJNZ R0, LOOP1 ; 外层自减 SJMP AGAIN ; 回到翻转

内层DJNZ R2,$每次执行占 2 个机器周期,250 次就是 500μs;中层 100 次、外层 10 次,粗算下来 500μs × 100 × 10 = 0.5 秒。这里的关键不是精确到微秒,而是理解“循环嵌套层数 × 计数初值 × 单次周期”这个乘法结构。常见的实现思路是把最内层做成 2 周期指令,外层用 8 位寄存器封顶 255,需要更长延时再套一层,这也是为什么指导书用 R0/R1/R2 三个寄存器而不是一个 16 位计数器——51 的寄存器就是 8 位的,分层是唯一选择。

2.2 为什么实际延时总是“差一点”

严格算会发现上面这段并不精确等于 0.5 秒:MOV R1,#100和MOV R2,#250本身也占周期,DJNZ R1是 2 周期,外层还有SJMP。真实耗时比 500000 个周期略多。指导书没有要求精确校准,因为 1Hz 方波对人眼来说差几个毫秒根本看不出来。但这里埋着一个血泪经验:如果你把这个延时直接搬到串口通信或 ADC 采样时序里,误差就会翻车。软件延时的本质是“用确定性的指令流换时间”,它的精度取决于你对每条指令周期的掌握程度,一旦编译器优化或中断插入,这个确定性就被打破了。所以后面实验 3 的 0.5ms 显示延时、实验 4 的 20ms 键盘消抖,都是同一套思路的不同量级应用。

2.3 双路反相方波:一条 CPL 之外的取反逻辑

实验内容 3 要求在 P1.0 和 P1.1 上输出同频反相的方波。最直白的写法是两次CPL,但两次翻转之间隔着指令,严格来说不是同一时刻。指导书没给标准答案,留作预习。合格的实现思路是用一个位变量记录当前电平,然后分别送到两个端口,或者利用CPL P1.0后立即CPL P1.1,接受那 1 个机器周期的相位差。对 1Hz 信号而言,1μs 的偏差可以忽略,但这道题真正想训练的是“端口是位可寻址的”这个认知——P1 的每一位都能单独翻转,不需要读改写整个字节,这正是 51 位寻址区设计的动机。

3. 十六进制转十进制:除法取余在 8 位机上的落地

3.1 除以权值的算法为什么选 100 和 10

实验 2 要把片内 RAM 30H 里的 2 位十六进制数(0x00~0xFF)拆成 3 位十进制,存到 31H~33H 的非压缩 BCD 码。指导书点明“依次除以 100、10,商和最后的余数就是十进制数”。这个算法的动机很朴素:51 没有硬件除法器之外的十进制转换指令,DIV AB只能做 8 位无符号除法,而 255 除以 100 商 2 余 55,再 55 除以 10 商 5 余 5,正好得到百位、十位、个位。选 100 和 10 而不是 16 的幂,是因为目标就是人读的十进制,权值必须和进制对齐。

常见的实现会写成这样:

; 入口:30H 存放待转换的十六进制数 ; 出口:31H=百位, 32H=十位, 33H=个位 MOV A, 30H MOV B, #100 DIV AB ; A=百位商, B=余数 MOV 31H, A MOV A, B MOV B, #10 DIV AB ; A=十位商, B=个位余数 MOV 32H, A MOV 33H, B

DIV AB执行后商在 A、余数在 B,这是 51 除法指令的固定约定,也是这段代码能两行搞定一位的原因。参数上要注意 B 寄存器在两次除法之间被复用,第一次的余数必须先搬到 A 再赋新除数,顺序错了结果就全乱。

3.2 单步调试:存储器窗口才是真正的“黑匣子”观察口

这个实验强制要求单步运行,目的不是折磨人,而是让你亲眼看到 30H 被改写后 31H~33H 逐字节变化的过程。指导书花了大量篇幅讲存储器窗口的地址前缀:d:看片内 RAM 低 128 字节和 SFR,i:看片内 RAM,c:看程序存储器,x:看片外 RAM。这套前缀不是 Keil 的随意发明,而是 51 哈佛架构的必然产物——程序空间和数据空间物理分离,片内片外又各有一套地址,不区分前缀根本没法定位。很多新手在这里踩坑,把d:30H写成x:30H,看到的是一片空白,还以为程序没跑。

提示:修改连续多个存储单元时,在 Modify Memory 对话框里用逗号分隔数据,比逐个改快得多,这也是指导书里一句容易被忽略的实用细节。

4. 动态扫描与矩阵键盘:8255 复用下的时序博弈

4.1 6 位 LED 动态显示:50Hz 刷新率是怎么保住的

实验 3 的核心机制是动态扫描:6 位 LED 共用一套段驱动(8255 B 口),位驱动由 A 口低 6 位控制,任一时刻只点亮一位,靠人眼视觉暂留合成整屏。指导书给出“刷新频率不小于 50Hz”这个门槛,换算下来每位点亮时间不能超过 1/50/6 ≈ 3.3ms,而子程序要求每位显示 0.5ms 后返回,6 位一轮 3ms,刷新率约 333Hz,余量充足。这个 0.5ms 不是随便定的,它和实验 1 的延时是同一套循环计数逻辑,只是量级缩小。

显示子程序的数据流值得拆开看:入口把 6 个待显示数放在 20H~25H,每个数是字形表索引(00H~1FH 对应 0~F、0.~F.、-、灭)。流程是取数、查表、送段口、送位口、延时、关显示、位码左移。位码初值 FEH(11111110B)表示最低位为低、选中左起第一位,左移一次就轮到下一位。这里有个设计取舍:段码表用MOVC A,@A+DPTR从程序存储器查,而不是放数据存储器,因为 51 的片内 RAM 只有 128 字节,字形表 32 个字节放程序空间更省,MOVC正是为此设计的指令。

; 显示子程序核心片段(示意) MOV R0, #20H ; 显示数地址指针 MOV R2, #0FEH ; 位码初值,选中第一位 DISP1: MOV A, @R0 ; 取待显示数 MOV DPTR, #TAB ; 字形表首址 MOVC A, @A+DPTR ; 查字形码 MOV DPTR, #4001H ; 8255 B 口,段驱动 MOVX @DPTR, A MOV A, R2 MOV DPTR, #4000H ; 8255 A 口,位驱动 MOVX @DPTR, A LCALL DELAY05 ; 延时 0.5ms MOV A, #0FFH ; 关所有位,防重影 MOVX @DPTR, A MOV A, R2 RL A ; 位码左移 MOV R2, A INC R0 CJNE R0, #26H, DISP1 RET

注意“关所有位”这一步,它是消隐的关键:如果不先关位再切下一位,上一位的段码会短暂串到下一位上,出现俗称“鬼影”的重影。这是动态扫描最经典的踩坑点,指导书流程图里专门画了“关闭所有显示位”这个框。

4.2 矩阵键盘扫描:列驱动加行读回的电气逻辑

实验 4 的 16 键矩阵接在 8255 C 口:PC0~PC3 是 4 行输入,PC4~PC7 是 4 列驱动。扫描原理是每次只让一列为低、其余列高,读行状态,若某行变低说明该列该行交叉点的键被按下。8255 初始化字10000001B的含义要拆开看:D7=1 方式设置有效,D6D5=00 A 口方式 0 输出,D4=0 A 口输出,D3=0 C 口高 4 位输出(列驱动),D0=1 C 口低 4 位输入(行读回)。一个字节同时配置了三个端口的方向,这就是 8255 控制字的设计动机——用一次写操作完成多端口方向设定。

查键值子程序KEY20的技巧在于把“列驱动码 + 行状态”打包进 A:高 4 位是列码(被扫描列为 0),低 4 位是行状态(按下位为 0)。先循环右移数出列号,乘 4 加上行号得到线性索引,再查KEYTAB得到键值。KEYTAB的排列0FH,0BH,07H,03H...对应图 4-1 的键值布局,索引和物理位置的映射全靠这张表,改键值只需改表,不用动逻辑,这是典型的“数据驱动”设计。

4.3 20ms 消抖与二次确认:机械触点为什么要等

键盘扫描流程图里有个“软件延时 20ms 再读行状态,二次相同?”的判断。机械按键闭合瞬间触点会弹跳,电平在几毫秒内反复跳变,直接读会误判成多次按下。20ms 是经验值,覆盖大多数轻触开关的抖动窗口。二次确认的动机是:延时后重读,若状态和第一次一致才认账,否则当作抖动丢弃。这里有个容易忽略的细节——指导书强调“读行状态直到键松开”时要滤掉高 4 位,因为 C 口低 4 位是输入、高 4 位是输出,读回时高 4 位反映的是你刚写出去的列驱动码,不是有效行信息,不屏蔽就会把列码误当行状态,导致键值算错。这个坑我在类似实验里见过不止一次。

5. 避坑与排查:实验箱上最容易翻车的几件事

5.1 现象:编译通过但硬件上 LED 全亮或全灭

原因通常是 8255 控制字写错端口方向,或者初始化前没做复位延时。指导书主程序里在 8255 初始化前有一段 0.5 秒延时“等待 8255 复位”,很多人预习时觉得多余就删了,结果 8255 上电复位未完成时写入的控制字被吞掉,端口停在默认输入态。解决办法是保留这段延时,并确认控制字10000001B与实验 4 的10000001B一致——实验 3 和实验 4 共用 8255,C 口方向配置不同,混用工程时最容易在这里翻车。

5.2 现象:单步能看到数据变化,连续运行却不对

原因多半是中断或寄存器冲突。实验 3 的显示子程序占用 R0、R1、R2、A、PSW、DPTR,主程序里如果也用 R1 做延时计数,调用显示子程序时计数被覆盖,延时长度就乱了。指导书在实验 3 重要提示里专门写“使用的计数单元与显示子程序中用到的不能冲突”,这不是废话。解决办法是给主程序和子程序划分不重叠的寄存器区,或者调用前 PUSH、返回前 POP。

5.3 现象:键盘按键后显示乱码或键值对不上

先查KEYTAB顺序是否和图 4-1 的物理布局一致,再查查键值子程序里MUL AB后是否误用了 B 寄存器。KEY22段用MOV B,#4和MUL AB算列偏移,乘法结果低字节在 A、高字节在 B,随后MOV R1,A只取了低字节,若列号较大导致结果超 255 就会丢高位——不过 4×4 键盘最大索引 15,不会溢出,这个隐患在更大键盘上才会暴露。更常见的是二次确认时忘了屏蔽高 4 位,把列驱动码当成了行状态。

5.4 现象:A/D 转换读回的值永远是 FF 或 00

实验 5 用 ADC0809,启动转换是“写任意数到 8004H”,取结果是“读 8004H”,EOC 经反相器接到 P1.0。查询 P1.0 时要注意反相器的存在:EOC 高电平表示转换结束,反相后 P1.0 变低,所以查询条件应该是等 P1.0 为低,而不是为高。搞反了就会在转换还没完成时就读结果,拿到的是上一次的残值或不定值。另外电位器 W1 分压进 IN4~IN7,确认接线选的是 IN4 而不是别的通道,通道选错读回的就是悬空值。

5.5 现象:修改程序后硬件行为不变

指导书反复强调“对程序修改后必须退出连接,进行编译后再重新连接才能生效”。硬件仿真模式下,Keil 通过 RS232 把代码灌进实验箱,不退出连接直接 Build,新代码根本没下载进去,你看到的还是旧程序在跑。这个坑几乎每个新手都踩过,表现为“改了跟没改一样”,排查时先看是否重新进入 Debug 会话,再看 HEX 文件生成选项是否勾选。

6. 从软件延时到查询式 I/O:这套实验真正想训练的判断力

把 6 个实验连起来看,会发现它们共享一个底层范式:不用中断、不用定时器,全靠 CPU 主动查询和软件计数来协调时序。实验 1 用循环数出 1Hz,实验 3 用循环数出 0.5ms 刷新窗口,实验 4 用循环数出 20ms 消抖,实验 5 用查询 P1.0 等 EOC。这种“查询式 I/O + 软件延时”的组合在真实产品里早就被定时器和中断取代了,但它作为教学载体有一个不可替代的价值:它把“时间”这个抽象概念彻底暴露成指令周期,让你对每一条指令的代价有肌肉记忆。

这里的设计取舍值得多想一层。软件延时的优点是零额外硬件、逻辑直白,缺点是 CPU 被完全占用、精度受中断和编译器影响;硬件定时器的优点是解放 CPU、精度由晶振保证,代价是要理解 TMOD、TH0/TL0、中断向量这些寄存器配置。指导书选择先教软件延时,是因为它把“时序 = 指令数 × 周期”这个等式摆在明面上,等你后面学定时器时,才知道硬件只是把这个等式固化进了计数器,本质没变。同理,查询式 A/D 和中断式 A/D 的差别,也只是“谁来等”的问题。

验证这套理解是否到位,有个简单办法:拿实验 1 的延时循环,把晶振从 12MHz 改成 11.0592MHz,重新算一遍计数初值,看方波频率偏了多少。11.0592MHz 是串口通信的常用晶振(能被 9600 等波特率整除),机器周期变成约 1.085μs,同样的循环会慢 8.5%,1Hz 变成约 0.92Hz。能自己推出这个偏差,说明你真的把晶振、机器周期、指令周期、循环结构这条链走通了,而不是背下了“延时 0.5 秒”这个结论。

我早年做类似实验时,习惯是每次改完延时参数先在软件仿真里单步数一遍循环次数,再上硬件看波形,从不直接信“应该差不多”。这套笨办法救过我好几次——有一次显示刷新率算错,仿真里数出来一轮 8ms,刷新率掉到 125Hz 以下,肉眼已经能看出闪烁,提前发现总比在实验箱前抓瞎强。这份指导书把 6 个实验的预习、调试、报告流程写得很细,真正照着走一遍,对 51 的时序直觉会比看十篇教程都扎实。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询