崿F(xiàn)與調(diào)試指南)
1. 項(xiàng)目緣起為什么要在XMC上折騰STDIO重定向如果你用過(guò)ARM Cortex-M系列的MCU尤其是英飛凌的XMC系列大概率會(huì)接觸過(guò)Keil MDK這個(gè)IDE。在調(diào)試過(guò)程中我們最常用的輸出信息方式除了點(diǎn)燈可能就是串口打印了。接上串口助手在代碼里調(diào)用printf看著字符一行行刷出來(lái)是調(diào)試的常規(guī)操作。但這個(gè)方法有幾個(gè)痛點(diǎn)首先它獨(dú)占一個(gè)硬件UART外設(shè)和一對(duì)GPIO其次波特率設(shè)置不當(dāng)可能導(dǎo)致亂碼或丟數(shù)據(jù)再者在低功耗模式下UART模塊可能被關(guān)閉導(dǎo)致調(diào)試信息丟失。那么有沒(méi)有一種方法既能像printf一樣方便地輸出調(diào)試信息又不占用額外的硬件資源還能在芯片睡眠時(shí)正常工作呢答案是肯定的而且Keil MDK早就為我們準(zhǔn)備好了這個(gè)“神器”——Event Recorder。這個(gè)項(xiàng)目要做的就是把標(biāo)準(zhǔn)的C庫(kù)輸入輸出STDIO也就是printf、scanf、getchar這些函數(shù)重定向到Event Recorder上。這樣一來(lái)你的printf輸出不再走向串口而是走向MDK調(diào)試器的內(nèi)部通道最終顯示在Debug Viewer窗口里。整個(gè)過(guò)程不依賴(lài)任何硬件外設(shè)速度極快并且?guī)缀醪挥绊懗绦虻膶?shí)際運(yùn)行。聽(tīng)起來(lái)很美好對(duì)吧但實(shí)際操作起來(lái)你會(huì)發(fā)現(xiàn)官方文檔往往語(yǔ)焉不詳或者提供的例子不夠“接地氣”。特別是在資源、時(shí)序要求各不相同的XMC系列MCU上如何穩(wěn)定、高效地完成這次重定向里面有不少門(mén)道。我最近在一個(gè)基于XMC4700的項(xiàng)目上完整實(shí)現(xiàn)了這個(gè)功能期間踩過(guò)坑也總結(jié)出一些確保穩(wěn)定性的關(guān)鍵配置。這篇文章我就來(lái)手把手拆解這個(gè)過(guò)程不僅告訴你“怎么做”更重點(diǎn)解釋“為什么這么做”以及那些容易忽略的細(xì)節(jié)。2. 核心組件解析Event Recorder與ARM Compiler的MICROLIB在動(dòng)手寫(xiě)代碼之前我們必須先理解兩個(gè)核心概念Event Recorder的本質(zhì)以及我們將要使用的C庫(kù)——Microlib。2.1 Event Recorder不止于“打印”的調(diào)試數(shù)據(jù)通道很多人把Event Recorder簡(jiǎn)單地理解為一個(gè)高級(jí)的printf窗口這低估了它的能力。本質(zhì)上Event Recorder是一個(gè)時(shí)間戳事件記錄系統(tǒng)。它通過(guò)一個(gè)名為“ITM”的硬件模塊Instrumentation Trace Macrocell屬于Cortex-M內(nèi)核調(diào)試組件的一部分在芯片內(nèi)部開(kāi)辟了一條從代碼到調(diào)試器的專(zhuān)用數(shù)據(jù)流。與UART這種異步串行通信不同ITM通道是同步的并且擁有更高的優(yōu)先級(jí)和帶寬。當(dāng)你調(diào)用重定向后的printf時(shí)字符串?dāng)?shù)據(jù)會(huì)被打包成ITM數(shù)據(jù)包通過(guò)內(nèi)核的調(diào)試接口通常是SWD或JTAG直接發(fā)送給Keil MDK的調(diào)試器最終被解析并顯示出來(lái)。這個(gè)過(guò)程有以下幾個(gè)關(guān)鍵優(yōu)勢(shì)零硬件依賴(lài)不占用UART、GPIO、DMA等任何片內(nèi)外設(shè)資源。極低延遲數(shù)據(jù)通過(guò)內(nèi)核調(diào)試單元直接傳輸速度遠(yuǎn)高于通常的串口波特率如115200。運(yùn)行中調(diào)試即使在芯片進(jìn)入睡眠模式Sleep時(shí)只要調(diào)試連接保持ITM通道在特定配置下仍可工作取決于DBGMCU的配置這對(duì)于調(diào)試低功耗應(yīng)用至關(guān)重要。時(shí)間戳Event Recorder會(huì)為每條消息附上精確到微秒級(jí)的時(shí)間戳這對(duì)于分析代碼執(zhí)行時(shí)序、性能瓶頸無(wú)比有用。在Keil MDK中Event Recorder的實(shí)現(xiàn)依賴(lài)于一個(gè)名為EventRecorder.c的庫(kù)文件以及相應(yīng)的頭文件。我們需要在工程中引入并配置它。2.2 Microlib為嵌入式而生的精簡(jiǎn)C庫(kù)標(biāo)準(zhǔn)C庫(kù)比如GNU的glibc功能強(qiáng)大但體積龐大不適合資源緊張的MCU。Keil ARM Compiler提供了一個(gè)名為Microlib的替代品。它是一個(gè)高度優(yōu)化、專(zhuān)為嵌入式系統(tǒng)設(shè)計(jì)的最小化C庫(kù)子集。為什么重定向STDIO通常需要Microlib因?yàn)闃?biāo)準(zhǔn)庫(kù)的printf實(shí)現(xiàn)非常復(fù)雜支持浮點(diǎn)數(shù)、多種格式等其底層依賴(lài)的_sys_xxx系列半主機(jī)Semihosting函數(shù)在無(wú)操作系統(tǒng)的嵌入式環(huán)境中默認(rèn)是無(wú)法工作的。Microlib提供了另一套更簡(jiǎn)單的底層樁函數(shù)Stub例如_sys_write、_sys_read等。我們需要重寫(xiě)的正是這些樁函數(shù)。在Keil工程中啟用Microlib很簡(jiǎn)單在“Target”選項(xiàng)頁(yè)勾選“Use MicroLIB”。但請(qǐng)注意Microlib與標(biāo)準(zhǔn)庫(kù)不兼容如果你的工程之前使用了標(biāo)準(zhǔn)庫(kù)的某些復(fù)雜功能如完整的文件I/O、區(qū)域設(shè)置等切換時(shí)可能需要做適配。注意Microlib為了追求體積做了一些犧牲。例如它對(duì)printf浮點(diǎn)數(shù)%f的支持不是默認(rèn)開(kāi)啟的需要額外配置編譯器標(biāo)志。如果你的調(diào)試信息需要打印浮點(diǎn)變量這一點(diǎn)必須留意。3. 工程配置與代碼實(shí)現(xiàn)全流程理解了原理我們開(kāi)始實(shí)戰(zhàn)。以下步驟基于Keil MDK v5和XMC4700 Relax Kit但思路適用于所有XMC系列及Cortex-M3/M4/M7內(nèi)核的芯片。3.1 第一步基礎(chǔ)工程配置與Event Recorder添加創(chuàng)建或打開(kāi)一個(gè)Keil工程。確保芯片支持包Device Family Pack已正確安裝。啟用Microlib進(jìn)入“Options for Target” - “Target”選項(xiàng)卡勾選“Use MicroLIB”。添加Event Recorder組件點(diǎn)擊工具欄的“Manage Run-Time Environment”按鈕或按AltF7。在“Compiler”組件組下找到“Event Recorder”。將其右側(cè)的選項(xiàng)框從“Unused”改為“Enabled”通常會(huì)自動(dòng)添加依賴(lài)的“Event Recorder: Core”和“Compiler: Event Recorder”。點(diǎn)擊“OK”Keil會(huì)自動(dòng)將必要的源文件如EventRecorder.c、EventRecorderConf.h添加到你的工程中。3.2 第二步解剖與修改EventRecorderConf.h這是最關(guān)鍵的一步。EventRecorderConf.h文件決定了Event Recorder的內(nèi)存分配、時(shí)間戳源、緩沖大小等核心參數(shù)。默認(rèn)配置可能不適合所有場(chǎng)景尤其是對(duì)實(shí)時(shí)性要求高或內(nèi)存緊張的系統(tǒng)。// 以下是一些關(guān)鍵配置項(xiàng)的解讀與建議設(shè)置針對(duì)XMC4700 128KB RAM #define EVENT_RECORD_COUNT 1024 // 記錄的事件數(shù)量。增大此值可以存儲(chǔ)更多歷史消息但消耗更多RAM。 #define EVENT_RECORD_EVR_MAX_SECTIONS 4 // 最大分區(qū)數(shù)。通常保持默認(rèn)即可。 #define EVENT_RECORD_SECTION_0_SIZE 256 // 分區(qū)0大小用于初始化事件。夠用即可。 #define EVENT_RECORD_SECTION_1_SIZE 768 // 分區(qū)1大小用于你的應(yīng)用事件包括printf。這是主要緩沖區(qū)建議設(shè)置大一些。 // 時(shí)間戳?xí)r鐘源配置這是穩(wěn)定性的關(guān)鍵 // XMC4700的CPU主頻是144MHz我們可以使用內(nèi)核時(shí)鐘(Cycle Counter)作為時(shí)間戳源精度最高。 #define EVENT_TIMESTAMP_SOURCE 1 // 1 DWT Cycle Counter (推薦), 2 SysTick, 3 用戶(hù)自定義 #define EVENT_TIMESTAMP_FREQ 144000000 // DWT時(shí)鐘頻率即CPU主頻單位Hz // 初始化延遲。在某些啟動(dòng)較慢的外設(shè)或復(fù)雜系統(tǒng)中可能需要延遲Event Recorder的初始化。 #define EVENT_RECORD_INIT_DELAY_MS 0 // 單位毫秒。如果啟動(dòng)時(shí)打印亂碼可以嘗試設(shè)為100。為什么選擇DWT Cycle Counter而不是SysTickSysTick通常被操作系統(tǒng)如FreeRTOS用作系統(tǒng)節(jié)拍定時(shí)器其優(yōu)先級(jí)可能被改變甚至可能被重載。而DWTData Watchpoint and Trace單元中的循環(huán)計(jì)數(shù)器是專(zhuān)門(mén)用于調(diào)試和性能分析的它自由運(yùn)行不受應(yīng)用軟件干擾能提供最穩(wěn)定、連續(xù)的時(shí)間戳。前提是需要在初始化時(shí)使能DWT單元。3.3 第三步重定向printf的核心代碼實(shí)現(xiàn)現(xiàn)在我們來(lái)編寫(xiě)重定向代碼。我們需要實(shí)現(xiàn)Microlib所需的底層函數(shù)。通常在一個(gè)名為retarget.c或stdio_redirect.c的文件中實(shí)現(xiàn)。#include stdio.h #include rt_sys.h #include EventRecorder.h // 重寫(xiě) _sys_write 函數(shù)。當(dāng)調(diào)用 printf、puts 等輸出函數(shù)時(shí)Microlib會(huì)調(diào)用它。 int _sys_write(int handle, const unsigned char *buf, int len) { // handle 是文件描述符。stdout 和 stderr 通常對(duì)應(yīng) handle1 和 handle2。 if (handle 1 || handle 2) { // 標(biāo)準(zhǔn)輸出和錯(cuò)誤輸出 // 關(guān)鍵使用 EventRecorderDataTransfer 而非 EventRecord2 // EventRecord2 用于記錄結(jié)構(gòu)化事件而 DataTransfer 專(zhuān)為傳輸連續(xù)數(shù)據(jù)如字符串優(yōu)化。 if (EventRecorderDataTransfer(EventRecorderDataTransferType_Data, buf, len) ! 0) { return len; // 成功發(fā)送返回寫(xiě)入的字節(jié)數(shù) } } // 如果發(fā)送失敗或不是stdout/stderr返回錯(cuò)誤標(biāo)志 return -1; } // 重寫(xiě) _sys_read 函數(shù)。如果要用 scanf 或 getchar需要實(shí)現(xiàn)它。 int _sys_read(int handle, unsigned char *buf, int len, int mode) { // 對(duì)于從Event Recorder輸入通常我們不實(shí)現(xiàn)。如果需要交互調(diào)試可以留空或返回0。 (void)handle; (void)buf; (void)len; (void)mode; return 0; // 表示沒(méi)有數(shù)據(jù)可讀 } // 重寫(xiě) _sys_istty 函數(shù)。詢(xún)問(wèn)句柄是否連接到一個(gè)終端設(shè)備。 int _sys_istty(int handle) { // 對(duì)于 stdout/stderr我們告訴庫(kù)這是一個(gè)“終端”交互設(shè)備。 if (handle 1 || handle 2) { return 1; } return 0; } // 可選但推薦重寫(xiě) _ttywrch 函數(shù)。用于處理單個(gè)字符輸出如某些低層函數(shù)。 void _ttywrch(int ch) { unsigned char c (unsigned char)ch; _sys_write(1, c, 1); // 直接復(fù)用上面的 _sys_write }代碼解析與避坑點(diǎn)EventRecorderDataTransfervsEventRecord2這是最容易混淆的地方。EventRecord2用于記錄帶有事件ID、參數(shù)的事件適合記錄“用戶(hù)登錄”、“按鍵按下”這類(lèi)結(jié)構(gòu)化事件。而printf輸出的是連續(xù)的字節(jié)流使用EventRecorderDataTransfer并指定DataTransferType_Data類(lèi)型才是正確選擇它能保證字符串的完整性在Debug Viewer中會(huì)以“Data”事件形式顯示并自動(dòng)拼接成完整行。返回值必須正確_sys_write必須返回成功寫(xiě)入的字節(jié)數(shù)這里是len如果返回-1可能導(dǎo)致上層printf函數(shù)行為異常。初始化順序務(wù)必在main()函數(shù)一開(kāi)始初始化完系統(tǒng)時(shí)鐘后就調(diào)用EventRecorderInitialize()。確保在調(diào)用任何printf之前Event Recorder已經(jīng)準(zhǔn)備就緒。3.4 第四步主函數(shù)中的初始化與測(cè)試#include XMC4700.h // 芯片頭文件 #include stdio.h #include EventRecorder.h int main(void) { // 1. 初始化芯片時(shí)鐘、外設(shè)等 SYSTEM_Init(); // 2. 使能DWT單元如果使用DWT作為時(shí)間戳源 CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; // 使能跟蹤單元 DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; // 使能循環(huán)計(jì)數(shù)器 // 3. 初始化Event Recorder必須在任何Event Recorder API調(diào)用前執(zhí)行 EventRecorderInitialize(EventRecordAll, 1U); // 記錄所有級(jí)別的事件初始化后立即啟動(dòng) // 可選延遲一段時(shí)間確保調(diào)試器已連接并準(zhǔn)備好接收數(shù)據(jù) // for(int i0; i1000000; i) __NOP(); // 4. 開(kāi)始你的應(yīng)用代碼 printf(【系統(tǒng)啟動(dòng)】XMC4700 STDIO重定向測(cè)試 %lu Hz\n, SystemCoreClock); int counter 0; float voltage 3.3f; while(1) { counter; // 模擬一些工作 for(int i0; i100000; i); // 使用printf輸出數(shù)據(jù)將流向Event Recorder printf(循環(huán)計(jì)數(shù): %d, 模擬電壓: %.2f V\n, counter, voltage); voltage - 0.01f; if(voltage 0) voltage 3.3f; // 也可以使用EventRecorderDataTransfer直接發(fā)送數(shù)據(jù) char custom_msg[64]; int len snprintf(custom_msg, sizeof(custom_msg), 自定義消息: Counter%d\n, counter); if(len 0) { EventRecorderDataTransfer(EventRecorderDataTransferType_Data, custom_msg, len); } } return 0; }4. 調(diào)試實(shí)戰(zhàn)連接、查看與問(wèn)題排查代碼寫(xiě)好編譯下載后真正的挑戰(zhàn)才剛剛開(kāi)始。如何看到輸出沒(méi)輸出怎么辦4.1 正確連接與查看輸出硬件連接使用調(diào)試器如J-Link通過(guò)SWD接口連接X(jué)MC開(kāi)發(fā)板。Keil調(diào)試配置在“Debug”設(shè)置中確保你的調(diào)試器型號(hào)正確并且在“Trace”選項(xiàng)卡中勾選了“Enable”并且“Core Clock”設(shè)置為你芯片的實(shí)際主頻如144MHz。這是ITM數(shù)據(jù)流能被正確解析的關(guān)鍵。開(kāi)始調(diào)試點(diǎn)擊“Start/Stop Debug Session”CtrlF5進(jìn)入調(diào)試模式。打開(kāi)Debug Viewer在菜單欄選擇“View” - “Analysis Windows” - “Debug (printf) Viewer”。你也可以在“View” - “Serial Windows”下找到它。運(yùn)行程序點(diǎn)擊運(yùn)行F5。如果一切正常你將在Debug Viewer窗口中看到printf輸出的信息并且每條信息前面都有一個(gè)精確的時(shí)間戳。4.2 常見(jiàn)問(wèn)題與深度排查指南如果Debug Viewer一片空白不要慌按照以下鏈路逐步排查問(wèn)題現(xiàn)象Debug Viewer無(wú)任何輸出。排查點(diǎn)1Event Recorder初始化是否成功檢查在EventRecorderInitialize()語(yǔ)句后設(shè)一個(gè)斷點(diǎn)。單步執(zhí)行過(guò)去然后查看“View” - “Analysis Windows” - “Event Recorder”窗口。如果初始化成功你應(yīng)該能看到幾條Event Recorder自身產(chǎn)生的初始化事件如“Event Recorder started”。原因與解決如果連這些事件都沒(méi)有說(shuō)明Event Recorder庫(kù)本身未正確初始化。檢查工程中是否確實(shí)包含了EventRecorder.cEventRecorderConf.h文件是否在包含路徑中其配置參數(shù)尤其是緩沖區(qū)大小是否合理可以嘗試將緩沖區(qū)調(diào)大。堆棧大小是否足夠Event Recorder內(nèi)部會(huì)使用一些堆棧在啟動(dòng)文件如startup_XMC4700.s中適當(dāng)增大堆棧Stack_Size。排查點(diǎn)2ITM Trace功能是否被正確使能檢查這是最容易被忽略的一點(diǎn)進(jìn)入調(diào)試模式后打開(kāi)“View” - “Analysis Windows” - “Trace”窗口。查看ITM Stimulus Ports刺激端口。printf重定向通常使用Port 0。確保Port 0是“Enabled”狀態(tài)。原因與解決如果Port 0是禁用的數(shù)據(jù)無(wú)法發(fā)出。你需要在代碼中顯式使能它。在初始化Event Recorder之后添加以下代碼// 使能ITM端口0用于printf重定向 ITM-TER[0] 0x01; // 啟用端口0 // 確保ITM控制寄存器使能了ITM和同步數(shù)據(jù)包 ITM-TCR (1UL 0) | // ITM Enable (1UL 3) | // Sync Enable (重要) (0x01 16); // TraceBusID (可設(shè)為任意非零值)深層原理ITM有32個(gè)刺激端口Stimulus Ports軟件可以往不同的端口寫(xiě)數(shù)據(jù)以實(shí)現(xiàn)多路復(fù)用。Event Recorder庫(kù)默認(rèn)可能使用某個(gè)端口但有時(shí)配置不會(huì)自動(dòng)生效。手動(dòng)使能Port 0是最保險(xiǎn)的做法。TCR寄存器中的“Sync Enable”位至關(guān)重要它允許調(diào)試器在數(shù)據(jù)流中插入同步包從而正確地對(duì)齊和解析后續(xù)的數(shù)據(jù)。排查點(diǎn)3編譯器優(yōu)化是否導(dǎo)致了問(wèn)題檢查嘗試將優(yōu)化等級(jí)暫時(shí)調(diào)整為“-O0”無(wú)優(yōu)化。原因與解決高優(yōu)化等級(jí)可能會(huì)將一些看似“無(wú)效”的printf調(diào)用完全刪除或者內(nèi)聯(lián)、重組函數(shù)導(dǎo)致底層_sys_write的調(diào)用鏈路異常。在調(diào)試階段使用低優(yōu)化等級(jí)是穩(wěn)妥的選擇。排查點(diǎn)4_sys_write函數(shù)真的被調(diào)用了嗎檢查在_sys_write函數(shù)內(nèi)部設(shè)置一個(gè)斷點(diǎn)。運(yùn)行程序看斷點(diǎn)是否被觸發(fā)。原因與解決如果沒(méi)觸發(fā)說(shuō)明Microlib沒(méi)有調(diào)用你的重定向函數(shù)。確認(rèn)“Use MicroLIB”選項(xiàng)已勾選并且你的retarget.c文件被正確編譯鏈接到了工程中。排查點(diǎn)5數(shù)據(jù)是否已發(fā)出但顯示異?,F(xiàn)象Debug Viewer有數(shù)據(jù)但是亂碼、不換行或擠在一起。檢查與解決亂碼檢查Keil Debug配置中的“Trace” - “Core Clock”是否與芯片實(shí)際主頻嚴(yán)格一致。頻率設(shè)錯(cuò)會(huì)導(dǎo)致時(shí)間戳和數(shù)據(jù)分析錯(cuò)誤。不換行printf輸出的字符串末尾需要包含換行符\n。Debug Viewer依賴(lài)換行符來(lái)分隔消息行。數(shù)據(jù)截?cái)嗷騺G失增大EventRecorderConf.h中的EVENT_RECORD_SECTION_1_SIZE。如果輸出頻率非常高小的緩沖區(qū)可能被快速填滿(mǎn)并覆蓋舊數(shù)據(jù)。5. 進(jìn)階技巧與性能考量當(dāng)基礎(chǔ)功能跑通后我們可以考慮如何用得更好、更穩(wěn)。5.1 實(shí)現(xiàn)格式化輸入scanf/getchar的重定向雖然調(diào)試中輸出更重要但有時(shí)輸入也很有用例如模擬交互命令。重定向輸入意味著從Debug Viewer的“Command”窗口讀取數(shù)據(jù)。這需要實(shí)現(xiàn)_sys_read函數(shù)并利用Event Recorder的數(shù)據(jù)輸入功能。實(shí)現(xiàn)起來(lái)比輸出復(fù)雜因?yàn)樾枰幚懋惒綌?shù)據(jù)接收。一個(gè)常見(jiàn)的做法是開(kāi)辟一個(gè)環(huán)形緩沖區(qū)在_sys_read中從緩沖區(qū)取數(shù)據(jù)而通過(guò)一個(gè)后臺(tái)任務(wù)或中斷如SysTick來(lái)調(diào)用EventRecorderDataTransfer的接收函數(shù)將調(diào)試器發(fā)送的數(shù)據(jù)填充到緩沖區(qū)。由于篇幅限制這是一個(gè)更高級(jí)的話題但核心思路是雙向數(shù)據(jù)流和緩沖區(qū)管理。5.2 在多任務(wù)環(huán)境如FreeRTOS下的使用在RTOS中多個(gè)任務(wù)可能同時(shí)調(diào)用printf。雖然EventRecorderDataTransfer函數(shù)本身可能是線程安全的取決于實(shí)現(xiàn)但為了確保輸出消息的原子性不被其他任務(wù)打斷最好對(duì)printf調(diào)用進(jìn)行保護(hù)。// 使用互斥信號(hào)量保護(hù)printf SemaphoreHandle_t printfMutex; void safe_printf(const char *format, ...) { if(xSemaphoreTake(printfMutex, portMAX_DELAY) pdTRUE) { va_list args; va_start(args, format); vprintf(format, args); // vprintf是printf的可變參數(shù)版本 va_end(args); xSemaphoreGive(printfMutex); } } // 在任務(wù)中調(diào)用 safe_printf 替代 printf safe_printf(Task %s is running.\n, pcTaskGetName(NULL));5.3 性能影響評(píng)估與優(yōu)化使用ITM輸出對(duì)代碼執(zhí)行時(shí)間的影響微乎其微但并非為零。每次調(diào)用printf都會(huì)執(zhí)行格式化字符串和調(diào)用_sys_write的開(kāi)銷(xiāo)。在極端追求性能的循環(huán)或中斷服務(wù)程序中頻繁調(diào)用printf是不可取的。優(yōu)化建議條件編譯使用宏來(lái)控制調(diào)試輸出在發(fā)布版本中徹底關(guān)閉。#ifdef DEBUG_ENABLE #define DEBUG_PRINTF(...) printf(__VA_ARGS__) #else #define DEBUG_PRINTF(...) #endif減少格式化開(kāi)銷(xiāo)對(duì)于固定字符串直接使用puts或調(diào)用EventRecorderDataTransfer發(fā)送原始字符串避免printf的格式化解析過(guò)程。批量輸出將多條信息攢成一條大的字符串再輸出減少函數(shù)調(diào)用和ITM數(shù)據(jù)包開(kāi)銷(xiāo)。5.4 與UART打印的共存方案有時(shí)你可能既需要不占資源的Event Recorder調(diào)試又需要真實(shí)的串口輸出給其他設(shè)備。這很容易實(shí)現(xiàn)只需在_sys_write函數(shù)中同時(shí)將數(shù)據(jù)發(fā)送給Event Recorder和UART。int _sys_write(int handle, const unsigned char *buf, int len) { int result -1; // 1. 發(fā)送到Event Recorder if (EventRecorderDataTransfer(EventRecorderDataTransferType_Data, buf, len) ! 0) { result len; // 至少一個(gè)通道成功 } // 2. 同時(shí)發(fā)送到硬件UART (假設(shè)已初始化) if (UART_Transmit(uart_handle, buf, len) UART_STATUS_OK) { result len; } else { // 如果UART發(fā)送失敗但Event Recorder成功仍返回成功 // 可以根據(jù)需要調(diào)整錯(cuò)誤處理邏輯 } return result; }這種“雙發(fā)”機(jī)制在項(xiàng)目前期調(diào)試和后期現(xiàn)場(chǎng)日志收集時(shí)非常有用。經(jīng)過(guò)以上步驟你應(yīng)該已經(jīng)成功地將XMC項(xiàng)目的STDIO重定向到了Event Recorder。這個(gè)過(guò)程不僅僅是配置幾個(gè)選項(xiàng)更是對(duì)ARM Cortex-M調(diào)試體系、Microlib工作機(jī)制的一次深入理解。最大的收獲可能不是最終那一行行跳出的調(diào)試信息而是在排查“為什么沒(méi)輸出”的過(guò)程中對(duì)芯片底層調(diào)試模塊和工具鏈協(xié)作方式建立起的清晰認(rèn)知。下次當(dāng)你需要一種輕量、高效、不占硬件的日志輸出方式時(shí)Event Recorder絕對(duì)是一個(gè)值得信賴(lài)的選擇。