将ssd1306 OLED显示器连接到STM32(SPI + DMA)

本文将介绍通过SPI接口将分辨率为128x64的ssd1306控制器的oled显示器连接到stm32f103C8T6微控制器的过程。我还想获得最大的显示刷新率,因此建议使用DMA,并使用CMSIS库对微控制器进行编程。



连接



我们将通过SPI1接口将显示器连接到微控制器,如下所示:



  • VDD-> + 3.3伏
  • GND->地
  • SCK-> PA5
  • SDA-> PA7(MOSI)
  • RES-> PA1
  • CS-> PA2
  • DS-> PA3


图片图片



数据传输发生在同步信号的上升沿,每帧1字节。SCK和SDA线用于通过SPI接口传输数据,RES-以低逻辑电平重新启动显示控制器,CS负责以低逻辑电平选择SPI总线上的设备,DS确定要传输的数据类型(命令-1 /数据-0)显示。由于无法从显示屏上读取任何内容,因此我们将不使用MISO输出。



显示控制器内存组织



在屏幕上显示任何内容之前,您需要了解ssd1306控制器中内存的组织方式。



图片

图片



所有图形内存(GDDRAM)的面积为128 * 64 = 8192位= 1KB。该区域分为8页,显示为128个8位段的集合。内存分别通过页码和段号寻址。



使用这种寻址方法,有一个非常不愉快的功能-无法将1位信息写入内存,因为记录是在一个段(每个8位)中进行的。由于要在屏幕上正确显示单个像素,有必要知道该段中其余像素的状态,因此建议在微控制器的存储器中创建一个1 KB的缓冲区,然后将其循环加载到显示存储器中(这是DMA派上用场的地方),以进行完整的更新。使用此方法,可以将内存中每个位的位置重新计算为经典坐标x,y。然后,要显示坐标为x和y的点,我们将使用以下方法:



displayBuff[x+(y/8)*SSD1306_WIDTH]|=(1<<(y%8));


并且为了消除重点



displayBuff[x+(y/8)*SSD1306_WIDTH]&=~(1<<(y%8));




SPI设置



如上所述,我们将显示器连接到STM32F103C8微控制器的SPI1。



图片



为了方便编写代码,我们将声明一些常量并创建一个函数来初始化SPI。



#define SSD1306_WIDTH 128
#define SSD1306_HEIGHT 64
#define BUFFER_SIZE 1024
//     ,     /
#define CS_SET GPIOA->BSRR|=GPIO_BSRR_BS2
#define CS_RES GPIOA->BSRR|=GPIO_BSRR_BR2
#define RESET_SET GPIOA->BSRR|=GPIO_BSRR_BS1
#define RESET_RES GPIOA->BSRR|=GPIO_BSRR_BR1
#define DATA GPIOA->BSRR|=GPIO_BSRR_BS3
#define COMMAND GPIOA->BSRR|=GPIO_BSRR_BR3

void spi1Init()
{
    return;
}


如上表所示,打开时钟并配置GPIO输出。




RCC->APB2ENR|=RCC_APB2ENR_SPI1EN | RCC_APB2ENR_IOPAEN;//  SPI1  GPIOA
RCC->AHBENR|=RCC_AHBENR_DMA1EN;//  DMA
GPIOA->CRL|= GPIO_CRL_MODE5 | GPIO_CRL_MODE7;//PA4,PA5,PA7    50MHz
GPIOA->CRL&= ~(GPIO_CRL_CNF5 | GPIO_CRL_CNF7);
GPIOA->CRL|=  GPIO_CRL_CNF5_1 | GPIO_CRL_CNF7_1;//PA5,PA7 -     push-pull, PA4 -  push-pull


接下来,让我们将SPI配置为主模式,频率为18 MHz。



SPI1->CR1|=SPI_CR1_MSTR;// 
SPI1->CR1|= (0x00 & SPI_CR1_BR);//   2
SPI1->CR1|=SPI_CR1_SSM;// NSS
SPI1->CR1|=SPI_CR1_SSI;//NSS - high
SPI1->CR2|=SPI_CR2_TXDMAEN;//  DMA
SPI1->CR1|=SPI_CR1_SPE;// SPI1


让我们设置DMA。



DMA1_Channel3->CCR|=DMA_CCR1_PSIZE_0;//  1
DMA1_Channel3->CCR|=DMA_CCR1_DIR;// DMA    
DMA1_Channel3->CCR|=DMA_CCR1_MINC;//  
DMA1_Channel3->CCR|=DMA_CCR1_PL;//  DMA


