STM32 USB声卡设计

前言

根据我日常使用的需求,需要设计一款Type-C连接的外置USB声卡,用于播放声音。本质上就是把USB Audio协议的数字音频信号转换成模拟信号,并从3.5mm耳机孔输出。本文将详细介绍这款USB声卡的设计、调试过程,为之后的开发积累经验。

构思

设计指标

这款声卡的音质尽可能好,尽量达到HiFi的标准。采样率不需要很大,48K即可,较高的采样率会导致Windows端超采样音源,得不偿失。位深度也不需要太大,16bit即可。最好能够兼容多重采样率和位深度。此外,这款声卡不允许有任何可闻底噪。

方案选择

DAC芯片

市面上的DAC芯片很多,例如近期比较火热的CS43198,经久不衰的旗舰AK4499、ES9018,性价比较高的ES9038Q2M、PCM27xx之类的。
我需要保证以下几点:

  • 价格不高,20元以内,尽可能便宜一些
  • 易于购买,至少在tb有多家可靠店铺售卖
  • 支持的采样率和位深度满足需求
  • 具有控制接口,能够在硬件上控制音量
    综合各方面因素,我选择CS4398这款音频专用DAC芯片。首先其价格不贵,12元一片,支持24bit 192k,远远超过我的需求。这款芯片是十年前Cirrus Logic的旗舰芯片,有很多顶尖台式解码器使用这个芯片,音质应该没有问题。

数字界面

几乎所有的音频DAC都需要I2S接口输入数据,I2S接口既包含音频数据,也包含该数据对应的采样率等信息,甚至可以为DAC芯片提供运行时钟。但是电脑是没有I2S接口的,这款声卡需要把电脑的USB音频信号转成I2S信号输出给DAC。这个过程是由数字界面完成的。
高端的数字界面一般都是使用Amanero或者XMOS的专用芯片,但这些芯片不仅贵的离谱而且非常不好购买。此外还有CT7601、CM108之类的专用芯片,价格还算可以接受,但是仍然不好购买。
我认为,数字的信号,传输过程是不会出现失真的,任何芯片都不可能把本来是0xC3的数据传输成0xC4,所以我想在数字界面上节省开支。我最终选择使用STM32作为数字界面芯片,其具有USB-OTG接口和I2S接口,只需要编写程序把数据搬移到I2S即可。

耳放

耳机放大器其实就是运算放大器,是3.5mm耳机接口前的最后一级,负责把DAC输出的模拟信号进行变换(I/V转换)、放大(或缩小)、滤波(低通滤波器),以便输出给耳机。由于我选择的DAC内部自带IV转换,输出的直接就是电压信号,所以运放只需要进行放大和滤波就足够。我选择的是TI家的OPA1642音频专用运放,主要看中他的输出电流不小,噪声很小的特点。

电源

HiFi设备的电源需要非常认真的设计,因为电源的抖动会直接耦合到模拟信号上,造成失真和底噪。由于不想引入额外噪声,我没有使用常用的隔直电容耦合音频信号,而是给整个系统引入负电源,使其具备双极性输出的能力。
这时候我发现了一款非常合适的芯片——LM27762。它具有正负双LDO,以及负电源电荷泵。只需五个外部电容,即可实现+5V转正负双电源,而且都是LDO输出的电源,可以直接作为模拟电路的供电。这款芯片看起来非常优秀,但是就是因为这个芯片的使用,给后面的调试带来了很大的困难。
MCU和DAC数字电源使用一个ME6211产生3.3V,没有什么可说的。

硬件设计

时钟

CS4398这款DAC需要I2S提供MCLK信号作为其工作的主时钟。但是STM32的I2S接口如果输出MCLK的话,时钟精度就会下降。例如,I2S设置48kHz的采样率,实际上,由于时钟误差,他的采样率是47.991kHz。这个采样率直接决定了I2S输出数据的速度。所以,这个采样率误差会导致I2S发送数据慢了9Hz。假设USB以48.000kHz输出数据,那么I2S就发不完USB传来的数据,最终会导致缓冲区溢出。所以,最好的办法是给STM32的I2S提供一个专用时钟,比如使用24.576MHz的晶振。在我的设计中,没有采用这个方案,仍然是8MHz晶振通过PLL产生I2S MCLK。通过算法解决缓冲区溢出的问题。

电源

电源电路的设计使用了0欧电阻隔离数字地和模拟地,用磁珠隔离VBUS和+5V,减少+5V的噪声。由于DAC需要+5V作为模拟供电,最简单的办法就是从VBUS获取,但是VBUS耦合了电脑的噪声,因此需要对其进行滤波。也可以采用升压+LDO的方式,但是显然会增加系统复杂度,这次就不考虑了。

原理图

软件开发

