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

单片机作为一种微控制器,最基本的用途便是通过其引脚与外界进行交互,而在单片机编程界,有这么一个程序,堪称单片机中的HelloWorld,不仅可以熟悉单片机的引脚控制,更能对单片机的时钟进行深入了解,那就是几乎所有单片机教程中都会提到的——流水灯

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

流水灯电路设计

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

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

电路原理图

根据最小系统板的引脚分布,选择GPIOA2~GPIOA7、GPIOB8、GPIOB9共8个引脚来控制流水灯(这8个引脚在笔者的系统板同一侧且相邻,用杜邦线连接整齐点o( ̄︶ ̄)o),电路原理图和实物图如下:
【STM32F103笔记】2、单片机中的HelloWorld——流水灯_第1张图片

电路参数设计分析

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

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

查阅STM32F103C8T6手册,引脚电压电流范围:
【STM32F103笔记】2、单片机中的HelloWorld——流水灯_第2张图片
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 I=(VccVD)/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 I=(VccVD)/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 I=(VccVD)/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 I=(VccVD)/R=3.0mA,引脚端电压为3.0V
由于STM32F103C8T6的引脚GPIOA2~ GPIOA7不能能耐受5V电平的引脚,因此,输入电压上限是VDD+0.3V=3.6V,为了保险起见,选取3.3V供电,虽然LED工作电流有点小,但是能亮就行了不是。(当然,选取5V供电其实问题不大,但是实际工业应用中应该避免器件超出规定的使用条件,当然也不能让器件在不满足工作条件的情况下使用,比如上述的LED电流,其实是笔者没有找到其他合适阻值的限流电阻了Orz,也就是说,应该设计好电路参数再选择器件)。

因此,通过上述分析,选取3.3V电压作为流水灯的供电电压,至于LED的工作电流过小,其实问题不大,我们这里要求的是LED能亮就可以,对亮度木有要求。

时钟及GPIO分析

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

STM32F103时钟分析

首先在STM32手册中找到时钟树,如下图,沿图中红色直线从左至右看:
【STM32F103笔记】2、单片机中的HelloWorld——流水灯_第3张图片
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总线的时钟,当然,这也是调用库函数就可以实现的:
【STM32F103笔记】2、单片机中的HelloWorld——流水灯_第4张图片

程序设计

新建一个LED流水灯文件夹,并复制一份工程模板到文件夹下,打开工程文件,进入Keil uVision5。
【STM32F103笔记】2、单片机中的HelloWorld——流水灯_第5张图片

时钟设置分析

首先对启动文件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文件中)。
【STM32F103笔记】2、单片机中的HelloWorld——流水灯_第6张图片
在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、APB1、APB2分频分别为1、2、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()函数,具体说明可以在库函数手册中查找到:
【STM32F103笔记】2、单片机中的HelloWorld——流水灯_第7张图片
开启GPIOA、GPIOB的时钟:

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

对GPIO引脚进行设置,库提供了GPIO初始化结构体GPIO_InitTypeDef:
【STM32F103笔记】2、单片机中的HelloWorld——流水灯_第8张图片
共有三个参数:

  • GPIO_Mode用于指定引脚输入输出方式,对应STM32手册中:
    【STM32F103笔记】2、单片机中的HelloWorld——流水灯_第9张图片
  • GPIO_Pin用于指定引脚编号,如GPIO_Pin_8,表示GPIOx的第8引脚;
  • GPIO_Speed用于指定引脚输出速率,共有三挡,10、2、50MHz:
    【STM32F103笔记】2、单片机中的HelloWorld——流水灯_第10张图片

设置好初始化结构体数据后,调用GPIO_Init()函数对GPIOx进行初始化:
【STM32F103笔记】2、单片机中的HelloWorld——流水灯_第11张图片
并调用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 f=72MHz,即一个机器周期时间为
T = 1 f = 1 72 μ s T=\frac{1}{f}=\frac{1}{72}\mu s T=f1=721μs
因此,若执行72个__nop()函数,则可以延时1 μ s \mu s μ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灯依次循环点亮:

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

你可能感兴趣的:(STM32F103笔记,单片机,嵌入式,stm32,程序设计)