接下来,我们将编写一个用于通过SPI发送数据的功能(到目前为止还没有DMA)。数据交换过程如下:



  1. 等待SPI被释放
  2. CS = 0
  3. 传送资料
  4. CS = 1



void spiTransmit(uint8_t data)
{
	CS_RES;	
	SPI1->DR = data;
	while((SPI1->SR & SPI_SR_BSY))
	{};
	CS_SET;
}


我们还将编写一个直接将命令发送到屏幕的功能(我们仅在发送命令时才切换DC线,然后将其返回到“数据”状态,因为我们不会如此频繁地发送命令并且不会失去性能)。



void ssd1306SendCommand(uint8_t command)
{
	COMMAND;
	spiTransmit(command);
	DATA;
}


接下来,我们将处理直接与DMA一起使用的函数,为此,我们将在微控制器存储器中声明一个缓冲区,并创建用于启动和停止将该缓冲区循环发送到屏幕存储器的函数。



static uint8_t displayBuff[BUFFER_SIZE];// 

void ssd1306RunDisplayUPD()
{
	DATA;
	DMA1_Channel3->CCR&=~(DMA_CCR1_EN);// DMA
	DMA1_Channel3->CPAR=(uint32_t)(&SPI1->DR);//  DMA    SPI1
	DMA1_Channel3->CMAR=(uint32_t)&displayBuff;// 
	DMA1_Channel3->CNDTR=sizeof(displayBuff);// 
	DMA1->IFCR&=~(DMA_IFCR_CGIF3);
	CS_RES;//   
	DMA1_Channel3->CCR|=DMA_CCR1_CIRC;//  DMA
	DMA1_Channel3->CCR|=DMA_CCR1_EN;// DMA
}

void ssd1306StopDispayUPD()
{
	CS_SET;//   
	DMA1_Channel3->CCR&=~(DMA_CCR1_EN);// DMA
	DMA1_Channel3->CCR&=~DMA_CCR1_CIRC;//  
}


屏幕初始化和数据输出



现在让我们创建一个函数来初始化屏幕本身。



void ssd1306Init()
{

}


首先,让我们设置CS,RESET和DC线,然后重置显示控制器。



uint16_t i;
GPIOA->CRL|= GPIO_CRL_MODE2 |GPIO_CRL_MODE1 | GPIO_CRL_MODE3;
GPIOA->CRL&= ~(GPIO_CRL_CNF1 | GPIO_CRL_CNF2 | GPIO_CRL_CNF3);//PA1,PA2,PA3   
//    
RESET_RES;
for(i=0;i<BUFFER_SIZE;i++)
{
	displayBuff[i]=0;
}
RESET_SET;
CS_SET;//   


接下来,我们将发送一系列用于初始化的命令(您可以在ssd1306控制器的文档中了解有关它们的更多信息)。



ssd1306SendCommand(0xAE); //display off
ssd1306SendCommand(0xD5); //Set Memory Addressing Mode
ssd1306SendCommand(0x80); //00,Horizontal Addressing Mode;01,Vertical
ssd1306SendCommand(0xA8); //Set Page Start Address for Page Addressing
ssd1306SendCommand(0x3F); //Set COM Output Scan Direction
ssd1306SendCommand(0xD3); //set low column address
ssd1306SendCommand(0x00); //set high column address
ssd1306SendCommand(0x40); //set start line address
ssd1306SendCommand(0x8D); //set contrast control register
ssd1306SendCommand(0x14);
ssd1306SendCommand(0x20); //set segment re-map 0 to 127
ssd1306SendCommand(0x00); //set normal display
ssd1306SendCommand(0xA1); //set multiplex ratio(1 to 64)
ssd1306SendCommand(0xC8); //
ssd1306SendCommand(0xDA); //0xa4,Output follows RAM
ssd1306SendCommand(0x12); //set display offset
ssd1306SendCommand(0x81); //not offset
ssd1306SendCommand(0x8F); //set display clock divide ratio/oscillator frequency
ssd1306SendCommand(0xD9); //set divide ratio
ssd1306SendCommand(0xF1); //set pre-charge period
ssd1306SendCommand(0xDB); 
ssd1306SendCommand(0x40); //set com pins hardware configuration
ssd1306SendCommand(0xA4);
ssd1306SendCommand(0xA6); //set vcomh
ssd1306SendCommand(0xAF); //0x20,0.77xVcc


