이 프로젝트는 벤 이터(Ben Eater)의 8비트 브레드보드 컴퓨터 프로젝트를 확장한 것입니다. 저는 벤의 브레드보드 컴퓨터 제작 영상을 따라 하며 직접 만들어 보는 과정을 매우 즐겼습니다. 아직 벤의 영상을 보지 않았다면 이 프로젝트를 읽기 전에 먼저 시청하기를 강력히 권장합니다. 배경 지식 없이는 이해하기 어려울 수 있습니다.
컴퓨터를 완성한 후, 매번 전원을 켤 때마다 DIP 스위치로 프로그램을 직접 입력해야 한다면 실용성이 거의 없다는 것을 깨달았습니다. 겨우 16바이트를 반복 입력하는 것만으로도 금방 지치게 됩니다. 게다가 메모리를 256바이트로 확장할 계획이었는데, 프로그램을 로드하고 저장할 방법이 없다면 확장의 의미가 퇴색됩니다.
이 프로젝트로 무엇을 할 수 있나요?
브레드보드 컴퓨터 RAM에서 프로그램 저장
브레드보드 컴퓨터 RAM에 프로그램 로드
브레드보드 컴퓨터 시작 시 저장된 프로그램 자동 실행
시리얼 연결을 통한 메모리 내용 검사 및 수정
어셈블리 언어로 브레드보드 컴퓨터 프로그래밍
메모리 내용 역어셈블(디스어셈블)
명령어 단위 단일 실행(싱글 스텝)
중단점 설정 및 중단점까지 실행
이 기능들을 시연하는 영상입니다. 영상 품질은 양해 바랍니다. 저는 전문 비디오 크리에이터가 아니니까요.
작동 원리
이 프로젝트를 만들며 몇 가지 목표를 세웠습니다:
필요한 하드웨어를 최소화할 것
브레드보드 컴퓨터 자체의 변경을 최소화할 것
처음에는 EEPROM에 프로그램을 저장하고, 이를 브레드보드 컴퓨터의 RAM으로 전송하는 로직을 구상했습니다. 하지만 전송 로직을 설계하는 것이 제 역량을 넘어섰습니다(저는 하드웨어 가까이에서 작업하는 것을 즐기지만 본질적으로 소프트웨어 엔지니어입니다). 아두이노 열성 팬으로서, 전송 로직을 아두이노로 대체하는 아이디어를 떠올렸습니다. 아두이노 우노조차 브레드보드 컴퓨터보다 훨씬 강력하므로 '부정행위'가 아닌지 고민했지만, 결과에 만족하며 타협했습니다.
아두이노가 수행해야 할 역할은 무엇일까요? 메모리에서 데이터를 읽고 쓰는 것입니다. 브레드보드 컴퓨터에 최소한으로 침투하는 방법은 기존 메모리 모듈의 인터페이스인 버스와 제어 신호를 그대로 활용하는 것입니다. 아두이노 디지털 I/O 핀은 양방향이므로, 8개를 버스에 직접 연결하면 버스에서 데이터를 읽고 쓸 수 있습니다. RAM 모듈이 버스에서 읽거나 쓰게 하려면 MI/RI/RO 신호를 적절히 설정하면 됩니다. 이 신호들은 보통 제어 로직의 EEPROM이 구동하는데, 아두이노가 동시에 제어하면 충돌과 단락 위험이 있습니다. 그러나 벤이 사용한 AT28C16 EEPROM에는 칩 인에이블(CE) 입력이 있어, 이를 하이 상태로 두면 모든 데이터 출력이 하이 임피던스(High-Z) 상태가 되어 아두이노가 신호를 조작할 수 있게 됩니다.
RAM 읽기 절차:
EEPROM의 CE 신호를 하이(제어 로직 비활성화)로 설정
읽을 첫 번째 주소를 버스에 출력
MI 신호를 하이로 설정하고 클럭의 로우-하이 전이 대기
MI를 로우, RO를 하이로 설정하고 클럭의 로우-하이 전이 대기
버스에서 데이터 바이트 읽기
2~5단계를 16개 주소 모두에 대해 반복
EEPROM의 CE 신호를 로우(제어 로직 재활성화)로 설정
RAM 쓰기도 유사하며, बस에 데이터를 쓰고 RO 대신 RI를 하이로 설정하면 됩니다. 물론 몇 가지 기술적 과제가 있지만, 이 기본 메커니즘이 프로젝트의 핵심입니다.
브레드보드 컴퓨터에 필요한 변경 사항
두 가지 변경이 필요합니다:
제어 로직 EEPROM의 데이터 출력에 풀다운 저항 추가
EEPROM이 비활성화되는 동안 링 카운터 비활성화(또는 지속적 리셋)
풀다운 저항
EEPROM이 비활성화되면, 제어 신호(AO, AI, EO 등)에 연결된 로직 게이트의 풀업 저항이 해당 신호들을 하이로 끌어올립니다. 그러면 여러 레지스터가 동시에 버스에 데이터를 쓰려 해 충돌이 발생합니다.
74LS 게이트 입력의 풀업 저항은 약 10kΩ입니다. 풀다운 저항은 전압을 로우 영역으로 끌어내릴 만큼 충분히 작아야 합니다. 대부분 신호 라인에는 3.3kΩ 저항을 사용했습니다. 예외 두 가지:
SU(Subtract) 신호: 8개의 XOR 게이트에 연결되어 실질적 풀업 저항이 1.25kΩ(10kΩ/8)입니다. 이를 로우로 내리려면 1kΩ 미만의 풀다운이 필요한데, SU 신호는 버스와의 상호작용을 제어하지 않으므로 풀다운 저항을 생략해도 됩니다.
CE(Counter Enable) 신호: 1kΩ 풀다운 저항이 필요했습니다. 더 큰 값은 일부 경우 프로그램 카운터의 무작위 동작을 유발했습니다.
풀다운 저항을 추가하기 가장 쉬운 위치는 파란색 LED가 장착된 브레드보드입니다. LED 애노드(EEPROM 출력과 연결)와 GND 사이에 저항을 납땜하면 됩니다. (HLT/MI/RI 신호용 저항은 공간 제약으로 브레드보드 다른 위치에 추가했습니다.)
링 카운터 리셋
이 수정은 프로그램 저장/로드 자체에는 필수적이지 않지만, 아두이노에서 제어 로직으로 제어권을 원활히 넘기는 데 필요합니다. CE가 하이(제어 로직 비활성화)인 동안 링 카운터를 0으로 유지합니다. 아두이노가 CE를 다시 로우(제어 로직 활성화)로 전환하면 링 카운터가 0 상태이므로 브레드보드 컴퓨터가 다음 명령어를 실행하기 시작합니다.
제작 당시 저는 EEPROM 출력 하나로 링 카운터를 리셋하는 방식을 사용했습니다. 이는 명령어 완료 즉시 링 카운터를 리셋해 성능을 높이고, 제어 로직 EEPROM이 비활성화될 때 풀다운 저항이 출력을 로우로 끌어내려 자동으로 링 카운터를 리셋하는 효과도 줍니다.
벤의 고정 5단계 링 카운터 구현을 사용하는 경우, 아래 회로 확장으로 CE가 하이일 때 카운터를 리셋할 수 있습니다(좌우 화살표로 벤의 원본 회로와 확장 버전 전환 가능):
이 방식은 3개의 NAND 게이트(74LS00 칩 1개 추가)가 필요합니다. 테스트하지는 않았지만 이론상 작동해야 합니다.
이 수정은 절대 필수는 아닙니다. 생략해도 저장/로드, 모니터/어셈블러/디스어셈블러는 정상 작동합니다. 하지만 아두이노에서 제어 로직으로 제어권 이양이 필요한 기능(시작 시 저장된 프로그램 자동 실행, 디버거의 싱글 스텝 및 실행)은 작동하지 않습니다.
아두이노 설정 방법
Git 아카이브의 스케치를 아두이노에 업로드하고 다음과 같이 연결하세요:
아두이노 5V 핀(Vin이 아님!) → 브레드보드 5V 레일
아두이노 GND 핀 → 브레드보드 GND
아두이노 디지털 핀 2~9 → 버스 0~7
아두이노 디지털 핀 10 → RO를 제어하는 제어 로직 EEPROM 출력
아두이노 디지털 핀 11 → RI를 제어하는 제어 로직 EEPROM 출력
아두이노 디지털 핀 12 → MI를 제어하는 제어 로직 EEPROM 출력
아두이노 아날로그 핀 0 → CLOCK 신호
아두이노 아날로그 핀 3 → 모든 제어 로직 EEPROM의 CE(핀 18) 및 10kΩ 저항을 통해 +5V 연결
이 외에 아날로그 1, 2번 핀을 DIP 스위치 및 푸시버튼에 연결해야 합니다(자세한 내용은 첨부된 프리칭 파일 참조).
최소 구성(기능은 유지):
AO, CO, EO, IO, RO를 제어하는 EEPROM 출력 핀에 3.3kΩ 풀다운 저항 추가
"링 카운터 리셋" 생략
위 배선대로 연결(마지막 단계의 10kΩ 저항 생략 가능)
아날로그 핀 1, 2를 모두 GND에 연결
로드/저장 버튼을 사용하려면 회로도에 따라 버튼, DIP 스위치, 관련 저항을 아날로그 핀 1, 2에 연결하세요.
자동 실행 기능을 사용하려면 아두이노가 리셋 중과 프로그램 전송 중에 프로그램 카운터와 링 카운터를 0으로 유지해야 합니다. 아날로그 핀 3과 +5V 사이의 풀업 저항은 아두이노 리셋 중 제어 로직을 비활성화(프로그램 카운터를 0 유지)합니다. 링 카운터는 위 "링 카운터 리셋" 지침을 따르세요.
프로그램 로드 및 저장 방법
최소 구성에서는 시리얼 인터페이스로 아두이노를 제어합니다. PuTTY나 TeraTerm 같은 터미널 프로그램이 필요합니다. 아두이노 IDE의 시리얼 모니터도 작동하지만 입출력 영역이 분리되어 있어 다소 불편합니다.
브레드보드 컴퓨터 전원 켜기
USB 케이블로 PC와 아두이노 연결
터미널 프로그램 실행, 9600 보드레이트, 8비트, 패리티 없음, 1 스톱 비트 설정
터미널 창에서 ESC 키 눌러 모니터 모드 진입
프롬프트 . 표시 확인
h 입력 후 엔터로 지원 명령어 목록 확인
최소 구성에서는 m, M, C, l, s 명령어를 사용할 수 있습니다. 메모리 내용 보기, 수정, 프로그램 로드/저장이 가능합니다.
버튼으로 프로그램 저장/로드:
브레드보드 컴퓨터 클럭 끄기
DIP 스위치로 저장할 파일 번호 선택
"저장" 또는 "로드" 버튼 누르기. CE에 연결된 LED 점등 → 아두이노가 제어권 획득
브레드보드 컴퓨터 클럭 켜기. 메모리 주소 레지스터 LED가 순차 점멸하며 주소 순회 확인
버스 LED 점멸 멈춤 및 메모리 주소 레지스터가 1111 표시될 때까지 대기
클럭 끄기. CE LED 소등 → 제어 로직에 제어권 반환
시작 시 자동 실행(필수 회로 구성 시): DIP 스위치를 저장된 프로그램 번호로 설정하고 브레드보드 컴퓨터 전원 켜기(또는 리셋 버튼 누르기). 특수 케이스: 모든 DIP 스위치 OFF → 일반 부팅(자동 실행 안 함). 모든 DIP 스위치 ON → 시작 시 바로 모니터 모드 진입.
어셈블러 및 디스어셈블러 사용법
어셈블러/디스어셈블러와 디버거 기능을 사용하려면 본인의 설정에 맞게 아두이노 프로그램의 opcodes_4bit 구조체 정의를 수정해야 합니다:
struct opcodes_struct opcodes_4bit [] = {
{"NOP ", B00000000, 0, false},
{"LDA ", B00010000, 2, true},
...
{".OR ", B11111110, 0, true}, // 시작 주소 설정
{".BY ", B11111111, 0, true}, // 데이터 바이트 정의
{NULL, 0, 0, false}
};
각 줄은 하나의 연산코드를 지정합니다:
첫 번째 필드: 니모닉(예: "LDA"). 즉시 주소 지정 모드에는 니모닉에 '#' 포함. 벤이 "LDI"라고 부르는 것은 여기서는 "LDA #"가 됩니다. 제가 C64에서 6510 어셈블러로 자라서 그렇습니다.
두 번째 필드: 연산코드 자체. 하위 4비트는 항상 0(특수 연산코드 .OR, .BY 제외).
세 번째 필드: 연산코드 실행에 소요되는 사이클 수(페치 사이클 제외). 제 구현에서 LDA는 연산코드 0001, 총 4사이클 소요(페치 2사이클 포함). 벤의 구현(모든 연산코드 5사이클)대로라면 항상 3이 됩니다.
네 번째 필드: 인수 필요 여부. LDA는 인수 필요, OUT은 불필요.
본인의 브레드보드 컴퓨터에 구현된 연산코드에 맞게 이 목록을 수정하세요. 마지막 두 줄(.OR, .BY)은 어셈블러용 특수 연산코드이니 그대로 두세요.
수정 후 아두이노에 업로드하세요. 터미널 연결 후 모니터 모드 진입(터미널에서 ESC 누르거나 DIP 스위치 모두 ON). 이제 프로그램을 디스어셈블할 수 있습니다. 모니터에서 d만 입력하면 주소 0부터 디스어셈블 시작.
어셈블러는 최소 기능이지만 잘 작동합니다. a 입력으로 주소 0부터 어셈블 시작. 규칙:
공백으로 시작하지 않는 줄은 레이블 정의로 시작해야 함. 레이블은 영문자로 시작, 영숫자 조합, 최대 3자, 대소문자 구분.
줄의 첫 공백 다음에 니모닉(LDA, STA, OUT 등) 기대.
특수 니모닉 .BY: 현재 위치에 저장할 데이터 바이트 직접 지정.
특수 니모닉 .OR: 어셈블러에게 새 주소에서 계속 어셈블하도록 지시.
인수가 영문자로 시작하면 레이블로 간주.
숫자 인수는 10진수. 16진수는 '$' 접두사 사용. 예: LDA #$FF.
; 이후는 주석으로 무시.
피보나치 코드 예시:
rst LDA #0 ; x = 0
STA x
LDA #1 ; y = 1
STA y
lp ADD x ; z = y + x
STA z
JC rst ; 오버플로 시 재시작
OUT ; z 출력
LDA y ; x = y
STA x
LDA z ; y = z
STA y
JMP lp ; 루프
x .BY 0
y .BY 0
z .BY 0
제한 사항
아두이노 RAM 절약을 위해 어셈블러는 1패스 어셈블러로 동작합니다(그렇지 않으면 소스 코드 전체를 버퍼링해야 함). 어셈블러는 입력과 동시에 브레드보드 컴퓨터 메모리에 연산코드를 씁니다. 따라서 어셈블 속도는 브레드보드 컴퓨터의 클럭 속도에 좌우됩니다. 터미널 창에 텍스트를 복사-붙여넣기하면 9600 보드레이트로 들어오는 문자를 아두이노가 따라잡지 못해 문자가 손실될 수 있습니다(브레드보드 컴퓨터 클럭 대기 시간이 길기 때문). 해결책: 보드레이트 낮추기, TeraTerm의 문자 간 지연 설정 사용, 또는 브레드보드 컴퓨터 클럭 속도 높이기. 제 클럭은 160kHz까지 올라가며, 이 속도에서는 9600 보드레이트로 복사-붙여넣기해도 문제없습니다.
기본 설정에서 아두이노 스케치는 브레드보드 컴퓨터 클럭 주파수 약 1~2kHz까지 처리 가능(벤의 기본 클럭은 500Hz 이하). 더 빠른 클럭이라면 코드에서 #ifdef FAST_IO 스위치를 찾아 활성화하세요. 최대 250kHz까지 지원하며 160kHz까지 테스트했습니다. 어셈블리로 시간 크리티컬 루프를 구현하면 더 높은 속도도 가능하겠지만, 160kHz만 해도 브레드보드 컴퓨터의 다른 제약으로 인해 체감상 너무 빠릅니다. FAST_IO 활성화 전 코드의 해당 주석을 꼭 읽어보세요.
아두이노에 1kB EEPROM이 있어 1024/16 = 64개 프로그램 저장 가능. 실제로는 설정 데이터용 16바이트 예약으로 63개. DIP 스위치로는 0~15번(자동 실행은 1~14)만 선택 가능하지만 s, l 명령어로는 0~62번 전체 범위 사용 가능.
깔끔하게 정리할 수 없을까요?
가능합니다! 최종 버전에서는 아두이노 우노 대신 베어 Atmega328P 칩(16MHz 크리스탈 및 커패시터 포함)을 사용했습니다. 우노의 전압 레귤레이터는 불필요합니다(아두이노가 우리 전원의 5V를 직접 사용하므로). 단점은 별도 USB-시리얼 컨버터(FTDI 등)가 필요하다는 점입니다. 전체적인 외관이 브레드보드 컴퓨터와 훨씬 잘 어울립니다:
또 다른 최적화는 아두이노/Atmega에서 부트로더를 제거하는 것입니다. 그러면 아두이노 부트로더 시작 시 2초 지연이 사라집니다. 관심 있으시면 댓글로 알려주세요. 방법 공유하겠습니다!