USB虚拟串口实验_STM32F1开发指南——USB学习笔记

时间:2024-04-02 16:37:48

前言

    STM32F103系列芯片都自带USB接口,不过STM32F103的USB都只能用来做设备,而不能用作主机。

目录:

 

53.1 USB简介

 

 

    USBF103自带的USB符合USB2.0规范。
    在USB主机上,D-和d +都接了15K下拉电阻到地,所以没有设备接入时,d +和D-都是低电平。
    在USB设备中,如果是高速设备,d +上接一个1.5K上拉电阻到VCC;如果是低速设备,D-上接入一个1.5K
。上拉电阻到VCC这样主机就能判断是否有设备接入,接入的设备是高速还是低速设备。

 

 

1)STM32的USB从控制器

    ①PC主机和MCU之间的数据传输是通过共享一专用的数据缓冲区来完成的,该数据缓冲区被USB外设直接

访问。这块专用数据缓冲区的大小由所使用的端点数目和每个端点最大的数据分组大小所决定,每个端点最大

可使用512字节缓冲区(专用512字节,和CAN公用),最多可用于16个单向或8个双向端点。

    ②USB模块和PC主机通信,根据USB规范实现令牌分组的检测,数据发送/接收处理,和握手分组的处理。整

个传输的过程由硬件完成,包括CRC的生成和校验。

    ③每个端点都有一个缓冲区描述块,描述该端点使用的缓冲区地址,大小和需要传输的字节数。当USB模块识

别出一个有效的功能/端点的令牌包分组时,(如果需要传输数据并且端点已配置)随之发生相关的数据传输。

        USB模块通过一个内部的16位寄存器实现端口与专用缓冲区之间的数据交换。在所有的数据传输完成后,如

果需要,根据传输的方向,发送或接收适当的握手分组。在数据传输结束时,USB模块将触发与端点相关的中断,

通过读状态寄存器和/或者利用不同的中断来处理。

    ④USB的中断映射单元:将可能产生中断的USB时间映射到三个不同的NVIC请求线上:

        一个,USB低优先级中断(通道20):可由所有的USB事件触发(正确传输,USB复位等)固件在处理中断前应先确定中断源。

        B,USB高优先级中断(通道19):仅能由同步和双缓冲批量传输的正确传输事件触发,目的是保证最大的传输速率。

        C,USB唤醒中断(通道42):由USB挂起模式的唤醒事件唤醒。

2)STM32的USB设备框图

USB虚拟串口实验_STM32F1开发指南——USB学习笔记

53.2硬件设计

    本章实验​​功能简介:

    利用STM32自带的USB功能,连接电脑USB虚拟出一个USB串口,实现电脑和开发板的数据通信。在找到虚拟串口

后,即可打开串口调试助手,通过USB虚拟串口和上位机对话,STM32在收到上位机发过来的字符串(以回车换行结

束)后,原样返回给上位机。下载后,DS0闪烁,提示程序在运行,每个一定时间,通过USB虚拟串口发送一段信息给

电脑。

    所要用到的硬件资源如下:

    1)指示灯DS0,DS1;

    2)串口;

    3)TFTLCD模块;

    4)USB SLAVE接口。

 

    的MiniUSB接口与STM32连接电路图:

USB虚拟串口实验_STM32F1开发指南——USB学习笔记

    因为PA11和PA12既是USB接口又是CAN接口,所以用连接座P9转接,需要用USB时,用跳线帽将PA11,PA12与

D-,d +连接即可。

53.3软件设计

1)stm32的USB固件库

USB虚拟串口实验_STM32F1开发指南——USB学习笔记

    移植时,重点要修改USB_CONFIG文件夹下的代码,USB_CORE下的代码一般不用修改。

    ①USB_CORE下代码介绍:

