はじめに
Ben Eater の 6502 computer を、キットを買って組み立てて、コンピューターの基礎を学びました。
6502 は1975年に発表されたCPUで、Apple IIに搭載されたり、互換CPUがファミコンで利用されたり、、、していたらしいです。
以下のものを組み合わせて基本的なコンピューターを作って遊びました。
“Hello, world” from scratch on a 6502 — Part 1
まずは 6502 をブレッドボードに差し、最低限必要な設定である入力を +5V もしくは GND に接続します。
配線
pin | 名称 | 接続 | 備考 |
---|---|---|---|
8 | VDD | + | Power |
21 | VSS | - | Ground |
2 | RDY | + | Ready |
4 | IRQB | + | Interrupt |
6 | NMIB | + | Non Maskable Interrupt |
36 | BE | + | Bus Enabled |
37 | PHI2 | Clock | Clock Module |
38 | SOB | + | Set Overflow |
40 | RESB | + | ボタンでGND接続 |
これで CPU を動作させます。
クロック
数日ぶり2度目のクロックモジュール pic.twitter.com/Wv9Ewehxx8
— Tasuku Suzuki (@task_jp) May 20, 2024
動作確認
次に、動作の挙動が確認できるように、Arduino Mega 2560 を利用して簡単なロジックアナライザを自作します。
アドレスが16本、データが8本あるのでつなぐだけでも大変です。
Arduino でクロックごとにアドレスとデータの確認ができるようになったら、
データを 11101010 となるように抵抗でハードコードをして、挙動を観察してみます。
リセット後に、fffc fffd のアドレスから ea
ea
を読み込み、 eaea のアドレスにジャンプし、ea
命令(=NOP) を実行し続けるようにできました。
How do CPUs read machine code? — 6502 part 2
32Kバイトのデータを保持できる EEPROM をブレッドボードに追加します。
配線
pin | 名称 | 接続 | 備考 |
---|---|---|---|
28 | VCC | + | Power |
14 | GND | - | Ground |
24 | /WE | + | Write Enable |
22 | /OE | - | Output Enable |
20 | /CE | ※1 | Chip Enable |
A0〜A14 | A0〜A14 on 6502 | Address line | |
D0〜D7 | D0〜D7 on 6502 | Data line |
CPU のアドレス空間は16bitで、ROM は15bitなので、8000〜FFFF を ROM に割り当てることにします。
※1: CPU の16bit目を(NAND で反転して) ROM の /CE に接続し、対象のアドレス以外の時にはデータの入出力を行わないようにします。
EEPROM
EEPROM を ea
で埋めて、動作確認をしました。
EEPROM のデータ確認した。正しくeaで埋まってる気がするなー。 pic.twitter.com/cG4wbni92S
— Tasuku Suzuki (@task_jp) May 21, 2024
スタートアドレス
fffc: 00
fffd: 80
とし、8000 にジャンプするようにしました。
EEPROM の先頭から CPU を実行することができたー pic.twitter.com/VOlO5GNpzV
— Tasuku Suzuki (@task_jp) May 21, 2024
プログラミング
LDA #$42
STA $6000
を実行してみます。EEPROM に書くバイナリは以下のとおり
a9 42 8d 00 60
6000番に0x42を保存するコードも動いた。CPU 動かしてる感がすごい。 pic.twitter.com/dUDX0fmuot
— Tasuku Suzuki (@task_jp) May 21, 2024
VIA の追加
6522 は Versatile Interface Adapter(多用途インタフェースアダプタ)で、以下のような機能を提供しています。
- I/O
- タイマー
- 割り込み
pin | 名称 | 接続 | 備考 |
---|---|---|---|
24 | CS1 | 6502 の A15 と A14 | A15 = 0, A4 = 1 で有効に |
23 | CS2B | 6502 の A13 | A14 = 1 で有効に |
22 | RWB | 6502 の RW | |
25 | PHI2 | Clock | 6502 と共通 |
35〜38 | RS0〜RS3 | 6502 の A0〜A3 |
6502 のアドレスラインが以下の際に 6522 が有効になるように配線をします。
A15: 0
A14: 1
A13: 1
6000番台を利用していることになります。
6000〜6004 でレジスタの設定ができるようにしています。
LDA #$ff
STA $6002
LDA #$55
STA $6000
を実行してみます。EEPROM に書くバイナリは以下のとおり
a9 ff 8d 20 60 a9 55 8d 00 60
6522 の Bレジスタに、01010101 を出力し(LEDで確認し)ました。
Lチカ
次に、ループしてみましょう。
LDA #$ff
STA $6002
LDA #$55
STA $6000
LDA #$aa
STA $6000
JMP $8005
を実行してみます。EEPROM に書くバイナリは以下のとおり
a9 ff 8d 20 60 a9 55 8d 00 60 a9 aa 8d 00 60 4c 05 80
CPUを利用したLチカ?できたー pic.twitter.com/N212Ep0PAd
— Tasuku Suzuki (@task_jp) May 22, 2024
Assembly language vs. machine code — 6502 part 3
バイナリでプログラミングをするのが辛くなってきたので、アセンブリ言語でプログラミングをして、vasm でコンピューターで実行可能な形式に変換することにしました。
Connecting an LCD to our computer — 6502 part 4
HD44780 という LCD ディスプレイを 6522 から操作します。
Bポートの8bitでデータの操作をし、Aポートの3bitでその他の制御をすることにしました。
様々な初期設定が若干面倒くさいですが、動画を見ながら地道に実装をします。
Hello World!
— Tasuku Suzuki (@task_jp) May 22, 2024
今まで書いたどんなHello Worldよりも学びが深いなー pic.twitter.com/at8g4EE5V5
What is a stack and how does it work? — 6502 part 5
雑にコピペでとりあえず動くようにしたソースコードを整理します。
jsr
と rts
でサブルーチンの実行をするように変更しました。
すると、謎の挙動で動作しなくなり、それがなぜなのか、スタックとはなんなのかを学習しました。
RAM and bus timing — 6502 part 6
62256 という RAM をブレッドボードに追加し、アドレスバスとデータバスの接続を行います。
RAM はアドレスを指定してからデータが利用可能になるまでに時間がかかるため、念入りにタイミングを確認します。
1MHz 程度では結局大丈夫なのですが、タイミングダイアグラムは読み方が難しい。
Subroutine calls, now with RAM — 6502 part 7
/OE, /CE を接続し、電源を入れると無事に意図したとおりの動作になりました。
Why build an entire computer on breadboards?
ここでちょっと本題からそれて、ブレッドボードの話が挟まります。
ブレッドボード比較して、ちゃんと いいもの を使うように促しています。
それから、全体の電圧が安定するように、コンデンサをできればICごとに、少なくとも電源ラインごとにつけましょうとのことです。
また、高周波数の電流を流すと、位相が遅れたり、減衰したりします。
様々な可能性を検討し、1MHz のクロックを採用したところ、意図したとおりに動作しなくなり、LCD まわりで追加の対応が必要になりました。
ブレッドボードの+と-が繋がってることがあるなんて… pic.twitter.com/g3zBCrDojn
— Tasuku Suzuki (@task_jp) May 30, 2024
How assembly language loops work
LCD パネルの初期化処理を見直して、LCD の busy フラグを確認するようにします。
また、個々に指定していた文字を、文字列形式で扱うように改善しました。
1Mhz で電源オンからちゃんとプログラムが動くようになった。
— Tasuku Suzuki (@task_jp) May 23, 2024
たった188バイトでテキスト出力できてちょっと感動している。 pic.twitter.com/UA9tVOhFCY
Binary to decimal can’t be that hard, right?
2進数の割り算について学び、任意の数字を10進数化することができました。
コンピューターはおもしろいですね。
Hardware interrupts
ハードウェアの割り込みについてお勉強します。
組み込みでたまに出てくる言葉だけどよく分からないやつですね。。。
割り込みちょっとだけ理解した pic.twitter.com/1ZGHDHAS7U
— Tasuku Suzuki (@task_jp) May 23, 2024
Interrupt handling
VIA の割り込み機能を利用して、より本格的な割り込みハンドラの実装を行います。
So how does a PS/2 keyboard interface work?
PS/2 接続でキーボードに対応してみます。
ちゃんとしたPS/2のキーボードを入手して、ちゃんと動くことを確認した。 pic.twitter.com/A4EZnE0pkd
— Tasuku Suzuki (@task_jp) May 30, 2024
シリアルデータを 74HC595 でパラレル化して扱います。
キーボードからのデータを可視化する仕組みを作って、基本的な仕組みやキーボードのコードについて学びました。
シフトレジスタでキー押下のシーケンスをパラレルで取れるようにした。 pic.twitter.com/1lpg2RFvbi
— Tasuku Suzuki (@task_jp) May 30, 2024
Keyboard interface hardware
パラレル化したデータを受けとるのに都合のいい信号を、キーボードの信号を工夫して作成します。
RC回路とダイオードと、シュミットトリガーのインバーターでいい感じに作れました。
突然 LCD が4bitモードに変更されていますが、深く考えずに先に進みましょう。
(詳細は パトロン になることで見ることができます)
キーボードからキーコードを取得してLCDに表示できた pic.twitter.com/slFEbcG037
— Tasuku Suzuki (@task_jp) May 30, 2024
Keyboard interface software
割り込みでスキャンコードを文字に変換し循環バッファにキューイングをして、
通常のループで LCD に表示処理をします。
変換テーブルを内部に用意し、スキャンコードから文字へ変換をします。
キーのプレス/リリースに対応し、シフトにも対応し、キーボードから Hello World! が入力できるようになりました!
Hello world!
— Tasuku Suzuki (@task_jp) May 30, 2024
たかがこれだけのためにすごい苦労をした。すごい。 pic.twitter.com/Cz5E7gPaLF
SPI: The serial peripheral interface
低レイヤーの通信でよく利用される SPI 通信をためします。
BME220と戦っている pic.twitter.com/TotI7E00lS
— Tasuku Suzuki (@task_jp) May 27, 2024
倒せませんでした。
How do hardware timers work?
まずは手始めに地味な方法で。。。
nop のループで LED を点滅させた pic.twitter.com/mxc12EKqDS
— Tasuku Suzuki (@task_jp) May 24, 2024
次に、6522 のタイマー機能を利用してみます。
250msごとにタイマーでLEDを点滅させるようにできた。 pic.twitter.com/sWxdH6vO9s
— Tasuku Suzuki (@task_jp) May 24, 2024
割り込みタイマーで10msごとに時刻を更新して、1秒ごとにLCDで数字を表示をするようにしてみた。LCDの制御とLEDで同じIOの別の番地を使ってるので、両方を同時に動かすのは少し頭を使う必要がありそう。 pic.twitter.com/RcdsmOGSAj
— Tasuku Suzuki (@task_jp) May 24, 2024
完璧にはできませんでした。
The RS-232 protocol
RS-232C でパソコンと通信をできるようにします。
まずは RX を直接つないで nop でタイミングを調整してデータを読んでみました。
RS-232 で Hello World ができた。 pic.twitter.com/nVxT9R0WgX
— Tasuku Suzuki (@task_jp) June 4, 2024
Let's build a voltage multiplier!
RS-232C の通信のために +5V 以上の電圧が必要になりました。
コンデンサを利用して、倍にしてみます。
コンデンサに充電して、5Vを倍の電圧にしてみた。 pic.twitter.com/9l62aGJUAh
— Tasuku Suzuki (@task_jp) June 4, 2024
555 タイマーで高速に切り替えをして(ほぼ)倍の安定した電圧を得ました。
9V弱の電圧になった。不思議というか、なんというか…。 pic.twitter.com/4xqUQ3VtBd
— Tasuku Suzuki (@task_jp) June 4, 2024
6502 serial interface
Serial interface Kit を利用してちゃんと RS-232C に対応していきます。
RS232C 用に MAX232 という IC を利用して電圧を得ます。
MAX232ってICがめちゃくちゃ熱い…のが可視化されててとても良い。めっちゃ熱い🔥 pic.twitter.com/fxUknNLNvX
— Tasuku Suzuki (@task_jp) June 4, 2024
動画の通りに接続したらすごく熱くなりました。。。
RS232 interface with the 6551 UART
W65C51 という IC を利用して UART 通信を簡単に行うことにします。
配線
pin | 名称 | 接続 | 備考 |
---|---|---|---|
10 | TxD | MAX232のTX | TX |
12 | RxD | MAX232のRX | RX |
6 | XTLI | 水晶振動子/1MΩ/- | 3つつなぐ |
7 | XTLO | 水晶振動子/1MΩ | 1.8432MHz |
18〜25 | D0〜D7 | D0〜D7 on 6502 | Data line |
2 | CS0 | 6502 | 5000〜5003のアドレスになるように |
3 | /CS1 | 同上 | 同上 |
13 | RS0 | 同上 | 同上 |
14 | RS1 | 同上 | 同上 |
27 | PH2 | クロック | |
28 | /RB | 6502のRW | |
4 | /RES | リセット |
Fixing a hardware bug in software (65C51 UART)
密かに配線が追加されています。
pin | 名称 | 接続 | 備考 |
---|---|---|---|
9 | /CTS | - | Clear to Send |
Clear to Send (CTSB)
The CTSB input pin controls the transmitter operation. The enable state is with CTSB low. The transmitter
is automatically disabled if CTSB is high.
RS0/RS1 に 5000〜5003 でアクセスして初期設定を済ませます。
データの受信も実装し順調に先に進みますが、データの送信の動作確認に移ったところで問題が発覚します。
6551 のステータスレジスタの送信が常に0を返すため、その情報は利用できず、自前の待ち処理で対応しました。
RS232Cでシリアル通信をして、PC から文字を送れるようになったー pic.twitter.com/bwzwIiGVxx
— Tasuku Suzuki (@task_jp) June 7, 2024
Running Apple 1 software on a breadboard computer (Wozmon)
自作の 6502 コンピューターで、Wozmon を動かしてみました。
.org $8000
.org $ff00
XAML = $24 ; Last "opened" location Low
XAMH = $25 ; Last "opened" location High
STL = $26 ; Store address Low
STH = $27 ; Store address High
L = $28 ; Hex value parsing Low
H = $29 ; Hex value parsing High
YSAV = $2A ; Used to see if hex value is given
MODE = $2B ; $00=XAM, $7F=STOR, $AE=BLOCK XAM
IN = $0200 ; Input buffer
ACIA_DATA = $5000
ACIA_STATUS = $5001
ACIA_CMD = $5002
ACIA_CTRL = $5003
RESET:
LDA #$1F ; 8-N-1, 19200 baud.
STA ACIA_CTRL
LDA #$0B ; No parity, no echo, no interrupts.
STA ACIA_CMD
LDA #$1B ; Begin with escape.
NOTCR:
CMP #$08 ; Backspace key?
BEQ BACKSPACE ; Yes.
CMP #$1B ; ESC?
BEQ ESCAPE ; Yes.
INY ; Advance text index.
BPL NEXTCHAR ; Auto ESC if line longer than 127.
ESCAPE:
LDA #$5C ; "\".
JSR ECHO ; Output it.
GETLINE:
LDA #$0D ; Send CR
JSR ECHO
LDY #$01 ; Initialize text index.
BACKSPACE: DEY ; Back up text index.
BMI GETLINE ; Beyond start of line, reinitialize.
NEXTCHAR:
LDA ACIA_STATUS ; Check status.
AND #$08 ; Key ready?
BEQ NEXTCHAR ; Loop until ready.
LDA ACIA_DATA ; Load character. B7 will be '0'.
STA IN,Y ; Add to text buffer.
JSR ECHO ; Display character.
CMP #$0D ; CR?
BNE NOTCR ; No.
LDY #$FF ; Reset text index.
LDA #$00 ; For XAM mode.
TAX ; X=0.
SETBLOCK:
ASL
SETSTOR:
ASL ; Leaves $7B if setting STOR mode.
STA MODE ; $00 = XAM, $74 = STOR, $B8 = BLOK XAM.
BLSKIP:
INY ; Advance text index.
NEXTITEM:
LDA IN,Y ; Get character.
CMP #$0D ; CR?
BEQ GETLINE ; Yes, done this line.
CMP #$2E ; "."?
BCC BLSKIP ; Skip delimiter.
BEQ SETBLOCK ; Set BLOCK XAM mode.
CMP #$3A ; ":"?
BEQ SETSTOR ; Yes, set STOR mode.
CMP #$52 ; "R"?
BEQ RUN ; Yes, run user program.
STX L ; $00 -> L.
STX H ; and H.
STY YSAV ; Save Y for comparison
NEXTHEX:
LDA IN,Y ; Get character for hex test.
EOR #$30 ; Map digits to $0-9.
CMP #$0A ; Digit?
BCC DIG ; Yes.
ADC #$88 ; Map letter "A"-"F" to $FA-FF.
CMP #$FA ; Hex letter?
BCC NOTHEX ; No, character not hex.
DIG:
ASL
ASL ; Hex digit to MSD of A.
ASL
ASL
LDX #$04 ; Shift count.
HEXSHIFT:
ASL ; Hex digit left, MSB to carry.
ROL L ; Rotate into LSD.
ROL H ; Rotate into MSD's.
DEX ; Done 4 shifts?
BNE HEXSHIFT ; No, loop.
INY ; Advance text index.
BNE NEXTHEX ; Always taken. Check next character for hex.
NOTHEX:
CPY YSAV ; Check if L, H empty (no hex digits).
BEQ ESCAPE ; Yes, generate ESC sequence.
BIT MODE ; Test MODE byte.
BVC NOTSTOR ; B6=0 is STOR, 1 is XAM and BLOCK XAM.
LDA L ; LSD's of hex data.
STA (STL,X) ; Store current 'store index'.
INC STL ; Increment store index.
BNE NEXTITEM ; Get next item (no carry).
INC STH ; Add carry to 'store index' high order.
TONEXTITEM: JMP NEXTITEM ; Get next command item.
RUN:
JMP (XAML) ; Run at current XAM index.
NOTSTOR:
BMI XAMNEXT ; B7 = 0 for XAM, 1 for BLOCK XAM.
LDX #$02 ; Byte count.
SETADR: LDA L-1,X ; Copy hex data to
STA STL-1,X ; 'store index'.
STA XAML-1,X ; And to 'XAM index'.
DEX ; Next of 2 bytes.
BNE SETADR ; Loop unless X = 0.
NXTPRNT:
BNE PRDATA ; NE means no address to print.
LDA #$0D ; CR.
JSR ECHO ; Output it.
LDA XAMH ; 'Examine index' high-order byte.
JSR PRBYTE ; Output it in hex format.
LDA XAML ; Low-order 'examine index' byte.
JSR PRBYTE ; Output it in hex format.
LDA #$3A ; ":".
JSR ECHO ; Output it.
PRDATA:
LDA #$20 ; Blank.
JSR ECHO ; Output it.
LDA (XAML,X) ; Get data byte at 'examine index'.
JSR PRBYTE ; Output it in hex format.
XAMNEXT: STX MODE ; 0 -> MODE (XAM mode).
LDA XAML
CMP L ; Compare 'examine index' to hex data.
LDA XAMH
SBC H
BCS TONEXTITEM ; Not less, so no more data to output.
INC XAML
BNE MOD8CHK ; Increment 'examine index'.
INC XAMH
MOD8CHK:
LDA XAML ; Check low-order 'examine index' byte
AND #$07 ; For MOD 8 = 0
BPL NXTPRNT ; Always taken.
PRBYTE:
PHA ; Save A for LSD.
LSR
LSR
LSR ; MSD to LSD position.
LSR
JSR PRHEX ; Output hex digit.
PLA ; Restore A.
PRHEX:
AND #$0F ; Mask LSD for hex print.
ORA #$30 ; Add "0".
CMP #$3A ; Digit?
BCC ECHO ; Yes, output it.
ADC #$06 ; Add offset for letter.
ECHO:
PHA ; Save A.
STA ACIA_DATA ; Output character.
LDA #$FF ; Initialize delay loop.
TXDELAY: DEC ; Decrement A.
BNE TXDELAY ; Until A gets to 0.
PLA ; Restore A.
RTS ; Return.
.org $FFFA
.word $0F00 ; NMI vector
.word RESET ; RESET vector
.word $0000 ; IRQ vector
6502で作るコンピュータのキットでWozmon が動いて、Lチカができた! pic.twitter.com/j5hBZjdbHz
— Tasuku Suzuki (@task_jp) June 7, 2024
Adapting WozMon for the breadboard 6502
前述の Wozmon の 6502 対応の詳細を見ていきました。
A simple BIOS for my breadboard computer
msbasic を動かすために ca65 という 6502 向けのコンパイラを導入します。
コンパイラとリンカで作業を分担して最終的なバイナリを作成できるように調整します。
それから、BIOS として、簡単な入出力の機能を関数化しました。
Running MSBASIC on my breadboard 6502 computer
MS Basic を他のボードの実装を参考に自前の対応を行い、動かすことに成功しました。
msbasic も動いた。basic 書いたの…昭和以来かもしれない。 pic.twitter.com/tT6IzHN8Cz
— Tasuku Suzuki (@task_jp) June 8, 2024
おわりに
6502 という約50年前に誕生した CPU と、その周辺のチップセットを利用し、コンピューターの基礎知識を学びました。
- CPU の基礎原理
- マシンコード
- EEPROM
- VIA
- LCD
- Stack
- RAM
- アセンブリ
- 2/10進数の計算
- 割り込み
- PS/2 キーボードによる操作
- SPI(途中)
- タイマー
- RS-232/ACIA
- UART
- Wozmon
- MS Basic
キットは こちら から購入可能です。
円安で割高感はありますが、それでも3万円くらいで上記がゼロから学べるなんて夢のようです。