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_0000trở đ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:
#define GPIOA_ODR   (*(volatile uint32_t*)0x40020014)Ở đây:
- 0x40020014là địa chỉ thực của thanh ghi điều khiển output của GPIOA
- GPIOA_ODRlà 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 | 
|---|---|---|---|
| APB1 | 0x4000 0000 | 0x4000 0000 – 0x4001 FFFF | 42 MHz | 
| APB2 | 0x4001 0000 | 0x4001 0000 – 0x4002 FFFF | 84 MHz | 
| AHB1 | 0x4002 0000 | 0x4002 0000 – 0x4FFF FFFF | 168 MHz | 
| AHB2 | 0x5000 0000 | 0x5000 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 | 
|---|---|---|
| GPIOA | 0x4002 0000 | 0x0000 0000 | 
| GPIOB | 0x4002 0400 | 0x0000 0400 | 
| GPIOC | 0x4002 0800 | 0x0000 0800 | 
| GPIOD | 0x4002 0C00 | 0x0000 0C00 | 
| GPIOE | 0x4002 1000 | 0x0000 1000 | 
| GPIOF | 0x4002 1400 | 0x0000 1400 | 
| GPIOG | 0x4002 1800 | 0x0000 1800 | 
| GPIOH | 0x4002 1C00 | 0x0000 1C00 | 
| GPIOI | 0x4002 2000 | 0x0000 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:
#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 ghi | Offset | Địa chỉ thực tế | Chức năng | 
|---|---|---|---|
| MODER | 0x00 | 0x40020000 | Chọn chế độ cho chân GPIO | 
| OTYPER | 0x04 | 0x40020004 | Kiểu output (push-pull/open-drain) | 
| OSPEEDR | 0x08 | 0x40020008 | Tốc độ xuất dữ liệu | 
| PUPDR | 0x0C | 0x4002000C | Chọn pull-up/pull-down | 
| IDR | 0x10 | 0x40020010 | Input data | 
| ODR | 0x14 | 0x40020014 | Output data | 
| BSRR | 0x18 | 0x40020018 | Set/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 PA5Cá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 ghi | Offset | Mục đích | 
|---|---|---|
| SR | 0x00 | Status register | 
| DR | 0x04 | Data register | 
| BRR | 0x08 | Baud rate register | 
| CR1,CR2,CR3 | 0x0C,0x10,0x14 | Control 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à:
0x4002 0000 + 0x14 = 0x4002 0014Bướ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 
/* 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- 0x40020014thành con trỏ tới vùng nhớ kiểu- uint32_tcó 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ị0xFFFFcho 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ỉ đó?
#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:
Bit:  31       ...         16 | 15  14 ... 1   0
       [không dùng]           | 1   1     1   1  → tất cả chân PAx = 1Việc ghi 0xFFFF vào thanh ghi này tương đương xuất mức cao ra toàn bộ chân.

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ư:
*(volatile uint32_t*)(0x40020014) = 0xFFFF;Bạn chỉ cần:
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 ghi | Dùng hàm thư viện | 
|---|---|---|
| Tốc độ thực thi | Nhanh, tối ưu | Chậm hơn một chút | 
| Tiêu tốn bộ nhớ | Ít | Nhiều hơn | 
| Dễ dùng | Khó, cần hiểu phần cứng rõ | Dễ, chỉ cần gọi đúng hàm | 
| Khả năng mở rộng | Kém, khó tái sử dụng | Cao, dễ phát triển dự án | 
| Hiểu bản chất | Rất rõ ràng | Bị 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