Zig組み込みベアメタル実践:OSなしで安全なファームウェアを構築する5つのコアパターン

编程语言

Zig組み込み:Cの代替、ベアメタル開発の新選択

組み込み開発は長らくC言語に依存してきましたが、メモリ安全脆弱性の頻発、ビルドシステムの煩雑さ、クロスコンパイル設定の複雑さが課題です。Zigはシステムプログラミング言語として、comptimeメタプログラミング、暗黙的動作の排除、組み込みクロスコンパイルの3つの特徴により、組み込みベアメタル開発の新選択肢となっています。2026年、Zig組み込みはARM Cortex-MやRISC-Vなどのプラットフォームで強力な生産性を示しています。

本記事では5つのコアパターンを通じて、クロスコンパイル→ベアメタル起動→ペリフェラルドライバ→割り込み処理→ファームウェアリリースのフルチェーン実践を解説します。


コア概念

概念 説明
Zig システムプログラミング言語、Cの代替
ベアメタル OSなしでハードウェア上で直接実行する開発モード
comptime Zigコンパイル時計算、ゼロコスト抽象化の中核
クロスコンパイル あるプラットフォームで別のプラットフォーム向けにコンパイル
no-std 標準ライブラリに依存しない開発モード
ペリフェラルドライバ ハードウェアレジスタを直接操作するドライバコード
リンカスクリプト メモリレイアウトを制御するLDファイル
割り込みベクタ プロセッサ例外と割り込みのエントリテーブル

問題分析:Zig組み込みの5つの課題

  1. Zigエコシステムの未成熟:C/Rustに比べ組み込みHALライブラリやドライバが少ない
  2. ベアメタルデバッグの困難さ:OS支援のデバッグインフラが不足
  3. comptimeの学習コスト:コンパイル時メタプログラミングパターンへの適応が必要
  4. ハードウェア抽象レイヤの欠如:レジスタマッピングとドライバの手動実装が必要
  5. ツールチェーンの安定性:Zigは急速に反復中でAPIが変更される可能性

ステップバイステップ:5つのZig組み込みパターン

パターン1:Zigクロスコンパイルとプロジェクト構造

