もったかぶった

プログラミングをしたりしなかったり

ZYBOでOPC UAサーバーを動かしてみた

はじめに

OPC UAを扱うためのOSSライブラリにopen62541があります。Linuxボードで動かすだけならFreeOpcUa/python-opcuaあたりも使えますが、個人が組み込みソフトウェアにOPC UAサーバーを載せたいと思ったときの選択肢はほぼopen62541一択です。

open62541の組み込み実装はv1.3までで最新の機能は使えませんが、OPC FOUNDATIONの認証を取得したのはv1.0なので、個人で遊ぶ分には仕様面で困ることはありません。

難点は、open62541を組み込みソフトウェアで動かしている実例がSTM32とESP32くらいしかないことです。導入方法はドキュメントに記述されていますが、ライブラリの実装が古くなっているものや詳細な必要環境は掲載されていません。

そこでこの記事では、組み込みでOPC UAサーバーを立てて遊びたい方が同じところで躓かないように、ZYBOというXilinx社製CPUの評価ボードでopen62541を動かすためにやったことをまとめておきます。

開発環境

  • Xilinx SDK 2019.1
  • Vivado 2019.1
  • Open62541 v1.28
  • ZYBO(Zynq Z-7010 + Realtek RTL8211E-VL PHY)
  • Ubunrtu20.04 (WSL2)

open62541の導入方法

自分はV1.28のソースコードを落としてきてWSL環境でビルド手順を通しました。ソースコードをクローンしたディレクトリで下記コマンドを順に実行すればopen62541.cとopen62541.hというファイルが生成されます。makeの途中でエラーが発生するのは無視してください。

mkdir build_freeRTOS
cd build_freeRTOS
cmake -DUA_ARCHITECTURE=freertosLWIP -DUA_ENABLE_AMALGAMATION=ON ../
make

生成されたopen62541.cとopen62541.hを自分のプロジェクトにインポートすれば使えるようになります。

Xilinx SDKでプロジェクトを作成する方法

Xilinx SDK 2019.1はサンプルプロジェクトでFreeRTOS+lwipの入ったプロジェクトを生成してくれます。open62541を組み込み環境で使うにはFreeRTOS+lwipが必要なのでサンプルプロジェクトを改造して作成しましょう。

ちなみに記事中でHardware Platformに指定しているsimplepsははwatakeさんの記事(ZYBO (Zynq) 初心者ガイド (1) 開発環境の準備 - Qiita)を参考に作成したものです。

Xilinx SDKの新規プロジェクト作成から、OSにfreertos10_xilinxを選びnextをクリックします。

Xilinx SDKプロジェクト作成画面

テンプレート選択でlwipのechoサーバーを選択します。

テンプレートの選択

これだけでZYBOがtelnetで接続してきた端末の送信データをそのままオウム返しするEchoサーバが立ち上がりません。

lwipのPHYドライバを騙す

サンプルプロジェクトのelfファイルをZYBOに書き込んでteratermなどで接続すると、下記のようにPhyセットアップにコケているログが表示されます。

Phyセットアップにコケる様子

残念ながらXilinx SDKが自動生成してくれるlwipはMarvell PHYしかサポートしていないようです。そのため、Realtek PHYを積んでいるZYBOはAuto Negotiationに失敗していつまでもネットに接続できません。

https://support.xilinx.com/s/article/63495?language=en_US

といってもコケているのはAuto Negotiationの最後、通信速度を決める段階だけです。本当はdatasheetを見て修正すべきですが、とっととOPC UAサーバーを立てたかったので無理やりxemacpsif_physpeed.cを下記のコードに書き換えて動作させました。

static u32_t get_Realtek_phy_speed(XEmacPs *xemacpsp, u32_t phy_addr)
{
...
    return 1000; /* ここより下でコけるので、固定通信速度で動かす */

    XEmacPs_PhyRead(xemacpsp, phy_addr,IEEE_SPECIFIC_STATUS_REG,
                    &status_speed);
    if (status_speed & 0x400) {
        temp_speed = status_speed & IEEE_SPEED_MASK;

        if (temp_speed == IEEE_SPEED_1000)
            return 1000;
        else if(temp_speed == IEEE_SPEED_100)
            return 100;
        else
            return 10;
    }

    return XST_FAILURE;
}

今度はPhyのセットアップが完了した

これで端末に指定されるIPアドレスtelnet接続すると、Echoサーバーとして動作している様子が確認できます。

open62541を動かすための設定。

前述の工程で作ったopen62541をインポートしてください。ビルドが通らなくなるので通すための手順を載せます。

