組み込みとC言語


IoT 用デバイスや、リアルタイム性が要求されるような機器を開発する時、ワンチップマイコンが使われることが依然としてあるかと思う。

組み込み系開発の経験の無い人に教え欲しいと言われることが、たまにあるので、自分の知っている範囲でおおまかに説明してみたいと思う。

マイコンの構成について

ワンチップマイコンは、CPU、プログラム用フラッシュメモリ、RAM、タイマ、 入出力機能(I/O)、 通信機能(シリアル通信など)などがワンチップに
収められたものだ。

レジスタ

CPU には、制御用のレジスタと、汎用レジスタがある。

制御用のレジスタには、以下のようなものがある。

  • プログラムカウンタ
  • スタックポインタ
  • ステータスレジスタ(プログラム・ステータス・ワードなど、各社名称が異なる)

プログラムカウンタはプログラムの実行中のアドレスを、スタックポインタは現在のスタックのアドレスを、ステータスレジスタは、割り込みの状態や演算結果などを持っている。

汎用レジスタは、数個あって、計算やメモリの参照に使う。汎用レジスタは CPU にもよるが、レジスタによって用途が違うことがある。特にアキュムレータと呼ばれる計算専用のレジスタがあることが多い。レジスタは高速に動作するが、足りない時はRAMを使う。(レジスタに比べると遅い。)

メモリ空間

0番地から始まるアドレスから

  • プログラム用フラッシュ
  • RAM
  • SFR(Special Function Register)

のように配置されるが、配置はマイコンによって異なる。

典型的には、プログラム用フラッシュメモリの先頭に先頭に、プログラム用のフラッシュメモリがあり、ここに割り込みベクタと、プログラムが配置される。電源を切っても消えないようになっている。

割り込みベクタの先頭は一般には、リセットベクターで、電源投入後実行するプログラムの番地が書かれている。

RAMは変数、スタック等の記憶領域として使われる。

スタックであるが、CPU はサブルーチンコール、または、割り込み発生時に、プログラムカウンタ(と割り込みの場合はステータスワード)をスタックに積み、リターンすると(割り込みの場合はステータスワードをスタックから戻し)プログラムカウンタをスタックから戻し、サブルーチンコール/割り込み発生の前の状態に復帰する。

スタックは、また、汎用レジスタの一時的な退避(push)/復旧(pop)用、局所変数領域として使われる。

SFR はタイマ、I/O、通信などの制御とデータの読み書き行うための特殊機能レジスタ領域。

C 言語 tips

CPU はメモリから機械語(インストラクションコード)を読み出して実行するが、この機械語を生成するために使うのがアセンブリ言語である。C言語は元々、このアセンブリ言語を出力するものだった。

インストラクションコードは各マイコン毎に違うが、それでは移植性も悪く、記述もしづらいため、可能な部分はC言語を使用して記述するのが一般的であろう。

メモリ、スタック等の初期化をして C 言語の main ルーチンを呼び出すまでの部分(スタートアップルーチン)は、アセンブラで記述することになるだろう。

メモリ割り当て

組み込み系で少し注意を要するのが C 言語の定数、変数であろうか。

定数、グローバル/スタティック変数、初期化の付いたグローバル/スタティック変数は自動的には割り当てられず、開発環境(IDE)のほうで、指定が必要となる。(セクションと呼ばれる)

例えば、Renesasのデータとプログラムのセクション割り当てにある、「セクション再配置属性」。プログラムは、.text 領域に、const は const に、グローバル/スタティック変数は、data(初期化あり) または bss(初期化なし) に割り当てられる。事前にどこに置くか計画しておいたほうがよいだろう。

ローカル変数はスタック上に確保される。局所変数として大きな配列などを取ってしまうと、スタック上に確保しようとするが、スタックとして割り当てた領域をオーバーしてしまう(そのようなチェックはされない)ため、グローバル/static に宣言した変数領域を上書きして壊してしまう。したがって、組み込みにおいては、どの程度のサイズの局所変数を使うのか、事前に見積る必要がある。

SFR へのアクセス

C 言語でタイマ、I/O、通信などの SFR にアクセスするには、ポインタを使う。 SFR は書き込み結果が読めるとは限らず、通常のポインタとは異なるので volatile uint8_t * のような宣言にする。

型宣言

int のサイズは CPU によって違うが、組み込み系では顕著だ。私は stdint.h をインクルードして uint32_t などとするようにしている。

printf

デバッグ用に printf を使いたいと思うかもしれないが、まず、シリアル通信機能などを使って、getchar、putchar を実装することが必要。

RTOS(リアルタイムOS)

簡単な機能のものであれば、main 内でループして、OSを使わないという選択肢も多いと思う。

しかし機能が複雑な場合は、uITRON や freertos などといった RTOS を使ってタスクに分割するほうがわかりやすいし、保守性、拡張性も高い。

RTOS は、以下のような機能を持っている。

  • タスクスケジューリング
  • メッセージキュー
  • 同期(セマフォ、mutex など)
  • メモリ管理
  • タイマ管理

uITRON であれば、タスクが起動し、タイマ、セマフォ、メッセージなどのイベント待ちになり、イベントを受けてタスクが実行される。同時に待ちがあると、優先度に応じてタスクが選ばれる。 (割り込みでなければ、次の待ちになるまで他のタスクに切り替わったりしない。)

タスク間はメッセージキューなどでデータの受け渡しを行う。

デバッグ

プログラムをコンパイルした後、デバッガでターゲットに書き込む。IDE から実行し、ブレークポイントを指定して止めることができる。

ブレークポイントはハードブレークポイトとソフトブレークポイントがある。ソフトブレークポイントは、メモリをブレーク用の命令に書き換えてブレークする。ハードブレークポイントは、指定のアドレスにマッチすると止まるが、設定できる数が限られている。(1個〜数個だと思う。)

ウォッチドッグタイマを使っている場合は、デバッグの時だけ define などで無効になるようにしてデバッガを使う。(ブレークしている間にリセットされるので)