一、项目描述
1.项目需求
设计一个基于基于iCE40UP5K的FPGA学习平台的可定时的音乐时钟。
通过核心板上的FPGA产生时钟。
时间在OLED12864显示屏上显示当前时间的小时、分钟、秒。
并且在12个ws2812彩灯上根据当前小时数亮起对应的彩灯。
可以设置时间,通过拓展板上的按键设置时间,到该时间点即(彩灯闪烁 + 音频播放),持续5秒钟时间,音乐播放的功能通过拓展板上的蜂鸣器实现。
2.需求分析
根据项目需求,该项目可以分解为以下功能:
(1)时钟分频:
将芯片板的12MHZ系统时钟分频为1HZ时钟。
(2)生成时间:
通过1MHZ时钟脉冲计数产生时间,小时、分钟、秒。
(3)设置闹钟时间:
通过PICO拓展板上的按键,实现对当前时间、闹钟时间的设置。
(4)彩灯显示:
用12个ws2812彩灯显示当前的小时数。
(5)当前时间显示:
将当前时间显示在OLED12864屏幕上
(6)闹钟效果:
当闹钟时间到时彩灯闪烁以及PICO拓展板上的蜂鸣器发出音乐,时间为5秒。
3.功能展示图
二、功能实现
初审退回后的修改地方
- 使用k1、k2键能修改当前时间和闹钟时间的小时、分钟、秒:k1按键用于切换状态,k2按键用于增加时间,每按一下对应调整时间位加1。当上电复位后默认进入到调整时间状态,此时默认调整当前时间的小时位,每按一下k1按键,状态切换如下:
当前时间的小时->当前时间的分钟->当前时间的秒 ->闹钟时间的小时 -> 闹钟时间的分钟-> 闹钟时间的秒->正常计时模式
正常计时模式下随1HZ时钟沿秒数加1。在正常模式下按k1键将进入调整时间状态。
- OLED部分数位显示异常问题修复:通过修改代码,将OLED显示问题解决,目前所有数位在验证下可以正常显示。
1.实现方式
使用基于ICE40UP5K的FPGA学习平台以及PICO40的拓展板,使用LAttice Radiant软件和verilog语言实现本项目。根据需求提出实现方式:
(1)产生1HZ时钟脉冲:
在time_cal模块中通过12MHZ的系统时钟脉冲,使用计数器计数12_000_000,当计数器满时输出1,其他时间输出0来产生1HZ时钟脉冲。
(2)产生时间:
在time_cal模块通过1HZ的脉冲作为时钟用于计数,每到来一个时钟沿,秒的计数位加1,满60清零;分钟位加1,满60清零;小时位加1,满12清零。
(3)设置闹钟时间:
在adjust_time模块中通过PICO扩展板的k1、k2按键的信号调整当前时间、闹钟时间的小时、分钟、秒,k1按键用于切换状态,k2按键用于增加时间,每按一下对应调整时间位加1。当上电复位后默认进入到调整时间状态,此时默认调整当前时间的小时位,每按一下k1按键,状态切换如下:
当前时间的小时->当前时间的分钟->当前时间的秒 ->闹钟时间的小时 -> 闹钟时间的分钟-> 闹钟时间的秒->正常计时模式,正常计时模式下随1HZ时钟沿秒数加1。在正常模式下按k1键将进入调整时间状态
(4)控制彩灯亮灭效果:
在WS2812模块中通过时间的小时位hour和标志闹钟时间到来的信号alarm_trigger控制彩灯亮灭的效果。
根据小时位hour选择对应的彩灯亮起绿色,其他彩灯熄灭。 若设置的闹钟时间到来则alarm_trigger为1,所有12颗彩灯闪烁红色持续5秒,通过计数器计数6_000_000即半秒,使彩灯每0.5秒在红色和熄灭状态切换,达到闪烁红光的效果。
(5)显示时间:
在led模块中通过状态机的方式定义IDLE、INIT、MAIN、SCAN、TIME、WRITE、DELAY状态来控制OLED显示数据。
当时钟沿到来或上电复位时,首先由IDLE状态进入INIT状态,进行命令写入等初始化操作;当时钟沿到来后进入MAIN状态进行刷屏和坐标赋值,通过SCAN或TIME状态从mem中读入要写入的数据所代表的控制8*8点阵亮灭情况的字节表,最后通过通过WRITE状态使用SPI协议向OLED屏幕传输数据,经过DELAY状态延时。
(6)播放音乐:
在music模块中通过闹钟时间到来的标志信号alarm_trigger控制音乐播放,当alarm_trigger为1使扩展板上的蜂鸣器以不同频率进行翻转来发出声音,持续5秒。
2.功能框图
3.流程图
3.代码及说明
1.时钟分频
// 定义1Hz时钟脉冲所需的计数值
// 12MHz / 1Hz = 12,000,000,所以计数器需要从0数到11,999,999
parameter N = 12000000;
always @(posedge clk or negedge rst_n) begin
if (~rst_n) begin
counter <= 0; // 复位时计数器清零
second_tick <= 0; // 复位时无脉冲
end else begin
if (counter == N - 1) begin
counter <= 0; // 达到1Hz时计数器清零
second_tick <= 1; // 产生1Hz脉冲
end else begin
counter <= counter + 1;
second_tick <= 0; // 不是1Hz脉冲时,输出为0
end
end
end
2.计时
// 每当1Hz时钟脉冲到来时更新时间
always @(posedge second_tick or negedge rst_n) begin
if (~rst_n) begin
sec <= 0; // 复位时秒数为0
min <= 0; // 复位时分钟为0
hour <= 0; // 复位时小时为0
end else begin
// 秒数自增
if (sec == 59) begin
sec <= 0;
// 分钟自增
if (min == 59) begin
min <= 0;
// 小时自增
if (hour == 11) begin
hour <= 0; // 小时归零
end else begin
hour <= hour + 1;
end
end else begin
min <= min + 1;
end
end else begin
sec <= sec + 1;
end
end
end
3.控制彩灯亮灭及颜色
// 根据小时设置对应LED颜色
always @(posedge clk or negedge rst) begin
if (!rst) begin
ledcolor[0] <= LED_OFF;
ledcolor[1] <= LED_OFF;
ledcolor[2] <= LED_OFF;
ledcolor[3] <= LED_OFF;
ledcolor[4] <= LED_OFF;
ledcolor[5] <= LED_OFF;
ledcolor[6] <= LED_OFF;
ledcolor[7] <= LED_OFF;
ledcolor[8] <= LED_OFF;
ledcolor[9] <= LED_OFF;
ledcolor[10] <= LED_OFF;
ledcolor[11] <= LED_OFF;
end
else if(alarm_trigger)begin
if(flash_cnt < N) flash_cnt <= flash_cnt + 1;
else if(flash_cnt >= N)begin
flash_cnt <=0;
//十二个彩灯闪烁
ledcolor[0] <= (ledcolor[0] == LED_OFF) ? LED_RED : LED_OFF;
ledcolor[1] <= (ledcolor[1] == LED_OFF) ? LED_RED : LED_OFF;
ledcolor[2] <= (ledcolor[2] == LED_OFF) ? LED_RED : LED_OFF;
ledcolor[3] <= (ledcolor[3] == LED_OFF) ? LED_RED : LED_OFF;
ledcolor[4] <= (ledcolor[4] == LED_OFF) ? LED_RED : LED_OFF;
ledcolor[5] <= (ledcolor[5] == LED_OFF) ? LED_RED : LED_OFF;
ledcolor[6] <= (ledcolor[6] == LED_OFF) ? LED_RED : LED_OFF;
ledcolor[7] <= (ledcolor[7] == LED_OFF) ? LED_RED : LED_OFF;
ledcolor[8] <= (ledcolor[8] == LED_OFF) ? LED_RED : LED_OFF;
ledcolor[9] <= (ledcolor[9] == LED_OFF) ? LED_RED : LED_OFF;
ledcolor[10] <= (ledcolor[10] == LED_OFF) ? LED_RED : LED_OFF;
ledcolor[11] <= (ledcolor[11] == LED_OFF) ? LED_RED : LED_OFF;
end
end
else begin
// 确保对应小时的LED颜色被正确设置
ledcolor[0] <= (hour == 1) ? LED_GREEN : LED_OFF;
ledcolor[1] <= (hour == 2) ? LED_GREEN : LED_OFF;
ledcolor[2] <= (hour == 3) ? LED_GREEN : LED_OFF;
ledcolor[3] <= (hour == 4) ? LED_GREEN : LED_OFF;
ledcolor[4] <= (hour == 5) ? LED_GREEN : LED_OFF;
ledcolor[5] <= (hour == 6) ? LED_GREEN : LED_OFF;
ledcolor[6] <= (hour == 7) ? LED_GREEN : LED_OFF;
ledcolor[7] <= (hour == 8) ? LED_GREEN : LED_OFF;
ledcolor[8] <= (hour == 9) ? LED_GREEN : LED_OFF;
ledcolor[9] <= (hour == 10) ? LED_GREEN : LED_OFF;
ledcolor[10] <= (hour == 11) ? LED_GREEN : LED_OFF;
ledcolor[11] <= (hour == 0) ? LED_GREEN : LED_OFF;
end
end
4.OLED显示时间
MAIN:begin
if(cnt_main >= 5'd12) cnt_main <= 5'd9;//接下来执行空操作,实现数据只刷新一次
else if(cnt_main > 0 || (cnt_main == 0 && init_done==1'b1)) cnt_main <= cnt_main + 1'b1;
case(cnt_main) //MAIN状态
5'd0 : begin
if(init_done==1'b0) state <= INIT;
end
5'd1 : begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= 10'd30;state <= SCAN; end //1-8将屏幕刷成空白
5'd2 : begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= 10'd30;state <= SCAN; end
5'd3 : begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= 10'd30;state <= SCAN; end
5'd4 : begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= 10'd30;state <= SCAN; end
5'd5 : begin y_p <= 8'hb4; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= 10'd30;state <= SCAN; end
5'd6 : begin y_p <= 8'hb5; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= 10'd30;state <= SCAN; end
5'd7 : begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= 10'd30;state <= SCAN; end
5'd8 : begin y_p <= 8'hb7; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= 10'd30;state <= SCAN; end
//打印第1行“time”
5'd9 : begin y_p <= 8'hb1; x_ph <= 8'h12; x_pl <= 8'h08; num <= 5'd4; char <= 10'd18;state <= SCAN; end
//打印第2行当前时间
5'd10 : begin
y_p <= 8'hb3; x_ph <= 8'h11; x_pl <= 8'h08; //
hour_shi <= chour_shi;hour_ge<=chour_ge; min_shi<=cmin_shi;min_ge<=cmin_ge;sec_shi<=csec_shi;sec_ge<=csec_ge;
state <= TIME;
end
//打印第3行“set”
5'd11 : begin y_p <= 8'hb5; x_ph <= 8'h12; x_pl <= 8'h08; num <= 5'd3; char <= 10'd22;state <= SCAN; end
//打印第4行设置时间
5'd12 : begin
y_p <= 8'hb7; x_ph <= 8'h11; x_pl <= 8'h08;//
hour_shi <= shour_shi;hour_ge<=shour_ge; min_shi<=smin_shi;min_ge<=smin_ge;sec_shi<=ssec_shi;sec_ge<=ssec_ge;
state <= TIME;
end
default:
begin
state <= IDLE;
end //如果你需要动态刷新一些信息,此行应该取消注释
endcase
// cnt_main <= cnt_main + 1'b1;
end
5.音乐播放
always@(posedge clk_in or negedge rst_n_in) begin
if(!rst_n_in) begin
nummax <= nummax1;
num <= 6'd0;
delay_cnt<=64'd0;
end
else if(num <= nummax) begin
if(delay_cnt <= 64'd1500000) begin
delay_cnt <= delay_cnt + 1'd1;
case(alarm_trigger)
1'd0: tone <= mem0;
default: tone <= mem1[num];
endcase
end
else begin
delay_cnt <= 64'd0;
num <= num + 1'd1;
end
end
else begin
num <= 7'd0;
end
end
4.FPGA资源占用情况
5.遇到的问题
实现期间主要遇到的问题是OLED屏幕显示出现问题,包括将示例代码烧录到板子上仍然显示出现问题,本来应该为熄灭区域出现竖线,显示时间不全等
1.代码跳过初始化状态
MAIN:begin
if(cnt_main >= 5'd14) cnt_main <= 5'd15;//接下来执行空操作,实现数据只刷新一次
else cnt_main <= cnt_main + 1'b1;
case(cnt_main) //MAIN状态
5'd0 : begin state <= INIT; end
在检查代码逻辑后发现在示例代码中,在时钟沿到来时或上电复位时IDLE状态会将cnt_main变量赋值为0后进入MAIN状态,由于先执行if-else逻辑,会将cnt_main加1变成1会才进入case语句的匹配,导致跳过了case0即进入INIT状态,导致没能进行OLED屏幕初始化,这是可能导致显示出现的原因。后来通过修改逻辑实现了屏幕初始化。
2.在不同坐标位置显示文字可能导致显示问题
在解决初始化问题后,仍会出现显示出现花点并且部分数字显示不全的情况,在检查多遍无果后尝试修改文字的显示位置即坐标,发现显示效果有所改善,后来在不断调整坐标后达到将预定文字显示出来且屏幕没有花点出现的效果。但仍存在当前时间的秒数的个位只能显示0或1即在0、1变化,不能显示数字2-9,但其他位数字显示正常,这一问题目前还无法解决。
6.心得与展望
复习巩固了FPGA开发板知识,并且完成从verilog语言编程到烧录硬件芯片板的从0到实现的全过程。希望以后可以尝试使用手中的ICE40芯片板实现其他项目功能及深入学习FPGA开发知识。