MCP2518FD はマイクロチップ社の CAN FD コントローラです。CAN FD の面倒を見る単機能のコントローラで、ホストのマイクロコントローラやコンピュータから SPI 接続して使います。CAN 機能を内蔵しているマイクロコントローラが増えている昨今、外付けの専用コントローラ一本なんて廃止の方向なのでは?と思ってたんですけれども、CAN FD 仕様の細かなところまで網羅しているし FIFO も大きいし、フィルタ機能も強力ですし、よそにない利点がてんこ盛りで使いでがあると思います。しかし、このコントローラは多機能なだけに設定方法が大変複雑です。作業していてもすぐに混乱してしまうので整理するためにこのページにまとめてみました。
MCP2518FD を様々なデバイスから使うためのライブラリを開発しています。コードは Github に公開しています。
https://github.com/naokiiwakami/can-controller
準備
資料集め
自分自身でデバイスの設定を行うなら、データシートとリファレンスマニュアルはぜひ必要です。データシートにはレジスタの詳細、リファレンスマニュアルにはデバイスの使い方が書かれています。
レジスタの意味や設定の方法はあまりデータシートには書かれておらず、それはリファレンスマニュアルに書かれています。反対にリファレンスマニュアルには各部分がどうやって動いていてどのように設定するかは書かれていますが、「ここのレジスタのどの値を設定して」と細かに書いてあるわけではありません。したがってドキュメントだけで動くものを実装するのはとても大変です。ですがマイクロチップ社が getting started ページを用意していてこれが助けになります。このページで配布しているプロジェクト例には MCP2518FD を使う API が含まれています。API はマイクロチップ社のデバイス用で他には使えませんが、API を動かすのに必要なデバイスの設定用の C ヘッダファイルが含まれていて、これは他環境でコントローラを動かす際に使えて大変助かります。具体的には drv_canfdspi_register.h と drv_canfdspi_defines.h です。ページ内のどのプロジェクトにも入っているようですが、私は最初の2プロジェクトだけ確認しました。
ハードウェア
この記事は以下の回路をラズベリーパイに繋いだ場合の MCP2518FD の設定について書いています。ラズベリーパイの SPI0 を MCP2518FD の SPI に繋いで使っています。

ラズベリーパイ側の使い方は記事と関係ないので省きますが公開されている github プロジェクトを見れば参考になるかもしれません。でもラズパイでは MCP2518FD 用のデバイスドライバが用意されていると思うので普通はそちらを使いますよね。以下、ハードウェアを設計する時に迷った点について簡単に触れておきます
電源
MCP2518FD は 3.3V 電源でも 5V 電源でも動きますが、ラズベリーパイの I/O が 3.3V レベルなので、レベルシフトを避けるために MCP2518FD も 3.3V 電源で使っています。上記の回路では、テストのためあえて 3.3V 電源で動く CAN トランシーバを使っていますが、CAN トランシーバの電源は 5V というやり方もできます。以前書いたブログ記事を参考にしてみてください。
クロック
MCP2518FD データシートに書かれていますが、推奨されているクロックは以下の三種類
- 水晶発振子 20 MHz を使い PLL をオフ、20 MHz のシステムクロック
- 水晶発振子 40 MHz を使い PLL をオフ、40 MHz のシステムクロック
- 水晶発振子 4 MHz を使い 10x の PLL をオン、40 MHz のシステムクロック
この記事では最初のオプションを使っています。今後 3 番目のオプションも試してみたいと考えています。
トランシーバ
CAN トランシーバは私が知っている限り全て 8 ピンで、電源・Tx・Rx・CANH・CANL のピン配列は同じです。CAN トランシーバはほとんどが 5V 電源に限られていて、3.3V の電源を与えるとシャットダウン中とみなしてスリープしてしまいます。この記事で使われている TCAN3413 は唯一見つけられた 3.3V 電源で稼働するトランシーバです。ですが 5V 電源 のトランシーバを 3.3V のコントローラに繋ぐことは可能です。また同一の CAN バス上に 5V 電源と 3.3V 電源のトランシーバが混在していても大丈夫です。詳しくは以前に書いたブログ記事を見てください。
トランシーバの 5 ピンと 8 ピンの役割は製品によって違います。TCAN3413 では 5 ピンはコントローラに繋ぐ I/O ピンのレベル合わせに使われます。コントローラの電源と同じ電位に繋げば OK です。8ピンはスタンバイピンで、H レベルの時には動作が止まります。このピン MCP2518FD の STBY ピンに繋いでコントローラから制御できるようにしました。この機能はオプションで使わなくても動作はします。使わない場合 GND に繋いで L レベルに固定しておきます。
割り込みピン
この記事の例では、ホストのアプリケーション側で、CAN フレーム受信の通知を受けることが必要で、そのため Rx を通知する INT1 ピンをホストに繋いでいます。ホストはラズベリーパイで INT1 ピン変化の検知には WiringPi を使いました。
使わなかった MCP2518FD のピン
汎用の割り込みピン INT とクロック出力ピンは使いませんでした。
MCP2518FD コントローラの概要
構成
電源部、SPI インタフェース、クロック発振器、CAN FD 制御、RAM などから構成されています。設定もおおまかにこれらのブロックごとに行います。特徴的なのは、メッセージの FIFO やキューを構成するため 2 KB の RAM を持っていることで、このメモリをどう使うか設定時に決める必要があります。

