前回は、 を参考にして、苦労の末、QEMU (ターゲットは STM32F4-Discovery)を立ち上げました。QEMU とは、実機が無くてもプロセッサ(CPU、評価ボード)を動かせるエミュレータです。
今回は、動作させたサンプルソースの内容の確認と、機能の確認をしていきます。
それでは、やっていきます。
参考文献
STM32F4 のマニュアル
下記リンクのドキュメント→リファレンスマニュアル、ドキュメント→プログラミングマニュアルなどを参照してください。最近は、マニュアルが日本語化されていて、とても便利です。
デザイン/サポート | STM32, STM8ファミリはSTの32bit/8bit汎用マイクロコントローラ製品
はじめに
「QEMUを動かす」の記事一覧です。良かったら参考にしてください。
・第2回:STM32(ARM Cortex-M)をQEMUで動かす(ソースコード確認編) ← 今回
・第3回:STM32(ARM Cortex-M)をQEMUで動かす(スタートアップルーチン編)
・第4回:STM32(ARM Cortex-M)をQEMUで動かす(リンカスクリプト編)
・第5回:STM32(ARM Cortex-M)のELFファイルの内容を確認する
・第6回:STM32(ARM Cortex-M)のELFファイル⇔バイナリの変換を行う
・第7回:STM32(ARM Cortex-M)のバイナリから構築したELFファイルをQEMUで動かす
・第8回:QEMUのビルドに必要なxpm(xPack Project Manager)について学ぶ
・第9回:QEMUをソースからビルドして動かす
・第10回:QEMUのソースコードを変更してSTM32の動作を変える
今回は、サンプルソースの内容を確認していきます。
サンプルソースは、Interfaceのホームページからダウンロードします。
下記の「7月号 仮想から実機まで マイコン開発入門」の「特集 第3部第1章 エミュレータQEMUを活用した開発の手引き」の「関連ファイル一式」をダウンロードします。
それでは、やっていきます!
メイン処理(PlantUML)
main() のフローチャートを書きました。
各初期化処理があり、あとは無限ループで、ステートマシンで各処理が実行されるという感じです。

