CH585 PCM 播放测试以及双 PWM 原理
2026/7/26 4:07:26 网站建设 项目流程
简单聊一下双 PWM 音频播放原理及实现以及 PCM 播放测试 ...... 矜辰所致

前言

上一篇文章我们测试了一下 SBC 音频解码然后通过 双 PWM 播放音频的效果,当时更多的是想看看效果,只做了一些必要的解释说明。考虑到后面自己应用上需要有一些修改,有些必要的原理还是要了解一下,这样后期的做一些应用也知道去改什么地方。

所以本文我们简单学习一下双 PWM 的原理 以及测试一下 PCM 音频播放。

相关文章:
嵌入式语音开发应用基础说明
CH585 SBC 音频解码播放测试说明
.
我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!

目录

  • 前言
  • 一、 双 PWM 原理
    • 1.1 基础说明
    • 1.2 程序中双 PWM 的体现
    • 1.3 PWM 的频率
    • 1.4 音量调节
  • 二、 PCM 播放测试
    • 2.1 PCM 数据说明
    • 2.2 音频文件生成
    • 2.3 APP修改测试
  • 结语

一、 双 PWM 原理

现在有了 AI ,很多通用技术的理论了解并不是什么难事,我们简单说明一下,主要还是结合一下程序中如何实现做一些分析。

1.1 基础说明

双 PWM(Dual PWM / Differential PWM)音频驱动的本质,是用两路互补的 PWM 信号来驱动扬声器,通过差分放大还原出模拟音频波形。

双 PWM 音频:
两路互补的数字开关信号 → 差分驱动扬声器 → 利用感性负载滤除载波 → 还原模拟声音

相对于单路 PWM ,双路 PWM 方案有以下优点:

  • 抗干扰强,外界电磁干扰同时耦合到两根线,差分相互抵消,信噪比更高;双路 PWM 喇叭线圈等效 LC 低通,自动滤除高频载波 。
  • 输出功率翻倍,差分峰峰值 = 2×VDD,同等供电下音量更大;
  • 无直流偏置:静态时两路 50% 占空比,扬声器两端电压差为 0,音圈不会静态偏移,不烧喇叭、低音更扎实;
  • 外围电路无需输出电容,单端需要隔直电容,差分直接耦合

常见波形与 PCM 映射逻辑如下:

音频采样值 → 调制为两路互补 PWM:
PWM_A 占空比 = 50% + Δ
PWM_B 占空比 = 50% - Δ

举个例子 :

设 PWM 周期固定,满幅占空比 0~100%:
.
静音(PCM=0):PWM + 占空比 50%,PWM - 占空比 50%;两路电压平均值相等,喇叭两端压差 = 0,不发声。
.
正半周音频(PCM>0):PWM + 占空比升高,PWM - 占空比同步降低;喇叭左端平均电压 > 右端,电流正向流过音圈。
.
负半周音频(PCM<0):PWM + 占空比降低,PWM - 占空比同步升高;喇叭右端平均电压> 左端,电流反向流过音圈。

还有一种单极性交替 PWM (本文测试示例就使用的这种):

pcm>0:PWM+输出幅值,PWM2=0
pcm<0:PWM-输出幅值,PWM1=0

等价 H 桥单向导通:

正半周:左 PWM 输出方波,右侧恒低,电流正向流过喇叭
负半周:右 PWM 输出方波,左侧恒低,电流反向流过喇叭
过零点(pcm=0)两路都是 0,无静态压差

1.2 程序中双 PWM 的体现

在我们的测试程序中,有这么一段数据的处理:

