当前位置: 首页 > news >正文

第二十一章 ESP32S3 IIC_OLED 实验

        本章学习使用 OLED 液晶显示屏,在开发板上预留了 OLED 模块接口,需要准备一个 OLED 显示模块。下面一起来点亮 OLED,并实现 ASCII 字符的显示。本章分为如
下几个小节:
21.1 OLED 简介
21.2 硬件设计
21.3 程序设计
21.4 下载验证

21.1 OLED 简介

        在介绍OLED之前,建议在B站上搜索看下OLED的工作原理,对下面学习有一定帮助。OLED(Organic Light-Emitting Diode,有机发光二极管)是一种​​半导体发光技术​​。它通过在电场驱动下,使非常薄的有机材料涂层发光来实现显示。与LCD(液晶显示屏)不同,OLED的每个像素都能​​主动发光​​,无需独立的背光层。
OLED如何工作?OLED的核心是夹在正负电极之间的​​有机材料薄膜层​​。当你施加电压时:

  • 注入​​:阴极注入电子,阳极注入“空穴”(可视为正电荷)。
  • 迁移与复合​​:电子和空穴在有机材料层中相向迁移,并在​​发光层​​相遇、复合。
  • 发光​​:复合过程释放能量,激发发光分子,使其发出特定波长的​​可见光​​。光的颜色由有机材料的种类决定。

        主要类型​​, 根据驱动方式,OLED主要分为:

  • AMOLED (主动矩阵OLED)​​:每个像素都有独立的TFT(薄膜晶体管)控制,​​更省电​​,​​刷新率更高​​,适合​​大尺寸、高分辨率​​屏幕(如高端手机、电视)。
  • ​​PMOLED (被动矩阵OLED)​​:通过扫描行列地址控制像素,​​结构简单、成本低​​,但​​功耗较高​​,更适合​​小尺寸​​屏幕(如早期MP3播放器、穿戴设备)

        下面对比OLED和LCD差别:

特性维度​

​OLED 特点​

​与传统LCD的主要区别​

​​发光原理​​

​自发光​​,每个像素是独立的微小光源

LCD需要​​背光源​​,通过液晶分子调制背光来显示图像

​​厚度与重量​​

​更薄更轻​​(厚度可小于1毫米),结构简单

有背光模组和液晶层,相对更厚更重

​​对比度与黑色表现​​

​近乎无限的对比度​​,黑色区域像素可完全关闭,实现​​纯黑​

背光始终开启,黑色实为深灰色,对比度有限

​​响应速度​​

​极快​​(微秒级),远超LCD,动态画面无拖影

毫秒级响应,快速运动画面可能出现拖影

​​视角​​

​广视角​​(通常可达170度),侧面观看色彩亮度衰减很小

视角相对较窄,侧面观看可能出现色彩失真和亮度下降

​​柔性设计​​

​可弯曲​​,可用于柔性屏、可折叠屏等创新形态

通常为刚性,难以弯曲

​​能效​​

​更省电​​,黑色像素不工作;但显示大面积高亮画面时功耗可能较高

背光持续工作,功耗相对固定

​​寿命与烧屏​​

​有机材料会随时间老化​​,长时间静态画面可能留下残影(烧屏)

寿命通常更长,不易烧屏

​​制造工艺与成本​​

​工艺复杂​​,尤其大尺寸屏幕,目前​​成本较高​

技术成熟,​​大尺寸成本相对较低​

表 21.1.1 OLED和LCD的对比

        本章使用是中景园电子的 0.96 寸 OLED 显示屏,该屏有以下特点:

  • 0.96 寸 OLED 有黄蓝,白,蓝三种颜色可选;其中黄蓝是屏上 1/4 部分为黄光,下 3/4 为蓝,而且是固定区域显示固定颜色,颜色和显示区域均不能修改;白光则为纯白,也就是黑底白字,蓝色则为纯蓝,也就是黑底蓝字;
  • 分辨率为 128*64;
  • 多种接口方式:OLED 裸屏总共种接口包括6800、8080 两种并行接口方式、3 线或 4 线的串行 SPI 接口方式、 IIC 接口方式(只需要 2 根线就可以控制 OLED ),这五种接口是通过 屏上的 BS0~BS2 来配置的;
  • 中景园电子开发了两种接口的 Demo 板,接口分别为七针的 SPI/IIC 兼容模块,四针的 IIC 模块。

七针SPI/IIC 接口如下图所示:  

图 21.1.1 七针SPI/IIC 接口(正反)实物图

七针SPI/IIC 模块接口定义:

管脚说明
GND电源地
VCC电源正(3~5.5V)
D0OLED 的 D0 脚,在 SPI 和 IIC 通信中为时钟管脚
D1OLED 的 D1 脚,在 SPI 和 IIC 通信中为数据管脚
RESOLED 的 RES#脚,用来复位(低电平复位)
DCOLED 的 D/C#E 脚,数据和命令控制管脚
CSOLED 的 CS#脚,也就是片选管脚

表 21.1.2 七针OLED模块接口说明

七针SPI/IIC 反面接口方式设置说明:

接口方式

说明
IICR1、R4、R6、R7、R8位置焊接电阻
4线SPIR3、R4位置焊接电阻
3线SPIR2、R3位置焊接电阻

表 21.1.3 七针OLED模块接口方式说明

四针IIC 如下图所示:

图 21.1.2 四针SPI/IIC 接口实物图

四针IIC 模块接口定义:

管脚说明
GND电源地
VCC电源正(3~5.5V)
SCLOLED 的 D0 脚,在 IIC 通信中为时钟管脚
SDAOLED 的 D1 脚,在 IIC 通信中为数据管脚 

表 21.1.4 四针OLED模块接口说明

        由于手头只有七针 0.96 寸 OLED 显示屏,因此本章先做 SPI实验,然后使用电烙铁焊接电阻R1、R4、R6、R7、R8修改为IIC接口方式做做IIC实验。

21.2 0.96 寸 OLED 驱动 IC

        本屏所用的驱动 IC 为 SSD1306,其具有内部升压功能;所以在设计的时候不需要再专一设计升压电路,当然了本屏也可以选用外部升压,具体的请详查数据手册。SSD1306 的每页包含了 128 个字节,总共 8 页,这样刚好是 128*64 的点阵大小。

21.2.1  0.96 寸 OLED 模块使用方法

        开发板配套的驱动接口采用 8*2 的 2.54 排针与外部连接,总共有 16 个管脚,在 16 条线中,只用了 15条,有一个是悬空的。15 条线中,电源和地线占了 2 条,还剩下 13 条信号线。在不同模式下,需要的信号线数量是不同的,在 8080 模式下,需要全部 13 条,而在该模块的 IIC 模式下,需要 4条线,分别为 OLED_D0(SCL)、 OLED_D1和 D2(SDA)、 OLED_DC、 OLED_RST。在 IIC模式下, OLED_D0 是作为 IIC 的 SCL 线, OLED_D1 和 D2 连接一起作为 IIC 的 SDA线,而OLED_DC是作为 SA0,用于设置 IIC器件地址, OLED_RST是复位线, RST上的低电平,将导致 OLED 复位,在每次初始化之前,都应该复位一下 OLED 模块。

图 21.2.1.1 开发板 OLED 模块原理图

        要进行 IIC 通信,首先得知道器件地址, OLED 器件地址是 7 位的,具体格式如下表所示。

Bit76543210
Symbol011110SA0R/W
固定部分可变部分读写位
器件地址

表 21.2.1.1 OLED 设备地址

        OLED 模块的主控芯片为 SSD1306, 从上表可以知道, SSD1306 器件地址由两部分组成,一部分就是“固定部分”即“011110”;另一部分就是“可变部分”即 SA0 引脚,在程序上会让该引脚输出低电平,所以该位为“0”。最终可得到, SSD1306 器件地址为“0111100”即 0x3C。读操作地址就为 0x79,即 0111 1001;写操作地址就为 0x78,即 0111 1000。

21.2.2 硬件驱动接口模式
  • 8080 并口模式

        首先介绍一下模块的 8080并行接口, 8080并行接口的发明者是 INTEL,至今仍在许多嵌入式显示和外设控制领域广泛应用,使得 MCU 可以快速的访问 OLED。下面是一个快速了解其核心特性的表格,然后是更详细的说明。

特性维度

说明

​名称来源​

源自Intel 8080微处理器的总线协议

​数据总线​

8位、16位或18位双向数据线(现代扩展,原始8080为8位)

​关键控制信号​

CS(片选)、RD(读)、WR(写)、D/C或RS(数据/命令选择)

​通信方式​

并行、异步、半双工

​主要应用​

中小尺寸LCD/OLED显示屏

、打印机、外部存储器扩展(如RAM、ROM)

表 21.2.2.1 8080并行接口说明

        8080 接口方式需要如下一些信号线:
CS: OLED 片选信号。
WR:向 OLED 写入数据。
RD:从 OLED 读取数据。
D[7: 0]: 8 位双向数据线。
RST(RES):硬复位 OLED。
DC:命令/数据标志(0,读写命令; 1,读写数据)。
模块的 8080 并口写的过程为:先根据要写入的数据的类型,设置 DC 为高(数据) /低(命令),设置 WR 起始电平为高,然后拉低片选,选中 SSD1306,接着在整个读时序上保持RD 为高电平,然后拉低 WR 的电平准备写入数据,向数据线(D[7:0])上输入要写的信息;拉高WR,这样得到一个 WR 的上升沿,在这个上升沿,使数据写入到 SSD1306 里面;SSD1306 的 8080 并口写时序图如图 21.1.1.1 所示:

图 21.2.2.1 8080 并口写时序图

        模块的 8080 并口读的过程为:先根据要写入的数据的类型,设置 DC 为高(数据) /低(命
令),设置 RD 起始电平为高,然后拉低片选 CS 信号,选中 SSD1306,接着在整个读时序上保持 WR 为高电平,然后类似写时序,同样的,在 RD 的上升沿,使数据锁存到数据线(D[7:
0])上; SSD1306 的 8080 并口读时序图如图 21.1.1.2 所示:

图 21.2.2.2 8080 并口读时序图

        SSD1306 的 8080 接口方式下,控制脚的信号状态所对应的功能如下表所示:

表 21.2.2.2 控制脚信号状态功能表

        在 8080 方式下读数据操作的时候,有时候(例如读显存的时候)需要一个假读命( Dummy Read),以使得微控制器的操作频率和显存的操作频率相匹配。在读取真正的数据之前,由一个的假读的过程。这里的假读,其实就是第一个读到的字节丢弃不要,从第二个开始,才是真正要读的数据。
一个典型的读显存的时序图,如下所示:

图 21.2.2.3 读显存时序图

        可以看到,在发送了列地址之后,开始读数据,第一个是 Dummy Read,也就是假读,从第二个开始,才算是真正有效的数据。并行接口模式就介绍到这里。

  • SPI 模式

        代码同时兼容 SPI 方式的驱动,如果你使用的是这种驱动方式,则应该把代码中的宏 OLED_MODE 设置为 0,但对于硬件,则需要查看 PCB 背面的电阻设置以确定当前使用的是否为 SPI 模式:

#define OLED_MODE 0 /* 0: 4 线串行模式 */

        接下来介绍一下 4 线串行(SPI)方式, 4 先串口模式使用的信号线有如下几条:
CS: OLED 片选信号。
RST(RES):硬复位 OLED。
DC:命令/数据标志( 0,读写命令; 1,读写数据)。
SCLK:串行时钟线。在 4 线串行模式下, D0 信号线作为串行时钟线 SCLK。
SDIN:串行数据线。在 4 线串行模式下, D1 信号线作为串行数据线 SDIN。
模块的 D2 需要悬空,其他引脚可以接到 GND。在 4 线串行模式下,只能往模块写数据而
不能读数据。
在 4 线 SPI 模式下,每个数据长度均为 8 位,在 SCLK 的上升沿,数据从 SDIN 移入到SSD1306,并且是高位在前的。 DC 线还是用作命令/数据的标志线。在 4 线 SPI 模式下,写操作的时序如图 21.1.1.4 所示:

图 21.2.2.4 4线SPI 写操作时序图

        4 线串行模式就为大家介绍到这里。其他还有几种模式,可以参考 SSD1306 的数据手的介绍,资料放到“开发板资料 A 盘->7,硬件资料\3,液晶资料\OLED 资料\SSD1306-Revision 1.1(Charge Pump).pdf”,如果要使用这些方式,请大家参考该手册并自行实现相应的功能代码。

21.2.3 OLED 显存

        接下来,介绍一下模块的显存, SSD1306 的显存总共为 128*64bit 大小, SSD1306 将这些显存分为了 8 页,不使用显存对应的行列的重映射,其对应关系如表 21.1.2.1 所示:

表 21.1.3.1 SSD1306 显存与屏幕对应关系表

        可以看出, SSD1306 的每页包含了 128 个字节,总共 8 页,这样刚好是 128*64 的点阵大小。当 GRAM 的写入模式为页模式时,需要设置低字节起始的列地址(0x00~0x0F)和高字节的起始列地址(0x10~0x1F),芯片手册中给出了写入 GRAM 与显示的对应关系,写入列地址在写完一字节后自动按列增长,如图 21.1.2.1 所示:

图 21.1.3.1 SSD1306 页 2 显存写入字节与屏幕坐标的关系

        SSD1306 的列地址需要分两次设置(低4位和高4位),主要是由芯片的指令集设计和寻址方式决定的。这样做是为了在保持指令简洁的同时,能够覆盖足够的地址范围。
SSD1306 的列地址是一个 ​​8位​​ 的值,可以寻址 ​​0-127​​ 列(共128列)。在页地址模式下,这8位地址被分成​​高4位​​和​​低4位​​,分别通过两个独立的命令来设置。
工作原理:
(1)设置低4位列地址 (命令 0x00~0x0F)​​:

  • 此命令用于设置列地址的​​低半字节​​(bit0~bit3)。
  • 命令字本身的高4位是操作码 0000,低4位就是要设置的列地址的低4位值。
  • 例如,要设置列地址的低4位为 1010(即十进制10),则发送命令 0x0A。

(2)设置高4位列地址 (命令 0x10~0x1F)​​:

  • 此命令用于设置列地址的​​高半字节​​(bit4~bit7)。
  • 命令字本身的高4位是操作码 0001,低4位就是你要设置的列地址的高4位值。
  • 例如,要设置列地址的高4位为 0011(即十进制3),则发送命令 0x13。

(3)组合与寻址:
将高4位和低4位组合起来,就得到了完整的8位列地址:高4位 << 4 | 低4位
举个例子​​:如果想将列地址设置为 25(二进制 0001 1001)。

  • 低4位​​ 1001(即 0x9) → 发送命令 0x09
  • 高4位​​ 0001(即 0x1) → 发送命令 0x11(注意:0x10 | 0x01 = 0x11)
  • 组合后的实际列地址就是:(0x01 << 4) | 0x09 = 0x10 + 0x09 = 0x19,即十进制 25。

(4)为何这样设计?
指令简洁性与兼容性​​:SSD1306 的指令系统设计得很紧凑。许多命令是单字节的,并通过命令字的高几位来区分功能。将8位地址拆分成两个4位来设置,只需要两个单字节命令即可完成,无需引入更复杂的多字节命令格式,这可能也是为了兼容早期或其他设计的一种考量。
硬件实现与功耗​​:这种设计可能简化了芯片内部指令解码器和地址寄存器的电路结构。分开设置可能有助于降低在地址更新时的功耗,因为每次只需改变一半的地址位。
(5)实际应用
在初始化 SSD1306 或切换显示区域时,你通常会看到类似这样的代码序列(以页地址模式为例):

// 设置要在哪一页(Page)上进行操作,例如第0页(PAGE0)
OLED_WR_Command(0xB0); // 设置页地址(Page Address),0xB0 代表第0页// 设置要操作的起始列地址(Column Address)
OLED_WR_Command(0x00); // 设置列地址低4位,这里设为0
OLED_WR_Command(0x10); // 设置列地址高4位,这里设为0 (0x10 | 0x0 = 0x10)
// 组合后起始列地址为 0

        因为每次写入都是按字节写入的,这就存在一个问题,如果使用只写方式操作模块,那么,每次要写 8 个点,这样,在画点的时候,就必须把要设置的点所在的字节的每个位都搞清楚当前的状态(0/1?),否则写入的数据就会覆盖掉之前的状态,结果就是有些不需要显示的点,显示出来了,或者该显示的没有显示了。这个问题在能读的模式下,可以先读出来要写入的那个字节,得到当前状况,在修改了要改写的位之后再写进 GRAM,这样就不会影响到之前的状况了。但是这样需要能读 GRAM,对于 4 线 SPI 模式/IIC 模式,模块是不支持读的,而且读→改→写的方式速度也比较慢。
所以采用的办法是在 ESP32 的内部建立一个虚拟的 OLED 的 GRAM(共 128*8=1024个字节),每次修改时,只修改 ESP32 上的 GRAM(实际上就是 SRAM),在修改完成后一次性把 ESP32 上的 GRAM 写入到 OLED 的 GRAM。当然这个方法也有坏处,一个对于那些 SRAM很小的单片机(比如 51 系列)不太友好,另一个是每次都写入全屏,屏幕刷新率会变低。
SSD1306 的命令比较多,具体如下:

命令类别

命令 (HEX)

功能描述

备注/参数

​显示开关​

0xAE

​关闭显示​

进入省电模式,屏幕内容保持但不显示 

0xAF

​开启显示​

正常显示模式 

​对比度控制​

0x81

​设置对比度​

后跟​​1字节参数​​ (0x00-0xFF),值越大通常越亮 

​显示模式​

0xA4

​正常显示​

像素亮灭取决于GDDRAM内容 

0xA5

​全屏点亮​

忽略GDDRAM,所有像素点亮 

0xA6

​正相显示​

GDDRAM中1表示像素亮 

0xA7

​反相显示​

GDDRAM中0表示像素亮 

​寻址模式​

0x20

​设置内存寻址模式​

后跟​​1字节参数​​:00=水平模式,01=垂直模式,02=页模式 (默认) 

​页地址设置​

0xB0 - 0xB7

​设置页起始地址​

用于页寻址模式,设置页0-7 

​列地址设置​

0x00 - 0x0F

​设置列起始地址低4位​​ (页寻址模式)

设置列地址的低半字节 

0x10 - 0x1F

​设置列起始地址高4位​​ (页寻址模式)

设置列地址的高半字节 

0x21

​设置列地址范围​​ (非页模式)

后跟​​2字节参数​​:起始列(0-127)和结束列(0-127) 

​显示起始行​

0x40 - 0x7F

​设置显示起始行​

指定GDDRAM中哪一行对应屏幕顶部,可实现滚动效果 

​扫描方向​

0xC0

​COM输出扫描方向:从上到下​

0xC8

​COM输出扫描方向:从下到上​

​段重映射​

0xA0

​列地址0映射到SEG0​​ (从左到右)

0xA1

​列地址127映射到SEG0​​ (从右到左)

​多路复用比率​

0xA8

​设置多路复用比率​

后跟​​1字节参数​​ (16-63, 复位值63),用于设置COM行数 (通常64行对应0x3F) 

​显示偏移​

0xD3

​设置显示偏移​

后跟​​1字节参数​​ (0-63),垂直移动显示内容 

​显示时钟​

0xD5

​设置显示时钟分频比/振荡器频率​

后跟​​1字节参数​​,高4位设振荡器频率,低4位设分频比 

​预充电周期​

0xD9

​设置预充电周期​

后跟​​1字节参数​​,用于设置预充电时间 

​VCOMH 电平​

0xDB

​设置VCOMH取消选择电平​

后跟​​1字节参数​​,影响像素关闭时的电压 

​电荷泵​

0x8D

​启用/禁用电荷泵​

后跟​​1字节参数​​:0x14=启用 (常用於3.3V供电),0x10=禁用 

​滚动控制​

0x26 / 0x27

​分别设置向右/向左水平滚动​

需配置参数 (起始页、结束页、滚动速度、垂直偏移等),最后用0x2F开启 

0x29 / 0x2A

​分别设置向右/向左垂直水平滚动​

需配置参数 

0x2E

​停止滚动​

0x2F

​开启滚动​

需先配置滚动参数 

表 21.1.3.2 SSD1306 命令表

        这里介绍几个比较常用的命令,如表 21.1.2.2 所示:

表 21.1.3.3 SSD1306 常用命令表

        第0个命令为0X81,用于设置对比度的,这个命令包含了两个字节,第一个0X81为命令,随后发送的一个字节为要设置的对比度的值。这个值设置得越大屏幕就越亮。
第 1 个命令为 0XAE/0XAF。 0XAE 为关闭显示命令; 0XAF 为开启显示命令。
第 2 个命令为 0X8D,该指令也包含 2 个字节,第一个为命令字,第二个为设置值,第二个字节的 BIT2 表示电荷泵的开关状态,该位为 1,则开启电荷泵,为 0 则关闭。在模块初始化的时候,这个必须要开启,否则是看不到屏幕显示的。
第 3个命令为 0XB0~B7,该命令用于设置页地址,其低三位的值对应着 GRAM 的页地址。
第 4 个指令为 0X00~0X0F,该指令用于设置显示时的起始列地址低四位。
第 5 个指令为 0X10~0X1F,该指令用于设置显示时的起始列地址高四位。
其他命令,可以参考 SSD1306 datasheet 的第 28 页。从这页开始,对 SSD1306 的指令有详细的介绍。

        最后,介绍一下 OLED 模块的初始化过程, SSD1306 典型初始化如下图所示:

图 21.1.3.2 SSD1306 初始化框图

        驱动 IC 的初始化代码,直接使用厂家推荐的设置就可以了,只要对细节部分进行一些修改,使其满足要求即可,其他不需要变动。
OLED 的介绍就到此为止,重点向大家介绍了OLED 模块的相关知识,接下来将使用这个模块来显示字符和数字。通过以上介绍,可以得出 OLED 显示需要的相
关设置步骤如下:
1)设置 ESP32 与 OLED 模块相连接的 IO。
这一步,先将与 OLED 模块相连的 IO 口设置为输出,具体使用哪些 IO 口,这里需要根据连接电路以及 OLED 模块所设置的通讯模式来确定。这些将在硬件设计部分向大家介绍。
2)初始化 OLED 模块。
其实这里就是上面的初始化框图的内容,通过对 OLED 相关寄存器的初始化,来启动OLED 的显示。为后续显示字符和数字做准备。
3)通过函数将字符和数字显示到 OLED 模块上。
这里就是通过设计的程序,将要显示的字符送到 OLED 模块就可以了,这些函数将在程序设计部分向大家介绍。
通过以上三步,就可以使用正点原子 OLED 模块来显示字符和数字了,在后面还将会给大家介绍显示汉字的方法。这一部分就先介绍到这里。

21.3 硬件设计
21.3.1 例程功能

        使用 IIC 模式驱动 OLED 模块,不停的显示 ASCII 码和码值。

21.3.2 硬件资源

        1. LED 灯
LED - IO1
2. XL9555
IIC_SDA-IO41
IIC_SCL-IO42
3. OLED
IIC_SCL - IO4
IIC_SDA - IO5
D2 - IO6
DC - IO38
RST - IO_05(XL9555)

21.3.3 原理图

        OLED 模块的原理图在前面已有详细说明了,这里介绍 OLED 模块与开发板的连接,开发板上有一个 OLED/CAMERA 的接口(P6 接口)本次需要通过线将对应驱动管脚连到对应OLED管脚上:

图 21.3.3.1 OLED 模块与开发板连接示意图

21.4 程序设计
21.4.1 程序流程图

        下面看看本实验的程序流程图:

图 21.4.1.1 OLED 实验程序流程图

21.4.2 IIC_OLED 函数解析

        由于 OLED是基于 IIC协议进行通讯的,并且 ESP32 的 IIC函数在前面的章节已经了解过了,因此,关于 ESP32 的 IIC 函数这一部分内容,可以前面章节。

21.4.3 IIC_OLED 驱动解析

        这里只看核心代码,详细的源码请大家参考光盘本实验对应源码, OLED 的驱动主要包括三个文件: oled.c、oled.h和oledfont.h。oledfont.h 头文件存放的是 ASCII 字符集, oled.h存放的是引脚接口宏定义和函数声明等, oled.c 则是驱动代码。
(1)oledfont.h 文件
首先看 oledfont.h 文件的 ASCII 字符集内容:

/* 常用 ASCII 表
* 偏移量 32
* ASCII 字符集: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]
^_`abcdefghijklmnopqrstuvwxyz{|}~
* PC2LCD2002 取模方式设置:阴码+逐列式+顺向+C51 格式
* 总共: 3 个字符集(12*12、 16*16 和 24*24),用户可以自行新增其他分辨率的字符集。
* 每个字符所占用的字节数为:(size/8+((size%8)?1:0))*(size/2),其中 size:是字库生成时的点
* 阵大小(12/16/24...)
*/
/* 12*12 ASCII 字符集点阵 */
const unsigned char oled_asc2_1206[95][12]={ ...这里省略字符集库... };
/* 16*16 ASCII 字符集点阵 */
const unsigned char oled_asc2_1608[95][16]={ ...这里省略字符集库... };
/* 24*24 ASICII 字符集点阵 */
const unsigned char oled_asc2_2412[95][36]={ ...这里省略字符集库... };

        该头文件中包含三个大小不同的 ASCII 字符集点阵,其中包括: 12*12 ASCII 字符集点阵、16*16 ASCII 字符集点阵、 24*24 ASICII 字符集点阵。每个字符集点阵都包含 95 个常用的 ASCII字符集 ,从空格符开始(即 ASCII 码 表 编 号 的 32~127 对 应 的 字 符), 分别为: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijkl
mnopqrstuvwxyz{|}~。
上面的 ASCII 字符集,可以使用一个款很好的字符提取软件来制作获取。字符提取软件为: PCtoLCD2002 完美版,该软件可以提供各种字符,包括汉字(字体和大小都可以自己设置)阵提取,且取模方式可以设置好几种,常用的取模方式,该软件都支持。该软件还支持图形模式,也就是用户可以自己定义图片的大小,然后画图,根据所画的图形再生成点阵数据,这功能在制作图标或图片的时候很有用。
该软件的界面如图 21.3.3.1 所示:

图 21.3.3.1 PCtoLCD2002 软件界面

        然后选择设置,在设置里面设置取模方式如图 21.3.3.2 所示:

图 21.3.3.2 设置取模方式

        上图设置的取模方式,在右上角的取模说明里面有,即:从第一列开始向下每取 8 个点作为一个字节,如果最后不足8个点就补满8位。取模顺序是从高到低,即第一个点作为最高位。如*-------取为 10000000。其实就是按如图 21.3.3.3 所示的这种方式:

图 21.3.3.3 取模方式图解

        从上到下,从左到右,高位在前。按这样的取模方式,然后把 ASCII 字符集按 12*6 大小、 16*8 和 24*12 大小取模出来(对应汉字大小为 12*12、 16*16 和 24*24,字符的只有汉字的一半大!),每个 12*6 的字符占用 12 个字节,每个 16*8 的字符占用 16 个字节,每个 24*12 的字符占用 36 个字节。

(2) oled.h 文件
下面先解析 oled.h 的程序。 对 OLED 模块 IIC 引脚和 OLED 器件地址做了相关定义。

#define OLED_ADDR       0X3C    /* OLED地址 */
#define OLED_CMD        0x00    /* 写命令 */
#define OLED_DATA       0x40    /* 写数据 *//* 引脚定义 */
#define OLED_SCL_PIN    4
#define OLED_SDA_PIN    5
#define OLED_D2_PIN     6
#define OLED_DC_PIN     38

        选择使用 IO4 作为 IIC的时钟线, IO5 作为 IIC 的数据线, IO6 为 OLED_D2_PIN, IO38为 OLED_DC_PIN。 OLED 的器件地址为 0x3C。
由于会使用到 RST 引脚,而 RST 引脚是 XL9555 器件上的 IO,这里便有OLED_RST宏用来控制 RST 引脚输出高低电平,实现 OLED 的硬件复位。

#define OLED_RST(x)     do{ x ?                                            \xl9555_pin_write(OV_RESET_IO, OLED_PIN_SET) :  \xl9555_pin_write(OV_RESET_IO, OLED_PIN_RESET); \}while(0)

(3)oled.c 文件
最后就是 oled.c 文件的驱动源码介绍。先是 OLED(SSD1306)的初始化函数,以 8080 并口方式为例,其定义如下:

/*** @brief       初始化OLED* @param       i2c_obj_t self: 传入的IIC初始化参数,用以判断是否已经完成IIC初始化* @retval      无*/
void oled_init(i2c_obj_t self)
{if (self.init_flag == ESP_FAIL){iic_init(I2C_NUM_1);                                /* 初始化IIC */}oled_master = self;gpio_config_t gpio_init_struct = {0};gpio_init_struct.intr_type = GPIO_INTR_DISABLE;         /* 失能引脚中断 */gpio_init_struct.mode = GPIO_MODE_INPUT;                /* 输入输出模式 */gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;       /* 使能上拉 */gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;  /* 失能下拉 */gpio_init_struct.pin_bit_mask = 1ULL << OLED_D2_PIN;    /* 设置的引脚的位掩码 */gpio_config(&gpio_init_struct);gpio_init_struct.intr_type = GPIO_INTR_DISABLE;         /* 失能引脚中断 */gpio_init_struct.mode = GPIO_MODE_OUTPUT;               /* 输入输出模式 */gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;       /* 使能上拉 */gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;  /* 失能下拉 */gpio_init_struct.pin_bit_mask = 1ULL << OLED_DC_PIN;    /* 设置的引脚的位掩码 */gpio_config(&gpio_init_struct);OLED_DC(0);/* 配置复位引脚电平 */xl9555_pin_write(OV_RESET_IO, 1);/*复位OLED*/OLED_RST(0);vTaskDelay(100);OLED_RST(1);vTaskDelay(100);/* 初始化代码 */oled_write_Byte(0xAE, OLED_CMD);    /* 关闭显示 */oled_write_Byte(0xD5, OLED_CMD);    /* 设置时钟分频因子,震荡频率 */oled_write_Byte(80, OLED_CMD);      /* [3:0],分频因子;[7:4],震荡频率 */oled_write_Byte(0xA8, OLED_CMD);    /* 设置驱动路数 */oled_write_Byte(0X3F, OLED_CMD);    /* 默认0X3F(1/64) */oled_write_Byte(0xD3, OLED_CMD);    /* 设置显示偏移 */oled_write_Byte(0X00, OLED_CMD);    /* 默认为0 */oled_write_Byte(0x40, OLED_CMD);    /* 设置显示开始行 [5:0],行数 */oled_write_Byte(0x8D, OLED_CMD);    /* 电荷泵设置 */oled_write_Byte(0x14, OLED_CMD);    /* bit2,开启/关闭 */oled_write_Byte(0x20, OLED_CMD);    /* 设置内存地址模式 */oled_write_Byte(0x02, OLED_CMD);    /* [1:0],00,列地址模式;01,行地址模式;10,页地址模式;默认10; */oled_write_Byte(0xA1, OLED_CMD);    /* 段重定义设置,bit0:0,0->0;1,0->127; */oled_write_Byte(0xC0, OLED_CMD);    /* 设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数 */oled_write_Byte(0xDA, OLED_CMD);    /* 设置COM硬件引脚配置 */oled_write_Byte(0x12, OLED_CMD);    /* [5:4]配置 */oled_write_Byte(0x81, OLED_CMD);    /* 对比度设置 */oled_write_Byte(0xEF, OLED_CMD);    /* 1~255;默认0X7F (亮度设置,越大越亮) */oled_write_Byte(0xD9, OLED_CMD);    /* 设置预充电周期 */oled_write_Byte(0xf1, OLED_CMD);    /* [3:0],PHASE 1;[7:4],PHASE 2; */oled_write_Byte(0xDB, OLED_CMD);    /* 设置VCOMH 电压倍率 */oled_write_Byte(0x30, OLED_CMD);    /* [6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc; */oled_write_Byte(0xA4, OLED_CMD);    /* 全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏) */oled_write_Byte(0xA6, OLED_CMD);    /* 设置显示方式;bit0:1,反相显示;0,正常显示 */oled_write_Byte(0xAF, OLED_CMD);    /* 开启显示 *//* 打开oled */oled_on();oled_clear();
}

        该函数的结构比较简单,开始是对 GPIO 口的初始化,这里用了宏定义来决定要设置的 IO口,后面的就是一些初始化序列了,按照厂家提供的资料来做就可以。值得注意一点的是,因为 OLED 是无背光的,在初始化之后,把显存都清空了,所以在屏幕上是看不到任何内容的,就像没通电一样,不要以为这就是初始化失败,要写入数据模块才会显示的。
接着,要介绍的是 oled_refresh_gram 更新显存到 OLED 函数,该函数的作用是在程序中定义的二维数组 g_oled_gram 的值一次性刷新到 OLED 的显存 GRAM 中。在 oled.c 文件开头定义了如下一个二维数组:

/*OLED 的显存
存放格式如下.
[0]0 1 2 3 ... 127
[1]0 1 2 3 ... 127
[2]0 1 2 3 ... 127
[3]0 1 2 3 ... 127
[4]0 1 2 3 ... 127
[5]0 1 2 3 ... 127
[6]0 1 2 3 ... 127
[7]0 1 2 3 ... 127
*/
uint8_t OLED_GRAM[128][8];

        该数组值与 OLED 显存 GRAM 值一一对应。在操作的时候只需要先修改该数组的值,然后再通过调用 oled_refresh_gram 函数把数组的值一次性刷新到 OLED 的 GRAM 上即可。oled_refresh_gram 函数定义如下:

/*** @brief       更新显存到OLED* @param       无* @retval      无*/
void oled_refresh_gram(void)
{uint8_t i, n;for (i = 0; i < 8; i++){  oled_write_Byte(0xb0 + i, OLED_CMD);     /* 设置页地址(0~7) */oled_write_Byte(0x00, OLED_CMD);         /* 设置显示位置—列低地址 */oled_write_Byte(0x10, OLED_CMD);         /* 设置显示位置—列高地址 */for (n = 0; n < 128; n++){oled_write_Byte(OLED_GRAM[n][i], OLED_DATA);}}
}

        oled_refresh_gram 函数先设置页地址,然后写入列地址(也就是纵坐标),然后从 0 开始写入128个字节,写满该页,最后循环把 8页的内容都写入,就实现了整个从 ESP32显存到 OLED显存的拷贝。
oled_refresh_gram()函数还调用了 oled_write_Byte()这个函数,也就是接着要介绍的函数:该函数和硬件相关,该函数定义如下:

/*** @brief       oled 写命令* @param       tx_data:数据* @param       command:命令值* @retval      无*/
void oled_write_Byte(unsigned char tx_data, unsigned char command)
{unsigned char data[2] = {command, tx_data};oled_write(data, sizeof(data));
}

        oled_write_Byte()函数还调用 oled_write()函数,其定义如下:

/*** @brief       oled IIC写数据* @param       data_wr:发送的数据或者命令* @param       size   :发送数据的大小* @retval      0:发送成功;非0值:发送失败*/
esp_err_t oled_write(uint8_t* data_wr, size_t size)
{i2c_buf_t bufs = {.len = size,.buf = data_wr,};i2c_transfer(&oled_master, OLED_ADDR, 1, &bufs, I2C_FLAG_STOP);return ESP_OK;
}

        oled_write()函数的处理方法,是通过调用IIC控制块下的收发函数来发送数据和命令。先将传入的参数统一存放到通过 IIC 控制块定义的一个结构体数组中,由该数组统一管理发送的数据和命令,最后以指针类型的方式传递给收发函数 i2c_transfer()。关于函数 i2c_transfer()的描述请参照 19.3.3 小节的内容。
g_oled_gram [128][8]二维数组中的 128 代表列数(x 坐标),而 8 代表的是页,每页又包含 8行,总共 64 行(y 坐标),从高到低对应行数从小到大,如表 21.3.3.1 所示:

表 21.3.3.1 OLED_GRAM 和 OLED 屏坐标对应关系

        上表中 G 代表 OLED_GRAM, G[0][0]就表示 OLED_GRAM [0][0]。比如,要在 x=3,y=9 这个点写入 1,则可以用这个句子实现:

                                OLED_GRAM [3][1] |= 1<<1;

        一个通用的在点(x, y)置 1 表达式为:

                            OLED_GRAM [x][y/8] |= 1<<(y%8);

        其中 x 的范围为: 0~127; y 的范围为: 0~63。
因此,可以得出接下来介绍的这个比较重要的函数: OLED 画点函数,其定义如下:

/*** @brief       OLED画点 * @param       x  : 0~127* @param       y  : 0~63* @param       dot: 1 填充 0,清空* @retval      无*/ 
void oled_draw_point(uint8_t x, uint8_t y, uint8_t dot)
{uint8_t pos, bx, temp = 0;if (x > 127 || y > 63){return;                     /* 超出范围了 */}pos = 7 - y / 8;                /* 计算GRAM里面的y坐标所在的字节, 每个字节可以存储8个行坐标 */bx = y % 8;                     /* 取余数,方便计算y在对应字节里面的位置,及行(y)位置 */temp = 1 << (7 - bx);           /* 高位表示高行号, 得到y对应的bit位置,将该bit先置1 */if(dot)                         /* 画实心点 */{OLED_GRAM[x][pos] |= temp;  }else                            /* 画空点,即不显示 */{OLED_GRAM[x][pos] &= ~ temp;}
}

        该函数有 3 个形参,前两个是横纵坐标,第三个 t 为要写入 1 还是 0。该函数实现了在OLED 模块上任意位置画点的功能。
前面知道取模方式是:从上到下,从左到右,高位在前。下面根据取模的方式来编写显示字符 oled_show_char 函数,其定义如下:

/*** @brief       在指定位置显示一个字符,包括部分字符 * @param       x   : 0~127* @param       y   : 0~63* @param       size: 选择字体 12/16/24* @param       mode: 0,反白显示;1,正常显示* @retval      无*/ 
void oled_show_char(uint8_t x, uint8_t y, uint8_t chr, uint8_t size, uint8_t mode)
{uint8_t temp, t, t1;uint8_t y0 = y;uint8_t csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2); /* 得到字体一个字符对应点阵集所占的字节数 */chr = chr - ' ';                                                /* 得到偏移后的值 */for (t = 0; t < csize; t ++){   if (size == 12){temp = atk_asc2_1206[chr][t];                           /* 调用1206字体 */}else if (size == 16){temp = atk_asc2_1608[chr][t];                           /* 调用1608字体 */}else if (size == 24){temp = atk_asc2_2412[chr][t];                           /* 调用2412字体 */}else{return;                                                 /* 没有的字库 */}for (t1 = 0; t1 < 8; t1++){if (temp & 0x80){oled_draw_point(x, y, mode);}else{oled_draw_point(x, y, !mode);}temp <<= 1;y++;if ((y - y0) == size){y = y0;x++;break;}}}
}

        该函数为字符以及字符串显示的核心部分,函数中 chr = chr - ' ';这句是要得到在字符点阵数据里面的实际地址,因为取模是从空格键开始的,例如 oled_asc2_1206 [0][0],代表的是空格符开始的点阵码。在接下来的代码,也是按照从上到小(先 y++),从左到右(再 x++)的取模方式来编写的,先得到最高位,然后判断是写 1 还是 0,画点;接着读第二位,如此循环,直到一个字符的点阵全部取完为止。这其中涉及到列地址和行地址的自增,根据取模方式来理解,就不难了。

        这里有一行关键代码(只适用于ASCII,不适用于汉字),计算一个大小为 size × size的点阵字符,在逐列式取模方式下,总共需要占用多少个字节:

uint8_t csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2); /* 得到字体一个字符对应点阵集所占的字节数 */

        公式:​​字节数 = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2)​
这里的 size是字模的点阵大小,例如 6x8, 8x16, 12x12, 16x16, 24x24 等。公式可以分解为两个部分。
一步:计算“高度”方向需要多少个字节(公式前半部分)
第一部分 (size / 8 + ((size % 8) ? 1 : 0))是为了解决​​高度方向​​的存储问题。

  • ​​核心概念​​:在计算机中,​​1个字节(Byte)有 8 个比特(bit)​​。每个比特可以表示一个像素点的状态(1代表亮,0代表灭)。
  • 问题所在​​:字符的​​高度(size)​​ 不一定是8的整数倍。例如,16、12、24等。
  • 解决方案​​:

        size / 8:计算高度方向​​正好包含多少个完整的8位(字节)​​。例如,16 / 8 = 2。
(size % 8) ? 1 : 0:这是一个​​三目运算符​​,意思是“​​求余数​​”。如果高度除以8后有余数(即 size % 8不等于0),那么就​​额外需要1个字节​​来存储这些多出来的点;如果没有余数,就不需要额外字节。例如,16 % 8 = 0,所以加0;12 % 8 = 4,所以加1。

  • 所以,这部分的结果就是:存储一列像素点所需的总字节数​​。把它记为 字节/列。

        对于 16x16的字:(16/8) + (16%8?1:0) = 2 + 0 = 2字节/列
对于 12x12的字:(12/8) + (12%8?1:0) = 1 + 1 = 2字节/列
对于 24x24的字:(24/8) + (24%8?1:0) = 3 + 0 = 3字节/列

        第二步:计算“宽度”方向有多少列(公式后半部分)
第二部分 * (size / 2)定义了字符的​​宽度(列数)​​。

  • 常见约定​​:在英文字符库(ASCII)中,字符的宽度通常是高度的一半,以达到较好的视觉比例,例如:

        一个 16x16的点阵,宽度是 16 / 2 = 8列。
一个 12x12的点阵,宽度是 12 / 2 = 6列。
一个 24x24的点阵,宽度是 24 / 2 = 12列。

  • 因此,(size / 2)表示这个字符总共有多少列​​。

        第三步:两者相乘得到总字节数
最后,将​​每列所需的字节数​​乘以​​总的列数​​,就得到了存储整个字符所需要的总字节数。
​​总字节数 = (每列所需的字节数) × (总列数)​​ ​​= [ (size / 8) + ((size % 8) ? 1 : 0) ] × (size / 2)​​
举例说明:

  • 16x16 点阵​​ (非常常见)

        每列字节数:(16/8) + (16%8?1:0) = 2 + 0 = 2字节
总列数:16 / 2 = 8列
总字节数:2 字节/列 × 8 列 = 16 字节​​
注意​​:可能见过“16x16点阵占32字节”的说法。那是指​​汉字库​​,因为一个标准汉字是方形的,宽度是16列(16/2中的2被替换成了1,即不折半)。英文字符库(ASCII)才采用宽度折半的约定。公式中的 (size/2)是针对ASCII字符的,如果是方形汉字,这里应为 size。所以一个16x16的汉字占 2 字节/列 × 16 列 = 32 字节。

  • 12x12 点阵​​

        每列字节数:(12/8) + (12%8?1:0) = 1 + 1 = 2字节
总列数:12 / 2 = 6列
​​总字节数:2 字节/列 × 6 列 = 12 字节​​

  • 6x8 点阵​​ (小字号ASCII)

        每列字节数:(8/8) + (8%8?1:0) = 1 + 0 = 1字节 (高度是8)
总列数:8 / 2 = 4列? 不对,这里通常宽度就是6。对于这种非方形字符,公式可能不通用,但原理相同。
实际计算:1 字节/列 × 6 列 = 6 字节。
为了更直观地理解不同点阵大小的字符如何存储,可以参考下面的对比表格

点阵大小

计算示例 (每列字节数 × 总列数)

总字节数

备注

​16x16 (ASCII)​

(2 + 0) × (16/2) = 2 × 8

​16​

英文字符,宽度折半

​16x16 (汉字)​

(2 + 0) × 16 = 2 × 16

​32​

方形汉字,宽度不折半

​12x12 (ASCII)​

(1 + 1) × (12/2) = 2 × 6

​12​

英文字符,宽度折半

​24x24 (汉字)​

(3 + 0) × 24 = 3 × 24

​72​

方形汉字,宽度不折半

​8x16 (ASCII)​

(2 + 0) × (16/2) = 2 × 8

​16​

一种常见的字符尺寸

​6x8 (ASCII)​

(1 + 0) × 6 = 1 × 6

​6​

小字号字符

总结:
这个公式的原理是基于以下几个关键点:
按列取模​​:数据是按照​​列​​来组织和存储的。
字节与点的映射​​:每​​8个垂直像素点​​占用​​1个字节​​。如果垂直像素数不是8的倍数,则需要进位到下一个字节。
字符宽高比​​:在英文字符库中,字符的​​宽度通常设定为高度的一半​​,以符合视觉习惯。
最终,公式 (size/8 + ((size%8)?1:0)) * (size/2)精确地计算出了一个ASCII字符在逐列取模方式下所占用的内存空间大小。如果要处理方形汉字,只需将公式中的 (size/2)替换为 size即可。    

        oled.c 的内容比较多,其他的函数计算过程注意上面说明比较好理解。

21.4.4 CMakeLists.txt 文件

        打开本实验 BSP 下的 CMakeLists.txt 文件,其内容如下所示:

set(src_dirsIICLEDOLEDXL9555)set(include_dirsIICLEDOLEDXL9555)set(requiresdriveresp_lcd)idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
21.4.5 实验应用代码

        打开 main/main.c 文件,该文件定义了工程入口函数,名为 app_main。该函数代码如下。

i2c_obj_t i2c0_master;
i2c_obj_t i2c1_master;/*** @brief       程序入口* @param       无* @retval      无*/
void app_main(void)
{uint8_t t = 0;led_init();                             /* 初始化LED */i2c0_master = iic_init(I2C_NUM_0);      /* 初始化IIC0 */i2c1_master = iic_init(I2C_NUM_1);      /* 初始化IIC1 */xl9555_init(i2c0_master);               /* 初始化XL9555 */oled_init(i2c1_master);                 /* 初始化OLED */oled_show_string(0, 0, "ALIENTEK", 24);oled_show_string(0, 24, "0.96' OLED TEST", 16);oled_show_string(0, 40, "ATOM 2023/8/26", 12);oled_show_string(0, 52, "ASCII:", 12);oled_show_string(64, 52, "CODE:", 12);oled_refresh_gram();                    /* 更新显示到OLED */t = ' ';while(1){oled_show_char(36, 52, t, 12, 1);   /* 显示ASCII字符 */oled_show_num(94, 52, t, 3, 12);    /* 显示ASCII字符的码值 */oled_refresh_gram();                /* 更新显示到OLED */t++;if (t > '~'){t = ' ';}vTaskDelay(500);LED_TOGGLE();                       /* LED闪烁 */}
}

        在实验章节中也介绍到 OLED 以 IIC 协议进行通讯,肯定要分配一个 IIC 端口,然而IO 扩展芯片 XL9555 也需要分配到一个 IIC 端口(IIC0)。为了保证两个设备之间不被干扰,分配另一个 IIC端口(IIC1)给 OLED使用,如此一来避免了因端口使用冲突而带来的影响,这也是在主函数中定义了两个 IIC 端口的原因。
main.c 主要功能就是在 OLED 上显示一些实验信息字符,然后开始从空格键开始不停的循环显示 ASCII 字符集,并显示该字符的 ASCII 值。最后 LED 闪烁提示程序正在运行。

21.5 下载验证

       待验证


文章转载自:

http://yGDmuMdZ.mkzdp.cn
http://lLH5RSYU.mkzdp.cn
http://3SKbuCA0.mkzdp.cn
http://NBWcQ5Nf.mkzdp.cn
http://KzewzAvi.mkzdp.cn
http://7AHnWC97.mkzdp.cn
http://5ywne8MN.mkzdp.cn
http://0OFKAGzI.mkzdp.cn
http://kgpepON9.mkzdp.cn
http://cbL22fmy.mkzdp.cn
http://VDZQ7sml.mkzdp.cn
http://WQs0ikVE.mkzdp.cn
http://82Mqj4du.mkzdp.cn
http://Q5l9TF6v.mkzdp.cn
http://VlI0EqRb.mkzdp.cn
http://uwugotb3.mkzdp.cn
http://ZFahXPnj.mkzdp.cn
http://Z9Wa0nGm.mkzdp.cn
http://awjnHYAf.mkzdp.cn
http://ZyziAGXw.mkzdp.cn
http://Qw4RYXCA.mkzdp.cn
http://a6N4MVaU.mkzdp.cn
http://TPOuLhjB.mkzdp.cn
http://sUsYx6Z5.mkzdp.cn
http://qIVgXV12.mkzdp.cn
http://vRjpJwVX.mkzdp.cn
http://yccqWe9X.mkzdp.cn
http://FKzbJV84.mkzdp.cn
http://3GjSyyVR.mkzdp.cn
http://JSfeI01d.mkzdp.cn
http://www.dtcms.com/a/384507.html

相关文章:

  • 能取代 transform 的架构目前看来 有哪些
  • 为什么基频是信号速率的1/2?
  • Unity UI坐标说明
  • 微美全息(NASDAQ:WIMI)以声誉混合多层共识,开启区块链共识算法创新篇章
  • LAN9253通过CHIP_MODE改变链路顺序
  • 矩阵运算_矩阵A和向量a的转置T相关
  • C++异步任务处理与消息可靠性保障指南:从基础到实战
  • 总结-十大管理输入输出
  • 【Vue3】09-编写vue时,reactive的使用
  • Transformer原理学习(2)位置编码
  • C++编程语言:标准库:第38章——输入输出流(Bjarne Stroustrup)
  • 北理工提出仅依赖机载传感器针对IAP的控制与状态估计框架
  • JVM 垃圾收集算法详解!
  • pycharm选择conda的interpreter
  • 为什么要将OpenCV帧转换为PIL图像
  • Apache ShardingSphere 实战:自定义 SQL 拦截插件开发指南
  • 【langchain】加载、处理和分割源数据文件
  • cmake .. -G “Visual Studio 12“
  • i.MX6ULL 外设初始化
  • Node.js如何实现一个WebSocket服务
  • 机器学习shap分析案例
  • Shebang:Node.js 脚本的魔法开头
  • [vue3] 使用reactive声明数组如何正确赋值
  • 微硕MOS管WSF12N15助力汽车电动转向系统
  • SpringBoot快速上手:基础+进阶+项目+源码
  • winscp连接虚拟机centos要求要密码,可是虚拟机登录不需要密码,怎么解决
  • NumPy全面学习笔记
  • Java 轻松实现 Markdown 转 Word、PDF、HTML
  • 时序数据库选型指南:Apache IoTDB企业级解决方案深度解析
  • Java 中 ArrayList 扩容机制的深度解析