MM32F0163D7P

1、MM32F0160的ADC简介

ADC外设是12位的逐次逼近型(SAR)模拟数字转换器,可以将模拟信号转换成数字信号。

ADC有16 个通道可测量内部或外部信号源,其中ADC有14 路外部输入通道和2路内部通道,如下图1 ADC的系统框图所示分别是通道14内部温度温度传感器通道,通道15内部电压传感器通道,即内部1.2V参考电压通道。

ADC的通道可以单次、单周期和连续进行转换。根据不同的方式又可以选择普通通道转换、任意通道转换、注入通道转换。ADC 的输入时钟不得超过16M,他是由 APB2 时钟(PCLK2) 分频产生。

1.png

图1 ADC系统框图

2、MM32F0160内部1.2V参考电压简介

MM32系列MCU出厂时,在Flash特定的内存地址存储了1.2V参考电压的校准值,即VDDA为3V3时采样1.2V电压的校准值。该校准值可以通过UM手册查阅。例如,可通过UM_MM32F0160_SC手册的ADC章节查阅到MM32F0163D7P的ADC内部1.2V参考电压的校准值存储在Flash特定的内存地址0x1FFFF7E0区域。根据本文章节1图1的ADC系统框图可知ADC内部1.2V参考电压通道为通道15。MM32其它系列的MCU的ADC内部1.2V参考电压校准值在Flash内存的存储地址,内部1.2V参考电压通道的通道号如下表1所示(注:其它MCU系列请参考UM手册)。

2.png

表1 MM32系列ADC内部1.2V参考电压校准值的存储地址和1.2V参考电压通道号

3、ADC内部1.2V参考电压的应用场景举例

ADC的参考电压为VDD,当MCU的供电VDD不稳定或采用电池供电时ADC的参考电压会随着VDD的波动而波动,随着电池电量和电压的下降而变化,从而影响ADC的采集和测量精度,这种场景就可以考虑使用MCU出厂时VDD为3.3V时ADC采样1.2V电压得到的采样校准值,读取该采样校准值并转换成电压值,作为ADC的间接参考电压使用。

4、ADC内部1.2V参考电压采样值的读取

以MM32F0163D7P为例,查阅UM_MM32F0160_SC手册ADC章节的ADC系统框图可知ADC内部1.2V参考电压通道为通道15(VSENSOR),在读取ADC内部1.2V参考电压采样值前需配置ADC通道15并使能该通道,使用任意通道配置核心代码如下所示,本文后面8.3章节有详细的应用举例。

/* Sampling value of each channel of ADC */
uint16_t ADC_Channel_Samp_Value[4] = {0x00};
/* Assign ADC1 1.2V Vref channel_15 to RANK 3 */   
ADC_ANY_CH_Config(ADC1,3,ADC_Channel_VoltReference);
/* Enable ADC 1.2V channel_15 voltage reference */
ADC1->ADCFG |=  ADC_ADCFG_VSEN;
/* Get the sampling value of ADC 1.2V channel 15 */    
ADC_Channel_Samp_Value[3] = ADC1->ADDR15;

5、ADC内部1.2V参考电压校准值的读取

以MM32F0163D7P为例,读取出厂VDDA为3V3时采样1.2V电压的采样校准值,并换算成电压值,核心代码如下所示。本文8.4章节有详细的读取举例。

/* 1.2V sampling calibration value storage address */
#define ADC_1_2V_SAMPLE_CALI_VALUE_MEM_ADDR 0x1FFFF7E0
/* Read ADC 1.2V Sample value */
Vref_1_2V_Cali = *(uint16_t*)(ADC_1_2V_SAMPLE_CALI_VALUE_MEM_ADDR);
/* Convert to voltage value */
Vref_1_2V_Cali_V = (float)(Vref_1_2V_Cali * (3.3/4096));

6、ADC各通道采样电压值与1.2V通道采样电压值换算关系

定义Calcu_ADC_Channel_V为缓存ADC各通道采样电压值,gADC_Sample_Value为缓存ADC各通道的采样值。ADC_Channel_Samp_Value[4]为缓存ADC各通道的采样值包括ADC的1.2V通道实时采样值,Vref_1_2V_Cali为VDDA为3V3时采样1.2V电压的采样校准值,Vref_1_2V_Cali由以上章节5 ADC内部1.2V参考电压校准值的读取得到,换算成电压值为Vref_1_2V_Cali_V = (float)(Vref_1_2V_Cali * (3.3/4096)) ,则存在以下关系式:

Calcu_ADC_Channel_V / gADC_Sample_Value = Vref_1_2V_Cali_V /ADC_Channel_Samp_Value[i]

其中i表示ADC各通道号包括ADC的1.2V通道。

由此,推得ADC各个通道的采样电压值为

Calcu_ADC_Channel_V = (float)(gADC_Sample_Value * (Vref_1_2V_Cali_V/ADC_Channel_Samp_Value[3]))

ADC_Channel_Samp_Value[3]缓存ADC的1.2V通道的实时采样值。即:ADC各通道的采样电压值=ADC各通道的采样值*(ADC内部采样1.2V电压的采样校准值换算得到的电压值 / ADC的1.2V通道的实时采样值)。

7、根据出厂ADC内部1.2V参考电压的校准值反推VDDA的电压值

由于Vref_1_2V_Cali / ADC_V1_2V_Channel_Sample = VDDA / 4096

得到VDDA = ( Vref_1_2V_Cali * 4096 ) / ADC_V1_2_Channel_Sample

注意事项:读取ADC_V1_2_Channel_Sample 采样值前需配置和使能1.2V通道(MM32F0163D7P的ADC 1.2V通道为通道15)。注:本文章节4有提到配置方法。

8、ADC使用注意事项

1)ADC高达1Msps转换速率,每个通道可以独立的设置采样保持时间,但需注意的是采样保持时间最小为2.5个ADC时钟周期,最大为240.5个ADC时钟周期,在应用场景条件允许情况下,在此范围内适当加大采样保持时间可以保证ADC采样精度。条件允许情况下适当降低ADC时钟频率也可以保证采样精度,但ADC的时钟不得超过16MHz。

2)ADC的内部1.2V Vref通道转换周期比规则转换通道的转换周期稍大一些,如果对ADC的采样速率有应用场景的要求,建议使用ADC的注入通道功能。

3)ADC的采样值受到输入阻抗的影响,关于ADC输入阻抗的计算可参考对应MCU系列的DS手册,电气特性,工作条件的ADC特性。

4)使用ADC时应注意用作ADC功能的GPIO的IO类型,例如IO类型为“TC”类型即标准的IO类型,不容忍VDD电压即ADC的输入采样电压不得超过VDD电源电压。此外GPIO内部带有上下拉钳位二极管,用户应避免输入ADC的采样电压过高引起倒灌影响ADC的工作条件和采样精度(关于IO类型的说明请参考对应MCU系列的DS手册管脚定义章节)。

9、ADC内部1.2V参考电压的使用完整举例

1)以MM32F0163D7P为例,查阅DS手册得到GPIO与ADC通道对应关系如下表2所示。

3.png表2 GPIO与ADC通道对应关系

2)以MM32F0163D7P为例,完整举例ADC内部1.2V参考电压的使用,本示例外接采样3路ADC电压值,首先初始化3路ADC通道,ADC输入的GPIO的分别为PA0对应ADC_Channel_0,PA1对应ADC_Channel_1,PA2对应ADC_Channel_2,ADC的GPIO初始化代码如下所示。

void ADC_GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    /* Enable GPIOA Clock */
    RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOA, ENABLE);
    GPIO_StructInit(&GPIO_InitStruct);
    /* PA0 ADC_CH0, PA1 ADC_CH1, PA2 ADC_CH2 */
    GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; 
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AIN;    
    GPIO_Init(GPIOA, &GPIO_InitStruct);
}

