STM32花式流水灯
前言
本文是简要介绍一下不同方式实现流水灯,比较不同方式下的异同。
以STM32最小系统核心板(STM32F103C8T6)+面板板+3只红绿蓝LED 搭建电路,使用GPIOA、GPIOB、GPIOC这3个端口控制LED灯,轮流闪烁,间隔时长1秒。
在这里我们采用GPIOA_Pin_12、GPIOB_Pin_1、GPIOC_Pin_14分别控制红、绿、蓝LED灯。
一、固件库流水灯
(一)新建工程
因为要使用ST固件库,这部分配置较为繁琐,具体可参考网上一些配置的流程
我配置的工程文件模板以及参考手册也放在后面,可供参考
(二)配置GPIO端口
GPIO端口的初始化设置三步骤
- 时钟配置
- 输入输出模式设置
- 最大速率设置
下面让我们跟着这个步骤开始配置GPIOB_Pin_1端口
stm32提供了一个用c语言封装好的固件库,我们要实现什么功能,直接调用相应的库函数即可。
- 配置时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //打开外设GPIOB的时钟
- GPIO初始化结构体
库函数中提供了一个结构体来配置GPIO端口的 输入输出模式设置 、 最大速率设置等
如下
typedef struct
{
uint16_t GPIO_Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins_define */
GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIOSpeed_TypeDef */
GPIOMode_TypeDef GPIO_Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIOMode_TypeDef */
}GPIO_InitTypeDef;
- 配置为通用推挽输出、输出速度为2M
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP; //输出模式为通用推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_1 ; //选定端口为GPIO_Pin_1
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_2MHz; //输出速度为2M
GPIO_Init(GPIOB,&GPIO_InitStruct);
至此一个GPIOB_Pin_1配置完毕。
由于这部分代码重复性较高,可自己编写函数封装上诉过程实现代码简洁性。
所以关于流水灯的有关函数我放在led.c
中并在led.h
中声明 。
- led.h函数
#ifndef _LED_H
#define _LED_H
#include "stm32f10x.h"
void LED_R_TOGGLE(void);
void LED_G_TOGGLE(void);
void LED_Y_TOGGLE(void);
void LED_Init(void);
#endif
- led.c函数
#include "led.h"
#include "delay.h"
void LED_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC,ENABLE); //打开外设GPIOA、GPIOB、GPIOC的时钟
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP; //输出模式为通用推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_12 ; //选定端口为GPIOA_Pin_12
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_2MHz; //输出速度为2M
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP; //输出模式为通用推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_1 ; //选定端口为GPIOB_Pin_1
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_2MHz; //输出速度为2M
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP; //输出模式为通用推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_14 ; //选定端口为GPIOC_Pin_14
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_2MHz; //输出速度为2M
GPIO_Init(GPIOC,&GPIO_InitStruct);
}
void LED_R_TOGGLE(void)
{
GPIO_SetBits(GPIOA, GPIO_Pin_12);
delay_ms(500);
GPIO_ResetBits(GPIOA,GPIO_Pin_12);
}
void LED_G_TOGGLE(void)
{
GPIO_SetBits(GPIOB, GPIO_Pin_1);
delay_ms(500);
GPIO_ResetBits(GPIOB,GPIO_Pin_1);
}
void LED_Y_TOGGLE(void)
{
GPIO_SetBits(GPIOC, GPIO_Pin_14);
delay_ms(500);
GPIO_ResetBits(GPIOC,GPIO_Pin_14);
}
(三)完善工程及搭建电路
- main.c函数
#include "stm32f10x.h"
#include "delay.h"
#include "led.h"
int main(void)
{
LED_Init(); //LED初始化
delay_init(); //延时初始化
while(1)
{
LED_R_TOGGLE(); //红灯闪烁
delay_ms(500);
LED_G_TOGGLE(); //绿灯闪烁
delay_ms(500);
LED_Y_TOGGLE(); //黄灯闪烁
delay_ms(500);
}
}
- 其他函数
GPIO端口配置完成之后,我们就能控制LED点亮点灭了,但是如果我们需要实现精确延时1s后LED灯闪烁,我们就需要添加延时函数,这部分系统中断,在这里我调用的是正点原子写好的延时函数,就不过多介绍了。
在这里也可以采用软件延时(循环操作时不进行任何操作),缺点是不能实现精确延时、且运行效率低。
- 搭建电路
本次我们在面包板上使用c8t6控制红、绿、黄三个灯轮流闪烁,所搭建的电路图如下。
关于面包板的基本简介,可参考
- 接线
USB转TTL模块GND、3v3与最小系统板的地和3v3相连
USB转TTL模块的TXD接最小系统板的PA10
USB转TTL模块的RXD接最小系统板的PA9
引出最小系统板的PA12、PB1、PC14分别接红、黄、绿LED灯的正极,LED灯的负极接地。
- 下载hex文件
这里有几点需要注意
关于BOOT0、BOOT1引脚的不同组合方式有多种下载方式
关于使用串口下载,可参考
在下载完成时,保持最小系统板不断电的情况下,将boot0引脚从电源接到地,下载的程序就转移到存储器中,断电也可保存程序,上电之后可直接运行,不然程序被保存到内存中,断电之后就丢失了。
最终的运行效果放在最后面了。
二、寄存器流水灯
(一)寄存器映射
1.学会查找寄存器地址
以GPIOB->CRL寄存器为例,在stm32f103x中文参考手册中查找寄存器地址
-
找到GPIOB端口的起始地址
可以看到,GPIOB端口起始地址为0X4001 0C00
-
找到GPIO寄存器中的端口配置低寄存器(GPIOx_CRL)
查询得到该寄存器偏移地址为0x00
,所以可以得出GPIOB->CRL的地址为(GPIOB端口的起始地址+偏移地址)0X4001 0C00
+0x00
=0X4001 0C00
2.写入其他寄存器地址
重复上述步骤,查找其他所需寄存器的地址
/*RCC外设基地址*/
#define RCC_BASE (unsigned int)(0x40021000)
/*RCC的AHB1时钟使能寄存器地址,强制转换成指针*/
#define RCC_APB2ENR *(unsigned int*)(RCC_BASE+0x18)
/*GPIOB外设基地址*/
#define GPIOB_BASE (unsigned int)(0X40010C00)
/* GPIOB寄存器地址,强制转换成指针 */
#define GPIOB_CRL *(unsigned int*)(GPIOB_BASE+0x00)
#define GPIOB_CRH *(unsigned int*)(GPIOB_BASE+0x04)
#define GPIOB_IDR *(unsigned int*)(GPIOB_BASE+0x08)
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
#define GPIOB_BSRR *(unsigned int*)(GPIOB_BASE+0x10)
#define GPIOB_BRR *(unsigned int*)(GPIOB_BASE+0x14)
#define GPIOB_LCKR *(unsigned int*)(GPIOB_BASE+0x18)
/*GPIOC外设基地址*/
#define GPIOC_BASE (unsigned int)(0x40011000)
/* GPIOC寄存器地址,强制转换成指针 */
#define GPIOC_CRL *(unsigned int*)(GPIOC_BASE+0x00)
#define GPIOC_CRH *(unsigned int*)(GPIOC_BASE+0x04)
#define GPIOC_IDR *(unsigned int*)(GPIOC_BASE+0x08)
#define GPIOC_ODR *(unsigned int*)(GPIOC_BASE+0x0C)
#define GPIOC_BSRR *(unsigned int*)(GPIOC_BASE+0x10)
#define GPIOC_BRR *(unsigned int*)(GPIOC_BASE+0x14)
#define GPIOC_LCKR *(unsigned int*)(GPIOC_BASE+0x18)
/*GPIOC外设基地址*/
#define GPIOD_BASE (unsigned int)(0x40011400)
/* GPIOC寄存器地址,强制转换成指针 */
#define GPIOD_CRL *(unsigned int*)(GPIOD_BASE+0x00)
#define GPIOD_CRH *(unsigned int*)(GPIOD_BASE+0x04)
#define GPIOD_IDR *(unsigned int*)(GPIOD_BASE+0x08)
#define GPIOD_ODR *(unsigned int*)(GPIOD_BASE+0x0C)
#define GPIOD_BSRR *(unsigned int*)(GPIOD_BASE+0x10)
#define GPIOD_BRR *(unsigned int*)(GPIOD_BASE+0x14)
#define GPIOD_LCKR *(unsigned int*)(GPIOD_BASE+0x18)
以上包括本次实验所涉及的所有寄存器,在这里使用宏定义是为了使代码具有较好的可读性,将寄存器名宏定义为该寄存器对应的地址,不然一直对着地址操作,到了后来,自己都不知道在操作哪个寄存器了。
(二)寄存器的作用
既然要涉及到寄存器编程,那么我们了解要用到的相关寄存器的作用就挺重要的了。
以配置GPIOB_Pin_1为通用推挽输出模式为例
- APB2 外设时钟使能寄存器(RCC_APB2ENR)
由于 STM32的外设很多,为了降低功耗,每个外设都对应着一个时钟,在芯片刚上电的时候这些时钟都是被关闭的,如果想要外设工作,必须把相应的时钟打开。
该寄存器的作用是控制所有挂载到APB2总线上外设时钟的开关,每使用一个外设,我们都要打开对应的时钟。这里我们可以看到RCC_APB2ENR寄存器的位3控制GPIOB的时钟打开与关闭。
- 打开GPIOB外设的时钟
RCC_APB2ENR |= (1<<3); //将1左移3位变为0x10,与RCC_APB2ENR进行或运算
- 端口配置低寄存器(GPIOx_CRL)
GPIOx_CRL 中包含 0-7 号引脚(控制GPIO低8位),每个引脚占用 4 个寄存器 位。 MODE 位用来配置输出的速度, CNF 位用来配置各种输入输出模式。
我们配置GPIOB_Pin_1为通用推挽输出,输出速度为2M
GPIOB_CRL &= ~( 0x0F<< (4*1));// 0x0F左移4位,取反,再与GPIOB_CRL进行与运算,将4-7位的数据变为0
GPIOB_CRL |= (2<<4*0); //2左移0位,及0x02,将第二位置为1,配置PB1为通用推挽输出,速度为2M
- 端口输出数据寄存器(GPIOx_ODR)
这个寄存器功能很简单,控制输出的数据为0或者1 。
所以我们控制LED延时闪烁也很简单,就是控制ODR寄存器先输出1,LED灯亮,延时一段时间,控制ODR寄存器先输出0,LED灯灭,一直循环,就能实现流水灯的效果。
控制GPIOB_Pin_1输出为1
GPIOB_ODR |= (1<<1); //1左移1位,变为0x10,与GPIOB_ODR进行或运算,将其第二位变为1
控制GPIOB_Pin_1输出为0
GPIOB_ODR &= ~(1<<1); 1左移1位,取反,与GPIOB_ODR进行与运算,将其第二位变为0
(三)寄存器编程实现
- 总的代码如下
void delay(unsigned int i);
int main(void)
{
// 开启GPIOA、GPIOB、GPIOC 端口时钟
RCC_APB2ENR |= (7<<2);
//清空控制PA12的端口位
GPIOA_CRH &= ~( 0x0F<< (4*4));
// 配置PA12为通用推挽输出,速度为2M
GPIOA_CRH |= (2<<4*4);
//清空控制PB1的端口位
GPIOB_CRL &= ~( 0x0F<< (4*1));
// 配置PB1为通用推挽输出,速度为2M
GPIOB_CRL |= (2<<4*1);
//清空控制PC14的端口位
GPIOC_CRH &= ~( 0x0F<< (4*6));
// 配置PC14为通用推挽输出,速度为2M
GPIOC_CRH |= (2<<4*6);
while(1)
{
GPIOB_ODR |= (1<<1);
delay(100); //红灯闪烁
GPIOB_ODR &= ~(1<<1);
delay(100);
GPIOA_ODR |= (1<<12);
delay(100); //黄灯闪烁
GPIOA_ODR &= ~(1<<12);
delay(100);
GPIOC_ODR |= (1<<14);
delay(100); //绿灯闪烁
GPIOC_ODR &= ~(1<<14);
delay(100);
}
}
void delay(unsigned int i)
{
unsigned char j; //简单延时函数
unsigned char k;
for(;i>0;i--)
for(j =500; j>0; j--)
for(k =200; k>0; k--);
}
注意:
在这个代码中用到4*0的作用是因为是用的是作用于GPIO_CRL(GPIO_CRL每4位控制1个引脚),将4*
0中的0改为1后就方便对后续引脚进行操作,这种写法是为了后续操作的快捷。
在自己编写代码时,不要忽略上面所定义的寄存器地址。
三、汇编语言流水灯
-
构建一个纯汇编的工程文件,配置工程文件环境时,不要选择’’
startup
’‘和’’core
’’(因为参考程序里有自带的类似启动文件startup
之类的功能,选择之后会导致冲突) -
代码如下
RCC_APB2ENR EQU 0x40021018
GPIOA_CRH EQU 0x40010804
GPIOA_ODR EQU 0x4001080C
GPIOB_CRL EQU 0x40010C00 ;寄存器映射
GPIOB_ODR EQU 0x40010C0C
GPIOC_CRH EQU 0x40011004
GPIOC_ODR EQU 0x4001100C
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
AREA RESET, DATA, READONLY
__Vectors DCD __initial_sp
DCD Reset_Handler
AREA |.text|, CODE, READONLY
THUMB
REQUIRE8
PRESERVE8
ENTRY
Reset_Handler
MainLoop BL LED2_Init
BL LED2_ON
BL Delay ;LED2灯闪烁
BL LED2_OFF
BL Delay
BL LED1_Init
BL LED1_ON
BL Delay ;LED1灯闪烁
BL LED1_OFF
BL Delay
BL LED3_Init
BL LED3_ON
BL Delay ;LED3灯闪烁
BL LED3_OFF
BL Delay
B MainLoop
LED1_Init
PUSH {R0,R1, LR}
LDR R0,=RCC_APB2ENR
ORR R0,R0,#0x08 ;开启端口GPIOB的时钟
LDR R1,=RCC_APB2ENR
STR R0,[R1]
LDR R0,=GPIOB_CRL
ORR R0,R0,#0X00000020 ;GPIOB_Pin_1配置为通用推挽输出
LDR R1,=GPIOB_CRL
STR R0,[R1]
LDR R0,=GPIOB_ODR
BIC R0,R0,#0X00000002
LDR R1,=GPIOB_ODR ;GPIOB_Pin_1输出为0
STR R0,[R1]
POP {R0,R1,PC}
LED1_OFF
PUSH {R0,R1, LR}
LDR R0,=GPIOB_ODR
BIC R0,R0,#0X00000002 ;GPIOB_Pin_1输出为0,LED1熄灭
LDR R1,=GPIOB_ODR
STR R0,[R1]
POP {R0,R1,PC}
LED1_ON
PUSH {R0,R1, LR}
LDR R0,=GPIOB_ODR
ORR R0,R0,#0X00000002 ;GPIOB_Pin_1输出为1,LED1亮
LDR R1,=GPIOB_ODR
STR R0,[R1]
POP {R0,R1,PC}
LED2_Init
PUSH {R0,R1, LR}
LDR R0,=RCC_APB2ENR
ORR R0,R0,#0x04 ;打开GPIOA的时钟
LDR R1,=RCC_APB2ENR
STR R0,[R1]
LDR R0,=GPIOA_CRH
ORR R0,R0,#0X00020000 ;GPIOA_Pin_12配置为通用推挽输出
LDR R1,=GPIOA_CRH
STR R0,[R1]
LDR R0,=GPIOA_ODR
BIC R0,R0,#0X00001000
LDR R1,=GPIOA_ODR ;GPIOA_Pin_12输出为0
STR R0,[R1]
POP {R0,R1,PC}
LED2_OFF
PUSH {R0,R1, LR}
LDR R0,=GPIOA_ODR
BIC R0,R0,#0X00001000 ;GPIOA_Pin_12输出为0,LED2熄灭
LDR R1,=GPIOA_ODR
STR R0,[R1]
POP {R0,R1,PC}
LED2_ON
PUSH {R0,R1, LR}
LDR R0,=GPIOA_ODR
ORR R0,R0,#0X00001000 ;GPIOA_Pin_12输出为1,LED2亮
LDR R1,=GPIOA_ODR
STR R0,[R1]
POP {R0,R1,PC}
LED3_Init
PUSH {R0,R1, LR}
LDR R0,=RCC_APB2ENR
ORR R0,R0,#0x10 ;打开GPIOC的时钟
LDR R1,=RCC_APB2ENR
STR R0,[R1]
LDR R0,=GPIOC_CRH
ORR R0,R0,#0X02000000 ;GPIOC_Pin_14配置为通用推挽输出
LDR R1,=GPIOC_CRH
STR R0,[R1]
LDR R0,=GPIOC_ODR
BIC R0,R0,#0X00004000 ;GPIOC_Pin_14输出为0
LDR R1,=GPIOC_ODR
STR R0,[R1]
POP {R0,R1,PC}
LED3_OFF
PUSH {R0,R1, LR}
LDR R0,=GPIOC_ODR
BIC R0,R0,#0X00004000 ;GPIOC_Pin_14输出为0,LED3熄灭
LDR R1,=GPIOC_ODR
STR R0,[R1]
POP {R0,R1,PC}
LED3_ON
PUSH {R0,R1, LR}
LDR R0,=GPIOC_ODR
ORR R0,R0,#0X00004000 ;GPIOC_Pin_14输出为1,LED3亮
LDR R1,=GPIOC_ODR
STR R0,[R1]
POP {R0,R1,PC}
Delay
PUSH {R0,R1, LR}
MOVS R0,#0
MOVS R1,#0
MOVS R2,#0
DelayLoop0
ADDS R0,R0,#1
CMP R0,#300
BCC DelayLoop0
MOVS R0,#0
ADDS R1,R1,#1
CMP R1,#300
BCC DelayLoop0
MOVS R0,#0
MOVS R1,#0
ADDS R2,R2,#1
CMP R2,#15
BCC DelayLoop0
POP {R0,R1,PC}
END
- 关于stm32启动文件
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
AREA RESET, DATA, READONLY
__Vectors DCD __initial_sp
DCD Reset_Handler
AREA |.text|, CODE, READONLY
THUMB
REQUIRE8
PRESERVE8
ENTRY
Reset_Handler
刚刚我们说,在新建工程的时候不要选择’’startup
’‘启动文件,这是因为我们在这里已经自己定义了关于startup`’'启动文件的一些功能,如果再包含startup启动文件,会引起冲突。
关于’’startup
’'启动文件的简要介绍
启动文件由汇编编写,是系统上电复位后第一个执行的程序。主要做了以下工作:
- 初始化堆栈指针 SP=_initial_sp
- 初始化 PC 指针 =Reset_Handler
- 初始化中断向量表
- 配置系统时钟
- 调用 C 库函数 _main 初始化用户堆栈,从而最终调用 main 函数去到 C 的世界
- LED端口初始化
LED1_Init
PUSH {R0,R1, LR}
LDR R0,=RCC_APB2ENR
ORR R0,R0,#0x08 ;开启端口GPIOB的时钟
LDR R1,=RCC_APB2ENR
STR R0,[R1]
LDR R0,=GPIOB_CRL
ORR R0,R0,#0X00000020 ;GPIOB_Pin_1配置为通用推挽输出,输出速度为2M
LDR R1,=GPIOB_CRL
STR R0,[R1]
LDR R0,=GPIOB_ODR
BIC R0,R0,#0X00000002 ;配置GPIOB_Pin_1输出为低电平,LED灯灭
LDR R1,=GPIOB_ODR
STR R0,[R1]
POP {R0,R1,PC}
- 软件延时
Delay
PUSH {R0,R1, LR}
MOVS R0,#0
MOVS R1,#0
MOVS R2,#0
DelayLoop0
ADDS R0,R0,#1
CMP R0,#300
BCC DelayLoop0
MOVS R0,#0
ADDS R1,R1,#1
CMP R1,#300
BCC DelayLoop0
MOVS R0,#0
MOVS R1,#0
ADDS R2,R2,#1
CMP R2,#15
BCC DelayLoop0
POP {R0,R1,PC}
这部分采用的是软件延时,具体原理为R0
、R1
、R2
初始化为0,R0
加1,当R0大于300时,R1加1,然后R0为变为0,R0继续加1,循环往复,当R1大于300时,R2加1,R0、R1变为0,继续此操作。
这个延时函数大概持续300*300*15=1350000
个指令周期。
- 小结
本次实例汇编代码有些部分重复,导致代码有些冗余。此外,其实我使用的这种点灯方式本质上就是操作寄存器,只不过使用的是汇编语言。大家可以参考上面寄存器点灯程序比较一下两者的区别。
为了对比方便,在汇编程序中的变量命名我尽量保持与寄存器中的命名一样。
四、实际效果
上面三种方式都是控制GPIOA_Pin_12 、GPIOB_Pin_1、GPIOC_Pin_14端口的,最终实现效果均如下所示。
总结
本次实验从c语言固件库到寄存器再到汇编语言,关于实验所涉及到的也越来越底层,关于stm32寄存器的一些功能有了更深刻的理解。
在寄存器操作时,对左移、右移、异或、按位与、按位或,还有关于c语言的结构体,指针、宏定义都用上了,也算是学以致用吧。
实验不足的地方是,在使用汇编语言时没有使用精确延时,而是退而求其次,采用准确度不高的软件延时,究其原因还是不熟悉。
有关上述有不当之处,望大家不吝指教。
参考
STM32从地址到寄存器
STM32寄存器的简介、地址查找,与直接操作寄存器
参考书籍
《STM32库开发指南-基于野火指南者开发板》
《STM32F10X参考手册》
资料链接
链接:https://pan.baidu.com/s/1w5iVrx-Ob_C5z5MljxZkww
提取码:i4wl