거북이처럼 천천히

Verilog RTL 설계(7월 23일 - 2, DHT11 구현 (1) ) 본문

RTL Design/Verilog RTL 설계

Verilog RTL 설계(7월 23일 - 2, DHT11 구현 (1) )

유로 청년 2024. 8. 10. 21:33

1. State diagram of dht11 Sensor Module

  • MCU와 DHT 11 모듈 간에 통신 과정을 State diagram으로 그린다면 다음과 같이 표현할 수 있다.

State diagram of Communication between DHT11 and MCU

 

 

 

 

2. DHT11로부터 온도, 습도 데이터 받아 Basys3의 FND로 출력

 

Verilog RTL 설계(7월 23일 - 1, DHT 기초)

1. DHT11 (Digital Humidity Temperature 11)이번에는 온도와 습도를 측정하는 디지털 센서, DHT11을 이용하여 온도와 습도 센서를 측정해보도록 하겠다.이를 통해 FSM(Finite State Machine) 구현와 통신 방식에 대

jbhdeve.tistory.com

 

  • DHT11로부터 온도, 습도 데이터를 받아 Basys3의 FND로 출력하는 설계 과정은 소스 코드와 함께 설명하도록 하겠다.

 

2.1. State 값 정의

  • 위 DHT11과 Basys3 간의 통신 과정을 크게 아래와 같이 6개의 state로 나누었다.
    - S_IDLE : 3초 대기 상태
    - S_LOW_20MS : Basys3의 Start Signal (Low Level, 20ms)
    - S_HIGH_20US : Basys3의 Start Signal (Impedance Level, 20us)
    - S_DHT_LOW : DHT11의 Response Signal (Low Level, Positive edge 때까지)
    - S_DHT_HIGH : DHT11의 Response Signal (High Level, Negative edge 때까지)
    - S_DHT_DATA : DHT11로부터 온,습도 데이터 받기.


  • S_DHT_DATA 상태내에서 사용할 Sub-State 값을 정의하였다.
    - S_WAIT_P_EDGE : Positive edge를 기다리는 상태
    - S_WAIT_N_EDGE : Negative edge를 기다리는 상태
    - Q) 왜 counter를 통해 시간을 Counting하지 않고, Edge를 detect하려고 하는가?
      A)
    DataSheet처럼 counter를 이용하여 시간을 Counting하여 데이터 값을 읽어도 되지만, DHT11은 저렴한
          센서이기 때문에 부정확을 고려하여 가장 확실한 Edge 감지를 통해 데이터를 읽도록 설계하였다.

 

 

2.2. Input / Output 변수 선언

module dth11_cntr(
    input clk, reset_p,
    inout dht11_data,
    output reg [7:0] humidity, temperature,
    output [5:0] led_debug);
  
endmodule
  • Q) dht11_data 를 양방향 포트를 나타내는 키워드로 작성한 이유는 뭔가?
  • A) DHT11과 MCU 사이는 양방향 단일선으로 연결되어 있다. 이 선을 통해 DHT11과 MCU간에 통신 및 데이터 전송이 이루어 진다. 하지만, 양 방향에서 동시에 데이터 전송을 할 수 없기 때문에 한 쪽에서 데이터를 전송한다면 다른 한 쪽에서는 임피던스 상태로 전환하여 읽기 모드로 들어 가야 한다.

  • Q) 온도와 습도 데이터는 정수부와 소수부 포함해서 각각 8bit와 8bit, 총 16bit인데, 왜 8bit만 선언했는가?
  • A) dht11은 저렴한 센서이기 때문에 소수부의 정확성이 떨어질 것으로 판단하여 상대적으로 정확할 것으로 생각하는 정수부 데이터만 받기 위해 정수부 데이터 크기인 8bit 크기로 선언하였다.
  • Q) led_state 는 어떤 역활을 수행하는가?
    A) DHT11과 MCU 간에 통신을 총 6개의 상태로 나누었다. led_state는 현재 어떤 상태에 있는지 여부를 나타내며, 만약 통신 과정에서, 컴파일 과정에서 문제 발생하면 LED를 통해 "현재 어느 상태에서 정체되어 있는가?" 여부를 즉각적으로 확인할 수 있다.

 

 

2.3. Main state와 Sub state를 Parameter로 정의

// Declare state parameter.
parameter S_IDLE = 6'b00_0001;
parameter S_LOW_20MS = 6'b00_0010;
parameter S_HIGH_20US = 6'b00_0100;
parameter S_DHT_LOW = 6'b00_1000;
parameter S_DHT_HIGH = 6'b01_0000;
parameter S_DHT_DATA = 6'b10_0000;
    
// Declare sub state parameter.
parameter S_WAIT_P_EDGE = 2'b01;
parameter S_WAIT_N_EDGE = 2'b10;
  • State diagram에서 설정하였던 state에 맞게 parameter를 각각 선언하였다.

 

 

 

2.4.  state와 next_state, sub_state를 저장할 Register 변수 선언 

// Declare state, next state parameter.
reg [5:0] state, next_state;
reg [1:0] sub_state;

 

 

 

2.5. 언제 다음 State로 넘어가는가?

 // 언제 다음 state으로 이동하는가?
always @(negedge clk or posedge reset_p) begin
    if(reset_p) state = S_IDLE; 
    else state = next_state;
end
  • clock pulse가 negative edge일 때, state에서 next_state 로 넘어가게 된다.
  • ★ ★ ★ ★ ★ ★
    여기서 이점을 꼭 기억하자.
    - 궁금하지 않은가? 왜? clock 신호가 Negative edge일때, 다음 state로 넘어가는지를 궁금하지 않은가?
    - Positive edge일 때, next_state로 넘어가도 되지 않는가?
    - 이부분에 대해서 나중에 통합하여 설명하도록 할테니, 이점을 기억하고 넘어가자.

 

 

 

2.6. 1usec 마다 Counting하는 Counter 만들기

// LED for checking current state.
assign led_debug = state;
    
// Making usecond counter
wire clk_usec;
clock_div_100 clock_usec (.clk(clk), .reset_p(reset_p), .clk_div_100_nedge(clk_usec));
    
// Making usec counter.
reg [21:0] counter_usec;
reg counter_usec_enable;
   
always @(negedge clk or posedge reset_p) begin
    if(reset_p) counter_usec = 0;
    else if(counter_usec_enable && clk_usec) counter_usec = counter_usec + 1;
    else if(!counter_usec_enable) counter_usec = 0;
end
  • Q) 왜 usec counter를 만드는가? 무슨 목적으로 사용하려고, usec counter를 만드는가?
  • A) data sheet를 보게 되면 20ms를 기다리거나, 20us를 기다려야 하는 상황이 필요하다는 것을 알 수 있다. 이처럼 dht11_data 와이어를 통해 일정한 시간동안 데이터를 전송해야 하는 상황을 위해 Counter를 이용하며, counting을 통해 원하는 시간동안 대기하거나 데이터를 전송할 수 있도록 설계할 수 있다.
     따라서 usec counter는 원하는 시간, usec 단위 시간동안 대기하기 위한 목적으로 사용할 것이며, 이를 위해 usec counter를 만들었다.
  • counter_usec_enable 변수는 usec counter를 컨트롤 할 목적으로 사용하는 변수이다.
    - counter_usec_enable = 0 : usec_counter 값을 0으로 초기화하면 disable 상태가 된다.
    - counter_usec_enable = 1 : usec_counter 값을 enable 상태이며, 1씩 counting하기 시작한다.

  • Q) 왜 usec counter의 크기를 22bit로 설정하였는가?
  • A) clk_usec 변수 값을 통해 1usec마다 Counting하기 때문에 22bit counter가 counting할 수 있는 값은 2^22 * 1usec = 4,194,304 [usec] = 4,194.304 [msec] = 4.194304[sec] 까지 Counting이 가능하다. 이번 dht11 구현에서 S_IDLE 상태에서 3초를 기다린 후, 데이터를 받을 것이기 때문에 S_IDLE에서 3초 동안 대기 하기 위해 counter는 22bit 크기를 갖는다.

  • Q) 그럼, 왜 S_IDLE 상태에서 3초를 대기하는가?
  • A) 3초를 대기 하지 않는다면, dht11과 Basys3 간에 통신은 최대 25ms 주기를 갖고, 온도 습도 데이터를 전송한다. 이는 현재 온도, 습도를 확인하는 데 있어 굉장히 빠르기 때문에 S_IDLE에서 3초 대기함으로서 dht11에서 보낸 현재 온도, 습도 데이터를 확인하기 위함이라고 할 수 있다.

 

 

 

2.7. dht11_data의 edge detect를 감지하여 이를 One Cycle Pulse로 표현하도록 설계

 // Get One Cycle Pulse of dht11_data.
wire dht11_data_p_edge, dht11_data_n_edge;
edge_detector edge_detector_0(.clk(clk), .reset_p(reset_p), .cp(dht11_data), .n_edge(dht11_data_n_edge), .p_edge(dht11_data_p_edge));
  • 양방향 단선인 dht11_data의 edge detector를 통해 감지한  One Cycle Pulse를 변수에 저장하기.
  • Q) 무슨 목적으로 dht11_data의 edge를 검출하고자 하는가?
  • A) dht11은 저렴한 센서이기 때문에 counter를 통해 정확한 시간을 counting하는 것은 어렵다. 따라서 dht11_data 변수를 통해 dht11과 Basys3 간에 데이터 신호의 변화를 감지함으로서 상대적으로 정확하게 데이터를 읽을 수 있기 때문에 이를 위해 edge detector를 통해 edge를 검출하였다.

 

 

 

 

★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★

2.8.  dht11_data 단자를 통해 데이터를 전달하기 위해 임시 버퍼 선언

// Making Buffer of dht11_data
// dht11_data 는 inout 자료형이기 때문에 reg 선언 불가
// 따라서 임시 버퍼 생성후, 임시 버퍼를 통해 dht11_data에 데이터 전달
reg buffer_of_dht11_data;
assign dht11_data = buffer_of_dht11_data;
  • Q) 왜 어째서 직접적으로 assign 키워드를 통해 데이터를 전달하지 않고, 임시 버퍼를 통해 데이터를 전달하는가?
  • A) inout 자료형을 선언한 dht11_data는 dht11와 Basys3 간에 데이터를 주고, 받고 하도록 해주는 wire 자료형을 갖는 변수이다. 

    하지만, always 문내에서 각 상태 단계에 따라 0값을 주거나 Impedance 값을 주거나 만들어야 하는 상황이 있다. 따이를 위해서는 Register 자료형으로 선언하여 상황에 따라 적절한 값을 줄 필요가 있는데, dht11_data 변수를 wire 자료형인 동시에 register 자료형으로 선언해 줄 수 없다.

    따라서 register 자료형으로 임시 버퍼, buffer_of_dht11_data를 선언하여 dht11_data 변수는 기본적으로 wire 자료형을 갖지만, always문내에서 state에 따라 0값과 impedance 값을 주기 위해 임시버퍼인 buffer_of_dht11_data을 통해 데이터를 basys3에서 dht11로 전달하도록 설계한 것이다.

 

 

 

 

★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★

2.9.  dht11로 부터 받은 데이터를 임시 저장하기 위해 레지스터 변수 선언

// dht11로부터 받은 데이터를 레지스터에 임시저장
// dht11로부터 데이터를 받는 과정에서 데이터 손실이 발생할 수 있기 때문에 
// 40bit 데이터를 다 받은 후, check sum 과 비교하여 문제가 없을 경우에만
// FND로 출력
reg [39:0] temp_reg;
reg [5:0] data_count;
  • Q) 왜 어째서 dht11로 부터 받은 데이터를 바로 출력하지 않고, 임시 저장하는가?
  • A) dht11로 부터 온도, 습도 데이터를 받는 과정에서 예기치 못한 상황으로 데이터가 손실되어 전송되기 전과 전송되어 basys3 측에서 받은 데이터가 다른 경우가 발생할 수 있다. 이를 대비하여 데이터를 받은 후, 임시적으로 데이터를 저장한 뒤, 8bit 크기의 check sum 데이터와 비교하여 "데이터 전송간에 데이터 손실이 발생하는가?" 여부를 확인하여 데이터 손실이 없는 경우에만 FND로 출력하기 위해 임시적으로 데이터를 저장하였다.
  • data_count 변수는 현재까지 받은 온도, 습도 데이터의 갯수를 count하기 위한 목적으로 사용한다.
  • dht11로부터 받는 데이터는 총 40bit크기이며, 온도, 습도, check sum 데이터로 구성되어 있다.

 

 

 

 

2.10. 각각의 변수 값 초기화

// 각 state에서의 동작 및 언제 다음 state으로 넘어가는가?
always @(posedge clk or posedge reset_p) begin
    if(reset_p) begin
       sub_state = S_WAIT_P_EDGE;
       next_state = S_IDLE;
       temp_reg = 0;
       data_count = 0;
       counter_usec_enable = 0;
end else begin
		''' (생략) '''
	end
