
简介面向工业自动化与PLC二次开发场景这份资源专门服务那些希望突破梯形图限制、用C为西门子PLC编写逻辑并完成上位机通信的开发者。压缩包内共33个文件以头文件、CPP源代码、Visual Studio工程配置、DLL动态链接库和可执行程序为主体整体大小约25.96MB既可用于阅读学习也能在本地打开工程进行编译与调试。已有1317人学习下载资料围绕西门子PLC的C编译、PLC与上位机数据交互、以及对数据块DB、位存储区M区、输入区I区和输出区O区的读写展开并带有可运行示例适合作为入门到进阶的参考。借助其中DaveMfc等工程和库函数读者能直观理解S7COMM、OPC UA等通信方式的调用逻辑掌握在TIA Portal、SIMATIC S7-PLCSIM Advanced等环境下完成C集成开发的关键路径对提升工业自动化系统调试与监控效率有直接帮助。1. 在工控现场引入C先搞清楚数据从哪来做西门子PLC项目超过五年的人大概率会遇到一个尴尬场景梯形图写了几千行结构化文本(ST)勉强撑住了复杂算法但一碰到上位机需要高速读写数据块(DB)、做边缘计算或者对接第三方系统PLC原生代码的边界就出来了。而C这一侧恰好能补上——代码执行效率高、可复用性强更重要的是它能直接通过网口或串口把西门子PLC的存储区当作内存块来访问读DB、写M区、订阅I/Q变化这些动作在C里都是普通的函数调用。很多人误以为可运行C的PLC指的是在PLC的CPU里直接跑C字节码实际并不是这样。更常见的做法是PLC固件保持梯形图/ST作为实时逻辑主载体C代码则跑在PC上位机、边缘网关甚至西门子IOT2000这类可运行Linux的工业设备上通过S7协议或PROFINET与PLC交换数据。本文以DaveMfc工程为例——这是一套基于libnodave库的MFC源码包完整演示了在Windows环境下用C读取S7-300/400系列PLC输入、输出、M区和DB区的全过程适合需要自己写上位机通信程序的人直接取用。2. 西门子PLC的存储区与通信协议选择MPI还是Profinet2.1 先分清S7-300/400的存储区结构否则读写全是错的西门子PLC的存储区是分门别类编址的C程序要读数据第一步不是写代码而是先确认目标数据躺在哪个区域。S7-300/400系列核心存储区分为以下几类区域符号名典型用途C访问方式输入映像区I区传感器信号输入readBytes / readBits输出映像区Q区执行器控制输出writeBytes位存储区M区中间变量、标志位readBits / writeBits数据块DB区自定义结构化数据readDB / writeDB定时器/计数器T/C区逻辑控制协议支持有限不推荐直接操作I区和Q区在S7协议中是按字节编址的例如I0.0到I0.7对应第0字节的8个位M区支持按位寻址也支持按字节、字、双字寻址DB区最特殊它有自己独立的编号和内部偏移量比如DB10.DBW0表示数据块10的从偏移0开始的字。DaveMfc工程里的核心读写操作都围绕这三个区展开在源码中可以看到它分别调用了readBytes(flags, area, offset, len)和writeBytes系列函数前者负责I/O区和M区后者负责写回DB和输出区。2.2 MPI、PROFIBUS、PROFINET、以太网S7通信的区别很多刚接触C连接西门子PLC的人卡在第一步PLC上有MPI口、有DP口、有PN口到底该连哪个这里要给一个明确结论——如果你的C程序跑在普通PC上首选是以太网S7通信其次是MPI转以太网网关。PROFIBUS的DP口通常不推荐直接用于上位机通信因为那是PLC与分布式IO之间的实时总线PC侧接入需要专门的CP5611等板卡驱动复杂且价格高。DaveMfc工程里libnodave库同时支持MPI协议和S7以太网协议MPI协议走串口或USB转MPI电缆适用于S7-200/300早期型号S7以太网协议走TCP 102端口适用于S7-300/400带PN口的CPU或通过CP343-1等通信模块扩展。libnodave的openSocket(int port, char *host)函数就是用来建立这种TCP连接的端口号固定为102host是被连接PLC的IP地址。2.2.1 S7通信的协议分层与端口关系从OSI模型角度看S7通信协议建立在TCP/IP之上应用层使用S7 PDU格式默认端口102。握手过程是客户端发送一串十六进制字节CRLoad、CCSetup、Negotiate服务端确认后进入PDU交换模式。libnodave把这一套封装成了negotiatePduLength函数用户通常不需要关心细节但有一个参数必须匹配——PDU长度协商值。S300和S400的PDU上限不一样如果协商失败C程序会直接报连接错误或超时这在后面第5章的排错部分会详细展开。3. DaveMfc工程结构libnodave的MFC封装是怎么转起来的3.1 源码包里的核心文件职责划分下载解压西门子PLC C DaveMfc后你看到的不是一个单一源码文件而是一个Visual Studio解决方案里面包含完整的MFC对话框工程。对这种C上位机工程我建议先按功能模块去读而不是从stdafx.h开始挨个翻文件/目录职责关键函数libnodave目录S7协议栈核心openSocket, readBytes, writeBytesDaveMfcDlg.cpp上位机界面逻辑与线程分发按钮事件、定时器回调DaveMfc.h全局数据结构与状态标志PLC连接状态、数据缓冲区dave.h/dave.c协议实现源码级可移植daveReadDB, daveWriteBytes这类工程的通用架构是MFC对话框作为主线程点击连接PLC后创建一条独立的通信线程通信线程里执行openSocket和connectPLC成功后主线程通过定时器或消息机制定时调用读写接口刷新界面显示。这里有一个工程设计的细节值得注意——绝不能在MFC的ON_PAINT或ON_TIMER里直接调用dave_read_bytes这类阻塞函数因为S7通信的最坏超时时间是秒级直接调用会导致界面卡死。源码中常见的做法是使用PostMessage把数据从读取线程送回UI线程然后用UpdateData(FALSE)刷新。3.2 libnodave在Windows下的编译要点这份源码的年代背景是VS2012/2013时代工程文件后缀是.sln和.vc.db如果你现在用VS2019或VS2022打开大概率会提示需要升级或忽略不可用组件。实际操作中有两个坑需要特别注意。第一个坑是VC运行时库的配置。MFC工程默认依赖共享DLL而libnodave是纯C源码建议两个模块都统一设置为多线程(/MT)或统一为多线程DLL(/MD)否则链接阶段会出现LNK2038不匹配错误。第二个坑是字符集——DaveMfc工程里如果UI代码用了CString需要确认项目字符集是使用Unicode字符集还是使用多字节字符集这会影响所有对话框控件的字符串赋值方式。3.2.1 重新编译的快捷路径如果升级到新版本Visual Studio后碰到大量编译错误有一个省事的做法是不直接升级整个工程而是新建一个空的MFC对话框工程只把libnodave目录下的C源文件和头文件添加进去再把DaveMfcDlg.cpp里的通信逻辑以类的方式重新组织。这样做的收益很直接脱离了老工程的引用链stdafx.h可以被替换为pch.h项目配置完全可控。我在迁移这类老通信工程时通常花在预处理宏和包含路径上的时间能占30%真正的协议逻辑反而不需要改动。4. 用libnodave读写DB块、M区和I/Q区含完整参数说明4.1 建立S7以太网连接并完成握手在编写任何读写代码前必须先建立C程序与PLC之间的S7连接。下面是基于libnodave的最小可用代码段适合直接抄进你的上位机工程中// 需要包含的头文件 #include libnodave.h // 全局连接句柄 daveInterface *daveIf NULL; daveConnection *daveCon NULL; unsigned char buffer[8192]; // 由daveInterface使用的I/O缓冲区 int connectPLC(const char* ip, int rack, int slot, int port 102) { int res 0; // 1. 初始化接口结构设置协议类型 daveIf openSocket(port, (char*)ip); if (daveIf NULL) { return -1; // TCP连接失败检查IP和网络 } // 2. 设置PDU长度和PLC类型 daveIf-timeout 5000; // 毫秒建议不小于3000 daveIf-error 0; // 3. 建立S7会话S7-300通常 rack0 slot2S7-400为 rack0 slot3 daveCon daveNewConnection(daveIf, rack, slot, 0); if (daveCon NULL) { closeSocket(port, (char*)ip); return -2; } // 4. 执行S7协议握手协商PDU长度 res daveConnectPLC(daveCon); if (res ! 0) { daveDisconnectPLC(daveCon); closeSocket(port, (char*)ip); return -3; // 握手失败检查rack/slot以及PLC防火墙设置 } return 0; }这段代码的逻辑分四层第一步调用openSocket建立的是TCP层连接这一步只看IP是否可达、PLC通信模块是否监听102端口第二步设置超时时间这里要注意libnodave的超时单位在不同版本中有微秒和毫秒之分建议在源码中查看struct daveInterface的timeout成员注释第三步的daveNewConnection参数里rack和slot对应S7 CPU在机架上的物理位置S7-300的CPU通常位于0号机架2号槽位S7-400则是0号机架3号槽位参数错误时连接不会建立成功。第四步是整个连接过程的关键——daveConnectPLC会完成S7协议层的CRLoad序列和PDU长度协商。很多人在这一步失败后反复检查IP、网址和硬件却忘了最容易错的是rack和slot值。有个简便的验证方法用西门子TIA Portal或Step7软件在线找到CPU属性里面会明确显示机架号和槽位号。4.2 读取DB块与M区的数据连接握手之后就可以对PLC存储区做读写。以下是从DB块读取数据的完整代码片段// 从DB块中读取多个字节 int readDBData(int dbNumber, int offset, unsigned char* dest, int len) { int res 0; daveResultSet *result NULL; // 调用libnodave的DB读函数 res daveReadDB(daveCon, dbNumber, offset, len, NULL); if (res ! 0) { return -1; // 读取失败检查DB号是否实际存在 } // 将结果从内部缓冲区拷贝到用户缓冲区 result daveCon-daveResultSet; if (result NULL || result-data NULL) { return -2; } memcpy(dest, result-data, len); return len; } // 使用示例读取DB10中偏移量为0的4字节一个REAL浮点数 unsigned char rawData[4]; memset(rawData, 0, sizeof(rawData)); int amount readDBData(10, 0, rawData, 4);这段代码并不直接返回数据而是先检查返回值是否为0再从daveCon-daveResultSet里取数据。这是因为libnodave的设计是数据先落到内部缓冲区再通过结果集暴露给用户。在readDBData函数里dbNumber是PLC中实际存在的数据块编号如果PLC里没有DB10调用会返回负值offset是这个DB内部的字节偏移西门子的DB区偏移是从0开始的这与M区的字节寻址方式一致。M区的读写方式与DB区稍有不同M区没有块编号只有直接地址。下面的代码展示如何读取M区中的16位整数和写入单个位// 读取M区偏移100开始的2字节并解释为16位整数 unsigned char mraw[2]; daveReadBytes(daveCon, daveFlags::daveInputs, // 注意这里不是输入区 daveMem, 100, 2, NULL, mraw); // daveMem表示M区标识 short mValue 0; memcpy(mValue, mraw, sizeof(short)); mValue (short)ntohs((u_short)mValue); // 字节序调整 // 写入M区第5个字节的第2位M5.2 daveWriteBits(daveCon, daveMem, 5, 0x02, 1); // 1表示ROOM(位状态)4.3 I区和Q区的直接控制I区输入和Q区输出的读写是C上位机中干扰性最强的操作——因为当你的程序在PC上向Q区写入数值时PLC物理输出模块的端子会立即产生电平变化对应的继电器会吸合。所以在代码层面有一段必须考虑数据转换的函数。// 写入Q区的特定字节 void writeOutputByte(int byteOffset, unsigned char value) { int res daveWriteBytes(daveCon, daveOutputs, byteOffset, 1, value); if (res ! 0) { // 写入失败检查是否被PlC写保护 return; } // 注意Q区的字节偏移与实际端子对应关系 // S7-300的Q0.0对应byteOffset0的bit0位 }这一段明确说明了操作逻辑中哪个字节对应哪个端子的问题byteOffset0对应Q0.0~Q0.7byteOffset1对应Q1.0~Q1.7。尤其要注意send、上电或系统复位后PLC会重新加载OB100/OB101中的初始化逻辑你在Q区写入的值可能被覆盖掉这是正常行为不是通信错误。4.4 读写的字节序问题最坑没经验必踩西门子PLC的S7协议默认采用大端字节序Motorola而Windows的x86/x64机器是小端字节序Intel。不做转换直接读取会把DB10.DBW0里的数值2600x0104显示为0x0401对应的1029。这个差异在高位字节包含非0长度时特别隐蔽。处理方式有两种第一种是使用ntohs/ntohl系列函数转换第二种是在PLC里把数据格式先转成实数或双整数再按字节拷贝。DaveMfc工程的网盘源码中有很多地方直接调用libnodave的bufToDint、bufToFloat等封装建议优先使用这些函数而不是自己用memcpy加位运算因为封装的这段代码逻辑已经天然处理了字节序差异。4.4.1 用数据可视化方式校验字节序是否正确写一个16位整数0x1234到DB1.DBW0再用PLC的变量监控表查看。如果显示为0x1234表示字节序一致如果显示0x3412则说明C侧多转了一次。通过这种方式用10分钟就能建立正确的处理流程。5. 编译配置、字节对齐与C回调现场最容易出问题的三件事5.1 把libnodave源码编进当前工程时的编辑器与编译器检查如果你不是直接使用DaveMfc这个老工程而是把libnodave的C源码复制到自己的新项目中必须先确认编译器对齐规则。因为S7协议的数据包是紧凑排列的如果你的工程使用了#pragma pack(8)或设置了struct默认对齐libnodave内部的daveResultSet等结构体解析会错位。这里推荐三个保险动作一是在包含dave.h之前强制定义#pragma pack(1)并在包含后恢复默认对齐二是在编译器命令行中不启用/Zp相关选项让结构体使用1字节对齐三是如果你需要修改协议头部字段建议用memcpy逐字段拷贝到局部变量而不是直接对结构体取成员地址。// 安全包含方式 #pragma pack(push, 1) #include dave.h #pragma pack(pop)这个写法在视觉层面就避免了因默认对齐导致的隐藏bug。注意到在项目中由于源码历史原因经常出现#pragma pack(1)在dave.h内部已经有过定义如果外部再次定义可能会引发警告但通常在/W3级别下不会报错。5.2 C回调函数与PLC数据更新的线程模型最稳妥的做法是MFC对话框里使用SetTimer(1, 100, NULL)在ON_WM_TIMER中调用一个非阻塞的读取函数读取完毕后直接更新控件。为什么不用单独的通信线程加WaitForSingleObject因为很多PLC的S7连接在同一时间内只允许极少数并发的PDU请求如果你开了两个线程同时发读写请求libnodave内部没有互斥锁结果就是数据包交错解析出来的结果全是乱的。在这个线程处理中还应该注意daveReadBytes返回时表示数据已经从PLC进入到PC网卡的接收缓冲区并拷贝到daveIf-buffer里了但在daveResultSet里的指针生命周期到下一次调用之前是有效的所以如果你需要跨两次循环使用数据务必先用memcpy存进自己的缓冲区。5.3 协议连接重置与S7-300的MPI地址限制当你连续读写几百次之后突然出现连接卡死多半不是因为网络而是因为PLC侧的S7连接资源被占满了。S7-300 CPU的PG/OP通道数量通常是1个或2个如果你的监控软件如博图在线监控、WinCC已经在占用一条连接这时C程序的连接请求就会被拒绝返回的错误是0x8301或0x8104但libnodave不一定把这个错误码打印出来它只返回一个负值或者-1。排查方式也很直接把所有西门子软件全部退出只保留你的C程序重新运行。如果恢复正常说明是连接资源竞争问题而非程序bug。另一种规避方案是在C程序里加入自动重连逻辑检测到返回值为ECONNRESET或-1时就调用daveDisconnectPLC、closeSocket然后间隔5秒重试。注意单次重试间隔不能太短因为PLC侧释放资源需要时间秒级重试会加剧资源碎片化。5.4 验证代码正确性用PLC模拟器代替实体硬件调试在没有硬件PLC的调试环境下可以使用S7-PLCSIM或带有S7模拟功能的OpenPLC软PLC做数据应回测试。S7-PLCSIM可以从STEP7/TIA Portal中启动它会占用主机的一个虚拟网卡并监听102端口C程序通过127.0.0.1或localhost即可连接。需要确认的是S7-PLCSIM的循环读取接口会生成DB区数据这样就能在不接触物理I/O模块的前提下验证字节序转换和DB偏移计算是否正确。同时在模拟环境下要打开PLC的内存保护设置确认C程序可以不受限制地访问DB块否则也会返回访问错误。另外把libnodave的日志打开是一个很实际的办法在daveInterface结构体中有logger函数指针你可以传一个自定义的打印函数把收发原始报文的hex码打印到控制台这样在看到PLC返回的错误码时可以直接定位到S7协议层交互过程中的哪一步失败。可以参照libnodave源码包自带的davemonitor示例程序它就是通过这种方式显示S7协议帧内容的。本文还有配套的精品资源点击获取