> 此處為 [學長筆記](https://hackmd.io/@xswzaq44321/ryyFgMffyx) 內所有 command 及程式的解釋
> [改好的 source code](https://github.com/weber-lin-nthu/NCKU_Computer-Architecture_Qemu-SystemC-Co-Simulation)
# QEMU
## QEMU (`qemu-6.2.0-rc2/`)
```sh
# 在解壓縮 QEMU 後的 qemu-6.2.0-rc2,新增 build 資料夾後進去
../configure --target-list="arm-softmmu,arm-linux-user" --prefix=${HOME}/workspace/qemu-bin-6.2.0-rc2
```
* `target-list`: 只編譯指定的模擬器 target
所有 target 可在 `configs/targets` 看到
* `arm-softmmu`: 針對整個平台 (包含 CPU 與周邊硬體) 做模擬 (如 realview versatile family),執行檔為 `qemu-bin-6.2.0-rc2/qemu-system-arm`
realview versatile family 為 ARM 的一系列硬體開發平台
* `arm-linux-user`: 只做 CPU 指令集架構的轉換模擬,執行檔為 `qemu-bin-6.2.0-rc2/qemu-arm`
* `prefix`: 設定安裝在此資料夾
```sh
make -j $(nproc)
make install
```
* `make -j $(nproc)`: 編譯 QEMU 原始碼,產生 `qemu-system-arm` 跟 `qemu-arm` 執行檔等檔案。 `-j $(nproc)` 為根據 CPU core 數量做平行處理加快編譯速度
* `make install`: 把那些編譯好的檔案複製到剛剛 configure 設定的 `prefix` 資料夾
## Cross Compile Toolchain (`arm-2014.05/`)
* `arm-2014.05/bin/` 內會放 cross compile toolchain 的執行檔。例如 `arm-2014.05/bin/arm-none-linux-gnueabi-gcc`:
* `arm`: target 架構為 arm
* `none`: 表示沒有特定廠商限制,為通用
* `linux`: target OS 為 Linux
* `gnueabi`
* EABI (Embedded Application Binary Interface) 為 ARM 定義的 ABI 標準,包含:
* 函式怎麼呼叫 (calling convention)
* 資料怎麼排版(結構對齊、stack 對齊)
* 參數怎麼傳遞(register 還是 stack)
* 哪些暫存器由 caller/callee 負責保存
* 浮點參數如何處理
* GNU EABI 即為 GNU 針對 EABI 的一個實作
## Linux Kernel (`linux-4.14.85/`)
* 原本需先下 `make ARCH=arm menuconfig` 來以互動式介面修改 `linux-4.14.85/.config` 檔來設定哪些功能、驅動程式、架構支援會被編譯進 kernel,此 Lab 直接使用 RealView EB 最簡單的設定檔複製到資料夾內
RealView EB (Emulation Baseboard) 為 ARM 官方的硬體開發板,用來模擬尚未量產的 ARM SoC
* `linux-4.14.85/.config` 範例
* `CONFIG_ARM=y`: 表示為 ARM 架構編譯的 kernel
* `CONFIG_MIGHT_HAVE_PCI=y`: kernel 可能支援 PCI 裝置
* `CONFIG_STACKTRACE_SUPPORT=y`: 啟用 stack 追蹤功能 (用來 debug crash)
* `CONFIG_PGTABLE_LEVELS=2`: 使用 2 階段 page table (ARMv5 架構常見)
```sh
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- zImage
```
* cross compile linux kernel 給 ARM 架構使用,產生可開機的 kernel 壓縮映像檔 (zImage)
* `ARCH=arm`: target 架構為 arm
* `CROSS_COMPILE=arm-none-linux-gnueabi-`: 使用的 cross compile toolchain 的 prefix
* `zImage`: 要求產出一個壓縮過的 Linux kernel 映像檔,路徑為 `arch/arm/boot/zImage`
```sh
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- dtbs
```
* cross compile linux kernel 中的 Device Tree Blob 檔案 (dtb) (硬體描述檔)
Device Tree 為一種文字化的硬體描述檔 (`.dts` (device tree source)、`.dtsi` (device tree source include,類似 C++ 的 header) 檔),告訴 Linux:
1. 有哪些硬體裝置 (CPU、RAM、UART、Ethernet ...)
2. 它們的記憶體位置在哪裡
3. 有哪些 driver 要綁定到哪些裝置
編譯後會變成二進位 `.dtb` 檔(Device Tree Blob),在開機時由 bootloader (或 QEMU) 傳給 kernel
* `dtbs`: 要求 kernel 編譯 dtb 檔案。會編譯出多個 dtb 檔,路徑為 `arch/arm/boot/dts/`,我們只會用到 `arch/arm/boot/dts/arm-realview-eb.dtb`
## initrd ramdisk (initrd) 及 BusyBox (`busybox-1.29.3/`)
* initrd ramdisk (initrd) 為在啟動階段被 Linux kernel 呼叫的臨時檔案系統,用於 root file system (RFS) 被掛載之前的準備工作
* Linux 首先要將 kernel 載入到主記憶體。initrd 通常被壓縮成 gzip,開機時由 bootloader (如 LILO、GRUB) 來告知 kernel initrd 的位置,使其被 kernel 存取,掛載成一個 loop device
* loop device (`/dev/loop*`) 為 Linux 的一種虛擬裝置,允許把一個普通的檔案當成一個 block device 來掛載
* [BusyBox](https://busybox.net/) 為將許多小版本的 UNIX 工具 (ex: `ls`, `mount`) 包裝成一個執行檔,非常適合拿來組一個 initrd
* 同 [Linux kernel](https://hackmd.io/p4MBgIlaRbm7LBsd5Zh1gw?stext=1279%3A24%3A0%3A1749741786%3AjMkH5u&both=) 一樣需先下 `make ARCH=arm menuconfig` 修改設定來產生 .config,這裡也一樣直接複製提供好的設定檔
* 有一個 config `CONFIG_LINUXRC=y` 表示要編譯出 linuxrc 檔
linuxrc 為早期 linux kernel 掛載 initrd 之後第一個執行的程式 (比 `/sbin/init` 還早,其中一個原因為因為 `/sbin/init` 在 root file system 內,還沒被掛載),現在的 linux kernel 不需要了
```sh
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi-
make install CROSS_COMPILE=arm-none-linux-gnueabi-
```
* 同 [Linux kernel](https://hackmd.io/p4MBgIlaRbm7LBsd5Zh1gw?both=&stext=1757%3A65%3A0%3A1749743167%3AafNm-V) 的參數,只是 BusyBox `make install` 預設的位置是 `_install/`
* 安裝完之後,`_install/` 內再自己加入 `dev/`, `lib/` (內放 [cross compile toolchain](https://hackmd.io/p4MBgIlaRbm7LBsd5Zh1gw?both=#Cross-Compile-Toolchain-arm-201405) 的函式庫 (`.so`) 檔 (在 `arm-2014.05/arm-none-linux-gnueabi/libc/lib/`)), `opt/`, `proc/`, `sys/`, `tmp/`, `var/`, `home/` 及提供的 `etc/` 資料夾,即成為一個 initrd。最後使用 `cpio` 指令將所有檔案打包成一個 cpio archive,並用 `gzip` 壓縮成 `.gz`
* `etc/init.d/` 資料夾為早期 System V 系統在開機時用來啟動服務的所有 script。現代 Linux 皆已改用 systemd,但還是有這個資料夾,是為了向下相容
## 執行 QEMU
```sh
~/workspace/qemu-bin-6.2.0-rc2/bin/qemu-system-arm -M realview-eb -m 128M -cpu arm1136 -kernel zImage -initrd ./initrd.gz -nographic -serial mon:stdio -dtb arm-realview-eb.dtb
```
* `-M`: 指定要模擬的開發版
* `-m`: 指定模擬機的記憶體大小
* `-cpu`: 指定使用的 CPU 型號
* `-kernel`: kernel 映像檔路徑
* `-initrd`: initrd 路徑。**之後模擬機開機後的 root file system 即為 initrd**,因為在嵌入式環境通常不用再掛載外部磁碟,故也不需要真正的 root file system
* `-nographic`: 使用純文字模式
* `-serial mon:stdio`: 讓 QEMU 的 monitor (mon, 控制介面) 和模擬機的 serial port **共用 host 的 terminal 的 stdio**。這樣可以在同一個 terminal 透過 **切換 (按 ctrl-a c)** 來對模擬機的 serial port 做 I/O 或對 QEMU 的 monitor 做 I/O
QEMU 的 monitor 為用來控制和觀察模擬機狀態的介面,屬於 QEMU
模擬機的 serial port 是因為模擬機沒有顯示器,所以跟 Linux 互動可以透過 serial port
* `-dtb`: Device Tree Blob 路徑
## Virtual Hardware
此範例為定義一個 calculator 硬體計算單元 (character device),透過 memory-mapped I/O (MMIO) 操作,並可支援 QEMU monitor 及 VMState 機制
### 定義
放在 `qemu-6.2.0-rc2/hw/char/`,表示為一個 character device
:::spoiler `caslab_calculator.h`
```cpp=
#ifndef CASLAB_CALCULATOR_H
#define CASLAB_CALCULATOR_H
#include "hw/sysbus.h"
#include "chardev/char-fe.h"
#include "hw/qdev-core.h"
#include "qapi/error.h"
#define TYPE_CASLAB_CALC "caslab_calc"
#define CASLAB_CALC(obj) OBJECT_CHECK(CASLabCalcState, \
(obj), \
TYPE_CASLAB_CALC); // OBJECT_CHECK() 為 QEMU 提供的 macro,為將 pointer 轉型並檢查型別是否正確
#define CASLAB_CALC_RegCount 6
/*
* 儲存此裝置的 state
*/
typedef struct {
SysBusDevice parent_obj; // 使用 C 模擬繼承關係,繼承自 SysBusDevice,表示此裝置是一個在 SysBus 上的裝置
MemoryRegion iomem; // 定義此裝置的 MMIO address 及大小
uint32_t Reg[6]; // 定義此裝置有 6 個 register (分別為 control, operator, operand1, operand2, result, status)
CharBackend chr; // 為 character device 與 host 通訊的管道
qemu_irq irq; // IRQ 線
const unsigned char *id; // 裝置 ID
} CASLabCalcState;
/*
* 創建裝置
*/
static inline DeviceState* caslab_calc_create(hwaddr addr,
qemu_irq irq,
Chardev *chr)
{
DeviceState *dev;
SysBusDevice *s;
dev = qdev_new(TYPE_CASLAB_CALC); // 建立名為 caslab_calc 的 QEMU 裝置
s = SYS_BUS_DEVICE(dev); // 將裝置轉型為 SysBusDevice parent class
qdev_prop_set_chr(dev, "chardev", chr); // 設定 QEMU property "chardev"。在 .c 檔有以 DEFINE_PROP_CHR 定義了 chardev 的 property。QEMU property 為裝置初始化時由 QEMU 傳給模擬裝置的設定值
qdev_realize_and_unref(dev, NULL, &error_fatal); // 正式實體化 (realize) 裝置
sysbus_mmio_map(s, 0, addr); // 把裝置的第 0 個 MMIO 區域 map 到系統 bus 的 addr 位址
sysbus_connect_irq(s, 0, irq); // 把裝置的第 0 條 IRQ 線連接到 CPU 的 irq interrupt port
return dev;
}
#endif
```
:::
:::spoiler `caslab_calculator.c`
```c=
#include "qemu/osdep.h"
#include "hw/sysbus.h"
#include "qemu/log.h"
// added
#include "migration/vmstate.h"
#include "hw/qdev-properties-system.h"
#include "hw/qdev-core.h"
#include "hw/irq.h"
#include "caslab_calculator.h"
//#define CASLAB_CALC_DEBUG
#ifdef CASLAB_CALC_DEBUG
#define DB_PRINT(...) \
do \
{ \
fprintf(stderr, "[DEBUG]: %s: ", __func__);\
fprintf(stderr, ##__VA_ARGS__); \
} while(0);
#else
#define DB_PRINT(...)
#endif
#define CALC_REG_CONTROL 0x00
#define CALC_REG_OPERATOR 0x01
#define CALC_REG_OPERAND1 0x02
#define CALC_REG_OPERAND2 0x03
#define CALC_REG_RESULT 0x04
#define CALC_REG_STATUS 0x05
/*
* 控制暫存器 (CALC_REG_CONTROL) 的各種操作指令
*/
#define CALC_CTRL_GO 0x01 // 開始運算 (呼叫 caslab_calc_op())
#define CALC_CTRL_ABORT 0x02 // 終止進行中的運算 (如果狀態是 CALC_STATUS_BUSY,會立刻變為 CALC_STATUS_IDLE 並發出 interrupt)
#define CALC_CTRL_CLRI 0x04 // 清除中斷 (clear IRQ, 代表 guest 程式已處理完結果)
#define CALC_STATUS_IDLE 0x00
#define CALC_STATUS_BUSY 0x01
#define CALC_OP_ADD 0x01
#define CALC_OP_SUB 0x02
#define CALC_OP_MUL 0x03
#define CALC_OP_DIV 0x04
static void caslab_calc_reset(DeviceState *dev); //forward decleration
static void caslab_calc_op(CASLabCalcState *s)
{
uint32_t op;
uint32_t src1, src2;
uint32_t result = 0;
if(s == NULL)
return;
s->Reg[CALC_REG_STATUS] = CALC_STATUS_BUSY;
op = s->Reg[CALC_REG_OPERATOR];
src1 = s->Reg[CALC_REG_OPERAND1];
src2 = s->Reg[CALC_REG_OPERAND2];
DB_PRINT("Doing OP %u with value %u, %u\n", op, src1, src2);
switch(op) {
case CALC_OP_ADD:
result = src1 + src2;
break;
case CALC_OP_SUB:
result = src1 - src2;
break;
case CALC_OP_MUL:
result = src1 * src2;
break;
case CALC_OP_DIV:
if(src2 == 0)
fprintf(stderr, "Calculator: Invalid operand_2 !!\n");
else
result = src1 / src2;
break;
default:
result = 0;
break;
}
s->Reg[CALC_REG_STATUS] = CALC_STATUS_IDLE;
s->Reg[CALC_REG_RESULT] = result;
qemu_irq_raise(s->irq); // 觸發 IRQ,同等於 qemu_set_irq(s->irq, 1);
}
/*
* 此裝置的 ID
*/
static const unsigned char caslab_calc_id_arm[8] =
{0x11, 0x10, 0x88, 0x08, 0x0d, 0xf0, 0x05, 0xb1};
/*
* 根據讀取的位址 (offset) 判斷要回傳裝置 ID 還是 register 值
*/
static uint64_t caslab_calc_read(void *opaque, hwaddr offset, unsigned size)
{
CASLabCalcState *s = opaque;
uint64_t ret = 0;
offset >>= 2; // 因為 register 是 32 bit,所以要把以 32 bit 為單位的位址 (0x0, 0x4, 0x8, ...) 轉為 array 的 index (0, 1, 2, ...) 時就要 shift 2 bit
switch(offset) {
case 0x3F8: // 0x3F8 是一般 legacy UART (COM1) 的位址,這邊不是拿來模擬 UART,而是把此區段當成裝置 ID 區
ret = caslab_calc_id_arm[0];
break;
case 0x3F9:
ret = caslab_calc_id_arm[1];
break;
case 0x3FA:
ret = caslab_calc_id_arm[2];
break;
case 0x3FB:
ret = caslab_calc_id_arm[3];
break;
case 0x3FC:
ret = caslab_calc_id_arm[4];
break;
case 0x3FD:
ret = caslab_calc_id_arm[5];
break;
case 0x3FE:
ret = caslab_calc_id_arm[6];
break;
case 0x3FF:
ret = caslab_calc_id_arm[7];
break;
case CALC_REG_OPERAND1:
case CALC_REG_OPERAND2:
case CALC_REG_RESULT:
case CALC_REG_STATUS:
ret = s->Reg[offset];
break;
default:
break;
};
DB_PRINT("Read value %lu @ 0x%02lx\n", ret, offset<<2);
return ret;
}
static void caslab_calc_write(void *opaque, hwaddr offset, uint64_t value, unsigned size)
{
CASLabCalcState *s = opaque;
DB_PRINT("Write value %lu @ 0x%02lx\n", value, offset);
offset >>= 2;
if(offset >= CASLAB_CALC_RegCount)
return;
else
{
switch(offset)
{
case CALC_REG_CONTROL:
if(CALC_STATUS_IDLE == s->Reg[CALC_REG_STATUS]) {
if( value == CALC_CTRL_GO ) {
DB_PRINT("CASlab Calculator commit\n");
caslab_calc_op(s);
} else if ( value == CALC_CTRL_CLRI ) {
qemu_set_irq(s->irq, 0);
}
} else if(CALC_STATUS_BUSY == s->Reg[CALC_REG_STATUS]) {
if ( value == CALC_CTRL_ABORT ) {
//Cancel operations
s->Reg[CALC_REG_STATUS] = CALC_STATUS_IDLE;
qemu_set_irq(s->irq, 1);
}
}
break;
case CALC_REG_OPERATOR://Writable register
case CALC_REG_OPERAND1:
case CALC_REG_OPERAND2:
if(s->Reg[CALC_REG_STATUS] == CALC_STATUS_IDLE)
s->Reg[offset] = value;
break;
default: //Read only register
break;
}
}
}
/*
* 在裝置被 instantiate 時會被呼叫
*/
static void caslab_calc_realize(DeviceState *dev, Error **errp)
{
//Nothing here curreltly
}
static void caslab_calc_reset(DeviceState *dev)
{
CASLabCalcState *s = CASLAB_CALC(dev);
memset(s->Reg, 0, CASLAB_CALC_RegCount * sizeof(*(s->Reg)));
qemu_set_irq(s->irq, 0);
}
/*
* 表示這個模擬裝置的 MMIO 存取操作規則 (當有 guest 程式對此裝置的記憶體做讀寫時,QEMU 就會參考這個來決定怎麼處理讀寫行為)
*/
static const MemoryRegionOps caslab_calc_ops = {
.read = caslab_calc_read,
.write = caslab_calc_write,
.endianness = DEVICE_NATIVE_ENDIAN,
.valid.min_access_size = 4, // 允許 guest 程式存取的最小資料長度
.valid.max_access_size = 4 // 允許 guest 程式存取的最大資料長度
};
static void caslab_calc_init(Object *obj)
{
// 型別轉換
SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
CASLabCalcState *s = CASLAB_CALC(obj);
memory_region_init_io(&s->iomem, OBJECT(s), &caslab_calc_ops, s,
TYPE_CASLAB_CALC, 0x1000); // 初始化一塊 MMIO 區塊,大小為 4 KB
sysbus_init_mmio(sbd, &s->iomem); // 把此 MMIO 區塊掛載到 SysBus,讓 bus 可以存取到此塊位址
sysbus_init_irq(sbd, &s->irq); // 初始化 IRQ 腳位,註冊一條中斷線給此裝置
DB_PRINT("INIT\n");
}
/*
* 此部分為 optional,為 QEMU 定義此裝置的快照狀態描述 (VM migration state)
*/
static const VMStateDescription vmstate_caslab_calc = {
.name = TYPE_CASLAB_CALC,
.version_id = 1,
.minimum_version_id = 1,
.fields = (VMStateField[]) { // 需要保存的欄位
VMSTATE_UINT32_ARRAY(Reg, CASLabCalcState,
CASLAB_CALC_RegCount), // 儲存 CASLabCalcState 中的 Reg 這個 uint32 array,array 長度為 CASLAB_CALC_RegCount
VMSTATE_END_OF_LIST()
}
};
/*
* 定義一個 character device property,名稱為 "chardev"
*
* 常見的使用方式為:
* 1. 在 QEMU 啟動時先使用 -chardev 參數來建立一個 character device
* > ex:
* > -chardev socket,id=mysock,path=/tmp/mysock,server,nowait
* 2. 再使用 -device 參數將該 character device 指定給 caslab_calc 裝置
* > ex:
* > -device caslab_calc,chardev=mysock
* > 此設定會將 id 為 mysock 的 character device 綁定到 caslab_calc 裝置的 "chardev" property,進而對應到 CasLabCalcState.chr 欄位 (因為此段程式碼有設定好了)。這樣之後 caslab_calc 裝置就可以透過 chr 欄位來做字元 I/O ,實現與外部的通訊功能。但此 Lab 沒有用到這個
*/
static Property caslab_calc_properties[] = {
DEFINE_PROP_CHR("chardev", CASLabCalcState, chr),
DEFINE_PROP_END_OF_LIST(),
};
/*
* 「裝置類別 (device class)」的初始化
* (caslab_calc_init() 是針對 instance device 做初始化,這個是針對 class 做初始化)
*/
static void caslab_calc_class_init(ObjectClass *oc, void *data)
{
// 型別轉換
DeviceClass *dc = DEVICE_CLASS(oc);
// 註冊
dc->realize = caslab_calc_realize;
dc->vmsd = &vmstate_caslab_calc;
dc->reset = caslab_calc_reset;
dc->props_ = caslab_calc_properties;
}
/*
* 定義一個新的「裝置型別 (device type)」
* device type 為針對 device class 的描述
*/
static const TypeInfo caslab_calc_info = {
.name = TYPE_CASLAB_CALC,
.parent = TYPE_SYS_BUS_DEVICE, // 定義裝置是掛在 SysBus 上
.instance_size = sizeof(CASLabCalcState), // instance 大小
.instance_init = caslab_calc_init, // 註冊
.class_init = caslab_calc_class_init, // 註冊
};
static void caslab_calc_register_types(void)
{
type_register_static(&caslab_calc_info);
}
type_init(caslab_calc_register_types)
```
:::
### 設定
#### `qemu-6.2.0-rc2/hw/char/meson.build`
```meson
# ...
softmmu_ss.add(files('caslab_calculator.c')) # Add
# ...
```
表示使用 [softmmu](https://hackmd.io/p4MBgIlaRbm7LBsd5Zh1gw?both=&stext=270%3A7%3A0%3A1749910537%3ARXND8E) 模擬器時,ss (source set) 新增需要編譯此檔案
#### `qemu-6.2.0-rc2/hw/arm/realview.c`
```c
// ...
for (n = 0; n < 64; n++) {
pic[n] = qdev_get_gpio_in(dev, n);
}
sysbus_create_simple("caslab_calc", 0x80000000, pic[30]); // Add
// ...
```
建立一個簡單的 System Bus 裝置 "caslab_calc",此裝置的實體位址為 0x80000000,並設定 PIC (在 ARM 稱為 GIC) 第 30 號 interrupt 線連接至此裝置 (則 IRQ number 為 30)
#### `linux-4.14.85/.config`
使用 `make ARCH=arm menuconfig` 修改 config "Enable loadable module support" 為開啟
### Kernel Module
此 kernel module 的功能為 calculator 的 driver
:::spoiler `calc_drv.c` (編譯成 `.ko` 來使用)
```c=
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/ioctl.h>
#include <linux/wait.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/vmalloc.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <asm/atomic.h>
#include <asm/io.h>
#include <asm/page.h>
#include <linux/device.h>
#include <asm/irq.h>
#include <linux/sched.h>
#include <linux/interrupt.h>
#include <linux/irqflags.h>
// 和 device 定義 (caslab_calculator.c) 內相同 ///////////////
#define CALC_REG_CONTROL 0x00
#define CALC_REG_OPERATOR 0x04
#define CALC_REG_OPERAND1 0x08
#define CALC_REG_OPERAND2 0x0C
#define CALC_REG_RESULT 0x10
#define CALC_REG_STATUS 0x14
#define CALC_STATUS_IDLE 0x00
#define CALC_CTRL_GO 0x01
#define CALC_CTRL_ABORT 0x02
#define CALC_CTRL_CLRI 0x04
#define CALC_CTRL_RESET 0x08
#define CALC_OP_ADD 0x01
#define CALC_OP_SUB 0x02
#define CALC_OP_MUL 0x03
#define CALC_OP_DIV 0x04
////////////////////////////////////////////////////////////
// Some CMD number is invaild(used by linux kernel)
// For detail read linux kernel document (Documentation/ioctl/ioctl-number.txt)
// and http://man7.org/linux/man-pages/man2/ioctl_list.2.html
#define CMD_STATUS 0x00
#define CMD_OP_SET 0x03
#define CMD_CTRL 0x04
#define BUFFER_SIZE 20
#define DRIVER_NAME "caslab_calc_hw"
#define DEVICE_NAME "caslab-calc"
#define DEVICE_COMP "caslab,calc" // device 的 compatible property 名稱,定義在 linux-4.14.85/arch/arm/boot/dts/arm-realview-eb.dtsi。每個 device node 都有一個 compatible property,用來描述該裝置的類型、廠商、相容性等資訊
// Can be found within linux kernel Documentation/admin-guide/devices.txt
// 252 is LOCAL/EXPERIMENTAL USE
#define CASCALC_MAJOR 252
#define CASCALC_MINOR 0
#define CASCALC_BASE 0x80000000
#define CASCALC_MEMREGION 0x1000
#define CASCALC_IRQ 30
MODULE_AUTHOR("DUN-JIE CHEN");
MODULE_LICENSE("GPL");
static DECLARE_WAIT_QUEUE_HEAD(wq); // DECLARE_WAIT_QUEUE_HEAD 為 linux 提供的 macro,定義 wq 為一個 driver 內的 waiting queue 的 head,用來放執行此 driver 的 user process,等待 interrupt 發生再繼續
static int flag = 0; // 0: 裝置尚未完成運算或 interrupt 尚未到來,process 還不能醒
// 1: 裝置已完成運算,interrupt 已發生,process 可被喚醒
static unsigned int irqnum = CASCALC_IRQ;
static int cascalc_major = CASCALC_MAJOR;
static int cascalc_minor = CASCALC_MINOR;
typedef struct {
unsigned int buffer[BUFFER_SIZE];
} DevicePrivate; // 每個使用此裝置的 process 都有一個此空間
static void __iomem *base; // 為 CASCALC_BASE (physical address) 的 virtual address
/*
* 透過 dev_compatible_name 字串在 device tree 中找到該節點,並取出它的 IRQ number
*/
static int get_dut_irq(const char* dev_compatible_name)
{
struct device_node* dev_node; // 作業系統筆記 p54
int irq = -1;
dev_node = of_find_compatible_node(NULL, NULL, dev_compatible_name); // 透過 compatible 名稱找出對應的 device node
if(!dev_node)
return -1;
irq = irq_of_parse_and_map(dev_node, 0);
of_node_put(dev_node); // 把剛剛 reference 的 device node 結構釋放 (降低 reference count)
return irq;
}
static DevicePrivate* allocate_memory_region(void)
{
DevicePrivate* _prv;
_prv = (DevicePrivate*)vmalloc(sizeof(DevicePrivate));
return _prv;
}
static void free_allocated_memory(DevicePrivate *_prv)
{
vfree(_prv);
_prv = NULL;
}
static int calc_open(struct inode *inode, struct file *file)
{
file->private_data = allocate_memory_region();
if(file->private_data == NULL)
return -ENOMEM;
return 0;
}
static int calc_release(struct inode *inode, struct file *file)
{
free_allocated_memory(file->private_data);
return 0;
}
static ssize_t calc_read(struct file *filp, char *buff, size_t len, loff_t *off)
{
int i;
DevicePrivate *prv_data = (DevicePrivate*)filp->private_data;
/* Only result is readable memory address */
if(len > 4)
len = 4;
for( i = 0; i < len/4; i++ )
prv_data->buffer[i] = readl(base + CALC_REG_RESULT + i * 4); // 從 MMIO 區域讀取 4 byte (l)
i = copy_to_user(buff, prv_data->buffer, len); // copy_to_user() 為 Linux 提供的 API,把 kernel space 的 buffer (prv_data->buffer) 複製到 user space 的 buffer (buff)
return len;
}
static ssize_t calc_write(struct file *filp, const char *buff, size_t len, loff_t *off)
{
int i;
DevicePrivate *prv_data = (DevicePrivate*)filp->private_data;
/* Two operands are writeable memory address */
if ( len > 8 )
len = 8;
i = copy_from_user(prv_data->buffer, buff, len);
for(i = 0; i < len/4; i++)
writel(prv_data->buffer[i], base + CALC_REG_OPERAND1 + i*4);
return len;
}
/*
* 較新版 kernel 從 ioctl 改成 unlocked_ioctl 了 (user program 呼叫還是用 ioctl())
* 可以把 ioctl 理解成一般操作 (read/write/open/...) 外的雜項
*/
static long calc_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
long ret = 0;
unsigned int tmp;
int i;
printk("IOCTL CMD = %u, value = %lu\n", cmd, arg);
switch(cmd)
{
case CMD_STATUS: // 查看 status
tmp = readl(base + CALC_REG_STATUS);
i = copy_to_user((void*)arg, &tmp, sizeof(unsigned int));
case CMD_OP_SET: // 設定 operator
if(arg >= CALC_OP_ADD && arg <= CALC_OP_DIV)
writel(arg, base + CALC_REG_OPERATOR);
else
printk("Invaild OP %lu\n", arg);
break;
case CMD_CTRL: // 控制硬體行為
tmp = readl(base + CALC_REG_STATUS);
if(tmp == CALC_STATUS_IDLE) { // 裝置處於 idle,啟動計算
writel(arg, base + CALC_REG_CONTROL); // 寫入 control register 之後,根據 caslab_calculator.c 的定義,會讓 hardware 開始計算
wait_event_interruptible(wq, flag != 0); // 將呼叫此 ioctl 的 user process 放入 waiting queue wq,並等待 hardware 計算,hardware 計算完之後會發出 IRQ,CPU 被 interrupt,執行 cascalc_interrupt(),把 flag 設為 1、並執行 wake_up_interruptible() 把 user process 從 wq 移出。此時 (flag != 0) 為 true,就可以繼續往下執行了
flag = 0;
} else {
if(arg == CALC_CTRL_ABORT || arg == CALC_CTRL_RESET) {
writel(arg, base + CALC_REG_CONTROL);
flag = 0;
}
}
break;
default:
printk("Invaild CMD %u\n", cmd);
ret = -ENOTTY;
break;
}
return ret;
}
/*
* struct cdev 為 linux kernel 中管理 character device 的核心資料結構,在 driver 初始化時 (cascalc_init) 與 file_operations 一起註冊,使裝置能透過 /dev/ 介面被使用
*/
static struct cdev cascalc_cdev = {
.kobj = { .name = DRIVER_NAME }, // kernel object (kobject),目前僅供內部使用或 debug
.owner = THIS_MODULE // 指定此裝置由目前這個 kernel module 擁有,可防止使用中被卸載
};
static struct file_operations cascalc_fops ={
.owner = THIS_MODULE,
.read = calc_read,
.write = calc_write,
.unlocked_ioctl = calc_unlocked_ioctl,
.open = calc_open,
.release = calc_release
};
/*
* 即 ISR,由 linux kernel 執行
*/
static irqreturn_t cascalc_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
// 參考 calc_unlocked_ioctl 的註解
if (irq == irqnum) {
printk("received interrupt from %s\n", DRIVER_NAME);
writel(CALC_CTRL_CLRI, base + CALC_REG_CONTROL);
printk("Release interrupt \n");
flag = 1;
wake_up_interruptible(&wq);
}
return IRQ_HANDLED;
}
/*
* 執行 insmod 時會呼叫此函數做初始化
*/
static int __init cascalc_init(void) {
dev_t dev = 0;
int result;
if(CASCALC_MAJOR) { // 有指定 major number
dev = MKDEV(cascalc_major, cascalc_minor); // linux kernel 提供的 macro,把 major number 跟 minor number 組合成 dev_t type
result = register_chrdev_region( dev, 1, DRIVER_NAME); // 註冊裝置號,註冊 1 個裝置
} else { // 沒有指定 major number
result = alloc_chrdev_region( &dev, cascalc_minor, 1, DRIVER_NAME); // 動態分配一個 major number (存入 dev),註冊裝置號,註冊 1 個裝置
cascalc_major = MAJOR(dev);
}
if(result < 0) {
printk(KERN_ERR DRIVER_NAME "Can't allocate major number %d for %s\n", cascalc_major, DEVICE_NAME);
return -EAGAIN;
}
cdev_init( &cascalc_cdev, &cascalc_fops); // 註冊 struct cdev 及 operations
result = cdev_add( &cascalc_cdev, dev, 1); // 加入 linux kernel,正式成為可用的 character device
if(result < 0) {
printk(KERN_ERR DRIVER_NAME "Can't add device %d\n", dev);
return -EAGAIN;
}
/* allocate memory */
base = ioremap_nocache(CASCALC_BASE , CASCALC_MEMREGION); // 把 physical address CASCALC_BASE map 成 virtual address,才能讓 kernel 做存取
if(base == NULL)
return -ENXIO;
irqnum = get_dut_irq(DEVICE_COMP);
if(irqnum == -1) {
printk(KERN_ERR DRIVER_NAME " Can't request IRQ, Device not found\n");
return -ENXIO;
}
result = request_irq(irqnum, (void *)cascalc_interrupt, 0, DEVICE_NAME, NULL); // 註冊 ISR
if(result) {
printk(KERN_ERR DRIVER_NAME " Can't request IRQ %d (return %d)\n", irqnum, result);
return -EFAULT;
}
return 0;
}
static void __exit cascalc_exit(void) {
dev_t dev;
dev = MKDEV(cascalc_major, cascalc_minor);
unregister_chrdev_region( dev, 1);
free_irq(irqnum, NULL);
iounmap((unsigned long *) base);
printk("CASLab_Calc driver exit.\n");
}
module_init(cascalc_init);
module_exit(cascalc_exit);
```
:::
### Device Tree
把主要的 device 定義放在 `.dtsi`,`.dts` 則用來表示這塊板子要啟用哪些裝置,以及針對這些裝置提供實際的使用設定。因為 `arm-realview-eb.dts` 內有 include `arm-realview-eb.dtsi`,所以編譯出的 `.dtb` 會包含 `arm-realview-eb.dtsi` 的所有 device
:::spoiler `linux-4.14.85/arch/arm/boot/dts/arm-realview-eb.dtsi`
```dtsi=
// ...
// Add
cascalc: cascalc@80000000 { // 表示有一個 device,實體位址為 80000000
compatible = "caslab,calc"; // 參考 driver 中的註解
reg = <0x80000000 0x1000>; // 裝置的記憶體範圍
clocks = <&pclk>; // 裝置的 clock source 為 pclk (此檔案有定義)
clock-names = "apb_pclk"; // 指定此裝置每個 clock source 的名稱。因為一個裝置的 clock source 可以有多個,這樣可以在 driver 使用這些名字來 access clock source
};
};
```
:::
:::spoiler `linux-4.14.85/arch/arm/boot/dts/arm-realview-eb.dts`
```dts=
// ...
// Add
&cascalc{ // 指向 .dtsi 內定義的 cascalc
interrupt-parent = <&intc>; // 表示此裝置的 interrupt 來源為 intc (此檔案有定義為 interrupt controller)
interrupts = <0 30 IRQ_TYPE_LEVEL_HIGH>;
}; // 0: SPI (Shared Peripheral Interrupt),為 ARM GIC interrupt 的一種類型,表示 GIC 可以分發此 interrupt 到任何一顆 CPU core
// 30: IRQ number
// IRQ_TYPE_LEVEL_HIGH: interrupt 訊號保持為高準位時視為觸發 interrupt 中,回到低準位時才表示結束 interrupt
```
:::
## User Program
使用 [Virtual Hardware](https://hackmd.io/p4MBgIlaRbm7LBsd5Zh1gw?both#Virtual-Hardware) 中定義好的 caslab_calc device,做所有 operation
:::spoiler `calc.c`
```c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#define DevPath "/dev/calculator" // 在此範例中會手動下 mknod /dev/calculator c 252 0 來在 /dev/ 建立一個 device node (作業系統筆記 p54)
// 和 device 定義 (caslab_calculator.c) 內相同 ///////////////
#define CALC_CTRL_GO 0x01
#define CALC_CTRL_ABORT 0x02
#define CALC_CTRL_CLRI 0x04
#define CALC_CTRL_RESET 0x08
#define CALC_OP_ADD 0x01
#define CALC_OP_SUB 0x02
#define CALC_OP_MUL 0x03
#define CALC_OP_DIV 0x04
#define CMD_STATUS 0x00
#define CMD_OP_SET 0x03
#define CMD_CTRL 0x04
////////////////////////////////////////////////////////////
static const unsigned int OP_LIST[4] = {
CALC_OP_ADD,
CALC_OP_SUB,
CALC_OP_MUL,
CALC_OP_DIV };
int main(int argc, char* argv[]) {
int i;
int buffer[2] = {0};
int result[4] = {0};
int calc_fd = 0;
if(argc < 3) {
printf("Usage: \n");
printf(" %s <operand1> <operand2>\n", argv[0]);
return 0;
} else {
// argv[1] 為 operand1、argv[2] 為 operand 2
buffer[0] = atoi(argv[1]);
buffer[1] = atoi(argv[2]);
}
calc_fd = open(DevPath, O_RDWR);
if(calc_fd == 0) {
perror("Error opening device\n");
return 1;
}
write(calc_fd, (void*)buffer, sizeof(unsigned int) * 2);
for(i = 0; i < 4; i++) {
ioctl(calc_fd, CMD_OP_SET, OP_LIST[i]);
ioctl(calc_fd, CMD_CTRL, CALC_CTRL_GO);
read(calc_fd, (void*)&(result[i]), sizeof(int));
}
printf("%d + %d = %d\n", buffer[0], buffer[1], result[0]);
printf("%d - %d = %d\n", buffer[0], buffer[1], result[1]);
printf("%d * %d = %d\n", buffer[0], buffer[1], result[2]);
printf("%d / %d = %d\n", buffer[0], buffer[1], result[3]);
close(calc_fd);
return 0;
}
```
:::
# QEMU-SystemC co-simulation
* 要做 QEMU-SystemC co-simulation 時才會用到
* 是把 QEMU-SystemC bridge 的 **QEMU 端** (`qemu-6.2.0-rc2/hw/char/caslab_sysbridge.c`) 視為一 virtual hardware,替代掉上面的 `caslab_calculator.h`、`caslab_calculator.c`;**SystemC 端** (`SystemC_Module/qsysbridge`) 使用另一 terminal 獨立執行
## SystemC 部分 (`SystemC_Module/`)
### calculator
:::spoiler `calculator.h`
```cpp=
#ifndef _H_CALCULATOR_
#define _H_CALCULATOR_
#include <systemc.h>
#include <ahb_slave_if.h>
#include <stdint.h>
#define CASLAB_CALC_RegCount 6
#define CAL_MEM_Region 0x1000
#define CALC_REG_CONTROL 0x00
#define CALC_REG_OPERATOR 0x01
#define CALC_REG_OPERAND1 0x02
#define CALC_REG_OPERAND2 0x03
#define CALC_REG_RESULT 0x04
#define CALC_REG_STATUS 0x05
#define CALC_CTRL_IDLE 0x00
#define CALC_CTRL_GO 0x01
#define CALC_CTRL_ABORT 0x02
#define CALC_CTRL_CLRI 0x04
#define CALC_STATUS_IDLE 0x00
#define CALC_STATUS_BUSY 0x01
#define CALC_OP_ADD 0x01
#define CALC_OP_SUB 0x02
#define CALC_OP_MUL 0x03
#define CALC_OP_DIV 0x04
class calculator:public ahb_slave_if, public sc_module
{
public:
sc_in_clk clk;
sc_out<bool> irq_out;
sc_in<bool> rst_in;
sc_uint<32> Reg[CASLAB_CALC_RegCount];
sc_uint<8> state;
SC_HAS_PROCESS(calculator);
calculator(sc_module_name name, uint32_t mapping_size);
~calculator(void);
void run();
bool read (uint32_t*, uint32_t, int);
bool write (uint32_t, uint32_t, int);
virtual bool local_access (bool write, uint32_t addr, uint32_t& data, unsigned int length);
private:
void caslab_calc_op(void);
const static unsigned char caslab_calc_id_arm[];
};
#endif
```
:::
:::spoiler `calculator.cpp`
```cpp=
#include "calculator.h"
using namespace std;
#ifdef CASLAB_CALC_DEBUG
#define DB_PRINT(...) \
do \
{ \
fprintf(stderr, "[CALC]: "); \
fprintf(stderr, ##__VA_ARGS__); \
} while (0);
#else
#define DB_PRINT(...)
#endif
const unsigned char calculator::caslab_calc_id_arm[] = {0x11, 0x10, 0x88, 0x08, 0x0d, 0xf0, 0x05, 0xb1};
calculator::calculator(sc_module_name name, uint32_t mapping_size) : ahb_slave_if(mapping_size), sc_module(name)
{
SC_THREAD(run);
sensitive << clk.pos();
reset_signal_is(rst_in, true);
}
calculator::~calculator(void)
{
}
void calculator::caslab_calc_op(void)
{
uint32_t i;
uint32_t op;
uint32_t src1, src2;
uint32_t result = 0;
op = Reg[CALC_REG_OPERATOR];
src1 = Reg[CALC_REG_OPERAND1];
src2 = Reg[CALC_REG_OPERAND2];
switch (op)
{
case CALC_OP_ADD:
result = src1 + src2;
wait();
break;
case CALC_OP_SUB:
result = src1 - src2;
wait();
break;
case CALC_OP_MUL:
result = src1 * src2;
for (i = 0; i < 4; i++)
wait();
break;
case CALC_OP_DIV:
if (src2 == 0)
fprintf(stderr, "Calculator: Invalid operand_2 !!\n");
else
{
result = src1 / src2;
for (i = 0; i < 13; i++)
wait();
}
break;
default:
break;
}
Reg[CALC_REG_RESULT] = result;
}
void calculator::run()
{
// Reset registers
for (unsigned i = 0; i < CASLAB_CALC_RegCount; i++)
Reg[i] = 0x00;
// Clear IRQ
irq_out = false;
state = 0; // 用來當作 state machine
while (1)
{
wait();
if (Reg[CALC_REG_STATUS] == CALC_STATUS_IDLE)
{
switch (Reg[CALC_REG_CONTROL])
{
case CALC_CTRL_GO:
irq_out = false;
state = 0;
Reg[CALC_REG_STATUS] = CALC_STATUS_BUSY;
break;
case CALC_CTRL_CLRI:
irq_out = false;
DB_PRINT("Reset IRQ.\n");
break;
default:
break;
}
}
else
{ // Run the state machine
if (Reg[CALC_REG_CONTROL] == CALC_CTRL_ABORT)
{
state = 0;
Reg[CALC_REG_STATUS] = CALC_STATUS_IDLE;
}
else
{
switch (state)
{
case 0:
caslab_calc_op();
state = state + 1;
break;
case 1:
irq_out = true;
Reg[CALC_REG_STATUS] = CALC_STATUS_IDLE;
state = 0;
DB_PRINT("Done, raising IRQ.\n");
break;
default:
state = 0;
break;
}
}
}
Reg[CALC_REG_CONTROL] = CALC_CTRL_IDLE;
}
}
bool calculator::read(uint32_t *data, uint32_t addr, int length)
{
bool success = true;
DB_PRINT("Reading data from address 0x%08x\n", addr);
if (addr < (CASLAB_CALC_RegCount << 2)) // addr 是要讀取 register
*data = Reg[addr >> 2];
else // addr 是要讀取裝置 ID
{
switch (addr >> 2)
{
case 0x3F8 ... 0x3FF:
*data = caslab_calc_id_arm[(addr >> 2) - 0x3F8];
break;
default:
*data = 0x00;
success = false;
}
}
return success;
}
bool calculator::write(uint32_t data, uint32_t addr, int length)
{
bool success = true;
DB_PRINT("Writting data [0x%08x] to address 0x%08x\n", data, addr);
if (addr < (CASLAB_CALC_RegCount << 2))
{
switch (addr >> 2)
{
case CALC_REG_OPERATOR:
case CALC_REG_OPERAND1:
case CALC_REG_OPERAND2:
case CALC_REG_CONTROL:
Reg[addr >> 2] = data;
break;
default: // Read only.
success = false;
break;
}
}
else
success = false;
return success;
}
bool calculator::local_access(bool write, uint32_t addr, uint32_t &data, unsigned int length)
{
bool success = true;
uint32_t local_address = addr & get_address_mask();
if (write)
success = this->write(data, local_address, length);
else
success = this->read(&data, local_address, length);
return success;
}
```
:::
### qemu_sc_bridge
即 QEMU-SystemC bridge SystemC 端
:::spoiler `qsbridge.h`
```cpp=
#ifndef _H_QSBRIDGE_
#define _H_QSBRIDGE_
#include <semaphore.h>
#include <systemc.h>
#include <ahb_master_if.h>
#include <stdint.h>
#define CAS_SYSBRG_SMID 0x8000
#define CAS_SYSBRG_IRQSEM "/CASSYS_IRQ"
#define CAS_SYSBRG_DATASEM "/CASSYS_DATA"
#define CAS_SYSBRG_ACKSEM "/CASSYS_ACK"
#define CAS_SYSBUS_CMD_Read 0x00
#define CAS_SYSBUS_CMD_Write 0x01
#define CAS_SYSBUS_CMD_Reset 0x02
#define CAS_SYSBUS_CMD_Exit 0x04
typedef struct
{
uint32_t Address;
uint32_t Data;
uint32_t ACK; // For bus access ack/nak
uint32_t CMD;
uint32_t ISR;
} CAS_SYSBUS_DATA;
class qsbridge : public sc_module, public ahb_master_if
{
public:
sc_in_clk clk;
sc_in<bool> irq_in; // 代收 calculator 的 interrupt
sc_out<bool> rst_out;
SC_HAS_PROCESS(qsbridge);
qsbridge(sc_module_name name);
~qsbridge();
void run();
protected:
bool bus_read(uint32_t *data, uint32_t addr, uint32_t length);
bool bus_read64(uint64_t *data, uint64_t addr, uint32_t length);
bool bus_write(uint32_t data, uint32_t addr, uint32_t length);
bool bus_write64(uint64_t data, uint64_t addr, uint32_t length);
private:
sem_t *SEM_IRQ;
sem_t *SEM_BUS; // 表示 QEMU 端 bridge 向 SystemC 端 bridge 發出各種指令 (CAS_SYSBUS_CMD_Read, CAS_SYSBUS_CMD_Write, CAS_SYSBUS_CMD_Reset, CAS_SYSBUS_CMD_Exit)
sem_t *SEM_BUS_Ack; // 表示:
// 1. SystemC 端 bridge 已準備好
// 2. SystemC 向 QEMU 表示命令處理完畢
// sem_t SEM_BUS_Lock;
int shm_id;
volatile CAS_SYSBUS_DATA *sh_mem;
bool connected;
};
#endif
```
:::
:::spoiler `qsbridge.cpp`
```cpp=
#include "qsbridge.h"
#include <signal.h>
#include <fcntl.h> /* For O_* constants */
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;
#define CASLAB_SYSBRG_DEBUG
#ifdef CASLAB_SYSBRG_DEBUG
#define DB_PRINT(...) \
do \
{ \
fprintf(stderr, "[QSBRIDGE]: ");\
fprintf(stderr, ##__VA_ARGS__); \
} while(0);
#else
#define DB_PRINT(...)
#endif
qsbridge::qsbridge(sc_module_name name): sc_module(name)
{
int init_state = 3;
shm_id = -1;
connected = false;
do {
SEM_IRQ = sem_open(CAS_SYSBRG_IRQSEM, O_CREAT, S_IRUSR | S_IWUSR, 0);
SEM_BUS = sem_open(CAS_SYSBRG_DATASEM, O_CREAT, S_IRUSR | S_IWUSR, 0);
SEM_BUS_Ack = sem_open(CAS_SYSBRG_ACKSEM, O_CREAT, S_IRUSR | S_IWUSR, 0);
if(SEM_FAILED == SEM_IRQ || SEM_FAILED == SEM_BUS || SEM_FAILED == SEM_BUS_Ack) {
fprintf(stderr, "ERROR: semaphore failed\n");
break;
}
init_state--;
//Clear semaphore (non-blocking 把 semaphore 值減到 0 為止)
while(sem_trywait(SEM_IRQ) == 0);
while(sem_trywait(SEM_BUS_Ack) == 0);
shm_id = shmget(CAS_SYSBRG_SMID, sizeof(CAS_SYSBUS_DATA), 0666 | IPC_CREAT);
if(shm_id == -1){
fprintf(stderr, "ERROR: shmget failed\n");
break;
}
init_state--;
sh_mem = (volatile CAS_SYSBUS_DATA*)shmat(shm_id, NULL, 0);
if(sh_mem == (void*)-1) {
fprintf(stderr, "ERROR: shmat failed\n");
break;
}
memset((void*)sh_mem, 0, sizeof(CAS_SYSBUS_DATA));
init_state--;
} while(0);
if(init_state != 0) {
struct shmid_ds buf;
switch(init_state) {
case 1://shmat failed
sh_mem = NULL;
if(shm_id != -1 && shmctl(shm_id, IPC_STAT, &buf) == 0)
shmctl(shm_id, IPC_RMID, &buf);//Delete shared memory.
case 2://shmget failed
sem_close(SEM_IRQ);
while(sem_trywait(SEM_BUS) == 0);
sem_close(SEM_BUS);
sem_close(SEM_BUS_Ack);
case 3://sem_open failed
SEM_IRQ = NULL;
SEM_BUS = NULL;
SEM_BUS_Ack = NULL;
exit(-1);
break;
default://Should not happen
exit(0);
break;
}
}
SC_THREAD(run);
sensitive << clk.pos();
dont_initialize();
}
qsbridge::~qsbridge()
{
DB_PRINT("qsbridge deinit.\n");
if(SEM_BUS != NULL) {
while(sem_trywait(SEM_BUS) == 0);
sem_close(SEM_BUS);
}
if(SEM_BUS_Ack != NULL) {
while(sem_trywait(SEM_BUS_Ack) == 0);
sem_close(SEM_BUS_Ack);
}
if(SEM_IRQ != NULL) {
if(sh_mem != NULL && connected) {
DB_PRINT("Sending exit signal(0x%02x) to QEMU\n", CAS_SYSBUS_CMD_Exit);
sh_mem->CMD = CAS_SYSBUS_CMD_Exit;
sem_post(SEM_IRQ);
}
sem_close(SEM_IRQ);
}
if(sh_mem != NULL)
shmdt((void*) sh_mem);
if(shm_id != -1) {
struct shmid_ds buf;
if(shmctl(shm_id, IPC_STAT, &buf) == 0)
shmctl(shm_id, IPC_RMID, &buf);//Delete shared memory.
}
}
void qsbridge::run()
{
bool success = false;
bool _previous_irq = false;
printf("Waiting QEMU UP....\n");
sem_wait(SEM_BUS); // QEMU 準備好後在 caslab_sysbridge.c 的 Thread_UpdateIRQ() post SEM_BUS
sem_post(SEM_BUS_Ack);
printf("Connected to QEMU.\n");
connected = true;
while(connected) {
/* At first, we handle the IRQ */
if(_previous_irq != irq_in) {
if(irq_in)
sh_mem->ISR |= 0x01;
else
sh_mem->ISR &= (uint32_t)(-1) ^ 0x01; // 清除 LSb。(0xFFFFFFFF ^ 0x00000001 = 0xFFFFFFFE)
sem_post(SEM_IRQ);
_previous_irq = irq_in;
}
/* And then, we process the memory access */
if(sem_trywait(SEM_BUS) == 0) {//There's bus command (non-blocking 對 SEM_BUS 減 1,如果原本 semaphore 值 > 0 則會回傳 0)
uint32_t temp;
switch(sh_mem->CMD) {
case CAS_SYSBUS_CMD_Read:
DB_PRINT("BUS Read, read from 0x%08x\n", sh_mem->Address);
success = this->bus_read(&temp, sh_mem->Address, 4);
sh_mem->Data = temp;
break;
case CAS_SYSBUS_CMD_Write:
DB_PRINT("BUS Write, Write 0x%04x to 0x%08x\n", sh_mem->Data, sh_mem->Address);
success = this->bus_write(sh_mem->Data, sh_mem->Address, 4);
break;
case CAS_SYSBUS_CMD_Reset:
DB_PRINT("BUS Reset\n");
rst_out = true;
wait();//Wait 1 cycle
rst_out = false;
success = true;
break;
case CAS_SYSBUS_CMD_Exit:
DB_PRINT("Simulation end\n");
success = true;
connected = false;
break;
}
sh_mem->ACK = (success)? 0x01:0x00;
sem_post(SEM_BUS_Ack);
if(!connected) break;
}
wait();/* wait for next clock trigger */
}
raise(SIGINT);
}
/*=================== TLM Bus Access Function ====================*/
bool qsbridge::bus_read(uint32_t* data, uint32_t addr, uint32_t length)
{
bool success;
success = bus_b_access(false, (sc_dt::uint64)addr, reinterpret_cast<unsigned char*>(data), length);
if(!success) {
DB_PRINT("bus read 0x%X failed\n", addr);
}
return success;
}
bool qsbridge::bus_read64(uint64_t* data, uint64_t addr, uint32_t length)
{
bool success1;
bool success2;
uint32_t tmp;
success1 = bus_b_access(false, (sc_dt::uint64)addr, reinterpret_cast<unsigned char*>(&tmp), length);
*data = tmp;
success2 = bus_b_access(false, (sc_dt::uint64)addr + 4, reinterpret_cast<unsigned char*>(&tmp), length);
*data |= ((uint64_t)tmp) << 32;
if(success1 && success2)
return true;
else
{
DB_PRINT("bus read 64 0x%lX failed\n", addr);
return false;
}
}
bool qsbridge::bus_write(uint32_t data, uint32_t addr, uint32_t length)
{
bool success;
success = bus_b_access(true, (sc_dt::uint64)addr, reinterpret_cast<unsigned char*>(&data), length);
if(!success) {
DB_PRINT("bus write 0x%X:0x%X failed\n", addr, data);
}
return success;
}
bool qsbridge::bus_write64(uint64_t data, uint64_t addr, uint32_t length)
{
bool success1;
bool success2;
uint32_t tmp;
tmp = data;
success1 = bus_b_access(true, (sc_dt::uint64)addr, reinterpret_cast<unsigned char*>(&tmp), length);
tmp = data >> 32;
success2 = bus_b_access(true, (sc_dt::uint64)addr + 4, reinterpret_cast<unsigned char*>(&tmp), length);
if(success1 && success2)
return true;
else
{
DB_PRINT("bus write 64 0x%lX:0x%lX failed\n", addr, data);
return false;
}
}
```
:::
## QEMU 部分 (Virtual Hardware)
### 定義
即 QEMU-SystemC bridge QEMU 端
:::spoiler `qemu-6.2.0-rc2/hw/char/caslab_sysbridge.c`
```c=
#include "qemu/osdep.h"
#include "hw/sysbus.h"
#include "chardev/char-fe.h"
#include "qemu/log.h"
// added
#include "migration/vmstate.h"
#include "hw/qdev-properties-system.h"
#include "hw/qdev-core.h"
#include "hw/irq.h"
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <sys/ipc.h>
#include <sys/shm.h>
#include <pthread.h>
#include <semaphore.h>
#define CASLAB_SYSBRG_DEBUG
#ifdef CASLAB_SYSBRG_DEBUG
#define DB_PRINT(...) \
do \
{ \
fprintf(stderr, "[DEBUG]: %s: ", __func__); \
fprintf(stderr, ##__VA_ARGS__); \
} while (0);
#else
#define DB_PRINT(...)
#endif
#define DeviceMemRange 0x1000
#define DeviceMemMask 0x0FFF
#define TYPE_CASLAB_SYSBRG "caslab_sysbridge"
#define CASLAB_SYSBRG(obj) OBJECT_CHECK(CASLabBridgeState, \
(obj), \
TYPE_CASLAB_SYSBRG)
#define CAS_SYSBRG_SMID 0x8000
#define CAS_SYSBRG_IRQSEM "/CASSYS_IRQ"
#define CAS_SYSBRG_DATASEM "/CASSYS_DATA"
#define CAS_SYSBRG_ACKSEM "/CASSYS_ACK"
#define CAS_SYSBUS_CMD_Read 0x00
#define CAS_SYSBUS_CMD_Write 0x01
#define CAS_SYSBUS_CMD_Reset 0x02
#define CAS_SYSBUS_CMD_Exit 0x04
typedef struct
{
uint32_t Address;
uint32_t Data;
uint32_t ACK; // For bus access ack/nak
uint32_t CMD;
uint32_t ISR;
} CAS_SYSBUS_DATA;
typedef struct
{
SysBusDevice parent_obj;
MemoryRegion iomem;
CharBackend chr;
qemu_irq irq;
const unsigned char *id;
sem_t *SEM_IRQ;
sem_t *SEM_BUS;
sem_t *SEM_BUS_Ack;
sem_t SEM_BUS_Key; // Unnamed semaphore,作為 shared memory 的 mutex lock。SystemC 部分因為不會造成 race 問題所以拿掉這個
bool _connected;
int shm_id;
volatile CAS_SYSBUS_DATA *sh_mem;
} CASLabBridgeState;
volatile bool PrgExit = false;
static pthread_t IRQ_Thread;
static CASLabBridgeState *_dev;
void *Thread_UpdateIRQ(void *);
static const unsigned char caslab_sysbridge_id_arm[8] =
{0x11, 0x10, 0x88, 0x08, 0x0d, 0xf0, 0x05, 0xb1};
static void caslab_sysbridge_deinit(CASLabBridgeState *s)
{
struct shmid_ds buf;
struct timespec wait_time = {0}; // 等待 exit 的最長時間
DB_PRINT("SYSBUS DEINIT\n");
clock_gettime(CLOCK_REALTIME, &wait_time); // CLOCK_REALRIME 表示 wall-clock time,將 wall-clock time 存進 wait_time
wait_time.tv_sec += 3; // wait 3s before force exit.
if (s->_connected)
{
s->_connected = false;
DB_PRINT("Disonnceting from HW.\n");
DB_PRINT("Invoking HW Bus Signal.\n")
s->sh_mem->CMD = CAS_SYSBUS_CMD_Exit;
while (sem_trywait(s->SEM_BUS_Ack) == 0)
;
sem_post(s->SEM_BUS);
sem_timedwait(s->SEM_BUS_Ack, &wait_time);
// reset sems.
while (sem_trywait(s->SEM_BUS) == 0)
;
while (sem_trywait(s->SEM_BUS_Ack) == 0)
;
while (sem_trywait(s->SEM_IRQ) == 0)
;
sem_close(s->SEM_BUS);
sem_close(s->SEM_BUS_Ack);
sem_close(s->SEM_IRQ);
}
shmdt((void *)s->sh_mem);
sem_destroy(&s->SEM_BUS_Key);
if (shmctl(s->shm_id, IPC_STAT, &buf) == 0)
shmctl(s->shm_id, IPC_RMID, &buf); // Delete shared memory.
}
/*
* 程式正常 exit/return 時會執行的 function
*/
static void ON_Module_Exit(void)
{
PrgExit = true;
if (_dev != NULL)
{
sem_post(_dev->SEM_IRQ); // 因為 Thread_UpdateIRQ() 內有 sem_wait(s->SEM_IRQ),可能 thread 會在這邊 wait 導致 thread 無法正常退出,故新增此行
// pthread_join(IRQ_Thread, NULL);
pthread_cancel(IRQ_Thread);
caslab_sysbridge_deinit(_dev);
}
}
/*
* thread 主程式,用來作為 IRQ listener
*/
void *Thread_UpdateIRQ(void *mem)
{
CASLabBridgeState *s;
s = (CASLabBridgeState *)mem;
while (!PrgExit)
{
if (s->_connected)
{
uint32_t ISR;
DB_PRINT("Waiting Device IRQ Signal.\n");
sem_wait(s->SEM_IRQ);
if (PrgExit)
break;
if (s->sh_mem->CMD == CAS_SYSBUS_CMD_Exit)
{
sem_wait(&s->SEM_BUS_Key);
s->sh_mem->CMD = CAS_SYSBUS_CMD_Read;
sem_post(&s->SEM_BUS_Key);
s->_connected = false;
DB_PRINT("Device Disconnected.\n");
while (sem_trywait(s->SEM_BUS) == 0)
;
while (sem_trywait(s->SEM_BUS_Ack) == 0)
;
while (sem_trywait(s->SEM_IRQ) == 0)
;
continue;
}
ISR = s->sh_mem->ISR;
DB_PRINT("Received IRQ Signal from HW.(0x%02x)\n", ISR);
qemu_mutex_lock_iothread(); // 由於以下會使用到 internal I/O usage (ex: qemu_irq_raise()),任何非 QEMU main thread 的 thread 要用到 internal I/O 時必須用此 macro 保護
if (ISR & 0x01)
qemu_irq_raise(s->irq);
else
qemu_irq_lower(s->irq);
qemu_mutex_unlock_iothread(); // Done, now unlock it.
}
else
{
DB_PRINT("Waiting Device up...\n");
sem_post(s->SEM_BUS); // ┐
sem_wait(s->SEM_BUS_Ack); // ┘參考 qsbridge.cpp 內註解
s->_connected = true;
DB_PRINT("Device connected.\n");
if (PrgExit)
break;
}
}
DB_PRINT("IRQ Listener exited.\n");
return NULL;
}
static const VMStateDescription vmstate_caslab_sysbridge = {
.name = TYPE_CASLAB_SYSBRG,
.version_id = 1,
.minimum_version_id = 1,
.fields = (VMStateField[]){
// nothing needed to be saved here.
VMSTATE_END_OF_LIST(),
}};
static uint64_t caslab_sysbridge_read(void *opaque, hwaddr offset, unsigned size)
{
CASLabBridgeState *s = opaque;
uint64_t ret = 0;
hwaddr temp;
switch (offset & DeviceMemMask)
{
case 0xFE0 ... 0xFFC: // 以最末 32 bytes 作為裝置 ID
temp = (offset & DeviceMemMask) - 0xFE0;
ret = caslab_sysbridge_id_arm[temp >> 2];
break;
default:
if (s->_connected && offset < DeviceMemRange)
{
sem_wait(&s->SEM_BUS_Key); // Acquire the key to access BUS
s->sh_mem->CMD = CAS_SYSBUS_CMD_Read;
s->sh_mem->Address = offset;
sem_post(s->SEM_BUS);
sem_wait(s->SEM_BUS_Ack);
ret = s->sh_mem->Data;
sem_post(&s->SEM_BUS_Key); // Release the key
}
break;
}
return ret;
}
static void caslab_sysbridge_write(void *opaque, hwaddr offset, uint64_t value, unsigned size)
{
CASLabBridgeState *s = opaque;
if (offset >= DeviceMemRange)
{
fprintf(stderr, "Device ERROR: Overflow!!!\n");
return;
}
if (!s->_connected)
return;
sem_wait(&s->SEM_BUS_Key); // Acquire the key to access BUS
s->sh_mem->CMD = CAS_SYSBUS_CMD_Write;
s->sh_mem->Address = offset;
s->sh_mem->Data = (uint32_t)value;
sem_post(s->SEM_BUS);
sem_wait(s->SEM_BUS_Ack);
sem_post(&s->SEM_BUS_Key); // Release the key
}
static void caslab_sysbridge_reset(DeviceState *dev)
{
CASLabBridgeState *s = CASLAB_SYSBRG(dev);
if (!s->_connected)
return;
sem_wait(&s->SEM_BUS_Key); // Acquire the key to access BUS
s->sh_mem->CMD = CAS_SYSBUS_CMD_Reset;
sem_post(s->SEM_BUS);
sem_wait(s->SEM_BUS_Ack);
sem_post(&s->SEM_BUS_Key); // Release the key
}
static void caslab_sysbridge_realize(DeviceState *dev, Error **errp)
{
// Nothing to do here currently.
}
static const MemoryRegionOps caslab_sysbridge_ops = {
.read = caslab_sysbridge_read,
.write = caslab_sysbridge_write,
.endianness = DEVICE_NATIVE_ENDIAN,
.valid.min_access_size = 4,
.valid.max_access_size = 4,
};
static void caslab_sysbridge_init(Object *obj)
{
SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
CASLabBridgeState *s = CASLAB_SYSBRG(obj);
int init_state = 5;
struct shmid_ds buf;
s->_connected = false;
s->shm_id = -1;
memory_region_init_io(&s->iomem, OBJECT(s), &caslab_sysbridge_ops, s,
TYPE_CASLAB_SYSBRG, DeviceMemRange);
sysbus_init_mmio(sbd, &s->iomem);
sysbus_init_irq(sbd, &s->irq);
do
{
if (0 != sem_init(&s->SEM_BUS_Key, 0, 1)) // Assign only 1 key to act like mutex
break;
init_state--;
// 和 qsbridge.cpp 共用 semaphore
s->SEM_BUS = sem_open(CAS_SYSBRG_DATASEM, O_CREAT, S_IRUSR | S_IWUSR, 0);
s->SEM_BUS_Ack = sem_open(CAS_SYSBRG_ACKSEM, O_CREAT, S_IRUSR | S_IWUSR, 0);
s->SEM_IRQ = sem_open(CAS_SYSBRG_IRQSEM, O_CREAT, S_IRUSR | S_IWUSR, 0);
if (SEM_FAILED == s->SEM_BUS || SEM_FAILED == s->SEM_BUS_Ack || SEM_FAILED == s->SEM_IRQ)
break;
init_state--;
// Reset sems
while (sem_trywait(s->SEM_BUS) == 0)
;
while (sem_trywait(s->SEM_BUS_Ack) == 0)
;
while (sem_trywait(s->SEM_IRQ) == 0)
;
s->shm_id = shmget(CAS_SYSBRG_SMID, sizeof(CAS_SYSBUS_DATA), 0666 | IPC_CREAT);
if (s->shm_id == -1)
break;
init_state--;
s->sh_mem = shmat(s->shm_id, NULL, 0);
if (s->sh_mem == (void *)-1)
break;
init_state--;
memset((void *)s->sh_mem, 0, sizeof(CAS_SYSBUS_DATA));
if (0 != pthread_create(&IRQ_Thread, NULL, Thread_UpdateIRQ, (void *)s))
break;
init_state--;
} while (0);
switch (init_state)
{
case 0:
DB_PRINT("SYSBUS INIT\n");
break;
case 1: // pthread_create failed
fprintf(stderr, "ERROR: pthread failed\n");
shmdt((void *)s->sh_mem);
s->sh_mem = NULL;
case 2: // shmat failed
fprintf(stderr, "ERROR: shmat failed\n");
if (shmctl(s->shm_id, IPC_STAT, &buf) == 0)
shmctl(s->shm_id, IPC_RMID, &buf);
case 3: // shmget failed
fprintf(stderr, "ERROR: shmget failed\n");
while (sem_trywait(s->SEM_BUS) == 0)
;
while (sem_trywait(s->SEM_BUS_Ack) == 0)
;
while (sem_trywait(s->SEM_IRQ) == 0)
;
sem_close(s->SEM_BUS);
sem_close(s->SEM_BUS_Ack);
sem_close(s->SEM_IRQ);
case 4: // shared semaphore failed
fprintf(stderr, "ERROR: semaphore failed\n");
sem_destroy(&s->SEM_BUS_Key);
case 5: // anonymous semaphore failed
fprintf(stderr, "ERROR: semaphore failed\n");
default:
exit(-1);
break;
}
_dev = s;
if (0 != atexit(ON_Module_Exit)) // 註冊正常 exit/return 時要執行的 function
fprintf(stderr, "failed to register on exit function, may cause memory violation when exit.\n");
}
static Property caslab_sysbridge_properties[] = {
DEFINE_PROP_CHR("chardev", CASLabBridgeState, chr),
DEFINE_PROP_END_OF_LIST(),
};
static void caslab_sysbridge_class_init(ObjectClass *oc, void *data)
{
DeviceClass *dc = DEVICE_CLASS(oc);
dc->realize = caslab_sysbridge_realize;
dc->vmsd = &vmstate_caslab_sysbridge;
dc->reset = caslab_sysbridge_reset;
dc->props_ = caslab_sysbridge_properties;
}
static const TypeInfo caslab_sysbridge_info = {
.name = TYPE_CASLAB_SYSBRG,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(CASLabBridgeState),
.instance_init = caslab_sysbridge_init,
.class_init = caslab_sysbridge_class_init};
static void caslab_sysbridge_register_types(void)
{
type_register_static(&caslab_sysbridge_info);
}
type_init(caslab_sysbridge_register_types)
```
:::
### 設定
[calculator 設定](https://hackmd.io/p4MBgIlaRbm7LBsd5Zh1gw?both#%E8%A8%AD%E5%AE%9A) 的 `qemu-6.2.0-rc2/hw/char/meson.build` 內 `caslab_calculator.c` 改為 `caslab_sysbridge.c`、`qemu-6.2.0-rc2/hw/arm/realview.c` 內 `caslab_calc` 改為 `caslab_sysbridge`
### Kernel Module
和 [calculator Kernel Module](https://hackmd.io/p4MBgIlaRbm7LBsd5Zh1gw?both#Kernel-Module) 相同
### Device Tree
和 [calculator Device Tree](https://hackmd.io/p4MBgIlaRbm7LBsd5Zh1gw?both#Device-Tree) 相同