水晶発振回路を使って精度の高いクロック信号を得ることができるようになりました。しかしこのままでは、カウントすることはできても現在の時刻に合わせることができませんので、その仕組みから検討することにしました。
パッと思いつくのは、前回作った回路に設定用のボタンとか時刻表示用の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Ω)をつけました。
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分もすると数秒進んでしまうことがわかりました。発振回路の容量を適当に決めてしまったのが悪かったのかもしれませんね・・・。まあ、これはおいおい調整するとして、次は音声で現在時刻や目覚ましの音声を喋らす機能を追加していくことにします。

