24-DAC 数模转换实验项目总结(PWM 呼吸灯)
工程路径:
C:\Users\54200\Desktop\51MCU\24-DAC数模转换实验目标芯片:STC89C52RC @ 普中(PRECHIN)51 开发板
开发环境:Keil5 C51 + STC-ISP 烧录
实验现象:下载程序后,DAC(PWM) 模块指示灯 DA1 呈呼吸灯效果,由暗渐亮再由亮渐暗
目录
- [1. 项目概述](#1. 项目概述)
- [2. 工程目录结构](#2. 工程目录结构)
- [3. 硬件平台与引脚分配](#3. 硬件平台与引脚分配)
- [4. 实验原理](#4. 实验原理)
- [5. 程序架构与模块划分](#5. 程序架构与模块划分)
- [6. 关键代码解析](#6. 关键代码解析)
- [7. 程序流程图](#7. 程序流程图)
- [8. 关键参数计算](#8. 关键参数计算)
- [9. 实验现象与结论](#9. 实验现象与结论)
- [10. 学习要点与反思](#10. 学习要点与反思)
1. 项目概述
本工程为普中 51 单片机教程(BV1Mb411e7re)第 24 讲配套实验。STC89C52RC 片内没有硬件 DAC 外设 ,实验采用定时器 0 中断产生软件 PWM ,PWM 波经模块上的 RC 低通滤波电路取出平均值,等效实现数模转换(DAC),驱动 DA1 指示灯。主循环以"占空比递增---递减"往复扫描的方式改变平均电压,形成呼吸灯效果。
核心特征:
| 项目 | 说明 |
|---|---|
| 实现方式 | 软件 PWM(定时器 0、工作方式 1)+ RC 滤波 |
| PWM 周期 | 100 × 0.01ms = 1ms(频率 1kHz) |
| 占空比范围 | 0 ~ 70(70 以上肉眼分辨不出亮度变化) |
| 输出引脚 | P2.1(sbit PWM = P2^1) |
| 主循环节奏 | 每步 delay_ms(1),约 1.4s 完成一次呼吸周期 |
2. 工程目录结构
24-DAC数模转换实验/
├── App/
│ └── pwm/ # PWM 驱动模块(核心)
│ ├── pwm.c # pwm_init / pwm_set_duty_cycle / T0 中断服务
│ └── pwm.h # 引脚定义、外部变量与函数声明
├── Public/
│ ├── public.c # 通用软件延时 delay_10us / delay_ms
│ └── public.h # 数据类型重定义 u8/u16/u32、延时函数声明
├── User/
│ └── main.c # 主函数:占空比三角波扫描
├── Obj/ # Keil 编译输出(.obj/.lst/.hex/.m51)
├── template.uvproj # Keil 工程文件
├── template.uvopt # Keil 工程选项
└── 呼吸灯一样闪烁.jpg # 实验现象参考图
分层结构:User(应用层)→ App(驱动层)→ Public(基础层),层次清晰、便于复用。
3. 硬件平台与引脚分配
- 芯片:STC89C52RC(内核兼容 8051,12MHz 晶振,机器周期 1μs)
- 开发板:普中(PRECHIN)51 开发板,DAC(PWM) 模块
- PWM 输出:P2.1 (在
pwm.h中以sbit PWM = P2^1定义,可移植时只需改这一处) - 后级电路:RC 低通滤波 → 运放缓冲 → LED(DA1)
- 定时器:定时器 0,工作方式 1(16 位定时)
4. 实验原理
4.1 PWM 模拟 DAC
数字引脚只能输出 0 或 VCC 两个电平,无法直接产生中间电压。但 PWM 波形的平均值等于 VCC × 占空比:
- 占空比 0% → 平均电压 0V → LED 熄灭
- 占空比 70% → 平均电压约 0.7×VCC → LED 较亮
- 占空比连续变化 → 平均电压线性变化 → 亮度平滑变化
当 PWM 频率(本实验 1kHz)远高于 LED/人眼响应频率时,经 RC 滤波后即可得到稳定的模拟电压,等效于一个分辨率约 100 级(tim_scale=100)的 DAC。
4.2 呼吸灯的三角波调制
主循环令占空比 duty 先从 0 递增至 70,再递减回 0,形成三角波扫描。平均电压随之先升后降,LED 亮度同步"渐亮→渐暗",每步延时 1ms 保证过渡流畅。占空比上限取 70 而非 100,是因为亮度与人眼感知非线性,超过 70 后继续增大肉眼几乎无法分辨。
5. 程序架构与模块划分
| 模块 | 文件 | 职责 |
|---|---|---|
| 应用层 | User/main.c |
方向变量 dir + 占空比 duty 三角波扫描 |
| 驱动层 | App/pwm/pwm.c |
PWM 初始化、占空比设置、定时器 0 中断服务程序 |
| 基础层 | Public/public.c |
通用软件延时函数 |
模块间通过全局变量 gduty(占空比)解耦:主循环只调用 pwm_set_duty_cycle() 写入目标值,波形生成完全由中断异步完成------这是典型的"前台逻辑 + 后台波形"前后台系统结构。
6. 关键代码解析
6.1 PWM 初始化(pwm.c)
c
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(16位)
TH0 = gtim_h;
TL0 = gtim_l;
ET0 = 1; // 允许定时器0中断
EA = 1; // 开总中断
TR0 = 1; // 启动定时器
}
入口参数解耦:初值、周期倍数、占空比均由调用方传入,模块可复用于任意频率/分辨率的 PWM 场景。
6.2 中断服务程序(波形生成核心)
c
void pwm(void) interrupt 1
{
static u16 time = 0;
TH0 = gtim_h; // 重装初值,保证精确 0.01ms
TL0 = gtim_l;
time++;
if (time >= gtim_scale) // 周期计满(100次)
time = 0;
if (time <= gduty) // 高电平窗口
PWM = 1;
else
PWM = 0;
}
要点:①中断内重装初值,抵消中断响应延迟,保证节拍精度;②static u16 time 只在中断上下文存活,主程序不可见;③比较 time <= gduty 输出高电平,gduty 即高电平计数值。
6.3 主循环三角波(main.c)
c
if (dir == 0) { // 递增方向
duty++;
if (duty == 70) dir = 1; // 到达上限,反向
} else { // 递减方向
duty--;
if (duty == 0) dir = 0; // 回到下限,再反向
}
pwm_set_duty_cycle(duty); // 更新占空比
delay_ms(1); // 控制呼吸速度
dir 作为方向标志位,实现 0→70→0 的往复扫描;duty 是"逻辑亮度",与 PWM 硬件输出解耦。
7. 程序流程图
7.1 总体流程(Mermaid,Obsidian 可直接渲染)
#mermaid-svg-hXD5HXf1e6UAO5uG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hXD5HXf1e6UAO5uG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hXD5HXf1e6UAO5uG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hXD5HXf1e6UAO5uG .error-icon{fill:#552222;}#mermaid-svg-hXD5HXf1e6UAO5uG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hXD5HXf1e6UAO5uG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hXD5HXf1e6UAO5uG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hXD5HXf1e6UAO5uG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hXD5HXf1e6UAO5uG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hXD5HXf1e6UAO5uG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hXD5HXf1e6UAO5uG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hXD5HXf1e6UAO5uG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hXD5HXf1e6UAO5uG .marker.cross{stroke:#333333;}#mermaid-svg-hXD5HXf1e6UAO5uG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hXD5HXf1e6UAO5uG p{margin:0;}#mermaid-svg-hXD5HXf1e6UAO5uG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hXD5HXf1e6UAO5uG .cluster-label text{fill:#333;}#mermaid-svg-hXD5HXf1e6UAO5uG .cluster-label span{color:#333;}#mermaid-svg-hXD5HXf1e6UAO5uG .cluster-label span p{background-color:transparent;}#mermaid-svg-hXD5HXf1e6UAO5uG .label text,#mermaid-svg-hXD5HXf1e6UAO5uG span{fill:#333;color:#333;}#mermaid-svg-hXD5HXf1e6UAO5uG .node rect,#mermaid-svg-hXD5HXf1e6UAO5uG .node circle,#mermaid-svg-hXD5HXf1e6UAO5uG .node ellipse,#mermaid-svg-hXD5HXf1e6UAO5uG .node polygon,#mermaid-svg-hXD5HXf1e6UAO5uG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hXD5HXf1e6UAO5uG .rough-node .label text,#mermaid-svg-hXD5HXf1e6UAO5uG .node .label text,#mermaid-svg-hXD5HXf1e6UAO5uG .image-shape .label,#mermaid-svg-hXD5HXf1e6UAO5uG .icon-shape .label{text-anchor:middle;}#mermaid-svg-hXD5HXf1e6UAO5uG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hXD5HXf1e6UAO5uG .rough-node .label,#mermaid-svg-hXD5HXf1e6UAO5uG .node .label,#mermaid-svg-hXD5HXf1e6UAO5uG .image-shape .label,#mermaid-svg-hXD5HXf1e6UAO5uG .icon-shape .label{text-align:center;}#mermaid-svg-hXD5HXf1e6UAO5uG .node.clickable{cursor:pointer;}#mermaid-svg-hXD5HXf1e6UAO5uG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hXD5HXf1e6UAO5uG .arrowheadPath{fill:#333333;}#mermaid-svg-hXD5HXf1e6UAO5uG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hXD5HXf1e6UAO5uG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hXD5HXf1e6UAO5uG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hXD5HXf1e6UAO5uG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hXD5HXf1e6UAO5uG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hXD5HXf1e6UAO5uG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hXD5HXf1e6UAO5uG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hXD5HXf1e6UAO5uG .cluster text{fill:#333;}#mermaid-svg-hXD5HXf1e6UAO5uG .cluster span{color:#333;}#mermaid-svg-hXD5HXf1e6UAO5uG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hXD5HXf1e6UAO5uG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hXD5HXf1e6UAO5uG rect.text{fill:none;stroke-width:0;}#mermaid-svg-hXD5HXf1e6UAO5uG .icon-shape,#mermaid-svg-hXD5HXf1e6UAO5uG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hXD5HXf1e6UAO5uG .icon-shape p,#mermaid-svg-hXD5HXf1e6UAO5uG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hXD5HXf1e6UAO5uG .icon-shape .label rect,#mermaid-svg-hXD5HXf1e6UAO5uG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hXD5HXf1e6UAO5uG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hXD5HXf1e6UAO5uG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hXD5HXf1e6UAO5uG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 定时器0中断(每0.01ms异步触发)
是(递增)
是
否
否(递减)
是
否
是
否
是
否
共享 gduty
上电复位
pwm_init(0xFF,0xF6,100,0)
T0方式1, 0.01ms定时, 周期1ms
开启 ET0/EA/TR0
PWM 中断启动
dir == 0 ?
duty++
duty == 70 ?
dir = 1
pwm_set_duty_cycle
更新占空比
duty--
duty == 0 ?
dir = 0
delay_ms 1ms
重装初值 TH0/TL0
time++
time >= 100 ?
time = 0
time <= gduty ?
PWM = 1
PWM = 0
RC低通滤波 → DA1 呼吸灯
7.2 时序关系
time: 0 1 2 ... duty ... 99 0 1 ...
PWM: ┌────高────┐└──────低──────────┘┌───高──...
|<--------- 1ms 周期 ---------->|
占空比 duty/tim_scale = 平均电压/VCC
8. 关键参数计算
| 参数 | 计算 | 结果 |
|---|---|---|
| 机器周期 | 12MHz / 12 | 1μs |
| 定时器初值 | 0xFFF6 = 65526 | 定时 (65536−65526)×1μs = 10μs(0.01ms) |
| PWM 周期 | tim_scale=100 × 10μs | 1ms |
| PWM 频率 | 1 / 1ms | 1kHz |
| PWM 分辨率 | tim_scale | 100 级(1%) |
| 呼吸半周期 | 70 步 × 1ms | 70ms,全周期约 140ms |
9. 实验现象与结论
-
现象 :程序烧录后 DA1 由暗渐亮、再由亮渐暗,循环往复,无可见频闪。

蓝色灯右上方红色为呼吸灯,虽然与右侧D2灯对应,但D2没有呼吸现象。
-
结论一:软件 PWM + RC 滤波可以在无硬件 DAC 的 51 单片机上有效实现模拟量输出,平均电压与占空比呈线性关系(V_avg = VCC × duty/tim_scale)。
-
结论二:前后台架构下,主循环仅维护"逻辑亮度",波形由中断保证,占空比切换不会造成波形毛刺。
-
结论三:占空比上限取 70 体现了感知层面的工程折中------超出人眼分辨范围的精度没有实际意义。
10. 学习要点与反思
- 软件 DAC 本质:PWM 占空比 ↔ 平均电压的线性映射,是"数字→模拟"的最低成本方案(对比 STM32F103 的硬件 DAC/高级定时器 PWM,原理相通)。
- 中断重装初值 :方式 1 不会自动重装,中断内先
TH0/TL0赋值再计数,是保证精度的关键细节。 - 参数化设计 :
pwm_init(h, l, scale, duty)把频率与分辨率交给调用者,模块可移植性强,值得在后续项目中沿用。 - volatile 意识 :
gduty在主循环与中断间共享,严格来说应加volatile防止编译器优化(本例单次赋值场景未出问题,但习惯要养成)。 - 与前序实验的衔接:与 STM32 线"6-4 PWM 驱动舵机""6-5 PWM 驱动直流电机"形成对照------同一 PWM 概念,51 用软件定时器模拟,STM32 用硬件定时器外设,可对比加深理解。