发新帖本帖赏金 30.00元(功能说明)我要提问
返回列表
打印
[PIC®/AVR®/dsPIC®产品]

一切都要从我捡到一个51单片机说起,深度再学习驱动OLED

[复制链接]
17642|18
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 gaoyang9992006 于 2023-4-23 16:15 编辑

#申请原创# @21小跑堂
捡到了一个十几年前学单片机时候的入门单片机:AT89S52,于是我觉得我应该做点什么,又去翻腾垃圾捡了一片DS1302,从一个玩具上捡到一个32.768KHZ晶振,又找到了一片0.91寸,I2C接口的OLED。还有一个ADC,TLC2543,DS12887,于是去嘉立创薅羊毛打了一个板子开始玩起来。
3D效果图


实物图


以前做什么都是本着能用就好的拿来主义,很少去深度思考人家怎么写的。比如这个OLED模块,大部分都是用厂家提供的那一套,没有思考怎么写的,底层怎么实现的,也就是不重复造轮子,然而我今天闲了,要看看轮子怎么造的,最后我也要仿造人家的轮子做个零件出来。
1、I2C的基础操作函数
看了一下厂家提供的示例,比如51单片机用IO模拟I2C的基础函数有:起始信号、结束信号、等待信号响应
//起始信号
void I2C_Start(void)
{
OLED_SDA_Set();
OLED_SCL_Set();
IIC_delay();
OLED_SDA_Clr();
IIC_delay();
OLED_SCL_Clr();

}

//结束信号
void I2C_Stop(void)
{
OLED_SDA_Clr();
OLED_SCL_Set();
IIC_delay();
OLED_SDA_Set();
}

//等待信号响应
void I2C_WaitAck(void) //测数据信号的电平
{
OLED_SDA_Set();
IIC_delay();
OLED_SCL_Set();
IIC_delay();
OLED_SCL_Clr();
IIC_delay();
}


2、I2C的字节写入函数:写入一个字节
//写入一个字节
void Send_Byte(u8 dat)
{
u8 i;
for(i=0;i<8;i++)
{
OLED_SCL_Clr();//将时钟信号设置为低电平
if(dat&0x80)//将dat的8位从最高位依次写入
{
OLED_SDA_Set();
}
else
{
OLED_SDA_Clr();
}
IIC_delay();
OLED_SCL_Set();
IIC_delay();
OLED_SCL_Clr();
dat<<=1;
}
}


3、在以上的基础函数前提下可以操作OLED了,通过以上的组合可以实现给OLED写入指令或数据
//发送一个字节
//向SSD1306写入一个字节。
//mode:数据/命令标志 0,表示命令;1,表示数据;
void OLED_WR_Byte(u8 dat,u8 mode)
{
I2C_Start();
Send_Byte(0x78);
I2C_WaitAck();
if(mode){Send_Byte(0x40);}
else{Send_Byte(0x00);}
I2C_WaitAck();
Send_Byte(dat);
I2C_WaitAck();
I2C_Stop();
}
4、利用基础的写入操作可以实现上层次的传送各种指令和数据给OLED的控制器SSD1306了。
/*
坐标设置,对于128*32分辨率的OLED:x从127;y从0到3
*/

void OLED_Set_Pos(u8 x, u8 y)
{
OLED_WR_Byte(0xb0+y,OLED_CMD);
OLED_WR_Byte(((x&0xf0)>>4)|0x10,OLED_CMD);
OLED_WR_Byte((x&0x0f),OLED_CMD);
}
//开启OLED显示
void OLED_Display_On(void)
{
OLED_WR_Byte(0X8D,OLED_CMD); //SET DCDC命令
OLED_WR_Byte(0X14,OLED_CMD); //DCDC ON
OLED_WR_Byte(0XAF,OLED_CMD); //DISPLAY ON
}
//关闭OLED显示
void OLED_Display_Off(void)
{
OLED_WR_Byte(0X8D,OLED_CMD); //SET DCDC命令
OLED_WR_Byte(0X10,OLED_CMD); //DCDC OFF
OLED_WR_Byte(0XAE,OLED_CMD); //DISPLAY OFF
}
//清屏函数,清完屏,整个屏幕是黑色的!和没点亮一样!!!
void OLED_Clear(void)
{
u8 i,n;
for(i=0;i<4;i++)
{
OLED_WR_Byte (0xb0+i,OLED_CMD); //设置页地址(0~7)
OLED_WR_Byte (0x00,OLED_CMD); //设置显示位置—列低地址
OLED_WR_Byte (0x10,OLED_CMD); //设置显示位置—列高地址
for(n=0;n<128;n++)OLED_WR_Byte(0,OLED_DATA);
} //更新显示
}



