news 2026/10/28 23:57:25

C# ModBus通信读写实战:RTU串口与TCP报文、CRC校验及寄存器地址避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# ModBus通信读写实战:RTU串口与TCP报文、CRC校验及寄存器地址避坑指南

简介:一套基于C#语言实现的ModBus通信组件,面向需要快速对接三菱、西门子、欧姆龙等支持ModBus协议的PLC或服务器的开发人员,零基础也可直接集成使用。代码全开源,不依赖第三方组件,支持ModBus Tcp、Rtu、Ascii及RtuOverTcp四种通信方式,读写操作可置于后台线程避免界面卡死,适合工业上位机、数据采集、设备监控等多种应用环境。压缩包共30个文件,体积仅42KB,主要包含9个C#源码文件、项目工程文件、配置与资源文件、可执行程序及使用说明文本,结构清晰,便于直接查看修改和二次开发。目前已有5038人学习下载,并已在多个项目中实际验证。通过该资源可快速掌握ModBus各协议的报文构造、串口与网络通信实现思路,并获得可直接运行的示例程序与配套说明,省去从零调试协议的繁琐过程,整体轻量,适合快速集成进现有软件系统。

1. C# ModBus通信读写操作:零基础也能快速对接的工业通信方案

做上位机开发的工程师,十有八九都会碰到 ModBus 通信的活。设备是 PLC、传感器、智能仪表也好,数据采集终端也罢,上位机要读数据、写参数,ModBus 基本是绕不开的协议。这套 C# 版 ModBus 通信读写资源,把 Rtu 串口通信和 Tcp 通信两块完整实现了,工程源码全开源。我拆完这套东西的感受是:它把通信中间那层最烦人的底给铺好了,你拿到手改改参数就能用。

这套资源解决的是实实在在的痛点:新手调串口通信,常常卡在报文格式、CRC 校验、寄存器地址换算这些地方;老手则烦反复封装通信层。它的做法是直接提供一套能跑的 C# 工程,把报文组包、CRC16 校验、串口收发、Tcp 连接管理都封装好,同时把关键逻辑留了接缝,方便你按自己的设备改。适合的读者很明确:做上位机开发的 C# 工程师、自动化集成调试人员,以及学校里刚接触工业通信的学生。核心用一句话概括:它不是一个库的文档,而是一套能直接编译、运行、对接真实设备的通信层源码。

2. ModBus协议核心:RTU报文、TCP报文与功能码选型

2.1 RTU和TCP的报文结构差异

ModBus 协议在应用层只有一套定义,但到了链路上就分成几种传输模式。这套资源里常见的是 ModBus RTU 和 ModBus TCP 两种。RTU 走串口,报文是二进制帧;TCP 走以太网,报文在 RTU 的基础上去掉了 CRC 校验,换成了 MBAP 头。很多刚接触的人会被这个差异弄糊涂,以为两套东西完全不同,其实把报文拆开看,核心数据区是完全一致的。

RTU 报文的帧结构是:从站地址(1字节)+ 功能码(1字节)+ 数据区(N字节)+ CRC16 校验(2字节)。其中 CRC16 校验是多出来的,因为串口链路噪声大,需要校验帧完整性。TCP 报文的帧结构是:事务处理标识符(2字节)+ 协议标识符(2字节,固定为0)+ 长度(2字节)+ 单元标识符(1字节)+ 功能码 + 数据区。注意 TCP 模式没有 CRC,因为 TCP/IP 协议栈自己做了可靠性保证。长度字段计算的是单元标识符到数据区末尾的字节数。

这两者的选型逻辑不复杂:设备只有串口,用 RTU;设备有网口或者通过网关转成 TCP,用 TCP。还有一个常见场景是串口服务器,把 RTU 经串口服务器转发成 TCP,这时候上位机的写法就是用 ModBus TCP 去连串口服务器的 IP 和端口,报文会原样透传。这套资源的工程里,两种模式的读写逻辑是分开封装的,但对外暴露的方法名保持一致的,这正是它方便的地方。

2.2 功能码分类与常用码值

