news 2026/10/1 12:06:11

NI-VISA下用C++调用数字万用表驱动:从SCPI到数据读取

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
NI-VISA下用C++调用数字万用表驱动:从SCPI到数据读取

简介:面向C++开发者和NI硬件用户的DMM驱动资源,聚焦NI数字万用表(DMM)板卡的编程控制。资源对应《深入理解DMM驱动:NI数字万用表的C++编程实践》,涵盖设备初始化、测量参数配置、数据采集、错误处理与设备关闭等核心环节,适合需要以C++直接操作万用表完成电压、电流、电阻自动化测量的工程场景。资源包为zip压缩包,仅含1个cpp源文件,包体约2KB,文件虽小但可作为入门示例,帮助理解NI-DMM API的基本调用流程。已有283人学习下载,适合实验室测试、生产检测及仪表自动化学习者参考。通过研读该驱动源码,可快速掌握NI万用表驱动程序的框架结构,学会封装底层硬件操作并接入自有测量程序,为构建更复杂的测试系统提供参考。

1. 为什么“DMM驱动”不只是一张光盘里的老掉牙文件

如果你在某个工控论坛或者旧电脑的D盘里翻到dmm.zip,大概率第一反应是“哪个年代的驱动包”。但真正在实验室里被NI数字万用表折磨过的人会知道,这个名字背后是一整套链路:硬件是Keysight或者NI的台式数字万用表,接口是GPIB、USB或者LAN,软件是NI-VISA,而驱动文件里的那些DLL和C++头文件,决定了你上层写的测量程序是秒回数据还是黑匣子一样弹出一串错误码。这个场景在产线老化测试、电池容量分选、传感器标定的工位上太常见了。

这文章要聊的不是怎么双击Setup.exe装个驱动程序,而是当你拿到dmm.zip这类驱动包之后,怎么在NI-VISA体系内用C++把它调起来,把数字万用表的电压、电流、电阻读数取回你的程序里。适合谁看?正在用LabVIEW但被要求改成C++上位机的工程师,或者手上有一台支持SCPI的台式万用表、想绕开厂商自带的上位机自己写采集逻辑的人。我会把驱动层次、NI-VISA的角色、C++调用方法、SCPI命令格式串起来讲,最后落在几个我踩过的坑上。

2. DMM驱动到底驱动了什么:硬件、固件还是SCPI指令

先把概念钉死:数字万用表不是声卡显卡那种即插即用的PC外设,它本质上是一台自带测量固件的仪器。所谓“驱动”,在台式仪器领域并不是给操作系统提供设备节点,而是给上层应用提供“访问测量功能”的接口。很多人翻车就是没分清这一点,以为装了驱动就等于能读数据,结果程序打开后连设备地址都枚举不到。

2.1 仪器驱动的三层结构:厂商DLL、VISA层和应用层

NI体系下的DMM驱动,常见的有两种形态。一种是厂商把功能封装成IVI-C或者IVI-COM驱动,你调用niDMM_Init、niDMM_ConfigureMeasurement这类API就行;另一种是更薄的做法,驱动里只提供VISA的viWrite和viRead封装,真正的测量逻辑靠你的代码写SCPI命令让万用表自己执行。dmm.zip这类包里通常两类东西都有:include目录下是头文件,lib目录下是导入库,还有一堆PDF是编程手册。

这三层的关系要捋清楚。最底层是VISA——NI-VISA是NI提供的IO接口标准,它统一了GPIB、USB、LAN和串口四种物理连接。中间层是厂商驱动DLL,它内部调用VISA来做传输。最上层才是你的C++程序。如果你用IVI方式,程序只跟厂商DLL打交道;如果你直接发SCPI,那程序绕过中间层,直接通过VISA的viWrite/viRead跟仪器对话。后者的好处是少一层封装,排查问题更直接,坏处是你要自己管理测量配置和错误检测。

2.2 从dmm.zip里先找这三个文件,别急着解压安装

解压驱动包后,别急着跑安装程序,先确认三样东西在不在。第一,编程手册PDF,里面通常有SCPI命令列表和VISA属性表;第二,对应版本的nidmm.h头文件(如果是NI的DMM),或者厂商自己的xxxDMM.h;第三,32位和64位的导入库,常见命名是nidmm.lib或者NIDMM_64.lib。缺任何一个,后面都有得折腾。

我一般会先装好NI-VISA,哪怕厂商驱动安装包里面自带VISA,也建议单独装一遍最新版。版本对齐是玄学也是血泪经验:NI-VISA 2010左右的老版本在Win10上经常枚举不到USB设备,而新版又可能跟厂商老驱动DLL的依赖冲突。一个稳妥的做法是:先装NI-VISA,再装驱动包,最后用NI MAX(Measurement & Automation Explorer)看一眼设备能不能在“设备和接口”里出现。这一步过了,C++侧的活才刚开始。

2.3 量程、分辨率、触发这些名词,其实是驱动API的参数

读万用表不是“打开设备然后一串数据流进来”,每次测量都是一次“配置—触发—读取—处理”的单次事务。驱动API的参数,比如量程(Range)、分辨率(Resolution)、触发源(Trigger Source)、采样数(Sample Count),直接映射到万用表前面板的功能键。在C++里你就是在跟这些参数打交道。

举例来说,niDMM_ConfigureMeasurement函数需要你传measurement function和range两个关键参数。测直流电压时,function选NIDMM_VAL_DC_VOLTS,range传预期最大值,如果传NIDMM_VAL_AUTO_RANGE就是自动量程。分辨率参数决定读数的位数——6位半还是5位半,数据处理速度完全不同。这些参数不是拍脑袋填的,在下一章接线和配置里我会把一套能直接跑的数值方案给出来。

3. 在NI-VISA下用C++跑通DMM的最小工程:从环境到第一个电压读数

这一章是全文的动手核心。不要被“万用表驱动”这个名字吓住,本质上就是:用C++调VISA函数,把一行SCPI命令发给仪器,再把返回的ASCII字符串解析成double。我按一套能最小复现的流程来,跟着做就能看到读数。

3.1 环境准备:VC++和NI-VISA的版本选择

先说编译器选择。NI-VISA对Visual Studio的支持比较老派,用VS2015到VS2019都没问题,VS2022也能跑但要注意平台工具集按VS2019(v142)来设置,避免某些头文件的CRT兼容问题。我一般用Visual Studio 2019,创建空控制台项目,项目属性里的配置类型选“应用程序”,平台选x64(前提是装了64位VISA)。

NI-VISA的安装版本建议用NI-VISA 20.0以上的版本,因为它包含VISA 5.4标准,支持USB RAW和最新的GPIB驱动。装完确认一下C:\Program Files\IVI Foundation\VISA\Win64\Include目录下有visa.h和visatype.h,Win64\Lib\msc\visa64.lib存在。这两个是硬依赖,缺一个连编译都过不了。

3.2 找到设备地址:NI MAX提供的“后悔药”

设备地址不对,后面全白搭。打开NI MAX,左侧“我的系统—设备和接口”,DMM设备会列出来。右键属性能看到VISA资源名称,常见格式是USB0::0x2A8D::0x0001::MY57290123::INSTR(Keysight)或者GPIB0::22::INSTR。这个字符串就是连接钥匙。

如果设备没出现在NI MAX里,先检查物理连接:USB线要插仪器的USB Device口而不是Host口,GPIB要确认板卡的IEEE-488地址和仪器面板设置一致。还有一个高频翻车点:仪器开机后默认处在“本地”模式(LOCAL按键灯亮),此时PC发的SCPI指令会被面板锁定,需要先通过驱动API里的viClear或者前面板LOCAL键解锁。这一条知道的人不少,但每次忘记都会浪费20分钟。

3.3 最小C++代码:发送SCPI并读取电压值

来看一段可以直接编译的代码。假设设备地址是上面Keysight那种USB格式,换成你自己的资源名即可。

