開発・デバッグ・テスト

エミュレータ / シミュレータ

実機なしで組み込みシステムの動作を再現する環境。

概要

エミュレータ(Emulator)とシミュレータ(Simulator)は、どちらも「実際のハードウェアを用意しなくても、組み込みシステムの動作を検証できる」ソフトウェア環境を指す言葉として使われることが多い。しかし厳密には両者の意味は異なり、開発現場でも混同されがちな用語である。

エミュレータは、対象ハードウェアの振る舞いをソフトウェアで忠実に再現するツールである。CPUのレジスタ操作、メモリマップ、周辺回路(ペリフェラル)のタイミング動作まで含めて模倣するため、実機向けにコンパイルしたファームウェアをそのままバイナリとして動作させられることが大きな特徴だ。QEMU や Renode がその代表例であり、ARM Cortex-M や RISC-V コアを丸ごとエミュレートして組み込みアプリケーションを実行できる。

シミュレータは、システムの動作を抽象モデルとして模倣する。実際のバイナリを動かすのではなく、アルゴリズムや処理ロジックをホスト PC 上で動作する形に書き直し(あるいは条件付きコンパイルで差し替えて)、システムの入出力やタイミングを数値的に検証する。組み込み文脈では、センサ値や通信プロトコルをソフトウェア上でスタブ化し、制御アルゴリズムの正確さを確かめる手法がシミュレーションにあたる。

どちらのアプローチも、量産前の段階でバグを早期発見し、開発コストを削減するために欠かせない技術である。また、実機が入手困難な局面——半導体不足、先行開発、希少デバイスのポーティング——では特に重要性が増す。本稿では両者の違いを整理したうえで、代表的なツール、動作原理、実践的な活用方法を詳説する。


歴史・背景

黎明期:ソフトウェアシミュレータの登場

エミュレータ・シミュレータという概念が組み込み開発に本格的に持ち込まれたのは 1980 年代後半から 1990 年代にかけてのことだ。当時の組み込みプロセッサ(Intel 8051、Motorola 68000 系など)向けに、ICE(In-Circuit Emulator)と呼ばれるハードウェアデバッガが普及していた。ICE は本物の CPU チップを取り除いてプローブを差し込み、外部から内部状態を監視するという手法で、価格は数百万円を超えることも珍しくなかった。

ソフトウェアシミュレータは、この高価な ICE の代替として登場した。CPU の命令セットをソフトウェアで再現し、PC 上でデバッグできるようにしたもので、精度は ICE に劣るものの低コストで利用できた。代表的なのは Keil MDK(当時の Keil Elektronik)に同梱されていた 8051 シミュレータで、学習用途や初期デバッグに広く使われた。

QEMU の登場と汎用エミュレータの普及

2003 年、Fabrice Bellard によって QEMU(Quick EMUlator)が公開された。QEMU は TCG(Tiny Code Generator)と呼ばれる動的バイナリ変換技術を採用し、x86 以外のアーキテクチャをホスト PC 上で高速にエミュレートすることを可能にした。Linux カーネルの ARM ポーティングやクロス開発ツールチェーンのテストに活用されるようになり、やがて組み込み分野にも広まった。

2010 年代に入ると、IoT デバイスの急増とともに組み込みシステムの多様化が進んだ。STM32 や ESP32 といった安価なマイコンが大量に普及する一方で、開発チームは複数ハードウェアプラットフォームに対応しなければならない状況が生まれた。こうした背景から、特定の MCU ボードを丸ごとエミュレートする専用ツールの需要が高まり、Renode(Antmicro 社)、avr-emu、ARM Fixed Virtual Platforms(FVP)などが登場した。

CI/CD との統合

2020 年代に入ると、エミュレータは単なる開発補助ツールにとどまらず、CI/CD パイプラインの一部として組み込まれるようになった。GitHub Actions や GitLab CI 上で QEMU を動かし、プルリクエストごとにファームウェアの自動テストを実行するワークフローが組み込み業界でも普及しつつある。これにより、実機がなくてもコードのリグレッションを継続的に検出できる環境が整いつつある。


