【IgH EtherCAT】EtherCAT串口设备驱动程序,主要用于控制Beckhoff EL6002双端口串口模块

这是一个EtherCAT串口设备驱动程序,主要用于控制Beckhoff EL6002双端口串口模块。代码的核心架构包括:

主要组件:

  1. 设备支持

    :支持Beckhoff EL6002和IDS CSI71A串口模块

  2. 双端口管理

    :每个EL6002设备包含两个独立的串口端口

  3. 状态机驱动

    :每个端口使用状态机管理初始化和配置过程

  4. TTY接口集成

    :与Linux TTY子系统集成,提供标准串口接口

核心功能:

  • PDO数据交换

    :通过EtherCAT的PDO(过程数据对象)进行实时数据传输

  • SDO配置管理

    :通过SDO(服务数据对象)异步配置串口参数

  • Toggle位同步

    :使用toggle位机制确保数据传输的可靠性

  • 参数配置

    :支持波特率、数据位、奇偶校验、停止位、RTS/CTS等配置

状态机流程:

  1. REQUEST_INIT

     → 请求初始化

  2. WAIT_FOR_INIT_RESPONSE

     → 等待初始化响应

  3. READY

     → 准备就绪,进行数据传输和配置

  4. SET_RTSCTS/BAUD_RATE/DATA_FRAME

     → 配置相应参数

这个驱动程序实现了工业级的实时串口通信,适用于需要高可靠性和实时性的工业自动化应用场景。

这段代码是一个功能完备的 Linux 内核驱动模块,其核心作用是为 IgH EtherCAT 主站添加对特定串口终端从站(如 Beckhoff EL6002)的支持。它充当了 EtherCAT 硬件和 Linux TTY 子系统之间的桥梁。

