CPU 暫存器和周邊暫存器差在哪裡?

剛開始讀 MCU 的 Reference Manual 時,相信你可能也會跟我一樣,看到一大堆「register」就有種丈二金剛摸不著頭腦、抓不到重點的感覺:GPIO 有 register,USART 有 register,Timer 也有 register。翻到 CPU 的章節,會出現像是 R0、R1、PC 這些東西,它們也叫做 register。不過,同一個字指的是兩種很不一樣的東西。

要把它們分清楚,我們可以先從一個問題想起:這個暫存器是在 CPU 的裡面,還是外面?通用暫存器在 CPU 裡面,是 CPU 運算時的工作台;周邊暫存器在 CPU 外面,是周邊電路開給 CPU 的存取窗口。

我們一樣以 STM32F401 晶片為例(核心是 ARM Cortex-M4),先從工作台看起。


CPU 的工作台

Cortex-M4 的 core 暫存器有 R0–R12、SP、LR、PC,再加上 PSR、PRIMASK、CONTROL 這幾個特殊暫存器(DUI 0553B)。ARM 的文件對 R0–R12 的描述很直接:它們是給資料運算用的通用暫存器。


運算就發生在這裡。加法、位元運算、比較,都是拿暫存器裡的值來算,結果也放回暫存器。這些暫存器數量不多,指令裡直接寫名字(例如 r3)就能指定,所以它們沒有位址,也不在 memory map 上。CPU 就像個勤奮的老實人,需要的不多,又很會工作。


周邊開的窗口

周邊暫存器屬於另一群硬體電路。Timer 在計數、USART 在收發、ADC 在轉換,這些有著特定目的或功能的工作都是周邊硬體自己在做的事;周邊暫存器則是它們和軟體交換設定、狀態與資料的地方。我們可以認為,這些暫存器就是「硬體功能的 API」,你可以透過暫存器將輸入餵給硬體,也可以從暫存器拿到輸出(資料或事件等)。

這些窗口開的位置都寫在 memory map 上了。以 USART2 為例,它的暫存器從 0x4000 4400 開始(RM0368),狀態暫存器 USART_SR 在偏移 0x00 的地方,資料暫存器 USART_DR 則是在偏移 0x04 之處。CPU 要找到這些窗口,靠的就是位址。





為什麼要分成兩種

ARM 對 load-store 架構的定義是:資料運算只對暫存器裡的內容動作,不直接對記憶體裡的內容動作。


換成工作台的說法,就是要算的東西得先搬上工作台才能運算。所以 Cortex-M4 的指令分成兩類:運算指令只在工作台上做事;load、store 負責在工作台和外面之間搬東西。外面的東西不管是 SRAM 裡的變數,還是周邊的窗口,都有位址,搬的方式也一樣。

〈MCU 的最小執行模型〉裡提過的 GPIOA_ODR |= (1u << 5); 就是這樣。|= 的意思是讀出、修改、再寫回,到了 CPU 上就會變成三步:先用 load 把窗口裡的值搬上工作台,在工作台上把 bit 5 設起來,再用 store 寫回窗口。

把周邊的窗口放在位址表上,好處是 CPU 不需要另外一套指令去和周邊打交道,同一組 load、store 就能碰到記憶體和周邊。

{alertInfo}CPU 內外的分界,每一顆 CPU 都有。load-store 架構的特點,是把這條分界寫進了指令集的規則裡。也有一些指令集允許運算指令直接帶記憶體位址,搬運和運算看起來是同一條指令;Cortex-M4 則把它們明確分開。

 

窗口和 SRAM 不一樣的地方

同樣用 load、store 存取,窗口後面接的卻是一個正在工作的電路,所以它的行為和 SRAM 不一樣。RM0368 裡隨手就能找到例子:

#include <stdint.h>

#define USART2_SR (*(volatile uint32_t *)0x40004400u)
#define USART2_DR (*(volatile uint32_t *)0x40004404u)

void poll_uart(void)
{
    if (USART2_SR & (1u << 5)) {            /* RXNE:收到一筆資料了 */
        uint8_t c = (uint8_t)USART2_DR;     /* 讀這一次,RXNE 就會被清掉 */
        (void)c;
    }
}

註:

  1. 這段程式省略了打開 USART2 時脈、設定鮑率等前置動作,這裡只拿它來說明概念。
  2. USART2 從 0x4000 4400 開始,USART_SR 的偏移是 0x00,USART_DR 的偏移是 0x04。
  • 讀取暫存器可能會改變狀態(副作用)。例如,USART_SR 的 RXNE 位元表示「收到資料了」,它由硬體設起來,讀一次 USART_DR 時它就會被清掉(RM0368 §19.6.1)。對 SRAM 裡的變數,讀幾次都不會改變任何東西。

  • 值是硬體寫進去的。GPIOx_IDR 反映的是接腳當下的電位,程式只能讀、不能寫,而且只能用 word 的寬度存取(RM0368 §8.4.5)。

  • 存取規則不同。周邊所在的區域,ARM 定為 Device 類型,處理器會保留這類存取之間的先後順序,也不能從這裡執行程式(DUI 0553B Table 2-11)。

也因為這樣,讀取周邊暫存器的「次數」本身就有意義,多讀一次、少讀一次,結果都可能不同。這也是我們要把周邊暫存器宣告成 volatile 的原因:compiler 必須讓每一次讀取都真的發生。


模型的邊界

「內外」的分法很好用,但有兩個地方要先知道:

  • 屬於 ARM,不代表在 CPU 裡面。NVIC、SysTick 是 ARM 自己的周邊,但它們的暫存器有位址(例如 SysTick 從 0xE000 E010、NVIC 從 0xE000 E100 開始),一樣要用 load、store 存取。判斷一個暫存器是哪一種,看的是它在 CPU 的裡面還是外面,跟它屬於誰沒有關係。

  • 工作台上也有幾格比較特別。PRIMASK、CONTROL 這些特殊暫存器在 CPU 裡面,沒有位址,但也不能拿來做一般運算,要用專門的 MRS、MSR 指令讀寫。

想一想

{collapse}1. CPU 寫入一個 uint32_t 全域變數和寫入 GPIOA_ODR,有什麼相同、什麼不同?
相同的是兩者都在 CPU 外面、都有位址,都用 store 指令寫入。不同的是變數在 SRAM,屬於 Normal 區域;GPIOA_ODR 在周邊區,屬於 Device 區域,寫進去之後還會改變接腳的電位。
{collapse}2. 如果程式為了「確認一下」,連續讀了兩次 USART_DR,會有什麼問題?
第一次讀取就已經清掉了 RXNE。如果程式之後還要靠 RXNE 判斷有沒有新資料,就會以為沒有收到東西。對周邊暫存器來說,讀取本身就是一個動作。

回到開頭的問題:兩種暫存器差在哪?通用暫存器在 CPU 裡面,是做運算的工作台;周邊暫存器在 CPU 外面,是周邊開給 CPU 的窗口,要靠位址和 load、store 才碰得到。


本文屬於「補破網」專欄:由我和 AI 協作寫成,內容經把關後發佈。關於本專欄,請見〈補破網〉。

simen

An enthusiastic engineer with a passion for learning. After completing my academic journey, I worked as an engineer in Hsinchu Science Park. Later, I ventured into academia to teach at a university. However, I have now returned to the industry as an engineer, again.

張貼留言 (0)
較新的 較舊