软件主要使用STM32CubeMX生成。其设置如下。
配置的时候需要把Middleware里面的USB DEVICE CLASS启用,选择USB Audio Class。PID和VID可以根据需要修改,设备名也可以自定义。这里我给设备起名叫做STM32 Audio Card
其次需要配置I2S的DMA,启用后,Mode保持默认,启用FIFO,Threshold设为Full,设置RAM的Burst Size为8 Increment,Peripheral的Burst Size设为No Increment。
配置完成之后,首先要实现USB数据的转发。STM32的USB Class库封装了AUDIO_AudioCmd_FS函数,在接收USB数据后会自动调用这个函数,通过传入不同的Cmd,执行不同的功能,修改这个函数,在其中增加对HAL_I2S_Transmit_DMA()的调用即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
static int8_t AUDIO_AudioCmd_FS(uint8_t* pbuf, uint32_t size, uint8_t cmd)
{
switch(cmd)
{
case AUDIO_CMD_START:
HAL_I2S_Transmit_DMA(&hi2s1, pbuf, size);
break;
case AUDIO_CMD_PLAY:
HAL_I2S_Transmit_DMA(&hi2s1, pbuf, size);
break;
}
return USBD_OK;
}

然后,由于USB Audio的Sync函数需要在I2S传输完成后调用,因此增加I2S的半满和全满中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void HAL_I2S_TxHalfTransferCpltCallback(I2S_HandleTypeDef *hi2s)
{
if(hi2s == &hi2s1)
{
HalfTransfer_Callback_FS();
}
}
void HAL_I2S_TxTransferCpltCallback(I2S_HandleTypeDef *hi2s)
{
if(hi2s == &hi2s1)
{
TransferComplete_Callback_FS();
}
}

此外,需要通过I2C接口对CS4398进行操作。根据数据手册,编写CS4398的驱动代码如下。

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
void CS4398_Write(uint8_t reg, uint8_t data)
{
extern I2C_HandleTypeDef hi2c1;
uint8_t txData[2] = {reg, data};

HAL_I2C_Master_Transmit(&hi2c1, CS4398_ADDR, txData, 2, 100);
}

/*
* @brief Initialize the CS4398
* @param None
* @retval None
* @note By calling this function, the CS4398 is enabled and the volume is set to minimum
*/
void CS4398_Init(void)
{
CS4398_Write(0x09, 0xC0); // Enable Control Interface
HAL_Delay(1);
CS4398_Write(0x03, 0x89); // Equal A and B volume
HAL_Delay(1);
CS4398_Write(0x05, 255); // Set volume to minimum
HAL_Delay(1);
CS4398_Write(0x07, 0xB0); // Set soft ramp mode
}

/*
* @brief Set the volume of the CS4398
* @param volume: 0x00 to 0xFF
* @retval None
* @note 0x00 is the maximum volume, 0xFF is the minimum volume
* The volume is set to both channels
*/
void CS4398_SetVolume(uint8_t volume)
{
CS4398_Write(0x05, volume);
}

/*
* @brief Mute the CS4398
* @param None
* @retval None
* @note CS4398 is muted in hardware
*/
void CS4398_Mute(void)
{
CS4398_Write(0x04, 0x18);
}

/*
* @brief Unmute the CS4398
* @param None
* @retval None
* @note CS4398 is unmuted in hardware
*/
void CS4398_Unmute(void)
{
CS4398_Write(0x04, 0x00);
}

调试

焊接效果

