天天看点

0.96寸OLED屏幕

(题外话)为什么选择寄存器来实现,对于初学者而言我非常建议从寄存器配置开始,主要是因为搞单片机本来就是一项接近于底层硬件的工作,不要嫌麻烦。了解硬件外设工作原理和配置过程会对以后的调试有很大帮助。更容易理解库函数开发。
           

1.硬件资源描述

主控 STM32F103RC
通讯方式 硬件SPI1 +DMA1(DMA可选)
屏幕 0.96寸蓝色OLED屏幕

下面是屏幕图片

0.96寸OLED屏幕
0.96寸OLED屏幕

2.OLED屏幕驱动方法说明

屏幕的话可以在那啥宝上买到大概10块钱,现在应该还没涨价吧。至于买IIC协议的还是SPI协议的就看单片机使用习惯了,个人还是喜欢SPI的,相比IIC讲SPI传输速度更快点。至于NSS是接地还是硬件控制,看个人习惯,我选择的是硬件控制。

屏幕与单片机接线如下:
           

注意:VDD和GND千万不要搞反了不然10块钱白给(我就烧坏了一个)😂

2.1:怎么和屏幕通讯并配置单片机的通讯

  • 下面这张图是OLED屏幕驱动芯片手册提供的SPI时序
    0.96寸OLED屏幕

    CS# :通讯时一定要保持低电平。

    D/C# :就是命令模式选择信号低电平时SDIN(D1)的数据就是命令控制字节。

    SCLK :空闲电平高低无关,从这条时钟线可以看出屏幕是在上升沿锁存数据,下降沿允许数据变化。(这点很重要!)

    SDIN(D1) :8为数据高位在前。

(第三行的时序就是最后两行的组合)

  • 单片机通讯配置
    0.96寸OLED屏幕

    这张图来自STM32F1参考手册

    stm32f1可以将SPI配置成4种通讯时序。对比我们的屏幕,只能选择上升沿采样的两种方式。

如下表:

序号 控制位 描述
1 CPHA=1 CPOL=1 空闲时高电平,第二个边沿采样(上升沿)
2 CPHA=0 CPOL=0 空闲时低电平,第一个边沿采样(上升沿)

下面的例程使用的是上表中序号2。

stm32SPI1寄存器配置如下:

void SPI1_Init(void)
{
	u16 spitest= 0;
	
	RCC->APB2ENR			|= 1<<2;				//IO端口A时钟开启
	RCC->APB2ENR			|= 1<<0;				//辅助功能IO时钟开启
	RCC->APB2ENR			|= 1<<12;				//SPI1时钟开启
	GPIOA->CRL				&= 0x0F00FFFF;			//PA4配置清除(NSS)		PA5配置清除(SCK)		PA7配置清除(MOSI)
	GPIOA->CRL				|= 0xB0BB0000;			//PA4复用功能推挽输出模式,输出模式,最大速度10MHz		PA5 & PA7复用功能推挽输出模式,输出模式,最大速度50MHz
	
	spitest						|= 1<<2;			//配置为主设备
	spitest						|= 1<<9;			//软件从设备管理
	spitest						|= 1<<8;			//NSS输出
	spitest						|= 1<<15;			//选择“单线双向”模式
	spitest						|= 1<<14;			//输出使能(只发模式)
	spitest						&=~(1<<1);			//空闲状态时,SCK保持低电平
	spitest						&=~(1<<0);			//数据采样从第一个时钟边沿开始
	spitest						&=~(1<<11);			//使用8位数据帧格式进行发送/接收
	spitest						&= 0xFFFFFFC7;		//波特率36Mbs= 4.5M/S
	spitest						&=~(1<<7);			//先发送MSB
	SPI1->CR1				   = spitest;
	SPI1->CR2					|= 1<<1;			//TXDMAEN:发送缓冲区DMA使能
	SPI1->CR1					|=1<<6;				//打开SPI设备
}

           

2.2:DMA

