基于STM32F103ZET6实现OLED显示(4线SPI)

硬件设备:
(1):0.96寸的oled显示屏
(2):stm32开发板,不带接口也可以可,可以用杜邦线引出来插上即可

目的:

我们将利用精英 STM32 开发板上的 OLED 模块接口来点亮OLED,并实现 ASCII 字符的显示。

原理

LCD 都需要背光,而 OLED 不需要,因为它是自发光的。
OLED有多种点亮方式,包括:
(1):6800并行接口方式
(2):8080并行接口方式
(3):三线spi接口方式
(4):四线spi接口方式
(5):IIC 接口方式(只需要 2 根线就可以控制 OLED 了!)
这五种模式怎们设置呢?就是在模块的背面有一些电阻,分别BS0与BS1在控制模式,想要改变模式,就在相应位置焊上电阻
在这里插入图片描述
当然了,有的小伙伴并不是这样的oled,我的模块就不是正点原子的0.96oled,我是自己定义的引脚来控制的!可以根据厂家提供的数据手册资料来修改模式,原理差不多!
在这里插入图片描述

重点来了(四线spi)

想要写好OLED程序,了解模块的原理,流程,数据写入的方向与方式等等都是很重要的,而不是拿着标准的程序死记硬背!下面就对我使用的OLED进行我的理解说明:
引脚说明
CS:OLED 片选信号。
RST(RES):硬复位 OLED。
DC:命令/数据标志(0,读写命令;1,读写数据)。
SCLK:串行时钟线,D0 信号线作为串行时钟线
SDIN:串行数据线,D1 信号线作为串行数据线
VCC与GND也是必须要有的,这么算下来就是7针的OLED模块
IO口的配置比较简单:配置的引脚也能看到

void oled_init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOG, ENABLE);
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3|GPIO_Pin_6;
	GPIO_Init(GPIOD,&GPIO_InitStructure);
	GPIO_SetBits(GPIOD,GPIO_Pin_3|GPIO_Pin_6);
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_1|GPIO_Pin_0;
	GPIO_Init(GPIOC,&GPIO_InitStructure);
	GPIO_SetBits(GPIOC,GPIO_Pin_1|GPIO_Pin_0);
	
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_15;
	GPIO_Init(GPIOG,&GPIO_InitStructure);
	GPIO_SetBits(GPIOG,GPIO_Pin_15);
	//oled¸´Î»
	OLED_RST=0;
	delay_ms(100);
	OLED_RST=1; 
	//oled³õʼ»¯
	oled_writebyte(0xAE,OLED_CMD); 
	oled_writebyte(0xD5,OLED_CMD); 
	oled_writebyte(0x80,OLED_CMD);  
	oled_writebyte(0xA8,OLED_CMD); 
	oled_writebyte(0X3F,OLED_CMD); 
	oled_writebyte(0xD3,OLED_CMD);
	oled_writebyte(0X00,OLED_CMD); 
	oled_writebyte(0x40,OLED_CMD); 										    
	oled_writebyte(0x8D,OLED_CMD); 
	oled_writebyte(0x14,OLED_CMD); 
	oled_writebyte(0x20,OLED_CMD); 
	oled_writebyte(0x02,OLED_CMD); 
	oled_writebyte(0xC8,OLED_CMD);
	oled_writebyte(0xA1,OLED_CMD); 
	oled_writebyte(0xDA,OLED_CMD); 
	oled_writebyte(0x12,OLED_CMD);
	oled_writebyte(0x81,OLED_CMD);
	oled_writebyte(0xEF,OLED_CMD); 
	oled_writebyte(0xD9,OLED_CMD);
	oled_writebyte(0xf1,OLED_CMD); 
	oled_writebyte(0xDB,OLED_CMD);
	oled_writebyte(0x30,OLED_CMD); 
	oled_writebyte(0xA4,OLED_CMD); 
	oled_writebyte(0xA6,OLED_CMD); 
	oled_writebyte(0xAF,OLED_CMD); 	
	OLED_Clear();
}

如图:
在这里插入图片描述
在 4 线 SPI 模式下,每个数据长度均为 8 位,在 SCLK 的上升沿,数据从 SDIN 移入到oled模块的SSD1306,并且是高位在前的。DC 线还是用作命令/数据的标志线。
在这里插入图片描述
口述一下从32传输一个字节到模块SSD1306的时序:
写入的数据我们分为了命令与数据,将片选位拉低,在传输数据的的开始,将时钟拉低,取出传输字节的最高位,拉高时钟,这时,1bit数据成功写入SSD1306,循环八次,方可完成一个字节的写入,此时拉高片选。

