STM32筆記(13):撰寫程式從暫存器到函數庫

前一篇描述如何使用暫存器的位元操作點亮 LED 燈,因為暫存器的位址難記憶,對於位元運算不熟的人也是一個障礙。本篇將接續上一篇的 LED 程式,逐步改用函數庫的方式來撰寫程式,瞭解如何將暫存器的地址改由變數名稱來替代,就樣就可以取容易記得或有意義的變數名稱在程式中,也可以另定廣域變數、函數或列舉等,將這些寫在 header(.h)檔或外部函數後,再逐步學習官方的標準函數庫的名稱及其功能。目前官方提供三種函數庫:
  1. Standard Peripheral Library:簡寫SPL,也叫標準外設函數庫,這是支援 STM32 外部設備所集合的一些 C 語言函數庫。
  2. HAL:Hardware Abstraction Layer,硬體抽象層函數庫。
  3. LL:Low-Layer,底層函數庫。

根據 stm32 embedded software offering 文件的描述,HAL 和 LL配合 STM32CubeMX 工具對 STM32 進行開發,也是目前 ST 官方主推的一套開發函數庫。HAL 的目標用戶是具有一定嵌入式基礎的開發人員,且具有較佳的移植性。而 LL 函數庫相較於 HAL 及 SPL 具有較簡單的結構。如以三者相比的移植、難易程度等可參考下表:
就可攜性(Portability)、優化(Optimization)、難易程度(Easy)及硬體覆蓋(Hardware coverage)來說以 HAL 硬體抽象層函數庫表現較佳。

要撰寫 STM32 程式,可以建立自己的 C 語言 header(.h)檔,將記憶體的位址用名稱當作變數,方便記憶與運算。STM32F103xx的參考手冊中,關於總線的位址如下:
查表得知三條總線 APB1、APB2、AHB 的基地址如下:
匯流排名稱匯流排基地址
APB10x4000 0000
APB20x4001 0000
AHB0x4001 8000

其中AHB查表得知其基地址應為0x40018000,從 0x40020000 以下為保留區,程式中就改以 0x40020000 當作基地址。
#define PERIPH_BASE   0x40000000    //總線基地址
//定義3條總線的地址
#define  APB1PERIPH_BASE   PERIPH_BASE
#define  APB2PERIPH_BASE   (PERIPH_BASE + 0x00010000)
#define  AHBPERIPH_BASE    (PERIPH_BASE + 0x00020000)
參考手冊中的 GPIOA 其位址在 APB2 上偏移 0x00000800,可將 GPIOA 的基底址定義如下:
#define  GPIOA_BASE  (APB2PERIPH_BASE + 0x00000800)

GPIOx 的位址及其功能定義的各個暫存器都是在 GPIOx_BASE 的基地址上偏移地址,每次都是 4 個字元(4x8=32位元)。以 GPIOA為例,定義如下:
#define GPIOA_CRL          *(unsigned int *)( GPIOA_BASE + 0x00)
#define GPIOA_CRH          *(unsigned int *)( GPIOA_BASE + 0x04)
#define GPIOA_IDR          *(unsigned int *)( GPIOA_BASE + 0x08)
#define GPIOA_ODR          *(unsigned int *)( GPIOA_BASE + 0x0C)
#define GPIOA_BSRR         *(unsigned int *)( GPIOA_BASE + 0x10)
#define GPIOA_BRR          *(unsigned int *)( GPIOA_BASE + 0x14)
#define GPIOA_LCKR         *(unsigned int *)( GPIOA_BASE + 0x18)
時鐘 RCC 的總線在 AHB ,偏移位址為 0x1000,可以將變數寫成如下公式:
#define  RCC_BASE  (AHBPERIPH_BASE + 0x1000)
RCC_APB2ENR 是在 RCC 基地址上偏移 0x18,如下圖:


可以將 RCC_APB2ENR 的變數定義更換為全域變數的名稱:
#define  RCC_APB2ENR  *(unsigned int*)(RCC_BASE + 0x18)
前一篇使用暫存器點亮 LED 燈的程式如下:
*(unsigned int*)0x40021018 |= (1<<2);
*(unsigned int*)0x40010800 &= ~(0x0F<<(4*4)); 
*(unsigned int*)0x40010800 |= (1<<16);
*(unsigned int*)0x4001080C &= ~(1<<4);
將「位址」改為「變數」的方式來編寫,程式如下:
RCC_APB2ENR |= (1<<2);
GPIOA_CRL &= ~(0x0F<<(4*4)); 
GPIOA_CRL |= (1<<16);
GPIOA_ODR &= ~(1<<4);
這樣看起來是不是比較容易閱讀?一看變數名稱 RCC 開頭的就知道是設定時鐘,以 GPIO 開頭的變數就是存取 GPIOx 的位址。完整的 main.c 程式如下:
#include "stm32f1xx.h"

int main()
{
	RCC_APB2ENR |= (1<<2);
	GPIOA_CRL &= ~(0x0F<<(4*4)); 
	GPIOA_CRL |= (1<<16);
	GPIOA_ODR &= ~(1<<4);
}
void SystemInit(void)
{
}
放置變數定義的 header檔(檔名可以自行定義):stm32f1xx.h 完整程式如下:
#define  PERIPH_BASE      	((unsigned int)0x40000000)
	
#define  APB1PERIPH_BASE   	PERIPH_BASE
#define  APB2PERIPH_BASE   	(PERIPH_BASE + 0x00010000)
#define  AHBPERIPH_BASE    	(PERIPH_BASE + 0x00020000)

#define  GPIOA_BASE  		(APB2PERIPH_BASE + 0x00000800)
#define  GPIOA_CRL              *(unsigned int *)(GPIOA_BASE + 0x00)
#define  GPIOA_CRH              *(unsigned int *)(GPIOA_BASE + 0x04)
#define  GPIOA_IDR              *(unsigned int *)(GPIOA_BASE + 0x08)
#define  GPIOA_ODR              *(unsigned int *)(GPIOA_BASE + 0x0C)
#define  GPIOA_BSRR             *(unsigned int *)(GPIOA_BASE + 0x10)
#define  GPIOA_BRR              *(unsigned int *)(GPIOA_BASE + 0x14)
#define  GPIOA_LCKR             *(unsigned int *)(GPIOA_BASE + 0x18)
	
#define  RCC_BASE  	        (AHBPERIPH_BASE + 0x1000)
#define  RCC_APB2ENR  	        *(unsigned int*)(RCC_BASE + 0x18)

[使用結構方式簡化程式]

上面的程式使用 GPIOA 的引腳接 LED 燈,只有一個引腳時,各自定義其時鐘、GPIO等還可以接受,如果是用到的引腳比較多,可以定義 A、B、C、D…G 組,程式就要重複寫很多行,各暫存器的偏移位址都一樣,不同的是 GPIOx 的 x 可以是A、B、C、D…G。也就是說 GPIOA 有 7 個暫存器控制,GPIOB也是有7個暫存器控制,我們可以採用結構體的方式來簡化程式的撰寫。

