拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Logisim搭建GB2312汉字字库电路:从内码到点阵显示

Logisim搭建GB2312汉字字库电路:从内码到点阵显示

做了这么多年计算机组成原理相关的实验,我一直觉得“汉字显示”是比“乘法器”“ALU”更让人头疼的东西。英文和数字有ASCII,一张7位编码的表就能搞定;可一到中文,编码方式、点阵字库、区位号、内码这些概念全搅在一起,不少人在Logisim里折腾一晚上,最后连个“汉”字都点不亮。这篇就打算用Logisim把GB2312汉字字库的完整搭建过程讲透,从编码原理到点阵存储,从生成字库文件到Logisim连线显示,全部手把手走一遍。

标题里提到的“完整电路图”,我这次不会直接甩一张大图了事,而是把每一段电路的连接逻辑拆开写清楚。因为根据我的经验,字库电路的核心不是“图好看”,而是“地址算得对”。只要理解了内码到ROM地址的换算,剩下全是连线活。这篇文章适合正在做单周期CPU、数字逻辑课设,或者对GB2312编码和点阵字库感兴趣的读者——哪怕你之前完全没碰过Logisim,跟着走也能把字库跑起来。

1. 为什么要自己搭一个GB2312字库

先说说我为什么非要折腾这个。前两年带学生做单周期CPU课程设计,很多同学做到最后一步“在屏幕上显示自己的名字缩写”时卡住了。英文字母好办,做一个8x8或者16x8的ASCII字库ROM就完事;中文就麻烦了,Windows里那些字体文件是矢量字库,Logisim根本读不了,更别说还要处理GB2312编码的区位换算。

其实中文显示在硬件层面特别“笨”——它不需要你认识汉字,只需要你按照编号去查一张大表,查到这个字的“点阵图”,然后一行一行点亮LED就够了。这和现实生活里查字典很像:字典里的字是按拼音或部首排序的,每个字有个固定的页码和位置;计算机里的汉字是按GB2312的区位码排序的,每个字有个固定的“区”和“位”,根据这个坐标就能在字库里找到它对应的16x16点阵。

那为什么非要用Logisim搭一遍?我觉得有三个实际价值:

  1. 彻底搞懂GB2312编码。很多人背过“GB2312内码等于区位码加0xA0A0”,但完全不知道为什么要加、加完怎么用。搭一次电路,这个公式就再也忘不掉了。

  2. 给自制CPU/显示器模块提供可复用的汉字输出单元。Logisim里做CPU实验,最后多半要接一个显示设备。有了字库ROM,你就可以用16位内码作为输入,让电路输出对应的点阵数据,等于给你的CPU装了一个“中文显卡”。

  3. 为真实的单片机/OLED项目打基础。你要是以后玩STM32、ESP32驱动OLED屏,原理一模一样:往字库里查点阵,往屏幕上刷像素。区别只是平台不同,逻辑完全通用。

所以这篇文章虽然用的是Logisim,但学会的东西放到真实硬件上一样成立。这恐怕是很多“纯软件”教程给不了你的收获。

2. 核心原理:从GB2312编码到点阵存储

2.1 GB2312到底是怎么给汉字编号的

GB2312是一个双字节编码方案,把可显示的字符放在一个94行乘94列的大表格里。行叫作“区”,列叫作“位”。每个字符用“区号+位号”定位,比如“汉”字的区位码是2626,意思就是第26区第26位。

但是计算机里不能直接传十进制区位码,所以GB2312规定了一种“内码”表示:把区号和位号分别加上0xA0。为什么要加0xA0?因为要避开ASCII码里0x00到0x7F的可显示区域和控制字符,保证中英文混排时不会冲突。这样一来,“汉”字的区位码2626,加上0xA0A0之后,内码就是0xBABA。你可以验证一下:0x26 + 0xA0 = 0xBA,两个字节都是BA,所以很多老程序员一看到BABA就知道是“汉”字。

GB2312共收录了6763个汉字,分为两级:

  • 一级汉字3755个,放在16区到55区,按拼音排序。比如16区01位是“啊”,内码0xB0A1,这也是绝大多数人验证GB2312时用的第一个字。
  • 二级汉字3008个,放在56区到87区,按部首排序。
  • 1区到9区是各种符号、数字、拉丁字母,10区到15区暂时空着。

这个分布非常关键,因为后面算ROM地址全靠它。我们只需要关心16区到87区这72个区的汉字部分。

2.2 16x16点阵字库的存储格式

