RustでSPI制御 (連載12)
いよいよ、Rustからハードウェアを制御していくプログラムを自分で書いていきます。
ネットワーク機能の事は後で考えます。
サンプルプログラムを理解する、というフェーズから一歩進みました。
はたして、書けるのか。。。(なぜこんなにも自信がないのか。。。)
1. 新規ワークスペースを作成
まず、新規ワークスペースを作成します。
ここは、こちらの記事の通りですので、今回は割愛します。
lib.rsが、プログラムを書いていく対象のファイルです。
func1() はslo_main()に名称変更します。
2.Cargo.tomlに依存関係を記述
こちらの記事の途中でも記載していますが、Cargo.tomlに依存関係を記述します。
https://note.com/kmc715/n/n3b503a948df5?magazine_key=m76fc246deaa1
以前と同様、Cargoパッケージ「bcm2711_pac」に含まれるライブラリクレートを使用する、と記述します。
bcm2711_pacは、以前gitで手動でダウンロードした事があるので、既に自分のファイルシステムの中にあります。
今回はそちらを使ってみます。
もちろん、前回同様、gitから自動的にダウンロードする想定で記述することも可能です。
Cargo.tomlは以下のようになりました。
[package]
name = "rust_spi_lib"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tock-registers = "0.7.0"
itron = { version = "= 0.1.9", features = ["unstable", "nightly", "solid_fmp3"] }
bcm2711_pac.path = "../../bcm2711_pac"
#bcm2711_pac = { git = "https://github.com/KyotoMicrocomputer/solid-rapi4-examples.git" }
[lib]
crate-type = ["staticlib"]
3.コーディング
端子機能の設定からSPI送信部まで、実機デバッグは行わないことにします。
送信部まで書いてビルドが通った後、デバッグでどれくらい時間を消費することになるのかを実験してみるため、です。
プログラムを作成するにあたり、参考にした資料は、BCM2711のperipheral manual です。
https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf
それと、加速度センサADXL345のマニュアルです。
https://akizukidenshi.com/download/ds/freescale/ADXL345_jp.pdf
それでは、行ってみましょう。
3.1 端子機能の設定
まず、GPIO8,9,10,11の各端子をSPI端子として使えるようにします。
具体的には、GPFSEL レジスタのGPIO8,9,10,11用のALT0に設定する必要があります。
BCM2711チップの内蔵I/OであるGPFSEL0, 1レジスタに、GPIO8,9,10,11の端子機能を設定するビットがあり、それを「ALT0」にする、という操作をします。
具体的には、GPFSELレジスタが持っている端子機能設定ビットを変更し、GPIO8,9,10,11の機能を0b100(ALT0)に設定します。
この操作をする部分は、spi_gpio_controlというモジュールにし、init()関数をパブリックで宣言することにします。
①まず最初に、レジスタリードライトを行うためのgpio_regs関数を宣言しました。
これは、今まで参考にしていたLED点滅サンプルからそのまま持ってきました。
mod spi_gpio_control {
use bcm2711_pac::gpio;
use tock_registers::interfaces::{ReadWriteable, Writeable}; //後で消す
fn gpio_regs() -> &'static gpio::Registers {
// Safety: SOLID for RaPi4B provides an identity mapping in this area, and we don't alter
// the mapping
unsafe { &*(gpio::BASE.to_arm_pa().unwrap() as usize as *const gpio::Registers) }
}
②次に、init関数を作成し、まずはGPFSELレジスタを操作しGPIO8の端子機能を「ALT0」にするコードを書きます。
今まで参考にしていたLED点滅サンプルを参照して書き方を学び、bcm2711_pacのgpio.rsを見てビット名等の情報を得ました。
pub fn init() {
const GPIO_NUM8: usize = 8;
//GPIO8,9,10,11の端子機能をALT0に設定
gpio_regs().gpfsel[GPIO_NUM8 / gpio::GPFSEL::PINS_PER_REGISTER].modify(
gpio::GPFSEL::pin(GPIO_NUM8 % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::ALT0),
);
}
ここでビルドを通してみたところ、ビルドが通りました!
なので、GPIO9,10,11に対しても同様に記述します。
pub fn init() {
const GPIO_NUM8: usize = 8;
const GPIO_NUM9: usize = 9;
const GPIO_NUM10: usize = 10;
const GPIO_NUM11: usize = 11;
//GPIO8,9,10,11の端子機能をALT0に設定
gpio_regs().gpfsel[GPIO_NUM8 / gpio::GPFSEL::PINS_PER_REGISTER].modify(
gpio::GPFSEL::pin(GPIO_NUM8 % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::ALT0),
);
gpio_regs().gpfsel[GPIO_NUM9 / gpio::GPFSEL::PINS_PER_REGISTER].modify(
gpio::GPFSEL::pin(GPIO_NUM9 % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::ALT0),
);
gpio_regs().gpfsel[GPIO_NUM10 / gpio::GPFSEL::PINS_PER_REGISTER].modify(
gpio::GPFSEL::pin(GPIO_NUM10 % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::ALT0),
);
gpio_regs().gpfsel[GPIO_NUM11 / gpio::GPFSEL::PINS_PER_REGISTER].modify(
gpio::GPFSEL::pin(GPIO_NUM11 % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::ALT0),
);
}
念のために再度ビルドし、ビルドが通ることを確認しました。
3.2 SPIレジスタの初期設定
意外とすんなりとGPIO端子設定処理を書けました。
次、SPIレジスタを操作するコードを追加してみます。
割り込みは使わず、SPIの送信・受信済ビットのポーリングです。
初期設定としては、
・SPIのCLKレジスタにSPI周波数を設定
・CSレジスタでSPI0_CS0#を選択
・CPOL, CPHAレジスタを加速度センサADXL345の仕様に合わせて設定します。
spi_controlというモジュールを作って、SPI関連の制御を行うことにします。
①先程のspi_gpio_controlモジュールの時と同様、レジスタリードライトを行うための関数を宣言しました。今回は関数名はspi_regにしました。
mod spi_control {
use itron::{task::delay, time::duration};
use bcm2711_pac::spi;
use tock_registers::interfaces::{ ReadWriteable, Writeable};
fn spi_regs() -> &'static spi::Registers {
// Safety: SOLID for RaPi4B provides an identity mapping in this area, and we don't alter
// the mapping
unsafe { &*(spi::BASE_SPI0.to_arm_pa().unwrap() as usize as *const spi::Registers) }
}
bcm2711_pacのspi.rsを見たところ、gpioと同じようにレジスタアクセスができそうなので、gpioのところをspiと変えただけ、のようなイメージです。
内蔵I/Oのレジスタアクセスは、こういうパターンで記述ができそうです。
②次に、init関数を作成し、まずはCLKレジスタを操作し、設定値0x00cbを書く処理を記述します。
mod spi_control {
pub fn init() {
const CLKSET: u32 = 0x00cb;
spi_regs().clk.write(
spi::CLK::CDIV.val(CLKSET)
);
}
これでビルドが通りました。
実はビルドが通るまで、紆余曲折ありましたが、結局こういう事かと自分なりに理解しました。
・spi_regs().clk.write( の部分:
自分解釈:spi_regs(register_structs構造体)のclkメンバに書き込みをせよ。
・spi::CLK::CDIV.val(CLKSET) の部分:
自分解釈:書き込む対象は spi::CLKクラスのCDIVメンバで、その値は .val()で操作すること
余談ですが、SOLID-IDEを使うとコード補完がきっちり表示されるので、とても使いやすかったです。
③次に、init関数にCSCPOL, CPHAレジスタへの設定処理を記述します。
今回は、writeではなくmodifyの方が良さそうですので、そうしました。
必要なビットだけを操作したいから、という意図です。
spi_regs().cs.modify(
spi::CS::CS::ChipSelect0,
);
spi_regs().cs.modify(
spi::CS::CPOL::RestStateIsHigh,
);
spi_regs().cs.modify(
spi::CS::CPHA::FirstSclkTransitionAtBeginningOFDataBit,
);
先程の、自分解釈と、自動補完機能を使って難なく記述できました。
ビルドし、ビルドが通ることを確認しました。
3.3 SPI送信部の実装
次に、SPIレジスタを操作しSPI送信を行う処理を実装してみます。
割り込みは使わず、SPIの送信済ビットのポーリングです。
加速度センサ側のレジスタアドレス0x2cに対し、データ0x0bを書き込んでみます。
送信のフローは以下です。
※1/31追記:以下のフローは一部間違っています。第13回で解説していますのでご参照ください。
①CSレジスタのTAビットを1にする。
②FIFOに送信データを書く(送信データ:ライト対象の加速度センサのレジスタ値”0x2c”)
③CSレジスタのTXDビットが1になるまで待つ
(待つ処理の前に5usほどウエイトを入れる。そもそも0になる前にチェックをしないようにするため。ウエイト値は後で見直す。)
④CSレジスタのCLEARビットに0x01を書いてFIFOクリア
⑤FIFOに送信データを書く(送信データ:先ほどのレジスタへのライト値”0x0b”)
⑥CSレジスタのTXDビットが1になるまで待つ
(待つ処理の前に5usほどウエイトを入れる。そもそも0になる前にチェックをしないようにするため。ウエイト値は後で見直す。)
⑦CSレジスタのCLEARビットに0x01を書いてFIFOクリア
⑧CSレジスタのDONEビットが1になるまで待つ
⑨CSレジスタのTAビットを0にして送信完了。
①と②は、先程の自分解釈と自動補完機能を使って難なく記述できました。
が、③について。
待つ方法ってどうやって書いたらいいのだろうか。。。
ビットが1になるのを待つための記述を知るために、tock_registersのレジスタインタフェースを調べました。
ReadOnly部に、is_setがあります。これが使えそうです。
is_setを使うために、use宣言の部分にReadableを追加しました。
use tock_registers::interfaces::{Readable, ReadWriteable, Writeable};
そして、C言語の時の同じような感じでwhile文を書いてみました。
mod spi_control {
let mut retval = false;
while retval == false
{
retval = spi_regs().cs.is_set(
spi::CS::TXD
);
}
これでビルドが通りました。
このようにして、送信部分は完成し、全部ビルドが通りました!
4.デバッグ
ここから、各レジスタへの設定値が正しいかどうか、デバッグしてみます。
まず、GPIO。
内蔵I/Oのレジスタの値を確認するためには、メモリウインドウはお勧めしません。
指定されているアクセス幅でアクセスしないと、メモリ内容が正しく読めないため、です。
SOLID-IDEメニューから
[デバッグ]-[PATNERコマンドウインドウ]を選んで表示されるウインドウから、専用のコマンドを発行します。
専用のコマンド:PI/PO (Peripheral Input/ Peripheral Output)) コマンド
コマンドの3文字目がアクセス幅で、b:8bit, w:16bit, d:32bit, q:64bit です。
①GPFSEL0(アドレス0xfe200000), GPFSEL1(アドレス0xfe200004)の確認
GPFSEL0,1レジスタに書き込む前は以下。
GPFSEL0,1レジスタに書き込んだ後。
書けてました!
じゃ、もういいかな?
一気に動かしてみましょう。
波形を見てみて、SDAやSDD端子、CS端子、CLKが反応していればOK。
見たところ、想定通り出てきています。
※1/27追記:SDD端子がロジックアナライザから外れていました。
本来、受信側も何か信号に反応はあるはずです。
なんと、デバッグの初回で動きました!
いつもだいたい、ポインタの中身なのかアドレスなのかごっちゃになってしまって、おかしなメモリをアクセスしてしまう、という、初歩的な間違いが付きものだったのに!
ポインタ、という概念から解き放たれただけでも、これだけストレスがないとは。。。
ちなみにCSレジスタのTXDビットチェックの前のウエイト処理を削除すると、想定通りの波形にならなかったので、多少のウエイトは必要そうです。
5.ソースコード
という事で、lib.rsは以下のようになりました。
#[no_mangle]
pub extern "C" fn slo_main() {
println!("Starting SPI control.");
spi_gpio_control::init();
spi_control::init();
spi_control::senddata();
}
mod spi_gpio_control {
use bcm2711_pac::gpio;
use tock_registers::interfaces::{ReadWriteable, Writeable};
fn gpio_regs() -> &'static gpio::Registers {
// Safety: SOLID for RaPi4B provides an identity mapping in this area, and we don't alter
// the mapping
unsafe { &*(gpio::BASE.to_arm_pa().unwrap() as usize as *const gpio::Registers) }
}
pub fn init() {
const GPIO_NUM8: usize = 8;
const GPIO_NUM9: usize = 9;
const GPIO_NUM10: usize = 10;
const GPIO_NUM11: usize = 11;
//GPIO8,9,10,11の端子機能をALT0に設定
gpio_regs().gpfsel[GPIO_NUM8 / gpio::GPFSEL::PINS_PER_REGISTER].modify(
gpio::GPFSEL::pin(GPIO_NUM8 % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::ALT0),
);
gpio_regs().gpfsel[GPIO_NUM9 / gpio::GPFSEL::PINS_PER_REGISTER].modify(
gpio::GPFSEL::pin(GPIO_NUM9 % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::ALT0),
);
gpio_regs().gpfsel[GPIO_NUM10 / gpio::GPFSEL::PINS_PER_REGISTER].modify(
gpio::GPFSEL::pin(GPIO_NUM10 % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::ALT0),
);
gpio_regs().gpfsel[GPIO_NUM11 / gpio::GPFSEL::PINS_PER_REGISTER].modify(
gpio::GPFSEL::pin(GPIO_NUM11 % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::ALT0),
);
}
}
mod spi_control {
use itron::{task::delay, time::duration};
use bcm2711_pac::spi;
use tock_registers::interfaces::{Readable, ReadWriteable, Writeable};
fn spi_regs() -> &'static spi::Registers {
// Safety: SOLID for RaPi4B provides an identity mapping in this area, and we don't alter
// the mapping
unsafe { &*(spi::BASE_SPI0.to_arm_pa().unwrap() as usize as *const spi::Registers) }
}
pub fn init() {
const CLKSET: u32 = 0x00cb;
//SPIのCLKレジスタにSPI周波数を設定
spi_regs().clk.write(
spi::CLK::CDIV.val(CLKSET)
);
//CS : 00 = Chip select 0 -> CS.bit0 and 1
//CPOL: Clock Polarity 1 = Rest state of clock = High -> CS.bit3
//CPHA: Clock Phase 1 = First SCLK transition at beginning of data bit. -> CS.bit2
spi_regs().cs.modify(
spi::CS::CS::ChipSelect0,
);
spi_regs().cs.modify(
spi::CS::CPOL::RestStateIsHigh,
);
spi_regs().cs.modify(
spi::CS::CPHA::FirstSclkTransitionAtBeginningOFDataBit,
);
}
pub fn senddata() {
const CS_TA_ACTIVE: u32 = 1;
const CS_TA_INACTIVE: u32 = 0;
//①CSレジスタのTAビットを1にする。
spi_regs().cs.modify(
spi::CS::TA.val(CS_TA_ACTIVE),
);
//②FIFOに送信データを書く(送信データ:ライト対象の加速度センサのレジスタ値; とりあえず0x2cと固定しておく。)
spi_regs().fifo.write(
spi::FIFO::DATA.val(0x2C),
);
//③CSレジスタのTXDビットが1になるまで待つ 待つ前にちょっとウエイト
delay(duration!(us: 5)).unwrap();
let mut retval = false;
while retval == false
{
retval = spi_regs().cs.is_set(
spi::CS::TXD
);
}
//④CSレジスタのCLEARビットに0x01を書いてFIFOクリア
spi_regs().cs.modify(
spi::CS::CLEAR_TX.val(0x01),
);
//⑤FIFOに送信データを書く(送信データ:先ほどのレジスタへのライト値)
spi_regs().fifo.write(
spi::FIFO::DATA.val(0x0b),
);
//⑥CSレジスタのTXDビットが1になるまで待つ 待つ前にちょっとウエイト
delay(duration!(us: 5)).unwrap();
let mut retval = false;
while retval == false
{
retval = spi_regs().cs.is_set(
spi::CS::TXD
);
}
//⑦CSレジスタのCLEARビットに0x01を書いてFIFOクリア
spi_regs().cs.modify(
spi::CS::CLEAR_TX.val(0x01),
);
//⑧CSレジスタのDONEビットが1になるまで待つ
let mut retval = false;
while retval == false
{
retval = spi_regs().cs.is_set(
spi::CS::DONE
);
}
//⑨CSレジスタのTAビットを0にして送信完了。
spi_regs().cs.modify(
spi::CS::TA.val(CS_TA_INACTIVE),
);
}
}
今回はここまで。
次回はSPI受信部を作っていきます。