最终需要实现的功能
-
实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒;
-
实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;
-
定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致;
-
PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;
-
音频文件播放完毕,OLED开始更新时间信息和当前的温度信息;
分析功能
实现思路
- 实现串口收发
- 实现ds18b20读取温度
- 实现‘大四加三’法完成温度数据转换为bcd码
- 实现串口发送当前温度
- 实现oled初始化与显示
- 实现显示缓存,这里我直接使用寄存器作为显示缓存
- 实现时钟计时
- 实现时间调节
- 实现按键消抖
- 实现pwm输出
- 实现uart数据转换为蜂鸣器控制数据
- 完成显示uart数据、发送音乐数据上位机
- 综合,整理。
关键代码
1.很多模块是触发式的,如果使用外部电平作为使能的话不利于编程。这里我使用了捕捉上升沿,产生一个时钟周期的脉冲信号的方式产生内部的使能信号。
//上升沿,得到一个时钟周期的脉冲信号
assign en_flag = (~tx_en_d1) & tx_en_d0;
//对发送使能信号tx_en延迟两个时钟周期
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx_en_d0 <= 1'b0;
tx_en_d1 <= 1'b0;
end
else begin
tx_en_d0 <= tx_en;
tx_en_d1 <= tx_en_d0;
end
end
2.串口向上位机输出的时候很难利用状态去连续发送一串字符。这里我使用了时隙流水的方式。其中cnt是主12MHz时钟上升沿产生的计数。
always @ (posedge clk or negedge rst_n) begin
if(!rst_n)
uart_en <= 1'b0;
else if(cnt[7:0] == 8'd100 && uart_en)
uart_en <= 1'b0;
else begin
case(cnt) // 写不同命令的字节
20'd10_000: begin uart_din <= 8'hB5; uart_en <= 1'b1;end //当
20'd20_000: begin uart_din <= 8'hB1; uart_en <= 1'b1;end
20'd30_000: begin uart_din <= 8'hC7; uart_en <= 1'b1;end //前
32'd40_000: begin uart_din <= 8'hB0; uart_en <= 1'b1;end
20'd50_000: begin uart_din <= 8'hCE; uart_en <= 1'b1;end //温
20'd60_000: begin uart_din <= 8'hC2; uart_en <= 1'b1;end
20'd70_000: begin uart_din <= 8'hB6; uart_en <= 1'b1;end //度
20'd80_000: begin uart_din <= 8'hC8; uart_en <= 1'b1;end
20'd90_000: begin uart_din <= 8'h3A; uart_en <= 1'b1;end //:
20'd100_000: begin uart_din <= 8'h30+temp_bcd[15:12]; uart_en <= 1'b1;end
20'd110_000: begin uart_din <= 8'h30+temp_bcd[11:8]; uart_en <= 1'b1;end
20'd120_000: begin uart_din <= 8'h2E; uart_en <= 1'b1;end //.
20'd130_000: begin uart_din <= 8'h30+temp_bcd[7:4]; uart_en <= 1'b1;end
20'd140_000: begin uart_din <= 8'h0D; uart_en <= 1'b1;end //\r
20'd150_000: begin uart_din <= 8'h0A; uart_en <= 1'b1;end //\n
default:begin uart_din <= uart_din; uart_en <= uart_en;end
endcase
end
end
3.这块小脚丫fpga里面是没有乘法器跟除法器的,对乘法运算需要转换,而除法运算主要是用于十六进制数转为十进制数,这里我使用的'大四加三'法
assign data2 = (data1<<2) + (data1<<1) + (data1>>2);// data1*6.25
关于'大四加三'法,可以看下面的文章
4.verilog里只能支持一维数组作为传参,但是一维数组作为显存不好操作,这里我利用的下面两个宏作为一维数据跟二维数组的转换
// unpack 1D-array to 2D-array
`define UNPACK_ARRAY(PK_WIDTH,PK_LEN,PK_DEST,PK_SRC) \
generate \
genvar unpk_idx; \
for (unpk_idx=0; unpk_idx<(PK_LEN); unpk_idx=unpk_idx+1) \
begin \
assign PK_DEST[unpk_idx][((PK_WIDTH)-1):0] = PK_SRC[((PK_WIDTH)*unpk_idx+(PK_WIDTH-1)):((PK_WIDTH)*unpk_idx)]; \
end \
endgenerate
// pack 2D-array to 1D-array
`define PACK_ARRAY(PK_WIDTH,PK_LEN,PK_SRC,PK_DEST) \
generate \
genvar pk_idx; \
for (pk_idx=0; pk_idx<(PK_LEN); pk_idx=pk_idx+1) \
begin \
assign PK_DEST[((PK_WIDTH)*pk_idx+((PK_WIDTH)-1)):((PK_WIDTH)*pk_idx)] = PK_SRC[pk_idx][((PK_WIDTH)-1):0]; \
end \
endgenerate
5.控制蜂鸣器播放音乐需要按音乐的拍子去控制蜂鸣器响的频率与时间,这里我利用上位机去控制这个时间。
//50ms 250ms
private void timer1_Tick(object sender, EventArgs e)
{
if (mus_time >= 5)
{
mus_time = 0;
music_num++;
if (music_data[music_num] == 101) //结束符
{
timer1.Enabled = false;
music_num = 0;
return;
}
}
try
{
if(mus_time == 0) //控制发声
com.Write(music_data, music_num, 1);
else if(mus_time == 4)
{
if(music_even[music_num] == 1) //如果是长拍子就一直播放
com.Write(music_data, music_num, 1);
else //停顿一下,让音乐有拍子
com.Write(music_even, music_num, 1);
}
}
catch
{
timer1.Enabled = false;
close_com();
mus_time = 0;
music_num = 0;
MessageBox.Show("串口被关闭", "错误");
return;
}
mus_time++;
}
6.设备端很难去判断播放的结束开始,这里我使用了几个特殊的数值作为开始结束的判断
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
beep_state <= 1'b0;
else
case(beep_data)
8'd01: cycle = 16'd45872; //L1,
8'd02: cycle = 16'd40858; //L2,
8'd03: cycle = 16'd36408; //L3,
8'd04: cycle = 16'd34364; //L4,
8'd05: cycle = 16'd30612; //L5,
8'd06: cycle = 16'd27273; //L6,
8'd07: cycle = 16'd24296; //L7,
8'd11: cycle = 16'd22931; //M1,
8'd12: cycle = 16'd20432; //M2,
8'd13: cycle = 16'd18201; //M3,
8'd14: cycle = 16'd17180; //M4,
8'd15: cycle = 16'd15306; //M5,
8'd16: cycle = 16'd13636; //M6,
8'd17: cycle = 16'd12148; //M7,
8'd21: cycle = 16'd11478; //H1,
8'd22: cycle = 16'd10215; //H2,
8'd100: beep_state <= 1'b0;
8'd200: beep_state <= 1'b1;
default: begin
cycle = 16'd0; //cycle为0,PWM占空比为0,低电平
beep_state <= beep_state;
end
endcase
end
总结反思
因为想完整的实现所有的代码,所以在设计中并没有使用ip核。不过,很多地方可以使用ip核进行简化,比如显示缓存、oled初始化命令存储。
简单化代码,不要写无法综合的语法。
多用仿真。
开发FPGA一定要有分模块思维,基于fpga并行的特点,寄存器是不可以在两个地方同时被修改,所以模块之间的耦合性要尽可能的降低。
多看资料,多看例程,吸收好的编程思路与编程方法。
最后附上资源使用