詳解全志 V85x E907 RISC-V小核的開發與使用方式

語言: CN / TW / HK

本文整理自:https://www.gloomyghost.com/live/20230215.aspx

v85x 平臺包括了 V853, V853s, V851s, V851se s字尾代表晶片內封了DDR記憶體,e字尾代表晶片內封 ephy。擁有 Cortex-A7 core@900MHz, RISC-V@600MHz 和一個 0.5TOPS(VIP9000PICO_PID0XEE, 567MACS, 576 x 348M x 2 ≈ 500GOPS) 的 NPU。其中的 RISC-V 小核心為 平頭哥玄鐵E907

E907 平臺

玄鐵E907 是一款完全可綜合的高階 MCU 處理器。它相容 RV32IMAC 指令集,提供可觀的整型效能提升以及高能效的浮點效能。E907 的主要特性包括:單雙精度浮點單元,以及快速中斷響應。

934bf475-964e-4113-b4ac-9a19c4bc8783-圖片.png

在V85x平臺中使用的E907為RV32IMAC,不包括 P 指令集。

V85x 平臺框圖

V851s

73aadf95-c5e3-48af-8e4b-7fe8c0b22391-圖片.png

晶片架構圖

4abb0717-8f54-40c3-a7fb-fd876d4d9afd-圖片.png

相關記憶體分佈

0deca913-2fca-415f-bd7b-1d9e9eb71d82-圖片.png

034997ed-c9bb-489d-a5c4-0ffabdba6610-圖片.png

E907 子系統框圖

d457b549-0491-4a17-8f3f-69b90b088ccf-圖片.png

具體的暫存器配置項這裡就不過多介紹了,具體可以參考資料手冊《V851S&V851SE_Datasheet_V1.0.pdf

V853 的異構系統通訊在硬體上使用的是 MSGBOX,在軟體層面上使用的是 AMP 與 RPMsg 通訊協議。其中 A7 上基於 Linux 標準的 RPMsg 驅動框架,E907基於 OpenAMP 異構通訊框架。

AMP 與 RPMSG

V853 所帶有的 A7 主核心與 E907 輔助核心是完全不同的兩個核心,為了最大限度的發揮他們的效能,協同完成某一任務,所以在不同的核心上面執行的系統也各不相同。這些不同架構的核心以及他們上面所執行的軟體組合在一起,就成了 AMP 系統 (Asymmetric Multiprocessing System, 異構多處理系統)。

由於兩個核心存在的目的是協同的處理,因此在異構多處理系統中往往會形成 Master - Remote 結構。主核心啟動後啟動從核心。當兩個核心上的系統都啟動完成後,他們之間就通過 IPC(Inter Processor Communication)方式進行通訊,而 RPMsg 就是 IPC 中的一種。

在AMP系統中,兩個核心通過共享記憶體的方式進行通訊。兩個核心通過 AMP 中斷來傳遞訊息。記憶體的管理由主核負責。

1c17ca43-f978-418c-b415-d9fcf203c195-圖片.png

軟體適配

這部分使用BSP開發包即可,配置裝置樹如下:

reserved-memory {                               // 配置預留記憶體區間
	e907_dram: riscv_memserve {                 // riscv 核心使用的記憶體
		reg = <0x0 0x43c00000 0x0 0x00400000>;  // 起始地址 0x43c00000 長度 4MB
		no-map;
	};

	vdev0buffer: vdev0buffer@0x43000000 {       // vdev裝置buffer預留記憶體
		compatible = "shared-dma-pool";
		reg = <0x0 0x43000000 0x0 0x40000>;
		no-map;
	};

	vdev0vring0: vdev0vring0@0x43040000 {       // 通訊使用的vring裝置0
		reg = <0x0 0x43040000 0x0 0x20000>;
		no-map;
	};

	vdev0vring1: vdev0vring1@0x43060000 {       // 通訊使用的vring裝置1
		reg = <0x0 0x43060000 0x0 0x20000>;
		no-map;
	};
};