一个16x16的汉字点阵,就是用一个16行乘16列的网格来描述字形,有笔画的格子记为1,没笔画的格子记为0。这样一个字就需要256个二进制位,按8位一个字节算,正好是32个字节。

这32个字节怎么排列?最经典的HZK16字库采用“逐行存放”的方式:每个汉字占32字节,分成16行,每行2字节,第r行的两个字节存放在缓冲区偏移r2和r2+1的位置。其中第r2字节表示这一行左半部分8个像素,第r2+1字节表示右半部分8个像素。每个字节里,最高位(bit7)对应最左边的像素点。

举个例子,如果某一行左半字节是0xFF,说明这一行左边8个点全亮;如果右半字节是0x80,说明右边第1个点亮,后面7个点灭。这里的“左边第1个”就是该行第9列。

这里有个容易踩坑的地方:网上有些取模软件生成的点阵是“纵向取模”或者“先右后左”,和HZK16的排列方式不一样。如果你拿到的字库显示出来左右颠倒或者上下颠倒,通常不是电路问题,而是字节序或者位序的问题。我在第7章会专门讲怎么处理。

2.3 从内码到ROM地址的换算

现在关键来了。既然字库文件里每个汉字从16区开始按顺序存放,那么给定一个汉字的区号qu和位号wei,它在字库文件中的字节偏移就是:

offset = ((qu - 16) * 94 + (wei - 1)) * 32

公式的含义是:先算出这个汉字排在所有汉字里的第几个(从0开始),再乘以每个汉字占用的32字节。因为区号从16开始,一级汉字和二级汉字连续排列,56区会无缝接在55区后面,这个公式对全部汉字都适用。

但是在Logisim电路里,我建议ROM的数据位宽设成16位而不是8位。为什么呢?因为16x16点阵的每一行正好是16个像素,用一个16位的字来表示一行再合适不过。这样每个汉字在ROM里占用的就不是32个字节,而是16个地址——每个地址存一行。换算关系就变成了:

rom_addr = ((qu - 16) * 94 + (wei - 1)) * 16

这个乘法看起来复杂,但拆开一点都不难。我们的电路实际上要做这么几件事:

  1. 输入内码高字节,减0xA0,得到区号qu;
  2. 输入内码低字节,减0xA0,得到位号wei;
  3. 计算(qu - 16),计算(wei - 1);
  4. 前者乘94,再加后者,得到汉字序号;
  5. 序号乘16,得到ROM地址。

这样一共需要两个减法器、一个乘法器、一个加法器,再加一个乘法器(或者左移4位),电路结构非常清晰。先把这段话记住,后面第4章就是照着这个思路连线。

3. 数据准备:用HZK16或Pillow生成Logisim字库文件

3.1 方案一:读取现成HZK16字库(推荐)

我自己最推荐的方法,是直接找一个现成的HZK16字库文件。很多嵌入式开源项目里都附带这个文件,像U8g2、各种LCD例程包里基本都有。它的好处是点阵数据经过长期验证,笔画清晰,不用自己调渲染参数。

拿到HZK16之后,我们只需要用Python写一个小脚本,按区位码定位并读取点阵,再输出成Logisim的.data格式即可。下面这个脚本我实测过,逻辑很直接:

def read_hzk16(qu, wei, filepath="HZK16"): # 计算该汉字在文件中的字节偏移:每个汉字32字节 offset = ((qu - 16) * 94 + (wei - 1)) * 32 with open(filepath, "rb") as f: f.seek(offset) buf = f.read(32) rows = [] for i in range(16): left = buf[i * 2] # 行左半8个像素 right = buf[i * 2 + 1] # 行右半8个像素 row = (left << 8) | right rows.append(row) return rows # 生成整个GB2312汉字区(16区~87区)的Logisim字库文件 with open("gb2312_16.data", "w") as out: for qu in range(16, 88): for wei in range(1, 95): rows = read_hzk16(qu, wei) for row in rows: out.write(str(row) + "\n")

这段代码输出的gb2312_16.data,每行是一个十进制数,范围在0到65535之间。整个文件有72个区乘94个位乘16行,约10.8万行,正好对应ROM地址从0到108287。需要注意,55区最后几个空位在标准HZK16里也占存储空间,所以不用跳过,否则地址会错位。这个细节我一开始没注意,结果后面所有的字都偏移了,排查了大半天。

