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

OLED(SSD306)移植全解-基于IIC

OLED(SSD306)移植全解-基于IIC

  • 一,什么是oled?
  • 二,什么是IIC协议
  • 三,IIC通信流程:
  • 四,针对SSD1306的IIC通信流程(结合芯片手册版)
    • 1,主机发送起始信号
    • 2,从设备应答 ACK 或 NACK
    • 3,主机发送控制字节
    • 4,重复 START
    • 5,STOP
  • 五,在 CubeMX 中配置 I²C 外设
    • 1,Mode
    • 2,Configuration
      • Parameter Settings
        • 1,Master Feasures
        • 2,Timing Configuration即 数字滤波器和模拟滤波器
        • 3,Slave Features即从机特性
      • GPIO Settings
  • 六,SSD1306驱动库
    • 1,IIC通信层
    • 2,亮灭控制与上电初始化
    • 3,设置坐标和清屏
    • 4,文本显示层
    • 5,高级图形显示层

一,什么是oled?

一种屏幕类型,每个像素都能自己发光,不需要背光

大小多为 128×32 或 128×64 像素(像素就是屏幕上最小的发光点),还有一种衡量大小的方式,就是使用对角线长度表示,例如128×32 和 128×64像素数分别对应对角线长0.91英寸 或 0.93 英寸

oled如何发光呢?
首先其内部会有一个驱动芯片ssd1306,

