Skip to content

3 Thanh ghi và hàm thư viện

Giới thiệu về thanh ghi

Ánh xạ bộ nhớ (memory mapping )

Bộ nhớ trong máy tính giống như một cái tủ nhiều ngăn. Mỗi ngăn (ô nhớ) có thể chứa dữ liệu, và để lấy đúng dữ liệu thì ta cần biết "địa chỉ" của ngăn đó – giống như số thứ tự dán trên ngăn tủ.

Tuy nhiên, bản thân một phần tử bộ nhớ ban đầu không có sẵn địa chỉ cố định. Nó chỉ là một vùng trống để lưu thông tin. Địa chỉ này sẽ được nhà sản xuất chip gán sẵn (như với thanh RAM) hoặc người lập trình phân bổ khi viết phần mềm.

Quá trình gán địa chỉ này gọi là “ánh xạ vùng nhớ” (memory mapping) – nghĩa là liên kết một vùng bộ nhớ với một địa chỉ cụ thể để hệ thống có thể truy cập và điều khiển nó.

Sau khi đã có địa chỉ, ta có thể dùng con trỏ – tức là một biến chứa địa chỉ ô nhớ đó – để truy xuất và thao tác dữ liệu trong phần tử bộ nhớ.

Bảng ánh xạ bộ nhớ (memory map table)

STM32 là loại vi điều khiển 32-bit, nghĩa là nó có thể xử lý địa chỉ bộ nhớ từ 0 đến 2^32 - 1, tức là có thể truy cập tới 4GB không gian địa chỉ.

Tuy nhiên, không phải toàn bộ 4GB đều là RAM hay Flash mà bạn có thể dùng để lưu dữ liệu. Bộ nhớ này được chia ra thành nhiều vùng khác nhau, dùng cho những mục đích cụ thể như:

  • RAM
  • Flash (bộ nhớ chương trình)
  • Các thiết bị ngoại vi như UART, SPI, GPIO…
  • Các vùng đặc biệt cho hệ thống (như bộ xử lý NVIC, SysTick...)

Để giảm độ phức tạp cho lập trình viên, ARM đã định nghĩa sẵn bảng ánh xạ vùng nhớ cho dòng vi xử lý Cortex-M4. Bảng này chỉ rõ vùng địa chỉ nào dùng cho mục đích gì – ví dụ:

  • Flash: từ địa chỉ 0x0800_0000
  • RAM: từ 0x2000_0000
  • Ngoại vi: từ 0x4000_0000 trở đi

Các vùng như thiết bị hệ thống (System Control Space) hoặc vùng ngoại vi lõi (Core Peripheral) là cố định và không thể thay đổi, do ARM quy định.

Phần còn lại của vùng địa chỉ (ví dụ như vùng dành cho ngoại vi cụ thể của từng chip STM32) sẽ được STMicroelectronics phân bổ và định nghĩa thêm, tuỳ thuộc vào từng dòng chip cụ thể.

Nếu bạn muốn tìm hiểu kỹ hơn về bảng ánh xạ bộ nhớ, hãy xem trang 71 trong tài liệu User Manual của STM32 — nơi liệt kê cụ thể từng vùng địa chỉ và chức năng tương ứng.

Thanh ghi (Register )

Thanh ghi (Register) là một loại bộ nhớ rất nhỏ nằm bên trong vi điều khiển hoặc vi xử lý, dùng để lưu trữ tạm thời dữ liệu hoặc điều khiển hoạt động của thiết bị ngoại vi như UART, SPI, GPIO,…

Thanh ghi khác với RAM thông thường ở chỗ:

  • Nó có chức năng cụ thể (ví dụ: điều khiển xuất dữ liệu, bật tắt chân GPIO…)
  • Nó thường được đặt tại địa chỉ cố định trong bảng ánh xạ bộ nhớ
  • Truy cập vào thanh ghi giúp ta điều khiển phần cứng trực tiếp

Ánh xạ thanh ghi (Register Mapping)

