目录
[4. 硬件设计](#4. 硬件设计)
[5. 软件设计](#5. 软件设计)
[5.1 pwm.c 与 pwm.h](#5.1 pwm.c 与 pwm.h)
[5.2 main.c](#5.2 main.c)
[5.3 public.c 与 public.h](#5.3 public.c 与 public.h)
要完成的实验:DAC(PWM)模块上的指示灯 DA1 呈呼吸灯效果,由暗变亮再由亮变暗
4. 硬件设计
由下图可以看出,PWM 输出控制管脚连接单片机 P2.1 管脚,DAC1 为 PWM 输出信号
该电路属于 DAC/PWM 系统的 "信号调理环节 ",弥补了数模转换输出信号驱动能力、幅值的不足,是数字转模拟后信号实用化的关键一步,核心是LM358 构成的同相放大电路,主打模拟信号的放大、滤波和驱动;
元件分工明确,运放负责放大,电容负责滤波,电阻负责限流 / 调增益,LED 负责状态指示;
① DAC 输出的模拟信号驱动能力弱,PWM 滤波后的信号幅值可能不足,该电路通过运放放大、滤波后,能提供更稳定、驱动能力更强的模拟信号,满足后续设备(如传感器、执行器)的使用要求;
② P21 输入信号,来源大概率是两类
DAC 直接输出的模拟电压(需放大提升驱动能力);
PWM 信号经 RC 滤波后的等效模拟电压(需放大调理);
③ DA1 可直观判断 DAC/PWM 转换后的模拟信号是否正常输出,是调试、故障排查的便捷手段。

| 元件类别 | 具体元件 | 核心作用 |
|---|---|---|
| 核心放大器件 | U15A(LM358) | 双运放芯片的其中一路,构成同相放大电路,实现模拟信号的放大与驱动 |
| 输入 / 反馈电阻 | R31(51K) | 同相输入端限流 / 匹配电阻,配合反馈电阻决定放大倍数 |
| R33(4.7K) | 负反馈电阻,调整电路放大倍数,稳定运放工作状态 | |
| 滤波元件 | C29(104)、C32(104) | 陶瓷电容(104=0.1μF),滤除电源 / 输入信号中的高频杂波,保证信号稳定 |
| 限流 / 分压电阻 | R28/R29(470R)、R30(330R)、R32(470R) | R28/R29 为运放供电端分压滤波;R30/R32 分别为输出信号、LED 限流,防止元件损坏 |
| 状态指示 | DA1(LED) | 信号可视化:有模拟信号输出时点亮,亮度可反映信号幅值大小 |
| 接口 / 电源 | J52(DAC1 AIN3) | 模拟信号对外输出接口,标注 "DAC1" 说明与数模转换输出直接关联 |
| VCC/GND | 为运放提供工作电源,保证电路正常工作 |
5. 软件设计
5.1 pwm.c 与 pwm.h
pwm.c
cpp
#include "pwm.h"
//全局变量定义
u8 gtim_h=0;//保存定时器初值高8位
u8 gtim_l=0;//保存定时器初值低8位
u8 gduty=0;//保存PWM占空比
u8 gtim_scale=0;//保存PWM周期=定时器初值*tim_scale
/*******************************************************************************
* 函 数 名 : pwm_init
* 函数功能 : PWM初始化函数
* 输 入 : tim_h:定时器高8位
tim_l:定时器低8位
tim_scale:PWM周期倍数:定时器初值*tim_scale
duty:PWM占空比(要小于等于tim_scale)
* 输 出 : 无
*******************************************************************************/
void pwm_init(u8 tim_h,u8 tim_l,u16 tim_scale,u8 duty)
{
gtim_h=tim_h;//将传入的初值保存在全局变量中,方便中断函数继续调用
gtim_l=tim_l;
gduty=duty;
gtim_scale=tim_scale;
TMOD|=0X01; //选择为定时器0模式,工作方式1
TH0 = gtim_h; //定时初值设置
TL0 = gtim_l;
ET0=1;//打开定时器0中断允许
EA=1;//打开总中断
TR0=1;//打开定时器
}
/*******************************************************************************
* 函 数 名 : pwm_set_duty_cycle
* 函数功能 : PWM设置占空比
* 输 入 : duty:PWM占空比(要小于等于tim_scale)
* 输 出 : 无
*******************************************************************************/
void pwm_set_duty_cycle(u8 duty)
{
gduty=duty;
}
void pwm(void) interrupt 1 //定时器0中断函数
{
static u16 time=0;
TH0 = gtim_h; //定时初值设置
TL0 = gtim_l;
time++;
if(time>=gtim_scale)//PWM周期=定时器初值*gtim_scale,重新开始计数
time=0;
if(time<=gduty)//占空比
PWM=1;
else
PWM=0;
}
pwm.h
cpp
#ifndef _pwm_H
#define _pwm_H
#include "public.h"
//管脚定义
sbit PWM=P2^1;
//变量声明
extern u8 gtim_scale;
//函数声明
void pwm_init(u8 tim_h,u8 tim_l,u16 tim_scale,u8 duty);
void pwm_set_duty_cycle(u8 duty);
#endif
这段代码是用软件方式模拟实现 PWM 信号(基于 51 单片机定时器 0 的中断机制),核心能力:
- 初始化 PWM 的周期(由定时器初值 + 周期倍数决定)和初始占空比;
- 运行时动态修改 PWM 占空比;
- 通过定时器中断的计数和电平切换,在指定引脚(由
PWM宏定义)输出占空比可调的方波信号。
1. 全局变量定义(核心参数存储)
cpp
u8 gtim_h=0;//保存定时器初值高8位
u8 gtim_l=0;//保存定时器初值低8位
u8 gduty=0;//保存PWM占空比
u8 gtim_scale=0;//保存PWM周期=定时器初值*tim_scale
gtim_h/gtim_l:存储定时器 0 的定时初值(51 单片机定时器 0 是 16 位,分高 8 位 TH0、低 8 位 TL0),决定单次定时器中断的时间(比如初值设置为 65536-1000=64536,对应 1ms 中断一次,晶振 11.0592MHz);gduty:存储 PWM 占空比的 "计数阈值"(不是百分比,是和gtim_scale的比值);gtim_scale:PWM 周期的 "计数倍数"------ 因为单定时器中断的时间通常很短(如 1ms),需要通过倍数扩展 PWM 周期(比如倍数 = 100,周期 = 1ms×100=100ms)。
2. PWM 初始化函数(pwm_init)
核心作用:配置定时器 0 并启动中断,初始化 PWM 的核心参数:
cpp
void pwm_init(u8 tim_h,u8 tim_l,u16 tim_scale,u8 duty)
{
gtim_h=tim_h; //将传入的初值保存在全局变量中,方便中断函数继续调用
gtim_l=tim_l;
gduty=duty;
gtim_scale=tim_scale;
TMOD|=0X01; //选择为定时器0模式,工作方式1
TH0 = gtim_h; //定时初值设置
TL0 = gtim_l;
ET0=1; //打开定时器0中断允许
EA=1; //打开总中断
TR0=1; //打开定时器
}
- 入参说明:
tim_h/tim_l:定时器 0 的 16 位初值(分高低 8 位传入);tim_scale:PWM 周期倍数(比如 100,代表 PWM 周期 = 单次中断时间 ×100);duty:初始占空比阈值(需≤tim_scale,比如 duty=30、scale=100,占空比 = 30%);
- 关键寄存器配置:
TMOD|= 0X01:设置定时器 0 为工作方式 1(16 位定时 / 计数模式);ET0 = 1:开启定时器 0 中断;EA=1:开启单片机总中断;TR0=1:启动定时器 0。
3. 占空比修改函数(pwm_set_duty_cycle)
核心作用 :运行时动态修改 PWM 占空比 ------ 只需修改全局变量gduty,中断函数会自动读取新值,无需重启定时器,实现占空比的实时调整。
cpp
void pwm_set_duty_cycle(u8 duty)
{
gduty=duty;
}
4. 定时器 0 中断函数(pwm,核心逻辑)
这段代码的核心是用定时器 0 中断累计计数,通过 "计数阈值对比" 实现 PWM 方波的高低电平切换;
cpp
void pwm(void) interrupt 1 //定时器0中断函数(中断号1)
{
static u16 time=0; //静态变量,中断中保持计数,不会每次重置
TH0 = gtim_h; //重新加载定时初值(方式1需手动重装)
TL0 = gtim_l;
time++; //每次中断,计数+1
if(time>=gtim_scale)//达到周期倍数,重置计数(PWM周期结束)
time=0;
if(time<=gduty)//计数≤占空比阈值:输出高电平
PWM=1;
else//计数>占空比阈值:输出低电平
PWM=0;
}
static u16 time=0:静态变量,只初始化一次,每次中断后保留计数结果(实现累计计数);TH0/TL0重装初值:定时器 0 方式 1 没有自动重装功能,必须手动重新写入初值,保证每次中断的时间一致;time++:每次定时器中断( 0.1ms 一次),计数加 1;time>=gtim_scale:当计数达到周期倍数(比如 100),重置为 0,代表一个 PWM 周期结束,开始下一个周期;time<=gduty:在一个周期内,计数≤占空比阈值时,PWM引脚置 1(高电平);否则置 0(低电平)------ 最终实现 "高电平占 duty/scale 的比例"。- 通过
time≥gtim_scale重置周期,通过time≤gduty切换电平,最终输出指定占空比的方波。
假设单片机晶振为 11.0592MHz,我们设置:
- 定时器 0 初值:
tim_h=0xff、tim_l=0x00(单次中断时间≈1ms); - 周期倍数:
tim_scale=100(PWM 周期 = 1ms×100=100ms); - 初始占空比:
duty=30(占空比 = 30/100=30%)。
运行过程:
- 定时器 0 每 1ms 触发一次中断,
time从 0 开始计数; time=0~29(前 30ms):PWM=1(高电平);time=30~99(后 70ms):PWM=0(低电平);time=100:重置为 0,重复上述过程;- 最终输出:100ms 周期、30% 占空比的方波信号。
5.2 main.c
cpp
/**************************************************************************************
实验名称:DAC模数转换实验
接线说明:
实验现象:下载程序后,DAC(PWM)模块上的指示灯DA1呈呼吸灯效果,由暗变亮再由亮变暗
注意事项:
***************************************************************************************/
#include "public.h"
#include "pwm.h"
/*******************************************************************************
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void main()
{
u8 dir=0;//默认为0
u8 duty=0;
pwm_init(0XFF,0XF6,100,0);//定时时间为0.01ms,PWM周期是100*0.01ms=1ms,占空比为0%
while(1)
{
if(dir==0)//当dir为递增方向
{
duty++;//占空比递增
if(duty==70)dir=1;//当到达一定值切换方向,占空比最大能到100,但到达70左右再递增,
//肉眼也分辨不出亮度变化
}
else
{
duty--;
if(duty==0)dir=0;//当到达一定值切换方向
}
pwm_set_duty_cycle(duty);//设置占空比
delay_ms(1);//短暂延时,让呼吸灯有一个流畅的效果
}
}
1.主函数变量定义
这两个变量是呼吸灯的 "节奏控制器":
cpp
u8 dir=0;//默认为0
u8 duty=0;
dir:方向标志位 (direction),控制占空比的变化趋势:dir=0:占空比递增(LED 由暗变亮);dir=1:占空比递减(LED 由亮变暗);
duty:当前 PWM 占空比的 "阈值"(对应占空比 =duty/gtim_scale ×100%,这里gtim_scale=100,所以 duty=70 对应 70% 占空比)。
2. PWM 初始化(关键参数拆解)
cpp
pwm_init(0XFF,0XF6,100,0);//定时时间为0.01ms,PWM周期是100*0.01ms=1ms,占空比为0%
| 参数值 | 具体作用 |
|---|---|
0XFF/0XF6 |
定时器 0 的 16 位初值(TH0=0XFF,TL0=0XF6):11.0592MHz 晶振下,单次定时器中断时间≈0.01ms(10μs); |
100 |
PWM 周期倍数(gtim_scale):PWM 周期 = 单次中断时间 ×100=0.01ms×100=1ms(方波每秒切换 1000 次,人眼无闪烁); |
0 |
初始占空比阈值(gduty):对应占空比 0%,DA1 初始为熄灭状态; |
3. 死循环(呼吸灯核心逻辑)
cpp
while(1)
{
if(dir==0)//当dir为递增方向(LED由暗变亮)
{
duty++;//占空比阈值每次+1(对应占空比+1%)
if(duty==70)dir=1;//占空比到70时切换方向
// 注释说明:占空比最大可设100,但70%时LED亮度已接近最大值,肉眼无法分辨后续亮度提升,设70更合理
}
else//dir为递减方向(LED由亮变暗)
{
duty--;//占空比阈值每次-1(对应占空比-1%)
if(duty==0)dir=0;//占空比到0时切换方向
}
pwm_set_duty_cycle(duty);//更新PWM占空比(让硬件输出新的方波)
delay_ms(1);//短暂延时1ms,让亮度变化更流畅
}
初始状态 :dir=0、duty=0 → 进入递增分支,duty从 0 开始每次 + 1,占空比从 0%→1%→2%...→70%,DA1 从熄灭慢慢变亮;
切换递减 :当duty=70(LED 最亮)→ dir=1,切换到递减分支,duty从 70→69→68...→0,DA1 从最亮慢慢熄灭;
切换递增 :当duty=0(LED 熄灭)→ dir=0,回到递增分支,循环往复;
delay_ms(1):如果没有这个延时,duty会瞬间从 0→70→0,LED 只会 "闪一下";加 1ms 延时后,占空比从 0 到 70 需要 70ms,从 70 到 0 也需要 70ms,一次完整 "呼吸" 约 140ms,视觉上是平滑渐变而非跳变;
占空比上限 70 而非 100:LED 亮度和占空比并非完全线性,70% 占空比时亮度已接近 100%,继续增加到 100% 无视觉差异,还能减少调整步数,让呼吸节奏更自然。
5.3 public.c 与 public.h
public.c
cpp
#include "public.h"
/*******************************************************************************
* 函 数 名 : delay_10us
* 函数功能 : 延时函数,ten_us=1时,大约延时10us
* 输 入 : ten_us
* 输 出 : 无
*******************************************************************************/
void delay_10us(u16 ten_us)
{
while(ten_us--);
}
/*******************************************************************************
* 函 数 名 : delay_ms
* 函数功能 : ms延时函数,ms=1时,大约延时1ms
* 输 入 : ms:ms延时时间
* 输 出 : 无
*******************************************************************************/
void delay_ms(u16 ms)
{
u16 i,j;
for(i=ms;i>0;i--)
for(j=110;j>0;j--);
}
public.h
cpp
#ifndef _public_H
#define _public_H
#include "reg52.h"
typedef unsigned int u16; //对系统默认数据类型进行重定义
typedef unsigned char u8;
typedef unsigned long u32;
void delay_10us(u16 ten_us);
void delay_ms(u16 ms);
#endif
5.4 思考
① 为什么会想到使用中断?
中断的本质是 "硬件触发、打断当前程序、优先执行指定任务,执行完再回到原程序"
举个直观的例子:
- 中断就像 "闹钟":每 0.01ms 响一次,你(主函数)不用一直看表,听到闹钟响就去做 "计数 + 调电平" 的小事,做完继续回来调整呼吸灯的占空比;
- 如果不用中断(轮询):你需要一直盯着表(主函数循环),既要看 0.01ms 到没到,又要调占空比,根本忙不过来,还容易出错。
此外,
PWM 需要 "精准、非阻塞的周期性计时",这是轮询 / 软件延时无法满足的;
不用中断会导致计时不准、主函数阻塞,呼吸灯效果失效;
51 单片机无硬件 PWM,定时器中断是实现精准周期性操作的最优解;
硬件级精准计时 + 非阻塞,让 PWM 计时和主函数的呼吸灯逻辑 "并行" 工作,最终实现稳定的呼吸灯效果
总之,要精准计时,又不想让主函数卡着不动",就自然会联想到 51 单片机最擅长的"定时器中断"
② 程序如何执行的?
主函数是 "呼吸节奏的指挥家",每隔 1ms 调整一次占空比;
中断是 "PWM 电平的执行者",每隔 0.01ms 按指挥的要求切换电平,两者配合完成稳定的呼吸灯 中断频率:100 次 /ms(每 0.01ms 一次);
主函数占空比调整频率:1 次 /ms(每 1ms 调整一次 duty);
PWM 周期:1ms(100 次中断为一个周期);
主函数执行的同时,定时器 0 一直在后台计时,这是中断与主函数 "并行" 的核心:
- 定时器 0 从
0XFF 0XF6计数到0XFFFF(约 0.01ms)→ 触发定时器 0 中断; - CPU立即暂停主函数的执行 (比如暂停在
delay_ms(1)或duty++步骤),跳转到中断函数pwm()执行; - 中断函数执行完毕,CPU回到主函数暂停的位置 ,继续执行主循环(比如继续完成
delay_ms(1)的延时)。 - 主函数负责 "慢节奏" 的占空比调整(1ms / 次),中断负责 "快节奏" 的 PWM 电平切换(0.01ms / 次),两者并行执行;主函数修改
gduty(占空比阈值),中断函数读取gduty并调整 PWM 电平,最终实现占空比渐变的呼吸灯效果。 - 执行顺序:上电→初始化→主循环 + 中断触发→中断打断主函数→执行中断→返回主函数→循环往复;
③ 如何触发中断的?
定时器 0 中断触发的核心是16 位计数器从设定初值(0XFFF6)计数到满值(0XFFFF)后溢出,这是硬件层面的触发信号;