STM32-驱动OLED显示屏使用SPI(软件模拟时序)实现

发布于:2025-08-05 ⋅ 阅读:(15) ⋅ 点赞:(0)

本章概述思维导图:

SPI通信协议

SPI通信协议介绍

SPI通讯:高速的、串行通讯、全双工、同步、总线协议;(通过片选信号选中设备);

注:SPI通讯通过片选信号选中设备,串口通讯通过端口号选中设备;

SPI通讯一般至少配置4根线:SCK时钟线,MOSI主机输出从机输入、MISO主机输入从机输出、CS片选;


SPI中根据时钟极性和时钟相位的不同,可以分为4种模式;

CPOL时钟极性:可以确定时钟线空闲时的电平状态;

CPHA时钟相位:可以确定第一个时钟沿是发送数据还是采样数据的


寻址方式:当主设备要和多个从机进行通信时,主设备需要先向对应从设备的片选线上发送使能信号(高电平或者低电平,根据从机而定)表示选中该从设备;


SPI总线在进行数据传送时,先传送高位,后传送低位;数据线为高电平表示逻辑‘1’,低电平表示逻辑‘0’,一字节传送完成后无需应答即可开始下一字节的传送;SPI总线采用同步方式工作,时钟线在上升沿或下降沿时发送器向数据线上发送数据,在紧接着的下降沿或上升沿接收器从数据线上读取数据,完成一位数据传送,八个时钟周期即可完成一个字节数据的传送;


极性和相位:

PI总线有4种不同的工作模式,取决于时钟极性CPOL和时钟相位CPHL这两个因素;

时钟极性CPOL表示时钟线SCLK空闲时的状态;

        CPOL=0时,空闲时时钟线SCLK为低电平;

        CPOL=1时,空闲时时钟线SCLK为高电平;

时钟相位CPHL表示采样时刻;

        CPHL=0时,每个周期的第一个时钟沿采样数据;

        CPHL=1时,每个周期的第二个时钟沿采样数据;

极性和相位设置

对于一个特定的从设备来说,一般在出厂时就会将其设计为某种特定的工作模式,我们在使用该设备就必须保证主设备的工作模式和该从设备保持一致否则是无法进行通信的,所以一般我们需要对主设备的CPOL极性和CPHA进行配置;


SPI四种工作模式:

注意,在此SPI传输数据采用的时软件模拟时序,跟此前串口采用的是硬件时序控制(寄存器控制)是不一样的;

软件模拟时序:软件模拟时序是通过编程代码(如循环、延时函数、定时器中断等)来控制信号的时序关系。开发者需要手动编写代码来生成特定的时序波形(如SPI、I2C、PWM等),通常依赖CPU的指令执行时间或定时器来控制信号的切换。

硬件时序控制:硬件时序控制是通过直接操作单片机的硬件寄存器(如定时器、PWM模块、SPI/I2C控制器等)来生成时序信号。单片机通常内置了专用的硬件模块,开发者通过配置这些模块的寄存器来实现特定的时序功能。

在以后编程开发中如何选择:

选择软件模拟时序:

1. 需要快速实现简单功能。

2.硬件平台没有专用硬件模块。

3.对时序精度要求不高。

选择硬件时序控制(寄存器操作):

1.需要高速、精确的时序控制。

2.需要节省CPU资源。

3.硬件平台提供专用硬件模块。


SPI模式1(软件模拟时序)

模式1:时钟极性CPOL=0;时钟相位CPHA=0;时钟线SCLK空闲时为低电平,数据线在第一个时钟沿(上升沿)采样数据;

代码示例:

/*
	SPI模式1软件模拟时序
	形参:dat-->发送的数据
*/
u8 SPI_WRbyte(u8 dat)
{
	SCLK=0;//时钟线拉低
	u8 data_rx=0;//读取数据
	for(u8 i=0;i<8;i++)
	{
		SCLK=0;//开始发送数据
		if(dar & 0x80) MOSI=1;//主机输出从机输入
		else MOSI=0;//主机输入从机输出
		SCLK=1;//一位数据发送完成
		dat<<=1;
		data_rx<<=1;//接收数据一定要先左移1位,因为先赋值为1的话,循环8次;最前面的1就会丢失
		if(MISO) data_rx|=0x01;
	}
	SCLK=0;//时钟线空闲电平为低电平
	return data_rx;
}

SPI模式2(软件模拟时序)

模式2:时钟极性CPOL=0;时钟相位CPHA=1;时钟线SCLK空闲时为低电平,数据线在第二个时钟沿(下降沿)采样数据;

代码示例:

/*
	SPI模式2软件模拟时序
	形参:dat-->发送的数据
*/
u8 SPI2_WRbyte(u8 dat)
{
	SCLK=0;//时钟线空闲电平为低电平
	u8 data_rx=0;//读取数据
	for(u8 i=0;i<8;i++)
	{
		SCLK=1;//开始发送数据
		if(dat & 0x80) MOSI=1;//主机输出,从机输入
		else MOSI=0;//主机输出,从机输入
		SCLK=0;//一位数据发送完成
		dat<<=1;
		data_rx<<=1;
		if(MISO) data_rx|=0x01; 
	}
	SCLK=0;//时钟线空闲电平为低电平
	return data_rx;
}

SPI模式3(软件模拟时序)

模式3:时钟极性CPOL=1;时钟相位CPHA=0;时钟线SCLK空闲时为高电平,数据线在第一个时钟沿(下降沿)采样数据;

代码示例:

/*
	SPI模式3软件模拟时序
	形参:dat-->发送的数据
*/
u8 SPI3_WRbyte(u8 dat)
{
	SCLK=1;//时钟线空闲电平为高电平
	u8 data_rx=0;//读取数据
	for(u8 i=0;i<8;i++)
	{
		SCLK=1;//开始发送数据
		if(dat & 0x80) MOSI=1;//主机输出,从机输入
		else MOSI=0;//主机输出从机输入
		SCLK=0;//一位数据发送完成
		dat<<=1;
		data_rx<<=1;
		if(MISO) data_rx|=0x01;
	}
	SCLK=1;//时钟线空闲电平为高电平
	return data_rx;
}

SPI模式4(软件模拟时序)

模式4:时钟极性CPOL=1;时钟相位CPHA=1;时钟线SCLK空闲时为高电平,数据线在第二个时钟沿(上升沿)采样数据

代码示例:

/*
	SPI模式3软件模拟时序
	形参:dat-->发送的数据
*/
u8 SPI4_WRbyte(u8 dat)
{
	SCLK=1;//时钟线空闲电平为高电平
	u8 data_rx=0;//读取数据
	for(u8 i=0;i<8;i++)
	{
		SCLK=0;//开始发送数据
		if(dat & 0x80) MOSI=1;//主机输出,从机输入
		else MISO=0;//主机输出从机输入
		SCLK=1;//一位数据发送完成
		dat<<=1;
		data_rx<<=1;
		if(MISO) data_rx|=0x01;
	}
	SCLk=1;//时钟线空闲电平为高电平
	return data_rx;
}

OLED显示屏

OLED显示屏介绍

屏幕类型:OLED(自发光显示屏)

屏幕接口:串行接口(只有一根数据线)、并行通讯(多条数据线);

接口协议:IIC或SPI(4线/3线模式);

屏幕尺寸:0.96寸,分辨率:128(列)*64(行);单色屏幕;

屏幕颜色:单色屏幕、16位(bit)屏幕(RGB565)、26位屏幕;

当前开发板:OLED屏幕,串行通讯方式,通讯接口:SPI

电路原理图:

根据原理图,查看对应引脚

OLED1脚(GND)——接地(低电平)

OLED2脚(VCC)——接3.3V高电压(高电平)

OLED3脚(D0)——PB14时钟线(SCK)

OLED4脚(D1)——PB13数据线(MOSI主机输出从机输入)

OLED5脚(RES)——PB12复位线(低电平复位)

OLED6脚(DC)——PB1数据命令选择脚(低电平0:发送命令;高电平1:发送数据)

OLED7脚(CS)——PA7片选信号线(低电平选中)

OLED显示屏引脚配置

配置步骤:

1.开时钟

2.配置GPIO引脚模式

3.上拉下拉

4.将每个引脚进行宏定义,方便操作;

代码示例:

//宏定义
#define OLED_CS(x) if(x==1){GPIOA->ODR|=1<<7;}\
                   else if(x==0){GPIOA->BRR|=1<<7;}
#define OLED_DC(x)  if(x==1){GPIOB->ODR|=1<<1;}\
                   else if(x==0){GPIOB->BRR|=1<<1;}
#define OLED_RES(x)  if(x==1){GPIOB->ODR|=1<<12;}\
                   else if(x==0){GPIOB->BRR|=1<<12;}
#define OLED_MOSI(x)  if(x==1){GPIOB->ODR|=1<<13;}\
                   else if(x==0){GPIOB->BRR|=1<<13;}   
#define OLED_SCLK(x)  if(x==1){GPIOB->ODR|=1<<14;}\
                   else if(x==0){GPIOB->BRR|=1<<14;}   

