文章目录
- 一、前言
- 二、技术栈
-
-
- arch体系架构
- 2.bootloader
-
- OS
-
- 网络协议
-
- 驱动
-
- 编程语言
-
- 文件系统
-
- 数据结构
-
- 设计模式
-
- 应用开发
-
- 调试工具
-
- 三、总结
-
一、前言
博主作为一个10年的嵌入式软件开发工程师,参与过众多嵌入式设备开发,摄像头、路由器交换机等设备,涉及多种操作系统和芯片架构,对嵌入式软件开发有比较深入的认识,本文整理嵌入式软件的技术栈,以及其中的一些要点,区分重点内容和了解内容,希望能帮助对嵌入式感兴趣的你建立一个全局的认识。
二、技术栈

嵌入式软件所涉及的知识点主要有如下方面:arch体系架构、bootloader、os、网络协议、驱动、编程语言、文件系统、数据结构、设计模式、应用开发、调试工具。
1. arch体系架构
|- 体系架构
|- 芯片架构
|- arm
|- arm64
|- rsic-v
|- x86
|-架构特性
|-哈佛架构/冯诺依曼架构
|-流水线
|-异常模式
|-寄存器
|-cache
|-中断GIC
|-MMU/TLB
|-虚拟化技术KVM
|-汇编
嵌入式会涉及底层硬件,对于芯片架构也是需要了解的,这对于我们分析一些性能问题、稳定性问题有很大的帮助,嵌入式最常用到的是arm架构,x86则在一些高性能服务器上使用较多,rsic-v则通常是一些mcu。需要重点了解arm/arm64芯片架构,cpu内部架构都是类似的,x86是冯诺依曼架构,地址和数据总线公用,而arm和rsic-v使用哈弗架构,地址和数据总线,地址和数据cache是分开的以及cpu的多级流水线:取址、译码、执行、访存、写回等内容,流水线优化linux中的unlikey分支预测技术,利用局部性访问原理优化cache和流水线。cpu通常有多种工作模式,用于实现内核态(特权模式)和用户态(非特权模式),用于控制一些指令的执行,比如MMU访问必须是内核态才可以访问,协寄存器访问需要在内核态,ARM中有7中工作模式,irq、fiq、svc(内核态)、user(用户态)等模式,而arm64中做简化成4个异常级别:EL0(用户态)、EL1(内核态)、EL2(虚拟机)、EL3(安全模式)。cache一般有多级cache,L1 cache通常是CPU每个核独有,L2 cache是多个核共享,多核之间还有cache一致性问题。了解必要的寄存器,有助于分析panic问题,比如arm中16个通用寄存器,SP、LR、FP、PC寄存器等,以及函数调用的传参顺序,比如arm中R0-R3用来传参。GIC是最常用的中断控制器,包括共享中断SPI、核独有中断LPI、核间通信IPI技术,以及中断异常向量表,ISR处理函数。Linux系统使用虚拟地址,需要使用MMU做虚拟地址核物理地址转换,页表信息保存地址转换关系,并且会做内存权限控制,页表保存在内存中,那每次访存要多一次内存访问操作:查找页表,所以为页表建立cache:TLB,TLB miss后才从DDR中加载页表。虚拟化技术也是最近比较火的技术,云服务提供商使用KVM为每个用户提供一个虚拟机,arm64中使用EL2异常模式运行虚拟机。
2.bootloader
|- bootloaer
|- romcode
|- preloader
|-ddr校准
|- uboot
|- 加载内核
|- 禁用看门狗
|- cache操作(禁用或者主动刷cache)
|- 初始化两个阶段
|- main_loop
|- cmd命令框架
|- board和driver驱动
|- config配置框架
|- UIP网络协议栈(扩展)
|- grub
|- XIP(片上执行代码)
bootloader是Linux之前运行的代码,用来加载内核,因为内核运行时需要传递一系列参数,由uboot进行传递。uboot之前通常有一段供应商的preloader代码,通常是闭源的,用来初始化SOC、DDR校准等动作。preloader和uboot都是保存在flash中,需要加载到DDR中才能执行,此时需要SOC内部固化的一段romcode来进行加载,如果是NOR FLASH也可以使用XIP片上执行代码的方式运行,因为NOR 可以按字节随机访问,所以uboot中有第一阶段board_f 和第二阶段board_r,第一阶段代码设计是为了XIP。uboot其它操作如:关闭看门狗,禁用cache或者需要主动flush cache,如果uboot开cache的情况加载内核,进入内核会默认关cache,数据会丢失,所以加载Linux镜像到DDR后,需要主动flush cache。 uboot的软件代码结构核心是cmd命令框架,以及mian_loop是主要修改的函数,driver驱动代码框架和Linux类似,board目录保存SOC供应商的驱动代码。config配置框架,不同的board对应不同的deconfig文件。如果想要在uboot中实现tcp等网络传输,可以使用开源的UIP协议栈。grub 则是用于x86机型上的引导程序,简单了解即可。
3. OS
|- OS
|-linux
|-调度子系统
|-cfs调度类
|- nice
|- vruntime
|-rt调度类
|-deadline调度类
|-内存子系统
|-早期内存管理
|-bootmem
|-memblock
|-reserve内存
|-伙伴系统
|-slab
|-动态映射vmalloc和匿名页
|-COW写时复制技术
|-网络子系统
|-netfilter
|-TCP/IP协议栈
|-网卡驱动
|-驱动子系统
|-驱动模型
|- bus-device-driver
|- platform模型
|- 设备树dts
|- 调试手段:
|- proc
|- sys
|- debugfs
|- devfs
|- uevent
|- mdev
|- udev
|- hotplugd
|-1号进程
|-启动脚本
|- freertos
|-任务调度
|-优先级反转问题
|-heap管理
|-临界区管理
|-禁调度
|-禁中断
|-中断管理
|-异常向量
|-tick中断
|-中断优先级和嵌套
|-lwip
操作系统层次是重点,这里分出两条路径:Linux方向和RTOS方向,
- Linux
Linux方向需要了解调度子系统,CFS公平调度类,比如基础的优先级nice值,以及基于虚拟运行时间vruntime的调度策略;实施调度rt,基于优先级调度;deadline调度类优先级最高。内存子系统,物理页使用伙伴系统管理,伙伴系统初始化之前使用memblock进行早期内存管理,将整个DDR当作一整块内存,然后不断切分小块,以及可以reserve一些内存不加入Linux内存管理。伙伴系统用于page分配,小于page的内存分配使用slab管理器,提供固定大小的内存块分配,比如kmalloc和skb等专用内存就是使用slab管理器。动态映射vmalloc机制以及用户态堆内存使用匿名页管理机制。COW写时复制机制则是Linux节省内存的方式,对于fork进程的内存,只复制页表,公用内存,只有在写入不同内容时才分配内存,以及malloc内存时页只分配虚拟内存,在write时才分配物理页机制。网络子系统主要了解网络收包流程、网卡驱动、以及内核中核心的netfilter框架,实现报文过滤等防火墙功能。驱动子系统主要了解Linux的驱动模型bus-device-driver模型,以及常用的DTS设备树配置。启动脚本也是Linux中经常改动的内容,实现应用自启动、文件系统mount等功能。
- FreeRTOS
FreeRTOS是使用最广的开源RTOS(实时操作系统),在穿戴设备、物联网设备中使用较多。Freertos是微内核,只提供任务调度和内存管理功能,对于驱动、文件系统,网络协议栈均不提供,网络协议栈通常搭配LWIP。其中的要点是优先级反转问题,以及PORT层对接arch实现中断禁用,上下文保存等功能。中断管理主要关注中断嵌套,以及调度使用的tick中断。
4. 网络协议
|- 网络协议
|-网络层:IP
|-传输层:TCP/UDP
|-应用层:FTP/HTTP/MQTT
|-物理层无线通信:
|-WIFI
|-蓝牙
|-LoRa
|-NB-Iot
|-通信协议设计:TLV
网络协议除了常用的TCP/IP/HTTP之外,物联网设备中常用的MQTT协议也需要学习,MQTT报文头开销小,基于订阅和发布机制,支持QOS。物理层的通信协议WIFI、蓝牙/BLE、以及物联网中使用较多的LoRa、NB-Iot。WIFI适合大数据传输,需要发现认证流程,重点学习WIFI6和WIFI7协议,对物理层和驱动CFG802.11驱动架构进行学习;蓝牙用于耳机等设备,BLE功耗低,传输距离较远,协议栈GATT/ATT;LoRa数据量小,传输距离远;NB-Iot则是用于蜂窝网络,物联网卡进行上网。应用层通信协议设计通常使用TLV作为参数传递和协议兼容性设计。
5. 驱动
|- 驱动
|- 总线
|-I2C
|-GPIO模拟I2C
|-I2C控制器
|- SPI
|- USB
|- MDIO
|- PCIE
|-GPIO
|-FLASH
|-IDS TABLE
|- NAND 坏块处理/位反转
|-寄存器
|- 控制寄存器
|- 状态寄存器
|- 数据寄存器
|- 操作注意禁用cache
|-block设备
|-char设备
驱动设计需要先关注硬件、总线协议,然后根据datasheet操作寄存器实现协议。常见总线由易到难:I2C、SPI、MDIO、USBPCIE、,启动I2C和SPI协议简单最常用,USB、PCIE、MDIO协议复杂,高速总线,最有价值,可以深挖。常见的驱动开发时block块设备和字符设备,flash驱动适配通常涉及IDS TABLE中添加flash的信息(如块大小、页大小、ECC能力,读写时序等),而对于NAND FLASH则要关注坏块处理和位翻转。驱动编写通常操作寄存器,需要根据datasheet介绍实现,这里寄存器分类通常分为3种类型:控制寄存器、状态寄存器、数据寄存器,操作寄存器需要使用ioremap禁用cache。
6. 编程语言
编程语言C/C++/SHELL是必须的,这没啥好说的,另外需要掌握python,平时编写一些自动化测试用例、小工具等。
7. 文件系统
flash文件系统,NOR使用jffs2,NAND使用ubifs,需要研究ubi机制中的坏块处理等,而rootfs通常使用压缩率较高的squashfs,另外sd卡和u盘通常使用fat/exfat,这些文件系统重点学习。Linux中的vfs是对具体的文件系统的抽象,比如文件系统描述内容super block,以及文件描述信息inode/dentry等信息。
|- 文件系统
|-fat/exfat
|-jffs
|-ubifs
|-squashfs
|- vfs
|-super block
|-inode
|-linux一切皆文件
8. 数据结构
|- 数据结构
|-线性表
|-哈希表
|-冲突解决-拉链法
|-冲突解决-扩表
|-树
|-rbtree
|-radixtree
嵌入式开发需要掌握基础的数据结构,如线性表(数据、链表),hash表,hash表需要学习冲突解决办法,可以有拉链法和扩表法。以及树结构在Linux中使用也非常广泛,rbtree和radixtree等。
9. 设计模式
|- 设计模式原则
|- 单一职业
|- 里氏替换原则
|- 开闭原则
|- 依赖倒置原则
嵌入式开发也需要使用面向对象的思想编程,以及需要考虑代码的可度性、可维护性、扩展性等,需要遵循4大原则,使软件系统具有高内聚、低耦合、易扩展和易维护的特性。
-
单一职责原则(SRP, Single Responsibility Principle)
定义:一个类应该只有一个引起它变化的原因,也就是说一个类只负责一件事情。
应用:如果一个类承担了多个职责,修改其中一个功能可能会影响其他功能,增加系统耦合度。通过职责分离,可以提高代码的可维护性和可测试性。
示例:在电话系统中,拨号、通话和数据传输应由不同类或接口处理,而不是集中在一个类中。
-
开闭原则(OCP, Open-Closed Principle)
定义:软件实体(类、模块、函数等)应对扩展开放,对修改封闭。
应用:当需求变化时,通过增加新类或实现接口来扩展功能,而不是修改已有代码,从而减少对现有系统的影响。
示例:在计算器程序中,新增减法功能时,不修改原有加法类,而是通过抽象运算符类扩展新功能。
-
依赖倒转原则(DIP, Dependence Inversion Principle)
定义:高层模块不应该依赖低层模块,二者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。
应用:通过接口或抽象类进行编程,高层模块通过抽象调用低层模块,实现模块间解耦。
示例:数据库访问模块提供接口,高层业务模块依赖接口而非具体实现,这样可以轻松替换数据库类型。
-
接口隔离原则(ISP, Interface Segregation Principle)
定义:不应该强迫客户端依赖它不使用的方法,一个接口只提供客户端需要的方法。
应用:将大接口拆分为多个小接口,使类只实现自己关心的功能,避免不必要的依赖和实现负担。
示例:将打印机接口拆分为打印接口和扫描接口,客户端只实现所需功能即可。
10. 应用开发
|- 应用开发
|- 软件结构
|- 黑板结构
|- 数据流结构
|- 数据库结构
|- 模块管理
|- 配置管理
|- OTA升级
|- 进程和线程通信
|- rpc机制
|- 事件总线
|- ubus
|- dbus
|- 通信机制
|- unix socket
|- msg
|- 共享内存
|- signal
|-同步机制
|-mutex
|-spinlock
|-atomic
|-读写锁
|-文件锁
应用开发首先需要了解软件结构:
1、黑板结构:发布订阅机制,通过订阅数据的形式,数据变化后完成发布,然后出发订阅者执行。
2、数据流结构:数据传输到不同节点执行,和流水线类似,网络收发包、视频流处理pipeline等。
3、数据库结构:配置保存到db中,消息通知和数据保存分开处理。
软件框架通常会做模块管理、配置管理等功能,以及进程间通信机制如unix socket、msg、共享内存、signal信号量,以及基于此实现的rpc、event机制。多进程和多线程之间同步机制,睡眠锁、自旋锁、原子变量、读写锁、文件锁等机制。
11. 调试工具
|- 调试工具
|-内存排查
|- asan/kasan
|- kmemleak
|- page owner
|- electric fence.
|- cpu排查:perf
|- 抓包: tcpdump
|- io: iostat
|- 内核观测
|- ebpf
|- func trace
|- 挂起调试
|- gdb
|- kgdb
|- crashdump
内存越界排查,用户态使用asan、electric fence,内核使用kasan,内存泄漏可以使用valgrind、page owner、slabinfo等工具。cpu利用率排查使用perf工具,抓包使用tcpdump,挂起使用gdb分析,以及内核挂起可以使用crash功能导出ddr内容进行分析,另外对于ebpf的内核观测技术近期也很火,需要深入研究。
三、总结
上述对于嵌入式全栈需要掌握的技术做了逐一介绍,仅简要说明,不做深入介绍,旨在为初学者提供学习方向,希望对各位有所帮助。