LSI Jiu-Jitsu

電子工作とブラジリアン柔術

STM32で遊ぶ (7) - I2CマスターWrite

f:id:mohran:20200315233500j:plain

I2Cを動かしてOLEDを表示させることができましたので、制御方法などについてを纏めます。
なお、マスター制御のライトのみですので、リードとスレーブ制御については何も情報ありません・・
(リードはそのうち・・)

I2C_CR1のPE(Bit[0])を1にする前に

I2C_CR2
I2C_CCR
I2C_TRISE

からSCLの周波数を設定します。
I2C_CR2.FREQ[5:0]にはAPB1のクロック周波数を設定し、I2C_CCR.CCR[11:0]と乗算した値がSCL周波数となります。

計算式はリファレンスマニュアルCCRに記載されています。
f:id:mohran:20200318000936j:plain

こちらのクロック構成よりSPLを使った環境では、APB1クロックは45MHzなので、各モードの設定値は下記のようになります。

Sm mode(100KHz)

SCL = CCR × FREQ × 2(Duty 1:1)
CCR = 100,000nsec(100KHz) / 2 / 22.222nsec(45MHz) ≠ 226

I2C1->CR2 = 45;
I2C1->CCR = 226;
Fm mode(400KHz) / DUTY bit=0

CCR × FREQ × 3(Duty 1:2)
CCR = 2,500nsec(400KHz) / 3 / 22.222nsec(45MHz) ≠ 38

I2C1->CR2 = 45;
I2C1->CCR = ((1 << 15) | (0 << 14) | 38);
Fm mode(400KHz) / DUTY bit=1

CCR × FREQ × 25(Duty 9:16)
CCR = 2,500nsec(400KHz) / 25 / 22.222nsec(45MHz) ≠ 5

I2C1->CR2 = 45;
I2C1->CCR = ((1 << 15) | (1 << 14) | 5);

また、I2C_TRISEはSCLのフィードバックループの最大期間を指定するそうです。
全く理解できていませんが(笑)リファレンスマニュアルによればSm modeでは、
 1,000nsec / APB1クロック周波数 + 1
を設定すれば良いようです。
f:id:mohran:20200318001003j:plain

1,000nsec / 22.222nsec(45MHz) + 1 = 46
となりました。
Fm modeでは、最大期間を300nsecで計算すれば良いと海外のフォーラムに書いてありましたので、
300nsec / 22.222nsec(45MHz) + 1 = 14
に設定しました。

PIC + OLEDで使った馬の画像を表示するプログラムを組んでみました。

#include "stm32f4xx_conf.h"

extern unsigned char oled_data[8][1024];

#define FM_MODE

int main(void){
  // Clock Enable
  RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
  RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;

  // PB8 - I2C1_SCL
  // PB9 - I2C1_SDA
  GPIOB->MODER   |= ((0x2 << 18) | (0x2 << 16));  // Alternate function mode
  GPIOB->OTYPER  |= ((0x1 <<  9) | (0x1 <<  8));  // Output open-drain
  GPIOB->PUPDR   |= ((0x1 << 18) | (0x1 << 16));  // Pull-up
  GPIOB->AFR[1]  |= ((0x4 <<  4) | (0x4 <<  0));  // AF4

  // I2C1 Reset
  I2C1->CR1   = (0x1 << 15);
  for(int i=0; i<1000; i++){ asm("NOP"); };
  I2C1->CR1   = (0x0 << 15);

#ifdef FM_MODE
  I2C1->CR2   = 45;
  I2C1->CCR   = ((1 << 15) | (0 << 14) | 38);
  I2C1->TRISE = 14;
#else
  I2C1->CR2   = 45;
  I2C1->CCR   = ((0 << 15) | (0 << 14) | 226);
  I2C1->TRISE = 46;
#endif

  I2C1->CR1 |= 1;

  I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
  I2C_SendData(I2C1, 0x78); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));  // アドレス 0x3c << 1
  I2C_SendData(I2C1, 0x00); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));  // コマンドライト
  I2C_SendData(I2C1, 0xa0); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));  // 表示モード設定 左右 - 通常:0xa0 / 逆転:0xa1
  I2C_SendData(I2C1, 0xc0); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));  // 表示モード設定 上下 - 通常:0xc0 / 逆転:0xc8
  I2C_SendData(I2C1, 0x20); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));  // アドレッシングモード指定
  I2C_SendData(I2C1, 0x00); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));  //   → Horizontalモード
  I2C_SendData(I2C1, 0x21); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));  // カラム指定
  I2C_SendData(I2C1, 0x00); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));  //   → 開始位置(0)
  I2C_SendData(I2C1, 0x7f); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));  //   → 終了位置(127)
  I2C_SendData(I2C1, 0x22); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));  // ページ指定
  I2C_SendData(I2C1, 0x00); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));  //   → 開始ページ(0)
  I2C_SendData(I2C1, 0x07); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));  //   → 終了ページ(7)
  I2C_SendData(I2C1, 0x8d); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));  // チャージポンプ設定
  I2C_SendData(I2C1, 0x14); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));  //   → On
  I2C_SendData(I2C1, 0xaf); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));   // 画面表示 On
  I2C_GenerateSTOP(I2C1, ENABLE);

  for(int h=0; h<100; h++){
    for(int j=0; j<8; j++){
      I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
      I2C_SendData(I2C1, 0x78); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));  // アドレス 0x3c << 1
      I2C_SendData(I2C1, 0x40); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));  // ライトデータ

      for(int i=0; i<1023; i++){
        I2C_SendData(I2C1, oled_data[j][i]); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));
      }
      I2C_SendData(I2C1, oled_data[j][1023]); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
      I2C_GenerateSTOP(I2C1, ENABLE);
    }
  }

  return(0);
}

SPLに用意されている関数を使えば簡単にI2C通信を行うことができました。
送信に使っている関数は

void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);

の3つです。
スタート、アドレス、データの送信完了の確認として

ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);

をコールしています。
引数に指定しているは

I2C_EVENT_MASTER_MODE_SELECT
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED
I2C_EVENT_MASTER_BYTE_TRANSMITTING
I2C_EVENT_MASTER_BYTE_TRANSMITTED

の4つでI2C_SR2とI2C_SR1を組み合わせた32bit値で
 StdPeriph_Driver\inc\stm32f4xx_i2c.h
で定義されています。
stm32f4xx_i2c.hのコメントを見ると、それぞれEV5、EV6、EV8、EV8_2と書かれており
f:id:mohran:20200319001147j:plain
これはリファレンスマニュアルの下記のタイミング図に対応しています。
f:id:mohran:20200318001018j:plain


100KHzと400KHzでそれぞれ動画に撮ってみました。
400KHzは驚くほど速かったです。

[参考記事]