lynxeyedの電音鍵盤

MBDとFPGAと車載で使うデバイスの備忘録

小規模マイコン+FPGA、QuickLogic EOS S3の試食

小さめなマイコン+グルーロジックの代表格はPSoC5 LPなどが挙げられます。回路規模でその上になるとXilinxであればZynq、IntelであればCyclone V SoCのような比較的中規模デバイスでしょう。

www.quicklogic.com
昨年の11月に購入して積んでいました。
f:id:Lynx-EyED:20201106100600j:plain
必要に駆られて使ってみることに。
f:id:Lynx-EyED:20201106100719j:plain
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)はオンプレのコンパイラ専用機として使用するので問題ありません。開発は手元のmacVS 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_helloworldhwqf_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入力を除くほぼ全てのピンに割り当て可能なはず(執筆時点で解決できていない部分があります。後述する「コード作成」セクションの「ピンへの反映」をご覧ください)です。
下図は、それぞれのパッケージにおける、個々のピンにアサイン可能な特定機能一覧の抜粋です。
f:id:Lynx-EyED:20210805130637p:plain
FBIOというのはFPGA Fabric IOのことです。アサイン可能なピンの詳細はEOS-S3データシートのp.99~p.102をご覧ください。


https://www.quicklogic.com/wp-content/uploads/2020/12/QL-EOS-S3-Ultra-Low-Power-multicore-MCU-Datasheet-2020.pdf


どのピンに何を割り当てたかはテーブルでソースコードに記述します。
サンプルプロジェクトの場合、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番地でしょう。
f:id:Lynx-EyED:20210805171116j:plain

上記アドレスのうち特にFPGAへインターフェースするメモリ領域は0x4002_0000~の128kB分です。
この番地はAHB Slave-Wishbone Masterブリッジが接続されている領域です。
f:id:Lynx-EyED:20210805171809j:plain
詳細は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-シリアルを接続する必要があります。

動作風景

youtu.be

プロジェクト

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パッケージの基板を製作したので、後日記事にできればと思います。

おわりに

すごく使いやすいわけではありませんが、必要十分なデバイスと思います。
FPGAもLVDSやDDRなどがあるわけではありませんが、最大100MHz動作で、80MHz動作のCortex-M4Fのサブシステムと協業するのには十分な性能だと思います。

小さい、安価であるところと、メモリが豊富なところをうまく使えないか考えているところです。


*1:intel FPGAのnios2開発環境が2021年6月時点でwsl1しか対応していないため