PIC時計を作る(2)

November 04, 2015

水晶発振回路を使って精度の高いクロック信号を得ることができるようになりました。しかしこのままでは、カウントすることはできても現在の時刻に合わせることができませんので、その仕組みから検討することにしました。

パッと思いつくのは、前回作った回路に設定用のボタンとか時刻表示用のLEDを追加して設定できるようにする、という方法ですが、これだとコストがかかる上、今回は基本音声だけで時間をお知らせしたり、目覚まし音を鳴らすのが目的ですので、既に持っているEDX-007(ボタンも7セグメントLEDも備えている)をつないで時刻設定や表示を行うことにしました。

そのためには、PICとEDX-007のFPGAとの間で通信を行う必要があるのですが、あまりたくさんのピンを使いたくなかったのでPICからFPGAに送る信号についてはシリアルで送ることにしました。PICで使われるシリアル通信についてちょっとだけ調べたところ、I2CやSPI、USARTなどあるのですが、今回は一番簡単そうなUARTを採用することにしました。FPGA側のシステムの仕様と構成は下記の資料のようにしました。

PIC時計用FPGAブロック図

回路図と実際の配線は下記の図のようになります。写真右下の基板がEDX-007(PICとの接続のため、ユーザーI/Oのランドにピンヘッダをつけてます)で、7セグメントLEDとボタンがそれぞれ4つずつ並んでいるのがわかると思いますが、これらを使って時計を設定できるようにしました。なお、1つのボタンはリセットボタンとして利用し、残りの3つで曜日、時、分を調整できるようにしました。EDX-007側の電源電圧は3.3Vであるのに対し、PIC側は5Vであるため、PICからEDX-007側への表示データ信号は抵抗で分圧したものを入力しています。EDX-007からPICへのボタン押し下げ信号はオープンドレイン出力としたので特に電圧の変換はしていません(押すとL、押さないとハイ・インピーダンス)。そのため、PIC側でボタン押し下げ信号の入力ピンとして使っているPORTBのRB5、6、7は内蔵プルアップ抵抗を有効にしています。リセット信号についてはPORTBではないので、プルアップ抵抗(10kΩ)をつけました。

clock_2

EDX-007のVerilog-HDLのソースは下記の通りです。トップモジュールのclock_setterは基本、下位のモジュルの配線を行うだけですが、ボタン入力(btn_in)の信号値変換だけはこの中で行っています。このボタンはアクテゥブ・ローですが、ボタンを離している時(つまり入力がHの時)は出力をハイ・インピーダンスにしたかったので、下記のように変換しています。rxdは9600bpsのUARTの受信信号で、PIC側で生成したLEDへの表示データを受信します。led_sa,led_sgはそれぞれ、EDX-007のLEDセグメント(ダイナミック点灯方式の4桁の7セグメントLED)のアノードセレクト信号(どの桁かを表す)、セグメント信号として出力されます。

module clock_setter(
 input clk,
 input rxd,
 input [3:0] btn_in,
 output [3:0] led_sa,
 output [6:0] led_sg,
 output [3:0] btn_out
 );
 wire [23:0] recv_bytes;
 wire rst_N;
 
 assign rst_N = btn_in[0];
 assign btn_out[0] = btn_in[0] ? 1'bZ : 1'b0;
 assign btn_out[1] = btn_in[1] ? 1'bZ : 1'b0;
 assign btn_out[2] = btn_in[2] ? 1'bZ : 1'b0;
 assign btn_out[3] = btn_in[3] ? 1'bZ : 1'b0;
 
 uart UAT(clk, rst_N, rxd, recv_bytes);
 led_driver LDV(clk, rst_N, recv_bytes, led_sa, led_sg);
endmodule

uartは受信専用(9600bps固定)で、1バイトがLEDに表示する各桁の数値に対応しています。受信したデータは上位2ビットの表示位置情報に従い、下位6ビットが出力バッファrecv_bytesの対応ビットフィールドに記録されます。

`define START_BIT 1'b0
`define STOP_BIT 1'b1
`define STATE_IDLE 2'b00
`define STATE_REC 2'b01
`define UART_DIV 16'h4E1
`define ROFST 16'h271

