Conexión de la pantalla OLED ssd1306 a STM32 (SPI + DMA)

Este artículo describirá el proceso de conectar una pantalla OLED con un controlador ssd1306 con una resolución de 128x64 a un microcontrolador stm32f103C8T6 a través de la interfaz SPI. También quería lograr la máxima frecuencia de actualización de la pantalla, por lo que es aconsejable usar DMA y programar el microcontrolador usando la biblioteca CMSIS.



Conexión



Conectaremos la pantalla al microcontrolador a través de la interfaz SPI1 de la siguiente manera:



  • VDD-> + 3,3 V
  • GND-> Tierra
  • SCK -> PA5
  • SDA -> PA7 (MOSI)
  • RES-> PA1
  • CS-> PA2
  • DS-> PA3


imagenimagen



La transmisión de datos se produce en el flanco ascendente de la señal de sincronización a 1 byte por trama. Las líneas SCK y SDA se utilizan para transferir datos a través de la interfaz SPI, RES: reinicia el controlador de pantalla a un nivel lógico bajo, CS es responsable de seleccionar un dispositivo en el bus SPI a un nivel lógico bajo, DS determina el tipo de datos (comando - 1 / datos - 0) que se transmiten monitor. Dado que no se puede leer nada en la pantalla, no usaremos la salida MISO.



Organización de la memoria del controlador de pantalla



Antes de mostrar algo en la pantalla, debe comprender cómo está organizada la memoria en el controlador ssd1306.



imagen

imagen



Toda la memoria de gráficos (GDDRAM) es un área de 128 * 64 = 8192 bits = 1KB. El área está dividida en 8 páginas, que se presentan como una colección de 128 segmentos de 8 bits. La memoria se direcciona por número de página y número de segmento, respectivamente.



Con este método de direccionamiento, existe una característica muy desagradable: la imposibilidad de escribir 1 bit de información en la memoria, ya que la grabación ocurre en un segmento (8 bits cada uno). Y dado que para la correcta visualización de un solo píxel en la pantalla, es necesario conocer el estado de los píxeles restantes en el segmento, es recomendable crear un búfer de 1 KB en la memoria del microcontrolador y cargarlo cíclicamente en la memoria de visualización (aquí es donde DMA es útil), respectivamente, haciendo su actualización completa. Usando este método, es posible recalcular la posición de cada bit en la memoria a las coordenadas clásicas x, y. Luego, para mostrar un punto con coordenadas xey, usaremos el siguiente método:



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


Y para borrar el punto



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




Configuración de SPI



Como se mencionó anteriormente, conectaremos la pantalla a SPI1 del microcontrolador STM32F103C8.



imagen



Para la conveniencia de escribir código, declararemos algunas constantes y crearemos una función para inicializar el 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;
}


Encienda el reloj y configure las salidas GPIO, como se muestra en la tabla anterior.




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


A continuación, configuremos SPI en modo maestro y una frecuencia de 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


Configuremos 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


A continuación, escribiremos una función para enviar datos a través de SPI (hasta ahora sin DMA). El proceso de intercambio de datos es el siguiente:



  1. Esperando que se publique SPI
  2. CS = 0
  3. Enviando datos
  4. CS = 1



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


También escribiremos una función para enviar directamente un comando a la pantalla (Cambiamos la línea DC solo cuando transmitimos un comando, y luego lo devolvemos al estado de "datos", ya que no transmitiremos comandos con tanta frecuencia y no perderemos rendimiento).



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


A continuación, nos ocuparemos de las funciones para trabajar directamente con DMA, para ello declararemos un búfer en la memoria del microcontrolador y crearemos funciones para iniciar y detener el envío cíclico de este búfer a la memoria de pantalla.



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;//  
}


Inicialización de pantalla y salida de datos



Ahora creemos una función para inicializar la propia pantalla.



void ssd1306Init()
{

}


Primero, configuremos CS, RESET y DC line, y también reiniciemos el controlador de pantalla.



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;//   


A continuación, enviaremos una secuencia de comandos para la inicialización (puede obtener más información sobre ellos en la documentación del controlador 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


Creemos funciones para llenar toda la pantalla con el color seleccionado y mostrar un píxel.



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


A continuación, en el cuerpo del programa principal, inicializamos el SPI y la pantalla.



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


La función RccClockInit () está destinada a configurar el reloj del microcontrolador.



Código 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;
}




Llene toda la pantalla con blanco y vea el resultado.



ssd1306RunDisplayUPD();
ssd1306FillDisplay(WHITE);


imagen



Dibujemos en la pantalla en una cuadrícula en incrementos de 10 píxeles.



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);
	}
}


imagen



Las funciones funcionan correctamente, el búfer se escribe continuamente en la memoria del controlador de pantalla, lo que permite utilizar el sistema de coordenadas cartesianas al mostrar primitivas gráficas.



Mostrar frecuencia de actualización



Dado que el búfer se envía cíclicamente a la memoria de la pantalla, será suficiente saber el tiempo que tarda el DMA en completar la transferencia de datos para estimar la frecuencia de actualización de la pantalla. Para la depuración en tiempo real, usaremos la biblioteca EventRecorder de Keil.



Para conocer el momento del final de la transferencia de datos, configuramos la interrupción DMA para finalizar la transferencia.



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


Realizaremos un seguimiento del intervalo de tiempo utilizando las funciones EventStart y EventStop.



imagen



Obtenemos 0.00400881-0.00377114 = 0.00012767 seg, que corresponde a una frecuencia de actualización de 4.2 KHz. De hecho, la frecuencia no es tan alta, lo que se debe a la inexactitud del método de medición, pero claramente más que el estándar de 60 Hz.



Enlaces






All Articles