不久前在数电中自学了DA与AD的内容, 书中介绍的主要是基于ADC, DAC芯片实现的数模之间的转换. 而实现DA还可以通过单片机输出PWM的方法.

网上找了相关资料, 一些作者的文章写得很不错, 让我这个“门外汉”也能简单了解PWM技术. 虽然不高深, 但这个过程中, 有数电, 模电和数学的综合运用, 这让我有点兴趣, 可以搞一搞. 尽管因为经验不能像那些文章那般专业, 但至少能把我的理解再简单地表述出来. 下面先从一些概念下手.

PWM(Pulse Width Modulation), 又称为脉冲宽度调制, 是利用MCU输出的数字信号(一般就是调制的矩形波)进而去产生控制模拟信号(各种其他波形等)的方法. PWM一般应用在测量通信方面(度娘如是说). PWM在这些领域如何风光暂且按下不表, 这次只对利用计时器I/O口输出PWM实现DA做思考.

顺便记一下, 单片机要实现PWM, 有两种方法:一种是利用计时器设置, I/O口输出PWM;另一种使用模块输出PWM. 一些功能强劲的MCU自带PWM模块用于产生PWM脉冲信号, 使用前对特定寄存器赋值, 比直接用计时器方便点. 如STC12C5A60S2系列, AVR和PIC等等.

大致的原理是这样的, 利用MCU的timer控制产生特定占空比频率的矩形波, 由傅里叶级数分析知道, 矩形波形函数可以分解为各级三角谐波, 所以运用模拟电子中的低通滤波器, 滤去高次谐波, 低频部分如果视为理想的话, 就能作为DA转换的直流输出. 因此关键是解决三个问题:

  • 1.如何调节矩形波频率产生所需占空比, 程序上如何实现.
  • 2.理想条件下, 滤去高次谐波后所得电压与控制量之间的关系, 具体来说数学表达式是怎样的.
  • 3.低通滤波器设计, 如何使其滤波效果较好.
    附带一个
  • 4.如此构建的DA转换分辨率如何及误差.

为了说明简单, 先假设通过PWM设定输出了一个稳定周期不变矩形波, 然后引入一些变量, 思路大概是这样:

调制波形

上图是调制后产生的稳定的矩形波, 参数说明如下:

  • T - 由单片机定时器设定的最小取样时间间隔
  • N - 一个周期包含的最小时间间隔数, 图中是10
  • n - 一个周期内输出高电平的最小时间间隔数, 图中是7
    函数方程:

采用展成复指数形式的傅里叶级数分析可能更简单些. 众所周知, 其级数系数$a_{k}$计算公式即:

简便起见, $V_{L}$视为0.

考虑$k=0$时, 在一个周期内积分得$a_{0}$:

这就是直流分量部分.
再考虑$k \neq 0$时:

这是滤波器需要去除的高次谐波部分.
从计算结果中可以看出, 如果能滤去较高次谐波, 只剩下$\frac{n}{N}V_{H}$, 当n从0变化到N, 输出电压范围从$V_{L}\sim0$. 设计滤波器需要计算计算截止频率以一次谐波频率为参照. 若使用最简单的RC低通滤波器, 特征频率应设置为$f_{0}=\frac{1}{NT}$,又因为RC滤波器$f_{c}=\frac{1}{2\pi RC}$, 应使$f_{c}$大于$f_{0}$, 得到NT小于2πRC,应该先设置好N与T再选择R与C, 在程序中N与T应该是设定不变的.

再稍微考虑下如何设置T与N, 这关乎到DA转换的精确度与分辨率.

说DAC芯片的时候有分辨率这一概念, 用以描述DA转换精度, 定义成能分辨出来的最小电压与最大电压之比. 比如10位DAC芯片, 分辨率表示为$\frac{1}{2^{10}}$.

而在用脉宽调制的方法时, 也是同样, 即n=1与n=N之比, 分辨率简单表示为$\frac{1}{N}$. 当然n也可以不以1作为变化间隔. 如令N=1024, 应该就可以大致模拟出10位DAC.

