前言
我的项目需要使用一个立体声ADC对运算放大器输出的模拟音频进行读取,并通过USB Audio Class传输到PC。
在群友的指导下,我选择了STM32F042K6T6作为主控,该型号支持硬件I2S,并支持USB Device(FS)。最重要的是,该型号在淘宝售价仅3元(还包邮)。
我选择的ADC型号位GC1808,这是一款低成本立体声音频模数转换器,最高支持96KHz 24bit,I2S接口输出,可通过引脚配置为主/从、飞利浦/MSB左对齐模式。该产品为TI生产的PCM1808的Pin-to-Pin兼容(山寨)芯片(数据手册的图都是从TI手册里截的),且在立创商城的ADC目录中按价格从低到高排名第一。
ADC芯片配置及输出验证
GC1808模块原理图如下图所示。手册要求大电容使用电解电容,我这里为了省空间改成了MLCC,总之能用。三个功能选择引脚接出来,用一坨锡就能修改。
我计划将音频采样率配置为48KHz,即:
选择$$f_{ICK} = 512 \times f_S = 24.576MHz$$
另选择工作在主机模式、数据格式为左对齐、24bit,故将引脚配置为:
引脚 | 电平 |
---|---|
FMT (Pin 12) | 高 |
MD1 (Pin 11) | 低 |
MD0 (Pin 10) | 高 |
由数据手册可知,每一帧由2个声道共32个位组成,故可计算得到BCK(位时钟)的频率为:
LRCK(左右时钟)的频率与输出音频采样率相同,即:
连接示波器,可以看到测试结果与理论值一致,如下图所示。其中,1通道(黄色)为BCK波形,2通道(青色)为LRCK波形。由于学校的包浆示波器探头找不到接地弹簧,我只能使用接地夹子测量,测量波形噪声较多、质量很差。
更换探头位置,1通道测量LRCK波形,2通道测量DOUT(数据输出)波形,结果如下图所示。可以看到,ADC正在发送两个声道的数据,与手册标注的格式一致。
STM32CubeMX配置与接线
为产生ADC所需的时钟信号,我使用了一个24.576MHz的无源晶振,并将整个单片机运行在24.576MHz。这款单片机似乎没有为I2S单独提供时钟的PLL,无法使用I2S的MCO(主时钟输出)功能输出ADC芯片所需的系统时钟,故配置时钟树将HCLK通过MCO引脚输出,连接到ADC的SCKI。时钟树如下图所示。
在左侧选择I2S,页面上方模式设置为半双工从机模式,传输模式修改为从机接收,通信标准为MSB优先左对齐,数据和帧格式为在32位帧上传输的24位数据,音频频率为48KHz。可以看到,得益于专门选择的晶体频率,频率误差为0。如下图所示。
切换到DMA设置标签页。点击加号添加一个DMA请求,STM32CubeMX自动帮我们配置了一些信息。我们需要将DMA设置请求中的模式切换为循环。根据参考手册,I2S接收的寄存器只有16位,32位的一帧需要DMA分两次搬运,故这里的数据宽度都选择半字(16位)。设置如下图所示。
STM32CubeMX已经帮我们分配了各个引脚,从机模式的I2S占用了3个引脚,分别为WS(字选择,输入)、CK(时钟,输入)、SD(串行数据,输入)。根据我们上面的设置,接线方式如下。
STM32 | GC1808 |
---|---|
WS | LRCK |
CK | BCK |
SD | DOUT |
MCO | SCKI |
代码
整体代码如下所示。
# main.c
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>
/* USER CODE END Includes *//* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define I2S_HALF_DMA_CACHE_SAMPLES 32 // 可按需求调整: 表示半次DMA缓冲中每个声道的采样数
/* USER CODE END PD *//* Private variables ---------------------------------------------------------*//* USER CODE BEGIN PV */// 原始DMA接收缓冲,I2S_HALF_DMA_CACHE_SAMPLES * 2声道 * 2个uint16_t * 2(前后分开处理)
static uint16_t i2s_rx_dma_buf[8 * I2S_HALF_DMA_CACHE_SAMPLES];// 分离后的左右声道数据 (存放在 32bit 中, 低 24bit 有效),前后分开处理
static int32_t i2s_left[I2S_HALF_DMA_CACHE_SAMPLES * 2];
static int32_t i2s_right[I2S_HALF_DMA_CACHE_SAMPLES * 2];// 统计静音采样数,调试用
static uint32_t leftzero_cnt = 0;
static uint32_t rightzero_cnt = 0;
/* USER CODE END PV *//* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */// 合并并符号扩展 24bit 数据
static void ProcessI2SBlock(const uint16_t *src_words, uint32_t samples, uint32_t offset)
{for (uint32_t i = offset; i < samples + offset; i++){uint32_t left_word = (uint32_t)src_words[i * 4] << 16 | src_words[i * 4 + 1];uint32_t right_word = (uint32_t)src_words[i * 4 + 2] << 16 | src_words[i * 4 + 3];// 取高24bitint32_t l = (int32_t)(left_word >> 8);int32_t r = (int32_t)(right_word >> 8);// 符号扩展 24bit -> 32bit (若第23位为1则为负数,需填充高位)if (l & 0x00800000u)l |= 0xFF000000u;if (r & 0x00800000u)r |= 0xFF000000u;i2s_left[i] = l;i2s_right[i] = r;// 简单统计静音采样数,调试用if (abs(l) <= 500)leftzero_cnt++;if (abs(r) <= 500)rightzero_cnt++;}
}void HAL_I2S_RxHalfCpltCallback(I2S_HandleTypeDef *hi2s)
{if (hi2s->Instance == SPI1){ProcessI2SBlock(i2s_rx_dma_buf, I2S_HALF_DMA_CACHE_SAMPLES, 0);}
}void HAL_I2S_RxCpltCallback(I2S_HandleTypeDef *hi2s)
{if (hi2s->Instance == SPI1){ProcessI2SBlock(i2s_rx_dma_buf, I2S_HALF_DMA_CACHE_SAMPLES, I2S_HALF_DMA_CACHE_SAMPLES);}
}
/* USER CODE END 0 *//*** @brief The application entry point.* @retval int*/
int main(void)
{/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_DMA_Init();MX_USART1_UART_Init();MX_I2S1_Init();/* USER CODE BEGIN 2 */// 启动 I2S DMA 接收: size为 2 * I2S_HALF_DMA_CACHE_SAMPLES 个 32bit 数据if (HAL_I2S_Receive_DMA(&hi2s1, i2s_rx_dma_buf, 4 * I2S_HALF_DMA_CACHE_SAMPLES) != HAL_OK){Error_Handler();}my_printf("I2S DMA started. FrameSamples=%d\r\n", I2S_HALF_DMA_CACHE_SAMPLES);/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */// 每秒打印一次统计信息static uint32_t tick_last = 0;if (HAL_GetTick() - tick_last > 1000){tick_last = HAL_GetTick();my_printf("leftzero=%lu rightzero=%lu\r\n", leftzero_cnt, rightzero_cnt);for (int i = 0; i < 2 * I2S_HALF_DMA_CACHE_SAMPLES; i++){my_printf("%ld %ld\r\n", i2s_left[i], i2s_right[i]);}leftzero_cnt = 0;rightzero_cnt = 0;}}/* USER CODE END 3 */
}
STM32CubeMX已经帮我们完成了大部分代码,我们只需要完成几个弱定义的函数就可以了。
需要注意的是,在HAL库的用户手册UM1785中,对HAL_I2S_Receive_DMA的描述如下:
When a 16-bit data frame or a 16-bit data frame extended is selected during the I2S
configuration phase, the Size parameter means the number of 16-bit data length in the
transaction and when a 24-bit data frame or a 32-bit data frame is selected the Size
parameter means the number of 16-bit data length.
但这个描述是错误的。在STM32Cube_FW_F0_V1.11.5\Drivers\STM32F0xx_HAL_Driver\
目录下的chm文档中,对HAL_I2S_Receive_DMA的描述如下:
When a 16-bit data frame or a 16-bit data frame extended is selected during the I2S configuration phase, the Size parameter means the number of 16-bit data length in the transaction and when a 24-bit data frame or a 32-bit data frame is selected the Size parameter means the number of 24-bit or 32-bit data length.
我也让了Copilot对HAL库的.c文件作了解读,看起来HAL库会根据配置自动处理,这里只需要填写24或32比特数据的长度即可。
此外,ST的文档中似乎只字未提左右声道的问题。根据ADC的数据手册,在I2S协议中,左声道数据总是先于右声道进行传输。为了验证读取的i2s_rx_dma_buf中的规律,我加了几个函数进行调试。
运行后,程序可以正常打印两个声道的int32数据,左列位左声道,右列为右声道,如下图所示。可以看出,两个声道的数值都很接近0。
用手指触摸13引脚对应的电容,数据变化如下。可以看到,左列数据发生了较大波动,不再接近0。
经过多次复位后重复实验,我们可以确认,在DMA读取的数组中,左声道数据同样先于右声道数据。