🚥

PiRacer Standard + Qt5でスピードメーターを実装する

2023/09/05に公開

はじめに

ドイツで自動車のソフトウェアエンジニアリングを学んでる奴 "shogura" です。

「Shaping the Future of mobility, together」を掲げ、自動車に関するソフトウェアエンジニアリングのプログラムを提供しているSEA:MEに参加しています。SEA:MEでは「組み込みシステム」「自動運転」「モビリティエコシステム」の3つのモジュールから自分の興味に合った分野を学習することができます。

SEA:MEに参加する前はフランス発のエンジニア養成機関42Tokyoでコンピュータサイエンスを学習していました。

本記事では、組み込みシステムモジュールの一環であるPiRacer Standard + Qt5を使用してスピードメーターを実装するという課題についてどのように実装したのかをお話しします。

完成イメージ

アーキテクチャ


まず、スピードセンサーが赤外線を感知するごとにArduino Nanoにシグナルで通知され、それを元にRPM(回転数)を計算します。その後、CANモジュールであるMCP2515に送られ、CAN-BUSを通してPythonプログラムに到達します。PythonプログラムはD-BUS(IPCの一種)を通してメインアプリケーションであるスピードメーター(ダッシュボード)にデータのやり取りを行います。

このアーキテクチャは、通常の自動車のシステムに比べて簡素化されていますが、今回の目標はスピードメーターを実装することですので、余分な部分を省いています。

ハードウェア

PiRacer Arduino Nano MCP2515 SpeedSensor (LM393) 2CH CAN BUS FD HAT GamePad Wire

開発環境

  • Raspberry Pi 4B
  • Raspbian OS Lite 64bit
  • Qt 5.12
  • QML /version/
  • Qt Creator /version/
  • Arduino IDE 1.8.19
  • Python3 /version/

配線

Sensor Sensor (LM393) -> Arduino Nano

LM393 Arduino
VCC 5V
GND GND
OUT D2
A0 A0

Note: A0はアナログ入力ピンであり、接続は必須ではありません。

Arduino Nano -> MCP2515

Arduino MCP2515
5V VCC
GND GND
D10 CS
D11 SI
D12 SO
D13 SCK
D2 or D3 INT

Note: 割り込みを使用したい場合は、D2またはD3をMCP2515のINTピンに接続してください。

2CH CAN FD HAT セットアップ
2CH CAN FD HATのCANモジュールを有効にするにはRaspberry Piでセットアップする必要があります。今回は以下の記事を参考にしてセットアップをしたので参考にしてください。
https://www.waveshare.com/wiki/2-CH_CAN_FD_HAT

CAN 通信

CAN通信とは

CANについて解説している記事はたくさんあるので、ここでは簡単に紹介させていただきます。
CAN(Controller Area Network)は、マイクロコントローラーとデバイスが自動車内で効率的に通信できるように設計された車両バス規格です。

CANが採用される以前、自動車メーカーはP2P(1対1のハードワイヤ接続)でECUの接続を行っていましたが、自動車の高性能化によって複数のECUが同一のセンサの値を必要とする場面が増加しました。CANを採用することで、ECU間の通信に必要なハードワイヤが少なくなり、大幅なコストダウンが可能になりました。

Controller Area Network (CAN) Overview

サンプルコード

こちらはCAN通信を用いてRaspberry Pi4B + 2CH CAN FD HATとArduino Nano + MCP2515の間でデータのやり取りを行うサンプルコードです。

transmitter.ino

#include <mcp_can.h>
#include <SPI.h>

#define SENSOR_PIN 2

MCP_CAN CAN(10);
uint8_t count = 0;

void Counter() {
 count++;
 Serial.println(count);
}

void setup() {

  Serial.begin(9600);

  CAN.begin(MCP_ANY, CAN_125KBPS, MCP_8MHZ);
  CAN.setMode(MCP_NORMAL);

  pinMode(SENSOR_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(SENSOR_PIN), Counter, RISING);
}

void loop() {

  uint8_t data[8];
  int can_id = 0x125; // optional can id
  int can_dlc = 8; // data length you want to send
  memcpy(data, &count, 8); // copy count to data array

  int status = CAN.sendMsgBuf(can_id, 0, can_dlc, data);

  if (status == CAN_OK)
    Serial.println("Success");
  else 
    Serial.println("Error");
  delay(1000);
}

receiver.py

import can
import time

can_interface = 'can0'

