ssd1306 OLEDディスプレイをSTM32(SPI + DMA)に接続する

この記事では、解像度128x64のssd1306コントローラーを備えたoledディスプレイをSPIインターフェイスを介してstm32f103C8T6マイクロコンピューターに接続するプロセスについて説明します。また、最大の表示リフレッシュレートを達成したかったので、DMAを使用し、CMSISライブラリを使用してマイクロコンピュータをプログラムすることをお勧めします。



接続



次のように、SPI1インターフェイスを介してディスプレイをマイクロコントローラに接続します。



  • VDD-> + 3.3V
  • GND->アース
  • SCK-> PA5
  • SDA-> PA7(MOSI)
  • RES-> PA1
  • CS-> PA2
  • DS-> PA3


画像画像



データ送信は、同期信号の立ち上がりエッジで1フレームあたり1バイトで発生します。SCKおよびSDAラインは、SPIインターフェイスを介してデータを転送するために使用されます。RES-低ロジックレベルでディスプレイコントローラを再起動します。CSは低ロジックレベルでSPIバス上のデバイスを選択し、DSは送信されるデータのタイプ(コマンド-1 /データ-0)を決定します。表示。ディスプレイからは何も読み取れないため、MISO出力は使用しません。



ディスプレイコントローラーのメモリ構成



画面に何かを表示する前に、ssd1306コントローラーでメモリがどのように構成されているかを理解する必要があります。



画像

画像



すべてのグラフィックメモリ(GDDRAM)は、128 * 64 = 8192ビット= 1KBの領域です。この領域は8ページに分割され、128個の8ビットセグメントのコレクションとして表示されます。メモリは、それぞれページ番号とセグメント番号でアドレス指定されます。



このアドレス指定方法には、非常に不快な機能があります。記録はセグメント(各8ビット)で行われるため、1ビットの情報をメモリに書き込むことができません。また、画面に1つのピクセルを正しく表示するには、セグメント内の残りのピクセルの状態を知る必要があるため、マイクロコンピューターのメモリに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をマスターモードと18MHzの周波数に構成しましょう。



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


画面全体を選択した色で塗りつぶし、1ピクセルを表示する関数を作成しましょう。



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.2KHzのリフレッシュレートに対応します。実際、周波数はそれほど高くはありません。これは、測定方法の不正確さによるものですが、明らかに標準の60Hzを超えています。



リンク






All Articles