首页/新闻资讯/正文详情

C#-WinForm-485型多功能空气质量变送器

发布时间:2026/9/27 10:50:10 来源:云帆数科 栏目:资讯中心
C#-WinForm-485型多功能空气质量变送器
目录一、设备二、设备物理连接三、连接测试四、 名词术语解释4.1 Modbus是什么4.2 四张数据表4.3 寄存器地址4.4 功能码Function Code4.5 CRC 校验4.6 字节序大端序4.7 校准值4.8 报文解析五、代码未使用NModbus4库5.1 Winform5.2 App.config5.3 核心代码5.3.1 连接串口5.3.2 发送读取请求整体流程5.3.3 接收5.3.4 实时接收5.3.5 校准5.4 完整代码六、结果官方文档http://save.sdvms.com/%E4%BA%A7%E5%93%81%E8%AF%B4%E6%98%8E%E4%B9%A6/%E6%B0%94%E8%B1%A1/%E5%A4%9A%E5%8A%9F%E8%83%BD%E7%A9%BA%E6%B0%94%E8%B4%A8%E9%87%8F/%E5%A8%81%E7%9B%9F%E5%A3%AB%E5%A4%9A%E5%8A%9F%E8%83%BD%E7%A9%BA%E6%B0%94%E8%B4%A8%E9%87%8F%E5%8F%98%E9%80%81%E5%99%A8%EF%BC%88485%E5%9E%8B%EF%BC%89.pdf多功能空气质量传感器 - 气象环境 - 山东威盟士科技有限公司一、设备设备1厂商威盟士型号SN-3001-MG111-N01设备名485型多功能空气质量变送器设备2USB转485模块其他485端子x2打印线二、设备物理连接打印线连电脑USB三、连接测试非必要步骤此时说明设备和电脑已联通四、 名词术语解释4.1 Modbus是什么Modbus 是一种主从式通信协议用于工业设备之间的数据交换。主站Master / Client主动发起请求比如你写的上位机、PLC。从站Slave / Server被动应答比如传感器、变送器、仪表。一主多从一个主站可以挂多个从站靠从站地址区分。通信流程主站问 → 从站答。从站从不主动说话。4.2 四张数据表从站内部的数据被抽象成四张表|名称|英文|读写|状态|地址区|用途| |-|-|-|-|-|-| |线圈|Coils|读写|0/1|0x|控制输出继电器、开关| |离散输入|Discrete Inputs|只读|0/1|1x|采集状态按钮、限位| |输入寄存器|Input Registers|只读|16位|3x|采集数据温度、电压| |保持寄存器|Holding Registers|读写|16位|4x|设置参数阈值、校准|4.3 寄存器地址16 位范围0x0000 ~ 0xFFFF (0 - 65535)一个寄存器存 16 位数据2 字节一个 32 位数据需要两个连续寄存器映射关系text线圈 00001 → 报文地址 0x0000 离散输入 10001 → 报文地址 0x0000 输入寄存器30001 → 报文地址 0x0000 保持寄存器40001 → 报文地址 0x00004.4 功能码Function Code功能码告诉从站我要做什么。|功能码|十六进制|名称|作用| |-|-|-|-| |01|0x01|Read Coils|读线圈| |02|0x02|Read Discrete Inputs|读离散输入| |03|0x03|Read Holding Registers|读保持寄存器| |04|0x04|Read Input Registers|读输入寄存器| |05|0x05|Write Single Coil|写单个线圈| |06|0x06|Write Single Register|写单个保持寄存器| |15|0x0F|Write Multiple Coils|写多个线圈| |16|0x10|Write Multiple Registers|写多个保持寄存器|例写校准值时使用“写单个保持寄存器”4.5 CRC 校验循环冗余校验2 字节附加在报文末尾。发送方算一次接收方重算一次不一致就丢弃。Modbus RTU 用CRC-16/MODBUS初始值0xFFFF多项式0xA001低字节在前。通用CRC校验代码示例//只要是ModbusRTU协议的CRC都可以使用这套代码 //CRC出现的根本目的是为了校验数据避免丢包或是干扰 private byte[] Crc16(byte[] data) { ushort crc16 0xFFFF; //初始化一个16位的无符号整数变量初始值0xFFFF for (int i 0; i data.Length; i) { crc16 ^ (ushort)data[i]; //异或操作目的是将crc16值与当前遍历的数据字节进行按位异或达到数据混合校验计算的目的第一步 for (int j 0; j 8; j) //j 8一个字节有8bit00000000 { if ((crc16 0x0001) ! 0) //检查最低位取出来最后一位如果是1进入判断 { crc16 1; //右移一位 //0xA001ModbusRTU 协议中的特征常数(0x8005的位反转形式) crc16 ^ 0xA001; //异或生成目的是只有当右移的那一位是1时才进行异或 } else { crc16 1; //如果移除的那一位是0则右移但不进行异或操作 } } } //返回的结果是高低位交换达到crc验证的效果 return new byte[] { //crc校验的第一个字节需要是低位的 (byte)(crc16 0xFF), //取第8位0xFF已经把高8位清零了 (byte)(crc16 8) //取高8位 }; }4.6 字节序大端序Modbus 规定大端序高字节在前低字节在后。16 位数据0x1234发送时是12 34。4.7 校准值作用修正传感器的测量偏差。场景标准仪器显示 25.0℃但变送器测出来是 24.5℃。往校准寄存器写25.0设备就会调整内部计算逻辑。注意写入校准值时要按文档的扩大倍数换算。扩大 10 倍 → 输入 25.0实际写 250扩大 100 倍 → 输入 0.55实际写 554.8 报文解析十进制报文1 3 20 0 144 0 189 1 25 1 109 0 0 0 0 0 0 0 0 0 0 0 0 37 184|序号|十进制|十六进制|含义| |-|-|-|-| |1|1|0x01|从站地址| |2|3|0x03|功能码读保持寄存器| |3|20|0x14|后面数据字节数20 字节 10 个寄存器| |4~23|…|…|10 个寄存器的值每个 2 字节0是高位144是低位| |24|37|0x25|CRC 低字节| |25|184|0xB8|CRC 高字节|总长度3 20 2 25 字节。五、代码未使用NModbus4库5.1 Winform5.2 App.config配置串口需要的值方便直接获取或后期Winform拓展?xml version1.0 encodingutf-8 ? configuration startup supportedRuntime versionv4.0 sku.NETFramework,Versionv4.7.2 / /startup appSettings !--COM口写设备连接进来的口其他数据根据设备文档配置-- add keyPortName valueCOM8/ add keyBaudRate value9600/ add keyDataBits value8/ add keyParity valueNone/ add keyStopBits valueOne/ /appSettings /configuration5.3 核心代码5.3.1 连接串口//点击“连接”按钮后触发 private void button1_Click(object sender, EventArgs e) { if (button1.Text 连接) { //读取xml文件内容 _serialPort.PortName ConfigurationManager.AppSettings[PortName]; _serialPort.BaudRate int.Parse(ConfigurationManager.AppSettings[BaudRate]); _serialPort.DataBits int.Parse(ConfigurationManager.AppSettings[DataBits]); _serialPort.Parity (Parity)Enum.Parse(typeof(Parity), ConfigurationManager.AppSettings[Parity]); _serialPort.StopBits (StopBits)Enum.Parse(typeof(StopBits), ConfigurationManager.AppSettings[StopBits]); try { //启动串口 _serialPort.Open(); button1.Text 断开; } catch (Exception ex) { MessageBox.Show(${ex.Message}); } } else { try { _serialPort?.Close(); button1.Text 连接; } catch (Exception) { } } }5.3.2 发送读取请求整体流程做这步之前请确保对端设备也成功连接打印线的usb端插电脑上了并且设备通电1、点击“读取”按钮触发//点击读取按钮后触发 private void button8_Click(object sender, EventArgs e) { if (!_serialPort.IsOpen) { MessageBox.Show(未连接串口); return; } //自定义方法用于发送读取请求 //0x00从寄存器地址0x00开始读 //10读取10个寄存器 Read(0x00, 10); }2、向对端设备发送读取请求/// summary /// 向设备发送读取请求 /// /summary /// param nameregisterAddress读取的起始16进制寄存器地址/param /// param namenum读取的寄存器数量/param private void Read(ushort registerAddress, ushort num) { //数据的读取是需要根据文档上的标注来的 //ReadingData自定义方法用于拼接要发送的请求内容 //1地址码(根据文档获取) //3功能码(根据文档获取) byte[] _data ReadingData(1, 3, registerAddress, num); //发送请求 _serialPort.Write(_data, 0, _data.Length); }3、拼接要发送的请求/// summary /// 拼接要发送的请求内容根据文档此设备的问询帧共有8字节。 /// 分别为地址码1 功能码1 寄存器起始地址2 寄存器长度2 校验码低位1 校验码高位1 /// /summary /// param nameslaveAddress地址码一般为1/param /// param namefunction功能码此设备为“读保持寄存器”功能码为3/param /// param nameregisterAddress寄存器起始地址/param /// param namenumber读取的寄存器长度数量/param /// returns已经拼接好的byte[]长度为8/returns private byte[] ReadingData(byte slaveAddress, byte function, ushort registerAddress, ushort number) { //先创建一个长度为6的byte数组CRC校验需要有前六位数组后才可以计算 byte[] data1 new byte[6]; //分别为地址码 功能码 寄存器起始地址(高位) 寄存器起始地址(低位) 寄存器长度(高位) 寄存器长度(低位) data1[0] slaveAddress; data1[1] function; data1[2] Convert.ToByte(registerAddress 8 255); data1[3] Convert.ToByte(registerAddress 255); data1[4] Convert.ToByte(number 8 255); data1[5] Convert.ToByte(number 255); //发送不包含CRC校验的byte数组 byte[] crc Crc16(data1); //创建一个完整的数组长度为8 byte[] data new byte[8]; //将两个数组拼接 Array.Copy(data1, 0, data, 0, 6); Array.Copy(crc, 0, data, 6, 2); return data; }4、CRC校验/// summary /// CRC校验算法 /// /summary /// param namedata前六位的内容/param /// returns已经拼接好的crcbyte[]长度为2分别为校验码低位 和 校验码高位/returns private byte[] Crc16(byte[] data) { ushort crc16 0xFFFF; for (int i 0; i data.Length; i) { crc16 ^ (ushort)data[i]; for (int j 0; j 8; j) { if ((crc16 0x0001) ! 0) { crc16 1; crc16 ^ 0xA001; } else { crc16 1; } } } return new byte[] { (byte)(crc16 0xFF), (byte)(crc16 8) }; }发送请求整体流程//点击读取按钮后触发 private void button8_Click(object sender, EventArgs e) { if (!_serialPort.IsOpen) { MessageBox.Show(未连接串口); return; } //自定义方法用于发送读取请求 //0x00从寄存器地址0x00开始读 //10读取10个寄存器 Read(0x00, 10); } #region 固定结构 /// summary /// 向设备发送读取请求 /// /summary /// param nameregisterAddress读取的起始16进制寄存器地址/param /// param namenum读取的寄存器数量/param private void Read(ushort registerAddress, ushort num) { //数据的读取是需要根据文档上的标注来的 //ReadingData自定义方法用于拼接要发送的请求内容 //1地址码(根据文档获取) //3功能码(根据文档获取) byte[] _data ReadingData(1, 3, registerAddress, num); //发送请求 _serialPort.Write(_data, 0, _data.Length); } /// summary /// 拼接要发送的请求内容根据文档此设备的问询帧共有8字节。 /// 分别为地址码1 功能码1 寄存器起始地址2 寄存器长度2 校验码低位1 校验码高位1 /// /summary /// param nameslaveAddress地址码一般为1/param /// param namefunction功能码此设备为“读保持寄存器”功能码为3/param /// param nameregisterAddress寄存器起始地址/param /// param namenumber读取的寄存器长度数量/param /// returns已经拼接好的byte[]长度为8/returns private byte[] ReadingData(byte slaveAddress, byte function, ushort registerAddress, ushort number) { //先创建一个长度为6的byte数组CRC校验需要有前六位数组后才可以计算 byte[] data1 new byte[6]; //分别为地址码 功能码 寄存器起始地址(高位) 寄存器起始地址(低位) 寄存器长度(高位) 寄存器长度(低位) data1[0] slaveAddress; data1[1] function; data1[2] Convert.ToByte(registerAddress 8 255); data1[3] Convert.ToByte(registerAddress 255); data1[4] Convert.ToByte(number 8 255); data1[5] Convert.ToByte(number 255); //发送不包含CRC校验的byte数组 byte[] crc Crc16(data1); //创建一个完整的数组长度为8 byte[] data new byte[8]; //将两个数组拼接 Array.Copy(data1, 0, data, 0, 6); Array.Copy(crc, 0, data, 6, 2); return data; } /// summary /// CRC校验算法 /// /summary /// param namedata前六位的内容/param /// returns已经拼接好的crcbyte[]长度为2分别为校验码低位 和 校验码高位/returns private byte[] Crc16(byte[] data) { ushort crc16 0xFFFF; for (int i 0; i data.Length; i) { crc16 ^ (ushort)data[i]; for (int j 0; j 8; j) { if ((crc16 0x0001) ! 0) { crc16 1; crc16 ^ 0xA001; } else { crc16 1; } } } return new byte[] { (byte)(crc16 0xFF), (byte)(crc16 8) }; } #endregion5.3.3 接收向接收事件添加方法当接收到数据时触发此方法public Form1() { InitializeComponent(); //向事件中添加方法当接收到数据时触发此事件 _serialPort.DataReceived _serialPort_DataReceived; }serialPortDataReceived方法//接收数据 private void _serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { //直接按照串口的读取业务逻辑即可 //当进入此方法时就说明一定接收到了数据所以可以直接根据_serialPort.BytesToRead确定byte[]的长度 byte[] buffer new byte[_serialPort.BytesToRead]; //读取操作 int num _serialPort.Read(buffer, 0, buffer.Length); if (num 0) return; //遍历输出调试 foreach (var item in buffer) { Console.Write(item ); } Console.WriteLine(); //简单判断总长度地址码 功能码 有效字节数 数据一区 第二数据区 第N数据区 校验码 //1 1 1 20 2 25 if (buffer.Length 25) { byte func buffer[1]; byte conut buffer[2]; //判断功能码 if (func 3) { //判断有效字节数是否缺失 if (conut 20) { //向textBox输出打印 Invoke(new Action(() { textBox2.Text ((buffer[3] * 256 buffer[4])).ToString(); textBox3.Text ((buffer[5] * 256 buffer[6])).ToString(); textBox4.Text ((buffer[7] * 256 buffer[8]) / 10).ToString(); textBox5.Text ((buffer[9] * 256 buffer[10]) / 10).ToString(); textBox6.Text ((buffer[19] * 256 buffer[20])).ToString(); textBox7.Text ((buffer[21] * 256 buffer[22]) / 100).ToString(); })); } } } }5.3.4 实时接收//实时 private void checkBox1_CheckedChanged(object sender, EventArgs e) { //判断实时按钮状态 if (checkBox1.Checked) { _cts new CancellationTokenSource(); _ SendMessage(_cts.Token); } else { _cts.Cancel(); _cts null; } } /// summary /// 实时获取发送方法 /// /summary /// param name_token/param /// returns/returns private async Task SendMessage(CancellationToken _token) { while (!_token.IsCancellationRequested _serialPort.IsOpen) { //发送 Read(0x00, 10); await Task.Delay(1000); } }5.3.5 校准//校准 //所有设置按钮都绑定这个事件 private void button_Click(object sender, EventArgs e) { if (!_serialPort.IsOpen) { MessageBox.Show(需要建立连接之后才能写入); return; } Button button (Button)sender; //根据button.Name判断谁点击的 switch (button.Name) { case btnPM25: Write(0x50, Convert.ToUInt16(textBox13.Text)); break; case btnPM10: Write(0x51, Convert.ToUInt16(textBox12.Text)); break; case btn湿度: Write(0x52, Convert.ToUInt16(double.Parse(textBox11.Text) * 10)); break; case btn温度: Write(0x53, Convert.ToUInt16(double.Parse(textBox10.Text) * 10)); break; case btn二氧化碳: Write(0x58, Convert.ToUInt16(textBox9.Text)); break; case btn甲醛: Write(0x59, Convert.ToUInt16(double.Parse(textBox8.Text) * 100)); break; default: break; } } /// summary /// 进行写入操作 /// /summary /// param nameregisterAddress寄存器地址根据官方文档/param /// param namevalue写入的值根据官方文档/param private void Write(ushort registerAddress, ushort value) { byte[] _data WritingData(1, 0x06, registerAddress, value); foreach (var item in _data) { Console.Write(item ); } Console.WriteLine(); _serialPort.Write(_data, 0, _data.Length); } // 构建写入报文功能码 06 //官方写入设备示例 //地址码 功能码 起始地址 写入数据区 校验码低字节 校验码高字节 //0x01 0x06 0x00 0x60 0x00 0x14 0x45 0xdB /// summary /// 拼接要发送的请求内容根据文档此设备的问询帧共有8字节。 /// 分别为地址码1 功能码1 寄存器地址2 寄存器长度2 校验码低位1 校验码高位1 /// /summary /// param nameslaveAddress地址码一般为1/param /// param namefunction功能码此设备为“写单个保持寄存器”功能码为6/param /// param nameregisterAddress寄存器地址/param /// param namevalue要写入的内容/param /// returns已经拼接好的byte[]长度为8/returns private byte[] WritingData(byte slaveAddress, byte function, ushort registerAddress, ushort value) { byte[] data1 new byte[6]; data1[0] slaveAddress; data1[1] function; // 0x06 data1[2] Convert.ToByte(registerAddress 8 255); data1[3] Convert.ToByte(registerAddress 255); data1[4] Convert.ToByte(value 8 255); data1[5] Convert.ToByte(value 255); byte[] crc Crc16(data1); byte[] data new byte[8]; Array.Copy(data1, 0, data, 0, 6); Array.Copy(crc, 0, data, 6, 2); return data; }5.4 完整代码using System; using System.Collections.Generic; using System.ComponentModel; using System.Configuration; using System.Data; using System.Drawing; using System.IO.Ports; using System.Linq; using System.Linq.Expressions; using System.Net.Sockets; using System.Runtime.InteropServices.ComTypes; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using System.Xml.Linq; using Modbus.Device; namespace _02_空气质量变送器数据采集练习 { public partial class Form1 : Form { SerialPort _serialPort new SerialPort(); CancellationTokenSource _cts new CancellationTokenSource(); public Form1() { InitializeComponent(); //向事件中添加方法当接收到数据时触发此事件 _serialPort.DataReceived _serialPort_DataReceived; } //点击“连接”按钮后触发 private void button1_Click(object sender, EventArgs e) { if (button1.Text 连接) { //读取xml文件内容 _serialPort.PortName ConfigurationManager.AppSettings[PortName]; _serialPort.BaudRate int.Parse(ConfigurationManager.AppSettings[BaudRate]); _serialPort.DataBits int.Parse(ConfigurationManager.AppSettings[DataBits]); _serialPort.Parity (Parity)Enum.Parse(typeof(Parity), ConfigurationManager.AppSettings[Parity]); _serialPort.StopBits (StopBits)Enum.Parse(typeof(StopBits), ConfigurationManager.AppSettings[StopBits]); try { //启动串口 _serialPort.Open(); button1.Text 断开; } catch (Exception ex) { MessageBox.Show(${ex.Message}); } } else { try { _serialPort?.Close(); button1.Text 连接; } catch (Exception) { } } } //点击读取按钮后触发 private void button8_Click(object sender, EventArgs e) { if (!_serialPort.IsOpen) { MessageBox.Show(未连接串口); return; } //自定义方法用于发送读取请求 //0x00从寄存器地址0x00开始读 //10读取10个寄存器 Read(0x00, 10); } #region 固定结构 /// summary /// 向设备发送读取请求 /// /summary /// param nameregisterAddress读取的起始16进制寄存器地址/param /// param namenum读取的寄存器数量/param private void Read(ushort registerAddress, ushort num) { //数据的读取是需要根据文档上的标注来的 //ReadingData自定义方法用于拼接要发送的请求内容 //1地址码(根据文档获取) //3功能码(根据文档获取) byte[] _data ReadingData(1, 3, registerAddress, num); //发送请求 _serialPort.Write(_data, 0, _data.Length); } /// summary /// 拼接要发送的请求内容根据文档此设备的问询帧共有8字节。 /// 分别为地址码1 功能码1 寄存器起始地址2 寄存器长度2 校验码低位1 校验码高位1 /// /summary /// param nameslaveAddress地址码一般为1/param /// param namefunction功能码此设备为“读保持寄存器”功能码为3/param /// param nameregisterAddress寄存器起始地址/param /// param namenumber读取的寄存器长度数量/param /// returns已经拼接好的byte[]长度为8/returns private byte[] ReadingData(byte slaveAddress, byte function, ushort registerAddress, ushort number) { //先创建一个长度为6的byte数组CRC校验需要有前六位数组后才可以计算 byte[] data1 new byte[6]; //分别为地址码 功能码 寄存器起始地址(高位) 寄存器起始地址(低位) 寄存器长度(高位) 寄存器长度(低位) data1[0] slaveAddress; data1[1] function; data1[2] Convert.ToByte(registerAddress 8 255); data1[3] Convert.ToByte(registerAddress 255); data1[4] Convert.ToByte(number 8 255); data1[5] Convert.ToByte(number 255); //发送不包含CRC校验的byte数组 byte[] crc Crc16(data1); //创建一个完整的数组长度为8 byte[] data new byte[8]; //将两个数组拼接 Array.Copy(data1, 0, data, 0, 6); Array.Copy(crc, 0, data, 6, 2); return data; } /// summary /// CRC校验算法 /// /summary /// param namedata前六位的内容/param /// returns已经拼接好的crcbyte[]长度为2分别为校验码低位 和 校验码高位/returns private byte[] Crc16(byte[] data) { ushort crc16 0xFFFF; for (int i 0; i data.Length; i) { crc16 ^ (ushort)data[i]; for (int j 0; j 8; j) { if ((crc16 0x0001) ! 0) { crc16 1; crc16 ^ 0xA001; } else { crc16 1; } } } return new byte[] { (byte)(crc16 0xFF), (byte)(crc16 8) }; } #endregion //接收数据 private void _serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { //直接按照串口的读取业务逻辑即可 //当进入此方法时就说明一定接收到了数据所以可以直接根据_serialPort.BytesToRead确定byte[]的长度 byte[] buffer new byte[_serialPort.BytesToRead]; //读取操作 int num _serialPort.Read(buffer, 0, buffer.Length); if (num 0) return; //遍历输出调试 foreach (var item in buffer) { Console.Write(item ); } Console.WriteLine(); //简单判断总长度地址码 功能码 有效字节数 数据一区 第二数据区 第N数据区 校验码 //1 1 1 20 2 25 if (buffer.Length 25) { byte func buffer[1]; byte conut buffer[2]; //判断功能码 if (func 3) { //判断有效字节数是否缺失 if (conut 20) { //向textBox输出打印 Invoke(new Action(() { textBox2.Text ((buffer[3] * 256 buffer[4])).ToString(); textBox3.Text ((buffer[5] * 256 buffer[6])).ToString(); textBox4.Text ((buffer[7] * 256 buffer[8]) / 10).ToString(); textBox5.Text ((buffer[9] * 256 buffer[10]) / 10).ToString(); textBox6.Text ((buffer[19] * 256 buffer[20])).ToString(); textBox7.Text ((buffer[21] * 256 buffer[22]) / 100).ToString(); })); } } } } //实时 private void checkBox1_CheckedChanged(object sender, EventArgs e) { //判断实时按钮状态 if (checkBox1.Checked) { _cts new CancellationTokenSource(); _ SendMessage(_cts.Token); } else { _cts.Cancel(); _cts null; } } /// summary /// 实时获取发送方法 /// /summary /// param name_token/param /// returns/returns private async Task SendMessage(CancellationToken _token) { while (!_token.IsCancellationRequested _serialPort.IsOpen) { //发送 Read(0x00, 10); await Task.Delay(1000); } } //校准 //所有设置按钮都绑定这个事件 private void button_Click(object sender, EventArgs e) { if (!_serialPort.IsOpen) { MessageBox.Show(需要建立连接之后才能写入); return; } Button button (Button)sender; //根据button.Name判断谁点击的 switch (button.Name) { case btnPM25: Write(0x50, Convert.ToUInt16(textBox13.Text)); break; case btnPM10: Write(0x51, Convert.ToUInt16(textBox12.Text)); break; case btn湿度: Write(0x52, Convert.ToUInt16(double.Parse(textBox11.Text) * 10)); break; case btn温度: Write(0x53, Convert.ToUInt16(double.Parse(textBox10.Text) * 10)); break; case btn二氧化碳: Write(0x58, Convert.ToUInt16(textBox9.Text)); break; case btn甲醛: Write(0x59, Convert.ToUInt16(double.Parse(textBox8.Text) * 100)); break; default: break; } } /// summary /// 进行写入操作 /// /summary /// param nameregisterAddress寄存器地址根据官方文档/param /// param namevalue写入的值根据官方文档/param private void Write(ushort registerAddress, ushort value) { byte[] _data WritingData(1, 0x06, registerAddress, value); foreach (var item in _data) { Console.Write(item ); } Console.WriteLine(); _serialPort.Write(_data, 0, _data.Length); } // 构建写入报文功能码 06 //官方写入设备示例 //地址码 功能码 起始地址 写入数据区 校验码低字节 校验码高字节 //0x01 0x06 0x00 0x60 0x00 0x14 0x45 0xdB /// summary /// 拼接要发送的请求内容根据文档此设备的问询帧共有8字节。 /// 分别为地址码1 功能码1 寄存器地址2 寄存器长度2 校验码低位1 校验码高位1 /// /summary /// param nameslaveAddress地址码一般为1/param /// param namefunction功能码此设备为“写单个保持寄存器”功能码为6/param /// param nameregisterAddress寄存器地址/param /// param namevalue要写入的内容/param /// returns已经拼接好的byte[]长度为8/returns private byte[] WritingData(byte slaveAddress, byte function, ushort registerAddress, ushort value) { byte[] data1 new byte[6]; data1[0] slaveAddress; data1[1] function; // 0x06 data1[2] Convert.ToByte(registerAddress 8 255); data1[3] Convert.ToByte(registerAddress 255); data1[4] Convert.ToByte(value 8 255); data1[5] Convert.ToByte(value 255); byte[] crc Crc16(data1); byte[] data new byte[8]; Array.Copy(data1, 0, data, 0, 6); Array.Copy(crc, 0, data, 6, 2); return data; } } }六、结果手头没有设备临时用了另一个设备出结果了就行效果一样只是值不对而已接收1 3 20 0 0 0 0 0 206 0 0 0 0 0 0 0 0 0 0 0 0 2 4 237 144解析1地址码3功能码20有效字节数数据一区0 0数据二区0 0数据三区0 206数据N区以此类推效验码237 144计算结果以2 4甲醛为例官方文档“扩大100倍上传”意思就是接收到的值“2 4”是被乘了100倍的所以需要除100倍才是原来的值因为2是高位所以需要乘256((2 * 256) 4) / 100 5如果需要结果更精确自行使用double类型即可

