14.1.1.4. การดีบักเฟิร์มแวร์

การดีบักบนฮาร์ดแวร์หมายถึงการหยุดโปรเซสเซอร์ การตั้งจุดหยุด (breakpoint) ในโค้ด C การรันทีละขั้น และการตรวจสอบตัวแปร หน่วยความจำ รีจิสเตอร์ และอุปกรณ์ต่อพ่วง -- ทั้งหมดจากภายใน VS Code ซึ่งต้องการสิ่งสำคัญสามอย่าง: debug build, SWD debug probe (Segger J-Link), และส่วนขยาย Cortex-Debug ที่ขับเคลื่อน arm-none-eabi-gdb กับ J-Link GDB server

14.1.1.4.1. การบิลด์สำหรับการดีบัก

สร้างเป้าหมายใหม่ด้วย DEBUG=1 เสมอ:

make -j$(nproc) TARGET=<TARGET> DEBUG=1

อิมเมจแบบ release (DEBUG=0) คอมไพล์ด้วย -O2 ในดีบักเกอร์คุณจะเห็น <optimized out> สำหรับตัวแปรหลายตัว ฟังก์ชัน inline จะถูกรวมเข้ากับผู้เรียก และการรันทีละขั้นจะกระโดดไปมาอย่างไม่สามารถคาดเดาได้ การบิลด์ด้วย DEBUG=1 ใช้ -Og -ggdb3 ซึ่งดีบักได้ขณะยังบูตบนกล้อง ไฟล์ ELF ที่คุณชี้ให้ดีบักเกอร์คือ:

build/<TARGET>/bin/firmware.elf

(สำหรับ Alif AE3 ให้ดีบัก build/OPENMV_AE3/bin/firmware_M55_HP.elf -- คอร์ประสิทธิภาพสูง)

14.1.1.4.3. การตั้งค่า VS Code Cortex-Debug

สร้าง .vscode/launch.json ใน repository กรณีที่ง่ายที่สุด -- VS Code, J-Link และ build อยู่บน เครื่องเดียวกัน Linux / macOS -- ใช้ servertype: "jlink" ซึ่งทำให้ Cortex-Debug เริ่ม J-Link GDB server เอง:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "OpenMV J-Link",
      "type": "cortex-debug",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "executable": "${workspaceFolder}/build/OPENMV4/bin/firmware.elf",
      "servertype": "jlink",
      "device": "STM32H743VI",
      "interface": "swd",
      "runToEntryPoint": "main",
      "armToolchainPath": "${env:HOME}/openmv-sdk-1.6.0/gcc/bin",
      "gdbPath": "${env:HOME}/openmv-sdk-1.6.0/gcc/bin/arm-none-eabi-gdb"
    }
  ]
}

เปลี่ยน executable และ device ตามบอร์ดของคุณ (ดูตารางด้านบน) กด F5 เพื่อบิลด์-แฟลช-และรันจนถึง main แล้วหยุดที่นั่น

Tip

หากต้องการบิลด์ใหม่โดยอัตโนมัติทุกครั้งที่เริ่มดีบัก ให้เพิ่ม build task ใน .vscode/tasks.json และอ้างอิงจาก launch config ด้วย "preLaunchTask" ตัวอย่างเช่น task ที่รัน make -j$(nproc) TARGET=OPENMV4 DEBUG=1 ชื่อ "build-firmware" บวก "preLaunchTask": "build-firmware" ในการตั้งค่าด้านบน เพื่อให้ F5 บิลด์ใหม่ แฟลช และเริ่มดีบักเกอร์ในขั้นตอนเดียว

Warning

Cortex-Debug ต้องการ arm-none-eabi-gdb ซึ่งมาพร้อม SDK ที่ ~/openmv-sdk-<version>/gcc/bin แต่ ไม่ได้ อยู่ใน PATH โดยค่าเริ่มต้น ทำให้การดีบักล้มเหลวด้วยข้อความ "GDB executable 'arm-none-eabi-gdb' was not found" แก้ไขได้โดยตั้งค่า armToolchainPath / gdbPath ดังที่แสดงด้านบน หรือเพิ่ม ~/openmv-sdk-<version>/gcc/bin เข้า PATH (printenv PATH ควรแสดงขึ้นมา)

