微机原理课程设计:基于8086与8255的洗衣机控制系统

发布时间:2026/9/19 7:04:31
微机原理课程设计:基于8086与8255的洗衣机控制系统 简介一份围绕“简易全自动洗衣机”的微机原理课程设计完整文档核心面向电气工程及其自动化、计算机等专业学生可解决课程设计中系统方案不完整、芯片选型与接口设计缺乏参考的问题。资源包为单个Word文档约184KB包含课程设计内容、系统功能要求、系统方案论证、硬件设计、软件设计、总结与附录等完整章节结构清晰。目前已有442人学习下载。文档以8051微控制器为核心结合8255A、8254、8259A、0809等常用芯片具体展示了数码管显示、倒计时、按键中断、水位传感、电机正反转控制及洗涤、甩干连续自动等模块的设计思路与实现方法并附有遇到的问题及解决措施、方案对比与程序流程能够直接作为设计报告的基础模板和功能调试的参考手册。1. 一台洗衣机在微机原理课里到底在练什么把洗衣机搬进微机原理课程设计不是为了研究家电而是把“CPU 怎么通过接口控制外部设备”这条主线压缩成 30 行状态转移表。一个全自动洗衣机的工作过程本质上是进水、洗涤、排水、脱水这几个机械动作按时间片的轮转而每个动作背后对应的是电机正转、反转、停止和阀门开关——这些恰恰是微处理器通过并行接口输出高低电平就能直接驱动的逻辑。做这个题目能覆盖的知识点很直接8255 并行接口的初始化与读写时序、地址译码、按键防抖、数码管动态扫描以及一个有限状态机如何在汇编或 C 语言里落地。对 5 年以上的人而言这个题目的价值在于重新审视“状态机外设时序”这种朴素组合在嵌入式里的边界而不是把注意力放在洗衣逻辑本身——那只是换了一组输出引脚。2. 系统方案与状态机设计先定状态再定电路2.1 选型理由为什么课程设计大多用 80868255微机原理与接口技术课程里洗衣机控制最常见的教学方案基于 8086 最小系统和 8255 并行接口原因和性能无关纯粹是教学目的——8086 的地址线和数据线需要锁存器、译码器才能接外设这个过程本身就是“接口技术”的核心训练。相比之下用 8051 单片机做同样的事会省掉大量译码逻辑直接查寄存器就能操作端口课堂上的“地址总线—设备—I/O 空间”这条线索就弱了。所以这道题的标准骨架是8086 CPU 工作在最小模式配合 74LS373 锁存地址、74LS138 译码产生片选信号8255 作为唯一的并行接口芯片挂在系统总线上。8255 的 A 口设为输出控制电机继电器和进水阀B 口设为输入读取按键C 口可拆成高低两半接数码管位选或状态指示灯。这个设计下洗衣机模型不再是一个完整的电路系统而是一个“CPU 看得见、摸得着”的外设集合软件不直接操作硬件引脚而是通过写 8255 端口寄存器来改变电机行为这正是微机原理要传达的分层思想。如果课程允许选择平台也可以直接用 8051 的 P1/P2 口做同等功能但要注意那属于单片机应用设计不是接口技术设计。两者的区别不在于最终效果而在于“CPU 如何找到外设”这一环是否被刻意放大。做课程报告时指导老师看重的也是这一环的完整度。2.2 洗衣机工作状态与状态转移表一个简易全自动洗衣机的机械动作可以压缩为六个状态待机、进水、洗涤、排水、脱水、结束。每个状态的进入条件和退出条件都是”时间到了“或者”按键被按下“不需要传感器参与——这非常关键因为课程设计通常没有水位开关和温度传感器所有判断都用定时器延时代替。给状态编号最直接的方式是 S0 到 S5状态含义输入条件输出动作下一状态S0待机/停止按下启动键所有设备关闭S1S1进水启动后自动进入进水阀打开电机停延时 60s 后到 S2S2洗涤进水完成电机正转 4s→停 1s→反转 4s→停 1s循环 5 次后到 S3S3排水洗涤结束排水阀打开电机停延时 30s 后到 S4S4脱水排水完成电机单向高速运转延时 20s 后到 S5S5结束脱水完成蜂鸣器响 3s回到 S0S0这个表的作用在课程设计里远超流程图。它把“洗衣机”的领域逻辑和“微机”的底层控制彻底分离状态转移表描述行为下面所有代码都围绕这张表展开输出动作那一列直接对应 8255 PA 口某一位的高低电平。扩展功能时也只需要往表里加行比如增加“漂洗”状态就是从 S3 回到 S1 而不是去 S4表的修改成本远远小于改代码。实际写报告时建议再补一列“位定义”例如 PA0 控制进水阀继电器、PA1 控制排水阀继电器、PA2 控制电机正转、PA3 控制电机反转这一列是后续驱动程序和电路图之间的桥梁。没有这一列代码和硬件图对不上是课程设计里最常见也最致命的错误。2.3 把机械流程拆成软件流程洗涤节奏如何划分洗涤环节的节奏拆分是软件设计的核心难点也是这道题最容易做“假”的地方。很多人直接把正转、反转写成两条死循环中间靠延时函数切换看起来实现了功能但按钮没有响应——因为 CPU 被循环占死了根本轮不到扫描按键。正确做法是把“一次洗涤”拆成不可再分的最小动作单元正转 4 秒、停 1 秒、反转 4 秒、停 1 秒。这四个动作组成一个洗涤周期每个周期完成后检查一个计数器满 5 个周期才进入排水。整个软件结构因此变成两层外层是状态机决定现在是进水、洗涤还是排水内层是策略表决定洗涤状态下当前该做什么动作。这种拆分方式让程序每个循环的耗时可预测即使按键扫描被插入在任意两个动作之间也不会破坏洗衣节奏。更精细的做法是引入“剩余时间递减”的思路用软定时器比如 10ms 中断或简单的轮询计数作为全局心跳每个状态只关心“心跳计数到没到阈值”而不是自己在延时循环里发呆。这样启动键和暂停键在任何时刻都能被响应因为主循环每圈都经过按键扫描只不过状态机只有在特定状态才接受特定按键。后面第 4 章会给出这种结构的代码骨架。3. 接口电路与驱动设计8255 怎么接电机和按键3.1 地址译码与 8255 端口基地址计算8086 访问外设采用独立 I/O 空间8255 内部有 A 口、B 口、C 口和控制字寄存器四个端口地址通过 A1、A0 选择。常见做法是把 8255 的 A1、A0 接到系统地址总线 A1、A0 上再用译码器产生片选信号。以 74LS138 为例若将 G1 接高电平、G2A 和 G2B 接译码使能且输入选择端 C、B、A 分别接入 A5、A4、A3当 A7 到 A3 的组合为 00000 时Y0 有效此时 8255 被选中I/O 地址范围落在 00H~07H 区间A2 可悬空或参与译码A1/A0 决定内部端口为避免和系统板卡常用地址冲突一般会把基地址拨到 280H~28FH 这类高位地址段即让 A9 参与译码、且高位地址线取特定组合。端口地址的计算要同步写入手册表格基地址 0 是 A 口1 是 B 口2 是 C 口3 是控制字寄存器。日常调试时最容易犯的错是基地址算对了但控制字写错导致输出口一直被当成输入口用电平怎么都拉不低。建议写完译码器接线后先用万用表量 8255 的 CS 引脚在程序写端口时看它有没有被拉低一次这比反复改地址快得多。3.2 PA 口输出驱动继电器与互锁保护8255 PA 口的驱动能力只有毫安级不能直接驱动继电器线圈课程设计中常见做法是加一级 NPN 三极管如 9013或直接使用 ULN2003 达林顿管阵列。ULN2003 的好处是内部自带续流二极管接感性负载继电器时不需要额外再并一个二极管这在学生焊接的板子上能减少一个常见故障点。每个继电器线圈串接在 12V 和 ULN2003 输出端之间输入端接 PA0~PA3同时在三极管基极串 1kΩ 限流电阻。比驱动电流更关键的问题是互锁。电机正转和反转如果同时接通继电器触点可能短路电源轻则跳闸重则烧板。软件层面当然可以通过状态机保证同一时刻只写一个位为 1但硬件上必须再加一道保险正转继电器和反转继电器采用双刀双掷触点把对方的常闭触点串进自己的供电回路。这样即使软件发生异常两个继电器在物理上也不可能同时吸合。这个设计在课程答辩时是一个明显的加分点因为很多学生只做了软件互锁回答不了“如果程序跑飞了怎么办”这个问题。如果只是演示模型用 LED 模拟电机转向也能过但驱动的电平逻辑和继电器方案完全一致——电机正转时 A 口某位为 1反转时另一位为 1全 0 时停止。切换电机转向时需要先输出全 0再延时 100ms 以上再输出反方向电平给继电器触点充分的释放时间否则会出现瞬间的机械卡顿或火花。这个 100ms 间隔应该在代码注释里写清楚。3.3 PB 口按键输入的服务流程B 口作为输入口接 3×2 矩阵键盘或直接接独立按键简易设计用独立按键即可每个按键一端接 PB0~PB1另一端接地PB 口内部或外部加上拉电阻。很多 8255 学习板上 PB 口已经有上拉可以直接接按键到地。但程序初始化时要把 B 口方式设为输入控制字写错的话按下按键读到的永远是 0xFF 或 0x00这种问题通常不是硬件故障而是配置字错误。独立按键的软件处理流程很简单主循环里周期读取 PB 口检测到某位为 0先延时 20ms 再读一次确认还是 0 才算有效按键然后转入相应状态处理函数等待按键释放。在状态机设计里启动键在 S0 态被按下进入 S1暂停键在 S2、S3、S4 态被按下则冻结当前剩余时间计数。暂停功能的实现不建议用中断直接在主循环里判断一个暂停标志位即可因为洗衣机的实时性要求极低毫秒级响应完全够用。3.4 数码管动态扫描显示显示部分至少要给出当前状态或剩余时间常见的是一组两位或三位共阴数码管通过 8255 C 口输出段码、经 74LS07 或 ULN2003 驱动位选。动态扫描的原理在这里值得展开人眼视觉暂留约 20ms所以轮流点亮每一位数码管刷新周期控制在 4~8ms 内看起来就是所有位同时亮的。比如三个数码管主循环里每轮只点亮第 1 位 2ms然后关断切到第 2 位 2ms再切到第 3 位循环往复。切换位选之前必须先把段码清空否则会看到拖影这是动态扫描最典型的显示瑕疵。在代码实现上显示刷新最好挂在主循环里而不是延时函数内部否则一旦进入长延时子程序数码管会停住不动影响使用体验。后面第 4 章的代码把显示刷新放在了主循环最高层任何状态处理函数返回后都会立即执行一次显示刷新这样即使洗涤循环里做了几秒的延时显示也不会长时间静止。状态机里的“剩余时间递减”是在延时子程序返回后才更新所以显示数值是跳跃式的但在课程演示中完全可接受。4. 软件实现状态机、防抖与延时的具体写法4.1 主程序框架与 8255 初始化主程序的骨架是一个死循环加一个状态分发器。初始化部分只做两件事向 8255 控制字寄存器写入工作方式控制字把端口方向配好然后给全局变量赋初值。8255 的方式控制字格式是 D71 表示方式字有效D6D5 为 A 组方式00 为方式 0D4 为 A 口方向1 为输入D3 为 C 口高半方向D2 为 B 组方式D1 为 B 口方向D0 为 C 口低半方向。若 A 口输出、B 口输入、C 口输出则控制字为 10000010B即 82H。#define PA 0x0280 /* 8255 A 口地址 */ #define PB 0x0281 /* 8255 B 口地址 */ #define PC 0x0282 /* 8255 C 口地址 */ #define CTRL 0x0283 /* 8255 控制字地址 */ void sys_init(void) { outp(CTRL, 0x82); /* A口输出B口输入C口输出 */ outp(PA, 0x00); /* 初始状态电机停阀门关 */ outp(PC, 0x00); /* 数码管全部熄灭 */ wash_round 0; /* 洗涤周期计数清零 */ state S0; /* 进入待机状态 */ } void main(void) { sys_init(); while (1) { key_scan(); /* 按键扫描带防抖 */ state_dispatch(); /* 状态机主分发 */ display_refresh(); /* 数码管动态刷新 */ } }这段代码的逻辑很直白三个函数各司其职主循环里按键扫描优先级最高保证任何状态下按键都能被及时读取状态分发根据当前状态执行对应动作显示刷新放到最后确保它不会阻塞前两个函数。outp是 Turbo C 时代直接操作 I/O 端口的函数在真实编译环境中也可以用outportb或者用内嵌汇编替代。三个地址宏定义要和 3.1 节算出的端口地址严格对应任何一处地址写错都会导致读写到错误的寄存器外表看起来就是“控制字发了没反应”。4.2 按键防抖与状态读取机械按键按下和松开时触点会有约 5~20ms 的抖动期间电平快速跳变。不处理抖动的话一次按压会被判定成多次程序可能在 S0 和 S1 之间来回跳。经典方案是“两次读数确认”第一次读到按下后延时 20ms再读一次两次都是按下状态才确认。注意这个延时不能用空循环里的递减计数器直接糊弄否则不同主频下防抖时间会相差很大最好用一个可调用的延时函数具体实现见 4.3。void key_scan(void) { unsigned char key inp(PB) 0x03; /* 只读取 PB0、PB1 */ static unsigned char last_key 0; static unsigned char confirmed 0; if (key ! last_key) { /* 电平发生变化 */ delay_ms(20); /* 跳过抖动区 */ key inp(PB) 0x03; /* 抖动后再读一次 */ if (key ! last_key) { last_key key; confirmed 1; /* 确认是一次有效变化 */ } } if (confirmed) { if ((last_key 0x01) 0) { set_event(EVENT_START); /* 启动键按下 */ } else if ((last_key 0x02) 0) { set_event(EVENT_PAUSE); /* 暂停键按下 */ } confirmed 0; } }这段代码里last_key记录的是上一次确认后的稳定电平任何一次抖动没有超过 20ms都不会被计入状态变化。confirmed标志位确保一次按压只产生一次按键事件而不是在电平稳定前反复触发。初学者常犯的错误是只判断按键电平为低就立即产生事件结果启动一次洗衣机直接跳过整个洗涤流程那基本就是防抖没做或者把事件设置放在了防抖判断之前。4.3 延时子程序的精确控制洗衣机的所有动作切换都依赖延时。延时子程序的写法决定了状态机的时间基准是否可靠。简易课程设计里不需要高精度时钟但至少要保证 1 秒的延时误差在可接受范围内否则“洗涤 5 分钟”可能变成“洗涤 3 分钟”。8086 汇编里的经典双重循环延时内层循环次数决定毫秒级时基外层循环进行秒级累加。下面以 8086 主频 5MHz 为例给出一个粗略的秒级延时计算过程。; 延时子程序 DELAY_SEC ; 入口: AX 延时秒数 DELAY_SEC PROC PUSH CX PUSH BX MOV CX, AX ; 外层秒数计数器 SEC_LOOP: MOV BX, 5000 ; 内层毫秒循环基数, 具体值需按主频标定 MS_LOOP: NOP NOP NOP DEC BX JNZ MS_LOOP LOOP SEC_LOOP POP BX POP CX RET DELAY_SEC ENDP参数说明内层循环中三条 NOP 加上 DEC 和 JNZ大约消耗 533331633 个时钟周期主频 5MHz 下每个周期 0.2µs单次内层循环约 6.6µsBX5000 时内层总耗时约 33ms外层循环 30 次差不多就是 1 秒。这里的 5000 不是算出来的精确值而是标定出来的实际调试时用示波器测某个输出引脚的周期然后微调 BX 初值。使用 C 语言时delay_ms函数可以同样用嵌套循环实现但某编译器在优化等级改变后循环可能被整体优化掉导致延时消失解决方法是把循环变量声明为 volatile 或直接查编译器的内建延迟函数。务必注意延时期间 CPU 被占用所以所有长延时都必须拆成“一小段延时就检查一次暂停标志”的结构。比如洗涤状态下正转 4 秒不要直接调用 delay 4 秒而是循环调用 delay_ms(50) 共 80 次每次循环里检查暂停键。这样暂停才能被执行否则按暂停键要等当前动作结束后才有反应体验上就和死机了差不多。4.4 用分支表驱动状态跳转状态机最稳妥的实现方式是函数指针表或跳转表C 语言里就是一个数组数组下标是状态编号元素是处理该状态的函数指针。这样state_dispatch里不需要写一长串 switch-case只需要一行state_table[state]()新增状态时只扩展数组不修改分发函数。下面是洗涤状态的处理函数展示动作拆分的粒度。void do_wash(void) { static int step 0; /* 当前洗涤步骤 */ static int round 0; /* 第几个洗涤周期 */ static int tick 0; /* 当前步骤剩余毫秒数 */ if (tick 0) { step (step 1) % 4; /* 0正转 1停止 2反转 3停止 */ tick (step % 2 0) ? 4000 : 1000; /* 正反转4s, 停1s */ if (step 0) { round; if (round 5) { /* 完成5个洗涤周期 */ set_state(S3); /* 进入排水 */ return; } } } outp(PA, motor_output[step]); /* 查表: 正转/停止/反转/停止 */ tick - 50; /* 每次分发消耗50ms */ delay_ms(50); /* 等待 */ }motor_output是一个长度为 4 的常量数组{0x04, 0x00, 0x08, 0x00}分别对应 PA2 置位正转、全零、PA3 置位反转、全零。状态函数内部用一个 static 的tick做倒计时每次进入函数就减 50小于等于 0 时切换步骤。这样做的好处是状态函数退出后主循环还能做按键扫描和显示刷新不会因为洗涤耗时 20 秒而丢按键。set_state负责切换状态机并刷新显示缓冲区进入新状态时应当重新初始化该状态内部的 static 变量否则可能出现上次洗涤的剩余步骤残留常见做法是给每个状态函数配一个独立的 reset 函数或直接把这个变量改成全局结构体。这种结构的分发函数非常短代码审查时一眼就能看出系统在哪个状态、每个状态消耗了多长时间。总流程用状态表表达执行用分支表表达整个程序的逻辑从“一堆循环嵌套”变成了“数据驱动”这在课程设计的报告里就是一篇能讲清楚的设计。5. 调试与验证先仿真再上机的实践顺序课程设计最后一周的调试效率取决于验证顺序是否合理。我的经验是先 Proteus 仿真后焊板子再在硬件上做最小功能确认每一步都有明确的测量点不要一上来就把整套系统全跑通——预期目标应该是“在 2 小时内把任一环节的错误定位到一个芯片引脚”。5.1 用示波器确认延时基准程序烧进硬件后第一个要验证的不是洗衣流程而是时间基准。用示波器探头夹在 PA2 引脚把状态机强制设为“点动模式”——按一次键输出 1 秒高电平、再按一次输出低电平——这样可以通过示波器读到的电平宽度直接确认延迟函数的实际值。主频 5MHz 的 8086 系统上写了 4 秒的延时实测 5.2 秒并不罕见因为内存访问插入的等待周期会让 CPU 实际执行速度低于标称。调整 4.3 节的循环基数让示波器读数稳定在 4.0±0.1 秒之后再放回去跑完整状态机。5.2 分支表状态跳转的可观测方法状态机跑起来后看不到内部状态是最大的调试障碍。推荐在代码里做一个调试钩子把状态编号回写到 C 口接 3 个 LED。程序里任何一次set_state调用都在切换后立即更新 C 口输出这样洗衣运行时 LED 的组合直接告诉观察者当前在哪个状态。LED 亮灭顺序和 2.2 节的状态转移表对不上哪一步电压不对一目了然不需猜。5.3 动态扫描拖影和按键误触发的快速修复如果数码管显示有拖影优先调整位选切换时序而不是换驱动芯片。把“清段码”提前到位选切换之前并在打开下一位之前插入 1ms 延时绝大多数拖影问题可以解决。按键误触发则去查防抖分支的时间常数在防抖的 20ms 延时期间把按键电平再采样一次和触发前的电平比较如果中途发生过反弹就不产生事件。这两类问题在课程设计的成品阶段最常出现修复成本都极低但需要在代码里留出清晰的调试钩子——比如把防抖延时和扫描间隔做成带#define的宏改一个数就能测试不同取值的效果。硬件上电前用万用表把 8255 的 PA0~PA3 对地电阻测一遍排除焊锡短路——这一点做得越早后续调试时间越短。本文还有配套的精品资源点击获取