技術仕様

エミュレータとシミュレータの違い

観点エミュレータシミュレータ
対象バイナリ実機向けバイナリをそのまま実行ホスト向けに再コンパイル/抽象化が必要
精度ハードウェアに近い(タイミングも含む)アルゴリズム的な動作を近似
速度実機より遅いことが多い高速(ネイティブ実行に近い)
ペリフェラル再現GPIO、UART、SPI なども模倣可能スタブ化が一般的
セットアップ難易度やや高い(MCU 定義が必要)低い(ソースコード変更で対応)
主な用途ブートシーケンス・ドライバ検証制御アルゴリズム・ロジック検証
代表ツールQEMU、Renode、ARM FVPSimulink、自作テストハーネス

主要ツール一覧

ツール名対応アーキテクチャライセンス特徴
QEMUARM、RISC-V、x86、MIPS などGPL v2汎用・広大なエコシステム
RenodeARM Cortex-M/A、RISC-V などMIT組み込み特化・CI 向け
ARM FVPARM Cortex-M/A(全世代)商用/無償版ありARM 公式・高精度
Keil MDK SimulatorARM Cortex-M商用IDE 統合・初心者向け
avr-emu / simavrAVR(ATmega/ATtiny)GPLArduino 互換ターゲット
GDB Simulator (sim)ARM、MIPS、RISC-V などGPLGDB 組み込み・軽量
OVPsimARM、MIPS、PowerPC など商用/無償版あり高速・命令精度高
Proteus VSM8051、PIC、AVR、ARM商用回路シミュレータ統合

QEMU がサポートする代表的な組み込みボード

ボード名MCU/CPU主なペリフェラル
netduinoplus2STM32F4UART、SPI、I2C、GPIO
stm32vldiscoverySTM32F1UART、GPIO
mps2-an385ARM Cortex-M3UART、Timer、GPIO
microbitNordic nRF51UART、GPIO、I2C
sifive_eRISC-V E51UART、GPIO
lm3s6965evbARM Cortex-M3UART、Timer、ADC

動作原理

エミュレータの動作原理

エミュレータは、対象 CPU の命令セットアーキテクチャ(ISA)を解釈・実行するエンジンを中核に持つ。実行方式には大きく分けて次の三種類がある。

インタープリタ方式は、ゲスト CPU の命令を 1 命令ずつ読み取り、ホスト CPU 上で対応する処理を実行するシンプルな方式だ。実装が容易で精度が高い一方、オーバーヘッドが大きく低速になりやすい。初期の組み込みシミュレータや学習用エミュレータに多い。

**動的バイナリ変換(JIT コンパイル)**は、ゲスト命令のブロックをホスト CPU のネイティブコードに変換・キャッシュして実行する方式だ。QEMU の TCG がこれにあたる。初回変換のコストはかかるが、繰り返し実行されるコードは高速に動作する。

ハードウェア支援仮想化は、x86 の VT-x や ARM の EL2 といった CPU 機能を活用してゲストコードをほぼネイティブ速度で実行する方式だ。主にサーバ仮想化(KVM など)で使われるが、同一アーキテクチャの組み込み Linux 環境をエミュレートする際にも恩恵がある。

QEMU はこれらを組み合わせて使用する。Cortex-M などの異アーキテクチャをエミュレートする場合は TCG が使われ、ARM ホスト上で ARM Cortex-A アプリを動かす場合は KVM が活用されることもある。

ペリフェラルのモデリング

MCU のエミュレーションで難しいのは、CPU コアだけでなく周辺回路(UART、SPI、I2C、タイマ、DMA、ADC など)のタイミング動作も再現しなければならない点だ。QEMU はペリフェラルを「デバイスモデル」として C 言語で実装し、メモリマップドレジスタへのアクセスをインターセプトして対応する処理を行う仕組みを持つ。