end
  • 설계에 필요한 변수들을 위와 같이 초기화 작업을 수행하도록 설계한다.
  • 각 변수의 역활은 다음과 같다.
    - sub_state : S_DHT_DATA 상태에서 사용되는 sub state 변수
    - next_state : Main state에서 다음 상태를 저장하는 변수
    - temp_reg : dht11로부터 받은 데이터를 임시저장하는 레지스터
    - data_count : dht11로부터 받은 데이터를 counting하는 변수
    - counter_usec_enable : usec counter를 컨트롤하는 변수

 

 

 

2.11.  1단계) S_IDLE 상태 

// 1단계) 3초 대기
S_IDLE : begin
       if(counter_usec < 22'd3_000_000) begin
           counter_usec_enable = 1;
           buffer_of_dht11_data = 'bz;
       end
       else begin
          counter_usec_enable = 0;
          buffer_of_dht11_data = 'b0;
          next_state = S_LOW_20MS;
       end
end
  • S_IDLE 단계는 DHT11와 basys3간에 데이터를 주고 받기 전, 잠시 대기 상태를 가지는 단계이며, 해당 상태에서 3초 동안 대기하게 된다.
  • 3초 동안 대기하기 위해서 이전에 만들었던 usec_counter를 통해 3000000 usec 까지 counter하기 전까지 대기하게 된다. 이때, dht11_data 와이어에 대해서는 임피던스 상태로 만들어 줘야한다.
  • usec counter를 통해 3초 동안 대기를 끝내면 다음 상태인 S_LOW_20MS로 넘어가게 된다.

 

 

2.12.  2단계) S_LOW_20MS 상태 

// 2단계) MCU Start Signal 20ms
S_LOW_20MS : begin
    if (counter_usec < 22'd20_000) begin
        counter_usec_enable = 1;
        buffer_of_dht11_data = 'b0;
        next_state = S_LOW_20MS;
    end
    else begin
        counter_usec_enable = 0;
        buffer_of_dht11_data = 'bz;
        next_state = S_HIGH_20US;
    end
end
  • S_LOW_20MS 상태, S_HIGH_20US 상태를 통해 MCU 측에서 DHT11 측으로 통신 연결이 잘되었는지 여부를 체크하는 Start Signal를 보내게 된다.
  • 이를 위해서는 data sheet에 맞게 최소 18ms 동안 buffer_of_dht11_data를 통해 dht11_data 와이어로 Low Level Signal을 보내야 하며, 이를 위해 usec counter를 활용하여 20ms 동안 대기 하였다.
  • 20ms 동안 Low Level Signal을 보낸 후, DHT11로부터 응답 신호를 받기 위해 dht11_data를 임피던스 상태로 만들어 준다.

 

 

 

2.13.  3단계) S_HIGH_20US  상태 

// 3단계) MCU Start Signal 20ms
S_HIGH_20US : begin
    if (counter_usec < 22'd20) begin
        counter_usec_enable = 1;
        buffer_of_dht11_data = 'bz;
        next_state = S_HIGH_20US;
    end
    else begin
        counter_usec_enable = 0;
        next_state = S_DHT_LOW;
    end
end
  • S_HIGH_20US 상태는 MCU 측에서 DHT11측으로 Start signal을 보낸 뒤, 이에 대한 Response Signal을 받기 위해 MCU 측에서 dht11_data 와이어를 임피던스 상태로 만든 상태이다.
  • 위에서 설명했듯이 양방향에서 동시에 데이터를 보낼 수 없기 때문에 한 쪽에서 데이터를 전송하기 위해서는 다른 한쪽에서는 임피던스 상태를 가질 필요가 있다.
  • usec counter를 통해 20us 동안 대기 한뒤, 다음 상태인 S_DHT_LOW 상태로 넘어가게 된다.

 

 

 

2.14. 4, 5단계) S_DHT_LOW, S_DHT_HIGH 상태

// 4단계) DHT Response signal
S_DHT_LOW :
    if (dht11_data_p_edge) next_state = S_DHT_HIGH;

