基于AT89S52的定时器综合实验:高精度可调PWM发生器设计 (频率/占空比双调)
摘要
本文记录了一个基于AT89S52单片机的定时器综合应用项目。针对"PWM频率与占空比均需可调"的实验要求,设计了一套基于 Timer0动态重装载 的核心算法。解决了定点MCU进行除法运算的精度丢失问题,并引入了中断延时软件补偿机制,使输出波形在 100Hz-1100Hz 范围内保持高精度。系统配合 4x4 矩阵键盘与数码管显示,实现了参数的实时调节与监控。
一、 实验要求与功能描述
1. 核心功能 (PWM生成)
- 输出目标:产生一个频率和占空比都能动态调节的 PWM 波形。
- 频率范围:100Hz - 1100Hz (覆盖音频范围)。
- 占空比范围:5% - 95%。
2. 扩展交互 (UI设计)
- 显示:数码管实时显示当前参数。
- 双模式操作 :
- 模式一 (精细模式):设定步进值 (如每次加0.1%),适合精细微调占空比。
- 模式二 (粗调模式):直接调节频率 (步进50Hz) 和占空比 (步进5%)。
二、 设计思路与核心算法 (图文详解)
本系统的程序逻辑分为"主循环"和"中断服务"两条线。主循环负责处理复杂的交互和数学运算,中断服务负责精准的IO翻转。
1. 软件执行流程图
graph TD
subgraph MainLoop [主循环: 交互与计算]
Start((系统上电)) --> Init[初始化: Freq=200Hz, Duty=50%]
Init --> ScanKey[矩阵按键扫描]
ScanKey --无按键--> RefreshDisp[刷新数码管显示]
ScanKey --有按键--> HandleKey[判断模式 & 更新参数]
HandleKey --> Calc[核心算法: 重算重装载值
1. Period = Clock / Freq
2. High = Period * Duty
3. Low = Period - High] Calc --> Comp[软件误差补偿
Reload = 65536 - Count + 12] Comp --> UpdateGlobal[更新全局中断变量] UpdateGlobal --> RefreshDisp RefreshDisp --> ScanKey end subgraph ISR [定时器中断: 波形生成] Int((中断触发)) --> Check{当前相位?} Check --是高电平--> LoadLow[装载低电平时间 TH0/TL0
PWM_OUT 置 0] Check --是低电平--> LoadHigh[装载高电平时间 TH0/TL0
PWM_OUT 置 1] LoadLow --> Ret(中断返回) LoadHigh --> Ret end
1. Period = Clock / Freq
2. High = Period * Duty
3. Low = Period - High] Calc --> Comp[软件误差补偿
Reload = 65536 - Count + 12] Comp --> UpdateGlobal[更新全局中断变量] UpdateGlobal --> RefreshDisp RefreshDisp --> ScanKey end subgraph ISR [定时器中断: 波形生成] Int((中断触发)) --> Check{当前相位?} Check --是高电平--> LoadLow[装载低电平时间 TH0/TL0
PWM_OUT 置 0] Check --是低电平--> LoadHigh[装载高电平时间 TH0/TL0
PWM_OUT 置 1] LoadLow --> Ret(中断返回) LoadHigh --> Ret end
2. 核心难点一:频率如何可调?
普通PWM通常利用自动重装载定时器产生固定频率。但本实验要求频率变化,这意味着定时器的溢出时间必须动态计算。
- 公式推导: 已知晶振 ,机器周期 。
例如:200Hz对应周期计数 4608 个 Tick。
3. 核心难点二:软件补偿机制
在实际测试中发现,当频率超过 1kHz 时,输出频率会偏低。这是因为中断响应和指令执行消耗了时间。
- 解决方案 :引入
COMP_TICKS(实测约12个机器周期)。
通过预先扣除指令消耗的时间,让定时器"提前"溢出,完美抵消了硬件延迟。
三、 硬件端口定义
| 变量/宏 | 对应IO口 | 功能说明 |
|---|---|---|
| PWM_OUT | P1.0 | PWM信号输出端 (接示波器或LED) |
| BUZZ | P2.3 | 有源蜂鸣器 (操作提示音) |
| dula/wela | P2.6/P2.7 | 数码管段选/位选控制 (573锁存器) |
| KEY_PORT | P3 | 4x4 矩阵按键接口 |
| Timer0 | 内部资源 | Mode 1 (16位定时器) |
四、 完整源代码 (单文件版)
为了方便大家直接编译运行,我将所有模块整合在了一个 main.c 文件中。
c
/******************************************************************
* Project: AT89S52 High Precision PWM Generator
* Author: 名字太难起了QAQ
* MCU: AT89S52 @ 11.0592 MHz
* Logic: Timer0 Interrupt + Software Compensation
******************************************************************/
#include <reg52.h>
/* 类型定义 */
#define uchar unsigned char
#define uint unsigned int
#define ulong unsigned long
/* --- 硬件引脚定义 --- */
sbit PWM_OUT = P1^0; // PWM波形输出
sbit BUZZ = P2^3; // 蜂鸣器
sbit dula = P2^6; // 数码管段选
sbit wela = P2^7; // 数码管位选
#define KEY_PORT P3 // 矩阵按键端口
/* --- 蜂鸣器控制宏 --- */
#define BEEP_ON() do{ BUZZ = 0; }while(0)
#define BEEP_OFF() do{ BUZZ = 1; }while(0)
/* --- 系统参数限制 --- */
#define FREQ_MIN 100u
#define FREQ_MAX 1100u
#define DUTY_MIN_X10 50u // 5.0%
#define DUTY_MAX_X10 950u // 95.0%
/* 定时器参数: Fosc/12 */
#define FTIMER_HZ 921600UL
/* 中断延时补偿 (指令周期数) */
#define COMP_TICKS 12u
/* --- 全局变量 --- */
uchar code wei_tab[] = {0xfe,0xfd,0xfb,0xf7,0xef,0xdf};
uchar code seg_tab[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x00};
uchar disp_buf[6] = {10,10,10,10,10,10}; // 显存
/* 模式标记: 0=精细模式, 1=粗调模式 */
bit current_mode = 0;
/* PWM 核心变量 (volatile防止编译器优化) */
volatile uint pwm_freq = 200; // 频率
volatile uint duty_x10 = 500; // 占空比 (单位0.1%)
volatile uint period_counts = 0; // 周期计数值
volatile uint on_counts_comp = 0; // 高电平重装值 (已补偿)
volatile uint off_counts_comp = 0; // 低电平重装值 (已补偿)
volatile uchar on_TH=0, on_TL=0;
volatile uchar off_TH=0, off_TL=0;
volatile bit phase_high = 1; // 相位标记
/* 模式1专用变量 */
uint step1_x10 = 50; // 默认步进 5.0%
/* ================= 辅助函数 ================= */
void delay_ms(uint ms){
uint i,j; for(i=0;i<ms;i++) for(j=0;j<110;j++);
}
/* ================= 核心算法: 计算定时器重装值 ================= */
void PWM_Recalc(void)
{
ulong tmp;
uint on_counts, off_counts;
uint reload;
/* 1. 频率 -> 总计数值 */
// Counts = 921600 / Freq
tmp = (FTIMER_HZ + (ulong)pwm_freq/2u) / (ulong)pwm_freq;
period_counts = (uint)tmp;
/* 2. 占空比 -> 高/低电平计数值 */
// On = Counts * Duty / 1000
tmp = (ulong)period_counts * (ulong)duty_x10 + 500ul;
tmp /= 1000ul;
on_counts = (uint)tmp;
off_counts = period_counts - on_counts;
/* 3. 软件补偿 & 计算重装值 */
// 实际写入值 = 65536 - (目标值 - 补偿值)
// 防止下溢:如果计数值太小,就不补偿了
if(on_counts > COMP_TICKS) on_counts -= COMP_TICKS;
if(off_counts > COMP_TICKS) off_counts -= COMP_TICKS;
reload = 65536u - on_counts;
on_TH = (uchar)(reload >> 8);
on_TL = (uchar)(reload & 0xFF);
reload = 65536u - off_counts;
off_TH = (uchar)(reload >> 8);
off_TL = (uchar)(reload & 0xFF);
}
/* ================= 硬件驱动 ================= */
void Timer0_Init(void)
{
TMOD &= 0xF0; TMOD |= 0x01; // Mode 1
ET0 = 1; EA = 1; TR0 = 1;
}
/* 定时器中断: PWM波形发生器 */
void timer0_isr(void) interrupt 1
{
if(phase_high) {
/* 高电平结束,装载低电平时间 */
TH0 = off_TH; TL0 = off_TL;
PWM_OUT = 0; BEEP_OFF();
phase_high = 0;
} else {
/* 低电平结束,装载高电平时间 */
TH0 = on_TH; TL0 = on_TL;
PWM_OUT = 1; BEEP_ON();
phase_high = 1;
}
}
/* 矩阵键盘扫描 */
uchar Key_Scan(void)
{
uchar c, r, key = 0xFF;
for(c=0; c<4; c++) {
// 逐列扫描
if(c==0) KEY_PORT=0xfe; else if(c==1) KEY_PORT=0xfd;
else if(c==2) KEY_PORT=0xfb; else KEY_PORT=0xf7;
r = KEY_PORT & 0xF0;
if(r != 0xF0) {
delay_ms(10); // 消抖
if((KEY_PORT & 0xF0) != 0xF0) {
if(r==0xE0) key=0+c; else if(r==0xD0) key=4+c;
else if(r==0xB0) key=8+c; else if(r==0x70) key=12+c;
while((KEY_PORT & 0xF0) != 0xF0); // 等待松手
return key + 1; // 返回 1-16
}
}
}
return 0;
}
/* ================= 业务逻辑 ================= */
void Update_Display_Buffer(void)
{
uint val1, val2;
// 模式1显示: 占空比.步进 | 模式2显示: 频率.占空比
if(current_mode == 0) { val1 = duty_x10; val2 = step1_x10; }
else { val1 = pwm_freq; val2 = duty_x10/10; }
if(current_mode == 0) { // HHH.SSS
disp_buf[0]=val1/100; disp_buf[1]=(val1/10)%10; disp_buf[2]=val1%10;
disp_buf[3]=val2/100; disp_buf[4]=(val2/10)%10; disp_buf[5]=val2%10;
if(disp_buf[0]==0) disp_buf[0]=10; // 消零
if(disp_buf[3]==0) disp_buf[3]=10;
} else { // FFFF.DD
disp_buf[0]=val1/1000; disp_buf[1]=(val1/100)%10;
disp_buf[2]=(val1/10)%10; disp_buf[3]=val1%10;
disp_buf[4]=val2/10; disp_buf[5]=val2%10;
if(disp_buf[0]==0) disp_buf[0]=10;
}
}
void Display_Scan(void)
{
uchar i, seg;
for(i=0; i<6; i++) {
P0 = wei_tab[i]; wela=1; wela=0;
seg = (disp_buf[i]<11)? seg_tab[disp_buf[i]] : 0x00;
// 模式1时加小数点区分
if(current_mode==0 && (i==1 || i==4)) seg |= 0x80;
P0 = seg; dula=1; dula=0;
delay_ms(1); P0=0; dula=1; dula=0; // 消隐
}
}
void Handle_Key(uchar key)
{
bit update = 0;
if(key == 16) { // 模式切换
current_mode = !current_mode;
update = 1;
}
else if(key == 7) { // 关输出
PWM_OUT = 0; EA = 0; // 简单粗暴关中断
}
else if(current_mode == 0) { // --- 模式1: 微调 ---
// S1-S4 调步进
if(key==1) step1_x10 += 50;
if(key==2 && step1_x10>50) step1_x10 -= 50;
if(key==3) step1_x10 += 1;
if(key==4 && step1_x10>1) step1_x10 -= 1;
if(step1_x10 > 100) step1_x10 = 100;
// S5-S6 调占空比
if(key==5) { duty_x10 += step1_x10; if(duty_x10 > 950) duty_x10 = 950; update=1; }
if(key==6) { if(duty_x10 > 50 + step1_x10) duty_x10 -= step1_x10; else duty_x10=50; update=1; }
}
else { // --- 模式2: 粗调 ---
// S1/S5/S9/S13 调频率
if(key==1 && pwm_freq+100 <= 1100) { pwm_freq+=100; update=1; }
if(key==5 && pwm_freq > 200) { pwm_freq-=100; update=1; }
if(key==9 && pwm_freq+50 <= 1100) { pwm_freq+=50; update=1; }
if(key==13 && pwm_freq > 150) { pwm_freq-=50; update=1; }
// S2/S6 调占空比
if(key==2 && duty_x10+50 <= 950) { duty_x10+=50; update=1; }
if(key==6 && duty_x10 >= 100) { duty_x10-=50; update=1; }
}
if(update) {
EA = 0; PWM_Recalc(); EA = 1; // 重新计算参数
}
}
void main(void)
{
EA = 0; PWM_Recalc(); EA = 1; // 初始计算
Timer0_Init();
while(1) {
uchar key = Key_Scan();
if(key) Handle_Key(key);
Update_Display_Buffer();
Display_Scan();
}
}
五、 易错点总结
- 关于
volatile关键字 : 在代码中,pwm_freq等变量在main函数中修改,同时影响中断服务的计算。必须加上volatile修饰符,防止编译器过度优化导致逻辑错误。 - 原子操作 (Atomic Operation) : 在
main函数中调用PWM_Recalc更新参数时,必须先关闭中断 (EA=0) 。否则,如果计算到一半进入了中断,TH0和TL0可能会装入不匹配的数据(例如高字节是新算的,低字节还是旧的),导致PWM波形瞬间错乱。 - 除法运算的代价 : 51单片机是8位机,进行
ulong(32位) 除法非常耗时。本实验中PWM_Recalc函数执行时间较长,调节按键时可能会感觉到数码管轻微闪烁,这是正常现象。若要追求极致体验,可将计算逻辑分散处理或使用查表法。