上述定義 GPIO各暫存器位址的變數定義如下:
#define GPIOA_CRL          *(unsigned int *)( GPIOA_BASE + 0x00)
#define GPIOA_CRH          *(unsigned int *)( GPIOA_BASE + 0x04)
#define GPIOA_IDR          *(unsigned int *)( GPIOA_BASE + 0x08)
#define GPIOA_ODR          *(unsigned int *)( GPIOA_BASE + 0x0C)
#define GPIOA_BSRR         *(unsigned int *)( GPIOA_BASE + 0x10)
#define GPIOA_BRR          *(unsigned int *)( GPIOA_BASE + 0x14)
#define GPIOA_LCKR         *(unsigned int *)( GPIOA_BASE + 0x18)
每個暫存器的位址佔用 4 字元(Bytes),等於 32 位元(bits),可使用 unsigned int 來定義一個每個變數為 32 位元的結構(Structure),編寫的方式如下:
typedef unsigned int uint32_t;
typedef struct 
{
	uint32_t CRL;
	uint32_t CRH;
	uint32_t IDR;
	uint32_t ODR;
	uint32_t BSRR;
	uint32_t BRR;
	uint32_t LCKR;
}GPIO_TypeDef;
也就是說把 32 位元的暫存器 CRL、CRH...LCKR 組成一個整體,取一個名字叫做 GPIO_TypeDef,在記憶體內的排序是連續的,假設 CRL 的地址為 0x00,CRH 的地址為0x04,依次排列下去為 0x08, 0x0C ...等。有了結構後,再定義:
#define  GPIOA  ((GPIO_TypeDef*)GPIOA_BASE)
(GPIO_TypeDef*)GPIOA_BASE 這個指令的意思是將 GPIOA_BASE 轉換成 GPIO_TypeDef 類型的地址。GPIOA_BASE 的值為 0x40010800,以該位址為基礎,CRL 的地址為 0x40010800,CRH 地址為 0x40010804... 依次下去,跟每個暫存器變數各自定義的一樣!同樣的 RCC 時鐘的暫存器地址也可以用類型的方式表示:
typedef unsigned int uint32_t;
typedef struct 
{
	uint32_t CR;
	uint32_t CFGR;
	uint32_t CIR;
	uint32_t APB2RSTR;
	uint32_t APB1RSTR;
	uint32_t AHBENR;
	uint32_t APB2ENR;
	uint32_t APB1ENR;
	uint32_t BDCR;
	uint32_t CSR;
}RCC_TypeDef;
建立結構後,再定義 RRC 時鐘的基地址:
#define  RCC  ((RCC_TypeDef*)RCC_BASE)

完整的 main.c 程式如下:
#include "stm32f1xx.h"

int main()
{
	RCC->APB2ENR |= (1<<2);
	GPIOA->CRL &= ~(0x0F1<<(4*4)); 
	GPIOA->CRL |= (1<<16);
	GPIOA->ODR &= ~(1<<4);
}
void SystemInit(void)
{
}

完整的 stm32f1xx.h 程式如下:
#define  PERIPH_BASE      	((unsigned int)0x40000000)
	
#define  APB1PERIPH_BASE     PERIPH_BASE
#define  APB2PERIPH_BASE     (PERIPH_BASE + 0x00010000)
#define  AHBPERIPH_BASE      (PERIPH_BASE + 0x00020000)

#define GPIOA_BASE           (APB2PERIPH_BASE + 0x0800)
#define RCC_BASE             (AHBPERIPH_BASE + 0x1000)

typedef unsigned int uint32_t;
typedef struct 
{
	uint32_t CRL;
	uint32_t CRH;
	uint32_t IDR;
	uint32_t ODR;
	uint32_t BSRR;
	uint32_t BRR;
	uint32_t LCKR;
}GPIO_TypeDef;

typedef struct 
{
	uint32_t CR;
	uint32_t CFGR;
	uint32_t CIR;
	uint32_t APB2RSTR;
	uint32_t APB1RSTR;
	uint32_t AHBENR;
	uint32_t APB2ENR;
	uint32_t APB1ENR;
	uint32_t BDCR;
	uint32_t CSR;
}RCC_TypeDef;

#define  GPIOA  ((GPIO_TypeDef*)GPIOA_BASE)
#define  RCC    ((RCC_TypeDef*)RCC_BASE)
感覺使用結構好像沒有比上一段的程式使用變數定義的方式來的簡化,那是因為我們只用一個 GPIO,未來如果使用多個 GPIO 時,程式將會簡化許多。

[結果]

編譯上傳完成畫面如下:

這裡發生個小插曲,我將程式編譯完成後上傳開發板後,按下 Reset 鍵,LED 燈竟然不會亮。程式檢查了很久,覺得都沒錯,後來發現是一個暫存器的數字寫錯了,將「#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)」的 0x0800 寫成 0x8000,就一字之差結果完全不一樣。這也是使用暫存器開發程式要特別注意的地方,否則一個位址錯誤,除錯要查很久。

[參考文件]


2 留言

  1. 看文章學習了很多東西﹑希望版主可以再寫相關的系列文章

    回覆刪除
    回覆
    1. 謝謝您的支持與鼓勵, 最近會再撥時間繼續研究 STM32 相關實作。

      刪除

張貼留言

較新的 較舊