CAN FD 制御部は CAN 通信をつかさどる部分で最も複雑です。内部は以下のようにいくつかの部分で構成されています。

ブロックの各々の概要はデータシートの 2.0 節に書かれていてそれを読むのが良いかと思いますが、メッセージのおおざっぱな流れをみるとどの部分がどんな時に動くか見えてくるかもしれません。
送信の場合、RAM 上に構成された TX FIFO または TXQ にメッセージオブジェクトを格納して FIFO 制御レジスタまたは TXQ 制御レジスタの送信依頼ビットを立てると送信が始まります。
受信の場合、メッセージはフィルタにかけられフィルタに通ったものが RAM 上に構成された RX FIFO に格納されて受信 FIFO レジスタが更新された後、受信通知が有効化されていた場合通知が行われます。FIFO は読まれれば空になります。
動作モード
MCP2518FD は以下のように大きく分けて4つの動作モードを持っています。電源投入時やリセット後には Configuration モードに入ります。その後各レジスタを使って設定を行ってから Normal モードに移るのが通常の動作です。
- Configuration
- Normal
- Sleep
- Debug
Normal モードには、CAN FD モードと CAN 2.0 モードがあって CAN 2.0 では FD フレームの送受信はできず、いわゆるクラシック CAN のコントローラとして動作します。
メモリ配置
デバイスのメモリ配置は右のようになっていています。
SFR とは Special Function Register の略でデバイスの設定を行う 32bit のレジスタです。CANFD SFR は CAN FD 制御部を操作するためのレジスタで、MCP2518FD SFR はクロックや電源などデバイスのシステムを設定するレジスタです。
デバイスへの操作はレジスタまたは RAM への読み書きを介して行われ、レジスタにもアドレスが割り振られているので、どの操作もどこかのメモリアドレスへの読み書きという形で実行されます。

