CANopen 前編 ―― デバイスが自分の辞書を持つ(2/5)
- canopen
- can
- python
- 産業用通信
- 組み込み
前回の Modbus では、読めたのは [1234, 0, 0, 0] という数字の列だけでした。それが何を意味するかは、ベンダのPDFの中にしかありませんでした。
今回の CANopen は、そこに手を入れます。デバイスが自分自身の中に「どの番号に何が入っているか」の辞書を持ちます。
そして今回もハードウェアなしで進めます。仮想のCANバスをPythonの中に作って、その上でノード同士を会話させます。
この記事で作るもの
PC の中に仮想の CAN バスを1本立て、そこに「マスタ」と「サーボ」の2ノードを置いて会話させます。
$ ./.venv/bin/python 02_canopen/canmon.py
0.002 0x703 HEARTBEAT node=3 [1] 00
時刻(s) COB-ID 種別 長さ データ
------------------------------------------------------------------
0.053 0x000 NMT (broadcast) [2] 01 03
0.104 0x603 SDO(rx) node=3 [8] 23 7a 60 00 10 27 00 00
0.154 0x080 SYNC (broadcast) [0] (empty)
0.154 0x183 TPDO1 node=3 [6] fa 00 00 00 37 02
0.205 0x080 SYNC (broadcast) [0] (empty)
0.205 0x183 TPDO1 node=3 [6] f4 01 00 00 37 02
0.255 0x080 SYNC (broadcast) [0] (empty)
0.255 0x183 TPDO1 node=3 [6] ee 02 00 00 37 02
------------------------------------------------------------------
観測フレーム数: 9
1行目だけ見出しより上に出ているのは、ノードがバスに出た直後に送るフレームで、モニタ側が見出しを描くより先に届くからです。この16進の羅列を、1バイトずつ最後まで読んでいきます。
使うもの(すべてオープンソース・費用ゼロ・ハード不要)
| ライセンス | ||
|---|---|---|
| python-can | 4.6.1 | LGPL-3.0-only |
./.venv/bin/pip install "python-can[multicast]==4.6.1" # Windows: .venv\Scripts\pip
コードは 02_canopen/smoke_test.py(最小構成)と 02_canopen/canmon.py(バスモニタ。candump 相当を自作したもの)の2本です。
./.venv/bin/python 02_canopen/smoke_test.py # 最小構成。SYNC と TPDO が成立することを確かめる
./.venv/bin/python 02_canopen/canmon.py # バスモニタ。冒頭の出力はこれ
どちらも1本で完結します。canmon.py は自分の中でノードとマスタを立ててから監視するので、別のターミナルでトラフィックを流す必要はありません。全OS(Windows含む)で動き、CAN のハードウェアも Linux の vcan カーネルモジュールも要りません。
この回で出てくる用語(8個)
先に並べておきます。以降はこの8語で話が進みます。
| 用語 | ひとことで |
|---|---|
| CAN | 物理層とデータリンク層。CANopen とは別物 |
| COB-ID | フレームの11bitの識別子。優先度も兼ねる |
| OD(オブジェクト辞書) | デバイスが持つ、番号から中身を引くための対応表 |
| EDS | OD をテキストファイルにしたもの |
| SDO | OD の任意の場所を読み書きする。確認応答つき・遅い |
| PDO | 事前に決めた値だけを流す。確認応答なし・速い |
| NMT | ネットワーク全体の状態管理(起動・停止) |
| SYNC | 全ノードに「今」を配るトリガ |
① CAN と CANopen は違うもの
ここを分けておくと、以降が読みやすくなります。
| 規格 | 何を決めているか | |
|---|---|---|
| CAN | ISO 11898-1 | フレームの形、衝突の解決 |
| CANopen | CiA 301 | そのフレームを使って何を喋るか |
CAN は自動車のために作られました。CANopen はその上に産業機器向けの共通語を載せたもので、同じ CAN の上には J1939(大型車両)という全く別の共通語もあります。なお ISO 11898-1 が定めるのはデータリンク層とフレーム形式で、電気的な特性は 11898-2 など別のパートです。
CAN の面白いところ: ぶつからない
共有メディア時代の Ethernet は、2台が同時に喋ると衝突して両方壊れました(CSMA/CD)。壊れたことを検出し、ランダムに待ってから再送します。
いま普通に使われているスイッチ全二重の Ethernet では、そもそも衝突が起きません。ただしそれは、スイッチという外部の装置が交通整理をしてくれているからです。
CAN はそのどちらでもありません。1本の線に全員がぶら下がったまま、衝突しても壊れません。
CAN の線には「劣勢(1)」と「優勢(0)」があり、優勢が劣勢を上書きします。送信中のノードは、自分が出したビットと線の上の実際のビットを常に見比べていて、「1 を出したのに 0 が読めた」瞬間に自分から降ります。
結果、識別子(COB-ID)の数字が小さいフレームが必ず勝ち、しかも勝ったフレームは1ビットも壊れずにそのまま流れ続けます。