14.1.1.4.4. มุมมองรีจิสเตอร์อุปกรณ์ต่อพ่วง (SVD)

ชี้ Cortex-Debug ไปที่ไฟล์ CMSIS SVD เพื่อรับมุมมองรีจิสเตอร์อุปกรณ์ต่อพ่วงที่ถอดรหัสแล้ว (ตัวจับเวลา, DMA, อินเทอร์เฟซกล้อง ฯลฯ) ตามชื่อและ bitfield:

"svdFile": "/path/to/STM32H743.svd"

สำหรับ STM32 และ MIMXRT ให้รับ SVD จาก ST / NXP CMSIS pack หรือ Cortex-Debug SVD registry SVD ของ Alif อยู่ใน firmware repo ที่ lib/micropython/lib/alif_ensemble-cmsis-dfp/Debug/SVD/ (ใช้ ..._CM55_HP_View.svd สำหรับ AE3 HP core)

14.1.1.4.6. การดีบักด้วย command line โดยใช้ gdbrunner

การตั้งค่า GDB session กับ embedded target ด้วยมือเป็นขั้นตอนห้าขั้น: เริ่ม J-Link / ST-Link GDB server ในหน้าต่างหนึ่งด้วยค่า device, port และ interface flag ที่ถูกต้อง; รอให้มันพิมพ์ Waiting for GDB connection; รัน arm-none-eabi-gdb ในหน้าต่างที่สอง; พิมพ์ target remote localhost:<port>; ชี้ gdb ไปที่ ELF เมื่อ gdb session สิ้นสุด ต้องจำปิดหน้าต่าง server ด้วย gdbrunner เป็น CLI ขนาดเล็กที่รวมทุกอย่างเข้าเป็นคำสั่งเดียวในเบื้องหน้า มาพร้อม Python environment ของ OpenMV SDK จึงไม่ต้องติดตั้งอะไร; จุดเข้าใช้งานปกติคือ make debug target ของ firmware repository:

make -j$(nproc) TARGET=<TARGET> DEBUG=1 debug

คำสั่งนี้รัน gdbrunner พร้อมอาร์กิวเมนต์ debugger จากการตั้งค่าของบอร์ด -- ชื่อดีไวซ์ J-Link และ external flash loader ของ ST-Link ในกรณีที่จำเป็น -- โดยมี arm-none-eabi-gdb ของ SDK อยู่ใน PATH แล้ว backend เริ่มต้นคือ J-Link; make DEBUGGER=STLINK debug ใช้งานได้กับ ST-Link probe แทน

gdbrunner ยังสามารถเรียกใช้โดยตรงได้ (นอก SDK, pip install gdbrunner):

gdbrunner jlink --device STM32H743VI build/OPENMV4/bin/firmware.elf
gdbrunner stlink build/OPENMV4/bin/firmware.elf
gdbrunner qemu --machine mps2-an500 build/MPS2_AN500/bin/firmware.elf

อาร์กิวเมนต์ตำแหน่งแรกเลือก server backend (jlink, stlink, qemu); ส่วนที่เหลือส่งต่อไปยัง backend นั้น พร้อมค่าเริ่มต้นที่ใช้ได้กับ OpenMV cam gdbrunner --help แสดงรายการ flag แต่ละ backend แบบครบถ้วน; ตาราง argument ของแต่ละ backend ขับเคลื่อนด้วย JSON (src/gdbrunner/backends.json) ดังนั้นการเพิ่ม server ใหม่เป็นเพียงการแก้ไขการตั้งค่าไม่ใช่โค้ด