デバイスの起動
起動までの流れ
前述の通り、デバイス起動もしくはリセット後には Configuration モードに入ります。CAN コントローラとして起動させるにはモードを Normal CAN FD または Normal CAN 2.0 に移行させますが、その前に必要な設定を済ませておかなくてはなりません。したがって起動の順序はこんな感じになります
- リセット
- システム設定
- CAN FD 制御部設定
- Normal モードへ移行
以下は、開発しているライブラリの初期化ルーチンを読みやすく直したものです。
uint8_t device_init(can_config_t *config) {
mcp25xxfd_reset();
// MCP2518FD system setup
mcp25xxfd_config_osc(config);
mcp25xxfd_config_io();
mcp25xxfd_config_bit_time(config);
mcp25xxfd_config_interrupt();
// CAN FD controller setup
mcp25xxfd_config_can_control(config);
mcp25xxfd_config_txfifo();
mcp25xxfd_config_rxfifo();
mcp25xxfd_config_default_filter();
return 0;
}Rustコントローラは多機能ですが、多少フィルタをかけるぐらいの最低限ぐらいの設定をしています。これぐらいまで来ると、レジスタをどう設定するかはリファレンスマニュアルとデータシートを交互に見て行けばわかるようになってきますから詳細は省略します。この記事では、具体的にレジスタを設定する方法と、設定に手こずったビットタイミングの設定、FIFO の設定、RX フィルタの設定についてだけ触れます。
レジスタの設定方法 (SPI インタフェース)
先述のように、レジスタはアドレスを持っていて、そのメモリアドレスへの読み書きを SPI を介して行えばレジスタ操作ができます。一回の SPI 操作で何バイト読み書きしてもかまいません。が、IOCON レジスタだけは 1 バイト書き込みでなくてはいけません。SPI では最初の 4 ビットにコマンド、次の 12 ビットにアドレスを入れて送信します。使えるコマンドは以下の通り
- RESET
- READ
- WRITE
- READ_CRC
- WRITE_CRC
- WRITE_SAFE
私の用途では RESET, READ, WRITE だけ使えば十分で残りのコマンドの詳細はすみませんが把握していません。SPI 通信の残りはデータバイトで WRITE 命令の時にはレジスタや RAM に描きこむ内容を載せます。READ 命令の時には無視されます。
レジスタのアドレスは下の図のようにデータシートに書いてあるのでそれを探せばよいです。

例えば、クロックの設定を行う OSC レジスタにアクセスするにはアドレス E00 に行けばよいです。1個のレジスタのサイズが 4 バイトなので、一度のコマンドでレジスタ最低一個分の操作ができることになります。
このコントローラは多機能でレジスタの数が多いので、コーディングの際にこのアドレスを一個一個打ち込むのは気の遠くなる作業ですし打ち間違いが発生しやすいので危なっかしいです。幸いなことに、前述の、マイクロチップ社が配布している drv_canfdspi_register.h ヘッダファイルにアドレスは全て書かれているので私はありがたくそれを使わせてもらいました。例えば OSC レジスタについての部分を抜粋するとこんな風になっています。
/* MCP25xxFD Specific */
#define cREGADDR_OSC 0xE00
#define cREGADDR_IOCON 0xE04
#define cREGADDR_CRC 0xE08
#define cREGADDR_ECCCON 0xE0C
#define cREGADDR_ECCSTA 0xE10
#ifndef MCP2517FD
#define cREGADDR_DEVID 0xE14
#endifRustそしてさらに OSC レジスタのビットの詳細はデータシートに書かれているわけですが、再び drv_canfdspi_register.h に以下のような定義があります。
typedef union _REG_OSC {
struct {
uint32_t PllEnable : 1;
uint32_t unimplemented1 : 1;
uint32_t OscDisable : 1;
#ifdef MCP2517FD
uint32_t unimplemented2 : 1;
#else
uint32_t LowPowerModeEnable : 1;
#endif
uint32_t SCLKDIV : 1;
uint32_t CLKODIV : 2;
uint32_t unimplemented3 : 1;
uint32_t PllReady : 1;
uint32_t unimplemented4 : 1;
uint32_t OscReady : 1;
uint32_t unimplemented5 : 1;
uint32_t SclkReady : 1;
uint32_t unimplemented6 : 19;
} bF;
uint32_t word;
uint8_t byte[4];
} REG_OSC;Rustこれはうまくできていて、4 バイトの大きさの union として定義されています。設定の時は、ビットフィールドを使ってレジスタのビット配分通りに定義されている構造体を使って値を当てて行けばビット操作することなくレジスタ値を作ることができます。SPI に送るときには byte メンバを使えば良く、レジスタの値をコピーする場合には word メンバを使えば memcpy を呼び出すことなく行うことができます。これが全レジスタについて定義されていて大変ありがたく。正直これがなければ設定プログラムの実装は厳しかったです。
ビットタイミング
ビットタイミングの設定は、データシートを見るだけ、で済むはずなんだけど、大変煩雑で毎度手こずる箇所です。ビットタイミングの計算方法はどのメーカーのデバイスの説明を見ても同じなのでおそらく CAN 規格で標準化されているのでしょう。設定のたびに忘れてしまってリファレンスマニュアルを読み直さなくてはいけなくて時間がかかるのと Embassy で自動計算してくれるのが便利だったので、自分のライブラリでも自動計算を実装しました。ついでにそれをこのページに移植してみました。
CAN / CAN-FD ビットタイミング計算機
Nominal Phase
Data Phase
要注意なのが、ビットタイミング計算で得られた値をそのままレジスタに入れてはだめで、値から 1 引いた数をレジスタに入れる必要があります。ビットタイミングで 0 値はないからだと思います。ビットタイミング計算については、いつか別記事がかけたらと考えています。
FIFO の設定
さあ、ビットタイミングの設定ができたから CAN 通信ができるようになったぞ。送信だ!とはなりません。 MCP2518FD は甘くないデバイスなのです。このコントローラは FIFO か TXQ を使って送信します。TXQ とは特殊な送信用の FIFO で、通常の FIFO より優先してメッセージが処理されます。FIFO や TXQ の実体は RAM ですから、メモリにデータを置いてから、行け、送信!と命令します。受信はその反対でやっぱり FIFO を使います。このページの前の方で触れましたが、FIFO の実体は 2KB の RAM です。ではどこのアドレスが FIFOでどこが TXQ? というと、それはユーザが決めることになっています。FIFO は最大 32 本まで持てて長さも設定できます。非常に柔軟ですが単純な用途にも FIFO の定義が漏れなく必要です。実際にはアドレスを具体的に計算する必要はなくて FIFO の番号と大きさを決めるだけで良いですが全体の設計で 2KB の RAM サイズを超えないように計算をしなくてはいけません。RAM 内でのメモリの構成は以下のようになっていきます。