脚本里(left << 8) | right这一行的含义要理解清楚:left是这一行左边8位,把它放到16位数字的高8位;right是右边8位,放到低8位。如果某个HZK16变体是先右后左存储,就把left和right换一下。

3.2 方案二:用Pillow从系统字体渲染(备选)

如果你一时找不到HZK16,或者想用自己电脑里的字体生成字库,那可以用Pillow库把汉字渲染成位图,再转成点阵。这个方法比较灵活,但需要调整的参数多一点。核心思路是:用TrueType字体把汉字画到一张大图上,然后缩小到16x16并二值化。

from PIL import Image, ImageDraw, ImageFont def render_char(ch, font_path="C:/Windows/Fonts/simsun.ttc"): # 先渲染到64x64的大图,避免字号太小时笔画粘连 big = Image.new("L", (64, 64), 255) draw = ImageDraw.Draw(big) font = ImageFont.truetype(font_path, 48) draw.text((6, 6), ch, font=font, fill=0) # 适当偏移,让字形尽量居中 small = big.resize((16, 16), Image.LANCZOS) px = small.load() rows = [] for y in range(16): row = 0 for x in range(16): if px[x, y] < 128: row |= (1 << (15 - x)) rows.append(row) return rows

这里有几个经验点。第一,不要直接用16号字体渲染再取点,那样笔画很容易糊成一片,而且字形位置不好控制。先渲染到64x64再缩放到16x16,相当于做了个简单的抗锯齿,最后二值化出来的点阵边缘会干净很多。第二,阈值128的意思就是灰度小于128的像素都算作“有笔画”,如果你用的字体笔画比较细,可以适当把这个阈值调小。

渲染方案的好处是你可以随意换字体。缺点是TrueType字体矢量轮廓和真正的16x16像素字体在笔画布局上不完全一样,个别字可能会有1到2像素的偏移。如果只是做Logisim实验,完全够用;但如果你追求那种老式宋体点阵字的规整感,我还是建议用HZK16。

3.3 生成文件格式的几个注意事项

Logisim的ROM可以直接加载两种格式:Intel HEX(.hex)和Logisim自己的.data格式。我们这里生成的就是.data格式。关于这个格式,我踩过几个坑,提醒一下:

  • 每行只能有一个数值,不能有逗号、空格、注释。
  • 十进制、十六进制、带0b前缀的二进制都可以识别,但为了保险,我建议统一用十进制。
  • 文件最后一行之后不要留多余的空行,Windows记事本保存时如果自动加了换行符一般没问题,但如果保存成UTF-8带BOM,Logisim某些版本会报错。所以我推荐用VS Code或Notepad++来生成和检查文件。

另外,如果你只是想先做一个通关测试,不着急生成全部6763个字,可以只生成“啊”、“汉”、“中”三个字,或者干脆只生成前10个汉字。这样ROM地址位宽设置成6位就够,看起来更清爽。等全部逻辑通了,再加载完整字库。

4. Logisim电路搭建:把内码翻译成ROM地址

4.1 版本选择与工程准备

Logisim有几个常见版本,传统Logisim 2.7.1功能稳定但组件偏旧,Logisim-evolution是目前维护最活跃的分支,界面更友好,还自带中文翻译和LED矩阵组件。我的建议是直接用Logisim-evolution,后面第6章讲到动态扫描显示时,它的LED Matrix组件会省很多事。

新建工程后,打开“项目”菜单里的“电路”面板,默认有一个main电路。如果要保持思路清晰,我习惯把整个字库电路分成三个子电路:addr_calc(地址计算)、rom_storage(ROM存储)、display(显示)。不过为了这篇教程好理解,我下面还是以单电路的方式来讲,你只要按模块分区布线,效果一样。

4.2 组件清单

先把你需要的组件摆好在画布上,下面是完整清单:

组件位置/库位宽或参数用途
输入引脚 Pin x2输入/输出库8位输入内码高字节和低字节
常量 Constant x2导线库8位,值160(0xA0)参与减法
减法器 Subtractor x2算术库数据位8位内码减0xA0得到区号、位号
常量 Constant x2导线库8位,值16 和 1计算偏移
减法器 Subtractor算术库数据位8位区号减16,位号减1
乘法器 Multiplier算术库数据位8位区偏移乘94
加法器 Adder算术库数据位16位区偏移和位偏移相加
乘法器 Multiplier算术库数据位16位汉字序号乘16
ROM存储器库地址位宽17,数据位宽16存放点阵
探针 Probe导线库16位调试用
LED x16输入/输出库无查看输出

