레이블이 EtherCAT인 게시물을 표시합니다. 모든 게시물 표시
레이블이 EtherCAT인 게시물을 표시합니다. 모든 게시물 표시

2018년 2월 14일 수요일

[EtherCAT] IgH EtherCAT Master Stack API 분석 예제

[준비물]


- IgH EtherCAT Master Stack 1.5.2와 Xenomai OS가 설치된 제어기
- 위 플랫폼에 대한 개발환경 (툴체인, 라이브러리 등)
- Digital I/O EtherCAT Slave 장치


[참고자료]



[예제코드]


#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

#include <native/task.h>
#include <native/timer.h>
#include <ecrt.h>

#define INTERVAL 1000000

static void rt_task_proc(void *arg);
static void sigint_handler(int sig);

/* PDO list to use in application */
unsigned int slave0_6000_01;
unsigned int slave0_6000_01_bit;
unsigned int slave0_7010_01;
unsigned int slave0_7010_01_bit;

static ec_pdo_entry_reg_t pdo_entry_reg[] = {
    {0, 0, 0x0, 0x0, 0x6000, 1, &slave0_6000_01, &slave0_6000_01_bit},
    {0, 0, 0x0, 0x0, 0x7010, 1, &slave0_7010_01, &slave0_7010_01_bit},
    {}
};

static ec_master_t* master = NULL;
static ec_domain_t* domain = NULL;
static uint8_t* domain_pd = NULL;

static int alive = 1;

int main(int argc, char** argv)
{
    int i;
    int ret = 0;

    ec_master_info_t master_info;
    ec_slave_info_t* slave_info_list = NULL;
    ec_slave_config_t* slave = NULL;
    int slave_count = 0;

    RT_TASK* rt_task_plc = NULL;

    /* signal handler registration */
    signal(SIGINT, sigint_handler);

    /* configure master */
    master = ecrt_request_master(0);
    if(master == NULL)
    {
        printf("EtherCAT master request failed!\n");
        return 1;
    }

    domain = ecrt_master_create_domain(master);
    if(domain == NULL)
    {
        printf("EtherCAT domain creation failed!\n");
        ret = 1;
        goto CLEANUP;
    }

    ret = ecrt_master(master, &master_info);
    if(ret != 0)
    {
        printf("EtherCAT master information request failed!\n");
        ret = -ret;
        goto CLEANUP;
    }

    /* allocate momory for slave information */
    slave_count = master_info.slave_count;
    slave_info_list = (ec_slave_info_t*)malloc(sizeof(ec_slave_info_t) * slave_count);

    /* configure slaves */
    for(i = 0; i < slave_count; i++)
    {
        ret = ecrt_master_get_slave(master, i, &slave_info_list[i]);
        if(ret != 0)
        {
            printf("EtherCAT slave information request failed!\n");
            ret = -ret;
            goto CLEANUP;
        }

        slave = ecrt_master_slave_config(master, 0, i, slave_info_list[i].vendor_id,
            slave_info_list[i].product_code);
        if(slave == NULL)
        {
            printf("EtherCAT slave configuration failed!\n");
            ret = 1;
            goto CLEANUP;
        }
    }

    /* setup PDO registration array */
    for(i = 0; pdo_entry_reg[i].index != 0; i++)
    {
        if(pdo_entry_reg[i].position < slave_count)
        {
            pdo_entry_reg[i].vendor_id =
                slave_info_list[pdo_entry_reg[i].position].vendor_id;
            pdo_entry_reg[i].product_code =
                slave_info_list[pdo_entry_reg[i].position].product_code;
        }
    }

    /* get PDO entry list */
    ret = ecrt_domain_reg_pdo_entry_list(domain, pdo_entry_reg);
    if(ret != 0)
    {
        printf("EtherCAT PDO registration failed!\n");
        goto CLEANUP;
    }

    /* create real-time periodic task */
    rt_task_plc = (RT_TASK*)malloc(sizeof(RT_TASK));
    ret = rt_task_create(rt_task_plc, "rt_task_plc", 0, 50, T_JOINABLE);
    if(ret != 0)
    {
        printf("Real-time task creation failed!\n");
        goto CLEANUP;
    }

    /* activate EtherCAT master */
    ret = ecrt_master_set_send_interval(master, INTERVAL);
    if(ret != 0)
    {
        printf("EtherCAT setting send interval failed!\n");
        ret = -ret;
        goto CLEANUP;
    }

    ret = ecrt_master_activate(master);
    if(ret != 0)
    {
        printf("EtherCAT master activation failed!\n");
        ret = -ret;
        goto CLEANUP;
    }

    /* get PDO domain pointer */
    domain_pd = ecrt_domain_data(domain);
    if(domain_pd == NULL)
    {
        printf("EtherCAT mapping process data failed!\n");
        ret = 1;
        goto CLEANUP;
    }

    /* start real-time periodic task */
    ret = rt_task_start(rt_task_plc, &rt_task_proc, rt_task_plc);
    if(ret != 0)
    {
        printf("Real-time task start failed!\n");
        goto CLEANUP;
    }
    rt_task_join(rt_task_plc);

CLEANUP :
    if(slave_info_list != NULL)
        free(slave_info_list);
    if(rt_task_plc != NULL)
        free(rt_task_plc);

    if(master != NULL)
        ecrt_release_master(master);

    return ret;
}

