基於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都不是問題,務必清楚原理後再看代碼學習,否則事倍功半!
還有顯示圈,圖片等等,原理都是一樣,希望一定要動手實踐操作!
此博客只寫了作爲寫入,沒涉及到讀出,還有很大的空間可以拓展,謝謝大家,也希望讀者有更好的建議給我,互相學習

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章