#はじめに
PCからCAN(Control Area Network)を介して通信していろんなものを制御したいことはあると思います.
CAN通信を選択すると,配線も簡単な二線で済むし,
通信距離も確保しながらまずまずの速度が出るので一部の業界では人気です.
PCに接続するインターフェースの有名な会社もありますが,そもそも見積もり依頼から始めなくてはだめで,
個人で購入できるのかもよくわかりません。
最終目的は,PCに接続するCANインターフェースをESP32を用いて,安価に,簡易に作成することです.
今回はそれへの第一歩となる,PCとインターフェース(ESP32)の間の通信を,python-canを使って行いましたという報告です.
ESP32の日本語の記事はまだ少ないですし,python-canを使った日本語の記事はもっと少ないので,皆さんのお役に立てれば幸いです.
しかしながらここに記載するプログラム等は,各自自己責任でご使用いただくようにお願いいたします.
また,インストール等はすでに整っている前提としています.
#セットアップ
-
PC
- OS: windows 10
- Python:v3.6(python-can 2.1.0, pyserialはセットアップ済み)
- Python開発環境:Spyder(3.2.6)
-
Target Device
- ESP32-DevKitC(秋月電子)
- ESP32用プログラム開発環境:Eclipse(ESP-IDF)
-
PC - TargetDevice
-
USB接続 windows側:"COM3"
-
USB接続 ESP32側:"UART_0" (DevKitC内蔵のSerial-USB変換で中継)
-
Baudrate :115200bps
以上の環境で作成動作確認しました.
#ESP32 プログラム
ESP32に書き込んだ,プログラムを次に示します.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/unistd.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "testSerialCan.h"
#define CAN_QUEUE_SIZE (64)
#define ACK_RESPONSE_AID (0xFFFFFFFF)
#define ACK_QUERY_AID (0xFFFFFFFE)
#define START_OF_FRAME (0xAA)
#define END_OF_FRAME (0xBB)
#define LEN_START_OF_FRAME (1)
#define LEN_TIME_STAMP (4)
#define LEN_DLC (1)
#define LEN_ARBITRATION_ID (4)
#define MAX_LEN_PAYLOAD (8)
#define LEN_END_OF_FRAME (1)
#define MAX_MESSAGE_LEN (LEN_START_OF_FRAME + LEN_TIME_STAMP + LEN_DLC\
+ LEN_ARBITRATION_ID + MAX_LEN_PAYLOAD\
+ LEN_END_OF_FRAME)
#define MIN_MESSAGE_LEN (MAX_MESSAGE_LEN - MAX_LEN_PAYLOAD)
#define UART_NUM UART_NUM_0
#define UART_BUFF_SIZE (256)
#define UART_QUEUE_SIZE (256)
#define UART_RECV_TIMEOUT (10)
typedef struct STRUCT_CAN_MESSAGE{
uint32_t aid;//arbitration_id
uint32_t timeStamp;//atached in message
uint8_t dlc; //length
uint8_t data[8];
struct timeval systemTime;//received or send query time
} CAN_MESSAGE;
static volatile QueueHandle_t canReceiveQueue = NULL;
static volatile QueueHandle_t canSendQueue = NULL;
static volatile QueueHandle_t uartQueue = NULL;
void TaskSerialCanReceive(void *arg){
CAN_MESSAGE message;
CAN_MESSAGE *pMessage = &message;
uint8_t i;
uart_event_t event;
uint8_t recv;
while(1){
if(xQueueReceive(uartQueue, (void * )&event, portMAX_DELAY)) {
if(event.type!=UART_DATA) continue;
if(event.size < MIN_MESSAGE_LEN) continue;
uart_read_bytes(UART_NUM, &recv, 1, UART_RECV_TIMEOUT);
if (recv==START_OF_FRAME){
gettimeofday(&(message.systemTime),NULL);
message.timeStamp = 0;
for(i=0;i<4;i++){
uart_read_bytes(UART_NUM, &recv, 1, UART_RECV_TIMEOUT);
message.timeStamp += recv<<(8*i);
}
uart_read_bytes(UART_NUM, &(message.dlc), 1, UART_RECV_TIMEOUT);
if(message.dlc > MAX_LEN_PAYLOAD) continue; // length error
message.aid = 0;
for(i=0;i<4;i++){
uart_read_bytes(UART_NUM, &recv, 1, UART_RECV_TIMEOUT);
message.aid += recv<<(8*i);
}
for(i=0;i<message.dlc;i++){
if(uart_read_bytes(UART_NUM,message.data+i,
1, UART_RECV_TIMEOUT) <=0) continue;//timeout
}
if(uart_read_bytes(UART_NUM, &recv ,
1, UART_RECV_TIMEOUT)<=0) continue;//timeout
if(recv == END_OF_FRAME){//complete the CAN frame
xQueueSend(canReceiveQueue, (void *)&pMessage,0);
}
}
}
}
}
void TaskSerialCanSend(void *arg){
CAN_MESSAGE message;
CAN_MESSAGE *pMessage;
uint8_t buffer[MAX_MESSAGE_LEN];
buffer[0] = START_OF_FRAME;
while(1){
if(xQueueReceive(canSendQueue,&pMessage,portMAX_DELAY)) {
message = *(pMessage);
for(uint8_t i=0; i<4 ;i++){
buffer[1+i]=0xFF&(message.timeStamp >> (8*i));
buffer[6+i]=0xFF&(message.aid >> (8*i));
}
buffer[5]=message.dlc;
if(buffer[5]>0){
memcpy((void*)(buffer+10),(void*)(message.data),message.dlc);
}
buffer[10+message.dlc] = END_OF_FRAME;
uart_write_bytes(UART_NUM, (char *)buffer,MIN_MESSAGE_LEN+message.dlc);
}
}
}
void app_main(){
CAN_MESSAGE message;
CAN_MESSAGE *pMessage;
uint8_t i;
struct timeval systemTime;
TaskHandle_t th[2];
uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};
uart_param_config(UART_NUM, &uart_config);
uart_set_pin(UART_NUM, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(UART_NUM, UART_BUFF_SIZE*2,UART_BUFF_SIZE*2,
UART_QUEUE_SIZE,(void**) &uartQueue, 0);
canReceiveQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CAN_MESSAGE *));
canSendQueue = xQueueCreate(CAN_QUEUE_SIZE, sizeof(CAN_MESSAGE *));
xTaskCreatePinnedToCore(TaskSerialCanReceive,"SerialCanReceive", 4096
,NULL, 1,&th[0],1);
xTaskCreatePinnedToCore(TaskSerialCanSend,"SerialCanSend", 4096
,NULL, 1,&th[1],1);
while(1){
if(xQueueReceive(canReceiveQueue,(void *)&pMessage,portMAX_DELAY)) {
message = *(pMessage);
if(message.aid == ACK_QUERY_AID){//response ack
message.aid = ACK_RESPONSE_AID;
for(i=0;i<4;i++){
message.data[i]=0xFF&(message.timeStamp>>(8*i));
}
gettimeofday(&systemTime,NULL);
int duration = (systemTime.tv_sec
- message.systemTime.tv_sec)*1000000;
duration += (systemTime.tv_usec
- message.systemTime.tv_usec);
for(i=0;i<4;i++){
message.data[4+i]=0xFF&(duration>>(8*i));
}
message.timeStamp = 0xFFFFFFFF & (systemTime.tv_sec*1000 +
systemTime.tv_usec/1000);
pMessage = &message;
xQueueSend(canSendQueue, (void *) &pMessage, 0);
}else{
//for test only, replay the data
xQueueSend(canSendQueue, (void *) &pMessage, 0);
}
}
}
}
長くて見にくいですね.
大まかにですが以下の通ような動作をするようにしています.
##TaskSerialCanReceive
このタスクではESP32のUARTから, python-canのInterfaceで定義されたプロトコルにのっとたデータを見つけ,
みつけたら,canReceiveQueueに受信したメッセージをなげています.
このプログラムでは,UARTからのキュー(uartQueue)で受信データ長が,プロトコルの最低長になるまで待機しています.
受信途中で,データ長が異常だったりすると,再度待ち受け状態に遷移させるようにしています.
##TaskSerialCanSend
このタスクでは,送信するメッセージがついたキュー(canSendQueue)を待ち受けます.
キューを受け取るとキューに付帯したメッセージをpython-canのInterfaceで定義されたプロトコルに従いデコードし,
ESP32のUARTからPCに向けて送信します.
##app_main
前半部は、UARTの設定・ドライバインストールと,
上記二つのタスクとデータをやり取りするためのキューを作成しタスクを開始しています.
後半部(while(1)内部)では,CANメッセージの受信の通告が,
TaskSerialCanReceiveから送られてくるのを待ち受けています.
メッセージ受信の通告があった場合特殊なarbitration_id(ACK_QUERY_AID:応答要求)の場合には,
データ領域に,受信したメッセージに付加されていたtimeStampと,ESP32内で処理に要した時間をセットして,
特殊なID(ACK_RESPONSE_AID:応答返答)として,メッセージを作成し,
キューをTaskSerialCanSendに投げPC側に送り返しています.
特殊なID以外の場合は,プログラムの確認用として,そのままPCに送り返しています.
#PC側(Python)のプログラム
PC側で実行したpythonプログラムを次に示します.
import can
from can.interfaces.serial import serial_can
import queue
import struct
from time import time,sleep
from datetime import datetime
from random import randint
ARDUINO_SERIAL_PORT = "COM3"
ACK_RESPONSE_AID = 0xFFFFFFFF
ACK_QUERY_AID = 0xFFFFFFFE
MIN_SEND_SPAN=0.004
class ComportCan:
def __init__(self, portName):
self.ack = self.AckDecode()
self.bus = can.interface.Bus(portName,bustype="serial")
self.bus.ser.readall()
self.listener = can.BufferedReader()
self.notifier = can.Notifier(self.bus\
,[self.listener\
,self.ack])
def shutdown(self):
self.notifier.stop()
sleep(0.2)
return self.bus.shutdown()
def __del__(self):
self.shutdown()
def Send(self,aid,data):
now = datetime.now()
secOfDay = (((now.hour*60+now.minute)\
*60+now.second)+now.microsecond/1000000)
msg = can.Message(timestamp=secOfDay\
,arbitration_id = aid\
,dlc = len(data)\
,data = bytearray(data))
self.bus.send(msg)
sleep(MIN_SEND_SPAN)
def GetMessages(self):
return self.listener.get_message(0.01)
def AckQuery(self):
return self.Send(ACK_QUERY_AID,[0 for i in range(8)])
class AckDecode(can.Listener):
delay = []
def on_message_received(self, msg):
if(msg.arbitration_id == ACK_RESPONSE_AID):
now = datetime.now()
secOfDay = (((now.hour*60+now.minute)\
*60+now.second)+now.microsecond/1000000)
recvTimeStamp = serial_can.SerialBus\
.convert_to_integer_milliseconds(secOfDay)
data = bytes(msg.data)
sendTimeStamp = struct.unpack_from("<I",data,0)[0]
responseTime = recvTimeStamp-sendTimeStamp \
if (recvTimeStamp>sendTimeStamp) \
else recvTimeStamp + (24*60*60*1000-sendTimeStamp)
processingTime = struct.unpack_from("<I",data,4)[0]
delay = responseTime - processingTime/1000
self.delay.append(delay)
if __name__ is "__main__":
ComCan = ComportCan(ARDUINO_SERIAL_PORT)
receiveData =[]
for i in range(100):
ComCan.Send(randint(0,0x7FF)\
,[randint(0,0xFF) for i in range(randint(0,8))])
ComCan.AckQuery()
r = ComCan.GetMessages()
while(r != None):
receiveData.append(r)
r = ComCan.GetMessages()
ComCan.shutdown()
##ComportCanクラス
###コンストラクタとAckDecodeクラス
このクラスは,SerialCanを自分なりに使いやすくするために作りました.
コンストラクタは,Windowsで認識されているESP32のポート名(今回はCOM3)を指定し,開始します.
コンストラクタ内部では,python-canのon_message_received機能である,
メッセージが届いたイベントに対し処理するための下処理をおこなっています.
(実は今回の記事は,この機能が確認したくてプログラムを作成したプログラムです)
AckDecodeクラスはcan.Listenerを継承したクラスで,
Listenerのon_message_receivedメソッドをオーバーロードしています.
このインスタンスをcan.Notifierに渡すと,受信イベントで処理をしてくれます.
###AckQueryメソッド
返答要求するメッセージを作成し,ESP32に送信します.
###on_message_receivedメソッド
前述のとおり、このメソッドはメッセージ受信時に,呼び出されます.
冒頭で自分の興味あるarbitration_idかを確認しています.
興味ある特殊なarbitration_idの場合には,以下の処理を実行します.
送信時のタイムスタンプと同様の方法で,現時刻(受信時刻)のタイムスタンプを作成しています.
返答メッセージのデータ領域(0~3バイト目)に格納された,返答要求送信時のタイムスタンプと比較し,
応答要求送信から応答返答受信までの所要時間(responseTimeミリ秒)を計算しています.
また,データ領域(4~7バイト目)に格納された,
ESPの応答要求受信から応答返答送信までの所要時間(processingTimeマイクロ秒)を計算しています.
これら二つの時間を差し引き,単純なメッセージの往復に必要な時間(delay)を計算しています.
後で,この遅延時間がどのくらい必要なを知りたかったので,呼び出されるたびに,
クラス内部のdelayに追加しています.今回は遅延時間の推移が知りたかったので,
このようなプログラムを書きました.
(自明ですが,実働させたときにはこの処理は消します)
##__main__
ComportCanのインスタンスを作成して,その後,100回データの送受信を繰り返しています.
データの送信では,通常arbitration_idの範囲(0x0~0x7FF)の乱数のidで乱数のデータを送信と,
応答要求を行っています.
データの受信では,arbitration_idの種類にかかわらず,receiveDataに格納しています.
#実験結果
ESPに書き込み遅延時間を計算してみました.
上のpythonプログラムのdelayの計算結果です.時々変な計算結果(負の遅延時間)になることがありましたが,
おおむね,数msec程度で通信できました.
負の遅延時間になる理由は,おそらく,pythonにおけるのタイムスタンプ格納時に,
convert_to_integer_millisecondsが呼び出されますが,その時int計算があり,
ミリsec以下を切り捨てているためです.
また,pythonプログラムのComportCan.Sendメソッドでは、謎のsleepで,4msec処理を停止しています.
これは,おおよそ2msec以下の停止時間では,上記のdelayが爆発的に増え,
ESP32が何も応答をしなくなったためです.
sleep時間を2msにした場合と,4msにした場合の遅延時間の比較結果を図に示します.
これの原因は,まだ解明できていませんが,現在のところESP32のUART受信処理の部分において,
処理が追い付かなくなっていると想定しています.
本原因に知見などあればコメントお願いします。
#まとめと今後の課題.
ESP32とPC間で,python-canのserial-canを用いて通信できることが確認できました.
遅延時間を適切に挿入しなくてはならないと,ESP32が動作不能になってしまうことを確認できました.
私の使用用途では,送信データはそれほど多くないため,問題ありませんので,このまま進めることとします.
しかし,ESP32が動作不能になったとき,頭を悩ましました.
それは,ESP32-IDFのmake monitorがつかえないことです.
今回問題となった事象は,pythonからデータを送らないと発生しないし,
pythonからデータを送ろうとすると,ほかのterminalソフトも使用できないし。。。
このままでは,printfデバッグも難しいです.
そこで,今後の課題ですが,ログ情報をWifiを通して送信するようにしたいと考えています.
デバッグするのに,他の危機を増やしたくないなと考えていたら,
ESP32にはWifiのインターフェースがあるじゃないかと,ひらめいたのであります,
Bluetoothも調べてみます.