嵌入式软件技术栈和学习路线详解

文章目录

  • 一、前言
  • 二、技术栈
      1. arch体系架构
    • 2.bootloader
      1. OS
      1. 网络协议
      1. 驱动
      1. 编程语言
      1. 文件系统
      1. 数据结构
      1. 设计模式
      1. 应用开发
      1. 调试工具
  • 三、总结

一、前言

博主作为一个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大原则,使软件系统具有高内聚、低耦合、易扩展和易维护的特性。

  1. 单一职责原则(SRP, Single Responsibility Principle)

    定义:一个类应该只有一个引起它变化的原因,也就是说一个类只负责一件事情。

    应用:如果一个类承担了多个职责,修改其中一个功能可能会影响其他功能,增加系统耦合度。通过职责分离,可以提高代码的可维护性和可测试性。

    示例:在电话系统中,拨号、通话和数据传输应由不同类或接口处理,而不是集中在一个类中。

  2. 开闭原则(OCP, Open-Closed Principle)

    定义:软件实体(类、模块、函数等)应对扩展开放,对修改封闭。

    应用:当需求变化时,通过增加新类或实现接口来扩展功能,而不是修改已有代码,从而减少对现有系统的影响。

    示例:在计算器程序中,新增减法功能时,不修改原有加法类,而是通过抽象运算符类扩展新功能。

  3. 依赖倒转原则(DIP, Dependence Inversion Principle)

    定义:高层模块不应该依赖低层模块,二者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。

    应用:通过接口或抽象类进行编程,高层模块通过抽象调用低层模块,实现模块间解耦。

    示例:数据库访问模块提供接口,高层业务模块依赖接口而非具体实现,这样可以轻松替换数据库类型。

  4. 接口隔离原则(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的内核观测技术近期也很火,需要深入研究。

三、总结

上述对于嵌入式全栈需要掌握的技术做了逐一介绍,仅简要说明,不做深入介绍,旨在为初学者提供学习方向,希望对各位有所帮助。

相关推荐
Oj92q85H513 小时前
如何在Dev-C++中使用TDM-GCC编译项目
linux·开发语言·c++
行走的大喇叭13 小时前
计算机系统组成及常见概念
linux·运维·计算机网络
kyle~13 小时前
ROS2---rosbag2记录和回放话题、服务和动作数据
linux·机器人·数据采集·ros2
j_xxx404_13 小时前
Linux线程控制:从用户态控制到内核级克隆全链路解析
linux·运维·服务器·开发语言·c++·ai
z2005093013 小时前
【linux学习】进程的概念和在linux系统下的基本实现情况01
linux·网络·学习
铅笔小新z13 小时前
【Linux】基础IO
linux·服务器
xiaoye-duck13 小时前
【Linux:文件】Linux 动静态库详解::制作、使用、原理与实战
linux
likerhood13 小时前
设计模式 · 代理模式(Proxy Pattern)java
java·设计模式·代理模式
大唐游子13 小时前
wsl安装高版本ubuntu(24.04)
linux·ubuntu