寒假在家练项目4——熊腾琼
1、时钟模块包括按键消抖、时钟计时模块; 2、温度检测模块包括ds18b20驱动电路、数据处理、串口发送模块; 3、蜂鸣器模块包括串口接收、PWM发生、频率控制; 4、OLED模块包括时间温度发送控制、发送信息控制、SPI发送模块;
标签
FPGA
ling
更新2021-02-22
973

项目需求

本项目为基于Lattice XO2-4000HC的一个完成定时、测温、报警、控制的小项目,具体功能如下:

  1. 定时时钟功能:在OLED显示屏上显示时间,时间精确到分钟。并可以通过板载按键对时间进行设置。同时在到达整点时蜂鸣器报警。
  2. 温度计功能:利用板载温度传感器实时监测环境温度并与时间一起显示在OLED屏幕上。
  3. 温度报警功能:在整点报警的同时,将当前温度信息以异步串口通信的方式传输到电脑上,并在电脑上显示温度信息。
  4. 控制蜂鸣器功能:通过uart发送一段音频到板子上,并通过接收到的数据控制蜂鸣器播放。在播放期间OLED上的时间和温度信息暂停更新,在播放后恢复更新。

项目实现过程

FvnckUs0GyeZRShJMuL9UcWU28_C

图1:顶层设计文件

代码说明:本设计代码都是采用明德杨至简设计法设计的,因为程序比较易读,便于调试,可能会比平时大家见到的代码篇幅较长,但是评价FPGA的程序好坏并不是以代码的长短来评价的吧,而是应该用生成电路所占用的资源以及电路所能跑的最快时钟来评价吧,就比如说FPGA实现乘法,你可以直接使用*,也可以用一段代码通过移位实现,综合出来的电路很多时候是直接使用*所占资源较多;我实现这个设计由于温度精确到四位小数,不管是数据处理还是显示资源使用都应该会多一点,下面是我的资源使用情况:

FhRWhCCQqJJvmdoMNdbQKSz82zr2

图2:资源占用

1、顶层模块

1.1、顶层信号

定义
clk I 1 系统时钟,12M
rst_n I 1 系统复位,低电平有效
key I 3 按键输入信号
uart_rx I 1 串口输入信号
uart_tx O 1 串口输出信号
beep O 1 蜂鸣器驱动信号
res O 1 OLED复位信号,低电平有效
cs O 1 OLED片选信号
dc O 1 OLED数据/指令指示信号
q IO 1 ds18b20数据线
sclk O 1 OLED时钟信号
sdin O 1 OLED数据信号线

1.2、参考代码

    assign  q = dq_out_en ? dq_out:1'bz;
    assign  dq_in = q;

    /**************时钟部分例化************/

    key uut_key(
        .clk    (clk    ),
        .rst_n  (rst_n  ),
        .key_in (key    ),
        .key_vld(key_vld)
    );

    time_data   uut_time_data(
        .clk        (clk        ),
        .rst_n      (rst_n      ),
        .key_in     (key_vld    ),
        .times      (times      ),
        .times_vld  (times_vld  ),
        .time_flag  (time_flag  )
    );

    /*****************温度部分例化*************/
   
    temp_come   uut_twmp_come(
        .clk        (clk        ),
        .rst_n      (rst_n      ),
        .rst_en     (rst_en     ),
        .wr_en      (wr_en      ),
        .wdata      (wdata      ),
        .rd_en      (rd_en      ),
        .rdy        (rdy        )
    );

    temp_byte   uut_temp_byte(
        .clk        (clk        ),
        .rst_n      (rst_n      ),
        .rst_en     (rst_en     ),
        .wr_en      (wr_en      ),
        .wdata      (wdata      ),
        .rd_en      (rd_en      ),
        .rdata      (rdata      ),
        .data       (data       ),
        .data_vld   (data_vld   ),
        .rdy        (rdy        ),
        .rst_en_bit (rst_en_bit ),
        .wr_en_bit  (wr_en_bit  ),
        .wdata_bit  (wdata_bit  ),
        .rd_en_bit  (rd_en_bit  ),
        .rdata_vld  (rdata_vld  ),
        .rdy_bit    (rdy_bit    )
    );

    temp_bit    uut_temp_bit(
        .clk        (clk        ),
        .rst_n      (rst_n      ),
        .rst_en     (rst_en_bit ),
        .wr_en      (wr_en_bit  ),
        .rd_en      (rd_en_bit  ),
        .wdata      (wdata_bit  ),
        .rdata      (rdata      ),
        .rdata_vld  (rdata_vld  ),
        .rdy        (rdy_bit    ),
        .dq_out     (dq_out     ),
        .dq_out_en  (dq_out_en  ),
        .dq_in      (dq_in      )
    );
    
    temp_data   uut_temp_data(
        .clk            (clk            ),
        .rst_n          (rst_n          ),
        .din_temp       (data           ),
        .din_temp_vld   (data_vld       ),
        .temp_uns       (temp_uns       ),
        .temp_uns_vld   (temp_uns_vld   )
    ); 

    temp_tx     uut_temp_tx(
        .clk            (clk            ),
        .rst_n          (rst_n          ),
        .din            (temp_uns       ),
        .din_vld        (time_flag   	),//整点时才有效;
        .uart_rdy       (uart_rdy       ),
        .uart_tx_din    (uart_tx_din    ),
        .uart_tx_din_vld(uart_tx_din_vld)
    );
    
    uart_tx     uut_uart_tx(
        .clk        (clk            ),
        .rst_n      (rst_n          ),
        .tx_data    (uart_tx_din    ),
        .tx_vld     (uart_tx_din_vld),
        .uart_tx    (uart_tx        ),
        .tx_rdy     (uart_rdy       )
    );

    /*************蜂鸣器例化******************/

    uart_rx     uut_uart_rx(
        .clk        (clk    ),
        .rst_n      (rst_n  ),
        .uart_rx    (uart_rx),
        .rx_data    (rx_data),
        .rx_vld     (rx_vld )
    );

    cy          uut_cy(
        .clk        (clk        ),
        .rst_n      (rst_n      ),
        .din        (rx_data    ),
        .din_vld    (rx_vld     ),
        .flag       (beep_flag  ),
        .cycle      (cycle      ),
        .beat_flag  (beat_flag  )
    );

    pwm#(.DATA_W(16))   uut_pwm(
        .clk        (clk        ),
        .rst_n      (rst_n      ),
        .flag       (beep_flag  ),
        .pwm        (beep       ),
        .x          (cycle      ),
        .duty       (cycle>>1   ),
        .beat_flag  (beat_flag  )
    );

    /************oled*****************/

    temp_time_control   uut_temp_time_control(
        .clk            (clk        ),
        .rst_n          (rst_n      ),
        .flag           (beep_flag  ),
        .times          (times      ),
        .times_vld      (times_vld  ),
        .temp_uns       (temp_uns   ),
        .temp_uns_vld   (temp_uns_vld),
        .dout           (dout       ),
        .dout_vld       (dout_vld   )
    );
    
    oled_control    uut_oled_control(
        .clk        (clk        ),
        .rst_n      (rst_n      ),
        .din        (dout       ),
        .din_vld    (dout_vld   ),
        .rdy        (rdy_spi    ),
        .wr_en      (wr_en_spi  ),
        .wdata      (wdata_spi  ),
        .res        (res        )
    );
    
    spi         uut_spi(
        .clk        (clk        ),
        .rst_n      (rst_n      ),
        .wr_en      (wr_en_spi  ),
        .wdata      (wdata_spi  ),
        .cs         (cs         ),
        .dc         (dc         ),
        .sclk       (sclk       ),
        .sdin       (sdin       ),
        .rdy        (rdy_spi    )
    );

2、时钟模块

2.1、按键消抖模块

主要经过一个计数器对输入信号进行消抖;

2.1.1、接口信号

信号 输入/输出 位宽 定义
clk I 1 系统时钟,12M
rst_n I 1 系统复位
key I 1 按键输入信号
key_vld O 3 按键消抖输出信号

2.1.2、该模块可以参考至简设计系列_按键控制数字时钟

2.2、时钟产生模块

简述:由于时间需要显示,为了不增加数据处理模块,所以主要利用七个计数器实现,秒计时计数器,秒个位计数器,秒十位计数器,分个位计数器,分十位计数器,时个位计数器,时十位计数器,输出信号也是由后六个计数器拼接而成,assign times = {cnt_ss,cnt_sg,cnt_fs,cnt_fg,cnt_ms,cnt_mg};按键设置时钟思路,使用三个按键实现,第一个按键进行模式选择,正常时为计数模式,按下之后进入设置模式,再次按下又进入计时模式;第二个按键要在设置模式下才有效,用于对时钟的哪一个位设置的选择,起始设置秒的个位,按一下之后对秒的十位进行设置,依次增加循环;第三个按键用于相应位的数据设置,仅在设置模式有效,按一下相应位数据加一,溢出后回到0;比如开始计数时钟位00:00:00;我们需要设置位01:58:50;那么操作是按下按键1进入设置模式,按下按键2选中秒的十位,按5下按键3,让秒十位加5;之后再次按一下按键2选中分的个位,按八下按键3设置位分个位为8,之后依次操作,设置完之后按按键1回到计时模式正常计数;注意times = {cnt_ss,cnt_sg,cnt_fs,cnt_fg,cnt_ms,cnt_mg}必须使用组合电路,不然最后会出现bug;

2.2.1、接口信号

信号 输入/输出 位宽 定义
clk I 1 系统时钟,12M
rst_n I 1 系统复位
key_in I 3 按键输入信号
times O 20

时钟拼接输出信号,[3:0]秒个位;[6:4]秒十位;[10:7]分个位,[13:11]分十位,[17:14]时个位,[19:18]时十位;

times_vld O 1 时钟输出有效指示信号
times_flag O 1 整点标志信号

2.2.2、参考代码

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            key0_flag <= 1'b0;
        end
        else if(key_in[0])begin
            key0_flag <= ~key0_flag;
        end
    end
      
    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            key2_flag <= 1'b0;
        end
        else if(key0_flag && key_in[2])begin
            key2_flag <= 1'b1;
        end
        else begin
            key2_flag <= 1'b0;
        end
    end

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt0 <= 0;
        end
        else if(add_cnt0)begin
            if(end_cnt0)
                cnt0 <= 0;
            else
                cnt0 <= cnt0 + 1;
        end
    end

    assign add_cnt0 = key0_flag && key_in[1];       
    assign end_cnt0 = add_cnt0 && cnt0==6-1;   
    
    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt1 <= 0;
        end
        else if(add_cnt1)begin
            if(end_cnt1)
                cnt1 <= 0;
            else
                cnt1 <= cnt1 + 1;
        end
    end

    assign add_cnt1 = !key0_flag;      
    assign end_cnt1 = add_cnt1 && cnt1==TIME_1S-1;

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt_mg <= 0;
        end
        else if(add_cnt_mg)begin
            if(end_cnt_mg)
                cnt_mg <= 0;
            else
                cnt_mg <= cnt_mg + 1;
        end
    end

    assign add_cnt_mg = (key0_flag && cnt0==0 && key2_flag) || (!key0_flag && end_cnt1);
    assign end_cnt_mg = add_cnt_mg && cnt_mg== 10-1;

    always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            cnt_ms <= 0;
        end
        else if(add_cnt_ms)begin
            if(end_cnt_ms)
                cnt_ms <= 0;
            else
                cnt_ms <= cnt_ms + 1;
        end
    end

    assign add_cnt_ms = (key0_flag && cnt0==1 && key2_flag) || (!key0_flag && end_cnt_mg);
    assign end_cnt_ms = add_cnt_ms && cnt_ms==6-1;

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt_fg <= 0;
        end
        else if(add_cnt_fg)begin
            if(end_cnt_fg)
                cnt_fg <= 0;
            else
                cnt_fg <= cnt_fg + 1;
        end
    end

    assign add_cnt_fg = (key0_flag && cnt0==2 && key2_flag) || (!key0_flag && end_cnt_ms);       
    assign end_cnt_fg = add_cnt_fg && cnt_fg==10-1;

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt_fs <= 0;
        end
        else if(add_cnt_fs)begin
            if(end_cnt_fs)
                cnt_fs <= 0;
            else
                cnt_fs <= cnt_fs + 1;
        end
    end

    assign add_cnt_fs = (key0_flag && cnt0==3 && key2_flag) || (!key0_flag && end_cnt_fg);       
    assign end_cnt_fs = add_cnt_fs && cnt_fs==6-1;

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt_sg <= 0;
        end
        else if(add_cnt_sg)begin
            if(end_cnt_sg)
                cnt_sg <= 0;
            else
                cnt_sg <= cnt_sg + 1;
        end
    end

    assign add_cnt_sg = (key0_flag && cnt0==4 && key2_flag) || (!key0_flag && end_cnt_fs);       
    assign end_cnt_sg = add_cnt_sg && cnt_sg==x-1;

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt_ss <= 0;
        end
        else if(add_cnt_ss)begin
            if(end_cnt_ss)
                cnt_ss <= 0;
            else
                cnt_ss <= cnt_ss + 1;
        end
    end

    assign add_cnt_ss = (key0_flag && cnt0==5 && key2_flag) || (!key0_flag && end_cnt_sg);       
    assign end_cnt_ss = add_cnt_ss && cnt_ss== 3-1;   

    always  @(*)begin
        if(cnt_ss==2)begin
            x = 4;
        end
        else begin
            x = 10;
        end
    end

    always  @(*)begin
        if(rst_n==1'b0)begin
            time_flag <= 1'b0;
        end
        else if((cnt_sg!=0 || cnt_ss!=0) && cnt_fs==0 && cnt_fg==0 && cnt_ms==0 && cnt_mg==0 && add_cnt_mg)begin
            time_flag <= 1'b1;
        end
        else begin
            time_flag <= 1'b0;
        end
    end

    assign times = {cnt_ss,cnt_sg,cnt_fs,cnt_fg,cnt_ms,cnt_mg};

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            times_vld <= 1'b0;
        end
        else if(add_cnt_mg || add_cnt_ms || add_cnt_fg || add_cnt_fs || add_cnt_sg || add_cnt_ss)begin
            times_vld <= 1'b1;
        end
        else begin
            times_vld <= 1'b0;
        end
    end

3、温度模块

3.1、ds18b20驱动模块

此部分分为了三个模块,temp_bit负责接收上游模块的指令和数据,实现对温度传感器DS18B20的复位、读和写,也就是实现怎么写,怎么读;temp_byte模块根据要求去复位、写1字节或读一字节;最后temp_come实现什么时候复位、读或写,以及写什么;

3.1.1、temp_bit

3.1.1.1、接口信号

信号 输入/输出 位宽 定义
clk I 1 系统时钟,12M
rst_n I 1 系统复位
rst_en I 1 ds18b20复位使能信号
wr_en I 1 ds18b20写使能信号
rd_en I 1 ds18b20读使能信号
wdata I 1 写数据信号
rdata O 1 从ds18b20读到的数据
rdata_vld O 1 读数据有效指示信号
dq_out O 1 三态门输入信号
dq_out_en O 1 三态门使能端
dq_in I 1 ds18b20输入本模块的数据
rdy O 1 模块忙闲指示信号

3.1.1.2、参考代码

    always@(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            state_c <= IDLE;
        end
        else begin
            state_c <= state_n;
        end
    end

    //第二段:组合逻辑always模块,描述状态转移条件判断
    always@(*)begin
        case(state_c)
            IDLE:begin
                if(idl2rst_start)begin
                    state_n = RST_S;
                end
                else if(idl2wr_start)begin
                    state_n = WR_S;
                end
                else if(idl2rd_start)begin
                    state_n = RD_S;
                end
                else begin
                    state_n = state_c;
                end
            end
            RST_S:begin
                if(rst2idl_start)begin
                    state_n = IDLE;
                end
                else begin
                    state_n = state_c;
                end
            end
            WR_S:begin
                if(wr2idl_start)begin
                    state_n = IDLE;
                end
                else begin
                    state_n = state_c;
                end
            end
            RD_S:begin
                if(rd2idl_start)begin
                    state_n = IDLE;
                end
                else begin
                    state_n = state_c;
                end
            end
            default:begin
                state_n = IDLE;
            end
        endcase
    end
    //第三段:设计转移条件
    assign idl2rst_start = state_c==IDLE && rst_en;
    assign idl2wr_start  = state_c==IDLE && wr_en ;
    assign idl2rd_start  = state_c==IDLE && rd_en ;
    assign rst2idl_start = state_c==RST_S&& cnt==TIME_RST-1;
    assign wr2idl_start  = state_c==WR_S && cnt==TIME_WR-1;
    assign rd2idl_start  = state_c==RD_S && cnt==TIME_RD-1;

    /**********************************************************
    计数器cnt:初始值为0,加一条件:不在IDLE状态,add_cnt=state_c!=IDLE;
    结束条件:复位,读,写结束,end_cnt=(rst2idl_start||wr2idl_start||rd2idl_start)&&add_cnt;
    **********************************************************/
    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt <= 0;
        end
        else if(add_cnt)begin
            if(end_cnt)
                cnt <= 0;
            else
                cnt <= cnt + 1;
        end
    end

    assign add_cnt = state_c!=IDLE;       
    assign end_cnt = add_cnt && ((state_c==RST_S && cnt==TIME_RST-1) || wr2idl_start || rd2idl_start);

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            wdata_ff0 <= 1'b0;
        end
        else if(state_c==IDLE && rst_en==1'b0 && wr_en)begin
            wdata_ff0 <= wdata;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            dq_out <= 1'b1;
        end
        else if(state_c==RST_S)begin
            if(cnt<TIME_RST_LOW)
                dq_out <= 1'b0;
            else
                dq_out <= 1'b1;
        end
        else if(state_c==WR_S)begin
            if(cnt<TIME_WR_INSTR)
                dq_out <= 1'b0;
            else
                dq_out <= wdata_ff0;
        end
        else if(state_c==RD_S)begin
            if(cnt<TIME_RD_INSTR)
                dq_out <= 1'b0;
            else
                dq_out <= 1'b1;
        end
        else begin
            dq_out <= 1'b1;
        end      
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            dq_out_en <= 1'b0;
        end
        else if(state_c!=IDLE)begin
            if(state_c==RD_S && cnt<TIME_RD_INSTR)
                dq_out_en <= 1'b1;
            else if(state_c==RST_S && cnt<TIME_RST_LOW)
                dq_out_en <= 1'b1;
            else if(state_c==WR_S && cnt<TIME_WR_DATA)
                dq_out_en <= 1'b1;
            else
                dq_out_en <= 1'b0;
        end
        else begin
            dq_out_en <= 1'b0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            rdata <= 1'b0;
        end
        else if(state_c==RD_S && cnt==TIME_RD_GET-1)begin
            rdata <= dq_in;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            rdata_vld <= 1'b0;
        end
        else if(state_c==RD_S && cnt==TIME_RD_GET-1)begin
            rdata_vld <= 1'b1;
        end
        else begin
            rdata_vld <= 1'b0;
        end
    end

    always  @(*)begin
        if(rst_en || wr_en || rd_en || state_c!=IDLE)begin
            rdy = 1'b0;
        end
        else begin
            rdy = 1'b1;
        end
    end

3.1.2、temp_byte模块

3.1.2.1、接口信号

信号 输入/输出 位宽 定义
clk I 1 系统时钟
rst_n I 1 系统复位
rst_en I 1 come模块复位使能信号
wr_en I 1 come模块写使能信号
wdata I 8 come给出的写数据
rd_en I 1 come模块读使能信号
rdata I 1 从bit模块读取温度单bit数据
data O 8 输出1byte温度数据
data_vld O 1 输出温度数据有效指示信号
rdy O 1 模块忙闲指示信号
rst_en_bit O 1 bit模块复位使能信号
wr_en_bit O 1 bit模块写使能信号
wdata_bit O 1 给bit模块单bit数据
rd_en_bit O 1 bit模块读使能信号
rdy_bit I 1 bit模块忙闲指示信号
rdata_vld I 1 从bit读单bit数据有效指示信号

3.1.2.2、参考代码

    always@(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            state_c <= IDLE;
        end
        else begin
            state_c <= state_n;
        end
    end

    //第二段:组合逻辑always模块,描述状态转移条件判断
    always@(*)begin
        case(state_c)
            IDLE:begin
                if(idl2rst_start)begin
                    state_n = RST;
                end
                else if(idl2wr_start)begin
                    state_n = WR;
                end
                else if(idl2rd_start)begin
                    state_n = RD;
                end
                else begin
                    state_n = state_c;
                end
            end
            RST:begin
                if(rst2idl_start)begin
                    state_n = IDLE;
                end
                else begin
                    state_n = state_c;
                end
            end
            WR:begin
                if(wr2idl_start)begin
                    state_n = IDLE;
                end
                else begin
                    state_n = state_c;
                end
            end
            RD:begin
                if(rd2idl_start)begin
                    state_n = IDLE;
                end
                else begin
                    state_n = state_c;
                end
            end
            default:begin
                state_n = IDLE;
            end
        endcase
    end
    //第三段:设计转移条件
    assign idl2rst_start = state_c==IDLE && rst_en;
    assign idl2wr_start  = state_c==IDLE && wr_en ;
    assign idl2rd_start  = state_c==IDLE && rd_en ;
    assign rst2idl_start = state_c==RST  && end_cnt;
    assign wr2idl_start  = state_c==WR   && end_cnt;
    assign rd2idl_start  = state_c==RD   && end_cnt;

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt <= 0;
        end
        else if(add_cnt)begin
            if(end_cnt)
                cnt <= 0;
            else
                cnt <= cnt + 1;
        end
    end

    assign add_cnt = (state_c==RD && rdata_vld)  || ((state_c==RST || state_c==WR) && rdy_bit);       
    assign end_cnt = add_cnt && cnt==x;

    always  @(*)begin
        if(state_c==WR || state_c==RD)begin
            x = 7;
        end
        else begin
            x = 0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            rst_en_bit <= 1'b0;
        end
        else if(state_c==RST && rdy_bit)begin
            rst_en_bit <= 1'b1;
        end
        else begin
            rst_en_bit <=1'b0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            wr_en_bit <= 1'b0;
        end
        else if(state_c==WR && rdy_bit)begin
            wr_en_bit <= 1'b1;
        end
        else begin
            wr_en_bit <= 1'b0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            wdata_ff0 <= 0;
        end
        else if(state_c==IDLE && rst_en==1'b0 && wr_en)begin
            wdata_ff0 <= wdata;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            wdata_bit <= 1'b0;
        end
        else if(state_c==WR && rdy_bit)begin
            wdata_bit <= wdata_ff0[cnt];
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            rd_en_bit <= 1'b0;
        end
        else if(state_c==RD && rdy_bit)begin
            rd_en_bit <= 1'b1;
        end
        else begin
            rd_en_bit <= 1'b0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            data <= 0;
        end
        else if(state_c==RD && rdata_vld)begin
            data[cnt] <= rdata;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            data_vld <= 1'b0;
        end
        else if(state_c==RD && rdata_vld && cnt==7)begin
            data_vld <= 1'b1;
        end
        else begin
            data_vld <= 1'b0;
        end
    end

    always  @(*)begin
        if(rst_en || wr_en || rd_en || state_c!=IDLE)begin
            rdy = 1'b0;
        end
        else begin
            rdy = 1'b1;
        end
    end

3.1.3、temp_come模块

3.1.3.1、接口信号

信号 输入/输出 位宽 定义
clk I 1 系统时钟
rst_n I 1 系统复位
rst_en O 1 byte模块复位使能信号
wr_en O 1 byte模块写使能
wdata O 8 byte模块写数据
rd_en O 1 byte模块读使能
rdy I 1 byte忙闲指示信号

3.1.3.2、参考代码

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt <= 0;
        end
        else if(add_cnt)begin
            if(end_cnt)
                cnt <= 0;
            else
                cnt <= cnt + 1;
        end
    end

    assign add_cnt = (rdy && !delay_flag) || end_cnt_750ms;    
    assign end_cnt = add_cnt && cnt== 8-1;

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt_750ms <= 0;
        end
        else if(add_cnt_750ms)begin
            if(end_cnt_750ms)
                cnt_750ms <= 0;
            else
                cnt_750ms <= cnt_750ms + 1;
        end
    end

    assign add_cnt_750ms = delay_flag && rdy;       
    assign end_cnt_750ms = add_cnt_750ms && cnt_750ms== TIME_750MS-1;

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            delay_flag <= 1'b0;
        end
        else if(wdata==8'h44)begin
            delay_flag <= 1'b1;
        end
        else if(add_cnt)begin
            delay_flag <= 1'b0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            rst_en <= 1'b0;
        end
        else if(add_cnt && (cnt==0 || cnt==3))begin
            rst_en <= 1'b1;
        end
        else begin
            rst_en <= 1'b0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            wr_en <= 1'b0;
        end
        else if(add_cnt && (cnt==1 || cnt==2 || cnt==4 || cnt==5))begin
            wr_en <= 1'b1;
        end
        else begin
            wr_en <= 1'b0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            rd_en <= 1'b0;
        end
        else if(add_cnt && (cnt==6 || cnt==7))begin
            rd_en <= 1'b1;
        end
        else begin
            rd_en <= 1'b0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            wdata <= 0;
        end
        else if(add_cnt && (cnt==1 || cnt==4))begin
            wdata <= 8'hCC;
        end
        else if(add_cnt && cnt==2)begin
            wdata <= 8'h44;
        end
        else if(add_cnt && cnt==5)begin
            wdata <= 8'hBE;
        end
        else begin
            wdata <= 0;
        end
    end

3.2、温度数据处理部分

此部分包括temp_data数据处理模块和temp_tx模块,temp_data先读取temp_byte模块输出的温度信息,将补码转化位二进制原码,之后利用BCD和hex2bcd模块分别将小数以及整数部分转化位BCD码格式,转化方法为“大四加三”,用两个模块的原因在于整数、小数部分同时进行转化,由于处理数据位宽不同,故使用两个模块;之后整点指示信号times_flag有效时,temp_tx模块将温度数据以及一些字符数据转化位ASCII码发送给串口发送模块uart_tx进行发送;

3.2.1、temp_data模块

3.2.1.1、接口信号

信号 输入/输出 位宽 定义
clk I 1 系统时钟
rst_n I 1 系统复位
din_temp I 8 byte温度输出信号
din_temp_vld I 1 byte温度输出有效指示信号
temp_uns O 32 温度数据BCD码格式输出信号[15:0]四位小数,[27:16]三位整数,[31:28]符号位,为F表示负数,0表示正数
temp_uns_vld O 1 温度数据输出有效指示信号

3.2.1.2、参考代码

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt <= 0;
        end
        else if(add_cnt)begin
            if(end_cnt)
                cnt <= 0;
            else
                cnt <= cnt + 1;
        end
    end

    assign add_cnt = din_temp_vld;       
    assign end_cnt = add_cnt && cnt==1;

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            ds_temp_lsb_req <= 8'b0;
            ds_temp_lsb_req_vld <= 1'b0;
        end
        else if(cnt==0 && add_cnt)begin
            ds_temp_lsb_req <= din_temp;
            ds_temp_lsb_req_vld <= 1'b1;
        end
        else begin
            ds_temp_lsb_req_vld <= 1'b0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            ds_temp_msb_req <= 8'b0;
            ds_temp_msb_req_vld <= 1'b0;
        end
        else if(end_cnt)begin
            ds_temp_msb_req <= din_temp;
            ds_temp_msb_req_vld <= 1'b1;
        end
        else begin
            ds_temp_msb_req_vld <= 1'b0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            ds_temp_req <= 0;
            ds_temp_req_vld <= 1'b0;
        end
        else if(ds_temp_msb_req_vld)begin
            ds_temp_req <= {ds_temp_msb_req,ds_temp_lsb_req};
            ds_temp_req_vld <= 1'b1;
        end
        else begin
            ds_temp_req_vld <= 1'b0;
        end
    end

    assign ds_temp_req_rev = {~ds_temp_req}+1;

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            temp_hex_dot <= 0;
            temp_hex_dot_vld <= 1'b0;
        end
        else if(ds_temp_req_vld)begin
            temp_hex_dot <= ((ds_temp_msb_req[7]? ds_temp_req_rev[3:0]:ds_temp_req[3:0])<<9)+((ds_temp_msb_req[7]? ds_temp_req_rev[3:0]:ds_temp_req[3:0])<<6)+((ds_temp_msb_req[7]? ds_temp_req_rev[3:0]:ds_temp_req[3:0])<<5)+((ds_temp_msb_req[7]? ds_temp_req_rev[3:0]:ds_temp_req[3:0])<<4)+(ds_temp_msb_req[7]? ds_temp_req_rev[3:0]:ds_temp_req[3:0]);
            temp_hex_dot_vld <= 1'b1;
        end
        else begin
            temp_hex_dot_vld <= 1'b0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            temp_hex_int <= 0;
            temp_hex_int_vld <= 1'b0;
        end
        else if(ds_temp_req_vld)begin
            temp_hex_int <= (ds_temp_msb_req[7]? ds_temp_req_rev[10:4]:ds_temp_req[10:4]);
            temp_hex_int_vld <= 1'b1;
        end
        else begin
            temp_hex_int_vld <= 1'b0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            temp_uns_sign <= 1'b0;
        end
        else if(ds_temp_msb_req_vld)begin
            temp_uns_sign <= ds_temp_msb_req[7];
        end
    end

    //将小数部分转化为BCD码;
    hex2bcd uut_bcd(
        .clk        (clk                ),
        .rst_n      (rst_n              ),
        .din        (temp_hex_dot       ),
        .din_vld    (temp_hex_dot_vld   ),
        .dout       (temp_dot_dout      ),
        .dout_vld   (temp_dot_dout_vld  )
    );

    //将整数部分转化为BCD码;
    bcd uut_bcd1(
        .clk        (clk                ),
        .rst_n      (rst_n              ),
        .din        ({1'b0,temp_hex_int}),
        .din_vld    (temp_hex_int_vld   ),
        .dout       (temp_int_dout      ),
        .dout_vld   (temp_int_dout_vld  )
    );

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            temp_uns <= 0;
        end
        else if(temp_int_dout_vld || temp_dot_dout_vld)begin
            temp_uns <= {{4{temp_uns_sign}},temp_int_dout,temp_dot_dout};
        end
    end

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt_vld <= 0;
        end
        else if(add_cnt_vld)begin
            if(end_cnt_vld)
                cnt_vld <= 0;
            else
                cnt_vld <= cnt_vld + 1;
        end
    end

    assign add_cnt_vld =temp_int_dout_vld || temp_dot_dout_vld;       
    assign end_cnt_vld = add_cnt_vld && cnt_vld==2-1;   

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            temp_uns_vld <= 1'b0;
        end
        else if(end_cnt_vld)begin
            temp_uns_vld <= 1'b1;
        end
        else begin
            temp_uns_vld <= 1'b0;
        end
    end

3.2.1.3、BCD模块和hex2bcd模块放在附件之中,原理可以参考至简设计系列_BCD译码实现

3.2.2、temp_tx模块

3.2.2.1、接口信号

信号 输入/输出 位宽 定义
clk I 1 系统时钟信号
rst_n I 1 系统复位信号
din I 32 温度数据输入信号
din_vld I 1 温度数据输入有效指示信号
uart_tx_din O 8 ASCII码输出信号
uart_tx_din_vld O 1 ASCII码输出信号有效指示信号
uart_rdy I 1 串口发送模块忙闲指示信号

3.2.2.1、参考代码

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            din_f <= 0;
        end
        else if(din_vld && !flag)begin
            din_f <= din;
        end
    end

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt <= 0;
        end
        else if(add_cnt)begin
            if(end_cnt)
                cnt <= 0;
            else
                cnt <= cnt + 1;
        end
    end

    assign add_cnt = flag && uart_rdy;       
    assign end_cnt = add_cnt && cnt== 20-1;

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            flag <= 1'b0;
        end
        else if(din_vld && !flag)begin
            flag <= 1'b1;
        end
        else if(end_cnt && flag)begin
            flag <= 1'b0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            uart_tx_din <= 1'b0;
        end
        else if(add_cnt)begin
            case(cnt)
            0 :  uart_tx_din <= 8'hB5;//当
            1 :  uart_tx_din <= 8'hB1;
            2 :  uart_tx_din <= 8'hC7;//前
            3 :  uart_tx_din <= 8'hB0;
            4 :  uart_tx_din <= 8'hCE;//温
            5 :  uart_tx_din <= 8'hC2;
            6 :  uart_tx_din <= 8'hB6;//度
            7 :  uart_tx_din <= 8'hC8;
            8 :  uart_tx_din <= 8'h3A;//:
            9 :  uart_tx_din <= data;//+或者-
            10,11,12,14,15,16,17
              :  uart_tx_din <= data +8'h30;//将数字转化为ASCII码
            13:  uart_tx_din <= 8'h2E;//.
            18:  uart_tx_din <= 8'h0D;//换行符
            19:  uart_tx_din <= 8'h0A;
            default : uart_tx_din <= 8'h00;
        endcase
    end
 end

    always  @(*)begin
        if(add_cnt)begin
            case(cnt)
                9  : 
                begin
                    if({din_f[31:28]}==4'h0)
                        data = 8'h2B;
                    else if({din_f[31:28]}==4'hF)
                        data = 8'h2D;
                    else 
                        data = 0;
                end
                10 : data = din_f[27:24];
                11 : data = din_f[23:20];
                12 : data = din_f[19:16];
                14 : data = din_f[15:12];
                15 : data = din_f[11:8 ];
                16 : data = din_f[7 :4 ];
                17 : data = din_f[3:0]  ;
               default : data = 0;
           endcase
       end 
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            uart_tx_din_vld <= 1'b0;
        end
        else if(add_cnt)begin
            uart_tx_din_vld <= 1'b1;
        end
        else begin
            uart_tx_din_vld <= 1'b0;
        end
    end

3.3、uart_tx串口发送模块

主要功能是将并行八位数据转化为按1位起始位,8位数据位,1位结束位的格式串行输出,波特率115200,由于上游模块一个时钟可以一次输入1byte数据,而本模块多个时钟一次只能输出1bit,故需要一个忙闲指示信号rdy来控制上游模块发数据的速度;

设计思路可以参考串口发送模块

3、蜂鸣器模块

本模块串口接收数据后按所收的八位二进制数据产生不同持续时间不同频率的PWM波驱动蜂鸣器,还要让OLED停止更新数据,也就是让OLED模块的输入数据有效指示信号在这段时间内无效,所以要引入一个标志信号flag,收到第一个数据拉高,收到最后一个数据拉低即可;串口发送的八位数据作用分别如下,第一个数据和最后一个数据最高位为1,用于产生flag信号,[6:4]用于确定PWM波持续产生的总时间,[3:0]用于确定PWM波的频率;

3.1、uart_rx串口接收模块

使用两个计数器实现,1位起始位,8位数据位,1位停止位,波特率位115200,关键是uart_rx信号是外来信号,应该打两拍减小异步信号的影响,防止产生亚稳态;设计思路可以参考串口接收模块

3.2、cy模块

3.2.1、参考代码

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            flag <= 1'b0;
        end
        else if(din_vld && din[7] && !flag)begin
            flag <= 1'b1;
        end
        else if(din_vld && din[7] && flag)begin
            flag <= 1'b0;
        end
    end

    always  @(*)begin
        if(rst_n==1'b0)begin
            beat <= 0;
            tone <= 0;
        end
        else if(din_vld)begin
            beat <= din[6:4];
            tone <= din[3:0];
        end
    end

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt <= 0;
        end
        else if(add_cnt)begin
            if(end_cnt)
                cnt <= 0;
            else
                cnt <= cnt + 1;
        end
    end

    assign add_cnt = beat_flag;       
    assign end_cnt = add_cnt && cnt==beat_length-1;

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            beat_flag <= 1'b0;
        end
        else if(din_vld)begin
            beat_flag <= 1'b1;
        end
        else if(end_cnt)begin
            beat_flag <= 1'b0;
        end
    end


    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            beat_length <= 0;
        end
        else if(din_vld)begin
            case(beat)
                0 : beat_length <= 12_000_000;//4拍;
                1 : beat_length <= 6_000_000;//2拍;
                2 : beat_length <= 2_760_000;//1拍;
                3 : beat_length <= 1_500_000 ;//1/2拍;
                4 : beat_length <= 750_000 ;//1/4拍;
                default : beat_length <= 0;
            endcase
        end
    end

    always  @(posedge clk or negedge rst_n)begin
    if(rst_n==1'b0)begin
        cycle <= 0;
    end
    else if(din_vld)begin
	case(tone)	
		0 : cycle <= 16'd22931;	//M1,
		1 : cycle <= 16'd20432;	//M2,
		2 : cycle <= 16'd18201;	//M3,
		3 : cycle <= 16'd17180;	//M4,
		4 : cycle <= 16'd15306;	//M5,
		5 : cycle <= 16'd13636;	//M6,
		6 : cycle <= 16'd12148;	//M7,
		7 : cycle <= 16'd11478;	//H1,
		8 : cycle <= 16'd10215;	//H2,
        9 : cycle <= 16'd9010 ;	//H3,
        10: cycle <= 16'd8590 ;	//H4,
        11: cycle <= 16'd7653 ;	//H5,
        12: cycle <= 16'd6818 ;	//H6,
        13: cycle <= 16'd6074 ;	//H7,
		default:cycle <= 16'd0;		//cycle为0,PWM占空比为0,低电平
	endcase
end
end

3.3、PWM模块参考代码

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt <= 0;
        end
        else if(add_cnt)begin
            if(end_cnt)
                cnt <= 0;
            else
                cnt <= cnt + 1;
        end
    end

    assign add_cnt = flag && beat_flag;       
    assign end_cnt = add_cnt && cnt==x-1;

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            pwm <= 1'b1;
        end
        else if(add_cnt && cnt==duty-1)begin
            pwm <= 1'b0;
        end
        else if(end_cnt || !flag || !beat_flag)begin
            pwm <= 1'b1;
        end
    end

4、OLED模块

要让OLED显示,应该要解决三个问题,下面三个模块解决这三个问题;

4.1、问题:什么时候显示,显示什么?

答:本设计要显示时间和温度并且蜂鸣器模块接收数据时显示不变,所以当温度数据有效指示信号或者时间有效指示信号有效,并且蜂鸣器模块标志信号flag无效时,将温度数据和时间数据传给下游模块对OLED进行显示,其他时刻均不传送有效信号给下游模块;这部分由temp_time_ control模块实现;

4.1.1、接口信号

信号 输入/输出 位宽 定义
clk I 1 系统时钟
rst_n I 1 系统复位
flag I 1 蜂鸣器模块标志信号
times I 20 时钟信号
times_vld I 1 时钟信号有效指示信号
temp_uns I 33 温度数据信号
temp_uns_vld I 1 温度数据有效指示信号
dout O 48 温度时钟拼接信号
dout_vld O 1 温度时钟拼接有效指示信号

4.1.2、参考代码

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            dout <= 0;
        end
        else if(temp_uns_vld || times_vld)begin
            dout <= {times,temp_uns[27:0]};
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            dout_vld <= 1'b0;
        end
        else if((times_vld || temp_uns_vld) && !flag)begin
            dout_vld <= 1'b1;
        end
        else begin
            dout_vld <= 1'b0;
        end
    end

4.2、问题:OLED显示需要发送什么?比如复位,初始化,数据转化

答:系统复位之后要先进行复位100ms,之后发送初始化指令,之后对屏幕进行清屏,然后就可以进行正常显示了,需要配置的OLED寄存器信息和显示字符所需要的字模都在配置表文件中保存,通过addr进行读取,10bit位宽的信号add_wdata为保存配置信息的信号,其中add_wdata[9]表示OLED的片选CS信息,当其为0时,表示此时有效;add_wdata[8]表示数据/指令指示信号,当其为1时,表示add_wdata[7:0]为8bit数据,当其为0时,表示add_wdata[7:0]为8bit指令;配置表前30个为OLED初始化所需指令,后面依次存放0,1,2,3,4,5,6,7,8,9,时,间,“:“这些需要显示字符的字模;主要通过一个状态机,外加两个计数器实现;通过oled_control模块实现;

4.2.1、接口信号

信号 输入/输出 位宽 定义
clk I 1 系统时钟
rst_n I 1 系统复位
din I 48 输入温度时钟拼接信号
din_vld I 1 输入温度时钟拼接有效指示信号
rdy I 1 SPI模块忙闲指示信号
wr_en O 1 SPI写使能信号
wdata O 10 SPI写数据信号
res O 1 OLED复位信号

4.2.2、这个模块代码不上传了,虽然很简单,由于有配置表,所以有七百多行代码;

4.3、问题:FPGA怎么将信号发给OLED?

答:该OLED模块通过SPI进行通信,所以采用SPI格式发送数据即可,用两个计数即可以实现;用SPI模块实现即可;

spi模块参考代码:

    always @(posedge clk or negedge rst_n)begin
        if(!rst_n)begin
            cnt0 <= 0;
        end
        else if(add_cnt0)begin
            if(end_cnt0)
                cnt0 <= 0;
            else
                cnt0 <= cnt0 + 1;
        end
    end

    assign add_cnt0 = flag;
    assign end_cnt0 = add_cnt0 && cnt0==6-1;

    always @(posedge clk or negedge rst_n)begin 
        if(!rst_n)begin
            cnt1 <= 0;
        end
        else if(add_cnt1)begin
            if(end_cnt1)
                cnt1 <= 0;
            else
                cnt1 <= cnt1 + 1;
        end
    end

    assign add_cnt1 = end_cnt0;
    assign end_cnt1 = add_cnt1 && cnt1==16-1;

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            flag <= 1'b0;
        end
        else if(wr_en && !flag)begin
            flag <= 1'b1;
        end
        else if(end_cnt1)begin
            flag <= 1'b0;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            wdata_ff0 <= 0;
        end
        else if(wr_en)begin
            wdata_ff0 <= wdata;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            cs <= 1'b1;
        end
        else if(flag&&cnt1<8)begin
            cs <= wdata_ff0[9];
        end
        else begin
            cs <= 1'b1;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            dc <= 1'b1;
        end
        else if(flag&&cnt1<8)begin
            dc <= wdata_ff0[8];
        end
        else begin
            dc <= 1'b1;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            sclk <= 1'b1;
        end
        else if(add_cnt0 && cnt0==0 && cnt1<8)begin
            sclk <= 1'b0;
        end
        else if(add_cnt0 && cnt0==3)begin
            sclk <= 1'b1;
        end
    end

    always  @(posedge clk or negedge rst_n)begin
        if(rst_n==1'b0)begin
            sdin <= 1'b0;
        end
        else if(add_cnt0 && cnt0==0 && cnt1<8)begin
            sdin <= wdata_ff0[7-cnt1];
        end
    end

    always  @(*)begin
        if(wr_en || flag)begin
            rdy = 1'b0;
        end
        else begin
            rdy = 1'b1;
        end
    end

 

总结:整个过程需要的代码其实很多网上都可以找到,但是大多都与我的编程风格差距太大,及其不便于阅读,调试,看起来跟C语言差不多,最后所有代码还是按我的风格写完了,其实与FPGA有关的代码调试在十多天之前就已经完成了,之后上位机卡了一段时间,再加上同时在学STM32和AD,所以一直没有录制视频上传;录制视频时那个温度其实PC显示和OLED显示在同一时间是相同的,由于一个手机录制,所以没有办法调节到相同时间看两处的信息,不管是温度值还是格式都是相同的,最后用PY写的上位机由于有50多M,所以没法上传,在使用时要先输入串口,输入一个串口对应数字就行,之后就可以按开始传输,把文件传输给FPGA;

附件下载
xjy_impl1.jed
FPGA程序烧录文件
hex2bcd.v
BCD.V
团队介绍
成都工业学院微电子科学与工程专业
团队成员
熊腾琼
大三开始学习STM32、FPGA、AD等软硬件设计的同学
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号