e907_rproc: e907_rproc@0 {                      // rproc相關配置
	compatible = "allwinner,sun8iw21p1-e907-rproc";
	clock-frequency = <600000000>;
	memory-region = <&e907_dram>, <&vdev0buffer>,
				<&vdev0vring0>, <&vdev0vring1>;

	mboxes = <&msgbox 0>;
	mbox-names = "mbox-chan";
	iommus = <&mmu_aw 5 1>;

	memory-mappings =
			/* DA 	         len         PA */
			/* DDR for e907  */
			< 0x43c00000 0x00400000 0x43c00000 >;
	core-name = "sun8iw21p1-e907";
	firmware-name = "melis-elf";
	status = "okay";
};

rpbuf_controller0: rpbuf_controller@0 {        // rpbuf配置
	compatible = "allwinner,rpbuf-controller";
	remoteproc = <&e907_rproc>;
	ctrl_id = <0>;	/* index of /dev/rpbuf_ctrl */
	iommus = <&mmu_aw 5 1>;
	status = "okay";
};

rpbuf_sample: rpbuf_sample@0 {
	compatible = "allwinner,rpbuf-sample";
	rpbuf = <&rpbuf_controller0>;
	status = "okay";
};

msgbox: msgbox@3003000 {                       // msgbox配置
	compatible = "allwinner,sunxi-msgbox";
	#mbox-cells = <1>;
	reg = <0x0 0x03003000 0x0 0x1000>,
		<0x0 0x06020000 0x0 0x1000>;
	interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>,
				<GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clk_msgbox0>;
	clock-names = "msgbox0";
	local_id = <0>;
	status = "okay";
};

e907_standby: e907_standby@0 {
	compatible = "allwinner,sunxi-e907-standby";

	firmware = "riscv.fex";
	mboxes = <&msgbox 1>;
	mbox-names = "mbox-chan";
	power-domains = <&pd V853_PD_E907>;
	status = "okay";
};

記憶體劃分

在裝置樹配置小核心使用的記憶體,包括小核自己使用的記憶體,裝置通訊記憶體,迴環記憶體等等,這裡E907 執行在 DRAM 內。記憶體起始地址可以在資料手冊查到。

d4ade8f6-b9ca-4c6e-9797-a8738a9047a5-圖片.png

通常來說我們把記憶體地址設定到末尾,例如這裡使用的 V851s,擁有 64MByte 記憶體,則記憶體範圍為 0x40000000 - 0x44000000,這裡配置到 0x43c00000 即可。對於 V853s 擁有 128M 記憶體則可以設定到 0x47C00000,以此類推。對於交換區記憶體則可以配置在附近。

reserved-memory {                               // 配置預留記憶體區間
	e907_dram: riscv_memserve {                 // riscv 核心使用的記憶體
		reg = <0x0 0x43c00000 0x0 0x00400000>;  // 起始地址 0x43c00000 長度 4MB
		no-map;
	};

	vdev0buffer: vdev0buffer@0x43000000 {       // vdev裝置buffer預留記憶體
		compatible = "shared-dma-pool";
		reg = <0x0 0x43000000 0x0 0x40000>;
		no-map;
	};

	vdev0vring0: vdev0vring0@0x43040000 {       // 通訊使用的vring裝置0
		reg = <0x0 0x43040000 0x0 0x20000>;
		no-map;
	};

	vdev0vring1: vdev0vring1@0x43060000 {       // 通訊使用的vring裝置1
		reg = <0x0 0x43060000 0x0 0x20000>;
		no-map;
	};
};

然後需要配置下 e907 的連結指令碼,找到 e907_rtos/rtos/source/projects/v851-e907-lizard/kernel.lds  ORIGIN 配置為上面預留的記憶體。

MEMORY
{
   /*DRAM_KERNEL: 4M */
   DRAM_SEG_KRN (rwx) : ORIGIN = 0x43c00000, LENGTH = 0x00400000
}

然後配置小核的 defconfig 位於 e907_rtos/rtos/source/projects/v851-e907-lizard/configs/defconfig 配置與其對應即可。

CONFIG_DRAM_PHYBASE=0x43c00000
CONFIG_DRAM_VIRTBASE=0x43c00000
CONFIG_DRAM_SIZE=0x0400000

配置啟動小核

配置啟動小核的流程如下,這裡只討論使用 linux 啟動小核的情況,不討論快啟相關。

