【GD32】从零开始学GD32单片机 | USB通用串行总线接口+HID键盘例程(GD32F470ZGT6)

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文件编写业务代码。

cpp 复制代码
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个延时函数。

cpp 复制代码
void usb_udelay(const uint32_t usec)
{
    delay_us(usec);
}

void usb_mdelay(const uint32_t msec)
{
    delay_ms(msec);
}

同时需要移植USBFS的中断,直接调官方驱动的函数即可,hid_keyboard是一个自己定义的一个全局变量。

cpp 复制代码
void USBFS_IRQHandler(void)
{
	extern usb_core_driver hid_keyboard;
    usbd_isr(&hid_keyboard);
}

3.4.3 业务功能部分

业务部分就是简单写一个按键的处理,配合USB驱动的函数。

cpp 复制代码
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函数可以帮我们完成所有的初始化工作,初始化后等待枚举成功才会进业务循环。

cpp 复制代码
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键盘设备。

按下我们设置的按键, 在文本框里面就会打出对应的字母。

相关推荐
励志成为嵌入式工程师3 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
hairenjing11235 小时前
使用 Mac 数据恢复从 iPhoto 图库中恢复照片
windows·stm32·嵌入式硬件·macos·word
hikktn6 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust
观音山保我别报错6 小时前
C语言扫雷小游戏
c语言·开发语言·算法
模拟IC攻城狮7 小时前
华为海思招聘-芯片与器件设计工程师-模拟芯片方向- 机试题-真题套题题目——共8套(每套四十题)
嵌入式硬件·华为·硬件架构·芯片
IT B业生7 小时前
51单片机教程(六)- LED流水灯
单片机·嵌入式硬件·51单片机
一枝小雨7 小时前
51单片机学习心得2(基于STC89C52):串口通信(UART)
单片机·嵌入式硬件·51单片机
IT B业生8 小时前
51单片机教程(一)- 开发环境搭建
单片机·嵌入式硬件·51单片机
小林熬夜学编程8 小时前
【Linux系统编程】第四十一弹---线程深度解析:从地址空间到多线程实践
linux·c语言·开发语言·c++·算法