89C51驱动12864并口屏生成二维码:资源优化与实现

发布时间:2026/11/5 9:32:29
89C51驱动12864并口屏生成二维码:资源优化与实现

简介:这份资源面向51单片机初学者与嵌入式教学实践者,聚焦在89C51平台上完成二维码生成与12864并口液晶屏显示这一典型应用,解决从编码算法到屏幕驱动落地的完整链路问题。压缩包共23个文件、约209KB,以C语言源码与头文件为核心,含main.c、QR_Encode.c、12864.c等驱动实现,并附带Keil工程文件、编译中间产物与可烧录的hex文件,另有一份使用说明文本帮助快速上手。目前已有3910人学习下载,说明其在入门实践中具备一定参考价值。源码注释较为清楚,覆盖二维码编码规范、LCD初始化与点屏划线、帧缓冲更新等关键环节,读者可据此理解并口时序控制与像素矩阵映射的实现思路,并借助现成工程在Keil中直接编译调试,适合作为嵌入式显示与二维码技术的练手案例。

1. 一块 12864 并口屏上跑出二维码,这事比想象中省资源

前阵子帮人调一块 89C51 的板子,需求很朴素:把一串短文本变成二维码,直接刷到 12864 并口屏上。第一反应是「51 那点 RAM 和 Flash 扛得住吗」,实际拆下来发现,只要把 QR Code 的生成逻辑按版本和纠错等级裁到最小,再配合 12864 的页地址写入方式,整个链路是能跑通的。这个项目干的就是这件事:在 51 单片机上用 C 语言生成 QR Code 点阵,通过并口驱动 12864 液晶显示出来,附带生成驱动的源码。适合手里有 89C51 开发板、想搞明白二维码底层编码流程、又不想上 Linux 或 STM32 的嵌入式开发者。下面按「能干什么、怎么跑、怎么改」把这份源码拆开讲。

2. 项目定位与技术选型:为什么是 89C51 加并口 12864

2.1 这套组合解决的是什么问题

二维码在很多人印象里是手机扫的东西,生成端往往跑在服务器或 App 里。但工业现场、离线设备、教学实验里,经常需要一块不带操作系统的板子自己把二维码画出来,比如设备铭牌上打一个含序列号的码、实验箱上显示一段配置信息。这个项目的价值就在于把「QR Code 编码 + 点阵渲染 + 液晶驱动」三件事压进一颗 8 位单片机里,不依赖任何联网能力,上电就能出图。

技术选型上,89C51 是 8051 内核的经典型号,资源大致是 4KB 程序空间、128 字节片内 RAM,部分型号可外扩。12864 并口屏指的是 128×64 点阵的图形液晶,常见控制芯片为 ST7920 一类,支持并行数据总线写入。选并口而不是串口,是因为并口一次传 8 位,刷整屏点阵时吞吐更直接,代价是占用端口多。二维码这边,标准 QR Code 有 40 个版本、4 个纠错等级,版本越高矩阵越大,对 51 来说必须做裁剪,通常只保留版本 1 到 3、纠错等级 L 或 M,才能把码字缓冲和生成中间量塞进有限 RAM。

2.2 资源账要先算清楚

动手前我一般先算三笔账,这个项目能不能成立全看这三笔。第一笔是显示账:12864 一屏 128×64 像素,一个版本 1 的二维码是 21×21 模块,按每模块 2×2 像素放大就是 42×42 像素,屏上放得下,还能留出边距和文字区;版本 3 是 29×29 模块,放大 2 倍是 58×58,也还行。第二笔是 RAM 账:二维码生成需要一个模块矩阵缓冲区,21×21 按位存约 56 字节,按字节存要 441 字节,后者对片内 RAM 是灾难,所以必须按位压缩或者按行流式生成。第三笔是 Flash 账:编码表、掩码表、纠错码生成逻辑加起来,代码量要控制在几 KB 内,函数能复用就复用,别引入浮点和大型库。

提示:如果目标板是增强型 8051(比如带 1KB 以上片内 RAM 的型号),矩阵缓冲区可以放宽到按字节存,调试会轻松很多;标准 89C51 就老老实实按位操作。

2.3 目录与模块划分

这类项目的源码结构通常不复杂,按职责拆成几块就够维护。下面是一个合理的组织方式,具体文件名以你拿到的源码为准,但模块边界基本一致:

qr51/ ├── main.c // 主流程:初始化、生成、显示 ├── qrcode.c // QR Code 编码核心:数据编码、纠错、掩码 ├── qrcode.h // 版本/纠错等级配置、矩阵访问接口 ├── lcd12864.c // 12864 并口驱动:写命令、写数据、画点 ├── lcd12864.h // 引脚定义、坐标常量 └── font.h // 可选:少量字符点阵,用于屏上配文字

qrcode.c是重头,负责把输入字符串变成模块矩阵;lcd12864.c只关心怎么把矩阵里的一个点落到屏上;main.c把两者串起来。这种分层的好处是,换屏型只改lcd12864.c,换二维码参数只改qrcode.h里的宏,互不牵连。

3. 核心模块拆解:从字符串到屏上点阵的完整链路

3.1 数据编码:先把字符变成比特流

QR Code 的第一步是模式选择。纯数字用 Numeric 模式,字母数字混合用 Alphanumeric 模式,其他用 Byte 模式。51 上为了省事,很多实现直接走 Byte 模式,把每个字符按 8 位塞进比特流,前面加模式指示符和字符计数。下面这段是 Byte 模式编码的骨架,写法贴近这类源码的常见风格:

/* 把输入字符串按 Byte 模式写入比特缓冲,返回写入的比特数 */ static uint16_t qr_encode_byte(const uint8_t *data, uint8_t len, uint8_t *bits) { uint16_t pos = 0; /* 模式指示符:Byte 模式为 0100 */ qr_put_bits(bits, &pos, 0x4, 4); /* 字符计数:版本 1-9 的 Byte 模式用 8 位计数 */ qr_put_bits(bits, &pos, len, 8); for (uint8_t i = 0; i < len; i++) { qr_put_bits(bits, &pos, data[i], 8); } return pos; /* 当前已写入的比特数 */ }

qr_put_bits是内部工具函数,把value的低n位按高位在前追加到bits数组的pos位置,同时推进pos。这里有两个参数要盯住:模式指示符的位宽固定 4 位,字符计数的位宽随版本变化,版本 1 到 9 是 8 位,10 以上是 16 位,裁剪版一般只处理前者。写完数据后还要补终止符和填充字节,终止符最多 4 个 0,然后按 0xEC、0x11 交替填充到码字容量,这一步漏了会导致后续纠错计算对不上。

3.2 纠错码生成:GF(256) 上的 Reed-Solomon

二维码的纠错用的是 Reed-Solomon 码,在 GF(256) 有限域上做多项式除法。51 上没有硬件乘法器,实现要点是预生成指数表和对数表,用查表代替乘法。下面是一个精简的纠错码生成函数:

/* 生成 ec_len 个纠错码字,data 为数据码字,data_len 为数据码字个数 */ static void qr_rs_encode(const uint8_t *data, uint8_t data_len, uint8_t *ec, uint8_t ec_len) { uint8_t gen[32]; /* 生成多项式系数,ec_len 不超过 30 */ uint8_t rem[32] = {0}; qr_build_generator(gen, ec_len); /* 预计算生成多项式 */ for (uint8_t i = 0; i < data_len; i++) { uint8_t factor = data[i] ^ rem[0]; for (uint8_t j = 0; j < ec_len - 1; j++) { rem[j] = rem[j + 1] ^ gf_mul(gen[j], factor); } rem[ec_len - 1] = gf_mul(gen[ec_len - 1], factor); } for (uint8_t i = 0; i < ec_len; i++) { ec[i] = rem[i]; } }

gf_mul用对数表实现:exp[log[a] + log[b]],两张表各 256 字节,放 Flash 里不占 RAM。qr_build_generator根据ec_len构造生成多项式,版本 1 的 L 级纠错是 7 个纠错码字,M 级是 10 个,这些数值来自标准表,不能随手改。这里最容易翻车的是rem数组的移位方向,写反了生成的码字能通过编译但扫不出来,属于典型的「玄学」问题,后面避坑章节会细说。

3.3 矩阵布局与掩码:把码字铺进方格

码字算完后要按标准规定的蛇形路径填进矩阵,再放功能图形:三个定位图案、定时图案、格式信息、版本信息(版本 7 以上才有)。掩码有 8 种,标准要求对每种掩码计算惩罚分,选分数最低的那个。51 上算 8 遍惩罚分开销不小,常见做法是固定用一种掩码,或者只对两三种候选做比较。下面这段演示矩阵写入和掩码应用:

/* 把码字按之字形填入矩阵,matrix 按行存,1 表示黑模块 */ static void qr_place_data(uint8_t matrix[QR_SIZE][QR_SIZE], const uint8_t *codewords, uint16_t count) { int16_t bit = 0; for (int16_t right = QR_SIZE - 1; right >= 1; right -= 2) { if (right == 6) right = 5; /* 跳过定时图案列 */ for (int16_t vert = 0; vert < QR_SIZE; vert++) { for (int16_t j = 0; j < 2; j++) { int16_t x = right - j; /* 奇数列向上、偶数列向下,形成之字形 */ int16_t y = ((right + 1) & 2) ? QR_SIZE - 1 - vert : vert; if (!qr_is_function(x, y) && bit < count * 8) { matrix[y][x] = (codewords[bit >> 3] >> (7 - (bit & 7))) & 1; bit++; } } } } }

QR_SIZE是矩阵边长,版本 1 为 21。qr_is_function判断该坐标是否已被功能图形占用,占用的位置不能写数据。之字形路径的方向由列号决定,这个细节写错会导致整张码错位。掩码应用就是对非功能模块做异或,掩码公式按标准表来,比如掩码 0 是(x + y) % 2 == 0时取反。

3.4 12864 并口驱动:一个点怎么落到屏上

12864 并口屏按页组织,常见 ST7920 类芯片把 64 行分成 8 页,每页 8 行,写入时一次写一个字节对应纵向 8 个像素。画点函数要把二维码矩阵的每个模块放大成 N×N 像素块,再映射到页和列。核心写入函数大致如下:

/* 向 12864 写一个字节数据,rs=1 表示数据,rs=0 表示命令 */ static void lcd_write_byte(uint8_t dat, uint8_t rs) { LCD_RS = rs; LCD_RW = 0; LCD_DATA = dat; /* P0 口接数据总线 */ LCD_EN = 1; lcd_delay(1); /* 使能脉宽,具体延时按晶振调 */ LCD_EN = 0; } /* 在 (x, y) 画一个 scale×scale 的实心块,用于放大二维码模块 */ void lcd_draw_module(uint8_t x, uint8_t y, uint8_t scale) { for (uint8_t dy = 0; dy < scale; dy++) { for (uint8_t dx = 0; dx < scale; dx++) { lcd_set_pixel(x * scale + dx, y * scale + dy); } } }

lcd_write_byte里LCD_EN的脉宽是关键,太快屏不响应,太慢刷整屏会明显变慢,我一般按晶振频率给 1 到 2 微秒。lcd_set_pixel内部要做坐标到页地址和列地址的换算,还要处理读改写:因为一个字节管 8 行,改一个点得先把原字节读回来、改对应位、再写回去。如果屏支持页写模式,可以先把一整页的 8 行像素在 RAM 里拼好再一次性写,能省掉大量读改写开销,这是刷二维码这种整屏图形时最值得做的优化。

4. 跑起来:编译、接线与上屏验证

4.1 编译环境与工程配置

这类 51 工程一般用 Keil C51 编译,新建工程选对应芯片型号,把main.c、qrcode.c、lcd12864.c加进工程,头文件路径指到源码目录。编译选项里注意两点:一是存储模式,标准 89C51 选 Small 模式,变量默认放片内 RAM;二是优化等级,二维码生成里有不少位操作,开中等优化即可,开太高偶尔会把延时循环优化掉,导致屏时序不对。生成 HEX 后用烧录工具写到芯片,晶振按板子实际值在代码里改lcd_delay的循环次数。

# 典型编译产物与烧录前检查 # 1. Keil 中 Build 生成 qr51.hex # 2. 确认 code size 未超过芯片 Flash # 3. 确认 data 段未超过片内 RAM

编译完先看 map 文件里的 data 段占用,如果接近 128 字节,说明矩阵缓冲区或临时数组太大,得改成按位存或复用缓冲。这一步不做,烧进去大概率是上电乱码或者跑飞。

4.2 接线对照与端口分配

并口 12864 至少需要数据总线 8 根、RS、RW、EN 三根控制线,加上背光和复位。典型接法是数据总线接 P0,RS 接 P2.0,RW 接 P2.1,EN 接 P2.2。P0 口作为开漏输出,外接 10K 上拉排阻,这点别省,省了会出现写进去的数据时对时错。下面表格是常见分配,按你板子实际改宏定义即可:

信号单片机引脚说明
DB0-DB7P0.0-P0.7数据总线,需上拉
RSP2.01 数据 / 0 命令
RWP2.1本工程固定写,接低
ENP2.2使能脉冲
PSB接高并口模式选择

4.3 上电验证顺序