module uart(
 input uclk,
 input reset_N,
 input rxd,
 output reg [23:0] recv_bytes
 );
 
 reg[7:0] buffer;
 reg [15:0] div;
 reg[6:0] pulsecnt;
 reg[1:0] state;

 always @(negedge uclk or negedge reset_N) begin
 if(!reset_N) begin
 recv_bytes <= 24'h000000;
 buffer <= 8'h00;
 state <=`STATE_IDLE;
 div <= 16'h0000;
 pulsecnt <= 7'b0000000;
 end else begin
 if (div == 16'h0000) begin
 case(state)
 `STATE_IDLE :
 begin
 if(rxd == `START_BIT) begin
 div <= 16'h0001;
 state <= `STATE_REC;
 end
 end
 `STATE_REC :
 case(pulsecnt)
 7'b0001001: 
 if(rxd == `STOP_BIT) div <= 16'h0001;
 7'b0001010: 
 begin
 case(buffer >> 6)
 2'b00:
 recv_bytes[5:0] <= buffer[5:0];
 2'b01:
 recv_bytes[11:6] <= buffer[5:0];
 2'b10:
 recv_bytes[17:12] <= buffer[5:0];
 2'b11:
 recv_bytes[23:18] <= buffer[5:0];
 endcase
 pulsecnt <= 7'b0000000;
 state <= `STATE_IDLE;
 end
 default:
 div <= 16'h0001;
 endcase
 endcase
 end else if (div == `UART_DIV) begin
 div <= 16'h0000;
 pulsecnt <= pulsecnt + 7'b0000001;
 end else begin
 if (div == `ROFST && state == `STATE_REC) begin
 case(pulsecnt)
 7'b0000001:
 buffer[0] <= rxd;
 7'b0000010:
 buffer[1] <= rxd;
 7'b0000011:
 buffer[2] <= rxd;
 7'b0000100:
 buffer[3] <= rxd;
 7'b0000101:
 buffer[4] <= rxd;
 7'b0000110:
 buffer[5] <= rxd;
 7'b0000111:
 buffer[6] <= rxd;
 7'b0001000:
 buffer[7] <= rxd;
 endcase
 end
 div <= div + 16'h0001;
 end
 end
 end
endmodule

led_driverはuartで受信した4つの6ビットのデータをデコードして各セグメントの信号を出力しています。また、アノードの選択も行っています。選択するための信号saは、オープンドレイン出力(OFFがL、ONがZ)にする必要があります(EDX-007のマニュアルに書いてある)が、ここによるとwire変数にしないとハイ・インピーダンスにならないようですので、wire変数としています。DISP_DIVで点灯する7セグを切り替えるタイミングを調整しています。このタイミングの間隔が短すぎると、本来光らないはずのセグメントがうっすら光って見えてしまうようなので、ゆっくり切り替えています。

`define DISP_DIV 16'hFFFF

module led_driver(
 input lclk,
 input rst_N,
 input [23:0] recv_bytes,
 output [3:0] sa,
 output [6:0] sg
 );
 
 reg [1:0] sa_id;
 reg [15:0] div;
 reg [5:0] val;
 
 assign sa = (sa_id == 2'b00) ? 4'bZZZ0 :
 (sa_id == 2'b01) ? 4'bZZ0Z :
 (sa_id == 2'b10) ? 4'bZ0ZZ : 4'b0ZZZ; 
 
 assign sg = (val == 6'b000000) ? 7'b1000000 :
 (val == 6'b000001) ? 7'b1111001 :
 (val == 6'b000010) ? 7'b0100100 :
 (val == 6'b000011) ? 7'b0110000 :
 (val == 6'b000100) ? 7'b0011001 :
 (val == 6'b000101) ? 7'b0010010 :
 (val == 6'b000110) ? 7'b0000010 :
 (val == 6'b000111) ? 7'b1111000 :
 (val == 6'b001000) ? 7'b0000000 :
 (val == 6'b001001) ? 7'b0010000 :
 (val == 6'b001010) ? 7'b0001000 : 7'b1111111;

 always @ (negedge lclk or negedge rst_N) begin
 if(!rst_N) begin
 div <= 16'h0000;
 sa_id <= 2'b00;
 val <= 6'b000000;
 end else if(div == `DISP_DIV ) begin
 div <= 16'h0000;
 case(sa_id)
 2'b00:
 begin
 sa_id <= 2'b01;
 val <= recv_bytes[11:6];
 end
 2'b01:
 begin
 sa_id <= 2'b10;
 val <= recv_bytes[17:12];
 end
 2'b10:
 begin
 sa_id <= 2'b11;
 val <= recv_bytes[23:18];
 end
 2'b11:
 begin
 sa_id <= 2'b00;
 val <= recv_bytes[5:0];
 end
 endcase
 end else begin
 div <= div + 16'h0001;
 end
 end
