【原創】EtherCAT主站IgH解析(一)--主站初始化、狀態機與EtherCAT報文


版權聲明:本文為本文為博主原創文章,轉載請注明出處。如有問題,歡迎指正。博客地址:https://www.cnblogs.com/wsg1100/

0 獲取源碼

IgH EtherCAT Master現已遷移到gitlab:https://gitlab.com/etherlab.org/ethercat,可以使用以下命令克隆存儲庫:

git clone https://gitlab.com/etherlab.org/ethercat.git
git checkout stable-1.5

1 啟動腳本

igh通過腳本來啟動,可以是systemd、init.d或sysconfig。分別位於源碼script目錄下:

image-20210219114206819

對於systemd方式,編譯時由ethercat.service.in文件生成ethercat.serviceethercat.service中指定了執行文件為ethercatctl.ethercatctl文件由``ethercatctl.in`生成。init.d和sysconfig類似,都是生成一個可執行腳本,且腳本完成的工作一致,主要完成加載主站模塊、網卡驅動、給主站內核模塊傳遞參數、卸載模塊等操作。

ethercat.conf共同的配置文件,配置主站使用的網卡、驅動等信息。下面看腳本start和stop所做的工作。

1.1 start

  1. 加載ec_master.ko

    模塊參數:

    • main_devices :主網卡MAC地址,多個main_devices 表示創建多個主站,MAC參數個數master_count。
    • backup_devices :備用網卡MAC地址,多個backup_devices 表示創建多個主站,MAC參數個數backup_count。
    • debug_level :調試level,調試信息輸出級別。
    • eoe_interfaces eoe接口,eoe_count表示eoe_interfaces 的個數。
    • eoe_autocreate 是否自動創建eoe handler。
    • pcap_size Pcap buffer size。
  2. 遍歷配置文件中/etc/sysconfig/ethercat的環境變量DEVICE_MODULES,位於ethercat.conf中。

  3. 在每個DEVICE_MODULES前添加前綴ec_替換,如DEVICE_MODULES為igb的話,添加前綴后為ec_igb

  4. modinfo檢查該模塊是否存在。

  5. 對於非genericrtdm的驅動,需要先將網卡與當前驅動unbindunbind后的網卡才能被新驅動接管

  6. 加載該驅動。

start加載了兩個內核模塊,ec_master.ko和網卡驅動ec_xxx.ko,ec_master先根據內核參數(網卡MAC)來創建主站實例,此時主站處於Orphaned phase。后續加載網卡驅動ec_xxx.ko,執行網卡驅動probe,根據MAC地址將網卡與主站實例匹配,此時主站得到操作的網卡設備,進入Idle phase。詳細過程見后文。

ecdev-offfer

1.2 stop

卸載內核模塊ec_master.ko和網卡驅動ec_xxx.ko。

  1. 遍歷配置文件中的環境變量DEVICE_MODULES。
  2. 在每個DEVICE_MODULES前添加前綴‘ec_’替換。
  3. lsmod檢查該模塊是否被加載。
  4. 卸載模塊。

后文“主站”和”master“均表示主站或主站實例對象,slave和從站表示從站或從站對象,中英混用,不刻意區分。

2 主站實例創建

一個使用IgH的控制器中可以有多個EtherCAT主站,每個主站可以綁定了一個主網卡和一個備用網卡,一般備用網卡不使用,線纜冗余功能時才使用備用網卡(文中假設只有一個主網卡)。

image-20210523100016531

start過程中執行insmod ec_master.ko,這個時候,先調用的就是 module_init 調用的初始化函數ec_init_module()。先根據參數main_devices 的個數master_count,每個master需要一個設備節點來與應用程序交互,所以master_count決定需要創建多少個matser和多少個字符設備;

這里先分配注冊master_count個字符設備的主次設備號device_number和名稱,這樣每個master對應的設備在文件系統中就是/dev/EtherCAT0/dev/EtherCAT1...(Linux下)。

if (master_count) {
        if (alloc_chrdev_region(&device_number,
                    0, master_count, "EtherCAT")) {
            EC_ERR("Failed to obtain device number(s)!\n");
...
        }
    }

 class = class_create(THIS_MODULE, "EtherCAT");

解析模塊參數得到MAC地址,保存到數組macs中。

 for (i = 0; i < master_count; i++) {
        ret = ec_mac_parse(macs[i][0], main_devices[i], 0);

        if (i < backup_count) {
            ret = ec_mac_parse(macs[i][1], backup_devices[i], 1);
        }
    }

分配master_count個主站對象的內存,調用ec_master_init()初始化這些實例。

    if (master_count) {
        if (!(masters = kmalloc(sizeof(ec_master_t) * master_count,
                        GFP_KERNEL))) {
            ...
        }
    }

    for (i = 0; i < master_count; i++) {
        ret = ec_master_init(&masters[i], i, macs[i][0], macs[i][1],
                    device_number, class, debug_level);
       ...
    }

2.1 Master Phases

igh中,狀態機是其核心思想,一切操作基於狀態機來執行,對創建的每個EtherCAT主站實例都需要經過如下階段轉換(見圖2.3),主站各階段操作如下:

image-20200917142438559

Orphaned phase 此時主站實例已經分配初始化,正在等待以太網設備連接,即還沒有與網卡驅動聯系起來,此時無法使用總線通訊。

Idle phase 當主站與網卡綁定后,Idle線程ec_master_idle_thread開始運行,主站處於IDLE狀態,ec_master_idle_thread主要完成從站拓撲掃描、配置站點地址等工作。該階段命令行工具能夠訪問總線,但無法進行過程數據交換,因為還缺少總線配置。

Operation phase 應用程序請求主站提供總線配置並激活主站后,主站處於operation狀態,ec_master_idle_thread停止運行,內核線程變為ec_master_operation_thread,之后應用可周期交換過程數據。

具體的后面會說到。

2.2 數據報與狀態機

繼續看master初始化代碼ec_master_init前,我們先了解數據報與狀態機的關系,這對后續理解很有幫助。

數據報

EtherCAT是以以太網為基礎的現場總線系統,EtherCAT使用標准的IEEE802.3以太網幀,在主站一側使用標准的以太網控制器,不需要額外的硬件。並在以太網幀頭使用以太網類型0x88A4來和其他以太網幀相區別(EtherCAT數據還可以通過UDP/IP 來傳輸,本文已忽略),標准的IEEE802.3以太網幀中數據部分為EtherCAT的數據,標准的IEEE802.3以太網幀與EtherCAT數據幀關系如下:

ethercat-fram-format

EtherCAT數據位於以太網幀數據區,EtherCAT數據由EtherCAT頭若干EtherCAT數據報文組成。其中EtheRCAT頭中記錄了EtherCAT數據報的長度、和類型,類型為1表示與從站通訊。EtherCAT數據報文內包含多個子報文,每個子報文又由子報文頭、數據和WKC域組成。子報文結構含義如下。

datagram-hy

整個EtherCAT網絡形成一個環狀,主站與從站之間是通過EtherCAT數據報來交互,一個EtherCAT報文從網卡TX發出后,從站ESC芯片EtherCAT報文進行交換數據,最后該報文回到主站。網上有個經典的EtherCAT動態圖(刷新后動態圖重新播放).

認識EtherCAT數據幀結構后,我們看IgH內是如何表示一個EtherCAT數據報文的?EtherCAT數據報文在igh中用對象ec_datagram_t表示。

typedef struct {
    struct list_head queue; /**< 發送和接收時插入主站幀隊列. */
    struct list_head sent; /**< 已發送數據報的主站列表項. */
    ec_device_index_t device_index; /**< 發送/接收數據報的設備。 */
	
    ec_datagram_type_t type; /**< 幀類型 (APRD, BWR, etc.). */
    uint8_t address[EC_ADDR_LEN]; /**< Recipient address. */
    uint8_t *data; /**< 數據. */
    ec_origin_t data_origin; /**< 數據保存的地方. */
    size_t mem_size; /**< Datagram \a data memory size. */
    size_t data_size; /**< Size of the data in \a data. */
    uint8_t index; /**< Index (set by master). */
    uint16_t working_counter; /**< 工作計數. */
    ec_datagram_state_t state; /**數據幀狀態 */
#ifdef EC_HAVE_CYCLES
    cycles_t cycles_sent; /**< Time, 數據報何時發送. */
#endif
    unsigned long jiffies_sent; /**< Jiffies,數據報何時發送. */
#ifdef EC_HAVE_CYCLES
    cycles_t cycles_received; /**< Time, 何時被接收. */
#endif
    unsigned long jiffies_received; /**< Jiffies,何時被接收. */
    unsigned int skip_count; /**< 尚未收到的重新排隊數. */
    unsigned long stats_output_jiffies; /**< Last statistics output. */
    char name[EC_DATAGRAM_NAME_SIZE]; /**< Description of the datagram. */
} ec_datagram_t;

可以看到上面子報文中各字段大都在ec_datagram_t中有表示了,不過多介紹,其它幾個成員簡單介紹下,

device_index表示這個數據報是屬於哪個網卡設備發送接收的,IgH中一個master實例可以使用多個多個網卡設備來發送/接收EtherCAT數據幀,device_index就是表示master下的網絡設備的。

data_origin表示每個master管理着多個空閑的ec_datagram_t,這些ec_datagram_t分成了三類,給不同的狀態機使用,具體的后文馬上會細說,data_origin就是表示這個ec_datagram_t是屬於哪類的。

index表示該數據報是EtherCAT數據區的第index個子報文,在master發送一個完整的EtherCAT數據報時,組裝以太網數據幀時使用。

data這個指向子報文的數據內存區,由於每個子報文交換數據不同,其大小不同,所以數據區為動態分配,mem_size表示的就是分配的大小。

data_size表示數據報操作的數據大小,比如該數據報用於讀從站的某個寄存器,該值就是這個寄存器的大小。

jiffies_sent、jiffies_received、cycles_sent、cycles_received使用不同時鍾方式是記錄數據幀發送接收時間的,用於統計。

ec_datagram_state_t表示數據報的狀態,每個數據報(ec_datagram_t)也是基於狀態來處理,有6種狀態:

  • EC_DATAGRAM_INIT :數據報已經初始化
  • EC_DATAGRAM_QUEUED :插入發送隊列准備發送
  • EC_DATAGRAM_SENT :已經發送(還存在隊列中)
  • EC_DATAGRAM_RECEIVED:該數據報已接收,並從發送隊列刪除
  • EC_DATAGRAM_TIMED_OUT :該數據報發送后,接收超時,從發送隊列刪除
  • EC_DATAGRAM_ERROR :發送和接收過程中出錯(從隊列刪除),校驗錯誤、不匹配等。

M位在master發送時組裝EtherCAT數據幀時確定,接收時也根據該位判斷后面還有沒有子報文。

數據報對象初始化由函數ec_datagram_init()完成:

void ec_datagram_init(ec_datagram_t *datagram /**< EtherCAT datagram. */)
{
    INIT_LIST_HEAD(&datagram->queue); // mark as unqueued
    datagram->device_index = EC_DEVICE_MAIN;  /*默認主設備使用*/
    datagram->type = EC_DATAGRAM_NONE;		/*數據報類型*/
    memset(datagram->address, 0x00, EC_ADDR_LEN);  /*數據報地址清零*/
    datagram->data = NULL;
    datagram->data_origin = EC_ORIG_INTERNAL; 	/*默認內部數據*/
    datagram->mem_size = 0;
    datagram->data_size = 0;
    datagram->index = 0x00;
    datagram->working_counter = 0x0000;
    datagram->state = EC_DATAGRAM_INIT; /*初始狀態*/
#ifdef EC_HAVE_CYCLES
    datagram->cycles_sent = 0;
#endif
    datagram->jiffies_sent = 0;
#ifdef EC_HAVE_CYCLES
    datagram->cycles_received = 0;
#endif
    datagram->jiffies_received = 0;
    datagram->skip_count = 0;
    datagram->stats_output_jiffies = 0;
    memset(datagram->name, 0x00, EC_DATAGRAM_NAME_SIZE);
}

狀態機

說完IgH數據報對象,我們來看有限狀態機(fsm),有限狀態機(fsm):表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學模型。說起狀態機,相信大家大學時候都有用過吧,不管是單片機、FPGA,用它寫按鍵、菜單、協議處理、控制器什么的爽的一塌糊塗。其實我們使用的計算機就是本就是基於狀態機作為計算模型的,它對數字系統的設計具有十分重要的作用。另外Linux TCP協議也是由狀態機實現。

同樣igh主站內部機制使用有限狀態機來實現,IgH內狀態機的基本表示如下:

struct ec_fsm_xxx {
    ec_datagram_t *datagram; /**< 主站狀態機使用的數據報對象 */
    void (*state)(ec_fsm_master_t *); /**< 狀態函數 */
    ....
};

IgH EtherCAT協議棧幾乎所有功能通過狀態機實現,每個狀態機管理着某個對象的狀態、功能實現的狀態裝換,而這些狀態轉換是基於EtherCAT數據報來進行的,如狀態機A0狀態函數填充datagram,經EtherCAT數據幀發出后,經過slave ESC處理,回到網卡接收端接收后,交給狀態機A1狀態的下一個狀態函數解析處理。所以每個狀態機內都包含有指向該狀態機操作的數據報對象指針datagram和狀態執行的狀態函數void (*state)(ec_fsm_master_t *)

總結一句話:狀態機是根據數據報的狀態來執行,每個狀態機都需要操作一個數據報對象

現在知道了狀態機與數據報的關系,下面介紹IgH EtherCAT協議棧中有哪些狀態機,及狀態機使用的數據報對象是從哪里分配如何管理的。

master狀態機

前面說到主站具有的三個階段,當主站與網卡設備attach后進入Idle phase,處於Idle phase后,開始執行主站狀態機。

主站狀態機包含1個主狀態機和許多子狀態機,matser狀態機主要目的是:

  • Bus monitoring 監控EtherCAT總線拓撲結構,如果發生改變,則重新掃描。

  • Slave con fguration 監視從站的應用程序層狀態。如果從站未處於其應有的狀態,則從站將被(重新)配置

  • Request handling 請求處理(源自應用程序或外部來源),主站任務應該處理異步請求,例如:SII訪問,SDO訪問或類似。

主狀態機ec_fsm_master_t結構如下:

struct ec_fsm_master {
    ec_master_t *master; /**< master the FSM runs on */
    ec_datagram_t *datagram; /**< 主站狀態機使用的數據報對象 */
    unsigned int retries; /**< retries on datagram timeout. */

    void (*state)(ec_fsm_master_t *); /**< master state function */
    ec_device_index_t dev_idx; /**< Current device index (for scanning etc.).
                                */
    int idle; /**< state machine is in idle phase */
    unsigned long scan_jiffies; /**< beginning of slave scanning */
    uint8_t link_state[EC_MAX_NUM_DEVICES]; /**< Last link state for every
                                              device. */
    unsigned int slaves_responding[EC_MAX_NUM_DEVICES]; /**<每個設備的響應從站數。*/
    unsigned int rescan_required; /**< A bus rescan is required. */
    ec_slave_state_t slave_states[EC_MAX_NUM_DEVICES]; /**< AL states of
                                                         responding slaves for
                                                         every device. */
    ec_slave_t *slave; /**< current slave */
    ec_sii_write_request_t *sii_request; /**< SII write request */
    off_t sii_index; /**< index to SII write request data */
    ec_sdo_request_t *sdo_request; /**< SDO request to process. */

    ec_fsm_coe_t fsm_coe; /**< CoE state machine */
    ec_fsm_soe_t fsm_soe; /**< SoE state machine */
    ec_fsm_pdo_t fsm_pdo; /**< PDO configuration state machine. */
    ec_fsm_change_t fsm_change; /**< State change state machine */
    ec_fsm_slave_config_t fsm_slave_config; /**< slave state machine */
    ec_fsm_slave_scan_t fsm_slave_scan; /**< slave state machine */
    ec_fsm_sii_t fsm_sii; /**< SII state machine */
};

可以看到,主站狀態機結構下還有很多子狀態機,想象一下如果主站的所有功能通過一個狀態機來完成,那么這個狀態機的狀態數量、各狀態之間的聯系會有多恐怖,復雜性級別將會提高到無法管理的水平。為此,IgH中,將EtherCAT主狀態機的某些功能用子狀態機完成。這有助於封裝相關工作流,並且避免“狀態爆炸”現象。這樣當主站完成coe功能時,可以由子狀態機fsm_coe去完成。具體各功能是如何通過狀態機完成的,文章后面會介紹。

slave狀態機

slave狀態機管理着每個從站的狀態,所以位於從站對象(ec_slave_t)內:

struct ec_slave
{
    ec_master_t *master; /**< Master owning the slave. */
.....
    ec_fsm_slave_t fsm; /**< Slave state machine. */
.....
};
struct ec_fsm_slave {
    ec_slave_t *slave; /**< slave the FSM runs on */
    struct list_head list; /**< Used for execution list. */
    ec_dict_request_t int_dict_request; /**< Internal dictionary request. */

    void (*state)(ec_fsm_slave_t *, ec_datagram_t *); /**< State function. */
    ec_datagram_t *datagram; /**< Previous state datagram. */
    ec_sdo_request_t *sdo_request; /**< SDO request to process. */
    ec_reg_request_t *reg_request; /**< Register request to process. */
    ec_foe_request_t *foe_request; /**< FoE request to process. */
    off_t foe_index; /**< Index to FoE write request data. */
    ec_soe_request_t *soe_request; /**< SoE request to process. */
    ec_eoe_request_t *eoe_request; /**< EoE request to process. */
    ec_mbg_request_t *mbg_request; /**< MBox Gateway request to process. */
    ec_dict_request_t *dict_request; /**< Dictionary request to process. */

    ec_fsm_coe_t fsm_coe; /**< CoE state machine. */
    ec_fsm_foe_t fsm_foe; /**< FoE state machine. */
    ec_fsm_soe_t fsm_soe; /**< SoE state machine. */
    ec_fsm_eoe_t fsm_eoe; /**< EoE state machine. */
    ec_fsm_mbg_t fsm_mbg; /**< MBox Gateway state machine. */
    ec_fsm_pdo_t fsm_pdo; /**< PDO configuration state machine. */
    ec_fsm_change_t fsm_change; /**< State change state machine */
    ec_fsm_slave_scan_t fsm_slave_scan; /**< slave scan state machine */
    ec_fsm_slave_config_t fsm_slave_config; /**< slave config state machine. */
};

slave狀態機和master狀態機類似,slave狀態機內還包含許多子狀態機。slave狀態機主要目的是:

  • 主站管理從站狀態
  • 主站與從站應用層(AL)通訊。比如具有EoE功能的從站,主站通過該從站下的子狀態機fsm_eoe來管理主站與從站應用層的EOE通訊。

數據報對象的管理

上面簡單介紹了IgH內的狀態機,狀態機輸入輸出的對象是datagram,fsm對象內只有數據報對象的指針,那fsm工作過程中的數據報對象從哪里分配?

由於每個循環周期都需要操作數據報對象,IgH為減少datagram的動態分配操作,提高主站性能,在master初始化的時候預分配了主站運行需要的所有datagram對象。在master實例我們可以看到下面的數據報對象:

struct ec_master {
    ...
    ec_datagram_t fsm_datagram; /**< Datagram used for state machines. */
    ...
    ec_datagram_t ref_sync_datagram; /**< Datagram used for synchronizing the
                                       reference clock to the master clock.*/
    ec_datagram_t sync_datagram; /**< Datagram used for DC drift
                                   compensation. */
    ec_datagram_t sync_mon_datagram; /**< Datagram used for DC synchronisation
                                       monitoring. */
    ...
    ec_datagram_t ext_datagram_ring[EC_EXT_RING_SIZE]; 
}

這些數據報對象都是已經分配內存的,但由於報文不同,報文操作的數據大小不同,所以datagram數據區大小隨狀態機的具體操作而變化,在具體使用時才分配數據區內存。
以上數據報對象給狀態機使用,別忘了還有過程數據也需要數據報對象,所以IgH中數據報類型分為以下四類:
分為三類(非常重要):

數據報對象 用途
Datagram_pairs 過程數據報
fsm_datagram[] Fsm_master及子狀態機專用的數據報對象。
ext_datagram_ring[] 動態分配給fsm_slave及其子fsm。
ref_sync_datagram
sync_datagram
sync64_datagram
sync_mon_datagram
應用專用數據報用於時鍾同步。

其中fsm_datagram為master狀態機及master下的子狀態機執行過程中操作的對象。

ext_datagram_ring[]是一個環形隊列,當fsm_slave從站狀態機處於ready狀態,可以開始處理與slave相關請求,如配置、掃描、SDO、PDO等,這時會從ext_datagram_ring[]中給該fsm_slave分配一個數據報,並運行fsm_slave狀態機檢查並處理請求。

應用專用數據報用於時鍾同步,與時鍾強相關,它們比較特殊,它們的數據區大小是恆定的,所以其數據區在主站初始化時就已分配內存,應用調用時直接填數據發送,避免linux的內存分配帶來時鍾的偏差。

數據報數據區(data)內存通過ec_datagram_prealloc()來分配.

int ec_datagram_prealloc(
        ec_datagram_t *datagram, /**< EtherCAT datagram. */
        size_t size /**< New payload size in bytes. */
        )
{
    if (datagram->data_origin == EC_ORIG_EXTERNAL
            || size <= datagram->mem_size)
        return 0;
	......

    if (!(datagram->data = kmalloc(size, GFP_KERNEL))) {
	......
    }

    datagram->mem_size = size;
    return 0;
}

數據區的大小為一個以太網幀中單個Ethercat數據報的最大數據大小EC_MAX_DATA_SIZE

/** Size of an EtherCAT frame header. */
#define EC_FRAME_HEADER_SIZE 2

/** Size of an EtherCAT datagram header. */
#define EC_DATAGRAM_HEADER_SIZE 10

/** Size of an EtherCAT datagram footer. */
#define EC_DATAGRAM_FOOTER_SIZE 2

/** Size of the EtherCAT address field. */
#define EC_ADDR_LEN 4

/** Resulting maximum data size of a single datagram in a frame. */
#define EC_MAX_DATA_SIZE (ETH_DATA_LEN - EC_FRAME_HEADER_SIZE \
                          - EC_DATAGRAM_HEADER_SIZE - EC_DATAGRAM_FOOTER_SIZE)

由於以太網幀的大小有限,因此數據報的最大大小受到限制,即以太網幀長度 1500 - ethercat頭2byte- ethercat子數據報報頭10字節-WKC 2字節,如圖:

image-20210222222428543

如果過程數據鏡像的大小超過該限制,就必須發送多個幀,並且必須對映像進行分區以使用多個數據報。 Domain自動進行管理。

2.3 master狀態機及數據報初始化

對狀態機及數據報對象有初步認識后,我們回到ec_master.ko模塊入口函數ec_init_module()主站實例初始化ec_master_init(),主要完成主站狀態機初始化及數據報:

    // init state machine datagram
    ec_datagram_init(&master->fsm_datagram); /*初始化數據報對象*/
    snprintf(master->fsm_datagram.name, EC_DATAGRAM_NAME_SIZE, "master-fsm");
    ret = ec_datagram_prealloc(&master->fsm_datagram, EC_MAX_DATA_SIZE);

    // create state machine object
    ec_fsm_master_init(&master->fsm, master, &master->fsm_datagram); /*初始化master fsm*/

其中ec_fsm_master_init初始化master fsm和子狀態機,並指定了master fsm使用的數據報對象fsm_datagram

void ec_fsm_master_init(
        ec_fsm_master_t *fsm, /**< Master state machine. */
        ec_master_t *master, /**< EtherCAT master. */
        ec_datagram_t *datagram /**< Datagram object to use. */
        )
{
    fsm->master = master;
    fsm->datagram = datagram;
    ec_fsm_master_reset(fsm);

    // init sub-state-machines
    ec_fsm_coe_init(&fsm->fsm_coe);
    ec_fsm_soe_init(&fsm->fsm_soe);
    ec_fsm_pdo_init(&fsm->fsm_pdo, &fsm->fsm_coe);
    ec_fsm_change_init(&fsm->fsm_change, fsm->datagram);
    ec_fsm_slave_config_init(&fsm->fsm_slave_config, fsm->datagram,
            &fsm->fsm_change, &fsm->fsm_coe, &fsm->fsm_soe, &fsm->fsm_pdo);
    ec_fsm_slave_scan_init(&fsm->fsm_slave_scan, fsm->datagram,
            &fsm->fsm_slave_config, &fsm->fsm_pdo);
    ec_fsm_sii_init(&fsm->fsm_sii, fsm->datagram);
}

初始化外部數據報隊列

外部數據報隊列用於從站狀態機,每個狀態機執行期間使用的數據報從該區域分配,下面是初始化ext_datagram_ring中每個結構:

     for (i = 0; i < EC_EXT_RING_SIZE; i++) {
        ec_datagram_t *datagram = &master->ext_datagram_ring[i];
        ec_datagram_init(datagram);
        snprintf(datagram->name, EC_DATAGRAM_NAME_SIZE, "ext-%u", i);
    }

非應用數據報隊列鏈表,如EOE數據報會插入該隊列后發送。

INIT_LIST_HEAD(&master->ext_datagram_queue);

同樣初始化幾個時鍾相關數據報對象,它們功能固定,所以數據區大小固定,就不貼代碼了,比如sync_mon_datagram,它的作用是用於同步監控,獲取從站系統時間差,所以是一個BRD數據報,在此直接將數據報操作偏移地址初始化,使用時能快速填充發送。

    ec_datagram_init(&master->sync_mon_datagram);
	......
    ret = ec_datagram_brd(&master->sync_mon_datagram, 0x092c, 4);
地址 名稱 描述 復位值
0x092c~0x092F 0~30 系統時間差 本地系統時間副本與參考時鍾系統時間值之差 0
31 符號 0:本地系統時間≥參考時鍾時間
1:本地系統時間<參考時鍾時間
0

另外比較重要的是將使用的網卡MAC地址放到macs[]中,在網卡驅動probe過程中根據MAC來匹配主站使用哪個網卡。

    for (dev_idx = EC_DEVICE_MAIN; dev_idx < EC_MAX_NUM_DEVICES; dev_idx++) {
        master->macs[dev_idx] = NULL;
    }

    master->macs[EC_DEVICE_MAIN] = main_mac;

2.4 初始化EtherCAT device

master協議棧主要完成EtherCAT數據報的解析和組裝,然后需要再添加EtherNet報頭和FCS組成一個完整的以太網幀,最后通過網卡設備發送出去。為與以太網設備驅動層解耦,igh使用ec_device_t來封裝底層以太網設備,一般來說每個master只有一個ec_device_t,這個編譯時配置決定,若啟用線纜冗余功能,可指定多個網卡設備:

struct ec_device
{
    ec_master_t *master; /**< EtherCAT master */
    struct net_device *dev; /**< 使用的網絡設備 */
    ec_pollfunc_t poll; /**< pointer to the device's poll function */
    struct module *module; /**< pointer to the device's owning module */
    uint8_t open; /**< true, if the net_device has been opened */
    uint8_t link_state; /**< device link state */
    struct sk_buff *tx_skb[EC_TX_RING_SIZE]; /**< transmit skb ring */
    unsigned int tx_ring_index; /**< last ring entry used to transmit */
#ifdef EC_HAVE_CYCLES
    cycles_t cycles_poll; /**< cycles of last poll */
#endif
#ifdef EC_DEBUG_RING
    struct timeval timeval_poll;
#endif
    unsigned long jiffies_poll; /**< jiffies of last poll */

    // Frame statistics
    u64 tx_count; /**< 發送的幀數 */
    u64 last_tx_count; /**<上次統計周期發送的幀數。 */
    u64 rx_count; /**< 接收的幀數 */
    u64 last_rx_count; /**< 上一個統計周期收到的幀數。 */
	
    u64 tx_bytes; /**< 發送的字節數 */
    u64 last_tx_bytes; /**< 上一個統計周期發送的字節數。 */
    u64 rx_bytes; /**< Number of bytes received. */
    u64 last_rx_bytes; /**< Number of bytes received of last statistics cycle.
                        */
    u64 tx_errors; /**< Number of transmit errors. */
    s32 tx_frame_rates[EC_RATE_COUNT]; /**< Transmit rates in frames/s for
                                         different statistics cycle periods.
                                        */
    s32 rx_frame_rates[EC_RATE_COUNT]; /**< Receive rates in frames/s for
                                         different statistics cycle periods.
                                        */
    s32 tx_byte_rates[EC_RATE_COUNT]; /**< Transmit rates in byte/s for
                                        different statistics cycle periods. */
    s32 rx_byte_rates[EC_RATE_COUNT]; /**< Receive rates in byte/s for
                                        different statistics cycle periods. */

......
};

成員*master表示改對象屬於哪個master,*dev指向使用的以太網設備net_device,poll該網絡設備poll函數,tx_skb[]以太網幀發送緩沖區隊列,需要發送的以太網幀會先放到該隊里,tx_ring_index管理tx_skb[],以及一些網絡統計變量,下面初始化ec_device_t對象:

/*\master\master.c*/
    for (dev_idx = EC_DEVICE_MAIN; dev_idx < ec_master_num_devices(master);
            dev_idx++) {
        ret = ec_device_init(&master->devices[dev_idx], master);
        if (ret < 0) {
            goto out_clear_devices;
        }
    }
/*\master\device.c*/
int ec_device_init(
        ec_device_t *device, /**< EtherCAT device */
        ec_master_t *master /**< master owning the device */
        )
{
    int ret;
    unsigned int i;
    struct ethhdr *eth;
....

    device->master = master;
    device->dev = NULL;
    device->poll = NULL;
    device->module = NULL;
    device->open = 0;
    device->link_state = 0;
    for (i = 0; i < EC_TX_RING_SIZE; i++) {
        device->tx_skb[i] = NULL;
    }
......

    ec_device_clear_stats(device);
......
    for (i = 0; i < EC_TX_RING_SIZE; i++) {
        if (!(device->tx_skb[i] = dev_alloc_skb(ETH_FRAME_LEN))) {
            ......
        }

        // add Ethernet-II-header
        skb_reserve(device->tx_skb[i], ETH_HLEN);
        eth = (struct ethhdr *) skb_push(device->tx_skb[i], ETH_HLEN);
        eth->h_proto = htons(0x88A4);
        memset(eth->h_dest, 0xFF, ETH_ALEN);
    }
.....
}

主要關注分配以太網幀發送隊列內存tx_skb[],並填充Ethernet報頭中的以太網類型字段為0x88A4,目標MAC地址0xFFFFFFFF FFFF,對於源MAC地址、sk_buff所屬網絡設備、ec_device_t對象使用的網絡設備net_device,將在網卡驅動初始化與master建立聯系過程中設置。

2.5 設置IDLE 線程的發送間隔:

ec_master_set_send_interval(master, 1000000 / HZ);

根據網卡速率計算:

void ec_master_set_send_interval(
        ec_master_t *master, /**< EtherCAT master */
        unsigned int send_interval /**< Send interval */
        )
{
    master->send_interval = send_interval;  //發送間隔 us
    master->max_queue_size =
        (send_interval * 1000) / EC_BYTE_TRANSMISSION_TIME_NS;
    master->max_queue_size -= master->max_queue_size / 10;
}

100Mbps網卡發送一字節數據需要的時間EC_BYTE_TRANSMISSION_TIME_NS: 1/(100 MBit/s / 8 bit/byte) = 80 ns/byte.

2.6 初始化字符設備

由於主站位於內核空間,用戶空間應用與主站交互通過字符設備來交互;
創建普通字符設備,給普通linux應用和Ethercat tool使用。若使用xenomai或RTAI,則再創建實時字符設備,提供給實時應用使用。

	......
	master->class_device = device_create(class, NULL,
            MKDEV(MAJOR(device_number), master->index), NULL,
            "EtherCAT%u", master->index);
    ......
#ifdef EC_RTDM
    // init RTDM device
    ret = ec_rtdm_dev_init(&master->rtdm_dev, master);
   ...
#endif

到這里明白了IgH中的狀態機與數據報之間的關系,主站對象也創建好了,但是主站還沒有網卡設備與之關聯,主站也還沒有工作,下面簡單看一下ecdev_offer流程。

關於網卡驅動代碼詳細解析推薦這兩篇文章:

Monitoring and Tuning the Linux Networking Stack: Sending Data

Monitoring and Tuning the Linux Networking Stack: Receiving Data

3 網卡

  1. 網卡probe

    static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
    {	
        ......
    	adapter->ecdev = ecdev_offer(netdev, ec_poll, THIS_MODULE);
    	if (adapter->ecdev) {	/*注冊打開ec_net設備*/
    		err = ecdev_open(adapter->ecdev);
    		.....
    		adapter->ec_watchdog_jiffies = jiffies;
    	} else { /*注冊普通網絡設備*/
    		......
    		err = register_netdev(netdev);
    		......
    	}
        ......
    }
    
  2. 給主站提供網絡設備:ecdev_offer

    根據MAC地址找到master下的ec_device_t對象

        device->dev = net_dev;
        device->poll = poll;
        device->module = module;
    

    上面我們只設置了ec_device_t->tx_skb[]sk_buff的以太網類型和目的地址,現在繼續填充源MAC地址為網卡的MAC地址、sk_buff所屬的net_device:

        for (i = 0; i < EC_TX_RING_SIZE; i++) {
            device->tx_skb[i]->dev = net_dev;
            eth = (struct ethhdr *) (device->tx_skb[i]->data);
            memcpy(eth->h_source, net_dev->dev_addr, ETH_ALEN);
        }
    
  3. 調用網絡設備接口打開網絡設備

int ec_device_open(
        ec_device_t *device /**< EtherCAT device */
        )
{
    int ret;
.....
    ret = device->dev->open(device->dev);
    if (!ret)
        device->open = 1;
....
    return ret;
}
  1. 當master下的所有的網絡設備都open后,master從ORPHANED轉到IDLE階段
int ec_master_enter_idle_phase(
        ec_master_t *master /**< EtherCAT master */
        )
{
    int ret;
    ec_device_index_t dev_idx;
	......
    master->send_cb = ec_master_internal_send_cb;
    master->receive_cb = ec_master_internal_receive_cb;
    master->cb_data = master;

    master->phase = EC_IDLE;  /*更新master狀態*/

    // reset number of responding slaves to trigger scanning
    for (dev_idx = EC_DEVICE_MAIN; dev_idx < ec_master_num_devices(master);
            dev_idx++) {
        master->fsm.slaves_responding[dev_idx] = 0;
    }

    ret = ec_master_nrthread_start(master, ec_master_idle_thread,
            "EtherCAT-IDLE");
    ....
    return ret;
}

其中主要設置master發送和接收回調函數,應用通過發送和接收數據時,將通過這兩接口直接發送和接收。創建master idle線程ec_master_idle_thread

4 IDLE階段內核線程

綜上,狀態機操作對象是datagram,datagram發送出去后回到主站交給狀態機的下一個狀態處理,所以主站需要循環地執行狀態機、發送EtherCAT數據幀、接收EtherCAT數據幀、執行狀態機、發送EtherCAT數據幀、……來驅動狀態機運行,這個循環由內核線程來完成。

image-20210523100034721

當主站與網卡綁定后,應用還沒有請求主站,主站處於IDLE狀態,這時循環由內核線程ec_master_idle_thread來完成,主要完成從站拓撲掃描、配置站點地址等工作。

static int ec_master_idle_thread(void *priv_data)
{
    ec_master_t *master = (ec_master_t *) priv_data;
    int fsm_exec;
#ifdef EC_USE_HRTIMER
    size_t sent_bytes;
#endif

    // send interval in IDLE phase
    ec_master_set_send_interval(master, 250000 / HZ);
    
    while (!kthread_should_stop()) {
        // receive
        ecrt_master_receive(master);  /*接收上個循環發送的數據幀*/
		......
        // execute master & slave state machines
        ......
        fsm_exec = ec_fsm_master_exec(&master->fsm); /*執行master狀態機*/

        ec_master_exec_slave_fsms(master); /*為從站狀態機分配datagram,並執行從站狀態機*/
		......
        if (fsm_exec) {
            ec_master_queue_datagram(master, &master->fsm_datagram); /*將master狀態機處理的datagram插入發送鏈表*/
        }
        // send
        ecrt_master_send(master); /*組裝以太網幀並調用網卡發送*/
        
        sent_bytes = master->devices[EC_DEVICE_MAIN].tx_skb[
            master->devices[EC_DEVICE_MAIN].tx_ring_index]->len;
        up(&master->io_sem);

        if (ec_fsm_master_idle(&master->fsm)) {
            ec_master_nanosleep(master->send_interval * 1000);
            set_current_state(TASK_INTERRUPTIBLE);
            schedule_timeout(1);
        } else {
            ec_master_nanosleep(sent_bytes * EC_BYTE_TRANSMISSION_TIME_NS);
        }
    }

    EC_MASTER_DBG(master, 1, "Master IDLE thread exiting...\n");

    return 0;
}

整個過程簡單概述如下。

4.1 數據報發送

下面介紹IgH中狀態機處理后數據報的發送流程(ecrt_master_send())。

image-20210523100210574

master使用一個鏈表datagram_queue來管理要發送的子報文對象datagram,需要發送的子報文對象會先插入該鏈表中,統一發送時,分配一個sock_buff,從datagram_queue上取出報文對象,設置indexindex是發送后接收回來與原報文對應的標識之一),將一個個報文對象按EtherCAT數據幀結構填充到sock_buff中,最后通過網卡設備驅動函數hard_start_xmit,將sock_buff從網卡發送出去。

image-20210523100225964

4.2 數據報接收

image-20210523100246660

接收數據時,通過網卡設備驅動ec_poll函數取出Packet得到以太網數據,然后解析其中的EtherCAT數據幀,解析流程如下:

  1. 得到子報文index,遍歷發送鏈表datagram_queue找到index對應的datagram

  2. 將子報文數據拷貝到datagram數據區。

  3. 將以太網幀內子報文中的WKC值復制到datagram中的WKC。

  4. datagram從鏈表datagram_queue刪除。

  5. 根據子報文頭M位判斷還有沒有子報文,有則跳轉1繼續處理下一個子報文,否則完成接收。

接收完成后,進入下一個循環,內核線程運行狀態機或周期應用進行下一個周期,處理接收的Ethercat報文。

先簡單介紹到這,敬請關注后續文章。。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM