前面简单讲解了WM8960语音芯片工作方式,WM8960做master,之前参数配置ADC/DAC采样速率的是44.1K,有点问题,现在改为16K,下面会解释为什么要改成16K。

WM8960参数配置如下:注意录音时关掉内部路径,否则会有杂音。

#ifdef ALOOPBACK //先关掉内部路径播放

0x2d,0x080, //Left Input Boost Mixer to Left Output Mixer

0x2e,0x080, //Right Input Boost Mixer to Right Output Mixer

#endif

//WM8960 slave mode//MCLK = 24MHz, SYSCLK = 12.288MHz,//PLL mode is fractional, MCLK div 2

const u16 wm8960_reg_master[]={0x0f,0x000,0x19,0x17e,0x1a,0x1e1,0x2f,0x03c,0x34,0x038,0x35,0x031,0x36,0x026,0x37,0x0e6,0x04,0x0dd, //ADC/DAC 采样速率12.288/(3*256) = 16KHz

0x08,0x00c,0x20,0x138,0x21,0x138,0x2b,0x000,0x2c,0x000,0x00,0x157,0x01,0x157,0x05,0x000,0x06,0x000,0x07,0x042, //bit6=1, Enable master mode; bit[1:0]=10,I2S Format;bit[3:2]=00,16 bits

0x18,0x004,0x30,0x000,0x02,0x163,//179,// //LOUT1 Volume

0x03,0x163,//0x179,//ROUT1 Volume

#ifdef DLOOPBACK//不开LOOPBACK

0x09,0x001,#endif

0x22,0x100, //Enable Left DAC to Left Output Mixer

0x25,0x100, //Enable Right DAC to Right Output Mixer

#ifdef ALOOPBACK//先关掉内部路径播放

0x2d,0x080, //Left Input Boost Mixer to Left Output Mixer

0x2e,0x080, //Right Input Boost Mixer to Right Output Mixer

#endif};

View Code

调用I2C函数进行初始化:

uint8_t WM8960_Set_Play_Recorde_Reg(void)

{

uint8_t i= 0;

uint8_t res= 0;

res= WM8960_Write_Reg((uint8_t)wm8960_reg_master[0], wm8960_reg_master[1]);if(res != 0)

{returnres;

}

Delay_ms(10);for(i=2; i

{

res= WM8960_Write_Reg((uint8_t)wm8960_reg_master[i],wm8960_reg_master[i+1]);

}return 0;

}

View Code

I2C发送函数,使用的是IO模拟:

void W8960_IIC_Init(void)

{

RCC->AHB1ENR|=1<<1; //使能PORTB时钟

GPIO_Set(GPIOB,PIN6|PIN7,GPIO_MODE_OUT,GPIO_OTYPE_PP,GPIO_SPEED_50M,GPIO_PUPD_PU);//PB10/PB11设置

W8960_IIC_SCL=1;

W8960_IIC_SDA=1;//GPIO_InitTypeDef GPIO_InitStructure;//RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); /* 打开GPIO时钟 *///GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;//GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//GPIO_InitStructure.GPIO_OType = GPIO_OType_OD; /* 开漏输出 *///GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;//GPIO_Init(GPIOB, &GPIO_InitStructure);

W8960_IIC_Stop();

}//产生IIC起始信号

void W8960_IIC_Start(void)

{

W8960_SDA_OUT();//sda线输出

W8960_IIC_SDA=1;

W8960_IIC_SCL=1;

Delay_us(8);

W8960_IIC_SDA=0;//START:when CLK is high,DATA change form high to low

Delay_us(8);

W8960_IIC_SCL=0;//钳住I2C总线,准备发送或接收数据

}//产生IIC停止信号

void W8960_IIC_Stop(void)

{

W8960_SDA_OUT();//sda线输出

W8960_IIC_SCL=0;

W8960_IIC_SDA=0;//STOP:when CLK is high DATA change form low to high

Delay_us(4);

W8960_IIC_SCL=1;

Delay_us(4);

W8960_IIC_SDA=1;//发送I2C总线结束信号

Delay_us(4);

}//等待应答信号到来//返回值:1,接收应答失败//0,接收应答成功

uint8_t W8960_IIC_Wait_Ack(void)

{

uint8_t ucErrTime=0;

W8960_SDA_IN();//SDA设置为输入

W8960_IIC_SDA=1;Delay_us(5);

W8960_IIC_SCL=1;Delay_us(5);while(W8960_READ_SDA)

{

ucErrTime++;if(ucErrTime>250)

{

W8960_IIC_Stop();return 1;

}

}

W8960_IIC_SCL=0;//时钟输出0

return 0;

}//产生ACK应答

void W8960_IIC_Ack(void)

{

W8960_IIC_SCL=0;

W8960_SDA_OUT();

W8960_IIC_SDA=0;

Delay_us(4);

W8960_IIC_SCL=1;

Delay_us(4);

W8960_IIC_SCL=0;

}//不产生ACK应答

void W8960_IIC_NAck(void)

{

W8960_IIC_SCL=0;

W8960_SDA_OUT();

W8960_IIC_SDA=1;

Delay_us(2);

W8960_IIC_SCL=1;

Delay_us(2);

W8960_IIC_SCL=0;

}//IIC发送一个字节//返回从机有无应答//1,有应答//0,无应答

voidW8960_IIC_Send_Byte(uint8_t txd)

{

uint8_t t;

W8960_SDA_OUT();

W8960_IIC_SCL=0;//拉低时钟开始数据传输

Delay_us(1);//发送前7bit,SCL下跳后1us才输出新SDA

for(t=0;t<7;t++)

{

W8960_IIC_SDA=(txd&0x80)>>7;

txd<<=1;

Delay_us(4); // W8960_IIC_SCL=1;

Delay_us(5);

W8960_IIC_SCL=0;

Delay_us(1);

}//发送最后1bit,SCL下跳后无任何延时,后直接进入waitAck (SDA变输入,防止短暂SDA互斥)

W8960_IIC_SDA=(txd&0x80)>>7;

Delay_us(5); // W8960_IIC_SCL=1;

Delay_us(5);

W8960_IIC_SCL=0;

}voidW8960_I2C_WriteByte(uint8_t DevAddr, uint8_t DataAddr, uint8_t DataToWrite)

{

W8960_IIC_Start();//Master发送起始信号

W8960_IIC_Send_Byte(DevAddr); //Master发送从设备地址

W8960_IIC_Wait_Ack();//if(I2C_Wait_ACK()) return I2C_TIMEOUT;//等待ACK超时错误

W8960_IIC_Send_Byte(DataAddr); //发送数据地址

W8960_IIC_Wait_Ack();//if(I2C_Wait_ACK()) return I2C_TIMEOUT;//等待ACK超时错误

W8960_IIC_Send_Byte(DataToWrite);

W8960_IIC_Wait_Ack();

Delay_us(10);

W8960_IIC_Stop();//发送停止信号

Delay_us(10);

}void W8960_I2C_WriteBurst(uint8_t DevAddr, uint8_t DataAddr, uint8_t*pData, uint32_t Num)

{

uint32_t i= 0;

W8960_IIC_Start();//Master发送起始信号

W8960_IIC_Send_Byte(DevAddr); //Master发送从设备地址

W8960_IIC_Wait_Ack();

W8960_IIC_Send_Byte(DataAddr);//发送数据地址

W8960_IIC_Wait_Ack();for(i = 0; i < Num; i++)

{

W8960_IIC_Send_Byte(*(pData+i)); //发送数据

W8960_IIC_Wait_Ack();

}

W8960_IIC_Stop();//发送停止信号

Delay_ms(1);

}//读1个字节,ack=1时,发送ACK,ack=0,发送nACK

uint8_t W8960_I2C_Read8bit(uint8_t ack)

{

unsignedchar i,receive=0;

W8960_SDA_IN();//SDA设置为输入

for(i=0;i<8;i++)

{

W8960_IIC_SCL=0;

Delay_us(4);

W8960_IIC_SCL=1;

receive<<=1;if(W8960_READ_SDA)receive++;

Delay_us(4);

}if (!ack)

W8960_IIC_NAck();//发送nACK

elseW8960_IIC_Ack();//发送ACK

returnreceive;

}void W8960_I2C_ReadBurst(uint8_t DevAddr, uint16_t DataAddr, uint8_t*pData, uint32_t Num)

{

uint32_t i= 0;

W8960_IIC_Start();//Master发送起始信号

W8960_IIC_Send_Byte(DevAddr); //Master发送从设备地址

W8960_IIC_Wait_Ack();//W8960_IIC_Send_Byte(DataAddr>>8);//发送数据地址//W8960_IIC_Wait_Ack();

W8960_IIC_Send_Byte(DataAddr); //发送数据地址

W8960_IIC_Wait_Ack();

W8960_IIC_Stop();

Delay_us(10);

W8960_IIC_Start();//Master发送起始信号

W8960_IIC_Send_Byte(DevAddr+1); //Master发送从设备读地址

W8960_IIC_Wait_Ack();for(i = 0; i < (Num-1); i++)

{*(pData+i) = W8960_I2C_Read8bit(1); //读数据,ACK

}*(pData+i) = W8960_I2C_Read8bit(0); //读数据,NACK

W8960_IIC_Stop();//发送停止信号

}

uint8_t WM8960_Write_Reg(uint8_t reg, uint16_t dat)

{

uint8_t tmp_H;

uint8_t tmp_L;

tmp_H=(uint8_t) (reg<<1)|((uint8_t) ((dat>>8)&0x0001)); //取高字节

tmp_L =(uint8_t) (dat&0x00FF); //取低8字节

W8960_IIC_Start();//Master发送起始信号

W8960_IIC_Send_Byte(WM8960_IIC_ADDR); //Master发送从设备地址

if(W8960_IIC_Wait_Ack()==1) {return 1;}; //等待超时返回1 failed

W8960_IIC_Send_Byte(tmp_H); //发送数据

if(W8960_IIC_Wait_Ack()==1) {return 1;}; //等待超时返回1 failed

W8960_IIC_Send_Byte(tmp_L); //发送数据

if(W8960_IIC_Wait_Ack()==1) {return 1;}; //等待超时返回1 failed

W8960_IIC_Stop(); //发送停止信号

return 0;

}

View Code

I2C发送函数,使用的是STM32库函数:

i2c.h

#ifndef __I2C_H#define __I2C_H#include"stm32f4xx.h"

#define I2C_LIB 1

#define DCMI_TIMEOUT_MAX 10000

#define DEVICE_WRITE_ADDRESS 0x34

#define DEVICE_READ_ADDRESS 0x35

/*Configure I2C1 pins: PB6->scl and PB7->sda*/

#define Open_I2Cx I2C1

#define Open_i2c_CLK RCC_APB1Periph_I2C1

#define Open_i2c_SDA_PIN GPIO_Pin_7

#define Open_i2c_SDA_GPIO_PORT GPIOB

#define Open_i2c_SDA_GPIO_CLK RCC_AHB1Periph_GPIOB

#define Open_i2c_SDA_SOURCE GPIO_PinSource7

#define Open_i2c_SDA_AF GPIO_AF_I2C1

#define Open_i2c_SCL_PIN GPIO_Pin_6

#define Open_i2c_SCL_GPIO_PORT GPIOB

#define Open_i2c_SCL_GPIO_CLK RCC_AHB1Periph_GPIOB

#define Open_i2c_SCL_SOURCE GPIO_PinSource6

#define Open_i2c_SCL_AF GPIO_AF_I2C1

#define I2C_SPEED 60000 //WM8960 I2C clk must 60KHz

#define I2C_SLAVE_ADDRESS7 0xFE

void I2C_GPIO_Config(void);

uint8_t I2C_Write_Byte(uint8_t Reg, uint8_t Data);#endif

View Code

i2c.c

#include "i2c.h"#include"stm32f4xx_i2c.h"

void I2C_GPIO_Config(void)

{

GPIO_InitTypeDef GPIO_InitStructure;

I2C_InitTypeDef i2c_InitStructure;

RCC_AHB1PeriphClockCmd(Open_i2c_SDA_GPIO_CLK|Open_i2c_SCL_GPIO_CLK,ENABLE);

RCC_APB1PeriphClockCmd(Open_i2c_CLK,ENABLE);

GPIO_PinAFConfig(Open_i2c_SDA_GPIO_PORT, Open_i2c_SDA_SOURCE, Open_i2c_SDA_AF);

GPIO_PinAFConfig(Open_i2c_SCL_GPIO_PORT, Open_i2c_SCL_SOURCE, Open_i2c_SCL_AF);

GPIO_InitStructure.GPIO_Pin= Open_i2c_SDA_PIN |Open_i2c_SCL_PIN;

GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF;

GPIO_InitStructure.GPIO_OType=GPIO_OType_OD;

GPIO_InitStructure.GPIO_Speed=GPIO_Speed_2MHz;

GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;

GPIO_Init(GPIOB,&GPIO_InitStructure);

I2C_DeInit(Open_I2Cx);

i2c_InitStructure.I2C_Mode=I2C_Mode_I2C;

i2c_InitStructure.I2C_DutyCycle=I2C_DutyCycle_2;

i2c_InitStructure.I2C_OwnAddress1=I2C_SLAVE_ADDRESS7;

i2c_InitStructure.I2C_Ack=I2C_Ack_Enable;

i2c_InitStructure.I2C_AcknowledgedAddress=I2C_AcknowledgedAddress_7bit;

i2c_InitStructure.I2C_ClockSpeed=I2C_SPEED;

I2C_Init(Open_I2Cx,&i2c_InitStructure);

I2C_Cmd(Open_I2Cx, ENABLE);

I2C_AcknowledgeConfig(Open_I2Cx, ENABLE);

}

uint8_t I2C_Write_Byte(uint8_t Reg, uint8_t Data)

{

uint32_t timeout=DCMI_TIMEOUT_MAX;/*Generate the Start Condition*/I2C_GenerateSTART(Open_I2Cx, ENABLE);/*Test on I2C2 EV5 and clear it*/timeout= DCMI_TIMEOUT_MAX; /*Initialize timeout value*/

while(!I2C_CheckEvent(Open_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))

{/*If the timeout delay is exeeded, exit with error code*/

if ((timeout--) == 0) return 0xFF;

}/*-----------------------------------------------------------------------------------*/

/*Send DCMI selcted device slave Address for write*/I2C_Send7bitAddress(Open_I2Cx, DEVICE_WRITE_ADDRESS, I2C_Direction_Transmitter);/*Test on I2C2 EV6 and clear it*/timeout= DCMI_TIMEOUT_MAX; /*Initialize timeout value*/

while(!I2C_CheckEvent(Open_I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))

{/*If the timeout delay is exeeded, exit with error code*/

if ((timeout--) == 0) return 0xFF;

}/*-----------------------------------------------------------------------------------*/

/*Send I2C2 location address LSB*/I2C_SendData(Open_I2Cx, (uint8_t)(Reg));/*Test on I2C2 EV8 and clear it*/timeout= DCMI_TIMEOUT_MAX; /*Initialize timeout value*/

while(!I2C_CheckEvent(Open_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))

{/*If the timeout delay is exeeded, exit with error code*/

if ((timeout--) == 0) return 0xFF;

}/*-----------------------------------------------------------------------------------*/

/*Send Data*/I2C_SendData(Open_I2Cx, Data);/*Test on I2C2 EV8 and clear it*/timeout= DCMI_TIMEOUT_MAX; /*Initialize timeout value*/

while(!I2C_CheckEvent(Open_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))

{/*If the timeout delay is exeeded, exit with error code*/

if ((timeout--) == 0) return 0xFF;

}/*-----------------------------------------------------------------------------------*/

/*Send I2C2 STOP Condition*/I2C_GenerateSTOP(Open_I2Cx, ENABLE);/*If operation is OK, return 0*/

return 0;

}

View Code

这里有个问题,I2C 时钟频率只能设置在60K,设置高点、低点,播放时就会有噪声,让人费解。

WM8960 I2C初始化函数 uint8_t WM8960_Set_Play_Recorde_Reg(void),注意这函数里有个数据格式转换,可以查看WM8960 I2C 传输格式,

地址寄存器有效为是7位[bit15 : bit9],数据有效为是9位[bit9 : bit0],但是发送的时候,是按照一个字节8bit发送的,先发地址寄存器7位 [bit15 : bit9]还得带上

数据位的最高bit8,构成[bit15:bit8]一个字节,所以数据得转换一下:

temp = sizeof(wm8960_reg_master)/sizeof(u16);for(i=0 ; i

{

WM8960_Reg_Config[i]= ((uint8_t)(wm8960_reg_master[i]<<1)) | ((uint8_t)((wm8960_reg_master[i+1]>>8)& 0x01));

WM8960_Reg_Config[i+1] = (uint8_t)(wm8960_reg_master[i+1] & 0xff);

}

#define REG_NUM 100uint8_t WM8960_Reg_Config[REG_NUM]={0};void Data_Format_Convert(void)

{

uint8_t i= 0;

uint8_t temp= 0;

temp= sizeof(wm8960_reg_master)/sizeof(u16);for(i=0 ; i

{

WM8960_Reg_Config[i]= ((uint8_t)(wm8960_reg_master[i]<<1)) | ((uint8_t)((wm8960_reg_master[i+1]>>8)& 0x01));

WM8960_Reg_Config[i+1] = (uint8_t)(wm8960_reg_master[i+1] & 0xff);

}

}

uint8_t WM8960_Set_Play_Recorde_Reg(void)

{

uint8_t i= 0;

uint8_t res= 0;

#ifdef I2C_LIB

Data_Format_Convert();//对WM890的数组寄存器数据进行转换

res = I2C_Write_Byte(WM8960_Reg_Config[0],WM8960_Reg_Config[1]);if(res != 0) returnres;

Delay_ms(10);for(i=2; i

{

res= I2C_Write_Byte(WM8960_Reg_Config[i],WM8960_Reg_Config[i+1]);

}#elseres= WM8960_Write_Reg((uint8_t)wm8960_reg_master[0], wm8960_reg_master[1]);if(res != 0)

{returnres;

}

Delay_ms(10);for(i=2; i

{

res= WM8960_Write_Reg((uint8_t)wm8960_reg_master[i],wm8960_reg_master[i+1]);

}#endif

returnres;

}

View Code

重点讲解下I2S的DMA方式及注意事项:

(1)、首先是I2S 管脚定义:

/**

* I2S×ÜÏß´«ÊäÒôƵÊý¾Ý¿ÚÏß

* WM8960_LRC -> PB12/I2S2_WS

* WM8960_BCLK -> PB13/I2S2_CK

* WM8960_ADCDAT -> PB14/I2S2ext_SD

* WM8960_DACDAT -> PB15/I2S2_SD

* WM8960_MCLK -> PC6/I2S2_MCK*/

/*Enable GPIO clock*/

void WM8960_I2S_GPIO_Config(void)

{

GPIO_InitTypeDef GPIO_InitStructure;

RCC_AHB1PeriphClockCmd(WM8960_LRC_GPIO_CLK|WM8960_BCLK_GPIO_CLK|\

WM8960_ADCDAT_GPIO_CLK|WM8960_DACDAT_GPIO_CLK|\

WM8960_MCLK_GPIO_CLK, ENABLE);

GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF;

GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;

GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;

GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_NOPULL;

GPIO_InitStructure.GPIO_Pin=WM8960_LRC_PIN;

GPIO_Init(WM8960_LRC_PORT,&GPIO_InitStructure);

GPIO_InitStructure.GPIO_Pin=WM8960_BCLK_PIN;

GPIO_Init(WM8960_BCLK_PORT,&GPIO_InitStructure);

GPIO_InitStructure.GPIO_Pin=WM8960_MCLK_PIN;

GPIO_Init(WM8960_MCLK_PORT,&GPIO_InitStructure);

GPIO_InitStructure.GPIO_Pin=WM8960_DACDAT_PIN;

GPIO_Init(WM8960_DACDAT_PORT,&GPIO_InitStructure);

GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;

GPIO_InitStructure.GPIO_Pin=WM8960_ADCDAT_PIN;

GPIO_Init(WM8960_ADCDAT_PORT,&GPIO_InitStructure);/*Connect pins to I2S peripheral*/GPIO_PinAFConfig(WM8960_LRC_PORT, WM8960_LRC_SOURCE, WM8960_LRC_AF);

GPIO_PinAFConfig(WM8960_BCLK_PORT, WM8960_BCLK_SOURCE, WM8960_BCLK_AF);

GPIO_PinAFConfig(WM8960_ADCDAT_PORT, WM8960_ADCDAT_SOURCE, WM8960_ADCDAT_AF);

GPIO_PinAFConfig(WM8960_DACDAT_PORT, WM8960_DACDAT_SOURCE, WM8960_DACDAT_AF);

GPIO_PinAFConfig(WM8960_MCLK_PORT, WM8960_MCLK_SOURCE, WM8960_MCLK_AF);

}

View Code

(2)、配置I2S 发送,STM32做的从机,所以不需要配置MCLK,当然也不需要输出:

void WM8960_I2Sx_Mode_Config(const uint16_t _usStandard,const uint16_t _usWordLen,constuint32_t _usAudioFreq)

{

I2S_InitTypeDef I2S_InitStructure;#if 0 //STM32作为从机不需要配置时钟uint32_t n= 0;

FlagStatus status=RESET;/**

* For I2S mode, make sure that either:

* - I2S PLL is configured using the functions RCC_I2SCLKConfig(RCC_I2S2CLKSource_PLLI2S),

* RCC_PLLI2SCmd(ENABLE) and RCC_GetFlagStatus(RCC_FLAG_PLLI2SRDY).*/RCC_I2SCLKConfig(RCC_I2S2CLKSource_PLLI2S);

RCC_PLLI2SCmd(ENABLE);for (n = 0; n < 500; n++)

{

status=RCC_GetFlagStatus(RCC_FLAG_PLLI2SRDY);if (status == 1)break;

}#endif

/*打开 I2S2 APB1 时钟*/RCC_APB1PeriphClockCmd(WM8960_CLK, ENABLE);/*复位 SPI2 外设到缺省状态*/SPI_I2S_DeInit(WM8960_I2Sx_SPI);/*I2S2 外设配置*/

/*配置I2S工作模式*/I2S_InitStructure.I2S_Mode= I2S_Mode_SlaveTx;//I2S_Mode_MasterTx;

/*接口标准*/I2S_InitStructure.I2S_Standard=_usStandard;/*数据格式,16bit*/I2S_InitStructure.I2S_DataFormat=_usWordLen;/*主时钟模式*/I2S_InitStructure.I2S_MCLKOutput= I2S_MCLKOutput_Disable;//I2S_MCLKOutput_Enable;

/*音频采样频率*/I2S_InitStructure.I2S_AudioFreq=_usAudioFreq;

I2S_InitStructure.I2S_CPOL=I2S_CPOL_Low;

I2S_Init(WM8960_I2Sx_SPI,&I2S_InitStructure);/*使能 SPI2/I2S2 外设*/I2S_Cmd(WM8960_I2Sx_SPI, ENABLE);

SPI_I2S_DMACmd(WM8960_I2Sx_SPI,SPI_I2S_DMAReq_Tx,ENABLE);//SPI2 TX DMA请求使能.

}

View Code

(3)、配置I2S接收模式,配置双缓冲模式

void WM8960_I2Sxext_Mode_Config(const uint16_t _usStandard, const uint16_t _usWordLen,constuint32_t _usAudioFreq)

{

I2S_InitTypeDef I2Sext_InitStructure;/*I2S2 外设配置*/RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);/*复位 SPI2 外设到缺省状态*/SPI_I2S_DeInit(I2S2ext);

I2Sext_InitStructure.I2S_Mode= I2S_Mode_SlaveTx; /*配置I2S工作模式 注意这里是SlaveTx 而不是Rx,容易误导*/I2Sext_InitStructure.I2S_Standard= _usStandard; /*接口标准*/I2Sext_InitStructure.I2S_DataFormat= _usWordLen; /*数据格式,16bit*/I2Sext_InitStructure.I2S_MCLKOutput= I2S_MCLKOutput_Disable; /*主时钟模式*/I2Sext_InitStructure.I2S_AudioFreq= _usAudioFreq; /*音频采样频率*/I2Sext_InitStructure.I2S_CPOL=I2S_CPOL_Low;

I2S_Init(I2S2ext,&I2Sext_InitStructure);

I2S_FullDuplexConfig(I2S2ext,&I2Sext_InitStructure); //可以进入函数中看到,当I2S_Mode == I2S_Mode_SlaveTx时,选择的是tmp = I2S_Mode_SlaveRx;

/*使能 SPI2/I2S2 外设*/I2S_Cmd(I2S2ext, ENABLE);

SPI_I2S_DMACmd(I2S2ext,SPI_I2S_DMAReq_Rx,ENABLE);//SPI2 RX DMA请求使能.

}

View Code

这里注意下I2S模式选的是I2S_Mode_SlaceTx,你可能会觉得这个地方配置错了。这地方我也是查询了好久才找到。坑爹玩意

I2Sext_InitStructure.I2S_Mode = I2S_Mode_SlaveTx; /*配置I2S工作模式 注意这里是SlaveTx 而不是Rx,容易误导*/

其实不是,进到void I2S_FullDuplexConfig(SPI_TypeDef* I2Sxext, I2S_InitTypeDef* I2S_InitStruct) 这个函数里头,发现这代码是这样写的,最终是temp=I2S_Mode_SlaveRx;

/*Get the mode to be configured for the extended I2S*/

if ((I2S_InitStruct->I2S_Mode == I2S_Mode_MasterTx) || (I2S_InitStruct->I2S_Mode ==I2S_Mode_SlaveTx))

{

tmp=I2S_Mode_SlaveRx;

}else{if ((I2S_InitStruct->I2S_Mode == I2S_Mode_MasterRx) || (I2S_InitStruct->I2S_Mode ==I2S_Mode_SlaveRx))

{

tmp=I2S_Mode_SlaveTx;

}

}

(3)、配置DMA双缓冲发送和发送完产生的中断函数:

#define WM8960_I2Sx_DMA DMA1

#define WM8960_I2Sx_DMA_CLK RCC_AHB1Periph_DMA1

#define WM8960_I2Sx_TX_DMA_CHANNEL DMA_Channel_0

#define WM8960_I2Sx_TX_DMA_STREAM DMA1_Stream4

#define WM8960_I2Sx_TX_DMA_IT_TCIF DMA_IT_TCIF4

#define WM8960_I2Sx_TX_DMA_STREAM_IRQn DMA1_Stream4_IRQn

#define WM8960_I2Sx_TX_DMA_STREAM_IRQFUN DMA1_Stream4_IRQHandler

void WM8960_I2Sx_TX_DMA_Init(const uint16_t *buffer0,const uint16_t *buffer1,constuint32_t num)

{

NVIC_InitTypeDef NVIC_InitStructure;

DMA_InitTypeDef DMA_InitStructure;

RCC_AHB1PeriphClockCmd(WM8960_I2Sx_DMA_CLK,ENABLE);//DMA1时钟使能

DMA_DeInit(WM8960_I2Sx_TX_DMA_STREAM);while (DMA_GetCmdStatus(WM8960_I2Sx_TX_DMA_STREAM) != DISABLE){}//等待DMA1_Stream4可配置

DMA_ClearITPendingBit(WM8960_I2Sx_TX_DMA_STREAM,DMA_IT_FEIF4|DMA_IT_DMEIF4|DMA_IT_TEIF4|DMA_IT_HTIF4|DMA_IT_TCIF4);//清空DMA1_Stream4上所有中断标志

/*配置 DMA Stream*/DMA_InitStructure.DMA_Channel= WM8960_I2Sx_TX_DMA_CHANNEL; //通道0 SPIx_TX通道

DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&WM8960_I2Sx_SPI->DR;//外设地址为:(u32)&SPI2->DR

DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)buffer0;//DMA 存储器0地址

DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;//存储器到外设模式

DMA_InitStructure.DMA_BufferSize = num;//数据传输量

DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式

DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式

DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设数据长度:16位

DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//存储器数据长度:16位

DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;//使用循环模式

DMA_InitStructure.DMA_Priority = DMA_Priority_High;//高优先级

DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; //不使用FIFO模式

DMA_InitStructure.DMA_FIFOThreshold =DMA_FIFOThreshold_1QuarterFull;

DMA_InitStructure.DMA_MemoryBurst= DMA_MemoryBurst_Single;//外设突发单次传输

DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//存储器突发单次传输

DMA_Init(WM8960_I2Sx_TX_DMA_STREAM, &DMA_InitStructure);//初始化DMA Stream

DMA_DoubleBufferModeConfig(WM8960_I2Sx_TX_DMA_STREAM,(uint32_t)buffer0,DMA_Memory_0);//双缓冲模式配置

DMA_DoubleBufferModeConfig(WM8960_I2Sx_TX_DMA_STREAM,(uint32_t)buffer1,DMA_Memory_1);//双缓冲模式配置

DMA_DoubleBufferModeCmd(WM8960_I2Sx_TX_DMA_STREAM,ENABLE);//双缓冲模式开启

DMA_ITConfig(WM8960_I2Sx_TX_DMA_STREAM,DMA_IT_TC,ENABLE);//开启传输完成中断

DMA_Cmd(WM8960_I2Sx_TX_DMA_STREAM,DISABLE);

NVIC_InitStructure.NVIC_IRQChannel=WM8960_I2Sx_TX_DMA_STREAM_IRQn;

NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority= 0;//抢占优先级1

NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//子优先级2

NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能外部中断通道

NVIC_Init(&NVIC_InitStructure);//配置

}void WM8960_I2Sx_TX_DMA_STREAM_IRQFUN(void)

{if(DMA_GetITStatus(WM8960_I2Sx_TX_DMA_STREAM,WM8960_I2Sx_TX_DMA_IT_TCIF)==SET)//DMA传输完成标志

{

DMA_ClearITPendingBit(WM8960_I2Sx_TX_DMA_STREAM,WM8960_I2Sx_TX_DMA_IT_TCIF);//清DMA传输完成标准

if(WM8960_I2Sx_TX_DMA_STREAM->CR&(1<<19)) //当前使用Memory1数据

{

bufflag=0; //可以将数据读取到缓冲区0

}else //当前使用Memory0数据

{

bufflag=1; //可以将数据读取到缓冲区1

}

Isread_tx++;

}

}

View Code

(4)、配置DMA双缓冲接收和接收中断函数:

void WM8960_I2Sxext_RX_DMA_Init(const uint16_t *buffer0,const uint16_t *buffer1,constuint32_t num)

{

NVIC_InitTypeDef NVIC_InitStructure1;

DMA_InitTypeDef DMA_InitStructure1;

RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);//DMA1时钟使能

DMA_DeInit(DMA1_Stream3);while (DMA_GetCmdStatus(DMA1_Stream3) != DISABLE){}//等待DMA1_Stream3可配置

DMA_ClearITPendingBit(DMA1_Stream3,DMA_IT_FEIF3|DMA_IT_DMEIF3|DMA_IT_TEIF3|DMA_IT_HTIF3|DMA_IT_TCIF3);//清空DMA1_Stream3上所有中断标志

/*配置 DMA Stream*/DMA_InitStructure1.DMA_Channel= DMA_Channel_3; //通道0 SPIx_TX通道

DMA_InitStructure1.DMA_PeripheralBaseAddr = (uint32_t)&WM8960_I2Sx_ext->DR;//外设地址为:(u32)&SPI2->DR

DMA_InitStructure1.DMA_Memory0BaseAddr = (uint32_t)buffer0;//DMA 存储器0地址

DMA_InitStructure1.DMA_DIR = DMA_DIR_PeripheralToMemory;//外设到存储器模式

DMA_InitStructure1.DMA_BufferSize = num;//数据传输量

DMA_InitStructure1.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式

DMA_InitStructure1.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式

DMA_InitStructure1.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设数据长度:16位

DMA_InitStructure1.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//存储器数据长度:16位

DMA_InitStructure1.DMA_Mode = DMA_Mode_Circular;//使用循环模式

DMA_InitStructure1.DMA_Priority =DMA_Priority_VeryHigh;

DMA_InitStructure1.DMA_FIFOMode= DMA_FIFOMode_Disable; //不使用FIFO模式

DMA_InitStructure1.DMA_FIFOThreshold =DMA_FIFOThreshold_1QuarterFull;

DMA_InitStructure1.DMA_MemoryBurst= DMA_MemoryBurst_Single;//外设突发单次传输

DMA_InitStructure1.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//存储器突发单次传输

DMA_Init(DMA1_Stream3, &DMA_InitStructure1);//初始化DMA Stream

DMA_DoubleBufferModeConfig(DMA1_Stream3,(uint32_t)buffer0,DMA_Memory_0);//双缓冲模式配置

DMA_DoubleBufferModeConfig(DMA1_Stream3,(uint32_t)buffer1,DMA_Memory_1);//双缓冲模式配置

DMA_DoubleBufferModeCmd(DMA1_Stream3,ENABLE);//双缓冲模式开启

DMA_ITConfig(DMA1_Stream3,DMA_IT_TC,ENABLE);//开启传输完成中断

DMA_Cmd(DMA1_Stream3,DISABLE);

NVIC_InitStructure1.NVIC_IRQChannel=DMA1_Stream3_IRQn;

NVIC_InitStructure1.NVIC_IRQChannelPreemptionPriority= 0;//抢占优先级0

NVIC_InitStructure1.NVIC_IRQChannelSubPriority = 0;//子优先级2

NVIC_InitStructure1.NVIC_IRQChannelCmd = ENABLE;//使能外部中断通道

NVIC_Init(&NVIC_InitStructure1);//配置

}void DMA1_Stream3_IRQHandler(void)

{if(DMA_GetITStatus(DMA1_Stream3,DMA_IT_TCIF3)==SET)

{

DMA_ClearITPendingBit(DMA1_Stream3,DMA_IT_TCIF3);if(DMA1_Stream3->CR&(1<<19)) //当前使用Memory1数据

{

bufflag=1;

}else //当前使用Memory0数据

{

bufflag=0;

}

Isread_rx++; //DMA传输完成标志

}

}

View Code

(5)、主函数中调用测试语音录音和播放:

void WM8960_I2S_Play_Start(void)

{

DMA_Cmd(WM8960_I2Sx_TX_DMA_STREAM,ENABLE);//开启DMA TX传输,开始播放

}void WM8960_I2S_Play_Stop(void)

{

DMA_Cmd(WM8960_I2Sx_TX_DMA_STREAM,DISABLE);//关闭DMA TX传输,结束播放

}void WM8960_I2Sxext_Recorde_Start(void)

{

DMA_Cmd(WM8960_I2Sxext_RX_DMA_STREAM,ENABLE);//开启DMA RX传输,开始录音

}void WM8960_I2Sxext_Recorde_Stop(void)

{

DMA_Cmd(WM8960_I2Sxext_RX_DMA_STREAM,DISABLE);//关闭DMA RX传输,结束录音

}

View Code

externu8 Isread_tx;externu8 Isread_rx;

//注意两个数组别定义太大,定义太大STM空间不够,编译器会报错,这里一个数组存放了48630,两个数组48630*2externuint16_t adudio_buffer0[]; //可以事先定义好一个数组buffer0,先存放一点数据进去,不录音之前,也能播放一段声音出来externuint16_t adudio_buffer1[]; //可以事先定义好一个数组buffer1,先存放一点数据进去,不录音之前,也能播放一段声音出来externuint16_t ADUDIO_BUFFER_SIZE; //uint16_t ADUDIO_BUFFER_SIZE = sizeof(adudio_buffer0)/sizeof(uint16_t);#define DMA_Point_to_Memory0 0

#define DMA_Point_to_Memory1 1

void Audio_Set(void)

{

WM8960_I2S_GPIO_Config(); //I2S IO配置if(WM8960_Set_Play_Recorde_Reg())//对WM8960进行初始化

{

printf("WM8960 Init Fail!!!\r\n");

}elseprintf("WM8960 Init Success!!!\r\n");

WM8960_I2Sx_Mode_Config(I2S_Standard_Phillips,I2S_DataFormat_16b,I2S_AudioFreq_16k);//音频采样速率,这里配置16K,收发要保持一致

WM8960_I2Sx_TX_DMA_Init(adudio_buffer1,adudio_buffer0,ADUDIO_BUFFER_SIZE);//注意数量大小就是一个缓冲区的大小,而不是两个缓冲区大小之和,buffer1传给memory0,buffer0传给memory1

WM8960_I2Sxext_Mode_Config(I2S_Standard_Phillips,I2S_DataFormat_16b,I2S_AudioFreq_16k);//音频采样速率,这里配置16K,收发保持一致,否则播放出的音速就变了

//假如现在有一段录音“1234”,再同一时间内,44K采集到的数据比16K采集的数据多,加上STM数组空间有限,如果用44K采集的话,可能采集到“12”就填满了两个数组,录音播放的时候就只能听到前半截.//如果用16K去采集数据,“1234”都能存到两个数组中,录音播放就会比较完整。这里的44K好比就是无损音乐,数据大;16K就相当于MP3格式的音乐,数据少。常规听起来感觉到不差异。

//这里就是要配置ADC/DAC采样速率16K的原因。 WM8960_I2Sxext_RX_DMA_Init(adudio_buffer1,adudio_buffer0,ADUDIO_BUFFER_SIZE); //配置I2S DMA双缓冲接收,buffer1传给memory0,buffer0传给memory1

}void Audio_Play_Recorde(void)

{

Audio_Set();while(1)

{//播放音乐

if(KEYL==0)//按下左按键,进行播放

{

Delay_ms(1000);

WM8960_I2S_Play_Start();//开启播放

}if(Isread_tx == 2)

{

WM8960_I2S_Play_Stop();//两个数组的数据播放完后,停止播放

Isread_tx= 0;//标志清0

printf("Play Complete!!!\r\n");

printf("Please Recorde!!!\r\n");

}if(KEYR==0)//按下右键将两个数组清0

{

Delay_ms(1000);

memset(adudio_buffer1,0,sizeof(uint16_t)*ADUDIO_BUFFER_SIZE);

memset(adudio_buffer0,0,sizeof(uint16_t)*ADUDIO_BUFFER_SIZE);

printf("Clear Buffer Complete!!!\r\n");

}//录音

if(KEYM==0)//按下左键,开启录音

{

Delay_ms(1000);

WM8960_I2Sxext_Recorde_Start();//开始录音

}if(Isread_rx == 2)

{

WM8960_I2Sxext_Recorde_Stop();//录满两个数组后,停止录音

Isread_rx= 0;//标志清0

printf("Recorde Complete!!!\r\n");

printf("Please Play!!!\r\n");

}//

//if(bufflag==DMA_Point_to_Memory0)//说明Memory1中的数据可以拷贝//{//rx_flag = 2;//memcpy(tx_buffer0,rx_buffer0,sizeof(u8)*BUFFER_SIZE);//}//if(bufflag==DMA_Point_to_Memory1)//说明Memory0中的数据可以拷贝//{//rx_flag = 2;//memcpy(tx_buffer1,rx_buffer1,sizeof(u8)*BUFFER_SIZE);//}

}

}

总结:

(1)、为什么STM32配置主机,WM8960作为从机,能播放,但是接收不了数据,所以始终进不了中断函数,就无法录音?

答:播放时,是STM32将数据发给WM8960,是主动发送数据,会有时钟输出,而录音时,是STM32接收数据,

一直在那等待数据,示波器量MCLK 和Bitcllk没有时钟,虽然程序里配置使能MCLK时钟输出,但就是没有。

后来想到SPI接收数据的场景,在接收数据时,得让STM32发送任意字节,目的就是让时钟输出。接收不了数据的问题估计就是出在这里了。

static uint16_t SPIx_Receive_byte(void)

{while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE)==RESET); //检查缓冲区是否为空

SPI_I2S_SendData(SPI2,Dummy_Byte);//发送任意字节,就是为了时钟输出

while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE)==RESET);returnSPI_I2S_ReceiveData(SPI2);

}

