Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 47.224 phút đọc

Bản đồ bộ nhớ

Sau bài này bạn sẽ làm được

  • Đọc được bản đồ bộ nhớ của một dòng vi điều khiển
  • Giải thích cơ chế vào ra ánh xạ bộ nhớ
  • Tính địa chỉ một thanh ghi từ địa chỉ nền cộng độ dời
  • Phân biệt vùng Flash, SRAM, ngoại vi và lõi

Trên vi điều khiển không có lệnh riêng để nói chuyện với phần cứng. Mọi thứ đều là địa chỉ: ghi một số vào ô nhớ 0x40020014 thì đèn sáng. Hiểu cơ chế đó là hiểu toàn bộ lập trình nhúng.

#Vào ra ánh xạ bộ nhớ

Vào ra ánh xạ bộ nhớ
Các thanh ghi điều khiển ngoại vi được gán những địa chỉ cố định trong cùng không gian địa chỉ với RAM. Lệnh đọc ghi bộ nhớ thông thường trở thành lệnh điều khiển phần cứng.
Flash, SRAM, ngoại vi và lõi cùng nằm trong một không gian địa chỉ 32 bit duy nhất.
dieu-khien-den.c
#include <stdint.h>

/* Ghi một số vào một địa chỉ, và một cái đèn sáng lên. */
#define GPIOA_ODR (*(volatile uint32_t *)0x40020014UL)

void bat_den(void)
{
    GPIOA_ODR |= (1u << 5);      /* chỉ là một lệnh ghi bộ nhớ */
}

#Bốn vùng địa chỉ

VùngĐịa chỉ nềnChứa gìĐọc ghi được không
Flash0x0800 0000Mã chương trình, hằng số, bảng vectorĐọc tự do, ghi phải qua thủ tục xóa và nạp
SRAM0x2000 0000Biến toàn cục, ngăn xếp, vùng cấp phátĐọc ghi tự do
Ngoại vi0x4000 0000Thanh ghi GPIO, UART, SPI, timerTùy thanh ghi, có cái chỉ đọc, có cái chỉ ghi
Lõi Cortex-M0xE000 0000NVIC, SysTick, SCB, đơn vị gỡ lỗiGiống ngoại vi

#Tính địa chỉ một thanh ghi

  1. Tra bản đồ bộ nhớ để lấy địa chỉ nền của ngoại vi

    Reference manual có một bảng liệt kê mọi ngoại vi kèm địa chỉ nền. GPIOA nằm ở 0x4002 0000.

  2. Tra bản đồ thanh ghi để lấy độ dời

    Chương của từng ngoại vi có bảng liệt kê mọi thanh ghi kèm độ dời. ODR nằm ở độ dời 0x14.

  3. Cộng lại

    0x4002 0000 cộng 0x14 bằng 0x4002 0014. Đó là địa chỉ của thanh ghi dữ liệu ra của cổng A.

Thanh ghiĐộ dờiĐịa chỉ tuyệt đốiChức năng
MODER0x000x4002 0000Chế độ từng chân, hai bit mỗi chân
OTYPER0x040x4002 0004Kiểu đầu ra, đẩy kéo hay cực máng hở
OSPEEDR0x080x4002 0008Tốc độ chuyển mức
PUPDR0x0C0x4002 000CĐiện trở kéo lên hoặc kéo xuống
IDR0x100x4002 0010Dữ liệu vào, chỉ đọc
ODR0x140x4002 0014Dữ liệu ra
BSRR0x180x4002 0018Đặt và xóa bit, chỉ ghi

Kiểm bằng chương trình

kiem-dia-chi.c
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

typedef struct {
    volatile uint32_t MODER;
    volatile uint32_t OTYPER;
    volatile uint32_t OSPEEDR;
    volatile uint32_t PUPDR;
    volatile uint32_t IDR;
    volatile uint32_t ODR;
    volatile uint32_t BSRR;
    volatile uint32_t LCKR;
    volatile uint32_t AFR[2];
} GPIO_TypeDef;

int main(void)
{
    printf("sizeof(GPIO_TypeDef) = %zu\n", sizeof(GPIO_TypeDef));
    printf("offsetof MODER = 0x%02zX\n", offsetof(GPIO_TypeDef, MODER));
    printf("offsetof IDR   = 0x%02zX\n", offsetof(GPIO_TypeDef, IDR));
    printf("offsetof ODR   = 0x%02zX\n", offsetof(GPIO_TypeDef, ODR));
    printf("offsetof BSRR  = 0x%02zX\n", offsetof(GPIO_TypeDef, BSRR));
    printf("offsetof AFR   = 0x%02zX\n", offsetof(GPIO_TypeDef, AFR));

    return 0;
}
terminal
gcc -std=c17 -Wall -Wextra kiem-dia-chi.c -o t && ./t
sizeof(GPIO_TypeDef) = 40
offsetof MODER = 0x00
offsetof IDR   = 0x10
offsetof ODR   = 0x14
offsetof BSRR  = 0x18
offsetof AFR   = 0x20

#Thanh ghi khác ô nhớ thường thế nào

Ô nhớ SRAMThanh ghi ngoại vi
Đọc lại cái vừa ghiLuôn được đúng giá trịCó thể khác hẳn
Đọc hai lần liên tiếpCùng giá trịCó thể khác nhau
Đọc có tác dụng phụKhôngCó, ví dụ đọc DR xóa cờ RXNE
Ghi có tác dụng phụKhôngCó, ví dụ ghi BSRR đổi ODR
Truy cập một byte trong từ 32 bitĐượcNhiều thanh ghi cấm
Trình biên dịch được bỏ bớt truy cậpĐược nếu không ai đọcKhông bao giờ, nhờ volatile

