snap7 ------ 西门子 S7 PLC 内部寄存器读写 Demo(C 语言)
结论先说:可行,而且对于「用上位机直接读写西门子 PLC 内部寄存器」这个需求,snap7 是目前最成熟的
开源选择。 它不走 Modbus,而是直接说西门子自己的 S7 协议(ISO-on-TCP / 端口 102) ,所以能直接读写 DB / M / I / Q / T / C 全部内部区域,不用在 PLC 里额外写 Modbus 映射程序。
本目录内容:
| 文件 | 说明 |
|---|---|
snap7.h |
snap7 官方 C 头文件(v1.4.2,从上游仓库取得,未做任何修改) |
snap7_demo.c |
完整可运行的 C 调用 demo(读 + 写 + 多变量 + 位操作) |
Makefile |
本机 / 交叉编译 / Windows 的编译脚本 |
README.md |
本文档 |
一、可行性分析
1.1 snap7 是什么
| 项 | 说明 |
|---|---|
| 来源 | http://snap7.sourceforge.net(作者 Davide Nardella) |
| 语言 | 核心 C/C++,另提供 Pascal / C# / Java / Python / Node.js 薄封装 |
| 许可证 | LGPL v3 ------ 可商用、可闭源发布,只要你动态链接 libsnap7.so/.dll 且不修改库本身 |
| 平台 | Windows / Linux(x86/x64/ARM/MIPS) / BSD / Solaris / macOS |
| 协议 | 西门子 S7 通信协议(ISO-on-TCP,TCP 102 端口) |
| 适用 PLC | S7-200 / 200 SMART / 300 / 400 / 1200 / 1500 / WinAC / LOGO 0BA7 / SINAMICS 驱动 |
1.2 能读到哪些「内部寄存器」
| S7 区域 | 含义 | 常量 | 读写 | 300/400 | 1200/1500 |
|---|---|---|---|---|---|
| DB | 数据块 | S7AreaDB (0x84) |
读/写 | ✅ | ✅(见 3.3 设置) |
| M | 位存储 / 内部标志 | S7AreaMK (0x83) |
读/写 | ✅ | ✅ |
| I (PE) | 过程映像输入 | S7AreaPE (0x81) |
只读 | ✅ | ✅ |
| Q (PA) | 过程映像输出 | S7AreaPA (0x82) |
读/写 | ✅ | ✅ |
| T | 定时器 | S7AreaTM (0x1D) |
读/写 | ✅ | ❌ 不支持 |
| C | 计数器 | S7AreaCT (0x1C) |
读/写 | ✅ | ❌ 不支持 |
还能做:CPU 启停(
Cli_PlcStop/HotStart/ColdStart)、读写 PLC 日期时间、读 SZL 系统状态、上传/下载程序块。这些属于「控制类」操作,正式项目里请谨慎使用。
1.3 需要注意的能力边界
- S7-1200/1500 只支持「基本数据交换」,即只能当 HMI 用:
DB / M / I / Q读写没问题,
但 T/C、目录、块上传下载等功能不可用。 - 库不做任何数据解释:读回来就是裸字节,是否需要大小端转换、怎么切分结构体,由你的代码决定。
- 一个
Cli_Create()出来的客户端对象不是线程安全的 ,多线程请每线程一个客户端,或用Cli_As*异步接口。
二、三步用起来
第 1 步:拿到库
方式 A:下载官方源码包自己编(推荐,能挑版本、能交叉编译)
bash
# 下载 snap7-full-1.4.2.7z (sourceforge: https://sourceforge.net/projects/snap7/files/1.4.2/)
p7zip -d snap7-full-1.4.2.7z
cd snap7-full-1.4.2/build/unix
make -f x86_64_linux.mk all # x86_64 Linux
# make -f arm_v7_linux.mk all # 32 位 ARM(多数 ARM 板)
# make -f aarch64_linux.mk all # 64 位 ARM
# make -f mips_linux.mk all # MIPS
# make -f i386_linux.mk all # 32 位 x86 Linux
sudo make -f x86_64_linux.mk install # 装到 /usr/local/{include,lib}
sudo ldconfig
产物在 snap7-full-1.4.2/build/bin/<arch>-linux/libsnap7.so,头文件在
snap7-full-1.4.2/release/wrappers/c-cpp/snap7.h(本目录已附带同一份)。
方式 B:Windows ------ 官方包里 release/Win32、release/Win64 已经带好了
snap7.dll + snap7.lib,开箱即用。
顺带提一句:官方还提供命令行工具
snap7(snap7 info / read / write / scan),其中
snap7 scan可以扫描局域网里所有 S7 设备并打印型号、机架槽号,用来做「第一天连通性验证」非常好用,比写代码快。
第 2 步:编译本 demo
snap7_demo.cpp
cpp
/*
******************************************************************************
* snap7_demo.c ------ 西门子 S7 系列 PLC「内部寄存器」读写 Demo
*
* 依赖 : Snap7 开源库 (LGPL v3) http://snap7.sourceforge.net
* 官方 C 头文件 snap7.h(本目录已附带,版本 1.4.2)
*
* 编译 : 见同目录 Makefile ------ make
* 交叉编译 ------ make CROSS_COMPILE=arm-linux-gnueabihf-
*
* 用法 : ./snap7_demo <PLC_IP> [rack] [slot] [dbNo] [-w]
* ./snap7_demo 192.168.0.1 0 1 1 # 只读演示(默认,安全)
* ./snap7_demo 192.168.0.1 0 1 1 -w # 追加写演示(会真的写 PLC!)
*
* --------------------------------------------------------------------------
* S7 地址 -> Snap7 参数 对照(本 Demo 的核心知识):
*
* S7 地址 区域常量 DBNumber Start 方式 WordLen
* ----------- ------------- -------- -------------- ----------
* I0.0/IB0 S7AreaPE(0x81) 0 字节偏移=0 S7WLBit/S7WLByte
* Q0.0/QB0 S7AreaPA(0x82) 0 字节偏移=0 S7WLBit/S7WLByte
* M0.0/MB0 S7AreaMK(0x83) 0 字节偏移=0 S7WLBit/S7WLByte
* DB1.DBX0.0 S7AreaDB(0x84) 1 字节偏移=0 S7WLBit
* DB1.DBW0 S7AreaDB(0x84) 1 字节偏移=0 S7WLWord
* DB1.DBD0 S7AreaDB(0x84) 1 字节偏移=0 S7WLDWord
* DB1.DBB0 S7AreaDB(0x84) 1 字节偏移=0 S7WLByte
* T1 S7AreaTM(0x1D) 0 定时器号=1 S7WLTimer
* C1 S7AreaCT(0x1C) 0 计数器号=1 S7WLCounter
*
* 位寻址规律: Start = 字节偏移 * 8 + 位号 (DBX0.3 -> Start=3)
*
* 重要:S7 的 Word / DWord / Real 在总线上是【大端】(Big-Endian),
* 而 x86/ARM 是小端,所以必须在本地做字节序转换(见 be_* 函数)。
* 只有 Byte/Char 数组和位不需要转换。
******************************************************************************
*/
#include <stdint.h> /* 必须放在 snap7.h 之前:让 snap7.h 复用标准 stdint 类型 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "snap7.h"
/* 注意:snap7.h 在 C 模式下自带 typedef int bool; 且 #define true 1; / #define false 0;
* 所以本文件里【不要】再 include <stdbool.h>,也不要使用 true/false 关键字。 */
/*============================================================================
* 1. 大端序工具(S7 的 Word/DWord/Real 均为大端)
*===========================================================================*/
static uint16_t be_get_u16(const uint8_t *p)
{
return (uint16_t)(((uint16_t)p[0] << 8) | p[1]);
}
static void be_set_u16(uint8_t *p, uint16_t v)
{
p[0] = (uint8_t)(v >> 8);
p[1] = (uint8_t)(v & 0xFF);
}
static uint32_t be_get_u32(const uint8_t *p)
{
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) | (uint32_t)p[3];
}
static void be_set_u32(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)(v >> 24);
p[1] = (uint8_t)(v >> 16);
p[2] = (uint8_t)(v >> 8);
p[3] = (uint8_t)(v);
}
static float be_get_real(const uint8_t *p) /* 4 字节 IEEE-754 大端 -> float */
{
uint32_t u = be_get_u32(p);
float f;
memcpy(&f, &u, sizeof(f));
return f;
}
static void be_set_real(uint8_t *p, float f)
{
uint32_t u;
memcpy(&u, &f, sizeof(u));
be_set_u32(p, u);
}
/*============================================================================
* 2. 统一错误检查:Snap7 所有函数返回 0 成功,非 0 为错误码
*===========================================================================*/
static int ok(int rc, const char *what)
{
if (rc == 0) {
printf(" [OK] %s\n", what);
return 0;
}
{
char txt[256];
memset(txt, 0, sizeof(txt));
Cli_ErrorText(rc, txt, (int)sizeof(txt) - 1); /* 把错误码翻成英文描述 */
printf(" [FAIL] %-30s rc=0x%08X %s\n", what, (unsigned)rc, txt);
return -1;
}
}
static void dump_hex(const char *tag, const uint8_t *p, int n)
{
int i;
printf(" %s (%d bytes):", tag, n);
for (i = 0; i < n; ++i) {
if (i % 16 == 0) printf("\n ");
printf("%02X ", p[i]);
}
printf("\n");
}
/*============================================================================
* 3. 各功能演示
*===========================================================================*/
/*--- 3.1 读 CPU 型号 / 运行状态 -------------------------------------------*/
static void demo_cpu_info(S7Object cli)
{
TS7CpuInfo info;
int status = 0;
printf("\n[1] CPU 信息 / 运行状态\n");
memset(&info, 0, sizeof(info));
if (ok(Cli_GetCpuInfo(cli, &info), "Cli_GetCpuInfo") == 0) {
printf(" ModuleType : %s\n", info.ModuleTypeName);
printf(" ModuleName : %s\n", info.ModuleName);
printf(" Serial : %s\n", info.SerialNumber);
}
if (ok(Cli_GetPlcStatus(cli, &status), "Cli_GetPlcStatus") == 0) {
const char *s = (status == S7CpuStatusRun) ? "RUN"
: (status == S7CpuStatusStop) ? "STOP"
: "UNKNOWN";
printf(" PLC Status : %s (0x%02X)\n", s, status);
}
}
/*--- 3.2 读数据块 DB:字节 / 字 / 双字 / 实数 ------------------------------
* 这里用「lean 函数」Cli_DBRead():它固定按字节读,最省事。
* 若想按 Word/Real 语义读,用通用函数 Cli_ReadArea() 并指定 WordLen。
*-------------------------------------------------------------------------*/
static void demo_read_db(S7Object cli, int dbNo)
{
uint8_t buf[64];
printf("\n[2] 读 DB%d(按字节整块读,再用大端工具解析)\n", dbNo);
memset(buf, 0, sizeof(buf));
/* Cli_DBRead(客户端, DB号, 起始字节, 字节数, 接收缓冲区) */
if (ok(Cli_DBRead(cli, dbNo, 0, 32, buf), "Cli_DBRead(0..31)") != 0)
return;
dump_hex("原始数据", buf, 32);
printf(" DB%d.DBW0 = %d (0x%04X)\n", dbNo, (int)be_get_u16(&buf[0]), be_get_u16(&buf[0]));
printf(" DB%d.DBD2 = %d (0x%08X)\n", dbNo, (int)be_get_u32(&buf[2]), be_get_u32(&buf[2]));
printf(" DB%d.DBD6 = %f\n", dbNo, be_get_real(&buf[6]));
/* 等价写法:通用函数按字读,一次读 4 个字(8字节)
uint8_t wbuf[8];
Cli_ReadArea(cli, S7AreaDB, dbNo, 0, 4, S7WLWord, wbuf); */
}
/*--- 3.3 写数据块 DB ------------------------------------------------------*/
static void demo_write_db(S7Object cli, int dbNo)
{
uint8_t w[8];
printf("\n[3] 写 DB%d(演示:DBW0=1234, DBD2=3.14)\n", dbNo);
be_set_u16(&w[0], 1234); /* DBW0 : INT = 1234 */
be_set_real(&w[2], 3.14f); /* DBD2 : REAL = 3.14 */
/* Cli_DBWrite(客户端, DB号, 起始字节, 字节数, 源数据) */
if (ok(Cli_DBWrite(cli, dbNo, 0, 6, w), "Cli_DBWrite(0..5)") != 0)
return;
/* 回读校验 */
{
uint8_t r[6];
memset(r, 0, sizeof(r));
if (ok(Cli_DBRead(cli, dbNo, 0, 6, r), "Cli_DBRead 回读") == 0) {
printf(" 回读 DBW0 = %d\n", (int)be_get_u16(&r[0]));
printf(" 回读 DBD2 = %f\n", be_get_real(&r[2]));
}
}
/* 等价写法:Cli_WriteArea(cli, S7AreaDB, dbNo, 0, 2, S7WLWord, w); */
}
/*--- 3.4 位存储 M 区:位 / 字节 / 字 --------------------------------------*/
static void demo_merker(S7Object cli, int do_write)
{
uint8_t bit = 0;
uint8_t mb = 0;
uint8_t mw[2];
int start_bit = 10 * 8 + 3; /* M10.3 : Start = 字节*8 + 位 */
printf("\n[4] 位存储区 M(S7AreaMK)\n");
/* ---- 读一位:M10.3 ---- */
if (ok(Cli_ReadArea(cli, S7AreaMK, 0, start_bit, 1, S7WLBit, &bit), "读 M10.3") == 0)
printf(" M10.3 = %d\n", bit & 1);
/* ---- 读一个字节:MB20 ---- */
if (ok(Cli_ReadArea(cli, S7AreaMK, 0, 20, 1, S7WLByte, &mb), "读 MB20") == 0)
printf(" MB20 = 0x%02X\n", mb);
/* ---- 读一个字:MW30(大端) ---- */
memset(mw, 0, sizeof(mw));
if (ok(Cli_ReadArea(cli, S7AreaMK, 0, 30, 1, S7WLWord, mw), "读 MW30") == 0)
printf(" MW30 = %d\n", (int)be_get_u16(mw));
if (!do_write)
return;
/* ---- 写一位:M10.3 = 1 ---- */
bit = 1;
if (ok(Cli_ReadArea(cli, S7AreaMK, 0, start_bit, 1, S7WLBit, &bit), "回读 M10.3") == 0)
printf(" (写前 M10.3 = %d)\n", bit & 1);
bit = 1;
ok(Cli_WriteArea(cli, S7AreaMK, 0, start_bit, 1, S7WLBit, &bit), "写 M10.3 = 1");
/* ---- 写一个字节:MB20 = 0x5A ---- */
mb = 0x5A;
ok(Cli_WriteArea(cli, S7AreaMK, 0, 20, 1, S7WLByte, &mb), "写 MB20 = 0x5A");
}
/*--- 3.5 过程映像输入 I / 输出 Q -----------------------------------------*/
static void demo_io(S7Object cli)
{
uint8_t ib = 0, qb = 0, ibit = 0;
printf("\n[5] 过程映像 输入 I(S7AreaPE,只读) / 输出 Q(S7AreaPA)\n");
if (ok(Cli_ReadArea(cli, S7AreaPE, 0, 0, 1, S7WLBit, &ibit), "读 I0.0") == 0)
printf(" I0.0 = %d\n", ibit & 1);
if (ok(Cli_ReadArea(cli, S7AreaPE, 0, 0, 1, S7WLByte, &ib), "读 IB0") == 0)
printf(" IB0 = 0x%02X\n", ib);
if (ok(Cli_ReadArea(cli, S7AreaPA, 0, 0, 1, S7WLByte, &qb), "读 QB0") == 0)
printf(" QB0 = 0x%02X\n", qb);
/* 注意:I(PE) 是只读的,对它调用 Cli_WriteArea 会返回错误码,这是正常的。 */
}
/*--- 3.6 多变量一次性读取(ReadMultiVars,一次网络往返读多个不连续地址) --*/
static void demo_multi_vars(S7Object cli, int dbNo)
{
uint8_t b_dbw[2];
uint8_t b_mw[2];
uint8_t b_i0;
TS7DataItem items[3];
printf("\n[6] 多变量读取 Cli_ReadMultiVars(3 个不连续地址,一次往返)\n");
memset(items, 0, sizeof(items));
memset(b_dbw, 0, sizeof(b_dbw));
memset(b_mw, 0, sizeof(b_mw));
b_i0 = 0;
/* item0 : DB{n}.DBW0 */
items[0].Area = S7AreaDB;
items[0].WordLen = S7WLWord;
items[0].DBNumber = dbNo;
items[0].Start = 0;
items[0].Amount = 1;
items[0].pdata = b_dbw;
/* item1 : MW30 */
items[1].Area = S7AreaMK;
items[1].WordLen = S7WLWord;
items[1].DBNumber = 0;
items[1].Start = 30;
items[1].Amount = 1;
items[1].pdata = b_mw;
/* item2 : I0.0 */
items[2].Area = S7AreaPE;
items[2].WordLen = S7WLBit;
items[2].DBNumber = 0;
items[2].Start = 0;
items[2].Amount = 1;
items[2].pdata = &b_i0;
if (ok(Cli_ReadMultiVars(cli, items, 3), "Cli_ReadMultiVars") == 0) {
printf(" DB%d.DBW0 = %d (item rc=%d)\n", dbNo, (int)be_get_u16(b_dbw), items[0].Result);
printf(" MW30 = %d (item rc=%d)\n", (int)be_get_u16(b_mw), items[1].Result);
printf(" I0.0 = %d (item rc=%d)\n", b_i0 & 1, items[2].Result);
}
}
/*--- 3.7 定时器 / 计数器 --------------------------------------------------*/
static void demo_timer_counter(S7Object cli)
{
uint8_t tv[2], cv[2];
printf("\n[7] 定时器 T / 计数器 C\n");
memset(tv, 0, sizeof(tv));
if (ok(Cli_ReadArea(cli, S7AreaTM, 0, 1, 1, S7WLTimer, tv), "读 T1") == 0)
printf(" T1 = 0x%04X (BCD, 含时基位)\n", be_get_u16(tv));
memset(cv, 0, sizeof(cv));
if (ok(Cli_ReadArea(cli, S7AreaCT, 0, 1, 1, S7WLCounter, cv), "读 C1") == 0)
printf(" C1 = %d\n", (int)be_get_u16(cv));
/* 提醒:S7-1200/1500 不支持 TM/CT 区域,调用会返回错误码,
* 这是协议限制,不是代码问题。 */
}
/*============================================================================
* 4. main
*===========================================================================*/
static void usage(const char *prog)
{
printf("用法: %s <PLC_IP> [rack] [slot] [dbNo] [-w]\n\n", prog);
printf(" PLC_IP 必填,PLC 的 IP,如 192.168.0.1\n");
printf(" rack 机架号,默认 0 (S7-300/400 用 0,槽号 2)\n");
printf(" slot 槽号, 默认 1 (S7-1200/1500 用 0/1)\n");
printf(" dbNo 要读写的 DB 号,默认 1\n");
printf(" -w 打开写演示(默认只读,不会改 PLC)\n\n");
printf("示例:\n");
printf(" %s 192.168.0.1 # S7-1200,只读\n", prog);
printf(" %s 192.168.0.1 0 2 100 -w # S7-300,读写 DB100\n", prog);
}
int main(int argc, char *argv[])
{
const char *ip = NULL;
int rack = 0;
int slot = 1;
int dbNo = 1;
int do_write = 0;
int i, rc, connected = 0;
S7Object cli;
/* ---- 解析参数 ---- */
{
int pos = 0;
for (i = 1; i < argc; ++i) {
if (strcmp(argv[i], "-w") == 0) { do_write = 1; continue; }
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { usage(argv[0]); return 0; }
switch (pos++) {
case 0: ip = argv[i]; break;
case 1: rack = atoi(argv[i]); break;
case 2: slot = atoi(argv[i]); break;
case 3: dbNo = atoi(argv[i]); if (dbNo <= 0) dbNo = 1; break;
default: break;
}
}
}
if (ip == NULL) { usage(argv[0]); return 1; }
printf("=== Snap7 演示 target=%s rack=%d slot=%d db=%d write=%s ===\n",
ip, rack, slot, dbNo, do_write ? "YES" : "NO");
/* ---- 1) 创建客户端对象 ---- */
cli = Cli_Create();
if (cli == 0) {
printf("[FATAL] Cli_Create() 失败(内存不足或库未正确加载)\n");
return 1;
}
/* ---- 2) 可选:设置超时(单位 ms)---- */
{
int t = 3000;
Cli_SetParam(cli, p_i32_PingTimeout, &t);
Cli_SetParam(cli, p_i32_RecvTimeout, &t);
Cli_SetParam(cli, p_i32_SendTimeout, &t);
}
/* ---- 3) 建立连接 ---- */
printf("\n[0] 连接 PLC\n");
rc = Cli_ConnectTo(cli, ip, rack, slot);
if (ok(rc, "Cli_ConnectTo") != 0) {
printf("\n连接失败排查清单:\n");
printf(" * IP / 端口 102 是否可达(ping + telnet %s 102)\n", ip);
printf(" * rack/slot 是否填对(1200/1500=0/1,300/400=0/2)\n");
printf(" * PLC 侧是否勾选 \"Permit access with PUT/GET\"\n");
Cli_Destroy(&cli);
return 1;
}
connected = 1;
/* ---- 4) 依次跑演示 ---- */
demo_cpu_info(cli);
demo_read_db(cli, dbNo);
if (do_write) demo_write_db(cli, dbNo);
demo_merker(cli, do_write);
demo_io(cli);
demo_multi_vars(cli, dbNo);
demo_timer_counter(cli);
/* ---- 5) 断开 & 销毁 ---- */
if (connected)
ok(Cli_Disconnect(cli), "Cli_Disconnect");
Cli_Destroy(&cli); /* 注意:Destroy 接收的是句柄的地址 */
printf("\n=== 演示结束 ===\n");
return 0;
}
bash
make # 默认找 /usr/local 下的 snap7
make SNAP7_DIR=/opt/snap7 # 库装在别的地方
make SNAP7_INC=./inc SNAP7_LIB=./lib # 头/库分别在别的地方
make syntax # 只做语法检查(不需要 snap7 库)
make CROSS_COMPILE=arm-linux-gnueabihf- # 交叉编译(你的 ma35d1/hi3536c 场景)
第 3 步:运行
bash
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH # 免得找不到 .so
./snap7_demo 192.168.0.1 # 只读演示(默认,不会改 PLC)
./snap7_demo 192.168.0.1 0 1 1 # ip rack slot db
./snap7_demo 192.168.0.1 0 2 100 -w # 追加写演示(真写!S7-300 rack0 slot2,DB100)
参数含义:
| 参数 | 默认 | 说明 |
|---|---|---|
PLC_IP |
必填 | PLC 的 IP |
rack |
0 | 机架号。S7-300/400 用 0;S7-1200/1500 用 0 |
slot |
1 | 槽号。S7-1200/1500 = 1;S7-300/400 = 2(填错会连不上) |
dbNo |
1 | 要读写的 DB 号 |
-w |
关 | 打开写演示(默认只读,避免误改现场 PLC) |
程序会依次跑:连接 → CPU 信息/运行状态 → 读 DB → 写 DB(-w)→ M 区位/字节/字 →
I/Q → 多变量读取 → 定时器/计数器 → 断开。
三、地址 → API 参数映射(核心知识)
snap7 的所有读写最终都归到两个通用函数:
c
int Cli_ReadArea (S7Object Client, int Area, int DBNumber, int Start, int Amount, int WordLen, void *pUsrData);
int Cli_WriteArea(S7Object Client, int Area, int DBNumber, int Start, int Amount, int WordLen, void *pUsrData);
| S7 地址 | Area | DBNumber | Start | Amount | WordLen |
|---|---|---|---|---|---|
I0.0 |
S7AreaPE |
0 | 0 | 1 | S7WLBit |
IB0 |
S7AreaPE |
0 | 0 | 1 | S7WLByte |
Q0.0 / QB0 |
S7AreaPA |
0 | 0 | 1 | S7WLBit / S7WLByte |
M10.3 |
S7AreaMK |
0 | 10*8+3 = 83 | 1 | S7WLBit |
MB20 |
S7AreaMK |
0 | 20 | 1 | S7WLByte |
MW30 |
S7AreaMK |
0 | 30 | 1 | S7WLWord |
MD40 |
S7AreaMK |
0 | 40 | 1 | S7WLDWord |
DB1.DBX0.3 |
S7AreaDB |
1 | 3 | 1 | S7WLBit |
DB1.DBB0 |
S7AreaDB |
1 | 0 | 1 | S7WLByte |
DB1.DBW0 |
S7AreaDB |
1 | 0 | 1 | S7WLWord |
DB1.DBD0 |
S7AreaDB |
1 | 0 | 1 | S7WLDWord |
DB1.DBD0(浮点) |
S7AreaDB |
1 | 0 | 1 | S7WLReal |
T1 |
S7AreaTM |
0 | 1 | 1 | S7WLTimer |
C1 |
S7AreaCT |
0 | 1 | 1 | S7WLCounter |
两条口诀:
- 位寻址 :
Start = 字节偏移 × 8 + 位号,WordLen = S7WLBit,Amount = 1。 - 字/双字/实数 :
Start = 字节偏移,Amount = 元素个数,WordLen按类型给。
如果区域在编译期就已知,可以用更省事的「lean 函数」(只按字节算,Size 是字节数):
c
Cli_DBRead (cli, dbNo, startByte, sizeBytes, buf); // DB
Cli_MBRead (cli, startByte, sizeBytes, buf); // M
Cli_EBRead (cli, startByte, sizeBytes, buf); // I
Cli_ABRead (cli, startByte, sizeBytes, buf); // Q
Cli_TMRead (cli, tmNo, amount, buf); // T(注意是数量不是字节)
Cli_CTRead (cli, ctNo, amount, buf); // C
不连续地址一次读 用 Cli_ReadMultiVars(),一趟网络往返搞定,适合轮询多个变量(demo 第 6 段)。
四、必须知道的 8 个坑
坑 1(最容易炸):snap7.h 不能被多个 .c 文件同时 include
snap7.h 里有一大堆文件作用域的 const 常量定义 (S7AreaDB、errIsoConnect 等)。
在 C 语言 里,这些常量是外部链接的,所以:
两个 .c 都
#include "snap7.h"→ 链接时报multiple definition of 'errParBufferTooSmall'等几十条错误。
(本 demo 开发过程中已实测复现。在 C++ 里这些常量是内部链接,不会报错,所以用 C++ 的人很少碰到。)
三种解法,任选:
| 方案 | 做法 | 评价 |
|---|---|---|
| A(推荐) | 只在一个 .c 里 include snap7.h;其它文件自己写 int Cli_DBRead(...) 之类的声明,或封装成 plc_client.c + plc_client.h |
干净、规范 |
| B | 链接时加 -Wl,--allow-multiple-definition |
一行搞定,但属于「掩盖问题」 |
| C | 用 C++ 编译器编译你的项目 | 问题自然消失 |
本 demo 是单文件,不受影响。
坑 2:字节序(大端)
S7 的 Word / DWord / Real 在总线上是大端 ,x86/ARM 是小端。直接 memcpy 到 uint16_t
会得到字节颠倒的值(比如 PLC 里 DBW0 = 1,直接读会变成 256)。
demo 里的 be_get_u16 / be_get_u32 / be_get_real 就是干这个的。
Byte 数组和位不需要转换。
坑 3:S7-1200/1500 必须改两处设置,否则读不到 DB
在 TIA Portal 里:
- DB 块属性 → 取消勾选 "优化的块访问 (Optimized block access)"
- CPU 属性 → 防护与安全 → 连接机制 → 勾选 "允许来自远程对象的 PUT/GET 通信访问" ,
并把访问级别设为 "完全访问"
另外:只能访问全局 DB,且建议 DB 用「绝对地址」访问。
坑 4:rack / slot 填错就连不上
| PLC 系列 | rack | slot |
|---|---|---|
| S7-1200 / S7-1500 | 0 | 1 |
| S7-300 / S7-400 | 0 | 2 |
| S7-200 系列 | 见官方 S7 200 and LOGO 章节(走不同 TSAP) |
现象是「IP 能 ping 通,但 Cli_ConnectTo 返回 0x00010000 (errIsoConnect)」。
坑 5:Windows 下 API 是 __stdcall
snap7.h 检测到 _WIN32 会自动把 OS_WINDOWS 打开,进而 S7API 展开成 __stdcall。
只要你的代码 include 了 snap7.h,调用侧就自动对,但自己写回调函数时记得也带 S7API。
用 MinGW 链接 snap7.dll 时需要从 .def 生成导入库,并必须加 --kill-at:
bash
dlltool -d snap7.def --kill-at -l libsnap7.a -D snap7.dll
或者干脆用 MSVC,官方的 snap7.lib 直接可用。
坑 6:端口 102
S7 走 ISO-on-TCP,固定 TCP 102 端口。现场交换机/防火墙经常把它拦掉,
调试时先 telnet <PLC_IP> 102 确认能通。
坑 7:线程安全
一个 Client 对象同一时刻只能被一个线程使用。多线程方案:
每线程 Cli_Create() 一个独立客户端;或单线程内用 Cli_AsReadArea() 等异步接口。