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 などで無効になるようにしてデバッガを使います。(ブレークしている間にリセットされるので)