2024년 1월 16일 화요일

ETRI 0.5um CMOS Std-Cell DK 예제: CPU 6502 [X]

ETRI 0.5um CMOS Std-Cell DK 예제: CPU 6502 [2]

II. 시스템 수준 테스트 벤치

    II-1. Apple-1 컴퓨터 시스템 모델
    II-2. 6502 CPU의 베릴로그 RTL

    II-3. 베릴로그 RTL과 SystemC/C++의 결합

        simulation/sc_CPU_6502_Top.h

        simulation/obj_dir/Vcpu.h
        simulation/mti_sim/CPU_6502.h
        simulation/mti_sim/Vcpu.h
        베릴레이터의 언어 변환(Language Translation) 시뮬레이터
        QuestaSim의 동적 링크(Dynamic Linking) 시뮬레이터
    II-4. 동기식 메모리 모델
        simulation/sc_mem.h
        simulation/sc_mem.cpp
    II-5. Apple-1 컴퓨터
        simulation/sc_main.cpp
    II-6. Apple-1의 표준 입력 장치: 키보드
        simulation/a1_keyboard.cpp
        Apple-1의 입력용 PIA: 메모리 매핑된 키보드 포트
    II-7. Apple-1의 표준 출력장치: 디스플레이
        simulation/a1_display.cpp
        Apple-1의 출력용 PIA: 메모리 매핑된 디스플레이 포트

----------------------------------------------------------------


II. 시스템 수준 테스트 벤치

넥슨 박물관의 Apple-1 시연 동영상 1분 7초께[바로가기] 해설자는 이런말은 한다.

    "지금은 당연하지만 (1976년) 당시 컴퓨터로는 (대화형은) 파격적인 기능이었죠."

키보드 입력에 대해 곧바로 화면으로 반응하는 컴퓨터가 등장한 것을 두고 '파격'이라는 평가에 기꺼이 동의한다. 전문 운영자 없이도 누구나 즉각 대화 할수 있게 되자 컴퓨터는 전산실의 설비에서 생활가전이 되었다. 언재든 무슨 일이든 시킬 수 있다는 컴퓨터는 CPU와 주변장치라는 전자부품을 가지고 하드웨어를 꾸몄다. 그리고 외부의 사건에 반응하여 소프트웨어로 구현된 정해진 작업을 수행한다. 하드웨어와 소프트웨어가 결합되어 컴퓨터라는 시스템이 되었다. 범용성을 가진 컴퓨터 시스템은 주어질 사건에 적절히 반응해야 한다. 사건을 받아들이고 그에 대응할 준비가 되어 있어야 한다. 하지만 시스템을 만들 때 어떤 사건이 언재 입력되리라는 예정이 없었다. Apple-1이라는 컴퓨터가 "파격"적이라고 하는 이유가 여기에 있다. 바로 '언재든' 명령만 내리면 반응하는 '대화형' 이라는 점이다. 물론 능력이 않되면 못하겠다고 서슴없이 들이대는 당돌함도 있다. 사용자의 실수나 무지를 즉시 지적하는 싸가지는 덤이다. 위의 동영상에서 화면에 누군가의 얼굴이 문자로 찍히자 호들갑 스러운 환호가 들린다. 오래된 컴퓨터가 다시 작동하는데 감동하는 것일게다. 수억을 들여 경매로 구입 했다하니 그럴만도 하다. 어쨌든 그 환호는 컴퓨터의 CPU보다 워즈니액의 모니터 프로그램에 보내는 찬사로 들린다.

CPU를 설계 하였지만 이 하드웨어가 어떤 구성의 컴퓨터 시스템이 될지 정의되지 않았다. 비록 그렇더라도 CPU 하드웨어 검증은 '시스템' 수준으로 이뤄져야 한다. 실제로 이 하드웨어가 소프트웨어와 어울려 컴퓨터 시스템으로 구성되었을 때를 상정하여 검증이 이뤄져야 한다. 즉 '대화형' 테스트 벤치를 꾸며야 한다는 뜻이다. CPU라는 반도체가 공장에서 출시될 때까지 기다렸다가는 제품 출시의 시기를 놓칠 수 있고 서둘러 제조하기에는 반도체 공정비용이 너무 높기 때문에 반도체 설계물은 철저히 검증 되어야 한다.

80년대에 들어 하드웨어 기술 언어(HDL, Hardware Description Language[바로가기])가 등장하자 모두가 열광했다. 하지만 하드웨어 기술 언어는 시스템 수준 검증에 매우 미흡하다. 설계 후 반드시 거처야 하는 검증용 테스트 벤치를 작성하기에 추상화 수준이 턱없이 미치지 못한다.  하드웨어 언어로 시스템 수준 테스트 벤치를 작성하기 위해 엄청난 공을 들여야 했다[주].

주] 문자열 출력 조차 눈물 겨웠다 C/UNIX Functions for VHDL Testbenches [바로가기].

C/C++ 언어가 추상화 수준을 높이며 발전하자 운영체제를 비롯하여 각종 시스템 프로그래밍에 사용되었고 컴퓨터 시스템은 비약적인 발전을 이뤘다. 하드웨어 언어도 진화해 왔으나 언어 자체의 발전보다 설계기법과 합성기(RTL Synthesizer) 같은 전자설계 자동화(Electronics Automation Tools) 도구에 치중 되었다. 반도체 설계 규모가 증가하자 등장한 하드웨어 설계 도구로서 전용 언어는 단시일 내에 디지털 반도체 설계의 주류가 되었다. 하지만 검증은 반도체 설계에서 항상 걸림돌이었다. 설계비용에서 검증이 차지하는 비율이 점점 작아지고 있긴 하지만 절반을 차지한다.

[출처] OpenROAD: Toward a Self-Driving,Open-Source Digital Layout Implementation Tool Chain[Link]

하드웨어 설계 언어가 검증의 검림돌이 된 이유는 언어가 가진 추상성 수준의 한계 때문이다. 하드웨어 기술 언어의 탄생 목적이 디지털 회로의 설계인데 객체의 비트폭과 클럭 마다 행위를 명확히 해야하는 레지스터 전송 수준(RTL, Register-Transfer Level)으로는 설계의 검증에 사용하기 곤란하다. 게다가 추상성을 높이는 방향으로 발전하기엔 자동화 도구들이 따라오기 어렵다.

소프트웨어를 위해 탄생한 프로그래밍 언어는 하드웨어의 특성을 표현할 수 없었다. 다행히 소프트웨어 언어의 추상성 폭이 매우 넓어지면서 하드웨어 모델링도 가능해졌다. 이종 언어간 혼합 실행 기법이 널리 활용되는 지금 굳이 어렵게 하드웨어 언어로 테스트 벤치 작성하는 일은 줄어들고 있다. 하드웨어 시뮬레이터는 결국 프로그래밍언어 이용해 만들어진 소프트웨어라는 점에 주목하자. SystemC는 하드웨어를 표현할 수 있도록 C++ 언어 크래스를 구축하고 병렬성을 모의실행 시킬수 있는 라이브러리를 묶은 시스템 모델링 도구다[바로가기]. SystemC는 이미 IEEE의 표준으로 등재되어 있고 오픈 소스다[바로가기]. SystemC는 새로운 언어가 아니기에 이미 널리 사용되는 모든 C++ 컴파일러(GNU C/C++, Microsoft Visual C/C++)를 써서 설계 검증 환경의 구축이 가능하다. 이에 덧붙여 현대 컴퓨팅 운영체제에서 제공하는 쓰레딩(POSIX 같은)과 프로세스간 통신(IPC, named pipe 또는 FIFO 같은)을 활용하여 시스템 모델을 작성 할 수 있다[주].

