試 STM32 實戰(zhàn):OpenOCD + Cortex-Debug 替代 Keil)
1. 為什么要在 VS Code 里調(diào)試 STM321.1 從 Keil 到 VS Code 的遷移動機我最早接觸 STM32 開發(fā)時用的是 Keil MDK。那個年代沒得選Keil 就是標(biāo)準(zhǔn)答案。但用久了之后有幾個問題越來越讓人難受編輯器體驗停留在十年前代碼補全基本靠猜多文件跳轉(zhuǎn)慢得讓人想砸鍵盤版本管理更是噩夢——.uvprojx文件每次合并都像拆炸彈。后來我開始嘗試在 VS Code 里寫代碼、在 Keil 里編譯調(diào)試這種“兩頭跑”的模式雖然能忍但終究不夠優(yōu)雅。真正讓我下決心徹底遷移的是項目規(guī)模變大之后。一個工程幾十個源文件Keil 的代碼導(dǎo)航幾乎癱瘓而 VS Code 的 IntelliSense、全局搜索、Git 集成、多光標(biāo)編輯每一樣都是實打?qū)嵉男侍嵘8P(guān)鍵的是VS Code 的調(diào)試體驗——變量監(jiān)視、調(diào)用棧、斷點管理、外設(shè)寄存器查看——一旦用順手就再也回不去了。這篇文章要聊的就是怎么在 VS Code 里搭建一套完整的 STM32 調(diào)試環(huán)境。不是那種“能編譯就行”的將就方案而是真正能替代 Keil 調(diào)試器的完整工作流。涉及的工具鏈包括ARM GNU Toolchain、OpenOCD、Cortex-Debug 插件以及 VS Code 本身的配置。適合已經(jīng)有一定 STM32 基礎(chǔ)、想提升開發(fā)效率的嵌入式軟件工程師也適合剛?cè)腴T、想從一開始就建立現(xiàn)代開發(fā)環(huán)境的新手。1.2 這套方案能解決什么問題先說清楚這套方案的能力邊界。配好之后你可以在 VS Code 里完成以下操作用arm-none-eabi-gcc編譯整個工程生成.elf和.bin文件用 OpenOCD 連接 ST-Link 或 J-Link燒錄程序到芯片在 VS Code 里直接下斷點、單步執(zhí)行、查看變量和寄存器通過launch.json配置多套調(diào)試方案一鍵切換配合 Cortex-Debug 插件實時查看外設(shè)寄存器的值用tasks.json把編譯、燒錄、調(diào)試串成一條流水線不能做的事也要說清楚這套方案不依賴 Keil 的編譯器所以如果你用的是 Keil 特有的語法擴展或者 RTE 組件需要做適配。另外STM32CubeMX 生成的工程默認(rèn)是給 Keil 或 IAR 用的需要手動調(diào)整 Makefile 或 CMakeLists。提示如果你現(xiàn)在還在用 Keil 的“Debug模式”查看結(jié)構(gòu)體變量遷移到 VS Code 后會發(fā)現(xiàn)變量監(jiān)視窗口的體驗完全不是一個級別——可以展開嵌套結(jié)構(gòu)體、可以固定監(jiān)視表達(dá)式、可以在調(diào)試控制臺里直接調(diào)用函數(shù)。1.3 整體工具鏈選型與理由在動手之前先把工具鏈的選型邏輯講清楚。嵌入式調(diào)試的本質(zhì)是IDE 通過調(diào)試探針與芯片的調(diào)試接口通信控制 CPU 的執(zhí)行流讀寫內(nèi)存和寄存器。VS Code 本身不具備這個能力它需要借助外部工具。我選擇的組合是組件選型作用為什么選它編譯器ARM GNU Toolchain把 C/C 源碼編譯成 ARM 機器碼免費、開源、社區(qū)支持好、與 CMake 集成順暢調(diào)試服務(wù)器OpenOCD與 ST-Link/J-Link 通信提供 GDB Server支持芯片多、配置靈活、開源調(diào)試客戶端GDB (arm-none-eabi-gdb)發(fā)送調(diào)試命令控制程序執(zhí)行GNU 標(biāo)準(zhǔn)工具與 OpenOCD 配合成熟VS Code 插件Cortex-Debug在 VS Code 里集成 GDB 調(diào)試界面專為 ARM Cortex-M 設(shè)計支持外設(shè)寄存器查看構(gòu)建系統(tǒng)Make 或 CMake管理編譯流程靈活、可腳本化、適合持續(xù)集成這套組合的核心優(yōu)勢是解耦編譯器、調(diào)試服務(wù)器、調(diào)試客戶端、IDE 界面各司其職任何一層都可以替換。比如你不想用 OpenOCD可以換成 pyOCD 或 J-Link GDB Server不想用 Make可以換成 CMake 或 Ninja。這種靈活性是 Keil 那種一體化 IDE 給不了的。2. 環(huán)境搭建的完整步驟2.1 安裝 ARM GNU Toolchain 與 OpenOCD第一步是裝編譯器。去 ARM 官網(wǎng)下載ARM GNU Toolchain的 Windows 或 Linux 版本注意選arm-none-eabi那個不要選成aarch64-none-elf。安裝時勾選“Add to PATH”這樣在終端里可以直接調(diào)用arm-none-eabi-gcc。裝完后驗證一下arm-none-eabi-gcc --version如果輸出了版本號說明安裝成功。我實測下來目前比較穩(wěn)定的版本是 12.3.rel1 和 13.2.rel1太新的版本偶爾會有鏈接腳本兼容性問題。接下來裝 OpenOCD。Windows 用戶可以去 xPack 的 GitHub Release 頁面下載預(yù)編譯包解壓后把bin目錄加到 PATH。Linux 用戶直接apt install openocd就行但要注意發(fā)行版自帶的版本可能比較老建議從源碼編譯或者用 xPack 的包。驗證 OpenOCDopenocd --version然后測試一下能不能識別到 ST-Linkopenocd -f interface/stlink.cfg -f target/stm32f1x.cfg如果輸出里出現(xiàn)Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints之類的信息說明連接正常。按CtrlC退出。注意OpenOCD 的target配置文件要根據(jù)你的芯片型號選。STM32F1 用stm32f1x.cfgF4 用stm32f4x.cfgH7 用stm32h7x.cfg。選錯了會連不上或者行為異常。2.2 VS Code 插件安裝與配置VS Code 本身去官網(wǎng)下載安裝這一步?jīng)]什么好說的。裝完后需要安裝幾個關(guān)鍵插件Cortex-Debug核心調(diào)試插件提供 GDB 集成和外設(shè)寄存器查看C/C提供 IntelliSense 和代碼導(dǎo)航Makefile Tools或CMake Tools根據(jù)你的構(gòu)建系統(tǒng)選ARM Assembly如果需要看匯編代碼這個插件提供語法高亮Cortex-Debug 的配置是重點。安裝后在 VS Code 的設(shè)置里搜索cortex-debug需要配置幾個關(guān)鍵項{ cortex-debug.armToolchainPath: C:/Program Files (x86)/Arm GNU Toolchain arm-none-eabi/12.3 rel1/bin, cortex-debug.openocdPath: C:/OpenOCD/bin/openocd.exe, cortex-debug.gdbPath: C:/Program Files (x86)/Arm GNU Toolchain arm-none-eabi/12.3 rel1/bin/arm-none-eabi-gdb.exe }路徑要根據(jù)你的實際安裝位置改。Linux 下通常是/usr/bin/arm-none-eabi-gdb和/usr/bin/openocd。2.3 工程目錄結(jié)構(gòu)與 Makefile 編寫VS Code 不像 Keil 那樣有固定的工程結(jié)構(gòu)你需要自己組織目錄。我習(xí)慣用這樣的結(jié)構(gòu)project/ ├── .vscode/ │ ├── launch.json │ ├── tasks.json │ └── settings.json ├── Core/ │ ├── Inc/ │ └── Src/ ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ ├── Startup/ │ └── startup_stm32f103xb.s ├── Linker/ │ └── STM32F103C8Tx_FLASH.ld ├── Makefile └── build/Makefile 是編譯的核心。我寫一個最簡版本你可以根據(jù)工程實際情況擴展TARGET firmware BUILD_DIR build CC arm-none-eabi-gcc AS arm-none-eabi-gcc -x assembler-with-cpp CP arm-none-eabi-objcopy SZ arm-none-eabi-size CPU -mcpucortex-m3 FPU MCU $(CPU) -mthumb $(FPU) C_SOURCES $(wildcard Core/Src/*.c) $(wildcard Drivers/STM32F1xx_HAL_Driver/Src/*.c) ASM_SOURCES Startup/startup_stm32f103xb.s C_INCLUDES -ICore/Inc -IDrivers/STM32F1xx_HAL_Driver/Inc -IDrivers/CMSIS/Include -IDrivers/CMSIS/Device/ST/STM32F1xx/Include CFLAGS $(MCU) $(C_INCLUDES) -Og -g3 -gdwarf-2 -Wall -fdata-sections -ffunction-sections LDFLAGS $(MCU) -TLinker/STM32F103C8Tx_FLASH.ld -Wl,--gc-sections -Wl,-Map$(BUILD_DIR)/$(TARGET).map OBJECTS $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c.o))) vpath %.c $(sort $(dir $(C_SOURCES))) OBJECTS $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.s.o))) vpath %.s $(sort $(dir $(ASM_SOURCES))) all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin $(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR) $(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms$(BUILD_DIR)/$(notdir $(:.c.lst)) $ -o $ $(BUILD_DIR)/%.o: %.s Makefile | $(BUILD_DIR) $(AS) -c $(CFLAGS) $ -o $ $(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile $(CC) $(OBJECTS) $(LDFLAGS) -o $ $(SZ) $ $(BUILD_DIR)/%.hex: $(BUILD_DIR)/%.elf | $(BUILD_DIR) $(CP) -O ihex $ $ $(BUILD_DIR)/%.bin: $(BUILD_DIR)/%.elf | $(BUILD_DIR) $(CP) -O binary -S $ $ $(BUILD_DIR): mkdir $ clean: -rm -fR $(BUILD_DIR)這個 Makefile 的關(guān)鍵點在于-Og -g3 -gdwarf-2這三個編譯選項。-Og是優(yōu)化等級比-O0稍好又不會像-O2那樣把變量優(yōu)化掉導(dǎo)致調(diào)試?yán)щy。-g3生成最詳細(xì)的調(diào)試信息包括宏定義。-gdwarf-2指定調(diào)試信息格式Cortex-Debug 對 DWARF 2 的支持最穩(wěn)定。實操心得如果你發(fā)現(xiàn)調(diào)試時某些變量顯示為optimized out八成是優(yōu)化等級太高。把-Og改成-O0試試。但-O0生成的代碼體積會大不少Flash 小的芯片要權(quán)衡。2.4 launch.json 與 tasks.json 配置詳解launch.json是調(diào)試配置的核心。下面是一個針對 STM32F103 ST-Link 的完整配置{ version: 0.2.0, configurations: [ { name: Debug (OpenOCD), type: cortex-debug, request: launch, servertype: openocd, cwd: ${workspaceFolder}, executable: ${workspaceFolder}/build/firmware.elf, device: STM32F103C8, configFiles: [ interface/stlink.cfg, target/stm32f1x.cfg ], svdFile: ${workspaceFolder}/STM32F103.svd, runToEntryPoint: main, preLaunchTask: build, showDevDebugOutput: none, armToolchainPath: C:/Program Files (x86)/Arm GNU Toolchain arm-none-eabi/12.3 rel1/bin, openocdPath: C:/OpenOCD/bin/openocd.exe, gdbPath: C:/Program Files (x86)/Arm GNU Toolchain arm-none-eabi/12.3 rel1/bin/arm-none-eabi-gdb.exe } ] }幾個關(guān)鍵字段解釋一下executable指向編譯生成的.elf文件GDB 從這里讀取符號信息device芯片型號Cortex-Debug 用它來匹配 SVD 文件svdFileSVD 文件路徑這是查看外設(shè)寄存器的關(guān)鍵。SVD 文件可以從 ST 官網(wǎng)下載或者從 Keil 的芯片包里提取runToEntryPoint啟動后自動運行到main函數(shù)省得手動下斷點preLaunchTask調(diào)試前自動執(zhí)行的任務(wù)這里指向buildtasks.json定義構(gòu)建任務(wù){(diào) version: 2.0.0, tasks: [ { label: build, type: shell, command: make, args: [-j4], group: { kind: build, isDefault: true }, problemMatcher: [$gcc], detail: 編譯整個工程 }, { label: flash, type: shell, command: openocd, args: [ -f, interface/stlink.cfg, -f, target/stm32f1x.cfg, -c, program build/firmware.elf verify reset exit ], dependsOn: [build], detail: 燒錄程序到芯片 } ] }-j4表示用 4 個線程并行編譯根據(jù)你的 CPU 核心數(shù)調(diào)整。problemMatcher設(shè)為$gcc后編譯錯誤會直接顯示在 VS Code 的問題面板里點擊就能跳轉(zhuǎn)到對應(yīng)代碼行。3. 調(diào)試實操與核心技巧3.1 斷點、單步與變量監(jiān)視的實戰(zhàn)操作配置好之后按F5啟動調(diào)試。Cortex-Debug 會自動啟動 OpenOCD、連接 GDB、燒錄程序、運行到main函數(shù)。整個過程大概兩三秒比 Keil 的調(diào)試啟動速度快不少。斷點的使用和 Keil 類似在行號左側(cè)點擊即可。但 VS Code 的斷點管理更強可以在斷點面板里給斷點加條件、加命中次數(shù)、加日志消息。比如你想在某個循環(huán)執(zhí)行到第 100 次時停下來右鍵斷點選“編輯斷點”輸入條件i 100就行。變量監(jiān)視窗口是重點。Keil 的變量窗口只能看基本類型結(jié)構(gòu)體要展開好幾層。VS Code 的監(jiān)視窗口支持展開任意深度的嵌套結(jié)構(gòu)體固定監(jiān)視表達(dá)式比如huart1在調(diào)試控制臺里直接調(diào)用函數(shù)比如HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)查看數(shù)組的任意區(qū)間比如buffer[10..20]我實測下來查看 STM32 HAL 庫里的UART_HandleTypeDef結(jié)構(gòu)體時VS Code 能直接展開到Instance-SR寄存器級別比 Keil 直觀得多。提示如果變量顯示為optimized out除了降低優(yōu)化等級還可以在launch.json里加gdbTarget: localhost:3333并配合-O0編譯。另外把變量聲明為volatile也能防止被優(yōu)化掉。3.2 外設(shè)寄存器查看與 SVD 文件配置SVDSystem View Description文件是 Cortex-Debug 的殺手锏。它用 XML 描述了芯片所有外設(shè)寄存器的地址、位域、讀寫權(quán)限。配置好之后調(diào)試時可以在“XPERIPHERALS”面板里實時查看和修改寄存器值。SVD 文件從哪來三個途徑ST 官網(wǎng)的芯片頁面在“Design Resources”里找 SVD 文件Keil 的芯片包安裝目錄通常在Keil_v5/ARM/PACK/Keil/STM32F1xx_DFP/x.x.x/CMSIS/SVD/GitHub 上的cmsis-svd倉庫有社區(qū)維護(hù)的版本拿到 SVD 文件后在launch.json里用svdFile字段指向它。調(diào)試啟動后VS Code 底部會出現(xiàn)“XPERIPHERALS”面板展開就能看到 GPIOA、USART1、TIM2 等外設(shè)的寄存器。這個功能在調(diào)試 GPIO 時特別有用。比如你配置了 PA0 為輸出但 LED 不亮可以直接在寄存器面板里看GPIOA-ODR的 bit0 是不是 1。如果不是說明代碼沒執(zhí)行到如果是 1 但燈不亮說明硬件有問題。這種排查效率比打印日志高得多。3.3 串口調(diào)試與日志輸出配合調(diào)試嵌入式程序串口打印是少不了的。VS Code 里可以用Serial Monitor插件或者直接用screen/minicom。我習(xí)慣在tasks.json里加一個任務(wù)調(diào)試時自動打開串口{ label: serial, type: shell, command: python, args: [-m, serial.tools.miniterm, COM3, 115200], problemMatcher: [] }更高級的玩法是把串口輸出和 GDB 調(diào)試結(jié)合起來。Cortex-Debug 支持rttConfig可以用 SEGGER RTT 代替串口速度更快還不占用 UART 外設(shè)。配置如下rttConfig: { enabled: true, address: auto, decoders: [ { port: 0, type: console, label: RTT Channel 0 } ] }RTT 需要在代碼里集成 SEGGER 的 RTT 庫然后在需要打印的地方調(diào)用SEGGER_RTT_printf(0, value %d\n, value)。輸出會直接顯示在 VS Code 的調(diào)試控制臺里和 GDB 的輸出混在一起非常方便。注意RTT 的address設(shè)為auto時Cortex-Debug 會自動搜索 RTT 控制塊。如果搜不到可以手動指定地址通常在 RAM 的末尾區(qū)域。3.4 多工程與多目標(biāo)調(diào)試配置實際項目中經(jīng)常需要在多個工程之間切換或者同一個工程有多個構(gòu)建目標(biāo)比如 Bootloader 和 Application。VS Code 的launch.json支持配置多個調(diào)試方案在調(diào)試面板的下拉菜單里切換。比如一個 STM32 項目有 Boot 和 App 兩個目標(biāo){ configurations: [ { name: Debug Bootloader, type: cortex-debug, request: launch, executable: ${workspaceFolder}/boot/build/boot.elf, device: STM32F103C8, configFiles: [interface/stlink.cfg, target/stm32f1x.cfg], svdFile: ${workspaceFolder}/STM32F103.svd }, { name: Debug Application, type: cortex-debug, request: launch, executable: ${workspaceFolder}/app/build/app.elf, device: STM32F103C8, configFiles: [interface/stlink.cfg, target/stm32f1x.cfg], svdFile: ${workspaceFolder}/STM32F103.svd } ] }調(diào)試 App 時需要先燒錄 Boot再燒錄 App然后附加到 App 的地址。可以在launch.json里加preLaunchTask: flash-boot來自動化這個過程。4. 常見問題與排查技巧實錄4.1 OpenOCD 連接失敗的五種典型情況調(diào)試環(huán)境搭不起來十有八九是 OpenOCD 連不上。我把踩過的坑整理成一張速查表現(xiàn)象可能原因解決方法Error: open failedST-Link 驅(qū)動沒裝好裝 ST-Link 官方驅(qū)動或換 USB 口Error: init mode failed芯片被讀保護(hù)或休眠用 ST-Link Utility 解除讀保護(hù)或按住復(fù)位鍵再連接Error: target not examined yet配置文件選錯確認(rèn)target/stm32f1x.cfg和芯片型號匹配Error: timed out while waiting for target halted芯片跑飛或時鐘配置錯誤在 OpenOCD 配置里加reset_config srst_onlyError: JTAG scan chain interrogation failedSWD 引腳被復(fù)用檢查代碼里有沒有把 PA13/PA14 配成普通 GPIO最坑的是最后一種。我有一次調(diào)一個項目OpenOCD 死活連不上換了三根線、兩個 ST-Link 都不行。最后發(fā)現(xiàn)是代碼里把 SWD 引腳復(fù)用成了普通 IO芯片一上電就執(zhí)行了那段代碼導(dǎo)致調(diào)試接口失效。解決辦法是在main函數(shù)開頭加延時或者用__HAL_RCC_GPIOA_CLK_ENABLE()之前先確保 SWD 引腳沒被改。實操心得如果芯片已經(jīng)被“鎖死”可以嘗試按住復(fù)位鍵點擊調(diào)試啟動等 OpenOCD 開始連接時松開復(fù)位鍵。這樣芯片會在復(fù)位后短暫停留在初始狀態(tài)給調(diào)試器一個連接窗口。4.2 變量顯示異常與優(yōu)化等級的關(guān)系調(diào)試時變量顯示為optimized out或者值不對是新手最常遇到的問題。根本原因是編譯器優(yōu)化把變量存到了寄存器里或者直接消除了。解決辦法分三層第一層把編譯選項改成-O0 -g3。這是最直接的辦法但代碼體積會增大 30% 到 50%Flash 小的芯片可能放不下。第二層給關(guān)鍵變量加volatile關(guān)鍵字。volatile告訴編譯器“這個變量可能被外部修改不要優(yōu)化它”。比如volatile uint32_t counter 0;第三層用__attribute__((used))強制保留變量__attribute__((used)) static uint32_t debug_buffer[16];我一般先用-Og編譯如果發(fā)現(xiàn)某個變量看不到再單獨給它加volatile。這樣兼顧了代碼體積和調(diào)試體驗。4.3 斷點不生效與 Flash 斷點數(shù)量限制STM32 的硬件斷點數(shù)量有限Cortex-M3/M4 通常只有 6 個M0 只有 4 個。如果你下了超過這個數(shù)量的斷點多出來的斷點會變成“軟件斷點”需要修改 Flash 內(nèi)容可能導(dǎo)致程序行為異常。Cortex-Debug 會在調(diào)試控制臺里提示斷點類型。如果看到Warning: Cannot insert breakpoint之類的信息說明硬件斷點用完了。解決辦法減少同時生效的斷點數(shù)量用條件斷點代替多個普通斷點把斷點下在 RAM 函數(shù)里RAM 斷點不占用硬件斷點資源用watchpoint代替斷點監(jiān)視變量變化而不是代碼位置另外Flash 斷點在每次修改代碼后需要重新燒錄才能生效。如果你改了代碼但沒重新編譯燒錄斷點位置可能對不上。養(yǎng)成習(xí)慣改完代碼先CtrlShiftB編譯再F5調(diào)試。4.4 調(diào)試速度慢與 GDB 配置調(diào)優(yōu)有些同學(xué)反映 VS Code 調(diào)試比 Keil 慢單步執(zhí)行要等一兩秒。這通常是 GDB 的配置問題??梢栽趌aunch.json里加幾個優(yōu)化項gdbTarget: localhost:3333, showDevDebugOutput: none, breakAfterReset: false, runToEntryPoint: main, preLaunchTask: buildshowDevDebugOutput設(shè)為none可以關(guān)閉 GDB 的詳細(xì)日志輸出減少界面刷新開銷。breakAfterReset設(shè)為false避免復(fù)位后自動暫停。還有一個隱藏的性能殺手是 SVD 文件太大。有些廠商的 SVD 文件包含了幾百個外設(shè)定義Cortex-Debug 每次刷新都要解析一遍。如果覺得卡可以手動裁剪 SVD 文件只保留你實際用到的外設(shè)。我實測下來優(yōu)化后的調(diào)試啟動時間在 2 秒左右單步執(zhí)行延遲在 200 毫秒以內(nèi)和 Keil 的體驗基本持平。如果還是慢檢查一下 USB 線是不是接在了 USB Hub 上直連主板 USB 口會快很多。4.5 從 Keil 遷移時的兼容性問題最后聊聊從 Keil 遷移過來時容易踩的坑。第一個坑是__weak函數(shù)。Keil 和 GCC 對__weak的支持方式不同。GCC 用__attribute__((weak))Keil 用__weak。STM32 HAL 庫里已經(jīng)做了兼容處理但你自己寫的代碼要注意。第二個坑是內(nèi)聯(lián)匯編語法。Keil 用__asm關(guān)鍵字GCC 用asm volatile。如果工程里有大量內(nèi)聯(lián)匯編遷移工作量不小。第三個坑是鏈接腳本。Keil 用.sct分散加載文件GCC 用.ld鏈接腳本。STM32CubeMX 生成的工程里兩個都有但內(nèi)容需要對應(yīng)。如果你改了內(nèi)存布局兩邊都要改。第四個坑是中斷向量表。Keil 的啟動文件是.s格式GCC 也是.s但語法有差異。建議直接用 STM32CubeMX 生成的 GCC 版本啟動文件不要手動改 Keil 的。遷移完成后你會發(fā)現(xiàn) VS Code 的調(diào)試體驗確實比 Keil 好一個檔次。特別是變量監(jiān)視和外設(shè)寄存器查看一旦用習(xí)慣就回不去了。我現(xiàn)在手頭的 STM32 項目全部遷移到了 VS CodeKeil 只留著打開老工程用。