Anschließen des ssd1306 OLED-Displays an STM32 (SPI + DMA)

Dieser Artikel beschreibt den Vorgang des Verbindens eines alten Displays mit einem ssd1306-Controller mit einer Auflösung von 128 x 64 über die SPI-Schnittstelle an einen stm32f103C8T6-Mikrocontroller. Ich wollte auch die maximale Aktualisierungsrate der Anzeige erreichen, daher ist es ratsam, DMA zu verwenden und den Mikrocontroller mithilfe der CMSIS-Bibliothek zu programmieren.



Verbindung



Wir werden das Display wie folgt über die SPI1-Schnittstelle mit dem Mikrocontroller verbinden:



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


BildBild



Die Datenübertragung erfolgt an der ansteigenden Flanke des Synchronisationssignals mit 1 Byte pro Frame. Die SCK- und SDA-Leitungen werden verwendet, um Daten über die SPI-Schnittstelle zu übertragen. RES - Startet den Display-Controller auf einem niedrigen Logikpegel neu. CS ist für die Auswahl eines Geräts auf dem SPI-Bus auf einem niedrigen Logikpegel verantwortlich. DS bestimmt den Datentyp (Befehl - 1 / Daten - 0), der übertragen wird Anzeige. Da nichts vom Display gelesen werden kann, wird der MISO-Ausgang nicht verwendet.



Organisation des Controller-Speichers anzeigen



Bevor Sie etwas auf dem Bildschirm anzeigen, müssen Sie wissen, wie der Speicher im ssd1306-Controller organisiert ist.



Bild

Bild



Der gesamte Grafikspeicher (GDDRAM) ist ein Bereich von 128 * 64 = 8192 Bit = 1 KB. Der Bereich ist in 8 Seiten unterteilt, die als Sammlung von 128 8-Bit-Segmenten dargestellt werden. Der Speicher wird durch die Seitenzahl bzw. die Segmentnummer adressiert.



Bei dieser Adressierungsmethode gibt es ein sehr unangenehmes Merkmal - die Unmöglichkeit, 1 Bit Information in den Speicher zu schreiben, da die Aufzeichnung in einem Segment (jeweils 8 Bit) erfolgt. Und da Sie für die korrekte Anzeige eines einzelnen Pixels auf dem Bildschirm den Status der verbleibenden Pixel im Segment kennen müssen, ist es ratsam, einen 1-KB-Puffer im Speicher des Mikrocontrollers zu erstellen und diesen zyklisch in den Anzeigespeicher zu laden (hier ist DMA hilfreich), um die vollständige Aktualisierung vorzunehmen. Mit dieser Methode ist es möglich, die Position jedes Bits im Speicher auf die klassischen Koordinaten x, y neu zu berechnen. Um dann einen Punkt mit den Koordinaten x und y anzuzeigen, verwenden wir die folgende Methode:



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


Und um den Punkt zu löschen



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




SPI-Setup



Wie oben erwähnt, verbinden wir das Display mit SPI1 des Mikrocontrollers STM32F103C8.



Bild



Um das Schreiben von Code zu vereinfachen, deklarieren wir einige Konstanten und erstellen eine Funktion zum Initialisieren des 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;
}


Schalten Sie die Taktung ein und konfigurieren Sie die GPIO-Ausgänge wie in der obigen Tabelle gezeigt.




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


Als nächstes konfigurieren wir SPI für den Master-Modus und eine Frequenz von 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


Lassen Sie uns DMA einrichten.



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


Als nächstes schreiben wir eine Funktion zum Senden von Daten über SPI (bisher ohne DMA). Der Datenaustauschprozess ist wie folgt:



  1. Warten auf die Veröffentlichung von SPI
  2. CS = 0
  3. Daten senden
  4. CS = 1



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


Wir werden auch eine Funktion zum direkten Senden eines Befehls an den Bildschirm schreiben (Wir schalten die DC-Leitung nur beim Senden eines Befehls um und bringen sie dann in den Status "Daten" zurück, da wir Befehle nicht so oft übertragen und die Leistung nicht verlieren).



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


Als nächstes werden wir uns mit Funktionen für die direkte Arbeit mit DMA befassen. Dazu deklarieren wir einen Puffer im Mikrocontrollerspeicher und erstellen Funktionen zum Starten und Stoppen des zyklischen Sendens dieses Puffers an den Bildschirmspeicher.



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


Bildschirminitialisierung und Datenausgabe



Erstellen wir nun eine Funktion zum Initialisieren des Bildschirms.



void ssd1306Init()
{

}


Zuerst konfigurieren wir CS-, RESET- und DC-Leitung und setzen auch den Display-Controller zurück.



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


Als Nächstes senden wir eine Folge von Befehlen zur Initialisierung (Weitere Informationen hierzu finden Sie in der Dokumentation zum ssd1306-Controller).



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


Erstellen wir Funktionen, um den gesamten Bildschirm mit der ausgewählten Farbe zu füllen und ein Pixel anzuzeigen.



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


Als nächstes initialisieren wir im Hauptprogramm den SPI und die Anzeige.



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


Die Funktion RccClockInit () dient zum Einstellen der Uhr des Mikrocontrollers.



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




Füllen Sie die gesamte Anzeige mit Weiß und sehen Sie das Ergebnis.



ssd1306RunDisplayUPD();
ssd1306FillDisplay(WHITE);


Bild



Zeichnen wir in 10-Pixel-Schritten in einem Raster auf dem Bildschirm.



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


Bild



Die Funktionen funktionieren ordnungsgemäß, der Puffer wird kontinuierlich in den Speicher der Anzeigesteuerung geschrieben, wodurch die Verwendung des kartesischen Koordinatensystems bei der Anzeige von Grafikprimitiven ermöglicht wird.



Aktualisierungsrate anzeigen



Da der Puffer zyklisch an den Anzeigespeicher gesendet wird, reicht es aus, die Zeit zu kennen, die der DMA benötigt, um die Daten auf eine grobe Schätzung der Anzeigeaktualisierungsrate zu übertragen. Für das Debuggen in Echtzeit verwenden wir die EventRecorder-Bibliothek von Keil.



Um den Zeitpunkt des Endes der Datenübertragung herauszufinden, konfigurieren wir den DMA-Interrupt so, dass die Übertragung beendet wird.



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


Wir werden das Zeitintervall mit den Funktionen EventStart und EventStop verfolgen.



Bild



Wir erhalten 0,00400881-0,00377114 = 0,00012767 Sekunden, was einer Bildwiederholfrequenz von 4,2 kHz entspricht. Tatsächlich ist die Frequenz nicht so hoch, was auf die Ungenauigkeit der Messmethode zurückzuführen ist, sondern deutlich höher als die Standardfrequenz von 60 Hz.



Links






All Articles