我想STM32肯定可以做主机接收数据,只是现在没调试好,如果在接收时,是通过STM32发送数据产生时钟,那么WM8960会不会有杂音播放呢?

再一个采用的是DMA方式读取,看不到类似SPI那种接收读取的函数。又该如何配置呢? 这个问题可以好好研究。

(2)、在一个就是I2S 接收时的配置,虽然网上例子中,I2Sext_InitStructure.I2S_Mode = I2S_Mode_SlaveTx;  没有重点说明,

一开始按照网上的资料配置,不能录音,仔细检查代码,发现这个有疑问,明明是接收,怎么是配置为发送呢,让我窃喜,肯定是这个地方出错了,

于是改成了I2Sext_InitStructure.I2S_Mode = I2S_Mode_SlaveRx; 结果很失望,还是不能录音,于是单步调试进入I2S_FullDuplexConfig(I2S2ext, &I2Sext_InitStructure);

函数中,发现里面有这一段代码,原来如此,之前人家配置的I2S_Mode_SlaveTX是正确的。单步调试进入官方的库函数,仔细查看函数里面做了哪些,这种方法是非常有效。

if ((I2S_InitStruct->I2S_Mode == I2S_Mode_MasterTx) || (I2S_InitStruct->I2S_Mode ==I2S_Mode_SlaveTx))

