ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA驱动DS18B20温度传感器:单总线时序与状态机设计详解

FPGA驱动DS18B20温度传感器:单总线时序与状态机设计详解 1. 项目概述与整体方案设计1.1 为什么要在FPGA上驱动DS18B20DS18B20这块温度传感器在嵌入式圈子里几乎人手一颗便宜、精度尚可、单总线协议只占一根IO。很多人最开始是在单片机比如STM32、STC89C52上通过延时模拟时序去读温度代码跑起来似乎也没什么问题。但一旦把平台换到FPGA事情就不一样了——软件延时没了CPU的while循环没了一切时序都得靠硬件逻辑去拧出来。我在做FPGA温控风扇项目的时候最开始也想省事直接用软核比如MicroBlaze或者RISC-V软核去模拟单总线时序。后来发现这条路对于学习FPGA的人来说其实亏了因为单总线时序本质上是一个对时序精确性要求非常高的协议恰好是用状态机、计数器、三态门这些FPGA基本功练手的好素材。而且在实际工程里很多场景并不想为了读一个温度就引入整个软核系统一片小小的FPGA作为前端传感器采集单元把温度数据通过UART或者SPI送给上位机这种架构在工业采集板里非常常见。所以这篇内容我直接把FPGA实现DS18B20的完整思路铺开讲从硬件连接、协议时序、状态机设计、仿真验证到板上调试踩坑全部基于我自己实际跑过的工程来写。适合的人群很明确已经掌握Verilog基础语法、想拿一个完整项目练手的FPGA初学者以及想在纯逻辑平台挂温度传感器的开发者。1.2 方案选型纯Verilog状态机 vs 软核方案先聊方案选型。DS18B20的单总线协议看起来只有一根线但它的时序要求非常苛刻复位脉冲、存在脉冲、写时隙、读时隙每一种操作都有明确的微秒级时间窗。在单片机里这些可以用定时器甚至空延时解决但在FPGA里你需要决定用哪种方式产生这些时序。第一种方案是纯Verilog状态机加计数器。核心思路是用一个高频基准时钟比如50MHz、100MHz分频出1MHz或者1kHz的节拍然后设计一个主状态机按DS18B20的时序要求逐拍输出高低电平、控制总线方向、锁存数据。这个方案的优点是逻辑完全可控不依赖任何软核资源占用极小也方便做时序仿真验证。缺点是需要自己把协议吃透状态机的状态拆得比较细。第二种方案是例化一个软核CPU跑C代码比如在Intel FPGA里用Nios II在Xilinx里用MicroBlaze。这种方式开发速度快有现成的延时函数可调跟写单片机差不多。但代价是资源开销大时序的确定性不如纯硬件方案——对于实时性要求高的场景软核的中断和调度会引入不确定性。我最终选择了纯Verilog方案。一来因为温控项目本身不需要复杂算法纯逻辑就能完成二来作为深度练习写一遍单总线时序的状态机对理解FPGA的时序设计会有质的提升。在后面的内容里我会把状态机的每一个状态和每一次跳变都拆开讲清楚。2. 单总线协议核心细节拆解2.1 硬件连接与总线结构DS18B20有三种封装TO-92、SO-8和MSOP-8最常见的是TO-92看起来像三极管。三个引脚分别是GND、DQ数据/时钟复用脚、VDD。供电方式支持寄生供电和外部供电两种我做设计时直接选了外部供电VDD接3.3VDQ通过一个4.7kΩ上拉电阻连接到FPGA的IO引脚GND共地。需要特别提醒的是FPGA的IO往往是bank供电的DQ引脚所连接的bank必须工作在3.3V或者2.5V确保与DS18B20的电平阈值匹配。如果FPGA的IO是1.8V那就要加电平转换芯片否则读回来的数据根本没有逻辑高电平。我在第一次上板时就吃过这个亏当时把一个1.8V bank的引脚直接连到传感器上读回来的温度永远是85℃——这个85℃其实是DS18B20上电复位后的默认温度值说明通信根本没有建立成功。单总线之所以叫单总线是因为所有设备的DQ引脚都挂在同一条线上通过开漏输出加上拉电阻实现线与逻辑。在FPGA内部实现开漏输出需要把IO配置为inout类型并在逻辑里控制输出使能信号。这个操作很多新手第一次接触会有点懵其实拆开就两个信号一个控制输出数据一个控制输出使能。发送时使能打开把数据打到总线上释放总线时使能关闭让引脚变成高阻态由上拉电阻把总线拉高。2.2 复位时序与存在脉冲检测DS18B20的所有通信都从复位开始。主机先把总线拉低持续时间至少480μs然后释放总线。释放后DS18B20会在15μs到60μs之间开始响应把总线拉低60μs到240μs这个低电平脉冲就是存在脉冲Presence Pulse相当于传感器在说“我在”。在FPGA实现中这一段的难点不是拉低和释放而是检测存在脉冲。因为脉冲出现的时间窗口是一个区间不是一个固定值设计采样逻辑时必须覆盖整个可能区间。我的做法是释放总线后启动一个计数器从15μs开始每隔1μs采样一次总线电平一直采样到60μs。一旦采到低电平就认为检测到存在脉冲如果整个窗口都没采到低电平那就判定器件不存在或接线有问题。这里我插一句经验存在脉冲的检测窗口不要卡得太死。有的板子在低温或者长走线情况下脉冲沿会有明显失真采样窗口留足余量能显著提高可靠性。工程上我习惯用1μs的采样间隔而不是直接在15μs、60μs两个固定点去采这样鲁棒性更好。2.3 写时隙与读时隙的区别复位之后就可以发命令了。单总线的写0、写1、读0、读1用到的都是“时隙”Time Slot的概念。一个时隙的长度最小60μs最大120μs。所有时隙都以主机拉低总线作为起始标志但具体波形不同。写逻辑其实是最简单的。写1的时序是主机拉低总线1μs到15μs然后释放总线让上拉电阻把总线拉高持续到整个时隙结束。这样DS18B20在时隙开始后的15μs到60μs窗口内采样看到高电平就是1。写0的时序是主机拉低总线并保持低电平持续整个时隙至少60μs。读时序稍微麻烦一点。主机拉低总线至少1μs然后释放总线接着在释放后的第15μs之内去采样总线电平。如果DS18B20想要返回0它会主动把总线拉低如果返回1总线维持上拉高电平。需要注意的是DS18B20返回0时拉低总线的时间窗口很短所以采样点一定要靠前一旦错过总线被上拉拉回高电平读到的就永远是1。在FPGA里实现的时候我喜欢把写时隙和读时隙各自封装成独立的子状态机而不是写在同一个大状态机里。这样在后续需要连续写入多个字节时只要反复触发写子状态机就行读多个字节时反复触发读子状态机。模块化程度高了调试的时候也容易定位问题。3. FPGA核心逻辑实现与仿真3.1 顶层模块划分我设计的系统分四个模块时钟分频模块clk_div、单总线控制器模块ds18b20_ctrl、UART发送模块uart_tx、顶层例化模块top。时钟分频模块做的事很简单把开发板上的50MHz时钟分频出1kHz和1MHz两路节拍。1MHz用作单总线时隙的基础计时单位1kHz用作UART波特率发生器的基础时钟。这里不需要用PLL直接用计数器分频就行因为温度采样的实时性要求没那么高几十纳秒的时钟抖动完全不影响结果。单总线控制器模块是整个系统的核心功能拆开看有四部分复位时序控制、ROM命令发送、温度转换触发、温度数据读取。我在这里用了三段式状态机来写主控制逻辑状态划分得很细从IDLE开始依次经过RESET、PRESENCE_CHECK、SKIP_ROM、CONVERT_T、WAIT_CONV、READ_SCRATCHPAD、SHIFT_DATA、OUTPUT_RESULT最后回到IDLE。每个状态内部又有若干子步骤通过计数器推进。UART发送模块不复杂波特率设为96008位数据位、无校验位、1位停止位。温度数据处理好之后通过这个模块发到电脑串口调试助手上显示。如果你手里有OLED或者LCD屏也可以把UART模块换成显示驱动模块这个看具体需求。3.2 状态机设计从复位到温度读取状态机的第一段是IDLE。系统上电后保持在这个状态等一个内部使能信号拉高然后跳到RESET状态。RESET状态要做的事是输出至少480μs的低电平脉冲。状态机在进入RESET时把输出信号拉低然后等计数器计数到480μs再把输出信号拉高跳到PRESENCE_CHECK状态。PRESENCE_CHECK状态的设计我之前提过核心是一个15μs到60μs的扫描窗口。状态机进入这个状态后启动一个window计数器每个1μs采样一次总线电平。由于FPGA采样需要和总线电平同步我在这里插入了一级触发器先打一拍再判断避免竞争冒险。检测到低电平就记录presence_flag等于1然后推进到下一个状态整个窗口扫描完还没检测到就报一个错误标志重新跳回IDLE。SKIP_ROM和CONVERT_T是两个连续的命令发送过程。因为总线上只挂了一个DS18B20没有必要做ROM匹配直接发0xCC跳过ROM寻址再发0x44启动温度转换。DS18B20的转换时间在12位分辨率下最长750ms所以我设了一个WAIT_CONV状态固定等待800ms再往下走。这里有个细节温度转换期间DS18B20会持续拉低总线表示忙如果想加快流程可以读总线电平判断是否转换完成但如果不在乎这几百毫秒直接延时最省事。READ_SCRATCHPAD状态用于发送0xBE读暂存器命令然后连续读9个字节。前两个字节是温度值低字节在前高字节在后。后面7个字节是报警阈值、配置寄存器和CRC校验值我在这个项目里没有处理CRC只取了前两个字节。3.3 核心代码思路与关键参数计算直接给完整代码会很长而且很多网上的例程代码风格不一致这里我挑核心逻辑讲方便你理解后自己写。首先是单总线时序的节拍产生。DS18B20要求的最低时间单位是1μs所以我用50MHz时钟计数50次得到1μs脉冲reg [5:0] cnt_1us; wire tick_1us (cnt_1us 6d49); always (posedge clk or negedge rst_n) begin if (!rst_n) cnt_1us 6d0; else if (tick_1us) cnt_1us 6d0; else cnt_1us cnt_1us 1b1; end然后是写时隙的实现。我需要一个bit计数器和一个slot状态机。每进入一个写时隙先把总线拉低1μs然后根据当前要发送的数据位决定是否释放总线。整个时隙长度我用65μs略大于最小要求的60μs留足余量。读时隙的实现和写时隙类似但关键在于采样点。我设计的读时隙起始后第10μs采样总线电平此时DS18B20的数据有效窗口还没关采到的值就是准确的。代码逻辑大致是// 读时隙示例逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) read_data 1b0; else if (read_sample_en) read_data dq_in; // 这里dq_in是打拍同步后的总线电平 end关于参数计算我对几个关键时序做了表格梳理方便你对照设计时序操作参数要求实际设计值说明复位低电平≥480μs500μs留20μs余量应对总线电容存在脉冲等待15μs~60μs15μs~70μs扫描窗口比手册放宽写时隙长度60μs~120μs65μs每bit独立时隙写1释放时间1μs2μs确保电平建立读采样点15μs10μs避开边界抖动转换等待≤750ms800ms12位分辨率下保险值3.4 仿真验证用Testbench把时序跑通FPGA开发一定要先仿真再上板这个习惯能省下大量调试时间。DS18B20的仿真不能直接拿现成的仿真模型跑我写了一个简单的behavioral model模拟它的复位响应、写时隙采样和读时隙返回数据的行为。其实不写完整模型也行只要能模拟出存在脉冲以及写命令后从暂存器回传温度数据就足够验证状态机逻辑了。仿真文件的核心思路是Testbench例化顶层模块提供一个模拟时钟再例化一个简化版的DS18B20行为模型。行为模型做的事情有三件。第一检测到总线拉低超过480μs后释放延迟20μs拉低总线60μs模拟存在脉冲。第二监视总线上出现的命令字节如果收到0xCC和0x44就把内部温度寄存器的值设置为一个固定值比如0x0191对应25.0625℃。第三监视0xBE命令收到后按位返温度寄存器的值。仿真跑起来后重点看三个波形复位阶段是否存在脉冲的检测逻辑对不对写命令时总线上电平变化是否在正确的时间窗口读数据时采样点是否在信号稳定区域。我用ModelSim和Vivado Simulator都跑过前者配置麻烦一点后者开箱即用新手上路建议直接用Vivado的仿真器。有一类仿真问题特别容易遇到行为模型里的延时语句#延时和FPGA门级仿真混在一起导致仿真极慢。解决方法是仿真时只跑行为级模型不要做门级仿真功能验证通过后再上板。上板之后再有问题那是另一类问题了下文单独讲。4. 实操过程与上板调试实录4.1 开发板与引脚分配方案我用的是Xilinx Artix-7系列的板子具体型号是XC7A35T。板上的50MHz晶振作为主时钟DS18B20的DQ引脚分配到FPGA的L14引脚对应一个3.3V的bank。UART_TX分配到M14引脚通过板载USB转串口芯片连接到电脑。引脚分配这一步要特别注意两点一是确认DQ引脚所在的bank电压域必须是3.3V二是DQ引脚要设置为inout类型在约束文件中用set_property IOSTANDARD LVCMOS33指定电平标准。如果板子上有可调bank电压的跳线先量一下实际电压再接线。我在Vivado中写约束文件时额外给DQ信号加了PULLUP属性。这个属性不是必须的因为外部已经有4.7kΩ上拉电阻了但加上它相当于双保险。有的板子如果走线长、上拉电阻焊接不良内部弱上拉能救一命。不过必须提醒内部上拉阻值通常是几十千欧远弱于外部4.7kΩ千万别指望靠内部上拉跑单总线外部电阻一定要焊。4.2 状态机时序错误的经典现象第一次上板跑起来之后串口打印的温度值稳定停在85℃我一看就明白了这是DS18B20的默认上电温度值说明读操作根本没有成功。用逻辑分析仪抓DQ引脚的波形后发现一个现象复位脉冲和存在脉冲都是正常的写命令也发得出去但读数据时采回来的每一位全是1。这个问题我排查了很久最后定位到是读时隙采样点太靠后。我最初把采样点设在时隙开始后的第15μs而这个值已经接近DS18B20返回数据有效窗口的后沿。在长走线和高负载电容的双重影响下返回0时总线拉低的电平还没稳定就被上拉电阻拉回去了采样自然全是1。修复方法很简单把采样点往前移到10μs问题立刻消失。很多单片机例程里采样点设在15μs还能正常工作是因为单片机IO的输入缓冲和走线条件比FPGA这种高速器件更“宽容”。这里我想强调一个概念FPGA的IO翻转速度极快但也因此对信号建立时间更敏感参考别人例程时不能照搬参数必须结合自己的硬件环境做调整。4.3 数据拼接与温度换算读回来的数据是16位有符号数低字节是LSB高字节是MSB。DS18B20的温度分辨率是12位其中最高位是符号位。温度值的换算规则是把16位数据右移4位得到11位有符号整数加小数部分每1个LSB对应0.0625℃。我在Verilog里做的操作是先拼接两个字节得到16位温度原始值再判断符号位。如果符号位为0直接把原始值乘以0.0625换算成实际温度如果符号位为1说明是负温度需要对原始值取补码再换算。由于FPGA做浮点运算不方便我直接输出两个值温度原始值的整数部分和小数部分由上位机或者显示模块去处理。比如读到的低字节是0x91高字节是0x01组合起来是0x0191换算十进制就是401乘以0.0625得到25.0625℃。我习惯把“扩大16倍后的整数”直接发给上位机401这个数在上位机侧除以16就能还原温度这样既避免了浮点运算又保留了精度。4.4 单总线电平竞争与三态门处理DQ引脚在FPGA内部是inout类型这意味着你需要在逻辑里同时控制输出使能和输出数据。很多新手容易犯一个错误当FPGA需要释放总线时只是把输出数据拉高而没有把输出使能关掉。这在电气上会造成FPGA强驱动高电平和DS18B20拉低总线产生竞争——严重时会损坏传感器。正确做法是定义一个三态门控制信号dq_en。当需要主机驱动总线时dq_en为1输出数据通过assign dq dq_en ? dq_out : 1bz送到引脚当需要释放总线时dq_en为0引脚变成高阻态。这个操作在Verilog里只有一行assign语句但电气上的意义非常关键。这里我也遇到过一个问题读时序释放总线后由于传输线反射DQ引脚上会出现振铃。如果采样点正好落在振铃区读到的数据就可能出错。解决手段有两个一是在采样前加一级同步寄存器让数据先稳定一个时钟周期再判断二是在PCB布局上缩短DQ引线长度减少反射。对于教学板来说前者更实用。5. 常见问题与排查技巧实录5.1 温度恒为85℃的排查方向85℃这个值我在多个平台上都见过它是DS18B20内部上电复位后暂存器SRAM中的默认值。出现这个情况说明你的读时序没有读到真实温度数据通信链路在“读”这个环节断裂了。排查方向有三个先看复位是否存在脉冲如果没有检查接线和上拉电阻再看写命令是否能正确发送可以抓波形对比0xCC、0x44、0xBE最后看读时隙采样点位置这是最容易出问题的环节。有个特别隐蔽的问题是总线上挂的传感器多于一个而且都没有做ROM匹配。多个DS18B20同时响应0xCC命令会在总线上产生数据冲突读回来的温度可能表现为混沌的乱码而不是固定值。如果有多个传感器必须先发0x33读ROM再发0x55匹配ROM最后才能发温度转换和读取命令。5.2 温度跳变和噪声干扰的处理温度值偶尔跳一下可能不是协议问题而是信号完整性问题。我遇到过一个现象温度值基本正确但每隔几秒会冒出一个明显异常的大数比如400多℃。用示波器看波形发现DQ线的下降沿有比较严重的过冲超过了FPGA IO的绝对最大额定值。这个问题的根源是DQ线太长或者线径太细加上上拉电阻到FPGA引脚之间有寄生电感形成了LC振铃。解决方式有三种把DS18B20的引线缩短到20cm以内在上拉电阻旁边并联一个100nF的电容做电源去耦如果走线必须很长可以在DQ线上串联一个100Ω的电阻抑制振铃。我用第三种方案效果最明显。另外一点软件层面的过滤技巧连续采样三次取中位数或者去掉最大最小值后取平均能有效过滤单次异常值。在FPGA里做这个不复杂把三次采样结果存到寄存器里比较一下就行。温控系统对实时性要求没那么极端三次采样过滤完全能接受。5.3 关于寄生供电的特别提醒有同学为了省一根线想用寄生供电方式让DS18B20直接从DQ线上取电。这个方案在某些场合可行但在FPGA系统里我不推荐。原因有两个第一FPGA的IO驱动能力有限寄生供电要求主机在总线高电平期间提供足够的电流而FPGA的IO拉高能力本身就弱第二温度转换期间DS18B20的峰值电流可达1mA以上如果DQ线上电压跌落超过允许范围转换结果会出错。如果因为结构限制必须做寄生供电有一个处理技巧在DQ线和GND之间接一个4.7μF的大电容利用电容储能维持转换期间的电压稳定。同时需要在每次温度转换命令后把DQ线保持在高电平至少750ms给电容充电。即便如此这个方案的可靠性还是不如外部供电工程上能避免就避免。5.4 仿真正常但上板失败的元凶清单仿真过了、上板失败这是所有FPGA项目都会经历的阶段。针对DS18B20项目我整理了几个高频原因供你排查时对照现象可能原因解决方案一直读到85℃读时隙采样点太靠后采样点提前到10μs附近读到温度乱跳DQ线振铃、干扰串100Ω电阻缩短走线复位检测失败上拉电阻虚焊或阻值过大检查4.7kΩ电阻温度转换后返回FF寄生供电电压跌落改用外部供电信号偶尔丢失异步信号未同步采样前加两级同步寄存器上板后FPGA发热IO驱动冲突检查三态门使能逻辑5.5 逻辑分析仪与在线调试的经验如果手头有逻辑分析仪调试这个项目会轻松很多。没有逻辑分析仪的朋友千万别灰心用FPGA内部集成逻辑分析仪比如Vivado的ILA或者Quartus的SignalTap一样能抓信号。区别只在于ILA需要占用额外的BRAM资源而且只能观察FPGA内部信号看不到真实引脚电平。我调试时习惯同时开三个信号观察dq_out主机输出数据、dq_en输出使能、dq_in同步后的输入数据。这三个信号组合起来就能还原总线上发生的所有事情。观察复位阶段确认dq_out低电平持续够480μs观察存在脉冲检测确认dq_in能采到低电平观察写阶段确认dq_en翻转的时机正确。通过ILA抓波形往往几分钟就能定位问题。这里说一个实用技巧调试时把温度转换等待时间临时改短比如从800ms改成100ms。不是要你量产时这么做而是调试模式下缩短等待能大幅提升迭代效率。等逻辑全部调通后再改回800ms做最终验证。6. 项目扩展方向与实际应用6.1 多传感器组网与ROM匹配单总线最吸引人的特性是可以在一条总线上挂多个DS18B20每个传感器都有唯一的64位ROM序列号。如果要做多点温度监测就不能再使用跳过ROM的0xCC命令而是要先发送0x33读ROM把总线上所有传感器的序列号都读出来然后针对每个序列号发0x55匹配命令再触发温度转换。FPGA实现ROM匹配的难点在于读ROM这个操作本身也很讲究时序。搜索ROM的算法0xF0本质上是一个二叉树搜索过程需要逐位读取并发送应答位。我在后续的项目里单独写过一轮单总线搜索逻辑代码量不算大但状态机比单传感器版本复杂不少。数据处理上64位ROM序列号在FPGA里可以存成一个reg [63:0]逐位移入移出即可。如果你只是想在一条总线上挂几个传感器不做搜索还有一个取巧的办法每个传感器通过不同的IO引脚连接到FPGA每个引脚跑一套独立的单总线控制器逻辑。这样从软件逻辑上它们就是完全独立的不用处理ROM匹配。代价是多占几个IO和一点逻辑资源对于小规模组网很划算。6.2 温度数据与UART上位机联动温度采集出来后串口打印只是第一步。我后来把整个系统扩展成了一个简易的电脑端温度监控报警器FPGA每隔1秒采集一次温度通过UART发送给上位机上位机用Python脚本读取串口数据、画实时曲线超过设定阈值时触发报警。做这个扩展几乎不用改FPGA逻辑只要把UART发送内容从“只发温度值”改成“发帧头温度值帧尾”的自定义协议即可。自定义协议的格式不复杂我用的是0xAA作为帧头接着是2字节温度原始值再发一个累加和校验字节最后0x55作为帧尾。上位机按帧解析既省心又能发现传输错误。这里插一句UART的波特率时钟最好用专用的波特率发生器不要和单总线的节拍分频混用否则改参数时会互相干扰。这个项目做完后你会对FPGA的“逻辑时序设计”有一个很直观的认知单总线协议、UART协议、甚至以后要做的SPI、I2C本质上都是在一根或几根线上按照约定好的时间窗口去驱动电平、采样电平。有了DS18B20的时序训练再去看其他协议的数据手册你会觉得里面的时序图不再那么抽象。我在实际开发中反复验证过用纯Verilog实现单总线温度采集资源的占用不到FPGA的5%主要开销是状态机逻辑和几个计数器。对比软核方案纯逻辑方案的功耗、成本、实时性都有明显优势。如果你正在学习FPGA我强烈建议把这个项目作为进阶的里程碑——从点亮LED到驱动DS18B20中间跨越的正是FPGA开发中最核心的时序设计思维。这篇文章里的所有代码思路和调试经验都是我自己实际踩坑后总结的希望能让你少走几步弯路。
返回列表