STM32 7针0.96寸OLED显示屏(硬件SPI+DMA)无需内核响应 超高刷新率!

芯片:STM32f103c8t6
f103型号大同小异,其他芯片请根据情况修改接口和配置

使用的是7针的0.96寸OLED屏幕,黑白两色显示
以下是实物图
STM32 7针0.96寸OLED显示屏(硬件SPI+DMA)无需内核响应 超高刷新率!_第1张图片

一.原理:

首先你要了解STM32上的AFIO(复用功能),DMA,SPI 和OLED上的SSD1306驱动的原理和命令,还有C语言的指针,如果其中任何一项不熟悉的话,请先学习一遍
相关资料:
DMA原理
SPI的基本原理(库函数版)
学习笔记 端口复用&重映射
一文彻底了解SSD1306驱动0.96寸OLED
通过学习后相信你会更好明白这篇文章以及对STM32的了解

1.STM32部分:

DMA每次会将8bit的数据从SRAM中传输到SPI的缓存区中
SPI启用DMA功能后会自动响应DMA传输完成的请求,将8bit数据写入发送缓存区,此款屏幕总共有128 * 64个像素,所以要传输128*64/8=1024次
当发送寄存器为空时,发送缓存区会将将数据写入到发送寄存器
当8bit数据写满时,发送寄存器通过MOSI口输出数据

STM32 7针0.96寸OLED显示屏(硬件SPI+DMA)无需内核响应 超高刷新率!_第2张图片

2.OLED上的SSD1306驱动部分:

OLED屏幕有三种刷新方式分别为页地址模式, 水平地址模式和垂直地址模式,。
水平地址模式和垂直地址模式可以在一页(一列)写完后自动换页(列)
所以在水平地址寻址或者垂直地址寻址模式下,只要源源不断的发送数据即可

页地址模式
STM32 7针0.96寸OLED显示屏(硬件SPI+DMA)无需内核响应 超高刷新率!_第3张图片

水平地址模式
STM32 7针0.96寸OLED显示屏(硬件SPI+DMA)无需内核响应 超高刷新率!_第4张图片
垂直地址模式
STM32 7针0.96寸OLED显示屏(硬件SPI+DMA)无需内核响应 超高刷新率!_第5张图片
参考资料
https://blog.csdn.net/gengyuchao/article/details/86608037

3.所以过程是:

SRAM->DMA传输->SPI响应DMA请求->写入缓存区->写入寄存器->自动写入发送寄存器->发送数据->OLED接收数据并写入显存->显示
这个过程是不需要STM32响应中断的,全程走DMA
缺点是需要一次性写入整个OLED_SRAM
但是DMA和硬件SPI拥有很快的传输速度,所以这个缺点也被克服了
经过测试,帧率有大约30帧左右

二.相关接口:

1.GND 电源地
2.VCC 电源正(3~5.5V)
3.D0(SCL) SCK管脚
4.D1(SDA) MOSI管脚
5.RES(RST) 用来复位(低电平复位)
6.DC(D/C) 数据和命令控制管脚 1表示数据 0表示命令
7.CS(NSS) 片选管脚

三.IO口和SPI初始化:

1.IO口设置

通过查阅STM32C8T6的原理图(其他型号芯片可以看自己的原理图),可以发现有两个SPI
我用的是SPI1
STM32 7针0.96寸OLED显示屏(硬件SPI+DMA)无需内核响应 超高刷新率!_第6张图片STM32 7针0.96寸OLED显示屏(硬件SPI+DMA)无需内核响应 超高刷新率!_第7张图片
因为在SPI启用了主机信号由软件控制,并且4线SPI的0.96寸OLED模块没有发送数据的针脚

所以将PA4作为RST功能使用,将PA6作为DC功能使用

#ifndef __oled_spi_dma_H
#define __oled_spi_dma_H
#include "sys.h"

#define S6X8 0
#define S8X16 1

#define OLED_SCL_CLR()	GPIO_ResetBits(GPIOA,GPIO_Pin_5) //时钟
#define OLED_SCL_SET()	GPIO_SetBits(GPIOA,GPIO_Pin_5)

#define OLED_SDA_LOW()	GPIO_ResetBits(GPIOA,GPIO_Pin_7) //MOSI主设备输出
#define OLED_SDA_HIGH()	GPIO_SetBits(GPIOA,GPIO_Pin_7)

#define OLED_RST_OFF()	GPIO_ResetBits(GPIOA,GPIO_Pin_4) //接低电平复位
#define OLED_RST_ON()	GPIO_SetBits(GPIOA,GPIO_Pin_4)                                                                  

