本书阐述的USB设备开发都是基于STM32单片机的,为了方便之前从未接触过该系列单片机的读者,我们会花费两章的篇幅熟悉相应的编程开发,同时还会深入探讨固件库与硬件底层寄存器之间的关联,这对后续顺利掌握USB控制器相关的源代码分析与编程也有着非凡的意义,对STM32单片机已有一定开发经验的读者可以选择跳过。
我们将要在图4.1所示最小系统上实现这样的功能:按下轻触按键K1则发光二极管D1点亮,松开K1则D1熄灭。在代码的设计思路上,我们只需将D1与K1对应的引脚分别初始化为输出与输入,然后再持续不断地根据读取的输入引脚的电平设置输出引脚的电平即可,相应的源代码如清单6.1所示。
清单6.1 轻触按键控制发光二极管功能
我们从一开始就调用了完成系统时钟初始化的SystemInit函数,通常每个应用程序都会先调用该函数,其具体执行过程牵涉很多底层寄存器。现阶段,我们只需知道清单6.1中的SystemInit函数默认会将系统时钟设置为72MHz即可。
接下来调用RCC_APB2PeriphClockCmd函数打开了GPIOA、GPIOB外设所在的时钟,因为D1与K1对应的引脚分别为PA5与PB8(见表4.2)。STM32单片机将所有模块按传输带宽的不同分别挂在AHB(Advanced High performance Bus)与APB(Advanced Peripheral Bus)总线上,AHB主要用于诸如CPU、DMA、DSP之类高性能模块之间的连接,而APB则用于低带宽的外设连接总线,通过总线桥接进一步分成APB1与APB2,STM32F10x系列单片机的系统简化结构如图6.1所示。
图6.1 STM32F10x系列单片机的系统简化结构
每个外设都有单独可控的时钟源,你想使用哪个外设,就得先使能相应的外设时钟。我们需要使用挂在APB2总线上的GPIO外设,所以需要通过RCC_APB2PeriphClockCmd函数使能相应的时钟,而每个外设的时钟使能位也被定义在stm32f10x_rcc.h头文件中,如清单6.2所示。
清单6.2 stm32f10x_rcc.h头文件(部分)
从清单6.2中可以看到,每一个外设的时钟都对应32位中的某一位,本质上由一个32位的寄存器控制,APB2PeriphClockCmd函数根据第二个参数(“ENABLE”)对该寄存器的某位进行置位与清零操作,大家了解一下即可。
使能GPIO外设对应的时钟后,接下来我们需要初始化使用到的引脚。所有外设都有一个唯一的基地址,它通常包括多个相关的控制寄存器,我们进行外设控制编程的主要思路与51单片机是相同的:对外设(此处为GPIO)相关的控制寄存器进行读写操作。
我们使用到了GPIO外设,为此声明了一个GPIO_InitStructure结构体变量,相应的数据类型GPIO_InitTypeDef被定义在stm32f10x_gpio.h头文件中,如清单6.3所示。
清单6.3 stm32f10x_gpio.h头文件(部分)
GPIO_InitTypeDef结构体包含了需要初始化的引脚(GPIO_Pin)以及相应的速度(Speed)与工作模式(Mode)。成员变量GPIO_Mode用于将引脚初始化为STM32单片机支持的8种工作模式之一,它们在stm32f10x_gpio.h头文件中被定义为枚举常量。由于控制D1的引脚需要用来驱动LED,所以可以将其初始化为推挽输出(GPIO_Mode_Out_PP),这样PA5引脚同时具有拉电流与灌电流的能力。当然,由于图4.1所示硬件电路中的LED是以低有效方式连接的,所以也可以将其初始化为开漏输出(GPIO_Mode_Out_OD),如果开发平台上的LED以高有效方式连接,则只能将其设置为推挽输出。对于读取按键状态的引脚PB8,我们将其设置为下拉输入(GPIO_Mode_IPD),因为硬件电路上并没有给轻触按键配置拉电阻,这可以防止引脚悬空时输入不定状态而影响系统工作。成员变量GPIO_Speed用于指定引脚驱动电路的响应速度,STM32单片机内部在GPIO的输出安排了多个响应不同速度的输出驱动电路,用户可以根据自己的需要选择,速度越快就意味着产生的噪声越大,消耗的电流也相应增加。如果没有特殊要求,可以设置为较小值,我们将其设置为最大值50MHz即可。
引脚的工作模式与响应速度具体针对哪个引脚呢?这是由GPIO_InitTypeDef结构体中的成员变量GPIO_Pin来决定的(此例使用GPIO_Pin_5与GPIO_Pin_8),其实就是设置引脚对应的位掩码,至于具体是哪个GPIO组,则由GPIO_Init函数指出,我们给它传入的是“GPIOA”与LED_InitStructure的地址,前者被定义在stm32f10x.h头文件中,如清单6.4所示。
清单6.4 stm32f10x.h头文件(部分)
GPIOx_BASE表示各组GPIO外设的基地址(均为常数),将其转换为指向GPIO_TypeDef结构体的指针则被定义为GPIOx(指针就是地址,但地址常数是无法直接赋值的),而GPIO_TypeDef结构体包含了7个成员变量,每一个成员变量对应一个GPIO外设相关的32位寄存器(修饰符“_IO”在核心文件core_cm3.h中被定义为关键字volatile,它告诉编译器不要随意优化后面定义的变量,程序应该总是直接针对变量地址进行访问,而不是可能的临时寄存器),它们分别为CRL、CRH、IDR、ODR、BSRR、BRR、LCKR,其中CRL与CRH用来配置引脚工作模式与响应速率的高低32位寄存器,GPIO_Init函数就是通过设置这两个寄存器来配置引脚的。IDR与ODR分别保存GPIO输入与输出的数据,即如果想读取输入引脚的状态,则应该读取IDR寄存器,如果想设置输出引脚的状态,则应该将数据写入ODR寄存器。值得一提的是,BSRR与BRR寄存器也可以用于清零与置位输出引脚的状态。
由于GPIO_TypeDef结构体中的每个成员变量都是无符号32位整型的,所以某个定义的GPIO_TypeDef结构体变量,其经编译后就会给其分配连续的地址空间(7×32位)。如果与GPIO外设相关的寄存器是按相同顺序定义的(事实上,GPIO_TypeDef结构体就是根据硬件寄存器定义的),那么GPIO_TypeDef结构体变量中的每个成员变量的地址和与GPIO外设相关寄存器的地址相同。换句话说,将GPIOA_BASE转换为指向GPIO_TypeDef结构体的指针,也就意味着CRL寄存器的地址为GPIOA_BASE、CRH寄存器的地址为GPIOA_BASE+0x04(因为寄存器均为32位,而地址空间的分配以字节为单位,所以占用4个地址)、IDR寄存器的地址为GPIOA_BASE+0x08,其他以此类推,如图6.2所示。
图6.2 GPIO_TypeDef结构体与GPIO外设相关寄存器之间的关系
也就是说,后续我们可以通过使用“指向GPIO_TypeDef结构体的指针访问成员变量”的方式访问与GPIO外设相关的寄存器。假设GPIOx为指向GPIO_TypeDef结构体的指针,则使用GPIOx->BSRR就可以访问BSRR寄存器,使用GPIOx->BRR就可以访问BRR寄存器,其他以此类推,这样在C语言层面实现起来就会很方便,而不需要通过“基地址与偏移量相加”这种不灵活的方式分别获取每个寄存器的地址。
我们还可以进一步从清单6.4中查到GPIOA_BASE被定义为APB2PERIPH_BASE+0x0800,而APB2PERIPH_BASE被定义为PERIPH_BASE+0x10000,PERIPH_BASE又被进一步定义为((uint32_t)0x40000000)。也就是说,GPIOA外设的基地址GPIOA_BASE为0x40010800。使用同样的方法可以得到GPIOB外设的基地址GPIOB_BASE为0x40010C00。
在while循环语句中,我们不断地获取轻触按键K1的状态(实际应用时通常还会进行按键消抖操作,但这已经超出本书讨论的范畴,因为我们的目的只是熟悉STM32单片机的固件库及编程风格,而不是具体功能的实现方法),这可以通过调用“需要传入GPIO外设基地址及引脚位”的GPIO_ReadInputDataBit函数来实现,它被声明在stm32f10x_gpio.h头文件中,相应的定义在stm32f10x_gpio.c源文件中,如清单6.5所示。
清单6.5 stm32f10x_gpio.c源文件(部分)
GPIO_ReadInputDataBit函数通过“判断IDR寄存器对应引脚的数据位是否为0”返回0或1(Bit_Set与Bit_RESET为枚举常量,见清单6.3)。如果读到的K1状态为1,意味着此时轻触按键处于按下状态,我们应该将PA5设置为低电平来点亮D1,这是通过GPIO_ResetBits函数来实现的;否则,应该将PA5设置为高电平来熄灭D1,这是通过GPIO_SetBits函数来实现的。这两个函数与GPIO_ReadInputDataBit函数定义在同一个文件中,并且需要传入GPIO外设的基地址及相应的引脚位。图6.3清晰展示了GPIO_SetBits函数对引脚位进行置位的操作过程,也就是通过对BSRR寄存器中对应的引脚位写入1完成的,而引脚复位操作则是通过对BRR寄存器对应的引脚位写入1完成的。
图6.3 GPIO_SetBits函数的执行过程