stm32 LED流水灯
一、实验准备
-
硬件组成:
-
STM32 最小系统核心板(STM32F103C8T6):包含微控制器芯片、晶振、复位电路等基本组成部分。
-
面包板:用于连接 LED 和核心板。
-
多个红绿蓝 LED:用于显示流水灯效果。
-
电路连接:
-
使用 GPIOA/GPIOB/GPIOC 端口控制 LED 灯,将 LED 的阳极连接到相应的 GPIO 端口,阴极通过限流电阻连接到地,根据选择的端口放置LED灯。
-
将系统板的上下两组3V3和GND接到上下两端的正负极。
-
实物图:
二、基于寄存器地址
1.基本原理
1. 基本概念
寄存器是一种数字电路组件,它是中央处理器(CPU)或其他数字芯片内部用于暂时存储数据的单元。可以把它想象成一个个小的存储盒子,每个盒子有一个特定的地址,用来存放特定类型的数据,如指令、地址或数据。
从硬件角度看,寄存器通常由一组触发器组成,触发器能够在时钟信号的控制下保存和更新数据。例如,D触发器是一种常用的构建寄存器的基本单元,它在时钟上升沿或下降沿(取决于具体的设计)时,将输入的数据保存到内部,并在其他时间保持该数据不变。
2. 数据存储和读取原理
存储:当CPU或其他控制单元要向寄存器写入数据时,会通过数据总线将数据发送到寄存器的输入端。在合适的时钟信号到来时,寄存器内部的电路(如上述的触发器)会将这些数据存储起来。这个过程就像是把物品放入存储盒子中。
读取:当需要读取寄存器中的数据时,CPU会通过地址总线发送寄存器的地址,选中要读取的寄存器。然后,寄存器中的数据会通过数据总线传输回CPU或其他接收单元。这就好比从特定的存储盒子中取出物品。
3. 在微处理器中的功能分类
通用寄存器:
这些寄存器主要用于存储数据和地址。例如,在一个典型的8位微处理器中,可能有像累加器(A寄存器)这样的通用寄存器。累加器用于算术和逻辑运算,当执行加法运算时,一个操作数通常存储在累加器中,另一个操作数通过数据总线输入,运算结果再存储回累加器。
通用寄存器还可以用于存储临时变量、指针等。比如在一个C语言程序编译后的机器码执行过程中,局部变量可能会存储在通用寄存器中,以方便快速访问和操作。
特殊功能寄存器:
特殊功能寄存器用于控制和监视微处理器的各种功能和外设。以单片机中的定时器为例,有定时器控制寄存器(TCR)和定时器计数寄存器(TCNT)。TCR用于设置定时器的工作模式(如定时模式还是计数模式)、启动/停止定时器等功能,而TCNT用于存储定时器的计数值。
在代码中,通过对特殊功能寄存器的操作可以实现对外设的精确控制。比如在之前看到的GPIO相关代码中,通过对GPIO控制寄存器和输出数据寄存器的操作,能够控制引脚的模式和输出电平,从而实现与外部设备(如LED)的连接和控制。
4. 与总线的关系
寄存器通过总线与微处理器的其他部分相连。数据总线用于传输要写入寄存器的数据或从寄存器读取的数据;地址总线用于选择要访问的特定寄存器;控制总线则用于传递读写等控制信号。
例如,当CPU要向一个特定的寄存器写入数据时,首先通过地址总线发送该寄存器的地址,然后通过控制总线发送写信号,最后通过数据总线发送要写入的数据。这个过程需要严格按照时序要求进行,以确保数据的正确存储和操作。
-
GPIO 端口寄存器地址和参数:
-
STM32F103C8T6 微控制器的 GPIO 端口由多个寄存器控制,包括配置寄存器(GPIOx_CRL、GPIOx_CRH)、数据寄存器(GPIOx_IDR、GPIOx_ODR)、位设置 / 清除寄存器(GPIOx_BSRR)等。
-
以 GPIOA 为例,其寄存器地址如下:
-
GPIOA_CRL:0x40010800
-
GPIOA_CRH:0x40010804
-
GPIOA_IDR:0x40010808
-
GPIOA_ODR:0x4001080C
-
GPIOA_BSRR:0x40010810
-
寄存器参数说明:
-
GPIOx_CRL 和 GPIOx_CRH:用于配置 GPIO 端口的引脚模式、速度等参数。每个引脚占用 4 位,分为两组(每组 8 个引脚)进行配置。
-
GPIOx_IDR:用于读取 GPIO 端口的输入数据。
-
GPIOx_ODR:用于设置 GPIO 端口的输出数据。
-
GPIOx_BSRR:用于设置或清除 GPIO 端口的单个引脚。
5.编程思路
首先通过宏定义了多个与相关寄存器对应的地址,如 APB2 相关寄存器、各 GPIO 端口的控制及输出数据寄存器等。接着定义了一个用于延时的函数Delay_ms
,然后有三个分别控制不同 LED 点亮的函数A_LED_LIGHT
、B_LED_LIGHT
、C_LED_LIGHT
,用于设置对应引脚输出高低电平来点亮不同位置的 LED。在main
函数中,先开启 GPIOA、GPIOB、GPIOC 的时钟,然后对各端口相应引脚的配置寄存器进行位操作,设置引脚模式和速度等,初始化对应引脚为输出且点亮 LED 进行初始状态设置,最后通过循环依次调用点亮不同 LED 的函数,并配合延时函数,实现每隔 1 秒切换点亮不同位置 LED 的闪烁效果。
2.创建工程
1.打开KEIL5软件,点击上面栏的Project,选择New uVision Project
2.保存在特定文件夹内
3.按照板子类型选择型号(STM32F103C8T6)
这个页面直接关掉即可
4.右键点击Sourse Group 1,选择添加新项目(add new item to group),命名为(main.c)
再次右键点击,选择添加已有项(add existing files to group source group 1),把startup_stm32f10x-md.s文件添加进来,keil软件的固件库中会有这个文件。
5.配置魔术棒选项卡
Target:勾选Use MicroLIB
Output:勾选Create HEX File
Listing:点击Select Folder for Listings…,让输出文件定位到Listing文件夹
点击OK
Debug:右边的Use选择ST-Link Debugger
Utilities:选择Use Debug Driver
Debug Settings:在Debug的Settings里,点击Flash Download,点击选择Reset and Run
点击确定,点击OK,完成配置。
3.代码部分
main.c
//--------------APB2使能时钟寄存器的地址定义------------------------
// 通过指针强制类型转换的方式,定义了指向APB2使能时钟寄存器地址(0x40021018)的指针,
// 方便后续对该寄存器进行操作,这里将其宏定义为RCC_AP2ENR,便于代码中使用
#define RCC_AP2ENR *((unsigned volatile int*)0x40021018)
//----------------GPIOA配置寄存器的地址定义 ------------------------
// 同样是指针强制类型转换,指向GPIOA配置寄存器地址(0x40010800),
// 宏定义为GPIOA_CRL,后续可通过它来配置GPIOA相关引脚的工作模式等属性
#define GPIOA_CRL *((unsigned volatile int*)0x40010800)
//----------------GPIOA输出寄存器的地址定义 ------------------------
// 指向GPIOA输出寄存器地址(0x4001080C)的指针宏定义,
// 用于控制GPIOA引脚的输出电平状态,定义为GPIOA_ORD
#define GPIOA_ORD *((unsigned volatile int*)0x4001080C)
//----------------GPIOB配置寄存器的地址定义 ------------------------
// 针对GPIOB配置寄存器,其地址为0x40010C04,通过此宏定义(GPIOB_CRH)来操作该寄存器,
// 实现对GPIOB相关引脚功能模式等的配置
#define GPIOB_CRH *((unsigned volatile int*)0x40010C04)
//----------------GPIOB输出寄存器的地址定义 ------------------------
// 定义指向GPIOB输出寄存器地址(0x40010C0C)的宏,方便设置GPIOB引脚输出电平,名为GPIOB_ORD
#define GPIOB_ORD *((unsigned volatile int*)0x40010C0C)
//----------------GPIOC配置寄存器的地址定义 ------------------------
// 宏定义指向GPIOC配置寄存器地址(0x40011004)的指针,
// 借助GPIOC_CRH来配置GPIOC引脚的工作模式等参数
#define GPIOC_CRH *((unsigned volatile int*)0x40011004)
//----------------GPIOC输出寄存器的地址定义 ------------------------
// 定义指向GPIOC输出寄存器地址(0x4001100C)的宏,用于操控GPIOC引脚输出电平,记为GPIOC_ORD
#define GPIOC_ORD *((unsigned volatile int*)0x4001100C)
//-------------------延时函数定义-----------------------
// 该函数用于实现简单的延时功能,通过循环来消耗一定的时间,达到延时的效果
// 参数t表示要延时的毫秒数,函数内部利用循环嵌套来大致模拟相应时长的延时
// 注意,这里只是一个简单的、粗略的延时实现方式,并非精准的定时
void Delay_ms( volatile unsigned int t)
{
unsigned int i;
while(t--)
for (i=0;i<800;i++);
}
// A7引脚设置为高电平,B9、C15引脚设置为低电平的函数,用于控制对应LED灯的亮灭状态,
// 这里假设这些引脚连接了LED灯,实现点亮特定LED(A对应的LED),熄灭其他LED的功能
void A_LED_LIGHT(){
GPIOA_ORD=0x1<<7; // 将GPIOA的第7引脚(A7)设置为高电平,点亮对应的LED灯
GPIOB_ORD=0x0<<9; // 将GPIOB的第9引脚(B9)设置为低电平,熄灭对应的LED灯
GPIOC_ORD=0x0<<15; // 将GPIOC的第15引脚(C15)设置为低电平,熄灭对应的LED灯
}
// B9引脚设置为高电平,A7、C15引脚设置为低电平的函数,用于控制对应LED灯亮灭,
// 实现点亮B对应的LED,熄灭另外两个LED的操作
void B_LED_LIGHT(){
GPIOA_ORD=0x0<<7; // 将GPIOA的第7引脚(A7)设置为低电平,熄灭对应的LED灯
GPIOB_ORD=0x1<<9; // 将GPIOB的第9引脚(B9)设置为高电平,点亮对应的LED灯
GPIOC_ORD=0x0<<15; // 将GPIOC的第15引脚(C15)设置为低电平,熄灭对应的LED灯
}
// C15引脚设置为高电平,A7、B9引脚设置为低电平的函数,用来控制LED灯亮灭情况,
// 达成点亮C对应的LED,关闭其余两个LED的目的
void C_LED_LIGHT(){
GPIOA_ORD=0x0<<7; // 将GPIOA的第7引脚(A7)设置为低电平,熄灭对应的LED灯
GPIOB_ORD=0x0<<9; // 将GPIOB的第9引脚(B9)设置为低电平,熄灭对应的LED灯
GPIOC_ORD=0x1<<15; // 将GPIOC的第15引脚(C15)设置为高电平,点亮对应的LED灯
}
int main()
{
int j=100;
// 使能GPIOA、GPIOB、GPIOC端口时钟,通过对APB2使能时钟寄存器相应位进行置位操作来实现,
// 为后续对这三个端口的引脚进行配置和操作提供时钟信号支持
RCC_AP2ENR|=1<<2;
RCC_AP2ENR|=1<<3;
RCC_AP2ENR|=1<<4;
// 以下是对GPIOA引脚相关配置及初始状态设置
// 先将GPIOA配置寄存器中对应位清零,清除之前可能存在的设置,确保后续配置的准确性
GPIOA_CRL&=0x0FFFFFFF;
// 然后将GPIOA的第7引脚(A7)配置为推挽输出模式,输出频率设置为2Mhz,便于后续控制LED灯亮灭
GPIOA_CRL|=0x20000000;
// 初始时将GPIOA的第7引脚(A7)输出高电平,使对应的LED灯初始状态为灭(假设低电平点亮LED)
GPIOA_ORD|=0x1<<7;
// 以下是对GPIOB引脚相关配置及初始状态设置
// 先清除GPIOB配置寄存器中对应位的原有设置,避免之前设置的干扰
GPIOB_CRH&=0xFFFFFF0F;
// 将GPIOB的第9引脚(B9)配置为推挽输出模式,输出频率为2Mhz,方便后续控制其连接的LED灯
GPIOB_CRH|=0x00000020;
// 初始设置GPIOB的第9引脚(B9)输出高电平,让对应的LED灯初始为灭的状态
GPIOB_ORD|=0x1<<9;
// 以下是对GPIOC引脚相关配置及初始状态设置
// 清除GPIOC配置寄存器对应位的原有设置,准备进行新的配置
GPIOC_CRH&=0x0FFFFFFF;
// 将GPIOC的第15引脚(C15)配置为推挽输出模式,输出频率设置为50Mhz,以满足后续使用需求
GPIOC_CRH|=0x20000000;
// 初始把GPIOC的第15引脚(C15)输出高电平,使对应的LED灯初始是灭的状态
GPIOC_ORD|=0x1<<15;
// 通过循环不断切换LED灯的亮灭状态,实现流水灯效果,循环100次,每次亮灯状态持续1000毫秒(1秒)
while(j)
{
A_LED_LIGHT(); // 点亮A对应的LED灯,熄灭其他灯,保持1秒
Delay_ms(1000);
B_LED_LIGHT(); // 点亮B对应的LED灯,熄灭其他灯,保持1秒
Delay_ms(1000);
C_LED_LIGHT(); // 点亮C对应的LED灯,熄灭其他灯,保持1秒
Delay_ms(1000);
}
}
我这里将A7、B9和C15选择为LED灯的位置
若出现以下报错
可在main.c里面添加以下代码:
// 函数为空,目的是为了骗过编译器不报错
void SystemInit(void)
{
}
编译成功,点击烧录程序
4.效果演示
烧录成功后,点击复位键,即可产生正常的实验现象
寄存器
5.通过PC13验证代码是否正常运行
main.c
#define RCC_AP2ENR *((unsigned volatile int*)0x40021018)
#define GPIOA_CRL *((unsigned volatile int*)0x40010800)
#define GPIOA_ORD *((unsigned volatile int*)0x4001080C)
#define GPIOB_CRH *((unsigned volatile int*)0x40010C04)
#define GPIOB_ORD *((unsigned volatile int*)0x40010C0C)
#define GPIOC_CRH *((unsigned volatile int*)0x40011004)
#define GPIOC_ORD *((unsigned volatile int*)0x4001100C)
void Delay_ms( volatile unsigned int t)
{
unsigned int i;
while(t--)
for (i=0;i<800;i++);
}
void A_LED_LIGHT(){
GPIOA_ORD=0x1<<7;
GPIOB_ORD=0x0<<9;
GPIOC_ORD=0x0<<13;
}
void B_LED_LIGHT(){
GPIOA_ORD=0x0<<7;
GPIOB_ORD=0x1<<9;
GPIOC_ORD=0x0<<13;
}
void C_LED_LIGHT(){
GPIOA_ORD=0x0<<7;
GPIOB_ORD=0x0<<9;
GPIOC_ORD=0x1<<13;
}
int main()
{
int j=100;
RCC_AP2ENR|=1<<2;
RCC_AP2ENR|=1<<3;
RCC_AP2ENR|=1<<4;
GPIOA_CRL&=0x0FFFFFFF;
GPIOA_CRL|=0x20000000;
GPIOA_ORD|=0x1<<7;
GPIOB_CRH&=0xFFFFFF0F;
GPIOB_CRH|=0x00000020;
GPIOB_ORD|=0x1<<9;
GPIOC_CRH &= 0xF0FFFFFF;
GPIOC_CRH |= 0x00200000;
GPIOC_ORD|=0x1<<13;
while(j)
{
A_LED_LIGHT();
Delay_ms(1000);
B_LED_LIGHT();
Delay_ms(1000);
C_LED_LIGHT();
Delay_ms(1000);
}
}
void SystemInit(void)
{
}
效果如视频
PC13
三、基于标准外设库
1.基本原理
1. 定义和目的
标准外设库是一种软件库,它的目的是为了方便开发者对微控制器(如STM32等)的外设进行操作。这些外设包括但不限于通用输入/输出(GPIO)端口、定时器、中断控制器、通信接口(如SPI、I2C、USART)等。通过使用标准外设库,开发者可以减少直接操作寄存器的复杂程度,提高开发效率。
2. 封装原理
寄存器操作封装:
标准外设库对底层的寄存器操作进行了封装。在不使用库的情况下,开发者需要通过定义寄存器地址(如`#define RCC_AP2ENR *((unsigned volatile int*)0x40021018)`)并直接对这些地址进行读写操作来控制外设。而标准外设库将这些寄存器操作封装成函数和结构体。
例如,对于GPIO的初始化,库中可能会有一个类似`GPIO_Init()`的函数。在这个函数内部,会根据传入的参数(如引脚模式、速度等)来自动设置对应的GPIO寄存器,而开发者不需要知道具体寄存器的地址和每一位的含义。这就好比把复杂的机器内部操作封装在一个带有简单操作按钮的盒子里,用户只需要按按钮(调用函数),而不用关心盒子内部的复杂机械结构(寄存器操作)。
功能模块封装:
它按照外设的功能将代码进行分类封装。以定时器为例,标准外设库会有一组函数专门用于定时器的初始化(设置定时器的预分频系数、自动重装载值等)、启动/停止定时器、获取定时器当前计数值等操作。
这些功能模块之间相互独立又相互配合。比如在使用中断功能时,中断控制器模块的库函数可以和定时器模块的库函数一起使用。当定时器计数溢出产生中断时,通过中断控制器库函数的设置,可以使处理器跳转到相应的中断服务程序进行处理。
3.初始化流程原理
设备配置结构体:
标准外设库通常会使用结构体来配置外设。以GPIO为例,会有一个`GPIO_InitTypeDef`结构体,这个结构体包含了GPIO引脚的各种参数,如`GPIO_Mode`(引脚模式,如输入、输出等)、`GPIO_Speed`(引脚速度)等。
开发者首先填充这个结构体,设置好所需的参数,然后将结构体作为参数传递给`GPIO_Init()`函数。`GPIO_Init()`函数会根据结构体中的参数来设置对应的GPIO寄存器,从而完成GPIO的初始化。这种方式使得配置过程更加清晰、有条理,就像按照清单(结构体参数)来组装设备(初始化外设)一样。
外设使能和配置顺序:
一般来说,首先要使能外设对应的时钟。这是因为如果时钟没有开启,外设是无法正常工作的。在标准外设库中,会有专门的函数用于使能时钟,如`RCC_APB2PeriphClockCmd()`(这是STM32标准外设库中的函数,用于使能APB2总线上的外设时钟)。
然后,按照外设的功能要求进行详细的配置,如对通信接口的波特率设置、数据格式设置等,或者对GPIO引脚的具体模式和速度设置。这些配置步骤都是基于库函数来完成的,通过合理的调用顺序,使外设能够正常工作并满足应用程序的需求。
4.中断处理原理
中断向量表和服务程序:
标准外设库会协助处理中断相关的操作。在微控制器中,有一个中断向量表,它定义了各种中断源对应的中断服务程序的入口地址。当一个中断事件发生时,处理器会根据中断向量表找到对应的中断服务程序并开始执行。
标准外设库会帮助开发者定义和注册中断服务程序。例如,在STM32中,库函数可以帮助设置中断优先级、使能中断等操作,并且可以将开发者编写的中断服务程序与相应的中断源关联起来。这样,当外设(如定时器或外部中断引脚)触发中断时,处理器能够正确地响应并执行相应的处理代码。
中断标志清除和处理流程:
对于每个中断源,通常都有对应的中断标志位。当中断事件发生时,这些标志位会被置位。在中断服务程序中,除了执行实际的中断处理任务外,还需要清除这些中断标志位,以便能够正确地响应下一次中断事件。
标准外设库提供了函数来帮助清除中断标志位。例如,对于定时器中断,会有一个函数用于清除定时器的溢出中断标志,这样可以保证定时器中断系统的正常运行,并且防止由于未清除的中断标志位导致的错误中断触发。
5.编程思路
首先通过`RCC_APB2PeriphClockCmd`函数开启GPIOA的时钟,因为操作外设前需先使能其时钟,否则操作无效。接着定义了`GPIO_InitTypeDef`结构体变量用于配置GPIOA,将其引脚模式设为推挽输出、引脚选择为所有引脚、速度设为50MHz,并把该结构体传递给`GPIO_Init`函数完成GPIOA的初始化。在主循环里,利用`GPIO_Write`函数按顺序设置GPIOA各引脚的高低电平,每次让一个引脚输出低电平(其余引脚为高电平)来模拟流水灯的效果,且每次设置电平后都调用`Delay_ms`函数进行一定时长的延时,使得流水灯效果能够清晰呈现,循环不断执行以持续展示流水灯闪烁变化的状态。
2.创建工程
1.可以之前的工程为基础,将添加的startup_stm32f10x-md.s文件删除,重新添加固件库文件。也可直接重新创建工程
退出keil软件,打开实验目录文件夹,继续新建三个文件“Start,Library、User”,然后打开固件库,找到启动文件(可参考江协科技的固件库),先全选,然后复制粘贴到Start文件夹下, 具体路径见下图:
然后继续在固件库里找到stm32f10x和system3个文件,复制粘贴到Start文件夹下:
在找到core_cm3的两个文件,复制粘贴到Start文件夹下,这样start文件夹就添加完成。
继续找到标准外设驱动的文件夹,打开src,全选复制,粘贴到Library文件夹下,然后打开inc,同样全部复制到Library文件下,Library文件夹配置完成。文件路径如图:
最后打开Project(江协提供的资料中),打开STM32F10x_StdPeriph_ Template的文件,选择其中的main、conf还有两个it文件,复制粘贴到User文件夹下:
到此为止,start、User、Library全部建立完毕,工程文件复制完毕。
2.文件建立完毕以后,回到keil软件,点击工程文件管理按钮(形状是三个方块),把默认的组删掉,新建start、Library、User,然后分别选中,点击Add Files,添加文件,路径就是之前配置的文件,注意添加start文件时,先选后缀为md的文件,再去添加其他的.c和.文件,其他两个全选添加:
新建start、Library、User
分别选中,点击Add Files,添加文件,路径就是之前配置的文件,注意添加start文件时,先选后缀为md的文件,再去添加其他的.c和.文件,其他两个全选添加:
点击OK,配置完毕
之后点击魔术棒选项,打开C/C++,在include paths栏,把添加的文件夹路径都添加进来
点击OK,在Define栏写上USE_STDPERIPH_DRIVER
打开Debug,调试器选择ST-Link
然后打开Flash Download,勾选Reset and Run
由于流水灯控制涉及延时,所以需要添加两个新的文件Delay.c和Delay.h,方法和之前建立start等文件的方法一致,在江协科技提供的程序源码中,如图是该文件的路径:
新建一个System文件存放以下文件,完整文件如下图所示:
至此,配置完毕
3.代码部分
mian.c
#include "stm32f10x.h" // 引入STM32F10x系列芯片的相关头文件
#include "Delay.h"
int main(void)
{
/* 开启时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能GPIOA的时钟,使用外设前需开启对应时钟,不然操作无效
/* GPIO初始化 */
GPIO_InitTypeDef GPIO_InitStructure; // 定义GPIO初始化结构体变量
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置GPIO为推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All; // 选择所有引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置引脚输出速度为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); // 传递参数给函数,完成GPIOA初始化,函数内部配置相应寄存器
/* 主循环,循环体内代码会一直循环执行 */
while (1)
{
/* 使用GPIO_Write同时设置GPIOA所有引脚高低电平,实现LED流水灯效果 */
GPIO_Write(GPIOA, ~0x0001); // 0000 0000 0000 0001,PA0引脚设为低电平,其余引脚为高电平(数据按位取反)
Delay_ms(0); // 延时0ms
GPIO_Write(GPIOA, ~0x0002); // 0000 0000 0000 0010,PA1引脚设为低电平,其余引脚为高电平
Delay_ms(1000); // 延时1000ms
GPIO_Write(GPIOA, ~0x0004); // 0000 0000 0000 0100,PA2引脚设为低电平,其余引脚为高电平
Delay_ms(0); // 延时0ms
GPIO_Write(GPIOA, ~0x0008); // 0000 0000 0000 1000,PA3引脚设为低电平,其余引脚为高电平
Delay_ms(0); // 延时0ms
GPIO_Write(GPIOA, ~0x0010); // 0000 0000 0001 0000,PA4引脚设为低电平,其余引脚为高电平
Delay_ms(1000); // 延时1000ms
GPIO_Write(GPIOA, ~0x0020); // 0000 0000 0010 0000,PA5引脚设为低电平,其余引脚为高电平
Delay_ms(0); // 延时0ms
GPIO_Write(GPIOA, ~0x0040); // 0000 0000 0100 0000,PA6引脚设为低电平,其余引脚为高电平
Delay_ms(0); // 延时0ms
GPIO_Write(GPIOA, ~0x0080); // 0000 0000 1000 0000,PA7引脚设为低电平,其余引脚为高电平
Delay_ms(1000); // 延时1000ms
}
}
我将P1、P4、P7设置为需要亮起的LED灯。
4.效果演示
烧录成功后,点击复位键,即可产生正常的实验现象
外设库
5.观察时序波形
1.点击魔术棒进入Debug,选中Use Simulator,"Dialog DLL" 和 "Parameter" 也需要更改,软件仿真填入 "DARMSTM.DLL" ,参数为 "-pSTM32F103C8",这里是我的STM32芯片型号。硬件仿真填入 "TARMSTM.DLL",参数同上
点击OK完成配置
2.点击上面栏的放大镜进行调试
3.点击
选择第一个
4.点击Setup,添加要观测波形的引脚
我选择观测PA1、PA4、PA7的波形,点击添加,输入:PORTA.1、PORTA.4、PORTA.7,Display Type改成Bit然后关闭即可
5.点击图标,开始观察
6.计算开始仿真分析高低电平转换周期(LED闪烁周期)
观察高低电平持续时间
打开计算器,算出时间
高电平
低电平
四、总结
1.对比两种方式
1.两种方法的流程图对比
2.优缺点:
1.基于寄存器地址实现 LED 流水灯
原理:这种方式是直接对芯片的寄存器地址进行读写操作。在代码中,通过将寄存器地址强制转换为指针类型,如#define RCC_AP2ENR *((unsigned volatile int*)0x40021018)
,来访问和修改寄存器的值。这样就可以直接控制硬件的行为,例如配置时钟使能、引脚功能(输入 / 输出模式)和输出电平。
灵活性:非常灵活,因为可以根据具体需求对寄存器进行精细的配置。例如,如果需要对特定引脚的特殊功能进行设置,或者对硬件进行一些非标准的操作(如自定义的时序控制等),直接操作寄存器可以很方便地实现。
效率:在代码执行效率方面,由于直接访问硬件寄存器,没有中间层的封装和处理,所以在某些对性能要求极高的场景下(如实时性要求很强的应用)可能会更高效。但是,这种方式编写的代码可读性相对较差,而且容易出错,因为需要开发者牢记寄存器的地址、位定义和功能等细节。
可移植性:可移植性较差。因为不同的芯片或微控制器的寄存器地址和功能定义是不同的。如果要将代码移植到其他芯片上,几乎需要对整个代码进行重新编写,尤其是与硬件寄存器相关的部分。
2.基于标准外设库实现 LED 流水灯
原理:标准外设库是芯片厂商提供的一套软件库,它对底层的硬件寄存器进行了封装。通过调用库函数来实现对硬件的操作,而不是直接访问寄存器地址。这些库函数通常具有较高的抽象层次,隐藏了底层寄存器操作的复杂性。
灵活性:相对基于寄存器方式灵活性稍低。因为标准外设库提供的是通用的功能函数,对于一些非常特殊的硬件操作可能无法直接支持,需要开发者深入到库的底层或者结合寄存器操作来实现。
效率:在效率方面,由于有函数调用的开销,可能会比直接操作寄存器稍慢一些。不过,在大多数实际应用中,这种性能损失通常是可以接受的。而且,标准外设库在实现函数时,也会考虑性能优化,不会有太大的性能瓶颈。
可移植性:可移植性较好。因为标准外设库在设计时考虑了不同芯片之间的兼容性。如果要将代码移植到同系列的其他芯片上,只要芯片的外设功能基本相似,只需要修改少量与芯片型号相关的初始化代码(如头文件引用、芯片型号定义等),大部分的功能函数调用可以保持不变。
2.实验总结
本次实验旨在实现LED流水灯效果,运用了基于寄存器地址和标准外设库两种方法。 基于寄存器地址方式,直接对芯片寄存器地址(如`RCC_AP2ENR`、`GPIOA_CRL`等)做强制类型转换后读写,以此精准控制时钟使能、引脚功能及电平。像在`main`函数里,通过位操作寄存器开启`GPIOA`、`GPIOB`、`GPIOC`时钟,配置引脚模式与初始电平来驱动LED。该法灵活且执行高效,利于特殊定制,但对芯片寄存器知识要求高,代码晦涩、移植性差。 基于标准外设库方式,调用厂商封装好的函数(类似`RCC_APB2PeriphClockCmd()`、`GPIO_Init()`),借助结构体传参操作硬件,隐藏复杂寄存器细节,提升可读性与开发效率,移植也相对轻松,只是函数调用产生额外开销,灵活性略逊。 实验成功呈现LED流水闪烁,助于理解硬件控制策略。今后可按需选法或混合运用,拓展功能,如结合定时器精准延时,完善流水灯及更多复杂应用开发。
作者:超级无敌牛肉面