//初始化
void OLED_Init(void)
{

OLED_WR_Byte(0xAE,OLED_CMD); /*display off*/
OLED_WR_Byte(0x00,OLED_CMD); /*set lower column address*/
OLED_WR_Byte(0x10,OLED_CMD); /*set higher column address*/
OLED_WR_Byte(0x00,OLED_CMD); /*set display start line*/
OLED_WR_Byte(0xB0,OLED_CMD); /*set page address*/
OLED_WR_Byte(0x81,OLED_CMD); /*contract control*/
OLED_WR_Byte(0xff,OLED_CMD); /*128*/
OLED_WR_Byte(0xA1,OLED_CMD); /*set segment remap*/
OLED_WR_Byte(0xA6,OLED_CMD); /*normal / reverse*/
OLED_WR_Byte(0xA8,OLED_CMD); /*multiplex ratio*/
OLED_WR_Byte(0x1F,OLED_CMD); /*duty = 1/32*/
OLED_WR_Byte(0xC8,OLED_CMD); /*Com scan direction*/
OLED_WR_Byte(0xD3,OLED_CMD); /*set display offset*/
OLED_WR_Byte(0x00,OLED_CMD);
OLED_WR_Byte(0xD5,OLED_CMD); /*set osc division*/
OLED_WR_Byte(0x80,OLED_CMD);
OLED_WR_Byte(0xD9,OLED_CMD); /*set pre-charge period*/
OLED_WR_Byte(0x1f,OLED_CMD);
OLED_WR_Byte(0xDA,OLED_CMD); /*set COM pins*/
OLED_WR_Byte(0x00,OLED_CMD);
OLED_WR_Byte(0xdb,OLED_CMD); /*set vcomh*/
OLED_WR_Byte(0x40,OLED_CMD);
OLED_WR_Byte(0x8d,OLED_CMD); /*set charge pump enable*/
OLED_WR_Byte(0x14,OLED_CMD);
OLED_Clear();
OLED_WR_Byte(0xAF,OLED_CMD); /*display ON*/
}
5、有了以上基础接下来研究传送进去的数据是怎么对应的显示在128*32的点阵上的。

这个图如果看不懂,就看我的文字,这个芯片最大支持128*64,我手里用的0.91寸的只有128*32,也就是只使用PAGE0~PAGE3
屏幕的点阵横着看作x,即x列,总数是128列,x∈[0,127]
屏幕的点阵竖向看作y,即y行,总数是032行,y∈[0,031]
而芯片写入是按照页写入的,即y属于PAGE0~PAGE3。
所以这一点很重要。注意,下面这个厂家提供的操作函数种的y是对应的页的编号
/*
坐标设置,对于128*32分辨率的OLED:x从127;y从0到3
*/

void OLED_Set_Pos(u8 x, u8 y) ;
于是乎显示一个1,我们要把1图像的每一列的8BIT装进一个页的数据里。例如下面的图像
                                         
这是6*8大小的点阵字符,如果从第一行写,那么就是写入第0页。然后将对应列的几个字节按顺序写入即可。
例如我写入左下角,那么对应的就是PAGE3,然后x坐标对应0,1,2,3,4,5
 OLED_Set_Pos(0,3);
OLED_WR_Byte(0x00,OLED_DATA);
OLED_Set_Pos(1,3);
OLED_WR_Byte(0x00,OLED_DATA);
OLED_Set_Pos(2,3);
OLED_WR_Byte(0x42,OLED_DATA);
OLED_Set_Pos(3,3);
OLED_WR_Byte(0x7F,OLED_DATA);
OLED_Set_Pos(4,3);
OLED_WR_Byte(0x40,OLED_DATA);
OLED_Set_Pos(5,3);
OLED_WR_Byte(0x00,OLED_DATA);
显示效果

所以明白了这一点,就可以实现各种自定义的图像了,另外也可以使用相关的生成工具生成相关的图像编码。
比如我们绘制一个电池的图标

将11个字节数据放到一个数组,这样我们可以用循环调用。
 unsigned char temp[11]={0x42,0xFF,0x81,0xBD,0xBD,0xBD,0xBD,0xBD,0x81,0xFF,0x18};
考虑到刚才显示1的那个位置有鼓包,我们将其向右便宜20个像素点放置。同样放在第三页显示。
 for(i=0;i<11;i++)
{
OLED_Set_Pos(i+20,3);
OLED_WR_Byte(temp[i],OLED_DATA);
}
delay_ms(2000);
显示效果如下,是不是很赞,现在你是不是学会显示任何图案了?

接下来我们造一个函数实现一个点的显示,参数为p(x,y)的绝对坐标
/*
x:0~127;y:0~31
*/