void oled_writebyte(unsigned char data,unsigned char oled_RS)
{
	int i;
	OLED_CS=0;//片选拉低
	OLED_RS=oled_RS;//发送的数据还是命令
	for(i=0;i<8;i++)
	{
		OLED_SCLK=0;
			if(data&0x80)
				OLED_SDIN=1;//数据位
			else 
				OLED_SDIN=0;//数据位
			
			data<<=1;//将次高位移动到最高位
			OLED_SCLK=1;	//时钟拉高
	}
	OLED_CS=1;//片选拉高
	OLED_RS=1;
}

写入SSD1306的data分为数据和命令,写入的命令是来设置OLED的显示参数,写入的数据用来显示,所以:
(1):了解要写入的命令,初始化oled模块
在这里插入图片描述
可根据此流程校验初始化流程!
(2):清除显示的数据是如何写入的,在显示屏中如何显示,现实的方向,大小等等,都很关键
第二点开始:
一个字节的数据写入了,
**如果是命令:**我们可以不用管如何让排列在模块里,因为这不会影响我们的显示,
如果是数据:
在这里插入图片描述
这是我们写入的数据对应屏幕的关系表,PAGE0包含128个字节,也就是1288个像素点,从PAGE0~PAGE7共有八个,也就是1288*8个像素点,我们取出PAGE0,
在这里插入图片描述
上面的数据只是代表个数,不代表真实数据,数字相同的八位构成一个字节,这个字节,就是我们写入的一个字节的显示数据,那这个遵守什么规律呢?
在这里插入图片描述
由写时序可知,先写高字节,再写低字节,所以数据写入的方向如图,总的方向为:从下到上,从左至右
在这里插入图片描述
写入字节的内部结构清楚了,在此之前我们设置显示这个字节的位置(一个字节控制从上到下的八个像素点我们应该是知道的就不详细说明了),上图只是我们的PAGE0,所以我们要确定在那个PAGE上写数据,每页从左到右也有一个位置,所以写入数据,这是两个必须写入的参数,每一页,只需要配置一次,如何更好的控制写入的数据点呢?
可以本地(自己)定义一个屏幕像素点大小的二维数组,eg:char a[128][8]

unsigned char GRAM[128][8];//128*8个字节=128*8*8个像素点

当我们数据点配置(本地的二维数组)好后,一起发送(refresh)到SSD1306里,想要什么图形自己都可以用一个一个的像素点拼凑出来!

void GRAM_REFRESH(void)
{
	int i;
	int j;
	for(i=0;i<8;i++)//8页循环8次
	{
		oled_writebyte(0xb0+i,OLED_CMD);//页地址,每次增1
		oled_writebyte(0x00,OLED_CMD);//显示时的起始列地址低四位
		oled_writebyte(0x10,OLED_CMD);//显示时的起始列地址高四位
		for(j=0;j<128;j++)//循环128次,每次从下到上写一个字节
		{
			oled_writebyte(GRAM[j][i],OLED_DATA);	
		}
	}
}

基本达成oled点亮

小进阶(显示单个字符)

还是一个原理:任何显示的信息,都可以先修改本地(自己定义的二维数组),修改结束后一起(refresh)写入SSD1306,那么怎们来显示字符呢?
我们现在要用到字符集点阵,这个字符集点阵有大有小,也就是控制字体的大小,在这个固定大小的区域内显示某个字符,这个字符点阵集可以是软件合成的,也可以在您周围的小伙伴哪里copy一下都可以,并且是是const类型的常量
在这里插入图片描述
上面就是1206大小的点阵字符集,代码太多,不便上码,如果我们要用这个大小的字体显示’!'号,也就是上图的第二行,1206的意思是指高12,宽为6的像素点组成的大小的显示平面,一个字节一个字节的写入,现在将一个字节看为整体,方向是从上到下,从左到右,如果把某个字节的1bit看为整体,就是我们前面描述到的。
在这里插入图片描述
虚线部分4bit为没写入的位,丢弃掉,这个大小总共占用12个字节,正好对应点阵字符集上的12个数据,

非常重要的函数

