news 2026/9/28 5:54:41

STM32嵌入式设备实现阿拉伯语LCD动态显示的完整方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32嵌入式设备实现阿拉伯语LCD动态显示的完整方案

1. 项目拆解:阿拉伯语显示到底难在哪里

1.1 阿拉伯语书写的三个“反常识”规则

如果你在嵌入式设备上显示过中文,大概率会觉得“显示阿拉伯语不就是再做一套字库嘛”。真做起来你会发现,事情远没有这么简单。阿拉伯语有三个完全不同于英文和中文的书写特征,每一项对MCU这种资源受限的环境都是麻烦。

第一,从右往左排。英文是LTR(Left-to-Right),中文也基本是LTR,但阿拉伯语是RTL(Right-to-Left),第一眼看到的东西在最右边,后面的内容依次往左排。这个顺序在最终呈现阶段影响非常大,不是把字符串翻转一下就能解决的。

第二,字母有连写规则。阿拉伯文字没有“印刷体独立字”的概念,一个字母出现在单词开头、中间、末尾,长得完全不一样。比如“ب”(baa),孤立形式是“ب”,但在单词开头是“بـ”,在单词中间是“ـبـ”,在单词末尾又是“ـب”。同一个逻辑字符,会根据它在词里的位置变化出四种字形。想在LCD上显示对方一眼能看懂的阿拉伯语,必须处理这四种形态。

第三,某些字母根本不跟后面的字母连接。阿拉伯语里有几个字母,比如“ا”、“د”、“ذ”、“ر”、“ز”、“و”,它们只跟左边的字符连接,不跟右边的字符连接。也就是说它们后面如果还有别的字母,那后面的字母就要回到“孤立形态”或者从“开头形态”重新开始。这条规则直接决定了你的字形切换算法里需要一张“非连接字符表”。

这三个特征叠加在一起,用传统的中文字库方案完全解决不了。中文只要一个字符对应一个字形,然后按序排就行,阿拉伯语必须动态判断每个字符在当前上下文里该用哪种字形,还要从右往左绘制。

1.2 为什么不能直接套用中文/英文字库

很多人最开始的想法是“用大容量Flash做全字库”。听起来可行,实际一算就露馅了。

我们先按中文的做法估算:如果拿26个英文字母做字库,每个字符16x16点阵,结束。但阿拉伯语基础字母28个,每一个平均要准备4种字形,实际上又是112个字模,再加上阿拉伯语数字、标点、常用变音符号,很快就超过200个字模。虽然总数并不夸张,但问题在于你没法像中文GB2312那样“放一个全字库就完事”,因为同一个字母不同形态的字模,必须在运行时根据上下文切换,你的程序里必须有一张“字形选择表”和一套“连写判断逻辑”。

另外,市面上的成熟GUI库,比如lvgl、TouchGFX,对阿拉伯语的支持程度参差不齐。有的只解决了“从右往左排版”,没解决“字母形态切换”;有的在PC模拟器上表现正常,一交叉编译到单片机就爆内存。与其在第三方库里做各种patch,不如自己写一套只依赖“点阵字模+基础绘图函数”的轻量渲染逻辑,20KB以内的Flash就能跑起来。

1.3 技术路线选型:图形LCD + 预渲染字形

我给这个项目的定调是:图形LCD + 预渲染点阵字形 + 轻量连接规则引擎。

图形LCD指的是TFT或OLED这类有独立像素寻址能力的屏幕,比如常见的ST7735、ILI9341、SSD1306。不能用HD44780那种字符型LCD,字符型LCD内部固化了ASCII字模,压根没法显示阿拉伯字母。

预渲染字形是相对另一种思路“运行时刻字库”而言的。有些方案会把字体文件存到SD卡,运行时用FreeType去渲染,这在带MMU的Linux单板机上没问题,但在普通MCU上想都不要想。正确做法是提前把需要的阿拉伯字形转成C语言数组,固化到Flash里,运行时直接查表。