static void rt_task_proc(void *arg)
{
    int sw, led;
    int count = 0;

    RT_TASK* rt_task_plc = (RT_TASK*)arg;
    RTIME current_time = rt_timer_read();

    /* set real-time task timer */
    rt_task_set_periodic(rt_task_plc, current_time + INTERVAL,
        rt_timer_ns2ticks(INTERVAL));

    while(alive)
    {
        /* retrieve */
        ecrt_master_receive(master);
        ecrt_domain_process(domain);
        sw = EC_READ_BIT(domain_pd + slave0_6000_01, slave0_6000_01_bit);

        /* computation */
        if(sw)
        {
            if(++count >= 500)
            {
                led = !led;
                count = 0;
            }
        }
        else
        {
            led = 0;
            count = 0;
        }

        /* publish */
        EC_WRITE_BIT(domain_pd + slave0_7010_01, slave0_7010_01_bit, led);        ecrt_domain_queue(domain);
        ecrt_master_send(master);

        /* wait until next period */
        rt_task_wait_period(NULL);
    }
}

static void sigint_handler(int sig)
{
    alive = 0;
}


[설명]


  본 예제 코드는 slave 장치의 스위치 (OD index=0x6000, subindex=0x1로 정의된)가 on 상태인 경우 LED (OD index=0x7010, subindex=0x1로 정의된)를 1Hz (0.5초마다 출력값 토글)하는 응용이다.
  IgH와 같은 역할을 하는 SOEM을 활용한 마스터 예제 코드와 비교해서 IgH가 더 성능도 좋고 기능도 많기 때문인지 예제 코드의 양이 많은 편이다.


[마스터 초기화]


1  master = ecrt_request_master(0);
2  domain = ecrt_master_create_domain(master);
3  ecrt_master(master, &master_info);
4  slave_count = master_info.slave_count;

  마스터 초기화는 현재 프로그램이 실행되고 있는 마스터 장치에서 0번 EtherCAT 인터페이스에 대한 오프젝트(master)를 받아온 후 그에 대한 도메인 오프젝트(domain)을 받는 것으로 이루어진다. 마스터 오브젝트(master)가 마스터 장치를 의미한다면, 도메인(domain)은 주기적으로 통신할 데이터(PDO)들을 의미한다.
  마스터 오브젝트의 필드는 IgH 라이브러리 내에서만 접근할 수 있도록 감추어져 있기 때문에 슬레이브 장치의 수 등 정보를 알기 위해서는 3 line 처럼 마스터 오브젝트의 정보를 받아와야 한다. 슬레이브 장치의 수는 4 line과 같이 알 수 있다.


[슬레이브 초기화]


1  for(i = 0; i < slave_count; i++)
2  {
3      ecrt_master_get_slave(master, i, &slave_info_list[i]);
4      ecrt_master_slave_config(master, 0, i, slave_info_list[i].vendor_id,
           slave_info_list[i].product_code);
5  }

  슬레이브 초기화를 위한 함수 ecrt_master_slave_config()는 초기화하려는 슬레이브의 Vendor ID와 Product code를 필요로 한다. 해당 정보는 ecrt_master_get_slave() 함수로 얻어올 수 있다.
  본 예제와 같이 슬레이브로부터 필요한 정보를 받아와서 초기화를 하는 방법이 있고 마스터에서 미리 설정된 정보를 이용하여 직접 초기화 하는 방법이 있다. 전자의 경우 슬레이브 내에 설정 정보들이 저장되어 있는 SII(Slave Information Interface)로부터 마스터가 초기화에 필요한 정보를 읽어 초기화가 이루어지며, 후자의 경우 사용자가 특정 슬레이브의 정보가 XML 포맷으로 정의된 ESI 파일을 마스터 프로그램(예를 들어 TwinCAT과 같은 프로그램)을 통해 입력하여 초기화가 이루어진다.


[입출력(PDO) 설정]


