从底层到应用:STM32F103C8T6 LED 流水灯双实现(寄存器 + 固件库)与仿真验证

一、基于固件库操作的LED流水灯

1.搭建工程环境

在keil中创建新的工程(建议单独放在一个文件夹里)

请添加图片描述

找到对应的芯片型号

请添加图片描述

往文件夹里添加启动文件

请添加图片描述

请添加图片描述

创建main函数

请添加图片描述

请添加图片描述

至此,工程创建完成。

2.在main.c函数中添加运行代码

(1)寄存器的定义

寄存器是存储代码的硬件,由多个具有存储器组合而成。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成。

(2)STM32中的GPIO口

GPIO(general porpose intput output):通用输入输出端口的简称。可以通过软件控制其输出和输入。stm32芯片的GPIO引脚与外部设备连接起来,从而实现与外部通信,控制以及数据采集的功能。

Cortex‐M3 支持4GB 存储空间。整块4G存储器开始地址标为0x0000_0000,结束地址为0xFFFF_FFFF,地址的位数是32位,那么2^32=4,294,967,296。
由于一个基本的存储单元是8bits即1Byte(每个地址对应一个存储单元,这样如果只是访问某一bit就要使用位操作,或者使用位带操作),因此4,294,967,296/1024=4,194,304KB,4,194,304/1024=4096MB,4094/1024=4GB。
这4GB的存储空间被划分成8个块,每一块用来与特定功能完成映射。

每个寄存器都是32bit,占用4个Byte即4个存储单元。可以把寄存器看作一个特殊的单元,一个这样的单元占32bit,只要找到这个单元的起始地址就可以对其进行操作。

其映射地址 = 外设总基地址(块基地址)+ 总线相对于外设总基地址的偏移 + 具体外设基地址相对于总线基地址的偏移 + 寄存器相对于具体外设基地址的偏移。

STM32 可控制的引脚

请添加图片描述

各种模式的原理图

请添加图片描述

(3)配置操作

a.配置时钟使能。

因为流水灯要操作的引脚都是在GPIO端口的,所以根据系统结构图,属于AHB总线,所以所要用的端口的复位和时间控制都受RCC控制

再看寄存器组起始地址表,可以看到RCC的地址范围,且可以看到要控制的寄存器都是在APB2总。

从上面发现复位和时钟控制的起始地址为0x4002 1000,

请添加图片描述

请添加图片描述

再翻到这里发下偏移量为0x18,所以该寄存器的地址为0x4002 1018

上图表格表示的寄存器里各位的含义,比如第三位也就是2那个位置为1时,就是GPIOA的时钟开启了。

#define RCC_AP2ENR  *((unsigned volatile int*)0x40021018) #时钟使能寄存器
RCC_AP2ENR|=1<<2;     //开启APB2-GPIOA外设时钟使能

b.配置端口配置寄存器
可以发现上面的时钟使能寄存器开启时钟是针对一个区域的,并不能确定引脚,而这个寄存器就是确定引脚的,端口配置寄存器有两个,分别为端口配置低寄存器(CRL)和端口配置高寄存器(CRH)。
每四位配置一个端口,如11 01,11就是选择开启功能,01就是选择模式和确定最大速度,但有一点不一样,低寄存器的偏移地址为0x00,高寄存器的偏移地址为0x04
请添加图片描述

以PA7为示例,相应端口配置器GPIOA_CRL地址为GPIOA的基址+偏移量

偏移量如下

请添加图片描述

GPIOA的地址为0x40010800, 低寄存器偏移如上图所示为0

请添加图片描述

以PA7口为输出口为例,就是要改变的是红色方框里的内容,
根据它下方表格里的内容得知要将他设置为推挽输出,输出模式最大速度为2MHz就是将它赋为0010,转换进制就是2,所以即将地址0x40010800的最前头一位赋为2,代码如下

#define GPIOA_CRL *((unsigned volatile int*)0x40010800)
GPIOA_CRL=0x20000000;   //PA7推挽输出,2Mhz

c.配置端口输出寄存器(ORD)

