[{"data":1,"prerenderedAt":1949},["ShallowReactive",2],{"page-\u002Fcsharp\u002Fmodbus通信协议":3},{"id":4,"title":5,"body":6,"description":1082,"extension":1943,"meta":1944,"navigation":1134,"path":1945,"seo":1946,"stem":1947,"__hash__":1948},"content\u002Fcsharp\u002Fmodbus通信协议.md","Modbus通信协议",{"type":7,"value":8,"toc":1919},"minimark",[9,14,22,25,28,41,48,54,58,61,72,87,90,109,125,131,143,146,156,160,165,211,214,239,244,248,253,267,270,284,301,316,333,337,340,343,376,405,410,433,438,441,455,461,464,467,477,480,486,489,618,621,855,859,866,869,872,875,878,881,884,890,892,895,898,901,904,907,910,913,919,925,931,937,942,945,956,963,969,972,977,984,989,995,1000,1003,1009,1015,1025,1032,1038,1048,1051,1057,1060,1069,1073,1076,1369,1383,1400,1405,1408,1470,1473,1538,1545,1582,1598,1634,1637,1643,1650,1656,1663,1724,1730,1733,1820,1826,1829,1861,1866,1869,1891,1903,1915],[10,11,13],"h2",{"id":12},"什么是modbus","什么是Modbus?",[15,16,17,21],"p",{},[18,19,20],"strong",{},"顾名思义","，它是一个Bus，即总线协议。比如串口协议、IIC协议、SPI都是通信协议。你接触到这种协议，相信你所处的行业是工业电子方面或者你的产品用于工业。",[15,23,24],{},"​\t好了，现在知道了大概知道了，这是一个总线协议，是一个Mod什么的公司发表的一个通信协议。那为什么要用这个呢? 答案就是他们都在用，你就得学",[15,26,27],{},"正经的说，它被工业领域所接受的原因是它具备一下三个优点",[29,30,31,35,38],"ul",{},[32,33,34],"li",{},"公开发表并且无版权要求",[32,36,37],{},"易于部署和维护",[32,39,40],{},"对供应商来说，修改移动本地的比特或字节没有很多限制",[15,42,43,44,47],{},"简单的概括,就是",[18,45,46],{},"免费+简单+方便修改","!",[15,49,50,51],{},"归纳：",[18,52,53],{},"Modbus就是一种用在工业上的简单协议!",[10,55,57],{"id":56},"modbus的内容是什么","Modbus的内容是什么?",[15,59,60],{},"​\t大致分为以下几种:",[29,62,63,66,69],{},[32,64,65],{},"Modbus-RTU",[32,67,68],{},"Modbus-ASCII",[32,70,71],{},"Modbus-TCP",[15,73,74,75,78,79,82,83,86],{},"​\t以上三种协议，一",[18,76,77],{},"个设备只会有一种协议","，如果你的设备使用的是",[18,80,81],{},"Modbus-RTU，只需查看以下对应部分","，一般来说大部分的设备都是",[18,84,85],{},"Modbus-RTU协议","的。",[10,88,89],{"id":89},"通讯过程",[15,91,92,93,96,97,100,101,104,105,108],{},"​\tModbus是",[18,94,95],{},"主从方式通信","，也就是说，不能",[18,98,99],{},"同步进行通信","，总线上",[18,102,103],{},"每次只有一个数据进行传输","，即主机发送，",[18,106,107],{},"从机应答","，主机不发送，总线上就没有数据通信。(所以说，这也算是一个缺点了)",[15,110,111,112,115,116,119,120,124],{},"​\t",[18,113,114],{},"举例1",": 一个总线上有一个主机，多个从机，主机查询其中一个从机，首先你必须得这些从机分配地址(这样才能知道哪个从机，而且每个地址必须唯一)，分配好地址后，主机要查询，然后数据下发(数据内容下面会介绍)，从机得到主机发送的数据,然后对应地址的从机回复，",[18,117,118],{},"主机得到从机数据，这样就是一个主机到从机的通信过程","，是不是很简单呢",[121,122],"img",{"alt":121,"src":123},"702204a8de3733d8aee40703ec00c7e6.png","?",[15,126,111,127,130],{},[18,128,129],{},"举例2",": 就像打电话，你得知道对方的电话(这就是唯一地址)，然后你打电话过去，相当于主机查找从机，然后对方接通电话，给你回复(返回数据)，正常是这样的。",[15,132,133,134,138,139,142],{},"​\t如果这时候，对方正在打电话，你应该听到的是\"",[135,136,137],"code",{},"sorry,you…","\"这一串英文，说明对方忙，但是",[135,140,141],{},"Modbus总线","不能判断对方是否忙，也没有对应的仲裁机制，好了你又知道了一个缺点了！但是，你可以用软件的办法进行适当的处理数据！",[10,144,85],{"id":145},"modbus-rtu协议",[15,147,111,148,151,152,155],{},[18,149,150],{},"设备必须要有RTU协议！这是Modbus协议上规定的，且默认模式必须是RTU，ASCII作为选项","。（也就是说，一般的设备只有RTU这个协议，ASCII一般很少）所以说，一般学习",[18,153,154],{},"Modbus协议，只需要了解RTU的协议","，ASCII作为学习的了解就足够了。",[157,158,159],"h3",{"id":159},"帧结构",[15,161,162],{},[18,163,164],{},"帧结构 = 地址 + 功能码+ 数据 + 校验",[29,166,167,181,191,201],{},[32,168,169,172,173,176,177,180],{},[18,170,171],{},"地址",": 占用一个字节，范围0-255，",[18,174,175],{},"其中有效范围是1-247","，其他有特殊用途，比如",[18,178,179],{},"255是广播地址(广播地址就是应答所有地址","，正常的需要两个设备的地址一样才能进行查询和回复)。",[32,182,183,186,187,190],{},[18,184,185],{},"功能码","：占用一个字节，功能码的意义就是，",[18,188,189],{},"知道这个指令是干啥的，比如你可以查询从机的数据，也可以修改数据","，所以不同功能码对应不同功能。可以类比一下RESTful的各种GET POST PUT...这样。",[32,192,193,196,197,200],{},[18,194,195],{},"数据","：根据",[18,198,199],{},"功能码不同，有不同结构，在下面的实例中有说明","。就像json xx-form一样。",[32,202,203,206,207,210],{},[18,204,205],{},"校验","：为了保证数据不错误，增加这个，然后",[18,208,209],{},"再把前面的数据进行计算看数据是否一致","，如果一致，就说明这帧数据是正确的，我再回复；如果不一样，说明你这个数据在传输的时候出了问题，数据不对的，所以就抛弃了。",[157,212,213],{"id":213},"实战",[15,215,216,217,220,221,224,225,228,229,231,232,235,236,238],{},"​\t只谈理论大家可能不太明白，下面举一个例子。Modbus-RTU协议一般我们用的最多功能码就是",[135,218,219],{},"03","和",[135,222,223],{},"06","，大部分都是用modbus来",[18,226,227],{},"查询传感器上的信息","用",[135,230,219],{},"查询功能码，如果需要",[18,233,234],{},"修改传感器寄存器","的值就用",[135,237,223],{},"修改功能码。",[15,240,241],{},[121,242],{"alt":121,"src":243},"135ce122ad96e8ac3c065f794581df14.png",[245,246,247],"h4",{"id":247},"回复数据解析",[15,249,250],{},[121,251],{"alt":121,"src":252},"48ff30b2a9213548522776f2f3ff3fde.png",[29,254,255,261],{},[32,256,257,260],{},[18,258,259],{},"发送","：从机的地址+我要干嘛的功能码+我要查的寄存器的地址+我要查的寄存器地址的个数+校验码",[32,262,263,266],{},[18,264,265],{},"回复","：从机的地址+主机发我的功能码+要发送给主机数据的字节数+数据+校验码",[10,268,269],{"id":269},"归纳",[15,271,272,273,276,277,276,280,283],{},"Modbus-RTU协议只需要看懂功能码",[135,274,275],{},"0x03","、",[135,278,279],{},"0x06",[135,281,282],{},"0x10","这三个基本的就已经足够了，分别回想下其数据域部分：",[15,285,286,287,290,291,294,295,290,298,300],{},"0x03—主机需要",[135,288,289],{},"发送起始地址","+",[135,292,293],{},"寄存器数量","，从机回复",[135,296,297],{},"总字节数",[135,299,195],{},"；",[15,302,303,304,290,307,310,311,290,313,315],{},"0x06—主机发送",[135,305,306],{},"起始地址",[135,308,309],{},"数据内容","（因为你只需要修改一个，所以起始地址就是所要修改的地址），从机返回",[135,312,306],{},[135,314,309],{},"（发现居然一样！）",[15,317,318,319,290,321,290,324,290,326,328,329,290,331],{},"0x10—主机发送",[135,320,306],{},[135,322,323],{},"寄存器个数",[135,325,297],{},[135,327,195],{},"，从机返回",[135,330,306],{},[135,332,293],{},[10,334,336],{"id":335},"modbus-acsii协议","Modbus-ACSII协议",[15,338,339],{},"​\t一般只需要了解RTU协议，因为前面有说过，必须要有RTU协议的，所以只需要了解了RTU协议，就可以读出设备信息了，至于ACSII协议，做个大概了解即可。",[157,341,342],{"id":342},"帧形式",[15,344,345,346,349,350,353,354,357,358,220,361,364,365,367,368,371,372,375],{},"​\t对于RTU协议，比如RTU发送一个字节：",[135,347,348],{},"0x12","；ASCII协议则需要发送2个字节：一个字节代表ASCII码",[135,351,352],{},"1","，一个代表ASCII码",[135,355,356],{},"2","，即",[135,359,360],{},"0x31",[135,362,363],{},"0x32","，才能代表",[135,366,348],{},"。所以，",[18,369,370],{},"ASCII协议的效率比较低","。但是",[18,373,374],{},"ASCII更符合串口打印查看","，因为串口发送的数据一般都是文本模式(ASCII)。",[15,377,378,379,382,383,385,386,389,390,393,394,220,397,400,401,404],{},"​\t比如用RTU方式，也叫16进制方式，要发0x03数据，RTU方式就发送",[135,380,381],{},"00000011","。用ASCII发送",[135,384,275],{},"，就要发送0的ASCII码",[135,387,388],{},"0x30","和3的ASCII码",[135,391,392],{},"0x33","，对应到2进制也就是发送",[135,395,396],{},"00110000",[135,398,399],{},"00110011","。很显然RTU方式只需要发送8位就可以了(加上起始位和停止位就是10位数据)。那么",[18,402,403],{},"ASCII码方式发送就需要两个8位(每个8位分别加上起始位和停止位就是20位数据)。也就是说ASCII码发送数据量是RTU方式的2倍，所以ASCII码效率更低","。",[15,406,407],{},[18,408,409],{},"那么ASCII码效率更低，数据发送量大为啥还采用这种方式呢？",[15,411,412,413,415,416,419,420,423,424,427,428,220,430,432],{},"​\t因为假如你要发送数据",[135,414,275],{},"，采用RTU方式(16进制发送)，",[18,417,418],{},"计算机中端设备接收到0x03后是不可以显示的","，就是不能把0x03打印出来。因为可见字符的ASCII码是从",[135,421,422],{},"32—126","，不是这个范围以外的显示屏上都看不到，会出现乱码，如果是串口助手的话就会显示",[135,425,426],{},"□□□□。","如果采用ASCII方式(文本模式发送)，就不会出现不可显示和乱码的情况，因为文本模式发送0x03，就是发送ASCII码0和ASCII码3，也就是",[135,429,388],{},[135,431,392],{},"，是可以正常显示在计算机中端的。所以现在知道为什么还要使用ASCII效率比较低的方式发送了吧，只是为了方便调试显示而已。",[15,434,435],{},[121,436],{"alt":121,"src":437},"d5e61973ca21adaa2711fe32a2262531.png",[15,439,440],{},"从上面的图中，看出：",[15,442,443,444,447,448,451,452],{},"1）比RTU多了起始段",[135,445,446],{},":","，多个结束符",[135,449,450],{},"CR","，",[135,453,454],{},"LF",[15,456,457,458,300],{},"2）地址和功能都变成了",[135,459,460],{},"2个字节",[15,462,463],{},"3）数据部分更加繁琐，但是更符合人们的查看；",[157,465,269],{"id":466},"归纳-1",[15,468,469,470,473,474,404],{},"​\t由于Modbus-RTU和Modbus-ACSII都是基于232和485链路的，",[18,471,472],{},"所以其通讯模式半双工","，一般是主机和从机的模式。其差别就是其字节的格式不同，一个是16进制的数据，一个是ASCII数据。ASCII多了帧头和帧尾，也就是说可以有用这个头尾判断一帧字节来判断是否结束；而RTU没有帧头和帧尾，所以协议里明确两帧之间要大于3.5个字节时间间隔，作为一帧结束的判断依据。",[18,475,476],{},"对于RS485来说，总线上一般允许最大32个设备",[10,478,479],{"id":479},"常见的功能码",[157,481,483],{"id":482},"modbus-四大数据区回顾",[18,484,485],{},"Modbus 四大数据区回顾",[15,487,488],{},"​\t首先，我们需要理解 Modbus 的世界主要围绕着四种基本的数据区（或称数据类型）构建。您的上位机程序就是通过不同的功能码来读写这些区域。",[490,491,492,514],"table",{},[493,494,495],"thead",{},[496,497,498,502,505,508,511],"tr",{},[499,500,501],"th",{},"数据区",[499,503,504],{},"寄存器地址范围",[499,506,507],{},"数据类型",[499,509,510],{},"可写性",[499,512,513],{},"通俗比喻",[515,516,517,544,569,594],"tbody",{},[496,518,519,525,530,533,538],{},[520,521,522],"td",{},[18,523,524],{},"线圈 (Coils)",[520,526,527],{},[135,528,529],{},"0xxxx",[520,531,532],{},"1-bit (开关量)",[520,534,535],{},[18,536,537],{},"可读可写",[520,539,540,541],{},"继电器、指示灯、电磁阀的",[18,542,543],{},"控制开关",[496,545,546,551,556,558,563],{},[520,547,548],{},[18,549,550],{},"离散输入 (Discrete Inputs)",[520,552,553],{},[135,554,555],{},"1xxxx",[520,557,532],{},[520,559,560],{},[18,561,562],{},"只读",[520,564,565,566],{},"物理按钮、传感器、行程开关的",[18,567,568],{},"状态指示灯",[496,570,571,576,581,584,588],{},[520,572,573],{},[18,574,575],{},"输入寄存器 (Input Registers)",[520,577,578],{},[135,579,580],{},"3xxxx",[520,582,583],{},"16-bit (模拟量)",[520,585,586],{},[18,587,562],{},[520,589,590,591],{},"温度\u002F压力传感器的",[18,592,593],{},"当前读数",[496,595,596,601,606,608,612],{},[520,597,598],{},[18,599,600],{},"保持寄存器 (Holding Registers)",[520,602,603],{},[135,604,605],{},"4xxxx",[520,607,583],{},[520,609,610],{},[18,611,537],{},[520,613,614,615],{},"温控器的",[18,616,617],{},"温度设定值",[15,619,620],{},"下面这张表包含了您在 99% 的工控应用开发中会遇到的核心功能码。",[490,622,623,641],{},[493,624,625],{},[496,626,627,629,632,635,638],{},[499,628,185],{},[499,630,631],{},"名称 (英文)",[499,633,634],{},"作用",[499,636,637],{},"操作对象",[499,639,640],{},"读\u002F写",[515,642,643,674,700,725,751,780,804,830],{},[496,644,645,657,660,667,669],{},[520,646,647,652,653,656],{},[18,648,649],{},[135,650,651],{},"01"," (",[135,654,655],{},"0x01",")",[520,658,659],{},"Read Coils",[520,661,662,663,666],{},"读取一个或多个",[18,664,665],{},"线圈","的状态",[520,668,524],{},[520,670,671],{},[18,672,673],{},"读",[496,675,676,686,689,694,696],{},[520,677,678,652,683,656],{},[18,679,680],{},[135,681,682],{},"02",[135,684,685],{},"0x02",[520,687,688],{},"Read Discrete Inputs",[520,690,662,691,666],{},[18,692,693],{},"离散输入",[520,695,693],{},[520,697,698],{},[18,699,673],{},[496,701,702,710,713,719,721],{},[520,703,704,652,708,656],{},[18,705,706],{},[135,707,219],{},[135,709,275],{},[520,711,712],{},"Read Holding Registers",[520,714,662,715,718],{},[18,716,717],{},"保持寄存器","的值",[520,720,717],{},[520,722,723],{},[18,724,673],{},[496,726,727,737,740,745,747],{},[520,728,729,652,734,656],{},[18,730,731],{},[135,732,733],{},"04",[135,735,736],{},"0x04",[520,738,739],{},"Read Input Registers",[520,741,662,742,718],{},[18,743,744],{},"输入寄存器",[520,746,744],{},[520,748,749],{},[18,750,673],{},[496,752,753,763,766,773,775],{},[520,754,755,652,760,656],{},[18,756,757],{},[135,758,759],{},"05",[135,761,762],{},"0x05",[520,764,765],{},"Write Single Coil",[520,767,768,769,772],{},"写入",[18,770,771],{},"单个线圈","的状态（置 ON 或 OFF）",[520,774,524],{},[520,776,777],{},[18,778,779],{},"写",[496,781,782,790,793,798,800],{},[520,783,784,652,788,656],{},[18,785,786],{},[135,787,223],{},[135,789,279],{},[520,791,792],{},"Write Single Holding Register",[520,794,768,795,718],{},[18,796,797],{},"单个保持寄存器",[520,799,717],{},[520,801,802],{},[18,803,779],{},[496,805,806,816,819,824,826],{},[520,807,808,652,813,656],{},[18,809,810],{},[135,811,812],{},"15",[135,814,815],{},"0x0F",[520,817,818],{},"Write Multiple Coils",[520,820,768,821,666],{},[18,822,823],{},"多个连续线圈",[520,825,524],{},[520,827,828],{},[18,829,779],{},[496,831,832,841,844,849,851],{},[520,833,834,652,839,656],{},[18,835,836],{},[135,837,838],{},"16",[135,840,282],{},[520,842,843],{},"Write Multiple Holding Registers",[520,845,768,846,718],{},[18,847,848],{},"多个连续保持寄存器",[520,850,717],{},[520,852,853],{},[18,854,779],{},[10,856,858],{"id":857},"modbus-tcp","Modbus TCP",[15,860,861,862,865],{},"　　Modbus TCP是",[18,863,864],{},"Modbus协议在以太网上的一种实现","，它保留了Modbus协议的核心功能和数据模型，但对消息封装进行了调整，使其符合TCP\u002FIP的要求。在进行下一步之前我们首先对Modbus中的一些概念做一个了解。",[15,867,868],{},"​\t存储区存储的数据类型分为布尔量和寄存器，布尔量如水阀的开关、灯的开关等数据。寄存器　如流速、导电率、温湿度、空气浓度等数据。",[15,870,871],{},"Modbus规定了4个存储区，分别是0、1、3、4，对应下面表格内容，具体为：",[15,873,874],{},"　　0区：可读可写的布尔量",[15,876,877],{},"　　1区：只读布尔量",[15,879,880],{},"　　3区：只读寄存器",[15,882,883],{},"　　4区：可读可写的寄存器",[15,885,886],{},[121,887],{"alt":888,"src":889},"图片","c573c8a00ebfdfc9f6de93d43705300e.png",[157,891,185],{"id":185},[15,893,894],{},"　　\tModbus定义了一系列的功能码，这些功能码表示了主站请求从站执行的具体操作，下面表格是一些常见的功能码，具体为：",[15,896,897],{},"　　1.4.1读操作",[15,899,900],{},"　　0x01：读输出线圈状态。",[15,902,903],{},"　　0x02：读输入线圈状态。",[15,905,906],{},"　　0x03：读输出寄存器的值。",[15,908,909],{},"　　0x04：读输入寄存器的值。",[15,911,912],{},"　　1.4.2写操作",[15,914,915,916,404],{},"　　0x05：写",[18,917,918],{},"单个线圈状态",[15,920,921,922,404],{},"　　0x06：写",[18,923,924],{},"单个寄存器的值",[15,926,927,928],{},"　　0x0F：写多个",[18,929,930],{},"线圈状态。",[15,932,933,934,404],{},"　　0x10：写多个",[18,935,936],{},"寄存器的值",[15,938,939],{},[121,940],{"alt":121,"src":941},"a8a8885036a248c381d54c25b1a12205.png",[15,943,944],{},"协议报文",[15,946,947,948,951,952,955],{},"　　Modbus TCP\u002FIP的报文被封装在一个",[18,949,950],{},"标准的TCP数据段内","，而",[18,953,954],{},"不是像串行通信那样直接在物理层上发送","。TCP报文提供了端到端的可靠传输，包括数据分段、重传、流量控制和拥塞控制等机制。",[15,957,958,959,962],{},"　　Modbus ",[18,960,961],{},"TCP\u002FIP的报文帧主要包括","：",[15,964,965,966],{},"　　MBAP头：包含",[18,967,968],{},"事务标识符、协议标识符、消息长度、设备地址",[15,970,971],{},"　　Modbus PDU：原始Modbus协议的数据部分，包括功能码、数据地址和数据值。",[15,973,974],{},[121,975],{"alt":888,"src":976},"ee4064594b7869da754fef0304bb2564.png",[15,978,979,980,983],{},"下面我们用一个表格来",[18,981,982],{},"根据一段报文解释这两部分的具体内容"," 主站发送报文：",[15,985,986],{},[121,987],{"alt":888,"src":988},"d26432f1e592c8bad5172713bc7ab747.png",[15,990,991,992,962],{},"从站",[18,993,994],{},"响应报文",[15,996,997],{},[121,998],{"alt":888,"src":999},"543dbc74f94c5895fe41fde9cd4083b2.png",[15,1001,1002],{},"　从上面的报文中我们可以知道：",[15,1004,1005,1006,404],{},"　　事务标识符：占2个字节，一次通信的过程中",[18,1007,1008],{},"主站和从站的报文帧的事务标识符是一致的",[15,1010,1011,1012,404],{},"　　协议标识符：占2个字节，00 00",[18,1013,1014],{},"表示Modbus TCP协议",[15,1016,1017,1018,1021,1022,404],{},"　　后面",[18,1019,1020],{},"报文的长度","：占2个字节，表示",[18,1023,1024],{},"后面报文的长度",[15,1026,1027,1028,1031],{},"　　从站地址：占1个字节，表示",[18,1029,1030],{},"设备的地址","，也就是Salve ID的值。",[15,1033,1034,1035,404],{},"　　功能码：占1个字节，例子中的03表示的是",[18,1036,1037],{},"读输出寄存器的值",[15,1039,1040,1041,1044,1045,404],{},"　　",[18,1042,1043],{},"起始","地址：占2个字节，表示",[18,1046,1047],{},"从该设备的哪个位置开始读",[15,1049,1050],{},"　　读取数量：占2个字节，表示从起始位置往后读的数量。",[15,1052,1053,1054,404],{},"　　长度：占2个字节，表示",[18,1055,1056],{},"后面还报文的长度",[15,1058,1059],{},"　　对应的值：根据不同的存储类型所占的字节不同。",[15,1061,1062,1063],{},"————————————————\n版权声明：本文为CSDN博主「上海研博数据」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。\n原文链接：",[1064,1065,1066],"a",{"href":1066,"rel":1067},"https:\u002F\u002Fblog.csdn.net\u002Fyanboshuju\u002Farticle\u002Fdetails\u002F142519763",[1068],"nofollow",[10,1070,1072],{"id":1071},"实战-1","实战：",[15,1074,1075],{},"​\t接下来我们尝试来写一个modbus tcp通信的从机。",[1077,1078,1083],"pre",{"className":1079,"code":1080,"language":1081,"meta":1082,"style":1082},"language-csharp shiki shiki-themes github-light github-dark","using NModbus;\nusing NModbus.Data;\nusing NModbus.Device;\nusing NModbus.Serial;\nusing System.IO.Ports;\nusing System.Net;\nusing System.Net.Sockets;\n\nnamespace modbus_slave;\n\nclass Program\n{\n    static async Task Main(string[] args)\n    {\n        \u002F\u002F --- 1. 定义从站和TCP参数 ---\n        byte slaveId = 1;\n        int port = 502; \u002F\u002F Modbus TCP 默认端口\n                        \u002F\u002F IPAddress.Any (0.0.0.0) 表示监听容器内所有可用的网络接口\n        var ipAddress = IPAddress.Any;\n\n        Console.WriteLine($\"准备在 {ipAddress}:{port} 上启动 Modbus TCP 从站 (ID: {slaveId})...\");\n\n        \u002F\u002F --- 2. 创建监听器 ---\n        var listener = new TcpListener(ipAddress, port);\n        listener.Start();\n\n        \u002F\u002F --- 3. 创建数据存储区 (与之前完全一样) ---\n        var dataStore = new SlaveDataStore();\n        dataStore.HoldingRegisters.WritePoints(1, new ushort[] { 10, 20, 30 }); \u002F\u002F 写入保持寄存器 (Holding Registers)。 这里是从地址1开始，也就是40001\n        dataStore.InputRegisters.WritePoints(1, new ushort[] { 100, 200, 300 }); \u002F\u002F 写入输入寄存器 (Input Registers)。从地址 1 开始写入数据，也就是30001这指定了我们要操作的是“线圈”区（地址范围 00001 起始），它是一种开关量（bool 类型）。这是可读可写的开关量数据区，通常用于控制继电器或指示灯。\n        dataStore.CoilDiscretes.WritePoints(1, new bool[] { true });\n\u002F\u002F 写入线圈 (Coils)。\n        Console.WriteLine(\"数据存储区已初始化。\");\n\n        \u002F\u002F --- 4. 创建 Modbus 从站网络 ---\n        var factory = new ModbusFactory();\n        var network = factory.CreateSlaveNetwork(listener);\n        var slave = factory.CreateSlave(slaveId, dataStore);\n        network.AddSlave(slave);\n\n        \u002F\u002F --- 5. 开始监听 ---\n        Console.ForegroundColor = ConsoleColor.Green;\n        Console.WriteLine($\"从站已在 TCP 端口 {port} 上开始监听... 按 Ctrl+C 退出。\");\n        Console.ResetColor();\n\n        await network.ListenAsync();\n    }\n}\n","csharp","",[135,1084,1085,1093,1099,1105,1111,1117,1123,1129,1136,1142,1147,1153,1159,1165,1171,1177,1183,1189,1195,1201,1206,1212,1217,1223,1229,1235,1240,1246,1252,1258,1264,1270,1276,1282,1287,1293,1299,1305,1311,1317,1322,1328,1334,1340,1346,1351,1357,1363],{"__ignoreMap":1082},[1086,1087,1090],"span",{"class":1088,"line":1089},"line",1,[1086,1091,1092],{},"using NModbus;\n",[1086,1094,1096],{"class":1088,"line":1095},2,[1086,1097,1098],{},"using NModbus.Data;\n",[1086,1100,1102],{"class":1088,"line":1101},3,[1086,1103,1104],{},"using NModbus.Device;\n",[1086,1106,1108],{"class":1088,"line":1107},4,[1086,1109,1110],{},"using NModbus.Serial;\n",[1086,1112,1114],{"class":1088,"line":1113},5,[1086,1115,1116],{},"using System.IO.Ports;\n",[1086,1118,1120],{"class":1088,"line":1119},6,[1086,1121,1122],{},"using System.Net;\n",[1086,1124,1126],{"class":1088,"line":1125},7,[1086,1127,1128],{},"using System.Net.Sockets;\n",[1086,1130,1132],{"class":1088,"line":1131},8,[1086,1133,1135],{"emptyLinePlaceholder":1134},true,"\n",[1086,1137,1139],{"class":1088,"line":1138},9,[1086,1140,1141],{},"namespace modbus_slave;\n",[1086,1143,1145],{"class":1088,"line":1144},10,[1086,1146,1135],{"emptyLinePlaceholder":1134},[1086,1148,1150],{"class":1088,"line":1149},11,[1086,1151,1152],{},"class Program\n",[1086,1154,1156],{"class":1088,"line":1155},12,[1086,1157,1158],{},"{\n",[1086,1160,1162],{"class":1088,"line":1161},13,[1086,1163,1164],{},"    static async Task Main(string[] args)\n",[1086,1166,1168],{"class":1088,"line":1167},14,[1086,1169,1170],{},"    {\n",[1086,1172,1174],{"class":1088,"line":1173},15,[1086,1175,1176],{},"        \u002F\u002F --- 1. 定义从站和TCP参数 ---\n",[1086,1178,1180],{"class":1088,"line":1179},16,[1086,1181,1182],{},"        byte slaveId = 1;\n",[1086,1184,1186],{"class":1088,"line":1185},17,[1086,1187,1188],{},"        int port = 502; \u002F\u002F Modbus TCP 默认端口\n",[1086,1190,1192],{"class":1088,"line":1191},18,[1086,1193,1194],{},"                        \u002F\u002F IPAddress.Any (0.0.0.0) 表示监听容器内所有可用的网络接口\n",[1086,1196,1198],{"class":1088,"line":1197},19,[1086,1199,1200],{},"        var ipAddress = IPAddress.Any;\n",[1086,1202,1204],{"class":1088,"line":1203},20,[1086,1205,1135],{"emptyLinePlaceholder":1134},[1086,1207,1209],{"class":1088,"line":1208},21,[1086,1210,1211],{},"        Console.WriteLine($\"准备在 {ipAddress}:{port} 上启动 Modbus TCP 从站 (ID: {slaveId})...\");\n",[1086,1213,1215],{"class":1088,"line":1214},22,[1086,1216,1135],{"emptyLinePlaceholder":1134},[1086,1218,1220],{"class":1088,"line":1219},23,[1086,1221,1222],{},"        \u002F\u002F --- 2. 创建监听器 ---\n",[1086,1224,1226],{"class":1088,"line":1225},24,[1086,1227,1228],{},"        var listener = new TcpListener(ipAddress, port);\n",[1086,1230,1232],{"class":1088,"line":1231},25,[1086,1233,1234],{},"        listener.Start();\n",[1086,1236,1238],{"class":1088,"line":1237},26,[1086,1239,1135],{"emptyLinePlaceholder":1134},[1086,1241,1243],{"class":1088,"line":1242},27,[1086,1244,1245],{},"        \u002F\u002F --- 3. 创建数据存储区 (与之前完全一样) ---\n",[1086,1247,1249],{"class":1088,"line":1248},28,[1086,1250,1251],{},"        var dataStore = new SlaveDataStore();\n",[1086,1253,1255],{"class":1088,"line":1254},29,[1086,1256,1257],{},"        dataStore.HoldingRegisters.WritePoints(1, new ushort[] { 10, 20, 30 }); \u002F\u002F 写入保持寄存器 (Holding Registers)。 这里是从地址1开始，也就是40001\n",[1086,1259,1261],{"class":1088,"line":1260},30,[1086,1262,1263],{},"        dataStore.InputRegisters.WritePoints(1, new ushort[] { 100, 200, 300 }); \u002F\u002F 写入输入寄存器 (Input Registers)。从地址 1 开始写入数据，也就是30001这指定了我们要操作的是“线圈”区（地址范围 00001 起始），它是一种开关量（bool 类型）。这是可读可写的开关量数据区，通常用于控制继电器或指示灯。\n",[1086,1265,1267],{"class":1088,"line":1266},31,[1086,1268,1269],{},"        dataStore.CoilDiscretes.WritePoints(1, new bool[] { true });\n",[1086,1271,1273],{"class":1088,"line":1272},32,[1086,1274,1275],{},"\u002F\u002F 写入线圈 (Coils)。\n",[1086,1277,1279],{"class":1088,"line":1278},33,[1086,1280,1281],{},"        Console.WriteLine(\"数据存储区已初始化。\");\n",[1086,1283,1285],{"class":1088,"line":1284},34,[1086,1286,1135],{"emptyLinePlaceholder":1134},[1086,1288,1290],{"class":1088,"line":1289},35,[1086,1291,1292],{},"        \u002F\u002F --- 4. 创建 Modbus 从站网络 ---\n",[1086,1294,1296],{"class":1088,"line":1295},36,[1086,1297,1298],{},"        var factory = new ModbusFactory();\n",[1086,1300,1302],{"class":1088,"line":1301},37,[1086,1303,1304],{},"        var network = factory.CreateSlaveNetwork(listener);\n",[1086,1306,1308],{"class":1088,"line":1307},38,[1086,1309,1310],{},"        var slave = factory.CreateSlave(slaveId, dataStore);\n",[1086,1312,1314],{"class":1088,"line":1313},39,[1086,1315,1316],{},"        network.AddSlave(slave);\n",[1086,1318,1320],{"class":1088,"line":1319},40,[1086,1321,1135],{"emptyLinePlaceholder":1134},[1086,1323,1325],{"class":1088,"line":1324},41,[1086,1326,1327],{},"        \u002F\u002F --- 5. 开始监听 ---\n",[1086,1329,1331],{"class":1088,"line":1330},42,[1086,1332,1333],{},"        Console.ForegroundColor = ConsoleColor.Green;\n",[1086,1335,1337],{"class":1088,"line":1336},43,[1086,1338,1339],{},"        Console.WriteLine($\"从站已在 TCP 端口 {port} 上开始监听... 按 Ctrl+C 退出。\");\n",[1086,1341,1343],{"class":1088,"line":1342},44,[1086,1344,1345],{},"        Console.ResetColor();\n",[1086,1347,1349],{"class":1088,"line":1348},45,[1086,1350,1135],{"emptyLinePlaceholder":1134},[1086,1352,1354],{"class":1088,"line":1353},46,[1086,1355,1356],{},"        await network.ListenAsync();\n",[1086,1358,1360],{"class":1088,"line":1359},47,[1086,1361,1362],{},"    }\n",[1086,1364,1366],{"class":1088,"line":1365},48,[1086,1367,1368],{},"}\n",[15,1370,1371,1372,1375,1376,1379,1380,404],{},"​\t首先我们要关注的就是数据存储器的代码，",[135,1373,1374],{},"SlaveDataStore"," 的作用是作为您的 Modbus 从站设备的",[18,1377,1378],{},"内存数据库","或",[18,1381,1382],{},"数据中心",[15,1384,1385,1386,1389,1390,1397,1398,404],{},"​\t您可以把它想象成一个模拟设备（比如 ",[18,1387,1388],{},"PLC 或传感器","）的**“大脑”或“内存条”",[18,1391,1392,1393,1396],{},"。当外部的主站（Master）设备发来一个请求，比如“读取3号寄存器的值”时，",[18,1394,1395],{},"NModbus 框架负责处理通信协议的细节","（接收数据、校验CRC、解析功能码），但它需要一个地方来查找“3号寄存器”的","真实数值**。这个地方，就是您所创建和管理的 ",[135,1399,1374],{},[157,1401,1403],{"id":1402},"数据存储区的作用",[18,1404,1402],{},[15,1406,1407],{},"整个工作流程是这样的：",[1409,1410,1411,1424,1431,1438,1448,1457],"ol",{},[32,1412,1413,1414,1416,1417,1419,1420,1423],{},"一个 Modbus 主站（例如测试软件）发送一个请求（比如：读取功能码 ",[135,1415,219],{},"，地址 ",[135,1418,352],{},"，数量 ",[135,1421,1422],{},"3","）。",[32,1425,1426,1427,1430],{},"您的 C# 从站程序的 ",[135,1428,1429],{},"ModbusSlaveNetwork"," 在串口或 TCP 端口上接收到这个请求并解析它。",[32,1432,1433,1434,1437],{},"它理解了这是一个“",[18,1435,1436],{},"读取保持寄存器","”的指令。",[32,1439,1440,1441,1443,1444,1447],{},"然后，它会转向您提供的 ",[135,1442,1374],{}," 对象，并向它查询：“请从你的 ",[135,1445,1446],{},"HoldingRegisters"," 集合里，把地址从 1 开始的 3 个值给我。”",[32,1449,1450,1452,1453,1456],{},[135,1451,1374],{}," 从自己的内存中找到这些值（例如 ",[135,1454,1455],{},"[10, 20, 30]","）并返回它们。",[32,1458,1459,1460,1462,1463,1466,1467,404],{},"最后，",[135,1461,1429],{}," 将这些值",[18,1464,1465],{},"打包成一个标准的 Modbus 响应帧","，通过",[18,1468,1469],{},"串口或 TCP 发送回主站",[15,1471,1472],{},"然后就是从站网络，从站网络的作用是调度中心和交通枢纽。如果把您的整个从站程序比作一个公司，那么：",[29,1474,1475,1494,1512,1527],{},[32,1476,1477,1486,1487,1379,1490,1493],{},[18,1478,1479,1482,1483],{},[135,1480,1481],{},"TcpListener"," \u002F ",[135,1484,1485],{},"SerialPort",": 是公司的",[18,1488,1489],{},"总机电话",[18,1491,1492],{},"大门口","，负责接收所有外部来电或访客。",[32,1495,1496,1505,1506,1379,1509,404],{},[18,1497,1498,1501,1502,656],{},[135,1499,1500],{},"IModbusSlaveNetwork"," (您代码中的 ",[135,1503,1504],{},"network",": 是坐在大门口的",[18,1507,1508],{},"前台",[18,1510,1511],{},"总机接线员",[32,1513,1514,1522,1523,1526],{},[18,1515,1516,1501,1519,656],{},[135,1517,1518],{},"IModbusSlave",[135,1520,1521],{},"slave",": 是公司里的",[18,1524,1525],{},"具体员工","（例如，工号为1的“张三”）。",[32,1528,1529,1533,1534,1537],{},[18,1530,1531],{},[135,1532,1374],{},": 是员工张三的",[18,1535,1536],{},"个人文件柜","，存放着他负责的所有数据。",[15,1539,1540,1541,1544],{},"​\tModbusFactory();这是一个遵循“工厂设计模式”的辅助类。它的作用是帮助我们创建所有符合 Modbus 规范的标准组件，避免我们手动 ",[135,1542,1543],{},"new"," 那些内部逻辑复杂的对象。您可以把它看作是公司的“行政部”，负责招聘员工和分配办公用品。",[15,1546,1547,1548,1551,1552,1554,1555,1561,1562,1565,1566,656,1568,1571,1572,1574,1575,1578,1579,1581],{},"​\tfactory.CreateSlaveNetwork(listener);",[18,1549,1550],{},"组成",": 这一步创建了一个 ",[135,1553,1500],{}," 的实例。它的核心组成部分是一个",[18,1556,1557,1558,656],{},"传输层接口 (",[135,1559,1560],{},"IModbusTransport","，这个接口包裹了您传入的 ",[135,1563,1564],{},"listener"," (TCP监听器)。这行代码正在创建**“前台\u002F总机接线员” (",[135,1567,1504],{},[18,1569,1570],{},"。您把公司的","“总机电话” (",[135,1573,1564],{},")** 交给了它，并告诉它：“从现在开始，你要负责监听这个电话的所有来电”。",[18,1576,1577],{},"此时状态",": ",[135,1580,1504],{}," 已经知道如何监听了，但它手头还没有任何员工的联系方式。如果现在有电话打进来，它不知道该转接给谁。",[15,1583,1584,1585,1587,1588,1591,1592,404],{},"​\tfactory.CreateSlave(slaveId, dataStore);这一步创建了一个 ",[135,1586,1518],{}," 的实例。它的核心组成是 ",[18,1589,1590],{},"从站ID (Unit ID)"," 和一个 ",[18,1593,1594,1595,656],{},"数据存储区 (",[135,1596,1597],{},"ISlaveDataStore",[15,1599,1600,1601,1603,1604,1622,1623,1626,1627,1630,1631,404],{},"​\tnetwork.AddSlave(slave);这一步是在 ",[135,1602,1500],{}," 内部的一个",[18,1605,1606,1607,1621],{},"字典（或集合）",[18,1608,1609,1610,1613,1614,1616,1617,1620],{},"中，添加一条记录，将 ",[135,1611,1612],{},"slaveId"," 和 ",[135,1615,1521],{}," 对象关联起来。可以多次调用 ",[135,1618,1619],{},"network.AddSlave()"," 来添加","更多具有不同 ID 的从站","。例如，您可以再创建一个 ",[135,1624,1625],{},"slaveId = 2"," 的 ",[135,1628,1629],{},"slave2"," 并添加进去。这样，",[18,1632,1633],{},"您的同一个程序就可以在同一个端口上模拟多个不同的 Modbus 设备",[15,1635,1636],{},"现在我们可以来看看报文，应该可以更好地理解它的组成。",[15,1638,1639],{},[121,1640],{"alt":1641,"src":1642},"image-20250912002107261","image-20250912002107261.png",[15,1644,1645,1646,1649],{},"​\t这里是一个主站master 程序在7789端口发往从站slave 502端口的一个指令。这个指令的具体意思是：“",[18,1647,1648],{},"你好，请 ID 为 1 的从站设备，把你的、从第 1 个地址开始的、连续 3 个保持寄存器 (Holding Registers) 的值告诉我。","”",[245,1651,1653],{"id":1652},"modbus-应用层-您最关心的信件内容",[18,1654,1655],{},"Modbus - 应用层 (您最关心的“信件内容”)",[15,1657,1658,1659,1662],{},"这是数据包的核心，定义了本次通信的",[18,1660,1661],{},"业务意图","。对应您截图中间详情窗口的最后一部分。",[29,1664,1665,1690,1707],{},[32,1666,1667,1672,1673],{},[18,1668,1669],{},[135,1670,1671],{},"Function Code: 3 (Read Holding Registers)",":\n",[29,1674,1675],{},[32,1676,1677,1682,1683,1685,1686,1689],{},[18,1678,1679,1680],{},"功能码 ",[135,1681,219],{},"。这明确了本次操作的类型是“",[18,1684,1436],{},"”。这是您在 C# 代码中调用 ",[135,1687,1688],{},"_master.ReadHoldingRegistersAsync(...)"," 所产生的结果。",[32,1691,1692,1672,1697],{},[18,1693,1694],{},[135,1695,1696],{},"Reference Number: 1",[29,1698,1699],{},[32,1700,1701,1706],{},[18,1702,1703,1704],{},"起始地址 ",[135,1705,352],{},"。这告诉从站要从哪个地址开始读取。",[32,1708,1709,1672,1714],{},[18,1710,1711],{},[135,1712,1713],{},"Word Count: 3",[29,1715,1716],{},[32,1717,1718,1723],{},[18,1719,1720,1721],{},"读取数量 ",[135,1722,1422],{},"。这告诉从站需要读取 3 个连续的寄存器（每个寄存器是 16-bit 的字）。",[245,1725,1727],{"id":1726},"modbustcp-应用层头部-mbap-header",[18,1728,1729],{},"Modbus\u002FTCP - 应用层头部 (MBAP Header)",[15,1731,1732],{},"这是为了让 Modbus 协议能够在 TCP\u002FIP 网络上传输而额外增加的一个“信封头”。",[29,1734,1735,1763,1781,1803],{},[32,1736,1737,1672,1742],{},[18,1738,1739],{},[135,1740,1741],{},"Transaction Identifier: 1",[29,1743,1744],{},[32,1745,1746,1751,1752,1755,1756,1759,1760,404],{},[18,1747,1748,1749],{},"事务标识符 ",[135,1750,352],{},"。这是一个序列号，由主站为",[18,1753,1754],{},"每个请求生成","。从站的响应中会",[18,1757,1758],{},"包含完全相同的标识符","，这样主站",[18,1761,1762],{},"就能将请求和响应一一对应起来，尤其是在异步通信中",[32,1764,1765,1672,1770],{},[18,1766,1767],{},[135,1768,1769],{},"Protocol Identifier: 0",[29,1771,1772],{},[32,1773,1774,1780],{},[18,1775,1776,1777],{},"协议标识符 ",[135,1778,1779],{},"0","。对于 Modbus 协议，这个值固定为 0。",[32,1782,1783,1672,1788],{},[18,1784,1785],{},[135,1786,1787],{},"Length: 6",[29,1789,1790],{},[32,1791,1792,1798,1799,1802],{},[18,1793,1794,1795],{},"长度 ",[135,1796,1797],{},"6","。它指明了",[18,1800,1801],{},"后面","还跟着 6 个字节的数据。我们来数一下：Unit Identifier (1字节) + Function Code (1字节) + Reference Number (2字节) + Word Count (2字节) = 6 字节。完全正确。",[32,1804,1805,1672,1810],{},[18,1806,1807],{},[135,1808,1809],{},"Unit Identifier: 1",[29,1811,1812],{},[32,1813,1814,1819],{},[18,1815,1816,1817],{},"单元标识符（从站ID） ",[135,1818,352],{},"。即使在 TCP 通信中通常是一对一，但为了兼容 Modbus 网关等设备，这个字段依然被保留，用于指定最终要访问的从站设备地址。",[245,1821,1823],{"id":1822},"tcp-和-ip-层-传输层和网络层",[18,1824,1825],{},"TCP 和 IP 层 (传输层和网络层)",[15,1827,1828],{},"这部分是为 Modbus 数据提供可靠的、端到端的传输服务。",[29,1830,1831,1844],{},[32,1832,1833,1578,1836,1839],{},[18,1834,1835],{},"Source Port (源端口)",[135,1837,1838],{},"7798",[29,1840,1841],{},[32,1842,1843],{},"这是您的主站程序（客户端）在发起连接时，由操作系统随机分配的一个临时端口。",[32,1845,1846,1578,1849,1852],{},[18,1847,1848],{},"Destination Port (目标端口)",[135,1850,1851],{},"502",[29,1853,1854],{},[32,1855,1856,1857,1860],{},"这是 Modbus TCP 协议的",[18,1858,1859],{},"标准服务端口","，您的从站程序正在这个端口上监听请求。",[157,1862,1864],{"id":1863},"数据包在列表中的上下文",[18,1865,1863],{},[15,1867,1868],{},"​\t在顶部的报文列表中，您可以看到：",[15,1870,1871,1872,1875,1876,1879,1880,1883,1884,1883,1887,1890],{},"在 ",[18,1873,1874],{},"#1528"," 这个 ",[135,1877,1878],{},"Query"," 包之前，有 ",[135,1881,1882],{},"[SYN]",", ",[135,1885,1886],{},"[SYN, ACK]",[135,1888,1889],{},"[ACK]"," 等包，那是 TCP 协议在建立连接的过程。",[15,1892,1893,1578,1899,1902],{},[18,1894,1895,1898],{},[135,1896,1897],{},"SYN"," (Synchronize)",[18,1900,1901],{},"同步请求标志","。发起方用它来表示“我想和你建立连接，并同步我们的通信序列号”。",[15,1904,1905,1578,1911,1914],{},[18,1906,1907,1910],{},[135,1908,1909],{},"ACK"," (Acknowledge)",[18,1912,1913],{},"确认标志","。接收方用它来表示“我收到了你的消息”。",[1916,1917,1918],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":1082,"searchDepth":1095,"depth":1095,"links":1920},[1921,1922,1923,1924,1928,1929,1933,1936,1939],{"id":12,"depth":1095,"text":13},{"id":56,"depth":1095,"text":57},{"id":89,"depth":1095,"text":89},{"id":145,"depth":1095,"text":85,"children":1925},[1926,1927],{"id":159,"depth":1101,"text":159},{"id":213,"depth":1101,"text":213},{"id":269,"depth":1095,"text":269},{"id":335,"depth":1095,"text":336,"children":1930},[1931,1932],{"id":342,"depth":1101,"text":342},{"id":466,"depth":1101,"text":269},{"id":479,"depth":1095,"text":479,"children":1934},[1935],{"id":482,"depth":1101,"text":485},{"id":857,"depth":1095,"text":858,"children":1937},[1938],{"id":185,"depth":1101,"text":185},{"id":1071,"depth":1095,"text":1072,"children":1940},[1941,1942],{"id":1402,"depth":1101,"text":1402},{"id":1863,"depth":1101,"text":1863},"md",{},"\u002Fcsharp\u002Fmodbus通信协议",{"description":1082},"csharp\u002Fmodbus通信协议","sD-OCPL6jSMjx51rfT0M9OdVHQeYQrZFI9wj8y_pSiM",1791042465249]