学习板:STM32F103ZET6
本来准备先总结一下GPIO、几种输入输出模式以及配置方法、寄存器种类、时钟及分频等,但是想了想,还是算了,一步步的来,到时候用到再总结吧 ,不然前面总结了,后面用到还得回顾。
至于前面的安装库、安装软件、Keil中添加文件等,就不在总结了,因为这些根本并不需要记的,新手可以去跟着视频走一遍。学习过程中,不用每次都自己去新建工程,直接将官方给的模板拷过来,修改一下文件夹名称即可。
本博板子STM32F103ZET6共有7组IO口,每组16个,共16×7=112个,分别为:
GPIOA——>PA0、PA1、PA2…PA15
GPIOB——>PB0、PB1、PB2…PB15
.
.
.
GPIOG——>PG0、PG1、PG2、…PG15
程序中标识 | 模式 |
---|---|
GPIO_Mode_AIN | 模拟输入 |
GPIO_Mode_IN_FLOATING | 浮空输入 |
GPIO_Mode_IPD | 下拉输入 |
GPIO_Mode_IPU | 上拉输入 |
GPIO_Mode_Out_OD | 开漏输出 |
GPIO_Mode_Out_PP | 推挽输出 |
GPIO_Mode_AF_OD | 复用开漏输出 |
GPIO_Mode_AF_PP | 复用推挽输出 |
1、 GPIO_Mode_AIN :模拟输入
一般用于ADC模拟输入
2、GPIO_Mode_IN_FLOATING :浮空输入
可用于按键KEY实验、发送接收信号RX、TX等,不过这些实验可以不用浮空输入,如KEY用到上拉和下拉
3、GPIO_Mode_IPD:下拉输入
4、GPIO_Mode_IPU:上拉输入
IO内部上拉电阻、下拉电组输入,使情况而定,比如刚刚说的key按键实验,原理图如下:
可以看到KEY_UP按下后,IO口应该是3V3电平输入,使用上拉输入模式,接到GPIO的上拉电阻,芯片可以更好的检查到高电平输入。
KEY0~2按下后,IO口是低电平输入,使用下拉输入模式,接到GPIO的下拉电阻后,芯片可以更好的检查到低电平输入。
5、GPIO_Mode_Out_OD:开漏输出
IO 输出 0 接 GND,IO 输出 1,悬空,需要外接上拉电阻,才能实现输出 高电平。当输出为 1 时,IO 口的状态由上拉电阻拉高电平,但由于是开漏输出模式,这样 IO 口也就可以 由外部电路改变为低电平或不变。该模式适合做电流型的驱动,吸收电流能力比较强。
6、GPIO_Mode_Out_PP:推挽输出
可以输出高、低电平。导通损耗小、效率高。既提高电路的负载能力,又提高开关速度。广泛各种实验,比如接下来要总结的LED。
7、GPIO_Mode_AF_OD:复用开漏输出
当GPIO为复用IO时的开漏输出模式,一般用于外设功能,如TX1
8、GPIO_Mode_AF_PP:复用推挽输出
当GPIO为复用IO时的推挽输出模式,一般用于外设功能,如I2C
在学习库函数之前,应该明白,STM32F1用的是Cortex-M3芯片,是由ARM公司设计的。所以芯片的标准是由ARM公司制定的,芯片内核架构有ARM公司提供。而我们现在用的STM32由ST公司生产,所以关系是:ARM制定内核架构,ST等芯片公司根据ARM公司的标准设计了芯片。ST等公司设计的芯片,不同的是存储容量、外设、串口数量等等。
以本博的学习板STM32F103ZET6为例,固件库(库函数的集合)是由官方提供的,这个官方是ST公司,而不是正点原子官方。也就是说不仅仅这一型号单片机,ST系列其他型号的单片机库函数依旧可以适用。所以不必担心更换板子后不知如何去编程。
ST公司推出官方固件库,将底层寄存器操作都封装起来,形成一套接口(API)供我们使用,大多数情况下我们不必去考虑底层寄存器。比如本博的LED,只需调用GPIO配置函数、时钟配置函数,然后主函数初始化后,直接给引脚赋值就可以实现LED的亮灭,而不用去考虑寄存器如何工作的。当然本博会把寄存器版的LED也总结一下。毕竟想要真正理解单片机,还得去真正理解寄存器,库函数版只是让我们停留在“会使用”。当然,对应大多数人来讲,“会使用”已经完全足够了。
1、设置时钟
2、设置GPIO
只要这两步的配置,再在主函数中给对应引脚传输高低电平即可。
打开原理图文件(下图我打开了6个文件,都是需要的,而且大部分情况下,有这6个文件足以。都在板子附带资料的文件夹里)
从原理图中得到以下信息:
①DS0 LED0阳极接+,阴极接PB5;DS1 LED1阳极接+,阴极接PE5。
②SYS LED由名称“PWR”顾名思义,为电源指示灯,所以单片机接通电源后,电源指示灯常亮。
③芯片的PB5引脚软件置0后,LED0亮;PE5引脚软件置0后,LED1亮。
所以要配置GPIOB(因为PB5)和GPIOE(因为PE5)。
然后是时钟设置,只要是对GPIO操作,就必须进行时钟配置(而且时钟配置在前)。GPIO是挂载在APB2总线上的外设,所以在对GPIO的时钟进行设置时,通过函数RCC_APB2PeriphClockCmd()来实现。
下面进入实战:
打开模板文件:(时间久远了,不知道模板文件原来放哪个文件夹下,找不到的话可以把LED官方例程打开,关于LED的.c和.h文件删掉,主函数清空,就可以当以后的模板来用了,不用每次都创建工程)
首先查看GPIO配置函数,既然是GPIO,那么先找一下头文件,在main.c下找GPIO头文件,并点击进入。
找到对应函数:(下一博客总结所有GPIO函数的用法、以及延时函数)
上图标注,GPIO_Init()函数初始化,进行设置GPIO,GPIO_SetBits()函数给对应引脚置1,GPIO_ReSetBits()函数给对应引脚置0。
右键选中函数,点击【Go to Definition of …】,进入函数详细说明
可以看到,函数的形参有两个,而且都是指针。进入第一个形参“类型”
看到GPIOx指针是指向上图这个结构体的,也就是每组GPIO都包含的7个寄存器。
比如LED实验,传递GPIOB(PB5)过来后,*GPIOB就指向这七个寄存器,初始化函数就是对七个寄存器的操作,不过被库函数封装起来了,emmmm…说太多了,只要知道GPIO_Init()传过来的第一个参数表示对该组GPIO配置就行了。
察看第二组形参“类型”
看到第二个形参也是结构体指针,指向的结构体含有三个参数GPIO_Pin、GPIO_Speed、GPIO_Mode
到这里就可以用C++语法来说明了。比如第二个形参是a(注意是指针),那么:
a.GPIO_Pin=…
a.GPIO_Speed=…
a.GPIO_Mode=…
就完成了对参数GPIO参数的设置。
接下来我们再看看上面三个赋值语句的右边究竟是什么东西:
转回到初始化函数:
1表示第二个形参
2表示对 GPIO_Init()的第一个形参的处理(就是那个结构体里有7个寄存器的东西)
3表示mode的配置
4表示pin的配置
5表示速speed的配置。
点开GPIO_Mode设置函数:
可以看到就是我们第一大部分总结的8中输入输出模式
点开pin设置函数
可以看到pin是我们第一大部分总结的一组GPIO的15个IO口
点开速度设置函数
可以看到速度可设置的值:
第一步:设置形参1和形参2
第二步:上面那三个赋值语句的设置
第三步:运行GPIO_Init()函数
程序:(先在主函数中书写,.c文件中书写接下来会总结)
GPIO_TypeDef GPIO_B;//形参1
GPIO_InitTypeDef GPIO_InitStruct;//形参2
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;//形参2.mode=推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;//形参2.pin=5
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;//形参2.speed=50MHZ
GPIO_Init(&GPIO_B,&GPIO_InitStruct);
注意GPIO_Init()传递的是指针,所以应该用取地址符“&”。
上面的程序还是有问题的,因为定义了GPIO_B为第一个参数,但是程序并不知道GPIO_B是对GPIOB的操作,所以在 GPIO_Init(&GPIO_B,&GPIO_InitStruct);语句中,“&GPIO_B”应该是真正的、物理上的地址,而不能像参数2一样,只是程序定义参数时分配的地址。
输入“GPIOB”,并进入
发现官方真的定义了GPIOB,而且还是真正的、物理层的地址
所以之前程序中的GPIO_B可以删掉了,不是物理层的地址,定义了、传递给GPIO_Init()函数也没用。
正确完整程序:
//GPIO_TypeDef GPIO_B;//形参1
GPIO_InitTypeDef GPIO_InitStruct;//形参2
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;//形参2.mode=推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;//形参2.pin=5
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;//形参2.speed=50MHZ
GPIO_Init(GPIOB,&GPIO_InitStruct);
//注意GPIOB是地址!GPIO_InitStruct是指针,传递过去后的俩个实参数都是指针。
以上配置了GPIOB是为了点亮LED0,现配置LED1(PE5)的GPIO,参考上面的程序:
GPIO_InitTypeDef GPIO_InitStruct;//形参2
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;//形参2.mode=推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;//形参2.pin=5
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;//形参2.speed=50MHZ
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;
GPIO_Init(GPIOE,&GPIO_InitStruct);
因为之前设置了mode和speed,而实参2是没有指向的,即并不能知道实参2属于实参1,所以哪怕再重新定义一个GPIOE的实参2,重新定义mode和speed也没有意义,所以就可以省略了。
接下来可以将LED引脚置高电平,熄灭LED。使初始状态下LED是灭的。
int main(void)
{
GPIO_InitTypeDef GPIO_InitStruct;//形参2
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;//形参2.mode=推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;//形参2.pin=5
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;//形参2.speed=50MHZ
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;
GPIO_Init(GPIOE,&GPIO_InitStruct);
GPIO_SetBits(GPIOB, GPIO_Pin_5);
GPIO_SetBits(GPIOE, GPIO_Pin_5);
}
之前说过,GPIO是挂载在APB2总线上的外设,所以在对GPIO的时钟进行设置时,通过函数RCC_APB2PeriphClockCmd()来实现。打开RCC.h头文件,找到时钟函数
同样的方法确定形参类型
形参1:
形参2:
所以程序:(注意时钟配置函数应该放在最前面)
GPIO_InitTypeDef GPIO_InitStruct;//形参2
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB |RCC_APB2Periph_GPIOE , ENABLE);
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;//形参2.mode=推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;//形参2.pin=5
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;//形参2.speed=50MHZ
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;
GPIO_Init(GPIOE,&GPIO_InitStruct);
GPIO_SetBits(GPIOB, GPIO_Pin_5);
GPIO_SetBits(GPIOE, GPIO_Pin_5);
加入延时函数,形成流水灯:(只在Main.c文件编程)
其中GPIO_SetBits(GPIOB, GPIO_Pin_5)是将PB5引脚置1;GPIO_ReSetBits(GPIOB, GPIO_Pin_5)是将PB5引脚置0
(是通过库函数对BSRR和BRR寄存器操作完成置0置1,下一博客会涉及到)
#include "stm32f10x.h"
#include "delay.h"
int main(void)
{
GPIO_InitTypeDef GPIO_InitStruct;//形参2
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB |RCC_APB2Periph_GPIOE , ENABLE);
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;//形参2.mode=推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;//形参2.pin=5
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;//形参2.speed=50MHZ
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;
GPIO_Init(GPIOE,&GPIO_InitStruct);
GPIO_SetBits(GPIOB, GPIO_Pin_5);
GPIO_SetBits(GPIOE, GPIO_Pin_5);
delay_init(); //延时函数初始化
while(1)
{
GPIO_SetBits(GPIOB, GPIO_Pin_5);
GPIO_SetBits(GPIOE, GPIO_Pin_5);
delay_ms(1000); //注意包含头文件delay.h,这个好像是正点原子官方写的
GPIO_ResetBits(GPIOB, GPIO_Pin_5);
GPIO_ResetBits(GPIOE, GPIO_Pin_5);
delay_ms(1000);
}
}
在LED使用的工程文件夹新建一个LED文件夹
进行下一步骤:
创建一个text文件,命名为led.h,保存在LED group中
将头文件添加进来
同理。新建一个led.c文件,将.c文件也添加进来
上述步骤是创建一个LED Group,现在将头文件添加进来:
找到刚刚创建的文件夹并添加
编辑头文件:
固定格式:
#ifndef 一个未定义字符串
#define 一个未定义字符串
#include ...
#include ... //各种需要在本.h文件中用到的头文件
...
...//一些函数声明、甚至定义
#endif
本实验led.h文件可这样写:
#ifndef __LED_H //led.h文件
#define __LED_H
void LED_Init(void);//初始化
#endif
接下来编辑led.c文件
需要有本.c文件用到的头文件,如果要用到别的文件中定义的变量,可以采用外部声明重新声明该变量。在.c文件实现.h文件声明的函数
将我们之前main函数中关于GPIO配置和时钟声明的函数移植过来得到完整的LED程序:
/**led.h**/
#ifndef __LED_H //led.h文件
#define __LED_H
void LED_Init(void);//初始化
#endif
/**led.c**/
#include "led.h"
#include "stm32f10x.h"
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;//形参2
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB |RCC_APB2Periph_GPIOE , ENABLE);
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;//形参2.mode=推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;//形参2.pin=5
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;//形参2.speed=50MHZ
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;
GPIO_Init(GPIOE,&GPIO_InitStruct);
GPIO_SetBits(GPIOB, GPIO_Pin_5);
GPIO_SetBits(GPIOE, GPIO_Pin_5);
}
/**main.c**/
#include "stm32f10x.h"
#include "delay.h"
#include "led.h"
int main(void)
{
LED_Init();
delay_init(); //延时函数初始化
while(1)
{
GPIO_SetBits(GPIOB, GPIO_Pin_5);
GPIO_SetBits(GPIOE, GPIO_Pin_5);
delay_ms(1000); //注意包含头文件delay.h,这个好像是正点原子官方写的
GPIO_ResetBits(GPIOB, GPIO_Pin_5);
GPIO_ResetBits(GPIOE, GPIO_Pin_5);
delay_ms(1000);
}
}
毕竟是32系列第一个博客,还是希望详细一点,之后的实验就会慢慢省略一部分东西,寄存器版本完整来一遍
打开模板文件,如果没有,就把LED库函数例程打开,删掉led.c和led.h,当做以后所有实验的模板就行了。
一般情况下,应该【HARDWARE】中创建.c文件。也可以在文件【HARDWARE】之外建,看个人习惯。
如果懒得去搞模板,就用我这个吧
先将模板文件拷过来,在HARDWARE文件夹下创建LED文件夹
打开模板文件
新建两个text文件,保存在刚刚创建的LED文件夹下,并改名为led.c、led.h
将led.c文件添加进工程
将LED文件目录添加进来
led.h编辑,之前库函数版本讲过了,直接附代码:
#ifndef LED_H //led.h文件
#define LED_H
int LED_Init(void);
#endif
led.c文件编辑
开始还是老规矩:
#include "led.h"
#include "stm32F10x.h"
int LED_Init(void)
{
}
打开《STM32中文参考手册》7.3.7 APB2外设时钟使能寄存器(RCC_APB2ENR)
寄存器下一博客总结,现在只需知道,APB2外设时钟使能寄存器的第3位和第6位分别对应GPIOB(LED0、DS0)和GPIOE(LED1、DS1)
时钟使能代码如下:
RCC->APB2ENR|=1<<3;
RCC->APB2ENR|=1<<6;
解释一下:
RCC->APB2ENR|=1的意思是:RCC->APB2ENR=RCC->APB2ENR|0x00000001(32位寄存器),也就是说将该寄存器的第0位软件置1,其它位保持不变。"<<3"是将刚刚设置的那个第0位的1左移3位,也就是此时第3位为1.同理“<<6”是将第6位设置为1;此时就使能了GPIOB和GPIOE的时钟
用到端口配置寄存器,由于是对PB5、PE5的配置,是低位IO口(Px0~Px7是低位、Px8 ~Px15是高位),所以用到端口配置低寄存器GPIOx_CRL
看到第21、20位控制模式和速度,为50M输出,所以这两位是11;23 、22控制哪种输出,为推挽输出,所以这两位为00,所以GPIOx_CRL的状态值为:0x00300000;程序如下:
GPIOB->CRL&=0xff0fffff; //PB5
GPIOB->CRL|=0x00300000;
GPIOE->CRL&=0xff0fffff; //PE5
GPIOE->CRL|=0x00300000;
解释一下:
GPIOB->CRL&=0xff0fffff 是将GPIOB的20、21、22、23这四位置0,其它位保持不变;
GPIOB->CRL|=0x00300000是将GPIOB的20、21、22、23这四位置1,其它位保持不变;
此时配置好了,然后可以给IO口赋初值,如开始时让LED处于熄灭状态,则PB5、PE5均置1。用到的寄存器:端口输出数据寄存器GPIOx_ODR
代码:
GPIOB->ODR|=1<<5;
GPIOE->ODR|=1<<5;
led.c文件完整代码:
#include "led.h" //led.c文件
#include "stm32F10x.h"
int LED_Init(void)
{
RCC->APB2ENR|=1<<3;
RCC->APB2ENR|=1<<6;
GPIOB->CRL&=0xff0fffff; //PB5
GPIOB->CRL|=0x00300000;
GPIOE->CRL&=0xff0fffff; //PE5
GPIOE->CRL|=0x00300000;
GPIOB->ODR|=1<<5;
GPIOE->ODR|=1<<5;
}
进入主函数后,首先应调用刚刚写的LED初始化函数,完成GPIO配置;程序会用到延时函数,将延时函数也初始化,代码:(头文件包含led.h)
#include "sys.h"
#include "delay.h"
#include "led.h"
int main(void)
{
LED_Init();
delay_init();
}
然后在死循环中,对PB5和PE5 IO口赋值就行了,还是用到端口输出数据寄存器GPIOx_ODR
不过给IO口置0时,需要注意,应该和0xffffffdf进行与运算
GPIOB->ODR&=0xffffffdf;//置0
GPIOE->ODR&=0xffffffdf;//置0
或者移位运算,将第0位置0再向左移5位
GPIOB->ODR&=0xfffffffe<<5;
GPIOE->ODR&=0xfffffffe<<5;
给IO口置1就和0x00000020进行或运算
GPIOB->ODR|=0x00000020;
GPIOE->ODR|=0x00000020;
或者直接位移运算,先和0x00000001进行或运算,使第0位置1,再将第0位向左移动5,代码:
GPIOB->ODR|=1<<5;
GPIOE->ODR|=1<<5;
main.c文件完整程序:
#include "sys.h" //main.c文件
#include "delay.h"
#include "led.h"
int main(void)
{
LED_Init();
delay_init();
while(1)
{
GPIOB->ODR&=0xffffffdf;//置0
GPIOE->ODR&=0xffffffdf;//置0
//GPIOB->ODR&=0xfffffffe<<5;
//GPIOE->ODR&=0xfffffffe<<5;
delay_ms(1000);
GPIOB->ODR|=0x00000020;
GPIOE->ODR|=0x00000020;
//GPIOB->ODR|=1<<5;
//GPIOE->ODR|=1<<5;
delay_ms(1000);
}
}
打开sys.h头文件
点亮熄灭DS0、DS1,只需:
PBout(5)=0;//点亮
PEout(5)=0;//点亮
delay_ms(1000);
PBout(5)=1;//熄灭
PEout(5)=1;//熄灭
delay_ms(1000);
或者定义:
#define LED0 PBout(5)
#define LED1 PEout(5)
LED0=0;//点亮
LED1=0;//点亮
delay_ms(1000);
LED0=1;//熄灭
LED1=1;//熄灭
delay_ms(1000);