Verilog Syntax for AI HW

Seungyun Lee·2026년 1월 25일

AI HW Paper

목록 보기
4/14

레지스터와 combinational logic의 조합으로 구성된 이 회로를 RTL, Register Transfer level이라고 합니다.

1. Define

1. 핵심 개념: "단순 텍스트 치환"

  • 컴파일러가 회로를 해석하기 전에, 지정된 문자를 값으로 '갈아끼우기'만 합니다.
  • 문법: `define 이름 값
  • 사용: 이름 (반드시 앞에 백틱 기호를 붙여야 함)
   `define WORD_SIZE 16
   
   wire [`WORD_SIZE-1:0] data; 
   // -> 전처리기가 wire [16-1:0] data; 로 바꿔치기 한 뒤 컴파일 시작

2. AI HW 설계에서의 주요 용도

(1) 프로젝트 전체 공통 상수 (Global Constant)

  • NPU(신경망 처리 장치)를 설계할 때, 명령어(OpCode)나 상태(State) 값은 모든 모듈에서 똑같이 알아야 합니다.
  • 이때 parameter를 쓰면 모듈마다 일일이 전달해야 해서 귀찮지만, define은 프로젝트 전체에 적용됩니다.

[예시: 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

(2) 조건부 컴파일 (ifdef, else, `endif)

  • 사용자가 질문한 메모에 있던 내용입니다.

  • "상황에 따라 코드를 넣었다 뺐다" 할 때 사용합니다.

  • 주로 시뮬레이션(검증)합성(실제 칩) 코드를 분리할 때 씁니다.

    [예시: 디버깅 로그 출력]

      `define SIMULATION_MODE // 이 줄을 주석 처리하면 아래 코드는 사라짐
          
          always @(posedge clk) begin
              `ifdef SIMULATION_MODE
                  // 시뮬레이션 때만 화면에 값을 찍어보고 싶음
                  // 실제 칩(합성)에서는 $display가 들어가면 에러가 나거나 무시됨
                  $display("Current Value: %d", data);
              `endif
          end

3. define vs parameter (면접 단골 질문 ★)

구분`define (매크로)parameter (파라미터)
적용 범위Global (전역)
한 파일에서 정의하면 프로젝트 내 다른 파일에서도 보임
Local (지역)
해당 모듈 {} 안에서만 유효함
처리 시점컴파일 전 (Pre-process)
단순 텍스트 치환
컴파일/합성 중
상수 값으로 해석됨
변경 가능성불가능 (고정된 값)가능 (Overridable)
모듈을 불러올 때(Instance) 값을 바꿀 수 있음
주 용도OpCode, FSM 상태 값, 조건부 컴파일모듈의 비트 폭(Width), FIFO 깊이 등 모듈별 설정

4. 매크로 함수 (Macros with Arguments)

  • `define은 단순 값뿐만 아니라, 간단한 '함수'처럼 동작하게 만들 수도 있습니다.
  • 복잡한 수식을 줄여서 가독성을 높일 때 씁니다.

[예시: 주소 계산 간소화]

   // 행(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); 로 치환함

2. parameter

parameter <name> = <constant>;
localparam <name> = <constant>; (모듈내부에서만 사용)

parameter WIDTH = 8;
wire [WIDTH-1:0] data;
  • parameter: 외부에서 값을 변경(Override)할 수 있음 (인스턴스화 할 때)
  • localparam: 외부에서 변경 불가능, 내부에서 계산된 상수에 사용

[예시: Conv 레이어의 출력 채널 계산]

module ConvLayer #(
    parameter IN_CH  = 32,
    parameter FILTER = 64
) ( ... );
    // 사용자가 건드리면 안 되고, 파라미터에 의해 자동 계산되는 값
    localparam TOTAL_WEIGHTS = IN_CH * FILTER; 
    
    wire [TOTAL_WEIGHTS-1:0] weight_bus;
endmodule

3. generate

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

4. vector/array

[예시]
  // [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 불가, 한 단어씩 접근)

벡터 (Vector) = "Packed Array"

: 대괄호가 변수 이름 '앞'에 위치 (예: wire [7:0] data;)

[개념]

  • "하나의 거대한 숫자 덩어리"입니다.
  • 물리적으로는 '전선(Wire) 다발'이나 '하나의 레지스터'로 취급됩니다.
  • 한 번에 연산(더하기, 빼기)이 가능합니다.
[예시]
reg [7:0] pixel; // 8비트짜리 픽셀 값 하나 (0~255)

// [특징] 수학 연산이 가능함
pixel = pixel + 1; // 8비트 전체가 하나의 숫자로 인식되어 1이 더해짐

어레이 (Array) = "Unpacked Array"

: 대괄호가 변수 이름 '뒤'에 위치 (예: reg data [0:7];)

[개념]

  • "개별적인 보관함의 연속(메모리)"입니다.
  • 물리적으로는 SRAM이나 레지스터 파일(Register File)처럼 주소(Address)를 가지고 접근해야 하는 저장 공간입니다.
  • ★중요: 어레이 전체를 한 번에 더하거나 복사할 수 없습니다. (한 칸씩 꺼내야 함)
[예시]
reg [7:0] pixel_val;     // (벡터) 8비트짜리 값
reg       my_mem [0:255]; // (어레이) 1비트짜리 저장소 256칸

// [특징] 덩어리 연산 불가능
// my_mem = 0;      <- (에러!) 사물함 256개를 한 번에 0으로 만들 수 없음
// my_mem[0] = 0;   <- (정상) 0번 칸에 접근해서 값을 넣어야 함

실전 AI HW 설계: "어레이 안에 벡터 넣기"

AI 칩에서는 "8비트(벡터) 데이터를 1024개(어레이) 저장하고 싶다"는 상황이 대부분입니다.
이때 두 문법을 섞어서 사용합니다.

reg [7:0] buffer [0:1023]; 
//  ^^^^^        ^^^^^^^
//  (벡터)       (어레이)
//  데이터 크기   저장 공간 개수(Depth)

[해석]

  • "8비트짜리 포스트잇(벡터)을 붙일 수 있는 1024칸짜리 게시판(어레이)"
  • buffer[0] 이라고 부르면 -> 0번 칸에 있는 '8비트 데이터 전체'가 나옴.
[접근 방법 예시]
wire [7:0] data_out;
assign data_out = buffer[5]; // 5번 주소의 8비트 값을 꺼내옴 (가능)
// assign data_out = buffer; // 에러! 게시판 통째로 전선에 연결 못 함

5. indexed part select

Verilog에서는 변수(variable)로 비트 범위를 자르는 것(예: data[i:j])이 불가능합니다.
이때 사용하는 것이 Indexed Part Select 문법입니다.

  • 문법: [시작비트 +: 비트폭] 또는 [시작비트 -: 비트폭]
  • 용도: MUX(멀티플렉서)나 시프트 레지스터, 가변 길이 데이터 처리에 필수.
 [예시: 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] ...
profile
Design Verification engineer

0개의 댓글