NSLで回路を作ろう#02: CPU組込み版 24時間時計


[【改訂2版】FPGAボードで学ぶ 組込みシステム開発入門［Intel FPGA編］][text]の第6章で紹介されているNios II CPUと自作回路を組込んだシステムで24時間時計を作ってみる。

# 1. 回路について

テキストではNiosII CPUを組込んだシステムで1時間計を作るまでが課題だったが、24時間時計についての指針は記載されていない。
そこで、以下の方針で回路を作っていく。
##  方針
デコーダ回路を追加する
- 1時間計ではNiosII CPUプログラム(ソフトウェア)側で4bitの値をデコードして、7セグメントLEDの表示パターンを出力していた。
  - 7セグの表示パターンは1桁に7～8bit必要
  - 6桁分だと42～48bit
    - 32bitを超えてしまう
- ソフトウェア側は4bit区切りで時分秒の各桁を出力するようにし、表示パターンのデコードはハードウェア側で行うようにした。

処理の内訳
- ハードウェア
  - スイッチ入力の受付
  - CPUから7セグメントLEDの表示桁を受取
  - 7セグメントLEDの表示パターンをデコード
- ソフトウェア
  - 24時間時計の制御
    - 1秒を計る
    - 時分秒の管理
    - 時刻調整
    - 12/24時間の切り替え

機能追加
- 前記事の回路と動作が同じだと面白くないので、12/24時間表示を切り替える機能を追加
  - スライドスイッチのオンオフで切り替える様にする。

## 概要
[前記事][prev]の24時間時計の回路をNiosIICPUで制御する回路。
- 自作する部分がハードウェアとソフトウェア2つになる。

ハードウェアの部分ではスイッチ入力やLEDへ出力する部分をCPUとやり取りする。
- Avalonバスでデータを受け取って、それをデコーダ回路を使って出力するイメージ

ソフトウェアでは時計の制御部分を実装する
- 1秒計るタイマー
  - タイマー割込みを使う
- 時分秒のカウント
- 時刻調整機能
  - ボタン入力(チャタリング除去)

NiosII CPU
- FPGAに内蔵できるCPU、Platform Designerで組込みシステムを構築して実装する。

Avalonバス
- NiosII CPUとデータをやり取りする際に使用するバスの規格
- マスター/スレーブとわかれており、CPUはマスター、今回作成する回路などの周辺機器はスレーブとして動作する。

# 2.作ってみる

今回作成するものは、ハードウェアとそれを組込んだNiosIIシステム、NiosII CPU を動かすソフトウェアに分かれる。

ハードウェア(mypio_ip: NSLで記述)
- Avalonバスのスレーブ回路
  - スイッチ入力と7セグメントLEDに出力したい値をCPUとやり取りする。
- 7セグメントLEDデコーダ回路
  - CPUから受け取った値を7セグメントLEDに出力できるデータに加工する。

NiosII システム
- NiosII CPU 
  - プログラムを動かすCPU本体
- オンチップメモリ
  - 動かすプログラムを記憶する
- JTAG UART
  - PCとのシリアル通信につかう
  - プログラム上で値を表示するためのデバッグ用
- System ID
  - システム固有の識別番号を持った回路
    - 回路とプログラムが一致しているかチェック
- mypio_ip(自作回路)
  - 前述の作成したハードウェアを組込む
- タイマー
  - 一定時間で割込み処理をさせる


ソフトウェア (NiosII CPU プログラム: C言語で作成)
- 時間を計る
- 時分秒の管理
- スイッチ入力のチャタリング除去
- 時刻調整機能
- 自作回路に値を書き込む

## ハードウェア

組込む自作回路(mypio_ip)には以下の機能を実装する。
- Avalonバスのスレーブ回路
  - スイッチ入力と7セグメントLEDに出力したい値をCPUとやり取りする。
  - やり取りする内容は以下のレジスタ表に準ずる。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/82e3f888-baea-0d6b-5149-5cad0af2cda5.png)

- 7セグメントLEDデコーダ回路
  - CPUから受け取った値を7セグメントLEDに出力できるデータに加工する。




### Avalonバスのスレーブ回路

AvalonバスでCPUからデータを読み書きしてもらうために、以下の信号を定義する。
- address
- write
- read
- writedata[32]
- readdata[32]

前述のレジスタ表の通りに回路を実装する。
- スイッチ入力をCPUプログラムに送信。
  - `address==1'b1`かつ`read==1`のときにスイッチ入力の値を`readdata`に出力する。
- 内部に32bitのレジスタを用意しておき、`address==1'b0`かつ`write==1`のときに`writedata`の値を記憶する。
  

<details>
<summary>Avalonバスのスレーブ回路</summary>

ヘッダファイル
```
#ifndef _MYPIO_NSH
#define _MYPIO_NSH


declare mypio {
    input   address;
    input   write;
    input   read;
    input   writedata[32];
    output  readdata[32];
    
    input   mypio_in[11];
    output  mypio_out[32];
}

#endif // _MYPIO_NSH
```

モジュール
```
#include "mypio.nsh"

module mypio {
    reg out_reg[32]  = 32'h0000_0000;
    if ( write && address==1'b0 ) {
        out_reg := writedata;
    }
    alt {
        read == 1'b0 : {
            readdata = 32'h0000_0000;
        }
        address == 1'b0 : {
            readdata = out_reg;
        }
        else : {
            readdata = {21'd0, mypio_in};
        }
    }
    mypio_out = out_reg;
}
```

</details>


### 7セグメントLEDデコーダ回路

CPUから書き込まれるデータを、それぞれの桁を7セグメントLEDに出力する為の表示パターンを出力。
- [前記事][prev]のデコーダ回路と仕様は同じ。

<details>
<summary>7セグメントLEDデコーダ回路</summary>

ヘッダファイル
```
#ifndef _SEG7DEC_NSH
#define _SEG7DEC_NSH

declare seg7dec {
	input din[4];
	output nHEX[7];
	func_in decode(din): nHEX;
}

#endif // _SEG7DEC_NSH
```

モジュール
```
#include "seg7dec.nsh"

module seg7dec{
	func decode {
		wire tmp[7];
		any {
			din==4'h0 : tmp = 7'b1000000;
			din==4'h1 : tmp = 7'b1111001;
			din==4'h2 : tmp = 7'b0100100;
			din==4'h3 : tmp = 7'b0110000;
			din==4'h4 : tmp = 7'b0011001;
			din==4'h5 : tmp = 7'b0010010;
			din==4'h6 : tmp = 7'b0000010;
			din==4'h7 : tmp = 7'b1011000;
			din==4'h8 : tmp = 7'b0000000;
			din==4'h9 : tmp = 7'b0010000;
			din==4'ha : tmp = 7'b0001000;
			din==4'hb : tmp = 7'b0000011;
			din==4'hc : tmp = 7'b1000110;
			din==4'hd : tmp = 7'b0100001;
			din==4'he : tmp = 7'b0000110;
			din==4'hf : tmp = 7'b0001110;
		}
		return tmp;
	}
}

```

</details>

### トップモジュール

上記の回路をまとめる配線を行う。
- Platform Designerに登録する際に、信号名を特定の名前すると、自動で識別してくれる。
  - `interface`を追加してクロックとリセット入力を自動で作らないようにした。

- NSLではモジュールのインスタンスを配列の宣言できる。
  - `ganerate`文でクロックとリセットの接続をまとめて行った。

- AvalonMMのスレーブ回路で記憶した値(`out_reg`)はレジスタ表に則ってデコーダ回路に接続する。
  - ここの部分も`ganerate`文にしたかったけどよくわからんorz

<details>
<summary>トップモジュール</summary>

ヘッダファイル
```
#ifndef _MYPIO_IP_NSH
#define _MYPIO_IP_NSH

#include "seg7dec.nsh"
#include "mypio.nsh"

declare mypio_ip interface {
    input clk;
    input reset;

    input   avs_address;
    input   avs_write;
    input   avs_read;
    input   avs_writedata[32];
    output  avs_readdata[32];

    input   coe_mypio_in[11];
    output  coe_mypio_hex0[8];
    output  coe_mypio_hex1[8];
    output  coe_mypio_hex2[8];
    output  coe_mypio_hex3[8];
    output  coe_mypio_hex4[8];
    output  coe_mypio_hex5[8];
}

#endif // _MYPIO_IP_NSH
```

モジュール
```
#include "mypio_ip.nsh"


module mypio_ip {
    mypio m;
    seg7dec seg[6];

    integer i;

    wire sec_on, min_on, hour_on;
    m.m_clock   = clk;
    m.p_reset   = reset;
    m.address   = avs_address;
    m.write     = avs_write;
    m.read      = avs_read;
    m.writedata = avs_writedata;
    m.mypio_in  = coe_mypio_in;
    
    avs_readdata  = m.readdata;
    sec_on  = m.mypio_out[24];
    min_on  = m.mypio_out[25];
    hour_on = m.mypio_out[26];

    generate(i = 0; i < 6; i++){
        seg[i].m_clock = clk;
        seg[i].p_reset = reset;
    }
    
    coe_mypio_hex0 = if( sec_on == 1) { 0b1, seg[0].decode(m.mypio_out[ 3: 0])} else 0xff;
    coe_mypio_hex1 = if( sec_on == 1) { 0b1, seg[1].decode(m.mypio_out[ 7: 4])} else 0xff;
    coe_mypio_hex2 = if( min_on == 1) { 0b1, seg[2].decode(m.mypio_out[11: 8])} else 0xff;
    coe_mypio_hex3 = if( min_on == 1) { 0b1, seg[3].decode(m.mypio_out[15:12])} else 0xff;
    coe_mypio_hex4 = if(hour_on == 1) { 0b1, seg[4].decode(m.mypio_out[19:16])} else 0xff;
    coe_mypio_hex5 = if(hour_on == 1) { 0b1, seg[5].decode(m.mypio_out[23:20])} else 0xff;
}
```

