FPGA模拟五颗接口芯片:80X86微机接口实验平台引脚与片选解析 简介《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_25CLK3当全局时钟扩展插座把 A0A15、D0D7、各芯片片选和读写信号全部引到排针上。后面七个实验的接线、端口地址、时钟源全部从这张引脚表长出来。整套课程资源的含金量也在这里它没把你当插线工而是把总线怎么分工摊开给你看让你知道每一次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 与 ICW248237 则要 A3A0 四根。低位地址线给芯片内部当“门牌号”高位地址线交给译码器产生 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。凭“相邻编号就是相邻位”的直觉去插线出错率极高。信号组典型引脚节选归属PA7PA01 / 3 / 10 / 31 / 33 / 38 / 42 / 448255 A 口PB7PB049 / 51 / 53 / 55 / 59 / 65 / 67 / 698255 B 口PC7PC011 / 32 / 34 / 39 / 43 / 46 / 50 / 528255 C 口A15A083 / 85 / 87 / 141 / 143 / 99 / 101 / 105 / 111 / 113 / 115 / 120 / 124 / 126 / 128 / 132各芯片地址线D7D0119 / 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_60CLK1 在 PIN_66、OUT1 在 PIN_68、GATE1 在 PIN_708251 的 CLK 输入在 PIN_908237 的时钟在 PIN_88。为什么发声实验要用 1MHz 而不是直接拿 50MHz 分频算一下就知道8254 是 16 位计数器最大初值 6553550MHz ÷ 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 的三个端口用得很干净PA0PA3 接键盘行线 X1X4PC0PC3 接列线 Y1Y4PB0PB7 接数码管的段码 adp位选 com1com6 由另一组端口位驱动。这样一片 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~gbit7 为 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 延时是软件消抖的常规量级机械触点抖动通常在 520ms 之间。push_key则是纯数组移位四个元素刚好对应四位数码管0x0F当空位标记显示时空位送灭段码。数码管这边是动态刷新同一时刻只点亮一位靠人眼视觉暂留形成同时显示的效果。刷新周期一般控制在 12ms 一位六位扫一轮不到 12ms就不会看出闪烁。如果把刷新塞进主循环里、又被 15ms 的键盘消抖卡住屏幕会明显发抖——这是后面排查章里会细说的典型翻车现场。4. 时间基准8254 的计数初值、8251 的波特率和点阵的行时序4.1 方式 3 方波发生器与计数初值让蜂鸣器唱歌的原理很朴素一个音符对应一个频率把该频率的方波送到扬声器上就发出这个音把一串音符的方波按顺序送出去就是一段旋律。8254 的方式 3 就是方波发生器写一个计数初值进去它就在 OUT 引脚上自动输出占空比 50% 的方波全程不占 CPU。计数初值的公式是初值 输入时钟 ÷ 输出频率。输入时钟取 1MHz要发 800Hz初值就是 1000000 ÷ 800 1250。这里有个隐含约束初值必须落在 165535 之间频率太低算出来超过 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所以一个输出端可以串多颗 LED25MHz 的移位时钟也足够在一行的时间里把 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 时序类能跑但跑得不对现象数码管显示的数字缺笔画或者全部亮成一团。原因段码表的位序与 PB0PB7 到 adp 的走线不一致或者共阴共阳搞反。解决先送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 引脚、片选、时钟、外设时序全摊在明面上每一层都能单独验证不会让你在黑匣子里瞎调。希望帮到你。本文还有配套的精品资源点击获取