PICを用いて時計を作ってみた

この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2015 の投稿記事です。

こんにちは。2015年度新卒エンジニアのポニー村山です。

近頃会社内でプチIoTブームが起きています。本記事ではIoTの入門として、初学者である私がPICマイコンを用いてデジタル時計を作った際のはまり所などを書いていきます。これからIoTを始めてみたい方々の励みになればと思います。

PICとは

wikipediaより引用

PIC(ピック)とは、Peripheral Interface Controller(ペリフェラル インターフェイス コントローラ)の略称であり、マイクロチップ・テクノロジー社(Microchip Technology Inc.)が製造しているマイクロコントローラ(制御用IC)製品群の総称である。

PICの写真

PICとはいわゆるICチップであり、上記のような外見です。物にもよりますが200円ほどで買うことができます。C言語やアセンブリによりプログラムを書き込むことができ、電子回路を組むことでIoTデバイスが作成できます。

PICのいい点

個人的に次のような点が魅力に感じました。

  • 安い
  • 小さい
  • C言語で書ける

今回作成した物

こちらの動画にあるようなデジタル時計を作りました。時刻の表示には7セグメントディスプレイというものを用いています。

購入が必要な物

以下に購入が必要な物を挙げます。秋葉原などで以下の物を買ってきます。

ブレッドボード
半田なしで回路を組むための基板。今回は配線が多かったためこちらのブレッドボートを2つ繋げて使いました。
ブレッドボード・ジャンパーワイヤ
ブレッドボードを配線するために利用。
ブレッドボード・ジャンパーコード
こちらは、遠くへ配線するのに使います。
PICライター
PICにプログラムを書くために必要。
ピンヘッダ(オスL型)
PICライターとブレッドボードを接続するために必要。
ピンソケット
こちらをL型のピンヘッダと組み合わせて、PICライターとブレッドボードを接続します。
抵抗 10kΩ
抵抗がないと、電流が流れすぎて7セグメントディスプレイが壊れる可能性があります1)もっと抵抗値の小さな物でよかったかもしれません…
PICマイコン
こちらにプログラムを書き込みます。今回は20ピンの16F1829を用いました。
7セグメントディスプレイ
時刻を表示するのに使います。
タクトスイッチ
時刻合わせに使います。
水晶発振子
正確に時を刻むために必要。
コンデンサ
こちらと水晶発振子を組み合わせてクロック発振回路を組むことで、一定の周波数で信号を送れます。

開発環境の構築

MPLABのインストール

PICにプログラムを書き込むための開発環境の作り方は、以下のサイトに詳しくまとまっています。

C言語コンパイラのインストール

PICごとに必要なコンパイラが違うため、必要な物をインストールします 。

  1. MPLAB® XC Compilersを訪れます
  2. PICのbit数を調べて、必要なXCコンパイラをインストールします(PIC16F1503の場合 MPLAB® XC8 Compiler v1.35)

PICスペックの確認

  1. microchipのページ右上の検索窓からPICに書いてある型番で検索を行います
  2. PICのページが見つかるので、Datasheetを確認する ※リンク先はPIC16F1829の場合の例
    Datasheetを見れば、各pinが何を意味しているのかがわかります。

MPLAB開発の準備

MPLAB Xにてプロジェクトを作成するなどを参考にプロジェクトを作成します。自分の持っているPICの型番(例:PIC16F1503)を確認してそれ用のプロジェクトを作成します。ここでXCコンパイラを選択するのですが自分のPICにあったものがない場合MPLAB® XC Compilersからインストールしてください。

次にMPLAB X IDEの使い方のPICに書き込む方法などを参考にPICkit™3から電源を供給する設定を行います。

設定が完了したらmain.cなどPICに書き込むためのソースコードを作成します。ファイルの作成方法は次などが参考になります。
プログラムソースを作成する(MPLAB X)PICのページのDocumentation & Softwareにサンプルコードが書いてあったりします。)

ビルド

試しに何も書かずにビルドしてみましょう。コンソールにProgramming/Verify completeと表示されれば成功です。ビルドが失敗する場合はコンパイラが適切にインストールされていない可能性があります。エラーが出てしまう場合、配線が正しいかPICライターから電源供給をする設定をしているかなどを確認しましょう。

時計の作成

配線

IMG_0231

見づらい部分もあるかと思いますが、時計を動かすにはこのように配線します。