</details>


## NiosII システムの作成
NiosIIのシステムの中に先ほど作成した回路を組込むために、以下のファイルを作成する。

<details>
<summary>mypio_hw.tcl</summary>

以下はPlatform Deginerで設定を行うと生成されるファイルを一部改変したもの
- 設定方法は、[テキスト][text]を見て欲しい。

```tcl
package require -exact qsys 16.1


# 
# module mypio
# 
set_module_property DESCRIPTION ""
set_module_property NAME mypio
set_module_property VERSION 1.0
set_module_property INTERNAL false
set_module_property OPAQUE_ADDRESS_MAP true
set_module_property AUTHOR ""
set_module_property DISPLAY_NAME mypio
set_module_property INSTANTIATE_IN_SYSTEM_MODULE true
set_module_property EDITABLE true
set_module_property REPORT_TO_TALKBACK false
set_module_property ALLOW_GREYBOX_GENERATION false
set_module_property REPORT_HIERARCHY false


# 
# file sets
# 
add_fileset QUARTUS_SYNTH QUARTUS_SYNTH "" ""
set_fileset_property QUARTUS_SYNTH TOP_LEVEL mypio_ip
set_fileset_property QUARTUS_SYNTH ENABLE_RELATIVE_INCLUDE_PATHS false
set_fileset_property QUARTUS_SYNTH ENABLE_FILE_OVERWRITE_MODE false
add_fileset_file mypio.v VERILOG PATH mypio.v
add_fileset_file mypio_ip.v VERILOG PATH mypio_ip.v TOP_LEVEL_FILE
add_fileset_file seg7dec.v VERILOG PATH seg7dec.v


# 
# parameters
# 


# 
# display items
# 


# 
# connection point avalon_slave_0
# 
add_interface avalon_slave_0 avalon end
set_interface_property avalon_slave_0 addressUnits WORDS
set_interface_property avalon_slave_0 associatedClock clock
set_interface_property avalon_slave_0 associatedReset reset
set_interface_property avalon_slave_0 bitsPerSymbol 8
set_interface_property avalon_slave_0 burstOnBurstBoundariesOnly false
set_interface_property avalon_slave_0 burstcountUnits WORDS
set_interface_property avalon_slave_0 explicitAddressSpan 0
set_interface_property avalon_slave_0 holdTime 0
set_interface_property avalon_slave_0 linewrapBursts false
set_interface_property avalon_slave_0 maximumPendingReadTransactions 0
set_interface_property avalon_slave_0 maximumPendingWriteTransactions 0
set_interface_property avalon_slave_0 readLatency 0
set_interface_property avalon_slave_0 readWaitTime 1
set_interface_property avalon_slave_0 setupTime 0
set_interface_property avalon_slave_0 timingUnits Cycles
set_interface_property avalon_slave_0 writeWaitTime 0
set_interface_property avalon_slave_0 ENABLED true
set_interface_property avalon_slave_0 EXPORT_OF ""
set_interface_property avalon_slave_0 PORT_NAME_MAP ""
set_interface_property avalon_slave_0 CMSIS_SVD_VARIABLES ""
set_interface_property avalon_slave_0 SVD_ADDRESS_GROUP ""

add_interface_port avalon_slave_0 avs_address address Input 1
add_interface_port avalon_slave_0 avs_write write Input 1
add_interface_port avalon_slave_0 avs_read read Input 1
add_interface_port avalon_slave_0 avs_writedata writedata Input 32
add_interface_port avalon_slave_0 avs_readdata readdata Output 32
set_interface_assignment avalon_slave_0 embeddedsw.configuration.isFlash 0
set_interface_assignment avalon_slave_0 embeddedsw.configuration.isMemoryDevice 0
set_interface_assignment avalon_slave_0 embeddedsw.configuration.isNonVolatileStorage 0
set_interface_assignment avalon_slave_0 embeddedsw.configuration.isPrintableDevice 0


# 
# connection point clock
# 
add_interface clock clock end
set_interface_property clock clockRate 0
set_interface_property clock ENABLED true
set_interface_property clock EXPORT_OF ""
set_interface_property clock PORT_NAME_MAP ""
set_interface_property clock CMSIS_SVD_VARIABLES ""
set_interface_property clock SVD_ADDRESS_GROUP ""

add_interface_port clock clk clk Input 1


# 
# connection point reset
# 
add_interface reset reset end
set_interface_property reset associatedClock clock
set_interface_property reset synchronousEdges DEASSERT
set_interface_property reset ENABLED true
set_interface_property reset EXPORT_OF ""
set_interface_property reset PORT_NAME_MAP ""
set_interface_property reset CMSIS_SVD_VARIABLES ""
set_interface_property reset SVD_ADDRESS_GROUP ""

add_interface_port reset reset reset Input 1


# 
# connection point conduit_end_0
# 
add_interface conduit_end_0 conduit end
set_interface_property conduit_end_0 associatedClock clock
set_interface_property conduit_end_0 associatedReset ""
set_interface_property conduit_end_0 ENABLED true
set_interface_property conduit_end_0 EXPORT_OF ""
set_interface_property conduit_end_0 PORT_NAME_MAP ""
set_interface_property conduit_end_0 CMSIS_SVD_VARIABLES ""
set_interface_property conduit_end_0 SVD_ADDRESS_GROUP ""

add_interface_port conduit_end_0 coe_mypio_in mypio_in Input 11
add_interface_port conduit_end_0 coe_mypio_hex0 mypio_hex0 Output 8
add_interface_port conduit_end_0 coe_mypio_hex1 mypio_hex1 Output 8
add_interface_port conduit_end_0 coe_mypio_hex2 mypio_hex2 Output 8
add_interface_port conduit_end_0 coe_mypio_hex3 mypio_hex3 Output 8
add_interface_port conduit_end_0 coe_mypio_hex4 mypio_hex4 Output 8
add_interface_port conduit_end_0 coe_mypio_hex5 mypio_hex5 Output 8
```