这个组件清单其实不复杂,核心就是“减法、乘法、加法”这几件事。但正因为组件少,所以每一根线的连接都值得仔细核对。

4.3 地址转换电路:一步步把内码变成ROM地址

下面把地址计算电路拆成4个阶段,每一阶段我都给出明确的接法。

第1步:内码拆分与减0xA0

把内码高字节引脚和高字节常量(0xA0)分别送入第一个减法器的A端和B端,输出就是区号qu。内码低字节同理,输出就是位号wei。

这里要特别检查减法器的输出位宽是否够用。8位减法器的输入是8位、输出也是8位时,遇到负数会显示成补码,但我们这里的情况是输入内码最小0xA1(对应区号1),减0xA0之后最小为1,不会出现负数,所以8位输出完全够。

第2步:减去汉字区起点

把区号qu接到减法器A端,常量16接到B端,输出就是(qu - 16)。注意这一步只对16区及以后的汉字有意义,如果你输入了符号区的内码(比如0xA1A1),结果会是负数,最终查出来的点阵就不对。这是正常现象,GB2312符号区我们没有做进去。

位号wei同理,接减法器减去常量1,得到(wei - 1)。

第3步:乘94加偏移

把(qu - 16)送入乘法器A端,常量94送入B端,输出就是(qu - 16) * 94。然后把结果送到加法器A端,(wei - 1)送到B端,输出就是汉字序号index。index的范围是0到6762,理论需要13位二进制,加法器输出设成16位足够。

这里有个实战技巧:乘法器在Logisim里的输出位宽默认是两个输入位宽之和。比如8位乘8位,输出就是16位。这个默认其实很好,不用手动改。只要记得加法器输出也设成16位以上,就不会出现数据被截断的问题。

第4步:汉字序号乘以16得到ROM地址

最后一步,把index送入第二个乘法器A端,常量16送入B端,输出就是ROM地址。这一步等价于把index左移4位。有的同学为了省乘法器,喜欢用移位器或者直接把index的低位拼接4个0,效果一样,但我还是建议用乘法器,因为含义最直白,出了问题也好排查。

ROM地址理论上最大是6762乘16等于108192,需要17位二进制。所以乘法器输出、以及后面ROM的地址位宽,都必须设置成17位。这个地方我见过太多人翻车:乘法器输出默认16位,结果一超过65535就溢出,汉字全乱套。

4.4 完整连线表

为了让你照着搭不出错,我干脆把这个电路画成一张“文字版连线表”,每一行是一根关键连线的去向:

源目标说明
内码高字节Pin减法器1的A端高字节
常量160减法器1的B端0xA0
内码低字节Pin减法器2的A端低字节
常量160减法器2的B端0xA0
减法器1输出减法器3的A端区号qu
常量16减法器3的B端减去16
减法器2输出减法器4的A端位号wei
常量1减法器4的B端减去1
减法器3输出乘法器1的A端qu-16
常量94乘法器1的B端乘94
乘法器1输出加法器1的A端区偏移
减法器4输出加法器1的B端位偏移
加法器1输出乘法器2的A端汉字序号
常量16乘法器2的B端乘16
乘法器2输出ROM的A端17位地址
ROM的D端16个LED(或LED Matrix)16位点阵数据

把这8行线连完,整个字库电路的“骨干”就已经完成了。下一步就是给ROM加载数据文件,然后把输出接上显示设备验证。

5. ROM配置与单行验证

5.1 设置ROM参数并加载字库

双击画布上的ROM组件,在属性面板里设置两个关键参数:Address Bit Width设为17,Data Bit Width设为16。然后找到“Image”这一项,点击后面的“Load Image…”按钮,选择我们刚才生成的gb2312_16.data文件。

加载成功后,你可以点一下菜单栏的“仿真”按钮,把输入引脚设置成想要的汉字内码,然后用探针看ROM的地址输出。我先拿“啊”字做测试,因为它是16区01位,算出来的地址应该是((16-16)*94 + 0) * 16 = 0,文件里的第一个字就是它,非常好验证。

“汉”字的内码是0xBABA,所以高位引脚设成0xBA,低位引脚设成0xBA。算一下地址:qu=26,wei=26,(26-16)*94 + 25 = 965,再乘16等于15440。你用探针看到这个数,就说明前面电路算对了。

5.2 用16个LED验证一行点阵