Bộ nhớ chương trình, bộ nhớ dữ liệu, thanh ghi và cổng I/O đều nằm trong cùng một không gian địa chỉ tuyến tính 4 GB. Mỗi thanh ghi tương ứng với các chức năng khác nhau, thao tác với thanh ghi tương ứng có thể cấu hình các chức năng khác nhau. Nếu chúng ta muốn điều khiển một thiết bị ngoại vi hoạt động, có thể tìm địa chỉ bắt đầu của đơn vị này rồi truy cập các phần tử bộ nhớ này qua con trỏ C. Nhưng thường chúng ta sẽ đặt cho phần bộ nhớ đặc biệt này một tên, quá trình đặt biệt danh cho phần bộ nhớ đã được phân bổ địa chỉ và có chức năng cụ thể gọi là ánh xạ thanh ghi, và biệt danh này chính là thanh ghi chúng ta nói tới.

Trong vi điều khiển như STM32, tất cả bộ nhớ chương trình, RAM, thanh ghi và thiết bị ngoại vi đều được sắp xếp trong cùng một không gian địa chỉ 4GB — nghĩa là mỗi thành phần phần cứng đều có một địa chỉ cụ thể trong bộ nhớ.

Khi viết chương trình bằng C, nếu bạn muốn điều khiển một ngoại vi, bạn cần truy cập đúng địa chỉ ô nhớ điều khiển nó.

Thay vì ghi trực tiếp địa chỉ như *(volatile uint32_t*)0x40020000, ta sẽ gán địa chỉ này vào một tên có ý nghĩa hơn (gọi là “biệt danh” cho dễ nhớ và dễ lập trình hơn).

Quá trình đặt tên cho một địa chỉ có sẵn và chức năng cụ thể được gọi là ánh xạ thanh ghi (Register Mapping).

Giả sử bạn muốn điều khiển chân GPIOA:

c
#define GPIOA_ODR   (*(volatile uint32_t*)0x40020014)

Ở đây:

  • 0x40020014 là địa chỉ thực của thanh ghi điều khiển output của GPIOA
  • GPIOA_ODR là tên gán vào để dễ gọi
  • Sau này bạn có thể viết GPIOA_ODR |= (1 << 5); để bật chân PA5 (LED chẳng hạn)

Chính dòng #define này là ánh xạ thanh ghi.

Định lại bộ đếm (Register Remapping)

Thông thường, mỗi thanh ghi điều khiển phần cứng (register) nằm ở một địa chỉ cố định do nhà sản xuất chip quy định. Tuy nhiên, trong một số trường hợp đặc biệt, ta có thể thay đổi (cấp phát lại) địa chỉ của các thanh ghi này, tức là gán một địa chỉ mới cho chức năng cũ.

Địa chỉ cơ sở của bus (Bus Base Address)

Trong STM32, các thiết bị ngoại vi như UART, SPI, GPIO… không kết nối trực tiếp với CPU, mà được phân bổ vào các bus – giống như các “đường truyền nội bộ” để giao tiếp giữa CPU và ngoại vi.

Trong vi điều khiển STM32, các thiết bị ngoại vi được kết nối qua các bus. Mỗi loại bus có tốc độ và vai trò khác nhau, cụ thể:

Tên busĐịa chỉ cơ sởPhạm vi địa chỉTốc độ tối đa
APB10x4000 00000x4000 0000 – 0x4001 FFFF42 MHz
APB20x4001 00000x4001 0000 – 0x4002 FFFF84 MHz
AHB10x4002 00000x4002 0000 – 0x4FFF FFFF168 MHz
AHB20x5000 00000x5000 0000 – 0x5FFF FFFF(tùy chip)

Mỗi bus sẽ được cấp một vùng địa chỉ riêng trong không gian 4GB của vi điều khiển. Địa chỉ thấp nhất trong vùng này gọi là địa chỉ cơ sở của bus – đây cũng chính là địa chỉ của thiết bị ngoại vi đầu tiên được gắn vào bus đó.

Địa chỉ cơ sở của thiết bị ngoại vi

Mỗi bus (như AHB1) có thể gắn nhiều thiết bị ngoại vi, mỗi thiết bị sẽ được cấp một vùng địa chỉ riêng biệt trong phạm vi của bus đó.

Trong trường hợp GPIO (General Purpose I/O) thuộc bus AHB1, mỗi port GPIO (A, B, C...) có một địa chỉ cơ sở như sau:

Thiết bị ngoại viĐịa chỉ cơ sở (Base Address)Offset so với AHB1
GPIOA0x4002 00000x0000 0000
GPIOB0x4002 04000x0000 0400
GPIOC0x4002 08000x0000 0800
GPIOD0x4002 0C000x0000 0C00
GPIOE0x4002 10000x0000 1000
GPIOF0x4002 14000x0000 1400
GPIOG0x4002 18000x0000 1800
GPIOH0x4002 1C000x0000 1C00
GPIOI0x4002 20000x0000 2000
  • Mỗi port GPIO (A đến I) nằm liền kề nhau trong bộ nhớ, cách nhau 0x400 (1024 bytes).
  • Việc này giúp bạn dễ tính địa chỉ nếu biết địa chỉ cơ sở của GPIOA:
    • GPIOB = GPIOA + 0x400
    • GPIOC = GPIOA + 0x800
    • ...

Trong code C, địa chỉ này được dùng để ánh xạ các thanh ghi điều khiển:

c
#define GPIOA_BASE   0x40020000U
#define GPIOB_BASE   0x40020400U
// ...

#define GPIOA_ODR    (*(volatile uint32_t*)(GPIOA_BASE + 0x14))
#define GPIOB_ODR    (*(volatile uint32_t*)(GPIOB_BASE + 0x14))

Bạn có thể truy cập trực tiếp đến các thanh ghi của GPIO như ODR, IDR, MODER… thông qua các địa chỉ này.

Địa chỉ thanh ghi của thiết bị ngoại vi (Peripheral Register Address)

Mỗi thiết bị ngoại vi (ví dụ: GPIO, UART, SPI, TIM…) trên STM32 được điều khiển thông qua một tập hợp các thanh ghi. Những thanh ghi này được đặt tại các địa chỉ cố định trong vùng nhớ, bắt đầu từ địa chỉ cơ sở (base address) của thiết bị đó.

Địa chỉ thanh ghi = Địa chỉ cơ sở của thiết bị + offset của thanh ghi

Offset là độ lệch của từng thanh ghi so với địa chỉ cơ sở

Ví dụ: GPIOA

  • Địa chỉ cơ sở của GPIOA: 0x4002 0000
  • Các thanh ghi quan trọng trong GPIOA:
Tên thanh ghiOffsetĐịa chỉ thực tếChức năng
MODER0x000x40020000Chọn chế độ cho chân GPIO
OTYPER0x040x40020004Kiểu output (push-pull/open-drain)
OSPEEDR0x080x40020008Tốc độ xuất dữ liệu
PUPDR0x0C0x4002000CChọn pull-up/pull-down
IDR0x100x40020010Input data
ODR0x140x40020014Output data
BSRR0x180x40020018Set/reset chân GPIO

Ví dụ muốn bật chân PA5:

#define GPIOA_ODR  (*(volatile uint32_t*)0x40020014)
GPIOA_ODR |= (1 << 5);  // Xuất mức cao tại chân PA5

Các thiết bị khác như USART2

  • Địa chỉ cơ sở của USART2 (trên APB1): 0x4000 4400
  • Các thanh ghi chính:
Tên thanh ghiOffsetMục đích
SR0x00Status register
DR0x04Data register
BRR0x08Baud rate register
CR1, CR2, CR30x0C, 0x10, 0x14Control register 1–3

Cách thao tác với thanh ghi

Ta muốn đặt tất cả 16 chân của GPIOA lên mức cao (giá trị 1) → nghĩa là bật tất cả các chân PA0 đến PA15.

Bước 1: Xác định địa chỉ thanh ghi điều khiển output – GPIOx_ODR

  • Địa chỉ cơ sở của GPIOA là: 0x4002 0000
  • Offset của ODR (Output Data Register) là: 0x14 ⟹ Địa chỉ thực tế là:
c
0x4002 0000 + 0x14 = 0x4002 0014

Bước 2: Viết mã C thao tác trực tiếp

Truy cập bộ nhớ bằng địa chỉ tuyệt đối
c
/* GPIOA bật hết 16 bit lên mức 1 */
/* Sử dụng volatile để ngăn tối ưu hóa không mong muốn */
*(volatile uint32_t *)(0x40020014) = 0xFFFF;