// dmm_read.cpp - 通过NI-VISA发送SCPI命令读取DMM读数 #include <stdio.h> #include <string.h> #include "visa.h" int main() { ViStatus status; ViSession rm, instr; ViUInt32 retCount; char buf[256] = {0}; char command[64] = "MEAS:VOLT:DC? 10.0,0.001"; // 测直流电压,量程10V,分辨率1mV // 1. 打开默认资源管理器会话 status = viOpenDefaultRM(&rm); if (status < 0) { printf("打开资源管理器失败,错误码: 0x%x\n", status); return -1; } // 2. 连接仪器,超时设为5000ms status = viOpen(rm, (ViString)"USB0::0x2A8D::0x0001::MY57290123::INSTR", VI_LOAD_LIBRARY_NATIVE, VI_NULL, &instr); if (status < 0) { printf("打开设备失败,检查VISA资源名称。错误码: 0x%x\n", status); viClose(rm); return -1; } // 3. 设置I/O超时 viSetAttribute(instr, VI_ATTR_TMO_VALUE, 5000); // 4. 清空输入输出缓冲区,避免残留数据干扰 viClear(instr); // 5. 发送命令 status = viWrite(instr, (ViBuf)command, (ViUInt32)strlen(command), &retCount); if (status < 0) { printf("指令发送失败,错误码: 0x%x\n", status); viClose(instr); viClose(rm); return -1; } // 6. 读取响应 status = viRead(instr, (ViBuf)buf, sizeof(buf) - 1, &retCount); if (status < 0) { printf("读取响应失败,错误码: 0x%x\n", status); viClose(instr); viClose(rm); return -1; } buf[retCount] = '\0'; // 7. 输出结果 printf("测量值: %s V\n", buf); // 8. 清理会话 viClose(instr); viClose(rm); return 0; }

逻辑说明:这段代码做了四件事——建立VISA会话、发命令、收响应、清理。viOpenDefaultRM打开VISA的资源管理器,返回会话句柄rm;viOpen把资源管理器会话和一个具体的仪器地址绑定,返回仪器的独立会话instr,后续所有操作都对着这个句柄来。viWrite发送SCPI命令,第三个参数是命令字符串的长度,retCount返回实际写入的字节数。viRead是阻塞读,它会在收到换行符或者达到Timeout后返回,超时时间在viSetAttribute里配了5秒。

参数说明:SCPI命令MEAS:VOLT:DC? 10.0,0.001是个“一条龙”测量命令,问号表示仪器测完直接把结果返回。第一个参数10.0是量程,第二个0.001是分辨率(单位是伏特)。如果量程参数传AUTO,仪器会先做一次预测量确定量程,速度稍慢但更省心。连接超时VI_ATTR_TMO_VALUE的单位是毫秒,表笔悬空或者量程设置错误时,命令会一直阻塞到超时,所以这个值别设太大,5000ms比较合理。

3.4 配置测量类型和触发模式的正式姿势

SCPI一步命令适合快速测试,但做正经采集时,最好用“配置-触发-读取”三步解析方式。原因是:一步命令每次都要重置仪器状态,连续采样的节拍会不稳;两步走可以预先配好量程和滤波,然后靠触发信号让每次采集的起点对齐。

两步前缀命令示例:

// 配置:直流电压,10V量程,分辨率1mV,内部触发 const char* configCmd = "CONF:VOLT:DC 10,0.001;TRIG:SOUR IMM;SENS:VOLT:DC:AVER:COUN 5"; // 触发并读取 const char* readCmd = "READ?";

CONF命令做测量配置,TRIG:SOUR IMM把触发源设为内部立即触发,SENS:VOLT:DC:AVER:COUN 5开启平均值滤波,采样5次取平均。之后每次需要新数据时,发READ?并用viRead读结果。

这一步的工程意义在于:滤波次数和读数速率是直接对立的。平均值次数设为1时,34401A类仪表每秒能出1000个读数;设成5次,速率降到大约每秒100。产线场景要求单次测量决策,多取几个平均值更稳;研发场景扫曲线恨不得每秒500点,那就得果断关掉滤波。这个参数取舍我后面还会再提。

4. 把DMM读数接入你自己的高精度系统:C++二次开发的关键

读到一个电压值只是第一步。实际工程里,你得把这个电压转成温度、电阻或功率,再跟上下限比较、写入数据库、画曲线。这个环节里,C++代码的组织方式直接决定调试效率。

4.1 封一个DMM类:用RAII管好VISA会话

VISA会话是资源,开着不关会导致下次打开设备失败。C++里自然要用RAII(资源获取即初始化)把viOpen和viClose包进构造函数和析构函数。下面这个类封装了最常用的三个动作:连接、读取、断开。