stm32的DMA可是32的一大特色,优点在于它不需要CPU干预,只要DMA被触发就能直接对数据进行搬运。所以使用它可以节省CPU的工作时间用来处理其他任务,在一些大项目中这点尤为突出。详细的配置介绍请参考STM32F1参考手册,这块我就不多言了。直接上代码。

(不想使用DMA的话可以将主函数注释掉的部分取消注释,并在屏幕初始化函数子函数内倒数第二行取消调用DMA函数)

inline static void DMA_OLED_Init(void)
{
	RCC->AHBENR				 |=	1<<0;		//DMA1时钟开启
	DMA1_Channel3->CPAR = (u32)(&(SPI1->DR));//CPAR:外设数据寄存器的基地址
	DMA1_Channel3->CMAR = (u32)(OLED_SRAM);	//CMAR:存储器的基地址
	DMA1_Channel3->CCR |= 0<<14;			//MEM2MEM:非存储器到存储器模式
	DMA1_Channel3->CCR |= 0<<6;				//MINC:不执行外设地址增量操作
	DMA1_Channel3->CCR |= 1<<7;				//MINC:存储器地址增量模式
	DMA1_Channel3->CCR |= 1<<5;				//CIRC:循环模式
	DMA1_Channel3->CCR |= 1<<4;				//DIR :从存储器读
	DMA1_Channel3->CNDTR= 0x400;			//1024个字节
	DMA1_Channel3->CCR |= 1<<0;				//EN  :通道开启

}
           

2.3:OLED屏幕的初始化及设置

相关的控制命令在屏幕手册中都有,下面要注意的一点就是DC控制要注意时间问题。DC是由GPIO控制的,所以反转速度很快,数据发送相比DC要慢得多。倒数第四行的 Wait_us(10); 就是在调试过程中遇到的问题,从代码上看数据已经发送完成了然后再改变DC命令选择,貌似没问题。但是屏幕一直没反应,用逻辑分析仪抓取时序图后发现该问题,下面附图。例程代码已纠正放心复制。

0.96寸OLED屏幕

图中时序自上而下分别是SCK,MISO,DC。明显看出数据还未发送结束DC已经改变。

分析原因:子函数OLED_SendCmd(unsigned char)是判断发送区是否为空,然后条件式的装入。而硬件SPI则需等待上一个数据发送完成才会再处理刚送进来的数据,同样也需要时间。

代码部分如下:

void OLED_Init(void) //初始化函数
{
	#define OLED_DC  	PA1
	#define OLED_RES 	PA3
	#define OLED_NSS 	PA4
	#define OLED_D0  	PA5
	#define OLED_D1  	PA7
	
	GPIOA->CRL				&= 0xFFFF0F0F;	//rs,dc
	GPIOA->CRL				|= 0x00003030;	//
	
	OLED_RES = 0;			 //低电平复位
	Wait_us(100);
	OLED_RES = 1;			 //复位结束
	OLED_DC	 = 0;			 //命令模式
	Wait_us(100);

	OLED_SendCmd(0xAE);//关闭显示
	OLED_SendCmd(0xD5);//设置时钟分频因子,震荡频率
	OLED_SendCmd(0xF0);//[3:0],分频因子;[7:4],震荡频率
	OLED_SendCmd(0x81);//设置对比度
	OLED_SendCmd(0x7F);//128
	OLED_SendCmd(0x8D);//设置电荷泵开关
	OLED_SendCmd(0x14);//开
	OLED_SendCmd(0x20);//设置模式
	OLED_SendCmd(0x00);//设置为水平地址模式
	OLED_SendCmd(0xD3);//行偏移命令
	OLED_SendCmd(0x2A);//校正参数
  	OLED_SendCmd(0x21);//设置列地址的起始和结束的位置
  	OLED_SendCmd(0x00);//0
  	OLED_SendCmd(0x7F);//127   
  	OLED_SendCmd(0x22);//设置页地址的起始和结束的位置
  	OLED_SendCmd(0x00);//0
 	OLED_SendCmd(0x07);//7
	OLED_SendCmd(0xC8);//0xc9上下反置 0xc8正常
 	OLED_SendCmd(0xA1);//0xa0左右反置 0xa1正常
	OLED_SendCmd(0xA4);//全局显示开启;0xa4正常,0xa5无视命令点亮全屏
	OLED_SendCmd(0xA6);//设置显示方式;A7,反相显示;A6,正常显示	
	OLED_SendCmd(0xAF);//开启显示
 	OLED_SendCmd(0x56);
	Wait_us(10);//
	OLED_DC	 = 1;			 //显示数据模式
	DMA_OLED_Init();
	Wait_us(10);//

}

           