for(uint32_ti=0;i<(out_len>>1);i++){if(pcm_buf[i]>0){*p_data=pcm_buf[i]/8;// 正值 → PWM1 有输出*p_data2=0;// PWM2 = 0}else{*p_data2=0-pcm_buf[i]/8;// 负值 → PWM2 有输出*p_data=0;// PWM1 = 0}p_data++;p_data2++;}

当音频信号为正(>0)时:p_data(PWM 通道 1)输出对应的占空比,p_data2(PWM 通道 2)静止在 0 电平。

当音频信号为负(<0)时:p_data2(PWM 通道 2)输出对应的占空比(取绝对值),p_data(PWM 通道 1)静止在 0 电平。

代码中的数据事直接决定 PWM 的占空比的,为什么上面的数据需要除 8 :

*p_data = pcm_sample / 8;

因为 这个数值是直接决定 PWM 的占空比的,在程序中, PWM 的占空比 也和总周期计数有关,这个值肯定不能大于总计数,这个我们需要先往下看,先了解一下 PWM 的频率有关的内容 。

1.3 PWM 的频率

PWM 音频播放,频率要调整到音频的采样率 , 在使用 MCU 的时候,一般的步骤如下:

  1. 确定/设置 定时器时钟来源;
  2. 确定/设置 有没有分频;
  3. 根据目标 PWM 载波频率,推算自动重载值。

对于本次的示例,自动重载值变量为max_cnt

// 根据采样率选择固定计数值if(rate==CH5XX_AUDIO_SAMPLE_RATE_16K){max_cnt=4875;}

这个值是怎么算出来的:

16K 采样配套:max_cnt = 4875
单次完整 PWM 周期计数总长 = max_cnt + 1 = 4876
16K 场景 PWM 载波频率:
F(pwm) = 78000000 / 4876 ≈ 15996.72 Hz

8K 采样率是 16K 的一半,所以 PWM 周期总计数必须翻倍:
总计数:4876*2 = 9752
max_cnt = 9752-1 = 9751
8K 下载波频率:
F(pwm) = 78000000 / 9752 ≈ 7998.36 Hz

好了,现在知道了在 16K 模式下面,最大重装载值为 4875, 也就可以解释为什么上面需要*p_data = pcm_sample / 8;了,示例中pcm_sample是一个int16_t的变量,数值范围:-32768 ~ 32767 。

如果不缩放直接赋值,大于 4875,会溢出失效。除以 8:最大输出 32767/8 ≈ 4095,小于 4875,留有安全余量;

这个值决定了占空比的大小,示例中是为了把他设置到合理范围,那么这里有一个比较重要的问题需要说明,就是改变这个单个的占空比,并不会改变声音内容,它改变的是声音大小!这里很多初学者会不太理解,但是要记住 。

❤️ 瞬时占空比数值:只决定当前音量大小;❤️

❤️ 占空比随时间变化的节奏 / 快慢:决定是什么声音、音调、语音内容。❤️

1.4 音量调节

上面我们已经说到了,改变瞬时占空比,就可以改变音量大小,我们在示例基础上,直接做了一个修改,实际应用中,就可以根据数值调整百分比,直接上一下有关部分的代码:

#defineVOL_MAX256// 改变 audio_vol 的值改变声音大小 0 静音-> 256 最大uint16_taudio_vol=64;// 0静音,256最大for(uint32_ti=0;i<(out_len>>1);i++){int16_traw=pcm_buf[i];// 音量缩放核心行int32_tvol_pcm=(int32_t)raw*audio_vol/VOL_MAX;if(vol_pcm>0){*p_data=vol_pcm/8;*p_data2=0;}else{*p_data2=(0-vol_pcm)/8;*p_data=0;}p_data++;p_data2++;}

二、 PCM 播放测试

OK,上面把一些需要了解的关键点理了一下,我们本文还要测试一下 PCM 播放 (实际应用中直接处理 PCM 的情况应该不会太多,因为不压缩数据量实在太大了,但是既然测试了,那就记录一下,而且可以方便的用来验证上面我们提到的原理部分的一些内容)。

2.1 PCM 数据说明

我们知道 PCM 就是原始的音频数据,我们嵌入式语音采样就是采样的原始的音频数据。 但是呢,因为原始的音频数据很大,一般应用中,都不太可能本地存储下来,除非是明确知道语音数据不大,Flash 空间足够,往往都是采集到了传输出去,而且很多时候都会压缩传输。这是采集相关的话题,后期我们应用可能需要分析说 。

PCM 音频理论上来说的占用空间 :

16k:
1 秒:32000 Byte ≈ 31.25 KB
5 秒:160000 Byte ≈ 156.25 KB
10 秒:320000 Byte ≈ 312.5 KB
15 秒:480000 Byte ≈ 468.75 KB
8K 比 16K 小一半

2.2 音频文件生成

PCM 文件使用 ffmpeg 工具可以直接生成,而且电脑端通过 ffplay 可以直接验证,这个我们可以先准备好,PC 端可以直接验证 。

生成 PCM 指令:

# 把mp3 转成 8K采样率 的 单声道 PCMffmpeg-i004.mp3-ar8000-ac1-fs16le 004p8k.pcm# 把 mp3 转成 16K采样率 的 单声道 PCMffmpeg-i004.mp3-ar16000-ac1-fs16le 004p16k.pcm# 把 mp3 的前 5S 转成 16K采样率 的 单声道 PCMffmpeg-t5-i004.mp3-ar16000-ac1-fs16le 004p16k5s.pcm

查看 PCM 大小:

#windows 下dir004p8k.pcm

示例如下:

C:\Users\OWNER\Desktop\Demo抽烟报警>dir 004p8k.pcm 驱动器 C 中的卷是 系统 卷的序列号是 6E0E-5AEB C:\Users\OWNER\Desktop\Demo抽烟报警 的目录2026/06/2617:1381,502004p8k.pcm1个文件81,502字节0个目录50,758,230,016 可用字节 C:\Users\OWNER\Desktop\Demo抽烟报警>dir 004p16k.pcm 驱动器 C 中的卷是 系统 卷的序列号是 6E0E-5AEB C:\Users\OWNER\Desktop\Demo抽烟报警 的目录2026/06/2617:13163,004004p16k.pcm1个文件163,004字节0个目录50,757,672,960 可用字节

PCM 文件这里 dir 查出来的大小是真实的大小。

使用如下指令可以在 PC 端直接播放 PCM 进行测试 :

#8K 单声道 s16le#-ch_layout mono:单声道,替代旧版 -ac 1ffplay-fs16le-ar8000-ch_layoutmono 004p8k.pcm#16K 单声道 s16leffplay-fs16le-ar16000-ch_layoutmono 004p16k.pcm

最后把 PCM 转成 Hex 文件:

srec_cat.exe 004p8k.pcm-binary-offset32768-o004p8k_h.hex-intelsrec_cat.exe 004p16k.pcm-binary-offset32768-o004p16k_h.hex-intel或者 bin2hex.exe--offset=32768004p8k.pcm 004p8k_h.hex

2.3 APP修改测试

现在存放的就是原始 PCM 数据,解压代码也不需要,从 Flash 读出来数据直接交给 PWM 输出就行了,简单修改了一下,直接上一下有关代码:

caseCH5XX_PWM_DMA_GET:{uint32_t*p_data=p_event->p_data;uint32_t*p_data2=p_event->p_data2;uint32_tlength=p_event->len;// 需要填充的采样点数switch(read_data_sta){case0:{// 计算还剩多少字节PCM数据uint32_tremain_bytes=sample.length-read_index;uint32_tneed_bytes=length*2;// 每个采样16bit=2字节if(remain_bytes==0){read_data_sta=1;break;}// 实际能读的字节数uint32_tcopy_bytes=(remain_bytes<need_bytes)?remain_bytes:need_bytes;uint32_tcopy_samples=copy_bytes/2;// 直接从Flash读PCM数据// 关键修复:Flash数据拷贝到RAM再读取// 使用__wrap_memcpy高速拷贝Flash数据到RAM缓存__wrap_memcpy(pcm_buf,(uint8_t*)(sample.start_addr+read_index),copy_bytes);for(uint32_ti=0;i<copy_samples;i++){int16_tpcm_sample=pcm_buf[i];if(pcm_sample>0){*p_data=pcm_sample/8;*p_data2=0;}else{*p_data2=(0-pcm_sample)/8;*p_data=0;}p_data++;p_data2++;}read_index+=copy_bytes;// 如果数据不够,补零(静音)for(uint32_ti=copy_samples;i<length;i++){*p_data++=0;*p_data2++=0;}if(read_index>=sample.length){PRINT("PCM play end\r\n");ch5xx_audio_stop();}}break;case1:ch5xx_audio_stop();break;default:break;}}break;

因为音频效果文章不好放,我就把我测试过程和结果描述一下。

先测试 16K 播放,就测试一个音频:

audio_record_tmusic_record[]={{0x000000,163004},// 大小按照原始大小};ch5xx_audio_init(CH5XX_AUDIO_SAMPLE_RATE_16K,audio_handler);

可以正常播放,只是声音感觉没有直接 SBC 解码好,但是语音完全够用。

再测试一下 8K :

audio_record_tmusic_record[]={{0x000000,163004},// 大小按照原始大小};ch5xx_audio_init(CH5XX_AUDIO_SAMPLE_RATE_8K,audio_handler);

我开始以为会多给了后半段会破音,但是测试下来,后半段没有尖锐的声音 ,这个在代码中处理好了,声音差了很多,语音也能听清楚,但是杂音比较明显。

因为改到了 8K, 我们的 PWM 最大重装载值翻倍了,我们可以改成如下方式把声音放大:

if(pcm_sample>0){*p_data=pcm_sample/4;*p_data2=0;}else{*p_data2=(0-pcm_sample)/4;*p_data=0;}

结语

本文我们简单说明了一下双 PWM 音频播放的基本原理,虽然并没有对着完全的源码分析,但是对于我们应用需要修改到的地方都有提到,不仅是在 CH585 上面,所有的 MCU 使用此方式驱动音频对应部分都是类似的处理。

之后我们还测试了 PCM 的播放,总的来说,作为语音播放来说 16K 完全可以满足需求,8K 的声音就没有那么清晰,会有杂音 (个人测试,仅供参考,与自己应用层代码处理也有关系) 。

对于语音播放,博主还计划要测试一下 ADPCM 解码播放说明,毕竟 ADPCM 也是嵌入式常用编解码方式。

好了,本文就到这里。谢谢大家!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询