</details>

プロジェクトを作成する場所にmypioフォルダを作成し、そこに先ほど作成したファイルとNSLファイルをコンパイルして生成されたVerilogファイルを入れる。

Platform Designer を起動する。

以下のコンポーネントを追加する
- NiosII CPU
- On Chip Memory
- JTAG UART
- System ID
- Interval Timer
- mypio_ip

追加しただけだと使えない(エラーが出ている)ので、クロックやリセット、バス、IRQの接続や外部入出力端子、アドレスの設定を行う
- クロック、バス、IRQの接続(赤丸、数字はIRQのID)、外部入出力端子の設定(橙枠)
![nios2system_setting01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/afb0b6d2-ebc1-5bcb-582b-32b3855a7254.png)

- リセットの接続、アドレスの設定
![nios2system_setting02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/60b05345-fc8f-4691-d877-9d1bcf0d53de.png)

- 設定後の状態
![nios2system_components.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/5fb52ea1-9cc3-f567-b9c2-c790aeb4fa58.png)



最後に CPUの設定
- NiosII CPUで右クリックして編集 > 以下のように設定を変更する
![Nios2CPU_setting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/0475c500-077e-bfee-c70c-8c9d75daea2e.png)


以上を終えたら[Generate HDL]をクリック。
&lt;プロジェクト名&gt;_qsys フォルダを出力先に設定して保存。


## ソフトウェア

NiosII CPU で動かすプログラムを作成していく
- 定義/宣言部分
- メイン関数
  - タイマー割込みの設定
  - 回路と値のやり取り
- タイマー割り込み
  - スイッチ入力のチャタリング除去
  - 時刻調整機能
  - 時分秒の管理

### 定義/宣言部分
プログラム内で必要な値を定義する。

<details>
<summary>長いので畳みます</summary>

- ライブラリのインクルード
- 定義/宣言部分


<details>
<summary>雑談</summary>

割込み処理から出る値共有のためグローバル変数大量発生地獄
- グローバル変数は不具合のもとなのであまり使いたくないが、うーん。
</details>


```c
#include "system.h"
#include "io.h"
#include "altera_avalon_timer_regs.h"
#include "sys/alt_irq.h"
#include "sys/alt_stdio.h"

// クロック 50 MHz <-> 20ns
// タイマーの周期 500,000 * 20ns = 10,000,000 ns = 10ms
#define PERIOD 499999

// スイッチ入力
int in;

// 秒
int sec1 = 0;
int sec10 = 0;
// 分
int min1 = 0;
int min10 = 0;
// 時
int hour = 0;
int hour1 = 0;
int hour10 = 0;

// 7セグメントLED、それぞれの桁を表示
int hour_on;
int min_on;
int sec_on;

// プッシュボタン押下状態の管理
int key_push;

// タイマー割り込み時に 0-99 まで数えて 1秒
int cnt = 0;

// 24時間表示切替
int mode24 = 0;

// 0.25s毎に変化： 0xffffffff ⇔ 0x00000000
int sig2hz = 0xffffffff;

// ステートマシン用の定数を列挙
enum cur_param {
	SEC    = 0x01000000,
	MIN    = 0x02000000,
	HOUR   = 0x04000000,
	NORMAL = 0xffffffff
} cur = NORMAL;

// プロトタイプ宣言
void add_sec(); // 1秒加算
void add_min(); // 1分加算
void add_hour(); // 1時間加算
void hour_disp(); // 7セグメントLEDに表示する値に変換
```

</details>

### メイン関数

プログラム起動時から行う処理をここで記述する
- タイマー割込みの設定
- 回路と値のやり取り
  - スイッチ入力の読込み
  - 7セグメントLEDに表示したい値を書き込む


<details>
<summary>メイン関数部分</summary>