ROM加载后,从D端引出一根16位总线,接一排16个LED。因为有16个LED,需要把总线拆开。方法是在总线和LED之间放一个Splitter组件,把16位总线拆成16根单线,再分别连到LED的输入端。

现在ROM输出的永远是“当前地址对应的那一行”的数据。一开始地址不是0,而是“汉”字算出来的15440,这样LED显示的是“汉”字第0行的16个像素。你可以来回切换高字节引脚,看LED亮灭的变化。如果没有LED矩阵,这种“一次看一行”的方法也足够验证数据对不对了。

我个人习惯在ROM的D端再接一个Probe(探针),这样不用看LED,直接看16位数值就能和脚本输出的数据核对。比如脚本里“汉”字第一行如果算出来是十进制12345,Probe显示12345,说明ROM加载和地址计算都正确;对不上,就先查地址,再查文件。

5.3 常见验证误判

用LED验证时最容易产生一个误解:当你输入“汉”字内码,16个LED只亮了一行,看起来不像汉字,就以为电路错了。其实这不是错,而是ROM本来就只输出了一行。你要做的是循环改变“第几行”这个信息,让ROM依次输出第0行、第1行……第15行。这就引出了第6章的动态扫描。

还有一种情况是LED亮的位置明显反了,比如左右镜像。这通常是字库数据生成时bit位顺序和LED电路的实际接法不一致。解决办法是把输出总线的某几位交换,或者在脚本里对每一行的16位数据做一次位反转。用Python处理的话,就是int('{:016b}'.format(row)[::-1], 2),非常简单。

6. 动态扫描:一行一行把字刷出来

6.1 为什么需要动态扫描

现在ROM能输出任意一行的数据了,但我们要让人眼“看到”一个完整的汉字,就得在一瞬间把16行数据全部显示出来。最直接的想法是:用16个16位的寄存器,把16行全部存下来,再同时输出到16x16的LED阵列。但这样电路会非常庞大,而且Logisim里操作起来也麻烦。

工程上更常用的做法是动态扫描:让所有行的LED共用一个数据端口,每次只点亮一行对应的LED,然后快速循环从第0行扫到第15行。只要循环速度够快,人眼的视觉暂留效应就会把16次闪烁“合成”成一个完整的汉字。你手机屏幕其实也是这么刷新的,只是刷新率更高而已。

6.2 用计数器驱动行号

动态扫描的电路只需要在原电路上增加三样东西:一个4位计数器、一个加法器、一个时钟源。

计数器从0计数到15,它的输出就代表当前要显示的行号。加法器的两个输入分别是“汉字起始地址”(也就是第4章算出来的ROM地址)和“计数器行号”,输出作为ROM的新地址。这样一来,随着计数器的增长,ROM会依次输出汉字第0行的数据、第1行的数据……第15行的数据,周而复始。

在Logisim里添加Counter组件后,记得把数据位宽设为4位,然后把“Maximum Value”设为15,确保它数到15之后回到0。时钟可以选择“Clock”组件,然后接计数器的CLK端。仿真时如果发现灯闪得太快看不清,就把时钟频率调低一点;如果看到图像在闪烁、有断层感,就把频率调高一些。Logisim的时钟频率在“仿真”菜单的“Tick Frequency”里设置,一般300到500Hz是一个比较舒服的区间。

6.3 显示设备:LED Matrix还是手动LED阵列

显示部分有两种方案,取决于你用的Logisim版本。

如果你是Logisim-evolution,画布上直接添加一个LED Matrix组件,行数设16、列数设16。然后需要把ROM输出的16位数据按位拆分,接到LED Matrix对应的行数据引脚上。这个组件还支持颜色设置,我当时把默认的红色改成绿色,看起来更有“老式点阵屏”的味道。

如果你是传统Logisim 2.7.1,没有LED Matrix,那就只能手动摆16x16个LED。摆放的时候有个技巧:每行16个LED排成一条,16行之间留一点间距,然后每一行内部用Splitter把总线拆开,16行的输入分别接一个多路选择器的输出。这样比把256个LED全部单独连线要简洁得多。不过说实话,手动摆256个LED很容易头大,所以我更建议用Logisim-evolution。

6.4 动态扫描的验证效果

扫描电路搭好后,输入“汉”字的内码0xBABA,你会看到LED矩阵上稳定地显示出一个“汉”字。这时候可以再试试“啊”字的内码0xB0A1,显示的是“啊”字。一个字一个字的切换内码,就相当于在做一台“单字显示器”。