Giải thích:

  • volatile uint32_t * là phép ép kiểu (cast) số hằng 0x40020014 thành con trỏ tới vùng nhớ kiểu uint32_t có khả năng thay đổi bất ngờ.
  • Toán tử * dùng để giải tham chiếu, truy cập giá trị tại địa chỉ đó.
  • Câu lệnh *(volatile uint32_t *)(0x40020014) = 0xFFFF; gán giá trị 0xFFFF cho thanh ghi ODR của GPIOA.
Truy cập bộ nhớ bằng bí danh

Phương pháp trên thật sự có thể thao tác với địa chỉ của bộ đếm, nhưng thao tác khá rắc rối, người dùng cũng không rõ chức năng của địa chỉ này. Nếu chúng ta đặt một tên cho mỗi địa chỉ, thì khi nhìn thấy tên không phải rõ hơn sao về chức năng của địa chỉ đó?

c
#define GPIOA_ODR   (*(volatile uint32_t*)0x40020014)

int main(void) {
    GPIOA_ODR = 0xFFFF;  // Đặt tất cả 16 bit lên mức 1
    while(1);
}

Giải thích:

  • volatile: thông báo cho trình biên dịch rằng giá trị tại địa chỉ này có thể thay đổi bất kỳ lúc nào (tránh tối ưu hóa)
  • 0xFFFF = 0b1111_1111_1111_1111 → tất cả 16 chân = 1

Hình minh họa thao tác bit

Giả sử thanh ghi GPIOA_ODR có 32 bit, nhưng chỉ dùng 16 bit đầu cho chân PA0–PA15:

c
Bit:  31       ...         16 | 15  14 ... 1   0
       [không dùng]           | 1   1     1   1  → tất cả chân PAx = 1

Việc ghi 0xFFFF vào thanh ghi này tương đương xuất mức cao ra toàn bộ chân.

img

Giới thiệu hàm thư viện

Tại sao nên dùng hàm thư viện (thay vì thao tác trực tiếp thanh ghi)?

Trong phần trước, bạn đã thấy cách điều khiển thiết bị ngoại vi bằng cách truy cập trực tiếp thanh ghi – tuy chính xác và nhanh, nhưng:

  • STM32 có rất nhiều thanh ghi, mỗi ngoại vi (GPIO, UART, TIM…) lại có cấu trúc khác nhau.
  • Bạn phải tự tra địa chỉ, offset, bit mask, giá trị cần gán…
  • Việc này rất tốn thời gian, dễ viết sai, và khó bảo trì

Do đó, hàm thư viện ra đời để đơn giản hóa công việc này.

Thư viện là gì?

Thư viện là các hàm có sẵn, được hãng ST cung cấp, giúp bạn:

  • Không cần nhớ địa chỉ thanh ghi
  • Không cần lo về cách gán bit
  • Chỉ cần gọi hàm đúng chức năng

Ví dụ: Thay vì viết thủ công như:

c
*(volatile uint32_t*)(0x40020014) = 0xFFFF;

Bạn chỉ cần:

c
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_All, GPIO_PIN_SET);

Thư viện nằm ở đâu?

Trong project STM32, bạn sẽ thấy:

  • stm32f4xx_gpio.c/h: Thư viện điều khiển GPIO
  • stm32f4xx_hal_*.c/h: Thư viện HAL (Hardware Abstraction Layer) – phổ biến nhất hiện nay

Hoặc nếu dùng firmware cũ: STM32F4xx_StdPeriph_Driver.

So sánh nhanh: Thanh ghi vs. Hàm thư viện

Tiêu chíTruy cập thanh ghiDùng hàm thư viện
Tốc độ thực thiNhanh, tối ưuChậm hơn một chút
Tiêu tốn bộ nhớÍtNhiều hơn
Dễ dùngKhó, cần hiểu phần cứng rõDễ, chỉ cần gọi đúng hàm
Khả năng mở rộngKém, khó tái sử dụngCao, dễ phát triển dự án
Hiểu bản chấtRất rõ ràngBị che giấu phần nền

Khi nào dùng gì?

  • Dùng thanh ghi khi:

    • Cần tốc độ cao (ví dụ: điều khiển thời gian thực)
    • Tài nguyên bộ nhớ bị hạn chế (vi điều khiển nhỏ)
    • Muốn hiểu rõ phần cứng hoạt động như thế nào
  • Dùng hàm thư viện khi:

    • Phát triển nhanh
    • Làm việc nhóm, dễ bảo trì
    • Không cần tối ưu quá sâu