然后就是核心的“连接规则引擎”。它接收一段Unicode码点数组,逐个字符判断它该显示成孤立、开头、中间、结尾四种形态里的哪一种,最后再按RTL顺序把对应字模画到LCD上。整条链路清晰,调试每一个环节都能独立验证。

新手问得最多的一个问题:这个方案能显示英文和数字吗?能。ASCII字符的字模一起放进去,渲染函数里对RTL做特殊处理,对LTR片段按正常从左到右画就行,纯阿拉伯语界面完全够用。

2. 硬件准备与开发环境

2.1 低成本硬件组合:STM32 + 1.8寸TFT

优先推荐一套非常成熟、资料也多到泛滥的组合:STM32F103C8T6 + 1.8寸TFT LCD(ST7735驱动,分辨率128x160)。这套硬件在某宝上的散件价格常年压得很低,而且每个外设都有现成驱动参考。如果你手头有STM32F411、GD32、ESP32,逻辑也是一样的,只是SPI初始化和引脚配置不同。

为什么选ST7735而不是ILI9341?因为128x160的分辨率对于阿拉伯语动态显示来说刚刚好,字模用16x16点阵,一行能显示8到10个阿拉伯字符,做菜单、状态栏、滚动通知足够。ST7735支持SPI接口,4根信号线就能驱动,MCU压力小。ILI9341 240x320当然也可以,但全屏刷新需要更大内存和更高总线时钟,对新手HAL库默认配置可能体验不到“流畅”。

屏幕驱动需要注意一个细节:ST7735有“窗口”的概念,往某个矩形区域写像素前,要先发一个Set Address Window命令锁定区域。阿拉伯语RTL渲染时,我们是从右往左画字符的,用窗口功能可以只刷新某个矩形区域,避免整屏重绘,这个后面动态显示会用到。

2.2 开发环境与代码烧录

开发环境我推荐两条路。第一是VSCode + Embedded插件 + arm-none-eabi-gcc,配合STM32CubeMX生成HAL初始化代码,编译、烧录、调试闭环非常顺畅。第二是JetBrains CLion,它对CMake工程支持更好,适合已经有了CMake习惯的人。但不管你用哪个IDE,底层编译器都是arm-none-eabi-gcc,所以这套工程本质上是一个标准GCC + STM32 HAL工程,不存在“换了IDE代码就跑不了”的问题。

烧录调试用ST-Link V2就够了,OpenOCD或STM32CubeProgrammer都能刷。个别新手卡在“编译通过但屏幕不亮”的环节,大多数情况是SPI引脚定义写错、复位引脚时序不对,或者手里屏幕是3.3V逻辑但接了5V电源。ST7735屏幕模块通常有个背光控制引脚,别把它跟系统电源直接短接而不做任何限流,容易把背光LED搞坏。

2.3 字模生成工具与字库组织

字模是阿拉伯语显示系统的地基。我用的工具是LCD Image Converter,支持直接把PC上的ttf字体按指定像素大小转换成C数组。你要做的只有一步:把ttf字体里需要的阿拉伯语字符先用Unicode输入法打出来,生成一张张独立的位图,再导入工具导出成数组。

另外一个常用工具是PCtoLCD2002,更老牌,针对点阵LCD做了很多字模格式选项,比如“列行式”“行列式”“阴码/阳码”之类的设置。这里有个值得强调的点:导出时要选择“横向取模”“高位在前”,并且字模数据必须和LCD的像素扫描方向一致。前后方向反了,字母会直接左右颠倒,看起来就像镜像字。

字库组织上,我用一个二维数组存所有字形,数组索引就是Glyph ID,上一节提到的“字形查找表”最终返回的也是这个Glyph ID。程序里通过一个ArabicCharEntry结构体,把Unicode码点、四种形态对应的Glyph ID关联起来。这个表是静态常量,放在Flash里,不占RAM。

