UC1698电表单色屏驱动:从RAM映射到Arduino局部刷新实作

发布时间:2026/9/16 17:01:43
UC1698电表单色屏驱动:从RAM映射到Arduino局部刷新实作 简介这是围绕UC1698液晶控制器打造的电表显示屏驱动资源包同时提供Arduino与STM32两种平台的驱动方案。资源涵盖了UC1698底层时序控制、图形点阵绘制、文本显示刷新等关键代码可用于智能电表、工业仪表等需要低成本图形界面的嵌入式场景。压缩包共183个文件以91个h头文件与69个c源文件为主另含15个s汇编启动文件以及mxproject、ioc、uvprojx等STM32CubeMX与Keil工程配置文件整体体积仅1.15MB结构紧凑便于阅读。目前已有454人学习下载适合具备一定嵌入式基础、希望掌握图形液晶显示驱动的开发者参考。通过阅读完整工程可深入理解串行/并行接口时序、灰度模式配置以及Arduino与STM32在显示控制上的不同实现思路对后续二次开发和低功耗优化有直接帮助。1. 为什么很多电表屏用的是 UC1698 而不是 ST7565UC1698 这个芯片在电表、充电桩和配电终端里比想象中常见。COG 封装的小黑点贴在屏排线上丝印只有一行 1698u分辨率从 160×128 到 240×128 都有。它和 ST7565、SSD1306 看起来都是单色点阵控制器但命令风格、显示 RAM 组织、扫描方向控制完全不是一回事。把 ST7565 的初始化序列抄过来结果是满屏竖道、左右镜像或者内容斜着走。真正的原因是 UC1698 的显示 RAM 按页与列双地址组织行扫描方向还可编程寄存器和常见库对不上。下面按显示原理、Arduino 接线、电表界面落地和坑位排查的顺序展开目标是让一块 UC1698 屏在半天内点上并跑出可用的电表数据页。2. UC1698 显示 RAM 的页列映射与扫描方向判断2.1 RAM 组织页地址与列地址的双坐标寻址UC1698 内部没有“每个像素一个地址”的线性显存而是把帧存组织成 Page页与 Column列两层。一页对应 8 条行线每个字节的 8 个 bit 在屏幕上表现为垂直排列的 8 个像素列地址则负责横向移动。读写以字节为单位库的底层把左上角像素换算成 (page, column, bit_offset) 三元组再拼成一次串行或并行写命令。这里最容易出问题的不是“页列坐标”本身而是地址计数器写完一个字节后的自动递增方向。UC1698 的 RAM Address Control 命令可以控制地址计数器是按列方向递增还是按页方向递增某些衍生屏代码里还会把 X 方向反着读。SSD1306 的习惯是列地址从左到右、页地址从上到下UC1698 不一定所以直接把 ST7565/SSD1306 的驱动塞进 UC1698 模组画面会呈现出“一行错位 8 像素”的典型故障。另外UC1698 内部支持 2bit 灰阶可以显示 4 级灰度。但对电表界面来说灰阶会降低阳光下可读性也增加页面设计成本实际工程里基本只用 0/1 两个状态。灰阶模式下页内一个字节不能简单当成 8 个独立像素Ucglib 这类库会把它切成两套位面这在排错时容易增加认知负担所以电表 HMI 一般关掉灰度。2.2 扫描方向与镜像为什么屏厂装配会改变显示结果同样的 UC1698 玻璃屏厂可能正装也可能倒装。生产线上把玻璃旋转 180° 贴合后行列扫描方向和公共端 COM 输出顺序都会变。模组规格书上通常会标 SHL0/1、ADC0/1 这类参数分别控制行扫描方向和列地址方向。Arduino 侧没有跳线可拨只能在软件层补偿。Ucglib 为 UC1698 提供了多套构造类里面的“18_T”“28_T”等变体就包含了偏压、扫描方向和页列映射的差异。选错类不会报错现象是文本左右镜像、整屏上下颠倒或外框少一条边。接线没问题、代码也能跑就是方向不对这种问题最容易浪费半天时间。2.3 用一条对角线快速验证方向是否对先把方向问题固定下来再谈写电表界面。上面的代码只画一条对角线和外框ucg.setColor(255, 255, 255); ucg.clearScreen(); ucg.drawFrame(0, 0, 240, 128); ucg.drawLine(0, 0, 239, 127);逻辑说明clearScreen 之后必须先 setColor 再绘图否则颜色状态可能停留在清屏时使用的值。drawFrame 检查四边是否能完整闭合drawLine 检查行方向和列方向是否一致。参数说明这里按 240×128 的模组写坐标如果你的屏是 160×128把 drawLine 终点改成 (159, 127)。对照下面这张表判断现象现象原因方向处理位置外框缺边内容斜向错位页列映射或分辨率不匹配检查 Ucglib 类名里的分辨率参数左右镜像列地址方向反查 RAM Address Control 的列方向位上下颠倒行扫描方向反查 SHL/COM 扫描方向配置对角线上端偏左下端偏右正常无需调整继续后续初始化如果对角线正常但文字是镜像的问题不在扫描方向而在后续画字形时的位序优先怀疑字体数据或库版本。先把对角线这个基准定下来后面所有界面排错都有参照。3. Arduino 驱动 UC1698 的最小接线、库安装与初始化3.1 SPI 模组接线Uno/Nano 上的 7 根线UC1698 模组有两种常见接口SPI 和 8080 并行。Arduino 上优先选 SPI 版本原因很直接并行口至少 12 根信号线占满 Uno 的 GPIO电表场景还要留引脚给计量芯片或 Modbus 串口根本不够用。SPI 版本最少只需要 5 根信号线外加电源和背光。以 Arduino Uno/Nano 为例接线如下模组引脚功能Uno/Nano 引脚备注VCC逻辑电源3.3V 或 5V以模组丝印为准不可乱接GND地GND与 Arduino 共地SCLKSPI 时钟D13硬件 SPI 默认引脚SDIN / MOSISPI 数据D11硬件 SPI 默认引脚CS片选D10可由任意 GPIO 控制DC / A0数据/命令选择D9有的模组标 RSRST复位D8低有效BLK背光3.3V 串 220Ω不要直接接 5V注意一点很多 UC1698 玻璃本身是 3.3V 逻辑Uno 的 5V SPI 输出直接怼上去不是所有模组都能承受。稳妥的做法是在 CS、DC、RST 三根低速信号上各串一个 1kΩ 电阻SCLK 和 MOSI 如果模组的电平转换电路不完整也从 3.3V 供电并用转换器。现场电表主控多数是 3.3V 的 MCU反而没这个问题。3.2 安装 UcglibArduino IDE 与 VSCodePlatformIO 两条路UC1698 的 Arduino 驱动常见做法是选 Ucglib作者是 olikraus。Adafruit 的 GFX 库面向 SSD1306、ST7735 这些控制器不认 UC1698方向从一开始就错了。Arduino IDE 里打开库管理器搜索 Ucglib直接安装即可。安装后 Examples 菜单里能看到 ucglogo、ucg_text 等示例先把示例中原有的控制器类名替换成 UC1698 的类名就能快速验证屏是否点亮。如果不用 Arduino IDEVSCode 装 PlatformIO 插件也完全可以替代编辑器。在 platformio.ini 里声明依赖lib_deps olikraus/Ucglib逻辑说明PlatformIO 会从库索引拉取 Ucglib不需要手动下载。它和 Arduino IDE 共享同一套源码和构造类只是构建和烧录流程放在 VSCode 里调参数、看串口输出都比老 IDE 顺手。对电表这种要长期维护的工程建议一开始就用 VSCodePlatformIO。3.3 begin、旋转与首个图形初始化代码很短#include SPI.h #include Ucglib.h // 硬件 SPI 版本的 UC1698240x128 分辨率 Ucglib_UC1698_18_T_240X128_HWSPI ucg(/*cd*/9, /*cs*/10, /*reset*/8); void setup() { ucg.begin(UCG_FONT_MODE_TRANSPARENT); ucg.clearScreen(); ucg.setColor(255, 255, 255); ucg.drawFrame(0, 0, 240, 128); } void loop() { }逻辑说明构造函数的三个参数分别是数据/命令选择脚、片选脚、复位脚。HWSPI 表示用 Arduino 硬件 SPI速度高但 MOSI 和 SCK 必须固定在 D11/D13如果你的板子引脚被占用把类名末尾换成 SWSPI硬件 SPI 的引脚约束就没了缺点是速度略慢。参数说明begin 里的 UCG_FONT_MODE_TRANSPARENT 对后续打印文本的透明背景有效这里先用它初始化。clearScreen 清空整个显示 RAM接着 setColor 设置前景色为 255,255,255后面的 drawFrame 才可预期。如果屏上没反应先量 RST 脚复位时序再看 CS 是否被正确拉低。3.4 上传超时先查屏供电Arduino 玩 UC1698 时最常见的一个现象是程序第一次烧进去了第二次开始报avrdude: stk500_recv: programmer is not responding。很多人怀疑是库或串口驱动问题其实多数是屏把 USB 转串口的电源拉垮了。背光全亮时模组电流可能到 30mA 以上如果屏和 Uno 共用一个 LDO复位电路电压被拉到临界值串口芯片就失联。遇到上传超时先拔掉屏的 VCC 线再烧录或者直接给屏单独供电。烧完断电重新插回排线再上电运行。给屏供电时BLK 引脚串电阻限制背光电流不要让背光直接吃 Arduino 的 3.3V 稳压器这个习惯能避免很多现场闪屏和重启问题。4. 电表界面落地7 段数字绘制、局部刷新与采集并发4.1 布局拆解标题栏、实时值区、累计电量区电表显示页面不需要复杂图形核心是三块顶部标题栏放当前费率或总功率标识中间放电压/电流实时值底部放累计电量。人对电表数字的阅读习惯是按位认读等宽 7 段数字比比例字体更直观而且 7 段数字能用画方块的方式实现不依赖字体库跨屏移植时少一道字体版权和编码问题。以 240×128 屏为例纵向划分如下015标题区显示“总功率”或“当前费率”1699实时值区电压和电流左右分栏100127累计电量区走字显示 kWh横向用三条线段把分区画出来视觉上就是一块完整表盘。分隔线用 drawLine 画一次后续刷新数值时不要整屏重画只更新各自区域。4.2 不依赖字体的 7 段数字绘制先定义 7 段数字的段真值表和绘制函数const uint8_t SEG_LEN 18; // 数字宽度 const uint8_t SEG_H 30; // 数字高度 const uint8_t SEG_TH 4; // 笔画厚度 const uint8_t SEG_GAP 6; // 数字间距 const uint8_t segTab[10][7] { {1,1,1,1,1,1,0}, // 0 {0,1,1,0,0,0,0}, // 1 {1,1,0,1,1,0,1}, // 2 {1,1,1,1,0,0,1}, // 3 {0,1,1,0,0,1,1}, // 4 {1,0,1,1,0,1,1}, // 5 {1,0,1,1,1,1,1}, // 6 {1,1,1,0,0,0,0}, // 7 {1,1,1,1,1,1,1}, // 8 {1,1,1,0,1,1,1}, // 9 }; void drawSeg(uint16_t x, uint16_t y, uint8_t s) { uint8_t half SEG_H / 2; switch (s) { case 0: ucg.drawBox(x, y, SEG_LEN, SEG_TH); break; // 上横 case 1: ucg.drawBox(x SEG_LEN - SEG_TH, y, SEG_TH, half); break; // 右上竖 case 2: ucg.drawBox(x SEG_LEN - SEG_TH, y half, SEG_TH, SEG_H - half); break; // 右下竖 case 3: ucg.drawBox(x, y SEG_H - SEG_TH, SEG_LEN, SEG_TH); break; // 下横 case 4: ucg.drawBox(x, y half, SEG_TH, SEG_H - half); break; // 左下竖 case 5: ucg.drawBox(x, y, SEG_TH, half); break; // 左上竖 default: ucg.drawBox(x, y half - SEG_TH / 2, SEG_LEN, SEG_TH); break; // 中横 } } void drawDigit7(uint16_t x, uint16_t y, uint8_t d) { for (uint8_t s 0; s 7; s) { if (segTab[d][s]) drawSeg(x, y, s); } }逻辑说明segTab 里的 7 个 bit 依次对应 a 到 g 七个段。绘制一个数字就是遍历七个段为 1 的段调用 drawSeg 画方块。这样不经过字符字体解析每次画段都是直接写显示 RAM速度比 print 一个字符再走字体光栅化要快。参数说明SEG_LEN 和 SEG_H 决定数字外形比例适合电表的是偏矮宽的数字18×30 的 0.6 宽高比比较接近机械计度器。SEG_TH 太小时数字在阳光下发虚太大时相邻段会连成方块4 像素在 240×128 的屏上是一个经验值。4.3 只更数值的局部刷新避免整屏闪烁电表数字每秒或每几百毫秒变化一次整屏 clearScreen 会带来明显闪烁也让 SPI 总线在刷新期间被占满。常见做法是先清掉旧数字区域再画新数字void clearNumber7(uint16_t x, uint16_t y, uint8_t digits) { ucg.setColor(0, 0, 0); // 关闭像素露出面板底色 ucg.drawBox(x, y, digits * (SEG_LEN SEG_GAP) SEG_GAP, SEG_H); } void showNumber7(uint16_t x, uint16_t y, unsigned long v, uint8_t digits) { clearNumber7(x, y, digits); ucg.setColor(255, 255, 255); // 点亮像素 for (uint8_t i 0; i digits; i) { uint8_t d v % 10; drawDigit7(x i * (SEG_LEN SEG_GAP), y, d); v / 10; } }逻辑说明清区宽度是digits * (SEG_LEN SEG_GAP) SEG_GAP比 6 个数字实际占用的宽度多出一个间距防止旧数字的笔画残留在右侧边界。先关像素再开像素中间不调用 clearScreen所以其他区域的标题框和单位符号不受影响。参数说明这里的颜色逻辑是“255 点亮、0 关闭”。如果你的屏是黑底白字把 clearNumber7 里 setColor(0,0,0) 改成 setColor(255,255,255)把 showNumber7 里 setColor(255,255,255) 改成 setColor(0,0,0) 即可。Ucglib 的颜色映射在不同偏压模组上表现相反这个宏应当在工程头文件里统一定义。4.4 刷新与电量采集共用主循环电表主循环里不能有长时间 delay否则计量芯片或 Modbus 报文会被卡住。用 millis() 做非阻塞刷新unsigned long lastTick 0; void loop() { if (millis() - lastTick 500) { lastTick millis(); unsigned long energy readEnergyReg(); // 来自计量芯片或 Modbus showNumber7(150, 104, energy % 1000000, 6); } }逻辑说明每 500ms 只重绘累计电量的 6 位数字清区和绘制一共只需几百微秒到 1 毫秒其余时间全部让给数据采集。电压电流值变化不频繁单独放一个 1s 定时器去刷避免两个刷新函数挤在同一帧里。参数说明energy % 1000000把超过 6 位的溢出去掉只显示末 6 位模拟电表计度器的末位走字。如果你的表底数很大需要显示整数部分把 digits 加大并调整 x 坐标7 段数字宽度是固定的可以直接算出整个数字串的像素宽度。5. UC1698 对比度与偏压调参以及电表工程的三个常见坑5.1 对比度寄存器与 setContrast 的调整范围Ucglib 对 UC1698 已经封装了对比度控制不需要手写底层寄存器ucg.setContrast(180);逻辑说明这个函数对应控制器内部的对比度寄存器值越大LCD 玻璃上点亮像素和熄灭像素的电压差越大显示越浓。每批模组的玻璃厚度和偏压电阻不完全一致固定写死在初始化里不是好习惯。参数说明建议从 0 开始逐步加每次调 8观察文字是否清晰。实践中 128191 是大多数屏的正常区间小于 96 通常过淡大于 224 在环境温度升高时会出现连笔和黑块。如果调到 255 仍然淡问题不在对比度而是偏压倍率或 LCD 供电电压不对需要检查模组的偏压电阻焊接。5.2 坑一镜像/颠倒不等于接线错对角线验证法在第 2 章已经提过。真正到电表界面时经常是把 Ucglib 的示例从 SSD1306 改成 UC1698屏能亮但文字反的。此时不要动接线先画一条水平线和一条垂直线ucg.drawLine(0, 64, 239, 64); ucg.drawLine(120, 0, 120, 127);如果横线在但竖线变成两截通常是页地址计数器在跨页边界时方向不对优先检查 Ucglib 类名里的分辨率参数和页列映射。如果两根线都完整但整个画面上下颠倒说明行扫描方向与玻璃装配相反需要换用带不同扫描方向配置的 Ucglib 构造类或者查看模组丝印旁的方向选择电阻。5.3 坑二白底黑字和黑底白字的颜色映射电表在夜间为了降低亮度常把显示整体反白。UC1698 的库函数不一定直接提供反白开关但用颜色对调实现同样效果白底黑字底色关像素数字开像素黑底白字底色开像素数字关像素把第 4 章的 setColor 参数抽成两个宏切换显示模式时只改宏定义然后调用一次整屏重绘。注意反白切换不要做得太频繁否则 clearScreen 和整屏重绘会造成明显闪烁。现场调试时如果发现文字变成了“黑块里的透明字”就是 FG/BG 色反了不是屏坏。5.4 坑三Arduino 上传超时与屏幕供电争夺回到第 3 章的坑这里给一个排查顺序先拔屏 VCC再点上传。上传成功后断电把屏装回再上电。如果拔屏能烧、插屏烧不了基本确定是屏和主控抢电源。电表工程如果只有一个 LDO 给 MCU 和屏同时供电背光一开MCU 复位电路电压就波动。常见做法是把背光电阻从 220Ω 加大到 470Ω 降低电流或者让屏的 VCC 走独立 3.3V 稳压器。ESP32 开发板也常遇到同类问题这类板子的 USB 转串口和 3.3V 稳压器共用 5V接上背光后上传不稳定烧录时先给屏断电是标准操作。6. 用 UC1698 做累积电量走字局部重绘与验证技巧6.1 机械计度器效果的实现思路累积电量走字和实时值刷新不一样它的变化特征是“低位频繁跳动高位偶尔进位”。整屏重绘或整串数字重绘会把高位也重画一遍高位闪烁会让现场人员误判表计异常。更好的做法是只重绘变化的段。void updateDigitOnly(uint16_t x, uint16_t y, uint8_t oldD, uint8_t newD) { for (uint8_t s 0; s 7; s) { if (segTab[oldD][s] ! segTab[newD][s]) { ucg.setColor(0, 0, 0); drawSeg(x, y, s); ucg.setColor(255, 255, 255); if (segTab[newD][s]) drawSeg(x, y, s); } } }逻辑说明旧数字和新数字的真值表逐段比较只有发生变化的段才执行“先清后画”。比如 9 变 0需要关掉中横 g其余已经点亮的段保持不变不会产生整字闪烁。这样既省 SPI 带宽也让数字切换速度接近机械计度器的瞬时跳动。参数说明这个函数要求调用方保留每一位的旧值。一般做法是维护一个 unsigned long 类型的 lastEnergy每次读到新值后从低位到高位逐个调用 updateDigitOnly最后把 lastEnergy 更新为当前值。如果新旧值位数不同先补足高位再比较。6.2 走字逻辑与验证手段走字逻辑只更新变化位但现场验收不能靠肉眼确认每一段是否正确尤其进位发生在同一帧里时。可以用一段简单的循环测耗时unsigned long t0 micros(); for (unsigned long n 0; n 1000; n) { showNumber7(150, 104, n, 6); } unsigned long dt micros() - t0;逻辑说明连续刷新 1000 帧测量整串数字重绘的总耗时。这个值除以 1000 就是单帧耗时用来判断是否满足 500ms 刷新周期。如果单帧耗时超过几十毫秒说明页面里混入了不必要的 clearScreen 或字体渲染。参数说明这个测试只测绘制耗时不测采集耗时。把电表里的真实电量值按每 100ms 递增一次喂给走字函数观察低位滚动时高位是否出现闪烁。高位闪烁说明 updateDigitOnly 的旧值缓存没更新或者某一位的段比较逻辑写成了整字重绘。UC1698 的显示 RAM 决定了它适合这种“按块局部写”的操作方式页和列的二维寻址让单段绘制只改写一个字节内的若干 bit不需要动整行整页。把颜色常量和单帧耗时上限抽到配置头文件里后续换屏型号时只改 Ucglib 类名和分辨率参数走字代码可以原样保留。本文还有配套的精品资源点击获取