// 5단계) DHT Response signal
S_DHT_HIGH :
    if (dht11_data_n_edge) next_state = S_DHT_DATA;
  • 해당 단계에서 부터는 DHT11 측에서 응답 신호를 보내게 된다. 따라서 MCU측에서는 임피던스 상태를 유지하며, 대기하면 된다.
  • Q) data sheet를 보게 되면 80us 동안 대기하라고 했는데, 왜 counter를 통해 대기 하지 않고, dht11_data의  edge를 이용하여 단계를 넘어가려고 하는가?
    A) 위에서도 설명했듯이 DHT11는 저렴한 소자이기 때문에 Timing 측면에서 부정확하다. 따라서 counter를 통해 80usec 동안 대기하는 것보다 dht11_data의 edge를 통해 감지하는 것이 상대적으로 정확하기 때문이다.

    dht11_data의 signal에서 positive edge가 검출 되면 다음 상태인 S_DHT_HIGH로 전이되며, dht11_data의 signal에서 negative edge가 검출되면 다음 상태인 S_DHT_DATA로 전이 된다.

 

 

 

2.15. 6단계) S_DHT_DATA 상태

// 6단계) Read data
S_DHT_DATA : begin
    case (sub_state)
        S_WAIT_P_EDGE : 
            if (dht11_data_p_edge) begin
                counter_usec_enable = 1;                              
                sub_state = S_WAIT_N_EDGE;
            end
        
        S_WAIT_N_EDGE : begin
            if (dht11_data_n_edge) begin
                if (counter_usec < 45)
                    temp_reg = {temp_reg[38:0], 1'b0};
                else
                    temp_reg = {temp_reg[38:0], 1'b1};
                
                counter_usec_enable = 0;
                data_count = data_count + 1;
                sub_state = S_WAIT_P_EDGE;
            end
        end   
    endcase
end
  • S_DHT_DATA 상태에서는 DHT11 측으로부터 40bit 온,습도 데이터를 받게 된다.
  • 이 과정에서도 dht11의 부정확성으로 인한 잘못된 데이터를 받는 것을 막기 위해 edge를 통해서 상태를 전이하게 된다.

    ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★
  • Q) 데이터를 0이냐? 1이냐? 구분하는 과정에서 전압 레벨이 High 인 상태를 지속하는 시간을 counter를 counting하지 않고, 45usec를 기준으로 작으면 0, 크면 1로 구분하는 이유는 무엇인가?
  • A) 이 또한 DHT11의 부정확성으로 인한 잘못된 데이터를 받는 것을 막기 위해 45usec 보다 작으면 0, 크면 1로 인식하기 위함이다. 이를 통해 counter로 구체적으로 counting하여 26~28usec 인지, 70usec인지 확인할 필요없이 상대적으로 정확하게 데이터를 읽을 수 있다.

  • Q) 왜 데이터를 임시적으로 temp_reg 레지스터에 저장하는가?
  • A) 이는 dht11로 부터 온도, 습도 데이터를 받는 과정에서 예기치 못한 상황이 발생하여 데이터가 손실되어 전송되기 전과 전송되어 basys3 측에서 받은 데이터가 다른 경우가 발생할 수 있기 때문에 이를 대비하여 데이터를 받은 후, 임시적으로 데이터를 저장한 뒤, 8bit 크기의 check sum 데이터와 비교하여 "데이터 전송간에 데이터 손실이 발생하는가?" 여부를 확인하여 데이터 손실이 없는 경우에만 FND로 출력하기 위해 임시적으로 데이터를 저장하였다.

 

 

 

 

 

2.12. CheckSum 데이터를 통해 데이터 손실이 발생했는지 여부 확인 

if(data_count >= 40) begin
        data_count = 0;
        next_state = S_IDLE;
        counter_usec_enable = 0;
        sub_state = S_WAIT_P_EDGE;
                        
        if(temp_reg[39:32] + temp_reg[31:24] + temp_reg[23:16] + temp_reg[15:8] == temp_reg[7:0]) begin
              humidity = temp_reg[39:32];
              temperature = temp_reg[23:16];
        end
end
  • MCU 측에서 40bit 데이터를 다 받으면 온도, 습도 데이터 (정수부, 소수부)를 모두 더한다.
    temp_reg[39:32] = 습도 정수부
    temp_reg[31:24] = 습도 소수부
    temp_reg[23:16] = 온도 정수부
    temp_reg[15:8] =  온도 소수부
  • check sum과 비교하여 동일하면 "전송간에 데이터 손실이 없다." 라고 판단하여 FND로 출력하게 된다.
  • 반대로 일치하지 않을 경우 "전송간에 데이터 손실이 발생했다." 라고 판단하여 출력으로 내보내지 않고, 다음 온도, 습도 데이터를 받게 된다.