ModBus 把设备里的数据分成了四类存储区,对应不同的功能码。线圈(Coil)是可读可写的位,对应 0x01 读和 0x05 写单线圈、0x0F 写多线圈。离散输入(Discrete Input)是只读的位,用 0x02 读。保持寄存器(Holding Register)是可读可写的 16 位字,用 0x03 读、0x06 写单寄存器、0x10 写多寄存器。输入寄存器(Input Register)是只读的 16 位字,用 0x04 读。

实际开发里,保持寄存器和输入寄存器最常用。比如读一个温控器的当前温度,设备手册说温度值在保持寄存器地址 0x0064,那上位机就发功能码 0x03,起始地址 0x0064,读取数量 1。返回值是两个字节,按手册约定的数据格式解析:可能是无符号整数、有符号整数,也可能是 IEEE 754 浮点数的高低位拼装。功能码选型的关键不在背码值,而在看清楚设备手册定义的数据类型和存储区类型,发错了功能码设备会回异常码 0x02(非法数据地址)或者干脆不响应。

2.3 寄存器地址的偏移换算规则

这是 ModBus 通信里最大的一个坑。设备厂商的说明书上,寄存器地址往往写成 40001、40002、30001 这种五位数,这是 PLC 时代的遗留习惯。40001 代表保持寄存器区的第一个寄存器,对应协议报文里的地址 0x0000;40002 对应 0x0001,依此类推。30001 是输入寄存器区的第一个,对应报文地址 0x0000。你把说明书上的 40020 填到报文里,设备会回异常码,因为 40020 在报文里的地址范围根本不存在。

这套资源在工程里做了一个地址换算的辅助方法,把用户输入的 40001 格式自动减 1 换算成协议地址,写寄存器时再换算回去。如果你拿到的资源没有这个换算,建议自己加一个,方法很简单:协议地址 = 说明书地址 - 1,前提是说明书地址属于 4xxxx 保持寄存器区。另外有些设备说明书直接给十六进制地址,比如 0x0064,这种情况下就不需要减 1,直接填进去就行。判断标准是看地址描述有没有把 4 那个区号写在开头。

3. C# ModBus RTU串口通信落地:SerialPort参数、CRC16校验与读写封装

3.1 串口参数初始化的选型逻辑

C# 上位机操作串口,用的是 System.IO.Ports.SerialPort 类。这套资源里把串口参数集中放在一个初始化方法里,核心参数包括端口号、波特率、数据位、停止位、校验位。波特率要和从站设备保持一致,常见的有 9600、19200、115200。数据位基本固定为 8,停止位常见 1 位,校验位常见无校验(None)。这些参数在设备手册里一定能找到,找与门禁控制器、温控器、仪表通信的参数表格,照着填就行。

一个容易被忽略的地方是超时设置。SerialPort.ReadTimeout 和 WriteTimeout 在串口通信里必须设置,否则设备没响应时 Read 方法会一直卡住。这套资源的做法是超时设成 1000 毫秒,读写失败直接抛异常,由上层调用方决定是重试还是告警。还有 Encoding 属性,串口通信用的是字节流,不需要设置编码,保持默认即可。

初始化代码大致是按这个结构组织的:

serialPort = new SerialPort(); serialPort.PortName = "COM3"; serialPort.BaudRate = 9600; serialPort.DataBits = 8; serialPort.StopBits = StopBits.One; serialPort.Parity = Parity.None; serialPort.ReadTimeout = 1000; serialPort.WriteTimeout = 1000;

这段代码定义了串口通信的基本参数。PortName 需要根据实际硬件确定,可以在 Windows 设备管理器里查看;波特率必须与对端设备一致,否则收到的全是乱码或者在噪声中挑不出完整帧;DataBits 和 StopBits 直接用工业默认的 8 位、1 位,除非设备手册明确要求别的。ReadTimeout 和 WriteTimeout 是必须的,不设的话,一旦设备掉线,读操作会永久阻塞,上位机界面直接卡死。我一般还会在初始化后单独写一个 OpenSerial 方法,把异常捕获和端口占用检测放在里面,这样启动时就能给出明确提示。

3.2 CRC16校验的计算与低字节在前的坑

RTU 模式比 TCP 多一步:报文的结尾要附加两字节 CRC16 校验码。计算规则是:CRC 初值为 0xFFFF;每个字节和 CRC 异或;对 CRC 的低字节右移一位,如果移出的位是 1,则与多项式 0xA001 异或,按位重复 8 次。这套资源的工程里有一个独立的 CRC16 静态方法,输入是报文的字节数组(不含 CRC 本身),输出是两字节校验码。

