組み込みとC言語
2019/01/03 - moriya - ~6 Minutes
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 などで無効になるようにしてデバッガを使う。(ブレークしている間にリセットされるので)