可以看到偏移量为0xc,所以该寄存器的地址等于端口的基址加上偏移量,在相应的位赋值可以控制输出电压,0为低电压,1为高电压,以pa7引脚为例子,想要输出高电压,就需要在第八位赋1。

#define GPIOA_ORD *((unsigned volatile int*)0x4001080C)
GPIOA_ORD|=1<<7;      //设置初始灯为亮
(4)代码
使用A7、B9、C15端口
//--------------APB2使能时钟寄存器------------------------APB2使能时钟寄存器。通过这个寄存器可以开启或关闭连接到APB2总线的外设的时钟。
#define RCC_AP2ENR *((unsigned volatile int*)0x40021018)
//GPIOA_CRL、GPIOA_ORD:GPIO的配置寄存器和输出数据寄存器。通过这些寄存器可以配置GPIO端口的工作模式和设置端口的输出状态。
//----------------GPIOA配置寄存器 ------------------------
#define GPIOA_CRL *((unsigned volatile int*)0x40010800)
//----------------GPIOA输出寄存器 ------------------------
#define GPIOA_ORD *((unsigned volatile int*)0x4001080C)
//----------------GPIOB配置寄存器 ------------------------
#define GPIOB_CRH *((unsigned volatile int*)0x40010C04)
//----------------GPIOB输出寄存器 ------------------------
#define GPIOB_ORD *((unsigned volatile int*)0x40010C0C)
//----------------GPIOC配置寄存器 ------------------------
#define GPIOC_CRH *((unsigned volatile int*)0x40011004)
//----------------GPIOC输出寄存器 ------------------------
#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++);
}

//A7高电平,B9、C15低电平 将GPIOA的第7位(A7)设置为高电平,同时将GPIOB的第9位(B9)和GPIOC的第15位(C15)设置为低电平。这通常用于点亮连接到A7的LED,同时熄灭连接到B9和C15的LED。
void A_LED_LIGHT(){
 GPIOA_ORD=0x0<<7;  
 GPIOB_ORD=0x1<<9;  
 GPIOC_ORD=0x1<<15;  
}
//B9高电平,A7、C15低电平
void B_LED_LIGHT(){
 GPIOA_ORD=0x1<<7;  
 GPIOB_ORD=0x0<<9;  
 GPIOC_ORD=0x1<<15;  
}
//C15高电平,A7、B9低电平
void C_LED_LIGHT(){
 GPIOA_ORD=0x1<<7;  
 GPIOB_ORD=0x1<<9;  
 GPIOC_ORD=0x0<<15;  
}


int main()
{
 int j=100;
	
//使能GPIOA、GPIOB、GPIOC端口时钟
 RCC_AP2ENR|=1<<2;   
 RCC_AP2ENR|=1<<3;   
 RCC_AP2ENR|=1<<4;   
	
//推挽输出模式,并设置初始状态为熄灭(高电平)。
 GPIOA_CRL&=0x0FFFFFFF;  //清除该位原来的设置
 GPIOA_CRL|=0x20000000;  //A7推挽输出,2Mhz
 GPIOA_ORD|=0x1<<7;        //设置初始灯为灭


 GPIOB_CRH&=0xFFFFFF0F;  //清除该位原来的设置
 GPIOB_CRH|=0x00000020;  //B9推挽输出,2Mhz
 GPIOB_ORD|=0x1<<9;        //设置初始灯为灭


 GPIOC_CRH&=0x0FFFFFFF;  //清除该位原来的设置
 GPIOC_CRH|=0x20000000;  //C15推挽输出,50Mhz
 GPIOC_ORD|=0x1<<15;     //设置初始灯为灭
 
 while(j)
 { 
  A_LED_LIGHT(); 
  Delay_ms(10000);
  B_LED_LIGHT();
  Delay_ms(10000);
  C_LED_LIGHT();
  Delay_ms(10000);
 }
}

运行效果

F103C8T6基于寄存器与固件库的LED流水灯

二、基于寄存器操作的LED流水灯

1.工程建立 添加工程必要文件

添加启动文件,复制到工程模板(新建Start)

