STM32 Rust ベアメタルにI2Cを使ってみる

  • 2022.04.02
  • I2C
STM32 Rust ベアメタルにI2Cを使ってみる

今回はI2Cを使ってみます。
ベアメタルと言われているレジスタにアクセスする方法を使います。

開発環境は以下の通りです。

PC:Windows10 OS
Board:STM32Nucleo-F401RE
デバイス:STM32F401RE
エディタ:VSCode
言語:Rust

ボードの情報は こちら からご覧いただけます。
環境構築については こちら をご覧になってください。

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Ω でプルアップしました。

アクセスのしかた

まず最初に指定するコントロールバイトについて見ていきましょう。
このデバイスの場合、上位4ビットは 1010 のパターンになります。
A2-A0はGNDに接続するので、000 のパターンになります。
素の状態のアドレスは 0x50 ですが、ご存じの通りI2Cでは左に1ビットシフトした状態で使うので
0xa0 を使い、更に最下位のビットは read時には 1、write時には 0 にします。
ただし read時に 1 をセットする役目は HAL の関数がやってくれるのでプログラム上で意識する必要はありません。

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

read時: 0xa1
write時: 0xa0

バイトライト

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

ページライト

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

ランダムリード

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

シーケンシャルリード

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

STM32のオルタネート機能とは

GPIO端子に汎用入出力以外の機能を割り当てる場合、動作モードをオルタネートに設定する必要があります。
データシートを見れば、端子にどの機能を割り当てることができるのか確認することができます。
(全ての機能を自由に端子に割り当てることはできません)

下表はSTM32F401REデータシート テーブル9 の一部抜粋です。

PB8,9にI2C1の機能を割り当てられることがわかります。
今回は PB8,9 の端子を使うことにします。

I2C1の機能を使うために GPIOB8,9のモードレジスタでオルタネートの設定を行い、もうひとつオルタネート機能上位レジスタの AFRH8,9 に AF4 を設定します。

AF4 はテーブル9の AF04 を指しています。

オルタネート機能レジスタには上位と下位があり、GPIOx0-7 (xはA, B, C…) に対しては下位に、GPIOx8-15 に対しては上位の方に設定を行います。

どの端子にどのペリフェラルや機能を割り当てるのか、回路を設計する上でテーブル9は重要な存在になります。

プロジェクトをつくる

コマンドプロンプトを起動し、cargo generate と git を使ってSTM32F401用のプロジェクトを作成します。

cargo generate –git https://github.com/rust-embedded/cortex-m-quickstart.git と入力しプロジェクト名を聞いてくるので stm32f401-i2c と入力します。

cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart.git
 Unable to load config file: C:\Users\xxxxx\.cargo\cargo-general.toml
 Project Name : stm32f401-i2c

C:\Users\xxxxx\stm32f401-i2c というフォルダができていることを確認します。

VSCodeを起動する

以下のようにコマンドを入力することで、stm32f401-i2cをカレントディレクトリとしてエディタ(VSCode)が起動します。

cd stm32f401-i2c
code .

C:\Users\xxxxx>cd stm32f401-i2c
C:\Users\xxxxx\stm32f401-spi>code .

config.tomlの編集

gdbの指定と Cortex-M4F をターゲットに指定します。

ツリーの >.cargo の >部分をクリックすると V .cargo となり、そのツリー下に config.toml というファイルが見えるのでこれをクリックして開きます。

(1)こちらの環境では 8行目に # runner = “arm-none-eabi-gdb -q -x openocd.gdb” があるので、先頭の # をはずします。
(2)こちらの環境では35行目に target = “thumbv7m-none-eabi” # Cortex-M3 があるので、先頭に # をつけます。
前後の行に合わせて # target = … のように記述しておきます。
(3)こちらの環境では37行目に # target = “thumbv7me-none-eabihf” # Cortex-M4F and Cortex-M7F (with FPU) があるので、先頭の # をはずします。

# のある行はコメント(無効)になるようですね。

編集が終わったら Ctrl + s で保存しておきます。

launch.jsonの編集

ここではデバイスの指定を行います。
ツリーの >.vscode の >部分をクリックすると V .vscode となり、そのツリー下に launch.json というファイルが見えるのでこれをクリックして開きます。

“configurations”: の中に {},{} で2つの項目が区切られています。
上の方は “name” が “Debug (QEMU)” となっていて、下の “name” が “Debug (OpenOCD)” です。
QEMUはエミュレーターで、今回は OpenOCD による Debug を行いますので、下の {} 内だけを編集します。

(1)こちらの環境では35行目に #device”: “STM32F303VCT6” とあるので “STM32F401RET6” に変更します。
(2)こちらの環境では38行目に “target/stm32f3x.cfg” とあるので “target/stm32f4x.cfg” に変更します。
(3)こちらの環境では40行目に “svdFile”: “${workspaceRoot}/.vscode/STM32F303.svd” とあるので最後の 303 を 401 に変更します。

編集が終わったら Ctrl + s で保存しておきます。

Cargo.tomlの編集

ツリーの下の方に Cargo.toml があります。

