ESP32は、CANコントローラを内蔵していて、外付けのCANトランシーバを利用すれば、CAN通信ができるようになります。
CAN?なにそれ?という方は、下の記事を参考にしてください。要するに自動車関係でよく使用されてるシリアル通信の規格です。
CANプロトコルを簡単に説明する | ぼくのマイコン開発のメモ (tekuteku-embedded.xyz)
ちなみにCANトランシーバは、シングルエンドの信号を差動信号として出力するICです。
FRDM-K64FにCANトランシーバをつける | ぼくのマイコン開発のメモ (tekuteku-embedded.xyz)
ESPのTWAIとはCANのこと
最初、ESPのリファレンスマニュアルでCANと検索しても引っかからないので、おかしいと思っていましたが、ESPでは、CAN通信のことをTWAI(two wire automoive interface)とうたっていました。認証絡みで、CANと明記できない理由があるんでしょうか。
とにかく、TWAI = CANのようです。
Arduino IDEで使えるライブラリ
Arduino用のCANライブラリは何種類かあるようですが、今回は上のものを使わせてもらいました。ライブラリの構成は下のとおりでした。
ちなみに、ArduinoのライブラリはDocumentフォルダの直下に作成されます。自分の環境では、ライブラリフォルダを下記に作成されてます。
C:\Users\<ユーザ名>\Documents\Arduino\libraries
基本的な使い方
ライブラリのexampleフォルダの中にサンプルコードがあります。
CANReceiver — 任意のCAN IDのパケットを受信し、1バイトずつモニタに表示する
CANReceiverCallback — CAN受信したら、割り込み処理をかけるサンプル(一方で、上のサンプルはポーリングで値を取得している)
CANSender —指定したIDのCANメッセージを送信する
どのサンプルもシンプルで簡単に理解できます。
初期化
サンプルの説明をします。
CAN.begin(500E3)
これが初期化。引数にボーレートを渡します。この例は500Mbps。1Mbpsとしたいときは、
CAN.begin(1000E3)
とかします。ちなみにEPS32に実装されているRTOSの都合上、50kbps以下には設定できません。戻り値がTrueなら初期化成功です。
実際に中では、
- TX, RXピンの割り当て
- ビットタイミングの設定
- CAN IDのフィルタ設定(指定した範囲のIDしか受信しない仕組みがある)
- 割り込み許可
- CANエラーフラグ、割り込みフラグのクリア
みたいなことをやっています。
受信(ポーリング)
CAN.parsePacket()
このメソッドでCAN受信用のメッセージバッファを確認して、受信データがなければ、0をかえし、受信があれば、そのデータサイズを返します。要するに受信メッセージのDLCが戻り値になります。内部的にはここで、受信データを配列に格納してます。
これをLoop関数内で呼べば、周期的にCAN信号を受信することができます。
CAN.packetExtended()
受信データが拡張フォーマットであれば、trueが戻り、標準フォーマットであれば、falseが戻ります。
CAN.packetRtr()
リモートフレームかデータフレームを確認するメソッドです。受信がリモートフレームであれば、trueが戻ります。
CAN.packetId()
受信データのCAN IDを戻します。
CAN.packetDlc()
受信データのDLCを戻します。
while (CAN.available()) {
Serial.print((char)CAN.read());
}
read関数で、parsePacketで取得したデータ配列を1バイトずつゲットします。
受信(割り込み)
割り込み処理は以下のようにします。
CAN.onReceive(onReceive)
これで、onReceiveがコールバック関数として定義されます。
void onReceive(int packetSize) {
// 割り込み時の処理をここに書く
}
送信
メッセージの送信は簡単です。beginPaketで送信したCAN IDを設定して、writeメソッドで1バイトずつデータを定義した後で、endPacketを呼び、送信を実行します。
CAN.beginPacket(0x12);
CAN.write('h');
CAN.write('e');
CAN.write('l');
CAN.write('l');
CAN.write('o');
CAN.endPacket();
複数バイトを一気に定義したい場合は、
uint8_t temp[4];
*(uint32_t *)temp = count_32;
CAN.write(temp,sizeof(temp));
CAN.endPacket();
とかします。
以上が、サンプルコードで使われているメソッドの説明ですが、それ以外にも必要と思われるメソッドが色々あったので、使い方を追記しときます。
CANのhigh/lowのピン設定
ライブラリはデフォルトで#4と#5ピンを使ってます(ESP32SJA100.h )
#define DEFAULT_CAN_RX_PIN GPIO_NUM_4
#define DEFAULT_CAN_TX_PIN GPIO_NUM_5
変更したい場合は、下のように直接、ESP32SJA100.hを編集するか。
#define DEFAULT_CAN_RX_PIN GPIO_NUM_26
#define DEFAULT_CAN_TX_PIN GPIO_NUM_25
それか、setPins関数を使います。
CAN.setPins(4,5)
CANのマスク設定
CANバス上にパケットが少ないなら、別に問題にならないですが、パケットがたくさんある場合、ESP32で使用しない(興味ない)パケットは無視したほうが、CPU負荷を温存できます。特に割り込みなどかけている場合は、すべての受信タイミングで割り込みがはいるので、プログラムが正常に動かなくなる場合もあります。
ESP32のCANコントローラではAcceptance Filterという機能があり、指定の範囲ID以外は、興味ないから受け付けないというような設定ができます。
上のようにACRとAMRというレジスタを設定することで、フィルター機能を有効化させます。
たとえば0x100と0x101だけ受信したい
0x100と0x101を2進数で表すと下の黄色の部分になります。LSBだけ0と1で値が違っています。
0x100と0x101のXNORは青色の部分です。各bitで値が一致していれば、1となります。なので、LSBのみ0です。
XNORを反転したものがMASKになります。これがAMRレジスタに設定する値です。
ACRには、0x100か0x101どちらでもいいので、設定します。ここでは0x100を設定したと想定しましょう。
すると上のブロック図の受信IDのところに0x101が来た場合
XNOR = 0b 111 1111 1110
下流側のORブロックは上の結果とMASKのORなので、
ORの結果 = 0b 111 1111 1111
結果、すべての値が1になり、受信してもいいってことになります。
受信IDのところに0x105(0b 001 0000 0101)がきたらどうでしょうか。
XNOR = 0b 111 1111 1010
ORの結果 = 0b 111 1111 1011
この結果は、3ビット目が0となり、受信拒否されます。
ライブラリにはfilterという関数があるので、これが上のようなフィルター設定をやってくれます。下がサンプルです。
uint16_t RX_ID = 0x100;
uint16_t RX_ID_2 = 0x101;
uint16_t RX_XNOR = RX_ID ^ RX_ID_2;
while(!CAN.filter(RX_ID,~RX_XNOR))
{
}
第一引数にCODE、第二引数にMASKをいれればオッケーです。
ビットタイミングの設定
CANバスにCANコントローラの動作周期が違うデバイスをぶら下げていたり、伝送経路が長くて、デバイス間で信号の遅延が発生したりすると、CANがうけとれないみたいな自体が発生します。この時は、ビットタイミングを調整します。
ライブラリのメソッドには、設定用のAPIが登録されていないので、ライブラリを直接編集します。
ESP32SJA1000.cpp内の69行目ぐらいから
//上省略
modifyRegister(REG_CDR, 0x80, 0x80); // pelican mode
modifyRegister(REG_BTR0, 0xc0, 0x0); // SJW = 1
modifyRegister(REG_BTR1, 0x70, 0x10); // TSEG2 = 2
switch (baudRate) {
case (long)1000E3:
modifyRegister(REG_BTR1, 0x0f, 0x04); //TSEG1 = 5
modifyRegister(REG_BTR0, 0x3f, 4); // 動作周波数 8MHz
break;
//下省略
設定方法はリファレンスマニュアルに載っています。マニアックな話なので、一応、この辺でいじれるんだぐらいにしときます。
テスト結果
0x101他のCANデバイスとつなげながら0x100を5ms周期で送信するテストを行いました。
下はCANbusに流れている全IDです。赤枠のパケットがESPが送信しているIDです。
この状態からお邪魔要素として、0x001~0x005までIDを0.5~1msの高周波で別のCANコントローラから送信します。
下が、アナライザの結果です。
バス負荷は95%までいっちゃっていますが、エラーフレームは0で特に問題はありません。
ESPからのパケットも確実に5msで送信されることが確認でき、途中で途絶えるみたいなこともありませんでした。
意外に使えるかもね。。。
TWAI BANZAI🙌
私もESP32からPCANにCANを送信してみましたが、波形は出るもののPCANで受信しないです。ESP32SJA1000.cppはどう修正しましたか?
自分が通信関連の設定で直接編集した箇所は、
modifyRegister(REG_BTR0, 0xc0, 0x0); // SJW
modifyRegister(REG_BTR1, 0x70, 0x10); // TSEG2
modifyRegister(REG_BTR1, 0x0f, 0x04);//TSEG
modifyRegister(REG_BTR0, 0x3f, 4);
ら辺でしょうか。
modifyRegister関数で下のレジスタ値を編集しました。
TWAI_BUS_TIMING_0_REG(0x0018)
TWAI_BUS_TIMING_1_REG(0x001C)
詳細は、マニュアルを確認していただきたいですが、
ここのレジスタでビットタイミングなどの通信設定をおこないます。