Chuỗi công cụ, nạp và gỡ lỗi
Sau bài này bạn sẽ làm được
- Biên dịch chéo bằng arm-none-eabi-gcc với đúng cờ
- Đọc tệp linker và hiểu bốn phân đoạn chính
- Đọc kết quả của lệnh size và biết chương trình còn bao nhiêu RAM
- Gỡ lỗi bằng SWD, bằng UART và bằng chân GPIO
Trên máy tính, gcc a.c rồi ./a.out là xong. Trên vi điều khiển, chương trình phải được dịch cho một kiến trúc khác, sắp xếp vào đúng địa chỉ, ghi vào Flash qua một sợi dây, rồi gỡ lỗi mà không có màn hình. Bài này đi qua cả bốn bước.
#Biên dịch chéo
gcc (GCC) 14.2.0 <- dich cho MAY CUA BAN
arm-none-eabi-gcc (Arm GNU Toolchain 13.3) 13.3.1 arm : kien truc dich none : khong he dieu hanh eabi : quy uoc goi ham chuan cua ARM
CC = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
SIZE = arm-none-eabi-size
# Chọn lõi. Sai một chữ ở đây là chương trình không chạy.
CPU = -mcpu=cortex-m4 -mthumb
FPU = -mfpu=fpv4-sp-d16 -mfloat-abi=hard
# Mỗi hàm và mỗi biến vào một phân đoạn riêng, để linker vứt được cái thừa
TACH = -ffunction-sections -fdata-sections
CFLAGS = $(CPU) $(FPU) $(TACH) -std=c11 -Os -g3
CFLAGS += -Wall -Wextra -Wdouble-promotion -Wconversion
CFLAGS += -DSTM32F401xE -Iinc
LDFLAGS = $(CPU) $(FPU) -T stm32f401.ld
LDFLAGS += -Wl,--gc-sections -Wl,-Map=build/firmware.map
LDFLAGS += -specs=nano.specs -specs=nosys.specs
NGUON = $(wildcard src/*.c)
DOI = $(NGUON:src/%.c=build/%.o)
all: build/firmware.bin
build/%.o: src/%.c | build
$(CC) $(CFLAGS) -c $< -o $@
build/firmware.elf: $(DOI)
$(CC) $(LDFLAGS) $^ -o $@
$(SIZE) $@
build/firmware.bin: build/firmware.elf
$(OBJCOPY) -O binary $< $@
build:
mkdir -p build
clean:
rm -rf build
.PHONY: all clean| Cờ | Ý nghĩa | Hậu quả nếu sai |
|---|---|---|
| -mcpu=cortex-m4 | Sinh mã cho lõi Cortex-M4 | Chip gặp lệnh không hiểu và nhảy vào HardFault |
| -mthumb | Dùng tập lệnh Thumb, bắt buộc trên Cortex-M | Linker báo lỗi hoặc chip fault ngay lệnh đầu |
| -mfpu=fpv4-sp-d16 | FPU đơn chính xác 16 thanh ghi kép | Không dùng được FPU, hoặc sinh lệnh chip không có |
| -mfloat-abi=hard | Truyền tham số float qua thanh ghi FPU | Trộn hard và soft thì linker từ chối liên kết |
| -T stm32f401.ld | Dùng tệp linker này để sắp địa chỉ | Chương trình bị đặt ở địa chỉ 0, nạp vào không chạy |
| -specs=nano.specs | Dùng newlib-nano thay newlib đầy đủ | Tốn thêm hàng chục KB Flash |
| -specs=nosys.specs | Nối các lời gọi hệ thống vào hàm rỗng | Linker báo thiếu _write, _sbrk, _close |
#Tệp linker và bốn phân đoạn
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 96K
}
/* Đỉnh ngăn xếp: cuối RAM, vì ngăn xếp mọc xuống */
_dinh_ngan_xep = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
/* 1. Bảng vector, PHẢI nằm đúng đầu Flash */
.isr_vector : {
KEEP(*(.isr_vector)) /* KEEP: đừng vứt, dù không ai gọi */
} > FLASH
/* 2. Mã lệnh */
.text : {
*(.text)
*(.text*)
*(.glue_7) *(.glue_7t)
. = ALIGN(4);
_het_text = .;
} > FLASH
/* 3. Hằng số chỉ đọc */
.rodata : {
*(.rodata)
*(.rodata*)
. = ALIGN(4);
} > FLASH
/* 4. Biến có giá trị đầu: CHẠY ở RAM, NẰM ở Flash */
_dau_nguon_data = LOADADDR(.data);
.data : {
. = ALIGN(4);
_dau_data = .;
*(.data)
*(.data*)
. = ALIGN(4);
_cuoi_data = .;
} > RAM AT> FLASH /* <- hai vùng nhớ, đây là mấu chốt */
/* 5. Biến không có giá trị đầu: chỉ chiếm chỗ RAM */
.bss : {
. = ALIGN(4);
_dau_bss = .;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_cuoi_bss = .;
} > RAM
/* 6. Phần RAM còn lại, chỉ để kiểm tra lúc liên kết */
. = ALIGN(8);
_dau_heap = .;
.kiem_tra_ngan_xep : {
. = . + 1K; /* heap tối thiểu */
. = . + 2K; /* ngăn xếp tối thiểu */
. = ALIGN(8);
} > RAM /* linker báo lỗi nếu không đủ chỗ */
}| Phân đoạn | Nằm ở | Chạy ở | Chứa gì |
|---|---|---|---|
| .text | Flash | Flash | Mã lệnh, chạy trực tiếp từ Flash không cần chép |
| .rodata | Flash | Flash | Chuỗi hằng, bảng const, giá trị khởi tạo của mảng const |
| .data | Flash | RAM | Biến toàn cục có giá trị đầu khác không |
| .bss | Không nằm đâu cả | RAM | Biến toàn cục bằng không hoặc không khởi tạo |
const char ten[] = "may do nhiet"; /* .rodata, trong Flash */
uint32_t dem = 0; /* .bss, khong ton Flash */
uint32_t nguong = 2500; /* .data, ton CA Flash lan RAM */
static char bo_dem[512]; /* .bss, 512 byte RAM, 0 byte Flash */
void tang(void) { ++dem; } /* .text, trong Flash */00000000 g F .text 0000000c tang 00000000 g O .rodata 0000000d ten 00000000 g O .data 00000004 nguong 00000000 g O .bss 00000004 dem 00000004 l O .bss 00000200 bo_dem
#Đọc kết quả size
text data bss dec hex filename 11704 108 4372 16184 3f38 build/firmware.elf
| Cột | Giá trị | Nó là gì |
|---|---|---|
| text | 11704 | Mã lệnh cộng hằng số chỉ đọc, nằm trong Flash |
| data | 108 | Biến có giá trị đầu, chiếm CẢ Flash lẫn RAM |
| bss | 4372 | Biến khởi tạo bằng không, chỉ chiếm RAM |
| dec | 16184 | Tổng ba cột trên, ít khi có ích |
| hex | 3f38 | Chính là dec viết dạng thập lục phân |
Flash dung = text + data = 11704 + 108 = 11812 byte
RAM dung = data + bss = 108 + 4372 = 4480 byte
Voi STM32F401RE (512 KB Flash, 96 KB RAM):
Flash: 11812 / 524288 = 2.3 phan tram
RAM : 4480 / 98304 = 4.6 phan tram
Con lai cho ngan xep va heap: 98304 - 4480 = 93824 byte.section size addr .isr_vector 428 134217728 .text 10252 134218156 .rodata 1024 134228408 .data 108 536870912 .bss 4372 536871020 Total 16184
0800215c 00000488 T _vfprintf_r 080018a4 000002b8 T __sfvwrite_r 08001b5c 000001f4 T _malloc_r 08002834 00000180 T loc_fft 080029b4 000000f8 T lenh_chay 08002aac 000000c4 T uart_gui_chuoi
20000418 00001000 b bo_dem_mau 20001418 00000200 b rx_vong 20001618 00000100 b tx_vong 2000171c 00000040 b s_lich_su 2000175c 00000018 d cau_hinh
#Nạp chương trình
Từ elf sang bin hoặc hex
Tệp
.elfchứa cả thông tin gỡ lỗi và địa chỉ, còn.binchỉ là các byte thô sẽ nằm trong Flash. Công cụ nạp cần một trong hai, tùy loại.arm-none-eabi-objcopy -O binary firmware.elf firmware.bin arm-none-eabi-objcopy -O ihex firmware.elf firmware.hexNối bo mạch qua SWD
SWD cần đúng bốn dây:
SWCLK,SWDIO,GNDvàNRST. Bo phát triển ST Nucleo và Discovery đã có sẵn mạch ST-LINK trên bo, chỉ cần cắm cáp USB.Ghi vào Flash
Công cụ xóa các trang Flash cần thiết, ghi dữ liệu, rồi đọc lại để so sánh. Bước so sánh mất thêm vài giây nhưng phát hiện được Flash hỏng, nên đừng tắt.
Đặt lại và chạy
Chip đọc hai từ đầu ở
0x08000000: đỉnh ngăn xếp và địa chỉReset_Handler. Xem lại Bài 49.2 về bảng vector.
st-flash 1.8.0 2026-08-18T10:14:22 INFO common.c: F4xx: 96 KiB SRAM, 512 KiB flash 2026-08-18T10:14:22 INFO common.c: Attempting to write 11812 bytes 2026-08-18T10:14:23 INFO common.c: Flash page at 0x8000000 erased 2026-08-18T10:14:24 INFO common.c: Starting verification of write complete 2026-08-18T10:14:24 INFO common.c: Flash written and verified! jolly good!
Info : clock speed 2000 kHz Info : STLINK V2J37 (API v2) VID:PID 0483:374B Info : stm32f4x.cpu: Cortex-M4 r0p1 processor detected ** Programming Started ** Info : Padding image section 0 at 0x08002e24 with 4 bytes ** Programming Finished ** ** Verified OK ** ** Resetting Target **
Opening DFU capable USB device... Device ID 0483:df11 Downloading element to address = 0x08000000, size = 11812 Download [=========================] 100% 11812 bytes File downloaded successfully
#Bốn cách gỡ lỗi
| Cách | Nhiễu vào thời gian | Cần gì | Hợp cho |
|---|---|---|---|
| GDB qua SWD | Dừng hẳn chương trình | Mạch nạp | Lỗi logic, xem biến, đọc thanh ghi |
| In qua UART | Hàng chục tới hàng trăm micro giây | Một dây và bộ chuyển USB | Theo dõi luồng chạy, ghi nhật ký |
| Đảo chân GPIO | Hai chu kỳ, khoảng 24 ns | Máy hiện sóng | Đo thời gian ISR, tìm tranh chấp |
| Đèn và biến đếm | Gần bằng không | Không cần gì | Xác nhận có tới được chỗ này không |
GDB qua SWD
Info : Listening on port 3333 for gdb connections
(gdb) target extended-remote :3333 (gdb) monitor reset halt (gdb) load (gdb) break lenh_chay (gdb) continue
# Xem thanh ghi ngoại vi bang dia chi
(gdb) x/4xw 0x40020000 # GPIOA_MODER va ba thanh ghi sau
# Dat diem dung theo dieu kien, chi dung khi bien vuot nguong
(gdb) break loc.c:42 if x > 4000
# Diem theo doi: dung khi mot bien BI GHI, du o dau
(gdb) watch quang_duong_mm
(gdb) continue
Hardware watchpoint 2: quang_duong_mm
Old value = 698
New value = 700
moi_xung () at src/encoder.c:18
# Xem ngan xep goi ham khi dang o trong HardFault
(gdb) bt
(gdb) info registers
(gdb) p/x *(uint32_t*)0xE000ED28 # CFSR: vi sao fault
# Chay tung lenh may, khong theo dong nguon
(gdb) stepi
(gdb) disassembleĐảo chân GPIO và máy hiện sóng
#define DO_BAT() (GPIOB->BSRR = (1u << 0))
#define DO_TAT() (GPIOB->BSRR = (1u << 16))
void TIM2_IRQHandler(void)
{
DO_BAT(); /* 1 lenh ghi, khoang 12 ns */
TIM2->SR = ~TIM_SR_UIF;
xu_ly_mau();
DO_TAT();
}Tự làm thử
- Viết Makefile với đủ cờ ở trên và xác nhận cờ
-mcpucùng-mfpucó mặt ở cả bước dịch lẫn bước liên kết. - Dịch cùng một chương trình với
-O0,-O2và-Os, ghi lại ba con sốtextvà so sánh. - Bỏ
--gc-sectionsđi rồi dịch lại, đo xem kích thước tăng bao nhiêu phần trăm. - Khai báo
static uint8_t m[4096] = { 1 }rồi xem cộtdatatrongsizenhảy lên bao nhiêu. - Tìm trong tệp bản đồ xem tệp nguồn nào của bạn kéo
mallocvào chương trình. - Viết hàm xử lý HardFault đọc được
pc, cố ý gây lỗi bằng cách ghi vào địa chỉ0x00000000, rồi dùngaddr2linetìm đúng dòng gây lỗi.
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
- Cờ chọn lõi và FPU phải giống hệt nhau ở mọi tệp và ở cả bước liên kết, nếu không linker từ chối hoặc chọn nhầm thư viện.
- Tệp linker đặt
.textvà.rodatavào Flash,.bssvào RAM, còn.datanằm ở Flash nhưng chạy ở RAM nhờ cú pháp> RAM AT> FLASH. - Flash dùng bằng
text + data, RAM dùng bằngdata + bss. Cộtdeckhông có ý nghĩa vật lý. KEEPquanh bảng vector là bắt buộc, nếu không--gc-sectionsvứt nó và chip không bao giờ tới đượcmain.- Đảo chân GPIO tốn khoảng 24 ns và cho biết những thứ mà GDB lẫn
printfkhông cho biết: ISR có lồng nhau không, có bị bỏ qua lần nào không.