Rustという言語でSTM32F3Discoveryというマイコンを動かし,4桁7セグメントLEDをダイナミック点灯方式で光らせるっていう記事の3編目です.
この記事を読み終わるころには完全に理解したって言ってもらえるくらい丁寧に書いたつもり.
成果物はこちら.↓
概要編では,主に4桁7セグメントLEDをダイナミック点灯方式で光らせる方法について,概要を説明しました.
実装編1では,ペリフェラル全般とか,*GPIO
*ペリフェラルについて説明しました.
今回は,前回の続きで,*SPI
*ペリフェラルのところから始めていきたいと思います.
筆者も組み込み初心者であるため,説明が行き届かないところもあるかもしれませんが,ご容赦ください.
誤情報だけは書かないように注意を払ってますので,ご安心ください.
はじめに
コードでは,Rustのstm32f30x
というクレートを用いていますが,本記事では,このクレートをただ利用するだけではなく,使用する構造体や関数が,どのような役目を果たしているかや,本当はどんなことをしているかなど,なるべく低レベルに降りて説明するようにしています.
多少自分に言い聞かせるように書いてる部分もあり,鬱陶しく感じることがあるかもしれません.そう感じたところはどうぞ読み飛ばしちゃってくださいませ.
では本題に入ります.
ここからはリファレンスマニュアルを横に並べて読むことをお勧めします.見るべきページは,適宜示します.
SPI1
SPIを用いて通信するため,SPI1
ペリフェラルを使う.
前回までで,GPIO
ペリフェラルに対して,PA5とPA7でそれぞれ*AF5
を使うよっていう設定までした.しかし,これだけではまだSPIは使えない.
AF5を使うことにしただけで,その実態はまだないわけです.
じゃあどうすればSPI*を使うことができるのだろうか.
GPIO
ペリフェラルには,SPI1
ペリフェラルが接続されている.このSPI1
ペリフェラルに電源を投入することで初めてPA5とPA7でSPIバスが使えるようになるのだ.
電源の投入とリセット
じゃあというわけでまずは電源とリセット.
はい,教科書p.150〜.
電源
リセット
*GPIO
*のときも似たようなことをしたので,説明は省く.
コードはこちら↓
let rcc = unsafe { &*RCC::ptr() };
rcc.apb2enr.modify(|_, w| w.spi1en().enabled());
rcc.apb2rstr.modify(|_, w| w.spi1rst().set_bit());
rcc.apb2rstr.modify(|_, w| w.spi1rst().clear_bit());
SPI1ペリフェラルの設定
*GPIO
ペリフェラルのときはピンごとに設定していたが,SPI1
*ペリフェラルの場合は複数のピンで1つの機能を成すので,それぞれのピンで別々に設定するということはしない.
どんな設定にすればいいかは概要編でほとんど話したので,端折り気味で説明する.
*SPI1
*ペリフェラルの設定を行うレジスタは2つある.
SPI1_CR1(SPI control register 1)
1つ目は*SPI1_CR1
.(p.998〜)
このレジスタで設定すべきなのは,
下から順にCPHA
,CPOL
,MSTR
,BR
,SPE
,LSBFIRST
,SSI
,SSM
,BIDIMODE
*の9個.
順番に説明していきます.
CPHA
とCPOL
については概要編で話した通り0
.↓の画像で思い出せない人は概要編に戻ってみてください.
MSTR
は,このマイコンをSPIバスのマスターとして使うかどうかってことなので,もちろん1.
*BR
*は少しややこしいので後回し.
SPE
はSPI1
ペリフェラルを有効化するかどうかってことなので,もちろん1.
LSBFIRST
は,8bitのデータを,先頭から送るか末尾から送るか.これはプログラムをこの設定に合わせればいい話なのでどっちでもいいが,どっちかには決めとかないとプログラムが書けないので好みで0(MSB first)か1(LSB first)にする.僕のプログラムではMSB firstになっている.
SSI
は,SPIバスにおいて,マスターモードのときのSS
線の出力を設定しておけばいい?はず?
次に示す,SSM
が1のときのみしか機能しない.
(すみません,このbitに関してはあまり理解していません.)
1にします.
SSM
は,SS
線の出力の制御を,自分で別に書く場合は1.
今回は1にする.詳しくはあとで触れる.
BIDIMODE
っていうのは,SPIバスで用いる通信方式を選択するためのbit.
実はSPIバスには一般的な全二重3線式の他にも,半二重とかいう方式もあるらしい.詳しく知りたい方はこちらの記事がわかりやすかった.
今回使うのは普通のやつなので,全二重3線式.
というわけでこちらは0.
最後に,後回しにしていた*BR
.
これは,つまりはボーレートのことで,簡単にいうなら通信速度.
単位はbpsで,1秒間に何bitの情報を送れるかということを表す.
SPI通信では,クロックの立ち上がりでデータがサンプルされるため,クロックの周波数と,このボーレート*の値は一致する.
がしかし,どんな周波数でも設定できるわけではない.
設定できる周波数は以下のようになっている.
pclk1の分周で設定するようだ.
通信相手の74HC595は,SCKに設定できるのは,最大で50MHz程度まで.
そもそもこのマイコンではpclk1の最大値が64MHzなので,BR
には32MHzまでしか設定できない.なのでこれは気にしなくて良さそう.
話を戻して,仮に1MHzに設定したいとしたら,どうすればいいだろうか.
まず,BR
には,pclk1の分周という形で設定するため,丁度1MHzに設定できるかどうかはpclk1の周波数次第ということになる.
pclk1の値さえわかれば,*BR
*の値は,$$\frac{pclk1}{1_\rm{MHz}}$$の値によって決められる.
コードだとこんな感じ↓だ.
let freq = 1000_000; // = 1MHz
let br = match pclk1 / freq {
0 => unreachable!(),
1..=2 => 0b000,
3..=5 => 0b001,
6..=11 => 0b010,
12..=23 => 0b011,
24..=39 => 0b100,
40..=95 => 0b101,
96..=191 => 0b110,
_ => 0b111,
};
さて,残る疑問はただ1つ.
pclk1の値はどうやって得られるのか.
教科書p.126!
ふむふむ,8MHzのHSIとやらを$\frac{1}{2}$倍してそれをPLLMUL倍したものがSYSCLKで.
それをAHB prescaler(hpre)で割ったものがHCLK.
んでそれをAPB1 prescaler(ppre1)で割ったものがpclk1か!
ところで,このpllmulとかhpreとかppre1ってどこからきてるんだろうか.
教科書p.138.
const HSI: u32 = 8_000_000;
... // 関係ないので省略
let rcc = unsafe { &*RCC::ptr() };
let pllmul_bits = rcc.cfgr.read().pllmul().bits();
let pllmul: u32 = u32(pllmul_bits + 2);
let ppre1_bits = rcc.cfgr.read().ppre1().bits();
let ppre1: u32 = if ppre1_bits & 0b100 == 0 { 1 } else { 1 << (ppre1_bits - 0b011) };
let hpre_bits = rcc.cfgr.read().hpre().bits();
let hpre: u32 = if hpre_bits & 0b1000 == 0 { 1 } else { 1 << (hpre_bits - 0b0111) };
let sysclk = pllmul * HSI / 2;
let hclk = sysclk / hpre;
let pclk1 = hclk / ppre1;
*SPI1_CR1
*についての説明はこれで終わり.
コードはこんな感じ↓.
spi.cr1.write(|w| unsafe {
w.cpha()
.bit(false)
.cpol()
.bit(false)
.mstr()
.set_bit()
.br()
.bits(br)
.spe()
.set_bit()
.lsbfirst()
.clear_bit()
.ssi()
.set_bit()
.ssm()
.set_bit()
.bidimode()
.clear_bit()
});
SPI1_CR2(SPI control register 2)
2つ目は*SPI1_CR2
.(p.998〜)
このレジスタで設定すべきなのは,
下から順にSSOE
,DS
*の2個.


