약 14분 소요
개요
환경
- Target HW: Basys3
- Development Tools: Vivado
FND Controller 설계 개요
Basys3 보드에는 FND가 내장되어 있다.

FND는 결국 LED를 제어하는 것이기 때문에 LED가 어떤 조건에서, 어떻게 켜지는지를 이해해야 한다.
FND 동작 원리
Basys3 Schematic에서 FND 회로는 다음과 같이 나온다.

우측에 pnp 트랜지스터가 4개 보이고, Emitter 쪽에 VCC 3.3V가 공통으로 물려있는 것을 확인할 수 있다.
위 회로에서는 Emitter에 3.3V가 걸려있기 때문에 Base 쪽인 AN 단자에 Low가 걸려야 Tr이 동작함을 확인할 수 있다
Tr이 동작하게 되면 A단자로 VCC 3.3V가 걸리게 되고, 각 LED에 해당하는 단자가 Low가 걸리면 LED 양단의 전압차가 발생하여 LED가 켜지는 원리이다.
기본적으로 이 방식을 Common Anode 방식이라고 부른다.
FND에서 7-segment는 총 4개로 구성되어 있고,
하나의 7-segment에는 총 8개의 LED가 들어가게 된다.
각 LED의 위치는 다음과 같다.

예를 들어서 3이라는 숫자를 만들기 위해서는 a,b,c,d,g를 LOW로 떨어트려주고, 나머지 LED는 HIGH를 인가해줘야 함을 의미한다.
따라서 이 자리수의 모든 LED를 제어하려면 총 8개의 핀이 필요함을 알 수 있다.
각 자리수마다 8개의 핀씩 필요하다고 가정하면 4자리 모두에 대해서 각개로 제어해주려면 총 32개의 핀이 필요하겠지만,
사람의 눈에는 잔상효과라는 게 있어서, 각 자리수를 선택하여 LED 제어를 해주고 아주 빠르게 다음 자리수로 스위칭하는 방식으로 제어하게 되면
핀 수도 12개로 줄어들고, 전력 소모도 효율적이게 된다.
그래서 위 모듈은 잔상효과를 이용하게 설계되었고,
자리수 선택 핀 4개와 LED 핀 8개 총 12개의 핀만 사용하면 4자리의 7-Segment를 제어할 수 있게 된다.
잔상효과는 사람의 눈에 60Hz 이상으로 LED가 깜빡이면 연속적으로 켜져 보이는 현상을 의미한다.
GPT에게 물어보니 실무에서는 일반적으로 100Hz~1kHz로 잡는다고 한다.
이번 설계에서는 100Hz를 기준으로 잡도록 하였다.
12.34 라는 데이터를 표시하려고 하면 다음과 같이 순차적으로 데이터를 입력시켜 줘야 한다.

2.5ms가 지나서 tick이 하나 발생하면 1의 자리수에는 4가 표시되어야 한다. 나머지 자리수는 Off되어야 한다.
이는 fnd 모듈 입력으로 CA~DP는 10011001이 들어와야 하고, an은 1110이 들어와야 함을 의미한다.

2.5ms가 지나서 tick이 하나 발생하면 10의 자리수에는 3이 표시되어야 한다. 나머지 자리수는 Off되어야 한다.
이는 fnd 모듈 입력으로 CA~DP는 00001101이 들어와야 하고, an은 1101이 들어와야 함을 의미한다.

2.5ms가 지나서 tick이 하나 발생하면 100의 자리수에는 2와 dp가 표시되어야 한다. 나머지 자리수는 Off되어야 한다.
이는 fnd 모듈 입력으로 CA~DP는 00100100이 들어와야 하고, an은 1011이 들어와야 함을 의미한다.

