51单片机——DAC数模转换实验(二)

目录

[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 的中断机制),核心能力:

  1. 初始化 PWM 的周期(由定时器初值 + 周期倍数决定)和初始占空比;
  2. 运行时动态修改 PWM 占空比;
  3. 通过定时器中断的计数和电平切换,在指定引脚(由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;		
}
  1. static u16 time=0:静态变量,只初始化一次,每次中断后保留计数结果(实现累计计数);
  2. TH0/TL0重装初值:定时器 0 方式 1 没有自动重装功能,必须手动重新写入初值,保证每次中断的时间一致;
  3. time++:每次定时器中断( 0.1ms 一次),计数加 1;
  4. time>=gtim_scale:当计数达到周期倍数(比如 100),重置为 0,代表一个 PWM 周期结束,开始下一个周期;
  5. time<=gduty:在一个周期内,计数≤占空比阈值时,PWM引脚置 1(高电平);否则置 0(低电平)------ 最终实现 "高电平占 duty/scale 的比例"。
  6. 通过time≥gtim_scale重置周期,通过time≤gduty切换电平,最终输出指定占空比的方波。

假设单片机晶振为 11.0592MHz,我们设置:

  • 定时器 0 初值:tim_h=0xfftim_l=0x00(单次中断时间≈1ms);
  • 周期倍数:tim_scale=100(PWM 周期 = 1ms×100=100ms);
  • 初始占空比:duty=30(占空比 = 30/100=30%)。

运行过程:

  1. 定时器 0 每 1ms 触发一次中断,time从 0 开始计数;
  2. time=0~29(前 30ms):PWM=1(高电平);
  3. time=30~99(后 70ms):PWM=0(低电平);
  4. time=100:重置为 0,重复上述过程;
  5. 最终输出: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=0duty=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 一直在后台计时,这是中断与主函数 "并行" 的核心

  1. 定时器 0 从0XFF 0XF6计数到0XFFFF(约 0.01ms)→ 触发定时器 0 中断;
  2. CPU立即暂停主函数的执行 (比如暂停在delay_ms(1)duty++步骤),跳转到中断函数pwm()执行;
  3. 中断函数执行完毕,CPU回到主函数暂停的位置 ,继续执行主循环(比如继续完成delay_ms(1)的延时)。
  4. 主函数负责 "慢节奏" 的占空比调整(1ms / 次),中断负责 "快节奏" 的 PWM 电平切换(0.01ms / 次),两者并行执行;主函数修改gduty(占空比阈值),中断函数读取gduty并调整 PWM 电平,最终实现占空比渐变的呼吸灯效果。
  5. 执行顺序:上电→初始化→主循环 + 中断触发→中断打断主函数→执行中断→返回主函数→循环往复;

③ 如何触发中断的?

定时器 0 中断触发的核心是16 位计数器从设定初值(0XFFF6)计数到满值(0XFFFF)后溢出,这是硬件层面的触发信号;

相关推荐
ChatGPT52 小时前
一个适用于嵌入式系统的轻量级、可移植LED控制模块。
单片机
boneStudent2 小时前
Day39:智能家居环境监测系统
stm32·单片机·嵌入式硬件·智能家居
polarislove02143 小时前
5.8W25Q64 实验(下)-嵌入式铁头山羊STM32笔记
笔记·stm32·嵌入式硬件
xingzhemengyou13 小时前
STM32 Cortex-M4内核时钟系统
stm32·单片机·嵌入式硬件
猪八戒1.05 小时前
机械狗软件部分
嵌入式硬件
悠哉悠哉愿意5 小时前
【EDA学习笔记】电子技术基础知识:元件数据手册
笔记·单片机·嵌入式硬件·学习·eda
点灯小铭6 小时前
基于单片机的档案库房漏水检测报警labview上位机系统设计
单片机·嵌入式硬件·毕业设计·课程设计·labview·期末大作业
Arciab6 小时前
51单片机学习板PCB制作
嵌入式硬件·学习·51单片机
一个平凡而乐于分享的小比特6 小时前
STM32 GPIO 8种工作模式深入详解
stm32·单片机·嵌入式硬件·gpio