简介:一份面向51单片机学习者与开发者的仿真实例,演示如何通过8255并行接口芯片扩展单片机I/O端口,解决实际应用中引脚不足的问题。实例以C语言编写控制程序,配合Proteus搭建硬件电路并完成联调,适合电子类专业学生进行课程设计或接口技术入门。资源包共14个文件,涵盖Proteus仿真图(DSN)、Keil工程源码(C/UV2)、编译输出(HEX/LST/OBJ)以及备份文件(DBK/PWI)等,压缩后仅51KB,结构清晰、便于直接打开学习。已有199人浏览学习。通过学习该实例,可重点掌握8255的端口工作模式配置、控制字设置、C语言层面的I/O读写操作,以及Proteus中加载代码和观察波形的方法。项目虽小但流程完整,能帮助读者建立从硬件设计到软件编程再到仿真验证的完整思路,为后续更复杂的接口扩展应用打下基础。
1. 用8255做接口扩展到底在解决什么问题
用51单片机做课程设计或小项目时,最让人头疼的不是C语言语法,而是IO口不够用。一个典型场景:P0接LED,P1接键盘,P2接数码管段码,P3还要接位选和外部中断,程序写完了,引脚却愣是插不下。这时候最常见的扩展方案是给51加一片8255,把P0数据总线引到8255的D0-D7,再用A0、A1和RD/WR做端口选择,一下子多出24个IO。8255带给你的不只是“多几个引脚”,它把单片机的总线读写引入接口扩展:对8255写数据,本质就是对外部RAM做写操作。这篇文章顺着“寄存器原理 → C语言驱动 → Proteus仿真 → 键盘扫描实战 → 时序调试”的顺序,把这套方案讲透,新手照着能跑,老手也能看到地址映射和时序上的坑。
2. 8255端口结构与C语言寄存器编程基础
2.1 从引脚到控制字:把三个端口拆成寄存器视图
8255内部有三个8位端口:PA、PB、PC,PC口又分高四位和低四位,可以分别设置方向。要让8255工作,不是直接往引脚塞数据,而是先通过控制字寄存器告诉它“哪个端口输入,哪个端口输出”。这个控制字本身也占一个端口地址,由A1、A0两根地址线的状态决定。
| A1 | A0 | 选中寄存器 | 用途 |
|---|---|---|---|
| 0 | 0 | PA | 8位数据输入/输出 |
| 0 | 1 | PB | 8位数据输入/输出 |
| 1 | 0 | PC | 8位数据输入/输出,或拆成高低4位 |
| 1 | 1 | CWR | 控制字寄存器,只写不读 |
A0、A1来自51单片机低8位地址总线。CS片选信号负责决定8255在单片机地址空间里的位置,只有CS为低电平时,A0、A1和RD/WR才真正生效。也就是说,8255对单片机来说就是四个连续排列的存储单元,只是最后一个单元不是RAM,而是控制字寄存器。
控制字的位定义是:D7必须为1,表示“方式定义有效”;D6、D5决定A组工作方式;D4决定PA方向;D3决定PC高四位方向;D2决定B组工作方式;D1决定PB方向;D0决定PC低四位方向。比如将控制字设为0x80,二进制是10000000,含义就是:PA输出,PB输出,PC高四位输出,PC低四位输出,全部工作在方式0。这也是最简单、最常用的初始化方式。
2.2 用C语言写一组8255底层驱动
在Keil C51里访问外部扩展RAM一般用XBYTE宏,它把地址和数据类型封装成“直接可读写的外部存储单元”。因为8255占用的是xdata空间,所以写成这样:
#include <reg51.h> #include <absacc.h> #define PA XBYTE[0x0000] // A1=0, A0=0 #define PB XBYTE[0x0001] // A1=0, A0=1 #define PC XBYTE[0x0002] // A1=1, A0=0 #define CWR XBYTE[0x0003] // A1=1, A0=1 void main(void) { CWR = 0x80; // 方式0,PA/PB/PC全部输出 while (1) { PA = 0x00; // PA口全部输出低电平 PA = 0xFF; // 然后全部拉高,观察电平变化 } }这段代码做的事情是:先把控制字0x80送入CWR,让8255三个端口都处于输出状态,然后在主循环里交替往PA口写0x00和0xFF。逻辑说明:第一个赋值语句不是“给一个变量赋值”,而是向外部地址0x0003发起一次写操作,8255在内部把D0-D7上的0x80锁存到控制寄存器,从而改变所有端口的方向。之后对PA的写操作同样会驱动P0数据总线,配合WR信号把数据送到8255的PA输出锁存器。
参数说明:地址偏移0x0000、0x0001、0x0002、0x0003不是随手写的。它们依赖A1、A0和CS的连接方式,如果CS接到P2.7,那么P2.7=0时8255才被选中,地址高位小于0x8000都会命中,所以低两位0x0000-0x0003就够用。如果你的板子上CS经过译码器接到P2.6,基地址就要换成0x4000-0x4003。这个映射关系必须在写代码前先画出来,否则编译通过但硬件上不工作。
2.3 常见误写:把8255当普通变量直接赋值
不少人在C语言里写完#define PA XBYTE[0x0000]之后,直接写PA = 1;,以为像P1=1一样只让PA最低位输出高电平。实际上PA是一个8位端口,PA = 1会把整个端口写成0x01,也就是PA0输出高,其余七位输出低。所以想让某一位单独动作,要么写完整字节,要么先读回原值再按位操作。8255在输出方式下不具有读回功能,直接PA |= 0x01这种“读改写”指令在C51里会产生一个对外部地址的读周期,而8255输出端口不返回数据,读回来的值是不可靠的。解决方法是把每次要输出的整个字节用一个软件备份变量维护,需要修改某一位时先改备份变量,再把整个变量赋值给PA。
另一个典型误用是忽略PC口高低四位的方向独立性。控制字0x82表示PA输出、PB输入、PC输出,但如果把PC高四位和低四位当成一个整体方向,就会在电路上产生“输入引脚当输出用”的错误。PC口的双向能力只在位级和半字节级有效,不能单独控制某一位。这个约束在稍后的键盘扫描例子里会直接影响按键列的接法。
3. Proteus仿真环境下搭建8255最小电路与C语言驱动实现
3.1 元件选型与引脚连接:AT89C51+8255组合
Proteus里搭8255仿真电路,不需要外接晶振和复位电路,因为仿真模型内置了这些功能,但为了接近真实板子,建议把AT89C51、8255、74LS373、LED、排阻都放进来。核心连接原则是:51的P0口既做数据总线又做低8位地址总线,所以必须先经过74LS373锁存地址,再用P0直接连8255的D0-D7。
| Proteus元件 | 库内名称 | 用途 |
|---|---|---|
| 51单片机 | AT89C51 | 主控 |
| 并行接口芯片 | 8255 | IO扩展 |
| 地址锁存器 | 74LS373 | 分离P0口地址/数据信号 |
| LED指示灯 | LED-RED | 观察PA/PB输出 |
| 数字按键 | BUTTON | 键盘扫描输入 |
连线要点:P0口接74LS373的D输入,51的ALE接74LS373的LE锁存控制,373的Q输出接8255的A0、A1和片选译码逻辑。P2.7直接接8255的CS,P2.7引脚地址最高位,当地址高字节小于0x80时CS为低,8255被选中。RD、WR引脚分别接8255的RD、WR,这两个信号在仿真里不能接反,否则读操作会变成写操作。8255的RESET最好和51的复位输出接在一起,保证程序下载后8255被复位到初始状态。
3.2 用流水灯验证扩展IO方向
搭好电路后先跑一个最小测试程序,不做键盘扫描,不接显示,只用PA口驱动8个LED,验证8255的初始化是否真让端口工作了。流水灯代码:
#include <reg51.h> #include <absacc.h> #define PA XBYTE[0x0000] #define CWR XBYTE[0x0003] void delay_ms(unsigned int n) { unsigned int i, j; for (i = 0; i < n; i++) for (j = 0; j < 125; j++); } void main(void) { unsigned char led = 0xFE; CWR = 0x80; // 三个端口全部方式0输出 while (1) { PA = led; // 点亮对应的LED delay_ms(200); led = (led << 1) | 0x01; if (led == 0xFF) led = 0xFE; } }逻辑说明:初始值0xFE对应11111110,PA0输出低电平,LED0点亮。每次循环左移一位,最低位补1,使低电平逐个向右移动,形成跑马灯效果。led == 0xFF表示已经移过8位,此时重置回0xFE,循环重新开始。整个过程只操作8255的PA口,没有绕开控制字,所以能直接验证PA口方向是否设置成功。
参数说明:delay_ms(200)里的200是软件延时毫秒数,Proteus仿真速度受主机性能影响,实际延时可能比真实芯片偏大或偏小。如果仿真里LED闪烁速度异常,先不要改延时,优先确认单片机运行频率设置,AT89C51的仿真模型默认12MHz,但快速仿真模式下所有外设都跟着加速。P2.7接CS时,在程序里不要同时使用其他外部RAM地址,否则此时8255也会被选中,两个器件驱动同一组数据总线会造成冲突。
3.3 时序检查:为什么CS比WR早有效
很多人在Proteus里发现IO口方向一直都是0xFF,或者LED全亮不受控,第一反应是程序没烧进去,其实问题往往在8255的读写时序。8255要求有效的CS、A0、A1信号先建立,然后WR或RD信号再发生变化,数据总线上才能正确锁存。51单片机访问xdata时,P2口在整个读写周期内保持地址稳定,P0先输出低8位地址,由ALE锁存,然后电总线自动切换为数据。这说明从CPU内部时序上看,CS和A0、A1确实比WR早有效,但前提是373锁存器连接正确。
检查时序可以这样操作:在Proteus中放置一个LOGIC ANALYSER,把CS、WR、RD三根线接进去,然后设置仿真步进比普通运行慢。当程序执行到第二行CWR = 0x80时,会看到CS先变成低电平,约几十纳秒后WR出现一个低脉冲,数据线上的0x80必须在WR上升沿附近保持稳定。如果CS和WR同时变化,通常是P2.7接错了位置,或者373的LE没有接对。Proteus仿真不像实物那样暴露毛刺,但地址重叠和总线冲突仍然能通过电压探针看出来。
4. 8255工作方式0的键盘扫描与LED数码管显示实战
4.1 把键盘矩阵接到PA口,数码管接到PB/PC
方式0是8255最基本的输入输出模式,A组、B组都不需要应答信号,适合做键盘扫描和简单的数码管显示。这里用PA口驱动4行键盘行线,PB口读4列按键状态,PC口控制4位数码管的位选。方向如下:PA输出,PB输入,PC整体输出。对应控制字0x82。
为什么不是0x80?因为PB必须读按键状态,所以D1位要置1。0x80的二进制是10000000,PB被设置成输出,这时往里读数据会一直读到无效值。0x82的二进制是10000010,PA、PC输出,PB输入,PC高四位和低四位都输出。注意PC口方向不能单独控制某一位,但可以在方式0下把PC整体设为输出,所以位选信号全由PC输出是可行的。
硬件连接时,4根行线分别接PA0-PA3,4根列线接PB0-PB3,每根列线到地接一个10k下拉电阻。为什么用下拉而不是上拉?扫描时PA输出低电平,没有按键按下的行线为高阻,PB读取到高电平;一旦某键按下,对应行线的低电平通过按键传到列线,PB对应位被拉低。如果列线接上拉到VCC,按键按下拉低到地,逻辑也可以,但这要求PB口内部带上拉,8255的输入引脚没有内部上拉,所以仿真里需要外部器件。
4.2 键盘扫描与动态显示的C语言实现
#include <reg51.h> #include <absacc.h> #define PA XBYTE[0x0000] #define PB XBYTE[0x0001] #define PC XBYTE[0x0002] #define CWR XBYTE[0x0003] unsigned char code row_code[4] = {0xFE, 0xFD, 0xFB, 0xF7}; unsigned char code seg_code[10] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90}; unsigned char code bit_code[4] = {0xFE, 0xFD, 0xFB, 0xF7}; void delay_ms(unsigned int n) { unsigned int i, j; for (i = 0; i < n; i++) for (j = 0; j < 125; j++); } unsigned char scan_key(void) { unsigned char i, col; for (i = 0; i < 4; i++) { PA = row_code[i]; col = PB & 0x0F; if (col != 0x0F) { delay_ms(10); col = PB & 0x0F; if (col != 0x0F) { if (col == 0x07) return i * 4 + 0; if (col == 0x0B) return i * 4 + 1; if (col == 0x0D) return i * 4 + 2; if (col == 0x0E) return i * 4 + 3; } } } return 0xFF; } void display_num(unsigned char num) { unsigned char a = 0, b = 0, c = 0, d = 0; if (num == 0xFF) return; d = num % 10; c = (num / 10) % 10; b = (num / 100) % 10; a = (num / 1000) % 10; PC = bit_code[3]; PB = seg_code[a]; delay_ms(2); PC = bit_code[2]; PB = seg_code[b]; delay_ms(2); PC = bit_code[1]; PB = seg_code[c]; delay_ms(2); PC = bit_code[0]; PB = seg_code[d]; delay_ms(2); } void main(void) { unsigned char key = 0xFF; CWR = 0x82; while (1) { key = scan_key(); if (key != 0xFF) { while (key == scan_key()); display_num(key); } } }逻辑说明:scan_key函数依次把PA口的行码设为0xFE、0xFD、0xFB、0xF7,每次只让一行输出低电平。然后读取PB低四位,如果某一位为0,说明这一列的按键把行线上的低电平引到了PB引脚。二次判断和10ms延时是为了消抖,防止按键弹跳导致一次按下被识别成多次。返回值i * 4 + 0到i * 4 + 3把行和列位置编码成0到15的键值,方便后面查表显示。
参数说明:行码0xFE对应11111110,也就是PA0低电平,这一行被扫描。PB & 0x0F只保留列线对应的低四位,高四位是PC口的输出电平,不参与按键判断。列值0x07表示PB0、PB1、PB2为低,只有PB3为高,意味着按键在第4列;0x0B、0x0D、0x0E分别对应11111011、11111101、11111110,对应第2、3、4列。这种编码方式容量靠列值,如果按键矩阵不是4x4,需要调整掩码和判断分支。
4.3 动态显示刷新频率与按键响应的取舍
上面的显示函数在每次调用时依次刷新4位数码管,每一位点亮2ms后切到下一位。整体刷新周期约8ms,看起来数码管稳定不闪烁。主循环里先扫描键盘,再刷新显示,所以每次按键扫描之间会插入显示刷新时间,扫描周期会比单纯按键程序长。对4x4矩阵来说,只要行扫描速度在1ms左右,按键不会漏识别。
8255在这里体现出优势:PB输入时,读回的是引脚电平,不是锁存值,所以按键状态是实时的;PC输出时,位选信号被8255锁存,即使CPU去读PB,也不会影响位选输出电平。如果用P0直接驱动数码管位选,P0在读取外部RAM时会切换地址/数据状态,数码管会闪。换到8255后,数据总线上的变化不会直接出现在PC口上,这就是接口扩展带来的隔离效果。
5. 通过Proteus时序验证和调试8255的实用技巧
调试8255时我最先做的一件事,是在初始化后立刻把控制字读回来。虽然CWR寄存器是只写的,无法直接读,但可以用PA口做回环验证:PA输出一个已知字节,用导线把PA的8个引脚接到PB的8个输入引脚,然后让程序读PB并和原值比较。如果读回不一致,说明PA方向、PB方向或CS地址中至少有一处配置错误。
| 现象 | 可能原因 | 检查位置 |
|---|---|---|
| PA输出一直为0xFF | CS未拉低,8255未选中 | P2.7与CS连线 |
| PA输出正确但PB读回全1 | PB被设为输出,或下拉电阻没接 | 控制字0x82,列线电阻 |
| 控制字写不进去,端口方向不变 | 8255 RESET持续为高 | RESET是否与51复位连接 |
| 电平变化但LED微亮 | 8255输出驱动能力不足 | LED限流电阻是否过大 |
在Proteus虚拟示波器里观察WR信号时,可以配合Keil的断点功能。在CWR = 0x80那行设置断点,单步执行到赋值语句前,先查看P2、P0、ALE的状态;执行后再看8255的CS、WR、D0-D7引脚电平。8255数据手册里要求的“地址建立时间先于写信号”,在Proteus里可以直观看到CS拉低和WR拉低的间隔。如果两者几乎同时变化,检查373的锁存地址是否提前于WR到达,必要时把373的输出直接连到8255,而不经过中间信号。
最后一个小技巧是把所有8255寄存器访问封装成带volatile的指针,并且在主程序里用一个全局结构体管理软硬件备份。比如要控制数码管位选时,先修改备份变量,再一次性写入PC口,避免读改写造成的输出毛刺。把“写备份 → 写8255”变成固定模式,能减少大部分Proteus仿真里的诡异现象。
所以把控制字0x82写进CWR后才能放心操作PB输入,地址偏移和片选映射要写在源码注释第一行。
本文还有配套的精品资源,点击获取