1  static ec_pdo_entry_reg_t pdo_entry_reg[] = {
2      {0, 0, 0x0, 0x0, 0x6000, 1, &slave0_6000_01, &slave0_6000_01_bit},
3      {0, 0, 0x0, 0x0, 0x7010, 1, &slave0_7010_01, &slave0_7010_01_bit},
4      {}
5  };
6  ecrt_domain_reg_pdo_entry_list(domain, pdo_entry_reg);

  주기적으로 통신할 데이터를 정의하는 입출력 설정은 ec_pdo_entry_reg_t 타입의 구조체와 ecrt_domain_reg_pdo_entry_list() 함수가 사용된다. ec_pdo_entry_reg_t 구조체는 ecrt.h 파일에 다음과 같이 정의되어 있다.

typedef struct {
    uint16_t alias;
    uint16_t position;
    uint32_t vendor_id;
    uint32_t product_code;
    uint16_t index;
    uint8_t subindex;
    unsigned int *offset;
    unsigned int *bit_position;
} ec_pdo_entry_reg_t;

  alias와 position을 어떤 슬레이브 장치인지 특정한다. vendor_id와 product_code는 특정한 슬레이브에 대한 정보로, 해당 슬레이브의 정보와 일치해야 한다. 예제 코드에서는 ecrt_master_get_slave() 함수를 통해 얻은 정보를 이용하여 이 값을 초기화하는 코드가 있다. index와 subindex는 특정한 슬레이브 장치에서 통신할 OD(Object Dictionary)의 index와 subindex를 의미한다. 이와 같은 정보를 입력한 구조체의 배열을 2~3 line과 같이 정의한 후 ecrt_domain_reg_pdo_entry_list() 함수를 호출하면 해당 구조체의 마지막 두 필드에 PDO 엔트리의 byte offset과 bit position이 저장된다. 이 값은 추후에 입출력을 위해 사용된다.


[마스터 활성화]


1  ecrt_master_set_send_interval(master, INTERVAL);
2  ecrt_master_activate(master);
3  domain_pd = ecrt_domain_data(domain);

  마스터가 통신할 주기를 설정하고 활성화한다. 통신할 주기는 ecrt_master_set_send_interval() 함수를 통해 설정할 수 있으며, 두 번째 인자에 주기를 나노초 단위로 입력하면 된다. 이후에 ecrt_master_activate() 함수를 통해 마스터 장치를 활성화한다. 활성화 이후에는 실시간 context로 전환되며 malloc()이나 대부분의 IgH 라이브러리 함수들을 포함하는 비실시간 함수들의 사용은 제한된다. 이 함수가 호출된 이후에는 실시간 함수들만 사용할 수 있다.
  ecrt_domain_data() 함수는 인자로 전달된 도메인에 대한 데이터 필드의 포인터를 받아온다. 이 값도 추후에 입출력을 위해 사용된다.


[실행 단계]


1   while(alive)
2   {
3       /* retrieve */
4       ecrt_master_receive(master);
5       ecrt_domain_process(domain);
6       sw = EC_READ_BIT(domain_pd + slave0_6000_01, slave0_6000_01_bit);
7
8       /* computation */
9       /* write some computation code... */
10
11      /* publish */
12      EC_WRITE_BIT(domain_pd + slave0_7010_01, slave0_7010_01_bit, led);
13      ecrt_domain_queue(domain);
14      ecrt_master_send(master);
15
16      /* wait until next period */
17  }

  실행 단계에서는 ecrt_master_set_send_interval() 함수를 통해 설정한 주기마다 [retrieve] - [computation] - [publish]를 반복한다.
  retrieve에서는 ecrt_mater_receive() 함수와 ecrt_domain_process() 함수가 기본적으로 필요하며, 앞서 입출력 설정에서 입력으로 설정한 데이터를 슬레이브로부터 받아오는 것으로 구성된다. 데이터를 받아오는 기능은 ecrt.h에 매크로로 정의되어 있으며 데이터의 타입별로 매크로가 따로 정의되어 있다. (EC_READ_BIT 등) 이 매크로를 사용할 때 마스터 활성화 단계에서 얻은 도메인 데이터 필드의 포인터(domain_pd)와 함께 입출력 설정에서 ecrt_domain_reg_pdo_entry_list() 함수를 통해 얻은 byte offset과 bit position이 사용된다.
  computation에서는 retrieve에서 읽어오는 데이터를 토대로 publish에서 쓸 데이터를 계산한다. 이 위치의 코드는 응용에 따라 자유롭게 구성된다.
  publish에서는 retrieve와 반대로 먼저 EC_WRITE_BIT 등의 매크로로 도메인 데이터 필드에 값을 쓴 다음 ecrt_domain_queue() 함수와 ecrt_master_send() 함수를 통해 그 값을 슬레이브로 출력한다.

2017년 7월 17일 월요일

[EtherCAT] SOEM을 활용하여 라즈베리파이를 EtherCAT 마스터로 만들기

라즈베리파이에 SOEM을 설치하여 EtherCAT 마스터로 동작하도록 할 것이다.
라즈베리파이는 B+ 모델을 사용했으며, OS로 2017년 03월 02일 버전 raspbian-jessie를 사용했다.