今回はhalを使わずに stm32f4 というクレートを使いますので、それを指定しておきます。
クレートとはライブラリのようなものです。

23~25行を以下のように編集します。
dependencies は依存関係の意味で、こういうクレートの、このバージョンのものを使いますよ とRustに教えてあげます。

今回は HAL を使わないので stm32f4クレートを指定します。

[dependencies.stm32f4]
features = ["stm32f401", "rt"]
version = "0.14"

(注意)今回の追加

8行目に cortex-m = “0.6.0” があるので Delayを使うために “0.7” にバージョンを上げておきます。

編集が終わったら Ctrl + s で保存しておきます。

クレートは cretes.io から探すことができます。

memory.xの編集

ツリーの下の方に memory.x があります。
デバイスによってFlashメモリーとRAMの容量が異なるので、ここで指定します。

6, 7行目を以下のように編集します。

  FLASH : ORIGIN = 0x08000000, LENGTH = 512K
  RAM : ORIGIN = 0x20000000, LENGTH = 96K

編集が終わったら Ctrl + s で保存しておきます。

svdファイル

svdファイルはデバッグに必要なファイルです。

svdファイルは こちら からダウンロードできます。

解凍して、data\STMicroの中にある STM32F401.svd を C:\Users\xxxxx\stm32f401-i2c\.vscode に保存します。

ソースコード

main()関数の外側は、組み込み特有の記述になります。
このように書くものだと考えておけば良いと思います。

#![no_std]
#![no_main]

const DEVICE_ADRS_FOR_WRITE: u8 = 0xa0;
const DEVICE_ADRS_FOR_READ: u8 = 0xa1;
const UPPER_ADRS: u8 = 0x00;
const LOWER_ADRS: u8 = 0x01;

use core::convert::TryInto;
use panic_halt as _; // you can put a breakpoint on `rust_begin_unwind` to catch panics
use cortex_m_rt::entry;
use cortex_m::delay;    // Delayを使う
use stm32f4::stm32f401;

#[entry]
fn main() -> ! {

    let dp = stm32f401::Peripherals::take().unwrap();   // デバイス用Peripheralsの取得
    let cp = cortex_m::peripheral::Peripherals::take().unwrap();    // cortex-m Peripheralsの取得
    let mut delay = delay::Delay::new(cp.SYST, 84000000_u32);   // Delayの生成
    clock_init(&dp);    // クロック関連の初期化
    gpiob89_init(&dp);  // GPIOBの初期化
    i2c1_init(&dp);     // I2C1の初期化

    i2c_set_ack(&dp, true); // ACKを返す指示
    i2c_start(&dp);
    i2c_control_byte_write(&dp, DEVICE_ADRS_FOR_WRITE);

    i2c_write(&dp, UPPER_ADRS);
    i2c_write(&dp, LOWER_ADRS);

    i2c_write(&dp, 18);   // 0x12
    i2c_write(&dp, 52);   // 0x34
    i2c_write(&dp, 86);   // 0x56

    i2c_stop_after_writing(&dp);

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

    delay.delay_ms(5_u32);  // 休憩する

    i2c_start(&dp);
    i2c_control_byte_write(&dp, DEVICE_ADRS_FOR_WRITE);

    i2c_write(&dp, UPPER_ADRS);
    i2c_write(&dp, LOWER_ADRS);

    i2c_start(&dp);
    i2c_control_byte_write(&dp, DEVICE_ADRS_FOR_READ);

    let _ul = i2c_read(&dp);
    let _um = i2c_read(&dp);
    i2c_set_ack(&dp, false); // ACKを返さない(NAK)指示
    let _uh = i2c_read(&dp);

    i2c_stop_after_reading(&dp);
    loop {
    }
}

fn clock_init(dp: &stm32f401::Peripherals) {

    // PLLSRC = HSI: 16MHz (default)
    dp.RCC.pllcfgr.modify(|_, w| w.pllp().div4());      // P=4
    dp.RCC.pllcfgr.modify(|_, w| unsafe { w.plln().bits(336) });    // N=336
    // PLLM = 16 (default)

    dp.RCC.cfgr.modify(|_, w| w.ppre1().div2());        // APB1 PSC = 1/2
    dp.RCC.cr.modify(|_, w| w.pllon().on());            // PLL On
    while dp.RCC.cr.read().pllrdy().is_not_ready() {    // 安定するまで待つ
        // PLLがロックするまで待つ (PLLRDY)
    }

    // データシートのテーブル15より
    dp.FLASH.acr.modify(|_,w| w.latency().bits(2));    // レイテンシの設定: 2ウェイト

    dp.RCC.cfgr.modify(|_,w| w.sw().pll());     // sysclk = PLL
    while !dp.RCC.cfgr.read().sws().is_pll() {  // SWS システムクロックソースがPLLになるまで待つ
    }
//  SYSCLK = 16MHz * 1/M * N * 1/P
//  SYSCLK = 16MHz * 1/16 * 336 * 1/4 = 84MHz
//  APB2 = 84MHz
//  APB1 = 42MHz (I2C1 pclk)
}

