目录
前言
上节课讲解了 自己写库——构建库函数雏形 ,包括基本的宏定义(直接操作寄存器)以及使用结构体(库函数)。本节课我们讲解如何实现 RCC 这个外设的寄存器结构体声明,把时钟相关的代码改成寄存器结构体操作的方式,以及第二节内容。
STM32第四节:自己写库——构建库函数雏形(第二节)
RCC外设寄存器结构体声明
首先,我们要定义RCC的基地址,通过PERIPH_BASE定义AHBPERIPH_BASE,然后就可以定义RCC_BASE。在定义完RCC_BASE后,我们就可以像上一节的GPIOB宏定义一样,使用#define RCC对其进行基地址定义。
#define PERIPH_BASE ((unsigned int)0x40000000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)
#define RCC ((RCC_TypeDef*)RCC_BASE)
然后我们只需要改变寄存器名称,就可以定义好RCC时钟端口的结构体。
typedef struct
{
uint32_t CR;
uint32_t CFGR;
uint32_t CIR;
uint32_t APB2RSTR;
uint32_t APB1RSTR;
uint32_t AHBENR;
uint32_t APB2ENR;
uint32_t APB1ENR;
uint32_t BDCR;
uint32_t CSR;
}RCC_TypeDef;
#define RCC ((RCC_TypeDef*)RCC_BASE)
端口置位或复位函数
防止重复定义框架
之前我们编写的函数是针对于GPIOB这个外设,那我们不妨新建两个驱动库文件,把宏定义,函数,声明什么的都放进去。于是就有了stm32f10x_gpio.c和stm32f10x_gpio.h文件。那么在stm32f10x_gpio.c中,我们要先包含头文件stm32f10x_gpio.h,把stm32f10x_gpio.h包含到stm32f10x_gpio.c中。
然后打开stm32f10x_gpio.h文件,写下如下代码:
#ifndef __STM32F10X_GPIO_H
#define __STM32F10X_GPIO_H
#include "stm32f10x.h"
#endif /* __STM32F10X_GPIO_H */
这段代码用于在STM32上操作GPIO(通用输入输出)引脚。接下来逐行解释下代码:
#ifndef __STM32F10X_GPIO_H
: 这是一个预处理指令,用于检查是否已经定义了__STM32F10X_GPIO_H
宏。如果未定义,则继续执行下面的代码,否则跳过。
#define __STM32F10X_GPIO_H
: 定义了__STM32F10X_GPIO_H
宏,以防止头文件被多次包含。
#include "stm32f10x.h"
: 包含了STM32F10X头文件,其中包含了STM32的寄存器定义和常用函数等。
#endif /* __STM32F10X_GPIO_H */
: 结束了条件编译指令,确保只有在未定义__STM32F10X_GPIO_H
宏时才会包含该头文件。
这段代码的作用是定义了一个用于STM32F10X GPIO操作的头文件,并防止多次包含同一个头文件。通过包含这个头文件,可以在STM32上进行GPIO引脚的操作。
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
但是当我们把stm32f10x_gpio.h头文件包含进main.c主函数里时,发现有两个error,检查发现是重复定义,原因是我们在stm32f10x_gpio.h中已经引入stm32f10x.h文件,而这个操作会把stm32f10x.h文件中的所有代码拷贝一份到stm32f10x_gpio.h文件中。那么怎么处理这个问题呢?
我们可以选择直接注释掉上面一行代码,使得不再是重复定义,但是我们有更加严谨的做法,我们打开stm32f10x.h文件,写一个相类似的代码:
#ifndef __STM32F10X_H
#define __STM32F10X_H
//用来存放STM32寄存器映射的代码
//外设 perirhral
#endif /* __STM32F10X_H */
我们在以后的代码编写过程中,要保持这个良好的习惯,不管该头文件中是否有内容,都在头文件中都以此格式编写框架,防止重复调用同一个文件。
编写端口置位或复位函数代码
打开stm32f10x_gpio.c文件,开始编写函数。我们知道,要定义一个置位或复位函数,要有两个输入变量,即GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin,第一个GPIO结构体类型变量,第二个short类型的Pin变量。而Pin变量就是调节哪一位置1的变量代码,如此这般,我们就可以通过宏定义来定义GPIO_Pin_x的地址,如下图所示:
#define GPIO_Pin_0 ((uint16_t)0x0001) /*!< 选择Pin0 */ //(00000000 00000001)b
#define GPIO_Pin_1 ((uint16_t)0x0002) /*!< 选择Pin1 */ //(00000000 00000010)b
#define GPIO_Pin_2 ((uint16_t)0x0004) /*!< 选择Pin2 */ //(00000000 00000100)b
#define GPIO_Pin_3 ((uint16_t)0x0008) /*!< 选择Pin3 */ //(00000000 00001000)b
#define GPIO_Pin_4 ((uint16_t)0x0010) /*!< 选择Pin4 */ //(00000000 00010000)b
#define GPIO_Pin_5 ((uint16_t)0x0020) /*!< 选择Pin5 */ //(00000000 00100000)b
#define GPIO_Pin_6 ((uint16_t)0x0040) /*!< 选择Pin6 */ //(00000000 01000000)b
#define GPIO_Pin_7 ((uint16_t)0x0080) /*!< 选择Pin7 */ //(00000000 10000000)b
#define GPIO_Pin_8 ((uint16_t)0x0100) /*!< 选择Pin8 */ //(00000001 00000000)b
#define GPIO_Pin_9 ((uint16_t)0x0200) /*!< 选择Pin9 */ //(00000010 00000000)b
#define GPIO_Pin_10 ((uint16_t)0x0400) /*!< 选择Pin10 */ //(00000100 00000000)b
#define GPIO_Pin_11 ((uint16_t)0x0800) /*!< 选择Pin11 */ //(00001000 00000000)b
#define GPIO_Pin_12 ((uint16_t)0x1000) /*!< 选择Pin12 */ //(00010000 00000000)b
#define GPIO_Pin_13 ((uint16_t)0x2000) /*!< 选择Pin13 */ //(00100000 00000000)b
#define GPIO_Pin_14 ((uint16_t)0x4000) /*!< 选择Pin14 */ //(01000000 00000000)b
#define GPIO_Pin_15 ((uint16_t)0x8000) /*!< 选择Pin15 */ //(10000000 00000000)b
#define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< 选择全部引脚*/ //(11111111 11111111)b
void GPIO_SetBits(GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin);
void GPIO_ResetBits( GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin );
第一个是置位函数,第二个是复位函数,写在stm32f10x_gpio.c文件中,并且在stm32f10x_gpio.h文件中引用。函数内容的话,就是通过GPIOB结构体寄存器->引用BSRR或BRR寄存器,和我们刚刚建立的GPIO_Pin_x宏定义地址进行相与操作置位。并且记得要在stm32f10x_gpio.h文件中进行函数的声明。
#include "stm32f10x_gpio.h"
void GPIO_SetBits(GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin)
{
GPIOx->BSRR |= GPIO_Pin;
}
void GPIO_ResetBits( GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin )
{
GPIOx->BRR |= GPIO_Pin;
}
编写到主函数实现替换ODR寄存器的作用
把原主函数上面的复制一份,#elif改为1,取消ODR寄存器的代码,把我们的置位函数和复位函数写入,观察PB口,即LED灯是否亮起熄灭。
#elif 1
//打开 GPIOB 端口的时钟
RCC->APB2ENR |= ((1)<<3);
//配置IO口为输出
GPIOB->CRL &= ~((0x0f)<<(4*0));
GPIOB->CRL |= ((1)<<(4*0));//*0,*1,*5
GPIO_SetBits(GPIOB,GPIO_Pin_0);
GPIO_ResetBits(GPIOB,GPIO_Pin_0);
代码展示
接下来展示主函数完整代码:
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
int main(void)
{
#if 0
//打开 GPIOB 端口的时钟
*(unsigned int*)0x40021018 |= (1<<3);
//配置IO口为输出
*(unsigned int*)0x40010c00 |= ((1)<<(4*0));//*0,*1,*5
//控制 ODR 寄存器
*(unsigned int*)0x40010c0c &= ~(1<<0);
#elif 0
//打开 GPIOB 端口的时钟
RCC_APB2ENR |= (1<<3);
//配置IO口为输出
GPIOB_CRL |= ((1)<<(4*0));//*0,*1,*5
//控制 ODR 寄存器
GPIOB_ODR &= ~(1<<0);
#elif 0
//打开 GPIOB 端口的时钟
RCC->APB2ENR |= ((1)<<3);
//配置IO口为输出
GPIOB->CRL &= ~((0x0f)<<(4*0));//*0,*1,*5
GPIOB->CRL |= ((1)<<(4*0));
//控制 ODR 寄存器
GPIOB->ODR &= ~(1<<0);
//GPIOB->ODR |= (1<<0);
#elif 1
//打开 GPIOB 端口的时钟
RCC->APB2ENR |= ((1)<<3);
//配置IO口为输出
GPIOB->CRL &= ~((0x0f)<<(4*0));
GPIOB->CRL |= ((1)<<(4*0));//*0,*1,*5
GPIO_SetBits(GPIOB,GPIO_Pin_0);
GPIO_ResetBits(GPIOB,GPIO_Pin_0);
#endif
}
void SystemInit(void)
{
//函数体为空,目的是为了骗过编译器不报错
}
代码解释
这段代码片段是用于控制STM32微控制器上的GPIO引脚的。该代码演示了使用寄存器级别访问和STM32标准外设库实现相同功能的不同方法。
在main
函数中,有四个部分被#if 0
和#elif 1
预处理指令包围。每个部分演示了控制GPIO引脚的不同方法:
- 第一部分使用直接寄存器访问通过写入内存地址来控制GPIOB引脚0。
- 第二部分使用STM32外设库提供的寄存器定义来实现相同的功能。
- 第三部分类似于第二部分,但使用箭头运算符(->)来访问结构成员,而不是点运算符。
- 第四部分使用STM32自定义外设库中的函数来设置和复位GPIOB上的GPIO引脚0。
SystemInit
函数是空的,目的是欺骗编译器不报错。
小结
本节课我们讲解了如何实现 RCC 这个外设的寄存器结构体声明,把时钟相关的代码改成寄存器结构体操作的方式,以及编写函数库(端口置位以及复位函数),包括防止重复定义框架·,具体代码以及主函数的更新还有main函数代码的解释。下节课我们学习如何更加系统的优化程序以及运用STM32官方给出的标准库。