RustでSPIデータ受信 (連載14)
前回までで、SPIレジスタに対し読み書きを行い、SPI信号を出力することができました。
今回は、前回作成した送信部を使える形にし、さらに受信部を書いていきたいと思います。
1. レジスタライト関数実装:送信先レジスタとデータを引数にする
前回、SPIレジスタの読み書きを正しくできるようにすることが目的だったため、SPI通信先とのアドレス指定、書き込みデータは固定していました。
今回は、上位関数からそれらを指定できるようにします。
1.1 まずは出ているWarningを取る。
Writeableは使っていないので、Warningになっています。
これは削除し,
use tock_registers::interfaces::ReadWriteable;
とします。
1.2 spi_controlモジュールのsenddata関数を充実させる
先述の通り、前回は動作確認のため、SPI通信先との書き込み先のレジスタアドレス指定、さらに書き込みデータも固定していました。
今回は、この関数に引数を持たせます。
引数:
・アクセスしたいSPI通信先(加速度センサADXL345)のレジスタアドレス値
・書き込みたいデータ
ついでに、senddata関数という関数名はあまり意図にそぐわないので、関数名も変えてしまいましょう。
regwrite関数とします。
ではregwrite関数に引数を二つ持たせましょう。
C言語では、
void regwrite(uint32_t regaddr, uint32_t writeval) {
ですね。
Rustではこうなります。
pub fn regwrite(regaddr: u32, writeval: u32) {
(心の声:逆やん!)
あとはこの引数を、実際に使用するコードで使えばよいだけです。
一回目のFIFOに書き込むデータとしてregaddr、2回目としてwritevalを指定するだけです。
ソースコードは後でまとめて記載します。
2.レジスタリード関数を追加する
次に、加速度センサのレジスタをリードする関数を追加します。
関数名はpub fn regread(regval: u32)にします。引数はアドレス値です。
SOLID-OSでは、SPIレジスタなどの内蔵I/O領域は非キャッシュに設定されています。
このため、今回はキャッシュ動作を意識することなくコードが書けます。
これはOSの仕様でありRustの仕様ではありません。
また、内蔵I/Oの中にはアクセス幅が限定されているものもありますが、そちらはbcm2711_pacであらかじめ定義されているので、ここでは意識する必要はありません。
2.1 加速度センサのレジスタリード処理
フローは以下です。
(1)CSレジスタのTAビットを1にする。
(2)TX-FIFO, RX-FIFOクリア
(3)FIFOに送信データを書く(送信データ:ライト対象の加速度センサのレジスタ値)
(4)CSレジスタのTXDビットが1になるまで待つ
(5)CSレジスタのRXDビットが1になるまで待つ(後述。(注)参照)
(6)CSレジスタのCLEARビットに0x02を書いてRX-FIFOクリア
(7)FIFOに送信データを書く(送信データ:押し出すだけなのでダミーで0)
(8)CSレジスタのTXDビットが1になるまで待つ
(9)CSレジスタのRXDビットが1になるまで待つ
(10)待ち終わったらFIFOリード
(11)FIFOに送信データを書く(送信データ:押し出すだけなのでダミーで0)
(12)CSレジスタのTXDビットが1になるまで待つ
(13)CSレジスタのRXDビットが1になるまで待つ
(14)待ち終わったらFIFOリード
(15)CSレジスタのDONEビットが1になるまで待つ
(16)CSレジスタのCLEARビットに0x03を書いてTX-FIFO, RX-FIFOクリア
(17)CSレジスタのTAビットを0にして送受信完了。
(注):ここでは読み込みは行わないのでRX-FIFOにデータが入ったことを確認する必要は基本的にはない。しかし、ここでシフトインされてくるデータがあるのも事実。
このため、今ここでシフトインされてくるデータを確実にRX-FIFOに格納した上でRX-FIFOをクリアすることで、確実にこのシフトインデータを読み飛ばしている。
この通りに実装してみましたが、一点だけひっかかったポイントがありました。
今まで、BCM2711の内蔵I/Oレジスタに対し、write, is_set, modify操作は行ってきましたが、純粋にreadしたことはありませんでした。
でもなんとなく、こんな感じ?と思って書いたらビルドが通りました。
retdata_l = spi_regs().fifo.read(spi::FIFO::DATA);
ビルド通ったから読めてるんだろう、という事で、次に進みます。
2.2 リード値の結合
このセンサの場合、加速度値は2回にわたってリードされます。
上位8ビットと下位8ビットです。
(正確には、上位5ビットですが、符号拡張計算の都合上8ビットで処理します。)
以下のように読みました。
let retdata_h: u32;
let retdata_l: u32;
:
retdata_l = spi_regs().fifo.read(spi::FIFO::DATA); //一回目
:
retdata_h = spi_regs().fifo.read(spi::FIFO::DATA); //二回目
rdata_hを8ビット左シフトし、retdata_lと加算すればOKです。
let retdata: i32;
:
retdata = retdata_l | (retdata_h << 8)
この書き方はC言語と一緒ですね。
2.3 戻り値
リード値を返す必要があります。すなわち、この関数は戻り値を持ちます。
まず、戻り値の型をどうするか?ですが、これは加速度センサなので符号付きの32ビット値にした方が後々都合良さそうです。(考え方はいろいろあると思いますが、一例として)
なので、i32の型で戻り値を定義しましょう。
pub fn regread(regval: u32) -> i32 {
次に、リターン値を考えます。
読めたレジスタを返すのですが、型が違います。u32型です。
変換しなきゃ。
u32からi32への変換にtry_fromを使ってみることにします。
i32変換し、その値をreturnで返します。
i32の変換値 = i32::try_from(FIFOリード値).unwrap();
ふむふむ、なかなか良さそうです。
という事で、以下のようになりました。
retdata = i32::try_from(retdata_l | (retdata_h << 8)).unwrap();
さらに、ここで符号付きに変換しておきましょう。
という事で、
retdata = i32::try_from(retdata_l | (retdata_h << 8)).unwrap();
if (retdata & 0x8000) != 0x0000 {
retdata = (!retdata & 0xFFFF) + 1;
retdata = 0 - retdata;
}
return retdata;
と、なりました。
そして、この計算により、retdataは定数ではなく変数になるので、mutを付けないといけません。
と、ビルドする前に教えてくれました。
3.加速度センサADXL345のレジスタ初期設定
作成したregwrite関数を用いて、加速度センサのレジスタ初期設定を行います。
詳しくは省略しますが、3つのレジスタに値を設定していきます。
1ビット:4 mg/LSB 、範囲+-16gの設定です。
4.加速度センサADXL345から加速度値をリード
作成したregread関数を用いて、加速度センサ値を取得していきます。
X, Y, Z軸の3値があります。
#連続リードを行うため 、アドレスの上位2ビットは0b11である必要があります。
さらに、読み込んだ加速度センサ値に対して0.0392266を掛ける事で、加速度値が算出できます。
0.0392266= (4/1000*9.80665)と計算しました。
ここで一点、引っ掛かりました。
これは浮動小数点の計算になるのですが、加速度センサを読み込んだ値はi32型です。
f32に変換しないといけないです。
ビルドするとエラーが出て、修正方法を教えてくれました。
i32のままでは計算できないよ。
以下のどれかを選んで。
という事で、f32を選びました。
let x_axis = spi_control::regread(0xc0 | 0x32) as f32 * 0.0392266;
5.取得した加速度値を表示
取得できたX, Y, Z軸の加速度値をprintln!マクロでUART出力し、PCのシリアルターミナルで見てみます。
加速度値取得の間隔はとりあえず500msとしました。
500msくらいあけないと、シリアルターミナルに表示されている文字スクロールが早すぎて見えないので。
おおー。取れてる取れてる。
Z軸方向の加速度が大きめに出てます。
キャリブレーションしないといけないですね。
値の正確性は、今回特に本質でないので、このままにしましょう。
X軸方向を上に向けてみました。
Y軸方向を下に向けてみました。
ちゃんとマイナスも計算できてます。
6.波形で見てみる
一つ目のデータ出力について、波形を取得してみました。
CS信号ベースで見て、6個のかたまりがあります。
最初の3つは初期設定のところ。
続く3つは、一回目の加速度値取得です。
ついでに、加速度値取得間隔を0.5msにしてみました。
println!の実行には時間がかかるのでコメントアウトしました。
今回は全体を見る感じで波形を取得してみました。
7.ソースコード
ソースコードはこうなりました。
use itron::{task::delay, time::duration};
#[no_mangle]
pub extern "C" fn slo_main() {
const BW_RATE: u32 = 0x2c;
const BW_RATE_VAL: u32 = 0x0b;
const DATA_FORMAT: u32 = 0x31;
const DATA_FORMAT_VAL: u32 = 0x0B; //(4 mg/LSB +-16g)
const POWER_CTL: u32 = 0x2D;
const POWER_CTL_VAL: u32 = 0x08;
println!("Starting SPI control.");
spi_gpio_control::init();
spi_control::init();
//ADXL345_init
spi_control::regwrite(BW_RATE, BW_RATE_VAL);
spi_control::regwrite(DATA_FORMAT, DATA_FORMAT_VAL);
spi_control::regwrite(POWER_CTL, POWER_CTL_VAL);
loop{
//連続複数リードを行う。-> 0xc0とORをとる。
//0.0392266=(4/1000*9.80665)
let x_axis = spi_control::regread(0xc0 | 0x32) as f32 * 0.0392266;
let y_axis = spi_control::regread(0xc0 | 0x34) as f32 * 0.0392266;
let z_axis = spi_control::regread(0xc0 | 0x36) as f32 * 0.0392266;
println!("x_axis:{} y_axis:{} z_axis:{}", x_axis, y_axis, z_axis);
delay(duration!(ms: 500)).unwrap(); //500ms待つ
}
}
mod spi_gpio_control {
use bcm2711_pac::gpio;
use tock_registers::interfaces::ReadWriteable;
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() {
for pin in 8..=11 {
gpio_regs().gpfsel[pin / gpio::GPFSEL::PINS_PER_REGISTER].modify(
gpio::GPFSEL::pin(pin % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::ALT0),
);
}
}
}
mod spi_control {
use bcm2711_pac::spi;
use tock_registers::interfaces::{ReadWriteable, Readable, 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 regwrite(regaddr: u32, writeval: u32) {
//(1)CSレジスタのTAビットを1にする。
spi_regs().cs.modify(spi::CS::TA::SET);
//(2)FIFOに送信データを書く(送信データ:ライト対象の加速度センサのレジスタ値)
spi_regs().fifo.write(spi::FIFO::DATA.val(regaddr));
//(3)CSレジスタのTXDビットが1になるまで待つ
while !spi_regs().cs.is_set(spi::CS::TXD) {}
//(4)FIFOに送信データを書く(送信データ:先ほどのレジスタへのライト値)
spi_regs().fifo.write(spi::FIFO::DATA.val(writeval));
//(5)CSレジスタのDONEビットが1になるまで待つ
while !spi_regs().cs.is_set(spi::CS::DONE) {}
//(6)CSレジスタのCLEARビットに0x01を書いてFIFOクリア
spi_regs().cs.modify(spi::CS::CLEAR_TX::SET);
//(7)CSレジスタのTAビットを0にして送信完了。
spi_regs().cs.modify(spi::CS::TA::CLEAR);
}
//TODO: エラー処理は今回は考えず、後で考える
pub fn regread(regval: u32) -> i32 {
let retdata_h: u32;
let retdata_l: u32;
let mut retdata: i32;
//(1)CSレジスタのTAビットを1にする。
spi_regs().cs.modify(spi::CS::TA::SET);
//(2)CSレジスタのCLEARビットに0x03を書いてTX-FIFO, RX-FIFOクリア
spi_regs().cs.modify(spi::CS::CLEAR_TX::SET + spi::CS::CLEAR_RX::SET);
//(3)FIFOに送信データを書く(送信データ:ライト対象の加速度センサのレジスタ値)
spi_regs().fifo.write(spi::FIFO::DATA.val(regval));
//(4)CSレジスタのTXDビットが1になるまで待つ
while !spi_regs().cs.is_set(spi::CS::TXD) {}
//(5)CSレジスタのRXDビットが1になるまで待つ
while !spi_regs().cs.is_set(spi::CS::RXD) {}
//(6)CSレジスタのCLEARビットに0x02を書いてRX-FIFOクリア
spi_regs().cs.modify(spi::CS::CLEAR_RX::SET);
//(7)FIFOに送信データを書く(送信データ:押し出すだけなのでダミーで0)
spi_regs().fifo.write(spi::FIFO::DATA.val(0x00));
//(8)CSレジスタのTXDビットが1になるまで待つ
while !spi_regs().cs.is_set(spi::CS::TXD) {}
//(9)CSレジスタのRXDビットが1になるまで待つ
while !spi_regs().cs.is_set(spi::CS::RXD) {}
//(10)待ち終わったらFIFOリード
retdata_l = spi_regs().fifo.read(spi::FIFO::DATA);
//(11)FIFOに送信データを書く(送信データ:押し出すだけなのでダミーで0)
spi_regs().fifo.write(spi::FIFO::DATA.val(0x00));
//(12)CSレジスタのTXDビットが1になるまで待つ
while !spi_regs().cs.is_set(spi::CS::TXD) {}
//(13)CSレジスタのRXDビットが1になるまで待つ
while !spi_regs().cs.is_set(spi::CS::RXD) {}
//(14)待ち終わったらFIFOリード
retdata_h = spi_regs().fifo.read(spi::FIFO::DATA);
//(15)CSレジスタのDONEビットが1になるまで待つ
while !spi_regs().cs.is_set(spi::CS::DONE) {}
//(16)CSレジスタのCLEARビットに0x03を書いてTX-FIFO, RX-FIFOクリア
spi_regs().cs.modify(spi::CS::CLEAR_TX::SET + spi::CS::CLEAR_RX::SET);
//(17)CSレジスタのTAビットを0にして送受信完了。
spi_regs().cs.modify(spi::CS::TA::CLEAR);
retdata = i32::try_from(retdata_l | (retdata_h << 8)).unwrap();
//符号アリに変換
if (retdata & 0x8000) != 0x0000 {
retdata = (!retdata & 0xFFFF) + 1;
retdata = 0 - retdata;
}
return retdata;
}
}
今回はここまで。
ところでここまで、所有権や借用の難しい問題に当たっていないことに気が付きました。
今のところ、デバイスのレジスタへ読み書きしているだけだからかもしれません。
次回は、今回の作業について、また有識者チェックをして頂く予定なので、その結果を記載します。
ついでに、できればADXL345センサのアクセスモジュールをクレート化してみようと思っています。