STM32(ARM Cortex-M)をQEMUで動かす(ソースコード確認編) - 土日の勉強ノート

土日の勉強ノート

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

STM32(ARM Cortex-M)をQEMUで動かす(ソースコード確認編)

前回は、Interface 2022年 7月号 を参考にして、苦労の末、QEMU (ターゲットは STM32F4-Discovery)を立ち上げました。QEMU とは、実機が無くてもプロセッサ(CPU、評価ボード)を動かせるエミュレータです。

今回は、動作させたサンプルソースの内容の確認と、機能の確認をしていきます。

それでは、やっていきます。

参考文献

STM32F4 のマニュアル

下記リンクのドキュメント→リファレンスマニュアル、ドキュメント→プログラミングマニュアルなどを参照してください。最近は、マニュアルが日本語化されていて、とても便利です。

デザイン/サポート | STM32, STM8ファミリはSTの32bit/8bit汎用マイクロコントローラ製品

はじめに

「QEMUを動かす」の記事一覧です。良かったら参考にしてください。

QEMUを動かすの記事一覧

今回は、サンプルソースの内容を確認していきます。

サンプルソースは、Interfaceのホームページからダウンロードします。

下記の「7月号 仮想から実機まで マイコン開発入門」の「特集 第3部第1章 エミュレータQEMUを活用した開発の手引き」の「関連ファイル一式」をダウンロードします。

www.cqpub.co.jp

それでは、やっていきます!

メイン処理(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言語のソースコードを見ましたが、次回は、スタートアップルーチン(アセンブラ)を見ていきます。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。