주] 리눅스 커널 API 프로그래밍을 다룬다. The Linux Programming Interface: A Linux and UNIX System Programming Handbook[바로가기][E-Book pdf]

하드웨어 언어는 설계에 집중하고 시스템 수준 검증도구로 C/C++를 활용하는 추세로 설계 방법론이 발전하는 덕분에 설계 비용에서 검증이 차지하는 비중이 줄어들고 있다[4]. 미래의 반도체 설계 언어로 C/C++가 도입되기 시작 했고 이를 RTL로 변환하는 자동화 도구 HLS (Hihg-Level Synthesis[Link])들이 상용화 되었다[5][6]. 이제 하드웨어 설계자 들도 RTL의 HDL에서 벗어날 때가 되었다. 오늘날 어셈블리 언어로 소프트웨어를 개발하는 사례를 찾아볼 수 없듯이 하드웨어 설계도 곧 그런 시대가 될 것이다.

주4] C/C++ 모델을 HDL과 연결 하려는 시도, Integrating SystemC Models with Verilog and SystemVerilog Models Using the SystemVerilog Direct Programming Interface [바로가기]

주5] Towards Automatic High-Level Code Deployment on Reconfigurable Platforms: A Survey of High-Level Synthesis Tools and Toolchains [link]

주6] HLS 블로그, "고위합성 튜토리얼"[바로가기]


[출처] High-Level Synthesis Blue Book [link]

본 예제에서는 C/C++ 언어로 6502 CPU를 채택한 컴퓨터 시스템을 테스트 벤치로 구성하여 베릴로그 RTL을 검증한다.

II-1. Apple-1 컴퓨터 시스템 모델

6502 CPU의 검증을 위해 메모리 그리고 입출력 장치를 갖춘 Apple-1 컴퓨터 시스템을 모델링 한다. 이 시스템을 구성하는 각 프로세스(CPU와 입력장치 그리고 표시장치)의 행위는 SystemC/C++로 기술하고 프로세스 간 통신은 운영체제(리눅스 또는 윈도우즈)의 IPC(명명된 파이프, named pipe 또는 FIFO)를 활용한다. 1976년 발매당시 Apple-1에 적재되었던 워즈니액의 모니터 롬 코드(Wozniac Monitor ROM code[바로가기])를 수정 없이 실행 시킬 수 있다.

8비트 CPU 6502의 예제의 묶음 파일 내려받을 수 있다[download]. 디자인 킷의 하위 폴더에 예제의 압축을 풀면 다시 여러 하위 폴더를 가진 구조를 취하는데 이 구조를 유지하면 관리에 유리하다.

+---------------------------+
| ~/ETRI050_DesignKit/v.1a/ |
+-------+-------------------+
        |
        +-- Tools : Open-source archives
        |
        +-- digital_ETRI -> digital_ETRI050_m2d (symbolic link)
        |
        +-- pads_ETRI050
        |
        +-- scripts
        |
        +-- tech
        |
        +-- Ref_Design/CPU_6502 : Project Home
                         |
                         +-- log
                         |
                         +-- source : RTL Verilog
                         |
                         +-- layout : P&R results
                         |
                         +-- simulation : C++ Tectbench
                         |        |
                         |        +--- Apple-1 : Assembler, Firmware
                         |
                         +-- synthesis : Synthesized netlists

위의 폴더 구조에서 베릴로그 파일들은 source 에, 테스트벤치 C++와 헤더 파일은 simulation에, 그리고 최종적으로 제작도면 GDS 는 layout 폴더에 있다. CPU를 설계하고 있으므로 이를 작동 시키기 위한 소프트웨어가 필요하다. 소프트웨어 개발 도구로 6502 어셈블러와 C 컴파일러를 사용한다. simulation 폴더 아래에 Apple-1 폴더는 6502 어셈블러와 C 컴파일러[바로가기], 워즈니액 모니터 wozmon.bin 를 비롯한 예제가 있다. 6502 CPU용 응용 프로그램명은 program.bin 또는 program.hex 로 고정되었다.

6502 CPU의 베릴로그 RTL을 검증하기 위해 구축한 Apple-1의 컴퓨터 시스템의 구성은 아래와 같다. 6502 CPU의 메모리와 주변장치 그리고 입출력 장치 프로세스들은 SystemC와 C++로 작성되었다. 컴퓨터의 모든 동작은 어떤 언어(베릴로그든 파이썬이든)를 사용하든 결국 C/C++를 통한다.

|<--------System Level Testbench-------->|<---IPC--->|<---I/O Dev-->|
                                          (named PIPE)  Interactive
+----------+  +-------------------------+
| 6502 CPU |  | sc_mem.h (
SystemC)      |            +--------------+
| (Verilog)|  |    +----------------+   |            a1_diaplay   |
|          |  |   0000   (RAM)      |   |            +--------------+
|          |  |    |...-------------+   |            |\             |
|          |  |    |     (PIA I/O)  |   |            |0:A9 00 AA .. |
|         <<DI<<  D011    weite(...)~~~~~~[FIFO]~~~~>|_             |
|          |  |    |                |   display.pipe |              |
|         >>DO>>  D012 KBD_Buf[]    |   |            +--------------+
|          |  |    |      |         |   |            +-------------+
|         >>AB>>   |    +-PThread---.-+ |            | a1_keyboard |
|          |  |    |    | thread_kbd()| |            +-------------+
|         >>WE>>   |    |   read(...)<~~~~[FIFO]~~~~~|:_           |
|          |  |    |    |             |keyboard.fifo |             |
|       <<clk |    |    +-----------.-+ |            +-------------+
+----------+  |    |                |   |
+-----------+ |    |                |   | +----+      Interactive
|        >clk>>   FF00--------------+   | |    |    Dynamic Stimulus
|           | |    |  Woz. Monitor  |   | |    |
|           | |    |    ...(ROM)... |   | |    |
|           | |   FFFE Reset Vector |   | |    |
|           | |    +----------------+   | |    |
|           | +-------------------------+ |    |
|           +-----------------------------+    |
| sc_CPU_6502_Top.h (SystemC)                  |
+----------------------------------------------+

예제를 내려받아 압축을 풀어보면 Apple-1 컴퓨터 시스템 모델의 소스 코드가 매우 단촐 하다는 것에 놀랄 것이다. 이렇게 되기까지 상당한 시간과 수고가 들긴 했다. 늘 그렇듯이 알고나면 별거 아니다.

II-2. 6502 CPU의 베릴로그 RTL

6502 CPU가 역사적인 의미를 가지고 있는 만큼 RTL 베릴로그가 다수 공개되어 있다. 본 예제는 그중 Arlet의 Verilog-6502를 깃허브를 통해 입수하여 활용한다[바로가기]. 이 모델은 2개의 베릴로그 파일로 구성되었다.