3)以MM32F0163D7P为例,初始化ADC为12bit精度,时钟为72M预分频为16分频,连续转换模式,数据格式右对齐,采样保持时间为240.5个ADC时钟周期,配置任意通道模式,配置并使能ADC内部1.2V通道Channel15,ADC的初始化代码如下所示。

void ADC_Configure(void)
{
    ADC_InitTypeDef ADC_InitStruct;
    RCC_APB2PeriphClockCmd(RCC_APB2ENR_ADC, ENABLE);    /* Enable ADC clock */
    ADC_StructInit(&ADC_InitStruct);
    ADC_InitStruct.ADC_Resolution = ADC_Resolution_12b;
    ADC_InitStruct.ADC_PRESCARE   = ADC_PCLK2_PRESCARE_16;
    /* ADC continue scan convert mode */
    ADC_InitStruct.ADC_Mode = ADC_Mode_Continue;   
/* AD data right-justified */       
    ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; 
    ADC_Init(ADC1, &ADC_InitStruct);
    /* Configure ADC ADC_Channel_x Sample time */
    ADC_Channel_Sample_time_Configure(ADC1,ADC_Channel_0,ADC_Samctl_240_5); 
    ADC_Channel_Sample_time_Configure(ADC1,ADC_Channel_1,ADC_Samctl_240_5);
    ADC_Channel_Sample_time_Configure(ADC1,ADC_Channel_2,ADC_Samctl_240_5);
    ADC_ANY_Cmd(ADC1, DISABLE); /* Disable ADC ANYChannel */   
    ADC_ANY_NUM_Config(ADC1,3); /* Configure Multi-Channel num */ 
    /* Assign PA0 ADC1 channel_0 to RANK 0 */     
    ADC_ANY_CH_Config(ADC1,0,ADC_Channel_0); 
    /* Assign PA1 ADC1 channel_1 to RANK 1 */ 
    ADC_ANY_CH_Config(ADC1,1,ADC_Channel_1); 
    /* Assign PA2 ADC1 channel_2 to RANK 2 */  
    ADC_ANY_CH_Config(ADC1,2,ADC_Channel_2); 
    /* Assign ADC1 1.2V Vref channel_15 to RANK 3 */   
    ADC_ANY_CH_Config(ADC1,3,ADC_Channel_VoltReference); 
    ADC_ANY_Cmd(ADC1, ENABLE);   /* Enable ADC ANYChannel */   
    /* Enable ADC 1.2V channel_15 voltage reference */
    ADC1->ADCFG |=  ADC_ADCFG_VSEN; 
    ADC_Cmd(ADC1, ENABLE); /* Enable ADC */
}

4)以MM32F0163D7P为例,读取出厂VDDA为3V3时采样1.2V电压的采样校准值,并换算成电压值,代码如下所示。

#define VDDA        3.3F
#define ADC_12BIT   4096U
#define ADC_RESOLUTION VDDA / ADC_12BIT
/* 1.2V sampling calibration value storage address */
#define ADC_1_2V_SAMPLE_CALI_VALUE_MEM_ADDR 0x1FFFF7E0
/* 1.2V sampling calibration value */
uint16_t Vref_1_2V_Cali  = 0;
/* 1.2V sampling calibration value Convert to voltage value */
float Vref_1_2V_Cali_V = 0.0;
void Read_Factory_1_2V_Sample_CaliValue(void)
{
    /* Read ADC 1.2V Sample value */
    Vref_1_2V_Cali = *(uint16_t*)(ADC_1_2V_SAMPLE_CALI_VALUE_MEM_ADDR);
    /* Convert to voltage value */
    Vref_1_2V_Cali_V = (float)(Vref_1_2V_Cali * ADC_RESOLUTION);
}

5)以MM32F0163D7P为例,由以上9.4章节读取出厂VDDA为3V3时采样1.2V电压的采样校准值,由以上9.3章节ADC内部1.2V参考电压通道的配置和使能,ADC的1.2V采样电压的校准值根据以上章节6表述的公式,结合ADC的1.2V通道15的实时采样值可用于反推VDDA的电压值,同时该采样校准值换算成电压值后可间接作为ADC多通道连续采样的参考电压。ADC内部1.2V参考电压实时采样值,ADC各通道采样值的读取并换算成电压值以及在main函数初始化UART1打印功能,ADC外设的初始化,读取ADC采样1.2V的校准值,在while(1)主循环中调用获取ADC各通道采样电压值函数的详细实现的代码如下所示。本示例通过UART1每隔200ms毫秒打印输出ADC各通道采样并换算得到电压值到串口调试助手。

/* Buffer the VDDA voltage value of the reversed ADC */
float ADC_VDDA = 0.0;
/* Sampling value of each channel of ADC */
uint16_t ADC_Channel_Samp_Value[4] = {0x00};
/* Buffer the sampling value of each channel of the ADC */
uint16_t gADC_Sample_Value = 0;
/* Buffer the voltage value of each channel of ADC */
float Calcu_ADC_Channel_V = 0.0;
void Calcu_ADC_Each_Channel_Voltage_Value(void)
{
    uint8_t i = 0;
    /* Software starts the ADC conversion */
    ADC_SoftwareStartConvCmd(ADC1, ENABLE); 
    /* ADC conversion flag */
    while(ADC_GetFlagStatus(ADC1, ADC_IT_EOC) == 0);
    /* Clears the adc's pending flags */
    ADC_ClearFlag(ADC1, ADC_IT_EOC);
    /* Get the sampling value of ADC PA0 channel 0 */
    ADC_Channel_Samp_Value[0] = ADC1->ADDR0;
    /* Get the sampling value of ADC PA1 channel 1 */
    ADC_Channel_Samp_Value[1] = ADC1->ADDR1;
    /* Get the sampling value of ADC PA2 channel 2 */
    ADC_Channel_Samp_Value[2] = ADC1->ADDR2;   
    /* Get the sampling value of ADC 1.2V channel 15 */    
    ADC_Channel_Samp_Value[3] = ADC1->ADDR15;               
    /* The factory 1.2V sampling calibration value and the 1.2V channel sampling value reverse the VDDA voltage value */
    ADC_VDDA = (Vref_1_2V_Cali_V *4096) / ADC_Channel_Samp_Value[3];
    printf("ADC_VDDA = %0.2f\r\n",ADC_VDDA);
    /* Print out the voltage value of each channel calculated by ADC */
    for(i = 0; i < 4; i++)
    {
        /* Obtain the sampling value of each channel of the ADC */
        gADC_Sample_Value = ADC_Channel_Samp_Value[i];      
        /* Calculate the voltage value of each channel according to the sampling value of each channel of the ADC */
        Calcu_ADC_Channel_V = (float)(gADC_Sample_Value * (Vref_1_2V_Cali_V/ADC_Channel_Samp_Value[3]));
        if(i == 3)
        {         
            printf("ADC1_CH15_VREF: %0.2fV\r\n",Calcu_ADC_Channel_V); 
        }
        else
        {
            printf("ADC1_CH%d: %0.2fV\r\n",i,Calcu_ADC_Channel_V);  
        }               
    } 
}


int main(void)
{
    /* SysTick Init */
    DELAY_Init();
    /* UART1 Init */
    CONSOLE_Init(115200);
    /* ADC Pin Configure */
    ADC_GPIO_Configure();
    /* ADC function Configure */
    ADC_Configure();
    /* Read factory 1.2V sample calibration value and convert to voltage value */
    Read_Factory_1_2V_Sample_CaliValue();
    while (1)
    {
        if(Tick_200ms_Flag == true)
        {
            Tick_200ms_Flag = false;
            /* Get and Calculate the voltage value of each channel of the ADC */
            Calcu_ADC_Each_Channel_Voltage_Value();
        }
    }
}