核心架构与功能:

  1. 内核模块形态:这是一个 .ko 文件,通过 insmod 加载到内核中运行。它不包含 main() 函数,而是通过 create_serial_devices() 和 free_serial_devices() 等导出函数与主 EtherCAT 内核模块进行交互。

  2. 设备发现与初始化

  • create_serial_devices()

     函数会在 EtherCAT 主站启动时被调用。它会扫描整个 EtherCAT 总线,查找特定厂商ID和产品ID的串口从站(EL6002 等)。

  • 对于每一个找到的串口从站,它会调用 create_el6002_handler() 来创建一个设备实例(一个 el6002_t 结构体),并将其加入一个全局的链表 handlers 中进行管理。

  • 在初始化过程中,它会为从站的每个物理串口(EL6002 有两个)调用 el60xx_port_init()

  • 桥接 EtherCAT 和 TTY

    • el60xx_port_init()

       的核心是调用 ectty_create()。这是 IgH Master 提供的一个关键函数,它会在 Linux 内核中注册一个新的 TTY 设备(例如 /dev/ttyEC0)。

    • 同时,它将一组操作回调函数 el60xx_tty_ops 与这个新的 TTY 设备关联起来。

  • 动态参数配置 (用户空间 -> 内核空间)

    • 当用户空间的应用程序(如 minicom 或自定义程序)打开 /dev/ttyEC0 并使用 ioctl (通过 termios 结构) 设置波特率、数据帧格式、硬件流控等参数时,内核的 TTY 子系统会调用 el60xx_tty_ops 中注册的 el60xx_cflag_changed 回调函数。

    • 这个回调函数将用户的 termios 设置翻译成 EL6002 从站能理解的 SDO 值,并准备通过 SDO 请求发送给从站。

  • 实时数据交换与状态机

    • SDO 配置

      :如果检测到用户请求的串口参数与当前不同,它会发起 SDO 写请求来更新从站的配置(如波特率)。

    • PDO 数据收发

      :通过一套基于“翻转位”(toggle bit)的握手协议与从站进行数据交换。

    • 发送

      :当从站准备好接收时,它从 ectty 的发送缓冲区(由用户空间程序写入)中取出数据,放入 PDO,并翻转“发送请求”位。

    • 接收

      :当检测到从站发来的“接收请求”位翻转时,它从 PDO 中读取数据,并将其推送到 ectty 的接收缓冲区,供用户空间程序读取。

    • 另一个主 EtherCAT 模块会在每个实时周期调用本模块的 run_serial_devices() 函数。

    • 该函数会遍历 handlers 链表,并为每个设备的每个端口调用 el60xx_port_run()

    • el60xx_port_run()

       是一个复杂的状态机,负责处理所有实时任务:

  • 资源清理:当 EtherCAT 主模块被卸载时,会调用 free_serial_devices(),该函数会遍历链表,释放所有 TTY 设备、清理所有配置并释放内核内存。

  • 总结:此内核模块是一个典型的设备驱动程序。它将一个复杂的、专有的硬件(EtherCAT 串口终端)抽象成一个标准的、通用的操作系统接口(Linux TTY 设备)。这使得任何能够操作标准串口的用户空间程序都能够无缝地与底层的 EtherCAT 设备进行通信,极大地提高了系统的易用性和集成性。

    #include <linux/module.h> // 包含 Linux 内核模块编程所需的核心头文件#include <linux/err.h> // 包含内核错误处理相关的头文件,如 IS_ERR, PTR_ERR#include <linux/termios.h> // 包含终端 I/O 结构体和常量定义,用于串口参数设置#include <linux/slab.h> // 包含内核内存分配器(slab allocator)的头文件,如 kmalloc, kfree#include "../../include/ecrt.h" // EtherCAT realtime interface // 包含 IgH EtherCAT 主站的实时接口头文件#include "../../include/ectty.h" // EtherCAT TTY interface // 包含 IgH EtherCAT 的 TTY(串行终端)接口头文件#include "serial.h" // 包含此模块自定义的头文件(可能定义了 run_serial_devices 等函数原型)/****************************************************************************/ // 分隔注释// Optional features // 区域注释:可选特性#define PFX "ec_tty_example: " // 宏定义:内核日志消息的前缀,方便调试#define DEBUG 0 // 宏定义:调试开关,为0时,#if DEBUG 块内的代码不被编译/****************************************************************************/ // 分隔注释#define VendorIdBeckhoff 0x00000002 // 宏定义:倍福(Beckhoff)公司的厂商ID#define ProductCodeBeckhoffEL6002 0x17723052 // 宏定义:倍福 EL6002 产品的产品代码#define VendorIdIds 0x000012ad // 宏定义:IDS 公司的厂商ID#define ProductCodeIdsCSI71A 0x17723052 // 宏定义:IDS CSI71A 产品的产品代码(与EL6002相同,可能是兼容设备)/****************************************************************************/ // 分隔注释typedef enum { // 定义一个枚举类型,表示 EL60xx 串口的状态机    SER_REQUEST_INIT, // 状态:请求初始化    SER_WAIT_FOR_INIT_RESPONSE, // 状态:等待初始化响应    SER_READY, // 状态:准备就绪,可以进行数据收发    SER_SET_RTSCTS, // 状态:正在设置硬件流控 (RTS/CTS)    SER_SET_BAUD_RATE, // 状态:正在设置波特率    SER_SET_DATA_FRAME, // 状态:正在设置数据帧格式} el60xx_port_state; // 枚举类型名#define EL6002_PORT_NAME_SIZE 16 // 宏定义:EL6002 端口名称字符串的最大尺寸typedef struct { // 定义一个结构体,用于管理一个 EL60xx 的串口端口    ec_tty_t *tty; // 指向由 ectty 接口创建的 TTY 对象的指针    char name[EL6002_PORT_NAME_SIZE]; // 端口的名称字符串    size_t max_tx_data_size; // 该端口一次可发送的最大数据字节数    size_t max_rx_data_size; // 该端口一次可接收的最大数据字节数    u8 *tx_data; // 指向发送数据缓冲区的指针    u8 tx_data_size; // 当前要发送的数据大小    el60xx_port_state state; // 当前端口的状态机状态    u8 tx_request_toggle; // 发送请求的翻转位(用于握手)    u8 tx_accepted_toggle; // 发送被接受的翻转位(用于握手)    u8 rx_request_toggle; // 接收请求的翻转位(用于握手)    u8 rx_accepted_toggle; // 接收被接受的翻转位(用于握手)    u16 control; // 将要写入到从站的16位控制字    u32 off_ctrl; // 控制字在过程数据域中的偏移量    u32 off_tx; // 发送数据在过程数据域中的偏移量    u32 off_status; // 状态字在过程数据域中的偏移量    u32 off_rx; // 接收数据在过程数据域中的偏移量    ec_sdo_request_t *rtscts_sdo; // 用于设置 RTS/CTS 的 SDO 请求对象指针    u8 requested_rtscts; // 用户请求的 RTS/CTS 设置    u8 current_rtscts; // 从站当前实际的 RTS/CTS 设置    ec_sdo_request_t *baud_sdo; // 用于设置波特率的 SDO 请求对象指针    u8 requested_baud_rate; // 用户请求的波特率设置    u8 current_baud_rate; // 从站当前实际的波特率设置    ec_sdo_request_t *frame_sdo; // 用于设置数据帧的 SDO 请求对象指针    u8 requested_data_frame; // 用户请求的数据帧设置    u8 current_data_frame; // 从站当前实际的数据帧设置    unsigned int config_error; // 配置错误的标志} el60xx_port_t; // 结构体类型名#define EL6002_PORTS 2 // 宏定义:一个 EL6002 模块有两个串口端口typedef struct { // 定义一个结构体,用于管理一个 EL6002 设备    struct list_head list; // 内核链表头,用于将多个 EL6002 设备串联起来    ec_slave_config_t *sc; // 指向该从站的配置对象的指针    el60xx_port_t port[EL6002_PORTS]; // 包含两个端口管理结构体的数组} el6002_t; // 结构体类型名LIST_HEAD(handlers); // 宏:在内核中定义并初始化一个名为 handlers 的双向链表头/****************************************************************************/ // 分隔注释/* Beckhoff EL6002 // 注释:倍福 EL6002 的详细信息 * Vendor ID:       0x00000002 // 厂商ID * Product code:    0x17723052 // 产品代码 * Revision number: 0x00100000 // 修订号 */// 以下是为 EL6002 从站进行 SII (从站信息接口) 覆盖配置所需的数据结构static const ec_pdo_entry_info_t el6002_pdo_entries[] = { // 定义 EL6002 的所有 PDO 条目信息   {0x7001, 0x01, 16}, /* Ctrl */ // 端口1 控制字   {0x7000, 0x11, 8}, /* Data Out 0 */ // 端口1 输出数据字节0   {0x7000, 0x12, 8}, /* Data Out 1 */ // ...   // ... 大量的数据输出字节定义 ...   {0x7000, 0x26, 8}, /* Data Out 21 */   {0x7011, 0x01, 16}, /* Ctrl */ // 端口2 控制字   {0x7010, 0x11, 8}, /* Data Out 0 */ // 端口2 输出数据字节0   // ... 大量的数据输出字节定义 ...   {0x7010, 0x26, 8}, /* Data Out 21 */   {0x6001, 0x01, 16}, /* Status */ // 端口1 状态字   {0x6000, 0x11, 8}, /* Data In 0 */ // 端口1 输入数据字节0   // ... 大量的数据输入字节定义 ...   {0x6000, 0x26, 8}, /* Data In 21 */   {0x6011, 0x01, 16}, /* Status */ // 端口2 状态字   {0x6010, 0x11, 8}, /* Data In 0 */ // 端口2 输入数据字节0   // ... 大量的数据输入字节定义 ...   {0x6010, 0x26, 8}, /* Data In 21 */};static const ec_pdo_info_t el6002_pdos[] = { // 定义 EL6002 的 PDO 信息 (将条目分组到PDO)   {0x1604, 23, el6002_pdo_entries + 0}, /* COM RxPDO-Map Outputs Ch.1 */ // 端口1 的 RxPDO (23个条目)   {0x1605, 23, el6002_pdo_entries + 23}, /* COM RxPDO-Map Outputs Ch.2 */ // 端口2 的 RxPDO (23个条目)   {0x1a04, 23, el6002_pdo_entries + 46}, /* COM TxPDO-Map Inputs Ch.1 */ // 端口1 的 TxPDO (23个条目)   {0x1a05, 23, el6002_pdo_entries + 69}, /* COM TxPDO-Map Inputs Ch.2 */ // 端口2 的 TxPDO (23个条目)};static const ec_sync_info_t el6002_syncs[] = { // 定义 EL6002 的同步管理器配置   {0, EC_DIR_OUTPUT, 0, NULL, EC_WD_DISABLE}, // SM0 (Outputs)   {1, EC_DIR_INPUT, 0, NULL, EC_WD_DISABLE}, // SM1 (Inputs)   {2, EC_DIR_OUTPUT, 2, el6002_pdos + 0, EC_WD_DISABLE}, // SM2 (Outputs),关联端口1和2的RxPDO   {3, EC_DIR_INPUT, 2, el6002_pdos + 2, EC_WD_DISABLE}, // SM3 (Inputs),关联端口1和2的TxPDO   {0xff} // 结束标志};typedef enum { // 定义一个枚举类型,表示奇偶校验类型    PAR_NONE, // 无校验    PAR_ODD,  // 奇校验    PAR_EVEN  // 偶校验} parity_t; // 枚举类型名typedef struct { // 定义一个结构体,用于映射 EL60xx 的数据帧 SDO 值到具体参数    u8 value; // SDO 值    unsigned int data_bits; // 数据位数    parity_t parity; // 奇偶校验类型    unsigned int stop_bits; // 停止位数} el600x_data_frame_t; // 结构体类型名/** EL600x supported values for data frame SDO. // Doxygen 注释:EL600x 支持的数据帧 SDO 值 */el600x_data_frame_t el600x_data_frame[] = { // 定义一个数组,存储所有支持的数据帧格式    {0x01, 7, PAR_EVEN, 1}, // 7位数据,偶校验,1位停止位    {0x09, 7, PAR_EVEN, 2}, // 7位数据,偶校验,2位停止位    {0x02, 7, PAR_ODD,  1}, // 7位数据,奇校验,1位停止位    {0x0a, 7, PAR_ODD,  2}, // 7位数据,奇校验,2位停止位    {0x03, 8, PAR_NONE, 1}, // 8位数据,无校验,1位停止位    {0x0b, 8, PAR_NONE, 2}, // 8位数据,无校验,2位停止位    {0x04, 8, PAR_EVEN, 1}, // 8位数据,偶校验,1位停止位    {0x0c, 8, PAR_EVEN, 2}, // 8位数据,偶校验,2位停止位    {0x05, 8, PAR_ODD,  1}, // 8位数据,奇校验,1位停止位    {0x0d, 8, PAR_ODD,  2}, // 8位数据,奇校验,2位停止位};typedef struct { // 定义一个结构体,用于映射 EL60xx 的波特率 SDO 值到具体参数    u8 value; // SDO 值    unsigned int baud; // 波特率数值    tcflag_t cbaud; // 对应的 termios c_cflag 中的波特率标志} el600x_baud_rate_t; // 结构体类型名/** EL600x supported values for baud rate SDO. // Doxygen 注释:EL600x 支持的波特率 SDO 值 */el600x_baud_rate_t el600x_baud_rate[] = { // 定义一个数组,存储所有支持的波特率    {1,   300,    B300}, // 300 bps    {2,   600,    B600}, // 600 bps    {3,   1200,   B1200}, // 1200 bps    {4,   2400,   B2400}, // 2400 bps    {5,   4800,   B4800}, // 4800 bps    {6,   9600,   B9600}, // 9600 bps    {7,   19200,  B19200}, // 19200 bps    {8,   38400,  B38400}, // 38400 bps    {9,   57600,  B57600}, // 57600 bps    {10,  115200, B115200} // 115200 bps};/****************************************************************************/ // 分隔注释// 这个函数是 ectty 操作的回调函数,当用户空间程序通过 ioctl 改变 TTY 设备参数时被调用static int el60xx_cflag_changed(void *data, tcflag_t cflag){    el60xx_port_t *port = (el60xx_port_t *) data; // 将 void* 指针强制转换为端口结构体指针    unsigned int data_bits, stop_bits; // 声明变量:数据位、停止位    tcflag_t cbaud, rtscts; // 声明变量:波特率标志、硬件流控标志    parity_t par; // 声明变量:奇偶校验类型    unsigned int i; // 声明循环变量    el600x_baud_rate_t *b_to_use = NULL; // 声明指针,指向将要使用的波特率配置    el600x_data_frame_t *df_to_use = NULL; // 声明指针,指向将要使用的数据帧配置#if DEBUG // 如果 DEBUG 宏为1    printk(KERN_INFO PFX "%s(%s, cflag=%x).\n", __func__, port->name, cflag); // 打印调试信息#endif // 结束 #if    rtscts = cflag & CRTSCTS; // 从 cflag 中提取硬件流控(RTS/CTS)标志    printk(KERN_INFO PFX "%s: Requested RTS/CTS: %s.\n", // 在内核日志中打印请求的硬件流控状态            port->name, rtscts ? "yes" : "no"); // 根据 rtscts 的值打印 "yes" 或 "no"    cbaud = cflag & CBAUD; // 从 cflag 中提取波特率标志    for (i = 0; i < sizeof(el600x_baud_rate) / sizeof(el600x_baud_rate_t); // 遍历支持的波特率列表            i++) { // 循环        el600x_baud_rate_t *b = el600x_baud_rate + i; // 获取当前波特率配置项        if (b->cbaud == cbaud) { // 如果 termios 标志匹配            b_to_use = b; // 找到匹配项,保存指针            break; // 退出循环        }    }    if (b_to_use) { // 如果找到了支持的波特率        printk(KERN_INFO PFX "%s: Requested baud rate: %u.\n", // 打印请求的波特率                port->name, b_to_use->baud);    } else { // 如果没有找到        printk(KERN_ERR PFX "Error: %s does not support" // 打印错误信息                " baud rate index %x.\n", port->name, cbaud);        return -EINVAL; // 返回无效参数错误    }    switch (cflag & CSIZE) { // 根据 cflag 中的数据位大小标志        case CS5: // 如果是 CS5            data_bits = 5; // 数据位为5            break; // 结束 switch        case CS6: // 如果是 CS6            data_bits = 6; // 数据位为6            break; // 结束 switch        case CS7: // 如果是 CS7            data_bits = 7; // 数据位为7            break; // 结束 switch        case CS8: // 如果是 CS8            data_bits = 8; // 数据位为8            break; // 结束 switch        default: /* CS5 or CS6 */ // 默认情况(可能是CS5或CS6,但这里注释不严谨)            data_bits = 0; // 设为无效值    }    if (cflag & PARENB) { // 如果启用了奇偶校验        par = (cflag & PARODD) ? PAR_ODD : PAR_EVEN; // 判断是奇校验还是偶校验    } else { // 如果未启用        par = PAR_NONE; // 无校验    }    stop_bits = (cflag & CSTOPB) ? 2 : 1; // 判断是2位停止位还是1位停止位    printk(KERN_INFO PFX "%s: Requested Data frame: %u%c%u.\n", // 打印请求的数据帧格式            port->name, data_bits, // 数据位            (par == PAR_NONE ? 'N' : (par == PAR_ODD ? 'O' : 'E')), // 校验位 (N/O/E)            stop_bits); // 停止位    for (i = 0; i < sizeof(el600x_data_frame) / sizeof(el600x_data_frame_t); // 遍历支持的数据帧列表            i++) { // 循环        el600x_data_frame_t *df = el600x_data_frame + i; // 获取当前数据帧配置项        if (df->data_bits == data_bits && // 如果数据位、                df->parity == par && // 奇偶校验、                df->stop_bits == stop_bits) { // 和停止位都匹配            df_to_use = df; // 找到匹配项,保存指针            break; // 退出循环        }    }    if (!df_to_use) { // 如果没有找到支持的数据帧格式        printk(KERN_ERR PFX "Error: %s does not support data frame type.\n", // 打印错误信息                port->name);        return -EINVAL; // 返回无效参数错误    }    // 将解析出的请求参数保存到端口结构体中,等待状态机处理    port->requested_rtscts = rtscts != 0; // 保存请求的RTS/CTS设置 (布尔值)    port->requested_baud_rate = b_to_use->value; // 保存请求的波特率SDO值    port->requested_data_frame = df_to_use->value; // 保存请求的数据帧SDO值    port->config_error = 0; // 清除配置错误标志    return 0; // 返回成功}/****************************************************************************/ // 分隔注释// 定义 ectty 的操作回调函数集合static ec_tty_operations_t el60xx_tty_ops = { // 结构体变量    .cflag_changed = el60xx_cflag_changed, // 将 cflag_changed 成员指向我们实现的 el60xx_cflag_changed 函数};/****************************************************************************/ // 分隔注释// 初始化一个 EL60xx 的端口static int el60xx_port_init(el60xx_port_t *port, ec_slave_config_t *sc,        ec_domain_t *domain, unsigned int slot_offset, const char *name){    int ret = 0; // 声明并初始化返回值    strncpy(port->name, name, EL6002_PORT_NAME_SIZE); // 复制端口名称到结构体中    port->tty = ectty_create(&el60xx_tty_ops, port); // 调用 ectty 接口创建一个 TTY 设备,并关联我们的操作函数和数据    if (IS_ERR(port->tty)) { // 检查 TTY 创建是否失败        printk(KERN_ERR PFX "Failed to create tty for %s.\n", // 打印错误信息                port->name);        ret = PTR_ERR(port->tty); // 获取错误码        goto out_return; // 跳转到返回处    }    // 初始化端口结构体的成员变量    port->max_tx_data_size = 22; // 设置最大发送数据大小为22字节    port->max_rx_data_size = 22; // 设置最大接收数据大小为22字节    port->tx_data = NULL; // 发送缓冲区指针初始化为 NULL    port->tx_data_size = 0; // 当前发送数据大小为0    port->state = SER_REQUEST_INIT; // 状态机初始状态为请求初始化    port->tx_request_toggle = 0; // 发送请求翻转位初始化为0    port->rx_accepted_toggle = 0; // 接收接受翻转位初始化为0    port->control = 0x0000; // 控制字初始化为0    port->off_ctrl = 0; // 控制字偏移量初始化为0    port->off_tx = 0; // 发送数据偏移量初始化为0    port->off_status = 0; // 状态字偏移量初始化为0    port->off_rx = 0; // 接收数据偏移量初始化为0    port->requested_rtscts = 0x00; // no hardware handshake // 默认请求的RTS/CTS为关闭    port->current_rtscts = 0xff; // 默认当前RTS/CTS为无效值,强制首次配置    port->requested_baud_rate = 6; // 9600 // 默认请求的波特率为9600 (SDO值=6)    port->current_baud_rate = 0; // 默认当前波特率为无效值    port->requested_data_frame = 0x03; // 8N1 // 默认请求的数据帧为8N1 (SDO值=0x03)    port->current_data_frame = 0x00; // 默认当前数据帧为无效值    port->config_error = 0; // 配置错误标志初始化为0    // 为设置硬件流控创建一个 SDO 请求对象    if (!(port->rtscts_sdo = ecrt_slave_config_create_sdo_request(sc, // 创建SDO请求                    0x8000 + slot_offset, 0x01, 1))) { // SDO索引 0x8000/0x8010, 子索引 0x01, 大小 1 字节        printk(KERN_ERR PFX "Failed to create SDO request for %s.\n", // 打印错误                port->name);        ret = -ENOMEM; // 设置返回值为内存不足        goto out_free_tty; // 跳转到清理处    }    // 为设置波特率创建一个 SDO 请求对象    if (!(port->baud_sdo = ecrt_slave_config_create_sdo_request(sc, // 创建SDO请求                    0x8000 + slot_offset, 0x11, 1))) { // SDO索引 0x8000/0x8010, 子索引 0x11, 大小 1 字节        printk(KERN_ERR PFX "Failed to create SDO request for %s.\n", // 打印错误                port->name);        ret = -ENOMEM; // 设置返回值为内存不足        goto out_free_tty; // 跳转到清理处    }    // 为设置数据帧创建一个 SDO 请求对象    if (!(port->frame_sdo = ecrt_slave_config_create_sdo_request(sc, // 创建SDO请求                    0x8000 + slot_offset, 0x15, 1))) { // SDO索引 0x8000/0x8010, 子索引 0x15, 大小 1 字节        printk(KERN_ERR PFX "Failed to create SDO request for %s\n", // 打印错误                port->name);        ret = -ENOMEM; // 设置返回值为内存不足        goto out_free_tty; // 跳转到清理处    }    // 注册控制字 PDO 条目到域中    ret = ecrt_slave_config_reg_pdo_entry( // 注册PDO条目            sc, 0x7001 + slot_offset, 0x01, domain, NULL); // PDO索引 0x7001/0x7011, 子索引 0x01    if (ret < 0) { // 检查注册是否失败        printk(KERN_ERR PFX "Failed to register PDO entry of %s\n", // 打印错误                port->name);        goto out_free_tty; // 跳转到清理处    }    port->off_ctrl = ret; // 保存返回的偏移量    // 注册发送数据 PDO 条目到域中    ret = ecrt_slave_config_reg_pdo_entry( // 注册PDO条目            sc, 0x7000 + slot_offset, 0x11, domain, NULL); // PDO索引 0x7000/0x7010, 子索引 0x11    if (ret < 0) { // 检查注册是否失败        printk(KERN_ERR PFX "Failed to register PDO entry of %s.\n", // 打印错误                port->name);        goto out_free_tty; // 跳转到清理处    }    port->off_tx = ret; // 保存返回的偏移量    // 注册状态字 PDO 条目到域中    ret = ecrt_slave_config_reg_pdo_entry( // 注册PDO条目            sc, 0x6001 + slot_offset, 0x01, domain, NULL); // PDO索引 0x6001/0x6011, 子索引 0x01    if (ret < 0) { // 检查注册是否失败        printk(KERN_ERR PFX "Failed to register PDO entry of %s.\n", // 打印错误                port->name);        goto out_free_tty; // 跳转到清理处    }    port->off_status = ret; // 保存返回的偏移量    // 注册接收数据 PDO 条目到域中    ret = ecrt_slave_config_reg_pdo_entry( // 注册PDO条目            sc, 0x6000 + slot_offset, 0x11, domain, NULL); // PDO索引 0x6000/0x6010, 子索引 0x11    if (ret < 0) { // 检查注册是否失败        printk(KERN_ERR PFX "Failed to register PDO entry of %s.\n", // 打印错误                port->name);        goto out_free_tty; // 跳转到清理处    }    port->off_rx = ret; // 保存返回的偏移量    if (port->max_tx_data_size > 0) { // 如果需要发送缓冲区        port->tx_data = kmalloc(port->max_tx_data_size, GFP_KERNEL); // 使用内核内存分配器分配内存        if (port->tx_data == NULL) { // 检查分配是否失败            printk(KERN_ERR PFX "Failed to allocate %zu bytes of TX" // 打印错误                    " memory for %s.\n", port->max_tx_data_size, port->name);            ret = -ENOMEM; // 设置返回值为内存不足            goto out_free_tty; // 跳转到清理处        }    }    return 0; // 返回成功out_free_tty: // 清理标签:释放 TTY    ectty_free(port->tty); // 释放 TTY 资源out_return: // 清理标签:返回    return ret; // 返回错误码}/****************************************************************************/ // 分隔注释// 清理一个 EL60xx 端口的资源static void el60xx_port_clear(el60xx_port_t *port){    ectty_free(port->tty); // 释放 TTY 资源    if (port->tx_data) { // 如果发送缓冲区已分配        kfree(port->tx_data); // 释放内存    }}/****************************************************************************/ // 分隔注释// 在每个实时周期运行一个 EL60xx 端口的逻辑static void el60xx_port_run(el60xx_port_t *port, u8 *pd){    u16 status = EC_READ_U16(pd + port->off_status); // 从过程数据中读取16位状态字    u8 *rx_data = pd + port->off_rx; // 获取接收数据在过程数据中的指针    uint8_t tx_accepted_toggle, rx_request_toggle; // 声明局部变量    switch (port->state) { // 根据当前端口的状态机状态进行操作        case SER_READY: // 如果是准备就绪状态            /* Check, if hardware handshaking has to be configured. */ // 注释:检查是否需要配置硬件握手            if (!port->config_error && // 如果没有配置错误 且                    port->requested_rtscts != port->current_rtscts) { // 请求的RTS/CTS设置与当前不同                EC_WRITE_U8(ecrt_sdo_request_data(port->rtscts_sdo), // 将请求的值写入SDO请求的缓冲区                        port->requested_rtscts);                ecrt_sdo_request_write(port->rtscts_sdo); // 发起 SDO 写请求                port->state = SER_SET_RTSCTS; // 切换到设置RTS/CTS状态                break; // 结束 switch            }            /* Check, if the baud rate has to be configured. */ // 注释:检查是否需要配置波特率            if (!port->config_error && // 如果没有配置错误 且                    port->requested_baud_rate != port->current_baud_rate) { // 请求的波特率与当前不同                EC_WRITE_U8(ecrt_sdo_request_data(port->baud_sdo), // 将请求的值写入SDO请求的缓冲区                        port->requested_baud_rate);                ecrt_sdo_request_write(port->baud_sdo); // 发起 SDO 写请求                port->state = SER_SET_BAUD_RATE; // 切换到设置波特率状态                break; // 结束 switch            }            /* Check, if the data frame has to be configured. */ // 注释:检查是否需要配置数据帧            if (!port->config_error && // 如果没有配置错误 且                    port->requested_data_frame != port->current_data_frame) { // 请求的数据帧与当前不同                EC_WRITE_U8(ecrt_sdo_request_data(port->frame_sdo), // 将请求的值写入SDO请求的缓冲区                        port->requested_data_frame);                ecrt_sdo_request_write(port->frame_sdo); // 发起 SDO 写请求                port->state = SER_SET_DATA_FRAME; // 切换到设置数据帧状态                break; // 结束 switch            }            /* Send data */ // 注释:发送数据            tx_accepted_toggle = status & 0x0001; // 从状态字中提取发送接受翻转位            if (tx_accepted_toggle != port->tx_accepted_toggle) { // ready // 如果翻转位变化,表示从站准备好接收新数据                port->tx_data_size = // 从 TTY 缓冲区获取要发送的数据                    ectty_tx_data(port->tty, port->tx_data, // 数据存入 port->tx_data                            port->max_tx_data_size); // 最大大小                if (port->tx_data_size) { // 如果有数据要发送#if DEBUG // 如果 DEBUG 宏为1                    printk(KERN_INFO PFX "%s: Sending %u bytes.\n", // 打印调试信息                            port->name, port->tx_data_size);#endif // 结束 #if                    port->tx_request_toggle = !port->tx_request_toggle; // 翻转发送请求位,通知从站有新数据                    port->tx_accepted_toggle = tx_accepted_toggle; // 更新本地的发送接受位                }            }            /* Receive data */ // 注释:接收数据            rx_request_toggle = (status & 0x0002) >> 1; // 从状态字中提取接收请求翻转位 (修正:原代码有误,应右移一位)            if (rx_request_toggle != port->rx_request_toggle) { // 如果翻转位变化,表示从站有新数据到达                uint8_t rx_data_size = status >> 8; // 从状态字的高8位获取接收到的数据大小                port->rx_request_toggle = rx_request_toggle; // 更新本地的接收请求位#if DEBUG // 如果 DEBUG 宏为1                printk(KERN_INFO PFX "%s: Received %u bytes.\n", // 打印调试信息                        port->name, rx_data_size);#endif // 结束 #if                ectty_rx_data(port->tty, rx_data, rx_data_size); // 将接收到的数据推送到 TTY 层的接收缓冲区                port->rx_accepted_toggle = !port->rx_accepted_toggle; // 翻转接收接受位,通知从站数据已接收            }            port->control = // 组合成新的16位控制字                port->tx_request_toggle | // 发送请求位                port->rx_accepted_toggle << 1 | // 接收接受位                port->tx_data_size << 8; // 发送数据大小            break; // 结束 SER_READY 状态        case SER_REQUEST_INIT: // 如果是请求初始化状态            if (status & (1 << 2)) { // 如果状态字的第2位(InitToogle)为1                port->control = 0x0000; // 清除控制字                port->state = SER_WAIT_FOR_INIT_RESPONSE; // 切换到等待初始化响应状态            } else { // 如果为0                port->control = 1 << 2; // CW.2, request initialization // 设置控制字的第2位,请求初始化            }            break; // 结束 SER_REQUEST_INIT 状态        case SER_WAIT_FOR_INIT_RESPONSE: // 如果是等待初始化响应状态            if (!(status & (1 << 2))) { // 如果状态字的第2位(InitToogle)变为0                printk(KERN_INFO PFX "%s: Init successful.\n", port->name); // 打印初始化成功信息                port->tx_accepted_toggle = 1; // 重置发送接受翻转位                port->control = 0x0000; // 清除控制字                port->state = SER_READY; // 切换到准备就绪状态            }            break; // 结束 SER_WAIT_FOR_INIT_RESPONSE 状态        case SER_SET_RTSCTS: // 如果是设置RTS/CTS状态            switch (ecrt_sdo_request_state(port->rtscts_sdo)) { // 检查SDO请求的状态                case EC_REQUEST_SUCCESS: // 如果成功                    printk(KERN_INFO PFX "%s: Accepted RTS/CTS.\n", // 打印成功信息                            port->name);                    port->current_rtscts = port->requested_rtscts; // 更新当前设置                    port->state = SER_REQUEST_INIT; // 返回请求初始化状态,以重新同步                    break; // 结束 switch                case EC_REQUEST_ERROR: // 如果失败                    printk(KERN_ERR PFX "Failed to set RTS/CTS on %s!\n", // 打印错误信息                            port->name);                    port->state = SER_REQUEST_INIT; // 返回请求初始化状态                    port->config_error = 1; // 设置配置错误标志                    break; // 结束 switch                default: // 其他情况(如 PENDING)                    break; // 保持当前状态,等待            }            break; // 结束 SER_SET_RTSCTS 状态        case SER_SET_BAUD_RATE: // 如果是设置波特率状态            switch (ecrt_sdo_request_state(port->baud_sdo)) { // 检查SDO请求的状态                case EC_REQUEST_SUCCESS: // 如果成功                    printk(KERN_INFO PFX "%s: Accepted baud rate.\n", // 打印成功信息                            port->name);                    port->current_baud_rate = port->requested_baud_rate; // 更新当前设置                    port->state = SER_REQUEST_INIT; // 返回请求初始化状态                    break; // 结束 switch                case EC_REQUEST_ERROR: // 如果失败                    printk(KERN_ERR PFX "Failed to set baud rate on %s!\n", // 打印错误信息                            port->name);                    port->state = SER_REQUEST_INIT; // 返回请求初始化状态                    port->config_error = 1; // 设置配置错误标志                    break; // 结束 switch                default: // 其他情况                    break; // 保持当前状态            }            break; // 结束 SER_SET_BAUD_RATE 状态        case SER_SET_DATA_FRAME: // 如果是设置数据帧状态            switch (ecrt_sdo_request_state(port->frame_sdo)) { // 检查SDO请求的状态                case EC_REQUEST_SUCCESS: // 如果成功                    printk(KERN_INFO PFX "%s: Accepted data frame.\n", // 打印成功信息                            port->name);                    port->current_data_frame = port->requested_data_frame; // 更新当前设置                    port->state = SER_REQUEST_INIT; // 返回请求初始化状态                    break; // 结束 switch                case EC_REQUEST_ERROR: // 如果失败                    printk(KERN_ERR PFX "Failed to set data frame on %s!\n", // 打印错误信息                            port->name);                    port->state = SER_REQUEST_INIT; // 返回请求初始化状态                    port->config_error = 1; // 设置配置错误标志                    break; // 结束 switch                default: // 其他情况                    break; // 保持当前状态            }            break; // 结束 SER_SET_DATA_FRAME 状态    }    EC_WRITE_U16(pd + port->off_ctrl, port->control); // 将计算出的控制字写入过程数据    memcpy(pd + port->off_tx, port->tx_data, port->tx_data_size); // 将要发送的数据复制到过程数据}/****************************************************************************/ // 分隔注释// 初始化一个 EL6002 设备(包含两个端口)static int el6002_init(el6002_t *el6002, ec_master_t *master, u16 position,        ec_domain_t *domain, u32 vendor, u32 product){    int ret = 0, i; // 声明返回值和循环变量    // 为该从站创建配置对象    if (!(el6002->sc = ecrt_master_slave_config( // 创建从站配置                    master, 0, position, vendor, product))) { // 主站索引0, 从站位置, 厂商ID, 产品ID        printk(KERN_ERR PFX "EL6002(%u): Failed to create" // 打印错误                " slave configuration.\n", position);        ret = -EBUSY; // 设置返回值为设备忙        goto out_return; // 跳转到返回处    }    // 为该从站配置 PDO    if (ecrt_slave_config_pdos(el6002->sc, EC_END, el6002_syncs)) { // 配置 PDO        printk(KERN_ERR PFX "EL6002(%u): Failed to configure PDOs.\n", // 打印错误                position);        ret = -ENOMEM; // 设置返回值为内存不足        goto out_return; // 跳转到返回处    }    for (i = 0; i < EL6002_PORTS; i++) { // 遍历该设备的两个端口        char name[EL6002_PORT_NAME_SIZE]; // 声明端口名称字符串        snprintf(name, EL6002_PORT_NAME_SIZE, "EL6002(%u) X%u", // 格式化端口名称                position, i + 1);        if (el60xx_port_init(el6002->port + i, el6002->sc, domain, i * 0x10, // 调用端口初始化函数                    name)) { // slot_offset: 端口2的PDO索引比端口1大0x10            printk(KERN_ERR PFX "EL6002(%u): Failed to init port X%u.\n", // 打印错误                    position, i);            goto out_ports; // 跳转到清理已初始化的端口        }    }    return 0; // 返回成功out_ports: // 清理标签:清理已初始化的端口    for (i--; i >= 0; i--) { // 反向循环        el60xx_port_clear(el6002->port + i); // 清理端口资源    }out_return: // 清理标签:返回    return ret; // 返回错误码}/****************************************************************************/ // 分隔注释// 清理一个 EL6002 设备的所有资源static void el6002_clear(el6002_t *el6002){    int i; // 声明循环变量    for (i = 0; i < EL6002_PORTS; i++) { // 遍历两个端口        el60xx_port_clear(el6002->port + i); // 调用端口清理函数    }}/****************************************************************************/ // 分隔注释// 在每个实时周期运行一个 EL6002 设备的逻辑static void el6002_run(el6002_t *el6002, u8 *pd){    int i; // 声明循环变量    for (i = 0; i < EL6002_PORTS; i++) { // 遍历两个端口        el60xx_port_run(el6002->port + i, pd); // 调用端口运行函数    }}/****************************************************************************/ // 分隔注释// 在每个实时周期运行所有已注册的串口设备的逻辑void run_serial_devices(u8 *pd){    el6002_t *el6002; // 声明一个指向 EL6002 结构体的指针    list_for_each_entry(el6002, &handlers, list) { // 遍历全局的 handlers 链表        el6002_run(el6002, pd); // 对链表中的每个设备调用其运行函数    }}/****************************************************************************/ // 分隔注释// 创建一个 EL6002 设备的处理器实例static int create_el6002_handler(ec_master_t *master, ec_domain_t *domain,        u16 position, u32 vendor, u32 product){    el6002_t *el6002; // 声明一个指向 EL6002 结构体的指针    int ret; // 声明返回值    printk(KERN_INFO PFX "Creating handler for EL6002 at position %u\n", // 打印信息            position);    el6002 = kmalloc(sizeof(*el6002), GFP_KERNEL); // 为 EL6002 结构体分配内存    if (!el6002) { // 检查分配是否失败        printk(KERN_ERR PFX "Failed to allocate serial device object.\n"); // 打印错误        return -ENOMEM; // 返回内存不足错误    }    ret = el6002_init(el6002, master, position, domain, vendor, product); // 调用设备初始化函数    if (ret) { // 如果初始化失败        kfree(el6002); // 释放之前分配的内存        return ret; // 返回错误码    }    list_add_tail(&el6002->list, &handlers); // 将新创建的设备添加到全局链表的尾部    return 0; // 返回成功}/****************************************************************************/ // 分隔注释// 创建所有找到的串口设备int create_serial_devices(ec_master_t *master, ec_domain_t *domain){    int i, ret; // 声明循环变量和返回值    ec_master_info_t master_info; // 声明主站信息结构体    ec_slave_info_t slave_info; // 声明从站信息结构体    el6002_t *ser, *next; // 声明用于安全遍历链表的指针    printk(KERN_INFO PFX "Registering serial devices...\n"); // 打印信息    ret = ecrt_master(master, &master_info); // 获取主站信息(如从站数量)    if (ret) { // 检查是否失败        printk(KERN_ERR PFX "Failed to obtain master information.\n"); // 打印错误        goto out_return; // 跳转到返回处    }    for (i = 0; i < master_info.slave_count; i++) { // 遍历总线上的所有从站        ret = ecrt_master_get_slave(master, i, &slave_info); // 获取指定位置的从站信息        if (ret) { // 检查是否失败            printk(KERN_ERR PFX "Failed to obtain slave information.\n"); // 打印错误            goto out_free_handlers; // 跳转到清理处        }        // 检查是否是 Beckhoff EL6002        if (slave_info.vendor_id == VendorIdBeckhoff                && slave_info.product_code == ProductCodeBeckhoffEL6002) {            if (create_el6002_handler(master, domain, i, // 如果是,则为其创建处理器                    slave_info.vendor_id, slave_info.product_code)) {                goto out_free_handlers; // 如果创建失败,跳转到清理处            }        }        // 检查是否是 IDS CSI71A        if (slave_info.vendor_id == VendorIdIds                && slave_info.product_code == ProductCodeIdsCSI71A) {            if (create_el6002_handler(master, domain, i, // 如果是,也为其创建处理器                    slave_info.vendor_id, slave_info.product_code)) {                goto out_free_handlers; // 如果创建失败,跳转到清理处            }        }    }    printk(KERN_INFO PFX "Finished registering serial devices.\n"); // 打印完成信息    return 0; // 返回成功out_free_handlers: // 清理标签:释放已创建的处理器    list_for_each_entry_safe(ser, next, &handlers, list) { // 安全地遍历链表        list_del(&ser->list); // 从链表中移除        el6002_clear(ser); // 清理设备资源        kfree(ser); // 释放内存    }out_return: // 清理标签:返回    return ret; // 返回错误码}/****************************************************************************/ // 分隔注释// 释放所有已创建的串口设备void free_serial_devices(void){    el6002_t *ser, *next; // 声明用于安全遍历链表的指针    printk(KERN_INFO PFX "Cleaning up serial devices...\n"); // 打印信息    list_for_each_entry_safe(ser, next, &handlers, list) { // 安全地遍历全局的 handlers 链表        list_del(&ser->list); // 从链表中移除        el6002_clear(ser); // 清理设备资源        kfree(ser); // 释放内存    }    printk(KERN_INFO PFX "Finished cleaning up serial devices.\n"); // 打印完成信息}

    这个 C 语言文件实现了一个自包含的 Linux 内核模块,它不仅充当了 EtherCAT 主站的应用层逻辑,还整合了之前示例中的串口设备驱动功能。它代表了一种在内核空间内完成所有 EtherCAT 实时控制的典型架构。

    核心架构与功能:

    1. 内核模块形态:这是一个完整的内核模块,通过 insmod 加载,通过 rmmod 卸载。它使用 module_init 和 module_exit 宏来注册其生命周期函数。

    2. 周期性任务驱动源 - 内核定时器

    • 该模块不使用 PREEMPT_RT 的高精度睡眠或任何多线程机制。它依赖于标准的 Linux 内核定时器 (struct timer_list) 来驱动其周期性任务。

    • 在 init_mini_module 函数中,一个定时器被初始化并设置在未来超时。

    • 当定时器超时时,内核会自动调用注册的回调函数 cyclic_task

    • 在 cyclic_task 的末尾,它会计算下一次超时时间并调用 add_timer(&timer) 来“重新武装”自己,从而形成一个持续的、周期性的执行链。

    • 注意

      :标准内核定时器的精度受限于内核的 HZ 值(时钟节拍频率,通常是 100, 250, 或 1000 Hz),因此它提供的是软实时保证,不适合需要微秒级精确抖动的硬实时应用。

  • 并发保护 - 内核信号量

    • 由于定时器回调函数可能在任何 CPU 核心上被调用,且可能与其他内核路径并发,代码使用了一个内核信号量 master_sem

    • 在所有访问共享的 EtherCAT 主站资源(master 对象)的代码路径前后,都通过 down(&master_sem) 和 up(&master_sem) 来加锁和解锁,确保了操作的原子性和线程安全。

  • 模块化设计与整合

    • 这个模块通过 #include "serial.h" 并调用 create_serial_devices() 和 run_serial_devices(),整合了前面示例中的串口设备驱动逻辑。

    • 在 init_mini_module 中,它会扫描并初始化总线上的所有 EL6002 串口设备。

    • 在 cyclic_task 中,它会调用 run_serial_devices() 来执行这些串口设备在每个周期内的状态机和数据交换逻辑。

  • 生命周期管理

    • 加载 (insmod)

      : 执行 init_mini_module。此函数完成所有设置:请求主站、创建域、配置从站(包括串口设备)、激活主站,并启动第一个内核定时器。

    • 运行

      : 内核定时器周期性地触发 cyclic_task,执行 EtherCAT I/O 和串口设备逻辑。

    • 卸载 (rmmod)

      : 执行 cleanup_mini_module。此函数首先通过 del_timer_sync 安全地停止定时器,然后调用 free_serial_devices 清理串口 TTY 设备,最后释放 EtherCAT 主站资源。

    总结:此代码是一个经典的软实时内核空间 EtherCAT 控制器示例。它将应用逻辑(周期性 I/O)和设备驱动逻辑(串口处理)全部放在内核中,通过标准的内核定时器实现周期性调度,并通过内核信号量保证数据一致性。这种架构适合那些对实时性要求不极端(毫秒级响应即可),但希望将所有控制逻辑都集成在内核中以避免内核/用户空间切换开销的场景。

    #include <linux/version.h> // 包含 Linux 内核版本信息头文件,用于条件编译#include <linux/module.h> // 包含 Linux 内核模块编程所需的核心头文件#include <linux/timer.h> // 包含内核定时器(jiffies, timer_list)相关的头文件#include <linux/interrupt.h> // 包含中断处理相关的头文件,虽然在此代码中未直接使用#include <linux/err.h> // 包含内核错误处理相关的头文件,如 IS_ERR, PTR_ERR#include <linux/semaphore.h> // 包含内核信号量相关的头文件,用于同步和互斥#include "../../include/ecrt.h" // EtherCAT realtime interface // 包含 IgH EtherCAT 主站的实时接口头文件#include "serial.h" // 包含此模块自定义的头文件(定义了 run_serial_devices 等函数原型)/****************************************************************************/ // 分隔注释// Module parameters // 区域注释:模块参数#define FREQUENCY 100 // 宏定义:周期性任务的频率为 100 Hz// Optional features // 区域注释:可选特性#define PFX "ec_tty_example: " // 宏定义:内核日志消息的前缀,方便调试/****************************************************************************/ // 分隔注释// EtherCAT // 区域注释:EtherCAT 相关全局变量static ec_master_t *master = NULL; // 声明一个静态的 EtherCAT 主站对象指针static ec_master_state_t master_state = {}; // 声明一个静态的主站状态结构体变量static struct semaphore master_sem; // 声明一个内核信号量,用于保护对主站对象的并发访问static ec_domain_t *domain1 = NULL; // 声明一个静态的 EtherCAT 过程数据域指针static ec_domain_state_t domain1_state = {}; // 声明一个静态的域状态结构体变量// Timer // 区域注释:定时器static struct timer_list timer; // 声明一个内核定时器结构体变量/****************************************************************************/ // 分隔注释// process data // 区域注释:过程数据static uint8_t *domain1_pd; // process data memory // 声明一个指向过程数据内存区域的指针#define BusCouplerPos  0, 0 // 宏定义:总线耦合器的位置#define Beckhoff_EK1100 0x00000002, 0x044c2c52 // 宏定义:倍福 EK1100 的厂商/产品IDstatic unsigned int counter = 0; // 声明一个静态无符号整型,用作通用计数器/****************************************************************************/ // 分隔注释static void check_domain1_state(void) // 定义一个函数,用于检查并打印域的状态变化{    ec_domain_state_t ds; // 声明一个局部的域状态结构体变量    down(&master_sem); // 获取信号量,实现互斥访问    ecrt_domain_state(domain1, &ds); // 调用 ecrt API,获取 domain1 的当前状态    up(&master_sem); // 释放信号量    if (ds.working_counter != domain1_state.working_counter) // 比较当前工作计数器(WC)与上次记录的WC        printk(KERN_INFO PFX "Domain1: WC %u.\n", ds.working_counter); // 如果不一致,打印新的WC值    if (ds.wc_state != domain1_state.wc_state) // 比较当前工作计数器状态与上次记录的状态        printk(KERN_INFO PFX "Domain1: State %u.\n", ds.wc_state); // 如果不一致,打印新的WC状态    domain1_state = ds; // 将当前状态赋值给全局变量,用于下次比较}/****************************************************************************/ // 分隔注释static void check_master_state(void) // 定义一个函数,用于检查并打印主站的状态变化{    ec_master_state_t ms; // 声明一个局部的主站状态结构体变量    down(&master_sem); // 获取信号量    ecrt_master_state(master, &ms); // 调用 ecrt API,获取主站的当前状态    up(&master_sem); // 释放信号量    if (ms.slaves_responding != master_state.slaves_responding) // 比较当前响应的从站数量与上次记录的数量        printk(KERN_INFO PFX "%u slave(s).\n", ms.slaves_responding); // 如果不一致,打印新的从站数量    if (ms.al_states != master_state.al_states) // 比较当前应用层(AL)状态与上次记录的状态        printk(KERN_INFO PFX "AL states: 0x%02X.\n", ms.al_states); // 如果不一致,以十六进制格式打印新的AL状态    if (ms.link_up != master_state.link_up) // 比较当前链路连接状态与上次记录的状态        printk(KERN_INFO PFX "Link is %s.\n", ms.link_up ? "up" : "down"); // 如果不一致,打印链路是 "up" 还是 "down"    master_state = ms; // 将当前状态赋值给全局变量,用于下次比较}/****************************************************************************/ // 分隔注释#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 15, 0) // C预处理指令,检查内核版本是否大于等于 4.15.0static void cyclic_task(struct timer_list *t) // 如果是,定时器回调函数的签名为 (struct timer_list *t)#else // 否则static void cyclic_task(unsigned long data) // 老版本内核的定时器回调函数签名为 (unsigned long data)#endif // 结束 #if 条件编译块{    // receive process data // 注释:接收过程数据    down(&master_sem); // 获取信号量    ecrt_master_receive(master); // 从网络接口接收数据帧    ecrt_domain_process(domain1); // 处理域的数据    up(&master_sem); // 释放信号量    // check process data state (optional) // 注释:检查过程数据状态(可选)    check_domain1_state(); // 调用函数检查并打印域的状态变化    if (counter) { // 如果计数器不为0        counter--; // 计数器减1    } else { // do this at 1 Hz // 如果计数器为0 (即每秒执行一次)        counter = FREQUENCY; // 重置计数器为频率值 (100)        // check for master state (optional) // 注释:检查主站状态(可选)        check_master_state(); // 调用函数检查并打印主站的状态变化    }    run_serial_devices(domain1_pd); // 调用 serial.c 中定义的函数,运行所有串口设备的状态机    // send process data // 注释:发送过程数据    down(&master_sem); // 获取信号量    ecrt_domain_queue(domain1); // 将要发送的域数据放入发送队列    ecrt_master_send(master); // 将数据帧发送到 EtherCAT 总线    up(&master_sem); // 释放信号量    // restart timer // 注释:重启定时器    timer.expires += HZ / FREQUENCY; // 计算下一个超时时间点 (HZ是内核定义的每秒jiffies数)    add_timer(&timer); // 将定时器重新加入内核的定时器列表,以在未来超时}/****************************************************************************/ // 分隔注释static void send_callback(void *cb_data) // 定义一个发送回调函数{    ec_master_t *m = (ec_master_t *) cb_data; // 将 void* 指针转换为 master 指针    down(&master_sem); // 获取信号量    ecrt_master_send_ext(m); // 调用扩展的发送函数    up(&master_sem); // 释放信号量}/****************************************************************************/ // 分隔注释static void receive_callback(void *cb_data) // 定义一个接收回调函数{    ec_master_t *m = (ec_master_t *) cb_data; // 将 void* 指针转换为 master 指针    down(&master_sem); // 获取信号量    ecrt_master_receive(m); // 调用接收函数    up(&master_sem); // 释放信号量}/****************************************************************************/ // 分隔注释// 内核模块的初始化函数,在 insmod 时被调用static int __init init_mini_module(void){    int ret = -1; // 声明并初始化返回值    ec_slave_config_t *sc; // 声明一个从站配置对象指针    printk(KERN_INFO PFX "Starting...\n"); // 在内核日志中打印启动信息    master = ecrt_request_master(0); // 请求索引为0的 EtherCAT 主站实例    if (!master) { // 检查主站请求是否成功        printk(KERN_ERR PFX "Requesting master 0 failed.\n"); // 打印错误信息        ret = -EBUSY; // 设置返回值为设备忙        goto out_return; // 跳转到返回处    }    sema_init(&master_sem, 1); // 初始化信号量,初始值为1(可用状态)    ecrt_master_callbacks(master, send_callback, receive_callback, master); // 注册发送和接收的回调函数 (当使用外部事件驱动时)    printk(KERN_INFO PFX "Registering domain...\n"); // 打印信息    if (!(domain1 = ecrt_master_create_domain(master))) { // 在主站上创建一个过程数据域        printk(KERN_ERR PFX "Domain creation failed!\n"); // 如果失败,打印错误        goto out_release_master; // 跳转到释放主站处    }    // Create configuration for bus coupler // 注释:为总线耦合器创建配置    sc = ecrt_master_slave_config(master, BusCouplerPos, Beckhoff_EK1100); // 为 EK1100 创建配置    if (!sc) { // 检查配置创建是否成功        printk(KERN_ERR PFX "Failed to create slave config.\n"); // 打印错误        ret = -ENOMEM; // 设置返回值为内存不足        goto out_release_master; // 跳转到释放主站处    }    create_serial_devices(master, domain1); // 调用 serial.c 中定义的函数,扫描并初始化所有串口设备    printk(KERN_INFO PFX "Activating master...\n"); // 打印信息    if (ecrt_master_activate(master)) { // 激活主站,开始总线通信        printk(KERN_ERR PFX "Failed to activate master!\n"); // 如果激活失败,打印错误        goto out_free_serial; // 跳转到释放串口设备处    }    // Get internal process data for domain // 注释:获取域的内部过程数据    domain1_pd = ecrt_domain_data(domain1); // 获取域的过程数据内存区指针    printk(KERN_INFO PFX "Starting cyclic sample thread.\n"); // 打印信息#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 15, 0) // 检查内核版本    timer_setup(&timer, cyclic_task, 0); // 使用新的 timer_setup 宏初始化定时器#else // 老版本内核    init_timer(&timer); // 使用旧的 init_timer 函数初始化    timer.function = cyclic_task; // 设置定时器的回调函数#endif // 结束 #if    timer.expires = jiffies + 10; // 设置第一次超时时间为当前 jiffies + 10 (jiffies是内核的时钟节拍计数)    add_timer(&timer); // 将定时器加入内核的定时器列表    printk(KERN_INFO PFX "Started.\n"); // 打印启动成功信息    return 0; // 返回成功out_free_serial: // 清理标签:释放串口设备    free_serial_devices(); // 调用 serial.c 中的函数释放资源out_release_master: // 清理标签:释放主站    printk(KERN_ERR PFX "Releasing master...\n"); // 打印信息    ecrt_release_master(master); // 释放主站资源out_return: // 清理标签:返回    printk(KERN_ERR PFX "Failed to load. Aborting.\n"); // 打印加载失败信息    return ret; // 返回错误码}/****************************************************************************/ // 分隔注释// 内核模块的退出函数,在 rmmod 时被调用static void __exit cleanup_mini_module(void){    printk(KERN_INFO PFX "Stopping...\n"); // 在内核日志中打印停止信息    del_timer_sync(&timer); // 同步地删除定时器,确保回调函数不会在模块卸载后运行    free_serial_devices(); // 调用 serial.c 中的函数释放所有串口设备资源    printk(KERN_INFO PFX "Releasing master...\n"); // 打印信息    ecrt_release_master(master); // 释放主站资源    printk(KERN_INFO PFX "Unloading.\n"); // 打印卸载完成信息}/****************************************************************************/ // 分隔注释MODULE_LICENSE("GPL"); // 宏:声明模块的许可证为 GPLMODULE_AUTHOR("Florian Pose <fp@igh.de>"); // 宏:声明模块的作者MODULE_DESCRIPTION("EtherCAT minimal test environment"); // 宏:声明模块的描述module_init(init_mini_module); // 宏:将 init_mini_module 函数注册为模块的初始化函数module_exit(cleanup_mini_module); // 宏:将 cleanup_mini_module 函数注册为模块的退出函数/****************************************************************************/
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值