I2C接続のOLEDと同様にSPI接続のOLEDも表示させてみました。
OLEDのコントローラーはSH1106になります。
SPI信号は
SPI1_NSS
SPI1_SCK
SPI1_MOSI
の3本を使用しました。
データシートより各信号のピン番号を調べると
PA4
PA5
PA7
に割り当たっています。
https://www.st.com/ja/microcontrollers-microprocessors/stm32f446re.html
また、ボード(NUCLEO-F446RE)上の端子位置はこちらのデータシートより調べます。
https://www.stmcu.jp/design/hwdevelop/nucleo/51836/
OLEDへは送信のみなので、それほど複雑な設定は必要無いのですが、データとクロックエッジの関係は適切に設定する必要がありました。
SH1106のデータシートによると、クロック(SCL)とデータ(SI)の関係はこのように記載されています。
RM0390 - Reference manualによりますとSPI_CR1レジスタの「CPHA」と「CPOL」の組み合わせで4パターンのタイミングを作ることができますので、どちらも「1」を設定することでSH1106向けのタイミングとなります。
また、SCK周波数はSPIが接続されているAPB2が90MHzなのでSPI_CR1レジスタのBRへ0x7を設定することで256分周で約350KHzとしています。
使用したSPL関数は、データ送信のSPI_I2S_SendDataと、ステータスリードのSPI_I2S_GetFlagStatusの2つのみです。
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
ほか、OLEDに対するDCとRESは
PB4 (DC)
PB5 (RES)
を使用しました。
※ 後で気が付いたのですが、PB4はデバッガのリセットとしても使用されているのでブレークやステップ実行を行う際はPB6などに変更した方が良いです。
以上の設定で、いつもの馬の表示プログラムで組み上げてみました。
#include "stm32f4xx.h"
#include "stm32f4xx_spi.h"
extern unsigned char oled_data[8][1024];
int main(void){
RCC->AHB1ENR |= (RCC_AHB1ENR_GPIOBEN | RCC_AHB1ENR_GPIOAEN);
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
GPIOB->MODER = ((0x1 << 10) | (0x1 << 8));
GPIOB->PUPDR = ((0x0 << 10) | (0x0 << 8));
GPIOB->OTYPER = ((0x0 << 5) | (0x0 << 4));
GPIOA->MODER |= ((0x2<<14)|(0x2<<10)|(0x2<< 8));
GPIOA->PUPDR |= ((0x0<<14)|(0x0<<10)|(0x0<< 8));
GPIOA->OTYPER |= ((0x0<< 7)|(0x0<< 5)|(0x0<< 4));
GPIOA->AFR[0] |= ((0x5<<28)|(0x5<<20)|(0x5<<16));
SPI1->I2SCFGR = ((0x0 << 12) & SPI_I2SCFGR_ASTRTEN) |
((0x0 << 11) & SPI_I2SCFGR_I2SMOD) |
((0x0 << 10) & SPI_I2SCFGR_I2SE) |
((0x0 << 9) & SPI_I2SCFGR_I2SCFG) |
((0x0 << 7) & SPI_I2SCFGR_PCMSYNC) |
((0x0 << 4) & SPI_I2SCFGR_I2SSTD) |
((0x0 << 3) & SPI_I2SCFGR_CKPOL) |
((0x0 << 1) & SPI_I2SCFGR_DATLEN) |
((0x0 << 0) & SPI_I2SCFGR_CHLEN);
SPI1->CR2 = ((0x0 << 7) & SPI_CR2_TXEIE) |
((0x0 << 6) & SPI_CR2_RXNEIE) |
((0x0 << 5) & SPI_CR2_ERRIE) |
((0x0 << 4) & ((uint8_t)0x10)) |
((0x1 << 2) & SPI_CR2_SSOE) |
((0x0 << 1) & SPI_CR2_TXDMAEN) |
((0x0 << 0) & SPI_CR2_RXDMAEN);
SPI1->CR1 = ((0x1 << 15) & SPI_CR1_BIDIMODE) |
((0x1 << 14) & SPI_CR1_BIDIOE) |
((0x0 << 13) & SPI_CR1_CRCEN) |
((0x0 << 12) & SPI_CR1_CRCNEXT) |
((0x0 << 11) & SPI_CR1_DFF) |
((0x0 << 10) & SPI_CR1_RXONLY) |
((0x0 << 9) & SPI_CR1_SSM) |
((0x0 << 8) & SPI_CR1_SSI) |
((0x0 << 7) & SPI_CR1_LSBFIRST) |
((0x1 << 6) & SPI_CR1_SPE) |
((0x7 << 3) & SPI_CR1_BR) |
((0x1 << 2) & SPI_CR1_MSTR) |
((0x1 << 1) & SPI_CR1_CPOL) |
((0x1 << 0) & SPI_CR1_CPHA);
GPIOB->ODR |= (0x1 << 5); for(int i=0; i<200; i++){ asm("nop"); }
GPIOB->ODR &= ~(0x1 << 5); for(int i=0; i<200; i++){ asm("nop"); }
GPIOB->ODR |= (0x1 << 5); for(int i=0; i<200; i++){ asm("nop"); }
GPIOB->ODR &= ~(0x1 << 4);
SPI_I2S_SendData(SPI1, 0xaf); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY));
while(1){
for(int j=0; j<8; j++){
for(int p=0; p<8; p++){
GPIOB->ODR &= ~(0x1 << 4);
SPI_I2S_SendData(SPI1, 0x10); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY));
SPI_I2S_SendData(SPI1, 0x02); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY));
SPI_I2S_SendData(SPI1, (0xb0 | (uint16_t)(p))); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY));
GPIOB->ODR |= (0x1 << 4);
for(int i=0; i<128; i++){
SPI_I2S_SendData(SPI1, oled_data[j][128 * p + i]);
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY));
}
}
}
}
return(0);
}
問題なく動きました。
ついでに先日購入した激安USBロジアナで波形を観測してみました。
SCKも約350KHzで想定通りの周波数になっています。
最後に、今までSPI OLEDのリセットは適当な時間でアサートしていましたが、この機会にきちんと調べてみました。
SH1106のデータシートによると、アサート時間10usec以上、デアサート時間2usec以上と記載されています。
プログラムではnopを200回入れて、アサートとデアサート時間を作っています。
GPIOB->ODR &= ~(0x1 << 5); for(int i=0; i<200; i++){ asm("nop"); }
GPIOB->ODR |= (0x1 << 5); for(int i=0; i<200; i++){ asm("nop"); }
ロジアナで調べると14.5usecあるので問題なさそうです。
[参考記事]