
先聊个题外话。最近好几位刚接触FPGA的朋友都在问我同一个问题除了流水灯、按键消抖和串口回环还有什么项目既能练手、又不至于简单到做完没成就感我每次都推荐他们去啃一遍DS18B20单总线温度采集。原因很直白——这个项目几乎把FPGA开发的核心基本功全串起来了时序协议解析、状态机设计、us级计数精度、边沿采样、跨时钟域处理如果再加上数码管或串口显示连模块化设计和通信协议也一并练到。DS18B20这颗温度传感器在嵌入式圈子里几乎是“万能配角”单片机上用延时函数“凑”时序很容易但换到FPGA上就不一样了没有delay()函数可用所有微秒级时序都必须靠状态机和计数器精确卡出来。这一卡你对“硬件思维”的理解会上升一个档次。我见过不少人在这个项目上卡了一两周但真正跑通的那一刻对FPGA的掌控感完全不一样。这篇文章不打算写教科书式的原理堆砌重点放在“我当时是怎么拆解这个项目的”“FPGA里具体怎么实现”以及在调试中踩过的坑。前后花了一周多时间从零到完整跑通整个决策过程和代码思路我尽量讲透打算上手的朋友可以直接照着做。1. 为什么要用FPGA驱动DS18B20一个看似简单、实则硬核的练手项目1.1 一颗传感器背后的“时序考卷”DS18B20是Dallas半导体现在归Maxim出的数字温度传感器量程-55℃到125℃12位分辨率下精度0.0625℃最关键的是它只需要一根数据线跟主机通信这就是“单总线”1-Wire名字的由来。单根线要同时传输时钟、数据、控制命令所以协议本身设计得非常抠门所有信号都靠在一根线上的“高低电平持续时间”来区分。对单片机来说这个协议用延时函数配合IO翻转就能软磨硬泡出来但FPGA没有“阻塞延时”的概念所有操作都是并行的必须用一个状态机把时序逐段“照做”。这正是这个项目的价值——你在FPGA里不是在“调延时”而是在“设计时序”。我最初画了张思维导图梳理需求发现如果只做“读温度”这一件事实际要解决的问题其实有三个层次底层单总线协议的时序如何产生和采样中间层DS18B20的命令流程初始化、跳过ROM、启动转换、读暂存器怎么调度顶层读回来的16位温度数据怎么处理、显示到哪。很多教程只讲中间层或者只给一段代码结果读者不知道协议细节出了问题该怎么排查。这篇文章会把三层全部串起来讲。1.2 和单片机方案的本质区别用STM32做DS18B20采集很多人的第一反应是“用IO模拟时序”。麻烦在于单片机如果正在执行其他中断或任务延时函数会被打断时序就容易跑飞——所以有经验的人会在关键时刻关中断或使用专用定时器。FPGA的思路完全不同。它没有“执行任务被占用”的问题因为状态机就是硬件逻辑到了哪个状态、输出什么电平、持续多少周期全部由硬件决定天然不受“指令流”干扰。只要你把计数器算对时序就是确定性的这在工业环境和长时间运行时非常可靠。但也正因如此FPGA方案对设计者的要求更高你必须把协议里的每个时间段在时钟周期里精确对应上任何“大概”都会让总线通信直接失败。这个“从大约到精确”的转变就是从嵌入式思维切换到硬件思维的关键一步。我实际用到的应用场景是一个小的实验室环境监测板需要同时采集环境温度并联动风扇。如果用MCU这个方案也能做但后期想扩展多路温度输入、在FPGA里做滤波和阈值判断资源就变得紧巴巴的。换到FPGA之后所有的采集调度、数据缓存、输出控制都在一个芯片里搞定扩展性好得多。1.3 需要准备的环境与物料清单在做这个项目之前建议先把开发环境和硬件物料备齐否则调试到一半发现缺东少西很影响心情。我的配置如下供参考开发板任意主流FPGA开发板即可我使用的是Intel Cyclone IV系列50MHz系统时钟板上自带USB-Blaster下载器温度传感器DS18B20建议买TO-92封装直插式好焊接带3根引脚VCC、DQ、GND上拉电阻4.7kΩ这个必不可少后面会单独说原因显示方案我用了4位数码管模块也有人用串口回传PC二选一即可开发环境Quartus II / Vivado / Gowin云源均可我用的Quartus Prime Lite免费的。如果你手上没有DS18B20实物也可以用仿真方式先跑通逻辑但最终还是建议上板实测一次因为温度传感器的真实波形和仿真差别很大调试经验正是在这里积累的。2. 单总线协议底层逻辑时序参数与五个关键动作2.1 单总线为什么能“一根线走天下”单总线协议的数据线是开漏输出默认通过上拉电阻保持高电平。主机和从机DS18B20都能把这条线拉低但不能主动拉高——想拉高只能释放总线靠上拉电阻恢复。这就带来一个显而易见的约束总线上有且只有一个主机。所有通信节奏由主机发起DS18B20只会在被点名或收到命令后响应。对于温度采集来说主机就是FPGA。单总线的“一根线走天下”主要体现在两个方面供电DS18B20支持寄生供电也就是直接从数据线上“偷电”数据线为高时内部电容充电数据线为低时靠电容放电维持工作。但寄生供电在长线、高精度场景下容易出问题最稳妥的做法还是三线制——VCC接3.3V或5VDQ接GPIOGND共地。通信所有命令和数据的发送都是“一个bit一个bit”地通过时隙完成的没有独立的时钟线收发双方要严格遵循时序参数。对FPGA而言“严格遵循时序参数”意味着设计时要把每一段高低电平持续时间都精确到微秒级。先看一张我整理的核心时序参数表后面所有状态机设计都围绕它展开。2.2 核心时序参数必须死磕的数字操作时序要求说明初始化复位主机拉低 ≥480μs典型480-960μs然后释放总线等待存在脉冲释放后等待15-60μs从机在这段时间内应答存在脉冲从机拉低60-240μs主机在此期间采样检测到低电平表示从机在线写0时隙主机拉低整个时隙保持低电平60-120μs用“持续低”表示0写1时隙主机拉低1-15μs然后释放剩余时隙为高用“短暂拉低后释放”表示1读时隙主机拉低≥1μs然后释放主机在15μs内采样从机控制总线输出0或1时隙间隔两个时隙之间至少1μs恢复时间保证总线电平稳定注意初始化低电平最容易被新手忽略的是“必须持续大于480μs”但我实测发现如果拉低900-1000μs可靠性反而更高因为DS18B20在上电后需要一段时间稳定。另外写时序和读时序的时隙总长要控制在60-120μs之间太短会导致从机采样失败太长则吞吐率下降不过单点测量场景下吞吐率无所谓稳定第一。2.3 五个关键动作从“复位”到“读数据”的完整流程DS18B20的操作流程可以拆成五个环节。每个环节对应FPGA状态机中的一个或几个状态。动作一初始化Reset Presence Pulse主机先把总线拉低至少480μs然后释放。DS18B20检测到这个下降沿和低电平时间后会在15-60μs内把一个存在脉冲Presence Pulse拉到低电平并保持60-240μs。主机在这个窗口内采样如果采到低电平说明从机在线。这个环节最常出的问题就是“为什么检测不到存在脉冲”。我遇到的情况往往是采样点设置得太早主机释放总线后立即去读这时从机还没把总线拉低读到的一直是高电平。正确做法是先等至少20-30μs再去采样并且采样窗口要覆盖整个存在脉冲区间。动作二发送ROM命令复位成功之后主机需要发命令来指定要操作哪个从机芯片。在总线上挂了多个DS18B20时才需要做ROM匹配常规的单点采集可以直接发“跳过ROM”0xCC让后续命令广播给总线上所有从机。动作三启动温度转换发送功能命令0x44让DS18B20开始一次模数转换。注意这个命令发出后不能立刻去读温度转换需要时间9位分辨率时最大93.75ms10位187.5ms11位375ms12位750ms。我实测12位分辨率下整个转换周期大约720-750ms所以状态机里至少要预留750ms以上的等待时间。动作四发出读命令转换完成后发送0xBE读暂存器命令DS18B20会依次把暂存器9个字节的数据通过读时隙传给主机。我们最需要的是前2个字节温度低字节和温度高字节。动作五读取温度数据读取过程是主机连续产生读时隙一个时隙读一个bit。低字节在前高字节在后先读到的bit是LSB。全部读完后再按分辨率把有效位移出来换算成实际温度值。2.4 为什么要特别强调“采样点”和“时隙窗口”很多参考设计能写出状态机但上板就失败问题往往出在“采样点”。单总线协议中无论是存在脉冲检测还是读数据采样点都必须落在从机驱动总线的有效窗口内存在脉冲窗口主机释放后15-60μs开始持续60-240μs读数据采样点主机拉低释放后从机会在15μs之内把总线拉到对应电平所以主机必须在15μs附近采样太早读到释放后的高电平太晚如果从机释放总线也会读到高电平。FPGA里处理采样点最简单的方法是用一个计数器计量“从拉低释放后过去了多少微秒”到指定时间点锁存IO输入电平。不要一释放就去读这是新手最容易犯的错误。3. FPGA实现把us级时序变成状态机与计数器3.1 系统总体架构模块怎么划分更清晰FPGA设计讲究“模块化、高内聚低耦合”。我当时把整个系统拆成以下模块clk_div时钟管理统一在顶层用PLL或分频生成内部工作时钟我这里直接用50MHz所有计数器都以20ns为最小单位onewire_ctrl单总线控制器负责产生复位、写bit、读bit等最底层的时序操作对外提供简单的任务接口比如do_reset、write_byte、read_byteds18b20_driver状态机主控调用onewire_ctrl的底层操作完成“初始化→跳过ROM→启动转换→等待→读暂存器→输出温度”的完整流程temp_calc把读到的16位原始数据转换成BCD码或十进制温度值display数码管显示模块或UART发送模块。这个分层方式的最大好处是底层协议模块完全可以复用——以后想换传感器型号只需要重写onewire_ctrl想增加多路采集也只需要在ds18b20_driver里多调度几个通道。3.2 核心计数器设计50MHz时钟下的微秒计算方法先解决“微秒怎么来”的问题。我使用的FPGA系统时钟为50MHz一个时钟周期是20ns。1μs等于1000ns所以计数50个时钟周期就是1μs。如果系统时钟是其他频率比如100MHz10ns周期那么1μs就要计100个周期。公式很简单周期数 所需时间(μs) × 系统时钟频率(MHz)。算得越准时序越稳。下面给出几个常用延时参数的计数器设计示例延时目标50MHz下计数周期数说明1μs50通用基础延时480μs24000初始化复位低电平最短时间60μs3000写时序/读时序的时隙长度750ms3750000012位分辨率转换等待时间需要32位计数器这里要注意一个细节如果直接用计数器判断“计数到3000就结束”实际上拉低的持续时间是从计数器启动到判断成立的最后一拍中间会有微小的额外时钟周期消耗所以要在参数上留余量。比如要求480μs我一般会设为490-500μs保证足够的裕量。3.3 状态机设计一次完整的温度读取流程如何调度ds18b20_driver的状态机是整个项目的指挥中心。我的状态定义大致如下IDLE → RESET_WAIT 发出复位脉冲后等待存在脉冲 → SKIP_ROM 发0xCC跳过ROM匹配 → CONVERT_T 发0x44启动温度转换 → WAIT_CONVERT 等待转换完成12位分辨率约750ms → READ_SCRATCHPAD 发0xBE命令 → READ_TEMP_LOW 读温度低字节 → READ_TEMP_HIGH 读温度高字节 → CALC_AND_DISPLAY 数据计算与显示输出 → IDLE每个状态内部都嵌有子状态或计数器。比如READ_TEMP_LOW这个状态它内部还要循环8次“读bit”操作每读1个bit又要经过“拉低、释放、等待采样点、锁存数据”这几个微步骤。设计时我用了一个“bit计数器”加一个“读时序子状态机”组合避免把所有东西写成庞大臃肿的一层。另外记住一个经验状态机里最好不要出现“等很久”的状态比如WAIT_CONVERT等待750ms否则系统仿真时会非常痛苦。可以把等待拆成“每1ms检查一次是否到达750ms”方便仿真时用更短的参数验证逻辑再在真实运行时用完整参数。3.4 单总线底层控制器的Verilog核心代码解析底层onewire_ctrl是整个项目的关键。这里给出一段写bit和读bit的核心逻辑框架Verilog描述具体工程代码建议根据自身板卡引脚调整。module onewire_ctrl( input wire clk, // 50MHz input wire rst_n, input wire start, input wire [7:0] data_in, input wire read_en, // 1: read bit, 0: write bit output reg dq_out, // 输出到DS18B20的DQ须三态控制 input wire dq_in, // 从DQ引脚输入 output reg done, output reg bit_value // 读到的bit值 );“写bit”的核心思想是利用计数器控制DQ输出低电平的持续时间区分写0和写1。// 伪代码逻辑 // 如果是写0拉低dq_out保持到整个时隙结束60μs // 如果是写1拉低dq_out只保持5μs然后释放DQ等待时隙结束“读bit”的核心是拉低DQ至少1μs然后释放等待约10-15μs时采样外部DP输入锁存到bit_value寄存器。实际操作中DQ引脚在FPGA上要设置为双向IO通过三态缓冲逻辑控制写数据时使能输出读数据时输出高阻、读取外部电平。这一点如果忽略单总线通信必定失败。3.5 温度数据计算与显示从原始数据到实际温度DS18B20的16位温度寄存器格式是bit15-bit11bit10-bit0符号位S温度数据12位有效12位分辨率下最低位代表0.0625℃。比如读回的数据是0x0191十六进制换算公式为0x0191 401401 × 0.0625 25.0625℃。如果是负数则需要对数据取反加一后再乘0.0625。在FPGA里做这个计算可以用乘法器IP核也可以直接用移位实现除以16即乘0.0625相当于右移4位。乘0.0625等价于数值 4吗严格来说0.0625 1/16所以对整数部分确实如此但要保证精度还是建议保留小数位比如先乘以625再除以10000或者直接输出给上位机让软件计算。我当时的做法很简单把原始16位数据加上12位小数的固定点数表示送数码管显示整数部分小数值舍弃。因为温度变化本身就存在微小的抖动显示到小数点一位已经足够。3.6 显示与输出方案数码管、串口还是LCD1602温度读出来总得让用户看到。我尝试过两种方式数码管显示4位共阴数码管动态扫描功耗低、实时性好适合嵌入式设备。缺点是只能显示整数或一位小数信息量有限。串口输出通过UART把温度值发到PC串口助手可以显示完整小数位也方便记录数据。缺点是要一条串口线本质上也是个独立模块。如果你手头有LCD1602模块也可以直接移植但驱动LCD1602本身又是一个时序工程会分散DS18B20的学习注意力。所以我的建议是这个项目的主线是单总线显示用最简单的数码管或串口就够了别把战线拉太长。4. 上板调试实录常见问题、排查套路与避坑清单4.1 一次“复位永远失败”的排查经历我印象最深的坑出现在第一次上板发完复位脉冲后FPGA总是检测不到DS18B20的存在脉冲示波器看DQ引脚发现释放后总线一直保持高电平。当时第一反应是上拉电阻没接但检查后确认4.7kΩ电阻在。后来发现是引脚复用问题——我用的FPGA开发板DS18B20接的引脚恰好没有启用内部弱上拉而且板卡上该引脚默认接了LEDLED的寄生电容直接把边沿拉垮了。解决办法很简单换到纯GPIO引脚并且在FPGA内部使能引脚的弱上拉。这里暴露了一个重要经验FPGA外部IO的驱动能力和边沿特性与单片机不同DS18B20对边沿要求不算苛刻但引脚寄生电容太大时时序边沿还是会被明显钝化。4.2 温度读回全是0xFF或0x00问题出在哪“读回全是1”和“读回全是0”是第二个高频问题。先说全是1这通常意味着读时序采样时总线并没有被从机拉低也就是从机没有响应读取。原因可能是之前发出的SKIP_ROM或启动转换命令本身就没发对或者时序参数不够标准。“读回全是0”则更诡异多半是读时隙的采样点设置得太晚。DS18B20在读时隙中如果传输1它会在主机释放总线后很快释放总线如果主机采样太晚总线已经被上拉电阻拉到高电平但如果采样太早从机还没完成拉低动作采到的还是高。踩了一圈后我的结论是采样点放在释放后10-12μs最稳不建议超过15μs。4.3 常见问题速查表现象可能原因解决思路初始化时检测不到存在脉冲上拉电阻未接或值过大等待时间不足IO引脚复用检查电路等待延长至30μs再采样更换纯GPIO引脚读回全是10xFF命令没发对读时隙采样过早DS18B20未正确供电先用示波器看总线波形重发命令并检查供电读回全是00x00读时隙采样过晚DS18B20未收到读命令采样点提前至10μs左右检查命令字0xBE温度值跳变、偶发错误时序余量不足有干扰供电纹波大加宽初始化脉冲缩短连接线电源加去耦电容转换后温度显示为-55℃或异常读取的字节顺序错乱分辨率配置不对确认先读温度低字节检查配置寄存器0x04为12位一上电就发烫或DQ频繁拉低引脚方向设错DQ输出驱动了外部低电平检查OD/三态逻辑确保释放总线时DQ为高阻4.4 仿真验证技巧仿真一时爽上板火葬场的避免方法FPGA项目的仿真不能只做“流程验证”还要做“边界验证”。我当时做了三个层面的仿真理想流程仿真用虚拟DS18B20模型或行为级模型把完整的“复位→命令→读温度”流程跑一遍确认状态机跳转正确异常仿真模拟DS18B20不存在超时不响应的情况看状态机是否会超时退出会不会卡死时序边界仿真人为把采样点挪到参数范围的边缘检查逻辑是否还能正确识别0和1。在仿真中我还做了一件值得推荐的事写了一个简单的testbench自动比对从机上发的每一个bit和我状态机读到的是否一致节省了大量肉眼盯波形的时间。4.5 上板调试的几条独门经验经验一先把初始化做扎实再谈后面。单总线通信就是“前面没做好后面全白搭”。我建议专门写一个test模式只发复位、检测存在脉冲用LED显示在线状态反复上电验证稳定后再继续写命令和读取。经验二示波器或逻辑分析仪是必需品。哪怕你用仿真观察到再多细节真实信号的边沿、噪声和上拉响应速度都会给你惊喜。寄希望于“仿真没问题上板就一定没问题”的想法在这个项目上会摔得很痛。经验三状态机永远要有超时保护。如果DS18B20没接好或被拔掉你的状态机要能回到IDLE并给出错误指示而不是卡在等待存在脉冲的状态里死循环。我就是在状态机里加了一个超时计数器超过2ms没收到存在脉冲就自动复位一次。经验四数据和时序分开调。先把数据线时序调到波形完美再去看温度数据对不对不要同时调两件事。温度数据不对时先确认时序波形再怀疑算法。5. 项目扩展建议从单点测温到更复杂的系统5.1 多路温度采集让一套FPGA主管一片区域前面提过DS18B20支持ROM命令可以在同一条总线上挂多颗传感器。挂多颗之后读取流程必须从“跳过ROM”改成“匹配ROM”先搜索所有在线从机的64位ROM地址再向特定地址发出器件命令。64位ROM地址由8位家族码、48位序列号和8位CRC组成。搜索ROM算法是单总线协议里最繁琐的部分要逐位判断、逐位读取同时处理多位从机响应冲突。FPGA实现搜索ROM有现成的算法框架但代码量不小。如果只是作为练手我建议先用“固定ROM地址”的方式每颗传感器只接一个IO口这样就是多个独立的单通道采集不涉及寻址冲突。我当时做了个折中方案FPGA上接了4路独立的DS18B20每个IO口分别控制一颗传感器四条数据线并列状态机时分复用轮流测量。这样硬件连接简单软件代码也好写只是占用IO多。如果IO紧张再上ROM搜索算法。5.2 与温控风扇联动把采集变成闭环控制温度采集本身只解决“读”的问题实际工作中更常用的是“采集→控制”的闭环。我在实验室监控板上就做了这个FPGA读取DS18B20温度超过阈值就启动PWM风扇调速温度越高占空比越大采用简单的PID控制。FPGA里做PID并不复杂关键是采样周期要稳定。用DS18B20作为反馈源时注意它的采样率很低12位分辨率下不到2HzPID的采样周期要和传感器转换时间匹配否则会引入较大滞后。我的经验是采样周期设1秒左右比较合适既不会因为传感器转换慢而“假死”也不会因为更新太快导致PID输出抖动。温控联动这个小扩展做完之后这个项目就不再是“玩具”而是可以做成一个小型恒温控制器或散热保护系统搬到实际的设备上使用。5.3 和图像处理、数据记录结合如果你已经把FPGA的基本功练到一定程度还可以考虑两个方向温度数据与FPGA图像处理联动比如做一个“温度异常检测图像抓拍”的系统温度数据通过单总线采集图像通过CMOS摄像头接口输入FPGA完成温度判断的同时对图像做实时处理。这类项目在工业检测、智能硬件里很常见。温度数据存储与回放FPGA内部或外挂Flash存储多天的温度记录再通过UART或USB导出到PC做曲线分析。这需要文件系统或简单的存储时序设计难度上比单总线高一个台阶。我在第二个方向上的实践是把温度数据每隔1分钟写入外挂的SPI Flash再用PC端的Python脚本读取并绘制温变曲线。做完之后这套系统就被用在了机房服务器散热评估项目里连续跑了好几个月稳定性和精度都让人满意。5.4 后续可以继续啃的技术点如果你跟着这篇文章把基础版本调通了接下来自学方向可以很自然地延伸到在总线上挂多颗DS18B20并实现ROM搜索算法把单总线控制器封装成独立的IP核支持AXI-Lite接口方便在SoC架构里复用增加CRC8校验提高数据可靠性使用状态机设计模式重构代码把复位、命令、读写的子状态做成可配置项适配其他单总线传感器。最后再分享一点个人体会实际做完这个项目我最深的感受是FPGA开发里最难的往往不是“算法”而是“时序的颗粒度”。DS18B20的时序参数不算复杂但你必须在每个微秒级别都想清楚“此时总线电平是谁在控制、我应该在哪个时钟沿采样”。这种思维一旦建立回头看很多接口协议SPI、I2C、UART都变得清晰起来。如果你正准备拿这个项目练手我的建议是先别急着上板花两个小时把协议时序图画明白再把状态机状态定义写在纸上最后才写代码。顺序反了的话大概率会在调试阶段反复推翻重来。另外仿真阶段一定要自己写testbench模拟DS18B20的行为不要直接拿现成模型跑一遍就完事。最后再分享一个小技巧调试时给DQ引脚留一个可观察的调试信号比如给状态机的每个状态输出到LED上这样传感器不响应时你一眼就能看出来是卡在复位、命令还是读取阶段。这个笨办法帮我在很多项目里省下了大量排查时间强烈推荐。