Zybo z7 の Linux で PmodCAN を使ってみる

# 概要

Zybo Z7 の PMOD 端子に接続した２つの CAN デバイスを使い、Linux 上で通信を行います。CAN デバイスは [Digilent 社の Pmod CAN](https://digilent.com/reference/pmod/pmodcan/start) を利用します。

Pmod CAN は専用の IP が Digilent 社から提供されていますが、ここでは専用 IP を使わない方法を記述しています。

# 環境

<dl>
    <dt>
        Bord
    </dt>
    <dd>
        Zybo Z7-20
    <dd>
    <dt>
        Can Board
    <dt>
    <dd>
        Digilent 社 PmodCAN
    </dd>
    <dt>
        Tool
    </dt>
    <dd>
        Vivado ML Edition 2023.2.1 (Windows11)
    </dd>
    <dd>
        Petalinux 2023.2.1 (Ubuntu 22.04.4)
    </dd>
</dl>

# Vivado 作業手順

今回は２つの SPI Master Controller を用意して、各 Controller に Pmod CAN を接続する形とします。

以下が Block Design です。

![完成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3332407/25b9bc62-6d05-c2f8-1471-26b5099a52b7.png)

作業手順は Tcl スクリプトで記述しています。スクリプトを vivado 起動後の Tcl console で実行すると構築手順が追えるかと思います。

## Project 作成

ここでは **c:\vivado\zybo-dual-pmodcan\\** フォルダ配下にプロジェクトを作成します。

```tcl
# create zybo-dual-pmodcan project
file mkdir c:/vivado
create_project zybo-dual-pmodcan C:/vivado/zybo-dual-pmodcan -part xc7z020clg400-1
set_property board_part digilentinc.com:zybo-z7-20:part0:1.1 [current_project]
```

## Block Design 作成
### Zynq-7000 追加

最初に`design_1`という名称で Block Design を作成して、Zynq-7000 を追加します。今回は PL 側からの割り込み信号を４本接続しますので、Zynq の外部割り込み線を接続可能にします。

```tcl
# create a block design
create_bd_design "design_1"
update_compile_order -fileset sources_1
# add Zynq-7000
startgroup
create_bd_cell -type ip -vlnv xilinx.com:ip:processing_system7:5.5 processing_system7_0
apply_bd_automation -rule xilinx.com:bd_rule:processing_system7 -config {make_external "FIXED_IO, DDR" apply_board_preset "1" Master "Disable" Slave "Disable" }  [get_bd_cells processing_system7_0]
# export interrupt lines
set_property -dict [list \
  CONFIG.PCW_IRQ_F2P_INTR {1} \
  CONFIG.PCW_USE_FABRIC_INTERRUPT {1} \
] [get_bd_cells processing_system7_0]
endgroup
```

### AXI Quad SPI 追加

AXI Quad SPI を２つ追加します。AXI Quad SPI の STARTUP Primitive は無効化しています。その後、各 Controller の信号を外部ポートに接続します。

```tcl
# add AXI Quad SPI#0
startgroup
create_bd_cell -type ip -vlnv xilinx.com:ip:axi_quad_spi:3.2 axi_quad_spi_0
set_property CONFIG.C_USE_STARTUP {0} [get_bd_cells axi_quad_spi_0]
create_bd_intf_port -mode Master -vlnv xilinx.com:interface:spi_rtl:1.0 SPI_0
connect_bd_intf_net [get_bd_intf_pins axi_quad_spi_0/SPI_0] [get_bd_intf_ports SPI_0]
endgroup
# add AXI Quad SPI#1
startgroup
create_bd_cell -type ip -vlnv xilinx.com:ip:axi_quad_spi:3.2 axi_quad_spi_1
set_property CONFIG.C_USE_STARTUP {0} [get_bd_cells axi_quad_spi_1]
create_bd_intf_port -mode Master -vlnv xilinx.com:interface:spi_rtl:1.0 SPI_1
connect_bd_intf_net [get_bd_intf_pins axi_quad_spi_1/SPI_0] [get_bd_intf_ports SPI_1]
endgroup
```

### 割り込み信号接続

SPI Controller と PmodCAN の割り込み信号を Zynq-7000 に接続します。今回は全部で４本（AXI Quad SPIx2, PmodCANx2）の割り込み信号線を接続しますが、複数の割り込み線は１度 Concat でまとめてから Zynq-7000 に接続する必要があります。

PmodCAN は PMOD の７番ピンに CAN Controller(MCP25625)
の割り込み信号が接続されています。これは Active-Low の信号ですが、Zynq-7000に接続する割り込み信号は Rising-Edge か Active-High である必要がありますので、NOT 回路(Utility Vector Logic) で反転してから concat に接続します。 

```tcl
# add a concat
startgroup
create_bd_cell -type ip -vlnv xilinx.com:ip:xlconcat:2.1 xlconcat_0
set_property CONFIG.NUM_PORTS {4} [get_bd_cells xlconcat_0]
endgroup
# add two utility vector logics
startgroup
create_bd_cell -type ip -vlnv xilinx.com:ip:util_vector_logic:2.0 util_vector_logic_0
set_property -dict [list \
  CONFIG.C_OPERATION {not} \
  CONFIG.C_SIZE {1} \
] [get_bd_cells util_vector_logic_0]
create_bd_cell -type ip -vlnv xilinx.com:ip:util_vector_logic:2.0 util_vector_logic_1
set_property -dict [list \
  CONFIG.C_OPERATION {not} \
  CONFIG.C_SIZE {1} \
] [get_bd_cells util_vector_logic_1]
set_property location {1 249 -236} [get_bd_cells util_vector_logic_1]
endgroup
# create ports for can controller interrupt
startgroup
create_bd_port -dir I -type intr CAN0_INT
set_property CONFIG.SENSITIVITY LEVEL_LOW [get_bd_ports CAN0_INT]
create_bd_port -dir I -type intr CAN1_INT
set_property CONFIG.SENSITIVITY LEVEL_LOW [get_bd_ports CAN1_INT]
endgroup
# connect interrupt lines
startgroup
connect_bd_net [get_bd_pins util_vector_logic_0/Res] [get_bd_pins xlconcat_0/In2]
connect_bd_net [get_bd_pins util_vector_logic_1/Res] [get_bd_pins xlconcat_0/In3]
connect_bd_net [get_bd_ports CAN0_INT] [get_bd_pins util_vector_logic_0/Op1]
connect_bd_net [get_bd_ports CAN1_INT] [get_bd_pins util_vector_logic_1/Op1]
connect_bd_net [get_bd_pins axi_quad_spi_0/ip2intc_irpt] [get_bd_pins xlconcat_0/In0]
connect_bd_net [get_bd_pins axi_quad_spi_1/ip2intc_irpt] [get_bd_pins xlconcat_0/In1]
connect_bd_net [get_bd_pins xlconcat_0/dout] [get_bd_pins processing_system7_0/IRQ_F2P]
endgroup
```

### SPI 用クロック追加

Clocking Wizard で SPI Bus で利用するクロックを追加します。[AXI Quad SPI のデータシート](https://docs.amd.com/v/u/en-US/ds843_axi_quad_spi)によると SPI Bus で利用する最大周波数×２のクロックを ext_spi_clk に接続する必要があります。

MCP25625 は最大 10MHz の SPI Clock に対応していますので、20MHz を ext_spi_clk に接続します。

```tcl
# add a clock generator for SPI bus
startgroup
create_bd_cell -type ip -vlnv xilinx.com:ip:clk_wiz:6.0 clk_wiz_0
set_property -dict [list \
  CONFIG.CLKOUT1_JITTER {193.154} \
  CONFIG.CLKOUT1_PHASE_ERROR {109.126} \
  CONFIG.CLKOUT1_REQUESTED_OUT_FREQ {20} \
  CONFIG.MMCM_CLKFBOUT_MULT_F {8.500} \
  CONFIG.MMCM_CLKOUT0_DIVIDE_F {42.500} \
] [get_bd_cells clk_wiz_0]
connect_bd_net [get_bd_pins processing_system7_0/FCLK_CLK0] [get_bd_pins clk_wiz_0/clk_in1]
connect_bd_net [get_bd_pins axi_quad_spi_0/ext_spi_clk] [get_bd_pins clk_wiz_0/clk_out1]
connect_bd_net [get_bd_pins axi_quad_spi_1/ext_spi_clk] [get_bd_pins clk_wiz_0/clk_out1]
endgroup
# connect the lock signal to LD0 (for debugging)
startgroup
create_bd_port -dir O -type data LED_LD0
connect_bd_net [get_bd_pins /clk_wiz_0/locked] [get_bd_ports LED_LD0]
endgroup
```

Clocking Wizard の入力クロックは Zynq-7000 の FCLK_CLK0 から取って来ます。このクロックはデフォルトで 50MHz となるので 20MHz を生成するには十分だと思います。

後、デバッグ用に lock 信号をボード上の LED LD0 に接続します。これにより SPI Bus へのクロック供給状態が LD0 の点灯／消灯で視認できます。

reset 信号は後で接続します。

### 途中確認

ここまでの TCL スクリプトを実行した後、`regenerate_bd_layout` を実行すると下図の様な Block Design になりますので、一度確認して下さい。

![途中.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3332407/3ed1c33e-1ff1-fe59-df80-9dfe9c001d4c.png)

### AXI とリセット信号を接続

後は AXI Bus とリセット信号を接続します。これらの作業の大部分は Vivado に自動で接続してもらいます。

```tcl
startgroup
apply_bd_automation -rule xilinx.com:bd_rule:axi4 -config { Clk_master {Auto} Clk_slave {Auto} Clk_xbar {Auto} Master {/processing_system7_0/M_AXI_GP0} Slave {/axi_quad_spi_0/AXI_LITE} ddr_seg {Auto} intc_ip {New AXI Interconnect} master_apm {0}}  [get_bd_intf_pins axi_quad_spi_0/AXI_LITE]
apply_bd_automation -rule xilinx.com:bd_rule:axi4 -config { Clk_master {Auto} Clk_slave {Auto} Clk_xbar {Auto} Master {/processing_system7_0/M_AXI_GP0} Slave {/axi_quad_spi_1/AXI_LITE} ddr_seg {Auto} intc_ip {New AXI Interconnect} master_apm {0}}  [get_bd_intf_pins axi_quad_spi_1/AXI_LITE]
connect_bd_net [get_bd_pins rst_ps7_0_50M/peripheral_reset] [get_bd_pins clk_wiz_0/reset]
create_bd_port -dir O -type rst CAN0_RESET_N
create_bd_port -dir O -type rst CAN1_RESET_N
connect_bd_net [get_bd_ports CAN0_RESET_N] [get_bd_pins rst_ps7_0_50M/peripheral_aresetn]
connect_bd_net [get_bd_ports CAN1_RESET_N] [get_bd_pins rst_ps7_0_50M/peripheral_aresetn]
endgroup
regenerate_bd_layout
```
最後に Clocking Wizard の reset 信号を自動で追加された Processor System Reset に接続しています。

## HDL wrapper 生成

作成した Block Design の HDL wrapper を生成します。

```tcl
# create a wrapper file
cd [get_property DIRECTORY [current_project]]
make_wrapper -files [get_files ./[current_project].srcs/sources_1/bd/design_1/design_1.bd] -top
add_files -norecurse ./[current_project].gen/sources_1/bd/design_1/hdl/design_1_wrapper.v
```

## 制約ファイル追加

制約ファイルを追加します。下記ファイルを `zybo-dual-pmodcan.xdc` というファイル名でプロジェクト直下に置いて下さい。

```:zybo-dual-pmodcan.xdc
# pmod JB
set_property PACKAGE_PIN V8 [get_ports {SPI_0_ss_io[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {SPI_0_ss_io[0]}]
set_property PACKAGE_PIN W8 [get_ports SPI_0_io0_io]
set_property IOSTANDARD LVCMOS33 [get_ports SPI_0_io0_io]
set_property PACKAGE_PIN U7 [get_ports SPI_0_io1_io]
set_property IOSTANDARD LVCMOS33 [get_ports SPI_0_io1_io]
set_property PACKAGE_PIN V7 [get_ports SPI_0_sck_io]
set_property IOSTANDARD LVCMOS33 [get_ports SPI_0_sck_io]

# pmod JD
set_property PACKAGE_PIN T14 [get_ports {SPI_1_ss_io[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {SPI_1_ss_io[0]}]
set_property PACKAGE_PIN T15 [get_ports SPI_1_io0_io]
set_property IOSTANDARD LVCMOS33 [get_ports SPI_1_io0_io]
set_property PACKAGE_PIN P14 [get_ports SPI_1_io1_io]
set_property IOSTANDARD LVCMOS33 [get_ports SPI_1_io1_io]
set_property PACKAGE_PIN R14 [get_ports SPI_1_sck_io]
set_property IOSTANDARD LVCMOS33 [get_ports SPI_1_sck_io]

# interrupt
set_property PACKAGE_PIN Y7 [get_ports CAN0_INT]
set_property IOSTANDARD LVCMOS33 [get_ports CAN0_INT]
set_property PACKAGE_PIN U14 [get_ports CAN1_INT]
set_property IOSTANDARD LVCMOS33 [get_ports CAN1_INT]

# reset
set_property PACKAGE_PIN Y6 [get_ports {CAN0_RESET_N[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {CAN0_RESET_N[0]}]
set_property PACKAGE_PIN U15 [get_ports {CAN1_RESET_N[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {CAN1_RESET_N[0]}]

# for debugging
set_property PACKAGE_PIN M14 [get_ports LED_LD0]
set_property IOSTANDARD LVCMOS33 [get_ports LED_LD0]
```

ファイルを置いたら下記コマンドでプロジェクトに追加します。

```tcl
add_files -fileset constrs_1 -norecurse [get_property DIRECTORY [current_project]]/[current_project].xdc
```

## bitsream と XSA ファイルの生成

Bitsream と XSA ファイルを生成します。-jobs の値は利用している PC に合わせて変更して下さい。

```tcl
# generate bitstream file
launch_runs synth_1 -jobs 4
wait_on_run synth_1
launch_runs impl_1 -to_step write_bitstream -jobs 4
wait_on_run impl_1
# export hardware platform
write_hw_platform -fixed -include_bit -force -file [get_property DIRECTORY [current_project]]/[current_project].xsa
```

成功するとプロジェクトフォルダ直下に `zybo-dual-pmodcan.xsa` が作成されます。

# petalinux 作業手順
## 前提条件

作業手順では下記条件での実行を前提としています。

* petalinux の各種ツールが利用可能
* ホームディレクトリ配下に zybo-dual-pmodcan.xsa がコピー済み

## 準備

最初に環境変数を設定します。ここでは ~/petalinux/ 配下にプロジェクトを作成する様に設定しています。

```bash
WORK_DIR=~/petalinux
PROJ_NAME=zybo-dual-pmodcan
PROJ_DIR=$WORK_DIR/$PROJ_NAME
PROJ_XSA=~/$PROJ_NAME.xsa
```

## プロジェクト作成

次にプロジェクトを作成し、作成した XSA ファイルを取り込みます。petalinux-config の実行でメニュー画面が表示されたら **\<Save\>** を実行してから終了して下さい。

```bash
mkdir -p $WORK_DIR
cd $WORK_DIR
# create project
petalinux-create --type project --template zynq --name $PROJ_NAME
cd $PROJ_DIR
# configure H/W
petalinux-config --get-hw-description $PROJ_XSA
```

## カーネルコンフィグレーション

デフォルトのカーネルコンフィグレーションでは MCP25625 のドライバは組み込まれていませんので、これを追加します。`petalinux-config -c kernel` でカーネルコンフィグレーションを操作するのも良いのですが、実行すると結構時間が掛かります。

ここでは下記コマンドを実行して bsp.cfg で MCP25625 を有効化します。

```bash:bsp.cfg 追加
echo CONFIG_CAN_MCP251X=y >> $PROJ_DIR/project-spec/meta-user/recipes-kernel/linux/linux-xlnx/bsp.cfg
```

今回利用した petalinux では kernel/rootfs 共にデフォルトで CAN 関連の設定が有効になっているので、この設定だけで CAN が利用可能となります。

## Devicetree 修正

次は Pmod CAN を利用する為に Device Tree の修正を行います。`$PROJ_DIR/project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi` を下記内容に置き換えます。

```:system-user.dtsi
/include/ "system-conf.dtsi"

/ {
	aliases {
                spi1 = &axi_quad_spi_0;
                spi2 = &axi_quad_spi_1;
	};

	osc: can-osc {
		compatible = "fixed-clock";
		#clock-cells = <0>;
		clock-frequency  = <20000000>;
	};
};

&axi_quad_spi_0 {
	xlnx,sck-ratio = <0x10>;
	can@0x00 {
		compatible = "microchip,mcp25625";
		spi-max-frequency = <10000000>;
		clocks = <&osc>;
		interrupt-parent = <&intc>;
		interrupts = <0 31 4>;
		reg = <0>;
		tx-fifo-depth = <0x10>;
		rx-fifo-depth = <0x10>;
	};
};

&axi_quad_spi_1 {
	xlnx,sck-ratio = <0x10>;
	can@0x00 {
		compatible = "microchip,mcp25625";
		spi-max-frequency = <10000000>;
		clocks = <&osc>;
		interrupt-parent = <&intc>;
		interrupts = <0 32 4>;
		reg = <0>;
		tx-fifo-depth = <0x10>;
		rx-fifo-depth = <0x10>;
	};
};
```
 
can-osc は 20MHz の固定クロックとして定義しています。[Pmod CAN の Schematic](https://digilent.com/reference/_media/reference/pmod/pmodcan/pmodcan_sch.pdf) を見ると MPC25625 にクロック源として [CSTCE20M0V13L99-R0](https://www.murata.com/ja-jp/products/productdetail?cate=cgsubResonators&partno=CSTCE20M0V13L99-R0) が接続されており、この出力である 20MHz が can-osc の定義値となっています。

意外な所で苦労したのが aliases のオーバーライドでした。私の環境では下記の様に axi_quad_spi_1, axi_quad_spi_0 の順で system-top.dts に定義されていました。

>aliases {
>&nbsp;&nbsp;&nbsp;&nbsp;ethernet0 = &gem0;
>&nbsp;&nbsp;&nbsp;&nbsp;serial0 = &uart1;
>&nbsp;&nbsp;&nbsp;&nbsp;spi0 = &qspi;
>&nbsp;&nbsp;&nbsp;&nbsp;spi1 = &axi_quad_spi_1;
>&nbsp;&nbsp;&nbsp;&nbsp;spi2 = &axi_quad_spi_0;
>};

どうやら aliases の定義順で CAN I/F の初期化が行われるらしく、元の定義だと axi_quad_spi_1, axi_quad_spi_0 の順で can0, can1 と Network I/F 名が付与されます。

FPGA の実装とソフトウエアで番号付けが異なるとトラブルの原因になるので aliases の spi1, spi2 を再定義しています。（実際に自分は混乱した）

## ビルド

最後に petalinux のビルドと BOOT.BIN の作成を実行します。

```bash
# build images
petalinux-build
# create BOOT.BIN
petalinux-package --boot --fsbl images/linux/zynq_fsbl.elf --fpga images/linux/system.bit --u-boot --force
```

# Linux による CAN 通信の確認

## CAN デバイスの接続

Pmod CAN ２つを下記手順で Zybo に接続します。
* 各 Pmod CAN のヘッダピン JP1 を短絡
* Pmod CAN の CANH 同士を接続
* Pmod CAN の CANL 同士を接続
* Pmod CAN を Zybo Z7-20 の JB, JD に接続

以下が接続イメージになります。

![PmodCAN接続.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3332407/9c9e7ffd-fb4e-22e8-cc49-2e3a608ecfe8.png)

## 起動

`$PROJ_DIR/images/linux/` 配下に作成された下記ファイルを microSD カードの FAT パーティションに書き込んで起動します。

* BOOT.BIN
* boot.scr
* image.ub

## CAN I/F の確認

Linux が起動したらユーザー名 petalinux でログインして`ip a`を実行して下さい。無事 CAN デバイスが認識されていれば以下の様に can0, can1 が表示されます。ボード上の LED LD0 もグリーンに点灯します。

```bash
zybo-dual-pmodcan:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: can0: <NOARP,ECHO> mtu 16 qdisc noop state DOWN group default qlen 10
    link/can
3: can1: <NOARP,ECHO> mtu 16 qdisc noop state DOWN group default qlen 10
    link/can
4: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000
    link/ether 00:0a:35:00:1e:53 brd ff:ff:ff:ff:ff:ff
5: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/sit 0.0.0.0 brd 0.0.0.0
```

## CAN デバイス初期化

実際の通信前に通信時のビットレートを設定します。ここでは 1Mbps に設定しています。**コマンドは root で実行して下さい。**

```bash
ip link set can0 type can bitrate 1000000
ip link set can0 up
ip link set can1 type can bitrate 1000000
ip link set can1 up
```

## 通信の確認

通信の確認は `cansend` と `candump` で行います。シリアルコンソールのみで実行する場合、最初に `candump` をバックグランドで実行してから `cansend` でデータを送信します。

以下は実行イメージです。

```
zybo-dual-pmodcan:~$ candump can1&
[1] 664
zybo-dual-pmodcan:~$ cansend can0 123#3456ab
  can1  123   [3]  34 56 AB
zybo-dual-pmodcan:~$ cansend can0 222#deadbeef
  can1  222   [4]  DE AD BE EF
```

SSH でログインして別々のコンソールから `cansend` `candump` を実行した方が解り易いかも知れません。

とりあえず**通信は出来ているみたい**です。

# 最後に言い訳

通信の確認で「通信は出来ているみたい」と記述したのは理由があります。

今回試したのは、同一ボード上に実装した CAN デバイス同士の通信であり、各デバイスを制御する SPI は同じクロックで動作しています。この様な場合、双方が同じ設定であれば**設定が間違っていても動作してしまう** 事があります。

厳密には、外部デバイスと通信してみないと正常に動作しているとは言えないです。

通信を試せる CAN デバイスを持ち合わせていなかったので、この点は確認出来ていません。