endmodule


NET "clk" LOC = P134;
NET "rxd" LOC = P143;
NET "btn_in[3]" LOC = P1;
NET "btn_in[2]" LOC = P2;
NET "btn_in[1]" LOC = P33;
NET "btn_in[0]" LOC = P34;
NET "btn_out[0]" LOC = P142;
NET "btn_out[1]" LOC = P141;
NET "btn_out[2]" LOC = P140;
NET "btn_out[3]" LOC = P139;
NET "led_sa[0]" LOC = P17;
NET "led_sa[1]" LOC = P16;
NET "led_sa[2]" LOC = P15;
NET "led_sa[3]" LOC = P14;
NET "led_sg[0]" LOC = P10;
NET "led_sg[1]" LOC = P12;
NET "led_sg[2]" LOC = P11;
NET "led_sg[3]" LOC = P9;
NET "led_sg[4]" LOC = P5;
NET "led_sg[5]" LOC = P7;
NET "led_sg[6]" LOC = P6;


NET "led_sg[6]" IOSTANDARD = LVCMOS33;
NET "led_sg[5]" IOSTANDARD = LVCMOS33;
NET "led_sg[4]" IOSTANDARD = LVCMOS33;
NET "led_sg[3]" IOSTANDARD = LVCMOS33;
NET "led_sg[2]" IOSTANDARD = LVCMOS33;
NET "led_sg[1]" IOSTANDARD = LVCMOS33;
NET "led_sg[0]" IOSTANDARD = LVCMOS33;
NET "led_sa[3]" IOSTANDARD = LVCMOS33;
NET "led_sa[2]" IOSTANDARD = LVCMOS33;
NET "led_sa[1]" IOSTANDARD = LVCMOS33;
NET "led_sa[0]" IOSTANDARD = LVCMOS33;
NET "btn_out[3]" IOSTANDARD = LVCMOS33;
NET "btn_out[2]" IOSTANDARD = LVCMOS33;
NET "btn_out[1]" IOSTANDARD = LVCMOS33;
NET "btn_out[0]" IOSTANDARD = LVCMOS33;
NET "btn_in[3]" IOSTANDARD = LVCMOS33;
NET "btn_in[2]" IOSTANDARD = LVCMOS33;
NET "btn_in[1]" IOSTANDARD = LVCMOS33;
NET "btn_in[0]" IOSTANDARD = LVCMOS33;

PIC側のプログラムは下記です。時刻データ(1秒ごとに発生するTMR0の割り込みで1秒進む)の表示データ(1バイト=表示桁位置+BCD)をEDX-007側に送り、ボタンの押し下げ時に時刻データを更新しています(曜日、時、分)。フリーのCコンパイラもあるようですが、またいろいろ調べるのが面倒だったのでとりあえず全部アセンブラで書くことにしました。PICのアセンブラの命令体系は非常にシンプルで覚えることも少ないですが、その分いろいろ自力でやらないといけないこととか知らないとハマることがあって大変でした。例えば、スタックがないのでやたらレジスタの管理を自分でやらないといけません。PUSHがあって当たり前、と思っていたので非常に面倒でした。あと、ハマったのが数値の比較結果の判定方法です。SUBLWでリテラル値からWレジスタを引いた時に、もし後者が大きければキャリーフラグが1になる、と思い込んでいたのですが、そうではないようです(詳細はこちら)。LEDの表示モードはとりあえず、曜日モードと時刻モードの2つにしました。秒は表示できません。モードの切り替えは、設定ボタンが押された時に行っています。

