HDLBits Procedures편

강정윤·2024년 9월 23일

안녕하세요 오늘은 HDLBits의 Procedures편을 다룰까합니다.
여기서는 Always block과 if-else, case state문에 대한 내용입니다.

자 그럼 시작해보도록 합시다.

Always blocks(combinational)

문제에 앞서서 always block에 대한 간략한 설명이 있군요.

Combinational logic : always@(*)
Clocked logic : always@(posedge clk)

이런식으로 Combinational logic을 작성하거나 sequential logic을 작성할 수도 있습니다.

여기서 Combinational logic을 작성하기 위해 사용하는 always@(*) block은 assgin statements를 사용해서 작성한 것과 동일하다고 합니다.

Always block을 잘 사용해서 HDL을 작성하면 Combinational logic을 작성할 때, 많이 편리합니다.
하나하나 assign하는 것 보다 마치 C언어와 유사하게 작성할 수 있다는 것이 매력이겠지요.

하지만 always block 내부에서는 continuous assignments 중 하나인 assign을 사용할 수 없습니다. 만일 사용하면 Syntax error를 만나게 될겁니다.
물론 assign과 비슷한 continuous assignments 중에서 always block 내부에서 사용할 수 있는 녀석들도 있습니다만, 이건 합성 불가능하기 때문에 H/W를 작성할 때는 단순하게 always block 내부에는 assign과 같은 continuous assignments는 사용하지 않도록 합니다.

ex1)
always@(*) 
	a = b | c & d;
ex2)
always@(a,b,c,d)
	a = b | c & d;

위의 두 예시는 동일한 combinational logic을 작성한 것입니다.

*가 무엇이냐 자동 감지 리스트입니다.
sensitivity list를 자동으로 생성하는 역할은 하는데,
always block 내부에서 사용된 모든 신호를 감지하고, 해당 신호들이 변경될 때마다 블록 내부가 동작할 수 있도록 해줍니다.

저 같은 경우 ex1)만 사용하는 것 같습니다. 초기에 Verilog를 배울 때는 ex2)처럼 작성했던 때도 있었긴 한데, 점차 ex1)만 사용하게 된 것 같습니다.
(내부 combinational logic을 이루는 wire가 많으면 작성하기도 귀찮고, 놓치면 아마 error가 발생했던 것으로 ....)

네 그렇습니다 위의 2가지 선택지 중에서 편하신 걸로 사용하시면 됩니다.

이제 always block을 만난 사람들이 정말로 헷갈려 하는 부분이 왔습니다.
always block을 소개하다보면 필수적으로 설명해야하는 reg변수입니다.

reg는 regsiter의 앞의 3글자만 때온 거 같지요?

네, 그 점이 바로 헷갈리게 하는 point입니다.

always block내부에서 left-hand side에 오는 녀석의 type은 무조건 reg이어야 합니다.

흑흑 갑자기 always@(*)로는 combinational logic을 작성할 때 사용한다 했는데, 왜 갑자기 reg냐....

헷갈릴 수 있습니다. 하지만 명확하게 알도록 합시다.

물론 sequential logic을 작성할 때, always@(posedge clk)를 사용할 때는 내부에 있는 reg type 변수들은 filp-flop(register)로 합성됩니다.

자 말이 너무 길었습니다.

always@(*) 내부에 쓰이는 reg type변수는 flip-flop(register)가 아니다!!!
always@(posedge clk) 내부에 쓰이는 reg type변수가 진짜 flip-flop(register)이다!!

always block소개는 여기까지 하도록 하고, 문제로 들어가봅시다.
문제에서는 AND gate를 assign statements를 사용해서 작성해보기도 하고, always block을 사용해서 작성해보라고 합니다.
(연습하는 입장이니까 그렇게 하라고 합니다...ㅋㅋㅋ 저도 동의합니다)

그림1 : always block(combinational logic)
module top_module(
    input a, 
    input b,
    output wire out_assign,
    output reg out_alwaysblock
);
    
    assign out_assign = a&b;
    
    always@(*) begin
       out_alwaysblock = a&b; 
        
    end

endmodule

어떤가요?
이번은 단순하게 AND gate를 작성해보는 것이라 많이 쉬웠을 겁니다.
그럼 다음 문제로 가봅시다

Always blocks(clocked)

이번에는 sequential logic을 always block을 사용해서 작성해보라는 문제입니다.
제가 위에 combination logic을 설명할 때, 이미 거의 설명해버렸군요...

  1. Continuous assignments (assign x = y & z;)와 같은 녀석들은 always block내부에서 사용할 수 없다!!
  1. Procedural blocking assignment (x = y & z;)는 always block내부에서 사용할 수 있다.
  1. Procedural non-blocking assignment(x <= y & z;)는 always block내부에서 사용할 수 있다.

네, 하지만 아직 제가 언급하지 않은 것이 있습니다.
위에서 바로 뭔가 처음 들어보는 것이 있지요?
그것은 바로..

Blocking vs Non-Blocking Assignment

combinational logic을 작성할 때는 blocking assignment를 사용해야 합니다.
sequential logic을 작성할 때는 non-blocking assignment를 사용해야 합니다.

그 이유를 이름으로부터 간단하게 유추할 수 있긴한데, 그래도 정확하게 설명하자면

blocking statement는 작성한 코드를 순서대로 실행하게 됩니다.
이것이 combinational logic에서 signal이 logic gate를 순서대로 통과하면서 출력을 만들어 내는 것과 동일한 결과를 만들어 줍니다.

예시로 들어보겠습니다.

always@(*) begin
	a = b;
    c = a + d;
end

위의 예시에서 먼저 a에 b가 할당되고, 나중에 c에 b+d결과가 할당되는데 이는 a+d가 할당되겠지요?

line이 순서대로 실행하는 logic을 생성하게 됩니다.

non-blocking statement는 작성한 코드를 순서대로 실행하는 것이 아닌,
clock신호에 동기화되어 모든 line들이 동시에 실행됩니다.

이게 무슨말이야 할 수 있습니다.
간단한 예시를 보시지요

always@(posedge clk)begin
	a <= b;
    c = a + d ;
end

위의 코드와 거의 동일해보입니다만, 다릅니다...

자 더 구체적인 예시를 들면서 설명해보도록 하겠습니다.

always@(*)로 작성된 circuit에 b = 1, d = 10을 입력으로 주면
출력인 a,c는 즉각적으로 a=1, c=11로 튀어나옵니다.

하지만 always@(posedge clk)로 작성된 것은 위에서도 언급했듯이 그 내부에 있는 left-hand side에 있는 변수들은 진짜로 register입니다.
이 녀석은 적어도 1clock cycle동안 값을 저장하는 기능을 합니다.
그렇다면 입력을 바꾸기 전에 a와 c는 각각 값을 가지고 있겠지요?
그 값을 예를 들어 a = 10, c = 15라고 생각해봅시다.
이게 입력으로 b = 3, d = 7을 넣었다하면,
그 결과가 어떻게 될까요??

  1. a = 3, c = 10
  2. a = 3, c = 17

둘 중 어떤 것이 정답일까요???
정답부터 말씀드리자면 2번입니다.
???????
잠깐만.... 왜 alway@(*) block에서 생각했던데로 안되는거냐... 할 수 있습니다.
네 regitser는 clock에 연동되어 동작합니다. 그래서 a에 b가 할당되고 그 a가 c에 들어갈 값으로 쓰이는 것이 아니라, 이전에 있던 녀석이 쓰입니다...
왜냐 register라는 기억장치니까요..

이 부분이 매우 헷갈리는 point입니다만, sequential logic에서 flip-flop부분을 제대로 이해하시고 오시는 것이 필요합니다.

Procedures편에 들어오니까 문제보다 문제에 나오는 녀석들을 설명하다보니 내용이 길어지는 느낌이 있습니다... 그 만큼 정확하게 아는 것이 중요한 파트입니다.

자 문제로 들어가봅시다!

그림2 : always block(sequential logic)

문제에서는 XOR gate를 작성해보라는데, 3번째의 경우 그 결과를 flip-flop의 입력으로 이어지도록 해라라는 의미입니다.

// synthesis verilog_input_version verilog_2001
module top_module(
    input clk,
    input a,
    input b,
    output wire out_assign,
    output reg out_always_comb,
    output reg out_always_ff   );
	
    assign out_assign = a^b;
    always@(*)begin
       out_always_comb = a^b; 
    end
    
    always@(posedge clk)begin
    	out_always_ff = a^b;
    end
    
endmodule

이번에는 waveform을 한번 짚고 넘어가도록 합시다.

flip-flop의 출력은 combinational logic과 달리 1cycle 뒤에 나왔지요?
flip-flop은 clock에 동기화되어 작동하기때문에 그 값이 clock의 edge에 반영됩니다.
따라서 combinational logic은 즉각적으로 결과가 나오지만, sequential logic은 clock edge가 되어야 그 값이 반영됩니다.

이 때 set-up time, hold time이라는 개념이 있는데, 이건 나중에 flip-flop에 대해 심도있게 다룰 때, timing과 함께 포스팅하도록 하겠습니다.

If statement

이번에는 if statement를 사용하는 파트입니다.
if문으로 2-to-1 mux를 만들 수 있습니다.

if(condition)
condition에 해당하는 곳에 참일 경우 if문을 수행하도록 하는 어떤 logic을 작성해주시면 됩니다.

자 문제로 바로 들어가보도록 합시다.

그림3 : If statement
// synthesis verilog_input_version verilog_2001
module top_module(
    input a,
    input b,
    input sel_b1,
    input sel_b2,
    output wire out_assign,
    output reg out_always   ); 
    
    assign out_assign = (sel_b1 & sel_b2) ? b : a;
    
    always@(*) begin
        if(sel_b1&sel_b2) begin
        	out_always = b;
        end
        else begin
            out_always = a;
        end
    end
    
endmodule

어때요 간단한가요?

If statement latches

자 이번에는 if statement를 사용할 때, 주의할 점입니다.
바로 combinational logic을 작성하기 위해 always block을 사용했지만...
latch가 합성되는 경우를 피해야 합니다.
latch에 대해 잠깐 다른 포스트에서 언급한 적이 있는데, latch를 만들면 작성한 circuit에서 오작동을 일으킬 가능성이 매우매우 큽니다.
또한 latch를 의도적으로 만드는 경우도 드물답니다.

자 이번 문제를 풀기 전에 HDLBits에서는 주의할 점으로 combinational logic을 작성할 때, 모든 경우에 대해 명확하게 작성하라고 합니다.

만약 if문으로만 작성했고 else문을 따로 사용하지 않았다..면
latch가 생길 가능성이 큽니다.

명확하게 작성하지 않은 조건이 입력으로 들어온 경우 Verilog는 출력값을 변경하지 않고 유지하려고 합니다.

이 말은 내가 if문에서 어떤 동작을 작성하고, 아닌 경우에는 그 반대로 동작하길 바라지만, 그렇지 않다는 겁니다.

따라서 각 경우에 대해 명확하게 작성하는 것이 필요하고, 그럴 수 없다면 else문으로 default 동작을 정의해주는 것이 필요합니다.

그림4 : If statements latches

이 예시는 computer가 overheat되었다면 shut off computer를 하고 아닌 경우에는 shut off를 하면 안될 것입니다.
하지만 그렇지 않은 경우 대해 따로 작성하지 않았으므로 이 회로를 합성할 때, 출력값을 유지하도록 합성되고, latch가 합성됩니다.

이렇게 오동작을 하는 회로가 합성되어서 문제가 생긴다면 힘들겠지요...
따라서 else 구문을 사용해서 아닌 경우를 명시해주는 것이 중요합니다.

자 문제로 들어가봅시다
문제에서는 이런 bug를 고치라고 합니다

// synthesis verilog_input_version verilog_2001
module top_module (
    input      cpu_overheated,
    output reg shut_off_computer,
    input      arrived,
    input      gas_tank_empty,
    output reg keep_driving  ); //

    always @(*) begin
        if (cpu_overheated)
           shut_off_computer = 1;
        else shut_off_computer = 0;
    end

    always @(*) begin
        if (~arrived)
           keep_driving = ~gas_tank_empty;
        else
            keep_driving = 0;
    end

endmodule

원래 있는 code에서 else부분을 작성해주면 통과됩니다.

Case statement

이제 Case statement가 나왔습니다.
case statement를 이용해서 combinational logic을 작성할 수 있습니다.
if-else를 너무 많이 사용하다보면 코드가 약간 장풍(?)을 쏘는 듯한 모습을 보일 수 있습니다...
(현실에서 못 쓴다고해서 여기서 쓰면 안됩니다...)

출처 : https://adrianalonso.es/desarrollo-web/apis/trabajando-con-promises-pagination-promise-chain/

네 물론 사진 속 코드는 Verilog는 아니지만, if-else를 너무 남발하면 이런 모양의 코드로 나올 수 있습니다.
나중에 Debugging할 때, 상당히 헷갈리게 하는 요인 중 하나이죠.
Verilog는 다른 프로그래밍 언어와 달리 동시성이 있습니다.
이 말은 지금 작성하는 코드 말고도 다른 부분도 고려해야한다는 것인데,
다른 프로그래밍언어는 코드가 위에서 아래로 순차적으로 실행되는 흐름으로 가지요..
Verilog는 그렇지 않아서 안 그래도 Debugging할 때, 생각할 것이 많은데 장풍 코드를 만들면 더 힘들어 질 겁니다.

그래서 Case statement를 적절하게 사용해주면 코드 가독성이 좋아집니다.
(이 말은 곧 Debugging할 때 조금 덜 헷갈릴 수 있겠지요?)

자 서론이 길었습니다.
case문도 if-else와 비슷합니다.
그럼 문제를 풀면서 차차 확인해보도록 합시다.

그림5 : Case statement

이번 문제에서는 6-to-1 mux를 작성해봐라 라고 합니다.

module top_module ( 
    input [2:0] sel, 
    input [3:0] data0,
    input [3:0] data1,
    input [3:0] data2,
    input [3:0] data3,
    input [3:0] data4,
    input [3:0] data5,
    output reg [3:0] out   );//

    always@(*) begin  // This is a combinational circuit
        case(sel)
            3'd0 : out = data0;
            3'd1 : out = data1;
            3'd2 : out = data2;
            3'd3 : out = data3;
            3'd4 : out = data4;
            3'd5 : out = data5;
            default : out = 0;
        endcase
    end

endmodule

이걸 if statement로 작성하고 있었으면 code가 조금 그렇겠지요?
자 그럼 다음 문제로 가보도록 합시다.

Priority encoder

이번은 우선순위가 있는 encoder를 작성하는 문제입니다.
LSB부터 쓱 보면서 먼저 1이 나오는 위치를 출력으로 보내면 됩니다.

// synthesis verilog_input_version verilog_2001
module top_module (
    input [3:0] in,
    output reg [1:0] pos  );
    
    always@(*)begin
        casex(in)
            4'bxxx1 : pos = 2'd0;
            4'bxx10 : pos = 2'd1;
            4'bx100 : pos = 2'd2;
            4'b1000 : pos = 2'd3;
			default : pos = 0;
        endcase
    end

endmodule

