FPGA之Verilog他山之石系列2

Posted by

前言:

自Verilog他山之石系列1 上电只运行一次的计数器跟大家见面,收到初学者的喜爱。的确,有事我们需要思考以下别人设计的巧妙之处,跟其他工程师在技术上对话。

笔者根据自己多年的实践经验、公司同事的经验以及国内外优秀开源FPGA代码纯逻辑设计层面,总结一些核心高价值代码片段,做成FPGA之Verilog他山之石系列。具体系列能写道多少,多久,本人也无法预料。

这是本人供职的天津大学四川院集成电路中心的网站,所以文章就从这个地方首发,会跟本人在知乎的账号上随想或者文章同步,欢迎FPGA初学者参考。

如果文章代码有错误也请指出,供大家进步。 写作方式: 代码、解析、仿真波形等。

FPGA之Verilog他山之石系列之2 状态机内每个状态,以流程方式规定每个时钟的动作

上代码

always @ (posedge clk_1us or negedge rst_n) begin
    if(!rst_n) begin
        flow_cnt     <=  4'b0;
        init_done    <=  1'b0;
        cnt_1us_en   <=  1'b1;
        dq_out       <=  1'bZ;
        st_done      <=  1'b0;
        rd_data      <= 16'b0;
        rd_cnt       <=  5'd0;
        wr_cnt       <=  4'd0;
        cmd_cnt      <=  3'd0;
    end
    else begin
        st_done <= 1'b0;
        case (next_state)
            init:begin                              //初始化
                init_done <= 1'b0;
                case(flow_cnt)
                    4'd0:
                        flow_cnt <= flow_cnt + 1'b1;
                    4'd1: begin                 //发出500us复位脉冲
                        cnt_1us_en <= 1'b1;         
                        if(cnt_1us < 20'd500)
                            dq_out <= 1'b0;         
                        else begin
                            cnt_1us_en <= 1'b0;
                            dq_out <= 1'bz;
                            flow_cnt <= flow_cnt + 1'b1;
                        end
                    end
                    4'd2:begin                      //释放总线,等待30us
                        cnt_1us_en <= 1'b1;
                        if(cnt_1us < 20'd30)
                            dq_out <= 1'bz;
                        else
                            flow_cnt <= flow_cnt + 1'b1;
                    end
                    4'd3: begin                     //检测响应信号
                        if(!dq)
                            flow_cnt <= flow_cnt + 1'b1;
                        else
                            flow_cnt <= flow_cnt;
                    end
                    4'd4: begin                     //等待初始化结束
                        if(cnt_1us == 20'd500) begin
                            cnt_1us_en <= 1'b0;
                            init_done  <= 1'b1;     //初始化完成
                            flow_cnt   <= 4'd0;
                        end
                        else
                            flow_cnt <= flow_cnt;
                    end
                    default: flow_cnt <= 4'd0;
                endcase
            end
            rom_skip: begin                         //加载跳过ROM操作指令
                wr_data  <= ROM_SKIP_CMD;
                flow_cnt <= 4'd0;
                st_done  <= 1'b1;
            end
            wr_byte: begin                          //写字节状态(发送指令)
                if(wr_cnt <= 4'd7) begin
                    case (flow_cnt)
                        4'd0: begin
                            dq_out <= 1'b0;         //拉低数据线,开始写操作
                            cnt_1us_en <= 1'b1;     //启动计时器
                            flow_cnt <= flow_cnt + 1'b1;
                        end
                        4'd1: begin                 //数据线拉低1us
                            flow_cnt <= flow_cnt + 1'b1;
                        end
                        4'd2: begin
                            if(cnt_1us < 20'd60)    //发送数据
                                dq_out <= wr_data[wr_cnt];
                            else if(cnt_1us < 20'd63)   
                                dq_out <= 1'bz;     //释放总线(发送间隔)
                            else
                                flow_cnt <= flow_cnt + 1'b1;
                        end
                        4'd3: begin                 //发送1位数据完成
                            flow_cnt <= 0;
                            cnt_1us_en <= 1'b0;
                            wr_cnt <= wr_cnt + 1'b1;//写计数器加1
                        end
                        default : flow_cnt <= 0;
                    endcase
                end
                else begin                          //发送指令(1Byte)结束
                    st_done <= 1'b1;
                    wr_cnt <= 4'b0;
                    cmd_cnt <= (cmd_cnt == 3'd4) ?  //标记当前发送的指令序号
                               3'd1 : (cmd_cnt+ 1'b1);
                end
            end
            temp_convert: begin                     //加载温度转换命令
                wr_data <= CONVERT_CMD;
                st_done <= 1'b1;
            end
            delay: begin                            //延时500ms等待温度转换结束
                cnt_1us_en <= 1'b1;
                if(cnt_1us == 20'd500000) begin
                    st_done <= 1'b1;
                    cnt_1us_en <= 1'b0;
                end 
            end 
            rd_temp: begin                          //加载读温度命令
                wr_data <= READ_TEMP;
                bit_width <= 5'd16;                 //指定读数据个数
                st_done <= 1'b1;
            end
            rd_byte: begin                          //接收16位温度数据
                if(rd_cnt < bit_width) begin
                    case(flow_cnt)
                        4'd0: begin
                            cnt_1us_en <= 1'b1;
                            dq_out <= 1'b0;         //拉低数据线,开始读操作
                            flow_cnt <= flow_cnt + 1'b1;
                        end
                        4'd1: begin
                            dq_out <= 1'bz;         //释放总线并在15us内接收数据
                            if(cnt_1us == 20'd14) begin
                                rd_data <= {dq,rd_data[15:1]};
                                flow_cnt <= flow_cnt + 1'b1 ;
                            end
                        end
                        4'd2: begin
                            if (cnt_1us <= 20'd64)  //读1位数据结束
                                dq_out <= 1'bz;
                            else begin
                                flow_cnt <= 4'd0;   
                                rd_cnt <= rd_cnt + 1'b1;//读计数器加1
                                cnt_1us_en <= 1'b0;
                            end
                        end
                        default : flow_cnt <= 4'd0;
                    endcase
                end
                else begin
                    st_done <= 1'b1;
                    temp_data_r  <= {rd_data[15],rd_data[10:4]};
                    rd_cnt <= 5'b0;
                end
            end
            default: ;
        endcase
    end 
end

这是一个单总线温度传感器读取数据代码,可以把读取过程分成几个状态。每个状态共用一个每个时钟跳动一次的计数器flow_cnt。从名字上明确指出就是一个流水式的计数器, case (flow_cnt) ,用flow_cnt标识状态,在每个状态内最后对flow_cnt加1,确保下个时钟能到下一个状态。

例子看到可以在两个状态共用一个这种状态寄存器。

这种设计非常适合流程式的操作,例子中就是需要指定时序的单总线控制,适合采用这种状态机内有flow_cnt来控制在一个状态到底呆多少时钟。

具体实现什么功能大家可以不关心,只需要记住这种写法即可。仿真的时候,就重点盯住flow_cnt看,自己设计这种状态机,就需要在每个状态规划好跳转到下个状态的触发条件。

例子用cnt_1us来触发状态跳转们。可以参考。

如果自己练习可以把自己开发板的LED设置成几个状态,每个状态停留不同的时长,来用以上的设计体会这种状态机设计的原理。

因为比较简单,就没有贴仿真图片,见谅。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

我们将24小时内回复。
取消