小規模マイコン+FPGA、QuickLogic EOS S3の試食
小さめなマイコン+グルーロジックの代表格はPSoC5 LPなどが挙げられます。回路規模でその上になるとXilinxであればZynq、IntelであればCyclone V SoCのような比較的中規模デバイスでしょう。
www.quicklogic.com
昨年の11月に購入して積んでいました。
必要に駆られて使ってみることに。
Quicklogic EOS-S3はこの両者の中間のデバイスと言えるかもしれません。このデバイスは80MHzで動作するCortex-M4Fを軸に小規模DSPとeFPGAを搭載した安価なデバイスです。少量購入でも単価1000円くらい。FPGA側はCPU(バスマスタ)にぶら下がるメモリマップドスレーブとして開発できます。(Wishboneバス)
Vivado IPインテグレータやPlatform DesignerみたいなGUIはありません。GUIのアシストが必要になるレベルの大規模なものを作る前提ではないのでしょう。いずれにせよ、今回のようなデバイスは使い捨てができる回路規模であり、そういう前提で使うのが吉です。末長く使う物でもないので、これを大黒柱にした開発は避けるべきでしょう。
今回の記事は以下のような構成です。前半は開発環境の整備。中盤はプログラミングに必要なGPIOやFPGA内部とのアクセスに必要なメモリマップの情報です。また後半ではオリジナルコードを記述しながら、FPGAのプログラミング時に必要なピンアサイン情報の設定方法も取り上げます。
開発環境の整備
(※Dockerを使用する場合の記事を書きました。私のように開発環境が多く、使用後はイメージごと使い捨てるスタイルの方はこちらの方が便利だと思います)
Quicklogicの提供するFPGA開発環境(ql_symbiflow)が、x86_64 Linuxを前提にしています。他はarm-none-eabi-gccおよびPython3が動作すればアーキテクチャはなんでも動くのですが、ここでCPUのアーキテクチャが固定されてしまいます。SymbiFlow本体にはそのような制約はないようですし、そのうち改善されるといいですね。
今回はWSL1を使いました。WSL1/2はこのブログ執筆時点でUSBが動作しませんが、私の場合はWindows(WSL)はオンプレのコンパイラ専用機として使用するので問題ありません。開発は手元のmacでVS Code Remote Developmentで行い、EOS-S3への書き込みはラズパイから実行します。
WSL1はubuntu-20.04を使用しています。諸事情*1でWSL2からダウングレードしています。
QORC-SDK導入
以下のgithubからcloneしてきます。
github.com
$ git clone -b v1.10.0 https://github.com/QuickLogic-Corp/qorc-sdk.git $ cd qorc-sdk/
コンパイラ導入
諸々のコンパイラ環境を一気に導入します。
$ source envsetup.sh
これだけです。
ここでanacondaをいきなりインストールされて、環境構築が始まります。私の環境ではタイムアウトに起因するエラーが何回か出ました。根気よく何回かトライする必要があるかも。
インストール時のエラーについて
- apioに関するエラー:WSLはUSB(シリアル)が動かないのでエラーで構わない。
- fasmに関するエラー:論理合成後のバイナリストリームをC言語ヘッダファイルに変換する部分がエラーを起こしています。再インストールが必要かもしれません。
加えて、gcc-arm-none-eabiはQORC-SDKが提供するバージョンを使うのが良いと思われます。バージョンによってはサンプルプログラムをビルドした際にRAM領域がオーバーフローすることがあります。
githubのREADMEをみるといろいろPATHを通すように指示がありますが、私の場合はログイン後に毎回source envsetup.sh
を走らせています。ログインと同時にPATHを通されてしまうとQORC-SDKのconda環境が別に構築したpip環境や、既存のconda環境を破壊することがあるので嫌だっただけです…pyenvを真面目に設定すればいいのでしょうが。
サンプルプロジェクトのお試しビルド
前節までで導入した環境の動作確認のためサンプルプロジェクトをビルドしてみます。ARMコンパイラとFPGA論理合成ソフトウェアql_symbiflowの両方の動作確認ができるサンプルコードを選択します。 qf_apps/qf_helloworldhw
やqf_apps/qf_advancedfpga
といったサンプルプロジェクトが最適と思われます。
例:qf_helloworldhwのサンプルをビルド
cd qf_apps/qf_helloworldhw make clean -C GCC_Project make -C GCC_Project
clean後のビルドはCコンパイルの他、論理合成も走るので5分くらい気長に待ちます。
(中略) make[1]: Entering directory '/mnt/c/qorc-sdk/qf_apps/qf_helloworldhw/GCC_Project' Linking ... Convert ELF to BIN Create text symbol table. make[1]: Leaving directory '/mnt/c/qorc-sdk/qf_apps/qf_helloworldhw/GCC_Project' Copy output files ... make: Leaving directory '/mnt/c/qorc-sdk/qf_apps/qf_helloworldhw/GCC_Project'
こんな感じで終了していれば成功。
もし
ERROR: 'AL4S3B_FPGA_top_bit.h' not found.
のようなエラーが出ている場合、ql_symbiflowのインストールに失敗しています。前節で実行したsource envsetup.sh
の際のエラーを確認することをお勧めします。
おまけ:ラズパイでの書き込み
Raspbianでもubuntuでも同様の手法です。下記のREADMEの通りに導入します。
github.com
$ git clone --recursive https://github.com/QuickLogic-Corp/TinyFPGA-Programmer-Application.git $ pip3 install tinyfpgab
毎回コマンド打つのは面倒ですので、スクリプトを書きます。書き込みするbinファイルは大抵同じファイル名だと思いますので、下記のようにしています。ttyポート名は各自の環境に合わせてください。
#!/bin/bash #qfprog --port /dev/ttyACM0 --bootloader *.bin --mode fpga-m4 BINFILE=qf_advancedfpga.bin qfprog="python3 /(your-installed-directory)/TinyFPGA-Programmer-Application/tinyfpga-programmer-gui.py" $(qfprog) --port /dev/ttyACM0 --m4app $(BINFILE) --mode fpga-m4
上記テキストを保存し、実行権限を付与すればOK.
GPIOとピンアサイン
閑話休題。フレキシブルなマイコンのイメージが先行し、I2CやSPI、UARTなどの特定機能は任意のピンに割り当てられそうに思いますが、残念ながらEOS-S3はあまり自由度はありません。Cortex-M4FやSensor Manager側に接続されている機能はピンアサインがほぼ固定されています。FPGAからの入出力であれば電源、クロック、ADC入力を除くほぼ全てのピンに割り当て可能なはず(執筆時点で解決できていない部分があります。後述する「コード作成」セクションの「ピンへの反映」をご覧ください)です。
下図は、それぞれのパッケージにおける、個々のピンにアサイン可能な特定機能一覧の抜粋です。
FBIOというのはFPGA Fabric IOのことです。アサイン可能なピンの詳細はEOS-S3データシートのp.99~p.102をご覧ください。
どのピンに何を割り当てたかはテーブルでソースコードに記述します。
サンプルプロジェクトの場合、src/pincfg_table.c
に記述されています。
PadConfig pincfg_table[] = { { // setup UART TX .ucPin = PAD_44, .ucFunc = PAD44_FUNC_SEL_UART_TXD, .ucCtrl = PAD_CTRL_SRC_A0, .ucMode = PAD_MODE_OUTPUT_EN, .ucPull = PAD_NOPULL, .ucDrv = PAD_DRV_STRENGHT_4MA, .ucSpeed = PAD_SLEW_RATE_SLOW, .ucSmtTrg = PAD_SMT_TRIG_DIS, }, { // setup UART RX .ucPin = PAD_45, // Options: 14, 16, 25, or 45 .ucFunc = PAD45_FUNC_SEL_UART_RXD, .ucCtrl = PAD_CTRL_SRC_A0, .ucMode = PAD_MODE_INPUT_EN, .ucPull = PAD_NOPULL, }, { // Pad 17 -- clock out from FPGA .ucPin = PAD_17, .ucFunc = PAD17_FUNC_SEL_FBIO_17, .ucCtrl = PAD_CTRL_SRC_FPGA, .ucMode = PAD_MODE_OUTPUT_EN, .ucPull = PAD_NOPULL, .ucDrv = PAD_DRV_STRENGHT_4MA, .ucSpeed = PAD_SLEW_RATE_FAST, .ucSmtTrg = PAD_SMT_TRIG_EN, }, //(中略)..... }
ピンアサインはパッケージのピン番号ではなく信号名(PAD_17 etc..)で記述します。
なお、FPGAの入出力の場合、Verilog側で記述した信号名と対応させる必要があるため、さらに*.pcfファイルを記述する必要があります。詳細はコード作成のセクションで扱います。
メモリマップについて
EOS-S3は様々なサブシステムが接続されており、メモリマップを網羅するのは大変です。この記事では今回使う範囲のメモリマップに限定します。
メモリマップ詳細はTRMのp.38~p.43をご覧ください。
https://www.quicklogic.com/wp-content/uploads/2020/06/QL-S3-Technical-Reference-Manual.pdf
プログラミングでよく使うのはPeripheralの0x4000_0000~0x5fff_ffff番地でしょう。
上記アドレスのうち特にFPGAへインターフェースするメモリ領域は0x4002_0000~の128kB分です。
この番地はAHB Slave-Wishbone Masterブリッジが接続されている領域です。
詳細はTRMのp.275~ご覧ください。また、後述する、コードの作成時にも触れますので参考になれば。
コード作成
今回、FPGA領域に簡単なGPIOポートを作成します。
Cortex-M4F側から32bitのデータを受け取り、下位4bitだけをGPIOポートに反映するという簡単な物です。一連の動作確認には必要十分だと思います。
既存のサンプルコードに直接手を加えるのはあまり賢明ではないでしょう。サンプルを雛形として新規にプロジェクトを作成することにします。すでにQORC-SDK自体にその仕組みがありますので利用しましょう。
既存プロジェクトを雛形に新規プロジェクトを作成
qf_apps
ディレクトリに戻ります。
既存プロジェクトqf_mqttsn_ai_app
を雛形にして、新しいプロジェクトqf_wbfpga_pio
を作ります
$ python create_newapp.py --source qf_mqttsn_ai_app --dest qf_wbfpga_pio
CPUからFPGAへアクセスするには
メモリマップのセクションでも取り上げた通り、AHB Slave-Wishbone Masterブリッジが接続されています。FPGA側に作成したオリジナル回路とCPU間でデータの送受信をする際には、オリジナル回路にWishbone Slaveを接続する必要があります。
これもサンプルがありますので、それを基に作ってしまいます。FPGA側の参考になるサンプルプロジェクトはqf_apps/qf_advancedfpga
です。
このプロジェクト内のfpga/rtl/AL4S3B_FPGA_Rgisters.v
を抜粋してみます。
Cortex-M4F -> FPGA
Wishboneマスタによるデータライト部分です。Cortex-M4Fからはデータ代入動作でこの部分にライトされます。
assign FB_COLORS_REG_Wr_Dcd = (WBs_ADR_i == FPGA_COLORS_ADR) & WBs_CYC_i & WBs_STB_i & WBs_WE_i & (~WBs_ACK_o); // 中略 if (FB_COLORS_REG_Wr_Dcd) begin color0 <= WBs_BYTE_STB_i[0] ? WBs_DAT_i[3:0] : color0; color1 <= WBs_BYTE_STB_i[1] ? WBs_DAT_i[10:8] : color1; color2 <= WBs_BYTE_STB_i[2] ? WBs_DAT_i[18:16] : color2; color3 <= WBs_BYTE_STB_i[3] ? WBs_DAT_i[26:24] : color3; end
FPGA -> Cortex-M4F
Wishboneマスタによるデータリード部分です。今回該当部分は0xDEADBEEF
がリードできるようにします。
always @( * ) begin case(WBs_ADR_i[ADDRWIDTH-1:0]) FPGA_REG_ID_VALUE_ADR : WBs_DAT_o <= Device_ID_o; FPGA_REV_NUM_ADR : WBs_DAT_o <= Rev_Num; FPGA_SCRATCH_REG_ADR : WBs_DAT_o <= { 16'h0, Scratch_reg }; FPGA_COLORS_ADR : WBs_DAT_o <= 32'hDEADBEEF; //{ 5'b0, color3, 5'b0, color2, 5'b0, color1, 5'b0, color0}; FPGA_DURATION0_ADR : WBs_DAT_o <= { 20'b0, duration0}; FPGA_DURATION1_ADR : WBs_DAT_o <= { 20'b0, duration1}; FPGA_DURATION2_ADR : WBs_DAT_o <= { 20'b0, duration2}; FPGA_DURATION3_ADR : WBs_DAT_o <= { 20'b0, duration3}; default : WBs_DAT_o <= AL4S3B_DEF_REG_VALUE; endcase end
Wishboneのアドレスによって、データを振り分けています。たくさんレジスタがありますが、今回の試行ではcolor0
レジスタだけを利用し、GPIOに反映するロジックを記述しようと思います。
ヘッダに記述された構造体も見てみます。fpga/inc/fpga_ledctlr.h
にあります。
便宜上2つ用意されています。この部分はqf_advancedfpga
のサンプルの便宜上の都合ですので、1つにシュリンクしても良いでしょう。
// 構造体1つ目 typedef struct fpga_ledctlr_regs { uint32_t device_id; // 0x00 uint32_t rev_num; // 0x04 uint16_t scratch_reg; // 0x08 uint16_t reserved1; // 0x0A uint32_t reserved2; // 0x0C uint8_t color0; // 0x10 uint8_t color1; // 0x11 uint8_t color2; // 0x12 uint8_t color3; // 0x13 uint32_t reserved7[3]; // 0x14 uint32_t duration0; // 0x20 uint32_t duration1; // 0x24 uint32_t duration2; // 0x28 uint32_t duration3; // 0x2C } fpga_ledctlr_regs_t; //構造体2つ目 typedef struct fpga_ledctlr_regs2 { uint32_t device_id; // 0x00 uint32_t rev_num; // 0x04 uint16_t scratch_reg; // 0x08 uint16_t reserved1; // 0x0A uint32_t reserved2; // 0x0C uint32_t colors; // 0x10 uint32_t reserved7[3]; // 0x14 uint32_t duration0; // 0x20 uint32_t duration1; // 0x24 uint32_t duration2; // 0x28 uint32_t duration3; // 0x2C } fpga_ledctlr_regs2_t;
Cortex-M4FからFPGAへデータライトするには
#define FPGA_PERIPH_BASE (0x40020000) // 中略 fpga_ledctlr_regs_t* pledctlr_regs = (fpga_ledctlr_regs_t*)(FPGA_PERIPH_BASE); pledctlr_regs->color0 = 0xXX; // 任意の数
のように変数に代入するだけでOKです。
Cortex-M4FへFPGAからデータリードするには
#define FPGA_PERIPH_BASE (0x40020000) // 中略 int32_t read_data; fpga_ledctlr_regs2_t* pledctlr_regs2 = (fpga_ledctlr_regs2_t*)(FPGA_PERIPH_BASE); read_data = pledctlr_regs2->colors; // read_dataに格納
これも簡単です。インターフェース部分が理解できてきました。
この情報をもとに、以下の方針で修正していきます。
pledctlr_regs->color0 = 任意の数値; ↓ FPGAのcolor0レジスタに反映 ↓ color0の下位4bitを{p3_o,p2_o,p1_o,p0_o}に反映 ↓ GPIOに反映
まず、qf_advancedfpga/fpga/
の部分を作成中のプロジェクトqf_wbfpga_pio
にコピーし、makefileを修正します。
makefileの修正は本質ではないものの面倒で時間が取られる部分です。修正後のプロジェクトをgithubに上げております。詳細はこのセクションの最後をご覧ください。
RTL修正部分:詳細は割愛しますが、レジスタ
color0
を4bit幅に直し、出力ポートp0
,p1
,p2
,p3
を追加するコードを追記しました。color0
,p0
,p1
,p2
,p3
といったキーワードでfpga/
ディレクトリをgrepするとわかると思います。GPIOに反映するロジックは簡単です。Wishboneクロックに同期して出力を反映しているだけです。
fpga/rtl/LED_controller.v
reg [3:0] port_l; always @(posedge rst or posedge clk) if (rst)begin port_l <= 4'h0; end else begin port_l <= color0; end assign p0 = port_l[0]; assign p1 = port_l[1]; assign p2 = port_l[2]; assign p3 = port_l[3]; endmodule
ピンへの反映
p0~p3までの4つの出力をIOに反映します。
fpga/rtl/quickfeather.pcf
を記述します。
*.pcfは
set_io 信号名 「このパッケージでのピン番号」または「IOエイリアス(IO_10など)」
という文法で記述します。
使用しているボード(QuickFeather)に搭載されているのはQFN(PU64)パッケージです。
set_io p0_o 40 set_io p1_o 42 set_io p2_o 37 set_io p3_o 28
したがって、BGA(PD64)パッケージの場合は以下のように記述します。
set_io p0_o E7 set_io p1_o D7 set_io p2_o G8 set_io p3_o H5
WCLSP(WR42)の場合は割愛します。
冒頭で述べた懸念点です。どこのFBIOでも指定できるはず、と述べたのですが、以下のピン番号は属性がIOTYPE=SDIOMUX
であり、IOに指定するとエラーが発生し論理合成を終えることができませんでした。このピンを避けた方が良いと思われます。
QFN(PU64)パッケージ: 22,21,20,18,17,15,16,11,13,14,10,9,8,7 番ピン
詳細は、Symbiflow Installation Guide and TutorialのPcfの記述方法をご覧ください。
Cソースコードの記述
FreeRTOSが移植されているので、そのまま使いました。
必要な部分のみの抜粋です。
#define FPGA_PERIPH_BASE (0x40020000) fpga_ledctlr_regs_t* pledctlr_regs = (fpga_ledctlr_regs_t*)(FPGA_PERIPH_BASE); fpga_ledctlr_regs2_t* pledctlr_regs2 = (fpga_ledctlr_regs2_t*)(FPGA_PERIPH_BASE); void vTask1(void *pvParameters); void vTask2(void *pvParameters); int main(){ xTaskCreate(vTask1,"Task1", 100, NULL, 1, NULL); xTaskCreate(vTask2,"Task2", 100, NULL, 1, NULL); vTaskStartScheduler(); while(1); } void vTask1(void *pvParameters){ while(1){ pledctlr_regs->color0 = 0x05; vTaskDelay(500); pledctlr_regs->color0 = 0x0a; vTaskDelay(500); } } void vTask2(void *pvParameters){ while(1){ vTaskDelay(1000); dbg_str("\r\n\r\nRead data from FPGA=0x"); dbg_hex32(pledctlr_regs2->colors); // データリード } }
1つ目のスレッドvTask1でFPGAのメモリ空間の指定した領域へ500msec毎に0x05、0x0Aを記述しています。
2つ目のスレッドはUARTに1秒間隔でFPGAからリードしたデータを(=0xdeadbeefになるはずです)表示しています。UARTは115200baudで、QuickFeather基板の場合J3ピンヘッダの2(TX),3(RX)にUSB-シリアルを接続する必要があります。
動作風景
プロジェクト
qorc-sdk/qf_apps
フォルダで、以下のプロジェクトをcloneしてください。
github.com
$ git clone -b v0.0.1 https://github.com/panda5mt/qf_wbfpga_pio.git
デバイスのパッケージについて(QFNは売ってない?)
2021年8月現在、Mouserで評価ボード、チップ単体を入手できます。
www.mouser.jp
BGA(PD64)、WCLSP(WR42)、QFN(PU64)の3タイプのパッケージがありますが、QFNは残念ながら量産計画はなく、評価ボードのみの供給になるようです。
EOS-S3データシートのp.98。
NOTE: The QFN pinout information is provided as reference for QuickFeather board only. The QFN package is not in mass production.
このPD64パッケージの基板を製作したので、後日記事にできればと思います。