7セグメントディスプレイには12個の端子があります。コモン端子と呼ばれる4つの端子が各桁に対応しており、それ以外の8端子が数字(と右下の点)のセグメントに対応しています。各端子がどの部分に対応しているかは、付属する説明書などに書かれています。

プログラムコード

/*
 * File:   main.c
 * Author: pony
 *
 * Created on December 10, 2015, 2:26 PM
 */
#include <stdio.h>
#include <stdlib.h>
#include <xc.h>
#define T1INIT        49152        // 49152から65536までカウントする
                                   // 1 / (65536 - 49152) * 32768(水晶発振子の周波数) / 2(プリスケーラ) = 1(秒)
#define T2INIT        0
#define _XTAL_FREQ    16000000     // delay用のクロック指定
#define SECOND_OF_MINUTE   60
#define MINUTE_OF_HOUR     60
#define HOUR_OF_DAY        24
#define SEG_M1        RC4
#define SEG_M2        RC3
#define SEG_M3        RC6
#define SEG_M4        RC5
#define SEG_P1        RC1
#define SEG_P2        RB5
#define SEG_P3        RA2
#define SEG_P4        RB4
#define SEG_P5        RC2
#define SEG_P6        RB6
#define SEG_P7        RC0
int second ;
int minute ;
int hour ;
int seg_m_flg ;                    // どのコモン端子を光らせるかのフラグ
// コンフィギュレーション1の設定
#pragma config FOSC     = INTOSC   // 内部クロック使用する(INTOSC)
#pragma config WDTE     = OFF      // ウオッチドッグタイマー無し(OFF)
#pragma config PWRTE    = ON       // 電源ONから64ms後にプログラムを開始する(ON)
#pragma config MCLRE    = OFF      // 外部リセット信号は使用せずにデジタル入力(RA3)ピンとする(OFF)
#pragma config CP       = OFF      // プログラムメモリーを保護しない(OFF)
#pragma config BOREN    = ON       // 電源電圧降下(BORV設定以下)常時監視機能ON(ON)
#pragma config CLKOUTEN = OFF      // CLKOUTピンをRA4ピンで使用する(OFF)
#pragma config IESO     = OFF      // 内部から外部クロックへの切替えでの起動はなし(OFF)
#pragma config FCMEN    = OFF      // 外部クロック監視しない(OFF)
// コンフィギュレーション2の設定
#pragma config WRT      = OFF      // Flashメモリーを保護しない(OFF)
#pragma config PLLEN    = OFF      // 動作クロックを32MHzでは動作させない(OFF)
#pragma config STVREN   = ON       // スタックがオーバフローやアンダーフローしたらリセットをする(ON)
#pragma config BORV     = HI       // 電源電圧降下常時監視電圧(2.7V)設定(HI)
#pragma config LVP      = OFF      // 低電圧プログラミング機能使用しない(OFF)
void updateSegP( int num ) ;
/*******************************************************************************
*  InterTimer()   タイマー割込みの処理                                             *
*******************************************************************************/
void interrupt InterTimer( void )
{
    if (TMR1IF == 1) {             // タイマー1の割込み発生か?
        TMR1H = (T1INIT >> 8) ;    // タイマー1の再初期値設定
        TMR1L = (T1INIT & 0x00ff) ;
        TMR1IF = 0 ;               // タイマー1の割込フラグをリセット
        // 時刻のインクリメント
        second = (second + 1) % SECOND_OF_MINUTE ;
        if (!second) {
            minute = (minute + 1) % MINUTE_OF_HOUR ;    // 秒が0になるタイミングでインクリメント
            if (!minute) {
                hour = (hour + 1) % HOUR_OF_DAY ;       // 分が0になるタイミングでインクリメント
            }
        }
    }
    if (TMR2IF == 1) {             // タイマー2の割込み発生か?
        TMR2 = T2INIT ;            // タイマー2の再初期値設定
        TMR2IF = 0 ;               // タイマー2の割込フラグをリセット
        seg_m_flg = (seg_m_flg + 1) & 0x0003 ;
        switch(seg_m_flg) {
            case 0:      
                updateSegP(hour / 10);      // 時の10の位を表示  
                SEG_M4 = 1 ;                // 時の10の位を消灯
                SEG_M1 = 0 ;                // 時の10の位を点灯
                break ;
            case 1:
                updateSegP(hour % 10) ;     // 時の1の位を表示
                SEG_M1 = 1 ;                // 時の1の位を消灯
                SEG_M2 = 0 ;                // 時の1の位を点灯
                break ;
            case 2:
                updateSegP(minute / 10) ;   // 分の10の位を表示
                SEG_M2 = 1 ;                // 分の10の位を消灯
                SEG_M3 = 0 ;                // 分の10の位を点灯
                break ;
            case 3:
                updateSegP(minute % 10) ;   // 分の1の位を表示
                SEG_M3 = 1 ;                // 分の1の位を消灯
                SEG_M4 = 0 ;                // 分の1の位を点灯
                break ;
            default:
                ;
        }
    }
}
/*******************************************************************************
*  updateSegP()   セグメントを光らせる                                             *
*******************************************************************************/
void updateSegP( int num )
{   
    switch(num) {               // 数字に合わせたセグメントを点灯
        case 0:
            SEG_P1 = 1 ;
            SEG_P2 = 1 ;
            SEG_P3 = 1 ;
            SEG_P4 = 1 ;
            SEG_P5 = 1 ;
            SEG_P6 = 1 ;
            SEG_P7 = 0 ;
            break ;
        case 1:
            SEG_P1 = 0 ;
            SEG_P2 = 1 ;
            SEG_P3 = 1 ;
            SEG_P4 = 0 ;
            SEG_P5 = 0 ;
            SEG_P6 = 0 ;
            SEG_P7 = 0 ;
            break ;
        case 2:
            SEG_P1 = 1 ;
            SEG_P2 = 1 ;
            SEG_P3 = 0 ;
            SEG_P4 = 1 ;
            SEG_P5 = 1 ;
            SEG_P6 = 0 ;
            SEG_P7 = 1 ;
            break ;
        case 3:
            SEG_P1 = 1 ;
            SEG_P2 = 1 ;
            SEG_P3 = 1 ;
            SEG_P4 = 1 ;
            SEG_P5 = 0 ;
            SEG_P6 = 0 ;
            SEG_P7 = 1 ;
            break ;
        case 4:
            SEG_P1 = 0 ;
            SEG_P2 = 1 ;
            SEG_P3 = 1 ;
            SEG_P4 = 0 ;
            SEG_P5 = 0 ;
            SEG_P6 = 1 ;
            SEG_P7 = 1 ;
            break ;
        case 5:
            SEG_P1 = 1 ;
            SEG_P2 = 0 ;
            SEG_P3 = 1 ;
            SEG_P4 = 1 ;
            SEG_P5 = 0 ;
            SEG_P6 = 1 ;
            SEG_P7 = 1 ;
            break ;
        case 6:
            SEG_P1 = 1 ;
            SEG_P2 = 0 ;
            SEG_P3 = 1 ;
            SEG_P4 = 1 ;
            SEG_P5 = 1 ;
            SEG_P6 = 1 ;
            SEG_P7 = 1 ;
            break ;
        case 7:
            SEG_P1 = 1 ;
            SEG_P2 = 1 ;
            SEG_P3 = 1 ;
            SEG_P4 = 0 ;
            SEG_P5 = 0 ;
            SEG_P6 = 0 ;
            SEG_P7 = 0 ;
            break ;        
        case 8:
            SEG_P1 = 1 ;
            SEG_P2 = 1 ;
            SEG_P3 = 1 ;
            SEG_P4 = 1 ;
            SEG_P5 = 1 ;
            SEG_P6 = 1 ;
            SEG_P7 = 1 ;
            break ;
        case 9:
            SEG_P1 = 1 ;
            SEG_P2 = 1 ;
            SEG_P3 = 1 ;
            SEG_P4 = 1 ;
            SEG_P5 = 0 ;
            SEG_P6 = 1 ;
            SEG_P7 = 1 ;
            break ;
        default:
            ;
    }
}
/*******************************************************************************
*  メインの処理                                                                  *
*******************************************************************************/
void main()
{
    OSCCON     = 0b01111010 ;  // 内部クロックは16MHzとする
    OPTION_REG = 0b00000000 ;  // デジタルI/Oに内部プルアップ抵抗を使用する
    ANSELA     = 0b00000000 ;  // AN0-AN4は使用しない全てデジタルI/Oとする
    ANSELB     = 0b00000000 ;  // AN5-AN11は使用しない全てデジタルI/Oとする
    ANSELC     = 0b00000000 ;  // AN4-AN7は使用しない全てデジタルI/Oとする
    TRISA      = 0b00000000 ;  // ピン(RA)は全て出力に割当てる
    TRISB      = 0b10000000 ;  // RB3は入力専用とし、それ以外のピン(RB)は全て出力に割当てる
    TRISC      = 0b10000000 ;  // RC3は入力専用とし、それ以外のピン(RC)は全て出力に割当てる
    WPUB       = 0b10000000 ;  // RB3は内部プルアップ抵抗を指定する
    WPUC       = 0b10000000 ;  // RB3は内部プルアップ抵抗を指定する
    PORTA      = 0b00000000 ;  // RA出力ピンの初期化(全てLOWにする)
    PORTB      = 0b00000000 ;  // RC出力ピンの初期化(全てLOWにする)
    PORTC      = 0b00000000 ;  // RC出力ピンの初期化(全てLOWにする)
    T1CON  = 0b10011110 ;      // 外部クロック(32.768KHz)でTIMER1をカウントする、プリスケーラカウント 1:2
    TMR1H  = (T1INIT >> 8) ;   // タイマー1の初期化
    TMR1L  = (T1INIT & 0x00ff) ;
    TMR1IF = 0 ;               // タイマー1割込フラグを0にする
    TMR1IE = 1 ;               // タイマー1割り込みを許可する
    T2CON  = 0b00000111 ;      // 内部クロック(4MHz)でTIMER2をカウントする、プリスケーラカウント値 1:8
    PR2    = 249 ;
    TMR2   = T2INIT ;          // タイマー2の初期化
    TMR2IF = 0 ;               // タイマー2割込フラグを0にする
    TMR2IE = 1 ;               // タイマー2割り込みを許可する
    PEIE   = 1 ;               // 周辺装置割り込みを許可する
    GIE    = 1 ;               // 全割込み処理を許可する
    TMR1ON = 1 ;               // タイマー1の開始
    TMR2ON = 1 ;               // タイマー2の開始
    while(1) {
        if (!RC7) {                 // 右側のタクトスイッチの入力をチェック、内部プルアップ抵抗を用いるので符号を反転
            __delay_ms(400) ;       // チャタリング対策のdelay
            minute = (minute + 1) % MINUTE_OF_HOUR ;
            if (!minute) {
                hour = (hour + 1) % HOUR_OF_DAY ;
            }          
            while (!RC7) ;          // 1回の押下で複数回カウントされないようにする
        } 
        if (!RB7) {                 // 左側のタクトスイッチの入力をチェック、内部プルアップ抵抗を用いるので符号を反転
            __delay_ms(400) ;       // チャタリング対策のdelay
            hour = (hour + 1) % HOUR_OF_DAY ;        
            while (!RB7) ;          // 1回の押下で複数回カウントされないようにする
        } 
    }
}