Renode はさらに一歩進んで、ペリフェラルの振る舞いを C# で記述した「モデルファイル」と、ボードの配線を記述した「プラットフォームファイル(.repl)」で分離管理する設計を採用している。これにより、MCU ベンダが提供するモデルを再利用しやすく、CI 環境での活用も容易だ。

シミュレータの動作原理

ソフトウェアシミュレータは、実際のバイナリを動かすのではなく、ハードウェアに依存する部分をスタブ(テスト用の代替実装)に置き換えてコンパイルする。典型的なアプローチは次のとおりだ。

  1. ハードウェア抽象化層(HAL)を設ける。GPIO や UART の操作を直接レジスタに書くのではなく、hal_gpio_write() などの関数を経由する。
  2. 本番ビルドでは実際のレジスタ操作を行う実装をリンクし、テストビルドではモック実装(ログ出力や値の記録だけを行う)をリンクする。
  3. ホスト PC 上でネイティブコンパイルして実行し、ユニットテストフレームワーク(Unity、CppUTest など)で検証する。

この手法は実行速度が速く、IDE のデバッガを使ったステップ実行も容易だが、タイミングに関わるバグ(割り込みのレース条件、DMA 転送中のバッファ競合など)は検出できない。


用途・ユースケース

1. ファームウェアの初期開発とデバッグ

ハードウェアの試作品(評価ボード)が届く前に、ファームウェアの基本動作を検証したい場合にエミュレータが活躍する。UART 通信の確認、ブートシーケンスのデバッグ、フラッシュメモリへの書き込みロジックのテストなどをエミュレータ上で先行して行うことで、評価ボード到着後の作業を大幅に短縮できる。

2. CI/CD パイプラインへの組み込み

GitHub Actions や GitLab CI などの CI 環境に QEMU や Renode を組み込むことで、コードのプッシュ・マージのたびにファームウェアの自動テストを実行できる。実機を CI サーバに接続するよりも管理コストが低く、テストの並列実行も容易だ。クロスコンパイルで生成したバイナリをそのままエミュレータで実行するため、ツールチェーンの整合性確認にもなる。

3. セキュリティ解析・ファジング

エミュレータは組み込みファームウェアのセキュリティ解析にも用いられる。QEMU を基盤にした AFL++(ファジングツール)と組み合わせることで、実機なしに組み込みバイナリの脆弱性を探索する手法(QEMU モード AFL)が研究・実践されている。また、Firmadyne や FirmAE などのフレームワークは、IoT 機器のファームウェアを QEMU 上で自動起動し、ネットワークサービスの挙動を解析する。

4. 教育・トレーニング

入手困難な実機の代わりとして、教育現場でエミュレータを活用するケースが増えている。Renode は公式チュートリアルに対応したサンプルを多数提供しており、ARM Cortex-M の割り込み処理や RTOS スケジューリングを実機なしに体験できる。RTOS の動作を可視化するデモも用意されており、初学者が概念を理解するのに役立つ。

5. ポーティング作業の初期検証

既存のファームウェアを新しい MCU に移植する際、ターゲット MCU の評価ボードより先にエミュレータで動作確認を行うことで、アーキテクチャ依存の問題(エンディアン、アライメント、割り込みベクタのレイアウト差異など)を早期に洗い出せる。

6. HIL テストとの役割分担

HIL(Hardware-in-the-Loop)テストは実機と外部シミュレータを組み合わせる手法だが、ソフトウェアエミュレータは HIL の前段階として機能する。エミュレータで基本動作を確認し、HIL で物理的なセンサ・アクチュエータとの統合を検証するという二段階のアプローチが合理的だ。


実装・開発のポイント

QEMU で STM32F4 を動かす基本手順