สิ่งที่ gdbrunner ทำสำหรับงาน command line:

  • กระบวนการเดียว วงชีวิตสะอาด Server เริ่มต้น gdb เชื่อมต่อเมื่อพอร์ตเปิด server ถูกปิดอย่างสะอาดเมื่อ gdb ออก ไม่มี JLinkGDBServer ค้างหลังจาก session จบ ไม่ต้องจัดการสองเทอร์มินัล

  • การค้นหา STM32CubeProgrammer อัตโนมัติ backend stlink ค้นหาในตำแหน่งติดตั้งปกติ (~/STM32CubeProgrammer/, /opt/st/, ต้นไม้ plugin ของ STM32CubeIDE) เพื่อหาเครื่องมือ STM32CubeProgrammer ดังนั้นไม่ต้องพิมพ์ path ยาว --cube-prog ทุกครั้ง SDK มีสำเนาของตัวเองที่ ~/openmv-sdk-<version>/stcubeprog/bin -- ชี้ --cube-prog ไปที่นั่นหากไม่มีการติดตั้งในระบบ

  • รองรับ gdbinit ต่อโปรเจกต์ ไฟล์ .gdbinit ในไดเรกทอรีปัจจุบันถูกโหลดด้วย -ix -- แทนที่ ~/.gdbinit ทั่วทั้งผู้ใช้ -- ดังนั้น gdb scripting ต่อโปรเจกต์ (pretty-printer, macro เฉพาะบอร์ด, ชุด breakpoint) ใช้งานได้โดยเพียงแค่มีในไดเรกทอรีทำงาน make debug รันจาก root ของ repository ดังนั้น .gdbinit ที่นั่นจะมีผล

  • Dry run --dryrun พิมพ์คำสั่ง server โดยไม่รัน มีประโยชน์สำหรับการปรับการเรียกใช้ให้เข้ากับ wrapper script คัดลอกไปยัง IDE launcher config หรือเพียงตรวจสอบอาร์กิวเมนต์ที่ gdbrunner สร้างขึ้น

  • แสดงผลลัพธ์ server --show-output คง stdout / stderr ของ server ให้มองเห็น โดยค่าเริ่มต้นจะซ่อนไว้ (เพื่อให้ UI ของ gdb สะอาด); เปิด flag นี้เมื่อ server เองมีปัญหา

  • QEMU backend qemu-system-arm ดีบัก firmware build โดยไม่ต้องเสียบบอร์ด เป้าหมาย MPS2_AN500 เลือก backend นี้ในการตั้งค่าบอร์ด ดังนั้น make TARGET=MPS2_AN500 DEBUG=1 debug บิลด์สำหรับเครื่อง mps2-an500 ของ QEMU และรันทีละขั้นโค้ดที่ไม่ขึ้นกับแพลตฟอร์ม -- ทุกอย่างที่ไม่ใช้ peripheral เฉพาะของกล้อง -- ในการจำลอง (qemu-system-arm ติดตั้งบน host ไม่ใช่ส่วนหนึ่งของ SDK)

สำหรับการรันทีละขั้นระดับ source code พร้อม breakpoint gutter และมุมมองรีจิสเตอร์อุปกรณ์ต่อพ่วง การตั้งค่า VS Code Cortex-Debug ด้านบนเป็นเครื่องมือที่ดีกว่า; gdbrunner เหมาะสำหรับทุกอย่างที่อยู่ใน command line

14.1.1.4.7. การใช้ดีบักเกอร์