b3d48344-f6d4-41a2-8e3b-aaf91eb8aef8-圖片.png

  1. 載入韌體
    1. 呼叫 firmware 介面獲取檔案系統中的韌體
    2. 解析韌體的 resource_table 段,該段有如下內容
      1. 宣告需要的記憶體(Linux 為其分配,裝置樹配置)
      2. 宣告使用的 vdev(固定為一個)
      3. 宣告使用的 vring(固定為兩個)
    3. 將韌體載入到指定地址
  2. 註冊 rpmsg virtio 裝置
    1. 提供 vdev->ops(基於 virtio 介面實現的)
    2.  rpmsg_bus 驅動匹配,完成 rpmsg 初始化
  3. 啟動小核
    1. 呼叫 rproc->ops->start

1. 載入韌體

驅動位於 kernel/linux-4.9/drivers/remoteproc/sunxi_rproc_firmware.c

首先呼叫 sunxi_request_firmware 函式

int sunxi_request_firmware(const struct firmware **fw, const char *name, struct device *dev)
{
	int ret, index;
	struct firmware *fw_p = NULL;
	u32 img_addr, img_len;

	ret = sunxi_find_firmware_storage();
	if (ret < 0) {
		dev_warn(dev, "Can't finded boot_package head\n");
		return -ENODEV;
	}

	index = ret;

	ret = sunxi_firmware_get_info(dev, index, name, &img_addr, &img_len);
	if (ret < 0) {
		dev_warn(dev, "failed to read boot_package item\n");
		ret = -EFAULT;
		goto out;
	}

	ret = sunxi_firmware_get_data(dev, index, img_addr, img_len, &fw_p);
	if (ret < 0) {
		dev_err(dev, "failed to read Firmware\n");
		ret = -ENOMEM;
		goto out;
	}

	*fw = fw_p;
out:
	return ret;
}

驅動會從韌體的特定位置讀取,使用函式 sunxi_find_firmware_storage,這裡會去固定的位置查詢韌體,位置包括 lib/firmware/dev/mtd0. /dev/mtd1, /dev/mmcblk0 等位置。對於Linux啟動我們只需要放置於 lib/firmware 即可。

static int sunxi_find_firmware_storage(void)
{
	struct firmware_head_info *head;
	int i, len, ret;
	loff_t pos;
	const char *path;
	u32 flag;

	len = sizeof(*head);
	head = kmalloc(len, GFP_KERNEL);
	if (!head)
		return -ENOMEM;

	ret = sunxi_get_storage_type();

	for (i = 0; i < ARRAY_SIZE(firmware_storages); i++) {
		path = firmware_storages[i].path;
		pos = firmware_storages[i].head_off;
		flag = firmware_storages[i].flag;

		if (flag != ret)
			continue;

		pr_debug("try to open %s\n", path);

		ret = sunxi_firmware_read(path, head, len, &pos, flag);
		if (ret < 0)
			pr_err("open %s failed,ret=%d\n", path, ret);

		if (ret != len)
			continue;

		if (head->magic == FIRMWARE_MAGIC) {
			kfree(head);
			return i;
		}
	}

	kfree(head);

	return -ENODEV;
}

2. 配置時鐘

配置clk與小核的 boot 選項,驅動位於kernel/linux-4.9/drivers/remoteproc/sunxi_rproc_boot.c 可以自行參考

struct sunxi_core *sunxi_remote_core_find(const char *name);

int sunxi_core_init(struct sunxi_core *core);

void sunxi_core_deinit(struct sunxi_core *core);

int sunxi_core_start(struct sunxi_core *core);

int sunxi_core_is_start(struct sunxi_core *core);

int sunxi_core_stop(struct sunxi_core *core);

void sunxi_core_set_start_addr(struct sunxi_core *core, u32 addr);

void sunxi_core_set_freq(struct sunxi_core *core, u32 freq);

使用 DEBUGFS 載入韌體

由於已經對外註冊了介面,這裡只需要使用命令即可啟動小核心。假設小核的elf名字叫e907.elf 並且已經放置進 lib/firmware 資料夾

echo e907.elf > /sys/kernel/debug/remoteproc/remoteproc0/firmware
echo start > /sys/kernel/debug/remoteproc/remoteproc0/state

E907 小核開發

這裡提供了一個 RTOS 以供開發使用,此 RTOS 基於 RTT 核心。地址 https://github.com/YuzukiHD/Yuzukilizard/tree/master/Software/BSP/e907_rtos

