opensbi串口驱动详解
一.
前言
本文分享
OpenSBI
串口驱动相关的实现
,
并实践添加自己的串口驱动。
整个框架如下
二.
串口驱动相关
Kconfig
与
Makefile
2.1 Kconfig
要了解串口驱动的相关结构,最好的方式是从
Kconfig
和
Makefile
入手,看到底编译了哪些文件,结构是怎么样的。
https://mp.weixin.qq.com/s/941A4li9L6kq1QB-fuyuVA opensbi
单独构建与
GDB
仿真调试一文中介绍了
Kconfig
的结构。这里重点关注串口驱动相关的。
输入如下命令进入配置界面
:
make PLATFORM=generic menuconfig
如下路径
Utils and Drivers Support --->
Serial Device Support--->
对应的文件就是
lib/utils/serial/Kconfig
该
Kconfig
下,首先有个总的配置
FDT_SERIAL,
该配置选中,下面才会有各个
driver
选择。
FDT_SERIAL
依赖于
FDT
配置。
而
FDT
配置是在
platform/generic/Kconfig
中,选中平台时自动选中的
(make PLATFORM=generic menuconfig
时自动选择的是
platform/generic/Kconfig)
。
Platform options --->
选中某个
driver
配置时,自动会
select
对应的串口底层实现
比如选中
config FDT_SERIAL_CADENCE
时自动选择
select SERIAL_CADENCE
。
2.2. Makefile
lib/utils/serial/objects.mk
中就会根据上述
Kconfig
配置,是否添加对应的
c
文件进行编译。
libsbiutils-objs-
$(
CONFIG_FDT_SERIAL
)
+= serial/fdt_serial.o
libsbiutils-objs-
$(
CONFIG_FDT_SERIAL
)
+= serial/fdt_serial_drivers.carray.o
fdt_serial.
c
始终会编译
fdt_serial_drivers.carray.
c
是脚本自动生成的,后面介绍
如下驱动代码按照配置添加
carray-fdt_serial_drivers-
$(
CONFIG_FDT_SERIAL_CADENCE
)
+= fdt_serial_cadence
libsbiutils-objs-
$(
CONFIG_FDT_SERIAL_CADENCE
)
+= serial/fdt_serial_cadence.o
......
如下再根据配置添加串口底层实现文件
libsbiutils-objs-
$(
CONFIG_SERIAL_CADENCE
)
+= serial/cadence-uart.o
......
上面提到的
build/platform/generic/lib/utils/serial/fdt_serial_drivers.carray.c
在
Makefile
中如下实现
$(
platform_build_dir
)
/%.carray.c:
$(
platform_src_dir
)
/%.carray
$(
call
compile_carray,
$@
,
$<
)
实现如下
compile_carray
=
$(
CMD_PREFIX
)
mkdir -p `dirname
$(
1
)
`;
\
echo " CARRAY
$(
subst
$(
build_dir
)/,,$(
1
))
";
\
$(
eval
CARRAY_VAR_LIST := $(
carray-
$(
subst
.carray.c,,$(
shell
basename $(
1
)))
-y
))
\
$(
src_dir
)
/scripts/carray.sh -i
$(
2
)
-l "
$(
CARRAY_VAR_LIST
)
" >
$(
1
)
即调用的脚本
scripts/carray.sh
根据
lib/utils/serial/fdt_serial_drivers.carray
HEADER: sbi_utils/serial/fdt_serial.h
//
头文件
TYPE: struct fdt_serial
//
数组类型
NAME: fdt_serial_drivers
//
指定数组结构名字,可以通过该全局变量索引指定的驱动
生成
build/platform/generic/lib/utils/serial/fdt_serial_drivers.carray.c
实际就是生成了全局数组 fdt_serial_drivers ,这个数组的成员即各个串口驱动的实现
// Generated with carray.sh from fdt_serial_drivers.carray
extern struct fdt_serial fdt_serial_cadence;
extern struct fdt_serial fdt_serial_gaisler;
extern struct fdt_serial fdt_serial_htif;
extern struct fdt_serial fdt_serial_renesas_scif;
extern struct fdt_serial fdt_serial_shakti;
extern struct fdt_serial fdt_serial_sifive;
extern struct fdt_serial fdt_serial_litex;
extern struct fdt_serial fdt_serial_uart8250;
extern struct fdt_serial fdt_serial_xlnx_uartlite;
struct fdt_serial *fdt_serial_drivers[] = {
&fdt_serial_cadence,
&fdt_serial_gaisler,
&fdt_serial_htif,
&fdt_serial_renesas_scif,
&fdt_serial_shakti,
&fdt_serial_sifive,
&fdt_serial_litex,
&fdt_serial_uart8250,
&fdt_serial_xlnx_uartlite,
};
unsigned long fdt_serial_drivers_size = sizeof(fdt_serial_drivers) / sizeof(struct fdt_serial *);
2.3
设备树
串口驱动上面可以选择同时支持多个,根据设备树来匹配具体是哪一个
sudo apt-get install device-tree-compiler
获取
c-sky/buildroot
的设备树。
dump
设备树,注意逗号
./host/csky-qemu/bin/qemu-system-riscv64 -M virt,dumpdtb=dump.dtb
反编译
dtc -o dump.dts -O dts -I dtb dump.dtb
将
dtb
文件放置于
platform/generic
目录下
make PLATFORM=
generic
FW_TEXT_START
=0x80000000
FW_FDT_PATH=/home/qinyunti/opensbi/platform/
generic
/dump.dtb
Dts
串口相关内容如下,这是
qemu
的串口,根据实际平台修改。
chosen
和
uart
两个节点必须要有。
/dts-v1/;
/ {
#address-cells = <0x02>;
#size-cells = <0x02>;
compatible = "riscv-virtio";
model = "riscv-virtio,qemu";
chosen {
bootargs = [00];
stdout-path = "/uart@10000000";
};
uart@10000000 {
interrupts = <0x0a>;
interrupt-parent = <0x03>;
clock-frequency = <1000000>;
current-speed = <115200>;
reg-shift = <0>;
reg-io-width = <1>;
reg-offset = <0>;
reg = <0x00 0x10000000 0x00 0x100>;
compatible = "xxx,xxx-uart";
};
......
lib/utils/serial/fdt_serial.c
中
fdt_serial_init
中
会匹配设备树的串口节点的
compatible
和驱动中的
serial_xxx_match
中的
compatible
,
一致则调用该驱动的实现。
三.
添加自定义驱动
以下以添加自定义驱动为例分享整个过程
3.1.
添加源码
lib/utils/serial/
下添加
fdt_serial_xxx.c
和
xxx_uart.c
前者实现
struct fdt_serial fdt_serial_
xxx
= {
.match_table = serial_
xxx
_match,
.init = serial_
xxx
_init
};
后者实现
int
xxx
_uart_init(unsigned long base, u32 in_freq, u32 baudrate)
并调用
sbi_console_set_device
注册标准输入输出接口。
驱动代码以
qemu
的为例,根据实际修改
lib/utils/serial/xxx-uart.c
内容如下
/*
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2019 .
*
* Authors:
*
*/
/* clang-format off */
/* clang-format on */
static volatile char *xxx_uart_base;
static u32 xxx_uart_in_freq;
static u32 xxx_uart_baudrate;
static u32 xxx_uart_reg_width;
static u32 xxx_uart_reg_shift;
static u32 get_reg(u32 num)
{
u32 offset = num << xxx_uart_reg_shift;
if (xxx_uart_reg_width == 1)
return readb(xxx_uart_base + offset);
else if (xxx_uart_reg_width == 2)
return readw(xxx_uart_base + offset);
else
return readl(xxx_uart_base + offset);
}
static void set_reg(u32 num, u32 val)
{
u32 offset = num << xxx_uart_reg_shift;
if (xxx_uart_reg_width == 1)
writeb(val, xxx_uart_base + offset);
else if (xxx_uart_reg_width == 2)
writew(val, xxx_uart_base + offset);
else
writel(val, xxx_uart_base + offset);
}
static void xxx_uart_putc(char ch)
{
while ((get_reg(UART_LSR_OFFSET) & UART_LSR_THRE) == 0)
;
set_reg(UART_THR_OFFSET, ch);
}
static int xxx_uart_getc(void)
{
if (get_reg(UART_LSR_OFFSET) & UART_LSR_DR)
return get_reg(UART_RBR_OFFSET);
return -1;
}
static struct sbi_console_device xxx_uart_console = {
.name = "xxx_uart",
.console_putc = xxx_uart_putc,
.console_getc = xxx_uart_getc
};
int xxx_uart_init(unsigned long base, u32 in_freq, u32 baudrate, u32 reg_shift,
u32 reg_width, u32 reg_offset)
{
u16 bdiv = 0;
volatile uint32_t* tmp = (uint32_t*)0x10000004;
*tmp = 0x11223344;
if(*tmp != 0x11223344)
{
while(1);
}
xxx_uart_base = (volatile char *)base + reg_offset;
xxx_uart_reg_shift = reg_shift;
xxx_uart_reg_width = reg_width;
xxx_uart_in_freq = in_freq;
xxx_uart_baudrate = baudrate;
if (xxx_uart_baudrate) {
bdiv = (xxx_uart_in_freq + 8 * xxx_uart_baudrate) /
(16 * xxx_uart_baudrate);
}
/* Disable all interrupts */
set_reg(UART_IER_OFFSET, 0x00);
/* Enable DLAB */
set_reg(UART_LCR_OFFSET, 0x80);
if (bdiv) {
/* Set divisor low byte */
set_reg(UART_DLL_OFFSET, bdiv & 0xff);
/* Set divisor high byte */
set_reg(UART_DLM_OFFSET, (bdiv >> 8) & 0xff);
}
/* 8 bits, no parity, one stop bit */
set_reg(UART_LCR_OFFSET, 0x03);
/* Enable FIFO */
set_reg(UART_FCR_OFFSET, 0x01);
/* No modem control DTR RTS */
set_reg(UART_MCR_OFFSET, 0x00);
/* Clear line status */
get_reg(UART_LSR_OFFSET);
/* Read receive buffer */
get_reg(UART_RBR_OFFSET);
/* Set scratchpad */
set_reg(UART_SCR_OFFSET, 0x00);
sbi_console_set_device(&xxx_uart_console);
return sbi_domain_root_add_memrange(base, PAGE_SIZE, PAGE_SIZE,
(SBI_DOMAIN_MEMREGION_MMIO |
SBI_DOMAIN_MEMREGION_SHARED_SURW_MRW));
}
lib/utils/serial/fdt_serial_xxx.c
内容如下
/*
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) .
*
* Authors:
*
*/
static int serial_xxx_init(const void *fdt, int nodeoff,
const struct fdt_match *match)
{
int rc;
struct platform_uart_data uart = { 0 };
rc = fdt_parse_uart_node(fdt, nodeoff, &uart);
if (rc)
return rc;
return xxx_uart_init(uart.addr, uart.freq, uart.baud,
uart.reg_shift, uart.reg_io_width,
uart.reg_offset);
}
static const struct fdt_match serial_xxx_match[] = {
{ .compatible = "xxx,xxx-uart" },
{ },
};
struct fdt_serial fdt_serial_xxx = {
.match_table = serial_xxx_match,
.init = serial_xxx_init,
};
添加
include/sbi_utils/serial/xxx-uart.h
文件
内容如下
/*
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2022 Technology Co., Ltd.
*
* Author:
*/
int xxx_uart_init(unsigned long base, u32 in_freq, u32 baudrate);
1.
配置
Kconfig
lib/utils/serial/Kconfig
config FDT_SERIAL_XILINX_UARTLITE
后添加
config FDT_SERIAL_XXX
bool
"XXX UART FDT driver"
select SERIAL_XXX
default
n
config SERIAL_XILINX_UARTLITE
后添加
config SERIAL_XXX
bool
"XXX UART support"
default
n
3.
配置
objects.mk
lib/utils/serial/objects.mk
添加
carray-fdt_serial_drivers-
$(
CONFIG_FDT_SERIAL_XXX
)
+= fdt_serial_xxx
libsbiutils-objs-
$(
CONFIG_FDT_SERIAL_XXX
)
+= serial/fdt_serial_xxx.o
libsbiutils-objs-
$(
CONFIG_SERIAL_XXX
)
+= serial/xxx-uart.o
4.
修改设备树
uart@10000000 {
的
compatible
改为和
lib/utils/serial/fdt_serial_xxx.c
中的
serial_xxx_match
一致
static
const
struct
fdt_match
serial_xxx_match
[]
=
{
{ .
compatible
=
"xxx,xxx-uart"
},
{ },
};
chosen {
bootargs = [00];
stdout-path = "/uart@10000000";
};
uart@10000000 {
interrupts = <0x0a>;
interrupt-parent = <0x03>;
clock-frequency = <1000000>;
current-speed = <115200>;
reg-shift = <0>;
reg-io-width = <32>;
reg-offset = <0>;
reg = <0x00 0x10000000 0x00 0x100>;
compatible = "xxx,xxx-uart";
};
编译设备树
dtc -I dts -O dtb -o xxx-uart.dtb platform/generic/qemu_dump.dts
5.
编译
export CROSS_COMPILE=~/buildroot/thead_920v2_enhanced_5.10_glibc_br_defconfig/host/bin/riscv64-unknown-linux-gnu-
export PLATFORM_RISCV_XLEN=64
make PLATFORM=
generic menuconfig
Utils and Drivers Support --->
Serial Device Support--->
make PLATFORM=
generic
FW_TEXT_START
=0x80000000 FW_FDT_PATH=/home/qinyunti/opensbi/platform/generic/xxx-uart.dtb
运行即可看到 qemu 的打印。
四.
标准输入输出重定向
4.1
输入输出接口
前面看到了打印,继续来看下,打印是如何实现的
串口初始化时注册底层读写接口
sbi_console_set_device(&xxx_uart_console);
static struct sbi_console_device xxx_uart_console = {
.name = "xxx_uart",
.console_putc = xxx_uart_putc,
.console_getc = xxx_uart_getc
};
还可以实现
console_puts
打印字符串可以提高效率。
lib/sbi/sbi_console.c
中设置全局变量
console_dev = dev;
sbi_printf
->print->printc->sbi_putc->nputs_all->nputs->console_dev->console_putc
或者
console_dev->console_puts
sbi_ecall_dbcn_handler->sbi_ngets/sbi_gets->sbi_getc->console_dev->console_getc
4.2
打印信息
这里来看下相关信息是在哪打印的
以下
BUILD_INFO=y
可以打印更多信息
make PLATFORM=generic FW_TEXT_START=0x80000000 FW_FDT_PATH=/home/qinyunti/opensbi/platform/generic/xxx-uart.dtb BUILD_INFO=y
Makefile
中
# Build Info:
# OPENSBI_BUILD_TIME_STAMP -- the compilation time stamp
# OPENSBI_BUILD_COMPILER_VERSION -- the compiler version info
BUILD_INFO ?= n
ifeq ($(BUILD_INFO),y)
OPENSBI_BUILD_DATE_FMT = +%Y-%m-%d %H:%M:%S %z
ifdef SOURCE_DATE_EPOCH
OPENSBI_BUILD_TIME_STAMP ?= $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" \
"$(OPENSBI_BUILD_DATE_FMT)" 2>/dev/null || \
date -u -r "$(SOURCE_DATE_EPOCH)" \
"$(OPENSBI_BUILD_DATE_FMT)" 2>/dev/null || \
date -u "$(OPENSBI_BUILD_DATE_FMT)")
else
OPENSBI_BUILD_TIME_STAMP ?= $(shell date "$(OPENSBI_BUILD_DATE_FMT)")
endif
OPENSBI_BUILD_COMPILER_VERSION=$(shell $(CC) -v 2>&1 | grep ' version ' | \
sed 's/[[:space:]]*$$//')
endif
ifeq ($(BUILD_INFO),y)
GENFLAGS += -DOPENSBI_BUILD_TIME_STAMP="\"$(OPENSBI_BUILD_TIME_STAMP)\""
GENFLAGS += -DOPENSBI_BUILD_COMPILER_VERSION="\"$(OPENSBI_BUILD_COMPILER_VERSION)\""
endif
ifeq ($(BUILD_INFO),y)
$(build_dir)/lib/sbi/sbi_init.o: $(libsbi_dir)/sbi_init.c FORCE
$(call compile_cc,$@,$<)
endif
lib/sbi/sbi_init.c
中
sbi_boot_print_banner
打印
LOGO
和编译版本信息
OPENSBI_BUILD_TIME_STAMP
和
OPENSBI_BUILD_COMPILER_VERSION
就是
BUILD_INFO=y
时自动生成。
static void sbi_boot_print_banner(struct sbi_scratch *scratch)
{
if (scratch->options & SBI_SCRATCH_NO_BOOT_PRINTS)
return;
sbi_printf("\nOpenSBI %s\n", OPENSBI_VERSION_GIT);
sbi_printf("\nOpenSBI v%d.%d\n", OPENSBI_VERSION_MAJOR,
OPENSBI_VERSION_MINOR);
sbi_printf("Build time: %s\n", OPENSBI_BUILD_TIME_STAMP);
sbi_printf("Build compiler: %s\n", OPENSBI_BUILD_COMPILER_VERSION);
sbi_printf(BANNER);
}
打印如下
五.
总结
串口驱动的实现实际是通过脚本,将各个串口驱动
c
文件中的
struct fdt_serial
全局变量,自动组合生成一个全局数组
fdt_serial_drivers
,该数组的成员就是各个串口驱动的实现
struct fdt_serial
。通过全局变量
fdt_serial_drivers
即可索引各个驱动,通过设备树节点的
compatible
来匹配驱动。