openvela系统启动流程
启动流程概述
参考文档:<u>https://doc.openvela.com/document?id=614&version=dev&language=cn</u>
从openvela启动的第1条指令开始,会完成这些事情:
-
CPU0的底层初始化:nuttx/arch/arm/src/armv7-a/arm_head.S
设置MMU、Cache、建立页表,调用“arm_boot”初始化C程序运行环境,调用nuttx的入口函数“nx_start” -
openvela操作系统的入口:nx_start
“nx_start”函数的调用关系非常繁杂,主要流程如下(为了方便学员复制,下面先以文字形式展示):
__start
// apps/vendor/allwinnertech/chips/r528/r528_boot.c
bl arm_boot
arm_lowputc('A'); // 第1个字符的输出
b nx_start
nx_start
tasklist_initialize
idle_task_initialize();
drivers_early_initialize
nxsem_initialize
up_allocate_heap(&heap_start, &heap_size);
kumm_initialize(heap_start, heap_size);
idle_group_initialize
task_initialize
instrument_initialize
fs_initialize
irq_initialize
g_irqvector\[i\].handler = irq_unexpected_isr;
// vendor/allwinnertech/chips/r528/r528_irq.c
up_irqinitialize
arm_gic0_initialize
arm_gic_initialize
// \_vector_start 在 nuttx/arch/arm/src/armv7-a/arm_vectortab.S
cp15_wrvbar((uint32_t)\_vector_start);
irq_enable();
clock_initialize
nxsig_initialize
nxmq_initialize
net_initialize
binfmt_initialize
up_initialize
arm_serialinit // vendor/allwinnertech/chips/r528/r528_serial.c
up_earlyserialinit
r528_uart1config // 配置串口引脚
uart_register("/dev/console", &CONSOLE_DEV); // CONSOLE_DEV = g_uart1port
uart_register("/dev/uart1", &g_uart1port);
drivers_initialize // 一些不涉及具体硬件的驱动,比如/dev/null, /dev/zero
board_early_initialize // 执行板级早期硬件初始化
r528_early_initialize
nx_smp_start
nx_bringup
nx_create_initthread
nxthread_create("AppBringUp", nx_start_task, ...)
nx_start_task
nx_start_application
nx_romfsetc();
// romfs_img 来自 apps/vendor/allwinnertech/boards/
// r528/r528s3-velaevb1/src/etctmp.c
// etctmp.c是通过nuttx/boards/Board.mk生成的romfs映像文件
// Board.mk里的文件来自apps/vendor/allwinnertech/boards/
// r528/r528s3-evb4/src/Makefile
// 注册设备: /dev/ram0
romdisk_register(...,romfs_img, ...)
nx_mount(..."/dev/ram0", "/etc", "romfs"...);
// apps/vendor/allwinnertech/boards/r528/r528s3-velaevb1/src/r528_boot.c
board_late_initialize // 执行板级主要驱动的初始化
r528_late_initialize(); // 注册单板大部分驱动程序
r528_bringup(); // 挂载一些文件系统
ret = mount(NULL, "/proc", "procfs", 0, NULL);
task_spawn("nsh_main")
nsh_main
nsh_initialize
boardctl(BOARDIOC_INIT, 0);
// apps/vendor/allwinnertech/boards/r528/r528s3-evb4/src/r528_appinit.c
// 初始化应用层
board_app_initialize // 空函数
// /etc/init.d/rc.sysinit
nsh_sysinitscript(&pstate-\>cn_vtbl);
nsh_script_redirect("sysinit", "/etc/init.d/rc.sysinit");
// 没有配置CONFIG_BOARDCTL_FINALINIT
// 这个函数没有被调用, 放在这里只是为了跟前面的board_app_initialize进行对比
// 在sysinit之后初始化应用层
boardctl(BOARDIOC_FINALINIT, 0);
board_app_finalinitialize
nsh_initscript(&pstate-\>cn_vtbl);
nsh_script_redirect("init", "/etc/init.d/rcS");
nsh_consolemain
// 循环等待用户输入、解析、执行
sched_unlock(); // 执行到这里后,前面创建的AppBringUp进程才能执行
for (; ; )
{
up_idle();
}
下面使用图形方式把要留意的部分用红框标出来:

一些主要函数,列表如下:

启动流程代码走读
视频讲解,重点讲解board_XX_init等单板相关的函数,这些涉及移植工作。
参考文档:docs/zh-cn/chip_porting/porting_guide.md
arm_lowputc
arm_lowputc:单板相关的最低层打印函数。
在内核里使用syslog来打印信息时,最终会调用到它。在“docs/en/device_dev_guide/kernel/logging/syslog.md”中有如下说明:
// Application/Kernel Log Call
syslog() / _info()
|
v-- Formatting & Dispatch
nx_vsyslog()
|
v-- C Library Standard Formatting
lib_vsprintf()
|
v-- Write to Syslog Output Stream
stream_putc() --> syslogstream_putc()
|
v-- Iterate All Registered Channels & Differentiate Context
syslog_putc()
|
v-- Call Channel Driver's Output Function
g_syslog_channel[i]->sc_ops->sc_putc() // Task context
g_syslog_channel[i]->sc_ops->sc_force() // Interrupt context
要使用syslog,头文件和函数原型如下:
#include <syslog.h>
void syslog(int priority, const char \*format, ...);
// \`priority\`: The priority of the log, such as \`LOG_INFO\`, \`LOG_ERR\`, etc.
// \`format\`: A format string compatible with \`printf\` syntax.
控制台初始化(console)
为了使应用可以通过标准输入/输出(“stdin/out/err”)访问物理串口,必须注册串口驱动。
每个架构提供“<arch>_serialinit”接口,由厂商实现。
它内部调用“uart_register”注册控制台和其他串口设备节点。
arm_serialinit > up_earlyserialinit:用来初始化控制台。
board_late_initialize
单板的驱动注册,可以放在这个函数里。
openvela中断体系
ARMv7中断体系与处理流程
GIC:通用中断控制器(“Generic Interrupt Controller”)。GIC 是 ARM 公司开发的一个中断硬件 IP,主要用于管理外设中断,是联系外设中断和 CPU 的桥梁,也是各 CPU 之间中断互联的通道。它可以做到使能或禁止中断、把中断分组、在多核系统中将中断分配到不同处理器上、设置电平触发还是边沿触发方式等。
假设按键引脚被配置为中断功能,按键被按下或松开时,GPIO模块检测到信号发生变化后向GIC发出中断,GIC再向CPU发出中断。

CPU每执行完一条指令都会检查有无中断/异常产生,发现有中断/异常产生后,就会暂停当前的程序去处理中断/异常。
对于ARMv7架构,有如下异常向量表:

A7的向量表中,放置的是各类异常的“跳转指令”。
比如发生“Reset”异常时,CPU就会从向量表里找到第0项,执行“ldr pc, .Lresethandler”指令。
比如发生任何的中断时,CPU就会从向量表里找到第6项,执行“ldr pc, .Lirqhandler”指令去处理中断。
-
跳转之前,硬件只会保存CPSR寄存器
-
跳转之后,软件要保存现场
-
处理中断的函数执行完毕,返回之前,软件恢复现场、恢复CPSR寄存器
怎么处理中断呢?
在CPU的角度,它可以从GIC里读取到“发生了n号”中断,但是它无法知道中断起源是“GPIO的m号引脚”。所以,我们需要给“n号GIC中断”提供处理函数A。函数A要去读取GPIO控制器,分辨出是“m号引脚触发了中断”,然后调用它的处理函数B。
在上述场景里,我们需要提供两个中断处理函数:n号GIC中断处理函数A,m号GPIO引脚的中断处理函数B。
假设I2C0模块直接向GIC控制器发出“k号”中断,GIC的k号中断来源只有I2C0模块,那么可以给“k号GIC中断”只提供一个处理函数C。
openvela中断处理流程
总体框图如下(后面详细讲解):

异常向量表
设置向量表的流程如下:
nx_start
tasklist_initialize
idle_task_initialize();
drivers_early_initialize
nxsem_initialize
up_allocate_heap(&heap_start, &heap_size);
kumm_initialize(heap_start, heap_size);
idle_group_initialize
task_initialize
instrument_initialize
fs_initialize
irq_initialize
g_irqvector\[i\].handler = irq_unexpected_isr;
// vendor/allwinnertech/chips/r528/r528_irq.c
up_irqinitialize
arm_gic0_initialize
arm_gic_initialize
// \_vector_start 在 nuttx/arch/arm/src/armv7-a/arm_vectortab.S
cp15_wrvbar((uint32_t)\_vector_start);
irq_enable();
向量表在“nuttx/arch/arm/src/armv7-a/arm_vectortab.S”中:

从上到下的中断处理过程
代码流程如下:
// nuttx/arch/arm/src/armv7-a/arm_vectors.S
arm_vectorirq
bl arm_decodeirq /* Call the handler */
// nuttx/arch/arm/src/armv7-a/arm_gicv2.c
arm_decodeirq
// nuttx/arch/arm/src/armv7-a/arm_gicv2.c
irq = (regval & GIC_ICCIAR_INTID_MASK) \>\> GIC_ICCIAR_INTID_SHIFT;
arm_doirq(irq, regs);
// nuttx/sched/irq/irq_dispatch.c
irq_dispatch(irq, regs);
unsigned int ndx = irq;
vector = g_irqvector\[ndx\].handler;
arg = g_irqvector\[ndx\].arg;
CALL_VECTOR(ndx, vector, irq, context, arg);
vector(ndx, context, arg);
从下到上的中断注册过程
第1步:GIC中断处理函数的注册,代码如下:
// nuttx/sched/irq/irq_attach.c
r528_early_initialize
r528_gpio_init
hal_gpio_init
ret = hal_request_irq(gpio_desc-\>irq\[i\], gpio_irq_handle, irqname, (void \*)&gpio_desc-\>irq\[i\]);
hal_enable_irq(gpio_desc-\>irq\[i\]);
第2步,GPIO引脚的注册过程:
// 设备驱动程序使用GPIO中断
hal_gpio_to_irq(gpio_pin_t pin, uint32_t *irq)
hal_gpio_irq_request(uint32_t irq, hal_irq_handler_t hdle, unsigned long flags, void *data)
gpio_desc-\>irq_desc\[irq - GPIO_IRQ_START\].handle_irq = hdle;
gpio_desc-\>irq_desc\[irq - GPIO_IRQ_START\].flags = flags;
gpio_desc-\>irq_desc\[irq - GPIO_IRQ_START\].data = data;
gpio_irq_set_type
hal_gpio_pinmux_set_function(gpio_desc-\>irq_desc\[irq - GPIO_IRQ_START\].pin, GPIO_MUXSEL_EINT);
文件系统的挂载
在开发板上执行mount命令可以看到已经挂载的文件系统:
nsh> mount
/data type yaffs // 来自flash
/etc type romfs // /dev/ram0
/proc type procfs // 虚拟文件系统
/resource type romfs // 来自flash
/tmp type tmpfs // 虚拟文件系统
/etc的挂载
源码流程如下:
nx_start
nx_bringup
nx_create_initthread
nxthread_create("AppBringUp", nx_start_task, ...)
nx_start_task
nx_start_application
nx_romfsetc();
// romfs_img 来自 apps/vendor/allwinnertech/boards/r528/
// r528s3-velaevb1/src/etctmp.c
// etctmp.c是通过nuttx/boards/Board.mk生成的romfs映像文件
// Board.mk里的文件来自apps/vendor/allwinnertech/boards/
// r528/r528s3-evb4/src/Makefile
// 注册设备: /dev/ram0
romdisk_register(...,romfs_img, ...)
nx_mount(..."/dev/ram0", "/etc", "romfs"...);
/proc的挂载
源码流程如下:
nx_start
nx_bringup
nx_create_initthread
nxthread_create("AppBringUp", nx_start_task, ...)
nx_start_task
nx_start_application
nx_romfsetc();
board_late_initialize // 执行板级主要驱动的初始化
r528_late_initialize(); // 注册单板大部分驱动程序
r528_bringup(); // 挂载一些文件系统
ret = mount(NULL, "/proc", "procfs", 0, NULL);
/tmp的挂载
源码流程如下:
nx_start
nx_bringup
nx_create_initthread
nxthread_create("AppBringUp", nx_start_task, ...)
nx_start_task
nx_start_application
task_spawn("nsh_main")
nsh_main
nsh_initialize
// /etc/init.d/rc.sysinit
nsh_sysinitscript(&pstate-\>cn_vtbl);
nsh_script_redirect("sysinit", "/etc/init.d/rc.sysinit");
单板“/etc/init.d/rc.sysinit”文件的内容,来自如下源码:

Flash分区的挂载
源码流程如下:
nx_start
nx_bringup
nx_create_initthread
nxthread_create("AppBringUp", nx_start_task, ...)
nx_start_task
nx_start_application
task_spawn("nsh_main")
nsh_main
nsh_initialize
// /etc/init.d/rc.sysinit
nsh_sysinitscript(&pstate-\>cn_vtbl);
nsh_script_redirect("sysinit", "/etc/init.d/rc.sysinit");
nsh_initscript(&pstate-\>cn_vtbl);
nsh_script_redirect("init", "/etc/init.d/rcS");
单板“/etc/init.d/rcS”文件的内容,来自如下源码:

Init程序的启动
源码流程如下:
nx_start
nx_bringup
nx_create_initthread
nxthread_create("AppBringUp", nx_start_task, ...)
nx_start_task
nx_start_application
task_spawn("nsh_main")
nsh_main
nsh_initialize
// /etc/init.d/rc.sysinit
nsh_sysinitscript(&pstate-\>cn_vtbl);
nsh_script_redirect("sysinit", "/etc/init.d/rc.sysinit");
nsh_initscript(&pstate-\>cn_vtbl);
nsh_script_redirect("init", "/etc/init.d/rcS");
如果想让某个程序自动运行,以hello程序为例,可以修改:
~/vela-opensource/vendor/allwinnertech/boards/r528/r528s3-velaevb1/src/etc/init.d/rcS
在文件最后加入:
hello &
视频里细讲“/etc/init.d/rcS”里的“hello”如何被启动。
无论是脚本里命令的执行,还是在控制终端输入命令,执行流程都是这样:
-
根据“命令”找到内嵌的命令或内嵌的APP
-
执行它