请添加图片描述

添加外设寄存器描述文件

请添加图片描述

回到STM32F10x(复制那三个文件stmxxxh、systemxxx.c、systemxxx.h)复制到Start,打开CM3->CoreSupport(将两个文件复制到Start)回到Keil将文件添加到工程->点击Target1,将Source Group1单击改名Start->右键选择添加已存在文件,打开Start文件夹,打开筛选器All files,添加启动文件(视频所用型号添加后缀md.s文件)和剩下所有.c.h文件

请添加图片描述

在工程选项添加该文件夹头文件路径(魔术棒按钮,C/C++,Include Paths栏点右边三个点按钮新建路径再点右三个点按钮添加Start路径,最后点ok)

请添加图片描述

打开新建工程文件夹添加新文件夹User(放main函数)

请添加图片描述

接着keil里Target右键新建(组别)改名User添加文件选.c名叫main(注意路径选择User文件夹)

请添加图片描述

在main里右键插入头文件stm32f10x.h(若用寄存器开发32,到此就完成工程建立) 扳手里Encoding->UTF-8可以防止中文乱码

运行时报错

请添加图片描述

解决参考:https://blog.csdn.net/qq_40836442/article/details/109560754

3.添加libraries文件夹,并且加到工程中

请添加图片描述

在User文件夹下粘贴(keil中同步)

请添加图片描述

使其包含标准外设库

请添加图片描述

路径添加完整

请添加图片描述

2、 LED流水灯

(1)代码

通过查看各个固件文件,查找函数完成对各个接口的操作,完成一下代码。

//Delay.c
#include "stm32f10x.h"

/**
  * @brief  微秒级延时
  * @param  xus 延时时长,范围:0~233015
  * @retval 无
  */
void Delay_us(uint32_t xus)
{
	SysTick->LOAD = 72 * xus;				//设置定时器重装值
	SysTick->VAL = 0x00;					//清空当前计数值
	SysTick->CTRL = 0x00000005;				//设置时钟源为HCLK,启动定时器
	while(!(SysTick->CTRL & 0x00010000));	//等待计数到0
	SysTick->CTRL = 0x00000004;				//关闭定时器
}

/**
  * @brief  毫秒级延时
  * @param  xms 延时时长,范围:0~4294967295
  * @retval 无
  */
void Delay_ms(uint32_t xms)
{
	while(xms--)
	{
		Delay_us(1000);
	}
}
 
/**
  * @brief  秒级延时
  * @param  xs 延时时长,范围:0~4294967295
  * @retval 无
  */
void Delay_s(uint32_t xs)
{
	while(xs--)
	{
		Delay_ms(1000);
	}
} 


//Delay.h
#ifndef __DELAY_H
#define __DELAY_H

void Delay_us(uint32_t us);
void Delay_ms(uint32_t ms);
void Delay_s(uint32_t s);

#endif


//main.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
int main(void)
{

	//设置时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
	
	//定义结构体
	GPIO_InitTypeDef GPIO_InitStructure;
	//将输出模式设置为推挽输出
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	//输出点为a0
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;
	//或使用GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 |GPIO_Pin_7;
	//输出速度为50HZ
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	//以上完成结构体GPIO_InitStructure的参数配置
	
	//用结构体完成对GPIO的配置
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	GPIO_Init(GPIOB,&GPIO_InitStructure);
    GPIO_Init(GPIOC,&GPIO_InitStructure);
	while(1)
	{
		//GPIO_WritBit,对单个端口进行写入、消除控制,
		//使用GPIOx,GPIO_Pin_0-16分别表示x0至x16
		//Bit_RESET为亮灯,Bit_SET为熄灭
		
	    GPIO_WriteBit(GPIOA,GPIO_Pin_7,Bit_RESET);
		GPIO_WriteBit(GPIOB,GPIO_Pin_9,Bit_SET);
		GPIO_WriteBit(GPIOC,GPIO_Pin_15,Bit_SET);
		Delay_ms(500);
		
		GPIO_WriteBit(GPIOA,GPIO_Pin_7,Bit_SET);
		GPIO_WriteBit(GPIOB,GPIO_Pin_9,Bit_RESET);
		GPIO_WriteBit(GPIOC,GPIO_Pin_15,Bit_SET);
		Delay_ms(500);
		
		GPIO_WriteBit(GPIOA,GPIO_Pin_7,Bit_SET);	
		GPIO_WriteBit(GPIOB,GPIO_Pin_9,Bit_SET);	
		GPIO_WriteBit(GPIOC,GPIO_Pin_15,Bit_RESET);
		Delay_ms(500);
		
	}
}

