前言:
自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设置成几个状态,每个状态停留不同的时长,来用以上的设计体会这种状态机设计的原理。
因为比较简单,就没有贴仿真图片,见谅。
发表回复