STM32 HAL学习笔记:GC1808(PCM1808)的使用以及使用I2S+DMA读取

前言

我的项目需要使用一个立体声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,总之能用。三个功能选择引脚接出来,用一坨锡就能修改。

Snipaste_2025-09-15_18-25-41

我计划将音频采样率配置为48KHz,即:

\[f_S = 48KHz \]

选择$$f_{ICK} = 512 \times f_S = 24.576MHz$$

另选择工作在主机模式、数据格式为左对齐、24bit,故将引脚配置为:

引脚 电平
FMT (Pin 12)
MD1 (Pin 11)
MD0 (Pin 10)

由数据手册可知,每一帧由2个声道共32个位组成,故可计算得到BCK(位时钟)的频率为:

\[f_{BCK}=48KHz \times 64 = 3.072MHz \]

LRCK(左右时钟)的频率与输出音频采样率相同,即:

\[f_{LRCK}=f_S=48KHz \]

连接示波器,可以看到测试结果与理论值一致,如下图所示。其中,1通道(黄色)为BCK波形,2通道(青色)为LRCK波形。由于学校的包浆示波器探头找不到接地弹簧,我只能使用接地夹子测量,测量波形噪声较多、质量很差。
DS2_20250914145633

更换探头位置,1通道测量LRCK波形,2通道测量DOUT(数据输出)波形,结果如下图所示。可以看到,ADC正在发送两个声道的数据,与手册标注的格式一致。
DS2_20250914145908

STM32CubeMX配置与接线

为产生ADC所需的时钟信号,我使用了一个24.576MHz的无源晶振,并将整个单片机运行在24.576MHz。这款单片机似乎没有为I2S单独提供时钟的PLL,无法使用I2S的MCO(主时钟输出)功能输出ADC芯片所需的系统时钟,故配置时钟树将HCLK通过MCO引脚输出,连接到ADC的SCKI。时钟树如下图所示。

Snipaste_2025-09-15_18-00-40

在左侧选择I2S,页面上方模式设置为半双工从机模式,传输模式修改为从机接收,通信标准为MSB优先左对齐,数据和帧格式为在32位帧上传输的24位数据,音频频率为48KHz。可以看到,得益于专门选择的晶体频率,频率误差为0。如下图所示。

Snipaste_2025-09-15_18-37-22

切换到DMA设置标签页。点击加号添加一个DMA请求,STM32CubeMX自动帮我们配置了一些信息。我们需要将DMA设置请求中的模式切换为循环。根据参考手册,I2S接收的寄存器只有16位,32位的一帧需要DMA分两次搬运,故这里的数据宽度都选择半字(16位)。设置如下图所示。

Snipaste_2025-09-15_18-38-12

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];

        // 取高24bit
        int32_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.\r\n");
    /* 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。

Snipaste_2025-09-15_19-39-40

用手指触摸13引脚对应的电容,数据变化如下。可以看到,左列数据发生了较大波动,不再接近0。

Snipaste_2025-09-15_19-44-03

经过多次复位后重复实验,我们可以确认,在DMA读取的数组中,左声道数据同样先于右声道数据。

posted @ 2025-09-15 19:53  GongYeSUDA  阅读(211)  评论(0)    收藏  举报