概要
ベアメタル(Bare Metal)とは、OS(オペレーティングシステム)を使用せず、マイコンのハードウェアを直接制御するプログラミング手法です。「OSなし」「Baremetal」「スーパーループ設計」などとも呼ばれます。
RTOSや組み込みLinuxが提供するタスク管理・ファイルシステム・ネットワークスタックなどの高レイヤー抽象化を使わず、CPUレジスタ・ペリフェラルレジスタを直接操作してシステムを構築します。
ベアメタルの最大の特徴は、予測可能な動作・最小リソース消費・短い起動時間です。処理の軽さが必要なセンサーノードや、数百バイトのRAMしかないマイコンでは、ベアメタルが唯一の選択肢となります。
歴史・背景
ベアメタルは組み込みソフトウェアの最も原始的な形態であり、組み込み開発の黎明期からある手法です。1970〜80年代の組み込みシステムはOSなしが当たり前であり、アセンブリ言語やC言語でハードウェアを直接制御していました。
1990年代にITRON・VxWorksなどのRTOSが産業向けに普及し始めましたが、家電・玩具・センサー向けの小型マイコンではRAM不足のためOSを使えないケースが多く、ベアメタル開発が継続されました。
2000年代にARMがCortex-M系を投入し、STM32・nRF51など安価なARMマイコンが普及しました。メーカー提供のHAL(Hardware Abstraction Layer)ライブラリの充実により、ベアメタル開発でも生産性が向上しました。現在でも、超低消費電力のIoTセンサーノードや単純な制御ロジックの機器では、ベアメタルが積極的に選択されています。
技術仕様
ベアメタルの基本構造
ベアメタルプログラムの最も典型的な構造は「スーパーループ(super loop)」です:
/* 典型的なベアメタルプログラム構造 */
int main(void) {
/* 1. ハードウェア初期化 */
SystemInit(); /* クロック設定 */
GPIO_Init(); /* GPIO設定 */
UART_Init(); /* UART設定(115200bps等) */
ADC_Init(); /* ADC設定 */
Timer_Init(); /* タイマー設定 */
/* 割り込み有効化 */
__enable_irq();
/* 2. スーパーループ(無限ループ) */
while (1) {
/* センサー読み取り */
uint16_t adc_val = ADC_Read();
/* データ処理 */
float voltage = adc_val * 3.3f / 4096.0f;
/* 出力 */
UART_SendFloat(voltage);
/* 待機(ポーリング or タイマー待ち) */
Delay_ms(100);
}
}
割り込みを組み合わせた設計
スーパーループだけでは応答性に問題が生じるため、時間制約のある処理はISR(割り込みサービスルーティン)で処理し、重い処理はメインループで行うパターンが一般的です:
/* グローバルフラグ(ISRからメインループへの通知に使用) */
volatile uint8_t uart_rx_flag = 0;
volatile uint8_t uart_rx_data = 0;
/* UART受信割り込みハンドラ */
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) {
uart_rx_data = USART1->DR; /* データ読み取り(割り込みクリア) */
uart_rx_flag = 1; /* フラグセット */
}
}
/* メインループでフラグをポーリング */
while (1) {
if (uart_rx_flag) {
uart_rx_flag = 0; /* フラグクリア(アトミックに行う必要あり) */
processCommand(uart_rx_data);
}
/* 他の処理... */
}
スタートアップコード
ベアメタルでは電源投入後からmain()が呼ばれるまでの処理(スタートアップコード)を理解することが重要です:
電源投入
↓
リセットハンドラ(startup_stm32f4xx.s または startup_stm32f4xx.c)
↓
1. スタックポインタ設定(SPをSRAMの末尾に設定)
2. ベクタテーブルの設定(VTOR)
3. .data セクション初期化(フラッシュから初期値をRAMへコピー)
4. .bss セクション初期化(ゼロクリア)
5. SystemInit()呼び出し(クロック等の最低限初期化)
↓
main()呼び出し
メモリマップの理解
ベアメタルではメモリマップを直接扱います。STM32F4の例:
0x00000000 - 0x001FFFFF Flash(コード・定数・.data初期値)
0x20000000 - 0x2001FFFF SRAM(.data・.bss・スタック・ヒープ)
0x40000000 - 0x5FFFFFFF APB/AHBペリフェラルレジスタ
0xE0000000 - 0xE00FFFFF CoreSight(デバッグ用)
0xE000E000 - 0xE000EFFF SCS(System Control Space:NVIC・SysTick等)
ペリフェラルレジスタへのアクセス例:
/* GPIOBのODRに直接アクセス(GPIOB->ODR = レジスタアドレス) */
#define GPIOB_BASE 0x40020400UL
#define GPIOB_ODR (*((volatile uint32_t *)(GPIOB_BASE + 0x14)))
GPIOB_ODR |= (1 << 0); /* GPIOB pin0 をHighに */
GPIOB_ODR &= ~(1 << 0); /* GPIOB pin0 をLowに */
/* 実際にはベンダーHALのマクロを使うことが多い */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
動作原理
ベクタテーブルと割り込み処理
ARM Cortex-Mでは、リセット・各割り込み・例外ハンドラのアドレスが「ベクタテーブル」に並んでいます:
/* ベクタテーブル(startup_stm32f446xx.s より) */
/* フラッシュの先頭(0x08000000)に配置 */
__Vectors:
.word _estack /* スタックポインタ初期値 */
.word Reset_Handler /* リセットハンドラ */
.word NMI_Handler /* NMI */
.word HardFault_Handler /* ハードフォルト */
/* ... 各割り込みハンドラ ... */
.word USART1_IRQHandler /* UART1割り込み */
.word USART2_IRQHandler /* UART2割り込み */
タイマーによる時間管理
ベアメタルで一定周期の処理を行う場合、ハードウェアタイマーの割り込みを使います:
/* SysTick(1ms間隔)割り込みでタイムスタンプ更新 */
volatile uint32_t ms_tick = 0;
void SysTick_Handler(void) {
ms_tick++;
}
uint32_t GetTick(void) {
return ms_tick;
}
void Delay_ms(uint32_t ms) {
uint32_t start = GetTick();
while ((GetTick() - start) < ms);
}
ステートマシン設計
ベアメタルで複数の処理を管理するもう1つのパターンは、状態機械(ステートマシン)です:
typedef enum {
STATE_INIT,
STATE_IDLE,
STATE_SENSING,
STATE_TRANSMIT,
STATE_SLEEP
} AppState;
AppState state = STATE_INIT;
while (1) {
switch (state) {
case STATE_INIT:
hardware_init();
state = STATE_IDLE;
break;
case STATE_IDLE:
if (timer_expired()) state = STATE_SENSING;
break;
case STATE_SENSING:
read_sensor();
state = STATE_TRANSMIT;
break;
case STATE_TRANSMIT:
if (transmit_done()) state = STATE_SLEEP;
break;
case STATE_SLEEP:
enter_deep_sleep(); /* 割り込みで復帰 */
state = STATE_IDLE;
break;
}
}
用途・ユースケース
超低消費電力センサーノード
コイン電池で数年動作するBLEビーコンやLPWAセンサーノードでは、OSのオーバーヘッドなしにディープスリープを最大限活用するベアメタルが有利です。起動→センサー読み取り→送信→スリープのサイクルをμs単位で制御できます。
単機能の制御器
DCモータードライバIC、LEDドライバ、電源制御など、処理が単純で高速応答が必要な制御器ではベアメタルが最適です。処理ステップが少ないため、割り込みレイテンシを最小化できます。
ブートローダー
ブートローダー自体はベアメタルで実装されています。U-Bootのような高機能なものも、起動初期はベアメタルで動作します。
コスト最重視の量産品
非常に廉価なマイコン(PIC12Fシリーズ、8051系等)ではRAMが64〜256バイトしかなく、RTOSを動かすことができません。こうした機器ではベアメタルが唯一の選択肢です。
実装・開発のポイント
アトミックなフラグ操作
ISRとメインループが共有変数を使う場合、レースコンディションを防ぐためにフラグアクセスをアトミックに行う必要があります:
/* volatile 修飾子 + 適切なメモリバリア */
volatile uint8_t flag = 0;
/* メインループでのフラグクリア */
__disable_irq(); /* 割り込み禁止(クリティカルセクション開始) */
uint8_t local_flag = flag;
flag = 0;
__enable_irq(); /* 割り込み許可(クリティカルセクション終了) */
if (local_flag) { /* 処理 */ }
デバッグ手法
ベアメタルのデバッグには以下の手法が使われます:
- JTAG / SWDデバッガ: GDBとOpenOCDで実行中のレジスタ・メモリを確認
- UART printf デバッグ:
printf()→UARTに出力してPC側でモニタリング - ロジックアナライザ: GPIOをトグルしてタイムスタンプを計測
- LEDトグル: 最もシンプルなデバッグ手法。処理の到達確認に使用
スタックサイズの見積もり
ベアメタルでは通常、リンカスクリプトでスタックサイズを静的に確保します:
/* リンカスクリプト(STM32F4例) */
_Min_Stack_Size = 0x400; /* 最低1KB */
_Min_Heap_Size = 0x200; /* ヒープ(不使用なら0でも可) */
他技術との比較
ベアメタル vs RTOS
| 項目 | ベアメタル | RTOS |
|---|---|---|
| ROM/RAM消費 | 最小 | 数KB〜数十KB追加 |
| 複数処理の管理 | 手動(ステートマシン等) | タスク・優先度スケジューラ |
| 応答性 | 設計次第で決定論的 | 優先度スケジューラで制御 |
| 開発難易度 | シンプルな処理なら低い | 初期学習が必要 |
| デバッグのしやすさ | シンプルで追いやすい | タスク間インタラクションが複雑 |
| 向く用途 | 単機能・超低消費電力 | 複数タスク・通信処理が複雑な場合 |
ベアメタルとRTOSの選択基準は「並行して動かす処理の複雑さ」です。処理が3〜4種類以上あり、それぞれが時間制約を持つ場合はRTOSの導入が管理しやすくなります。