内部プルアップ抵抗

センサ道場 第4回 プルアップ・プルダウン抵抗で書かれているように、タクトスイッチをナイーブに接続すると、OFF時の電位差が不安定になり、0と1が連続で入力される状態になってしまいます。そこで今回は内部プルアップ抵抗を使うことで、ON時とOFF時に0と1の入力が正しく行われるようにしました。

外部クロック

PIC18F14K50使い方:Timer1のセカンダリオシレータで書かれているように、PICの内部クロックは精度がよくないため時計には適していません。今回使用しているPIC16F1829などでは、水晶振動子を用いた外部クロックを利用することで、正確に時を刻めます。TIMER1は16ビットで動作し、カウント値の上限が65536となっています。今回はカウントの初期値を49152としたことで、カウント回数が(65536 - 49152)回となります。水晶振動子の周波数は32.768kHzで、TIMER1のカウントの周期は外部クロック周期 × プリスケーラ設定値となるため、1 / 32768 * 2となり、InterTimer割込の周期は 1 / 32768 * 2(プリスケーラ) * (65536 - 49152) = 1(秒) となります。

7セグメントディスプレイのダイナミック点灯

7セグメントディスプレイには4つのコモン端子があります。時計を作るには4桁の数字をそれぞれ独立に点灯させなければならないため、これらの端子を常にGNDに繋いだままにするわけにはいきません。そこでダイナミック点灯を用います。各桁を超高速で点滅させると、人間の目にはそれぞれが光り続けているように見えます。本プログラムでは、seg_m_flgの値により点灯させる桁を選択しています。

やってみた感想

IoTはほぼ初挑戦でしたが、C言語で書けるため思ったより障壁は低めでした。実際にやってみるとビルドを通したりLEDを点滅させたりするまでが大変で、そこから先はいつも通りのプログラミングだと感じます。各種センサーなどを使えばもっと色々なことができると思うので、今後も挑戦していきたいと思います!

脚注

脚注
1 もっと抵抗値の小さな物でよかったかもしれません…