ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C#仪器控制避坑:VISA资源、Query封装与示波器波形读取

C#仪器控制避坑:VISA资源、Query封装与示波器波形读取

简介:这份压缩包面向具备一定C#基础、希望用VISA接口控制示波器、信号发生器、数字万用表等测试仪器的开发者与自动化测试人员,围绕统一资源管理、GPIB设备连接、命令读写和会话关闭等核心环节提供可运行的入门示例。包内共15个文件,约40KB,以6个C#源码为主,辅以2个XAML界面文件、解决方案与项目配置、说明文本及少量资源图片,构成一个结构紧凑的WPF示例工程,便于在Visual Studio中直接打开并对照理解VISA调用的代码组织方式。现有3735人学习,说明该主题在仪器控制与数据采集场景中具有较高关注度。读者可从中获得VISA资源初始化与设备通信的基础写法、界面与后台逻辑的衔接参考,以及进一步扩展SCPI命令解析、异步读取和错误处理的起点。

1. 一条 USB 线接上仪器之后:为什么大多数 C# 控制程序死在前二十行

桌面上一台数字示波器、一台可编程电源,USB 线插好,设备管理器里也认到了。打开 Visual Studio 新建控制台工程,引用 VISA 库,写下第一行Open("USB0::..."),程序抛出VisaException,错误码0xBFFF0011,翻译过来是「找不到资源」。做 C# 仪器控制的同行大概都在这个位置卡过一次,问题几乎从来不在 SCPI 命令写得对不对,而在 VISA 这一层没通:运行时没装、进程位数和运行时位数对不上、资源字符串拼错、会话被别家软件占着。

这篇笔记拆的就是这条链路——从资源地址怎么来、会话怎么开、Query 怎么封,到示波器波形块怎么收、跑一整天会冒出来的那些坑。适合已经会用串口助手发命令、但要把测试逻辑写进 C# 程序的工程师,也适合手里有一堆仪器却还在手动按面板的人。

2. VISA 地址与运行时选型:链路先通,代码后写

VISA 的本质是一层中间层:它把 USB-TMC、LAN、GPIB、串口这些物理链路统一抽象成「资源字符串 + 会话」两件事。上层代码只认Open(资源字符串),不关心底下走的是 USB 还是网线。这个抽象带来一个副作用——绝大多数连不上的问题,都能在不写一行代码的前提下定位,只要会用 VISA 自带的设备管理面板扫一遍资源列表。所以顺序应该是:装运行时 → 面板扫出资源 → 复制字符串 → 再动代码。

2.1 五类物理链路对应的资源字符串

资源字符串是有固定语法的,接口类型[板号]::地址::INSTR或...::SOCKET。板号默认为 0,同一台机器上装了两块 GPIB 卡才需要区分。下面这张表是我自己贴在工位上的速查版,改地址的时候对着改就行。

物理链路资源字符串样例关键字段常见翻车点
USB-TMCUSB0::0x1234::0x0001::SN00012345::0::INSTRVID、PID、序列号换 USB 口后序列号段变化,硬编码就会失效
LAN(VXI-11)TCPIP0::192.168.1.50::inst0::INSTR仪器 IP端口 111 与动态端口被防火墙拦掉
LAN(裸 socket)TCPIP0::192.168.1.50::5025::SOCKET端口号,SCPI 惯例 5025必须自己把终止符设成\n,否则永远读不满
GPIBGPIB0::12::INSTR主地址 0–30两台仪器地址拨成一样,扫出来只有一个
串口ASRL3::INSTRCOM 口号波特率、数据位、流控不在资源串里,要在会话上配

USB 那一条最容易被写死。VID/PID 是厂商分配的,序列号才是区分同型号不同台的唯一标识,但它只在设备枚举时稳定。我一般会在配置文件里存两套:优先按序列号匹配,匹配不到再按 VID/PID 取枚举到的第一个,并打一条告警日志。这样实验室里换机器不至于整个程序起不来。

串口那条要单独说。ASRL3::INSTR只指定了端口,通信参数得用属性设:

// 串口类仪器的通信参数不在资源字符串里,必须在会话上显式配置 session.SerialSettings.BaudRate = 9600; session.SerialSettings.DataBits = 8; session.SerialSettings.StopBits = SerialStopBits.One; session.SerialSettings.Parity = SerialParity.None; session.SerialSettings.FlowControl = SerialFlowControl.None; session.TerminationCharacter = 0x0A; // 命令以 \n 结尾 session.TerminationCharacterEnabled = true;

