前面的文章我们介绍了如何为CH582芯片移植CherryUSB Device,前些天我考虑将Host也移植一下,但是那会CherryUSB还没有CH582主机的驱动,所以我们也需要自己编写主机驱动,在对比了CH58x系列和CH32系列的(USBFS OTGFS)主机寄存器后发现他们几乎没什么区别,就只有几个bit有差异,所以基本可以一同编写使用,下面我们列出wch主机ip的一些差异。
CH58x,CH57x是一个主机IP(USBFS),CH32V,CH32F是一个主机IP(USBFS和OTG_FS)。这里我们就对比CH58x和CH32V的主机寄存器差异。
1、 USBFS_UH_PRE_PID_EN,USBFS_UH_SOF_EN
2、 USBFS_UH_R_AUTO_TOG,USBFS_UH_R_TOG,USBFS_UH_R_RES
2、 USBFS_UH_T_AUTO_TOG,USBFS_UH_T_TOG,USBFS_UH_T_RES
好了,就上面这些简单的差异,不过我还是挺好奇的,为啥就偏偏搞几个寄存器是不一样的,对于这些差异,我的处理就是根据用户选择的芯片,把这个宏取消掉的,然后再定义一下即可,如下图,详细代码见文章末尾的仓库地址。
CherryUSB的主机协议栈是要依赖操作系统的,这里我选择沁恒已经适配好的rt-thread。下面我们就开始正式编写wch host驱动代码。代码结构参照CherryUSB port/musb/usb_hc_musb.c。
本文只介绍全速主机的移植,高速见仓库内的代码,基本是一致的。
1、usb_hc_init
看上图,黄色框是为host管道创建一些信号量,防止在不同管道之间出现访问冲突,蓝色框的部分来自于沁恒的下面这段代码:
这里我们可以看到官方没开中断,用的是查询的方式,我们将其改成中断的方式,所以在最后使能了传输完成中断和插拔中断,还有就是官方静态分配了一段内存,USBFS_RX_Buf,USBFS_TX_Buf,并将地址给到了dma地址寄存器,这样就只能每次都要将待发送的数据copy到TX数组,或者从RX数组中读取接收的数据,其实是损失了dma的性能的。其实我们可以设计成将用户的buff地址直接给到dma地址寄存器,所以在初始化函数中,我们并没有操作dma地址寄存器。
2、usbh_reset_port
这一部分参照官方给的如下代码:
在复位完成以后我们就可以使能端口了,并且开启自动发送sof包。
3、usbh_ep0_pipe_reconfigure
这一部分代码会在主机第一次获取完设备描述符后调用(根据设备描述符重新配置端点0的最大包长),也会在设置完地址以后调用(重新配置设备地址),实现如图中所示,不做过多解释。
4、usbh_pipe_alloc
这一部分跟硬件相关的东西不多,因为沁恒的主机就一组收发端点和一组收发dma,所以这一部分主要是配置一下软件虚拟的管道,存储一些管道的相关变量,供协议栈来分配。
5、usbh_get_port_speed
这一部分参照官方给的如下代码:
6、usbh_submit_urb
蓝色框中的是框架中的代码,可以参照CherryUSB的其他主机驱动,黄色框中是因为沁恒就一个主机管道,防止发生访问冲突导致失败。
上图中这一部分我们后面会说。
7、chusb_host_pipe_transfer
上图中黄色框中代码,更新全局变量current_token,记录当前事务令牌,记录当前管道current_pipe,记录current_time_out(大于0则需要nak重试),更新主管道正在使用flag,为编写中断服务函数做准备。
下面我按照SETUP,OUT,IN事务分了三个区域。
一、SETUP
二、OUT
三、IN
8、chusb_control_pipe_init
需要注意的就是设置完自动翻转DATAPID,就不能手动设置了,需要清除自动翻转那一个bit才能手动写进去,见如下代码:
9、chusb_bulk_pipe_init
10、chusb_intr_pipe_init
可以看到其实跟bulk是一样的。
11、chusb_iso_pipe_init
12、USBH_IRQHandler
上图中黄色部分:
这是官方在移植rtos的时候做的一些东西,主要就是堆栈的切换。我们需要在中断服务函数中添加这俩函数,要不然会HardFault。
蓝色框框选的就是传输完成中断和插拔中断。
1、插拔中断
主要就是设备连接和断开的操作,然后要调用usbh_roothub_thread_wakeup来唤醒主机协议栈中的线程。
2、传输完成中断
根据当前事务令牌来分。
一、先看out的。
设备回复stall,表示发生错误,这个处理比较简单,自己去看源码就好。
1、我们先看一下设备回复主机nak的。
如果是控制端点,setup发生nak那一定是出错了,out发生nak,则需要重新发起传输。
上图已经基本注释清除了。
2、接下来看设备回复ack的。
上图是控制端点且是setup传输完成,根据ep0_state来决定下一步将发起怎样的传输。
上图是控制端点且是out数据阶段传输完成,红字已经注释的差不多了,主要就是再次启动发送与状态阶段。
上图是成功发送out包作为状态阶段的处理,复位ep0_state为setup,放下信号量并调用回调。
上图是非控制端点的out成功处理。
上图示out超时做的处理,如果不是同步端点超时都认为是出错。
到此,out处理完成。
二、接下来看in
上图就是in的处理框架。
1、我们先看nak的
是控制端点,则需要重传,(协议栈中控制端点的pipe_timeout是大于0的)。
上图的注释中,可能有点费解的就是主机获取0长度字节包收到nak的,我也有考虑这个是否可以不要,因为在控制传输中的最后一包是最大包长的时候,我们是需要设备发送0长度字节包告诉主机已经发送完成的。但是对于普通端点来说,都是主机轮询访问,大部分设备的app都不会在发送最大包长的包后面跟上一个0长度字节包,所以我写了在这种情况的nak进回调,没什么大问题,但是可能会损失一些性能。
2、正确收到数据
上图是控制写传输完成,最后以一个in作为状态阶段结束。
上图是控制读的数据阶段的处理,红字已经基本注释清楚。
上图是普通端点的in处理,收到短包或者0长度字节包则是传输完成。
到此全速主机的移植就全部结束了。
CherryUSB仓库地址:GitHub - sakumisu/CherryUSB: Tiny and portable USB Stack (device & host) for embedded system with USB IP
WCH MCU USB Demo仓库地址:GitHub - CherryUSB/cherryusb_wch: CherryUSB Demo for WCH