很多第一次写 ModBus 的人在这里翻车,因为 CRC 附加到报文时,低字节在前、高字节在后。比如计算结果是 0x1234,报文里要写 0x34 0x12,写反了设备照样回异常或者不响应。这跟 TCP 报文的长度字段高字节在前完全相反,初学者容易把两种模式的分字节序搞混。建议在调试时用 ModBus 调试工具生成一帧标准报文,把 CRC 高低字节和你的代码输出做比对,一次就能确认字节序对不对。

一个真实的案例是读一个流量计的瞬时流量,组包过程是:从站地址 0x01、功能码 0x03、起始地址 0x0064、读取数量 0x0002,然后把这 6 个字节丢进 CRC16 方法得到校验码,附加时按低字节在前规则,最终发送 8 个字节。上位机收到的响应帧结构是:地址、功能码、字节数(0x04)、数据 4 字节、CRC 2 字节。因为读的是 2 个寄存器,共占 4 字节,单精度浮点数正好对应一个 4 字节 float 值,这属于 ModBus 常见的数据拼装场景。

3.3 读保持寄存器和写寄存器的业务流程

这套资源的读写方法都遵循同一个业务逻辑:组包 → 发送 → 接收 → 校验 → 解析。读保持寄存器的方法入参是从站地址、起始地址(协议地址)、读取数量,返回值是寄存器数组。组包时,起始地址按 2 字节高字节在前拼装,读取数量同样按 2 字节。发送后立即进入接收循环,循环截止条件有两个:收到的数据够一帧完整报文,或者超时。完整报文长度可以从响应帧第 2 个字节读出数据区长度,再加 5 个固定字节。

写寄存器分为写单寄存器和写多寄存器两种。写单寄存器用功能码 0x06,报文是地址、功能码、寄存器地址、写入值。写多寄存器用功能码 0x10,报文会多一个字节数描述数据区长度,以及连续的数据字节。实际设备里,修改一个参数用 0x06 就够了;批量初始化参数或下发配方时用 0x10。这套资源里两个方法都实现了,写多寄存器时还会校验响应帧的数据区,确认写入的地址和数量与请求一致,防止写错位置。

串口读写和网络通信有一个明显差异:串口没有连接的语义,发送完一帧就是纯粹把字节丢进缓冲区,对端是否收到、如何响应,都要靠本机的等待和超时判断。这也意味着串口通信不能并行,同一时间只能有一个请求在等待响应,调试和设计时必须给通信方法加个信号量或者队列,把并发请求串行化。否则两个线程同时向串口写数据,会导致帧混合,对端解析必然出错,这是我们实际项目里踩过的实实在在的坑。

4. C# ModBus TCP通信落地:连接管理与读写方法封装

4.1 TCPClient连接的生命周期管理

ModBus TCP 在 C# 里的实现基础是 System.Net.Sockets.TcpClient。相比串口的“发完拉倒”,TCP 模式必须先建立连接,再发请求,收到响应之后连接可以保持,供后续请求复用。这套资源的 Tcp 通信类把连接生命周期管起来了:首次读写前检查连接状态,如果未连接或已断开,先执行 Connect;每次读写操作完成后不断开,保持长连接。这样做的原因是工业设备普遍对频繁建连不友好,比如有些 PLC 的以太网模块,每秒新建连接次数会被限流,连接建立本身就是有开销的。

连接的核心参数是 IP 地址和端口号,端口号 ModBus 标准的默认值是 502。有些网关设备允许自定义端口,比如工业网关把 ModBus TCP 映射到 1502 或 5000,这时候需要改默认端口。超时设置同样重要,TcpClient.Connect 本身没有超时参数,这套资源的做法是开一个 Task 去执行连接,用 Task.WaitAsync 或轮询方式判断是否超时,超时后主动关闭客户端并抛异常。读写的超时通过 NetworkStream.ReadTimeout 和 WriteTimeout 控制,这套资源的默认值设在 3000 毫秒左右,比串口稍长,因为中间隔着一层路由器或交换机,物理时延更高。

