STM32 LLを使ってI2Cでメモリーにアクセスする

  • 2021.01.29
  • LL
STM32 LLを使ってI2Cでメモリーにアクセスする

今回はLL I2Cを使ってメモリー(EEPROM)にアクセスしてみます。

投稿時の開発環境を記しておきます。

PC:Windows10 OS
IDE: STM32CubeIDE Version1.5.0
Configurator: STM32CubeMX Version6.1.0
Board: STM32Nucleo-F401RE

I2C通信の概要

・フィリップス(現NXP)セミコンダクターズが開発した通信方式
・SCLとSDAの2本の通信ラインを使って双方向で通信が可能
・通信速度は100k, 1Mbits/secなどいくつか選択肢がある
・マスタとスレーブが存在し、マルチマスタでの構築も可能

I2Cの詳しい仕様については こちら を見て頂ければと思います。

スレーブとなるターゲット

STM32(をマスタ)で制御し、スレーブとなるデバイスメモリーはマイクロチップ社製の 24LC256 を使ってみました。

EEPROM は Electrically Erased PROM の略で電気的に消去可能な不揮発性メモリーです。

アプリケーションで設定値を保存したい場合などに使うことができます。

データシートは DataSheet 24LC256 を確認してください。

やや雑談になってしまいますが、

24の部分がI2Cインターフェースを表しています。
25だとSPIです。

LCの部分は使用電圧だったり、通信速度だったりするようです。
LCでは 2.5~5.5V ですから、3.3Vで使用可能です。
256はサイズで 256Kbit (32K x 8bit) を表しています。

ピン番号の説明をしておきますと、

1:A0
2:A1
3:A2

1~3番はアドレス指定するピンになっています。
これにより最大8つまでのデバイスを、ひとつのI2Cバスにつなぐこどができます。
今回は1つのデバイスだけをつなぐので、これらを全てGNDにつないでおきます。

4:GND

GNDピンです。

5:SDA

マイコンのSDAをつなぎます。

6:SCL

マイコンのSCLをつなぎます。

7:WP

ハイアクティブのライトプロテクトです。
プロテクトしたら書き込みができないので、GNDにつなぎます。

8:VCC

3.3Vをつなぎます。

接続

下表のとおりに接続します。
SDA と SCL の信号は3.3Vにプルアップしてください。
今回は 10kΩ でプルアップしました。

プロジェクトを作成する

IDEを起動し、File- New – STM32 Project を選択し、Target Selection ウィンドウが出たら Board Selector タブを選択し Boards List から NUCLEO-F401RE を選択し Next ボタンを押します。

Project 名に F401I2cLLMem と入力し、Finishボタンを押します。
Initialize all peripherals with their default Mode ? と聞いてくるので Yesを押します。
This kind of project is associated with the STM32CubeMx perspective. Do you want to open this perspective now ? と聞いてくるので Yesを押します。

ゴンフィグレーターでI2Cインターフェースを追加する

今回使うI2Cの端子はPB8とPB9です。(画像の左上付近の赤枠で囲った部分)

PB8を右クリックして選択し、I2C1_SCL を選択します。
PB9を右クリックして選択し、I2C1_SDA を選択します。

端子の色がグレーから黄色にかわることを確認します。

そして Pinout & Configuration – Categories – Connectivity – I2C1 を選択し、
I2C1 Mode and Configuration – Mode の I2C のリストから I2C を選択します。
パラメーターは初期値のままにしておきます。クロックは100kHzです。

端子の色が黄色から緑色にかわります。

I2Cのレジスタ解説

LLを使ってI2Cを使っていくにはペリフェラルのレジスタについて理解しておく必要があります。

そのために、このボードで使っているマイコンのリファレンスマニュアル RM0368 を こちら からダウンロードしておいてください。

今回は I2C を動かしますので 18項の I2C の部分を良くご覧になってください。

使用するライブラリをHALからLLに変更する

ここまで設定するとI2CでLLを選択することができるようになります。

