ARTICLE DETAIL

资讯详情

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

GD32F103驱动LCD12864实战:SPI接口初始化与调试全解析

GD32F103驱动LCD12864实战:SPI接口初始化与调试全解析 简介一份LCD12864点阵液晶屏的串行控制程序面向单片机与嵌入式开发学习者解决通过串行接口驱动128×64像素屏幕、显示字符和简单图形的问题适用于Arduino、STM32等需要节省I/O引脚的场景。压缩包仅含2个文件——C源码与配套H头文件整体约3KB结构精简却覆盖完整核心驱动逻辑C文件承载初始化、串行指令收发和显示实现H文件提供函数声明及相关宏定义方便直接集成到工程也适合逐行阅读学习。代码封装了字符显示、光标定位、清屏及简单图片点阵数据写入等功能涉及串行时序、ASCII编码转换、点阵刷新等关键知识点可帮助开发者快速上手液晶屏控制并进一步理解液晶屏工作原理和底层驱动封装思路。已有183人浏览学习既可作为LCD显示的入门示例也能作为基础框架进行二次开发快速输出直观的人机交互界面。1. LCD12864 不是“接上电就能亮”的那种屏幕LCD12864 在嵌入式圈子里存在感极高点阵数 128×64内置中文字库的版本能直接显示汉字价格便宜工业仪器、开发板扩展屏、家庭小项目都在用它。仓库名里的 anybdoy392 不影响这块屏的驱动逻辑真正决定好不好用的是初始化序列、时序和电平匹配这三件事。很多人第一次拿 GD32F103 驱动这块屏照着网上的 STM32 例程搬过来却发现白屏原因一般不是代码写错而是没搞清 ST7920 控制器的初始化节奏和 SPI 帧格式。下面的内容从硬件选型讲到驱动代码最后落到调试手段适合做嵌入式开发、想跳过坑直接点亮屏幕的人。2. 接口选型与电平匹配SPI、8 位并口、4 位并口怎么定2.1 三种接口的取舍逻辑先说结论如果你用 GD32F103 驱动 LCD12864优先选 SPI 串行模式也就是模块上标着 PSB 的那个选择位。PSB 拉高走并口拉低走串口。并口方式要占用 11 个 GPIO包括 8 条数据线和 RS、RW、E 三条控制线读回数据方便但接线多、驱动代码也长。SPI 方式只占 4 根线CS、SID、SCLK 加一根可选的 PSB 接地线GD32F103 的硬件 SPI0 正好靠 PA4-PA7 输出一个外设就能解决。顺带要提的是 4 位并口它介于两者之间数据线只需要 4 根但 ST7920 的 4 位模式使用频率不高代码要自己拼字节多数人没用它。下表把三种方式的对比列出来方便你根据手里的 GPIO 余量做决定。接口模式所需引脚数据传输位宽典型速率适配场景SPI 串行CS、SID、SCLK 电源每次 1 bit 传输2-8 MHz引脚紧张、产品化8 位并口D0-D7、RS、RW、E8 bit 并行2-4 MHz读显存、需要回读4 位并口D4-D7、RS、RW、E4 bit 两次传输1-2 MHz折中方案不常用2.2 GD32F103 的 SPI 引脚分配与接线表GD32F103 的 SPI0 复用引脚默认在 PA4-PA7 上PA5 是 SCKPA6 是 MISOPA7 是 MOSIPA4 可以复用为 NSS也可以当作软件控制的片选。LCD12864 的串口只需要三根有效信号线MISO 用不上不接就行。常见模块的丝印会把串口标成 CS、SID或 MOSI、SCLK映射关系如下LCD12864 引脚GD32F103 引脚SPI 角色说明CSPA4片选拉低选中高电平释放SIDPA7MOSI数据线必须先发同步位SCLKPA5SCK时钟空闲拉高PSBGND或 3.3V模式选择串口方式必须接低V0电位器中点对比度串联 1k-10k 可调电阻PSB 这个地方最容易踩坑。有的模块默认预留了一个排针悬空状态并不等于低电平你必须把它显式接到 GND。还有的模块上的 PSB 和 VCC 之间有一个默认的贴片电阻直接拿万用表量一下引脚电平最稳。除此之外V0 引脚的对比度调节不能省白屏或黑屏在不知道从哪里入手查初始化代码时先把 V0 旋到中间位置试一次。2.3 3.3V 与 5V 电平匹配的实际处理很多 LCD12864 模块明确写着 5V 供电而 GD32F103 是 3.3V 器件。如果你的模块上没有板载电平转换芯片直接把 GD32F103 的 3.3V 输出接到模块的电源端模块大概率能亮背光但对比度很怪逻辑电平也可能读不到高。判断方法很简单看模块背面是不是有一颗 74HC4050、74LVC4245 或类似型号的转换芯片。有则引脚直连没有则在 MOSI 和 SCLK 上各串一个 1k 电阻做简单的电平衰减同时把模块的 VDD 接 5VVSS 接公共地。串电阻的做法在低速 SPI 下足够可靠因为 ST7920 的输入阈值在 2V 左右3.3V 高电平直接看成逻辑“1”没有压力串电阻是为了在模块输入端出现毛刺时限制电流属于保护性设计。GD32F103 这边只需要把 GPIO 配置为推挽复用输出不需要开漏。如果模块和 MCU 共用同一个 3.3V 电源那风险更低确认 PSB 和 CS 的电平状态就可以直接刷代码了。3. GD32F103 驱动 LCD12864 的初始化序列与最小可运行代码3.1 SPI 外设的初始化参数如何对齐 ST7920用硬件 SPI 驱动 ST7920参数上要满足一个硬性条件时钟极性和相位要跟 ST7920 的串行时序对齐。ST7920 采用“同步位 控制位 数据位”的帧格式要求空闲时时钟保持高电平数据在下降沿被采样对应 SPI_MODE_3。另外 ST7920 的时钟上限通常是 20MHz 左右但很多模块的 PCB 布线不佳工作频率超过 8MHz 后容易出现数据错位GD32F103 的 SPI 分频后取 2.25MHz 或 4.5MHz 比较稳。下面是 GD32F103 的 SPI0 初始化代码用的是标准外设库的写法配置在 APB2 总线上。默认情况下 APB2 时钟从系统主频分频而来prescale选 32 后 SPI 时钟在 2~3MHz 的范围这个速度在 10cm 以内的杜邦线连接下没有问题。#include gd32f10x.h void lcd_spi_init(void) { spi_parameter_struct spi_init_struct; /* 使能 GPIOA 时钟和 SPI0 时钟 */ rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_SPI0); /* PA5SCK、PA7MOSI 复用推挽输出PA4CS 普通推挽输出 */ gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_5 | GPIO_PIN_7); gpio_init(GPIOA, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4); /* 8 位传输主模式SPI_MODE_3CPOL1、CPHA1*/ spi_init_struct.trans_mode SPI_TRANSMODE_FULLDUPLEX; spi_init_struct.device_mode SPI_MASTER; spi_init_struct.frame_size SPI_FRAMESIZE_8BIT; spi_init_struct.clock_polarity_phase SPI_CK_PL_HIGH_PH_2EDGE; spi_init_struct.nss SPI_NSS_SOFT; spi_init_struct.prescale SPI_PSC_32; spi_init_struct.endian SPI_ENDIAN_MSB; spi_init(SPI0, spi_init_struct); spi_enable(SPI0); }clock_polarity_phase对应 CPOL1、CPHA1也就是模式 3。SPI_NSS_SOFT表示片选完全由 GPIO 控制CS 拉低之前要先把 SPI 的数据寄存器准备好否则最后一个字节可能多移位一次。prescale SPI_PSC_32把 SPI 时钟降到 2~3MHz 区间这个速度在短距离连接下不会出错如果模块走线长或者用了软排线再降一档到 1.125MHz 更稳。3.2 写指令与写数据的帧结构ST7920 的串行帧以 5 个连续的1作为同步位紧跟着 1 个 RW 位和 1 个 RS 位最后是 8 位数据。RW 固定为 0 表示写操作RS 为 0 是写指令RS 为 1 是写数据。每个字节的最高位先发。由于 GD32F103 的 SPI 是 8 位一帧计数向 ST7920 发送内容时要把 CS 在字节之间不断拉高再拉低来分割帧。第二种做法占 CPU但代码直观适合先跑通功能。void lcd_spi_send_byte(uint8_t data) { while (RESET spi_i2s_flag_get(SPI0, SPI_FLAG_TBE)); spi_i2s_data_transmit(SPI0, data); while (RESET spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE)); spi_i2s_data_receive(SPI0); /* 清空接收缓冲避免标志滞留 */ } void lcd_write_byte(uint8_t rs, uint8_t data) { gpio_bit_reset(GPIOA, GPIO_PIN_4); /* CS 拉低 */ lcd_spi_send_byte(0xF8 | (rs 2)); /* 同步位 11111 RW0 RS */ lcd_spi_send_byte(data 0xF0); /* 高 4 位补 0 发送 */ lcd_spi_send_byte((data 0x0F) 4); /* 低 4 位补 0 发送 */ gpio_bit_set(GPIOA, GPIO_PIN_4); /* CS 拉高结束本帧 */ lcd_delay_us(10); }RS 通过0xF8 | (rs 2)拼进第一个字节RS0 时首字节是 0xF8RS1 时首字节是 0xFA。后面两个字节分别放置数据的高 4 位和低 4 位ST7920 会忽略每个字节的低 4 位。每次写完拉高 CS 再拉低相当于给 ST7920 一个帧结束的标志。注意spi_i2s_data_receive必须读一次否则 RBNE 标志一直保持后续发送会被卡在等待上。提示CS 不能在整个传输过程中一直保持低电平。ST7920 的串行状态机依靠同步位来对齐每帧CS 高电平是状态机复位的必要条件连续多发几轮数据会导致屏幕显示完全错位。3.3 初始化序列为什么是那 8 条指令ST7920 的初始化在网上有大量版本差别集中在第一条和最后一条。标准的完成序列是复位0x30连续两次进入基本指令集0x30关闭显示0x08清屏0x01地址自增方式0x06开启显示且关闭光标0x0C退出基本指令集0x30再延时。下面这段代码把整个过程封装成函数调一次就能点亮。void lcd_init(void) { lcd_spi_init(); lcd_delay_ms(50); /* 等模块内部复位完成 */ lcd_write_cmd(0x30); /* 8 位接口基本指令集 */ lcd_delay_ms(10); lcd_write_cmd(0x30); lcd_delay_ms(10); lcd_write_cmd(0x30); lcd_delay_us(100); lcd_write_cmd(0x08); /* 关闭显示确保清屏时不闪 */ lcd_write_cmd(0x01); /* 清屏清屏需要较长延时 */ lcd_delay_ms(20); lcd_write_cmd(0x06); /* 地址指针自增写数据后光标右移 */ lcd_write_cmd(0x0C); /* 开显示关光标不闪烁 */ lcd_write_cmd(0x30); /* 回到基本指令集 */ lcd_clear(); /* 再清一次屏覆盖上电乱码 */ }0x30 代表 8 位总线长度和基本指令集连续三次是公认稳妥的做法。0x01 清屏指令会执行几十毫秒期间不能马上写下一指令代码里给了 20ms 的延时。如果你跳过 0x08 直接发 0x01在上电瞬间屏幕还可能残留 ST7920 内部的随机显存清屏过程会出现一闪而过的噪点虽然不影响最终效果但在产品上电时容易被客户拍下来当故障反馈。初始化完成后屏幕应该显示为全白的空白区域对比度电位器调好后底灰均匀。3.4 显示坐标与中文字符集的关系LCD12864 的显示区域分为四个水平基地址第一、三行的基地址是 0x80 到 0x87第二、四行是 0x90 到 0x97。每一行 16 个汉字位串行模式下写中文字库需要先把地址设置到对应行。下面是设置地址和写汉字的核心函数void lcd_set_pos(uint8_t x, uint8_t y) { uint8_t addr; switch (y) { case 0: addr 0x80 x; break; case 1: addr 0x90 x; break; case 2: addr 0x88 x; break; /* 第三行从 0x88 开始 */ case 3: addr 0x98 x; break; /* 第四行从 0x98 开始 */ default: addr 0x80; break; } lcd_write_cmd(addr); } void lcd_show_hanzi(const char *str) { while (*str) { lcd_write_data(*str); /* 汉字是双字节编码按 GB2312 依次输出 */ lcd_write_data(*str); } }常见误区是把第二行直接写成 0x90 x第三行写成 0xA0 x。实际上第二行归零后是 0x90第三行是 0x88第四行是 0x98。原因在于 ST7920 的 DDRAM 地址低 4 位是水平方向高 2 位是页方向第三行和第一行共享同一个高页只是水平地址差了一格。写坐标时如果发现字符错行先查这里的地址偏移。4. 显示效率与中文字库帧缓冲、局部刷新和滚动动画4.1 用取模软件生成字模再交给帧缓冲LCD12864 内置中文字库能覆盖 GB2312 常用汉字但图标、箭头、柱状图这些图形必须走自定义字符或图形模式。取模软件生成的 16×16 汉字字模一般是 32 字节数组每行两个字节第一个字节对应左边 8 个点第二个字节对应右边 8 个点。在 GD32F103 这类 MCU 上推荐先把字模点进帧缓冲再由统一的刷新函数上屏。void fb_draw_bmp(uint8_t x0, uint8_t y0, const uint8_t *bmp, uint8_t w, uint8_t h) { uint8_t x, y; for (y 0; y h; y) { for (x 0; x w; x) { if (bmp[(y * w x) / 8] (0x80 (x % 8))) { uint8_t px x0 x; uint8_t py y0 y; if (px 128 py 64) { fb[py / 8][px / 8] | (0x80 (px % 8)); } } } } fb_refresh(); }fb_draw_bmp把字模数组按位展开到帧缓冲py / 8定位到帧缓冲的行px / 8定位到该行中的字节0x80 (px % 8)用来置位对应像素列。这样做的一个额外好处是多个图形叠加绘制时不会互相覆盖因为每次操作都只把像素位置 1不清掉该位置已有的其他内容。如果要支持擦除再加一个参数控制是置 1 还是清 0 即可。4.2 1KB 帧缓冲与局部刷新策略GD32F103 的 RAM 通常在 8KB 到 20KB 之间128×64 单色位图只需要 1KB放在哪个型号上都够用。常见做法是定义一个uint8_t fb[8][16];每个元素对应屏幕上 8 像素高、16 字节宽的块。所有文字、图形先画进帧缓冲再一次性刷新到 LCD这样文本覆盖图形、图形覆盖文本都不需要重新规划分层。uint8_t fb[8][16]; /* 8 行 × 16 字节对应 128×64 显示区需要 #include string.h */ void fb_clear(void) { memset(fb, 0, sizeof(fb)); } void fb_refresh(void) { uint8_t row, col; lcd_write_cmd(0x34); /* 进入扩展指令集 */ for (row 0; row 8; row) { lcd_write_cmd(0x80 row); /* 垂直地址一页 8 像素高 */ lcd_write_cmd(0x80); /* 水平地址从第 0 列开始 */ for (col 0; col 16; col) { lcd_write_data(fb[row][col]); } } lcd_write_cmd(0x36); /* 关闭扩展指令集 */ }fb_refresh每次刷新 8×16 字节即使 MCU 全速跑 UI 逻辑也只占用几百微秒足够应付 50ms 级的数据刷新频率。觉得整屏刷新太浪费的话可以把col的循环范围改成固定区间实现局部刷新。局部刷新的坑在于地址回跳ST7920 的地址指针在绘图模式下到达行尾后会自动跳到下一行如果只刷 3 列刷新结束后的地址指针停在不固定的位置下一次写文本前必须重新设置坐标。4.3 用帧缓冲偏移做出滚动动画文本模式下的滚动可以用延时控制指令也可以直接操纵地址指针做出像素级平移。像素级平移动画适合做进度条和波形图把下一帧数据写入显存然后在刷新函数里传入偏移量offset数据从fb[8][16]中以偏移后的下标取值实现水平方向平滑移动。void fb_refresh_offset(uint8_t offset) { uint8_t row, col; lcd_write_cmd(0x34); for (row 0; row 8; row) { lcd_write_cmd(0x80 row); lcd_write_cmd(0x80); for (col 0; col 16; col) { lcd_write_data(fb[row][(col offset) 0x0F]); } } lcd_write_cmd(0x36); }(col offset) 0x0F做的是环形取模当偏移超过 16 时自动回绕适合无限循环的水平流动文字。这种做法把作用范围限制在 1KB 的显存内对控制成本敏感的仪表产品很实用。动画过程中不需要清屏因为新的数据直接覆盖旧值避免了白屏闪烁。5. 用逻辑分析仪和自检页快速定位 LCD12864 的黑屏与乱码5.1 先跑一个不依赖字库的自检页点亮前先让屏幕显示固定图案能区分接线问题、初始化问题和字模问题。自检页的第一屏放 0xFF 和 0x00 交替的横条纹第二屏放棋盘格。横条纹可以验证数据线和时钟是否正常棋盘格可以验证地址递增是否跳行。这段代码放在lcd_init之后执行不需要额外硬件void lcd_selftest(void) { uint8_t i, j; uint8_t pattern[16]; lcd_write_cmd(0x34); /* 进入扩展指令集 */ for (i 0; i 8; i) { lcd_write_cmd(0x80 i); lcd_write_cmd(0x80); for (j 0; j 16; j) { pattern[j] (i % 2) ? 0x00 : 0xFF; lcd_write_data(pattern[j]); } } lcd_write_cmd(0x36); }如果屏幕上出现清晰的横条纹说明时序没问题下一步刷字库。如果横条纹出现但不连续每隔几行错位说明地址计算有误或 SPI 字节顺序异常。如果屏幕完全没有反应优先测量 CS 和 SCLK 引脚波形确认 MCU 侧的代码有没有真正输出。很多情况下问题出在 GPIO 复用配置没有生效PA4 仍然被当作普通 IO 操作导致 CS 一直拉高屏幕上永远等不到一帧完整数据。5.2 用逻辑分析仪抓三根线的时序特征SPI 方式驱动 ST7920 时逻辑分析仪是最常用的调试工具。接好 CS、SID、SCLK 三根线设置采样率大于 10MHz触发沿设为 CS 下降沿跑一次初始化序列后就能看到完整帧。正常波形上CS 拉低后 SID 先出现 5 个连续的1接着是 RS 位、8 个数据位、CS 拉高。如果看到的数据位乱掉了多半是 SPI 模式配置错了ST7920 要求空闲时钟高电平、下降沿采样对应 SPI_MODE_3如果你用的是模式 0数据会在上升沿被采样屏幕上就会显示毫无规律的乱码。十几块钱的 8 通道逻辑分析仪就能胜任只要采样率不低于 10MHz 就行。连接时注意 SID 是单向输出接到逻辑分析仪的通道后不要和 MOSI 混接避免把通道位置接错导致波形看起来正常但实际没有任何数据到达屏幕。5.3 常见故障与对应处理手段| 故障现象 | 可能原因 | 处理方式 | |---------|---------|---------|---------| | 白屏无任何显示 | 对比度偏低或初始化未完整执行 | 先把 V0 调至中间位置再检查 0x30 指令序列 | | 屏幕上全是黑块 | 写入的是随机数据或 SPI 模式错误 | 用逻辑分析仪确认帧格式检查 SPI_MODE_3 | | 文字错行、重叠 | 坐标地址计算错误 | 核对第二行 0x90、第三行 0x88 的地址关系 | | 显示闪烁、滚动不连续 | 清屏太频繁或刷新时序过慢 | 使用局部刷新减少 0x01 清屏指令的使用频率 | | 上电瞬间有噪点 | 清屏指令执行不及时 | 初始化时先发 0x08 关闭显示再清屏 |除了表格里列的情形还有一个高频坑模块上电瞬间 CS 和 SCLK 处于未定义电平ST7920 会把噪声误判为同步位导致内部状态机错乱。解决办法是在初始化前把 CS 拉高保持 100ms 以上然后再拉低进入首帧。这个处理在 MCU 侧只需加一句gpio_bit_set(GPIOA, GPIO_PIN_4);但很多人跳过了它结果每次上电的行为都不一样。加完这行延时后再运行自检页白屏、乱码、错行三类问题基本都能一眼定位。最后再说一个偏门但有效的验证技巧把模块拆掉 PSB 跳线改用 8 位并口模式跑一遍同样的自检页。如果并口下正常而串口下异常说明串口帧格式或 SPI 参数有问题如果两者都异常问题大概率在模块本身或对比度调节。这个交叉验证法不需要额外仪器非常适合手头没有逻辑分析仪的场合。本文还有配套的精品资源点击获取
返回列表