見出し画像

Jetson Xavier NXのGPIOを使ったパルス速度を100倍速にしてみた

IoTで用いるエッジコンピューターとしてはRaspberry Piが定番ですが、画像処理を前提とする場合はNVIDIAのJetsonシリーズの利用が多いのではないでしょうか。
今回はJetsonシリーズのなかでも、価格性能比の高いJetson Xavier NXでモーターを回した際に調べたGPIOについて書いていきたいと思います。

モーター

ステッピングモーター

エッジコンピューター側でセンシングした結果として、物理的な動きをさせたい場合があります。そんな時、オープンループ制御で使えるステッピングモーターがとても便利です。

ステッピングモータ自体については、書籍やウェブサイトに丁寧な解説がありますので、そちらをご覧いただくとして、ここでは肝となるパルスについて。

パルス

電気信号のレベルがH, Lと繰り返されるものです。
このH, Lの1サイクルを1パルスと数えます。ステッピングモーターの場合、1パルス入力される毎に1ステップ分の角度だけモーターが回転します。
つまり、このパルスを綺麗にそして自由に制御することが、ステッピングモーターを利用するためには重要なことになります。

マイクロステップ

ステッピングモーターは1パルスでステップ角度だけ回転するのですが、モーターのコイルに流す電流を制御することで、ステップ角度を細分化することができ、そういった制御方法をマイクロステップ制御といいます。
マイクロステップ制御を行えるモータードライバを使うためには、より高い周波数でパルスを生成できるようにしておく必要があります。

GPIOを使ってみる

GPIO

昨今のエッジコンピューター(あるいは、Single Board Computer)の多くは、Raspberry Piの成功を受けてか40ピンのピンヘッダが設けられ、ピンにはSoC(System on Chip)の各種機能が割り当てられています。そして、そこには「General Purpose Input/Output (GPIO)」という汎用入出力ポートが必ずと言っていいほど割り当てられています。

汎用と謳われている通り、設定次第で入力にも出力にも、プルアップ, プルダウン, スリーステートやエッジトリガー, レベルトリガーなど、利用目的にあった設定をすることが可能です。

このGPIOをプログラム側で適切に設定し駆動することで、前述のパルスを発生させることができます。

Jetson.GPIO

Raspberry PiのPythonライブラリで著名なRPi.GPIO互換のPythonライブラリが、NVIDIAによって作成され公開されています。

パッケージでも存在していて、以下のようにしてインストールします。

$ sudo apt install python3-jetson-gpio

どの程度のパルスを出せるか、簡単なコードで確認してみます(対象は12番ピンとしています)。

import RPi.GPIO as GPIO

pin = 12

def main():
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(pin, GPIO.OUT)
    try:
        while True:
            GPIO.output(pin, GPIO.HIGH)
            GPIO.output(pin, GPIO.LOW)
    finally:
        GPIO.cleanup()

if __name__ == '__main__':
    main()

まずは、Power modeを10w 2coreに設定して計測。周波数は約13.5kHzとさみしい結果に。

Python: Power mode 10w 2core

Power modeをCPUの駆動周波数が一番高くなる20w 2coreにしても約16.8kHzと2割程度の改善にとどまりました。

Python: Power mode 20w 2core

C++でデバイスファイルを直接I/O

簡単にパルスを生成させることができたPythonですが、周波数はかなり低めでした。そこで、C++でGPIOのデバイスファイルを直接利用してみます。

自前でやる場合は、12番ピンに対応するsysfsにおける番号を知る必要があります。きちんと調べるべきですが、手を抜いて前述のPythonコードを実行して調べます。

$ strace python3 gpio.py 

上記のように実行し、/sys/class/gpio/export ファイルにどんな値を書き込んでいるかを確認します。