3. 5步实现阿拉伯语LCD动态显示

3.1 第1步:建立字符表与字形索引

这一步的目标是把“Unicode逻辑字符”翻译成“具体字形ID”。

我先把所有要用到的阿拉伯基础字母,按28个字母的Unicode码点整理一遍。比如“ب”的Unicode码点就是0x0628,“ت”是0x062A,“س”是0x0633。在代码里我直接用一个uint16_t数组存字符串,每个元素是一个码点,省去UTF-8解析的开销。因为MCU内存小,用UTF-8存储一大段字符串虽然省空间,但取第N个字符要逐字节解析,既慢又容易出错。全用uint16_t数组,下标访问就是码点,逻辑最直接。

接着定义字形表。每个阿拉伯字符在“孤立、开头、中间、结尾”四种形态下,都要对应一个Glyph ID。我用下面的结构体组织:

typedef struct { uint16_t uni; // Unicode码点 uint16_t g[4]; // 孤立 / 开头 / 中间 / 结尾 } ArabicCharEntry;

比如“ب”这四项分别指向四个不同字模的Glyph ID。有些字母的孤立和结尾形态长得一样,比如“ا”,那就在字模表里让两份指向同一个数组元素。这种复用能省不少Flash,看起来是小事,28个字母全算下来能省约10%的字形空间。

这一步完成后,你手上应该有一张“码点表”和一张“字模表”。码点表负责映射,字模表负责存储点阵数据,两者分开,后面扩展新字符时不用动连接规则代码。

3.2 第2步:实现连接形态转换

连接判断是全网资料最少的环节。很多项目做到这里就放弃了,因为网上能找到的代码要么只处理英文,要么用完整ICU库杀鸡用牛刀。

我抽出来的核心逻辑并不长,关键就两点:每个字符能否“向左连笔”,以及能否“向右连笔”。如果左右两个邻居都愿意连接,当前字符就用中间形态;如果只有左边有邻居而且它愿意接受连接,当前字符就用自己的开头形态;如果只有右边有邻居而且它愿意接受连接,当前字符就用结尾形态;两边都不满足,就用孤立形态。

判断“愿不愿意”需要一张表。阿拉伯语里,多数基础字母是双向连接,但“ا د ذ ر ز و”这类字母是单向连接,它们不能向左边的下一个字符伸出笔触,所以它们后面跟的字符会断连。另外,“ء”这个Hamza字母几乎不参与连写,直接按孤立处理。

我给出的简化判断函数如下:

static bool can_join_left(uint16_t ch) { // 这些字符不向左连笔 switch (ch) { case 0x0621: // ء case 0x0627: // ا case 0x062F: // د case 0x0630: // ذ case 0x0631: // ر case 0x0632: // ز case 0x0648: // و return false; default: return (ch >= 0x0622 && ch <= 0x064A); } } static bool can_join_right(uint16_t ch) { // 所有阿拉伯基础字符都允许向右连笔 return (ch >= 0x0621 && ch <= 0x064A); }

注意这个写法是“简化版”,它把“لا”这种复合字符和部分扩展字符排除在外了,但对90%的嵌入式界面文案够用。接着是形态判定函数:

typedef enum { GLYPH_ISOLATED = 0, GLYPH_INITIAL = 1, GLYPH_MEDIAL = 2, GLYPH_FINAL = 3 } GlyphForm; GlyphForm get_arabic_form(const uint16_t *str, int i, int len) { bool join_left = (i + 1 < len) && can_join_left(str[i]) && can_join_right(str[i + 1]); bool join_right = (i > 0) && can_join_right(str[i]) && can_join_left(str[i - 1]); if (join_left && join_right) return GLYPH_MEDIAL; if (join_left) return GLYPH_INITIAL; if (join_right) return GLYPH_FINAL; return GLYPH_ISOLATED; }

这个函数是整个渲染引擎里最重要的一环。至于为什么右边邻居的判断要写成can_join_right(str[i+1]),可以这样理解:从右往左书写时,逻辑上的后一个字符实际出现在当前字符的左边,它必须“愿意向右伸出笔触”,你跟它才能连得起来。我早期直接把顺序写反,结果所有字母全断开,屏幕上看就是一堆独立字母拼在一起,完全不像正经阿拉伯文。

3.3 第3步:RTL逆向排版与坐标计算

形态切换搞定后,排版就简单一半了。阿拉伯语是RTL,第一个逻辑字符显示在最右边,后面的字符依次往左。

我直接写一个从头到尾的渲染函数,遍历字符串索引0到len-1,每拿到一个字符的字形,先检查它的像素宽度,然后从右侧起始坐标开始向左递减:

void lcd_put_arabic_string(uint16_t x_right, uint16_t y, const uint16_t *str, uint16_t len, uint16_t color, uint16_t bg) { uint16_t cur_x = x_right; for (int i = 0; i < len; i++) { uint16_t ch = str[i]; if (ch == 0x20) { // 空格 cur_x -= 8; continue; } GlyphForm form = get_arabic_form(str, i, len); uint16_t glyph = lookup_glyph(ch, form); uint8_t w = get_glyph_width(glyph); cur_x -= w; draw_glyph(glyph, cur_x, y, color, bg); } }

调用时,x_right传屏幕的右边界,比如128。第一个字符画在128-width的位置,第二个字符画在它左边,以此类推。这里有个很容易踩的坑:不要把字符串头尾翻转后从左往右画。表面看结果可能差不多,但一旦遇到连写形态,翻转会把连接判断彻底弄乱,因为连接判断依赖原始逻辑顺序。

3.4 第4步:LCD驱动适配与绘制

字模有了,排版顺序有了,接下来就是把字模点阵准确刷到屏幕上。

以ST7735为例,最底层的绘制函数可以抽象成两个:一个画单色点,一个填充矩形。点阵字模本质上是“把有像素的点用前景色画出来,把没像素的点用背景色画出来”。我建议不要一个点一个点地调用画点函数,那样会慢到肉眼可见。正确做法是用ST7735的窗口命令,先把字模所在矩形区域一次性锁住,然后连续发送这个矩形内所有像素的颜色数据。

static void draw_glyph(uint16_t glyph, uint16_t x, uint16_t y, uint16_t color, uint16_t bg) { const uint8_t *bitmap = glyph_bitmap[glyph]; uint8_t w = glyph_width[glyph]; uint8_t h = 16; lcd_set_window(x, y, x + w - 1, y + h - 1); for (int row = 0; row < h; row++) { for (int col = 0; col < w; col++) { uint8_t bit = (bitmap[row * ((w + 7) / 8)] >> (7 - (col % 8))) & 0x01; lcd_push_data(bit ? color : bg); } } }

lcd_set_window对应ST7735的CASET/RASET命令,lcd_push_data用SPI连续发16位RGB565颜色。这段代码对ST7735和ILI9341基本通用,差别只在一开始的驱动初始化函数,不影响渲染逻辑。

字模宽度不建议固定成16,因为阿拉伯字母高矮胖瘦差很多,比如“ر”特别矮,“م”特别宽。如果强制所有字模等宽,连写虽然能连上,但整个单词单词之间的字距会很傻。所以我在字模表旁边再维护一个宽度表,每个字形有自己真实的宽度。

3.5 第5步:动态刷新与整机联调

静态显示跑通后,动态刷新才是平时遇得最多的需求:时间走秒、温度变化、滚动菜单。

动态刷新的核心原则是“哪里变了刷哪里”。比如屏幕最下方显示一行阿拉伯语日期,秒数每秒变化,如果每秒都把整屏160x128像素全量重绘,SPI刷新会占用大量时间,还会明显闪烁。正确做法是先用背景色填充旧数字所在的矩形,再在新位置绘制新数字。如果数字颜色和背景色、字体像素状态都处理得当,这个操作几乎看不出闪烁。

如果你的MCU有充裕的RAM,另一个思路是开一块与屏幕等大的帧缓冲,所有绘制操作先写进缓冲区,最后一次性把整块缓冲提交到LCD。128x160x2字节,大约40KB,对F103系列来说偏大,但对STM32F411或ESP32完全没问题。有帧缓冲时,动态刷新天然不闪,因为人可以先把“旧内容擦除+新内容写入”在缓冲里全部完成,才安排一次DMA刷新屏幕,屏幕上不会出现中间态。

我当时在F103上做的是局部刷新,没有帧缓冲,靠的是把需要变化的区域精确控制在几十个像素宽的小范围内。只要不每次都整屏重绘,效果已经非常稳定。

4. 完整代码示例与核心实现

4.1 数据结构与字形查找表

下面给出一段可直接参考的工程代码骨架,硬件驱动层假设已经有lcd_set_window和lcd_push_data,这两段函数在ST7735驱动里属于标配。

字符映射表以“ا、ب、د、ر”几个典型字母为例,完整28个字母按同样格式补全即可。字模位图数组限于篇幅,我用注释占位,实际位置放LCD Image Converter导出的数组:

#define AR_MAP_SIZE 4 typedef struct { uint16_t uni; uint16_t g[4]; } ArabicCharEntry; // 字形索引定义,需要和 glyph_bitmap 数组顺序一致 enum { GLY_ALEF_ISOL = 0, GLY_ALEF_FIN = 0, GLY_ALEF_INIT = 1, GLY_ALEF_MED = 1, GLY_BAA_ISOL = 2, GLY_BAA_INIT = 3, GLY_BAA_MED = 4, GLY_BAA_FIN = 5, // ... }; const ArabicCharEntry arabic_map[AR_MAP_SIZE] = { {0x0627, {GLY_ALEF_ISOL, GLY_ALEF_INIT, GLY_ALEF_MED, GLY_ALEF_FIN}}, {0x0628, {GLY_BAA_ISOL, GLY_BAA_INIT, GLY_BAA_MED, GLY_BAA_FIN}}, // 其他字母按序补全 }; // 字模数组,每项为一个16x16点阵,多少像素按实际尺寸 static const uint8_t glyph_bitmap[][32] = { /* 0: ا 孤立形态 */ {0x00,0x00,0x00,0x00, /* ... */}, /* 1: ا 开头形态 */ {0x00,0x00,0x00,0x00, /* ... */}, /* 2: ب 孤立形态 */ {0x00,0x00,0x00,0x00, /* ... */}, // ... }; static const uint8_t glyph_width[] = { 16, 16, 12, 12, 16, 8, }; uint16_t lookup_glyph(uint16_t ch, GlyphForm form) { for (int i = 0; i < AR_MAP_SIZE; i++) { if (arabic_map[i].uni == ch) return arabic_map[i].g[form]; } return 0; // 找不到就画空格 } uint8_t get_glyph_width(uint16_t glyph) { return glyph_width[glyph]; }

这个结构解决的事情很纯粹:给定一个Unicode码点和一种形态,返回一个可以直接从字模数组取数据的索引。建议把glyph_width这个数组和glyph_bitmap放在同一个源文件里,免得改字模时漏改宽度表。

4.2 连接规则判定核心代码

连接判定和字形查找在上一节已经露过脸,这里把完整版凑齐。重点放在两个判断函数和形态确定函数之间的配合上。为了加深理解,我加了一段注释:i-1是右侧邻居,i+1是左侧邻居。

static bool can_join_left(uint16_t ch) { switch (ch) { case 0x0621: case 0x0627: case 0x062F: case 0x0630: case 0x0631: case 0x0632: case 0x0648: return false; default: return (ch >= 0x0622 && ch <= 0x064A); } } static bool can_join_right(uint16_t ch) { return (ch >= 0x0621 && ch <= 0x064A); } GlyphForm get_arabic_form(const uint16_t *str, int i, int len) { bool join_left = (i + 1 < len) && can_join_left(str[i]) && can_join_right(str[i + 1]); bool join_right = (i > 0) && can_join_right(str[i]) && can_join_left(str[i - 1]); if (join_left && join_right) return GLYPH_MEDIAL; if (join_left) return GLYPH_INITIAL; if (join_right) return GLYPH_FINAL; return GLYPH_ISOLATED; }

判断逻辑用一句话概括:两个字符要连笔,必须是左字符“愿意向右伸笔”并且右字符“愿意向左接笔”,两边缺一不可。很多人在这里搞混,把“左右邻居”理解反了,结果就是所有代码看起来都对,屏幕上的字却全断着。

4.3 从右到左渲染函数完整代码

有了前面所有工具函数,渲染就只剩坐标更新和逐字形绘制了。完整版处理了空格和超出屏幕边界的问题:

void lcd_put_arabic_string(uint16_t x_right, uint16_t y, const uint16_t *str, uint16_t len, uint16_t color, uint16_t bg) { uint16_t cur_x = x_right; for (int i = 0; i < len; i++) { uint16_t ch = str[i]; if (ch == 0x20) { cur_x -= 8; continue; } GlyphForm form = get_arabic_form(str, i, len); uint16_t glyph = lookup_glyph(ch, form); uint8_t w = get_glyph_width(glyph); if (cur_x < w) break; // 左边空间不足,不再绘制 cur_x -= w; draw_glyph(glyph, cur_x, y, color, bg); } }

这个函数的入口参数x_right表示整段文本右边界的横坐标。比如想在128x160的屏幕上留出8像素右边距,就传120。第一个逻辑字符从最右侧开始,最后一个逻辑字符落在最左侧,肉眼看到的就是正常阿拉伯语阅读顺序。

4.4 动态显示:秒级刷新示例

动态刷新的核心是“每次只重绘变化的那一小块”。下面的例子模拟阿拉伯语时间界面:“الوقت”表示“时间”,后面跟两个阿拉伯语-印度数字显示小时和分钟。

// 阿拉伯语词“الوقت” static const uint16_t text_time[] = { 0x0627, 0x0644, 0x0648, 0x0642, 0x062A }; #define TEXT_TIME_LEN 5 static void refresh_time_display(uint8_t hour, uint8_t minute) { uint16_t x_time = 124; // 右侧坐标 uint16_t x_digit = 40; // 数字区域左侧坐标 // 1. 重绘固定阿拉伯语文本 lcd_put_arabic_string(x_time, 10, text_time, TEXT_TIME_LEN, 0xFFFF, 0x0000); // 2. 清除旧数字区域,宽40,高16 lcd_fill_rect(x_digit, 10, 40, 16, 0x0000); // 3. 绘制两位数,数字按从左往右排 char buf[8]; snprintf(buf, sizeof(buf), "%02d:%02d", hour, minute); lcd_put_string_ltr(x_digit, 10, buf, 0xFFFF, 0x0000); }

这里的lcd_fill_rect负责用背景色填充旧数字区域。正是因为阿拉伯语文本字形固定不变,所以只需要在开机时或者文本变化时节省一次刷新,数字区域每次单独处理,显示效果稳定也不卡顿。

5. 常见问题与调试实录

5.1 字母全部断开,连笔效果出不来

这个问题十有八九出在连接形态判断上。我用“سلام”这个测试词,它是一个标准连写词。如果你屏幕上显示出来的是四个互不相连的独立字母,请先检查三个地方。

第一,确认你的字符映射表里四个形态的Glyph ID不是全填成孤立形态的ID。很多人在做字模表时偷懒,四种形态全指向同一个字模,那显示出来当然全是孤立形态。第二,确认can_join_left的判断范围没把常用连接字母误判为不连接。第三,确认get_arabic_form里左右邻居索引方向正确,这个方向我第3.2节重点强调过,一旦反了,几乎所有多字母词都会断。

5.2 整串字方向反了,像镜像字

如果你看到阿拉伯语字符本身是对的,但整行顺序从左往右排,那就是渲染函数里的坐标逻辑写反了。我调试时惯用的检查技巧是:取一个只有两个字符的测试词,比如“بيت”(意为房子),先手动算一下每个字符应该落在什么坐标,然后用调试器看运行时cur_x的变化,对比差异很快就能定位。

另外一个容易跟它混淆的问题是字模取模方向反了,比如字母“ب”的下方圆点跑到了上方。这种属于字模生成工具的取模设置问题,需要回到LCD Image Converter里调整像素扫描方向,不是改代码能救回来的。

5.3 字模大小/Flash容量不足

28个阿拉伯字母,按平均每个形态32字节算,基础字库大概3.5KB,加上映射表、控制逻辑、LCD驱动,整个工程在STM32F103C8T6的64KB Flash里绰绰有余。如果你用了全字库手段,把几百个字符全塞进去,Flash才会吃紧。

应对方法有三个:把不再需要的调试串口日志去掉;把频率不高的字形从Flash搬到外部SPI NOR Flash,运行时按需读取;把不必要的GUI库裁剪掉。除非你手头芯片Flash低于32KB,否则前两步基本够用。

5.4 动态刷新闪烁、花屏

花屏八成和SPI时序有关,闪烁则八成和刷新策略有关。SPI时序问题一般表现为随机出现彩色噪点、字符残缺,这时候先降低SPI时钟频率,比如从18MHz降到9MHz,看是否恢复。很多ST7735模块走杜邦线连接,高频信号质量很差,降频是最立竿见影的手段。

闪烁问题用局部刷新解决。具体做法是先计算出“变更前区域”和“变更后区域”的并集,只在这个矩形范围内调用lcd_set_window重刷。如果整屏更新不可避免,建议改用DMA+帧缓冲方案,把绘制和刷新时间拆开,屏幕稳定性会好很多。

5.5 特殊符号、变音符号如何处理

阿拉伯语文本里常出现“ً ٌ ٍ َ ُ ِ ّ ْ”这类变音符号,它们叠加在字母上方或下方,不是独立的字符。MCU上没有成熟方案自动处理,最实用的策略是业务界面里不用这些符号。产品文案设计阶段就跟内容方约法三章:菜单、状态栏、按钮、通知栏一律不带变音符号。这样既省字模,也避免符号越界、重叠的麻烦。

如果确实要显示变音符号,就得把字形数据升级成带高度的组合结构,比如每个基础字形额外预留6到8像素的上方空间,让变音符号绘制在同一个区域内。这个工作量和存储开销都会明显上升,做之前先想清楚需求是不是真需要。

6. 这个方案的后续扩展

6.1 全字库与外部存储

如果产品还要显示阿拉伯语长文本,比如新闻摘要、帮助页面、多语言说明书,那建议把字模从程序Flash挪到外置SPI NOR Flash。字模索引方式和第3.1节完全一致,只是查找字模时多一步Flash读取。用内存映射方式读取SPI NOR Flash的MCU会很方便,不支持内存映射的话,也可以每次读取一页然后缓存到RAM里。

外部字库的文件格式建议用二进制顺序排列,头部放一个字符映射表,后面跟着字形数据。这样即使以后PC端内容有增删,也只需要更新这个外部文件,不需要重新烧录整个固件。

6.2 混排数字与双向文本

阿拉伯语界面的数字分两种:一种是阿拉伯语-印度数字“٠١٢٣٤٥٦٧٨٩”,另一种是西阿拉伯数字“0123456789”。很多产品经理最后会选西阿拉伯数字,因为它对全球用户更熟悉。

问题来了:如果一行文本是“الوقت 12:30”,其中“12:30”这段数字内部是LTR顺序,但整体又处于RTL上下文中。我的建议是不要试图在一个纯RTL渲染函数里解决双向文本,而是把字符串按文本方向拆成几个片段:阿拉伯语片段用lcd_put_arabic_string,数字片段用lcd_put_string_ltr,中间通过坐标计算拼接。虽然代码看着啰嗦一点,但逻辑可控、调试方便。

6.3 图形库对接与最终建议

如果你用的是lvgl这类图形库,新版lvgl其实已经加入了阿拉伯语连写处理接口,支持通过三个回调函数完成字形选择和文本整形。你可以把我前面实现的get_arabic_form和lookup_glyph对接进去,替代lvgl默认的拉丁字符处理流程。对接时注意lvgl的字库文件同样可以用LCD Image Converter生成,但格式要转换成lvgl需要的字体结构体,不能用普通裸数直接丢进去。

最后给后来者一句经验:不要一上来就追求“所有阿拉伯语文本都能完美显示”。先把菜单级、状态栏级的短文本做扎实,跑通“断开、乱序、闪屏”这三个最要命的问题,再考虑长篇内容。嵌入式项目里,稳妥永远是第一位的。我最初在这个方案上调试连接规则也花了整整两个晚上,但只要把“左连接/右连接”这个模型想清楚,后面所有问题都是水到渠成的事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 5:54:34

响应式网站技术新手入门:3个档位报价单拆解,避开改需求拖一周的坑

响应式网站技术新手入门:3个档位报价单拆解,避开改需求拖一周的坑 上周刚跟一家做建材出口的公司谈完,对方老板拿着另一家公司的报价单,指着上面那行“响应式适配费:3000元”问我:“为什么我明明只要改个手机端按钮颜色,对方说技术架构要重构,拖了一周还没动静?这钱到底花在哪了?”…

作者头像 李华
网站建设 2026/9/28 5:54:28

5年老兵揭秘:推广策划书模板从零搭建避坑指南

5年老兵揭秘:推广策划书模板从零搭建避坑指南 域名服务器搞不懂?别慌,这行十年,我见过太多老板被这一关卡住。 其实核心就两点:服务器买错配置,域名没绑解析。 今天把推广策划书模板从零搭建的账算清楚,让你心里有底。 方案类型与适用场景…

作者头像 李华
网站建设 2026/9/28 5:54:21

Keyboard Shortcuts 配置 TaoToken:settings.json 骨架与验证动作

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 5:54:23

网站怎样做301跳转图解步骤避坑指南

网站怎样做301跳转图解步骤避坑指南 找建站公司怕被坑高价?别慌,其实很多“高深”的技术问题,拆开看就是几行代码的事。比如 网站怎样做301跳转 ,这不仅是SEO的基础,更是保护你品牌权重的关键动作。很多小白一听到“301”就头大,觉得需要找专家花大价钱处理,结果交出去一堆费用,最后发现就是改个配置…

作者头像 李华
网站建设 2026/9/28 5:53:29

南宁网站开发价格全解析:备案图解步骤避坑指南

南宁网站开发价格全解析:备案图解步骤避坑指南 很多老板盯着南宁网站开发价格表,心里却慌得一批,不是怕贵,是怕备案流程一头雾水。明明预算定了,域名服务器也买了,结果卡在工信部备案环节,网站迟迟打不开,客户流量全跑隔壁去了。别急,今天不整虚的,直接上 图解步骤…

作者头像 李华
网站建设 2026/9/28 5:53:20

Unity工程Artifacts文件夹膨胀:原因、清理与根治方案

写这篇东西的起因&#xff0c;是我连续在三个Unity项目里都被同一个问题逼疯了&#xff1a;项目工程越滚越大&#xff0c;同步备份的时候居然要拷好几十个G&#xff0c;一开始我还以为是场景贴图太多&#xff0c;顺手用资源分析工具扫了一遍&#xff0c;结果发现罪魁祸首是一个…

作者头像 李华