def receive_can_messages():
    bus = can.interface.Bus(channel=can_interface, bustype='socketcan')

    while True:
        message = bus.recv()
        print(f"recieve ID={message.arbitration_id}, data={message.data}")
        time.sleep(1);

if __name__ == "__main__":
    receive_can_messages()

transmitter.inoはArduino上で実行され、Arduinoに接続されているスピードセンサー(LM393)が赤外線を感知する度にCounter()が発火する仕組みになっています。この仕組みはattachInterrupt(digitalPinToInterrupt(SENSOR_PIN), Counter, RISING);で設定されており、RISINGはlow状態からhighの状態になったときに発火するトリガーです。CAN.sendMsgBuf()を使用することで最大8バイトまでのデータをCAN-BUSに送信します。

receiver側はcanモジュールのcan.interface.Bus()でCAN-BUSノードを作成しています。whileループ内でbus.recv()を呼び出すことでTransmitterであるArduinoからデータを取得できます。

attachInterrupt()
MCP_CAN Library
MCP2515 Library
python-can module

スピード計算方法


本来、自動車にスピードセンサーを取り付ける位置はアウトプットシャフトやホイールハブに取り付けられるのが一般的ですが、今回使用するPiRacer Standardには公式でセンサーを取り付ける部分がないので車輪に接する形で自力で設置しました。

Step1 サンプリング・レートの計算

サンプリング定理に従って、スピードセンサーが感知するパルス数を計算します。ナイキスト定理とは、サンプリング周波数が信号周波数の2倍以上である場合、元の信号を復元できるという定理です。今回の場合、スピードセンサーが感知するパルス数が信号周波数に相当します。

T_a - サンプリング・レート
f_{max} - 最大周波数

T_a \leq \frac{1}{2 \cdot f_{max}}

今回、100%出力ではスピードセンサーの最大RPMは1800まで上昇するのを確認しました

sampleRate = \frac{1}{2 \cdot \frac{RPM_{max}}{60}} * 1000

Step2 RPM計算式

RPM_s - スピードセンサーのRPM
RPM_w - 車両ホイールのRPM
PPR - 1回転あたりのパルス数
pulse - 1秒間にスピードセンサーが感知するパルス数
GearRatio - ギア比

RPM_s = \frac{pulse}{PPR} \cdot 60
RPM_w = RPM_s \cdot GearRatio \\ (GearRatio < 1)

さらに、正確なRPMの値を取得したい場合、パルスカウンタを使用できます。

frqRaw (pulse) = \frac{1000000}{ElapsedTimeAvg}
ElapsedTimeAvg = \frac{ElapsedTime}{ReadingCount}

Step3 Speed計算式

Speed - 速度 (m/min)
rpm - RPM
d - 車輪の直径 (mm)
C - 車輪の円周 (m)

C = \frac{d_1 \cdot \pi}{1000}
Speed = RPM_w \cdot C

実際のコード

https://github.com/Shuta-syd/DES02-PiRacer-instrument/blob/dbus-version/can-modules/speedsensor/rpm-calculator.ino

D-BUS

D-BUSとは

D-BUSはプロセス間通信(IPC)の一種であり、Linux上で動作するプロセス間でデータのやり取りを行うことができます。D-BUSはシステムバスとセッションバスの2種類があり、システムバスはシステム全体で使用されるデータのやり取りを行うのに対し、セッションバスはユーザーのログインセッションで使用されるデータのやり取りを行います。D-BUSを採用する前は、各プロセスが直接接続し合う必要があるため、菱形のような複雑な構造になっていました。D-BUSを採用することで、各プロセスはD-BUSに接続するだけでデータのやり取りを行うことができるようになります。

ただし、プロジェクトの規模が小さい場合は、D-Busを使用しなくても問題になりにくいです。プロセス間の通信が少ない場合は、直接接続した方が効率的である場合もあります。

D-Bus Wikipedia

今回のD-BUSアーキテクチャ



DBUSには3つノードが接続されている状態であり、バッテリー情報を供給するBattery Service, Speed, Rpmその他データを供給するDBUS Service、それらの情報を受け取ってダッシュボードに表示するQtという構成になっています。

Dbus Service

https://github.com/Shuta-syd/DES02-PiRacer-instrument/blob/dbus-version/app/piracer_py/dbus/dbus_service.py

71行目のbus.publish("com.test.dbusService", DbusService())によりSessionBusに com.test.dbusServiceオブジェクトを作成してノードとして公開しています。これによりDBUS Serviceは他のノードからデータのやり取りを行うことができるようになります。

