8086计算器课设全解析:从8255A扩展到Proteus仿真 “8086计算器”这个课设题目,几乎每个学微机原理的工科生都会碰到。我当年看到题目的时候觉得挺简单:一个四位一体数码管、一个4x4矩阵键盘、加个加减乘除,能有多难?结果一上手才发现,从8086最小系统到8255A扩展、从键盘扫描到动态显示,再到Proteus仿真里各种莫名其妙的坑,每一步都够写一篇小作文的。这篇文章我打算按自己做这个项目的完整流程来讲:先拆需求,再搭硬件,然后写汇编,最后聊仿真调试和踩坑经验。内容不追求代码逐行注释,重点是让你把这个系统的数据流、状态机、端口规划彻底想明白。看完之后,不管你是照着复现还是改一改进阶功能,都能有个清晰的底子。1. 项目定位与功能拆解1.1 看似简单,拆开其实是四个子系统题目给的需求很克制:实现加减乘除运算,通过四位一体数码管显示。但这句话落到8086这种纯CPU芯片上,等于什么都没说。8086本身没有键盘接口、没有显示驱动能力,所有外设都得靠总线扩展。真正拆开,一个完整的计算器至少包含四块:输入子系统:4x4矩阵键盘 扫描程序,解决“用户按下哪个键”的问题。计算子系统:用8086的加减乘除指令完成运算,处理负数、溢出、除数为零这些边界情况。显示子系统:四位一体数码管 动态扫描程序,解决“结果怎么显示出来”的问题。控制中枢:一段主循环状态机,把输入、运算、显示按正确的顺序串起来。这四个子系统单独拎出来都不难,难点在它们之间的配合:键盘按下数字键,要能正确拼成操作数;按运算符,要切状态;按等号,要调运算子程序;运算结果要转成每位数字,再让数码管稳定地显示出来。一条数据流从头走到尾,中间任何一步出岔子,最终看到的都是错误数字或者乱码。1.2 总体架构:8086 8255A 的经典组合CPU选型没悬念,题目指定的8086。但8086这种CPU是没法直接驱动数码管和键盘的,它只能通过总线给外设发地址和数据。所以我用了经典的8255A可编程并行接口芯片,作为8086和外设之间的“中转站”。数据流的走向是这样的:按键按下 → 8255A的PC口扫描键盘 → 8086读回键值 → 存入内存里的操作数/运算符 → 运算子程序得到结果 → 结果转成十进制数字 → 通过8255A的PA口送段码、PB口送位选 → 数码管显示这里8255A承担了两头差事:一头是输入(P口读键盘列信号),一头是输出(PA口送段码、PB口送位选)。8086只需要往端口地址写数据、从端口地址读数据,剩下的并行时序全交给8255A处理,这是微机原理课设里最标准的扩展方案。1.3 一个关键的设计决策:四位一体显示约束四位一体数码管只有4位,这意味着任何超出四位显示范围的结果都无法完整显示。如果输入99和99,乘法结果是9801,四位正好;但如果是999乘999,结果是998001,直接爆掉。所以我在设计时把输入操作数限制为0~99的两位数,这样加减乘除结果都落在四位显示范围里:加法最大198,减法最小-99,乘法最大9801,除法最多99。负数怎么显示?我选择让最高位显示负号,后两位放绝对值,第四位熄灭。比如-98,就是“-98”空一位。这样做的好处是逻辑简单,不引入额外的符号灯。注意:如果你希望支持三位数输入,就必须增加溢出检测,超出四位范围时在数码管上显示一个“E”作为错误提示。这个我在后面会专门讲实现思路。2. 硬件电路设计要点2.1 8086最小系统:先让CPU跑起来在Proteus里搭8086系统,比实物省心很多,不用自己焊晶振电路,芯片内置了程序存储器和数据存储器。但有几个引脚必须接对,否则CPU根本没反应:MN/MX引脚接5V:8086工作在最小模式,才能从外部引脚直接引出ALE、RD、WR这些控制信号。CLK接时钟源:我习惯把频率设为1MHz,不是不能更高,而是仿真速度会明显变慢,调试时很痛苦。RESET接地:8086的复位引脚是高电平有效,正常工作时要让它处于低电平。我见过有人把RESET直接接5V,结果整个仿真里CPU一直在复位状态,程序死活跑不起来。READY接5V,INTR、NMI接地:用不到的请求引脚全部关掉,避免干扰。最小模式下,8086的AD0~AD15是地址数据复用总线,时序上是先出地址、后出数据。要让8255A稳定地拿到底地址,必须用74LS373锁存器把地址锁下来。接法也不复杂:74LS373的输入接AD0~AD7,使能端G接8086的ALE引脚,输出端就得到了可靠的A0~A7地址线。如果要做更完整的系统,还可以加第二片74LS373锁存高8位地址,但纯IO扩展场景下低8位地址够用。2.2 8255A端口规划:每个口干什么,提前说清楚8255A有3个8位端口(A、B、C),工作方式很多,但这里只用方式0——也就是简单的并行输入输出,每个端口方向由初始化控制字决定。我的分配方案是这样的:端口方向用途PA口输出数码管段码:接a~g和dpPB口输出数码管位选:控制四位中哪一位亮PC低4位输出键盘行扫描信号PC高4位输入键盘列检测信号初始化时向控制寄存器写控制字0x85(二进制1000_0101),含义是:A组方式0、PA输出、PC高4位输入,B组方式0、PB输出、PC低4位输出。这个0x85经常有人写错,一定记清楚PC口是“半输入半输出”,不是全输入也不是全输出。2.3 键盘矩阵和四位一体数码管的接线4x4矩阵键盘一共16个键,我分配的布局是:行0: 1 2 3 行1: 4 5 6 - 行2: 7 8 9 * 行3: C 0 /行线接PC0~PC3,列线接PC4~PC7。扫描的基本原理就是:先把某一行的行线拉低,然后读列线。如果某列也变低了,说明这个交叉点的按键被按下。比如行0输出低电平、读到列1为低,那按下的就是数字键“2”。数码管我用的是四位一体共阴型号。段码端a~g、dp经过限流电阻接PA0~PA7,位选端COM1~COM4接PB0~PB3。共阴管的位选是低电平有效:某一位的COM被拉低,这一位才会工作。所以位选码要设置成:显示位PB口输出位选码第1位(最左)11100xFE第2位11010xFD第3位10110xFB第4位(最右)01110xF7注意:Proteus仿真里,8255A的输出能力可以直接点亮数码管,但做实物板子时不能这么干。段码和位选都必须加驱动,位选用PNP三极管或者ULN2003都是常规操作。仿真能跑,不代表实物能跑,这是两个世界。2.4 地址译码与端口地址约定8255A内部有4个寄存器(PA、PB、PC、控制字),需要通过A1、A0两根地址线区分。但同时整个8255A要有一个片选信号CS,这个我用74LS138译码器产生。最简单的做法:74LS138的C、B、A分别接A7、A6、A5,Y0作为8255A的CS。这样只要高三位地址是000,8255A就被选中。A1、A0接锁存后的地址线,于是四个端口地址可以固定为:PA口 0000HPB口 0002HPC口 0004H控制字 0006H这几个地址在汇编代码里直接用EQU定义就行。端口地址是“电路决定”的,不是写死的,所以每个人接线不同,端口地址可能不同,但生成规则是:只要记住A1、A0决定内部寄存器,片选决定基地址,就能推出来。3. 软件系统:从按键到显示的完整链路3.1 数据区和状态机设计写汇编之前,先把数据区规划好。我的内存布局大概是这样的:DATA SEGMENT NUM1 DW 0 ; 第一个操作数 NUM2 DW 0 ; 第二个操作数 RESULT DW 0 ; 运算结果 SIGN DB 0 ; 结果符号:0为正,1为负 ERR_FLAG DB 0 ; 错误标志:0正常,1溢出/除零 OPCODE DB 0 ; 运算符:1,2-,3*,4/ KEYVAL DB 0 ; 当前按键值 DISP_BUF DB 4 DUP(0FH) ; 显示缓冲区,0FH表示熄灭 STATE DB 0 ; 状态机状态 DATA ENDS这个计算器需要三种状态:状态0:等待输入第一个操作数。此时按数字键就拼NUM1,按运算符就记录OPCODE并切到状态1。状态1:等待输入第二个操作数。按数字拼NUM2,按等号就执行运算,进状态2。状态2:显示结果。按“C”清零回状态0,按数字键则自动开始新一轮运算。状态机虽然简单,但它是整个程序的主心骨。你千万别把所有逻辑都堆在一个大循环里判来判去,那样写着写着就乱套了。先用一个STATE变量把“现在处于什么阶段”定义清楚,后面的代码结构会清爽很多。3.2 键盘扫描:行列扫描算法键盘扫描是高频执行的核心子程序。完整流程分四步:向PC口低4位全部输出0,相当于同时扫描四行。读PC口高4位,如果全部为1,说明没有按键被按下。如果有某一列为0,则一行一行精确扫描:每次只把一行拉低,配合前面的列信息确定键值。返回键值,并等待按键释放。这里要注意一个很多人会踩的坑:按键在按下和释放的瞬间都有机械抖动,仿真里虽然不明显,但实物的抖动可以持续十几毫秒。我习惯在检测到按键之后加一段20ms延时,再确认一次电平,确保不是误触。放掉按键之前,程序不继续处理下一次按键。核心扫描逻辑的汇编骨架:KEY_SCAN PROC ; 先整体扫描,判断是否有键按下 MOV DX, PORT_C MOV AL, 00H OUT DX, AL IN AL, DX AND AL, 0F0H CMP AL, 0F0H JZ NO_KEY ; 有键,逐行扫描 MOV BX, 0 ; BX低8位存行号,BL行号 ROW_LOOP: ; 根据BL的值计算行扫描码:1110,1101,1011,0111 MOV AL, 0FEH ; 先按第0行计算 ... ; 读列,判断键值 INC BL CMP BL, 4 JB ROW_LOOP NO_KEY: RET KEY_SCAN ENDP简化起见,上面只写了结构,实际代码里需要把行扫描码放进一个表或者用移位方式生成。键值的计算很简单:键号 行号 × 4 列号,然后查一个键值映射表,转成它代表的数字或运算符。3.3 计算核心:加减乘除与边界处理这个部分是整个项目的“智力担当”。虽然只有四条指令,但每条都有讲究。加法用ADD指令,但我建议加完判断溢出标志OF。虽然两位数加法不可能溢出,但保留这个判断,以后扩展输入范围时不用改框架。减法要处理负数。SUB之后如果结果小于0(CF1),就用NEG取补得到绝对值,同时把SIGN标志置1。这相当于手动实现了一次取绝对值:如果是负数,取反加一,记一个负号;如果是正数,直接存结果。乘法用IMUL有符号乘。8086的16位乘法结果是32位,DX:AX存储。如果DX是0或FFFFH(DX是AX的符号扩展),说明结果还在16位范围内;否则就是真正的溢出,置ERR_FLAG。在我们的输入范围里(NUM1、NUM2都不超过99),乘法结果一般不会溢出,但判断逻辑还是要写,以后改成三位输入就用得上了。除法是最大的坑。首先,除数为0会触发8086的0号中断,在Proteus里通常表现为程序直接飞掉,没有任何提示。所以除之前必须判断NUM2是否为0。然后在做IDIV之前,先要用CWD指令把AX的符号扩展到DX:AX,否则除法的被除数扩展不完整,结果完全不可信。CASE_DIV: MOV AX, NUM2 OR AX, AX JZ DIV_ZERO ; 除数为0,置错误标志 MOV AX, NUM1 CWD ; 符号扩展 IDIV NUM2 ; 商在AX MOV RESULT, AX JMP SHOW_RESULT DIV_ZERO: MOV ERR_FLAG, 1 JMP SHOW_ERROR这里我特地说明:除法只保留整数商,余数被丢弃。这是课设级别的简化,不会出现小数。想要小数显示,需要在数码管上加小数点逻辑,复杂度会上一档,基础版不建议碰。3.4 动态显示:人眼欺骗术四位一体数码管内部其实是四个独立的共阴数码管,它们共用段码线,但各自的COM引脚独立。这意味着任何时刻,同一段码会同时送到四个位,但只有被选中的那一位会显示。要显示一个四位数字,就必须“动态扫描”:一位一位轮流点亮,每位置点亮大约1~2ms,四位数码管全部扫完一轮不超过8ms,人眼视觉暂留就会把它看成同时稳定的四位数字。显示子程序循环执行四步:从DISP_BUF取当前位对应的数字。查段码表,送PA口。查位选表,送PB口。延时1ms,然后继续下一位。段码表是共阴数码管的标准码:SEG_TABLE DB 3FH,06H,5BH,4FH,66H DB 6DH,7DH,07H,7FH,6FH对应数字0~9。如果结果带负号,最高位直接写一个负号段码40H。如果出现错误,就显示“E”,段码是79H。这里要特别强调:主循环必须持续调用显示子程序,不能只在按键操作之后刷一次。很多新手把显示放在按键处理流程里,结果就变成“没按键时数码管闪烁或熄灭,按一下才亮一下”。正确做法是主循环里无限循环:扫键盘 → 如果有键就处理 → 调用显示 → 再扫键盘。哪怕CPU在等待按键释放,也要保持显示刷新,否则人眼看着就是闪。从结果到DISP_BUF的转换用“除10取余”法:把结果反复除10,每次的余数就是从低位到高位的数字。比如9801,第一次除10余1,再除10余0,再除余8,最后商9,得到1、0、8、9,倒序放到缓冲区就是9、8、0、1。这个转换子程序不依赖具体位数,只要循环直到商为零,非常通用。4. Proteus仿真落地4.1 工具链与开发流程我用的开发方式是:汇编编译用MASM,仿真用Proteus ISIS。写代码可以直接用记事本或者任意编辑器,也可以拿emu8086先做算法验证。我个人的建议是:先在emu8086这种模拟环境里把运算逻辑(state machine、数值转换)调通,因为那里可以单步看寄存器、改内存,非常直观;然后复制到MASM环境下编译,最后进Proteus调外设时序。为什么不一开始就在Proteus里调?因为Proteus的8086仿真虽然能做单步,但在外设多的场景下非常卡,单步一次要等好几秒,调试效率低到让人怀疑人生。算法层面的错误(比如状态机串了、符号扩展忘了)用模拟器几分钟就能看出来,没必要在Proteus里硬碰。4.2 仿真工程搭建与程序加载在Proteus里新建工程之后,按下面的顺序搭电路:放一个8086、两片74LS373、一片74LS138、一片8255A。放一个时钟源CLOCK,频率设1MHz。放一个四位一体共阴数码管7SEG-MPX4-CC和一个4x4键盘,或者用独立按键拼矩阵。按前面说的接线方式连起来,特别注意MN/MX、RESET、READY这些关键引脚。程序加载是很多新手卡住的地方:双击Proteus里的8086芯片,在属性面板里找到“Program File”,把MASM编译出来的EXE文件选进去;同时把“Internal Program Memory Size”设成比程序实际大小更大的值,比如0x2000。如果加载完运行起来什么都没发生,先确认RESET确实处于低电平,再确认时钟源真的在产生脉冲。用逻辑探针在CLK引脚测一下,立刻就知道引脚接没接对。4.3 仿真里的调试手段Proteus里的“电压探针”和“逻辑探针”是调试外设的利器。我特别喜欢在8255A的PA、PB输出脚上挂逻辑探针,配合数码管显示来判断问题出在哪一层:如果PA口有正确的段码输出但数码管不亮,问题在数码管供电或共阴共阳设置。如果PB口位选码在变化但某一位永远不亮,检查位选码和COM脚的对应关系。如果键盘怎么按都没反应,在PC口挂探针,看行扫描码是否在轮换、列电平是否变化。比探针更好用的是“单步运行”,配合查看8086内部寄存器和内存数据。每次单步后观察AX、BX这些寄存器是否符合预期,能快速定位到是哪条指令算错了。5. 常见问题与避坑记录5.1 显示类故障现象可能原因解决办法数码管上电全亮/全乱码上电没清显示缓冲区初始化时立刻写熄灭段码00H,并把位选全置无效只有一个位亮,其他三位熄灭动态扫描没跑起来,只写了一次位选确认主循环里持续调用显示子程序显示的数字笔画不对段码表和共阴/共阳不匹配先单独测试PA口输出0x06,应该显示“1”数字明显连在一起或者变暗扫描周期太长或太短每位置亮1~2ms,一轮不超过10ms,这是视觉暂留的合理参数5.2 按键类故障现象可能原因解决办法按一下键当成按了好多次没做松手检测识别到按键后等释放,再做下一次扫描某些按键永远按不出来行列接反或者PC口方向配错重新检查行线接PC0~3、列线接PC4~7,控制字0x85确认无误按键反应特别慢消抖延时太长消抖控制在20ms左右,别用几秒的延时两个键同时按下会乱矩阵键盘没有二极管隔离课程设计不要求支持多键同时按,避免这个问题即可5.3 运算异常与仿真异常现象可能原因解决办法除法一执行整个仿真就卡死除数为0触发0号中断除之前必须判断NUM2是否为0负数结果显示成了很大的正数没有处理SIGN标志SUB后判断CF,用NEG取绝对值乘法结果明显不对忘记检查DX的符号扩展IMUL后判断DX是0还是FFFFH仿真速度非常慢时钟频率太高或者动画帧率太高CLK降到1MHz,Proteus菜单里关掉部分动画更新程序加载后整个CPU没有任何反应RESET被接成高电平RESET接GND,确认后CPU才会从复位状态释放5.4 我最想强调的调试顺序如果你照着做,千万不要一上来就把整个程序写完再跑仿真,那样出了问题会很难定位。我自己的顺序永远是模块化验证:先让数码管固定显示一个“1”,证明段码表、位选、PA/PB口配置全对;再写动态扫描,让四个位串行亮起来;然后单测键盘,按下一个键使某个端口翻转一次,确认键值和扫描逻辑没问题;最后才把运算和状态机接进去。每一步都有明确的验收标准,调试就是填空,不是大海捞针。6. 关于扩展和一点个人经验这个项目做完,如果还有余力,我最推荐的几个扩展方向:支持三位数输入并把溢出结果显示成“E”;增加连续运算(按完等号之后再按运算符,直接拿结果当下一轮的操作数1);用共阳数码管和PNP三极管驱动,把仿真方案无缝转化成实物。这些扩展都只改软件状态机和数据宽度,硬件几乎不用动。我个人做下来最大的体会是:这门课设真正的难点不是“会写几条汇编指令”,而是把一个简单的需求拆成可靠的外设链路。指令总共就那么二十来条,但端口规划、状态机设计、时序配合,每一个决策背后都有取舍。你要是能把这里面的逻辑想明白,以后在单片机上写计算器、写仪表界面,思路都是通的。参考资料也是真的,不是客套话。如果你打算深挖,建议把以下几个点挨个查透:8255A方式0控制字解析、74LS373地址锁存原理、74LS138译码真值表、8086最小模式下的ALE时序、动态扫描的时间参数计算。把这五样东西吃透,这个项目就不仅仅是一个课程作业了。