相关推荐

Operit 动态思考选项 Provider 能力映射:从供应商规则生成到请求参数写入的完整链路
Operit 动态思考选项 Provider 能力映射:从供应商规则生成到请求参数写入的完整链路

AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆 【免费下载链接】Operit The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent 项目地址: https://gitcode.com/gh_mirrors/o… · 2026/9/27 10:50:03

WordPress访问要10多秒?老手教你怎么选加速方案
WordPress访问要10多秒?老手教你怎么选加速方案

WordPress访问要10多秒?老手教你怎么选加速方案 昨天刚接到一个急单,客户老板急得跳脚,说他那个用WordPress做的官网,打开居然要等10多秒,还没加载完呢,访客全跑了。更惨的是,他之前为了省那点服务器钱,没做安全加固,结果网站… · 2026/9/27 10:50:03

Penrose Substance 索引语句(Indexed Statements)全解析:模板标识符、范围展开与 where 条件过滤
Penrose Substance 索引语句(Indexed Statements)全解析:模板标识符、范围展开与 where 条件过滤

开发工具数据可视化 【免费下载链接】penrose Create beautiful diagrams just by typing notation in plain text. 项目地址: https://gitcode.com/gh_mirrors/pe/penrose 点击查看 免费下载 索引语句(Indexed Statements)是 Penrose 的 Su… · 2026/9/27 10:49:57

