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.
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ền | Chứa gì | Đọc ghi được không |
|---|---|---|---|
| Flash | 0x0800 0000 | Mã 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 |
| SRAM | 0x2000 0000 | Biến toàn cục, ngăn xếp, vùng cấp phát | Đọc ghi tự do |
| Ngoại vi | 0x4000 0000 | Thanh ghi GPIO, UART, SPI, timer | Tùy thanh ghi, có cái chỉ đọc, có cái chỉ ghi |
| Lõi Cortex-M | 0xE000 0000 | NVIC, SysTick, SCB, đơn vị gỡ lỗi | Giống ngoại vi |
#Tính địa chỉ một thanh ghi
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.
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.
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 đối | Chức năng |
|---|---|---|---|
| MODER | 0x00 | 0x4002 0000 | Chế độ từng chân, hai bit mỗi chân |
| OTYPER | 0x04 | 0x4002 0004 | Kiểu đầu ra, đẩy kéo hay cực máng hở |
| OSPEEDR | 0x08 | 0x4002 0008 | Tốc độ chuyển mức |
| PUPDR | 0x0C | 0x4002 000C | Điện trở kéo lên hoặc kéo xuống |
| IDR | 0x10 | 0x4002 0010 | Dữ liệu vào, chỉ đọc |
| ODR | 0x14 | 0x4002 0014 | Dữ liệu ra |
| BSRR | 0x18 | 0x4002 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ớ SRAM | Thanh ghi ngoại vi | |
|---|---|---|
| Đọc lại cái vừa ghi | Luôn được đúng giá trị | Có thể khác hẳn |
| Đọc hai lần liên tiếp | Cùng giá trị | Có thể khác nhau |
| Đọc có tác dụng phụ | Không | Có, ví dụ đọc DR xóa cờ RXNE |
| Ghi có tác dụng phụ | Không | Có, ví dụ ghi BSRR đổi ODR |
| Truy cập một byte trong từ 32 bit | Được | Nhiều thanh ghi cấm |
| Trình biên dịch được bỏ bớt truy cập | Được nếu không ai đọc | Khô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ử
- Tra reference manual của chip bạn có, ghi lại địa chỉ nền của GPIOA, USART1 và TIM2.
- Viết struct
GPIO_TypeDefvà dùng_Static_assertkiểm mọi độ dời khớp với datasheet. - 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. - Chạy
arm-none-eabi-size -Atrê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ớ. - Viết
Reset_Handlertự 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_Handlertự chép.datatừ Flash sang SRAM và xóa.bss, vì không có hệ điều hành làm hộ.