让我们创建一些函数,用选定的颜色填充整个屏幕并显示一个像素。



typedef enum COLOR
{
	BLACK,
	WHITE
}COLOR;

void ssd1306DrawPixel(uint16_t x, uint16_t y,COLOR color){
	if(x<SSD1306_WIDTH && y <SSD1306_HEIGHT && x>=0 && y>=0)
	{
		if(color==WHITE)
		{
			displayBuff[x+(y/8)*SSD1306_WIDTH]|=(1<<(y%8));
		}
		else if(color==BLACK)
		{
			displayBuff[x+(y/8)*SSD1306_WIDTH]&=~(1<<(y%8));
		}
	}
}

void ssd1306FillDisplay(COLOR color)
{
	uint16_t i;
	for(i=0;i<SSD1306_HEIGHT*SSD1306_WIDTH;i++)
	{
		if(color==WHITE)
			displayBuff[i]=0xFF;
		else if(color==BLACK)
			displayBuff[i]=0;
	}
}


接下来,在主程序的主体中,我们初始化SPI和显示。



RccClockInit();
spi1Init();
ssd1306Init();


RccClockInit()函数旨在调整微控制器的时钟。



RccClockInit代码
int RccClockInit()
{
	//Enable HSE
	//Setting PLL
	//Enable PLL
	//Setting count wait cycles of FLASH
	//Setting AHB1,AHB2 prescaler
	//Switch to PLL	
	uint16_t timeDelay;
	RCC->CR|=RCC_CR_HSEON;//Enable HSE
	for(timeDelay=0;;timeDelay++)
	{
		if(RCC->CR&RCC_CR_HSERDY) break;
		if(timeDelay>0x1000)
		{
			RCC->CR&=~RCC_CR_HSEON;
			return 1;
		}
	}	
	RCC->CFGR|=RCC_CFGR_PLLMULL9;//PLL x9
	RCC->CFGR|=RCC_CFGR_PLLSRC_HSE;//PLL sourse:HSE
	RCC->CR|=RCC_CR_PLLON;//Enable PLL
	for(timeDelay=0;;timeDelay++)
	{
		if(RCC->CR&RCC_CR_PLLRDY) break;
		if(timeDelay>0x1000)
		{
			RCC->CR&=~RCC_CR_HSEON;
			RCC->CR&=~RCC_CR_PLLON;
			return 2;
		}
	}
	FLASH->ACR|=FLASH_ACR_LATENCY_2;
	RCC->CFGR|=RCC_CFGR_PPRE1_DIV2;//APB1 prescaler=2
	RCC->CFGR|=RCC_CFGR_SW_PLL;//Switch to PLL
	while((RCC->CFGR&RCC_CFGR_SWS)!=(0x02<<2)){}
	RCC->CR&=~RCC_CR_HSION;//Disable HSI
	return 0;
}




用白色填充整个显示屏,然后查看结果。



ssd1306RunDisplayUPD();
ssd1306FillDisplay(WHITE);


图片



让我们以10像素的增量在屏幕上绘制网格。



for(i=0;i<SSD1306_WIDTH;i++)
{
	for(j=0;j<SSD1306_HEIGHT;j++)
	{
		if(j%10==0 || i%10==0)
			ssd1306DrawPixel(i,j,WHITE);
	}
}


图片



功能正常工作,缓冲区连续写入显示控制器的内存,从而在显示图形基元时允许使用笛卡尔坐标系。



显示刷新率



由于缓冲区被周期性地发送到显示存储器,因此足以知道DMA将数据传输到显示刷新率的大致估计所需的时间。对于实时调试,我们将使用Keil的EventRecorder库。



为了找出数据传输结束的时刻,我们将在传输结束时配置DMA中断。



DMA1_Channel3->CCR|=DMA_CCR1_TCIE;//   
DMA1->IFCR&=~DMA_IFCR_CTCIF3;//  
NVIC_EnableIRQ(DMA1_Channel3_IRQn);// 


我们将使用EventStart和EventStop函数跟踪时间间隔。



图片



我们得到0.00400881-0.00377114 = 0.00012767秒,对应于4.2 KHz的刷新速率。实际上,频率不是很高,这是由于测量方法的不准确性所致,但显然比标准的60 Hz高。



链接






All Articles