谷动谷力

 找回密码
 立即注册
查看: 1334|回复: 0
打印 上一主题 下一主题
收起左侧

一切都要从我捡到一个51单片机说起

[复制链接]
跳转到指定楼层
楼主
发表于 2023-5-22 23:26:47 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
一切都要从我捡到一个51单片机说起


深度再学习驱动OLED
最近捡到一个十几年前学单片机时候的入门单片机: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了,通过以上的组合可以实现给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,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();
}


+10
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|深圳市光明谷科技有限公司|光明谷商城|Sunshine Silicon Corpporation ( 粤ICP备14060730号|Sitemap

GMT+8, 2024-11-17 16:31 , Processed in 0.383593 second(s), 42 queries .

Powered by Discuz! X3.2 Licensed

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表