当前位置: 首页 > news >正文

Winform Modbus 316线程 异步 λ表达式 泛型与数组 Encoding.ASCII.GetBytes bitConverter 大端小端 寄存器与label

this.Invoke

首先纠正:代码里不是List.Invoke,是**this.Invokethis代表当前的FrmMain窗体对象),这是WinForm开发中跨线程更新UI的核心方法**,灯珠状态、仪表、图表这些UI控件的更新都靠它,下面结合代码里的灯珠更新逻辑,通俗讲透作用+原理+写法,全程贴合你的代码:

一、先看代码里的真实写法(灯珠更新处的this.Invoke

// 后台线程读取灯珠状态(bool[] blStates)bool[]blStates=awaitmaster.ReadCoilsAsync(1,0,5);// 跨线程更新UI:必须通过this.Invokethis.Invoke(newAction(()=>{// 所有UI更新逻辑都写在这里面(灯珠、仪表、图表)if(!isWriting){chkState_01.Checked=blStates[0];//1号灯珠chkState_02.Checked=blStates[1];//2号灯珠// ... 3/4/5号灯珠}// 还有仪表、图表的更新逻辑也在这个Invoke里}));

二、this.Invoke核心作用:解决「跨线程更新UI的报错问题」

1. 先明确WinForm的铁律:UI控件只能由**创建它的线程(主线程/UI线程)**更新,其他线程(比如Task.Run开的后台数据采集线程)直接操作UI控件会报程序异常
  • 你的灯珠复选框chkState_01~chkState_05、仪表umTemperature、图表pvTrends,都是**主线程(UI线程)**创建的;
  • 读取Modbus灯珠状态的逻辑,在Task.Run开的后台工作线程里执行;
  • 如果直接写chkState_01.Checked = blStates[0];(后台线程直接操作UI),程序会立刻抛出**“跨线程操作控件”**的异常,直接崩溃。
2.this.Invoke就是WinForm提供的跨线程UI更新“桥梁”

它的作用是把“更新UI的代码逻辑”,从后台线程“委托”给主线程(UI线程)执行,本质是:后台线程不直接碰UI,只把“要更UI的指令”传给主线程,让主线程自己来更,完美遵守WinForm的铁律,不会报错。

三、拆解this.Invoke(new Action(() => { ... }))语法含义(通俗版,不用抠专业术语)

这个写法是WinForm跨线程更新UI的固定模板,拆开看每一部分都是为了“把UI逻辑传给主线程”:

  1. new Action(() => { ... }):把花括号里的UI更新代码,打包成一个「可被主线程执行的“指令包”」(Action是C#的无返回值委托,简单理解就是“一段可执行的代码块”);
  2. this.Invoke(...):把这个「指令包」提交给主线程(UI线程),主线程会在空闲时执行包里的UI更新逻辑;
  3. 花括号里的代码:所有要更新的UI操作(灯珠复选框、仪表数值、图表曲线),都必须写在这里面。

四、为什么灯珠、仪表、图表的更新都要放在同一个this.Invoke

代码里把灯珠、仪表、湿度/温度/亮度数值、图表曲线的更新,全塞在同一个this.Invoke的花括号里,核心原因是:
减少跨线程委托的次数,提升程序效率
每次this.Invoke都是后台线程和主线程的一次“通信”,如果把灯珠、仪表、图表各写一个this.Invoke,会产生3次通信,效率低;打包在一个里,只需要1次通信,主线程一次性完成所有UI更新,更高效。

五、补充:和this.Invoke类似的this.BeginInvoke(了解即可)

开发中还有一个this.BeginInvoke,和Invoke作用一致(都是跨线程更UI),区别仅在于:

  • this.Invoke同步等待主线程执行完UI更新,再继续执行后台线程的后续代码;
  • this.BeginInvoke异步不等待,把UI指令包传给主线程后,后台线程直接继续干活,不用等主线程更完UI。
    你的代码里用this.Invoke是更稳妥的选择,避免后台线程数据更新过快,导致UI更新错乱。

六、极简总结(针对你的问题)

  1. 代码里是**this.Invoke**不是List.Invokethis代表当前窗体,是跨线程更新UI的核心方法;
  2. 核心作用:解决后台数据采集线程直接更新灯珠/仪表/图表等UI控件的报错问题,通过“委托主线程更新UI”遵守WinForm的线程规则;
  3. new Action(() => { ... }):把灯珠更新等UI逻辑,打包成主线程能执行的“代码包”;
  4. 所有UI更新(灯珠、仪表、图表)都放这里面,是为了减少跨线程通信次数,提升效率

简单说,this.Invoke就是后台线程和UI线程之间的**“UI更新快递员”**:后台线程把“要更UI的指令”交给快递员,快递员送到主线程,主线程自己执行更新,全程不违规、不报错。

Task.Run

你的总结100%精准,把两个核心点的作用和区别完全抓透了!一句话再帮你定调,记死这个核心结论就行:
Task.Run(async => {})是开后台异步线程,解决「主线程卡死」;this.Invoke是跨线程委托,解决「后台线程不能直接更UI」,两者是配套使用的黄金组合

再用最简洁的话帮你梳理两者的核心定位+配合逻辑,更清晰:

1.Task.Run( async () => { ... } )—— 后台异步执行,防主线程卡死

  • 核心:开独立的后台工作线程,把「读Modbus硬件、生成模拟数据、计算最值」这些耗时/循环的脏活累活丢进去;
  • 目的:让**主线程(UI线程)**彻底解放,专门管界面渲染、按钮点击等交互,杜绝界面卡死
  • 关键词:异步、后台线程、解耦UI和数据采集。

2.this.Invoke( new Action(() => { ... }) )—— 跨线程UI更新,守WinForm规则

  • 核心:WinForm铁律「UI控件只能由主线程更新」,后台线程不能直接碰UI;
  • 目的:把「更新仪表、灯珠复选框、图表、标签」的UI操作,委托给主线程执行,既不报错,又能实时刷新界面;
  • 关键词:跨线程、UI委托、主线程执行。

两者的配套配合逻辑(整个系统的核心运行流程)

Task.Run开后台线程采数据/算数据→ 把最新数据存到temperatureValue等变量 → 调用this.Invoke委托主线程,用这些变量更新所有UI控件 → 后台线程延时1秒,重复上述流程。

简单说,这就是WinForm做实时数据监控系统经典固定写法
异步开线程采数据,跨线程委托更UI,既保证界面丝滑不卡死,又能合规、实时地展示数据。

chkState_all.Checked = !blStates.ToList().Exists(b => !b);

核心解读:一行代码实现「全选/全不选」灯珠的状态联动

这行代码是5个灯珠复选框的“全选状态”自动判断逻辑,作用是:当且仅当所有灯珠都处于开启状态(blStates里所有值都是true)时,chkState_all(全选复选框)才勾选;只要有1个灯珠关闭,全选框就取消勾选。

先拆解代码的每一部分,用通俗的语言讲透(结合灯珠场景):

1. 代码逐段拆解

chkState_all.Checked=!blStates.ToList().Exists(b=>!b);
代码片段含义(灯珠场景)
blStates从Modbus读取的5个灯珠状态数组(bool[5]),blStates[0]=1号灯、…blStates[4]=5号灯,true=开、false=关
.ToList()把数组转成List,只为调用Exists方法(数组本身没有Exists,List有)
.Exists(b => !b)检查List中是否存在满足条件的元素
b => !b= “元素的值是false(灯珠关闭)”
→ 结果:true=有灯珠关,false=所有灯珠开
!(最前面的取反)Exists的结果取反:
如果Exists返回true(有灯关)→ 取反后false(全选框不勾);
如果Exists返回false(全灯开)→ 取反后true(全选框勾)
chkState_all.Checked =把最终结果赋值给全选复选框的勾选状态

2. 举2个实际例子(一看就懂)

例子1:所有灯珠都开(blStates = [true,true,true,true,true])
  • blStates.ToList().Exists(b => !b)→ 检查是否有false?→ 结果false
  • 取反后 →true
  • chkState_all.Checked = true→ 全选框勾选。
例子2:3号灯珠关闭(blStates = [true,true,false,true,true])
  • blStates.ToList().Exists(b => !b)→ 检查是否有false?→ 结果true
  • 取反后 →false
  • chkState_all.Checked = false→ 全选框取消勾选。

3. 简化理解:等价的“人话代码”

这行代码完全等价于下面的写法(新手更易读),只是原代码用LINQ简化成了一行:

// 遍历所有灯珠状态,判断是否全部为trueboolisAllOn=true;foreach(boollightStateinblStates){if(lightState==false)// 只要有一个灯关{isAllOn=false;break;// 不用继续遍历了}}// 给全选框赋值chkState_all.Checked=isAllOn;

4. 核心设计目的

这是WinForm里**“批量控件+全选控件”的经典联动逻辑**:

  • 从硬件读取5个灯珠的实时状态后,自动同步“全选框”的勾选状态;
  • 用户看全选框就能快速判断“是否所有灯珠都开启”,不用逐个看5个灯珠复选框,提升界面易用性。

总结

  1. 核心功能:根据5个灯珠的实时状态,自动更新“全选复选框”的勾选状态
  2. 逻辑本质:判断blStates数组中是否“全为true”,是则勾选全选框,否则取消;
  3. 写法特点:用LINQ的Exists方法简化了遍历判断,一行代码搞定,是C#中简洁高效的写法;
  4. 最终目的:提升界面易用性,让用户快速感知所有灯珠的整体状态。

简单说,这行代码就是“全选框跟着所有灯珠的状态自动走”的核心逻辑,是界面交互设计中很常见的优化写法。

List 与 byte[]

你的理解抓到了核心!先给你结论:
✅ 用List<byte>核心就是利用它动态扩容的特性,解决字符串转字节后奇数长度补0的问题;
✅ 不是不能直接用List<byte>接,而是Encoding.ASCII.GetBytes(sendText)本身只能返回固定长度的数组,必须先转成数组再丢进List,这是方法返回值的限制,而非List的使用限制。

下面基于你真实的GetUshortData代码,逐行讲透每一步的原因,没有一句脑补:

先贴你代码里的真实方法

privateushort[]GetUshortData(stringsendText){List<byte>byteList=newList<byte>();byte[]textBytes=Encoding.ASCII.GetBytes(sendText);//将文本转换为字节数组byteList.AddRange(textBytes);//ushort 1个ushort=2字节if(textBytes.Length%2==1){byteList.Add(0x00);}List<ushort>ushortList=newList<ushort>();for(inti=0;i<byteList.Count;i+=2){byte[]item=newbyte[2];item[0]=byteList[i+1];item[1]=byteList[i];ushortList.Add(BitConverter.ToUInt16(item,0));}returnushortList.ToArray();}

逐问解答:为什么要定义2个byte相关容器?为什么不能直接用List接?

问题1:为什么先定义byte[] textBytes,再用byteList.AddRange(textBytes),而不是直接用List接sendText?

核心原因Encoding.ASCII.GetBytes(string)这个方法的返回值类型是固定的byte[],它没有提供直接返回List<byte>的重载方法!

  • 你想把字符串转成ASCII字节,只能先得到固定长度的byte数组,再通过AddRange把数组里的所有字节添加到List<byte>中;
  • 这是C#类库的方法设计限制,不是List的问题,List本身可以接收任意字节,但转ASCII的方法只给数组,必须做这一步转换。
问题2:为什么一定要再包一层List<byte> byteList?核心就是你说的动态扩容

这个方法的最终目的是把字节转成ushort[],而1个ushort必须占2个字节(Modbus协议要求:写入保持寄存器的ushort是16位,2字节),所以必须保证字节总数是偶数

  • 如果sendText转成字节后是奇数长度(比如3个字节),直接转ushort会少1个字节,程序报错;
  • List<byte>动态Add方法可以轻松实现补0操作byteList.Add(0x00)),而如果直接用byte[](固定长度),补0需要重新创建新数组、复制旧数据,代码会非常繁琐。
问题3:如果不用List,直接用byte[]会怎么样?(给你写对比代码,看差距)

如果硬要用固定数组实现,代码会变成这样,繁琐且易出错:

// 不用List的糟糕写法,对比你的代码privateushort[]GetUshortData_Bad(stringsendText){byte[]textBytes=Encoding.ASCII.GetBytes(sendText);// 第一步:计算新长度,奇数补1intnewLen=textBytes.Length%2==0?textBytes.Length:textBytes.Length+1;// 第二步:创建新数组,复制旧数据byte[]tempBytes=newbyte[newLen];Array.Copy(textBytes,tempBytes,textBytes.Length);// 第三步:奇数的话最后一位补0(数组默认0,可省略,但逻辑要写清)if(textBytes.Length%2==1)tempBytes[newLen-1]=0x00;// 后续转ushort逻辑不变...List<ushort>ushortList=newList<ushort>();for(inti=0;i<tempBytes.Length;i+=2){byte[]item=newbyte[2];item[0]=tempBytes[i+1];item[1]=tempBytes[i];ushortList.Add(BitConverter.ToUInt16(item,0));}returnushortList.ToArray();}

对比你的代码,用List少了计算新长度、创建新数组、数组复制三步,代码简洁至少50%,这就是List<byte>动态扩容的核心价值!

再解答你隐含的疑问:循环里的byte[] item=new byte[2]是干嘛的?

这不是多余的,是Modbus协议的字节序要求(大端序),和List无关,顺带讲透:

  • BitConverter.ToUInt16在Windows系统中默认是小端序(低字节在前,高字节在后);
  • Modbus RTU协议要求大端序(高字节在前,低字节在后);
  • 所以要创建2字节的临时数组,把byteList[i+1](高字节)放item[0],byteList[i](低字节)放item[1],实现字节序反转,保证Modbus设备能正确解析数据。

核心总结(完全基于你的代码+问题)

  1. 定义2个byte相关容器(byte[]+List<byte>不是多余的
    • byte[] textBytes:因为Encoding.ASCII.GetBytes只能返回固定数组,是方法限制;
    • List<byte> byteList:利用动态扩容,轻松实现奇数长度补0,避免固定数组的繁琐操作;
  2. 你的理解完全正确:泛型List的动态更新长度是这一步的核心原因;
  3. 不是不能直接用List接sendText,而是转ASCII的方法不支持直接返回List,必须先转数组再入List。

简单说,这一步的写法是C#类库限制+Modbus协议要求下的最优解,既解决了字节补0的问题,又让代码最简洁。

bitConverter

先给你核心结论(直击问题)

  1. BitConverter.ToUInt16C#内置静态方法,作用就是把字节(Byte)数组转成16位无符号整数ushort(即UInt16),你的理解完全正确;
  2. Byte ≠ Bit:1个Byte(字节)= 8个Bit(比特/位),代码里操作的是字节数组,不是比特;
  3. 没有ByteConverter:因为BitConverter是C#官方命名,它的核心是按“二进制位(Bit)”解析字节(Byte),覆盖所有基础类型的字节转值,无需单独做ByteConverter

逐问讲透,全程贴合你的代码场景

问题1:BitConverter.ToUInt16到底转的是Byte还是Bit?

转的是Byte数组,方法名带Bit是因为底层按二进制位解析
你的代码里:

BitConverter.ToUInt16(item,0);
  • 入参item2个Byte的数组(16个Bit,刚好对应1个UInt16);
  • 方法底层会把这2个Byte的16个二进制位按顺序拼接,转换成对应的UInt16数值;
  • 命名为BitConverter,是因为它的核心是操作二进制位,而不是直接操作Byte,Byte只是承载二进制位的容器。
问题2:Byte和Bit的核心区别(代码里用的是Byte,别搞混)
名称缩写大小代码里的类型/使用
比特Bit1个二进制位底层解析,代码不直接操作
字节Byte8个Bit代码里的byte[]/List<byte>,Modbus通信的基本单位
✅ 你的代码里全程操作的是Byte:字符串转byte[]List<byte>补0、itembyte[],Modbus RTU协议的通信单位也是字节,Bit只是底层最小单位。
问题3:为什么C#没有ByteConverter,只有BitConverter

因为BitConverter通用型工具类,覆盖了所有基础类型的「字节数组↔数值」转换,功能远大于单独的ByteConverter,官方没必要重复造轮子:

// BitConverter支持所有基础类型,你的代码只用了ToUInt16BitConverter.ToUInt16(byte[]value,intstartIndex);// 字节转UInt16/ushortBitConverter.ToInt32(byte[]value,intstartIndex);// 字节转Int32/intBitConverter.ToDouble(byte[]value,intstartIndex);// 字节转Double/doubleBitConverter.GetBytes(intvalue);// 数值转字节数组(反向)// ... 还有bool/long/float等所有类型

如果做ByteConverter,只能处理字节相关转换,功能单一;而BitConverter二进制位为核心,能处理所有基于字节的类型转换,是更通用的设计。

问题4:结合你的代码,再看BitConverter.ToUInt16的实际作用

你的代码里,item2个Byte的大端序数组,调用该方法后:

  1. 方法读取item[0]item[1]16个二进制位
  2. 按Windows小端序规则拼接成二进制数;
  3. 转换成对应的十进制ushort数值;
  4. 最终存入ushortList,传给Modbus的WriteMultipleRegistersAsync方法(要求入参是ushort[])。

极简总结(记死这3点就够)

  1. BitConverter.ToUInt16是C#内置方法,把2个Byte的数组转成UInt16/ushort,你的代码里用它完全匹配Modbus的参数要求;
  2. Byte≠Bit:1Byte=8Bit,代码里/Modbus里操作的都是Byte,Bit是底层解析单位;
  3. ByteConverter:因为BitConverter通用型字节转值工具,覆盖所有基础类型,官方统一命名和设计,无需单独的字节转换器。

简单说:BitConverter是“大而全”的工具,你的代码只是用了它其中一个功能(Byte转UInt16),这也是C#官方的标准设计思路~

大小端

你的总结100%精准
大端序的核心就是:高字节在前,低字节在后(存/传的时候,高位字节放数组/协议帧的低索引位,低位字节放高索引位)。

结合你代码里的item数组再强化下,更贴合你的实际使用场景:

byte[]item=newbyte[2];item[0]=byteList[i+1];// 高字节 → 放数组【第0位(前)】item[1]=byteList[i];// 低字节 → 放数组【第1位(后)】

这行代码就是纯纯的大端序实现,完全匹配Modbus RTU协议的字节序要求。

补充个小对比,帮你记死:

  • 大端序(Modbus用):高字节→数组0位,低字节→数组1位(高前低后)
  • 小端序(Windows默认):低字节→数组0位,高字节→数组1位(低前高后)

你这一句就抓到了大端序的本质,完全不用再记复杂概念了!
你的理解是对的,0x00就是在byteList末尾追加一个值为0的字节,不会报错,反而这步是必须的容错处理,核心是为了适配ushort双字节的转换要求,我结合你的代码把这步的逻辑、作用、为什么加0x00讲透:

byteList.Add(0x00)

1. 先明确:byteList.Add(0x00);到底做了什么?

0x00十六进制表示的0字节,和十进制的0、字节型的(byte)0完全等价,这行代码就是:

sendText转成的字节数是奇数时,在byteList的最后,追加一个值为0的空字节,让整个字节列表的长度变成偶数

它是纯纯的补位操作,不会修改原有字节,也不会触发任何报错,是安全的容错处理。

2. 为什么必须加这个0x00?核心原因:ushort2字节固定长度

你的最终目标是把字节转成ushort[]传给Modbus,而1个ushort(UInt16)必须由2个字节组成,这是C#的类型规定,也是Modbus协议的要求:

  • 如果字节数是偶数(2/4/6…):可以刚好分成N组2字节,完美转成N个ushort,无需补位;
  • 如果字节数是奇数(1/3/5…):最后会剩1个孤立字节,无法组成ushort,直接转换会索引越界报错
举个你的代码场景的例子:
  • 场景1:发送字符串"AB"→转ASCII字节[0x41,0x42](2个,偶数)→不用补0,直接转1个ushort;
  • 场景2:发送字符串"A"→转ASCII字节[0x41](1个,奇数)→必须补0,变成[0x41,0x00](2个,偶数),才能转1个ushort,否则循环i+=2时会超出数组长度,程序崩溃。

3. 为什么补的是0x00,而不是其他数字?

0x00(空字节)是工业通信的通用标准,原因有2个:

  1. 不影响原始数据:0字节是“无意义补位”,Modbus设备解析时,会识别出这是补的空字节,不会把它当成有效数据,能正确还原原始的字符串;
  2. ASCII编码的兼容性:ASCII编码中,0x00空字符(NUL),不属于任何可打印字符,不会和原始字符串的ASCII字节冲突,是最安全的补位值。

4. 结合你的代码,看补0的完整逻辑

byte[]textBytes=Encoding.ASCII.GetBytes(sendText);byteList.AddRange(textBytes);// 仅当字节数为奇数时,补0x00,让长度变偶数if(textBytes.Length%2==1){byteList.Add(0x00);// 追加0字节,无报错,纯补位}// 后续循环i+=2,按2字节一组转ushort,永远不会越界

核心总结(一句话讲透)

byteList.Add(0x00);不会报错,反而能避免报错
它是在字节数为奇数时,追加一个0值字节做补位,让字节总数变成偶数,保证后续能按“2字节一组”顺利转成ushort,是适配C#类型和Modbus协议的必要容错步骤,补的0x00就是十六进制的0字节,是工业通信中最安全的补位值。

反向验证:如果注释掉这行代码会怎样?

你可以试一下,当发送奇数长度的字符串(如"A"/"123")时,程序会直接抛出**IndexOutOfRangeException(索引越界异常),因为循环到最后会只剩1个字节,无法取到i+1的位置,这就是这行补0代码的核心价值。
你的疑问特别关键,核心是
「界面只显示1个Label,但数据在Modbus侧是按「寄存器」拆分存储的,所以会占用多个寄存器」,和Label数量无关,只和你发送的字符串转成的ushort[]长度有关**,我基于你的代码+Modbus规则讲透,一看就懂:

寄存器 与label

先定核心规则(Modbus侧的硬性要求,和界面无关)

Modbus的保持寄存器是**16位(2字节)**为1个单位,1个ushort刚好对应1个寄存器——你代码里把字符串转成了ushort[]数组,数组有多少个元素,就会占用Modbus多少个寄存器,这就是“写多个寄存器”的原因。

结合你的代码,拆解「1个Label显示 → 多个寄存器存储」的逻辑

步骤1:界面侧——1个Label只负责「最终显示字符串」,不管底层存储

你界面上的lblShowMsg.Text = sendText;只是把原始字符串展示出来,它是“结果呈现”,只认字符串,不管这个字符串在Modbus侧被拆成了多少份,所以看起来只是“1个内容”。

步骤2:Modbus侧——字符串必须拆成ushort[],自然占用多个寄存器

你的GetUshortData方法会把字符串转成ushort[]数组长度由字符串的字节数决定(补0后偶数字节,2字节=1个ushort),举3个实际例子(最直观):

你输入的字符串转ASCII字节数补0后字节数转成ushort[]长度占用Modbus寄存器数量写入的寄存器地址(起始8)
“A”1211个8号
“AB”2211个8号
“ABC”3422个8号、9号
“ABCD”4422个8号、9号

✅ 结论:不是Label多,是字符串转成的ushort[]有多个元素,所以必须写「多个」寄存器;哪怕只占1个寄存器,代码也用WriteMultipleRegistersAsync(写多寄存器方法),因为这个方法兼容「1个/多个」寄存器写入,不用单独区分,是开发的便捷写法。

再解答:为什么不用「写单寄存器方法」,非要用「写多寄存器方法」?

你代码里用WriteMultipleRegistersAsync(功能码0x10)而不用WriteSingleRegisterAsync(功能码0x06),核心是兼容性

  • 写单寄存器方法:只能写1个ushort(1个寄存器),如果字符串转成的ushort[]长度>1(比如"ABC"转成2个ushort),就会报错,无法发送;
  • 写多寄存器方法:兼容1个/多个ushort写入,不管字符串多长,转成的ushort[]有多少元素,都能一次性写入对应的寄存器,不用写判断逻辑,代码更简洁。

简单说:用写多寄存器方法,一个方法适配所有字符串长度的发送场景,不用区分“1个寄存器”还是“多个寄存器”。

最后梳理完整逻辑(界面1个Label ↔ Modbus多个寄存器)

  1. 你在txtSendMsg输入任意字符串 → 界面只展示1个输入框、1个显示Label;
  2. 代码把字符串转成byte[]→补0成偶数字节→转成ushort[](N个元素);
  3. 调用写多寄存器方法,把N个ushort一次性写入Modbus从8号开始的N个寄存器
  4. Modbus设备接收后,从对应寄存器中读取所有ushort→反向解析成byte[]→转成原始字符串;
  5. 你界面的lblShowMsg直接显示原始字符串,完全不用管Modbus侧拆成了多少寄存器。

极简总结

  1. Label数量≠寄存器数量:Label只是界面的“结果展示”,只认字符串;寄存器是Modbus的“数据存储单位”,按2字节/个拆分数据,和界面无关;
  2. 写多寄存器的原因:字符串转成的ushort[]可能有多个元素,1个ushort对应1个寄存器,自然会占用多个寄存器;
  3. 代码用写多寄存器方法是为了兼容:不管字符串多长,都能一次性写入,不用区分单/多寄存器,代码更简洁。

简单说:界面看的是“整体的字符串”,Modbus看的是“拆分后的ushort数据块”,这就是为什么界面只有1个Label,却要写多个寄存器的核心原因!

http://www.jsqmd.com/news/491536/

相关文章:

  • 大模型连载8:词向量如何表示近义词?
  • VScode快捷键
  • 2026年国标减压阀厂家推荐:煤气阀/液化气减压阀/大火力减压阀/汀鸥减压阀供应企业推荐 - 品牌推荐官
  • 2026年锅炉除氧器厂家专业选型参考:真空/大气式/热力/余热锅炉除氧器供应商推荐 - 品牌推荐官
  • 在idea中配置maven本地仓库
  • 239 滑动窗口最大值
  • gpt-claude-gemini稳定api代理
  • 进程间通信(IPC)- 管道专题学习笔记
  • 小白从零开始勇闯人工智能:LangChain 入门指南(下)
  • GoChatIAI -Go语言AI应用服务平台(3)
  • 酒店地毯供应商实力评测及选购指南 - 优质品牌商家
  • 6款思维导图软件深度评测:协作、AI能力与工具选型对比
  • 基于javaweb和mysql的ssm酒吧后台管理系统(java+ssm+jsp+html+mysql)
  • ToB/ToC 前端开发:程序员选赛道必看!从业务本质到技术选型,避开高频坑
  • 2026大型集团资产管理系统选型指南:五大主流平台深度解析与推荐 - 品牌2026
  • 计算机毕业设计之springboot疫情背景下光明小区管理系统的设计与实现
  • 国产替代:福尔蒂vs利安隆/金发/普立万在阻燃PC母粒的技术代差与应用边界
  • buuctf BabyUpload
  • 以太坊 vs Polkadot 预编译合约对比 | 同样的入口,完全不同的能力边界
  • 题目2281:蓝桥杯2018年第九届真题-次数差
  • Windows 10系统盘制作(纯净版)
  • 具身智能中的VLA基础概念
  • 【Spring框架】别再死记硬背!AOP 原来这么简单
  • 回归实战2
  • 一次试样失败催生的技术革新:福尔蒂吹瓶专用ACR助剂逆向推演与流变拟合
  • 半监督食物图像分类项目
  • 国内首个,面向中小企业数据资产估值体系:“荟宸信科面向中小企业数据资产估值体系”正式发布(一)
  • iPhone开发 - %1$、%2$的写法
  • 就让我们从react的渲染逻辑出发吧
  • WordPress报错:preg_match() Compilation failed 错误解决方法