1.任务目标
可定时的音乐时钟:
本项目基于iCE40 FPGA开发平台,利用核心板生成时钟信号,在OLED显示屏上以模拟或数字方式显示当前时间(小时、分钟、秒),并通过扩展板上的12颗彩灯对应12小时,以自定义效果显示当前“小时”信息;同时,通过按键设置定时功能,到达设定时间时触发彩灯闪烁和蜂鸣器播放音频,持续5秒,实现时间显示、彩灯指示和定时提醒功能
2.硬件介绍
板卡:iCE40UP5K
- 3个按键输入
- 12个WS2812B RGB三色灯
- 1个128*64 OLED显示屏
- 1个蜂鸣器
3.方案框图和项目设计思路
方案框图:
项目设计思路:
根据用户输入的按键信号控制时间系统的功能,并通过OLED屏幕的显示将结果反馈给用户,同时根据时间系统的状态控制彩灯与蜂鸣器。
具体为:
- 用户按键输入控制菜单的逻辑,菜单可修改时间寄存器里的时间、控制闹钟开关、控制屏幕显示的内容
- 时间计时器用于每秒更新一次时间寄存器中存储的当前时间
- 菜单的逻辑与时间寄存器共同决定OLED屏幕显示的内容。屏幕主要显示两个界面,即界面1主菜单界面和界面2时间显示界面。主菜单界面有显示时间、修改时间、修改闹钟时间和闹钟开关四个选项,其中前三个选项按下确认后进入界面2时间显示界面(这三项虽然功能不同,但可使用同样的界面来展示)。选项四闹钟开关默认关闭,设置好闹钟时间后自动开启闹钟,之后也可在主菜单选中该选项按下确认手动关闭或开启闹钟。
- 根据闹钟判断逻辑控制闹钟的报警。报警时使用不同颜色彩灯旋转循环显示,同时蜂鸣器响起铃声,持续5秒。
- 彩灯会根据时间寄存器里的当前时间是上午还是下午用不同的颜色表示出当前小时,白天用白灯的数量表示小时,晚上则在一圈白灯的基础上根据小时亮起对应数量的紫灯。
4.软件流程图和关键代码介绍
软件流程图:
关键代码介绍:
此处为菜单处理的逻辑,可以选择功能、对时间或闹钟数字进行修改。k1返回主菜单,k2切换选项/数字,run确定。
// 菜单处理逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_1 <= MAIN_MENU; // 初始在主菜单
state_2 <= OPTION_0; // 初始为菜单选项0
time_setting <= hour_tens; // 数位初始为小时的十位
alarm_switch <= 1'b0; // 闹钟关闭
alarm_second <= 6'd0; // 闹钟时间初始化
alarm_minute <= 6'd0;
alarm_hour <= 6'd0;
setting_flag1 <= 1'b0; // 时间修改信号1
end else begin
case (state_1)
MAIN_MENU: begin
if (k2) begin // 切换菜单选项
state_2 <= (state_2 + 1) % 4;
end else if (run) begin
case(state_2)
OPTION_0:begin
state_1 <= MENU_SELECT;
end
OPTION_1:begin
state_1 <= MENU_SELECT;
time_setting <= hour_tens;
reg_h_t <= current_hour / 10;
reg_h_u <= current_hour % 10;
reg_m_t <= current_minute / 10;
reg_m_u <= current_minute % 10;
reg_s_t <= current_second / 10;
reg_s_u <= current_second % 10;
end
OPTION_2:begin
state_1 <= MENU_SELECT;
time_setting <= hour_tens;
reg_h_t <= alarm_hour / 10;
reg_h_u <= alarm_hour % 10;
reg_m_t <= alarm_minute / 10;
reg_m_u <= alarm_minute % 10;
reg_s_t <= alarm_second / 10;
reg_s_u <= alarm_second % 10;
end
OPTION_3:begin
alarm_switch <= ~alarm_switch;
end
endcase
end
end
MENU_SELECT: begin
if(k1) state_1 <= MAIN_MENU;
else if (state_2 == OPTION_1 ||state_2 == OPTION_2) begin
if (run) begin
case(time_setting)
hour_tens: time_setting <= hour_units;
hour_units: time_setting <= minute_tens;
minute_tens: time_setting <= minute_units;
minute_units: time_setting <= second_tens;
second_tens: time_setting <= second_units;
second_units: begin
case(state_2)
OPTION_1:begin
state_1 <= MAIN_MENU;
setting_flag1 <= ~setting_flag1;
end
OPTION_2:begin
alarm_hour <= reg_h_t * 10 + reg_h_u;
alarm_minute <= reg_m_t * 10 + reg_m_u;
alarm_second <= reg_s_t * 10 + reg_s_u;
alarm_switch <= 1'b1;
state_1 <= MAIN_MENU;
end
endcase
end
endcase
end
if (k2) begin
case(time_setting)
hour_tens:begin
reg_h_t <= (reg_h_t + 1) % 3;
if(reg_h_t == 1 && reg_h_u > 3) reg_h_u <= 3;
end
hour_units: reg_h_u <= (reg_h_t == 2) ? ((reg_h_u + 1) % 4) : ((reg_h_u + 1) % 10);
minute_tens: reg_m_t <= (reg_m_t + 1) % 6;
minute_units: reg_m_u <= (reg_m_u + 1) % 10;
second_tens: reg_s_t <= (reg_s_t + 1) % 6;
second_units: reg_s_u <= (reg_s_u + 1) % 10;
endcase
end
end
end
default: state_1 <= MAIN_MENU;
endcase
end
end
计时器,用于更新时间。setting_flag1和setting_flag2搭配使用,可实现在不同always语句里控制时间的手动修改或每秒自动更新。
// 时间计时器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位初始化
counter <= 24'd0;
current_second <= 6'd0;
current_minute <= 6'd0;
current_hour <= 6'd12;
setting_flag2 <= 1'b0;
hour <= 6'd0;
end else begin
if(setting_flag1 != setting_flag2)begin // 手动修改时间
current_hour <= reg_h_t * 10 + reg_h_u;
current_minute <= reg_m_t * 10 + reg_m_u;
current_second <= reg_s_t * 10 + reg_s_u;
setting_flag2 <= ~setting_flag2;
end else begin // 每秒自动计时
if (counter == 24'd11_999_999) begin
// 每秒触发一次
counter <= 24'd0;
// 更新秒
if (current_second == 6'd59) begin
current_second <= 6'd0;
// 更新分钟
if (current_minute == 6'd59) begin
current_minute <= 6'd0;
// 更新小时
current_hour <= (current_hour == 6'd23) ? 6'd0 : (current_hour + 1);
end
else begin
current_minute <= current_minute + 1;
end
end else begin
current_second <= current_second + 1;
end
end else begin
counter <= counter + 24'd1;
end
end
hour <= current_hour;
end
end
闹钟控制,到点且闹钟处于开启状态时发出报警信号
// 闹钟响铃判定
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
alarm <= 1'b0;
end else if ((alarm_switch) && (alarm_hour == current_hour) && (alarm_minute == current_minute) && (alarm_second == current_second)) begin
alarm <= 1'b1;
end else begin
alarm <= 1'b0;
end
end
屏幕显示,根据菜单处理的结果进行不同的显示,以下为核心代码。其余部分参考了电子森林的代码,故不进行展示
MAIN:begin
case(state_1)
MAIN_MENU:begin
cnt_main <= 5'd0;
if(cnt_main >= 5'd26) cnt_main <= 5'd27;//接下来执行空操作,实现数据只刷新一次
else cnt_main <= cnt_main + 1'b1;
case(cnt_main) //MAIN状态
5'd0 : begin state <= INIT; end
// 刷新屏幕
5'd1 : begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
5'd2 : begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
5'd3 : begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
5'd4 : begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
5'd5 : begin y_p <= 8'hb4; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
5'd6 : begin y_p <= 8'hb5; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
5'd7 : begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
5'd8 : begin y_p <= 8'hb7; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
// 时间
6'd9 : begin y_p <= 8'hb0; x_ph <= 8'h11; x_pl <= 8'h00; mem_hanzi_num <= 6'd4 ; state <= CHINESE; end
6'd10: begin y_p <= 8'hb0; x_ph <= 8'h12; x_pl <= 8'h00; mem_hanzi_num <= 6'd6 ; state <= CHINESE; end
// 修改时间
6'd11: begin y_p <= 8'hb2; x_ph <= 8'h11; x_pl <= 8'h00; mem_hanzi_num <= 6'd0 ; state <= CHINESE; end
6'd12: begin y_p <= 8'hb2; x_ph <= 8'h12; x_pl <= 8'h00; mem_hanzi_num <= 6'd2 ; state <= CHINESE; end
6'd13: begin y_p <= 8'hb2; x_ph <= 8'h13; x_pl <= 8'h00; mem_hanzi_num <= 6'd4 ; state <= CHINESE; end
6'd14: begin y_p <= 8'hb2; x_ph <= 8'h14; x_pl <= 8'h00; mem_hanzi_num <= 6'd6 ; state <= CHINESE; end
// 修改闹钟
6'd15: begin y_p <= 8'hb4; x_ph <= 8'h11; x_pl <= 8'h00; mem_hanzi_num <= 6'd0 ; state <= CHINESE; end
6'd16: begin y_p <= 8'hb4; x_ph <= 8'h12; x_pl <= 8'h00; mem_hanzi_num <= 6'd2 ; state <= CHINESE; end
6'd17: begin y_p <= 8'hb4; x_ph <= 8'h13; x_pl <= 8'h00; mem_hanzi_num <= 6'd8 ; state <= CHINESE; end
6'd18: begin y_p <= 8'hb4; x_ph <= 8'h14; x_pl <= 8'h00; mem_hanzi_num <= 6'd10; state <= CHINESE; end
// 闹钟状态:
6'd19: begin y_p <= 8'hb6; x_ph <= 8'h11; x_pl <= 8'h00; mem_hanzi_num <= 6'd8 ; state <= CHINESE; end
6'd20: begin y_p <= 8'hb6; x_ph <= 8'h12; x_pl <= 8'h00; mem_hanzi_num <= 6'd10; state <= CHINESE; end
6'd21: begin y_p <= 8'hb6; x_ph <= 8'h13; x_pl <= 8'h00; mem_hanzi_num <= 6'd12; state <= CHINESE; end
6'd22: begin y_p <= 8'hb6; x_ph <= 8'h14; x_pl <= 8'h00; mem_hanzi_num <= 6'd14; state <= CHINESE; end
6'd23: begin y_p <= 8'hb6; x_ph <= 8'h15; x_pl <= 8'h00; mem_hanzi_num <= 6'd40; state <= CHINESE; end
// 关/开
6'd24: case(alarm_switch)
1'b0:begin y_p <= 8'hb6; x_ph <= 8'h16; x_pl <= 8'h00; mem_hanzi_num <= 6'd18; state <= CHINESE; end
1'b1:begin y_p <= 8'hb6; x_ph <= 8'h16; x_pl <= 8'h00; mem_hanzi_num <= 6'd16; state <= CHINESE; end
endcase
// >菜单选项选中标记<
6'd25:
case(state_2)
OPTION_0:begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; mem_hanzi_num <= 6'd42; state <= CHINESE; end
OPTION_1:begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; mem_hanzi_num <= 6'd42; state <= CHINESE; end
OPTION_2:begin y_p <= 8'hb4; x_ph <= 8'h10; x_pl <= 8'h00; mem_hanzi_num <= 6'd42; state <= CHINESE; end
OPTION_3:begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; mem_hanzi_num <= 6'd42; state <= CHINESE; end
endcase
6'd26: case(state_2)
OPTION_0:begin y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h08; mem_hanzi_num <= 6'd44; state <= CHINESE; end
OPTION_1:begin y_p <= 8'hb2; x_ph <= 8'h15; x_pl <= 8'h08; mem_hanzi_num <= 6'd44; state <= CHINESE; end
OPTION_2:begin y_p <= 8'hb4; x_ph <= 8'h15; x_pl <= 8'h08; mem_hanzi_num <= 6'd44; state <= CHINESE; end
OPTION_3:begin y_p <= 8'hb6; x_ph <= 8'h17; x_pl <= 8'h08; mem_hanzi_num <= 6'd44; state <= CHINESE; end
endcase
//default: state <= IDLE;
endcase
end
MENU_SELECT:begin
case(state_2)
OPTION_0:begin
oled_h_t <= current_hour / 10;
oled_h_u <= current_hour % 10;
oled_m_t <= current_minute / 10;
oled_m_u <= current_minute % 10;
oled_s_t <= current_second / 10;
oled_s_u <= current_second % 10;
end
OPTION_1:begin
oled_h_t <= reg_h_t;
oled_h_u <= reg_h_u;
oled_m_t <= reg_m_t;
oled_m_u <= reg_m_u;
oled_s_t <= reg_s_t;
oled_s_u <= reg_s_u;
end
OPTION_2:begin
oled_h_t <= reg_h_t;
oled_h_u <= reg_h_u;
oled_m_t <= reg_m_t;
oled_m_u <= reg_m_u;
oled_s_t <= reg_s_t;
oled_s_u <= reg_s_u;
end
endcase
if(cnt_main >= 6'd17) cnt_main <= 6'd18;
else cnt_main <= cnt_main + 1'b1;
case(cnt_main)
// 刷新屏幕
6'd0: begin state <= INIT; end
6'd1 : begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
6'd2 : begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
6'd3 : begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
6'd4 : begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
6'd5 : begin y_p <= 8'hb4; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
6'd6 : begin y_p <= 8'hb5; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
6'd7 : begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
6'd8 : begin y_p <= 8'hb7; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; state <= SCAN; end
// 显示时间
6'd9 : begin y_p <= 8'hb3; x_ph <= 8'h12; x_pl <= 8'h00; mem_hanzi_num <= oled_h_t * 2 + 20; state <= CHINESE; end
6'd10: begin y_p <= 8'hb3; x_ph <= 8'h12; x_pl <= 8'h08; mem_hanzi_num <= oled_h_u * 2 + 20; state <= CHINESE; end
6'd11: begin y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h00; mem_hanzi_num <= 6'd40 ; state <= CHINESE; end
6'd12: begin y_p <= 8'hb3; x_ph <= 8'h13; x_pl <= 8'h08; mem_hanzi_num <= oled_m_t * 2 + 20; state <= CHINESE; end
6'd13: begin y_p <= 8'hb3; x_ph <= 8'h14; x_pl <= 8'h00; mem_hanzi_num <= oled_m_u * 2 + 20; state <= CHINESE; end
6'd14: begin y_p <= 8'hb3; x_ph <= 8'h14; x_pl <= 8'h08; mem_hanzi_num <= 6'd40 ; state <= CHINESE; end
6'd15: begin y_p <= 8'hb3; x_ph <= 8'h15; x_pl <= 8'h00; mem_hanzi_num <= oled_s_t * 2 + 20; state <= CHINESE; end
6'd16: begin y_p <= 8'hb3; x_ph <= 8'h15; x_pl <= 8'h08; mem_hanzi_num <= oled_s_u * 2 + 20; state <= CHINESE; end
6'd17: begin
if(state_2 == OPTION_1 || state_2 == OPTION_2)begin // 修改时间时标记正在修改的数位
case(time_setting)
hour_tens: begin x_ph <= 8'h12; x_pl <= 8'h00; end
hour_units: begin x_ph <= 8'h12; x_pl <= 8'h08; end
minute_tens: begin x_ph <= 8'h13; x_pl <= 8'h08; end
minute_units: begin x_ph <= 8'h14; x_pl <= 8'h00; end
second_tens: begin x_ph <= 8'h15; x_pl <= 8'h00; end
second_units: begin x_ph <= 8'h15; x_pl <= 8'h08; end
endcase
y_p <= 8'hb5; mem_hanzi_num <= 6'd46 ; state <= CHINESE;
end
end
endcase
end
endcase
end
循环彩灯控制:以1/3秒作为一个周期,5秒钟共15个周期,根据周期数改变rgb三色,取值范围为0到5。
小时彩灯控制:根据时间取0或7两个不同的值,实现上午用白灯表示小时,下午用紫灯表示小时
if (alarm_active) begin
if (timer < 15) begin
// 报警循环彩灯
for (i = 0; i < 12; i = i + 1) begin
r = (i + timer) % 6;
g = (r + 2 < 6) ? (r + 2) : (r - 4);
b = (g + 2 < 6) ? (r + 2) : (r - 4);
led_data[i] <= {g, r, b};
end
end else begin
alarm_active <= 1'b0;
end
end else begin
// 按小时更新彩灯
for (i = 0; i < 12; i = i + 1) begin
g = ((i < (hour % 12) && hour < 12) || (i >= (hour % 12) && hour > 11)) ? 7 : 0;
r = (i < (hour % 12) || hour > 11) ? 7 : 0;
b = (i < (hour % 12) || hour > 11) ? 7 : 0;
led_data[i] <= {g, r, b};
end
end
蜂鸣器控制代码,检测到报警信号时激活,计时5秒钟,并发出警报,声音大致为(滴~滴~滴~滴~~~)*5
// 报警激活检测
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
alarm_active <= 0;
total_timer <= 0;
end else begin
if(alarm && !alarm_active) begin
alarm_active <= 1'b1;
total_timer <= 0;
end
if(alarm_active) begin
total_timer <= total_timer + 1;
if(total_timer >= CLK_FREQ*5 -1) // 5秒后关闭
alarm_active <= 1'b0;
end
end
end
// 主控制逻辑
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
phase_timer <= 0;
cycle_cnt <= 0;
phase <= 0;
pwm_cnt <= 0;
beep <= 0;
end else if(alarm_active) begin
case(phase)
0: begin // 发声阶段
beep <= (pwm_cnt <= DOH_CNT_MAX) ? 1 : 0;
pwm_cnt <= (pwm_cnt >= (DOH_CNT_MAX*2)) ? 0 : pwm_cnt + 1;
phase_timer <= phase_timer + 1;
if(phase_timer >= SOUND_CYCLES -1) begin
phase <= 1;
phase_timer <= 0;
end
end
1: begin // 静音阶段
beep <= 0;
phase_timer <= phase_timer + 1;
// 判断静音类型
if(cycle_cnt < CYCLE_NUM-1) begin
if(phase_timer >= GAP_CYCLES -1) begin
phase <= 0;
cycle_cnt <= cycle_cnt + 1;
phase_timer <= 0;
end
end else begin
if(phase_timer >= FINAL_GAP_CYC -1) begin
phase <= 0;
cycle_cnt <= 0;
phase_timer <= 0;
end
end
end
endcase
end else begin
beep <= 0;
phase_timer <= 0;
cycle_cnt <= 0;
phase <= 0;
pwm_cnt <= 0;
end
end
5.功能展示图及说明
左键k1为主菜单键,在修改时间时可作为返回键使用。右键k2为切换选项/数字键,下键run为确认键。
主菜单界面,选中的选项用“>选项<”标注
屏幕显示时间上午十点,同时亮起10个白灯。为便于观查彩灯颜色,将镜头进光量调低展示。
下午10点在一圈白灯基础上亮起10个紫灯
闹钟响时时不同颜色彩灯旋转循环亮起持续5秒
修改时间时,处于修改状态的部分下方会用“-”标注,k1取消修改并返回主菜单,k2修改(数值+1),run确认并修改下一位。最后一位数字修改完成时按下run即可完成修改并应用。
图片能展示的功能有限,具体功能请参考视频。
6.项目中遇到的难题和解决方法
时间每秒需要更新一次,而用户手动更新时间同样需要对时间进行修改,所以时间的更新会产生冲突,代码报错。自动更新需要使用计时器每秒钟更新一次,而手动修改时间需要特定的按键操作进入特定状态才能触发,二者因为更新逻辑不同(在不同always语句)不能直接使用if-else语句来进行控制。
最终解决方法为引入两个控制标记,两个标记初始值相同,当需要手动修改时标记1取反,修改完后标记2取反,手动修改发生的逻辑为标记1与标记2不相等,这样即可实现使用if-else语句在不同always语句控制同一个变量而不报错。
7.心得体会
这是我第一次使用verilog代码进行编程,在编程过程中遇到了很多问题,如不了解代码与硬件的联系、同步时序的逻辑等。编写OLED屏幕,要么直接不显示,要么就是显示了但按下按键时没有任何反应。编写彩灯代码时,差点把灯搞坏不说,眼睛都快闪瞎了,最后终于学会把r,g,b三种颜色赋值到10以内而不是255,降低更新频率。好在最终还是完成了任务。
资源占用情况