2024年寒假练 - 基于Lattice MXO2实现的程控彩色LED
基于Lattice MXO2实现的程控彩色LED。使用小脚丫STEP MXO2-LPC开发板,用户可以通过板载串口、板载按键改变三色LED显示颜色
标签
FPGA
开发板
kevin_1231
更新2024-04-01
195

任务描述

  1. 根据拨动开关的选择,将正在显示的R、G、B中一种颜色值显示在数码管上,两个数码管可以显示从0-FF之间的数值
  2. 使用小脚丫FPGA上的拨动开关SW1~SW3(对应红、绿、蓝三色)选择要在数码管上要显示其值,以及轻触开关变化要控制的颜色
  3. 使用小脚丫FPGA上的轻触开关K1和K2(分别对应加、减亮度操作),来控制拨动开关SW1到SW3选择好的颜色,进行颜色值的调节
  4. 在PC上设计一个简单的界面(可以使用任何一种编程语言或工具),能够单独控制R、G、B的三个颜色值从0到255,可以通过鼠标(电脑上)、和通过UART传递过来的控制按键来调节R、G、B的值
  5. 调节好的值通过UART传递到小脚丫FPGA上,控制一颗三色LED的状态

 

需求分析

       该项目涉及到的外设较多、功能需求繁杂。这要求设计者具有工程化设计理念、规范化的设计流程。故需要先对项目解耦,拆分出相应模块逐步实现(这点可以借助chatgpt等工具提供思路):

  • 串口通信模块:负责与电脑进行串口通信,接收来自电脑的控制指令。
  • RGB LED PWM控制模块:包含逻辑,将指令转换为相应的 RGB LED 的PWM控制信号。
  • 数码管字符编码模块:接收来自由编码开关选择读取模块的数值,做相应转换以控制两位数码管显示对应的 RGB 通道值。
  • 交互逻辑模块:接收来自串口模块、按键模块的信号,解析动作指令并传递至LED控制模块和数码管显示模块以执行操作。

 

实现方式

       将相应需求输入chatGPT,为模块拆分、和具体代码写作思路:

批注 2024-03-05 015025.png

批注 2024-03-05 015158-3.png

批注 2024-03-05 015025-1.png

批注 2024-03-05 015158-2.png

获得最初代码并复制到WebIDE中。再根据逻辑需要和仿真波形进行调整修改。

使用了小脚丫在线WebIDE开发。完整工程项目请见:https://www.stepfpga.com/project/10137

 

功能框图

图片.png

硬件FPGA部分功能框图


图片.png

上位机部分流程图


代码及说明

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控制模块波形:

图片.png

数码管控制模块波形:

图片.png

串口接收模块波形:

图片.png

顶层模块波形:

图片.png

FPGA资源占用情况

图片.png

逻辑资源占用约7%。

期间遇到的问题

开发过程中遇到了仿真波形与实际波形遇到不同的问题。

主要是根据综合器的日志输出进行分析:

  1. Lattice的综合器会将状态机的状态变量映射为One-hot code,传统写法的4状态状态机需要两位寄存器即可,但映射后需要4位寄存器。状态的定义、寄存器的位宽需要特别注意。这点在Xilinx和Altera公司产品的开发过程中没有遇到。
  2. WebIDE中设置的Target Frequency 为 1MHz,但开发板的时钟输入为12MHz晶振。在用WebIDE开发过程中,遇到了串口接收失灵的情况,但换到本机Diamond编译结果正常工作,怀疑是由于时序问题导致。将输入时钟进行分频,得到600kHz左右的时钟提供给其他模块后,未再发现问题。

未来规划与期望

本次实验成功完成了任务的所有指标功能。对Lattice FPGA开发流程有了一定了解。但未使用片上外设,如PLL、I2C、SPI等功能。下一步可以深入了解这些外设的开发方法。实现对芯片资源更好地利用。

附件下载
project_archive.zip
Verilog 代码 (或访问 https://www.stepfpga.com/project/10137)
uart_controller.py
上位机代码
uart_led_new.lpf
引脚约束文件
团队介绍
普通大学生
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号