【PICマイコン入門4】C言語ソースファイル分割のやり方

ファイル分割方法 ソフト設計



この記事でわかること

・プログラムを複数のファイルに分ける方法
・ヘッダファイルの使い方
・ファイル間で変数や関数を共用する方法

今回は、大規模プログラムを設計する際に、ファイルを分割する方法について解説します。

ここでは、新居浜高専PICマイコン学習キットVer.3のプログラムが、
複数ファイルから構成されているため、これを例にして説明します。

学習キットについては下記記事で解説しています。

このプログラムは学習キットを販売している秋月電子や、
キットの解説本を出版している技術評論社のHPから無償でダウンロードできます。

秋月電子 新居浜高専PICマイコン学習キットVer.3 ACアダプタ付

技術評論社 新居浜高専PICマイコン学習キット Ver.3 完全ガイド サポートページ

このVer.3にはキット上のLEDやブザー等を動作させる16種類のソフトと、
モニタプログラム(※1)が書込まれています。

※1:パソコンと通信を行い、コマンド操作により、LEDの点灯や、ブザーを鳴らしたりでき、
   簡単なプログラムを組んで動作させることができる。

このモニタプログラムはVer.3から追加されたもので、
Ver.2にあった16種類のソフトが記載されたソースファイルに対し、
モニタプログラムのソースファイルを追加した構成となっています。

本記事では、このソースファイルが複数あるプログラムが
どのような内容になっているか解説します。

※本記事はPICマイコン学習方法の一例を紹介するものであり、
 ここで紹介するキットやソフトの動作を保証するものではありません。
 キット等の購入については自己責任でお願いします。
 (不明点等の質問にはお答えできません)

<本ソフトの利用環境と設定について>
本記事におけるMPLABの開発環境は以下の通りです。
 ・MPLAB X IDE v.15
 ・XC8 v2.46
 ・Device Family Pack PIC16F1xxxx_DFP(1.24.387)

※ダウンロードしたプロジェクトファイル(NNCTkit_v3.X)は、
 そのままビルドするとエラーになるため、以下の変更を実施しました。

