在嵌入式设备上显示汉字,最常见的方案就是使用点阵字库。我刚入行时也试过用矢量字体,但在STM32这类资源有限的MCU上,矢量字体解析需要大量计算资源,显示速度慢到让人抓狂。点阵字库的优势在于预渲染——所有汉字都以像素矩阵的形式预先存储,显示时直接读取数据往屏幕上"戳点"就行。
举个例子,16x16的汉字点阵,每个字只需要32字节存储(1位表示1个像素)。实测在STM32F103上,这种方案刷屏速度能达到矢量字体的5倍以上。不过点阵字库也有明显缺点:字体大小固定。想要显示12pt、16pt、24pt三种字号?那就得存三套字库,非常占空间。
国内项目基本都用GBK编码,它向下兼容GB2312,同时支持繁体字和生僻字。GBK的编码规则特别有意思——每个汉字用2个字节表示,这两个字节可以看作坐标系:
这样组合能表示126×190=23,940个汉字。我在实际项目中验证过,用下面这个公式可以快速计算汉字在字库中的位置:
c复制// 计算GBK字库偏移量
uint32_t Get_GBK_Offset(uint8_t high, uint8_t low, uint8_t fontSize) {
uint16_t zone = high - 0x81; // 区号转换
uint16_t pos = (low < 0x7F) ? (low - 0x40) : (low - 0x41);
uint8_t bytesPerChar = (fontSize * fontSize) / 8;
return (zone * 190 + pos) * bytesPerChar;
}
遇到过一个坑:某些GBK字符的第二个字节可能是0xFF(比如某些特殊符号)。如果不做校验直接计算偏移量,会导致数组越界。后来我在代码里加了防护:
c复制if(high<0x81 || low<0x40 || high==0xFF || low==0xFF) {
// 返回空白字符
memset(buffer, 0, sizeof(buffer));
return;
}
推荐用PctoLCD2002这个老牌工具(虽然界面复古但很稳定)。关键配置参数:
| 参数项 | 推荐值 | 说明 |
|---|---|---|
| 取模方式 | 纵向取模,字节倒序 | 兼容大多数LCD驱动IC |
| 字体大小 | 12/16/24pt | 对应16x16/24x24点阵 |
| 输出格式 | C语言数组 | 方便直接嵌入程序 |
实测发现一个细节:电脑上的12pt字体实际对应16x16点阵,换算公式是实际点阵 = 字号×1.33。如果设置不对,生成的字符会变形。
完整GBK字库很大(16x16的字库约750KB),我通常用这三招省空间:
c复制// SPI Flash存储布局示例
#define FONT_BASE_ADDR 0x100000
typedef struct {
uint32_t magic; // 标识符 0xAA55AA55
uint32_t addr12; // 12pt字库起始地址
uint32_t addr16; // 16pt字库起始地址
uint32_t addr24; // 24pt字库起始地址
} FontHeader;
原始方案是每次显示都从Flash读取数据,在480x320屏幕上实测每秒只能刷新20个汉字。后来我改用双缓冲机制:
优化后刷新速度提升到150字/秒,核心代码如下:
c复制// 双缓冲结构体
typedef struct {
uint8_t buffer[2][2048]; // 双缓冲
uint8_t activeBuf; // 当前使用的缓冲区
uint32_t readPos; // 读取位置
uint32_t fillPos; // 填充位置
} FontCache;
// 后台填充线程
void FontCache_Fill(FontCache* cache) {
while(1) {
if(NeedMoreData(cache)) {
uint8_t* targetBuf = cache->buffer[!cache->activeBuf];
uint32_t offset = Get_GBK_Offset(gbkHigh, gbkLow, fontSize);
W25QXX_Read(targetBuf + cache->fillPos, offset, readSize);
cache->fillPos += readSize;
}
osDelay(1);
}
}
LCD直接写点阵容易出现闪烁,我总结出三个解决方案:
c复制// 使用DMA2D加速绘制(STM32H7示例)
void Draw_Fast(uint16_t x, uint16_t y, uint8_t* bitmap) {
DMA2D->CR = 0x00010000UL; // 内存到内存模式
DMA2D->FGMAR = (uint32_t)bitmap;
DMA2D->OMAR = (uint32_t)(LCD_FRAME_BUFFER + y*LCD_WIDTH + x);
DMA2D->FGOR = 0;
DMA2D->OOR = LCD_WIDTH - 16;
DMA2D->FGPFCCR = DMA2D_INPUT_RGB565;
DMA2D->NLR = (16 << 16) | 16;
DMA2D->CR |= DMA2D_CR_START;
while(DMA2D->CR & DMA2D_CR_START);
}
在资源紧张的STM32F103(仅20KB RAM)上,我这样优化:
c复制// LRU缓存实现
#define CACHE_SIZE 50
typedef struct {
uint16_t gbkCode;
uint8_t data[32]; // 16x16点阵
uint8_t lruCount;
} CharCache;
CharCache cache[CACHE_SIZE];
void Update_Cache(uint16_t gbk, uint8_t* fontData) {
int oldest = 0;
for(int i=0; i<CACHE_SIZE; i++) {
if(cache[i].gbkCode == gbk) {
cache[i].lruCount = 0;
return;
}
if(cache[i].lruCount > cache[oldest].lruCount) {
oldest = i;
}
}
// 替换最久未使用的
cache[oldest].gbkCode = gbk;
cache[oldest].lruCount = 0;
memcpy(cache[oldest].data, fontData, 32);
}
要让文字显示更美观,可以加入这些处理:
c复制// 简单抗锯齿实现
void Draw_AA(uint16_t x, uint16_t y, uint8_t* bitmap) {
for(int i=0; i<24; i++) {
for(int j=0; j<3; j++) {
uint8_t byte = bitmap[i*3 + j];
for(int k=0; k<8; k++) {
uint8_t alpha = (byte & (1<<(7-k))) ? 255 : 0;
if(alpha) {
LCD_BlendPixel(x+j*8+k, y+i, TEXT_COLOR, alpha);
}
}
}
}
}
遇到过最头疼的问题是显示乱码,通常有这些原因:
我的调试方法:
printf输出原始GBK码(如%02X %02X)当显示卡顿时,用STM32的DWT计数器测量关键函数耗时:
c复制uint32_t start, end;
start = DWT->CYCCNT;
Show_String("测试文本", x, y);
end = DWT->CYCCNT;
printf("耗时: %d cycles\n", end - start);
常见优化点:
需要显示英文、中文、特殊符号时,我这样设计:
c复制typedef void (*FontRenderer)(uint16_t x, uint16_t y, uint32_t code);
void Render_ASCII(uint16_t x, uint16_t y, uint32_t code) {
// 8x16英文字体渲染
}
void Render_GBK(uint16_t x, uint16_t y, uint32_t code) {
// 16x16中文字体渲染
}
FontRenderer Get_Renderer(uint32_t code) {
return (code < 128) ? Render_ASCII : Render_GBK;
}
结合触摸屏做菜单时,要注意:
c复制// 简单的触摸菜单项检测
uint8_t Check_Menu_Touch(MenuItem* items, uint8_t count, uint16_t x, uint16_t y) {
for(int i=0; i<count; i++) {
if(x >= items[i].x - 5 && x <= items[i].x + items[i].w + 5 &&
y >= items[i].y - 5 && y <= items[i].y + items[i].h + 5) {
// 绘制选中效果
LCD_InvertRect(items[i].x, items[i].y, items[i].w, items[i].h);
return i+1;
}
}
return 0;
}
在最近的一个智能家居项目中,这套方案成功实现了同时显示中英文菜单,并且支持触摸滑动。关键是把字库处理优化到极致,最终在STM32F429上实现了60fps的界面刷新率。最让我自豪的是,即使用最便宜的128x64 OLED屏,这套方案也能流畅显示中文菜单。