STM32——OLED外设库的讲解(1/2)
引言
本来我是不打算说OLED这个外设的,但我发现好多教程只是在教你原理,而不教你如何构建库。也正是这个原因出现了很多C/V工程师。
目的
在这个章节中我会让你知道这个库如何建立,为什么要这么建立,我自己的库我该如何使用!
注意
当你了解IIC协议,OLED工作原理后再去看!不了解你甚至都不知道在干什么!
一个合格的OLED库
OLED初始化
void OLED_GPIO_Init(void)
{
uint32_t i, j;
for (i = 0; i < 1000; i ++)
{
for (j = 0; j < 1000; j ++);
}
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
首先解决第一个问题,为什么我要写一个循环,因为在某些硬件上,当OLED或其他设备刚上电时,需要一些时间让其稳定工作。因此,加入这样的延时可以确保在初始化GPIO之前,OLED或其他设备已经准备好。(其实是在给OLED上的VCC引脚充足的时间为其供电)但请注意,这种延时方式并不是最精确的,并且其实际延时时间取决于CPU的速度和编译器优化。但对于我们小工程来说完全可以了哈!
IIC协议设定
void OLED_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_6, (BitAction)BitValue);
}
void OLED_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_7, (BitAction)BitValue);
}
IIC协议就是控制SCL,SDA两线程的高低电平变化从而达到控制数据的收发功能。所以IIC对高低电平的控制函数就OLED库构建的地基。OLED所有显示函数都少不了对SCL,SDA电平的控制。
具体我就不说了,明天我会主要讲讲IIC协议。
直接看代码!
GPIO_WriteBit(GPIOB, GPIO_Pin_6, (BitAction)BitValue);我觉得大家应该都知道,这其实就是将(BitAction)BitValue写入GPIOB的引脚6。这里的问题就是(BitAction)BitValue是什么意思。
(BitAction)BitValue
这里将BitValue
强制转换为BitAction
枚举类型。BitAction
是一个枚举类型,通常定义在STM32的GPIO库中,它有两个可能的值:Bit_RESET
(通常为0)和Bit_SET
(通常为1)。
直接看我们的固件库。
typedef enum
{ Bit_RESET = 0,
Bit_SET
}BitAction;
含义是设定一个枚举类型,名字叫BitAction。它的取值分为0(Bit_RESET)/1(Bit_SET)。
所以说,我这样使用OLED_W_SCL(1)意思就是将SCL拉成高电平!下一个操作SDA的函数同理。
但我要说的是,操作SCL,SDA时如果单片机运行速度过快,就有可能无法捕捉SCL,SDA的设定。所以我们在这里可以加入一个延时函数。专业来讲
当微控制器的时钟速度很快时,即使你的代码逻辑是正确的,但由于执行速度过快,可能会在I2C时序要求上产生问题。例如,你可能在SCL线的一个时钟周期内更改了SDA线的值多次,或者SDA线的值在SCL线的一个时钟周期内没有保持稳定足够长的时间。
为了避免这种情况,有时需要在SDA线的值改变后添加一些延时,以确保SDA线的值在SCL线的时钟周期内保持稳定。这种延时通常被称为“位时间”或“位周期”延时,并且它的长度应该与I2C通信的时钟速度相匹配。
那么接下来OLED显示的函数就是对SCL,SDA的配合进行操作。
OLED显示
/**
* 函 数:I2C起始
* 参 数:无
* 返 回 值:无
*/
void OLED_I2C_Start(void)
{
OLED_W_SDA(1); //释放SDA,确保SDA为高电平
OLED_W_SCL(1); //释放SCL,确保SCL为高电平
OLED_W_SDA(0); //在SCL高电平期间,拉低SDA,产生起始信号
OLED_W_SCL(0); //起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}
/**
* 函 数:I2C终止
* 参 数:无
* 返 回 值:无
*/
void OLED_I2C_Stop(void)
{
OLED_W_SDA(0); //拉低SDA,确保SDA为低电平
OLED_W_SCL(1); //释放SCL,使SCL呈现高电平
OLED_W_SDA(1); //在SCL高电平期间,释放SDA,产生终止信号
}
配合我的图看吧!
灰色部分表示产生动作,所以我们只看灰色前的操作就行。所以当释放SDA(使SDA从低电平变为高电平), 释放SCL(使SCL保持高电平)——>拉低SDA后就会产生起始信号,及意味着我的IIC准备好了。但是在其他操作时我们仍需要将SCL拉低,这样称为占用总线,意思是告诉IIC谁已经准备好和你通信了!所以为了方便我们将SCL拉低一起写入产生起始信号的函数中。
那么在停止信号函数中,我们将SDA拉低,将SCL释放(意思是我们的通讯已经结束了,我SCL已经不需要占据总线了,所以将SCL释放)——>但你自己的SCL释放了,仅仅只有你SCL知到,所以为了确保SDA也清楚,我们就需要叫SDA释放(称为应答)。函数结束后,IIC停止传输数据。
再详细生动点讲,我会在明天说,所以先点个关注吧。
void OLED_I2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
OLED_W_SDA(!!(Byte & (0x80 >> i)));
OLED_W_SCL(1); //释放SCL,从机在SCL高电平期间读取SDA
OLED_W_SCL(0); //拉低SCL,主机开始发送下一位数据
}
OLED_W_SCL(1); //额外的一个时钟,不处理应答信号
OLED_W_SCL(0);
}
一个字节有八位所以当我们应当一位一位地发送,因此我们这里使用到了一个循环。
OLED_W_SDA(!!(Byte & (0x80 >> i)))
0x80 >> i
: 通过右移操作,我们得到从最高位(第7位)开始,每一位都为1的掩码。例如,当i=0
时,掩码为0x80
(二进制10000000
);当i=1
时,掩码为0x40
(二进制01000000
),以此类推。Byte & (0x80 >> i)
: 使用按位与操作,我们可以检查Byte
的当前位(由i
指定)是否为1。
!!
: 这是一个双非操作。在C语言中,非操作!
会将非零值转换为0,将零值转换为1。双非操作确保结果要么是0(如果Byte
的当前位为0),要么是1(如果Byte
的当前位为1)。这样,我们就可以得到一个表示当前位状态的布尔值(0或1)。OLED_W_SDA(...)
: 这是一个函数调用,用于设置SDA线的状态。根据前面的解释,它将设置SDA线为高(如果Byte
的当前位为1)或低(如果Byte
的当前位为0)。
OLED_W_SCL(1)
将SCL线设置为高电平,允许从机(在这里是OLED屏幕)在SCL高电平期间读取SDA线的状态。
OLED_W_SCL(0)
将SCL线拉低,准备发送下一位数据。
但在发送完8位数据后,通常会有一个额外的时钟周期用于从机(如OLED屏幕)发送应答信号(ACK)。因为OLED屏幕不需要应答信号但此周期确实存在所以我们重复上一个周期的SCL的操作即可。
到这里,你已经攻克了OLED中最难理解的部分!
出厂函数
(0xAE); //设置显示开启/关闭,0xAE关闭,0xAF开启
(0xD5); //设置显示时钟分频比/振荡器频率
(0x80); //0x00~0xFF
(0xA8); //设置多路复用率
(0x3F); //0x0E~0x3F
(0xD3); //设置显示偏移
(0x00); //0x00~0x7F
(0x40); //设置显示开始行,0x40~0x7F
(0xA1); //设置左右方向,0xA1正常,0xA0左右反置
(0xC8); //设置上下方向,0xC8正常,0xC0上下反置
(0xDA); //设置COM引脚硬件配置
(0x12);
(0x26);
(0x29);
(0x81); //设置对比度
(0xCF); //0x00~0xFF
(0xD9); //设置预充电周期
(0xF1);
(0xDB); //设置VCOMH取消选择级别
(0x30);
(0xA4); //设置整个显示打开/关闭
(0xA6); //设置正常/反色显示,0xA6正常,0xA7反色
(0x8D); //设置充电泵
(0x14);
(0xAF); //开启显示
这些数据都是OLED出厂 就有的函数,当我们给OLED发送这些信号后,我们的OLED就会根据以上代码的解析作出反应,无需我们编程!!!!(还有很多出厂函数,小伙伴们可以自行上网搜索,我们就不列举了)
所以他们的使用往往是
先打开IIC,再使用OLED_I2C_SentByte(0xAE),再写入我们要发送的数据,再关闭IIC。
但我们总不能用一次就写一次吧,所以我们将其写入一个集成函数中
void OLED_WriteCommand(uint8_t Command)
{
OLED_I2C_Start(); //I2C起始
OLED_I2C_SendByte(0x78); //发送OLED的I2C从机地址
OLED_I2C_SendByte(0x00); //控制字节,给0x00,表示即将写命令
OLED_I2C_SendByte(Command); //写入指定的命令
OLED_I2C_Stop(); //I2C终止
}
void OLED_WriteData(uint8_t *Data, uint8_t Count)
{
uint8_t i;
OLED_I2C_Start(); //I2C起始
OLED_I2C_SendByte(0x78); //发送OLED的I2C从机地址
OLED_I2C_SendByte(0x40); //控制字节,给0x40,表示即将写数量
/*循环Count次,进行连续的数据写入*/
for (i = 0; i < Count; i ++)
{
OLED_I2C_SendByte(Data[i]); //依次发送Data的每一个数据
}
OLED_I2C_Stop(); //I2C终止
}
这里的0x78,0x00,0x40也属于原厂函数哦。
在这里我只解释
0x78
发送字节0x78
,这通常是OLED显示屏的I2C从机地址(也称为设备地址或目标地址)。在I2C通信中,发送方(通常是微控制器或处理器)需要知道它正在与哪个设备通信。(就是选择我们的设备!)
0x00 0x40
一个字节0x00
。这个字节通常是控制字节或命令/数据选择字节。
在我们OLED显示屏的I2C通信协议中,0x00
通常表示接下来的数据是命令(而不是数据)。这意味着后续发送的字节将被解释为命令,而不是要在屏幕上显示的数据。与之相对,另一个常见的值(例如0x40
)表示接下来的数据是显示数据,应该被写入显示屏的某个位置。
0x00后面出现了OLED_I2C_SendByte(Command)(我们发送命令的函数)
0x40后出现了
for (i = 0; i < Count; i ++)
{
OLED_I2C_SendByte(Data[i]); //依次发送Data的每一个数据
}
OLED显示的本质
OLED显示的本质就是操作我们的像素点,通过改变像素点的位置与分布来控制我们的内容。
我们举一个清屏函数
void OLED_Clear(void)
{
uint8_t i, j;
for (j = 0; j < 8; j ++) //遍历8页
{
for (i = 0; i < 128; i ++) //遍历128列
{
OLED_DisplayBuf[j][i] = 0x00; //将显存数组数据全部清零
}
}
}
我们将OLED的8页内容的每页的128列全部设置为空(0000 0000)。
对于uint8_t OLED_DisplayBuf[8][128];其实就是OLED显示的一个缓冲序列,我们想让OLED现实的内容都将会先进入此区间等待我们操作。因为OLED显存数组所有的显示函数,都只是对此显存数组进行读写,随后调用OLED_Update函数或OLED_UpdateArea函数才会将显存数组的数据发送到OLED硬件进行显示。
循循渐进我们直接来讲讲更新函数!
void OLED_Update(void)
{
uint8_t j;
/*遍历每一页*/
for (j = 0; j < 8; j ++)
{
/*设置光标位置为每一页的第一列*/
OLED_SetCursor(j, 0);
/*连续写入128个数据,将显存数组的数据写入到OLED硬件*/
OLED_WriteData(OLED_DisplayBuf[j], 128);
}
}
我们将光标放入8页中每一页的第一列,这样子我们便可以在第一列按顺序写入我们缓存中的数据!其他的我们都认识但是这里出现了一个SetCursor函数,那么我们继续往回看!
void OLED_SetCursor(uint8_t Page, uint8_t X)
{
/*通过指令设置页地址和列地址*/
OLED_WriteCommand(0xB0 | Page); //设置页位置
OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); //设置X位置高4位
OLED_WriteCommand(0x00 | (X & 0x0F)); //设置X位置低4位
}
光标函数,我们将通过命令设置页的位置,列的位置!(相当于一个框出页面)
呢么我们认识一下区域更新函数。
void OLED_UpdateArea(uint8_t X, uint8_t Y, uint8_t Width, uint8_t Height)
{
uint8_t j;
/*参数检查,保证指定区域不会超出屏幕范围*/
if (X > 127) {return;}
if (Y > 63) {return;}
if (X + Width > 128) {Width = 128 - X;}
if (Y + Height > 64) {Height = 64 - Y;}
/*遍历指定区域涉及的相关页*/
/*(Y + Height - 1) / 8 + 1的目的是(Y + Height) / 8并向上取整*/
for (j = Y / 8; j < (Y + Height - 1) / 8 + 1; j ++)
{
/*设置光标位置为相关页的指定列*/
OLED_SetCursor(j, X);
/*连续写入Width个数据,将显存数组的数据写入到OLED硬件*/
OLED_WriteData(&OLED_DisplayBuf[j][X], Width);
}
}
选定特定的页与列,然后放置光标,填入我们缓冲序列中的内容。(值得一提的是,我们自选时为了防止选定区域时超出范围造成乱码,我们需要有检查参数的函数)
首先,函数检查X和Y的值是否超出了屏幕的范围(假设屏幕宽度为128像素,高度为64像素)。如果超出,函数立即返回,不执行任何操作。
接下来,它检查更新区域的右边界和下边界是否超出了屏幕范围。如果超出,它会相应地调整Width和Height的值,以确保更新区域完全在屏幕内。
for (j = Y / 8; j < (Y + Height – 1) / 8 + 1; j ++)
这个循环遍历了更新区域涉及的“页”。在OLED显示中,显存通常是以字节(8位)为单位组织的,而不是以像素为单位。因此,即使一个像素行只有几个像素宽,它也会占用整个字节。
Y / 8 计算出更新区域的起始页。
(Y + Height – 1) / 8 + 1 计算出更新区域的结束页(注意这里使用-1和+1是为了确保包含完整的最后一行)。
已经不早了,楼主要睡了哈,剩下的明天更新!!!
作者:STM大善人