随着科技的发展,人们对嵌入式设备的性能和运行效率要求越来越苛刻,传统嵌入式设备存在非常单一的流向、极低的CPU利用率等缺点,实时性对于汽车、航天领域至关重要,可能会因为处理器没有及时响应任务而造成极大的损失。针对这一不足,提出将嵌入式操作系统移植到微处理器中以提高运行效率和稳定性。RT Thread作为一款实时、抢占式多任务操作系统,在智能家居、航天、安防、可穿戴电子产品方面应用广泛,势必会成为未来AIoT(人工智能物联网)平台主流的操作系统。
为了抢先一步占领市场,将RT Thread移植到各大厂商的芯片上具有极其重要的现实意义,但是目前并没有将RT Thread移植到BL602的成熟方案,并且移植具有一定的难度。针对这一现象,本文提出了将RT Thread移植到BL602的方法和关键步骤。
1 软硬件环境概述
1.1 硬件概述
本文所采用的BL602 芯片使用RISCV 架构的CPU,其超低功耗的电源管理单元可用于高性能应用开发,采用外部时钟源可以获得高精度和稳定的时钟,另外还可以选配闪存(Flash),高速缓存可以加快CPU 访问存储器的速率,大大提高CPU 利用率和程序执行速率。
1.2 软件准备
本文采用RT Thread 3.1.1标准版本的源码进行裁剪移植,为了避免从头开始编写代码,提前在博流官网下载bl_mcu_sdk代码用作参考。
2 RT Thread工程框架启动原理
2.1 总体启动流程
整个工程启动流程分为BL602芯片启动和RT Thread实时内核启动。该过程与U boot启动过程类似。对于前者,芯片上电后会首先从start.S汇编代码处开始执行,最后通过调用entry()进入应用程序入口,进而调用内核启动函数rtthread_startup()将系统控制权转移给RT Thread,具体流程如图1所示。
图1 总体启动流程
2.2 RT Thread实时内核启动流程
RT Thread实时内核启动主要由rtthread_startup()函数完成,主要进行板级初始化、打印RT Thread版本信息、定时器初始化、调度器初始化、应用程序初始化、定时器线程初始化、空闲线程初始化、调度器启动。
2.2.1 板级初始化
板级初始化过程就是通过调用rt_hw_board_init()函数实现,对于不同型号的芯片,该函数所要处理的任务大同小异,一般都是初始化芯片引脚和串口、配置系统时钟、初始化堆空间。对于BL602芯片,因其内部自带了一个64位的定时器mtimer,将会对该定时器进行初始化以设置系统定时时间,其作用与STM32芯片中的嘀嗒时钟类似。
2.2.2 定时器初始化
调用rt_system_timer_init()函数初始化的是硬件中断模式下的定时器,RT Thread的硬件定时器由一个静态的双向链表构成,刚开始初始化定时器队列为空,即L>next=L >pre=L,具体结构如图2所示。
图2 初始化后的定时器结构
2.2.3 调度器初始化
调度器用于调度线程运行,在当前系统的就绪队列中找到优先级最高的就绪线程来执行,RTThread中线程一共有32个等级的优先级,数值越大,优先级越低。调度器初始化主要对线程优先队列进行初始化为NULL,设置当前线程优先级:RT_THREAD_PRIORITY_MAX-1=31为最低的优先级,初始化线程控制块指针为NULL,初始化线程就绪优先级组为0。
2.2.4 应用程序初始化
通过调用rt_application_init()来动态创建main主线程,在线程函数main_thread_entry()调用main(),从而进入用户主函数。不过此时线程并没有启动,而是根据优先级插入到了线程就绪优先队列指定位置,等到调度器启动后才会真正运行主线程。
2.2.5 调度器启动
通过调用rt_system_scheduler_start()启动调度器,main线程、idle线程才会真正被调度,系统会从线程就绪队列中选择优先级最高的一个线程并执行。如果系统的就绪队列中还有线程1(优先级为7)、线程2(优先级为10)、线程3(优先级为7)、tshell(优先级为20)四个线程。当调度器启动后,系统中各个线程指向关系如图3所示。
图3 线程指向关系
3 RT Thread移植过程
RT Thread移植就是让RTThread实时操作系统能够在BL602芯片上跑起来,并且可以实现任务管理、任务创建、任务调度等功能。移植过程主要分为以下几个部分:启动入口、系统时钟配置、mtimer定时器配置、注册中断回调函数、实现Finsh功能、rt_kprintf实现、任务创建和任务调度。下面将对具体步骤进行详细介绍。
3.1 添加单板BL602和修改源码目录
为了移植最小内核到BL602芯片上,有必要对RT Thread源码进行裁剪。保留src、include目录文件不变,components目录下面只保留finsh文件夹,bsp目录下添加BL602单板目录,在libcpu/risc_v目录下新建一个bl602文件夹,向libcpu/risc_v/bl602中添加对应的 interrupt_gcc.S、cpuport.c、context_gcc.S,这几个汇编和C文件可以从其他RISC V 架构下的芯片中复制,CPU 架构移植主要实现如下功能:中断使能/失能、任务切换、线程栈初始化、中断处理等。把bl_mcu_sdk中的drivers/soc/bl602/startup/start.S 启动文件复制到libcpu/risc_v/bl602目录下,作为芯片上电启动文件。rtconfig.h头文件中定义了一些预编译宏,只需定义一些基本的宏就行,至此源码裁剪工作基本完成。
3.2 修改入口函数
裸机程序中启动文件start.S中最后几行汇编指令如下所示(现在需要修改jal main为 jalentry,从而跳转到应用程序入口调用rtthread_startup()启动函数启动内核,至此系统控制权就转交给了操作系统):
//系统初始化,保存默认配置、清除所有中断
jal SystemInit
/*从ITCM、DTCM、RAM 复制数据到TCM 中,并且清空.bss区*/
jal start_load
//设置PDS、HBN时钟
jal System_Post_Init
……
//跳转到用户主程序
jal main 修改为jalentry
3.3 适配板级初始化函数
对开发板的初始化操作一般都在rt_hw_board_init()接口进行,比如系统时钟初始化、外设时钟初始化、定时器初始化、串口初始化、GPIO初始化、堆初始化等。
3.3.1 系统时钟初始化
视系统需求,外部晶振时钟可选24、32、38.4、40 MHz,可以设置外部晶振XTAL为40 MHz,配置系统时钟为最高的192 MHz。通过配置clk_cfg0寄存器(地址为0x40000000)的bit[0:3]为1,使能PLLCLK、BCLK、FCLK、HCLK;通过设置bit[4:5]都为1,选择PLL输出时钟为192 MHz;设置bit[6:7]都为1,选择root clock来自RC32M(RC振荡器频率为32 MHz);最后选择PLL 输出192MHz作为system clock。
3.3.2 外设时钟初始化
首先使能外围设备时钟,然后再进行配置。BL602芯片的外围设备时钟包括Flash、UART、I2C、GPIO 等,这里只用到了UART 外围设备,bl_mcu_sdk在peripheral_clock_init()只需要执行两个操作:一是使能UART 时钟(通过写0x4000 0024的第16位为1);二是配置UART时钟为160 MHz(通过配置0x40000008的bit7为选择时钟源,通过0x4000 0008的bit[0:2]设置分频系数为0,设置bit4为1使能串口时钟)。
3.3.3 配置mtimer定时器
mtimer定时器是RISC V 内核自带的一个64位定时器,可以通过配置0x4000 0090寄存器来使能mtimer时钟和选择时钟类型,还可以选择分频系数,与STM32芯片里面的嘀嗒时钟类似。bl_mcu_sdk的中断采用vector mode,一共有18个中断源,中断号0~15为RISCV保留中断,而mtimer中断号是7,UART0中断号是(IRQ_NUM_BASE+29),其中IRQ_NUM_BASE为16,可以在rt_hw_board_init()中调用bflb_mtimer_config(1000000,SysTick_Handler),从而将mtimer中断号与中断服务函数SysTick_Handler绑定在一起,假如时钟经过分频以后为 1 MHz,经过以上设置后,mtimer定时时间即为1 s。
3.3.4 串口初始化
主要对引脚和UART 外设进行初始化,bl_mcu_sdk中有个函数已经实现该功能,可以直接在rt_hw_board_init()里调用console_init(),该函数里面调用bflb_gpio_uart_init()实现了GPIO输入输出引脚的初始化以及调用bflb_uart_init()实现了uart0初始化。
3.3.5 堆初始化
和其他操作系统不同的是,RT Thread堆空间大小由程序员自定义的一个静态数组heap_buf[]决定,数组大小不能超过SRAM 最大值,然后通过rt_system_heap_init(begin_addr, end_addr)函数对堆空间的起始和终止地址进行初始化,此处应为heap_buf和(heap_buf + sizeof(heap_buf) 1),并对begin_addr进行向上4字节对齐操作,对end_addr进行向下4字节对齐操作。在rtconfig.h头文件中定义RT_USING_HEAP 宏后才能动态创建main主线程,否则只能通过静态方法创建线程。
3.4 实现rt_kprintf功能
要想实现RT Thread的打印功能,其实就是实现rt_hw_console_output()函数向串口输出数据,BL602参考手册里面有两个寄存器需要设置:一个是uart_fifo_config_1,地址为:0x4000 a084,需要设置bit[0:5]位用于TX FIFO可用计数;另外一个是uart_fifo_wdata,地址为:0x4000a088,用来将一个字符写进0x4000 a088地址里,bl_mcu_sdk里面已经实现过该部分,直接在rt_hw_console_output()里面调用即可,至此可以通过串口uart0输出数据。
3.5 移植Finsh
移植Finsh组件,实现命令行交互功能,首先需要添加Finsh代码,在rtconfig.h头文件中定义RT_USING_FINSH以及与Finsh线程相关的一些宏定义,然后对接rt_hw_console_getchar()函数即可,其中获取字符有两种方式:采用查询方式或者中断方式,建议采用中断方式获取字符。
3.5.1 查询方式
查询方式下不能使能串口接收中断,且此方法相较于中断方式较为简单,不过效率很低,需要对两个寄存器进行配置:一个是uart_fifo_config_1,地址为0x4000 a084,需要设置bit[8:13]用于RX FIFO 可用计数;另外一个是uart_fifo_rdata,地址为0x4000 a08c,用于从0x4000 a08c地址中读取一个字符到串口。
3.5.2 中断方式
采取中断方式必须使能串口接收中断,参考BL602数据手册得知,可以通过配置uart_int_en寄存器bit3使能串口接收中断,寄存器地址为0x4000 a02c。中断方式获取字符流程如下:定义一个buffer缓冲区和一个信号量,当uart接收产生中断时,会在中断服务函数中将读取到的数据缓存到buffer缓冲区,然后释放信号量来通知finsh线程接收数据,finsh线程将从缓冲区中读取数据。所以还需要实现串口接收中断服务函数Uart_IRQHandler,并通过bflb_irq_attach(45, Uart_IRQHandler)将UART0中断号与之绑定在一起,UART0中断号为(IRQ_NUM_BASE+29),IRQ_NUM_BASE为16。
4 测试结果
操作系统启动后,会自动创建main、idle、tshell线程。下面将在main函数中动态创建3个线程,将线程1、3优先级设置为7,线程2优先级设置为10,tick都设置为10,在线程函数中都同时延时1 s,观察打印出的当前线程数量是否正确以及各个线程是否按照优先级正确被调度执行。实验结果表明,RT Thread操作系统能够在BL602上稳定运行,并且可以正常进行线程创建和调度,测试结果如图4所示。
图4 测试结果
5 结 语
本文在介绍了RT Thread实时操作系统基础上讲解了如何在BL602芯片上成功移植RT Thread实时操作系统的方法。最后在BL602上进行测试,可以正常运行,为其他RTOS移植到芯片上提供了参考和借鉴。
(本文由《单片机与嵌入式系统应用》杂志授权发表,原文刊发在2023年第10期)
全部0条评论
快来发表一下你的评论吧 !