6)编译程序把烧录程序到核心板,如下图2所示,3路ADC分别采集外接的电压值,ADC各通道采样换算得到的电压值打印输出到串口调试助手,测试结果分别为ADC_VDDA该值是通过读取出厂VDDA为3V3时采样1.2V电压的采样校准值结合1.2V通道15的实时采样值反推得到的VDDA(即VDD的电压值)的电压值。ADC_CH0采集1.0V电压,实际采到1.03V,ADC_CH1接GND采集0V电压,实际采集到0V,ADC_CH2采集0.50V电压,实际采集到0.50V。其中ADC_CH15_VREF采集ADC内部1.2V通道实时采样得到的电压值,采集到1.18V,该值可参考以上9.4章节表述的出厂VDDA为3V3时采样1.2V电压的采样校准值换算得到的电压值接近。

4.png

图2

7)如下图3所示,换一组电压采集,ADC_CH0采集2.50V电压实际采集到2.51V,ADC_CH1采集3.32V电源电压实际采集到3.31V,ADC_CH2采集1.50V电压,实际采到1.50V电压。以上测试均在ADC允许的误差范围。

5.png

图3

来源:灵动MM32MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 83

1、TinyUSB(UAC)音频简介

UAC是USB Audio Class的缩写,USB音频类,一个像USB这样的通用数据接口,可以有很多种实现数字音频数据传输的方式。不同的开发者可以根据自己的喜好和需求,定义任意的控制方式,传输模式,音频格式等等参数。

在上一节我们在MM32F0163D7P 平台上成功的移植了TinyUSB,基于这个平台,今天我们来实现一个 uac2_headset 音频设备,这个设备支持基础的录音和放音功能,如果要支持音量调节/静音功能,还需要再添加一个 HID 变成复合设备。

2、快速移植一个UAC+HID复合设备

按照前面两篇文章,我们可以快速的移植出一个基于TinyUSB的UAC+HID复合设备,主要的代码片段如下:

/*------------- MAIN -------------*/
int main(void)
{
  USB_DeviceClockInit();//board_init();

  CONSOLE_Init(460800);  //enable printf debug

  // init device stack on configured roothub port
  tud_init(BOARD_TUD_RHPORT);

  TU_LOG1("UAC Headset & HID running\r\n");   // CFG_TUSB_DEBUG for debugging #if CFG_TUSB_DEBUG
// 0 : no debug 
// 1 : print error
// 2 : print warning
// 3 : print info

  while (1)
  {
    tud_task(); // TinyUSB device task
    audio_task();
    hid_task();
  }

  return 0;
}


#define EPNUM_AUDIO_IN    0x01
#define EPNUM_AUDIO_OUT   0x01

#define EPNUM_HID   0x03


uint8_t const desc_configuration[] =
{
    // Interface count, string index, total length, attribute, power in mA
    TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),

    // Interface number, string index, EP Out & EP In address, EP size
    TUD_AUDIO_HEADSET_STEREO_DESCRIPTOR(2, EPNUM_AUDIO_OUT, EPNUM_AUDIO_IN | 0x80),

    // Interface number, string index, protocol, report descriptor len, EP Out & In address, size & polling interval
    TUD_HID_INOUT_DESCRIPTOR(ITF_NUM_HID, 6, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, 0x80 | EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 10), 

};


//--------------------------------------------------------------------+
// String Descriptors
//--------------------------------------------------------------------+

// array of pointer to string descriptors
char const* string_desc_arr [] =
{
  (const char[]) { 0x09, 0x04 },  // 0: is supported language is English (0x0409)
  "TinyUSB",                      // 1: Manufacturer
  "TinyUSB headset",              // 2: Product
  "000001",                       // 3: Serials, should use chip ID
  "TinyUSB Speakers",             // 4: Audio Interface
  "TinyUSB Microphone",           // 5: Audio Interface
  "TinyUSB HID",                  // 6: HID Interface
};

1.png

图1 枚举过程

2.png

图2 枚举设备

工程文件树:

1.TinyUSB_UAC_HID

2.    │

3.    ├─USER

4.    │       main.c

5.    │       usb_descriptors.c

6.    │       usb_dcd_port.c

7.    │  

8.    └─TinyUSB

9. 

10.            tusb.c

11.            audio_device.c

12.            tud_fifo.c

13.            usbd.c

14.            usb_control.c

15.            hid_device.c

3、耳机设备设置修改音量

在耳机属性里面可以操作修改音量大小,同时通过抓包工具可以抓到主机下发的SET CUR命令。

3.png

图3 设置音量

4、耳机设备设置修改采样频率和位深度

在耳机属性高级里面默认格式可以看到下拉框有两种格式,一个是2通道 16位48000Hz,另外一个是2通道 24位 48000Hz,播放声音下发ISOC包的时候根据这个选择传输。

4.png

图4 设置采样频率和位深度

通过设置,下位机可以在播放或者录音时处理不同的采样频率和位深度。样例中全局变量current_resolution是位深度,current_sample_rate是采样频率。

5、音频数据的收发处理

播放时ISOC下发的数据包缓存在spk_buf里面。

录用时麦克风的声音采样数据写入mic_buf,通过ISOC同步上传到电脑端。(例程是将播放的数据包处理后通过接口又返回电脑端,由tud_audio_write((uint8_t *)mic_buf, (uint16_t) (spk_data_size / 2))实现)

//--------------------------------------------------------------------+
// AUDIO Task
//--------------------------------------------------------------------+

void audio_task(void)
{
  // When new data arrived, copy data from speaker buffer, to microphone buffer
  // and send it over
  // Only support speaker & headphone both have the same resolution
  // If one is 16bit another is 24bit be care of LOUD noise !
  if (spk_data_size)
  {
    if (current_resolution == 16)         
    {
      int16_t *src = (int16_t*)spk_buf;
      int16_t *limit = (int16_t*)spk_buf + spk_data_size / 2;
      int16_t *dst = (int16_t*)mic_buf;
      while (src < limit)
      {
        // Combine two channels into one
        int32_t left = *src++;
        int32_t right = *src++;
        *dst++ = (int16_t) ((left >> 1) + (right >> 1));
      }
      memset(mic_buf,0xCC,sizeof(mic_buf));  //mm32 test code 
      tud_audio_write((uint8_t *)mic_buf, (uint16_t) (spk_data_size / 2));
      spk_data_size = 0;
    }
    else if (current_resolution == 24)
    {
      int32_t *src = spk_buf;
      int32_t *limit = spk_buf + spk_data_size / 4;
      int32_t *dst = mic_buf;
      while (src < limit)
      {
        // Combine two channels into one
        int32_t left = *src++;
        int32_t right = *src++;
        *dst++ = (int32_t) ((uint32_t) ((left >> 1) + (right >> 1)) & 0xffffff00ul);
      }
      tud_audio_write((uint8_t *)mic_buf, (uint16_t) (spk_data_size / 2));
      spk_data_size = 0;
    }
  }
}

6、功能验证测试

将uac2_headset 音频设备插入PC,在计算机管理->设备管理器->音频输入和输出里面出现耳机和麦克风两个TinyUSB headset设备证明枚举成功。

5.png

图5 设备管理器音频设备

电脑端播放音乐使用抓包工具抓到ISOC的数据包:

6.png

图6 ISOC数据包

来源:灵动MM32MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 177

1、新增一个class里面没有参考示例的设备

如果用户想增加一个device设备但是在TinyUSB class里面又没有参考示例,本次我们一起来移植一个CDC+printer复合设备。操作步骤还是和上篇一样,先将TinyUSB 从GitHub上克隆下来,将src 整个文件夹copy替换到例程components目录下的src。

1.png

图1 TinyUSB源码

