【STM32F103筆記】2、單片機中的HelloWorld——流水燈

單片機作爲一種微控制器,最基本的用途便是通過其引腳與外界進行交互,而在單片機編程界,有這麼一個程序,堪稱單片機中的HelloWorld,不僅可以熟悉單片機的引腳控制,更能對單片機的時鐘進行深入瞭解,那就是幾乎所有單片機教程中都會提到的——流水燈

在上一篇中我們已經搭建好了STM32開發環境,點亮了第一個LED燈,這一篇將從電路原理分析開始,對流水燈的控制原理電路參數設計,STM32F103引腳時鐘的設置進行介紹。

流水燈電路設計

顧名思義,流水燈就是像水流一樣,依次點亮的一組燈。設計流水燈爲間隔固定的時間每次點亮一個LED,因此在點亮下一個LED的同時,要關閉上一個LED,並且進行計時,以循環下去。

在上一篇中我們用單片機的GPIOB8引腳點亮了最小系統板上自帶的一個LED,根據其電路原理圖,可以設計流水燈的電路原理。

電路原理圖

根據最小系統板的引腳分佈,選擇GPIOA2~GPIOA7、GPIOB8、GPIOB9共8個引腳來控制流水燈(這8個引腳在筆者的系統板同一側且相鄰,用杜邦線連接整齊點o( ̄︶ ̄)o),電路原理圖和實物圖如下:
在這裏插入圖片描述

電路參數設計分析

這裏實物是自己焊的小板子,用的1KΩ\Omega的排阻作爲限流電阻,選的白髮綠LED:

驅動電壓 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=(VccVD)/R=2.1mAI=(V_{cc}-V_D)/R=2.1mA,引腳端電壓爲2.1V
LED導通壓降VD=2.0V:I=(VccVD)/R=1.3mAI=(V_{cc}-V_D)/R=1.3mA,引腳端電壓爲1.3V
當Vcc使用5.0V供電時,選用限流電阻爲1KΩ\Omega計算極端情況下:
LED導通壓降VD=1.2V:I=(VccVD)/R=3.8mAI=(V_{cc}-V_D)/R=3.8mA,引腳端電壓爲3.8V
LED導通壓降VD=2.0V:I=(VccVD)/R=3.0mAI=(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能亮就可以,對亮度木有要求。

時鐘及GPIO分析

控制流水燈正常運行,需要對8個LED的控制引腳進行設置,並按固定時間對其輸出進行控制。由於時鐘是單片機運行的基礎,首先對時鐘進行分析。

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、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()函數,具體說明可以在庫函數手冊中查找到:
在這裏插入圖片描述
開啓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用於指定引腳輸出速率,共有三擋,10、2、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=72MHzf=72MHz,即一個機器週期時間爲
T=1f=172μ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燈依次循環點亮:
在這裏插入圖片描述

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章