저 같은 경우에는 casex를 사용해서 don't care를 이용했습니다.
don't care를 사용하지 않고 하나하나 전부 작성하면

module top_module (
	input [3:0] in,
	output reg [1:0] pos
);

	always @(*) begin			// Combinational always block
		case (in)
			4'h0: pos = 2'h0;	// I like hexadecimal because it saves typing.
			4'h1: pos = 2'h0;
			4'h2: pos = 2'h1;
			4'h3: pos = 2'h0;
			4'h4: pos = 2'h2;
			4'h5: pos = 2'h0;
			4'h6: pos = 2'h1;
			4'h7: pos = 2'h0;
			4'h8: pos = 2'h3;
			4'h9: pos = 2'h0;
			4'ha: pos = 2'h1;
			4'hb: pos = 2'h0;
			4'hc: pos = 2'h2;
			4'hd: pos = 2'h0;
			4'he: pos = 2'h1;
			4'hf: pos = 2'h0;
			default: pos = 2'b0;	// Default case is not strictly necessary because all 16 combinations are covered.
		endcase
	end
	
	// There is an easier way to code this. See the next problem (always_casez).
	
endmodule

이런식으로 코드가 길어지겠지요...

자 갑자기 case를 하는 중에 casex가 뭐냐 할 수 있습니다.

casex는 x,z bit를 don't care처리 해주는 녀석입니다.
만약 case에서 x bit을 사용하면 의도한대로 회로가 합성 되지 않을 것입니다.

이와 비슷하게 casez라는 녀석이 있는데, casez는 z bit는 don't care처리하지만, x bit는 비교를 합니다. 즉 2'b01이냐 2'bx1이냐 를 따진다는 것이지요.

자 그럼 다음 문제로 가보도록 합시다.

Priority enconder with casez

네 위에서 말하자마자 바로 casez가 나오는군요...
HDLBits에서 정말로 세심하게 하나하나 해보라는 의미인 것 같습니다.

자 casez에서 don't care를 사용하고 싶으면 z bit을 사용하면 됩니다. 그리고 ?을 대신 사용해도 됩니다.

module top_module (
    input [7:0] in,
    output reg [2:0] pos );
    
    always@(*) begin
        casez(in)
            8'b????_???1 : pos = 3'd0;
            8'b????_??10 : pos = 3'd1;
            8'b????_?100 : pos = 3'd2;
            8'b????_1000 : pos = 3'd3;
            8'b???1_0000 : pos = 3'd4;
            8'b??10_0000 : pos = 3'd5;
            8'b?100_0000 : pos = 3'd6;
            8'b1000_0000 : pos = 3'd7;
            default : pos = 0;
                
            
        endcase
        
    end

endmodule

어떤가요? 8bit encoder를 작성하는데 9줄만 작성하면 됩니다.
8bit encoder라 해서 256가지 경우의 수에 대해 작성할 필요가 없습니다.

Avoiding latches

이번은 Procedures편의 마지막인 latch생성을 피하는 부분을 다루는 문제입니다.
제가 개인적으로 매우 중요하다고 생각하는 부분인데요,

때로는 case문에서 default를 작성해준다고 완벽하게 latch를 피할 수 있는 것은 아닙니다.
갑자기 ??이런 의문이 들 것입니다....
case문에서 default를 사용하는 이유는 case문에서 다루지 못한 입력에 대한 처리입니다.
출력에 대해서는 처리해주는 녀석이 아니라는 것이죠...

그래서 case문이든 if문이든 출력에 대한 기본값을 always@(*) 블럭 초기에 작성해주는 것이 latch를 생성하는 것을 피하게 해주는데,
그렇지 않으면 매 case마다 전체 출력에 대한 부분을 전부 하나하나 작성해야합니다.

아래 예시를 보면서 이해를 하도록 합니다.