void setPixel(int x, int y)
{
unsigned char page;
unsigned char bits;
page = y / 8;
bits = y % 8;
OLED_Set_Pos(x,page);
OLED_WR_Byte(1<<bits,OLED_DATA);
}
利用这个函数我们可以绘制正弦曲线了。
接下来测试51使用math.h库函数计算正弦波图像,用于显示正弦波,先直接输出一个,然后翻转一个显示。
//正弦波
for(i=0;i<128;i++)
{
y=16.0+sin(i*3.1415926/32.0)*16.0;
j=(unsigned int)(y);
setPixel(i,j);

}
OLED_Clear();
//正弦波
for(i=0;i<128;i++)
{
y=16.0-sin(i*3.1415926/32.0)*16.0;
j=(unsigned int)(y);
setPixel(i,j);

}
OLED_Clear();
请注意上面的函数,因为计算过程,正弦函数出来的都是0到1之间的小数,所以要用浮点型,即y为浮点型变量,参与计算的常数也要写作浮点型,免得给优化掉,这样就只能出来一条线了。。。
同样如果更改周期参数即可实现不同周期的正弦波显示。
   for(k=8;k<=64;k=k*2)
{
for(i=0;i<128;i++)
{
y=16.0-sin(i*3.1415926/(float)k)*16.0;
j=(unsigned int)(y);
setPixel(i,j);
}
OLED_Clear();
}
https://www.bilibili.com/video/BV1Vv4y1E7zb?t=3.8




使用特权

评论回复
沙发
21小跑堂| | 2023-4-23 16:35 | 只看该作者
执行力max

使用特权

评论回复
板凳
gaoyang9992006|  楼主 | 2023-4-23 19:43 | 只看该作者
如果需要同时使用多个页显示一个符号,可以通过判断是否需要切换下一个页。

比如这个大大的带框的1.
根据点阵写出占用60个字节的数据
unsigned char yi[60]={
0XFC,0XFC,0X0C,0X0C,0X0C,0X0C,0X0C,0X0C,0X8C,0X0C,0X0C,0X0C,0X0C,0XFC,0XFC,
0XFF,0XFF,0X00,0X00,0X00,0X00,0X02,0XFF,0XFF,0X00,0X00,0X00,0X00,0XFF,0XFF,
0XFF,0XFF,0X00,0X00,0X00,0X00,0X80,0XFF,0XFF,0X80,0X00,0X00,0X00,0XFF,0XFF,
0X3F,0X3F,0X30,0X30,0X30,0X30,0X30,0X30,0X30,0X30,0X30,0X30,0X30,0X3F,0X3F};
 for(i=0;i<60;i++)
        {
                OLED_Set_Pos(i%15,i/15);
                OLED_WR_Byte(yi[i],OLED_DATA);
        }
这是从第一个位置显示,如果需要指定起点坐标添加一个偏移量在第一个参数即可。

使用特权

评论回复

打赏榜单

21小跑堂 打赏了 30.00 元 2023-04-26
理由:恭喜通过原创审核!期待您更多的原创作品~

地板
天灵灵地灵灵| | 2023-4-23 22:21 | 只看该作者
讲的很不错,我就是那种会用不懂的。

使用特权

评论回复
5
xch| | 2023-4-24 16:34 | 只看该作者
厉害!

使用特权

评论回复
6
woai32lala| | 2023-4-24 17:11 | 只看该作者
学习

使用特权

评论回复
7
chineseboyzxy| | 2023-4-25 11:21 | 只看该作者
07年用12C887和AT89C51做了一个数码管时钟,钉个钉子挂墙上,用了好几年。废旧报警器里抠的12887和89C51,路边捡来的苹果机数码管显示板割掉了一块儿带2003驱动和BCD-7段译码器和数码管的板子。

使用特权

评论回复
8
gaoyang9992006|  楼主 | 2023-4-26 08:57 | 只看该作者
chineseboyzxy 发表于 2023-4-25 11:21
07年用12C887和AT89C51做了一个数码管时钟,钉个钉子挂墙上,用了好几年。废旧报警器里抠的12887和89C51, ...

很有时代感的芯片组合。

使用特权

评论回复
9
gaoyang9992006|  楼主 | 2023-4-26 15:29 | 只看该作者
本帖最后由 gaoyang9992006 于 2023-4-26 15:31 编辑