将tinyusb 目录下example下的对应文件,将tinyusb\examples\device\cdc_dual_ports\src 三个文件copy到例程user文件夹里面,其他增加时钟配置函数和添加tud_dcd_port.c 接口函数文件和上一篇一致(tud_dcd_port.c 文件可以参考现有例程或者联系灵动技术支持)。

2.png

图2 工程的USER文件

在tinyusb\src\class里面新增一个printer文件夹,可以参考其他设备比如从hid里面copy两个hid_device.c和hid_device.h 文件做为模板,将文件名修改成printer_device.c和printer_device.h,在这个文件里面修改对应的函数接口。

3.png

图3 新增printer文件

工程文件树如下:

1. TinyUSB_CDC_Printer

2.     │

3.     ├─USER

4.     │       main.c

5.     │       usb_descriptors.c

6.     │       usb_dcd_port.c

7.     │  

8.     └─TinyUSB

9.  

10.            tusb.c

11.            cdc_device.c

12.            tud_fifo.c

13.            usbd.c

14.            usb_control.c

15.           printer_device.c

2、修改printer接口函数

1、在printer接口文件里面需要修改实现以下几个函数:

printer_init,              //接口变量初始化
printer_reset,            //接口变量重置
printer_open,           
printer_control_xfer_cb,   //控制接口回调
printer_xfer_cb           //数据接口回调
tud_printer_task           //while(1)循环打印机数据处理

4.png

图4 需要实现的接口函数

2、在tusb_config.h 文件里面增加宏定义#define CFG_TUD_PRINTER 1 ,同时将对应的设备define改成2 ( #define CFG_TUD_CDC 2 ) ,使能两个CDC设备。

//------------- CLASS -------------//
#define CFG_TUD_CDC               2
#define CFG_TUD_MSC               0
#define CFG_TUD_PRINTER           1
#define CFG_TUD_HID               0
#define CFG_TUD_MIDI              0
#define CFG_TUD_VENDOR            0

3、在usbd.c 里面增加printer回调函数接口处理。

#if CFG_TUD_PRINTER
    {
        DRIVER_NAME("PRINTER")
        .init             = printer_init,
        .reset            = printer_reset,
        .open             = printer_open,
        .control_xfer_cb  = printer_control_xfer_cb,
        .xfer_cb          = printer_xfer_cb,
        .sof              = NULL
    },
#endif

4、在tusb.h增加printer对应的头文件。

#if CFG_TUD_PRINTER
  #include "class/printer/printer_device.h"
#endif

5、在tusb.h增加printer描述符。

//--------------------------------------------------------------------+
// PRINTER Descriptor Templates
//--------------------------------------------------------------------+

// Length of template descriptor: 23 bytes
#define TUD_PRINTER_DESC_LEN    (9 + 7 + 7)

// Interface number, string index, EP Out & EP In address, EP size
#define TUD_PRINTER_DESCRIPTOR(_itfnum, _stridx, _epout, _epin, _epsize) \
  /* Interface */\
  9, TUSB_DESC_INTERFACE, _itfnum, 0, 2, TUSB_CLASS_PRINTER, 0x01, 0x02, 0,\
  /* Endpoint Out */\
  7, TUSB_DESC_ENDPOINT, _epout, TUSB_XFER_BULK, U16_TO_U8S_LE(_epsize), 0x00,\
  /* Endpoint In */\
  7, TUSB_DESC_ENDPOINT, _epin, TUSB_XFER_BULK, U16_TO_U8S_LE(_epsize), 0x00

6、修改usb_descriptors.c 的描述符,增加一个printer的描述符和对应的端点。

//--------------------------------------------------------------------+
// Configuration Descriptor
//--------------------------------------------------------------------+
enum
{
  ITF_NUM_PRINTER= 0,    
  ITF_NUM_CDC_0,
  ITF_NUM_CDC_0_DATA,
  ITF_NUM_CDC_1,
  ITF_NUM_CDC_1_DATA,
  ITF_NUM_TOTAL
};

  #define EPNUM_CDC_0_NOTIF   0x81
  #define EPNUM_CDC_0_OUT     0x02
  #define EPNUM_CDC_0_IN      0x82

  #define EPNUM_CDC_1_NOTIF   0x83
  #define EPNUM_CDC_1_OUT     0x04
  #define EPNUM_CDC_1_IN      0x84

  #define EPNUM_PRINTER_OUT     0x05
  #define EPNUM_PRINTER_IN      0x86

uint8_t const desc_fs_configuration[] =
{
  // Config number, interface count, string index, total length, attribute, power in mA
  TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100),

  TUD_PRINTER_DESCRIPTOR(ITF_NUM_PRINTER, 5, EPNUM_PRINTER_OUT, EPNUM_PRINTER_IN, 64),  

  // 1st CDC: Interface number, string index, EP notification address and size, EP data address (out, in) and size.
  TUD_CDC_DESCRIPTOR(ITF_NUM_CDC_0, 4, EPNUM_CDC_0_NOTIF, 8, EPNUM_CDC_0_OUT, EPNUM_CDC_0_IN, 64),

  // 2nd CDC: Interface number, string index, EP notification address and size, EP data address (out, in) and size.
  TUD_CDC_DESCRIPTOR(ITF_NUM_CDC_1, 4, EPNUM_CDC_1_NOTIF, 8, EPNUM_CDC_1_OUT, EPNUM_CDC_1_IN, 64),
};

7、增加device id描述符和string字符串。

//--------------------------------------------------------------------+
// String Descriptors
//--------------------------------------------------------------------+

// array of pointer to string descriptors
char const* string_desc_arr [] =
{
  (const char[]) { 0x09, 0x04 }, // 0: is supported language is English (0x0409)
  "TinyUSB",                      // 1: Manufacturer
  "TinyUSB Device",              // 2: Product
  "123456",                       // 3: Serials, should use chip ID
  "TinyUSB CDC",                 // 4: CDC Interface
  "MM32 Printer",                // 5: printer Interface
};

uint8_t const desc_printer_device_id[] = {
   0x00,0x08, 'p', 'r', 'i', 'n', 't', '0', '0', '0',
};

uint8_t * tud_printer_device_id_cb(void)
{
    //(void) itf;
    return desc_printer_device_id;
}


8、对应的需要在printer_device.c的printer_control_xfer_cb实现 device id的数据回复。

bool printer_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const* p_request)
{
    uint8_t  * devicedesc;
    // nothing to do with DATA & ACK stage
    if (stage != CONTROL_STAGE_SETUP) return true;
        // Handle class request only
    TU_VERIFY(p_request->bmRequestType_bit.type == TUSB_REQ_TYPE_CLASS);

    switch ( p_request->bRequest ) {
        case 0x00:
             devicedesc = tud_printer_device_id_cb();
             tud_control_xfer(rhport, p_request, (void*) devicedesc, 10);

            break;
        case 0x01:
             devicedesc = tud_printer_device_id_cb();
             tud_control_xfer(rhport, p_request, (void*) devicedesc, 10);            
            break;
        default:
            return false; // stall unsupported request
    }

    return true;
}

9、在tusb_config.h文件里面添加#define CFG_TUSB_MCU OPT_MCU_MM32F016X

Tusb_option.h 文件里面增加:

#define OPT_MCU_MM32F016X        1501 ///< MindMotion MM32F0160

否则TUP_DCD_ENDPOINT_MAX 没有定义。

5.png

6.png

图5 增加MM32F0160 宏定义

10、在main.c 里面主循环增加三个处理函数:

tud_task();    
cdc_task();   
tud_printer_task();

main函数:

/*------------- MAIN -------------*/
int main(void)
{
  USB_DeviceClockInit(); // board_init();

  CONSOLE_Init(460800); //Config uart2

  // init device stack on configured roothub port
  tud_init(BOARD_TUD_RHPORT);

  TU_LOG1("TinyUSB Printer & CDC\r\n");

  while (1)
  {
    tud_task(); // tinyusb device task
    cdc_task();
    tud_printer_task();  //user printer task
  }
}

