まえがき
前回の記事「STM32でRust 電光掲示板を作る。SPI編」では、MAX2719を使用したマトリクスLEDに固定文字を表示させました。
文字が表示できると、欲が出るもので、簡易な表示装置として使えるようにしたいなというのが、今回の目標です。
今回のようなベアメタルのプログラムでは、stdライブラリが使用できません。そのため、削られた大きな機能の一つが「println!」系マクロです。(cortex_mライブラリには、openocdへデバッグメッセージを出すためのprintln系のマクロがいくつか定義されています。)
せっかく文字が表示できたので、ここへprintln!で表示したいっ!というを目標点とします。まだ成長途中で、8文字までのascii文字のみという制約をつけておきます。
また、前回のプログラムでは、SPIへの送信終了をループで待たせました。STM32F401には、DMA転送のための回路がついています。これを利用して、割込みで、処理をするように改良したいと思います。
DMA転送
DMA転送
DMAとは、Direct Memory Accessの略です。
今回の題材を例に取ると、前回のプログラムでは、SPIの送信レジスタに1ハーフワードをセット。その後、送信完了を待ち、次のハーフワードをセット。と言った具合に、送信するデータを一個づつ、プログラムで処理をしていました。割込みを利用すれば、「待ち」の部分だけが省略できます。
この、メモリーにある内容を、別のIOやメモリーに転送する作業を、自動的にハードウェアが処理する機構がDMAです。一旦処理を開始すると、DMAハードウェアが自分で直接メモリーを読んで別のメモリーやIOに直接書き込みを行うので、Direct Memory Access(直接メモリーにアクセスする)と言った言葉になっています。
一旦送信を指示すれば、前回のプログラムのように、「一つデータを送って終了を待ち・・・」の部分が全部自動化出来、CPUは、別の仕事をするなり、寝るなりすることが出来ます。
DMA転送には、大きく、次のようなことを指定することが必要です。
- 送信元のメモリーアドレス
- 送信先のメモリーアドレス
- 転送サイズ
- 転送のタイミングを指定するための制御信号
STM32のようなメモリーマップドIOでは、ペリフェラルもメモリーと同様の扱いをされますので、メモリーからペリフェラルや、その逆の転送が自由に出来ます。ただ、最後の制御信号が必要なので、ペリフェラルがDMAに対応していることが必要となります。
今回のSPIペリフェラルは、送受信とも、DMAに対応しています。
DMAの設定
STM32F401のDMAは、8回線のDMAペリフェラルが2つ搭載されています。合計最大16回線の転送を同時に扱えます。でも、制御回線が、GPIOの割付のときと同じで、対象とするペリフェラルによって、どこの回線を使うががある程度決められていて、その中から選択するという形になるので、完全に16回線が自由に使えるわけでもありません。
DMAの回線の割付は、リファレンスマニュアルの「DMAコントローラ/DMAの機能説明/チャンネル選択」に、DMA?リクエストマッピングという表に掲載されています。
今回使用したいのは、SPI1の送信です。SPI1_TXを探して、その中から選ぶわけですが、今回は、DMA2のストーム3を選択することにします。
転送元は、メモリーで転送先は、SPI1ペリフェラルとなります。
このDMAには、FIFOが搭載されています。メモリーの読み書き速度とSPI通信の転送速度には大きな差が有りますから、この速度差を吸収するためのバッファとして機能します。
さらに、DMA転送が完了した際に、割込みで通知を行うようにします。
今回の事例では、MAX2719の仕様により、4ハーフワードを送る度に、CSをリセットしてデータを確定させる必要が有ります。このため、全てのデータを一気に転送することが出来ません。
そのため、この割込みを使って、データの確定処理と次のデータ群の送信処理を行うことにします。
後、細かい設定等を含めて、DMAの設定手順は、概ね次のようになります。
- DMA2ペリフェラル電源ON(RCC_AHB1ENRのDMA2ENを1
- DMA2 STREAM3のI/F選択をSPI1に(DMA2 S3CRのCHSELを3)
- DMA2 STREAM3のメモリーバーストアクセスをINCR4に(DMA2_S3CRのMBURSTを01)
- DMA2 STREAM3のペリフェラルバーストモードをシングル(DMA2_S3CRのPBURSTを00)
- メモリーとペリフェラルのデータサイズを16ビットに(DMA2_S3CRのMSIZEとPSIZEを01)
- メモリーのアドレス自動インクリメントを有効に(DMA2_S3CRのMINCを1)
- ペリフェラルのアドレス自動インクリメントを無効に(DMA2_S3CRのPINCを0)
- データ転送方向は、メモリー→ペリフェラル(DMA2_S3CRのDIRを01)
- TCIE以外の全ての割込みを無効。TCIEのみ有効(DMA2_S3CRの**IEを設定)
- FIFOの転送しきい値を1/2に(DMA2_S3FCRのFTHを01)
- SPI1の送信DMA転送を有効に(SPI1_CR2のTXDMAENを1)
- DMAの転送先アドレスをSPI1_DRレジスタに
SPI1_DRレジスタのアドレスですが、リファレンスマニュアルを見れば、わかります。ただし、これを数字で直接打つのは嫌です。stm32f401クレートから、もらってきます。
Peripheral構造体のSPI1の実態ですが、これ、実は、SPI設定レジスタ群のベースアドレスそのものです。そして、さらに、SPI1.drは、DRレジスタのアドレスそのものです。というわけで、SPI1.drのアドレスを汎用の生ポインタに変換します。「生ポインタ」に変換するのが大事です。実は、RUSTのポインタは、単なるアドレスではなく、その他の情報を持たせるために拡張されています。次のように出来ます。
let spi1_dr = &self.device.SPI1.dr as *const _ as u32;
ちなみに、生ポインタを取得するのは、unsafeではありません。unsafeになるのは、これを逆参照するときです。そして、今回、プログラムでは逆参照することはありません。ハードウェアにおまかせしますから。
以上で、DMAセットアップ用の関数は、こんな感じになります。
/// DMAのセットアップ
fn dma_setup(&self) {
self.device.RCC.ahb1enr.modify(|_, w| w.dma2en().enabled());
// DMAストリーム3のチャンネル3使用
let st3_3 = &self.device.DMA2.st[3];
st3_3.cr.modify(|_, w| {
w.chsel().bits(3u8);
w.mburst().incr4();
w.pburst().single();
w.ct().memory0();
w.dbm().disabled();
w.pl().medium();
w.pincos().psize();
w.msize().bits16();
w.psize().bits16();
w.minc().incremented();
w.pinc().fixed();
w.circ().disabled();
w.dir().memory_to_peripheral();
w.tcie().enabled();
w.htie().disabled();
w.teie().disabled();
w.dmeie().disabled()
});
st3_3.fcr.modify(|_, w| {
w.feie().disabled();
w.dmdis().disabled();
w.fth().half()
});
let spi1_dr = &self.device.SPI1.dr as *const _ as u32;
st3_3.par.write(|w| w.pa().bits(spi1_dr));
unsafe {
cortex_m::peripheral::NVIC::unmask(stm32f401::interrupt::DMA2_STREAM3);
}
}
DMA転送の開始
DMA転送の開始に関しては、そんなに難しいことはありません。DMA設定で送信元アドレスを指定してあげて、開始ビットを叩くだけです。ただし、その前に各種イベントフラグをクリアすることがマニュアルで推奨されています。
DMAに指定する送信元データのアドレスに関しては、素直に、as_ptr()で取得することが出来ます。
これだけの手順ですが、ソースの該当部分を掲載します。ただし、一レコード目の処理だけです。
2レコード目からは、割込み処理ルーチンの仕事になります。
/// SPI1 データのDMA送信要求
/// MatrixLED 4ブロック*行数 分のデータの送信を行う。
/// 送信データは、事前にDMA_BUFFに投入済みのこと。
fn send_request_to_dma(&self) {
let dma = &self.device.DMA2;
let mut i = DMA_BUFF.iter();
if let Some(data) = i.next() {
while dma.st[3].cr.read().en().is_enabled() {}
let adr = data.as_ptr() as u32;
dma.st[3].m0ar.write(|w| w.m0a().bits(adr));
dma.st[3].ndtr.write(|w| w.ndt().bits(4u16));
Self::spi_enable(&self.device);
Self::dma_start(&self.device);
}
// 以降、2レコード目からの転送は、割込みルーチンにて
}
/// DMAの完了フラグをクリアし、DMAを開始する
fn dma_start(device: &stm32f401::Peripherals) {
let dma = &device.DMA2;
dma.lifcr.write(|w| {
w.ctcif3().clear();
w.chtif3().clear();
w.cteif3().clear();
w.cdmeif3().clear()
});
dma.st[3].cr.modify(|_, w| w.en().enabled());
}
割込み処理関係
グローバル変数
割込み処理ですが、割込み関数には、データを渡すことが出来ません。今回は、転送する2レコード目から8レコード目までのデータを渡す必要が有ります。したがって、グローバル変数を扱うための小細工が必要です。
まず、前提として、この表示関係のルーチンは、モジュールとして独立させます。これによって、このモジュールで定義したstatic変数は、モジュール変数として隔離することが出来ます。他の関係ないルーチンがこの変数を操作することは一切考えなくてよいわけです。
さっきのsend_request_to_dmaにて、dma2のストリーム3が、処理中でないことを確認していました。これも、割込み関数との変数アクセス衝突を避けるための処理です。さらに、この変数の一部は、DMAペリフェラルがCPUの外で勝手に処理します。そのため、いずれにせよ、DMAの処理中は、この変数を操作することは出来ないわけです。(この辺のチェックをサボると、DMA処理の気が狂い、結果として、panicします。)
では、グローバル変数の作成です。
基本的には、UnsafeCell<[u16;4];8]>
の固定長二次配列でもたせます。内側の[u16;4]が一行分ですね。
この配列内の変数を扱う部分は、unsafeの山となりますので、構造体として分離することにします。unsafeの契約条件は、「DMAの転送処理中、つまり、DMA割込み関数が呼ばれる可能性がある間は、通常処理ルーチン側では、この変数を触らないように通常処理ルーチン側で確認処理する。」ことです。これを破ると、きっちりpanic、又は、暴走することになります。
また、[u16;4]を単位として、8回のループをさせるために、簡単なイテレータを定義しておきます。
グローバル変数関係のソースは、次のようになります。
/// DMAビジーフラグ
static DMA_BUSY: Mutex<RefCell<bool>> = Mutex::new(RefCell::new(false));
/// DMAバッファ領域
/// グローバル変数・matrix_ledモジュール以外での操作禁止
/// DMA2_S3CR.ENビットが0の時のみ操作可能
static DMA_BUFF: DmaBuff = DMA_BUFF_INIT;
use core::cell::UnsafeCell;
struct DmaBuff {
buff: UnsafeCell<[[u16; 4]; 8]>,
data_count: UnsafeCell<usize>,
}
const DMA_BUFF_INIT: DmaBuff = DmaBuff {
buff: UnsafeCell::new([[0u16; 4]; 8]),
data_count: UnsafeCell::new(0),
};
unsafe impl Sync for DmaBuff {}
impl DmaBuff {
pub fn clear_buff(&self, device: &stm32f401::Peripherals) -> Result<()> {
Self::is_dma_inactive(device)?;
unsafe {
*self.data_count.get() = 0;
}
Ok(())
}
pub fn add_buff(&self, data: &[u16], device: &stm32f401::Peripherals) -> Result<()> {
Self::is_dma_inactive(device)?;
unsafe {
if *self.data_count.get() < 8 {
*self.data_count.get() += 1;
} else {
return Err("Buffer over flow");
}
&(*self.buff.get())[*self.data_count.get() - 1].clone_from_slice(&data[0..4]);
}
Ok(())
}
pub fn iter(&self) -> DmaBuffIter {
DmaBuffIter { cur_index: None }
}
fn is_dma_inactive(device: &stm32f401::Peripherals) -> Result<()> {
if device.DMA2.st[3].cr.read().en().is_enabled() {
Err("DMA2 stream active")
} else {
Ok(())
}
}
fn get_buff(&self, index: usize) -> Option<&[u16; 4]> {
unsafe {
if index < *self.data_count.get() {
Some(&(*self.buff.get())[index])
} else {
None
}
}
}
}
/// DmaBuff用Iterator
struct DmaBuffIter {
cur_index: Option<usize>,
}
impl Iterator for DmaBuffIter {
type Item = &'static [u16; 4];
fn next(&mut self) -> Option<Self::Item> {
match &mut self.cur_index {
Some(i) => {
*i += 1;
}
None => {
self.cur_index = Some(0);
}
};
DMA_BUFF.get_buff(self.cur_index.unwrap())
}
}
冒頭のDMA_BUSYは、通常処理ルーチン側で、DMA転送処理中であることを確認するためのフラグです。DMA転送開始処理直前にセットして、割込み処理ルーチンで、8レコード目の処理終了後にリセットしています。単純に考えると、DMA2_S3CRのENフラグで確認できそうですが、各レコードを送ってから次のレコードを送るまでの間に、このENフラグがオフになる期間があり、連続して、LEDへの表示更新関数を呼び出すと、この間に割り込んでしまうことが出来るようです。当然、契約条件に反しますので暴走しました。
割込み処理
ここまで用意すれば、割込み処理内では、単純に、一つづつのレコードをDMAに送り込んでいくだけです。
が、この割込み、実は、呼ばれるのが少々早すぎます。DMA転送が終了した時点で呼ばれるのですが、これは、SPI1のTXレジスタにデータをセットした時点です。このときには、SPIはまだ、前のデータを送信しています。つまり、約2データ分タイミングが早いのです。そのため、
- DMA割込み発生
- SPIの送信完了を待つ。
- CSビットのリセットをしてMAX2719の行データを確定。
- 次のデータをDMAにセット。
- DMAの送信開始
が、単純に考えた場合の手順になります。でも、割込みの時点でDMAの処理は終了しているので、終了待ちの間に、次のデータの準備までは可能です。
それを考慮して、割込み処理ルーチンは、次のようにしています。
/// DMA2 Stream3 割込み関数
# [interrupt]
fn DMA2_STREAM3() {
static mut ITER: Option<DmaBuffIter> = None;
let device;
unsafe {
device = stm32f401::Peripherals::steal();
}
let dma = &device.DMA2;
if dma.lisr.read().tcif3().is_complete() {
dma.lifcr.write(|w| w.ctcif3().clear());
if let None = ITER {
*ITER = Some(DMA_BUFF.iter());
ITER.as_mut().unwrap().next();
}
match ITER.as_mut().unwrap().next() {
Some(data) => {
//次のデータの準備
let adr = data.as_ptr() as u32;
dma.st[3].m0ar.write(|w| w.m0a().bits(adr));
dma.st[3].ndtr.write(|w| w.ndt().bits(4u16));
//前データの確定終了処理
Matrix::spi_disable(&device);
//次のデータの送信開始
Matrix::spi_enable(&device);
Matrix::dma_start(&device);
}
None => {
//前データの確定終了処理
Matrix::spi_disable(&device);
*ITER = None;
free(|cs| *DMA_BUSY.borrow(cs).borrow_mut() = false);
}
}
} else {
dma.lifcr.write(|w| {
w.ctcif3().clear();
w.chtif3().clear();
w.cteif3().clear();
w.cdmeif3().clear()
});
}
}
print!の実装
概要
さて、print!マクロを実装してみます。
rustの標準ライブラリーのprintln!ですが、大きく分けて、2つの機能からなります。一つは、フォーマット文字列と引数を解釈して、結果の表示文字列を作る機能。もう一つは、結果の表示文字列を標準出力に書き出す機能です。
no_std環境でも、この前半の表示文字列を作る機能は、実はちゃんと実装されています。かけているのは、後半の標準出力への書き出しです。まぁ、stm32f401のボードに標準出力なんてありませんからね。当然のことと言えば当然のことです。
というわけで、欠けている部分をなんとかして、マトリクスLEDにつないであげれば、めでたく、print!系マクロが実装できます。std::println!では、さまざまな環境に対応すべく、いろいろと小難しいことをしていますが、今回の目的は、マトリクスLEDに出力できればOKです。簡略化していくことにします。
core::fmt::Write
coreライブラリーに、fmtというモジュールが有ります。(当然、stdにもあります。)このモジュールが、フォーマット文字列の処理をするための一式の構造体・トレイト群を用意してくれています。
この中に、fmt::Writeというトレイトが有ります。no_std環境で欠けているものが、このトレイトの実装です。このトレイトを実装した構造体が、出力処理を担うことになります。
fmt::Writeトレイトを実装するにあたって必須の関数は、write_str(&mut self, s: &str) -> fmt::Result
のひとつだけです。この関数は、見ての通り、入力に文字列スライスを取り、Resultを返します。今回は、この文字列をマトリクスLEDに出力することが目標となります。
非常にゆるい形でしか定義されておらず、中身は本当に、自分のプラグラム依存となります。出力のタイミングも、バッファの存在も何も規定されていません。自由です。逆に、この関数が呼ばれる正確なタイミングもブラックボックスです。つまり、一回のprint!呼び出しで、このwrite_strが何回呼ばれるかは、不明です。実際に出力するタイミングに関して、ちゃんと自分でコントロールする必要が有ります。
fmt::Writeの実装
さて、今回のマトリクスLEDに文字列を表示するわけですが、仕様は、次のようにします。
- 入力文字列は、ASCII文字列のみとする。
- LED出力は、4*8フォントを使用する。
- 出力する文字は、8文字までとし、それ以上の文字は切り捨てる。
- 文字列に、"\n"が来た時に出力する。"\n"に後続の文字列も切り捨てる。
と簡略化しておきます。
制限を外すなら、この関数を書き換えれば済むだけです。
今回、出力処理を作るに当たり、display_ledというモジュールを単体で作成することにします。どうせ、後々、上記の制限をゆるくしたくなることは目に見えてますし、そうすると結構複雑化するのも見えていますから。
これを実装すると、今度は、matrix_ledモジュール内の公開機能を、メインルーチンから直接操作されると、矛盾が出る可能性が出てきます。
そこで、MatrixLedの定義を少々変更します。
まず、今回のプロジェクト内に、2つのモジュールが出ますので、lib.rsを次のように定義します。
# ![no_std]
// matrix ledの制御
pub mod display_led;
pub mod matrix_led;
こうして、matrix_led::Matrix::newの定義を次のように変えます。
impl<'a> Matrix<'a> {
pub(super) fn new(device: &stm32f401::Peripherals) -> Matrix {
この、pub(super)は、new関数の公開範囲を限定します。matrix_led本体から見て、その親、つまり、lib.rsの中までが可視範囲となります。lib.rs内のモジュールからしか、new関数が見えなくなれば、今回のMatrixを直接操作されるということもなくなります。そもそも構造体が作れませんから。
次に、display_ledモジュール内に、デイスプレイドライバと称して実装をしていくことにします。
本体のDisplayLed構造体の定義は、次のような感じです。
///ディスプレイドライバ
pub struct DisplayLed<'a> {
led: Matrix<'a>,
buff: [u8; 50],
buff_len: usize,
}
impl<'a> DisplayLed<'a> {
pub fn new(device: &'a stm32f401::Peripherals) -> Self {
let mut display = DisplayLed {
led: Matrix::new(&device),
buff: [0; 50],
buff_len: 0,
};
display.led.clear();
display
}
pub fn clear(&mut self) {
self.led.clear();
}
}
次に、この構造体に、fmt::Writeトレイトを実装します。
impl fmt::Write for DisplayLed<'_> {
fn write_str(&mut self, s: &str) -> fmt::Result {
if s.len() == 0 {
return Ok(());
}
let mut is_output = false;
for c in s.chars() {
match c {
'\n' => {
is_output = true;
}
cc if cc.is_ascii_control() => {}
cc => {
if self.buff_len < 50 {
self.buff[self.buff_len] = cc as u8;
self.buff_len += 1;
}
}
}
}
if is_output == true {
self.led.clear();
for (i, c) in self.buff[0..self.buff_len].iter().enumerate() {
let font = FONT48.get_char(*c);
self.led.draw_bitmap((i * 4) as i32, 0, 4, font);
while let Err(_) = self.led.flash_led() {}
}
self.buff_len = 0;
}
Ok(())
}
}
単純に受けた文字列を全部表示すればよいのですが、文字列が分割してやってくる可能性があるので、\nをトリガとして、Marix::flash_led()を呼んでいます。さらに、なぜか、空文字列で一回余計に関数を呼ばれるようなので、無駄な処理を省くために、冒頭の早期リターンをつけてあります。
さて、こうして、fmt::Writeトレイトを実装すると、このトレイトで定義されているfmt::write_fmtがおまけでついてきます。これは、フォーマット文字列群を取り、結果の文字列を作り、write_strを呼んでくれるまでの処理をしてくれます。
これを呼び出すマクロを作れば、今回の目標は達成です。
マクロの引数を、fmt::Argumentsの形に仕立てて、write_fmtを呼ぶだけです。
マクロ定義です。
pub fn print_led_fmt(disp: &mut DisplayLed, args: fmt::Arguments) {
disp.write_fmt(args).unwrap()
}
# [macro_export]
macro_rules! print_led {
($disp:ident, $($arg:tt)*) => {
display_led::print_led_fmt(&mut $disp, format_args!($($arg)*));
}
}
write_strさえあれば、あっさりと、これで完成です。
メイン関数からの呼び出し方は、こうなります。
use matrixled::display_led::DisplayLed;
use matrixled::print_led;
// 中略
let mut led = DisplayLed::new(&device);
// 中略
print_led!(led, "{:>02}:{:>02}:{:>02}\n", hh, mm, ss);
これ、実は、ちょっと悩みました。どうも、マクロは、サブモジュール内にあっても、クレートのトップにある扱いをされるようです。
ですので、use文が、 use matrixled::display_led::print_led;
でなく、上のようになるようです。実は、どうも、このあたりの扱いが今ひとつ理解しきれてません。
終わり
これで、当初の目標は一応達成です。
いつもだと、ここで、全ソースを掲げていたのですが・・・さすがに少々長すぎる。というわけで、githubにあげておきます。
git clone https://github.com/mitoneko/clock.git
【編集(2020.06.27)】
元のgithubのコードは、関連する必要な自作クレートがないなど、少々中途半端でしたので、次の記事を書くのに作ったclockに差し替えます。mainの内容が前回ここに書いた内容と変わっちゃってますが、ご了承ください。
githubのリポジトリそのものは、一度あげちゃったものですから、消さずにそのまま置いておきます。IDから引っ張れば、みることも可能かと。
main.rsには、無機能カウントアップタイマを実装してあります。単純に、tim11を使って、一秒に一度、表示をカウントアップしているだけです。リセットを押せばスタートするストップウォッチ?(笑)
これの動きを見ていると、チップ内蔵のクロック発振器がいかに不正確かがよく見えます。私の機体だと、10分ほど動かすと、目に見えて時間が狂っています。そんなに?と思って、ハードウェア仕様を見ると、誤差1%とあるので、100秒に1秒と考えれば、まぁ、確かに、そんなもんかと。
うん。時間が測りたければ、外部クロックが必要になるわけですね。
ボードには、RTCと、RTC用の外部発振器がついている。これ使えば、正確になるかな。と手がけてみると、RTCって、実は結構な難物でした。これは、また機会があれば、そのうち。(先の注釈どおり、書きました。STM32でRust 時計の作成 RTC編)