本帖最后由 糜恒博 于 2019-8-30 12:40 编辑
rt-boot QQ群:751088367 声明:这只是一个uboot的mod版,不是一个全新的bootloader,我做这个主要是因为目前市面上缺少一个开源的多线程bootloader,想通过开源rt-uboot揭开其中的技术细节,方便大家学术交流,实现自己的多线程bootloader,同时为追求自主可控的应用场合提供一个可替代的多线程bootloader 目前该uboot_mod基本已经可以正常使用,根据GPL协议,公开其全部源代码:
Github上已经更新了全部代码 适用于ARM版(armv7[包括cortex-a7、cortex-a8和cortex-a9],主要有IPQ40XX、IPQ80XX和MT7622[以及4412、A10s、A20、A31s、A83T]和armv8[包括cortex-a53和cortex-a57,主要有MT7623、A64])已经全部完工,基于QEMU测试了两个平台(vexpress_ca9x4[cortex-a9]和xilinx_zcu102[cortex-a53])没有问题,其中ARMV8版的多线程RT-Thread内核的移植应该算是首发了[截止18-10-06,rt-thread的git上没有armv8的移植],而且ARMV8的内核同时支持EL1、EL2、EL3,应该在开源RTOS里也算首发了 QEMU仿真截图:左为armv7(cortex-a9)右为armv8(cortex-a53) 但是请注意,ARM版都是基于QEMU开发,没有实际开发板,没有实物的开发通常会有一些残留BUG,请具备救砖能力再刷
ARM版主要的设计难度就是不同的SOC其中断控制器和Timer没有统一标准,这点和MIPS相比差远了(MIPS全系列几乎一套代码都通用),即使是IPQ40XX和IPQ80XX其Timer都截然不同,而且ARMv7到ARMv8更是带来指令集等翻天覆地的变化 为了开发ARM版本我爸特意给我找出来很久之前的Tiny4412开发板,并根据Lintel前辈的指引测试了据说同是rt thread内核的superboot,但遗憾的是经过行为模型分析,superboot很可能只是uboot的改版,它不具备RTOS的特征,第一个ARM版RT-UBOOT在Tiny4412上测试通过,但是随后我才发现它也仅仅能在4412上运行,因为其他平台的中断控制器和Timer都需要重新移植,而这几乎等于一半以上的移植工作量 对于BCM平台将新开rt-cfe,不再并入rt-uboot【其实CFE本身很像是一种多任务OS(但是并不是),我觉得已经不错了,移植多线程OS上去没有太大意义】
一部分测试版已经发布,有兴趣的可以尝试一下:
包含如下机型的固件:
以下这些功能都是原版功能,除了增加多线程以外没改动啥(源码都放了想改啥想加啥可以自己改)
QCA/AR:上电按住reset,3s进入web failsafe模式,5s进入ttl串口终端,7s进入网络终端上电启动固件失败,会自动进入web failsafe模式MTK/RT:上电按住WPS,进入web failsafe模式,进入其他模式需要串口输数字
需要手工在电脑上设置ip
ip:192.168.1.x,网关192.168.1.1,升级地址192.168.1.1或192.168.1.111
启动串口输出如下:
更新界面如下:
另uboot.html art.html分别可更新对应的文件
串口下更新信息如下
启动固件时信息如下
MTK/RT平台楼主手里只有一个RT3352F,仅仅在RT3352F上工作测试正常【坛友反馈MT7688也正常】,其他SOC不知,RT3352F上截图如下:
ARMv7版在qemu仿真器下模拟执行ok(没有真实开发板测试)
ARMv8版在qemu仿真器下模拟执行ok(没有真实开发板测试) 章节更新到插入*1章
============2018-09-10============
第一章
本文的目的呢,并不是想宣传这个uboot_mod,而是想交流一下其设计原理 根据楼主计划,第一篇大致章节如下: 前言 1.uboot本身的坑 2.rtos-kernel选型 3.rtos-集成及遇到的问题 4.由于位置无关技术引发的特殊处理 5.demo 尾言
随更随写,后续篇幅将逐渐介绍更多功能及其实现机理,比如引入中断的驱动程序、mips calltrace、pcie设备和独立PHY的驱动和引入posix机制等...
现在先更第一篇(相应的,github也是对应第一篇的程序,后续的章节会随着发布而添加新的branch)
前言 楼主是如何想起来要做这个的呢? 这还要从头说起,楼主的妈妈是一个物联网工程师,主要做基于OpenWRT的各类软件开发,楼主的爸爸是某研究所的,主要做基于龙芯的底层开发。 因此作为一个从小接受uboot、openwrt熏陶的女汉子,楼主的童年就是在各种编译中度过的。 事情发生在一周前,当时一家人吃晚饭的时候,突然就聊到了uboot,当然也就聊到了某个著名的、功能强大的多线程bootloader,爸妈就聊起了整个bootloder的技术实现,我爸突然说起,他曾经设计过一个实时linux_mod,自带Linux镜像烧录功能,也算是个多线程bootloader吧。 说实话,我用某个著名的、功能强大的多线程bootloader已经很久了,可是一直不清楚多线程bootloader的技术细节,于是我表示对此很感兴趣,希望我爸能教我设计一个多线程bootloader,于是这就顺理成章的成了我这周的第二课堂作业。
终于经过五天晚上和两整天的努力,我完成了多线程bootloader的设计,由于是uboot魔改版,姑且叫rt-uboot吧。
1. uboot本身的坑 Uboot是一个非常古老的bootloader,可用于引导多种处理器平台,而QCA/AR/MTK/RT平台使用的是其中的1.1.4版,也算是很老的一版了,在其本身之中,存在着一些会对操作系统运行产生致命影响的深坑: (1)K0全局指针问题 Mips架构中,中断异常可以使用的寄存器是k0,k1,因此普通程序是不能使用的,因为它很有可能会在你眼皮子底下发生变化,但是在AR/QCA的uboot中,在没有进行RAM初始化之前,k0被当做全局数据指针使用,这会导致软件GG 但是,其实当RAM初始化之后,k0就已经没有必要使用了,那么不如我们将程序改一点,当RAM初始化后,使用全局变量维护全局数据。 (2)Timer使用问题 作为一个rtos,它的本质是在timer中断(或者其他中断)中对线程进行调度,因此timer很重要,但是uboot将timer用作delay函数的延时标志了,那么不如加个指示量,当rtos运行之后把函数指向rtos的delay函数好了。 (3)部分寄存器寻址问题 Mips在内核态默认寻址kseg0和kseg1,而uboot没有将实际地址映射到其中,直接导致tlb错误,这部分就直接改正吧。
2. rtos-kernel选型 在uboot中使用rtos,自己写其实也可以,楼主还是自己写过rtos的,但是,使用第三方rtos的好处是可以有大量的团队进行代码的维护,免去自己维护的尴尬,而且本身这套程序就打算开源,因此用开源的第三方rtos也是上上之选。 基本上可选的rtos如下: 1. freertos:最广泛的rtos,不过除了内核啥都没有。 2. Uc/os:同rtos,商业应用还要收费。 3. Rt-thread:非常棒的国产rtos,外围组件支持度极其丰富。 4. Alios-things:阿里出品,但是乱,组件乱的一团糟,不知道开发团队怎么凑出来的。 5. Liteos:华为出品,但是组件太少。 6. Ecos:巨强的一个rtos,但是是c++的,不喜欢。 7. SMT-OS:楼主自己写的rtos,主要为某些DSP设计,使用了大量的DSP优化技巧如软中断机制,但是无外围组件支持。
*还有一个不属于rtos范畴的,linux hhhhhhhhh,其实最开始做的第一版是uboot spl+压缩超精简linux,过程简直超级简单,体积12+647k,但是可以通过改openwrt代码来避免覆盖,然后楼主就被老爸胖揍一顿。。。。。。
综上,最后敲定rt-thread。
3. rtos-集成及遇到的问题 Rtos的集成主要分成几部分,中断体系适配和与uboot的集成。 这部分其实是硬伤,一般不看datasheet很难有所成,不过好在楼主老爸是mips架构老砖家了,边做边请教学起来简直so easy啊,简单来说,mips对中断的处理主要依靠status、cause等寄存器,具体这里就不列了,可以参考see mips run。 Uboot的集成这部分有一个明显的问题,就是rtos应该被集成在哪个阶段。众所周知,uboot启动按细分可分为四个阶段:flash、ram【重定向】、ram【解压镜像】、ram【解压镜像-重定向】,那么加载哪里好呢? 首先flash不可取,太慢;第一个ram阶段能加载的东西太少了,不可取;第二个ram阶段还没有重定向,也不可取,所以最终敲定最后一个阶段。 Rtos的启动代码放在这个阶段的哪个步骤呢?由于楼主只有一周时间,所以就偷了个懒,希望尽量使用uboot的代码,于是就放在了最后一步,main_loop之前。 还有一个问题是系统调用的方式,一般来说,可以直接使用函数调用,也可以像mips-linux那样使用syscall,syscall在linux下可以隔离用户应用于操作系统api,不过由于rtos所有程序都运行在内核态,因此使用syscall没啥卵用还占用了宝贵的上下文调度时间,所以使用第一种方式。
4. 由于位置无关技术引发的特殊处理 Uboot由于需要避免与linux内核占内存,同时为了适应各种大小的内存,使用了位置无关技术,具体来说,就是利用gp指针指向全局变量表地址,每次取变量时,先从地址表查地址,然后再读取内容,这套技术一共使用了两个独特的寄存器gp、t9,gp寄存器的用处我们已经说完了,那么t9是干什么的呢? 由于位置无关技术允许有多个gp指针,所以,当子程序进入之后,首先要更新自己的gp指针,一般来说,可以通过pc相对寻址来找到gp指针,但是不幸的是,mips没有显示读取pc指针的方式,那么只有从过程调用下手,过程调用使用jalr t9的方式调用,此时子程序就从t9获得了自己的pc指针 这对于无os的开发没有什么大的影响,只要最开始维护好gp指针,一切后续都好说,但是对于os不是这样,因为中断发生时,谁也不能保证此时的gp指针是可用的,这就使得所有全局变量和函数不可用,事情一下子变得很尴尬起来 这个问题其实有两个解决方案,一个是我自己想的,一个是我爸教我的 (1)利用bal子函数调用和ra实现 在中断中,在使用gp之前,利用bal指令向后跳转两条指令,利用空出来的空间存放gp指针,跳转后,利用ra寄存器找到原位置,这需要对内存数据打补丁。 (2)对内存指令打patch实现 我爸提醒我,既然能对内存数据打补丁,当然也可以直接生成内存指令,具体操作是当系统初始化之后,根据gp在ebase指定的0x1000空间内写上几条指令 - li k0 #gp
- li k1 #program
- jr k1
- nop
复制代码
这几条指令把gp赋值给k0,然后使用k1跳转到真正的中断处理函数,中断处理函数中,保存完上下文之后,将k0的值赋给gp 如此,则完成了中断中gp指针环境的建立 由于使用了位置无关技术,因此还有一点需要注意,就是t9的维护。一个很容易被忽视的细节就是,当子线程启动时t9的维护,因此,操作系统建立堆栈时需要将t9指向thread_entry。
5. Demo 为了验证rt-uboot的集成性,做了个小demo,demo包括3个线程,一个是thread_main,用于执行uboot中的main_loop()函数,一个是thread_led,用于1Hz频率闪烁led,一个是后台worker,用于当idle哨兵顺带处理其他杂务(具体可以看代码)。
尾言 到这里一个多线程的rt-uboot就已经实现了,当然,目前的功能还只是引入了rtos,驱动还采用轮询的方式,那么下一步的操作也就是做一下基于中断的驱动【代码已完成串口和net】,顺带引入posix机制【代码已完成libc移植】、加入更多好玩的功能,由于楼主的学业任务比较繁重,第二篇第三篇什么时候能与大家见面还不一定,不过请大家放心,只要有时间,我就会搞这个的。
最最后,要感谢我老爸老妈给与的悉心指导,还要感谢我的师叔----大连理工大学创新实验学院的李航老师在我童年时代给予的启蒙,就是在李航老师的指导下,我迈入了路由器的大坑。最后感谢大家的耐心观看!谢谢!
============2018-09-11============
第一章后记
在第一章中,虽然我们实现了一个多线程uboot mod,但是仍有些细节没有说清和解决,主要有:
1.中断处理函数问题
rtos中,中断处理是重要的一部分,mips自身为了简单起见,使用了通用中断入口而非向量中断入口(高版本也可以使用中断向量,不过一般没人用),因此各个中断入口处理基本相同,除了cache错误的中断,在这个中断中,我们首先要禁用cache,由于只有k1可用(k0放gp),所以只有写一个折中的处理代码:
- mfc0 k1, CP0_CONFIG
- ehb
- ori k1, 7
- subu k1, 7 //或xori k1,7
- ori k1, 2
- mtc0 k1, CP0_CONFIG
- ehb
复制代码
注意此时万万不可使用堆栈保存其他值,因为此时堆栈很有可能指向了cached memory。
2.uboot网络驱动的坑
在第一章中实现的rt-uboot,经测试网络总是出现问题,经过仔细查找,发现是延时函数udelay的问题,由于rtos无法实现us级的延时,所以这种轮询型驱动会出问题,解决办法是对于1ms以内的中断,强制禁用中断,使用NOP方式跑完延迟(尽管禁用了中断,但由于有中断标志位,因此无需担心错过中断)。
3.uboot自身的大坑
uboot自身由于没有开启中断,同时又处在error模式,所以很多程序的大坑都没有被发现,比如httpd中连续free两次内存的问题,这在rtos下会导致第二次free时产生tlb错误,解决办法就是第一次free时同时释放指针。
4.syscall的问题
有人说syscall可以防止位置无关情况下的程序调用时的可能存在的一些隐患,因为此时gp指针能够保证始终正确,这是对的,不仅如此,由于syscall下,线程一定是被挂起的(被保存到了堆栈上),所以此时的调度也是非常好做的,但是,syscall不是没有代价的,保存中断现场和恢复中断现场会占用大量的时钟周期,而且由于rtos的普通线程也工作在内核态,因此如果程序想破坏操作系统是非常容易的,使用极大的时钟周期为代价换来这一点点的安全性,我认为不值得。
============2018-09-14============
5.又是uboot的坑
在uboot初始化中,init_f到init_r之间传递了全局数据id,本来这个是非常棒的设计,但是init_r里面的程序就是沿用已经失效了的指针gp【启用RAM指针时会失效】,没办法,手工更新一下吧。
6.神秘的BUG
在rtos中,由于系统为每个线程都保存了一套自己的cp0_status寄存器副本,所以当一个线程对status寄存器进行操作时,其他线程是看不到的,这会产生一些迷之BUG,比如一个线程关了中断然后调用sleep函数(尽管这在操作流程中是不允许的),对于其他线程来说,就好像什么都没发生一样。
7.mips R2000/R3000的特殊处理
mips r2000 r3000的中断处理与r4000大同小异,但是没有提供eret指令,而是使用rfe指令,rfe与eret相比,仅仅提供了基本状态的转换,而不包括程序指针的跳转,所以中断返回需要显示指引:
8.算是uboot的坑?
mtk/rt的uboot在mt7628/mt7620启动时,会在ram初始化前调用udelay,导致系统panic,修正为新的eraly_udelay即可
第二章引子
前文书说道,第二章我们讲述如何设计中断下的驱动。这是个头疼的事情,在RTOS领域叫做BSP适配,一般来说,对于不同的SOC甚至是同SOC不同的板子之间,这部分代码的可重用性不高。
昨天晚上的时候,跟我爸聊了聊这个问题,抱怨了一下不同SOC适配工作太繁琐太机械,结果我爸一脸神秘的跟我说:“你知不知道上古有一种操作系统,能够直接引用uboot中的轮询型驱动?”
当时我就吃了一惊,世界上居然还有这种操作系统????
看着我一脸懵逼的表情,我爸得意的说出了答案:
非、抢、占、式、操、作、系、统~ 俗称多任务操作系统
当时我觉得内心受到了一万点伤害,我当时就在想,这种操作系统也能用?不过仔细和老爸探讨了一下之后,我发现其实这种操作系统的确非常适合跟uboot耦合的。
多任务操作系统的不同任务(只能叫任务了)之间只需要靠主动释放CPU来实现多任务机制的,所以不用担心多资源被重复占用的问题,同时又能尽可能的把CPU跑满,当然缺点就是系统的稳定性跟单个应用息息相关了,在这种模式下,甚至可以不启用中断,只要做好部分寄存器的保存和还原工作就可以了。
这个实现方式,可能会叫mt-uboot吧。。。。
不过目前已经开了rt-uboot的坑,就算含着泪也要把坑填完,等以后有空了,再实现一般多任务uboot。
============2018-09-15============
思绪来了真的是停不下来,今天主要就在做multi-task版本的uboot了,连作业都没来得及写简述一下mt-uboot的关键部分吧,由于没有使用中断,所以就不用考虑上下文切换的事情,唯一需要考虑的是任务主动释放cpu时的切换问题,这部分直接采取以下策略:
某个任务->
调用:延时/睡眠函数->
调用:切换函数:将主要寄存器【可以不包含调用过程中不需保存的寄存器】扔进任务结构体的寄存器副本中,包括SP->
调用:调度函数,返回新任务结构体->
真返回:切换函数:根据任务结构体的寄存器副本将主要寄存器更新【可以不包含调用过程中不需保存的寄存器】,包括SP->
伪返回:延时/睡眠函数【此返回已经不再是原来的任务序列了】->
新任务继续执行。。。
由于没有了系统时钟,所以部分系统api需要时刻关注timer寄存器的值,以达到精确记录时间的目的【不过再怎么也肯定不如中断来的一丝不漏】,好在mips是32位时钟,溢出的几率不是那么大【还可以强制使能时钟中断,但是不启用中断,根据中断等待标志位(不是所有平台都有)来防止溢出】
今天就更这么点,赶紧写作业睡觉
============2018-09-28============
插入*1章
本章主要说一下armv7的rt-uboot设计遇到的问题,也许能给其他多线程bootloader提供一点点借鉴
1.armv7的初始化
armv7的初始化一般如下:
配置cache、ram、mmu->可重定向->配置各个模式下影子寄存器->构造中断向量表->初始化中断控制器->初始化时钟,注册中断->操作系统启动
2.armv7的可重定位
armv7的c代码可以自动可重定位,但是汇编下不如mips那么方便【数据存取不方便,程序跳转只需使用bl,非常方便】,没有直接能够构造自动重定位的汇编伪码【主要因为没有找到在汇编中显示使用全局数据offset的办法】,所以我使用的方式是:
当读取xxxxx时,使用
- ldr r0 _xxxxx
- _xxxxx:
- .word xxxxx
复制代码
当操作系统初始化时,手工指引修改_xxxxx指向重定向后的xxxxx
3.armv7的堆栈
armv7下,统一使用svc的堆栈
============2018-10-06============
插入*2章
本章主要说一下armv8的rt-uboot设计遇到的问题,也许能给其他多线程bootloader提供一点点借鉴
1.armv8的初始化
arm的初始化一般如下:
配置cache、ram、mmu->配置ELx模式->初始化中断控制器->初始化时钟,注册中断->操作系统启动其中ELx的模式配置很关键,如果不正确的配置,会出现cpu无法响应中断的情况
2.arm的可重定位
armv8的代码可以全自动重定位,程序跳转使用BL,访问数据使用ADR
3.armv8的堆栈
armv8下,应用程序使用EL0,中断处理使用ELx
4.armv8的中断
armv8的中断配置异常繁琐,比armv7复杂很多,而且兼容性只能说...一般,因为不同处理器在支持虚拟化、支持安全模式上有很多种选择,每一种情况都有一种对应的初始化行为
首先必须配置armv8的中断响应路由,在EL3下是SCR_EL3,在EL2下是HCR_EL2,只有正确配置了中断路由,cpu才能正确响应你的中断
其次是向量基地址,armv8中,不同的EL级别下有不同的向量基地址,必须配置成指定模式的地址
最后是GIC配置,由于GIC可以设计成支持安全模式,因此是否带安全模式、是否处于安全模式,其寄存器配置是不同的,甚至有很大差异,如果不能正确配置,同样会不响应中断
5.支持EL2
现在能够找到的开源支持armv8的rtos一般都只支持EL1和EL3,但是很多处理器因为需要先加载安全平台,所以进入bootloader后是EL2模式,一般大部分RTOS选择降权到EL1中再进行操作
其实想要支持EL2,只需要做好EL2模式下的时钟初始化以及中断路由配置就可以了,其中最难的就是中断路由配置,我卡死在这里了一下午
EL2和其他模式的显著区别在于:EL3属于实实在在的上帝模式,想干啥干它丫的;EL1属于实实在在的白板模式,就那么点权限,该干啥就干啥了;而EL2正好处于两者中间,上面有EL3的安全固件管控(如果有实现),下面还要控制EL1,所以在这个模式下,如果模式配置不正确,会非常麻烦。如果沿用EL1的配置方式,中断会被路由到EL1模式,此时对于cpu来说,中断会从EL2跳到EL1,这种降权限的中断,在armv8的体系结构上是不被支持的(中断只能够平级维持或提升权限),所以此时中断不会被响应,只有配置HCR_EL2后,中断跳转变成了同级别跳转,此时中断才会被响应[这真是个大大大大坑]
6.64位的支持
rt thread原生其实是不是特别支持64位操作系统的,这点从头文件中可以看到,并没有rt_uint64_t这个定义,而且有些该用rt_base_t的地方用了rt_uint32_t,不过这都不要紧,因为大部分平台依然是通过32位有效地址去访问外部空间的,所以指针只需要以32位保存,使用时加上32个0在前面即可
另1:armv8刚开始看觉得很乱,但是用顺了之后发现比armv7好用的不是一丁半点
另2:arm系列其实再做不死uboot我觉得似乎没啥特别的必要,好多平台(高通、全志、联发科)的BL0都是自带usb线刷能力的,也就是说短接刷机引脚或者长按reset键上电,连接usb到电脑就能直接线刷,超级方便
|