寒假在家一起练(4) - 基于小脚丫FPGA的定时、测温、报警、控制平台
基于Lattice XO2 4000HC的小脚丫开发板和综合技能训练板的定时、测温、报警、控制装置。
标签
FPGA
显示
网络与通信
氢化脱氯次氯酸
更新2021-03-04
1197

1 项目需求

  • 实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒;
  • 实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;
  • 定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致;
  • PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;
  • 音频文件播放完毕,OLED开始更新时间信息和当前的温度信息

2 实现的思路

  • 使用OLED例程代码驱动OLED显示,并尝试改变显示内容;
  • 用分频器得到1Hz的时钟信号,实现定时时钟的计时,并显示在屏幕上;
  • 实现按键消抖,并通过按键控制时间的调节;
  • 了解温度传感器的协议,将温度转换为BCD码并显示在屏幕上;
  • 使用UART例程,例化单字节UART发送与接收,并编写字符串的UART发送与接收模块;
  • 将温度信息通过UART发送至上位机;
  • 使用蜂鸣器例程,首先不连接上位机,将音乐写进程序中直接播放;
  • 删除程序内的音乐,编写上位机程序,将音乐通过上位机以UART方式发送,蜂鸣器播放;
  • 整合各个模块,明确各模块间的触发关系。

3 完成的功能

3.1 时间与温度显示

通电后,OLED屏幕中心显示时间与温度信息。

Fg6mYIErErAemxwDwS3-XrGrK9x8

3.2 时间调整

拨码开关1、2为时间调整开关,按键1、2用来改变时间。其功能表如所示:

拨码开关                     按键开关 按下KEY1 按下KEY2
1OFF 2OFF 无变化,正常计时 无变化,正常计时
1OFF 2ON 分钟数+1 分钟数-1
1ON 2OFF 小时数+1 小时数-1
1ON 2ON 小时数+1 小时数-1

3.3 整点报时

整点时,FPGA向电脑发送当前温度信息。电脑同时运行Matlab脚本读取温度信息并显示,之后立即将已编码的二进制音乐文件逐字节通过串口发送,全部发送完毕后蜂鸣器播放音乐,同时OLED停止更新。播放完毕后继续更新。

Fn_1SU8wf70vigTNaumpXDLFPSdp

FtXNoy_yT_oLV9QBr4Wj2iB3yaxE

4 实现过程

4.1 层次结构

FhNLAkSi-AARrori126Q0DqF1vAz

FqkfmUPjyOMOjgeyXUTBocRfdxgy

资源消耗:

Fu67wipny03DGJwDbf0SbKDBhs8u

4.2 数字钟部分

// clock_module: 时钟模块
// 输入:
//      clk: 12MHz时钟信号
//      rst_n: 复位信号,低电平有效
//      calibrate: 校准开关,2'b00不校准,2'b01校准分钟,2'b10和2'b11校准小时
//      up_pulse: 每输入一个高电平脉冲校准时小时/分钟数加一
//      down_pulse: 每输入一个低电平脉冲校准时小时/分钟数减一
// 输出:
//      sec: 秒输出 (BCD编码)
//      min: 分输出 (BCD编码)
//      hour: 时输出 (BCD编码)
//      hour_update: 整点信号,整点时输出一个时钟周期的高电平脉冲
module clock_module (
    input               clk,
    input               rst_n,
    input       [1:0]   calibrate,
    input               up_pulse,
    input               down_pulse,
    output  reg [7:0]   sec,
    output  reg [7:0]   min,
    output  reg [7:0]   hour,
    output  reg         hour_update
);

wire clk_1hz;
reg min_update;
reg [7:0] prev_sec, prev_min;

// 得到1Hz的时钟信号,作为秒钟的触发时钟
divide get1hz(
    .clk(clk),
    .rst_n(rst_n),
    .clkout(clk_1hz)
);

// 秒控制
always @(posedge clk_1hz or negedge rst_n) begin
    if (!rst_n)
        sec <= 8'h0;
    else if (sec >= 8'h59)
        sec <= 8'h0;
    else if (sec[3:0] == 4'h9)  // BCD码进位要在低位加7
        sec <= sec + 8'h7;
    else if (!calibrate)  // 非校准时正常计时
        sec <= sec + 8'h1;
end

// 记录上一时钟(12MHz)周期的秒数
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        prev_sec <= 8'h0;
    else
        prev_sec <= sec;
end

// 当秒数从59变为0且不在校准状态时,产生一个时钟(12MHz)周期的分钟更新信号
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        min_update <= 1'b0;
    else if (!calibrate && sec == 8'h0 && prev_sec == 8'h59)
        min_update <= 1'b1;
    else
        min_update <= 1'b0;
end

// 分控制,由于需要校准,其触发频率不能为1Hz,需要使用12MHz时钟
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        min <= 8'h0;
    else if (calibrate == 2'b10) begin  // 校准分钟
        if (up_pulse) begin
            if (min >= 8'h59)
                min <= 8'h0;
            else if (min[3:0] == 4'h9)
                min <= min + 8'h7;
            else
                min <= min + 8'h1; 
        end
        else if (down_pulse) begin
            if (min == 8'h0)
                min <= 8'h59;
            else if (min[3:0] == 4'h0)
                min <= min - 8'h7;
            else
                min <= min - 8'h1; 
        end
    end
    else if (min_update) begin  // 不在校准状态,正常计时
        if (min >= 8'h59)
            min <= 8'h0;
        else if (min[3:0] == 4'h9)
            min <= min + 8'h7;
        else
            min <= min + 8'h1; 
    end
end

// 记录上一时钟(12MHz)周期的分钟数
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        prev_min <= 8'h0;
    else
        prev_min <= min;
end

// 当分钟数从59变为0且不在校准状态时,产生一个时钟(12MHz)周期的小时数更新信号
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        hour_update <= 1'b0;
    else if (!calibrate && min == 8'h0 && prev_min == 8'h59)
        hour_update <= 1'b1;
    else
        hour_update <= 1'b0;
end

// 时控制
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        hour <= 8'h0;
    else if (calibrate[0]) begin  // 校准小时
        if (up_pulse) begin
            if (hour >= 8'h23)
                hour <= 8'h0;
            else if (hour[3:0] == 4'h9)
                hour <= hour + 8'h7;
            else
                hour <= hour + 8'h1; 
        end
        else if (down_pulse) begin
            if (hour == 8'h0)
                hour <= 8'h23;
            else if (hour[3:0] == 4'h0)
                hour <= hour - 8'h7;
            else
                hour <= hour - 8'h1; 
        end
    end
    else if (hour_update) begin  // 不在校准状态,正常计时
        if (hour >= 8'h23)
            hour <= 8'h0;
        else if (hour[3:0] == 4'h9)
            hour <= hour + 8'h7;
        else
            hour <= hour + 8'h1;
    end
end

endmodule

4.3 温度计部分

温度传感器的底层驱动使用电子森林提供的代码,对采集到的数据处理部分代码如下:(将温度转化为bcd码字符串,保留一位小数)

// temp_data_decode: 对传感器传来的原始数据进行处理
// 输入:
//      rst_n: 复位信号,低电平有效
//      data_in: 温度传感器的原始数据
// 输出:
//      data_out: 处理后的温度数据,为bcd码字符串,如" 30.1"
module temp_data_decode (
    input               rst_n,
    input       [15:0]  data_in,
    output  reg [39:0]  data_out
);

wire [11:0] temp_int;

always @(*) begin
    if (!rst_n)
        data_out <= " 00.0";
    else begin
        data_out[31:8] = {temp_int[7:4] + 8'h30, temp_int[3:0] + 8'h30, "."};
        if (data_in[15:11] == 5'b11111)
            data_out[39:32] = "-";
        else if (temp_int[11:8] == 4'h1)
            data_out[39:32] = "1";
        else
            data_out[39:32] = " ";
            
        case (data_in[3:0])
            4'h0:       data_out[7:0] <= 8'h30;
            4'h1, 4'h2: data_out[7:0] <= 8'h31;
            4'h3:       data_out[7:0] <= 8'h32;
            4'h4, 4'h5: data_out[7:0] <= 8'h33;
            4'h6, 4'h7: data_out[7:0] <= 8'h34;
            4'h8:       data_out[7:0] <= 8'h35;
            4'h9, 4'ha: data_out[7:0] <= 8'h36;
            4'hb:       data_out[7:0] <= 8'h37;
            4'hc, 4'hd: data_out[7:0] <= 8'h38;
            4'he, 4'hf: data_out[7:0] <= 8'h39;
            default:    data_out[7:0] <= 8'h30;
        endcase
    end
end

bin_to_bcd temp_data_conversion(
    .bin_data(data_in[10:4]),
    .bcd_data(temp_int)
);

endmodule

4.4 UART部分

UART的单字节发送与接收使用电子森林的代码,利用单字节模块,编写字符串的UART发送与接收代码:

// uart_send: UART发送模块,发送整段温度数据
// 输入:
//      clk: 12MHz时钟信号
//      rst_n: 复位信号,低电平有效
//      tx_data: 要发送的数据(字符串)
//      length: 发送数据字节数
//      send_start: 发送开始信号,输入一个时钟周期高电平脉冲代表整段序列的发送开始
//      bps_clk: 发送时钟输入
// 输出:
//      bps_en: 发送时钟使能
//      rs232_tx: UART发送端口
module uart_send(
    input                   clk,
    input                   rst_n,
    input           [255:0] tx_data,
    input           [7:0]   length,
    input                   send_start, 
    input                   bps_clk,
    output                  bps_en,
    output                  rs232_tx
);

reg tx_start;  // 发送一个字节的开始信号,并非整个序列
reg [7:0] tx_data_addr;  // 当前发送字节在整个发送序列的地址
reg [7:0] tx_byte;

always @(*) begin
    if (!rst_n)
        tx_byte = 8'h0;
    else if (tx_start)  // 每次发送一个字节前,先将该字节写入发送寄存器
        tx_byte = tx_data[tx_data_addr-:8];
end

// 更新发送字节的地址
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        tx_data_addr = 8'd255;
    else if (send_start)
        tx_data_addr = length * 8 - 8'd1;
    else if (tx_finish) begin
        tx_data_addr = tx_data_addr - 8'd8;
    end
end

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        tx_start <= 1'b0;
    else if (send_start)
        tx_start <= 1'b1;
    else if (tx_finish && tx_data_addr != 8'd255)
        tx_start <= 1'b1;
    else
        tx_start <= 1'b0;
end


//UART发送字节模块 例化
Uart_Tx Uart_Tx_uut
(
    .clk_in                 (clk            ),  //系统时钟
    .rst_n_in               (rst_n          ),  //系统复位,低有效
    .bps_en                 (bps_en         ),  //发送时钟使能
    .bps_clk                (bps_clk        ),  //发送时钟输入
    .tx_start               (tx_start       ),  //发送开始信号
    .tx_byte                (tx_byte        ),  //需要发出的数据 (字节)
    .rs232_tx               (rs232_tx       ),  //UART发送输出
    .tx_finish              (tx_finish      )   //发送结束信号 (该字节发送完毕,并非整个序列)
);

endmodule


// uart_recv: UART接收模块,接收数据并进行数据写入处理
// 输入:
//      clk: 12MHz时钟信号
//      rst_n: 复位信号,低电平有效
//      rs232_rx: UART接收端口
//      bps_clk: 接收时钟输入
//      bps_en: 接收时钟使能
// 输出:
//      rx_tone: 当前接收音符音高
//      rx_duration: 当前接收音符时值
//      duration_wr_en: 时值写入使能
//      tone_wr_en: 音高写入使能
//      wr_addr: 写入地址
//      recv_finish: 接收完毕信号,整段音乐接收完毕产生一个时钟周期的高电平脉冲
module uart_recv(
    input                       clk,
    input                       rst_n,
    input                       rs232_rx,
    input                       bps_clk,
    output                      bps_en,
    output  reg     [4:0]       rx_tone,
    output  reg     [4:0]       rx_duration,
    output  reg                 duration_wr_en,
    output  reg                 tone_wr_en,
    output  reg     [5:0]       wr_addr,
    output  reg                 recv_finish
);

reg bps_en_r;
wire rx_finish = bps_en_r & (~bps_en);
integer i;
wire [7:0] rx_byte;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        bps_en_r <= 1'b0;
    else
        bps_en_r <= bps_en;
end

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        rx_tone <= 5'o0;
        rx_duration <= 5'd0;
        duration_wr_en <= 1'b0;
        tone_wr_en <= 1'b0;
        wr_addr <= 6'b111111;
        recv_finish <= 1'b0;
    end
    else if (recv_finish)
        recv_finish <= 1'b0;
    else if (rx_finish) begin
        if (rx_byte == 8'hff) begin
            if (tone_wr_en) begin
                tone_wr_en <= 1'b0;
                duration_wr_en <= 1'b1;
            end
            else begin
                recv_finish <= 1'b1;
                duration_wr_en <= 1'b0;
            end
            wr_addr <= 6'b111111;
        end
        else if (duration_wr_en) begin
            wr_addr <= wr_addr + 6'd1;
            rx_duration <= rx_byte[4:0];
        end
        else begin
            tone_wr_en <= 1'b1;
            wr_addr <= wr_addr + 6'd1;
            rx_tone <= {rx_byte[5:4], rx_byte[2:0]};
        end
    end
end

//UART接收字节模块 例化
Uart_Rx Uart_Rx_uut
(
.clk_in                 (clk            ),  //系统时钟
.rst_n_in               (rst_n          ),  //系统复位,低有效
.bps_en                 (bps_en         ),  //接收时钟使能
.bps_clk                (bps_clk        ),  //接收时钟输入
.rs232_rx               (rs232_rx       ),  //UART接收输入
.rx_byte                (rx_byte        )   //接收到的数据
);

endmodule

4.5 音乐播放部分

蜂鸣器的驱动使用电子森林的代码,整段音乐的处理和播放模块代码如下:

// music_module: 音乐播放模块
// 输入:
//      clk: 12MHz时钟信号
//      rst_n: 复位信号,低电平有效
//      music_start: 开始播放信号,一个时钟周期的高电平脉冲代表音乐开始
//      music_tone: 音符音高输入,用5位八进制数表示
//                  0~2位代表音级(do~si: 3'o1~3'o7, 休止符: 3'o0)
//                  3~4位代表音组(高音: 2'o2、中音: 2'o1、低音: 2'o0)
//      music_duration: 音符时值输入,用5位十进制数表示,单位为1/8秒,0代表音乐结束
// 输出:
//      music_playing: 音乐播放信号,播放音乐时输出高电平
//      rd_addr: RAM读地址,用来读取音高和时值信息
//      beeper_pin: 蜂鸣器输出端口

module music_module (
    input               clk,
    input               rst_n,
    input               music_start,
    input       [4:0]   music_tone,
    input       [4:0]   music_duration,
    output reg          music_playing,
    output reg  [5:0]   rd_addr,
    output              beeper_pin
);

wire clk_8hz;
reg music_start_r;
reg [4:0] duration_cnt;

// 检测music_start的正脉冲,检测到后将music_playing置为1
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        music_start_r <= 1'b0;
    else if (music_start == 1'b1) begin
        music_start_r <= 1'b1;
    end
    else if (music_playing)
        music_start_r <= 1'b0;
end

always @(posedge clk_8hz or negedge rst_n) begin
    if (!rst_n)
        music_playing <=1'b0;
    else if (music_start_r)
        music_playing <= 1'b1;
    else if (music_duration == 5'd0)
        music_playing <= 1'b0;
end

// 时值控制
always @(posedge clk_8hz or negedge rst_n) begin
    if (!rst_n)
        duration_cnt <= 5'd0;
    else if (!music_playing)
        duration_cnt <= 5'd0;
    else if (duration_cnt >= music_duration - 5'd1)
        duration_cnt <= 5'd0;
    else
        duration_cnt <= duration_cnt + 5'd1;
end

// 音乐RAM地址更新
always @(posedge clk_8hz or negedge rst_n) begin
    if (!rst_n)
        rd_addr <= 6'd0;
    else if (!music_playing)
        rd_addr <= 6'd0;
    else if (duration_cnt >= music_duration - 1)begin  // 该音符播放完毕
        rd_addr <= rd_addr + 6'd1;
    end
end

// 例化蜂鸣器
Beeper bp
(
    .clk_in(clk),       //系统时钟
    .rst_n_in(rst_n),   //系统复位,低有效
    .music_playing(music_playing),  //蜂鸣器使能信号
    .tone(music_tone),      //蜂鸣器音节控制
    .piano_out(beeper_pin)  //蜂鸣器控制输出

);

// 产生8Hz时钟信号
divide #(.N(1_500_000), .WIDTH(21)) metronome(
    .clk(clk),
    .rst_n(rst_n),
    .clkout(clk_8hz)
);

endmodule


// music_ram: 音乐RAM模块
// 输入:
//      clk: 12MHz时钟信号
//      rst_n: 复位信号,低电平有效
//      tone_wr_en: 音高写入使能,高电平使能
//      duration_wr_en: 时值写入使能,高电平使能
//      rd_en: 读取使能
//      wr_addr: 写入地址(0~63)
//      rd_addr: 读取地址(0~63)
//      tone_in: 音高写入数值
//      duration_in: 时值写入数值
// 输出:
//      tone_out: 音高读取数值
//      duration_out: 时值读取数值
module music_ram(
    input                   clk,
    input                   rst_n,
    input                   tone_wr_en,
    input                   duration_wr_en,
    input                   rd_en,
    input [5:0]             wr_addr,
    input [5:0]             rd_addr,
    input [4:0]             tone_in,
    input [4:0]             duration_in,
    output [4:0]            tone_out,
    output [4:0]            duration_out
);

    reg [4:0]           tone_ram[63:0];
    reg [4:0]           duration_ram[63:0];   
    integer             i;   

    always @(posedge clk or negedge rst_n)
    begin
        if (!rst_n) begin
            for(i=0;i<64;i=i+1)  begin
                tone_ram[i] <= 5'b0;
                duration_ram[i] <= 5'b0;
            end
        end
        else if (tone_wr_en)
            tone_ram[wr_addr] <= tone_in;
        else if (duration_wr_en)
            duration_ram[wr_addr] <= duration_in;
    end

    assign tone_out = rd_en? tone_ram[rd_addr] : 5'bz;
    assign duration_out = rd_en? duration_ram[rd_addr] : 5'bz;
endmodule

4.6 上位机

上位机始终运行Matlab脚本监视串口,一旦串口有信息发送就将其显示。同时发送音乐信息。

上位机Matlab脚本代码如下:

music_file = fopen("music.bin");
music_data = fread(music_file);
fclose(music_file);
s = serialport("COM7", 9600, "Timeout", 4000);
configureTerminator(s,"CR/LF");
while 1
    disp(readline(s));
    write(s, music_data, "uint8");
end

其中上位机发送的音乐文件由另一个脚本生成。该脚本将各音符的音调和时值以二进制模式编码。每个音符的音调和时值位宽都是8位,最多支持63个音符。音调前4位为低音(0)、中音(1)和高音(2),后四位为do~si(1~7)和休止符(0),其余的编码均无效。时值为1~31的整数,单位时间为1/8秒,时值0代表音乐的结束。音调和时值的数组结尾为0xff作为接收结束标志。

生成音乐文件的Matlab脚本代码如下:

f = fopen("music.bin", "w");
tone = [
    0x21, 0x15, 0x21, 0x25, 0x24, 0x23, 0x22, 0x17, 0x00, 0x17, 0x15, 0x17, 0x23, 0x22, 0x17, 0x21, 0x23,...
    0x21, 0x15, 0x21, 0x25, 0x24, 0x23, 0x22, 0x17, 0x00, 0x17, 0x15, 0x17, 0x23, 0x22, 0x17, 0x21,...
    0x23, 0x25, 0x24, 0x25, 0x24, 0x23, 0x22, 0x17, 0x22, 0x23, 0x24, 0x23, 0x22, 0x21,...
    0x23, 0x25, 0x24, 0x23, 0x24, 0x25, 0x26, 0x25, 0x24, 0x23, 0x24, 0x23, 0x24, 0x23, 0x22, 0x21, 0x00];
duration = [
    0x02, 0x02, 0x02, 0x04, 0x04, 0x02, 0x02, 0x0d, 0x01, 0x02, 0x02, 0x02, 0x04, 0x04, 0x02, 0x02, 0x0e,...
    0x02, 0x02, 0x02, 0x04, 0x04, 0x02, 0x02, 0x0d, 0x01, 0x02, 0x02, 0x02, 0x04, 0x04, 0x02, 0x10,...
    0x08, 0x08, 0x02, 0x02, 0x02, 0x02, 0x08, 0x08, 0x08, 0x02, 0x02, 0x02, 0x02, 0x08,...
    0x08, 0x08, 0x02, 0x02, 0x02, 0x02, 0x08, 0x04, 0x02, 0x02, 0x08, 0x02, 0x02, 0x02, 0x02, 0x08, 0x00];
fwrite(f, [tone 0xff duration 0xff]);
fclose(f);

5 遇到的主要难题

这个项目是我接触FPGA的首个项目,之前我一直学习的是单片机的编程,习惯了单片机程序顺序执行的思想,所以刚开始面对FPGA的并行思想时感到无从下手。经过查找资料,学习FPGA相关的例程,并通过简单的项目,如按键消抖等开始练习,逐渐编写各个模块,最后进行综合,完成了任务要求。

对我来说,FPGA编程的时序逻辑是非常重要的一方面,编写程序时需要明确各个功能块何时触发,有时差一个时钟周期就不能得到想要的结果。另外,有些模块可以用组合逻辑来实现,不需要考虑其触发时间。时序逻辑的问题通过仿真,一般都会发现,仿真是FPGA编程中必不可少的一部分。

6 未来的计划建议

  • 由于开发板上没有纽扣电池,所以只要开发板一断电,之间的时间信息就会丢失,上电后需要重新校准时间,非常麻烦。当连接上位机时,上位机可以将当前时间通过串口发送给开发板,开发板自动校准时间,更为方便。
  • 可以增加闹钟功能,除整点外可以另外设置一个响铃时间,并播放不同的音乐。
附件下载
stepfpga_training.jed
jed文件
code.zip
Diamond工程文件
matlab code.zip
上位机Matlab代码和生成音乐的脚本
团队介绍
中国科学技术大学
团队成员
王赫男
中国科学技术大学电子信息工程专业
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号