别一上来就刷二维码,按「屏自检 → 画点 → 画框 → 刷码」四步走。先写一个只初始化屏、清屏、在固定位置画一个实心方块的测试程序,确认接线和时序没问题;再画一个 21×21 的棋盘格,验证坐标映射;最后才接二维码生成。这个顺序能把问题定位在驱动层还是编码层,省掉大量来回烧录的时间。验证时用手机扫码 App 对着屏扫,扫不出来先看码的三个定位角是否完整、静区(码四周留白)是否够,静区不足是最常见的扫不出原因。

5. 避坑与排查:五个真实踩过的坑

5.1 屏上出图但扫不出来

现象是二维码图形看着完整,手机怎么扫都识别不了。原因多半在纠错码字算错或者掩码和格式信息不匹配,图形对但数据错。解决方法是先用一个已知正确的二维码生成工具生成同样内容的码,把矩阵逐行对比,差异集中在数据区就查 RS 编码的移位方向,差异在格式信息区就查格式信息的那 15 位有没有按 BCH 码算对。对比法比盲改快得多。

5.2 上电花屏或只显示半屏

现象是清屏后仍有残留,或者只刷了上半部分。原因通常是初始化序列不完整,或者页地址设置越界。12864 的页地址是 0 到 7,列地址 0 到 127,写超了会回绕。解决方法是把初始化序列按芯片手册逐条核对,尤其是扩展指令集和基本指令集的切换;刷屏循环里加边界判断,别让列地址超过 127。

5.3 编译通过但运行跑飞

现象是程序烧进去后看门狗复位或者直接死机。原因一般是 RAM 溢出,局部大数组把栈冲了。51 的栈很小,函数里别定义几十字节的局部数组,矩阵缓冲和码字缓冲改成全局静态变量,并且用idata或xdata明确指定存储区。编译后看 map 文件是最直接的排查手段。

5.4 刷屏慢到肉眼可见

现象是整屏刷新要好几秒。原因是逐点读改写,每个点两次总线操作。解决方法是按页缓冲:先在 RAM 里拼出一页 128 列的数据,再连续写 128 个字节,把读改写次数降到零。二维码是静态图,甚至可以只算一次矩阵,之后每次刷新直接查矩阵写页。

5.5 换一块屏就不显示

现象是同一份代码换屏后黑屏。原因是不同批次 12864 的控制芯片指令集有差异,有的默认并行模式需要拉特定引脚,有的初始化延时要求更长。解决方法是先确认屏的型号和手册,把lcd_write_byte里的延时加长试一次,再核对模式选择引脚电平。驱动层做好宏隔离,换屏只改一个文件。

6. 二次开发:把固定内容改成可配置、可扩展

拿到源码后最常做的改造,是把写死的二维码内容改成运行时可变的。我的做法是在main.c里留一个内容缓冲和长度变量,上电从 EEPROM 或串口读入,再调生成函数。生成接口设计成「输入字符串 + 版本 + 纠错等级 → 输出矩阵」,矩阵用全局缓冲,避免每次调用重新分配。下面是一个改造后的调用示例:

/* 运行时生成:内容来自串口或 EEPROM,版本和纠错等级可配 */ uint8_t payload[32]; uint8_t payload_len = uart_read_line(payload, sizeof(payload)); if (payload_len > 0 && qr_generate(payload, payload_len, QR_VER_2, QR_ECC_M)) { lcd_clear(); qr_render_to_lcd(2); /* 每模块放大 2 倍 */ } else { lcd_show_text(0, 0, "QR GEN FAIL"); /* 失败要有可见反馈 */ }

qr_generate返回成功与否很重要,内容超出版本容量时要能报错而不是静默截断。qr_render_to_lcd的放大倍数参数决定屏上占用面积,版本 2 是 25×25 模块,放大 2 倍占 50×50 像素,屏上还能放一行文字。验证改造是否成功,我习惯用「同一内容分别用本程序和电脑端工具生成,逐模块比对矩阵」这个笨办法,比反复扫码可靠。

再往深走可以做两件事:一是把纠错等级做成编译期宏,方便在容量和抗损之间取舍;二是把矩阵渲染和屏驱动彻底解耦,渲染层只输出一个「模块坐标 → 黑白」的回调,这样换 OLED 或串口屏时只写新的回调。我早年图省事把渲染和驱动写在一起,后来换屏时整段重写,从那以后每次做显示类项目都强制把「数据」和「落屏」分成两层,哪怕多写一个函数指针也认了。希望这份拆解帮到你,拿到源码先跑通屏自检那一步,剩下的就是按需改参数了。

本文还有配套的精品资源,点击获取