・helpstr.h 9行目
 変更前:const __section(“title”) unsigned char title[224]={
 変更後:const __section(“help_title“) unsigned char help_title[224]={

セクション名が「title」だとエラーになるので、別の名前にします。
ここでは「help_title」にしましたが、何でも良いです。

この時、配列名は「title」のままでも問題ありませんが、
他のデータ同様、配列名をセクション名と同じにする場合は、
この配列を使用しているソースファイルの方も直します。

・monitor.c 2093行目
 変更前:TX1REG = title[i];
 変更後:TX1REG = help_title[i];

<本記事での文章表現について>
本記事ではソフトウェアの処理を以下の表現で説明します。

 宣言:変数や関数の型(データのサイズや範囲)を記述すること。
    宣言時に、変数の値や関数の中身も記述することもできますが、
    ここでは、名前だけを宣言するプロトタイプ宣言のことを指します。 
    例:void Clock(void);

 定義:変数の値や、関数の中身(処理内容)を記述すること。
     例:Clock(void)
        {
         具体的な処理内容を記載
        }

 コール:関数を呼び出して使用すること。
     例:Clock();

複数のソースファイルがある構成

このソフト(プロジェクト名:NNCTkit_v3)は、
以下の4つのプログラムファイルから構成されています。

ソースファイル
 nnct_kit_v3.c
  16種類のプログラムを定義
 monitor.c
  モニタプログラムを定義

ヘッダファイル
 helpstr.h
  ヘルプ画面表示の文字列データを定義
 sampleprog.h
  サンプルプログラムのコード画面表示時の文字列データを定義

このうち、monitor.cと、2つのヘッダファイルはVer.3で追加されたものです。(※2)

※2:Ver.3ではPICマイコンがPIC16F18857と、Ver.2のPIC16F886から変更になっているため、
   ソースコードの一部が異なることから、Ver.2のファイル名(nnct_kit_v2.c)は違います。

<ソースファイルとヘッダファイルについて>
ソースファイル
プログラムが記載されたファイルで、
C言語でプログラムが書かれているので、拡張子が.cとなっています。
ちなみに、アセンブリ言語で書かれたものは拡張子が.asmになります。

ヘッダファイル
定数や関数の宣言が記載されたファイルです。(※3)
このファイルをソースファイルでインクルード(include:組込む)することで、
その宣言がソースファイルに記載された場合と同じになります。

※3:関数の中身であるプログラムそのものをヘッダファイルに記載することも可能ですが、
  一般的には、宣言部分だけを記載します。

ヘッダファイルについては、後で詳しく説明します。



別ファイルで定義された関数や変数を使う方法

ソースファイル(nnct_kit_v3.c)にはmain関数が記述されています。
このmain関数は最初に実行される関数で、ソフトの根幹となるものです。

main関数では、キットにある4つのスイッチのON/OFF状態の組合せに応じて、
16種類のプログラム関数(Clock関数など)をコールします。

関数はコールする前に宣言しておく必要があるため、
main関数よりも前に宣言を記述しておきます。

関数の宣言(void Clock(void); 等)で登場するvoidとは「からっぽ」を意味します。
先頭のvoidは、この関数に戻り値が無いことを示し、
括弧内のvoidは、この関数に引数が無いことを示します。

<戻り値と引数について>
戻り値:関数が実行された時に関数そのものにセットされる値
     宣言の例:unsigned char random(void); 
         // random関数は(unsigned char型の)戻り値がある(引数は無し)
          unsigned char型:符号なし8ビットの整数(0~255)

    上記関数をコールする場合の例:R_duty = random(); 
         // random関数の値(戻り値)をR_dutyにセットする

引数:関数を実行する時に入力される値
    宣言の例:void bin16_to_bcd(unsigned int bin16);
        // bin16_to_bcd関数は(unsigned int 型の)引数bin16がある(戻り値は無し)
        unsigned int型:符号なし16ビットの整数(0~65535)

    上記関数をコールする場合の例:bin16_to_bcd(ad_result);
                 // ad_resultの値を引数として関数に入力する

    上記関数を定義する場合の例:
     void bin16_to_bcd(unsigned int bin16) 
     {
      ・・・・
       bin16 += bin16;   // 引数bin16を+1する
      ・・・・
      }  
     // ad_resultの値がbin16にセットされて関数が実行される。

ここに提示した例は、全て本ソフトに出てくるものです。

この例以外にも、Clock関数のように戻り値も引数も無い関数や、
戻り値と引数が両方ある関数(このソフトでは出てきませんが)もあります。

宣言した各関数の定義は、main関数の後で記述されています。

Ver.3で追加のモニタプログラムはmonitor関数で行っており、
Clock関数内でコールしています。

これは、デジタル時計モード(Clock関数)動作時に、
スイッチ操作でモニタプログラムを起動させるようになっているためです。

このmonitor関数の定義も、
Clock関数などと同じソースファイル(nnct_kit_v3.c)に記述しても良いのですが、
規模が大きい(ソースコードが2000行以上ある)ため、
新たに設けたソースファイル(monitor.c)に記述しています。

このように別のソースファイルに記述した関数をコールする場合、
extern宣言を行う必要があります。

(extern:エクスターン、「外部」という意味)

ここでは、以下の様にextern宣言をしています。
 extern void monitor(void);

これによって、別のソースファイル(monitor.c)で宣言&定義されたmonitor関数を
ソースファイル(nnct_kit_v3.c)上でコールすることができます。

このextern宣言は、追加したソースファイル(monitor.c)内でも出てきています。

このdo1関数は、元々のソースファイル(nnct_kit_v3.c)内で使用していたもので、
monitor.c内でも使用(コール)できるようにするためにextern宣言をしています。

また、グローバル変数も、extern宣言することで、
別のソースファイルで定義した変数を使うことができます。

<グローバル変数とローカル変数について>
ソースファイル(nnct_kit_v3.c)内の①と②が、それぞれに該当します。

①グローバル変数:関数の外で宣言した関数で、同じソースファイル内の関数で共用できる。

②ローカル変数:関数の中で宣言した関数で、その関数内でのみ使用できる。
       ローカル変数名は、関数が違えば同じ名前でも無関係なので、
       本ソフトでは、繰返し制御用のカウント数として、iやjを各関数で使用してます。



ヘッダファイルの使い方

このソフトでは以下のヘッダファイルが使われています。

 #include   <pic16f18857.h>
 #include   <stdlib.h>
 #include   <stdio.h>

 #include   “helpstr.h”
 #include   “sampleprog.h”

上の3つは、2つのソースファイル(nnct_kit_v3.cとmonitor.c)両方で登場しますが、
下2つは、monitor.cのみにあり、今回のVer.3から追加されたヘッダファイルです。

pic16f18857.hは、PIC16F18857用のレジスタの名称やビット名が宣言されています。

ちなみに、ここのヘッダファイル名をxc.hにすれば、
MPLABのプロジェクトでデバイス設定したPICマイコンのヘッダファイルを
自動的にインクルードするので、
PICマイコンの種類に関係なくソースコードを共通にできます。

MPLABの使い方については下記記事を参照下さい。

stdio.hは、入出力やファイル操作等の標準入出力関係の関数を宣言しています。
(stdioは、standard input/outputの略)

stdlib.hは、C言語で使用される標準関数を宣言しています。
(stdlibは、standard libraryの略)

この3つは、コンパイラ内に標準で用意されているインクルードファイルで、
ファイル名を< >で囲むことで、
コンパイル時に、XC8コンパイラ内にあるヘッダーファイルを検索しにいきます。

(xc8 コンパイラVer.2.26の場合 C:¥Program Files¥Microchips¥xc8¥v2.46¥pic¥include)

下の2つ(helpstr.h、sampleprog.h)は、
このプログラムで作成したオリジナルのヘッダファイルで、
ファイル名をダブルクォーテーション( “ ” )で囲むことで、
プロジェクト(NNCTkit_v3)内にあるヘッダーファイルを検索しにいきます。

両ヘッダファイルは、モニタプログラムにおいて、
通信時にPC画面に表示させる文字データを宣言&定義しています。

これらの文字データは、monitor関数しか使用しないので、
ソースファイル(monitor.c)内に記載しても良いのですが、
プログラムコードの行数が膨大になるため、ヘッダファイルに分けています。

また、各文字データ(配列)はconst宣言することで、
変更されない定数となり、プログラムメモリ内に配置されます。

上記のsampleprog.hの場合、その後に続く、__section(“prog1”) をつけることで、
プログラムメモリ内にprog1というセクション名をつけた領域に配列prog1[ ]が配置されます。

ここでは、分かり易いようにセクション名を配列名と同じ名前にしていますが、
別の名前をつけても構いません。(セクション名をつけなくてもビルドは可能)

プログラムメモリに配置する理由ですが、
2つのヘッダファイルで定義しているデータ量が大きいためです。
(2つ合わせて1300行以上ある)

通常、変数はデータメモリに配置されますが、
プログラムメモリに比べ、データメモリの容量は小さい(※4)ことから、
プログラム動作で変更されることのない変数については、
データメモリに配置しないことで、メモリ不足になることを回避します。

 ※4:PIC16F18857の場合、プログラムメモリ:56kB、データメモリ:4kB(バイト)



ソースファイルを分割する

1つのソースファイルから、ファイルを分割する場合の実施例を紹介します。

ここでは、ソースファイル(nnct_kit_v3.c)が2000行以上あるので、
16種類のプログラムを行う関数(Clock関数など)毎にソースファイルを分けてみました。

また、各関数において、共通で使用するグローバル変数や関数の宣言部分を
新たに設けたヘッダファイル (kit16prog.h)に移しました。

このヘッダファイルの先頭には、以下のコードが記載されています。
 #ifndef _KIT16PROG_H
 #define _KIT16PROG_H

1行目の#ifndefは、識別子_KIT16PROG_Hが未定義なら、
このファイルの最後にある#endifまでの範囲をコンパイルします。

2行目の#defineで、識別子_KIT16PROG_Hを定義するので、
別のソースファイルで、kit16prog.hをインクルードしていても、
このヘッダファイルの内容はコンパイルされません。

これを行う理由は、同じ関数や変数を二重に宣言してしまうと
ビルドエラーになることを防止するためです。

この二重インクルード防止は、
ヘッダファイルstdlib.h、stdio.hにもありますが、
helpstr.h、sampleprog.hには記載がありません。

それは、この2つのファイルがmonitor.cしか使われていないため、
二重インクルードの恐れがないからです。

識別子の名前については、何でも良いのですが、
ヘッダファイルの名前に由来した大文字にするのが一般的です。

このファイル分割によって、
元々のソースファイル(nnct_kit_v3.c)は400行程度に小さくでき、
16種類のプログラム単位でソースファイルを分けたことで、
管理や変更がしやすくなりました。