基于8086的交通灯控制系统设计:从最小系统到完整课设

发布时间:2026/9/6 14:36:09
基于8086的交通灯控制系统设计:从最小系统到完整课设 简介基于8086的交通灯控制系统课程设计说明书面向微机原理与接口技术课程设计及汇编语言实践者。文档按课程设计任务书、系统总体方案、芯片选型、硬件电路、软件流程、程序调试与设计心得组织章节结构完整。设计采用8086最小模式配合8255A并行接口、8253定时器和LED数码管完成十字路口红、绿、黄灯的定时切换与倒计时显示内容涵盖电源、存储器、I/O接口、LED驱动等硬件电路并给出控制程序流程图、完整汇编源程序和调试说明。资源为1个PDF文件压缩包仅944KB便于直接阅读打印。已有139人学习适合正在完成8086/8088课程设计或希望掌握微型机应用系统开发方法的读者。通过该说明书可快速复现设计思路、复用汇编程序框架并参考规范格式撰写课程设计报告。 又到了一年一度电子系统设计课交报告的节点。我整理移动硬盘时翻出了当年做8086交通灯控制系统设计的整套资料Proteus仿真图、汇编源码、设计文档、答辩PPT零零散散一大堆。这个题目看起来简单无非是四个方向的几盏灯按顺序亮灭但真正从8086最小系统开始搭把定时、倒计时显示、按键干预、异常状态全串起来之后你才会发现它几乎覆盖了微机原理接口技术的全部重点。所以每年选这个题目的人都非常多也每年都有人在地址译码、8253初值、数码管段码上熬到半夜。这篇文章我打算把老资料重新整理一遍结合当年踩过的坑给正在做基于8086的交通灯控制系统设计的朋友一份能直接参考的完整思路。1. 交通灯课设究竟在考什么状态、时间与异常处理1.1 一个十字路口的基本运行逻辑交通灯控制系统的功能拆开来看其实很朴素。以最常见的双向四组灯为例东西方向和南北方向按照固定时序交替通行东西绿灯亮30秒紧接着东西黄灯亮3秒然后东西红灯亮起南北方向绿灯放行30秒再南北黄灯3秒如此循环。最关键的一点是这个循环不是简单的延时之后翻转IO而是有明确的状态边界。东西方向的状态依次是绿灯、黄灯、红灯南北方向状态依次是红灯、红灯、绿灯、黄灯两边永远保持互斥关系。如果设计得不好比如在状态切换瞬间把两边的绿灯同时点亮这在真实交通场景里就是事故在课设答辩里就是老师追问你的第一个漏洞。除了正常时序题目通常还要求倒计时显示。每个方向剩余多少秒要能在数码管上直观看到这意味着系统必须维护两个独立的倒计时变量东西方向的剩余时间和南北方向的剩余时间。再加上常见的扩展功能比如紧急模式按键、夜间黄灯闪烁这个看起来很简单的交通灯系统就具备了一个完整状态机的复杂度。1.2 为什么是8086而不是单片机这是很多刚接触这个题目的同学最想不通的一点。如果让我用51单片机或者STM32写交通灯几个GPIO翻转配合软件延时就能搞定代码量小、调试也快。但课程设计指定8086目的本来就不是让你做产品而是通过这个项目验证你有没有真正理解CPU总线、接口芯片、地址译码和时序配合。8086本身并没有内置可编程定时器也没有并行IO口可以直接挂外设。它要跟外设通信必须经过地址锁存、数据缓冲、译码选通这一整套流程。你要自己搭74LS373锁存地址用74LS245做总线驱动用74LS138把I/O空间分配给8255和8253再编写汇编程序初始化这些芯片并完成功能逻辑。这个过程恰恰就是把微机原理从理论变成实践的过程。所以做这个题目的时候不要只盯着红绿灯能不能转起来而要把重点放在CPU是怎么通过地址总线找到外设的、8255的三种工作方式为什么选方式0、8253的计数初值如何计算。这些才是老师真正要考察的东西。1.3 设计要覆盖哪些功能点以我当时的方案为例功能清单可以列成下面这些正常红绿灯循环东西绿30秒、东西黄3秒、南北绿30秒、南北黄3秒倒计时显示两组两位数码管分别显示东西方向和南北方向的剩余秒数紧急模式按下按键后所有方向立即强制红灯持续5秒后恢复正常循环夜间模式拨码开关开启后两方向黄灯以1秒周期交替闪烁蜂鸣器提示状态切换时短鸣一声可选扩展8253计数器2可以实现。这套功能做完不管是课程验收还是笔试答辩基本都能覆盖到。下面我会按照硬件搭建、定时方案、软件框架、调试经验四块展开把每一步的具体做法和原理讲清楚。2. 硬件平台的搭法最小系统、总线缓冲与器件映射2.1 8086最小模式下的总线连接8086有最大模式和最小模式两种工作方式交通灯系统没有复杂多处理器需求用最小模式就够了。把MN/MX引脚接高电平即可。在这个模式下8086自己产生ALE、DEN、DT/R、M/IO这些总线控制信号省去了一大堆总线控制器逻辑。8086的地址/数据总线是分时复用的。AD0到AD15这16根引脚先输出地址然后切换为数据。如果直接把8255、8253的数据引脚接到AD0到AD15上芯片根本分不清哪一刻是地址、哪一刻是数据。所以第一步必须用74LS373做地址锁存。8086在总线周期的T1状态会输出ALE高电平脉冲把这个信号接到74LS373的LE脚ALE下降沿把AD0到AD15当时的地址状态锁存到373的输出端得到稳定的A0到A15地址总线。数据总线方面可以用74LS245做收发缓冲。DIR接8086的DT/R数据传输方向由CPU读写控制G端接DEN注意8086的DEN是低电平有效通常要经过一个反相器再接74LS245的使能端。74LS245的作用一方面是提高总线驱动能力另一方面把8086的复用数据总线跟外设数据总线隔离开避免外设接口直接挂在CPU脚上造成负载过重。这里有一个比较容易忽略的细节BHE信号也需要单独锁存它跟A0配合决定当前访问的是高字节还是低字节。2.2 地址译码74LS138如何分配端口8086访问外设的时候地址总线上的信息必须经过译码产生对应芯片的片选信号。我在这套方案里用了一片74LS138三线八线译码器来分配I/O空间。具体的接法是这样74LS138的G1接5VG2A接8086地址总线锁存后的A0G2B接8086的M/IO信号。输入选择端C、B、A分别接A7、A6、A5。这样只有当M/IO为低电平当前是I/O访问且A0为低电平访问偶地址时译码器才可能输出有效片选信号。Y0分配给8255A使用选择条件为A7A6A5000Y1分配给8253使用选择条件为A7A6A5001。这里要特别解释两个容易出问题的地方。第一为什么G2A要接A0因为8086是16位数据总线外设如果只接在低8位数据线D0到D7上就必须使用偶地址访问也就是A0必须为0。奇地址访问时数据走的是D8到D15低8位外设根本没接这部分总线读写都会失败。所以让A0参与译码从硬件上保证所有片选都落在偶地址空间。第二8255和8253的片内寄存器选择脚A0、A1不能直接接系统地址总线的A0、A1。因为数据总线接的是低8位偶地址步进是2系统A1变化对应的是外设A0变化系统A2变化对应外设A1变化。正确的接法是系统A1接8255的A0脚系统A2接8255的A1脚。8253同样处理。很多Proteus示例图为了画起来方便直接把8255的A0、A1接AD0、AD1仿真可能能过但真实硬件上会有问题。如果你希望这个设计不只是仿真跑通建议从一开始就按正确接法来。2.3 外设端口地址表按照上面这套连接方式我整理了一份端口地址映射表后续写汇编代码时所有IN、OUT指令的端口号都以此为准。芯片寄存器/端口端口地址说明8255AA口00H输出信号灯控制信号8255AB口02H输出数码管段码8255AC口04H高4位输出位选低4位输入按键/秒脉冲8255A控制字寄存器06H初始化方式字8253计数器020H1MHz时钟输入输出1kHz8253计数器122H1kHz时钟输入输出1Hz秒脉冲8253计数器224H可接蜂鸣器8253控制字寄存器26H写入三个计数器的工作方式信号灯和8255A口的对应关系我定义成下面这样后续状态表也按这个编码来写A口位控制对象PA0东西红灯PA1东西黄灯PA2东西绿灯PA3南北红灯PA4南北黄灯PA5南北绿灯这套灯的接法很直白输出高电平点亮对应的LED。真实板子上如果要驱动较大的信号灯不能拿8255直接驱动需要经过三极管或者继电器并且继电器线圈要并联续流二极管否则关断瞬间的反向电动势很容易把驱动管打坏。这个属于实际制作时一定要加的保护措施。3. 定时部分8253级联产生秒脉冲3.1 初值计算与工作方式选择8086没有内部定时器交通灯又需要精确的秒级时基这时候就要用到可编程定时器8253。8253内部有三个独立的16位计数器每个都可以单独编程工作方式。这里我在想一个更简洁的方案直接给计数器0接一个1MHz时钟让它分频出1kHz再用计数器1继续分频出1Hz。两个计数器级联的本质是分频器串联。8253的计数初值等于输入时钟频率除以期望输出频率。计数器0输入1MHz想要输出1kHz初值就是1MHz除以1kHz等于1000计数器1输入是计数器0的输出1kHz想要输出1Hz初值同样是1000。这个计算过程并不复杂但非常容易出错的点是16位写入顺序和进制选择。1000换算成十六进制是03E8H低字节E8H高字节03H。编程时控制字必须设置成先写低字节、再写高字节然后连续两次OUT写入相应的计数器端口。如果把高低字节顺序写反或者漏写了控制字里的二进制计数/BCD计数选择位计数器输出的频率就会跟预期差距极大。计数器0我选择方式3也就是方波发生器输出占空比接近50%的1kHz方波。计数器1选择方式2速率发生器输出的是周期为1秒、低电平持续一个输入CLK周期也就是1毫秒的负脉冲。为什么计数器1不用方式3因为方式3输出的是方波高电平和低电平各占0.5秒如果程序在低电平期间没有及时查询就会丢失一次秒中断。方式2输出的负脉冲只有1毫秒主循环只要跑得够快就能稳定读到这个下降沿。查询式定时最怕的就是脉冲太宽导致重复计数负脉冲方案可以最大程度避免这个问题。8253初始化代码段如下端口地址使用上表定义; 初始化8253计数器0: 方式3, 输出1kHz MOV AL, 00110110B ; 计数器0, 先低后高, 方式3, 二进制计数 OUT 26H, AL MOV AL, 0E8H ; 初值低字节, 1000的低字节 OUT 20H, AL MOV AL, 03H ; 初值高字节 OUT 20H, AL ; 初始化8253计数器1: 方式2, 输出1Hz负脉冲 MOV AL, 01110100B ; 计数器1, 先低后高, 方式2, 二进制计数 OUT 26H, AL MOV AL, 0E8H OUT 22H, AL MOV AL, 03H OUT 22H, AL初始化完成后计数器1的OUT1引脚每秒钟输出一个低电平脉冲这个信号接到8255A的PC3引脚CPU在主循环里通过查询C口电平变化来产生秒标志。3.2 查询式秒标志的实现秒标志是整个软件系统的心跳。主循环每轮扫描一遍8255的C口发现PC3出现下降沿就在内存变量SEC_FLAG里写入1主循环检测到这个标志后完成倒计时更新和状态切换然后把标志清零。边沿检测的代码逻辑很关键。不能简单判断PC3为低电平就认为是新的秒脉冲因为8253方式2低电平持续1毫秒主循环扫描期间可能多次读到同一个低电平如果每次都当作一次秒中断倒计时会飞快地乱跳。正确的做法是保存上一轮PC3的状态只有在上一次为高电平、本次为低电平时才确认为一次有效下降沿。查询秒脉冲的核心代码CHECK_SEC: MOV DX, 04H ; 读8255 C口 IN AL, DX AND AL, 08H ; 只保留PC3 CMP AL, PREV_SEC ; 与上次状态比较 JE SEC_NO_CHANGE ; 状态发生变化 CMP AL, 0 ; 当前为低电平 JNZ SEC_LOW_TO_HIGH MOV SEC_FLAG, 1 ; 下降沿秒标志置位 SEC_LOW_TO_HIGH: MOV PREV_SEC, AL ; 保存最新状态 SEC_NO_CHANGE: RET这个方法在实际调试中非常稳。主循环的执行时间通常只有几十微秒远小于1毫秒脉冲宽度完全来得及捕获。如果你发现秒标志偶尔丢优先检查8253的初始化是否成功用一个示波器或者Proteus里的虚拟仪器先看OUT1引脚有没有波形再查软件边沿检测逻辑。3.3 关于中断方案的取舍这个题目如果做成真正的中断方式需要在8086和8253之间再加一片8259A中断控制器把OUT1接到8259A的IR0初始化中断向量表编写中断服务程序。这样CPU平时可以忙别的事情每秒由硬件打断一次。从技术实现上看中断方式显然更高级。但我个人建议如果你只是课程设计查询方式已经足够不建议一上来就上8259A。原因有两个。第一8086的中断响应过程要读取中断类型码系统里必须配备8259A并正确初始化ICW1到ICW4任何一个初始化字写错整个中断系统就收不到响应排错成本很高。第二交通灯系统本身没有大量后台任务需要CPU去处理主循环空转的时间非常多查询并不会浪费多少资源。把精力花在状态机逻辑和显示刷新上性价比更高。如果你想把中断方式做成项目亮点那可以在基础功能全部跑通之后再加。8259A的初始化流程网上资料很多做成之后确实能在答辩时加分但不要在系统还没稳定之前就急着加否则调试难度会成倍上升。4. 软件骨架状态机、倒计时与按键处理4.1 状态转移表交通灯软件部分的核心是一个四状态有限状态机。我习惯先把状态转移表画出来再对照着写汇编代码这样逻辑不容易乱。当前状态东西信号灯南北信号灯持续时间转入状态S0绿灯红灯30秒S1S1黄灯红灯3秒S2S2红灯绿灯30秒S3S3红灯黄灯3秒S0在内存里维护两个变量一个存当前状态STATE一个存当前状态的剩余秒数TIME_LEFT。到1秒边界时先判断TIME_LEFT是否大于0大于0就递减等于0就切换到下一个状态同时把下一个状态的持续时间装载到TIME_LEFT里。这种做法的好处是以后想调整绿灯时间只需要改状态转移表里的数字主循环代码一行都不用动。我甚至建议把每个状态的持续时间定义成常量方便扩展。4.2 8255初始化与端口定义8255A要工作在方式0A口和B口全部输出C口高4位输出、低4位输入。C口高4位用于数码管位选低4位用来接按键和秒脉冲输入。控制字格式是D71表示方式字有效D6D500表示A口方式0D41表示A口输出D31表示C口高4位输出D20表示B口方式0D11表示B口输出D01表示C口低4位输入。合在一起就是10001011B也就是8BH。初始化代码MOV DX, 06H MOV AL, 10001011B OUT DX, AL这里有一个容易踩的坑。C口高4位输出和低4位输入是同时配置的如果你把C口整个配置成输出那么PC3上的秒脉冲信号就永远读不进来系统倒计时完全不动。反过来如果整个配成输入数码管的位选信号又送不出去。所以8255的控制字一定要对照引脚功能仔细算一遍不要凭感觉写。4.3 主循环与状态更新主循环的整体流程是检测紧急按键检测秒标志如果到了1秒就更新状态机然后刷新数码管显示不断循环。我把核心流程用伪代码方式写出来MAIN_LOOP: CALL CHECK_EMERGENCY ; 检查紧急模式按键 CALL CHECK_NIGHT ; 检查夜间模式拨码 CALL CHECK_SEC ; 检查秒标志 CMP SEC_FLAG, 1 JNE MAIN_LOOP MOV SEC_FLAG, 0 CALL UPDATE_STATE ; 更新状态机 CALL DISPLAY ; 刷新倒计时显示 JMP MAIN_LOOPUPDATE_STATE过程里做的事情就是前面说的状态转移。这里需要把每个状态对应的灯组输出值整理成一张表STATE_TAB: DB 0CH ; S0: 东西绿灯PA21, 南北红灯PA31 DB 02H ; S1: 东西黄灯PA11, 南北红灯PA31 DB 28H ; S2: 东西红灯PA01, 南北绿灯PA51 DB 19H ; S3: 东西红灯PA01, 南北黄灯PA41注意这些值是根据我的PA定义算出来的。S0状态下东西绿灯对应PA2置1后的值是04H南北红灯对应PA3置1后的值是08H加起来就是0CH。每次状态切换时用STATE作为索引从表里取一个字节送到8255A的A口灯组就会立刻更新到对应状态。倒计时部分要维护两个变量EW_TIME表示东西方向剩余秒数NS_TIME表示南北方向剩余秒数。状态S0和S1时东西方向还有时间在走S2和S3时南北方向在走。更新状态机的时候要把时间变量跟当前状态对应起来不能搞混。UPDATE_STATE PROC CMP TIME_LEFT, 0 JE NEXT_STATE DEC TIME_LEFT ; 剩余时间减1 ; 根据当前状态同步更新EW_TIME或NS_TIME CMP STATE, 0 JE UPDATE_EW CMP STATE, 1 JE UPDATE_EW DEC NS_TIME JMP UPDATE_DONE UPDATE_EW: DEC EW_TIME JMP UPDATE_DONE NEXT_STATE: ; 切换状态并装载新的持续时间 ... UPDATE_DONE: RET UPDATE_STATE ENDP这里特别提醒EW_TIME和NS_TIME是给数码管显示用的绝对剩余秒数TIME_LEFT是当前状态倒计时的剩余秒数。两个概念要区分清楚很多同学的倒计时显示对不上就是因为只维护了TIME_LEFT没有单独维护每个方向的实际剩余时间。4.4 紧急模式与夜间闪烁的扩展紧急模式我用PC0接一个按键按下接地平时通过上拉电阻保持高电平。检测流程是读到PC0为低电平先延时10毫秒消抖再读一次还是低电平确认按键有效。然后强制把A口输出为全红状态也就是东西红灯和南北红灯同时点亮AL09H持续5秒后恢复S0状态重新开始正常循环。夜间模式用PC1接拨码开关开关闭合后进入夜间黄灯闪烁。在这个模式下正常状态机被跳过黄灯以秒标志为周期交替亮灭。奇数秒亮黄灯偶数秒灭黄灯逻辑很小但扩展出来之后功能列表会好看不少答辩时也能体现你考虑了真实交通场景。按键消抖的延时子程序我就不贴完整代码了基本就是多层嵌套循环凑出10毫秒。注意8086的时钟频率不同循环次数要重新估算不要直接抄网上的数字。我当时在Proteus里仿真频率设的是5MHz算出来的循环次数跟实际板子如果用8MHz晶振会有偏差。这个点可以在文档里写清楚老师问起来也能答得有理有据。5. 调试阶段的高频翻车点5.1 地址错位与片选混乱这个坑绝大多数人都会踩一次包括我自己。现象是程序一跑8255完全没反应或者向A口写数据结果灯的亮灭反应跟预期完全对不上。排查的时候先量74LS373锁存后的地址线确认A0到A7有没有正确输出再看74LS138的G2B有没有接到M/IO如果没接访问内存和访问I/O时的片选会混在一起最后看G2A有没有接A0如果不接A08255的端口地址区间会出现大量镜像地址看起来端口号怎么改都能访问但实际数据可能跑到高字节总线上去了。我当时的排查方法是在汇编里写一个简单的回环测试向8255的A口写55H然后用万用表量A口引脚电平。如果8根脚输出高低交错说明8255基本工作正常如果全部没反应先查片选信号有没有到来如果引脚电平跟写入数据对不上再查数据总线连接。这个方法可以把问题快速定位到CPU到8255还是8255之后。5.2 段码和位选极性搞反数码管显示是另一个重灾区。共阴极和共阳极的段码完全相反同一个数字0在共阴数码管和共阳数码管上的编码就差一个非的关系。如果你用的是Proteus里默认的数码管一定要先确认它是共阴还是共阳再写段码表。我的方案里用的是共阴极数码管段码表如下SEG_TAB: DB 3FH ; 0 DB 06H ; 1 DB 5BH ; 2 DB 4FH ; 3 DB 66H ; 4 DB 6DH ; 5 DB 7DH ; 6 DB 07H ; 7 DB 7FH ; 8 DB 6FH ; 9位选信号我接在C口高4位低电平选通。如果你直接拿C口高4位输出高电平驱动PNP三极管的基极要搞清楚三极管是低电平导通还是高电平导通。Proteus仿真里很多时候不接三极管直接连数码管位选引脚也能亮但真实板子上必须加驱动位选极性错了整个显示就灭了。调试数码管的时候我建议先做一个固定显示测试初始化完之后直接让某一位显示数字8其它位全灭。如果这一位能稳定显示8说明段码、位选、扫描方向全部正确再去做动态扫描。不要一上来就写完整显示函数出问题根本不知道是扫描太快还是段码错。5.3 按键抖动与秒脉冲丢失按键抖动是硬件电路里非常典型的问题。机械按键按下和松开的瞬间电平会在几十毫秒内反复跳变如果不做消抖一次按键会被识别成很多次紧急模式可能会被反复触发。软件消抖的原理很简单第一次读到低电平后不立刻处理先延时10毫秒再读一次如果还是低电平才认为是有效按下。这个10毫秒的延时不能省。秒脉冲丢失的问题主要出现在边沿检测逻辑里。我见过一些同学直接判断PC3为低就置秒标志结果倒计时每秒钟跳好几下。这就是因为没有做边沿检测同一个低电平脉冲被主循环重复读取了。正确做法参照前面CHECK_SEC过程必须保存上一次的电平状态只有检测到高到低的跳变才生成一次秒标志。5.4 仿真与真实板子的差异Proteus仿真能帮你验证逻辑但它对很多硬件细节是理想化的。仿真里不接上拉电阻按键照样能工作真实板子上如果不接上拉按键脚悬空电平就是不确定的程序会随机读到高或者低。仿真里不接续流二极管继电器照样能切换真实板子上不加续流驱动管很可能会被烧掉。所以我的建议是仿真通过只是第一步画PCB或者搭面包板之前把所有上拉电阻、限流电阻、驱动三极管、续流二极管都按规范补上。8086系统的时钟电路和复位电路也必须在真实板子上仔细检查仿真模型里这些经常被默认初始化好但真实芯片不会自己产生可靠的复位信号。5.5 一份可以直接抄作业的调试顺序如果你现在还在一个一个模块调试我建议按照下面这个顺序走可以少走很多弯路先跑一个最简单的汇编程序向8255A口循环输出55H和AAH确认CPU、锁存器、译码器、8255这条链路完全畅通用示波器或者Proteus虚拟仪器看8253的OUT0有没有1kHz方波OUT1有没有1Hz脉冲确认定时链路正常把OUT1接入PC3运行秒标志测试程序看SEC_FLAG是否每秒钟置位一次只写一个状态的灯组输出确认对应方向的红黄绿灯点亮正确加上倒计时显示确认数码管扫描正常最后把状态机、按键、紧急模式全部整合进去。按这个顺序每一步的结论都是确定的出了问题能很快定位到具体模块。反过来如果一上来就把整个系统写完再调试一个现象背后可能有四五个嫌疑点排查效率会非常低。个人经验交通灯这个系统最有价值的部分其实不是灯亮得有多好看而是你通过它把CPU和外设之间到底怎么通信这件事彻底想通了。后面做8259A中断、做串口通信、做A/D采集底层思路都是同一套。如果你做完之后还有时间强烈建议试试把查询式秒标志改成8259A中断驱动再把紧急模式改成真正的外部中断输入这个升级做完你对8086系统的理解会上一个台阶。等答辩的时候老师随口问一句你这里还有哪些可以改进的地方你就能很自然地讲出已经做过的扩展和还没做的设想这个印象分会非常加分。本文还有配套的精品资源点击获取