鳥なき里のマイコン屋(124) M5StackとUno間、CANフレームのラウンドトリップ

Joseph Halfmoon

前回はM5StackからArduino Unoへ向かって一方通行のCANフレームの通信を行ってみました。当然、今回は双方向での通信の確認であります。お手軽にM5Stack発でUno経由M5Stack着の「ラウンドトリップ」です。同じArduino IDE使用で同じような仕事ですが、衣の下がちょっと違うのが見えまする。

今回は前回使用のサンプルプログラム(M5Stack用はM5Stack社の、Arduino Uno用はCAN-BUSモジュール販売のLongan Lab社のコード)のチョイ変で実験しています。参考のため当方で勝手改変したコード全文を末尾に掲げてありますが、オリジナルへのURLを併記してあります。

実験の概要

出発点は、M5Stack Grayといたしました。サンプルコード通りAボタンまたはBボタンを押すと、予め決められたCANフレームをCA-IS3050G搭載のCAN-IFから送信します。フレームは以下のフォーマットです

  • 標準(11bit ID)のデータフレーム、8バイトペイロード、128Kbps
  • ID=0x101

とりあえずM5Stackの送信フレームのIDは 0x1zz (zzは0x00から0xff)と勝手に決めました。

当面、拡張フォーマットもリモートフレームも使用しない(CAN-FDなどはそもそもハード的に対応できない)予定です。CANフレームのフォーマットについては例えば以下のページなどで確認できます(車載関係のベンダさんなど多数あり。)

CAN通信におけるデータ送信の仕組みとは?

送信されたCANフレームはCANバスを経由しMCP2515搭載のCAN-IFで受信されます。こちらにはArduino Unoが接続されています。フレームを受け取ったArduino UnoはフレームのIDとペイロードをモニタ用の仮想端末に出力するとともに、「チョイ変」して送り返します。チョイ変の内容は、

  • 受け取ったペイロードの1バイト目のデータを+1する
  • 操作した1バイトのみを送り返す(DLC=1)
  • ID=0x201

とりあえずArduino Unoの送信フレームのIDは 0x2zz (zzは0x00から0xff)と勝手に決めました。CANバスの規定により、CANバス上でフレームが衝突した場合IDの小さいものが勝つので、M5Stackが常に優先されることになります。

実験用プログラムについて

さきほど書いたとおり、どちらの実験用プログラムもサンプルコードの継ぎはぎなのですが、1つ目立ったのが、

M5Stack用の受信コードは FreeRTOS の機能を使っている

ということであります。以下で、FreeRTOSのQueueを生成しています。

 CAN_cfg.rx_queue = xQueueCreate(10,sizeof(CAN_frame_t));

また、受信では上記のQueueからフレームを取り出しています。Queueの「向こう側」には、実際にCANフレームの受信処理を行ってQueueに詰め込んでくれているFreeRTOSのTaskがいる筈です。そのTaskはCANバスIFをイニシャライズするときに生成されているものと考えられます。

M5Stackのコアである ESP32上では、もともとFreeRTOSが走っているので、その気になればArduino環境からでも、いつでもRTOSの機能を使うことができます。別シリーズ「モダンOSのお砂場」でも FreeRTOSのQueue の実習(ターゲットボードはESP32-DevKitCですが)などしてみています。Arduino環境のつもりで書いていても、ちょこっとRTOSを呼び出せるのはなかなか便利じゃないかと思います。個人的にはESP32コアのデバイスを使用する際の「推し」の機能であります。

これに対してArduino Unoでは裏で走っている受信TASKなどというものは無いので、CANフレームの受信を待ち受けるためにループでポーリングし続けています。仕方が無いか。

実行結果

以下に往復旅行(ラウンドトリップ)の旅行先となるArduino Uno側の仮想端末の様子を掲げます。前回同様に、フレーム到着の都度、IDとペイロードバイト列を16進表示しています。前回と異なるのは、受信直後にID=0x201で、復路のフレームをUnoから送信していることです。復路のフレームには往路のペイロードの先頭バイト+1をペイロードとして搭載しています。例えば、受信が0x48(アスキーコードのH)であれば、送信は0x49(アスキーコードのI)、受信が0x4D(アスキーコードのM)であれば、送信は0x4E(アスキーコードのN)を送り返します。

RT_ArduinoUno出発点でありゴールのM5Stack側は以下のようです。Send Message A/Bでフレームを送信したことを知らせ、RCV fromでフレームの受信を知らせます。Aで送信するのは”HelloCAN”なので、折り返し返信されてくるのは “I” という文字の筈です。実際Aの直後Arduino側のID=0x201でペイロード “I” が戻ってきています。Bは”M5Stack “なので、返信は “N” の筈。これもペイロード的にはOK。

RT_M5Stack

しかし問題に気づきました。

Arduino Uno側からはDLC=1で送信している筈なのに8と認識されている

私が何かバグを作りこんでいる可能性が高いですが、Arduino Uno側の問題か、M5Stack側か?例によってAnalog Discovery2のロジアナ機能でCANバスを観察すれば一発で分かるでしょう。が、今日はここまで。また後で。

鳥なき里のマイコン屋(123) M5StackとArduino Uno、CANバス接続 へ戻る