OLED_SendCmd(0xA4);//全局显示开启;0xa4正常,0xa5无视命令点亮全屏

这行代码在程序调试的时候可以将参数改为0xa5判断通讯是否正常。

这个是命令发送函数

OLED屏幕刷新一帧需要128*8个字节,每个字节的每一位控制屏幕的一个像素点(位的0或1表示亮灭),刚好是128 *64个位。

static void OLED_SendCmd(unsigned char ctrl_data)
{
	unsigned char t=200;
	while(! (SPI1->SR & 1<<1) )			//SPI1->SR & 1<<1=1:发送缓冲为空。
	{
		t--;
		if(t<=0)
			break;
	}

	SPI1->DR	= ctrl_data;
}




void OLED_Write(unsigned char ASII,unsigned char ye,unsigned char lie)
{
	char i;
	for(i=0;i<6;i++)
	{
		OLED_SRAM[ye][lie+i]=F6X8[(ASII-32)*6+i];
	}
}

           

这个是ASII字库,网上能找到太多了。

const unsigned char F6X8[] =
{
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ,   // sp
    0x00, 0x00, 0x00, 0x2f, 0x00, 0x00 ,   // !
    0x00, 0x00, 0x07, 0x00, 0x07, 0x00 ,   // "
    0x00, 0x14, 0x7f, 0x14, 0x7f, 0x14 ,   // #
    0x00, 0x24, 0x2a, 0x7f, 0x2a, 0x12 ,   // $
    0x00, 0x62, 0x64, 0x08, 0x13, 0x23 ,   // %
    0x00, 0x36, 0x49, 0x55, 0x22, 0x50 ,   // &
    0x00, 0x00, 0x05, 0x03, 0x00, 0x00 ,   // '
    0x00, 0x00, 0x1c, 0x22, 0x41, 0x00 ,   // (
    0x00, 0x00, 0x41, 0x22, 0x1c, 0x00 ,   // )
    0x00, 0x14, 0x08, 0x3E, 0x08, 0x14 ,   // *
    0x00, 0x08, 0x08, 0x3E, 0x08, 0x08 ,   // +
    0x00, 0x00, 0x00, 0xA0, 0x60, 0x00 ,   // ,
    0x00, 0x08, 0x08, 0x08, 0x08, 0x08 ,   // -
    0x00, 0x00, 0x60, 0x60, 0x00, 0x00 ,   // .
    0x00, 0x20, 0x10, 0x08, 0x04, 0x02 ,   // /
    0x00, 0x3E, 0x51, 0x49, 0x45, 0x3E ,   // 0				//30[16]48
    0x00, 0x00, 0x42, 0x7F, 0x40, 0x00 ,   // 1
    0x00, 0x42, 0x61, 0x51, 0x49, 0x46 ,   // 2
    0x00, 0x21, 0x41, 0x45, 0x4B, 0x31 ,   // 3
    0x00, 0x18, 0x14, 0x12, 0x7F, 0x10 ,   // 4
    0x00, 0x27, 0x45, 0x45, 0x45, 0x39 ,   // 5
    0x00, 0x3C, 0x4A, 0x49, 0x49, 0x30 ,   // 6
    0x00, 0x01, 0x71, 0x09, 0x05, 0x03 ,   // 7
    0x00, 0x36, 0x49, 0x49, 0x49, 0x36 ,   // 8
    0x00, 0x06, 0x49, 0x49, 0x29, 0x1E ,   // 9
    0x00, 0x00, 0x36, 0x36, 0x00, 0x00 ,   // :
    0x00, 0x00, 0x56, 0x36, 0x00, 0x00 ,   // ;
    0x00, 0x08, 0x14, 0x22, 0x41, 0x00 ,   // <
    0x00, 0x14, 0x14, 0x14, 0x14, 0x14 ,   // =
    0x00, 0x00, 0x41, 0x22, 0x14, 0x08 ,   // >
    0x00, 0x02, 0x01, 0x51, 0x09, 0x06 ,   // ?
    0x00, 0x32, 0x49, 0x59, 0x51, 0x3E ,   // @
    0x00, 0x7C, 0x12, 0x11, 0x12, 0x7C ,   // A
    0x00, 0x7F, 0x49, 0x49, 0x49, 0x36 ,   // B
    0x00, 0x3E, 0x41, 0x41, 0x41, 0x22 ,   // C
    0x00, 0x7F, 0x41, 0x41, 0x22, 0x1C ,   // D
    0x00, 0x7F, 0x49, 0x49, 0x49, 0x41 ,   // E
    0x00, 0x7F, 0x09, 0x09, 0x09, 0x01 ,   // F
    0x00, 0x3E, 0x41, 0x49, 0x49, 0x7A ,   // G
    0x00, 0x7F, 0x08, 0x08, 0x08, 0x7F ,   // H
    0x00, 0x00, 0x41, 0x7F, 0x41, 0x00 ,   // I
    0x00, 0x20, 0x40, 0x41, 0x3F, 0x01 ,   // J
    0x00, 0x7F, 0x08, 0x14, 0x22, 0x41 ,   // K
    0x00, 0x7F, 0x40, 0x40, 0x40, 0x40 ,   // L
    0x00, 0x7F, 0x02, 0x0C, 0x02, 0x7F ,   // M
    0x00, 0x7F, 0x04, 0x08, 0x10, 0x7F ,   // N
    0x00, 0x3E, 0x41, 0x41, 0x41, 0x3E ,   // O
    0x00, 0x7F, 0x09, 0x09, 0x09, 0x06 ,   // P
    0x00, 0x3E, 0x41, 0x51, 0x21, 0x5E ,   // Q
    0x00, 0x7F, 0x09, 0x19, 0x29, 0x46 ,   // R
    0x00, 0x46, 0x49, 0x49, 0x49, 0x31 ,   // S
    0x00, 0x01, 0x01, 0x7F, 0x01, 0x01 ,   // T
    0x00, 0x3F, 0x40, 0x40, 0x40, 0x3F ,   // U
    0x00, 0x1F, 0x20, 0x40, 0x20, 0x1F ,   // V
    0x00, 0x3F, 0x40, 0x38, 0x40, 0x3F ,   // W
    0x00, 0x63, 0x14, 0x08, 0x14, 0x63 ,   // X
    0x00, 0x07, 0x08, 0x70, 0x08, 0x07 ,   // Y
    0x00, 0x61, 0x51, 0x49, 0x45, 0x43 ,   // Z
    0x00, 0x00, 0x7F, 0x41, 0x41, 0x00 ,   // [
    0x00, 0x55, 0x2A, 0x55, 0x2A, 0x55 ,   // 55
    0x00, 0x00, 0x41, 0x41, 0x7F, 0x00 ,   // ]
    0x00, 0x04, 0x02, 0x01, 0x02, 0x04 ,   // ^
    0x00, 0x40, 0x40, 0x40, 0x40, 0x40 ,   // _
    0x00, 0x00, 0x01, 0x02, 0x04, 0x00 ,   // '
    0x00, 0x20, 0x54, 0x54, 0x54, 0x78 ,   // a
    0x00, 0x7F, 0x48, 0x44, 0x44, 0x38 ,   // b
    0x00, 0x38, 0x44, 0x44, 0x44, 0x20 ,   // c
    0x00, 0x38, 0x44, 0x44, 0x48, 0x7F ,   // d
    0x00, 0x38, 0x54, 0x54, 0x54, 0x18 ,   // e
    0x00, 0x08, 0x7E, 0x09, 0x01, 0x02 ,   // f
    0x00, 0x18, 0xA4, 0xA4, 0xA4, 0x7C ,   // g
    0x00, 0x7F, 0x08, 0x04, 0x04, 0x78 ,   // h
    0x00, 0x00, 0x44, 0x7D, 0x40, 0x00 ,   // i
    0x00, 0x40, 0x80, 0x84, 0x7D, 0x00 ,   // j
    0x00, 0x7F, 0x10, 0x28, 0x44, 0x00 ,   // k
    0x00, 0x00, 0x41, 0x7F, 0x40, 0x00 ,   // l
    0x00, 0x7C, 0x04, 0x18, 0x04, 0x78 ,   // m
    0x00, 0x7C, 0x08, 0x04, 0x04, 0x78 ,   // n
    0x00, 0x38, 0x44, 0x44, 0x44, 0x38 ,   // o
    0x00, 0xFC, 0x24, 0x24, 0x24, 0x18 ,   // p
    0x00, 0x18, 0x24, 0x24, 0x18, 0xFC ,   // q
    0x00, 0x7C, 0x08, 0x04, 0x04, 0x08 ,   // r
    0x00, 0x48, 0x54, 0x54, 0x54, 0x20 ,   // s
    0x00, 0x04, 0x3F, 0x44, 0x40, 0x20 ,   // t
    0x00, 0x3C, 0x40, 0x40, 0x20, 0x7C ,   // u
    0x00, 0x1C, 0x20, 0x40, 0x20, 0x1C ,   // v
    0x00, 0x3C, 0x40, 0x30, 0x40, 0x3C ,   // w
    0x00, 0x44, 0x28, 0x10, 0x28, 0x44 ,   // x
    0x00, 0x1C, 0xA0, 0xA0, 0xA0, 0x7C ,   // y
    0x00, 0x44, 0x64, 0x54, 0x4C, 0x44 ,   // z
    0x14, 0x14, 0x14, 0x14, 0x14, 0x14     // horiz lines
};

           

