工作之后我也时长回忆起在基地学习的时光,那时主要还是学习MCU相关,使用的是ST和TI家族的芯片,移植库函数或者自行编写驱动库,烧录到片内flash上,驱动不同的外设,搭载一些如PID,卡尔曼滤波等算法,加入一些逻辑上的业务代码,即可做出一些不错的“小产品”出来。
今天来看,这样的开发历程更像是“全栈式”的开发,因为这个过程不仅覆盖了驱动部分的移植,也包括了功能,也即应用部分的开发,这种开发路子常见于个人开发者,或是规模非常小的公司。一般情况下,工程量到达一定规模时,就会将二者分离到不同的岗位上来,各司其职。
然而,当前大部分公司依然不会保留太多单纯的驱动开发岗位,更多得还是BSP和应用部分,大多数驱动开发岗位还是存在与供应关系的上游,也即芯片原厂,不管是MCU还是其他高端的SoC,芯片驱动多数都被原厂包圆了,下游的厂商多数都是在做BSP级别的开发,以及应用开发(甚至有些小公司连BSP都没有,例如我司的一些客户…,都让原厂负责了)也就是说,过去我是Consumer,而现在,我是Provider,负责为我司设计出品的芯片提供所有的Firmware,驱动代码,以及SDK等。
总的来说,驱动这个岗位所要负责的内容以及工作难度,在不同的企业类型,呈现这样的一个关系:芯片原厂 > 方案厂 > 模组厂 > 板卡厂 > 其他应用厂商。
为了更加具体的体现出芯片原厂的驱动开发在做些什么,下面我将逐步带大家过一遍一颗芯片从设计到Bringup,再到交付完整的SDK包到客户手中,到底经历了哪些环节。驱动开发者,在其中又做了哪些努力。
一般性的芯片开发流程:
- 芯片定义:指定芯片的规格(芯片spec)
- 芯片设计:硅片设计和封装设计(主要关注硅片设计)
- 芯片制造:设计好之后,就要送到foundry(代工厂)加工制造,(晶圆)
- 芯片测封:先对晶圆进行中测,然后进行划片封装,最后对封装后的芯片进行成测。确保不会由生产环节引入错误。
- 芯片验证:将芯片焊接到预先设计好的电路板上,装配成机器并加载软件,然后开始验证。
其中,我们十分关注芯片设计与芯片验证环节。芯片设计的周期通常来说会非常漫长,这个时间段是IC设计工程师大展拳脚的时候,他们也像软件工程师一样,需要对设计出的成果作调试,验证。随着大规模集成电路设计复杂性的逐渐提升,芯片验证面临的资金与时间的挑战也变的越来越大,芯片的验证阶段占据了整个芯片开发的大部分时间。从芯片需求定义、功能设计开发到物理实现制造,每个环节都需要进行大量的验证。早期开发者想验证芯片的设计是否符合预设,只有等待漫长的模拟结果,或是等待流片成果。时间成本和经济成本都较高。
现如今验证方法也越来越多,例如:逻辑仿真(功能验证),形式验证,原型验证。功能验证基于软件,验证成本较低,验证环境方便,但性能较差;形式验证为静态验证方式,但不可仿真DUT的一些动态行为。其中,大多公司都会选择先在SoC验证部门,进行逻辑仿真验证,然后再交由基础软件开发部门,使用FPGA原型验证的手段,对其进行二次验证覆盖。
FPGA原型验证是一种成熟的技术,用于通过将RTL移植到现场可编程门阵列(FPGA)来验证专门应用的集成电路(ASIC),专用标准产品(ASSP)和片上系统(SoC)的功能和性能。FPGA和ASIC前端代码都是基于 Verilog HDL 开发的,所以ASIC代码理论上是可以在FPGA平台上跑起来的,在流片之前,尽可能的去确定芯片功能的正确性的一种验证方式。
在我司,FPGA原型验证的任务,交由驱动部门执行,通常芯片设计部门的小伙伴会给我们出携带各种IP功能的”bit文件“,FPGA平台(HAPS 80)有一个附带的烧录软件,为了让FPGA平台变成目标SoC的”样子“,需要使用这个烧录软件将bit文件烧录到FPGA上。
SoC形成之后,我们会使用Arm的DS-5 IDE构建一个Bare Metal(裸机,不带操作系统)工程,编写当前验证SoC的初始化代码,各个IP的驱动代码,各个IP的测试demo,以及多个IP之间共同工作时的方案性的demo。主要目的是覆盖测试SoC在实际运行时可能遇到的情况。
完成SoC本身的验证之后,还会进行Bootrom的开发,同样也是在FPGA环境上进行验证,这个环节非常关键,bootrom是芯片开始运行的起点,后期bringup是否可以成功,芯片的所用功能,性能,是否能够有机会绽放出来,都得看bootrom的表现。有些企业会自己设计Bootrom的代码,也有些企业则会基于安全启动的考虑,会去使用Arm推出的解决方案 — ATF。
ARM TrustZone是ARM公司推出的SoC及CPU系统范围的安全解决方案,具有基于硬件的安全功能。它通过对原有硬件架构进行修改,在处理器层次引入了两个不同权限的保护域——安全世界和普通世界,任何时刻处理器仅在其中的一个环境内运行。
同时这两个世界完全是硬件隔离的,并具有不同的权限,正常世界中运行的应用程序或操作系统访问安全世界的资源受到严格的限制,反过来安全世界中运行的程序可以正常访问正常世界中的资源。ATF将启动过程分为三个阶段:
- Boot Loader stage 1 (BL1) AP Trusted ROM: 用于实现bootrom
- Boot Loader stage 2 (BL2) Trusted Boot Firmware,二级loader
- Boot Loader stage 3-1 (BL31) EL3 Runtime Software, 提供PSCI(电源管理等)、secure和nosecure切换等运行时服务
- Boot Loader stage 3-2 (BL32) Secure-EL1 Payload (optional), 第三方的secure OS,如TEE等
- Boot Loader stage 3-3 (BL33) Non-trusted Firmware,一般是uboot
其中,二级loader中常常会执行DDR的初始化,将该阶段独立出来是非常有必要的,因为一颗SoC使用的DDR颗粒并不是永久一致的,如果将DDR初始化部分的代码整合进BOOT阶段,那么这颗SoC就不能使用其他的DDR。
驱动工程师在此的工作有许多,其中一部分为:
- 为基础的部件,如GPIO,Pinctrl,Clock,Reset等模块编写驱动。
- 将ATF的IO存储抽象层中的存储介质相关的接口,适配为自家SoC上存储介质的驱动。启动方式有很多,如耳熟能详的,spi-nor,spi-nand,uart。或者emmc,sd card,xip启动等,通过Boot select的拨码开关去选择不同的启动方式。
- 适配DDR。
- 实现热启动warm reset(直接从ATF阶段跳转到内核运行),各家SoC的warm reset实现手段不同,依托的硬件机制不同。
- 实现多分区启动等容灾手段
- 基于ATF提供的工具,实现安全启动,对rom的镜像进行加密。
- and soon…
这个阶段的工作十分精密,需付出很多精力在FPGA平台上,仔细验证启动功能,定时发送新的bootrom版本给到SoC验证部门,在他们的环境上进行逻辑时序的仿真,SoC的验证更加精细,在时序层面上可以分析出C程序对机器的每次操作,甚至是一次寄存器的读写,都可以进行捕捉。
SoC验证也会对我们给出的bootrom程序进行性能上的分析,建立benchmark。两个部门的交互在此阶段非常频繁(所以一定要和SoC部门的人打好关系,在他们的层面上去理解硬件,对于我们的帮助会更大)
在Bootrom的验证成功之后,就会将文件发到代工厂进行流片。利用流片的这个时间空隙,驱动部门会进行SDK的回片前的预开发。
原厂给出的SDK通常包括uboot,kernel,prebuilts,libs,apps以及build。其中驱动部门所要负责的是uboot,build,prebuilts,以及最重要的kernel。(应用开发部门主要负责libs以及apps,驱动开发也会涉及一些libs目录的责任,比如一些不对外开放的驱动源码,会放在libs目录,编译后就会删除)
build目录主要是一些编译脚本,SDK支持的开发板非常多,支持一键切换编译出不同的开发板的镜像,构建根文件系统,lib,并进行打包。
由于一些历史原因,uboot中各个层次之间组织架构,和linux kernel的组织非常类似,都是board—>machine—>arch—>cpu这样的框架:
为了移植性,更容易地跨平台运行,它俩的架构逐渐变成了上图这个样子。这张图(来自蜗窝科技)揭示了uboot和kernel对于多种CPU,多种体系结构,多种机器类型,以及多种板级的抽象层次。我粗浅地认为,做嵌入式很难的一部分也正是在此(除开算法部分):
软件工程中的抽象和封装,力求以最简洁、最高效的方式,实现尽可能多的功能,通过软件抽象来掩盖硬件的差异。这一点上,uboot和kernel的结构做得非常优雅。
SDK的回片前的预开发也就是在这里CPU,Arch,Mach,Board四层次移植,适配目标SoC的代码以及配置,各个模块的驱动移植与开发(uboot也是需要开发驱动的),以uboot为例:
CPU层次,如ARMv8的初始化代码,在ARM体系结构这个层次上对SoC进行初始化,比如MMU的配置,Cache的配置,大小端,异常等级的设置,通用定时器配置等
Arch层次,如arm的公共代码,为后期执行板级的配置(board_init_f/r),初始化执行环境。
Machine层次,实则是一个通用的库
Board层次,板级的特殊代码,著名的board_init_f和board_init_r就会在此处实现。
如我前面所言,这个阶段属于预开发阶段,所有的操作都在FPGA上运行,等到芯片实际流片回来,Bringup成功,就会将这段时间里的成果,迁移到实际的Evaluation board上去,以快速给到客户实际的产品。
最后就来到SLT(system level test)了,这是芯片量产前的最后一个测试,可能也会由驱动部门负责,这一块我了解的不多,驱动这边主要在为每个驱动模块编写自动化测试软件。SLT所用的测试板会做的非常的庞大,因为需要对所有模块进行统一的测试,通常需要上位机的支持,给测试板下达测试指令,以自动化地测试芯片各个模块是否正常。SLT是一种纯粹通过运行和使用来完成的测试。
到此处为止,还都是硅前阶段,即行业黑话Pre-silicon,指硅片实际流片出之前的所有步骤,原则上验证人员需要将所有的Bug拦截在硅前阶段。这个阶段漫长且如履薄冰,必须小心翼翼,规格严格,功夫到家。相比而言,驱动工程师在这段时间内也一般来说不会很繁忙。
第二个阶段是硅后验证阶段(Post-Silicon),指的是在产品流片、发布以后查缺补漏的阶段,这个阶段对设计缺陷的修复成本将会变得非常高。
硅后阶段,驱动部门所需负责的工作:
- 对流片后的芯片进行BringUp
- 迁移FPGA上预开发版本的各个库到EVB版本,并在EVB板上实际调通
- 各个模块的驱动调通并验证,做性能测试
- 给每个驱动写测试Demo
- 支持客户的疑难杂症,支持应用部门的疑问和需求,维护驱动等
相比硅前而言,硅后的工作节奏会变得非常快,压力也会更大,Bringup失败怎么办,如何避免ECO?后期发现有一些非必现的BUG又怎么办?特别是第5点,这是一个长期的过程,对于每个原厂的驱动开发者而言,这是一条永远存在的副本,每个人管理自己所负责的那些驱动(有些比较复杂的驱动,如ISP,VPE等,则单独交给某几个人去负责)。
客户的问题会被相应地路由到各自的Task List上来。内核部分的异常属于公共任务,由leader判定哪些开发者来解决(驱动开发者也需要非常理解linux内核的工作机制)。一些大公司会有单独的内核部门解决内核异常(如华为)。一般来说初创公司的SDK都会有很多问题。
比方说,有一次客户反馈,我们的linux内核,换了一个解压算法后,性能表现特别差,约莫45s才能进到内核初始化部分。经过我进两个月的定位与查找,发现是我们的uboot内核中,没有打开一个Errata项,Arm Core中有一个寄存器中的一个bit没有配置上,该bit管理着Cache的表现,如果这个bit不打开,即使Cache已经使能了,也不会生效。后来我在海思的Patch列表中,也看到了这一点,看来各个IC公司都是一步步摸过来的。
上面简单地叙述了一下一个驱动开发者在培育一颗芯片的过程中所付出的努力。总的来说,原厂的驱动开发者,特别是小厂的开发者,确实需要负责很多事务。
但我想说,透过现象看本质,在这个开发链条中,无论是Bare Metal,Uboot,Kernel,都是在配置寄存器,驱动工程师也被戏称为寄存器配置工程师,这一点也没错,驱动开发就是在看手册,看懂手册后去写配置代码,让硬件按想要的方式运行。这个过程也就是Bare Metal级别的开发逻辑,MCU开发者或许很熟悉ST的标准库,HAL库,LL库,这些也就是在配置寄存器的基础上套了一层框架,方便开发者对于驱动代码的使用。
对于Uboot和Kernel部分的驱动开发,底层逻辑依然是如此,只不过uboot与kernel为了统一开发者的行为,方便开发者将驱动加入他们的代码,制定了一系列规则与框架,只需看懂他们的规则,了解他们的框架是如何运行,即可将驱动插入到其中。
所以对于”驱动怎么学习“这个命题来说,我想先往底层去讨论,向底层,即探索硬件的运行机制,最标准,不走弯路的做法就是去啃芯片手册,这个过程初入手也许非常痛苦,茫茫手册,动辄百千页,如何去看?
无他,唯手熟尔。
有些同学可能过于依赖库函数编程或者类似Cube IDE图形化配置的这类模式,遇到一颗新的SoC就会觉得头大。
其实手册看多了也就习惯了,看着页数很多,一大部分都是对寄存器的description,这部分当作字典查就好了,重要的是手册头部到中间那些东西。关于如何去看一份手册,在宋宝华的《Linux设备驱动开发详解》中有一些描述,可以作为参考:
写好单一的模块驱动,也许并不难,但如果将它与其他的模块交互,与CPU的体系结构有一些关系呢?
在OSPI控制器的INDAC (Indirect Access Controller)模式下访问时数据由SRAM中转,Master通过AHB访问读写SRAM中的数据,这里SRAM的中转地址的内存属性是否能够随意设置呢?
诸如这些问题也暗示驱动开发不能只见树木,不见森林,需要适当地去学习整体的体系结构的知识,学习SoC各个部分的关系。这种需求在做电源域以及低功耗设计,ATF的BL31,uboot和kernel的启动等部分时,都会有深刻的感觉。如果有机会去做虚拟化,安全加解密部分的工作,相信这种体验会更加深刻。
向上探索,首先遇到的问题应该是“框架”,这是一个比较大的话题,对于uboot来说就是它的DM模型,对于Kernel或者RTOS来说,就是整个操作系统的原理和设备驱动程序机制这种驱动开发必须面对的问题,这一块没办法展开多讲,以书籍和视频资源为辅,多看源码,多调试,多去写一些针对性的测试demo。
针对操作系统,最近两年我个人看得比较多的是南京大学的操作系统课程(所用教材是operating system-three easy pieces)以及MIT 6.S081(RISC-V架构)(我们电气学院几乎没有开设针对OS的课程,如果想做Linux或者FreeBSD这类操作系统下的开发,还是有必要学习一下的)。
这两门课主要讲解操作系统的理论知识,但其精髓还是在实验课上,如果能够吃透这两门课的实验课,进行魔改,写到简历上也是个不错的项目经验。
面对现代Linux这样的操作系统,直接去看它的源码其实是非常吃力的,它在漫长的发展长河中进化出了很多”额外“的特性,如支持了一些调试手段,还有一些面对编译器优化的特性(如likely和unlikely),直接去看源码会迷失在这些细节当中。可以先从RTOS或者RVOS,LMOS,Xv6这样的小型OS来入手研究。
在我看来,学习操作系统要面对的困难主要是两点:
- 体系结构部分。这部分对于大多数驱动开发这里来说非常陌生,特别是只做BSP层以上的开发者(可以跳过对体系结构的理解)。操作系统是建立在硬件上的,其多数重要的机制都依赖于体系结构提供的支持。
对于同一功能如进程管理,主流的体系结构都是类似的。相对而言,RISC-V的难度比Arm的难度小很多,可以从RISC-V的手册开始看,但是毕竟当前岗位数量上还是ARM多,我们也可以功利一些,直接入手ARM,旧版本如ARMv7可以看杜春雷的ARM体系结构与编程,MCU级别的可以看Cortex系列的权威指南,高版本如ARMv8以上可以看笨叔的《ARM64体系结构与编程》。
- 算法。如内存管理方面的算法,SLAB,Buddy。进程调度方面的算法,CFS等。如果就业目标并不是专精内核的方向,这一块无需做多深入,将更多的精力放在设备驱动和体系结构上会更有性价比。
再向上走,可能就是一些”形而上学“的东西:软件架构。这一点在上面谈到linux kernel的组织时已经谈过一些。Linux kernel虽然是以C这样的面向过程的语言去编写,但也处处使用着面向对象的思想,了解一些软件架构上的做法,如解耦,开闭,接口隔离等,有助于理解kernel中各个组件的代码。
最后我想说,做嵌入式驱动,毕竟不是搞科研,而是工程学,切莫只研究理论而丢失实际操作,学习一些调试手段,多去调试几个工程是必要的。(但其实我也不会多少调试手段,当然了,printk应该算是玩的很6…,GDB在调试内核时用的并不多,这是一个非常强大的工具但是我没有heavliy地用过,倒是仿真器用得不少)
这两年常有许多朋友向我吐槽当前的就业环境,如果将刚毕业的我放在这个环境下,我应该也没有多少把握去拿到一个满意的offer,所以我在此篇文章中所给出的建议只能说是我主观上觉得有用,有意义的建议。
如果让我回到基地再重新经历一遍,我还是会去参加各类比赛,利用课余时间多做一些自己感兴趣的小玩意儿,本科阶段毕竟不是在工程或科研上厮杀的时候,但环境总会逼着我们不断前进。
嵌入式,不管是驱动还是应用开发,不管是做MCU还是ARM Linux,不管身处哪个方向:芯片,医疗,电机控制,运动控制,AI,协议栈或是上位机,都是需要不断学习,不断专研,才能做得长久的。这需要我们拥有一些对行业的热爱以及一些敢想敢做的极客精神。
当然,在前行的过程中,应该时刻记得最重要的,还是自己身心的健康愉悦。我的leader也曾这样和我说过:能做好这一行的,无非就是两种人。
第一种,天生就适合干这一行,我们组有个小哥,当时芯片回来要做HDMI,全公司没有人做过,交给他两天后就将屏幕点亮了,这种可能就是骨骼惊奇。第二种,就是能够坐得住的,不断摸索,肯花时间。
最后我再将上面的一些tips罗列一下:
- 基础知识(计算机四大件)一定要学好,计组,OS这些在原理层面上的基础要打牢,网络不做相关方向的工作的话可以放一放以后再学。
- 做驱动的朋友,芯片手册要认真研究,遇到问题先找手册。比如IIC延展这些问题其实在芯片手册里也会提到,DMA的S-G模式在图像数据搬运时的应用,诸如此类的很多知识在芯片手册上都有,当然ARM的核心手册(TRM)也得好好专研。
- 学会调试,多动手。
- 在项目中锻炼自己通常是最快的。
- 两句真言:Read The F**king Source Code/Read the Friendly Manual
视频:
- 韦东山的教程,不必多说了,祖师爷.
- 南京大学操作系统:[操作系统概述 (为什么要学操作系统) 南京大学2022操作系统-蒋炎岩-P1]哔哩哔哩bilibili
- MIT 6.S081:【操作系统工程】精译【MIT 公开课 MIT6.S081】哔哩哔哩bilibili
- 计算机基础课:南京大学 – 计算机系统基础 袁春风(一)- 程序的表示、转换与链接哔哩哔哩bilibili
- 一生一芯:[“一生一芯”概述 第六期“一生一芯”计划 – P1]哔哩哔哩bilibili
- 中科大RVOS:https://www.bilibili.com/video/BV1Q5411w7z5/?spm_id_from=333.337.search-card.all.click
书籍:
C语言功底:
- C prime Plus
- C专家编程
- C接口与实现
- 多看源码,多动手写代码,多抄优质的代码
计算机体系结构:
- 计算机组成与设计-硬件软件接口
- 深入理解计算机系统(CSAPP)
- 笨叔的《ARM64体系结构与编程》
操作系统部分
- 操作系统导论 operating system-three easy pieces
- 深度探索嵌入式操作系统(LMOS)
- 《Understanding the linux kernel 3edt》
- 《linux设计与实现》
编译原理部分:程序员的自我修养 – 编译 装载 链接
博客:
- 蜗窝科技的博客Linux内核分析 – 蜗窝科技 (wowotech.net)
面向Linux驱动开发:
- linux设备驱动程序内核机制 — 陈学松
- linux设备驱动开发详解 — 宋宝华
- Mastering Linux Kernel Development — Robert Love
面向实战:
- 韦东山的一些项目还挺不错的
- 蜗窝科技的博客上有一个X-project项目,很贴近实际企业的开发模式与开发内容
对于选择参加工作的同学,找工作前可以利用寒暑假找一份实习。拿到offer后最好仔细甄别,做好背调。例如,对于那些面试难度与薪酬待遇不成正比的公司应保持谨慎。信息收集可以通过脉脉,看准,知乎这类软件,也可以询问身边的学长学姐。
希望大家都能找到满意的工作,身体健康,工作顺利,学业进步。
原创文章,作者:guozi,如若转载,请注明出处:https://www.sudun.com/ask/81496.html