2.5ms가 지나서 tick이 하나 발생하면 1000의 자리수에는 1이 표시되어야 한다. 나머지 자리수는 Off되어야 한다.
이는 fnd 모듈 입력으로 CA~DP는 10011111이 들어와야 하고, an은 0111이 들어와야 함을 의미한다.
FND Controller 설계
동작 정의 및 모듈 정의
위의 FND 동작을 바탕으로 FND 컨트롤러의 동작을 1차원적으로 생각해보자면 다음과 같을 것이다.
- 각 자리수마다 100Hz로 깜빡여야 하고,
- 각 자리수별로 순서대로 스위칭을 해가면서
- 각 자리수의 데이터에 해당하는 값을 각 자리에 LED로 표시하여야 한다.
FND Controller를 IP로 설계하려 한다면 이 FND Controller의 책임은 어디까지일까?
- 0~9999까지의 숫자 데이터가 입력으로 들어오면 FND 컨트롤러가 알아서 자리수와 데이터를 선택해가면서 각 LED를 ON/OFF시켜줘야 한다.
- DP 표시 입력이 들어오면 그 자리수에 해당하는 DP가 켜져야 한다.
- 선택된 an 신호에는 의존성이 있지만 0~9999의 숫자 데이터에는 의존성이 없어야 하고 별개로 DP가 컨트롤 될 수 있어야 한다.
이 관점에서 FND 동작을 다시 자세하게 정리해보면 다음과 같을 것이다.
- 400Hz의 tick을 생성(100Hz x 4자리)하고
- 0~9999까지의 숫자 데이터를 입력받아 변환하고
- tick이 발생할 때마다(2.5ms마다) 표시할 자리수를 순서대로 선택하고
- 그 자리수의 숫자에 해당하는 값에 해당하는 led의 코드값을 출력
- DP 입력이 Active Low로 4비트가 들어오면 그에 해당하는 DP LED를 밝힌다. (켜고 끄는 것은 소프트웨어에서 제어)
데이터의 관점에서 생각해보자.
이런 방식으로 설계한다면 다음의 모듈들이 필요하게 된다.
- 잔상효과를 주기 위해 필요한 400Hz Tick Generator – tick_generator
- 자리수를 나눠주는 모듈 (1234 -> 1, 2, 3, 4) – bin2bcd
- 여기서 1234는 14비트에 해당하는 binary Data이고, 1, 2, 3, 4로 분리된 각각의 데이터는 0~9까지만 해당하는 BCD 데이터이다.
- 자리수를 순차적으로 선택해주기 위한 2비트 카운터 – digit_selector
- 자리수 선택 값이 들어왔을 때 그에 해당하는 bcd 값을 선택 – mux_4x1
- 숫자를 LED 패턴으로 출력해주는 모듈 (1->10011001, 4->10011111) – decoder_bcd2led
- 자리수를 선택된 값이 들어왔을 때 그에 해당하는 an 값을 출력하는 모듈 – decoder_digit2an
- DP 입력이 0으로 들어오면 해당 DP를 켜는 모듈 – decoder_bcd2led를 수정
일단 DP를 제외하고 설계를 해보면 블록 다이어그램은 다음과 같이 그릴 수 있다.

이제 DP 신호를 추가해보자.
FND Controller 입장에서는 모든 자리의 DP를 컨트롤 할 수 있게 밖으로 빼주어야 한다.
따라서 DP 신호는 4비트로 입력받으면 될 것이다.
또한, DP 신호도 MUX 값을 받아야 어떤 자리에서 작업중인지를 판단할 수 있게 되고, 그에 맞는 dp 값을 select할 수 있게 된다.
이까지의 다이어그램을 그리면 다음과 같다.