ちなみに、今回使用しているPICは16F84Aですので、usartなどのシリアル通信機能は付いていません。なので、今回はソフトウエアだけで実現しています(但し出力のみ)。信号を送るタイミングはPIC自体のクロック(12MHz)で制御しています(ここを参考にさせていただきました)。


#INCLUDE "P16F84A.INC"
 
 __CONFIG _FOSC_HS & _WDTE_OFF & _PWRTE_ON & _CP_OFF

save_st EQU 0x0E
save_w EQU 0x0F
time_d EQU 0x10
time_h EQU 0x11
time_m EQU 0x12
time_s EQU 0x13
s_byte EQU 0x14
tmp0 EQU 0x15
tmp1 EQU 0x16
rslt0 EQU 0x17
rslt1 EQU 0x18
parm0 EQU 0x19
parm1 EQU 0x1A
parm2 EQU 0x1B
parm3 EQU 0x1C
dmode EQU 0x1D
 
; 9600bpsのタイミング生成用
_UWAIT_TIME EQU 0x34
 
;PORTAのピン番号
P_FPGA EQU 0x01

;PORTBのピン番号
P_BTN0 EQU 0x05
P_BTN1 EQU 0x06
P_BTN2 EQU 0x07
 

RES_VECT CODE 0x0000 ; processor reset vector
 GOTO START ; go to beginning of program

ISR CODE 0x0004 ; interrupt vector location
 MOVWF save_w ;Wレジスタの保存
 SWAPF STATUS,W
 MOVWF save_st ;ステータスレジスタの保存

 ; 各割り込みの処理の振り分け
 BTFSC INTCON,T0IF ; TIMER0割り込み判定
 CALL ISRTMR0
 BTFSC INTCON,RBIF ; RBポート変更割り込み判定
 CALL ISRRBC
 
 ;復帰処理
 SWAPF save_st,W
 MOVWF STATUS
 SWAPF save_w,F
 SWAPF save_w,W
 RETFIE ; 割り込み復帰
 
ISRTMR0
 ; 時刻データの更新
 CALL ADV_1SEC
 BCF INTCON,T0IF ; 割り込みフラグをクリアする
 CLRF TMR0 ; カウンタを0に初期化する
 RETURN
 
ISRRBC
 ; RBポート変更(ボタン押し下げ、上げ)
 ; どれかひとつが押されたか、全部押されていない状態になったものとして処理する
 BTFSC PORTB,P_BTN0
 GOTO ISRRBC_BTN1
 ; 曜日を進める
 MOVLW 0x01
 MOVWF dmode
 INCF time_d,F
 MOVFW time_d
 SUBLW 0x06
 BTFSS STATUS,C
 CLRF time_d
 GOTO ISRRBC_FINISH
ISRRBC_BTN1
 BTFSC PORTB,P_BTN1
 GOTO ISRRBC_BTN2
 ; 時を進める
 MOVLW 0x00
 MOVWF dmode
 INCF time_h,F
 MOVFW time_h
 SUBLW 0x017
 BTFSS STATUS,C
 CLRF time_h
 GOTO ISRRBC_FINISH
ISRRBC_BTN2
 BTFSC PORTB,P_BTN2
 GOTO ISRRBC_FINISH
 ; 分を進める
 MOVLW 0x00
 MOVWF dmode
 INCF time_m,F
 MOVFW time_m
 SUBLW 0x03B
 BTFSS STATUS,C
 CLRF time_m
 ; 秒もゼロに変更
 CLRF time_s
 CLRF TMR0
ISRRBC_FINISH
 BCF INTCON,RBIF ; 割り込みフラグをオフする
 RETURN
 
;*******************************************************************************
; MAIN PROGRAM
;*******************************************************************************

MAIN_PROG CODE ; let linker place main program

