3、从站扫描:ec_config()函数详解、从站信息读取、PDO映射解析
好,咱们接着往下走。上一章我们把主站初始化搞定了,网卡也绑好了。但这时候,EtherCAT总线上到底挂了几个从站?它们都是什么型号?支持哪些功能?——这些信息,我们一概不知。
所以这一章的核心任务,就是把总线上的从站“摸清楚”。说白了,就是让主站去挨个敲门,问清楚每个从站的身份和能力。这个动作,在SOEM里就是调用 ec_config() 函数。
3.1 ec_config() 干了什么?
我个人习惯把 ec_config() 叫做“一键扫描”。你想想看,这个函数一调用,SOEM内部会做一连串的事情:
- 发送BRD广播命令:主站先发一个广播帧,问“总线上有没有从站?”
- 自动枚举从站:从站们会依次响应,主站给每个从站分配一个站号(从1开始)。
- 读取从站EEPROM:每个从站都有一个SII(从站信息接口)EEPROM,里面存着厂商ID、产品码、版本号等关键信息。
- 建立从站信息列表:扫描完成后,所有从站的信息会存到
ec_slave[]这个全局数组里。
嗯,这里要注意:ec_config() 默认只做基础扫描。它不会自动帮你配置PDO映射,也不会设置SM(同步管理器)通道。这些事,得我们后面手动干。
核心结论:ec_config() 是主站和从站建立“通讯关系”的第一步。调用成功后,ec_slavecount 变量会告诉你总线上有多少个从站。
3.2 从站信息读取——读懂ec_slave[]
扫描完成后,我们怎么知道每个从站的具体信息?答案就在 ec_slave[] 这个结构体数组里。我截取几个最常用的字段给你看:
// ec_slave[] 结构体关键字段
typedef struct {
uint16_t state; // 从站当前状态
uint16_t eep_man; // 厂商ID
uint16_t eep_id; // 产品码
uint32_t eep_rev; // 版本号
uint16_t eep_build; // 构建号
uint16_t Ibits; // 输入位长度
uint16_t Obits; // 输出位长度
ec_smt sms[EC_MAX_SM]; // 同步管理器配置
ec_pdo pdos[EC_MAX_PDO]; // PDO列表
} ec_slavet;
我在项目中遇到过好几次这样的情况:明明从站已经上电了,但 ec_config() 返回的从站数量却是0。后来排查发现,是网线接触不良,或者从站的E-Bus供电不足。所以,扫描完成后,第一件事就是检查 ec_slavecount 的值。
调试小技巧:你可以写一个简单的循环,把每个从站的厂商ID和产品码打印出来。如果打印出来的值和从站手册对不上,那就要怀疑是不是扫描出错了。
3.3 PDO映射解析——数据交换的“翻译官”
好,现在从站信息读到了。但主站和从站之间到底交换什么数据?是4个字节的模拟量?还是8个字节的数字量?——这就涉及到PDO映射了。
PDO,全称Process Data Object,过程数据对象。你可以把它理解成主站和从站之间交换数据的“快递单”。每个从站会告诉你:
- RxPDO:主站发给从站的数据,比如控制字、目标位置。
- TxPDO:从站发给主站的数据,比如状态字、实际位置。
每个PDO里面又包含若干个PDO条目(Entry),每个条目对应一个具体的参数。比如一个伺服驱动器的TxPDO可能包含:
| 索引 | 子索引 | 名称 | 数据类型 | 位长度 |
|---|---|---|---|---|
| 0x6041 | 0x00 | 状态字 | UNSIGNED16 | 16 |
| 0x6064 | 0x00 | 实际位置 | INTEGER32 | 32 |
| 0x606C | 0x00 | 实际速度 | INTEGER32 | 32 |
你看,这个TxPDO一共占了80位(16+32+32)。主站收到数据后,就要按照这个映射关系去解析每个字节。
我曾经踩过一个坑:某个从站的PDO映射里,条目顺序和手册上写的不一样。手册上写的是“状态字在前,位置在后”,但实际读出来是反的。后来我直接用SOEM的 ec_readPDOmap() 函数去读从站的CoE字典,才发现了真相。所以,永远不要100%相信手册,要以实际读到的映射为准。
3.4 实战:如何读取并打印PDO映射
说了这么多,咱们直接上代码。下面这段代码,可以打印出第一个从站的所有PDO信息:
#include <stdio.h>
#include "ethercat.h"
int main() {
// 初始化并扫描
if (ec_init("eth0") == 0) {
printf("初始化失败\n");
return -1;
}
ec_config();
printf("扫描到 %d 个从站\n", ec_slavecount);
// 读取从站0的PDO映射
ec_readPDOmap(1); // 参数是从站号,从1开始
// 打印PDO信息
ec_slavet *slave = &ec_slave[1]; // 数组下标从1开始
printf("从站 %d 的PDO信息:\n", 1);
for (int i = 0; i < slave->nPDO; i++) {
ec_pdot *pdo = &slave->pdos[i];
printf(" PDO %d: 索引 0x%04X, 长度 %d 位\n",
i, pdo->index, pdo->length);
for (int j = 0; j < pdo->nEntry; j++) {
ec_pdoent *entry = &pdo->entries[j];
printf(" 条目 %d: 索引 0x%04X, 子索引 0x%02X, 长度 %d 位\n",
j, entry->index, entry->subindex, entry->bitlength);
}
}
return 0;
}
这段代码跑起来后,你会看到类似这样的输出:
扫描到 2 个从站
从站 1 的PDO信息:
PDO 0: 索引 0x1600, 长度 64 位
条目 0: 索引 0x6040, 子索引 0x00, 长度 16 位
条目 1: 索引 0x607A, 子索引 0x00, 长度 32 位
条目 2: 索引 0x60FF, 子索引 0x00, 长度 16 位
PDO 1: 索引 0x1A00, 长度 80 位
条目 0: 索引 0x6041, 子索引 0x00, 长度 16 位
条目 1: 索引 0x6064, 子索引 0x00, 长度 32 位
条目 2: 索引 0x606C, 子索引 0x00, 长度 32 位
你看,0x1600是RxPDO(主站发给从站),0x1A00是TxPDO(从站发给主站)。每个条目都对应一个具体的参数,这就是我们后面做数据交换的基础。
3.5 本章知识体系
为了让你更直观地理解这一章的内容,我画了一张流程图:
这张图把整个流程串起来了。你从 ec_config() 开始,一路走到PDO映射解析,每一步都有对应的SOEM函数和数据结构。嗯,说白了,这就是我们后面做多从站协同控制的基础。
我的个人习惯:每次拿到一个新的从站,我都会先跑一遍上面的代码,把PDO映射打印出来,然后和手册对照。这样做的好处是,能提前发现手册和实际固件版本不一致的问题。别问我怎么知道的——都是泪的教训。