Zig選択課題01: 組み込みシステム - ベアメタルプログラミング
課題説明
背景
組み込みシステムは、OS上ではなく直接ハードウェアと対話するプログラムです。この課題では、Zigの低レベルプログラミング機能を活用して、RISC-Vアーキテクチャのベアメタル環境でブートローダーとシンプルな組み込みシステムを実装します。
Zigは組み込みシステム開発に特に適しています:
- ランタイム不要: 最小限のバイナリサイズ
- クロスコンパイル: ビルトインでRISC-Vサポート
- コンパイル時保証: ベアメタル環境でも型安全性
- C互換性: 既存のハードウェアライブラリとの統合
- ブートローダー実装
要件
以下の機能を実装してください:
- メモリマップドI/O
- 割り込み処理
- 基本的なデバイスドライバ
- QEMUエミュレーション
制約
- Zigの標準ライブラリは使用不可(
@import("std")禁止) - フリースタンディング環境(
-target riscv64-freestanding) - ヒープアロケーション禁止
- 全てのメモリ管理は静的または自前実装
- QEMUのvirt RISC-V 64ビットマシンをターゲット
---
想定解答
プロジェクト構造
zig-embed/
├── build.zig
├── src/
│ ├── main.zig
│ ├── boot.zig
│ ├── uart.zig
│ ├── gpio.zig
│ ├── timer.zig
│ └── interrupt.zig
└── linker.ld
リンカスクリプト (linker.ld)
/* RISC-V 64ビット用リンカスクリプト */
OUTPUT_ARCH(riscv)
ENTRY(_start)
MEMORY
{
/* QEMU virt マシンのメモリマップ */
RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 128M
}
SECTIONS
{
. = 0x80000000;
.text : {
*(.text.boot) /* ブートコード */
*(.text) /* プログラムコード */
*(.text.*)
} > RAM
.rodata : {
*(.rodata) /* 読み取り専用データ */
*(.rodata.*)
} > RAM
.data : {
*(.data) /* 初期化済みデータ */
*(.data.*)
} > RAM
.bss : {
__bss_start = .;
*(.bss) /* 未初期化データ */
*(.bss.*)
*(COMMON)
__bss_end = .;
} > RAM
/* スタック領域 */
. = ALIGN(16);
__stack_top = . + 0x10000; /* 64KB スタック */
}
ブートコード (boot.zig)
// ブートストラップコード
// RISC-Vハードウェアの初期化を行う
const uart = @import("uart.zig");
const interrupt = @import("interrupt.zig");
// リセットベクタ: プログラムのエントリポイント
export fn _start() callconv(.Naked) noreturn {
// RISC-Vアセンブリでスタートアップコードを記述
asm volatile (
\\ .option push
\\ .option norelax
\\ la gp, __global_pointer$
\\ .option pop
\\ la sp, __stack_top // スタックポインタ設定
\\ call boot_main // Zig関数へジャンプ
\\ j . // 無限ループ(到達しないはず)
);
unreachable;
}
export fn boot_main() noreturn {
// BSSセクションのゼロクリア
clearBss();
// UART初期化
uart.init();
uart.puts("Booting Zig embedded system...\r\n");
// 割り込みシステム初期化
interrupt.init();
uart.puts("Interrupt system initialized\r\n");
// メインプログラムへ
@import("main.zig").main();
}
fn clearBss() void {
// リンカスクリプトで定義されたシンボル
extern const __bss_start: u8;
extern const __bss_end: u8;
const bss_start = @intFromPtr(&__bss_start);
const bss_end = @intFromPtr(&__bss_end);
const bss_len = bss_end - bss_start;
// BSSセクションを0で埋める
@memset(@as([*]u8, @ptrFromInt(bss_start))[0..bss_len], 0);
}
UART ドライバ (uart.zig)
// UART (Universal Asynchronous Receiver/Transmitter) ドライバ
// シリアル通信の実装
// QEMU virt マシンのUARTレジスタアドレス
const UART_BASE: usize = 0x10000000;
// UARTレジスタオフセット (NS16550A互換)
const UART_RHR: usize = 0; // Receive Holding Register (読み取り)
const UART_THR: usize = 0; // Transmit Holding Register (書き込み)
const UART_IER: usize = 1; // Interrupt Enable Register
const UART_FCR: usize = 2; // FIFO Control Register
const UART_LCR: usize = 3; // Line Control Register
const UART_LSR: usize = 5; // Line Status Register
const UART_LSR_TX_IDLE: u8 = 1 << 5; // 送信可能フラグ
const UART_LSR_RX_READY: u8 = 1 << 0; // 受信データあり
// レジスタ読み書き用ヘルパー
fn readReg(offset: usize) u8 {
const addr = UART_BASE + offset;
const ptr = @as(*volatile u8, @ptrFromInt(addr));
return ptr.*;
}
fn writeReg(offset: usize, value: u8) void {
const addr = UART_BASE + offset;
const ptr = @as(*volatile u8, @ptrFromInt(addr));
ptr.* = value;
}
// UART初期化
pub fn init() void {
// LCR: 8ビット、パリティなし、1ストップビット
writeReg(UART_LCR, 0b00000011);
// FCR: FIFOを有効化
writeReg(UART_FCR, 0b00000001);
// IER: 割り込み有効化(受信割り込み)
writeReg(UART_IER, 0b00000001);
}
// 1文字送信
pub fn putc(ch: u8) void {
// 送信可能になるまで待機
while ((readReg(UART_LSR) & UART_LSR_TX_IDLE) == 0) {}
writeReg(UART_THR, ch);
}
// 文字列送信
pub fn puts(str: []const u8) void {
for (str) |ch| {
putc(ch);
}
}
// 1文字受信(ブロッキング)
pub fn getc() u8 {
// 受信データがあるまで待機
while ((readReg(UART_LSR) & UART_LSR_RX_READY) == 0) {}
return readReg(UART_RHR);
}
// 1文字受信(ノンブロッキング)
pub fn tryGetc() ?u8 {
if ((readReg(UART_LSR) & UART_LSR_RX_READY) != 0) {
return readReg(UART_RHR);
}
return null;
}
// 数値を16進数で出力(デバッグ用)
pub fn putHex(value: u64) void {
const hex_chars = "0123456789ABCDEF";
puts("0x");
var i: u6 = 60; // 64ビット = 16桁
while (true) : (i -= 4) {
const nibble = @as(u8, @truncate((value >> i) & 0xF));
putc(hex_chars[nibble]);
if (i == 0) break;
}
}
GPIO ドライバ (gpio.zig)
// GPIO (General Purpose Input/Output) ドライバ
// LEDなどの制御に使用
// QEMU virt マシンではシンプルなGPIOをシミュレート
const GPIO_BASE: usize = 0x10060000;
pub const Pin = enum(u8) {
pin0 = 0,
pin1 = 1,
pin2 = 2,
pin3 = 3,
led_red = 0, // エイリアス
led_green = 1,
led_blue = 2,
};
pub const Direction = enum {
input,
output,
};
pub const Level = enum(u8) {
low = 0,
high = 1,
};
// GPIOピンを出力に設定
pub fn setDirection(pin: Pin, dir: Direction) void {
const offset = @intFromEnum(pin);
const addr = GPIO_BASE + offset * 4;
const ptr = @as(*volatile u32, @ptrFromInt(addr));
switch (dir) {
.output => ptr.* |= (1 << 0), // ビット0: 方向
.input => ptr.* &= ~@as(u32, 1 << 0),
}
}
// GPIOピンのレベルを設定
pub fn write(pin: Pin, level: Level) void {
const offset = @intFromEnum(pin);
const addr = GPIO_BASE + offset * 4;
const ptr = @as(*volatile u32, @ptrFromInt(addr));
switch (level) {
.high => ptr.* |= (1 << 1), // ビット1: 値
.low => ptr.* &= ~@as(u32, 1 << 1),
}
}
// GPIOピンの値を読み取り
pub fn read(pin: Pin) Level {
const offset = @intFromEnum(pin);
const addr = GPIO_BASE + offset * 4;
const ptr = @as(*volatile u32, @ptrFromInt(addr));
return if ((ptr.* & (1 << 1)) != 0) .high else .low;
}
// GPIOピンをトグル
pub fn toggle(pin: Pin) void {
const current = read(pin);
write(pin, if (current == .high) .low else .high);
}
タイマードライバ (timer.zig)
// RISC-V タイマードライバ
// 周期的な割り込みを生成
const uart = @import("uart.zig");
// RISC-V タイマーレジスタ (mtime, mtimecmp)
const MTIME_ADDR: usize = 0x200BFF8;
const MTIMECMP_ADDR: usize = 0x2004000;
// タイマー周波数 (QEMU virt: 10MHz)
const TIMER_FREQ: u64 = 10_000_000;
// タイマー割り込みカウンタ
var tick_count: u64 = 0;
// 現在時刻を取得 (マイクロ秒)
pub fn getTime() u64 {
const ptr = @as(*volatile u64, @ptrFromInt(MTIME_ADDR));
return ptr.*;
}
// タイマー比較値を設定
fn setTimerCmp(value: u64) void {
const ptr = @as(*volatile u64, @ptrFromInt(MTIMECMP_ADDR));
ptr.* = value;
}
// タイマー初期化 (1秒ごとに割り込み)
pub fn init() void {
const interval = TIMER_FREQ; // 1秒
const next_time = getTime() + interval;
setTimerCmp(next_time);
}
// タイマー割り込みハンドラ
pub fn handleInterrupt() void {
tick_count += 1;
// 次の割り込み時刻を設定
const interval = TIMER_FREQ;
const next_time = getTime() + interval;
setTimerCmp(next_time);
// デバッグ出力
uart.puts("Timer tick: ");
uart.putHex(tick_count);
uart.puts("\r\n");
}
// 経過秒数を取得
pub fn getTicks() u64 {
return tick_count;
}
// 指定ミリ秒だけビジーウェイト
pub fn delayMs(ms: u64) void {
const start = getTime();
const wait_cycles = (TIMER_FREQ / 1000) * ms;
while ((getTime() - start) < wait_cycles) {}
}
割り込み処理 (interrupt.zig)
// RISC-V 割り込み処理システム
const uart = @import("uart.zig");
const timer = @import("timer.zig");
// RISC-V 例外コード
const INTERRUPT_TIMER: u64 = 0x8000000000000007;
const INTERRUPT_EXTERNAL: u64 = 0x800000000000000B;
// トラップハンドラ(割り込み・例外の共通エントリポイント)
export fn trap_handler() callconv(.Naked) void {
// レジスタを保存してtrap_handler_mainを呼び出す
asm volatile (
\\ addi sp, sp, -256
\\ sd x1, 0(sp)
\\ sd x2, 8(sp)
\\ sd x3, 16(sp)
\\ sd x4, 24(sp)
\\ sd x5, 32(sp)
\\ sd x6, 40(sp)
\\ sd x7, 48(sp)
\\ sd x8, 56(sp)
\\ sd x9, 64(sp)
\\ sd x10, 72(sp)
\\ sd x11, 80(sp)
\\ sd x12, 88(sp)
\\ sd x13, 96(sp)
\\ sd x14, 104(sp)
\\ sd x15, 112(sp)
\\ sd x16, 120(sp)
\\ sd x17, 128(sp)
\\ sd x18, 136(sp)
\\ sd x19, 144(sp)
\\ sd x20, 152(sp)
\\ sd x21, 160(sp)
\\ sd x22, 168(sp)
\\ sd x23, 176(sp)
\\ sd x24, 184(sp)
\\ sd x25, 192(sp)
\\ sd x26, 200(sp)
\\ sd x27, 208(sp)
\\ sd x28, 216(sp)
\\ sd x29, 224(sp)
\\ sd x30, 232(sp)
\\ sd x31, 240(sp)
\\ call trap_handler_main
\\ ld x1, 0(sp)
\\ ld x2, 8(sp)
\\ ld x3, 16(sp)
\\ ld x4, 24(sp)
\\ ld x5, 32(sp)
\\ ld x6, 40(sp)
\\ ld x7, 48(sp)
\\ ld x8, 56(sp)
\\ ld x9, 64(sp)
\\ ld x10, 72(sp)
\\ ld x11, 80(sp)
\\ ld x12, 88(sp)
\\ ld x13, 96(sp)
\\ ld x14, 104(sp)
\\ ld x15, 112(sp)
\\ ld x16, 120(sp)
\\ ld x17, 128(sp)
\\ ld x18, 136(sp)
\\ ld x19, 144(sp)
\\ ld x20, 152(sp)
\\ ld x21, 160(sp)
\\ ld x22, 168(sp)
\\ ld x23, 176(sp)
\\ ld x24, 184(sp)
\\ ld x25, 192(sp)
\\ ld x26, 200(sp)
\\ ld x27, 208(sp)
\\ ld x28, 216(sp)
\\ ld x29, 224(sp)
\\ ld x30, 232(sp)
\\ ld x31, 240(sp)
\\ addi sp, sp, 256
\\ mret
);
}
// トラップハンドラのメイン処理
export fn trap_handler_main() void {
const mcause = readMcause();
// 割り込みかどうか判定(最上位ビット)
if ((mcause & (1 << 63)) != 0) {
// 割り込み
const interrupt_code = mcause & 0x7FFFFFFFFFFFFFFF;
handleInterrupt(interrupt_code);
} else {
// 例外
uart.puts("Exception occurred: ");
uart.putHex(mcause);
uart.puts("\r\n");
// 例外は致命的なので無限ループ
while (true) {}
}
}
fn handleInterrupt(code: u64) void {
switch (code) {
7 => {
// タイマー割り込み
timer.handleInterrupt();
},
11 => {
// 外部割り込み (UART等)
uart.puts("External interrupt\r\n");
},
else => {
uart.puts("Unknown interrupt: ");
uart.putHex(code);
uart.puts("\r\n");
},
}
}
// CSR (Control and Status Register) 読み書き関数
fn readMcause() u64 {
return asm volatile ("csrr %[ret], mcause"
: [ret] "=r" (-> u64),
);
}
fn writeMtvec(value: u64) void {
asm volatile ("csrw mtvec, %[val]"
:
: [val] "r" (value),
);
}
fn writeMstatus(value: u64) void {
asm volatile ("csrw mstatus, %[val]"
:
: [val] "r" (value),
);
}
fn writeMie(value: u64) void {
asm volatile ("csrw mie, %[val]"
:
: [val] "r" (value),
);
}
// 割り込みシステム初期化
pub fn init() void {
// トラップベクタを設定(ダイレクトモード)
const trap_addr = @intFromPtr(&trap_handler);
writeMtvec(trap_addr);
// 割り込み有効化 (MIE: Machine Interrupt Enable)
// ビット7: タイマー割り込み
// ビット11: 外部割り込み
writeMie((1 << 7) | (1 << 11));
// グローバル割り込み有効化 (MSTATUS.MIE)
const mstatus: u64 = 0b1000; // ビット3: MIE
writeMstatus(mstatus);
}
メインプログラム (main.zig)
// メインプログラム
// LEDの点滅とシリアル通信のデモ
const uart = @import("uart.zig");
const gpio = @import("gpio.zig");
const timer = @import("timer.zig");
pub fn main() noreturn {
uart.puts("\r\n");
uart.puts("=================================\r\n");
uart.puts("Zig Embedded System Demo\r\n");
uart.puts("=================================\r\n");
uart.puts("\r\n");
// GPIO初期化(LED制御用)
gpio.setDirection(.led_red, .output);
gpio.setDirection(.led_green, .output);
gpio.setDirection(.led_blue, .output);
// タイマー初期化
timer.init();
uart.puts("System initialized. Starting main loop...\r\n");
uart.puts("Type 'r', 'g', 'b' to toggle LEDs\r\n");
// メインループ
var led_state: u8 = 0;
while (true) {
// ユーザー入力をチェック(ノンブロッキング)
if (uart.tryGetc()) |ch| {
handleInput(ch);
}
// 2秒ごとにLEDパターンを変更
const ticks = timer.getTicks();
const new_state = @as(u8, @truncate(ticks % 8));
if (new_state != led_state) {
led_state = new_state;
updateLeds(led_state);
}
// CPUを休ませる (WFI: Wait For Interrupt)
waitForInterrupt();
}
}
fn handleInput(ch: u8) void {
// エコーバック
uart.putc(ch);
uart.puts("\r\n");
switch (ch) {
'r', 'R' => {
gpio.toggle(.led_red);
uart.puts("Red LED toggled\r\n");
},
'g', 'G' => {
gpio.toggle(.led_green);
uart.puts("Green LED toggled\r\n");
},
'b', 'B' => {
gpio.toggle(.led_blue);
uart.puts("Blue LED toggled\r\n");
},
'h', 'H', '?' => {
printHelp();
},
's', 'S' => {
printStatus();
},
else => {
uart.puts("Unknown command. Type 'h' for help\r\n");
},
}
}
fn updateLeds(pattern: u8) void {
const red = if ((pattern & 0b001) != 0) gpio.Level.high else gpio.Level.low;
const green = if ((pattern & 0b010) != 0) gpio.Level.high else gpio.Level.low;
const blue = if ((pattern & 0b100) != 0) gpio.Level.high else gpio.Level.low;
gpio.write(.led_red, red);
gpio.write(.led_green, green);
gpio.write(.led_blue, blue);
}
fn printHelp() void {
uart.puts("\r\n");
uart.puts("Available commands:\r\n");
uart.puts(" r - Toggle red LED\r\n");
uart.puts(" g - Toggle green LED\r\n");
uart.puts(" b - Toggle blue LED\r\n");
uart.puts(" s - Show system status\r\n");
uart.puts(" h - Show this help\r\n");
uart.puts("\r\n");
}
fn printStatus() void {
uart.puts("\r\n");
uart.puts("System Status:\r\n");
uart.puts(" Uptime: ");
uart.putHex(timer.getTicks());
uart.puts(" seconds\r\n");
uart.puts(" Red LED: ");
uart.puts(if (gpio.read(.led_red) == .high) "ON\r\n" else "OFF\r\n");
uart.puts(" Green LED: ");
uart.puts(if (gpio.read(.led_green) == .high) "ON\r\n" else "OFF\r\n");
uart.puts(" Blue LED: ");
uart.puts(if (gpio.read(.led_blue) == .high) "ON\r\n" else "OFF\r\n");
uart.puts("\r\n");
}
fn waitForInterrupt() void {
asm volatile ("wfi"); // Wait For Interrupt
}
ビルドスクリプト (build.zig)
const std = @import("std");
pub fn build(b: *std.Build) void {
// RISC-V 64ビット ベアメタルターゲット
const target = b.resolveTargetQuery(.{
.cpu_arch = .riscv64,
.os_tag = .freestanding,
.abi = .none,
});
const optimize = b.standardOptimizeOption(.{});
// 実行可能ファイル
const exe = b.addExecutable(.{
.name = "zig-embed",
.root_source_file = b.path("src/boot.zig"),
.target = target,
.optimize = optimize,
});
// リンカスクリプトを使用
exe.setLinkerScriptPath(b.path("linker.ld"));
// ビルド成果物をインストール
b.installArtifact(exe);
// QEMUで実行するステップ
const qemu = b.addSystemCommand(&[_][]const u8{
"qemu-system-riscv64",
"-machine", "virt",
"-bios", "none",
"-kernel", "zig-out/bin/zig-embed",
"-m", "128M",
"-nographic",
"-serial", "mon:stdio",
});
qemu.step.dependOn(&exe.step);
const run_step = b.step("run", "Run in QEMU");
run_step.dependOn(&qemu.step);
// デバッグ実行(GDBサーバーモード)
const qemu_debug = b.addSystemCommand(&[_][]const u8{
"qemu-system-riscv64",
"-machine", "virt",
"-bios", "none",
"-kernel", "zig-out/bin/zig-embed",
"-m", "128M",
"-nographic",
"-serial", "mon:stdio",
"-s", // GDBサーバーをポート1234で起動
"-S", // 起動時に一時停止
});
qemu_debug.step.dependOn(&exe.step);
const debug_step = b.step("debug", "Run in QEMU with GDB server");
debug_step.dependOn(&qemu_debug.step);
}
---
解説
実装のポイント
1. ベアメタル環境の構築
ベアメタルプログラミングでは、OSが提供する機能(メモリ管理、デバイスドライバ等)が一切使えません。全てを自分で実装する必要があります。
スタートアップコード:
export fn _start() callconv(.Naked) noreturn {
asm volatile (
\\ la sp, __stack_top // スタックポインタ設定
\\ call boot_main // Zig関数へジャンプ
);
unreachable;
}
.Naked呼び出し規約: コンパイラがプロローグ/エピローグを生成しない- インラインアセンブリで直接レジスタを操作
- スタックポインタ(
sp)を手動で設定
BSSセクションのクリア:
fn clearBss() void {
extern const __bss_start: u8;
extern const __bss_end: u8;
const bss_len = @intFromPtr(&__bss_end) - @intFromPtr(&__bss_start);
@memset(@as([*]u8, @ptrFromInt(bss_start))[0..bss_len], 0);
}
グローバル変数の未初期化領域(BSS)をゼロクリアします。Cの規格ではBSSは0初期化が保証されていますが、ベアメタルでは明示的に行う必要があります。
2. メモリマップドI/O
ハードウェアレジスタは特定のメモリアドレスにマップされています。
const UART_BASE: usize = 0x10000000;
fn writeReg(offset: usize, value: u8) void {
const addr = UART_BASE + offset;
const ptr = @as(*volatile u8, @ptrFromInt(addr));
ptr.* = value;
}
volatileの重要性:
- コンパイラの最適化を抑制
- メモリマップドI/Oは「読み取るだけで副作用がある」
volatileがないと、コンパイラが読み書きを削除する可能性
3. 割り込み処理
割り込みはハードウェアイベント(タイマー、I/O完了等)に応じて非同期に発生します。
トラップハンドラ:
export fn trap_handler() callconv(.Naked) void {
// 全レジスタをスタックに保存
// trap_handler_mainを呼び出し
// レジスタを復元して復帰 (mret)
}
RISC-Vでは、割り込み発生時に自動的にプログラムカウンタがmtvecレジスタの値にジャンプします。
CSR(制御・ステータスレジスタ):
fn writeMtvec(value: u64) void {
asm volatile ("csrw mtvec, %[val]"
:
: [val] "r" (value),
);
}
RISC-Vの特権命令を使ってCSRレジスタを操作します。
4. リンカスクリプト
メモリレイアウトを制御します。
SECTIONS
{
.text : { *(.text.boot) *(.text) } > RAM
.rodata : { *(.rodata) } > RAM
.data : { *(.data) } > RAM
.bss : { __bss_start = .; *(.bss) __bss_end = .; } > RAM
}
.text.boot: ブートコード(最初に配置).text: プログラムコード.rodata: 読み取り専用データ.data: 初期化済みグローバル変数.bss: 未初期化グローバル変数
設計判断
なぜRISC-Vを選んだか?
- オープンなISA: ライセンスフリー、学習に最適
- シンプルな設計: 命令セットが明快で理解しやすい
- QEMUサポート: エミュレーション環境が整っている
- Zigのビルトインサポート: クロスコンパイルが容易
なぜQEMU virtマシンか?
- 標準的なペリフェラル(UART、タイマー)が揃っている
- 実機不要で開発可能
- デバッグ機能(GDBサーバー)が充実
エラー処理: ベアメタルではパニックできません。
if ((uart.readReg(UART_LSR) & UART_LSR_TX_IDLE) == 0) {
// 無限ループで待機
while ((readReg(UART_LSR) & UART_LSR_TX_IDLE) == 0) {}
}
代替案
ARM Cortex-M:
- より一般的な組み込みアーキテクチャ
- 豊富な実機ボード(STM32等)
- RISC-Vより複雑
x86ベアメタル:
- PC互換機で動作
- BIOSやUEFIの知識が必要
- より複雑な初期化処理
- ベアメタルプログラミング
---
学習の意図
習得概念
- メモリマップドI/O
- 割り込み処理
- リンカとメモリレイアウト
- Zigの低レベル機能
@intFromPtr, @ptrFromInt)
- 呼び出し規約(.Naked)CS基礎との関連
コンピュータアーキテクチャ:
- CPUの動作原理(命令フェッチ、実行サイクル)
- メモリ階層(レジスタ、RAM)
- 割り込みとトラップ
オペレーティングシステム:
- ブートストラップ
- デバイスドライバ
- 割り込みハンドラ
- プロセスコンテキスト
コンパイラとリンカ:
- オブジェクトファイルの構造
- リンク処理
- メモリレイアウト
- 呼び出し規約
---
テスト方法
環境構築
必要なツール
# QEMU RISC-Vエミュレータ
# macOS
brew install qemu
# Ubuntu/Debian
sudo apt install qemu-system-riscv64
# Zigコンパイラ(最新版推奨)
# 公式サイトからダウンロード: https://ziglang.org/download/
ビルドと実行
# プロジェクトディレクトリで
zig build
# QEMUで実行
zig build run
期待される出力
Booting Zig embedded system...
Interrupt system initialized
=================================
Zig Embedded System Demo
=================================
System initialized. Starting main loop...
Type 'r', 'g', 'b' to toggle LEDs
Timer tick: 0x0000000000000001
Timer tick: 0x0000000000000002
インタラクティブテスト
QEMUコンソールで以下のコマンドを試してください:
r # Red LEDをトグル
g # Green LEDをトグル
b # Blue LEDをトグル
s # システムステータスを表示
h # ヘルプを表示
デバッグ
GDBでのデバッグ:
# ターミナル1: QEMUをGDBサーバーモードで起動
zig build debug
# ターミナル2: GDBを起動
riscv64-unknown-elf-gdb zig-out/bin/zig-embed
(gdb) target remote :1234
(gdb) break boot_main
(gdb) continue
自動テスト
QEMUのシリアル出力をキャプチャして検証:
#!/bin/bash
# test.sh
timeout 5 zig build run > output.txt 2>&1 &
QEMU_PID=$!
sleep 2
# 期待される文字列が出力されているか確認
if grep -q "Zig Embedded System Demo" output.txt; then
echo "Test PASSED"
else
echo "Test FAILED"
fi
kill $QEMU_PID
---
評価基準
基本要件(70%)
- [ ] UART通信 (20%)
- [ ] タイマーと割り込み (20%)
- [ ] GPIO制御 (10%)
発展要件(30%)
- [ ] 追加デバイスドライバ (10%)
- [ ] マルチタスク機能 (10%)
- [ ] メモリ管理 (10%)
コード品質
- 可読性: ハードウェア操作のコメントが充実しているか
- 安全性: volatileが適切に使用されているか
- 構造: ドライバが適切にモジュール化されているか
- QEMUでの動作デモ
- LEDのインタラクティブ制御
- タイマー割り込みの動作確認
デモ
---
参考資料
RISC-V仕様書
Zigベアメタル開発
QEMU RISC-V
---
この課題を通じて、OSの下層で何が起こっているかを深く理解し、Zigの低レベルプログラミング機能を実践的に習得できます。組み込みシステムの世界への第一歩を踏み出しましょう!