同時,docker 映象內也已包含此開發包,可以直接使用。

搭建開發環境

使用 DOCKER

直接拉取 gloomyghost/yuzukilizard 即可

 docker pull gloomyghost/yuzukilizard

d149f7d8-56be-4d35-b90c-9d0ed5b87216-圖片.png

獨立搭建開發環境

使用 git 命令下載(不可以直接到 Github 下載 zip,會破壞超連結與檔案屬性)

git clone --depth=1 https://github.com/YuzukiHD/Yuzukilizard.git

88163ab7-2efc-449c-9d51-dfd884b61537-圖片.png

然後複製到當前目錄下

 cp -rf Yuzukilizard/Software/BSP/e907_rtos/ . && cd e907_rtos

下載編譯工具鏈到指定目錄

cd rtos/tools/xcompiler/on_linux/compiler/ && wget https://github.com/YuzukiHD/Yuzukilizard/releases/download/Compiler.0.0.1/riscv64-elf-x86_64-20201104.tar.gz && cd -

b14dedd1-b35a-4641-ba88-f3dde82a8f96-圖片.png

編譯第一個 elf 系統

進入 rtos/source 資料夾

cd rtos/source/

c5383bb2-cc52-4bdf-bd00-e9418c6fe799-圖片.png

應用環境變數並載入方案

source melis-env.sh;lunch

8dec3054-1b72-43ac-8394-97a67503087c-圖片.png

然後直接編譯即可,他會自動解壓配置工具鏈。編譯完成後可以在 ekernel/melis30.elf 找到韌體。

make -j

b30c9554-4528-4442-8b24-02e3dfe00ac0-圖片.png

配置小核系統

小核的編譯框架與 kernel 類似,使用 kconfig 作為配置項。使用 make menuconfig 進入配置頁。

e022d40f-a2e1-4886-a7d7-8b272cb44e8a-圖片.png

其餘使用與標準 menuconfig 相同這裡不過多贅述。

小核使用

小核使用 UART 輸出 console

首先配置小核的 PINMUX 編輯檔案 e907_rtos/rtos/source/projects/v851-e907-lizard/configs/sys_config.fex 這裡使用 UART3 , 引腳為PE12, PE13 , mux 為 7

[uart3]
uart_tx         = port:PE12<7><1><default><default>
uart_rx         = port:PE13<7><1><default><default>

然後配置使用 uart3 作為輸出,執行 make menuconfig 居進入配置

 Kernel Setup  --->
 	Drivers Setup  --->
 		Melis Source Support  --->
 			[*] Support Serial Driver
 		SoC HAL Drivers  --->
 			Common Option  --->
 				[*] enable sysconfig                // 啟用讀取解析 sys_config.fex 功能
 			UART Devices  --->
 				[*] enable uart driver              // 啟用驅動
 				[*]   support uart3 device          // 使用 uart3
 				(3)   cli uart port number          // cli 配置到 uart3
 Subsystem support  --->
 	devicetree support  --->
 		[*] support traditional fex configuration method parser. // 啟用 sys_config.fex 解析器

 linux 中配置裝置樹,將裝置樹配置相應的引腳與 mux

f078de4c-3773-4bf2-b6e7-28c1ada4bde7-圖片.png

如果裝置樹不做配置引腳和 mux,kernel會很貼心的幫你把沒使用的 Pin 設定 io_disable 。由於使用的是 iommu 操作 UART 裝置,會導致 io 不可使用。如下所示。

22eaa050-073c-468d-a7d8-6d9a7bad95cb-圖片.png

486ea195-9f2d-48d0-bb0d-47c13385f25f-圖片.png

此外,還需要將 uart3 的節點配置 disable,否則 kernel 會優先佔用此裝置。

&uart3 {
        pinctrl-names = "default", "sleep";
        pinctrl-0 = <&uart3_pins_active>;
        pinctrl-1 = <&uart3_pins_sleep>;
        status = "disabled";
};

如果配置 okay 會出現以下提示。

uart: create mailbox fail
uart: irq for uart3 already enabled
uart: create mailbox fail

啟動小核韌體後就可以看到輸出了

ad0747ea-fff4-4216-83f2-362355f519cf-圖片.png

核心通訊

建立通訊節點

