前言:
如今小伙伴们对“单片机流水灯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);#endifled.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}
这部分采用的是软件延时,具体原理为R0、R1、R2初始化为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语言流水灯