12下一页
返回列表 发新帖我要提问本帖赏金: 30.00元(功能说明)

[PIC®/AVR®/dsPIC®产品] 一切都要从我捡到一个51单片机说起,深度再学习驱动OLED

[复制链接]
19376|23
 楼主| gaoyang9992006 发表于 2023-4-23 16:10 | 显示全部楼层 |阅读模式
本帖最后由 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的基础函数有:起始信号、结束信号、等待信号响应
  1. //起始信号
  2. void I2C_Start(void)
  3. {
  4. OLED_SDA_Set();
  5. OLED_SCL_Set();
  6. IIC_delay();
  7. OLED_SDA_Clr();
  8. IIC_delay();
  9. OLED_SCL_Clr();

  10. }

  11. //结束信号
  12. void I2C_Stop(void)
  13. {
  14. OLED_SDA_Clr();
  15. OLED_SCL_Set();
  16. IIC_delay();
  17. OLED_SDA_Set();
  18. }

  19. //等待信号响应
  20. void I2C_WaitAck(void) //测数据信号的电平
  21. {
  22. OLED_SDA_Set();
  23. IIC_delay();
  24. OLED_SCL_Set();
  25. IIC_delay();
  26. OLED_SCL_Clr();
  27. IIC_delay();
  28. }


