マイコンのプログラミング

注意

実験用CPUボードの概要

この実験では、写真のようなボードを使います。

このボードには、次のようなものが載っています。 (LED:える・いー・でぃー; Light Emitting Diode=発光ダイオード)

これらが、次の図のように接続されています。

まずはここでは、この全体構成図の詳細までみる必要はありません。 載っているLEDやスイッチなどが、CPUが実行する プログラムによって制御できるんだ、ということだけ、 頭に入れておいてください。

メモリマップドI/O

では、LEDやスイッチなどは、CPUが実行するプログラムからは どのように操作すればよいのでしょうか。

「CPUがプログラムを実行する」とは、メモリに入っている プログラム(作業手順書)を順に「読み出し」、解釈して 実行し、必要に応じて結果をメモリに「書き込む」こと、 と言うことができます。

そしてメモリから命令やデータを読み書きするときには、 メモリの中の場所を「アドレス(address)」という数値によって 指定をするのでした。

このボードでは、LEDやスイッチなどの、入出力装置(Input/Output; I/Oと略記します)も、この「メモリに対する読み書き(アクセス)」と 同様に行うことができるようになっています。 このような仕組みを メモリマップドI/O (Memory-mapped I/O)と呼びます。

具体的には、それぞれのI/O装置が、次のように 割り当て(memory-mapped)されています。
I/O装置 アドレス(16進数表記) アクセス方法
7セグメントLED(0桁目) 0x4000番地 書き込み
7セグメントLED(1桁目) 0x4001番地 書き込み
7セグメントLED(2桁目) 0x4002番地 書き込み
7セグメントLED(3桁目) 0x4003番地 書き込み
スイッチ(8個) 0x4004番地 読み出し
LED(8個) 0x4005番地 書き込み
ちなみに0x***という表記は、***が16進数であることを表すC言語風の表記です。 つまち0x4000とは、16進数で「4000」であることを表します。(よんせん、ではない)

例えば、0x4004番地を「読み出す」と、スイッチの状態がわかり、 0x4000番地に数値を「書き込む」と、7セグメントLEDの0桁目(右端)の 表示を設定できる、というわけです。

なお、書き込み用のものを読み出そうとしても、 正しい値を読み出すことはできません。 つまり右端の7セグメントLEDに表示されている値を調べようと思って、 次のような記述をしても、正しい値は得られません。 (正しくない値が読み出される可能性が高い。 偶然表示されている値が得られることもあるが、 得られないことも多い。 詳細はこちらをどうぞ。)

  i = num0;        /* NG (read)  */
  if (num0 == 0)   /* NG (read) */

LED・SWの操作

メモリマップドI/O装置は、プログラムを書くときには次のような 表記によって、「変数」として定義をすることになっています。 (「volatile」, 「BYTE xdata」, 「_at_以降」は 「おまじない」と思っておいてかまいません。)
volatile BYTE xdata num0       _at_ 0x4000;  // 0 digit of 7seg.LED
volatile BYTE xdata num1       _at_ 0x4001;  // 1 digit of 7seg.LED
volatile BYTE xdata num2       _at_ 0x4002;  // 2 digit of 7seg.LED
volatile BYTE xdata num3       _at_ 0x4003;  // 3 digit of 7seg.LED
volatile BYTE xdata sw         _at_ 0x4004;  // SWs
volatile BYTE xdata led        _at_ 0x4005;  // LEDs
例えば、「led」というBYTE型(1バイト=8ビット)の変数が 0x4005番地に割り当てられています。 この0x4005番地の内容は、先のとおり「LED(8個)の点灯・消灯を決める メモリマップドI/O」でした。 そのため、変数ledに値を書き込むと、実際には0x4005番地に 書き込まれ、結果としてその値に応じて各LEDが点灯・消灯することに なります。 なおここで、変数ledの値は1バイト=8ビット、つまり 8組の「0または1の値」ですが、 その各桁が、8個のLEDそれぞれの点灯・消灯に対応していることに 注意しましょう。

例えばこの変数ledに、次のように値0x81を代入するとします。

  led = 0x81;
ここで代入している値0x81は、2進数で書くと"10000001"ですから、 値が1になっている、一番左端のLED(LED7)と一番右端のLED(LED0)のみが 点灯し、それ以外は消灯することになります。