// DMMSession.h #pragma once #include <string> #include "visa.h" class DMMSession { public: DMMSession(const std::string& resourceName); ~DMMSession(); double ReadVoltageDc(double range = 10.0, double resolution = 0.001); bool IsOpen() const { return m_open; } private: ViSession m_rm = VI_NULL; ViSession m_instr = VI_NULL; bool m_open = false; bool SendCommand(const std::string& cmd); }; // DMMSession.cpp #include "DMMSession.h" #include <cstring> #include <cstdio> DMMSession::DMMSession(const std::string& resourceName) { if (viOpenDefaultRM(&m_rm) < 0) { printf("默认资源管理器打开失败\n"); return; } if (viOpen(m_rm, (ViString)resourceName.c_str(), VI_LOAD_LIBRARY_NATIVE, VI_NULL, &m_instr) < 0) { printf("设备打开失败: %s\n", resourceName.c_str()); viClose(m_rm); m_rm = VI_NULL; return; } viSetAttribute(m_instr, VI_ATTR_TMO_VALUE, 3000); viClear(m_instr); m_open = true; } DMMSession::~DMMSession() { if (m_instr != VI_NULL) { viClose(m_instr); m_instr = VI_NULL; } if (m_rm != VI_NULL) { viClose(m_rm); m_rm = VI_NULL; } } bool DMMSession::SendCommand(const std::string& cmd) { if (!m_open) return false; ViUInt32 retCount = 0; ViStatus status = viWrite(m_instr, (ViBuf)cmd.c_str(), (ViUInt32)cmd.size(), &retCount); return status >= 0; } double DMMSession::ReadVoltageDc(double range, double resolution) { char cmd[128]; snprintf(cmd, sizeof(cmd), "MEAS:VOLT:DC? %.4f,%.6f", range, resolution); if (!SendCommand(cmd)) return -999.0; char buf[64] = {0}; ViUInt32 retCount = 0; if (viRead(m_instr, (ViBuf)buf, sizeof(buf) - 1, &retCount) < 0) return -999.0; buf[retCount] = '\0'; return atof(buf); }

逻辑说明:构造函数里连续做了三件事——开资源管理器、开设备、设超时和清缓冲区。任何一个失败,m_open保持false,析构时也不会误关句柄。ReadVoltageDc把上文的SCPI一步命令包的严严实实,返回-999.0作为错误哨兵值。在工控里,错误的统一处理很重要,为0可能会被当成正常读数,这个哨兵值我习惯用-999.0,在配置上下限时天然不会误判。

参数说明:snprintf里的%.4f和%.6f是格式控制——量程打印4位小数(如10.0000),分辨率打印6位小数(如0.001000)。严格来说,SCPI解析器容忍多余空格和零,但强迫症式的格式对齐能让你在调试日志里快速比对命令差异。另外注意viClear和超时的组合:如果设备卡在某个错误状态,viClear能发设备清除信号,比纯软件等超时可靠。

4.2 多设备与多线程:VISA会话不是线程安全的

很多自动测试系统里,一台主机控制四台万用表,每个表测不同的点。别用共享会话加锁的笨办法,正确姿势是每个线程自己开一个viOpen会话。VISA对同一设备允许多个会话,日在硬件层面做了仲裁,代价是并发性能一点点,但对几十赫兹的采样率来说完全够用。

常见翻车写法是把DMMSession的对象作为全局变量,然后多线程直接调ReadVoltageDc。别这么干。VISA内部虽然有些锁,但厂商驱动DLL的很多静态变量不是线程安全的,轻则数据错乱,重则访问冲突。我自己的习惯是按测试工位分区:工位1线程实例化自己的DMMSession,工位2线程实例化自己的,互不相干。

4.3 上报状态码:用字符串转义错误信息

C++里viRead返回负数状态码,光打印0x80001004这种裸码没人看得懂。VISA提供了viStatusDesc函数把状态码转成描述字符串。调试时建议统一打出来:

ViStatus err = viRead(m_instr, (ViBuf)buf, sizeof(buf) - 1, &retCount); if (err < 0) { char desc[256] = {0}; viStatusDesc(m_instr, err, desc); printf("viRead失败: %s\n", desc); return -999.0; }

viStatusDesc的第三个参数是输出缓冲区,典型问题会显示“VI_ERROR_TMO”(超时)或“VI_ERROR_RSRC_BUSY”(资源忙)。超时90%是命令语法错导致仪器不响应;资源忙在GPIB菊花链拓扑里更常见,检查一下链上其他设备的地址是否冲突。把这些描述打进日志文件,比写errno管用得多。

5. 避坑指南:DMM驱动和C++联调中我踩过的五个典型问题