{

tmp=I2S_Mode_SlaveRx;

}else{if ((I2S_InitStruct->I2S_Mode == I2S_Mode_MasterRx) || (I2S_InitStruct->I2S_Mode ==I2S_Mode_SlaveRx))

{

tmp=I2S_Mode_SlaveTx;

}

}

还有个细节,在配置I2S接收工作模式时,I2S_Init要不要写?"因为看到了I2S_FullDuplexConfig(I2S2ext, &I2Sext_InitStructure);

也是将I2Sext_InitStructure变量传进去进行初始化,I2S_Init()是不是多余了,这个还真不是多余,得一定要配置,I2S_Init()是对I2S协议、

数据格式、主时钟、音频采样、工作模式和空闲时的时钟电平状态进行初始化,而I2S_FullDuplexConfig函数仅仅是将I2S2ext扩展口设置为全双工模式。

I2S_Init(I2S2ext, &I2Sext_InitStructure);

I2S_FullDuplexConfig(I2S2ext,&I2Sext_InitStructure); //可以进入函数中看到,当I2S_Mode == I2S_Mode_SlaveTx时,选择的是tmp = I2S_Mode_SlaveRx;

(3)、其实到现在我还有个疑问?I2S2ext扩展口设置全双工模式时使用,全双工的无非就是发送时,一根数据线在发送,另一根数据线在接收,看下引脚对应

