做课设或者复习微机原理的时候,这种题目特别容易让人一头雾水:8086、8253、方波发生器、Proteus仿真,每个词都认识,连在一起就不知道该怎么下手。我刚做完这个项目,把整个过程从头到尾捋了一遍,从8253的分频原理到汇编代码,再到Proteus里怎么把波形拉出来,中间踩了不少坑。这文章就当一份完整复盘,给正要动手的同学参考,也顺便聊聊那些教程里经常含糊带过的细节。
先说我最终实现的方案:用8086最小模式作为控制核心,外接一片8253可编程定时器,把三个计数器都配置成工作方式3(方波发生器),输入统一给1MHz时钟,三个通道分别输出10kHz、5kHz、1kHz的方波。在Proteus里用虚拟示波器三路信号一起看,效果非常直观。接下来我会按需求拆解、原理计算、硬件连接、程序设计、仿真联调、问题排查这条主线,把自己的做法和思考完整写出来。
1. 先拆需求:这个方波发生器到底要做什么
1.1 为什么是8253而不是软件延时
生成方波最简单的思路,是用8086通过延时翻转某个IO口的高低电平,循环执行就能得到方波,但这么做的缺点非常明显:CPU被一路方波输出占死,什么都干不了,频率还受指令周期、循环分支、外部读写等待状态影响,根本谈不上精确。8253这类可编程定时器/计数器存在的意义,就是把这些计数工作从CPU里剥离出来,独立成硬件逻辑。
8253内部有三个16位递减计数器,每个都独立工作。你把分频系数写进去,它会自己数时钟脉冲,数到边界时自动翻转输出并重新装载初值。CPU完成初始化之后就可以去干别的,方波会一直稳定输出,不会受到主程序乱跳的影响。这在真实嵌入式系统里是很常见的思路:显示刷新、蜂鸣器、串口波特率都靠定时器驱动。做这种实验,最重要的不是背8253引脚和寄存器的表格,而是理解硬件定时器解决了什么问题,以及为什么外设芯片能比CPU更可靠地维持信号节奏。
1.2 一个项目三条输出,直接体现“不同频率”
题目里说的是“不同频率的方波发生器”,翻译成可实施的设计,最简单直观的做法就是让8253的三个通道同时输出三路不同频率的方波。8253天然有三个独立计数器,用同一个1MHz时钟作为输入,分别装入不同的初值,三个OUT引脚就能各自输出对应的方波,互不干扰。用示波器三根探头一起看,立刻能判断每个通道的分频是否正确。
我选的三个输出频率是10kHz、5kHz、1kHz。原因不是这三个数有什么特殊,而是它们对应时钟频率1MHz的整数百分之一、二百分之一、千分之一,初值分别是100、200、1000,算起来不费劲,写在代码里也是一眼能看懂的十进制。读者拿到以后换成2kHz、500Hz、250Hz之类都没有任何问题,只要初值保持整数关系。
1.3 Proteus仿真比实物实验好在哪
如果你之前玩过实物微机实验箱,应该知道那种板子上跳线多、开关多,示波器探头接触不良就够折腾半天,更不用说8086的时序和8253的波形只能靠猜测。Proteus仿真的优势在于,所有节点都能直接拉出探针看电平,还可以放一个虚拟示波器实时观察输出波形,改代码、改接线也只是一次编辑的事,不存在烧芯片的风险。
但Proteus也不是万能。仿真模型对某些引脚时序的处理和真实芯片会有细微差异,比如输入时钟边沿的响应、复位后的启动过程,实际环境下需要考虑的外设时序、总线驱动能力在仿真里常常被简化。所以做这个项目时,既要依赖仿真工具去验证逻辑,也要时刻清楚它只是一个理想化模型。下面我会在硬连接和调试部分把这类差异单独拿出来说。
2. 8253的关键参数计算:控制字和分频初值
2.1 三个计数器、六种工作方式怎么选
8253的每个计数器都有CLK、GATE、OUT三个引脚,16位计数初值,支持六种工作方式。方式0是事件计数,计数到零时输出变高并保持,适合标志触发;方式1是可编程单稳态,宽度由初值决定;方式2是比率发生器,输出一个很窄的低脉冲,周期由初值决定;方式3是方波发生器,周期等于初值乘以时钟周期,占空比接近50%;方式4和方式5分别由软件和硬件触发启动,产生一次选通脉冲。
其中最容易混的就是方式2和方式3。方式2的输出在一个计数周期内大部分时间是高电平,到最后一个时钟周期变成低电平,然后重装初值,所以它本质上是“窄脉冲分频”;方式3则是在半个计数值内维持高电平、另外半个计数值维持低电平,输出才是方波。做方波发生器必须选方式3,这个选择不能拍脑袋,因为方式和后续的初值计算直接关联。
2.2 控制字逐位拆解
8253的操作方式由控制字寄存器决定,这个寄存器地址通常排在三个计数器之后,也就是四个端口的最后一个。控制字的每一位都有明确含义,我整理在这张表里:
| 位段 | 含义 | 我们的取值 | 说明 |
|---|---|---|---|
| D7 D6 | 计数器选择 | 00/01/10 | 00计数器0,01计数器1,10计数器2 |
| D5 D4 | 读写格式 | 11 | 先写低字节,再写高字节 |
| D3 D2 D1 | 工作方式 | 011 | 方式3,方波发生器 |
| D0 | 数制选择 | 0 | 0表示二进制计数,1表示BCD计数 |
对应到三个计数器,控制字分别是:
- 计数器0:
0011 0110,即36H - 计数器1:
0111 0110,即76H - 计数器2:
1011 0110,即B6H
提示:控制字必须写在初值之前。因为读写格式、数制选择都记录在控制字里,如果先写初值再写控制字,8253会按未知的模式接收数据,结果大概率是错误的。
2.3 分频系数 N = 输入时钟频率 / 输出频率
分频的数学关系非常直接:计数器初值N等于输入时钟频率除以想要的输出频率。比如输入时钟是1MHz,想要10kHz,N就是100。这个N是十进制数,初始化时要拆成低字节和高字节,先写低字节后写高字节。
三个通道的具体计算如下:
| 输出通道 | 输入时钟 | 目标频率 | 初值N | 十六进制 | 低字节 | 高字节 |
|---|---|---|---|---|---|---|
| OUT0 | 1MHz | 10kHz | 100 | 0064H | 64H | 00H |
| OUT1 | 1MHz | 5kHz | 200 | 00C8H | C8H | 00H |
| OUT2 | 1MHz | 1kHz | 1000 | 03E8H | E8H | 03H |
这里有几个细节需要留意。首先,单通道初值上限是65535,所以理论上最低输出频率是输入时钟除以65536,在1MHz输入下大约15Hz。想要更低的频率,最实用的办法是把两个计数器级联,比如计数器0先做1000分频输出1kHz,再把OUT0接到计数器1的CLK输入,计数器1做100分频,最终输出就是10Hz,总的分频系数是两者相乘。
其次,16位初值分两次写入,这个过程有一个容易被忽视的行为:8253只写低字节时,计数器不会立刻开始工作,要等高字节写入后才装载新初值并启动。所以如果你只打算支持小于等于255的初值,可以在控制字里把读写格式改成“只读写低字节”,这样写一个字节就生效。我代码里统一用“先低后高”的完整格式,减少认知负担。
3. 8086最小系统下的8253硬件连接
3.1 8086的引脚怎么接才稳定
8086有两种工作模式:最小模式和最大模式,通过MN/MX引脚选择。这个实验控制逻辑简单,用最小模式就够了,把MN/MX接+5V。接着是复位电路,RESET引脚需要上电高电平再归零,Proteus里用一个按钮加RC电路就能模拟。READY引脚接高电平,告诉CPU每次都无需等待;TEST引脚同样接高电平;INTR和NMI都接地,不让外部中断来打扰。
CLK引脚接时钟源,8086典型工作频率是5MHz。Proteus里用DCLOCK数字时钟组件给CLK供一个固定频率的方波。
8086的AD0到AD15是地址数据复用引脚,在总线周期的前半段输出地址,后半段传输数据,所以必须用锁存器把地址锁存出来。最小模式下ALE引脚就是用来控制锁存时刻的:ALE高电平时地址线有效,ALE下降沿把地址锁进74LS373或74LS573,之后总线才切换为数据状态。这个锁存过程是整个8086最小系统能正常读写外设的基础,千万别省。
3.2 8253和8086的总线怎么连
8253的D0到D7数据脚直接接到系统的数据总线上,A0、A1接8086的地址总线低位A0、A1,用来选择内部四个寄存器。RD和WR接8086的RD和WR信号,读写信号配合片选CS一起工作:只有CS有效时,8086才能通过数据总线访问8253。
8253的GATE引脚是门控输入,方式3下必须接高电平,否则计数器不会运行。很多人画图的时候漏接GATE,导致输出端死活没有波形,这是初学者特别容易栽的一个坑。
3.3 端口地址译码到底怎么算
8253内部只有两个地址引脚A0和A1,能区分四端口,但片选CS需要额外电路来产生,这是整个硬件连接里最需要动脑子的一步。8086访问IO设备时,IO/M引脚为低电平,表示当前周期是IO读写;我的方案用一个或非门电路,把IO/M和地址线A2组合成CS信号。当IO/M为低、A2也为低时,CS有效,8253被选中。
这种情况下,8253四个端口的地址就落在00H到03H,具体对应关系是:A1A0为00时是计数器0,01是计数器1,10是计数器2,11是控制字寄存器。
如果系统里还有其他IO设备,低地址撞车,就把A2换成A9之类的更高位地址线,或者在IO周期里对高位地址做更严格的译码。用74LS138译码器可以一次解决多设备分配,像8255、8259这些外设同时出现时,每个设备分到一个译码输出,互不冲突。这个实验只有一个外设,门电路方案最直观,也最适合用来理解译码的本质:地址线和IO/M经过组合逻辑,产生一个低有效的片选,让外设只在指定地址区间参与总线访问。
3.4 时钟源的几个要点
8253的CLK输入独立于8086的系统时钟,Proteus仿真的话,三个CLK直接各接一个DCLOCK,或者共用一个DCLOCK分三路引出,频率设为1MHz。8253要求CLK输入是标准TTL电平的方波,高电平低电平都要满足最小脉宽,DCLOCK的默认参数已经满足。
需要注意,CLK频率决定了你能输出的频率范围,初值上限65535决定了最低频率下限。如果想要非常精准的输出频率,最好把CLK来源做成晶振分频或者用频率更高的时钟。8253本身允许的输入频率最高到几兆赫兹,1MHz是一个很安全的选择。
4. 汇编程序设计以及动态切换频率的思路
4.1 初始化流程和代码骨架
程序的核心逻辑只有两步:先写控制字,再写初值。控制字决定了每个计数器的工作方式、读写格式、数制,初值决定了输出频率。代码里我用16位读写格式,所以每个通道要装两次数据,低字节在前高字节在后。
初始化三个通道的完整代码我贴在这里:
; 不同频率方波发生器 ; 8253三通道均工作于方式3(方波发生器) ; CLK0=CLK1=CLK2=1MHz ; 输出:OUT0=10kHz, OUT1=5kHz, OUT2=1kHz ; 8253端口地址:00H~03H ; ; 00H 计数器0 ; 01H 计数器1 ; 02H 计数器2 ; 03H 控制字 CODE SEGMENT ASSUME CS:CODE START: ; ---- 三个计数器都初始化为方式3 ---- MOV DX, 03H ; 指向控制字寄存器 MOV AL, 36H ; 计数器0:方式3,二进制,先低后高 OUT DX, AL MOV AL, 76H ; 计数器1 OUT DX, AL MOV AL, 0B6H ; 计数器2 OUT DX, AL ; ---- 计数器0初值100 -> 10kHz ---- MOV DX, 00H MOV AX, 100 OUT DX, AL ; 写低字节64H MOV AL, AH OUT DX, AL ; 写高字节00H ; ---- 计数器1初值200 -> 5kHz ---- MOV DX, 01H MOV AX, 200 OUT DX, AL MOV AL, AH OUT DX, AL ; ---- 计数器2初值1000 -> 1kHz ---- MOV DX, 02H MOV AX, 1000 OUT DX, AL MOV AL, AH OUT DX, AL HERE: JMP HERE ; 主程序驻留 CODE ENDS END START这段代码写完以后,三路方波会立刻开始输出。主程序最后用一个跳转占住CPU,保证程序不会跑进未定义的区域。在很多真实课设里,最后往往还应该加一个停机或者返回到操作系统的操作,但在裸机仿真里,原地驻留是最稳妥的结束方式。
4.2 动态切换频率的思路
固定三路输出能证明8253基础能力,但“不同频率”若还能动态变化,效果会更好。8253支持运行期间随时重新装载初值,所以只要程序按需向某个计数器端口写入新初值,输出频率就会跟着改变。
做法很简单:先初始化某个通道为方式3,然后在一个循环里每隔一段时间改变初值。比如计数器0先装初值100输出10kHz,延时一段时间后重新写入初值200输出5kHz,再写入400输出2.5kHz,如此循环。8253在重新写入初值时并不会立刻打断当前输出,它会等当前半周期走到边界之后按新初值继续工作,所以切换过程不会出现毛刺。
这个特性在很多需要动态变频的场景中很有用,比如产生不同音调的提示音,或作为某种可调基准信号源。在学习层面,理解“随时重装初值”这件事,比死记某个控制字更有价值。
5. 在Proteus里搭电路、加载程序、量波形
5.1 元件选择与原理图绘制顺序
Proteus里需要放置的元件包括8086、8253、74LS373锁存器、或非门、存储芯片、DCLOCK时钟源、按钮和示波器。顺序上建议先画8086最小系统,再画8253,最后接存储芯片和虚拟仪器,这样调试时每部分都能独立确认。
画8086时,先把MN/MX、READY、TEST、INTR、NMI、RESET这些控制引脚处理完,再接总线。AD0到AD15引出到锁存器输入端,锁存器输出就是稳定的地址线A0到A15。74LS373的OE引脚要接地,G引脚接8086的ALE。8253的A0、A1接锁存后的地址线A0、A1,数据线直接连AD0到AD7。
8253的CS我用或非门电路生成:或非门一个输入接IO/M,另一个输入接地址线A2的反相输出,当IO/M为低且A2为低时输出有效,连接8253的CS。RD接8086的RD,WR接8086的WR,GATE0到GATE2全部接+5V,CLK0到CLK2接到1MHz的DCLOCK。
存储芯片这一块,我建议选一片容量足够的SRAM,比如62256,地址线和数据线按常规方式接到系统总线,程序加载区域和栈区域都放在这片RAM里。还要在复位向量所在位置放一条跳转指令,做好了以后CPU复位能跳到主程序入口。
5.2 编译、加载程序的具体操作
汇编程序写完后,用MASM或EMU8086编译。如果用的是MASM流程,ML /c编译出obj,再用LINK链接成exe,最后用EXE2BIN工具转成bin文件;EMU8086可以直接导出bin格式。
在Proteus里加载程序,常见做法是双击8086模型,在属性对话框里找到程序文件设定项,把bin文件路径填进去。也可以把bin文件内容加载到外部RAM里。这个加载操作完成之后,运行仿真,8086会从复位向量开始取指。
注意:如果仿真运行后程序没有按预期初始化8253,优先检查复位向量位置上的跳转指令是否正确。8086上电复位固定从FFFF0H执行,如果那里没有跳转指令,CPU就会把随机数据当指令执行,表现出什么都不发生的现象。
5.3 虚拟示波器的接线和读数
Proteus左侧工具栏有虚拟仪器,选择Oscilloscope,拖到原理图里。示波器的A、B、C输入通道分别接OUT0、OUT1、OUT2,示波器地线接电路地。运行仿真后打开示波器面板,设置合适的时基,10kHz信号周期100微秒,5kHz周期200微秒,1kHz周期1毫秒,用1ms或者2ms的时基可以同时看到三路波形。
更省事的方法是在三个OUT引脚上挂频率计组件,直接显示频率数值。虚拟频率计同时显示频率和周期,验证代码里的计算公式非常方便。我第一次跑仿真时,先用频率计确认每个通道的频率,再用示波器看波形形状,两个工具配合起来效率很高。
6. 调试过程中踩过的坑和排查顺序
6.1 示波器完全没有波形,先查这些
如果仿真跑起来但OUT引脚什么都没有,不要急着怀疑8253坏了,按这个顺序查:第一,看8086有没有真正执行到初始化代码,最好的方式是给控制字端口处加一个电压探针,如果能观察到写脉冲,说明CPU至少执行到了OUT指令;第二,检查GATE是不是都接了高电平,8253在GATE为低时会冻结计数,输出停在当前状态;第三,检查CS片选信号,用逻辑探针看I/O访问周期里CS有没有变低;第四,确认控制字和初值有没有写到正确的地址,如果端口地址计算错了一位,所有初始化都白做。
6.2 复位后程序不启动,大概率是复位向量问题
这是8086仿真里最隐蔽的坑。8086复位后,CS寄存器被设置为F000H,IP被设置为FFF0H,取指地址是FFFF0H。这个位置不在普通RAM的低地址区域,如果那里没有正确的跳转指令,CPU取到的是随机数据,程序自然跑飞。解决方法是把复位向量位置的跳转指令提前烧进程序文件:在代码里把跳转指令定位到FFFF0H处,让它跳到主程序所在地址。不同Proteus版本对程序文件入口的处理可能不同,但理解复位向量原理后,无论模型怎么变都能对症下药。
6.3 输出频率不是预期值,计算还是配置问题
频率不对的排查点主要有三个。第一,初值计算公式是否正确,N等于输入频率除以输出频率,有人会把周期概念混进来,导致初值差了一千倍。第二,时钟源频率是否真的是1MHz,DCLOCK属性里的Frequency单位可能是Hz,要确认没把1MHz填成1Hz。第三,控制字的BCD位有没有写成1,如果用了BCD计数而初值里有大于9的位,计数器会完全错乱,一般项目都用二进制模式。
还有一个和占空比有关的判断:如果方波的占空比失真,比如高低电平时间明显不同,先看初值是不是奇数。方式3在偶数初值时输出严格50%占空比,奇数初值时高电平会比低电平多一个时钟周期,这是8253的正常行为,不是故障。
6.4 多个设备呼吸冲突和总线驱动力问题
如果系统里叠加了其他设备,地址译码必须做到“只命中自己”。比如用译码器方案时,未参与译码的高位地址线如果没有固定电平,就可能出现多个地址都能选中同一个设备。解决办法是让所有地址线要么参与译码,要么明确接到固定电位。
另一个容易被忽略的地方是总线驱动。Proteus仿真里总线连接相对宽松,真实硬件则要考虑接在总线上的所有芯片输出使能是否冲突,特别是存储器读周期和8253读周期都发生在同一个IO/M判断体系下,需要仔细核对每个设备的输出使能。做仿真项目时提前养成标注总线方向的习惯,对后续做硬件非常有帮助。
7. 一些仿真之外的补充认识
8253在做这个实验的时候表现很稳定,但大多数人很容易把它的能力和8254搞混。8254是8253的升级版,引脚兼容,最高工作频率更高,还增加了回读命令,可以一次读取多个计数器的当前值和状态。Proteus元件库里两者都有,如果选到的是8254,基本流程完全一致。另一个扩展方向是级联,当目标频率低于十几赫兹时,单个通道的16位初值已经不够用,把两个计数器串联是标准解法,总初值等于两个通道初值的乘积。
方波发生器做完以后,可以继续加一个8255并行接口,把频率选择和显示功能外接出来,用拨码开关选择频率档位,LED或数码管显示当前输出频率,这样整个系统就从“纯定时器演示”变成了“带人机交互的小仪器”。这类扩展对理解微机系统的IO扩展方式帮助很大,也能让课设项目更完整。
我自己在做完这个实验之后,最深的体会是:仿真里出现问题时别急着怀疑芯片模型,先把自己的地址、时钟、控制字这三个基础参数核对一遍,大部分问题都出在“我以为是”和“实际是”之间的那一点点偏差上。把IO读写时序和8253的分频原理吃透之后,这个项目不仅不再神秘,还会成为后续所有8253、8254扩展应用的底子。