0%

I2S_DAC_TM8211_Drived_by_GD32F350_MCU

这个也是一个拖了很久的事情,大概两三年前看到一个很便宜的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);//打开外设时钟
/* configure I2S0 GPIO: SCK/PB3, MOSI/PB5 CS/PA15*/
//gpio_af_set(GPIOB, GPIO_AF_0, GPIO_PIN_5 | GPIO_PIN_3);
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);
}
//配置GD32 I2S
void I2S_config()
{
rcu_periph_clock_enable(RCU_SPI0);
/* deinitilize SPI and the parameters */
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
/* enable the I2S0 peripheral */
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++)
{
//i2s_data_stereo
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);

/* configure I2S0 transmit DMA: DMA_CH2 */
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,并最后使能

/* configure DMA */
dma_config();
dma_transfer_number_config(DMA_CH2, waveLength*2);
//---开启循环传输--
dma_circulation_enable(DMA_CH2);
/* enable DMA channel */
dma_channel_enable(DMA_CH2);//SPI0的DMA通道是CH2 见用户手册
/* enable SPI DMA */
spi_dma_enable(SPI0, SPI_DMA_TRANSMIT);

开启循环传输后会自动循环,这里测试输出信号可以启用。 主函数循环中可以添加判断来在DMA传输完成的时候做别的事情

  /* wait DMA transmit completed */
while(!dma_flag_get(DMA_CH2, DMA_FLAG_FTF)) {
//
__NOP();
}

同样这个也可以在中断中来做。

注意点,DMA传输长度和位宽不要错。

可以提前在DMA配置结构体中设置好初值,也可以调用函数来更新长度等信息,例如

dma_transfer_number_config(DMA_CH2, waveLength*2);//waveLength为前面计算序列函数返回的长度值

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