FPGA之DS18B20
0赞之前用单片机加proteus,来实现对DS18B20的驱动。也了解了DS18B20的驱动原理。然后就开始用FPGA来进行驱动了。
介绍这里就不介绍了,可以看看之前写的博文。
以下程序,只考虑外接一个DS18B20,因为开发板上就挂了一个。只读取温度16位值,没有读取其他寄存器值。需要其他操作,可以在程序的基础上改改就行了。也很简单的。主要是底层的驱动,发数据和读数据,复位和读取存在脉冲。
首先是底层的驱动,即复位,读取存在脉冲宽度,写数据,读数据。
首先是信号列表:
module DS18B02( input clk, input rst_n, input start, input mode, //write or read 1 read 0 write //ds18b20 interface inout DQ, input [7:0] rom_command, //rom cpmmand data ,this only use cc input [7:0] command, //command data this only use 44 and be // input [7:0] data, //write 8-bits data output reg[15:0] ds18b20_read_data, output reg read_finish, output reg command_finish , output look_dq_in );
说明一下。
Start: 启动DS18B20发数据或者读数据。
Mode: 控制模式,1表示读数据,0表示写数据。
DQ: DS18B20的数据端口。
rom_command: 器件命令。
Command: 操作命令。
ds18b20_read_data: 读取的8位值。
read_finish: 读取数据完成信号
command_finish: 发送命令完成信号。
至于最后一个是用chipscope调试用的,保存DQ作为输入时候的值。
首先介绍一下对DQ信号的处理,因为DQ是inout信号,所以不能直接进行操作的。需要进行一些处理。
reg DQ_reg; reg enable_read; assign DQ = enable_read ? 1'bz:DQ_reg; assign look_dq_in = enable_read ? DQ:1'b0;
这里定义了一个enable_read,当为1,表示DQ为输入,否则为输出。然后定义一个DQ_reg,用来设定DQ作为输出时候的输出值。look_dq_in在读使能的时候,为DQ值,否则为0,这样就保存了DQ作为输入时候的值。
然后就是状态机的设计:
localparam idle_state ='d0; localparam reset_state ='d1; localparam read_pulse_state ='d2; localparam write_rom_command_state ='d3; localparam write_command_state ='d4; localparam read_state ='d5;
总共5个状态。从名字也可以看出该状态是干嘛的。
第一个状态,idle_state:
idle_state: begin DQ_reg = 1'b1; enable_read = 1'b0; //default DQ is output delay_time_next = 'd0; bit_counter_next = 'd0; read_data_next = 'd0; ack_next = 'b1; if(start) state_next = reset_state; end
其实就是对一些变量清零。然后等待开始信号,一旦有开始信号,就跳转到复位状态,即复位总线。
这里设置enable_read为0,即DQ是作为输出的。输出的值由DQ_reg决定,所以为1.
这里说明一下上面一些变量作用:
delay_time_next和之后的delay_time是用来对状态的持续时间进行计数的。因为要满足要求时序,所以每个状态要需要制定的时间才能进行跳转。
bit_counter_next和bit_counter用来指示传输的第几位。
ack_next 和ack用来保存读取到的存在脉冲的响应。
read_data_next和read_data用来保存读取数据的中间移位的值。
第二个状态:复位状态
每次操作之前,需要对总线进行复位。
如上图,需要DQ发送低电平,时间为480us到960us之间。
如下程序:
reset_state: begin //DQ output 0 600us enable_read = 0; //DQ is output DQ_reg = 0; //DQ output 0 if(delay_time >= 30000 - 1 ) //delay 600us begin delay_time_next = 'd0; state_next = read_pulse_state; end else delay_time_next = delay_time + 1'b1; end
还是设定enable_read为0,DQ作为输出。然后DQ_reg赋值为0。那么DQ就输出0了。规定时间在480us到960us之间。这里就设定600us。然后通过delay_time_next和delay_time计数就可以了。
计时到600us后,就跳转到读取脉冲响应状态。然后清零计数器。以便下一状态计数。
第三个状态:读取存在脉冲。
在复位之后,需要读取存在脉冲。即判断外部有DS18B20器件。时序就是复位之后,DQ作为输入。然后在最大60us之后,外部DS18B20会把DQ给置低。保持一段时间,然后在释放总线。(注意DQ是要接上拉电阻的,所以释放总线后,总线就输出高电平)。
然后这个读取过程至少要480us。
以下程序:
read_pulse_state: begin enable_read = 1; //DQ is input DQ_reg = 1; if(delay_time == 3500 - 1) //delay 70us ack_next = DQ; if(delay_time >= 25000 - 1 ) //delay 500us begin delay_time_next = 0; //if receive pulse ack, write rom command, otherwise there is no ds18b20 is bus,go idle_state if(ack == 0) state_next = write_rom_command_state; else state_next = idle_state; end else delay_time_next = delay_time + 1'b1; end
这里将enable_read置1,表示DQ为输入。DQ_reg 赋值1,这里给什么其实没有什么影响。但是考虑到后面如果DQ_reg为0,当enable_read为0,DQ作为输出,那么DQ会输出一个0,可能就会产生误操作了。
等待70us,读取DQ的值,这时候DQ的值应该为0.等待500us,跳转到写器件命令状态。满足设定的最小480us设定。
这里要注意的就是读取DQ值的时机,不能太早,也不能太晚。不然可能会读取不到低电平的ACK信号。
第四个状态:写器件命令状态
对于写数据,数据是1位1位的写。对于每一位,写1和写0的时序是不同的。这里操作就是,FPGA拉低DQ,保持2us时间。然后判断写的数据是0还是1,是0的话,继续拉低,是1的话,就拉高。然后总共的时间为100us。当写0的时候,在90us的时候,就要拉高DQ。
以上的操作是符合规定的时序的。其实只要符合规定的时序,想怎么操作就怎么操作。
以下代码:
write_rom_command_state: begin enable_read = 0; //DQ is output if(delay_time < 99) //DQ output 0 keep 2us DQ_reg = 0; else if(delay_time < 4500 - 1) //delay 90us if(rom_command[bit_counter] == 1) DQ_reg = 1; else DQ_reg = 0; else DQ_reg = 1; if(delay_time >= 5000 - 1) //delay 100us begin delay_time_next = 0; if(bit_counter == 'd7) begin bit_counter_next = 'd0; state_next = write_command_state; end else bit_counter_next = bit_counter + 1'b1; end else delay_time_next = delay_time + 1'b1; end
首先,设定enable_read为0.表示DQ为输出。在2us时间内,DQ输出0.在2us到90us时间内,判断写的数据为0还是1,从而判断DQ的值。90us到100us时间,DQ输出1.整个操作时间为100us。100us时间到,就写了1位。然后就判断是不是写了8位数据,如果写了,就状态跳转。否则将计数器清零,再次执行这个状态。
第五个状态:写命令状态
这里时序和写器件命令时序一样的,只是写的数据不一样而已。这个时候写的数据为command。
以下代码:
write_command_state: begin enable_read = 0; //DQ is output if(delay_time < 99) //DQ output 0 keep 2us DQ_reg = 0; else if(delay_time < 4500 - 1) //delay 90us if(command[bit_counter] == 1) DQ_reg = 1; else DQ_reg = 0; else DQ_reg = 1; if(delay_time >= 5000 - 1) //delay 100us begin delay_time_next = 0; if(bit_counter == 'd7) begin bit_counter_next = 'd0; command_finish = 1'b1; //if write, write finish ,go idle_state //if read, go read_state if(mode == 0) state_next = idle_state; else state_next = read_state; end else bit_counter_next = bit_counter + 1'b1; end else delay_time_next = delay_time + 1'b1; end
这里要注意的是状态跳转,在写模式下,即写命令,写完后,跳转回idle状态。但是在读模式下,接着就要进行数据读取了,所以要跳转到读模式状态。
然后就是读数据了。读数据要稍微麻烦点。
第六个状态,读数据。
这里操作是:DQ首先作为输出,拉低保持2us。然后DQ作为输入,在10us时间读取DQ数据,90us时间后,DQ作为输出。操作时间为100us。也是符合规定的时序的。
read_state: begin if(delay_time < 99) //DQ output 0 keep 2us begin enable_read = 0; // DQ is output DQ_reg = 0; //DQ output 0 end else if(delay_time < 4500 - 1) //delay 90us begin enable_read = 1; if(delay_time == 500 -1) //at 10us read DQdata read_data_next = {DQ,read_data[15:1]}; end else begin enable_read = 0; // DQ is output DQ_reg = 1; end if(delay_time >= 5000 - 1) //delay 100us begin delay_time_next = 0; if(bit_counter == 'd15) begin bit_counter_next = 'd0; state_next = idle_state; read_finish = 1'b1; end else bit_counter_next = bit_counter + 1'b1; end else delay_time_next = delay_time + 1'b1; end
这里在2us时间内,enable_read为0,DQ输出,DQ输出0。 2us时间到90us时间内,enable_read为1,DQ作为输入,在10us时间的时候,读取DQ值,存在移位寄存器的高位中。
时间到100us后判断是否读取了16个数据。因为温度的值是以16位数据保存的。读了16个数据,就状态跳转。否则计数器清零,再次执行这个状态。
以上就是核心的底层驱动。剩下就是在顶层例化这个模块,然后发对应命令即可。
在外部另外编写代码,进行命令发送和数据读取。
信号列表:
module DS18B20_control( input clk, input rst_n, input start_ds18b20, inout DQ, output [15:0] read_data, output read_finish, output look_dq_in );
这里的信号就不用介绍了,看名字就知道这些信号是干啥的。
然后就是状态机的设计,
localparam idle_state = 'd0; localparam write_command_state = 'd1; localparam wait_convert_state = 'd2; localparam read_temperature_state = 'd3;
定义了4个状态:
第一个状态:idle状态。
这个状态就是不操作DS18B20的时候,各个信号值的状态是怎么样的。
idle_state: begin delay_time_next = 'd0; if(start_ds18b20) state_next = write_command_state; end
代码也比较简单,判断是否有使能操作DS18B20操作信号,有就状态跳转,否则就不跳转。
第二个状态:write_command_state,写命令状态
这个状态,其实就是写命令0xcc和0x44。开启DS18B20转化温度。
write_command_state: begin rom_command = 8'hcc; command = 8'h44; start = 1; if(command_finish == 1) begin start = 0; state_next = wait_convert_state; end end
器件命令赋值cc,命令赋值44。然后开启DS18B20,等待command_finish信号。当两个命令发送完成后。会反馈command_finish为高。检测到为高后,就状态跳转。
第三个模块:wait_convert_state。等待转换
DS18B20转换温度是需要时间的。不能说你发完命令后,就可以直接读取值。因为这个时候值还没有变化。所以需要等待时间。这里是等待1s。
wait_convert_state: begin if(delay_time >= 50_000_000 - 1) //delay 1s //if(delay_time >= 50_0 - 1) //delay 1s state_next = read_temperature_state; else delay_time_next = delay_time + 1'b1; end
注意,这里状态跳转的时候,没有对计数器清零。因为在idle_state的时候,有对计数器清零。
第四个状态:read_temperature_state,读取温度状态。
这个状态,就要读取温度了。
read_temperature_state: begin rom_command = 8'hcc; command = 8'hBE; start = 1; mode = 'd1; if(read_finish) begin start = 0; state_next = idle_state; end end
首先,器件命令为cc,操作命令为BE。表示读暂存寄存器。然后就开始读值。当read_finish为1的时候,表示16位温度数据读取结束。然后就可以进行状态的跳转了。
以上就是DS18B20的驱动了。将这个模块封装。给外部调用。外部调用的时候,给start_ds18b20信号。然后等待read_finish。当这个信号为1,读取温度结束,然后就可以从read_data读取16位温度值了。
这里为了能下到开发板中,看到实验现象。在外部写了一个顶层代码,调用上述模块。
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: // Engineer: // // Create Date: 18:09:53 11/27/2014 // Design Name: // Module Name: DS18B20_top // Project Name: // Target Devices: // Tool versions: // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// module DS18B20_top( input clk, input rst_n, inout DS18B20, output [7:0] LED, output look_dq_in ); wire [15:0] read_data; reg start_ds18b20; wire read_finish; localparam idle_state = 'd0; localparam read_state = 'd1; reg state; always@(posedge clk or negedge rst_n) begin if(!rst_n) begin state <= idle_state; start_ds18b20 <= 1'b0; end else case(state) idle_state: state <= read_state; read_state: begin if(read_finish == 1) begin state <= idle_state; start_ds18b20 <= 1'b0; end else start_ds18b20 <= 1'b1; end endcase end DS18B20_control DS18B20_control_1( .clk(clk), .rst_n(rst_n), .start_ds18b20(start_ds18b20), .DQ(DS18B20), .read_data(read_data[15:0]), .read_finish(read_finish), .look_dq_in(look_dq_in) ); assign LED = ~read_data[11:4]; endmodule
代码也很简单。最后读到的温度值。将11位到4位值赋给8位LED。这样看LED灯就知道读取温度值为多少了。
最后,将代码综合,分配管脚,生成bit。下载到芯片中。可以看到LED灯亮了。指示当前检测到温度为多少。