1. cpu.v: 6502 CPU의 내부 레지스터들과 명령 디코더 및 제어 기능을 기술한다.
2. ALU.v: CPU 내부의 산술논리연산 부분을 기술한다.

+---------------------------------------------------------+
cpu.v                                                   |
|      +=============+=========================+==========> DI[7:0]
|      |             |                         |          |
|   +--V--+       +--V--+                      |          |
|    \     \     /     /                +------V------+   |
|     \     +---+     /  +--------+     |             |   |
|      \     ALU.v   /   |  ACC   |     | Instruction |   |
|       \           /    +--------+     |  Decoder    |   |
|        +----+----+     |   X    |     |             |   |
|             | +--------+--------+     +....(FSM)....+   |
|             | |  Flag  |   Y    |     |             |<--- IRQ
|             | +--------+--------+     |  Control    |<--- NMI
|             | |   PCH  |  PCL   |     |   Unit      |<--- RDY
|             | +---+----+---+----+     +------+------+   |
|             |     |        |                 |          |
|             +=====:========:=================*==========> DO[7:0]
|                   +========+=================:==========> AB[15:0]
|                                              +----------> WE
|   6502 CPU                                             |
|                                                       <----- RESET
|                                                       <----- CLK
+---------------------------------------------------------+   

6502 CPU를 RTL로 기술한 베릴로그 파일들은 합성을 거쳐 반도체 칩 제작을 위한 레이아웃 생성으로 이어진다. 하드웨어 설계자라면 RTL 구문이 어렵지 않을 것이기 때문에 6502 CPU의 베릴로그 소스를 자세히 다루지 않겠다. 레지스터 전송 수준(RTL, Register-Transfer Level)의 베릴로그는 따로 다루기로 한다[주].

주] 베릴로그 학습, Verilog Tutorial 으로 검색하면 하루만에 배울 수 있다는 페이지들이 많이 검색된다. 컴퓨팅 언어를 이해하고 있다면 사실 그렇다고 단언 한다. 다만 검색되는 페이지들늬 수준이 문제다. 동영상 강좌도 많다. RTL 위주로 설명된 강좌글이나 동영상을 권한다. 기왕이면 익히 들어본 회사들의 컨텐츠를 고르자. Verilog는 Verilog HDL Basic, Intel , SystemC는 Learn SystemC, Forte DS 를 추천한다.

II-3. 베릴로그 RTL과 SystemC/C++의 결합

RTL 베릴로그 소스를 자세히 다루지 않은 이유는 구문구조가 워낙 단순하기 때문이다. 디지털 논리회로를 안다면 구문은 반나절만 배우면 이해할 수 있다. 문제는 시스템 수준 검증 체계를 세우는 일이다. 하드웨어 언어로 테스트 벤치를 작성하느라 수많은 기교를 부리며 마치 HDL의 최고 수준인 양 뽐내는 것을 본다. 초보 소프트웨어 개발자가 보더라도 굳이 부질없는 짓을 한다고 할지 모른다. 그만큼 HDL은 검증용으로 쓰기에 추상성이 매우 낮다는 뜻이된다. 이제부터 6502 CPU를 검증하기 위한 Apple-1 컴퓨터를 구성하면서 C++로 얼마나 쉽게 시스템 검증 환경을 구축 할 수 있는지 보자. Apple-1 컴퓨터 모델에 사용된 소스 파일들은 아래와 같다.

simulation/sc_CPU_6502_Top.h

베릴로그 RTL을 SystemC의 환경에 통합하기 위한  감싸기(wapper)용 C++ 헤더 파일이다. 베릴로그를 이해하지 못하는 C++ 컴파일러에게 6502 CPU의 외형을 SystemC의 모듈 크래스에 싸서 노출 시킨다. C++컴파일러는 동작하는 객체(이를 흔히 API, Application Programing Interface,라고 한다)가 있다는 것 외에 감싸기 뒤에 뭐가 있는지 알필요도 없다.

    #include "Vcpu.h"

    SC_MODULE(sc_CPU_6502_Top)    // C++ Class
    {
        sc_clock            clk;
        sc_signal<bool>     reset;
        sc_signal<uint32_t> DI;     // Data-in
        sc_signal<uint32_t> DO;     // Data-out
        sc_signal<bool>     WE;     // Write Enable
        sc_signal<bool>     IRQ;    // Interrupt Request
        sc_signal<bool>     NMI;    // Non-Maskable Interrupt
        sc_signal<bool>     RDY;    // Ready
        sc_signal<uint32_t> AB;     // Address Bus

        // Verilated DUT
        Vcpu*   u_CPU_6502;    // 6502 CPU Verilog

        // Memory & I/O Model
        sc_mem* u_mem;

        SC_CTOR(sc_CPU_6502_Top) :   // Constructor
            clk("clk", 100, SC_NS, 0.5, 0.0, SC_NS, false),
            reset("reset"), DI("DI"), DO("DO"), WE("WE"), 
AB("AB"),
            IRQ("IRQ"), NMI("NMI"), RDY("RDY")
        {
            // DUT Instantiation
            u_CPU_6502 = new Vcpu("u_CPU_6502");
            // Binding
            u_CPU_6502->clk(clk);
            u_CPU_6502->reset(reset);
            u_CPU_6502->DI(DI);
            u_CPU_6502->DO(DO);
            u_CPU_6502->WE(WE);
            u_CPU_6502->IRQ(IRQ);
            u_CPU_6502->NMI(NMI);
            u_CPU_6502->RDY(RDY);
            u_CPU_6502->AB(AB);

            // Memory Instantiation
            u_mem = new sc_mem("u_mem");
            // Binding
            u_mem->clk(clk);
            u_mem->DI(DO);
            u_mem->DO(DI);
            u_mem->WE(WE);
            u_mem->AB(AB);

            // Environments: Reset/RDY/IRQ/NMI
            SC_THREAD(reset_generator);
            sensitive << clk;
        }

        // Environment: Reset/RDY/IRQ/MNI
        void reset_generator()
        {
            IRQ.write(0);
            NMI.write(0);
            RDY.write(0);
            reset.write(1);
            wait(clk.posedge_event());
            RDY.write(1);
            wait(clk.posedge_event());
            wait(clk.negedge_event());
            reset.write(0);
        }
    };

위의 감싸기 헤더에 선언된 객체 Vcpu는 베릴로그 언어로 기술된 CPU 하드웨어를 C++ 영역으로 들여옴을 뜻한다. 이종의 언어로 기술된 객체를 들여오는 방법은 언어 변환기(language translatoe)를 쓰거나 병행 시뮬레이션(co-simulation)을 수행할 수도 있다.

먼저, 언어 변환기를 사용하는 방법이다. 베릴레이터(Verilator)는 베릴로그(Verilog) 언어로 기술된 구문을 SystemC/C++로 변환해 주는 오픈-소스 도구다[바로가기]. 사용법도 매우 간단하다.

$ verilator --sc -Wall --top-module cpu \
                ../source/cpu.v ../source/ALU.v

