这个也是一个拖了很久的事情,大概两三年前看到一个很便宜的DAC
TM8211,当时还不太清楚音频DAC和通用DAC的区别,就想着测试一下这个。不过当时没有找到合适的单片机测试,用adruino
UNO模拟了个时序之后结果似乎不对就一直搁置了,这个焊接到直插转接板上的DAC就辗转了很多个地方一直没有测试。
由于之前屯了一批GD32F350,正好这个MCU带有一路I2S,因此就选用这个来做。
GD32F350上这一路I2S是和SPI0复用的。
TM8211 I2S传输格式
国产这个版本的描述很生草 “输入采用LSBJ (Least Significant Bit
Justified ) 格式,
数字编码格式采用MSB在前的补码格式”,也没有给出时序图。实际上可以看PT8211的手册上的时序图,可以看到其实就是右对齐格式,也叫日本格式,在GD32的库中对应
I2S_STD_LSB
。
其数据为16bit,对于I2S传输中更长的数据则直接丢弃,所以可以兼容一般的16bit数据32bit长度的格式。
编程
在主函数中添加
#include "gd32f3x0_spi.h" ···
|
然后配置I2S的IO功能。 在GD32F350中每个I2S
pin都有两个IO可以选择,这里选择全部位于GPIOA上的PIN.
MCK不是必须的,这里驱动这个DAC则用不上。可以省掉。
#define I2S0_AF GPIO_AF_0 #define I2S0_WS_PORT GPIOA #define I2S0_SCK_PORT GPIOA #define I2S0_SD_PORT GPIOA #define I2S0_PORT GPIOA
#define I2S0_WS_PIN GPIO_PIN_4 #define I2S0_SCK_PIN GPIO_PIN_5 #define I2S0_SD_PIN GPIO_PIN_7 #define I2S0_MCK_PIN GPIO_PIN_6
void io_init() { rcu_periph_clock_enable(RCU_GPIOA); gpio_af_set(I2S0_PORT, I2S0_AF, I2S0_WS_PIN | I2S0_SCK_PIN | I2S0_SD_PIN| I2S0_MCK_PIN); gpio_mode_set(I2S0_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, I2S0_WS_PIN | I2S0_SCK_PIN | I2S0_SD_PIN | I2S0_MCK_PIN); gpio_output_options_set(I2S0_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, I2S0_WS_PIN | I2S0_SCK_PIN | I2S0_SD_PIN | I2S0_MCK_PIN); }
void I2S_config() { rcu_periph_clock_enable(RCU_SPI0); spi_i2s_deinit(SPI0); i2s_init(SPI0,I2S_MODE_MASTERTX,I2S_STD_LSB,I2S_CKPL_HIGH); #ifdef MCK_ENABLE i2s_psc_config(SPI0,I2S_AUDIOSAMPLE_48K,I2S_FRAMEFORMAT_DT16B_CH32B,I2S_MCKOUT_ENABLE); #else i2s_psc_config(SPI0,I2S_AUDIOSAMPLE_48K,I2S_FRAMEFORMAT_DT16B_CH32B,I2S_MCKOUT_DISABLE); #endif i2s_enable(SPI0); }
|
分别在主函数中调用
rcu_config(); io_init(); I2S_config();
|
然后生成双通道的16bit补码用于测试
这里直接生成一条正弦波后减去偏移量
typedef struct audio_data_stereo { int16_t R; int16_t L; }audio_data_stereo_Typedef; ···
audio_data_stereo_Typedef audioDatas[512]; for(int c = 0;c<waveLength;c++) { audioDatas[c].R = dataarr[c]-0x8000; audioDatas[c].L = dataarr[c]-0x8000; }
|
I2S
DAC会交替发送接收到的数据分别转化到左通道和右通道,因此可以把填充后的双通道数据直接发送到I2S外设
while(1) { for(int c = 0;c<waveLength*2;c++) { while(RESET == spi_i2s_flag_get(SPI0, I2S_FLAG_TBE)); spi_i2s_data_transmit(SPI0,*(((int16_t*)audioDatas)+c)); } }
|
也可以开启发送中断,在发送完成后发送下一次数据。
DMA
除了上面用的在主函数里面循环写入I2S寄存器的方法,还可以使用DMA来发送。
对于GD32F350,开启DMA_CH2,SPI0的DMA通道是CH2 见用户手册.
配置DMA结构
void dma_config(void) { dma_parameter_struct dma_init_struct; dma_struct_para_init(&dma_init_struct);
dma_deinit(DMA_CH2); dma_init_struct.periph_addr = (uint32_t)&SPI_DATA(SPI0); dma_init_struct.memory_addr = (uint32_t)audioDatas; dma_init_struct.direction = DMA_MEMORY_TO_PERIPHERAL; dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_16BIT; dma_init_struct.memory_width = DMA_MEMORY_WIDTH_16BIT; dma_init_struct.priority = DMA_PRIORITY_LOW; dma_init_struct.number = ARRAYSIZE; dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; dma_init(DMA_CH2, &dma_init_struct); }
|
然后在I2S配置完成后配置DMA,并最后使能
dma_config(); dma_transfer_number_config(DMA_CH2, waveLength*2); dma_circulation_enable(DMA_CH2); dma_channel_enable(DMA_CH2); spi_dma_enable(SPI0, SPI_DMA_TRANSMIT);
|
开启循环传输后会自动循环,这里测试输出信号可以启用。
主函数循环中可以添加判断来在DMA传输完成的时候做别的事情
while(!dma_flag_get(DMA_CH2, DMA_FLAG_FTF)) {
__NOP(); }
|
同样这个也可以在中断中来做。
注意点,DMA传输长度和位宽不要错。
可以提前在DMA配置结构体中设置好初值,也可以调用函数来更新长度等信息,例如
dma_transfer_number_config(DMA_CH2, waveLength*2);
|
I2S在MCU上使用的时候时钟问题
如果想要连续发送时钟,例如利用I2S的DMA来传送的时候,那么I2S的数据BIT时钟就需要满足
Freq = 采样率×通道数x单个数据长度,这时候假设需要48khz 2通道
32bit,则需要3.072Mhz的I2S时钟,而GD32的I2S时钟只有整数分频也没有倍频,这里主时钟如果不是需要的I2S时钟的整数倍就会出现分频不准确的情况,信号的频率就会偏移。
而开发板上的时钟如果不是专门选的音频用的时钟,那么就会需要分频得到准确的时钟。例如我的这个板子上的晶振是8Mhz,不过好在GD32的主时钟PLL配置比较自由,采用8Mhz的晶振的情况下,只需要设置倍频64,分频5,就可以得到76.8Mhz的主时钟,分频系数为25就可以得到3.072Mhz的I2S_CLK.
测试
首先是看数据传输格式:
其中双色是WS(声道选择),蓝色是数据线,可以看到是右边对齐的。
然后可以看一下输出:
可以看到两个通道相位是一致的,黄色通道遮挡了蓝色通道。
直流DC量则是1.267V.
当然对于音频来说这个直流量的准确度不重要。输出滤波依然需要交流耦合。
是否支持直流输出?
TM8211也可以单次发送设定一个值,不过依然是交替左右通道来设置,设置之后能保留直流。
而WS线的上升沿和下降沿是必须的,不能通过单独设定高电平或者低电平来保持一个通道输出,否则没有输出。
当然,可以通过每次发送两个一样的值来保证两个通道更新为一样的输出。
而且音频DAC的直流一致性是没有保证的,因此如果是设定直流的应用还是不要使用这种。
测试环境
IDE: Segger Embedded IDE 7.12 Debugger: WCH-LinK(DAPlink v2)
实物:
示波器:DHO804