STM32 USBH CDC开发及应用

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中断定时使𠹌通道,然后进行数据接收发送,usbh会发送之前的in令牌包。修改如下函数:

		  static void HCD_HC_IN_IRQHandler   (HCD_HandleTypeDef *hhcd, uint8_t chnum)
		  {
				..........
				else if ((USBx_HC(chnum)->HCINT) &  USB_OTG_HCINT_CHH)
				{
					.......
					else if (hhcd->hc[chnum].state == HC_NAK)
					{
					  hhcd->hc[chnum].urb_state  = URB_NOTREADY;
					}
				}
		  }
		uint32_t count[12] = {0}; 
		void HAL_HCD_IRQHandler(HCD_HandleTypeDef *hhcd)
		{
		    .......
			/* Handle Host SOF Interrupts */
			if(__HAL_HCD_GET_FLAG(hhcd, USB_OTG_GINTSTS_SOF))
			{
			  HAL_HCD_SOF_Callback(hhcd);
			  __HAL_HCD_CLEAR_FLAG(hhcd, USB_OTG_GINTSTS_SOF);
			  for (i = 0; i < hhcd->Init.Host_channels ; i++)
			  {
				if (hhcd->hc[i].state == HC_NAK)
				{
				  if(count[i]++ >= 20)
				  {
					count[i] = 0;
					uint32_t tmpreg = 0;
					tmpreg = USBx_HC(i)->HCCHAR;
					tmpreg &= ~USB_OTG_HCCHAR_CHDIS;
					tmpreg |= USB_OTG_HCCHAR_CHENA;
					USBx_HC(i)->HCCHAR = tmpreg;
				  }
				}
				else if((hhcd->hc[i].state == HC_XACTERR) ||
						(hhcd->hc[i].state == HC_DATATGLERR))
				{
					count[i] = 0;
					uint32_t tmpreg = 0;
					tmpreg = USBx_HC(i)->HCCHAR;
					tmpreg &= ~USB_OTG_HCCHAR_CHDIS;
					tmpreg |= USB_OTG_HCCHAR_CHENA;
					USBx_HC(i)->HCCHAR = tmpreg;
				}
				else
				{
					count[i] = 0;
				}
			  }
			}
			........
		}

2:当设备刚连接usb主机后,USBH_Process()函数中处理状态机状态变为不是空闲状态后,快速拔掉usb线,且usb主机没有检测到usb断开,从而不进行复位状态,当再次插上usb,usb主机不能进行正常的设备识别。
解决思路:当再次检测到设备连接上之后,则关闭端口使能状态,让状态机进入断开处理。然后重新进入设备识别流程。
解决方法如下:

        static void HCD_Port_IRQHandler  (HCD_HandleTypeDef *hhcd)
		{
			......
		  /* Check whether Port Connect detected */
		  if((hprt0 & USB_OTG_HPRT_PCDET) == USB_OTG_HPRT_PCDET)
		  {
			if((hprt0 & USB_OTG_HPRT_PCSTS) == USB_OTG_HPRT_PCSTS)
			{
			  USB_MASK_INTERRUPT(hhcd->Instance, USB_OTG_GINTSTS_DISCINT);
			  ((USBH_HandleTypeDef *)(hhcd->pData))->device.PortEnabled = 0;
			  HAL_HCD_Connect_Callback(hhcd);
			}
			hprt0_dup  |= USB_OTG_HPRT_PCDET;
		  }
		}
  • 11
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值