一、项目介绍
本项目基于iCE40UP5K的FPGA学习平台设计可定时的音乐时钟。任务的要求为:
1.使用扩展板上的12颗彩灯对应于12个小时;
2.核心板上的FPGA产生时钟,在OLED显示屏上通过模拟或者数字的方式显示当前的时间 - 小时、分、秒;
3.将“小时”的信息通过12颗彩灯来显示,效果自行设计;
4.具有定时的功能,通过扩展板上的按键设置时间,到该时间点即(彩灯闪烁 + 音频播放),持续5秒钟时间;
5.音频播放通过扩展板上的蜂鸣器来实现。
二、硬件介绍
2.1 核心板
核心板为基于Lattice的FPGA - iCE40UP5K,板上有一颗R、G、B三色LED,分别连接FPGA的三根专用于驱动LED的管脚39、40、41,可以用于状态显示及数字逻辑实验;一个RESET按键,用于对RISC-V系统进行复位;总计28个IO用于扩展使用;板上晶体振荡器时钟产生12MHz供FPGA工作,FPGA可以通过内部锁相环工作于48MHz。
2.2 扩展板
扩展板上包括:2个按键输入;4个单色LED;12个WS2812B RGB三色灯;1个姿态传感器;1个128*64 OLED显示屏;1个蜂鸣器等。
2.3 板卡特性
具有灵活的逻辑架构,拥有2800或5280个4输入LUT、自定义I/O、多达80 Kb和1Mb的嵌入式存储器;
超低功耗的先进工艺,睡眠电流低至75 uA,工作电流仅为1-10mA;
使用DSP模块实现高性能信号处理,支持乘法和累加功能;
神经网络软IP和编译器实现灵活的机器学习/人工智能应用。
三、方案框图和项目设计思路
上图展示了项目代码的组织结构,top为主模块;debounce为按键消抖模块;ws2812为12个彩灯显示模块;oled为文字显示模块;key为按键设置模块;buzzer_module为蜂鸣器定义模块;time_cal为时间计算模块,其中又用到了time_difference时间差计算与beeper_ctrl蜂鸣器控制两个模块的内容。
四、软件流程图及关键代码介绍
//当前时间设置模式
always @(posedge clk or negedge rst) begin
if (!rst) begin
counter <= 0; sec <= 0; min <= 0; hour <= 0; // 初始默认为0时0分0秒
curr_hour0 <= 0; curr_hour1 <= 0; curr_min0 <= 0; curr_min1 <= 0;
curr_sec0 <= 0; curr_sec1 <= 0;
end else begin
if (counter == MAX_1s - 1) begin counter <= 0;
if(key2_pulse)begin
case(oled_mode) // 根据模式调整当前时间
3:begin
if (sec == 58) begin sec <= 0;
if (min == 59) begin min <= 0;
hour <= (hour == 11) ? 0 : hour + 1;
end else min <= min + 1;
end else sec <= sec + 2;
end
2:begin
if (sec == 59) begin sec <= 0;
if (min == 58) begin min <= 0;
hour <= (hour == 11) ? 0 : hour + 1;
end else min <= min + 1;
end else begin
sec <= sec + 1;
min <= min + 1;
end
end
1:begin
if (sec == 59) begin sec <= 0;
if (min == 59) begin min <= 0;
hour <= (hour == 10) ? 0 : hour + 1;
end else min <= min + 1;
end else begin
sec <= sec + 1;
hour <= hour + 1;
end
end
default:begin
if (sec == 59) begin sec <= 0;
if (min == 59) begin min <= 0;
hour <= (hour == 11) ? 0 : hour + 1;
end else min <= min + 1;
end else sec <= sec + 1;
end
endcase
end else begin
if (sec == 59) begin sec <= 0;
if (min == 59) begin min <= 0;
hour <= (hour == 11) ? 0 : hour + 1;
end else min <= min + 1;
end else sec <= sec + 1;
end
end else begin counter <= counter + 1;
if (key2_pulse == 1) begin
case (oled_mode)
3: begin
if (sec == 59) begin sec <= 0;
if (min == 59) begin min <= 0;
hour <= (hour == 11) ? 0 : hour + 1;
end else min <= min + 1;
end else sec <= sec + 1;
end
2: begin
if (min == 59) begin min <= 0;
hour <= (hour == 11) ? 0 : hour + 1;
end else min <= min + 1;
end
1: begin
hour <= (hour == 11) ? 0 : hour + 1;
end
default: begin
end
endcase
end
end
curr_hour1 <= hour / 10; curr_hour0 <= hour % 10;
curr_min1 <= min / 10; curr_min0 <= min % 10;
curr_sec1 <= sec / 10; curr_sec0 <= sec % 10;
end
end
// 闹钟时间设置模式(模式4-6)
always @(posedge clk or negedge rst) begin
if (!rst) begin
next_sec <= 0;next_min <= 0;next_hour <= 0;
next_hour0 <= 0;next_hour1 <= 0;next_min0 <= 0;next_min1 <= 0;
next_sec0 <= 0;next_sec1 <= 0;
end else begin
if (key2_pulse == 1) begin
case (oled_mode)
6: begin // 模式6:设置闹钟秒
if (next_sec == 59) begin next_sec <= 0;
if (next_min == 59) begin next_min <= 0;
next_hour <= (next_hour == 11) ? 0 : next_hour + 1;
end else next_min <= next_min + 1;
end else next_sec <= next_sec + 1;
end
5: begin // 模式5:设置闹钟分
if (next_min == 59) begin next_min <= 0;
next_hour <= (next_hour == 11) ? 0 : next_hour + 1;
end else next_min <= next_min + 1;
end
4: begin // 模式4:设置闹钟小时
next_hour <= (next_hour == 11) ? 0 : next_hour + 1;
end
default: begin
end
endcase
end
next_hour1 <= next_hour / 10;next_hour0 <= next_hour % 10;
next_min1 <= next_min / 10;next_min0 <= next_min % 10;
next_sec1 <= next_sec / 10;next_sec0 <= next_sec % 10;
end
end
上面展示了time_cal时间计算模块的部分代码,主要功能是实现现在时间与闹钟时间的设置。代码的重点是实现时间的时分秒之间的转换,初始的默认时间均为0时0分0秒。
mem_chinese[107] = {8'h04,8'h84,8'h84,8'hFC,8'h84,8'h84,8'h00,8'hFE};/*"现",0*/
mem_chinese[108] = {8'h02,8'h02,8'hF2,8'h02,8'h02,8'hFE,8'h00,8'h00};
mem_chinese[109] = {8'h20,8'h60,8'h20,8'h1F,8'h10,8'h90,8'h40,8'h23};
mem_chinese[110] = {8'h18,8'h06,8'h01,8'h7E,8'h80,8'h83,8'hE0,8'h00};
mem_chinese[111] = {8'h08,8'h08,8'h88,8'hC8,8'h38,8'h0C,8'h0B,8'h08};/*"在",1*/
mem_chinese[112] = {8'h08,8'hE8,8'h08,8'h08,8'h08,8'h08,8'h08,8'h00};
mem_chinese[113] = {8'h02,8'h01,8'h00,8'hFF,8'h40,8'h41,8'h41,8'h41};
mem_chinese[114] = {8'h41,8'h7F,8'h41,8'h41,8'h41,8'h41,8'h40,8'h00};
mem_chinese[115] = {8'h00,8'hFC,8'h84,8'h84,8'h84,8'hFC,8'h00,8'h10}; // 时
mem_chinese[116] = {8'h10,8'h10,8'h10,8'hFF,8'h10,8'h10,8'h00,8'h00};
mem_chinese[117] = {8'h00,8'h3F,8'h10,8'h10,8'h10,8'h3F,8'h00,8'h00};
mem_chinese[118] = {8'h01,8'h06,8'h40,8'h80,8'h7F,8'h00,8'h00,8'h00};
mem_chinese[119] = {8'h00,8'hF8,8'h01,8'h06,8'h00,8'hF0,8'h12,8'h12}; // 间
mem_chinese[120] = {8'h12,8'hF2,8'h02,8'h02,8'h02,8'hFE,8'h00,8'h00};
mem_chinese[121] = {8'h00,8'hFF,8'h00,8'h00,8'h00,8'h1F,8'h11,8'h11};
mem_chinese[122] = {8'h11,8'h1F,8'h00,8'h40,8'h80,8'h7F,8'h00,8'h00};
mem_chinese[123] = {8'h00,8'hF8,8'h01,8'h22,8'h20,8'h22,8'h2A,8'hF2};/*"闹",0*/
mem_chinese[124] = {8'h22,8'h22,8'h22,8'h22,8'h02,8'hFE,8'h00,8'h00};
mem_chinese[125] = {8'h00,8'hFF,8'h00,8'h00,8'h1F,8'h01,8'h01,8'h7F};
mem_chinese[126] = {8'h09,8'h11,8'h0F,8'h40,8'h80,8'h7F,8'h00,8'h00};
mem_chinese[127] = {8'h20,8'h10,8'h2C,8'hE7,8'h24,8'h24,8'h00,8'hF0};/*"钟",1*/
mem_chinese[128] = {8'h10,8'h10,8'hFF,8'h10,8'h10,8'hF0,8'h00,8'h00};
mem_chinese[129] = {8'h01,8'h01,8'h01,8'h7F,8'h21,8'h11,8'h00,8'h07};
mem_chinese[130] = {8'h02,8'h02,8'hFF,8'h02,8'h02,8'h07,8'h00,8'h00};
上面展示了部分项目中用到的汉字字库代码,其中字模的编码是通过教程中的PCtoLCD2002软件生成的,打开软件设置好模式要求后,输入想要的汉字,便可以生成相应的字模编码,如下图所示。
always@(tone) begin
case(tone)
5'd1: time_end = 16'd22935; //L1,
5'd2: time_end = 16'd20428; //L2,
5'd3: time_end = 16'd18203; //L3,
5'd4: time_end = 16'd17181; //L4,
5'd5: time_end = 16'd15305; //L5,
5'd6: time_end = 16'd13635; //L6,
5'd7: time_end = 16'd12147; //L7,
5'd8: time_end = 16'd11464; //M1,
5'd9: time_end = 16'd10215; //M2,
5'd10: time_end = 16'd9100; //M3,
5'd11: time_end = 16'd8589; //M4,
5'd12: time_end = 16'd7652; //M5,
5'd13: time_end = 16'd6817; //M6,
5'd14: time_end = 16'd6073; //M7,
5'd15: time_end = 16'd5740; //H1,
5'd16: time_end = 16'd5107; //H2,
5'd17: time_end = 16'd4549; //H3,
5'd18: time_end = 16'd4294; //H4,
5'd19: time_end = 16'd3825; //H5,
5'd20: time_end = 16'd3408; //H6,
5'd21: time_end = 16'd3036; //H7,
default:time_end = 16'd65535;
endcase
end
always@(posedge rst_n_in) begin//音符表
note[0] = {5'd8};
note[1] = {5'd9};
note[2] = {5'd10};
note[3] = {5'd11};
note[4] = {5'd12};
note[5] = {5'd13};
note[6] = {5'd14};
note[7] = {5'd15};
//2
note[8] = {5'd16};
note[9] = {5'd15};
note[10] = {5'd13};
note[11] = {5'd16};
note[12] = {5'd17};
note[13] = {5'd16};
note[14] = {5'd15};
note[15] = {5'd12};
note[16] = {5'd16};
note[17] = {5'd17};
//3
note[18] = {5'd16};
note[19] = {5'd15};
note[20] = {5'd13};
note[21] = {5'd15};
note[22] = {5'd16};
note[23] = {5'd13};
note[24] = {5'd17};
note[25] = {5'd15};
note[26] = {5'd16};
note[27] = {5'd17};
note[28] = {5'd19};
//4
note[29] = {5'd19};
note[30] = {5'd17};
note[31] = {5'd17};
note[32] = {5'd16};
note[33] = {5'd15};
note[34] = {5'd16};
note[35] = {5'd15};
note[36] = {5'd13};
note[37] = {5'd15};
note[38] = {5'd16};
note[39] = {5'd17};
//5
note[40] = {5'd19};
note[41] = {5'd0};
note[42] = {5'd9};
note[43] = {5'd8};
note[44] = {5'd6};
//6
note[45] = {5'd8};
note[46] = {5'd8};
note[47] = {5'd6};
note[48] = {5'd8};
note[49] = {5'd8};
note[50] = {5'd6};
note[51] = {5'd8};
note[52] = {5'd6};
note[53] = {5'd5};
note[54] = {5'd0};
note[55] = {5'd9};
note[56] = {5'd8};
note[57] = {5'd6};
//7
note[58] = {5'd8};
note[59] = {5'd8};
note[60] = {5'd6};
note[61] = {5'd8};
note[62] = {5'd8};
note[63] = {5'd10};
note[64] = {5'd9};
note[65] = {5'd8};
note[66] = {5'd8};
note[67] = {5'd0};
note[68] = {5'd5};
note[69] = {5'd6};
note[70] = {5'd10};
end
上面展示了buzzer_module蜂鸣器模块的部分代码,其中参考了板卡开源平台中音频播放项目的一些实现方式。模块定义了音调、节拍等信息,构建了一一对应的音符表与节拍表,来实现音频播放。
五、功能展示图及说明
向板卡中拖入项目的rbt文件后,按下运行键,板卡默认显示当前时间与闹钟时间均为0时0分0秒,ws2812彩灯显示当前时间为0时也即12时的位置。
按下左边按键,可以切换模式,分别设置现在时间与闹钟的时、分、秒。
用右边的按键来调整数字,可以看到,当调整现在时间的小时的时候,彩灯会如钟表一样显示出当前的小时。
左边按键切换到闹钟设置模式后,同样用右面按键来设置闹钟具体时间。设置好后,切换回显示模式,等待闹钟响起。
可以看到,到了设定的时间后,蜂鸣器产生音频的同时伴随着彩灯的闪烁。音频结束后,按下右边按键可结束彩灯闪烁,进行下一次设置;按下运行键可复位至初始状态。
六、项目中遇到的难题和解决方法
由于我之前没有做过这种难度的项目,一开始确实会有种无从下手的感觉。为此,我首先仔细分析了项目实现的要求,在将任务分解成几个不同的模块之后逐一完成。
具体实现的过程中,我认为闹钟逻辑的编写与OLED的显示最为困难。为了实现闹钟的播放,我通过计算现在时间与闹钟时间的时间差来控制蜂鸣器的音频。在OLED显示中,字库的建构与信息的显示也用了很长时间来编写。在一些开源代码的参考以及大语言模型的代码生成功能辅助下,我得以解决大部分问题,实现了任务要求的基本功能。
然而,项目还存在着一些不足之处,比如音频播放时的控制还不完善。后续我也会针对特定问题来继续优化项目。
七、对本次活动的心得体会
本次寒假练活动让我受益匪浅。通过编写项目,我掌握了Verilog语言和FPGA的开发流程,理解了硬件思维与软件编程的差异。活动中调试时序问题和代码编写的过程极大提升了我的分析能力。虽然初期因不熟悉相关细节而遇到了很多阻碍,但开源平台等多种资源的帮助让我最终完成了项目。这次经历不仅让我巩固了数电知识,更让我体会到了硬件开发的乐趣,为后续的发展打下了坚实基础。