UDA1334A音频模块简介:
1.该模块工作在I2S协议,也支持LSB 16/20/24数据,理论可以比PCM5102更加保真
2. 只需要关注VIN,GND,WSEL,DIN,BCLK即可,其余引脚都是默认(上一排默认均为低电平),除了:使用其他协议,多级设备级联。
STM32F411CEU6:
一款开源的mini单片机,
用到的I2S-2引脚:
PB10:I2S-CLK 连接模块的 BCLK
PB12:I2S-WS 连接模块的 WSEL
PB15:I2S-SD 连接模块的 DIN
5V 连接模块的 VIN
GND 连接模块的 GND
主要配置和代码:
0.cubemx初始化代码
RCC 使用外置晶振,配置时钟
debug模式sw串口
1.cubemx----usb_otg_fs,配置如图,选择device——only,其余为默认(注意D+的usb需要设置为上拉,实测不上拉也可以,建议上拉)
2.打开usb_device,选择音频设备,将频率设置为48000,其余默认
3.设置I2S,DMA和NVIC,usb默认为0,其余见图,注意DMA为half word,GPIO设置为超高速
4.增大单片机的堆栈大小,确保usb库正常运行
5.生成初始化代码后,打开 usbd_audio_if.c 文件,仅需要简单的修改即可以实现声卡,
打开注释的2行代码,当接收到数据,即开始播放
打开注释的代码,与usb接受的回调函数配合,实现连续播放
6.上述简单实现经常出现破音,因为usb接收和DMA发送不同步造成的,可以在usb接受的回调函数中,搬运走数据,单独播放
然后在I2S回调函数中实现buffer检测和持续播放
usb缓存50ms/帧的数据后开始播放,并在主程序监测两者不匹配的程度
实测,usb接受的快,i2s播放的慢,当mis match达到一定程度后,直接舍弃150个buffer
如果想实现更加无感的数据平滑,当usb接受的数据超过i2s播放的一定程度时,则可以压缩USB接受到的数据帧,逐渐对齐到i2s播放的buffer
以上就可以完成了稳定的USB声卡,我自己懒得调整了,直接暴力舍弃
总结1:
1.由于没有实现暂停、继续和停止播放的控制命令,所以在切换不同声源时,会出现持续播放已经缓存的最后一点缓存声音,导致存在杂音
2.由于直接舍弃了buffer,则发生不匹配时,可以明显感知到破音,可以通过仔细的调整i2s播放的buffer来确保两者同步
3.但是上面这样理论上会导致1ms中有那么1-2个数据质量降低,对于hifi的玩家是不能接受的,实测由于时钟比较精准(i2s = 47.991,失真率为-0.01%),实际很少很少能出现150ms的破音,所以就这样吧
补充:
为了充分发挥这个24bit的音频模块,可以使用以下配置为LSB 24 bit的协议,并且需要修改stm32 的usb音频协议配置
1.i2s协议修改:
2.usb协议修改:
2.1 修改usb audio.c文件,一个pack的大小为288字节,通过以下公式计算:48000(频率)*2(声道数)*3(24bit位数据)/ 1000 = 288字节,将这个字节转化为usb枚举所需要的字节数
#define AUDIO_PACKET_SZE(frq) \
(uint8_t)(((frq * 2U * 3U) / 1000U) & 0xFFU), (uint8_t)((((frq * 2U * 3U) / 1000U) >> 8) & 0xFFU)
修改为3个字节,24bit数据,
2.2 修改usb audio.h文件,找到以下宏定义并修改,以及修改usb库所用的缓存buffer大小
//修改单个buffer的数据大小
#define AUDIO_OUT_PACKET (uint16_t)(((USBD_AUDIO_FREQ * 2U * 3U) / 1000U))
//修改总缓存的buffer的大小
#define AUDIO_OUT_PACKET_NUM 60U
//总的数据大小
#define AUDIO_TOTAL_BUF_SIZE ((uint16_t)(AUDIO_OUT_PACKET * AUDIO_OUT_PACKET_NUM))
2.3 计算usb所用到的栈区内存大小,并且重新修改stm32的堆栈大小
在void USBD_AUDIO_Sync(USBD_HandleTypeDef *pdev, AUDIO_OffsetTypeDef offset)函数中
uint32_t BufferSize = AUDIO_TOTAL_BUF_SIZE / 2U;
可知大小=48 * 2 * 3 * 60 * 2 = 34,560 byte = 0x8700 栈大小需要大于这个数值,本例设置为0x10000,本例堆大小为0x4000
注意由于修改了usb库,如果使用stm32 cubemx重新配置堆栈大小初始化代码,刚才对usb库的修改都会消失!!!!!
3.24bit的usb数据重组为32位封包的24位LSB的数据包:下面有详细说明
需要用到的数组和变量,注意需要在对应的文件内声明以及补完include
uint32_t i2s2_buffer[40][48*2] __attribute__((used));
uint32_t i2s2_nop_buffer[48*2] __attribute__((used));
uint32_t usb_rece_index = 0;
uint32_t usb_rece_all = 0;
uint32_t i2s_play_index = 0;
uint32_t i2s_play_all = 0;
uint8_t i2s_play_flag = 0;
uint32_t dismatch = 0;
/**
* @brief AUDIO_PeriodicT_FS
* @param cmd: Command opcode
* @retval USBD_OK if all operations are OK else USBD_FAIL
*/
static int8_t AUDIO_PeriodicTC_FS(uint8_t *pbuf, uint32_t size, uint8_t cmd)
{
/* USER CODE BEGIN 5 */
//usb接受字节L1L, L1M, L1H 分别是最低有效字节、中间字节和最高有效字节。
//usb 接受的三个比特数据是:01 02 ab //usb数据 0xab0201
// I2S发送这三个比特,数据重组:0x 02(usb中间) 01(usb-lsb) xx(自动归零) ab(msb)
// 实际这个32bit: 0x020100ab 就可以正确的发送usb接受的数据了
for(uint32_t x = 0; x < 96; x++){ //48 x 2 x 3的byte总量
// 直接从缓冲区读取24位数据并按照格式转换为32位整数
uint32_t index = 3 * x; // 3 bytes per sample
uint8_t one_ch[3];
one_ch[0] = *(pbuf+index); //lsb
one_ch[1] = *(pbuf+index+1);
one_ch[2] = *(pbuf+index+2); //msb
// 将24位数据左对齐并存储到I2S缓冲区 // 注意:STM32的I2S通常在传输时使用左对齐格式
i2s2_buffer[usb_rece_index][x] = (one_ch[1]<<24) | (one_ch[0]<<16) | one_ch[2];
} //注意i2s的DMA宽为half word,地址不增加,如果使用fifo,内存数据为word,fifo满,brust=4
usb_rece_all++;
usb_rece_index = usb_rece_all % 40;
if(usb_rece_all==3){
HAL_I2S_Transmit_DMA(&hi2s2,(uint16_t*)i2s2_buffer[i2s_play_index],96); //注意外设为半字宽
}
return (USBD_OK);
/* USER CODE END 5 */
}
4.补完i2s的回调函数,监控缓存buffer,解决破音问题:
void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s)
{
if(hi2s->Instance==SPI2){
//测试下来,usb接收的速度稍微快一点
(usb_rece_index>i2s_play_index)?(dismatch=usb_rece_index-i2s_play_index):\
(dismatch=i2s_play_index-usb_rece_index);
if(dismatch<=1){
HAL_I2S_Transmit_DMA(&hi2s2,(uint16_t*)i2s2_nop_buffer,96);
} //播放一个空buffer,让usb的数据先刷新,相当于舍弃了40ms/个buffer的数据
else{
i2s_play_all++; //完成了1次发送
i2s_play_index=i2s_play_all%40; //生成下个传输的序号
HAL_I2S_Transmit_DMA(&hi2s2,(uint16_t*)i2s2_buffer[i2s_play_index],96);
}
}
}
这样基本没有感知到过破音了