레지스터와 combinational logic의 조합으로 구성된 이 회로를 RTL, Register Transfer level이라고 합니다.
이름 (반드시 앞에 백틱 기호를 붙여야 함) `define WORD_SIZE 16
wire [`WORD_SIZE-1:0] data;
// -> 전처리기가 wire [16-1:0] data; 로 바꿔치기 한 뒤 컴파일 시작
[예시: NPU 명령어 정의]
// defines.v 파일에 모아두고 사용
`define OP_CONV 4'b0001 // 컨볼루션
`define OP_POOL 4'b0010 // 풀링
`define OP_RELU 4'b0011 // 활성화 함수
// Decoder 모듈
if (instruction == `OP_CONV) begin end
ifdef, else, `endif)사용자가 질문한 메모에 있던 내용입니다.
"상황에 따라 코드를 넣었다 뺐다" 할 때 사용합니다.
주로 시뮬레이션(검증)과 합성(실제 칩) 코드를 분리할 때 씁니다.
[예시: 디버깅 로그 출력]
`define SIMULATION_MODE // 이 줄을 주석 처리하면 아래 코드는 사라짐
always @(posedge clk) begin
`ifdef SIMULATION_MODE
// 시뮬레이션 때만 화면에 값을 찍어보고 싶음
// 실제 칩(합성)에서는 $display가 들어가면 에러가 나거나 무시됨
$display("Current Value: %d", data);
`endif
end
| 구분 | `define (매크로) | parameter (파라미터) |
|---|---|---|
| 적용 범위 | Global (전역) 한 파일에서 정의하면 프로젝트 내 다른 파일에서도 보임 | Local (지역) 해당 모듈 {} 안에서만 유효함 |
| 처리 시점 | 컴파일 전 (Pre-process) 단순 텍스트 치환 | 컴파일/합성 중 상수 값으로 해석됨 |
| 변경 가능성 | 불가능 (고정된 값) | 가능 (Overridable) 모듈을 불러올 때(Instance) 값을 바꿀 수 있음 |
| 주 용도 | OpCode, FSM 상태 값, 조건부 컴파일 | 모듈의 비트 폭(Width), FIFO 깊이 등 모듈별 설정 |
[예시: 주소 계산 간소화]
// 행(row)과 열(col)로 메모리 주소 계산하는 공식 매크로
`define GET_ADDR(row, col) ((row * 32) + col)
assign memory_addr = `GET_ADDR(current_y, current_x);
// -> 전처리기가 assign memory_addr = ((current_y * 32) + current_x); 로 치환함
parameter <name> = <constant>;
localparam <name> = <constant>; (모듈내부에서만 사용)
parameter WIDTH = 8;
wire [WIDTH-1:0] data;
[예시: Conv 레이어의 출력 채널 계산]
module ConvLayer #(
parameter IN_CH = 32,
parameter FILTER = 64
) ( ... );
// 사용자가 건드리면 안 되고, 파라미터에 의해 자동 계산되는 값
localparam TOTAL_WEIGHTS = IN_CH * FILTER;
wire [TOTAL_WEIGHTS-1:0] weight_bus;
endmodule
AI 가속기는 동일한 연산 장치(PE)가 수십~수천 개 반복되는 구조(Systolic Array 등)이므로 직접 코드를 100줄 쓰는 게 아니라 generate loop를 써야 합니다.
[예시: 16개의 PE를 일렬로 배치하기]
genvar i; // generate용 루프 변수 선언
generate
for (i = 0; i < 16; i = i + 1) begin : PE_ARRAY_LOOP
// 각 PE에 고유한 이름을 부여하며 인스턴스화
// i=0일 때, pe_inst[0] 생성
PE_Unit #(.ID(i)) pe_inst (
.clk(clk),
.in_data(data_bus[i]),
.out_data(result_bus[i])
);
end
endgenerate
[예시]
// [Vector] 128비트짜리 긴 데이터 (예: 128-bit SIMD 데이터)
wire [127:0] wide_bus;
// [Array] 8비트 데이터가 1024개 있는 메모리 (예: 1KB SRAM)
reg [7:0] sram_buffer [0:1023];
// 접근 차이:
// wide_bus[7:0] -> 하위 8비트 조각 (Slicing 가능)
// sram_buffer[0] -> 0번 주소의 8비트 값 전체 (Slicing 불가, 한 단어씩 접근)
: 대괄호가 변수 이름 '앞'에 위치 (예: wire [7:0] data;)
[개념]
[예시]
reg [7:0] pixel; // 8비트짜리 픽셀 값 하나 (0~255)
// [특징] 수학 연산이 가능함
pixel = pixel + 1; // 8비트 전체가 하나의 숫자로 인식되어 1이 더해짐
: 대괄호가 변수 이름 '뒤'에 위치 (예: reg data [0:7];)
[개념]
[예시]
reg [7:0] pixel_val; // (벡터) 8비트짜리 값
reg my_mem [0:255]; // (어레이) 1비트짜리 저장소 256칸
// [특징] 덩어리 연산 불가능
// my_mem = 0; <- (에러!) 사물함 256개를 한 번에 0으로 만들 수 없음
// my_mem[0] = 0; <- (정상) 0번 칸에 접근해서 값을 넣어야 함
AI 칩에서는 "8비트(벡터) 데이터를 1024개(어레이) 저장하고 싶다"는 상황이 대부분입니다.
이때 두 문법을 섞어서 사용합니다.
reg [7:0] buffer [0:1023];
// ^^^^^ ^^^^^^^
// (벡터) (어레이)
// 데이터 크기 저장 공간 개수(Depth)
[해석]
[접근 방법 예시]
wire [7:0] data_out;
assign data_out = buffer[5]; // 5번 주소의 8비트 값을 꺼내옴 (가능)
// assign data_out = buffer; // 에러! 게시판 통째로 전선에 연결 못 함
Verilog에서는 변수(variable)로 비트 범위를 자르는 것(예: data[i:j])이 불가능합니다.
이때 사용하는 것이 Indexed Part Select 문법입니다.
[예시: 32비트 버스에서 가변적인 위치의 8비트(1바이트)만 뽑아내기]
reg [31:0] data_bus;
reg [1:0] byte_sel; // 0, 1, 2, 3 선택
wire [7:0] selected_byte;
// 틀린 표현 (Error): data_bus[byte_sel*8 + 7 : byte_sel*8] -> 범위에 변수 사용 불가
// 올바른 표현 (Indexed Part Select)
// "byte_sel*8 위치에서 시작해서, 윗방향(+)으로 8비트 가져와라"
assign selected_byte = data_bus[byte_sel*8 +: 8];
// 동작:
// byte_sel = 0 -> data_bus[7:0]
// byte_sel = 1 -> data_bus[15:8]
// byte_sel = 2 -> data_bus[23:16] ...