USB声卡噪音问题,USB声卡中文名设置,基于STM32F411

目录

1.USB声卡噪音问题

 1.1USB声卡噪音问题解决

1.2USB消除噪声测试

2.USB声卡中文名设置

2.1USB想要中文名,咋整

3.晒晒板子


1.USB声卡噪音问题

使用STM32F411的USB库直接配置声卡操作流程非常简单,但是仔细听了一段时间后发现有些瑕疵,而且是不能容忍的瑕疵。

STM32F411配置USB声卡请见:STM32 USB声卡 CUBEMX配置 极简配置十分钟解决 STM32+PCM5120A_Fairchild_1947的博客-CSDN博客

出现的瑕疵主要体现在播放一段时间后,会出现噪音,而且这个出现噪音的时间是有规律的。至于原因,其实仔细想想就能得出答案:

虽然电脑和声卡都设置了相同的采样率(如48KHZ),但是由于时钟产生的源头不同,暂不考虑相位的问题,就单单频率也不可能完全相同,这就会导致电脑和USB声卡两者在没有任何干预的情况下是不可能正常配合工作的。

说简单点,就是电脑发送的数据没有和单片机(USB设备端)这边同步,然后I2S读的数据错位了,于是就有噪音了。

至于STM32F4系列的I2S频率配置,可以参考下表(SAI的配置和I2S不一样,不要用这个表配置SAI):

 1.1USB声卡噪音问题解决

USB声卡同步问题其实是做USB声卡躲不过去的一个难点。USB声卡常见的的有Synchronous、Adaptive、Asynchronous。其中Asynchronous,即异步方式需要有电脑端的驱动程序支持,这也正是为什么XCOM数字界面需要额外安装驱动的原因。而Synchronous方式需要与USB 的SOF帧同步,放到STM32上就是,USB外设接收的SOF帧和I2S同步,这个硬件在STM32内部是没有的。因此STM32最终使用的依然是使用最广泛的Adaptive方式。

开启改同步方式的关键之处在于对函数USBD_AUDIO_Sync(在usbd_audio.c)的理解和使用。与之对应的是函数HalfTransfer_CallBack_FS(在usbd_audio_if.c)和TransferComplete_CallBack_FS(在usbd_audio_if.c)函数,这两个函数均是USBD_AUDIO_Sync的再次封装,分别在I2S半传输中断和完全传输中断时调用,并在此函数中计算从USB接收的数据的数量和I2S发送出去的数量,从而决定如何同步。USBD_AUDIO_Sync(在usbd_audio.c)的源码如下。

/**
  * @brief  USBD_AUDIO_SOF
  *         handle SOF event
  * @param  pdev: device instance
  * @retval status
  */
void USBD_AUDIO_Sync(USBD_HandleTypeDef *pdev, AUDIO_OffsetTypeDef offset)
{
  USBD_AUDIO_HandleTypeDef *haudio;
  volatile uint32_t BufferSize = AUDIO_TOTAL_BUF_SIZE / 2U;

  if (pdev->pClassData == NULL)
  {
    return;
  }

  haudio = (USBD_AUDIO_HandleTypeDef *)pdev->pClassData;

  haudio->offset = offset;

  if (haudio->rd_enable == 1U)
  {
    haudio->rd_ptr += (uint16_t)BufferSize;

    if (haudio->rd_ptr == AUDIO_TOTAL_BUF_SIZE)
    {
      /* roll back */
      haudio->rd_ptr = 0U;
    }
  }

  if (haudio->rd_ptr > haudio->wr_ptr)
  {
    if ((haudio->rd_ptr - haudio->wr_ptr) < AUDIO_OUT_PACKET)
    {
      BufferSize += 4U;
    }
    else
    {
      if ((haudio->rd_ptr - haudio->wr_ptr) > (AUDIO_TOTAL_BUF_SIZE - AUDIO_OUT_PACKET))
      {
        BufferSize -= 4U;
      }
    }
  }
  else
  {
    if ((haudio->wr_ptr - haudio->rd_ptr) < AUDIO_OUT_PACKET)
    {
      BufferSize -= 4U;
    }
    else
    {
      if ((haudio->wr_ptr - haudio->rd_ptr) > (AUDIO_TOTAL_BUF_SIZE - AUDIO_OUT_PACKET))
      {
        BufferSize += 4U;
      }
    }
  }

  if (haudio->offset == AUDIO_OFFSET_FULL)
  {
    ((USBD_AUDIO_ItfTypeDef *)pdev->pUserData)->AudioCmd(&haudio->buffer[0],
                                                         BufferSize, AUDIO_CMD_PLAY);
    haudio->offset = AUDIO_OFFSET_NONE;
  }
}

 仔细看这段代码用结构体指针访问其成员时的成员名称,主要出现了rd_ptr和wr_ptr两个,从名字可以看出,rd_ptr是读相关的wr_ptr是写相关的,至于读写谁是读者谁是写着,主要看谁是主人公。这里是USB的地盘儿,主人公当然是USB咯。所以写是写入USB,读是从USB读出。