以下は、ARM GNU ツールチェーンでビルドしたファームウェアを QEMU の STM32 エミュレーション(netduinoplus2 ボード)で実行する例だ。

# ARM GNU ツールチェーンのインストール(macOS)
brew install --cask gcc-arm-embedded

# QEMU のインストール
brew install qemu

# ファームウェアのビルド(CMake + arm-none-eabi-gcc)
mkdir build && cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-none-eabi.cmake
make -j$(nproc)

# QEMU で実行(netduinoplus2 は STM32F4 ベース)
qemu-system-arm \
  -machine netduinoplus2 \
  -cpu cortex-m4 \
  -nographic \
  -semihosting-config enable=on,target=native \
  -kernel build/firmware.elf

UART 出力を確認するには -serial stdio オプションを追加する。-semihosting-config は、printf() などの標準出力をホスト PC のターミナルに転送するセミホスティング機能を有効にする。

GDB を使ったリモートデバッグ

QEMU は GDB リモートデバッグプロトコルをサポートしており、IDE の GDB クライアントや CLI から接続してステップ実行・ブレークポイントを設定できる。

# QEMU をデバッグモードで起動(ポート 1234 で待機)
qemu-system-arm \
  -machine netduinoplus2 \
  -cpu cortex-m4 \
  -nographic \
  -semihosting-config enable=on,target=native \
  -kernel build/firmware.elf \
  -S -gdb tcp::1234
# -S: 起動直後に停止して GDB の接続を待つ
# -gdb tcp::1234: GDB リモートプロトコルをポート 1234 で公開

# 別ターミナルで GDB を起動して接続
arm-none-eabi-gdb build/firmware.elf
(gdb) target remote localhost:1234
(gdb) break main
(gdb) continue
(gdb) next
(gdb) info registers

VS Code の cortex-debug 拡張と組み合わせれば、グラフィカルなデバッグ環境を構築できる。.vscode/launch.json に以下を追加する。

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "QEMU Debug",
      "type": "cortex-debug",
      "request": "launch",
      "servertype": "external",
      "gdbTarget": "localhost:1234",
      "executable": "${workspaceFolder}/build/firmware.elf",
      "runToEntryPoint": "main",
      "showDevDebugOutput": "none",
      "preLaunchTask": "Start QEMU"
    }
  ]
}

Renode を使った STM32 エミュレーション

Renode はより組み込みフレンドリーな設計で、ボード定義ファイル(.repl)とスクリプト(.resc)でエミュレーション環境を宣言的に記述できる。

# stm32f4_demo.resc(Renode スクリプト)
using sysbus

# STM32F4 Discovery ボードのプラットフォームをロード
mach create "stm32f4-discovery"
machine LoadPlatformDescription @platforms/boards/stm32f4_discovery.repl

# ファームウェアをロード
sysbus LoadELF @firmware.elf

# UART モニタを設定
showAnalyzer sysbus.usart2

# 実行開始
start
# Renode CLI でスクリプトを実行
renode stm32f4_demo.resc

Renode の強みはネットワーク通信のエミュレーションにある。複数の仮想ボードを同一ネットワークに接続してマルチノードのプロトコルテスト(MQTT、CoAP など)を行うことができる。

HAL を使ったシミュレータ対応コードの書き方

ファームウェアをシミュレータ上で動かすためには、ハードウェア依存部分を HAL で抽象化することが重要だ。以下に典型的な構成例を示す。

/* hal_gpio.h — ハードウェア抽象化層のインタフェース */
#ifndef HAL_GPIO_H
#define HAL_GPIO_H

#include <stdint.h>

typedef enum {
    HAL_GPIO_PIN_RESET = 0,
    HAL_GPIO_PIN_SET   = 1,
} HAL_GPIO_PinState;

void     hal_gpio_init(void);
void     hal_gpio_write(uint32_t pin, HAL_GPIO_PinState state);
HAL_GPIO_PinState hal_gpio_read(uint32_t pin);