* I2S总线传输音频数据口线

* WM8960_LRC -> PB12/I2S2_WS //映射到NSS引脚,即帧时钟,用于切换左右声道的数据,WS频率等于音频信号采样率(fs),

//STM32发送时是要产生一个时钟给WM8960,WM8960根据该时钟区分左右声道,接收时呢? 目前程序里面好像是不用关心,是真的吗?

* WM8960_BCLK -> PB13/I2S2_CK //串行时钟(映射到SPI_SCK引脚),即位时钟,是主模式下的串行时钟输出以及从模式下的串行时钟输入。

* WM8960_ADCDAT -> PB14/I2S2ext_SD//扩展串行数据线(MISO),用于全双工传输的数据接收

* WM8960_DACDAT -> PB15/I2S2_SD //串行数据线(映射MOSI),用于发送或接收两个时分复用的数据通道上的数据(仅半双工模式),如果是全双工模式,该信号仅用于发送数据

* WM8960_MCLK -> PC6/I2S2_MCK //附加时钟,给外设音频模块提供工作主时钟

目前程序里好像只用到了半双工,收发都不是同时。

dma接收双缓存 stm32_STM32和WM8960 I2S 利用DMA双缓冲音频播放和录音(二)相关推荐

  1. 第38章 I2S—音频播放与录音输入—零死角玩转STM32-F429系列

    第38章     I2S-音频播放与录音输入 全套200集视频教程和1000页PDF教程请到秉火论坛下载:www.firebbs.cn 野火视频教程优酷观看网址:http://i.youku.com/ ...

  2. ws2812 程序设计与应用(2)DMA 控制 PWM 占空比(双缓存降低内存消耗)

    本文开发环境: MCU型号:STM32F103C8T6 IDE环境: MDK 5.27 代码生成工具:STM32CubeMx 5.6.1 HAL库版本:STM32Cube_FW_F1_V1.8.0 本 ...

  3. android 串口一直打开_STM32之串口DMA接收不定长数据

    STM32之串口DMA接收不定长数据 引言 在使用stm32或者其他单片机的时候,会经常使用到串口通讯,那么如何有效地接收数据呢?假如这段数据是不定长的有如何高效接收呢? 同学A:数据来了就会进入串口 ...

  4. STM32使用串口1配合DMA接收不定长数据,大大减轻CPU载荷。

    最近经常看见坛友在论坛上问串口接收的问题,我之前刚好由于项目需要用到PLC的PPI协议,需要不停地利用串口接收数据,一开始的时候采用单字节中断的方式接收判断.但是用来做通信的时候需要不停的产生串口接收 ...

  5. STM32使用串口1配合DMA接收不定长数据,大大减轻CPU载荷

    摘自:http://www.openedv.com/thread-63849-1-1.html 参考:https://blog.csdn.net/heda3/article/details/80602 ...

  6. STM32使用串口1配合DMA接收不定长数据,减轻CPU载荷

    STM32使用串口1配合DMA接收不定长数据,减轻CPU载荷 http://www.openedv.com/thread-63849-1-1.html 实现思路:采 用STM32F103的串口1,并配 ...

  7. rtthread 串口dma接收_RT-Thread 设备驱动UART浅析

    OS版本:RT-Thread 4.0.0 芯片:STM32F407 RT-Thread的串口驱动框架与Linux相识,分成 I/O设备框架 + 设备底层驱动: 1. serial设备初始化及使用 将配 ...

  8. 随想录(canvas双缓存下的性能分析)

    [ 声明:版权所有,欢迎转载,请勿用于商业用途. 联系信箱:feixiaoxing @163.com] 有过canvas编程经验的同学都知道,如果希望在客户端屏幕上不出现闪烁的情况,最好使用双缓存输出 ...

  9. 全志V3S裸机串口驱动(中断方式接收,DMA接收有问题,小于32字节数据无法触发DMA传输)

    调试DMA接收遇到了个很奇怪的问题,就是DMA发送没问题,DMA接收的时候,如果数据小于32字节,数据被DMA从串口接收FIFO中取走了,但是并不会传输到指定的buff中,这个就没法用于接收未知长度的 ...