序号 文件名 作用 备注
1 usb_regs.c 操作USB控制寄存器; 里面有对各种USB寄存器的底层操作函数;
2 usb_init.c 初始化USB控制器; 只有一个函数USB_Init,调用其它函数,以使代码规范;
3 usb_int.c 中断处理函数; 只有两个函数:
CTR_LP:负责USB低优先级中断的处理;
CTR_HP:负责USB高优先级中断的处理;
4 usb_mem.c 处理PMA数据; PMA全称为数据包存储区,是stm32内部用于USB / CAN的专用数据缓冲区。
只有两个函数:
PMAToUserBufferCopy:将USB数据传送到
主机; UserToPMABufferCopy:将主机数据传送到USB;
usb_core.c 处理USB2.0协议  
6 usb_sil.c 为USB端点提供简化的读写访问函数。  

    以上的函数具有很强的独立性,直接调用内部的函数即可。

    ②USB_CONFIG下的代码:

序号 文件名 作用
1 hw_config.c 配置硬件,比如初始化USB时钟,USB中断,低功耗模式处理等;
2 usb_desc.c 处理Virtual Com描述符;
3 usb_endp.c 处理正确传输中断回调函数,用于非控制传输;
4 usb_pwr.c 管理USB控制器的电源;
usb_istr.c 处理USB中断;
6 usb_prop.c 处理所有Virtual Com的相关事件,包括Virtual Com的初始化,复位等操作。

    ③其他

序号 文件名 作用 备注
1 stm32_it.c 处理USB相关中断。 包括两个中断服务函数:
USB_LP_CAN1_RX0_IRQHandler:调用USB_Istr函数,处理USB发生的各种中断;
USBWakeUp_IRQHandler:清除中断标志。
注:为了方便,一般把USB中断相关代码放到hw_config.c里面。

2)USB实现代码

int main(void)
{
u16 t;
u16 len;
u16 times=0;
u8 usbstatus=0;
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
uart_init(115200); //串口初始化为115200

USB_Port_Set(0);     //USB先断开
delay_ms(700);
USB_Port_Set(1);         //USB再次连接

Set_USBClock(); //配置USB时钟,即从72M主频得到48M的USB时钟(1.5分频)
USB_Interrupts_Config(); //设置USB唤醒中断和USB低优先级数据处理中断
USB_Init();   //初始化USB,主要是调用Virtual_Com_Port_init函数,开启USB部分的电源等

while(1)
{
if(usbstatus!=bDeviceState)//USB连接状态发生了改变.
{
usbstatus=bDeviceState;//记录新的状态
if(usbstatus==CONFIGURED)
{
LED1=0;//DS1亮 提示USB连接成功
}else
{
LED1=1;//DS1灭 提示USB断开
}
}

if(USB_USART_RX_STA&0x8000)
{
len=USB_USART_RX_STA&0x3FFF; //得到此次接收到的数据长度

usb_printf("\r\n您发送的消息为:%d\r\n\r\n",len);
for(t=0;t<len;t++)
{
USB_USART_SendData(USB_USART_RX_BUF[t]); //以字节方式,发送给USB
}
usb_printf("\r\n\r\n"); //插入换行

USB_USART_RX_STA=0;
}
else
{
times++;
if(times%5000==0)
{
usb_printf("\r\n战舰STM32开发板USB虚拟串口实验\r\n");
usb_printf("正点原子@ALIENTEK\r\n\r\n");
}

if(times%200==0)
usb_printf( “请输入数据,以回车键结束\ r \ n”);
if(times%30==0)
LED0 = LED0; //闪烁的LED,提示系统正在运行。
delay_ms(10);
}
}
}

    主要代码作用:

    USB的配置通过三个函数完成:

       Set_USBClock(); //配置USB时钟,即从72M主频得到48M的USB时钟(1.5分频)

       USB_Interrupts_Config(); //设置USB唤醒中断和USB低优先级数据处理中断  

       USB_Init(); //初始化USB,主要是调用Virtual_Com_Port_init函数,开启USB部分的电源等

     为什么不设置PA11和PA12管脚?

        因为,一旦开启USB电源(USB_CNTR的PDWN位清零),PA11和PA12将不再做其他功能使用,仅供USB使用,所以在