하위 폴더 obj_dir에 SystemC 로 변환된 베릴로그 모듈이 생성된다. 최상위 모듈의 C++ 크래스 명이 Vcpu 다.

simulation/obj_dir/Vcpu.h

하위 폴더 obj_dir은 베릴레이터가 변환한 C++의 파일들이 저장되는 곳이다. 구문은 표준 C++ 이긴 하지만 기계 번역의 산물 이다. 인간이 읽기엔 내용이 매우 산만하다. 베릴로그 상위 모듈 cpu에서 번역되어 C++로 노출시킨 크래스 이름이 Vcpu이며 입출력 포트의 선언(방향과 자료형) 정도만 유의하여 살펴봐 두자.

    class alignas(VL_CACHE_LINE_BYTES) Vcpu VL_NOT_FINAL :
     public ::sc_core::sc_module, public VerilatedModel {
         ...........
        public:
        // PORTS
        sc_in<bool> &clk;
        sc_in<bool> &reset;
        sc_in<uint32_t> &DI;
        sc_out<uint32_t> &DO;
        sc_out<bool> &WE;
        sc_in<bool> &IRQ;
        sc_in<bool> &NMI;
        sc_in<bool> &RDY;
        sc_out<uint32_t> &AB;
         ............
        // CONSTRUCTORS
        SC_CTOR(Vcpu);
         ............
    };

이종의 언어로 기술된 객체를 들여오는 또다른 방법은 병행 시뮬레이션(Co-Simulation)이다. 실행중인 프로세스가 다른 실행 파일(쉐어드 오브젝트, shared object 혹은 다이나믹 링킹 라이브러리, DLL)를 함수처럼 부르는 방법이다. 베릴로그 시뮬레이터도 어짜피 컴퓨팅 언어의 컴파일러와 다를바 없다. 소스 코드를 컴파일하여 실행 코드(바이너리)를 만들고 이를 컴퓨터에서 실행 시킨다. 컴퓨터의 운영체제는 실행 중인 소프트웨어(이를 프로세스라고 한다)들 사이에 데이터를 주고받도록 허용하는데 이를 IPC 라고 한다. 병행 시뮬레이션도 IPC를 활용하여 각각 실행중인 베릴로그 시뮬레이터와 C++ 코드 사이에 실행중 데이터를 주고 받는다. C++에서 베릴로그 시뮬레이터를 마치 하위 함수(API)처럼 취급하는 셈이다. C++ 컴파일러는 실행 중 호출할 다른 프로세스의 함수를 알고 있어야 한다. 이 함수를 정의하고 노출 시키는 방법은 하드웨어 언어마다 다르다. 하드웨어의 검증에 C 언어 함수를 불러 쓸 필요성이 절실했기에 베릴로그는 진즉부터 PLI/VPI라는 표준을 제정해 두었다. 하지만 매우 불편했다. 시스템베릴로그 SystemVerilog에서 이를 개선하여 DPI-C 를 표준으로 세웠다. 이 표준에 따르면 "직접 연결(direct programing interface)[SystemVerilog DPI]"이라고 하지만 중간 절차가 복잡하고 보기 사납다[바로가기]. 상용 HDL 시뮬레이터 QuestaSim은 대표적인 하드웨어 언어(Verilog, SystemVerilog, VHDL)를 단일 실행체계에서 취급한다. SystemC 가 표준으로 제정됨에 따라 이에 추가되었다. 최상위 모듈이 어떤 언어로 되어 있던지 상관하지 않는다.

simulation/mti_sim/CPU_6502.h

시뮬레이션을 위한 최상위 모듈(테스트 벤치)이 SystemC일 경우 하위 모듈이 HDL로 작성 되었다면 C++ 컴파일러[주]에게 하드웨어 언어로 작성된 모듈이 있다는 표시(sc_foreign_module)를 해주고 그 모듈의 이름을 알려주기만 하면 된다. C++의 가상 크래스를 활용하면 하위 모듈이 다른 언어로 작성 되었으므로 컴파일 시 내용은 무시하고 외곽만 유지하게 만들 수 있다. 아래의 예에서 CPU_6502 크래스는 SystemC/C++로 노출될 입출력 신호의 소속 데이터만 선언되어 있고 소속 함수가 없는 껍데기다. 크래스의 구성자(constructor)에 연결할 베릴로그의 모듈명 "cpu" 을 적시하고 있다. 이런 크래스를 베릴로그 감싸개(Verilog wrapper)라고 한다.

    /***************************************************
    Filename: CPU_6502.h
    Purpose: Verilog wrapper for Arlet's Verilog-6502
    ****************************************************/
    class CPU_6502 : public sc_foreign_module
    {
    public:
        sc_in<bool> clk;
        sc_in<bool> reset;
        sc_out<sc_bv<16> > AB;
        sc_in<sc_bv<8> > DI;
        sc_out<sc_bv<8> > DO;
        sc_out<bool> WE;
        sc_in<bool> IRQ;
        sc_in<bool> NMI;
        sc_in<bool> RDY;
        CPU_6502(sc_module_name nm) :
            sc_foreign_module(nm, "cpu"), // Verilog module name
            clk("clk"), reset("reset"),
            AB("AB"), DI("DI"), DO("DO"), WE("WE"),
            IRQ("IRQ"),
            NMI("NMI"),
            RDY("RDY")
        {
        }
    };

[주] QuestaSim 에 SystemC를 컴파일 하기 위해 C++ 컴파일러를 내장하고 있다. 이 컴파일러는 리눅스 운영체제의 GNU의 GCC 툴 체인과 동일 하다. 이는 하드웨어 시뮬레이터에서 리눅스 운영체제의 모든 기능을 그대로 활용할 수 있다는 뜻이기도 하다.

simulation/mti_sim/Vcpu.h

베릴로그는 비트 크기를 상세히 지정한 RTL이지만 베릴로그 언어 변환기 베릴레이터는 이보다 높은 수준의 32-비트 정수 자료형  uint32_t 를 사용한다. 하지만 HDL 시뮬레이터 QuestaSim의 베릴로그 감싸개는 RTL 이다. 입출력 포트의 자료형을 맞춰주는 포장기를 한꺼풀 더 씌워 줬다. Apple-1 컴퓨터 시스템을 구성하는 메모리 모델이 베릴레이터의 자료형을 따랐기 때문이다.

    #include "systemc.h"
    #include "CPU_6502.h"
    /********************************************************
    Filename: Vcpu.h
    Purpose: SystemC interface wrapper compatible with
                Verilated SC module
    *********************************************************/
    SC_MODULE(Vcpu)
    {
        sc_in<bool>         clk;
        sc_in<bool>         reset;
        sc_in<uint32_t>     DI;
        sc_out<uint32_t>    DO;
        sc_out<bool>        WE;
        sc_in<bool>         IRQ;
        sc_in<bool>         NMI;
        sc_in<bool>         RDY;
        sc_out<uint32_t>    AB;

        sc_signal<bool>         _clk;
        sc_signal<bool>         _reset;
        sc_signal<sc_bv<16> >   _AB;
        sc_signal<sc_bv<8> >    _DI;
        sc_signal<sc_bv<8> >    _DO;
        sc_signal<bool>         _WE;
        sc_signal<bool>         _IRQ;
        sc_signal<bool>         _NMI;
        sc_signal<bool>         _RDY;

        CPU_6502*   u_CPU_6502;    // foreign module

        void eval()
        {
            _clk = clk.read();
            _reset = reset.read();
        
            _DI = DI.read();
            DO.write(sc_uint<32>(_DO));
            WE.write(_WE);
            AB.write(sc_uint<32>(_AB));
            _IRQ = IRQ.read();
            _NMI = NMI.read();
            _RDY = RDY.read();
        }
    
        SC_CTOR(Vcpu) : // constructor
            clk("clk"), reset("reset"),
            AB("AB"), DI("DI"), DO("DO"), WE("WE"),
            IRQ("IRQ"), NMI("NMI"), RDY("RDY")
        {
            SC_METHOD(eval);
            sensitive << clk << reset;
            sensitive << DI << IRQ << NMI << RDY;

            // Verilog model instantiation
            u_CPU_6502 = new CPU_6502("u_CPU_6502");
            // Binding
            u_CPU_6502->clk(_clk);
            u_CPU_6502->reset(_reset);
            u_CPU_6502->DI(_DI);
            u_CPU_6502->DO(_DO);
            u_CPU_6502->WE(_WE);
            u_CPU_6502->IRQ(_IRQ);
            u_CPU_6502->NMI(_NMI);
            u_CPU_6502->RDY(_RDY);
            u_CPU_6502->AB(_AB);
        }
    };

SystemC/C++ 환경에서 베릴로그 RTL을 불러 결합하는 두가지 방법에 대해 다뤘다. 하위 모듈을 결합하는 방식의 차이를 비교하면 아래와 같다.

베릴레이터의 언어 변환(Language Translation)

 +-------------------+
 | sc_CPU_6502_Top   |
 +------+------------+
        |  +-----------------+
        +--| obj_dir/Vcpu.h  |
           +--+--------------+--+     Language
              | Behavior in C++ |<--- Translation --- Verilog
              +-----------------+     (Verilator)

                                 +-------------------------------+
                                 |   sc_CPU_6502_Top             |
                                 |  +-------------------------+  |
                                 |  | Vcpu                    |  |
 +------------------+            |  | (SystemC wrapper)       |  |
 | cpu              |            |  |            :            |  |
 |(Verilog)     clk <            |  |            :   clk[bool]<---
 |             reset<            |  |    C++     : reset[bool]<---
 |           DI[7:0]<            |  | Translated :DI[uint32_t]<---
 |           DO[7:0]> --Verilator-->|  Behavior  :DO[uint32_t]>---
 |          AB[15:0]>            |  |            :AB[uint32_t]>---
 |                WE>            |  |            :    WE[bool]>---
 |               IRQ<            |  |            :   IRQ[bool]<---
 |               NMI<            |  |            :   NMI[bool]<---
 |               RDY<            |  |            :   RDY[bool]<---
 |                  |            |  |            :            |  |
 +------------------+            |  |            :            |  |
                                 |  |            :            |  |
                                 |  +-------------------------+  |
                                 |  (SystemC/C++)                |
                                 +-------------------------------+

QuestaSim의 동적 링크(Dynamic Linking)

 +-------------------+
 | sc_CPU_6502_Top   |
 +------+------------+
        |  +----------------+
        +--| mti_sim/Vcpu.h |
           +----+-----------+
                |  +-----------------+
                +--| CPU_6502.h      |                   +----------+
                   | SystemC wrapper <...Dynamic Linking |Verilog   |
                   +-----------------+    at Run-Time ...>  Binary  |
                                                         +----------+

                    +---------------------------------------+
                    |                        sc_CPU_6502_Top|
                    |                +----------------+     |
                    | +--------------+---+  Vcpu      |     |
                    | |        CPU_6502  |(SC wrapper)|     |
                    | +---+    (Verilog  |            |     |
                    +----+|      wrapper)|            |     |
                         ||              |            |     |
 +------------------+    ||              |            |     |
 |cpu               |  D ||              |            |     |
 |(Verilog)     clk <~~Y~~<_clk[bool]    |   clk[bool]<------
 |             reset<~~N~~<_reset[bool]  | reset[bool]<------
 |           DI[7:0]<~~A~~<_DI[sc_bv<8>] |DI[uint32_t]<------
 |           DO[7:0]>~~M~~>_DO[sc_bv<8>] |DO[uint32_t]>------
 |          AB[15:0]>~~I~~>_AB[sc_bv<16>]|AB[uint32_t]>------
 |                WE>~~C~~>WE[bool]      |    WE[bool]>------
 |               IRQ<~~~~<IRQ[bool]      |   IRQ[bool]<------
 |               NMI<~~L~~<NMI[bool]     |   NMI[bool]<------
 |               RDY<~~I~~<RDY[bool]     |   RDY[bool]<------
 |                  |  N ||              |            |     |
 +------------------+  K ||              |            |     |
                         ||              |            |     |
                    +----+|              |            |     |
                    | +---+              |            |     |
                    | |                  |            |     |
                    | +--------------+---+            |     |
                    |                +----------------+     |
                    | (SystemC/C++)                         |
                    +---------------------------------------+

위의 두가지 방법중 어떤 경우에도 SystemC/C++로 노출되는 모듈의 크래스는 Vcpu 이며 입출력 자료형도 동일하다. SystemC로 작성된 상위 시스템 수준 테스트 벤치에 어떤 방법을 사용하던 동일하게 적용할 수 있다.

II-4. 동기식 메모리 모델

베릴로그 RTL로 기술된 CPU 6502의 메모리 인터페이스는 동기형 방식을 사용하고 있다는 점에 유의한다. 동기식 메모리(synchronous memory) 인터페이스 타이밍은 아래와 같다. 클럭의 엣지에 맞춰 입출력이 이뤄진다. 주소는 클럭 엣지 이전에 준비되어야 하며 해당 주소의 데이터는 다음 클럭에서 사용 할 수 있다. 디지털 회로가 모두 클럭에 맞춰 동기되는 만큼(그래서 RTL 이다) 칩에 내장된 메모리의 입출력 타이밍도 이를 따른다. 입출력 타이밍을 칩 내의 플립플롭과 동기화 시켜  놓음으로써 지연이 심한 메모리의 입출력 데이타가 회로 전체에 주는 영향을 배제할 수 있다.

[이미지출처] https://docs.xilinx.com/r/en-US/am007-versal-memory/NO_CHANGE-Mode-DEFAULT

simulation/sc_mem.h

동기형 메모리의 SystemC 모델은 아래와 같다. SystemC는 매크로 정의를 적극 활용하고 있어서 마치 다른 언어로 보이지만 C++의 크래스다. 헤더 파일 sc_mem.h 에 메모리의 외형인 입출력 신호를 선언하고 소속 함수 mem_Thread()는 쓰레드로서 클럭 신호 clk 에 감응시켰다. 입력 clk에 사건이 발생하면 이에 반응하는 사건구동(event-driven)방식 시뮬레이션 모델이다.

