動(dòng)串口屏USART_HMI:從協(xié)議到調(diào)試全解析)
寫(xiě)這個(gè)系列之前我一直覺(jué)得FPGA搞顯示是很勸退的一件事。數(shù)字邏輯寫(xiě)到能跑UART、能收發(fā)字符串已經(jīng)算入門(mén)了結(jié)果一看需求要把采集到的電壓、溫度、狀態(tài)幀值顯示在屏幕上還得有幾個(gè)按鈕能切頁(yè)面——很多人在這一步直接轉(zhuǎn)投ARM了。直到我把USART_HMI串口屏引入項(xiàng)目才發(fā)現(xiàn)FPGA做交互界面也可以這么省心。這篇是FPGA串口收發(fā)字符串系列的第四篇專(zhuān)門(mén)把串口屏USART_HMI這塊講透包括協(xié)議幀格式、FPGA側(cè)指令發(fā)送模塊怎么寫(xiě)、聯(lián)調(diào)時(shí)會(huì)遇到哪些坑以及后續(xù)可以往雙向交互延伸的方向。手里已經(jīng)跑通UART收發(fā)模塊的朋友這一章可以直接落地就算前面幾篇沒(méi)讀只要懂串口字節(jié)怎么發(fā)也能跟上。1. 為什么FPGA項(xiàng)目里我會(huì)選串口屏來(lái)顯示1.1 顯示方案的選型對(duì)比很多FPGA工程師第一次面對(duì)要給項(xiàng)目加顯示這個(gè)問(wèn)題時(shí)列出的候選方案大概跟我當(dāng)年差不多數(shù)碼管、LCD1602/COG12864、SPI或者RGB接口的TFT屏最后才輪到串口屏。我把實(shí)際對(duì)比結(jié)果放在一張表里大家感受一下差異。方案顯示能力人機(jī)交互開(kāi)發(fā)工作量FPGA資源占用適合場(chǎng)景數(shù)碼管數(shù)字和少量字母內(nèi)容有限基本無(wú)低動(dòng)態(tài)掃描即可低電壓、計(jì)數(shù)、狀態(tài)指示LCD1602/COG12864字符或簡(jiǎn)單圖形需自己取模無(wú)或按鍵擴(kuò)展中初始化時(shí)序和字庫(kù)都要處理中簡(jiǎn)單文本顯示SPI/RGB接口TFT色彩豐富像素級(jí)控制需要自己寫(xiě)觸摸和GUI高顯存、字庫(kù)、GUI框架全自己做高RGB屏還得外掛SRAM/SDRAM對(duì)顯示刷新速度要求極高的場(chǎng)合USART_HMI串口屏彩色顯示文字、圖形、曲線、圖片均可自帶觸摸事件上報(bào)低上位機(jī)做界面FPGA只發(fā)指令極低占兩個(gè)IO和一個(gè)UART設(shè)備狀態(tài)顯示、參數(shù)面板、人機(jī)交互這里不是說(shuō)數(shù)碼管和TFT不好而是工程上有性?xún)r(jià)比的考量。數(shù)碼管適合那種看一眼就夠的數(shù)據(jù)TFT屏適合需要高頻刷新畫(huà)面的場(chǎng)景但代價(jià)是FPGA里要維護(hù)一整套路顯存和繪圖庫(kù)開(kāi)發(fā)周期以周為單位。串口屏把這塊全包了它內(nèi)部有一顆主控芯片專(zhuān)門(mén)負(fù)責(zé)解析指令、刷新LCD、掃描觸摸FPGA要做的只是按協(xié)議把字符串或者數(shù)值幀發(fā)過(guò)去。1.2 USART_HMI解決的核心問(wèn)題用一句話概括串口屏的本質(zhì)它不是一塊普通顯示屏而是一個(gè)帶GUI引擎的UART從設(shè)備。你在上位機(jī)軟件里畫(huà)好界面拖好文本控件、數(shù)字控件、按鈕分配好變量地址燒錄到屏幕里然后FPGA這邊只需要通過(guò)串口告訴它把變量0x0001的內(nèi)容改成多少多少屏幕上對(duì)應(yīng)的控件就會(huì)自動(dòng)刷新。這和傳統(tǒng)TFT方案完全是兩種思維方式。以前是FPGA逐像素去畫(huà)現(xiàn)在是FPGA只發(fā)命令畫(huà)面怎么渲染、字體怎么顯示、按鈕怎么響應(yīng)都是串口屏自己去處理。就像一個(gè)是自己調(diào)油漆刷墻另一個(gè)是打電話讓裝修隊(duì)干活活一樣能干完但工作量根本不是一個(gè)量級(jí)。我用串口屏之后最直觀的感受是FPGA側(cè)的邏輯可以專(zhuān)注在信號(hào)采集、算法處理、狀態(tài)控制這些真正核心的事情上界面交互只是偶爾發(fā)一串字節(jié)的事。開(kāi)發(fā)周期從幾周縮短到一兩天而且改界面布局不用重新綜合FPGA工程只改串口屏上位機(jī)的工程再燒錄就行這在項(xiàng)目調(diào)試階段非常救命。1.3 什么場(chǎng)景不適合串口屏串口屏不是銀彈它的短板也很明顯我這里把丑話說(shuō)在前面。首先是刷新速度以115200波特率為例理論上一秒只能傳約11520字節(jié)一條簡(jiǎn)單的寫(xiě)變量指令大概10字節(jié)左右也就是說(shuō)一秒最多發(fā)一千多次但實(shí)際考慮幀間隙和串口屏內(nèi)部處理時(shí)間穩(wěn)定跑到幾十赫茲就到頭了。所以它適合展示狀態(tài)數(shù)據(jù)和參數(shù)不適合做高速波形輸出之類(lèi)需要像素級(jí)實(shí)時(shí)更新的界面。其次是成本串口屏的單價(jià)明顯高于同等尺寸的裸屏加驅(qū)動(dòng)方案如果產(chǎn)品已經(jīng)確定要大規(guī)模量產(chǎn)而且顯示內(nèi)容極其固定那還是老老實(shí)實(shí)自己驅(qū)動(dòng)屏幕更劃算。最后還有一點(diǎn)容易忽略串口屏是帶固件的黑盒一旦遇到官方固件不支持的協(xié)議細(xì)節(jié)你沒(méi)法繞過(guò)它直接操作像素靈活度確實(shí)比不上自己寫(xiě)驅(qū)動(dòng)。理解了這個(gè)邊界你才知道什么時(shí)候該用它什么時(shí)候不該用。2. USART_HMI協(xié)議先看懂再動(dòng)手2.1 指令幀的基本結(jié)構(gòu)串口屏雖然界面華麗但通信協(xié)議其實(shí)相當(dāng)簡(jiǎn)潔核心就是一幀一指令。USART_HMI的指令幀通常由幀頭、數(shù)據(jù)長(zhǎng)度、指令碼、數(shù)據(jù)區(qū)和校驗(yàn)字段組成大致是這個(gè)樣子字段長(zhǎng)度說(shuō)明幀頭1字節(jié)固定值0xA5表示一幀的開(kāi)始數(shù)據(jù)長(zhǎng)度1字節(jié)從指令碼到校驗(yàn)之前的字節(jié)數(shù)用于接收端分幀指令碼1字節(jié)標(biāo)識(shí)這條指令要做什么比如寫(xiě)變量、讀變量、跳頁(yè)面數(shù)據(jù)區(qū)N字節(jié)變量地址、數(shù)據(jù)類(lèi)型、具體的數(shù)據(jù)內(nèi)容校驗(yàn)1字節(jié)通常是對(duì)前面數(shù)據(jù)區(qū)做異或和用于驗(yàn)證幀沒(méi)有傳錯(cuò)做FPGA的人看到這種結(jié)構(gòu)應(yīng)該很親切它跟我們的UART狀態(tài)機(jī)是天然匹配的。接收端先等幀頭0xA5收到之后再讀數(shù)據(jù)長(zhǎng)度然后按長(zhǎng)度把剩下的字節(jié)收滿最后做一遍校驗(yàn)幀就完整還原了。這也是為什么串口屏特別適合跟FPGA配合——協(xié)議本身就對(duì)硬件邏輯友好不需要PC上那種復(fù)雜的緩沖解析。不同品牌和固件版本在字段定義細(xì)節(jié)上有差異比如淘晶馳TJC系列、Nextion系列和迪文DGUS系列就是三套不同的體系甚至同一品牌不同批次固件對(duì)長(zhǎng)度字段的計(jì)數(shù)方式都可能不一樣。我在項(xiàng)目里固定用淘晶馳的USART_HMI協(xié)議本文也以它為例。動(dòng)手之前一定要把自己手上屏的協(xié)議手冊(cè)打印出來(lái)對(duì)照別拿網(wǎng)上例程的幀直接抄。2.2 最常用的幾條指令用串口屏控制界面絕大多數(shù)時(shí)間就三條指令寫(xiě)變量、讀變量、跳頁(yè)面。所謂變量就是你在上位機(jī)里給每個(gè)控件綁定的數(shù)據(jù)通道比如文本控件t0綁定了變量v0數(shù)字控件n0綁定了變量n0這些變量在屏的內(nèi)部存儲(chǔ)里都有固定地址FPGA只要往對(duì)應(yīng)地址寫(xiě)數(shù)據(jù)控件就會(huì)刷新。寫(xiě)變量指令的語(yǔ)義是往地址X寫(xiě)入數(shù)據(jù)Y。數(shù)據(jù)Y可以是字符串也可以是二進(jìn)制數(shù)值具體是哪一種由數(shù)據(jù)類(lèi)型字段區(qū)分。舉個(gè)例子如果要在文本控件里顯示OK兩個(gè)字指令幀的數(shù)據(jù)區(qū)就包含控件地址、字符串類(lèi)型標(biāo)識(shí)、以及字符O和K的ASCII碼。FPGA要做的事情并不復(fù)雜地址、類(lèi)型、ASCII碼都是確定的算好長(zhǎng)度和校驗(yàn)把整幀發(fā)出去即可。讀變量指令是反過(guò)來(lái)FPGA向屏請(qǐng)求讀取某地址的數(shù)據(jù)屏收到后會(huì)把當(dāng)前值回傳。這條指令在做雙向交互時(shí)很有用比如FPGA上電后要讀取屏上保存的參數(shù)。跳頁(yè)面指令就更直白了告訴屏切換到某個(gè)頁(yè)面ID適合做多菜單、多界面切換。在FPGA里設(shè)計(jì)發(fā)送模塊的時(shí)候我建議把這些指令統(tǒng)一封裝成一個(gè)指令序列器每條指令是一個(gè)固定模板需要變化的只是地址和數(shù)據(jù)區(qū)。模板加上動(dòng)態(tài)參數(shù)拼成一幀再送到UART模塊發(fā)出去。這樣代碼結(jié)構(gòu)清晰后面要加新指令只是加一個(gè)模板的事。2.3 控件變量地址與數(shù)據(jù)類(lèi)型的坑用串口屏上位機(jī)拖控件的時(shí)候很多人會(huì)忽略地址分配這件事。上位機(jī)軟件的變量控件列表里每個(gè)變量都有一個(gè)地址一般是按創(chuàng)建順序從0x0000開(kāi)始編號(hào)。FPGA側(cè)寫(xiě)指令時(shí)用的就是這些地址。如果你只記得控件名字不記得地址那寫(xiě)指令時(shí)就會(huì)對(duì)不上。我一般會(huì)在上位機(jī)里把變量名和地址做成一張對(duì)照表和FPGA代碼放同一個(gè)工程目錄下改一次同步一次不然調(diào)試時(shí)極容易暈。數(shù)據(jù)類(lèi)型字段是另一個(gè)容易出岔子的地方。字符串和數(shù)值的編碼方式不一樣字符串直接用ASCII碼字節(jié)流數(shù)值可以用二進(jìn)制直接傳輸也可以轉(zhuǎn)成ASCII形式的數(shù)字字符。串口屏接收不同數(shù)據(jù)類(lèi)型時(shí)解析邏輯完全不同一旦類(lèi)型標(biāo)識(shí)傳錯(cuò)屏上顯示的就是亂碼或者0。這個(gè)字段在上位機(jī)協(xié)議文檔里有明確的取值表寫(xiě)代碼前要確認(rèn)好。還有一個(gè)細(xì)節(jié)容易被新手忽略字符串長(zhǎng)度和控件顯示區(qū)域的關(guān)系。文本控件如果設(shè)置了最多顯示4個(gè)字符你一幀發(fā)8個(gè)字符進(jìn)去要么被截?cái)嘁窗雅赃厓?nèi)容擠亂。所以FPGA側(cè)發(fā)送字符串之前要么自己做好截?cái)嘁丛谏衔粰C(jī)里把控件長(zhǎng)度放寬。我自己寫(xiě)模塊時(shí)會(huì)在指令序列器里加一個(gè)最大長(zhǎng)度寄存器配合控件屬性一起設(shè)置避免指令本身沒(méi)問(wèn)題但顯示效果稀碎。3. FPGA端串口指令發(fā)送模塊怎么設(shè)計(jì)3.1 模塊劃分思路FPGA端驅(qū)動(dòng)串口屏不需要一個(gè)萬(wàn)能協(xié)議棧那反而把事情搞復(fù)雜了。我習(xí)慣把邏輯拆成三塊指令序列器、字節(jié)發(fā)送器、參數(shù)生成邏輯。字節(jié)發(fā)送器說(shuō)白了就是系列前幾章寫(xiě)的UART TX模塊負(fù)責(zé)把一個(gè)字節(jié)變成串行波形發(fā)出去帶busy或者done信號(hào)。參數(shù)生成邏輯負(fù)責(zé)算地址、算長(zhǎng)度、做校驗(yàn)比如數(shù)值轉(zhuǎn)ASCII、字符串拼接。指令序列器是大腦負(fù)責(zé)按順序把一幀里的字節(jié)逐個(gè)送給字節(jié)發(fā)送器中間控制節(jié)奏保證幀內(nèi)字節(jié)連續(xù)、幀間留出間隔。這個(gè)劃分的好處是每一塊都能單獨(dú)測(cè)試。UART TX單獨(dú)連電腦驗(yàn)證過(guò)參數(shù)生成邏輯可以用Modelsim仿真對(duì)比輸出字節(jié)流指令序列器只要狀態(tài)機(jī)不跑飛整幀順序就不會(huì)亂。等到最后聯(lián)調(diào)時(shí)問(wèn)題范圍能快速鎖定到某一小塊不會(huì)滿工程找bug。3.2 狀態(tài)機(jī)加FIFO的設(shè)計(jì)方式指令序列器我一律采用先組裝到發(fā)送FIFO再自動(dòng)發(fā)送的方式而不是一字節(jié)一狀態(tài)地硬憋。為什么因?yàn)橹噶顜囱b過(guò)程中可能要插入動(dòng)態(tài)參數(shù)如果整個(gè)幀的字節(jié)都是一個(gè)時(shí)鐘一個(gè)時(shí)鐘地直接塞給UART中間一旦被更高優(yōu)先級(jí)的邏輯打斷幀就殘了。用FIFO緩沖一層組裝過(guò)程可以分步完成組裝完畢后UART模塊從FIFO里連續(xù)取字節(jié)發(fā)送幀的原子性就有保證。核心狀態(tài)機(jī)示意代碼大致是這樣// 指令序列器核心狀態(tài)機(jī)節(jié)選 localparam IDLE 3d0; localparam ASSEMBLE 3d1; localparam WAIT_EMPTY 3d2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; end else begin case (state) IDLE: if (tx_start) begin byte_cnt 0; state ASSEMBLE; end ASSEMBLE: begin // 每個(gè)時(shí)鐘寫(xiě)一個(gè)字節(jié)到FIFO // 先寫(xiě)幀頭0xA5再寫(xiě)長(zhǎng)度、指令碼、數(shù)據(jù)區(qū)、校驗(yàn) fifo_wr_en 1b1; fifo_wr_data frame_byte_gen(byte_cnt); if (byte_cnt frame_total_len - 1) begin fifo_wr_en 1b0; state WAIT_EMPTY; end else begin byte_cnt byte_cnt 1b1; end end WAIT_EMPTY: if (fifo_empty) begin // 整幀發(fā)送完成 tx_done 1b1; state IDLE; end endcase end end實(shí)際工程里frame_byte_gen不會(huì)是一個(gè)簡(jiǎn)單函數(shù)而是根據(jù)byte_cnt查表0號(hào)字節(jié)固定是0xA51號(hào)字節(jié)是長(zhǎng)度2號(hào)字節(jié)是指令碼后面的字節(jié)可能是變量地址、數(shù)據(jù)類(lèi)型、數(shù)據(jù)內(nèi)容最后是校驗(yàn)字節(jié)。動(dòng)態(tài)參數(shù)部分在上層邏輯觸發(fā)tx_start之前就預(yù)先算好放在參數(shù)寄存器里組裝階段只是把它們按順序搬進(jìn)FIFO。UART模塊那邊從FIFO讀字節(jié)、逐字節(jié)發(fā)送發(fā)送完成信號(hào)把FIFO讀指針推進(jìn)。這個(gè)方案的妙處在于FIFO天然隔離了兩個(gè)時(shí)鐘域如果有的話和兩級(jí)邏輯組裝過(guò)程不占用UART發(fā)送時(shí)間幀與幀之間還能方便地插入任意延時(shí)。3.3 長(zhǎng)度和校驗(yàn)的計(jì)算時(shí)機(jī)長(zhǎng)度和校驗(yàn)是FPGA工程師最容易寫(xiě)錯(cuò)的地方。長(zhǎng)度字段指的是從這個(gè)字段自身后面開(kāi)始、直到數(shù)據(jù)區(qū)結(jié)束的字節(jié)數(shù)。校驗(yàn)字段通常是對(duì)長(zhǎng)度、指令碼和數(shù)據(jù)區(qū)做異或幀頭0xA5不參與校驗(yàn)。這兩者的計(jì)算必須放在組裝階段的開(kāi)頭算出結(jié)果存進(jìn)寄存器隨幀一起寫(xiě)入FIFO而不是在UART發(fā)送過(guò)程中臨時(shí)去算。為什么強(qiáng)調(diào)時(shí)機(jī)因?yàn)楹芏嘈率謺?huì)在發(fā)送每一字節(jié)的組合邏輯里去動(dòng)態(tài)計(jì)算校驗(yàn)結(jié)果綜合出來(lái)的電路出現(xiàn)奇怪的時(shí)序問(wèn)題幀內(nèi)容對(duì)了但校驗(yàn)總不對(duì)。我現(xiàn)在的做法是先算后發(fā)。幀長(zhǎng)如果固定長(zhǎng)度字段就直接是一個(gè)常量校驗(yàn)則在PREPARE階段用組合邏輯異或樹(shù)算好或者更省事——因?yàn)橐粠止?jié)數(shù)不多完全可以在寫(xiě)入FIFO的時(shí)候同步計(jì)算校驗(yàn)最后一個(gè)字節(jié)寫(xiě)進(jìn)去時(shí)校驗(yàn)值也已經(jīng)算出來(lái)直接跟著寫(xiě)進(jìn)去。這里還有個(gè)工程上的建議第一版盡量把所有指令設(shè)計(jì)成定長(zhǎng)幀。定長(zhǎng)幀的長(zhǎng)度字段是常量地址偏移是常量校驗(yàn)樹(shù)也可以預(yù)先確定結(jié)構(gòu)調(diào)試難度會(huì)低很多。等定長(zhǎng)幀跑通了再改成變長(zhǎng)幀處理動(dòng)態(tài)字符串就會(huì)從容很多。4. FPGA變量如何變成屏幕上的數(shù)字和字符串4.1 數(shù)值轉(zhuǎn)ASCII的處理FPGA內(nèi)部處理的數(shù)據(jù)是二進(jìn)制數(shù)但串口屏要顯示的是人類(lèi)能讀的十進(jìn)制數(shù)字。所以中間必須有一個(gè)數(shù)值轉(zhuǎn)ASCII的過(guò)程。最基礎(chǔ)的方法是把二進(jìn)制數(shù)轉(zhuǎn)成BCD碼然后把每一位BCD碼加上0x30就得到對(duì)應(yīng)數(shù)字的ASCII碼。比如二進(jìn)制數(shù)27轉(zhuǎn)成BCD碼是0010 0111即十位2、個(gè)位7加上0x30后得到0x32和0x37對(duì)應(yīng)字符2和7。二進(jìn)制轉(zhuǎn)BCD常用的是加3移位算法也叫移位加3法一段循環(huán)邏輯就能實(shí)現(xiàn)不需要除法器非常適合FPGA。對(duì)于0到9999范圍的數(shù)值移位16次就可以完成轉(zhuǎn)換占用資源很少。如果你要顯示的是有符號(hào)數(shù)還得先處理符號(hào)位在前面加一個(gè)負(fù)號(hào)0x2D。我看到有些人在FPGA里直接用取模和除法運(yùn)算符去轉(zhuǎn)ASCII仿真沒(méi)問(wèn)題但綜合出的除法器吃資源比較多而且路徑延遲大。在小規(guī)模顯示應(yīng)用里加3移位法既省資源又容易控制時(shí)序是我比較推薦的方案。4.2 字符串指令幀的拼接實(shí)例舉個(gè)實(shí)際例子假設(shè)要把溫度值27度顯示在屏上的文本控件控件地址是0x0001。FPGA側(cè)要發(fā)出的指令幀大致是這樣的先幀頭0xA5然后是長(zhǎng)度、寫(xiě)變量指令碼、地址高字節(jié)、地址低字節(jié)、數(shù)據(jù)類(lèi)型標(biāo)識(shí)接著是2和7兩個(gè)ASCII字節(jié)最后是校驗(yàn)。整個(gè)過(guò)程在指令序列器里就是按byte_cnt依次輸出這些字節(jié)。數(shù)值轉(zhuǎn)ASCII的結(jié)果怎么和固定幀頭拼接呢我一般用一個(gè)可變長(zhǎng)度移位寄存器組方式把固定部分、動(dòng)態(tài)部分、校驗(yàn)部分分別存到三個(gè)小寄存器區(qū)組裝時(shí)按順序選通。如果幀長(zhǎng)固定動(dòng)態(tài)部分也只是長(zhǎng)度固定那整個(gè)幀甚至可以定義成一個(gè)寄存器數(shù)組ASSEMBLE狀態(tài)里按地址查表輸出即可。調(diào)試的時(shí)候有一個(gè)很實(shí)用的技巧在UART TX的輸出引腳上接邏輯分析儀或者直接在FPGA里把那路信號(hào)引到一個(gè)空閑引腳用示波器看波形。整幀字節(jié)的時(shí)序波形一目了然哪里的字節(jié)不對(duì)對(duì)照協(xié)議文檔一比對(duì)就能定位到是拼接邏輯還是數(shù)值轉(zhuǎn)換的問(wèn)題。4.3 顯示刷新節(jié)奏與帶寬估算串口屏能接受的顯示刷新速度沒(méi)有你想象的那么高。我實(shí)測(cè)下來(lái)狀態(tài)類(lèi)數(shù)據(jù)10到20Hz刷新就已經(jīng)非常平滑了沒(méi)必要每毫秒無(wú)腦刷。原因有兩個(gè)一是串口帶寬有限115200波特率一秒鐘才能傳一萬(wàn)多字節(jié)一條指令就是十幾字節(jié)刷新頻率越高留給其他指令的帶寬就越少二是串口屏內(nèi)部解析幀、刷新LCD也需要時(shí)間指令過(guò)來(lái)太密會(huì)出現(xiàn)排隊(duì)甚至丟幀。所以我一般在FPGA里給串口屏發(fā)送加一個(gè)節(jié)流器比如用一個(gè)計(jì)數(shù)器產(chǎn)生10ms或50ms的周期脈沖每個(gè)脈沖最多觸發(fā)一幀顯示指令。這樣既保證了人眼看起來(lái)連續(xù)又不會(huì)把串口屏和串口帶寬壓到極限。我在做多通道數(shù)據(jù)采集顯示時(shí)用5ms周期更新一個(gè)頁(yè)面同時(shí)還要處理觸摸事件上報(bào)沒(méi)有出現(xiàn)過(guò)串口擁堵的問(wèn)題。記住一個(gè)原則顯示類(lèi)的數(shù)據(jù)寧可少刷不要暴刷。5. 聯(lián)調(diào)實(shí)錄這幾個(gè)坑我替你們踩過(guò)了5.1 波特率誤差帶來(lái)的隱性故障FPGA和串口屏之間的波特率如果對(duì)不上表現(xiàn)不是完全不通而是時(shí)好時(shí)壞尤其當(dāng)誤差積累到采樣的臨界點(diǎn)時(shí)會(huì)隨機(jī)出亂碼。這個(gè)坑隱蔽性很高因?yàn)樗环€(wěn)定復(fù)現(xiàn)很容易讓人懷疑是屏壞了或者代碼邏輯錯(cuò)了。正確的做法是先用頻率計(jì)確認(rèn)FPGA系統(tǒng)的基準(zhǔn)時(shí)鐘頻率再算波特率分頻值。比如50MHz系統(tǒng)時(shí)鐘跑115200波特率分頻值是50_000_000/115200約等于434取整后誤差不到0.01%基本可以忽略。但如果你的板子用的是24MHz或者12MHz晶振分頻后的誤差就可能超過(guò)1%這時(shí)候就要考慮換一個(gè)更合適的波特率或者用小數(shù)分頻器來(lái)處理。我的習(xí)慣是FPGA側(cè)和串口屏都固定用115200這幾乎是兩者之間最穩(wěn)的一個(gè)平衡點(diǎn)。9600雖然更抗干擾但顯示刷新有明顯的打字機(jī)感不太適合做界面。另外要注意串口屏模塊的默認(rèn)波特率可能和它實(shí)際跑的波特率不一致收到屏之后第一次通電就要用串口助手確認(rèn)屏的配置。5.2 幀被切斷和指令被吞的問(wèn)題聯(lián)調(diào)時(shí)最常遇到的現(xiàn)象是屏上偶爾顯示一次正確內(nèi)容然后連續(xù)發(fā)好幾條指令都沒(méi)反應(yīng)或者顯示出來(lái)的是殘缺數(shù)據(jù)。排查下來(lái)的典型原因是FPGA側(cè)的指令序列器在多幀連續(xù)發(fā)送時(shí)幀間隙控制不好或者處理器邏輯在發(fā)送過(guò)程中搶占了串口發(fā)送的優(yōu)先級(jí)導(dǎo)致幀頭后面的數(shù)據(jù)沒(méi)跟上。解決思路就是我前面說(shuō)的FIFO緩沖方案讓整幀組裝成一個(gè)不可分割的整體。發(fā)送期間絕不允許其他邏輯往UART模塊里插入字節(jié)必須等當(dāng)前幀的最后一個(gè)字節(jié)發(fā)完、FIFO清空之后才允許下一幀開(kāi)始組裝。這個(gè)原子性設(shè)計(jì)不是可選項(xiàng)而是必須項(xiàng)。另外幀與幀之間要留出足夠的間隔讓串口屏來(lái)得及解析上一幀。我實(shí)測(cè)淘晶馳系列的屏處理一幀普通指令大概需要幾毫秒到十幾毫秒不等取決于指令類(lèi)型。所以我在指令序列器里配置了一個(gè)幀間隔計(jì)數(shù)器一般取5ms到10ms如果連續(xù)快速切換頁(yè)面時(shí)發(fā)現(xiàn)偶發(fā)不響應(yīng)就把這個(gè)間隔再拉大一些。5.3 上電時(shí)序和初始化等待FPGA和串口屏有一個(gè)很容易被忽略的時(shí)序關(guān)系上電瞬間誰(shuí)先就緒。串口屏內(nèi)部有操作系統(tǒng)和GUI引擎上電后需要幾百毫秒甚至更長(zhǎng)時(shí)間初始化如果FPGA在這個(gè)窗口期就往外發(fā)指令屏根本收不到。我見(jiàn)過(guò)最頭疼的問(wèn)題就是FPGA啟動(dòng)后立刻發(fā)初始化指令配置界面結(jié)果指令被吞界面停留在默認(rèn)狀態(tài)但單獨(dú)用串口助手手動(dòng)發(fā)指令又一切正常。解決方案很簡(jiǎn)單也很粗暴FPGA的串口屏初始化流程必須在一個(gè)延時(shí)后才開(kāi)始。我在頂層狀態(tài)機(jī)里加了一個(gè)上電等待計(jì)數(shù)器一般取500ms到1秒。先讓串口屏初始化完再發(fā)第一條寫(xiě)變量或跳頁(yè)面指令。如果有條件更穩(wěn)妥的做法是先用串口助手上電觀察屏的返回信息確認(rèn)屏完全就緒后再讓FPGA發(fā)指令。如果做出來(lái)的產(chǎn)品還有休眠喚醒這類(lèi)需求那上電時(shí)序就更要謹(jǐn)慎了。串口屏進(jìn)入休眠后不代表串口還能照常收數(shù)據(jù)喚醒時(shí)序也得按手冊(cè)來(lái)不能默認(rèn)它一直在線。5.4 先用串口助手驗(yàn)證再上FPGA這個(gè)經(jīng)驗(yàn)我每次帶人都要強(qiáng)調(diào)一遍不要一上來(lái)就拿FPGA和串口屏對(duì)調(diào)先用USB轉(zhuǎn)串口模塊接電腦用串口調(diào)試助手手工發(fā)指令確認(rèn)屏的界面和協(xié)議沒(méi)問(wèn)題。這個(gè)操作能幫你把問(wèn)題分成兩個(gè)階段非常省時(shí)間。具體操作流程是第一步用串口助手連接屏幕把上位機(jī)生成的指令示例逐條發(fā)過(guò)去觀察屏幕反應(yīng)確認(rèn)地址、長(zhǎng)度、校驗(yàn)都對(duì)第二步把電腦斷開(kāi)把FPGA的UART TX引腳接到串口屏的RX引腳先用FPGA發(fā)一條最簡(jiǎn)單的指令用邏輯分析儀抓UART TX的波形和串口助手里發(fā)過(guò)的指令字節(jié)流做對(duì)比第三步確認(rèn)波形一致后再讓FPGA跑完整邏輯。這三步看起來(lái)多花了一點(diǎn)時(shí)間但實(shí)際上能省掉大量無(wú)頭緒的排查。我遇到過(guò)很多人把問(wèn)題定位在FPGA側(cè)結(jié)果是串口屏上位機(jī)工程里控件地址沒(méi)分配對(duì)也有人懷疑屏壞了結(jié)果根本原因是FPGA的引腳電平不匹配。先驗(yàn)證屏自身沒(méi)問(wèn)題是從源頭排除故障面的好習(xí)慣。6. 從這一章還能往哪走雙向交互與進(jìn)階玩法6.1 觸摸事件上報(bào)與FPGA接收解析串口屏不光是顯示設(shè)備它還是一個(gè)完整的人機(jī)交互終端。當(dāng)你用手指按下屏上的按鈕時(shí)串口屏?xí)鲃?dòng)向主機(jī)發(fā)送一條事件上報(bào)幀里面通常包含頁(yè)面ID、控件ID和事件類(lèi)型。這就意味著FPGA不光要發(fā)還要收。接收側(cè)的核心也是那個(gè)熟悉的套路狀態(tài)機(jī)檢測(cè)幀頭0xA5然后讀長(zhǎng)度字段按長(zhǎng)度收完整幀做校驗(yàn)。解析到是一個(gè)觸摸事件后根據(jù)控件ID跳轉(zhuǎn)到對(duì)應(yīng)的處理邏輯。比如一個(gè)啟動(dòng)/停止按鈕FPGA接收到按下事件后置位或清除一個(gè)控制寄存器進(jìn)而控制外設(shè)狀態(tài)。我建議接收和發(fā)送共用同一個(gè)FIFO緩沖思路只不過(guò)方向反過(guò)來(lái)。串口屏發(fā)來(lái)的字節(jié)流被UART RX模塊接收后先寫(xiě)進(jìn)接收FIFOFPGA主邏輯空閑時(shí)再去解析。這樣即使一幀事件在接收過(guò)程中被其他邏輯打斷也不會(huì)丟字節(jié)。等這一塊跑通你的FPGA系統(tǒng)就真正有了面板操作的能力而不只是單方向顯示。6.2 頁(yè)面切換、曲線顯示與后續(xù)擴(kuò)展USART_HMI協(xié)議里還有很多值得玩的功能。頁(yè)面切換指令可以讓你的FPGA設(shè)備實(shí)現(xiàn)菜單層級(jí)跳轉(zhuǎn)按下設(shè)置進(jìn)入設(shè)置頁(yè)按下返回回到主頁(yè)面。曲線控件可以把采集到的連續(xù)數(shù)據(jù)流動(dòng)態(tài)畫(huà)出來(lái)雖然刷新率不如直接驅(qū)動(dòng)屏幕高但做溫濕度歷史曲線這類(lèi)低頻波形完全夠用。圖片推送功能甚至可以把FPGA采集的圖像數(shù)據(jù)一幀一幀推到串口屏上顯示配合SD卡存儲(chǔ)可以做一個(gè)很輕量的數(shù)據(jù)記錄儀。如果項(xiàng)目到了一定規(guī)模我建議把串口屏通信封裝成一個(gè)獨(dú)立模塊對(duì)外只留寫(xiě)字符串變量寫(xiě)數(shù)值變量注冊(cè)觸摸回調(diào)這幾個(gè)接口。上層邏輯永遠(yuǎn)不直接拼幀所有協(xié)議細(xì)節(jié)都隔離在模塊內(nèi)部。這樣以后換屏幕品牌、升級(jí)協(xié)議版本只需要替換這一個(gè)模塊頂層控制邏輯一行都不用改。我現(xiàn)在項(xiàng)目里的分工基本是FPGA專(zhuān)心做信號(hào)采集和算法處理串口屏專(zhuān)心做人機(jī)界面兩者之間只有一組UART連接。這種把“界面”從“邏輯”里剝離開(kāi)的思路其實(shí)是FPGA工程成熟之后很自然的演進(jìn)方向。串口收發(fā)字符串本身只是基本功接上一個(gè)像USART_HMI串口屏這樣真正能出活的終端它才變成生產(chǎn)力。這大概是我寫(xiě)這個(gè)系列到現(xiàn)在最想留給你的東西。