Connexion de l'écran OLED ssd1306 à STM32 (SPI + DMA)

Cet article décrit le processus de connexion d'un écran OLED avec un contrôleur ssd1306 avec une résolution de 128 x 64 à un microcontrôleur stm32f103C8T6 via l'interface SPI. Je voulais également atteindre le taux de rafraîchissement maximum de l'affichage, il est donc conseillé d'utiliser DMA et de programmer le microcontrôleur à l'aide de la bibliothèque CMSIS.



Lien



Nous allons connecter l'écran au microcontrôleur via l'interface SPI1 comme suit:



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


imageimage



La transmission de données se produit sur le front montant du signal de synchronisation à 1 octet par trame. Les lignes SCK et SDA sont utilisées pour transférer des données via l'interface SPI, RES - redémarre le contrôleur d'affichage à un niveau logique bas, CS est responsable de la sélection d'un appareil sur le bus SPI à un niveau logique bas, DS détermine le type de données (commande - 1 / données - 0) qui sont transmises afficher. Comme rien ne peut être lu sur l'écran, nous n'utiliserons pas la sortie MISO.



Organisation de la mémoire du contrôleur d'affichage



Avant d'afficher quoi que ce soit à l'écran, vous devez comprendre comment la mémoire est organisée dans le contrôleur ssd1306.



image

image



Toute la mémoire graphique (GDDRAM) est une zone de 128 * 64 = 8192 bits = 1 Ko. La zone est divisée en 8 pages, qui sont présentées comme une collection de 128 segments de 8 bits. La mémoire est adressée par numéro de page et numéro de segment, respectivement.



Avec cette méthode d'adressage, il y a une caractéristique très désagréable - l'impossibilité d'écrire 1 bit d'information en mémoire, puisque l'enregistrement se produit dans un segment (8 bits chacun). Et comme pour l'affichage correct d'un seul pixel à l'écran, vous devez connaître l'état des pixels restants dans le segment, il est conseillé de créer un tampon de 1 Ko dans la mémoire du microcontrôleur et de le charger cycliquement dans la mémoire d'affichage (c'est là que DMA est utile), respectivement, en effectuant sa mise à jour complète. En utilisant cette méthode, il est possible de recalculer la position de chaque bit en mémoire aux coordonnées classiques x, y. Ensuite, pour afficher un point avec les coordonnées x et y, nous utiliserons la méthode suivante:



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


Et pour effacer le point



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




Configuration SPI



Comme mentionné ci-dessus, nous allons connecter l'écran à SPI1 du microcontrôleur STM32F103C8.



image



Pour faciliter l'écriture du code, nous déclarerons certaines constantes et créerons une fonction pour initialiser le 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;
}


Activez la synchronisation et configurez les sorties GPIO, comme indiqué dans le tableau ci-dessus.




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


Ensuite, configurons SPI en mode maître et une fréquence 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


Configurons 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


Ensuite, nous écrirons une fonction d'envoi de données via SPI (jusqu'à présent sans DMA). Le processus d'échange de données est le suivant:



  1. En attente de la sortie de SPI
  2. CS = 0
  3. Envoi de données
  4. CS = 1



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


Nous écrirons également une fonction pour envoyer directement une commande à l'écran (Nous commutons la ligne DC uniquement lors de la transmission d'une commande, puis la remettons à l'état "données", car nous ne transmettrons pas de commandes si souvent et ne perdrons pas de performances).



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


Ensuite, nous traiterons des fonctions permettant de travailler directement avec DMA, pour cela nous déclarerons un tampon dans la mémoire du microcontrôleur et créerons des fonctions pour démarrer et arrêter l'envoi cyclique de ce tampon vers la mémoire écran.



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


Initialisation de l'écran et sortie des données



Créons maintenant une fonction pour initialiser l'écran lui-même.



void ssd1306Init()
{

}


Commençons par configurer CS, RESET et DC line, et également réinitialiser le contrôleur d'affichage.



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


Ensuite, nous enverrons une séquence de commandes pour l'initialisation (vous pouvez en savoir plus à leur sujet dans la documentation du contrôleur 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


Créons des fonctions pour remplir tout l'écran avec la couleur sélectionnée et afficher un pixel.



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


Ensuite, dans le corps du programme principal, nous initialisons le SPI et l'affichage.



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


La fonction RccClockInit () est destinée à régler l'horloge du microcontrôleur.



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




Remplissez tout l'écran de blanc et voyez le résultat.



ssd1306RunDisplayUPD();
ssd1306FillDisplay(WHITE);


image



Dessinons sur l'écran dans une grille par incréments de 10 pixels.



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


image



Les fonctions fonctionnent correctement, le tampon est écrit en permanence dans la mémoire du contrôleur d'affichage, ce qui permet d'utiliser le système de coordonnées cartésien lors de l'affichage des primitives graphiques.



Afficher le taux de rafraîchissement



Puisque le tampon est envoyé de manière cyclique à la mémoire d'affichage, il suffira de connaître le temps nécessaire au DMA pour transférer les données vers une estimation approximative du taux de rafraîchissement de l'affichage. Pour le débogage en temps réel, nous utiliserons la bibliothèque EventRecorder de Keil.



Afin de connaître le moment de la fin du transfert de données, nous allons configurer l'interruption DMA à la fin du transfert.



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


Nous suivrons l'intervalle de temps à l'aide des fonctions EventStart et EventStop.



image



Nous obtenons 0,00400881-0,00377114 = 0,00012767 s, ce qui correspond à un taux de rafraîchissement de 4,2 KHz. En fait, la fréquence n'est pas si élevée, ce qui est dû à l'imprécision de la méthode de mesure, mais clairement supérieure à la norme 60 Hz.



Liens






All Articles