openat(AT_FDCWD, "/sys/class/gpio/export", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 3
fstat(3, {st_mode=S_IFREG|0220, st_size=4096, ...}) = 0
ioctl(3, TCGETS, 0x7fcfb2ee50)          = -1 ENOTTY (Inappropriate ioctl for device)
lseek(3, 0, SEEK_CUR)                   = 0
ioctl(3, TCGETS, 0x7fcfb2ee50)          = -1 ENOTTY (Inappropriate ioctl for device)
lseek(3, 0, SEEK_CUR)                   = 0
lseek(3, 0, SEEK_CUR)                   = 0
write(3, "445", 3)                      = 3

「445」を書き出しているので、12番ピンに対応するデバイスファイルは「/sys/class/gpio/gpio445」配下のファイルとなります。
この情報をもとに、パルスを生成するコードです。

#include <cstdint>
#include <cstring>
#include <fcntl.h>
#include <iostream>
#include <signal.h>
#include <stdexcept>
#include <sys/param.h>
#include <unistd.h>

class GPIOPin {
public:
    GPIOPin(int pin) : pin_(pin), value_fd_(0), direction_fd_(0) {
        try {
            export_pin(pin_);
        } catch (std::exception& e) {
            throw;
        }
        char value_path[MAXPATHLEN];
        char direction_path[MAXPATHLEN];
        snprintf(value_path, sizeof(value_path), "/sys/class/gpio/gpio%d/value", pin_);
        snprintf(direction_path, sizeof(direction_path), "/sys/class/gpio/gpio%d/direction", pin_);
        value_fd_ = open(value_path, O_RDWR);
        direction_fd_ = open(direction_path, O_RDWR);
    }
    ~GPIOPin() {
        try {
            unexport_pin(pin_);
        } catch (std::exception& e) {
            std::cerr << e.what() << std::endl;
        }
    }
    void setup(bool output) {
        if (output) {
            write(direction_fd_, "out\n", 4);
        } else {
            write(direction_fd_, "in\n", 3);
        }
    }
    void high() { write(value_fd_, "1\n", 2); }
    void low() { write(value_fd_, "0\n", 2); }

private:
    void export_pin(int pin) {
        const char *kEXPORT_PATH = "/sys/class/gpio/export";
        int fd = open(kEXPORT_PATH, O_WRONLY);
        char msgbuf[32];
        snprintf(msgbuf, sizeof(msgbuf), "%d\n", pin);
        write(fd, msgbuf, strlen(msgbuf));
        close(fd);
    }
    void unexport_pin(int pin) {
        const char *kUNEXPORT_PATH = "/sys/class/gpio/unexport";
        int fd = open(kUNEXPORT_PATH, O_WRONLY);
        char msgbuf[32];
        snprintf(msgbuf, sizeof(msgbuf), "%d\n", pin);
        write(fd, msgbuf, strlen(msgbuf));
        close(fd);
    }

private:
    int pin_;
    int value_fd_;
    int direction_fd_;
};

volatile int interrupted = 0;

void signal_handler(int signo) {
    interrupted = 1;
}

int main(int argc, char** argv) {
    try {
        signal(SIGINT, signal_handler);
        GPIOPin pin(445);

        pin.setup(true);
        while (!interrupted) {
            pin.high();
            pin.low();
        }
    } catch (std::exception& e) {
        std::cout << e.what() << std::endl;
    }
}

Power modeを10w 2coreと20w 2coreで行ったところ、それぞれ約287kHz, 約326kHzと20倍ぐらい改善しました。

C++: Power mode 10w 2core
C++: Power mode 20w 2core

もっと高速に

GPIOはOS(今回はLinux)が管理している資源で、OSを経由してGPIOを利用する限りはこれ以上の速度改善が見込めません。
ユーザー空間のプログラムでさらなる改善を目指す場合の手法として、GPIO関連の制御レジスタをmmap(2)でユーザー空間にマップし、そのレジスタを直接操作するというものがあります。

Raspberry PiやJetson Nanoは利用ユーザーが多いため、レジスタのアドレスや設定などの情報は簡単にみつかりますが、Jetson Xavier NXはユーザーが少ないようで情報が見つかりませんでした。

ここでは、関連する制御レジスタなどの探し方を書いていきたいと思います。

調査方法

必要な情報の収集

Jetson Download Centerに行き、いくつかの資料を入手します。

  • Xavier Series SoC Technical Reference Manual (Xavier_TRM_DP09253002_v1.4p.pdf)

  • Jetson Xavier NX Developer Kit Carrier Board Specification(Jetson_Xavier_NX_DevKit_Carrier_Board_Specification_v1.0.pdf)

  • Jetson Xavier NX Pinmux Table(Jetson_Xavier_NX_Pinmux_Configuration_Template_v1.06.xlsm)

資料のほかにソースコード等も入手しておきます。

  • Driver Package (Jetson_Linux_R35.2.1_aarch64.tbz2)

  • Driver Package (BSP) Sources (public_sources.tbz2)

12番ピン

今回は12番ピンを使いますので、まずは何が割り当てられているかをJetson_Xavier_NX_DevKit_Carrier_Board_Specification_v1.0.pdfの「3.3 40-Pin Expansion Header」にある図と表から確認します。

  • 「Figure 3-1. Expansion Header Connections」

  • 「Table 3-3. Expansion Header Pin Description – J12」

12番ピンは「I2S0_SCLK」という名前ですが、SoCとしては「DAP5_SCLK」というピンになっている。標準では、複数機能のうちGPIOが割り当てられていて、「PT.05」というGPIOが対応していることがわかります。

複数機能からGPIOを選択するためのレジスタのアドレス、GPIOで出力をできるようにするためのレジスタのアドレス、そして、適切な動作をさせるための設定方法を調べることになります。

レジスタのアドレス

正しい手順はXavier_TRM_DP09253002_v1.4p.pdfの以下の章を読んでなのですが、ここでは手を抜いた方法を記載します。

  • 3.1.2 System Address Map

  • 8.5.3 Programming Guidelines

手順としては以下のようになります。

  1. Jetson_Xavier_NX_Pinmux_Configuration_Template_v1.06.xlsmでDevice Tree用の情報を生成

  2. Driver Packageに含まれているスクリプトで機能割り当て(PinMux)のレジスタのアドレスとGPIOのレジスタのアドレスを生成

Jetson_Xavier_NX_Pinmux_Configuration_Template_v1.06.xlsmのシート「Jetson_Xavier_NX_Module」を開きます。12番ピンの名前「I2S0_SCLK」の行を見ると、Carrier BoardのPDFファイルに記載されていたように、GPIOとして「GPIO3_PT.05」が割り当たるとなっています。

PinMux用情報の生成は、AV列付近にある「Generate DT File」を押下します。ポップアップウインドウが出て「Would you like to generate device tree file for pinmux table?」と聞かれますので「はい」を押下します。
その後「Board Name」が二回聞かれますので、適当な同じ名前を二回入力すると、以下の3ファイルが生成されます。

  • tegra19x-ボード名-gpio-default.dtsi

  • tegra19x-ボード名-padvoltage-default.dtsi

  • tegra19x-ボード名-pinmux.dtsi

次は、Driver PackageのJetson_Linux_R35.2.1_aarch64.tbz2を展開し、「Linux_for_Tegra/kernel/pinmux/t19x」にある変換スクリプトpinmux-dts2cfg.pyを先ほど生成されてたファイルを指定して実行します。スクリプトの使い方は、同じディレクトリー内のREADME.txt に書かれています。

$ bzip2 -cd Jetson_Linux_R35.2.1_aarch64.tbz2 | tar xf -
$ cd Linux_for_Tegra/kernel/pinmux/t19x
$ python pinmux-dts2cfg.py --pinmux addr_info.txt gpio_addr_info.txt por_val.txt --mandatory_pinmux_file mandatory_pinmux.txt tegra19x-XXXX-pinmux.dtsi tegra19x-XXXX-gpio-default.dtsi 1.0 > pinmux.cfg

スクリプトが幾つかのエラーを報告しますが、気にせずリダイレクトしたファイルを開き、「GPIO3_PT.05」に対応する部分を参照します。

pinmux.0x022138a0 = 0x00000001; # CONFIG T5
pinmux.0x02431080 = 0x00000000; # GPIO dap5_sclk_pt5

上記のように出力されていると思います。
コメントとして「CONFIT T5」がついている方がGPIOを制御するためのレジスタのアドレス、「GPIO dap5_sclk_pt5」のコメントがついている方がPinMuxのレジスタのアドレスとなります。

レジスタへの設定値

レジスタのアドレスはわかりましたが、所望の動作をさせるために何を書き込めば良いかです。

PinMuxの方は、説明を省きますがXavier_TRM_DP09253002_v1.4p.pdfの「8.5.4.2 Audio PAD Control Registers」にある「PADCTL_AUDIO_DAP5_SCLK_0」がそれに当たり、出力用のGPIOとして機能させるためには各ビットを以下のようにします。

  • シュミットトリガー: DISABLE

  • GPIOとSFIOの選択: GPIO

  • 入力: DISABLE

  • トライステート: PASSTHROUGH

  • プルアップ,プルダウン指定: NONE

  • PM指定: デフォルトのI2S5

結局、0x00000000(0bxxxx,xxxx,xxxx,xxxx,xxx0,x0x0,x0x0,0000)となります。

GPIOの方は、Driver Package (BSP) Sourcesを展開し、その中のgpio-tegra186.cを参照します。
問題のgpio-tegra186.cは、Linux_for_Tegra/source/public/kernel/kernel-5.10/drivers/gpio内にあります。

$ bzip2 -cd public_sources.tbz2 | tar xf -
$ cd Linux_for_Tegra/source/public/kernel/kernel-5.10/drivers/gpio
$ ls gpio-tegra186.c

関数tegra186_gpio_direction_outputを読むと、出力できるようにするには
アドレス+TEGRA186_GPIO_OUTPUT_CONTROL(0x0C)の
ビットTEGRA186_GPIO_OUTPUT_CONTROL_FLOATED(0x00)を0にして
アドレス+TEGRA186_GPIO_ENABLE_CONFIG(0x00)の
ビットTEGRA186_GPIO_ENABLE_CONFIG_ENABLE(0x0)と
ビットTEGRA186_GPIO_ENABLE_CONFIG_OUT(0x1)の二つを1にすればよいと分かります。

関数tegra186_gpio_setを読むと
アドレス+TEGRA186_GPIO_OUTPUT_VALUE(0x10)の
ビットTEGRA186_GPIO_OUTPUT_VALUE_HIGH(0x0)を1とするとHが
ビットTEGRA186_GPIO_OUTPUT_VALUE_HIGH(0x0)を0とするとLが出力されると分かります。

実行結果

実装コードは長くなるので割愛しますが、レジスタを適宜マップし、レジスタに対して所定の値を書き込むようにして実行しました。

約1.37MHzとなり、Pythonライブラリに対して約100倍まで改善しました。ちなみに、Power mode 10w 2core と20w 2coreで差はなく、ボトルネックがCPUではなくなっています。

レジスタアクセス: Power mode 20w 2core

最後に

実際の使用にあたっては、ソフトウェア的にはエラーハンドリングや後処理が必要です。また、ハードウェア的にはそもそもCarrier Boardから出ているピンは、モータードライバーを駆動するだけの電流を流せませんので、モータードライバーのドライバーなどを用意する必要があります。
しかしながら、Jetson_Xavier_NX_Pinmux_Configuration_Template_v1.06.xlsmでピンへの割り当てとしてGPIOを選択してファイルを生成することで、同様の手順で情報を入手し利用することが可能となります。

説明を端折ったり、提供されているマクロやスクリプトの利用を前提としていろいろと省いたりしてしまいましたが、Jetson Xavier NXのGPIOを制御するための情報となれば幸いです。


この記事が気に入ったらサポートをしてみませんか?