概述
第一、二章节中,STM32是纯裸开发,通过自定义地址来进行写寄存器;STM32其实提供了底层固件库,定义好了通用功能,所以如果是常规功能只需要调用固件库的API即可实现功能。所以我在番外篇说了,其实熬过了前两章,后面的内容反而要简单。
从本章开始,我们的绝大多数的开发内容都是基于STM32的固件库进行的。
从main函数说起
用c编写函数,都知道入口函数是main函数,程序跑起来一定会找main函数;所以我们的编译器在编译的时候还会做强制的main函数重复检测,避免定义多个main函数执行的时候导致不可预知的结果;
但是,为什么选择的是main呢?我们觉得理所当然,其实有人替你负重前行,如果你做gcc编译的c代码,然后在Linux/ Windows上面执行,能够直接跑进main函数是因为底层的操作系统定义了可执行规范(Windows是COFF规范,而Linux则是ELF规范),如果你做过Java,通过java -jar可以运行可执行jar文件里面main函数,则是因为JVM规范里面有定义规则;
但是对于STM32的板子而言,没有JVM,更没有操作系统,有的只是CPU的操作指令;而可以直接让CPU执行的是机器码;可以直接编译为机器码的是汇编语言;所以,定义相应的规范汇编代码是最佳的选择,在STM32固件库里面,第一个就是封装定义各种底层规则的汇编代码:startup_stm32f10x_md.S(汇编文件一般都是.S结尾的);
不需要逐行理解.S文件,你只需要关注几个地方即可,后面我们会逐渐展开。本节你需要知道的为什么会是main函数被选择:
... ...
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
... ...
.S文件可以直接编译为机器码执行,而且是从头到尾顺序执行的,除非有goto语句;强调这一点是要说.S文件执行规则不是找main函数,就是从头一条一条的执行。这里面你将会看到IMPORT __main的语句,这里就是定义了入口函数地址,当main.c被编译之后,将在到内存里面,所有的函数其实是地址的索引,这里import __main其实就是记录一下main函数的地址,当.S文件执行完毕后(环境初始化工作完成了),将会执行main函数地址后面的代码语句。
小贴士
IMPORT main之后就是IMPORT SystemInit,这里需要注意,如果你没有引用STM32官方固件库之前(一堆.c,.h文件),示例代码3行到5行是需要被注释的,因为这里IMPORT的函数地址SystemInit是固件库的函数,没有引用固件库就没有编译文件,没有编译文件就不会加载到内存里面,这里IMPORT其实是找不到函数地址的,于是执行到这里就会发生异常;
所以,在没有引入固件库之前不要IMPORT SystemInit;如果引入了固件库之后,在放开System_Init,可以节省你的很多工作量,比如RCC的配置,默认情况其实已经很好了,就不需要你在手工对各个寄存器进行配置了。
基于固件库的LED点亮
代码一览
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
void delay(unsigned int time)
{
unsigned int i=0;
while(time--)
{
i=1000000;
while(i--) ;
}
}
int main(void)
{
GPIO_InitTypeDef led;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
led.GPIO_Pin = GPIO_Pin_13;
led.GPIO_Mode = GPIO_Mode_Out_PP;
led.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &led);
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
while (1)
{
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
delay(1);
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
delay(1);
}
}
只有这些;是的,你没有看错,如果引入了固件库,实现前面两章的内容只需要这30多行代码;
定义GPIO初始化结构体
第一行代码是定义GPIO初始化所有的结构体
GPIO_InitTypeDef led;
后面你可以以着属性赋值的方式来设置寄存器;
小贴士
对于变量的声明需要放在一个函数体的最前面,如果把16行代码和18行代码交换,使用Keil5的ARM编译器编译会报错的,所以对于结构体的声明要放在前面:
使能APB总线时钟
第二行代码则是使能APB总线时钟,
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
有了前面两章基础,你在看这句代码会不会更加有画面感?
1.通过查手册,进行地址的计算;
2.通过查手册确认RCC_APB2ENR所在章节;
3.查看寄存器里面定义,确认IOPC_EN位置;
4.通过位运算来为寄存器赋值。
GPIO配置
接下来就是做GPIO相关配置:
led.GPIO_Pin = GPIO_Pin_13;
led.GPIO_Mode = GPIO_Mode_Out_PP;
led.GPIO_Speed = GPIO_Speed_50MHz;
引脚配置
是配置目标引脚的编号是13(GPIOC_13);
输出模式配置
然后是配置GPIO的模式,类型为PP,是Push Pull,中文直译是推拉,不过他的学名叫推挽,挽,就是拉的意思,手挽手。这里配置的是输出模式,为什么要配置输出模式,要明白PC13和VCC3.3是在LED灯的两端,两端所谓的电压差其实都是输出才会形成电压差;所以这里要配置PC13为输出;
在STM32里面,GPIO的输出模式有两种,一种是开漏,另外一种就是推挽;对于推挽和开漏的理解要从电子电路的角度来理解,这里就不做详细解释,你只要记住两者的应用场景即可,推挽用于需要高速/频繁切换开关的场景,类似于开关,可以直接软件层面配置,直接输出高电平和低电平;在我们这里要实现的呼吸灯,PC13电平是在高低电平之间做切换的,所以推挽模式是适合的;
而开漏,应为是open drain,drain是排水,外流的意思,所以open drain就开漏,可以理解为开闸放水,为什么这么比喻呢?开漏输出模式一般用于采集多个引脚状态,通过与逻辑运算了计算结果来指导电路行为,例如在I2C总线,当PIN_A,PIN_B以及PIN_C任意一个是低电平,“与运算”结果就是0,开漏输出逻辑值就是0,即可判断总线处于占用状态。
所以,大多数场景输出都是推挽模式。
频率配置
最后一个配置是GPIO的频率,这里选择的是50MHz;这里频率大小是要看应用频率,按照奎斯特定理,采样率是要高于信号频率2倍以上的,在实际的操作过程中,一般采用5~10倍;所以引脚(采样)频率设置高是没有问题,但是低可能会有反应延迟问题;不过采样频率过高也有一个能耗问题;
我们此次应用是呼吸灯,每秒亮灭切换1次,即两个周期,所以周期=1/2;频率是周期(时钟)的倒数,所以有2/1 =2 ;所以时钟理论只要大于2Hz即可,为了确保可靠,一般需要设置为20Hz;50MHz远远超过了20Hz范围内的,不过配置再小一些没有问题。
我们可以看一下stm32固件库提供的几种频率:
typedef enum
{
GPIO_Speed_10MHz = 1,
GPIO_Speed_2MHz,
GPIO_Speed_50MHz
} GPIOSpeed_TypeDef;
从大到小依次是50MHz,10MHz以及2MHz;
GPIO配置生效
上面做的仅是记录对于GPIO的配置,下面代码则是将配置生效:
GPIO_Init(GPIOC, &led);
这个初始化函数将会执行对于相应的寄存器的位的设置;
通过参数可以看到明确了要初始化的PC系列引脚,具体的配置信息则是在led结构体中;注意这里传递是led的地址;为什么呢?因为人家函数定义的参数类型是指针类型,注意最后一个参数GPIO_InitStruct的类型是GPIO_InitTypeDef(注意类型最后有一个“”,这个*就代表指针类型):
// stm32f10x_gpio.c
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
... ...
}
为什么要定义为指针类型呢?因为节省空间,因为函数的参数分配内存空间是在栈上面,而内存栈空间一般都是比较小;如果直接复杂度数据类型作为函数参数会占用比较大的栈空间,甚至可能导致栈空间内存溢出,但是如果类型是指针,那么传递就是地址,最多占用的就是32bit(STM32地址占位32bit),即4个字节,比较节省空间,所以如果要在函数间传递复杂的数据类型,比如结构体,联合体等都是设置为指针类型。
呼吸灯效果
接着while内部实现的则是呼吸灯的效果:
while (1)
{
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
delay(1);
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
delay(1);
}
GPIO_WriteBit函数用于向设置指定引脚的高低电平,通过函数的参数指定了要给PC13引脚高电平(Bit_SET)或者低电平(Bit_RESET);怎么知道Bit_SET是代表高电平还是低电平?查看他的定义:
// stm32f10x_gpio.c
typedef enum
{
Bit_RESET = 0,
Bit_SET
}BitAction;
Bit_RESET值是0,代表低电平;默认情况下,由c语言中enum规则,后续的元素依次+1,所以Bit_SET值就是1,代表高电平。