PiRacer Standard + Qt5でスピードメーターを実装する
はじめに
ドイツで自動車のソフトウェアエンジニアリングを学んでる奴 "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でセットアップする必要があります。今回は以下の記事を参考にしてセットアップをしたので参考にしてください。
CAN 通信
CAN通信とは
CANについて解説している記事はたくさんあるので、ここでは簡単に紹介させていただきます。
CAN(Controller Area Network)は、マイクロコントローラーとデバイスが自動車内で効率的に通信できるように設計された車両バス規格です。
CANが採用される以前、自動車メーカーはP2P(1対1のハードワイヤ接続)でECUの接続を行っていましたが、自動車の高性能化によって複数のECUが同一のセンサの値を必要とする場面が増加しました。CANを採用することで、ECU間の通信に必要なハードワイヤが少なくなり、大幅なコストダウンが可能になりました。
サンプルコード
こちらは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倍以上である場合、元の信号を復元できるという定理です。今回の場合、スピードセンサーが感知するパルス数が信号周波数に相当します。
今回、100%出力ではスピードセンサーの最大RPMは1800まで上昇するのを確認しました
Step2 RPM計算式
さらに、正確なRPMの値を取得したい場合、パルスカウンタを使用できます。
Step3 Speed計算式
実際のコード
D-BUS
D-BUSとは
D-BUSはプロセス間通信(IPC)の一種であり、Linux上で動作するプロセス間でデータのやり取りを行うことができます。D-BUSはシステムバスとセッションバスの2種類があり、システムバスはシステム全体で使用されるデータのやり取りを行うのに対し、セッションバスはユーザーのログインセッションで使用されるデータのやり取りを行います。D-BUSを採用する前は、各プロセスが直接接続し合う必要があるため、菱形のような複雑な構造になっていました。D-BUSを採用することで、各プロセスはD-BUSに接続するだけでデータのやり取りを行うことができるようになります。
ただし、プロジェクトの規模が小さい場合は、D-Busを使用しなくても問題になりにくいです。プロセス間の通信が少ない場合は、直接接続した方が効率的である場合もあります。
今回のD-BUSアーキテクチャ
DBUSには3つノードが接続されている状態であり、バッテリー情報を供給するBattery Service, Speed, Rpmその他データを供給するDBUS Service、それらの情報を受け取ってダッシュボードに表示するQtという構成になっています。
Dbus Service
71行目のbus.publish("com.test.dbusService", DbusService())
によりSessionBusに com.test.dbusService
オブジェクトを作成してノードとして公開しています。これによりDBUS Serviceは他のノードからデータのやり取りを行うことができるようになります。
python classをDBUS上に公開するには、クラスのイントロスペクションためにXMLを記述する必要があります。XMLの記述内容は以下を参考にしました。
Battery Service
DBUS Serviceと同様 bus.publish("com.dbus.batteryService", BatteryService(vehicle))
によりSessionBusに com.dbus.batteryService
オブジェクトを作成してノードとして公開しています。
Qt(D-BUS Client)
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アプリケーションエンジンにロードします。
Qml (dashboard.qml)
dashboard.qmlはダッシュボード全体の構成を管理するqmlファイルです。ここでは、スピードメーター全体構成のデザインを行っています。
Qml (DashboardGaugeStyle.qml)
DashboardGaugeStyle.qmlはスピードメーターの詳細デザインを管理するqmlファイルです。
Qml (ValueSource.qml)
ValueSource.qmlは主に今回のスピードメータに関わるデータを管理するqmlファイルです。Qtのシグナルイベントが発火した時にdbusclient classのgetterからデータを取得しています。
プロセスモニター
アーキテクチャで示した通り、今回マルチプロセスを監視するmainプロセスのmonitorスレッド、mainプロセスを監視するmonitor_main.shを実装しました。mainプロセスの監視には他にsupervisor
が有力ですが今回は簡素化のためにシェルスクリプトで簡単に実装しました。
monitor_main.sh
monitor.py
おわりに
本記事では、組み込みシステムモジュールの一環であるPiRacer Standard + Qt5を使用してスピードメーターを実装するという課題についてどのように実装したのかをお話しました。
私が参加しているSEA:MEプログラムは、自動車のソフトウェアエンジニアリングについて学ぶことができるプログラムです。SEA:ME / 42に興味がある方は、ぜひお気軽にメッセージください。
Discussion