3ノードが同時に送信を始めても、優勢ビット(0)が劣勢ビット(1)を上書きし、負けたノードが降ります。勝ったフレームは1ビットも壊れずに残ります。
これが「CANは遅いが、大事なメッセージは必ず時間内に届く」と言われる理由です。
ただし必ず届くのは最優先のメッセージだけです。優先度の低いものは理屈の上でいくらでも待たされるので、実務では「最悪でも何ms以内か」を計算で証明します。
② 仕組み
COB-ID — 識別子が優先度を兼ねる
CAN の識別子には11ビットの標準フォーマットと29ビットの拡張フォーマットがあります。CANopen が基本に使うのは11ビットのほうで、これを2つに割ります。
10 9 8 7 | 6 5 4 3 2 1 0
└ Function ─┘ └──── Node-ID ─────┘
Code 4bit 7bit (1〜127)

11bit の COB-ID は Function Code 4bit と Node-ID 7bit に分かれます。値が小さいほど優先度が高く、優先度は NMT、SYNC、PDO、SDO、ハートビートの順に低くなります。
主なものだけ:
| オブジェクト | COB-ID |
|---|---|
| NMT(起動・停止命令) | 0x000 |
| SYNC | 0x080 |
| TPDO1(ノードが送る) | 0x180 + Node-ID |
| RPDO1(ノードが受け取る) | 0x200 + Node-ID |
| SDO 応答 / 要求 | 0x580 + Node-ID / 0x600 + Node-ID |
| ハートビート | 0x700 + Node-ID |
数字が小さいほど優先なので、優先順位は自動的にこうなります。
NMT > SYNC > EMCY > TPDO1 > RPDO1 > … > SDO > ハートビート
ネットワーク全体を止められる NMT が最優先で、制御データの PDO は設定用の SDO より優先されます。設計の意図が番号の並びにそのまま出ています。
OD(オブジェクト辞書)— 今回の主役
CANopen のデバイスは、内部に表を持っています。
| インデックス | 中身 | 型 |
|---|---|---|
| 0x1000 | Device type | UNSIGNED32 |
| 0x6040 / 0x6041 | Controlword / Statusword | UNSIGNED16 |
| 0x6064 | Position actual value | INTEGER32 |
| 0x607A | Target position | INTEGER32 |
Modbus との違いは、番地に型と名前とアクセス権が付いていることです。0x6000 番台は「サーボならこの番号にこれを置く」と規格で決まっています(CiA 402、後編で扱います)。
前回の Modbus の 40001 番地には、名前も型も単位もありませんでした。
SDO と PDO — 「速い SDO」ではない
CANopen には、OD にアクセスする方法が2つあります。速さの違いではなく、構造が違います。
| SDO | PDO | |
|---|---|---|
| 確認応答 | あり(要求と応答が対) | なし(投げっぱなし) |
| インデックス番号 | フレームに入る | 入らない |
| 1回で運べる量 | 4バイト(それ以上は分割) | 0〜8バイト(可変) |
| 用途 | 設定・任意の場所を読む | 周期的な制御データ |
PDO にインデックス番号が入らない、というのが肝です。では受け取ったバイト列をどう解釈するのか。答えは「COB-ID そのものが意味になっている」。事前に「0x181 で来たフレームは、先頭4バイトが 0x6064(現在位置)、続く2バイトが 0x6041(状態)」と合意しておきます。これを PDO マッピングと呼びます。長さは 0〜8バイトの範囲で決まり、この例なら6バイトです。

