过程数据
通过 PnSProcessDataInputsMappingRO / PnSProcessDataOutputsMapping 把整幅过程映像映射成结构体,通过 PnSProcessDataIn* / PnSProcessDataOut*、PnSServiceReadBool 这类按偏移 / 按地址的口读写单个值。
过程数据就是控制器与设备之间每周期自动搬运的那段字节:写 Q(设备 → 控制器),读 I(控制器 → 设备)。最省事的用法是把这段字节一次映射成一个 #pragma pack(push, 1) 结构体,之后读写字段就是直接读写过程映像本身 —— 0 拷贝,不用拷贝、不用刷新:读到的永远是当前周期,写下的下个周期发出。
I 区只读:控制器每周期整段覆盖输入映像,写它静默无效(下一拍被冲掉,也不报错)—— 要写过程映像只有 Q 区一条路。Q 区反过来是设备唯一可写方向,改字节即改进程映像,内核每周期原样取走,没有任何提交调用。
零拷贝只有整幅过程映像的映射这一条路,出口两种形态:类型化的结构体映射(PnSProcessDataInputsMappingRO / PnSProcessDataOutputsMapping),以及共享区的原始净荷字节口(pns_shm_i_payload / pns_shm_q_payload,见「共享区直连」)。
除此以外的入口都是拷贝:按偏移取(PnSProcessDataInI16 一族)、按项取(PnSProcessDataItemIn / Out + 项上定宽读写)、按地址取(I0.0 / IW2 / QD4)。它们的好处是不用先建结构体,代价是每次调用都把那一小段字节拷出来再按大端解释。
总线周期由内核完成。事件流报的是连接 / 状态 / 链路 / IO 质量这类运行态(见事件);净荷的变化按你自己的节拍去取(PnSServiceRead / PnSServiceGetIo / 映射视图)。调度(循环 / 定时器 / 线程)由你自己定。
- 必须
#pragma pack(push, 1):过程映像是线上字节流,不能有对齐填充。SDK 只给指针 + 长度,不校验打包与尺寸 —— 没 pack 的结构体会整体错位且不报错,用_Static_assert(sizeof(T) == 打包尺寸)在编译期钉死。 - 判据三缺一即禁止映射:
rc == PNS_OK && *p != NULL && *n >= sizeof(T)。失败时*p = NULL、*n = 0,不会留旧指针。 - 多字节是网络大端(PROFINET 网络序):映射出来的是线上字节,把
int16_t/int32_t/float字段当宿主数值直接用会反序 —— 要宿主数值走按偏移的类型化口(它们替你按大端解码),或解析字节字段。 - 方向:Q 区(
PnSProcessDataOutputsMapping)可写;I 区(PnSProcessDataInputsMappingRO)只读。 - 窗口起点是映射那一次算出来的:I 侧净荷起点 = 先剥掉 2 个 IOPS 前缀,再看净荷开头连续有几个
0x80再剥(最多 4 个)——所以它取决于当次区内容,不是常量;能用的窗口比PnSServiceGetIo回的 I 长少 2–6 字节(Q 侧是固定 +3、不扫描)。判据只看*n,别自己写死偏移。 - 映射从净荷第 0 字节起:要映射中段,在结构体开头放一段占位数组(
uint8_t _skip[k];)顶开;或改用PnSServiceReadArea按偏移取字节。 - 生命周期:映射在会话存续期有效,
PnSServiceClose后失效;Stop只停刷新不卸映射(能读回旧值),但Stop之后新的映射调用一律PNS_ERR_NO_STACK。地址稳定性不在承诺里 —— 生命周期是真的,偏移不是。 - Q 区映射是单 owner:读可并发,写请调用方自行串行化。
GSDML 里那 20 字节的 Input(设备 → 控制器)就是 SDK 的 Q;GSDML 里那 32 字节的 Output(控制器 → 设备)就是 SDK 的 I。
设备是固定单模块(槽 1 / 子槽 1),C 口的函数签名里没有 slot / subslot 参数 —— pns_service_address_t 里的 slot / subslot 恒为 1,不需要调用方填。
使用案例
日常使用
① 结构体映射(0 拷贝,持续用):进数据交换后映射一次,之后当变量读写。
#include "pns.h"
#pragma pack(push, 1)
typedef struct { uint8_t present; int16_t recipe; uint8_t flag; } from_plc_t; /* I:控制器 → 设备 */
typedef struct { uint8_t ready; uint8_t busy; } to_plc_t; /* Q:设备 → 控制器 */
#pragma pack(pop)
_Static_assert(sizeof(from_plc_t) == 4, "缺 #pragma pack(push, 1)");
_Static_assert(sizeof(to_plc_t) == 2, "缺 #pragma pack(push, 1)");
pns_service_handle_t session = NULL;
if (PnSServiceConnectRealtime(&session) != PNS_OK) return 1; /* 实时附着(占用唯一门) */
if (PnSServiceStart(session) != PNS_OK)
{ PnSServiceClose(session); return 1; } /* 启用 IO 设备 */
const uint8_t *i = NULL; volatile uint8_t *q = NULL; size_t ni = 0, nq = 0;
if (PnSProcessDataInputsMappingRO(session, &i, &ni) != PNS_OK || i == NULL || ni < sizeof(from_plc_t))
{ PnSServiceClose(session); return 1; } /* I:只读映射 */
if (PnSProcessDataOutputsMapping(session, &q, &nq) != PNS_OK || q == NULL || nq < sizeof(to_plc_t))
{ PnSServiceClose(session); return 1; } /* Q:可写映射 */
const from_plc_t *inp = (const from_plc_t *)i; /* 映射一次,之后当变量用 */
to_plc_t *outp = (to_plc_t *)q;
outp->ready = 1; /* 写下的下个周期发出,无需提交调用 */
uint8_t present = inp->present; /* 读到的永远是当前周期 */
字段宽度大于 1 字节又想要宿主数值时,别直接读结构体字段(那是大端字节),走按偏移的类型化口:
int16_t recipe = 0;
if (PnSProcessDataInI16(session, 1, &recipe) == PNS_OK) /* 偏移 1:按大端解码成宿主 int16 */
printf("recipe=%d\n", recipe);
② 单次读取(一次性取):要整段字节,或只要一个字段。
uint8_t buf[64]; uint16_t len = sizeof(buf);
PnSServiceRead(session, buf, sizeof(buf), &len); /* I 整段:一次读出,同一拍自洽 */
int16_t recipe = 0;
PnSProcessDataInI16(session, 1, &recipe); /* I:偏移 1 的 int16,大端 */
pns_process_data_item_t item = PnSProcessDataItemIn(session, 0); /* 见 ④:项(下标)形态 */
uint16_t v = 0;
PnSProcessDataItemReadU16(&item, &v); /* 项上读 2 字节,大端 */
字符串字段在过程映像里是定长字节(char 数组),一般不带结尾 0:
typedef struct { char part_no[16]; } from_plc_text_t; /* 定义放进 #pragma pack(push, 1) */
const from_plc_text_t *txt = (const from_plc_text_t *)i; /* i 同 ①,同一份映射 */
char part_no[17];
memcpy(part_no, txt->part_no, 16);
part_no[16] = '\0';
③ 按地址读写(照着工程软件里的地址写):区域字母 + 字节偏移 + 位。
bool done = false;
PnSServiceReadBool(session, "I0.0", &done); /* 读位:控制器 → 设备 */
int16_t recipe = 0;
PnSServiceReadInt16(session, "IW2", &recipe); /* 读字:大端 */
int32_t counter = 0;
PnSServiceReadInt32(session, "ID4", &counter); /* 读双字:大端 */
PnSServiceWriteBool(session, "Q0.0", true); /* 写位(读改写同字节其它位) */
PnSServiceWriteInt16(session, "QW2", 123); /* 写字:大端 */
PnSServiceWriteInt32(session, "QD4", 10000); /* 写双字:大端 */
④ 下标取元素(项形态):项 = 会话 + 偏移 + 方向;在同一坐标上取定宽标量。项是拷贝,不是视图。
pns_process_data_item_t in_item = PnSProcessDataItemIn(session, 1); /* I 区只读项 */
pns_process_data_item_t out_item = PnSProcessDataItemOut(session, 0); /* Q 区可写项 */
int16_t recipe = 0;
if (PnSProcessDataItemReadI16(&in_item, &recipe) == PNS_OK) /* 大端解码 */
printf("recipe=%d\n", recipe);
if (PnSProcessDataItemWriteU16(&out_item, 123) == PNS_OK) /* 当场提交到共享区 */
printf("已写入 Q 偏移 0\n");
if (PnSProcessDataItemWriteU16(&in_item, 1) == PNS_ERR_NOT_SUPPORTED) /* I 项不可写,如实报错 */
printf("I 区只读\n");
周期变化
同一份映射放进你自己的循环直接用(内核每周期搬运,读到的就是当前拍)。
while (running) { /* 你自己的周期任务,节拍自定 */
outp->ready = (uint8_t)(inp->present + 1); /* 读当前拍 → 算 → 写下拍发出 */
Sleep(10); /* 拍间等待自备:Sleep 来自 <windows.h>,本 SDK 头不引入 */
}
映射在循环外取一次仍然是推荐用法:省掉每拍一次解析,内核每周期搬过程数据,不需要任何额外调用。代价要说清:净荷起点是映射那一次算出来的(见上面契约),一旦偏移漂移,你读到的是错位的字节 —— 不抛异常,*n >= sizeof(T) 这种尺寸门也拦不住。所以取值范围自检留在自己手里;要定位稳定的字节,用按偏移 / 按地址的口。Stop 后映射仍可读(读回旧值),PnSServiceClose 后映射失效。
按偏移 / 按项 / 按地址的口每次调用都直接取当前值,所以一次循环里对同一字节的多次取可能落在两个不同的总线周期。要一拍之内多字段互相自洽(例如"这一拍的 present 和 recipe 必须来自同一帧"),用 PnSServiceRead 取整段快照,或直接读 ① 的结构体映射(结构体映射每次读都是当前映像,不需要重取)。
结构体映射(零拷贝)
PnSProcessDataInputsMappingRO()
static inline pns_result_t PnSProcessDataInputsMappingRO(
pns_service_handle_t session,
const uint8_t ** p,
size_t * n);
I 区(控制器 → 设备)的 0 拷贝映射,出参是 const uint8_t ** —— 类型上就把写口封死。它是 I 区唯一推荐的映射入口。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
PNS_ERR_INVALID_PARAM—p或n为NULLPNS_ERR_NO_STACK— 会话未 Start 或已 Stop,文案:服务未 Start 或已 Stop: 过程映像/钉页不可用 (禁止 overlay 未运行的过程映像), 请先 PnSServiceStartPNS_ERR_NATIVE— 过程映像不可用,文案:GlobalIO 未映射, 无法进行过程数据 IO (SDK IO 禁 HTTP);或净荷为空- 失败时
*p = NULL、*n = 0一并写实,不会把上一次的旧指针留给调用方 *n是剥掉 IOPS 前缀后的净荷字节数,比整区长(PnSServiceGetIo回的 I 长)少前缀字节;判据三缺一就不要拿它当结构体用- 起点是映射那一次算出来的(剥 2 个 IOPS,再扫描净荷开头连续的
0x80,最多 4 个):窗口比 I 整区长少 2–6 字节,偏移不是常量。不要把它当固定偏移存起来复用,也不要跨次映射缓存自己算的字节偏移 —— 要稳定偏移的定位就用按偏移 / 按地址的口 - 映射存续期有效:
PnSServiceClose后失效;Stop只停刷新不卸映射,但Stop之后新的映射调用返回PNS_ERR_NO_STACK(旧映射仍指同一页,还能读) - 映射的是线上字节 = 网络大端;任意线程可读
示例:
const uint8_t *i = NULL;
size_t n = 0;
pns_result_t rc = PnSProcessDataInputsMappingRO(session, &i, &n);
if (rc == PNS_OK && i != NULL && n >= sizeof(from_plc_t)) {
const from_plc_t *inp = (const from_plc_t *)i; /* 钉在过程映像上 */
printf("present=%u\n", inp->present); /* 读到的永远是当前周期 */
} else {
printf("映射未就绪: rc=%d (%s)\n", (int)rc, PnSServiceLastError(session));
}
PnSProcessDataOutputsMapping()
pns_result_t PnSProcessDataOutputsMapping(
pns_service_handle_t session,
volatile uint8_t ** p,
size_t * n);
Q 区(设备 → 控制器)的 0 拷贝映射:这是过程映像唯一可写方向,改 *p 指向的字节即改进程映像,内核每周期原样取走(不按脏标记门控),无需任何提交调用。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
PNS_ERR_INVALID_PARAM—p或n为NULLPNS_ERR_NO_STACK— 会话未 Start 或已 Stop,文案:服务未 Start 或已 Stop: 过程映像/钉页不可用 (禁止 overlay 未运行的过程映像), 请先 PnSServiceStartPNS_ERR_NATIVE— 过程映像不可用(GlobalIO 未映射, 无法进行过程数据 IO (SDK IO 禁 HTTP)),或映射自身失败,文案:过程数据输出映像不可用- 失败时
*p = NULL、*n = 0 - 净荷窗口 = 输出区 + PPM 槽 1 固定前缀(+3,是 0 长子模块的 IOPS 状态字节,不是槽 1 净荷,固定 3 字节不扫描),与
PnSServiceWriteArea/PnSProcessDataWriteStruct的写入落点逐字节一致;*n是剥前缀后的净荷长 - 并发契约:读可并发;写请调用方自行串行化;同一映像同一时刻只允许一个可写映射
- 多字节字段写宿主
int16_t/int32_t/float前须自行转大端(或改用PnSProcessDataWriteStruct/ 按偏移的PnSProcessDataOut*一族)
示例:
volatile uint8_t *q = NULL;
size_t n = 0;
pns_result_t rc = PnSProcessDataOutputsMapping(session, &q, &n);
if (rc == PNS_OK && q != NULL && n >= sizeof(to_plc_t)) {
to_plc_t *outp = (to_plc_t *)q;
outp->ready = 1; /* 改字节即改过程映像,下个周期发出 */
outp->busy = 0;
} else {
printf("映射未就绪: rc=%d (%s)\n", (int)rc, PnSServiceLastError(session));
}
PnSProcessDataInputsMapping()
pns_result_t PnSProcessDataInputsMapping(
pns_service_handle_t session,
volatile uint8_t ** p,
size_t * n);
与 PnSProcessDataInputsMappingRO 同一页、同一长度、同一失败语义,区别只在出参类型是 volatile uint8_t **(这是既有签名,保留不动)。只读消费(解码 / 判断 / 转发)请用 RO 版本;拿到 volatile 指针不等于 I 区可写 —— 写它静默无效。
返回值:
pns_result_t— 与PnSProcessDataInputsMappingRO逐条相同
说明:
PNS_ERR_INVALID_PARAM/PNS_ERR_NO_STACK/PNS_ERR_NATIVE的判据与文案同 RO 版本,失败时*p = NULL、*n = 0- 两个版本的指针指向同一页的同一偏移,判据也相同:
rc == PNS_OK && *p != NULL && *n >= sizeof(T)
示例:
volatile uint8_t *raw = NULL;
size_t n = 0;
if (PnSProcessDataInputsMapping(session, &raw, &n) == PNS_OK && raw != NULL && n >= 2) {
uint16_t sw = (uint16_t)(((uint16_t)raw[0] << 8) | (uint16_t)raw[1]); /* 大端自行拆字节 */
printf("sw=0x%04X\n", sw);
}
pns_shm_i_payload() / pns_shm_q_payload()
pns_result_t pns_shm_i_payload(pns_shm_map_t * map, volatile uint8_t ** p, size_t * n);
pns_result_t pns_shm_q_payload(pns_shm_map_t * map, volatile uint8_t ** p, size_t * n);
直连映射句柄上的 0 拷贝净荷映射,与 PnSProcessDataInputsMappingRO / PnSProcessDataOutputsMapping 同语义同落点(剥前缀算法一致),只是不经过服务会话。起点同样按当次区内容剥出来(I 侧剥 2 个 IOPS + 扫描 0x80,窗口比整区少 2–6 字节;Q 侧固定 +3),判据只看 *n。
返回值:
pns_result_t—PNS_OK成功;失败(未映射 / 净荷为空)时*p = NULL、*n = 0,此时禁止映射
说明:
pns_shm_i_payload给 I 区净荷(只读方向,控制器每周期整段覆盖);pns_shm_q_payload给 Q 区净荷(可写方向,改字节即改进程映像,内核不按脏标记门控,无需提交调用)- Q 侧净荷窗口同样带那 3 字节固定前缀口径:与
pns_shm_write_to_plc的落点同一偏移 *n是剥前缀后的净荷字节数,比pns_shm_read_from_plc回的整区长少前缀字节*p在pns_shm_unmap之前有效- 结构体请按此视图映射 —— 映射的是线上字节(PROFINET 网络大端),多字节字段当宿主
int/short用会反序
示例:
#pragma pack(push, 1)
typedef struct { uint8_t present; uint8_t flag; } from_plc_t;
typedef struct { uint8_t ready; uint8_t busy; } to_plc_t;
#pragma pack(pop)
volatile uint8_t *i = NULL, *q = NULL;
size_t ni = 0, nq = 0;
if (pns_shm_i_payload(shm, &i, &ni) == PNS_OK && i != NULL && ni >= sizeof(from_plc_t)
&& pns_shm_q_payload(shm, &q, &nq) == PNS_OK && q != NULL && nq >= sizeof(to_plc_t)) {
const from_plc_t *inp = (const from_plc_t *)i;
to_plc_t *outp = (to_plc_t *)q;
outp->ready = inp->present ? 1 : 0;
}
直接访问(不映射也能用)
不建映射也能取单个值。这一节的口每次调用都拷一段字节出来,不是视图 —— 要"源头变字段自己变"只有上面的整幅映射。
PnSProcessDataIn*() 一族(按偏移读 I)
static inline pns_result_t PnSProcessDataInI16(
pns_service_handle_t session,
uint16_t offset,
int16_t * p_value);
In* 一族:在 I 区(控制器 → 设备)的字节偏移 offset 处读一个定宽标量,多字节一律网络大端解码。偏移即字节偏移,从净荷第 0 字节起算(0 = 映射视图的第 0 字节,不做任何 +1 调整)。
返回值:
pns_result_t—PNS_OK成功,值写进*p_value;其余取值见下
说明:
PNS_ERR_INVALID_PARAM—p_value为NULL;区域越界(offset + 宽度超出 I 区,文案区域越界: 偏移 %u + 长度 %u 超出过程映像区 %u 字节);会话为NULLPNS_ERR_NO_STACK— 会话未 Start 或已 StopPNS_ERR_NATIVE— 过程映像不可用(通道未就绪)- 失败时
*p_value不改写,不会留下半解出来的值 - 不强制 2 / 4 对齐:
offset就是字节偏移,偏移 3 读I16合法(代价是跨了两个字节边界) - 这是拷贝:每次调用都从共享区取当前值,一端是值不是指针
示例:
int16_t recipe = 0;
if (PnSProcessDataInI16(session, 1, &recipe) == PNS_OK)
printf("recipe=%d\n", recipe); /* 偏移 1 起 2 字节,大端 */
else
printf("读失败: %s\n", PnSServiceLastError(session));
相关结构:
/* 读 I(控制器 → 设备):全部 static inline,逐字取自 pns_process_data.h */
PnSProcessDataInI8 (session, offset, int8_t * v); /* 1 字节 有符号 */
PnSProcessDataInU8 (session, offset, uint8_t * v); /* 1 字节 无符号 */
PnSProcessDataInI16(session, offset, int16_t * v); /* 2 字节 有符号 大端 */
PnSProcessDataInU16(session, offset, uint16_t * v); /* 2 字节 无符号 大端 */
PnSProcessDataInI32(session, offset, int32_t * v); /* 4 字节 有符号 大端 */
PnSProcessDataInU32(session, offset, uint32_t * v); /* 4 字节 无符号 大端 */
PnSProcessDataInI64(session, offset, int64_t * v); /* 8 字节 有符号 大端 */
PnSProcessDataInU64(session, offset, uint64_t * v); /* 8 字节 无符号 大端 */
PnSProcessDataInF32(session, offset, float * v); /* 4 字节 IEEE754 大端 */
PnSProcessDataInF64(session, offset, double * v); /* 8 字节 IEEE754 大端 */
PnSProcessDataInBool(session, offset, bool * v); /* 1 字节 按 bit0 判真:0x00=false, 0x01=true, 0x02/0x80=false */
PnSProcessDataInBit (session, offset, int bit, bool * v); /* 1 字节 的第 bit 位,bit 0..7(0 = LSB),越界 INVALID_PARAM */
PnSProcessDataOut*() 一族(按偏移写 Q)
static inline pns_result_t PnSProcessDataOutI16(
pns_service_handle_t session,
uint16_t offset,
int16_t value);
Out* 一族:把值按定宽大端编好,写到 Q 区(设备 → 控制器)的字节偏移 offset 处。数据当场提交到过程映像共享区,返回 PNS_OK ⟺ 字节已在共享区,没有延迟提交、没有提交调用。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
PNS_ERR_INVALID_PARAM— 区域越界(offset + 宽度超出 Q 区净荷窗口,文案区域越界: 偏移 %u + 长度 %u 超出过程映像净荷区 %u 字节);会话为NULLPNS_ERR_NO_STACK— 会话未 Start 或已 StopPNS_ERR_NATIVE— 过程映像不可用- 越界不静默短写:一个字节都没落进去,如实报
PNS_ERR_INVALID_PARAM,不做"部分覆盖 + 报成功" PnSProcessDataOutBit是读改写:先读回同字节,只改目标位,其余位保持原值PnSProcessDataOutBool写true=0x01、false=0x00(整字节,且只产生规范值)
示例:
if (PnSProcessDataOutI16(session, 0, 123) == PNS_OK) /* Q 偏移 0 起 2 字节,大端 */
printf("已提交\n");
PnSProcessDataOutBit(session, 2, 0, true); /* Q 偏移 2 的第 0 位:只改这一位 */
相关结构:
/* 写 Q(设备 → 控制器):全部 static inline,逐字取自 pns_process_data.h */
PnSProcessDataOutI8 (session, offset, int8_t v); /* 1 字节 有符号 */
PnSProcessDataOutU8 (session, offset, uint8_t v); /* 1 字节 无符号 */
PnSProcessDataOutI16(session, offset, int16_t v); /* 2 字节 有符号 大端 */
PnSProcessDataOutU16(session, offset, uint16_t v); /* 2 字节 无符号 大端 */
PnSProcessDataOutI32(session, offset, int32_t v); /* 4 字节 有符号 大端 */
PnSProcessDataOutU32(session, offset, uint32_t v); /* 4 字节 无符号 大端 */
PnSProcessDataOutI64(session, offset, int64_t v); /* 8 字节 有符号 大端 */
PnSProcessDataOutU64(session, offset, uint64_t v); /* 8 字节 无符号 大端 */
PnSProcessDataOutF32(session, offset, float v); /* 4 字节 IEEE754 大端 */
PnSProcessDataOutF64(session, offset, double v); /* 8 字节 IEEE754 大端 */
PnSProcessDataOutBool(session, offset, bool v); /* 1 字节 整字节覆盖:true=0x01 / false=0x00 */
PnSProcessDataOutBit (session, offset, int bit, bool v); /* 读改写同字节第 bit 位(0..7),其余位不变 */
PnSProcessDataCopyInputsTo() / PnSProcessDataCopyToOutputs()
static inline pns_result_t PnSProcessDataCopyInputsTo(
pns_service_handle_t session,
uint8_t * dest,
uint16_t dest_len,
uint16_t * copied);
static inline pns_result_t PnSProcessDataCopyToOutputs(
pns_service_handle_t session,
const uint8_t * src,
uint16_t src_len,
uint16_t * written);
I 区整区 → 你的缓冲(快照拷贝),Q 区从偏移 0 起短写(允许短于整区)。两条都是快照,不是映射。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
CopyInputsTo:copied为NULL→PNS_ERR_INVALID_PARAM;dest传NULL只查长度(*copied回写 I 整区长,返回PNS_OK);dest_len < 需要的长度→PNS_ERR_INVALID_PARAM,*copied已回写所需长度,不半帧拷贝CopyToOutputs:written为NULL、src为NULL或src_len == 0→PNS_ERR_INVALID_PARAM(*written先被清 0);成功时*written = src_len- 两条都会把底层的过程映像判据透出来:会话未 Start / 已 Stop →
PNS_ERR_NO_STACK;通道未就绪 →PNS_ERR_NATIVE - 拷出来的缓冲是你的内存:拷完之后不跟着总线变,可以慢慢解析、跨线程传阅;要"多字段同一拍自洽"时这是首选来源
示例:
uint8_t i_buf[64];
uint16_t copied = 0;
if (PnSProcessDataCopyInputsTo(session, i_buf, sizeof(i_buf), &copied) == PNS_OK)
printf("这一拍 %u 字节\n", copied);
else
printf("需要 %u 字节的缓冲\n", copied); /* 缓冲过小时 copied 是所需长度 */
uint8_t q_head[2] = { 0x01, 0x00 };
uint16_t written = 0;
PnSProcessDataCopyToOutputs(session, q_head, sizeof(q_head), &written); /* 短写 Q 前 2 字节 */
PnSProcessDataReadStruct() / PnSProcessDataWriteStruct()
static inline pns_result_t PnSProcessDataReadStruct(
pns_service_handle_t session,
void * dest,
uint16_t len);
static inline pns_result_t PnSProcessDataWriteStruct(
pns_service_handle_t session,
const void * src,
uint16_t len);
把 I 区前 len 字节拷进一个打包结构体,或把一个打包结构体写到 Q 区前 len 字节。快照拷贝,不是映射 —— 名字里的 Struct 指"打包结构体",不指活视图。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
dest/src为NULL、len == 0→PNS_ERR_INVALID_PARAM- 越界、未 Start、通道未就绪的码与文案同按偏移一族(
区域越界: …、PNS_ERR_NO_STACK、PNS_ERR_NATIVE) - 拷出来的是线上字节:结构体里的多字节字段仍是大端,要宿主数值请自己按大端解码(或用
PnSProcessDataIn*一族) - 写侧是允许短写的整段覆盖:
len可以小于 Q 区整长,只覆盖前len字节
示例:
#pragma pack(push, 1)
typedef struct { uint8_t present; int16_t recipe_be; uint8_t flag; } from_plc_t;
#pragma pack(pop)
from_plc_t snap;
if (PnSProcessDataReadStruct(session, &snap, (uint16_t)sizeof snap) == PNS_OK) {
const uint8_t *rb = (const uint8_t *)&snap.recipe_be;
int16_t recipe = (int16_t)(((uint16_t)rb[0] << 8) | (uint16_t)rb[1]); /* 自行按大端解码 */
printf("present=%u recipe=%d\n", snap.present, recipe);
}
to_plc_t out = { 1, 0 };
PnSProcessDataWriteStruct(session, &out, (uint16_t)sizeof out); /* 覆盖 Q 区前 2 字节 */
PnSProcessDataItemIn() / PnSProcessDataItemOut()
static inline pns_process_data_item_t PnSProcessDataItemIn(pns_service_handle_t session, uint16_t offset);
static inline pns_process_data_item_t PnSProcessDataItemOut(pns_service_handle_t session, uint16_t offset);
取一个"项":把(会话,偏移,方向)三样打包成一个值。In 项是 I 区只读项(控制器 → 设备),Out 项是 Q 区可读影子 + 可写项(设备 → 控制器)。项与 PnSProcessDataInU16(session, offset, …) 同一坐标:字节 0 = 净荷第 0 字节,不做任何 +1 调整。
返回值:
pns_process_data_item_t— 按值返回的项(session/offset/writable),不失败:参数合不合法要等真正读写时才判
说明:
- 取项本身不做任何 IO,也不碰会话;
session传NULL也能取到项,随后读写时如实返回PNS_ERR_INVALID_PARAM - 项不持有过程映像、也不缓存值:每次读都是"读那一刻"的当前值,每次写都是当场提交
- 项是拷贝入口,不是视图(0 拷贝只有整幅映射那一条路)
示例:
pns_process_data_item_t item = PnSProcessDataItemIn(session, 1); /* I 区偏移 1,只读 */
uint16_t v = 0;
if (PnSProcessDataItemReadU16(&item, &v) == PNS_OK)
printf("v=%u\n", v);
相关结构:
typedef struct pns_process_data_item
{
pns_service_handle_t session; /* 会话句柄(可为 NULL,读写时如实报错) */
uint16_t offset; /* 净荷字节偏移:0 = In[0] / Out[0] */
bool writable; /* false = In 项(I 区只读)/ true = Out 项(Q 区可读影子 + 可写) */
} pns_process_data_item_t;
pns_process_data_item_read() / pns_process_data_item_write()
static inline pns_result_t pns_process_data_item_read(const pns_process_data_item_t * item, uint16_t n, uint8_t * buf);
static inline pns_result_t pns_process_data_item_write(const pns_process_data_item_t * item, const uint8_t * buf, uint16_t n);
项上整段读 / 写 n 字节:In 项读 I 区,Out 项读 Q 影子(你最近写进去的那份,不是控制器回执),写仅 Out 项。这是同一族里唯一按字节数工作的两个口(其余都是定宽标量)。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
item/buf为NULL、n == 0→PNS_ERR_INVALID_PARAM(先判参数,再碰会话)- 对 In 项调用写 →
PNS_ERR_NOT_SUPPORTED(I 区由控制器每周期整段覆盖,写它无效,不静默当成功) - 越界、未 Start、通道未就绪:码与文案同底层整区口(
区域越界: …/PNS_ERR_NO_STACK/PNS_ERR_NATIVE) - 写是当场提交,返回
PNS_OK⟺ 字节已在共享区 - 只要按偏移读 Q 影子、不建项时用
pns_process_data_read_q(session, offset, n, buf)(与pns_process_data_read_i同形,区域换成输出区;Out项读回走的就是这一条)
示例:
uint8_t raw[2] = { 0 };
if (pns_process_data_item_read(&item, 2, raw) == PNS_OK)
printf("raw=0x%02X%02X\n", raw[0], raw[1]); /* 大端:高字节在前 */
if (pns_process_data_item_write(&out_item, raw, 2) == PNS_OK) /* Out 项:当场提交 */
printf("Q 偏移 0 起 2 字节已提交\n");
if (pns_process_data_item_write(&item, raw, 2) == PNS_ERR_NOT_SUPPORTED) /* In 项:I 区只读 */
printf("In 项不可写\n");
PnSProcessDataItemRead*() 一族
static inline pns_result_t PnSProcessDataItemReadU16(
const pns_process_data_item_t * item,
uint16_t * p_value);
项上按定宽读一个标量,多字节大端解码,In / Out 项都能读(Out 项读的是 Q 影子,写完当场读回即同值)。
返回值:
pns_result_t—PNS_OK成功,值写进*p_value;其余取值见下
说明:
item或p_value为NULL→PNS_ERR_INVALID_PARAM- 越界(
offset + 宽度超出该方向窗口)→PNS_ERR_INVALID_PARAM,文案区域越界: 偏移 %u + 长度 %u 超出过程映像区 %u 字节 - 会话未 Start / 已 Stop →
PNS_ERR_NO_STACK;通道未就绪 →PNS_ERR_NATIVE - 失败时
*p_value不改写 ReadBoolBit0按 bit0 判真:0x00→false、0x01→true、非规范字节0x02/0x80→false。要按整字节 0 / 非 0 判真假,用PnSProcessDataItemReadU8取原始字节自己比ReadBit的bit取0..7(0 = LSB),越界在碰会话之前就返回PNS_ERR_INVALID_PARAM
示例:
int16_t recipe = 0;
if (PnSProcessDataItemReadI16(&item, &recipe) == PNS_OK)
printf("recipe=%d\n", recipe);
bool flag = false;
if (PnSProcessDataItemReadBoolBit0(&item, &flag) == PNS_OK)
printf("bit0=%s\n", flag ? "true" : "false"); /* 0x02 / 0x80 也是 false */
bool b3 = false;
if (PnSProcessDataItemReadBit(&item, 8, &b3) == PNS_ERR_INVALID_PARAM)
printf("位号必须在 0..7\n");
相关结构:
/* 项上读(In 项读 I / Out 项读 Q 影子):static inline,逐字取自 pns_process_data.h */
PnSProcessDataItemReadU8 (item, uint8_t * v); /* 1 字节 无符号 */
PnSProcessDataItemReadI8 (item, int8_t * v); /* 1 字节 有符号 */
PnSProcessDataItemReadU16(item, uint16_t * v); /* 2 字节 无符号 大端 */
PnSProcessDataItemReadI16(item, int16_t * v); /* 2 字节 有符号 大端 */
PnSProcessDataItemReadU32(item, uint32_t * v); /* 4 字节 无符号 大端 */
PnSProcessDataItemReadI32(item, int32_t * v); /* 4 字节 有符号 大端 */
PnSProcessDataItemReadU64(item, uint64_t * v); /* 8 字节 无符号 大端 */
PnSProcessDataItemReadI64(item, int64_t * v); /* 8 字节 有符号 大端 */
PnSProcessDataItemReadF32(item, float * v); /* 4 字节 IEEE754 大端 */
PnSProcessDataItemReadF64(item, double * v); /* 8 字节 IEEE754 大端 */
PnSProcessDataItemReadBoolBit0(item, bool * v); /* 1 字节 bit0 判真:0x00=false, 0x01=true, 0x02/0x80=false */
PnSProcessDataItemReadBit(item, int bit, bool * v); /* 1 字节 的第 bit 位(0..7,0 = LSB) */
PnSProcessDataItemWrite*() 一族
static inline pns_result_t PnSProcessDataItemWriteU16(
const pns_process_data_item_t * item,
uint16_t value);
项上按定宽写一个标量,多字节大端编码,仅 Out 项:In 项一律返回 PNS_ERR_NOT_SUPPORTED,一个字节都不写。写完当场提交到共享区。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
In项 →PNS_ERR_NOT_SUPPORTED(判方向在碰会话之前,不做无谓读)item为NULL→PNS_ERR_INVALID_PARAM- 越界 →
PNS_ERR_INVALID_PARAM,文案区域越界: 偏移 %u + 长度 %u 超出过程映像净荷区 %u 字节,不静默短写 - 会话未 Start / 已 Stop →
PNS_ERR_NO_STACK;通道未就绪 →PNS_ERR_NATIVE WriteBoolBit0是整字节覆盖,且只产生规范值:true=0x01、false=0x00WriteBit是读改写:只改目标位,同字节其余位保持原值;bit越界 →PNS_ERR_INVALID_PARAM
示例:
if (PnSProcessDataItemWriteU16(&out_item, 123) == PNS_OK)
printf("已提交 Q 偏移 0\n");
uint16_t back = 0;
PnSProcessDataItemReadU16(&out_item, &back); /* Q 影子读回:写完当场读即同值 */
printf("读回 %u\n", back);
if (PnSProcessDataItemWriteBit(&out_item, 3, true) == PNS_OK) /* 只置第 3 位,其余位不动 */
printf("已置第 3 位\n");
相关结构:
/* 项上写(仅 Out 项;In 项 → PNS_ERR_NOT_SUPPORTED):static inline,逐字取自 pns_process_data.h */
PnSProcessDataItemWriteU8 (item, uint8_t v); /* 1 字节 无符号 */
PnSProcessDataItemWriteI8 (item, int8_t v); /* 1 字节 有符号 */
PnSProcessDataItemWriteU16(item, uint16_t v); /* 2 字节 无符号 大端 */
PnSProcessDataItemWriteI16(item, int16_t v); /* 2 字节 有符号 大端 */
PnSProcessDataItemWriteU32(item, uint32_t v); /* 4 字节 无符号 大端 */
PnSProcessDataItemWriteI32(item, int32_t v); /* 4 字节 有符号 大端 */
PnSProcessDataItemWriteU64(item, uint64_t v); /* 8 字节 无符号 大端 */
PnSProcessDataItemWriteI64(item, int64_t v); /* 8 字节 有符号 大端 */
PnSProcessDataItemWriteF32(item, float v); /* 4 字节 IEEE754 大端 */
PnSProcessDataItemWriteF64(item, double v); /* 8 字节 IEEE754 大端 */
PnSProcessDataItemWriteBoolBit0(item, bool v); /* 1 字节 整字节覆盖:true=0x01 / false=0x00 */
PnSProcessDataItemWriteBit(item, int bit, bool v); /* 读改写同字节第 bit 位(0..7),其余位不变 */
按地址读写(区域 + 字节 + 位)
不想自己算偏移、想照着工程软件里的地址写时用这一族:区域字母 + 字节偏移 + 位。
| 地址写法 | 是什么 | 读写口 |
|---|---|---|
| I0.0 / Q0.0 | 位地址:第 0 字节的第 0 位,位号只能 0–7 | PnSServiceReadBool / PnSServiceWriteBool |
| IB0 / QB0 | 字节(区域后可省类型字符:I0 与 IB0 同义) | PnSServiceReadBytes / PnSServiceWriteBytes |
| IW2 / QW2 | 字:2 字节,大端 | PnSServiceReadInt16 / PnSServiceWriteInt16 |
| ID4 / QD4 | 双字:4 字节,大端 | PnSServiceReadInt32 / PnSServiceWriteInt32 |
- 不强制 2 / 4 对齐:
IW3/ID2都合法,偏移就是字节偏移,字 / 双字不会替你补齐。 - 位地址只落在该偏移的这一个字节:位号
0..7(0 = LSB),要跨字节定位就自己把字节偏移加上去。 - 不支持 M 区 / DB 区:
M0.0/DB1.DBW0一律解析失败返回PNS_ERR_INVALID_PARAM,不做映射、不猜。 - I 区只写不支持:写 I 侧返回
PNS_ERR_NOT_SUPPORTED,不静默丢。 - 大小写不敏感(
i0.0/q1.2等价),地址前导空格 / 制表符会被跳过。 - 偏移是十进制,最大
0xFFFF;超过即解析失败。 - 失败时具体文案读
PnSServiceLastError(session)(例如地址越界: 偏移 4 + 长度 2 超出过程映像区 20 字节):错误码只区分大类,越界 / 语法 / 方向这些细节都在文案里。 - Q 区的读回是本地影子值(你最近写进去的那份,不是控制器的回执);I 区的读是控制器发来的当前值。
PnSServiceParseAddress()
pns_result_t PnSServiceParseAddress(
const char * text,
pns_service_address_t * p_out);
只解析地址字符串,不碰过程映像:把 I0.0 / IB0 / IW2 / ID4 / Q… 解析成结构化的区域 + 偏移 + 位 + 宽度。想让自己的代码先校验地址、或者要把地址算成字节偏移时用它。
返回值:
pns_result_t—PNS_OK成功(p_out已填好);PNS_ERR_INVALID_PARAM地址非法
说明:
PNS_ERR_INVALID_PARAM的触发面:text/p_out为NULL;空串;DB开头;区域字母不是I/Q(M也不支持);位号不在0..7;偏移不是十进制或超过0xFFFF;位地址缺偏移(如I.0)- 成功时
slot/subslot恒为 1,bit为-1表示非位寻址;length是宽度,位地址 = 1(跨 1 字节),B/无类型字符 = 1,W= 2,D= 4 - 解析成功不代表一定能读:越界要等真去读写时才报
示例:
pns_service_address_t addr;
if (PnSServiceParseAddress("IW3", &addr) == PNS_OK) {
printf("area=%d offset=%u length=%d bit=%d\n",
(int)addr.area, addr.byte_offset, addr.length, addr.bit); /* 不强制 2 字节对齐 */
}
if (PnSServiceParseAddress("M0.0", &addr) != PNS_OK)
printf("M 区不支持\n");
相关结构:
typedef enum pns_service_area
{
PNS_SERVICE_AREA_UNKNOWN = 0,
PNS_SERVICE_AREA_INPUT = 1, /* I 区: 只读 (控制器 -> 设备) */
PNS_SERVICE_AREA_OUTPUT = 2, /* Q 区: 只写 (设备 -> 控制器) */
PNS_SERVICE_AREA_MEMORY = 3, /* M 区: 不支持 (解析失败, 不映射) */
PNS_SERVICE_AREA_DB = 4, /* DB 区: 不支持 (解析失败, 不映射) */
} pns_service_area_t;
typedef struct pns_service_address
{
pns_service_area_t area; /* 区域 */
uint16_t slot; /* 槽位号 (DB 编号; I/Q/M 区固定 1) */
uint16_t subslot; /* 子槽位号 (固定 1) */
uint16_t byte_offset; /* 字节偏移 */
int bit; /* 位号 (0-7); -1 = 非位寻址 */
int length; /* 数据宽度 (位 = 1 字节跨度, 字节 = 1, 字 = 2, 双字 = 4) */
} pns_service_address_t;
PnSServiceReadBool() / PnSServiceWriteBool()
pns_result_t PnSServiceReadBool(
pns_service_handle_t session,
const char * address,
bool * p_value);
pns_result_t PnSServiceWriteBool(
pns_service_handle_t session,
const char * address,
bool value);
读 / 写位地址(I0.0 / Q0.0)。读 I 区是控制器发来的当前值,读 Q 区读回你最近写入的本机影子值。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
PNS_ERR_INVALID_PARAM—p_value为NULL(读);地址非法(含M/DB、位号越界、缺位号 —— 例如IB0没有位号,读位会报参数非法);地址越界(文案地址越界: 偏移 %u + 长度 %d 超出过程映像区 %u 字节)PNS_ERR_NOT_SUPPORTED— 写 I 侧(I0.0这类)PNS_ERR_NO_STACK— 会话未 Start 或已 StopPNS_ERR_NATIVE— 过程数据通道未就绪- 写是读改写:先读回该字节、只翻目标位、再整字节写回,同字节其它位保持不变
- 读失败时
*p_value先被置false,不会留下旧值
示例:
bool done = false;
if (PnSServiceReadBool(session, "I0.0", &done) == PNS_OK)
printf("done=%d\n", done);
if (PnSServiceWriteBool(session, "Q0.0", true) == PNS_OK)
printf("Q0.0 已置位\n");
if (PnSServiceWriteBool(session, "I0.0", true) == PNS_ERR_NOT_SUPPORTED)
printf("I 区只读\n");
PnSServiceReadInt16() / PnSServiceWriteInt16()
pns_result_t PnSServiceReadInt16(
pns_service_handle_t session,
const char * address,
int16_t * p_value);
pns_result_t PnSServiceWriteInt16(
pns_service_handle_t session,
const char * address,
int16_t value);
按字地址(IW2 / QW2)读 / 写 2 字节整数,大端。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
- 地址必须是
W形式且非位寻址:IB0(1 字节)或I0.0(位地址)一律PNS_ERR_INVALID_PARAM;IW3合法(不强制 2 字节对齐) - 越界(
offset + 2超出该方向)→PNS_ERR_INVALID_PARAM,文案地址越界: 偏移 %u + 长度 %d 超出过程映像区 %u 字节 - 写 I 侧 →
PNS_ERR_NOT_SUPPORTED;未 Start / 已 Stop →PNS_ERR_NO_STACK;通道未就绪 →PNS_ERR_NATIVE - 读失败时
*p_value先被置 0 - 写是整 2 字节覆盖,编码为大端(不在局部做读改写)
示例:
int16_t recipe = 0;
if (PnSServiceReadInt16(session, "IW2", &recipe) == PNS_OK)
printf("recipe=%d\n", recipe);
PnSServiceWriteInt16(session, "QW2", 123); /* 大端落到 Q 偏移 2 */
PnSServiceReadInt32() / PnSServiceWriteInt32()
pns_result_t PnSServiceReadInt32(
pns_service_handle_t session,
const char * address,
int32_t * p_value);
pns_result_t PnSServiceWriteInt32(
pns_service_handle_t session,
const char * address,
int32_t value);
按双字地址(ID4 / QD4)读 / 写 4 字节整数,大端。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
- 地址必须是
D形式且非位寻址:IW2/I0.0一律PNS_ERR_INVALID_PARAM;ID2合法(不强制 4 字节对齐) - 失败码与文案同
PnSServiceReadInt16(地址越界: …/PNS_ERR_NOT_SUPPORTED/PNS_ERR_NO_STACK/PNS_ERR_NATIVE) - 读失败时
*p_value先被置 0
示例:
int32_t counter = 0;
if (PnSServiceReadInt32(session, "ID4", &counter) == PNS_OK)
printf("counter=%d\n", counter);
PnSServiceWriteInt32(session, "QD4", 10000);
PnSServiceReadBytes() / PnSServiceWriteBytes()
pns_result_t PnSServiceReadBytes(
pns_service_handle_t session,
const pns_service_address_t * p_addr,
uint8_t * p_data);
pns_result_t PnSServiceWriteBytes(
pns_service_handle_t session,
const pns_service_address_t * p_addr,
const uint8_t * p_data);
按解析好的地址结构读写原始字节 —— 上面那几个地址化口内部走的就是这两个。要自己拿 PnSServiceParseAddress 的结果做批量判断,或者地址是拼出来的,可以直接用它们。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
- 读:
session/p_addr/p_data为NULL→PNS_ERR_INVALID_PARAM;区域不是I/Q→PNS_ERR_INVALID_PARAM;length不在1..4→PNS_ERR_INVALID_PARAM;越界 →PNS_ERR_INVALID_PARAM,文案地址越界: 偏移 %u + 长度 %d 超出过程映像区 %u 字节 - 读:会话未 Start →
PNS_ERR_NO_STACK(文案服务未 Start 或已 Stop: …);通道未就绪 →PNS_ERR_NATIVE - 写:区域为
I→PNS_ERR_NOT_SUPPORTED;区域不是Q→PNS_ERR_INVALID_PARAM;未 Start →PNS_ERR_NO_STACK;length不在1..4→PNS_ERR_INVALID_PARAM - 写越界按净荷窗口核对,文案
地址越界: 偏移 %u + 长度 %d 超出过程映像净荷区 %u 字节,不做静默短写冒充成功 - 两条都不会替你判地址语义:
p_addr里area直接决定读哪边写哪边,别拿pns_service_address_t手工拼出area = PNS_SERVICE_AREA_MEMORY再用
示例:
pns_service_address_t addr;
uint8_t word[2] = { 0 };
if (PnSServiceParseAddress("QW2", &addr) == PNS_OK
&& PnSServiceReadBytes(session, &addr, word) == PNS_OK) /* Q 影子读回 */
printf("QW2 当前值 = 0x%02X%02X\n", word[0], word[1]);
word[0] = 0x00; word[1] = 0x7B; /* 123,大端 */
PnSServiceWriteBytes(session, &addr, word);
整区入口(整段 / 短写)
PnSServiceRead()
pns_result_t PnSServiceRead(
pns_service_handle_t session,
uint8_t * p_data,
uint16_t cap,
uint16_t * p_len);
读 I 区整段(控制器 → 设备)到你的缓冲 —— 一次调用给出同一拍自洽的一份字节。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
p_len为NULL→PNS_ERR_INVALID_PARAMp_data传NULL/cap传0只查长度:*p_len回写 I 整区长,返回PNS_OK- 缓冲过小 →
PNS_ERR_INVALID_PARAM且*p_len回写所需长度,不半帧拷贝 - 会话未 Start / 已 Stop →
PNS_ERR_NO_STACK;通道未就绪 →PNS_ERR_NATIVE - 长度是该方向的整区长,比映射出来的净荷长多前缀字节(映射那侧的
*n已剥前缀);长度由运行配置决定,不要写死 20 / 32
示例:
uint8_t i_buf[64];
uint16_t n = sizeof(i_buf);
if (PnSServiceRead(session, i_buf, sizeof(i_buf), &n) == PNS_OK)
printf("这一拍 %u 字节\n", n);
else
printf("需要 %u 字节的缓冲\n", n); /* 长度已回写 */
uint16_t need = 0;
PnSServiceRead(session, NULL, 0, &need); /* 只查长度 */
PnSServiceWrite()
pns_result_t PnSServiceWrite(
pns_service_handle_t session,
const uint8_t * p_data,
uint16_t length);
从偏移 0 起短写 Q 区(设备 → 控制器):length 允许短于整区,只覆盖前 length 字节。当场提交到共享区,返回 PNS_OK ⟺ 字节已在共享区。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
p_data为NULL或length == 0→PNS_ERR_INVALID_PARAM(空数据不当成功)- 越界(
length超出 Q 区净荷窗口)→PNS_ERR_INVALID_PARAM,文案区域越界: 偏移 0 + 长度 %u 超出过程映像净荷区 %u 字节 - 会话未 Start / 已 Stop →
PNS_ERR_NO_STACK;通道未就绪 →PNS_ERR_NATIVE - 短写只覆盖前缀,不动的尾部字节保持原值(不是整区清零)
示例:
uint8_t q[2] = { 0x01, 0x00 };
if (PnSServiceWrite(session, q, sizeof(q)) == PNS_OK)
printf("Q 前 2 字节已提交\n");
PnSServiceReadArea()
pns_result_t PnSServiceReadArea(
pns_service_handle_t session,
pns_service_area_t area,
uint16_t offset,
uint16_t length,
uint16_t * p_len, uint8_t * p_data);
按区域 + 偏移 + 长度整段读:PNS_SERVICE_AREA_INPUT 读 I,PNS_SERVICE_AREA_OUTPUT 读 Q 影子。不知道净荷有多长、只想要中间一段时用它。
返回值:
pns_result_t—PNS_OK成功,*p_len= 实际字节数;其余取值见下
说明:
session/p_len/p_data为NULL→PNS_ERR_INVALID_PARAMarea不是PNS_SERVICE_AREA_INPUT/PNS_SERVICE_AREA_OUTPUT(含M/DB)→PNS_ERR_INVALID_PARAMlength == 0→*p_len = 0且返回PNS_OK(空读直接成功)- 会话未 Start / 已 Stop →
PNS_ERR_NO_STACK - 越界 →
PNS_ERR_INVALID_PARAM,文案区域越界: 偏移 %u + 长度 %u 超出过程映像区 %u 字节,*p_len置 0 - 缓冲过小 →
PNS_ERR_INVALID_PARAM,*p_len回写所需长度(不半帧拷贝) - 每次直读共享区当前值
示例:
uint8_t part[2]; uint16_t pn = sizeof(part);
if (PnSServiceReadArea(session, PNS_SERVICE_AREA_INPUT, 4, 2, &pn, part) == PNS_OK)
printf("I 偏移 4 起 2 字节:0x%02X%02X\n", part[0], part[1]);
PnSServiceWriteArea()
pns_result_t PnSServiceWriteArea(
pns_service_handle_t session,
pns_service_area_t area,
uint16_t offset,
const uint8_t * p_data,
uint16_t length);
按区域 + 偏移 + 长度部分覆盖写 Q 区。这是"我自己合并好了一段、一次写进去"的入口。当场提交,返回 PNS_OK ⟺ 字节已在共享区。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
- 写 I 区(
PNS_SERVICE_AREA_INPUT)→PNS_ERR_NOT_SUPPORTED(I 区只读) area不是PNS_SERVICE_AREA_INPUT/PNS_SERVICE_AREA_OUTPUT→PNS_ERR_INVALID_PARAM(M/DB不支持)session/p_data为NULL→PNS_ERR_INVALID_PARAMlength == 0→PNS_ERR_INVALID_PARAM(空数据)- 会话未 Start / 已 Stop →
PNS_ERR_NO_STACK - 越界 →
PNS_ERR_INVALID_PARAM,文案区域越界: 偏移 %u + 长度 %u 超出过程映像净荷区 %u 字节;不靠底层截断兜底(截断 + 报成功会让人以为整段已落共享区) - 一次只写一段连续区域;不相邻的改动请自己合并成一次写,或分多次调(每次调用都是一次提交)
示例:
uint8_t q[2] = { 0x00, 0x7B };
if (PnSServiceWriteArea(session, PNS_SERVICE_AREA_OUTPUT, 2, q, sizeof(q)) == PNS_OK)
printf("Q 偏移 2 起 2 字节已提交\n");
if (PnSServiceWriteArea(session, PNS_SERVICE_AREA_INPUT, 0, q, sizeof(q)) == PNS_ERR_NOT_SUPPORTED)
printf("I 区只读\n");
PnSServiceGetIo()
pns_result_t PnSServiceGetIo(
pns_service_handle_t session,
uint16_t * p_input_len, uint8_t * p_input,
uint16_t * p_output_len, uint8_t * p_output);
一次把两个方向都取回本机缓冲。不想分两次调用时用它,也可以只取长度。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
- 出参名与承载区域对着看:第 1 组
p_input_len/p_input装的是服务端输出区 = 地址化 Q(设备 → 控制器);第 3 组p_output_len/p_output装的是服务端输入区 = 地址化 I(控制器 → 设备)。PnSServiceRead读 I 走的就是第 3 组出参 - 两组出参都可传
NULL表示"这一组不要";但只给缓冲不给容量(p_input_len == NULL && p_input != NULL,或输出组同理)→PNS_ERR_INVALID_PARAM p_*_len入 = 缓冲容量,出 = 实际字节数;容量为 0 只查长度- 某一组缓冲过小 →
PNS_ERR_INVALID_PARAM,*p_*_len回写所需长度,文案过程映像 Q 缓冲过小: 需要 %u 字节, 容量 %u/过程映像 I 缓冲过小: 需要 %u 字节, 容量 %u(不半帧拷贝) - 会话未 Start / 已 Stop →
PNS_ERR_NO_STACK;通道未就绪 →PNS_ERR_NATIVE - 长度不要写死 20 / 32
示例:
uint8_t i[64], q[64]; uint16_t ilen = sizeof(i), qlen = sizeof(q);
if (PnSServiceGetIo(session, &qlen, q, &ilen, i) == PNS_OK) /* 第 1 组 = Q,第 3 组 = I */
printf("Q %u 字节 / I %u 字节\n", qlen, ilen);
uint16_t q_need = 0, i_need = 0;
PnSServiceGetIo(session, &q_need, NULL, &i_need, NULL); /* 只查两边长度 */
共享区直连(同一份过程映像的另一条路)
pns_shm_* 一族直接读写过程映像,不必建服务会话。判据(未映射 / 未连接)与失败语义与上面那条路一致,区别在于它多一个"允许短写"的写入行为。
pns_shm_read_from_plc() / pns_shm_read_to_plc_shadow()
pns_result_t pns_shm_read_from_plc(pns_shm_map_t * map,
uint8_t * buf, size_t buf_cap, size_t * p_len);
pns_result_t pns_shm_read_to_plc_shadow(pns_shm_map_t * map,
uint8_t * buf, size_t buf_cap, size_t * p_len);
读输入区(I,控制器 → 设备)与读输出区影子(Q,你最近写进去的那份)。
返回值:
pns_result_t—PNS_OK成功;PNS_ERR_INVALID_PARAM缓冲容量不足(*p_len已回写所需长度);PNS_ERR_NO_STACK未映射
说明:
- 已剥离线上控制字前缀;
pns_shm_read_from_plc返回长度恒 = 输入区长(剥前缀前):净荷前置、尾部补 0(真零数据返回同长全 0,不假装没数据) pns_shm_read_to_plc_shadow与pns_shm_write_to_plc对称:写什么读回什么;输出区尚未就绪时*p_len = 0且返回PNS_OKbuf传NULL只查长度(返回PNS_OK,*p_len仍写回所需长度)- 本族的读是快照,可能与运行时并发更新撕裂;要同一拍自洽就一次读够
示例:
uint8_t i_buf[64]; size_t n = 0;
if (pns_shm_read_from_plc(shm, i_buf, sizeof(i_buf), &n) == PNS_OK)
printf("I %zu 字节\n", n);
size_t qn = 0;
pns_shm_read_to_plc_shadow(shm, NULL, 0, &qn); /* 只查影子长度 */
pns_shm_write_to_plc()
pns_result_t pns_shm_write_to_plc(pns_shm_map_t * map,
uint32_t offset,
const uint8_t * data, size_t len);
从净荷偏移 offset 起写 Q 区(设备 → 控制器)。内部先写数据再置提交标志。
返回值:
pns_result_t—PNS_OK成功(含短写);PNS_ERR_INVALID_PARAM参数非法 /data为NULL/ 偏移越界 / 一个字节都写不进;PNS_ERR_NO_STACK未映射
说明:
- 允许短写:写不下时按可容纳长度截断,仍返回
PNS_OK(实际落盘长度可用pns_shm_read_to_plc_shadow复核)。要越界即报错的口,用PnSServiceWriteArea/PnSProcessDataOut*一族 len == 0时仅置提交标志,不拷数据- 调用方之间同一句柄不保证互斥,多线程写请自行串行化
示例:
uint8_t q[2] = { 0x01, 0x00 };
if (pns_shm_write_to_plc(shm, 0, q, sizeof(q)) == PNS_OK) {
uint8_t back[2]; size_t n = 0;
pns_shm_read_to_plc_shadow(shm, back, sizeof(back), &n); /* 复核实际落盘 */
}
pns_shm_connected() / pns_shm_is_mapped()
int pns_shm_connected(pns_shm_map_t * map);
int pns_shm_is_mapped(const pns_shm_map_t * map);
两个本地判据:映射是否还在,控制器周期数据是否正常。取自运行时看门狗字段,不假绿。
返回值:
int—pns_shm_connected:1= 控制器周期数据正常;0= 未连接 / 安全态 / 未映射int—pns_shm_is_mapped:1= 已映射;0=NULL或未映射
说明:
- 这两个函数不产生 IO,也不阻塞;周期循环里先判
pns_shm_connected再决定用数据还是走安全态 - 未连接时读到的仍是映像里的字节,但它是停止刷新前的旧值,别当当前拍用
示例:
while (running) {
if (!pns_shm_connected(shm)) { /* 未连接 / 安全态 */
Sleep(10); /* 拍间等待自备:Sleep 来自 <windows.h>,本 SDK 头不引入 */
continue;
}
pns_shm_read_from_plc(shm, i_buf, sizeof(i_buf), &n);
/* ... 算 ... */
pns_shm_write_to_plc(shm, 0, q, sizeof(q));
Sleep(10); /* 等待下一拍(同上) */
}
记录索引空间(非周期数据)
非周期数据走记录索引,索引号的空间是分段的:本 SDK 有入口的只有用户区记录与 I&M 两段,这两条路互不覆盖。
| 索引段 | 是什么 | 本 SDK 走哪 |
|---|---|---|
| 0x0000 – 0x7FFF | 用户 / 厂商区:控制器写进来、由设备侧保存的应用参数记录 | PnSServiceGetRecords |
| 0x8000 起 | 标准索引区(期望 / 实际标识、诊断、替代值、端口统计这类标准化索引) | 本 SDK 不提供通用记录读口;诊断走 PnSServiceGetDiag / PnSServiceGetIoStatus(见诊断) |
| 0xAFF0 – 0xAFF4 | I&M 标识:I&M0(0xAFF0)与 I&M1–4(0xAFF1–0xAFF4) | PnSServiceGetIm |
- I&M 不在用户记录口里:用户区记录只收 0x0000–0x7FFF 这一段,I&M(0xAFF0 起)由协议栈自行处理,
PnSServiceGetRecords永远看不到 I&M 索引。要设备标识就走PnSServiceGetIm。 - 记录这条链是只读观测:控制器写给设备的记录由设备侧接收保存,SDK 负责读出来给你看,没有"从 SDK 写记录给控制器"的入口。
- 记录与过程数据无关:写记录不会改变周期映像,读记录也不会。
PnSServiceGetRecords()
pns_result_t PnSServiceGetRecords(
pns_service_handle_t session,
pns_service_user_record_t * p_items,
size_t items_cap,
size_t * p_count);
取用户区记录表(索引 0x0000–0x7FFF 那一段),两段式调用。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
session/p_count为NULL→PNS_ERR_INVALID_PARAM- 只查数量:
p_items传NULL且items_cap必须为0;p_items非NULL而items_cap == 0也非法 - 实际条数多于容量 →
PNS_ERR_INVALID_PARAM,已填充的条目仍然有效,*p_count回写实际条数,文案用户区记录缓冲过小: 实际 %u 条, 容量 %u - 服务不可达 →
PNS_ERR_NO_CONNECTION;服务端错误(非 200)→PNS_ERR_NATIVE。走的是服务控制面请求,不是过程数据通道 - 字段缺失按 0 / 空处理,不虚构;
data_hex超长如实截断(NUL 结尾) - 字段与逐项契约见诊断(本页只定位索引空间)
示例:
size_t count = 0;
PnSServiceGetRecords(session, NULL, 0, &count); /* 先查条数 */
pns_service_user_record_t recs[8];
if (PnSServiceGetRecords(session, recs, 8, &count) == PNS_OK) {
for (size_t k = 0; k < count; k++)
printf("idx=0x%04X slot=%u len=%u\n", recs[k].index, recs[k].slot, recs[k].length);
}
PnSServiceGetIm()
pns_result_t PnSServiceGetIm(
pns_service_handle_t session,
pns_service_im_info_t * p_im);
取设备标识(I&M0 的生效值 + I&M1–I&M3 字段 + 工程标识字段)—— 这就是 I&M 索引段(0xAFF0–0xAFF4)在本 SDK 里的唯一入口。
返回值:
pns_result_t—PNS_OK成功;其余取值见下
说明:
session/p_im为NULL→PNS_ERR_INVALID_PARAM- 服务不可达 →
PNS_ERR_NO_CONNECTION;服务端错误(非 200)→PNS_ERR_NATIVE - I&M1–I&M3 已由原生栈启用并真读(栈运行中
im14_supported = 1、im_supported列出实际启用集):主站没写过时读到的是出厂占位值(I&M1 功能标签Darra、安装位置Darra PnS 20I/32O、I&M2 日期、I&M3 描述),不是空串;I&M4 未启用,im4_signature_hex不承诺有值 - 栈未运行 / I&M1–4 不可读时这些字段为空串、
im14_supported = 0,订货号 / 序列号 / 软硬件版本是原生栈固定默认值镜像 —— 缘由都如实写在im_note里,不虚构能力 - 字段缺失保持零值(空串 / 0),不编;字符串字段 NUL 结尾,超长如实截断
- 字段清单与逐项含义见诊断
示例:
pns_service_im_info_t im;
if (PnSServiceGetIm(session, &im) == PNS_OK) {
printf("vendor=0x%04X device=0x%04X station=%s\n",
im.vendor_id, im.device_id, im.station_name);
printf("I&M: %s (I&M1-4 supported=%d)\n", im.im_supported, im.im14_supported);
}
字节数怎么拿
长度由运行配置决定,不要写死 20 / 32(改过组态就变)。
| 想拿 | 走哪 |
|---|---|
| I 整区长度 | PnSServiceRead(session, NULL, 0, &n)(p_data 传 NULL 只查长度,不拷数据) |
| Q 净荷窗口长度 | PnSServiceGetIo(session, &q_need, NULL, NULL, NULL)(第 1 组出参装的是 Q,已剥 3 字节固定前缀,与 Q 映射的 *n 同口径,不是整区长度) |
| 两边一起取 | PnSServiceGetIo(session, &q_len, q, &i_len, i)(Q 组是净荷窗口、I 组是整区长度),任一出参可传 NULL 只查长度 |
| 映射净荷长度 | 映射出参 *n(已剥 IOPS 前缀,比整区长度少前缀字节) |
| 两个方向的配置长度 | PnSServiceGetDiag 的 input_area_length / output_area_length |
| 共享区直连的长度 | pns_shm_read_from_plc / pns_shm_read_to_plc_shadow 的 *p_len(buf 传 NULL 只查长度) |
判据三缺一就不要拿映射当结构体用:rc == PNS_OK && *p != NULL && *n >= sizeof(T)。
通道未就绪时的行为
过程数据通道(共享区过程映像)没就绪时,各口一律如实报错:
- 会话没 Start / 已经 Stop:
PNS_ERR_NO_STACK,文案服务未 Start 或已 Stop: 过程映像/钉页不可用 (禁止 overlay 未运行的过程映像), 请先 PnSServiceStart - 映像没映射 / 净荷为空:
PNS_ERR_NATIVE,文案GlobalIO 未映射, 无法进行过程数据 IO (SDK IO 禁 HTTP) - 具体文案读
PnSServiceLastError(session)(进程级兜底pns_last_error())
IOPS / IOCS 这类质量字节走服务面的 /api/io,与过程数据是两条独立的路径,见诊断。
PnSProcessDataInBool / PnSProcessDataItemReadBoolBit0 按 bit0 判真:字节 0x00 读 false、0x01 读 true,而 0x02 与 0x80 都读 false —— 这不是"非 0 即真",很多人第一次会踩。要按整字节 0 / 非 0 判真假,用 PnSProcessDataInU8 / PnSProcessDataItemReadU8 取原始字节自己比;本 SDK 不提供第二个"非 0 即真"的布尔口。写侧对称:OutBool / WriteBoolBit0 只产生 0x01 / 0x00。
I 区由控制器每周期整段覆盖,写 I 区的字节静默无效(下一拍被冲掉)。所以:PnSProcessDataInputsMappingRO 给的是 const 指针;PnSServiceWriteArea / PnSServiceWriteBool / PnSProcessDataItemWrite* 碰到 I 侧一律 PNS_ERR_NOT_SUPPORTED。要写过程映像走 Q 区。