前言
1. 确定实现LED流水灯代码的逻辑
需要的功能有:
① 点亮与熄灭流水灯–实验的首要功能
② 计时器–用于以时间控制led效果
2. 准备工作
① LED灯珠
② STM32电路板
本次选择STM32F103C8T6
③ 标准外设库
④ 选择控制的引脚:本次选择A10,B5以及C13引脚
3. 算法思路
① 设计延时函数
延时函数,用于以秒为单位进行延时
② 启用GPIO的时钟:
在STM32微控制器中,每个GPIO端口都有对应的时钟使能位,需要启用相应的GPIO端口时钟才能进行配置和控制。GPIOA、GPIOB、和GPIOC分别对应不同的GPIO端口。
这是因为时钟管理是微控制器内部各个模块的重要组成部分,它可以用来控制不同模块的运行时钟。通过启用相应的GPIO端口时钟,你允许这些GPIO端口正常工作,并可以进行配置和控制。
具体来说,通过启用GPIO端口的时钟,你可以:
配置GPIO引脚的工作模式(比如输入、输出、复用功能等)。
控制GPIO引脚的电平状态(点亮LED、控制外部设备)。
使用GPIO引脚来连接外部电路或器件,如传感器、开关等。
监控GPIO引脚的输入状态,以获取外部信息。
因此,启用GPIO端口的时钟是确保你可以正确配置和使用微控制器的GPIO引脚的重要步骤。启用GPIOA、GPIOB和GPIOC端口的时钟,使它们可用于配置和控制对应的A10,B5,C13引脚
③配置引脚为通用推挽输出
GPIO支持4种输入模式(浮空输入、上拉输入、下拉输入、模拟输入)和4种输出模式(开漏输出、开漏复用输出、推挽输出、推挽复用输出)。同时,GPIO还支持三种最大翻转速度(2MHz、10MHz、50MHz)。
每个I/O口可以自由编程,但I/O口寄存器必须按32位字被访问。
1.GPIO_Mode_AIN 模拟输入
模拟输入模式下,I/O端口的模拟信号(电压信号,而非电平信号)直接模拟输入到片上外设模块,比如ADC模块等等
2.GPIO_Mode_IN_FLOATING 浮空输入
浮空输入模式下,I/O端口的电平信号直接进入输入数据寄存器。也就是说,I/O的电平状态是不确定的,完全由外部输入决定;如果在该引脚悬空(在无信号输入)的情况下,读取该端口的电平是不确定的
3.GPIO_Mode_IPD 下拉输入
下拉输入模式下,I/O端口的电平信号直接进入输入数据寄存器。但是在I/O端口悬空(在无信号输入)的情况下,输入端的电平可以保持在低电平;并且在I/O端口输入为高电平的时候,输入端的电平也还是高电平
4.GPIO_Mode_IPU 上拉输入
上拉输入模式下,I/O端口的电平信号直接进入输入数据寄存器。但是在I/O端口悬空(在无信号输入)的情况下,输入端的电平可以保持在高电平;并且在I/O端口输入为低电平的时候,输入端的电平也还是低电平
5.GPIO_Mode_Out_OD 开漏输出
开漏输出模式下,通过设置位设置/清除寄存器或者输出数据寄存器的值,途经N-MOS管,最终输出到I/O端口。这里要注意N-MOS管,当设置输出的值为高电平的时候,N-MOS管处于关闭状态,此时I/O端口的电平就不会由输出的高低电平决定,而是由I/O端口外部的上拉或者下拉决定;当设置输出的值为低电平的时候,N-MOS管处于开启状态,此时I/O端口的电平就是低电平。同时,I/O端口的电平也可以通过输入电路进行读取;注意,I/O端口的电平不一定是输出的电平
6.GPIO_Mode_Out_PP 推挽输出
推挽输出模式下,通过设置位设置/清除寄存器或者输出数据寄存器的值,途经P-MOS管和N-MOS管,最终输出到I/O端口。这里要注意P-MOS管和N-MOS管,当设置输出的值为高电平的时候,P-MOS管处于开启状态,N-MOS管处于关闭状态,此时I/O端口的电平就由P-MOS管决定:高电平;当设置输出的值为低电平的时候,P-MOS管处于关闭状态,N-MOS管处于开启状态,此时I/O端口的电平就由N-MOS管决定:低电平。同时,I/O端口的电平也可以通过输入电路进行读取;注意,此时I/O端口的电平一定是输出的电平
7.GPIO_Mode_AF_OD 复用开漏输出
开漏复用输出模式,与开漏输出模式很是类似。只是输出的高低电平的来源,不是让CPU直接写输出数据寄存器,取而代之利用片上外设模块的复用功能输出来决定的
8.GPIO_Mode_AF_PP 复用推挽输出
推挽复用输出模式,与推挽输出模式很是类似。只是输出的高低电平的来源,不是让CPU直接写输出数据寄存器,取而代之利用片上外设模块的复用功能输出来决定的
Q:为何要设置为推挽输出模式?
点亮LED或控制其他输出设备时,使用通用推挽输出模式是常见的选择,因为这种输出模式提供了许多优点:
-
高驱动能力:通用推挽输出模式能够提供较高的输出电流,可以驱动各种负载,包括LED、继电器、电机和其他外部设备。这意味着它能够轻松地控制不同类型的负载。
-
双向控制:通用推挽输出模式允许引脚的状态既能设为高电平又能设为低电平。这使得你可以轻松地在需要时点亮或熄灭LED,也可以控制其他双向负载。
-
抗干扰能力:通用推挽输出模式对于外部干扰(例如噪声)具有一定的抗干扰能力,因为它在高电平和低电平状态都有一个相对稳定的输出电压。
-
低功耗:与开漏输出模式相比,通用推挽输出模式通常在输出高电平时具有更低的功耗。这有助于在需要时节省电能。
-
多功能性:通用推挽输出模式可以用于多种用途,因此适用于多种应用场景,包括输入捕获、输出比较等。
④ 通过输出高低电平控制LED
电平(Voltage Level)是指电压的状态,通常用于描述数字电路中的信号状态。在数字电路中,通常有两个电平,即高电平(High Level)和低电平(Low Level)。
高电平:通常表示为逻辑1或者是高电压状态,它对应于正电压,通常表示为“1”或“真”。
低电平:通常表示为逻辑0或者是低电压状态,它对应于负电压,通常表示为“0”或“假”。
在数字电路中,通过电压的高低电平来表示和传递信息。例如,通常情况下,LED(Light Emitting Diode,发光二极管)的亮灭状态与控制它的引脚的电平有关。LED通常需要连接到电源(高电平)和地(低电平)来工作,因此控制引脚的高低电平就能控制LED的亮灭。
具体来说,LED是一种半导体器件,当它的两端之间施加电压时,会发光。LED通常是双向极性器件,其中一个引脚是阳极(Anode),另一个引脚是阴极(Cathode)。通过将阳极连接到高电平(正电压)而将阴极连接到低电平(地或负电压),电流会在LED中流动,激活LED并导致其发光。
因此,通过控制连接LED的引脚的高低电平,你可以实现LED的亮灭控制。当引脚处于高电平时,LED亮;当引脚处于低电平时,LED灭。这是数字电路中基本的亮灭控制原理。当然,使灯亮是高电平还是低电平,需要看具体的线路,下文会提到。
一、 基于寄存器点亮LED流水灯
1.创建工程
新建工程
选择对应型号
勾选
新建main.c
添加到Source Group1
2. 编写代码
Ⅰ 代码
先上代码再解释:
#include "stm32f10x.h"
void SysTick_Handler(void) {
// 什么也不做,只是为了满足SysTick中断的要求
}
void delay_s(uint32_t s) {
// 配置SysTick定时器为1毫秒滴答
SysTick_Config(SystemCoreClock / 1000);
// 计算所需的滴答数
uint32_t ticks = s * 1000;
while (ticks) {
if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) {
// 如果SysTick倒计时到0
ticks--;
}
}
// 关闭SysTick定时器
SysTick->CTRL = 0;
}
int main(void) {
// 启用GPIOA、GPIOB和GPIOC时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN;
// 配置引脚为通用推挽输出
GPIOA->CRH |= GPIO_CRH_MODE10 ;
GPIOA->CRH &= ~GPIO_CRH_CNF10;
GPIOB->CRL |= GPIO_CRL_MODE5;
GPIOB->CRL &= ~GPIO_CRL_CNF5;
GPIOC->CRH |= GPIO_CRH_MODE13;
GPIOC->CRH &= ~GPIO_CRH_CNF13;
while (1) {
// 点亮A10引脚,同时熄灭 B5, PC13引脚
GPIOA->BSRR = GPIO_BSRR_BS10;
GPIOB->BSRR = GPIO_BSRR_BR5;
GPIOC->BSRR = GPIO_BSRR_BS13;
delay_s(1); // 延时一秒
// 点亮B5引脚,同时熄灭A10, PC13引脚
GPIOB->BSRR = GPIO_BSRR_BS5;
GPIOA->BSRR = GPIO_BSRR_BR10;
GPIOC->BSRR = GPIO_BSRR_BS13;
delay_s(1); // 延时一秒
// 点亮PC13引脚,同时熄灭A10, B5引脚
GPIOC->BSRR = GPIO_BSRR_BR13;
GPIOB->BSRR = GPIO_BSRR_BR5;
GPIOA->BSRR = GPIO_BSRR_BR10;
delay_s(1); // 延时一秒
}
return 0;
}
Ⅱ 算法思路解释
①延时函数:
这段代码是用于实现毫秒级别的延迟功能,基于STM32中的SysTick定时器。以下是对这段代码的详细解释:
void SysTick_Handler(void): 这是SysTick定时器的中断处理函数。在这个特定的实现中,这个函数实际上是一个空函数,什么也不做。它的存在是为了满足SysTick中断的要求,因为SysTick定时器在每个滴答(tick)结束时都会引发一个中断,而中断处理函数必须存在,即使它不执行任何操作。
void delay_s(uint32_t s): 这个函数用于实现以秒为单位的延迟。它的功能如下:
首先,它配置SysTick定时器为1毫秒滴答。通过 SysTick_Config 函数,它将SysTick定时器的时钟频率设置为系统核心时钟频率(SystemCoreClock)除以1000,以便每毫秒引发一次SysTick中断。
接着,函数计算所需的滴答数(ticks),通过将所需的秒数(s)乘以1000来得到。这是因为每秒有1000毫秒,所以将秒数转换为毫秒级别。
进入一个while循环,检查SysTick定时器是否到达0。SysTick定时器的倒计时从一个大整数开始,每经过一个滴答(tick),倒计时减小1。
如果SysTick->CTRL 寄存器中的COUNTFLAG位被置位(指示定时器已到达0),则减小 ticks 变量的值。
循环会一直运行,直到 ticks 变为0,即所需的延迟时间已经经过。
最后,函数关闭SysTick定时器,通过将SysTick->CTRL 寄存器设置为0,以停止SysTick定时器的计数。
这个delay_s 函数允许你在程序中添加毫秒级别的延迟,它使用SysTick定时器来实现,因此延迟时间是相对精确的,不受其他代码的影响。需要注意的是,具体的延迟时间可能会受到系统核心时钟频率的影响
② 设置推挽输出
GPIOA->CRH |= GPIO_CRH_MODE10;: 这一行代码设置GPIOA端口的控制寄存器高字(Control Register High)中与引脚A10对应的位,以配置A10引脚的输出模式。具体来说,它执行了按位或运算,将 GPIO_CRH_MODE10 定义的位设置为1。
GPIO_CRH_MODE10 是一个宏,它表示要配置的引脚的输出模式。通常,这里的 MODE 位是2位字段,用于指定引脚的输出模式。在这种情况下,MODE10 代表A10引脚。通过将其设置为1,你指示该引脚应该被配置为通用推挽输出模式。
GPIOA->CRH &= ~GPIO_CRH_CNF10;: 这一行代码用于清除GPIOA端口的控制寄存器高字中与引脚A10对应的CNF位。具体来说,它执行了按位与非运算,将 GPIO_CRH_CNF10 定义的位设置为0。
GPIO_CRH_CNF10 是一个宏,它表示要配置的引脚的控制寄存器配置模式。通常,这里的 CNF 位是2位字段,用于指定引脚的配置模式。通过将其设置为0,你指示该引脚应该被配置为通用推挽输出模式的配置模式
③ 控制引脚高低电平
当使用STM32微控制器的GPIO引脚控制LED或其他外部设备时,你可以通过直接操作寄存器来控制引脚的电平状态。具体来说:
GPIO[A\B\C]->BSRR = GPIO_BSRR_BS[引脚序号] 用于将引脚置为高电平。
GPIO[A\B\C]->BSRR = GPIO_BSRR_BR[引脚序号] 用于将引脚置为低电平。
根据LED与引脚连接方式不同,决定了控制LED亮起的高低电平选择不同:
①若LED正极连接引脚,则高电平为亮
②若LED负极连接引脚,则低电平为亮,此时LED正极需要连接电源
③PC13引脚需要输出低电平才能使内置灯亮起
3. 测试
将代码编译烧录进stm32中,运行效果如图:
二、通过标准库函数控制LED流水灯
1. 准备环境
Ⅰ 下载并配置模板
① 下载
点击链接进入官网下载:
https://www.st.com/zh/embedded-software/stm32-standard-peripheral-libraries.html
选择型号:
这里我选择F1
选择下载版本,这里我选择get latest,最新版本
点击接受:
初次下载会填写个人信息,邮箱填真实的就行,验证后通过发送到邮箱的链接即可下载
②配置
解压下载好的压缩包:
新建一个文件夹,然后在其中新建以下四个文件夹:
然后根据表格复制粘贴文件到对应文件夹里:
文件路径 | 文件名 | 目标路径 |
---|---|---|
.\STM32F10x_StdPeriph_Lib_V3.6.0\Libraries\CMSIS\CM3\CoreSupport | core_cm3.c、core_cm3.h | Core |
.\STM32F10x_StdPeriph_Lib_V3.6.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm | 根据型号选择,本次选择’startup_stm32f10x_md.s’ | Core |
.\STM32F10x_StdPeriph_Lib_V3.6.0\Libraries\STM32F10x_StdPeriph_Driver | inc和src两个文件夹 | Lib |
.\STM32F10x_StdPeriph_Lib_V3.6.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x | stm32f10x.h、system_stm32f10x.c、system_stm32f10x.c.h | User |
.\STM32F10x_StdPeriph_Lib_V3.6.0\Project\STM32F10x_StdPeriph_Template | main.c(也可以自己创建)、stm32f10x_conf.h、stm32f10x_it.c、stm32f10x_it.h | User |
至此一个项目模板配置好了
Ⅱ. 新建项目
① 创建一个项目的文件夹,将配置好的模板的所有文件夹粘贴到该文件夹下
② 创建项目
在KEIL中新建项目,把项目路径选择到User文件夹内
T
选择F103C8T6
不勾选任何一项,直接点ok
④ 配置项目环境
点击"品"字按钮
把Groups栏的内容修改为模板的文件夹名称
然后每个group按照下图添加文件:
点击魔法棒,在C/C++栏中更改如下:
STM32F10X_MD,USE_STDPERIPH_DRIVER
注意STM32F10X_MD的MD是根据选择的启动文件设置的,本次选择的是startup_stm32f10x_md.s,所以后缀是MD
然后在main函数里简单输入一点代码编译测试一下
#include "stm32f10x.h"
int main(void)
{
}
运行后发现报四个错
本人并没有分析错误的根本原因,而是根据错误定位到发生错误的文件,并把文件中报错词条删除掉,再编译便不会再次报错
2. 编写代码
Ⅰ 上代码
#include "stm32f10x.h"
// 定义LED连接的引脚
#define LED_A10 GPIO_Pin_10
#define LED_B5 GPIO_Pin_5
#define LED_C13 GPIO_Pin_13
void Delay(uint32_t nCount) {
for (uint32_t i = 0; i < nCount; i++) {
__NOP();
}
}
void GPIO_Init_LEDs(void) {
// 使能相应的GPIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
// 初始化A10引脚
GPIO_InitStruct.GPIO_Pin = LED_A10;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 初始化B5引脚
GPIO_InitStruct.GPIO_Pin = LED_B5;
GPIO_Init(GPIOB, &GPIO_InitStruct);
// 初始化C13引脚
GPIO_InitStruct.GPIO_Pin = LED_C13;
GPIO_Init(GPIOC, &GPIO_InitStruct);
}
int main(void) {
// 初始化系统时钟等设置
GPIO_Init_LEDs();
while (1) {
// 依次点亮LED灯
//亮A10
GPIO_SetBits(GPIOA, LED_A10);
GPIO_ResetBits(GPIOB,LED_B5);
GPIO_SetBits(GPIOC,LED_C13);
Delay(2000000);
//亮B5
GPIO_SetBits(GPIOB,LED_B5);
GPIO_ResetBits(GPIOA, LED_A10);
GPIO_SetBits(GPIOC,LED_C13);
Delay(2000000);
//亮C13
GPIO_ResetBits(GPIOB,LED_B5);
GPIO_ResetBits(GPIOA, LED_A10);
GPIO_ResetBits(GPIOC,LED_C13);
Delay(2000000);
}
}
Ⅱ 解释
①延时函数
与上一段代码原理相似,都是重复运行无功能的空代码循环,以做到延时效果
② 使能GPIO时钟
定义了一个名为 GPIO_Init_LEDs 的函数,它的目的是初始化控制LED的GPIO引脚。以下是代码的逐行解释:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE);
这一行代码用于启用与GPIOA、GPIOB和GPIOC端口相关的时钟。在STM32微控制器中,时钟管理是非常重要的,要想使用特定的GPIO端口,首先必须启用相应的时钟。这行代码通过将三个时钟位的逻辑或操作(|)来同时启用这三个GPIO端口的时钟。
GPIO_InitTypeDef GPIO_InitStruct;
这一行代码定义了一个名为 GPIO_InitStruct 的结构体变量,用于配置GPIO引脚的初始化参数。这个结构体包含了一些配置选项,如GPIO引脚的模式、速度等。
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
:这一行代码将 GPIO_Mode 字段设置为 GPIO_Mode_Out_PP,表示要配置这些引脚为通用推挽输出模式。通用推挽输出模式适用于控制LED等需要点亮和熄灭的外部设备。
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
这一行代码将 GPIO_Speed 字段设置为 GPIO_Speed_50MHz,表示引脚的输出速度为50MHz。这通常是一个合适的设置,可以提供足够的输出速度来控制LED等外部设备。
- 接下来的三段代码用于分别初始化A10、B5和C13引脚。每段代码都将 GPIO_InitStruct 结构体的 GPIO_Pin 字段设置为相应的LED引脚(LED_A10、LED_B5、LED_C13),然后通过调用 GPIO_Init 函数来将这些配置应用到相应的GPIO端口(GPIOA、GPIOB、GPIOC)。这样就完成了对这些引脚的初始化。
③ 控制引脚高低电平
GPIO_SetBits(GPIO[A/B/C], LED_[引脚序号,如A10]);//高电平
GPIO_ResetBits(GPIO[A/B/C], LED_[引脚序号,如A10]);//低电平
3. 测试
4. 分析
LED的亮/灭周期主要受到以下因素的影响:
程序中的延时函数:代码使用了自定义的延时函数 Delay 和 delay_s 来实现LED的亮/灭控制。这些延时函数的准确性决定了LED的闪烁周期。
系统时钟频率:程序中使用的 SystemCoreClock 决定了延时函数中的延时时间单位。系统时钟频率的设置在程序启动时由芯片的时钟配置决定。更高的时钟频率可以提供更精确的延时。
其他中断和处理:在实时系统中,可能会有其他中断和处理程序运行,它们可能会影响LED的闪烁周期。这些中断的优先级和执行时间可能会导致延时的变化
使用软件仿真逻辑分析仪分析
① 点击魔法棒,按照图示进行修改
② 点击调试按钮
③ 选择逻辑分析仪
熟悉窗口:
④ 选择实时显示波形
⑤ 点击setup,输入一个引脚进行监听
⑥ 修改参数
⑦ 运行
记得鼠标移动至表格内,然后滑动滚轮进行缩放调整,调整到合适grid的大小再进行观测
可以看到A10引脚的周期大致为1.5秒,即高电平0.5秒,而后低电平1秒。所以得知程序中的一个灯的点亮时间实际上是0.5秒,修改代码中Delay函数的参数后再进行观测:
可以看到现在A10的周期为3秒(高电平1秒,低电平2秒)
我们进一步观察其余的B5,C13引脚:
参考链接: