

앞서 우리가 설계했던 대본의 택배 상자(cfs_apb_item_drv) 안에는 APB Master가 제어해야 할 필수 정보들(addr, wr_data, direction, delay)이 들어있습니다.
이제 uvm_info로 화면에 출력만 하던 코드를 싹 지우고, 저 상자 안의 데이터를 꺼내서 실제 물리적 인터페이스 핀에 쏘아주는(Drive) 진짜 로직으로 교체할 준비를 합니다.

`ifndef CFS_APB_DRIVER_SV
`define CFS_APB_DRIVER_SV
class cfs_apb_driver extends uvm_driver#(.REQ(cfs_apb_item_drv));
//Pointer to agent configuration
cfs_apb_agent_config agent_config;
`uvm_component_utils(cfs_apb_driver)
function new(string name = "", uvm_component parent);
super.new(name, parent);
endfunction
virtual task run_phase(uvm_phase phase);
drive_transactions();
endtask
//Task which drives one single item on the bus
protected virtual task drive_transaction(cfs_apb_item_drv item);
cfs_apb_vif vif = agent_config.get_vif();
`uvm_info("DEBUG", $sformatf("Driving \"%0s\": %0s", item.get_full_name(), item.convert2string()), UVM_NONE)
for(int i = 0; i < item.pre_drive_delay; i++) begin
@(posedge vif.pclk);
end
vif.psel <= 1;
vif.pwrite <= bit'(item.dir);
vif.paddr <= item.addr;
if(item.dir == CFS_APB_WRITE) begin
vif.pwdata <= item.data;
end
@(posedge vif.pclk);
vif.penable <= 1;
@(posedge vif.pclk);
while(vif.pready !== 1) begin
@(posedge vif.pclk);
end
vif.psel <= 0;
vif.penable <= 0;
vif.pwrite <= 0;
vif.paddr <= 0;
vif.pwdata <= 0;
for(int i = 0; i < item.post_drive_delay; i++) begin
@(posedge vif.pclk);
end
endtask
//----------------------------------------------------------------
//Task for driving all transactions
protected virtual task drive_transactions();
cfs_apb_vif vif = agent_config.get_vif();
//Initialize the signals
vif.psel <= 0;
vif.penable <= 0;
vif.pwrite <= 0;
vif.paddr <= 0;
vif.pwdata <= 0;
forever begin
cfs_apb_item_drv item;
seq_item_port.get_next_item(item);
drive_transaction(item);
seq_item_port.item_done();
end
endtask
endclass
`endif
이 태스크는 대본에서 받은 item 상자를 열어서, 정확히 APB 하드웨어 스펙에 맞춰 핀을 흔듭니다.
drive_transaction(item) (단수형 - 1건 전담 실무자)
이 녀석은 관리자가 택배 상자를 하나 넘겨줄 때마다 호출되어, 그 상자 딱 1개(Single item)에 대한 물리적 핀 제어만 전담하는 실무자(Worker)입니다.
drive_transactions() (복수형 - 무한 루프 관리자)
이 녀석은 시뮬레이션이 시작되자마자 0초부터 끝날 때까지 쉬지 않고 돌아가는 공장 라인의 관리자(Manager)입니다.
그냥 하나의 forever 루프 안에 코드를 다 때려 넣으면 타이핑도 덜 하고 편할 텐데, 굳이 함수를 두 개로 나눈 결정적인 이유가 있습니다. 바로 '리셋(Reset) 상황에 대처하기 위해서'입니다.
나중에 칩을 검증하다 보면, 칩이 한창 데이터를 주고받는 중간에 밖에서 리셋 버튼을 콱 눌러버리는 극한의 테스트(Reset On The Fly)를 하게 됩니다.
만약 코드가 하나로 뭉쳐있다면? 핀이 중간에 멈춰서 엉키고 시뮬레이션이 꼬여버립니다.
하지만 이렇게 쪼개 놓으면? 리셋 신호가 들어왔을 때 관리자가 돌고 있는 drive_transactions 프로세스 전체를 외부에서 강제로 킬(Kill/Disable)해버리고 다시 처음부터 깔끔하게 재시작시키기가 엄청나게 쉬워집니다. 강사가 나중 튜토리얼을 위해 미리 "리셋 방어용 뼈대"를 세워둔 것이죠!
// Wait for the requested pre-drive delay cycles
for(int i = 0; i < item.pre_drive_delay; i++) begin
@(posedge vif.pclk);
end
대본에서 "이 상자 던지기 전에 3 클럭 쉬어!"(pre_drive_delay)라고 명령했다면, 그만큼 클럭의 상승 엣지(posedge)를 기다리며 뜸을 들입니다.
// Setup Phase: Assert PSEL and drive address/control signals
vif.psel <= 1;
vif.pwrite <= bit'(item.dir);
vif.paddr <= item.addr;
if(item.dir == CFS_APB_WRITE) begin
vif.pwdata <= item.data;
end
@(posedge vif.pclk); // Wait 1 clock cycle
APB 통신의 첫 번째 단계인 Setup Phase입니다.
PSEL(Chip Select)을 1로 띄워서 "야, 슬레이브! 너한테 데이터 보낼 거야!"라고 깨웁니다. 동시에 주소(paddr)와 읽기/쓰기 방향(pwrite), 그리고 쓰기 모드라면 데이터(pwdata)까지 버스에 쫙 깔아줍니다.
그리고 슬레이브가 이 신호들을 인식할 수 있도록 정확히 1 클럭(@(posedge vif.pclk);)을 대기합니다.
// Access Phase: Assert PENABLE
vif.penable <= 1;
@(posedge vif.pclk); // Wait 1 clock cycle
// Wait States: Stall until the slave is ready (PREADY == 1)
while(vif.pready !== 1) begin
@(posedge vif.pclk);
end
1 클럭이 지나면 Access Phase로 넘어갑니다. 이때 반드시 PENABLE 신호를 1로 띄워야 합니다. (이게 APB 스펙의 핵심입니다!)
그리고 또 1 클럭을 쉰 다음, 슬레이브가 PREADY 신호를 1로 줄 때까지 무한정 대기(while 루프)합니다. 슬레이브가 바빠서 PREADY를 0으로 잡고 있으면, 마스터(우리의 드라이버)는 핀 상태를 그대로 유지한 채 클럭만 보내며 얌전히 기다려야 합니다.
1 클럭이 지나면 Access Phase로 넘어갑니다. 이때 반드시 PENABLE 신호를 1로 띄워야 합니다. (이게 APB 스펙의 핵심입니다!)
그리고 또 1 클럭을 쉰 다음, 슬레이브가 PREADY 신호를 1로 줄 때까지 무한정 대기(while 루프)합니다. 슬레이브가 바빠서 PREADY를 0으로 잡고 있으면, 마스터(우리의 드라이버)는 핀 상태를 그대로 유지한 채 클럭만 보내며 얌전히 기다려야 합니다.
// End of transfer: De-assert all signals
vif.psel <= 0;
vif.penable <= 0;
// ... (clear other signals) ...
슬레이브가 데이터를 잘 받았다고 PREADY를 띄우면, 띄워놨던 PSEL, PENABLE 등 모든 핀을 다시 0으로 깔끔하게 바닥으로 내립니다(De-assert).
`ifndef CFS_ALGN_TEST_REG_ACCESS_SV
`define CFS_ALGN_TEST_REG_ACCESS_SV
class cfs_algn_test_reg_access extends cfs_algn_test_base;
`uvm_component_utils(cfs_algn_test_reg_access)
function new(string name = "", uvm_component parent);
super.new(name, parent);
endfunction
virtual task run_phase(uvm_phase phase);
phase.raise_objection(this, "TEST_DONE");
#(100ns);
fork
begin
cfs_apb_sequence_simple seq_simple = cfs_apb_sequence_simple::type_id::create("seq_simple");
void'(seq_simple.randomize() with {
item.addr == 'h0; // Address = 0 (Hex)
item.dir == CFS_APB_WRITE; // Direction = WRITE
item.data == 'h0011; // Data = 0x0011 (Hex)
});
seq_simple.start(env.apb_agent.sequencer);
//[대본_객체].start([타겟_조감독_경로]);
// play 버튼
end
begin
cfs_apb_sequence_rw seq_rw = cfs_apb_sequence_rw::type_id::create("seq_rw");
void'(seq_rw.randomize() with {
addr == 'hC; // Address = 0xC (Hex)
});
seq_rw.start(env.apb_agent.sequencer);
end
begin
cfs_apb_sequence_random seq_random = cfs_apb_sequence_random::type_id::create("seq_random");
void'(seq_random.randomize() with {
num_items == 3;
});
seq_random.start(env.apb_agent.sequencer);
end
join
`uvm_info("DEBUG", "this is the end of the test", UVM_LOW)
phase.drop_objection(this, "TEST_DONE");
endtask
endclass
`endif
드디어 UVM 피라미드의 맨 꼭대기, 모든 대본(Sequence)을 총괄하고 시뮬레이션의 시작과 끝을 통제하는 '최고 지휘관(Test)' 파일에 도달하셨습니다!
지금까지 우리가 하나하나 뜯어보았던 simple, rw, random 대본 3형제가 이 테스트 벤치 안에서 어떻게 불려 나와 활약하는지 완벽하게 보여주는 마스터피스입니다.
// 1. Keep the simulation alive!
phase.raise_objection(this, "TEST_DONE");
// ... (Do all the testing) ...
// 2. We are done, you can stop the simulation now!
phase.drop_objection(this, "TEST_DONE");
만약 이 코드가 없으면 UVM 시뮬레이션은 0초 만에 아무 일도 안 하고 바로 종료(Finish)되어 버립니다.
raise_objection: 지휘관이 UVM 본부에 "잠깐! 나 지금부터 칩 테스트할 거니까 절대로 시뮬레이션 끄지 마!"라고 타이머 정지를 요청하는 겁니다.
drop_objection: 모든 테스트가 끝난 후, "나 이제 다 했어! 시뮬레이션 종료해도 좋아!"라고 허락해 주는 완벽한 생명 주기 관리 기법입니다.
fork
begin
// Thread 1: Start 'simple' sequence
end
begin
// Thread 2: Start 'rw' sequence
end
begin
// Thread 3: Start 'random' sequence
end
join
일반적인 begin ... end는 코드를 위에서부터 차례대로(순차적으로) 실행하지만, fork ... join은 안에 있는 모든 블록(Thread)을 시뮬레이션 시간으로 '정확히 똑같은 시간(동시에)' 출발시킵니다.
왜 굳이 동시에 쏠까요? (면접 단골 질문!): 3개의 대본이 하나의 조감독(Sequencer)에게 동시에 택배 상자를 던지면, 조감독은 병목 현상(Bottleneck)을 겪게 됩니다. 이때 조감독이 어떤 순서로 상자를 드라이버에게 넘겨줄지 결정하는 '교통정리(Arbitration)' 능력을 극한으로 테스트하기 위해 일부러 트래픽을 한꺼번에 쏟아붓는 실무 테크닉입니다.
아까 우리가 대본들을 분석할 때, "밖에서 지휘관이 값을 세팅해 줄 거다"라고 했던 것 기억하시죠? 바로 여기서 그 마법이 일어납니다!
// Override the variables inside the sequence BEFORE starting it!
void'(seq_random.randomize() with {
num_items == 3;
});
대본 파일(seq_random) 안에는 기본적으로 num_items가 1~10번 돌게끔 soft 제약이 걸려 있었습니다.
하지만 최고 지휘관이 여기서 with { num_items == 3; }이라고 강제 명령(Inline Constraint)을 내리면, 원래 있던 soft 규칙은 가볍게 무시되고 무조건 3번만 루프를 돌게 됩니다.
이렇게 하면 대본 코드는 단 한 줄도 수정하지 않고, 테스트 파일만 여러 개 복붙해서 "이번 테스트는 100번 쏴!", "이번 테스트는 주소 0번지에만 쏴!" 하면서 수십 가지의 테스트 시나리오를 아주 쉽게 만들어낼 수 있습니다.
엔지니어님, 방금 이 코드를 이해하심으로써 UVM의 데이터를 쏘는 과정(Stimulus Generation) 전체를 완벽하게 정복하셨습니다!