51单片机播放音乐(三):PWM播放任意波形

51单片机播放音乐(三):PWM播放任意波形

  • 原理
    • PWM
    • 仿真电路图
    • 音频文件转成PWM代码
  • 单片机代码
  • 仿真输出波形

本文完整源码

原理

PWM

详细的参考这篇文章,这里简单说一下

脉冲宽度调制(PWM)的基本原理是冲量相等而形状不同的窄脉冲加在具有惯性的环节上时,其效果基本相同。冲量指窄脉冲的面积。效果基本相同,是指环节的输出响应波形基本相同,低频段非常接近,仅在高频段略有差异。这样,输出频率相同占空比不同的方波,接上惯性环节,就能实现DA转换了

51单片机播放音乐(三):PWM播放任意波形_第1张图片

从滤波的角度看,就是用一个低通滤波器把PWM波的交流部分过滤掉,只留下直流部分。这个滤波器的参数有几点要考虑:

  1. 不同占空比的方波频域第一个峰都是载波频率,所以要把滤波器的截止频率设置在0Hz到PWM载波频率之间
  2. 截止频率低会导致建立时间长,波形跟不上;截止频率高会导致纹波大,表现为听到的声音一直有高频噪声
  3. 增加滤波器阶数可以加快高频部分的衰减,这样建立时间和滤波性能都有改善,但是增加阶数会导致成本变高(仿真中不用考虑)
  4. 增加PWM载波频率可以改善性能,但是由于单片机速度限制载波频率很低(大约5000~8000Hz)

仿真电路图

51单片机播放音乐(三):PWM播放任意波形_第2张图片

单片机从P1.0口输出PWM波,经过6阶RC低通滤波器,隔直,放大20倍后接到扬声器。滤波器的幅频特性曲线如下图所示,3.89Hz已经达到-60dB,这样噪声放大后的幅度大约也就1%

51单片机播放音乐(三):PWM播放任意波形_第3张图片

音频文件转成PWM代码

这次要用2个定时器,定时器0负责输出高电平,频率等于PWM载波频率,定时器1负责输出低电平,它的定时时间等于本周期内高电平持续时间,根据占空比决定。因为单片机速度太慢,计算要尽量在电脑上完成,电脑上要计算好定时器1的初始计数。为了节省空间和使用8位自动重载定时器来提高精度,我限制了PWM频率不能太低,这样每个周期只需要1个字节编码

import wave

import numpy as np

# 晶振频率(Hz)
CRYSTAL_FREQUENCY = 11059200
# 计数周期(s)
COUNT_PERIOD = 1 / (CRYSTAL_FREQUENCY / 12)


def wav_to_pwm(wav_path, output_path):
    with wave.open(wav_path, 'rb') as f:
        n_channels, sample_width, frame_rate, n_frames, _, _ = f.getparams()
        assert sample_width in (1, 2), '只支持8位或16位采样'
        frame_period = 1 / frame_rate
        pcm = f.readframes(n_frames)

    # 定时器0初始计数,影响PWM载波频率。载波频率越高越好,这里只取载波频率 = 采样频率
    init_count = 65536 - int(frame_period / COUNT_PERIOD)
    init_th0 = init_count // 256
    # 如果采样周期太长,每个周期需要2字节编码,浪费空间。最低频率3600Hz
    assert init_th0 == 255, '采样率太低'
    init_tl0 = init_count % 256

    if sample_width == 1:
        pcm = np.fromstring(pcm, np.int8).astype(np.float)
        # 取第一个声道
        pcm = pcm.reshape(n_frames, n_channels)[:, 0]
        # 方波下的面积在本周期占的比例,作为PWM方波占空比
        duty_cycles = (pcm + 128) / 256
    else:
        pcm = np.fromstring(pcm, np.int16).astype(np.float)
        pcm = pcm.reshape(n_frames, n_channels)[:, 0]
        duty_cycles = (pcm + 32768) / 65536
    # 定时器1在每个周期的初始计数,影响高电平时间
    init_tl1s = 256 - (duty_cycles * (frame_period / COUNT_PERIOD)).astype(np.int32)
    # 防溢出
    for index in np.where(init_tl1s < 0):
        init_tl1s[index] = 0
    for index in np.where(init_tl1s > 255):
        init_tl1s[index] = 255

    with open(output_path + '.h', 'w') as f:
        f.write(f"""#define SAMPLE_RATE {frame_rate}
// #define INIT_TH0 {init_th0}
#define INIT_TL0 {init_tl0}
#define PWM_DATA_LEN {len(init_tl1s)}
extern const unsigned char code pwmData[];
""")

    with open(output_path + '.c', 'w') as f:
        f.write('const unsigned char code pwmData[] = (\n')
        for i in range(0, len(init_tl1s), 32):
            f.write('\t"')
            for init_tl1 in init_tl1s[i: i + 32]:
                f.write(f'\\x{init_tl1:02X}')
            f.write('"\n')
        f.write(');\n')


def main():
    wav_to_pwm('flower dance.wav', '../pwm/music_data')


if __name__ == '__main__':
    main()

单片机代码

#include 

#include "music_data.h"

// 引脚定义
#define pwmOut P1_0

unsigned int iPwmData = 0;

void Init() {
	// 开启中断
	EA = 1;
	ET0 = 1;
	ET1 = 1;
	// 定时器0工作方式2(8位自动重载定时器)
	// 定时器1工作方式1(16位定时器)
	TMOD = 0x12;
	
	TH0 = TL0 = INIT_TL0;
	TH1 = 255;
	TL1 = pwmData[iPwmData];
	
	pwmOut = 1;
	// 开启定时器
	TR0 = 1;
	TR1 = 1;
}

int main() {
	Init();
	while (1);
	
	return 0;
}

// 定时器0,输出高电平,周期为PWM载波周期
void HandleTimer0() interrupt 1 {
	pwmOut = 1;
	if (++iPwmData >= PWM_DATA_LEN)
		iPwmData = 0;
	
	TH1 = 255;
	TL1 = pwmData[iPwmData];
	
	TR1 = 1;
}

// 定时器1,输出低电平,周期根据占空比定
void HandleTimer1() interrupt 3 {
	pwmOut = 0;
	TR1 = 0;
}

仿真输出波形

从上到下是PWM波形、经过滤波器的波形和经过放大的波形

51单片机播放音乐(三):PWM播放任意波形_第4张图片

虽然还是有一些噪声,但是可以清楚听到钢琴的声音了。结论就是PWM适合低频的信号,高频的还是老老实实用DA转换吧,也不用费脑调整滤波器参数

你可能感兴趣的:(音频处理,单片机,音乐,C51,PWM,脉宽调制)