python classをDBUS上に公開するには、クラスのイントロスペクションためにXMLを記述する必要があります。XMLの記述内容は以下を参考にしました。
https://pydbus.readthedocs.io/en/latest/legacydocs/tutorial.html
https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format

Battery Service

https://github.com/Shuta-syd/DES02-PiRacer-instrument/blob/dbus-version/app/piracer_py/dbus/battery_service.py

DBUS Serviceと同様 bus.publish("com.dbus.batteryService", BatteryService(vehicle))によりSessionBusに com.dbus.batteryServiceオブジェクトを作成してノードとして公開しています。

Qt(D-BUS Client)

https://github.com/Shuta-syd/DES02-PiRacer-instrument/blob/dbus-version/app/dashboard/dbusclient.cpp#L6-L24

D-BUS Clientであるdbusclient.cppではQTimer::timeoutのシグナルをCLOCK_TIMEの100msごとに発火させ、setData()を呼び出します。setData()ではD-BUSからデータを取得して、それぞれのメンバ変数にセットしています。

this->_iface = new QDBusInterface("com.test.dbusService", "/com/test/dbusService", "com.test.dbusService");ではD-BUSに接続するノードを指定しています。第一引数はD-BUSの名前、第二引数はD-BUSのパス、第三引数はD-BUSのインターフェース名です。QDBusInterfaceのインスタンスが作成されると、D-BUSに接続されます。

Qt Qml

Qt Qmlとは

Qtとは、C++で実装されているいるクロスプラットフォームアプリケーションフレームワークです。「Qt Creator」「Qt Design Studio」などの開発ツールも併せて提供しており、直感的なUIを開発できます。QmlとはJavaScriptをベースとした言語であり、アプリケーションのユーザインタフェースをデザインするためのCSSやJSONのような宣言型言語で、QtアプリケーションのUIに使用されます。

Qt (main.cpp)

QUrl url("qrc:/asset/qml/dashboard.qml");でメインとなるアプリケーションのリソースからQMLファイルを取得します。engine.load(url);でqmlファイルをQMLアプリケーションエンジンにロードします。
https://github.com/Shuta-syd/DES02-PiRacer-instrument/blob/dbus-version/app/dashboard/main.cpp

Qml (dashboard.qml)

dashboard.qmlはダッシュボード全体の構成を管理するqmlファイルです。ここでは、スピードメーター全体構成のデザインを行っています。
https://github.com/Shuta-syd/DES02-PiRacer-instrument/blob/dbus-version/app/dashboard/asset/qml/dashboard.qml

Qml (DashboardGaugeStyle.qml)

DashboardGaugeStyle.qmlはスピードメーターの詳細デザインを管理するqmlファイルです。
https://github.com/Shuta-syd/DES02-PiRacer-instrument/blob/dbus-version/app/dashboard/asset/qml/DashboardGaugeStyle.qml

Qml (ValueSource.qml)

ValueSource.qmlは主に今回のスピードメータに関わるデータを管理するqmlファイルです。Qtのシグナルイベントが発火した時にdbusclient classのgetterからデータを取得しています。
https://github.com/Shuta-syd/DES02-PiRacer-instrument/blob/dbus-version/app/dashboard/asset/qml/ValueSource.qml

プロセスモニター

アーキテクチャで示した通り、今回マルチプロセスを監視するmainプロセスのmonitorスレッド、mainプロセスを監視するmonitor_main.shを実装しました。mainプロセスの監視には他にsupervisorが有力ですが今回は簡素化のためにシェルスクリプトで簡単に実装しました。

monitor_main.sh

https://github.com/Shuta-syd/DES02-PiRacer-instrument/blob/dbus-version/app/piracer_py/dbus/script/monitor_main.sh
https://github.com/Shuta-syd/DES02-PiRacer-instrument/blob/dbus-version/app/piracer_py/dbus/script/restart.sh

monitor.py

https://github.com/Shuta-syd/DES02-PiRacer-instrument/blob/dbus-version/app/piracer_py/dbus/monitor.py

おわりに

本記事では、組み込みシステムモジュールの一環であるPiRacer Standard + Qt5を使用してスピードメーターを実装するという課題についてどのように実装したのかをお話しました。

私が参加しているSEA:MEプログラムは、自動車のソフトウェアエンジニアリングについて学ぶことができるプログラムです。SEA:ME / 42に興味がある方は、ぜひお気軽にメッセージください。
https://seame.space/
https://42tokyo.jp/

Discussion