概要
『CPUの創りかた』(渡波郁、マイナビ出版)に解説されている4ビットCPU「TD4」をCPLD「MAX V (5M240ZT100C5N)」で作ってみる。IOは3.3 Vとし、クロックジェネレーター(1 Hz, 10 Hz, 手動クロック)およびROMはマイコンATmega328Pで代用する。
全体の構成
TD4部
TD4の中身はテキストとまったく同じである。開発ツールにはQuartus Prime Lite Editionを使う。HDLではなく回路図エディターで入力した。配線がごちゃごちゃするところはシンボル化してまとめた。MAX Vのブレークアウトボードは https://www.azumino-denshi.com/SHOP/AZMMAXVT0RE240.html を使った。
参考:
後閑哲也, (2019), 「74シリーズで始めるフルディジタル電子工作[準備編], 『トランジスタ技術』2019年12月号(CQ出版), pp.60ff.
`回路入力例を見る/隠す`

クロックジェネレーター & ROM部
クロックジェネレーターとROMはどちらもATmega328Pでまかなう。Atmel Studio 7でビルドする。
TD4_external.cpp
# include "TD4_external.h"
/*
ニモニック(のようなもの)一覧:
MOV_A(Im) : AレジスタにImを転送
MOV_B(Im) : BレジスタにImを転送
MOV_AB    : AレジスタにBレジスタを転送
MOV_BA    : BレジスタにAレジスタを転送
ADD_A(Im) : AレジスタにImを加算
ADD_B(Im) : BレジスタにImを加算
IN_A      : 入力ポートからAレジスタへ転送
IN_B      : 入力ポートからBレジスタへ転送
OUT(Im)   : 出力ポートへImを転送
OUT_B     : 出力ポートへBレジスタを転送
JMP(Im)   : Im番地へジャンプ
JNC(Im)   : Cフラグが1ではないときにIm番地へジャンプ
*/
int main(void){
	// アセンブル(のようなこと)をしてROM (に見立てた配列)へ格納する。
	_[0] = OUT_B;
	_[1] = ADD_B(1);
	_[2] = JMP(0);
	// アドレスの変化に応じてROM (に見立てた配列)からデータが出力できるようにする。
	// 最初のクロックが立ち上がる前のこの時点で0番地のデータが出力される。
	enable_rom();
	// PD2のスイッチ入力に応じてPD5から手動クロックが出力できるようにする。
	enable_manual_clock();
	// PD7から10Hzを、PD6から1Hzを出力する。
	enable_1Hz_10Hz_clock();
	// PD2のスイッチ入力をディバウンスしてPD5から出力する(押してH出力、放してL出力)。
	while(1) debounce_PD2_to_PD5();
	return 0;
}
`ヘッダーファイルを見る/隠す`
# ifndef TD4_EXTERNAL_H_
# define TD4_EXTERNAL_H_
# define F_CPU 8000000UL
# include <avr/io.h>
# include <avr/interrupt.h>
# include <util/delay.h>
# define MOV_A(Im) ( 3 << 4 | ((Im) & 0xF)) // AレジスタにImを転送
# define MOV_B(Im) ( 7 << 4 | ((Im) & 0xF)) // BレジスタにImを転送
# define MOV_AB    ( 1 << 4)                // AレジスタにBレジスタを転送
# define MOV_BA    ( 4 << 4)                // BレジスタにAレジスタを転送
# define ADD_A(Im) ( 0 << 4 | ((Im) & 0xF)) // AレジスタにImを加算
# define ADD_B(Im) ( 5 << 4 | ((Im) & 0xF)) // BレジスタにImを加算
# define IN_A      ( 2 << 4)                // 入力ポートからAレジスタへ転送
# define IN_B      ( 6 << 4)                // 入力ポートからBレジスタへ転送
# define OUT(Im)   (11 << 4 | ((Im) & 0xF)) // 出力ポートへImを転送
# define OUT_B     ( 9 << 4)                // 出力ポートへBレジスタを転送
# define JMP(Im)   (15 << 4 | ((Im) & 0xF)) // Im番地へジャンプ
# define JNC(Im)   (14 << 4 | ((Im) & 0xF)) // Cフラグが1ではないときにIm番地へジャンプ
volatile uint8_t _[16]; // 命令を格納しておく配列。
volatile uint8_t switch_changed = 0;
volatile int8_t counter = 10;
ISR(INT0_vect){         // 手動クロック用のタクトスイッチが押されるか放されるかしたら、
	switch_changed = 1; // そのフラグを立てる
}
ISR(PCINT1_vect){           // アドレスが変化したら、
	PORTB = _[PINC & 0x0F]; // そのアドレスに応じたデータを出力する。
}
ISR(TIMER1_COMPA_vect){
	PORTD ^= (1 << PD7); // 20Hzの頻度でPD7出力をトグルする。結局10Hzの方形波が出力される。
	if(--counter <= 0){  // その方形波を10分周してPD6から出力する。結局1Hzの方形波が出力される。
		PORTD ^= (1 << PD6);
		counter = 10;
	}
}
/*
void enable_reset(void){
	DDRD  |=  (1 << PD3); // PD3から!リセット信号を出力することにする。
	PORTD &= ~(1 << PD3); // 最初に!リセット信号をLにしておいて、
	_delay_ms(100 - 65);  // 適当にリセット時間を設けて、(65msはATmega328P自体のリセットからの遅延時間)
	PORTD |= (1 << PD3);  // リセット期間が明けたら!リセット信号をHに戻す。
}
*/
void enable_manual_clock(void){
	DDRD  |=  (1 << PD5);   // PD5から手動クロックを出力することにする。 
	PORTD &= ~(1 << PD5);   // 最初はLを出力しておく。
	DDRD  &= ~(1 << PD2);   // PD2をスイッチ入力にする。
	PORTD |=  (1 << PD2);   // PD2を内部プルアップする。
	EICRA |=  (1 << ISC00); // INT0 (PD2)が変化したときに割り込み要求を出すことにする。
	EIMSK |=  (1 << INT0);  // 外部割り込みINT0を有効にする。
	sei();	
}
void debounce_PD2_to_PD5(void){
	if(switch_changed){                              // PD2に接続したスイッチが変化したら、
		switch_changed = 0;
		_delay_ms(5);                                // バウンスの収まるまで待ってから、
		if(PIND & (1 << PD2)){PORTD &= ~(1 << PD5);} // スイッチが放されていたらPD5からLを出力し、
		else                 {PORTD |=  (1 << PD5);} // スイッチが押されていたらPD5からHを出力する。
	}
}
void enable_rom(void){
	DDRC  &= ~0x0F; // PC3:0にアドレスを入力することにする。
	//PORTC |=  0x0F; // PC3:0を内部プルアップする。★実際にTD4に接続するときは内部プルアップは不要。
	DDRB  = 0xFF; // PB7:0から命令を出力することにする。
	PORTB = _[0]; // 最初のクロックが立ち上がる前に0番地の命令を命令デコーダーへ与えておく。
	// PCINT11:8 (PC3:0)からピン変化割り込みをかけることにする。
	PCICR  |= (1 << PCIE1);
	PCMSK1 |= ((1 << PCINT11)|(1 << PCINT10)|(1 << PCINT9)|(1 << PCINT8));
	sei();
}
void enable_1Hz_10Hz_clock(void){
	DDRD   |= (1 << PD6)|(1 << PD7); // クロックを出力する端子を指定する。
	TCCR1B |= (1 << WGM12);          // タイマー1をCTCモードで動かす。
	TCCR1B |= (1 << CS11);           // 8分周してタイマー1でカウントする。
	OCR1A   = 49999;                 // コンペア値。(F_CPU/8分周)/(10Hz*2)-1 = 49999
	TIMSK1 |= (1 << OCIE1A);         // タイマー1のコンペアマッチ割り込みを有効にする。
	sei();
}
/*テキストの3分15秒ラーメンタイマー(クロックは1Hz)
_[ 0] = OUT(0b0111); // LEDを3つ点灯
_[ 1] = ADD_A(1);
_[ 2] = JNC(1);      // 16回ループ
_[ 3] = ADD_A(1);
_[ 4] = JNC(3);      // 16回ループ
_[ 5] = OUT(0b0110); // LEDを2つ点灯
_[ 6] = ADD_A(1);
_[ 7] = JNC(6);      // 16回ループ
_[ 8] = ADD_A(1);
_[ 9] = JNC(8);      // 16回ループ
_[10] = OUT(0b0000);
_[11] = OUT(0b0100);
_[12] = ADD_A(1);
_[13] = JNC(10);     // LED点滅を16回ループ
_[14] = OUT(0b1000); // LEDを1つ点灯
_[15] = JMP(15);     // ここにとどまる。
*/
/*テキストのLEDちかちか
_[0] = OUT(0b0011);
_[1] = OUT(0b0110);
_[2] = OUT(0b1100);
_[3] = OUT(0b1000);
_[4] = OUT(0b1000);
_[5] = OUT(0b1100);
_[6] = OUT(0b0110);
_[7] = OUT(0b0011);
_[8] = OUT(0b0001);
_[9] = JMP(0);
*/
/* 外部入力をそのまま外部出力する。
_[0] = IN_B;
_[1] = OUT_B;
_[2] = JMP(0);
*/
/* 0~15まで1ずつインクリメントする、を繰り返す。
_[0] = OUT_B;
_[1] = ADD_B(1);
_[2] = JMP(0);
*/
# endif
上のプログラムを実行しているところ
ファイル一式
(整理中)


