本来不想讨论这个问题,但是太多的初学者问这个问题。因此单开一个章节来表明一下我们的观点。 首先声明下:我们所有的例程都是用ST官方固件库进行开发的,全部是C语言。 很多从51过来的初学者很排斥用固件库,认为固件库太复杂,看不懂,找个寄存器定义都找不到;用库学不到东西。如果你持有这种观点,只能说明你c语言没有过关。还停留在一个 main.c 文件就是全部程序的最初级阶段。 所谓的固件库就是将一组功能相关的特殊功能寄存器的操作过程封装为一些C语言函数,以方便应用程序调用,方便使用者记忆。 其实固件库中的每一个函数,如果单步跟踪进去,就是原生的寄存器操作代码。也许你看不懂这种寄存器操作形式,不过没关系,我们后面将举个例子来介绍如何阅读这些代码。我在这个地方需要表达的是,直接操作寄存器和调用固件库中函数本质上没有区别,不会因为就多封装了1层就改变了性质。 下面这个库函数在stm32f4xx_gpio.c, 相关的宏定义在stm32f4xx_gpio.h 文件。 GPIO_WriteBit()是操作GPIO的函数。使用方法: GPIO_WriteBit(GPIOA, GPIO_Pin_6, 0); // 表示 PA6 = 0, 这是使用库函数 GPIO_WriteBit(GPIOA, GPIO_Pin_6, 1); // 表示 PA6 = 1, 这是使用库函数 将上面2行库函数代码展开,直接用寄存器方式表达如下: GPIOA->BSRRH = GPIO_Pin_6 ; // 表示 PA6 = 0. 这就是直接操作寄存器, 寄存器名字就用固件库中定义好的名字 GPIOA->BSRRL = GPIO_Pin_6 ; // 表示 PA6 = 1. 这就是直接操作寄存器 如果你喜欢,可以再封装一个define宏定义,以使代码更容易懂:(我们大多采用这种方法) #define PA6_0( ) GPIOA->BSRRH = GPIO_Pin_6 // 宏的名字可以取得更有意义一些,比如 SPI_CS_0() #define PA6_1( ) GPIOA->BSRRL = GPIO_Pin_6 // #define GPIO_Pin_6 ((uint16_t)0x0040) /* Pin 6 selected */ 这样给IO赋值的操作就变为: PA6_0( ); PA6_1( ); 下面是库函数的实现 void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal) { /* Check the parameters */ assert_param(IS_GPIO_ALL_PERIPH(GPIOx)); -à 检查入口参数合法性,暂时不理会 assert_param(IS_GET_GPIO_PIN(GPIO_Pin)); -à 检查入口参数合法性,暂时不理会 assert_param(IS_GPIO_BIT_ACTION(BitVal)); -à 检查入口参数合法性,暂时不理会 if (BitVal != Bit_RESET) { GPIOx->BSRRL = GPIO_Pin; -à这就是直接操作寄存器 } else { GPIOx->BSRRH = GPIO_Pin ; -à这就是直接操作寄存器 } } 如果对执行效率很在意,你可以将库函数就地展看。我们提供的例程中,有很操作GPIO的宏定义就是使用这种就地展开的写法以提高GPIO翻转效率。 下面我们说说如何查看 GPIOA->BSRRL 这个寄存器的物理地址。 相关的宏定义如下: 在stm32f4xx.h文件 #define PERIPH_BASE ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region #define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000) #define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000) typedef struct { __IO uint32_t MODER; /*!< GPIO port mode register, Address offset: 0x00 */ __IO uint32_t OTYPER; /*!< GPIO port output type register, Address offset: 0x04 */ __IO uint32_t OSPEEDR; /*!< GPIO port output speed register, Address offset: 0x08 */ __IO uint32_t PUPDR; /*!< GPIO port pull-up/pull-down register, Address offset: 0x0C */ __IO uint32_t IDR; /*!< GPIO port input data register, Address offset: 0x10 */ __IO uint32_t ODR; /*!< GPIO port output data register, Address offset: 0x14 */ __IO uint16_t BSRRL; /*!< GPIO port bit set/reset low register, Address offset: 0x18 */ __IO uint16_t BSRRH; /*!< GPIO port bit set/reset high register, Address offset: 0x1A */ __IO uint32_t LCKR; /*!< GPIO port configuration lock register, Address offset: 0x1C */ __IO uint32_t AFR[2]; /*!< GPIO alternate function registers, Address offset: 0x20-0x24 */ } GPIO_TypeDef; #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) GPIOA 其实是一个宏,我们将 GPIOA->BSRRL 中的宏展开,得到: GPIOA->BSRRL ===> ((GPIO_TypeDef *) 0x40020000)->BSRRL ((GPIO_TypeDef *) 0x40020000) 等效于是一个结构体类型(类型名字为 GPIO_TypeDef)的指针,它指向位于0x40020000存储单元(就是STM32的特殊功能寄存器区)的一个 GPIO_TypeDef 型的结构体变量。((GPIO_TypeDef *) 0x40020000)->BSRRL 表示这个结构体的成员变量 BSRRL。BSRRL成员变量在结构体中的偏移地址为24,因此 GPIOA->BSRRL 这个变量实际地址是 0x40020018。(如果你不能理解这段文字,就需要找本c语言的书籍,仔细看看结构体和指针的章节) GPIOA->BSRRL 这个寄存器的地址为 0x40020000 + 24 ---à 24 是结构体成员变量BSRRL的偏移地址。 知道了地址后,你就可以自己定义一个自己喜欢的名字,比如我们将这个寄存器命名为 GPIOA_BSRRL #define GPIOA_BSRRL *(__IO uint16_t *) 0x40020018 之后,访问这个寄存器就和51程序一样了, 比如: RegValue = GPIOA_BSRRL; /* 读寄存器的值 */ GPIOA_BSRRL = 0x40; /* 向寄存器写入一个值 */ 其实变换来变换去,只是代码形式的差异。既然ST已经先入为主定义好了,那么我们直接用就是了,没必要再另起炉灶重新定义一组寄存器宏。 因此,如果你喜欢寄存器操作,就这样写好了: RegValue = GPIOA->BSRRL; /* 读寄存器的值 */ GPIOA->BSRRL = 0x40; /* 向寄存器写入一个值 */ ST固件库的软件结构非常好,编码很规范,值得我们学习。 我非常感谢ST工程师设计了这套固件库,它很好用,以至于我认为完全没有必要去看CPU的寄存器手册(事实上我也从来没有去看过CPU寄存器资料,我只知道如何使用库函数)。我可以腾出更多的时间去学习更有意义的知识。WiFi、NAND 文件系统、emWin、uCOS-iii、Zigbee 等等还等着我去钻研。
我在用其他单片机(比如 飞思卡尔、PIC单片机)开发项目时,因为没有这么完整的固件库,只有硬着头皮看寄存器手册,浪费了不少时间。 |