```c
// メイン関数
int main()
{
	int out;

	alt_ic_isr_register( TIMER_0_IRQ_INTERRUPT_CONTROLLER_ID, TIMER_0_IRQ,
			alarm_callback, (void *)0, (void *)0 );
	IOWR_ALTERA_AVALON_TIMER_PERIODL(TIMER_0_BASE, PERIOD & 0xffff);
	IOWR_ALTERA_AVALON_TIMER_PERIODH(TIMER_0_BASE, PERIOD >> 16);
	IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER_0_BASE,
		ALTERA_AVALON_TIMER_CONTROL_ITO_MSK  |
		ALTERA_AVALON_TIMER_CONTROL_CONT_MSK |
		ALTERA_AVALON_TIMER_CONTROL_START_MSK );

	while (1) {
		in = IORD_32DIRECT(MYPIO_0_BASE, 4);
		out = hour_on | min_on | sec_on
			 |  hour10 << 20 | hour1 << 16
			 |   min10 << 12 |   min1 <<  8
			 |   sec10 <<  4 |   sec1;
		IOWR_32DIRECT(MYPIO_0_BASE, 0, out);
	}
	return 0;
}
```

</details>


#### タイマー割り込みの設定
時間経過を計るのにタイマーを周辺機器に追加した。
- そのタイマー回路の割込み処理を追加して、1秒の計測を行う。
- 後にチャタリング除去を実装するため 1秒に100回タイマー割り込みをさせるようにした。


```c
// 割り込みとタイマーの初期化
alt_ic_isr_register( TIMER_0_IRQ_INTERRUPT_CONTROLLER_ID, TIMER_0_IRQ,
    alarm_callback, (void *)0, (void *)0 );
IOWR_ALTERA_AVALON_TIMER_PERIODL(TIMER_0_BASE, PERIOD & 0xffff);
IOWR_ALTERA_AVALON_TIMER_PERIODH(TIMER_0_BASE, PERIOD >> 16);
IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER_0_BASE,
  ALTERA_AVALON_TIMER_CONTROL_ITO_MSK  |
  ALTERA_AVALON_TIMER_CONTROL_CONT_MSK |
  ALTERA_AVALON_TIMER_CONTROL_START_MSK );
```

#### 回路と値のやり取り
周辺機器として接続しているmypio_ipと値をやり取りする。この部分は無限ループになっているので常に行われている。
- スイッチ入力の読込み
  - スライドスイッチの入力とチャタリング除去されていない状態のプッシュボタンの値を取得している。
    - 下位 10～2bit (0x07FC) スライドスイッチ
    - 下位 1～0bit (0x0003) プッシュボタン
- 7セグメントLEDに表示したい値を書き込む
  - 7セグメントLEDのオンオフ情報と、表示したい値(4bit区切り)を回路に書き込む。

詳しくは以下の表を参照
- 


- 回路に7セグメントLEDに表示する値を書き込む

変数の値をAvalonバス経由でmypio_ip回路のレジスタに書き込む。
書き込みした値は回路側でデコードされ、7セグメントLEDに出力される。

<details>
<summary></summary>

```c
while (1) {
	// スイッチ入力の読み取り
	in = IORD_32DIRECT(MYPIO_0_BASE, 4);
	// 回路に送る情報
	out = hour_on | min_on | sec_on
		 |  hour10 << 20 | hour1 << 16
		 |   min10 << 12 |   min1 <<  8
		 |   sec10 <<  4 |   sec1;
	// 回路に出力
	IOWR_32DIRECT(MYPIO_0_BASE, 0, out);
}
```

</details>


### タイマー割り込み

メイン関数で設定したタイマー割り込みの処理。1秒間に100回実行。
- スイッチ入力のチャタリング除去
- 時刻調整機能
- 時分秒の管理


<details>
<summary>タイマー割り込みのプログラム</summary>

```c
void alarm_callback( void* context )
{
　// スイッチのチャタリング除去
	key_check();

  // 割り込みのフラグbitをクリア
	IOWR_ALTERA_AVALON_TIMER_STATUS(TIMER_0_BASE, 0);
	// 1秒のカウント
  if (cnt == 99){
		cnt = 0;
    // 点滅処理
		sig2hz = 0xffffffff;
		add_sec();
	}else{
    // 点滅処理
		if(cnt == 24 || cnt == 49 || cnt == 74)
			sig2hz = ~sig2hz;
		cnt++;
	}

  // 時刻調整機能
  // ADJUST
	if ( key_push & 0x1 ) {
		key_push &= ~0x1;
		switch (cur){
		case SEC:
			sec1 = 0;
			sec10 = 0;
			break;
		case MIN:
			add_min();
			break;
		case HOUR:
			add_hour();
			break;
		default:
			break;
		}
	}
  // SELECT
	if ( key_push & 0x2 ){
		key_push &= ~0x2;
		switch (cur){
		case SEC:
			cur = MIN;
			break;
		case MIN:
			cur = HOUR;
			break;
		case HOUR:
			cur = SEC;
			break;
		default:
			break;
		}
	}
  // MODE スイッチ
	if ( in & 0x4 ) {
		switch (cur){
		case NORMAL:
			cur = SEC;
			break;
		default:
			break;
		}
	}else{
		cur = NORMAL;
	}
  // MODE24 スイッチ
	if (((in&0x8)==0) != mode24){
		mode24 = ((in&0x8)==0);
		hour_disp();
	}
  // 時刻調整機能が有効になっているとき、調整する桁を点滅させる。
	sec_on  = (cur ==  SEC) ?  SEC & sig2hz:  SEC;
	min_on  = (cur ==  MIN) ?  MIN & sig2hz:  MIN;
	hour_on = (cur == HOUR) ? HOUR & sig2hz: HOUR;
	// デバッグ用の表示
  if(cnt == 0)
	alt_printf(
		"hour= %x%x min=%x%x sec=%x%x in=%x\n",
		hour10, hour1, min10, min1, sec10, sec1, in
	);
}

```