#define OLED_DC_CMD()	GPIO_ResetBits(GPIOA,GPIO_Pin_6) //模式 
#define OLED_DC_DAT()	GPIO_SetBits(GPIOA,GPIO_Pin_6)

void GPIO_Configuration(void); //GPIO和SPI初始化
void OLED_Write(u8 ye,u8 lie,u8* ascii,u8 size); //写入ASCII文字
void OLED_SendCmd(u8 TxData); //发送命令
void OLED_Init(void); //OLED初始化
void DMA_OLED_Init(void); //DMA初始化
#endif

2.通过SPI通信原理图:

STM32 7针0.96寸OLED显示屏(硬件SPI+DMA)无需内核响应 超高刷新率!_第8张图片

将SPI配置成

1.单线只发送或者双线全双工
2.主机模式
3.一次发送8位数据
4.空闲时间为低电平时
5.第一个上升沿采样
6.主机片选信号(CS)由软件控制
7.预分频 256
8.数据传输从 MSB 高位开始 低位为LSB
9.CRC 值计算的多项式设置为大于1即可
10.SPI1->CR2=1 << 1; //允许DMA往缓冲区内发送
(这是最重要的一步即启用SPI的DMA响应功能)
以下是代码

void GPIO_Configuration(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	SPI_InitTypeDef SPI_InitStructure;
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO | RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1 , ENABLE);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5|GPIO_Pin_7;
 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; 		 //复用输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度50MHz
	GPIO_Init(GPIOA, &GPIO_InitStructure);	  //初始化GPIOA5,7
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_6;	 
 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度50MHz
 	GPIO_Init(GPIOA, &GPIO_InitStructure);	  //初始化GPIOA4,6
	GPIO_SetBits(GPIOA,GPIO_Pin_5|GPIO_Pin_7);  //PA5/PA7上拉
	
	SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx; //设置 SPI 单线只发送
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //主 SPI
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // SPI 发送接收 8 位帧结构
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;//串行同步时钟的空闲状态为低电平
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;//第1个跳变沿数据被采样
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS 主机片选信号(CS)由软件控制
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //预分频 256
	//SPI 速度设置函数(调整传输速度快慢 只有4个分频可选)
	//SPI_BaudRatePrescaler_2 2 分频 (SPI 36M@sys 72M)
	//SPI_BaudRatePrescaler_8 8 分频 (SPI 9M@sys 72M)
	//SPI_BaudRatePrescaler_16 16 分频 (SPI 4.5M@sys 72M)
	//SPI_BaudRatePrescaler_256 256 分频 (SPI 281.25K@sys 72M)
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //数据传输从 MSB 高位开始 低位为LSB
	SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC 值计算的多项式
	SPI_Init(SPI1, &SPI_InitStructure); //根据指定的参数初始化外设 SPIx 寄存器
	SPI1->CR2=1 << 1; //允许DMA往缓冲区内发送
	
	SPI_Cmd(SPI1, ENABLE); //使能 SPI 外设
} ;

四.DMA功能初始化

通过查阅STM32参考手册9.3.3
数据从SRAM传输到SPI1的缓存区
SPI1_TX在DMA1的CH3通道
STM32 7针0.96寸OLED显示屏(硬件SPI+DMA)无需内核响应 超高刷新率!_第9张图片

将DMA1配置成

1.外设基地址为缓存区 即&SPI1-DR 并且强制转换成u32类型
2.内存基地址为SRAM 即OLED_SRAM 并且强制转换成u32类型
3.方向是从储存器读取发送到外设
4.通道缓存的大小 一个画面要传输1024次
5.外设地址偏移不变,因为缓存区的地址不变
6.内存地址要递增,因为OLED_SRAM是个数组,数据存在里面,指针地址要递增
7.传输数据大小为8bit
8.内存数据大小也为8bit
9.工作在循环模式(此模式不需要中断,次数结束后会自动重载为1024次)
10.不是非内存到内存的传输

代码如下

void DMA_OLED_Init(void)
{
	DMA_InitTypeDef DMA_InitStructure;
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能 DMA 时钟
	DMA_DeInit(DMA1_Channel3);
	DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&SPI1->DR; //DMA 外设 ADC 基地址
	DMA_InitStructure.DMA_MemoryBaseAddr = (u32)OLED_SRAM; //DMA 内存基地址
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //从储存器读取发送到外设
	DMA_InitStructure.DMA_BufferSize = 1024; //DMA 通道的 DMA 缓存的大小
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设地址不变
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址递增
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //8 位
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 8 位
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //工作在循环传输模式
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //DMA 通道 x 拥有中优先级
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //非内存到内存传输
	DMA_Init(DMA1_Channel3, &DMA_InitStructure); //根据指定的参数初始化

//DMA_Cmd(DMA1_Channel3, DISABLE); //不使能DMA1 CH3所指示的通道
  DMA_Cmd(DMA1_Channel3, ENABLE); //使能DMA1 CH3所指示的通道
}

