STM32学习笔记----USB虚拟串口

USB简介

USB (Universal Serial BUS),即通用串行总线,是一个外部总线标准,用于规范电脑与外部设备的链接和通信, USB在1994年末由英特尔、康柏、IBM、Microsoft等多家公司联合提出。web

USB有USB1.0/1.1/2.0/3.0等多个版本。目前用的最多的是USB2.0和USB3.0。标准USB由四根线组成:VCC、GND、D+和D-,D+和D-是数据线,采用差分传输。在USB主机上,D-和D+都是接了15K的电阻到地的,因此在没有设备接入的时候,D+、D-均是低电平。而在USB设备中,若是是高速设备,则会在D+上接一个1.5K的电阻到VCC,而若是是低速设备,则会在D-上接一个1.5K的电阻到VCC,当设备接入主机时,主机就能够判断是否有设备接入,并能判断设备是高速设备仍是低速设备。数组

STM32F103系列芯片,都自带了一个USB从控制器,支持USB全速(FS,12Mbps)通讯,符合USB 2.0技术规范。缓存

STM32F103的USB模块实现了标准USB接口的全部特性,它由:串行接口控制器(SIE)、定时器、分组缓冲器接口、端点相关寄存器、控制寄存器和中断寄存器等部分组成。app

USB数据传输经过一个专用的数据缓冲区来完成,它能被USB外设直接访问,其大小由所使用的端点数目和每一个端点最大数据分组大小所决定,每一个端点最大可以使用512字节缓冲区(专用的512字节,和CAN共用),最多可用于16个单向或8个双向端点。USB模块同PC主机通讯,根据USB规范实现了:令牌分组的检测、数据发送/接收的处理、握手分组的处理。整个传输的格式由硬件完成,其中包括CRC的生成和校验。
注意:USB与CAN共用一个专用的512字节SRAM存储器,用于数据的收发,他们必须互斥访问,因此CAN和USB不能同时使用!!!

详细的USB协议是极其复杂的,建议买本书看吧。
electron

USB结构框图

在这里插入图片描述

USB驱动

要正常使用STM32F103的USB,就得编写USB驱动,而整个USB通讯的详细过程是很复杂的,若是要咱们本身编写USB驱动,那是一件至关困难的事情。
不过,ST提供了咱们一个完整的USB FS从机驱动库和一个操做说明文件“UM0424 User manual.pdf”,经过这些,咱们能够很方便的实现咱们所要的功能,而不须要详细了解USB的整个驱动,大大缩短了咱们的开发时间和精力。svg

ST提供的USB FS从机驱动库,咱们能够在ST官方社区: http://www.stmcu.org下载到(搜索关键字:UM0424)。下载的文件名:STSW-STM32121.zip 。该库包含了STM32F103的USB从机(Device)驱动库,并提供了8个例程供咱们参考。
在这里插入图片描述
这8个例程,虽然都是基于官方EVAL板,可是很容易移植到咱们的STM32开发板上,本例程咱们就移植:STM32_USB-FS-Device_Lib_V4.0.0
Projects\Virtual_COM_Port这个例程,以实现USB虚拟串口的功能。函数

硬件链接

STM32F103芯片具备USB功能,其引脚对应以下:
PA11----USBDM----D-
PA12----USBDP----D+
在这里插入图片描述测试

准备文件

一、ST提供的USB FS从机驱动库,咱们能够在ST官方社区: http://www.stmcu.org下载到(搜索关键字:UM0424)。下载的文件名:STSW-STM32121.zip 。该库包含了STM32F103的USB从机(Device)驱动库,并提供了8个例程供咱们参考。
二、TFTLCD工程实验工程大数据

开始移植库

一、打开官方的例程 STM32_USB-FS-Device_Lib_V4.0.0 --> Projects --> Virtual_COM_Port,打开时提示须要安装部分固件库,直接忽视,随后选择红色箭头的标识。此时编译会有报错,这是由于例程是在MDK4下的,使用MDK5的话须要在 inc 文件夹里添加core_cm3.h、 core_cm3.h(库函数版本的工程),或者 core_cm3.h、core_cmInstr.h、core_cmFunc.h(寄存器版本的工程,从寄存器例程里拷贝)。ui

这里只是打开查看下驱动文件,core3_cm3.h等文件在TFTLCD卡实验工程里有的。
在这里插入图片描述
在这里插入图片描述
二、拷贝USB从机驱动
1)在 TFTLCD 工程目录下新建 USB 文件夹。
2)打开 STM32_USB-FS-Device_Lib_V4.0.0 --> Libraries 找到 STM32_USB-FS-Device_Driver 文件夹,将该文件夹拷贝到 USB 目录下。
3)在USB目录下新建config文件夹,拷贝 Virtual_COM_Port 相关代码进去。
在这里插入图片描述
三、打开 TFTLCD 工程,添加USB代码
在这里插入图片描述
四、编译代码,根据报错修改(12个报错)
1)platform_config.h的include部分,使用以下代码替代:

#include "sys.h"

在这里插入图片描述
2)修改hw_config.c
a. 去掉全部的头文件,包括stm32_it.h,并添加一些其余头文件,以下:

#include "usb_lib.h"
#include "usb_prop.h"
#include "usb_desc.h"
#include "usb_istr.h"
#include "hw_config.h"
#include "usb_pwr.h" 
#include "usart.h" 
#include "string.h" 
#include "stdarg.h" 
#include "stdio.h"

在这里插入图片描述
b. 去掉HSEStartUpStatus和EXTI_InitStructure等结构体和变量的定义,采用以下代码替代:

usb_usart_fifo uu_txfifo;//USB串口发送FIFO结构体,在hw_config.h定义
 
u8  USART_PRINTF_Buffer[USB_USART_REC_LEN];//usb_printf发送缓冲区
//用相似串口1(串口通讯实验)接收数据的方法,来处理USB虚拟串口接收到的数据.
u8 USB_USART_RX_BUF[USB_USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节

//接收状态
//bit15, 接收完成标志
//bit14, 接收到0x0d
//bit13~0, 接收到的有效字节数目
u16 USB_USART_RX_STA=0;//接收状态标记

extern LINE_CODING linecoding;//USB虚拟串口配置信息(波特率,位数等),在usb_prop.c定义

c. 删掉Set_System函数,并添加USBWakeUp_IRQHandler和USB_LP_
CAN1_RX0_IRQHandler函数(官方例程是在stm32_it.c里面,咱们将其移到这里),代码以下:

//USB唤醒中断服务函数
void USBWakeUp_IRQHandler(void) 
{
	EXTI_ClearITPendingBit(EXTI_Line18);//清除USB唤醒中断挂起位
} 
//USB中断处理函数
void USB_LP_CAN1_RX0_IRQHandler(void) 
{
	USB_Istr();//处理各类USB中断事件
}

d. 修改Set_USBClock函数为:

//USB时钟配置函数,USBclk=48Mhz@HCLK=72Mhz
void Set_USBClock(void)
{
	RCC_USBCLKConfig(RCC_USBCLKSource_PLLCLK_1Div5);	//USBclk=PLLclk/1.5=48Mhz
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USB, ENABLE);	 //USB时钟使能 
}

e. 修改Enter_LowPowerMode函数为:

void Enter_LowPowerMode(void)
{
 	printf("usb enter low power mode\r\n");
	bDeviceState=SUSPENDED;
	//bDeviceState记录USB链接状态,在usb_pwr.c里面定义
}

f. 修改Leave_LowPowerMode函数为:

void Leave_LowPowerMode(void)
{
	DEVICE_INFO *pInfo=&Device_Info;
	printf("leave low power mode\r\n"); 
	if (pInfo->Current_Configuration!=0)bDeviceState=CONFIGURED; 
	else bDeviceState = ATTACHED; 
}

g. 修改USB_Interrupts_Config函数为

void USB_Interrupts_Config(void)
{ 	
	NVIC_InitTypeDef NVIC_InitStructure;
	EXTI_InitTypeDef EXTI_InitStructure; 
	/* Configure the EXTI line 18 connected internally to the USB IP */
	EXTI_ClearITPendingBit(EXTI_Line18);		// 开启线18上的中断
	EXTI_InitStructure.EXTI_Line = EXTI_Line18; 	// USB resume from suspend mode
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;	//line 18上事件上升降沿触发
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
	EXTI_Init(&EXTI_InitStructure); 	

	/* Enable the USB interrupt */
	NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;//组2,优先级次之 
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
	
	/* Enable the USB Wake-up interrupt */
	NVIC_InitStructure.NVIC_IRQChannel = USBWakeUp_IRQn;   //组2,优先级最高 
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_Init(&NVIC_InitStructure);  	
}

h. 修改USB_Cable_Config函数为

//USB接口配置(配置1.5K上拉电阻,ALIENTEK的M3系列开发板,不须要配置,固定加了上拉电阻)
//NewState:DISABLE,不上拉,ENABLE,上拉
void USB_Cable_Config (FunctionalState NewState)
{ 
	if (NewState!=DISABLE)printf("usb pull up enable\r\n"); 
	else printf("usb pull up disable\r\n"); 
}