在这里插入图片描述
[media pointer=“file-service://file-QsH4on2Cfai99p4uU4zGB4”]
OLED 屏幕多为 128×32 或 128×64 像素(像素就是屏幕上最小的发光点)。那0.91和0.93是什么意思?你说简单说,STM32F4 通过 I²C 总线把「命令+数据」发给 SSD1306,SSD1306 再把对应的点阵内容显示到 OLED 屏幕上,难道ssd1306是通过iic接收stm32f4的数据信号,然后依据此数据想stm32一样控制io引脚电平去点亮像素点?如果不是像stm32通过io引脚输出高低电平,那是怎么点亮像素点的呢,依据上面的原理图讲讲?

这个驱动芯片并不是像STM32一样直接通过控制 GPIO 引脚来点亮像素点,它通过iic协议接收STM32发过来的数据和命令(SSD1306 会根据 DC 引脚的状态判断接收到的是命令还是数据。在写入控制命令时,DC 设为低电平;在写入像素数据时,DC 设为高电平),内部电路对这些数据进行解析,然后将数据通过驱动电路输出到 OLED 屏幕上。

那么什么是命令(Command)?
命令就是不会改变oled的显示内容,就算说命令配置的不是屏幕,而是ssd1306的寄存器,如:
打开显示、关闭显示。
设置显示的分辨率(128x32 或 128x64)。
设置显示起始地址或列地址。
什么又是数据(Data)呢?
数据就是可以直接控制oled显示内容的数据,控制像素的亮灭,直接存储在显存中,1字节数据1页中1个列的像素的亮灭
这里提到了页,在OLED中什么叫页呢?
以分辨率为128*64的oled为例,128代表整个屏幕有128列像素点,64代表整个屏幕有64行像素点,页的划分就是每8行表示1个页,那么整个屏幕就分为8个页。
如果我们在每个页中讨论,1个页的列(不是整个屏幕的列)即8个像素就是一组,由一个字节数据控制亮灭

STM32F4 向 SSD1306 发送特定的命令和显示数据,你提到的命令和数据有什么区别和联系吗?这些命令和数据有什么作用啊,能不能举例说明?你说ssd1306有自己的内存区域(显存),用于存储屏幕上所有像素的状态,你提到的显存只是存储像素状态,并没有说明ssd1306怎么通过显存数据控制像素的亮灭啊?好像意思是说ssd1306通过iic接收stm32的命令和数据,然后ssd1306去根据这些命令和数据改变自己的显存数据?这个内部有一个 点阵矩阵,每个像素(点)由电子方式控制。好像说明了像素点亮的原理,但我还是不懂啊?每个字节存储 8 位像素信息(对于 128×32 分辨率,显示内容被存储为 128 列 * 4 页)。这又是什么意思啊,一个oled屏幕不是有12864个像素点吗,如果显存中的一字节存储8个像素点的状态,那就需要12864/8 = 1024个字节,也就是说ssd1306的显存只有1024B即1KB,而你又说显示内容被存储为 128 列 * 4 页,这个页是什么意思,我记得32位是一个字啊?这里怎么用页表示,这里4页是代表一列的32个位吗?还有你的这个举例我也不理解啊,发送一个字节数据 0xFF 表示这一列的所有 8 个像素都被点亮,而 0x00 则表示这一列的所有像素都熄灭,0xFF应该表示2个字节吧?怎么只表示了1列,而且只有8个点即1字节呢?

二,什么是IIC协议

接下来我们该了解什么是IIC通信协议:

I²C(Inter-Integrated Circuit, 发音“eye-squared-see”)是一种串行总线协议。由飞利浦公司(现恩智浦半导体)开发的串行通信总线,广泛应用于连接微控制器和低速外围设备。

什么是低速外围设备呢?
举几个低速外围设备的例子:
传感器:比如温度传感器、湿度传感器、光传感器等
显示屏:例如 OLED 显示屏
RTC(实时时钟):用于保持时间的模块
EEPROM(电可擦可编程只读存储器)
按键、开关、LED 灯等简单接口设备
对应按键和LED使用IIC驱动见很少见,但不是没有,例如MCP23017 是一个 I²C 控制的 GPIO 扩展器芯片,可以用来控制多个 LED 灯的亮灭。

它只有两根线:
SCL(时钟线):告诉对方“我开始传/收数据了,每一个时钟脉冲传/收一个 bit”。
SDA(数据线):真正传输数据的线,每个时钟周期上或下会放置一个数据位(0 或 1)

主机/从机:
STM32F4 配置成 I²C 主机,由它来产生时钟脉冲。
SSD1306 芯片作为 I²C 从机,等待主机发命令或数据。

I²C 地址:
SSD1306 常见硬件地址通常是 0x3C(有的模块也可能用 0x3D)。这个地址是写死在 OLED 板子上的跳线,或者由焊盘决定。
地址的作用就是让STM32找到ssd1306

核心特点:
1,支持多主机多从机架构(最多128个设备,使用7位寻址)
什么叫做多主机呢?
就是多个主机可以共享同一总线,但是只有一个主机可以在某一时刻控制总线,如何实现多台主机的发送冲突呢?如果两个主机同时尝试发送数据时,较低的电平会“获胜”,另一方的发送被停止,从而避免了冲突。

注意:虽然支持多主机,但在实际应用中,多主机模式不常用。大多数情况下,我们看到的都是一个主机控制多个从机的结构。

多从机的7位地址是什么?
如果我们使用一个主机控制多个从机,那么为了方便主机向特定的从机发送数据,我们为每个从机设备设置了唯一的一个地址,这个地址用7位数据表示,那么最多可以设置27=128个从机设备(范围是从 0x00 到 0x7F,其实有些地址会被保留,使用实际上小于128),

2,通信速率灵活(标准模式100kHz,快速模式400kHz等)
我们知道在串口通信中使用波特率来表示每秒传输的数据位数,而在IIC通信当中,我们使用比特每秒(bps)表示每秒传输的位数,这里也可以用Hz表示(在这里1bps = 1Hz)
为了适应不同的设备需求不同的数据传输速度,我们的IIC支持多种通信速度:
标准模式(100 kHz):低速传感器、简单的控制设备、温度传感器、RTC(实时时钟)模块
快速模式(400 kHz):显示屏
高速模式(3.4 MHz): 图像传感器、 高速数据采集
超高速模式(5 MHz):某些高速视频采集系统

3,内置硬件应答机制确保可靠传输
什么是IIC的应答机制呢?
就是接收方不管有没有接收到数据,都要发送应答信号/非应答信号给发送方(接收方可以是主机也可以是从机)
应答信号(ACK):接收方成功接收到数据并准备好继续接收下一个字节时,发送 低电平(0),表示应答。
非应答信号(NACK):接收方没有成功接收到数据,或者接收完所有数据后不再接收时,发送 高电平(1),表示没有应答

4,起始和停止条件
I²C通信使用特殊的总线状态表示通信的开始和结束。起始条件(START)是在SCL高电平时,SDA从高变为低;停止条件(STOP)是在SCL高电平时,SDA从低变为高。这两种条件定义了一个完整通信帧的边界

三,IIC通信流程:

前面介绍了IIC协议的基础知识,那么在一个完整的IIC通信过程中,IIC的两条总线会经历什么呢?
1,IIC的两条线(SCL和SDA)默认处于高电平的状态
2,当某一个主机想访问某个从设备时,会发送起始信号(SCL保持高电平,SDA从高电平变为低电平),此时其他主设备不能访问总线,所有的从设备都进入准备接收接下来的地址数据

补充:此时总线进入“忙碌(Busy)”状态,其他主机检测到 SDA 被拉低时就知道总线已被占用。不过如果在多主机场景下,可能出现同时两个主机几乎同时发起 START,此时会进行总线仲裁(Arbitration)。
若两个主机都在同一个时钟周期尝试拉低 SDA,不同主机对 SDA 的读写一致时继续,若出现冲突则掉线的主机会自动放弃,从而保证一个主机获得总线控制权

3,主设备发送7位(或10位)从设备地址,紧跟一个读/写控制位(表示接下来是读还是写),构成8个位即1字节,所有从设备接收并比对此地址,仅地址匹配的从设备继续参与后续通信。

补充:对于 10 位地址寻址,需要发送两次地址字节(第一字节包含前两位“11110”、接着高 2 位地址和 R/W=0,第二字节才是真正的低 8 位地址),详细流程略微复杂,但大方向与 7 位寻址相似。

4,如果总线上存在匹配地址的从设备,从机会将SDA线拉低一个时钟周期(发送ACK);如果不存在匹配的从设备,SDA线保持高电平(NACK)

在发送完 8 位(地址+R/W)之后,主机会释放 SDA,进入第 9 个时钟周期。此时若有从设备应答, 从机就在第 9 个时钟上拉低 SDA,表示 ACK;否则 SDA 处于高电平表示 NACK
如果出现 NACK,主机常见做法就是发出 STOP 条件中断本次传输,然后视情况重试或放弃。

5,地址确认后,主设备和从设备开始根据之前的读/写位进行数据交换。数据以8位字节为单位传输,每个字节后必须有一个应答位
6,当完成一次数据交换后,主机还想发送其他数据,为避免了其他主设备在操作中途获取总线控制权,主设备可以发送新的起始条件(SCL高,SDA由高变低),不需要停止再开始

重复 START 用于在一次事务(Transaction)中先进行写地址/写数据,再切换为读数据(或反之),而不释放总线。例如:先给某个寄存器写入要读的起始地址,然后立刻发 Re-START,再执行读操作。这避免了总线空闲期间其他主机介入的可能。

7,通信结束于停止条件:SCL保持高电平,SDA从低电平变为高电平。这一转换表明主设备释放总线,将总线状态恢复为"空闲"。停止条件后,任何主设备都可以通过发送起始条件获取总线控制权。

四,针对SSD1306的IIC通信流程(结合芯片手册版)

1,主机发送起始信号

主设备将 7 位从设备地址 + R/W 位构成一个“字节”传给总线
7位从设备地址哪里来呢?
看芯片手册:The device will respond to the slave address following by the slave address bit (“SA0” bit) and the read/write select bit (“R/W#” bit) with the following byte format, b7 b6 b5 b4 b3 b2 b1 b0 = 011110 SA0 R/W#.” (D/C# pin acts as SA0)这里是说ssd1306的从地址是011110 SA0 R/W,当SA0为低电平时,7为地址为0111100即0x3c,这也是默认的地址,如果想改变,可以的,这里有一个地址拓展位SA0,将这个引脚改为高电平则7位地址就变成了0111101即0x3d

实际上主机发送的起始信号是8个位组成1字节,这第8个位哪里?不是只有7位地址吗?
我们的0x3c和0x3d会左移一位,将第8位留给R/W,R/W 为 0 表示“下面我要写数据给你”;R/W 为 1 表示“我想从你那里读数据”。

2,从设备应答 ACK 或 NACK

主机发送完起始信号之后,会在第 9 个时钟周期会先松开 SDA(恢复到高电平) ,看总线上有没有某个从机把 SDA 拉低(从机拉低 SDA 就表示向主机发送ACK即应答信号,表示从机接收到了地址数据,准备被读/被写)
如果主机发现SDA没有被拉低,而是SDA在第9个时钟周期,一直保持高电平,就表示从机发送了NACK即非应答信号,于是主机通常会放弃这次尝试,发 STOP(停止)
参考手册:After the transmission of the slave address, an acknowledgement signal will be generated after receiving one byte … The acknowledge bit is defined as the SDA line is pulled down during the HIGH period of the acknowledgement related clock pulse.”
“If there is no ACK, the master usually sends a STOP condition to terminate the current communication attempt

3,主机发送控制字节

对于步骤 5:发送控制字节(Co + D/C#)及后续数据或命令,我连什么是控制字节都不知道啊?Co 位(Continuity bit):决定本次 I²C 传输里是否还会接着再发“下一个控制字节”;这又是什么意思啊?如果会再发控制字节会怎么样?不发又会怎么样?你说D/C# 位(Data/Command bit):决定下面真正的“8 位”是命令(Command),还是显示数据(Data,即写入显存);这里的下面的8位又是在哪里啊?问题又来了,Co和D/C是什么时候发送的?在起始信号之后,命令/数据信号之前?用 “0x00” 作为“单字节命令”的控制字节;用 “0x40” 作为“写数据到显存”的控制字节,这里0x00是控制字节?又提到了控制字节,我还是不理解,单字节命令又是什么东西?0x40是写数据到显存的控制字节,这里我感觉控制字节是命令啊?他们是什么关系?主机在收到 ACK 之后,紧接着发:0x00(控制字节,表示“下一个字节当命令”)。然后发命令。例如:0xA8 (设置 Multiplex Ratio),后面再跟一个参数字节,比如 0x1F 表示 32 行面板。这里我好像理解了什么是控制字节和命令,控制字节0x00是在每个命令之前都要发送的,表示下面要发送命令,但是你这里又提出了设置0xa8的参数要发送0x1F,我看你的代码里面,也是在0x00之后发送0x1f,难道0x1f也是命令?为什么没有紧跟着0xa8呢?现在我还是不理解Co位和D/C位对应在0x00和0x40的哪些位置?显示之前通常都要先用一系列命令设置好“内存地址模式”(Page 模式 / Horizontal 模式 / Vertical 模式)、“页面起始地址”、“列起始地址”+“列结束地址”……以便告诉 SSD1306 后面连续发来的显存数据要怎么往 GDDRAM 里放,这一步怎么实现,我是小白,什么都不懂啊?

什么是“控制字节”,前面的iic通信流程里面没有讲啊?
我们知道主机会发送起始信号(地址+读/写),然后会发送命令(操作ssd1306寄存器)和数据(操作屏幕),但是其实在发送数据之前,主机还会向ssd1306发送控制字节(就仅仅8个位),这个“控制字节”本身不算是命令或数据,他有两个功能:
1,告诉ssd1306,接下来发的下一个字节,是命令(Command)还是要写到显存里的数据(Data)
2,告诉ssd1306,以后还会不会再发另一个控制字节。

这个控制字节是怎么组成的呢?

Bit7Bit6Bit5Bit4Bit3Bit2Bit1Bit0
CoD/C#000000

Co位和D/C#的作用:
Bit7(Co,Continuity即连续性)
如果 Co = 0:表示“我只发这一个控制字节,不紧跟另一个控制字节”。

就是说下一个字节是数据/命令,不是控制字节了,但是当发送完数据/命令,主机仍然可以发送0x00控制字节,只是两个控制字节不能连续发送

如果 Co = 1:表示“我这次发完这个控制字节后,还会接着发下一个控制字节
Bit6(D/C#,Data/Command即数据/命令)
如果 D/C# = 0:表示“接下来的字节,要被 SSD1306 当作命令(Command)来执行”。
如果 D/C# = 1:表示“接下来的字节,要被 SSD1306 当作数据(Data,写入显存 GDDRAM)”。
Bit5…Bit0(6 位)都必须填 0。

这里我们先举一个发送命令的例子:
首先明确:0xA8是命令Set Multiplex Ratio,0x1F 是命令 0xA8 的参数(也属于命令)
我们主机想要发送这两个命令给从机,它会这么发送呢?
1,发送0x00表示下一个是命令
2,0xA8表示Set Multiplex Ratio(但是这个命令还需要一个参数)
3,0x00又一个控制字节:因为 0xA8 还有参数要发
4,0x1F是命令 0xA8 的参数,属于命令范畴

注意:0x1F 不是新的“命令”,它只是“命令 0xA8 的参数”,但它也仍然是通过控制字节 0x00 告诉 SSD1306:下一个字节 (0x1F) 是命令相关的数据

这里0xA8是一个多字节命令,就是除了命令本身,还有配套的参数0x1F,这种命令发送时时,它自己和参数前面都要发送0x00,不能只发送一个0x00,然后两者紧接着发送
如果是单字节命令,就不需要考虑这些,直接发送0x00,然后在将自己发出去就行了

再举一个发送数据的例子:
首先我们在MCU即STM32中定义一个空间uint8_t buffer[512],这有512个字节,刚好覆盖128*64分辨率的oled的所有像素点
接下来通过iic通信将这512个字节发送到屏幕上:
1,0x40表示接下来主机要发送数据到显存 GDDRAM
2,buffer[0]
3,buffer[1]

4,buffer[512]发送完最后一个字节

参考手册:After the transmission of the slave address, either the control byte or the data byte may be sent across the SDA. A control byte mainly consists of Co and D/C# bits following by six 0’s … If the D/C# bit is set to logic 1, it defines the following data byte as data which will be stored at the GDDRAM. The GDDRAM column address pointer will be increased by one automatically after each data write.

到了这里,相信你已经理解了,控制字节的作用就是控制发送命令和数据,但是在发送数据之前,我们会发送命令给ssd1306,而不是直接发送数据,我因为我们要对ssd1306的寄存器进行配置,具体包括:内存地址模式,页面地址,列地址
下面结合前面的知识,我们一个一个进行配置:
1,内存地址模式:设置此模式的命令是0x20
此命令的参数是:
0x00 → Horizontal 模式
0x01 → Vertical 模式
0x02 → Page 模式

这三个模式的区别是什么呢?
Page 模式:每次写完 1 列就保持在当前页不动,需要你手动再发命令才换下一列,或者换下一页。
Vertical 模式:写完一个字节后,会自动把 “页指针”+1(从 Page0 跳到 Page1),到页末就翻回到起始页同时 “列指针”+1。
Horizontal 模式:写完一个字节后,会自动把 “列指针”+1(从 Column0 跳到 Column1),到列末就翻回起始列同时 “页指针”+1。

代码示例

// 发控制字节,告诉它我接下来要发命令
send_I2C(0x00);
// 发命令本身:Set Memory Addressing Mode
send_I2C(0x20);// 因为这条命令还要跟一个参数,所以再发控制字节
send_I2C(0x00);
// 发参数:0x02 → Page 模式
send_I2C(0x02);

2,页面起始地址
在 Page 模式下,显存被分成若干“页”,前面我们知道,8行为1页,128*32的分辨率就分成了4页,每一页对应的命令是:
Page0 → 0xB0
Page1 → 0xB1
Page2 → 0xB2
Page3 → 0xB3
发送对应的命令,就表示以这一页的开头作为起始地址,例如:

send_I2C(0x00);  // 控制字节:准备发命令
send_I2C(0xB1);  // 命令:Page1

发完这条命令,SSD1306 内部就把“当前页指针”切到 Page1。接下来再发 0x40 + 数据,就会把数据写入 Page1 这一行的 128 列里。

3,列地址
我们知道1页有8行,128列,前面我们通过命令设置了写入哪一个页(Page1),接下来通过命令就可以设置写入这个页Page1的哪一个列,这个命令分为两部分:列低地址命令和列高地址命令
列低地址命令0x00~0x0F,分别表示写入第0 ~ 15列
列高地址命令0x10~0x17,即0001 0000 ~ 0001 0111,结合列低地址命令表示第16 ~ 127列,这里每加1代表加16列,因为列低地址命令代表低4位,这个代表高3位
发送命令时列高地址命令和列低地址命令要一起发送组成一个完整的地址

列低地址命令 = 0x00 ~ 0x0F = (N & 0x0F)
列高地址命令 = 0x10 ~ 0x17 = 0x10 | ((N >> 4) & 0x07)

例如我要写在第66列:就发送0x01和0x14
send I2C 0x01 //发送低4位
send I2C 0x14 //发送高4位
结合起来就是4*16+2=66列
还不明白就自己思考,反正我理解了

其实就相当于ssd1306里面有两个8位的寄存器:列高寄存器(0001 0xxx)和列低寄存器(0000 xxxx),可以看到列低寄存器在0000 0000 ~ 0000 1111里面变化共16种可能,而列高寄存器在0001 0000 ~ 0001 0111里面变化,列低寄存器变化16位,列高寄存器就变化1位,所以总共就有8*16 = 128种可能,对应屏幕的128列,每次发送命令时,需要两个一起发送,组成对应的列号

4,重复 START

什么是“重复 START”?
I²C 允许在不发 STOP(释放总线)的情况下,再发起另一次 START 条件。这样做的好处是:
总线不中断:其他主机没法趁你 STOP 之后插队。
可以在同一次事务里先写命令、再读状态或数据

参考手册:If the master device needs to transmit more data, it can issue a Repeated START condition (SCL = HIGH, SDA: HIGH→LOW) without issuing a STOP. This avoids other masters from taking control of the bus in the middle of the operation.

在ssd1306中的用法是:先通过 PAGE 模式写入一部分显存,再 Re-START,切换到 Horizontal 模式写下一部分。所以无需在两个不同事务间 STOP,再重新 START。

为什么都是些数据,我们先通过pqge模式写,再通过 Horizontal 写呢?
回顾前面这2种模式的区别,pqge模式可以选择写的位置而且写完后指针不加1,而Horizontal 写完后指针加1
这就导致2种模式有自己的用处:Page 模式负责把起始指针“精准定位”到某一页某一列,定位后切换到 Horizontal 模式,一口气写后面要填的 N 列数据

5,STOP

通知所有从机“本次通信搞完了,可以释放总线”
把 SDA 和 SCL 都拉回到空闲状态(都 HIGH),让下一个想用总线的主机知道“现在可以发起新的 START”。

参考手册:The write mode will be finished when a stop condition is applied. The stop condition is … established by pulling the SDA from LOW to HIGH while the SCL stays HIGH

五,在 CubeMX 中配置 I²C 外设

1,Mode

Disable:禁用 I2C 外设。如果选择该选项,对应的IIC引脚就是普通的GPIO引脚
I2C:表示 I2C 外设工作在 I²C 通信模式,STM32 可以作为主机 (Master) 或从机 (Slave)。

当我们需要主机和和从机设备通信时,需要选择此模式

SMBus-Alert-mode、SMBus-two-wire-Interface:系统管理总线模式,是 I2C 的一个变种,具有更严格的时序和协议要求,例如超时检测、PEC (Packet Error Checking) 等。通常用于电源管理或特定的传感器。

2,Configuration

在这里插入图片描述

Clock Speed 为 100kHz 是 SSD1306 的推荐设置,完全满足其需求,避免了较高频率带来的不必要的噪声问题,为什么说这是推荐设置?从哪里看到呢?我是小白,不懂这些,这个配置在图中属于Master Feasures,这个配置一般在芯片手册的哪一个地方啊?如果我要开发其他的IIC芯片,这一项该怎么选择?看芯片手册的哪里?你总结一下常见的IIC芯片 对应这一项该选择什么?Master Feasures这一项的含义是什么呢?这个好像是针对主机即STM32CubeMX而言的,设置的速度有什么有呢?这是IIC通信的速度吗?Timing Configuration
Coefficient of Digital Filter 设置为 0:这个数字滤波器用于抑制干扰信号,通常对于 I2C 协议,默认设置即可,不需要修改。
Analog Filter 设置为 Enabled:这个模拟滤波器会启用 I2C 总线上的模拟过滤功能,默认启用即可。这里的数字滤波器是干什么的?有什么用?具体讲讲,我是小白,芯片参考手册里面有Timing Configuration的相关信息吗?Slave Features这是针对从机的配置吗?Clock No Stretch Mode 设置为 Disabled:这是控制 I²C 总线时钟拉伸功能的选项。由于 SSD1306 使用的是标准的 7 位地址 I²C 协议,因此可以保持禁用。什么是总线拉伸功能?为什么说7位地址就要保持禁用?在芯片手册里面能找到相关信息吗?Primary Address Length selection 设置为 7-bit:这表明你正在使用 7 位地址。SSD1306 使用的是 7 位地址,例如 0x3C,因此这一选项保持为 7 位是正确的。这里的Primary 代表什么特殊含义吗?这个能在芯片手册里面找到吗?Dual Address Acknowledged 设置为 Disabled:SSD1306 只有一个地址,不需要支持双地址。因此,禁用此选项是正确的。这里为什么又出现了双地址?什么是双地址?我是嵌入式小白,没有见过啊?能不能举一些例子?对于IIC芯片手册,一般在哪里能够找到地址信息啊?在哪里能够找到有无双地址的信息啊?Primary slave address 设置为 0:这表示主设备地址的设置。你不需要修改这个,主机通信时会使用从设备的地址。你说这个是主地址的设置,这个选项有什么应用场景吗?在芯片手册里面的哪里有相关的信息?General Call address detection 设置为 Disabled:不启用通用调用地址检测。对于 SSD1306,这个选项可以禁用,确保通信时只针对你设置的从设备地址。这里通用地址检查又是什么东西啊,我是小白,不懂啊?这里配置选项一般在芯片手册的哪里啊?

Parameter Settings

1,Master Feasures

这里有两个选项:
IIC Speed Mode I2C 时钟速度 :
IIC Clock Speed(Hz),IIC Speed Mode可以选择 Standard Mode 和 Fast Mode ,如果选择前者那么下面的IIC Clock Speed最大可以设置为100 000,如果选择Fast Mode 高速模式,则IIC Clock Speed最大可以设置为400 000

IIC Clock Speed是我们期望的频率,但是系统时钟输入到IIC的频率(通过APB1总线的频率)不等于IIC Clock Speed,还需要CCR(Clock Control Register)这个分频器,将IIC总线上的频率分频为我们期望的频率,为什么一定是分频呢?因为在我们选择IIC Clock Speed时,手册就要求 I2CCLK (APB1总线频率)高于我们选择IIC Clock Speed, I2CCLK 至少是 Standard Mode 频率的 2 倍,Fast Mode 频率的 3 倍

其实还有第三个选项Fast Mode Plus: 最高速率 1 MHz,但是这个需要外设和 GPIO 都支持 FMP(Fast Mode Plus)特性

对应SSD1306而言选择100 000 Hz的速度就足够了

当我们选择 Fast Mode 时:还可以设置 Clock Duty Cycle - Fast Mode快速模式占空比:
这里有两个选项:
DUTYCYCLE_2: 低电平时间是高电平时间的 2 倍。这是推荐和常用的设置。
DUTYCYCLE_16_9: 低电平时间是高电平时间的 16/9 倍。在某些特定从设备或总线条件下可能需要。
我们选择DUTYCYCLE_2即可

2,Timing Configuration即 数字滤波器和模拟滤波器

Coefficient of Digital Filter 数字滤波器:它处理的是 I²C 总线上的 数据,只有当总线噪声较大时,才需要考虑启用它。

Analog Filter 模拟滤波器:它帮助滤除噪声源,确保 I2C 数据和时钟信号不受干扰。SSD1306 的 I²C 通信中,通常会保持启用模拟滤波器来提高可靠性。

在 STM32 中,默认 启用模拟滤波器,这是因为 模拟信号更容易受到干扰,明明IIC总线上的信号是数字信号,哪里来的模拟信号呢?其实在IIC总线的数字信号之前是模拟信号,模拟滤波器用来在 I²C 总线的数字信号传输之前 处理这些噪声

3,Slave Features即从机特性

这些选项主要用于配置 I2C 总线的从设备功能,而不涉及主设备 STM32 的配置

Primary Addressing Length selection主要寻址模式:
可以选择7/10,这个选项只影响HAL库函数HAL_I2C_Master_Transmit 的地址参数处理,如果从机既有7位地址,又有10位地址,HAL_I2C_Master_Transmit函数可以自动根据传入的参数进行处理

Clock No Stretch Mode
I²C 总线的一些从设备可能会在数据传输时“拉伸”时钟线,表示它还在处理数据

Primary Address Length selection:选择主地址长度
在 I²C 通信中,SSD1306 的从设备地址通常是 0x3C,这就是 7 位地址

Primary Slave Address本机地址 1: 直接输入地址值

注意: 这里输入的是 7 位地址本身,不需要左移或添加读写位

Dual Address Acknowledged:即确认双地址
对于某些 I²C 设备,它们支持“双地址模式”,允许它们同时使用两个地址进行通信。对于 SSD1306,由于它只需要一个地址,因此禁用此选项。
如果使能该选项,CubeMX就会多出一个从地址,本机地址2
在这里插入图片描述
General Call Address Detection 广播呼叫地址检测:
之前说过7位地址最多不一定支持128个设备,还有些地址被保留了,0x00就是被保留的地址即广播呼叫地址
当主机向此地址发送信息时,使能此项的从机会响应发往0x00地址的信息

Clock Stretching时钟延长 :
只有在确定主机不支持时钟延长或追求极低延迟且能保证从机处理速度的情况下才考虑禁用。

GPIO Settings

GPIO Output level: 无需配置

此项表示GPIO输出的高低电平,因为我们使用的是硬件IIC,总线的电平由外设控制,不需要我们手动设置GPIO电平(软件IIC),所以不需要勾选此项

什么是软件iic?
用普通GPIO口,通过软件程序控制GPIO的电平变化,来模拟I2C的时序和协议
什么是硬件iic?
主机和从机的的IIC模块自动完成IIC通信,不需要我们手动配置GPIO电平

对于GPIO Output level,在硬件IIC模式时,就算我们配置了此项,也没有用,因为I2C模块自动驱动引脚,你手动设置电平是无效的,配置会被硬件覆盖或忽略。

GPIO mode: Alternate Function Open Drain

Alternate Function: 将引脚功能配置为连接到 I2C 外设,而不是通用输入输出。
Open Drain (开漏): 允许多个设备连接到同一总线。设备只能将线拉低,不能主动推高。高电平由外部上拉电阻提供。

GPIO Pull-up/Pull-down: Pull-up 或 No pull-up and no pull-down
I2C 协议要求 SCL 和 SDA 必须有上拉电阻。
选项:
Pull-up: 启用 STM32 内部自带的弱上拉电阻 (通常 30kΩ - 50kΩ)。仅适用于低速 (<=100kHz) 且总线负载很小 (设备少、线短) 的情况。
No pull-up and no pull-down: 推荐 禁用内部上拉。此时必须在 PCB 上为 SCL 和 SDA 各添加一个外部上拉电阻。 这是最可靠的做法,允许根据总线速度和电容选择合适的电阻值。
外部上拉电阻选择: 典型值在 1.5kΩ 到 10kΩ 之间。常用 4.7kΩ (适用于 100kHz/400kHz,中等负载)。更高速率或更大总线电容需要更小的上拉电阻 (如 2.2kΩ, 1.8kΩ)。具体计算需参考 I2C 规范和总线电容。

Maximum output speed: High 或 Very High
为确保信号边沿足够陡峭以满足 I2C 时序要求(尤其是在 Fast Mode 或更高速度下),应选择较高的 GPIO 输出速度

User Label: (可选) 为引脚添加自定义标签

六,SSD1306驱动库

/*
this library is a 0.91'OLED(ssd1306) driver
*///Header file reference
//The oledfont.h, oled.h and STM32's i2c.h files need to be referenced in the oled.c file
#include "oled.h"
#include "oledfont.h"
#include "i2c.h"#define OLED_ADDR_7BIT   0x3C          						// iic 的7位地址				or 0x3D取决于iic设备
#define OLED_ADDRESS     (OLED_ADDR_7BIT << 1)    // HAL_I2C_Mem_Write要求的 8 位地址/*** 0.91 "OLED initialization control word* Each control word can change the display properties of the screen according to the manufacturer's Datasheet* For example, in the fifth line from the bottom, the control word 0x81,0x80.you can changes the contrast by changing 0x80* 
*/
uint8_t initcmd1[] = {0xAE,		//display off0xD5, 0x80, //Set Display Clock Divide Ratio/Oscillator Frequency0xA8, 0x1F, //set multiplex Ratio			//128*32
//	0xA8, 0x3F, //set multiplex Ratio				//128*640xD3, 0x00, //display offset0x40,		//set display start line0x8d, 0x14, //set charge pump0xa1,		//set segment remap0xc8,		//set com output scan direction
//	0xda, 0x12, //set com pins hardware configuration					//128*640xda, 0x00, //set com pins hardware configuration				//128*320x81, 0x80, //set contrast control0xd9, 0x1f, //set pre-charge period0xdb, 0x40, //set vcom deselect level0xa4,		//Set Entire Display On/Off0xaf,		//set display on
};
//uint8_t initcmd_sh1106[] = {
//    0xAE,          // 关闭显示
//    0xD5, 0x80,    // 设置显示时钟分频比/振荡器频率
//    0xA8, 0x3F,    // 设置复用比率(SH1106通常使用0x3F)
//    0xD3, 0x00,    // 显示偏移
//    0x40,          // 设置显示起始行
//    0x8D, 0x14,    // 设置电荷泵
//    0xA1,          // 设置段重映射
//    0xC8,          // 设置COM输出扫描方向
//    0xDA, 0x12,    // 设置COM引脚硬件配置
//    0x81, 0x7F,    // 设置对比度控制(SH1106的对比度设置可以不同)
//    0xD9, 0x22,    // 设置预充电周期
//    0xDB, 0x30,    // 设置VCOM取消选择电平
//    0xA4,          // 全屏显示开启
//    0xAF,          // 打开显示
//};/*** OLED writes commands and data functions* OLED writes commands, data functions, and changes the contents of these two functions if you want to migrate them to another development board* For example: I use the i2c2 interface, then you only need to change &hi2c1 to &hi2c2.
**/
void OLED_Write_cmd(uint8_t cmd)
{HAL_I2C_Mem_Write(&hi2c1, OLED_ADDRESS, 0x00, I2C_MEMADD_SIZE_8BIT, &cmd, 1, 0x100);
}
void OLED_Write_data(uint8_t data)
{HAL_I2C_Mem_Write(&hi2c1, OLED_ADDRESS, 0x40, I2C_MEMADD_SIZE_8BIT, &data, 1, 0x100);
}/*** @brief	Image display function* @param x0  Image display start position x-axis* @param y0  Image display start position y-axis* @param x1  Image display end position x-axis 1 - 127* @param y1  Image display end position x-axis 1 - 4* @param BMP Image display pointer address* @note	The image needs to be converted to an array and passed into this function
*/
void OLED_ShowPic(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t BMP[])
{uint16_t i = 0;uint8_t x, y;for (y = y0; y < y1; y++){OLED_Set_Position(x0, y);for (x = x0; x < x1; x++){OLED_Write_data(BMP[i++]);}}
}/*** @brief	Display a 16*16 pixel Chinese character* @param x  position x-axis  0 - 127* @param y  position y-axis  0 - 3* @param no  The order of the Chinese characters in the hzk[] array* @note	The Chinese character library is in the Hzk array in the oledfont.h file, * You need to convert Chinese characters into arrays
*/
void OLED_ShowHanzi(uint8_t x, uint8_t y, uint8_t no)
{uint8_t t, adder = 0;OLED_Set_Position(x, y);for (t = 0; t < 16; t++){OLED_Write_data(Hzk[2 * no][t]);adder += 1;}OLED_Set_Position(x, y + 1);for (t = 0; t < 16; t++){OLED_Write_data(Hzk[2 * no + 1][t]);adder += 1;}
}/*** @brief	Display a 32*32 pixel Chinese character .all screen display* @param x  position x-axis  0 - 127* @param y  position y-axis  0* @param n  The order of the Chinese characters in the Hzb[] array* @note	
*/
void OLED_ShowHzbig(uint8_t x, uint8_t y, uint8_t n)
{uint8_t t, adder = 0;OLED_Set_Position(x, y);for (t = 0; t < 32; t++){OLED_Write_data(Hzb[4 * n][t]);adder += 1;}OLED_Set_Position(x, y + 1);for (t = 0; t < 32; t++){OLED_Write_data(Hzb[4 * n + 1][t]);adder += 1;}OLED_Set_Position(x, y + 2);for (t = 0; t < 32; t++){OLED_Write_data(Hzb[4 * n + 2][t]);adder += 1;}OLED_Set_Position(x, y + 3);for (t = 0; t < 32; t++){OLED_Write_data(Hzb[4 * n + 3][t]);adder += 1;}
}/*** @brief	Display a float * @param x  position x-axis  0 - 127* @param y  position y-axis  0* @param num  The order of the Chinese characters in the Hzb[] array* @param accuracy Preserve decimal places* @param fontsize 8/16* @note	
*/
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t accuracy, uint8_t fontsize)
{uint8_t i = 0;uint8_t j = 0;uint8_t t = 0;uint8_t temp = 0;uint16_t numel = 0;uint32_t integer = 0;float decimals = 0;//Is a negative number?if (num < 0){OLED_ShowChar(x, y, '-', fontsize);num = 0 - num;i++;}integer = (uint32_t)num;decimals = num - integer;//Integer partif (integer){numel = integer;while (numel){numel /= 10;j++;}i += (j - 1);for (temp = 0; temp < j; temp++){OLED_ShowChar(x + 8 * (i - temp), y, integer % 10 + '0', fontsize); // 显示整数部分integer /= 10;}}else{OLED_ShowChar(x + 8 * i, y, temp + '0', fontsize);}i++;//Decimal partif (accuracy){OLED_ShowChar(x + 8 * i, y, '.', fontsize);i++;for (t = 0; t < accuracy; t++){decimals *= 10;temp = (uint8_t)decimals;OLED_ShowChar(x + 8 * (i + t), y, temp + '0', fontsize);decimals -= temp;}}
}/*** @brief	OLED pow function* @param m - base* @param n - exponent* @return result
*/
static uint32_t OLED_Pow(uint8_t a, uint8_t n)
{uint32_t result = 1;while (n--){result *= a;}return result;
}/*** @brief	Display a uint32 Interger* @param x  position x-axis  0 - 127* @param y  position y-axis  0 - 3* @param num  Displayed integers* @param length Number of integer digits* @note	
*/
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t length, uint8_t fontsize)
{uint8_t t, temp;uint8_t enshow = 0;for (t = 0; t < length; t++){temp = (num / OLED_Pow(10, length - t - 1)) % 10;if (enshow == 0 && t < (length - 1)){if (temp == 0){OLED_ShowChar(x + (fontsize / 2) * t, y, ' ', fontsize);continue;}elseenshow = 1;}OLED_ShowChar(x + (fontsize / 2) * t, y, temp + '0', fontsize);}
}/*** @brief	Display ascii string* @param x  String start position on the X-axis  range:0 - 127* @param y  String start position on the Y-axis  range:0 - 3 * @param ch  String pointer* @param fontsize You can choose from two fonts 8/16
**/
void OLED_ShowStr(uint8_t x, uint8_t y, char *ch, uint8_t fontsize)
{uint8_t j = 0;while (ch[j] != '\0'){OLED_ShowChar(x, y, ch[j], fontsize);x += 8;if (x > 120){x = 0;y += (fontsize == 16) ? 2 : 1;  // 根据字体大小决定跳几页}j++;}
}/*** @brief	Displays ASCII characters* @param x  Character position on the X-axis  range:0 - 127* @param y  Character position on the Y-axis  range:0 - 3 * @param no  character* @param fontsize You can choose from three fonts 8/16
**/
void OLED_ShowChar(uint8_t x, uint8_t y, uint8_t ch, uint8_t fontsize)
{uint8_t c = 0, i = 0;c = ch - ' ';if (x > 127) //beyond the right boundary{x = 0;y++;}if (fontsize == 16){OLED_Set_Position(x, y);for (i = 0; i < 8; i++){OLED_Write_data(F8X16[c * 16 + i]);}OLED_Set_Position(x, y + 1);for (i = 0; i < 8; i++){OLED_Write_data(F8X16[c * 16 + i + 8]);}}else{OLED_Set_Position(x, y);for (i = 0; i < 6; i++){OLED_Write_data(F6X8[c][i]);}}
}/*** OLED fill function, after using the function 0.91 inch oled screen into full white
**/
void OLED_Allfill(void)
{uint8_t i, j;for (i = 0; i < 4; i++)						//根据分辨率修改总页数{OLED_Write_cmd(0xb0 + i);				//决定写白几页OLED_Write_cmd(0x00);OLED_Write_cmd(0x10);for (j = 0; j < 128; j++){OLED_Write_data(0xFF);}}
}/*** @brief Set coordinates* @param x: X position, range 0 - 127  Because our OLED screen resolution is 128*32, so the horizontal is 128 pixels* @param y: Y position, range 0 - 3    Because the vertical pixels are positioned in pages, each page has 8 pixels, so there are 4 pages
**/
void OLED_Set_Position(uint8_t x, uint8_t y)
{OLED_Write_cmd(0xb0 + y);											//决定哪一页写白OLED_Write_cmd(((x & 0xf0) >> 4) | 0x10);OLED_Write_cmd((x & 0x0f) | 0x00);
}//void OLED_Set_Position(uint8_t x, uint8_t y)
//{
//    OLED_Write_cmd(0xB0 + y);  // 页面寻址
//    OLED_Write_cmd(0x00 + (x & 0x0F));  // x地址的低四位
//    OLED_Write_cmd(0x10 + ((x >> 4) & 0x0F));  // x地址的高四位
//}/*** Clear Screen Function* Fill each row and column with 0
**/
void OLED_Clear(void)
{uint8_t i, n;for (i = 0; i < 8; i++){OLED_Write_cmd(0xb0 + i);									//决定消除几页OLED_Write_cmd(0x00);OLED_Write_cmd(0x10);for (n = 0; n < 128; n++){OLED_Write_data(0);}}
}
/*** Turn screen display on and off
**/
void OLED_Display_On(void)
{OLED_Write_cmd(0x8D);OLED_Write_cmd(0x14);OLED_Write_cmd(0xAF);
}
void OLED_Display_Off(void)
{OLED_Write_cmd(0x8D);OLED_Write_cmd(0x10);OLED_Write_cmd(0xAF);
}/*** Initialize the screen* Function:send control words one by one
**/
void OLED_Init(void)
{HAL_Delay(100);uint8_t i;for (i = 0; i < sizeof(initcmd1); i++){OLED_Write_cmd(initcmd1[i]); //display off}OLED_Clear();OLED_Set_Position(0, 0);
}
//void OLED_Init(void)
//{
//    HAL_Delay(100);  // 初始化前的延迟
//    uint8_t i;
//    for (i = 0; i < sizeof(initcmd_sh1106); i++)
//    {
//        OLED_Write_cmd(initcmd_sh1106[i]);  // 发送SH1106初始化命令
//    }//    OLED_Clear();  // 清屏
//    OLED_Set_Position(0, 0);  // 设置初始光标位置
//}
/*** @brief 将 WouoUI 缓冲区的数据发送到 OLED 显示* @param buff WouoUI 提供的缓冲区指针,大小为 [高度/8][宽度] 或 [4][128] for 128x32*/

1,初始化指令表 initcmd1[],包含了ssd1306的上电流程

2,底层写入函数:
void OLED_Write_cmd(uint8_t cmd)
void OLED_Write_data(uint8_t data)

3,设置初始写指针:
OLED_Set_Position(x, y)

4,全屏/清屏
OLED_Clear(); // 全 0
OLED_Allfill(); // 全 1

5,初始化流程 OLED_Init()

HAL_Delay(100);             // 上电稳定
for(i=0;i<sizeof(initcmd1);i++)OLED_Write_cmd(initcmd1[i]);
OLED_Clear();               // 防止上电随机 RAM
OLED_Set_Position(0,0);     // 光标归零

接下来详细介绍一下这个ssd1306驱动库,以至于我们可以将它移植到sh1106上面:

1,IIC通信层

OLED_Write_cmd和OLED_Write_data这两个函数其实就是对前面讲的IIC的API的再次封装:

void OLED_Write_cmd(uint8_t cmd)
{HAL_I2C_Mem_Write(&hi2c1, OLED_ADDRESS, 0x00, I2C_MEMADD_SIZE_8BIT, &cmd, 1, 0x100);
}void OLED_Write_data(uint8_t data)
{HAL_I2C_Mem_Write(&hi2c1, OLED_ADDRESS, 0x40, I2C_MEMADD_SIZE_8BIT, &data, 1, 0x100);
}

竟然HAL库都已经有IIC发送数据的专用API了,为什么我们还有再次封装它呢?
调用OLED_Write_cmd比直接调用HAL函数更清晰,我们直接在封装后的函数写入命令就行,不用在发送控制字节

2,亮灭控制与上电初始化

初始化命令initcmd1数组:

uint8_t initcmd1[] = {0xAE,		//display off0xD5, 0x80, //Set Display Clock Divide Ratio/Oscillator Frequency0xA8, 0x1F, //set multiplex Ratio			//128*32
//	0xA8, 0x3F, //set multiplex Ratio				//128*640xD3, 0x00, //display offset0x40,		//set display start line0x8d, 0x14, //set charge pump0xa1,		//set segment remap0xc8,		//set com output scan direction
//	0xda, 0x12, //set com pins hardware configuration					//128*640xda, 0x00, //set com pins hardware configuration				//128*320x81, 0x80, //set contrast control0xd9, 0x1f, //set pre-charge period0xdb, 0x40, //set vcom deselect level0xa4,		//Set Entire Display On/Off0xaf,		//set display on
};

这些命令来自于哪里?ssd1306的12832与12864分辨率的初始化命令有什么区别?

如果我要移植到sh1106上面需要修改什么?

上电初始化函数:

void OLED_Init(void)
{HAL_Delay(100);uint8_t i;for (i = 0; i < sizeof(initcmd1); i++){OLED_Write_cmd(initcmd1[i]); //display off}OLED_Clear();OLED_Set_Position(0, 0);
}

这里为什么要HAL_Delay?明明我才上电为什么要清屏?OLED_Set_Position有什么用?
HAL_Delay(100):确保OLED完全上电后再发送命令
清屏:确保显示从干净状态开始,避免上电时显示残留内容
OLED_Set_Position(0,0):将光标设置到初始位置,准备显示内容

3,设置坐标和清屏

OLED_Set_Position:设置光标位置(x,y)
OLED_Clear:清屏
OLED_Allfill:全屏填充白色

/*** OLED fill function, after using the function 0.91 inch oled screen into full white
**/
void OLED_Allfill(void)
{uint8_t i, j;for (i = 0; i < 4; i++)						//根据分辨率修改总页数{OLED_Write_cmd(0xb0 + i);				//决定写白几页OLED_Write_cmd(0x00);OLED_Write_cmd(0x10);for (j = 0; j < 128; j++){OLED_Write_data(0xFF);}}
}/*** @brief Set coordinates* @param x: X position, range 0 - 127  Because our OLED screen resolution is 128*32, so the horizontal is 128 pixels* @param y: Y position, range 0 - 3    Because the vertical pixels are positioned in pages, each page has 8 pixels, so there are 4 pages
**/
void OLED_Set_Position(uint8_t x, uint8_t y)		//把“写数据指针”移动到 列 x、页 y,
{OLED_Write_cmd(0xb0 + y);											//决定哪一页写白OLED_Write_cmd(((x & 0xf0) >> 4) | 0x10);			//列地址“高四位”OLED_Write_cmd((x & 0x0f) | 0x00);						//列地址“低四位”
}/*** Clear Screen Function* Fill each row and column with 0
**/
void OLED_Clear(void)
{uint8_t i, n;for (i = 0; i < 8; i++){OLED_Write_cmd(0xb0 + i);									//决定消除几页OLED_Write_cmd(0x00);OLED_Write_cmd(0x10);for (n = 0; n < 128; n++){OLED_Write_data(0);}}
}

这里的清屏和覆盖全屏操作中的命令0xb0+i,0x01是什么含义?
0xb0+i:设置页地址(0-7),每页8像素高
0x00和0x10:设置列地址的低4位和高4位

不是说在发送命令之前都要先发送0x00吗?为什么在0xb0+i之前没有呢?
OLED_Write_cmd函数已经通过参数0x00指明了这是命令而非数据

这里的设置坐标的函数逻辑是什么啊?

4,文本显示层

字符显示:OLED_ShowChar (支持8x6和8x16字体)

字符串显示:OLED_ShowStr

数字显示:OLED_ShowNum和OLED_ShowFloat

5,高级图形显示层

中文显示:OLED_ShowHanzi(16x16)和OLED_ShowHzbig(32x32)

图片显示:OLED_ShowPic

这里的文本显示层和图像显示层怎么使用啊?
下面是我使用显示字符串的代码:

int Oled_Printf(uint8_t x, uint8_t y, const char *format, ...)
{char buffer[128]; 															// 缓冲区大小根据需要调整va_list arg;int len;va_start(arg, format);len = vsnprintf(buffer, sizeof(buffer), format, arg);va_end(arg);OLED_ShowStr(x, y, buffer, 8); 									// 将 buffer 转为 uint8_t*,为什么不是char//8:F6*8,一行写满跳一页    16:F8*16像素,一行写满跳两页return len;}

下面的函数没有介绍啊?

/*** @brief	OLED pow function* @param m - base* @param n - exponent* @return result
*/
static uint32_t OLED_Pow(uint8_t a, uint8_t n)
{uint32_t result = 1;while (n--){result *= a;}return result;
}

这是一个辅助函数,计算a的n次方。在OLED_ShowNum函数中用于从整数中提取各个位的数字。例如,计算10^3用于提取千位数字

相关文章:

  • Semaphore - 信号量
  • CPP基础
  • 西门子 S7-1200 PLC 海外远程运维技术方案
  • DAX权威指南8:DAX引擎与存储优化
  • 第七章:未名湖畔的樱花网关
  • 书籍推荐 --- 《筚路维艰:中国经济社会主义路径的五次选择》
  • 【信息系统项目管理师-案例真题】2025上半年(第二批)案例分析答案和详解(回忆版)
  • ​​Java 异常处理​​ 的详细说明及示例,涵盖 try-catch-finally、自定义异常、throws 与 throw 的核心概念和使用场景
  • 在Mathematica中实现Newton-Raphson迭代的收敛时间算法(一般三次多项式)
  • Benchmarking Potential Based Rewards for Learning Humanoid Locomotion
  • 关于锁策略的简单介绍
  • 固态继电器与驱动隔离器:电力系统的守护者
  • C++.OpenGL (6/64)坐标系统(Coordinate Systems)
  • 为什么要对邮件列表清洗?
  • C++ --- vector
  • 深入理解指针(二)
  • [蓝桥杯]整理玩具
  • 如何使用 Bulk Rename Utility 批量为文件名添加统一后缀?
  • CountDownLatch和CyclicBarrier
  • 森马下沙奥莱旗舰店盛大启幕:以“新常服“理念重塑消费体验新范式
  • 软件开发培训班哪个好/seo
  • 网站建设的社会/seo站内优化技巧
  • 郑州做网站要多少钱/google 官网入口
  • 免费的网站建设开发/软文平台
  • 中国能源建设集团网站群/十大看免费行情的软件下载
  • 农村网站建设必要性/腾讯广告投放推广平台