1. 串口屏到底解决了我什么问题
做FPGA开发的人,十有八九会撞上同一个尴尬:板子调通了,逻辑仿真波形也验证过了,但数据看不着。早期靠LED灯拼二进制,后来用数码管,再后来学会了HDL的VGA驱动、写完字库模块——但真要显示完整一句话、一个动态数值、甚至一个简单菜单界面,单靠FPGA直接撸VGA那工作量可一点都不小。如果项目又要求量产成本可控,你可能还会发现,光是给屏幕配一颗带显存和控制器的主控芯片,整套方案已经变得很臃肿了。
串口屏这个东西,说白了就是把“显示”这件事从FPGA侧剥离出去。它内部有一颗专门处理用户界面的MCU芯片,自带Flash存储字库、图片和控件配置,通过TTL串口接收外部设备发过来的指令,就能完成页面切换、显示字符串、更新数值、改变控件属性这些操作。你不需要自己在FPGA里写字符取模、做显存管理、处理像素同步时序,只需要按协议帧格式发一串字节,屏幕上就会乖乖出现你要的内容。
市面上常见的USART_HMI串口屏,比如淘晶驰、迪文、中显光电这些品牌,基本都走“上位机配置界面 + 串口指令驱动”的思路。开发者先用厂家提供的上位机软件,拖几个控件、放几张图片,把界面布局和控件ID定义好,下载到屏里;FPGA端真正要写的,只是一个串口发送模块和一套协议帧打包逻辑。
所以这篇文章,我打算直接以一个具体的交互场景为例:FPGA作为主控,通过USART_HMI串口屏显示一段字符串,并解析按键返回给FPGA。把协议帧结构、FPGA状态机设计、代码实现和调试踩坑全走一遍。适合已经做过FPGA基础串口收发、想在项目里接块真屏幕的开发者参考。
2. 动手前先摸清USART_HMI协议帧和串口屏工作机制
2.1 USART_HMI的帧格式拆解
FPGA和串口屏通信,本质就是串口收发。难点不在于UART波特率时序,而在于“怎么把一条完整的指令准确发到屏上、怎么从屏的响应里解析出有效数据”。USART_HMI的帧格式很有代表性,搞清楚它,其他品牌串口屏也就是换皮不换骨。
以淘晶驰USART_HMI为例,协议帧标准格式如下:
帧头(0xAA) + 指令类型(0x01/0x02/0x03...) + 数据块(长度可变) + 结束符(0xCC 0x33 0xC5 0x3C)比如我要让屏幕上的文本控件t0显示字符串“FPGA OK”,实际发送的完整字节序列是:
AA 01 74 30 07 46 50 47 41 20 4F 4B CC 33 C5 3C逐字节拆开看:
AA:帧头,告诉串口屏“新指令开始了”。01:指令类型。0x01表示写文本控件内容。74 30:控件名的ASCII码,也就是“t0”两个字符。07:后面待发送字符串的长度(“FPGA OK”共7个字符)。46 50 47 41 20 4F 4B:字符串“FPGA OK”的ASCII码。CC 33 C5 3C:固定结束符,表示帧结束。
这里值得多说一嘴的是07这个长度字节。很多新手第一次发指令不成功,就是栽在长度上:要么没把长度算对,要么写的是十六进制0x07但发成了ASCII字符“07”。USART_HMI规定这一字节就是二进制长度值,不是ASCII码。后面我会专门写一个在线计算长度的经验,避免反复数错。
2.2 页码、控件ID与工程配置之间的关系
串口屏的操作对象不是“像素坐标”,而是“控件”。你在上位机软件里设计的每一个文本框、进度条、按钮,都会绑定一个唯一的控件名(比如t0、n0、b0)。屏幕的页面编号是page0、page1,控件从属于某个页面。向FPGA端发来的控制指令,本质上就是在说:去page0页面,把控件n0的数值改成123。
还有一个容易忽视的点:每个控件在系统内部对应一个变量地址。上位机软件里可以看到控件属性面板里有vscope、vaddr这些参数,默认情况下系统自动分配,也可以手动修改。FPGA侧其实不需要关心这个地址,因为协议帧里传的是控件名ASCII码,屏端会自己完成“控件名→内部变量地址”的映射。
但如果你要写的是批量数据,或者要在两个控件之间复制数值,那理解vaddr能帮你少走弯路。特别是做曲线显示、历史数据回放这类场景,有些指令格式是直接带地址的,那时候再回头翻上位机手册不迟。
2.3 全双工串口屏的交互模型:不只是单向下发
很多人的第一反应是:串口屏不就是个显示器嘛,我发给它帧,它显示内容就完事了。一直到调试才发现,屏还会往回发数据。USART_HMI支持双向通信,这一点在这个系列里是核心进阶点。
屏回发的数据主要分三类:
| 返回类型 | 触发条件 | 典型数据格式 |
|---|---|---|
| 触摸按钮按下 | 用户在屏上点了结束符为0x01的按钮控件 | AA 01 62 30 CC 33 C5 3C |
| 字符串数据回传 | 用户用输入控件提交文字 | AA 02 74 31 05 41 42 43 44 45 FF FF FF |
| 系统/变量数值回传 | 控件关联的变量发生变化且设置了回调 | AA 05 6E 30 31 32 33 CC 33 C5 3C |
这里有个很大的坑:FPGA端的UART接收模块,不能只要看到AA当成一帧开始,就傻乎乎地一直接收。它必须同时处理“帧头识别”“帧长判断”“超时判断”三个问题。因为屏回传的帧不是固定长度,如果中间某个字节丢了或者时序错乱,接收端会卡死。
我自己的做法是:接收状态机里加一个超时计数器,超过一定字节时间没收到新字节就强制回到IDLE状态,丢弃半包数据,等待下一个帧头。这套策略放到任何品牌串口屏上都适用,算是通用保底方案。
3. 串口屏与FPGA硬件连接,别让电平标准毁了调试心情
3.1 TTL电平接口是首选,RS232是给老设备的备胎
串口屏模块的接口一般有TTL、RS232、RS485三种版本。开发阶段默认买TTL就够了,因为它直接就着FPGA开发板的UART引脚电平匹配,不需要额外的电平转换芯片。
这里特别强调一下:TTL串口屏的逻辑1电平是3.3V(部分货是5V),而RS232串口屏的逻辑1是-12V、逻辑0是+12V。如果你手里是RS232版本的屏幕,直接怼到FPGA的引脚上,轻则屏幕完全不响应,重则烧毁FPGA引脚。
实测经验:淘晶驰和迪文的常见型号,基本都是3.3V TTL,和Xilinx Artix-7、Altera Cyclone IV的BANK电平能直接对接。如果是5V TTL的屏幕,建议加一个电平转换芯片,比如TXS0108E或SN74LVC4245,别赌FPGA引脚耐压。
3.2 交叉接线是基础中的基础,但也是翻车高发区
FPGA的UART_TX要接屏幕的RX,FPGA的UART_RX接屏幕的TX。这个“交叉”原则看起来简单,实际调试中我至少见身边三个人接反过。屏幕没有任何反应,查了半小时才发现是TX接TX。
另外还有几个隐藏细节:
- 共地。FPGA开发板和串口屏必须共地,否则串口电平参考点不一致,会出现偶发乱码。
- 杜邦线尽量短。115200波特率下,杜邦线超过20厘米就有风险。串口屏电流不大,但信号线过长时寄生电容会拉缓边沿,导致采样点错误。
- 屏幕供电别从FPGA的3.3V引脚直接拉太大电流。USART_HMI大部分型号屏幕背光全开时电流能到200-300mA。很多FPGA核心板的3.3V是LDO输出的,余量不大,我建议用独立的5V给屏幕供电,单独一个GPIO控制背光。
3.3 用USB转串口工具先验证屏幕指令,再动FPGA
这个步骤省不得。拿到一块新串口屏,第一步不要急着接FPGA,先做一个“屏幕自主性测试”:
- 用一根USB转TTL的串口线接屏幕。
- 电脑上打开串口调试助手,波特率设成9600或115200(与上位机配置一致)。
- 手动填入一帧完整的指令,比如显示字符串的帧。
- 点击发送,观察屏幕是否响应。
这样做的意义是:先把FPGA和屏幕两侧的“嫌疑”分开。如果屏幕用电脑发指令能正常工作,说明屏没问题;剩余问题就在FPGA侧的UART模块或接线。否则,问题大概率在屏幕配置或线序上。这个排障思路在后面会经常用到,值得养成习惯。
4. FPGA串口发送模块设计:状态机拆分到帧级别
4.1 顶层架构:一个发送模块完成“数据加载+字节发送”两个环节
用FPGA驱动串口屏,不需要什么复杂总线,核心就是一个“按帧发送”的串口发送模块。整体架构我建议拆成两层:
- 底层:一个最基础的UART TX字节发送模块(8位数据、可配波特率、发送完成拉高一个周期的脉冲)。
- 上层:一个帧级状态机,负责把要发送的字符串逐字节送入底层UART TX模块。
为什么要拆?因为帧级状态机要处理的是“整个指令帧的字节流”,而底层UART TX模块只管“一个字节怎么按位发出去”。职责分离之后,调试定位问题会非常轻松。底层的字节发送模块是纯硬时序;上层是逻辑状态控制,两边独立仿真、独立测试。
4.2 帧级发送状态机的经典结构
以“发送一段字符串到串口屏”为例,我贴上常用的状态机设计思路:
localparam IDLE = 3'd0; localparam START_TX = 3'd1; localparam WAIT_TXDN = 3'd2; localparam NEXT_BYTE = 3'd3; localparam GEN_CRC = 3'd4; // 如果协议需要校验字节再启用 localparam FINISH = 3'd5;处理流程是这样的:
- IDLE:等待外部发送使能信号(比如一个周期的高脉冲)。
- START_TX:把要发送的当前字节送入UART TX模块,同时拉高字节发送的触发信号。
- WAIT_TXDN:等待UART TX模块返回发送完成信号tx_done。
- NEXT_BYTE:字节计数加1,判断是否已经发完整个帧的所有字节。
- FINISH:发完整帧后,拉高frame_done,回到IDLE。
这里面有个工程细节:外部使能信号来的时候,帧数据可能是一个长字符串(比如20个字符),那么状态机不能只在START_TX状态把字符串首字节送出去就完事。最可靠的做法是把整个字符串存放在一个寄存器数组里,帧状态机用计数器循环取字节。如果以后字符串会变化,就做一个FIFO,把要显示的内容先写入FIFO,状态机只要负责从FIFO读出再发。这种方法在动态显示数字、浮点结果时尤其好用。
4.3 波特率的产生:分频计数器的精度问题
以50MHz系统时钟、115200波特率为例,每个bit持续时间为:
周期数 = 50_000_000 / 115200 = 434.0277...取整为434。那么在发送一个字节时,每位采样点就是系统时钟计数到434的倍数位置。误差约为0.006%,完全可以忽略。
但如果你的板子晶振是27MHz、或者波特率是250000这种非整分频的,就要算一下误差是否超过2%。UART接收端取样的容错空间大约在48%~52%位时间内,误差太大就会在高速率传输时出现偶尔丢字节。
一个稳妥的写法是:
parameter CLK_FREQ = 50_000_000; parameter BAUD_RATE = 115200; localparam BAUD_CNT = CLK_FREQ / BAUD_RATE; localparam BAUD_CNT_HALF = BAUD_CNT / 2;发送模块里用一个计数器做分频,每当计数到BAUD_CNT就产生一个移位脉冲。接收模块则用BAUD_CNT_HALF做中点采样。这个设计在FPGA入门项目里非常常见,不依赖厂商IP核,移植性极强。
4.4 发送一个字符串到串口屏的完整流程
假设需求是:FPGA上电后,自动让串口屏page0的文本控件t0显示字符串“HELLO”,显示完再发一帧让页面切到page1。
上位机软件里我们已经配置好t0控件。那么第一帧为:
AA 01 74 30 05 48 45 4C 4C 4F CC 33 C5 3C其中74 30是t0的ASCII,05是HELLO字符个数,后面是HELLO的ASCII,最后是结束符。
在FPGA代码里,我会把这些字节定义成一个参数数组:
parameter [7:0] FRAME_HELLO [0:14] = '{8'hAA, 8'h01, 8'h74, 8'h30, 8'h05, 8'h48, 8'h45, 8'h4C, 8'h4C, 8'h4F, 8'hCC, 8'h33, 8'hC5, 8'h3C};第二帧切页面的指令为:
AA 01 70 61 67 65 31 CC 33 C5 3C70 61 67 65 31是“page1”的ASCII码,页面切换指令同样是0x01,但数据内容是页面名字符串。
这里特别提醒:不同品牌串口屏的页面切换指令可能不同。淘晶驰是发一个独立的页名字符串帧;迪文老型号则用专门的指令码。所以一定以所买屏幕的手册为准。
5. 收到屏幕按键回传,FPGA如何安全解析
5.1 帧接收状态机:不要见AA就以为是帧头
FPGA接收串口屏的回传数据,比发送更容易出岔子。因为FPGA并不知道屏幕什么时候会发数据,只能被动接收。接收状态机最常见的错误是:只判断数据是否为0xAA,是就直接进入接收数据体状态。结果屏幕偶尔发来一个包含AA字节的数据帧,状态机直接就错乱了。
稳妥的接收状态机设计要考虑三件事:
- 帧头识别:收到0xAA后,继续等待指令类型字节。
- 长度判断:部分指令类型自带长度字段,先收完长度字段再决定收多少数据字节。
- 超时保护:任何状态超过N个字节时间没有新数据,强制复位到IDLE。
拿按钮按下回传的帧举例:
AA 01 62 30 CC 33 C5 3C其中62 30是按钮控件名“b0”。“CC 33 C5 3C”是结束符。屏上按钮是“返回式”的,按下时瞬间回传一个帧,松开不回传。帧里没有长度字段,解析方式就是靠结束符判断帧尾。
有的屏幕型号支持设置回传格式为“按下和释放都回传”,那就要会看到两帧几乎连续的数据。我给FPGA做解析时,会额外用一个脉冲寄存器检测帧间隔,避免两次回传被吞并成一帧。
5.2 字符串回传的解析难点:中英文长度不一样
如果屏幕端用了输入控件(比如用户要输入一个设备编号),屏回传字符串的帧格式会带上数据内容,并且字符串长度不是固定的。典型回传格式:
AA 02 74 31 05 41 42 43 44 45 FF FF FF02表示字符串回传指令,74 31是输入控件名t1,05是数据长度,后面是数据“ABCDE”,FF FF FF是结束标志。
注意:这里结束符和前面的CC 33 C5 3C不一样,是三个FF。很多FPGA初学者第一次写解析逻辑,统一按五个结束符判断,结果字符串数据里出现FF就解析失败。正确做法是把结束符也做成状态判断的一部分,而且要为“一个FF出现后,后面一个不是FF”也能兜底的情况设计状态回退逻辑。
中文场景更麻烦。如果显示屏上默认用GBK编码,一个中文汉字占两个字节,FPGA端如果按一个字符一字节来解析长度,字节数和实际内容对不上。比较推荐的做法是:FPGA端只做透传,不做编码转换。屏幕端配置输入格式时限制为ASCII或半角数字,从根源上避开中文编码的长度计算问题。
5.3 解析结果的脉冲化与跨时钟域处理
FPGA主逻辑如果是50MHz,串口波特率115200时每个字节要花近87微秒,解析状态机每一步都远小于这个时间,本身不存在跨时钟域问题。但是屏幕上按钮的按下回传是一个异步事件,如果这帧数据要和内部某个功能模块联动(比如按下按钮后启动DAC输出),不要直接把解析完成信号当作长电平拉过去。正确做法是:解析到结束符后,拉一个单周期的脉冲信号,触发后续逻辑。
如果FPGA侧还有AXI总线或自定义总线,要把这个帧解析结果作为写请求发出去,最好在脉冲域加一个边沿检测或握手信号(AXI的valid-ready机制)。我用过最简单的方案是:解析完成打一拍,上升沿作为触发;如果后续逻辑没来得及处理,就置一个标志位,主状态机轮询后再清除。
6. 一个完整Demo:FPGA控制串口屏显示温度并响应按键
6.1 功能需求和模块划分
这几天我手头正好有个小项目,用FPGA接一个DS18B20温度传感器,温度值通过USART_HMI串口屏显示出来,同时在屏幕上放了两个触摸按钮,一个切换摄氏度/华氏度显示,另一个控制蜂鸣器开关。这个项目的核心链路很有代表性:
- 传感器模块:DS18B20温度采集,输出一个BCD码或二进制温度值。
- 格式转换模块:把温度值转换成ASCII字符串,比如“+25.5”或“25.5C”。
- 串口发送模块:驱动串口屏显示。
- 串口接收模块:解析屏幕按钮回传的指令,转成按键脉冲。
- 蜂鸣器控制:按键脉冲控制输出到蜂鸣器引脚。
这个划分逻辑很清晰:数字处理和字符串生成是一层,串口协议收发是一层,外设控制是另一层。每一层单独仿真,都能快速定位问题。
6.2 数字转字符串模块的Verilog实现思路
把数字转成字符串是FPGA开发里一个高频需求,配合串口屏更有实际意义。温度值是16位有符号数,单位是0.0625度,要显示成度加小数点的形式。
第一步先算出整数部分和小数部分:
integer int_part; integer frac_part; assign int_part = temp_value / 16; // 整数部分 assign frac_part = (temp_value % 16) * 100 / 16; // 小数部分,保留两位第二步把整数部分逐位转换成ASCII码。常见的做法是:
// 假设int_part范围0~99,取十位和个位 digit_tens = int_part / 10; digit_ones = int_part % 10; ascii_tens = digit_tens + 8'h30; ascii_ones = digit_ones + 8'h30;如果温度可能为负,还要先用符号位判断,在字符串前拼接一个-字符(0x2D)。最终组成“+25.50”这样一帧字符串,再通过帧状态机统一发送。这个模块的仿真非常推荐做一下,边界条件比较多:0度、负温度、最小值等。
6.3 接收模块解析屏幕按键,控制蜂鸣器开关
屏幕端按钮b0配置成“按下回传”,FPGA接收模块回传帧解析后,判断字节序列是“b0”且帧完整,就拉高一次按键触发信号。
蜂鸣器的控制逻辑很简单:
always @(posedge clk) begin if (key_pulse) buzzer_en <= ~buzzer_en; end这里我没做消抖处理,因为USART_HMI屏幕硬件本身已经做了触摸消抖和按键去抖,回传的帧只有一次。如果用的是没有去抖的廉价按键屏,才需要在FPGA侧加20ms左右的消抖逻辑。
6.4 实际调试中的常见问题
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 屏幕完全无响应 | TX/RX接反、共地没接、波特率不一致 | 先用USB转串口线直接验证屏幕自主性 |
| 发字符串只显示第一个字符 | 长度字节算错 | 检查帧中的长度字段,确认为二进制值 |
| 乱码 | 波特率误差偏大、接线过长 | 降低波特率,缩短杜邦线,或换屏蔽线 |
| 按钮回传时好时坏 | 接收状态机没有超时保护 | 增加超时强制复位逻辑 |
| 屏幕显示正常但切页不生效 | 页名写错或未下载到屏里 | 上位机软件里确认页面名与指令一致并重新下载工程 |
7. 几个项目级避坑经验,能帮你少走三个月弯路
第一,所有协议帧里的十六进制字节,一律用Verilog的8'hXX格式书写。我碰过不止一次,有人把十六进制手动转成十进制后,再换算时出错。直接以参数数组的方式组织帧内容,代码可读性和后续改动效率都高一大截。
第二,做FPGA和串口屏联调时,一定要留一个调试端口,把发送/接收的原始字节引到逻辑分析仪或串口调试助手上。我常用的一种做法是:FPGA内部做一个环回,把RX收到的字节直接转发到PC串口显示,同时驱动屏幕。哪边出问题,一眼就能在PC端看出来。这种“FPGA透传+PC监控”的方式比只盯仿真波形高效得多。
第三,屏幕端控件属性里的“帧格式”“波特率”必须与实际代码一致。淘晶驰串口屏支持8-N-1,波特率可选择9600到921600。我建议在项目初期统一规划:调试阶段用115200,量产阶段如果信号线较长再降到9600或19200。不要小看这个决定:有的接插件质量差,高波特率下容易受干扰丢字节,降低波特率解决得干干净净。
第四,如果项目要量产,务必在屏幕端做“上电默认页面+超时返回”的逻辑。比如让屏在开机时自动显示page0,FPGA检测到屏幕握手后切到主界面。很多商用屏幕支持“上电自动加载某个页属性”,在上位机软件里配置好,就省掉了FPGA发送开机页指令的环节。
第五,关于字符串的动态更新频率,串口屏虽然能接收指令,但它内部MCU处理每条指令也是要花时间的。实测下来,同一控件连续刷新,指令间隔最好在10ms以上。如果FPGA侧需要以很高频率更新数值(比如100Hz刷新率显示电压波形),可以开多个文本控件轮换显示,或者改用指令中更高效的数值写入方式,不要频繁发整段字符串。
8. 我的建议:先把单字节收发闭环,再上串口屏
我在实际带人做FPGA项目时,一直坚持一个原则:串口屏调试不能一上来就调屏幕。先把FPGA的UART收发模块做到能用串口调试助手在PC端自发自收,再接入串口屏,成功概率会高很多。
具体来说,在接屏幕之前,至少先完成这几个实验:
- FPGA发送一段固定字符串“Hello FPGA”,PC串口助手能收到。
- PC串口助手发字符串“abc”,FPGA收到后原样回传,PC端能看到“abc”。
- FPGA解析PC发来的特定字符串,比如“LED_ON”,能控制LED引脚翻转。
这三个实验做完,说明UART收发模块稳定、时序没问题,再接入串口屏。后面如果屏幕不工作,问题基本就锁定在协议帧构造成屏端配置上了。
串口屏本身是个“能把复杂显示问题简化成协议帧问题”的工具,用好了它,FPGA项目的人机交互会变得非常轻松。后续我还会继续写这个系列:串口屏多页面交互、FPGA使用Flash存储字库、以及更高性能的双缓冲页面切换方案,到时再和大家分享更多细节。