SerialSettings是ISerialSession上的属性,强转之前先用as判空,不然链路上本来走的是 USB,代码一跑就是空引用。流控这一项最容易漏:仪器侧开了硬件流控、程序侧写None,表现是短命令能通、长命令截断,看起来像玄学,其实是 RTS/CTS 没握手。

2.2 运行时选型:能不能混装、位数怎么对

市面上主流有两套商业 VISA 实现,业内习惯称 A 系和 B 系,各自带一套 IO 库和一套设备管理面板。不要混装,这是第一条纪律:两套实现的动态库同名,装了第二套会覆盖第一套的注册信息,表现是昨天还好好的程序今天报「找不到 VISA 实现」。如果必须共存,装的时候把主实现设成默认,另一套只保留不注册的绿色副本。

位数问题比混装更隐蔽。VISA 通过 COM 暴露接口,32 位进程只能加载 32 位组件,64 位进程只能加载 64 位组件。Visual Studio 新建的 C# 控制台工程默认是 AnyCPU,在 64 位系统上跑起来就是 64 位进程;如果装的运行时只注册了 32 位组件,new ResourceManager()会抛COMException,错误描述含糊得像黑匣子。核对方法很土但有效:在面板工具的「关于」里看版本旁边标的位数,再去项目属性里把平台目标改成一致的那个。

至于 .NET 侧用哪套 API,两条路:

  • IVI 标准的 .NET 互操作程序集(命名空间Ivi.Visa)。这是跨厂商通用的一套,接口语义和 VISA COM 一一对应,换实现不用改代码。新项目我一般走这条。
  • 特定实现方提供的原生 .NET 封装。API 更顺手,但绑死一家。

无论哪条,都建议把 VISA 访问收在一个自己写的IInstrumentSession接口后面。理由很现实:产线机器换代、运行时从 A 系换到 B 系的时候,你只需要重写一个类,而不是在两百个文件里搜ResourceManager。

2.3 先用面板扫一遍,再写第一行代码

装完运行时,插上仪器,打开设备管理面板。它会枚举所有已连接资源,并且能直接对某个资源发*IDN?。这一步的价值在于把故障域切开:面板能读到 IDN,说明驱动、线缆、地址全对,后面代码出问题就只可能是程序自己的事;面板也读不到,那就在硬件层查,别在代码里瞎改。

操作顺序大致是这样:

  1. 关掉所有可能占用仪器的软件(示波器上位机、厂商的波形查看器、上次没退干净的调试进程)。
  2. 打开面板,刷新资源树,确认目标仪器出现在列表里,资源字符串完整显示。
  3. 对该资源发*IDN?,返回形如厂商,型号,序列号,固件版本的四段式字符串。
  4. 把界面上的资源字符串原样复制进配置文件,不要手敲,0和O这种字符肉眼分不出来。
  5. 面板里的会话用完就断开,别让它一直占着——它占用的锁和你程序里争的是同一把。

提示:面板里能读到 IDN 但程序报资源忙,九成是面板自己的会话没释放,或者上一次调试时进程被强杀、句柄没回收。注销重登能解决大部分这类残留。

3. 最小可用闭环:从*IDN?到能复用的 Query

第一段能跑的代码不要写复杂,就三件事:开会话、发一条查询、把结果读回来。跑通之后再把这三件事封装掉,后面所有仪器操作都靠这个封装。这个顺序反过来——先设计一套漂亮的抽象层再调试——往往调到最后不知道是抽象层错了还是链路错了。

3.1 开会话与超时:别用默认值

VISA 会话有一堆属性,前期真正需要动的只有超时和终止符。默认超时通常是 2000 毫秒,对单次查询够用,但对「发一条命令让仪器做一次长积分再返回」就不够;有些实现默认超时是无限等待,程序卡死在那里,连异常都不给你。我一般显式设成 5000 毫秒起步,长操作单独调到 30000。