这里就引出一个矛盾, 增大N的设定值, 能够提高分辨率, 但也会增大NT, 使得高次谐波的频率降低, 这又为低通滤波器的工作带来麻烦, 会夹杂更多的纹波而降低精确度. 因此并不是N越大越好. 同时在选择R与C时, 也可以考虑留一些余地.

那么减小T如何呢, 这是个不错的选择, T往往受限于单片机的工作频率与其速度性能.

叙述完毕, 先用Proteus绘制仿真电路, 最终方案采用RC二阶无源低通滤波, 为能有一定带载能力输出采用电压跟随器. 如果是需要驱动大功率设备, 需要再加个功放.

程序的实现是这样的(先让n固定, 也就是输出电压不变):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<reg52.h>
#define uint unsigned int
#define uchar unsigned char
void init(); //初始化计时器0函数
sbit pwm = P2^1; //P2.1口输出pwm脉冲
uint count = 0; //用于计数

void main()
{
init();
while (1)
{
if (count < 900) //count由计时器设定每1us加1, 900这个位置
pwm = 1; //就是n, 这里先固定为900
else
pwm = 0;
}
}
void init()
{
TMOD &= 0x0f;
TMOD |= 0x01;
TH0 = (65536 - 1) / 256; //定的时间即上文提到的T, 为了精确这里定时1us
TL0 = (65536 - 1) % 256; //是不改变时钟周期下的极限
ET0 = 1;
EA = 1;
TR0 = 1;
}
void timer0() interrupt 1 //计时器0中断函数
{
TH0 = (65536 - 1) / 256;
TL0 = (65536 - 1) % 256;
count++;
if (count > 1024) //1024的位置就是N, 这个一般设定不变
{ //如果count计数到头了清零重新计数
count = 0;
}
}

粗略仿真波形结果

Proteus仿真波形

从上到下ABCD通道分别是:
A - P2.1口输出调制后的矩形脉冲
B - 经过一次RC低通滤波
C - 二次RC低通滤波
D - 电压跟随器输出

由于定时器定时1us, 已经达到了指令周期级别, 输出的矩形波会有一些不稳定, 这恐怕是这种方法难以避免的.

不稳定的波形

利用protues中的电压探针可以更为精确的测定输出的电压与程序中设定n的关系. 为了计算简单把N设定为1000吧.

测试结果

从仿真测试结果来看, 还是可以的嘛o(* ̄▽ ̄*)ブ

简单地应用下实现 LED呼吸灯
在原来程序上稍微改改, 嗯…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include<reg52.h>
#define uint unsigned int
#define uchar unsigned char
void init();
sbit pwm = P2^1;
uint n = 0, t = 0, flag = 0, count = 0;

void main()
{
init();
while (1)
{
if(t > 100) //这里设定n的变化快慢, 100us变化一次亮度
{
t = 0;
if (flag == 1) //flag用于控制n的递增与递减
{
n++;
}
if (flag == 0)
{
n--;
}
}
if (n == 1000) //当n到1000时递减, 到0时递增
{ //led从暗到明, 再从明到暗, 制造呼吸效果
flag = 0;
}
if (n == 0)
{
flag = 1;
}
if (count < n) //这里使用n, 因为输出电压需要变化
pwm = 1;
else
pwm = 0;
}
}

void init()
{
TMOD &= 0x0f;
TMOD |= 0x01;
TH0 = (65536 - 1) / 256;
TL0 = (65536 - 1) % 256;
ET0 = 1;
EA = 1;
TR0 = 1;
}

void timer0() interrupt 1
{
TH0 = (65536 - 1) / 256;
TL0 = (65536 - 1) % 256;
count++;
if (count > 1000)
{
count = 0;
}
t++;
}

参考: 《基于PWM实现DA转换电路设计》
.
.
.
.
.
.
.
.
.
終わり