</details>


#### 時分秒の管理
時分秒の管理は、以下の通り(基本的には前記事の24時間時計の制御と変わらない...はず)
- 1秒を計る
  - タイマー割り込みが1秒に100回来るので0-99のカウント1周で1秒
- 10時1時10分1分10秒1秒の桁をそれぞれグローバル変数で管理
  - 自作回路に書き込む値
- 時分秒をカウントアップさせる関数をそれぞれ作る
  - タイマー割り込み時に秒の関数を呼び出し、桁上がりに応じて上位桁の関数を呼ぶ。
- 時のカウントアップは特殊
  - カウントアップ時は`hour`を+1して、`0-23`の値を7セグメントLED表示用に`hour10`と`hour1`に分ける。
    
<details>
<summary>1秒を計る</summary>

```c
if (cnt == 99){
  cnt = 0;
  add_sec();
}else{
  cnt++;
}
```

</details>

<details>
<summary>時分秒加算部分</summary>

```c
// 1秒加算
void add_sec(){
	sec1++;
	while ( sec1>=10 ) {
		sec1 -= 10;
		sec10++;
		while (sec10 >= 6){
			sec10 -= 6;
			add_min();
		}
	}
}
// 1分加算
void add_min(){
	min1++;
	while (min1>=10){
		min1 -= 10;
		min10++;
		while ( min10 >= 6 ){
			min10 -= 6;
			add_hour();
		}
	}
}
// 1時間加算
void add_hour(){
	hour++;
	while(hour >= 24){
		hour -= 24;
	}
	hour_disp();
}
```
</details>


<details>
<summary>hour桁の分割</summary>

- デコーダでもいいがソフトウェアの特権(?)割り算で分割
    - `hour10 = hour / 10` 10で割った商
    - `hour10 = hour % 10` 10で割った余り
```c
// 7セグメントLEDに表示する値に変換
void hour_disp(){
	int temp;
	temp = (!mode24&&hour>=12) ? hour - 12: hour;
	hour1  = temp % 10;
	hour10 = temp / 10;
}
```

</details>


#### スイッチ入力のチャタリング除去
テキストの1時間計ではチャタリング除去を行わず、1秒に1回に起こるタイマー割り込み時に時間調整していたが、タイマー割り込みを1秒に100回にしたので、ソフトウェアでチャタリング除去をしてみる。

チャタリング除去の手順は以下の通り。
- タイマー割り込みの度にスイッチ入力を読み取る。
- 3回同じ入力がされた場合は確定値として保存する
- 前回の確定値と比較して、エッジ検出。

<details>
<summary>キー入力チェック</summary>

- この関数をタイマー割り込みの中で呼び出す
```c
void key_check(){
	static unsigned int key_c0 = 0;
	static unsigned int key_p1 = 0;
	static unsigned int key_p2 = 0;
	static unsigned int key_now = 0;
	static unsigned int key_last = 0;

	key_p2 = key_p1;
	key_p1 = key_c0;
	key_c0 = (~in) & 0x3;
	if (key_p2 == key_p1 && key_p1 == key_c0){
		key_now = key_c0;
	}
	if (key_now != key_last){
		key_push = ~key_last & key_now;
		key_last = key_now;
	}
}
```

</details>

#####  Q. この部分はハードウェアでも良いのでは?
  - ボードによってスイッチの数は異なることが考えられるので、その差分をソフトウェアで処理することで吸収できる。


#### 時刻調整機能
スイッチ入力を受け取って時刻調整を行う。
- スイッチ入力を取得
  - MODE24, MODE (in [3:2]) : スライドスイッチ
    - 回路から読込みしてきた値を直接使う。
  - SELECT, ADJUST(key_push [1:0]) : プッシュボタン
    - チャタリング除去後の押された判定を使う
