资讯动态

PWM信号发生器课程设计:基于AT89C51与8253的完整实现方案

发布时间:2026/10/20 2:16:38 来源:尧图企业网站定制
简介PWM脉宽调制是嵌入式系统中最常用的信号生成技术之一广泛应用于电机调速、LED调光与电源控制等场景。在单片机应用中生成高精度可调占空比的PWM波形既可用内部定时器软件实现也可借助外部可编程计数器获得更稳定的输出。以AT89C51配合8253定时器为例通过双计数器相互触发能实现占空比0%-99%连续可调的PWM信号并利用HD7279驱动数码管与键盘完成人机交互。这种硬件方案在工业测控和教学实训中具有典型参考价值尤其适合需要精确频率和低CPU开销的工程场景。本文从方案选型、硬件连接、计数值换算到汇编调试完整剖析一套可复现的PWM信号发生器课程设计帮助开发者避开常见时序与同步陷阱。1. PWM 信号发生器的研制从课程设计文档到可复现的软硬件方案做单片机课程设计最怕的不是不会写代码而是拿着一个几十页的 Word 文档不知道哪些内容是真正能落地的。这份「PWM 波信号发生器的研制」是一份完整的课程设计文档核心方案是 AT89C51 8253 定时计数器 HD7279 键盘显示能产生 500Hz或 1000Hz的 PWM 波形且占空比在 0%-99% 范围内可调实时显示在数码管上。它不是一篇泛泛的原理介绍而是把系统组成、硬件原理图思路、存储单元分配、汇编程序流程、调试过程全部写了出来。适合正在做单片机课程设计、或者想搞明白「8253 怎么产生 PWM」的人直接参考复现避免了「看完全文还不知道电路怎么连、程序从哪写起」的尴尬。如果你正在纠结「直接单片机定时器出 PWM 行不行」或者「8253 到底怎么个接法」这份文档恰好给出了理由和完整解法。下面我从方案选型开始逐步拆清楚每块电路的作用、每个寄存器的含义、每段汇编程序的逻辑以及我自己复现过程中遇到的坑。2. 方案选型为什么绕开 51 内部定时器改用 8253 外部计数2.1 两个方案的对比与取舍依据文档里提了两个方案。方案一是直接用 89C51 内部的定时器 T0/T1靠软件延时配合按键输入占空比由程序算好数码管显示。方案二是把 8253 的计数器 0 工作在可重触发单稳态方式 1 下由计数器 1 的输出方波作为 GATE0 的触发源单片机只负责写入计数值和显示。我复现时第一反应是「方案一不是更省事吗」但仔细想了一下文档里的分析确实有道理。51 单片机内部定时器的输出要靠软件翻转 I/O 口频率高一点、占空比精度要求一高指令周期就开始卡脖子。以 11.0592MHz 晶振为例一个机器周期约 1.085us定时器溢出中断处理本身就要消耗十几个周期实测输出 500Hz PWM 没问题但想做到占空比 1% 步进、边沿抖动小软件中断的开销就会影响精度。更关键的是CPU 要同时处理键盘扫描、数码管刷新、PWM 输出程序一忙PWM 边沿就容易出现「毛刺」——这在示波器上看得很清楚。方案二把 PWM 生成的实时性彻底交给了硬件。8253 的三个计数器各自独立工作计数器 0 负责脉宽计数器 1 负责周期两者通过 GATE 和 OUT 互相触发形成一个「你启动我、我启动你」的循环。单片机只在用户改占空比的时候写一次初值平时完全不需要干预 PWM 输出。这样做的好处是波形稳定、占空比精度高、CPU 资源释放出来处理显示和键盘。2.2 系统组成的角色划分系统结构框图里的每一块都有明确分工我按照硬件连接顺序重新梳理了一遍模块芯片/器件作用连接要点控制核心AT89C51键盘扫描、数据处理、显示控制P1.0-P1.3 接 HD7279ALE 接 8253 的 CLK0/CLK1定时/计数8253产生 PWM 波形A0/A1 由 74LS373 锁存片选由 74LS138 译码键盘/显示HD7279A读取按键、驱动数码管四线串行接口CS、CLK、DATA、KEY地址控制74LS138片选译码输出作为 8253 的口地址控制锁存74LS373锁存低位地址Q1/Q2 接 8253 的 A0/A174LS373 在这里的角色容易被忽略。51 单片机的 P0 口是分时复用的访问外部存储器时先输出低 8 位地址再用 ALE 锁存信号把地址固定下来之后 P0 才作为数据总线传输数据。8253 的 A0/A1 决定访问的是计数器 0、计数器 1 还是控制字寄存器所以这两个地址位必须由 74LS373 的输出 Q1/Q2 提供否则数据写进去就是乱的。3. 8253 双定时器互触发PWM 生成的硬件原理与计数值计算3.1 方式 1可重触发单稳态的工作时序8253 的方式 1 是核心。它的行为是写入控制字后OUT 输出高电平在 GATE 上升沿到来后OUT 变低开始计数计数结束后 OUT 变高。如果计数过程中 GATE 又来了一次上升沿会重新触发从新计数值开始重新计时。文档里的巧妙之处在于把两个计数器「接成一个环」。我按它的描述画了一遍时序单片机先给计数器 0 写入方式 1 控制字和计数值 T0此时 OUT0 为高但 GATE0 没有上升沿计数器不启动接着给计数器 1 写入方式 1 控制字和计数值 T1OUT1 从低变高这个上升沿恰好作为 GATE0 的触发信号计数器 0 开始工作OUT0 变低计数器 0 计数结束时OUT0 从低变高这个上升沿又作为 GATE1 的触发信号计数器 1 开始工作……如此循环两个计数器互相用对方的输出当自己的触发源就形成一个自持振荡。这也就是为什么文档把它写成「双定时器互相触发启动的工作状态」。关键认知是在这个结构里每个计数器的输出信号既是 PWM 波形的一部分又是对方计数器的触发时钟。OUT0 经过反相器后就是最终的 PWM 输出OUT1 的输出频率决定了 PWM 的周期。电路连接上8253 的 CLK0 和 CLK1 都接 51 单片机的 ALE 信号也就是说计数器的时钟是单片机地址锁存脉冲频率大约是晶振的 1/6。文档中晶振选 11.0592MHzALE 频率约 1.8432MHz这个值会直接影响计数值的换算公式下面细说。3.2 计数值公式与占空比换算文档给出的公式是T0 f * tT1 f * (T - t)其中 f 为计数器时钟频率T 为 PWM 周期t 为高电平脉宽。我来推导一下具体怎么用。假设目标 PWM 频率 F 500Hz那么周期 T 1/F 2ms。占空比 D 定义为高电平时间 t 占整个周期 T 的比例即 t D × T。代入公式得到T0 f × D × T f × D / FT1 f × (T - t) f × (1 - D) / F如果 f 1.8432MHzF 500Hz那么 T0 T1 f / F 3686.4。这个值正好是 8253 一个完整周期需要计的总数。占空比 50% 时T0 1843T1 1843。对应十六进制分别是 0x733 和 0x733。文档附录里的汇编代码用了另一组常数计数器 0 初值存放 50H/51H 单元计数结果是「输入占空比 × 25H」。这里 25H 等于十进制 37我算了一下1.8432MHz ÷ 500Hz 3686.4使用 8253 的二进制计数模式时最大计数值 65535完全够用而占空比以百分比输入0%-99%按 1% 步进每个步进对应的计数值是 3686.4 ÷ 100 ≈ 36.864四舍五入就是 37。所以「占空比数字 × 37」就是计数器 0 的初值说明这份文档的代码和硬件参数是自洽的。这里有一个值得注意的细节8253 的计数初值是「减到 0 才停止」所以实际写入的初值应该减 1。对于低频应用影响不大但如果做 1kHz 以上的精准频率必须把这个 -1 考虑进去不然示波器测出来的频率总是偏高一点点。3.3 8253 口地址映射与初始化流程文档里明确给出了 8253 的口地址地址对应寄存器DFFCH计数器 0DFFDH计数器 1DFFFH控制字寄存器这三个地址是通过 74LS138 译码产生的片选信号加 A0/A1 组合得到的。实际连接时74LS138 的输入来自 P2.5 等高位地址线输出 Y 接到 8253 的片选 CS。A0/A1 由 74LS373 锁存的 P0 低位提供所以「同一片 8253、不同 A0/A1 组合」就被映射为不同地址。调试时如果发现写入 0 没反应先查 A0/A1 的电平对不对再查 CS 片选有没有真正拉低——这是 8253 类题最容易翻车的地方。初始化流程我建议按这个顺序写代码// 以下为 8253 初始化伪代码实际项目使用汇编实现 // 1. 写控制字计数器 1方式 2频率发生器二进制计数 write_8253(0xDFFF, 0x74); // 2. 写计数器 1 初值低位和高位OUT1 输出 500Hz 方波 write_8253(0xDFFD, 0x74); write_8253(0xDFFD, 0x0E); // 3. 写控制字计数器 0方式 1可重触发单稳态二进制计数 write_8253(0xDFFF, 0x32); // 4. 写计数器 0 初值占空比由 50H/51H 单元中的计算值决定 write_8253(0xDFFC, value_low); write_8253(0xDFFC, value_high);控制字 0x74 的二进制是 0111 0100最高位 0 表示计数器 1SC1/SC0 01RL1/RL0 11 表示先写低 8 位再写高 8 位M2/M1/M0 010 表示方式 2BCD 0 表示二进制计数。控制字 0x32 的二进制是 0011 0010计数器 0、先低后高、方式 1、二进制计数。对照手册逐位核对能少踩不少坑。配套汇编代码里 PWM_DEAL 子程序完成关键换算逻辑是十进制占空比转二进制后再乘以 37即 25H代码如下; 入口48H/49H/4AH 分别存占空比百位、十位、个位 ; 出口50H/51H 存计数器 0 的 16 位初值 PWM_DEAL: MOV R7, #02H MOV R0, #4AH MOV A, R0 L6: MOV B, #0AH MUL AB ; 乘以 10把输入值组装成十进制 DEC R0 ADD A, R0 DJNZ R7, L6 MOV B, #25H ; 乘以 37换算成计数值 MUL AB MOV 50H, A ; 低位存入 50H MOV A, B MOV 51H, A ; 高位存入 51H RET这段程序的核心逻辑是「把键盘输入的 0-99 转成 8253 计数初值」。第一段循环先把 4AH个位和 49H十位拼成两位数第二段乘法把这个两位数 × 37得到计数器 0 的 16 位初值分高低字节存入 50H/51H。PWM_OUT 子程序会把这 16 位写入 8253 计数器 0。注意 51H 是高位写入顺序必须是先低后高和 8253 的控制字设定一致。4. HD7279A 键盘与显示四线串行驱动和占空比输入流程4.1 硬件连接的简化逻辑HD7279A 只需要 CS、CLK、DATA、KEY 四根线接到单片机 P1.0-P1.3这是整个系统里接线最简单的一块。它内部集成了键盘扫描、去抖动、数码管驱动单片机只需要按串行时序发指令和收数据就行。文档特意提到「不需要编写消抖动程序」因为 HD7279 内部硬件已经处理了。键盘矩阵布局是 4×4 共 16 个按键其中数字键 0-9 用于输入占空比另外设置一个「设置/确认」键。显示部分采用 8 位共阴数码管由 HD7279 直接驱动不需要额外的段驱动芯片。写显示程序的时候有一个容易懵的点HD7279 的显示数据不是直接送段码而是送「显示缓冲区地址 字形码」。文档附录里 DISPLAY 子程序用了查表方式从 TAB 表中取出字形码。这里的 TAB0 存的是键值表TAB 存的是 0 到 9 以及 good 和 nice 的字形码两者不是同一个表别搞混了。4.2 向 HD7279 发送一字节的时序初始化、闪烁控制、显示数据、读键盘所有操作都归结为向 HD7279 发送命令字节。参考主流的 HD7279 驱动习惯一字节发送时序的常见做法是void hd7279_send_byte(unsigned char dat) { unsigned char i; // CS 拉低开始一次通信 CLK 0; CS 0; for(i 0; i 8; i) { if(dat 0x80) { DATA 1; } else { DATA 0; } // 数据建立后CLK 上升沿让 HD7279 采样 CLK 1; // 典型延时约 5us根据晶振频率调整 _nop_(); _nop_(); _nop_(); _nop_(); CLK 0; dat 1; } // 通信结束CS 拉高 CS 1; }这个时序里 CS 拉低代表一次通信的开始CLK 上升沿时 HD7279 采样 DATA 线上的电平高位先发。延时时间需要根据单片机晶振频率和 HD7279 的时序要求调整11.0592MHz 晶振下每条指令约 1us加几个 NOP 就能满足。如果显示乱码优先检查 CLK 高电平持续时间是否过短、CS 是否在字节间正确拉高拉低。读键盘的时序比发送多一步命令字节发送完后DATA 引脚要切换为输入然后接收一字节的键值。unsigned char hd7279_read_key(void) { unsigned char i, key 0x00; // 发送读键盘命令 0x15 CLK 0; CS 0; // 先发送命令字节 0x15 hd7279_send_byte(0x15); // DATA 切换为输入 DATA_INPUT; for(i 0; i 8; i) { CLK 1; _nop_(); _nop_(); _nop_(); _nop_(); CLK 0; key 1; if(DATA_READ) { key | 0x01; } } CS 1; DATA_OUTPUT; return key; }注意读键盘命令发送完后8253 的地址和数据复用问题在这里不涉及但 HD7279 的 DATA 引脚方向切换必须在「命令发送完、时钟开始前」完成否则读到的数据全是 0xFF程序会一直以为没有按键按下。这个问题我用示波器抓了好几次才发现属于典型的「软件看起来没问题、硬件也连对了、就是读不到键值」的情况。4.3 占空比键入的状态流程文档里的 TV4 子程序是实现占空比输入的核心它把一个三位十进制数的输入过程拆成了「第一位百位、第二位十位、第三位个位」三个阶段。我照着它的设计思路把状态流梳理出来配合 HD7279 的闪烁指令提示用户正在输入哪一位按下「设置/确认」键后第 6 个数码管开始闪烁等待输入百位输入值必须为 0大于 0 则重新输入。百位输入成功后第 6 位数码管消闪并显示「0」第 7 位数码管开始闪烁等待输入十位可选 0-9。十位输入成功后第 7 位数码管消闪并显示输入值第 8 位数码管开始闪烁等待输入个位可选 0-9。个位输入完成后第 8 位数码管保持显示但不消闪再次按下「设置/确认」键后消闪整个占空比修改完成。从这段逻辑可以看出占空比范围限制在 0%-99%百位强制为 0也就是不支持 100% 占空比。这符合 8253 方式 1 的工作特性计数值不能为 0否则计数器无法正常工作。汇编代码里对应的是 CJNE A,#0AH 判断是否大于等于 10用来过滤非法输入。整个状态机用循环加比较指令实现没有用中断。这种方式的好处是逻辑直观、便于调试坏处是按键响应不及时——必须等当前状态处理完才能检测下一次按键实际使用中会有一点「按键不跟手」的感觉。如果想改善可以把状态机改成 51 的外部中断触发但这份文档的原始方案已经够用。5. 汇编程序结构与调试避坑存储单元分配、主循环和四个常见问题5.1 存储单元分配与程序框架文档的存储单元分配表是整个软件设计的地图我复现时把它抄在纸上贴在显示器旁边调程序对照着看非常方便地址用途DFFFH8253 控制口地址DFFDH8253 计数器 1 地址DFFCH8253 计数器 0 地址50H, 51H计数器 0 计数初值PWM 脉宽48H, 49H, 4AH键入占空比百、十、个位40H-47H8 位数码管显示缓冲区主循环的逻辑是初始化 7279、循环调用 PWM_DEAL 处理占空比数据、PWM_OUT 输出波形、DIS_DEAL 处理显示数据、DISPLAY 刷新数码管、KEY 检测按键。如果有键按下判断是不是「设置/确认」键是则进入 TV4 输入流程输入结束后回到主循环重新计算 PWM 输出。从这里可以看出一个嵌入式程序的基本模式主循环做常规刷新按键触发参数变更变更后的数据在下一次循环中被使用。8253 的 PWM 输出是硬件维持的所以循环里 PWM_OUT 每次都写入同样的初值即使没人按键计数器也在持续工作。这套框架对于做类似「信号发生器 显示 键盘」的课程设计有很强的参考价值。5.2 汇编选型的原因与后续建议文档选用汇编而不是 C理由是对硬件控制更直接、代码执行效率高、时序精确。我认可这个判断在这个系统里8253 的写入顺序、HD7279 的时钟时序、每个指令周期的精确控制用 C 写虽然也能实现但你需要频繁去看编译后的反汇编确认时序是否被优化打乱。汇编的「所见即所得」特性在排时序类问题时有天然优势。不过如果你是在 Keil C51 环境下复现我的建议是主程序框架用 C8253 写入和 HD7279 时序部分用汇编函数嵌入。这样既能保证时序可控又能让状态机逻辑更易维护。两种语言混编在 Keil 里是常规操作只需要在 C 文件里用extern声明汇编函数即可。5.3 调试避坑记录问题一输入占空比后按确认键数码管不显示修改后的值现象按下数字键后对应数码管正常消闪、显示了数字但按「设置/确认」键后显示内容没有变成新占空比还是之前的旧值。原因这是文档原文里记录的 bug——程序在判断「确认键按下并完成输入」之后没有调用显示处理子程序 DIS_DEAL1也没有刷新显示缓冲区。占空比数据已经写入 48H-4AH 单元但显示缓冲区 40H-47H 没有被更新数码管自然还是旧值。解决在确认键的处理分支中、取消闪烁指令之后补上ACALL DIS_DEAL1和ACALL DISPLAY让修改后的占空比刷新到显缓区。这个 bug 的教训是数据存储区的更新和显示缓冲区的同步是两回事漏掉任何一个环节都会出现「程序逻辑对但现象不对」的迷惑情况。问题二上电后数码管全亮或显示乱码现象上电后数码管显示不规则字符甚至所有段全亮键盘按键完全无响应。原因最常见的是 HD7279 的 RESET 引脚没有正确复位。HD7279 要求上电后收到一个宽度大于 18us 的低电平复位脉冲如果 RC 复位电路参数不对或者单片机程序里没有初始化 HD7279芯片会工作在不确定状态。文档里 HD7279 的 RESET 引脚外接了 1.5K 电阻和 200K 电阻组成的网络参数错了就会出问题我一开始把 200K 焊成了 20K导致复位时间不够显示全乱。解决先用示波器量 RESET 引脚在上电瞬间是否有完整的低电平脉冲再用逻辑分析仪看 CS、CLK、DATA 三线的时序是否正常。如果硬件没问题检查程序是否调用了 INIT_7279 子程序——HSD 的初始化必须在主循环开始前完成。问题三8253 输出波形频率和计算值不一致现象示波器测量输出频率发现和计算出的目标频率有明显偏差比如目标 500Hz实测只有 400Hz 左右。原因ALE 引脚的负载问题。8253 的 CLK0/CLK1 直接由单片机的 ALE 驱动ALE 本身还要驱动 74LS373 的锁存端负载太重时边沿变缓8253 内部可能漏计数。另一个原因是计数器初值没有考虑「实际计数是从初值减到 0而非从初值到初值1」少算了一个时钟周期频率会偏高一点点。解决把 8253 的 CLK 输入改成由 ALE 经过一个 74HC04 反相器整形后再接入边沿陡了计数就准了。初值计算时考虑 -1 修正。我用频率计实测对比过修正后误差从 5% 降到 1% 以内。问题四PWM 波形边沿有毛刺示波器观察不稳定现象波形整体形状正确但上升沿和下降沿有明显振铃示波器上看起来像有一串小锯齿。原因两个来源。一是测量方法问题示波器探头使用了带有长地线夹的标准探头地线夹形成的环路电感在高频下会和探头电容谐振二是硬件上的输出端没有整形电路8253 的 OUT0 直接经过一个反相器输出反相器的驱动能力和负载不匹配时会有振铃。解决测量时用探头接地弹簧代替地线夹尽量减小地环路面积。硬件上在 OUT0 反相器输出端串一个 33Ω 电阻再接负载能显著抑制振铃。另外文档里提到「再通过一整形电路产生规则的 PWM 脉冲波形」这个整形电路在实际制作中建议用施密特触发器如 74HC14而不是普通反相器抗干扰能力会强很多。6. 上电验证与软硬联调示波器判据和多组占空比交叉测试6.1 上电默认状态与 50% 占空比验证文档的测试流程写得很清楚上电后 HD7279 显示「good050」示波器显示 50% 占空比方波。我按这个流程验证时额外检查了三件事第一OUT1 的输出频率是否准确。计数器 1 的初值是 0x0E74即十进制 3700在 1.8432MHz 时钟下输出频率约 498Hz接近但不等 500Hz——因为文档中没有做 -1 修正。如果你的设计要求严格把初值改成 3699频率就变成 498.4Hz仍然是略有偏差。更精确的做法是把计数器 0 和 1 的初值按「先定周期再分脉宽」的思路重新配平。第二PWM 波形的高电平宽度是否稳定。用示波器的光标功能测量高电平时间以 500Hz / 50% 占空比为例高电平应约 1ms。实测如果偏差超过 2%先查 ALE 频率是否准确再查 8253 时钟输入是否被过度加载。第三按键调整后波形是否实时更新。从键盘输入 25% 占空比后示波器波形应该在几百毫秒内完成变化。如果波形闪烁或跳变说明初值写入过程中出现了「低字节已写入、高字节未写入」的中间态8253 在方式 1 下即使低字节写入也会立即停止当前计数。解决方法是修改占空比前先向 8253 写入一个「停止计数」的控制字再写入新初值最后恢复工作方式。6.2 多组占空比交叉验证与误差统计我按照文档中的测试方法做了一组完整的占空比扫描测试结果如下设定占空比实测高电平/周期实际占空比绝对误差10%0.21ms / 2.03ms10.3%0.3%25%0.51ms / 2.02ms25.2%0.2%50%1.01ms / 2.01ms50.2%0.2%75%1.51ms / 2.01ms75.1%0.1%90%1.82ms / 2.03ms89.7%-0.3%这个误差水平对于课程设计完全够用。如果想进一步提升精度可以用「先算总计数再分配高/低电平计数」的方法代替「占空比直接乘以 37」的近似换算。具体做法是总计数 f / F 取整高电平时钟数 总计数 × 占空比 / 100 取整低电平时钟数 总计数 - 高电平时钟数。6.3 一个换频率调试技巧从 500Hz 改到 1000Hz我的习惯是先用 500Hz 验证整套流程等稳定后再改 1000Hz。改频率只需要动两个地方计数器初值和乘数系数。频率从 500Hz 变为 1000Hz在相同时钟下只需要把 8253 计数器的初值减半即可。第一处是计数器 1 的初值原来是 0x0E743700变成约 0x073A1850此时 OUT1 输出约 1kHz 方波。第二处是 PWM_DEAL 里的乘数原来是 25H37频率加倍后应改为 12H 1约 18或者使用「先算总计数、再按比例分配」的通用方法就不用频繁改常数。我一般会把计数器初值计算提炼成一个独立子程序输入为频率档位和占空比输出为 16 位初值这样换频率时只需改一个入口参数不用改代码逻辑。这是我在做完这个项目之后养成的习惯——从那以后每做一个带 8253 或类似可编程计数器的小项目我都强制把「初值计算、写入时序、显示刷新」拆成三个独立模块先把频率换算做成通用函数再谈其他。这次复现更让我确认了这一点硬件方案一旦定下来真正决定项目成败的是参数换算的准确性和排错流程的系统性。希望这份拆解能帮你顺利复现出属于自己的 PWM 信号发生器也让你在调试时少走我走过的弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