CH582是一款优秀的芯片,它有着极高的性价比,拥有两个USB(HD)和BLE5,本偏文章将介绍如何将CH582的USB移植到一个优秀的开源项目:CherryUSB。
第一步:需要了解CH582的USB外设,我们直接上寄存器文档,了解CH582的USB寄存器即可。其实CH571,CH572,CH573,CH581,CH582,CH583用的基本是一个USBIP,CH579用的是另一个USBIP,但是它们都很相似。CH571,CH572,CH573与CH581,CH582,CH583的USBIP差别在于端点数目,前三者5个端点,后三者是8个端点,其余都是一样的。CH579的USBIP在中断处理上有不一样的地方,这个我们后面再说。
我们先看全局寄存器。
1、USB控制寄存器(R8_USB_CTRL)
这个寄存器我们主要关注红色框框出来的位,文档里面将这些位的功能都有了比较详细的描述,这里我们就解释一下RB_UC_INT_BUSY 这个bit,使能这个bit以后,如果传输完成中断标志未清0,设备对主机会自动回应NAK,表示繁忙。如果不使能这个bit,例如我们中断服务函数中将ep0的发送端点设置为ACK,表示主机再向控制端点发送in包的时候,数据会被主机拿走,但是这个时候还没退出中断服务函数,传输完成中断标志未清0,其实是在忙的状态,理应该回复主机NAK,使能这个bit,在此种情况就可以自动向主机回复NAK。
2、中断使能寄存器
中断使能寄存器,顾名思义,就是控制开启哪些中断,我们主要关注红框框出来的几个bit,分别问 SUSPEND ,TRANSFER ,BUS_RST ,协议栈设计的是传输完成中断,只有ACK了才进中断,所有NAK中断不需要,SOF(起始帧中断)也不需要(可以开但是用不到)。
3、设备地址寄存器
这个寄存器存放的是设备的地址,在USB总线复位的时候,这个寄存器需要被清0,因为主机在枚举初期会以0地址来和设备通讯,在设备接收到设置地址(set_address)命令的时候,我们不能立马把地址填写到这个寄存器,因为那个时候控制传输还没完成(set_address : [setup] -- [nodata] -- [status phase:in]),控制传输分为三个阶段,建立阶段,数据阶段(可有可无),状态阶段。设置地址这条下发的时候,地址是被放在setup包里面一起下发给设备的,主机还会下发一个in包(状态阶段 以0地址下发)来表示控制传输完成,所以我们需要在状态阶段以后,再将地址写入到设备地址寄存器中,下一次主机与设备通讯的时候就会用新地址了。
4、杂项状态寄存器
这个寄存器没啥好说的,值需要关注bit2,用来判断一下是挂起还是唤醒
5、中断标志寄存器
我们只需要关注红框标记出来的bit,因为前文我们已经说到了,设计的时候只开了三个中断。
6、中断状态寄存器
这个寄存器的每一个bit我们都需要关注,文档中都已经解释的很清楚了。
文档中这段话解释了这个USBIP不太常规的地方,在设计USB中断函数的时候要注意以下描述的内内容,这个需要结合代码,后面再说。
7、接收长度寄存器
这个寄存器就是存放的当前传输端点接收数据的长度,但是当EP0收到setup包的时候,这个寄存器不会是8字节,前文提到过setup令牌不会影响到这个寄存器。
8、设备物理端口控制寄存器
我们需要关注红框框出来的bit,禁用下拉和使能物理端口。
9、端点模式控制寄存器((R8_UEP4_1_MOD为例)和缓冲区模式
上图上半部分是使能端点发送和接收的一些控制,下半部分描述了一些比较奇怪的东西,不知道为什么这个IP要设计成这样,端点4和端点0的DMA缓冲区有关联,也就是配置了端点0的dma缓冲区地址就会关联性的配置端点4的dma缓冲区地址。搞不懂为什么要这样设计。结合其他端点缓冲区描述一起看一下,如下图:
本身是可以直接将用户的数据buffer地址直接给到dma地址寄存器的,这样能比较好的发挥dma的性能,如果端点是单向端点是可以的,因为配置dma地址的时候只有一个:R16_UEPn_DMA寄存器
没有去区分是in还是out,这个IP设计的如果端点是双向端点,那么out端点的dma地址就是R16_UEPn_DMA的值,但是in端点的dma地址就是R16_UEPn_DMA+64。如果开启双向端点就不能直接指定dma地址为用户buffer地址了,加上本身端点就不多,我们就设计成对所有端点默认开启双向,然后静态分配一块内存供协议栈来中转,将用户的buffer复制到这块内存,dma地址寄存器不去修改(初始化的时候固定为静态分配的这块内存的地址,注意没有用到双缓冲模式)。
10、端点发送长度寄存器
端点发送数据的时候,此寄存器需要填写待发送数据的长度。
11、端点控制寄存器
这些bit我们都需要注意,着重注意一下红色框框出来的bit,因为在设计端点发送这个函数的时候,同步端点与其他端点有点差别,其实就是同步端点发送的时候不需要期待主机ACK,如果将其当作普通端点来处理,会出现无法进入发送成功中断的情况,这个IP还有一个奇怪的地方,端点0和端点4不支持同步触发位的自动翻转,但是其他端点支持。所以我们在设计中断服务函数的时候要注意发送成功以后需要对0和4端点进行手动的同步触发位翻转。
看到这里,设备寄存器也就基本没了,(沁恒这个IP用起来还是挺方便的,毕竟寄存器比较简单),ch582有两个usb,考虑到官方没有将寄存器写成结构体表的形式,对于切换usb外设来说不是很方便,这里根据文档将寄存器表写出来,如下:
typedef struct
{__IO uint8_t USB_CTRL; /*!< 0x40008000 */union {__IO uint8_t UDEV_CTRL; /*!< 0x40008001 */__IO uint8_t UHOST_CTRL; /*!< 0x40008001 */};__IO uint8_t USB_INT_EN; /*!< 0x40008002 */__IO uint8_t USB_DEV_AD; /*!< 0x40008003 */__IO uint8_t USB_STATUS0; /*!< 0x40008004 */__IO uint8_t USB_MIS_ST; /*!< 0x40008005 */__IO uint8_t USB_INT_FG; /*!< 0x40008006 */__IO uint8_t USB_INT_ST; /*!< 0x40008007 */__IO uint8_t USB_RX_LEN; /*!< 0x40008008 */__IO uint8_t Reserve1; /*!< 0x40008009 */__IO uint8_t Reserve2; /*!< 0x4000800a */__IO uint8_t Reserve3; /*!< 0x4000800b */__IO uint8_t UEP4_1_MOD; /*!< 0x4000800c */union {__IO uint8_t UEP2_3_MOD; /*!< 0x4000800d */__IO uint8_t UH_EP_MOD; /*!< 0x4000800d */};__IO uint8_t UEP567_MOD; /*!< 0x4000800e */__IO uint8_t Reserve4; /*!< 0x4000800f */__IO uint16_t UEP0_DMA; /*!< 0x40008010 */__IO uint16_t Reserve5; /*!< 0x40008012 */__IO uint16_t UEP1_DMA; /*!< 0x40008014 */__IO uint16_t Reserve6; /*!< 0x40008016 */union {__IO uint16_t UEP2_DMA; /*!< 0x40008018 */__IO uint16_t UH_RX_DMA; /*!< 0x40008018 */};__IO uint16_t Reserve7; /*!< 0x4000801a */union {__IO uint16_t UEP3_DMA; /*!< 0x4000801c */__IO uint16_t UH_TX_DMA; /*!< 0x4000801c */};__IO uint16_t Reserve8; /*!< 0x4000801e */__IO uint8_t UEP0_T_LEN; /*!< 0x40008020 */__IO uint8_t Reserve9; /*!< 0x40008021 */__IO uint8_t UEP0_CTRL; /*!< 0x40008022 */__IO uint8_t Reserve10; /*!< 0x40008023 */__IO uint8_t UEP1_T_LEN; /*!< 0x40008024 */__IO uint8_t Reserve11; /*!< 0x40008025 */union {__IO uint8_t UEP1_CTRL; /*!< 0x40008026 */__IO uint8_t UH_SETUP; /*!< 0x40008026 */};__IO uint8_t Reserve12; /*!< 0x40008027 */union {__IO uint8_t UEP2_T_LEN; /*!< 0x40008028 */__IO uint8_t UH_EP_PID; /*!< 0x40008028 */};__IO uint8_t Reserve13; /*!< 0x40008029 */union {__IO uint8_t UEP2_CTRL; /*!< 0x4000802a */__IO uint8_t UH_RX_CTRL; /*!< 0x4000802a */};__IO uint8_t Reserve14; /*!< 0x4000802b */union {__IO uint8_t UEP3_T_LEN; /*!< 0x4000802c */__IO uint8_t UH_TX_LEN; /*!< 0x4000802c */};__IO uint8_t Reserve15; /*!< 0x4000802d */union {__IO uint8_t UEP3_CTRL; /*!< 0x4000802e */__IO uint8_t UH_TX_CTRL; /*!< 0x4000802e */};__IO uint8_t Reserve16; /*!< 0x4000802f */__IO uint8_t UEP4_T_LEN; /*!< 0x40008030 */__IO uint8_t Reserve17; /*!< 0x40008031 */__IO uint8_t UEP4_CTRL; /*!< 0x40008032 */__IO uint8_t Reserve18[33]; /*!< 0x40008033 */__IO uint16_t UEP5_DMA; /*!< 0x40008054 */__IO uint16_t Reserve19; /*!< 0x40008056 */__IO uint16_t UEP6_DMA; /*!< 0x40008058 */__IO uint16_t Reserve20; /*!< 0x4000805a */__IO uint16_t UEP7_DMA; /*!< 0x4000805c */__IO uint8_t Reserve21[6]; /*!< 0x4000805e */__IO uint8_t UEP5_T_LEN; /*!< 0x40008064 */__IO uint8_t Reserve22; /*!< 0x40008065 */__IO uint8_t UEP5_CTRL; /*!< 0x40008066 */__IO uint8_t Reserve23; /*!< 0x40008067 */__IO uint8_t UEP6_T_LEN; /*!< 0x40008068 */__IO uint8_t Reserve24; /*!< 0x40008069 */__IO uint8_t UEP6_CTRL; /*!< 0x4000806a */__IO uint8_t Reserve25; /*!< 0x4000806b */__IO uint8_t UEP7_T_LEN; /*!< 0x4000806c */__IO uint8_t Reserve26; /*!< 0x4000806d */__IO uint8_t UEP7_CTRL; /*!< 0x4000806e */
} USB_FS_TypeDef;
接下来我们看CherryUSB部分。
第二步:我们需要大致了解CherryUSB的port api,下面列出并且简要介绍每个api需要设计成什么样的作用。
int usb_dc_init(void)int usb_dc_deinit(void)int usbd_set_address(const uint8_t addr)int usbd_ep_open(const struct usbd_endpoint_cfg *ep_cfg)int usbd_ep_close(const uint8_t ep)int usbd_ep_start_write(const uint8_t ep, const uint8_t *data, uint32_t data_len)int usbd_ep_start_read(const uint8_t ep, uint8_t *data, uint32_t data_len)int usbd_ep_set_stall(const uint8_t ep)int usbd_ep_clear_stall(const uint8_t ep)int usbd_ep_is_stalled(const uint8_t ep, uint8_t *stalled)void USBD_IRQHandler(void)
以上就是我们移植索要编写的所有函数。
1、int usb_dc_init(void) usb device control init,usb设备控制驱动程序初始化,我们需要在这个函数里面完成usb外设的初始化,中断使能等操作,同时为每个端点的dma静态分配一段内存。
2、int usb_dc_deinit(void),usb设备控制驱动程序去初始化,其实这个函数可写也可不写,写的话就是关闭中断,清除usb控制寄存器等。
3、usbd_set_address(const uint8_t addr),设置地址,这个函数我们设计成若传入的地址不是0,将其保存下来,在设置地址命令的状态阶段完成以后将其填入地址寄存器。
4、int usbd_ep_open(const struct usbd_endpoint_cfg *ep_cfg),这个函数就是打开端点,这里其实我们设计的时候在int usb_dc_init(void)已经将所有端点都打开了,因为考虑到IP有一个dma缓冲区的限制,默认所有端点都打开,且都是双向端点,所以在usbd_ep_open这个函数中我们设计成配置端点的使能标志和最大包长(供协议栈处理用,不涉及硬件配置)。
5、int usbd_ep_close(const uint8_t ep),这个函数就是关闭端点,设计成清除端点的使能标志。
6、int usbd_ep_start_write(const uint8_t ep, const uint8_t *data, uint32_t data_len),端点开始写入数据,设计成不限制传入数据的长度,在中断里面软件分包继续发送。
7、int usbd_ep_start_read(const uint8_t ep, uint8_t *data, uint32_t data_len),端点开始读取,这个函数的意思是准备好接收缓冲区,等待主机发送数据,因为中断out设计的是out完成中断,也就是一旦进入out回调了,那么数据就已经在我们事先准备好的接收缓冲区里面了,跟out中断还是有区别的,out中断是进入out回调后需要用户调用一次ep_read将数据从fifo里面读取出来 ,这个ep_read是在进入回调之后才调用,而out完成是在进入回调之前就要调用ep_start_read来准备好接收缓冲区。
8、int usbd_ep_set_stall(const uint8_t ep),在传输出现错误的时候,我们需要向主机回复stall。
9、int usbd_ep_clear_stall(const uint8_t ep), 这个函数会在端点请求的clear feature中被调用,用于清除端点stall。
10、int usbd_ep_is_stalled(const uint8_t ep, uint8_t *stalled),其实这个函数协议栈里面目前没有用到,直接返回0即可。
11、USBD_IRQHandler(void),usb中断处理函数,后面代码说。
第三步:开始编写porting文件
1、首先定义两个USB外设的基地址,这里默认使用USB0,基地址为0x40008000u
2、静态分配一些内存,因为要兼容CH571,CH572,CH573,所以默认的端点数目为5个,当然EP_NUMS在CH58x上面可以修改为8。
3、配合协议栈进行处理的一些变量
4、usb_dc_init编写
第一部分是将dma缓冲区的地址给到前面ep_info的一个指针,方便后续处理的编写。
第二部分是端点使能以及端点dma地址的配置。
第三部分是将所有端点的状态设置为NAK,并且能开启自动翻转的开启自动翻转,将设备地址复位为0,开启上拉,开启忙自动回复NAK,使能dma。
第四部分根据基地址来判断是哪个USB外设,复用对应的USB数据线引脚,清除中断flag,使能硬件端口,开启前文提到的三个中断。最后一个 usb_dc_low_level_init();需要用户自己去实现,这里主要就是开启PFIC的USB中断。
5、usb_dc_deinit 没有实现这个函数。
6、usbd_set_address 编写,不做过多描述,前文已经说明了。
7、usbd_ep_open
8、usbd_ep_close
9、usbd_ep_start_write
需要注意的就是同步端点和普通端点的有区别
10、usbd_ep_start_read
这里的端点的dma地址已经在dc_init里面设置好了,所以在设计这个函数的时候,只需要把用户的buffer地址记录下来,在中断里面将数据复制到用户的buffer即可。
11、usbd_ep_set_stall
12、usbd_ep_clear_stall
13、usbd_ep_is_stalled
14、USBD_IRQHandler
我们先看大体框架:
接着我们看传输完成中断:
上文阅读文档时提到,在有in或者out和setup同时存在的情况下,应该优先处理前者,处理完成以后清除中断标志再处理后者。
我们先看端点0的in中断:
首先我们需要明白,端点0的in中断有哪些情况,第一种就是ep0成功发送数据以后会产生in中断,第二种是主机发送in包产生的状态阶段(实际上是设备发送了一个0长度数据包)
setup(host->dev) data(可有可无) status phase(in)
setup(dev->host) data status phase(out)
所以在设计这一部分的时候,我们判断了一下当前setup的方向,如果是get,说明是上一次发送数据成功产生的in中断,这种情况我们需要翻转同步标识位(注意同步标识位在进setup中断的时候被复位成DATA1,因为setup包是以DATA0下发的,下一个不管是in包还是out包都将是DATA1)。
这里还有一个注意的点就是需要手动将端点状态设置为NAK,否则会一直中断。
接下来就是协议栈的一些处理,基本都是相同的,直接看代码应该就能看懂。
如果是set,看下图,我们可以判断一下当前的setup得请求是否是设置地址,如果是设置地址,说明状态阶段已经完成,我们可以将地址填入到设备地址寄存器,并且将ep0发送状态设置为NAK,ep0接收设置为ACK,以便能接收下一次的setup包。如果不是设置地址,也是状态阶段,但是不需要做别的操作,只需要将ep0发送状态设置为NAK,ep0接收设置为ACK,以便能接收下一次的setup包。
(其实这里也可以直接判断地址是否大于0)。到这里端点0的in我们就讲完了。
接下来我们看其他端点的in中断,看下图:
其实注意的地方一个是ep4需要手动翻转同步标识符,另一个就是需要判断一下是否能进用户的回调函数,因为当发送的包长大于端点的最大包长的时候,我们是在中断里面分包继续发送的。
接着我们看端点0的out中断,看下图:
需要注意的就是要手动翻转同步标识符,如果是0长度数据包,要使能端点接收,准备接收下一次的setup包。
其他端点的out中断,端点4需要手动翻转同步标识符。在一开始的时候检查是否同步,不同步的包可以丢弃。
到这里in和out中断看完了,接着我们看setup中断,看下图:
接下来看reset中断,见下图:
基本就是复位地址,调用协议栈的resthandler,然后使能端点0接收,以便能成功接收到setup包。
接下来是suspend中断,见下图:
到此,移植全部结束。详细代码请见CherryUSB主分支。
https://github.com/sakumisu/CherryUSB