Bit-banding, một cách truy cập từng bit

bit-band.c
/* Cortex-M3 va M4 anh xa moi BIT cua vung 0x2000 0000 tới 0x2010 0000
   va vung 0x4000 0000 tới 0x4010 0000 thanh mot TU rieng.

   Nho vay dat mot bit chi can MOT lenh ghi, khong can doc sua ghi. */

#define BIT_BAND(dia_chi, bit) \
    (*(volatile uint32_t *)((((uint32_t)(dia_chi) & 0xF0000000u) + 0x02000000u) \
                            + (((uint32_t)(dia_chi) & 0x000FFFFFu) << 5) \
                            + ((bit) << 2)))

/* Bat chan 5 cua GPIOA, mot lenh ghi, khong bi ngat xen vao giua */
BIT_BAND(&GPIOA->ODR, 5) = 1;

/* Tuong duong nhung an toan hon hai lenh:
     uint32_t t = GPIOA->ODR;  t |= (1u << 5);  GPIOA->ODR = t; */

#Chương trình nằm ở đâu khi khởi động

Trình tự khởi động
1. Cấp điện hoặc nhấn reset.

2. Lõi đọc 4 byte đầu ở địa chỉ 0x0000 0000
   -> nạp vào con trỏ ngăn xếp chính MSP.
   Đây là lý do phần tử đầu bảng vector là địa chỉ đỉnh ngăn xếp.

3. Lõi đọc 4 byte tiếp theo ở 0x0000 0004
   -> nạp vào PC, tức nhảy tới Reset_Handler.

4. Reset_Handler làm ba việc:
   a. Chép phân đoạn .data từ Flash sang SRAM
      (biến toàn cục có giá trị khởi tạo khác 0)
   b. Đặt phân đoạn .bss về 0
      (biến toàn cục bằng 0 hoặc chưa khởi tạo)
   c. Gọi SystemInit rồi gọi main

5. main chạy, và không bao giờ trả về.
startup.c
#include <stdint.h>

extern uint32_t _sidata, _sdata, _edata, _sbss, _ebss, _estack;

int main(void);
void SystemInit(void);

void Reset_Handler(void)
{
    /* a. Chép .data từ Flash sang SRAM */
    uint32_t *nguon = &_sidata;
    uint32_t *dich  = &_sdata;

    while (dich < &_edata) *dich++ = *nguon++;

    /* b. Xóa .bss */
    dich = &_sbss;

    while (dich < &_ebss) *dich++ = 0;

    /* c. Khởi tạo hệ thống rồi vào main */
    SystemInit();
    main();

    for (;;) { }      /* main không nên trả về, nhưng phòng trường hợp đó */
}

/* Bảng vector nằm đầu Flash, xem Bài 49.2 */
__attribute__((section(".isr_vector"), used))
void (*const bang_vector[])(void) = {
    (void (*)(void))&_estack,      /* 0: đỉnh ngăn xếp ban đầu */
    Reset_Handler,                 /* 1: điểm vào chương trình */
    /* ... các bộ xử lý ngoại lệ khác ... */
};
terminal
arm-none-eabi-size -A firmware.elf
section          size        addr
.isr_vector       428   134217728
.text           17812   134218156
.rodata          1024   134235968
.data             108   536870912
.bss             4372   536871020
Total           23744

134217728 = 0x08000000  -> Flash
536870912 = 0x20000000  -> SRAM
# Xem bản sao gốc của .data nằm ở đâu trong Flash
arm-none-eabi-objdump -h firmware.elf | grep -A1 '\.data'
  4 .data         0000006c  20000000  08004b94  00010000  2**2
                  CONTENTS, ALLOC, LOAD, DATA

VMA 0x20000000 (SRAM luc chay), LMA 0x08004b94 (Flash luc nap)

Tự làm thử

  1. Tra reference manual của chip bạn có, ghi lại địa chỉ nền của GPIOA, USART1 và TIM2.
  2. Viết struct GPIO_TypeDef và dùng _Static_assert kiểm mọi độ dời khớp với datasheet.
  3. Thử đọc một địa chỉ không có gì và xem chip nhảy vào HardFault_Handler, rồi viết hàm đó in ra địa chỉ gây lỗi.
  4. Chạy arm-none-eabi-size -A trên một chương trình của bạn và đối chiếu địa chỉ từng phân đoạn với bản đồ bộ nhớ.
  5. Viết Reset_Handler tự tay, cố tình bỏ vòng lặp xóa .bss, rồi in giá trị một biến toàn cục chưa khởi tạo.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Thanh ghi ngoại vi nằm trong cùng không gian địa chỉ với RAM, nên điều khiển phần cứng chỉ là đọc ghi bộ nhớ.
  • Địa chỉ một thanh ghi bằng địa chỉ nền của ngoại vi cộng độ dời ghi trong reference manual.
  • Thanh ghi khác ô nhớ thường: có cái chỉ đọc, có cái chỉ ghi, và nhiều cái tự xóa cờ khi đọc.
  • Dùng struct kiểu CMSIS thì độ dời tự khớp, nhưng phải chèn trường dành riêng cho mọi lỗ hổng và kiểm bằng _Static_assert.
  • Lúc khởi động, Reset_Handler tự chép .data từ Flash sang SRAM và xóa .bss, vì không có hệ điều hành làm hộ.