Project Managerタブを選択し Advanced Settings の I2C を HAL (初期値)から LL に変更します。(コンボボックスで選択する)

HALの部分をクリックするとコンボボックスのリストが表示されるので、LLを選択します。

アクセスのしかた

まず最初に指定するコントロールバイトについて見ていきましょう。
このデバイスの場合、上位4ビットは 1010 のパターンになります。
A2-A0はGNDに接続するので、000 のパターンになります。
素の状態のアドレスは 0x50 ですが、ご存じの通りI2Cでは左に1ビットシフトした状態で使うので
0xa0 を使い、更に最下位のビットは read時には 1、write時には 0 にします。

コントロールバイトは16進表記で以下のようになります。

read時: 0xa1
write時: 0xa0

バイトライト

バイト単位で書き込みを行うフォーマットは以下の通りです。
このデバイスは32KByteなので16ビットアドレスの最上位ビットは意味を持たないために、Don’t cate となっています。

ページライト

ページ単位で書き込みを行うフォーマットは以下の通りです。
1ページは64バイトです。

ランダムリード

ランダムに読むフォーマットは以下の通りです。

シーケンシャルリード

シーケンシャルに読むフォーマットは以下の通りです。

/////////////////////////////////////////////////////////////////////////////// この先を手直しする

コードを追加する

MX_I2C1_Init()関数内で完結するコードを書いてみました。
/* USER CODE BEGIN I2C1_Init 2 */以降の #defineの部分からコードを追加します。

3バイトを書いて、読むだけのものですが I2C は細かく制御する必要があるので手間がかかります。
だらだらっとシーケンスがわかりやすいように書きましたので、I2Cの一連の動作が確認できると思います。

static void MX_I2C1_Init(void)
{
  /* USER CODE BEGIN I2C1_Init 0 */

  /* USER CODE END I2C1_Init 0 */

  LL_I2C_InitTypeDef I2C_InitStruct = {0};

  LL_GPIO_InitTypeDef GPIO_InitStruct = {0};

  LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOB);
  /**I2C1 GPIO Configuration
  PB8   ------> I2C1_SCL
  PB9   ------> I2C1_SDA
  */
  GPIO_InitStruct.Pin = LL_GPIO_PIN_8|LL_GPIO_PIN_9;
  GPIO_InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
  GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
  GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_OPENDRAIN;
  GPIO_InitStruct.Pull = LL_GPIO_PULL_UP;
  GPIO_InitStruct.Alternate = LL_GPIO_AF_4;
  LL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  /* Peripheral clock enable */
  LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_I2C1);

  /* USER CODE BEGIN I2C1_Init 1 */

  /* USER CODE END I2C1_Init 1 */
  /** I2C Initialization
  */
  LL_I2C_DisableOwnAddress2(I2C1);
  LL_I2C_DisableGeneralCall(I2C1);
  LL_I2C_EnableClockStretching(I2C1);
  I2C_InitStruct.PeripheralMode = LL_I2C_MODE_I2C;
  I2C_InitStruct.ClockSpeed = 100000;
  I2C_InitStruct.DutyCycle = LL_I2C_DUTYCYCLE_2;
  I2C_InitStruct.OwnAddress1 = 0;
  I2C_InitStruct.TypeAcknowledge = LL_I2C_ACK;
  I2C_InitStruct.OwnAddrSize = LL_I2C_OWNADDRESS1_7BIT;
  LL_I2C_Init(I2C1, &I2C_InitStruct);
  LL_I2C_SetOwnAddress2(I2C1, 0);
  /* USER CODE BEGIN I2C1_Init 2 */