always @(*) begin
    up = 1'b0; down = 1'b0; left = 1'b0; right = 1'b0;

    case (direction)
        2'b00: up = 1'b1;
        2'b01: down = 1'b1;
        2'b10: left = 1'b1;
        2'b11: right = 1'b1;
        default: begin
            // 정의되지 않은 입력 값일 때 모든 출력을 비활성화
            up = 1'b0;
            down = 1'b0;
            left = 1'b0;
            right = 1'b0;
        end
    endcase
end

위의 code block에서는 사실 default를 작성할 필요가 없어보입니다만, case구문에서 x,z와 같이 unknown, high-z와 같은 신호가 회로의 오동작으로 입력으로 들어올 수 있습니다. 따라서 그런 상황에서 회로가 정상적으로 동작하기 위해서는 default동작을 항상 기술해주는 것이 중요합니다.

그렇다면 출력에 대해서 한번 생각해봅시다. combinational logic에 대해서는 입력에 대한 출력을 정확하게 기술해줘야 합니다.
출력에 대해 매 case마다 작성한다면 아래와 될 것입니다.

always @(*) begin
    case (direction)
        2'b00: begin
            up = 1'b1;
            down = 1'b0;
            left = 1'b0;
            right = 1'b0;
        end
        2'b01: begin
            up = 1'b0;
            down = 1'b1;
            left = 1'b0;
            right = 1'b0;
        end
        2'b10: begin
            up = 1'b0;
            down = 1'b0;
            left = 1'b1;
            right = 1'b0;
        end
        2'b11: begin
            up = 1'b0;
            down = 1'b0;
            left = 1'b0;
            right = 1'b1;
        end
    endcase
end

endmodule

이런식으로 매 case마다 출력을 모두 정의해줘야 합니다.
귀찮죠
그래서 이럴 때 하는 것이 출력에 대한 초기값 혹은 기본값을 작성해주는 것입니다.

따라서 오늘 procedures부분에서 제가 강조하는 것은 latch를 만들지 말자인데,

latch를 만들지 않기 위해서는

  1. 입력에 대한 모든 조건이 기술되었는가?
    그렇지 않다면 default동작을 기술해두었는가? (case문의 경우)
  1. 출력에 대한 기본값을 작성해두었는가?
    그렇지 않다면 매 case마다 명확하게 모든 출력에 대해 작성해주었는가?

3.if-else문에서 else문을 작성하였는가?
빠진 경우는 없는가?

4.자신 없으면 assign statement를 사용해라...

생각보다 combinational logic을 always block을 이용해서 작성하는 것이 쉽지 않다는 생각이 듭니다...
하지만 assign statement를 사용해서 작성하는 것 또한 쉽지 않아보입니다.
따라서 우리는 combinational logic을 작성하는 순간에는 정말로 집중해야할 것입니다.

자 이번편의 마지막 문제를 풀어보도록 합시다.

그림6 : Avoiding latches

이번 문제는 키보드의 입력을 처리하는 circuit을 작성하는 문제입니다.

module top_module (
    input [15:0] scancode,
    output reg left,
    output reg down,
    output reg right,
    output reg up  ); 
    
    always@(*) begin
       left = 1'b0; down = 1'b0; right = 1'b0; up = 1'b0;
        
        case(scancode)
            16'he06b : left = 1'b1;
            16'he072 : down = 1'b1;
            16'he074 : right = 1'b1;
            16'he075 : up = 1'b1;
            default : begin left = 1'b0; down = 1'b0; right = 1'b0; up = 1'b0; end
        endcase
        
    end

endmodule

어떤가요 이번에 latch를 만들지 않고 잘 작성하셨나요?
오늘 procedures편은 상당히 내용도 길었고, 설명도 많았습니다.
또한 질문도 많이 생길 것이라 생각합니다.
혹시나 오늘 이 내용을 보면서 이해가 안되는 분들은 댓글을 남겨주시면 답변을 드리겠습니다.
감사합니다.

profile
muscle brain

0개의 댓글