【智能家居项目】裸机版本——字体子系统 | 显示子系统
🐱作者:一只大喵咪1201
🐱专栏:《智能家居项目》
🔥格言:你只管努力,剩下的交给时间!

今天实现上图整个项目系统中的字体子系统和显示子系统。
目录
- 🀄设计思路
- 🀄字体子系统
- 🃏管理层
- 🃏子系统层
- 🃏字库层
- 🀄显示子系统
- 🃏编程
- 🀄测试
🀄设计思路
在显示设备上显示字体其实也是比较复杂的,显示的字体有点阵字体,矢量字体等方式。
- 使用点阵绘制文字时:每个文字的大小一样,前后文字互不影响:

如上图所示,点阵字体中的每个字体的点阵大小都是固定的,也就是需要的像素点个数是固定的,例如8*16就是宽用8个像素点,长用16个像素点,无论是汉字,字母,数字甚至是一个标点符合,都用8*16个像素点。
- 点阵方式的字体并不连续,字体与字体之间分隔较远,看上去并不是那么美观。
- 使用Freetype(矢量字体)绘制文字时:大小可能不同,前面文字会影响后面文字:

如上图所示,矢量字体中的每个字体的大小是不一样的,根据字体的类型,是汉字还是字母甚至是标点符合,以及前一个字体的位置,会对显示的字体做适当的调整。
- 矢量字体的排列更加紧凑合理,看起来也更美观,符合我们的生活经验。
描述点阵字体:

对于普通的点阵字体,描述该字体需要:
- X、Y方向大小及原点坐标
- 每个像素点的值
所以可以用如下结构体来表示一个点阵字体:
struct dot_font
{int iX; int iY; //字体坐标int iWidth;//宽度int iHeight;//高度unsigned char* dots;//用于显示该字体的16字节数组(字模)
}
通过坐标以及长度和宽度可以确定字体的轮廓,将dots数组中的数据发送给显存,一个完整的字体就显示在屏幕上了。
描述矢量字体:

如上图所示,对于矢量字体,每个字体的大小可能不一样,前一个字体会影响下一个字体,其中有两黑点非常重要:
- 左边的黑点:当前字符的原点。
- 右边的黑点:下一个字符的原点。
下一个字符的位置和上一个字符息息相关,还有其他要素,如宽度,高度,绘制的起始点等等组合在一起才可以确定一个字符。
- 绘制起点和原点不是一个,原点是相当于是整个字符的标点,字符的位置由原点决定。
- 绘制起点也就是显示开始的位置,一般是字符的左上角。
所以可以用如下结构体来表示一个矢量字体:
typedef struct FontBitMap
{int iLeftUpX; /* 位图左上角X坐标 */ int iLeftUpY; /* 位图左上角Y坐标 */int iWidth; /* 字体宽度 */int iHeight; /* 字体高度 */int iCurOriginX; /* 原点X坐标 */int iCurOriginY; /* 原点Y坐标 */int iNextOriginX; /* 下一个字符X坐标 */ int iNextOriginY; /* 下一个字符Y坐标 */ unsigned char* Buffer;/* 字符点阵 */
}FontBitMap, *pFontBitMap;
包含矢量字体的绘制左边(左上角坐标),字体的宽度和高度,当前字体的原点,下一个字体的原点,以及一个字模数组。
现在我们要做的就是抽象出一个结构体,既能描述点阵字体,也能描述矢量字体。
- 能用来描述矢量字体的结构体必然也能够描述点阵字体。
绘制起始坐标以及宽度和高度和点阵字体中的坐标位置以及x和y方向的长度一样,当前字符原点和下一个字符原点,点阵字体也可以通过计算得到,所以无论是点阵字体还是矢量字体,都可以共用这一个结构体。

如上图,整个字体系统并不涉及到内核或者芯片,它是属于软件层面的,所以并不需要分那么多层,都放在一层中即可。
无论是点阵字体还是矢量字体,它们都必须有字库,将字符在字库中对应的数据发送给显示设备才能显示出相应字符,常见的字库有ASCII码字库,GBK字符,以及FreeType字库。
- 字库也要被描述,也要被管理起来。
🀄字体子系统
🃏管理层
先来实现对字符位图以及字库的管理。

如上图头文件中代码所示,结构体FontBitLib是用来描述一个要显示字符的位图的,每一个字符都会创建一个结构体对象,而成员中的Buffer中存放的就是该字符的字母数据,将这些数据发送给显示设备就能显示出对应字符。
结构体FontLib是用来描述字库的,本喵这里只会实现ASCII码字库,其中的成员函数有很多在这里是用不到的,但是为了符合所有字库,这里本喵仍然写了,方便以后的扩展和维护。
还有一个注册字库的函数声明和一个获取字库的函数声明,获取字库的函数有__表明该函数在另一层被调用,这样也是为了避免重复包含的问题。

如上图所示源文件代码,创建了一个用来管理字库的全局链表,以及实现了注册字库和获取字库的函数。
🃏子系统层
字库的管理已经实现了,下面该实现一下子系统层调用这些管理函数的字体系统了:

如上图所示头文件代码,提供了对字库进行一系列操作的函数声明。

如上图源文件代码所示,由于在同一时刻只能使用一个字库,所以创建了一个全局的默认字库变量,还提供了操作字库所用方法的具体实现,在初始化默认字库的时候,需要判断该字库的初始化方法是否为空,为空说明不用初始化。
🃏字库层
此时对字库的管理以及各种操作都已经实现了,但是字库还没有,所以接下来就需要实现ASCII码字库:

如上图所示是ASCII字库的头文件,只有一个增加ASCII码字库的函数声明。

如上图所示源文件中代码,包含一个ASCII码字库,这是一个全局的二维数组,字库中的数据是通过字模制作软件生成的,在本喵的文章I2C通信协议 | OLED屏中详细讲解过,有兴趣的小伙伴可以移步。
创建了全局的ASCII码字库结构体变量并进行了初始化,本喵这里的ASCII码大小是固定的,就是8*16的,所以就在函数中就直接给了定值,对于获取字符的位图函数本喵单独讲解一下:

仍然是这个图,在显示该字符的时候,是通过原点坐标来确定位置的,也就是图中坐标的黑点,这个黑点的坐标是由用户指定的,所以在函数中该坐标是已知的。
左上角的绘画起始坐标和原点在x方向上相同,在y方向上相差字体的高度,ASCII码字符中就是16,所以可以通过当前原点坐标计算出左上角坐标。
下一个字符的原点坐标,在x方向上和当前字符原点坐标相差字体的宽度,ASCII码字符中就是8,所以也可以通过当前原点坐标计算出下一个字符的原点坐标。
字符的高度和宽度是固定的,也就是8*16的,最重要的字模数组Buffer中的内容就来自前面的字库ascii_font二维数组,如果用户没有向Buffer中存放数据,那么就直接返回字库中对应的字模数据,如果用户存放了,那么就将字库中的字模数据复制到Buffer中。

如上图代码就是用来从字库中获取指定字符的位图数据的。
虽然将字体系统分为了三层,但是它们仍然属于系统层,只是在系统层中又细分出来的三层。

如上图所示便是这三小层各种的功能和互相之间的调用关系,子系统层只负责使用字库,并不关心字库的维护和管理,管理层则要做到细节处的管理,管理多个字库,但是并不用知道每个字库中的内容,字库层则需要详细实现自己所代表的字库,包括所有字模数据,以及字库结构体中的那些成员方法。
🀄显示子系统
显示子系统和设备子系统中的显示设备并不是一回事。

如上图,文字子系统会将从字体子系统中取出的点阵发送给设备子系统中的显示设备绘制点阵从而显示出来。
编码集:

如上图所示,字符串“ABC中国”使用不同的编码集在内存中的数据不一样,使用Unicode编码集中的UTF-8(左边内存窗口),每一个汉字占用3个字节,使用GB2312编码集(右边内存窗口),每一个汉字占用2个字节。
- 无论什么编码集,对ASCII码都是用一个字节表示。
编码格式:
拿常用的Unicode编码集举例,它包含三种编码格式,其中最常用的就是UTF-8编码格式:
| Unicode数值范围(16进制) | UTF-8编码方式(二级制) |
|---|---|
| 0000 0000-0000 007F | 0xxxxxxx |
| 0000 0080-0000 07FF | 110xxxxx 10xxxxxx |
| 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
| 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
第一列表示Unicode编码集支持的编码范围,第二列表示UTF-8编码格式,该编码格式中每个字节中包含有其他信息:
- 每个字节中,从高位到地位,遇到第一个0之前,有几个1就表示这个字符有几个字节共同表示,如果是
10xxxxxx,10表示当前字节和前一个字节共同表示一个字符。- 去掉表示字节个数的位以后,将剩余的位放在一起,组成的数值就是
UTF-8编码值。
如1110xxxx 10yyyyyy 10zzzzzz这三个字节表示的字符,这串二进制序列表示该字符用3个字节表示,组合成UTF-8编码值为xxxxyyyy yyzzzzzz两个字节大小。
除了UTF-8编码格式外,还有UTF-16LE,UTF-16BE等编码格式,不同的编码格式,表示同一个字符的数据也会不一样。
点阵:
从默认字库中取出字符的点阵数据,然后再选择显示设备,将字符在LCD或者OLED上显示。