连接方法的代码结构大致如下:

public bool Connect(string ip, int port = 502) { try { tcpClient = new TcpClient(); var task = tcpClient.ConnectAsync(ip, port); if (!task.Wait(3000)) { Close(); throw new TimeoutException("ModBus TCP连接超时"); } stream = tcpClient.GetStream(); stream.ReadTimeout = 3000; stream.WriteTimeout = 3000; return true; } catch (Exception ex) { lastError = ex.Message; return false; } }

Connect 方法建立了到设备的底层 TCP 通道,重点在于超时控制和失败处理。ConnectAsync 配合 Wait(3000) 保证了在指定时间内没有成功就不继续等待,而 Close 方法必须把 TcpClient 和 NetworkStream 都释放干净,否则下次 Connect 会报“地址已在使用”的套接字异常。GetStream 拿到的是网络流,后续所有 ModBus 报文的读写都在这条流上进行,ReadTimeout 和 WriteTimeout 设成 3000 毫秒能避免设备卡住时 UI 线程被白白阻塞。实际调试中,我习惯在 Connect 方法里加一条日志记录连接的端口号,因为排查生产环境问题时会发现端口不一致是常见配置错误。

4.2 TCP模式下读写方法与RTU模式的差异

TCP 读保持寄存器的方法,入参和 RTU 版本保持一致,但组包过程有明显差异。TCP 请求帧由 7 个字节的 MBAP 头和请求数据组成,MBAP 头包含事务标识、协议标识、长度、单元标识。事务标识每次请求递增,用来匹配请求和响应,比如第一次请求时它等于 0x0001,第二次等于 0x0002,对端响应时会回显相同的事务标识。长度字段描述的是从单元标识到数据区末尾的字节数,需要根据请求内容动态计算。

响应帧的解析也比 RTU 多一步:先从 MBAP 头中取出长度字段,计算出响应帧总长;再从缓冲区内读取完整一帧;最后确认事务标识和单元标识与请求一致,再做数据区解析。这套资源的 TCP 读写方法把这三步封装在了一个 ReceiveResponse 方法里,调用方拿到的就是和 RTU 相同的寄存器数组,业务代码可以复用。这种设计在实际开发中很友好,因为切换设备通信方式时不用重写业务逻辑。

这里插一句,很多人以为 TCP 模式的报文没有 CRC 校验,就认为它一定比 RTU 可靠。实际不是这样:TCP 的可靠性只保证字节不丢、不乱序,但保证不了应用层报文的完整性。比如设备正好发到一半断电,上位机可能收到半帧;或者网关转发时额外插入了一个字节,直接把帧边界打乱。所以 TCP 模式下同样要做帧完整性校验,判断标准是长度字段与实际收到的字节数是否一致。这套资源在 ReceiveResponse 里做了这个检查,收到的数据长度与长度字段不匹配时会继续等待剩余字节,而不是立刻解析。

4.3 实际对接设备时的连接池与重连策略

工业上位机不是只连一台设备,一条产线十几台仪表、几个 PLC 的常见。每台设备一个 TcpClient 实例,如果并发读写,就需要一个连接管理池。这套资源的工程里有一个简单的连接池实现,字典结构,Key 是 IP+端口,Value 是 TcpClient 实例,读写前从池里取连接,完成后归还。连接池的大小建议控制在 10 个以内,太多的长连接会把设备端口占满。

断开重连也是个需要考虑的策略。TCP 连接不是永久可靠的,设备断电、网线松动、路由表变化都可能让连接静默断开。在 TCP 长连接里,有一类情况是最隐蔽的:本机没有被系统通知套接字异常,写数据也显示成功,实际对端已经消失,这叫半开连接。这套资源的做法是读写操作如果收到 SocketException 或者 IOException,就主动标记连接失效,下次请求前先重连一次。重连的次数限制在 3 次,超过后抛异常交由上层处理,避免设备恢复前程序陷入无限重试的死循环。实际项目里,配合心跳请求效果更好,每隔几秒发一次功能码 0x07 的读异常状态请求,既能检测连接又能收集设备状态。

5. 实战避坑:ModBus 收发常见问题与排查记录

5.1 寄存器地址偏移错误导致设备回异常码