接下来简单分析这个功能是个什么原理:

首先这个函数是I2S的DMA在半传输和完全传输时调用的,所以进入该函数时rd_ptr的值是确定的,即缓冲区大小的一半或者缓冲去大小。而wr_ptr的值来自于从USB的数据点读取的实际数据数量,具体可以看函数USBD_AUDIO_DataOut(在usbd_audio.c)。通过rd_ptr和wr_ptr两者之间的关系得出现在是I2S发送的进度超前了USB发送数据还是落后了。例如rd_ptr大于wr_ptr,证明在相同的时间内I2S发送的进度超前了,反之则落后了。如果超前了,那就多发点来拖时间,如果落后了那就发少点,追上来。这个多少就是由BufferSize决定的,正如上面代码里面的BufferSize大小变化,这个大小变化将会直接传入函数AudioCmd,而此函数的调用正是对应着此前第一次配置USB声卡时的重要函数AUDIO_AudioCmd_FS(在usbd_audio_if.c)且该函数传入的参数注意是“AUDIO_CMD_PLAY”,记着这个参数,我们接着来看函数AUDIO_AudioCmd_FS(在usbd_audio_if.c)

/**
  * @brief  Handles AUDIO command.
  * @param  pbuf: Pointer to buffer of data to be sent
  * @param  size: Number of data to be sent (in bytes)
  * @param  cmd: Command opcode
  * @retval USBD_OK if all operations are OK else USBD_FAIL
  */
static int8_t AUDIO_AudioCmd_FS(uint8_t* pbuf, uint32_t size, uint8_t cmd)
{
  /* USER CODE BEGIN 2 */
	extern osSemaphoreId_t I2S_TX_BUFF0Handle;
	extern osSemaphoreId_t I2S_TX_BUFF1Handle;
extern osSemaphoreId_t AudioDecoder_BUFF0Handle;
extern osSemaphoreId_t AudioDecoder_BUFF1Handle;	
	static uint8_t choose=0;
  switch(cmd)
  {
    case AUDIO_CMD_START:
		AudioCard_Play((uint16_t*)pbuf, size);
    break;

    case AUDIO_CMD_PLAY:
		AudioCard_Countinue_Play((uint16_t*)pbuf, size);
    break;
  }
  UNUSED(pbuf);
  UNUSED(size);
  UNUSED(cmd);
  return (USBD_OK);
  /* USER CODE END 2 */
}

可以看到“AUDIO_CMD_PLAY”参数是switch中的第二个选项,而第一个选项正是第一次启动USB声卡时的初始化选项。接下来看后一个参数“size”,该参数意为I2S的DMA传输的数量,我起初认为I2S的DMA传输长度是不会变的,没有理会这个参数,直到最后才发现,这个参数非常非常重要,I2S的DMA传输长度是需要变化的!这个参数直接对应函数USBD_AUDIO_Sync(在usbd_audio.c)中根据I2S的传输进度和USB接收数据的进度计算得出的BufferSize。

1.2USB消除噪声测试

这样修改后就可以基本消除USB声卡的噪音了。注意I2S配置DMA的时候不要使用循环模式!

其次,ST的USB提供的这个方法是固定步长的调整效果不是很理想,I2S的频率和USB的频率差值一定要小,而且调整过程长,大概需要20S。

2.USB声卡中文名设置

CUBEMX生成的代码在连接电脑后,会这样显示

初期感觉,显示STM32就感觉挺有趣的,哈哈,不过看久了,也就没意思了,我想自己取个名字,就像这样的:

 这个图不是P的哦,这个是最后的结果,哈哈。

2.1USB想要中文名,咋整

首先,找到ST的USB库中定义USB设备名称和厂商名称的地方,在usbd_desc.c文件的开头处,对应在CUBEMX中的设置为此页:

 不过,我们不能直接在CUBEMX里直接改,我们需要自己动手修改usbd_desc.c文件。

