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つの課題
- Zigエコシステムの未成熟:C/Rustに比べ組み込みHALライブラリやドライバが少ない
- ベアメタルデバッグの困難さ:OS支援のデバッグインフラが不足
- comptimeの学習コスト:コンパイル時メタプログラミングパターンへの適応が必要
- ハードウェア抽象レイヤの欠如:レジスタマッピングとドライバの手動実装が必要
- ツールチェーンの安定性: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)でアライメントを指定 |
高度な最適化
- comptimeレジスタ生成:comptimeでレジスタ構造体を自動生成し、手書きエラーを排除
- ゼロコピーDMA転送:alignとvolatileでゼロコピーのペリフェラルデータ転送を実現
- 割り込み優先度グループ化:NVIC優先度グループを設定し、重要な割り込みのプリエンプションを防止
- 低消費電力モード:WFI命令とペリフェラルクロックゲーティングでスタンバイ電力をμAレベルに削減
- OTAファームウェアアップグレード:デュアルバンクFlash設計で安全なワイヤレスファームウェア更新を実現
比較分析
| 次元 | Zig | C | Rust (no_std) | Assembly |
|---|---|---|---|---|
| メモリ安全性 | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| クロスコンパイル | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| コードサイズ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| エコシステム | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| コンパイル速度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 学習曲線 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
まとめ:Zig組み込みベアメタル開発は、comptimeメタプログラミングと組み込みクロスコンパイルにより、安全性と開発効率で従来のC開発を凌駕します。Zigはメモリ安全性を求めつつRustの学習コストを避けたい組み込みチームに適しています。2026年、Zig組み込みエコシステムはまだ成長中ですが、コア言語機能はプロダクション品質のファームウェア開発に十分です。
オンラインツール推薦
- JSONフォーマッター:/ja/json/format
- Hash計算:/ja/encode/hash
- cURL to Code:/ja/dev/curl-to-code
ブラウザローカルツールを無料で試す →
#Zig嵌入式#裸机开发#Zig交叉编译#no-std#2026#编程语言