using Ivi.Visa; using Ivi.Visa.FormattedIO; // 打开消息型会话;仪器控制绝大多数走 IMessageBasedSession IMessageBasedSession session = (IMessageBasedSession)GlobalResourceManager.Open("TCPIP0::192.168.1.50::inst0::INSTR"); session.TimeoutMilliseconds = 5000; // 单条命令 5 秒上限 session.TerminationCharacter = 0x0A; // '\n' session.TerminationCharacterEnabled = true; // 读到终止符即判定一条消息结束 // 清掉可能残留的输入输出缓冲,避免上一次异常留下的半条消息干扰 session.RawIO.Flush(IOBuffers.Receive | IOBuffers.Send); session.Clear();

逐行说一下值得注意的:

  • GlobalResourceManager.Open是静态入口,它内部维护连接池,同一个资源字符串重复 Open 拿到的可能是同一个底层会话。所以别在循环里反复 Open/Dispose,那是给自己制造锁竞争。
  • TimeoutMilliseconds是单次读或写的超时,不是整条命令往返的超时。Write很快返回,真正卡的是Read。
  • TerminationCharacterEnabled设为true之后,ReadString会一直读到\n为止。走 TCP 裸 socket 时这一项必须开,因为 socket 本身没有消息边界,VISA 只能靠终止符切分。
  • Flush和Clear是两个不同层级的动作:前者清 VISA 自己的缓冲区,后者往仪器发 IEEE 488.2 的*CLS类清除。异常恢复路径里两个都要做。

3.2 把 Write/Read 收成一个 Query

最原始的写法是先Write再Read:

session.RawIO.Write("*IDN?\n"); string idn = session.RawIO.ReadString();

问题在于手工拼\n容易漏,而且一旦某次Write成功、Read抛异常,缓冲区里就留着一条没被取走的消息,下一次Read读到的是上一次的结果。这种错位极难查——现象是程序返回的数据总是慢一拍,或者解析报格式错误。封装成 Query 的价值不只是少写两行,而是把「一写一读必须成对」这条约束收进一处:

