本帖最后由 小恩GG 于 2025-11-21 11:04 编辑
用 FRDM-MCXN236 的 FlexIO 模拟 I2S 实现音频输出
概述在嵌入式音频应用中,I2S 接口是连接音频 Codec 与微控制器的标准方案。NXP MCXN 系列芯片虽搭载 SAI 模块,但实际项目中可能面临 I2S 通道数量不足的问题,此时借助灵活可配置的 FlexIO 外设模拟 I2S 接口,是高效的功能扩展方案。 NXP 官网已提供 MCXA156 芯片通过 FlexIO 模拟 I2S 的应用笔记 AN14527 及配套源码 AN14527SW,本文在此基础上,补充 FRDM-MCXN236 开发板的完整实现案例。该开发板具备 3.5mm 耳机接口,且支持焊接 DA7212 音频 Codec(默认 DNP 状态,需用户自行焊接),硬件条件适配音频输出验证需求。 下文将详细拆解基于 MCXN236 芯片的 FlexIO 模拟 I2S 实现流程,为开发者提供可直接复用的技术参考。
核心概念:什么是 FlexIO?
FlexIO 是一款高度灵活的可编程外设,核心由移位寄存器(Shifter)和定时器(Timer)构成。通过软件编程对这些基础资源进行组合配置,可灵活模拟 UART、SPI、I2C、I2S 等多种串 / 并行通信协议,无需依赖专用硬件模块。这一特性极大提升了微控制器外设资源的复用率,降低了对专用接口的依赖,为嵌入式项目开发提供了更高的灵活性。关于 FlexIO 的更多底层细节,可参考 AN14527 应用笔记的 3.1 节内容以及芯片的参考手册。
硬件准备完成本实验需准备以下硬件设备及配件: · FRDM-MCXN236 开发板(Rev B) · Type-C USB 电缆 · 个人电脑 · 符合OMTP 标准的3.5mm耳机(需配合自行焊接的板载 3.5mm 接口使用) · DA7212 AudioCodec(需用户自行焊接至开发板)
引脚连接FRDM-MCXN236 的FlexIO 引脚需与板载音频相关引脚精准映射,以实现I2S 信号的正常传输,具体连接关系如下:
软件实现(基于NXP MCUXpresso IDE)软件实现核心围绕 FlexIO 初始化、I2S 协议配置、MCLK 生成及中断传输展开,以下是完整代码解析。 1. 基础配置:宏定义与全局变量 该部分明确硬件引脚、时钟参数、音频格式及Codec 配置,为后续初始化提供基础参数,确保各模块参数一致。 - #include "pin_mux.h"
- #include "clock_config.h"
- #include "board.h"
- #include "fsl_flexio_i2s.h"
- #include "fsl_debug_console.h"
- #include "fsl_codec_common.h"
- #include "fsl_codec_adapter.h"
- #include "fsl_dialog7212.h"
- #include "music.h" // 包含 215Hz 正弦波音频数据
- /* 硬件实例定义 */
- #define DEMO_I2C LPI2C2 // Codec 控制 I2C 实例
- #define DEMO_FLEXIO_BASE FLEXIO0 // 模拟 I2S 的 FlexIO 实例
- /* 时钟与引脚配置 */
- #define DEMO_FLEXIO_CLK_FREQ CLOCK_GetFlexioClkFreq() // 获取 FlexIO 时钟频率
- #define BCLK_PIN (5U) // FlexIO_D5 (P0_21) - I2S 位时钟
- #define FRAME_SYNC_PIN (4U) // FlexIO_D4 (P0_20) - I2S 帧同步
- #define TX_DATA_PIN (2U) // FlexIO_D2 (P0_18) - I2S 发送数据
- #define RX_DATA_PIN (3U) // FlexIO_D3 (P0_19) - I2S 接收数据(预留)
- #define MCLK_PIN (6U) // FlexIO_D6 (P0_22) - I2S 主时钟
- /* 音频与缓冲配置 */
- #define OVER_SAMPLE_RATE (384) // MCLK 倍频系数(MCLK = 采样率 × 倍频系数)
- #define BUFFER_SIZE (256) // 单块音频缓冲大小(字节)
- #define BUFFER_NUM (4) // 乒乓缓冲块数量
- #define DEMO_AUDIO_SAMPLE_RATE (kFLEXIO_I2S_SampleRate16KHz) // 采样率:16kHz
- #define DEMO_AUDIO_DATA_CHANNEL (2U) // 通道数:立体声
- #define DEMO_AUDIO_BIT_WIDTH (kFLEXIO_I2S_WordWidth16bits) // 位宽:16位
- #define DEMO_CODEC_VOLUME 60U // 音量(0-100,避免过载失真)
- /* MCLK 频率计算(适配不同硬件场景) */
- #if (defined FSL_FEATURE_SAI_HAS_MCLKDIV_REGISTER && FSL_FEATURE_SAI_HAS_MCLKDIV_REGISTER) || \
- (defined FSL_FEATURE_PCC_HAS_SAI_DIVIDER && FSL_FEATURE_PCC_HAS_SAI_DIVIDER)
- #define DEMO_AUDIO_MASTER_CLOCK OVER_SAMPLE_RATE *DEMO_AUDIO_SAMPLE_RATE // 16kHz×384=6.144MHz
- #else
- #define DEMO_AUDIO_MASTER_CLOCK DEMO_SAI_CLK_FREQ
- #endif
- /* DA7212 Codec 配置(与硬件匹配) */
- da7212_config_t da7212Config = {
- .i2cConfig = {.codecI2CInstance = BOARD_CODEC_I2C_INSTANCE, .codecI2CSourceClock = 12000000},
- .dacSource = kDA7212_DACSourceInputStream,
- .slaveAddress = DA7212_ADDRESS,
- .protocol = kDA7212_BusI2S,
- .format = {.mclk_HZ = 12288000, .sampleRate = DEMO_AUDIO_SAMPLE_RATE, .bitWidth = DEMO_AUDIO_BIT_WIDTH},
- .sysClkSource = kDA7212_SysClkSourceMCLK,
- .isMaster = false, // Codec 为从机,由 FlexIO 提供时钟
- };
- /* 通用 Codec 配置(适配驱动框架) */
- codec_config_t boardCodecConfig = {.codecDevType = kCODEC_DA7212, .codecDevConfig = &da7212Config};
- /* 传输相关全局变量 */
- flexio_i2s_handle_t txHandle = {0}; // 发送句柄(音频输出)
- flexio_i2s_handle_t rxHandle = {0}; // 接收句柄(预留)
- static volatile bool isTxFinished = false; // 发送完成标志
- static volatile uint32_t beginCount = 0, sendCount = 0; // 发送计数
- static volatile uint8_t emptyBlock = 0; // 空缓冲块数
- static volatile bool isZeroBuffer = true; // 零数据缓冲标志(静音切换)
- FLEXIO_I2S_Type s_base; // FlexIO I2S 核心结构体
- codec_handle_t codecHandle; // Codec 操作句柄
复制代码
2. 中断回调函数:乒乓缓冲管理 采用中断驱动的非阻塞机制,避免占用 CPU 资源,同时通过乒乓缓冲管理,确保音频输出连续性,解决数据生成与传输的时序冲突。
- /**
- * @brief 发送中断回调函数(音频数据发送完成后触发)
- * 管理缓冲状态,更新发送计数,标记播放完成
- */
- static void txCallback(FLEXIO_I2S_Type *i2sBase, flexio_i2s_handle_t *handle, status_t status, void *userData)
- {
- // 若存在空缓冲且当前不是零数据缓冲,更新空缓冲计数与发送计数
- if ((emptyBlock < BUFFER_NUM) && (!isZeroBuffer))
- {
- emptyBlock++; // 空缓冲块+1(表示可向该缓冲填充新音频数据)
- sendCount++; // 已发送块数+1(记录播放进度)
- }
- // 若当前使用零数据缓冲(静音),切换为非零缓冲(准备播放音频)
- if (isZeroBuffer)
- {
- isZeroBuffer = false;
- }
- // 若已发送块数达到总发送块数,标记发送完成(播放结束)
- if (sendCount == beginCount)
- {
- isTxFinished = true;
- }
- }
- /**
- * @brief 接收中断回调函数(预留,本实验未使用 RX 通道)
- * @note 若后续需要音频输入(如麦克风),可基于此函数扩展
- */
- static void rxCallback(FLEXIO_I2S_Type *i2sBase, flexio_i2s_handle_t *handle, status_t status, void *userData)
- {
- // 若存在空缓冲,更新缓冲计数与接收计数(预留逻辑)
- if (emptyBlock > 0)
- {
- emptyBlock--; // 空缓冲块-1(表示缓冲已被接收数据填充)
- receiveCount++; // 已接收块数+1(记录接收进度)
- }
- // 若已接收块数达到总发送块数,标记接收完成(预留逻辑)
- if (receiveCount == beginCount)
- {
- isRxFinished = true;
- }
- }
复制代码
3. 核心初始化:FlexIO I2S 配置 该部分是核心逻辑,分为 “FlexIO 核心初始化” 和 “MCLK 初始化”,分别配置 I2S 协议时序(移位寄存器 + 定时器)和 Codec 工作时钟,确保符合 I2S 协议要求。 3.1 FlexIO I2S 核心初始化(driver fsl_flexio_i2s 中) 配置移位寄存器(数据收发)和定时器(BCLK/FS时钟),完全遵循 I2S 协议时序,支持主 / 从模式切换。
3.2 MCLK 初始化(Codec 工作时钟) I2S 协议中 MCLK 为 Codec 提供稳定工作时钟,通过 FlexIO 定时器生成,确保与音频采样时序严格同步。
- /**
- * @brief FlexIO I2S MCLK 初始化函数(生成 Codec 所需的主时钟)
- * @note 仅在 FlexIO 作为 I2S 主机时生效,通过定时器生成稳定 MCLK
- */
- void FLEXIO_I2S_Init_FOR_MCLK(FLEXIO_I2S_Type *base, const flexio_i2s_config_t *config)
- {
- flexio_timer_config_t timerConfig = {0}; // MCLK 定时器配置结构体
- /* 仅主机模式需要生成 MCLK(从机由外部提供 MCLK) */
- if (config->masterSlave == kFLEXIO_I2S_Master)
- {
- // 触发源:发送移位寄存器状态(确保 MCLK 与音频发送时序同步)
- timerConfig.triggerSelect = FLEXIO_TIMER_TRIGGER_SEL_SHIFTnSTAT(base->txShifterIndex);
- timerConfig.triggerPolarity = kFLEXIO_TimerTriggerPolarityActiveLow; // 触发极性:低电平有效
- timerConfig.triggerSource = kFLEXIO_TimerTriggerSourceInternal; // 触发源:内部(FlexIO 资源)
- timerConfig.pinSelect = MCLK_PIN; // 引脚:MCLK 引脚
- timerConfig.pinConfig = kFLEXIO_PinConfigOutput; // 引脚模式:输出
- timerConfig.pinPolarity = kFLEXIO_PinActiveHigh; // 引脚极性:高电平有效
- timerConfig.timerMode = kFLEXIO_TimerModeDual8BitBaudBit; // 定时器模式:双8位波特率(生成时钟)
- timerConfig.timerOutput = kFLEXIO_TimerOutputOneNotAffectedByReset; // 复位不影响输出(时钟连续)
- timerConfig.timerDecrement = kFLEXIO_TimerDecSrcOnFlexIOClockShiftTimerOutput; // 递减源:FlexIO 时钟
- timerConfig.timerReset = kFLEXIO_TimerResetNever; // 复位:从不复位(持续生成时钟)
- timerConfig.timerDisable = kFLEXIO_TimerDisableNever; // 禁用:从不禁用(持续生成时钟)
- timerConfig.timerEnable = kFLEXIO_TimerEnableOnTriggerHigh; // 使能:触发信号高电平时启用
- timerConfig.timerStart = kFLEXIO_TimerStartBitEnabled; // 启动:允许启动(使能后立即生成)
- timerConfig.timerStop = kFLEXIO_TimerStopBitDisabled; // 停止:不允许停止(持续生成)
- }
- // 应用 MCLK 定时器配置(绑定到 FlexIO 定时器)
- FLEXIO_SetTimerConfig(base->flexioBase, MCLK_PIN, &timerConfig);
- // 设置 MCLK 频率(通过定时器比较值调整,此处配置为 ~12MHz,匹配 da7212Config 中的 mclk_HZ)
- base->flexioBase->TIMCMP[MCLK_PIN] = FLEXIO_TIMCMP_CMP(2 - 1U);/*--10-12M...*/
- }
- FLEXIO_SetTimerConfig(base->flexioBase, MCLK_PIN, &timerConfig);
- base->flexioBase->TIMCMP[MCLK_PIN] = FLEXIO_TIMCMP_CMP(2 - 1U); // 配置 MCLK 频率(~12MHz)
- }
复制代码
4. 主函数:流程整合与启动 主函数按 “时钟初始化→板级初始化→FlexIO/I2S 配置→Codec 初始化→中断传输启动” 的顺序整合所有模块,确保硬件就绪后启动音频输出。 - int main(void)
- {
- /* 局部变量定义 */
- flexio_i2s_config_t config;
- flexio_i2s_format_t format;
- flexio_i2s_transfer_t txXfer, rxXfer;
- /*****************************************************************************
- * 1. 时钟初始化(确保串口、I2C、FlexIO 时钟稳定)
- ****************************************************************************/
- // 调试串口时钟配置
- CLOCK_SetClkDiv(kCLOCK_DivFlexcom4Clk, 1u);
- CLOCK_AttachClk(BOARD_DEBUG_UART_CLK_ATTACH);
- // I2C 时钟配置(Codec 通信)
- CLOCK_SetClkDiv(kCLOCK_DivFlexcom2Clk, 1u);
- CLOCK_AttachClk(kFRO12M_to_FLEXCOMM2);
- // 系统时钟与 PLL1 配置(生成 FlexIO 所需 49.152MHz 时钟)
- CLOCK_EnableClock(kCLOCK_Scg);
- CLOCK_SetupFROHFClocking(48000000U);
- const pll_setup_t pll1Setup = {
- .pllctrl = SCG_SPLLCTRL_SOURCE(1U) | SCG_SPLLCTRL_SELI(3U) | SCG_SPLLCTRL_SELP(1U),
- .pllndiv = SCG_SPLLNDIV_NDIV(25U),
- .pllpdiv = SCG_SPLLPDIV_PDIV(5U),
- .pllmdiv = SCG_SPLLMDIV_MDIV(256U),
- .pllRate = 49152000U
- };
- CLOCK_SetPLL1Freq(&pll1Setup);
- CLOCK_SetClkDiv(kCLOCK_DivPLL1Clk0, 1U);
- // FlexIO 时钟配置(绑定 PLL1 时钟)
- CLOCK_SetClkDiv(kCLOCK_DivFlexioClk, 1u);
- CLOCK_AttachClk(kPLL1_CLK0_to_FLEXIO);
- /*****************************************************************************
- * 2. 板级初始化(引脚、时钟、串口就绪)
- ****************************************************************************/
- BOARD_InitPins();
- BOARD_InitBootClocks();
- BOARD_InitDebugConsole();
- PRINTF("FLEXIO_I2S interrupt example started!\n\r");
- /*****************************************************************************
- * 3. FlexIO I2S 硬件资源映射(引脚、移位寄存器、定时器绑定)
- ****************************************************************************/
- s_base.bclkPinIndex = BCLK_PIN;
- s_base.fsPinIndex = FRAME_SYNC_PIN;
- s_base.txPinIndex = TX_DATA_PIN;
- s_base.rxPinIndex = RX_DATA_PIN;
- s_base.txShifterIndex = 0;
- s_base.rxShifterIndex = 2;
- s_base.bclkTimerIndex = 0;
- s_base.fsTimerIndex = 1;
- s_base.flexioBase = DEMO_FLEXIO_BASE;
- /*****************************************************************************
- * 4. FlexIO I2S 协议配置(主从模式、功能启用)
- ****************************************************************************/
- FLEXIO_I2S_GetDefaultConfig(&config);
- config.masterSlave = kFLEXIO_I2S_Master; // FlexIO 作为 I2S 主机
- config.enableI2S = true; // 启用 I2S 功能
- FLEXIO_I2S_Init(&s_base, &config); // 初始化 I2S 核心
- FLEXIO_I2S_Init_FOR_MCLK(&s_base, &config); // 初始化 MCLK
- /*****************************************************************************
- * 5. 音频格式配置(与 Codec 保持一致)
- ****************************************************************************/
- format.bitWidth = DEMO_AUDIO_BIT_WIDTH;
- format.sampleRate_Hz = DEMO_AUDIO_SAMPLE_RATE;
- /*****************************************************************************
- * 6. Codec 初始化(配置 DA7212,设置音量)
- ****************************************************************************/
- if (CODEC_Init(&codecHandle, &boardCodecConfig) != kStatus_Success)
- {
- assert(false); // 初始化失败,检查 I2C 或 Codec 焊接
- }
- if (CODEC_SetVolume(&codecHandle, kCODEC_PlayChannelHeadphoneLeft | kCODEC_PlayChannelHeadphoneRight,
- DEMO_CODEC_VOLUME) != kStatus_Success)
- {
- assert(false); // 音量设置失败
- }
- /*****************************************************************************
- * 7. 中断传输初始化(创建句柄,绑定回调)
- ****************************************************************************/
- FLEXIO_I2S_TransferTxCreateHandle(&s_base, &txHandle, txCallback, NULL);
- FLEXIO_I2S_TransferRxCreateHandle(&s_base, &rxHandle, rxCallback, NULL);
- FLEXIO_I2S_TransferSetFormat(&s_base, &txHandle, &format, DEMO_FLEXIO_CLK_FREQ);
- FLEXIO_I2S_TransferSetFormat(&s_base, &rxHandle, &format, DEMO_FLEXIO_CLK_FREQ);
- /*****************************************************************************
- * 8. 启动音频非阻塞发送(中断驱动)
- ****************************************************************************/
- txXfer.dataSize = sizeof(music);
- txXfer.data = music;
- FLEXIO_I2S_TransferSendNonBlocking(&s_base, &txHandle, &txXfer);
- PRINTF("\n\r FLEXIO_I2S interrupt example finished!\n\r ");
- /*****************************************************************************
- * 9. 主循环(音频传输由中断驱动,可扩展循环播放逻辑)
- ****************************************************************************/
- while(1)
- {
- // 可选:循环播放
- // if (isTxFinished)
- // {
- // isTxFinished = false;
- // sendCount = 0;
- // FLEXIO_I2S_TransferSendNonBlocking(&s_base, &txHandle, &txXfer);
- // }
- }
- }
复制代码
测试步骤1. 将USB 电缆连接到 PC和开发板的MCU-Link USB 端口 2. 打开串口终端,配置参数:波特率 115200、数据位 8、校验位无、停止位 1 3. 将程序下载到开发板 4. 将耳机插入板上的耳机插孔(J14) 5. 按下复位按钮或启动调试器开始运行程序
实验结果
当程序正常运行时,耳机将输出清晰的 215Hz 正弦波声音,表明 FlexIO 模拟的 I2S 接口工作稳定,数据传输时序准确,音频 Codec 与微控制器通信正常。
总结本文详细展示了基于FRDM-MCXN236 开发板的FlexIO 模拟 I2S 接口实现方案,通过中断驱动的乒乓缓冲机制,实现了音频数据传输。该方案充分利用FlexIO 外设的灵活性,为I2S 通道不足或无专用I2S 硬件的 MCU 提供了可靠的音频解决方案。 此外,FlexIO的配置思路可复用至其他通信协议模拟(如SPI、I2C)。实际应用中,开发者可根据需求调整音频采样率、位宽、缓冲区大小及 MCLK 倍频系数等参数,适配不同规格的音频 Codec 与应用场景。使用 FlexIO 模块模拟 I2S 接口时,需要注意两个注意事项: 1. 由于 FlexIO 存在同步延迟,在其模拟 I2S 主设备时,I2S 的 BCLK 最大时钟频率为 FlexIO 时钟频率的四分之一;而当模拟 I2S 从设备时,I2S 的 BCLK 最大时钟频率则为 FlexIO 时钟频率的六分之一。 2. FlexIO 是一个功能强大,非常灵活的模块,除了本文给出的 Timer 和 SHIFTER 的配置外,读者也可以利用其它配置模拟出I2S 接口。
参考资料• MCX N23x Reference Manual (Rev. 4, 03/2025) (document MCXN23XRM) • Emulating I2S Bus with the FlexIO on MCXA156 (document AN14527) • DA7212 Data Sheet • FRDM-MCXN236 Board User Manual (document UM12041)
|