【C言語】組み込みで構造体を使うときの注意点|アラインメント・パディング・通信データとの対応

  • URLをコピーしました!

今回は、組み込み開発で構造体を使うときに気をつけたいポイントを紹介します。

構造体は、設定値やセンサデータ、通信フレームなどをまとめて扱えるため便利です。
一方で、アラインメントパディングを意識せずに使うと、想定と異なるサイズになったり、通信データとずれたりして不具合の原因になります。

特に、マイコンのレジスタ定義、UARTやSPIの受信データ、ログ保存用バイナリ形式では、構造体のメモリ配置を正しく理解しておくことが重要です。
この記事では、組み込みで構造体を使う際の基本的な注意点と、実務で詰まりやすい部分をC言語のコード例を交えて解説します。

目次

構造体は便利だが「メモリ上でどう並ぶか」が重要

構造体は複数の変数をひとまとめにできる仕組みです。たとえば、センサの計測結果を次のように表せます。

typedef struct {
    uint8_t id;
    uint16_t temperature;
    uint32_t timestamp;
} SensorData;

このように書くと見た目は分かりやすいですが、メンバが宣言順に隙間なく並ぶとは限りません
コンパイラはCPUが効率よくアクセスできるように、メンバの間へ余分な領域を入れることがあります。これがパディングです。

アラインメントとパディングとは

アラインメント

アラインメントとは、データを特定の境界にそろえて配置することです。
たとえば uint32_t は4バイト境界に配置されたほうが扱いやすい環境があります。

パディング

パディングは、アラインメントを満たすために自動で挿入される詰め物です。
先ほどの構造体を例にすると、環境によっては次のような並びになります。

  • id : 1バイト
  • パディング : 1バイト
  • temperature : 2バイト
  • timestamp : 4バイト

この場合、見た目の合計は 1 + 2 + 4 = 7 バイトですが、実際の sizeof(SensorData) は 8 バイトになることがあります。

printf("%zu\n", sizeof(SensorData));

この差を見落とすと、通信仕様書どおりに送ったつもりでも、相手側で正しく解釈できないことがあります。

構造体を通信データにそのまま使うと危険な理由

UARTやCAN、BLEなどでバイナリデータをやり取りするとき、受信バッファを構造体へそのままキャストしたくなることがあります。

SensorData *data = (SensorData *)rx_buffer;

ただし、この方法には注意が必要です。

サイズが仕様と一致しないことがある

通信仕様では7バイトと決めていても、構造体は8バイトになることがあります。
この状態でそのまま送受信すると、データの境界がずれて解析に失敗します。

エンディアン差の問題がある

uint16_tuint32_t は、CPUによってバイト順が異なることがあります。
送信側と受信側で環境が違う場合、そのまま構造体へ詰めると値が変わって見えることがあります。

非アラインアクセスで落ちる環境がある

一部のCPUでは、適切にそろっていないアドレスへ多バイトアクセスすると例外になることがあります。
「PC上では動いたのに、実機で落ちる」というときはこの問題を疑う価値があります。

実務で安全に扱うための方法

1. 構造体サイズを sizeof で必ず確認する

まずは、想定サイズと実サイズが一致しているかを確認します。

typedef struct {
    uint8_t header;
    uint16_t value;
    uint8_t checksum;
} Packet;

この構造体は見た目では4バイトですが、環境によっては6バイトになる可能性があります。
レビュー時に「見た目で判断しない」ことが大切です。

2. メンバ順を見直して無駄なパディングを減らす

並び順を変えるだけでサイズが小さくなることがあります。

typedef struct {
    uint32_t timestamp;
    uint16_t temperature;
    uint8_t id;
} SensorData2;

サイズ最適化が重要な組み込みでは、サイズの大きい型から並べるのが基本パターンです。
ただし、通信仕様に合わせる構造体内部管理用の構造体は分けて考えたほうが安全です。

3. 通信データは手動でシリアライズする

通信や保存用のデータは、構造体をそのまま送るより、1バイトずつ詰めるほうが確実です。

tx_buffer[0] = id;
tx_buffer[1] = (temperature >> 8) & 0xFF;
tx_buffer[2] = temperature & 0xFF;

少し手間は増えますが、サイズ・並び順・エンディアンを明示できるため、不具合を減らしやすくなります。

4. packed指定は便利だが多用しない

コンパイラによっては、パディングを抑える指定が使えます。

typedef struct __attribute__((packed)) {
    uint8_t id;
    uint16_t temperature;
    uint32_t timestamp;
} PackedSensorData;

ただし、packed構造体はCPUによってはアクセス効率が落ちたり、非アラインアクセスの問題を招くことがあります。
「通信レイアウトを表現するためだけに使い、演算には通常の構造体へコピーする」という使い分けが無難です。

まとめ

組み込みで構造体を使うときは、文法そのものよりもメモリ配置をどう扱うかが重要です。

特に押さえておきたい点は次のとおりです。

  • 構造体にはパディングが入ることがある
  • sizeof は見た目の合計と一致しない場合がある
  • 通信データへそのままキャストするのは危険
  • エンディアンや非アラインアクセスも考慮する
  • 通信や保存用途では手動シリアライズが安全

構造体は便利ですが、「そのまま使える場面」と「明示的に変換すべき場面」を分けて考えることが、組み込みでの安定した実装につながります。

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

エンジニア。20代。組み込みエンジニアとして働き始めるも、働き方や業務内容に限界を感じ、 AI,Web3エンジニアを目指して勉強中。 エンジニアとして思うことや、学んだことを発信します。

目次