同様に例えば、 1バイトの変数swの値を読み出すと 8個のスイッチそれぞれの状態がわかることになります。 つまり8ビットのそれぞれが8個のスイッチに対応し、 最上位が左端のSW7、最下位が右端のSW0の値をあらわし、 各スイッチが押されていれば、その桁(ビット)の値が1に、 押されていなければ0になります。 そこで、例えばSW3の値をif文で調べたければ、 次のように書けばよいでしょう。

  if ((sw & 0x08) != 0){
    ..
詳細はビット操作を参照。

プログラムの作成・編集・コンパイル

では早速、実際に実験ボード上でプログラムを実行し、 スイッチやLEDを操作してみましょう。

まず、実験用のフォルダを作成します。 スタート→コンピューター→Zドライブの中、 または、「マイ ドキュメント」の中に、 実験第1用のフォルダを作成します。 今後の作業はすべてこの中で行うことにします。 なお、「Zドライブ」または「マイ ドキュメント」に置いたファイルは どのパソコンでも共有されます。

次に、先ほどのフォルダ内に、適切な名称で、これからの作業用の フォルダを作成します。 そこにサンプルプログラムとして、 sample.csample.Uv2ダウンロードします。 その中にあるsample.Uv2の次のアイコンをダブルクリックすると、 コンパイラなどが統合された開発環境uVision2が起動します。


この左側のウインドウ内の、Target1→SourceGroup1と順に +マークをクリックすると、sample.cが見つかるはずですので、 それをダブルクリックすると、右側のウインドウに、 プログラムsample.cの内容が表示されます。

#include "Fx2.h"
#include "Fx2regs.h"

// definition of memory-mapped I/O devices and external memories
volatile BYTE xdata num0       _at_ 0x4000;  // 0 digit of 7seg.LED
volatile BYTE xdata num1       _at_ 0x4001;  // 1 digit of 7seg.LED
volatile BYTE xdata num2       _at_ 0x4002;  // 2 digit of 7seg.LED
volatile BYTE xdata num3       _at_ 0x4003;  // 3 digit of 7seg.LED
volatile BYTE xdata sw         _at_ 0x4004;  // SWs
volatile BYTE xdata led        _at_ 0x4005;  // LEDs

main()
{
  // initialization
  CKCON |= 0x07; OEE = 0xef;
  led = 0; num0 = 10; num1 = 10; num2 = 10; num3 = 10;

  while(1){
    // write your code here
  }
}
最後のほうに、while文がありますが、while(1)ですので、 {}内が無限ループになっています。 そこでこの{}内に、実行するべきプログラムを書くことにします。 main()関数を終了させると予測不可能な動作をしますので、 無限ループを残しておいてください。

今回は、例として次のように書いてみましょう。

  while(1){
    // write your code here
    led = sw;
  }
プログラムを記述したら、保存した後、 メニューからProject→Build Target(F7)を 選んで、プログラムのコンパイルを行います。 (ファンクションキーのF7を押してもかまいません) コンパイルの過程が下のウインドウに表示されますので、 エラーなどがないか確認します。 プログラムの書き換えができないときは、 ファイルが読み取り専用になっていないか 確認しましょう。

プログラムの転送・実行

無事コンパイルが完了したら、 ボード上のCPUに転送し、実行させます。

ボードとパソコンのUSBポートをケーブルで接続します。 (このとき、最初だけドライバのインストールが行われることがあります) このとき、8個のLEDのすべてまたは一部が点灯することがありますが、 ここでは気にしなくてかまいません。

その後、 タスクバーにある"EZ-USB Control Panel"ボタン ボタン をクリックして EZ-USB Control Panel(以下、ControlPanelと略記)を起動します。 起動すると、次のような画面になっているはずです。

なおControlPanelの中央上の「Target」のところが"FX2"となっていることを 確認しておいてください。
EZ-USBのTarget付近
もしこのようになっていないときや、"No EZ-USB Device Found"と 表示されるときは、 ボードの接続を確認し、[Open All]ボタンを押してみましょう。

もし、以下のようなボード選択画面が表示された場合には、 "FX2"を選択してください。 Dialog for board selection

続いて、コンパイルしたプログラムを、ボードに転送します。 [Download]ボタンを押すと、転送するプログラムファイルを選ぶ 画面が現れますので、さきほど作成したフォルダ内の sample.hexを選択します。

すると転送が行われ、直ちにプログラムの実行が開始されます。

なお2回目以降プログラムを転送するとき、 同じプログラムファイルを修正しただけであれば、 uVision2でコンパイル後に、ControlPanelで[Re-Load]ボタンを 押すだけで転送・実行が行われます。 (つまり毎回転送するプログラムファイルを選択する必要がない) ちなみに[HOLD]ボタンでCPUをリセットさせることができます。 また[RUN]ボタンで、プログラムの実行を停止・再開することができます。

演習

このプログラムの動作を確認し、その動作原理を理解しましょう。 (ヒント:スイッチSW0〜SW7を押してみる)

注意

7セグメントLEDの操作

ボード上にある4個の7セグメントLEDは、4桁の数値を表示することが できますが、これらは、CPLD(Complex Programmable Logic Device)と 呼ばれるIC(集積回路)による専用の制御回路によって 制御されています。

そしてプログラム側では、 それぞれの桁に表示させたい値(0〜9)を、 それぞれのメモリマップドI/Oのアドレスに書き込むだけで その値が表示されます。 例えば0桁目に「3」と表示させたければ、 次のように記述すればよいことになります。

  num0 = 3;
なお書き込む値が0〜9の範囲外のときは、数値が表示されません。 また前述のように、値を読み出すこともできません。 これを確認するには、例えば、
  num0 = 3;
  num1 = 1;
  num2 = num0;
を実行してみると良いでしょう。

演習

0〜9999の値を表示させるプログラムを記述し、 いろいろな数値を表示させて動作を確認しましょう。 int型の変数から各桁の値を計算し、 それらを表示させるようにします。 このとき、int型の引数を持つ関数
void ShowInt(int d)
{
  // write your code here
}
のように作成し、main()関数から呼び出すようにしましょう。

注意とヒント:

応用:

演習

0〜9999の値を順に表示するプログラムを記述してみましょう。 数値を増加させるタイミングは、
  1. 一定時間ごと
  2. スイッチによって制御
    例えば、スイッチSW0を押している間は数値が増加する、 あるいはスイッチSW0を押している間は1ずつ増加、 SW1を押している間は10ずつ増加する、など。
の2通りを実装してみましょう。

ただし、 表示させる0〜9999の数値は 内部ではint型の変数一個に保持させることとし、 その変数の値から千の位〜一の位をそれぞれ求め、 それらを7セグメントLEDに表示させることとします。

ヒント:

変数の型とビット数と数値の限界

変数にはchar型やint型などの「型」がありますが、 これはその変数に割り当てられたメモリのビット数に対応します。 例えば、一般にchar型は8ビット(1バイト)となります。

一般には、ある型が何ビット(何バイト)であるかは、一定ではなく、 CPUの種類や用いるコンパイラに依存します。 変数の型が何バイトであるかはsizeofという演算子で得ることができます。 「sizeof (データ型)」という式は、 「データ型」が必要とするバイト数を与えます。 「sizeof 変数名」は、「変数名」のバイト数を与えます。

このバイト数は、その変数に代入できる数値の限界に関係してきます。 例えば符号なし8ビット(unsigned char型)では0〜255の数値を表現できます。 代入できる数値の限界には、符号の有無も関係しています。 同じ8ビット(1バイト)であっても、 符号つき8ビット(char型)では-128〜127になります。 このように変数がプログラム中でとりうる値に 注意する必要があります。

演習

次のような記述を含むプログラムを実行し、 整数型char, short, int, longが それぞれ何バイトになるのかを確認しましょう。 また、整数型char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long型が とりうる値の範囲を求めましょう。
  num3 = sizeof(long);
  num2 = sizeof(int);
  num1 = sizeof(short);
  num0 = sizeof(char);

注意:

応用

とりうる値の範囲を超えた際にどのような結果が得られるのかを確認しましょう。 例えば、
  unsigned char x;
である時に、
  x = 255;
  x = x + 1;

  x = 16 * 17;

  x = 16 * 16;
などを計算してみましょう。 また、符号を表示できるプログラムを作った場合は、
  char y;
である時に、
  y = 128;

  y = 16 * 17;

  y = 16 * 16;

  y = 16 * 15;

  y = 16 * 8;
なども計算してみると良いでしょう。

発展 (余裕があれば)

C言語には、volatile修飾子と呼ばれる変数の扱い方を規定する予約語があります。 このvolatile修飾子について調べてみましょう。
  1. どのような時に使われるのでしょう? (複数あります)
  2. 今回のプログラムでは、どれに該当するのでしょう?
  3. どのような効果があるのでしょう?
  4. C言語の修飾子にはvolatile以外にどのようなものがあるでしょう?
  5. volatile修飾子の効果を確かめてみましょう
    以下のプログラムで、 volatileがある場合と無い場合の違いを調べてみると良いでしょう。
    #include "Fx2.h"
    #include "Fx2regs.h"
    
    // definition of memory-mapped I/O devices and external memories
    volatile BYTE xdata num0       _at_ 0x4000;  // 0 digit of 7seg.LED
    volatile BYTE xdata num1       _at_ 0x4001;  // 1 digit of 7seg.LED
    volatile BYTE xdata num2       _at_ 0x4002;  // 2 digit of 7seg.LED
    volatile BYTE xdata num3       _at_ 0x4003;  // 3 digit of 7seg.LED
    volatile BYTE xdata sw         _at_ 0x4004;  // SWs
    volatile BYTE xdata led        _at_ 0x4005;  // LEDs
    
    main()
    {
      // initialization
      CKCON |= 0x07; OEE = 0xef;
      led = 0; num0 = 10; num1 = 10; num2 = 10; num3 = 10;
    
      while(1){
        num0 = 1;
        num1 = 2;
        num2 = num0 + 2;
        num3 = num1 + 2;
      }
    }
    
  6. この違いはどうして起こるのでしょう? (難易度高: 要ハードウェアの知識)
    ヒント: 7セグメントLED (num0〜num3) は書き込み専用で読み出せません。 無理に読み出しをすると、そのアドレスに書き込まれた値ではなく、 最後に外部データバスに出力された値をそのまま返します。

Index Prev: 実験目的 Next: D/A変換器・A/D変換器