运行效果:LED灯的亮/灭周期为1000毫秒(1秒)

F103C8T6基于寄存器与固件库的LED流水灯

3、使用Keil的软件仿真逻辑分析仪功能观察管脚的时序波形

使用keil的软件仿真功能中的逻辑分析仪查看波形,从而更快地定位到问题所在,进而解决问题。

(1)首先,设置options for target

Debug页的设置:

请添加图片描述

(2)点击Debug,进入调试界面

请添加图片描述

(3)选择逻辑分析仪

请添加图片描述

(4)选择要观察的引脚
①点击Setup Logic Analyzer

请添加图片描述

②添加要观察的引脚

在新建的时候直接输入 PORTA.8 代表PA8口,输入完之后按回车键,软件会自动变成位定义。

请添加图片描述

(5)运行程序

请添加图片描述

(6)观察波形,把光标移动到逻辑分析仪显示波形的区域,上下滚动滑轮,就可以放大和缩小波形:

仿真效果:红色为PA7 绿色为PB9 紫色为PC15 可以看出与预期效果一致,与上板实际效果也一致

高低电平转换周期实际为1s

请添加图片描述

三、心得体会

实验心得体会详细分析

对照实验图完成 STM32F103C8T6 LED 流水灯实验后,从硬件接线到软件实现的全流程操作,让我在硬件认知、编程选择和问题排查上有了更具象的收获。​

从实验图能清晰看到,LED 需通过限流电阻分别连接 GPIOA、GPIOB、GPIOC 指定引脚(如 PA7、PB9、PC15),这也让寄存器操作的底层逻辑更易理解:需先牢记对应 GPIO 基地址(如 GPIOA 为 0x40010800),通过 RCC 寄存器开启对应端口时钟,再用 GPIO 配置寄存器(如 PA7 对应 CRL 寄存器)设为推挽输出模式,每步都要手动计算位操作值 —— 虽繁琐,但看着图中 LED 按代码逻辑轮流点亮,更真切体会到 “软件控硬件即读写寄存器” 的本质。​

固件库的高效则在对照实验图编程时更明显:不用记地址,只需按图中 LED 对应的引脚,在GPIO_InitTypeDef结构体中设置GPIO_Pin(如 PA7)、GPIO_Mode(推挽输出)等参数,调用GPIO_Init函数即可完成配置。两者对比清晰:若实验图中需微调硬件时序(如更快响应),寄存器方式更精准;若想快速实现图中流水效果,固件库更高效,需依需求选择。​

实验中踩的坑也与实验图关联紧密:曾因没注意实验图中 PB3 引脚的标注(默认复用功能),导致对应 LED 不亮,查手册修正后才解决;用 Keil 仿真时,对照实验图预期的流水周期,通过逻辑分析仪观察波形(如图中 请添加图片描述
A7 引脚低电平持续时间),发现软件延时有偏差,才意识到需用定时器实现精准延时;固件库编译报错时,也是对照图中工程文件结构,发现漏定义USE_STDPERIPH_DRIVER宏,补充后才通过 —— 这让我更重视实验图细节、硬件手册和工程配置的关联性。​

最终明白,嵌入式开发要 “图、理、码结合”:既要对照实验图理清硬件连接(如限流电阻作用、引脚对应关系),懂 GPIO、时钟等原理,也要善用 Keil 逻辑分析仪验证图中预期效果,在精准(寄存器)与高效(固件库)间找平衡,为后续按图开发更复杂外设打牢基础。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