简介:《TDX-PITE-80X86微机接口应用技术实验教程》面向学习微机原理与接口技术的高校学生及实验指导教师,以西安唐都科教仪器的实验平台为依托,帮助读者把课堂上抽象的总线、芯片时序知识落到具体硬件操作中。教程先从FPGA单元资源讲起,说明50MHz有源晶振经PIN_25提供全局时钟、JTAG下载口的位置以及扩展插座引脚分配,并列出8259、8255、8254、8251、8237等接口芯片与FPGA引脚的对应关系,为后续实验打基础;随后依次展开键盘扫描及显示、电子发声、点阵LED显示、双机通讯、步进电机、温度闭环控制与直流电机闭环调速七个实验,每个实验都给出电路连接、驱动方式与控制思路,读者可据此完成从接口编程到闭环反馈调节的完整训练。资源为1个PDF文件,压缩包约909KB,篇幅紧凑便于打印随堂使用,文末还附有技术支持网址、邮箱与QQ,遇到调试问题可循渠道求助。目前已有664人学习,适合作为实验课配套讲义或自学者的动手手册。
1. 一块 FPGA 顶五颗芯片:80X86 实验平台的“接口全在逻辑里”
一套 80X86 微机接口实验平台打开机箱盖,最反直觉的画面是:丝印上写着 8255、8254、8251、8259、8237 的位置,插座上却是空的。真正在应答 I/O 读写、能在总线上被 CPU 认出来的,是旁边那块 FPGA。这份课程资源的第一章就把这层窗户纸捅破了——出厂时 FPGA 内部已经烧好了五颗接口芯片的逻辑模型,一块 50MHz 有源晶振接到 PIN_25(CLK3)当全局时钟,扩展插座把 A0~A15、D0~D7、各芯片片选和读写信号全部引到排针上。后面七个实验的接线、端口地址、时钟源,全部从这张引脚表长出来。整套课程资源的含金量也在这里:它没把你当插线工,而是把总线怎么分工摊开给你看,让你知道每一次OUT DX, AL背后究竟是谁在应答。
2. 引脚映射的真相:一组地址/数据线怎么被五颗“芯片”分时占用
2.1 片选即身份:五个 cs 信号决定谁应答
80X86 的 I/O 空间是独立编址的。CPU 执行OUT DX, AL时,地址总线上送出端口号,IOR# 或 IOW# 拉低,总线上所有器件都能看见这组信号。谁响应,唯一的裁判是片选:只有 cs 有效的那个器件才会驱动数据总线或锁存写数据。FPGA 里的五个逻辑模型各有一根 cs 引出到排针——8255_cs 在 PIN_23、8254_cs 在 PIN_30、8251_cs 在 PIN_24、8259_cs 在 PIN_28、8237_cs 在 PIN_86,由实验平台上的 I/O 译码单元用 IOY0、IOY1、IOY2 之类的译码输出供给。
这套做法把“芯片身份”从硬件实体变成了接线关系。你想让 8255 出现在某个地址段,就把它的 cs 接到对应的译码输出上;想让两块逻辑共享一段地址,那必然有一个永远不应答。常见的实现思路是每个实验只把当前要用的 cs 接上,其他悬空或接无效电平,这样即使地址重叠也不会打架。
理解了这一层,排查就有方向了:程序写进去没反应,第一嫌疑不是代码,而是那根细细的片选线,或者 A1A0 两根低位地址线接反导致端口错位。8255 需要 A1、A0 选 PA/PB/PC/控制字四个端口,8254 需要 A1、A0 选三个计数器加控制字,8251 需要一根地址线区分数据口和命令口,8259 用 A0 区分 ICW1 与 ICW2~4,8237 则要 A3~A0 四根。低位地址线给芯片内部当“门牌号”,高位地址线交给译码器产生 cs,这个分工是整张表的骨架。
2.2 读引脚表:从 PIN_83 到 A15 的映射为什么必须记
引脚分配表看着枯燥,但它是修改接线时唯一的依据。FPGA 的引脚编号和信号名不是顺序对应的,A15 落在 PIN_83、A14 在 PIN_85、A13 在 PIN_87、A12 在 PIN_141,中间还跳着走;数据线同样跳:D7 在 PIN_119、D0 在 PIN_138。凭“相邻编号就是相邻位”的直觉去插线,出错率极高。
| 信号组 | 典型引脚(节选) | 归属 |
|---|---|---|
| PA7~PA0 | 1 / 3 / 10 / 31 / 33 / 38 / 42 / 44 | 8255 A 口 |
| PB7~PB0 | 49 / 51 / 53 / 55 / 59 / 65 / 67 / 69 | 8255 B 口 |
| PC7~PC0 | 11 / 32 / 34 / 39 / 43 / 46 / 50 / 52 | 8255 C 口 |
| A15~A0 | 83 / 85 / 87 / 141 / 143 / 99 / 101 / 105 / 111 / 113 / 115 / 120 / 124 / 126 / 128 / 132 | 各芯片地址线 |
| D7~D0 | 119 / 121 / 125 / 127 / 129 / 133 / 136 / 138 | 各芯片数据线 |
| IOW / IOR / RST# | 73 / 71 / 84 | 公共控制 |
这张表还解释了一个设计取舍:8255 的三个端口足足占了 24 根引脚,加上 16 根地址、8 根数据,FPGA 的可用 IO 消耗得很快。所以 8237 的 DMA 请求与应答信号(req0/req1、ack0/ack1、hold/hlda)只在需要做 DMA 实验时才接出来,平时空着。这不是偷懒,是把有限的引脚预算花在七个必做实验上。
2.3 时钟与复位:50MHz 进来,1MHz 出去
整块板子的时间基准只有两个来源。一个是 PIN_25 上的 50MHz 有源晶振,它同时充当系统全局时钟;另一个是双机通讯实验里那枚 1.8432MHz 的晶振,专门喂给 8254 的计数器 1 做波特率分频。8254 的 CLK0 在 PIN_54、OUT0 在 PIN_58、GATE0 在 PIN_60;CLK1 在 PIN_66、OUT1 在 PIN_68、GATE1 在 PIN_70;8251 的 CLK 输入在 PIN_90,8237 的时钟在 PIN_88。
为什么发声实验要用 1MHz 而不是直接拿 50MHz 分频?算一下就知道:8254 是 16 位计数器,最大初值 65535,50MHz ÷ 65535 约等于 763Hz,也就是说拿 50MHz 当输入,最低只能发出 763Hz 的音,低音区全丢了。换成 1MHz 输入,最低可到 15.3Hz,正好覆盖乐音范围;而 1MHz 本身通常就是对 50MHz 做一次固定分频得到的。这是个典型的“先降频再计数”的工程习惯——牺牲一点分辨率,换回整个可用频段。
复位 RST# 在 PIN_84,所有逻辑模型共用。JTAG 下载口夹在 8251 单元和 8237 单元之间,用来把新的逻辑配置或调试镜像灌进 FPGA。这一步是很多人的黑匣子:以为改了代码就能生效,实际上 FPGA 里的逻辑是出厂预置的,除非你重新下载,否则它不会变。
3. 8255 的两副面孔:矩阵键盘的扫描与数码管的动态刷新
3.1 4×4 键盘为什么必须“扫描”而不是“读电平”
16 个按键如果一对一接线,要吃掉 16 根 IO。做成 4 行 4 列的矩阵,只需要 8 根:4 根行线、4 根列线,交点处放一个按键。这样做的代价是——你没法从电平直接知道“哪个键按下了”。当所有行线同时为低,任何一列被拉低都只说明“这一列上有键按下”,共 4 种可能;只有把行线逐行拉低、其余行保持高电平,再读列线,才能把“第几行 × 第几列”唯一确定下来。
这就是扫描的本质:用时间换引脚。每一轮只激活一行,读一次列,四轮走完就遍历了全部 16 个键。电路上列线通过 1KΩ 电阻上拉到 VCC,并经过缓冲器整形,所以没有键按下时读到的列值是全 1,一旦某列被拉低就说明该行该列交点闭合。
3.2 端口分工:PA 行、PC 列、PB 段码
平台上的接线图把 8255 的三个端口用得很干净:PA0~PA3 接键盘行线 X1~X4,PC0~PC3 接列线 Y1~Y4,PB0~PB7 接数码管的段码 a~dp,位选 com1~com6 由另一组端口位驱动。这样一片 8255 同时承担了输入(读列)和两路输出(行扫描、段码),是接口课里最经典的复用示范。
要注意段码位序和硬件走线的对应关系。PB0 对 a、PB1 对 b……PB7 对小数点,这个顺序一旦和段码表里的位定义错开,显示出来的数字就是乱的,而且乱得有规律——比如 1 显示成别的形状。写程序前先拿一个“全段点亮”的测试值0xFF跑一遍,六个数码管全亮,再逐段点亮验证,比对着代码猜快得多。
3.3 最近四次按键的环形缓存
实验要求数码管显示最近 4 次按下的键号,新按键挤掉最旧的那个。这就是一个长度为 4 的环形队列,每来一个新键就整体前移一位,新值放到队尾。用数组实现时不需要真的“环”,四次赋值即可。
/* 端口地址按实际片选 IOY 与 A1A0 译码修改,这里用宏占位 */ #define PORT_A 0x0280 #define PORT_B 0x0281 #define PORT_C 0x0282 #define PORT_CTL 0x0283 static const unsigned char seg7[16] = { 0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07, /* 0~7 */ 0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71 /* 8~F */ }; /* bit0~bit6 对应 a~g,bit7 为 dp,共阴编码 */ static unsigned char hist[4] = {0x0F,0x0F,0x0F,0x0F}; unsigned char scan_key(void) { unsigned char row, col, code = 0xFF; for (row = 0; row < 4; row++) { outb(PORT_A, (unsigned char)~(1 << row)); /* 当前行拉低,其余为高 */ col = inb(PORT_C) & 0x0F; /* 只取低四位列线 */ if (col != 0x0F) { /* 有列被拉低,可能按下 */ delay_ms(15); /* 消抖:等抖动过去 */ col = inb(PORT_C) & 0x0F; if (col != 0x0F) { /* 找出最低的那个 0 位,就是列号 */ unsigned char bit = 0; while ((col & (1 << bit)) != 0) bit++; code = (unsigned char)(row * 4 + bit); while ((inb(PORT_C) & 0x0F) != 0x0F) ; /* 等按键释放,防止连发 */ } } } return code; /* 0~15 为键号,0xFF 表示本轮无键 */ } void push_key(unsigned char k) { hist[0] = hist[1]; hist[1] = hist[2]; hist[2] = hist[3]; hist[3] = k; /* 新键进队尾,最旧的 hist[0] 被挤掉 */ }scan_key的关键在三处:~(1 << row)保证任何时刻只有一根行线为低,否则列读数会混叠;& 0x0F是因为 8 位端口的高四位可能另作他用,不屏蔽就读到假数据;15ms 延时是软件消抖的常规量级,机械触点抖动通常在 5~20ms 之间。push_key则是纯数组移位,四个元素刚好对应四位数码管,0x0F当空位标记,显示时空位送灭段码。
数码管这边是动态刷新:同一时刻只点亮一位,靠人眼视觉暂留形成同时显示的效果。刷新周期一般控制在 1~2ms 一位,六位扫一轮不到 12ms,就不会看出闪烁。如果把刷新塞进主循环里、又被 15ms 的键盘消抖卡住,屏幕会明显发抖——这是后面排查章里会细说的典型翻车现场。
4. 时间基准:8254 的计数初值、8251 的波特率和点阵的行时序
4.1 方式 3 方波发生器与计数初值
让蜂鸣器唱歌的原理很朴素:一个音符对应一个频率,把该频率的方波送到扬声器上就发出这个音,把一串音符的方波按顺序送出去就是一段旋律。8254 的方式 3 就是方波发生器,写一个计数初值进去,它就在 OUT 引脚上自动输出占空比 50% 的方波,全程不占 CPU。
计数初值的公式是初值 = 输入时钟 ÷ 输出频率。输入时钟取 1MHz,要发 800Hz,初值就是 1000000 ÷ 800 = 1250。这里有个隐含约束:初值必须落在 1~65535 之间,频率太低算出来超过 65535 就会溢出成别的音,频率太高则精度变差。以 1MHz 输入为例,可发声范围大约是 15.3Hz 到 1MHz,覆盖了全部乐音。
#define P8254_BASE 0x0200 /* 按实际片选与 A1A0 译码修改 */ #define P8254_CTRL (P8254_BASE + 3) /* freq 单位 Hz,输入时钟固定 1MHz */ void tone_start(unsigned int freq) { unsigned int div = (unsigned int)(1000000UL / freq); if (div < 1 || div > 65535) return; /* 超出 16 位量程直接放弃 */ outb(P8254_CTRL, 0x36); /* 计数器0,先低后高,方式3,二进制 */ outb(P8254_BASE + 0, (unsigned char)(div & 0xFF)); outb(P8254_BASE + 0, (unsigned char)(div >> 8)); }控制字 0x36 拆开看是00 11 011 0:最高两位选计数器 0,接着两位 11 表示先读写低字节再读写高字节,011 是方式 3,最低位 0 表示二进制计数(若是 1 则按 BCD 解释,初值会被当成十进制数,音就全跑了)。写入顺序必须是先控制字、后初值,而且低字节在前;顺序反了,8254 会把高字节当低字节锁进去,发出来的频率差 256 倍。
还有个容易忽略的引脚:GATE0。方式 3 下 GATE 为低会禁止计数,输出停在当前电平。接线时 GATE 一般接高电平或由端口控制,忘了这根线,程序写得再对也是一片寂静。
4.2 频率表与时间表的双指针结构
一段曲子需要两张表:频率表记录每个音符的频率,时间表记录每个音符持续多少个单位延时。两张表一一对应,用两个指针同步推进,读到频率表的 0 就说明到曲末,回头重来。这个结构的好处是把“音高”和“节奏”彻底解耦,改曲子只需要改表,代码一行不动。
/* 简谱片段示意:音高与相对时值各一张表,末项 0 作为结束标志 */ const unsigned int freq_tab[] = {262, 294, 330, 349, 392, 440, 494, 523, 0}; const unsigned char time_tab[] = { 4, 4, 4, 4, 2, 2, 4, 8, 0}; void play_song(void) { unsigned char i = 0; while (freq_tab[i] != 0) { tone_start(freq_tab[i]); /* 装初值,开始发声 */ delay_units(time_tab[i]); /* 按拍数延时,控制音长 */ i++; } tone_stop(); /* 曲末关掉 GATE 或写 0 静音 */ }delay_units对应教程里那个用MOV CX,0010H套MOV AX,0F00H递减的软件延时子程序,DL传倍数。它是彻头彻尾的忙等:CPU 把所有时间耗在循环里,什么也干不了。代价是节拍完全依赖 CPU 主频——换一台机器、换一档主频,同一份时间表奏出来的速度就不一样,这是它最典型的血泪经验。用 8254 的另一个计数器做硬件定时中断来打拍子会稳得多,但教程选择软件延时,是因为它把“定时器输出波形”和“CPU 控制节奏”这两件事拆开讲,学习者能分别看清各自的职责。
顺带说,音符与频率的对照表是查表法的基础:每个音名对应一个固定频率值,程序里存的是频率而不是音名,省去运行时的换算。表越长,能表达的音域越宽,代价是占用的存储空间。
4.3 点阵:16206 移位锁存与 5166 行译码的配合
16×16 点阵由四块 8×8 拼成,显示一个汉字需要 16 行 × 16 列共 256 个点。逐点控制要 256 根线,显然不现实,所以列方向用串行移位、行方向用译码扫描,最终只需要几根信号线。
列驱动用 SM16206,它内部是移位寄存器加锁存器,串行数据从 SDI 进、SCK 打拍、LE 锁存、OE# 控制输出使能,把串行位流一次性变成并行输出。它的输出耐压可达 +15V,所以一个输出端可以串多颗 LED;25MHz 的移位时钟也足够在一行的时间里把 16 位数据全部推完。行驱动用 SM5166,内置 3-8 译码器,A2A1A0 给出 8 选 1 的行地址,EN1/EN2 使能,每个通道最大 2.5A,输出端还带钳位电路,专门压掉 LED 屏切换行时那种“列上拖影”。
点亮逻辑是行高有效、列低有效:某行给 1、对应列给 0,交点那颗 LED 就亮。所以扫描时一行一行来——选通一行、送这一行的 16 位列数据、延时约 1ms、换下一行,16 行走完是一帧,帧率约 60Hz,肉眼看上去就是稳定的一整幅字。
/* 送出一行 16 位点阵数据,row 取 0~7,由两片 5166 各管 8 行 */ void led_send_row(unsigned char row, unsigned int data) { int i; outb(LED_OE, 0x01); /* 先消隐,避免换行瞬间的残影 */ outb(LED_ROW, row & 0x07); /* 5166 的 A2A0 选行,EN 保持有效 */ for (i = 0; i < 16; i++) { /* 16 位串行移入 16206 */ outb(LED_CLK, 0x00); outb(LED_SDI, (data & 0x8000) ? 1 : 0); /* 高位先出 */ data <<= 1; outb(LED_CLK, 0x01); /* 上升沿移入一位 */ } outb(LED_LE, 0x01); /* 锁存,并行送到列线 */ outb(LED_LE, 0x00); outb(LED_OE, 0x00); /* 开显示 */ delay_us(1000); /* 每行停留 1ms,一帧约 16ms */ }参数上有三个细节必须和硬件对齐。第一是移位方向:代码里取最高位先出,如果 16206 的移位顺序相反,整行数据要按位倒过来,否则显示出来是左右镜像。第二是消隐顺序:先关 OE、再换行、再送数据、最后开 OE,这个顺序错一步就会出现横条拖影。第三是字模的取法:教程里用取模工具按“横向 8 点一个字节、最左点为 BIT0、最右点为 BIT7”的格式生成,16×16 汉字每行 2 字节、共 16 行 32 字节,四块 8×8 的取字顺序是左上、右上、左下、右下。取模方式和扫描时序一旦不一致,字就会缺角或者上下错行——这是点阵实验里最常见的“明明代码没改却显示错位”的根源。
5. 常见问题排查:接线、端口与取模的五个坑
5.1 接线类:程序跑得通,硬件没反应
现象:程序编译下载一切正常,运行时读端口恒为 0xFF、写端口外设毫无动静。原因:片选线没接到译码输出,或者 A1、A0 两根低位地址线在排针上插反了,导致 CPU 访问的端口号和芯片内部寄存器对不上。解决:先拿万用表确认 cs 与 IOY 的连通性,再写一个只访问单个端口的循环,用示波器看片选上有没有出现低脉冲;没有脉冲就是译码或接线问题,有脉冲但数据不对就是 A0/A1 错位。
现象:8255 的 PA 口输出正常,PC 口读回来永远是同一个值。原因:8255 的控制字只写了模式,没有把 PC 口的高低位方向设对,或者把 PC 口当成了输出口却在读它。解决:重新确认控制字里 A 组、B 组的模式位与方向位,读之前先写一次控制字把 PC 高四位设成输入。
5.2 时序类:能跑但跑得不对
现象:数码管显示的数字缺笔画,或者全部亮成一团。原因:段码表的位序与 PB0~PB7 到 a~dp 的走线不一致,或者共阴共阳搞反。解决:先送0xFF看全亮,再送0x01、0x02逐位点,确定哪一位对应哪一段;共阳编码就是共阴编码按位取反。
现象:发声实验出来的调子整体偏高或偏低,甚至像噪音。原因:8254 的输入时钟不是设想的 1MHz,而是直接从 50MHz 全局时钟接过来的,或者初值算出超过 65535 被截断,又或者控制字写成了 BCD 计数。解决:先量 CLK0 的实际频率,再核对控制字 0x36 的四段含义,最后确认 GATE0 为高。
现象:双机通讯收端屏幕上什么也没有。原因:两台机器的 TXD、RXD 没有交叉(都接成了 TXD 对 TXD),或者地线没共,或者两边的波特率初值不一致,或者 8251 的模式字、命令字顺序写反。解决:先做自环——把本机 TXD 短接到 RXD,自发自收能通再连对方;8251 复位后必须先写模式字再写命令字,顺序反了芯片会一直停在等待状态。
5.3 取模类:显示出来是“半个字”
现象:点阵上能看出字的轮廓,但笔画错位、上下断开。原因:取模时选的扫描方向与程序里的行递增方向不一致,或者一个字节内 BIT0 与 BIT7 的定义和硬件移位顺序相反。解决:写一个只点亮左上角一个点的测试图案,看它实际出现在哪个位置,据此决定是否整体按位反转、是否要倒序送行。这一步不要靠猜,靠一个点就能定位。
提示:上面五个坑里有四个的共同点是“软件逻辑对、软硬边界错”。排查时永远从最靠近硬件的那一层往上走——先量电平,再看时序,最后才怀疑算法。
6. 开环、闭环与采样周期:三个控制类实验背后的取舍账
步进电机、温度闭环、直流电机调速这三个实验,本质上是同一道题的三档答案。步进电机是开环:发多少个脉冲就走多少步,控制器完全不知道电机有没有丢步,好处是结构简单、响应直接,坏处是负载一大或加速太猛就会失步,而且失步之后没有任何反馈能纠正。温度闭环和直流调速则是闭环:传感器定期采样,和设定值比较,误差送进控制算法去调整执行器。多出来的这层反馈,换来了抗扰动能力,也换来了采样周期、控制周期、执行延迟这三者互相牵制的麻烦。
常见的实现思路是先用最笨的阈值控制跑通链路——温度低于下限就加热、高于上限就断电,直流电机就用固定占空比的 PWM 试转——确认采样通路和执行通路都活着,再换成 PID 或者带死区的控制。直接从 PID 起步的人,往往在参数整定上卡住,却分不清到底是增益不对,还是采样周期比被控对象的响应时间还慢。
这里有个很实在的取舍:采样周期越短,控制越及时,但 CPU 被中断吃掉的越多,而且传感器本身的响应速度可能根本跟不上;采样周期太长,系统就会在设定值附近来回振荡,看起来像参数没调好,实际是采样太稀。我在做这类实验时的习惯是先把采样周期设成被控对象时间常数的十分之一左右,跑通了再往短里压。
从那以后我每次碰闭环实验,都强制先把开环跑一遍、记录下每个环节的延迟和量程,再去闭合反馈——不然调参调到最后,根本分不清是算法的问题还是某一段链路的延迟在捣鬼。这套课程资源的好处就在于它把 FPGA 引脚、片选、时钟、外设时序全摊在明面上,每一层都能单独验证,不会让你在黑匣子里瞎调。希望帮到你。
本文还有配套的精品资源,点击获取