fn gpiob89_init(dp: &stm32f401::Peripherals) {

    dp.RCC.ahb1enr.modify(|_, w| w.gpioben().enabled());            // GPIOBのクロックを有効にする

    dp.GPIOB.otyper.modify(|_, w| w.ot8().open_drain());            // GPIOB8をオープンドレイン出力にする
    dp.GPIOB.otyper.modify(|_, w| w.ot9().open_drain());            // GPIOB9をオープンドレイン出力にする
    dp.GPIOB.ospeedr.modify(|_, w| w.ospeedr8().very_high_speed()); // GPIOB8を高速動作にする
    dp.GPIOB.ospeedr.modify(|_, w| w.ospeedr9().very_high_speed()); // GPIOB9を高速動作にする

    dp.GPIOB.moder.modify(|_, w| w.moder8().alternate());           // GPIOB8をオルタネートに設定    
    dp.GPIOB.afrh.modify(|_, w| w.afrh8().af4());                   // GPIOB8をAF4に設定    
    dp.GPIOB.moder.modify(|_, w| w.moder9().alternate());           // GPIOB9をオルタネートに設定    
    dp.GPIOB.afrh.modify(|_, w| w.afrh9().af4());                   // GPIOB9をAF4に設定    

}

fn i2c1_init(dp: &stm32f401::Peripherals) {

    // I2C1のクロックイネーブル機能は APB1 にある
    dp.RCC.apb1enr.modify(|_,w| w.i2c1en().enabled());      // I2C1のクロックを有効にする

    dp.I2C1.cr2.modify(|_, w| unsafe { w.freq().bits(42) });// クロック=42MHz (APB1)
    dp.I2C1.ccr.modify(|_, w| unsafe { w.bits(210) });
    dp.I2C1.trise.modify(|_, w| unsafe { w.bits(43) }); 
    dp.I2C1.cr1.modify(|_, w| w.pe().enabled());            // ペリフェラルイネーブル

    // f = 100kHz として計算
    // CCR:210の計算
    // Thigh = 1/200kHz, CCR = 42M / 200k = 210
    // TRISE:43の計算
    // 42M * 1000n = 42, 42 + 1 = 43

}

fn i2c_set_ack(dp: &stm32f401::Peripherals, ack: bool) {
    if ack {
        dp.I2C1.cr1.modify(|_, w| w.ack().ack());   // ACKを返すように指示
    } else {
        dp.I2C1.cr1.modify(|_, w| w.ack().nak()); // ACKを返さないように指示
    }
}

fn i2c_start(dp: &stm32f401::Peripherals) {
    dp.I2C1.cr1.modify(|_, w| w.start().set_bit()); // スタートコンディション
    while dp.I2C1.sr1.read().sb().is_no_start() {    // 生成されるまで待つ
    }
}

fn i2c_stop_after_writing(dp: &stm32f401::Peripherals) {
    while dp.I2C1.sr1.read().tx_e().is_not_empty() {    // エンプティでない間待つ
    }
    dp.I2C1.cr1.modify(|_, w| w.stop().set_bit());  // ストップコンディション
}

fn i2c_stop_after_reading(dp: &stm32f401::Peripherals) {
    dp.I2C1.cr1.modify(|_, w| w.stop().set_bit());  // ストップコンディション
}

fn i2c_write(dp: &stm32f401::Peripherals, data: u8) {
    while dp.I2C1.sr1.read().tx_e().is_not_empty() {   // エンプティでない間待つ
    }
    dp.I2C1.dr.write(|w| w.dr().bits(data));    // データをライト
    while dp.I2C1.sr1.read().btf().is_not_finished() {    // byte transfer finished
    }
}

fn i2c_read(dp: &stm32f401::Peripherals) -> u8 {
    while dp.I2C1.sr1.read().rx_ne().is_empty() {   // エンプティの間待つ
    }
    dp.I2C1.dr.read().bits().try_into().unwrap()    // データをリード
}

fn i2c_control_byte_write(dp: &stm32f401::Peripherals, data: u8) {
    dp.I2C1.dr.write(|w| w.dr().bits(data));    // コントロールバイトをライト
    while dp.I2C1.sr1.read().addr().is_not_match() {    // 一致するまで待つ
    }
    let _ = dp.I2C1.sr2.read().busy();  // ダミーリードする
}

コードの解説

要所にコメントを入れたので参考にしてください。
クロック部分のレジスタの説明が良く理解できなかったので、私なりに f = 100kHz と想定して値を決めてみました。

メモリーの1番地から3バイト適当な値を書いて、読み出してみました。

動作させてみる

それでは F5キーでプログラムを起動した後、entry で停止したらもう一度F5キーを押してプログラムを動作させてみます。

デバッグモードは OpenOCD に設定する必要があるので、エラーが出る場合には確認してみてください。

こちらでは書いた値3バイトを正しく読むことができました。

いかがでしたか?
皆さんの環境では、うまく動作しましたか?

今回のプロジェクトを stm32f401-i2c におきましたので、よろしければ参考になさってください。

お疲れさまでした。

I2Cカテゴリの最新記事