START

 ; TODO Step #5 - Insert Your Program Here

 BSF STATUS,RP0 ; バンク1に切り替え
 ; PORTAはRA4はクロック入力、それ以外は出力(RA1はFPGA,RA2はATPへのUART出力)
 MOVLW 0x10
 MOVWF TRISA
 ; PORTBは上位3ビット(RB5,6,7)がFPGAからのボタン入力、それ以外は全部出力(未使用)
 MOVLW 0xE0
 MOVWF TRISB
 ; TMR0を外部クロックにする
 ; RBPU 0 : PORTBのPullUp = あり
 ; INTEDGE 1 : INT割り込み信号のエッジ=立ち上がり
 ; T0CS 1 : 入力の選択 = RA4ピン指定
 ; T0SE 1 : インクリメントするタイミング = RA4の入力がHからL
 ; PSA 0 : プリスケーラ有無 = 有り
 ; PS1~3 110 : プリスケーラ値 = 128
 MOVLW 0x76
 MOVWF OPTION_REG 
 BCF STATUS,RP0 ; バンク0に切り替え
 ; TMR0割り込みとRBポート変化割り込みを許可
 MOVLW 0xA8
 MOVWF INTCON
 CLRF TMR0
 ; STOPビット
 BSF PORTA,P_FPGA
 BSF PORTA,P_ATP
 ; 時刻をリセット
 CLRF time_s
 CLRF time_m
 CLRF time_h
 CLRF time_d
 ; 表示モードリセット
 CLRF dmode
LOOP
 ; 表示の更新(設定ツール用)
 MOVFW dmode
 XORLW 0x00
 BTFSS STATUS,Z
 GOTO CHK_DMODE1
 CALL DISP_DMODE0
 GOTO DISP_END
CHK_DMODE1
 MOVFW dmode
 XORLW 0x01
 BTFSS STATUS,Z
 GOTO DISP_END
 CALL DISP_DMODE1
DISP_END
 CALL UWAIT
 CALL UWAIT
 CALL UWAIT
 CALL UWAIT
 CALL UWAIT
 CALL UWAIT
 CALL UWAIT
 CALL UWAIT
 CALL UWAIT
 CALL UWAIT
 GOTO LOOP ; loop forever


DISP_DMODE0
 ; 現在時刻の表示
 MOVF time_m,W
 MOVWF parm0
 CALL TRANS2BCD
 MOVF rslt0,W
 MOVWF parm0
 CALL UPDATE_LED0
 MOVF rslt1,W
 MOVWF parm0
 CALL UPDATE_LED1
 MOVF time_h,W
 MOVWF parm0
 CALL TRANS2BCD
 MOVF rslt0,W
 MOVWF parm0
 CALL UPDATE_LED2
 MOVF rslt1,W
 MOVWF parm0
 CALL UPDATE_LED3
 RETURN
 
DISP_DMODE1
 ; 曜日の表示
 MOVF time_d,W
 MOVWF parm0
 CALL TRANS2BCD
 MOVF rslt0,W
 MOVWF parm0
 CALL UPDATE_LED0
 MOVLW 0x3F
 MOVWF parm0
 CALL UPDATE_LED1
 CALL UPDATE_LED2
 CALL UPDATE_LED3
 RETURN
 
UPDATE_LED0
 ; 7segLED0にUARTで表示する数値(6bit長)を送る
 ; 入力 parm0
 MOVF parm0,W
 MOVWF s_byte
 BCF s_byte, 6
 BCF s_byte, 7
 CALL UAPUTB2FPGA
 RETURN
 
UPDATE_LED1
 ; 7segLED1にUARTで表示する数値(6bit長)を送る
 ; 入力 parm0
 MOVF parm0,W
 MOVWF s_byte
 BSF s_byte, 6
 BCF s_byte, 7
 CALL UAPUTB2FPGA
 RETURN
 
UPDATE_LED2
 ; 7segLED2にUARTで表示する数値(6bit長)を送る
 ; 入力 parm0
 MOVF parm0,W
 MOVWF s_byte
 BCF s_byte, 6
 BSF s_byte, 7
 CALL UAPUTB2FPGA
 RETURN
 
UPDATE_LED3
 ; 7segLED3にUARTで表示する数値(6bit長)を送る
 ; 入力 parm0
 MOVF parm0,W
 MOVWF s_byte
 BSF s_byte, 6
 BSF s_byte, 7
 CALL UAPUTB2FPGA
 RETURN
 
 
