1. 项目缘起:从“能用”到“好用”的PWM发生器
最近在整理实验室的旧设备,翻出来一台基于8086和8253、8255芯片的老式PWM信号发生器。这玩意儿当年可是宝贝,用它驱动过电机、调过灯光,甚至做过简单的电源控制。但说实话,用它做项目,调试过程堪称“折磨”——频率、占空比得靠拨码开关和跳线帽来设定,改个参数得断电、拔插、再上电,效率低不说,还容易出错。看着现在满大街的STM32,一个CubeMX点几下就能出PWM,我就在想,能不能用这些“古董”芯片,做一台既保留硬件直接控制的高可靠性,又具备现代设备易用性的PWM信号发生器?不是为了怀旧,而是想彻底搞明白,在没有现成库函数和高级定时器的年代,一个稳定、灵活、可编程的PWM信号到底是怎么从芯片引脚上“变”出来的。这个过程,对理解今天MCU内部PWM外设的工作原理,有莫大的帮助。
所以,这个“研制”项目,目标很明确:以经典的Intel 8086微处理器为核心,搭配8253可编程定时器和8255可编程并行接口芯片,设计并制作一个数字可调的PWM信号发生器。它要能通过软件(汇编语言)灵活设定频率和占空比,输出稳定、干净的方波,并且具备一定的驱动能力和隔离保护,可以直接用于控制舵机、调试电机驱动电路,或是作为其他数字电路的时钟/控制信号源。这不仅仅是一个简单的电路搭建,更是一次对计算机硬件原理、定时器工作模式、以及软件精确时序控制的深度实践。
2. 核心架构设计:为什么是8086+8253+8255这个“铁三角”?
提到用微处理器产生PWM,很多人的第一反应是STM32、Arduino,甚至用NE555搭个硬件电路。选择8086这套古老的组合,恰恰是为了避开“黑盒”,从最底层厘清信号产生的每一个环节。这个架构的每一个选择,背后都有其明确的工程考量。
2.1 处理器选型:8086的“纯粹”与控制力
为什么不用更简单的51单片机?因为8086是一个“纯粹”的微处理器,它没有片内ROM、RAM,更没有PWM外设。所有功能都必须通过外部总线连接芯片来实现。这种“赤裸”的状态,迫使我们必须清晰地规划地址总线、数据总线和控制总线,理解CPU如何与外围芯片通信(IO映射还是内存映射),如何通过指令精确地控制定时器的每一个计数周期。这个过程,是理解任何现代MCU外设驱动本质的绝佳训练。8086的16位数据总线也能更方便地处理我们后续需要设置的定时器初值(尤其是8253的16位计数寄存器)。当然,你也可以用8088,区别不大,核心思想一致。
2.2 定时器核心:8253如何成为“数字时钟发生器”
PWM的核心是一个可精确控制时间和占空比的时钟源。8253芯片内部有三个独立的16位减法计数器,每个都可以工作在六种不同模式。对于PWM生成,我们主要用到它的模式2(速率发生器)和模式3(方波发生器)。
- 模式3(方波发生器):这是产生对称方波(占空比50%)的天然选择。计数器减到一半时输出变低,减到零时输出变高并自动重装初值,循环往复。但它的占空比是固定的50%,无法直接调节。
- 模式2(速率发生器):这个模式更灵活。计数器工作时输出为高电平,当计数器减到1时,输出一个时钟周期的低脉冲,然后自动重装初值,输出恢复高电平。这里就是关键:如果我们用软件动态地、在计数器运行过程中改变重装值(即计数初值),就能改变输出高电平的持续时间,从而改变占空比。但8253在模式2下,低脉冲宽度是固定的(一个输入时钟周期),这会导致在低频时,低电平脉宽占比过大,不适合做PWM。
因此,更经典的方案是使用两个8253计数器级联。Counter0工作在模式3,产生一个固定频率的基准时钟(比如100kHz);这个时钟作为Counter1的输入。Counter1则工作在模式1(可重触发的单稳态触发器)或模式2。通过程序不断改变Counter1的计数初值,就能在Counter1的输出端得到占空比可变的脉冲信号。但这种方法对软件时序要求极高。
实际上,一个更直接、更稳定的方法是:只用一个8253计数器(如Counter0)工作在模式3,产生一个固定频率(比如1kHz)的方波作为“时间基准帧”。然后,我们利用8086的软件和另一个外围芯片(8255)来在这个“帧”内,用数字逻辑“画出”高电平的宽度。这就是8255登场的原因。
2.3 接口与逻辑控制:8255的角色从“扩展IO”到“硬件比较器”
8255是一颗经典的并行接口芯片,有3个8位端口(PA, PB, PC),通常用来连接键盘、显示器等。但在这个项目里,我们赋予它一个更核心的角色:硬件比较与信号生成器。
具体思路如下:
- 8253(Counter0):配置为模式3,输出一个固定频率(如1kHz)的方波。这个方波的上升沿代表一个PWM周期的开始。我们将这个信号连接到8255的PC4(或某个端口引脚),作为“周期开始”的中断或查询信号。
- 8086软件:根据用户设定的占空比(比如50%),计算出一个对应于高电平时间的计数值。例如,PWM周期为1ms(1kHz),占空比50%,则高电平时间应为500us。如果CPU时钟是5MHz,那么需要执行的指令周期数约为
500us / 0.2us = 2500个周期。我们把这个“高电平时间”对应的值,通过数据总线写入8255的某个端口(例如PA口)。 - 8255的“神奇”用法:我们将8253输出的周期信号和8086写入的“占空比设定值”都送入一个由数字逻辑芯片(如74LS85比较器)搭建的小电路,但这里我们可以用8255的端口C的位控功能来模拟一部分逻辑。更典型的做法是,8086在检测到周期开始(PC4上升沿)后,立即将8255的某个输出引脚(如PC0)置为高电平(开启PWM输出),同时启动一个软件延时循环或利用另一个8253计数器(Counter1)做硬件计时。当延时到达设定值时,8086再将PC0拉低。这样,从PC0输出的就是由软件和硬件共同确定的PWM波。
这个架构的精妙之处在于,8253提供了精准的周期基准,解决了频率稳定性的问题;而8086和8255则通过软件和可编程IO,实现了占空比的灵活可变。它分离了“定时”和“控脉宽”两个任务,比单纯依赖一个定时器硬件模式要更直观、更易于理解,也更容易调试。
3. 硬件电路设计与关键细节
有了架构,就需要把它落实到具体的电路连接上。这里面的每一个连接点,都藏着可能让你调试一整天的“坑”。
3.1 最小系统与总线连接
8086需要构成一个最小系统:地址锁存器(如74LS373)、数据总线收发器(如74LS245)、时钟发生器(如8284A)、以及EPROM(如2764)和SRAM(如6264)构成的内存系统。这是基础,必须稳定可靠。8253和8255作为8086的IO外设,通过地址译码器(如74LS138)分配独立的端口地址。
- 地址译码:假设我们使用线选法或译码器,为8253分配端口地址60H~63H(对应控制字和三个计数器),为8255分配端口地址70H~73H(对应控制字和三个端口)。这个地址必须在软件中严格对应。
- 关键连接:
- 8253的CLK0:连接8284A输出的系统时钟(如2MHz)。这是8253计时的时间基准,其稳定性直接决定PWM频率的稳定性。
- 8253的OUT0:输出我们需要的“周期帧”信号(如1kHz方波)。此信号连接至8255的PC4,同时也可以接一个LED用于直观指示。
- 8255的PA口:设置为输出模式,可以连接LED阵列,用于显示当前设定的占空比数值(二进制或BCD码形式),方便调试。
- 8255的PC0:这是我们最终的PWM信号输出引脚。设置为输出模式。非常重要的一点:PC0引脚驱动能力有限(通常几个mA),不能直接驱动电机或大电流LED。必须后级接驱动电路。
3.2 输出驱动与隔离电路
直接从8255引脚出来的PWM信号是TTL电平(0-5V),电流极小。要驱动舵机、电机或者作为其他系统的输入,必须经过缓冲和隔离。
- 缓冲驱动:使用一个74HC04(非门)或74HC07(缓冲器)来增强电流驱动能力。也可以使用晶体管(如2N2222)搭建一个简单的开关电路。当PC0为高时,晶体管饱和导通,输出端近似接地(低电平);当PC0为低时,晶体管截止,输出端通过上拉电阻变为高电平。注意:这是一个反相器,软件逻辑需要相应取反。
- 光耦隔离:如果PWM发生器用于控制电机等可能引入干扰的强电设备,光耦隔离是必须的。推荐使用TLP521-1或PC817。将8255的输出经过一个限流电阻连接到光耦的发光二极管侧,光耦的三极管侧连接目标系统的电源和信号输入。这样可以完全隔离两个系统的地线,避免噪声和高压窜入损坏精密的数字电路。
- RC滤波(可选):如果你需要将PWM转换为模拟电压(比如做简易DAC),可以在输出端接一个低通滤波器(一个电阻和一个电容到地)。PWM频率越高,滤波后的电压纹波越小。这是实现“PWM转4-20mA”或类似功能的基础。
3.3 电源与去耦
8086、8253、8255都是老式的5V CMOS/TTL芯片。一个干净的5V电源是系统稳定的基石。建议使用7805线性稳压芯片,并在其输入、输出端并联足够大的电解电容(如100uF)和小的瓷片电容(0.1uF)。最关键的是,必须在每一片数字芯片的VCC和GND引脚之间,就近放置一个0.1uF的瓷片去耦电容。这个电容可以为芯片高速开关时产生的瞬间电流提供本地能量,是消除数字噪声、防止系统莫名其妙重启或工作不稳定的最有效、最廉价的措施。
4. 软件设计:汇编语言下的精确时序舞蹈
硬件是躯体,软件是灵魂。用汇编语言给8086编程来控制PWM,是对程序员理解硬件和时序能力的终极考验。这里没有HAL_PWM_Start()这样的函数,每一个微秒都需要你精心安排。
4.1 初始化流程:给芯片“上规矩”
程序的第一步是初始化所有芯片,告诉它们该如何工作。
; 假设端口地址定义 PORT_8255_CTL EQU 73H ; 8255控制口 PORT_8255_A EQU 70H ; 8255 A口 PORT_8255_C EQU 72H ; 8255 C口 PORT_8253_CTL EQU 63H ; 8253控制口 PORT_8253_CNT0 EQU 60H ; 8253计数器0 ORG 1000H ; 程序起始地址 START: ; 1. 初始化8255:A口输出,C口高4位输入(接8253 OUT0),低4位输出(PC0输出PWM) MOV AL, 10001001B ; 控制字:A口模式0输出,C口高4位输入,低4位输出,B口未用可设 OUT PORT_8255_CTL, AL MOV AL, 00000000B ; 初始时,PC0输出低电平 OUT PORT_8255_C, AL ; 2. 初始化8253计数器0:模式3,先读写低字节,后高字节,二进制计数 MOV AL, 00110110B ; 控制字:选择计数器0,读写双字节,模式3,二进制 OUT PORT_8253_CTL, AL ; 计算计数初值。假设系统CLK=2MHz,要产生1kHz方波(周期1ms) ; 计数初值 = 输入时钟频率 / 输出频率 = 2,000,000 / 1,000 = 2000 MOV AX, 2000 OUT PORT_8253_CNT0, AL ; 先写低字节 MOV AL, AH OUT PORT_8253_CNT0, AL ; 再写高字节 ; 此时,8253的OUT0引脚应该已经开始输出1kHz的方波。4.2 核心PWM生成循环:在“时间窗口”内操作
初始化完成后,就进入主循环。我们的策略是:不断查询8255 PC4(连接8253 OUT0)的状态,检测其上升沿。一旦检测到上升沿(表示一个新的PWM周期开始),就立即设置PC0为高,然后根据设定的占空比进行延时,延时结束后再将PC0拉低,等待下一个周期开始。
; 假设占空比设定值存储在变量DUTY_CYCLE中(单位:微秒对应的延时循环次数) ; 例如,占空比50%,周期1ms,则高电平时间500us。假设一个循环约10us,则需要循环50次。 MOV DUTY_CYCLE, 50 MAIN_LOOP: ; 等待上升沿:先等待PC4为低 WAIT_LOW: IN AL, PORT_8255_C AND AL, 00010000B ; 屏蔽其他位,只检查PC4 (BIT4) JNZ WAIT_LOW ; 不为0(高电平),则继续等 ; 再等待PC4变高 WAIT_HIGH: IN AL, PORT_8255_C AND AL, 00010000B JZ WAIT_HIGH ; 为0(低电平),则继续等 ; 检测到上升沿!开始新的PWM周期 ; 1. 立即将PC0置为高电平,输出PWM高电平 IN AL, PORT_8255_C ; 读回C口当前状态 OR AL, 00000001B ; 将PC0 (BIT0)置1,不影响其他位 OUT PORT_8255_C, AL ; 2. 根据占空比进行软件延时 MOV CX, DUTY_CYCLE DELAY_HIGH: LOOP DELAY_HIGH ; CX减1,不为0则跳转。注意LOOP指令耗时较多,需校准。 ; 3. 延时结束,将PC0拉低 IN AL, PORT_8255_C AND AL, 11111110B ; 将PC0 (BIT0)清0 OUT PORT_8255_C, AL ; 4. 循环继续,等待下一个上升沿 JMP MAIN_LOOP注意:上述代码中的软件延时
LOOP指令非常不精确,且受中断影响。这只是原理演示。在实际项目中,绝对不应该用软件空循环来实现高精度的PWM延时,这会导致CPU利用率100%且精度极差。
4.3 高精度占空比控制方案:引入第二个硬件定时器
为了获得精确且不占用全部CPU时间的PWM,必须使用硬件定时。我们可以启用8253的另一个计数器(Counter1)。
- 初始化Counter1为模式1(硬件可重触发单稳态)。它的特点是:写入初值后,OUT1输出低电平;当GATE1引脚出现一个上升沿时,OUT1跳变为高电平,并开始减计数;计数到0时,OUT1跳回低电平。高电平的持续时间完全由计数初值决定。
- 连接电路:将8253的OUT0(周期信号)连接到Counter1的GATE1。同时,将Counter1的OUT1作为最终的PWM输出。
- 软件操作:程序只需要在需要改变占空比时,向Counter1写入新的计数初值即可。下一个OUT0的上升沿(周期开始)到来时,Counter1会自动产生一个精确宽度的正脉冲。
这种方案将占空比的定时精度完全交给了8253硬件,软件负担极小,精度极高,是工业级的做法。软件部分简化如下:
; 初始化8253 Counter1 MOV AL, 01110010B ; 控制字:计数器1,读写双字节,模式1,二进制 OUT PORT_8253_CTL, AL ; 占空比初值,例如高电平500us,时钟2MHz,初值= 500us / 0.5us = 1000 MOV AX, 1000 OUT PORT_8253_CNT1, AL ; 写低字节 MOV AL, AH OUT PORT_8253_CNT1, AL ; 写高字节 ; 主循环只需要响应用户输入,更新占空比值即可 UPDATE_LOOP: ; ... 检测按键或串口命令,计算新的占空比初值到AX ... OUT PORT_8253_CNT1, AL MOV AL, AH OUT PORT_8253_CNT1, AL JMP UPDATE_LOOP5. 调试、测量与常见问题排查
电路焊好了,程序烧进去了,但示波器上可能什么都没有,或者波形惨不忍睹。别慌,按照以下步骤排查,绝大部分问题都能解决。
5.1 上电无输出:从电源到信号流
- 电源与复位:首先用万用表测量所有芯片的VCC引脚是否为稳定的+5V(±5%)。检查8086的RESET引脚在上电后是否有一个从高到低的跳变(可用示波器看)。没有正确的复位,CPU不会开始执行程序。
- 时钟信号:用示波器检查8284A的CLK输出(连接到8086和8253的CLK引脚)是否有规则的方波(例如2MHz)。这是系统的心跳。
- 总线活动:将示波器探头放在8086的地址线(如A0)或数据线(如D0)上,上电后应该能看到杂乱无章的脉冲信号,这说明CPU正在从EPROM中读取指令并执行。如果总线上是死寂的直流电平,说明CPU没有工作,检查复位、时钟、EA(接高电平)和ALE信号。
- 芯片片选:检查8253和8255的CS(片选)引脚,在程序访问它们对应的端口地址时,应该有负脉冲出现。如果没有,说明地址译码电路有问题,芯片永远未被选中。
5.2 有输出但波形不对:频率、占空比、毛刺
- 频率不准:如果8253 OUT0的频率不是预期的1kHz,首先确认写入8253的计数初值是否正确(注意先低后高字节)。其次,用示波器测量8253的CLK0输入频率是否准确为2MHz。最后,检查8253的控制字是否写错,模式3才会输出方波。
- 占空比不可调或不准:
- 软件延时法:如果采用软件延时,占空比严重不准是必然的,因为循环次数受中断和指令执行时间波动影响。必须换用硬件定时器方案(8253 Counter1)。
- 硬件定时器法:如果用了Counter1,占空比仍不准,检查写入Counter1的初值是否正确,以及Counter1是否配置为模式1。关键一点:模式1下,GATE1的上升沿才会触发新的单稳态周期,确保OUT0到GATE1的连接可靠。
- 输出波形有毛刺:
- 驱动能力不足:当PWM输出线连接较长的导线或容性负载时,边沿会产生振铃。在输出引脚串联一个22-100欧姆的小电阻,可以显著改善。
- 电源噪声:用示波器探头尖和地线环直接测量芯片电源引脚上的电压,看PWM跳变时是否有跌落或尖峰。加强去耦电容(在芯片电源引脚并接10uF电解和0.1uF瓷片)。
- 地线环路:示波器探头地线会引入环路干扰。尝试使用探头附带的接地弹簧代替长地线夹。
5.3 驱动外部负载失败
- 舵机不转:舵机需要周期为20ms(50Hz),高电平脉宽在0.5ms到2.5ms之间的PWM信号。首先确保你的发生器频率是50Hz(周期20ms)。其次,用示波器测量输出到舵机信号线的实际波形,电压是否达到5V(有些舵机要求5V,有些3.3V),高电平脉宽是否在范围内。如果电压不够,需要加缓冲器(如74HC07)或晶体管。
- 电机不转或抖动:L298N等电机驱动模块的使能端(ENA, ENB)通常需要接受PWM。确保你的发生器输出连接到正确的使能端,而不是输入端。同时,电机是感性负载,在PWM关断时会产生很高的反向电动势,必须在电机两端并联续流二极管(通常L298N模块内部已有),否则可能损坏驱动芯片或你的发生器输出级。
- LED亮度调节不平滑:用PWM调光时,如果频率太低(比如低于100Hz),人眼会感到闪烁。建议将PWM频率提高到200Hz以上。如果提高频率后LED还是有关断时的微光,可能是驱动晶体管关断不彻底,检查基极/栅极的下拉电阻是否足够小(确保晶体管可靠截止)。
6. 从经典架构到现代思维的延伸
成功让这台基于8086的PWM发生器工作后,回头再看STM32的PWM,你会发现一切豁然开朗。STM32的定时器高级功能,如互补输出、死区插入、刹车功能,其本质思想与我们手动用8253和逻辑组合来实现是相通的,只不过它把这些复杂的逻辑和寄存器操作都集成在了硅片内部,并通过HAL库或LL库提供了简洁的API。
- STM32的PWM模式:对应8253的模式1或模式2,但它的比较寄存器(CCR)可以随时被更新,并且有“预装载”机制,可以在下一个周期生效,避免了我们在更新8253初值时可能出现的“毛刺”周期。
- 中心对齐模式:我们设计的对称PWM是从周期开始计算高电平时间。中心对齐模式则是高电平在周期中间对称分布,这对于某些电机驱动(如三相逆变)可以减少谐波。用8253和逻辑电路也能模拟,但复杂得多。
- 死区时间:在驱动H桥时,为了防止上下管直通,需要在互补的PWM波之间插入一段两者都为低电平的时间。用我们的系统,需要生成两路互补的PWM,并用单稳态触发器或数字延迟芯片来插入死区。而在STM32中,这仅仅是定时器寄存器里的一个数值。
这个项目的价值,不在于做出了一个比STM32开发板更好用的工具,而在于像解剖麻雀一样,亲手搭建并理解了“PWM发生器”这个抽象功能背后每一根信号线的跳动、每一个时钟周期的意义。当你再面对“STM32没有PWM波形输出”、“死区时间怎么设置”、“中心对齐和边沿对齐区别”这些问题时,你脑子里浮现的不再是晦涩的寄存器描述,而是8253的OUT引脚、8086的OUT指令、以及示波器上那个可以被你精确操控的脉冲。这种从底层建立起来的认知,是任何现成库函数都无法给予的。