この辺のことはリファレンスマニュアルが正確で必要なこと全て網羅されているのでそちらを見れば良いですが、大変面倒な部分なので、この記事では、Tx FIFO 一本と Rx FIFO 一本、TXQ は使わない用途の場合どんな設定になるかの例を載せることにします。以下、ライブラリの実際のコードで Tx FIFO の設定をしている部分です。
uint8_t mcp25xxfd_config_txfifo() {
// Use FIFO1 for TX
CAN_FIFO_CHANNEL channel = CAN_FIFO_CH1;
union RegisterSpiObject info = {0};
REG_CiFIFOCON fifo_con;
fifo_con.word = canFifoResetValues[0];
fifo_con.txBF.PayLoadSize = CAN_PLSIZE_8;
fifo_con.txBF.FifoSize = 0x4; // 5 messages
fifo_con.txBF.TxAttempts = 0x3; // unlimited
fifo_con.txBF.TxPriority = 0x10; // setting medium
fifo_con.txBF.TxEnable = 1;
info.data.body.word = fifo_con.word;
uint16_t address = cREGADDR_CiFIFOCON + channel * CiFIFO_OFFSET;
mcp25xxfd_write_register(address, &info, 4);
return 0;
}
Rustこんな風に、FIFO ごとに FIFO 制御レジスタがあるので、そこに FIFO の用途大きさ等必要情報を書き込みます。これで FIFO の使い道と大きさが決まって、RAM 上に領域が割り当てられます。アドレスがどこになったかなどはここではなく FIFO 操作レジスタに現れるので送受信の時に触れます。
フィルタの設定
たいていの CAN コントローラがそうですが、フィルタを設定しないと受信ができません。フィルタのことを書くとそれだけで記事一本になってしまいそうなので、ここでは大まかに何をする必要があるかだけ書きます。以下はライブラリで使っている「全部受信」するデフォルトフィルタの設定部です。
uint8_t mcp25xxfd_config_default_filter() {
uint16_t address;
// disable filter 0
mcp25xxfd_disable_filter(CAN_FILTER0);
// Configure filter 0 object and mask to match all
CAN_FILTER filter = CAN_FILTER0;
union RegisterSpiObject info = {0};
info.data.body.word = 0;
address = cREGADDR_CiFLTOBJ + (filter * CiFILTER_OFFSET);
mcp25xxfd_write_register(address, &info, 4);
info.data.body.word = 0;
address = cREGADDR_CiMASK + (filter * CiFILTER_OFFSET);
mcp25xxfd_write_register(address, &info, 4);
// link filter to the RX fifo, then enable the filter
mcp25xxfd_link_filter_to_fifo(CAN_FILTER0, CAN_FIFO_CH2, 1);
return 0;
}RustFIFO に受信メッセージを入れるには最低一個のフィルタがついてなくてはいけなくて、上のプログラムは全通しのフィルタを受信用 FIFO に紐づけています。
ノーマルモードへの移行
これで今度こそメッセージの送受信が可能な状態になります。ここで、モードを Configuration から Normal に切り替えて運用開始します。
モード移行は C1CON レジスタの REQOP ビットを変更することで行われます。
メッセージの送信
メッセージの送信手順はリファレンスマニュアルの 4 節に書かれています。FIFO または TXQ にメッセージオブジェクトを入れてから C1FIFOCONn レジスタに送信依頼を送ります。n は FIFO 番号です。
メッセージオブジェクトの作成
メッセージオブジェクトは、RAM に置く CAN メッセージのイメージです。メモリにどんな風にマッピングするかはデータシートの 3.3 節に書かれています。

