前回は、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ポートを出力ポートに設定しています。
void led_init(void)
{
(*REG_RCC_AHB1ENR) |= RCC_AHB1ENR_GPIODEN;
REG_GPIOD->MODER |= ((GPIO_MODER_OUTPUT << 30) | (GPIO_MODER_OUTPUT << 28) |
(GPIO_MODER_OUTPUT << 26) | (GPIO_MODER_OUTPUT << 24));
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 は消灯させる処理です。
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倍しています。
uint32_t g_system_time = 0;
#define TICK_MS (10)
#define LOAD_VALUE (16000 * TICK_MS)
上で説明したように、タイマの初期化をしています。
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文でレジスタの値を確認しているのは、念のためでしょうか。無くても動くような気もしますね。
__attribute__((interrupt)) void systick_handler(void)
{
if ((REG_SYSTICK->CTRL & 0x00010000UL) != 0UL) {
g_system_time += TICK_MS;
}
}
button_init()
は、main関数の先頭にある初期化処理の一つで、入力ポートの初期化をしています。
void button_init(void)
{
(*REG_RCC_AHB1ENR) |= RCC_AHB1ENR_GPIOAEN;
}
button_pushed()
は、ボタンが押されたときに1回だけ 1 を返し、それ以外は 0 を返します(ボタンを押しっぱなしにしても 1 を返すのは1回だけ)。
チャタリングの対策が入ってます。チャタリングというのは、ボタンが押されるときは、「押されてない状態→押された状態」が、1回で移行せず、しばらくの時間は、押されてない状態と押された状態を行き来することを言います。
チャタリング対策は、上記のような動きをしても正しくボタンの状態を認識するため、ある程度の時間をかけて、「押されてない状態→押された状態」を認識するための処理です。
具体的な処理の内容を見てみると、前回この関数が呼ばれたときから 20ms が経過していたら、if文の中に入ります。前回のボタンの状態が 0(ボタンが押されない状態) で、今回のボタンの状態が 1(ボタンが押されている状態)のときにだけ 1を返す処理になっています。
#define BOUNCE_TIME (20)
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)) {
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;
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();
button_init();
systick_init();
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()) {
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_id = ((time / (1000 / speed)) % 4);
led_set(1 << led_id);
} else {
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)
{
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)
{
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言語のソースコードを見ましたが、次回は、スタートアップルーチン(アセンブラ)を見ていきます。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。