现象:上位机向设备发送读取请求,设备不返回数据,或者返回异常码 0x02、0x03。原因:地址填错是最常见的,说明书写的是 40001,报文里也填了 40001,实际协议地址要填 0x0000;还有一个原因是起始地址和读取数量的组合越界,比如设备只有 10 个寄存器,一次读 20 个,也会回异常。解决:把说明书地址减去 1 再填入报文;读取数量要按设备手册确认寄存器个数,不确认就一次只读 1 个。调试时先用 ModBus Poll 这类工具读一遍,确认地址没问题再写代码。

5.2 串口端口被占用导致 SerialPort 打开异常

现象:程序启动时 SerialPort.Open 抛 UnauthorizedAccessException,或者直接弹“端口被占用”的提示。原因:串口调试助手、ModBus Slave 模拟工具占着 COM 口没释放;或者上一次程序异常退出,SerialPort 对象没有被 Dispose,系统认为端口仍在占用。解决:先把占用的工具关掉;程序里加 using 或者在 finally 里调用 Close 和 Dispose;异常退出后,在设备管理器中禁用到启用一次该 COM 口,可以强制释放残留占用。排查时可以用命令行工具列出当前端口占用情况,确认端口才去打开。

5.3 CRC校验码字节序颠倒导致设备不响应

现象:报文格式看起来完全正确,地址、功能码、数据都对,但设备就是不回。原因:CRC 计算出的两字节附加到报文时顺序写反了,RTU 协议要求低字节在前、高字节在后,写反后设备本地计算 CRC 与报文尾部不匹配,整帧被丢弃。解决:无条件按低字节在前附加,也就是 crcLow = crc & 0xFF;crcHigh = crc >> 8,拼装时先加 crcLow 再加 crcHigh。验证方式是用 ModBus 调试工具生成同一报文的 CRC,用十六进制比较工具对照,OK 就说明顺序和算法都对。

5.4 TCP半开连接导致写操作假成功

现象:程序往设备写寄存器,方法返回成功,但设备实际没执行;过一会儿再读,数据还是旧值。原因:底层 TCP 连接已经是半开状态,本机不知道对端已断,写操作被系统缓冲区接收,实际没到设备端。解决:给写操作加上设备级确认,写寄存器后立即回读一次该寄存器的值,对比写入值和回读值;检测到不一致,主动关闭连接并重连,再执行一次写操作。读操作也建议做值合法性校验,收到的数据明显不符合物理范围(比如温度 -300 度),就判定数据不可信,重新读取。

5.5 浮点数读取的字节序混乱问题

现象:读回来的 4 字节拼成 float 后,数值很明显不对,比如 25.5 读出来变成了 1.7E-18 这种天文数字。原因:设备存储浮点数的字节序不同。常见的有三种:ABCD(字节序正常)、CDAB(寄存器之间顺序颠倒)、BADC(高低字节颠倒)。每个厂商的设备寄存器布局不一样,没有统一标准。解决:把 4 字节按三种顺序分别拼装成 float,输出三个候选值,再对照设备显示的实际值选正确的。或者用资源的字节序切换方法,这个方法传入 4 字节数组和枚举类型,按指定顺序拼装成单精度浮点数。换新设备时,先读一个已知数值,用这方法跑一遍,确定该设备的字节序再固化到代码里。

6. 用 ModBus Slave 和 ModBus Poll 做从站模拟:验证读写功能的正确姿势

调试这套资源时,手边没有真实设备是常态。ModBus Slave 是免费的单机从站模拟工具,可以在电脑上虚拟出若干个从站设备;ModBus Poll 同样是免费的 Master 模拟工具,用来模拟上位机发请求。一主一从配合使用,就能在不接触硬件的情况下完整验证这套 C# 资源的读写逻辑。

先用 ModBus Slave 建一个从站,设置从站地址为 1,功能选 03 保持寄存器,寄存器数量设 10 个,手动给几个寄存器填入已知值,比如地址 0 的寄存器填 25。然后运行这套资源的 C# 程序,从站地址填 1、功能码 03、起始地址 0、读取数量 1。程序读回来的值应该是 25,如果报文格式有问题,ModBus Slave 的错误计数会增长,同时 C# 程序会走到超时或者异常分支。这个过程能同时验证 CRC 计算、报文拼装、超时处理和地址换算四个环节,覆盖面比对着代码看高得多。