补充一个实现反色显示数字的函数
unsigned char code num[][16]={
{0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,0x00,0x0F,0x10,0x20,0x20,0x10,0x0F,0x00},/*"0",0*/
{0x00,0x00,0x10,0x10,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00},/*"1",1*/
{0x00,0x70,0x08,0x08,0x08,0x08,0xF0,0x00,0x00,0x30,0x28,0x24,0x22,0x21,0x30,0x00},/*"2",2*/
{0x00,0x30,0x08,0x08,0x08,0x88,0x70,0x00,0x00,0x18,0x20,0x21,0x21,0x22,0x1C,0x00},/*"3",3*/
{0x00,0x00,0x80,0x40,0x30,0xF8,0x00,0x00,0x00,0x06,0x05,0x24,0x24,0x3F,0x24,0x24},/*"4",4*/
{0x00,0xF8,0x88,0x88,0x88,0x08,0x08,0x00,0x00,0x19,0x20,0x20,0x20,0x11,0x0E,0x00},/*"5",5*/
{0x00,0xE0,0x10,0x88,0x88,0x90,0x00,0x00,0x00,0x0F,0x11,0x20,0x20,0x20,0x1F,0x00},/*"6",6*/
{0x00,0x18,0x08,0x08,0x88,0x68,0x18,0x00,0x00,0x00,0x00,0x3E,0x01,0x00,0x00,0x00},/*"7",7*/
{0x00,0x70,0x88,0x08,0x08,0x88,0x70,0x00,0x00,0x1C,0x22,0x21,0x21,0x22,0x1C,0x00},/*"8",8*/
{0x00,0xF0,0x08,0x08,0x08,0x10,0xE0,0x00,0x00,0x01,0x12,0x22,0x22,0x11,0x0F,0x00},/*"9",9*/
};


void numberx(unsigned char numb, unsigned char x, unsigned char y, unsigned char flag) {
    unsigned char i;
    for (i = 0; i < 16; i++) {
        if (i == 0 || i == 15) {
            OLED_Set_Pos(x + i, y + 0);
            OLED_WR_Byte(flag ? ~0xFF : 0xFF, OLED_DATA);
            OLED_Set_Pos(x + i, y + 1);
            OLED_WR_Byte(flag ? ~0xFF : 0xFF, OLED_DATA);
        } else if ((i > 0 && i < 4) || (i > 12 && i < 16)) {
            OLED_Set_Pos(x + i, y + 0);
            OLED_WR_Byte(flag ? ~0x01 : 0x01, OLED_DATA);
            OLED_Set_Pos(x + i, y + 1);
            OLED_WR_Byte(flag ? ~0x80 : 0x80, OLED_DATA);
        } else {
            OLED_Set_Pos(x + i, y + 0);
            OLED_WR_Byte(flag ? ~(0x01 | num[numb][i - 4]) : (0x01 | num[numb][i - 4]), OLED_DATA);
            OLED_Set_Pos(x + i, y + 1);
            OLED_WR_Byte(flag ? ~(0x80 | num[numb][i + 4]) : (0x80 | num[numb][i + 4]), OLED_DATA);
        }
    }
}


效果测试

使用特权

评论回复
评论
gaoyang9992006 2023-4-26 15:32 回复TA
该函数实现了给数字加框操作,同时也可以实现反色显示。 
10
ningling_21| | 2023-4-26 18:38 | 只看该作者
不错哦

使用特权

评论回复
11
xinxianshi| | 2023-4-27 16:08 | 只看该作者
讲的明白,好懂。学会了。

使用特权

评论回复
12
xiaofei558008| | 2023-4-28 09:40 | 只看该作者
羔羊大哥,真幸福~~

使用特权

评论回复
13
chenqianqian| | 2023-5-2 08:33 | 只看该作者
好大一个时钟芯片

使用特权

评论回复
14
gaoyang9992006|  楼主 | 2023-5-3 11:21 | 只看该作者
chenqianqian 发表于 2023-5-2 08:33
好大一个时钟芯片

是的,太大了,我都懒得用它,因为是并口数据接口,所以很大。

使用特权

评论回复
15
WK520077778| | 2023-5-14 19:13 | 只看该作者
学习了

使用特权

评论回复
16
saservice| | 2023-6-10 17:41 | 只看该作者
用51单片机最小系统点亮iic的oled显示屏

使用特权

评论回复
17
maqianqu| | 2023-6-13 22:31 | 只看该作者
怎么用51单片机控制OLED 显示屏

使用特权

评论回复
18
gaoyang9992006|  楼主 | 2023-6-14 09:03 | 只看该作者
maqianqu 发表于 2023-6-13 22:31
怎么用51单片机控制OLED 显示屏

IO模拟I2C时序。

使用特权

评论回复
发新帖 本帖赏金 30.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

认证:西安公路研究院南京院
简介:主要工作从事监控网络与通信网络设计,以及从事基于嵌入式的通信与控制设备研发。擅长单片机嵌入式系统物联网设备开发,音频功放电路开发。

1961

主题

15933

帖子

208

粉丝