CH585 PCM 播放测试以及双 PWM 原理

复制代码
简单聊一下双 PWM 音频播放原理及实现以及 PCM 播放测试     ...... 矜辰所致

前言

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

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

相关文章:

嵌入式语音开发应用基础说明

CH585 SBC 音频解码播放测试说明

.

我是矜辰所致,全网同名,尽量用心写好每一系列文章,不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开!

目录

  • 前言
  • [一、 双 PWM 原理](#一、 双 PWM 原理)
    • [1.1 基础说明](#1.1 基础说明)
    • [1.2 程序中双 PWM 的体现](#1.2 程序中双 PWM 的体现)
    • [1.3 PWM 的频率](#1.3 PWM 的频率)
    • [1.4 音量调节](#1.4 音量调节)
  • [二、 PCM 播放测试](#二、 PCM 播放测试)
    • [2.1 PCM 数据说明](#2.1 PCM 数据说明)
    • [2.2 音频文件生成](#2.2 音频文件生成)
    • [2.3 APP修改测试](#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 的体现

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

c 复制代码
for(uint32_t i=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

c 复制代码
// 根据采样率选择固定计数值
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 音量调节

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

c 复制代码
#define VOL_MAX 256
// 改变 audio_vol 的值改变声音大小 0 静音-> 256 最大
uint16_t audio_vol = 64; // 0静音,256最大

for(uint32_t i=0;i<(out_len>>1);i++) 
{
    int16_t raw = pcm_buf[i];
    // 音量缩放核心行
    int32_t vol_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 指令:

bash 复制代码
# 把mp3 转成 8K采样率 的 单声道 PCM
ffmpeg -i 004.mp3 -ar 8000 -ac 1 -f s16le 004p8k.pcm  
# 把 mp3 转成 16K采样率 的 单声道 PCM 
ffmpeg -i 004.mp3 -ar 16000 -ac 1 -f s16le 004p16k.pcm 
# 把 mp3 的前 5S 转成 16K采样率 的 单声道 PCM  
ffmpeg -t 5 -i 004.mp3 -ar 16000 -ac 1 -f s16le 004p16k5s.pcm    

查看 PCM 大小:

bash 复制代码
#windows 下
dir 004p8k.pcm

示例如下:

bash 复制代码
C:\Users\OWNER\Desktop\Demo抽烟报警>dir 004p8k.pcm
 驱动器 C 中的卷是 系统
 卷的序列号是 6E0E-5AEB

C:\Users\OWNER\Desktop\Demo抽烟报警 的目录

2026/06/26  17:13            81,502 004p8k.pcm
               1 个文件         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/26  17:13           163,004 004p16k.pcm
               1 个文件        163,004 字节
               0 个目录 50,757,672,960 可用字节

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

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

bash 复制代码
#8K 单声道 s16le
#-ch_layout mono:单声道,替代旧版 -ac 1
ffplay -f s16le -ar 8000 -ch_layout mono 004p8k.pcm
#16K 单声道 s16le
ffplay -f s16le -ar 16000 -ch_layout mono 004p16k.pcm

最后把 PCM 转成 Hex 文件:

bash 复制代码
srec_cat.exe 004p8k.pcm -binary -offset 32768 -o 004p8k_h.hex -intel
srec_cat.exe 004p16k.pcm -binary -offset 32768 -o 004p16k_h.hex -intel
或者
bin2hex.exe --offset=32768 004p8k.pcm 004p8k_h.hex

2.3 APP修改测试

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

c 复制代码
case CH5XX_PWM_DMA_GET:
   {
       uint32_t *p_data = p_event->p_data;
       uint32_t *p_data2 = p_event->p_data2;
       uint32_t length = p_event->len;  // 需要填充的采样点数
       
       switch(read_data_sta){
       case 0:
           {
               // 计算还剩多少字节PCM数据
               uint32_t remain_bytes = sample.length - read_index;
               uint32_t need_bytes = length * 2;  // 每个采样16bit=2字节
               
               if(remain_bytes == 0) {
                   read_data_sta = 1;
                   break;
               }
               
               // 实际能读的字节数
               uint32_t copy_bytes = (remain_bytes < need_bytes) ? remain_bytes : need_bytes;
               uint32_t copy_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_t i = 0; i < copy_samples; i++) {
                   int16_t pcm_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_t i = 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;
       case 1:
           ch5xx_audio_stop();
           break;
       default:
           break;
       }
   }
   break;

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

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

c 复制代码
audio_record_t music_record[] = {      
    { 0x000000, 163004 },  // 大小按照原始大小
};

ch5xx_audio_init(CH5XX_AUDIO_SAMPLE_RATE_16K,audio_handler);

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

再测试一下 8K :

c 复制代码
audio_record_t music_record[] = {      
    { 0x000000, 163004 },  // 大小按照原始大小
};

ch5xx_audio_init(CH5XX_AUDIO_SAMPLE_RATE_8K,audio_handler);

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

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

c 复制代码
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 也是开源的,而且对单片机也是很友好的。

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