フローチャートの PlantUML のソースも貼っておきます。
@startuml #lightblue:main(); :initialise_monitor_handles()| floating note right: セミホスティング機能初期化 :led_init()| floating note right: LED初期化 :button_init()| floating note right: ボタン初期化 :systick_init()| floating note right: SysTickの初期化 while(loop forever) :time = g_system_time - lap_time; switch (state?) case ( WAINTING ) :waiting()| case ( RUNNING ) :running()| case ( STOP_OK ) :stop_ok()| case ( STOP_NG ) :stop_ng()| endswitch endwhile -[hidden]-> @enduml
では、詳細を順番に見ていきます。
board.c
章立ての関係上、board.c を先に見ますが、main() だけは、先にざっと見ておいた方が分かりやすいと思います。
board.c は、評価ボードの処理が書かれています。
6つの関数があります。
led_init()
led_init() は、main関数の先頭にある初期化処理の一つで、LEDのポートの初期化をしています。
4つのI/Oポートを使っていて、それを4つのLEDに割り当ててるようです。
細かい内容は省きますが、I/Oポートを出力ポートに設定しています。
/** * LEDの初期化 * User LED は、以下のI/Oポートに接続されている * LD4 (green) : PD12 * LD3 (orange): PD13 * LD5 (red) : PD14 * LD6 (blue) : PD15 */ void led_init(void) { // IO port D のクロックを有効化 (*REG_RCC_AHB1ENR) |= RCC_AHB1ENR_GPIODEN; // PD12, PD13, PD14, PD15を出力モードに設定 REG_GPIOD->MODER |= ((GPIO_MODER_OUTPUT << 30) | (GPIO_MODER_OUTPUT << 28) | (GPIO_MODER_OUTPUT << 26) | (GPIO_MODER_OUTPUT << 24)); // PD12, PD13, PD14, PD15の出力スピードをハイスピードに設定 REG_GPIOD->OSPEEDR |= ((GPIO_OSPEEDR_HIGH << 30) | (GPIO_OSPEEDR_HIGH << 28) | (GPIO_OSPEEDR_HIGH << 26) | (GPIO_OSPEEDR_HIGH << 24)); }
led_set()
led_set() は、引数で渡された mask のセットされているビットに応じて、LED を点灯させて、セットされてないビットの LED は消灯させる処理です。
/** * LEDの点灯/消灯制御 * maskのビット値に対応するLEDを点灯し、それ以外を消灯する * - LED_GREEN (0x1) * - LED_ORANGE (0x2) * - LED_RED (0x4) * - LED_BLUE (0x8) */ void led_set(uint32_t mask) { uint32_t odr = REG_GPIOD->ODR; odr &= ~(0xF << 12); odr |= ((mask & 0xF) << 12); REG_GPIOD->ODR = odr; }
systick_init()
ソースコードの記載されている順番と違いますが、先にタイマを見ていきます。
systick_init() は、main関数の先頭にある初期化処理の一つで、タイマの初期化をしています。
LOAD_VALUE の定義を先に載せておきます。
g_system_time は、ms単位のシステム時刻を保持しているようです。
10msごとにダウンカウンタのタイマが割り込みを出して、後述の割り込みハンドラで、g_system_time に 10 を加算します。
よって、10msごとに割り込みを出すように、ダウンカウンタを設定する必要があります。LOAD_VALUE は、10ms を知らせるカウンタ値です。クロックは 16MHz のようです。16MHz というのは 1秒間のクロック数なので、それを ms単位にするために 1000 で割って、10ms単位にするために 10倍しています。
// システム時刻 (ms) uint32_t g_system_time = 0; // チック間隔 #define TICK_MS (10) // カウントダウンの開始値 // クロック周波数とチック間隔から設定値を算出 // LOAD_VALUE: clock / 1000 * tick = 16MHz / 1000 * 10ms #define LOAD_VALUE (16000 * TICK_MS)
上で説明したように、タイマの初期化をしています。
/** * SysTickの初期化 */ void systick_init(void) { REG_SYSTICK->CTRL = 0x00000000UL; // 状態情報を初期化 REG_SYSTICK->VAL = 0UL; // カウント値を初期化 REG_SYSTICK->LOAD = LOAD_VALUE - 1UL; // カウントダウンの開始値 REG_SYSTICK->CTRL = 0x00000007UL; // 有効化(カウントダウン開始) }
systick_handler()
systick_handler() は、上のタイマの割り込みハンドラです。
上で説明したように、割り込みハンドラで、10msを加算しています。if文でレジスタの値を確認しているのは、念のためでしょうか。無くても動くような気もしますね。
/** * SysTick割込みハンドラ */ // 割込みハンドラの関数には、interrupt属性を指定する __attribute__((interrupt)) void systick_handler(void) { if ((REG_SYSTICK->CTRL & 0x00010000UL) != 0UL) { // LOADの値からカウントダウンして、 // カウントが0(CTRLのCOUNTFLAGビットが1)になったら、 // システム時刻をインクリメントする g_system_time += TICK_MS; } }
button_init()
button_init() は、main関数の先頭にある初期化処理の一つで、入力ポートの初期化をしています。
/** * Button (PA0) の初期化 */ void button_init(void) { // IO port A のクロックを有効化 (*REG_RCC_AHB1ENR) |= RCC_AHB1ENR_GPIOAEN; }
button_pushed()
button_pushed() は、ボタンが押されたときに1回だけ 1 を返し、それ以外は 0 を返します(ボタンを押しっぱなしにしても 1 を返すのは1回だけ)。
チャタリングの対策が入ってます。チャタリングというのは、ボタンが押されるときは、「押されてない状態→押された状態」が、1回で移行せず、しばらくの時間は、押されてない状態と押された状態を行き来することを言います。
チャタリング対策は、上記のような動きをしても正しくボタンの状態を認識するため、ある程度の時間をかけて、「押されてない状態→押された状態」を認識するための処理です。
具体的な処理の内容を見てみると、前回この関数が呼ばれたときから 20ms が経過していたら、if文の中に入ります。前回のボタンの状態が 0(ボタンが押されない状態) で、今回のボタンの状態が 1(ボタンが押されている状態)のときにだけ 1を返す処理になっています。
// チャタリング防止時間 (ms) #define BOUNCE_TIME (20) /** * Button (PA0) の状態取得 (立ち上がりエッジ) */ uint32_t button_pushed(void) { static uint32_t prev_time = 0; static uint32_t prev_state = 0; uint32_t state = (REG_GPIOA->IDR & 1U); uint32_t result = 0; if ((prev_time + BOUNCE_TIME) < g_system_time) { // チャタリング防止時間を経過したとき if ((!prev_state) && (state)) { // 0 -> 1 に変化したときのみ、1と判定. result = 1; } prev_time = g_system_time; prev_state = state; } return result; }
board.c の 6つの関数は以上です。
main.c
次は、main.c を見ていきます。
全体の流れを見るために、main() から見ていきます。
main()
先に、main.c の、いくつかのenum定義、変数を載せておきます。
// ルーレットの状態 typedef enum state { WAINTING = 0, // 開始待ち RUNNING = 1, // ルーレット回転中 STOP_OK = 2, // 停止(成功) STOP_NG = 3, // 停止(失敗) } state_t; static state_t state = WAINTING; // 現在の状態 static uint32_t speed = 1; // 現在の回転速度 static uint32_t led_id = 0; // RUNNING状態で点灯中のLED static uint32_t lap_time = 0; // 各状態開始時のシステム時刻 static uint32_t time = 0; // 各状態での経過時間
これを踏まえて、見ていきます。
コメントが書かれているので、分かりやすいです。
いくつかの初期化を行い、メインループ(無限ループ)があります。
g_system_time はシステム時刻で、lap_time は、状態遷移などのイベント発生時のシステム時刻を保持しています。つまり、time は、イベント発生からの時間が設定されます。
ステートマシンになっていて、「開始待ち」、「ルーレット回転中」、「停止(成功)」、「停止(失敗)」の4つの状態があります。
int main(void) { initialise_monitor_handles(); // セミホスティング機能の初期化 led_init(); // LEDの初期化 button_init(); // Buttonの初期化 systick_init(); // SysTickの初期化 // メインループ while (1) { time = g_system_time - lap_time; switch (state) { case WAINTING: waiting(); // 開始待ち break; case RUNNING: running(); // ルーレット回転中 break; case STOP_OK: stop_ok(); // 停止(成功) break; case STOP_NG: default: stop_ng(); // 停止(失敗) break; } } // ここには到達しない return 0; }
次は、ステートごとの各処理を見ていきます。
waiting()
ステートが「開始待ち」の処理です。
全体が if else で分かれています。コメントによると、ボタンが押されるまでは、if が真のところが動作します。前回確認したLEDの点滅のところです。
speed の初期値は 1 なので、1秒に1回、LEDの緑が点灯、消灯が切り替わります。
ボタンが押されたら、lap_time に現在のシステム時刻を格納して、ステートを「ルーレット回転中」に遷移させます。
static void waiting(void) { if (!button_pushed()) { // ボタンが押されるまで、LEDを点滅 if ((time / (1000 / speed)) % 2) { led_set(LED_GREEN); } else { led_set(0); } } else { // ボタンが押されたら、ルーレット開始 lap_time = g_system_time; state = RUNNING; } }
running()
ステートが「ルーレット回転中」の処理です。
「開始待ち」処理と同じように、ボタンが押されるまでは、if文の真の方を通ります。今度は、点灯させる LED を切り替えています。enumの値を見ると、緑→橙→赤→青→緑... の順番のようです。
ボタンが押されると、そのときのシステム時刻を lap_time に保存して、LEDが緑の状態でボタンが押されたなら「停止(成功)」にステートを遷移させて、LEDが緑以外の状態でボタンが押されたなら「停止(失敗)」にステートを遷移させます。
static void running(void) { if (!button_pushed()) { // ボタンが押されるまで、回転速度に応じた時間で、LEDを点滅 led_id = ((time / (1000 / speed)) % 4); led_set(1 << led_id); } else { // ボタンが押されたら、LEDがLED_GREENか否か判定し、停止状態に遷移 lap_time = g_system_time; if ((1 << led_id) == LED_GREEN) { state = STOP_OK; } else { state = STOP_NG; } } }
stop_ok()
ステートが「停止(成功)」の処理です。
3秒間経過するまでは、if文の真の方を通り、3秒間経過するとif文の偽の方を通ります。
3秒間経過するまでは、全てのLEDが、100msごとに点滅します。
3秒間経過すると、speed をインクリメント(スピードアップ)して、lap_time に現在のシステム時刻を格納して、ステートを「開始待ち」に遷移させます。
static void stop_ok(void) { // 3秒間LEDを点滅後、回転速度を上げて、WAITINGに遷移 if (time < 3000) { if ((time / 100) % 2) { led_set(LED_GREEN | LED_ORANGE | LED_RED | LED_BLUE); } else { led_set(0); } } else { speed++; printf("speed up!! - %d\n", speed); lap_time = g_system_time; state = WAINTING; } }
stop_ng()
ステートが「停止(失敗)」の処理です。
「停止(成功)」の処理とほとんど同じです。違いは、「停止(成功)」の処理では、speed をインクリメント(スピードアップ)してましたが、「停止(失敗)」では、speed を 1 に戻しています。
また、3秒間経過するまでは、全てのLEDが、点滅ではなく、点灯しっぱなしです。
つまり、緑の点灯中にうまくボタンが押されたら、ルーレットの回転速度がどんどん上がります。緑の点灯中にボタンが押せなかったら、ルーレットの回転速度はリセットされるという動きをするようです。
static void stop_ng(void) { // 3秒間LEDを点灯後、回転速度をリセットし、WAITINGに遷移 if (time < 3000) { led_set(LED_GREEN | LED_ORANGE | LED_RED | LED_BLUE); } else { speed = 1; printf("speed reset - %d\n", speed); lap_time = g_system_time; state = WAINTING; } }
ルーレットゲームを実行してみる
それでは、実際にボタンを押して、機能を確認していきます。
まずは、緑の点灯中に、うまくボタンが押せて、ルーレットの回転速度がスピードアップしたところです。動画はループしてるので分かりにくいかもしれませんが、ルーレットの回転速度が2倍になっているはずです。

次に、かなりルーレットの回転速度が速くて、失敗したところです。

おわりに
今回は、ソースコードの内容の確認と、実際にルーレットゲームを実行してみました。
今回は C言語のソースコードを見ましたが、次回は、スタートアップルーチン(アセンブラ)を見ていきます。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。