まずlwipopts.hに下記defineを追記してください。

#define LWIP_COMPAT_SOCKETS 0 // Don't do name define-transformation in networking function names.
#define LWIP_SOCKET 1 // Enable Socket API (normally already set)
#define LWIP_DNS 1 // enable the lwip_getaddrinfo function, struct addrinfo and more.
#define SO_REUSE 1 // Allows to set the socket as reusable
#define LWIP_TIMEVAL_PRIVATE 0 // This is optional. Set this flag if you get a compilation error about redefinition of struct timeval

次にFreeRTOSConfig.hのdefineを下記のように修正します。

#define configCHECK_FOR_STACK_OVERFLOW 1
#define configUSE_MALLOC_FAILED_HOOK 1
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 131070) )

次にプロジェクトに下記シンボルを追加してください。

UA_ARCHITECTURE_FREERTOSLWIP
OPEN62541_FEERTOS_USE_OWN_MEM

最後にheap_4.cに下記コードを追加してください。

void *pvPortCalloc(size_t count, size_t size)
{
  void *p;

  /* allocate 'count' objects of size 'size' */
  p = pvPortMalloc(count * size);
  if (p) {
    /* zero the memory */
    memset(p, 0, count * size);
  }
  return p;
}

void *pvPortRealloc(void *mem, size_t newsize)
{
    if (newsize == 0) {
        vPortFree(mem);
        return NULL;
    }

    void *p;
    p = pvPortMalloc(newsize);
    if (p) {
        /* zero the memory */
        if (mem != NULL) {
            memcpy(p, mem, newsize);
            vPortFree(mem);
        }
    }
    return p;
}

これでビルドが通るようになります。

OPC UAサーバータスクを書いて立ち上げる。

ドキュメントと一か所だけ異なり、UA_ServerConfig_setMinimal関数の代わりにUA_ServerConfig_setMinimalCustomBuffer関数を使っています。恐らくドキュメントが書かれた頃と定義が変わったものと思われます。

static void opcua_thread(void *arg){

        //The default 64KB of memory for sending and receicing buffer caused problems to many users. With the code below, they are reduced to ~16KB
        UA_UInt32 sendBufferSize = 16000;       //64 KB was too much for my platform
        UA_UInt32 recvBufferSize = 16000;       //64 KB was too much for my platform
        UA_UInt16 portNumber = 4840;

        UA_Server* mUaServer = UA_Server_new();
        UA_ServerConfig *uaServerConfig = UA_Server_getConfig(mUaServer);
        UA_ServerConfig_setMinimalCustomBuffer(uaServerConfig, portNumber, 0, sendBufferSize, recvBufferSize);

        //VERY IMPORTANT: Set the hostname with your IP before starting the server
        UA_ServerConfig_setCustomHostname(uaServerConfig, UA_STRING("192.168.0.102"));

        //The rest is the same as the example

        UA_Boolean running = true;

        // add a variable node to the adresspace
        UA_VariableAttributes attr = UA_VariableAttributes_default;
        UA_Int32 myInteger = 42;
        UA_Variant_setScalarCopy(&attr.value, &myInteger, &UA_TYPES[UA_TYPES_INT32]);
        attr.description = UA_LOCALIZEDTEXT_ALLOC("en-US","the answer");
        attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US","the answer");
        UA_NodeId myIntegerNodeId = UA_NODEID_STRING_ALLOC(1, "the.answer");
        UA_QualifiedName myIntegerName = UA_QUALIFIEDNAME_ALLOC(1, "the answer");
        UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
        UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);
        UA_Server_addVariableNode(mUaServer, myIntegerNodeId, parentNodeId,
                                                                parentReferenceNodeId, myIntegerName,
                                                                UA_NODEID_NULL, attr, NULL, NULL);

        /* allocations on the heap need to be freed */
        UA_VariableAttributes_clear(&attr);
        UA_NodeId_clear(&myIntegerNodeId);
        UA_QualifiedName_clear(&myIntegerName);

        UA_StatusCode retval = UA_Server_run(mUaServer, &running);
        UA_Server_delete(mUaServer);
}

main_thread関数の末尾に下記を追記する。

sys_thread_new("opcua_thread", opcua_thread, NULL, 8000, 8);

ビルド&OPC UAサーバーの動作確認!

以上でZYBOからOPC UAサーバーを動かすためにやったことは終わりです。試しに適当なクライアントから自分の環境のエンドポイント"opc.tcp://192.168.3.36:4840"にアクセスすると無事アドレススペースを公開してくれていることが確認できます。