Bootloader是一个用于引导嵌入式系统的软件程序,通常存储在芯片的非易失性存储器中,如闪存。Bootloader的主要功能是在系统上电后,负责初始化系统硬件并加载操作系统或应用程序到内存中执行。 下面是一个简要的介绍Bootloader的工作流程和主要功能: 硬件初始化:Bootloader首先负责初始化处理器、时钟、内存和外设等硬件资源,确保系统处于合适的状态以便后续的操作。 引导设备选择:Bootloader可能需要检测可用的引导设备,例如闪存、外部存储器或串口等,并选择其中一个作为加载操作系统或应用程序的来源。 加载操作系统或应用程序:Bootloader根据预定义的引导策略,从选定的引导设备中读取操作系统镜像或应用程序,并将其加载到内存中。 执行加载的程序:一旦程序加载到内存中,Bootloader会跳转到该程序的入口点,启动执行。对于操作系统,这意味着将控制权移交给操作系统内核,而对于应用程序,它将开始执行应用程序的主函数。 错误处理和回滚:Bootloader通常包含错误处理机制,以便在发生错误时执行相应的操作,例如跳转到备用引导设备或者恢复到出厂设置状态。 升级支持:一些Bootloader还提供了固件升级的支持,允许通过特定的接口或协议更新系统固件,而无需物理访问设备。 总的来说,Bootloader在嵌入式系统中扮演着关键的角色,负责启动和管理系统的引导过程,同时提供了灵活性和可靠性,以适应不同的应用需求。 当我面对一个有一定规模、 稍显复杂的嵌入式项目时, 我通常并不会直接专注于主要功能的实现, 而是会做一些磨刀不误砍柴工的工作———设计一个 Bootloader(以下简称 BL) 以及构建一个Shell 框架。可能有人会觉得它们很高深, 实则不难, 正所谓“会者不难, 难者不会”。 本章就针对 BL 进行详细的讲解, 希望让大家可以体会到它的重要性。 1. 烧录方式的更新迭代 1.1 古老的烧录方式 单片机诞生于20 世纪80 年代, 以51 为代表开始广泛应用于工业控制、 家电等很多行业中。起初对于单片机的烧录, 也就是将可执行的程序写入到其内部的 ROM 中, 这不是一件容易的事情, 而且成本不低, 因为需要依赖于专门的烧录设备。而且受到半导体技术与工艺的限制, 对于 ROM 的烧写大多需要高压。这种境况一直持续到2000 年左右( 我上大学的时候还曾用过这种专门的烧录器),: 1.2 ISP 与ICP 烧录方式 随着低压电可擦写 ROM 的成熟, 单片机开始集成可通过数字电平直接读写的存储介质。其最大的优势在于可实现在系统或在电路直接烧录程序, 而无须像以前一样把单片机芯片从电路中拿出来, 放到编程器上, 这种烧录方式就是ISP(In System Programming) 或ICP (In Circuit Programming), 如图7.2 所示。 有人问过这样一个问题:“ISP 和ICP 我都听说过, 都说是可以在电路板上直接烧录程序,而无须拿下芯片, 那ISP 和ICP 有什么区别?”从广义上来说, 两者没有区别, 平时我们把其意义混淆也毫无问题。非要刨根问底的话, 那可以这样来理解:ISP 要求单片机中驻留有专门的程序, 用以与上位机进行通信, 接收固件数据并烧录到自身的 ROM 中, 很显然ISP 的单片机是需要可运行的, 即要具备基本的最小系统电路(时钟和复位); 而ICP 可以理解为 MCU 就是一块可供外部读写的存储电路, 它不需要预置任何程序, 也不需要单片机芯片处于可运行的状态。 支持ISP 或ICP 的芯片, 以 AT89S51 最为经典, 当时从 AT89C51 换成 S51 , 多少人曾因此不再依赖烧录器而大呼爽哉。这种并口下载线非常流行, 如图7.3 所示, 网上还有各种ISP小软件, 可以说它降低了很多人入门单片机的门槛, 让单片机变得喜闻乐见。一台电脑、一个S51 最小系统板、 一条并口ISP 下载线, 齐了! 1.3 更方便的ISP 烧录方式 1. 串口ISP 但是后来我们发现带有并口的电脑越来越少。那是在2005 年前后,STC 单片机开始大量出现, 在功能上其实与S51 相差无几, 甚至比同期的一些高端51 单片机还要逊色。但是它凭借一个优势让人们对它爱不释手, 进一步降低了单片机的学习门槛。这个优势就是———串口ISP, 这是真正意义上的ISP, 如图7.4 和图7.5 所示。再后来,9 针串口都很少见了, 只有 USB。这促使一个烧录和调试神器炙手可热———USB-TTL 串口。这下232 转换芯片省掉了, 直接通过 USB 进行烧录。这种方式造福了无数的单片机学习者和工程师。我本人虽然已经搞了近20 年单片机和嵌入式, USB 串口依然是不可或缺的调试工具。 多年来, 在串口与单片机的交互上, 我动了很多脑筋, 这也是我乐于开发 Bootloader 的一个原因。我希望“USB 串口在手, 一切全有!” STC 并不是第一个使用串口ISP 烧录程序的, 但它是最成功和最深入人心的。与之同期的很多单片机, 包括时至今日仍然应用最广泛的 STM32 全系列也都支持了串口ISP, 它成了一种标配的、 非常普遍的程序烧录手段。 2. 各种 USBISP 串口ISP 固然方便, 但是下载速度是它的硬伤, 当固件体积比较大的时候, 比如一些大型嵌入式项目的固件动辄几百 KB, 甚至几 MB, 再用串口ISP 就未免太慢了。所以一些单片机配有专门的 USBISP 下载器。以下列举几种比较主流的单片机及其 USBISP 下载器。 (1) AVR AVR 单片机曾经盛极一时, 但经历了2016 年的缺芯风波之后, 加之 STM32 的冲击, 开始变得一蹶不振, 鲜有人用了。与之配套的 USBISP 下载器非常多样, 有些是官方发布的, 更多的是爱好者开源项目的成果, 如图7.6 所示。 (2) C8051F( 见图7.7) (3) MSP430( 见图7.8) 我们会发现, 一个具有良好生态的主流单片机, 一定有配套的高效便捷的烧录下载工具。可见一种好的烧录方式, 对单片机开发是多么重要。 不论是串口ISP 还是各种专用的ISP 下载器, 都有一些共同的弊端。 依赖于专门的上位机或下载器硬件, 不能做到统型; 下载器价格仍然比较高, 尤其是原厂的, 这也是为什么有些单片机催生出很多第三方的下载器, 比如 AVR; 下载的时候通常需要附加额外的操作, 比如 STC 要重新上电、STM32 需要设置BOOT 引脚电平等。 这些额外的操作都增加了烧录的复杂性。尤其是在产品形态下要去重新烧录程序, 比如嵌入式升级, 就要打开外壳, 或将附加信号引出到壳外。 这都是非常不高效, 不友好的做法。如果有一种烧录方法, 对于任何一种单片机: 通信方式统一(比如一律都用串口); 提供一个友好的操作界面(比如命令行方式); 高效快速, 没有附加操作, 最好一键自动化烧录; 另外再增加一些嵌入式固件管理的功能(比如固件版本管理)。 这一定会让我们事半功倍。Bootloader 就能实现上述的这一切! 2. 关于Bootloader 2.1 Bootloader 的基本形态 直接看图7.9: 可以看到 BL 就是一段存储在 ROM 中的程序, 它主要实现4 个功能: 通过某种途径获取要烧录的固件数据; 将固件数据写入到 ROM 的 APP 区中; 跳转到 APP 区运行, 将烧录进去的用户程序引导起来; 在此过程中, 提供必要而友好的人机交互界面。这么说可能不好理解, 我们还是通过实例来进行讲解。 2.2 Bootloader 的两个设计实例 下面的两个实例, 用于说明 BL 的实际应用形态, 不涉及具体的实现细节, 旨在让大家了解 BL 实际是如何运行的。 1. 带Shell 命令行的串口BL 基本的操作逻辑如下: 通过超级终端、SecureCRT 或 Xshell 之类的串口终端输入命令program; BL 接收到命令后, 开始等待接收固件文件数据; 串口终端通过某种文件数据传输协议将固件数据传给 BL; BL 将固件数据写入到 ROM 的 APP 区中; BL 将 APP 区中的程序引导运行起来。更具体的示意如图7.10 所示。这里把操作逻辑说得很简单, 实际实现起来却并不容易, 我们放在后面去细究其具体实现。 2. 插SD 卡即烧录的BL 基本的操作逻辑如下: 将待烧录的固件拷贝到SD 卡中; 将SD 卡插入到卡槽中; BL 检测到SD 卡插入, 搜索卡中 BIN 文件; 将 BIN 文件数据读出写入到 ROM 的 APP 区中; BL 将 APP 区中的程序引导运行起来。 如图7.11 所示。通过这两个设计实例, 大家应该已经了解BL 是什么了吧。有没有感受到BL 是比ISP 烧录器更通用、 更灵活、 更友好、 功能更强大的固件烧录和管理手段呢? 有人可能知道 Linux 下的 Uboot, 它就是一个强大的 BL, 它提供非常强大的刷机(烧录操作系统镜像) 的功能以及完备而灵活的Shell 界面, 如图7.12 所示。其实我们电脑的 BIOS 也是一种广义的 BL。 那如何实现一个 BL 呢? 别急, 要实现 BL 是需要满足一些基本要求的。 2.3 BL 实现的要点 首先要说, 并不是任何一个单片机都可以实现 BL 的, 要满足几个要点。 1. 芯片体系架构要支持 来看图7.13 。 我们知道单片机程序的最开头是中断向量表, 包含了程序栈顶地址以及 Reset 程序入口,通过它才能把程序运行起来。很显然在从 BL 向 APP 跳转的时候,APP 程序必须有自己的中断向量表。而且单片机体系架构上要允许中断向量表的重定向。 传统51 单片机的中断向量表只允许放到 ROM 开头, 而不能有偏移量, 所以传统51单片机是不能支持 BL 的。有人要问“你这不是自相矛盾吗? 你前面说 STC 的51 单片机是支持串口ISP 的, 那它应该内置有ISP 程序, 我理解它应该和BL 是一个道理。”没错, 它内置的ISP程序就是一种 BL。STC 之所以可以实现 BL 功能, 是因为宏晶半导体公司对它的硬件架构进行了改进, 请看图7.14 可以看到,STC51 单片机多出了一块专门存放 BL 的 ROM, 称为 BOOTROM。 网上有一位叫 shaoziyang 的网友为 AVR 单片机写了一个 BL, 还配套开发了 一 款 叫AVRUBD 的上位机, 如图7.15 (AVRUBD 是很有用的, 本章后面会介绍, 它可以让我们实现隔空烧录) 所示, 实现了 AVR 单片机的串口烧录, 让很多人摆脱了对 USBISP 之类ISP 下载器的依赖(虽然ISP 下载器已经很方便了, 但它毕竟还需要银子嘛)。 AVR 在硬件架构上与STC51 是一个套路, 如图7.16 所示。 通过配置 AVR 的熔丝位可以控制复位入口地址以及 BOOT 区的大小和开始地址, 如图7.17 所示。 讲到这里, 有人会说:“那有没有一种单片机, 程序放在 ROM 的任何位置都可以运行起来, 也就是中断向量表可以重定位?”当然有, 这种单片机还很多, 其中最典型的就是 STM32。它的程序之所以可以放之各地皆可运行, 是因为在它的 NVIC 控制器中提供了中断向量表偏移量的相关配置, 这个后面我们再详细说。 2. ROM 要支持IAP 这也是需要单片机硬件支持的。很好理解, 在 BL 获取到固件数据之后, 需要将它写入到ROM 的 APP 区中, 所以说单片机需要支持IAP 操作, 所谓IAP 就是 In Application Programming, 即在应用烧录。也就是在程序运行过程中, 可以对自身 ROM 进行擦除和编程操作 。 大家仔细想想是不是这样? 似乎支持串口ISP 的单片机都支持IAP 功能。STC 还把这一功能包装成了它的一大特色, 可以用内部 ROM 来充当 EEPROM 的功能, 可以在运行时记录一些掉电不丢失的参数信息。 STM32 的 ROM 擦写在配套的固件库(标准库或 HAL 库) 中已经有实现, 大家可以参考或直接使用。 3. APP 程序的配套修改 为了让 BL 可以顺利地将 APP 程序引导运行起来, APP 程序在开发的时候需要配合 BL做出相应的修改。最重要的就是 APP 程序的开始地址(即中断向量表的开始地址) 以及对中断控制器的相应配置。 对于51 、AVR 这类单片机 APP 程序不用修改, 具体原因大家应该明白。这里主要对STM32 APP 程序如何修改进行详细讲解。 我们依然是结合实例, 如图7.18 所示。 假设我们所使用的STM32 的 ROM 总大小为128KB,BL 程序的体积是16KB,APP 程序紧邻 BL, 那么 APP 区的开始地址为0X08004000, 也就是 APP 程序的中断向量表偏移地址为0X4000。 如果我们使用 MDK 作为开发环境的话, 需要修改这里, 如图7.19 所示。 而如果我们使用的是gcc 的话, 则需要对link.ld 链接文件进行修改, 如图7.20 所示。 然后我们还需要对 NVIC 的中断向量表相关参数进行配置, 主要是中断向量表的偏移量,如下代码: #define VECT_TAB_OFFSET 0x4000 OK, 经过修改后的程序, 我们把它放到 ROM 的0X08004000 开始地址上, 然后再让 BL跳转到这个地址, 我们的程序就能运行起来了。 有人又会问:“BL 中的跳转代码怎么写?”别急, 这是我们要讲的下一个要点 4. BL 中的跳转代码 跳转代码是 BL 要点中的关键, 直接关系到 APP 程序能否正常运行, 如图7.21 所示。我直接给出STM32 的 jump _app 函数代码。 typedef void ( * iapfun)(void); iapfun jump2app; void MSR_MSP(u32 addr) { __ASM volatile("MSR MSP, r0" ); //set Main Stack value __ASM volatile("BX r14" ); } void load_app(u32 appxaddr) { if((( * (vu32* )appxaddr)&0x2FFE0000) ==0x20000000) //检查栈顶地址合法 { //用户代码区第二个字为程序开始地址(复位地址) jump2app= (iapfun) * (vu32* )(appxaddr+4); //初始化APP 堆栈指针(用户代码区的第一个字用于存放栈顶地址) MSR_MSP( * (vu32* )appxaddr); jump2app(); //跳转到APP. } } 这段代码大家自行研究, 如果展开讲就属于赘述了。到这里 BL 相关的要点就介绍完了, 大家应该有能力去完成一个简单的 BL 了。我基于STM32 设计了一个小实验, 大家有兴趣可以小试牛刀一下, 如图7.22 所示。 我们将 BL 程序用Jlink 烧录到0X08000000 位置, 而把 APP 程序烧录到0X08002000 开始位置, 然后复位, 如果串口打印了hello world 或流水灯亮起来了, 就说明我们的BL 成功了。 3. 把Bootloader 玩出花 上面我所讲的都是 BL 最基础的一些内容, 是我们实现 BL 所必须了解的。BL 真正的亮点在于多种多样的固件数据获取方式。 3.1 BL 的实现与延伸(串口传输固件) 前面我讲到过两个 BL 应用的实例, 一个是串口传输固件文件, 一个是 SD 卡拷贝固件文件。它们是在实际工程中经常被用到的两种 BL 形式。 这里着重对前一个实例的实现细节进行讲解剖析, 因为它非常具有典型意义, 如图7.23 所示。 这个流程图提出了3 个问题: 串口通信协议是如何实现的? 为什么获取到上位机传来的固件数据,不是直接写入到 APP 区,而是先暂存,还要校验? 对固件数据是如何实现校验的? 串口通信协议以及文件传输实现的相关内容略显繁杂, 在本书《 大话文件传输》一章中会专门进行讲解。 第二个问题: 经过串口传输最终由单片机接收到的固件数据是可能出现差错的, 而有错误的固件冒然直接写入到 APP 区, 是一定运行不起来的。所以, 我们要对数据各帧进行暂存, 等全部传输完成后, 对其进行整体校验, 以保证固件数据的绝对正确。 针对第三个问题, 我们要着重探讨一下。 一个文件从发送方传输到接收方, 如何确定它是否存在错误? 通常的做法在文件中加入校验码, 接收方对数据按照相同的校验码计算方法计算得到校验码, 将之与文件中的校验码进行对比, 一致则说明传输无误, 如图7.24 所示。 图7.24 是对固件文件的补齐以及追加校验码的示意。为什么要对文件补齐? 嵌入式程序经过交叉编译生成的可烧录文件, 比如 BIN, 多数情况下都不是128、256 、512 或1024 的整数倍。这就会导致在传输的时候, 最后一帧数据的长度不足整帧, 就会产生一个数据尾巴。取整补齐是解决数据尾巴最直接的方法。这一操作是在上位机上完成的, 通常是编写一个小软件来实现。这个小软件同时会将校验码追加到固件文件末尾。这个校验码可以使用校验和(CheckSum) 或者 CRC, 一般是16 位或32 位, 如图7.25 所示。 又有人会问:“要把整个固件暂存下来, 再作校验, 那得需要额外的存 储 空 间 吧, 外 扩ROM(FlashROM 或 EEPROM)?”是的。如果想节省成本, 我们也可以不暂存, 传输时直接烧写到 APP 区。这是有风险的, 但是一般来说问题不大(STC 和 STM32 的串口ISP 其实也都是实时烧写, 并不暂存)。因为在传输的过程中, 传输协议对数据的正确性是有一定保障的, 它会对每一帧数据进行校验, 失败的话会有重传, 连续失败可能会直接终止传输。所以说, 一般只要传输能够完成, 基本上数据正确性不会有问题。但是仍然建议对固件进行整体校验, 在成本允许的情况下适当扩大 ROM 容量。同时, 固件暂存还有一个另外的好处, 在 APP 区中的固件受到损坏的时候, 比如固件意外丢失或IAP 时不小心擦除了 APP 区, 此时我们还可以从暂存固件恢复回来(完备的 BL 会包含固件恢复的功能)。 其实也不必非要外扩 ROM, 如果固件体积比较小的话, 我们可以把单片机的片上 ROM砍成两半来用, 用后一半来作固件暂存。 如图7.26 所示, 我们将片上 ROM 划分为3 部分, 分别用于存储 BL、APP 固件以及暂存固件。比如 我 们 使 用 STM32F103RBT6 , 它 一 共 有 128 KB 的 ROM, 可 以 划 分 为 16 KB/ 56 KB/56 KB。 有些 产 品 对 成 本 极 为 敏 感。我 就 有 过 这 样 的 开 发 经 历, 当 时 使 用 的 单 片 机 是STM32F103C8T6 , 片上 ROM 总容量为64 KB, 固件大小为48 KB,BL 为12 KB。在通过BL进行固件烧写时根本没有多余的 ROM 进行固件暂存。我使用了一招“狗尾续貂”, 如图7.27所示。 我无意中了解到 STM32F103C8T6 与 RBT6 的晶元是同一个。只 是 因 为 有 些 芯 片 后64 KB 的 ROM 性能不佳或有瑕疵, 而被限制使用了。我实际测试了一下, 确实如此。但是后64 KB ROM 的使用是有前提的, 也就是需要事先对其好坏进行验证。如果是好的, 则暂存校验, 再写入 APP 区; 而如果是坏的, 那么就直接在固件传输时实时写入 APP 区(这个办法我屡试不爽, 还没有发现后64KB 有坏的)。 以上所介绍的是一种“骚操作”, 根本上还是有一定的风险的,ST 官方有声明过, 对后64K ROM 的质量不作保证, 所以还是要慎用。 3.2 10 米之内隔空烧录 OTA (On the Air) 这个“隔空烧录”源于我的一个IoT 项目, 它是对空调的外机进行工况监测。大家知道,空调外机的安装那可不是一般人能干的, 它要不就在楼顶, 要不就在悬窗上。这给硬件升级嵌入式程序带来很大的困难。所以, 我实现了“隔空烧录”的功能, 其实它就是串口 BL 应用的一个延伸, 如图7.28 所示。 “隔空烧录确实牛, 但是总要抱着一个电脑, 这不太方便吧。”确实是! 还记得前面我提过的 AVRUBD 通信协议吗? (详见“大话文件传输”一章) 它的上位机软件是有手机版的。这样我们只要有手机, 就能“隔空烧录”了, 如图7.29 所示。 “哪个 APP? 快告诉我名字”, 别急, 蓝牙串口助手安卓版, 图 7.30 是正在传输固件的界面。 AVRUBD 其实是对 Xmodem 协议的改进, 这个我们放在专门的章节进行详细讲解。 3.3 BL 的分散烧录 我们知道 BL 的核心功能其实就是程序烧录。那你有没有遇到过比较复杂的情况, 如图7.31 所示。 这种情况是有可能遇到的。主 MCU+CPLD+ 通信协处理器+ 采集协处理器就是典型的复杂系统架构。这种产品在批量生产阶段, 烧录程序是非常烦琐的。首先需要维护多个固件, 再就是需要一个个给每一个部件进行烧写, 烧写方式可能还不尽相同。所以我引入了一个机制, 叫“BL 的分散烧录”。 首先我们将所有的固件拼装成一个大固件(依次数据拼接), 并将这个大固件预先批量烧录到外扩 ROM 中, 比如spiFlash; 再将主 MCU 预先烧录好 BL; 然后进行SMT 焊接。PCBA生产出来之后, 只要一上测试工装(首次上电),BL 会去外扩 ROM 中读取大固件, 并从中分离出各个小固件, 分别以相应的接口烧录到各个部件中去。配合工装的测试命令, 直接进行自检。这样做, 批量化生产是非常高效的。当然, 这个 BL 开发起来也会有一定难度, 最大问题可能还是各个部件烧录接口的实现( 有些部件的烧录协 议 是 比 较 复 杂 的, 比 如 STM32 的SWD 或者 ESP8266 的SLIP)。 OK, 上面对一些 BL 实例的实现和应用场景进行了介绍。还有一些实例没有介绍, 比如通过 CAN 总线或SPI 进行文件传输, 这个我们还是放到专门的章节去详细讲解。当然, 各位读者可以在此基础上衍生出更多有特色而又实用的 BL 来。 BL 没有最好的, 只有最适合自己的。通常来说, 我们并不会把 BL 设计得非常复杂, 原则上它应该尽量短小精炼, 以便为 APP 区节省出更多的 ROM 空间。毕竟不能喧宾夺主, APP才是产品的主角。 4. 不走寻常路的BL 4.1 Bootpatcher 我来问大家一个问题:“Bootloader 在 ROM 中的位置一定是在 APP 区前面吗?”很显然不是,AVR 就是最好的例子。那如果我们限定是STM32 呢? 似乎是的。上电复位一定是从0X08000000 位置开始运行的, 而且 BL 一定是先于 APP 运行的。 在某些特殊的情况下, 如果 APP 必须要放在0X08000000 位置上的话, 请问还有办法实现 BL 串口烧录吗? 要知道 APP 在运行的时候, 是不能IAP 自己的程序存储器的(就是自己不能擦除自己来烧录新固件), 如图7.32 所示。 APP 运行时, 想要重新烧录自身, 它可以直接跳转到后面的BL 上,BL 运行起来之后开始接收固件文件, 暂存校验 OK 之后, 将固件写入到前面的 APP 区。然后跳转到0X08000000, 或者直接重启。这样新的 APP 就运行起来了。这个位于 APP 后面的 BL, 我们称之为 Bootpatcher(意为启动补丁)。但是这种做法是有风险的, 一旦 APP 区烧录失败, 那产品就变砖了。所以这种方法一般不用。 4.2 APP 反烧BL 前面我们都是在讲 BL 烧录 APP, 那如果 BL 需要升级怎么办呢? 用JLINK。不错, 不过有更直接的方法, 如图7.33 所示。 这是一种逆向思维, 我们在 APP 程序中也实现接收固件文件, 暂存校验, 然后将其烧录到BL 区。这种做法与 Bootpatcher 同理, 也是有一定风险的, 但一般都没有问题。
1、MCU最开始一启动后去哪里读代码?CPU上电启动后被设计为去地址0x00000000位置处读取代码;首先会连续读取两个字,分别是栈指针初始值和复位异常处理函数的地址;然后跳去执行复位异常处理函数。 当然在一些早期的ARM处理器设计中,如Arm7TDMI,复位后会直接读取0地址处的代码进行执行,由软件初始化栈指针,0地址处存放的直接就是中断处理函数,而不是函数地址。 所以我们可以有理由推测出,第一个字是栈地址是因为接下来的复位中断处理函数涉及函数跳转,可能已经需要存放内容在栈里了。 2. 0x0地址处是bootROM代码吗,还是用户bootloader代码? 答案是都可以。这其实取决于用户的代码是存放在哪里的。比如说对于一些性能强的MCU(如Cortex-A系列)来说,代码本身体积比较大,存放在SD卡里或者QSPI/SPI Flash里都有可能,这些MCU启动一定是先去bootROM执行代码,因为SD卡、SPI Flash的储存不在MCU的统一编址空间里,没初始化这些外设前根本无法访问,bootROM这块Nor Flash就一定是可以被MCU直接通过总线地址访问的,0地址的代码位于bootROM中。代码从bootROM中起来后,通过启动引脚判断从哪个外设中搬用户程序,并去初始化相应外设,将外设中存储的用户代码搬到内部SRAM中执行。后续的启动流程不赘述。 对于一些小容量的MCU来说,比如Cortex-M3/M4,他们的芯片里有内置Flash,这个Flash的特点跟上面说的bootROM很像,是MCU可以直接通过地址总线去访问到的,不需要进行外设初始化的。当然,这些MCU内部也是有bootROM的,因此这些MCU一上电可以选择从bootROM中启动,也可以选择从内置Flash中启动,是通过外部引脚进行选择的,选择了谁,就把谁的起始地址映射到0地址处。 3. 类似Cortex-M3/M4是如何保证Flash起始地址是栈指针和复位异常处理函数指针的? 这一点实际是通过编译的链接文件制定的。比如说如下是我截取的IAR的链接文件.icf。 4. MCU有可能不从0地址开始读代码吗? M7内核芯片比较灵活了,改变了固定从0x0000 0000地址读取中断向量表的问题,以STM32H7为例,可以从 0x0000 0000 到 0x3FFF 0000 所有地址进行启动。专门安排了个选项字节来配置。
一、深入理解IO 1、什么是操作系统IO I/O,即输入(input)和输出(output),也可以理解为读(Read)和写(Write); I/O模式可以划分为本地IO,模型(内存、磁盘)和网络IO模型; I/O关系到用户空间和内核空间的转换,也称为用户缓冲区和内核缓冲区; 用户态的应用程序不能直接操作内核空间,需要将数据从内核空间拷贝到用户空间才能使用。 read和write操作,都只能在内核空间里执行,磁盘IO和网络IO请求都是先放在内核空间,然后加载到用户态内存的数据。 2、IO读写性能差距实操 # dd用于指定大小的块,拷贝一个文件,并在拷贝的同时进行转换。# if=文件名:输入文件名,缺省为标准输入,即指定源文件# of=文件名:输出文件名,缺省为标准输出,即指定目的文件# bs=bytes:同时设置读入/输出的块大小为bytes个字节# count=blocks:仅拷贝blocks个块,块大小等于指定的字节数# bs是每次读或写的大小,即一个块的大小,count是读写块的数量。 # 释放所有缓存echo 3 > /proc/sys/vm/drop_caches # 操作一dd if=/dev/zero of=xdclass_testio1 bs=1M count=1024echo 3 > /proc/sys/vm/drop_caches # 操作二dd if=/dev/zero of=xdclass_testio2 bs=1M count=1024 oflag=directecho 3 > /proc/sys/vm/drop_caches# 操作三dd if=/dev/zero of=xdclass_testio3 bs=1M count=1024 oflag=sync 我们可以发现,参数的不同,会导致磁盘IO速度的不同: 没有oflag参数时,文件复制速度是oflag=direct的数倍:默认是buffered I/O,数据写到缓存层便返回,所以速度最快。 oflag=direct的速度比oflag=sync快一些:数据写到磁盘缓存便返回,但是速度比buffered I/O慢一些。 oflag=sync的速度最慢:写入的数据全部落盘才返回,所以速度比上面的仅写到磁盘缓存慢。 物理磁盘也会带有缓存disk cache,用于提高I/O速度,一般磁盘中带有电容,断电也能把缓存数据刷写到磁盘中。 3、什么是文件系统 (1)简介 在Linux系统中,一切皆是文件,文件系统管理磁盘上的全部文件,文件管理组织方式多种多样,所以文件系统存在多样化。 系统把文件持久化存储在磁盘上,文件系统就会实现文件数据的查询和存储。 文件系统是管理数据,而存储数据的物理设备有硬盘、U盘、SD卡、网络存储设备等。 不同的存储设备其物理结构不同,不同的物理结构就需要不同的文件系统去管理。 比如说,Windows有FAT12、FAT16、FAT32、NTFS、exFAT等文件系统;Linux有Ext2、Ext3、Ext4、tmpfs、NFS等文件系统。 # 查询系统用了哪些文件系统 [root@localhost test]# df -h -TFilesystem Type Size Used Avail Use% Mounted ondevtmpfs devtmpfs 1.9G 0 1.9G 0% /devtmpfs tmpfs 1.9G 0 1.9G 0% /dev/shmtmpfs tmpfs 1.9G 8.7M 1.9G 1% /runtmpfs tmpfs 1.9G 0 1.9G 0% /sys/fs/cgroup/dev/sda1 xfs 40G 22G 19G 54% /tmpfs tmpfs 379M 0 379M 0% /run/user/0overlay overlay 40G 22G 19G 54% /var/lib/docker/overlay2/6cce6b0ef229cb98e74ac34161938ffb11333b4f4fd26c298a53e8cc714e2d55/mergedoverlay overlay 40G 22G 19G 54% /var/lib/docker/overlay2/ae42c3f3e656cc8e13fe70723172f0756769edf4ac89fe2b8d1afe34f293718d/merged (2)索引节点和目录项 索引节点(index): 简称inode,记录文件的元信息,比如文件大小、访问权限、修改日期、数据存储位置等; 索引节点也需要持久化存储,占用磁盘空间。 目录项(directory entry): 简称为dentry,记录目录结构,比如文件的名字、索引节点和其他目录项的关联关系登,树状结构居多; 存储在内存中,也叫目录项缓存。 (3)什么是虚拟文件系统VFS(Virtual File System) 操作系统上有那么多的文件系统和物理存储介质,就是靠着虚拟文件系统为各类文件系统提供统一的接口进行交互,应用程序调用读写位于不同物理介质上的不同文件系统。 虚拟文件系统在应用程序和具体的文件系统之间引入了一个抽象层,开发者不用关心底层的存储介质和文件系统类型就可以使用。 (4)Linux的IO存储栈 https://www.thomas-krenn.com/en/wiki/Linux_Storage_Stack_Diagram 上面的总图实在太复杂,简单总结一下: 平时调用write的时候,数据是从应用写入到了C标准库的IO Buffer(用户态),这个Buffer在应用内存中,应用挂了,数据就没了; 在关闭流之前调用flush,通过flush将数据主动写入到内核的Page Cache中,应用挂了,数据也安全(内核态),但是系统挂了数据就没了; 将内核中的Page Cache中的数据写入到磁盘(缓存)中,系统挂了,数据也不丢失,需要调用fsync(持久化介质)。 总体来说,这就是操作系统的多级缓存和数据的可用性。操作系统也是程序,靠着多线程、异步、多级缓存实现高性能。 我们日常的业务开发,也可以借鉴这种思想。 需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享 二、深入理解磁盘 1、机械硬盘 (1)结构 机械硬盘(HDD)组成结构很多,重点关注磁盘、磁头臂、磁头,每个硬盘的正反两面都有对应的磁头。 数据存储在盘片的环状磁道中,最小读写单位是扇区(sector),一般大小为512字节,这样的话读写单位太小,性能不高。 文件系统把连续的扇区组成逻辑块(block),以逻辑块为最小单元来管理数据。 一般逻辑块大小为4KB,是由连续的8个扇区组成。 (2)如何读取数据 磁臂摆动+盘片转动(耗时大所以导致慢,随机在硬盘上找一个数据,需要8-14ms),定位到目标扇区读取数据; 磁臂在一定范围内摆动,来找到目标扇区,靠磁头把某个扇区的数据传输到总线上; 磁臂摆动范围有限,触达不到比较远的扇区,靠转轴来转动盘片,比如磁盘转速有7200转/分,1秒就是120圈; 常规1秒可以做100次随机IO,所以高并发业务单靠磁盘是扛不住的,基本都要结合缓存; 机械硬盘想要优化,就不能用随机IO,要用顺序IO,节省大量的物理耗时,比如Kafka、RockerMQ都是使用顺序IO。 2、磁盘读写常见指标 (1)IOPS(Input/Output Operations per Second) 指每秒能处理的I/O个数,表示块存储处理读写(输出/输入)的能力,单位为次,有顺序IOPS和随机IOPS。 阿里云盘性能参考: 高效云盘:2120 IOPS; ESSD云盘:2280 IOPS; SSD云盘:3000IOPS。 (2)吞吐量/带宽(Throughput) 是指单位时间内可以成功传输的数据数量,单位为MB/s。 比如一个硬盘的读写IO是1MB,硬盘的IOPS是100,那么硬盘总的吞吐率就是100MB/S,带宽=IOPS*IO大小。 如果需要部署大量顺序读写的应用,例如Hadoop离线计算型业务等典型场景,需要关注吞吐量。 (3)访问时延(Latency) 是指IO请求从发出到收到响应的间隔时间,常以毫秒(ms)或者微秒(us)为单位; 硬盘响应时间 = 硬盘访问时间 + IO排队延迟; 过高的时延会导致应用性能下降或报错; 如果应用对高时延比较敏感,例如数据库应用,建议使用ESSD AutoPL云盘、SSD云盘或本地SSD盘类产品; 普通的HDD磁盘,随机IO读写延迟是10毫秒,IO带宽大约100MB/秒,随机IOPS一般在100左右。 (4)容量(Capacity) 是指存储空间大小,单位为TiB、GiB、Mib、Kib,块存储容量按照二进制单位计算: 1B(byte字节)=8bit 1KB(Kilobyte千字节) = 1024B 1MB(Megabyte兆字节,简称兆) = 1024KB 1GB(Gigabyte吉字节,简称“千兆”) = 1024MB 1TB(Terabyte万亿字节,简称“太字节”) = 1024GB 1PB(Petabyte千万亿字节,简称“拍字节”) = 1024TB 1EB(Exabyte百亿亿字节,简称“艾字节”) = 1024PB # 查看容量[root@localhost test]# df -hFilesystem Size Used Avail Use% Mounted ondevtmpfs 1.9G 0 1.9G 0% /devtmpfs 1.9G 0 1.9G 0% /dev/shmtmpfs 1.9G 8.7M 1.9G 1% /runtmpfs 1.9G 0 1.9G 0% /sys/fs/cgroup/dev/sda1 40G 22G 19G 54% /tmpfs 379M 0 379M 0% /run/user/0 (5)使用率(Utilization) 指磁盘处理I/O的时间百分比,过高的使用率,常规字段Utilization-缩写%util表示; 如果超过80%意味着磁盘I/O存在性能瓶颈。 (6)IO等待队列长度(Queue Length) 表示等待处理的I/O请求的数目,如果I/O请求压力持续超出磁盘处理能力,就会增大队列长度。 (7)饱和度 指磁盘处理I/O的繁忙程度,过高的饱和度说明磁盘存在严重的性能瓶颈。 当饱和度为100%时,磁盘无法接受新的IO请求。 注意,使用率和饱和度是完全不同的,使用率只考虑有没有IO,不考虑IO的大小;当使用率是100%时,磁盘也可能接收新的IO请求。 3、磁盘IOPS性能测试 (1)安装fio yum install -y fio (2)使用 参数 说明 filename 待测试的文件或块设备 如果是文件,则是测试文件系统的性能;例:-filename=/work/fstest/fio.img 如果是块设备,则是测试裸设备的性能;例:-filename=/dev/vda1(容易损坏磁盘) ioengin IO引擎fio支持多种引擎,例如:cpuio、mmap、sync、psync、filecreate、libaio等 常用libaio是Linux异步读写IO(Linuxnative asynchronous I/O) iodepth 表示使用AIO时,同时发出I/O数的上限为128 direct 是否采用直接IO(direct IO)方式进行读写 如果采用直接IO,则取值-direct=1,否则取值-direct=0 一般是用直接IO写此时,测试结果更加真实 rw 读写模式 read:顺序读测试,使用方式-rw=read write:顺序写测试,使用方式-rw=write randread:随机读测试,使用方式-rw=randread randwrite:随机写测试,使用方式-rw=randwrite randrw:随机读写,-rw=randrw;默认比率为5:5 numjobs 测试进程的并发数,比如-numjobs=16 bs 单次IO的大小,比如-bs=4k size 测试文件的大小,比如-size=1G sync 设置同步模式,同步-sync=1,异步-sync=0 runtime 设置测试运行的时间,单位秒,比如-runtime=100 group_reporting 结果把多线程汇总输出 # 随机读fio -direct=1 -iodepth=128 -rw=randread -ioengine=libaio -bs=4k -size=1G -runtime=10 -filename=iotest1 -name=iotest1 # 随机写fio -direct=1 -iodepth=128 -rw=randwrite -ioengine=libaio -bs=4k -size=1G -runtime=10 -filename=iotest2 -name=iotest2 # 顺序读fio -direct=1 -iodepth=128 -rw=read -ioengine=libaio -bs=4k -size=1G -runtime=10 -filename=iotest3 -name=iotest3 # 顺序写fio -direct=1 -iodepth=128 -rw=write -ioengine=libaio -bs=4k -size=1G -runtime=10 -filename=iotest4 -name=iotest4 # 随机读fio -direct=1 -iodepth=128 -rw=randrw -ioengine=libaio -bs=4k -size=1G -runtime=10 -filename=iotest5 -name=iotest5 4、固态硬盘SSD (1)结构 固态硬盘由固态电子元器组成,没有盘片、磁臂等机械部件,不需要磁道寻址,靠电容存储数据。 某块区域存在数据,机械硬盘写入可以直接覆盖,而固态硬盘需要先擦除,再写入,block块擦的越多寿命越短,业务数据高频更新,则不太建议使用固态硬盘。 最小读写单位是页,通常大小是4KB、8KB。 性能高,IOPS可以达到几万以上,价格比机械硬盘贵,寿命较短。 固态硬盘由多个裸片叠加组成,一个裸片有多个块,一个块有很多的页,一个页的大小通常是4KB。 (2)磁盘数据的擦写 SSD里面最小读写单位是page,但是最小擦除单位是block。 一个块上的某些页的数据被标记删除,不能直接擦除这些的页,除非整个块上的页都被标记删除; 块还有其他有效数据,当有新数据只能写入白色区域,并不能利用红色区域,时间越长,不能被使用的碎片越多。 GC(Garbagecollection)垃圾回收: 有一套标记整理机制程序,“有效”页数据复制到一个“空白”块里,然后把这个块完全擦除; 那些被移动出数据的块上面的页要么没数据,要么是标记删除的数据,直接对这个块进行擦除; 擦除数据类似JVM的GC,使用标记整理算法Mark Compact。(先对对象进行一个标记,看看哪些对象是垃圾;整理会在清除的过程中,把可用的对象向前移动,让内存更为紧凑,避免内存碎片的产生;整理之后发现内存更紧凑,连续的空间更多,就不会造成内存碎片的问题) 5、磁盘分区 (1)概念 计算机中存放信息的主要存储设备就是硬盘,但是硬盘不能直接使用,必须对硬盘进行分割成一块块的硬盘区域就是磁盘分区。 磁盘分区(比如windows的C、D、E盘),方便管理、提升系统的效率和做好存储空间隔离分配: 将系统中的程序数据按不同的使用分为几类,将不同类型的数据分别存放在不同的磁盘分区中; 在每个分区上存放的都是相似的数据或程序,这样管理和维护就容易多; 分区可以提升系统的效率,系统读写磁盘时,磁头移动的距离缩短了,即搜寻的范围小了; 如果不运用分区,每次在硬盘上寻找信息时可能要寻找整个硬盘,所以速度会很慢。 磁盘分区,允许在一个磁盘上有多个文件系统,每个分区可以分配不同的文件系统; 从而使操作系统可以识别每个分区的文件系统,从而实现文件的存储和管理; 创建硬盘分区后,还不能立即使用,还需要创建文件系统,即格式化; 格式化后常见的磁盘格式有:FAT(FAT16)、FAT32、NTFS、ext2、ext3等。 (2)硬盘分区类型 不同类型磁盘支持分区的数量有限制。 主分区:主直接在硬盘上划分的,一个硬盘可以有1到3个主分区和1个扩展分区。 扩展分区:是一个概念,实际在硬盘中是看不到的,也无法直接使用扩展分区,在扩展分区中建立逻辑分区。 (3)容量 硬盘的容量 = 主分区的容量 + 扩展分区的容量 扩展分区的容量 = 各个逻辑分区的容量之和 (4)Linux系统下磁盘分区设备名称 设备 介绍 设备在Linux中的文件名 IDE硬盘Hard Disk Integrated Drive Electronics电子集成驱动器 /dev/hd是IDE接口硬盘分区,一般用于普通桌面和服务器 /dev/hd[字母递增][数字递增] SCSI光盘Solid Disk Small Computer System Interface 小型计算机系统接口 /dev/sd是SCSI接口硬盘分区,一般用于服务器 /dev/sd[字母递增][数字递增] virtio虚拟磁盘Virtual Disk /dev/vd虚拟磁盘分区,一般用于在虚拟机上扩展存储空间 /dev/vd[字母递增][数字递增] 注:字母表示硬盘,数字代表硬盘的分区 比如:/dev/hda1表示第一块硬盘的第一个分区 (5)管理磁盘分区 # 分区管理[root@localhost test]# fdisk -l # 只有一块磁盘/dev/sdaDisk /dev/sda: 42.9 GB, 42949672960 bytes, 83886080 sectorsUnits = sectors of 1 * 512 = 512 bytesSector size (logical/physical): 512 bytes / 512 bytesI/O size (minimum/optimal): 512 bytes / 512 bytesDisk label type: dosDisk identifier: 0x0009ef1a# 只有一个分区/dev/sda1 Device Boot Start End Blocks Id System/dev/sda1 * 2048 83886079 41942016 83 Linux # 查看容量、占用空间、剩余空间[root@localhost test]# df -h -TFilesystem Type Size Used Avail Use% Mounted ondevtmpfs devtmpfs 1.9G 0 1.9G 0% /devtmpfs tmpfs 1.9G 0 1.9G 0% /dev/shmtmpfs tmpfs 1.9G 8.7M 1.9G 1% /runtmpfs tmpfs 1.9G 0 1.9G 0% /sys/fs/cgroup/dev/sda1 xfs 40G 23G 18G 57% /tmpfs tmpfs 379M 0 379M 0% /run/user/0 6、磁盘高可用:磁盘冗余阵列 (1)简介 磁盘阵列高可用方案 - 独立磁盘冗余阵列(RAID - Redundant Array of Independent Disks)。 是一种提供高可用性和数据容错性的数据存储技术,把几块硬盘组成一个阵列,并将它们的数据分布在不同的磁盘上。 在磁盘发生故障时保护数据,还可以提高I/O性能,使系统能够更快地完成任务。 简单来说,就是把相同的数据存储在多个硬盘的不同的地方,储存冗余数据增加了容错性。 根据性能、容量、可靠性,有多个级别,比如RAID0、RAID1、RAID5、RAID10。 (2)RAID0磁盘阵列 至少需要两块硬盘,磁盘越多,读写速度越快,读写速度约等于一个磁盘的吞吐量 * 磁盘数,没有冗余。 这种方案磁盘利用率100%,安全性最低,一块硬盘出现故障就会导致数据损坏。 读写性能好。 (类似redis、mongodb的数据分片存储) (3)RAID1镜像阵列 全部数据都分别复制到多块硬盘上,当其中一块硬盘出现故障时,另一块硬盘的数据可以被立即使用,从而保证数据的安全性。 每次写入数据时都会同时写入镜像盘,读写性能较低,只能用两块硬盘,一块硬盘冗余,磁盘利用率为50%。 优点是数据冗余性高,缺点是读写性能比一般硬盘差。 适合服务器、数据库存储等领域。 (4)RAID5条带阵列 至少需要3块磁盘,一块磁盘冗余,是将多块磁盘按特定顺序组合起来,是最通用流行的配置方式。 在每块磁盘上都会存储1份数据和1份校验信息,1块硬盘出现故障时,根据另外2块磁盘的校验信息可以恢复数据。 这种存储方式只允许有一块硬盘出现故障,出现故障时需要尽快更换。 综合了RAID0和RAID1的优点和缺点,是RAID0和RAID1的折中方案。 适合需要安全和成本兼顾的领域,性能要求稍高,比如金融数据库存储。 (5)RAID10、RAID50 安全性和读写性能高,但是价格昂贵。 7、磁盘IO性能分析 (1)iostat sysstat提供了Linux性能监控的工具集,包括iostat、mpstat、pidstat、sar等。 iostat查看系统综合的磁盘IO情况 # -c 仅显示CPU状态统计信息# -d 仅显示磁盘统计信息# -k 或 -m 以Kb或Mb为单位显示,常用-h可读性高# -p 指定显示IO的设备,ALL表示显示所有# -x 显示详细信息 字段 说明 【重要】r/s 每秒发送给磁盘的读请求次数,r/s+w/s 是磁盘 IOPS 【重要】w/s 每秒发送给磁盘的写请求次数,r/s+w/s 是磁盘IOPS 【重要】rkB/s 每秒从磁盘读取的数据量,rkB/s+wkB/s 是吞吐量 【重要】wkB/s 每秒向磁盘写入的数据量,rkB/s+wkB/s 是吞吐量 【重要】r_await 读请求处理完成等待时间,包括在队列中的等待时间和设备实际处理时间 rawait+w_await,是RT响应时间 【重要】w_await 写请求处理完成等待时间,包括在队列中的等待时间和设备实际处理时间 r_await+w_await,是RT响应时间 【重要】aqu-sz 平均请求队列长度 rareq-Sz 平均读请求大小 wareg-Sz 平均写请求大小 【重要】%util 磁盘处理I/O的时间百分比,表示的是磁盘的忙碌情况;如果>80%就是磁盘可能处于忙碌状态 -秒中有百分之多少的时间用于I/O操作,或者说一秒中有多少时间I/O队列是非空的 (2)iotop 参数: -o:只显示正在读写磁盘的程序 -d:跟一个数值,表示iotop命令刷新时间 三、深入理解操作系统IO底层 1、DMA(Direct Memory Access) (1)应用程序从磁盘读写数据的时序图(未用DMA技术) 我们发现,应用程序如果想从磁盘读取数据,CPU会发生两次上下文的切换,并且数据会进行两次拷贝。 (2)使用DMA(Direct Memory Access)直接内存访问 直接内存访问,直接内存访问是计算机科学中的一种内存访问技术。 DMA之前,要把外设的数据读入内存或把内存的数据传送到外设,一般都要通过CPU控制完成,利用中断技术。 DMA允许某些硬件系统能够独立于CPU直接读写操作系统的内存,不需要CPU介入处理。 数据传输操作在一个DMA控制器(DMAC)的控制下进行,在传输过程中CPU可以继续进行其它的工作。 在大部分时间CPU和I/O操作都处于并行状态,系统的效率更高。 此时,如果是读数据: 1、操作系统检查内核缓冲区读取,如果存在则直接把内核空间的数据copy到用户空间(CPU处理),应用程序即可使用。 2、如果内核缓冲区没数据,则从磁盘中读取文件数据到内核缓冲区(DMA处理),再把内核空间的数据copy到用户空间(CPU处理),应用程序即可使用。 3、硬盘 ->内核缓冲区 ->用户缓冲区。 写操作: 根据操作系统的写入方式不一样,buffer IO和direct IO,写入磁盘时机不一样。 buffer IO:应用程序把数据从用户空间copy到内核空间的缓冲区(CPU处理),再把内核缓冲区的数据写到磁盘(DMA处理)。 direct IO:应用程序把数据直接从用户态地址空间写入到磁盘中,直接跳过内核空间缓冲区,减少操作系统缓冲区和用户地址空间的拷贝次数,降低了CPU和内存开销。 读网络数据: 网卡Socket(类似磁盘)中读取客户端发送的数据到内核空间(DMA处理),再把内核空间的数据copy到用户空间(CPU处理),然后应用程序使用。 写网络数据: 用户缓冲区中的数据copy到内核缓冲区的Socket Buffer中(CPU处理),再将内核空间中的Socket BUffer拷贝到Socket协议栈(网卡设备)进行传输(DMA处理)。 (3)DMA技术里面的损耗 (读)从磁盘的缓冲区到内核缓冲区的拷贝工作; (读)从网卡设备到内核的socket buffer的拷贝工作; (写)从内核缓冲区到磁盘缓冲区的拷贝工作; (写)从内核的socket buffer到网卡设备的拷贝工作。 所以,内核缓冲区到用户缓冲区之间的拷贝工作仍然由CPU负责。 以下是应用程序从磁盘读取文件到发送到网络的流程,程序先read数据,然后write网络,其中包含四次内核态和用户态的切换、四次缓冲区的拷贝: DMA技术虽然能提高一部分性能,但是仍然有一些不必要的资源损耗,其中包括CPU的用户态和内核态的切换、CPU内存拷贝的消耗。 2、零拷贝 (1)概念 零拷贝旨在减少不必要的内核缓冲区跟用户缓冲区之间的拷贝工作,从而减少CPU的开销和减少了kernel和user模式的上下文切换,提升性能。 从磁盘中读取文件通过网络发送出去,只需要拷贝2/3次和2/4的内核态和用户态的切换即可。 ZeroCopy技术实现有两种(内核态和用户态切换次数不一样): 方式一:mmap + write; 方式二:sendfile。 (2)mmap实现 mmap+write是ZeroCopy的实现方式之一。 操作系统都使用虚拟内存,虚拟地址通过多级页表来映射物理地址,多个虚拟内存可以指向同一个物理地址,虚拟内存的总空间远大于物理内存空间。 如果把内核空间和用户空间的虚拟地址映射到同一个物理地址,就不需要来回复制数据了。 mmap系统调用函数会直接把内核缓冲区里的数据映射到用户空间,这样内核空间和用户空间就不需要进行数据拷贝操作,节省了CPU开销。 相关函数(C):mmap()读取,write()写出。 还是以应用程序从磁盘读取文件到发送到网络的流程为例,步骤: 1、应用程序先调用mmap()方法,将数据从磁盘拷贝到内核缓冲区,返回结束(DMA负责); 2、再调用write(),内核缓冲区的数据直接拷贝到内核socket buffer(CPU负责); 3、然后把内核缓冲区的Socket Buffer直接拷贝给Socket协议栈,即网卡设备中,返回结束(DMA负责)。 没用零拷贝时,发生4次CPU上下文切换和4次数据拷贝。 使用mmap,CPU用户态和内核态上下文切换仍然是4次,和3次数据拷贝(2次DMA拷贝,1次CPU拷贝)。 减少了1次CPU拷贝(只有内核之间有一次拷贝。) (3)sendfile实现 sendfile是ZeroCopy的另一种实现方式。 Linux kernal 2.1新增了一个发送文件的系统调用函数sendfile()。 替代read()和write()两个系统调用,减少一次系统调用,即减少2次CPU上下文切换的开销。 调用sendfile(),从磁盘读取数据到内核缓冲区,然后直接把内核缓冲区的数据拷贝到socket buffer缓冲区里,再把内核缓冲区的Socket Buffer直接拷贝给Socket协议栈,即网卡设备中(DMA负责)。 相关函数(C):sendfile() 还是以应用程序从磁盘读取文件到发送到网络的流程为例,步骤: 1、应用程序先调用sendfile()方法,将数据从磁盘拷贝到内核缓冲区(DMA负责); 2、然后把内核缓冲区的数据直接拷贝到内核socket buffer(CPU负责); 3、然后把内核缓冲区的Socket Buffer直接拷贝给Socket协议栈,即网卡设备中,返回结束(DMA负责)。 没用零拷贝时,发生4次CPU上下文切换和4次数据拷贝。 使用sendfile(),CPU用户态和内核态上下文切换是2次,3次数据拷贝(2次DMA拷贝,1次CPU拷贝)。 (4)改进的sendfile linux2.4+版本之后改进了sendfile,利用DMA Gather(带有收集功能的DMA),变成了真正的零拷贝(没有CPU Copy)。 还是以应用程序从磁盘读取文件到发送到网络的流程为例,步骤: 1、应用程序先调用sendfile()方法,将数据从磁盘拷贝到内核缓冲区(DMA负责); 2、把内存地址、偏移量的缓冲区fd描述符 拷贝到Socket Buffer中去,(拷贝很少的数据,可忽略,本质和虚拟内存的解决方法思路一样,就是内存地址的记录); 3、然后把内核缓冲区的Socket Buffer直接拷贝给Socket协议栈,即网卡设备中,返回结束(DMA负责)。 (5)splice Linux 从 2.6.17 支持 splice。 数据从磁盘读取到 OS 内核缓冲区后,在内核缓冲区直接可将其转成内核空间其他数据buffer,而不需要拷贝到用户空间。 如下图所示,从磁盘读取到内核 buffer 后,在内核空间直接与 socket buffer 建立 pipe管道。 和 sendfile()不同的是,splice()不需要硬件支持。 注意 splice 和 sendfile 的不同,sendfile 是 DMA 硬件设备不支持的情况下将磁盘数据加载到 kernel buffer 后,需要一次 CPU copy,拷贝到 socket buffer。 而 splice 是更进一步,连这个 CPU copy 也不需要了,直接将两个内核空间的 buffer 进行 pipe。 splice 会经历 2 次拷贝: 0 次 cpu copy 2 次 DMA copy; 以及 2 次上下文切换 3、总结 (1)零拷贝的目标 解放CPU,避免CPU做太多事情; 减少内存带宽占用; 减少用户态和内核态上下文切换过多; 在文件较小的时候mmap用时更短,文件较大时sendfile方式最优。 (2)零拷贝方式对比 sendfile: 无法在调用过程中修改数据,只适用于应用程序不需要对所访问数据进行处理修改情况; 比如静态文件传输、MQ的Broker发送消息给消费者; 想要在传输过程中修改数据,可以使用mmap系统调用; 文件大小:适合大文件传输; 切换和拷贝:2次上下文切换,最少2次数据拷贝。 mmap: mmap调用可以在应用程序中直接修改Page Cache中的数据,使用的是mmap+write两步; 调用比sendfile成本高,但优于传统I/O的零拷贝实现方式,虽然比sendfile多了上下文切换; 用户空间与内核空间并不需要数据拷贝,在正确使用情况下并不比sendfile效率差; 适用于多个线程以只读的方式同时访问同一个文件,mmap机制下多线程共享同一物理内存,节约内存; 文件大小:适合小数据量读写; 切换和拷贝:4次上下文切换,3次数据拷贝。
在刚开始接触STM32的时候,使用的keil作为IDE,由于在这之前,使用过VS, 使用过eclipse,因而在使用keil之后,实在难以忍受keil编辑器简陋的功能,可以说是极其糟糕的写代码体验。 之后,尝试过各种IDE,使用eclipse+keil,结果发现eclipse对C语言的支持也是鸡肋,使用emBits+gcc,需要和其他人协同的话就比较麻烦,之后发现了platformIO,也是使用gcc作为编译器,不过只支持HAL库,而且还有一个重要的原因,同事都是用的keil,如果我使用gcc,就不能协同工作了。 最后,通过使用VS Code + keil的方式,完美解决了写代码的体验问题,以及工程协作问题,其实网上使用VS Code作为编辑器,keil作为编译器的教程很多,不过基本都是需要在VS Code中编辑,然后在keil中编译,下载,调试,本文就要实现编辑,编译,下载,调试,全部使用VS Code。 Part1环境 (1)VS Code; (2)keil;python; (3)GNU Arm Embedded Toolchain(arm gcc工具链); (4)C/C++(VS Code 插件); (5)Cortex-Debug(VS Code 插件); (6)其他VS Code插件(提升体验)。 Part2前提 正式写代码之前,首先需要建立好一个工程,这个需要使用keil完成,包括工程配置,文件添加… Part3编辑 在安装好VS Code插件之后,VS Code编写C代码本身体验就已经很好了, 但是,因为我们使用的是keil环境,所以需要配置头文件包含,宏定义等,在工程路径的.vscode文件夹下打开c_cpp_properties.json文件,没有自己新建一个,内容配置如下: { "configurations": [ { "name": "STM32", "includePath": [ "D:/Program Files/MDK5/ARM/ARMCC/**", "${workspaceFolder}/**", "" ], "browse": { "limitSymbolsToIncludedHeaders": true, "databaseFilename": "${workspaceRoot}/.vscode/.browse.c_cpp.db", "path": [ "D:/Program Files/MDK5/ARM/ARMCC/**", "${workspaceFolder}/**", "" ] }, "defines": [ "_DEBUG", "UNICODE", "_UNICODE", "__CC_ARM", "USE_STDPERIPH_DRIVER", "STM32F10X_MD" ], "intelliSenseMode": "msvc-x64" } ], "version": 4 } 其中,需要在includePath和path中添加头文件路径,${workspaceFolder}/**是工程路径,不用改动,额外需要添加的是keil的头文件路径, 然后在defines中添加宏,也就是在keil的Options for Target的C++选项卡中配置的宏,然后就可以体验VS Code强大的代码提示,函数跳转等功能了(甩keil的编辑器一整个时代)。 Part4编译、烧录 编译和烧录通过VS Code的Task功能实现,通过Task,使用命令行的方式调用keil进行编译和烧录。 keil本身就支持命令行调用,具体可以参考keil的手册,这里就不多说了,但是问题在于,使用命令行调用keil,不管是什么操作,他的输出都不会输出到控制台上!!!(要你这命令行支持有何用) 不过好在,keil支持输出到文件中,那我们就只能利用这个做点骚操作了。一边执行命令,一边读取文件内容并打印到控制台,从而就实现了输出在控制台上,我们就能直接在VS Code中看到编译过程了 为此,我编写了一个Python脚本,实现keil的命令行调用并同时读取文件输出到控制台。 #!/usr/bin/python # -*- coding:UTF-8 -*- import os import threading import sys runing = True def readfile(logfile): with open(logfile, 'w') as f: pass with open(logfile, 'r') as f: while runing: line = f.readline(1000) if line != '': line = line.replace('\\', '/') print(line, end = '') if __name__ == '__main__': modulePath = os.path.abspath(os.curdir) logfile = modulePath + '/build.log' cmd = '\"D:/Program Files/MDK5/UV4/UV4.exe\" ' for i in range(1, len(sys.argv)): cmd += sys.argv[i] + ' ' cmd += '-j0 -o ' + logfile thread = threading.Thread(target=readfile, args=(logfile,)) thread.start() code = os.system(cmd) runing = False thread.join() sys.exit(code) 此脚本需要结合VS Code的Task运行,通过配置Task,我们还需要匹配输出中的错误信息(编译错误),实现在keil中,点击错误直接跳转到错误代码处,具体如何配置请参考VS Code的文档,这里给出我的Task。 { // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ { "label": "build", "type": "shell", "command": "py", "args": [ "-3", "${workspaceFolder}/scripts/build.py", "-b", "${config:uvprojxPath}" ], "group": { "kind": "build", "isDefault": true }, "problemMatcher": [ { "owner": "c", "fileLocation": [ "relative", "${workspaceFolder}/Project" ], "pattern": { "regexp": "^(.*)\\((\\d+)\\):\\s+(warning|error):\\s+(.*):\\s+(.*)$", "file": 1, "line": 2, "severity": 3, "code": 4, "message": 5 } } ] }, { "label": "rebuild", "type": "shell", "command": "py", "args": [ "-3", "${workspaceFolder}/scripts/build.py", "-r", "${config:uvprojxPath}" ], "group": "build", "problemMatcher": [ { "owner": "c", "fileLocation": [ "relative", "${workspaceFolder}/Project" ], "pattern": { "regexp": "^(.*)\\((\\d+)\\):\\s+(warning|error):\\s+(.*):\\s+(.*)$", "file": 1, "line": 2, "severity": 3, "code": 4, "message": 5 } } ] }, { "label": "download", "type": "shell", "command": "py", "args": [ "-3", "E:\\Work\\Store\\MyWork\\STM32F1\\FreeModbus_M3\\scripts\\build.py", "-f", "${config:uvprojxPath}" ], "group": "test" }, { "label": "open in keil", "type": "process", "command": "${config:uvPath}", "args": [ "${config:uvprojxPath}" ], "group": "test" } ] } 对于使用ARM Compiler 6编译的工程,build和rebuild中的problemMatcher应该配置为: "problemMatcher": [ { "owner": "c", "fileLocation": ["relative", "${workspaceFolder}/MDK-ARM"], "pattern": { "regexp": "^(.*)\\((\\d+)\\):\\s+(warning|error):\\s+(.*)$", "file": 1, "line": 2, "severity": 3, "message": 4, } } ] 文件中的config:uvPath和config:uvprojxPath分别为keil的UV4.exe文件路径和工程路径(.uvprojx),可以直接修改为具体路径,或者在VS Code的setting.json中增加对应的项,至此,我们已经完美实现了在VS Code中编辑,编译,下载了。 编译输出: 有错误时输出: 错误匹配: Part5调试 调试需要使用到Cortex-Debug插件,以及arm gcc工具链,这部分可以参考Cortex-Debug的文档,说的比较详细; 首先安装Cortex-Debug插件和arm gcc工具链,然后配置好环境路径,如果使用Jlink调试,需要下载Jlink套件,安转好之后,找到JLinkGDBServerCL.exe这个程序,在VS Code的设置中添加"cortex-debug.JLinkGDBServerPath": "C:/Program Files (x86)/SEGGER/JLink/JLinkGDBServerCL.exe",后面的路径是你自己的路径。 这里补充一下arm gcc工具链的配置:"cortex-debug.armToolchainPath": "D:\\Program Files (x86)\\GNU Arm Embedded Toolchain\\9 2020-q2-update\\bin",后面的路径是你自己的路径。如果使用STLink调试,需要下载stutil工具,在GitHub上搜索即可找到,同样配置好路径即可。 以上步骤弄好之后,可以直接点击VS Code的调试按钮,此时会新建luanch.json文件,这个文件就是VS Code的调试配置文件,可参考我的文件进行配置。 { // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Cortex Debug(JLINK)", "cwd": "${workspaceRoot}", "executable": "${workspaceRoot}/Project/Objects/Demo.axf", "request": "attach", "type": "cortex-debug", "servertype": "jlink", "device": "STM32F103C8", "svdFile": "D:\\Program Files\\ARM\\Packs\\Keil\\STM32F1xx_DFP\\2.3.0\\SVD\\STM32F103xx.svd", "interface": "swd", "ipAddress": null, "serialNumber": null }, { "name": "Cortex Debug(ST-LINK)", "cwd": "${workspaceRoot}", "executable": "${workspaceRoot}/Project/Objects/Demo.axf", "request": "attach", "type": "cortex-debug", "servertype": "stutil", "svdFile": "D:\\Program Files\\ARM\\Packs\\Keil\\STM32F1xx_DFP\\2.3.0\\SVD\\STM32F103xx.svd", "device": "STM32F103C8", "v1": false } ] } 注意其中几个需要修改的地方,executable修改为你的工程生成的目标文件,也就是工程的.axf文件,svdFile用于对MCU外设的监控,该文件可以在keil的安装路径中找到,可以参考我的路径去找,配置完成后,再次点击调试按钮即可进行调试。 相比keil自己的调试功能,VS Code还支持条件断点,可以设置命中条件,次数等,可以极大的方便调试。 总结 通过以上的配置,我们基本上,除了建立工程和往工程中添加文件,其他完全不需要打开keil,所以也无妨说一句,再见,智障keil!
1.Keil编程开发环境(必备) 这个是最核心的工具了,用来编写和编译程序,还有一个最重要的功能就是仿真,快速地帮你定位程序BUG,不过要配合ST-Link或者其他仿真器用。 一般51我是用C51V9.0的,STM32我是用Keil4.72...