i. 删除USART_Config_Default函数 ,新增USB_Port_Set函数,代码以下

//USB使能链接/断线 enable:0,断开;1,容许链接 
void USB_Port_Set(u8 enable)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);    //使能PORTA时钟 
	if(enable)_SetCNTR(_GetCNTR()&(~(1<<1)));//退出断电模式
	else
	{	  
		_SetCNTR(_GetCNTR()|(1<<1));  // 断电模式
		GPIOA->CRH&=0XFFF00FFF;
		GPIOA->CRH|=0X00033000;
		PAout(12)=0;	    		  
	}
}

j. 修改USART_Config函数为:

//USB COM口的配置信息,经过此函数打印出来. 
bool USART_Config(void)
{
	uu_txfifo.readptr=0;	//清空读指针
	uu_txfifo.writeptr=0;	//清空写指针
	USB_USART_RX_STA=0;		//USB USART接收状态清零
	printf("linecoding.format:%d\r\n",linecoding.format);
  	printf("linecoding.paritytype:%d\r\n",linecoding.paritytype);
	printf("linecoding.datatype:%d\r\n",linecoding.datatype);
	printf("linecoding.bitrate:%d\r\n",linecoding.bitrate);
	return (TRUE);
}

k. 修改USB_To_USART_Send_Data函数为:

//处理从USB虚拟串口接收到的数据。参数:databuffer:数据缓存区,Nb_bytes:接收到的字节数.
void USB_To_USART_Send_Data(u8* data_buffer, u8 Nb_bytes)
{ 
    u8 i; u8 res;
    for(i=0;i<Nb_bytes;i++)
    {  
        res=data_buffer[i]; 
        if((USB_USART_RX_STA&0x8000)==0)	//接收未完成
        {
            if(USB_USART_RX_STA&0x4000)	//接收到了0x0d
            {
                 if(res!=0x0a)USB_USART_RX_STA=0;	//错误,从新开始
                 else USB_USART_RX_STA|=0x8000;	//接收完成了 
             }else //还没收到0X0D
             {	
                  if(res==0x0d)USB_USART_RX_STA|=0x4000;//标记接收到了0X0d
                  else
                  {       	          
                        USB_USART_RX_BUF[USB_USART_RX_STA&0X3FFF]=res;USB_USART_RX_STA++; 	   
                        if(USB_USART_RX_STA>(USB_USART_REC_LEN-1))USB_USART_RX_STA=0;//错误,重收
                   }					
             }
         }   
     }
}

l. 删除Handle_USBAsynchXfer和USART_To_USB_Send_Data这两个函数,而后,新增USB_USART_SendData函数,用于虚拟串口发送一个字节到USB(这里实际上只写到了发送FIFO,最终仍是由EP1_IN_Callback函数实现输出给USB):

//发送一个字节数据到USB虚拟串口
void USB_USART_SendData(u8 data)
{
	uu_txfifo.buffer[uu_txfifo.writeptr]=data;
	uu_txfifo.writeptr++;
	if(uu_txfifo.writeptr==USB_USART_TXFIFO_SIZE)//超过buf大小了,归零.
	{
		uu_txfifo.writeptr=0;
	} 
}

m .删除IntToUnicode函数前面的static关键字,新增usb_printf函数,用于实现USB虚拟串口的printf,代码以下:

//usb虚拟串口,printf 函数
//确保一次发送数据不超USB_USART_REC_LEN字节
void usb_printf(char* fmt,...)  
{  
	u16 i,j;
	va_list ap;
	va_start(ap,fmt);
	vsprintf((char*)USART_PRINTF_Buffer,fmt,ap);
	va_end(ap);
	i=strlen((const char*)USART_PRINTF_Buffer);//这次发送数据的长度
	for(j=0;j<i;j++)//循环发送数据
	{
		USB_USART_SendData(USART_PRINTF_Buffer[j]); 
	}
}

n. 修改hw_config.h,删除MASS_MEMORY_START等宏定义,而后,新增USB_USART_TXFIFO_SIZE等宏定义和结构体,代码以下:

#define USB_USART_TXFIFO_SIZE 1024 //USB虚拟串口发送FIFO大小 
#define USB_USART_REC_LEN 200 //USB串口接收缓冲区最大字节数
//定义一个USB USART FIFO结构体
typedef struct  
{								    
	u8  buffer[USB_USART_TXFIFO_SIZE];	//buffer
	vu16 writeptr;			//写指针
	vu16 readptr;			//读指针
}_usb_usart_fifo; 
extern _usb_usart_fifo uu_txfifo;		//USB串口发送FIFO
extern u8  USB_USART_RX_BUF[USB_USART_REC_LEN]; //接收缓冲,最大USB_USART_REC_LEN个字节 
extern u16 USB_USART_RX_STA;   		//接收状态标记