这个是用到的延时初始化函数和ms级延时函数

inline void Wait_Init(void)
{
	SysTick->CTRL			&= (unsigned int)(~(1<<0));			//关闭SysTick定时器
	SysTick->CTRL			&= (unsigned int)(~(1<<2));			//9MHz
	SysTick->CTRL			&= (unsigned int)(~(1<<1));			//不产生下溢中断
}
void Wait_ms(unsigned int t)
{
#define ms_t 9000
	SysTick->LOAD			 = ms_t;				//1ms定时
	SysTick->VAL			 = 0;						//当前值清零
	SysTick->CTRL			|= 1<<0;				//打开SysTick定时器
	while(t)
	{
		if(SysTick->CTRL & 1<<16)				//判断下溢
		{t--;}
	}
}

           

下面是主函数

注意:前面用到的子函数请做好声明

unsigned char OLED_SRAM[8][128]; //图像储存
extern const unsigned char F6X8[];

int main()
{
	Wait_Init();//这里的延时函数我用的是SYSTICK
	SPI1_Init();
	OLED_Init();
	OLED_Write('L',0,0);
	OLED_Write('i',0,6);
	OLED_Write('a',0,12);
	OLED_Write('n',0,18);
	OLED_Write('g',0,24);
	while(1)
	{
//		for(j=0;j<8;j++)
//		{
//				for(i=0;i<128;i++)
//					OLED_Write(OLED_SRAM[j][i]);
//		}
		
	}
}

           

结果

0.96寸OLED屏幕

如果有问题欢迎指正和讨论

继续阅读