赞
踩
USB,是英文 Universal Serial BUS (通用串行总线)的缩写,其是一个外部总线标准,用于规范USB主机与外部设备的连接和通讯。由于项目需要,需要开发基于STM32 USB主机(HOST)的CDC的开发,用于编队表演系统中底座跟无人机间的数据交互,同时通过usb接口给无人机充电。
在做usb主机开发之前,我们需要先了解一个概念,USB 设备是被动触发,USB主机掌握主动权,发送什么数据,什么时候发送,是给设备发送数据还是从设备请求数据,都是由USB主机完成的,USB设备只是配合主机完成设备的枚举、数据方向和大小。根据数据特性再决定该不该回复该如何回复、该不该接收该如何接收这些动作。
开发过程:
1.从ST官方提供的USB库将USB主机(HOST)库及CDC类代码加入到自己的工程中。
2.根据USB硬件修改usbh_conf中的初始化代码。
3.根据自己应用编写应用代码,由于开发是基于rtthread系统开发的,可以将usb当做一个serial设备,利用rtthread系统设备操作,实现应用与底层软件分离。
思路:
a:实现serial设备的操作方法,利用ringbuffer功能,将usb设备发送数据先写入到ringbuffer中,然后usb处理线程从ringbuffer中获取数据发送数据,实现与应用分离。usb数据接收到的数据写入serial封装的ringbuffer中。
usb数据发送操作封装:
static uint32_t usbh_vcp_send(uint8_t * Buf, uint32_t Len)
{
uint32_t len = 0;
if(usbh_vcp_ringbuffer.buffer_ptr != RT_NULL)
{
len = rt_ringbuffer_put_force(&usbh_vcp_ringbuffer, Buf, Len);
}
return len;
}
uint32_t copy_data_to_usbh(uint8_t *usbh_buf, uint32_t size)
{
uint32_t len = 0;
if(usbh_vcp_ringbuffer.buffer_ptr != RT_NULL)
{
len = rt_ringbuffer_get(&usbh_vcp_ringbuffer, usbh_buf, size);
}
return len;
}
static int usbh_vcp_putc(struct rt_serial_device * serial, char c)
{
return usbh_vcp_send((uint8_t *) &c, 1);
}
usb数据接收操作封装:
在这里插入代码片
uint32_t usbh_vcp_recv(uint8_t * Buf, uint32_t Len)
{
uint32_t size = 0;
if(usbh_vcom_serial.serial_rx != RT_NULL)
{
size = rt_ringbuffer_put_force(& ((struct rt_serial_rx_virtual *) (usbh_vcom_serial.serial_rx))->ringbuffer, Buf,Len);
rt_hw_serial_isr(&usbh_vcom_serial, RT_SERIAL_EVENT_RX_VIRTUALDONE);
}
return size;
}
static rt_err_t usbh_vcp_configure(struct rt_serial_device * serial, struct serial_configure * cfg)
{
return RT_EOK;
}
static rt_err_t usbh_vcp_control(struct rt_serial_device * serial, int cmd, void * arg)
{
return RT_EOK;
} static int usbh_vcp_getc(struct rt_serial_device * serial)
{
int result = -1;
rt_uint8_t ch;
if(serial->serial_rx != RT_NULL)
{
if(rt_ringbuffer_getchar(& ((struct rt_serial_rx_virtual *) (serial->serial_rx))->ringbuffer, &ch) != 0)
{
result = ch;
}
}
return result;
}
rt_inline rt_size_t usbh_vcp_transmit(struct rt_serial_device *serial, rt_uint8_t *buf, rt_size_t size, int direction)
{
rt_size_t ret = 0;
RT_ASSERT(serial != RT_NULL);
if(direction == RT_SERIAL_VIRTUAL_TX)
{
return usbh_vcp_send(buf, size);
}
else if(direction == RT_SERIAL_VIRTUAL_RX)
{
if(serial->serial_rx != RT_NULL)
{
ret = rt_ringbuffer_get(&((struct rt_serial_rx_virtual *) (serial->serial_rx))->ringbuffer, (rt_uint8_t *) buf, size);
}
}
return ret;
}
static const struct rt_uart_ops usbh_vcp_ops =
{
usbh_vcp_configure,
usbh_vcp_control,
usbh_vcp_putc,
usbh_vcp_getc,
usbh_vcp_transmit,
};
b:为usb发送申请ringbuffer,注册usb serial设备
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT2;
if(usbh_vcp_send_buffer == RT_NULL)
{
usbh_vcp_send_buffer = (uint8_t *)rt_malloc(TX_RINGBUFFER_SIZE);
if(usbh_vcp_send_buffer != RT_NULL)
{
rt_ringbuffer_init(&usbh_vcp_ringbuffer, usbh_vcp_send_buffer, TX_RINGBUFFER_SIZE);
}
else
{
rt_kprintf("[usbh] malloc vcp tx_buffer failed\r\n");
}
}
/* init usbh_cdc_serial */
usbh_vcom_serial.ops = &usbh_vcp_ops;
usbh_vcom_serial.config = config;
/* register virtual serial device */
if(rt_hw_serial_register(&usbh_vcom_serial, "usbh_vcp", RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_VIRTUAL_RX | RT_DEVICE_FLAG_VIRTUAL_TX, 0) == RT_EOK)
{
rt_kprintf("[usbh] usbh_vcom port registered.\n");
}
c:初始化usbh,添加接口类,启动usb
/* Init Host Library */
USBH_Init(&usbh_cdc, USBH_UserProcess, 0);
/* Add Supported Class */
USBH_RegisterClass(&usbh_cdc, USBH_CDC_CLASS);
/* Start Host Process */
USBH_Start(&usbh_cdc);
d:修改cdc类发送接收
extern uint32_t copy_data_to_usbh(uint8_t *usbh_buf, uint32_t size);
static void CDC_ProcessTransmission(USBH_HandleTypeDef *phost)
{
CDC_HandleTypeDef *CDC_Handle = (CDC_HandleTypeDef*) phost->pActiveClass->pData;
USBH_URBStateTypeDef URB_Status = USBH_URB_IDLE;
static uint32_t send_len = 0;
switch (CDC_Handle->data_tx_state)
{
case CDC_SEND_DATA:
//从缓冲区拿数据(最大为端点数据输出大小),如果有数据则发送数据
if((URB_Status == USBH_URB_IDLE) || (URB_Status == USBH_URB_DONE))
{
send_len = copy_data_to_usbh(CDC_Handle->pTxData, CDC_Handle->DataItf.OutEpSize);
}
if(send_len > 0)
{
USBH_BulkSendData(phost,CDC_Handle->pTxData, send_len, CDC_Handle->DataItf.OutPipe, 1U);
CDC_Handle->data_tx_state = CDC_SEND_DATA_WAIT;
}
break;
case CDC_SEND_DATA_WAIT:
URB_Status = USBH_LL_GetURBState(phost, CDC_Handle->DataItf.OutPipe);
/* Check the status done for transmission */
if (URB_Status == USBH_URB_DONE)
{
CDC_Handle->data_tx_state = CDC_SEND_DATA;
}
else
{
if (URB_Status == USBH_URB_NOTREADY)
{
CDC_Handle->data_tx_state = CDC_SEND_DATA;
}
}
break;
default:
break;
}
}
extern uint32_t usbh_vcp_send(uint8_t * Buf, uint32_t Len);
extern uint32_t usbh_vcp_recv(uint8_t * Buf, uint32_t Len);
static void CDC_ProcessReception(USBH_HandleTypeDef *phost)
{
CDC_HandleTypeDef *CDC_Handle = (CDC_HandleTypeDef*) phost->pActiveClass->pData;
USBH_URBStateTypeDef URB_Status = USBH_URB_IDLE;
uint32_t length;
switch(CDC_Handle->data_rx_state)
{
case CDC_RECEIVE_DATA:
USBH_BulkReceiveData (phost,
CDC_Handle->pRxData,
CDC_Handle->DataItf.InEpSize,
CDC_Handle->DataItf.InPipe);
CDC_Handle->data_rx_state = CDC_RECEIVE_DATA_WAIT;
break;
case CDC_RECEIVE_DATA_WAIT:
URB_Status = USBH_LL_GetURBState(phost, CDC_Handle->DataItf.InPipe);
/*Check the status done for reception*/
if(URB_Status == USBH_URB_DONE)
{
length = USBH_LL_GetLastXferSize(phost, CDC_Handle->DataItf.InPipe);
if(length > 0)
{
usbh_vcp_recv(CDC_Handle->pRxData, length);
}
CDC_Handle->data_rx_state = CDC_RECEIVE_DATA;
}
break;
default:
break;
}
}
e:usbh识别device及数据处理
while (1)
{
/* USB Host Background task */
USBH_Process(&usbh_cdc);
rt_thread_delay(10);
}
e:利用rtthread系统系统的设备操作操作函数进行具体应用通讯。操作函数有:
rt_thread_find(.....)
rt_thread_read(.....)
rt_thread_write(.....)
进行stm32 usbh移植修改后,我们来了解一下usb主机识别usb设备过程
1:usbh等待设备连接中断,当设备接入中断产生,延迟200ms发送复位信号到usb设备。
2:等待usbh端口使能中断发生,当端口使能后,延迟获取从设备通讯速度,为端点0申请pipe,打开端点0的发送接收pipe,进入枚举状态。
3:枚举过程:
a:usbh通过端点0获取设备描述符前8字节数据,拿到端点0的最大传输包大小,修改端点0的发送接收pipe信息。
b:获取usb设备的全部描述信息,然后解析描述符内容。
c:设置从机的通讯地址,修改端点0的地址pipe信息。
d:获取配置描述前9字节的数据,拿到配置描述长度及配置描述符中支持的接口数。
f:获取全部配置描述符,解析数据得到接口描述符及端点描述符信息。
g:若有厂商,产品,序列号信息,则获取相对应的信息。
4:进入主机配置状态,发送设备配置信息到从设备。
5:设置usb设备属性。
6:校验usb接口类型,如果是注册的usb类,则初始化接口,打开通讯接口(通讯端点)及数据接口(数据输入输出端点)
7:发送类请求,处理接口类数据。
开发中遇到的疑问。
1.USB设备如何向USB主机发送数据?
理解:USB设备在主动发送给USB主机时,需要USB主机先发送一个IN令牌包,然后USB设备自动把之前保存在IN端点缓存区的数据发送给USB主机,然后USB主机再返回ACK;
问题:例如在USB转串口这类USB中,USB设备串口中断接收到数据,然后主动发送给USB主机,通讯过程是怎样的?IN令牌包是如何获取的?(主机怎么知道设备端写了数据,然后就下发IN包?)
答:host并不知道device上是不是有了数据,host发in包,是因为host上有程序在读设备。也就是说主机是间隔发送IN包(相当于轮询),主机根据设备的返回信号(NCK、STALL、DATAx)来确定接下来的动作,如果是DATA,就表示有数据,那么host的协议栈和OS会将数据传递给对应的app,表现出来就是程序调用的read读到了数据。如果是NAK,host会继续发in,app继续阻塞。如果是STALL,host会对app返回一个错误。
2.设备向EP缓冲写数据时,是要判断之前的数据是否已经被取走了的。只有主机下发了IN令牌,设备才能提交EP缓冲数据给HOST。
问题1:在低速与全速设备中,SOF每1ms发送一次,看了一些资料上面说的是用于同步;
疑惑点:SOF用于同步什么东西?SOF起到的作用是什么?SOF跟发送IN、OUT等指令有啥联系吗?
答:SOF目的是device和host之间的时间同步,如果你的USB设备里面没有用到同步传输和中断传输,可以忽略SOF。HOST广播发送SOF,挂起状态没有SOF。设备挂起由HOST控制,HOST停止给DEVICE发送SOF,3ms后设备就认为是挂起请求,进入挂起状态,设备不可自行进入挂起状态。
问题2:主机需要读数据的时候,会向设备发送IN指令,然后开始传输数据,
疑惑点:IN指令是什么时候发的?还是主机固定周期发?周期是SOF的1ms吗?
例如:设备有数据要发送,但是主机没有发送IN指令给设备。
答:每1ms称为一个Frame,其中INT,ISO,CTRL,BULK四种传输类型会有一个先后优先次序。1ms内可以发出很多个BULK端点的IN令牌。如果DEVICE要发送数据,必须等到IN令牌来了才能发出,否则不可以发出。可以理解为是HOST轮询收发的
STM32_usb_cdc开发问题
问题:CDC类开发时,无法从设备端向主机端发送64整数倍数据,最本质的原因就是,当发送数据长度恰好是DataIn端点的最大包长整数倍时,最后一包数据必须是零长度的数据包(ZLP)。这是由于在USB标准中,接收端并不是通过已经接收的数据长度来判断是否接收完成,且发送端也并没有给出将要发送多长的数据,因此,接收端在接收数据前,并不知道将要接收的数据是多少,那么,问题就来了,接收端又是如何判断当前的数据已经全部接收了呢?
分析:
1:若接收到的数据包长不足最大包长时,则认为当前传输完成
2:如接收到的数据包长为零时,则认为当前传输完成。
3:正式由于上述两种判断,当传输的数据刚好是端点的最大包长时,当发送完最后一包(比如64个字节)时,接收端无法判断是否传输结束,进而继续等待下一包数据。这个就是问题本质所在。
解决:
1:总的原则就是,在发送完最后一包数据后,判断发送的包长是否为端点最大包长的整数倍,如是,则补发一个零长度的数据包(ZLP)。
STM32_USB数据发送流程分析
在对USB CDC协议栈进行修改之前,我们先来梳理下USB发送的流程。
发送USB数据大概过程如下:
1: 填写DIEPTSIZ寄存器的发送包数(pakage count)和传输大小(transfer size)。
2: 使能发送端点的发送空中断(DIEPEMPMSK,利用发送空中断TXFE来将发送数据填充到DFIFO)。
3: 使能中断。
4:后续就是中断的事了。
后续将会有3次中断:
1> USB_OTG_DIEPINT_TXFE中断:在此中断处理中,程序将发送缓冲的数据分包填充到DFIFO(不能超过最大包长,只有最后一包数据才有可能小于最大包长)。
2> USB_OTG_DIEPINT_TXFE中断: 还是TXFE中断,上次TXFE填充的发送数据全部发送完了后,最终还是会继续触发TXFE中断,也就是这次中断,在这次FXFE中断中禁止FXFE。也就是说,后续不会再有TXFE中断,除非再次使能。
3> USB_OTG_DIEPINT_XFRC中断: 传输完成中断,表示到这次中断为止,传输完成。在这个中断中将回调HAL_PCD_DataInStageCallback()函数,就相当于发送中断一样。
这就是USB device cdc数据发送的流程,这里需要注意地是,对于端点0和非端点0来说,在具体流程实现上还是稍微有所差异的。究其原因,主要是端点0和非端点0的DIEPTSIZ寄存器的包大小和传输大小位宽是不一样的。
项目中遇到的问题:
1:当usb主机向从设备拿数据时,从设备没有数据,返回NAK给usb主机,usb主机会重新使能通道,再次发送IN牌包到usb设备,usb设备没有数据,仍然返回NAK给主机,一致循环下去,导致主机从机CPU占用率过高,导致很多任务无法运行。
调试现象:usb主机会一直进入到接收fifo非空中断,然而从fifo中又拿不到数据,信息是通道中断,usbh然后进入输入端点中断,处理NAK及HHC中断,不更改程序会发现进入fifo非空跟输入端点中断比较频繁。cpu一直处理中断,导致没时间处理其他任务。
解决思路:当usb主机收到NAK数据后,禁止通道接收发送,延迟后再开启通道接收发送使能,使得usb主机再次发送IN令牌包。
解决方法:
当收到通道停止中断后,并且状态是NAK状态,则不使能通道接收发送,通过sof中断定时使
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。