本帖最后由 小恩GG 于 2021-3-26 15:35 编辑
【经验分享】RT10xx SAI模块基础与工程构建测试 一 文档简介
RT10xx的audio模块有SAI,SPDIF和MQS。SAI模块是用于音频数据传输的同步串行接口。SPDIF是立体声收发器,可以接收发送数字音频,MQS用于从SAI3转换I2S音频数据为PWM,然后可以驱动外部扬声器,当然实际中还是需要添加功放驱动电路。
在实际使用SAI的过程中,涉及到音频文件的播放或者采集。本文将基于MIMXRT1060-EVK,讲解关于RT10XX SAI模块基础知识,PCM编码waveform音频文件格式,音频剪辑转换工具,MCUXPresso IDE CFG外设模块构建SAI工程并且在MIMXRT1060-EVK开发板上测试播放音乐文件。
二 基础知识与工具
在进入工程的具体测试之前,了解SAI模块知识,以及音频文件格式,音频剪辑转换工具是必不可少的,本章将讲解相关内容。
2.1 SAI模块基础
RT10XX SAI模块可以支持I2S,AC97,TDM和codec/DSP接口。
SAI模块分为发送和接收两部分,相关信号线:
SAI_MCLK: master时钟,用于生成发送接收的Bit clock,主机发从机收
SAI_TX_BCLK:发送位时钟,主机发从机收
SAI_TX_SYNC:发送帧同步信号,主机发从机收,左右声道选择
SAI_TX_DATA[4]:发送数据线,1-3和RX_DATA[1-3]共用
SAI_RX_BCLK:接收位时钟
SAI_RX_SYNC:接收帧同步信号
SAI_RX_DATA[4]:接收数据线
SAI模块时钟有3种: audio master clock,bus clock, bit clock
SAI模块同步模式3种:
发送与接收异步:发送,接收各自用自己的BCLK与SYNC 发送异步,接收同步:发送与接收都用发送的BCLK与SYNC,发送后开先关 接收异步,发送同步:发送与接收都用接收的BCLK与SYNC,接收后开先关
Frame 同步,使能发送接收之后的前4个bitclock不会生成有效帧同步:
图1
SAI模块时钟结构:
图2
SAI模块时钟源有三个:PLL3_PFD3, PLL5,PLL4
上图中配置完的SAI1_CLK_ROOT也是MCLK的时钟,关于BCLK时钟:
BCLK= master clock/(TCR2[DIV]+1)*2
samplerate = Bitclockfreq /(bitwidth*channel)
2.2 waveform音频格式
WAVE文件用来保存PCM编码数据,WAVE以RIFF格式为标准,RIFF文件基本单元是CK结构题,CKID用于标识块中所包含的数据类型,值可以是:“RIFF”,“LIST”,“fmt”, “data”等。
RIFF文件按照小端little-endian字节顺序写入。
RIFF结构体:
- typedef unsigned long DWORD;//4B
- typedef unsigned char BYTE;//1B
- typedef DWORD FOURCC; // 4B
- typedef struct {
- FOURCC ckID; //4B
- DWORD ckSize; //4B
- union {
- FOURCC fccType; // RIFF form type 4B
- BYTE ckData[ckSize]; //ckSize*1B
- } ckData;
- } RIFFCK;
复制代码
图3
实际举例,取一个16k 2通道的wav音频文件:
图4
图中黄色块为CKID,绿色块数据长度,紫色块数据段。
具体解析结构如下:
图5
所以可以看到,实际的音频数据,去掉头,大小是1279860bytes.
2.3 音频格式转换
在实际的使用中,音频文件可能不是我们所需要的通道频率配置,或者文件格式不是wav,或者时间太长需要截取,或者采样率通道数不对,那么可以使用什么工具转换呢?
这里我们推荐使用ffmpeg工具:
具体使用可以查看该工具的文档和命令,这里给出两种常用的命令:
Mp3文件转成16K,16bit,双通道的wav文件:
ffmpeg -i test.mp3 -acodecpcm_s16le -ar 16000 -ac 2 test.wav
从test.wav 00:00:00开始截取35s,并且转存为test1.wav:
ffmpeg -ss 00:00:00 -i test.wav -t35.0 -c copy test1.wav
图6
图7
2.4 提取wav音频左右声道数据
目前代码是把音频数据直接以数组形式放在RT芯片内存里,所以这里涉及到如何把wav的音频数据给提取出来。
这里可以使用python读取wav的头,并且获取到音频数据的大小值,然后转存到一个数组中,供SAI去调取。具体Python代码如下:
- import sys
- import wave
- def wav2hex(strWav, strHex):
- with wave.open(strWav, "rb") as fWav:
- wavChannels = fWav.getnchannels()
- wavSampleWidth = fWav.getsampwidth()
- wavFrameRate = fWav.getframerate()
- wavFrameNum = fWav.getnframes()
- wavFrames = fWav.readframes(wavFrameNum)
- wavDuration = wavFrameNum / wavFrameRate
- wafFramebytes = wavFrameNum * wavChannels * wavSampleWidth
- print("Channels: {}".format(wavChannels))
- print("Sample width: {}bits".format(wavSampleWidth * 8))
- print("Sample rate: {}kHz".format(wavFrameRate/1000))
- print("Frames number: {}".format(wavFrameNum))
- print("Duration: {}s".format(wavDuration))
- print("Frames bytes: {}".format(wafFramebytes))
- fWav.close()
- pass
- with open(strHex, "w") as fHex:
- # Print WAV parameters
- fHex.write("/*\n");
- fHex.write(" Channels: {}\n".format(wavChannels))
- fHex.write(" Sample width: {}bits\n".format(wavSampleWidth * 8))
- fHex.write(" Sample rate: {}kHz\n".format(wavFrameRate/1000))
- fHex.write(" Frames number: {}\n".format(wavFrameNum))
- fHex.write(" Duration: {}s\n".format(wavDuration))
- fHex.write(" Frames bytes: {}\n".format(wafFramebytes))
- fHex.write("*/\n\n")
- # Print WAV frames
- fHex.write("uint8_t music[] = {\n")
- print("Transferring...")
- i = 0
- while wafFramebytes > 0:
- if(wafFramebytes < 16):
- BytesToPrint = wafFramebytes
- else:
- BytesToPrint = 16
- fHex.write(" ")
- for j in range(0, BytesToPrint):
- if j != 0:
- fHex.write(' ')
- fHex.write("0x{:0>2x},".format(wavFrames[i]))
- i+=1
- j+=1
- fHex.write("\n")
- wafFramebytes -= BytesToPrint
- fHex.write("};\n")
- fHex.close()
- print("Done!")
- wav2hex(sys.argv[1], sys.argv[2])
复制代码 同样以music1.wav 为例,转换音频数据:
图8
2.5 数据与音频对应关系
16bit的数据范围位-32768到32767,对于goldwav中相对值是(-1~1)。
使用goldwave打开例程中的music1.wav,查看位于1s的数据,左声道的相对数据为-0.08227, 右声道的相对数据为-0.2257。
图9 图10
下面开始计算左右声道实际数据,并且找到在例子music1.h中的位置:
图11
从图8中可以看到转换好后的音频左右声道数据从第11行开始,每行16字节。
所以根据music1.wav在goldwave中相对数据可以算出对应的左右声道数据,对比提取出来的音频数据可以发现,完全一致。
三 SAI MCUXpresso工程构建
本文使用MCUXpresso基于SDK_2.9.1_EVK-MIMXRT1060,新建一个SAI DMA播放音乐的例程。音频数据即使用上述导出的music1.h。
新建一个bare metal 的project:
Drivers 勾选:
clock,common, dmamux, edma,gpio,i2c,iomuxc,lpuart,sai,sai_edma,xip_device
Utilities勾选:
Debug_console,lpuart_adapter,serial_manager,serial_manager_uart
Board components 勾选:
Xip_board
Abstraction Layer勾选:
Codec,codec_wm8960_adapter,lpi2c_adapter
Software Components勾选:
Codec_i2c,lists,wm8960
新建工程之后,打开clocks,配置时钟,core,FlexSPI时钟使用默认时钟,主要配置SAI1相关时钟:
图12
选择时钟源为PLL4,PLL4_MAIN_CLK配置为786.48Mhz.
SAI1 clock 配置为6.144375Mhz.
配置好后,生成代码。
打开Pins,配置SAI1相关引脚:
图13
生成代码。
打开peripherals,配置DMA,SAI,NVIC
图14
图15
DMA配置如下:
图16
配置好后生成代码。
因为SAI模块这边不能直接选择SAI发送,而是playback,既带有SAI发送,也带有SAI接收,所以在生成好的代码SAI1_init函数中可以屏蔽掉关于接收的代码。
上述我们将完成SAI模块DMA传送的配置,SAI master模式,16bit,采样率16Khz,双通道,DMA发送,bit clock512Khz。
在source中添加生成好的musc1.h
主函数文件中添加关于codec初始化,音频数据调用,以及DMA中断callback, SAI模块中断服务函数的处理。
//添加代码
- void callback(I2S_Type *base, sai_edma_handle_t *handle, status_t status, void *userData)
- {
- if (kStatus_SAI_RxError == status)
- {
- }
- else
- {
- finishIndex++;
- emptyBlock++;
- /* Judge whether the music array is completely transfered. */
- if (MUSIC_LEN / BUFFER_SIZE == finishIndex)
- {
- isFinished = true;
- finishIndex = 0;
- emptyBlock = BUFFER_NUM;
- tx_index = 0;
- cpy_index = 0;
- }
- }
- }
- void DelayMS(uint32_t ms)
- {
- for (uint32_t i = 0; i < ms; i++)
- {
- SDK_DelayAtLeastUs(1000, SystemCoreClock);
- }
- }
- /*
- * @brief Application entry point.
- */
- int main(void) {
- sai_transfer_t xfer;
- /* Init board hardware. */
- BOARD_ConfigMPU();
- BOARD_InitPins();
- BOARD_InitBootClocks();
- BOARD_InitBootPeripherals();
- #ifndef BOARD_INIT_DEBUG_CONSOLE_PERIPHERAL
- /* Init FSL debug console. */
- BOARD_InitDebugConsole();
- #endif
- PRINTF(" SAI wav module test!\n\r");
- /* Use default setting to init codec */
- if (CODEC_Init(&codecHandle, &boardCodecConfig) != kStatus_Success)
- {
- assert(false);
- }
- /* delay for codec output stable */
- DelayMS(DEMO_CODEC_INIT_DELAY_MS);
- CODEC_SetVolume(&codecHandle,2U,50); // set 50% value
- EnableIRQ(DEMO_SAI_IRQ);
- SAI_TxEnableInterrupts(DEMO_SAI, kSAI_FIFOErrorInterruptEnable);
- PRINTF(" MUSIC PLAY Start!\n\r");
- while (1)
- {
- PRINTF(" MUSIC PLAY Again\n\r");
- isFinished = false;
- while (!isFinished)
- {
- if ((emptyBlock > 0U) && (cpy_index < MUSIC_LEN / BUFFER_SIZE))
- {
- /* Fill in the buffers. */
- memcpy((uint8_t *)&buffer[BUFFER_SIZE * (cpy_index % BUFFER_NUM)],
- (uint8_t *)&music[cpy_index * BUFFER_SIZE], sizeof(uint8_t) * BUFFER_SIZE);
- emptyBlock--;
- cpy_index++;
- }
- if (emptyBlock < BUFFER_NUM)
- {
- /* xfer structure */
- xfer.data = (uint8_t *)&buffer[BUFFER_SIZE * (tx_index % BUFFER_NUM)];
- xfer.dataSize = BUFFER_SIZE;
- /* Wait for available queue. */
- if (kStatus_Success == SAI_TransferSendEDMA(DEMO_SAI, &SAI1_SAI_Tx_eDMA_Handle, &xfer))
- {
- tx_index++;
- }
- }
- }
- }
- }
- void DEMO_SAITxIRQHandler(void)
- {
- /* Clear the FIFO error flag */
- SAI_TxClearStatusFlags(DEMO_SAI, kSAI_FIFOErrorFlag);
- /* Reset FIFO */
- SAI_TxSoftwareReset(DEMO_SAI, kSAI_ResetTypeFIFO);
- SDK_ISR_EXIT_BARRIER;
- }
复制代码
四 SAI测试结果
为了查看实际左右声道数据发送出去的波形对应情况,下面修改music开始的16字节左右声道数据为:0x55,0xaa,0x01,0x00,0x02,0x00,0x03,0x00,0x04,0x00,0x05,0x00,0x06,0x00,0x07,0x00
然后测试SAI_MCLK,SAI_TX_BCLK,SAI_TX_SYNC,SAI_TXD的波形,查看数据对应的情况。
因为代码配置的是极性是低有效,即下降沿输出,上升沿采样。
测试点在MIMXRT1060-EVK板子Codec位置:
图17
4.1 逻辑分析测试波形
图18
MCLK的频率为6.144375Mhz, BCLK频率为512khz,SYNC的频率为16Khz。
图19
第一帧数据为:1010101001010101 0000000000000001
0XAA55 0X0001
和表格中左右声道一致,小端存放。
SYNC为低左声道16bit,为高右声道16bit。
4.2 示波器测试波形
和逻辑分析仪同样的数据,示波器测试结果:
图20
音频实际播放效果,添加配置好的music.h文件,并且让主程序循环播放music文件里面的音频数据。具体测试结果见附件.mp3,附件还添加另外一个左右声道区分明显的音乐文件,可以直接放到代码测试。
|