简介:基于Qt/C++开发的单片机点阵取字模软件,面向嵌入式、自动化、电子信息等专业的学生与开发者,旨在解决LED点阵屏字符与图形取模繁琐的问题。软件提供可视化点阵编辑界面,可将汉字、字符或图形快速转换为单片机可用的字模数据,并支持格式调整与导出,适合课程设计、毕业设计及项目二次开发。资源共78个文件,以SVG矢量图标、QSS样式表、CPP源文件、UI界面文件、QRC资源文件为主,包体仅294KB,目录结构简洁清晰,便于定位源码、界面与资源配置。项目代码已经过测试运行,功能稳定,曾获导师认可,答辩评审分95分,附详细文档,适合需要完整参考实现的人群。目前已有84人学习,可放心下载研究。
1. 点阵取字模为什么值得用Qt C++重写一遍
单片机显示驱动开发里,取字模是流程中特别容易被卡住的一环。51单片机、STC这类平台的RAM通常只有几百字节到几KB,OLED或LCD上要显示中文时,系统内存带不动字库,可行方案就是把每个文字提前拆成点阵字节写进Flash。市面上的取模工具版本普遍老旧,扫描方式经常和具体屏的驱动芯片对不上,默认输出的字节序也要手工去改,改错一个位,屏上显示的就是镜像字或乱码。用Qt C++实现一个点阵取字模工具,本质是把字体渲染、逐像素扫描、字节拼接和代码导出固化成一条可配置的流水线,能针对自家屏的参数随时调整,也方便扩展批量取模、预览回画这类功能。这篇文章从取模原理讲到Qt C++落地实现,再给出写入单片机前的验证思路,适合正在写显示驱动、被字模坑过的工程师。
2. 字模结构与取模方向:先弄清字节里每一位的含义
2.1 16×16点阵与32字节数组的对应关系
单片机取模最常见的场景有两类:ASCCII字符用8×8或8×16,汉字用16×16。16×16点阵包含256个像素点,8个点合并成一个字节,所以总字节数是32。驱动代码里定义字库时,最常见的写法是这样:
const unsigned char code font16x16[][32] = { // 每个汉字占32字节 {0x00, 0x04, /* ... */ }, };code关键字是51单片机的常量段修饰符,STM32平台上则用const uint8_t。这里无论用哪个关键字,[32]这个容量都来自16×16点阵的结构,不能随意改动。如果某天发现数组长度变成33或34,多半是字号设置出了问题,而不是编译器多给了空间。
点阵字节在数组里的排列取决于取模工具如何把“行”切分。横向取模(逐行式)的逻辑是:每一行16个点拆成左右两个字节,左边字节表示第0~7列,右边字节表示第8~15列,再从第0行开始自上而下扫完16行。于是有了下面这张固定的映射表:
| 字节索引 | 屏幕行 | 覆盖的列范围 | 含义 |
|---|---|---|---|
| 0 | 第0行 | 第0~7列 | 左上角字节 |
| 1 | 第0行 | 第8~15列 | 右上角字节 |
| 2 | 第1行 | 第0~7列 | 第二行左半 |
| 3 | 第1行 | 第8~15列 | 第二行右半 |
| … | … | … | … |
| 30 | 第15行 | 第0~7列 | 最后一行左半 |
| 31 | 第15行 | 第8~15列 | 最后一行右半 |
如果驱动芯片是SSD1306这类OLED,屏幕按8像素一页的方式组织数据,显示汉字时通常改用纵向(逐列式)取模:以一列为单位,从上到下8个点合成一个字节,再从左到右推进。同一套字库,横向和纵向取出来的字节序列完全不同,写入驱动后的显示结果自然也不同,所以第一步就得确认屏幕控制器的数据组织方式。
2.2 横向、纵向与逆向取模:方向决定字节顺序
取模方向直接决定字节在数组里的先后顺序,这也是网上现成工具最容易出错的地方。常见的扫描方式需要从三个维度确认:按行还是按列、从左还是从右、从上还是从下。我一般把选项按下面的表拆开:
| 取模方向 | 字节生成顺序 | 适用面 |
|---|---|---|
| 逐行正向 | 从左到右,自上而下 | 并口LCD、TFT、带字库模块 |
| 逐行逆向 | 从右到左,自上而下 | 数据线接法反向的屏 |
| 逐列正向 | 从上到下,自左而右 | SSD1306、ST7567等OLED |
| 逐列逆向 | 从下到上,自右而左 | 面板旋转或镜像显示 |
一个常见的低级错误,是把取模方向和汉字编码顺序混为一谈。取模方向描述的是“字节内位和字节间顺序”,汉字编码顺序描述的是“字库索引方式”,两者互不影响。比如用GB2312索引,“中”和“国”对应各自的32字节,与取模方向无关;同一个字在不同取模方向下,只是32字节的内容和排列不一样,字与字的相对编号不会变。
2.3 字节内的位序选择:MSB在前还是LSB在前
字节排列方向之外,还有一个更隐蔽的位序问题。逐行取模时,一行拆成两组8像素点,最左边那个点对应的是bit7还是bit0,由驱动芯片读取数据时的位序决定。
多数屏幕驱动是高位在前,即最左点对应0x80。用Qt做取模时,从QImage里读到的像素坐标是绝对的(x, y),需要自己把坐标换算进字节里的位:
// pixel为0表示黑点(笔画),col为列坐标,范围0~7 if (pixel == 0) { byte |= (0x80 >> col); // 高位在前:最左点落在bit7 }0x80 >> col就是高位在前的写法;如果目标屏要求低位在前,把它换成1 << col即可。位序这个参数最好在界面里做成选项,因为在实物屏上错了,表现就是左右镜像,而且不容易从数组内容上看出来。
2.4 阴码与阳码:背景像素要不要输出
字节里每一位的含义也有两种约定。笔画所在的点置1、背景置0,叫阳码;反过来笔画置0、背景置1,叫阴码。OLED和TFT常见反色显示需求,某些驱动代码里也会直接做~buf[i]取反,这时如果取模工具输出的已经是阴码,叠加到驱动里就会整屏反转。
取模工具里需要提供阴码/阳码切换,实现上其实就是输出前对每个字节做一次按位取反。这个操作可以放到字节拼接完成后统一处理,不要把阴码阳码的逻辑散落在扫描循环里,否则后面加新功能时很容易漏改一处。选项名可以明确写成“输出反色数据”,避免用户不知道阴码阳码分别是什么意思。
3. 用Qt C++把汉字画成点阵再转成字节数组
3.1 用QFont和QImage得到无抗锯齿的点阵位图
取字模的起点是获得一张“字形的黑白位图”。自己解析TrueType轮廓不划算,Qt的字体引擎已经做完了这个工作:QFont负责选字形,QPainter负责绘制,QImage负责承载像素。真正需要操心的不是如何描述汉字,而是如何让字体引擎不要“美化”输出:关闭抗锯齿、关闭亚像素渲染、用像素尺寸而不是磅值。
我把渲染函数封装成下面这种形式,界面和批量取模都复用它:
#include <QImage> #include <QPainter> #include <QFont> QImage renderGlyph(const QString &ch, int width, int height) { QImage image(width, height, QImage::Format_Mono); // 1bit/像素,非黑即白 image.fill(1); // 背景填充为白 QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing, false); painter.setRenderHint(QPainter::TextAntialiasing, false); QFont font(QStringLiteral("SimSun")); // 取模用中文字体 font.setPixelSize(height); // 像素尺寸,严格等于点阵高 font.setStyleStrategy(QFont::NoAntialias); painter.setFont(font); painter.drawText(image.rect(), Qt::AlignCenter, ch); painter.end(); return image; }这里有两个关键点。QImage::Format_Mono把每个像素压缩成1位,后续读像素时不会出现灰度过渡;setPixelSize按像素设置字号,保证字形尺寸和点阵宽高严格一致。如果改用font.setPointSize,最终位图高度会按屏幕DPI换算,16号的字在某些高分屏上可能渲染成18或20像素,数组长度随之变化。
image.fill(1)的含义也值得说一句:Format_Mono下1表示白色背景,0表示黑色笔画。后续取模时,判断像素为0的位置才是有效点。如果程序跑在不同的显示后端上,建议先打印image.pixel(0,0)确认黑白定义,有的平台对单色位图做了颜色反转。
3.2 按扫描方向逐位读取并拼装字节
拿到黑白位图后,原理就很直接了:逐行或逐列扫描像素,按上一章确定的规则把位组装成字节。逐行扫描的完整实现如下:
QByteArray makeRowMajor(const QImage &img, int width, int height) { QByteArray data; data.reserve((width / 8) * height); for (int y = 0; y < height; ++y) { for (int x = 0; x < width; x += 8) { quint8 byte = 0; for (int bit = 0; bit < 8; ++bit) { QRgb c = img.pixel(x + bit, y); if (qRed(c) == 0) { // 黑点为笔画 byte |= (0x80 >> bit); // MSB在前 } } data.append(static_cast<char>(byte)); } } return data; }这个函数对16×16输入会返回32字节。循环的逻辑是:外层按行,内层先按8像素分组,第三层再逐位判断。0x80 >> bit对应高位在前,最左像素落在bit7,最右像素落在bit0。如果屏幕驱动要求低位在前,把这句改成byte |= (1 << bit)。
3.3.1 大尺寸点阵下的像素读取性能
img.pixel()每次调用都会做坐标到内存地址的换算,16×16时影响不大,但点阵放大到32×32或48×48后,批量取模的速度会明显下降。这时建议改用img.bits()直接读位图内存:
bool isBlackFast(const QImage &img, int x, int y) { const uchar *bits = img.bits(); int byteIndex = y * img.bytesPerLine() + x / 8; return (bits[byteIndex] & (0x80 >> (x % 8))) == 0; }注意这里要用bytesPerLine()计算行偏移,不要自己按width / 8去算。QImage内部为了内存对齐,每行字节数可能比实际数据多出几个填充字节,写死步长会在图片宽度不是8的倍数时读到错位。
3.3 把字节数组格式化成可粘贴的C源码
取模出来的字节最后要进单片机工程,格式化成C数组是工具的收尾环节。输出格式需要支持51单片机的code关键字和ARM平台的const,我一般把数组定义写成模板函数:
QString formatArray(const QByteArray &data, const QString &typeName) { QString out; out += QString("const unsigned char %1[] = {\n").arg(typeName); for (int i = 0; i < data.size(); ++i) { out += QString("0x%1") .arg(static_cast<quint8>(data[i]), 2, 16, QLatin1Char('0')); if (i != data.size() - 1) out += ", "; if ((i + 1) % 8 == 0) out += "\n"; } out += "\n};\n"; return out; }参数2, 16, QLatin1Char('0')的含义是:最小宽度2位、十六进制、左边补0。有个容易踩的坑:QString::arg接收int,如果直接传data[i],有符号char会被转成负数,输出变成0xffffff80这种8位十六进制,整个数组就废了,所以必须先static_cast<quint8>回无符号类型。
3.4 字体选择与覆盖范围对取模结果的影响
取模工具的实用性很大程度上由字体决定。系统字体列表可以通过QFontDatabase::families()枚举,我建议在界面里放一个“仅显示中文字体”的过滤条件,默认选中宋体或黑体。等宽字体对ASCII和数字的取模更整齐,显示英文时可以切换。
中文字体的覆盖范围也有坑:部分精简字体的汉字数量只有几千个,生僻字渲染出来是空框,取模成功但屏幕显示空白,排查起来很费劲。取模前最好把目标字集和字体覆盖范围做一次比对,或者直接提醒用户换成全字库字体,这样至少不会出现“取模能过、屏幕不显示”的诡异问题。
4. 界面与参数面板:让取模工具适配不同屏幕驱动
4.1 控件布局:参数区、预览区、代码区的分工
取模工具的界面不宜复杂,核心区域就是三块:左侧参数区、中间预览区、下方代码区。参数区用一组QComboBox和QSpinBox纵向排放,从上到下依次是字体、字号、取模方向、位序、阴码阳码。
字号用QSpinBox并允许单步1像素调整,范围设置在8到48比较合理。取模是像素级操作,字号跨度太大会让预览区频繁重绘,拖慢交互。取模方向用QComboBox枚举“逐行、逐列、逆向”几个选项,切换后立即重算当前字符的字模并刷新界面。
4.2 信号槽联动与实时刷新
界面的价值在于即时反馈。不要在用户点“取模”按钮后才刷新,应该在QLineEdit的textChanged信号里触发更新。刷新做两件事:把当前字符重新渲染成位图显示在预览区,同时更新下方代码区的内容。
void MainWindow::onTextChanged(const QString &text) { if (text.isEmpty()) { return; } const QChar ch = text.right(1).at(0); const int sizeVal = ui_ ->sizeSpinBox->value(); QImage img = renderGlyph(QString(ch), sizeVal, sizeVal); ui_ ->previewLabel->setPixmap(scaleForPreview(img)); updateCodeArea(img); }scaleForPreview不能直接调用QPixmap::fromImage后拉伸,那样会产生平滑过渡的假象,看不出真实点阵边界。更好的做法是按像素放大,比如每个原始点画成8×8的小方格,再叠加网格线。这样预览区看到的就是和最终显示一致的像素块,比抗锯齿渲染直观得多。
4.3 批量取模、去重与进度反馈
批量取模的目标是把一段文本一次性转成多组32字节。这里要处理两个实际问题:字符去重和进度反馈。去重是因为文本里重复的字没必要反复取模,按字符顺序去重后直接生成数组,能节省不少Flash空间。进度反馈用QProgressDialog,处理长文本时保持UI响应:
QProgressDialog progress(tr("正在取模..."), tr("取消"), 0, chars.size(), this); progress.setWindowModality(Qt::WindowModal); for (int i = 0; i < chars.size(); ++i) { if (progress.wasCanceled()) { break; } QImage img = renderGlyph(chars[i], sizeVal, sizeVal); glyphMap.insert(chars[i], makeRowMajor(img, sizeVal, sizeVal)); progress.setValue(i + 1); QCoreApplication::processEvents(); }processEvents()手动处理事件队列里待处理的事件,让进度条能及时刷新,不然批量取模时窗口会像卡死一样。批量输出除了字模数组本身,还要同时生成“字符索引映射”,单片机侧才能知道第几个字模对应哪个字符。常见做法是维护下面这个结构体:
struct GlyphItem { QString ch; // 字符本身 QByteArray data; // 取出的字模字节 };导出后,驱动侧遍历这个数组,比对字符编码,找到匹配项后直接拷贝32字节到显存缓冲区。
4.4 按目标单片机平台切换导出模板
不同编译器和单片机的常量关键字不一样。51单片机用unsigned char code font[][32],STM32常用const uint8_t font[][32]。界面里用一个QComboBox选择目标平台,导出时替换数组声明和头文件包含。声明好格式模板后,后续新增平台只改模板字符串,不碰取模逻辑,这是把工具沉淀下来的关键一步。
5. 字模进单片机前,先在PC端回画验证一遍
5.1 把32字节数组反向渲染回点阵图像
字模烧进单片机后出问题,定位成本比PC端高得多。取模工具输出前完全可以在PC端做一次“回画”:把生成的32字节按同样的扫描方式还原成像素点,刷新到预览区。如果回画结果和原始字符一致,说明取模链路没有坏。
回画是取模的逆运算,核心代码:
QImage drawBack(const QByteArray &data, int width, int height) { QImage img(width, height, QImage::Format_Mono); img.fill(1); int idx = 0; for (int y = 0; y < height; ++y) { for (int x = 0; x < width; x += 8) { quint8 byte = static_cast<quint8>(data[idx++]); for (int bit = 0; bit < 8; ++bit) { if (byte & (0x80 >> bit)) { img.setPixel(x + bit, y, 0); } } } } return img; }把回画图和渲染原图做像素级对比,能立刻发现取模方向设置和位序设置的问题。逐列取模模式下的回画逻辑要把外层循环换成列,内层改成行,参数不一致时回画会直接花屏,比人眼查数组高效得多。这个验证步骤应该做成界面上的一个“校验”按钮,一键把两个QImage逐像素比较,返回差异点坐标。
5.2 三个高频坑与排查位置
实际项目中字模出错最常见的位置就三个:
- 数组元素数量不匹配。16×16必须32字节,字号设置成17后,数组就会变成33字节以上,驱动按32字节拷贝就会整体错位。检查时先数输出数组长度,再对照点阵尺寸。
- 编码不一致。取模工具输出时用UTF-8保存,单片机编译器按GB2312索引字符串,首字符对应关系就乱了。导出选项里必须明确编码格式,取模端和编译端保持一致。
- 显示方向反转。竖屏和横屏切换场景下,不要在驱动里逐像素翻转,那会浪费运行期资源。直接在取模工具里选逆向取模,把反转交给离线流程处理。
5.3 把工具参数固化成配置文件,减少团队返工
如果项目里有多块不同参数的屏幕,这个Qt工具最终会自然演进成一个小库:字体列表、取模方向、导出模板都存进一个配置文件,不同项目组共享同一个可执行文件,避免每个人手里都留着一份不同参数的工具。源码和文档一起交付后,新同事改参数时只看配置不看代码,也不容易把取模方向改错。这个配置化的思路,比在代码里堆一堆硬编码常量要省心得多。
本文还有配套的精品资源,点击获取