基于AT89S52的定时器综合实验:高精度可调PWM发生器设计 (频率/占空比双调)

基于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

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();
    }
}

五、 易错点总结

  1. 关于 volatile 关键字 : 在代码中,pwm_freq 等变量在 main 函数中修改,同时影响中断服务的计算。必须加上 volatile 修饰符,防止编译器过度优化导致逻辑错误。
  2. 原子操作 (Atomic Operation) : 在 main 函数中调用 PWM_Recalc 更新参数时,必须先关闭中断 (EA=0) 。否则,如果计算到一半进入了中断,TH0TL0 可能会装入不匹配的数据(例如高字节是新算的,低字节还是旧的),导致PWM波形瞬间错乱。
  3. 除法运算的代价 : 51单片机是8位机,进行 ulong (32位) 除法非常耗时。本实验中 PWM_Recalc 函数执行时间较长,调节按键时可能会感觉到数码管轻微闪烁,这是正常现象。若要追求极致体验,可将计算逻辑分散处理或使用查表法。
相关推荐
冬奇Lab5 小时前
一天一个开源项目(第25篇):Clawra - 为 OpenClaw 赋予「自拍」能力的 Skill
人工智能·开源·资讯
说私域7 小时前
流量思维向长效思维转型:开源链动2+1模式AI智能名片小程序赋能私域电商品牌建设
人工智能·小程序·开源·产品运营·私域运营
无巧不成书02187 小时前
【RN鸿蒙教学|第11课时】应用打包与部署实战:完成从开发到落地的全闭环
react native·华为·开源·交互·harmonyos
caol6419 小时前
「文颜」家族迎来大幅更新
开源
无巧不成书02181 天前
【RN鸿蒙教学|第7课时】表单开发实战:TextInput输入+表单验证+鸿蒙多终端适配
react native·华为·开源·交互·harmonyos
紫微AI1 天前
OpenClaw:从周末实验到现象级开源 AI 代理
人工智能·开源
冬奇Lab1 天前
一天一个开源项目(第24篇):OpenClawInstaller - 一键部署私人 AI 助手 OpenClaw
人工智能·开源·资讯
正宗咸豆花1 天前
开源大模型涨价策略分析:Llama 3.5 与 GLM-5 的商业化博弈
开源·llama
名字太难起了QAQ1 天前
基于AT89S52单片机的“流水灯与蜂鸣器配合”实验
开源