任务描述
- 根据拨动开关的选择,将正在显示的R、G、B中一种颜色值显示在数码管上,两个数码管可以显示从0-FF之间的数值
- 使用小脚丫FPGA上的拨动开关SW1~SW3(对应红、绿、蓝三色)选择要在数码管上要显示其值,以及轻触开关变化要控制的颜色
- 使用小脚丫FPGA上的轻触开关K1和K2(分别对应加、减亮度操作),来控制拨动开关SW1到SW3选择好的颜色,进行颜色值的调节
- 在PC上设计一个简单的界面(可以使用任何一种编程语言或工具),能够单独控制R、G、B的三个颜色值从0到255,可以通过鼠标(电脑上)、和通过UART传递过来的控制按键来调节R、G、B的值
- 调节好的值通过UART传递到小脚丫FPGA上,控制一颗三色LED的状态
需求分析
该项目涉及到的外设较多、功能需求繁杂。这要求设计者具有工程化设计理念、规范化的设计流程。故需要先对项目解耦,拆分出相应模块逐步实现(这点可以借助chatgpt等工具提供思路):
- 串口通信模块:负责与电脑进行串口通信,接收来自电脑的控制指令。
- RGB LED PWM控制模块:包含逻辑,将指令转换为相应的 RGB LED 的PWM控制信号。
- 数码管字符编码模块:接收来自由编码开关选择读取模块的数值,做相应转换以控制两位数码管显示对应的 RGB 通道值。
- 交互逻辑模块:接收来自串口模块、按键模块的信号,解析动作指令并传递至LED控制模块和数码管显示模块以执行操作。
实现方式
将相应需求输入chatGPT,为模块拆分、和具体代码写作思路:
获得最初代码并复制到WebIDE中。再根据逻辑需要和仿真波形进行调整修改。
使用了小脚丫在线WebIDE开发。完整工程项目请见:https://www.stepfpga.com/project/10137
功能框图
硬件FPGA部分功能框图
上位机部分流程图
代码及说明
FPGA部分
PWM控制部分
使用count寄存器计数,比较count与duty_cycle大小得到占空比可变的PWM信号
// led pwm control
module pwm_control(
input wire clk, // system clock
input wire rst_n,
input wire [7:0] duty_cycle, // light intensity
output wire pwm_out // PWM output
);
reg [7:0] count; // counter
assign pwm_out = (count > duty_cycle);
always @(posedge clk) begin
if (!rst_n) begin
count <= 0;
end
else begin
if (count == 8'b11111111) begin
count <= 8'b0;
end else begin
count <= count + 1'b1;
end
end
end
endmodule
数码管字符编码部分
对显示的数据(00-FF)进行拆分,形成两个4bit数据(0-F),分别查找对应显示样式。
module nibble_to_segment(
input wire [3:0] nibble, // Input 4-bit nibble representing a hexadecimal digit
input wire en, // Enable to display number or just '-'
output wire [6:0] seg_display_opt // Seven-segment display control signals
);
reg [6:0] seg_display;
assign seg_display_opt = seg_display[6:0];
always @(*) begin
if (en) begin
case(nibble)
4'b0000: seg_display <= ~7'b1000000; // Display digit 0
4'b0001: seg_display <= ~7'b1111001; // Display digit 1
4'b0010: seg_display <= ~7'b0100100; // Display digit 2
4'b0011: seg_display <= ~7'b0110000; // Display digit 3
4'b0100: seg_display <= ~7'b0011001; // Display digit 4
4'b0101: seg_display <= ~7'b0010010; // Display digit 5
4'b0110: seg_display <= ~7'b0000010; // Display digit 6
4'b0111: seg_display <= ~7'b1111000; // Display digit 7
4'b1000: seg_display <= ~7'b0000000; // Display digit 8
4'b1001: seg_display <= ~7'b0010000; // Display digit 9
4'b1010: seg_display <= ~7'b0001000; // Display digit A
4'b1011: seg_display <= ~7'b0000011; // Display digit B
4'b1100: seg_display <= ~7'b1000110; // Display digit C
4'b1101: seg_display <= ~7'b0100001; // Display digit D
4'b1110: seg_display <= ~7'b0000110; // Display digit E
4'b1111: seg_display <= ~7'b0001110; // Display digit F
default: seg_display <= ~7'b0111111; // Display blank for other values (-)
endcase
end else
seg_display <= ~7'b0111111;
end
endmodule
module split_and_display(
input wire [7:0] input_value, // Input 8-bit value
input wire en, // Enable to display hex number
output wire [6:0] seg_display1, // Seven-segment display control for first digit
output wire [6:0] seg_display2 // Seven-segment display control for second digit
);
wire [3:0] digit1; // Variable to store the first digit
wire [3:0] digit2; // Variable to store the second digit
// Split the input value into two hexadecimal digits
assign digit1 = input_value[7:4]; // First digit (most significant 4 bits)
assign digit2 = input_value[3:0]; // Second digit (least significant 4 bits)
// Convert each digit to seven-segment display control signals using the nibble_to_segment module
nibble_to_segment nibble_converter1(.nibble(digit1), .en(en), .seg_display_opt(seg_display1));
nibble_to_segment nibble_converter2(.nibble(digit2), .en(en), .seg_display_opt(seg_display2));
endmodule
UART接收模块
该部分分为两部分:uart_clock_generator和uart_receiver。uart_clock_generator产生19200波特率对应的采样时钟,uart_receiver中的状态机根据产生的采样时钟,进行转移,最后得到并行8bit数据,传给交互逻辑处理部分。
module uart_clock_generator(
input wire clk, // clock
input wire rst_n,
input wire en,
output wire uart_clk // UART sample clock
);
reg [15:0] counter; // counter
assign uart_clk = (counter == 15);
localparam COUNTER_MAX = 31 - 1;
always @(posedge clk or negedge rst_n) begin
if (!rst_n || !en) begin
counter = 0;
end
else begin
if (counter == COUNTER_MAX) begin
counter <= 0;
end
else
counter <= counter + 1'b1;
end
end
endmodule
module uart_receiver(
input wire clk, // system clock
input wire rx, // UART RX wire
input wire rst_n,
output reg [7:0] received_data, // parallel data output
output wire data_ready // parallel data ready
);
// state defination
localparam UART_IDLE = 2'b01;
localparam UART_RECEIVING = 2'b10;
wire sclk; // sample clock
reg sclk_en; // sample clock enable
uart_clock_generator uart_clock_generator_inst(
.clk(clk),
.rst_n(rst_n),
.en(sclk_en),
.uart_clk(sclk)
);
// rx buffer
reg rx_prev;
// state register
reg [1:0] state;
reg [3:0] bit_count;
assign data_ready = (bit_count == 4'b1111);
// data register
reg [7:0] data_reg = 8'b00000000;
// sample clk enable
always @(*) begin
if(!rst_n) begin
sclk_en <= 0;
end
else begin
if (rx_prev && !rx ) begin
sclk_en <= 1;
end
else if (bit_count == 4'b1111) begin
sclk_en <= 0;
end
end
end
always @(posedge clk) begin
if(!rst_n) begin
rx_prev <= 1'b1;
end
else
rx_prev <= rx;
end
//state machine
always @(posedge clk) begin
if(!rst_n) begin
state <= UART_IDLE;
data_reg <= 8'b00000000;
bit_count <= 4'b0000;
received_data <= 8'b00000000;
end
else begin
if(!(state == UART_IDLE || state == UART_RECEIVING))
state <= UART_IDLE;
if (rx_prev && !rx && bit_count == 4'b1111) begin
bit_count <= 4'b0000;
end
if(sclk == 1) begin
case(state)
UART_IDLE:
if (!rx) begin
state <= UART_RECEIVING;
bit_count <= 4'b0000;
end
UART_RECEIVING:
begin
if (bit_count < 8) begin
data_reg[bit_count] <= rx;
bit_count <= bit_count + 1'b1;
end
else
begin
received_data <= data_reg;
state <= UART_IDLE;
bit_count <= 4'b1111;
end
end
default:
begin
state <= UART_IDLE;
bit_count <= 4'b0000;
end
endcase
end
end
end
endmodule
交互逻辑处理模块(顶层模块)
接收来自串口解码后的命令和按键输出,选择对应的RGB通道数据进行修改操作
该部分对各个模块实例化,以及时钟分频,并负责串口数据包的解析。
module top_module(
input wire sys_clk, // system clock
input wire rst_n, // reset botton
input wire up_btn, // density up
input wire down_btn, // density down
input wire uart_rx_data, // RX wire
input wire [2:0] toggle_switch, // color switch
output wire [6:0] seg_display1,
output wire [6:0] seg_display2,
output wire seg1_en,
output wire seg2_en,
output wire [2:0] leds, // color led
output wire hex_display_en
);
reg [4:0] sys_clk_divder_counter = 4'b0000;
reg clk = 1'b1;
always @(posedge sys_clk) begin
begin
if(sys_clk_divder_counter >= 9) begin
sys_clk_divder_counter <= 0;
clk <= ~clk;
end
else
sys_clk_divder_counter <= sys_clk_divder_counter + 1'b1;
end
end
reg [7:0] led_r; // R channel intensity
reg [7:0] led_g; // G channel intensity
reg [7:0] led_b; // B channel intensity
wire [7:0] hex_display;
wire [7:0] received_data;
wire data_ready;
assign seg1_en = 0;
assign seg2_en = 0;
split_and_display display_inst( // channle intensity display instance
.input_value(hex_display),
.en(hex_display_en),
.seg_display1(seg_display1),
.seg_display2(seg_display2)
);
uart_receiver uart_receiver_inst( // uart receiver instance
.clk(clk),
.rx(uart_rx_data),
.rst_n(rst_n),
.received_data(received_data),
.data_ready(data_ready)
);
// color led PWM controller instance
pwm_control pwm_control_r_inst(
.clk(clk),
.rst_n(rst_n),
.duty_cycle(led_r),
.pwm_out(leds[2])
);
pwm_control pwm_control_g_inst(
.clk(clk),
.rst_n(rst_n),
.duty_cycle(led_g),
.pwm_out(leds[1])
);
pwm_control pwm_control_b_inst(
.clk(clk),
.rst_n(rst_n),
.duty_cycle(led_b),
.pwm_out(leds[0])
);
assign hex_display = (toggle_switch == 3'b001) ? led_r :
((toggle_switch == 3'b010) ? led_g :
led_b
);
assign hex_display_en = (toggle_switch == 3'b001) || (toggle_switch == 3'b010) || (toggle_switch == 3'b100);
reg [3:0] mod_status; // UART parser state register
reg prev_up;
reg prev_down;
reg prev_ready;
wire up_flag = (prev_up == 1'b1 && up_btn == 1'b0); // up button pressed
wire down_flag = (prev_down == 1'b1 && down_btn == 1'b0); // down button pressed
wire ready_flag = (prev_ready == 1'b0 && data_ready == 1'b1); // UART data ready
always @(posedge clk) begin
if(!rst_n) begin
prev_up <= 1;
prev_down <= 1;
prev_ready <= 0;
end
prev_up <= up_btn;
prev_down <= down_btn;
prev_ready <= data_ready;
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
led_r <= 8'b10000000;
led_b <= 8'b10000000;
led_g <= 8'b10000000;
mod_status <= 4'b1000;
end
else begin
if(ready_flag) begin // state machine for UART command parser
case(mod_status)
4'b1000:
if(received_data[7:0] == 8'b00000000) mod_status <= 4'b0001;
4'b0001:
begin
led_r[7:0] <= received_data[7:0];
mod_status = 4'b0010;
end
4'b0010:
begin
led_g[7:0] <= received_data[7:0];
mod_status = 4'b0100;
end
4'b0100:
begin
led_b[7:0] <= received_data[7:0];
mod_status = 4'b1000;
end
default:
mod_status = 4'b1000;
endcase
end
if(up_flag) begin
case(toggle_switch)
3'b001:
begin
if(led_r != 8'b11111111)
led_r <= led_r + 1'b1;
end
3'b010:
begin
if(led_g != 8'b11111111)
led_g <= led_g + 1'b1;
end
3'b100:
begin
if(led_b != 8'b11111111)
led_b <= led_b + 1'b1;
end
endcase
end
if(down_flag) begin
case(toggle_switch)
3'b001:
begin
if(led_r != 8'b00000000)
led_r <= led_r - 1'b1;
end
3'b010:
begin
if(led_g != 8'b00000000)
led_g <= led_g - 1'b1;
end
3'b100:
begin
if(led_b != 8'b00000000)
led_b <= led_b - 1'b1;
end
endcase
end
end
end
endmodule
上位机部分代码
使用Python3编写上位机,配合serial、tkinter自带库实现
当用户选择发送操作后,程序首先读取计算机串口列表。找到相应端口设备后,开始按照特定的包格式传输用户设定的RGB颜色值。
import serial
import serial.tools.list_ports
import tkinter as tk
from tkinter import Scale
after_id = 0
# Listing available serial ports
def detect_serial_ports():
ports = serial.tools.list_ports.comports()
if not ports:
print("No serial ports detected.")
return []
else:
available_ports = []
for port, desc, hwid in sorted(ports):
available_ports.append(port)
# print(f"- {port}: {desc}")
return available_ports
prev_data = [0, -1, -1, -1]
# Function to send RGB values to the serial port
def send_serial_data():
global prev_data
available_ports = detect_serial_ports()
if available_ports:
for port in available_ports:
ser = serial.Serial(port, 19200) # Change 9600 to the desired baud rate
if prev_data == [0, red_slider.get(), green_slider.get(), blue_slider.get()]:
break
data = bytes([0, red_slider.get(), green_slider.get(), blue_slider.get()])
prev_data = [0, red_slider.get(), green_slider.get(), blue_slider.get()]
print (prev_data)
ser.write(data)
ser.close()
else:
print("No available serial ports to send data.")
def send_forever():
send_serial_data()
global after_id
after_id = root.after(10, send_forever)
def stop_send():
global after_id
root.after_cancel(after_id)
after_id = 0
def toggle_send():
if after_id:
stop_send()
else:
send_forever()
# Create the GUI window
root = tk.Tk()
root.title("RGB Control")
# Create sliders for R, G, B values
red_slider = Scale(root, from_=0, to=255, orient='horizontal', label='R')
red_slider.pack()
green_slider = Scale(root, from_=0, to=255, orient='horizontal', label='G')
green_slider.pack()
blue_slider = Scale(root, from_=0, to=255, orient='horizontal', label='B')
blue_slider.pack()
# Button to send RGB data
send_button = tk.Button(root, text="Send RGB Data", command=toggle_send)
send_button.pack()
root.mainloop()
FPGA部分仿真波形
该部分的仿真激励包括一个完整的串口数据包、以及亮度+、亮度-按下的事件。以显示完整的亮度调整功能。
PWM控制模块波形:
数码管控制模块波形:
串口接收模块波形:
顶层模块波形:
FPGA资源占用情况
逻辑资源占用约7%。
期间遇到的问题
开发过程中遇到了仿真波形与实际波形遇到不同的问题。
主要是根据综合器的日志输出进行分析:
- Lattice的综合器会将状态机的状态变量映射为One-hot code,传统写法的4状态状态机需要两位寄存器即可,但映射后需要4位寄存器。状态的定义、寄存器的位宽需要特别注意。这点在Xilinx和Altera公司产品的开发过程中没有遇到。
- WebIDE中设置的Target Frequency 为 1MHz,但开发板的时钟输入为12MHz晶振。在用WebIDE开发过程中,遇到了串口接收失灵的情况,但换到本机Diamond编译结果正常工作,怀疑是由于时序问题导致。将输入时钟进行分频,得到600kHz左右的时钟提供给其他模块后,未再发现问题。
未来规划与期望
本次实验成功完成了任务的所有指标功能。对Lattice FPGA开发流程有了一定了解。但未使用片上外设,如PLL、I2C、SPI等功能。下一步可以深入了解这些外设的开发方法。实现对芯片资源更好地利用。