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

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

Biên dịch chéo
Dịch chương trình trên một kiến trúc để chạy trên kiến trúc khác. Máy của bạn là x86 hoặc ARM 64 bit, còn chip đích là Cortex-M 32 bit, nên trình biên dịch phải sinh mã cho đích chứ không cho máy chủ.
terminal
gcc --version
gcc (GCC) 14.2.0        <- dich cho MAY CUA BAN
arm-none-eabi-gcc --version
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
Makefile
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ĩaHậu quả nếu sai
-mcpu=cortex-m4Sinh mã cho lõi Cortex-M4Chip gặp lệnh không hiểu và nhảy vào HardFault
-mthumbDùng tập lệnh Thumb, bắt buộc trên Cortex-MLinker báo lỗi hoặc chip fault ngay lệnh đầu
-mfpu=fpv4-sp-d16FPU đơn chính xác 16 thanh ghi képKhông dùng được FPU, hoặc sinh lệnh chip không có
-mfloat-abi=hardTruyền tham số float qua thanh ghi FPUTrộn hard và soft thì linker từ chối liên kết
-T stm32f401.ldDù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.specsDùng newlib-nano thay newlib đầy đủTốn thêm hàng chục KB Flash
-specs=nosys.specsNối các lời gọi hệ thống vào hàm rỗngLinker báo thiếu _write, _sbrk, _close

#Tệp linker và bốn phân đoạn

stm32f401.ld
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ạnNằm ởChạy ởChứa gì
.textFlashFlashMã lệnh, chạy trực tiếp từ Flash không cần chép
.rodataFlashFlashChuỗi hằng, bảng const, giá trị khởi tạo của mảng const
.dataFlashRAMBiến toàn cục có giá trị đầu khác không
.bssKhông nằm đâu cảRAMBiến toàn cục bằng không hoặc không khởi tạo
Ví dụ: bốn biến vào bốn phân đoạn
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 */
terminal
arm-none-eabi-objdump -t vidu.o | sort -k4
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

terminal
arm-none-eabi-size build/firmware.elf
   text    data     bss     dec     hex filename
  11704     108    4372   16184    3f38 build/firmware.elf
CộtGiá trịNó là gì
text11704Mã lệnh cộng hằng số chỉ đọc, nằm trong Flash
data108Biến có giá trị đầu, chiếm CẢ Flash lẫn RAM
bss4372Biến khởi tạo bằng không, chỉ chiếm RAM
dec16184Tổng ba cột trên, ít khi có ích
hex3f38Chí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.
terminal
# Xem chi tiết từng phân đoạn với địa chỉ
arm-none-eabi-size -A build/firmware.elf
section          size        addr
.isr_vector       428   134217728
.text           10252   134218156
.rodata          1024   134228408
.data             108   536870912
.bss             4372   536871020
Total           16184
# Tìm thủ phạm chiếm Flash
arm-none-eabi-nm --size-sort -S -r build/firmware.elf | head -6
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
# Tìm thủ phạm chiếm RAM
arm-none-eabi-nm --size-sort -S -r build/firmware.elf | grep -E ' [bBdD] ' | head -5
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

  1. Từ elf sang bin hoặc hex

    Tệp .elf chứa cả thông tin gỡ lỗi và địa chỉ, còn .bin chỉ 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.hex
  2. Nối bo mạch qua SWD

    SWD cần đúng bốn dây: SWCLK, SWDIO, GND và 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.

  3. 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.

  4. Đặ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.

terminal
# Cách 1: st-flash, nhẹ nhất, chỉ cho ST-LINK
st-flash write build/firmware.bin 0x08000000
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!
# Cách 2: OpenOCD, chạy được với nhiều loại mạch nạp
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c 'program build/firmware.elf verify reset exit'
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 **
# Cách 3: bộ nạp có sẵn trong ROM, không cần mạch nạp
dfu-util -a 0 -s 0x08000000:leave -D build/firmware.bin
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áchNhiễu vào thời gianCần gìHợp cho
GDB qua SWDDừng hẳn chương trìnhMạch nạpLỗi logic, xem biến, đọc thanh ghi
In qua UARTHàng chục tới hàng trăm micro giâyMột dây và bộ chuyển USBTheo dõi luồng chạy, ghi nhật ký
Đảo chân GPIOHai chu kỳ, khoảng 24 nsMáy hiện sóngĐo thời gian ISR, tìm tranh chấp
Đèn và biến đếmGần bằng khôngKhông cần gìXác nhận có tới được chỗ này không

GDB qua SWD

terminal
# Cửa sổ 1: máy chủ gỡ lỗi
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
Info : Listening on port 3333 for gdb connections
# Cửa sổ 2: GDB
arm-none-eabi-gdb build/firmware.elf
(gdb) target extended-remote :3333
(gdb) monitor reset halt
(gdb) load
(gdb) break lenh_chay
(gdb) continue
Những lệnh GDB đáng nhớ trên vi điều khiển
# 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

do-thoi-gian.c
#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ử

  1. Viết Makefile với đủ cờ ở trên và xác nhận cờ -mcpu cùng -mfpu có mặt ở cả bước dịch lẫn bước liên kết.
  2. Dịch cùng một chương trình với -O0, -O2 và -Os, ghi lại ba con số text và so sánh.
  3. 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.
  4. Khai báo static uint8_t m[4096] = { 1 } rồi xem cột data trong size nhảy lên bao nhiêu.
  5. Tìm trong tệp bản đồ xem tệp nguồn nào của bạn kéo malloc vào chương trình.
  6. 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ùng addr2line tì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 .text và .rodata vào Flash, .bss vào RAM, còn .data nằ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ằng data + bss. Cột dec không có ý nghĩa vật lý.
  • KEEP quanh bảng vector là bắt buộc, nếu không --gc-sections vứt nó và chip không bao giờ tới được main.
  • Đảo chân GPIO tốn khoảng 24 ns và cho biết những thứ mà GDB lẫn printf không cho biết: ISR có lồng nhau không, có bị bỏ qua lần nào không.