2、I2C的字节写入函数:写入一个字节
  1. //写入一个字节
  2. void Send_Byte(u8 dat)
  3. {
  4. u8 i;
  5. for(i=0;i<8;i++)
  6. {
  7. OLED_SCL_Clr();//将时钟信号设置为低电平
  8. if(dat&0x80)//将dat的8位从最高位依次写入
  9. {
  10. OLED_SDA_Set();
  11. }
  12. else
  13. {
  14. OLED_SDA_Clr();
  15. }
  16. IIC_delay();
  17. OLED_SCL_Set();
  18. IIC_delay();
  19. OLED_SCL_Clr();
  20. dat<<=1;
  21. }
  22. }


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

  4. void OLED_Set_Pos(u8 x, u8 y)
  5. {
  6. OLED_WR_Byte(0xb0+y,OLED_CMD);
  7. OLED_WR_Byte(((x&0xf0)>>4)|0x10,OLED_CMD);
  8. OLED_WR_Byte((x&0x0f),OLED_CMD);
  9. }
  10. //开启OLED显示
  11. void OLED_Display_On(void)
  12. {
  13. OLED_WR_Byte(0X8D,OLED_CMD); //SET DCDC命令
  14. OLED_WR_Byte(0X14,OLED_CMD); //DCDC ON
  15. OLED_WR_Byte(0XAF,OLED_CMD); //DISPLAY ON
  16. }
  17. //关闭OLED显示
  18. void OLED_Display_Off(void)
  19. {
  20. OLED_WR_Byte(0X8D,OLED_CMD); //SET DCDC命令
  21. OLED_WR_Byte(0X10,OLED_CMD); //DCDC OFF
  22. OLED_WR_Byte(0XAE,OLED_CMD); //DISPLAY OFF
  23. }
  24. //清屏函数,清完屏,整个屏幕是黑色的!和没点亮一样!!!
  25. void OLED_Clear(void)
  26. {
  27. u8 i,n;
  28. for(i=0;i<4;i++)
  29. {
  30. OLED_WR_Byte (0xb0+i,OLED_CMD); //设置页地址(0~7)
  31. OLED_WR_Byte (0x00,OLED_CMD); //设置显示位置—列低地址
  32. OLED_WR_Byte (0x10,OLED_CMD); //设置显示位置—列高地址
  33. for(n=0;n<128;n++)OLED_WR_Byte(0,OLED_DATA);
  34. } //更新显示
  35. }



  36. //初始化
  37. void OLED_Init(void)
  38. {

  39. OLED_WR_Byte(0xAE,OLED_CMD); /*display off*/
  40. OLED_WR_Byte(0x00,OLED_CMD); /*set lower column address*/
  41. OLED_WR_Byte(0x10,OLED_CMD); /*set higher column address*/
  42. OLED_WR_Byte(0x00,OLED_CMD); /*set display start line*/
  43. OLED_WR_Byte(0xB0,OLED_CMD); /*set page address*/
  44. OLED_WR_Byte(0x81,OLED_CMD); /*contract control*/
  45. OLED_WR_Byte(0xff,OLED_CMD); /*128*/
  46. OLED_WR_Byte(0xA1,OLED_CMD); /*set segment remap*/
  47. OLED_WR_Byte(0xA6,OLED_CMD); /*normal / reverse*/
  48. OLED_WR_Byte(0xA8,OLED_CMD); /*multiplex ratio*/
  49. OLED_WR_Byte(0x1F,OLED_CMD); /*duty = 1/32*/
  50. OLED_WR_Byte(0xC8,OLED_CMD); /*Com scan direction*/
  51. OLED_WR_Byte(0xD3,OLED_CMD); /*set display offset*/
  52. OLED_WR_Byte(0x00,OLED_CMD);
  53. OLED_WR_Byte(0xD5,OLED_CMD); /*set osc division*/
  54. OLED_WR_Byte(0x80,OLED_CMD);
  55. OLED_WR_Byte(0xD9,OLED_CMD); /*set pre-charge period*/
  56. OLED_WR_Byte(0x1f,OLED_CMD);
  57. OLED_WR_Byte(0xDA,OLED_CMD); /*set COM pins*/
  58. OLED_WR_Byte(0x00,OLED_CMD);
  59. OLED_WR_Byte(0xdb,OLED_CMD); /*set vcomh*/
  60. OLED_WR_Byte(0x40,OLED_CMD);
  61. OLED_WR_Byte(0x8d,OLED_CMD); /*set charge pump enable*/
  62. OLED_WR_Byte(0x14,OLED_CMD);
  63. OLED_Clear();
  64. OLED_WR_Byte(0xAF,OLED_CMD); /*display ON*/
  65. }
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是对应的页的编号
  1. /*
  2. 坐标设置,对于128*32分辨率的OLED:x从127;y从0到3
  3. */

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

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

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

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

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

  7. }
  8. OLED_Clear();
  9. //正弦波
  10. for(i=0;i<128;i++)
  11. {
  12. y=16.0-sin(i*3.1415926/32.0)*16.0;
  13. j=(unsigned int)(y);
  14. setPixel(i,j);

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




本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
21小跑堂 发表于 2023-4-23 16:35 | 显示全部楼层
执行力max
 楼主| gaoyang9992006 发表于 2023-4-23 19:43 | 显示全部楼层
如果需要同时使用多个页显示一个符号,可以通过判断是否需要切换下一个页。

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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×

打赏榜单

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

天灵灵地灵灵 发表于 2023-4-23 22:21 | 显示全部楼层
讲的很不错,我就是那种会用不懂的。
xch 发表于 2023-4-24 16:34 | 显示全部楼层
厉害!
woai32lala 发表于 2023-4-24 17:11 | 显示全部楼层
chineseboyzxy 发表于 2023-4-25 11:21 | 显示全部楼层
07年用12C887和AT89C51做了一个数码管时钟,钉个钉子挂墙上,用了好几年。废旧报警器里抠的12887和89C51,路边捡来的苹果机数码管显示板割掉了一块儿带2003驱动和BCD-7段译码器和数码管的板子。
 楼主| gaoyang9992006 发表于 2023-4-26 08:57 | 显示全部楼层
chineseboyzxy 发表于 2023-4-25 11:21
07年用12C887和AT89C51做了一个数码管时钟,钉个钉子挂墙上,用了好几年。废旧报警器里抠的12887和89C51, ...

很有时代感的芯片组合。
 楼主| gaoyang9992006 发表于 2023-4-26 15:29 | 显示全部楼层
本帖最后由 gaoyang9992006 于 2023-4-26 15:31 编辑

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


  1. void numberx(unsigned char numb, unsigned char x, unsigned char y, unsigned char flag) {
  2.     unsigned char i;
  3.     for (i = 0; i < 16; i++) {
  4.         if (i == 0 || i == 15) {
  5.             OLED_Set_Pos(x + i, y + 0);
  6.             OLED_WR_Byte(flag ? ~0xFF : 0xFF, OLED_DATA);
  7.             OLED_Set_Pos(x + i, y + 1);
  8.             OLED_WR_Byte(flag ? ~0xFF : 0xFF, OLED_DATA);
  9.         } else if ((i > 0 && i < 4) || (i > 12 && i < 16)) {
  10.             OLED_Set_Pos(x + i, y + 0);
  11.             OLED_WR_Byte(flag ? ~0x01 : 0x01, OLED_DATA);
  12.             OLED_Set_Pos(x + i, y + 1);
  13.             OLED_WR_Byte(flag ? ~0x80 : 0x80, OLED_DATA);
  14.         } else {
  15.             OLED_Set_Pos(x + i, y + 0);
  16.             OLED_WR_Byte(flag ? ~(0x01 | num[numb][i - 4]) : (0x01 | num[numb][i - 4]), OLED_DATA);
  17.             OLED_Set_Pos(x + i, y + 1);
  18.             OLED_WR_Byte(flag ? ~(0x80 | num[numb][i + 4]) : (0x80 | num[numb][i + 4]), OLED_DATA);
  19.         }
  20.     }
  21. }


效果测试

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×

评论

该函数实现了给数字加框操作,同时也可以实现反色显示。  发表于 2023-4-26 15:32
ningling_21 发表于 2023-4-26 18:38 | 显示全部楼层
不错哦
xinxianshi 发表于 2023-4-27 16:08 | 显示全部楼层
讲的明白,好懂。学会了。
xiaofei558008 发表于 2023-4-28 09:40 | 显示全部楼层
羔羊大哥,真幸福~~
chenqianqian 发表于 2023-5-2 08:33 来自手机 | 显示全部楼层
好大一个时钟芯片
 楼主| gaoyang9992006 发表于 2023-5-3 11:21 | 显示全部楼层
chenqianqian 发表于 2023-5-2 08:33
好大一个时钟芯片

是的,太大了,我都懒得用它,因为是并口数据接口,所以很大。
WK520077778 发表于 2023-5-14 19:13 | 显示全部楼层
学习了
saservice 发表于 2023-6-10 17:41 | 显示全部楼层
用51单片机最小系统点亮iic的oled显示屏
maqianqu 发表于 2023-6-13 22:31 | 显示全部楼层
怎么用51单片机控制OLED 显示屏
 楼主| gaoyang9992006 发表于 2023-6-14 09:03 | 显示全部楼层
maqianqu 发表于 2023-6-13 22:31
怎么用51单片机控制OLED 显示屏

IO模拟I2C时序。
598330983 发表于 2025-2-22 11:10 | 显示全部楼层
好帖,深入浅出,讲明白了时许的模拟以及这个屏幕显示的原理,如何将图像显示到屏幕。
jiekou001 发表于 2025-2-26 16:13 | 显示全部楼层
现在没人做项目用这种单片机和这么大的时钟元件了。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

个人签名:如果你觉得我的分享或者答复还可以,请给我点赞,谢谢。

2052

主题

16403

帖子

222

粉丝
快速回复 在线客服 返回列表 返回顶部