概要
エミュレータ(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 FVP | Simulink、自作テストハーネス |
主要ツール一覧
| ツール名 | 対応アーキテクチャ | ライセンス | 特徴 |
|---|---|---|---|
| QEMU | ARM、RISC-V、x86、MIPS など | GPL v2 | 汎用・広大なエコシステム |
| Renode | ARM Cortex-M/A、RISC-V など | MIT | 組み込み特化・CI 向け |
| ARM FVP | ARM Cortex-M/A(全世代) | 商用/無償版あり | ARM 公式・高精度 |
| Keil MDK Simulator | ARM Cortex-M | 商用 | IDE 統合・初心者向け |
| avr-emu / simavr | AVR(ATmega/ATtiny) | GPL | Arduino 互換ターゲット |
| GDB Simulator (sim) | ARM、MIPS、RISC-V など | GPL | GDB 組み込み・軽量 |
| OVPsim | ARM、MIPS、PowerPC など | 商用/無償版あり | 高速・命令精度高 |
| Proteus VSM | 8051、PIC、AVR、ARM | 商用 | 回路シミュレータ統合 |
QEMU がサポートする代表的な組み込みボード
| ボード名 | MCU/CPU | 主なペリフェラル |
|---|---|---|
| netduinoplus2 | STM32F4 | UART、SPI、I2C、GPIO |
| stm32vldiscovery | STM32F1 | UART、GPIO |
| mps2-an385 | ARM Cortex-M3 | UART、Timer、GPIO |
| microbit | Nordic nRF51 | UART、GPIO、I2C |
| sifive_e | RISC-V E51 | UART、GPIO |
| lm3s6965evb | ARM Cortex-M3 | UART、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 環境での活用も容易だ。
シミュレータの動作原理
ソフトウェアシミュレータは、実際のバイナリを動かすのではなく、ハードウェアに依存する部分をスタブ(テスト用の代替実装)に置き換えてコンパイルする。典型的なアプローチは次のとおりだ。
- ハードウェア抽象化層(HAL)を設ける。GPIO や UART の操作を直接レジスタに書くのではなく、
hal_gpio_write()などの関数を経由する。 - 本番ビルドでは実際のレジスタ操作を行う実装をリンクし、テストビルドではモック実装(ログ出力や値の記録だけを行う)をリンクする。
- ホスト 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 パイプラインへの組み込みが現実的な選択肢となっている。クロスコンパイル環境の整備と合わせて、エミュレータベースの自動テストを早期に導入することで、実機依存のボトルネックを解消し、開発スピードと品質の両立を図ることができる。