public string Query(string command, int timeoutMs = 5000) { if (command == null) throw new ArgumentNullException(nameof(command)); _session.TimeoutMilliseconds = timeoutMs; // 写之前先清接收缓冲,防止上一次异常遗留的半条消息被当成本次结果 _session.RawIO.Flush(IOBuffers.Receive); try { _session.FormattedIO.WriteLine(command); // WriteLine 会自动补终止符 return _session.FormattedIO.ReadLine().Trim(); } catch (VisaException ex) { // 超时后仪器可能仍在发送,必须清一次,否则后续命令全部错位 _session.RawIO.Flush(IOBuffers.Receive); throw new InstrumentException($"{command} 执行失败,状态码 0x{ex.ErrorCode:X8}", ex); } }

FormattedIO.WriteLine和RawIO.Write的区别是前者会按TerminationCharacter自动补结束符,后者要自己拼。混用这两套 API 是另一个常见错位来源:WriteLine加一个\n,你又手工拼一个\n,仪器收到空行,返回-113 Undefined header,而你还在奇怪为什么每次查询都多一条错误。

Trim()不是可有可无的装饰。不同厂商返回的 IDN 尾部带不带\r不一致,带\r的字符串去做字典匹配永远匹配不上。同理,解析数值前统一做double.Parse(s, CultureInfo.InvariantCulture),别用当前区域设置——某些区域的小数点是逗号,1.234会被解析成1234,这种错在中文环境里反而不会暴露,到海外现场就炸。

3.3 会话生命周期与断线重连

会话是稀缺资源,仪器侧的连接数通常只有个位数。写法上最稳的是把会话收在一个长生命周期的对象里,程序退出时统一释放,而不是每次操作开一个。释放顺序也有讲究:先Dispose会话,再释放ResourceManager,反过来会让会话持有已失效的引用。

长时间运行的产线程序必然遇到断线——网线被碰掉、仪器重启、USB 口供电波动。裸写using块的话,一次断线整个程序就废了。我的做法是包一层带重试的会话管理:

public string QueryWithRetry(string command, int retry = 2) { for (int attempt = 0; attempt <= retry; attempt++) { try { if (_session == null) OpenSession(); // 延迟重建 return Query(command); } catch (VisaException) when (attempt < retry) { CloseSession(); // 关键:先把坏会话彻底丢弃 Thread.Sleep(200 * (attempt + 1)); // 递增退避,给仪器恢复时间 } } throw new InstrumentException($"重试 {retry} 次后仍无法与仪器通信:{command}"); }

CloseSession里必须真的Dispose并把字段置空,不能只是 try-catch 吞掉异常继续用旧会话——一个处于错误状态的会话后续所有操作都会失败,而且失败得很随机。退避时间从 200 毫秒起递增,是因为仪器重启后 VISA 服务重新枚举资源需要时间,立刻重连只会连续失败。

注意:重连逻辑只对「链路类」异常有意义。如果仪器返回的是 SCPI 语法错误(-113、-100),重试一万次也是同样的结果,那属于命令写错了,应该在解析层拦下来,别混进重连分支。

4. 波形块数据:示波器读数最容易错的一环

文本查询好办,读到字符串解析就完事。波形不一样,它是二进制块,一次可能几十万个采样点,编码方式还分 BYTE 和 WORD 两种。这一段搞错,画出来的波形要么是一条直线,要么幅度差一千倍,而且不报错——VISA 只管搬运字节,不负责告诉你这些字节是什么意思。

4.1 先读前置参数,再决定读多少点

示波器的波形数据不是裸数组,它依赖一组前置参数来解释。标准做法是先发:WAVeform:PREamble?,拿回十个逗号分隔的值,再发:WAVeform:DATA?取数据。这十个值的含义固定但顺序容易记混,我列一下:

序号字段含义单位
1format数据格式,0=BYTE,1=WORD枚举
2type波形类型,0=普通,1=平均,2=峰值枚举
3points本次传输的采样点数个
4count平均次数(非平均模式为 1)次
5xincrement相邻采样点的时间间隔秒
6xorigin第一个采样点的时间坐标秒
7xreference时间轴参考点索引索引
8yincrement纵向每个码值代表的电压V/LSB
9yorigin纵向零点偏移V
10yreference纵向参考点码值码值

取数据之前还有一步经常被跳过:设置点数。默认点数可能只有 1000,你想要全屏 100 万个点,得显式设:WAVeform:POINts,或者在支持的模式下设成最大值。点数设得比屏幕采样率还大,仪器会做插值,出来的波形看着平滑但已经不是原始数据了,做参数测量的时候这会带来系统性偏差。

// 1) 先固定数据源与格式,再设点数,最后取值——顺序反了会拿到旧格式的数据 Write(":WAVeform:SOURce CHANnel1"); Write(":WAVeform:FORMat BYTE"); // BYTE 每点 1 字节,传输快,精度 8 位 Write(":WAVeform:POINts 10000"); // 显式指定点数,不要依赖默认值 // 2) 读前置参数,按逗号切分 string pre = Query(":WAVeform:PREamble?"); string[] f = pre.Split(','); int points = int.Parse(f[2], CultureInfo.InvariantCulture); double xIncrement = double.Parse(f[4], CultureInfo.InvariantCulture); double xOrigin = double.Parse(f[5], CultureInfo.InvariantCulture); double yIncrement = double.Parse(f[7], CultureInfo.InvariantCulture); double yOrigin = double.Parse(f[8], CultureInfo.InvariantCulture); double yReference = double.Parse(f[9], CultureInfo.InvariantCulture);

先说格式再设点数,这条顺序是有原因的:部分仪器在FORMat切换时会重置POINts,你要是先设点数再改格式,点数被悄悄改回去,读回来的数组长度和预期对不上。这类隐式重置在手册里通常写在很不起眼的脚注里。

4.2 二进制块的两种取法

ReadBinaryBlockOfByte()是最省事的一条路,VISA 帮你把 IEEE 488.2 的块头解析掉,直接返回纯数据:

Write(":WAVeform:DATA?"); byte[] raw = _session.FormattedIO.ReadBinaryBlockOfByte(); if (raw.Length != points) throw new InstrumentException($"点数不一致:前置参数 {points},实际收到 {raw.Length}");

块头长这样:#8+ 8 位十进制长度 + 二进制数据。#后面那个数字表示「长度字段占几位」,8就是八位,最大能描述 99999999 字节。也有#0表示长度不定、以终止符结束的写法,这种在二进制数据里极其危险——数据里只要出现一个0x0A,读取就提前截断。所以看到#0的仪器,要么改用定长格式,要么退回RawIO.Read()自己按长度字段解析。

如果手上的封装没有ReadBinaryBlockOfByte,手工解析也不复杂:

string header = _session.RawIO.ReadString(); // 读到 '#' 为止 int digits = int.Parse(header.Substring(1, 1)); // 长度字段的位数 int length = int.Parse(header.Substring(2, digits)); byte[] payload = _session.RawIO.ReadBytes(length); // 只读数据部分,不多读一个字节

ReadBytes(length)的关键是精确读 length 个字节。用ReadString去读二进制数据是新手最容易犯的错:数据里只要出现0x00或0x0A,字符串会被截断或提前结束,返回的数组长度不对,你还在怀疑是仪器设置问题。

4.3 换算公式与核对方法

BYTE 格式下,电压换算的通行公式是:

V[i] = (raw[i] - yReference) * yIncrement + yOrigin T[i] = (i - xReference) * xIncrement + xOrigin

注意yReference是码值不是电压,yOrigin才是电压偏移。这两个字段名只差三个字母,写反了波形整体上下平移,肉眼看不出来,做上升沿时间测量时误差全跑出来。我见过一个项目组排查了两天,最后发现是把yOrigin和yReference用反了。

WORD 格式要额外处理字节序。仪器吐出来的 WORD 数据通常是大端,而 x86 平台是小端,直接BitConverter.ToInt16会得到字节交换后的值:

// 大端 WORD -> 小端 short;用 BinaryPrimitives 比手写移位干净 short[] samples = new short[raw.Length / 2]; for (int i = 0; i < samples.Length; i++) { samples[i] = BinaryPrimitives.ReadInt16BigEndian(raw.AsSpan(i * 2, 2)); }

核对换算对不对,有个不依赖仪器的土办法:把探头接到仪器自带的校准方波输出上(通常是 1 kHz、0.5 V 或 1 V 峰峰值),读一屏波形,看程序算出来的峰峰值和幅度是不是和面板显示一致。差一倍,大概率是 BYTE/WORD 判错;差固定偏移,是yOrigin用错;完全是一条直线,先去查:WAVeform:SOURce是不是指到了一个没接信号的通道。

现象高概率原因核对动作
波形幅度约为面板值一半BYTE 数据按 WORD 解析,或反之打印format字段实际值
波形上下平移固定量yOrigin与yReference用反用校准方波比对零点
波形为直线但非零:WAVeform:SOURce通道指错读通道号回显
数据长度对不上读数前POINts被格式切换重置读POINts?回显确认

5. 避坑与排查:五条踩出来的记录

下面这五条,每一条我都在真实项目里遇到过不止一次,而且共同点是「现象看着像软件 bug,根因在别处」。整理成现象-原因-解决三段式,方便对着查。

5.1 链路层:能 ping 通,资源打不开

现象:仪器 IP 能 ping 通,用浏览器访问它内嵌页面也正常,但Open("TCPIP0::192.168.1.50::inst0::INSTR")直接抛「找不到资源」。

原因:VXI-11 协议用的是 111 端口做端口映射,之后再在动态高端口上建数据连接。企业网的安全策略经常只放行已知端口,111 通了、动态端口被挡,握手卡在中间。另一种情况是这台仪器根本不支持 VXI-11,只实现了裸 socket 的 SCPI 服务,端口 5025。

解决:先用Test-NetConnection或厂商面板确认 VXI-11 是否可用;不可用就换裸 socket 资源串TCPIP0::192.168.1.50::5025::SOCKET,同时打开TerminationCharacterEnabled。换完之后仍然不通,抓一次包看 TCP 三次握手有没有建立——握手都没建立的话,问题在网络层,别在 VISA 上耗。

# 先确认 5025 端口是否在监听,有输出说明裸 socket 服务是开的 Test-NetConnection 192.168.1.50 -Port 5025 # VXI-11 用的端口映射服务 Test-NetConnection 192.168.1.50 -Port 111

5.2 会话层:第二次运行报「资源被占用」

现象:第一次跑得好好的,关掉程序再启动,Open抛0xBFFF0016资源忙。任务管理器里已经看不到自己的进程了。

原因:进程被强杀(调试时点停止、或者任务管理器结束任务)时,Dispose没执行,仪器侧的会话在超时前一直挂着。VISA 的连接池也还留着一条半死的记录。还有一种可能是设备管理面板自己开着一个会话没断。

解决:程序里所有会话都用using或者try/finally保证释放;调试时尽量用控制台的 Ctrl+C 触发ProcessExit做清理,而不是直接结束进程。已经卡住了,就关掉面板工具、等 30 秒让仪器侧会话超时回收,或者给仪器断电重启。写产线程序的话,加一个启动时的自检:能打开就继续,打不开先尝试Clear()再等一轮,还是不行就明确报错让人去处理,别静默重试。

5.3 会话层:查询结果永远慢一拍

现象:发:MEASure:VOLTage?,返回的是上一条命令的结果。第一次查询超时,之后每次都对不上。

原因:某次Write成功但Read抛了超时异常,仪器其实已经把响应发出去了,数据躺在 VISA 接收缓冲里。下一次Read先取到这条陈旧数据,于是整体错位一位。

解决:所有异常路径都要Flush(IOBuffers.Receive),Query 封装里写前写后各清一次最保险。另外别用「发一条读一条」之外的模式去猜仪器行为,比如连发三条查询再连读三次——中间任何一条出错,后面全乱,而且定位不到是哪条坏的。宁可慢一点,一条一条同步走。

5.4 数据层:串口仪器返回乱码

现象:同一个ASRL资源,短命令正常,超过 20 个字符的命令返回一堆???或者半截字符串。

原因:仪器侧开了 RTS/CTS 硬件流控,程序侧SerialSettings.FlowControl还是None。短命令在缓冲区内一次发完,长命令被仪器拉低 CTS 要求暂停,程序没理,继续灌数据,接收端就丢字节了。

解决:把流控对齐成仪器手册里写的那个值,两边必须一致。没有流控需求就两边都关。另外确认DataBits、StopBits、Parity三项也是对齐的,这三项错了通常是一条都通不了,而流控错了是「有时候通」,最具迷惑性。

5.5 数据层:跑八小时之后内存缓慢上涨

现象:短时间测试正常,产线连续跑一晚上,进程内存从 80 MB 涨到 2 GB,最后 OOM。

原因:每次测量都GlobalResourceManager.Open一个新会话,用完没有Dispose。VISA 的连接池会保留这些引用,仪器侧连接数也被慢慢耗尽,最后新连接全部失败。

解决:会话按「一台仪器一个」的粒度创建,程序生命周期内复用。真需要并发访问同一台仪器,就加锁串行化,别开多个会话——仪器端的命令解析本来就是单线程的,多会话并发只会互相干扰,返回结果张冠李戴。用IDisposable把所有会话收在一个InstrumentManager里,退出时统一释放。

提示:这五条里有四条的共同根因是「异常路径没处理」。VISA 的异常不像托管异常那样安全,它背后挂着真实的硬件资源。写的时候把每个catch都当成一个必须清理的现场,能省掉后面大半的排查时间。

6. 塞进产线程序:把测量做成可校验的流程

单次测量跑通到产线可用,中间隔着的不是技术难点,是工程习惯。我后来所有仪器控制项目都强制加一个「自检 + 回读」的前置环节:程序启动时先对每台仪器发*IDN?,把返回的型号和序列号和配置文件里的期望值比对,不一致直接拒绝启动,不让它带着错配的仪器跑完一整批。

第二个习惯是每条会改变仪器状态但不返回值的命令后面跟一条同步。*OPC?返回1表示前面的操作已经执行完,适合放在「设置量程 → 等稳定 → 读取」这类需要确定性的流程里。没有它,你会靠Thread.Sleep硬等,等短了数据不稳,等长了节拍上不去。

// 设置后同步,避免用 sleep 猜时间 Write(":SENSe:VOLTage:DC:RANGe 10"); Query("*OPC?"); // 阻塞到量程切换真正完成 string reading = Query(":MEASure:VOLTage:DC?");

第三个习惯是错误队列必须定期清。多数仪器支持:SYSTem:ERRor?,每轮测试结束读一次,非零就记进日志。这些错误在正常流程里不会让程序崩溃,但会悄悄让你的测量值偏离——比如超量程时返回的是一个被截断的数,看着完全合理。

最后,把所有仪器命令集中在配置文件里,别散落在业务代码中间。换型号的时候改配置,不改代码,这一条能省掉后面无数次的重新编译和回归测试。

说个自己的教训:早年做一个多工位测试台,我图省事在每个工位的线程里各自Open同一台电源,测试跑十分钟就有一两个工位拿到别人的电压读数。查了三天,最后发现是并发会话互相覆盖。从那以后我每次接新项目,第一件事就是把会话层写成单例 + 串行队列,宁可牺牲一点理论吞吐,也不让两个线程碰同一个句柄。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进