/* ecat_native.c — 原型 A:原生 PDO 守护进程(SOEM 直用,无 Python/GIL) * * 目的:验证"换语言"能把 PDO 循环的天花板从 Python 的 ~5kHz 抬到多高、 * 尾抖动能多干净。总线核心收发本来就是 C(pysoem 只是 SOEM 的包装), * 这里把热循环从解释器里拿出来,其余一切不动。 * * 契约:复刻 ecat_server.py 的 3 条热路由(POST /api/motor/state|period|dbg), * 只绑 127.0.0.1、无 CORS —— 与 Python 服务同口径,cadence_diag/rate_probe * 直接 --base http://127.0.0.1:8091 就能打过来。其余路由仍留在 Python。 * * 时间线元组与 Python 完全同构:(t0, ts, tm, tr, te, k) * t0=循环顶 ts=send 前(原生无锁等待,≈t0) tm=send 后 tr=receive 后 * te=循环底(跳拍判定时刻) k=本拍网格跳过数 * 时刻用 QueryPerformanceCounter 秒 —— 与 Python perf_counter 同一时间基 * (实测差 ≤6µs),cadence_diag 的差分口径不变。 */ #include #include #include #include #include #include #include #include "ethercat.h" #define RING 2048 /* 与 Python _cyc_t maxlen 同 */ #define IO_MAP_SIZE 4096 #define STATS_N 400 /* state 里 cyc_us 分位数的样本窗口,同 _cyc_stats(n=400) */ /* ── 计时原语 ── */ static double g_qpc2s; static inline double now_s(void) { LARGE_INTEGER c; QueryPerformanceCounter(&c); return (double)c.QuadPart * g_qpc2s; } static inline void spin_until(double deadline) { while (now_s() < deadline) YieldProcessor(); /* PAUSE:SMT 兄弟友好,不烧功耗 */ } /* ── 共享状态 ── */ typedef struct { double t0, ts, tm, tr, te; int k; } beat_t; static beat_t g_ring[RING]; static int g_ring_i; /* 写指针:仅 PDO 线程动 */ static long long g_total; /* 累计拍数 */ static long long g_skip_total; /* 累计网格跳过拍 */ static CRITICAL_SECTION g_ring_lock; /* 保护 ring/total/skip 的读侧 */ static CRITICAL_SECTION g_cfg_lock; /* 保护 g_period_ms */ static double g_period_ms = 0.25; static volatile LONG g_stop; static volatile LONG g_wkc; static long long g_expected_wkc; static int g_slavecount; static char g_slave_names[16][64]; static uint8 g_iomap[IO_MAP_SIZE]; static ULONGLONG g_pcore_mask; /* --mask,0=不钉 */ /* ── PDO 线程:网格 + 自旋 + 分段计时,逻辑与 _pdo_worker 一一对应 ── */ static DWORD WINAPI pdo_worker(LPVOID arg) { double per, t0, ts, tm, tr, now, te, next_t = 0; int have_next = 0, k, i, wkc; SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); if (g_pcore_mask) SetThreadAffinityMask(GetCurrentThread(), (DWORD_PTR)g_pcore_mask); while (!InterlockedOr(&g_stop, 0)) { t0 = now_s(); EnterCriticalSection(&g_cfg_lock); per = g_period_ms / 1000.0; LeaveCriticalSection(&g_cfg_lock); if (!have_next) { next_t = t0 + per; have_next = 1; } ts = now_s(); /* 原生无锁无填充 → pre≈0 */ ec_send_processdata(); tm = now_s(); wkc = ec_receive_processdata(EC_TIMEOUTRET); tr = now_s(); InterlockedExchange(&g_wkc, wkc); /* 绝对网格截止点 + 超期跳过(不挪锚点),与 Python 同 */ now = now_s(); k = 0; if (next_t <= now) { while (next_t <= now) { next_t += per; k++; } } te = now; EnterCriticalSection(&g_ring_lock); i = g_ring_i; g_ring_i = (i + 1) % RING; g_ring[i].t0 = t0; g_ring[i].ts = ts; g_ring[i].tm = tm; g_ring[i].tr = tr; g_ring[i].te = te; g_ring[i].k = k; g_total++; g_skip_total += k; LeaveCriticalSection(&g_ring_lock); if (k == 0) { /* 没超期才等下一拍;超期直接落下去补 */ spin_until(next_t); next_t += per; } } return 0; } /* ── 分位数(读侧现算,同 _cyc_fresh 的口径)── */ static int cmp_d(const void *a, const void *b) { double x = *(const double *)a, y = *(const double *)b; return (x > y) - (x < y); } static void pct3(const double *v, int n, char *out, size_t osz) { double *s = (double *)malloc(n * sizeof(double)); int i; if (!s) { snprintf(out, osz, "{}"); return; } memcpy(s, v, n * sizeof(double)); qsort(s, n, sizeof(double), cmp_d); snprintf(out, osz, "{\"p50\":%.1f,\"p90\":%.1f,\"max\":%.1f}", s[(int)(0.5 * (n - 1))], s[(int)(0.9 * (n - 1))], s[n - 1]); free(s); } /* 取最近 n 拍(按时间序)。ring 单写者,读侧加锁拷贝。 */ static int recent_beats(beat_t *dst, int n) { int have, start, i; EnterCriticalSection(&g_ring_lock); have = (g_total < RING) ? (int)g_total : RING; if (n > have) n = have; start = (g_ring_i - n + RING * 2) % RING; for (i = 0; i < n; i++) dst[i] = g_ring[(start + i) % RING]; LeaveCriticalSection(&g_ring_lock); return n; } static void build_cyc_us(char *out, size_t osz) { static beat_t b[RING]; static double work[STATS_N], send[STATS_N], rx[STATS_N], tot[STATS_N]; char sw[96], sr[96], st[96], swork[96]; int n = recent_beats(b, RING), i, m = n < STATS_N ? n : STATS_N; if (m < 2) { snprintf(out, osz, "{}"); return; } for (i = 0; i < m; i++) { beat_t *p = &b[n - m + i]; work[i] = (p->te - p->t0) * 1e6; send[i] = (p->tm - p->ts) * 1e6; rx[i] = (p->tr - p->tm) * 1e6; tot[i] = (p->tr - p->t0) * 1e6; } pct3(work, m, swork, sizeof swork); pct3(send, m, sw, sizeof sw); pct3(rx, m, sr, sizeof sr); pct3(tot, m, st, sizeof st); snprintf(out, osz, "\"cyc_us\":{\"n\":%d,\"work\":%s,\"send\":%s,\"rx\":%s,\"total\":%s," "\"late\":0,\"lost\":0,\"gc\":{\"n\":[0,0,0],\"max_us\":[0,0,0],\"pdo\":0}}", m, swork, sw, sr, st); } /* ── 极简 HTTP/1.1(POST-only JSON,thread-per-connection)── */ static int json_num(const char *body, const char *key, double *val) { const char *p = strstr(body, key); if (!p) return 0; p += strlen(key); while (*p && *p != ':') p++; if (*p != ':') return 0; p++; while (*p == ' ' || *p == '\t') p++; char *end; double v = strtod(p, &end); if (end == p) return 0; *val = v; return 1; } static void handle_conn(SOCKET cs) { char *buf = (char *)malloc(65536); int len = 0, i, cl = 0; char *hdr_end, *body, path[128] = ""; char *resp = (char *)malloc(256 * 1024); int rl = 0; char extra[4096]; if (!buf || !resp) { closesocket(cs); free(buf); free(resp); return; } /* 读到头 */ for (;;) { int n = recv(cs, buf + len, 65535 - len, 0); if (n <= 0) goto done; len += n; buf[len] = 0; hdr_end = strstr(buf, "\r\n\r\n"); if (hdr_end) break; } body = hdr_end + 4; { /* Content-Length + 收满 body */ char *p = buf; for (p = strstr(p, "\n"); p && p < hdr_end; p = strstr(p + 1, "\n")) { /* p 停在上一行的 \n 上,下一行从 p+1 开始 */ if (!_strnicmp(p + 1, "content-length:", 15)) { cl = atoi(p + 16); break; } } while ((body + cl) > (buf + len)) { int need = (int)((body + cl) - (buf + len)); int r = recv(cs, buf + len, need > 65535 - len ? 65535 - len : need, 0); if (r <= 0) goto done; len += r; buf[len] = 0; } if (cl > 65535 - (int)(body - buf)) cl = (int)(buf + len - body); body[cl] = 0; } if (sscanf(buf, "%*s %127s", path) != 1) goto done; if (!strcmp(path, "/api/motor/state")) { build_cyc_us(extra, sizeof extra); rl = snprintf(resp, 256 * 1024, "{\"ok\":true,\"log\":[],\"cycles\":%lld,\"period_ms\":%.4f,\"spin\":true," "\"wkc\":%d,\"expected_wkc\":%d,\"slot_period_ms\":%.4f,\"win_bases\":0," "\"queue_depth\":0,\"sent_seq\":0,\"sent_frames\":0,\"ts\":0," "\"head\":{},\"motors\":[", g_total, g_period_ms, (int)g_wkc, (int)g_expected_wkc, g_period_ms); for (i = 0; i < g_slavecount && i < 16; i++) rl += snprintf(resp + rl, 256 * 1024 - rl, "%s{\"name\":\"%s\",\"valid\":true}", i ? "," : "", g_slave_names[i]); rl += snprintf(resp + rl, 256 * 1024 - rl, "],\"bridge\":{},\"xrx\":{},\"rx_batch\":{},%s}", extra); } else if (!strcmp(path, "/api/motor/period")) { double v = -1, old = g_period_ms; if (json_num(body, "period_us", &v) && v > 0) v /= 1000.0; else if (!json_num(body, "period_ms", &v)) v = -1; if (v < 0.0625 || v > 1000.0) { rl = snprintf(resp, 256 * 1024, "{\"ok\":false,\"log\":[\"周期 %s 超出允许范围 [62.5us, 1000ms]\"]}", body); } else { EnterCriticalSection(&g_cfg_lock); g_period_ms = v; LeaveCriticalSection(&g_cfg_lock); rl = snprintf(resp, 256 * 1024, "{\"ok\":true,\"log\":[\"PDO 周期(native) %gms → %gms\"],\"period_ms\":%.4f}", old, v, v); } } else if (!strcmp(path, "/api/motor/dbg")) { static beat_t b[RING]; int n = recent_beats(b, RING); rl = snprintf(resp, 256 * 1024, "{\"ok\":true,\"period_ms\":%.4f,\"spin\":true,\"gc_enabled\":false," "\"cycles\":%lld,\"skip_total\":%lld,\"cyc\":[", g_period_ms, g_total, g_skip_total); for (i = 0; i < n; i++) rl += snprintf(resp + rl, 256 * 1024 - rl, "%s[%.7f,%.7f,%.7f,%.7f,%.7f,%d]", i ? "," : "", b[i].t0, b[i].ts, b[i].tm, b[i].tr, b[i].te, b[i].k); rl += snprintf(resp + rl, 256 * 1024 - rl, "],\"gc_events\":[],\"http\":[]}"); } else if (!strcmp(path, "/") || !strcmp(path, "/api/native/health")) { rl = snprintf(resp, 256 * 1024, "{\"ok\":true,\"name\":\"ecat_native\",\"slaves\":%d,\"wkc\":%d,\"cycles\":%lld}", g_slavecount, (int)g_wkc, g_total); } else { rl = snprintf(resp, 256 * 1024, "{\"ok\":false,\"log\":[\"接口不存在(native 原型只有 state/period/dbg): %s\"]}", path); } { /* 发送 */ char hdr[256]; int hl = snprintf(hdr, sizeof hdr, "HTTP/1.1 200 OK\r\nContent-Type: application/json; charset=utf-8\r\n" "Cache-Control: no-store\r\nContent-Length: %d\r\nConnection: close\r\n\r\n", rl); send(cs, hdr, hl, 0); send(cs, resp, rl, 0); } done: closesocket(cs); free(buf); free(resp); } static DWORD WINAPI conn_thread(LPVOID p) { handle_conn((SOCKET)(intptr_t)p); return 0; } int main(int argc, char **argv) { const char *substr = "I226"; int port = 8091, i; LARGE_INTEGER f; ec_adaptert *ad, *pick = NULL; WSADATA wsa; SOCKET ls; struct sockaddr_in sa; HANDLE th; for (i = 1; i < argc - 0; i++) { if (!strcmp(argv[i], "--substr") && i + 1 < argc) substr = argv[++i]; else if (!strcmp(argv[i], "--port") && i + 1 < argc) port = atoi(argv[++i]); else if (!strcmp(argv[i], "--mask") && i + 1 < argc) g_pcore_mask = strtoull(argv[++i], NULL, 0); } QueryPerformanceFrequency(&f); g_qpc2s = 1.0 / (double)f.QuadPart; InitializeCriticalSection(&g_ring_lock); InitializeCriticalSection(&g_cfg_lock); /* 找网卡(desc/name 含 substr) */ for (ad = ec_find_adapters(); ad; ad = ad->next) { if (strstr(ad->desc, substr) || strstr(ad->name, substr)) { pick = ad; break; } } if (!pick) { fprintf(stderr, "[native] 找不到含 \"%s\" 的网卡\n", substr); return 2; } if (!ec_init(pick->name)) { fprintf(stderr, "[native] ec_init 失败: %s\n", pick->name); return 3; } printf("[native] 打开网卡: %s\n", pick->desc); /* 预清理:广播 INIT|ACK 清掉上一主站(可能被强杀)残留的 AL 错误位,再重枚举。 SSC 的 AL_ControlInd 对"错误位已置 + 请求不含 ACK(bit4)"的迁移直接忽略 (ecatslv.c),这是 native 之前卡 PREOP 的根因 —— 与 Python motor/start 的 预清理同理。config_init 会重写 SM0/SM1,等效"断开重连"。 */ ec_slave[0].state = EC_STATE_INIT | EC_STATE_ACK; ec_writestate(0); ec_statecheck(0, EC_STATE_INIT, 1000000); if (ec_config_init(FALSE) <= 0) { fprintf(stderr, "[native] 枚举从站失败(邮箱楔死需断电重启板子)\n"); ec_close(); return 4; } ec_config_map(&g_iomap); ec_readstate(); /* LRW 的期望 WKC:每从站读输出+1、写输入+1、回读+1 → outputsWKC*2 + inputsWKC (与 pysoem 同式;本机 1 从站 = 1*2+1 = 3,与实测 wkc 一致) */ g_expected_wkc = ec_group[0].outputsWKC * 2 + ec_group[0].inputsWKC; g_slavecount = ec_slavecount; for (i = 1; i <= ec_slavecount && i <= 16; i++) snprintf(g_slave_names[i - 1], 64, "%s", ec_slave[i].name); printf("[native] 枚举 %d 从站, config_map 后 AL=0x%02x, expected_wkc=%lld\n", ec_slavecount, ec_slave[1].state & 0x0F, g_expected_wkc); /* config_map 内部请求 SAFEOP 不带 ACK,残留错误位下会被忽略 → 显式补一次 SAFEOP|ACK 写,确保到 SAFEOP。 */ ec_slave[0].state = EC_STATE_SAFE_OP | EC_STATE_ACK; ec_writestate(0); ec_statecheck(0, EC_STATE_SAFE_OP, 2000000); /* SAFEOP→OP:从站要先收到≥1帧过程数据才 ack OP,且带错误位时只有 OP|ACK 才清错并迁移 → 边发 PD 边带 ACK 重写 OP,每 200ms 一轮,readstate 拿真实态。 进 OP 后再起稳态线程(避免主线程与线程并发操作同一 ecx_port 打架)。 */ for (i = 0; i < 60; i++) { ec_send_processdata(); ec_receive_processdata(EC_TIMEOUTRET); ec_slave[0].state = EC_STATE_OPERATIONAL | EC_STATE_ACK; ec_writestate(0); ec_readstate(); if ((ec_slave[1].state & 0x0F) == EC_STATE_OPERATIONAL) break; Sleep(200); } if ((ec_slave[1].state & 0x0F) != EC_STATE_OPERATIONAL) { fprintf(stderr, "[native] 进 OP 失败(AL=0x%02x code=0x%04x)—— 若反复如此," "可能是上一主站退出后 SSC 邮箱楔死,需断电重启板子\n", ec_slave[1].state & 0x0F, ec_slave[1].ALstatuscode); ec_close(); return 5; } printf("[native] OP 达成: %d 从站\n", ec_slavecount); th = CreateThread(NULL, 0, pdo_worker, NULL, 0, NULL); SetThreadPriority(th, THREAD_PRIORITY_TIME_CRITICAL); WSAStartup(MAKEWORD(2, 2), &wsa); ls = socket(AF_INET, SOCK_STREAM, 0); { int one = 1; setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, (char *)&one, sizeof one); } sa.sin_family = AF_INET; sa.sin_port = htons((u_short)port); sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK); /* 只绑 127.0.0.1,同 Python 口径 */ if (bind(ls, (struct sockaddr *)&sa, sizeof sa) == SOCKET_ERROR) { fprintf(stderr, "[native] bind %d 失败: %d\n", port, WSAGetLastError()); return 6; } listen(ls, 32); printf("[native] 监听 http://127.0.0.1:%d (state/period/dbg)\n", port); for (;;) { SOCKET cs = accept(ls, NULL, NULL); HANDLE t; if (cs == INVALID_SOCKET) continue; t = CreateThread(NULL, 0, conn_thread, (LPVOID)(intptr_t)cs, 0, NULL); if (t) CloseHandle(t); else closesocket(cs); } return 0; }