🌱 Core 6. ARM Memory Map & Memory Mapped Registers
Ở bài này chúng ta sẽ cùng tìm hiểu về Memory Mapped Registers - Các thanh ghi ánh xạ bộ nhớ của ARM Cortex Mx, và bản đồ bộ nhớ - Memory Map của Vi điều khiển STM32.
Memory Mapped & Non - Memory Mapped Registers
Như ở bài Core 3 chúng ta đã cùng tìm hiểu về Core Registers, có thể thấy các Core Registers đều không có địa chỉ, chúng được gọi là những Thanh ghi Non – Memory Mapped.
Chúng đều nằm trong Core bộ xử lý và không có bất kỳ địa chỉ nào để truy cập
chúng từ chương trình 'C'. Muốn truy cập đến chúng, bắt buộc phải dùng tập lệnh ASM –
Assembly.
Chẳng hạn: MOV R0, 1H
Ngược lại với Non - Memory Mapped Registers, các Memory Mapped Register - Thanh ghi ánh xạ bộ nhớ là một phần của Memory - Bản đồ bộ nhớ.
Mỗi thanh ghi này đều có địa chỉ. Bằng cách sử dụng địa chỉ này, ta có thể đọc hoặc ghi dữ liệu vào thanh ghi này bằng chương trình 'C'. Sử dụng địa chỉ dereferencing của chúng.
Memory Mapped Registers chia làm 2 loại:
- Các thanh ghi liên quan đến ngoại vi bộ xử lý (NVIC – Quản lý vector ngắt, MPU – Bảo vệ bộ nhớ, SCB – Điều khiển hệ thống, DEBUG – Bộ gỡ lỗi,…)
- Các thanh ghi liên quan ngoại vi của vi điều khiển (GPIO, RTC, I2C, TIMER, CAN, USB,…)
Cách truy cập Memory Mapped Registers
Để truy cập vào Memory Mapped Registers từ chương trình C, chúng ta sử dụng con trỏ (pointer) để dereferencing địa chỉ của thanh ghi. Có 3 cách phổ biến:
1. Truy cập trực tiếp qua con trỏ:
// Ví dụ: Đọc giá trị từ thanh ghi GPIOA_IDR (Input Data Register)
#define GPIOA_IDR_ADDR 0x40020010
uint32_t *pGPIOA_IDR = (uint32_t*)GPIOA_IDR_ADDR;
uint32_t value = *pGPIOA_IDR; // Đọc giá trị
// Ghi giá trị vào thanh ghi GPIOA_ODR (Output Data Register)
#define GPIOA_ODR_ADDR 0x40020014
uint32_t *pGPIOA_ODR = (uint32_t*)GPIOA_ODR_ADDR;
*pGPIOA_ODR = 0x0001; // Ghi giá trị
2. Sử dụng Structure để nhóm các thanh ghi:
Đây là cách được khuyến nghị và được sử dụng trong CMSIS (Cortex Microcontroller Software Interface Standard):
// Định nghĩa cấu trúc cho GPIO
typedef struct {
volatile uint32_t MODER; // Mode register
volatile uint32_t OTYPER; // Output type register
volatile uint32_t OSPEEDR; // Output speed register
volatile uint32_t PUPDR; // Pull-up/pull-down register
volatile uint32_t IDR; // Input data register
volatile uint32_t ODR; // Output data register
volatile uint32_t BSRR; // Bit set/reset register
volatile uint32_t LCKR; // Configuration lock register
volatile uint32_t AFR[2]; // Alternate function registers
} GPIO_TypeDef;
// Ánh xạ địa chỉ base của GPIOA
#define GPIOA_BASE 0x40020000
#define GPIOA ((GPIO_TypeDef*)GPIOA_BASE)
// Sử dụng
GPIOA->ODR = 0x0001; // Ghi vào Output Data Register
uint32_t input = GPIOA->IDR; // Đọc từ Input Data Register
3. Sử dụng Bit-banding (chỉ có trên Cortex-M3/M4):
Bit-banding cho phép truy cập từng bit riêng lẻ như thể nó là một byte trong bộ nhớ:
// Công thức tính địa chỉ bit-band
// bit_band_addr = bit_band_base + (byte_offset × 32) + (bit_number × 4)
#define BITBAND_PERI_REF 0x40000000
#define BITBAND_PERI_BASE 0x42000000
#define BITBAND_PERI(addr, bit) \
((BITBAND_PERI_BASE + ((addr) - BITBAND_PERI_REF) * 32 + (bit) * 4))
// Ví dụ: Set bit 5 của GPIOA_ODR
#define GPIOA_ODR 0x40020014
volatile uint32_t *bit5 = (uint32_t*)BITBAND_PERI(GPIOA_ODR, 5);
*bit5 = 1; // Set bit 5
*bit5 = 0; // Clear bit 5
Lưu ý quan trọng: Luôn sử dụng từ khóa volatile khi khai báo con trỏ trỏ đến thanh ghi phần cứng. Điều này đảm bảo compiler không tối ưu hóa bỏ qua các lệnh đọc/ghi, vì giá trị thanh ghi có thể thay đổi bởi phần cứng.
Memory Map
Có 2 loại bộ nhớ: Bộ nhớ chương trình – Code Memory và
Bộ nhớ dữ liệu – Data Memory.
- Bộ nhớ chương trình là nơi lưu trữ tạm thời các lệnh của chương trình
- Bộ nhớ dữ liệu là nơi lưu trữ dữ liệu tạm thời của chương trình
Có một bus kết nối giữa bộ xử lý với các bộ nhớ và thiết
bị ngoại vi khác nhau: Kênh địa chỉ 32 bit và kênh dữ liệu 32 bit.
Bộ xử lý 32 bits của Vi điều khiển STM32 có thể tạo ra 4 GB giá trị vị trí bộ nhớ khác
nhau, từ 0 đến 0xFFFF FFFF.
- Vùng nhớ Code - 0.5GB: Có thể là bộ nhớ flash, EEPROM, ROM, OTP, … dùng để lưu code và các lệnh của chương trình.
- Vùng nhớ SRAM - 512MB: Chủ yếu sử dụng để kết nối SRAM trên chip, dùng để lưu trữ dữ liệu tạm thời khi run-time.
- Vùng nhớ Peripheral - 512MB: Sử dụng cho các thiết bị ngoại vi trên chip – của vi điều khiển.
- Vùng nhớ External RAM - 1GB: Kết nối các RAM ngoài chẳng hạn SDRAM.
- Vùng nhớ Private Peripheral Bus - 512MB: Vùng Bus ngoại vi riêng tư: Các thanh ghi ngoại vi của bộ xử lý: như NVIC, Systick, ...
Chi tiết các vùng nhớ trong ARM Cortex-M
1. Vùng nhớ Code (0x0000 0000 - 0x1FFF FFFF)
Vùng nhớ Code có dung lượng 512MB, được chia thành các phần:
- Flash Memory (0x0800 0000 - 0x080F FFFF): Lưu trữ chương trình chính, vector table, và các hằng số (const). Trên STM32F4, Flash có thể từ 512KB đến 2MB tùy model.
- System Memory (0x1FFF 0000 - 0x1FFF 77FF): Chứa bootloader của nhà sản xuất, dùng để nạp firmware qua UART, USB, CAN.
- OTP (One-Time Programmable) Area: Vùng nhớ chỉ ghi được một lần, dùng để lưu serial number, MAC address, license key.
2. Vùng nhớ SRAM (0x2000 0000 - 0x3FFF FFFF)
SRAM là bộ nhớ truy cập nhanh nhất, dùng để lưu:
- Stack: Lưu biến local, return address, context của hàm
- Heap: Vùng nhớ động (malloc, new)
- Global/Static variables: Biến toàn cục và static
- .data section: Biến đã khởi tạo giá trị
- .bss section: Biến chưa khởi tạo (zero-initialized)
STM32F4 có thể có nhiều vùng SRAM:
- SRAM1: 112KB, có thể truy cập qua DMA
- SRAM2: 16KB, có thể truy cập qua DMA
- CCM RAM (Core Coupled Memory): 64KB, chỉ CPU truy cập được, không qua bus matrix nên nhanh hơn nhưng DMA không dùng được
3. Vùng nhớ Peripheral (0x4000 0000 - 0x5FFF FFFF)
Vùng này chứa tất cả các thanh ghi điều khiển ngoại vi của MCU, được chia thành các bus:
- APB1 (Advanced Peripheral Bus 1) - 0x4000 0000: Các ngoại vi tốc độ thấp (TIM2-7, RTC, WWDG, IWDG, SPI2/3, USART2/3, I2C1/2, CAN, PWR, DAC)
- APB2 (Advanced Peripheral Bus 2) - 0x4001 0000: Các ngoại vi tốc độ cao (TIM1/8, USART1, ADC1/2/3, SPI1, SYSCFG, EXTI)
- AHB1 (Advanced High-performance Bus 1) - 0x4002 0000: GPIO, RCC, Flash interface, CRC, DMA1/2
- AHB2 - 0x5000 0000: USB OTG FS, DCMI, RNG
// Ví dụ địa chỉ base của các ngoại vi STM32F4
#define PERIPH_BASE 0x40000000UL
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000UL)
#define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000UL)
#define AHB2PERIPH_BASE (PERIPH_BASE + 0x10000000UL)
// GPIO
#define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000UL) // 0x40020000
#define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400UL) // 0x40020400
#define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800UL) // 0x40020800
// USART
#define USART1_BASE (APB2PERIPH_BASE + 0x1000UL) // 0x40011000
#define USART2_BASE (APB1PERIPH_BASE + 0x4400UL) // 0x40004400
4. Vùng nhớ External RAM (0x6000 0000 - 0x9FFF FFFF)
Vùng này dùng để kết nối bộ nhớ ngoài qua FMC (Flexible Memory Controller) hoặc FSMC (Flexible Static Memory Controller):
- Bank 1 (0x6000 0000 - 0x6FFF FFFF): NOR Flash, PSRAM, SRAM
- Bank 2 (0x7000 0000 - 0x7FFF FFFF): NAND Flash
- Bank 3 (0xC000 0000 - 0xCFFF FFFF): SDRAM (trên STM32F4 có FMC)
5. Vùng Private Peripheral Bus (0xE000 0000 - 0xE00F FFFF)
Đây là vùng chứa các ngoại vi của ARM Core, không phải của nhà sản xuất MCU:
- ITM (Instrumentation Trace Macrocell) - 0xE000 0000: Debug trace
- DWT (Data Watchpoint and Trace) - 0xE000 1000: Debug watchpoint, profiling
- FPB (Flash Patch and Breakpoint) - 0xE000 2000: Hardware breakpoint
- SCS (System Control Space) - 0xE000 E000: Chứa các thanh ghi quan trọng:
- SysTick (0xE000 E010): Timer hệ thống
- NVIC (0xE000 E100): Nested Vectored Interrupt Controller
- SCB (0xE000 ED00): System Control Block
- MPU (0xE000 ED90): Memory Protection Unit
Bus Matrix và Cơ chế truy cập bộ nhớ
ARM Cortex-M sử dụng Bus Matrix để kết nối CPU với các vùng nhớ và ngoại vi. Bus Matrix cho phép nhiều master (CPU, DMA) truy cập đồng thời vào các slave (Flash, SRAM, Peripheral) khác nhau.
Các Bus Interface của Cortex-M4:
- I-Code Bus: Instruction fetch từ vùng Code (0x0000 0000 - 0x1FFF FFFF)
- D-Code Bus: Data access từ vùng Code (đọc const, literal pool)
- System Bus: Truy cập SRAM, Peripheral, External RAM
- PPB (Private Peripheral Bus): Truy cập các ngoại vi của Core
Memory Access Latency:
Thời gian truy cập vào các vùng nhớ khác nhau:
- SRAM: 1 clock cycle (nhanh nhất)
- CCM RAM: 1 clock cycle (nhanh như SRAM nhưng không qua bus matrix)
- Flash: 0-7 wait states tùy tốc độ CPU (ở 168MHz cần 5 wait states)
- Peripheral: 1-2 clock cycles
- External RAM: Phụ thuộc cấu hình FMC/FSMC (thường 10-20 cycles)
Memory Barriers và Memory Ordering
Trong lập trình embedded, đặc biệt khi làm việc với interrupt và DMA, việc đảm bảo thứ tự thực thi các lệnh truy cập bộ nhớ là rất quan trọng. ARM Cortex-M cung cấp các Memory Barrier Instructions:
Các loại Memory Barrier:
- DMB (Data Memory Barrier): Đảm bảo tất cả memory access trước DMB hoàn thành trước khi thực hiện memory access sau DMB
- DSB (Data Synchronization Barrier): Giống DMB nhưng còn đợi tất cả instruction hoàn thành
- ISB (Instruction Synchronization Barrier): Flush pipeline, đảm bảo instruction sau ISB được fetch lại
// Ví dụ sử dụng Memory Barrier trong CMSIS
#include "core_cm4.h"
void configure_peripheral(void) {
// Cấu hình thanh ghi điều khiển
PERIPHERAL->CTRL = 0x01;
// Đảm bảo ghi vào CTRL hoàn thành trước khi enable
__DMB();
// Enable peripheral
PERIPHERAL->ENABLE = 0x01;
}
void enter_critical_section(void) {
__disable_irq(); // Tắt interrupt
__DSB(); // Đảm bảo disable_irq có hiệu lực
__ISB(); // Flush pipeline
}
void exit_critical_section(void) {
__DSB(); // Đảm bảo tất cả thao tác hoàn thành
__ISB(); // Flush pipeline
__enable_irq(); // Bật lại interrupt
}
// Ví dụ với DMA
void setup_dma_transfer(uint32_t *src, uint32_t *dst, uint32_t size) {
// Chuẩn bị data trong buffer
for(int i = 0; i < size; i++) {
src[i] = i;
}
// Đảm bảo data đã được ghi vào memory
__DMB();
// Cấu hình và start DMA
DMA->SRC_ADDR = (uint32_t)src;
DMA->DST_ADDR = (uint32_t)dst;
DMA->SIZE = size;
DMA->CTRL = DMA_START;
}
Khi nào cần dùng Memory Barrier:
- Khi cấu hình DMA trước khi start transfer
- Khi thay đổi cấu hình interrupt (enable/disable)
- Khi thay đổi memory mapping hoặc MPU configuration
- Khi làm việc với shared data giữa interrupt và main code
- Khi cấu hình cache (trên Cortex-M7)
Ví dụ thực tế: Điều khiển LED qua GPIO
Hãy cùng xem một ví dụ hoàn chỉnh về cách sử dụng Memory Mapped Registers để điều khiển LED trên STM32F4:
// Định nghĩa địa chỉ base
#define RCC_BASE 0x40023800
#define GPIOA_BASE 0x40020000
// Định nghĩa cấu trúc RCC (Reset and Clock Control)
typedef struct {
volatile uint32_t CR; // Clock control register
volatile uint32_t PLLCFGR; // PLL configuration register
volatile uint32_t CFGR; // Clock configuration register
volatile uint32_t CIR; // Clock interrupt register
volatile uint32_t AHB1RSTR; // AHB1 peripheral reset register
volatile uint32_t AHB2RSTR; // AHB2 peripheral reset register
volatile uint32_t AHB3RSTR; // AHB3 peripheral reset register
uint32_t RESERVED0;
volatile uint32_t APB1RSTR; // APB1 peripheral reset register
volatile uint32_t APB2RSTR; // APB2 peripheral reset register
uint32_t RESERVED1[2];
volatile uint32_t AHB1ENR; // AHB1 peripheral clock enable register
volatile uint32_t AHB2ENR; // AHB2 peripheral clock enable register
volatile uint32_t AHB3ENR; // AHB3 peripheral clock enable register
uint32_t RESERVED2;
volatile uint32_t APB1ENR; // APB1 peripheral clock enable register
volatile uint32_t APB2ENR; // APB2 peripheral clock enable register
} RCC_TypeDef;
// Định nghĩa cấu trúc GPIO
typedef struct {
volatile uint32_t MODER; // Mode register
volatile uint32_t OTYPER; // Output type register
volatile uint32_t OSPEEDR; // Output speed register
volatile uint32_t PUPDR; // Pull-up/pull-down register
volatile uint32_t IDR; // Input data register
volatile uint32_t ODR; // Output data register
volatile uint32_t BSRR; // Bit set/reset register
volatile uint32_t LCKR; // Configuration lock register
volatile uint32_t AFR[2]; // Alternate function registers
} GPIO_TypeDef;
// Ánh xạ địa chỉ
#define RCC ((RCC_TypeDef*)RCC_BASE)
#define GPIOA ((GPIO_TypeDef*)GPIOA_BASE)
// Bit definitions
#define RCC_AHB1ENR_GPIOAEN (1 << 0) // GPIOA clock enable bit
void led_init(void) {
// Bước 1: Enable clock cho GPIOA
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
// Bước 2: Cấu hình PA5 là output (LED trên STM32F4 Discovery)
// MODER[11:10] = 01 (General purpose output mode)
GPIOA->MODER &= ~(0x3 << (5 * 2)); // Clear bits
GPIOA->MODER |= (0x1 << (5 * 2)); // Set to output
// Bước 3: Cấu hình output type là push-pull
GPIOA->OTYPER &= ~(1 << 5); // 0 = Push-pull
// Bước 4: Cấu hình output speed là medium
GPIOA->OSPEEDR &= ~(0x3 << (5 * 2));
GPIOA->OSPEEDR |= (0x1 << (5 * 2)); // 01 = Medium speed
// Bước 5: Không dùng pull-up/pull-down
GPIOA->PUPDR &= ~(0x3 << (5 * 2)); // 00 = No pull-up, pull-down
}
void led_on(void) {
// Cách 1: Ghi trực tiếp vào ODR
GPIOA->ODR |= (1 << 5);
// Cách 2: Dùng BSRR (Bit Set/Reset Register) - Atomic operation
// GPIOA->BSRR = (1 << 5); // Set bit 5
}
void led_off(void) {
// Cách 1: Ghi trực tiếp vào ODR
GPIOA->ODR &= ~(1 << 5);
// Cách 2: Dùng BSRR - Atomic operation
// GPIOA->BSRR = (1 << (5 + 16)); // Reset bit 5
}
void led_toggle(void) {
GPIOA->ODR ^= (1 << 5); // XOR để toggle
}
// Delay đơn giản (không chính xác)
void delay_ms(uint32_t ms) {
for(uint32_t i = 0; i < ms * 1000; i++) {
__NOP(); // No operation
}
}
int main(void) {
led_init();
while(1) {
led_on();
delay_ms(500);
led_off();
delay_ms(500);
}
}
- RCC->AHB1ENR: Phải enable clock cho GPIO trước khi sử dụng, nếu không thanh ghi GPIO sẽ không hoạt động
- MODER: Mỗi pin dùng 2 bit (00=Input, 01=Output, 10=Alternate function, 11=Analog)
- BSRR: Bit 0-15 để set, bit 16-31 để reset. Đây là atomic operation, an toàn trong interrupt
- volatile: Bắt buộc phải có để compiler không tối ưu hóa bỏ qua các lệnh đọc/ghi thanh ghi
Memory Protection Unit (MPU)
MPU là một tính năng tùy chọn trên ARM Cortex-M3/M4/M7, cho phép chia vùng nhớ thành các region với các quyền truy cập khác nhau. MPU giúp:
- Bảo vệ vùng nhớ quan trọng (OS kernel, critical data)
- Phát hiện lỗi truy cập bộ nhớ sai (buffer overflow, null pointer)
- Tách biệt các task trong RTOS
- Ngăn chặn thực thi code từ vùng data (security)
Cấu hình MPU Region:
// Ví dụ cấu hình MPU để bảo vệ vùng Flash
void mpu_config(void) {
// Disable MPU
MPU->CTRL = 0;
// Configure Region 0: Flash (Read-only, Executable)
MPU->RNR = 0; // Region number 0
MPU->RBAR = 0x08000000; // Base address: Flash start
MPU->RASR = (0x1D << 1) | // Size: 2^(0x1D+1) = 1GB
(0x06 << 24) | // AP: Read-only
(1 << 0); // Enable region
// Configure Region 1: SRAM (Read-Write, Non-executable)
MPU->RNR = 1; // Region number 1
MPU->RBAR = 0x20000000; // Base address: SRAM start
MPU->RASR = (0x11 << 1) | // Size: 2^(0x11+1) = 128KB
(0x03 << 24) | // AP: Full access
(1 << 28) | // XN: Execute Never
(1 << 0); // Enable region
// Enable MPU with default memory map
MPU->CTRL = (1 << 2) | // PRIVDEFENA: Enable default memory map
(1 << 0); // ENABLE: Enable MPU
// Memory barrier
__DSB();
__ISB();
}
Khi có vi phạm MPU (truy cập vào vùng cấm), CPU sẽ trigger MemManage Exception, cho phép developer xử lý lỗi một cách có kiểm soát thay vì crash không rõ nguyên nhân.
>>>>>> Follow ngay <<<<<<<
Để nhận được những bài học miễn phí mới nhất nhé 😊
Chúc các bạn học tập tốt 😊


