目录
工程介绍
实例工程为基于KEIL MDK下的GD32F103C8T6工程,移植了官方的标准外设库和USBD(USB设备驱动)库,并完成了CDC_ACM、HID、WINUSB设备类的驱动和应用。
获取方式
文章绑定资源,下载不了私信。
工程目录
打开资源中的数据手册我们可以知道,这个系列芯片下的USB外设只有USBD,一个仅设备外设模块,打开工程可以看到如下目录。
1.USB/USBD:官方库文件,这个目录下是最底层的寄存器接口函数封装的功能和官方提供的中断处理函数,这个是最底层的驱动。
2.USB/Device:官方库文件,这个目录下是基于中断处理函数和底层寄存器接口函数上的事务处理封装,包含了枚举处理,事务处理,电源管理,以及对外声明功能函数,应用层的接口由usbd_core.c提供。
3.USB/Class:部分官方文件,部分用户文件,这个目录下存在设备类的实现,互相独立,只能以任意一种设备类的实现存在,可以在usbd_conf.h中用宏定义控制,这个文件为全局配置文件。
4.Driver:这个目录下存放着USBD的应用示例,我们可以看一下,具体在usbd.c中。
void UBSD_Init(void) { uint32_t system_clock; uint8_t power_reset_flag; rcu_periph_clock_enable(RCU_AHBPeriph_GPIO_PULLUP);//使能检测引脚IO时钟 // USBD时钟配置 system_clock= rcu_clock_freq_get(CK_SYS);//获取系统主频 根据主频设置USBD分频系数 if (system_clock == 48000000U) { rcu_usb_clock_config(RCU_CKUSB_CKPLL_DIV1); } else if (system_clock == 72000000U) { rcu_usb_clock_config(RCU_CKUSB_CKPLL_DIV1_5); } else if (system_clock == 96000000U) { rcu_usb_clock_config(RCU_CKUSB_CKPLL_DIV2); } else { /* reserved */ } // USBD设备选择 #if (USE_USBD_CDCACM) desc = &cdc_desc; class = &cdc_class; #endif #if (USE_USBD_HIDKEY) desc = &hid_desc; class = &hid_class; hid_fop.hid_itf_config = Key_config; hid_fop.hid_itf_data_process = hid_key_data_send; hid_itfop_register(&usbd_cdc,&hid_fop); #endif #if (USE_USBD_WINUSB) desc = &winusb_desc; class = &winusb_class; #endif rcu_periph_clock_enable(RCU_USBD);//使能USBD外设时钟 //gpio_init(USB_PULLUP, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, USB_PULLUP_PIN);//初始化IO检测引脚为推挽输出 //SetSerialString("LJY CDC-ACM USB2.0"); usbd_init(&usbd_cdc, desc, class);//初始化USBD硬件控制器 并开启对应的中断 /*共4位可控优先级 设置1位配置抢占优先级 3位配置响应优先级*/ nvic_priority_group_set(NVIC_PRIGROUP_PRE1_SUB3); /*使能中断源 并设置其抢占和响应优先级*/ nvic_irq_enable((uint8_t)USBD_LP_CAN0_RX0_IRQn, 1U, 0U); //usbd_connect(&usbd_cdc);//上拉检测IO,等待主机枚举 power_reset_flag = GetSysemtRest(); while (USBD_CONFIGURED != usbd_cdc.cur_status && power_reset_flag) { /* 等待枚举完成 */ } #if (USE_USBD_CDCACM) uint8_t usbd_info[] = "This print message comes from USBD CDC_ACD Deivce\n"; usb_cdc_handler *cdc = (usb_cdc_handler *)usbd_cdc.class_data[CDC_COM_INTERFACE]; memcpy(cdc->data,usbd_info,sizeof(usbd_info) - 1); cdc->receive_length = sizeof(usbd_info) - 1; cdc_acm_data_send(&usbd_cdc); #endif }
这是一个封装好的初始化函数,我们解析一下。
1.USBD外设时钟设置:在数据手册上有明确说明,这个外设必须以48M的频率运行才能正常工作,所以对于系统主频和外设的分频系数有一定的要求。
2.无需复用引脚:其实关于USB的D+ D-引脚复用我以前在用stm32的时候就研究过了,当时我移植完了USB库,并成功跑起来了demo,然后我没看到引脚复用,我就一直找,后来才发现不需要复用,不管是stm32还是gd32,在USBD外设中都有一段这样的描述。
意思就是,只要这个外设使能,对应的引脚会自动配置复用好,可以这么理解。
3.初始化条件:可以看到我们初始化最终是调用了这个函数 usbd_init(&usbd_cdc, desc, class);//初始化USBD硬件控制器 并开启对应的中断
这个函数是由前面说的库里 usbd_core.c 提供的,原型是
所以说整个USBD驱动库的实现对外提供了一个总的初始化封装函数,也就是这个,我们从这个函数入手,看一下它初始化所需要的东西和关联的函数。
工程解析
建议打开工程文件配合解析一起看,分析能力强可以直接分析工程文件,大多数关键代码已经打上注释,目的是彻底理解驱动框架。
一.数据结构分析
void usbd_init (usb_dev *udev, usb_desc *desc, usb_class *usbc)
从这个函数的原型可以看出,需要一个3个信息结构体的实例地址。
1.usb_dev
USB核心驱动结构体
struct _usb_dev { /* basic parameters */ uint8_t config; uint8_t dev_addr; __IO uint8_t cur_status; __IO uint8_t backup_status; usb_pm pm; #ifdef LPM_ENABLED usb_lpm lpm; #endif /* LPM_ENABLED */ usb_control control; usb_transc transc_out[EP_COUNT]; usb_transc transc_in[EP_COUNT]; usb_ep_transc ep_transc[EP_COUNT][USBD_TRANSC_COUNT]; /* device class */ usb_desc *desc; usb_class *class_core; usb_handler *drv_handler; void *class_data[USBD_ITF_MAX_NUM]; void *user_data; void *data; };
其中有一个成员 drv_handler 在函数实现中有一个这样的操作
udev->drv_handler = &usbd_drv_handler;
void usbd_init (usb_dev *udev, usb_desc *desc, usb_class *usbc) { /* configure USBD core basic attributes */ usbd_core.basic.max_ep_count = 8U; usbd_core.basic.twin_buf = 1U; usbd_core.basic.ram_size = 512U; usbd_core.dev = udev; udev->desc = desc; udev->class_core = usbc; udev->drv_handler = &usbd_drv_handler; udev->control.ctl_state = USBD_CTL_IDLE; udev->ep_transc[0][TRANSC_SETUP] = _usb_setup_transc; udev->ep_transc[0][TRANSC_OUT] = _usb_out0_transc; udev->ep_transc[0][TRANSC_IN] = _usb_in0_transc; /* configure power management */ udev->pm.power_mode = (udev->desc->config_desc[7] & 0x40U) >> 5U; /* enable USB suspend */ udev->pm.suspend_enabled = 1U; /* USB low level initialization */ udev->drv_handler->init(); /* create serial string */ serial_string_get((uint16_t *)udev->desc->strings[STR_IDX_SERIAL]); }
把另外一个实例的地址给到它,通过追溯实例可以发现 usbd_drv_handler 是一个包含了 由底层寄存器封装好的功能接口实例。
这里的实现在usbd_lld_core.c文件中。
2.usb_desc
包含有关设备类的所有描述符信息的实例。
打开工程中usbd_conf.h配置文件中任意设备类的开关,找到其实现文件,可以看到具体的数据结构和作用。
3.usb_class
包含有关设备类的功能接口,主要用于处理特定的设备类的请求。
打开工程中usbd_conf.h配置文件中任意设备类的开关,找到其实现文件,可以看到具体的数据结构和作用。
二.中断处理
基本上所有的USB主机/设备硬件外设的固件实现中事务的处理都依赖中断,为了达到快速响应枚举阶段和通信阶段的事务,需要依赖中断的实现,但是中断不会处理占用时间过多的事务,一般是传递回调函数和一些标志位的管理。
在usbd_lld_int.c中提供了官方的中断处理函数。
void usbd_isr (void);
这个函数实现了整个过程中所需要的处理,但是需要用户自己调用,在USBD的中断子程序中调用。
void usbd_isr (void)
{
__IO uint16_t int_status = (uint16_t)USBD_INTF;
__IO uint16_t int_flag = (uint16_t)(USBD_INTF & USBD_INTEN);
uint16_t ctl_reg = (uint16_t)(USBD_CTL);
int_flag &= ctl_reg;
usb_dev *udev = usbd_core.dev;
if (INTF_STIF & int_flag) {
/* wait till interrupts are not pending */
while ((int_status = (uint16_t)USBD_INTF) & (uint16_t)INTF_STIF) {
#if USBD_INT_DEBUG
printf("1\n");
#endif
/* get endpoint number */
uint8_t ep_num = (uint8_t)(int_status & INTF_EPNUM);
if (int_status & INTF_DIR) {
//printf("USBD[info]:STIF int -> OUT DIR\n");
/* handle the USB OUT direction transaction */
if (USBD_EPxCS(ep_num) & EPxCS_RX_ST) {
/* clear successful receive interrupt flag */
USBD_EP_RX_ST_CLEAR(ep_num);
if (USBD_EPxCS(ep_num) & EPxCS_SETUP) {
if (0U == ep_num) {
udev->ep_transc[ep_num][TRANSC_SETUP](udev, ep_num);
} else {
return;
}
} else {
usb_transc *transc = &udev->transc_out[ep_num];
uint16_t count = udev->drv_handler->ep_read (transc->xfer_buf, ep_num, (uint8_t)EP_BUF_SNG);
//缓存区里的端点缓存区描述符接收计数是硬件设置的 调试软件没有任何意义
//count返回的是这次数据包的长度 每次数据包都会返回
transc->xfer_buf += count;
transc->xfer_count += count;
if ((transc->xfer_count >= transc->xfer_len) || (count < transc->max_len)) {
if (udev->ep_transc[ep_num][TRANSC_OUT]) {
udev->ep_transc[ep_num][TRANSC_OUT](udev, ep_num);
}
} else {
udev->drv_handler->ep_rx_enable(udev, ep_num);//使能将触发中断
}
}
}
} else {
/* handle the USB IN direction transaction */
if (USBD_EPxCS(ep_num) & EPxCS_TX_ST) {
/* clear successful transmit interrupt flag */
USBD_EP_TX_ST_CLEAR(ep_num);
usb_transc *transc = &udev->transc_in[ep_num];
if (0U == transc->xfer_len) {
if (udev->ep_transc[ep_num][TRANSC_IN]) {
udev->ep_transc[ep_num][TRANSC_IN](udev, ep_num);
}
} else {
usbd_ep_send(udev, ep_num, transc->xfer_buf, transc->xfer_len);
}
}
}
}
}
if (INTF_WKUPIF & int_flag) {
#if USBD_INT_DEBUG
printf("2\n");
#endif
/* clear wakeup interrupt flag in INTF */
CLR(WKUPIF);
/* restore the old cur_status */
udev->cur_status = udev->backup_status;
#ifdef LPM_ENABLED
if ((0U == udev->pm.remote_wakeup_on) && (0U == udev->lpm.L1_resume)) {
resume_mcu(udev);
} else if (1U == udev->pm.remote_wakeup_on) {
/* no operation */
} else {
udev->lpm.L1_resume = 0U;
}
/* clear L1 remote wakeup flag */
udev->lpm.L1_remote_wakeup = 0U;
#else
if (0U == udev->pm.remote_wakeup_on) {
resume_mcu(udev);
}
#endif /* LPM_ENABLED */
}
if (INTF_SPSIF & int_flag) {
#if USBD_INT_DEBUG
printf("3\n");
#endif
if(!(USBD_CTL & CTL_RSREQ)) {
usbd_int_suspend (udev);
/* clear of suspend interrupt flag bit must be done after setting of CTLR_SETSPS */
CLR(SPSIF);
}
}
if (INTF_SOFIF & int_flag) {
#if USBD_INT_DEBUG
//printf("4\n");
#endif
/* clear SOF interrupt flag in INTF */
CLR(SOFIF);
/* if necessary, user can add code here */
if (NULL != usbd_int_fops) {
(void)usbd_int_fops->SOF(udev);
}
}
if (INTF_ESOFIF & int_flag) {
#if USBD_INT_DEBUG
//printf("5\n");
#endif
/* clear ESOF interrupt flag in INTF */
CLR(ESOFIF);
/* control resume time by ESOFs */
if (udev->pm.esof_count > 0U) {
if (0U == --udev->pm.esof_count) {
if (udev->pm.remote_wakeup_on) {
USBD_CTL &= ~CTL_RSREQ;
udev->pm.remote_wakeup_on = 0U;
} else {
USBD_CTL |= CTL_RSREQ;
udev->pm.esof_count = 3U;
udev->pm.remote_wakeup_on = 1U;
}
}
}
}
if (INTF_RSTIF & int_flag) {
/* clear reset interrupt flag in INTF */
CLR(RSTIF);
#if USBD_INT_DEBUG
printf("6\n");
#endif
udev->drv_handler->ep_reset(udev);
}
#ifdef LPM_ENABLED
if (INTF_L1REQ & int_flag) {
/* clear L1 ST bit in LPM INTF */
USBD_INTF = CLR(L1REQ);
/* read BESL field from subendpoint0 register which corresponds to HIRD parameter in LPM spec */
udev->lpm.besl = (USBD_LPMCS & LPMCS_BLSTAT) >> 4;
/* read BREMOTEWAKE bit from subendpoint0 register which corresponding to bRemoteWake bit in LPM request */
udev->lpm.L1_remote_wakeup = (USBD_LPMCS & LPMCS_REMWK) >> 3;
/* process USB device core layer suspend routine */
usbd_int_suspend(udev);
}
#endif /* LPM_ENABLED */
}
1.传输完成中断
在传输完成中断触发后,会轮询检测是否开启的了这个中断,当正确使能这个中断并触发后,判断会话方向(硬件指示),分别有OUT方向和IN方向。
OUT方向:这个方向有两种情况,一个是正常的接收到数据,一个是setup(硬件指示),根据不同的情况处理。
SETUP的处理:
udev->ep_transc[ep_num][TRANSC_SETUP](udev, ep_num);
这个端点号一定是0, 0号端点。找到这个函数的实现,在usb_transc.c种事务处理函数,在初始化的时候挂载到设备驱动成员上,最终调用的函数是setup的事务处理函数。
这个函数也是枚举的核心,根据获取的请求数进行不同的请求处理器,分为三个标志,标准的设备请求,设备类请求,特定厂商请求。具体请求的定义在usbd_enum.c文件和设备类文件中定义。
IN方向:当设备需要向主机发送数据时进行处理。
2.唤醒中断
唤醒中断触发时,清除中断标志位,然后设置设备状态标志,根据是否进行电源管理做不一样的处理。
3.挂起中断
当总线上超过3帧心跳,主机对设备没有任何操作时,进入这个中断,设置挂起状态,并根据是否配置了低功耗模式进入低功耗状态。
4.帧起始中断
前面几章有提到,在完成枚举后,USB2.0全速设备下,主机会以1ms的时间间隔不断的发送一个SOF令牌数据包,表示一个帧的开始,以此来轮询设备的状态,当设备收到这个包后进入中断,调用对应的处理函数,在这里的函数这个函数的定义是空的,可以由用户自由处理。
5.预期的帧起始中断
当硬件准备接收下一个帧的开始信号,但是下一帧还没到来的时候触发。
6.复位中断
当主机发送复位信号被检测到时,触发复位中断,这个中断用于枚举阶段, 初始化硬件外设和端点缓冲区,配置0端点。
上述6个中断构成整个事务的处理,但是并不是所有的中断都是必须,有的是可选的。在调用初始化函数后,会使能对应的中断,然后在做完基本的操作后,会等待复位中断的到来,只有在复位中断到来后才能开始枚举。
三.设备类的实现
设备类的实现将在后续章节分开来说,其本质就是在设备需要的时候,将描述符的数据通过端点传递给主机,主机不会设置描述符的信息,描述符的信息是只读的。
对于整个工程文件而言,在博客文章中解说实在过于鸡肋,很多具体的东西需要看源码之后才能理解,所有在看完文章后仍觉得疑惑可以继续查看工程源码,大多数地方也写上了注释,设备类中提供的描述符和函数接口都只是为中断事务处理提供服务,我们的目的是理解整个框架的分层以及对应的处理在哪里,就可以去修改和优化,以达到在枚举阶段中完成特定的请求和在通信阶段实现任意通信情景。