五.命令发送函数和OLED初始化:

DC脚低电平时写入命令,高电平时写入数据

SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE)函数获取缓存区是否为空
SPI_I2S_SendData(SPI1, TxData)函数发送发送寄存器中的数据

(这两个函数是官方标准库中自带的
可以在stm32f10x_spi.c和stm32f10x_spi.h找到使用方法)

void OLED_SendCmd(u8 TxData) //发送命令
{
	u8 retry=0;
	OLED_DC_CMD(); //命令模式
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) //检查指定的 SPI标志位设置与否:发送缓存空标志位
	{
		retry++;
		if(retry>200) return ;
	}
	delay_ms(100);
	
	SPI_I2S_SendData(SPI1, TxData); //通过外设 SPIx 发送一个数据
	retry=0;
	OLED_DC_DAT(); //数据模式
}

关于OLED初始化
以下是需要注意的

  1. OLED_RST_OFF();
    delay_ms(100); //延时很重要!否则电平变化太快ssd1306无法检测到变化
    OLED_RST_ON();
    重置OLED
  2. OLED_SendCmd(0xd5);//设置时钟分频因子,震荡频率
    OLED_SendCmd(0x80);//[3:0],分频因子;[7:4],震荡频率
    不分频,震荡频率即有效采样八次后写入数据
  3. OLED_SendCmd(0xc9);//0xc9上下反置 0xc8正常
    OLED_SendCmd(0xa1);//0xa0左右反置 0xa1正常

    因为OLED_SRAM[X][Y]中X,Y越小越先被输入到OLED中
    如果设置为正常模式,则会显示反过来的图像
void OLED_Init(void) //初始化函数
{
	GPIO_Configuration();//端口初始化
	delay_ms(1000);

	OLED_RST_OFF();
	delay_ms(100);
	OLED_RST_ON();
	OLED_SendCmd(0xae);//关闭显示
	OLED_SendCmd(0xd5);//设置时钟分频因子,震荡频率
	OLED_SendCmd(0x80);//[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(0x21);//设置列地址的起始和结束的位置
    OLED_SendCmd(0x00);//0
    OLED_SendCmd(0x7f);//127   
    OLED_SendCmd(0x22);//设置页地址的起始和结束的位置
    OLED_SendCmd(0x00);//0
    OLED_SendCmd(0x07);//7
	OLED_SendCmd(0xc9);//0xc9上下反置 0xc8正常
    OLED_SendCmd(0xa1);//0xa0左右反置 0xa1正常
	
	OLED_SendCmd(0xa4);//全局显示开启;0xa4正常,0xa5无视命令点亮全屏
	OLED_SendCmd(0xa6);//设置显示方式;bit0:1,反相显示;0,正常显示	
	OLED_SendCmd(0xaf);//开启显示

   OLED_SendCmd(0x56);
   DMA_OLED_Init();//DMA初始化
}

六.写入文字

通过之前的配置,STM32已经能够写入OLED的显存啦!
因为此款OLED没有自带字库,所以在STM32中写入字库

const u8 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
    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
};

下面是我自己写的OLED_Write的函数,调用即可
OLED_Write(页,列,文字,大小) 暂时只支持S6X8,S8X16大小暂不可用

void OLED_Write(u8 ye,u8 lie,u8* ascii,u8 size)
{
	u8 i=0,j=0,c=*ascii;
	if (size==S6X8)
	{
		for (i=0;i<6;i++)
		{
			OLED_SRAM[ye][lie+i]=F6X8[(c-32)*6+1+i];
		}
	}
	else if(size==S8X16)
	{
		for (j=0;j<2;j++)
		{
			for (i=0;i<8;i++)
			{
				OLED_SRAM[ye+j][lie+i]=F8X16[(c-32)*8+1+i];
			}
		}
	}

7.main主函数和测试

在[0,1]的位置显示A
在[1,1]的位置显示B
在[3,50]的位置显示C

#include "oled_spi_dma.h"
#include "delay.h"
int main()
{
	uint8_t OLED_SRAM[8][128]; //图像储存在SRAM里
	delay_init(); //delay函数不初始化要出大问题!!!!!!!!!
	OLED_Init();
	OLED_Write(0,1,"A",0);
	OLED_Write(2,1,"B",0);
	OLED_Write(3,50,"C",0);
}

STM32 7针0.96寸OLED显示屏(硬件SPI+DMA)无需内核响应 超高刷新率!_第10张图片
成功!

你可能感兴趣的:(STM32,嵌入式,stm32,单片机,dma,spi)