CAN 通信は自動車や工業系で主に用いられる通信手法です。差動通信であるためノイズ耐性が高く、バス型方式をとっているため配線が容易です。
送信者は送信ノードと呼ばれ、自由なタイミングで送信を行えます。他の送信ノードが送信中の場合、調停を行い送信タイミングを自動的に調整します。受信者は受信ノードと呼ばれ、送信者が持つ識別子を識別し受信を行います。
本ライブラリの CAN 通信クラスは通信バスクラス、送受信ノードクラスから構成され、組み合わせて使用します。
- 通信バスクラス
- 送信ノードクラス
- 受信ノードクラス
- デバッグ
- クラスの組み合わせ色々
- バイト列を直接送受信
個別インクルード
通信バスクラス
送受信処理、通信が行えているかどうかのチェックを行います。使用するマイコンや CAN コントローラーによって適切なバスクラスを選択します。
CAN2.0A プロトコルで通信を行います。ID は 0x000~0x7FF の範囲で使用できます。
CAN2.0A は 一度に 8 バイトのデータしか送信できません。8 バイトより長いデータは分割して送受信します。この時、1 バイト目にインデックス番号が付与されます。8 バイト以下のデータはインデックスを付与せず送受信するため、他の CAN デバイス (市販モーター等) との通信にも使用できます。詳しくは バイト列を直接送受信 を参照ください。
■ Teensy
内臓 CAN コントローラーを使用し CAN 通信を行います。受信ノードが 8 個以内の場合、受信フィルタの設定を行います。
flowchart LR
subgraph 基板
Teensy --CAN TX/RX--> CANトランシーバー
end
CANトランシーバー --CAN H/L--> CANバス
クラスのテンプレートパラメーターに使用する CAN ポートを指定します。
Teensy用 CANバスクラス
Definition CanBusTeensy.hpp:31
詳細な設定
Udon::CanBusTeensy::Config
構造体を用いて詳細な設定が可能です。構造体は次のように定義されています。
struct Config
{
uint32_t transmitInterval = 5;
uint32_t transmitTimeout = 100;
uint32_t receiveTimeout = 100;
uint32_t canBaudrate = 1'000'000;
};
.transmitInterval = 5,
.transmitTimeout = 100,
.receiveTimeout = 100,
.canBaudrate = 1'000'000,
});
回路
Teensy4.0 + MCP2562 の例を示します。
- Teensy4.0 の CAN1 ポートのデフォルト端子と接続しています。
- CAN の信号は
CAN-H
CAN-L
線から出力されます。
- 同じ名前の配線は内部的に接続されています。
- 終端抵抗の有無を切り替えられるようにする必要があります。
- MCP2562 の VIO 端子は、トランシーバー側 IO 端子の基準電源として機能します。マイコン側の IO 端子電圧(0〜3.3V)に一致するように、この端子は 3.3V に接続されています。
■ Raspberry Pi Pico
外部 CAN コントローラー (MCP2515 or MCP25625) を使用し CAN 通信を行います。受信ノードが 6 個以内の場合、受信フィルタの設定を行います。受信フィルタが有効の場合、登録していない ID の受信を関知しないため、不要な処理の削減につながります。
flowchart LR
subgraph 基板
RaspberryPiPico --SPI--> CANコントローラー --CAN TX/RX--> CANトランシーバー
end
CANトランシーバー --CAN H/L--> CANバス
Raspberry Pi Pico用バスクラス
Definition CanBusSpiPico.hpp:24
詳細な設定
ピン設定等は設定値を格納するための構造体 Udon::CanBusSpi::Config
を用いて設定します。構造体は次のように定義されています。
struct Config
{
spi_inst_t* channel = spi_default;
uint8_t cs = PICO_DEFAULT_SPI_CSN_PIN;
uint8_t mosi = PICO_DEFAULT_SPI_TX_PIN;
uint8_t miso = PICO_DEFAULT_SPI_RX_PIN;
uint8_t sck = PICO_DEFAULT_SPI_SCK_PIN;
uint8_t interrupt = 20;
uint32_t spiClock = 1'000'000;
uint32_t transmitInterval = 5;
uint32_t transmitTimeout = 100;
uint32_t receiveTimeout = 100;
CAN_SPEED canBaudrate = CAN_1000KBPS;
CAN_CLOCK mcpClock = MCP_16MHZ;
};
.channel = spi1,
.cs = PICO_DEFAULT_SPI_CSN_PIN;
.mosi = PICO_DEFAULT_SPI_TX_PIN;
.miso = PICO_DEFAULT_SPI_RX_PIN;
.sck = PICO_DEFAULT_SPI_SCK_PIN;
.interrupt = 20;
.spiClock = 1'000'000;
.transmitInterval = 5;
.transmitTimeout = 100;
.receiveTimeout = 100;
.canBaudrate = CAN_1000KBPS;
.mcpClock = MCP_16MHZ;
});
回路
Raspberry Pi Pico + MCP25625 の例を示します。MCP25625 は CAN コントローラー、トランシーバーが統合されたチップで、外付け発振子を必要とします。
- Raspberry Pi Pico の spi0 のデフォルト端子と接続しています。
- CAN の信号は
CAN-H
CAN-L
線から出力されます。
- 同じ名前の配線は内部的に接続されています。
- 終端抵抗の有無を切り替えられるようにする必要があります。
- MCP25625 の VIO 端子は、トランシーバー側 IO 端子の基準電源として機能します。マイコン側の IO 端子電圧(0〜3.3V)に一致するように、この端子は 3.3V に接続されています。
バスの生存確認
各バスクラスに定義されている operator bool
メンバ関数で受信タイムアウト、送信タイムアウトを検出できます。この関数は特定の ID のエラーを検出できるものではなく、バスと通信できているか確認する簡易的なチェックです。
受信タイムアウトの際は受信クラスの getMessage
関数が nullopt を返すのでそちらで判定するのがいいかと思います。
void loop()
{
if (bus)
{
}
else
{
}
}
送信ノードクラス
Udon::CanWriter<T>
T
に指定された型のオブジェクトをバスへ送信します。一つのインスタンスが一つの送信ノードを表します。
void setup()
{
}
void loop()
{
writer.setMessage(vector);
delay(10);
}
void update()
バス更新
Definition CanBusTeensy.hpp:89
void begin()
通信開始
Definition CanBusTeensy.hpp:37
CAN通信 送信クラス
Definition CanWriter.hpp:23
二次元ベクトル
Definition Vector2D.hpp:22
受信ノードクラス
Udon::CanReader<T>
T
に指定された型のオブジェクトをバスから取得します。送信ノードの T
と同じ型である必要があります。一つのインスタンスが一つの受信ノードを表します。
void setup()
{
}
void loop()
{
{
Serial.print(vector->x); Serial.print('\t');
Serial.print(vector->y); Serial.print('\n');
}
else
{
Serial.println("receive failed!!");
}
delay(10);
}
CAN通信 受信クラス
Definition CanReader.hpp:24
オプショナル型
Definition Optional.hpp:62
getMessage
は正常にオブジェクトが受信できたかどうか判定できるように Udon::Optional<T>
を返します。通信エラー時は Udon::nullopt
が返されます。Udon::Optional ドキュメント
デバッグ
全 CAN 通信クラスは show()
メンバ関数を持っており、通信の状態をシリアルモニターへ出力します。
通信バスクラスの show()
はバスに参加している送受信ノードの列挙、送受信データ(バイト列)を出力します。
void show() const
バス情報を表示する
Definition CanBusTeensy.hpp:125
CanBusTeensy
TX 0x010 3 byte (single frame) [ 100 200 300 ]
TX 0x011 3 byte (single frame) [ 100 200 300 ]
TX 0x012 3 byte (single frame) [ 100 200 300 ]
RX 0x020 8 byte (single frame) [ 100 200 178 190 210 230 250 255 ]
RX 0x021 8 byte (single frame) [ 100 200 178 190 210 230 250 255 ]
RX 0x022 9 byte (multi frame) [ 100 200 178 190 210 230 250 255 255 ]
reader.show();
writer.show();
クラスの組み合わせ色々
一つのバスへ複数送受信ノードが参加する(よくある)
二つのバスへ受信ノードが参加する(バスの負荷分散目的)
異なる種類のバスへ参加する(激レア)
バイト列を直接送受信
ロボマスモーター等の CAN 通信を用いる市販のモーターをドライブするには、バイト列で直接やり取りする必要があります。この場合、CRC を付与する CanReader
CanWriter
クラスは使用できません。バイト列を直接やり取りするには送受信ノードを createTx
createRx
関数を用いて作成し、作成したノードに対しデータの書き込み、読み出しを行います。
8 バイトより長いデータは分割して送受信されます。この時、1 バイト目にインデックス番号が付与されます。8 バイト以下のデータはインデックスを付与せず送受信します。
■ 送信ノード
送信ノードの構造
struct CanTxNode
{
const uint32_t id;
std::vector<uint8_t> data;
uint32_t transmitMs;
};
基本的な例
void setup()
{
}
void loop()
{
}
CanTxNode * createTx(uint32_t id, size_t length) override
送信ノードをバスに参加させる
Definition CanBusTeensy.hpp:177
CAN送信ノード
Definition ICanBus.hpp:17
std::vector< uint8_t > data
Definition ICanBus.hpp:20
ラッパークラスを作成する例
class MyCanWriter
{
public:
: txNode(bus.createTx(id, length))
{
}
void update()
{
}
};
static MyCanWriter writer{ bus, 0x000, 10 };
void setup()
{
}
void loop()
{
writer.update();
}
CANバス管理クラス インターフェース
Definition ICanBus.hpp:49
■ 受信ノード
受信ノードの構造
struct CanRxNode
{
const uint32_t id;
std::vector<uint8_t> data;
void (*onReceive)(void*);
void* param;
uint32_t transmitMs;
};
基本的な例
void setup()
{
}
void loop()
{
Serial.print(rxNode->
data[0]); Serial.print(
'\t');
Serial.print(rxNode->
data[1]); Serial.print(
'\n');
}
CanRxNode * createRx(uint32_t id, size_t length) override
受信ノードをバスに参加させる
Definition CanBusTeensy.hpp:192
CAN受信ノード
Definition ICanBus.hpp:28
std::vector< uint8_t > data
Definition ICanBus.hpp:31
コールバックを行う例
受信ノードには、受信時に呼び出すコールバック関数を登録できます。一つのバイト列に対して毎度コールバック関数を呼ぶため、データの取りこぼしが起きません。コールバック関数は割り込みではなく、bus.update()
が呼び出します。
8 バイトより長いバイト列は複数のフレームに分割されて送信されます。この場合、コールバック関数が呼び出されるのは最終フレーム受信時です。
コールバック関数には param
メンバを通じて任意の void ポインタを渡せます。以下の例では CanRxNode ポインタを渡しています。
void onReceive(void* param)
{
Serial.print(node->id); Serial.print('\t');
Serial.print(node->data[0]); Serial.print('\t');
Serial.print(node->data[1]); Serial.print('\t');
Serial.print(node->data[2]); Serial.print('\t');
Serial.print(node->data[3]); Serial.print('\t');
Serial.print(node->data[4]); Serial.print('\t');
Serial.print(node->data[5]); Serial.print('\t');
Serial.print(node->data[6]); Serial.print('\t');
Serial.print(node->data[7]); Serial.print('\n');
}
void setup()
{
}
void loop()
{
}
void * param
Definition ICanBus.hpp:34
void(* onReceive)(void *)
Definition ICanBus.hpp:33
ラッパークラスを作成する例
param
メンバを通じて this ポインタを渡すことで、メンバへアクセスできます。CanReader
クラスもこの機能を用いてメンバ関数のコールバックを行っています。
class MyCanReader
{
int16_t value;
public:
: rxNode(bus.createRx(id, length))
{
}
MyCanReader(MyCanReader&& other)
: rxNode(other.rxNode)
{
}
void update()
{
Serial.println(value);
}
private:
static void onReceive(void* param)
{
auto self = static_cast<MyCanReader*>(param);
self->value = (self->rxNode->data[0] << 8) | self->rxNode->data[1];
}
};
static MyCanReader reader{ bus, 0x000, 10 };
void setup()
{
}
void loop()
{
reader.update();
}