最新文章

  1. Web页面布局方式小结
  2. 巧用 GitHub 创建自己的私人 Maven 仓库,及一些开发Library的建议
  3. windows消息定义
  4. 组合模式java怎么获取钥匙_java中组合模式详解和使用方法
  5. 16行代码AC——紫书| 例题7-3 Fractions Again?! (UVA - 10976)_时间复杂度O(n)
  6. [JLOI2014]松鼠的新家
  7. ubuntu16.04caffe训练mnist数据集
  8. python 从大到小循环_python算法(3) 插入排序
  9. 个人作业1——四则运算题目生成程序(基于java)
  10. 嘉楠勘智 K210 RISC-V 64位双核处理器开发板(荔枝丹)
  11. 1. crontab 简介
  12. Set集合之CopyOnWriteArraySet
  13. 关于 tp5 事务操作总结
  14. edius隐藏快捷键_Edius常用快捷键
  15. 人工智能资料下载地址分享
  16. 树莓派python识别二维码_树莓派识别二维码
  17. scl语言用plc脉冲做定时器_请问SCL语言如何调用定时器
  18. [dlang](二)用vibe.d+thrift做网络编程
  19. WorldPress博客系统更换域名导致博客访问出错的解决方案
  20. android popup

热门文章

  1. [渝粤教育] 西南科技大学 供应链管理 在线考试复习资料
  2. uart转RS422
  3. u盘linux和win7双系统安装教程,双系统安装Win7系统U盘安装linux centos7,详细步骤...
  4. LFSR线性反馈移位寄存器Verilog实现
  5. linux合并iso文件,多个ISO文件怎样合并为一个ISO文件(Linux下的操作)
  6. 支持Mac 基于SymbianOS的XCode插件发布
  7. 机房搬迁AIX系统调研指令模板
  8. jeecg导出excel设置样式XLS(03)与XLSX(07)
  9. 8086段地址为什么是16的倍数?
  10. 丛林战争项目八之Workbench创建数据库和表