如上图所示便是整个显示的过程,先确定编码集为Unicode,再确定编码格式为UTF-8,然后算出编码值,取出对应的点阵数据,最后在显示设备上显示。
🃏编程
显示子系统是在使用字体子系统和设备子系统中的显示设备,所以它并不用分很多层,只工作在显示子系统层(应用层)。

如上图,要想在显示设备上显示字符,有三要素,分别是具体的显示设备,字符的坐标,已经要显示的字符。

如上图代码便是整个显示系统中的核心代码,里面有几个被调用的函数本喵会单独拿出来讲解。
调用该函数显示字符串的时候,首先做的第一步就是获得字符的编码值,这里调用了GetCodeForStr函数来获得编码值:

如上图代码所示,专门创建一个文件encoding来处理编码值,在该函数中,可以获取指定字符不同编码集下的编码值,这里本喵就只用ASCII编码集。
使用该函数获取的是一个字符的编码值,所以对于ASCII编码集来说,直接返回该字符的ASCII码值作为该字符的编码值即可,因为ASCII编码集默认就是支持的。
得到字符的编码值以后,就要获取该编码值对应的点阵数据。

如上图,根据编码值获取点阵数据是从字体子系统中获取,该函数在ascii_font.c中已经实现了,调用默认字库中的GetFontBitMap即可从默认字库(ASCII码字库)中获得点阵数据。
点阵数据有了以后,就需要将点阵数据写入到显示设备在RAM中的显存中去:

如上图代码所示,该函数的就是将点阵数据写入到RAM中的显存中的,点阵数据是在FontBitMap对象中的,将该字符的所有像素点数据都获取到并写入到显存中。
本喵这里使用的OLED显示,所以按照OLED显示方式来分析如何获取像素点的位置:

如上图所示,OLED显示的字符是8*16的,前一页显示一个字符点阵的前八8字节,后一也显示后8个字节,每个字节中的一个比特位就是一个像素点,所以要获取的是每一个比特位中的值。

如上图代码所示,获取(iX,iY)坐标像素点的像素值时,按照OLED显示方式图,通过横坐标x确定像素点所在字节在Buffer中的地址,然后根据像素点的y坐标确定是该字节的哪个比特位,然后通过按位与和左移操作返回该像素点的值。

如上图所示,整个显示子系统的调用关系,首先调用显示子系统中的ShowTextInDisplayDevice函数去显示字符str,应用层只用关心这一个函数怎么调用,不比管它的实现,站在应用层的角度,此时就可以显示出指定字符了。
显示函数又调用显示子系统中编码集层中的GetCodeStr得到该字符的编码值,再用编码值去字体子系统中的默认字库中得到点阵数据,数据放在FontBitMap结构体对象中。
显示子系统再调用DrawBitMapOnFrameBuffer将点阵数据写到设备子系统中显示设备的RAM显存中,在这个函数内部,显示子系统会先调用GetPixelColorFromBitMap函数获取像素点的颜色数据,然后再调用显示设备自带的SetPixel方法将颜色数据写到RAM显存中。
最后会调用Flush函数将RAM显存中的数据发送到显示设备自带的显存中,至此整个显示流程就结束了。
- 在获取编码值的时候,根据不同的编码集和编码方式获得编码值,这里可以进行扩展维护。
- 在获取像素点颜色数据的时候,显示设备的显示规则不同,获取编码值的方式也就不同,这里也可以扩展维护。
🀄测试
最后就是测试显示子系统,设备子系统和字体子系统三个系统能否成功配合在OLED上显示字符了:

如上图代码所示,在测试函数中,先将字体系统中的字库初始化完毕,包括添加字库设置默认字库等等,然后再初始化显示设备,包括指定显示设备,初始化等等步骤。
- 显示字符只用调用一个函数
ShowTextInDidsplayDevice(ptDev,16,16,str)即可在作为为(16,16)处显示指定字符str。