5.1 现象:NI-VISA设备枚举存在,但viOpen返回VI_ERROR_RSRC_NLOCKED

原因:这台仪器被另一个进程占用了。最常见的是NI MAX在后台打开了Test Panel,或者你的调试程序上次异常退出没执行viClose。VISA的锁定分共享锁和排他锁,viOpen默认排他。

解决:先把NI MAX所有关联窗口关掉。程序里给viOpen的第五个参数传VI_EXCLUSIVE_LOCK(默认就是),并在析构函数里幂等关闭。如果确认没有其他进程占用,重启万用表电源(不是重启电脑)即可释放硬件锁。这一条几乎每个第一次接仪器的人都会撞上。

5.2 现象:读数突然变成9.91000000e+37开头的一串数

原因:这串数在DMM世界里是“过载(Overload)”。表笔没接好,或者输入超出当前量程。34401A和34410A在超量程时返回的就是+9.91000000E+37,注意,这个数字不是真实读数。

解决:程序里加上越界判断,读取到这个前缀时直接报“端子开路或超量程”,然后重新viClear仪器。不要试图把它当成一个很大的电压参与运算,否则后面算平均值时会被一次性拉飞。自动化产线里更强的习惯是测之前做自检:先撤掉被测对象(或者短路输入端子),读一个接近0V的底噪,确认链路正常再开始正式测量。

5.3 现象:viWrite成功但viRead超时,程序卡在读取那里十几秒

原因:你发的SCPI命令不是问号命令(如CONF:VOLT:DC),仪器只配置不返回数据,而viRead永远等不到东西。

解决:先确认协议,命令里带?仪器才吐数据。另一种情况是仪器开启了连续测量模式(INIT后),测量在后台跑,但READ?被堵塞在高优先级触发里。解决方式是发TRIG:SOUR IMM强制触发,或者用ABOR中止当前测量后再READ。写测量循环时,记住“每一条读操作之前都必然有一次写操作”,没有例外的。

5.4 现象:Debug模式下程序正常,切换到Release模式后C++调用出现Access Violation C0000005

原因:这是老生常谈的调用约定和平台位数不一致。最常见于你Debug用Win32编译,Release改成了x64,但链接的导入库是32位的。VISA的老版本中,visa32.lib和visa64.lib不能混用。

解决:项目属性—链接器—输入—附加依赖项里,Win32平台写visa32.lib,x64平台写visa64.lib。另一种可能:你自己包装的DLL导出函数用了__cdecl,但头文件声明是__stdcall。检查所有声明,ViStatus已经是_VI_FAR定义过的宏,通常无需改,但如果你在类外单独声明VISA函数,一定要保持完全一致。

5.5 现象:AGILENT/KEYSIGHT的34461A和NI驱动不兼容,发命令没反应

原因:NI的nidmm.dll只能控制NI自己的DMM,或者明确声明支持IVI-Compatibility的型号。Keysight的34461A虽然支持SCPI,但如果你用的是NI-DMM驱动而不是VISA直发通道,驱动层会拒绝识别。

解决:对非NI品牌的万用表,统一走VISA直发SCPI路线,跳过NI的nidmm。把代码里对niDMM_Init的调用全换成viOpen加viWrite,这个思路对Keysight、Rigol、Keithley都成立。反正SCPI命令大体兼容,MEAS:VOLT:DC?全系通用,只是分辨率的特性和响应格式略有不同。

6. 进阶玩法:用空测量校准通道,把短时漂移压到最低

如果你要做高精度计量或者长时间老化数据采集,就会发现同一个电压今天测是10.0001V,明天变成了10.0018V。除了万用表自身的温度系数,还有一个容易被忽略的影响——热电动势和输入通道接触电阻。把DMM当成一台“能读电压的仪器”是最低境界,再往上走,是学会利用它内部的自校准机制。

我的做法是每个测量周期开头做一次“空测”:在表笔短接的状态下读100个点,取平均得到底噪补偿值,然后在正式测量序列里把每个读数减去这个底噪。不要小看这一步,对数字万用表而言,量程在1V以下时,短接状态的残余电压常常在几十微伏到几百微伏之间波动,如果被测信号本身是毫伏级的热电偶输出,这误差就会直接变成温度偏差。

代码层面的植入很简单,在DMMSession里加一个成员函数CalibrateOffset:

double DMMSession::CalibrateOffset(int samples) { double sum = 0.0; for (int i = 0; i < samples; ++i) { sum += ReadVoltageDc(0.1, 0.00001); // 100mV量程, 10uV分辨率 } return sum / samples; }

调用约定是:接线时把输入端子短路,开机预热至少30分钟,然后执行这个函数,拿到offset。后续每次ReadVoltageDc的实际数据表达式变成raw - offset。这里注意:量程越低,分辨率越高,空测效果越明显;如果你在10V量程下做空测,那个残差本身就被量程噪声盖住了,校准等于白做。

还有一个小技巧跟触发源配合:如果你做的是连续的自动化测试,试着把万用表的触发源设为外部触发(比如数字IO板卡的脉冲),然后把采样节奏交给硬件时钟,软件循环只负责收集读数。这样做的优点是测量间隔抖动从毫秒级降到微秒级,在电池内阻测试或者继电器吸合时序测试里特别有用。SCPI配置命令是TRIG:SOUR EXT,接线参考万用表手册的External Trigger端口。

最后说个习惯问题:每次改完代码重新编译之前,我都会习惯性地先把viClear(instr)放进构造函数里执行一次。这不是多余的防御,而是避免上一次程序残留未决的SCPI操作把当前会话带进黑洞。VISA是黑匣子,你看不到它内部堆了多久的陈旧调用,只有那次主动清空才是在真正重置仪器状态。这个习惯救过我很多次。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 12:04:21

Qoder本地AI编程引擎:告别HTTP延迟,实现毫秒级代码补全

1. 从“Codex用户”到“Qoder信徒”&#xff1a;一场IDE内AI编程体验的断崖式升级我第一次在IntelliJ IDEA里敲出// TODO: implement retry logic with exponential backoff&#xff0c;然后按下快捷键&#xff0c;等了3秒——光标没动&#xff0c;状态栏显示“Waiting for Cod…

作者头像 李华
网站建设 2026/10/1 12:04:10

字符串处理实战:多语言逆序、分割与转换陷阱解析

字符串大概是编程里最“不起眼”却又最能暴露水平的部分。我写了十几年代码&#xff0c;从C语言的char[]一路折腾到 Java、Python、C#、JavaScript 和各类SQL方言&#xff0c;发现一个很现实的问题&#xff1a;越基础的操作越容易翻车。逆序一个字符串人人都会&#xff0c;但遇…

作者头像 李华
网站建设 2026/10/1 12:04:08

电动车真空助力制动系统建模:从机理到数据驱动的实践

只有真正做过整车项目的工程师才懂&#xff0c;电动车制动系统最神奇的地方&#xff0c;不在卡钳和ESP&#xff0c;而在那块你几乎永远不会注意到的“真空”。每天早晚高峰&#xff0c;你踩下制动踏板&#xff0c;制动力在几十毫秒内建立&#xff0c;脚感和老燃油车几乎没差别。…

作者头像 李华
网站建设 2026/10/1 12:02:11

PyTorch非线性函数拟合实战:从数据归一化到激活函数选择

如果让我给刚接触 PyTorch 的朋友推荐一个练手项目&#xff0c;我大概率会先说&#xff1a;别急着上图像分类&#xff0c;也不用一上来就啃 Transformer&#xff0c;先拿一个非线性函数拟合任务把整个训练流程跑通再说。这个项目标题看起来很朴素——基于 PyTorch 实现的非线性…

作者头像 李华
网站建设 2026/10/1 12:00:20

Transformer架构原理与TensorFlow实现关系解析

1. 这不是“同类比较”&#xff0c;而是“苹果和水果刀”的关系 很多人第一次看到“Transformer和TensorFlow的区别”这个标题&#xff0c;下意识会以为这是两个并列的AI框架或模型——就像问“PyTorch和Keras哪个好”一样。但事实恰恰相反&#xff1a; Transformer是一种神经…

作者头像 李华
网站建设 2026/10/1 12:00:18

HHT时频图从解压到画对:EMD分解、Hilbert谱与调参避坑全流程

简介&#xff1a;希尔伯特-黄变换&#xff08;HHT&#xff09;时频图绘制的 MATLAB 代码示例包&#xff0c;面向信号处理研究者、机械故障诊断与生物医学信号分析人员&#xff0c;解决非线性、非平稳信号时频分布可视化的实现与调参问题。压缩包体积约 1KB&#xff0c;仅含 1 个…

作者头像 李华