組み込み開発において、「なぜか動かない」「時々おかしくなる」「デバッグが困難」といった状況に遭遇することは日常茶飯事です。特に組み込み開発では、PCアプリ開発とは異なる特殊な制約や環境要因が絡み合い、原因特定が困難なバグが頻発します。
この記事では、組み込み開発の現場で10年以上の経験を持つ筆者が、実際に何度も遭遇し、多くの開発者が苦労している代表的なバグを10個厳選し、それぞれの原因と実践的な解決法を詳しく解説します。
1位:ウォッチドッグタイマーによる予期しないリセット
問題の症状
システムが定期的に、または特定の処理中に突然リセットされる。デバッガを接続すると正常に動作することが多い。
原因
ウォッチドッグタイマー(WDT)のクリアを忘れた、またはタイミングが間に合わない処理がある。
悪い例
void main_loop() {
while(1) {
process_data(); // 重い処理
communicate_with_server(); // 通信処理(時間がかかる)
update_display(); // 表示更新
// WDTクリアがループの最後にしかない
watchdog_clear();
delay(100);
}
}
解決法とベストプラクティス
// 解決法1: 適切な間隔でWDTをクリア
void main_loop() {
while(1) {
watchdog_clear(); // ループ開始時にクリア
process_data();
watchdog_clear(); // 重い処理の後にクリア
communicate_with_server();
watchdog_clear(); // 通信処理の後にクリア
update_display();
delay(100);
}
}
// 解決法2: タイマー割り込みでWDTを自動クリア
volatile bool wdt_clear_flag = false;
void timer_interrupt_handler() {
wdt_clear_flag = true;
}
void main_loop() {
while(1) {
if (wdt_clear_flag) {
watchdog_clear();
wdt_clear_flag = false;
}
// 通常処理
process_data();
communicate_with_server();
update_display();
delay(100);
}
}
予防策:
- WDTのタイムアウト時間を処理時間の3〜5倍に設定
- 長時間処理の途中で定期的にWDTをクリア
- デバッグビルドではWDTを無効化する設定を用意
2位:割り込み処理での共有変数の破損
問題の症状
変数の値が予期しない値に変わっている。特にマルチバイト変数で発生しやすい。
原因
割り込み処理とメイン処理で同じ変数にアクセスする際の排他制御不備。
悪い例
volatile uint32_t counter = 0;
volatile uint32_t total_time = 0;
void timer_interrupt() {
counter++;
total_time += 10; // 10ms間隔のタイマー
}
void main_loop() {
uint32_t local_counter, local_time;
// 危険: 読み取り中に割り込みが発生する可能性
local_counter = counter;
local_time = total_time;
printf("Counter: %lu, Time: %lu\n", local_counter, local_time);
}
解決法
volatile uint32_t counter = 0;
volatile uint32_t total_time = 0;
void timer_interrupt() {
counter++;
total_time += 10;
}
void main_loop() {
uint32_t local_counter, local_time;
// 解決法1: 割り込み禁止/許可で保護
disable_interrupts();
local_counter = counter;
local_time = total_time;
enable_interrupts();
printf("Counter: %lu, Time: %lu\n", local_counter, local_time);
}
// 解決法2: アトミック操作の実装
typedef struct {
uint32_t counter;
uint32_t total_time;
volatile bool update_flag;
} shared_data_t;
shared_data_t shared_data;
shared_data_t stable_data;
void timer_interrupt() {
shared_data.counter++;
shared_data.total_time += 10;
shared_data.update_flag = true;
}
void main_loop() {
if (shared_data.update_flag) {
disable_interrupts();
stable_data = shared_data;
shared_data.update_flag = false;
enable_interrupts();
}
printf("Counter: %lu, Time: %lu\n",
stable_data.counter, stable_data.total_time);
}
3位:スタックオーバーフローによる不具合
問題の症状
プログラムが突然異常終了したり、変数の値が破損したりする。特に関数呼び出しが深くなる処理で発生。
原因
スタック領域の不足。再帰処理、大きなローカル変数、深い関数呼び出しが主因。
悪い例
void recursive_function(int depth) {
char large_buffer[1024]; // 大きなローカル変数
// 大きなバッファの初期化
memset(large_buffer, 0, sizeof(large_buffer));
if (depth > 0) {
recursive_function(depth - 1); // スタックを消費
}
// バッファを使用した処理
process_data(large_buffer);
}
解決法
// 解決法1: 大きなバッファをstaticまたはヒープに配置
static char shared_buffer[1024]; // 静的領域に配置
void safe_recursive_function(int depth) {
// ローカル変数は最小限に
int local_var = depth;
if (depth > 0) {
safe_recursive_function(depth - 1);
}
// 共有バッファを使用
memset(shared_buffer, 0, sizeof(shared_buffer));
process_data(shared_buffer);
}
// 解決法2: 再帰をループに変換
void iterative_function(int max_depth) {
for (int depth = max_depth; depth > 0; depth--) {
memset(shared_buffer, 0, sizeof(shared_buffer));
process_data_with_depth(shared_buffer, depth);
}
}
// 解決法3: スタック使用量監視機能
void monitor_stack_usage() {
extern char _stack_start;
extern char _stack_end;
char current_sp;
size_t used = &_stack_start - ¤t_sp;
size_t total = &_stack_start - &_stack_end;
if (used > total * 0.8) { // 80%以上使用で警告
printf("WARNING: Stack usage high: %zu/%zu bytes\n", used, total);
}
}
4位:タイミングに依存するバグ(レースコンディション)
問題の症状
「たまに」発生する不具合。デバッガで追跡すると再現しない。負荷が高いときに発生しやすい。
原因
複数のタスクや割り込みが共有リソースに同時アクセスすることで発生。
悪い例
volatile bool data_ready = false;
volatile uint8_t sensor_data[10];
volatile int data_index = 0;
// センサー読み取り割り込み
void sensor_interrupt() {
sensor_data[data_index++] = read_sensor();
if (data_index >= 10) {
data_ready = true;
data_index = 0; // 危険: メイン処理で読み取り中の可能性
}
}
// メイン処理
void main_loop() {
if (data_ready) {
// 危険: コピー中に新しいデータが書き込まれる可能性
uint8_t local_data[10];
for (int i = 0; i < 10; i++) {
local_data[i] = sensor_data[i];
}
data_ready = false;
process_sensor_data(local_data);
}
}
解決法
// 解決法1: ダブルバッファリング
volatile uint8_t sensor_buffer[2][10]; // ダブルバッファ
volatile int write_buffer = 0;
volatile int read_buffer = 1;
volatile bool data_ready = false;
volatile int data_index = 0;
void sensor_interrupt() {
sensor_buffer[write_buffer][data_index++] = read_sensor();
if (data_index >= 10) {
data_index = 0;
// バッファを入れ替え
disable_interrupts();
int temp = write_buffer;
write_buffer = read_buffer;
read_buffer = temp;
data_ready = true;
enable_interrupts();
}
}
void main_loop() {
if (data_ready) {
uint8_t local_data[10];
// 読み取り専用バッファから安全にコピー
memcpy(local_data, (const uint8_t*)sensor_buffer[read_buffer], 10);
data_ready = false;
process_sensor_data(local_data);
}
}
// 解決法2: リングバッファの使用
typedef struct {
uint8_t data[100];
volatile int head;
volatile int tail;
volatile int count;
} ring_buffer_t;
ring_buffer_t sensor_ring;
bool ring_put(ring_buffer_t* ring, uint8_t data) {
disable_interrupts();
if (ring->count >= sizeof(ring->data)) {
enable_interrupts();
return false; // バッファフル
}
ring->data[ring->head] = data;
ring->head = (ring->head + 1) % sizeof(ring->data);
ring->count++;
enable_interrupts();
return true;
}
bool ring_get(ring_buffer_t* ring, uint8_t* data) {
disable_interrupts();
if (ring->count == 0) {
enable_interrupts();
return false; // データなし
}
*data = ring->data[ring->tail];
ring->tail = (ring->tail + 1) % sizeof(ring->data);
ring->count--;
enable_interrupts();
return true;
}
5位:電源・ノイズ関連の不安定動作
問題の症状
システムが時々フリーズ、リセット、誤動作を起こす。特に電源投入時やモーター動作時に発生。
原因
電源電圧の不安定、電磁ノイズ、グラウンドループなどのハードウェア要因。
ソフトウェアでの対策例
// 電源電圧監視
void power_supply_monitor() {
uint16_t vcc_voltage = read_vcc_voltage(); // 内蔵ADCで電源電圧測定
if (vcc_voltage < VCC_MIN_THRESHOLD) {
// 低電圧時の緊急処理
save_critical_data_to_eeprom();
disable_non_essential_functions();
enter_low_power_mode();
}
if (vcc_voltage > VCC_MAX_THRESHOLD) {
// 過電圧保護
emergency_shutdown();
}
}
// ノイズ対策のためのデジタルフィルター
typedef struct {
uint16_t buffer[FILTER_SIZE];
int index;
bool filled;
} noise_filter_t;
uint16_t noise_resistant_read(noise_filter_t* filter, uint16_t new_value) {
filter->buffer[filter->index] = new_value;
filter->index = (filter->index + 1) % FILTER_SIZE;
if (!filter->filled && filter->index == 0) {
filter->filled = true;
}
if (!filter->filled) {
return new_value; // バッファが満たされるまでは生の値を返す
}
// メディアンフィルターまたは平均値フィルター
uint32_t sum = 0;
for (int i = 0; i < FILTER_SIZE; i++) {
sum += filter->buffer[i];
}
return sum / FILTER_SIZE;
}
// チェックサム付きデータ保存でノイズ対策
typedef struct {
uint32_t data;
uint16_t checksum;
uint16_t magic;
} protected_data_t;
bool save_protected_data(uint32_t data) {
protected_data_t pdata;
pdata.data = data;
pdata.magic = DATA_MAGIC_NUMBER;
pdata.checksum = calculate_checksum((uint8_t*)&pdata.data, sizeof(pdata.data));
// 複数回書き込みで確実性を向上
for (int retry = 0; retry < 3; retry++) {
write_to_eeprom(PROTECTED_DATA_ADDR, &pdata, sizeof(pdata));
protected_data_t verify;
read_from_eeprom(PROTECTED_DATA_ADDR, &verify, sizeof(verify));
if (memcmp(&pdata, &verify, sizeof(pdata)) == 0) {
return true; // 書き込み成功
}
delay(10);
}
return false; // 書き込み失敗
}
6位:ペリフェラル設定ミスによる動作不良
問題の症状
センサーから値が読めない、通信ができない、PWMが出力されないなど、ハードウェア周りの機能が動作しない。
原因
レジスタ設定の誤り、クロック設定ミス、ピン機能設定の間違いなど。
悪い例と解決法
// 悪い例: ADC設定の不備
void adc_init_bad() {
// クロック有効化を忘れている
// RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; // コメントアウト
ADC1->CR2 |= ADC_CR2_ADON; // ADC有効化
// 変換時間の設定を忘れている
}
// 正しい例: 段階的で確実な設定
void adc_init_correct() {
// 1. クロック有効化
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // GPIO A のクロック
// 2. GPIOをアナログモードに設定
GPIOA->MODER |= (GPIO_MODER_MODE0); // PA0をアナログモードに
// 3. ADC設定
ADC1->SMPR2 |= ADC_SMPR2_SMP0_2; // 84サイクル変換時間
ADC1->CR2 |= ADC_CR2_ADON; // ADC有効化
// 4. 動作確認
delay_us(10); // 安定化待ち
// 5. テスト読み取り
ADC1->CR2 |= ADC_CR2_SWSTART;
while (!(ADC1->SR & ADC_SR_EOC));
uint16_t test_value = ADC1->DR;
printf("ADC test value: %d\n", test_value);
}
// デバッグ用のペリフェラル状態確認関数
void debug_print_adc_status() {
printf("=== ADC Status Debug ===\n");
printf("RCC->APB2ENR: 0x%08lX\n", RCC->APB2ENR);
printf("GPIOA->MODER: 0x%08lX\n", GPIOA->MODER);
printf("ADC1->CR2: 0x%08lX\n", ADC1->CR2);
printf("ADC1->SR: 0x%08lX\n", ADC1->SR);
printf("=======================\n");
}
// UART設定の確認例
bool uart_self_test() {
// ループバックテストで動作確認
const char test_data[] = "TEST";
char receive_buffer[10];
// ループバックモード設定(可能であれば)
USART1->CR1 |= USART_CR1_UE; // UART有効化
// データ送信
for (int i = 0; i < strlen(test_data); i++) {
while (!(USART1->SR & USART_SR_TXE));
USART1->DR = test_data[i];
}
// データ受信(外部ループバック必要)
int received_count = 0;
uint32_t timeout = get_tick() + 1000; // 1秒タイムアウト
while (received_count < strlen(test_data) && get_tick() < timeout) {
if (USART1->SR & USART_SR_RXNE) {
receive_buffer[received_count++] = USART1->DR;
}
}
if (received_count == strlen(test_data) &&
memcmp(test_data, receive_buffer, strlen(test_data)) == 0) {
printf("UART self-test: PASS\n");
return true;
} else {
printf("UART self-test: FAIL\n");
return false;
}
}
7位:エンディアン問題によるデータ化け
問題の症状
ネットワーク通信や外部デバイスとのデータ交換で、数値が正しく伝送されない。
原因
ビッグエンディアンとリトルエンディアンの違いを考慮していない。
問題例と解決法
// 問題のあるコード
typedef struct {
uint16_t command;
uint32_t data;
} network_packet_t;
void send_packet_bad(uint16_t cmd, uint32_t value) {
network_packet_t packet;
packet.command = cmd; // エンディアン変換なし
packet.data = value; // エンディアン変換なし
send_bytes((uint8_t*)&packet, sizeof(packet));
}
// 正しいコード
uint16_t htons_custom(uint16_t hostshort) {
return ((hostshort >> 8) & 0xFF) | ((hostshort & 0xFF) << 8);
}
uint32_t htonl_custom(uint32_t hostlong) {
return ((hostlong >> 24) & 0xFF) |
((hostlong >> 8) & 0xFF00) |
((hostlong & 0xFF00) << 8) |
((hostlong & 0xFF) << 24);
}
void send_packet_correct(uint16_t cmd, uint32_t value) {
network_packet_t packet;
packet.command = htons_custom(cmd); // ネットワークバイトオーダーに変換
packet.data = htonl_custom(value); // ネットワークバイトオーダーに変換
send_bytes((uint8_t*)&packet, sizeof(packet));
}
// 受信時の処理
bool receive_packet(network_packet_t* packet) {
if (receive_bytes((uint8_t*)packet, sizeof(*packet))) {
// ホストバイトオーダーに変換
packet->command = ntohs_custom(packet->command);
packet->data = ntohl_custom(packet->data);
return true;
}
return false;
}
// エンディアン安全なデータアクセス関数
uint32_t safe_read_uint32(const uint8_t* data) {
return (uint32_t)data[0] |
((uint32_t)data[1] << 8) |
((uint32_t)data[2] << 16) |
((uint32_t)data[3] << 24);
}
void safe_write_uint32(uint8_t* data, uint32_t value) {
data[0] = value & 0xFF;
data[1] = (value >> 8) & 0xFF;
data[2] = (value >> 16) & 0xFF;
data[3] = (value >> 24) & 0xFF;
}
8位:未初期化変数によるランダムな不具合
問題の症状
プログラムが時々異なる動作をする。特に電源投入直後の動作が不安定。
原因
変数の初期化を忘れた、またはスタック上の未初期化領域を使用している。
悪い例と解決法
// 悪い例: 未初期化変数
static int sensor_count; // 初期化されない場合がある
static bool system_initialized; // 初期化されない
void sensor_process() {
if (system_initialized) { // 未初期化の変数を判定に使用
sensor_count++; // 未初期化の変数に加算
if (sensor_count > MAX_SENSORS) {
reset_system();
}
}
}
// 正しい例: 適切な初期化
static int sensor_count = 0; // 明示的に初期化
static bool system_initialized = false; // 明示的に初期化
// システム初期化関数で確実に初期化
void system_init() {
sensor_count = 0;
system_initialized = false;
// ハードウェア初期化...
system_initialized = true;
}
// 構造体の初期化
typedef struct {
int value;
bool valid;
char name[32];
} sensor_data_t;
// 悪い例
void process_sensor_bad() {
sensor_data_t data; // 未初期化
if (data.valid) { // 未初期化フィールドの使用
// ...
}
}
// 正しい例
void process_sensor_good() {
sensor_data_t data = {0}; // ゼロ初期化
// または
memset(&data, 0, sizeof(data));
// 必要な値を設定
data.value = read_sensor();
data.valid = true;
strncpy(data.name, "Temperature", sizeof(data.name) - 1);
if (data.valid) {
// 安全に使用可能
}
}
// デバッグ用: 未初期化メモリの検出
void debug_fill_memory_pattern() {
// デバッグビルド時にメモリを特定パターンで埋める
extern char _sbss, _ebss; // BSSセクションの開始・終了
#ifdef DEBUG
memset(&_sbss, 0xCC, &_ebss - &_sbss); // 未初期化検出用パターン
#endif
}
// メモリリーク・未初期化検出のためのメモリプール
typedef struct {
void* ptr;
size_t size;
const char* file;
int line;
uint32_t magic;
} mem_debug_info_t;
#ifdef DEBUG
#define malloc_debug(size) malloc_debug_impl(size, __FILE__, __LINE__)
void* malloc_debug_impl(size_t size, const char* file, int line) {
// デバッグ情報付きメモリ確保
mem_debug_info_t* info = malloc(sizeof(mem_debug_info_t) + size);
if (info) {
info->ptr = (char*)info + sizeof(mem_debug_info_t);
info->size = size;
info->file = file;
info->line = line;
info->magic = 0xDEADBEEF;
// 確保したメモリを特定パターンで初期化
memset(info->ptr, 0xAA, size);
return info->ptr;
}
return NULL;
}
#else
#define malloc_debug(size) malloc(size)
#endif
9位:タイマー・カウンタのオーバーフローによる異常
問題の症状
長時間動作後に時刻がリセットされる、カウンタが巻き戻る、タイミングがずれるなど。
原因
タイマー値のオーバーフロー処理が不適切。特に32bitタイマーでも49日程度で発生。
問題例と解決法
// 問題のあるコード: オーバーフロー考慮なし
uint32_t last_time = 0;
bool check_timeout(uint32_t timeout_ms) {
uint32_t current_time = get_tick_count(); // ミリ秒カウント
// オーバーフロー時に負の値になってしまう
if ((current_time - last_time) > timeout_ms) {
last_time = current_time;
return true;
}
return false;
}
// 正しいコード: オーバーフロー対応
bool check_timeout_safe(uint32_t timeout_ms) {
static uint32_t last_time = 0;
uint32_t current_time = get_tick_count();
// オーバーフローに対応した差分計算
uint32_t elapsed = current_time - last_time;
if (elapsed >= timeout_ms) {
last_time = current_time;
return true;
}
return false;
}
// より安全な実装: 符号付き演算を使用
bool is_time_after(uint32_t time1, uint32_t time2) {
// オーバーフロー対応の時刻比較
return (int32_t)(time1 - time2) > 0;
}
bool check_timeout_safer(uint32_t timeout_ms) {
static uint32_t last_time = 0;
uint32_t current_time = get_tick_count();
if (is_time_after(current_time, last_time + timeout_ms)) {
last_time = current_time;
return true;
}
return false;
}
// 高精度タイマー処理(マイクロ秒対応)
typedef struct {
uint32_t seconds;
uint32_t microseconds;
} precise_time_t;
precise_time_t get_precise_time() {
precise_time_t time;
uint32_t timer_value = TIM2->CNT; // 32bitタイマーの現在値
static uint32_t last_timer_value = 0;
static uint32_t overflow_count = 0;
// オーバーフローの検出
if (timer_value < last_timer_value) {
overflow_count++;
}
last_timer_value = timer_value;
// 総マイクロ秒数の計算
uint64_t total_us = ((uint64_t)overflow_count << 32) + timer_value;
time.seconds = total_us / 1000000;
time.microseconds = total_us % 1000000;
return time;
}
// 長期間動作対応のカウンター
typedef struct {
uint32_t high; // 上位32bit
uint32_t low; // 下位32bit(ハードウェアカウンター)
} extended_counter_t;
void update_extended_counter(extended_counter_t* counter) {
static uint32_t last_hw_counter = 0;
uint32_t current_hw_counter = get_hardware_counter();
// ハードウェアカウンターのオーバーフローを検出
if (current_hw_counter < last_hw_counter) {
counter->high++;
}
counter->low = current_hw_counter;
last_hw_counter = current_hw_counter;
}
uint64_t get_extended_count(const extended_counter_t* counter) {
return ((uint64_t)counter->high << 32) | counter->low;
}
10位:デバッグ環境依存の問題
問題の症状
デバッガ接続時は正常動作するが、単体動作時に不具合が発生する。
原因
デバッガによるクロックの変更、タイミングの変化、メモリ初期化の違いなど。
対策例
// デバッガ検出とそれに応じた処理
bool is_debugger_connected() {
#ifdef STM32F4
// デバッガ接続時はDHCSR.C_DEBUGENビットが1
return (CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk) != 0;
#else
return false; // プラットフォーム依存
#endif
}
void init_system() {
if (is_debugger_connected()) {
printf("Debugger detected - using debug configuration\n");
// デバッグ用の設定
watchdog_disable();
slow_clock_for_debug();
} else {
printf("Standalone mode - using release configuration\n");
// リリース用の設定
watchdog_enable(5000); // 5秒タイムアウト
optimize_clock_settings();
}
}
// タイミング問題の回避
void delay_for_hardware_settling() {
if (is_debugger_connected()) {
// デバッガ接続時は長めの待機
delay_ms(100);
} else {
// 単体動作時は最小限の待機
delay_ms(10);
}
}
// デバッグビルドとリリースビルドの区別
#ifdef DEBUG
#define DEBUG_PRINT(fmt, ...) printf(fmt, ##__VA_ARGS__)
#define DEBUG_ASSERT(condition) \
do { \
if (!(condition)) { \
printf("ASSERTION FAILED: %s:%d %s\n", __FILE__, __LINE__, #condition); \
while(1); \
} \
} while(0)
#else
#define DEBUG_PRINT(fmt, ...)
#define DEBUG_ASSERT(condition)
#endif
// ログレベル制御
typedef enum {
LOG_ERROR = 0,
LOG_WARN = 1,
LOG_INFO = 2,
LOG_DEBUG = 3
} log_level_t;
static log_level_t current_log_level = LOG_INFO;
void log_message(log_level_t level, const char* format, ...) {
if (level <= current_log_level) {
va_list args;
va_start(args, format);
// デバッガ接続時は詳細ログ
if (is_debugger_connected()) {
printf("[%lu] ", get_tick_count());
}
vprintf(format, args);
printf("\n");
va_end(args);
}
}
// リリースビルドでの安全な動作確認
void production_self_test() {
bool all_tests_passed = true;
// 基本機能テスト
if (!test_eeprom()) {
log_message(LOG_ERROR, "EEPROM test failed");
all_tests_passed = false;
}
if (!test_adc()) {
log_message(LOG_ERROR, "ADC test failed");
all_tests_passed = false;
}
if (!test_uart()) {
log_message(LOG_ERROR, "UART test failed");
all_tests_passed = false;
}
if (!all_tests_passed) {
// テスト失敗時の処理
enter_safe_mode();
}
log_message(LOG_INFO, "System self-test: %s",
all_tests_passed ? "PASS" : "FAIL");
}
まとめ:バグを減らすための開発方針
予防策のベストプラクティス
- 段階的な開発とテスト
- 機能を小さく分けて実装
- 各段階でのテストを徹底
- ハードウェアとソフトウェアを分離してデバッグ
- 確実な初期化処理
- 全変数の明示的な初期化
- ハードウェア設定の段階的確認
- 初期化完了フラグの活用
- タイミング問題への対策
- 割り込み処理の最小化
- 共有リソースの排他制御
- ダブルバッファリングの活用
- ロバスト(頑健)な設計
- エラー処理の充実
- 異常状態からの復帰機能
- ウォッチドッグタイマーの適切な使用
- デバッグ支援機能の充実
- ログ機能の実装
- 状態監視機能
- 自己診断機能
最後に
組み込み開発でのバグは、PCアプリケーション開発とは異なる特殊性があります。限られたリソース、リアルタイム性の要求、ハードウェアとの密接な関係など、多くの制約の中での開発となります。
これらのバグパターンを知ることで、事前に対策を講じたり、問題が発生した際の原因特定を迅速に行うことができるようになります。特に重要なのは、「デバッガ環境と実機環境の違い」を常に意識し、実機での動作検証を怠らないことです。
組み込み開発は奥深く、経験とともにスキルが向上していく分野です。これらのバグパターンを参考に、より安定で信頼性の高いシステムを構築していきましょう。