3、从站扫描:ec_config()函数详解、从站信息读取、PDO映射解析

好,咱们接着往下走。上一章我们把主站初始化搞定了,网卡也绑好了。但这时候,EtherCAT总线上到底挂了几个从站?它们都是什么型号?支持哪些功能?——这些信息,我们一概不知。

所以这一章的核心任务,就是把总线上的从站“摸清楚”。说白了,就是让主站去挨个敲门,问清楚每个从站的身份和能力。这个动作,在SOEM里就是调用 ec_config() 函数。

3.1 ec_config() 干了什么?

我个人习惯把 ec_config() 叫做“一键扫描”。你想想看,这个函数一调用,SOEM内部会做一连串的事情:

  1. 发送BRD广播命令:主站先发一个广播帧,问“总线上有没有从站?”
  2. 自动枚举从站:从站们会依次响应,主站给每个从站分配一个站号(从1开始)。
  3. 读取从站EEPROM:每个从站都有一个SII(从站信息接口)EEPROM,里面存着厂商ID、产品码、版本号等关键信息。
  4. 建立从站信息列表:扫描完成后,所有从站的信息会存到 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() 调用 发送BRD广播命令 从站自动枚举 分配站号(1~N) 读取SII EEPROM 厂商ID、产品码等 ec_slave[] 数组填充完成 ec_slavecount = 从站数量 读取PDO映射 ec_readPDOmap() 解析PDO条目 索引、子索引、位长度

这张图把整个流程串起来了。你从 ec_config() 开始,一路走到PDO映射解析,每一步都有对应的SOEM函数和数据结构。嗯,说白了,这就是我们后面做多从站协同控制的基础。

我的个人习惯:每次拿到一个新的从站,我都会先跑一遍上面的代码,把PDO映射打印出来,然后和手册对照。这样做的好处是,能提前发现手册和实际固件版本不一致的问题。别问我怎么知道的——都是泪的教训。

专注资料整理