RustでSPIデータ受信―有識者レビューとクレート化 (連載15)
前回書いたサンプルプログラムや記事を、有識者の方にチェックいただきました。
そのフィードバックの内容について書きます。
さらに、加速度センサADXL345制御部をクレート化しておこうと思います。
1. 有識者レビュー結果
1.1 引数型は意味に即した型を使用すること
例えば加速度センサADXL345のレジスタに書き込む以下の関数について、見てみます。
pub fn regwrite(regaddr: u32, writeval: u32)
--------- [指摘内容]---------
意味に即した型を使用するのが慣習的です。例えば、ADXL345のレジスタアドレスと値はそれぞれ8ビット幅なので、u8が引数型になります。
------------------------------
ここで筆者がなぜu32にしたかというと、これはBCM2711チップの内蔵I/Oである SPIのFIFOレジスタに書き込むデータであり、そのFIFOレジスタのサイズが32ビットだからです。
Cargoパッケージ「bcm2711_pac」に含まれるライブラリクレートに以下のように定義されています。
register_bitfields! {u32,
pub FIFO [
/// Read from RX FIFO or write to TX FIFO
///
/// *DMA Mode (`DMAEN` set):* If `TA` is clear, the first 32-bit write
/// to this register will control `SPIDLEN` and `SPICS`. Subsequent
/// reads and writes will be taken as four-byte data words to be read
/// or written to the FIFOs.
///
/// *Poll/Interrupt Mode (`DMAEN` clear, `TA` set):* Writes to the
/// register write bytes to the TX FIFO. Reads from the register read
/// bytes from the RX FIFO.
DATA OFFSET(0) NUMBITS(32) [],
]
}
u32ですね。
すなわち、筆者はアクセス先のレジスタサイズと同じサイズ(u32)にしました。
しかしそうではなく、使い道に即した型にすることが慣習だとの事でした。
今回はFIFO32ビット分すべて使用するわけではありません。
理由はADXL345のレジスタアドレスが8ビット、値も8ビットだからです。
8ビット分押し出せば済みます。
したがって、ここはu8とします。
しかしそうすると、問題が出てきます。
先述の通り、FIFOのレジスタは32ビットアクセスです。
これについても、アドバイス頂きました。
--------- [指摘内容]---------
u8 -> u32は無損失変換なので、“.into()” で変換できます。
例:
spi::FIFO::DATA.val(writeval.into()));
------------------------------
ふむふむ。
“.into()” は、組み込みではいろいろ出てきそうですね。
要チェックです。
同じことは受信関数にも言えます。
pub fn regread(regval: u32) -> i32 {
引数はu8にしましょう。
戻り値はi32、このままで。
pub fn regread(regval: u8) -> i32 {
そしてFIFOのアクセスも同様に“.into()” で変換。
//(3)FIFOに送信データを書く(送信データ:ライト対象の加速度センサのレジスタ値)
spi_regs().fifo.write(spi::FIFO::DATA.val(regval.into()));
これで完了。
1.2 型変換
受信部の最後、符号付に変換したり型変換したりしている部分があります。
retdata = i32::try_from(retdata_l | (retdata_h << 8)).unwrap();
if (retdata & 0x8000) != 0x0000 {
retdata = (!retdata & 0xFFFF) + 1;
retdata = 0 - retdata;
}
ここ、なんと一行で書けてしまいました!
[ポイント1]
FIFOから読んできたretdata_lとretdata_h, u32だがu8として扱い、さらにそれらをfrom_le_bytesを用いて下位側から詰めていく。
参考:https://doc.rust-lang.org/std/primitive.u32.html#method.from_le_bytes
[ポイント2]
i16 -> i32は無損失変換なので、“.into()” または “u32::from( … )” で変換できる。
したがって、以下の一行になりました。
retdata =i32::from(i16::from_le_bytes([retdata_l as u8, retdata_h as u8]));
1.3 return不要
受信関数の最後のreturnは不要です。
(と、過去の記事に書きました。自分で。。。)
https://note.com/yn_2022/n/nf64d5d2b3547#d3706e78-c097-4c12-be4d-941c174e2f63
「Rustでは、return文がなくても、最後の式の値が戻り値になります。」
retdata = i32::from(i16::from_le_bytes([retdata_l as u8, retdata_h as u8]));
return retdata;
という事で、returnを削除し、式にします。
i32::from(i16::from_le_bytes([retdata_l as u8, retdata_h as u8]));
ビルドエラーが出ました。
セミコロンがあると、「式」ではないそうです。
なので、セミコロンは削除し、無事、ビルドが通りました。
[注] 2/14補足
・ブロック中の最後のステートメントの後に式がある場合、それがブロックの値になる
・セミコロン終端された式は「式ステートメント」という扱いになり、ステートメントになるため、リターン値を返すにはその後に式が必要。
ソースコードは最後にまとめて書きます。
2.クレート化
連載8でクレート化を試してみました。
https://note.com/yn_2022/n/nf9851aff3f18
この経験を用いて、加速度センサADXL345制御部をクレート化してみます。
今、以下二つのモジュールがあります。
mod spi_gpio_control
mod spi_control
これら、mod spi_controlに統一し、そしてクレート「spi_adxl345」を作ることにします。
作成の方法は、連載8で書いたことと何ら変わらないので、省略します。
3.動かしてみる
変わらず動きました。
4.ソースコード
ソースコードはこうなりました。
ADXL345センサ制御部をクレート化したので、すっきりしました。
4.1 メイン部
use itron::{task::delay, time::duration};
#[no_mangle]
pub extern "C" fn slo_main() {
const BW_RATE: u8 = 0x2c;
const BW_RATE_VAL: u8 = 0x0b;
const DATA_FORMAT: u8 = 0x31;
const DATA_FORMAT_VAL: u8 = 0x0B; //(4 mg/LSB +-16g)
const POWER_CTL: u8 = 0x2D;
const POWER_CTL_VAL: u8 = 0x08;
println!("Starting SPI control.");
spi_adxl345::init();
//ADXL345_init
spi_adxl345::regwrite(BW_RATE, BW_RATE_VAL);
spi_adxl345::regwrite(DATA_FORMAT, DATA_FORMAT_VAL);
spi_adxl345::regwrite(POWER_CTL, POWER_CTL_VAL);
loop{
//連続複数リードを行う。-> 0xc0とORをとる。
//0.0392266=(4/1000*9.80665)
let x_axis = spi_adxl345::regread(0xc0 | 0x32) as f32 * 0.0392266;
let y_axis = spi_adxl345::regread(0xc0 | 0x34) as f32 * 0.0392266;
let z_axis = spi_adxl345::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待つ
}
}
4.2 クレート部
use bcm2711_pac::gpio;
use bcm2711_pac::spi;
use tock_registers::interfaces::{ReadWriteable, Readable, 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) }
}
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) }
}
fn gpio_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),
);
}
}
pub fn init() {
const CLKSET: u32 = 0x00cb;
gpio_init();
//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: u8, writeval: u8) {
//(1)CSレジスタのTAビットを1にする。
spi_regs().cs.modify(spi::CS::TA::SET);
//(2)FIFOに送信データを書く(送信データ:ライト対象の加速度センサのレジスタ値)
spi_regs().fifo.write(spi::FIFO::DATA.val(regaddr.into()));
//(3)CSレジスタのTXDビットが1になるまで待つ
while !spi_regs().cs.is_set(spi::CS::TXD) {}
//(4)FIFOに送信データを書く(送信データ:先ほどのレジスタへのライト値)
spi_regs().fifo.write(spi::FIFO::DATA.val(writeval.into()));
//(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: u8) -> i32 {
let retdata_h: u32;
let retdata_l: u32;
//(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.into()));
//(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);
i32::from(i16::from_le_bytes([retdata_l as u8, retdata_h as u8]))
}
今回はここまで。
今回、クレートのインタフェースを、
・init()関数
・regwrite()関数
・regread()関数
としましたが、次回はspi_adxl345への制御を完全に隠蔽してしまうべく変更をしたいと思います。
・init()関数:spi_adxl345への初期化を完全に終わらせる
・current_acceleration()関数:X, Y, Z軸の加速度値を取得する(Main関数で計算不要にする)
それが終わったらSPIのことは一旦忘れて、Linuxのネットワーク機能をSOLID-OSから使用することを試してみます。