PR

QEMUで組み込みC++ベアメタルプログラミング環境を構築する|15分でUART出力

組み込み開発

組み込み開発は「実機がないと始められない」と思っていませんか?

実は、QEMU(キューエミュ)という仮想マシンエミュレータを使えば、PC上だけでベアメタル開発を始めることができます。しかも、QEMUは無料で使えて、環境構築も意外と簡単です。

本記事では、Windows環境を対象に、

  • QEMUのインストール
  • クロスコンパイラによるビルド
  • シリアル出力で「Hello Embedded from QEMU」を表示する

という最小限のステップで、OSなしで動くCプログラムをQEMU上で動かす手順を丁寧に解説します。

CMakeのセットアップが済んでいない方は、先に こちらの環境構築ガイド を完了してから読み進めるとスムーズです。こちらもチェックしてみてください。

それでは、どうぞ。

開発に必要な3つのツールをインストールする

ベアメタル開発環境を構築するには、次の3つのツールをインストールします。

インストールするツール一覧

ツール名目的入手方法
QEMU仮想マシン実行環境公式インストーラ
arm-none-eabi-gccARM向けクロスコンパイラ公式インストーラ
Ninja高速ビルドツールwingetで導入

QEMUのインストール

QEMUは、エミュレーションにより仮想ハードウェア上でコードを実行できるツールです。

以下のサイトから qemu-w64-setup-*.exe をダウンロードします。

QEMU for Windows – Installers (64 bit)
QEMU Binaries for Windows

ダウンロードした .exe ファイルを実行して、QEMUをインストールします。

インストールディレクトリ(例:C:\Program Files\qemu)に qemu-system-arm.exe が含まれていればOKです。インストールディレクトリのパスを通します。

ARM用クロスコンパイラのインストール

ここでは Arm 公式の GNU Toolchain(arm-none-eabi)を使用します。

以下のURLから「Windows, AArch32 bare-metal target (arm-none-eabi)」のインストーラをダウンロードします。

Arm GNU Toolchain Downloads ??? Arm Developer
Download the Arm GNU Toolchain, an open-source suite of tools for C, C++, and Assembly programming for the Arm architect...

インストール先は任意のディレクトリに変更できます。C:\gcc\arm等わかりやすいディレクトリを指定しましょう。

こちらはパスは通さなくてOKです。後述する方法でビルド時のみ一時的にパスを追加する形で作業します。

Ninjaのインストール(winget使用)

ターミナルを開いて以下のコマンドを実行してください。

winget install Ninja-build.Ninja

インストール後、以下のコマンドで確認します。

ninja --version

補足:arm-none-eabi-gcc の PATH を一時的に通す

cmakeによるビルド操作などを行う前にPowerShell で環境変数を一時的に追加します。

$env:PATH = "C:\Path\To\ArmGCC\bin;" + $env:PATH

C:\Path\To\ArmGCC\bin は、実際のインストール先に置き換えてください。
※ 永続化はせず、一時的な追加で構いません。

ここまでで、以下の3つのコマンドが実行できるようになっていれば準備完了です。

qemu-system-arm --version
arm-none-eabi-gcc --version
ninja --version

QEMUが正しく動くか確認する

QEMUのインストールが完了したら、仮想マシンが正常に起動するかどうかを確認してみましょう。

versatilepb ボードで仮想マシンを起動する

次のようにコマンドを実行してみましょう。

qemu-system-arm -M versatilepb -nographic

この時点では何も表示されずに止まっていて問題ありません。次のステップで、仮想マシン上で動作するプログラムを作って、UART出力を表示させます。

最小構成のベアメタルプロジェクトを作る

本記事で使用するサンプルコードはgithubで公開しています。
https://github.com/mappuu-dev/qemu-arm-bare-metal

GitHub - mappuu-dev/qemu-arm-bare-metal: sample bare-metal application for ARM QEMU.
sample bare-metal application for ARM QEMU. Contribute to mappuu-dev/qemu-arm-bare-metal development by creating an acco...

以下のような構成でプロジェクトを作成します。

qemu-arm-bare-metal/
├── CMakeLists.txt
├── toolchain_arm.cmake
├── linker/
│   └── arm_versatilepb.ld
├── src/
│   ├── main.c
│   ├── startup.s
│   ├── syscalls.c
│   └── uart.c

それでは実際にファイルの中身を見ていきます。

main.c