- スイッチ入力の値のbit毎に以下の操作を行う
  - (1000) → MODE24 スイッチ
    - 12/24時間表示の切り替えを行う
      - "0" : 24時間表示
      - "1" : 12時間表示
  - (0100) → MODE スイッチ
    - 時刻調整機能の有効/無効を切り替える
      - "0" : 無効
      - "1" : 有効
  - (0010) → SELECT ボタン
    - 調整する時分秒を選ぶ
  - (0001) → ADJUST ボタン
    - 選ばれた時分秒の状態で以下の挙動をする
      - 時、分選択時: それぞれの値を+1
      - 秒選択時: 値を0にクリア

<details>
<summary>時刻調整部分</summary>

- 列挙型の定義
  - 状態管理用に時分秒の選択を保持する変数とその値を定義する。
```c
enum cur_param {
	SEC    = 0x01000000, //  sec_on のbit
	MIN    = 0x02000000, //  min_on のbit
	HOUR   = 0x04000000, // hour_on のbit
	NORMAL = 0xffffffff
} cur = NORMAL;
```

- スイッチ入力に応じて時刻調整
```c
if ( key_push & 0x1 ) { // ADJUST ボタン
	key_push &= ~0x1;
	switch (cur){
	case SEC:
		sec1 = 0;
		sec10 = 0;
		break;
	case MIN:
		add_min();
		break;
	case HOUR:
		add_hour();
		break;
	default:
		break;
	}
}
if ( key_push & 0x2 ){ // SELECT ボタン
	key_push &= ~0x2;
	switch (cur){
	case SEC:
		cur = HOUR;
		break;
	case MIN:
		cur = SEC;
		break;
	case HOUR:
		cur = MIN;
		break;
	default:
		break;
	}
}
if ( in & 0x4 ) { // MODE スイッチ
	switch (cur){
	case NORMAL:
		cur = SEC;
		break;
	default:
		break;
	}
}else{
	cur = NORMAL;
}
if (((in&0x8)==0) != mode24){
	mode24 = ((in&0x8)==0);
	hour_disp();
}
```

</details>

<details>
<summary>LED点滅部分</summary>

- LED点滅するタイミングを設定
  - 1秒計るカウンタ部分に処理を追加
```c
if (cnt == 99){
	cnt = 0;
	sig2hz = 0xffffffff;// 追加
	add_sec();
}else{
	if(cnt == 24 || cnt == 49 || cnt == 74) // 追加
		sig2hz = ~sig2hz;// 追加
	cnt++;
}
```

- 選択している桁を点滅させる
```c
sec_on  = (cur ==  SEC) ?  SEC & sig2hz:  SEC;
min_on  = (cur ==  MIN) ?  MIN & sig2hz:  MIN;
hour_on = (cur == HOUR) ? HOUR & sig2hz: HOUR;
```
</details>

#### プログラム全体

<details>
<summary>以上までのプログラムをまとめた全体のCの記述はこちら</summary>

