【笔记】制作简易指针式表盘界面和太空人界面 基于stm32驱动7脚SPI协议OLED液晶显示屏 RTC内部时钟(一)理解OLED画点

下篇:http://t.csdnimg.cn/ZIdM6

取模工具及图片例子:

链接:https://pan.baidu.com/s/16e7AYqi7cloBjGDbh_LWMg?pwd=n5cj 
提取码:n5cj

前言

        本文仅为个人笔记,对于OLED的原理没有太多的讲解,只有在画点这里认为需要理解,其他的感兴趣可以查阅对应的数据手册,程序部分包括画点、文字显示、图片显示、GIP动态显示等,包括取模流程。

以下最终的界面效果:

af0d91ccd34441e8bb472872b36f209e.gif

目录

前言

一、oled概念

二、SSD1306驱动芯片

2-1 简介

2-2 SSD1306命令

三、oled的引脚

 四、画点原理(理解显示原理)

4-1 OLED模块显存:

4-2 设置内存地址模式(20h)

4-3 页地址模式(A[1:0]=02h)详解


一、oled概念

        OLED,即有机发光二极管( Organic Light Emitting Diode)。 OLED 由于同时具备自发光,不需背光源、对比度高、厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、构造及制程较简单等优异之特性,被认为是下一代的平面显示器新兴应用技术。 LCD 都需要背光,而 OLED 不需要,因为它是自发光的。这样同样的显示 OLED 效果要来得好一些。我们介绍的是0.96寸OLED显示屏,实物图如下:

20fceab67fa14dca955c39aa9a09ecd6.png              33fad9627a8340ab8c8d99985dc70534.png


二、SSD1306驱动芯片

2-1 简介

        SSD1306 是一个单片 CMOS OLED/PLED 驱动芯片可以驱动有机/聚合发光二极管点阵图形显示系统。由 128 segments 和64 Commons组成。该芯片专为共阴极 OLED 面板设计。SSD1309中嵌入了对比度控制器、显示 RAM 和晶振,并因此减少了外部器件和功耗。有 256级亮度控制。数据/命令的发送有三种接口可选择:6800/8000 串口,I2C 接口或 SPI 接口。适用于多数简介的应用,注入移动电话的屏显,MP3 播放器和计算器等。

        OLED控制器为SSD1306,也就是说:裸屏由SSD1306驱动,这也是一种较为广泛使用的led驱动芯片。所以,我们通过向SSD1306写入命令以达到想要的功能。


2-2 SSD1306命令

        SSD1306的命令有很多,这就代表了它有很多的功能,但是在这里就不过多赘述,我们就了解几个常用的命令,掌握这些常用的命令便可使用基本的功能,最开始当然就是显示一个点,如果掌握显示一个点的基本原理,oled模块就几乎可以掌握了,再去学字符的显示、图形的显示和其他命令很快的掌握。下面介绍几个常见的命令:

        1、命令0XB0~0xB7:用于设置页地址,其低三位的值对应着GRAM的页地址。
        2、命令0X00~0X0F:用于设置显示时的起始列地址低四位。
        3、命令0X10~0X1F:用于设置显示时的起始列地址高四位。

        这三个命令是画点函数所要使用的,对于理解画点流程至关重要,在后面的画点实验会详细介绍(什么是列高列低也会讲到)。

        4、命令0X81:设置对比度。包含两个字节,第一个0X81为命令,随后发送的一个字节为要设置的对比度的值。这个值设置得越大屏幕就越亮。
        5、命令0XAE/0XAF:0XAE为关闭显示命令;0XAF为开启显示命令。

        6、命令0XC0/0XC8:设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数   0xc0上下反置 0xc8正常


三、oled的引脚

oled的介绍和驱动的介绍就先到这里,如果想要了解更多的功能和命令,可以去查查SSD1306的手册。上次我们对SPI协议和两个单片机利用SPI进行通信做了简单的介绍和实操,接下来我们通过spi驱动oled模块,在这里简单介绍一下,oled对于单片机来说就是一个从机,单片机可以通过向SSD1306写数据命令oled模块做出相应的功能,而写数据就是通过SPI通信。

        我们这里介绍的是7针oled;(如果是其他协议,只要能够与oled通信就好)

        我们讲了SPI是全双工的,主机向从机发送数据,同时从机也可以向主机发送数据,但是oled模块不能向主机写数据,只能读出主机发过来的命令执行相应的功能,那么就意味着SPI的4条逻辑线中MISO(Master input slave output 主机输入,从机输出)就不需要了。