3、功能验证测试

完成上述移植修改解决基本的编译问题后烧录测试能枚举打印机和CDC两个设备。

7.png

图6 枚举过程

8.png

图7 枚举成功

选择打印机设备,直接打印word的内容,能抓到通过枚举的打印机28号设备端点4下发的数据。

9.png

图8 枚举成功

10.png

图9 printer EP4 OUT包数据

相关链接:基于MM32F0163D7P的USB接口TinyUSB应用:移植和新增设备(一)

来源:灵动MM32MCU百科

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 251

1、TinyUSB基本介绍

TinyUSB是一个用于嵌入式系统的开源的跨平台USB协议栈,协议栈中包含了主机端及设备端的协议栈,由于不使用动态内存分配以及采用阻塞所有中断事件,在非ISR任务功能中处理中断事件的设计方式,所以此协议栈的内存安全性及线程安全性极高。

源码是托管在GitHub上面,地址是:https://github.com/hathach/tinyusb。

1.png

2、TinyUSB基本移植介绍

MM32已基于TinyUSB开发完成相应的参考例程,并可以给客户提供参考,本次我们介绍的移植将基于此基础上进行。将TinyUSB从GitHub上克隆到本地,可以得到如下内容:

2.png

图1 TinyUSB源码

移植TinyUSB到MM32F0160需要添加如下文件:

属于工程专用文件:

usb_descriptors.c

usb_dcd_port.c

tusb_config.h

usb_descriptors.h

属于库内文件:

tusb.c

usbd.c

usbd_control.c

*_device.c

tusb_fifo.c

将src 整个文件夹copy替换到例程components目录下的src。

将tinyusb 目录下example下的对应文件,以device uac2_headset为例,将tinyusb\examples\device\uac2_headset\src 四个文件copy到例程user文件夹里面。

3.png

图2 源码的device文件

4.png

图3 用户工程文件

USB时钟频率是48MHz,HSE可以经过PLL倍频到48MHz或96MHz,然后经过分频到48MHz,注意需要使用外部晶振,如果使用内部时钟HSI,需要使能时钟回馈系统CRS功能。

在main.c 增加USB时钟配置函数void USB_DeviceClockInit(void),同时将board_init();替换成usb时钟初始化函数,主频配置96MHz,USB选择PLL1输入二分频到USB。

有使用TU_LOG做串口输出,可以使能CFG_TUSB_DEBUG为需要的输出等级,同时将

#define tu_printf    printf

改到串口输出,Keil Options->Target 勾选Use MicroLIB,并实现重定向函数。

//------------- clock initial -------------//
void USB_DeviceClockInit(void)         //HSE 96M
{
    /* Select USBCLK source */
    RCC->CFGR &= ~(1 << 19);           //USB CLK SEL PLL1

    RCC->CFGR &= ~(0x03 << 22);
    RCC->CFGR |= 0x01 << 22;

    RCC_AHBPeriphClockCmd(RCC_AHBENR_USB, ENABLE);
}

//------------- MAIN -------------//
int main(void)
{
    USB_DeviceClockInit();             //board_init();

    CONSOLE_Init(460800);              //enable printf debug

    //init device stack on configured roothub port
    tud_init(BOARD_TUD_RHPORT);

    TU_LOG1("Headset running\r\n"); //CFG_TUSB_DEBUG for debugging #if CFG_TUSB_DEBUG

    //0 : no debug
    //1 : print error
    //2 : print warning
    //3 : print info
    while (1)
    {
        tud_task();                    //tinyusb device task
        led_blinking_task();
        audio_task();
    }
}

添加tud_dcd_port.c 接口函数文件,Keil下Options C/C++勾选C99和GNU externsions(tud_dcd_port.c 文件可以参考现有例程或者联系灵动技术支持)。

5.png

图4 工程设置

移植修改其他设备基本流程和上述一致,将tinyusb 目录example\device\ 里面将想要修改的设备src文件夹里面四个文件copy到例程user文件夹里面替换。

3、修改一个uac2_headset Device设备

在克隆下来的的文件夹examples\device里面找到需要修改的device设备,本次修改uac2_headset。将里面的文件都copy到工程USER目录里面,然后Keil工程按如下文件树添加对应文件。

6.png

图5 uac2_headset设备描述符文件

文件树:

1.TinyUSB_UAC

2.    │

3.    ├─USER

4.    │       main.c

5.    │       usb_descriptors.c

6.    │       usb_dcd_port.c

7.    │  

8.    └─TinyUSB

9.            tusb.c

10.            audio_device.c

11.            tud_fifo.c

12.            usbd.c

13.            usb_control.c

在tusb_config.h文件里面CLASS将对应的设备define改成1 ( #define CFG_TUD_AUDIO  1 ) ,使能AUDIO设备。

//------------- CLASS -------------//
#define CFG_TUD_CDC               0
#define CFG_TUD_MSC               0
#define CFG_TUD_HID               0
#define CFG_TUD_MIDI              0
#define CFG_TUD_AUDIO             1
#define CFG_TUD_VENDOR            0

按照前面的移植步骤,只需要修改main.c 里面的时钟初始化部分即可,其他的device设备修改流程一致。

4、新增一个设备变成复合设备

USB设备主要四个描述符,分别是设备描述符(Device Descriptors),配置描述符(Configuration Descriptor),报告描述符(Configuration Descriptor)和字符描述符(String Descriptors)。

添加复合设备device文件,本例程在上述3(修改一个uac2_headset Device设备)例程的基础上增加一个HID设备变成复合设备,首先将工程目录下的components\tinyusb\src\class\hid\hid_device.c文件添加到工程。

7.png

图6 源码HID设备参考文件

8.png

图7 添加device文件

在tusb_config.h 文件里面 CLASS 使能HID宏,本例程是复合设备(Audio+HID)所以两个宏都为1。

//------------- CLASS -------------//
#define CFG_TUD_CDC               0
#define CFG_TUD_MSC               0
#define CFG_TUD_HID               1
#define CFG_TUD_MIDI              0
#define CFG_TUD_AUDIO             1
#define CFG_TUD_VENDOR            0

在usb_descriptors.c 文件里面添加HID的描述符,增加HID Report Descriptor相关函数。

//--------------------------------------------------------------------+
//HID Report Descriptor
//--------------------------------------------------------------------+
uint8_t const desc_hid_report[] =
{
    TUD_HID_REPORT_DESC_GENERIC_INOUT(CFG_TUD_HID_EP_BUFSIZE)
};

//Invoked when received GET HID REPORT DESCRIPTOR
//Application return pointer to descriptor
//Descriptor contents must exist long enough for transfer to complete
uint8_t const *tud_hid_descriptor_report_cb(uint8_t itf)
{
    (void)itf;
    return (desc_hid_report);
}

在usb_descriptors.c 文件里面添加HID Descriptor length(注意:长度一定要和下面DESCRIPTOR对应,否则枚举会失败)。

#define CONFIG_TOTAL_LEN        (TUD_CONFIG_DESC_LEN + CFG_TUD_AUDIO * TUD_AUDIO_HEADSET_STEREO_DESC_LEN + TUD_HID_INOUT_DESC_LEN)

在usb_descriptors.c 文件里面添加HID描述符文件,例程使用的是TUD_HID_INOUT_DESCRIPTOR,和上述CONFIG_TOTAL_LEN里面的TUD_HID_INOUT_DESC_LEN对应,然后配置HID IN OUT通讯选择哪个端点。

#define EPNUM_HID               0x03

uint8_t const desc_configuration[] =
{
    //Interface count, string index, total length, attribute, power in mA
    TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),

    //Interface number, string index, EP Out & EP In address, EP size
    TUD_AUDIO_HEADSET_STEREO_DESCRIPTOR(2, EPNUM_AUDIO_OUT, EPNUM_AUDIO_IN | 0x80),

    //Interface number, string index, protocol, report descriptor len, EP Out & In address, size & polling interval
    TUD_HID_INOUT_DESCRIPTOR(ITF_NUM_HID, 6, HID_ITF_PROTOCOL_NONE,  sizeof(desc_hid_report), EPNUM_HID, 0x80 | EPNUM_HID,CFG_TUD_HID_EP_BUFSIZE, 10),
};