```c
#include "system.h"
#include "io.h"
#include "altera_avalon_timer_regs.h"
#include "sys/alt_irq.h"
#include "sys/alt_stdio.h"

// クロック 50 MHz <-> 20ns
// タイマーの周期 500,000 * 20ns = 10,000,000 ns = 10ms
#define PERIOD 499999

// スイッチ入力
int in;

// 秒
int sec1 = 0;
int sec10 = 0;
// 分
int min1 = 0;
int min10 = 0;
// 時
int hour = 0;
int hour1 = 0;
int hour10 = 0;

// 7セグメントLED、それぞれの桁を表示
int hour_on;
int min_on;
int sec_on;

// プッシュボタン押下状態の管理
int key_push;

// タイマー割り込み時に 0-99 まで数えて 1秒
int cnt = 0;

// 24時間表示切替
int mode24 = 0;

// 0.25s毎に変化： 0xffffffff ⇔ 0x00000000
int sig2hz = 0xffffffff;

// ステートマシン用の定数を列挙
enum cur_param {
	SEC    = 0x01000000,
	MIN    = 0x02000000,
	HOUR   = 0x04000000,
	NORMAL = 0xffffffff
} cur = NORMAL;

// プロトタイプ宣言
void add_sec(); // 1秒加算
void add_min(); // 1分加算
void add_hour(); // 1時間加算
void hour_disp(); // 7セグメントLEDに表示する値に変換

void key_check(){
	static unsigned int key_c0 = 0;
	static unsigned int key_p1 = 0;
	static unsigned int key_p2 = 0;
	static unsigned int key_now = 0;
	static unsigned int key_last = 0;

	key_p2 = key_p1;
	key_p1 = key_c0;
	key_c0 = (~in) & 0x3;
	if (key_p2 == key_p1 && key_p1 == key_c0){
		key_now = key_c0;
	}
	if (key_now != key_last){
		key_push = ~key_last & key_now;
		key_last = key_now;
	}
}

void alarm_callback( void* context )
{

	key_check();


	IOWR_ALTERA_AVALON_TIMER_STATUS(TIMER_0_BASE, 0);
	if (cnt == 99){
		cnt = 0;
		sig2hz = 0xffffffff;
		add_sec();
	}else{
		if(cnt == 24 || cnt == 49 || cnt == 74)
			sig2hz = ~sig2hz;
		cnt++;
	}

	if ( key_push & 0x1 ) {
		key_push &= ~0x1;
		switch (cur){
		case SEC:
			sec1 = 0;
			sec10 = 0;
			break;
		case MIN:
			add_min();
			break;
		case HOUR:
			add_hour();
			break;
		default:
			break;
		}
	}
	if ( key_push & 0x2 ){
		key_push &= ~0x2;
		switch (cur){
		case SEC:
			cur = MIN;
			break;
		case MIN:
			cur = HOUR;
			break;
		case HOUR:
			cur = SEC;
			break;
		default:
			break;
		}
	}
	if ( in & 0x4 ) {
		switch (cur){
		case NORMAL:
			cur = SEC;
			break;
		default:
			break;
		}
	}else{
		cur = NORMAL;
	}
	if (((in&0x8)==0) != mode24){
		mode24 = ((in&0x8)==0);
		hour_disp();
	}
	sec_on  = (cur ==  SEC) ?  SEC & sig2hz:  SEC;
	min_on  = (cur ==  MIN) ?  MIN & sig2hz:  MIN;
	hour_on = (cur == HOUR) ? HOUR & sig2hz: HOUR;
	if(cnt == 0)
	alt_printf(
		"hour= %x%x min=%x%x sec=%x%x in=%x\n",
		hour10, hour1, min10, min1, sec10, sec1, in
	);
}

// メイン関数
int main()
{
	int out;

	// 割り込みとタイマーの初期化
	alt_ic_isr_register( TIMER_0_IRQ_INTERRUPT_CONTROLLER_ID, TIMER_0_IRQ,
			alarm_callback, (void *)0, (void *)0 );
	IOWR_ALTERA_AVALON_TIMER_PERIODL(TIMER_0_BASE, PERIOD & 0xffff);
	IOWR_ALTERA_AVALON_TIMER_PERIODH(TIMER_0_BASE, PERIOD >> 16);
	IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER_0_BASE,
		ALTERA_AVALON_TIMER_CONTROL_ITO_MSK  |
		ALTERA_AVALON_TIMER_CONTROL_CONT_MSK |
		ALTERA_AVALON_TIMER_CONTROL_START_MSK );

	while (1) {
		// スイッチ入力の読み取り
		in = IORD_32DIRECT(MYPIO_0_BASE, 4);
		// 回路に送る情報
		out = hour_on | min_on | sec_on
			 |  hour10 << 20 | hour1 << 16
			 |   min10 << 12 |   min1 <<  8
			 |   sec10 <<  4 |   sec1;
		// 回路に出力
		IOWR_32DIRECT(MYPIO_0_BASE, 0, out);
	}
	return 0;
}
// 1秒加算
void add_sec(){
	sec1++;
	while ( sec1>=10 ) {
		sec1 -= 10;
		sec10++;
		while (sec10 >= 6){
			sec10 -= 6;
			add_min();
		}
	}
}
// 1分加算
void add_min(){
	min1++;
	while (min1>=10){
		min1 -= 10;
		min10++;
		while ( min10 >= 6 ){
			min10 -= 6;
			add_hour();
		}
	}
}
// 1時間加算
void add_hour(){
	hour++;
	while(hour >= 24){
		hour -= 24;
	}
	hour_disp();
}
// 7セグメントLEDに表示する値に変換
void hour_disp(){
	int temp;
	temp = (!mode24&&hour>=12) ? hour - 12: hour;
	hour1  = temp % 10;
	hour10 = temp / 10;
}

```

</details>


# 3. 動作チェック

- **注** : 動作チェックはハードウェア(RTL)のみ行う。
  - ソフトウェアのチェックは省略

ModelSim使って波形を見てみる。


- 7セグメントLEDのデコーダ回路が動いているかチェック
![check01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/a1ed80fa-27b4-bc41-9dd6-6a398395d72c.png)
![check02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/f51f34dd-4928-f9ad-1ed7-08d3cba21cac.png)


- LEDの点滅用の信号が機能しているかチェック
![check03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/7e1d767c-5107-acbe-e676-53d8a5bbb7a4.png)


- スイッチ入力の値が読み取れるかチェック
![check04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/ab9118d4-9522-5bd0-23a0-bf161c7dc354.png)


# 4. まとめ

NiosII CPUを組込んだシステムで24時間時計を作った。
- ソフトウェアの割込み処理で時間を計測
- チャタリング除去をソフトウェアで実装
- 自作回路で7セグメントLEDにデコードして出力


次回は[テキスト]の第7章PS/2インターフェース回路とキーボード入力やマウス入力を受け取るプログラムを作ってみます。


# 参考文献

- [【改訂2版】FPGAボードで学ぶ 組込みシステム開発入門［Intel FPGA編］][text]

[text]: https://gihyo.jp/book/2018/978-4-7741-9388-5
[prev]: ../vol01_clock_24h/clock_24h_with_NSL.md