【STM32F103笔记】二、单片机中的HelloWorld——流水灯

单片机做为一种微控制器,最基本的用途即是经过其引脚与外界进行交互,而在单片机编程界,有这么一个程序,堪称单片机中的HelloWorld,不只能够熟悉单片机的引脚控制,更能对单片机的时钟进行深刻了解,那就是几乎全部单片机教程中都会提到的——流水灯html

在上一篇中咱们已经搭建好了STM32开发环境,点亮了第一个LED灯,这一篇将从电路原理分析开始,对流水灯的控制原理电路参数设计,STM32F103引脚时钟的设置进行介绍。web

流水灯电路设计

顾名思义,流水灯就是像水流同样,依次点亮的一组灯。设计流水灯为间隔固定的时间每次点亮一个LED,所以在点亮下一个LED的同时,要关闭上一个LED,而且进行计时,以循环下去。编程

在上一篇中咱们用单片机的GPIOB8引脚点亮了最小系统板上自带的一个LED,根据其电路原理图,能够设计流水灯的电路原理。app

电路原理图

根据最小系统板的引脚分布,选择GPIOA2~GPIOA七、GPIOB八、GPIOB9共8个引脚来控制流水灯(这8个引脚在笔者的系统板同一侧且相邻,用杜邦线链接整齐点o( ̄︶ ̄)o),电路原理图和实物图以下:
在这里插入图片描述ide

电路参数设计分析

这里实物是本身焊的小板子,用的1K Ω \Omega 的排阻做为限流电阻,选的白发绿LED:svg

驱动电压 2.8~3.3V
电流 5~18mA
导通压降 1.2~2V

查阅STM32F103C8T6手册,引脚电压电流范围:
在这里插入图片描述
VIN表示引脚输入电平的范围,有5V电平耐受能力的引脚能够达5.5V,这里PA2~PA7(PA-GPIOA)不具备5V电平耐受能力(也在手册中能找到,引脚定义表里标记FT即能耐受5V电平)。
IIO表示引脚输入输出电流的范围,均为25mA。函数

(下面这一段其实能够略过不看)
当Vcc使用3.3V供电时,选用限流电阻为1K Ω \Omega 计算极端状况下:
LED导通压降VD=1.2V: I = ( V c c V D ) / R = 2.1 m A I=(V_{cc}-V_D)/R=2.1mA ,引脚端电压为2.1V
LED导通压降VD=2.0V: I = ( V c c V D ) / R = 1.3 m A I=(V_{cc}-V_D)/R=1.3mA ,引脚端电压为1.3V
当Vcc使用5.0V供电时,选用限流电阻为1K Ω \Omega 计算极端状况下:
LED导通压降VD=1.2V: I = ( V c c V D ) / R = 3.8 m A I=(V_{cc}-V_D)/R=3.8mA ,引脚端电压为3.8V
LED导通压降VD=2.0V: I = ( V c c V D ) / R = 3.0 m A I=(V_{cc}-V_D)/R=3.0mA ,引脚端电压为3.0V
因为STM32F103C8T6的引脚GPIOA2~ GPIOA7不能能耐受5V电平的引脚,所以,输入电压上限是VDD+0.3V=3.6V,为了保险起见,选取3.3V供电,虽然LED工做电流有点小,可是能亮就好了不是。(固然,选取5V供电其实问题不大,可是实际工业应用中应该避免器件超出规定的使用条件,固然也不能让器件在不知足工做条件的状况下使用,好比上述的LED电流,实际上是笔者没有找到其余合适阻值的限流电阻了Orz,也就是说,应该设计好电路参数再选择器件)。性能

所以,经过上述分析,选取3.3V电压做为流水灯的供电电压,至于LED的工做电流太小,其实问题不大,咱们这里要求的是LED能亮就能够,对亮度木有要求。ui

时钟及GPIO分析

控制流水灯正常运行,须要对8个LED的控制引脚进行设置,并按固定时间对其输出进行控制。因为时钟是单片机运行的基础,首先对时钟进行分析。spa

STM32F103时钟分析

首先在STM32手册中找到时钟树,以下图,沿图中红色直线从左至右看:
在这里插入图片描述
OSC_OUT和OSC_IN就是STM32的外部时钟输入,范围是4-16MHz,这里最小系统板用的是8MHz的晶振,下面的OSC32_OUT和OSC32_IN适用于32KHz的时钟输入,暂时不用考虑;