在usb_descriptors.c 文件里面添加 HID string字符串。

//array of pointer to string descriptors
char const *string_desc_arr[] =
{
    (const char[]){ 0x09, 0x04 },    //0: is supported language is English (0x0409)
    "TinyUSB",                         //1: Manufacturer
    "TinyUSB headset",                //2: Product
    "000001",                          //3: Serials, should use chip ID
    "TinyUSB Speakers",               //4: Audio Interface
    "TinyUSB Microphone",             //5: Audio Interface
    "TinyUSB HID",                     //6: HID Interface
};

在usb_descriptors.h 文件里面ITF_NUM_TOTAL 增加一个 ITF_NUM_HID。

enum
{
    ITF_NUM_AUDIO_CONTROL = 0,
    ITF_NUM_AUDIO_STREAMING_SPK,
    ITF_NUM_AUDIO_STREAMING_MIC,
    ITF_NUM_HID,
    ITF_NUM_TOTAL
};

在main.c 里面增加hid_task(); 然后将HID的其他处理函数添加到main.c。

/*------------- MAIN -------------*/
int main(void)
{
  USB_DeviceClockInit();//board_init();

  CONSOLE_Init(460800);  //enable printf debug

  // init device stack on configured roothub port
  tud_init(BOARD_TUD_RHPORT);

  TU_LOG1("UAC2 Headset & HID running\r\n");   /// CFG_TUSB_DEBUG for debugging #if CFG_TUSB_DEBUG
  // 0 : no debug 
  // 1 : print error
  // 2 : print warning
  // 3 : print info

  while (1)
  {
    tud_task(); // TinyUSB device task
    audio_task();
    hid_task();
  }

  return 0;
}

在hid_task()函数中添加需要处理的用户程序。

//--------------------------------------------------------------------+
// USB HID
//--------------------------------------------------------------------+
uint8_t hid_report_data[64];

static void send_hid_report(uint8_t report_id, uint32_t btn)
{
  // skip if hid is not ready yet
  if ( !tud_hid_ready() ) return;

  switch(report_id)
  {
    case REPORT_ID_MOUSE:
    {
      int8_t const delta = 5;

      // no button, right + down, no scroll, no pan
      if(btn)
      {
        tud_hid_mouse_report(REPORT_ID_MOUSE, 0x00, delta, delta, 0, 0);
      }
    }
    break;

    default: break;
  }

}

// Every 10ms, we will sent 1 report for each HID profile (keyboard, mouse etc ..)
// tud_hid_report_complete_cb() is used to send the next report after previous one is complete
void hid_task(void)
{
  uint32_t const btn = 1u;

  // Remote wakeup
  if ( tud_suspended() && btn )
  {
    // Wake up host if we are in suspend mode
    // and REMOTE_WAKEUP feature is enabled by host
    tud_remote_wakeup();
  }else
  {
    // Send the 1st of report chain, the rest will be sent by tud_hid_report_complete_cb()
    send_hid_report(REPORT_ID_MOUSE, btn);
  }
}

5、功能验证测试

完成上述移植,解决基本的编译问题后烧录测试能枚举正常。

9.png

图8 枚举过程

10.png

图9 枚举成功

来源:灵动MM32MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 852

I2S(Inter-IC Sound)总线,又称集成电路内置音频总线,是飞利浦公司为数字音频设备之间的音频数据传输而制定的一种总线标准。采用了独立的导线传输时钟与数据信号的设计,通过将数据和时钟信号分离,避免了因时差诱发的失真。

MM32F0160系列的I2S 接口有以下主要特征:

  • 半双工通信(仅发送器或接收器)

  • 主操作或从操作

  • 9 位可配置线性预分频器,以达到精确的音频采样频率(8KHz~192KHz)

  • 数据帧格式可配置为 16 位、24 位或 32 位

  • 数据包帧固定为 16 位(16 位有效数据)或 32 位(16 位、24 位、32 位有效数据)

  • 可配置时钟极性(稳定状态)

  • 发送模式下具有下溢标志(仅从机),接收模式下具有上溢标志(主/从机)和发送/接收模式下的帧错误标志(仅从机)

  • 用于传输和接收的 32 位寄存器为两个声道分时复用

  • 数据方向始终是 MSB 优先

  • 支持 I2S 协议:

    飞利浦标准

    MSB 对齐标准(MSB 位向左对齐)

    LSB 对齐标准(LSB 位向右对齐)

    PCM 标准(具有短帧同步模式、长帧同步模式的两种方式)

  • 利用 DMA 请求传输数据(32 位宽)

  • 可配置 MCLK 时钟输出来驱动外部音频组件,其比率固定在 256×Fs(其中 Fs 为音频采样频率)

1、MM32的I2S总线简述

MM32F0160的I2S总线与SPI总线复用即SPI_I2S串行外设(串行外设接口与集成电路内置音频总线)。

I2S总线接口与SPI总线接口引脚复用关系如下:

SD:串行数据(映射在MOSI引脚上),用于发送或接收两次多路数据通道(仅在半双工模式下)。

WS:声道选择(映射在NSS引脚上),是主模式控制数据的输出信号,或从模式的输入。

CK:串行时钟(映射在SCK引脚上),是主模式串行时钟的输出,或从模式串行时钟的输入。

MCK:可选的串行时钟(映射在MISO引脚上),用于驱动外部音频组件(仅当外部音频设备需要时钟输入时使用,由主模式提供)。

2、SPI_I2S功能框图简介

1.png

图1 SPI_I2S功能框图

如上图1所示为SPI_I2S外设的功能框图,SPI_I2S通过“总线接口逻辑”挂载在APB和DMA总线上,TXREG和RXREG寄存器、主模式控制单元和从模式控制单元、主从选择控制、收发控制逻辑以及时钟生成及控制单元,8Byte的发送缓冲和8Byte的接收缓冲构成,时钟控制单元由Spbrg和Pclk提供时钟。

3、SPI_I2S外设的I2S时钟预分频器

I2SCLK时钟由系统APB时钟提供,I2S模块的预分频器电路结构如下图2所示:

2.png

图2 SPI_I2S时钟预分频器电路结构图

如上图2所示,当MCKOE位为‘0’时芯片不需要输出MCK时钟,预分频器直接将I2SCLK分频到CK;当MCKOE位为‘1’时芯片会输出MCK时钟,预分频器将I2SCLK分频后得到MCK,然后再经过分频处理才得到CK(分频倍数由CHLEN选择为 4或8)。

音频采样率一般常用 192KHz,96 KHz,48 KHz,44.1 KHz,32 KHz,22.05 KHz,16 KHz,11.025KHz,8 KHz。因此可根据 I2S 时钟分频器的电路功能式样,配置寄存器 I2SCFGR中的I2SDIV[8:0]、MCKOE和CHLEN 位来得到期望的音频采样率。

I2S 传输数据时,比特率计算公式如下表1所示(CK 输出一个时钟周期对应传输 1 比特数据,因此比特率 = CK频率FCK)。

3.png

表1

音频采样率(Fs)和 I2S 比特率的关系由如下的公式定义:

Fs = I2S 比特率/(通道长度×通道数)= FCK /(通道长度×通道数)

注:通道长度,即数据包帧长度,可配置为16位或32位;通道数为左右声道,值固定为2。

