龙空技术网

STM32花式点流水灯教程

沃爱单片机 688

前言:

如今小伙伴们对“单片机流水灯c语言程序”大概比较看重,小伙伴们都需要分析一些“单片机流水灯c语言程序”的相关知识。那么小编同时在网络上汇集了一些有关“单片机流水灯c语言程序””的相关内容,希望看官们能喜欢,小伙伴们快快来了解一下吧!

前言

本文是简要介绍一下不同方式实现流水灯,比较不同方式下的异同。

以STM32最小系统核心板(STM32F103C8T6)+面板板+3只红绿蓝LED 搭建电路,使用GPIOA、GPIOB、GPIOC这3个端口控制LED灯,轮流闪烁,间隔时长1秒。

在这里我们采用GPIOA_Pin_12、GPIOB_Pin_1、GPIOC_Pin_14分别控制红、绿、蓝LED灯。

一、固件库流水灯(一)新建工程

因为要使用ST固件库,这部分配置较为繁琐,具体可参考网上一些配置的流程

stm32f103c8t6工程模板的建立

我配置的工程文件模板以及参考手册也放在后面,可供参考

(二)配置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引脚的不同组合方式有多种下载方式

关于使用串口下载,可参考

STM32串口下载程序:

在下载完成时,保持最小系统板不断电的情况下,将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位的数据变为0GPIOB_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 0x40021018GPIOA_CRH EQU   0x40010804GPIOA_ODR EQU   0x4001080C                                    GPIOB_CRL EQU   0x40010C00    ;寄存器映射GPIOB_ODR EQU   0x40010C0C		GPIOC_CRH EQU   0x40011004GPIOC_ODR EQU   0x4001100C			Stack_Size      EQU     0x00000400                AREA    STACK, NOINIT, READWRITE, ALIGN=3Stack_Mem       SPACE   Stack_Size__initial_sp                AREA    RESET, DATA, READONLY__Vectors       DCD     __initial_sp                               DCD     Reset_Handler                                                                      AREA    |.text|, CODE, READONLY                                    THUMB                REQUIRE8                PRESERVE8                                    ENTRYReset_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=3Stack_Mem       SPACE   Stack_Size__initial_sp                AREA    RESET, DATA, READONLY__Vectors       DCD     __initial_sp                               DCD     Reset_Handler                                                                      AREA    |.text|, CODE, READONLY                                    THUMB                REQUIRE8                PRESERVE8                                    ENTRYReset_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}    

这部分采用的是软件延时,具体原理为R0R1R2初始化为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参考手册》

资料链接

链接: 提取码:i4wl

标签: #单片机流水灯c语言程序 #c语言流水灯