而后遇到分频器PLLXTPRE(HSE divider for PLL entry),能够对输入的时钟进行2分频(即频率除以2)或不分频;

而后进入PLLSRC(PLL entry clock source),这个是选择时钟来源
为HSI(内部高速时钟High Speed Internal clock)或者HSE(High Speed External clock);

而后进入倍频器PLLMUL(PLL multiplication factor),对时钟频率进行倍频(乘上一个因数);

而后进入SW(System clock switch),能够选择系统时钟来源;

而后进入预分频器AHB Prescaler(Advanced High performance Bus,即高级高性能总线时钟的预分频器);从AHB预分频器出来的时钟信号再分给其余外设;

好比进入APB1 Prescaler(Advanced Peripheral Bus,即高级外设总线时钟的预分频器),能够提供给挂载在APB1总线上的外设;进入APB2 Prescaler能够提供给挂载在APB2总线上的外设。

上述提到的PLLXTPREPLLSRCPLLMULSWAHB PrescalerAPB1 PrescalerAPB2 Prescaler等相关器件都有相应的寄存器进行控制,感兴趣的朋友能够在STM32手册中找到相关寄存器的说明,这里就略过啦啦啦(~ ̄▽ ̄)~。

GPIO分析

GPIO即General-purpose IOs,通用输入输出引脚。STM32F103C8T6共有48个引脚,其中一部分是电源、时钟输入、启动方式、复位等特殊功能的引脚,剩下的引脚又可分为通用输入输出引脚GPIOs和复用功能输入输出引脚AFIOs,这些引脚均可以:
经过Port configuration register(端口配置寄存器)进行功能配置;
经过Port input data register(端口输入数据寄存器),读取各个引脚输入数据(高低电平);
经过Port output data register(端口输出数据寄存器),向外部输出数据(高低电平);
经过Port bit set/reset register(端口位设置/清除寄存器),对端口的某个数据位(至关于某一引脚)进行清0或置1;
经过Port bit reset register(端口位清除寄存器),对端口的某个数据位进行清0。

固然,上面全部的寄存器均可以经过调用库函数来进行设置,实现GPIO的控制功能。

再看到下面STM32手册中的系统结构图,在右下部分能够看到全部GPIO都挂载在APB2总线上,所以在使用GPIO时,须要先初始化APB2总线也就是初始化APB2总线的时钟,固然,这也是调用库函数就能够实现的:
在这里插入图片描述

程序设计

新建一个LED流水灯文件夹,并复制一份工程模板到文件夹下,打开工程文件,进入Keil uVision5。
在这里插入图片描述

时钟设置分析

首先对启动文件startup_stm32f10x_hd.s中关于系统时钟设置的相关内容进行分析。打开startup_stm32f10x_hd.s文件:

第1-33行:文件说明注释;
第35-145行:堆栈以及中断向量地址的设置;

从147行为复位中断向量标号,便可以认为STM32在上电或复位后跳转到标号位置运行:

; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit
                BLX     R0               
                LDR     R0, =__main
                BX      R0
                ENDP

148行:输出Reset_Handler标号,这样其它文件也可使用这个标号来进行跳转;
149行:导入__main标号,能够认为是导入main函数所在地址的标号;
150行:导入SystemInit标号;
151-152行:设置寄存器R0的值为SystemInit标号表明的地址,而后跳转到这个地址运行,即调用SystemInit函数
153行开始运行main函数。
startup_stm32f10x_hd.s文件的分析能够参考:专家揭秘:STM32启动过程全解

经过分析,在上电或者复位后,首先调用SystemInit函数,而后再进入main函数继续运行,所以先分析SystemInit函数,在SystemInit标号上右键->Go To Definition of ‘SystemInit’,进入SystemInit函数(位于system_stm32f10x.c文件中)。
在这里插入图片描述
在SystemInit函数中,有详细的注释,简略说明:
第214-258行:将全部与时钟相关的寄存器复位为默认值
第262行调用SetSysClock()函数,对系统时钟进行设置,一样右键->Go To Definition of ‘SetSysClock’,跳转到SetSYSClock函数。