順番に説明していきます.
SSOE
は,SS
線の出力を有効にするかどうか.
今回はSS
線の出力の制御は,自分で別に書くので無効にする.
というわけで0.
*DS
*は,データサイズ.
今回は8bitなので,0b0111.
*SPI1_CR2
*についての説明はこれで終わり.
コードはこんな感じ↓.
spi.cr2.write(|w| unsafe {
w.ds().bits(0b0111).ssoe().clear_bit()
});
SPI1ペリフェラルを使ってデータを送信
設定が終わったので,あとは実際にシフトレジスタにデータを送信するだけです.
p.1004
この*SPI1_DR
*というレジスタに送信したいデータを書き込むだけ.
でも忘れてはいけないのが,SS
線(シフトレジスタのRCK
).
この信号は自分で別に書くことにしていたので,それも忘れずに.
前回PA4に割り当てた*GPIO
のpush-pull output
でSS
*線の制御を行う.
なのでコードはこんな感じ↓.
const NUMBERS: [u8; 11] = [
0b11101011, // 0
0b00101000, // 1
0b10110011, // 2
0b10111010, // 3
0b01111000, // 4
0b11011010, // 5
0b11011011, // 6
0b11101000, // 7
0b11111011, // 8
0b11111010, // 9
0b00000000 // OFF
];
... // 省略
pub fn display_num(&self, number: usize) {
self.rck.set_low();
unsafe { ptr::write_volatile(&(*SPI1::ptr()).dr as *const _ as *mut u8, NUMBERS[number]) };
self.rck.set_high();
}
これで*SPI1
*ペリフェラルについての説明は終わりです.
あとは,7セグの,表示する桁とLEDを一定時間ごとに切り替えるだけです!
そのために,今度は*TIM2
っていうペリフェラル*を使います.
次回
というわけで,次回はこの*TIM
*ペリフェラルの有効化〜設定について書きます.
拙い文章ですが,ここまで読んでいただきありがとうございました.