模块接口定义:

GND 

VCC

D0

 D1

RES

DC

 CS

电源地

电源正(3~5.5V)

OLED 的 D0 脚,在 SPI 和 IIC 通信中为时钟管脚

OLED 的 D1 脚,在 SPI 和 IIC 通信中为数据管脚

OLED 的 RES#脚,用来复位(低电平复位)

OLED 的 D/C#E 脚,数据和命令控制管脚(D/C为1代表的是发送数据,D/C为0的代表是发送命令)

OLED 的 CS#脚,也就是片选管脚

在这里,重点介绍一下DC,在向SSD1306写命令或是写数据时,先要向SSD1306写入控制字节信息,简单理解你可以理解当DC置0时,后面发送的是写命令,命令就是上述提到的一些常用命令,当DC置0时,后面发送的是写数据,数据一般是控制oled每个像素的的亮灭,后面会介绍。

为了对应SPI的逻辑线,做了简单的对照:

D0->SCLK: Serial Clock 串行时钟信号,由主机产生发送给从机;

D1->MOSI:Master output slave input 主机输出,从机输入(数据来自主机);

CS->SS/CS:Slave Select/Chip Select 从设备使能信号,由主设备控制。 片选信号,由主机发送,以控制与哪个从机通信,通常是低电平有效信号,oled的也是。


 四、画点原理(理解显示原理)

——可以先看第二部分-写函数部分,先把程序搭建起来,不然测试不了只能想象,根据详解的步骤验证打点的效果

        在写画点函数之前,我们得先了解它的发光原理,那么,如何实现一个点的点亮呢?

        首先我们提到oled的分辨率为 128*64 ,怎么理解?可以看作oled由128列、64行的像素的组成,每个像素点放在一个小格子里,而要想点亮一个点,就在对应的格子里写入1,1为亮,0为灭。

        那么怎么在指定的位置写入一个点呢?

        步骤:设置内存地址模式(20h)->我们这选择页地址模式->确认是在哪一页->确认是这一页的哪一列->写数据


4-1 OLED模块显存:

        oled的数据存放模式,OLED本身是没有显存的,它的显存是依赖于SSD1306提供的。SSD1306的显存总共为128 * 64bit大小,SSD1306将这些显存分为了8页。每页包含了128个字节,总共8页,这样刚好是128*64的点阵大小。

f1015154a54f4d579a36573a186f9ff6.png