硬件调试

  1. 上电测试
    万用表测试+5V和GND没有短路之后,使用可调电源给板子提供5V工作电压。观察到电流0.02A,3.3V、+4.2V、-4.2V均正常输出。
  2. USB供电测试
    使用USB供电之后,问题就出现了。刚才正常的+4.2V、-4.2V现在不正常了,输出电压呈现非常大幅度的变化,大体呈现三角波的形状。
    测试进行到这里,遇到了很大的问题。经过排查,发现问题出在USB的VBUS供电上。使用可调电源提供供电的时候,+5V波动较小,使用电脑USB供电时,由于USB的瞬时电流提供能力不足,出现掉压的现象,掉压幅度达到了200mV。
    进一步排查发现,USB VBUS的掉压是周期性的,频率大约为30kHz。这个现象不算很奇怪,很有可能是开关电源导致的瞬时电流拉低了VBUS电压。整个电路中只有电荷泵是开关电源,问题只可能出在这里。但是这样就说不通了,电荷泵的开关频率是2.2MHz,怎么可能出现30kHz左右的波动呢?
    改回可调电源供电,用示波器查看LM27762的电荷泵电容,可以看到一个特殊的现象,LM27762先工作在连续开关模式,这时电流是连续的,然后工作在PFM模式,开关波形始终为高电平,此时电流为0,一段时间后,从PFM模式转变为连续开关模式,这个过程会导致电流从0突变为一个大于0的值,过一段时间后再次进入PFM模式。这个模式变换的频率,与VBUS掉压的频率完全相符。观察他们的对应关系发现,正是PFM转换成连续开关模式的一瞬间,产生了VBUS的掉压。问题的源头找到了。
    下一个困难就是,怎么解决这个问题呢?PFM是开关电源在轻载状况下提高效率、降低损耗用的,只要提高输出电流就肯定能解决这个问题。但是这个电荷泵的输出电流有250mA,在我的电路中,他只给运放提供负电源,工作电流1mA可能都没有。如果硬要把电流提高,那么损耗也太大了点,99%的电流都会损耗掉。
    目前没有更好的办法,只能先并联电阻提高输出电流。将一个100欧姆的电阻并联在-4.2V的输出上,用来提高输出电流。观察开关波形发现,PFM的切换频率提高了,也就是说,提高电流会加快PFM的变换,那么再提高点是不是就一直出在PWM模式了。
    再并联一个100欧姆的电阻并联在-4.2V的输出上,形成50欧姆左右的负载,电流大约10mA,PFM变换速率更快了,但是还是无法满足要求。
    上网查找资料,发现在TI的E2E论坛上,有人提出过这个问题。参考https://e2e.ti.com/support/power-management-group/power-management/f/power-management-forum/1054414/lm27762-pfm-and-constant-switching-frequency-for-light-load上的方案。首先将Cp减少一半,没什么效果,仍然会工作在PFM和PWM快速变换的状态。其次,在Cp上并联100欧姆电阻,直接在电荷泵上消耗电流,并联完成之后,USB供电也能够正常工作了。
    这个问题可能是芯片设计缺陷,也很有可能是我的PCB走线有点太随意了,LM27762的供电过了好几个过孔,进一步恶化了瞬态电流相应。后续可以重新设计PCB,有可能能解决这个问题。此外,LM27762的VIN电容最好能够加大一些,来抵消电感导致的电压突变效应。

软件调试

软件上没有遇到很大的问题。软件烧录完成后,插上耳机,就能够正常听到电脑的声音了。
STM32官方的USB Audio库在软件上解决了电脑和声卡音频频率不匹配的问题,具体解决的方法是如果检测到I2S发送的较慢,则跳过4个数据点,从第五个开始发送,来弥补时钟频率慢导致的发送速度慢。这种方法显然会损失音频的保真程度,而且会导致音频不连续的问题。但是实际测试下来,正常播放音乐、视频、玩游戏,声音并没有可闻瑕疵。这一点是USB Audio的硬伤,到目前没有什么很好的解决方案。

存在问题

  1. CS4398的输出幅值太高了,耳放也没有对其进行有效的限幅,导致就算CS4398的音量设置成最小(-125dB),插上耳机声音仍然很大,导致软件上必须把CS4398的音量设置成最小。
  2. 如果进入游戏,然后强制退出游戏,游戏的声音会在耳机上不断播放,就像卡带了一样。这个原因主要是USB突然不再发送音频了之后,STM32的DMA并不会停止发送,会一直发送缓冲区中的数据。这点最终没有再花时间解决,因为只要随便播放一个音频,就能够覆盖掉缓冲区的内容,就不会再重复播放了。不算是什么大问题
  3. 重新插上声卡后,电脑上需要较长的时间识别STM32声卡,这段时间无法使用。原因应该是没有在USB D+上添加上拉电阻,导致电脑只能通过定时的硬件扫描找到声卡,无法实现插入瞬间的识别。这个问题目前没有解决的计划,由于我的声卡永远插在电脑上,开机的时候就识别好了,不影响使用。
  4. 播放单一频率音频时,会有规律性的电流“嗒嗒”声音。这是由于I2S频率不是精准的48kHz,导致USB发送的数据无法被I2S全部转发,算法上发现缓冲区将满,强制跳过了4个采样点,导致的声音不连续。但是正常播放音乐或者声音是没有这种情况的,不影响使用。想要解决也简单,给I2S提供专用时钟或者直接把STM32的工作晶振改成24.576MHz,这样能够产生一个没有误差的48kHz采样时钟。

总结

这次STM32的声卡制作花费了一定的时间,而且遇到了不少的问题。完成了硬件和软件的开发调试之后,我也为他制作了一个简单的外壳,安装到位之后,想要修改程序压根不可能了。后续如果想要改版,解决一些问题的话,就得重新设计PCB了。整体上,音质效果很好,听下来没有什么瑕疵,动态范围也还可以,安静情况下没有底噪,整体上达到了设计指标和要求。这个项目算得是一个比较成功的项目。

作者

Zhu Wenguan

发布于

2025-03-01

更新于

2025-06-07

许可协议