文章说明:
1.基于atmel 的sama5d3处理器, linux kernel 4.1.20.
2.本文章的原doc格式文档, 文章中流程图的原drawio格式文件存放路径:
https://gitee.com/suiren/sama5d3_usb_gadget_read/tree/master/sama5d3_read
3.文章中分析使用的精简的gadget zero驱动, 和测试该zero驱动所使用的host端驱动例程路径:
https://gitee.com/suiren/sama5d3_usb_gadget_read/tree/master/usb_bbb
4.Gadget代码中结构体与代码的关系说明:
https://gitee.com/suiren/sama5d3_usb_gadget_read/blob/master/sama5d3_read/gadget_read.c
5.本文章使用SED情景分析法阅读而得.
-
usb gadget框架, usb device控制器和gadget_zero功能的代码流程总结:
1.1 Gadget初始化
usba_udc_probe()
->读取dts, 获得控制器寄存器的地址,和ep数据内存的地址,获取控制器irq号. 为struct usba_udc *udc_a 分配内存.
->atmel_udc_of_init()
->读取dts, 获得usb_vbus和usb_id引脚的gpio号, 获取所有端点的信息,包含每个端点支持的能力. 当前usb device控制器拥有16个端点. 为udc_a->usba_ep数组分配内存, 根据dts的信息填充usba_ep数组. usba_ep[0]->ep作为控制端点供枚举流程使用, usba_ep[1~15]->ep可供用户编程使用. 将usba_ep[1~15]->ep添加进ep_list列表.
->注册控制器的irq中断服务函数, 用于处理控制器的状态变化,ep收发等中断. 注册usb_vbus引脚中断,处理设备的拔插. 注册usb_id引脚中断, 处理id引脚的电平, 开关usb_host的vbus的电压输出.
->usb_add_gadget_udc()
->usb_add_gadget_udc_release()
->为 struct usb_udc *udc分配内存, 将udc添加进udc_list列表.
usb_composite_probe()
->usb_gadget_probe_driver()
-> 遍历 udc_list列表, 找到udc->driver为NULL的udc结构体.
->udc_bind_to_driver()
->driver->bind() ==> composite_bind()
->为struct usb_composite_dev *cdev分配内存.
->composite_dev_prepare()
->设置cdev->req, 该req用于设备枚举流程的数据传输.为cdev->req->buf分配内存.
->composite->bind() ==> zero_bind()
#注释: loopback_driver 是包含纯粹的配置描述符的信息,func_lb则是包含每个接口描述符的信息. 每个接口描述符紧跟着还有还有端点描述符的信息.
->usb_add_config_only(cdev, &loopback_driver)
-> 将loopback_driver 加入cdev->configs列表. loopback_driver包含配置描述符的部分头部信息.
->usb_add_function(&loopback_driver, func_lb)
->将func_lb加入loopback_driver->functions列表.
->function->bind() ==> loopback_bind()
->usb_interface_id()
-> 将func_lb 指针复制到 loopback_driver->interface[x]. x从0开始计数. 多次调用usb_add_function(), 就可以向interface[]数组添加更多的functions,即接口描述符+端点描述符.
->usb_ep_autoconfig()
->根据端点描述符从ep_list列表内,找到符合的,未被使用的ep, 并根据端点描述符设置改ep的地址.
->usb_gadget_udc_start()
->udc->gadget->ops->udc_start ==> atmel_usba_start()
->获取usb_vbus引脚的电压,高电平为已经接入设备.那么将执行 usba_start()函数去初始化usb device控制器. 当前电压为低电平.
1.2 设备接入主机后,引发中断
usba_vbus_irq_thread()
-> 使能时钟. 使能控制器, 使能写控制器寄存器功能. 使能控制器reset完成中断, 即控制器reset完成后,将发出中断.
1.3 控制器reset完成后
usba_udc_irq()
->if (status & USBA_END_OF_RESET)
->清楚中断标志位. 写寄存器,设置端点0属性:最大包大小64,类型为控制端点. 使能端点0 RX_setup中断, 即端点0收到控制传输的SETUP包时将发出中断. 使能 suspend中断, 使能resume 完成中断, 使能ep0 中断.
1.4 控制器suspend完成后:
usba_udc_irq()
-> 清除中断标志位
1.5 ep0 接收到控制传输的SETUP包,host请求设备描述符
usba_udc_irq()
->if (ep_status)
-> 遍历udc_a->usba_ep数组, 找到类型为控制端点的ep, 都执行usba_control_irq()函数. 当前找到的ep为 struct usba_ep eps[0]
->usba_control_irq()
-> 读取端点状态寄存器, 判断RX_SETUP位, 值为1继续执行.
->从eps[0]->fifo获取host发来的数据,数据内容为:
bRequestType 0x80. bRequest 0x06, wValue 0x100length 64
即host请求设备描述符.
清除RX_SETUP中断标志位.
-> 判断当前ep的索引值, 为0代表时默认用于控制传输的ep0, 则继续执行下面代码.
->handle_ep0_setup()
->switch (crq->bRequest) {
default:
->udc->driver->setup() ==> composite_setup()
-> 根据ep获得的数据ep->fifo, 判断此次host获取描述符的请求是要获取设备描述符.
-> 将根据配置描述符数量,ep0最大包大小等数据构建的cdev->desc设备描述符 复制到 cdev->req->buf. buf的内存在composite_dev_prepare()内被分配.
-> 判断ep要发送的数据长度大于等于0, 继续执行
->若长度为0,就要设置为要发送0长数据包,以告诉host数据传输完毕.
->composite_ep0_queue()
->usb_ep_queue()
->ep->ops->queue() ==> usba_ep_queue()
->将cdev->req 添加进struct usba_ep *eps[0]->queue列表.
-> 写控制器寄存器, 使能tx包准备完成中断.
1.6 ep0 IN数据包准备完成中断发生, 即要发送给host的数据包准备好了
usba_udc_irq()
->if (ep_status)
-> 遍历udc_a->usba_ep数组, 找到类型为控制端点的ep, 都执行usba_control_irq()函数.
->usba_control_irq()
-> 读取端点状态寄存器的值. 从struct usba_ep *eps[0]->queue列表中取出一个req.
-> 判断端点状态寄存器的tx包准备完成中断标志位(USBA_TX_PK_RDY)值为1, 则继续执行.
->submit_request()
->next_fifo_transaction()
->将req->buf的数据复制到struct usba_ep eps[0]->fifo 内.
-> 写控制器寄存器, 使能tx包准备完成中断.
->判断req->last_transaction, 值为1, 代表所有数据都复制到eps[0]->fifo. 值为1, 则继续执行.
-> 关闭tx包准备完成中断(USBA_TX_PK_RDY), 使能IN数据传输完成中断, 即数据向host发送完成时,将发出中断.
-> 读端点控制寄存器, 判断 USBA_TX_COMPLETE 标志位. 即IN数据传输完成后的中断标志位. 值为1, 继续执行.
->根据端点状态寄存器, 判断当前控制传输处于数据IN阶段
switch (ep->state) {
case DATA_STAGE_IN:
-> 使能OUT数据接收中断, 即当conghost接收到数据时将发出中断. 关闭IN数据发生中断.
-> 读端点控制寄存器, 判断 USBA_RX_BK_RDY 标志位. 即OUT数据接收完成后的中断标志位. 值为1, 继续执行.
->根据端点状态寄存器, 判断当前控制传输处于数据OUT阶段
switch (ep->state) {
case STATUS_STAGE_OUT:
-> 关闭OUT数据接收完成中断. 清除OUT数据接收完成中断标志位. 将req从eps[0]->queue队列中移除.
1.7 再次发生控制器reset完成中断
1.8 ep0 接收到控制传输的SETUP包, host请求设置设备地址
usba_udc_irq()
->if (ep_status)
# 与上一次接收到setup包的流程相似,但这次为host请求设置设备地址.将省略类似的代码.
->switch (crq->bRequest) {
case USB_REQ_SET_ADDRESS:
->写控制器寄存器, 设置设备地址.使能IN数据输出完成中断.
1.9 发生IN数据传输完成中断:
usba_udc_irq()
->if (ep_status)
-> 判断当前端点寄存器的 IN数据传输完成中断标志位位1.
if ((epstatus & epctrl) & USBA_TX_COMPLETE)
->switch (ep->state) {
case STATUS_STAGE_ADDR:
-> 写控制器寄存器, 使能设备地址.
1.10 ep0 接收到控制传输的SETUP包,host请求完整设备描述符:
usba_udc_irq()
->if (ep_status)
# 此次host再次请求设备描述符, 不再重复描述.
1.11 ep0 接收到控制传输的SETUP包, 此次时获取配置描述符:
usba_udc_irq()
->if (ep_status)
->usba_control_irq()
-> 从eps[0]->fifo获取host发来的数据.
->handle_ep0_setup()
->udc->driver->setup() ==> composite_setup()
->case USB_REQ_GET_DESCRIPTOR:
switch (w_value >> 8) {
case USB_DT_CONFIG:
-> 从eps[0]->fifo中,得知host希望获取的配置描述符的编号(w_index的低8位). 遍历cdev->configs队列,找到第x个()的配置(config).
->config_buf()
-> 将配置描述符(config)的内容复制到cdev->req->buf内. 遍历config->functions列表, 找到匹配gadget->speed的function(包含接口描述符和端点描述符), 将function的内容复制到cdev->req->buf.
->usb_ep_queue()
->将cdev->req 加入eps[0]->queue列表
# 接下来就是将cdev->req->buf的内容复制到eps[0]->fifo,发送给host
1.12 ep0 接收到控制传输的SETUP包, 依旧是获取配置描述符
上一次只是获取描述符而已. 这一次获取描述符+接口,端点描述符. 不进行重复描述.
1.13 ep0 收到控制传输的SETUP包, 此次host请求设置配置描述符
usba_udc_irq()
->if (ep_status)
->usba_control_irq()
->handle_ep0_setup()
->composite_setup()
->switch (ctrl->bRequest) {
case USB_REQ_SET_CONFIGURATION:
-> 遍历cdev->configs列表所有的配置, 找到对应host请求编号的config. 遍历config->interface[]数组内所有的描述符,对与接口描述符则执行其set_alt()函数,进行接口的初始化.该函数由gadget设备驱动定义,即由用户定义.
->f->set_alt() ==> loopback_set_alt()
->enable_endpoint()
->usb_ep_enable()
->ep->ops->enable ==> usba_ep_enable()
-> 根据loopback->in_ep 和 loopback->out_ep 找到对应的物理端点寄存器, 使能对应端点. 根据端点描述符, 设置端点的最大包大小,端点传输方向, 端点类型.
->alloc_requests()
-> 为loopback->in_req 和out_ereq成员分配内存, 为xx_req->buf分配内存. 设置xx_req->comlete数据传输完成函数(当buf接收到数据,或数据传输为host之后,会执行该函数) 为loopback_complete().
->usb_ep_queue()
-> 将xx_req加入loopback->xx_ep->queue队列内. 等待数据接收或被传输至host..
1.14 epx 端点发生中断, 是host有批量传输的请求.
usba_udc_irq()
-> if (dma_status) {
-> 遍历所有端点,读取对应端点的状态寄存器, 判断是否该端点发生数据传输的中断.
从xx_ep->queue队列取出位于队首的req, 将它移除队列. 执行该req->complete()函数.
-
usb device 控制器寄存器说明:
2.1 在usba_start()函数中使用
CTRL USBA_ENABLE_MASK 使能控制器, 使能写控制器寄存器功能
INT_ENB USBA_END_OF_RESET 控制器reset 完成时, 引发中断
2.2 在usba_udc_irq()函数中使用
INT_STA USBA_DET_SUSPEND 控制器suspend 完成标志位
DMA_INT DMA中断发生标志位
EPT_INT 端点中断标志位
USBA_END_OF_RESET 控制器reset完成标志位
USBA_HIGH_SPEED 控制器的工作速度,满速或高速
if (status & USBA_DET_SUSPEND)
INT_CLR USBA_DET_SUSPEND 清除INT_STA寄存器的中断标志位
if (dma_status)
DMA CONTROL USBA_DMA_END_TR_IE 数据传输完成时产生中断
USBA_DMA_CH_EN 使能该DMA通道
USBA_DMA_END_BUF_IE data 全部传输完成会产生中断
USBA_DMA_END_BUF_EN
if (ep_status)
EP STA USBA_RX_SETUP 端点收到控制传输的SETUP包
USBA_TX_PK_RDY 向host发送的数据包准备完成.
EP CTL USBA_TX_PK_RDY 向host发送的数据包准备完成时, 将发出中断
EP CLR_STA USBA_RX_SETUP 清除 UDPHS_EPTSTAx 寄存器中的 RX_SETUP 标志位
EP CTL_ENB USBA_TX_PK_RDY 使能tx包准备ok,和tx传输错误中断
USBA_EPT_ENABLE 根据设备描述符 使能该端点
USBA_RX_SETUP 使能 RX_SETUP 中断
EP SET_STA USBA_TX_PK_RDY 表示ep->fifo已经写入数据, 可以进行数据传输
USBA_TX_COMPLETE 使能 IN data 传输完成中断
EP CTL_DIS USBA_TX_PK_RDY 关闭tx包准备OK, 和tx传输错误中断
USBA_TX_COMPLETE 关闭 IN data 传输完成 中断
EP CFG USBA_EPT_TYPE_BULK 端点类型: 批量端点
USBA_BK_NUMBER_DOUBLE
USBA_EPT_DIR_IN 方向为向host发送数据
USBA_EPT_TYPE_CONTROL 控制端点
USBA_BK_NUMBER_ONE
if (status & USBA_END_OF_RESET)
INT_ENB USBA_DET_SUSPEND: 使能 suspend 中断
USBA_END_OF_RESUME: 使能 END of resume 中断. resume完成后引发中断
EPT_0: 使能 ep0 中断
-
3 usb gadget 驱动编写注意事项
-
3.1 Complete函数必须有效
-
struct usb_request *req.
req->complete 该函数指针必须被设置, gadget框架在调用它时并没有检查其有效性.
3.2 usb_ep_queue() 不能对一个端点连续多次使用.
由于该函数有将 struct usb_request *req->buf 的数据写入当前端点的fifo寄存器或dma buf地址寄存器的操作,
因此多次调用该函数后, 最终仅仅是最后一次的调用有效. 每个端点的fifo寄存器或dma buf地址寄存器是独立的,各自调用usb_ep_queue()并不影响.
3.3 usb_eq_queue()正确调用方法
req = alloc_ep_req(xxx) 分配req.
req->complete = out_complete; 必须设置complete()函数
usb_ep_queue(ep, req); 将req加入该ep的队列内.
当req->buf的数据发送给host,或从host发来数据,产生中断, 调用req->complete().
req->complete()
那么在comlete()函数内, 可以再次分配一个req,或继续使用当前req, 执行usb_ep_queue()将req加入队列.
设置req->complete()指向不同的函数, 以匹配不同的传输流程.
4 usb_ep_queue()函数相关代码的执行流程图
5 usb gadget驱动与 gadget框架间的关系
5.1 设备描述符的设置
gadget驱动提供的设备描述符,仅仅是包含pid和vid等少量信息.
bMaxPacketSize0 为gadget框架根据该usb device控制器的ep0的最大包大小进行设置;
bNumConfigurations 为gadget框架根据配置描述符数组中已加入的描述符数量进行设置;
5.2 gadget驱动提供的struct usb_function.set_alt()函数, 是在gadget框架收到usb host发来请求设置配置描述符后,
根据所请求的描述符index,从配置描述符数组中找到下标为index的配置描述符, 对配置描述符之下的接口描述符一一进行初始化.一般而言, 一个接口描述符对应着一个function, 对应着struct usb_function 结构体.
5.3 ep0 端点由gadget框架接管, 未加入ep_list中, 而分配给gadget驱动的ep都从ep_list中获取. 既然ep0 由gadget框架使用, 因此枚举过程全程由gadget框架负责, 将gadet驱动提供的描述符发送给usb host.
-
usb gadget驱动和usb host驱动例程
-
-
使用方法
-
解压压缩包, 修改文件夹内的Makefile的内核路径以及编译器路径.
在gadget目录和host目录执行make生成ko文件. 复制到开发板
开发板的usb host与usb device接口相连, 加载ko文件.