/** * @brief Configures the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers. * @param None * @retval None */
static void SetSysClock(void)
{
#ifdef SYSCLK_FREQ_HSE
  SetSysClockToHSE();
#elif defined SYSCLK_FREQ_24MHz
  SetSysClockTo24();
#elif defined SYSCLK_FREQ_36MHz
  SetSysClockTo36();
#elif defined SYSCLK_FREQ_48MHz
  SetSysClockTo48();
#elif defined SYSCLK_FREQ_56MHz
  SetSysClockTo56();  
#elif defined SYSCLK_FREQ_72MHz
  SetSysClockTo72();
#endif
 
 /* If none of the define above is enabled, the HSI is used as System clock source (default after reset) */ 
}

从函数内容能够知道,这个函数根据宏定义,再调用具体的函数对系统时钟进行设置;而在system_stm32f10x.c文件的115行,有SYSCLK_FREQ_72MHz定义(在106行的if判断里,STM32F10X_LD_VL、STM32F10X_MD_VL、STM32F10X_HD_VL均未定义,故进入else分支):

#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
 #define SYSCLK_FREQ_24MHz 24000000
#else
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
/* #define SYSCLK_FREQ_24MHz 24000000 */ 
/* #define SYSCLK_FREQ_36MHz 36000000 */
/* #define SYSCLK_FREQ_48MHz 48000000 */
/* #define SYSCLK_FREQ_56MHz 56000000 */
#define SYSCLK_FREQ_72MHz 72000000
#endif

综合上述分析,在系统上电或复位后,进入复位中断向量地址,调用SystemInit()函数->调用SetSysClock()函数->调用SetSysClockTo72()函数。

在SetSysClockTo72()函数中将系统时钟设置为72MHz,即SYSCLK时钟频率为72MHz,并设置AHB、APB一、APB2分频分别为一、二、1,即:
系统时钟SYSCLK频率为72MHz
AHB总线时钟HCLK频率为72MHz
APB1总线时钟PCLK1频率为36MHz
APB2总线时钟PCLK2频率为72MHZ

(APB1分频系数设为2是由于PCLK1时钟频率不能大于36MHz)

程序

首先理清程序思路,开发库的启动文件已经完成了系统时钟的设置,但外设时钟还未开启,所以程序流程以下:

  • 开启外设时钟(GPIOA、GPIOB都挂载在APB2上);
  • 对GPIOA、GPIOB相关引脚进行配置(设置为输出方式、输出速度)
  • 按照流水灯模式,依次开启LED(引脚清0,输出低电平)并关闭上一个LED(引脚置1,输出高电平),并延时必定时间。
开启外设时钟

开启APB2总线上的外设时钟,须要调用RCC_APB2PeriphClockCmd()函数,具体说明能够在库函数手册中查找到:
在这里插入图片描述
开启GPIOA、GPIOB的时钟:

RCC_APB2PeriphClockCmd(GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(GPIOB, ENABLE);
配置GPIO

对GPIO引脚进行设置,库提供了GPIO初始化结构体GPIO_InitTypeDef:
在这里插入图片描述
共有三个参数:

  • GPIO_Mode用于指定引脚输入输出方式,对应STM32手册中:
    在这里插入图片描述
  • GPIO_Pin用于指定引脚编号,如GPIO_Pin_8,表示GPIOx的第8引脚;
  • GPIO_Speed用于指定引脚输出速率,共有三挡,十、二、50MHz:
    在这里插入图片描述

设置好初始化结构体数据后,调用GPIO_Init()函数对GPIOx进行初始化:
在这里插入图片描述
并调用GPIO_SetBits()函数将引脚置1,输出高电平,使LED在开始时处于关闭状态;而调用GPIO_ResetBits()函数能够将引脚清0,输出低电平,点亮LED。
具体函数用法请自行查询库函数手册。

配置GPIOA、GPIOB :

GPIO_InitTypeDef GPIOInitStruct;
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIOInitStruct);	
	GPIO_SetBits(GPIOA, GPIOInitStruct.GPIO_Pin);
	
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIOInitStruct);
	GPIO_SetBits(GPIOB, GPIOInitStruct.GPIO_Pin);
延时函数

在这里使用粗略的延时函数,所谓延时,即什么也不作,让处理器进行等待,对应的操做称为nop,表示等待一个机器周期:

__nop();