DP 신호는 직관적으로 1을 주면 DP를 표시하고, 0을 주면 DP를 표시하지 않는 방식으로 설계하기 위해 Not Gate를 달았다.
(common anode 타입이기 때문에 0을 줘야 LED가 켜지게 되므로 Not gate를 거쳐줘야 맞다.)
소스 코드 (fnd_controller.v)
`timescale 1ns / 1ps
module fnd_controller (
input wire clk,
input wire reset_n,
input wire [13:0] in_data,
input wire [ 3:0] dp,
output wire [ 3:0] digit_sel_an,
output wire [ 7:0] led_segment
);
wire [7:0] w_led_segment;
wire [3:0] bcd_digit1, bcd_digit10, bcd_digit100, bcd_digit1000, bcd_digit, w_digit_sel_an;
wire [1:0] w_digit_sel;
wire tick_400Hz, dp_mux_out;
assign digit_sel_an = w_digit_sel_an;
assign led_segment = w_led_segment;
tick_generator #(
.FREQUENCY(400)
) U_TICK_GEN_400Hz (
.clk (clk),
.reset_n(reset_n),
.o_tick (tick_400Hz)
);
bin2bcd U_BIN2BCD (
.binary (in_data),
.bcd_digit1 (bcd_digit1),
.bcd_digit10 (bcd_digit10),
.bcd_digit100 (bcd_digit100),
.bcd_digit1000(bcd_digit1000)
);
digit_counter U_DIGIT_COUNTER (
.clk (clk),
.reset_n(reset_n),
.i_tick (tick_400Hz),
.count (w_digit_sel)
);
decoder_digit2an U_DECODER_DIGIT2AN (
.digit(w_digit_sel),
.an (w_digit_sel_an)
);
mux_4x1 #(
.WIDTH(4)
) U_BCD_MUX_4X1 (
.sel(w_digit_sel),
.x0 (bcd_digit1000),
.x1 (bcd_digit100),
.x2 (bcd_digit10),
.x3 (bcd_digit1),
.y (bcd_digit)
);
mux_4x1 #(
.WIDTH(1)
) U_DP_MUX_4X1 (
.sel(w_digit_sel),
.x0 (dp[3]),
.x1 (dp[2]),
.x2 (dp[1]),
.x3 (dp[0]),
.y (dp_mux_out)
);
decoder_bcd2led U_DECODER_BCD2LED (
.bcd_digit(bcd_digit),
.dp(~dp_mux_out),
.led_segment(w_led_segment)
);
endmodule
module tick_generator #(
parameter FREQUENCY = 400
) (
input wire clk,
input wire reset_n,
output reg o_tick
);
reg [$clog2(100_000_000/FREQUENCY)-1:0] count;
always @(posedge clk) begin
if (!reset_n) begin
count <= 0;
o_tick <= 0;
end else begin
if (count == (100_000_000 / FREQUENCY) - 1) begin
count <= 0;
o_tick <= 1;
end else begin
count <= count + 1;
o_tick <= 0;
end
end
end
endmodule
module bin2bcd (
input wire [$clog2(9999)-1:0] binary,
output reg [ 3:0] bcd_digit1,
output reg [ 3:0] bcd_digit10,
output reg [ 3:0] bcd_digit100,
output reg [ 3:0] bcd_digit1000
);
always @(*) begin
bcd_digit1 = binary % 10;
bcd_digit10 = (binary / 10) % 10;
bcd_digit100 = (binary / 100) % 10;
bcd_digit1000 = (binary / 1000) % 10;
end
endmodule
module digit_counter (
input wire clk,
input wire reset_n,
input wire i_tick,
output reg [1:0] count
);
always @(posedge clk) begin
if (!reset_n) begin
count <= 0;
end else begin
if (i_tick) begin
count <= count + 1;
end
end
end
endmodule
module mux_4x1 #(
parameter WIDTH = 4
) (
input wire [1:0] sel,
input wire [WIDTH-1:0] x0,
input wire [WIDTH-1:0] x1,
input wire [WIDTH-1:0] x2,
input wire [WIDTH-1:0] x3,
output reg [WIDTH-1:0] y
);
always @(*) begin
y = 0;
case (sel)
2'd0: y = x0;
2'd1: y = x1;
2'd2: y = x2;
2'd3: y = x3;
default: y = 0;
endcase
end
endmodule
module decoder_bcd2led (
input wire [3:0] bcd_digit,
input wire dp,
output reg [7:0] led_segment
);
always @(*) begin
led_segment = 8'b11111111;
case (bcd_digit)
4'h0: led_segment = {dp, 7'b1000000};
4'h1: led_segment = {dp, 7'b1111001};
4'h2: led_segment = {dp, 7'b0100100};
4'h3: led_segment = {dp, 7'b0110000};
4'h4: led_segment = {dp, 7'b0011001};
4'h5: led_segment = {dp, 7'b0010010};
4'h6: led_segment = {dp, 7'b0000010};
4'h7: led_segment = {dp, 7'b1111000};
4'h8: led_segment = {dp, 7'b0000000};
4'h9: led_segment = {dp, 7'b0010000};
default: led_segment = 8'b11111111;
endcase
end
endmodule
module decoder_digit2an (
input wire [1:0] digit,
output reg [3:0] an
);
always @(*) begin
an = 4'b1111;
case (digit)
2'b00: an = 4'b1110;
2'b01: an = 4'b1101;
2'b10: an = 4'b1011;
2'b11: an = 4'b0111;
default: an = 4'b1111;
endcase
end
endmodule
테스트벤치
소스 코드 (tb_fnd_controller.sv)
`timescale 1ns / 1ps
module tb_fnd_controller ();
logic clk;
logic reset_n;
logic [13:0] in_data;
logic [ 3:0] dp;
logic [ 3:0] digit_sel_an;
logic [ 7:0] led_segment;
fnd_controller U_DUT (
.clk (clk),
.reset_n (reset_n),
.in_data (in_data),
.dp (dp),
.digit_sel_an(digit_sel_an),
.led_segment (led_segment)
);
always #5 clk = ~clk;
initial begin
clk = 0;
reset_n = 0;
#10;
reset_n = 1;
in_data = 14'd1234;
dp = 4'b0001;
#10000000;
in_data = 14'd5678;
dp = 4'b0010;
#10000000;
in_data = 14'd9090;
dp = 4'b0100;
#10000000;
in_data = 14'd0000;
dp = 4'b1000;
#10000000;
in_data = 14'd9999;
dp = 4'b0000;
#10000000;
in_data = 14'd10123;
dp = 4'b1111;
#10000000;
$finish;
end
endmodule
시뮬레이션 결과 분석

첫번째 사이클에서 1234.가 표시되도록 출력되고,
두번째 사이클에서 567.8이 표시되도록 출력되고,
세번째 사이클에서 90.90가 표시되도록 출력되고,
네번째 사이클에서 0.000가 표시되도록 출력되고,
5번째 사이클에서 9999가 표시되도록 출력되고,
6번째 사이클에서 0.1.2.3.가 표시되도록 출력되는 것을 확인
동작
XDC 파일 설정
- 스위치 0~13 -> 각 숫자의 자리수
- 스위치 15번 -> reset(Active Low)
- 버튼 4개 -> DP 입력