综上所述,根据I2SDIV[8: 0]、MCKOE和CHLEN位的配置情况, 得到音频采样率与FI2SCLK(APB时钟频率)的关系如下表2所示:

4.png

表2

4、SPI_I2S外设的I2S接口的音乐播放器

工作原理介绍

基于I2S接口的音乐播放器工作原理框图如下图3所示:

5.png

图3 I2S接口的音乐播放器原理框图

如上图3所示为I2S接口的音乐播放器工作原理框图:

MM32F0160作为Host MCU其SPI1接口用于驱动25WQ80存储器用于写入和读取存储的音频文件。

MCU端I2S2(SPI2_I2S2)接口工作在从机模式,MCLK不输出时钟。通过PWM输出12MHz的REF_Clock给NAU88C22音频编解码芯片MCLK脚,NAU88C22内部PLL合成稳定的12.288MHz作为内部IMLCK主时钟。NAU88C22 BCLK输出bit clock时钟到MCU端I2S2_CK作为音频采样时钟。I2S2_WS接口即FS用于分时切换左右声道。I2S2_SD接口即DACIN输出从25WQ80存储器读取的音频信号流DAC Stream传输给NAU88C22音频编解码芯片。

MCU端I2C_SDA和I2C_SCL接口用于设置NAU88C22工作模式和参数。NAU88C22 DAC输出可以选择从Speaker PA输出推喇叭或者从Earphone PA耳机接口输出推动耳机。

5、I2S接口的GPIO初始化

I2S接口GPIO的初始化代码如下所示:

void I2S2_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;

    RCC_GPIO_ClockCmd(GPIOB, ENABLE);

    GPIO_StructInit(&GPIO_InitStruct);
    /* PB12 I2S2_WS */
    GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_12;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    /* PB13 I2S2_CK */
    GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_13;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    /* PB14 I2S2_MCLK */
    GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_14;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_IPU;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    /* PB15 I2S2_SD */
    GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_15;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    /* PB12 AF I2S2_WS */
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource12, GPIO_AF_0);

    /* PB12 AF I2S2_CK */
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource13, GPIO_AF_0);

    /* PB12 AF I2S2_MCLK */
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource14, GPIO_AF_0);

    /* PB12 AF I2S2_SD */
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource15, GPIO_AF_0);
}

6、I2S从机的初始化

I2S从机的初始化步骤如下所示:

1)配置SPI_I2S_GCTL.SPIEN位为‘1’,开启模块使能;

2)配置SPI_I2S_GCTL.MODE位为‘0’,使模块功能为从模式;

3)配置寄存器SPI_I2S_I2SCFGR中的 I2SDIV[8:0]、 DATLEN 和 CHLEN 位,以符合希望得到的音频采样频率及数据包帧格式;

4)配置SPI_I2S_I2SCFGR.SPI_I2S位为‘1’,使能 I2S 传输功能;

5)配置寄存器SPI_I2S_I2SCFGR中的I2SSTD[1: 0]、 PCMSYNC 位,选择I2S传输时使用的通信标准;

6)配置SPI_I2S_GCTL.DMAMODE 位为‘1’,以启用 DMA 传输;

7)开启半双工传输许可, 即配置寄存器SPI_I2S_GCTL 中的 TXEN 或 RXEN 位为‘1’ (TXEN、 RXEN不可同时配置为‘1’)。

注意:从模式下发送时,在检测到WS的边沿之前,需要对寄存器SPI_I2S_TXREG进行1次数据写入操作;而且,从模式下接收时,在配置RXEN位为‘1’之前,需要一直维持WS输入信号在高电平。

I2S从机的初始化代码如下所示:

void I2S2_Slave_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1ENR_SPI2, ENABLE); /* Enable SPI2_I2S Clock */

    SPI2->CCTL &= ~SPI_CCTL_LSBFE;  /* MSB first enable */
    SPI2->CCTL &= ~SPI_CCTL_CPHA;   /* Clock phase select start second clock */    
    SPI2->CCTL |= SPI_CCTL_CPHASEL; /* CPHA polarity select start second clock */
    SPI2->CCTL |= SPI_CCTL_SPILEN;  /* SPI character length 8bit data */
    SPI2->CCTL |= SPI_CCTL_CPOL;    /* Clock polarity select high */
    SPI2->CCTL |= SPI_CCTL_TXEDGE;  /* Transmit data edge for i2s bus */

    SPI2->I2SCFGR &= ~SPI_I2SCFGR_MCKOE; /* I2S master clock output disable */  
    SPI2->I2SCFGR &= ~SPI_I2SCFGR_CHLEN; /* Vocal tract length 16bit */    
    SPI2->I2SCFGR |= SPI_I2SCFGR_DATLEN_32; /* Audio data width 32 */
    SPI2->I2SCFGR |= SPI_I2SCFGR_I2SSTD_Philips; /* I2S STD Philips */
    SPI2->I2SCFGR |= SPI_I2SCFGR_SPI_I2S; /* SPI/I2S module function selection */

    SPI2->GCTL &= ~SPI_GCTL_MODE;           /* I2S Slave mode */
    SPI2->GCTL |= SPI_GCTL_DW_8_32;         /* double-word data select signal */
    SPI2->GCTL |= SPI_GCTL_DMAMODE;         /* DMA access mode enable */
    SPI2->GCTL |= SPI_GCTL_TXEN;            /* I2S Transmit enable */
    SPI2->GCTL |= SPI_GCTL_INTEN;           /* SPI_I2S interrupt enable */
    SPI2->GCTL |= SPI_GCTL_SPIEN;           /* Enable I2S */    
}

7、I2S发送DAC音频信号流

MM32F0163D7P的I2S2发送DAC音频信号流使用DMA中断发送,代码接口如下所示:

void I2S2_TxData_DMA_Interrupt(uint8_t *Buffer, uint8_t datasize)
{
    DMA_InitTypeDef  DMA_InitStruct;
    NVIC_InitTypeDef NVIC_InitStruct;

    RCC_DMA_ClockCmd(DMA1, ENABLE);                /* Enable DMA1 Clock */

    DMA_DeInit(DMA1_Channel5);   

    DMA_StructInit(&DMA_InitStruct);                        
    DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&(SPI2->TXREG);   /* SPI2_I2S2 BaseAddr */
    DMA_InitStruct.DMA_MemoryBaseAddr     = (uint32_t)(Buffer);         /* Memory buffer for music data*/
    DMA_InitStruct.DMA_DIR                = DMA_DIR_PeripheralDST;
    DMA_InitStruct.DMA_BufferSize         = datasize;                   /* Left and Right channel audio buffer size */
    DMA_InitStruct.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
    DMA_InitStruct.DMA_MemoryInc          = DMA_MemoryInc_Enable;       /* memory increment */
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; /* half word transfer */
    DMA_InitStruct.DMA_MemoryDataSize     = DMA_MemoryDataSize_HalfWord;    /* half word transfer */
    DMA_InitStruct.DMA_Mode               = DMA_Mode_Normal;                /* Normal mode */
    DMA_InitStruct.DMA_Priority           = DMA_Priority_Medium;            /* DMA Priority Medium */
    DMA_InitStruct.DMA_M2M                = DMA_M2M_Disable;                /* Disable memory to memory transfer */
    DMA_InitStruct.DMA_Auto_reload        = DMA_Auto_Reload_Enable;         /* Enable auto reload */
    DMA_Init(DMA1_Channel5, &DMA_InitStruct);
    DMA_ClearFlag(DMA1_FLAG_TC5);                           /* Clear transfer complete flag */
    DMA_ITConfig(DMA1_Channel5, DMA_IT_TC, ENABLE);         /* Enable DMA Channel5 SPI2_I2S2 DMA transfer complete interrupt */                   
    NVIC_InitStruct.NVIC_IRQChannel = DMA1_Channel4_7_IRQn; /* Set SPI2_I2S2 DMA Channel NVIC intterrupt priority */
    NVIC_InitStruct.NVIC_IRQChannelPriority = 0x01;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;            /* Enable NVIC IRQChannel */
    NVIC_Init(&NVIC_InitStruct);    

    DMA_Cmd(DMA1_Channel5, ENABLE); /* Enable SPI2_I2S2 DMA Channel5 */
    SPI_DMACmd(SPI1, ENABLE);       /* Enable SPI_I2S */

    while (0 == I2S2_TX_DMA_InterruptFlag)  
    {
    }
}