经过对STM32系统时钟初始化的分析可知,系统时钟SYSCLK初始化为 f = 72 M H z f=72MHz ,即一个机器周期时间为
T = 1 f = 1 72 μ s T=\frac{1}{f}=\frac{1}{72}\mu s
所以,若执行72个__nop()函数,则能够延时1 μ s \mu s ,但考虑到函数调用与返回须要两个机器周期,所以设计执行70个__nop()函数,延时函数以下:

static void delay_1us()
{
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
}

(不用数了,就是70个__nop(),其实之后能够利用SysTick寄存器进行精确延时计算)
粗略延时函数:

/** * @brief 粗略延时t us * @param t * @retval None */
void delay_u(int t)
{
	while(t--)
		delay_1us();
}

/** * @brief 粗略延时t ms * @param t * @retval None */
void delay_m(int t)
{
	while(t--)
		delay_u(1000);
}
流水灯控制

部分程序以下,思路就是关闭前一个LED而后点亮下一个LED,并延时一段时间:

...
		GPIO_SetBits(GPIOB, GPIO_Pin_9);
		GPIO_ResetBits(GPIOA, GPIO_Pin_2);
		delay_m(200);
		GPIO_SetBits(GPIOA, GPIO_Pin_2);
		GPIO_ResetBits(GPIOA, GPIO_Pin_3);
		delay_m(200);
		...
完整程序
/* Includes ------------------------------------------------------------------*/
#include "stm32f10x.h"

/* Private functions Declaration ---------------------------------------------*/
void GPIOConfig(void);
void delay_m(int t);
void delay_u(int t);


/** * @brief Main program. * @param None * @retval None */
int main(void)
{
	GPIOConfig();
	
	while(1)
	{
		GPIO_SetBits(GPIOB, GPIO_Pin_9);
		GPIO_ResetBits(GPIOA, GPIO_Pin_2);
		delay_m(200);
		GPIO_SetBits(GPIOA, GPIO_Pin_2);
		GPIO_ResetBits(GPIOA, GPIO_Pin_3);
		delay_m(200);
		GPIO_SetBits(GPIOA, GPIO_Pin_3);
		GPIO_ResetBits(GPIOA, GPIO_Pin_4);
		delay_m(200);
		GPIO_SetBits(GPIOA, GPIO_Pin_4);
		GPIO_ResetBits(GPIOA, GPIO_Pin_5);
		delay_m(200);
		GPIO_SetBits(GPIOA, GPIO_Pin_5);
		GPIO_ResetBits(GPIOA, GPIO_Pin_6);
		delay_m(200);
		GPIO_SetBits(GPIOA, GPIO_Pin_6);
		GPIO_ResetBits(GPIOA, GPIO_Pin_7);
		delay_m(200);
		GPIO_SetBits(GPIOA, GPIO_Pin_7);
		GPIO_ResetBits(GPIOB, GPIO_Pin_8);
		delay_m(200);
		GPIO_SetBits(GPIOB, GPIO_Pin_8);
		GPIO_ResetBits(GPIOB, GPIO_Pin_9);
		delay_m(200);
	}
}

/** * @brief 初始化GPIO * @param None * @retval None */
void GPIOConfig(void)
{
	// GPIO初始化结构体
	GPIO_InitTypeDef GPIOInitStruct;
	
	// 开启GPIOA、GPIOB外设时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	// 设置GPIOA初始化结构体
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	// 初始化GPIOA并将引脚置1
	GPIO_Init(GPIOA, &GPIOInitStruct);	
	GPIO_SetBits(GPIOA, GPIOInitStruct.GPIO_Pin);
	
	// 设置GPIOB初始化结构体
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	// 初始化GPIOB并将引脚置1
	GPIO_Init(GPIOB, &GPIOInitStruct);
	GPIO_SetBits(GPIOB, GPIOInitStruct.GPIO_Pin);
}

/** * @brief 延时1us * @param None * @retval None */
static void delay_1us()
{
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
	__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();__nop();
}

/** * @brief 粗略延时t us * @param t * @retval None */
void delay_u(int t)
{
	while(t--)
		delay_1us();
}

/** * @brief 粗略延时t ms * @param t * @retval None */
void delay_m(int t)
{
	while(t--)
		delay_u(1000);
}

运行结果

一样经过mcuisp软件利用串口下载编译好的.hex程序文件,能够看到8个LED灯依次循环点亮:
在这里插入图片描述

完结撒花✿✿ヽ(°▽°)ノ✿