เมื่อ session กำลังรัน (โปรเซสเซอร์หยุดที่ main):

  • Breakpoints -- คลิกที่ gutter ถัดจากบรรทัด C หรือใน Debug Console break <file>:<line> / break <function> คอร์ Cortex-M มี hardware breakpoint comparator จำนวนน้อย (โดยทั่วไป 6--8 ใน M7 / H7, 8 ใน M55) หากเกินจำนวนนั้นบนโค้ดใน flash จะล้มเหลวโดยไม่แจ้ง -- รักษาจำนวน breakpoint ที่ใช้งานอยู่ให้พอเหมาะ

  • การรันทีละขั้น -- F10 ข้ามบรรทัด (next), F11 เข้าไปข้างใน (step), Shift+F11 ออก (finish), F5 ดำเนินต่อ การรันทีละคำสั่งใช้ stepi / nexti ใน Debug Console

  • ตัวแปร / watch / call stack -- แผง Variables และ Call Stack แสดงตัวแปรท้องถิ่นและ backtrace; เพิ่มนิพจน์ใน Watch วางเมาส์บนตัวแปรใน source เพื่อดูค่า ทุกอย่างที่แสดง <optimized out> หมายความว่าคุณไม่ได้บิลด์ด้วย DEBUG=1

  • Watchpoints (data breakpoints) -- watch <expr> หยุดเมื่อตัวแปรถูกเขียน rwatch เมื่ออ่าน awatch ทั้งสองกรณี หน่วย Cortex-M DWT รองรับ ~4 hardware watchpoint -- มีประโยชน์มากในการหาว่า ใคร ทำให้ตัวแปรเสียหาย

  • รีจิสเตอร์และอุปกรณ์ต่อพ่วง -- มุมมอง Cortex Registers แสดง core register; เมื่อตั้ง svdFile มุมมอง Peripherals จะถอดรหัสรีจิสเตอร์และ bitfield ของอุปกรณ์ต่อพ่วงทุกตัว (DMA, ตัวจับเวลา, อินเทอร์เฟซกล้อง / CSI, XSPI ฯลฯ) -- วิธีที่เร็วที่สุดในการดูว่าทำไม driver ถึงทำงานผิดปกติ

  • Memory -- ใช้ตัวดูหน่วยความจำของ Cortex-Debug หรือ gdb x/ เพื่อตรวจสอบบัฟเฟอร์เฟรม, DMA buffer และโครงสร้างข้อมูลโดยตรง

  • printf โดยไม่หยุด (SWO/RTT) -- สำหรับปัญหาที่เกี่ยวกับเวลา Segger RTT หรือ SWO ให้ printf แบบ overhead แทบเป็นศูนย์ขณะเป้าหมายรัน บิลด์ด้วย DEBUG_PRINTF=1 และเพิ่ม rttConfig (RTT) หรือ swoConfig (SWO ต้องการ core clock) ของ Cortex-Debug นี่คือเครื่องมือที่เหมาะเมื่อ breakpoint จะเปลี่ยนแปลง timing ที่กำลังสังเกตอยู่

  • การตัดการเชื่อมต่อ -- Stop บน session launch หยุดเป้าหมาย; Disconnect บน session attach ปล่อยให้กล้องทำงานต่อ รีสตาร์ทกล้องด้วยการถอดไฟเพื่อกลับสู่การทำงานปกติ

14.1.1.4.8. ปัญหาที่พบบ่อยในการดีบัก

  • ตัวแปรที่ถูก optimize ออก ทุกอย่างแสดง <optimized out> -- คุณบิลด์ด้วย DEBUG=0 บิลด์ใหม่ด้วย DEBUG=1

  • "GDB executable not found" -- gcc/bin ของ SDK ไม่อยู่ใน PATH; ตั้ง armToolchainPath / gdbPath

  • "Cannot connect" / wrong memory map -- ชื่อ device ผิดหรือขาดหายไป; ใช้สตริงตรงตามตาราง

  • Breakpoints ไม่หยุดโดยไม่แจ้ง -- hardware breakpoint บนโค้ดที่อยู่ใน flash มีมากเกินไป; ลดจำนวนลง

  • Source path ไม่ตรงกัน (ELF ที่บิลด์ด้วย Docker) -- บิลด์ด้วย Docker target build-firmware-dev (path สัมบูรณ์เดียวกันทั้งใน container และนอก container) หรือตั้ง gdb set substitute-path