ADV_1SEC
 ; 時刻を1秒進める
 INCF time_s,F
 MOVLW 0x3C
 XORWF time_s,W
 BTFSS STATUS,Z
 RETURN
 CLRF time_s
 INCF time_m,F
 MOVLW 0x3C
 XORWF time_m,W
 BTFSS STATUS,Z
 RETURN
 CLRF time_m
 INCF time_h,F
 MOVLW 0x18
 XORWF time_h,W
 BTFSS STATUS,Z
 RETURN
 CLRF time_h
 INCF time_d,F
 MOVLW 0x07
 XORWF time_d,W
 BTFSS STATUS,Z
 RETURN
 CLRF time_d
 RETURN
 
TRANS2BCD
 ; 1バイトの値(0-99)を2バイトのBCDに変換
 ; 入力 parm0 出力 rslt0, rslt1
 ; tmp0:入力値のシフト演算バッファ
 ; tmp1:残りシフト実行回数
 ; 初期化
 MOVF parm0,W
 MOVWF tmp0
 CLRF rslt0
 MOVLW 0x08
 MOVWF tmp1
TB_LOOP
 RLF tmp0, F
 RLF rslt0, F
 ; 終了判定
 ; 全部シフトしたら終了
 DECFSZ tmp1,F
 GOTO TB_CONTINUE
 ; rlt0の上位4bitをrlt1,下位4bitをrlt0に代入
 MOVF rslt0, W
 MOVWF rslt1
 SWAPF rslt1, F
 MOVLW 0x0F
 ANDWF rslt0, F
 ANDWF rslt1, F
 RETURN
TB_CONTINUE
 ; rslt & 0x0F > 4ならばrslt=rslt+3
 MOVLW 0x0F
 ANDWF rslt0, W
 SUBLW 0x04
 BTFSC STATUS, C
 GOTO TB_LOOP
 INCF rslt0,F
 INCF rslt0,F
 INCF rslt0,F
 GOTO TB_LOOP

UAPUTB2FPGA
 ;FPGAに1バイト送信(入力はs_byte)
 ;STARTビット生成 
 BCF PORTA,P_FPGA
 CALL UWAIT
 
 BTFSS s_byte, 0
 BCF PORTA,P_FPGA
 BTFSC s_byte, 0
 BSF PORTA,P_FPGA
 CALL UWAIT
 
 BTFSS s_byte, 1
 BCF PORTA,P_FPGA
 BTFSC s_byte, 1
 BSF PORTA,P_FPGA
 CALL UWAIT
 
 BTFSS s_byte, 2
 BCF PORTA,P_FPGA
 BTFSC s_byte, 2
 BSF PORTA,P_FPGA
 CALL UWAIT
 
 BTFSS s_byte, 3
 BCF PORTA,P_FPGA
 BTFSC s_byte, 3
 BSF PORTA,P_FPGA
 CALL UWAIT
 
 BTFSS s_byte, 4
 BCF PORTA,P_FPGA
 BTFSC s_byte, 4
 BSF PORTA,P_FPGA
 CALL UWAIT
 
 BTFSS s_byte, 5
 BCF PORTA,P_FPGA
 BTFSC s_byte, 5
 BSF PORTA,P_FPGA
 CALL UWAIT
 
 BTFSS s_byte, 6
 BCF PORTA,P_FPGA
 BTFSC s_byte, 6
 BSF PORTA,P_FPGA
 CALL UWAIT
 
 BTFSS s_byte, 7
 BCF PORTA,P_FPGA
 BTFSC s_byte, 7
 BSF PORTA,P_FPGA
 CALL UWAIT
 
 ;STOPビット生成
 BSF PORTA,P_FPGA
 CALL UWAIT
 
 RETURN

 
UWAIT
 ;ビジーウエイト(UARTのタイミング生成用)
 MOVLW _UWAIT_TIME
 MOVWF tmp0
UWAIT_LOOP
 ; 4CLK/1命令で、12MHzなので、1ループ=4命令+GOTO(2命令分消費)=2μsec
 NOP
 NOP
 NOP
 DECFSZ tmp0, F
 GOTO UWAIT_LOOP
 
 RETURN
 
 END

これでようやく、時刻を設定できるようになりました。早速試してみたのですが、30分もすると数秒進んでしまうことがわかりました。発振回路の容量を適当に決めてしまったのが悪かったのかもしれませんね・・・。まあ、これはおいおい調整するとして、次は音声で現在時刻や目覚ましの音声を喋らす機能を追加していくことにします。