这里写目录标题
前言
本文章主要讲解如何使用STM32F103系列单片机点亮LED,从直接驱动相应内部寄存器到一步一步封装成库函数。
1. 需驱动的寄存器及所在地址
1-1. RCC的所在地址
1-2. APB2时钟使能寄存器
1-3. GPIOA的所在地址
1-4. GPIO端口相应的寄存器
该图是从STM32F103参考手册剪辑过来的,目前只显示用到的寄存器及它们的地址。具体内容以STM32F103参考手册为主。
2. 直接寄存器配置
硬件只需一个LED灯,由GPIOA_Pin_0引脚控制。GPIOA的驱动流程请见下面文章。
2.1驱动流程
(1)驱动APB2时钟使能寄存器开启GPIOA的时钟。
GPIOA;
/* GPIOA的时钟 */
*(unsigned int*)(40021000) |= (1<<2);
(2)配置GPIOA寄存器组中的CRL(GPIO低8位控制寄存器),配置参数为:输出模式为推挽式输出,输出速度为50MH。
CRL低8位寄存器四位控制一位引脚。
/* 清除对应GPIOA端口第0引脚 */
*(unsigned int*)(40010800) &= ~(0x0F<<(4*0));
设置工作模式为:推挽式输出,输出速度为:50MHz。
/* CRL_MODEx =3; CNFx =0; */
*(unsigned int*)(40010800) |= (3<<(4*0));
(3)控制相应的GPIOA端口第0引脚输出高低电平,需控制的GPIO寄存器有BSRR,BRR,ODR,可选其中一个。
/* 用BSRR寄存器控制PA0引脚输出高电平 */
*(unsigned int*)(40010800+0x10) |= (1<<(0+0));
/* 用BSRR寄存器控制PA0引脚输出低电平 */
*(unsigned int*)(40010800+0x10) |= (1<<(16+0));
/* 用BRR寄存器控制PA0引脚输出低电平 */
*(unsigned int*)(40010800+0x14) |= (1<<(0+0));
/* 用ODR寄存器控制PA0引脚输出高电平 */
*(unsigned int*)(40010800+0x0C) |= (1<<(0+0));
/* 用ODR寄存器控制PA0引脚输出低电平 */
*(unsigned int*)(40010800+0x0C) |=(0<<(0+0));
先找到外设的基础地址如RCC的0x40021000,GPIOA的40010800,(在STM32F103系列参考手册存储器映像中),之后在加上相关的寄存器偏移地址,具体请查阅STM32F103系列参考手册中的寄存器介绍。确定寄存器所在地址后强制转换为 unsigned int *(无符号整型变量指针)再用间接运算符,取该地址的值以便之后参与运算。
函数的实现部分:
void main()
{
/* GPIOA的时钟 */
(unsigned int*)(40021000) |= (1<<2);
/* 清除对应GPIOA端口第0引脚 */
*(unsigned int*)(40010800) &= ~(0x0F<<(4*0));
/* CRL_MODEx =3; CNFx =0; */
*(unsigned int*)(40010800) |= (3<<(4*0));
/* 用BSRR寄存器控制PA0引脚输出高电平 */
// *(unsigned int*)(40010800+0x10) |= (1<<(0+0));
/* 用BSRR寄存器控制PA0引脚输出低电平 */
// *(unsigned int*)(40010800+0x10) |= (1<<(16+0));
/* 用BRR寄存器控制PA0引脚输出低电平 */
// *(unsigned int*)(40010800+0x14) |= (1<<(0+0));
/* 用ODR寄存器控制PA0引脚输出高电平 */
*(unsigned int*)(40010800+0x0C) |= (1<<(0+0));
/* 用ODR寄存器控制PA0引脚输出低电平 */
*(unsigned int*)(40010800+0x0C) |=(0<<(0+0));
}
void SystemInit(void)
{
/* 时钟配置函数,在这里不编写任何东西,主要目的是使编译器不报错
也可以把启动文件 straup_stm32f10x_.s中的
SystemInit 注释掉 ;相当于C语言中的//
在STM32启动时会进入启动文件中执行以下参数:
1.初始化栈堆指针
2.初始化程序指针
3.初始化中断向量表
4.配置系统时钟
5.调用C库函数_main()初始化用户程序指针
6.进入C语言main()主函数中
*/
}
3. 固件库开发
3.1 前言
在前面讲解了直接面向寄存器开发,我想读者已经注意到了一个非常重要的问题。在那一串一串的16进制数中如果不查阅相关的手册,里面有什么,具体叫什么,实现什么功能我们根本不知道。所以这样的程序可读性,维护性上都不高效。但是这样的程序在编译运行上比用固件库编写出的程序还要快。不过随着STM32F103时钟频率越来越高、寄存器越来越多,底层编程已经显的很吃力。我们宁愿放弃这一点速度,换取可读性,可维护性上。在使用固件库开发的路上,用户无需了解函数是如何实现你想要的功能的,像C语言标准库中的格式输出函数 printf(),用户只需如何使用该函数即可。在官方库未完善时很多用户觉得使用固件库开发简直是在用浮沙筑高楼,完全脱离底层。不过随着官方固件库渐渐完善,用户也慢慢开始接受。在使用固件库开发时你可以把库层层揭开,你将看到固件库编写者在里面留下的智慧结晶。配合相关手册阅读你将被它的所魅力吸引。分析库可使你更了解底层以及寄存器的驱动方式。下面我将带读者从寄存器向上一步一步构建库,带读者了解库是如何形成的。
3.2 封装(映像)
把各个外设中的地址用C语言中的宏定义(预处理命令中的一种)给各地址起别名,此过成称为地址映像或寄存器映像。以后使用这些宏名就可达到操作地址的目的。下面就是用宏封装的地址。
/* 外设总线的地址*/
#define PERPH_BASE 0x40000000
/* 相对总线的外设基地址 */
#define APB1_PERPH_BASE PERPH_BASE
/* 高速72MHzz总线地址 */
#define APB2_PERPH_BASE (PERPH_BASE + 0x10000)
/* 系统总线地址 */
#define AHB_PERPH_BASE (PERPH_BASE + 0x20000)
/* GPIOA寄存器的所在地址 */
#define GPIOA_BASE (APB2_PERPH_BASE + 0x0800)
/* 时钟RCC地址 */
#define RCC_BASE *(unsigned int*)(AHB_PERPH_BASE + 0x1000)
/* 低8位控制寄存器的地址 */
#define GPIOA_CRL *(unsigned int*)(GPIOA_BASE + 0x00)
/* 设置/清除寄存器的地址 */
#define GPIOA_BSRR *(unsigned int*)(GPIOA_BASE + 0x10)
/* 清除寄存器的地址 */
#define GPIOA_BRR *(unsigned int*)(GPIOA_BASE + 0x14)
/* 数据输出寄存器的地址 */
#define GPIOA_ODR *(unsigned int*)(GPIOA_BASE + 0x0C)
之后把main()函数中的部分内容修改为如下所示
void main()
{
/* GPIOA的时钟 */
RCC_BASE |= (1<<2);
/* 清除对应GPIOA端口第0引脚 */
GPIOA_CRL &= ~(0x0F<<(4*0));
/* CRL_MODEx =3; CNFx =0; */
GPIOA_CRL |= (3<<(4*0));
/* 用BSRR寄存器控制PA0引脚输出高电平 */
// GPIOA_BSRR |= (1<<(0+0));
/* 用BSRR寄存器控制PA0引脚输出低电平 */
// GPIOA_BSRR |= (1<<(16+0));
/* 用BRR寄存器控制PA0引脚输出低电平 */
// GPIOA_BRR |= (1<<0);
/* 用ODR寄存器控制PA0引脚输出高电平 */
GPIOA_ODR |= (1<<0);
/* 用ODR寄存器控制PA0引脚输出低电平 */
GPIOA_ODR |=(0<<0);
}
void SystemInit(void)
{
/* 时钟配置函数,在这里不编写任何东西,主要目的是使编译器不报错
也可以把启动文件 straup_stm32f10x_.s中的
SystemInit 注释掉 ;相当于C语言中的//
在STM32启动时会进入启动文件中执行以下参数:
1.初始化栈堆指针
2.初始化程序指针
3.初始化中断向量表
4.配置系统时钟
5.调用C库函数_main()初始化用户程序指针
6.进入C语言main()主函数中
*/
}
看到这样的程序是不是一下就知道具体是实现什么功能了。这里我们把整型指针也放到了里面去,在使用时只需调用该宏名即可。现在用户不会再看到一串一串16进制数而感到头皮发麻。
但是现在又出现了一个问题,像STM32F103的外设这么多,许多外设的寄存器都是一样的。如GPIOx(x=A~G)它们的寄存器都是一样的,如果这样编写下去宏名就已经泛滥成灾了。不过C语言提供了一个非常好的解决方法,结构体指针类型。
3.3 封装(结构体)
在STM32F103中许多外设的寄存器都是一样的,如果单单用宏来封装,重复编写的工作量会变得很大,可读性也会慢慢降低。在这里就介绍如何使用结构体来封装重复的寄存器所在地址。
学过结构体的用户都知道结构体成员在存储器中所占空间是连续的或有一些偏移地址(对齐)。而STM32中的各个寄存器所占4个字节(32位),这样就可以定义结构体成员为 unsigned int ,因为整型变量在这占4个字节。成员与成员之间的偏移地址也为4个字节,符合外设寄存器之间的偏移地址。定义结构体成员时,外设有多少个寄存器就定义多少个结构体。如下定义GPIO端口结构体所示
/* GPIO端口寄存器成员 */
typedef struct {
unsigned int CRL;
unsigned int CRH;
unsigned int ODR;
unsigned int IDR;
unsigned int BSRR;
unsigned int BRR;
unsigned int LCKR;
} GPIO_TypeDef;
之后再把该结构体与外设地址关联起来,如下所示
/* GPIOA_BASE = 0x40010800 */
#define GPIOA ((GPIO_TypeDef*)(GPIOA_BASE))
修改程序如下所示
/* GPIOA的时钟 */
RCC_BASE |= (1<<2);
/* 清除对应GPIOA端口第0引脚 */
GPIOA->CRL &= ~(0x0F<<(4*0));
/* CRL_MODEx =3; CNFx =0; */
GPIOA->CRL |= (3<<(4*0));
/* 用BSRR寄存器控制PA0引脚输出高电平 */
// GPIOA->BSRR |= (1<<(0+0));
/* 用BSRR寄存器控制PA0引脚输出低电平 */
// GPIOA->BSRR |= (1<<(16+0));
/* 用BRR寄存器控制PA0引脚输出低电平 */
// GPIOA->BRR |= (1<<0);
/* 用ODR寄存器控制PA0引脚输出高电平 */
GPIOA->ODR |= (1<<0);
/* 用ODR寄存器控制PA0引脚输出低电平 */
GPIOA->ODR |=(0<<0);
不过我看不出有什么区别呀,不过就是把下斜杆 _ 换成指向运算符 -> 而已。但是如果要驱动的外设多了缺点就会越来越明显,大量意义相同的宏定义看着头皮发麻。
现在明显有点像库的感觉了,可是好像还差了点什么。认真看一下写入寄存器的值是用位运算符计算出值送入的,但是要知道计算出的值是否符合想要送入的值,这还是要查询关于寄存器的介绍手册才行。
我们再定义一个包含三个成员的结构体变量用来设置GPIOA的工作环境。成员具体实现的功能有:指定 1.GPIO端口引脚,1. 工作模式,3.输出速度,如下所示
/* GPIO端口参数设置结构体 */
typedef struct {
unsigned short GPIO_Pin; /* GPIO口*/
unsigned short GPIO_Speed; /* 输出速度*/
unsigned short GPIO_Mode; /* 工作模式*/
} GPIO_InitTypeDef;
/* CRL/CRH 输出速度定义 */
typedef enum {
GPIO_Speed_10MHz = 1,
GPIO_Speed_2MHz ,
GPIO_Speed_50MHz
} GPIOSpeed_TypeDef;
/* CRL/CRH 工作模式选择 */
typedef enum {
/* 输入模式 */
GPIO_Mode_AIN = 0X0, /* 模拟*/
GPIO_Mode_IN_FLOATING = 0X04, /* 浮空*/
GPIO_Mode_IPD = 0X28, /* 下拉*/
GPIO_Mode_IPU = 0X48, /* 上拉*/
/* 输出模式 */
GPIO_Mode_Out_OD = 0X14, /* 开漏*/
GPIO_Mode_Out_PP = 0X10, /* 推挽*/
GPIO_Mode_AF_OD = 0X1C, /* 复用开漏*/
GPIO_Mode_AF_PP = 0X18 /* 复用推挽*/
} GPIOMode_TypeDef;
/* GPIO端口引脚定义 */
#define GPIO_Pin_0 ((uint16_t) 0x0001) /* 1<<0 */
#define GPIO_Pin_1 ((uint16_t) 0x0002) /* 1<<1 */
#define GPIO_Pin_2 ((uint16_t) 0x0004) /* 1<<2 */
#define GPIO_Pin_3 ((uint16_t) 0x0008) /* 1<<3 */
#define GPIO_Pin_4 ((uint16_t) 0x0010)
在初始化结构体的同时还定义了两个枚举类型,为GPIO端口配置工作模式及输出速度。和一些宏,具体对应GPIO端口的引脚。它们配置完参数时还需调用一个GPIO端口初始化函数 GPIO_Init(GPIO_TypeDef * ,GPIO_InitTypeDef *),由该函数把参数写入到对应的寄存器中。
该函数的写入过程:
(1)判断是否为输出模式(根据工作模式参数中的第4位来判断,0为输入,1为输出),如为1 则把输出速度加入进去。
(2)寻找需驱动的引脚,找到则把参数写入到控制该引脚的配置寄存器CRL/CRH中(4位控制一个引脚)。
(3)判断输入模式如果是下拉输入则驱动,清除位寄存器(BRR)。如果为上拉输入则驱动,设置/清除寄存器(BSRR)低16位。具体该函数的实现部分请查看官方库函数中的 GPIO_Init(GPIO_TypeDef * ,GPIO_InitTypeDef)。
函数如下所示
/* 先前编写的一些宏定义,结构体,枚举类型都在这个头文件中 */
#include “stm32f10x.h”
/* 延迟函数 */
void LED_Delay(unsigned int dat);
void main()
{
GPIO_InitTypeDef GPIO_InitStructure ;
/* 开启GPIO_A的时钟 */
RCC_APB2ENR |= (1<<2);
/* 选择GPIO_0 端口 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 ;
/* 工作模式为 推挽式输出 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ;
/* 输出速度为 10MHz */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz ;
/* 初始化GPIOA端口 */
GPIO_Init(GPIOA , &GPIO_InitStructure);
while(1)
{
/* 高电平 */
GPIOA->BSRR = GPIO_Pin_0;
LED_Delay(unsigned int dat)
/* 低电平 */
GPIOA->BRR = GPIO_Pin_0;
LED_Delay(unsigned int dat)
}
}
void LED_Delay(unsigned int dat)
{
for(; dat>0; dat--);
}
4 小结
(1)带读者熟悉一些需驱动的寄存器如APB2时钟配置寄存器,CRL/CRH端口配置寄存
器,ODR输出寄存器,BRR清除寄存器,设置/清除寄存器。
(2)面向寄存器直接配置参数,带读者了解它的优缺点。
(3)用宏封装寄存器的所在地址。
(4)用结构体定义需配置的寄存器参数,最后调用GPIO端口初始化函数
void GPIO_Init(GPIO_Typedf *GPIOx , GPIO_InitTypedf *InitStruct);