I2S的DMA中断处理函数:

void DMA1_Channel4_7_IRQHandler(void)
{
    if(SET == DMA_GetITStatus(DMA1_IT_TC5))
    {
        /* I2S2 DMA interrupt transfer flag */
        I2S2_TX_DMA_InterruptFlag = 1;

        /* Clear transfer complete interrupt flag */
        DMA_ClearITPendingBit(DMA1_IT_TC5);

        /* Enable SPI2_I2S2 DMA Channel5 */
        DMA_Cmd(DMA1_Channel5, ENABLE);
    }
}

8、MCU输出12MHz的PWM给NAU88C22音频编解码芯片

配置MM32F0163D7P TIM1输出12MHz的PWM给NAU88C22合成12.228MHz时钟给NAU88C22的IMCLK和MCU的I2S,可参考MM32F0160_Samples中的TIM1 PWM输出例程。

9、SPI1接口的GPIO驱动25WQ80存储器

SPI1接口的GPIO初始化代码如下所示:

void SPI1_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;

    RCC_GPIO_ClockCmd(GPIOA, ENABLE);
    RCC_GPIO_ClockCmd(GPIOB, ENABLE);

    GPIO_PinAFConfig(GPIOA, GPIO_PinSource15, GPIO_AF_0);  /* PA15 AF SPI1_CS */
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource3, GPIO_AF_0);   /* PB3 AF SPI1_SCK */
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource4, GPIO_AF_0);   /* PB5 AF SPI1_MOSI */
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource5, GPIO_AF_0);   /* PB4 AF SPI1_MISO */

    GPIO_StructInit(&GPIO_InitStruct);
    GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_15;               /* PA15 SPI1_CS */
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_3;                /* PB3 SPI1_CS */
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_5;                /* PB5 SPI1_CS */
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Pin   = GPIO_Pin_4;                /* PB4 SPI1_CS */
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_IPU;
    GPIO_Init(GPIOB, &GPIO_InitStruct);
}

SPI1接口驱动25WQ80存储器,存储待读取的音频信号,经过SPI1读取后通过I2S2接口传输音频信号流到NAU88C22音频编解码芯片解码播放音乐,其初始化代码如下所示:

void SPI1_NVIC_Config(uint16_t spi_baud_div)
{
    SPI_InitTypeDef SPI_InitStruct;
    NVIC_InitTypeDef NVIC_InitStruct;

    /* SPI1 NVIC Priority Config */
    NVIC_InitStruct.NVIC_IRQChannel = SPI1_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPriority = 0x01;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);    

    RCC_SPI_ClockCmd(SPI1, ENABLE);             /* Enable SPI1 Clock */

    SPI_DeInit(SPI1);
    SPI_StructInit(&SPI_InitStruct);
    SPI_InitStruct.SPI_Mode = SPI_Mode_Master;  /* SPI master mode */
    SPI_InitStruct.SPI_DataSize  = SPI_DataSize_8b;
    SPI_InitStruct.SPI_DataWidth = 8;
    SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low;     /* The clock is low in idle state. */
    SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;   /* Data sampling starts from the first clock edge */
    SPI_InitStruct.SPI_NSS  = SPI_NSS_Soft;   

    /* SPI data edge adjust in fast speed mode */
    SPI_InitStruct.SPI_BaudRatePrescaler = (SPI_BaudRatePrescaler_TypeDef)spi_baud_div
    ;
    SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB; /* Data transfers start from MSB */
    SPI_Init(SPI1, &SPI_InitStruct);

    if(SPI_InitStruct.SPI_BaudRatePrescaler <= 8)
    {
        exSPI_DataEdgeAdjust(SPI1, SPI_DataEdgeAdjust_FAST);
    }
    /* Enable Receive available data interrupt and transmitter empty interrupt */
    SPI_ITConfig(SPI1, SPI_IT_RX | SPI_IT_TXEPT, ENABLE);

    SPI_BiDirectionalLineConfig(SPI1, SPI_Direction_Rx);    /* Receive enable */
    SPI_BiDirectionalLineConfig(SPI1, SPI_Direction_Tx);    /* Transmit enable */
    SPI_Cmd(SPI1, ENABLE);                                     /* Enable SPI1 */
}

SPI1读写25WQ80存储器函数接口请参考MM32F0160_Samples中的SPI_FLASH_Interrupt例程。

10、I2C接口的初始化

I2C1主机模式驱动NAU88C22音频编解码芯片用于发送指令控制NAU88C22音频编解码芯片工作。I2C1接口的GPIO初始化代码如下所示:

void I2C1_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;

    RCC_GPIO_ClockCmd(GPIOB, ENABLE);

    GPIO_PinAFConfig(GPIOB, GPIO_PinSource10, GPIO_AF_1);  /* PB10 AF I2C1_SCL */
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource11, GPIO_AF_1);  /* PB10 AF I2C1_SDA */

    GPIO_StructInit(&GPIO_InitStruct);
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;                 /* PB10 I2C1_SCL */
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;

    /* Keep the bus free which means SCK & SDA is high */
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_11;                 /* PB11 I2C1_SDA */
    GPIO_Init(GPIOB, &GPIO_InitStruct);
}

I2C1接口作为主机驱动NAU88C22音频编解码芯片工作,其初始化代码如下所示:

void I2C_Master_Mode_Init(I2C_TypeDef *I2Cx, uint32_t I2C_speed)
{
    I2C_InitTypeDef I2C_InitStruct;

    RCC_I2C_ClockCmd(I2C1, ENABLE);         /* Enable I2C clock */

    I2C_StructInit(&I2C_InitStruct);
    I2C_InitStruct.Mode = I2C_CR_MASTER;    /* Configure I2C as master mode */
    I2C_InitStruct.OwnAddress = 0;

    if (I2C_speed > 200000)                 /* more than 200K */
    {
        I2C_InitStruct.Speed = I2C_CR_SPEED_FAST;   /* I2C fast speed mode */
    }
    else
    {
        I2C_InitStruct.Speed = I2C_CR_SPEED_STD;    /* I2C standard speed mode */
    }

    I2C_InitStruct.ClockSpeed = I2C_speed;          /* I2C Speed */

    I2C_Init(I2Cx, &I2C_InitStruct);
    I2C_Cmd(I2Cx, ENABLE);
}

I2C1设置从机地址代码如下所示:

void I2C_Set_DeviceAddr(I2C_TypeDef *I2Cx, uint8_t deviceaddr)
{
    /* Disable I2C */
    I2C_Cmd(I2Cx, DISABLE);

    /* Set the device address */
    I2C_Send7bitAddress(I2Cx, deviceaddr, I2C_Direction_Transmitter);

    /* Enable I2C */
    I2C_Cmd(I2Cx, ENABLE);
}

I2C1的读写数据和命令的操作参考LibSamples_MM32F0160 I2C例程。

11、编译I2S程序烧录烧录到开发板中播放音乐

编译MM32F0163D7P的I2S2程序并烧录到开发板中实现音乐播放功能。

实验1:播放三角波测试文件实测输出波形

6.png

实验2:播放48KHz/24BIT立体声WAVE格式音乐文件实测输出波形

7.png

来源:灵动MM32MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 248
订阅 RSS - MM32F0163D7P