먼저 최신 버전의 SOEM을 다운로드 받는다.

# git clone https://github.com/OpenEtherCATsociety/SOEM

http://openethercatsociety.github.io/ 에서 받을 수 있는 1.3.1 버전에서는 make 기반으로 빌드가 가능했었는데, git에서 받은 버전은 cmake 기반으로 빌드를 한다.

사용하는 라즈베리파이에는 cmake가 설치되어있지 않으니 현재 최신 릴리즈 버전인 3.8.2 버전으로 설치했다. [참고]

# wget https://cmake.org/files/v3.8/cmake-3.8.2.tar.gz
# tar -xvzf cmake-3.8.2.tar.gz
# cd cmake-3.8.2/
# ./bootstrap
# make
# make install

라즈베리파이가 구닥다리라 엄청 오래 걸렸다..

이제 README.md에 나와있는 방법대로 SOEM 빌드를 한다!

# cd SOEM
# mkdir build
# cd build
# cmake ..
# make

빌드하고 나면 몇 가지 예제 프로그램이 생성되는데 그 중 SOEM/build/test/linux/simple_test/ 경로에 있는 simple_test라는 프로그램을 실행할 것이다.

그 전에 하드웨어 세팅을 시작하자.
구닥다리 라즈베리파이 B+에는 wifi가 기본 내장되어있지 않아서 ssh를 쓰기 위해 wifi 동글을 사용했다. 따라서 네트워크 인터페이스가 wlan0, eth0 두 개가 되는데, wlan0은 ssh를 포함한 인터넷을, eth0은 EtherCAT 포트로 사용한다.

EtherCAT 슬레이브로 사용할 장치는 Beckhoff사의 EL9800 보드로 하였다.
EtherCAT을 통해 제어 가능한 각 8개의 On-board LED와 스위치가 있다.

<연결한 모습>

이제 예제 프로그램을 실행한다.

# cd test/linux/simple_test/
# ./simple_test eth0

실행시키면 다음 그림과 같이 PDO 값에 대해 모니터링해 준다.


O: 뒤에 첫 1바이트가 LED를 의미하고, I: 뒤의 첫 1바이트가 스위치를 의미한다.

그냥 실행만 시키면 재미없으니 LED에 불을 켜 보자!

SOEM/test/linux/simple_test/simple_test.c 파일을 열어서 PDO 교환을 수행하는 다음 코드를 보면

/* cyclic loop */
for(i = 1; i <= 10000; i++)
{
    ec_send_processdata();
    wkc = ec_receive_processdata(EC_TIMEOUTRET);

    if(wkc >= expectedWKC)
    {
        printf("Processdata cycle %4d, WKC %d , O:", i, wkc);

        for(j = 0 ; j < oloop; j++)
        {
            printf(" %2.2x", *(ec_slave[0].outputs + j));
        }

        printf(" I:");
        for(j = 0 ; j < iloop; j++)
        {
            printf(" %2.2x", *(ec_slave[0].inputs + j));
        }
        printf(" T:%"PRId64"\r",ec_DCtime);
        needlf = TRUE;
    }
    osal_usleep(5000);
}

ec_slave[0].outputs + offset이 출력(마스터 -> 슬레이브) PDO, 그러니까 RxPDO를 의미하고 ec_slave[0].inputs + offset이 TxPDO를 의미한다.

LED는 RxPDO의 첫 번째 바이트로 다음 변수를 통해 접근할 수 있다.
ec_slave[0].outputs + 0

정확한 내 장치의 SII에 저장된 PDO 목록을 보고싶다면 같이 빌드되는 예제 프로그램인 slaveinfo를 실행해보면 안다. 다음 명령어를 실행하면

# ./slaveinfo eth0 -map


슬레이브의 SII에 저장된 PDO 매핑 정보가 출력되어 나온다.

다시 LED를 제어하는 것으로 돌아와서!
위 코드의 for 블록문 첫 번째에 다음 코드를 넣었다.
물론 함수의 첫 부분에 int형 ledval(초기값 0x1)과 tick 변수(초기값 0x0)를 선언하였다.

/* cyclic loop */
for(i = 1; i <= 10000; i++)
{
    /* for LED output test */
    if(!(++tick % 50))
    {
        ledval <<= 1;
        if(ledval >= 0x100)
            ledval = 0x1;
        *(ec_slave[0].outputs) = ledval;
        tick = 0;
    }
    /* for LED output test */
    ec_send_processdata();
    wkc = ec_receive_processdata(EC_TIMEOUTRET);

이제 다시 build 디렉토리로 돌아와서 빌드한다.
다시 빌드할 때에는 make만 하면 된다.

# cd ../../../build/
# make

이제 실행시켜보면 된다.