当前位置: 首页 > news >正文

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];// 取高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。

Snipaste_2025-09-15_19-39-40

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

Snipaste_2025-09-15_19-44-03

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

http://www.wxhsa.cn/company.asp?id=4940

相关文章:

  • 完整教程:【视频系统】技术汇编
  • MSTP 单域
  • 阿里云百炼平台使用避坑记录 - 详解
  • springboot的run
  • ubuntu服务器docker日期安装mysql
  • springboot的启动流程
  • 萤火虫旅行网和萤火虫文旅的关系是什么
  • 「微积分 A1」基础知识(连载中)
  • 第2周-预习作业
  • P12546 [UOI 2025] Convex Array
  • 一个新词:测试可靠性
  • CF827F Dirty Arkadys Kitchen
  • P2839 [国家集训队] middle
  • wuti
  • 友链
  • 向量化存储与知识图谱的比较
  • 力扣17题 电话号码的字母组合
  • 萤火虫文旅年票、为什么能做到低至4.2元一张景区门票、还能高达50%的毛利润?
  • ubuntu服务器docker容器安装nacos
  • PWN手的成长之路-02-r3m4ke
  • SAP 采购订单税率及含税金额取数
  • 深入解析:Linux x86 stability和coredump
  • 9.15更新linux命令
  • Jenkins 容器和 Kubernetes Agent
  • LGP7916 [CSP-S 2021] 交通规划 学习笔记
  • 详细介绍:【Kubernetes】常见面试题汇总(十四)
  • 萤火虫文旅年票、为何能成为撬动万亿文旅市场的利器
  • 教育行业API安全最佳实践:全知科技以国家标准引领数据防护新范式
  • Codecademy Pro是否值得?2023年深度评测与技术特性解析
  • Qt处理USB摄像头开发说明与QtMultimedia与V4L2融合应用