//-------------------------------------------------------------
// Filename: sc_mem.h
SC_MODULE(sc_mem)
{
    sc_in<bool>         clk;    // Clock for Synchronous Memory model
    sc_in<uint32_t>     AB;     // Address Bus
    sc_in<uint32_t>     DI;     // Data In
    sc_out<uint32_t>    DO;     // Data Out
    sc_in<bool>         WE;     // Write Enable

    void mem_Thread();
    uint32_t* mem;

    // Util
    int ReadHEX(char* Hex_Filename, int nOffset);
    int ReadBIN(char* BIN_Filename, int nOffset);

    SC_CTOR(sc_mem) :   // Constructor
        clk("clk"),
        AB("AB"),
        DI("DI"),
        DO("DO"),
        WE("WE")
    {
        SC_THREAD(mem_Thread);
        sensitive << clk;

        mem = new uint32_t[65536];

        ReadBIN((char*)"./Apple-1/wosmon.bin", 0xFF00);
    }
};

simulation/sc_mem.cpp

SystemC의 모듈은 C++의 크래스다. 내부 구성은 하드웨어 언어의 순차처리 블럭과 동일한 방식[주3]으로 하드웨어를 기술 한다. 단, C++언어이기 때문에 근본적으로 병렬 할당문(concurrent assign)은 허용되지 않는다. 사건에 반응하여 호출될 동기식 메모리 모델의 쓰레드 함수 mem_Thread() 는 다음과 같다.

주3] 베릴로그의 always, VHDL의 process()에 주어지는 감응 리스트(sensitivity list)

// -------------------------------------------------------------
// Filename: sc_mem.cpp

unsigned char KBD_Buff[MAX_BUF] = "";

// Keyboard Thread
void* thread_keyboard(void *arg) {
    int readfd = open(A1_KEYBOARD, O_RDONLY);

    char fifoBuf[MAX_BUF];
    int n;
    while(true) {
        n = read(readfd,(char*)fifoBuf,MAX_BUF-1)); // Read from FIFO
 
        for (int i=0; i<n; i++) {
            KBD_Buff[i] = fifoBuf[i];
            KBD_Buff[i+1] = '\0';
        }
    }
}

// -----------------------------------------------
// Synchronous Memory Model & Memory mapped I/O
// -----------------------------------------------
void sc_mem::mem_Thread()
{
    uint32_t    Address;
    int         nIdx = 0;

    // FIFO(named pipe) for display terminal
    int writefd = open(A1_DISPLAY, O_WRONLY);

    // Keyboard input from another terminal
    pthread_t thread_kbd;
    pthread_create(&thread_kbd, NULL, thread_keyboard, NULL);

    while (true)    // Thread: wait forever
    {
        wait(clk.posedge_event()); wait for event on 'clk'

        Address = AB.read(); // Read Address-Bus

        if (WE.read()) // Memory Write Enable?
        {
            mem[Address] = DI.read(); // WRITE MEMORY

            // Memory Mapped I/O (Screen & Keyboard)
            if (Address == PIA_DSP_REG)    // PIA.B display register
            {
                if(write(writefd, (char*)&c, 1) < 0) // Write to FIFO
                    perror("PIA_DSP_REG: write fail");
                mem[Address] = (mem[Address] & 0x07F); // Clear B7 
            }
        }
        else // Memory Read
        {
            if (Address == PIA_KBD_REG)    // PIA.A keyboard input
            {
                switch(KBD_Buff[nIdx])
                {
                case '\0':
                    mem[PIA_KBD_CTL] = (mem[PIA_KBD_CTL]&0x3F);
                    nIdx = 0;
                    break;
                ............
                default:
                    mem[Address] = (KBD_Buff[nIdx] | 0x80);
                    mem[PIA_KBD_CTL] = (mem[PIA_KBD_CTL]|0x80);
                    KBD_Buff[nIdx++] = '\0';
                    break;
                }
            }
            
            DO.write(mem[Address]); // READ MEMORY
        }
    }
}

6502 CPU의 입출력 처리 방식은 메모리 매핑(memory mapped I/O)이다. 할당된 주소에 접근 하면 입출력 장치가 작동된다. 따라서 메모리 모델 내에 입출력 처리 부분이 포함 되어있다. Apple I 시스템에 부착된 입출력 장치는 키보드와 스크린이다. 스크린의 경우 CPU에서 출력의 시점을 결정하므로 메모리 쓰기로 단순 하지만 키보드 입력은 임의 시점에 일어나므로 단순히 메모리 읽기로 처리하기 어렵다. 임의 시점 입력을 처리하는 방법으로 인터럽트를 쓰거나 주기적으로 검사하는 폴링 방식이 있다. Apple I 컴퓨터는 키보드 입력을 입력주변장치 칩(PIA, Parallel Interface Adapter)를 부착하고 폴링 방식을 적용하고 있다. 입력이 존재 유무를 위해 PIA의 컨트롤 레지스터에 핸드 쉐이크(Hand-Shake) 비트를 두고있다.

6502 CPU의 입출력 장치는 메모리에 매핑되어 있다. Apple I 컴퓨터 시스템에는 기본 입력장치로 키보드(KBD)와 출력 장치로 디스플레이(DSP)가 각각 0xD010 와 0xD012에 할당되어 있다.

[그림출처] Apple-1 Operation Manual [바로가기]  


II-5. Apple-1 컴퓨터

CPU에 동기식 메모리 모델이 연결된 Apple-1 컴퓨터 시스템을의 구성은 아래와 같다. 6502의 입출력 장치는 메모리 매핑 이므로 메모리 모델 내에 키보드와 디스플레이 주변장치를 구현했다. 입출력 장치의 메모리 매핑은 외부 입출력 장치 편에서 다룬다.

+--Apple-1 Computer System------------------------------------------+
|                                                  sc_CPU_6502_Top  |
|                                                                   |
|    +----------------------------------------------+               |
|    |                               (SystemC)      |               |
|    +------------------+    CPU_6502  :  Vcpu      |               |
|+------------------+   |              :            |    +---------+|
||CPU_6502          |   |              :            |    |         ||
||(Verilog)     clk <---<_clk[bool]    :   clk[bool]<    |         ||
||             reset<---<_reset[bool]  : reset[bool]<    | sc_mem  ||
||           DI[7:0]<---<_DI[sc_bv<8>] :DI[uint32_t]< <==>         ||
||           DO[7:0]>--->_DO[sc_bv<8>] :DO[uint32_t]>    |         ||
||          AB[15:0]>--->_AB[sc_bv<16>]:AB[uint32_t]>  Apple-1 I/O ||
||                WE>--->WE[bool]      :    WE[bool]>  PIA_KBD: D010|
||               IRQ<---<IRQ[bool]     :   IRQ[bool]<  PIA_DSP: D012|
||               NMI<---<NMI[bool]     :   NMI[bool]<    |         ||
||               RDY<---<RDY[bool]     :   RDY[bool]<    |         ||
||                  |   |              :            |    |         ||
|+------------------+   |              :            |    |         ||
|    +------------------+              :            |    +---------+|
|    |                                 :            |               |
|    +----------------------------------------------+               |
|                                                                   |
+-------------------------------------------------------------------+

simulation/sc_main.cpp