o. 修改hw_config.h, 新增IntToUnicode、USB_USART_SendData、USB_Port_Set和usb_printf等函数的声明,代码以下:

void IntToUnicode (u32 value,u8 *pbuf,u8 len);
void USB_USART_SendData(u8 data);
void USB_Port_Set(u8 enable);
void usb_printf(char* fmt,...);
  1. 修改usb_endp.c文件
    a. 修改EP1_IN_Callback函数为:
void EP1_IN_Callback (void)
{
	u16 USB_Tx_ptr; u16 USB_Tx_length; 
	if(uu_txfifo.readptr==uu_txfifo.writeptr)return;	//无任何数据要发送,直接退出
	if(uu_txfifo.readptr<uu_txfifo.writeptr)		//没有超过数组,读指针<写指针
	{
		USB_Tx_length=uu_txfifo.writeptr-uu_txfifo.readptr;	//获得要发送的数据长度
	}else USB_Tx_length=USB_USART_TXFIFO_SIZE-uu_txfifo.readptr;//读指针>写指针时发送数据长度 
	if(USB_Tx_length>VIRTUAL_COM_PORT_DATA_SIZE)	//超过64字节?
	{
		USB_Tx_length=VIRTUAL_COM_PORT_DATA_SIZE;//这次发送数据量
	}
	USB_Tx_ptr=uu_txfifo.readptr;		//发送起始地址 
	uu_txfifo.readptr+=USB_Tx_length;		//读指针偏移 
	if(uu_txfifo.readptr>=USB_USART_TXFIFO_SIZE)	//读指针归零
	{
		uu_txfifo.readptr=0;
	} 
	UserToPMABufferCopy(&uu_txfifo.buffer[USB_Tx_ptr], ENDP1_TXADDR, USB_Tx_length);//拷贝数据
	SetEPTxCount(ENDP1, USB_Tx_length); //设置端点1发送数据长度
	SetEPTxValid(ENDP1); //设置端点1发送有效
}

b. 修改SOF_Callback函数为:

//帧首信号回调函数,对全速设备每1ms有一个帧首信号
void SOF_Callback(void)
{
	static uint32_t FrameCount = 0;
	if(bDeviceState == CONFIGURED)
	{
		if (FrameCount++ == VCOMPORT_IN_FRAME_INTERVAL)
		{ 
			FrameCount = 0;//重设帧计数器
			EP1_IN_Callback();//经过EP1_IN_Callback函数实现TX数据发送给USB 
		}
	}  
}
  1. 修改usb_prop.c文件。本例程没有用到USART_Config_Default函数,因此:注释掉Virtual_Com_Port_init函数里面对该函数的调用。

  2. ,修改usb_pwr.c文件。
    修改Suspend函数为:

void Suspend(void)
{
	uint32_t i =0;uint16_t wCNTR;__IO uint32_t savePWR_CR=0; 
	wCNTR = _GetCNTR(); //Store CNTR value 
	for (i=0;i<8;i++) EP[i] = _GetENDPOINT(i); 
	wCNTR|=CNTR_RESETM;//unmask RESET flag
	_SetCNTR(wCNTR); 
	wCNTR|=CNTR_FRES;//apply FRES
	_SetCNTR(wCNTR); 
	wCNTR&=~CNTR_FRES;//clear FRES
	_SetCNTR(wCNTR); 
	while((_GetISTR()&ISTR_RESET) == 0);	//poll for RESET flag in ISTR
	_SetISTR((uint16_t)CLR_RESET);		//clear RESET flag in ISTR
	for (i=0;i<8;i++)_SetENDPOINT(i, EP[i]);		//restore Enpoints 
	wCNTR |= CNTR_FSUSP;
	_SetCNTR(wCNTR);  
	wCNTR = _GetCNTR();//force low-power mode in the macrocell
	wCNTR |= CNTR_LPMODE;
	_SetCNTR(wCNTR); 
	Enter_LowPowerMode();
}
  1. 修改main函数,其实就是把USB虚拟串口例程的主函数拷贝下。

测试

注意:USB虚拟串口实验,在测试的时候,须要安装ST提供的一个电脑端虚拟串口驱动软件VCP_V1.4.0_Setup.exe,不然电脑将没法识别。 若是测试时,不能自动安装成功,请手动选择驱动路径(见:C:\Program Files (x86)\STMicroelectronics Software\Virtual comport driver\WIN7/8),让电脑完成安装便可。