實(shí)戰(zhàn):從枚舉到穩(wěn)定收發(fā))
簡(jiǎn)介這份資源面向嵌入式開(kāi)發(fā)初學(xué)者與需要快速驗(yàn)證USB通信的工程師提供基于GD32芯片的USB虛擬串口收發(fā)程序幫助理解CDC-ACM設(shè)備類在ARM Cortex-M平臺(tái)上的實(shí)現(xiàn)方式。壓縮包共271個(gè)文件約5.96MB以c與h源碼為主配合o、d、crf等編譯中間文件以及hex、bin固件、sct分散加載腳本和uvprojx、eww等工程文件覆蓋Keil與IAR兩套開(kāi)發(fā)環(huán)境便于直接編譯下載。內(nèi)容圍繞USB協(xié)議、CDC類、GD32內(nèi)置USB OTG控制器、中斷服務(wù)程序與數(shù)據(jù)緩沖區(qū)管理展開(kāi)官網(wǎng)例程給出配置USB控制器、注冊(cè)中斷、建立通信管道的完整細(xì)節(jié)可對(duì)照參考手冊(cè)調(diào)整收發(fā)邏輯與錯(cuò)誤處理。目前已有5297人學(xué)習(xí)下載適合作為USB虛擬串口調(diào)試與固件移植的實(shí)踐起點(diǎn)。1. GD32 芯片 USB 虛擬串口收發(fā)程序從零跑通 CDC 回環(huán)的實(shí)戰(zhàn)拆解手頭有一塊 GD32 開(kāi)發(fā)板USB 口插上電腦卻只認(rèn)成未知設(shè)備串口助手翻遍端口列表也找不到那個(gè) COM 號(hào)——這是很多嵌入式同行第一次碰 USB 虛擬串口CDC時(shí)的經(jīng)典翻車(chē)現(xiàn)場(chǎng)。這份 GD32 芯片 USB 虛擬串口收發(fā)程序資源解決的正是這件事它把 GD32 系列 MCU 的 USB 外設(shè)配置成 CDC 類設(shè)備讓 PC 端無(wú)需額外驅(qū)動(dòng)就能識(shí)別出一個(gè)標(biāo)準(zhǔn)串口實(shí)現(xiàn)雙向收發(fā)。適合正在做數(shù)據(jù)采集、調(diào)試通道、設(shè)備升級(jí)通道的嵌入式開(kāi)發(fā)者尤其是從 STM32 轉(zhuǎn) GD32 平臺(tái)、發(fā)現(xiàn) USB 庫(kù)調(diào)用習(xí)慣不太一樣的那批人。下面按「資源是什么、怎么落地、坑在哪」的順序拆開(kāi)講。2. USB CDC 協(xié)議棧與 GD32 外設(shè)選型為什么不是隨便配個(gè) GPIO 就行2.1 CDC 類設(shè)備的枚舉流程與端點(diǎn)分配USB 虛擬串口本質(zhì)上是 USB CDC-ACMCommunication Device Class - Abstract Control Model類設(shè)備。主機(jī)上電后先做設(shè)備枚舉主機(jī)發(fā) GET_DESCRIPTOR 拿設(shè)備描述符再拿配置描述符集合里面包含接口描述符、端點(diǎn)描述符和 CDC 特有的功能描述符如 Call Management、Abstract Control Management。GD32 的 USB 外設(shè)支持全速12 Mbps設(shè)備模式枚舉階段需要固件正確響應(yīng)標(biāo)準(zhǔn)請(qǐng)求否則主機(jī)不會(huì)加載 CDC 驅(qū)動(dòng)自然也不會(huì)出現(xiàn) COM 端口。端點(diǎn)分配是第一個(gè)容易搞混的點(diǎn)。CDC-ACM 通常需要三個(gè)端點(diǎn)一個(gè)控制端點(diǎn) EP0雙向枚舉和類請(qǐng)求走這里一個(gè)批量輸入端點(diǎn)設(shè)備到主機(jī)比如 EP1_IN一個(gè)批量輸出端點(diǎn)主機(jī)到設(shè)備比如 EP2_OUT。有些實(shí)現(xiàn)還會(huì)加一個(gè)中斷輸入端點(diǎn)用于串口狀態(tài)通知但最小回環(huán)只需要前三個(gè)。GD32 的 USBFS 外設(shè)端點(diǎn) FIFO 是共享的分配時(shí)要算好每個(gè)端點(diǎn)占多少深度不然枚舉能過(guò)但一發(fā)數(shù)據(jù)就丟包。常見(jiàn)做法是直接復(fù)用官方 USB 庫(kù)里的 CDC 例程框架把端點(diǎn)號(hào)、FIFO 深度、緩沖區(qū)大小按自己的芯片型號(hào)改。GD32F103、GD32F303、GD32E230 這幾個(gè)系列的 USB 寄存器布局有差異不能拿一份代碼無(wú)腦通刷。2.2 時(shí)鐘樹(shù)與 USB 48 MHz 時(shí)鐘源配置USB 全速設(shè)備要求 48 MHz 時(shí)鐘精度不夠就會(huì)枚舉失敗或者頻繁掉線。GD32 的 USB 時(shí)鐘來(lái)源一般有兩個(gè)內(nèi)部 IRC48M 或者外部晶振經(jīng) PLL 倍頻。用內(nèi)部 IRC48M 省事但溫漂和精度在批量產(chǎn)品里是隱患用外部 8 MHz 晶振經(jīng) PLL 到 48 MHz 更穩(wěn)但時(shí)鐘樹(shù)配置錯(cuò)一步就出不來(lái)。以 GD32F103 為例常見(jiàn)配置是外部 8 MHz 晶振PLL 倍頻到 72 MHz 作為系統(tǒng)時(shí)鐘USB 預(yù)分頻器設(shè)置為 1.5 分頻得到 48 MHz。這里有個(gè)血淚經(jīng)驗(yàn)USB 預(yù)分頻寄存器的值不是隨便填的必須保證輸出嚴(yán)格 48 MHz差一點(diǎn)枚舉就時(shí)好時(shí)壞。配置完可以用 MCO 引腳輸出 USB 時(shí)鐘拿示波器量一下比盲猜靠譜。/* 時(shí)鐘配置片段外部8MHz晶振 - PLL 72MHz - USB 48MHz */ void system_clock_config(void) { /* 使能外部高速晶振 */ rcu_osci_on(RCU_HXTAL); while(rcu_osci_stab_wait(RCU_HXTAL) ! SUCCESS); /* 配置PLLHXTAL 8MHz預(yù)分頻1倍頻9 - 72MHz */ rcu_pll_config(RCU_PLLSRC_HXTAL, RCU_PLL_MUL9); /* 配置USB預(yù)分頻72MHz / 1.5 48MHz */ rcu_usb_clock_config(RCU_CKUSB_CKPLL_DIV1_5); /* 使能PLL并切換系統(tǒng)時(shí)鐘 */ rcu_osci_on(RCU_PLL_CK); while(rcu_osci_stab_wait(RCU_PLL_CK) ! SUCCESS); rcu_system_clock_source_config(RCU_CKSYSSRC_PLL); while(rcu_system_clock_source_get() ! RCU_SCSS_PLL); }這段代碼的關(guān)鍵參數(shù)是RCU_PLL_MUL9和RCU_CKUSB_CKPLL_DIV1_5。前者決定系統(tǒng)主頻后者決定 USB 時(shí)鐘。如果你的板子用的是 12 MHz 晶振倍頻系數(shù)和預(yù)分頻都要重算不能照抄。配置完建議先點(diǎn)個(gè)燈確認(rèn)系統(tǒng)時(shí)鐘跑起來(lái)了再往下調(diào) USB。2.3 描述符定制VID/PID 與字符串描述符枚舉能不能過(guò)描述符占一半責(zé)任。設(shè)備描述符里的 VID廠商 ID和 PID產(chǎn)品 ID如果和主機(jī)上已有驅(qū)動(dòng)沖突或者用了未授權(quán)的值Windows 可能直接拒絕加載。常見(jiàn)做法是開(kāi)發(fā)階段先用一個(gè)不沖突的自定義 VID/PID量產(chǎn)前再換成正式申請(qǐng)的值。字符串描述符支持多語(yǔ)言索引 0 是語(yǔ)言 ID 列表后面才是廠商字符串、產(chǎn)品字符串、序列號(hào)字符串。序列號(hào)建議每臺(tái)設(shè)備唯一方便主機(jī)區(qū)分多個(gè)同型號(hào)設(shè)備。如果序列號(hào)全一樣插兩個(gè)上去主機(jī)可能只認(rèn)一個(gè)。/* 設(shè)備描述符關(guān)鍵字段 */ const usb_desc_dev cdc_dev_desc { .header { .bLength USB_DEV_DESC_LEN, .bDescriptorType USB_DESCTYPE_DEV, }, .bcdUSB 0x0200, /* USB 2.0 */ .bDeviceClass USB_CLASS_CDC, /* CDC類 */ .bDeviceSubClass 0x00, .bDeviceProtocol 0x00, .bMaxPacketSize0 64, /* EP0最大包64字節(jié) */ .idVendor 0x1234, /* 自定義VID量產(chǎn)需替換 */ .idProduct 0x5678, /* 自定義PID */ .bcdDevice 0x0100, .iManufacturer 0x01, .iProduct 0x02, .iSerialNumber 0x03, .bNumberConfigurations 0x01, };bMaxPacketSize0對(duì)全速設(shè)備固定是 64低速才是 8填錯(cuò)枚舉直接掛。bDeviceClass填 CDC 類有些實(shí)現(xiàn)填 0 讓接口描述符去聲明類兩種都能過(guò)但填 0 時(shí)接口描述符必須寫(xiě)清楚不然主機(jī)不知道加載什么驅(qū)動(dòng)。3. 收發(fā)程序落地從枚舉成功到雙向回環(huán)的完整步驟3.1 工程搭建與 USB 庫(kù)文件裁剪拿到這份資源后第一步不是急著編譯而是把工程結(jié)構(gòu)理清楚。典型目錄里會(huì)有 USB 庫(kù)文件usbd_core.c、usbd_std.c 等、CDC 類實(shí)現(xiàn)文件usbd_cdc_core.c、硬件驅(qū)動(dòng)gd32f10x_usb.c 之類和應(yīng)用層main.c、usb_cdc_app.c。不同 GD32 系列的庫(kù)文件名可能帶不同前綴比如 GD32F30x 和 GD32E23x 的庫(kù)不通用。裁剪原則是只保留用到的類。如果只做 CDCHID、MSC 那些文件全刪掉減少編譯時(shí)間和 Flash 占用。中斷向量表里 USB 中斷的入口要對(duì)應(yīng)上GD32 的 USBFS 中斷處理函數(shù)名一般是USBFS_IRQHandler在啟動(dòng)文件里確認(rèn)沒(méi)被別的外設(shè)占用。# 典型工程目錄結(jié)構(gòu)以GD32F103為例 project/ ├── firmware/ │ ├── gd32f10x_usb.c # USB外設(shè)底層驅(qū)動(dòng) │ └── gd32f10x_usb.h ├── usbd/ │ ├── usbd_core.c # USB設(shè)備核心狀態(tài)機(jī) │ ├── usbd_std.c # 標(biāo)準(zhǔn)請(qǐng)求處理 │ └── cdc/ │ ├── usbd_cdc_core.c # CDC類實(shí)現(xiàn) │ └── usbd_cdc_core.h ├── app/ │ ├── main.c # 主循環(huán) │ └── usb_cdc_app.c # 收發(fā)應(yīng)用邏輯 └── startup/ └── startup_gd32f10x.s # 啟動(dòng)文件確認(rèn)USB中斷入口編譯前先確認(rèn)啟動(dòng)文件里的堆棧大小夠用USB 枚舉階段會(huì)用到一定??臻g棧太小會(huì)進(jìn) HardFault。我一般把棧調(diào)到 0x400 以上保險(xiǎn)。3.2 端點(diǎn)初始化與數(shù)據(jù)收發(fā)函數(shù)USB 初始化分兩步先初始化 USB 外設(shè)和端點(diǎn)再等主機(jī)枚舉完成。枚舉完成的標(biāo)志是usbd_cdc_core里的配置狀態(tài)變成已配置此時(shí)才能開(kāi)始批量端點(diǎn)收發(fā)。發(fā)送函數(shù)的核心是把數(shù)據(jù)拷進(jìn)端點(diǎn) FIFO然后置發(fā)送允許位。接收則是等端點(diǎn)收到數(shù)據(jù)后從 FIFO 讀出來(lái)。GD32 的 USB FIFO 訪問(wèn)有 32 位對(duì)齊要求按字節(jié)讀會(huì)出問(wèn)題必須用 32 位讀再拆字節(jié)。/* CDC數(shù)據(jù)發(fā)送把buf里的len字節(jié)通過(guò)EP1_IN發(fā)出 */ uint8_t cdc_send_data(uint8_t *buf, uint16_t len) { uint16_t sent 0; /* 等待上一次發(fā)送完成 */ while(usb_tx_flag 0); usb_tx_flag 0; /* 寫(xiě)入端點(diǎn)FIFO注意32位對(duì)齊 */ while(sent len) { uint32_t word 0; for(int i 0; i 4 (sent i) len; i) { word | (uint32_t)buf[sent i] (8 * i); } usb_ep_write_fifo(EP1_IN, word); sent 4; } /* 設(shè)置傳輸長(zhǎng)度并允許發(fā)送 */ usb_ep_set_tx_len(EP1_IN, len); usb_ep_tx_enable(EP1_IN); return 0; } /* CDC數(shù)據(jù)接收從EP2_OUT讀數(shù)據(jù)到buf */ uint16_t cdc_recv_data(uint8_t *buf, uint16_t max_len) { uint16_t recv_len usb_ep_get_rx_len(EP2_OUT); if(recv_len 0 || recv_len max_len) return 0; for(uint16_t i 0; i recv_len; i 4) { uint32_t word usb_ep_read_fifo(EP2_OUT); for(int j 0; j 4 (i j) recv_len; j) { buf[i j] (word (8 * j)) 0xFF; } } usb_ep_rx_enable(EP2_OUT); /* 重新允許接收 */ return recv_len; }usb_tx_flag是個(gè)軟件標(biāo)志在發(fā)送完成中斷里置位主循環(huán)靠它判斷能不能發(fā)下一包。沒(méi)有這個(gè)標(biāo)志直接連續(xù)發(fā)第二包會(huì)覆蓋第一包現(xiàn)象就是發(fā)出去的數(shù)據(jù)缺斤少兩。接收端每次讀完必須重新使能端點(diǎn)否則只能收到第一包這是新手最容易踩的坑之一。3.3 主循環(huán)回環(huán)測(cè)試與串口助手驗(yàn)證應(yīng)用層最簡(jiǎn)單的驗(yàn)證就是回環(huán)收到什么原樣發(fā)回去。主循環(huán)里輪詢接收標(biāo)志有數(shù)據(jù)就調(diào)發(fā)送函數(shù)。PC 端打開(kāi)串口助手波特率隨便填CDC 虛擬串口波特率是形式上的實(shí)際速率由 USB 決定發(fā)一串字符能原樣收到就說(shuō)明收發(fā)通了。/* 主循環(huán)回環(huán)測(cè)試 */ int main(void) { uint8_t rx_buf[64]; uint16_t rx_len; system_clock_config(); usb_gpio_config(); usbd_init(); /* USB設(shè)備初始化 */ usbd_connect(); /* 連接主機(jī)觸發(fā)枚舉 */ while(1) { if(usb_rx_flag) { usb_rx_flag 0; rx_len cdc_recv_data(rx_buf, sizeof(rx_buf)); if(rx_len 0) { cdc_send_data(rx_buf, rx_len); /* 原樣回發(fā) */ } } } }驗(yàn)證時(shí)注意串口助手要選對(duì) COM 號(hào)設(shè)備管理器里能看到「USB 串行設(shè)備」才算枚舉成功。如果發(fā)數(shù)據(jù)沒(méi)反應(yīng)先看設(shè)備管理器有沒(méi)有黃色感嘆號(hào)再看發(fā)送標(biāo)志是不是卡住了?;丨h(huán)通了之后再把回環(huán)邏輯換成自己的業(yè)務(wù)邏輯比如把收到的數(shù)據(jù)解析后控制外設(shè)或者把傳感器數(shù)據(jù)打包上傳。4. 避坑與排查CDC 虛擬串口最常見(jiàn)的五類翻車(chē)4.1 枚舉失敗設(shè)備管理器顯示未知設(shè)備現(xiàn)象插上 USB 后電腦提示「無(wú)法識(shí)別的 USB 設(shè)備」設(shè)備管理器里帶黃色感嘆號(hào)串口列表里沒(méi)有新端口。原因描述符錯(cuò)誤、時(shí)鐘不對(duì)、端點(diǎn) FIFO 配置沖突或者 USB 中斷沒(méi)使能。最常見(jiàn)的是描述符長(zhǎng)度字段填錯(cuò)主機(jī)解析到一半發(fā)現(xiàn)長(zhǎng)度對(duì)不上就放棄枚舉。解決先用 USB 分析儀或者軟件抓包看枚舉卡在哪一步。沒(méi)有分析儀就逐項(xiàng)檢查設(shè)備描述符bLength是不是 18配置描述符集合總長(zhǎng)度對(duì)不對(duì)bMaxPacketSize0是不是 64。時(shí)鐘用 MCO 輸出量一下確認(rèn) 48 MHz。中斷向量表里 USB 中斷入口確認(rèn)沒(méi)被覆蓋。4.2 枚舉成功但發(fā)數(shù)據(jù)丟包現(xiàn)象COM 口出來(lái)了發(fā)少量數(shù)據(jù)正常一發(fā)長(zhǎng)數(shù)據(jù)就丟或者發(fā)幾包之后卡死。原因發(fā)送標(biāo)志沒(méi)等就連續(xù)寫(xiě) FIFO或者端點(diǎn) FIFO 深度不夠長(zhǎng)包被截?cái)唷R灿锌赡苁侵餮h(huán)里收發(fā)邏輯阻塞太久USB 中斷響應(yīng)不及時(shí)。解決發(fā)送前必須等usb_tx_flag這個(gè)標(biāo)志在傳輸完成中斷里置位。FIFO 深度按最大包長(zhǎng)配全速批量端點(diǎn)最大 64 字節(jié)配 64 或 128 都行別配小了。主循環(huán)里別放延時(shí)函數(shù)USB 中斷優(yōu)先級(jí)要高于其他非關(guān)鍵中斷。4.3 接收只能收到第一包現(xiàn)象第一包數(shù)據(jù)能收到之后再也不進(jìn)接收中斷或者讀出來(lái)全是 0。原因接收端點(diǎn)讀完沒(méi)有重新使能。GD32 的 USB 端點(diǎn)收到數(shù)據(jù)后會(huì)自動(dòng) NA K必須軟件重新置接收允許位否則主機(jī)發(fā)不進(jìn)來(lái)。解決在接收處理函數(shù)末尾加usb_ep_rx_enable(EP2_OUT)確保每次讀完都重新打開(kāi)。這個(gè)操作在中斷里做還是主循環(huán)里做都行但必須做。4.4 換一塊同型號(hào)板子就不行現(xiàn)象在自己板子上跑得好好的換一塊同型號(hào)板子有的能枚舉有的不能。原因晶振精度不一致或者 USB 差分線走線質(zhì)量差。USB 全速對(duì)差分信號(hào)質(zhì)量有要求D 和 D- 走線不等長(zhǎng)、沒(méi)做阻抗匹配就會(huì)出現(xiàn)枚舉時(shí)好時(shí)壞。解決優(yōu)先用外部晶振而不是內(nèi)部 IRC48M。差分線走線盡量等長(zhǎng)串 22 歐姆電阻做匹配。如果板子已經(jīng)做好了改不了可以在軟件里加枚舉重試機(jī)制枚舉失敗后延時(shí)重新連接。4.5 主機(jī)休眠后無(wú)法喚醒現(xiàn)象電腦休眠再喚醒COM 口還在但發(fā)數(shù)據(jù)沒(méi)反應(yīng)拔插一次才恢復(fù)。原因USB 掛起和恢復(fù)處理沒(méi)做完整。主機(jī)休眠時(shí) USB 總線進(jìn)入掛起狀態(tài)設(shè)備固件如果沒(méi)正確處理恢復(fù)中斷就會(huì)卡在掛起狀態(tài)出不來(lái)。解決在 USB 中斷里處理掛起和恢復(fù)事件恢復(fù)時(shí)重新初始化端點(diǎn)。如果不需要低功耗可以在描述符里把遠(yuǎn)程喚醒關(guān)掉減少主機(jī)掛起的概率。但根本辦法還是把掛起恢復(fù)流程寫(xiě)對(duì)。5. 進(jìn)階技巧用環(huán)形緩沖把收發(fā)解耦順帶驗(yàn)證穩(wěn)定性回環(huán)測(cè)試跑通只是起點(diǎn)真正用到項(xiàng)目里收發(fā)速率一上來(lái)主循環(huán)輪詢那套就頂不住了。我一般會(huì)加一層環(huán)形緩沖把 USB 中斷和業(yè)務(wù)處理解耦中斷里只負(fù)責(zé)把數(shù)據(jù)塞進(jìn)環(huán)形緩沖或者從緩沖取數(shù)據(jù)發(fā)走主循環(huán)慢慢消費(fèi)。這樣即使業(yè)務(wù)處理偶爾卡一下USB 也不會(huì)丟包。環(huán)形緩沖的實(shí)現(xiàn)要點(diǎn)是讀寫(xiě)指針用volatile修飾防止編譯器優(yōu)化出玄學(xué)問(wèn)題。緩沖區(qū)大小取 2 的冪用位與代替取模速度快且不會(huì)算錯(cuò)。下面是一個(gè)最小實(shí)現(xiàn)/* 環(huán)形緩沖大小256字節(jié)2的冪方便位與 */ #define RING_BUF_SIZE 256 #define RING_BUF_MASK (RING_BUF_SIZE - 1) typedef struct { volatile uint16_t head; /* 寫(xiě)指針 */ volatile uint16_t tail; /* 讀指針 */ uint8_t buf[RING_BUF_SIZE]; } ring_buf_t; /* 寫(xiě)入數(shù)據(jù)返回實(shí)際寫(xiě)入字節(jié)數(shù) */ uint16_t ring_write(ring_buf_t *rb, uint8_t *data, uint16_t len) { uint16_t i; for(i 0; i len; i) { uint16_t next (rb-head 1) RING_BUF_MASK; if(next rb-tail) break; /* 緩沖滿 */ rb-buf[rb-head] data[i]; rb-head next; } return i; } /* 讀出數(shù)據(jù)返回實(shí)際讀出字節(jié)數(shù) */ uint16_t ring_read(ring_buf_t *rb, uint8_t *data, uint16_t max_len) { uint16_t i; for(i 0; i max_len; i) { if(rb-tail rb-head) break; /* 緩沖空 */ data[i] rb-buf[rb-tail]; rb-tail (rb-tail 1) RING_BUF_MASK; } return i; }head和tail用volatile是必須的因?yàn)橹袛嗪椭餮h(huán)都會(huì)改它們不加編譯器可能把判斷優(yōu)化掉現(xiàn)象就是緩沖明明有數(shù)據(jù)但讀不出來(lái)。RING_BUF_MASK用位與代替%運(yùn)算在 Cortex-M 上快很多而且不會(huì)因?yàn)樨?fù)數(shù)取模出問(wèn)題。驗(yàn)證穩(wěn)定性有個(gè)土辦法但很有效PC 端用腳本持續(xù)發(fā)隨機(jī)長(zhǎng)度數(shù)據(jù)設(shè)備端回環(huán)跑一晚上第二天看有沒(méi)有丟包或者死機(jī)。我一般會(huì)寫(xiě)個(gè)簡(jiǎn)單的 Python 腳本用 pyserial 每隔幾毫秒發(fā)一包包長(zhǎng)在 1 到 64 之間隨機(jī)同時(shí)統(tǒng)計(jì)發(fā)送和接收的字節(jié)數(shù)對(duì)不上就說(shuō)明中間丟了。# 穩(wěn)定性測(cè)試腳本持續(xù)發(fā)隨機(jī)長(zhǎng)度數(shù)據(jù)并校驗(yàn)回環(huán) import serial import random import time ser serial.Serial(COM3, 115200, timeout1) total_sent 0 total_recv 0 try: while True: length random.randint(1, 64) data bytes([random.randint(0, 255) for _ in range(length)]) ser.write(data) total_sent length echo ser.read(length) total_recv len(echo) if echo ! data: print(f數(shù)據(jù)不匹配: 發(fā)送{data.hex()}, 收到{echo.hex()}) time.sleep(0.005) except KeyboardInterrupt: print(f發(fā)送{total_sent}字節(jié), 接收{(diào)total_recv}字節(jié)) ser.close()這個(gè)腳本跑起來(lái)后如果total_sent和total_recv對(duì)不上或者頻繁打印數(shù)據(jù)不匹配就說(shuō)明固件端還有丟包。常見(jiàn)原因是環(huán)形緩沖太小高速率下溢出把RING_BUF_SIZE加到 512 或 1024 再試。另一個(gè)原因是主循環(huán)消費(fèi)太慢可以在主循環(huán)里一次多讀幾個(gè)字節(jié)減少循環(huán)次數(shù)。從那以后我每次調(diào) USB 收發(fā)都強(qiáng)制先跑一遍這個(gè)隨機(jī)長(zhǎng)度回環(huán)測(cè)試確認(rèn)不丟包了再往上疊業(yè)務(wù)邏輯。很多看起來(lái)是業(yè)務(wù)層的問(wèn)題根子其實(shí)在 USB 收發(fā)這一層沒(méi)做穩(wěn)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取