一切都要从我捡到一个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(); }
|