#endif /* HAL_GPIO_H */
/* hal_gpio_stm32.c — STM32 向け実装(実機ビルド用) */
#include "hal_gpio.h"
#include "stm32f4xx_hal.h"

void hal_gpio_write(uint32_t pin, HAL_GPIO_PinState state) {
    HAL_GPIO_WritePin(GPIOA, (uint16_t)pin,
                      (GPIO_PinState)state);
}

HAL_GPIO_PinState hal_gpio_read(uint32_t pin) {
    return (HAL_GPIO_PinState)HAL_GPIO_ReadPin(GPIOA, (uint16_t)pin);
}
/* hal_gpio_sim.c — シミュレータ向けモック実装(ホスト PC ビルド用) */
#include "hal_gpio.h"
#include <stdio.h>

static HAL_GPIO_PinState pin_state[32] = {0};

void hal_gpio_write(uint32_t pin, HAL_GPIO_PinState state) {
    if (pin < 32) {
        pin_state[pin] = state;
        printf("[SIM] GPIO pin %u -> %s\n",
               pin, state ? "HIGH" : "LOW");
    }
}

HAL_GPIO_PinState hal_gpio_read(uint32_t pin) {
    return (pin < 32) ? pin_state[pin] : HAL_GPIO_PIN_RESET;
}

CMakeLists.txt でビルドターゲットを分けることで、同一ソースコードを実機向けとシミュレータ向けに切り替えられる。

# CMakeLists.txt(抜粋)
option(SIMULATION_BUILD "Build for host simulation" OFF)

if(SIMULATION_BUILD)
    # ホスト GCC でシミュレータビルド
    add_executable(firmware_sim
        src/main.c
        src/app/control_logic.c
        src/hal/hal_gpio_sim.c
        src/hal/hal_uart_sim.c
    )
    target_compile_definitions(firmware_sim PRIVATE SIMULATION=1)
else()
    # ARM クロスコンパイラで実機ビルド
    add_executable(firmware.elf
        src/main.c
        src/app/control_logic.c
        src/hal/hal_gpio_stm32.c
        src/hal/hal_uart_stm32.c
    )
    target_link_options(firmware.elf PRIVATE -T${LINKER_SCRIPT})
endif()

CI/CD パイプラインへの組み込み例

GitHub Actions を使い、プッシュのたびに QEMU 上でファームウェアのスモークテストを行うワークフロー例を示す。

# .github/workflows/firmware-test.yml
name: Firmware CI

on: [push, pull_request]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            gcc-arm-none-eabi \
            qemu-system-arm \
            cmake ninja-build

      - name: Build firmware
        run: |
          mkdir build && cd build
          cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-none-eabi.cmake
          ninja

      - name: Run on QEMU (smoke test)
        run: |
          timeout 30 qemu-system-arm \
            -machine lm3s6965evb \
            -nographic \
            -semihosting-config enable=on,target=native \
            -kernel build/firmware.elf \
            2>&1 | tee qemu_output.txt
          grep -q "TESTS PASSED" qemu_output.txt

      - name: Run host simulation unit tests
        run: |
          mkdir build_sim && cd build_sim
          cmake .. -DSIMULATION_BUILD=ON
          make
          ./firmware_sim --test

よくある問題と対処法

問題1:QEMU でペリフェラルが未実装

QEMU のボードモデルは完全ではなく、ADC や特定タイマが未実装の場合がある。-d unimp オプションを付けると未実装レジスタへのアクセスをログに出力できる。

qemu-system-arm -machine netduinoplus2 -cpu cortex-m4 \
  -kernel firmware.elf -nographic -d unimp 2>&1 | grep "unimplemented"

未実装ペリフェラルが問題になる場合は、Renode への移行を検討する。Renode のモデルライブラリはより組み込み向けに充実している。

問題2:タイミング依存のバグが再現しない

