FPGA ——DS18B20 数字温度传感器驱动实战文章摘要本文面向 FPGA 零基础入门学习者完整讲解 DS18B20 数字温度传感器硬件特性、1-Wire 单总线协议时序规范采用三段式状态机完成 Verilog 驱动开发配套补码转换、BIN 转 BCD、数码管动态显示全套模块化代码最后补充上板实操要点与新手踩坑总结适合 Quartus 工程复刻、课程实验复习代码可直接综合下载至 FPGA 开发板运行。文章目录FPGA ——DS18B20 数字温度传感器驱动实战文章摘要一、器件特点二、操作整体流程三、1-Wire 单总线协议3.1 物理层3.2 协议层3.2.1 基础时序时序参数对比表3.2.2 指令集分类ROM 指令用于匹配总线上不同传感器设备功能指令操作传感器内部寄存器3.2.3 数据存储格式四、Verilog 模块化代码实现4.1 模块整体设计思路4.2 核心功能代码五、上板验证一、器件特点DS18B20 是常用数字温度传感器依靠1-Wire 单总线完成通信硬件接线简单非常适合 FPGA 入门外设驱动练习。通信方式单总线1-Wire仅需一根 IO 完成双向数据交互供电方案外部供电外接独立 VCC 供电稳定性更好工程常用寄生供电数据线 DQ 通过上拉电阻供电无需独立电源布线简单但长距离传输容易不稳定分辨率配置可配置 9/10/11/12bit 温度分辨率9bit转换时间 93.75ms10bit转换时间 187.5ms11bit转换时间 375ms12bit转换时间 750ms芯片上电默认配置本实验采用测温范围-55℃ ~ 125℃多设备支持单根总线挂载多个 DS18B20依靠唯一 64 位 ROM 地址区分不同传感器封装形式TO-92、SOP8 等我的板子只挂载了 1 片 DS18B20直接使用Skip ROM指令不用处理 ROM 寻址降低代码复杂度。二、操作整体流程主机FPGA和 DS18B20 通信固定流程FPGA 发送复位脉冲拉低 DQ 总线完成初始化等待 DS18B20 返回存在脉冲判断传感器是否在线主机发送 ROM 指令设备匹配、读取序列号等主机发送 功能指令启动温度转换、读取寄存器数据等等待温度转换完成读取寄存器原始温度数据三、1-Wire 单总线协议3.1 物理层总线结构单根 DQ 双向数据线必须搭配典型4.7kΩ 上拉电阻在代码书写过程中需要使用到三态门。电平逻辑总线空闲上拉电阻将总线拉至高电平主机拉低总线输出逻辑 0、产生复位脉冲主机释放总线由上拉电阻恢复高电平驱动规则主机、从机均为开漏输出只能主动拉低总线无法直接驱动总线为高高电平依靠外部上拉电阻实现拓扑支持一主多从架构总线布线不宜过长长线传输容易出现时序采样错误3.2 协议层3.2.1 基础时序分析代码只实现了复位时序没有采样判断存在脉冲无法识别传感器是否正常挂载工程中建议增加脉冲采样判断本实验为简化入门流程暂未实现。复位时序初始化主机拉低 DQ ≥480us主机释放总线等待从机拉低总线 15~60us存在脉冲主机等待存在脉冲结束初始化完成写时序主机 → 从机写 0主机拉低总线 ≥60us写 1主机拉低总线 1~15us 后释放总线两次写操作间隔 ≥1us读时序从机 → 主机主机拉低总线 1us 以内随后释放总线在拉低后的15us 内采样总线电平获取 bit 数据采样窗口很窄时序出错必读错温度单次读时隙总时长 ≥60us时序参数对比表时序类型拉低时间采样窗口总时隙本实验代码参数值50MHz 时钟复位时序≥480us15~60us存在脉冲960usTIME_480us 24_000、TIME_960us 48_000写 0≥60us无持续拉低60~120usTIME_60us 3_000写 11~15us无释放总线60~120usTIME_1us 50、TIME_60us 3_000读时序1us 以内拉低后 15us 内采样≥60usTIME_1us 50、TIME_15us 750、TIME_60us 3_000说明本实验代码中TIME_15us/2即 7.5us 处采样位于 15us 采样窗口内写 1 时主机在TIME_1us后释放总线由外部上拉电阻恢复高电平。3.2.2 指令集分类ROM 指令用于匹配总线上不同传感器设备0x33Read ROM读取 DS18B20 64 位唯一 ROM 序列号仅单设备总线可用0x55Match ROM匹配指定 ROM 地址的传感器多设备寻址使用0xCCSkip ROM跳过 ROM 匹配总线只有 1 个传感器时使用本次实验使用0xF0Search ROM总线设备搜索枚举全部传感器地址0xECAlarm Search搜索触发温度报警的传感器功能指令操作传感器内部寄存器0x44Convert T启动温度转换0xBERead Scratchpad读取高速暂存寄存器温度高低字节、配置寄存器等 9 字节数据0x4EWrite Scratchpad写入配置寄存器设置分辨率、上下限报警温度0x48Copy Scratchpad将报警阈值、配置参数存入非易失 EEPROM0xB8Recall E2从 EEPROM 加载参数到暂存寄存器0xB4Read Power Supply读取传感器供电模式寄生供电 / 外部供电3.2.3 数据存储格式高速暂存寄存器一共 9 字节表格字节序号作用0温度低 8 位LSB1温度高 8 位MSB2TH 高温报警阈值3TL 低温报警阈值4配置寄存器设置分辨率5~7保留寄存器8CRC 校验字节温度计算规则12bit 模式正数原始数值 × 0.0625 ℃负数对补码取反加 1再 × 0.0625 ℃四、Verilog 模块化代码实现4.1 模块整体设计思路本次工程实现单 DS18B20 实时测温 数码管动态显示温度仅用到指令0xCC(Skip ROM)、0x44(启动转换)、0xBE(读取温度)采用三段式状态机编写 DS18B20 驱动核心模块工程模块化拆分ds_ctrlDS18B20 底层时序驱动输出 16bit 温度补码 数据有效标志data_ctrl温度补码处理正负温度换算bin_to_bcd二进制数值转换为 BCD 码适配数码管显示smg6 位动态数码管驱动模块top顶层模块完成各个子模块信号连线模块化优势各个功能解耦方便单独仿真调试后期修改分辨率、增加多传感器时改动更小此代码为练习的时候写的后续会持续优化。4.2 核心功能代码ds18b20控制模块也是最主要的功能模块具体的分析可以看代码注释//上电-按键按下-初始化时序控制DQ按照固定时序发送0和1-初始化完成后控制DQ发送CC跳过ROM然后发送温度转换指令DQ恢复高阻等待750ms //-初始化时序控制DQ按照固定时序发送0和1-初始化完成后控制DQ发送CC跳过ROM然后发送温度读取指令释放DQ完成温度数据的读取 module ds_ctrl ( input clk , input rst_n , inout dq ,//读写操作总线 output [15:0] data ,//输出二进制温度的补码 output data_vld //温度数据有效信号与数据对齐 ); //参数 parameter TIME_1us 50 ,//1us TIME_15us 750 ,//15us TIME_60us 3_000 ,//60us TIME_480us 24_000 ,//480us TIME_960us 48_000 ,//960us TIME_750ms 37_500_000,//750ms BIT_MAX_W 8 ,//一字节比特数、写的时候按字节 BIT_MAX_R 16 ,//读的时候按温度数据的长度 BYTE_MAX 2 ;//字节最大数、这里一个指令组只有两个字节指令 //工作状态:如果初始化只为1个状态就要考虑初始化到指令组1和指令组2的条件 //这里可以设置一个单比特的reg变量在CMD_1和CMD_2进行翻转其他状态保持在初始化状态就作为状态转移的条件 localparam INIT 5b00001, CMD_1 5b00010, WAIT 5b00100, CMD_2 5b01000, READ 5b10000; reg [4:0] state_c,state_n; //状态转移:初始化时序、发送指令组合1 CC和44、等待750ms、初始化、发送指令组合2 CC和BE、采样读取完成之后又重复 wire INIT_CMD_1 , CMD_1_WAIT , WAIT_INIT , INIT_CMD_2 , CMD_2_READ , READ_INIT ; //内部信号 reg [25:0] cnt_time;//时序时间计数器 reg [3:0] cnt_bit ;//比特数计数器一个字节8bit reg cnt_byte;//字节计数器在指令组别状态来给指令寄存作变换条件 reg [7:0] cmd_reg ;//指令寄存器 reg cmd_sw ;//指令组别寄存 reg [15:0] data_reg;//16bit补码内容采样寄存器 reg dq_en ;//主机使能 reg dq_W ;//写入的内容 //三态门 assign dq dq_en ? dq_W : 1bz;//dq_en为1就对dq进行操作为0就高阻断路交给从机 //状态机第一段 always (posedge clk or negedge rst_n) begin if (rst_n 0) begin state_c INIT; end else begin state_c state_n; end end //状态机第二段 always (*) begin if (rst_n 0) begin state_n INIT; end else begin case (state_c) INIT : state_n (INIT_CMD_1) ? CMD_1 : (INIT_CMD_2) ? CMD_2 : INIT; CMD_1 : state_n (CMD_1_WAIT) ? WAIT : CMD_1 ; WAIT : state_n (WAIT_INIT ) ? INIT : WAIT ; CMD_2 : state_n (CMD_2_READ) ? READ : CMD_2 ; READ : state_n (READ_INIT ) ? INIT : READ ; default: state_n INIT; endcase end end //转移条件 assign INIT_CMD_1 (state_c INIT ) cmd_sw 0 cnt_time TIME_960us - 1; assign CMD_1_WAIT (state_c CMD_1) cnt_byte BYTE_MAX - 1 cnt_bit BIT_MAX_W - 1 cnt_time TIME_60us TIME_1us - 1; assign WAIT_INIT (state_c WAIT ) cnt_time TIME_1us - 1; assign INIT_CMD_2 (state_c INIT ) cmd_sw 1 cnt_time TIME_960us - 1; assign CMD_2_READ (state_c CMD_2) cnt_byte BYTE_MAX - 1 cnt_bit BIT_MAX_W - 1 cnt_time TIME_60us TIME_1us - 1; assign READ_INIT (state_c READ ) cnt_bit BIT_MAX_R - 1 cnt_time TIME_60us TIME_1us - 1; //状态机第三段 always (posedge clk or negedge rst_n) begin if (rst_n 0) begin cnt_time 0; cnt_bit 0; cnt_byte 0; cmd_reg 0; cmd_sw 0; dq_en 0; dq_W 0; end else begin case (state_c) INIT :begin//初始化状态 if (cnt_time TIME_960us - 1) begin//整个过程持续960us cnt_time 0; end else begin cnt_time cnt_time 1; end cnt_bit 0; cnt_byte 0; cmd_reg 0; cmd_sw cmd_sw; if (cnt_time TIME_480us) begin dq_en 1; end else begin dq_en 0; end dq_W 0; end CMD_1 :begin//指令组1状态两个指令共两个字节 if (cnt_time TIME_60us TIME_1us - 1) begin//1us主机开始时序、60us总工作时隙、1us的结束时隙rom指令和工作指令一起共62*2us cnt_time 0; end else begin cnt_time cnt_time 1; end //比特计数 if (state_c CMD_1 cnt_time TIME_60us TIME_1us - 1) begin if (cnt_bit BIT_MAX_W - 1) begin cnt_bit 0; end else begin cnt_bit cnt_bit 1; end end //字节计数器 if (state_c CMD_1 cnt_bit BIT_MAX_W - 1 cnt_time TIME_60us TIME_1us - 1) begin//cnt_bit加到7且执行完该bit任务后才对字节加1 if (cnt_byte BYTE_MAX - 1) begin cnt_byte 0; end else begin cnt_byte cnt_byte 1; end end cmd_reg (cnt_byte 0) ? 8hCC : 8h44;//相当于if else cmd_sw (state_c CMD_1 cnt_byte BYTE_MAX - 1 cnt_bit BIT_MAX_W - 1 cnt_time TIME_60us TIME_1us - 1) ? ~cmd_sw : cmd_sw;//指令组状态进行翻转 if (cnt_time TIME_1us) begin//每次开始都要进行开始时隙 dq_en 1; dq_W 0; end else if(cnt_time TIME_1us cnt_time TIME_60us)begin//工作时隙 dq_en ~cmd_reg[cnt_bit];//写0的时候就把dq给到主机工作写1就释放dq dq_W 0; end else begin//停止时隙必须释放dq dq_en 0; end end WAIT :begin//等待750ms //750ms计数器 if (cnt_time TIME_1us - 1) begin cnt_time 0; end else begin cnt_time cnt_time 1; end cnt_bit 0; cnt_byte 0; cmd_reg 0; cmd_sw cmd_sw;//保持指令组标志否则会一直重复CMD_1而走不到CMD_2/READ dq_en 0; dq_W 0; end CMD_2 :begin//指令组2状态两个指令共两个字节 //1us主机开始时序、60us总工作时隙、1us的结束时隙——进行一比特内容的写入 if (cnt_time TIME_60us TIME_1us - 1) begin cnt_time 0; end else begin cnt_time cnt_time 1; end //写比特计数 if (state_c CMD_2 cnt_time TIME_60us TIME_1us - 1) begin if (cnt_bit BIT_MAX_W - 1) begin cnt_bit 0; end else begin cnt_bit cnt_bit 1; end end //写字节计数器 if (state_c CMD_2 cnt_bit BIT_MAX_W - 1 cnt_time TIME_60us TIME_1us - 1) begin//cnt_bit加到7且执行完该bit任务后才对字节加1 if (cnt_byte BYTE_MAX - 1) begin cnt_byte 0; end else begin cnt_byte cnt_byte 1; end end cmd_reg (cnt_byte 0) ? 8hCC : 8hBE;//相当于if else cmd_sw (state_c CMD_2 cnt_byte BYTE_MAX - 1 cnt_bit BIT_MAX_W - 1 cnt_time TIME_60us TIME_1us - 1) ? ~cmd_sw : cmd_sw;//指令组状态进行翻转 if (cnt_time TIME_1us) begin//每次开始都要进行开始时隙 dq_en 1; dq_W 0; end else if(cnt_time TIME_1us cnt_time TIME_60us)begin//工作时隙 dq_en ~cmd_reg[cnt_bit];//写0的时候就把dq给到主机工作写1就释放dq dq_W 0; end else begin//停止时隙必须释放dq dq_en 0; end end READ :begin//读状态、现在需要读取16bit也就是两个字节的内容 //1us主机开始时序、60us总工作时隙、1us的结束时隙——进行一比特内容的读出采样 if (cnt_time TIME_60us TIME_1us - 1) begin cnt_time 0; end else begin cnt_time cnt_time 1; end //比特计数器、这里因为要读取16bit数采样给data_reg所以最大值为16 if (state_c READ cnt_time TIME_60us TIME_1us - 1) begin if (cnt_bit BIT_MAX_R - 1) begin cnt_bit 0; end else begin cnt_bit cnt_bit 1; end end cnt_byte 0; cmd_reg 0;//这里是读所以用不到指令 cmd_sw cmd_sw; if (cnt_time TIME_1us) begin//每次开始都要进行开始时隙 dq_en 1; dq_W 0; end else if(cnt_time TIME_1us cnt_time TIME_60us)begin//工作时隙 dq_en 0;//释放总线交给从机否则主机一直拉低只会读到0 if (cnt_time TIME_15us/2) begin//在15us内进行采样 data_reg[cnt_bit] dq; end end else begin//停止时隙必须释放dq dq_en 0; end end default:begin cnt_time 0; cnt_bit 0; cnt_byte 0; cmd_reg 0; cmd_sw 0; dq_en 0; dq_W 0; end endcase end end //输出信号 assign data data_reg ; assign data_vld READ_INIT; endmodule补码转原码模块(这里也可以不用把这个模块单独拎出来)module data_ctrl ( input clk , input rst_n , input [15:0] data ,//温度传感器直接输出的补码 input data_vld, output [19:0] data_in ); parameter TIME 50_000_000; reg [25:0] cnt_time ; reg [15:0] data_conver ; reg [19:0] data_in_reg ; always (posedge clk or negedge rst_n) begin if (rst_n 0) begin cnt_time 0; end else if (cnt_time TIME - 1) begin cnt_time 0; end else begin cnt_time cnt_time 1; end end always (posedge clk or negedge rst_n) begin if (rst_n 0) begin data_conver 0; end else if (data_vld) begin if (data[15] 0) begin data_conver data; end else begin data_conver ~data 1; end end end // 采样到寄存器只在 1s 边界更新一次 always (posedge clk or negedge rst_n) begin if (rst_n 0) begin data_in_reg 0; end else if (cnt_time TIME - 1) begin data_in_reg data_conver[11:0] * 625; // 只取有效温度 end end assign data_in data_in_reg; endmodule二进制转BCD码模块module bin_to_bcd ( input clk , input rst_n , input [19:0] data_in , //输入待转换的数据 output [23:0] data_bcd //输出转换后的BCD码数据 ); reg [43:0] data_reg ; //寄存和扩编data_in //BCD码转换 always (posedge clk or negedge rst_n) if(rst_n 0) data_reg 0; else begin //寄存和扩编data_ave(扩编16位是根据要得到的个BCD码数据来判断) data_reg {24b0,data_in}; //循环13次判断移位操作(次数是根据位宽来判断) repeat(20) begin //判断是否大于等于5,是就加3 if(data_reg[23:20] 5) data_reg[23:20] data_reg[23:20] 3; if(data_reg[27:24] 5) data_reg[27:24] data_reg[27:24] 3; if(data_reg[31:28] 5) data_reg[31:28] data_reg[31:28] 3; if(data_reg[35:32] 5) data_reg[35:32] data_reg[35:32] 3; if(data_reg[39:36] 5) data_reg[39:36] data_reg[39:36] 3; if(data_reg[43:40] 5) data_reg[43:40] data_reg[43:40] 3; //判断不大于等于5就进行移位 data_reg data_reg 1; end end //输出的8421BCD码 assign data_bcd data_reg[43:20]; endmodule数码管显示模块module smg ( input clk , input rst_n , input [23:0] num , output reg [5:0] sel , output reg [7:0] seg ); //参数 parameter TIME 250_000;//5MS //内部信号 reg [17:0] cnt_time;//余晖效应、暂留效应的刷新率50 wire [3:0] data_1 , data_2 , data_3 , data_4 , data_5 , data_6 ; reg [3:0] data_smg ; reg [23:0] num_reg ; reg point ; //5ms计数器 always (posedge clk or negedge rst_n) begin if (rst_n 0) begin cnt_time 0; end else if (cnt_time TIME - 1) begin cnt_time 0; end else begin cnt_time cnt_time 1; end end //data赋值 assign data_1 num[23:20] ; assign data_2 num[19:16] ; assign data_3 num[15:12] ; assign data_4 num[11:8] ; assign data_5 num[7:4] ; assign data_6 num[3:0] ; //位选动态数码管 always (posedge clk or negedge rst_n) begin if(rst_n 0) begin sel 6b111_110; end else if (cnt_time TIME - 1) begin sel {sel[4:0],sel[5]}; end end //各位选对应的数据 always (*) begin case (sel) 6b111_110:begin data_smg data_1;point 1;end 6b111_101:begin data_smg data_2;point 0;end 6b111_011:begin data_smg data_3;point 1;end 6b110_111:begin data_smg data_4;point 1;end 6b101_111:begin data_smg data_5;point 1;end 6b011_111:begin data_smg data_6;point 1;end default:begin data_smg data_1;point 1;end endcase end //段选 always (posedge clk or negedge rst_n) begin if(rst_n 0) begin seg 8b1_100_0000; end else begin case (data_smg) 0 : seg {point,7b100_0000}; 1 : seg {point,7b111_1001}; 2 : seg {point,7b010_0100}; 3 : seg {point,7b011_0000}; 4 : seg {point,7b001_1001}; 5 : seg {point,7b001_0010}; 6 : seg {point,7b000_0010}; 7 : seg {point,7b111_1000}; 8 : seg {point,7b000_0000}; 9 : seg {point,7b001_0000}; default:seg 8b1_100_0000; endcase end end endmodule顶层模块//除了完成连线任务、还有将两者之间的数据进行转换 module top ( input clk , input rst_n , inout dq , output [5:0] sel , output [7:0] seg ); //连线信号 wire data_vld ; wire [15:0] data ; wire [23:0] num ; wire [19:0] data_in ; data_ctrl data_ctrl_inst( .clk (clk ), .rst_n (rst_n ), .data (data ),//输入二进制温度的补码 .data_vld (data_vld), .data_in (data_in ) ); //数字温度传感器模块现在开放的功能是温度的读取输出16位二进制温度数据的补码 ds_ctrl ds_ctrl_inst( .clk (clk ), .rst_n (rst_n ), .dq (dq ), .data (data ), .data_vld (data_vld) ); smg smg_inst( .clk (clk ), .rst_n (rst_n ), .num (num ), .sel (sel ), .seg (seg ) ); bin_to_bcd bin_to_bcd_inst( .clk (clk ), .rst_n (rst_n ), .data_in (data_in ), .data_bcd (num ) ); endmodule五、上板验证