其次,usbd_desc.c文件如何改。USB显示名称时候使用的是UNICONE码,中文名字需要通过转换得到UNICONE码,UNICONE的转码软件直接用线上的即可,比如菜鸟教程出的:在线 Unicode 编码转换 | 菜鸟工具

 而且,由于在USB启动时,会将USB的名称当作字符串读取,故会强制转换为8位的数组,所以这里需要对UNICONE码的转换结果再次处理一下,调整顺序,使之成为小端格式,例如:

三 对应的UNICONE码是 \u4e09  ,将其改为小端格式的8位数组时为 0x09,0x4e  。其余字符以此类推,我修改后的文件如下:

/* USER CODE BEGIN PRIVATE_TYPES */
uint8_t PRODUCT_STRING_SYH[]= {0x09,0x4e,0x08,0x67,0xb1,0x82,0xd1,0x79,0x80,0x62,0xf0,0x58,0x61,0x53,0x08,0xff,0x52,0x97,0x25,0x66,0x48,0x72,0x09,0xff,0x00};
uint8_t MANFACTURER_STRING_SYH[] = 	{0x09,0x4e,0x08,0x67,0xb1,0x82,0xd1,0x79,0x80,0x62,0x00};
/* USER CODE END PRIVATE_TYPES */

/**
  * @}
  */

/** @defgroup USBD_DESC_Private_Defines USBD_DESC_Private_Defines
  * @brief Private defines.
  * @{
  */

#define USBD_VID     1155
#define USBD_LANGID_STRING	1033
#define USBD_MANUFACTURER_STRING     MANFACTURER_STRING_SYH
#define USBD_PID_FS     22337
#define USBD_PRODUCT_STRING_FS     PRODUCT_STRING_SYH
#define USBD_CONFIGURATION_STRING_FS     "AUDIO Config"
#define USBD_INTERFACE_STRING_FS     "AUDIO Interface"

#define USB_SIZ_BOS_DESC            0x0C

由于转换后的数组太长,所以使用宏定义替换。

再次,这样修改之后发现还是会显示乱码,为什么呢?因为英文的UNICONE字符的高字节都是0,所以英文转UNICONE字符时直接加个高字节并且赋值0即可,这个工作在ST的USB库中实现了,这个功能的存在方便了英文名称的显示,但是会使得中文名称无法显示,我们需要修改函数USBD_GetString(在usbd_ctlreq.c)将最后两行注释,具体见下面的代码:

/**
  * @brief  USBD_GetString
  *         Convert Ascii string into unicode one
  * @param  desc : descriptor buffer
  * @param  unicode : Formatted string buffer (unicode)
  * @param  len : descriptor length
  * @retval None
  */
void USBD_GetString(uint8_t *desc, uint8_t *unicode, uint16_t *len)
{
  uint8_t idx = 0U;
  uint8_t *pdesc;

  if (desc == NULL)
  {
    return;
  }

  pdesc = desc;
  *len = ((uint16_t)USBD_GetLen(pdesc) * 2U) + 2U;

  unicode[idx] = *(uint8_t *)len;
  idx++;
  unicode[idx] = USB_DESC_TYPE_STRING;
  idx++;

  while (*pdesc != (uint8_t)'\0')
  {
    unicode[idx] = *pdesc;
    pdesc++;
    idx++;

//    unicode[idx] = 0U;
//    idx++;
  }
}

这样修改之后,中文可以正常显示了,但是如果显示的是中英文混合的名称,还是会出问题,因为这样修改由与字符串长度测量的功能相互冲突,具体见函数USBD_GetLen(在usbd_ctlreq.c)。可以修改这个函数,或者直接传递数组长度,因为我没有打算显示中英文混合的名称,所以没有具体做这个功能。

感觉现在完事具备了,不过下载后,重启板子,显示的还是老名字,此时不要气馁,其实已经成功了,只是没有刷新显示出来而已。这里有两种解决的方法:

1.进入电脑的设备管理器,右键点击卸载设备,卸载后,再在空白处右键选择扫描硬件设备改动,此时便会自动连接USB声卡,并重新安装驱动,安装好驱动后,发现名字也更新过来了;

2.修改设备编号(PID),这个在usbd_desc.c文件的开头,修改一个不一样的号即可。

最后便能正常显示中文名称了。

3.晒晒板子

猜你喜欢

转载自blog.csdn.net/Fairchild_1947/article/details/123391638