鳥なき里のマイコン屋(125) Nucleo+MCP2562でCANバスにフレーム送信 へ進む

Longan Lab社のexamplesプログラムを継ぎはぎして作成したArduino Uno側のCANフレーム「ラウンドトリップ」用テストコード

元のexamplesの出典はこちら、Longan-Labs/Serial_CAN_Arduino

#include "Serial_CAN_Module.h"
#include <SoftwareSerial.h>

#define CAN_RATE_100    12
#define CAN_RATE_125    13
#define CAN_RATE_200    14
#define CAN_RATE_250    15
#define CAN_RATE_500    16
#define CAN_RATE_666    17
#define CAN_RATE_1000   18
#define can_tx  2
#define can_rx  3

Serial_CAN can;

void setup()
{
  Serial.begin(115200);
  can.begin(can_tx, can_rx, 57600);
  Serial.println("Uno_can_recvXmit");
  if(!can.baudRate(SERIAL_RATE_57600))
  {
      Serial.println("set baud rate fail");
  }
  if(!can.canRate(CAN_RATE_125))
  {
      Serial.println("set can rate fail");
  }
}

unsigned long id = 0;
unsigned char dta[8];

void printHex(unsigned int arg)
{
  Serial.print("0x");
  Serial.print(arg, HEX);
  Serial.print(" ");
}

void loop()
{
    if(can.recv(&id, dta))
    {
        Serial.print("GET DATA FROM ID: ");
        printHex( (unsigned int)id );
        for(int i=0; i<8; i++)
        {
          printHex( (unsigned int)dta[i]);
        }
        Serial.println();
        dta[0] += 1;
        can.send(0x201, 0, 0, 1, dta);
    }
}
M5Stack社のexamplesプログラムを継ぎはぎして作成したM5Stack側のCANフレーム「ラウンドトリップ」用テストコード

元のexamplesの出典はこちら M5-ProductExampleCodes/Unit/CAN

#include <M5Stack.h>
#include "ESP32CAN.h"
#include "CAN_config.h"

#define TX GPIO_NUM_17
#define RX GPIO_NUM_16

CAN_device_t CAN_cfg;

int i = 0;

void header(const char *string, uint16_t color)
{
    M5.Lcd.fillScreen(color);
    M5.Lcd.setTextSize(1);
    M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
    M5.Lcd.fillRect(0, 0, 320, 30, TFT_BLACK);
    M5.Lcd.setTextDatum(TC_DATUM);
    M5.Lcd.drawString(string, 160, 3, 4); 
}

void setup() {
  M5.begin(true, false, true);
  M5.Power.begin();
  Serial.println("CAN Unit Send/Receive");
  header("CAN-Bus Send/receive", BLACK);
  M5.Lcd.setCursor(0, 60, 4);
  
  CAN_cfg.speed = CAN_SPEED_125KBPS;
  CAN_cfg.tx_pin_id = TX;
  CAN_cfg.rx_pin_id = RX;
  CAN_cfg.rx_queue = xQueueCreate(10,sizeof(CAN_frame_t));
  // Init CAN Module
  ESP32Can.CANInit();
  Serial.println("Start");
}

void loop() {
  CAN_frame_t rx_frame;
  CAN_frame_t tx_frame;
  
  if(M5.BtnA.wasPressed()){
      tx_frame.FIR.B.FF = CAN_frame_std;
      tx_frame.MsgID = 0x101;
      tx_frame.FIR.B.DLC = 8;
      tx_frame.data.u8[0] = 'H';
      tx_frame.data.u8[1] = 'e';
      tx_frame.data.u8[2] = 'l';
      tx_frame.data.u8[3] = 'l';
      tx_frame.data.u8[4] = 'o';
      tx_frame.data.u8[5] = 'C';
      tx_frame.data.u8[6] = 'A';
      tx_frame.data.u8[7] = 'N';
          
      ESP32Can.CANWriteFrame(&tx_frame);
      M5.Lcd.println("Send Message A");
      Serial.println("Send Message A");
  }
  if(M5.BtnB.wasPressed()){
      tx_frame.FIR.B.FF = CAN_frame_std;
      tx_frame.MsgID = 0x101;
      tx_frame.FIR.B.DLC = 8;
      tx_frame.data.u8[0] = 'M';
      tx_frame.data.u8[1] = '5';
      tx_frame.data.u8[2] = 'S';
      tx_frame.data.u8[3] = 'T';
      tx_frame.data.u8[4] = 'A';
      tx_frame.data.u8[5] = 'C';
      tx_frame.data.u8[6] = 'K';
      tx_frame.data.u8[7] = ' ';
          
      ESP32Can.CANWriteFrame(&tx_frame);
      M5.Lcd.println("Send Message B");
      Serial.println("Send Message B");
  }
  if(xQueueReceive(CAN_cfg.rx_queue,&rx_frame, 3*portTICK_PERIOD_MS)==pdTRUE){
    printf("RCV from 0x%03x, DLC=%d ",rx_frame.MsgID,  rx_frame.FIR.B.DLC);
    for(int i = 0; i < rx_frame.FIR.B.DLC; i++){
      printf("%c\t", (char)rx_frame.data.u8[i]);
    }
    printf("\n\r");
  }
  M5.update();
  delay(200);
}