STM32 基于寄存器&标准外设库的LED流水灯
引言:
STM32是一款广泛应用于嵌入式系统开发的微控制器系列,具有高性能、低功耗和丰富的外设功能。在STM32的开发过程中,我们可以选择使用基于寄存器或标准外设库的编程方式。LED流水灯是一种常见的嵌入式开发实践项目,通过控制多个LED灯的亮灭顺序,展示出流动的效果。本文将介绍如何使用STM32的基于寄存器或标准外设库的编程方式实现LED流水灯效果。无论您是初学者还是有一定经验的开发者,本文都将为您提供详细的步骤和示例代码,帮助您快速上手STM32的开发工作。无论您选择使用基于寄存器还是标准外设库的编程方式,都能够实现出色的LED流水灯效果,为您的嵌入式系统增添亮点。让我们开始这个有趣而有挑战性的STM32开发之旅吧!
~ ₍ᐢ…ᐢ₎♡ ~ ₍ᐢ…ᐢ₎♡ ~ ₍ᐢ…ᐢ₎♡
文章目录
一、创建一个Stm32 的基本工程
创建一个 Stm32工程 为基本项目,本文不给予介绍
下面给出一个大佬链接
૮(˶ᵔ ᵕ ᵔ˶)ა
希望大家自行参考:https://blog.youkuaiyun.com/weixin_44631044/article/details/114302471
二、使用基于寄存器实现 -> LED流水灯
1、寄存器讲解
STM32寄存器是指STM32微控制器中的特殊功能寄存器(Special Function Registers,简称SFR),用于控制和配置微控制器的各种功能和外设。寄存器是一种存储器元素,用于存储和操作数据。
-
STM32微控制器的寄存器可以分为通用寄存器和特殊功能寄存器两类。
-
通用寄存器:包括通用寄存器组和堆栈指针寄存器。通用寄存器组包括R0~R15,用于存储临时数据和计算结果。堆栈指针寄存器(SP)用于指示堆栈的当前位置。
-
特殊功能寄存器:包括控制寄存器、状态寄存器、数据寄存器等。这些寄存器用于配置和控制微控制器的各种外设和功能模块,如GPIO控制寄存器、定时器控制寄存器、串口控制寄存器等。
-
-
在STM32寄存器编址方案中,每个寄存器都有一个唯一的地址,通过读写这些地址来操作寄存器。寄存器的位域(bit field)用于表示寄存器中的各个位的功能和含义,可以通过设置或清除位域来配置寄存器的不同功能。
2、GPIO讲解
GPIO是通用输入输出端口(General-purpose input/output)的英文简写,是所有的微控制器必不可少的外设之一,可以由STM32直接驱动从而实现与外部设备通信、控制以及采集和捕获的功能。STM32单片机的GPIO被分为很多组,每组有16个引脚,不同型号的MCU的GPIO个数是不同的,比如STM32F103C8T6只有PA、PB以及个别PC引脚而STM32F103ZET6拥有PA~PG的全部112个引脚。所有的GPIO都有基本的输入输出功能,同时GPIO还可以作为其它的外设功能引脚。
作为STM32最基本的外设,GPIO最基本的输出功能是由STM32控制 引脚输出高低电平,比如可以把GPIO接LED灯来控制其亮灭,也可以接继电器或者三极管,通过继电器或三极管来控制外部大功率电路的通断。
- GPIO的硬件结构框图
3、GPIO工作模式
STM32的GPIO共有8种工作模式,分别是输入模式的模拟输入、上拉输入、下拉输入和浮空输入以及输出模式的推挽输出、开漏输出、推挽复用输出和开漏复用输出
为了便于理解,使用结构框图来详细讲解每一种模式:
- 浮空输入模式
GPIO作为输入功能的浮空输入时,电信号使由外部流向内部的,从结构图的右侧往左侧看,信号流经顺序是①端口——②施密特触发器——③输入数据寄存器——④读取
- 上拉输入模式
上拉输入和浮空输入的区别就是在第①和第②之间多了一个上拉电阻,这样GPIO在没有连接外部部件时的默认电平是高电平,其它流程和原来一样。
- 下拉输入模式
下拉输入和浮空输入的区别就是在第①和第②之间多了一个下拉电阻,这样GPIO在没有连接外部部件时的默认电平是低电平,其它流程和原来一样。
- 模拟输入模式
模拟输入模式和其它三种输入模式不同,它的外部电平信号没有流入输入数据寄存器,而是直接流入模拟输入部分。模拟输入一般是用来ADC读取和转换的。
- 开漏输出模式
GPIO 的输出模式比输入模式复杂,首先看开漏输出模式,电平信号由STM32内部流出引脚,因此流向是①写(包括位设置/清除寄存器、输出数据寄存器)——②输出控制电路——③N-MOS管——④I/O端口
- 开漏复用输出模式
开漏复用输出和开漏输出的区别在于信号来源,复用的来源不是内部直接通过输出数据寄存器写的,而是由复用功能的外设决定的。
- 推挽输出模式
推挽输出模式和开漏输出模式有一定的区别,其控制输出的寄存器是一样的,但是②部分的写1有效,即输出控制电路输出1的时候,P-MOS管导通,N-MOS管截止,这样I/O口电平就会被P-MOS管拉高,输出强高电平;相反,当输出控制电路输出0时,P-MOS管截止,N-MOS管导通,I/O端口电平被N-MOS管拉低,输出强低电平。同样,输出的电平信号可以被输入数据寄存器读取。
- 推挽复用输出模式
推挽复用输出和推挽输出的区别在于信号来源,其信号来源是由复用功能相关的通信通道来控制。
5、快速入门STM32 GPIO的配置寄存器(CRL、CRH)
-
问题
在使用STM32的时候配置GPIO是最常见的操作,可以使用比较简单明白的库函数配置,但很繁杂。使用寄存器的方式可以快速配置,对于同一个IO口的输入输出都需要使用到的时候,比如IIC通讯的SDA接口就是要输出和检测输入。
我们在很多工程都能看到比如下面的一些代码:
//IO方向设置
#define SDA_IN() {GPIOA->CRL&=0XFFFFFFF0;GPIOA->CRL|=8;}
#define SDA_OUT() {GPIOA->CRL&=0XFFFFFFF0;GPIOA->CRL|=3;}
代码:GPIOA->CRL&=0XFFFFFFF0;GPIOA->CRL|=8;是什么意思呢?意思很简单就是配置IO的工作模式嘛!至于是怎么来写,我以前一直没弄懂,现在弄懂了记录下来。
- GPIO的配置寄存器CRL和CRH
STM32的一组GPIO有16个IO口,比如GPIOA这一组,有GPIOA0~GPIOA15一共16个IO口。每一个IO口需要寄存器的4位用来配置工作模式。
那么一组GPIO就需要16x4=64位的寄存器来存放这一组GPIO的工作模式的配置,但STM32的寄存器都是32位的,所以只能使用2个32位的寄存器来存放了。CRL用来存放低八位的IO口(GPIOx0—GPIOx7)的配置,CRH用来存放高八位的IO口(GPIOx8—GPIOx15)的配置。
这两个寄存器的全称是:端口配置低寄存器(GPIOx_CRL) (x=A…E) 和 端口配置高寄存器(GPIOx_CRH) (x=A…E)
也就是每一组GPIO都有两个32位的寄存器是用来配置IO口的工作模式的。
我们都清楚STM32的GPIO有八种工作模式,4个二进制数可以组合出16种情况,而我们只需要8种就行了。至于4位数怎么组合是什么工作模式,我们看STM32的手册。
3、工作模式的配置
我们直接看手册的说明:
可以看出,4位中又分为了CNFy和MODEy(y表示这组GPIO的第几个IO口),现在我们分析这两个的作用。
MODEy:
00:输入模式(复位后的状态)
01:输出模式,最大速度10MHz
10:输出模式,最大速度2MHz
11:输出模式,最大速度50MHz
可以看出MODEy是用来配置是输出还是输入模式的。一般是使用00和11这两种情况。00是输入模式,11是输出模式。
CNFy:
在输入模式(MODE[1:0]=00):
00:模拟输入模式
01:浮空输入模式(复位后的状态)
10:上拉/下拉输入模式
11:保留
在输出模式(MODE[1:0]>00):
00:通用推挽输出模式
01:通用开漏输出模式
10:复用功能推挽输出模式
11:复用功能开漏输出模式
这些就是CNFy的配置,配置具体的工作模式。配合MODEy就可以配置出所有的工作模式了。
比如我需要配置上拉输入模式,那么4位寄存器的配置就是CNFy【10】MODEy【00】:1000换成十进制数就是8。
GPIOA->CRL&=0XFFFFFFF0;GPIOA->CRL|=8;
所以这段代码的意思就是将GPIOA0配置成上拉(下拉)输入模式。
根据上面的学习,我们可以写出一下的寄存器版代码
4、寄存器版代码–流水灯
#define RCC_AP2ENR *((unsigned volatile int*)0x40021018)
//----------------GPIOA配置寄存器 ------------------------
#define GPIOA_CRL *((unsigned volatile int*)0x40010800)
#define GPIOA_ORD *((unsigned volatile int*)0x4001080C)
//----------------GPIOB配置寄存器 ------------------------
#define GPIOB_CRH *((unsigned volatile int*)0x40010C04)
#define GPIOB_ORD *((unsigned volatile int*)0x40010C0C)
//----------------GPIOC配置寄存器 ------------------------
#define GPIOC_CRH *((unsigned volatile int*)0x40011004)
#define GPIOC_ORD *((unsigned volatile int*)0x4001100C)
//-------------------简单的延时函数-----------------------
#include "stm32f10x.h" // Device header
#include "Delay.h"
//------------------------主函数--------------------------
int main()
{
RCC_AP2ENR|=1<<2; //APB2-GPIOA外设时钟开启
RCC_AP2ENR|=1<<3; //APB2-GPIOB外设时钟开启
RCC_AP2ENR|=1<<4; //APB2-GPIOC外设时钟开启
// A灯
GPIOA_CRL&=0xFF0FFFFF; //设置位 清零
GPIOA_CRL|=0x00200000; //PA12推挽输出
GPIOA_ORD|=0<<5; //设置初始灯为灭
// B灯
GPIOB_CRH&=0xFFFFF0FF; //设置位 清零
GPIOB_CRH|=0x00000200; //PB1推挽输出
GPIOB_ORD|=0<<10; //设置初始灯为灭
// C灯
GPIOC_CRH&=0xFF0FFFFF; //设置位 清零
GPIOC_CRH|=0x00200000; //PC14推挽输出
GPIOC_ORD|=0<<13; //设置初始灯为灭
while(1)
{
GPIOA_ORD=0x1<<5; //PA5高电平
Delay_ms(1000);
GPIOA_ORD=0x0<<5; //PA5低电平
Delay_ms(1000);
GPIOB_ORD=0x1<<10; //PB10高电平
Delay_ms(1000);
GPIOB_ORD=0x0<<10; //PB10低电平
Delay_ms(1000);
GPIOC_ORD=0x1<<13; //PC13高电平
Delay_ms(1000);
GPIOC_ORD=0x0<<13; //PC13低电平
Delay_ms(1000);
}
}
5、结果展示
VID_20231014_103543
三、使用基于寄存器实现 -> LED流水灯
代码: main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
int main(void)
{
LED_Init();
while (1)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_5,1);
Delay_ms(1000);
GPIO_WriteBit(GPIOA,GPIO_Pin_5,0);
Delay_ms(1000);
GPIO_WriteBit(GPIOB,GPIO_Pin_10,1);
Delay_ms(1000);
GPIO_WriteBit(GPIOB,GPIO_Pin_10,0);
Delay_ms(1000);
GPIO_WriteBit(GPIOC,GPIO_Pin_13,1);
Delay_ms(1000);
GPIO_WriteBit(GPIOC,GPIO_Pin_13,0);
Delay_ms(1000);
}
}
代码: LED.c
void LED_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); // 打开外设,使能GPIOC端口时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); // 打开外设,使能GPIOC端口时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE); // 打开外设,使能GPIOC端口时钟
// 配置 端口数据
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; // 配置端口
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;// 配置IO口的速度
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // 配置端口
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;// 配置IO口的速度
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; // 配置端口
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;// 配置IO口的速度
GPIO_Init(GPIOC,&GPIO_InitStructure);
GPIO_ResetBits(GPIOA,GPIO_Pin_5); // 设置初始熄灭
GPIO_ResetBits(GPIOB,GPIO_Pin_10); // 设置初始熄灭
GPIO_ResetBits(GPIOC,GPIO_Pin_13); // 设置初始熄灭
}
结果展示:
VID_20231014_104233
KEIL的软件仿真分析仪观察GPIO的波形
QQ录屏20231014105702
四、两种方式对比
基于寄存器
- 直接操作GPIO相关寄存器实现LED控制,如GPIOx_MODER、GPIOx_ODR等
- 代码更低级,接近硬件寄存器操作
- 控制更细致,可以实现更复杂的LED动画效果
- 需要了解STM32单片机GPIO相关所有寄存器
- 代码难度较大,调试和维护也较复杂
基于固件库
- 使用STM32官方固件库GPIO相关函数实现LED控制,如HAL_GPIO_WritePin()等
- 代码更高级,抽象掉了寄存器操作细节
- 控制相对简单,主要实现简单的LED亮灭
- 不需要了解STM32底层GPIO寄存器
- 代码难度小,开发和调试更简单
- 但动画效果实现难度较大,需要更多固件库支持
五、总结
总的来说,基于寄存器的方法更加底层、灵活和精细,适合对硬件有更深入了解和更高级的控制需求。而基于固件库的方法更加高级、简洁和易于理解,适合快速开发和简单应用。选择哪种方法取决于个人的编程经验、项目需求和时间限制。通过这个实验,我对STM32的寄存器操作和HAL库的使用有了更深入的了解,并且能够根据项目需求选择合适的开发方法。
最后感谢大佬友情链接:
- https://blog.youkuaiyun.com/qq_39530692/article/details/130835922
- https://blog.youkuaiyun.com/qq_44016222/article/details/123206403
- https://blog.youkuaiyun.com/qq_43279579/article/details/110320013
- https://blog.youkuaiyun.com/weixin_45915259/article/details/123878323