#include"OLED.h"
/*
  OLED显示屏引脚配置函数
  1脚(GND)——接地(低电平)
  2脚(VCC)——接3.3V高电压(高电平)
  3脚(D0)——PB14时钟线(SCK)
  4脚(D1)——PB13数据线(MOSI主机输出从机输入)
  5脚(RES)——PB12复位线(低电平复位)
  6脚(DC)——PB1数据命令选择脚(低电平0:发送命令;高电平1:发送数据)
  7脚(CS)——PA7片选信号线(低电平选中)
*/
void OLED_GPIO_Init(void)
{
  //开时钟
  RCC->APB2ENR|=1<<2;//开启PA时钟
  RCC->APB2ENR|=1<<3;//开启PB时钟
  //配置GPIO引脚模式
  GPIOB->CRL&=0xffffff0f;
  GPIOB->CRL|=0x00000030;//PB1
  GPIOB->CRH&=0xf000ffff;
  GPIOB->CRH|=0x03330000;//PB12,13,14;
  GPIOA->CRL&=0x0fffffff;
  GPIOA->CRL|=0x30000000;//PA7
  //上拉下拉
  GPIOA->ODR|=1<<7;//取消选中设备
}
  

OLED屏幕实现发送1字节数据函数(软件模拟时序)

分析OLED显示屏时序接口:

CS片选信号是低电平选中设备

SCLK时钟线空闲时既可以为高电平也可以为低电平,上升沿为读取数据,下降沿为发送数据。推断出时钟极性既可以为1也可以为0。

当时钟极性为0时,第一个时钟沿(上升沿)为采样读取数据,相位为0;为SPI工作模式1;

当时钟极性为1时,第二个时钟沿(上升沿)为采样读取数据,相位为1;为SPI工作模式4;

实现发送一字节数据函数(软件模拟时序)代码示例:

/*
  OLED屏幕SPI模式1发送一字节函数
  形参:	dat-->发送的数据
        flag-->DC数据命令选择标志位
当时钟极性为0时,第一个时钟沿(上升沿)为采样读取数据,相位为0;为SPI工作模式1;
*/
void OLED_Wbyte(u8 dat,u8 flag) 
{
	OLED_CS(0);//片选线低电平选中设备
	OLED_SCLK(0);//时钟线空闲电平为低电平
	OLED_DC(flag);//数据命令选择线选择发送命令还是数据
	for(u8 i=0;i<8;i++)
	{
		OLED_SCLK(0);//开始发送时间
    if(dat & 0x80)
    {
      OLED_MOSI(1);
    }
    else OLED_MOSI(0);
    OLED_SCLK(1);//数据发送完成
    dat<<=1;
  }
  OLED_SCLK(0);//时钟线空闲电平为低电平
  OLED_CS(1);//取消选中设备
}

清屏函数

清屏本质:将屏幕缓冲区(显存)全部填充为0(黑色)或0xFF(白色);

根据SSD1306驱动芯片OLED屏中文参考手册(第7页)

通过命令B0h到B7h,设置目标显示位置的页起始地址。

通过命令00h到0Fh设置指针的下起始列地址。

通过命令10h到1Fh设置指针的上起始列地址。

寻址方式:页面寻址,把68行分成了8页,列不变,依旧时128列。

在页面寻址下,在显示RAW读/写之后,列地址指针会自动增加1,行地址不会;

代码示例:

/*
  OLED屏幕清屏函数
  形参:dat->清屏的数据
*/
#define OLED_cmd 0//写入命令
#define OLED_dat 1//写入数据
void OLED_clear(u8 dat)
{
  for(u8 i=0;i<8;i++)
  {
    OLED_Wbyte(0xb0+i,OLED_cmd);//页起始地址
    OLED_Wbyte(0x00,OLED_cmd);//下列低起始地址
    OLED_Wbyte(0x10,OLED_cmd);//上列高起始地址
    for(u8 j=0;j<128;j++)
    {
      OLED_Wbyte(dat,OLED_dat);
    }
  }
}

OLED屏幕初始化函数

OLED屏幕代码序列厂家都会提供好的,我们只用复制粘贴在修改即可(这里将清屏函数写入0xff是起全部点亮效果用来测试OLED屏幕初始化是否有用。正常写入0,即可);

代码示例:

/*
  OLED显示屏初始化函数
*/
void OLED_Init(void)
{
    OLED_GPIO_Init();//OLED引脚配置函数
    OLED_RES(1);//复位
    Delay_MS(100);
    OLED_RES(0);
    Delay_MS(100);
    OLED_RES(1);//复位
    OLED_Wbyte(0xAE,OLED_cmd); /*display off*/ 
	OLED_Wbyte(0x00,OLED_cmd); /*set lower column address*/
	OLED_Wbyte(0x10,OLED_cmd); /*set higher column address*/
	OLED_Wbyte(0x40,OLED_cmd); /*set display start line*/ 
	OLED_Wbyte(0xB0,OLED_cmd); /*set page address*/ 
	OLED_Wbyte(0x81,OLED_cmd); /*contract control*/ 
	OLED_Wbyte(0xCF,OLED_cmd); /*128*/ 
	OLED_Wbyte(0xA1,OLED_cmd); /*set segment remap*/ 
	OLED_Wbyte(0xA6,OLED_cmd); /*normal / reverse*/ 
	OLED_Wbyte(0xA8,OLED_cmd); /*multiplex ratio*/ 
	OLED_Wbyte(0x3F,OLED_cmd); /*duty = 1/64*/ 
	OLED_Wbyte(0xC8,OLED_cmd); /*Com scan direction*/ 
	OLED_Wbyte(0xD3,OLED_cmd); /*set display offset*/ 
	OLED_Wbyte(0x00,OLED_cmd);
	OLED_Wbyte(0xD5,OLED_cmd); /*set osc division*/ 
	OLED_Wbyte(0x80,OLED_cmd);
	OLED_Wbyte(0xD9,OLED_cmd); /*set pre-charge period*/ 
	OLED_Wbyte(0Xf1,OLED_cmd);
	OLED_Wbyte(0xDA,OLED_cmd); /*set COM pins*/ 
	OLED_Wbyte(0x12,OLED_cmd);
	OLED_Wbyte(0xdb,OLED_cmd); /*set vcomh*/ 
	OLED_Wbyte(0x30,OLED_cmd);
	OLED_Wbyte(0x8d,OLED_cmd); /*set charge pump enable*/ 
	OLED_Wbyte(0x14,OLED_cmd);
	OLED_Wbyte(0xAF,OLED_cmd); /*display ON*/
	OLED_clear(0xff);//清屏函数写0xff全部点亮
}

主函数代码示例:

#include "stdio.h"
#include "OLED.h"
int main()
{
  OLED_Init();//显示屏初始化函数
  printf("各模块初始化完成\n");
  while(1)
  {
  }
}

代码运行效果:


设置光标函数

x--列        0列-127列

y--行        0行-7行(页面寻址),每8行为一个页面

代码示例:

/*
设置光标
形参:x --列0~127
      y --行0~7(页面寻址),每8行为一个页面
*/
void OLED_setpos(u8 x,u8 y)
{
  OLED_Wbyte(0xb0+(y&0x7),OLED_cmd);//页地址选择
  OLED_Wbyte(0x00|(x&0x0f),OLED_cmd);//设置列低4位
  OLED_Wbyte(0x10|((x>>4)&0xf),OLED_cmd);//设置列高4位
}

汉字显示函数

/*显示汉字*/
//形参:size--汉字的大小,y--光标设置的行,X--关闭设置的列
void OLED_DisplayFont(u8 x,u8 y,u8 size,const u8 *font)
{
  u8 i=0,j=0;
  for(i=0;i<size/8;i++)//汉字需要占用的页面数量
  {
    OLED_setpos(x,y+i);
    for(j=0;j<size;j++)//要显示的列数
    {
      OLED_Wbyte(font[i*size+j],OLED_dat);
    }    
  }
}

显示函数首先要在取模软件取模(取模软件设置):

取模代码:

const u8 font1[][16*16/8]=
{
 {0x00,0xFC,0x04,0x04,0xFC,0x20,0x10,0x4C,0x4B,0x48,0x48,0x48,0xC8,0x08,0x08,0x00,0x00,0x0F,0x04,0x04,0x0F,0x00,0x30,0x48,0x44,0x42,0x42,0x41,0x40,0x40,0x70,0x00},/*"吃",0*/

{0x02,0x02,0xE2,0x22,0x22,0xFE,0x22,0x22,0x22,0xFE,0x22,0x22,0xE2,0x02,0x02,0x00,0x00,0x00,0xFF,0x48,0x44,0x43,0x40,0x40,0x40,0x43,0x44,0x44,0xFF,0x00,0x00,0x00},/*"西",1*/

{0x00,0x00,0x00,0xFC,0x04,0x04,0xFC,0x04,0x02,0x02,0xFE,0x03,0x02,0x00,0x00,0x00,0x80,0x60,0x18,0x07,0x00,0x00,0x7F,0x20,0x14,0x08,0x31,0x0E,0x30,0x40,0x80,0x00},/*"瓜",2*/

{0x00,0x00,0x00,0xFE,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x33,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"!",3*/
};

主函数代码示例:


#include "OLED.h"


int main()
{
  OLED_Init();//显示屏初始化函数
  while(1)
  {
    OLED_DisplayFont(16+16,3,16,font1[0]);
    OLED_DisplayFont(16+16*2,3,16,font1[1]);
    OLED_DisplayFont(16+16*3,3,16,font1[2]);
    OLED_DisplayFont(16+16*4,3,16,font1[3]);
  }
}

代码运行结果:


制作不易!喜欢的小伙伴给个小赞赞!喜欢我的小伙伴点个关注!有不懂的地方和需要的资源随时问我哟!


网站公告

今日签到

点亮在社区的每一天
去签到