尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32驱动LCD12864实战:从底层时序到多级菜单框架

STM32驱动LCD12864实战:从底层时序到多级菜单框架 简介这套基于STM32单片机的LCD12864显示例程源码面向嵌入式初学者与开发者主要解决开机画面绘制与多级菜单交互设计问题。压缩包共157个文件大小仅2.78MB以34个.h与33个.c源文件为核心同时包含.uvproj、.uvopt等Keil工程文件及.hex、.axf等编译产物便于直接打开工程阅读和重新编译。LCD12864驱动部分覆盖GPIO配置、SPI/I2C接口通信、显示数据传输等关键环节菜单部分采用多级结构设计并融入状态机与中断处理逻辑源码中还附带AD9851相关驱动可作为外设扩展参考。已有937人学习下载适合通过实际例程理解液晶控制原理并在此基础上快速移植或扩展功能。1. 为什么一个STM32驱动LCD12864的工程还能拆出这么多东西接手过带屏小仪器的人都有这种经历给设备加个「开机显示LOGO、按键进入设置」的交互方案里塞一块串口屏嫌贵上RGB彩屏又得折腾驱动板和DMA最后回到LCD12864。这块128×64、内置ST7920控制器的点阵屏串行时序只要CS、SCLK、SID三根线3.3V能跑还带中文字库小批量成本压得很低。这个工程跟网上常见的「点亮屏幕」demo不太一样。目录里除了lcd12864.__i还有menu.__i和ad9851.__i可以看出整套例程是「DDS信号源 多级菜单」的结构开机画面只是第一步后面频率设置、波形选择都走同一套菜单框架。想拿来做课程设计或者把一台旧设备改成按键操作这套代码是合适的参考底子。适合谁刚把F103点亮的入门者可以照着时序把屏刷起来做过多级菜单的熟手重点看菜单节点的组织和GDRAM刷图的方式。2. LCD12864驱动底层GPIO配置与ST7920串行时序2.1 接口选型为什么这颗屏的串行方式不能照搬SPILCD12864模块常见的控制器是ST7920它原生支持并行和串行两种接口。并行8080方式需要DB0~DB7、RS、RW、E至少11根线占用太多GPIO串行方式把CS、SCLK、SID三根线拉出来对STM32F103这种引脚紧张的单片机友好得多。但要注意ST7920的串行帧格式不是标准SPI。它每一帧固定是15位先发5个高电平同步位再发1个RW位写为0接着1个RS位0命令/1数据最后8位数据、高位在前。标准SPI是8位或16位一帧直接拿硬件SPI去发需要把15位拆拼成两个字节处理起来很别扭实际工程里反而很少这么做。我一般直接用GPIO模拟这15位时序。这样做还有个好处换到GD32F103、APM32这类兼容芯片时只需要改RCC和GPIO初始化时序代码完全不用动。这套例程虽然主体是STM32F103标准外设库写的但搬运成本很低。LCD12864模块这边需要确认一个引脚PSB。它接地低电平才进入串行模式悬空或者接高电平是并行模式。很多板子默认通过跳线或电阻把PSB拉高了接好线不显示先查它准没错。LCD12864引脚接到STM32方向说明CSPB12输出片选高电平选中SCLKPB13输出同步时钟上升沿采样SIDPB14输出串行数据高位先出PSBGND-接地进入串行模式V010K电位器中点-调节对比度2.2 GPIO初始化与底层写字节函数先看GPIO配置。这里用的是标准外设库时钟使能、推挽输出、50MHz速率void LCD_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); LCD_CS_H(); }GPIO_Mode_Out_PP是推挽输出驱动能力足够直接带ST7920的串行输入脚。如果你的模块是5V供电的需要确认它的VIH门槛部分5V模块用3.3V推挽能驱动但也有模块需要加电平转换这里建议先查模块手册别直接上。底层写位函数是整个驱动的基础时序要求是SID数据先稳定然后SCLK拉高、再拉低数据在上升沿被采样static void LCD_Delay(void) { volatile uint8_t i 30; while (i--); } static void LCD_WriteBit(uint8_t bit) { if (bit) LCD_SID_H(); else LCD_SID_L(); LCD_SCLK_H(); // SID 稳定后时钟上升沿 LCD_Delay(); LCD_SCLK_L(); LCD_Delay(); }这段代码里LCD_SID_H()这类宏建议直接用寄存器操作比如GPIOB-BSRR GPIO_Pin_14比函数调用快也方便在不同工程间复制。LCD_Delay()的空循环次数跟主频有关STM32F103跑72MHz时30次循环大约在1~2微秒量级能满足ST7920的时序要求。有了写位函数就能组合出写命令/写数据的完整帧void LCD_WriteByte(uint8_t isData, uint8_t dat) { uint8_t i; LCD_CS_H(); // 片选拉高开始一帧 for (i 0; i 5; i) // 5 位同步高电平 LCD_WriteBit(1); LCD_WriteBit(0); // RW 0写操作 if (isData) LCD_WriteBit(1); // RS 1写数据 else LCD_WriteBit(0); // RS 0写命令 for (i 0; i 8; i) // 8 位数据高位在前 { LCD_WriteBit((dat 7) 0x01); dat 1; } LCD_CS_L(); // 片选拉低结束一帧 }参数isData用来区分命令和数据0发命令1发数据。整个函数不关心这颗屏内部在做什么它只负责把ST7920认识的串行帧送出去。后面所有显示操作包括初始化、清屏、画图、写字符最终都归结为调这个函数。2.3 初始化序列和DDRAM地址映射ST7920的初始化序列有一定要求不能上电直接写显示命令。常见做法是连续写多次0x30做同步再依次设地址归零、显示开关和清屏void LCD_Init(void) { LCD_GPIO_Init(); LCD_DelayMs(50); // 上电等待电源稳定 LCD_WriteByte(0, 0x30); LCD_DelayMs(5); LCD_WriteByte(0, 0x30); LCD_DelayMs(1); LCD_WriteByte(0, 0x30); // 连续三次 0x30让控制器稳定在 8 位基本指令集 LCD_DelayMs(1); LCD_WriteByte(0, 0x02); // DDRAM 地址归零 LCD_WriteByte(0, 0x06); // 写入后光标右移 LCD_WriteByte(0, 0x0C); // 显示开光标关闪烁关 LCD_WriteByte(0, 0x01); // 清屏 LCD_DelayMs(2); }初始化完成后文本模式可以直接用了。ST7920文本模式下DDRAM地址是分四行的行首地址不连续这是最容易踩坑的地方显示行行首地址一行的汉字数第1行0x808个第2行0x908个第3行0x888个第4行0x988个第3行地址是0x88而不是0xA0因为ST7920内部DDRAM按两块64×16区域组织。写字符时先设地址再连续写地址会自动递增写到行尾后会自动跳到下一块所以每行显示前都要重新设置一次行首地址。3. 开机画面字模数据、GDRAM写入与取模方向3.1 用取模软件生成128×64的图像数组LCD12864的图形区逻辑分辨率是128×64像素一屏完整图像在代码里表现为1024字节的数组128×64÷8。生成这个数组我一般用PCtoLCD2002或者字模大师设置项比较关键稍微设错屏幕上出现的就是乱条纹。参数推荐设置说明图片尺寸128×64正好一屏取模方式逐行式行列式和GDRAM写入顺序对应字节顺序高位在前和LCD_WriteByte移位方向一致颜色阴码像素点为1不点亮点为0生成出来的数组长这样const uint8_t logo_12864[] { 0x00, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x80, 0x0F, 0x80, 0x00, 0x00, // ...... 共 1024 字节 };图片不足128像素宽时右侧补0x00。这里强调阴码是因为LCD12864的LED背光像素点亮逻辑是写1点亮取反后显示效果正好相反。3.2 GDRAM写入函数先关显示再整帧写入ST7920的图形区叫GDRAM它和文本区DDRAM是两套独立地址空间。写GDRAM必须切到扩展指令集也就是先发0x34。这里有个经验写GDRAM之前最好先把显示关掉全部写完再开否则刷屏过程中能看到明显的雪花和撕裂感。void LCD_DrawBitmap(const uint8_t *data) { uint8_t y, i; LCD_WriteByte(0, 0x34); // 扩展指令集关显示进入 GDRAM 访问模式 for (y 0; y 32; y) // GDRAM 垂直地址 0~31 { LCD_WriteByte(0, 0x80 y); // 设置垂直地址 LCD_WriteByte(0, 0x80); // 水平地址从 0 开始 for (i 0; i 16; i) // 16 次每次 2 字节共 32 字节 { LCD_WriteByte(1, *data); LCD_WriteByte(1, *data); } } LCD_WriteByte(0, 0x36); // 扩展指令集开显示 LCD_WriteByte(0, 0x30); // 回到基本指令集恢复文本操作 }参数data指向1024字节的图像数组。我按逐行式取模来写每个垂直地址对应屏幕上下两条8像素行前16字节是上方行后16字节是下方行。如果你的取模软件输出的是按页排列的数据把这两段交换即可。最后那两条命令经常被漏掉。0x36是扩展指令集下的开显示0x30是切回基本指令集。如果只写0x36不切回后面调用LCD_ShowString时地址计算全会乱。3.3 开机流程从Logo到主菜单实际开机显示流程顺序是清屏、刷Logo、停留、回文本模式、显示版本信息、切到菜单框架void ShowWelcome(void) { LCD_WriteByte(0, 0x01); // 清屏 LCD_DrawBitmap(logo_12864); // 全屏刷 Logo LCD_DelayMs(1500); LCD_WriteByte(0, 0x30); // 先切回基本指令集 LCD_WriteByte(0, 0x01); // 再清屏 LCD_ShowString(0, 0, (char*)欢迎使用信号源); LCD_ShowString(1, 0, (char*)V1.0); LCD_DelayMs(800); }顺序上有个细节清屏命令0x01在扩展指令集和基本指令集下含义不同建议先切回0x30再清屏避免清不干净。多级菜单的入口函数在开机流程最后调用这样从开机画面到系统设置菜单的过渡是连续的。4. 多级菜单结构体数组、按键扫描与状态机4.1 菜单数据结构用数组代替链表多级菜单最常见的实现方式是「结构体数组 父子节点索引」。菜单项数量在编译期就是确定的用链表反而增加动态内存管理负担。STM32F103的RAM本来就紧张菜单表放Flash更合适。typedef struct { uint8_t id; // 节点编号 uint8_t parent_id; // 父节点编号0xFF 表示根 uint8_t item_count; // 子项数量 uint8_t sub_start; // 第一个子项在数组中的下标 uint8_t *title; // 菜单标题汉字字符串 void (*on_enter)(void); // 进入节点时执行的回调NULL 表示无 } MenuNode;id和parent_id组成树形关系item_count和sub_start用来遍历子项。on_enter函数指针是关键设计选中某个菜单项按下确认键时直接调用对应的处理函数比如设置频率、切换波形。菜单表这样组织const MenuNode menu_table[] { {0, 0xFF, 2, 1, (uint8_t*)系统设置, NULL}, // 根菜单 {1, 0, 3, 3, (uint8_t*)频率输出, NULL}, // 根菜单的第 1 个子项 {2, 0, 2, 6, (uint8_t*)载波电平, NULL}, // 根菜单的第 2 个子项 {3, 1, 0, 0, (uint8_t*)10MHz, set_freq_10M}, {4, 1, 0, 0, (uint8_t*)20MHz, set_freq_20M}, {5, 1, 0, 0, (uint8_t*)50MHz, set_freq_50M}, // 后续节点依次排列 };节点顺序上把同一个父节点的子项连续存放sub_start就能直接索引到第一个子项。这种组织方式在调试时特别方便menu_table[3]一定是menu_table[0]的第3个子项不需要遍历链表。4.2 按键扫描与菜单状态机按键处理我习惯做成独立模块返回一个事件而不是直接操作菜单这样按键消抖逻辑和菜单逻辑解耦。四个按键上翻、下翻、确认、返回。uint8_t Key_Scan(void) { static uint8_t last_state 0; uint8_t state 0; if (KEY_UP_PIN 0) state | KEY_UP; if (KEY_DOWN_PIN 0) state | KEY_DOWN; if (KEY_ENTER_PIN 0) state | KEY_ENTER; if (KEY_BACK_PIN 0) state | KEY_BACK; if (state ! last_state) { DelayMs(10); // 简单消抖 last_state state; } return last_state; }状态机处理菜单跳转这是整个菜单系统的核心typedef struct { uint8_t current_node; // 当前所在节点 uint8_t selected_index; // 当前选中项在本层中的序号 } MenuState; void Menu_ProcessEvent(uint8_t key, MenuState *state) { const MenuNode *node menu_table[state-current_node]; switch (key) { case KEY_UP: if (state-selected_index 0) state-selected_index node-item_count - 1; // 循环回卷 else state-selected_index--; break; case KEY_DOWN: state-selected_index (state-selected_index 1) % node-item_count; break; case KEY_ENTER: { MenuNode *child (MenuNode *)menu_table[node-sub_start state-selected_index]; if (child-item_count 0) { state-current_node child-id; // 进入子菜单 state-selected_index 0; } else if (child-on_enter) { child-on_enter(); // 叶子节点执行功能 } break; } case KEY_BACK: if (node-parent_id ! 0xFF) { state-current_node node-parent_id; // 返回父菜单 state-selected_index 0; } break; } }selected_index是本层序号不是全局节点ID这一点要注意。进入子菜单时selected_index归零返回父菜单时也归零。循环回卷用的取模运算上翻和下翻都能在首尾之间绕圈避免菜单项多时翻到边界卡住。4.3 菜单渲染每屏四行与选中项指示LCD12864文本模式下最多显示4行中文所以每屏菜单最多放4个子项。绘制菜单时先清屏再显示当前节点标题然后从sub_start开始取子项逐行打印void Menu_Draw(const MenuState *state) { uint8_t i; const MenuNode *node menu_table[state-current_node]; LCD_WriteByte(0, 0x01); // 清屏 LCD_ShowString(0, 0, node-title); for (i 0; i node-item_count i 4; i) { const MenuNode *child menu_table[node-sub_start i]; LCD_ShowString(i 1, 0, child-title); if (i state-selected_index) { // 选中行在行首补一个 16x16 的三角图标字模 // 或者用反显方式把整行区域填黑后重新写白字 } } }菜单项超过4个时需要做翻页而不是继续往下打否则会冲到DDRAM下一行。一般做法是增加一个page_offset偏移量一次最多显示当前页的4项。选中行指示最省事的办法是在行首输出一个小三角图标字模或者利用GDRAM画一个16×16的黑色方块反显整行。注意GDRAM和DDRAM是两个独立图层反显后重新刷新文本时GDRAM层不会被清掉所以要在每次Menu_Draw开头把图形区一起清理。5. 仿真与真机调试从波形、PSB电平到字模方向的几个验证点5.1 先看CS/SCLK/SID波形再怀疑代码串行LCD12864不上电没反应先别急着改驱动。拿示波器或者逻辑分析仪探头接SID和SCLK按一下复位键抓初始化那段波形。正常应该看到CS拉高后SCLK上先出现5个高电平然后RW位、RS位再跟着8位数据。如果SCLK没有上升沿检查GPIO时钟是否使能如果SID一直为低检查是不是把命令和数据参数传反了。没有示波器时可以写一个测试循环把PB12、PB13、PB14同时翻转用万用表测引脚电平变化能确认GPIO模式是不是推挽输出。5.2 PSB引脚和对比度电平是两大隐形开关串行模式必须把模块的PSB引脚接地。某些模块PSB引脚默认通过跳线帽接VCC不拆跳线帽发再多命令屏幕也不亮。另外V0引脚要接一个10K电位器到GND调出合适的对比度电压ST7920的显示电压典型值在3~5V之间太高会全黑太低会全白。5.3 用0xAA和0x55图案验证字模方向刷开机位图出现乱条纹优先怀疑取模设置而不是驱动逻辑。写一个测试图像填进GDRAM用最直观的方法判断字节顺序uint8_t test_pattern[1024]; memset(test_pattern, 0xAA, sizeof(test_pattern)); // 每字节 10101010 LCD_DrawBitmap(test_pattern);0xAA每字节是10101010正常显示应该是横向棋盘格。如果看到的是竖条纹说明字节内的位序反了取模软件里把「高位在前」改成「低位在前」。如果看到的是斜向条纹说明逐行式和逐列式选错了。这个测试图案比对着资料猜快得多。最后说一个提升体验的技巧开机Logo刷新时整个GDRAM写入过程都处于0x34关显示状态全部1024字节写完后再用0x36开显示这样用户看到的是完整画面一次出现而不是从上往下逐行刷出来。同理菜单切换时先清GDRAM、再写新内容、最后开显示视觉上就不会有残影和撕裂。本文还有配套的精品资源点击获取
返回列表