// build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.resolveTargetQuery(.{
        .cpu_arch = .thumb,
        .os_tag = .freestanding,
        .abi = .eabi,
        .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m4 },
    });

    const optimize = b.standardOptimizeOption(.{});

    const firmware = b.addExecutable(.{
        .name = "firmware.elf",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    firmware.setLinkerScript(b.path("linker.ld"));
}

パターン2:ベアメタル起動とリンカスクリプト

// src/startup.zig
const std = @import("std");

export fn _start() callconv(.Naked) noreturn {
    @as(*volatile u32, @ptrFromInt(0x2000_0000)).* = 0x2000_4000;
    asm volatile ("bl reset_handler");
    while (true) {}
}

export fn reset_handler() void {
    init_bss();
    init_data();
    main();
    while (true) {}
}

fn init_bss() void {
    const bss_start: [*]u8 = @ptrFromInt(@intFromPtr(&bss_start_addr));
    const bss_end: [*]u8 = @ptrFromInt(@intFromPtr(&bss_end_addr));
    @memset(bss_start[0 .. @intFromPtr(bss_end) - @intFromPtr(bss_start)], 0);
}

パターン3:レジスタマッピングとペリフェラルドライバ

// src/regs.zig
pub const GPIOA = struct {
    pub const base: u32 = 0x4002_0000;

    pub const MODER = @as(*volatile u32, @ptrFromInt(base + 0x00));
    pub const ODR = @as(*volatile u32, @ptrFromInt(base + 0x14));
    pub const BSRR = @as(*volatile u32, @ptrFromInt(base + 0x18));

    pub fn setPin(pin: u4, mode: u2) void {
        const shift: u5 = @intCast(@as(u5, pin) * 2);
        const mask: u32 = ~(@as(u32, 0b11) << shift);
        MODER.* = (MODER.* & mask) | (@as(u32, mode) << shift);
    }

    pub fn writePin(pin: u4, val: bool) void {
        if (val) {
            BSRR.* = @as(u32, 1) << pin;
        } else {
            BSRR.* = @as(u32, 1) << (pin + 16);
        }
    }
};

パターン4:割り込み処理とベクタテーブル

// src/interrupt.zig
const regs = @import("regs.zig");

var tick_count: u32 = 0;

export fn SysTick_Handler() void {
    tick_count += 1;
    if (tick_count % 500 == 0) {
        regs.GPIOA.writePin(5, tick_count % 1000 == 0);
    }
}

pub fn setup_systick() void {
    const systick_ctrl: *volatile u32 = @ptrFromInt(0xE000_E010);
    const systick_load: *volatile u32 = @ptrFromInt(0xE000_E014);
    systick_load.* = 16_000;
    systick_ctrl.* = 0b111;
}

パターン5:ファームウェアフラッシュとデバッグ

# ファームウェアのフラッシュ
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
  -c "program zig-out/bin/firmware.elf verify reset exit"

# GDBデバッグ
arm-none-eabi-gdb zig-out/bin/firmware.elf \
  -ex "target remote :3333" \
  -ex "monitor reset halt" \
  -ex "break main" \
  -ex "continue"

落とし穴ガイド

落とし穴1:レジスタのvolatile修飾忘れ

// ❌ 間違い:コンパイラがレジスタアクセスを最適化する可能性
const reg: *u32 = @ptrFromInt(0x4002_0000);
reg.* = 1;

// ✅ 正しい:volatileを使用
const reg: *volatile u32 = @ptrFromInt(0x4002_0000);
reg.* = 1;

落とし穴2:割り込みハンドラのシグネチャエラー

// ❌ 間違い:割り込みハンドラに戻り値がある
export fn SysTick_Handler() void { ... }

// ✅ 正しい:ベアメタル割り込みにはNaked呼び出し規約が必要(一部プラットフォーム)
export fn HardFault_Handler() callconv(.Naked) noreturn {
    while (true) {}
}

落とし穴3:スタックサイズ不足

/* ❌ 間違い:十分なスタック空間を予約していない */
_stack_top = ORIGIN(RAM) + LENGTH(RAM);

/* ✅ 正しい:スタックサイズを明示的に定義 */
_stack_size = 4K;
_stack_start = ORIGIN(RAM) + LENGTH(RAM) - _stack_size;
_stack_top = _stack_start + _stack_size;

落とし穴4:BSSセクションのゼロ初期化忘れ

// ❌ 間違い:BSS初期化をスキップ、グローバル変数の値が未定義
export fn reset_handler() void {
    main();
}

// ✅ 正しい:起動時にBSSセクションをゼロクリア
fn init_bss() void {
    @memset(@as([*]u8, @ptrFromInt(@intFromPtr(&bss_start)))[0..bss_len], 0);
}

落とし穴5:コンパイラアライメントの未処理

// ❌ 間違い:DMAバッファがアライメントされていない
var dma_buffer: [256]u8 = undefined;

// ✅ 正しい:キャッシュラインに強制アライメント
var dma_buffer: [256]u8 align(32) = undefined;

エラートラブルシューティング

# エラー 原因 解決方法
1 error: undefined symbol: __aeabi_memcpy コンパイラ組み込み関数の不足 compiler_rtを追加または手動実装
2 error: unable to create compilation クロスコンパイルターゲット非対応 target queryのcpu_archとos_tagを確認
3 Segmentation fault ランタイム スタックオーバーフローまたは無効メモリアクセス スタックサイズを増加、ポインタの有効性を確認
4 linker script error LDファイルの構文エラー MEMORYとSECTIONSの構文を確認
5 error: volatile store volatile型の不一致 レジスタポインタに*volatileを使用
6 HardFault ハードウェア例外(ヌルポインタ/アライメント) HardFault_Handlerでデバッグ情報を出力
7 openocd connection failed デバッガ未接続 ST-LinkドライバとUSB接続を確認
8 flash write failed チップの書き込み保護 openocdで読み出し保護を解除
9 error: comptime unable to evaluate コンパイル時計算の失敗 comptime式が静的に評価可能か確認
10 error: alignment mismatch メモリアライメントの不一致 align(N)でアライメントを指定

高度な最適化

  1. comptimeレジスタ生成:comptimeでレジスタ構造体を自動生成し、手書きエラーを排除
  2. ゼロコピーDMA転送:alignとvolatileでゼロコピーのペリフェラルデータ転送を実現
  3. 割り込み優先度グループ化:NVIC優先度グループを設定し、重要な割り込みのプリエンプションを防止
  4. 低消費電力モード:WFI命令とペリフェラルクロックゲーティングでスタンバイ電力をμAレベルに削減
  5. OTAファームウェアアップグレード:デュアルバンクFlash設計で安全なワイヤレスファームウェア更新を実現

比較分析

次元 Zig C Rust (no_std) Assembly
メモリ安全性 ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
クロスコンパイル ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
コードサイズ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
エコシステム ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐
コンパイル速度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
学習曲線 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐

まとめ:Zig組み込みベアメタル開発は、comptimeメタプログラミングと組み込みクロスコンパイルにより、安全性と開発効率で従来のC開発を凌駕します。Zigはメモリ安全性を求めつつRustの学習コストを避けたい組み込みチームに適しています。2026年、Zig組み込みエコシステムはまだ成長中ですが、コア言語機能はプロダクション品質のファームウェア開発に十分です。


オンラインツール推薦

ブラウザローカルツールを無料で試す →

#Zig嵌入式#裸机开发#Zig交叉编译#no-std#2026#编程语言