在WB32F103(ARM cortex m3内核,96Mhz)的gpio初始化中有一段代码,充分的结合了硬件特征并使用C语言的技巧来快速的配置对应的GPIO的功能,堪称经典和楷模,代码异常简洁,执行速度快,配置任意IO方便快捷。
我们先来看看这一段源代码:
void GPIO_Init(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, uint32_t PinConfig)
{
uint32_t tmp = PinConfig;
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_Pin));
GPIOx->CFGMSK = ~GPIO_Pin;
GPIOx->MODER = ((tmp >> 28) & 0x3) * 0x55555555U;
GPIOx->OTYPER = ((tmp >> 24) & 0x1) * 0xFFFFFFFFU;
GPIOx->OSPEEDR = ((tmp >> 20) & 0x3) * 0x55555555U;
GPIOx->PUPDR = ((tmp >> 16) & 0x3) * 0x55555555U;
tmp = (tmp & 0xF) * 0x11111111U;
GPIOx->AFRL = tmp;
GPIOx->AFRH = tmp;
}
相应的调用方式如下:
GPIO_Init(GPIOB, GPIO_Pin_8|GPIO_Pin_9, GPIO_MODE_IN | GPIO_PUPD_UP|GPIO_SPEED_LOW);
一条简单的函数调用代码,就可以完成对同一组IO的多个相同功能IO的同时配置,代码不可谓不经典和优雅,高效易读易懂。
但是很多人在阅读GPIO_Init函数的源代码的时候,对里面的一些运算操作确感觉到懵懵懂懂,理解不了,搞不明白。下面我们结合该硬件的特征讲解和说明一下(注意:不同的MCU的硬件寄存器会有不同的操作方式,需要紧密结合你使用的MCU的硬件寄存器的特征来理解),可以起到举一反三和触类旁通的作用:
要完成这个“一步到位”的操作,首先要对相关的宏进行合理的有技巧的定义,看看相关IO操作的宏定义如下:
/** @defgroup GPIO_MODE_define
* @{
*/
#define GPIO_MODE_IN 0x00000000U /*!< Input mode */
#define GPIO_MODE_OUT 0x10000000U /*!< Output mode */
#define GPIO_MODE_AF 0x20000000U /*!< Alternate function mode */
#define GPIO_MODE_ANA 0x30000000U /*!< Analog mode */
/**
* @}
*/
/** @defgroup GPIO_OTYPE_define
* @{
*/
#define GPIO_OTYPE_PP 0x00000000U /*!< Output push-pull */
#define GPIO_OTYPE_OD 0x01000000U /*!< Output open-drain */
/**
* @}
*/
/** @defgroup GPIO_SPEED_define
* @{
*/
#define GPIO_SPEED_LOW 0x00100000U /*!< Low speed */
#define GPIO_SPEED_HIGH 0x00000000U /*!< High speed */
/**
* @}
*/
/** @defgroup GPIO_PUPD_define
* @{
*/
#define GPIO_PUPD_NOPULL 0x00000000U /*!< No pull resistor */
#define GPIO_PUPD_UP 0x00010000U /*!< Pull up resistor enabled */
#define GPIO_PUPD_DOWN 0x00020000U /*!< Pull down resistor enabled */
/**
* @}
*/
/** @defgroup GPIO_Pin_sources
* @{
*/
#define GPIO_PinSource0 ((uint8_t)0x00)
#define GPIO_PinSource1 ((uint8_t)0x01)
#define GPIO_PinSource2 ((uint8_t)0x02)
#define GPIO_PinSource3 ((uint8_t)0x03)
#define GPIO_PinSource4 ((uint8_t)0x04)
#define GPIO_PinSource5 ((uint8_t)0x05)
#define GPIO_PinSource6 ((uint8_t)0x06)
#define GPIO_PinSource7 ((uint8_t)0x07)
#define GPIO_PinSource8 ((uint8_t)0x08)
#define GPIO_PinSource9 ((uint8_t)0x09)
#define GPIO_PinSource10 ((uint8_t)0x0A)
#define GPIO_PinSource11 ((uint8_t)0x0B)
#define GPIO_PinSource12 ((uint8_t)0x0C)
#define GPIO_PinSource13 ((uint8_t)0x0D)
#define GPIO_PinSource14 ((uint8_t)0x0E)
#define GPIO_PinSource15 ((uint8_t)0x0F)
/**
* @}
*/
宏里面分别定义了GPIO的pin引脚序号(GPIO_PinSource0-15),输入输入模式配置(输入,输出,特殊功能,模拟),输出类型(推挽,开漏),IO速度(高速,低速),输入电阻配置(无上下拉,上拉,下拉)。他们的定义bit位置是经过精心的安排和计算的(比如不同的功能定义占用的bit位置不重叠,方便进行移位运算,和对应的寄存器的操作有一一的对应关系),以便于后续代码设计和简化代码的操作。好了,准备好这些原材料后,我们具体看看代码的实现过程:
GPIOx->CFGMSK = ~GPIO_Pin;
这第一行代码,非常关键,要明白它的作用,要对应的查看mcu的规格书,我们发现该mcu有一个操作gpio配置寄存器的特殊功能,其说明如下:
以上说明再翻译一下:这一行代码的作用,就是允许后续写其他IO配置寄存器的时候,只对本次要配置的gpio的对应bit进行写操作,不影响无需配置的其他bit(后面代码解释再说明)。
对应的寄存器定义如下:
好了,接下来看看第二行代码的作用和操作技巧:
GPIOx->MODER = ((tmp >> 28) & 0x3) * 0x55555555U;
该代码操作的对象是端口模式寄存器,对应的寄存器功能如上图(用一个32bit的寄存器来表示16个io的模式配置,每一个io的模式配置位占2bit,并且按照顺序排列).
#define GPIO_MODE_IN 0x00000000U /*!< Input mode */
#define GPIO_MODE_OUT 0x10000000U /*!< Output mode */
#define GPIO_MODE_AF 0x20000000U /*!< Alternate function mode */
#define GPIO_MODE_ANA 0x30000000U /*!< Analog mode */
结合前面模式定义的宏来理解:
(tmp >> 28) & 0x3-— 该代码的作用就是取到传入的模式配置数据(模式配置定义在最高4个bit,所以先右移位28bit,然后与3,取出来其值),其结果可能的数据为:0,1,2,3,刚好和寄存器的2个bit的4种组合对应:
00 --对应输入模式
01–对应输出模式
10–复用功能模式
11–模拟模式
比较让人疑惑或者难以理解就是后续这个乘以0x55555555U,要理解这个作用,我们把代码换一种写法来看看:
GPIOx->MODER = 0x55555555U * ((tmp >> 28) & 0x3);
0x55555555U用二进制来看看是什么样子:0101 0101 0101 0101 0101 0101 0101 0101
我们结合寄存器的每两个bit表示一个gpio的模式配置来看看,也就是对应于每一个gpio的配置位初始值为01,如果把这个01和前面的模式值(((tmp >> 28) & 0x3))进行运算,我们发现得到如下结果:
0101 0101 0101 0101 0101 0101 0101 0101: 和0相乘,结果为0000 0000 0000 0000 0000 0000 0000 0000 ,所有GPIO的配置bit为输入模式(00)
0101 0101 0101 0101 0101 0101 0101 0101: 和1相乘,结果不变,所有GPIO的配置bit为输出模式(01)
0101 0101 0101 0101 0101 0101 0101 0101: 和2相乘,相当于左移1位,结果为:1010 1010 1010 1010 1010 1010 1010 1010 所有GPIO的配置bit为复用功能模式(10)
0101 0101 0101 0101 0101 0101 0101 0101: 和3相乘,结果为:1111 1111 1111 1111 1111 1111 1111 1111 ,所有GPIO的配置bit为模拟模式(11)
这真是一个非常高效和简洁,优雅的设计技巧(包括硬件和软件)。
关键点来了,这个值的修改是对所有16个gpio进行同时操作的,如果我只是设置某一个gpio,会不会影响到其他gpio的配置呢?答案是肯定不会。
回到前面我们看看有一个关键寄存器GPIOx->CFGMSK,英文全称应该是config mask,中文翻译为配置辅助寄存器,直译为配置屏蔽寄存器可能更容易理解一些。
GPIOx->CFGMSK = ~GPIO_Pin;通过前面这一条代码的操作,屏蔽了不需要操作的gpio配置位(也就是说关闭了对无关gpio的bit写的作用),比如你本次只是操作gpio0,这条代码就会把对gpio1-15的操作屏蔽,以后写其他配置寄存器(比如前面的MODER寄存器),就只有gpio0对应的bit起作用,其他bit不会影响原来的值。
接下来的其他几条语句的作用类似,参考规格书就可以分析和看明白。
这再一次说明了一个道理,嵌入式开发,软件和硬件要充分结合,才能设计高效的代码。
根据硬件特征,设计合理和简洁的操作代码,也算是一种算法–一种针对硬件进行操作优化的算法。
文章为原创,欢迎转载,请注明出处