CPU와 메모리 그리고 입출력장치로 구성된 Apple-1 컴퓨터 시스템 모델을 SystemC의 시뮬레이션 커널에 올려놓도록 한다. 여느 C/C++의 소프트웨어 처럼 main() 함수에 선언해 주는 것으로 Apple-1 컴퓨터가 사례화(instantiation[주])된다.

주] 사례화(instantiation)는 실행형 객체를 존재케 한다는 뜻이다. C++의 크래스는 데이타와 코드를 함께 묶어놓은 매우 추상적인 객체다. 사례화된 C++의 크래스는 처음 내부 조성을 상세화(elaboration) 한다. 이때 자동적으로 호출되는 소속 함수가 구성자(constructor)다. 감응될 사건(sensivity list)과 이에 반응하여 호출될 쓰레드 함수의 지정이 필수다.


    /***********************************************************
    Associated Filename: sc_main.cpp
    Purpose: sc_main()
    ************************************************************/
    #include "sc_CPU_6502_Top.h"    // Apple-1 Computer Model

    int sc_main(int argc, char** argv)
    {
        // Instantiate Apple-1 Computer
        sc_CPU_6502_Top u_sc_CPU_6502_Top("u_sc_CPU_6502_Top");

        sc_start();    
// Run Simulator
        return 0;
    }

사례화된 객체가 구성되면 sc_start()를 호출하여 시뮬레이터를 진행 시킨다. 사건구동(event-driven) 시뮬레이터는 사건의 발생과 이에 반응하는 쓰레드가 존재하는한 수행이 이어진다. 앞서 살펴본 Apple-1 컴퓨터를 묘사한 크래스 sc_CPU_6502_Top에 클럭 발생 함수가 있다.

    clk("clk", 100, SC_NS, 0.5, 0.0, SC_NS, false)

그리고 Vcpu 크래스에 이 클럭에 감응된 쓰레드가 있다. 베릴로그에서 변환된 모델의 감응지정은 아래와 같다.

    // obj_dir/Vcpu.cpp
    Vcpu::Vcpu(sc_module_name /* unused */)
        : VerilatedModel{*Verilated::threadContextp()}
        , vlSymsp{new Vcpu__Syms(contextp(), name(), this)}
        , clk{vlSymsp->TOP.clk}
            ......
    {
        ......
        // Sensitivities on all clocks and combinational inputs
        SC_METHOD(eval);
        sensitive << clk;
        ......
    }

베릴로그 시뮬레이터 QuestaSim 에 동적 링크시키는 경우 Vcpu 크래스는 자료형 변환을 위한 메쏘드 함수에 모든 입력 신호를 감응 지정했다.

    // Filename: mti_sim/Vcpu.h
    SC_MODULE(Vcpu)
    {
        SC_CTOR(Vcpu) : // constructor
            clk("clk"), reset("reset"), ......
        {
            SC_METHOD(eval);
            sensitive << clk << reset;
            sensitive << DI << IRQ << NMI << RDY;
            ......
        }
    };

베릴로그 포장기(Verilog wrapper) 크래스 CPU_6502 에는 입출력 신호 지정외에 다른 조치는 필요 없다. 신호를 넘겨주면 베릴로그 시뮬레이터가 구동될 것이기 때문이다. 결국 두개의 사건구동 시뮬레이터가 서로 맞물려 작동하는 셈이다. 시뮬레이션 실행 속도가 느려지는 이유다.

II-6. Apple-1의 표준 입력 장치: 키보드

소프트웨어의 멀티 쓰레딩(multi-threding[바로가기])과 운영체제의 프로세스간 통신(IPC, Inter-Process Communication[바로가기])을 동원 함으로써 임의 순간에 이뤄지는 사건을 처리하여 하드웨어의 검증에 활용 할 수 있다. 이 기법들은 다중처리용으로도 이미 확립되어 있으므로 쉽게 소프트웨어를 제작할 수 있다. 게다가 이들 소프트웨어 장치들은 가장 원론적인 C/C++로 작성되어 있기에 하드웨어 행위 기술용 SystemC와 함께 활용하면 대화형 하드웨어 테스트벤치를 어렵지 않게 구축할 수 있다.

simulation/a1_keyboard.cpp

Apple-1의 기본 입력장치는 키보드(keyboard)다. 키보드는 외부장치로 별도의 프로세스(프로그램)로 작동 하며 명명된 파이프(named pipe 또는 FIFO)를 통해 Apple-1 컴퓨터 시뮬레이터와  통신한다. 키보드 프로세스의 C++ 코드는 아래와 같다. 프롬프트 '>' 를 표시한 후 리눅스 시스템의 표준입력장치 stdin (키보드)에서 ASCII 문자열을 받아 FIFO에 전송을 반복하는 매우 단순한 프로그램이다.

    // Filename: a1_keyboard.cpp
        ......
    int main()
    {
        int writefd, n;
        char buf[MAX_BUF]={0,};

        mkfifo(A1_KEYBOARD, 0666);
        writefd = open(
"keyboard.fifo"O_WRONLY);

        while(1)
        {
            putchar('>');
            fgets(buf, MAX_BUF, stdin);

            n = strlen(buf) + 2;
            write(writefd, buf, n);
        }

        close(writefd);
        unlink("keyboard.fifo");
    }

키보드 프로세스는 키보드 데이터 전송하기 전에 리눅스 운영체제의 시스템 함수 mkfifo()로 Apple-1 프로세스와 통신할 FIFO(named pipe)를 생성한다. 운영체제는 FIFO를 파일로 간주하므로 시스템 함수 open()으로 "keyboard.fifo"로 명명된 파일을 쓰기 모드 O_WRONLY로 연 후 입력된 키보드 문자열을 write()를 이용해 써넣다.

Apple-1의 입력용 PIA: 메모리 매핑된 키보드 포트

Apple-1 컴퓨터의 키보드 입력 포트(input port)는 주소 0xD010(PIA_KBD_REG)에 메모리 매핑 되어 있다. CPU가 읽기동작으로 접근해 오면 동기식 메모리는 주소가 키보드 입력 포트 PIA_KBD_REG와 일치할 경우 키보드 버퍼 KBD_Buff[] 에서 1바이트를 내보낸다.

    // Filename: sc_mem.cpp
    #define PIA_KBD_REG 0xD010
    #define PIA_KBD_CTL 0xD011
        ......
    void sc_mem::mem_Thread(){
        ......
        while (true){   // Thread: wait forever
            ......
            wait(clk.posedge_event()); // wait for event on 'clk'
            Address = AB.read(); // Read Address-Bus
            ......
            if (WE.read()){    // Memory Write Enable?
                ......
            }
            else { // Memory Read
                ......
                if (Address == PIA_KBD_REG){// PIA.A keyboard input
                    ......
                    if (KBD_Buff[nIdx]=='\0') {
                        ......
                        mem[Address] = 0x00;
                        mem[PIA_KBD_CTL]=(mem[PIA_KBD_CTL]&0x3F);
                        nIdx = 0;
                    } else {
                        ......
                        mem[Address] = (KBD_Buff[nIdx] | 0x80);
                        mem[PIA_KBD_CTL] = (mem[PIA_KBD_CTL]&0x3F);
                        KBD_Buff[nIdx++] = '\0';
                    }
                }
                else if (Address == PIA_KBD_CTL) { // PIA.A Control
                    if (KBD_Buff[nIdx])
                        mem[Address] = (mem[Address]|0x80);// Ready
                }
                    ......
                DO.write(mem[Address]); // READ MEMORY
            }
        }
    }