可以看到,成功显示字符,源代码中的字符串太长,涉及到了换行,所以本喵在仅显示了A Big MiaoMi字符串,没有实现换行。
相关文章:
【智能家居项目】裸机版本——字体子系统 | 显示子系统
🐱作者:一只大喵咪1201 🐱专栏:《智能家居项目》 🔥格言:你只管努力,剩下的交给时间! 今天实现上图整个项目系统中的字体子系统和显示子系统。 目录 🀄设计思路…...
PDF中跳转到参考文献后,如何回到原文
在PDF中,点击了参考文献的超链接可以直接跳至参考文献的位置。 如果想从当前参考文献在回到正文中对应位置时,可以通过 Alt \red{\text{Alt}} Alt ← \red{\leftarrow} ← 实现。...
了解基于Elasticsearch 的站内搜索,及其替代方案
对于一家公司而言,数据量越来越多,如果快速去查找这些信息是一个很难的问题,在计算机领域有一个专门的领域IR(Information Retrival)研究如何获取信息,做信息检索。在国内的如百度这样的搜索引擎也属于这个…...
【多模态融合】TransFusion学习笔记(2)
接上篇【多模态融合】TransFusion学习笔记(1)。 从TransFusion-L到TransFusion ok,终于可以给出论文中那个完整的框架图了,我第一眼看到这个图有几个疑问: Q:Image Guidance这条虚线引出的Query Initialization是什么意思? Q:图像分支中的…...
Pyhon-每日一练(1)
🌈write in front🌈 🧸大家好,我是Aileen🧸.希望你看完之后,能对你有所帮助,不足请指正!共同学习交流. 🆔本文由Aileen_0v0🧸 原创 CSDN首发🐒 如…...
MySQL:数据库的物理备份和恢复-冷备份(3)
介绍 物理备份: 直接复制数据文件进行的备份 优点:不需要其他的工具,直接复制就好,恢复直接复制备份文件即可 缺点:与存储引擎有关,跨平台能力较弱 逻辑备份: 从数据库中导出数据另存而进行的备…...
功能比较:Redisson vs Jedis
Redis最流行的两个Java客户端库是Redisson和Jedis。Redisson提供内存中的数据网格功能,支持Redis的各种分布式对象和服务。另一方面,Jedis是一个更轻量级的产品,它缺乏其他库的某些功能。 如果你正在为Redis寻找一个Java客户端库…...
Spring web security
儅使用spring的web security時,默認會轉向自帶的spring security example page。而不會轉向error page。 TODO: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId> &l…...
SpringCloud(二)Docker、Spring AMQP、ElasticSearch
文章目录 DockerDocker与虚拟机Docker架构镜像、容器、镜像托管平台Docker架构Docker实践 Spring AMQP简单使用案例工作队列- WorkQueue发布订阅服务FanoutExchangeDirectExchangeTopicExchange 消息转换器 ElasticSearch倒排索引IK分词器IK分词拓展与停用字典 操作索引库mappi…...
7.Tensors For Beginneers - Convector Components
介绍协向量时,曾说过它们有点像 行向量, 行向量确实以某种方式代表了协向量, 这里说明一下: 协向量是不变的; 协向量组件是可变的。 协向量不依赖坐标系,协向量的组件取决于坐标系。 当我们说协向量具有组…...
直线导轨坏了可以维修吗?
直线导轨是工业自动化设备中常用的零部件,其性能和使用寿命对设备的稳定运行和产能有着直接的影响,在生产中,由于各种原因,直线导轨会出现各种问题,那么,直线导轨的维修方法究竟是怎样的呢?我们…...
Java基础--泛型详解
一、背景 java推出泛型之前,集合元素类型可以是object类型,能够存储任意的数据类型对象,但是在使用过程中,如果不知道集合里面的各个元素的类型,在进行类型转换的时候就很容易引发ClassCastException异常。 二、概念 …...
学习搜狗的workflow,MacBook上如何编译
官网说可以在MacBook上也可以运行,但是编译的时候却有找不到openssl的错误: 看其他博客也有类似的错误,按照类似的思路去解决 问题原因和解决办法 cmake编译的时候,没有找到openssl的头文件,需要设置cmake编译环境下…...
Ubuntu使用cmake和vscode开发自己的项目,引用自己的头文件和openCV
创建文件夹 mkdir my_proj 继续创建include 和 src文件夹,形成如下的目录结构 用vscode打开项目 创建add.h #ifndef ADD_H #define ADD_Hint add(int numA, int numB);#endif add.cpp #include "add.h"int add(int numA, int numB) {return numA nu…...
2) dataset, dataloader
dataset, dataloader torchvision.datasets里面集成了一些常见的数据集,例如MNIST和CIFAR10 1) Dataset 以MNIST为例,其使用方式如下 import torch import torchvision from torchvision import transformstrain_dataset = torchvision.datasets.MNIST(root=../data,trai…...
阿里云PolarDB自研数据库详细介绍_兼容MySQL、PostgreSQL和Oracle语法
阿里云PolarDB数据库是阿里巴巴自研的关系型分布式云原生数据库,PolarDB兼容三种数据库引擎:MySQL、PostgreSQL、Oracle(语法兼容),目前提供云原生数据库PolarDB MySQL版、云原生数据库PolarDB PostgreSQL版和云原生数…...
[软件工具]opencv-svm快速训练助手教程解决opencv C++ SVM模型训练与分类实现任务支持C# python调用
opencv中已经提供了svm算法可以对图像实现多分类,使用svm算法对图像分类的任务多用于场景简单且对时间有要求的场景,因为opencv的svm训练一般只需要很短时间就可以完成训练任务。但是目前网上没有一个工具很好解决训练问题,大部分需要自己编程…...
邮件注册(一)验证码发送
通过邮箱实现注册,用户请求验证码完成注册操作。 导入依赖: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency><dependency><g…...
【网络安全---sql注入(2)】如何通过SQL注入getshell?如何通过SQL注入读取文件或者数据库数据?一篇文章告诉你过程和原理。
前言 本篇博客主要是通过piakchu靶场来讲解如何通过SQL注入漏洞来写入文件,读取文件。通过SQL输入来注入木马来getshell等,讲解了比较详细的过程; 如果想要学习SQL注入原理以及如何进行SQL注入,我也写了一篇详细的SQL注入方法及…...
正点原子嵌入式linux驱动开发——TF-A移植
经过了之前的学习,除了TF-A的详细启动流程仍待更新,TF-A的使用和其对应的大致启动流程已经进行过了学习。但是当我们实际做产品时,硬件平台肯定会和ST官方的有区别,比如DDR容量会改变,自己的硬件没有使用到官方EVK开发…...
龙虎榜——20250610
上证指数放量收阴线,个股多数下跌,盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型,指数短线有调整的需求,大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的:御银股份、雄帝科技 驱动…...
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以? 在 Golang 的面试中,map 类型的使用是一个常见的考点,其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...
ssc377d修改flash分区大小
1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...
屋顶变身“发电站” ,中天合创屋面分布式光伏发电项目顺利并网!
5月28日,中天合创屋面分布式光伏发电项目顺利并网发电,该项目位于内蒙古自治区鄂尔多斯市乌审旗,项目利用中天合创聚乙烯、聚丙烯仓库屋面作为场地建设光伏电站,总装机容量为9.96MWp。 项目投运后,每年可节约标煤3670…...
Yolov8 目标检测蒸馏学习记录
yolov8系列模型蒸馏基本流程,代码下载:这里本人提交了一个demo:djdll/Yolov8_Distillation: Yolov8轻量化_蒸馏代码实现 在轻量化模型设计中,**知识蒸馏(Knowledge Distillation)**被广泛应用,作为提升模型…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
【C++】纯虚函数类外可以写实现吗?
1. 答案 先说答案,可以。 2.代码测试 .h头文件 #include <iostream> #include <string>// 抽象基类 class AbstractBase { public:AbstractBase() default;virtual ~AbstractBase() default; // 默认析构函数public:virtual int PureVirtualFunct…...
Docker拉取MySQL后数据库连接失败的解决方案
在使用Docker部署MySQL时,拉取并启动容器后,有时可能会遇到数据库连接失败的问题。这种问题可能由多种原因导致,包括配置错误、网络设置问题、权限问题等。本文将分析可能的原因,并提供解决方案。 一、确认MySQL容器的运行状态 …...
结构化文件管理实战:实现目录自动创建与归类
手动操作容易因疲劳或疏忽导致命名错误、路径混乱等问题,进而引发后续程序异常。使用工具进行标准化操作,能有效降低出错概率。 需要快速整理大量文件的技术用户而言,这款工具提供了一种轻便高效的解决方案。程序体积仅有 156KB,…...
高端性能封装正在突破性能壁垒,其芯片集成技术助力人工智能革命。
2024 年,高端封装市场规模为 80 亿美元,预计到 2030 年将超过 280 亿美元,2024-2030 年复合年增长率为 23%。 细分到各个终端市场,最大的高端性能封装市场是“电信和基础设施”,2024 年该市场创造了超过 67% 的收入。…...

