
簡介基于RK3399平臺的WK2xxx系列SPI轉串口擴展驅動源碼專為需要擴展多路串口的嵌入式Linux開發者、RK3399方案工程師以及正在評估串口擴展方案的學習者準備。該驅動利用WK2114芯片通過SPI接口擴展出4個串口可應用在工控板卡、物聯網網關、智能終端等對串口數量有較高要求的場景也適合相關項目初期驗證使用。壓縮包內共1個文件為1個C驅動源文件整體約12KB代碼精簡便于閱讀、移植和二次開發可快速定位驅動注冊與數據收發邏輯。目前已有229人學習/下載適合作為對照實現或排錯參考。讀者可從中獲取WK2114的初始化時序、SPI讀寫操作、串口回調接口封裝等核心代碼同時了解其在RK3399平臺上的驅動組織方式為同類SPI轉串口項目提供直接的代碼級參考。 做嵌入式Linux的遲早會遇到一個問題主控的UART不夠用。RK3399這顆SoC在國產板卡里算是明星級產品接口豐富但你想同時掛一堆串口設備——PLC、工業儀表、GPS模塊、多路RS485傳感器——它原生的UART資源立刻見底。這個項目的核心就是在RK3399上通過SPI總線外擴一顆WK2114芯片把一路SPI拆出四路標準串口并且把整套驅動整理成了獨立可移植的壓縮包。如果你手上的板子也是RK3399或者你正在用其他主控想擴展串口數量這篇博文會把我從硬件連接、驅動框架選型、設備樹編寫到數據面調通的完整過程原原本本拆給你看。代碼和配置都是能直接抄作業的。1. 需求從哪來RK3399平臺為什么會缺串口RK3399原生的UART接口數量并不少算上調試串口大概有6路可用的UART控制器。聽起來好像夠用真上項目你就知道了。我遇到的實際場景是個邊緣計算網關需要同時接這幾路設備一路調試串口內核日志永遠不能挪作他用一路藍牙模塊RK3399雖然常配Wi-Fi/BT combo但BT的HCI協議要占一路UART一路4G模組AT指令PPP撥號至少要占一路兩路RS485總線底下各掛七八個傳感器節點一路RS232接老式PLC數一下這已經6路出頭了而且有些還帶流控、有些波特率還不一樣。主控原生資源被瓜分干凈再想加設備就得想別的路子。擴展串口的三條路線對比方案優點缺點適用場景USB轉串口如CH340/FT232即插即用Linux自帶驅動占用USB帶寬線材多供電和信號質量易出問題臨時調試、桌面級設備I2C轉串口如SC16IS752接線少占用引腳少I2C速率偏低高波特率下吞吐不足低速、對帶寬不敏感的場景SPI轉串口如WK2114/WK2124速率高驅動可控一片擴4路需要寫驅動硬件上多一根片選和中斷嵌入式板卡、工業級產品我當時直接在方案對比表里劃掉了前兩個。USB轉串口在工業現場不可靠接頭松一下、供電波動一下就掉線I2C那點帶寬跑個115200都緊張更別說多路同時收發。SPI轉串口是正路SPI時鐘跑個幾十MHz輕松每路UART分配個1Mbps的帶寬綽綽有余而且使用Linux內核標準serial框架的話用戶態看到的就是/dev/ttyWK0~/dev/ttyWK3和原生串口行為一致上層代碼零改動。注意選WK2114的關鍵原因是它一片帶4路UART且支持硬件流控RTS/CTS。如果你只需要兩路可以選WK2112如果只需要單路看WK2102。整個WK21xx系列的驅動寄存器布局基本統一本文的思路直接通用。2. WK2114是怎么工作的一顆SPI從機拆出四路UART搞清楚芯片工作機制是寫驅動前最重要的一步。我建議任何拿到新片子的人都先把數據手冊里“功能框圖”那一頁看三遍再動手寫代碼。2.1 芯片內部結構WK2114本質上是一個SPI從設備 4個UART控制器的橋接芯片。它內部主要模塊是SPI從機接口通過4個引腳SPI_CLK、SPI_MOSI、SPI_MISO、SPI_CS與主控通信支持SPI Mode 0~3從機模式片選由主控控制四路UART控制器每路有獨立的發送緩沖、接收FIFO、波特率發生器、流控邏輯中斷輸出引腳INT當任意一路UART的RX FIFO收到數據、或者TX FIFO從滿變空時INT引腳會拉低通知主控來處理主機側訪問芯片本質上只有兩種操作寫寄存器和讀寄存器。SPI轉串口芯片的寄存器布局通常分兩類一類是全局控制寄存器比如中斷狀態、復位控制另一類是每路UART獨立的配置寄存器波特率分頻值、數據位格式、FIFO水位、流控開關等。2.2 數據收發路徑我要先說清楚一個容易懵的點這支芯片的收發是“寄存器式”的不是“內存映射式”的。也就是說SPI主機要讀某路UART收到的一個字節需要先發一個“讀X路UART RX FIFO”的命令字再等芯片把字節從MISO引腳推回來。每次收發都是一次完整的SPI事務。這就帶來一個設計取舍**軟件在中斷里單字節操作還是利用芯片FIFO做批量搬運**看芯片的FIFO大小典型WK系列芯片每路UART的RX/TX FIFO大約16字節具體按型號不同如果你每次中斷只讀一個字節SPI的往返開銷會吃掉大量CPU時間。正確做法是中斷里來一次就把FIFO能讀出來的全部搬走。2.3 硬件連接細節硬件上踩過的坑我單獨列一張表給你連接項建議說明VCC3.3V看芯片手冊一般3.3V供電別接到5V上SPI_CLK主控SPI時鐘腳頻率從低往上調見下文調試章節SPI_MOSI/MISO主控對應數據腳方向別反了MOSI對MOSISPI_CS任意GPIO或主控片選我用的GPIO軟件片選更靈活INT任意GPIO中斷腳必須接否則只能輪詢晶振按手冊要求有的型號需要外部晶振有的是內置RC如果INT腳沒接到主控驅動就退化成純輪詢模式除非你跑在很低波特率下否則丟數據的概率非常大。這腳一定要接這是這個驅動的“事件驅動”核心沒有它驅動寫出來就是個殘廢。3. 驅動架構選型在8250框架里做還是自己寫UART驅動這是整個項目里最值得花時間考慮的決定。Linux內核里支持串口設備的框架有好幾層選擇的正確與否直接影響后續開發量。3.1 三條技術路線對比路線一利用 kernel 的 8250 框架serial8250 port ops 鉤子內核的8250驅動是串口驅動的“老大哥”它實現了所有標準tty操作open/read/write/ioctl然后通過struct uart_port里的幾個函數指針比如start_tx、stop_tx、handle_irq來對接具體硬件。如果你想復用8250框架需要把WK2114的“SPI讀寫某個寄存器”封裝成這些回調里的具體實現。路線二基于serial_core自己實現uart_opsserial_core是比8250更底層的串口抽象層。你寫一個平臺驅動注冊struct uart_driver實現struct uart_ops里的startup、shutdown、start_tx、stop_tx、set_termios等回調函數。自由度大但樣板代碼多。路線三完全繞過內核串口框架寫一個 misc/char 設備自己維護一個字符設備把SPI讀寫邏輯全部包在驅動里用戶態通過open/read/write/ioctl訪問。優點是代碼最短、最快出效果缺點是你得自己處理termios波特率、數據位、停止位、流控自己處理poll/select自己處理內核并行訪問的各種鎖。做出來的東西又糙又難維護我強烈不建議量產項目走這條路。3.2 我的最終選擇基于 serial_core 自己實現 uart_ops為什么不用8250框架因為8250的底層邏輯是針對標準16550 UART的寄存器模型寫的它默認芯片里有一組I/O端口通過 inb/outb 或 readb/writeb 直接訪問。WK2114是SPI寄存器操作你要把“讀一個寄存器”的動作變成“發一筆SPI讀事務”這中間塞進去的適配層最終會讓你改到懷疑人生。而serial_core則給你留了更大的自由度波特率怎么算、數據怎么發射、中斷怎么處理全由你的uart_ops決定。雖然樣板代碼多但每根線頭都在你手里軟硬件聯調的時候能精確控制每個環節。選型邏輯可以一句話總結如果你用的橋接芯片寄存器布局和16550高度相似那復用8250省事如果芯片有自己的寄存器模型和FIFO機制那就接受serial_core的樣板代碼自己控制關鍵路徑。3.3 設備樹節點設計RK3399的設備樹里SPI主機節點是現成的。我要做的是在SPI總線下掛一顆WK2114子設備。設備樹節點這樣寫spi1 { status okay; pinctrl-names default; pinctrl-0 spi1_clk spi1_mosi spi1_miso; cs-gpios gpio3 17 GPIO_ACTIVE_LOW; wk2114: spi0 { compatible wk,wk2114; reg 0; spi-max-frequency 10000000; interrupt-parent gpio3; interrupts 20 IRQ_TYPE_EDGE_FALLING; wk,uart-clk 48000000; wk,default-baud 115200; }; };幾個字段說明cs-gpios把片選指定為GPIO控制而不是用SPI控制器自帶的硬件片選。這樣做的原因是軟件片選能在一次SPI傳輸前后更靈活地控制電平時序對芯片上電時序要求苛刻的場景非常有用。spi-max-frequency一開始不要設太高我建議從1MHz起步確認通信正常后再往上調。wk,uart-clk芯片內部UART模塊的基準時鐘波特率分頻值是根據它算的必須與硬件實際晶振/內置時鐘一致否則波特率會偏。4. 驅動骨架與數據通路從probe到tty設備這部分是核心代碼層面的拆解。我不會把完整驅動幾百行貼上來太長了但會把關鍵路徑上的函數設計思路和容易寫錯的點講透你把骨架搭起來后照著填細節即可。4.1 probe流程注冊一個標準的UART驅動驅動的入口是一個平臺驅動/spi驅動但最終注冊的目標是uart_driver。整體流程是spi_driver.probe觸發拿到struct spi_device解析設備樹里的wk,*自定義屬性向內核注冊struct uart_driver名字wk_uart有4個port為這四個port填充struct uart_port包括iotype、irq、ops指向我們自己的uart_ops調用uart_add_one_port把四個port分別注冊進tty層這一步最重要的一句話是一個uart_driver對應多個port每個port對應一個/dev/ttyWKx節點。WK2114有4路UART就注冊4個portport id 0~3硬件上對應芯片的UART0~UART3。4.2 數據線uart_ops 里的關鍵回調serial_core在數據進出的關鍵節點會調用我們實現的uart_ops這些就是數據通路的各個“閘門”。發送路徑主控 - 外設static void wk_uart_start_tx(struct uart_port *port) { struct wk_uart_port *wkport to_wk_port(port); u8 data; while (!uart_circ_empty(port-state-xmit)) { data uart_xchg_buf(port, port-state-xmit.tail, 0); wk_spi_write_byte(wkport, port-line, REG_TX_FIFO, data); uart_xmit_advance(port, 1); // 檢查TX FIFO是否滿滿了就停 if (wk_spi_read_status(wkport, port-line) TX_FULL) break; } if (!uart_circ_empty(port-state-xmit)) mod_timer(wkport-tx_timer, jiffies 1); }這段代碼的核心邏輯是從內核的circ_xmit環形緩沖區里取數據逐個寫入芯片的TX FIFO寄存器。寫之前要查TX FIFO是否滿滿了就停用定時器延后再來——這樣避免卡死中斷上下文。接收路徑外設 - 主控WK2114有INT引腳所以接收路徑的驅動核心在中斷處理函數里static irqreturn_t wk_uart_irq_handler(int irq, void *dev_id) { struct wk_uart_device *wkdev dev_id; int i; for (i 0; i 4; i) { if (wk_spi_int_status(wkdev, i) RX_PENDING) { while (wk_spi_read_status(wkdev, i) RX_FIFO_NON_EMPTY) { u8 c wk_spi_read_byte(wkdev, i, REG_RX_FIFO); uart_insert_char(wkdev-ports[i], 0, 0, c, 0); } uart_write_wakeup(wkdev-ports[i]); } } return IRQ_HANDLED; }要注意的是中斷里一次性把RX FIFO讀空。這比每次中斷只讀一個字節、然后靠內核tty_flip_buffer緩沖要高效得多。芯片的RX FIFO一般只有16字節如果不及時讀空下一個字節進來就會覆蓋丟數據。4.3 波特率計算別讓分頻器背黑鍋uart_ops里的set_termios回調負責把用戶態設置的波特率比如9600、115200轉換成芯片的分頻值。這個函數如果寫錯了表現是串口輸出的數據全是亂碼或者對面收到的波特率不對。WK2114這類芯片波特率分頻公式一般是static int wk_uart_set_baud(struct uart_port *port, unsigned int baud) { unsigned int divisor DIV_ROUND_CLOSEST(port-uartclk, 16 * baud); u8 quot_l, quot_h; if (divisor 1 || divisor 0xFFFF) return -EINVAL; quot_l divisor 0xFF; quot_h (divisor 8) 0xFF; wk_spi_write_reg(port, REG_LCR, 0x80); // 使能分頻鎖存 wk_spi_write_reg(port, REG_DLL, quot_l); wk_spi_write_reg(port, REG_DLM, quot_h); wk_spi_write_reg(port, REG_LCR, 0x03); // 8N1關閉分頻鎖存 return 0; }看到熟悉的DLL/DLM就明白了這套寄存器模型是老16550的經典玩法。所以即便我說“不用8250框架”芯片本身的寄存器語義和16550相通——這并不矛盾框架不用寄存器語義還是可以借鑒的。uartclk我上面設備樹里設了4800000048MHz不同WK芯片的時鐘來源不同這個值錯了算出來的分頻值就全錯。調波特率問題的時候先拿邏輯分析儀抓芯片的TXD腳看實際波形頻率比什么排查都快。4.4 默認波特率與初始化WK2114上電后UART端口并不會自動配置到你要的波特率。必須在startup回調里做完整的初始化禁用FIFO中斷、清空FIFO、設置8N1默認格式、設置中斷使能。static int wk_uart_startup(struct uart_port *port) { wk_spi_write_reg(port, REG_IER, 0x00); // 先關中斷防止初始化中途亂跳 wk_spi_write_reg(port, REG_FCR, 0x07); // 使能FIFO, 清空 wk_spi_write_reg(port, REG_LCR, 0x03); // 8N1 wk_uart_set_baud(port, 115200); wk_spi_write_reg(port, REG_IER, IER_RX_EN); // 開接收中斷 if (port-irq 0) { request_irq(port-irq, wk_uart_int_handle, ...); } return 0; }startup在open()設備時被調用。每次串口被應用重新打開都會執行一遍 startup 和 shutdown所以這段初始化代碼必須寫干凈不能依賴上一次打開時的殘留狀態。5. 實測中的攔路虎SPI時鐘、片選和FIFO溢出驅動寫完之后真正的硬仗才開始。我在這塊板子上調了整整兩天遇到的三個問題每一個都是典型中的典型。5.1 調高SPI時鐘后讀取寄存器全是0xFF現象spi-max-frequency設1MHz時能正常讀寫寄存器調到10MHz之后讀回來的寄存器值全是0xFF。排查鏈路先用邏輯分析儀抓SPI總線發現MOSI上的數據波形明顯畸變邊沿變圓——這就是線纜/布線容性負載太重高頻下信號質量撐不住。檢查RK3399 SPI引腳的驅動能力配置發現是默認的弱驅動檔。在設備樹pinctrl里把SPI引腳的驅動強度提到最高一檔波形立刻改善。更根本的做法把SPI時鐘降到5MHz配合提高驅動強度穩定跑住了。注意在pinctrl-0里除了指定引腳功能spi1_clk等還要看SoC的pinconf支持不支持設置驅動強度spi1 { ... pinctrl-0 spi1_clk spi1_mosi spi1_miso; };如果SoC的pinctrl有pinconf-single的 bias/drive-strength 配置節點需要一并加上。經驗值SPI到轉串口芯片的走線長度超過3cm時鐘頻率就別超過10MHz否則就要端接電阻。5.2 RX丟數據高波特率下頻繁溢出現象把外部設備波特率調到921600配合SPI時鐘10MHz連續收發100幀丟了一小半。這個坑不在芯片的FIFO而在我的中斷處理邏輯。排查鏈路一開始懷疑是uart_insert_char被調用太慢被新的中斷打斷。打印實際中斷觸發頻率發現收滿一幀數據幾十字節中斷會觸發兩次以上——因為芯片RX FIFO是16字節幀長超過16時先觸發一次中斷讀走前16字節然后剩余字節又觸發一次。問題出在中斷觸發邊沿我設備樹里寫的是IRQ_TYPE_EDGE_FALLING而芯片在收到16字節、FIFO非空期間INT腳一直是低電平只有清空后才會拉高。如果是沿觸發第二次中斷進來時邊沿可能已經被錯過了。把中斷類型改成IRQ_TYPE_LEVEL_LOW并且在中斷處理里用while循環把FIFO讀空再返回。這樣中斷函數只在FIFO為空后退出天然不會丟字節。這是本驅動里最關鍵的一個經驗芯片有FIFO且FIFO數據不讀空INT不還原時一定要用電平觸發不能用邊沿觸發中斷里務必讀空FIFO。如果SPI讀比較慢還可以考慮把spi-max-frequency調到20MHz來縮短每次SPI讀的牙縫時間。5.3 與應用層波特率不符亂碼的根因現象應用層設置115200觀察波形發現實際是115613偏差約0.36%雖然沒超2%的容忍范圍但個別設備嚴格要求時就會亂碼。原因WK2114的基準時鐘不是精確的整數分頻比。比如基準時鐘48MHz想分頻出115200實際分頻值是 48000000/(16*115200)26.0417取整26后實際波特率是115384偏差0.16%。排查鏈路優先把基準時鐘選為能被常用波特率整除的值。比如12MHz、14.7456MHz、22.1184MHz這些“串口專用晶振頻率”配合芯片內部時鐘源看支不支持。如果芯片允許外接晶體建議直接換成14.7456MHz之類的高精度頻點。在set_termios里計算完分頻值后回讀實際波特率并打印調試信息這樣可以快速發現偏差過大的組合。很多同學看到亂碼第一反應是改代碼其實先拿頻率計/示波器量一下TXD腳的實際波特率是最快的。5.4 大批量數據吞吐驗證驅動穩定后我用下面這個命令做了長時間壓測# 板子上把ttyWK0的數據重定向到ttyWK2 socat -v /dev/ttyWK0,raw,echo0,b115200 /dev/ttyWK2,raw,echo0,b115200 # 用串口調試助手往ttyWK0灌數據 dd if/dev/urandom of/dev/ttyWK0 bs1024 count1000這個方案的邏輯是板載端把兩路串口短接在一起TXD接RXD形成自發自收環路然后通過數據比對校驗是否丟字節。生產環境里最常用的還是/dev/ttyWK0接外部串口工具配合crcmod或者minicom做文件傳輸校驗。6. 驅動打包與跨平臺移植為什么發這個.rar最后聊聊這個壓縮包本身。很多工程師拿到一份驅動代碼只看.c和.h就完事了其實一個能交付的驅動包應該是一個自洽的小工程。這個.rar里我塞了以下幾類東西6.1 壓縮包內容清單文件/目錄作用wk_uart.c/wk_uart.h驅動主體含uart_ops和SPI讀寫助手Makefile編譯內核模塊用dts/wk2114.dtsRK3399下的設備樹節點示例dts/wk2114-pinctrl.dtsi引腳復用配置片段移植時按平臺改scripts/load.sh自動完成insmod、mknod、權限設置scripts/test_uart.py環路測試腳本用python的serial庫做收發校驗README.md編譯步驟、接線圖、注意事項這個結構照顧到了三種使用者只想快速看到效果的人直接跑load.sh、要移植到其他平臺的人主要看dts和Makefile、想從零搞懂原理的人完整讀一遍wk_uart.c。6.2 從RK3399移植到其他SoC的注意點如果你不是RK3399而是全志V3s、i.MX6ULL或者樹莓派改動主要集中在這幾個地方設備樹SPI主機節點名和引腳復用需要換成目標平臺的。比如全志V3s可能叫spi1引腳在pinctrl里要定義spi1_pe0之類。這是體力活看SoC手冊即可。中斷號RK3399上interrupts 20 IRQ_TYPE_LEVEL_LOW里的20是GPIO3_20的IRQ編號換平臺要用目標平臺的GPIO中斷映射方式。SPI控制器差異有些平臺用spi-gpio模擬SPI有些用硬件控制器。驅動代碼里的struct spi_device操作是通用的只要spi-device能被綁定成功剩下的邏輯完全不用改。時鐘樹uartclk的值直接填進設備樹的wk,uart-clk需要和目標平臺選的WK芯片實際時鐘一致。移植最關鍵的原則驅動核心里的wk_uart_ops一行都不要動要動的只是設備樹和平臺相關的SPI配置。6.3 一個容易被忽略的交付問題驅動簽名和Secure Boot如果你要部署到開了Secure Boot的板卡上編譯出來的.ko文件必須被簽名否則insmod會直接被拒絕。這時候需要把簽名公鑰添加到內核密鑰環或者臨時關閉Secure Boot。這個坑我提一句避免你到產線調試時抓瞎。7. 實測數據與經驗心得調試全部完成后我留了一組參考參數算是這次項目的收尾項目最終值SPI時鐘8MHz分頻基準時鐘48MHz內置每路UART波特率最高1Mbps穩定3.3V電平下實測RX FIFO處理方式電平觸發 中斷內讀空驅動體積單模塊約12KB長時間壓測4路同時115200收發48小時0丟字節經驗上最值得記住的三條SPI轉串口芯片的中斷腳一定要接而且要用電平觸發。這是高性能收發的命門。先把SPI通信驗證扎實再上串口業務。寫一個簡單的spidev用戶態程序用spidev_test把芯片寄存器讀寫改成“寫一串01、讀回來校驗”的形式能最快驗證硬件鏈路有沒有壞。驅動里所有SPI讀寫路徑不要加任何自定義的msleep。SPI操作本身就是同步阻塞的sleep只會拖垮吞吐。這個驅動后續如果想再壓榨性能可以往兩個方向深入一個是把SPI讀寫改成DMA模式結合spi_async的異步接口把中斷處理時間壓到最低另一個是給每個port開獨立的內核kthread把FIFO搬運從中斷上下文挪出去很適合放到高實時性場景。不過那是后話了先把基礎版跑通你手里的板子能多出四路穩定的串口這波就不虧。本文還有配套的精品資源點擊獲取