开启了USB电源之后,无论怎么配置这两个管脚,都是无效的。要再次获取这两个管脚的配置权,需要关闭USB电源,即置

位USB_CNTR的PDWN位,我们通过USB_Port_Set函数来禁止/使能USB连接,在复位时,先禁止,再使能,这样每次我们

按复位键,电脑都可以识别到USB鼠标,而不需要我们每次都拔USB线.USB_Port_Set函数在hw_config.c中实现,

代码如下:

//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;
}
}

    USB虚拟串口数据发送:

    该函数在hw_config.c中


//发送一个字节数据到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;
}
}

    该函数实现发送1字节数据到虚拟串口中。

    这里用到了一个uu_txfifo结构体,该结构体是一个USB虚拟串口发送数据FIFO结构体,定义如下:

#define USB_USART_TXFIFO_SIZE 1024 //USB虚拟串口发送FIFO大小

//定义一个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

    该结构体用于处理USB串口要发送的数据,所以通过USB串口发送的数据,都将先存到结构体的缓冲数组(FIFO缓存区)里

面,USB_USART_TXFIFO_SIZE定义了该数组的大小,通过writeptr和readptr来控制FIFO的写入和读出。该结构体缓冲数

据的写入,是通过USB_USART_SendData()函数实现的。

    缓冲数据的读出(然后发送到USB)则是通过端点1的回调函数EP1_IN_Callback()函数实现的。

    该函数在usb_endp.c中代码如下:

//Function Name : 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;    //得到要发送的数据长度
}
否//超过数组了读指针>写指针
{
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);
SetEPTxValid(ENDP1);
}

    函数这个由USB中断处理相关函数调用,将通过USB发送给电脑的数据拷贝到端点1的发送区,然后通过USB发送给电脑,

从而实现串口数据的发送。

    因为每次传输数据长度不超过VIRTUAL_COM_PORT_DATA_SIZE,所以USB的发送数据长度:USB_Tx_length的最大值

只能是VIRTUAL_COM_PORT_DATA_SIZE。

    以上就是USB虚拟串口数据发送过程。

 

    USB虚拟串口数据接收:

    USB虚拟串口的接收,通过端点3来实现,端点3的回调函数为EP3_OUT_Callback(),该函数也在usb_endp.c中,代码如下:

//Function Name : EP3_OUT_Callback
void EP3_OUT_Callback(void)
{
u16 USB_Rx_Cnt;
USB_Rx_Cnt = USB_SIL_Read(EP3_OUT, USB_Rx_Buffer); //得到USB接收到的数据及其长度
USB_To_USART_Send_Data(USB_Rx_Buffer, USB_Rx_Cnt); //处理数据(其实就是保存数据)
SetEPRxValid(ENDP3);     //使能端点3的数据接收
}

    该函数也是由USB中断处理相关函数调用,该函数通过调用USB_To_USART_Send_Data函数,实现USB接收数据的保存,

该函数在hw_config.c中实现,代码如下:

u8 USB_USART_RX_BUF[USB_USART_REC_LEN];    //接收缓冲,最大USART_REC_LEN个字节。
u16 USB_USART_RX_STA=0;                    //接收状态,bit15, 接收完成标志,bit14, 接收到 0x0d,bit13~0,接收到的有效字节数目
//处理从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;
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; //接收数据错误,重新开始接收
}
}
}
}
}

    用类似串口1接收数据的方法,来处理USB虚拟串口接收到的数据。

53.4下载验证

    本例程需要在电脑上先安装ST提供的USB虚拟串口驱动软件,安装好后,重新插入USB数据线,可以看到如下信息。

USB虚拟串口实验_STM32F1开发指南——USB学习笔记

    打开串口调试助手,选择串口号,打开串口,就可以进行测试了。

USB虚拟串口实验_STM32F1开发指南——USB学习笔记

    可以看到,串口调试助手,收到了开发板的数据,同时,按发送按钮(串口调试助手必须勾选“发送新行”),也可以收到电脑发送

给开发板的数据(原样返回),说明实验成功,实验现象同串口帐相同。