#include <stdio.h>

void main(void) {
    printf("Hello Embedded from QEMU\n");
    while (1){}
}

uart.c

#define UART_BASE 0x101f1000
#define UART_DR (*(volatile unsigned int *)(UART_BASE + 0x00))
#define UART_FR (*(volatile unsigned int *)(UART_BASE + 0x18))
#define UART_FR_TXFF 0x20

void uart_putc(char c) {
    while (UART_FR & UART_FR_TXFF) {
    }
    UART_DR = c;
}

void uart_puts(const char *s) {
    while (*s) {
        uart_putc(*s++);
    }
}

int putchar(int c) {
    uart_putc((char)c);
    return c;
}

startup.s

.global _start

_start:
    ldr sp, =_stack_end
    bl main
    b .

linker.ld

ENTRY(_start)

MEMORY
{
    RAM  : ORIGIN = 0x00010000, LENGTH = 32M
}

SECTIONS
{
    .text : {
        PROVIDE(_text_start = .);
        *(.text .text.*)
        PROVIDE(_text_end = .);
    } > RAM
    
    .rodata : {
        PROVIDE(_rodata_start = .);
        *(.rodata .rodata.*)
        PROVIDE(_rodata_end = .);
    } > RAM
    
    .data : {
        PROVIDE(_data_start = .);
        *(.data .data.*)
        PROVIDE(_data_end = .);
    } > RAM
    
    .bss : {
        PROVIDE(_bss_start = .);
        *(.bss .bss.*)
        PROVIDE(_bss_end = .);
    } > RAM

    PROVIDE(_end = _bss_end);
    
    .stack : {
        . = ALIGN(16);
        PROVIDE(_stack_start = .);
        . += 4096;
        PROVIDE(_stack_end = .);
    } > RAM
}

toolchain_arm.cmake

set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CROSS_COMPILE_PREFIX arm-none-eabi-)

set(CMAKE_C_COMPILER ${CROSS_COMPILE_PREFIX}gcc)
set(CMAKE_CXX_COMPILER ${CROSS_COMPILE_PREFIX}g++)
set(CMAKE_ASM_COMPILER ${CROSS_COMPILE_PREFIX}as)
set(CMAKE_OBJCOPY ${CROSS_COMPILE_PREFIX}objcopy)
set(CMAKE_OBJDUMP ${CROSS_COMPILE_PREFIX}objdump)
set(CMAKE_SIZE ${CROSS_COMPILE_PREFIX}size)

set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(baremetal C CXX ASM)

add_executable(${PROJECT_NAME}.elf
    src/startup.s
    src/main.c
    src/syscalls.c
    src/uart.c
)


set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mcpu=arm926ej-s")
set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -mcpu=arm926ej-s")
target_link_options(${PROJECT_NAME}.elf PRIVATE -T${CMAKE_SOURCE_DIR}/linker/linker.ld)

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ffreestanding -g")
set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -g")

target_link_options(${PROJECT_NAME}.elf PRIVATE
    -nostartfiles
    -lc
)

add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD
    COMMAND ${CMAKE_OBJCOPY} -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin
)

CMakeでビルドして実行用バイナリを作成する

cmake -S . -B build -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="toolchain_arm.cmake"
cmake --build build

build/にbaremetal.elf、baremetal.binが生成されていれば成功です。

QEMU上で「Hello Embedded from QEMU」を表示する

qemu-system-arm -M versatilepb -nographic -kernel build/baremetal.bin

表示結果

Hello Embedded from QEMU

まとめ

本記事では、以下のステップで実機なしのベアメタル開発を実現しました:

  • Windows環境にQEMU、クロスコンパイラ、Ninjaをインストールしました
  • クロスコンパイルでARM用のバイナリを生成できるようになりました
  • QEMU上でバイナリを実行してUART出力を確認しました

この構成をベースに、割り込み処理やRTOS、デバッグ機能なども拡張できます。

GDBによるQEMUデバッグ方法も記事を書く予定です。お楽しみに。

本記事で使ったサンプルコードはgithubで公開しています。
https://github.com/mappuu-dev/qemu-arm-bare-metal

GitHub - mappuu-dev/qemu-arm-bare-metal: sample bare-metal application for ARM QEMU.
sample bare-metal application for ARM QEMU. Contribute to mappuu-dev/qemu-arm-bare-metal development by creating an acco...

コメント

タイトルとURLをコピーしました