4-2 设置内存地址模式(20h

        SSD1306 中有三种不同的内存地址模式:页地址模式,水平地址模式,垂直地址模式。这个命令将内存地址模式设置成这三种中的一种。A[1:0] :00,列地址模式;01,行地址模式;10,页地址模式;默认10;这个在初始化的时候要设置的。下面的三种模式可以简单过一下,我会选择页地址模式进行详细的讲解:

4-2-1 页地址模式(A[1:0]=02h)

        在页地址模式下,在显示 RAM 读写之后,列地址指针自动加一。如果列地址指针达到了列的结束地址,列地址指针重置为列开始地址并且也地址指针不会改变。用户需要设置新的页和列地址来访问下一页 RAM 内从。页地址模式下 PAGE 和列地址指针的移动模式参考下图:

3ab31881b73248c4af1857410d32eb47.png

  在正常显示数据 RAM 读或写和页地址模式,要求使用下面的步骤来定义开始 RAM 访问的位置:

  • 通过命令 B0h 到 B7h 来设置目标显示位置的页开始地址
  • 通过命令 00h~0Fh 来设置低位列地址
  • 通过命令 10h~1Fh 来设置高位列地址(这3点后面后说)

4-2-2 水平寻址模式(A[1:0]= 00b

        在水平寻址模式下,当显示 RAM 被读写之后,列地址指针自动加一。如果列地址指针达到列的结束地址,列地址指针重置为列的开始地址,并且页地址指针自动加 1。水平寻址模式下页和列地址的移动顺序如下图所示。当列地址和页地址都达到了结束地址,指针重设为列地址和页地址的开始地址。

43cdfe24e6524cb8bf41b3726877d5cb.png

4-2-3 垂直寻址模式(A[1:0]=01b

        在垂直寻址模式下,当显示 RAM 被读写之后,页地址指针自动加一。如果页地址达到了页的结束地址,页地址自动重置为页的开始地址,列地址自动加一。页地址和列地址的移动顺序如下图所示。当列地址和页地址都达到结束地址后,指针自动重置为开始地址。 

9a6e42f65531444885d86dfdaca8bbf2.png

在正常显示 RAM 读或写,水平/垂直寻址模式下,要求用下面的步骤来定义 RAM 访问指针位置:

        用 21h 命令设置目标显示位置的列的开始和结束地址;

        用命令 22h 设置目标显示位置的页的开始和结束地址。

4-3 页地址模式(A[1:0]=02h)详解

在正常显示数据 RAM 读或写和页地址模式,要求使用下面的步骤来定义开始 RAM 访问的位置:

1.通过命令 B0h 到 B7h 来设置目标显示位置的页开始地址

        将128*64分为8页,也就是每8行为1页,比如写入命令0xB0,就是在第一页开始写入数据,也就是1到8行(定义是从0开始的,也就是0代表第一行或者第一列),怎么写命令呢?就用到我们写的函数(OLED_CMD是0的宏定义,这个函数在下面有定义):

OLED_WR_Byte(unsigned char dat1,unsigned char cmd)

写入0xB0就是:
OLED_WR_Byte(0xB0,OLED_CMD);

2.通过命令 00h~0Fh 来设置低位列地址

3.通过命令 10h~1Fh 来设置高位列地址

什么是低位列地址、高位列地址,比方说:十六进制0x1F,二进制为1000 1111,那么它的低地址就是1111;高地址就是1000。命令 00h~0F,命令 10h~1Fh

这两个命令就是指定是哪一列的,是必须一起用的,放在页地址后面,比如:

我要在第3页的第4列(对应关系是PAGE2的SEG3,4列的十六进制0x03:低位列地址:0x03,高位列地址:0x10)开始写入数据,那么我们给的命令就是:

OLED_WR_Byte(0xB2,OLED_CMD);
OLED_WR_Byte(0x03,OLED_CMD);
OLED_WR_Byte (0x10,OLED_CMD);

这样就意味着开始列是PAGE2 的 SEG3.RAM 访问指针的位置如图所示。输出数据字节将写到 RAM 列 3 的位置。

d579b2ab569540cd939381b2180273af.png

        知道如何写命令让其在指定的页和列开始写入数据,但是每一页的每一列有8个像素点,如何写入数据呢?还是从PAGE2的第三列写入开始,我们可以试试全部点亮(0灭1亮),那么就写入0xFF,格式如下:

/*
注意以下设置(OLED_Init();里面) :
OLED_WR_Byte(0xC0,OLED_CMD); //设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数   0xc0上下反置 0xc8正常
也就是我把屏幕上下关于镜像反过来了
*/
OLED_WR_Byte(0xB2,OLED_CMD);/*第3页,上面设置COM扫描方向是上下反置,所以实际的是倒数第三页,有排针的那边朝上*/
OLED_WR_Byte(0x03,OLED_CMD);/*0x03的底地址*/
OLED_WR_Byte(0x10,OLED_CMD);/*0x03的高地址*/
OLED_WR_Byte(0xFF,OLED_DATA);//注意这次是写数据

         可以看到,OLED不能一次控制一个点阵,只能一次性控制8个点阵;而且是垂直方向扫描控制;所以我们写入数据时要写8bit数据。那么,每次要写8个点,这样,我们在画点的时候,就必须把要设置的点所在的字节的每个位都搞清楚当前的状态(0/1? ),否则写入的数据就会覆盖掉之前的状态,结果就是有些不需要显示的点,显示出来了,或者该显示的没有显示了。

  比如说某一页上的数据是 (0100 0000) 。如果想让其他像素点显示,会对这一页重新赋值,比如(0010 0000)。赋值过后,先前的数据就会被覆盖。也就是说一页只能打一个点,因此为了避免这种情况,要记录之前显示过的值,必须要在单片机内部申请一块内存区域充当OLED的显示缓存区,每次打点的时候读取先前的数据进行运算后再给OLED传送数据。

        我们采用的办法是在单片机的内部建立一个OLED的GRAM(共128个字节),在每次修改的时候,只是修改GRAM(实际上就是SRAM),在修改完了之后,一次性把单片机上的GRAM写入到OLED的GRAM。这里的GRAM可以理解为建立一个二维数组OLED_GRAM[128][8];

        当然这个方法也有坏处,对于51系列的SRAM很小,其内存大小为均1280字节。OLED显示屏共128列8页,若在单片机中申请内存则是128×8×1=1024个字节,就比较麻烦了。但是对于32来说,还是能够承受的,这样也会理解起来和操作起来方便。

         讲到这里,大家应该理解了画点的思路和原理了吧,如果还不会,可以不断地修改页地址和列地址以及写入的数据,观察oled的现象,慢慢体会画一个像素点的原理。

主函数验证:

代码:

/*==============================================================================
 *函数名称:My_mian
 *函数功能:主函数调用,while死循环
 *修改时间:2024 03 25 xp
 这个函数放在主函数的while循环里面
==============================================================================*/
void My_mian(void)
{
	static uint8_t flag=0;
	if(flag) /*while循环*/
	{
		/*
		注意以下设置(OLED_Init();里面):
		OLED_WR_Byte(0xC0,OLED_CMD); //设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM[N-1]->COM0;N:驱动路数   0xc0上下反置 0xc8正常
		也就是我把屏幕上下关于镜像反过来了
		*/
		OLED_WR_Byte(0xB2,OLED_CMD);/*第3页,上面设置COM扫描方向是上下反置,所以实际的是倒数第三页,有排针的那边朝上*/
		OLED_WR_Byte(0x03,OLED_CMD);/*0x03的底地址*/
		OLED_WR_Byte(0x10,OLED_CMD);/*0x03的高地址*/
		OLED_WR_Byte(0xFF,OLED_DATA);//注意这次是写数据
	}
	else    /*初始化,程序只进来一次*/
	{
		OLED_Init();
		flag=1;
	}
}

b4e889eb3db64aed8fd2a4a36d737321.png

 效果(根据命令显示,上述的是在倒数第三页第4列全部点亮(正向显示白点为亮)):

0fb885b229244dd2a6182a4863fe6f6d.png

如果命令如下:

OLED_WR_Byte(0xB7,OLED_CMD);/*第1页,上面设置COM扫描方向是上下反置,所以实际的是倒数第7页,有排针的那边朝上*/
OLED_WR_Byte(0x00,OLED_CMD);/*0x00的底地址*/
OLED_WR_Byte(0x10,OLED_CMD);/*0x00的高地址*/
OLED_WR_Byte(0xFF,OLED_DATA);//注意这次是写数据

效果:

6395911e628c499082ad07050e3cf290.png

  • 11
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是基于STM32F103系列驱动RTC实时时钟并使用OLED显示的示例代码: ```c #include "stm32f10x.h" #include "stm32f10x_rcc.h" #include "stm32f10x_rtc.h" #include "stm32f10x_i2c.h" #include "oled.h" void RTC_Configuration(void); void I2C_Configuration(void); void OLED_Configuration(void); void OLED_UpdateTime(RTC_TimeTypeDef RTC_TimeStruct, RTC_DateTypeDef RTC_DateStruct); int main(void) { RTC_Configuration(); //初始化RTC I2C_Configuration(); //初始化I2C OLED_Configuration(); //初始化OLED while(1) { RTC_TimeTypeDef RTC_TimeStruct; RTC_DateTypeDef RTC_DateStruct; RTC_GetTime(RTC_Format_BIN, &RTC_TimeStruct); //获取当前时间 RTC_GetDate(RTC_Format_BIN, &RTC_DateStruct); //获取当前日期 OLED_UpdateTime(RTC_TimeStruct, RTC_DateStruct); //更新OLED显示时间 } } void RTC_Configuration(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能PWR和BKP外设时钟 PWR_BackupAccessCmd(ENABLE); //使能RTC和后备寄存器访问 RCC_LSEConfig(RCC_LSE_ON); //打开LSE外部晶振 while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); //等待LSE晶振稳定 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //将RTC时钟源改为LSE外部晶振 RCC_RTCCLKCmd(ENABLE); //使能RTC时钟 RTC_WaitForSynchro(); //等待RTC寄存器同步 RTC_InitTypeDef RTC_InitStructure; RTC_InitStructure.RTC_HourFormat = RTC_HourFormat_24; //时间格式为24小时制 RTC_InitStructure.RTC_AsynchPrediv = 0x7F; //RTC异步分频系数为0x7F+1=128 RTC_InitStructure.RTC_SynchPrediv = 0xFF; //RTC同步分频系数为0xFF+1=256 RTC_Init(&RTC_InitStructure); //初始化RTC RTC_TimeTypeDef RTC_TimeStruct; RTC_TimeStruct.RTC_Hours = 0x00; //设置RTC小时数 RTC_TimeStruct.RTC_Minutes = 0x00; //设置RTC分钟数 RTC_TimeStruct.RTC_Seconds = 0x00; //设置RTC秒数 RTC_SetTime(RTC_Format_BIN, &RTC_TimeStruct); //设置RTC时间 RTC_DateTypeDef RTC_DateStruct; RTC_DateStruct.RTC_Year = 0x20; //设置RTC年份 RTC_DateStruct.RTC_Month = RTC_Month_January; //设置RTC月份 RTC_DateStruct.RTC_Date = 0x01; //设置RTC日数 RTC_DateStruct.RTC_WeekDay = RTC_Weekday_Thursday; //设置RTC星期几 RTC_SetDate(RTC_Format_BIN, &RTC_DateStruct); //设置RTC日期 } void I2C_Configuration(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); //使能I2C1外设时钟 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; //PB6和PB7引 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; //开漏输出 GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化GPIOB I2C_InitTypeDef I2C_InitStructure; I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; //I2C模式 I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; //占空比为2 I2C_InitStructure.I2C_OwnAddress1 = 0x00; //本地地址为0 I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //使能应答 I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //7位地址模式 I2C_InitStructure.I2C_ClockSpeed = 100000; //时钟速度为100kHz I2C_Cmd(I2C1, ENABLE); //使能I2C1 I2C_Init(I2C1, &I2C_InitStructure); //初始化I2C1 } void OLED_Configuration(void) { OLED_Init(); //初始化OLED OLED_Clear(); //清屏 } void OLED_UpdateTime(RTC_TimeTypeDef RTC_TimeStruct, RTC_DateTypeDef RTC_DateStruct) { char time_str[20]; sprintf(time_str, "%02d:%02d:%02d", RTC_TimeStruct.RTC_Hours, RTC_TimeStruct.RTC_Minutes, RTC_TimeStruct.RTC_Seconds); char date_str[20]; sprintf(date_str, "%04d-%02d-%02d", RTC_DateStruct.RTC_Year, RTC_DateStruct.RTC_Month, RTC_DateStruct.RTC_Date); OLED_ShowString(0, 0, "Time:"); OLED_ShowString(48, 0, time_str); OLED_ShowString(0, 2, "Date:"); OLED_ShowString(48, 2, date_str); } ``` 这段代码中,我们在`main()`函数中调用`RTC_Configuration()`函数、`I2C_Configuration()`函数和`OLED_Configuration()`函数,分别初始化RTC、I2C和OLED。然后我们在一个无限循环中不断获取当前的时间和日期,并调用`OLED_UpdateTime()`函数来更新OLED上的时间和日期显示。`OLED_UpdateTime()`函数中,我们首先使用`sprintf()`函数将当前时间和日期转换为字符串。然后我们调用`OLED_ShowString()`函数来在OLED上显示时间和日期。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值