CAN メッセージの各種フラグと ID をメモリ上に配置した後 DLC で指定したバイト数だけのデータを入れます。
メッセージオブジェクトの FIFO へのロード
メッセージオブジェクトをロードするにあたって、
- FIFO に空きはあるか?
- FIFO 最後尾のアドレスはどこか?
をコントローラに問い合わせます。FIFO の状態は CiFIFOCONm レジスタ、アドレスは CiFIFOUAm レジスタから取得します。

FIFO に空きがあるかは TFNRFNIF (Transmit/Receive FIFO Not Full/Not Empty Interrupt Flag) が 1 であるかどうかでわかります。空きがなければ待つ必要があります。
次に FIFO に入れるべきアドレスは以下のように計算します。0x400 は前述のメモリマップの通り RAM の先頭アドレスですね。
このアドレスにメッセージオブジェクトを書き込んだらロード完了です。Tx 制御部は DLC 分のデータしか読まないので書き込みも DLC 分で十分ですが、MCP2518FD の SPI コマンドで RAM へ読み書きする際は必ず 4 バイトの倍数で行わなくてはならないので注意が必要です(コマンド 2byte + データ 4 x N byte)。(データシート 4.2 節参照)
メッセージをロードし終わったら、CiFIFOCONm レジスタの UNIQ ビットを 1 にセットすると FIFO が一個進められます。この操作は次のメッセージ送信依頼とまとめて行っても大丈夫です。
メッセージ送信依頼
CiFIFOCONm レジスタの送信依頼ビット TXREQ を 1 にセットすると送信が開始されます。
送信バンド幅共有
TBD
再送信
TBD
受信メッセージのフィルタリング
TBD
メッセージの受信
Tx と同様 CiFIFOCONm レジスタの TFNRFNIF ビットが 1 なら FIFO にメッセージが入っています。CiFIFOUAm レジスタから FIFO アドレスを取得して以下の構造のメッセージオブジェクトを取得します。

まとめ
以上、MCP2518FD の使い方の概要を書きました。あまりに多機能でリファレンスマニュアルとデータシートの情報が膨大なので、全体の流れが理解しやすいようにすることを目的としています。割り込みや I/O の設定、エラーハンドリングをどうするかの設定など加えたい項目もありますがまずはここまで。




