一.项目需求
- 在PC上设计一个简单的界面(可以使用任何一种编程语言或工具),能够单独控制R、G、B的三个颜色值从0到255,可以通过鼠标(电脑上)、和通过UART传递过来的控制按键来调节R、G、B的值
- 调节好的值通过UART传递到小脚丫FPGA上,控制一颗三色LED的状态
- 根据拨动开关的选择,将正在显示的R、G、B中一种颜色值显示在数码管上,两个数码管可以显示从0-FF之间的数值
- 使用小脚丫FPGA上的拨动开关SW1~SW3选择要在数码管上要显示其值,以及轻触开关变化要控制的颜色:
- SW1:红色R
- SW2:绿色G
- SW3:蓝色B
- 使用小脚丫FPGA上的轻触开关K1和K2,来控制拨动开关SW1到SW3选择好的颜色,进行颜色值的调节:
- K1: 颜色值+1
- K2: 颜色值-1
- 使用鼠标控制上位机界面中的任意一种颜色,改变小脚丫FPGA上的三色LED的显示,以及数码管上显示的颜色值(根据SW1-3的状态选择其一),通过三种颜色的调节,生成白色的效果,并将能够生成白色的R、G、B三种颜色的值记录下来,写在报告中。
二.需求分析和实现方式
根据要求,可以总结出人机操作界面一种是本地按键、一种是上位机软件通过串口下传。FPGA需要一个写一个串口接收模块,按键消抖模块,以及总的处理模块。作为显示,需要数码管和RGB灯,所以需要一个数码管译码的模块和3个PWM模块。把这些模块通过GPT生成、改写、仿真、烧录基本上就可以满足任务需求。
三.功能框图
根据如上分析,可以设像出需要的模块。在FPGA中需要顶层模块、按键消抖模块、数码管译码模块、串口接收模块、PWM信号发生模块。由这些模块,可以画出如下功能框图。
上位机是使用MATLAB的appdesigner写的。由选择串口的下拉框、打开串口的按钮、RGB三个通道的滑动条、RGB三个通道的数值显示文本框、fwrite()用于串口发送几部分组成。上位机框图如下。
四.代码介绍和仿真波形
①按键消抖模块
使用微软edge浏览器的 Toggle ChatGPT Sidebar ,使用它可以不翻墙直接使用,免费版是3.5版本,免费版每天限制30条。使用GPT生成按键模块如下。
经过整理格式,变为
module debounce( input wire clk, input wire rst, input wire btn, output reg btn_debounced );
reg [3:0] count;
reg [1:0] state;
parameter DEBOUNCE_TIME = 20000000; // 20ms
always @(posedge clk or posedge rst)
begin
if (rst)
begin
count <= 0;
state <= 0;
btn_debounced <= 0;
end
else
begin
case (state)
2'b00: begin
if (btn)
begin
count <= count + 1;
if (count == DEBOUNCE_TIME)
begin
state <= 2'b01;
count <= 0;
end
end
else
begin
count <= 0;
end
end
2'b01: begin
if (!btn)
begin
state <= 2'b00;
btn_debounced <= 1;
end
end
endcase
end
end
endmodule
以上代码可见的错误:
①count位数不够
②rst和btn都是高有效,和板子不匹配
③always块中逻辑有问题,不能做到达到时间后输出一个脉冲。
所以做如下更改
module Debounce(
input wire clk,
input wire rst,
input wire btn,
output reg btn_debounced
);
reg [18:0] count;
reg [1:0] state;
parameter DEBOUNCE_TIME = 240000; // 20ms
always @(posedge clk or negedge rst)
begin
if (!rst)
begin
count <= 0;
state <= 0;
btn_debounced <= 0;
end
else
begin
case (state)
2'b00: begin
if (!btn)
begin
if (count == DEBOUNCE_TIME) //reach 20ms
begin
state <= 2'b01;
count <= 0;
btn_debounced<=1;
end
else
begin
count <= count + 1;//
btn_debounced<=0;
end
end
else
begin
count <= 0;
btn_debounced<=0;
end
end
2'b01: begin
if(!btn)//Wait for key release
begin
state <= 2'b01;
btn_debounced <= 0;
end
else
begin
state <= 2'b00;
btn_debounced <= 0;
end
end
endcase
end
end
endmodule
由于网页ide仿真只能10ms,所以我把消抖时间设置为2ms,实际下载到板子的程序需要改回来,仿真代码如下。
`timescale 1ns/1ns
module test_tb();
reg clk;
reg rst;
reg btn;
always #(83.33/2) clk=~clk;//12M clock
Debounce u1(
.clk(clk),
.rst(rst),
.btn(btn),
.btn_debounced(btn_debounced)
);
initial
begin
btn=1;
clk=1'b0;
rst=0;
#80 rst=1;
#1100 btn=0;//shake
#1150 btn=1;
#1200 btn=0;
#1300 btn=1;
#1400 btn=0;
#2100000 btn=1;//release
#2501100 btn=0;
#2501150 btn=1;
#2501200 btn=0;
#2501300 btn=1;
#2501400 btn=0;
#5100000 btn=1;
#5101100 btn=0;
#5101500 btn=1;
end
endmodule
可得仿真图
↑总体图,可见按下两次按键,到时间后btn_debounced会有一个脉冲输出
↑起始状态有几个按键抖动,每次抖动过后计数器都会从新开始计,直到达到2ms输出一个脉冲
↑ 2ms在12M下为0.002*12000000=24000,转为16进制是5DC0与上图对应。
②UART模块
需要接收上位机传回来的数据,因此需要串口模块。由于只有接收不发送,所以只需要写一个接收部分即可。
GPT生成代码如下。
经过整理格式如下。
module uart_receiver(
input clk,
input rst,
input rx,
output reg [7:0] data_out,
output reg data_ready
);
reg [2:0] state;
reg [3:0] bit_count;
reg [7:0] shift_reg;
parameter IDLE = 3'b000;
parameter START_BIT = 3'b001;
parameter DATA_BITS = 3'b010;
parameter STOP_BIT = 3'b011;
always @(posedge clk or posedge rst) begin
if (rst) begin
state <= IDLE;
bit_count <= 0;
shift_reg <= 0;
data_ready <= 0;
end else begin
case (state)
IDLE: begin
if (!rx) begin
state <= START_BIT;
bit_count <= 0;
end
end
START_BIT: begin
if (bit_count < 3) begin
bit_count <= bit_count + 1;
end else begin
state <= DATA_BITS;
bit_count <= 0;
end
end
DATA_BITS: begin
if (bit_count < 8) begin
shift_reg <= {shift_reg[6:0], rx};
bit_count <= bit_count + 1;
end else begin
state <= STOP_BIT;
bit_count <= 0;
end
end
STOP_BIT: begin
if (bit_count < 1) begin
bit_count <= bit_count + 1;
end else begin
state <= IDLE;
data_out <= shift_reg;
data_ready <= 1;
end
end
endcase
end
end
endmodule
经过观察测试可见:
①缺少波特率发生器,该代码clk直接是波特率对应频率,而实际clk接12M,所以需要一个分配器。
②接收数据的移位寄存器是先接收高位,而串口实际是先接收低位,需要反方向。
③起始信号到来打了4拍,而后面接收数据时都是1拍,所以应该加一个计数器,每接收1bit打8拍。
④缺少接收完成信号,可以利用data_ready的上升沿来生成一个接收完成信号。
所以改写后的代码如下。
module uart_receiver(
input clk,
input rst,
input rx,
output reg [7:0] data_out,
output reg rdy//end of receive flag
);
reg [2:0] state;
reg [3:0] bit_count;
reg [7:0] shift_reg;
parameter IDLE = 3'b000;
parameter START_BIT = 3'b001;
parameter DATA_BITS = 3'b010;
parameter STOP_BIT = 3'b011;
wire clken;
parameter BAUD = 115200;//Baud rate
parameter RX_ACC_MAX = (12000000 / BAUD / 8)-1;
parameter RX_ACC_WIDTH = 8;
reg [RX_ACC_WIDTH - 1:0] rx_acc = 1;
assign clken = (rx_acc == 5'd1);
always @(posedge clk)
begin
if(rx_acc == RX_ACC_MAX[RX_ACC_WIDTH - 1:0])
rx_acc <= 0;
else
rx_acc <= rx_acc + 5'b1;//Divider
end
reg [2:0] rdyr;
reg data_ready;
always @(posedge clk)
begin
rdyr <= {rdyr[1:0], data_ready};
rdy <= (rdyr[2:1]==2'b01); //get rising edge
end
reg [3:0]sample;
always @(posedge clk or negedge rst) begin
if (!rst) begin
state <= IDLE;
bit_count <= 0;
shift_reg <= 0;
data_ready <= 0;
end else begin
if(clken)
case (state)
IDLE: begin
if (!rx) begin//start bit come
state <= START_BIT;
bit_count <= 0;
data_ready<=0;
end
end
START_BIT: begin
if (bit_count < 3) begin
bit_count <= bit_count + 1;
sample<=0;
end else begin
state <= DATA_BITS;
bit_count <= 0;
end
end
DATA_BITS: begin//receive 8 bit
if (bit_count < 8) begin
if(sample<7)
sample<=sample+1;
else
begin
shift_reg <= {rx,shift_reg[7:1]}; //shifting register
bit_count <= bit_count + 1;
sample<=0;
end
end else begin
state <= STOP_BIT;
bit_count <= 0;
end
end
STOP_BIT: begin
if (bit_count < 1) begin
bit_count <= bit_count + 1;
end else begin
state <= IDLE;
data_out <= shift_reg;
data_ready <= 1;
end
end
endcase
end
end
endmodule
编写仿真文件如下:
`timescale 1ns/1ns
module uart_tb();
reg clk;
reg rst;
reg rx;
always #(83.33/2) clk=~clk;//12M clock
uart_receiver u1(
.clk(clk),
.rst(rst),
.rx(rx),
.data_out(),
.rdy()
);
initial
begin
rx=1'b1;
clk=1'b0;
rst=1'b0;
#300 rst=1'b1;
#20000 rx=1'b0;
#8680 rx=1;
#8680 rx=0;
#8680 rx=1;
#8680 rx=0;
#8680 rx=1;
#8680 rx=0;
#8680 rx=1;
#8680 rx=0;
#8680 rx=1; //10101010->01010101 8'h55
#8680 rx=1;
#8680 rx=1;
#8680 rx=1;
#8680 rx=1;
#20000 rx=1'b0;
#8680 rx=1;
#8680 rx=0;
#8680 rx=1;
#8680 rx=1;
#8680 rx=1;
#8680 rx=0;
#8680 rx=1;
#8680 rx=0;
#8680 rx=1;//10111010->01011101 8'h5d
#8680 rx=1;
#8680 rx=1;
#8680 rx=1;
#8680 rx=1;
end
endmodule
可以得到仿真波形如下:
↑可见data_out输出了所要求的8’h55和8‘h5d2,而且接收完成时rdy会有一个时钟周期的脉冲输出。
③PWM模块
pwm模块需要输入0-255的数,输出为pwm波。使用GPT生成。
整理可得
module pwm (
input clk,
input rst,
input [7:0] duty_cycle,
output reg pwm_out
);
reg [7:0] counter;
always @(posedge clk or posedge rst)
begin
if (rst)
counter <= 0;
else if (counter == 255)
counter <= 0;
else
counter <= counter + 1;
end
always @(posedge clk)
begin
if (counter < duty_cycle)
pwm_out <= 1;
else
pwm_out <= 0;
end
经观察和验证,可以发现:
①代码基本正确,可以使用
②由于LED的解法,需要将高低电平相反一下,这样才可以做到给的值越大灯越亮
③为了节省资源,可以将计数器的复位这部分省略,可以利用8位计数器到255后加1自动回0
改写后的代码如下。
module pwm (
input clk,
input [7:0] duty_cycle,
output reg pwm_out
);
reg [7:0] counter;
always @(posedge clk)
begin
counter <= counter + 1;
end
always @(posedge clk)
begin
if (counter < duty_cycle)
pwm_out <= 0;
else
pwm_out <= 1;
end
endmodule
编写仿真文件如下。
`timescale 1ns/1ns
module pwm_tb();
reg clk;
reg [7:0] duty;
always #(83.33/2) clk=~clk;//12M clock
pwm u2(
.clk(clk),
.duty_cycle(duty),
.pwm_out()
);
initial
begin
clk=1'b0;
duty=8'h00;
#30000 duty=8'd63;
#30000 duty=8'd127;
#30000 duty=8'd191;
end
endmodule
可以得到仿真波形如下。
↑由波形可得,3f为63,得到波形占空比为75%,由于LED是低电平点亮,所以对应占空比为25%。当7f时127,波形占空比50%。当bf时是191,所以波形占空比为25%。符合设计要求。
④数码管译码模块
需要显示0~f,所以输入为4bit数,输出为段码表。使用GPT生成如下。
整理得↓
module seven_segment_display(
input [3:0] data,
output reg [7:0] seg
);
always @(*) begin
case(data)
4'b0000: seg = 8'b11111100; // 0
4'b0001: seg = 8'b01100000; // 1
4'b0010: seg = 8'b11011010; // 2
4'b0011: seg = 8'b11110010; // 3
4'b0100: seg = 8'b01100110; // 4
4'b0101: seg = 8'b10110110; // 5
4'b0110: seg = 8'b10111110; // 6
4'b0111: seg = 8'b11100000; // 7
4'b1000: seg = 8'b11111110; // 8
4'b1001: seg = 8'b11110110; // 9
4'b1010: seg = 8'b11101110; // A
4'b1011: seg = 8'b00111110; // B
4'b1100: seg = 8'b10011100; // C
4'b1101: seg = 8'b01111010; // D
4'b1110: seg = 8'b10011110; // E
4'b1111: seg = 8'b10001110; // F
default: seg = 8'b11111111; // Blank
endcase
end
endmodule
该模块中可见0~f一共16个数,所以case中default永远不会出现,所以可以删去该行来节省资源,改后如下。
module seven_segment_display(
input [3:0] data,
output reg [7:0] seg
);
always @(*) begin
case(data)
4'b0000: seg = 8'b11111100; // 0
4'b0001: seg = 8'b01100000; // 1
4'b0010: seg = 8'b11011010; // 2
4'b0011: seg = 8'b11110010; // 3
4'b0100: seg = 8'b01100110; // 4
4'b0101: seg = 8'b10110110; // 5
4'b0110: seg = 8'b10111110; // 6
4'b0111: seg = 8'b11100000; // 7
4'b1000: seg = 8'b11111110; // 8
4'b1001: seg = 8'b11110110; // 9
4'b1010: seg = 8'b11101110; // A
4'b1011: seg = 8'b00111110; // B
4'b1100: seg = 8'b10011100; // C
4'b1101: seg = 8'b01111010; // D
4'b1110: seg = 8'b10011110; // E
4'b1111: seg = 8'b10001110; // F
endcase
end
endmodule
编写仿真文件如下。
`timescale 1ns/1ns
module display8_tb();
reg clk;
reg [3:0] seg_in;
always #(83.33/2) clk=~clk;//12M clock
seven_segment_display u5(
.data(seg_in),
.seg()
);
initial
begin
clk=1'b0;
seg_in=4'd0;
#200 seg_in=4'd0;
#200 seg_in=4'd1;
#200 seg_in=4'd2;
#200 seg_in=4'd3;
#200 seg_in=4'd4;
#200 seg_in=4'd5;
#200 seg_in=4'd6;
#200 seg_in=4'd7;
#200 seg_in=4'd8;
#200 seg_in=4'd9;
#200 seg_in=4'd10;
#200 seg_in=4'd11;
#200 seg_in=4'd12;
#200 seg_in=4'd13;
#200 seg_in=4'd14;
#200 seg_in=4'd15;
#200 seg_in=4'd0;
end
endmodule
得到的波形图如下。
↑seg_in在0~f变化,seg输入也变化
⑤顶层模块
顶层模块将几个顶层模块组装一下即可。其中按键操作和uart的逻辑比较复杂,GPT生成的不是很满意,所以改部分自己写。
always@(posedge clk or negedge rst)
begin
if(!rst)
begin
R_duty<=8'd0;
G_duty<=8'd0;
B_duty<=8'd0;
state<=IDLE;
end
else
begin
case(state)
IDLE:begin
if(rdy&&data_out==8'hff)state<=UART;
else
begin
state<=IDLE;
if(sw[0])
begin
if(increase_out)R_duty<=R_duty+1;
if(decrease_out)R_duty<=R_duty-1;
end
if(sw[1])
begin
if(increase_out)G_duty<=G_duty+1;
if(decrease_out)G_duty<=G_duty-1;
end
if(sw[2])
begin
if(increase_out)B_duty<=B_duty+1;
if(decrease_out)B_duty<=B_duty-1;
end
end
end
UART:begin
if(rdy&&data_out==8'hfe)state<=UARTR;
else if(rdy) state<=IDLE;
else state<=UART;
end
UARTR:begin
if(rdy)begin
state<=UARTG;
R_duty<=data_out;
end
else state<=UARTR;
end
UARTG:begin
if(rdy)begin
state<=UARTB;
G_duty<=data_out;
end
else state<=UARTG;
end
UARTB:begin
if(rdy)begin
state<=IDLE;
B_duty<=data_out;
end
else state<=UARTB;
end
default:state<=IDLE;
endcase
end
case(sw)
3'b001:seg_in<=R_duty;
3'b010:seg_in<=G_duty;
3'b100:seg_in<=B_duty;
default:seg_in<=8'd0;
endcase
end
使用一个状态机来实现这部分逻辑。UART发送5个字节,帧头是0xff 0xfe 后三个字节是RGB的颜色值。正常处于IDLE状态,这时可以用按键操作,当串口接收过程中是不可以用按键操作的,当然,这个状态只有几毫秒,忽略不计。帧头两个字节是因为0xff代表进入串口接收,0xfe若不对则立马退出串口接收,这样防止一次错误后永远卡在串口接收环节,导致按键无效。
五.上位机介绍
上位机使用matlab编写,界面如下。
打开串口之后,拉动滑动条,自动下传到下位机更新RGB颜色。一共有三个滑动条,可以创建一个SliderValueChanging回调函数,这样在滑动过程中text框中的数值会实时更新。
% Value changing function: RSlider
function RSliderValueChanging(app, event)
app.r = event.Value;
app.TextArea_4.BackgroundColor = [app.r/255.0 app.GSlider.Value/255.0 app.BSlider.Value/255.0];
app.TextArea.Value = {int2str(app.r)};
app.TextArea_2.Value = {int2str(app.GSlider.Value)};
app.TextArea_3.Value = {int2str(app.BSlider.Value)};
end
而串口发送可以创建一个SliderValueChanged回调函数,这样在滑动结束后会自动通过串口下传数据。使用try catch函数可以保证在串口关掉后,还可以拉动滑块看调色板。
% Value changed function: RSlider
function RSliderValueChanged(app, event)
try
fwrite(app.setupCOM, 255)
fwrite(app.setupCOM, 254)
fwrite(app.setupCOM, app.RSlider.Value)
fwrite(app.setupCOM, app.GSlider.Value)
fwrite(app.setupCOM, app.BSlider.Value)
catch
end
end
六.FPGA资源利用
七.实物效果
通过调节,可以配出白色的RGB值为:255 209 182 如下图所示。
八.总结
本次硬禾学堂寒假一起练,创新地要求使用GPT生成,驱使着我去尝试使用GPT写程序。体验后发现,GPT确实可以提升效率,写出来的程序一眼看上去大体框架是都正确的,还需要人工小做修改。同时本次任务还要求使用仿真验证,由于以前都是自己随便玩,一般调试都是烧录后看现象,最多使用一下逻辑分析功能,在这次任务的驱使下去学习了一下最简单的仿真文件编写,发现仿真确实可以提高开发效率,特别是逻辑比较复杂的时候,通过看波形可以很明了地发现错误。