ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

野火MINI开发板LCD汉字显示:FontCvtST字模生成与STM32移植全攻略

野火MINI开发板LCD汉字显示:FontCvtST字模生成与STM32移植全攻略 开年第一件事就是折腾手里那块吃灰很久的野火MINI开发板。这次想做个小界面要在LCD的文本框和按钮上显示中文结果卡在了字库生成这一步。网上资料虽然多但大多是只言片语或者直接给你一个工具让你自己玩参数设置错了也没人告诉你为什么。这篇文章就是把我这次踩坑、试错、最后跑通的全过程记录下来从FontCvtST.exe怎么用到生成的.c文件怎么和野火MINI无缝对接一次讲透。1. 项目整体设计与需求拆解1.1 嵌入式LCD显示汉字的痛点STM32这类MCU上的LCD显示本质上做的是点阵填充——屏幕上的每个像素点要么亮要么灭或者用不同颜色亮度表示。英文和数字用8x16、6x12这类ASCII字库就能搞定一个字符占几十个字节。但汉字就麻烦了常用GB2312编码里面有6763个汉字每个字至少16x16点阵全量字库下来少说也要200多KB。野火MINI板上的Flash通常也就512KB到1MB级别RAM更是紧张根本塞不下。所以实际项目里我们通常只提取自己用到的几个汉字做成一个精简字模数组再烧进代码里。这种做法的核心工具就是字模提取软件——比如标题里提到的FontCvtST.exe。我最初的想法是直接在屏幕上用串口或者按键输入汉字然后显示出来。后来发现这条路走不通汉字不能像ASCII那样直接从显示库里边查边画必须预先做字模然后让GUI库去调用。更麻烦的是不同的GUI库比如emWin、uCGUI或者自己裸写的LCD驱动对字模数据格式的要求还不一样。多少次我在屏幕上看到一条条横线竖线或者在文本框里打出来的字完全变成了乱码马赛克最后发现不是硬件有问题而是字模取模方向和LCD驱动扫描方向不一致。方向对了整个世界就对了。1.2 为什么选择FontCvtST.exe这个方案FontCvtST.exe是野火官方配套的一个字体转换工具它的全称是Font Convert Symbian Tool严格说起来这是从Symbian时代流传下来的工具但野火在ST系列板子上把它做成了标配。它能把Windows系统字体.ttf/.fon里的指定字符按照你设定的像素大小和取模方式转换成C语言数组生成一个可以直接放进Keil工程的.c文件。选择它的理由有三个第一操作门槛低。不需要装额外的仿真环境或者庞大字体工具链双击运行点几下就能出结果。第二生成格式灵活。它能直接输出标准C数组你不需要自己写脚本去解析font文件省掉了很多工作量。第三生态匹配。野火MINI的例程里本身就封装好了LCD驱动以及调用字模的接口用这个工具生成的字库配合野火的显示函数几乎不用改代码就能跑起来。当然网上也有Image2Lcd、PCtoLCD2002这样的字模工具它们也能用。但如果你手里的板子是野火并且不想在驱动层做过多改动FontCvtST.exe是最直接的一张牌。2. FontCvtST.exe工具解析与字模生成实操2.1 工具界面与核心参数详解打开FontCvtST.exe界面并不复杂上方是字体选择、大小、笔画粗细等常规选项中间有一个预览框可以敲几个字符进去看看效果左侧是生成字库时需要设置的一些格式参数。字体这一栏没什么说的选你喜欢的字体就行楷体、宋体、黑体都行但嵌入式屏幕上推荐黑体/无衬线字体笔画清晰小了也容易辨认。真正关键的是下面这几个参数这里单独拿出来说字体大小通常用16、20、24、32。字体大小决定一个汉字在屏幕上占多少像素。16x16是最省空间的基本规格适合列表文本20或24适合按钮标题32适合大标题或者需要突出显示的数字。取模方式逐行式横向取模和逐列式纵向取模。这个选项最坑必须和你的LCD驱动扫描方式对齐。后面我会详细讲。字节位序高位在前还是低位在前通常配合取模方式一起选择。生成格式这里选生成C源码.c文件而不是BIN文件因为我要把它直接编进固件里。字符集范围如果只需要显示指定的几十个汉字就自己敲进去如果需要全量字库就加载Unicode或者GB2312字库表但那样生成的.c文件会很大编译和烧录的时候要有思想准备。2.2 生成.c文件的具体操作步骤我这里以要做一个“温湿度传感器界面”为例界面上有“当前温度25.5°C”、“湿度60%RH”这样的文本还有两个按钮“刷新”、“返回”。至少需要以下字符当前温度25.5°C 湿度60%RH 刷新 返回注意半角冒号和全角冒号半角数字、全角数字这些都要提前规划好不然生成字模的时候漏掉几个后面调用时就会出现空格或者显示不出来的“空心字”。具体操作如下第一步打开FontCvtST.exe从字体列表里选择黑体大小选16。第二步在预览输入框里输入上面这一串字符建议每行一个词方便后续检查。第三步设置取模格式。这里我需要先说清楚一个原理LCD屏在驱动时像素是从左到右、从上到下逐点刷新数据位通过并口或者SPI总线一位一位送过去。如果你的字模是按横向取模即一行一行取点而驱动代码按纵向扫描方式去逐位画点那么画出来的字就是歪的、断的甚至完全不可辨认。野火MINI板上的LCD驱动一般是ILI9341这类TFT屏默认是按横向扫描的所以我在这边取模方式选择“逐行式”。字节位序选择“高位在前”。然后点“生成”或者“转换”软件会弹出一个对话框选择保存成.c文件。第四步把刚才生成的文件用一个字模查看器工具打开或者直接记事本打开确认第一行前面有const unsigned char font_16[] { 0x00, 0x18, ...};这样的结构并且数组的大小符合预期。如果每个字的字模大小是32字节16x16/8那么5个字的数组就是160字节大致可以估算有没有漏字。2.3 字模C文件内部结构分析用记事本打开生成的.c文件你会看到类似这样的结构const unsigned char code font16x16[] { /* 当 */ 0x00,0x00,0x00,0x00,0x40,0x00,0x40,0x00,0x20,0x00,0x20,0x00,... /* 前 */ 0x00,0x00,0x3F,0xFC,0x20,0x04,0x20,0x04,0x3F,0xFC,0x20,0x04,... };数组名可以自己改注释里会标明每个字模对应的汉字。每一行有16个字节对应16行每行1字节这样一个16x16汉字占32字节。这种结构你可以直接把它包含进你的显示驱动模块。如果你用的是野火自带的LCD驱动你会发现它的底层画点函数一般是这样void LCD_Fast_DrawPoint(uint16_t x, uint16_t y, uint16_t color);有了这个函数字模的本质就是告诉驱动从(x,y)开始在每个像素点上判断“这一位是1还是0”是1就画前景色是0就跳过或者画背景色。所以字模文件本身不依赖任何GUI库甚至可以自己手写一个简单的显示函数去调用它。这里顺便提一句如果你是使用emWin这类GUI库那么它要求字模数据符合它的字体结构格式比如GUI_FONT_INFO而不是简单的位图数组。碰到这种情况要么用emWin自带的Font Converter工具要么在生成的.c文件基础上再做一层封装。不过本文以野火的裸驱方案为主emWin只作提及。3. STM32野火MINI上的移植与显示实现3.1 野火MINI开发板的LCD驱动基础野火MINI通过FSMC总线或者SPI接口连接TFT-LCD。FSMC灵活的静态存储控制器方式性能高可以把LCD当成一块外部存储器来读写所以刷屏速度很快。初始化流程一般是这样开启FSMC时钟配置GPIO复用功能设置FSMC控制器时序地址建立时间、数据建立时间等需要参考具体LCD控制器的数据手册配置LCD控制器如ILI9341的显示方向、色彩格式调用初始化序列点亮背光。这部分例程野火官方例程里写得很全一般不需要大改。你只需要确认LCD屏上横、竖屏的扫描方向参数。在例子工程里lcd_init.c或是ltdc.c文件里面有关于扫描方向的宏定义比如#define USE_HORIZONTAL 1这个宏会决定画点函数中x、y坐标的映射关系进而影响字模取模时横向还是纵向扫描。如果你发现字是倒着的、旋转的先查这个宏和字模取模方向是否匹配基本就能解决。3.2 将字模.c文件接入Keil工程把上一步生成的font16x16.c文件拷贝到工程目录下然后在Keil里点“Add Existing Files to Group”把该.c文件加入工程的“APP”或者“FONT”分组里在你自己的显示模块头文件里使用extern声明这个数组extern const unsigned char font16x16[];编写一个显示汉字的函数。这里给一个实际能用的裸驱代码核心思路是定位字模在数组中的偏移位置然后逐字节逐位画点。#define FONT_WIDTH 16 #define FONT_HEIGHT 16 #define FONT_BYTES (FONT_WIDTH * FONT_HEIGHT / 8) // 在(x, y)处以color颜色显示一个汉字ch void LCD_ShowChinese(uint16_t x, uint16_t y, uint16_t ch_index, uint16_t color, uint16_t bkcolor) { uint16_t i, j; uint8_t byte_val; const unsigned char *p font16x16[ch_index * FONT_BYTES]; for (i 0; i FONT_HEIGHT; i) { byte_val p[i]; // 这一行对应一个字节 for (j 0; j 8; j) { if (byte_val (0x80 j)) { LCD_Fast_DrawPoint(x j, y i, color); } else if (bkcolor ! 0xFFFF) // 如果不需要透明背景画背景色 { LCD_Fast_DrawPoint(x j, y i, bkcolor); } } } }注意这里有个细节ch_index怎么算如果你按照固定字符顺序生成字库可以用一个映射表把汉字和下标对应起来。比如typedef struct { char *text; uint16_t index; } FontMap; const FontMap font_map[] { {当, 0}, {前, 1}, {温, 2}, {度, 3}, {, 4}, ... };调用时遍历这个映射表找到对应的索引再传入LCD_ShowChinese。实际项目里我一般会把字符串解析和字模索引查询封装在一起这样上层传进来的就是普通字符串不用自己手动计算下标。3.3 文本框场景的汉字渲染文本框这个概念在界面上一般是指一个方框区域里面显示一段文字经常需要和背景色分开。比如我做的“当前温度25.5°C”文本框底色是深蓝色字是白色。文本框的渲染分成三块边框、背景、文本。边框就是画四条线这个用LCD驱动里面现成的画线函数LCD_DrawLine即可。背景填充就是用一个颜色填满区域通常用LCD_Fill(x1, y1, x2, y2, bkcolor)。重点是文本部分。文本框内的字符串可能是英文数字汉字混合的。所以显示函数需要区分ASCII字符和汉字字符如果是ASCII字符比如数字、小数点、字母C用自带的8x16或者6x12点阵字模如果是汉字字符GB2312的两个字节查font_map映射表再用上面写的LCD_ShowChinese函数显示。一个简化版的实现思路void LCD_ShowString_TextBox(uint16_t x, uint16_t y, char *str, uint16_t fontColor, uint16_t bkColor) { while (*str) { uint8_t ch (uint8_t)*str; if (ch 0x80) { LCD_ShowChar(x, y, ch, fontColor, bkColor); x 8; // ASCII一个字符占8像素宽 str; } else { // 汉字编码两个字节先从映射表找索引 uint16_t index FindFontIndex((uint8_t*)str); LCD_ShowChinese(x, y, index, fontColor, bkColor); x 16; // 16x16字体 str 2; } } }这样处理后文本框里的内容可以是混合文本比如“T:25.5°C”这种。有一点要留意如果文本框宽度不够字串会显示到框外面去。建议在渲染前先用strlen估算字符串宽度再决定字体要不要缩小或者直接把字符串裁成两行。3.4 按钮场景的汉字渲染按钮在界面上的表现为一个矩形区域有背景色、边框、文字。按下和松开时背景色会反转或者文字颜色改变。这个场景下的汉字渲染和文本框本质是一样只是加了交互反馈逻辑。具体来说按钮的汉字渲染要注意两点第一字的水平垂直居中。不能简单地按左上角坐标开始画。需要先计算按钮区域的中心点再减去汉字串的总宽度和总高度的一半得到起始坐标。对于一组汉字总宽度 汉字个数 x 16。举例按钮宽80像素高40像素上面要显示“刷新”两个字两个16x16汉字总宽32像素那么起始x坐标 按钮x (80 - 32)/2 按钮x 24起始y坐标 按钮y (40 - 16)/2 按钮y 12。第二按下效果。主要是交换前景色和背景色或者让字的颜色变暗。这个切换在触摸响应函数里做。触摸按下时用一个变量记录按钮状态并调用LCD_ShowChinese重新绘制汉字。松开时再恢复常态颜色。这里要用到LCD快速画点函数动作要快不然会有“残影”或者闪烁感。按钮按下态的伪代码void Button_DrawPressed(Btn_TypeDef *btn) { // 1. 填充按下后的背景色 LCD_Fill(btn-x, btn-y, btn-x btn-width, btn-y btn-height, BTN_PRESS_BG); // 2. 居中绘制汉字 uint16_t start_x btn-x (btn-width - btn-text_width) / 2; uint16_t start_y btn-y (btn-height - FONT_HEIGHT) / 2; LCD_ShowString_TextBox(start_x, start_y, btn-text, BTN_PRESS_FG, BTN_PRESS_BG); }按钮状态切换的关键在于text_width要提前算好。如果按钮字串是固定的“刷新”、“返回”那直接写死就行如果是动态生成的比如“确认”变“取消”要在切换前重新计算。4. 常见问题与排查技巧实录4.1 取模方式与LCD驱动不匹配导致乱码这是我这次遇到的最头大的问题。第一次生成字库时我图省事选了“逐列式”结果在屏幕上显示出来的汉字像是被揉碎了一样完全认不出来。后来对照LCD的扫描顺序才发现ILI9341在横向显示模式下是一行一行刷的而逐列取模先取的是第一列8个点、第二列8个点……两者方向不匹配画出来的点全乱了。解决办法很简单把取模方式改成“逐行式高位在前”。但如果你用的是纵向屏竖屏显示也可能需要反过来。关键是记住这条规律LCD一行一行刷就用逐行取模LCD一列一列刷就用逐列取模。改了LCD扫描方向字库也要重新生成别偷懒。4.2 Keil工程中.c文件编码格式问题有一个极其隐蔽的坑就是FontCvtST.exe生成的.c文件默认可能是GB2312编码而Keil工程里其他文件用的是UTF-8。当你用const char *str 当前温度;这种字符串然后直接去和字模索引映射表做匹配时如果编码不对汉字字符串在内存里的字节序就不一样查映射表就会卡死。我的排查方法是在Keil里编译在调试模式下查看str指向的那几个字节是不是正常汉字编码再对照映射表。如果发现字模表里用的索引对不上两个办法一是在FontCvtST.exe生成字符列表时尽量用同样的编码方式UTF-8 vs GBK和你的源码保持一致二是干脆不依赖字符串匹配直接用Unicode编码值做索引。比如用L刷新这种宽字符串提前在工程里指定好字符编码为UTF-8这样字模表也按UTF-8生成两边都是同一种字节流就不会有匹配不上的问题。实际上我现在更推荐第二种做法把界面字符串集中放在一个头文件里用宏定义或者枚举常量来管理索引而不是写死“刷新”两个中文字符在代码里到处引用。这样编码和安全都可控。4.3 内存占用过大或生成不完全我试过把整个GB2312常用汉字表大概3000多字全部生成一个.c文件16x16编码下每个字32字节算下来也要100KB左右。野火MINI板能放下但编译时间明显变长下载到MCU里也要等更久。而且如果Flash空间不足Keil会直接报Area EROM cant fit之类的错误。更理性的做法是只生成界面用到的汉字。我第一次做温湿度界面时就是把所有可能出现的字段先列出来比如“温度”、“湿度”、“正常”、“异常”、“刷新”、“返回”、“历史”、“记录”等再去生成字模。这样生成的.c文件可能就几十个字节到几百个字节完全无压力。但如果你的产品需要支持任意输入就必须用字库芯片如GT21L16S2W或者外部Flash存储全字库启动时加载到外部RAM的映射区域这个就不属于本次讨论范围了。至少要形成这种认知MCU里不适合装全字库按需取字才合理。4.4 字体太小看不清的优化方案16x16的字在128x160这种小屏上还可以但在320x240的屏上看就觉得小。这个时候有几种优化思路第一种换更大字号的字模。重新在FontCvtST.exe里选24或者32号字体生成的字体数组相应变大。好处是省心、效果直观代价是RAM和Flash占用增加。我的做法是界面里面大标题用24x24内容文本用16x16按钮用20x20这样既突出重点又不至于每个角落都是大黑字。第二种微软雅黑这类字体虽然有更好的抗锯齿效果但MCU上直接生成灰度字模比较麻烦因为每个像素不仅要有“亮灭”信息还要有灰度级别比如4bit、8bit数组体积直接以倍数增长。除非你的LCD屏本身支持RGB565颜色且RAM足够不然还是老老实实用单色字模靠前景色和背景色搭配来做层次。第三种用颜色来区分优先级而不是单纯靠字号。这一点在文本框场景里特别好用正常温度用白色超过阈值用红色闪烁提示信息用黄色。用户扫一眼就能看到重点根本不用凑近看字大小。4.5 触摸按钮响应与字体刷新时序问题野火MINI板上的触摸方案常见有两种电阻屏XPT2046和电容屏GT9147之类。不管哪种触摸扫描是一个中断或者轮询任务。如果在触摸响应回调里直接刷新汉字频繁点击时可能出现“闪屏”或者“刷新未完成又收到新触摸”的竞态情况。我自己的习惯是触摸扫描只维护一个事件标志在主循环里统一去绘制按钮按下态和汉字。这样即使触摸中断频率再高显示刷新也不会被打断画面稳定很多。代码结构大致是while (1) { if (touch_event_flag) { touch_event_flag 0; uint16_t x get_touch_x(); uint16_t y get_touch_y(); if (is_in_button(btn_refresh, x, y)) { Button_DrawPressed(btn_refresh); delay_ms(100); // 按键消抖防止误触发 Button_DrawReleased(btn_refresh); // 执行刷新逻辑 } } }这里的delay_ms只是演示实际项目里建议用定时器或者状态机来避免阻塞。核心思想是界面刷新和触摸事件要在同一个线程模型下协调而不是在中断里做重活。5. 一点实操心得FontCvtST.exe其实只是一个“取字模”的工具真正决定汉字显示效果的是你对LCD扫描方式的理解和字模数据格式的掌握。这次在野火MINI上跑通之后我最大的收获不是“会生成font文件了”而是明白了一个道理字模方向、编码格式、渲染时机这些事情在前期如果规划好后面基本不会出幺蛾子。我个人的建议是第一次做这个功能时不要一上来就追求完美界面。先在屏幕上显示一个16x16的单个汉字确认取模方向正确、编码匹配。然后扩展到一行字符串最后再做文本框和按钮交互。每一步都验证通过再继续叠功能。这样排查起来你永远知道自己错在哪一步。另外一个提升效率的小技巧把字模.c文件和普通业务代码分开目录管理命名规则统一比如font_h16.c表示16x16高度的字体。这样你的工程结构清晰后续要换字体大小或者加字直接重新生成文件替换即可不会影响别的代码逻辑。如果你手里也有野火MINI板而且卡在汉字显示这一关希望这篇记录能帮你少走点弯路。写完这段字我去改一版24号字模把按钮标题做大一点给家里老人用起来更方便。
返回列表