SPIでMAX7219のマトリクスLEDを動かす
STM32F401をRustでプログラミングしようの第2段です。
今回は、SPIインターフェースを動かすということで、題材は、MAX7219でコントロールしている88のマトリクスLEDを取り上げます。
ものは、こんな感じ。
「MAX7219 マトリクスLED」で検索すれば、いろいろと商品は出てくると思います。88のLEDをMAX7219でコントロールしている基盤を4つ、シリアルに接続している構成になっています。
MAX7219のデータシートは、参考文献1を見てください。
ハードウェア構成
マイコンボードに関しては、引き続き、NUCLEO-F401REを使用します。
今回は、MAX7219をSPI接続で使用するので、まずは、STM32F401のSPIのピンを探す必要が有ります。恒例のごとく、STM32でも各種I/Fに使用できるピンが制限されています。
思いっきり手抜きは、STM32CUBE IDEでボードを指定して、ピン設定でSPIを使うと指定すれば、あっという間にピン番号も判明します。
正面から行くのであれば、参考文献2のデータシートから、第4章のPinouts and pin descriptionにあるTable9.Alternate function mappingが一番調べやすいかと思います。ピン毎に機能の名前が一覧されていますので、ここから、必要なI/Fを探します。
今回は、SPI1を使用します。MAX7219は、DIN・CLK・CSの3線を要求します。このうち、DINは、SPIだと、MOSIに相当します。
先の表のAF05の列を下がっていくと、SPI1_MOSI(PA-7)・SPI1_SCK(PA-5)・SPI1_NSS(PA-4)が発見できます。今回は、SPI1_MISOは使用しません。
「AF05」は、GPIOのピンにどの機能を割り当てるかの識別符号です。これもメモっておきます。
もう一つ、問題点は、NUCLEO-F401REのボードでは、IO系統の電圧を3.3Vで供給しています。一方、MAX7219は、5Vの電圧を要求します。3.3VのIOでは、残念ながらHIGHの電圧が足りません。
STM32のGPIOは、高機能で、5Vトレラントの上、設定でオープンドレインにすることも出来ます。回路上は、この機能を使って、IOをオープンドレインにして、5Vにプルアップすれば動きそうな気もするのですが、実験した結果は駄目でした。動作が使用不可能なほどに不安定になってしまいました。
というわけで、素直に、3.3V-5Vの双方向レベルコンバータをはさみます。
さて、回路図です。
左側の方は、次に実験しようと思ってつけたもので、今回は、使用しません。右側半分が、今回のテーマです。
LEDはデバッグに重宝するので、つけてあります。
ソフトウェア
動作目標
今回は、電光掲示板を目標にします。
お題目は、Hello World日本語版。最小のプログラムの代名詞ですが・・・嘘です(笑)
表示器全体は328ドットなので、88ドットのフォントを使えば、4文字が表示できる寸法です。文字数が足りませんので、スクロールするようにします。
タイマー系統は割込みを使用しますが、SPIに関しては割込みを使用せずにプログラムしてみます。
gpioの初期化
今回SPI端子として使用するPA-7とPA-5をSPI端子として割り付ける必要が有ります。
このためには、GPIOx_MODERのMODERxxをオルタネート機能モードに設定します。そのうえで、さらに、GPIOx_AFRyレジスタに機能番号をセットするのですが、このレジスタは1ポート当たり4ビットを使用するので、GPIOx_AFRHとGPIOx_AFRLの2つのレジスタに、各々8ポートずつ割り当てられています。今回は、GPIOA_AFRLのAFRL7とAFRL5の設定となります。ここに、ハードウェアの項でメモっておいたAF番号をセットします。今回は、5ですね。
後、GPIOx_OSPEEDRというレジスタが有ります。これは、IOポートが動作する速度を制御しているレジスタです。Lチカですと動作はゆっくりで良いので初期値のロースピードで良いのですが、SPIは結構高速に動く必要があるので、ハイスピードに変更します。
では、レジスタの設定手順です。
- GPIOA_MODERのMODER7(5)を10にする(オルタネートモード)
- GPIOA_AFRLのALRL7(5)を5にする(AF05 spi動作)
- GPIOA_OSPEEDRのOSPEEDR7(5)を11にする(gpioハイスピード)
PA-4の設定に関しては、今回は、GPIOとして直接制御します。よって通常のアウトプット設定です。
この他に、GPIOAの電源を入れるのも忘れずに・・・
以下は、GPIO初期設定部分です。
/// gpioのセットアップ
fn gpio_setup(device : &stm32f401::Peripherals) {
// GPIOA 電源
device.RCC.ahb1enr.modify(|_,w| w.gpioaen().enabled());
// GPIOA セットアップ
let gpioa = &device.GPIOA;
gpioa.moder.modify(|_,w| w.moder1().output());
gpioa.moder.modify(|_,w| w.moder10().output());
gpioa.moder.modify(|_,w| w.moder11().output());
// SPI端子割付け
gpioa.moder.modify(|_,w| w.moder7().alternate()); // SPI1_MOSI
gpioa.afrl.modify(|_,w| w.afrl7().af5());
gpioa.ospeedr.modify(|_,w| w.ospeedr7().very_high_speed());
gpioa.otyper.modify(|_,w| w.ot7().push_pull());
gpioa.moder.modify(|_,w| w.moder5().alternate()); // SPI1_CLK
gpioa.afrl.modify(|_,w| w.afrl5().af5());
gpioa.ospeedr.modify(|_,w| w.ospeedr5().very_high_speed());
gpioa.otyper.modify(|_,w| w.ot5().push_pull());
gpioa.moder.modify(|_,w| w.moder4().output()); // NSS(CS)
gpioa.ospeedr.modify(|_,w| w.ospeedr4().very_high_speed());
gpioa.otyper.modify(|_,w| w.ot4().push_pull());
}
SPI I/Fの初期化手順
STM32F401のSPIインターフェースですが、これも、結構機能が盛り沢山です。ここまでハードウェアでやってくれるなら、別に、HALに頼る必要なんて殆どないんじゃないのかとおもうくらい、全部やってくれます。
世の中、高機能になればマニュアルを読むのは大変になるのが通例で、今回も、必要な機能を選り分けるのが大変という世界になりました。
SPI通信は、通信の定義が実におおらかで、なんでも繋げるのは良いのですが、その分設定する事項も多くなります。
基本的には、MAX7219のデータシートを見ながら、この仕様にSTM32の通信設定を合わしこんでいくことになります。
まず、SPIのマスターは、STM32側です。
通信方向は、STM32→MAX7219の単方向となります。
通信のクロックは天下りですが、12MHzとします。
クロックは、Lowから始まるようにします。
さらに、通信の信号は、最初の立ち上がりクロックをデータの送信タイミングとします。
CS信号(STM32側ではNSS信号)ですが、これはちょっと挙動が特殊になります。
MAX7219には、DOUTという端子がついています。これを次段のDINに接続することにより、直列接続が出来ます。そうすると、直列接続されたMAX7219全体が一つのシフトレジスタとして機能するようになっています。今回の回路では、4段のMAX7219が接続されている形になります。通信形態としては、4段分のMAX7219に送信するデータを一気に送信した後、CS信号をHIGHにすると、その立ち上がりで全てのデータが確定されます。標準的なSPI通信のCS制御とは異なるので、このピンに関しては、生のGPIOとして自分で制御します。
SPI通信初期化のレジスタ
では、通信関係のSIM32のレジスタの設定です。
- SPI1_CR1のMSTRを1に(STM32がマスター側)
- RCC_APB2ENRのSPI1ENビットを1にして電源を入れる。
- SPI1_CR1のBIDIMODEを0に(単方向通信モード)
- SPI1_CR1のDFFを1に(16bit通信モード)
- SPI1_CR1のBRを001に(システムクロック/4で12MHzとする)
- SPI1_CR1のCPOLを0に(クロック極性。アイドル時Low)
- SPI1_CR1のCPHAを0に(クロックの立ち上がりでデータセット)
- SPI1_CR1のSSMを1に(CS制御を手動にします。)
- SPI1_CR1のSSIを0に(SSNをLowにします。外部出力はGPIOで制御しますが、I/Fの動作が、これをクリアしないと送信状態になりません。)
初期化部の実装は、こんな感じです。
/// SPIのセットアップ
fn spi1_setup(device : &stm32f401::Peripherals) {
// 電源投入
device.RCC.apb2enr.modify(|_,w| w.spi1en().enabled());
let spi1 = &device.SPI1;
spi1.cr1.modify(|_,w| w.bidimode().unidirectional());
spi1.cr1.modify(|_,w| w.dff().sixteen_bit());
spi1.cr1.modify(|_,w| w.lsbfirst().msbfirst());
spi1.cr1.modify(|_,w| w.br().div4()); // 基準クロックは48MHz
spi1.cr1.modify(|_,w| w.mstr().master());
spi1.cr1.modify(|_,w| w.cpol().idle_low());
spi1.cr1.modify(|_,w| w.cpha().first_edge());
spi1.cr1.modify(|_,w| w.ssm().enabled());
spi1.cr1.modify(|_,w| w.ssi().slave_not_selected());
}
SPI通信の開始と終了
SPI通信の有効化には、SPI1.CR1レジスタのSPEビットを1にします。SPI通信の無効化は、TXバッファの空とSPI通信が終了していることの2つを確認して、同じSPEビットを0にします。
今回は、それと同時に、4つのMAX7219に全部データを送り終わった時点で、CSビットを立ち上げる処理をここに組み込みます。SPI通信の開始時にCSをLowにして、SPI通信が終了した時点で、CSをHIGHにセットすることによりMAX7219のデータを確定し、LED表示に反映させます。
SPI通信の開始と終了手続きは、次のような感じです。
/// spi通信有効にセット
fn spi_enable(&self) {
self.cs_enable();
self.spi.cr1.modify(|_,w| w.spe().enabled());
}
/// spi通信無効にセット
/// LEDのデータ確定シーケンス含む
fn spi_disable(&self) {
while self.spi.sr.read().txe().is_not_empty() {
cortex_m::asm::nop();
}
while self.spi.sr.read().bsy().is_busy() {
cortex_m::asm::nop(); // wait
}
self.cs_disable();
self.spi.cr1.modify(|_,w| w.spe().disabled());
}
/// CS(DATA) ピンを 通信無効(HI)にする
/// CSピンは、PA4に固定(ハードコート)
fn cs_disable(&self) {
self.device.GPIOA.bsrr.write(|w| w.bs4().set());
for _x in 0..10 {
// 通信終了時は、データの確定待ちが必要
// 最低50ns 48MHzクロックで最低3クロック
cortex_m::asm::nop();
}
}
/// CS(DATA) ピンを通信有効(LO)にする
/// CSピンは、PA4に固定(ハードコート)
fn cs_enable(&self) {
self.device.GPIOA.bsrr.write(|w| w.br4().reset());
}
ここで、TXEとBUSYフラグの確認待ちループの中に、 cortex_m::asm::nop()
が入っています。これは、文字通り、アセンブラのnop命令です。つまり、何もしないなんですが、コンパイラの最適化阻止の為のダミーです。ループの中に何もないことを見たコンパイラがループそのものを抹殺することを防止しています。
また、cs_desableの中で、10回ループが有ります。コメントの通り、データ確定のためのCS信号の立ち上げは、max7219のデータシートによると最低50ns維持する必要が有ります。この待ちを単純にnopループで稼いでいます。
SPI通信、データの送信
SPIのデータ送信は、基本的に、SPI I/Fが持つ、TXバッファレジスタに送信データをロードするだけです。
TXバッファレジスタとRXバッファレジスタは、DRレジスタという一つのレジスタに2重化されています。DRレジスタに書き込むとTXバッファレジスタに書き込み、DRレジスタを読むとRXバッファレジスタの内容を読み、データ受信フラグをクリアするという挙動をします。TXレジスタに書き込みをすると、SPI_SRレジスタのTXEフラグが立ち、送信用のシフトレジスタが空き次第、自動的に、SPI送受信シフトレジスタに値を転送し、送信を行ってくれるようになっています。TXバッファレジスタの値が、SPI送受信シフトレジスタに転送されると、SPI_SRレジスタのTXEフラグがクリアされます。
送信の手順は、次のようになります。
- SPI1.SRレジスタのTXEビットがセット(TXバッファ空)されていることを確認する。
- SPI1.DRレジスタに、送信するデータをロードする。
16bitのデータを送信する処理は、次のような感じになります。
/// SPI1 16ビットのデータを送信する。
fn spi_send_word(&self, data: u16) {
while self.spi.sr.read().txe().is_not_empty() {
cortex_m::asm::nop(); // wait
}
self.spi.dr.write(|w| w.dr().bits(data));
}
MAX7219の初期化
これで、SPI通信の部品は整いました。
つづいて、MAX7219の初期化です。
設定内容は、ごく少ないです。まず、このLSIは本来7セグメントLEDを制御するためのチップだったりします。そのため、チップ内に、数字を7セグメントLEDのパターンに展開するデコーダが内蔵されています。今回は、使いませんので、このデコーダを無効にします。
さらに、7セグLEDの桁数を制限することが出来る様にスキャン桁数の設定も出来ます。これも使いませんので、全部点灯するようにします。
輝度の制御が、16段階で出来ます。0xFが一番明るく、0x0が一番暗くなります。が、このLED、すごく明るいです。0x0でも、充分使えたりします。
最後に、シャットダウンモードを解除してLED表示を開始します。
コマンド形式は、16bitの数値の上位8ビットがチップのレジスタアドレスで、下位8ビットがデータという形になっています。
アドレスと機能の対応、今回設定する値は、次の表のとおりです。
アドレス | 機能 | 今回の設定 |
---|---|---|
0x09 | BCDデコーダ有効・無効 | 0x00(無効) |
0x0A | 輝度制御(0x0〜0xF) | 0x02 |
0x0B | スキャン桁指定 | 0x07(全桁点灯) |
0x0C | シャットダウンモード | 0x00(解除。つまりRUN) |
0x0F | テストモード | 0x00(無効) |
この数値をSPIで送信しますが、MAX7219は4つあります。つまり、同じ16bitの値を4回送ってから、SPI通信をオフにしてデータを確定させます。
この部分のソフトは次のようになります。
/// Matrix LED 初期化
fn init_mat_led(&self) {
const INIT_PAT: [u16; 5] = [0x0F00, // テストモード解除
0x0900, // BCDデコードバイパス
0x0A02, // 輝度制御 下位4bit MAX:F
0x0B07, // スキャン桁指定 下位4bit MAX:7
0x0C01, // シャットダウンモード 解除
];
for pat in &INIT_PAT {
self.spi_send_word4(*pat);
}
}
/// LED4セットに同じ16bitデータを送信する
fn spi_send_word4(&self, data: u16) {
self.spi_enable();
for _x in 0..5 {
self.spi_send_word(data);
}
self.spi_disable();
}
LEDへのパターン表示
MAX7219は、各桁毎に1アドレスの形でVRAMを持ち、一度値を書き込むとそのパターンの表示を維持するようになっています。
さて、7セグLEDだと「桁」になるわけですが、今回のマトリクスLEDですと、これが「行」になります。
MAX7219のアドレスは、次の通り。
アドレス | 行数 |
---|---|
0x01 | 1行目 |
0x02 | 2行目 |
0x03 | 3行目 |
・・・ | ・・・ |
0x07 | 7行目 |
0x08 | 8行目 |
このアドレスに、8ビットの数値でパターンを書き込みます。この数値は、MSBが一番左で、LSBが一番右になります。
1行は、8ビット幅のLEDユニットが4つで構成されるので、一行を32ビットとして、video_ram[u32; 8]
の形でビデオバッファをもたせます。
このビデオバッファの内容を表示する関数を次に示します。
/// Matrix LEDにvideo_ramの内容を表示する。
pub fn flash_led(&self) {
for x in 0..8 {
self.send_oneline_mat_led(x);
}
}
/// Matrix LED に一行を送る
/// # 引数
/// line_num: 一番上が0。一番下が7
pub fn send_oneline_mat_led(&self, line_num: u32) {
let digi_code :u16 = ((line_num+1)<<8) as u16;
let pat = self.video_ram[line_num as usize];
let dat :[u16; 4] = [ digi_code | (((pat>>24)&0x00FF) as u16),
digi_code | (((pat>>16)&0x00FF) as u16),
digi_code | (((pat>>08)&0x00FF) as u16),
digi_code | (((pat)&0x00FF) as u16),
];
self.spi_enable();
for d in &dat {
self.spi_send_word(*d);
}
self.spi_disable();
}
マトリクスLED制御関係モジュール全体
さて、後は、video_ramのクリアとvideo_ramに矩形パターンを書き込みの関数を付け加えて、制御クラス全体を構成します。今回は、この部分を全部モジュールの形で分離し、matrix_ledと名付けます。
モジュール全体は次の通り。
//! matrix_ledの制御
//! ledサイズ 32*8
use stm32f4::stm32f401;
/// Matrix Ledの制御
pub struct Matrix<'a> {
video_ram: [u32;8], // 左上を基点(0,0)として、各u32のMSBと[0]が基点
device: &'a stm32f401::Peripherals,
spi: &'a stm32f401::SPI1,
}
impl<'a> Matrix<'a> {
pub fn new(device: &stm32f401::Peripherals) -> Matrix {
let led = Matrix {
video_ram: [0;8],
device,
spi: &device.SPI1
};
led.init_mat_led();
led
}
/// Video RAMをクリアする
pub fn clear(&mut self) {
for line in &mut self.video_ram {
*line=0;
}
}
/// 指定の場所に、指定の矩形のビットマップを表示する。
///
/// 原点は、左上隅(0,0)。
/// ビットマップの最大サイズは8*8。
///
/// 幅が8未満の場合は、LSBより詰めること。
/// 矩形の高さは、bitmapの要素数に等しい。
pub fn draw_bitmap(&mut self, px:i32, py:u32, width:u32, bitmap:&[u8]) {
let width = if width<=8 {width as i32} else {8};
let shift:i32 = 31-px-width+1 ;
let mask:u32 = (1 << width)-1;
let mut y = if py>=8 {return} else {py as usize};
for line in bitmap {
self.video_ram[y] |= if shift>=0 {
((*line as u32) & mask) << shift
} else {
((*line as u32) & mask) >> -shift
};
y += 1;
if y >= 8 {break;}
}
}
/// Matrix LEDにvideo_ramの内容を表示する。
pub fn flash_led(&self) {
for x in 0..8 {
self.send_oneline_mat_led(x);
}
}
/// Matrix LED に一行を送る
/// # 引数
/// line_num: 一番上が0。一番下が7
pub fn send_oneline_mat_led(&self, line_num: u32) {
let digi_code :u16 = ((line_num+1)<<8) as u16;
let pat = self.video_ram[line_num as usize];
let dat :[u16; 4] = [ digi_code | (((pat>>24)&0x00FF) as u16),
digi_code | (((pat>>16)&0x00FF) as u16),
digi_code | (((pat>>08)&0x00FF) as u16),
digi_code | (((pat)&0x00FF) as u16),
];
self.spi_enable();
for d in &dat {
self.spi_send_word(*d);
}
self.spi_disable();
}
/// Matrix LED 初期化
fn init_mat_led(&self) {
const INIT_PAT: [u16; 5] = [0x0F00, // テストモード解除
0x0900, // BCDデコードバイパス
0x0A02, // 輝度制御 下位4bit MAX:F
0x0B07, // スキャン桁指定 下位4bit MAX:7
0x0C01, // シャットダウンモード 解除
];
for pat in &INIT_PAT {
self.spi_send_word4(*pat);
}
}
/// LED4セットに同じ16bitデータを送信する
fn spi_send_word4(&self, data: u16) {
self.spi_enable();
for _x in 0..5 {
self.spi_send_word(data);
}
self.spi_disable();
}
/// SPI1 16ビットのデータを送信する。
fn spi_send_word(&self, data: u16) {
while self.spi.sr.read().txe().is_not_empty() {
cortex_m::asm::nop(); // wait
}
self.spi.dr.write(|w| w.dr().bits(data));
}
/// spi通信有効にセット
fn spi_enable(&self) {
self.cs_enable();
self.spi.cr1.modify(|_,w| w.spe().enabled());
}
/// spi通信無効にセット
/// LEDのデータ確定シーケンス含む
fn spi_disable(&self) {
while self.spi.sr.read().txe().is_not_empty() {
cortex_m::asm::nop();
}
while self.spi.sr.read().bsy().is_busy() {
cortex_m::asm::nop(); // wait
}
self.cs_disable();
self.spi.cr1.modify(|_,w| w.spe().disabled());
}
/// CS(DATA) ピンを 通信無効(HI)にする
/// CSピンは、PA4に固定(ハードコート)
fn cs_disable(&self) {
self.device.GPIOA.bsrr.write(|w| w.bs4().set());
for _x in 0..10 {
// 通信終了時は、データの確定待ちが必要
// 最低50ns 48MHzクロックで最低3クロック
cortex_m::asm::nop();
}
}
/// CS(DATA) ピンを通信有効(LO)にする
/// CSピンは、PA4に固定(ハードコート)
fn cs_enable(&self) {
self.device.GPIOA.bsrr.write(|w| w.br4().reset());
}
}
フォントモジュール
さて、日本語で「こんにちは」をやると言っちゃったので、LEDに表示する文字パターンが必要です。このモジュールは、次の形で、半角と全角のパターンをASCII又は、EUC文字コード表の句点をキーとする定数配列の形で実装します。
/// 半角フォント
pub struct Font48 ([Font48Data; 256]);
struct Font48Data([u8; 8]);
impl Font48 {
pub fn get_char(&self, code: u8) -> &[u8; 8] {
&self.0[code as usize].0
}
}
pub const FONT48:Font48 = Font48 ( [
Font48Data([0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0]),
Font48Data([0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0]),
Font48Data([0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0]),
Font48Data([0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0]),
Font48Data([0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0]),
Font48Data([0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0]),
Font48Data([0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0]),
Font48Data([0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0]),
//以下略
続いて、全角
/// 全角フォント
pub struct Font88 ([[Font88Data; 94]; 94]);
struct Font88Data([u8; 8]);
impl Font88 {
pub fn get_char(&self, code_hi: u8, code_low: u8) -> &[u8; 8] {
let code_hi = (code_hi - 0xa1) as usize;
let code_low = (code_low - 0xa1) as usize;
&self.0[code_hi][code_low].0
}
}
pub const FONT88:Font88 = Font88 ( [
// [
// Font88Data([1,2,3,4,5,6,7,8]),
// Font88Data([1,2,3,4,5,6,7,8]),
// ...
// ],
// [
// Font88Data([1,2,3,4,5,6,7,8]),
// Font88Data([1,2,3,4,5,6,7,8]),
// ...
// ],
// ...
// ]) ;
[
Font88Data([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]),
Font88Data([0x00,0x00,0x00,0x00,0x00,0x80,0x40,0x00]),
Font88Data([0x00,0x00,0x00,0x00,0x40,0xa0,0x40,0x00]),
フォントモジュール作成
このフォントデータ全部手で打つのはとてもやってられません。
まず、フォントのデータは、美咲フォントを使わせていただくことにしました。【参考文献3】このフォントデータ、ちゃんとBDF形式でも有ります。でも、悲しいかな、このBDFフォントファイルの読み方がわからない(笑)というわけで、調べるのを手抜きします。幸い、PNGファイルも、このフォントには添付されています。このPNGファイルのドットを全部拾って、先のフォントモジュールの形に仕立てます。
一回こっきり使うだけのユーティリティーなので、思いっきり手抜きのプログラムです。
名前は、makefont。ソースの掲載に止めます。
[package]
name = "makefont"
version = "0.1.0"
authors = ["********"]
edition = "2018"
[dependencies]
image = "0.23.3"
use std::fs::File;
use std::io::prelude::*;
const BASE48_FILE_NAME: &str = "font48.rs.base";
const FONT48_FILE_NAME: &str = "misaki_gothic_2nd_4x8.png";
const OUTPUT48_FILE_NAME: &str = "font48.rs";
const BASE88_FILE_NAME: &str = "font88.rs.base";
const FONT88_FILE_NAME: &str = "misaki_gothic_2nd.png";
const OUTPUT88_FILE_NAME: &str = "font88.rs";
fn main() {
make48();
make88();
}
fn make88() {
// base_fileを書き込む
let mut base_file = File::open(BASE88_FILE_NAME)
.expect("basefile not exist");
let mut contents = Vec::new();
base_file.read_to_end(&mut contents).expect("basefile read error");
let output_file = File::create(OUTPUT88_FILE_NAME)
.expect("output_file fail open");
let mut output_file = std::io::BufWriter::new(output_file);
output_file.write_all(&contents).expect("fail write");
// 画像の読み込み
let img = image::open(FONT88_FILE_NAME).expect("picture file open error");
let img = img.to_rgb();
//let wigth = img.width();
//let height = img.height();
// フォントの切り出し 及び 2値化
let mut fonts = Vec::<Vec::<[u8; 8]>>::new();
for row in 0..94 {
let mut fonts_row = Vec::<[u8; 8]>::new();
for col in 0..94 {
let mut font = [0u8; 8];
let base_x = col*8;
let base_y = row*8;
for i in 0..7 {
let mut font_row:u8 = 0;
for j in 0..8 {
let pixel = img.get_pixel(base_x+j, base_y+i);
let r=pixel.0[0] as u32;
let g=pixel.0[1] as u32;
let b=pixel.0[2] as u32;
font_row |=
if r+g+b < 128*3 {
1u8 << 7-j
} else {
0
};
}
font[i as usize]=font_row;
}
fonts_row.push(font);
}
fonts.push(fonts_row);
}
// フォント定数値の書き出し
for row in &fonts {
output_file.write_all(b"[\n")
.expect("write row start fail"); // 区の始まり
for font in row {
write!(output_file,
"Font88Data([0x{:02x},0x{:02x},0x{:02x},0x{:02x},\
0x{:02x},0x{:02x},0x{:02x},0x{:02x}]),\n",
font[0],font[1],font[2],font[3],font[4],
font[5],font[6],font[7]).expect("write value error");
}
output_file.write_all(b"],\n")
.expect("write row end fail"); // 区の終わり
}
// ファイル終端の書き出し
output_file.write_all(b"] );\n").expect("write end terminate");
}
fn make48() {
// base_fileを書き込む
let mut base_file = File::open(BASE48_FILE_NAME)
.expect("basefile not exist");
let mut contents = Vec::new();
base_file.read_to_end(&mut contents).expect("basefile read error");
let output_file = File::create(OUTPUT48_FILE_NAME)
.expect("output_file fail open");
let mut output_file = std::io::BufWriter::new(output_file);
output_file.write_all(&contents).expect("fail write");
// 画像の読み込み
let img = image::open(FONT48_FILE_NAME).expect("picture file open error");
let img = img.to_rgb();
//let wigth = img.width();
//let height = img.height();
// フォントの切り出し 及び 2値化
let mut fonts = Vec::<[u8; 8]>::new();
for row in 0..16 {
for col in 0..16 {
let mut font = [0u8; 8];
let base_x = col*4;
let base_y = row*8;
for i in 0..7 {
let mut font_row:u8 = 0;
for j in 0..4 {
let pixel = img.get_pixel(base_x+j, base_y+i);
let r=pixel.0[0] as u32;
let g=pixel.0[1] as u32;
let b=pixel.0[2] as u32;
font_row |=
if r+g+b < 128*3 {
1u8 << 3-j
} else {
0
};
}
font[i as usize]=font_row;
}
fonts.push(font);
}
}
// フォント定数値の書き出し
for font in &fonts {
write!(output_file,
"Font48Data([0x{:x},0x{:x},0x{:x},0x{:x},0x{:x},0x{:x},0x{:x},0x{:x}]),\n",
font[0],font[1],font[2],font[3],font[4],
font[5],font[6],font[7]).expect("write value error");
}
// ファイル終端の書き出し
output_file.write_all(b"] );\n").expect("write end terminate");
}
/// 半角フォント
pub struct Font48 ([Font48Data; 256]);
struct Font48Data([u8; 8]);
impl Font48 {
pub fn get_char(&self, code: u8) -> &[u8; 8] {
&self.0[code as usize].0
}
}
pub const FONT48:Font48 = Font48 ( [
/// 全角フォント
pub struct Font88 ([[Font88Data; 94]; 94]);
struct Font88Data([u8; 8]);
impl Font88 {
pub fn get_char(&self, code_hi: u8, code_low: u8) -> &[u8; 8] {
let code_hi = (code_hi - 0xa1) as usize;
let code_low = (code_low - 0xa1) as usize;
&self.0[code_hi][code_low].0
}
}
pub const FONT88:Font88 = Font88 ( [
// [
// Font88Data([1,2,3,4,5,6,7,8]),
// Font88Data([1,2,3,4,5,6,7,8]),
// ...
// ],
// [
// Font88Data([1,2,3,4,5,6,7,8]),
// Font88Data([1,2,3,4,5,6,7,8]),
// ...
// ],
// ...
// ]) ;
このプログラムを実行すると、font48.rsとfont88.rsのふたつのファイルを吐き出してくれます。
これをそのまま、モジュールファイルとして使います。
メインモジュール
さて、やっと終わりが見えてきました。
メインモジュールです。
このモジュールでやっているのは、先に掲載したLチカと大して変わりません。STM32を初期化して、matrix_ledモジュールを使用してLEDへの表示を行っています。
メインループでは、一回表示する度に、cortex_m::asm::wfi()
を呼び出してスリープモードに入っています。このスリープは、TIM11の割込みで解除され、TIM11の割込みルーチンでタイマー起動フラグを立てた後、mainのループに戻ります。これにより、定期的にスクロールを実現しています。
表示文字列のcharsは、EUCコードです。「こんにちは、美都さん」としてあります。
これも調べるのも面倒ですから、
# !/bin/bash
iconv -t EUCJP | od -An -tx1 --endian=big | awk '{c="0x"$1;for(i=2;i<NF;i++) c=c",0x"$i; print c}'
と限りなくワンライナーに近いスクリプトで。
このスクリプトに、標準入力で文字列を与えてあげると、カンマ区切りでEUC-JPコードを返してくれます。
では、仕上げのmain.rsです。
# ![no_std]
# ![no_main]
// pick a panicking behavior
extern crate panic_halt; // you can put a breakpoint on `rust_begin_unwind` to catch panics
// extern crate panic_abort; // requires nightly
// extern crate panic_itm; // logs messages over ITM; requires ITM support
// extern crate panic_semihosting; // logs messages to the host stderr; requires a debugger
use cortex_m_rt::entry;
use cortex_m::interrupt::free;
use stm32f4::stm32f401;
use stm32f4::stm32f401::interrupt;
//use cortex_m_semihosting::dbg;
use misakifont::font88::FONT88;
use matrixled::matrix_led;
const START_TIME:u16 = 1500u16;
const CONTICUE_TIME:u16 = 200u16;
static WAKE_TIMER :WakeTimer = WAKE_TIMER_INIT;
# [entry]
fn main() -> ! {
let device = stm32f401::Peripherals::take().unwrap();
init_clock(&device);
gpio_setup(&device);
spi1_setup(&device);
tim11_setup(&device);
let mut matrix = matrix_led::Matrix::new(&device);
device.GPIOA.bsrr.write(|w| w.bs10().set());
//device.GPIOA.bsrr.write(|w| w.bs11().set());
let chars=[
0xa4,0xb3,0xa4,0xf3,0xa4,0xcb,0xa4,0xc1,0xa4,0xcf,0xa1,0xa2,
0xc8,0xfe,0xc5,0xd4,0xa4,0xb5,0xa4,0xf3,
0xa1,0xa1,0xa1,0xa1,0xa1,0xa1,0xa1,0xa1,
];
device.GPIOA.bsrr.write(|w| w.bs11().set());
let tim11 = &device.TIM11;
tim11.arr.modify(|_,w| unsafe { w.arr().bits(START_TIME) });
tim11.cr1.modify(|_,w| w.cen().enabled());
free(|cs| WAKE_TIMER.set(cs));
let char_count = chars.len()/2;
let mut start_point = 0;
loop {
if free(|cs| WAKE_TIMER.get(cs)) { // タイマー割込みの確認
if start_point==0 {
tim11.arr.modify(|_,w| unsafe { w.arr().bits(START_TIME) });
} else {
tim11.arr.modify(|_,w| unsafe { w.arr().bits(CONTICUE_TIME) });
}
// 漢字の表示位置算出と描画
matrix.clear();
let char_start = start_point / 8;
let char_end = if (start_point % 8)==0 {
char_start+3
} else {
char_start+4
};
let char_end = core::cmp::min(char_end, char_count);
let mut disp_xpos:i32 = -((start_point%8) as i32);
for i in char_start..char_end+1 { // 各漢字の表示
let font = FONT88.get_char(chars[i*2], chars[i*2+1]);
matrix.draw_bitmap(disp_xpos, 0, 8, font);
disp_xpos += 8;
}
matrix.flash_led(); // LED表示の更新
start_point += 1;
if start_point > 8*char_count - 32 {
start_point = 0;
}
free(|cs| WAKE_TIMER.reset(cs));
}
device.GPIOA.bsrr.write(|w| w.br1().reset());
cortex_m::asm::wfi();
device.GPIOA.bsrr.write(|w| w.bs1().set());
}
}
use core::cell::UnsafeCell;
/// TIM11割り込み関数
# [interrupt]
fn TIM1_TRG_COM_TIM11() {
free(|cs| {
unsafe {
let device = stm32f401::Peripherals::steal();
device.TIM11.sr.modify(|_,w| w.uif().clear());
}
WAKE_TIMER.set(cs);
});
}
/// タイマーの起動を知らせるフラグ
/// グローバル イミュータブル変数とする
struct WakeTimer(UnsafeCell<bool>);
const WAKE_TIMER_INIT: WakeTimer = WakeTimer(UnsafeCell::new(false));
impl WakeTimer {
pub fn set(&self, _cs: &cortex_m::interrupt::CriticalSection) {
unsafe { *self.0.get() = true };
}
pub fn reset(&self, _cs: &cortex_m::interrupt::CriticalSection) {
unsafe { *self.0.get() = false };
}
pub fn get(&self, _cs: &cortex_m::interrupt::CriticalSection) -> bool {
unsafe { *self.0.get() }
}
}
unsafe impl Sync for WakeTimer { }
/// システムクロックの初期設定
/// クロック周波数 48MHz
fn init_clock(device : &stm32f401::Peripherals) {
// システムクロック 48MHz
// PLLCFGR設定
// hsi(16M)/8*192/8=48MHz
{
let pllcfgr = &device.RCC.pllcfgr;
pllcfgr.modify(|_,w| w.pllsrc().hsi());
pllcfgr.modify(|_,w| w.pllp().div8());
pllcfgr.modify(|_,w| unsafe { w.plln().bits(192u16) });
pllcfgr.modify(|_,w| unsafe { w.pllm().bits(8u8) });
}
// PLL起動
device.RCC.cr.modify(|_,w| w.pllon().on());
while device.RCC.cr.read().pllrdy().is_not_ready() {
// PLLの安定をただひたすら待つ
}
// フラッシュ読み出し遅延の変更
device.FLASH.acr.modify(|_,w| unsafe {w.latency().bits(1u8)});
// システムクロックをPLLに切り替え
device.RCC.cfgr.modify(|_,w| w.sw().pll());
while !device.RCC.cfgr.read().sws().is_pll() { /*wait*/ }
// APB2のクロックを1/16
//device.RCC.cfgr.modify(|_,w| w.ppre2().div2());
}
/// gpioのセットアップ
fn gpio_setup(device : &stm32f401::Peripherals) {
// GPIOA 電源
device.RCC.ahb1enr.modify(|_,w| w.gpioaen().enabled());
// GPIOA セットアップ
let gpioa = &device.GPIOA;
gpioa.moder.modify(|_,w| w.moder1().output());
gpioa.moder.modify(|_,w| w.moder10().output());
gpioa.moder.modify(|_,w| w.moder11().output());
// SPI端子割付け
gpioa.moder.modify(|_,w| w.moder7().alternate()); // SPI1_MOSI
gpioa.afrl.modify(|_,w| w.afrl7().af5());
gpioa.ospeedr.modify(|_,w| w.ospeedr7().very_high_speed());
gpioa.otyper.modify(|_,w| w.ot7().push_pull());
gpioa.moder.modify(|_,w| w.moder5().alternate()); // SPI1_CLK
gpioa.afrl.modify(|_,w| w.afrl5().af5());
gpioa.ospeedr.modify(|_,w| w.ospeedr5().very_high_speed());
gpioa.otyper.modify(|_,w| w.ot5().push_pull());
gpioa.moder.modify(|_,w| w.moder4().output()); // NSS(CS)
gpioa.ospeedr.modify(|_,w| w.ospeedr4().very_high_speed());
gpioa.otyper.modify(|_,w| w.ot4().push_pull());
}
/// SPIのセットアップ
fn spi1_setup(device : &stm32f401::Peripherals) {
// 電源投入
device.RCC.apb2enr.modify(|_,w| w.spi1en().enabled());
let spi1 = &device.SPI1;
spi1.cr1.modify(|_,w| w.bidimode().unidirectional());
spi1.cr1.modify(|_,w| w.dff().sixteen_bit());
spi1.cr1.modify(|_,w| w.lsbfirst().msbfirst());
spi1.cr1.modify(|_,w| w.br().div4()); // 基準クロックは48MHz
spi1.cr1.modify(|_,w| w.mstr().master());
spi1.cr1.modify(|_,w| w.cpol().idle_low());
spi1.cr1.modify(|_,w| w.cpha().first_edge());
spi1.cr1.modify(|_,w| w.ssm().enabled());
spi1.cr1.modify(|_,w| w.ssi().slave_not_selected());
}
/// TIM11のセットアップ
fn tim11_setup(device : &stm32f401::Peripherals) {
// TIM11 電源
device.RCC.apb2enr.modify(|_,w| w.tim11en().enabled());
// TIM11 セットアップ
let tim11 = &device.TIM11;
tim11.psc.modify(|_,w| w.psc().bits(48_000u16 - 1)); // 1ms
tim11.dier.modify(|_,w| w.uie().enabled());
unsafe {
cortex_m::peripheral::NVIC::unmask(
stm32f401::interrupt::TIM1_TRG_COM_TIM11);
}
}
流石に、長いので、各処理を全部関数に切り分けています。
各関数の処理内容は、前回、Lチカを書いたときと全く同じです。
もし、前回のflash.cfgを使ってアップロードするときは・・・
今回、CPUをスリープモードに入れています。CPUは大半がスリープしています。このため、flash.cnfの起動が素直に出来ません。
CPUがスリープしているせいで、openocdがcpuへの接続に失敗するのです。
対策は、openocd -f openocd.cfg
を起動して、
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
adapter speed: 2000 kHz
adapter_nsrst_delay: 100
none separate
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : Unable to match requested speed 2000 kHz, using 1800 kHz
Info : clock speed 1800 kHz
Info : STLINK v2 JTAG v36 API v2 SWIM v26 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 3.266272
Info : stm32f4x.cpu: hardware has 6 breakpoints, 4 watchpoints
最後の行が、この様になるまで、ボードのリセットを何度か押してみます。
こうなったら、別のコンソールで、
$ telnet localhost 4444
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Open On-Chip Debugger
> reset halt
Unable to match requested speed 2000 kHz, using 1800 kHz
Unable to match requested speed 2000 kHz, using 1800 kHz
adapter speed: 1800 kHz
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x0800079e msp: 0x20018000
>
のように、telnet localhost 4444
で、CPUに接続して、reset halt
コマンドを発行します。
これで、CPUがhaltしますので、このtelnetと先のopenocdを終了させて、openocd -f flash.cfg -c "flash_elf &prog"
で書き込みできます。ただ、なぜか、エラーになることが有ります。エラーになったらもう一度やってみてください。大概、1回か2回やってみれば書き込めます。
Hello Worldにしては、異常に長い道のりでしたが、これで終わりです。
長々読んでいただいてありがとうございました。
参考文献
1. MAX7219/MAX7221 シリアルインタフェース、8桁LEDディスプレイドライバ
2. STM32F401xD STM32F401xE データシート 【英語】
3. STM32F401xB/C and STM32F401xD/E advanced ARM®-based 32-bit MCUs 【RM0368】【日本語有り】
3. 8×8 ドット日本語フォント「美咲フォント」
4.