1. 简介
USB,全称通用串行总线,相信大家都非常熟悉了,日常生活只要用到手机电脑都离不开这个接口,像鼠标键盘U盘都需要使用这个接口进行数据传输,下面简单介绍一下。
1.1 版本标准
USB的标准总体可以分为低速、全速和高速,分别对应USB 1.0、USB 1.1和USB 2.0版本;当然后面推出了USB 3.0、USB 3.1和目前最新的USB4标准,下面的表格列出了各个USB版本的差异。
USB标准 | 理论速度 |
USB 1.0 | 1.5Mbps |
USB 1.1 | 12Mbps |
USB 2.0 | 480Mbps |
USB 3.0 | 5Gbps |
USB 3.1 | 10Gbps |
USB4 | 50Gbps |
在GD32F4系列芯片中,内部搭载了USB全速和高速接口,因此是可以使用USB 2.0及以下的标准。
但USB的工作光有接口还不行,必须还要对应的PHY才行,GD32F4内部自带有USB全速PHY,但没有USB高速PHY,所以如果要使用高速USB得在外部硬件电路上添加对应的PHY芯片。所以后面的例程会使用USB的全速标准。
1.2 接口
经过几十年的发展,USB衍生出了众多接口,像我们常用的有USB Type-A和USB Type-C接口。最简单的USB接口只需要4根线即可——电源线(VBUS)、地线(GND)、差分正(DP)和差分负(DP)。
USB为了实现高速的数据传输,是使用差分信号进行通讯的,差分信号具有非常优秀的抗干扰性。在差分通讯中,DP线电压高于DM线电压,代表逻辑1;反之,DP线低于DM线电压,则代表逻辑0。不过,在编程中我们是不需要关心这个的,因为PHY电路会自动为我们处理这些信号。
随着USB的速度越来越快,显然一对差分线就不能满足了,所以USB 2.0以上的USB接口就需要三对差分线进行数据的传输,下面就是USB 3.0接口的管脚定义。
1.3 设备类
使用USB协议的设备众多,显然、每种类型的设备需要传输的数据是不同的,因此USB给每一类的设备定义了对应的设备类(class)。像鼠标、键盘使用的是HID设备类,U盘等存储介质使用的是MSC设备类,同时USB也可以配置成虚拟串口,使用的是CDC设备类。
1.4 通讯
USB是一种热插拔接口,因此在用户插入设备后主机和设备会有一系列的通讯过程,来配置USB的工作环境,之后才能够进行对应的数据传输。
1.4.1 枚举
USB通讯前,主机需要了解怎么与插入的这个设备交流,因此需要有一个枚举的过程,配置相关的信息。
USB设备插入主机,HUB初始化成功后主机会为设备供电,此时设备进入默认状态;接着主机给设备分配地址,进行基本的配置,配置过程一般就是设备告诉主机自己的名字、PID、VID、支持的设备类、供电能力等等信息;每种设备类需要提供主机的信息是不同的,具体可以在USB官网下载对应的文档研究。
1.4.2 传输类型
USB一共有4种数据传输类型——中断传输、同步传输、控制传输和批量传输。
1. 中断传输。低速率,固定延迟。HID设备的典型传输方式。
2. 同步传输。周期、连续的主从信息传递,常用于与时间相关的数据。多用于传输视频帧数据。
3. 控制传输。突发、非周期的由主机发起的通讯,设备的枚举过程就是使用控制传输。
4. 批量传输。非周期、大块数据的突发通讯,MSC设备的典型传输方式。
1.4.3 管道、接口和端点
USB的通讯逻辑由管道、接口和端点组成。
USB通讯的最基本单元是端点(Endpoint),分为输入端点和输出端点,无论是数据还是命令都是通过端点进行传输的;其中端点0是专门用于控制传输的,像枚举过程、主机命令下发都使用端点0;其他的端点的话就可以自定义。
接口(Interface)可以理解为一组端点的集合,它是面向功能而言的。就比如说,我这个设备既支持鼠标操作又支持键盘操作,那么相当于这个设备就有两个功能,所以接口也对应有两个。
管道(Pipe)是用来联系端点与主机软件,它决定数据如何在主机和设备间传输,所以数据在端点的每次传输都要建立管道实现。管道又分为流管道和消息管道;流管道用于传输与USB规范无关的数据,如用户数据;消息管道用于传输包含USB规范的数据。
2. 时钟
USB工作需要48MHz的时钟,在GD32F4系列中,USB时钟可以由内部的RC 48MHz震荡器或PLL锁相环分频得到,一般都会使用RC震荡器(下面时钟树红线路径),因为这个震荡器是带CTC模块的,即可以自动对时钟进行校准。
3. 例程
例程会初始化一个基于HID设备类的键盘,当按下板子上的按钮会向电脑发送键位‘A’。
3.1 HID设备
简单介绍一下例程中涉及到的HID设备类,HID全称人机交互接口,像我们常用的键盘、鼠标、触摸板、手柄等交互类设备都是使用HID。
在设备的枚举过程中HID设备需要提供物理描述符和汇报描述符;物理描述符是可选的,它主要描述这个设备是由人体的哪个或哪些部位所使用的;汇报描述符是必要的,而且非常重要,它描述数据的组织排列方式,主机是通过汇报描述符提供的信息来解析消息或构建数据包的。
不过汇报描述符的格式在这里就不介绍了,要讲的话另开一篇都讲不完,官方文档多达一千多页,而且是全英文的,感兴趣的同学可以下载研究研究。
3.2 枚举过程
USB的枚举都是基于描述符的,描述符在代码中其实就是一个个数组,我们需要根据官方文档中的协议规范往里面填数据。
HID的枚举过程,首先发送设备描述符(Device Descriptor),里面一般包含PID、VID、序列号等信息;接着发送配置描述符(Configuration Descriptor),里面一般包含接口数量和供电配置信息;然后主机会根据配置描述符中的接口数量询问每一个接口的配置,这里就要发送端点描述符(Endpoint Descriptor)和上面提到的HID描述符(HID Descriptor);端点描述符一般包含端点的地址、最大包大小、传输间隔等信息。
除了以上的描述符,主机还会请求字符串描述符(String Descriptor),这个一般就是描述厂商名字、产品名字等信息,每个字符串用一个描述符;这个是可选的,不发或发个空的也没啥问题。
3.3 时钟校准控制器(CTC)
在进入代码前还要再介绍一个外设——CTC。这个外设是专门用来校准IRC48M时钟的,因为内部时钟的精度是比较差的,而USB对时钟的要求是比较高的,因此如果我们使用IRC48M作为USB的时钟的话,就要使用CTC来实时校准IRC48M的精度。
从上面可以看到, CTC的校准时钟可以选择GPIO时钟或外部低速时钟(LXTAL),一般会选择LXTAL。
CTC的校准原理可以大概理解为:当REF同步脉冲信号出现时,时钟频率评估功能开始执行。如果REF同步脉冲信号出现在计数器向下计数的过程中,说明当前时钟频率比期望时钟频率(频率为48M)慢,需要增大TRIMVALUE值(时钟校准值)。如果REF同步脉冲信号出现在计数器向上计数的过程中,说明当前时钟频率比期望时钟频率快,需要减小TRIMVALUE值。
状态寄存器中的CKOKIF、CKWARNIF、CKERR和REFMISS位反映了频率评估的状态。
3.4 代码
3.4.1 官方驱动移植
官方例程里面已经基本上写好了大体的框架了,我们可以基于官方的代码进行修改,先导入一些必须的文件,在路径GD32F4xx_Firmware_Library_V3.2.0\Firmware\GD32F4xx_usb_library下面,全部导入的文件如下。
导入的头文件路径参考如下。
在全局宏定义里面加上USE_USB_FS。
3.4.2 初始化
自己创建.c和.h文件编写业务代码。
static void hid_keyboard_bsp_init(void)
{
/* 初始化GPIO */
rcu_periph_clock_enable(RCU_GPIOA);
gpio_mode_set(GPIOA, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN_0);
/* 初始化EXTI */
rcu_periph_clock_enable(RCU_SYSCFG);
syscfg_exti_line_config(EXTI_SOURCE_GPIOA, EXTI_SOURCE_PIN0);
nvic_irq_enable(EXTI0_IRQn, 1, 0);
exti_init(EXTI_0, EXTI_INTERRUPT, EXTI_TRIG_FALLING);
exti_interrupt_enable(EXTI_0);
/* 初始化USB */
rcu_osci_on(RCU_IRC48M); // 使能IRC48M时钟
while(ERROR == rcu_osci_stab_wait(RCU_IRC48M)); // 等待时钟稳定
/* 初始化外部低速时钟 */
rcu_periph_clock_enable(RCU_PMU);
pmu_backup_write_enable();
rcu_osci_on(RCU_LXTAL);
while(ERROR == rcu_osci_stab_wait(RCU_LXTAL));
rcu_ckout0_config(RCU_CKOUT0SRC_LXTAL, RCU_CKOUT0_DIV1); // 使能时钟输出,1分频
/* 初始化CTC外设 */
rcu_periph_clock_enable(RCU_CTC);
ctc_refsource_prescaler_config(CTC_REFSOURCE_PSC_OFF); // 不使用预分频
ctc_refsource_signal_select(CTC_REFSOURCE_LXTAL); // 校准源使用外部低速时钟
ctc_refsource_polarity_config(CTC_REFSOURCE_POLARITY_RISING); // 上升沿启动新一轮校准
ctc_hardware_trim_mode_config(CTC_HARDWARE_TRIM_MODE_ENABLE); // 使能硬件校准
ctc_counter_reload_value_config(0x05B8); // 1464 * 32.768kHz ≈ 48MHz
ctc_clock_limit_value_config(0x0002); // 校准精度,±2个参考时钟周期
ctc_counter_enable(); // 使能CTC
while (ctc_flag_get(CTC_FLAG_CKOK) == RESET); // 等待校准完成
rcu_ck48m_clock_config(RCU_CK48MSRC_IRC48M); // 选择IRC48M时钟为USB时钟
rcu_periph_clock_enable(RCU_USBFS); // 使能USB时钟
/* 初始化USB管脚 */
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_11 | GPIO_PIN_12);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_11 | GPIO_PIN_12);
gpio_af_set(GPIOA, GPIO_AF_10, GPIO_PIN_11 | GPIO_PIN_12);
nvic_irq_enable(USBFS_IRQn, 2, 0);
}
初始化的内容较多。首先就是初始化用户按键,随便选一个初始化GPIO和EXTI。接着使能IRC48M时钟,初始化CTC外设,这个比较重要。
CTC我使用LXTAL,即外部低速晶振作为校准源,因此还需要初始化LXTAL;LXTAL部分需要使能PMU的时钟和使能backup域写,因为LXTAL是工作在Vbat域的。CTC的reload和limit值是关键,reload值是用来确定最终校准的时钟频率的,reload值×32.768kHz应该要尽可能等于48MHz,即USB的工作频率;limit值是确定校准的精度的,当测量出的时钟超过±limit值个参考时钟,CTC就认为时钟不稳定,会进行时钟校准。
最后就是使能USBFS的时钟,和初始化USB的GPIO和中断,USB的中断优先级不要设得太高(不要高于延时的中断优先级),因为USB中断里面是会调延时函数的,如果USB中断优先级太高,延时中断就没办法处理了。
既然讲到了延时,USB驱动需要移植2个延时函数。
void usb_udelay(const uint32_t usec)
{
delay_us(usec);
}
void usb_mdelay(const uint32_t msec)
{
delay_ms(msec);
}
同时需要移植USBFS的中断,直接调官方驱动的函数即可,hid_keyboard是一个自己定义的一个全局变量。
void USBFS_IRQHandler(void)
{
extern usb_core_driver hid_keyboard;
usbd_isr(&hid_keyboard);
}
3.4.3 业务功能部分
业务部分就是简单写一个按键的处理,配合USB驱动的函数。
void hid_keyboard_process(usb_dev *udev)
{
if (send_flag) {
standard_hid_handler *hid = (standard_hid_handler *)udev->dev.class_data[USBD_HID_INTERFACE];
if (hid->prev_transfer_complete) {
/* 发送按键A */
hid->data[2] = 0x04U;
hid_report_send(udev, hid->data, HID_IN_PACKET);
printf("send key\r\n");
}
send_flag = 0;
}
}
void EXTI0_IRQHandler(void)
{
exti_interrupt_flag_clear(EXTI_0);
send_flag = 1;
}
HID键盘的数据包是固定8字节的,具体的定义可以看USB官方文档学习,这里只需要知道从第3个字节开始填键值即可,一个键值一字节。每个按键的键值是多少也是要看官方文档,字母A的键值就是4。调hid_report_send就可以发数据给主机了。
3.4.4 主函数
usb_init函数可以帮我们完成所有的初始化工作,初始化后等待枚举成功才会进业务循环。
usb_core_driver hid_keyboard;
int main(void)
{
NVIC_SetPriorityGrouping(NVIC_PRIGROUP_PRE4_SUB0);
debug_init();
printf("hid keyboard demo\r\n");
hid_keyboard_init();
usbd_init(&hid_keyboard, USB_CORE_ENUM_FS, &hid_desc, &usbd_hid_cb);
printf("usb init done\r\n");
/* 等待USB枚举成功 */
while (USBD_CONFIGURED != hid_keyboard.dev.cur_status);
printf("usb enumation\r\n");
while (1) {
hid_keyboard_process(&hid_keyboard);
}
}
3.5 运行测试
烧录代码后用USB线连接开发板和电脑,在设备管理器里面就能看到多了一个HID键盘设备。
按下我们设置的按键, 在文本框里面就会打出对应的字母。