在STM32项目中使用LCD显示汉字时,最直接的方法是把汉字字模直接存储在MCU的Flash中。比如要显示"欢迎"两个字,我们可以用取模软件生成这两个字的点阵数据,然后硬编码到程序里。这种方法在显示固定标语时确实可行,但存在几个致命问题:
我在一个智能家居项目中就踩过这个坑。最初把所有界面文字都硬编码在代码里,结果产品经理临时要求增加多语言支持时,不得不重写整个显示模块。后来改用W25Q64存储字库后,不仅支持了中英文切换,还能通过SPI DMA实现丝滑的菜单动画效果。
W25Q64是Winbond推出的64M-bit(8MB)串行Flash,通过SPI接口与STM32通信。典型接线方式如下:
code复制W25Q64 STM32
CS → PA4(SPI1_NSS)
DO → PA6(SPI1_MISO)
WP → 3.3V
DI → PA7(SPI1_MOSI)
CLK → PA5(SPI1_SCK)
HOLD → 3.3V
VCC → 3.3V
GND → GND
建议在PCB布局时:
使用PC端工具生成GBK字库时,有几个关键参数需要注意:
这里分享一个实用技巧:用Python可以批量处理字库文件:
python复制# 字库文件校验工具
def check_font_file(filename):
with open(filename, 'rb') as f:
data = f.read()
print(f"文件大小: {len(data)//1024}KB")
print(f"起始标志: {hex(data[0])}{hex(data[1])}")
print(f"结束标志: {hex(data[-2])}{hex(data[-1])}")
# 示例:检查生成的GBK16.bin
check_font_file("GBK16.bin")
在未使用DMA时,读取一个16x16汉字(32字节)的流程如下:
实测在72MHz系统时钟下,读取一个汉字需要约56us,这在显示长文本时会出现明显卡顿。
以STM32F103的SPI1为例,DMA配置代码如下:
c复制// SPI1 RX DMA配置(通道2)
DMA_InitTypeDef DMA_InitStructure;
DMA_DeInit(DMA1_Channel2);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_buffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = buffer_size;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel2, &DMA_InitStructure);
// 启用DMA中断
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
DMA_ITConfig(DMA1_Channel2, DMA_IT_TC, ENABLE);
使用逻辑分析仪抓取波形,得到不同读取方式的时间消耗:
| 读取方式 | 32字节耗时 | CPU占用率 |
|---|---|---|
| 传统SPI | 56μs | 100% |
| SPI+DMA | 12μs | <5% |
| SPI+DMA+双缓冲 | 8μs | <2% |
双缓冲技术的实现要点:
要实现类似车站信息的横向滚动效果,需要处理以下几个关键点:
c复制// 滚动字幕核心逻辑
void TIM3_IRQHandler(void) {
if(TIM_GetITStatus(TIM3, TIM_IT_Update)) {
static int offset = 0;
LCD_ClearLine(SCROLL_LINE); // 清空显示行
// 绘制当前帧
for(int i=0; i<VISIBLE_CHARS; i++){
int char_pos = (offset/8) + i;
if(char_pos < text_length){
LCD_DisplayChineseDMA(8*i - offset%8, LINE_Y,
&text_buffer[char_pos*2], RED);
}
}
offset = (offset + 2) % (text_length*8);
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
}
}
在嵌入式菜单系统中,汉字显示要解决两个特殊问题:
c复制// 多语言支持示例
uint32_t GetFontAddress(uint8_t lang, uint16_t font_code){
const uint32_t lang_offset[] = {0, 0x100000, 0x200000};
if(lang >= sizeof(lang_offset)/sizeof(uint32_t)) return 0;
// 计算在对应语言字库中的偏移
uint32_t offset = ((font_code>>8)-0x81)*190;
offset += (font_code&0xFF)<0x7F ? (font_code&0xFF)-0x40 : (font_code&0xFF)-0x41;
return lang_offset[lang] + offset * FONT_SIZE;
}
遇到乱码时,建议按照以下步骤排查:
检查字库烧录:用读取函数验证W25Q64中的关键位置数据
c复制// 验证"汉"字(0xBABA)的存储
uint8_t test[32];
W25Q64_ReadData(0xBABA*32, test, 32); // 应该看到有效的点阵数据
确认编码格式:确保字符串是GBK编码,常见的UTF-8转GBK问题可以通过以下方式验证:
c复制printf("测试字符:%x %x\n", "测"[0], "测"[1]); // GBK应为0xB2 0xE2
SPI时序检查:用示波器测量CLK和DATA线,确认通信波形正常
DMA配置不当会导致数据传输不完整,可以通过以下方式调试:
c复制void DMA1_Channel2_IRQHandler(void) {
if(DMA_GetITStatus(DMA1_IT_TC2)){
uint16_t remaining = DMA_GetCurrDataCounter(DMA1_Channel2);
if(remaining != 0){
// 传输未完成,处理错误
}
DMA_ClearITPendingBit(DMA1_IT_TC2);
}
}
对于需要更高性能的场景,可以考虑以下优化手段:
c复制// 字库缓存示例
#define CACHE_SIZE 50
typedef struct {
uint16_t gbk_code;
uint8_t bitmap[32];
} FontCache;
FontCache cache[CACHE_SIZE];
int cache_index = 0;
uint8_t* GetFontFromCache(uint16_t gbk){
for(int i=0; i<CACHE_SIZE; i++){
if(cache[i].gbk_code == gbk){
return cache[i].bitmap;
}
}
// 未命中缓存,从Flash读取
uint8_t* data = cache[cache_index].bitmap;
W25Q64_ReadDataDMA(GetFontAddress(gbk), data, 32);
cache[cache_index].gbk_code = gbk;
cache_index = (cache_index + 1) % CACHE_SIZE;
return data;
}
在实际项目中,我发现将最常用的100个汉字缓存后,菜单响应速度提升了近3倍。不过要注意SRAM的使用情况,避免影响其他功能。