Proteus 8.15安装教程:单片机仿真与Keil联合仿真从零搞定
Proteus 8.15安装教程:单片机仿真与Keil联合仿真从零搞定

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 20:46:06

网站建设和网袷宣传从零搭建实战避坑指南
网站建设和网袷宣传从零搭建实战避坑指南

网站建设和网袷宣传从零搭建实战避坑指南 网站上线三个月,后台流量个位数,转化率几乎为零? 这是很多老板和项目经理最头疼的噩梦。 别再怪推广费烧得慢,问题往往出在 从零搭建 的底层逻辑上。 很多人以为, 网站建设和网袷宣传 就是找个模板,把… · 2026/9/27 20:46:06

VS Code搭建51单片机开发环境:Keil C51编译与stcgal烧录实战
VS Code搭建51单片机开发环境:Keil C51编译与stcgal烧录实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 20:45:59

QT与FFmpeg视频播放器开发实战:架构、同步与跨平台避坑指南
QT与FFmpeg视频播放器开发实战:架构、同步与跨平台避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 20:45:59

DIY单级电磁炮制作指南:从电磁发射原理到实装调试全教程
DIY单级电磁炮制作指南:从电磁发射原理到实装调试全教程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 20:45:59

1.6T光模块量产前夜:上游产业链卡位与价值量拆解
1.6T光模块量产前夜:上游产业链卡位与价值量拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 20:45:59

MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现

简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01

汕头网站建设制作厂家避坑指南:5大注意事项救急
汕头网站建设制作厂家避坑指南:5大注意事项救急

汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01

多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习

简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01

MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现

简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01

汕头网站建设制作厂家避坑指南:5大注意事项救急
汕头网站建设制作厂家避坑指南:5大注意事项救急

汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01

多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习

简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码