void draw_char(unsigned char x,unsigned char y,unsigned char chr,unsigned char size)
{
	unsigned char csize;
	unsigned char num1;
	unsigned char num2;
	unsigned char data;
	unsigned char num3=y;
	unsigned char chr1;
	csize= (size/8+((size%8)?1:0))*(size/2);//确定字体占用页数
	chr1=chr-' ';	//得到偏移后的值,因为点阵字符集的第一个字符为空''
	for(num1=0;num1<csize;num1++)
	{
		if(size==12)
		{data=asc2_1206[chr1][num1];}//1206字体
		else if(size==16)
			{data=asc2_1608[chr1][num1];}//1608的字体
		else if(size==24)
				{data=asc2_2412[chr1][num1];}//2412的字体
		else 
			return ;
		for(num2=0;num2<8;num2++)
		{
					if(data&0x80)
				{
					draw_point(x,y);
				}
				else 
				{
					clean_point(x,y);
				}
				data<<=1;
				y++;
				if((y-num3)==size)
				{
					y=num3;
					x++;
					break;
				}
		}			
	}
}

这个函数将本地的二维数组已经根据字符布置好了,如果想在(20,20)的位置上用12字体显示字符0,可以这样用函数

draw_char(20,20,'0',12)

当然,想要用此函数显示字符串,除了人为的大间隔法(离旁边的单个字符很远处再写个字符),拼凑出来在显示屏上显示的字符串,当然还有显示字符串的方法:

进阶1(显示字符串)

此时我们用到了大家喜欢的指针,也会用到上面的draw_char();函数,在这里我提供给大家两种算法:
(1):

void draw_string(unsigned char x,unsigned char y, char* a,unsigned char size)
{
	unsigned char X,i;
	X=x;
	for(;*a!='\0';a++)
	{
		draw_char(x,y,*a,size);
		x+=size/2;
		if(x+size>128) //此行已经不能容纳更多字符,换行
		{
			x=X;//与上一排字符串同x起始位
			y+=size;//y显示上增加一个字体高度
    }
  }
}

draw_char参数里面传入的是字符,在这里我们应该注意,我们可以循环判断传入的字符串是否一字符串结束符’\0’结束作为标志位
(2):利用95个字符判断,处于/" ",0/<(a+i)</"~",94*/之间就为存在字符

void draw_string(unsigned char x,unsigned char y, char* a,unsigned char size)
{
	while((*a>=' ')&&(*a<='~'))//判断字符是否为这95个字符中的
	{
		draw_char(x,y,*a,size);
		x+=size/2if(x+size>127)  
		{
			x=X;
			y+=size;
    }
		a++;
  }
}

如果说想要显示文字,可以通过点阵字符制作软件生成,自行探索!
效果如图
在这里插入图片描述

进阶2(显示直线)

相信能做完前面的流程,显示直线也应该能够完成,直接上码,大家应该能看得懂:

void draw_line(unsigned char x1,unsigned char y1,unsigned char x2,unsigned char y2 )
{
	unsigned  char k1,k2,i,k;
	if(x1==x2)//画竖线
	{
		if(y1>y2)
		{
				for(y2;y2<=y1;y2++)
			{
					draw_point(x2,y2);
			}
		}
		else 
		{
			for(y1;y1<=y2;y1++)
			{
					draw_point(x1,y1);
			}
		}
	}
	else if(y1==y2)   //画横线
	{
		
			for(i=0;i<=(x2-x1);i++)
			{
				draw_point(x1+i,y1);
			}
  }
	else if(x1!=x2&&y1!=y2)//画斜线
	{
		k1=y2-y1;
		k2=x2-x1;
		k=k1*10/k2;
		for(i=0;i<(x2-x1);i++)
			{
			  draw_point(x1+i,y1+i*k/10);
			}
  }
}

参数x1,y1,x2,y2是直线两点的坐标;

一定在最后不要忘记refresh函数,否则无显示

在此,我只是提供了算法与思路,代码太多,希望对读者有用,懂原理,参考数据手册,其他的oled都不是问题,务必清楚原理后再看代码学习,否则事倍功半!
还有显示圈,图片等等,原理都是一样,希望一定要动手实践操作!
此博客只写了作为写入,没涉及到读出,还有很大的空间可以拓展,谢谢大家,也希望读者有更好的建议给我,互相学习

发布了13 篇原创文章 · 获赞 21 · 访问量 5792

猜你喜欢

转载自blog.csdn.net/weixin_42271802/article/details/104838607