西门子 S7 PLC 内部寄存器读写 Demo(C 语言)

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

两条口诀:

  1. 位寻址 :Start = 字节偏移 × 8 + 位号,WordLen = S7WLBit,Amount = 1。
  2. 字/双字/实数 :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 里:

  1. DB 块属性 → 取消勾选 "优化的块访问 (Optimized block access)"
  2. 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() 等异步接口。

相关推荐
benchmark_cc1 小时前
量化项目启动时,应该先设计数据接口还是先编写策略?从系统架构角度做出正确选择
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
2601_962885721 小时前
如何用 Python 判断一只股票“追涨”还是“抄底”更有效?
android·开发语言·python
denlaku2 小时前
JDK 15 新特性详解
java·开发语言
滴滴答答哒2 小时前
C# 技巧:优雅实现对象的“局部更新”与“字段合并”
开发语言·c#
灯澜忆梦2 小时前
【面向对象编程c++】| 指针与自定义数据类型
开发语言·c++·算法
Brilliantwxx2 小时前
【Linux】 自主shell编写巩固复习篇
linux·运维·服务器·开发语言
十年Java程序媛2 小时前
Java 多线程基础与锁机制详解|进程线程、锁升级、synchronized 与 Lock 对比
java·开发语言·spring boot
弯_弯2 小时前
FDE 的工程能力画像:数据接入、PoC 与可复用资产的落地口径
java·服务器·开发语言
雾沉川2 小时前
Visual C++运行库合集 v105.0 安装教程
开发语言·c++·dll