例程實戰(zhàn):從官方庫到完整枚舉)
簡介面向嵌入式開發(fā)者的GD32 USB鼠標(biāo)例程解決了在GD32上通過USB OTG與電容式觸摸傳感器構(gòu)建觸控鼠標(biāo)的關(guān)鍵問題。壓縮包共182個文件大小約979KB以80個H頭文件和78個C源文件為主體H文件承載寄存器定義與接口聲明C文件實現(xiàn)USB協(xié)議處理和觸摸事件轉(zhuǎn)換另附Keil工程配置、hex固件、腳本及說明文檔便于直接編譯、燒錄與二次開發(fā)。代碼覆蓋USB設(shè)備枚舉、描述符配置、端點管理、中斷處理、觸摸數(shù)據(jù)采集、坐標(biāo)映射以及連接斷開邏輯。目前已有1593人學(xué)習(xí)下載適合正在研究USB協(xié)議?;蛞?guī)劃觸控交互產(chǎn)品的工程師無論用于課程設(shè)計還是產(chǎn)品預(yù)研都能快速定位所需模塊。例程完整走通了從初始化USB控制器、配置觸摸傳感器到注冊鼠標(biāo)事件處理并發(fā)送USB中斷報告的流程同時對枚舉過程、錯誤處理和兼容性測試給出了可參考的代碼路徑讀者既可以按模塊逐段閱讀也能在現(xiàn)有工程上直接驗證擴(kuò)展雙擊、滾輪或手勢識別對深入理解GD32外設(shè)驅(qū)動與USB設(shè)備開發(fā)很有幫助。一句話定位如果你手里正好有一塊GD32開發(fā)板想快速跑通USB HID設(shè)備比如鼠標(biāo)的完整流程同時又不想被官方庫的層層封裝繞暈這篇實戰(zhàn)筆記應(yīng)該能幫你省下至少兩天的摸索時間。本文基于GD32F303系列其他型號思路通用從硬件準(zhǔn)備、官方例程結(jié)構(gòu)、代碼拆解、枚舉失敗排查到玩法擴(kuò)展逐層講透。1. 這套例程水有多深先看清官方打包了哪些東西很多人拿到GD32的USB鼠標(biāo)例程第一反應(yīng)是打開工程直接編譯下載結(jié)果鼠標(biāo)不動就懵了。其實這套例程真正的復(fù)雜度不在鼠標(biāo)怎么動而在USB協(xié)議棧的完整鏈路。GD32官方庫GD32F30x_Firmware_Library里的USB鼠標(biāo)例程默認(rèn)放在Examples/USB/USB_Mouse目錄下它不是一個孤立的main.c文件而是一整套USB設(shè)備協(xié)議棧的骨架包含USB設(shè)備核心層負(fù)責(zé)枚舉、標(biāo)準(zhǔn)請求處理、端點調(diào)度。HID類處理層負(fù)責(zé)HID描述符解析、報告收發(fā)、鼠標(biāo)按鍵與位移的編碼。硬件抽象層USB D / D- 引腳配置、中斷處理、端點寄存器讀寫。應(yīng)用層main函數(shù)里初始化時鐘、配置GPIO、啟動USB然后每隔一段時間模擬鼠標(biāo)移動或按鍵。拿到例程后第一件事不是編譯而是先看usbd_hid_core.c和usbd_hw.c這兩個文件的代碼量——前者是HID協(xié)議實現(xiàn)后者是硬件底層。如果這兩個文件的結(jié)構(gòu)你能說出個大概后面排查問題就有方向了。官方例程默認(rèn)的鼠標(biāo)行為是上電后USB枚舉成功然后每500ms左右向右移動一段像素同時周期性觸發(fā)左鍵點擊。這個行為被封裝在usbd_hid_mouse_process這類函數(shù)里。也就是說你只要編譯燒錄成功插上電腦不需要任何驅(qū)動鼠標(biāo)指針就會自己動起來。如果沒動問題一定出在枚舉鏈路或端點配置上。注意GD32和STM32的USB外設(shè)雖然都是USB 2.0 FS設(shè)備但寄存器映射、描述符緩沖區(qū)的訪問方式、端點號分配邏輯有差異不能直接拿STM32的HAL庫代碼套。這也是為什么很多人用網(wǎng)上淘來的STM32鼠標(biāo)例程改GD32折騰半天也枚舉不了。2. 硬件準(zhǔn)備與開發(fā)環(huán)境里那些容易埋雷的細(xì)節(jié)2.1 開發(fā)板選型與USB接口電路GD32支持USB的型號很多以最常見的GD32F303系列為例它內(nèi)置了USB 2.0 FS設(shè)備控制器不需要外部PHY芯片只要在D引腳上接一個1.5kΩ上拉電阻到3.3V部分核心板已集成D-和D走差分線就能直接連電腦USB口。選板子時務(wù)必確認(rèn)三點板子上的USB座是不是直接連到MCU的PA11D-和PA12D。有些板子這兩個引腳被復(fù)用到別的功能比如USBDM和USBDP需要跳線或改配置。供電是否穩(wěn)定。USB枚舉瞬間電流較大如果板子靠USB口供電且板上還有其他負(fù)載比如OLED屏、蜂鳴器可能導(dǎo)致電壓跌落導(dǎo)致枚舉失敗。晶體頻率。GD32F303的USB要求USB時鐘必須是48MHz而USB時鐘來源于CK_PLL。如果外部晶振是8MHzPLL倍頻必須配置成48MHz或者使用內(nèi)部IRC48M。這一條是很多人忽略的重災(zāi)區(qū)——用25M晶振的板子直接跑例程USB時鐘根本不對枚舉必然失敗。2.2 IDE與固件庫版本的選擇GD32官方支持Keil MDK、IAR、Eclipse搭配編譯工具鏈。我日常用Keil MDK因為GD32官方Demo工程默認(rèn)就是Keil格式雙擊.uvprojx就能打開省去手動移植的功夫。固件庫版本建議使用最新版GD32F30x_Firmware_Library_V2.x以上。老版本V1.x的USB協(xié)議棧和描述符結(jié)構(gòu)體定義差異較大網(wǎng)上搜到的很多解決方案都是基于老版本的直接照抄到新版本上會導(dǎo)致編譯錯誤或邏輯錯亂。如果你手里的例程是V2.x但報錯提示找不到usbd_conf.h多半是沒有把Firmware_Library的USB_Device目錄完整添加到工程Include路徑。2.3 燒錄工具與調(diào)試方式GD32支持通過串口ISP、SWDJ-Link/ST-Link/DAP-Link等方式燒錄。這里有個小提醒不能用ST-Link直接連GD32F303的SWD接口刷固件雖然芯片是Cortex-M4內(nèi)核且SWD協(xié)議通用但ST-Link的固件版本和驅(qū)動對GD32的支持時好時壞。我自己遇到過ST-Link能識別芯片、能擦除但寫入后校驗失敗的情況換用DAP-Link或J-Link就正常了。調(diào)試USB設(shè)備時除了常規(guī)的單步調(diào)試強烈建議準(zhǔn)備一個USB抓包工具——如果手里沒有邏輯分析儀可以用Wireshark配合USBPcapWindows平臺抓USB HID層的數(shù)據(jù)包。這在你后面排查枚舉失敗時會救命。3. 核心代碼逐段拆解描述符、端點和HID報告的配合關(guān)系3.1 設(shè)備描述符與配置描述符的結(jié)構(gòu)打開usbd_desc.c你會看到一堆uint8_t數(shù)組。USB主機你的PC在枚舉時就是靠這些數(shù)組識別設(shè)備是誰、要干什么。/* USB Device Descriptor */ uint8_t usbd_dev_desc[] { 0x12, // bLength 0x01, // bDescriptorType: Device 0x00, 0x02, // bcdUSB: 2.00 0x00, // bDeviceClass: Per interface ... 0x01, // iManufacturer 0x02, // iProduct 0x03, // iSerialNumber 0x01 // bNumConfigurations };這里有幾個坑點與業(yè)務(wù)直接相關(guān)bDeviceClass如果填0x00表示設(shè)備類由各個接口的bInterfaceClass決定。HID鼠標(biāo)通常每個接口的bInterfaceClass填0x03HID類。如果你在這里填了0x00以外其他值Windows可能把它當(dāng)復(fù)合設(shè)備或特定類設(shè)備處理導(dǎo)致鼠標(biāo)功能異常。idVendor和idProduct如果沿用官方默認(rèn)值比如0x28E9是GD32的VID系統(tǒng)會識別為GD32設(shè)備。如果你后續(xù)要量產(chǎn)需要申請自己的VID/PID否則在電腦的設(shè)備管理器里會有黃色感嘆號提示“設(shè)備無法啟動”的可能性實際原因是PID沖突或驅(qū)動綁定錯誤。字符串描述符iManufacturer、iProduct、iSerialNumber編碼方式是UTF-16LE即每個字符占2字節(jié)且需要以0x03類型開頭。很多新手直接改成純ASCII數(shù)組結(jié)果Windows上設(shè)備描述符字符串解析異常。3.2 配置描述符集合的組成配置描述符集合通常由以下部分串聯(lián)描述符類型作用典型長度配置描述符描述整個配置的總長度、接口數(shù)量、供電方式9字節(jié)接口描述符描述一個接口的類、端點數(shù)等9字節(jié)HID描述符描述HID版本、報告描述符長度9字節(jié)端點描述符描述中斷輸入/輸出端點屬性7字節(jié)在usbd_hid_core.c中官方例程給了USBD_HID_config這個配置描述符集合。重點要看端點描述符里的bInterval查詢間隔。對鼠標(biāo)這類低帶寬設(shè)備bInterval設(shè)為1~10單位ms都可以。如果設(shè)得太小比如1ms數(shù)據(jù)量不大但會增加CPU中斷頻率設(shè)得太大比如100ms鼠標(biāo)會明顯感到卡頓指針移動不跟手。3.3 HID報告描述符才是鼠標(biāo)的靈魂這是整套例程里最容易被忽略又最關(guān)鍵的部分。鼠標(biāo)的按鍵狀態(tài)和位移必須按照HID報告描述符定義的格式打包上傳Windows才認(rèn)得出光標(biāo)向右移動了X像素。__ALIGN_BEGIN static uint8_t usbd_hid_mouse_report_desc[] { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x02, // Usage (Mouse) 0xA1, 0x01, // Collection (Application) 0x09, 0x01, // Usage (Pointer) 0xA1, 0x00, // Collection (Physical) 0x05, 0x09, // Usage Page (Button) 0x19, 0x01, // Usage Minimum (1) 0x29, 0x03, // Usage Maximum (3) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x95, 0x03, // Report Count (3) 0x75, 0x01, // Report Size (1) 0x81, 0x02, // Input (Data, Variable, Absolute) ... 0xC0, // End Collection 0xC0 // End Collection };這段描述符定義了一份4字節(jié)的HID報告第1字節(jié)按鍵狀態(tài)bit0表示左鍵bit1表示右鍵bit2表示中鍵。第2字節(jié)X方向位移有符號數(shù)-127~127。第3字節(jié)Y方向位移有符號數(shù)。第4字節(jié)滾輪位移有符號數(shù)。實際發(fā)報告時你只需要構(gòu)造一個4字節(jié)數(shù)組并發(fā)送到中斷端點即可。官方例程里的usbd_hid_mouse_send_report函數(shù)就是干這件事的。如果你改了報告描述符比如增加了自定義按鍵、多媒體鍵那么發(fā)送緩沖區(qū)長度和報告描述符的Report Count必須嚴(yán)格匹配否則Windows會報HID設(shè)備描述符請求失敗。4. 從編譯到枚舉鼠標(biāo)不動時的完整排查鏈路我拿到這塊板子第一次跑例程燒錄完成后插上電腦設(shè)備管理器里什么反應(yīng)都沒有。排查了整整一晚上最終定位到問題。這里我分享一下完整的排查鏈路按優(yōu)先級從低級到高級排列。4.1 第一步時鐘配置錯誤導(dǎo)致USB時鐘不對打開system_gd32f30x.c確認(rèn)系統(tǒng)時鐘使用的是外部高速晶振HXTAL還是內(nèi)部IRC。如果板子上的晶振是8MHz那么必須確保PLL配置成PLL_MUL128MHz * 12 96MHz然后USBCK CK_PLL / 2 48MHz。如果板子上的晶振是25MHzPLL倍頻系數(shù)就要重新算。一個非常隱蔽的點GD32F303的USB時鐘源選擇和STM32不一樣STM32是USBCLK由PLL輸出直接分頻GD32是USBCK CK_PLL / 2。很多人按STM32的思維去改時鐘結(jié)果差了一倍枚舉直接失敗。檢查方法在調(diào)試器里給RCC_CFG寄存器打個斷點查看USBCK位的值或者用邏輯分析儀抓D引腳如果枚舉正常D上應(yīng)該有一系列脈沖。沒有脈沖就是USB時鐘沒起來。4.2 第二步上拉電阻沒接好D上拉電阻是USB主機識別全速設(shè)備12Mbps的關(guān)鍵信號。FS設(shè)備通過把D拉高來告訴主機我在這里我是全速設(shè)備。如果這個1.5kΩ電阻沒接或者虛焊主機完全感知不到設(shè)備插入。很多開發(fā)板為了節(jié)省一個電阻直接用MCU的GPIO控制上拉。例程里會初始化一個GPIO輸出高電平來接“模擬上拉”。此時要確認(rèn)GPIO是否復(fù)用為USB功能上拉動作是否在USB初始化之前完成如果使用外部上拉電阻檢查其一端接D另一端接3.3V不能接5V——接5V可能直接燒壞MCU的USB PHY。4.3 第三步枚舉失敗但時鐘和上拉正常這時候要抓包一旦確認(rèn)時鐘和上拉沒問題但Windows仍然不識別就要抓包看枚舉到哪一步卡住了。我用Wireshark加USBPcap抓過幾次常見失敗點有GET_DESCRIPTOR(Device)不響應(yīng)問題在設(shè)備描述符數(shù)組導(dǎo)入不正確或者USB中斷沒開啟。檢查usbd_core.c中的usbd_core_init是否被調(diào)用以及NVIC中USB中斷優(yōu)先級是否配置。GET_DESCRIPTOR(Config)返回長度錯誤配置描述符集合的總長度wTotalLength與實際數(shù)組長度不符。很多人在配置描述符里單獨加了一個自定義端點但忘記更新wTotalLength字段導(dǎo)致主機在讀取配置描述符集合時數(shù)據(jù)截斷。SET_CONFIGURATION后無響應(yīng)設(shè)置配置完成后端點要立即激活。檢查usbd_hid_core.c里usbd_hid_set_config的實現(xiàn)確認(rèn)端點的EPTYPE是否配置為中斷傳輸、方向是否正確IN方向是設(shè)備到主機。4.4 第四步報告發(fā)送失敗導(dǎo)致鼠標(biāo)不動枚舉成功設(shè)備管理器顯示HID-compliant mouse但鼠標(biāo)就是不動問題大概率出在報告發(fā)送鏈路。常見原因發(fā)送函數(shù)調(diào)用前沒有檢查USB狀態(tài)是否為USBD_CONFIGURED。枚舉結(jié)束后USB狀態(tài)會切到CONFIGURED但你在main循環(huán)里可能提前調(diào)用了發(fā)送函數(shù)此時端點還沒激活數(shù)據(jù)直接丟棄。端點發(fā)送緩沖區(qū)忙。USB中斷端點發(fā)送完畢后需要等待USBD_EPT_OK標(biāo)志。如果你在緊接著的第二次發(fā)送時上一次發(fā)送還沒完成數(shù)據(jù)會覆蓋導(dǎo)致偶發(fā)丟包。使用DMA傳輸時緩沖區(qū)地址沒有對齊。GD32的USB DMA要求緩沖區(qū)地址4字節(jié)對齊局部變量uint8_t buffer[4]在棧上的地址是隨機的可能導(dǎo)致DMA傳輸異常。解決方法是定義成全局?jǐn)?shù)組或者用__ALIGN_BEGIN修飾符。5. 從HID數(shù)據(jù)格式到上位機聯(lián)動再往深處玩的花樣5.1 模擬鼠標(biāo)軌跡很多人在熱詞里提到模擬鼠標(biāo)運動軌跡這在GD32上其實可以做得非常平滑。HID報告里的X/Y位移是相對位移即每次上報的是一個增量。Windows的指針移動是增量累加。所以如果想畫一條直線你可以在MCU里用Bresenham算法逐步生成位移坐標(biāo)每隔一小段時間發(fā)一次報告。但是如果想實現(xiàn)鼠標(biāo)移到指定坐標(biāo)點擊只靠相對位移是不行的——你沒法知道當(dāng)前指針的絕對坐標(biāo)。這種情況要么在PC端配合上位機軟件把絕對坐標(biāo)換算成相對位移再下發(fā)需要額外的串口或網(wǎng)絡(luò)通道要么使用HID絕對定位表Digitizer風(fēng)格讓上位機直接寫屏。后者在Windows上同樣不需要驅(qū)動但需要修改HID報告描述符中的Usage (X)和Usage (Y)并設(shè)置Logical Minimum / Maximum為屏幕分辨率范圍。5.2 USB轉(zhuǎn)串口的組合應(yīng)用GD32的USB鼠標(biāo)例程可以和USB轉(zhuǎn)串口CDC類組合做一個串口遙控鼠標(biāo)PC上位機通過虛擬串口發(fā)送坐標(biāo)和點擊指令給GD32GD32收到后解析并模擬鼠標(biāo)上報。這種方案在工業(yè)控制場景非常常見比如自動化測試臺、無人工位的電腦操作工具。實現(xiàn)組合時需要注意復(fù)合設(shè)備的配置描述符集合要包含兩個接口描述符一個HID接口鼠標(biāo)一個CDC接口虛擬串口。Windows會把它識別為“USB輸入設(shè)備 虛擬COM口”這時鼠標(biāo)功能仍然不需要驅(qū)動CDC則需要系統(tǒng)自帶驅(qū)動usbser.sys。兩個接口共享同一套USB設(shè)備地址和電源配置但端點號不能沖突。HID用IN端點1CDC的發(fā)送和接收分別用IN端點2、OUT端點2。枚舉時復(fù)合設(shè)備的bDeviceClass建議設(shè)為0xEFMiscellaneousbDeviceSubClass設(shè)為0x02bDeviceProtocol設(shè)為0x01這樣Windows能正確拆分接口功能。5.3 自制USB鍵鼠一體設(shè)備官方還有USB鍵盤例程Examples/USB/USB_Keyboard兩者可以合并成一個復(fù)合HID設(shè)備同時上報鼠標(biāo)和鍵盤事件。這個在電競外設(shè)、快捷按鍵板、輔助輸入設(shè)備等場景很實用。合并時要把HID報告描述符改成一個總集合包含鍵盤用到的按鍵數(shù)據(jù)段和鼠標(biāo)用到的位移/按鍵數(shù)據(jù)段。此時Report ID就變得重要了——如果沒有Report ID兩個功能的數(shù)據(jù)會混在一起加了Report ID后比如鍵盤Report ID 1鼠標(biāo)Report ID 2PC就能區(qū)分包的類型。這個改動雖然不復(fù)雜但涉及描述符結(jié)構(gòu)大改建議先跑通單獨的鼠標(biāo)例程再去改。6. 調(diào)試經(jīng)驗總結(jié)那些文檔里不會寫的事最后分享幾條實打?qū)嵉慕?jīng)驗USB例程里所有帶__ALIGN_BEGIN修飾的全局?jǐn)?shù)組不要輕易去掉對齊。GD32F303的DMA/描述符緩沖區(qū)要求4字節(jié)對齊全局變量編譯器默認(rèn)4字節(jié)對齊但局部變量不保證。如果你圖省事把描述符數(shù)組改成局部變量極大概率踩到對齊坑。USB中斷優(yōu)先級不是越高越好。在官方例程中USB中斷通常設(shè)置為NVIC_PRIGROUP_1或NVIC_PRIGROUP_2下的搶占優(yōu)先級1或2。如果低于系統(tǒng)滴答定時器優(yōu)先級那么在高頻中斷場景下可能丟包。如果高于串口中斷優(yōu)先級串口打印調(diào)試信息時會被USB打斷導(dǎo)致打印亂序。建議USB搶占優(yōu)先級設(shè)為2子優(yōu)先級設(shè)為0。熱插拔時的處理。Windows對USB設(shè)備熱插拔非常敏感如果你在調(diào)試時頻繁拔插建議使用帶ESD保護(hù)的USB線/座并且每次重新插拔后清空一下電腦的Device Manager緩存不然偶爾會遇到設(shè)備描述符請求失敗的假故障實際是系統(tǒng)緩存了舊的配置信息。用邏輯分析儀抓D/D-信號時采樣率建議至少100MS/s25MS/s也能看個大概但波形細(xì)節(jié)不夠。如果只是確認(rèn)有沒有枚舉動作25MS/s能湊合要分析時序細(xì)節(jié)上100MS/s。最后如果你把鼠標(biāo)例程改成了鍵盤、游戲手柄甚至自定義HID設(shè)備報告描述符每改一次建議先用USBPcap抓一次包核對主機端枚舉時讀到的報告描述符長度是否和你代碼里定義的一致。這個習(xí)慣能省掉大量看起來很對但不工作的排查時間。這套例程跑通之后你會對GD32的USB外設(shè)架構(gòu)有比較完整的認(rèn)知后面再接觸CDC、MSC或者做復(fù)合設(shè)備都會順暢得多。如果卡在某個具體報錯上歡迎帶著寄存器截圖或枚舉抓包來交流。本文還有配套的精品資源點擊獲取