啟動小核後,使用 eptdev_bind test 2 建立兩個通訊節點的監聽,可以用 rpmsg_list_listen 命令檢視監聽節點。

831ae5a8-e15c-43bd-ab28-e1d33c24d5ef-圖片.png

然後在 Linux 內建立通訊節點,由於我們上面啟用了兩個監聽所以這裡也開兩個節點

echo test > /sys/class/rpmsg/rpmsg_ctrl0/open
echo test > /sys/class/rpmsg/rpmsg_ctrl0/open

8c1a29ef-b2e9-48a6-a6fd-7142756b0d83-圖片.png

然後就可以在 /dev/ 下看到通訊節點 /dev/rpmsg0/dev/rpmsg1

a9761355-b5fd-4dfb-9568-7a57f0105044-圖片.png

也可以在小核控制檯看到節點的建立

6ee569c7-18f5-4b10-a8ee-3a8c97154f72-圖片.png

核心通訊

Linux -> e907

可以直接操作 Linux 端的節點,使用 echo 寫入資料

echo "Linux Message 0" > /dev/rpmsg0
echo "Linux Message 0" > /dev/rpmsg1

c6c6d0e6-9dd5-4681-afff-3dee914f5429-圖片.png

小核即可收到資料

5143299e-ccf3-4ca0-a255-58bd611ea1d9-圖片.png

e907 -> Linux

使用命令 eptdev_send 用法 eptdev_send <id> <data>

eptdev_send 0 "E907 Message"
eptdev_send 1 "E907 Message"

3d7ae909-6756-496f-a5d5-1aee7f07065a-圖片.png

在 Linux 側直接可以讀取出來

cat /dev/rpmsg0
cat /dev/rpmsg1

c82a5efc-b528-40be-838e-1172539d0946-圖片.png

可以一直監聽,例如多次傳送資料

81c4b823-9c4b-49ec-b897-f6da88215871-圖片.png

Linux 側獲得的資料也會增加

e8822c02-75d2-4925-8632-d936e7f8a2c8-圖片.png

關閉通訊

Linux 側關閉,操作控制節點,echo <id> 給節點即可

echo 0 > /sys/class/rpmsg/rpmsg_ctrl0/close
echo 1 > /sys/class/rpmsg/rpmsg_ctrl0/close

af4724c5-be15-4755-b8d9-0a810b5126b5-圖片.png

同時 E907 也會列印連結關閉

df78c714-00bf-4f10-9946-83c082797ab0-圖片.png

rpmsg 需知

  1. 端點是 rpmsg 通訊的基礎;每個端點都有自己的 src  dst 地址,範圍(1 - 1023,除了
    0x35
  2. rpmsg 每次傳送資料最大為512 -16 位元組;(資料塊大小為 512,頭部佔用 16 位元組)
  3. rpmsg 使用 name server 機制,當 E907 建立的端點名,和 linux 註冊的 rpmsg 驅動名一
    樣的時候,rpmsg bus 匯流排會呼叫其 probe 介面。所以如果需要 Linux 端主動發起建立端
    點並通知 e907,則需要藉助上面提到的 rpmsg_ctrl 驅動。
  4. rpmsg 是序列呼叫回撥的,故建議 rpmsg_driver 的回撥中不要呼叫耗時長的函式,避免影
    響其他 rpmsg 驅動的執行

自定義小核 APP

小核的程式入口位於 e907_rtos/rtos/source/projects/v851-e907-lizard/src/main.c

#include <stdio.h>
#include <openamp/sunxi_helper/openamp.h>

int app_entry(void *param)
{
    return 0;
}

可以自定義小核所執行的程式。

自定義小核命令

SDK 提供了 FINSH_FUNCTION_EXPORT_ALIAS 繫結方法,具體為

FINSH_FUNCTION_EXPORT_ALIAS(<函式名稱>, <命令>, <命令的描述>)

例如編寫一個 hello 命令,功能是輸出 Hello World,描述為 Show Hello World

int hello_cmd(int argc, const char **argv)
{
    printf("Hello World\n");
}
FINSH_FUNCTION_EXPORT_ALIAS(hello_cmd, hello, Show Hello World)

即可在小核找到命令與輸出。

07adfaef-d366-4b3e-939d-e0f214832e23-圖片.png