하드웨어의 주변 인터페이스 장치 PIA(Peripheral Interface Adaptor)는 포트를 감시하고 유효 데이터 전송을 확인하는 일련의 동작을 구현하기 위해 핸드-쉐이크(hand-shake)를 장치한다. PIA는 포트마다 데이터를 저장하는 레지스터와 핸드쉐이크용 컨트롤 레지스터를 둔다. 입력 포트에 유효한 데이터가 들어오면 준비되었음을 표시하고 CPU가 읽어가면 이 비트는 지워진다. Apple-1에 장치된 PIA는 컨트롤 레지스트 PIA_KBD_CTRL 상위 7번째 비트를 핸드-쉐이크 용으로 사용하고 있다. 키보드로부터 유효한 데이터가 있음을 알리기 위해 핸드-쉐이크 비트를 세운다.

    if (KBD_Buff[nIdx])
        mem[PIA_KBD_CTL] = (mem[PIA_KBD_CTL]|0x80); // Input Ready

CPU가 입력 포트의 데이터를 읽어가고 핸드-쉐이크 비트를 지운다.

    mem[Address] = (KBD_Buff[nIdx] | 0x80);      // Read KBD Reg.
    mem[PIA_KBD_CTL] = (mem[PIA_KBD_CTL]|0x3F);  // Clear B7

키보드 프로그램 a1_keyboard 에서 Apple-1 시뮬레이터로 키보드 값이 언재 전송될지 특정 되지 않았다. 대화형(interactive) 시뮬레이터라는 점을 기억하자. CPU는 시뮬레이터의 클럭 발생기에 반응하여 주기적으로 동작한다. 주변 장치는 임의 시점에 입력될 키보드 값을 취하기 위해 버퍼를 둔다. 하드웨어에서 입력을 감시하는 동작을 구현하기 위해 외부 프로세스로부터 전송되는 통로 FIFO를 감시하는 별도의 쓰레드를 띄우고 유효한 데이터가 들어올 경우 읽어서 메모리 데이터 버스로 출력한다. 동기실 메모리 모델 중 PIA의 입력 포트를 기술한 부분은 아래와 같다.

    // Filename: sc_mem.cpp
        ......
    unsigned char KBD_Buff[MAX_BUF] = "";

    // Keyboard Thread
    void* thread_keyboard(void *arg){
        char fifoBuf[MAX_BUF];
        int n;
        int readfd = open("keyboard.fifo", O_RDONLY);
            ......
        while(true) {
            n = read(readfd,(char*)fifoBuf,MAX_BUF-1)); // Read FIFO
            for (int i=0; i<n; i++) {
                KBD_Buff[i] = fifoBuf[i];
                KBD_Buff[i+1] = '\0';
            }
        }
    }

    // -----------------------------------------------
    // Synchronous Memory Model & Memory mapped I/O
    // -----------------------------------------------
    void sc_mem::mem_Thread() {
        uint32_t    Address;
        int         nIdx = 0;

        // Keyboard input from another terminal
        pthread_t thread_kbd;
        pthread_create(&thread_kbd, NULL, thread_keyboard, NULL);
            ......
        while (true){   // Thread: wait forever
            wait(clk.posedge_event()); // wait for event on 'clk'
            ......
            if (WE.read()) { // Memory Write Enable?
                ......
            }
            else { // Memory Read
                ......
                if (Address == PIA_KBD_REG) {// PIA.A keyboard input
                        ......
                    if (KBD_Buff[nIdx]=='\0'){
                        ......
                    }
                }
            }
        }
    }

시스템 수준 모델링에서 FIFO 장치의 역활이 매우 중요하다.

II-7. Apple-1의 표준 출력장치: 디스플레이

사건을 일으키는(또는 데이터를 생성하는) 주체에 따라 모델의 복잡도가 매우 차이난다.

simulation/a1_display.cpp

Apple-1 컴퓨터의 기본 출력 장치는 문자 디스플레이다. 디스플레이는 외부장치로 별도의 프로세스(프로그램)로 작동 하며 명명된 파이프(named pipe 또는 FIFO)를 통해 Apple-1 컴퓨터 시뮬레이터가 출력하는 문자를 표시한다. 디스플레이 프로세스의 C++ 코드는 아래와 같다. FIFO에 유효한 데이터가 차면 이를 읽어 출력하는 매우 단순한 프로그램이다. 키보드 프로세스보다 간단하다. 반복 while() 구간 내에 read()는 FIFO 읽기용 시스템 함수다. FIFO 파일이 비었을 경우 다른 프로세스에게 실행권을 넘기는 블로킹 시스템 함수를 씀으로써 무한 반복 구역내에서 폭주하지 않는다.

    // Filename: a1_display.cpp
    int main()
    {
        int readfd, n;
        char buf[MAX_BUF]={0,};

        mkfifo("display.fifo", 0666);
        readfd = open(
"display.fifo", O_RDONLY);

        while(1){
            n = read(readfd, buf, 1));   // MAX_BUF
            putchar(buf[0]);
        }
    }

Apple-1의 출력용 PIA: 메모리 매핑된 디스플레이 포트

Apple-1 컴퓨터의 디스플레이 출력 포트(output port)는 주소 0xD012(PIA_DSP_REG)에 메모리 매핑 되어 있다. CPU가 쓰기동작으로 동기식 메모리에 접근해 올때 주소가 디스플레이 출력 포트 PIA_DSP_REG와 일치할 경우 "display.fifo"로 명명된 파이프(named pipe 또는 FIFO)에 1바이트를 써넣는다. 동기식 메모리 모델 중 PIA의 출력 포트 부분은 아래와 같다.

    // Filename: sc_mem.cpp
    ......
    #define PIA_DSP_REG 0xD012
    #define PIA_DSP_CTL 0xD013
    ......
    // ------------------------------------------------
    // Synchronous Memory Model & Memory mapped I/O
    // ------------------------------------------------
    void sc_mem::mem_Thread()
    {
        uint32_t Address;
        .....
        // FIFO(named pipe) for display terminal
        int writefd = open("display.fifo", O_WRONLY);
        .....
        while (true) {
            wait(clk.posedge_event());
            Address = AB.read(); // Read Address-Bus

            if (WE.read()){ // Memory Write
                mem[Address] = DI.read();  // WRITE MEMORY
                ......
                // Memory Mapped I/O (Screen & Keyboard)
                if (Address == PIA_KBD_REG)    // PIA.A keyboard reg.
                    .....
                else if (Address == PIA_DSP_REG) { // PIA.B display
                    unsigned char c = (unsigned char)(mem[Address]);
                    write(writefd, (char*)&c, 1); // Write to FIFO
                    mem[Address] = (mem[Address]&0x07F); // Clear B7
                }
            }
            else { // Memory Read
            .....
            }
        }
    }

--------------------------------

[목차][이전][다음]


댓글 없음:

댓글 쓰기