#define DEVICE_ADDR_FOR_WRITE 0xa0
#define DEVICE_ADDR_FOR_READ 0xa1	// Readを指示するにはLSBを1にする
#define UPPER_ADDR 0x00
#define LOWER_ADDR 0x01

  // まず書き込み

  i2cSetAck(LL_I2C_ACK);	// ACKで応答する指示
  i2cStart();

  i2cAddress(DEVICE_ADDR_FOR_WRITE);

  i2cWrite(UPPER_ADDR);
  i2cWrite(LOWER_ADDR);
  i2cWrite(0x5a);
  i2cWrite(0x39);
  i2cWrite(0xa7);

  i2cStopAfterWriting();

  // 書き込んだアドレス(01番地)から値を読む

  HAL_Delay(5);

  i2cStart();

  i2cAddress(DEVICE_ADDR_FOR_WRITE);

  i2cWrite(UPPER_ADDR);
  i2cWrite(LOWER_ADDR);

  i2cStart();	// 方向をかえる(読む)ためにRestartする

  i2cAddress(DEVICE_ADDR_FOR_READ); // スレーブから読むことを伝える

  uint8_t c1 = i2cRead();
  uint8_t c2 = i2cRead();
  i2cSetAck(LL_I2C_NACK);	// NACKで応答し終了することを伝える
  uint8_t c3 = i2cRead();
  i2cStopAfterReading();

I2Cの処理を関数に分けてみました。

void i2cSetAck(uint32_t ack);
void i2cStart();
void i2cStopAfterWriting();
void i2cStopAfterReading();
void i2cWrite(uint8_t data);
void i2cAddress(uint8_t addr);
uint8_t i2cRead();

void i2cSetAck(uint32_t ack)
{
  LL_I2C_AcknowledgeNextData(I2C1, ack); // ACK or NAK で応答する指示を設定する
}

void i2cStart()
{
  LL_I2C_GenerateStartCondition(I2C1); // Start Condition 発行
  while (!LL_I2C_IsActiveFlag_SB(I2C1))	// 認識するまで待つ
  {
    ;
  }
}

void i2cStopAfterWriting()
{
  while (!LL_I2C_IsActiveFlag_TXE(I2C1)) // 送るまで待つ
  {
    ;
  }
  LL_I2C_GenerateStopCondition(I2C1);	// Stop Condition 発行
}

void i2cStopAfterReading()
{
  LL_I2C_GenerateStopCondition(I2C1);	// Stop Condition 発行
}

void i2cWrite(uint8_t data)
{
  while (!LL_I2C_IsActiveFlag_TXE(I2C1)) // バッファが空であることを確認する
  {
    ;
  }
  LL_I2C_TransmitData8(I2C1, data);
  while (!LL_I2C_IsActiveFlag_BTF(I2C1)) // 送り終えたことを確認する
  {
    ;
  }
}

uint8_t i2cRead()
{
  while (!LL_I2C_IsActiveFlag_RXNE(I2C1)) // データがあることを確認してから
  {
    ;
  }
  return LL_I2C_ReceiveData8(I2C1);
}

void i2cAddress(uint8_t addr)
{
  LL_I2C_TransmitData8(I2C1, addr);
  while (!LL_I2C_IsActiveFlag_ADDR(I2C1))
  {
    ;
  }
  LL_I2C_IsActiveFlag_BUSY(I2C1);	// Read SR2 (Dummy)
}

ビルドする

Project Explorer で F401I2cLLMem を選択し Project – Build Project でビルドしエラーをなくして動作確認します。

波形を確認できたので貼っておきます。
上が SDA 下が SCL です。

最初のライト時

リード時

メモリーの 01番地から 3バイト書いた値を読みだして一致することが確認できました。

解説

処理を関数で区切ったので、理解しやすいのではないかと思います。

ステータスレジスタの値を確認しながら進まないと、うまく動きませんでした。

SPIに比べると通信制御が難しくクセが強いですけれど、LLで動かすことができるようになりI2Cへの理解を深めることができました。

細かい制御がたいへんですが、実装してみるとHALよりはだいぶ軽いのではないかと思います。

※エラー処理は省いていますので、適時実装する必要があるかも知れません。
タイムアウト処理が入っていないので、何かの拍子にロックしてしまうことがあるかも知れません。

いかがでしたか?
皆さまの環境では、うまく読み書きできましたか?

LLカテゴリの最新記事