项目总结报告
1,项目介绍
基于iCE40UP5K实现可定时的音乐时钟,实现功能:
- 使用扩展板上的12颗彩灯对应于12个小时
- 核心板上的FPGA产生时钟,在OLED显示评上通过模拟或者数字的方式显示当前的时间 - 小时、分、秒
- 将“小时”的信息通过12颗彩灯来显示
- 具有定时的功能,通过扩展板上的按键设置时间,到该时间点即(彩灯闪烁 + 音频播放),持续5秒钟时间
使用板卡:iCE40核心板+一圈灯底板
利用FPGA开发板实现上述功能,该设备适用于家庭等场景。
2,硬件介绍
iCE40UP5K由于其独特的性能得到海外创客们的热捧,尤其是其支持开源的开发工具链。不少RISC-V软核都是运行在iCE40UP5K上,基于这个大背景我们开发了一款FPGA核心模块,主要特点:
兼容树莓派基金会刚推出的Pico模块的管脚定义,Pico的GPIO性能比较强大,因此玩家可以对比Pico和FPGA IO管脚的使用,另外也可以借助位PICO推出的各种外设模块(本模块没有串行ADC的功能,只能支持数字端口的模块);
板上集成了基于DAPLink下载功能的STEPLINK功能,可以直接通过USB端口对FPGA的串行Flash进行配置,并可以通过同一个USB端口支持UART通信;
有开源的RISC-V可以移植使用,进而学习在FPGA上完成软核+FPGA的异构设计
本设计基于Lattice的ICE40UP5K FPGA,板载LPC11U35下载器,可以通过USB-C接口进行FPGA的配置,支持在ICE40UP5K上对RISC-V软核的移植以及开源的FPGA开发工具链,板上RGB三色LED灯用于简单的调试,总计28个IO用于扩展使用。
- ws2812可编程LED
- ICE40UP5K FPGA开发板
- OLED显示屏:本项目使用的OLED显示屏为0.96英寸、128x64分辨率的SPI接口显示屏。它具有高对比度、低功耗的特点,适合用于显示实时电流值和其他相关信息。
- 其他硬件 蜂鸣器 按键等
3,方案框图和项目设计思路
方案框图
- 功能设置
- 代码设计
项目设计思路
实现OLED,ws2812,蜂鸣器的驱动,然后用top.v将其组装起来,上电之后实现显示时钟页面,按任意键进入闹钟编辑模式,5s未操作自动退回时钟显示,到达指定时间后蜂鸣器响起,彩灯闪烁,持续5s,恢复显示时钟页面
软件流程图和关键代码介绍
- 流程图
- 状态机实现刷屏
5'd1: begin
x_ph <= 8'h10 + loop_idx[4:2];
y_p <= 8'hb0 + (loop_idx[1:0] * 2);
x_pl <= 8'h00;
find_num <= 8'd11;
state <= FIND;
if (loop_idx == 5'd31) begin
loop_idx <= 5'd0;
end else begin
loop_idx <= loop_idx + 1'b1;
end
end
5'd2 : begin y_p <= 8'hb4; x_ph <= 8'h10; x_pl <= 8'h00;find_num<=data[23:20];state<= FIND;end
5'd3 : begin y_p <= 8'hb4; x_ph <= 8'h11; x_pl <= 8'h00;find_num<=data[19:16];state<= FIND;end
5'd4: begin y_p <= 8'hb4; x_ph <= 8'h12; x_pl <= 8'h00;find_num<=8'd10;state<= FIND;end
5'd5 : begin y_p <= 8'hb4; x_ph <= 8'h13; x_pl <= 8'h00;find_num<=data[15:12];state<= FIND;end
5'd6 : begin y_p <= 8'hb4; x_ph <= 8'h14; x_pl <= 8'h00;find_num<=data[11:8];state<= FIND;end
5'd7: begin y_p <= 8'hb4; x_ph <= 8'h15; x_pl <= 8'h00;find_num<=8'd10;state<= FIND;end
5'd8 : begin y_p <= 8'hb4; x_ph <= 8'h16; x_pl <= 8'h00;find_num<=data[7:4];state<= FIND;end
5'd9 : begin y_p <= 8'hb4; x_ph <= 8'h17; x_pl <= 8'h00;find_num<=data[3:0];state<= FIND;end
default: state <= IDLE;
endcase
end
INIT:begin
case(cnt_init)
5'd0: begin oled_rst <= LOW; cnt_init <= cnt_init + 1'b1; end
5'd1: begin num_delay <= 16'd25000; state <= DELAY; state_back <= INIT; cnt_init <= cnt_init + 1'b1; end
5'd2: begin oled_rst <= HIGH; cnt_init <= cnt_init + 1'b1; end //婢跺秳缍呴幁銏狀槻
5'd3: begin num_delay <= 16'd25000; state <= DELAY; state_back <= INIT; cnt_init <= cnt_init + 1'b1; end
5'd4: begin
if(cnt>=INIT_DEPTH) begin
cnt <= 1'b0;
cnt_init <= cnt_init + 1'b1;
end else begin
cnt <= cnt + 1'b1; num_delay <= 16'd5;
oled_dcn <= CMD; char_reg <= cmd[cnt]; state <= WRITE; state_back <= INIT;
end
end
5'd5: begin cnt_init <= 1'b0; state <= MAIN; end
default: state <= IDLE;
endcase
end
- 实现计时
module timer(
input clk, //
input rst_n, //
input L_button,
input R_button,
output reg [4:0] tone,
output [23:0] time_mem ,
output reg [3:0] led_time
);
// 鍒嗛鍙傛暟锛�12MHz -> 1Hz锛�
localparam DIVIDER = 24'd12_000_000 - 24'd1;
reg [23:0] counter;
reg sec_en;
reg [25:0] counter_1;
reg sec_en_1;
reg [2:0] flag_3;
reg [3:0] sec_unit;
reg [3:0] sec_ten;
reg [3:0] min_unit;
reg [3:0] min_ten;
reg [3:0] hour_unit;
reg [3:0] hour_ten;
reg menu;
//闹钟
reg [3:0] sec_unit_a;
reg [3:0] sec_ten_a;
reg [3:0] min_unit_a;
reg [3:0] min_ten_a;
reg [3:0] hour_unit_a;
reg [3:0] hour_ten_a;
reg flag;
reg [3:0] tens; // 十位BCD码
reg [3:0] units; // 个位BCD码
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tone <= 5'd0;
flag_3 <= 3'd0;
end else begin
// 条件1:时间匹配时触发
if (sec_unit == 4'd0 && sec_ten == 4'd0 &&
min_unit == min_unit_a &&
min_ten == min_ten_a &&
hour_unit == hour_unit_a &&
hour_ten == hour_ten_a)
begin
tone <= 5'd1;
end
// 条件2:秒使能且 tone <=1 时计数
if (sec_en == 1'b1 && tone <= 5'd1) begin
if (flag_3 == 3'd5) begin
tone <= 5'd0;
flag_3 <= 3'd0;
end else begin
flag_3 <= flag_3 + 3'd1;
end
end
end // end of else
end // end of always
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sec_unit_a <= 4'd0;
sec_ten_a <= 4'd0;
min_unit_a <= 4'd0;
min_ten_a <= 4'd0;
hour_unit_a <= 4'd0;
hour_ten_a <= 4'd0;
flag <= 1'b0;
menu <= 1'b0;
end else begin
// 左键处理:递增分钟(带进位)
if (!L_button && !flag) begin
if (min_unit_a == 4'd9) begin
min_unit_a <= 4'd0;
if (min_ten_a == 4'd5) begin
min_ten_a <= 4'd0;
end else begin
min_ten_a <= min_ten_a + 1;
end
end else begin
min_unit_a <= min_unit_a + 1;
end
flag <= 1'b1;
//menu <= 1'b1;
end
// 右键处理:递增小时(24小时制)
if (!R_button && !flag) begin
if (hour_ten_a== 8'd2 && hour_unit_a== 8'd3) begin
hour_ten_a <= 4'd0;
hour_unit_a <= 4'd0;
end else if (hour_unit_a == 4'd9) begin
hour_unit_a <= 4'd0;
hour_ten_a <= hour_ten_a + 1;
end else begin
hour_unit_a <= hour_unit_a + 1;
end
flag <= 1'b1;
//menu <= 1'b1;
end
// 按键释放时清标志位
if (L_button && R_button) begin
flag <= 1'b0;
if(sec_en_1 == 1'b1)begin
menu <= 0;
end
end
if(!L_button || !R_button)begin
menu <= 1;
end
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n||!L_button || !R_button) begin
counter_1 <= 26'd0;
sec_en_1 <= 1'b0;
end else if (menu) begin
if (counter_1 == 26'd35_999_999) begin // 修正比较值
counter_1 <= 26'd0;
sec_en_1 <= 1'b1;
end else begin
counter_1 <= counter_1 + 1;
sec_en_1 <= 1'b0;
end
end else begin
counter_1 <= 26'd0; // menu=0时清零
end
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
counter <= 24'd0;
sec_en <= 1'b0;
end else begin
if(counter == DIVIDER) begin
counter <= 24'd0;
sec_en <= 1'b1;
end else begin
counter <= counter + 24'd1;
sec_en <= 1'b0;
end
end
endalways @(posedge clk or negedge rst_n) begin
if(!rst_n) sec_unit <= 4'd0;
else if(sec_en) begin
sec_unit <= (sec_unit == 4'd9) ? 4'd0 : sec_unit + 1'b1;
end
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n) sec_ten <= 4'd0;
else if(sec_en && (sec_unit == 4'd9)) begin
sec_ten <= (sec_ten == 4'd5) ? 4'd0 : sec_ten + 1'b1;
end
end
wire min_inc = sec_en && (sec_unit == 4'd9) && (sec_ten == 4'd5);
always @(posedge clk or negedge rst_n) begin
if(!rst_n) min_unit <= 4'd0;
else if(min_inc) begin
min_unit <= (min_unit == 4'd9) ? 4'd0 : min_unit + 1'b1;
end
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n) min_ten <= 4'd0;
else if(min_inc && (min_unit == 4'd9)) begin
min_ten <= (min_ten == 4'd5) ? 4'd0 : min_ten + 1'b1;
end
end
wire hour_inc = min_inc && (min_unit == 4'd9) && (min_ten == 4'd5);
always @(posedge clk or negedge rst_n) begin
if(!rst_n) hour_unit <= 4'd0;
else if(hour_inc) begin
if(hour_ten == 4'd2) begin
hour_unit <= (hour_unit == 4'd3) ? 4'd0 : hour_unit + 1'b1;
end else begin
hour_unit <= (hour_unit == 4'd9) ? 4'd0 : hour_unit + 1'b1;
end
end
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n) hour_ten <= 4'd0;
else if(hour_inc) begin
if(hour_ten == 4'd2 && hour_unit == 4'd3) begin
hour_ten <= 4'd0;
end else if(hour_unit == 4'd9) begin
hour_ten <= hour_ten + 1'b1;
end
end
end
always @(posedge clk) begin
tens <= time_mem[23:20]; // 高4位为十位
units <= time_mem[19:16]; // 低4位为个位
led_time <= (tens == 4'd0 && units == 4'd0) ? 4'd12-1 :
(tens * 4'd10 + units > 12) ? tens * 4'd10 + units - 13 :
tens * 4'd10 + units-1;
end
assign time_mem = (menu == 0)?{
hour_ten, hour_unit,
min_ten, min_unit,
sec_ten, sec_unit
}:{ hour_ten_a, hour_unit_a,
min_ten_a, min_unit_a,
sec_ten_a, sec_unit_a};
endmodule
4,功能展示及说明
功能实现项目要求的任务2,可定时的音乐时钟,使用扩展版上的12个彩灯对应12个小时(模拟真实的时钟),核心板使用内部时钟(12Mhz)生成一组1s时钟驱动该项目,项目默认显示24小时制时间,闹钟功能通过扩展版上的两个key控制,其中左键控制时,右键控制分钟,时钟工作情况下按任意键进入闹钟编辑状态,设定好后,3s内回复原菜单,即时钟显示菜单,到达设定时间后,彩灯闪烁,蜂鸣器响五秒,结束
- 时钟功能
- 编辑功能(左右键分别为时分编辑)
- 响铃展示
- 资源占用截图
5,项目中遇到的困难和解决办法
困难1:OLED驱动问题,因为以前没用过FPGA开发板,驱动OLED都是用别人开发好的库函数,用商家给的程序后虽能工作,但一改就报错,在此非常感谢硬禾科技的工程师,对方非常热心,甚至在周末都会解答我的困惑,在网上搜寻大量资料和工程师的帮助下,我写出了正确的OLED驱动程序,在这个模块上完美工作
困难2:模块整合问题,我所选的项目是一个非常简单的时钟(本来是这么想的)
但是也涉及到包括计时器,OLED显示,蜂鸣器的驱动,彩灯的驱动,每一个的代码都要去找对应的资料
困难3:对硬件编程语言Verilog的不熟悉,上面已经提过,在b站和硬禾科技的官方网站上中找到了许多有用的资料
6,对本次活动的心得体会
非常喜欢这次的活动,感谢硬禾科技的此次活动,让我对硬件底层的实现有了更多的认识,让我对FPGA这种可编程硬件有了更多的认识,完成返还全额的规则也让学生有更多的机会取接触这种相对昂贵的硬件,感谢硬禾科技!对于未来的改进,我计划在项目中加入更多的实用功能,以进一步提升设备的实用性。