エミュレータは命令実行順序は再現するが、実時間のタイミング(クロックサイクル精度)は完全ではない。割り込みのタイミングに依存するバグは、エミュレータでは再現しないことがある。このようなバグにはインサーキットデバッガ(ICD)を使った実機デバッグが必要だ。

問題3:フラッシュ/RAM マップのミスマッチ

リンカスクリプトのメモリマップがエミュレータのボード定義と一致していないと、ファームウェアが起動しない。QEMU のボード定義(hw/arm/ 以下の C ファイル)と自プロジェクトのリンカスクリプトのアドレスを照合する。


他技術との比較

エミュレータ vs インサーキットデバッガ(ICD)

インサーキットデバッガ(ICD)は、実際のターゲットハードウェアに JTAG/SWD インタフェースで接続し、リアルタイムに CPU の状態を読み書きするデバッグツールだ。エミュレータとは次の点で異なる。

観点エミュレータICD(JTAG/SWD)
実機の必要性不要必要
タイミング精度近似実時間
ペリフェラル動作モデルに依存実回路そのまま
コスト低(ソフトウェアのみ)中〜高(プローブが必要)
CI 統合容易難しい(物理接続が必要)
利用場面初期開発・自動テスト最終デバッグ・タイミング問題

理想的な開発フローは、エミュレータで初期開発と自動テストを行い、実機が必要な段階で ICD に切り替えるというものだ。

エミュレータ vs HIL テスト

HIL(Hardware-in-the-Loop)は実機のコントロールユニットと、外部のシミュレータ(dSPACE、NI VeriStand など)を組み合わせてテストする手法で、特に自動車・産業機器分野で広く使われる。

観点ソフトウェアエミュレータHIL テスト
実機ハードウェア不要コントロールユニットは実機を使用
外部環境ソフトウェアモデル物理シミュレータまたはモデル
コスト非常に低い非常に高い(専用機材が必要)
精度中程度高い(実回路動作)
適用フェーズ初期〜中期開発後期統合テスト・認証

エミュレータ vs ユニットテスト(ホストシミュレーション)

ユニットテストのホストシミュレーション(前述のモックベースアプローチ)は、ロジックの正確さを素早く検証するのに最適だが、ハードウェア固有の問題は見つけられない。エミュレータはその中間に位置し、実機に近い動作環境でありながら実機不要という特性を持つ。

テストピラミッド(組み込み版)

        ┌────────────────────┐
        │   実機 + ICD       │ ← 最終確認・タイミング問題
        │   HIL テスト       │
        ├────────────────────┤
        │   エミュレータ      │ ← 統合動作確認・ブート検証
        ├────────────────────┤
        │   ユニットテスト    │ ← ロジック検証・最多実行
        │   (ホスト実行)    │
        └────────────────────┘

テストピラミッドの考え方を組み込み開発に適用すると、最も頻繁に実行するのはホストシミュレーションのユニットテスト、次いでエミュレータでの統合確認、最後に実機または HIL で最終検証という三層構造が合理的だ。CI/CD パイプラインでは下二層を自動化し、実機テストは手動または定期実行にとどめることで、開発速度とテスト品質のバランスを取ることができる。


まとめ

エミュレータとシミュレータは、それぞれ異なる強みを持つ相補的な技術だ。エミュレータは実機バイナリをそのまま動かせる高い再現性が強みであり、シミュレータはセットアップの容易さと実行速度が強みだ。組み込み開発においては、どちらか一方に頼るのではなく、開発フェーズと検証目的に応じて使い分けることが重要である。

近年は QEMU や Renode の成熟により、CI/CD パイプラインへの組み込みが現実的な選択肢となっている。クロスコンパイル環境の整備と合わせて、エミュレータベースの自動テストを早期に導入することで、実機依存のボトルネックを解消し、開発スピードと品質の両立を図ることができる。

関連用語

参考リンク