有个细节值得注意:动态扫描时,如果计数器频率太低,会看到明显的“扫描条”——同一时刻只有一行在亮,然后这一行在屏幕上往下跑。这不是电路接错,而是刷新率不够。把Tick Frequency调高之后,扫描条就会消失。

如果你觉得一行一行扫过去还是太闪烁,还有一个更“豪华”的方案:用16个16位寄存器把整个汉字的16行数据全部锁存住,然后再用16x16的LED矩阵同时输出。这样就不需要动态扫描,静态就能稳定显示。代价是电路规模会大不少,我觉得做实验没必要,但如果你想挑战一下自己,完全可以试试。

7. 常见问题与排查表

这个电路算不上复杂,但我在带学生做实验的过程中,几乎每次都会遇到下面这些问题。我整理成一个排查表,你照着对号入座就行:

现象可能原因解决办法
ROM加载后所有输出都是0文件路径错、格式错、ROM地址位宽不对先看Probe显示的地址值;确认文件是纯数字文本,且ROM的Image选项确实加载成功
输入“汉”内码,LED完全没反应内码输入引脚位宽不是8位,或者高位低位接反检查两个Pin的位宽,交换高低字节试试
显示出来的字左右颠倒点阵字节左右顺序反了在脚本里对16位数据做位反转,或者交换每行两个字节的位置
显示出来的字上下颠倒字库文件的行存储顺序是“下半部分在前”让计数器从15倒计数到0,或者调整ROM数据的行顺序
所有字都对,但整体偏移一个区块55区空位没有保留,或HZK16版本不标准确认字库文件是标准全区位版;如果是精简版,需要改偏移公式
ROM地址永远不对,数值特别大乘法器或加法器输出位宽不够,高位被截断ROM地址相关连线全部统一为17位,乘法器输出位宽要大于16位
动态扫描时画面闪烁严重时钟频率太低在Tick Frequency里把频率提高到300Hz以上
扫描条很明显,一明一暗往下滚计数器没有设置最大值,数到15之后继续往上加把Counter的Maximum Value设为15,或者用比较器让它到15自动复位

除了表格里的问题,我还想分享一个排查思路。不要一上来就盯着整块电路看,把电路拆成“地址计算”和“数据输出”两段,分别验证。地址计算段,你用Probe看最终送进ROM的地址值;数据输出段,你手动用一个Constant作为ROM地址,比如固定成0,看看输出的是不是“啊”字的第一行。这样二分定位,比从头到尾查线快得多。

另一个我很推荐的调试习惯是:先把ROM地址位宽改小,加载一个迷你字库文件(只含几个字),确保小范围逻辑正确,再换全量字库。很多人一上来就要显示“中华人民共和国”,结果全量文件10万多行,ROM加载慢不说,出了问题也很难定位。先用1到2个字跑通,后面换全量就是水到渠成的事。

8. 实操心得与扩展方向

我在实际使用中发现,这套电路最大的价值不只是显示汉字,而是它把“编码”和“硬件查表”这两个概念打通了。你写完这个字库电路之后再去看GB2312、甚至看UTF-16的编码规则,会感觉它们都只是“查表地址计算方式不同”而已。GB2312需要先减0xA0再乘94,UTF-16如果按线性排列,可能连减法器都省了。

如果你做完这个实验还想继续扩展,我建议往这几个方向走:

  1. 加入符号区支持。GB2312的1到9区还有各种标点符号,扩展电路只需要在地址计算前加一个区号判断:如果区号小于16,走符号区地址计算;否则走汉字区。ROM里也对应补充这些符号的点阵数据。

  2. 支持24x24或32x32点阵。做大字号显示时,每个汉字占的字节数变多,ROM地址的乘法系数也要跟着变。这个改起来不复杂,但会让你对“字号和存储容量”的关系有更深的理解。

  3. 把字库接到你自制的CPU上。如果你的Logisim单周期CPU已经能执行指令,试着加一条“输出汉字内码”的指令,让CPU把内码写到某个寄存器,然后由字库电路自动查表送显示。到了这一步,你就等于做了一个最小化的“中文显示终端”,很多东西都可以在这个基础上玩了。

我个人的体会是:像GB2312字库这种东西,看起来知识点很杂,又是编码又是点阵又是ROM,但真把它拆开,每一步逻辑都很朴素。只要把“内码到地址”这条主线抓在手里,剩下的都是水到渠成的事。希望这篇教程能帮你少走几步弯路,有没讲透的地方,欢迎按你自己的实验环境再调一调。

返回列表