如果你想验证反方向,用 ModBus Poll 模拟上位机,把你的 C# 程序改造为从站模式运行,Poll 发起读请求,程序解析请求并回送响应。串口模式下两者需要用到虚拟串口工具,比如 VSPD 创建一个 COM3/COM4 的虚拟串口对,ModBus Slave 挂在 COM4,C# 程序操作 COM3。TCP 模式下不需要串口工具,ModBus Slave 监听 502 端口,C# 程序直接连 127.0.0.1:502,整个过程不碰真实网络设备。

TCP 调试中有一个实用的小技巧:把 ModBus Slave 的监听端口设成一个不常用的端口(比如 1502),避免和本机已有服务冲突;同时把 C# 程序的连接超时调短到 1000 毫秒,这样能快速发现从站没启动或者端口错了。在真实设备上调试时,我一般会先用 ModBus Poll 连接设备,确认地址映射和数据值都符合预期,再切到 C# 程序验证同一组数据。这个习惯帮我挡掉了很多“代码看起来对,设备就是不应答”的沟通成本。

从那以后,我每接一个新设备,都会强制走一遍“先用 Poll/Slave 验证协议、再上 C# 程序对接、最后才写业务逻辑”的流程,省下的排查时间远超调试工具的投入。这套 C# ModBus 通信资源适合作为这个流程的基础层,你拿它直接做二次开发,或者只借用它的 CRC 和组包逻辑,都能省掉不少重复工作。希望帮到你。

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

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

驱动断链隐藏原理与实现:从PsLoadedModuleList摘除节点

简介:面向驱动开发者和逆向安全研究人员的驱动隐藏源码包,以HideDriver项目为主线,集中演示驱动断链隐藏、注册回调隐藏以及针对TP等动态调试工具的检测与规避思路,可帮助读者理解内核驱动如何绕过模块枚举、隐藏自身存在&#xf…

作者头像 李华
网站建设 2026/10/28 23:56:54

基于Multisim的心电信号放大器仿真设计:从微弱ECG到1000倍增益

简介:心电信号放大器的Multisim仿真项目面向生物医学电子、电路设计方向的学生与工程师,提供一套从电路搭建到仿真验证的完整方案。资源共7个文件,涵盖设计报告、思路说明、Multisim源文件、原理图及PCB设计文档,压缩包约3.38MB&a…

作者头像 李华
网站建设 2026/10/28 23:54:04

配电网重构与多时间断面潮流计算优化实践

1. 项目概述:配电网重构与多时间断面潮流计算在电力系统优化领域,配电网重构(Distribution Network Reconfiguration, DNR)是提升电网运行效率的关键技术手段。这个项目聚焦于利用IEEE 33节点和PG&E 69节点这两个电力系统标准…

作者头像 李华
网站建设 2026/10/28 23:53:20

大语言模型查询调度优化:前缀重用与延迟约束实践

1. 项目概述:大语言模型查询调度的核心挑战在2025年NIPS会议上提出的"LLM Query Scheduling with Prefix Reuse and Latency Constraints"研究,针对大语言模型服务部署中的关键性能瓶颈提出了创新解决方案。当前LLM服务面临的核心矛盾在于&…

作者头像 李华
网站建设 2026/10/28 23:52:29

CEEMDAN非平稳信号分解:从EMD模态混叠到参数配置与验收

简介:面向从事非线性、非平稳信号分析的研究生、算法工程师与科研人员,这套 CEEMDAN 实现可作为从 EMD、EEMD 过渡到自适应噪声完备集成经验模态分解的实践材料。其核心在于按信号特性动态加入噪声,经多次 EMD 迭代与 IMF 平均抑制模态混叠&a…

作者头像 李华
网站建设 2026/10/28 23:52:05

2026大模型API价格战:六家计费机制拆解与成本优化选型指南

1. 大模型API价格战背后的真实格局2026年开年到现在,我几乎每周都要帮客户重新算一遍大模型API的调用成本。原因很简单——价格变动太频繁了,去年还觉得DeepSeek已经把价格打到地板了,结果豆包和Kimi接连出招,Gemini又时不时调整免…

作者头像 李华