OD の項目(0x6064 現在位置・0x6041 状態)が、PDO マッピングの取り決めを介して、フレームのどこに詰まるかが決まります。この例では6バイトぶんが埋まり、残り2バイトは使われません。
sequenceDiagram
participant M as Master
participant S as Servo
Note over M,S: SDO は往復する(確認あり)
M->>S: 要求(OD の場所を指定して読み書き)
S-->>M: 応答(確かに書いた / これが値だ)
Note over M,S: PDO は投げっぱなし(確認なし)
M->>S: RPDO を投げるだけ(応答なし)
S->>M: TPDO を投げるだけ(応答なし)
意味を毎回送るのをやめ、事前の取り決めに追い出したぶん、PDO は速くなります。
TPDO と RPDO の向き — 基準はデバイス側
T は Transmit、R は Receive です。問題は「誰から見た送受信か」で、答えはデバイス(スレーブ)視点です。仕様書にもわざわざ "Table 34 has to be seen from the devices point of view" と書いてあります。
- TPDO = デバイスが送る = マスタから見れば受信
- RPDO = デバイスが受け取る = マスタから見れば送信
NMT — ネットワークの状態
デバイスは Initialisation・Pre-operational・Operational・Stopped の4つの状態を持ちます。
stateDiagram-v2
[*] --> Init
Init : Initialisation
Preop : Pre-operational
Op : Operational
Stop : Stopped
Init --> Preop : ブート後に自動
Preop --> Op : Start Remote Node
Op --> Preop : Enter Pre-operational
Preop --> Stop : Stop Remote Node
Op --> Stop : Stop Remote Node
Stop --> Preop : Enter Pre-operational
Stop --> Op : Start Remote Node
Op --> Init : Reset Node
Preop --> Init : Reset Node
Stop --> Init : Reset Node
| 状態 | SDO | PDO |
|---|---|---|
| Pre-operational | 動く | 動かない |
| Operational | 動く | 動く |
| Stopped | 動かない | 動かない |
設定は Pre-operational、制御は Operational という運用は、この表がそのまま理由です。Reset Communication は NMT のコマンド名であり、同時に、それを受けて入る Initialisation のサブ状態の名前でもあります(Reset Node コマンドで入るサブ状態は Reset application です)。
この表は規格の話です。リポジトリの servo.py には Stopped がなく、NMT コマンドで PDO の可否だけを切り替えて、SDO はどの状態でも受け取ります。
ハートビートと、その前身
ノードが生きているかの確認には、ハートビートを使います。ノードが周期的に 0x700 + Node-ID へ自分から送るフレームです。
古くはノードガーディング(マスタが問い合わせ、ノードが答える)もありましたが、CiA が明確に非推奨としています("guarding is an outdated method")。理由もはっきりしていて、この方式が使う RTR フレームは CAN FD に存在せず、そのままでは引き継げません。
SYNC — 全員に「今」を配る
COB-ID 0x080 の、通常はデータ長0のフレームです(オプションで1バイトのカウンタを載せられます)。同期型に設定された PDO(伝送タイプ 0〜240)を持つノードは、これが来た瞬間に測定値をラッチし、指令値を適用します。非同期の PDO は SYNC と関係なく流れるので、この効き方は設定しだいです。複数の軸を協調させるには「同じ瞬間」の合意が要ります。その合意を配るのが SYNC です。
ただし SYNC が配るのはミリ秒オーダーのトリガであって、EtherCAT の分散クロックのようなナノ秒精度の同期ではありません。
冒頭のログを読み下す
用語が揃ったので、最初に貼ったモニタの出力に戻ります。
0x703 [1] 00 は 0x700 + Node-ID 3 です。ただし中身はハートビートではなく、ノードが起動した直後に1回だけ出すブートアップメッセージです。0x700 番台のデータバイトは NMT の状態を表します。00 がブートアップ、7F が Pre-operational、05 が Operational、04 が Stopped です。自作モニタは COB-ID の上位ビットだけで種別を引いているので、両者を区別せず HEARTBEAT と表示しています。
0x000 [2] 01 03 は NMT です。1バイト目がコマンド指定子で、01 が Start Remote Node にあたります。2バイト目が宛先の Node-ID で、ここでは 3 番です(00 にすると全ノード宛になります)。
0x603 [8] 23 7a 60 00 10 27 00 00 は 0x600 + 3、Node 3 への SDO 要求です。先頭の 23 が「expedited download・データ4バイト」を表し、続く 7a 60 がインデックス 0x607A(Target position)、00 がサブインデックスです。インデックスも値もリトルエンディアンなので下位バイトが先に来ます。残りの 10 27 00 00 が書き込む値の 10000 です。
0x183 [6] fa 00 00 00 37 02 は 0x180 + 3 の TPDO1 です。前半4バイトが現在位置の 250、後半2バイトが 0x0237 で、これは後編で扱うステータスワードです。
③ 手を動かす
仮想CANバスの選び方
python-can には、ハードなしで使えるバックエンドが2つあります。性質が違うので先に選びます。
virtual |
udp_multicast |
|
|---|---|---|
| 別プロセスに届くか | 届かない(同一プロセス内のみ) | 届く |
| Windows | ○ | ✕(公式に非対応と明記) |
| 自分が送ったフレームが自分に返るか | 返らない | 返る(罠) |
この記事では、全OSで動くことを優先して virtual を使います。その代わりマスタとサーボを別ターミナルにはできないので、1つの Python プロセスの中に、マスタ役のオブジェクトとサーボ役のスレッドを同居させます。バス上を流れるフレームは本物と同じ形です。
virtual が真似できないもの(正直に)
- アービトレーション — 優先度は効きません。全部そのまま流れます
- ビットタイミング / ボーレート — 時間の概念がありません
- エラーフレーム / バスオフ — 存在しません
優先度が実際に効く様子は、実機のバスでしか見られません。一方で、実機とまったく同じ部分もあります。COB-ID の割り当てと、SDO / PDO のバイト並びは、本物のバスと1ビットも変わりません。NMT の状態遷移はフレームの上の話ではなく上位の実装が持つものですが、やり取りされるフレーム自体は同じです。
時間の話は仮想バスでは測れませんが、計算でなら示せます。1 Mbit/s・8バイト・スタッフビット込みで、1フレームはおおむね 110〜135 µs です。1 ms の周期に入るのは 7〜8 フレームが限界で、SYNC と各軸の PDO を数えると、CANopen の多軸協調が数軸規模の話になる理由が見えてきます。
(この記事では、仮想環境で測った時間を性能値として一切出しません。物理CANの数値とは因果関係がないからです)
最小のコード
02_canopen/smoke_test.py の中身です(サーボ役の部分だけ抜粋。コメントは短くしてあります。全文はリポジトリ)。
CHANNEL = "canopen-demo"
NODE_ID = 1
SYNC_COB_ID = 0x080
TPDO1 = 0x180 + NODE_ID # ノードが送る(TPDO はデバイス視点)
RPDO1 = 0x200 + NODE_ID # ノードが受け取る
def servo_node(stop, ready):
"""SYNC を受けたら現在位置を TPDO で返すだけの最小ノード"""
bus = can.Bus(interface="virtual", channel=CHANNEL)
ready.set() # これが無いと最初のフレームを取りこぼす(後述)
position, target = 0, 0
while not stop.is_set():
msg = bus.recv(timeout=0.1)
if msg is None:
continue
if msg.arbitration_id == RPDO1:
target = int.from_bytes(msg.data[:4], "little", signed=True)
elif msg.arbitration_id == SYNC_COB_ID:
position += (target - position) // 4 # 1次遅れ相当の追従
bus.send(can.Message(arbitration_id=TPDO1,
data=position.to_bytes(4, "little", signed=True),
is_extended_id=False))
bus.shutdown()
マスタ側はこのスレッドを起動して ready.wait() で待ち、RPDO で目標値を送ってから SYNC を打ちます。
sync # 0 target=10000 actual= 2500
sync # 5 target=10000 actual= 8219
sync #10 target=10000 actual= 9577
sync #15 target=10000 actual= 9898
TPDO replies: 20/20
起動の競合という罠
上のコードの ready.wait() は飾りではありません。
virtual バスは、受信側の can.Bus() がまだ作られていない間に送られたフレームを、黙って捨てます。スレッドを起動した直後にマスタが送信すると、最初の RPDO が消えます。エラーも例外も出ません。
実際に ready.wait() を外して走らせると、20回打った SYNC のうち1回ぶんの応答が返らず、actual は 0 のままになります。ただし競合なので毎回は起きません。手元で20回走らせたときに再現したのは6回でした。起きなかったからといって、この問題が無いわけではないところが厄介です。サーボは何事もなかったかのように、ただ動きません。本物の CAN なら電源の入ったノードは常にそこにいるので起きない、シミュレーション固有の現象です。
発展: 別プロセスに分けたい人へ
Linux / macOS なら udp_multicast バックエンドでマスタとサーボを別ターミナルに分けられます(Windows では動きません)。ただし python-can の実装上、receive_own_messages=False を指定しても自分の送信フレームが返ってくるので、can_filters で受理する COB-ID を絞る必要があります。これは CAN の仕様ではなくライブラリの制約です。
コード
この連載で動かすコードは、5回ぶんまとめて GitHub に置いてあります。今回の 02_canopen/ のほかに、Modbus と OPC UA も同じリポジトリです。
https://github.com/logicia32/industrial-networks-lab
④ 次回
ここまでで、CANopen の「配管」は一通り通りました。NMT で起動して、SDO で設定して、PDO と SYNC で周期的にデータを流します。
次回は、この配管に中身を流します。0x6040、0x6041、0x607A を使う、サーボモータの共通語 CiA 402 です。仮想のサーボを Operation Enabled まで持っていって、位置指令に追従させ、その追従カーブをグラフにします。
そこで書くコードの知識が、そのまま EtherCAT にも通用する理由も説明します。
今回使ったもののライセンス
| ライセンス | |
|---|---|
| python-can 4.6.1 | LGPL-3.0-only |
仕様書
- CiA 301(CANopen アプリケーション層・通信プロファイル)は PAS(Publicly Available Specification) として無料で入手できます(can-cia.org での無料登録が必要)。
https://www.can-cia.org/can-knowledge/canopen - can-cia.org の
can-knowledge/配下の解説ページ群は登録不要で読めます。規格団体自身が書いたものなので信頼できます。
この記事は Zenn に初出したものを加筆・補足したものです ── Zenn の元記事を見る