Skip to content

Latest commit

 

History

History
575 lines (321 loc) · 30.1 KB

File metadata and controls

575 lines (321 loc) · 30.1 KB

一、嵌入式系统基础

嵌入式系统是将硬件和软件组件相结合以解决较大系统或设备中的特定任务的计算机系统。 与通用计算机不同,它们是高度专业化和优化的,只执行一项任务,但做得非常好。

它们在我们周围无处不在,但我们很少注意到它们。 你几乎可以在每个家用电器或小工具中找到它们,比如微波炉、电视机、网络存储或智能恒温器。 您的汽车包含几个相互连接的嵌入式系统,用于处理刹车、燃油喷射和信息娱乐。

在本章中,我们将讨论以下有关嵌入式系统的主题:

  • 探索嵌入式系统
  • 利用有限的资源工作
  • 查看性能影响
  • 使用不同的架构
  • 处理硬件错误
  • 使用 C++ 进行嵌入式开发
  • 远程部署软件
  • 远程运行软件
  • 日志记录和诊断

探索嵌入式系统

作为较大系统或设备的一部分,为解决特定问题而创建的每个计算机系统都是嵌入式系统。 即使是您的通用 PC 或笔记本电脑也包含许多嵌入式系统。 键盘、硬盘、网卡或 Wi-Fi 模块-每一个都是嵌入式系统,带有处理器(通常称为微控制器)和自己的软件(通常称为固件)。

现在让我们深入了解嵌入式系统的不同功能。

它们与桌面或 Web 应用有何不同?

与台式机或服务器相比,嵌入式系统最显著的特点是其专门用于完成特定任务的硬件和软件的紧密耦合。

嵌入式设备可以在各种物理和环境条件下工作。 它们中的大多数并不是专为在专用的有条件的数据中心或办公室工作而设计的。 它们必须在无法控制的环境中发挥作用,通常没有任何监督和维护。

由于它们是专门化的,因此会精确计算硬件要求,以完成尽可能经济高效的任务。 因此,该软件的目标是以最少的储备或没有储备的方式利用 100%的可用资源。

与常规台式机和服务器相比,嵌入式系统的硬件有很大的不同。 每个系统的设计都是个性化的。 它们可能需要非常具体的 CPU 和将其连接到存储器和外部硬件的原理图。

嵌入式系统是为与外部硬件通信而设计的。 嵌入式程序的主要部分是检查状态、读取输入、发送数据或控制外部设备。 嵌入式系统没有用户界面是很常见的。 与在传统桌面或 Web 应用上执行相同操作相比,这使得开发、调试和诊断要困难得多。

嵌入式系统的类型

嵌入式系统涵盖了广泛的使用案例和技术-从用于自动驾驶或大规模存储系统的强大系统,到用于控制灯泡或 LED 显示器的微型微控制器。

根据硬件的集成度和专业化程度,嵌入式系统大致可以分为以下几类:

  • 微控制器(MCU)
  • A****片上系统(SoC)
  • 专用集成电路(ASIC)
  • 现场可编程门阵列(FPGA)

微控制器

MCU 是专为嵌入式应用设计的通用集成电路。 单个 MCU 芯片通常包含一个或多个 CPU、存储器和可编程输入/输出外部设备。 它们的设计允许它们直接与传感器或执行器连接,而无需添加任何额外组件。

MCU 广泛应用于汽车发动机控制系统、医疗设备、遥控器、办公机器、家用电器、电动工具和玩具。

它们的 CPU 各不相同,从简单的 8 位处理器到更复杂的 32 位甚至 64 位处理器。

MCU 种类繁多,目前最常见的有以下几种:

  • 英特尔 MCS-51 或 8051 MCU。
  • 爱特梅尔公司的 AVR
  • MicroChip Technology 的可编程接口控制器(PIC)
  • 各种基于 ARM 的 MCU

片上系统

SoC 是一种集成电路,它将解决某类特定问题所需的所有电子电路和部件组合在一块芯片上。

它可能包含数字、模拟或混合信号功能,具体取决于应用。 将大多数电子部件集成在一块芯片上有两大好处:小型化和低功耗。 与集成度较低的硬件设计相比,SoC 需要的功耗要低得多。 硬件和软件层面的功耗优化使其能够创建在没有外部电源的情况下依靠电池工作数天、数月甚至数年的系统。 通常,它还集成了射频信号处理,再加上其紧凑的物理尺寸,使其成为移动应用的理想解决方案。 此外,SoC 通常用于汽车行业、可穿戴电子产品以及物联网(IoT):

Figure 1.1: A Raspberry Pi Model B+

Raspberry Pi 系列单板计算机就是基于 SoC 设计的系统示例。 B+型构建在 Broadcom BCM2837B0 SoC 之上,集成了一个基于 ARM 的四核 1.4 Hz CPU、1 GB 内存、一个网络接口控制器和四个以太网接口。

该主板具有四个 USB 接口、一个用于启动操作系统和存储数据的 MicroSD 卡端口、以太网和 Wi-Fi 网络接口、HDMI 视频输出以及一个 40 针 GPIO 接口,用于连接自定义外部硬件。

它随 Linux 操作系统一起提供,是教育和DIY项目的绝佳选择。

专用集成电路

专用集成电路ASIC是由其制造商为特定用途定制的集成电路。 定制是一个昂贵的过程,但允许它们满足基于通用硬件的解决方案通常不可行的要求。 例如,现代高效比特币矿工通常建立在专用 ASIC 芯片之上。

为了定义 ASIC 的功能,硬件设计人员使用一种硬件描述语言,如 Verilog 或 VHDL。

现场可编程门阵列

与 SoC、ASIC 和 MCU 不同,现场可编程门阵列FPGA是可以在制造后在硬件级别重新编程的半导体器件。 它们基于通过可编程互连连接的可配置逻辑块(CLB)的矩阵。 开发人员可以对互连进行编程,以根据他们的要求执行特定功能。 FPGA 采用硬件定义语言(HDL)编程。 它允许实现数字功能的任意组合,以便非常快速和高效地处理大量数据。

利用有限的资源工作

一种常见的误解是,嵌入式系统基于的硬件比常规台式机或服务器硬件慢得多。 虽然情况通常如此,但并非总是如此。

某些特定应用可能需要大量内存的大量计算能力。 例如,自动驾驶需要内存和 CPU 资源来实时处理来自使用人工智能算法的各种传感器的大量数据。 另一个例子是利用大量内存和资源进行数据缓存、复制和加密的高端存储系统。

在任何一种情况下,嵌入式系统硬件的设计都是为了最小化整个系统的成本。 对于使用嵌入式系统的软件工程师来说,结果是资源稀缺。 他们应该利用所有可用的资源,非常认真地对待性能和内存优化。

查看性能影响

大多数嵌入式应用都针对性能进行了优化。 正如前面所讨论的,目标 CPU 被选择为经济高效的,开发人员可以利用它所能提供的所有计算能力。 另一个因素是与外部硬件的通信。 这通常需要精确而快速的反应时间。 因此,脚本、可解释的字节码语言(如 Python 或 Java)的空间有限。 大多数嵌入式程序都是用编译成本机代码的语言编写的,主要是 C 和 C++。

为了实现最高性能,嵌入式程序利用了编译器的所有性能优化功能。 现代编译器非常擅长代码优化,其性能超过了熟练开发人员用汇编语言编写的代码。

然而,工程师不能仅仅依靠编译器提供的性能优化。 为了实现最高效率,他们必须考虑目标平台的具体情况。 通常用于在 x86 平台上运行的桌面或服务器应用的编码实践对于 ARM 或 MIPS 等不同的体系结构可能是低效的。 目标体系结构的特定功能的利用通常会给程序带来显著的性能提升。

使用不同的架构

桌面应用的开发人员通常很少关注硬件架构。 首先,它们通常使用高级编程语言来隐藏这些复杂性,但代价是性能有所下降。 其次,在大多数情况下,他们的代码运行在 x86 架构上,他们通常认为 x86 的特性是理所当然的。 例如,它们可能假设int的大小是32位,但在许多情况下并非如此。

嵌入式开发人员需要处理更多种类的体系结构。 即使他们不是用目标平台原生的汇编语言编写代码,他们也应该知道所有的 C 和 C++ 基础类型都是依赖于体系结构的;标准只保证int至少是16位。 他们还应该了解特定体系结构的特点,如字符顺序对齐,并考虑到浮点或 64 位数字的操作在 x86 体系结构上相对便宜,在其他体系结构上可能要昂贵得多。

字节顺序

Endianness定义表示大数值的字节在内存中的存储顺序。

有两种类型的字符顺序:

  • Big-endian:首先存储最高有效字节。 将0x0102030432 位值存储在ptr地址如下:

    | 内存中的偏移量 | | | ptr | 。 0x01 | | ptr + 1 | 0x02 0x02 | | ptr + 2 | 0x03 0x03 | | ptr + 3 | 0x04 |

大端架构的例子有 AVR32 和摩托罗拉 68000。

  • Little-endian:首先存储最低有效字节。 将0x0102030432 位值存储在ptr地址如下:

    | 内存中的偏移量 | | | ptr | 。 0x04 | | ptr + 1 | 0x03 0x03 | | ptr + 2 | 0x02 0x02 | | ptr + 3 | 0x01 |

X86 架构是小端的。

  • 双字节顺序:硬件支持可切换的字节顺序。 一些示例包括 PowerPC、ARMv3 和前面的示例。

在与其他系统交换数据时,字节序尤其重要。 如果开发人员按原样发送0x0102030432 位整数,如果接收方的字符顺序与发送方的字符顺序不匹配,则可能会将其读取为0x04030201。 这就是数据应该序列化的原因。

此 C++ 代码段可用于确定系统的字节顺序:

#include <iostream>
int main() {
  union {
    uint32_t i;
    uint8_t c[4];
  } data;
  data.i = 0x01020304;
  if (data.c[0] == 0x01) {
    std::cout << "Big-endian" << std::endl;
  } else {
    std::cout << "Little-endian" << std::endl;
  }
}

对齐

处理器不以字节为单位读写数据,而是以个内存字-与其数据地址大小匹配的区块为单位读取和写入数据。 32 位处理器使用 32 位字,64 位处理器使用 64 位字,依此类推。

当字对齐时,读写效率最高-数据地址是字大小的倍数。 例如,对于 32 位架构,0x00000004地址是对齐的,而0x00000005是未对齐的。

编译器自动对齐数据以实现最高效的数据访问。 当涉及到结构时,结果可能会让那些没有意识到对齐的开发人员感到惊讶:

 struct {

    uint8_t c;

    uint32_t i;

  } a = {1, 1};

  std::cout << sizeof(a) << std::endl;

前面的代码片段的输出是什么? uint8_t的大小是1uint32_t的大小是4。 开发人员可能会认为结构的大小是各个大小的总和。 然而,结果在很大程度上取决于目标架构。

对于 x86,结果是8。 让我们在i之前再添加一个uint8_t字段:

struct {

    uint8_t c;

    uint8_t cc;

    uint32_t i;

  } a = {1, 1};

  std::cout << sizeof(a) << std::endl;

结果仍然是8! 编译器通过添加填充字节,根据对齐规则优化数据字段在结构中的位置。 规则依赖于体系结构,对于其他体系结构,结果可能会有所不同。 因此,如果没有序列化*,*,就不能在两个不同的系统之间直接交换结构,这将在第 8 章通信和序列化中进行更深入的解释。

除了 CPU,访问数据对齐对于通过硬件地址转换机制进行高效的内存映射也是至关重要的。 现代操作系统操作 4KB 的内存块或页面来将进程虚拟地址空间映射到物理内存。 在 4 KB 边界上对齐数据结构可以提高性能。

固定宽度整数类型

C 和 C++ 开发人员经常忘记基本数据类型(如charshortint)的大小取决于体系结构。 为了使代码可移植,嵌入式开发人员通常使用固定大小的整数类型来显式指定数据字段的大小。

最常用的数据类型如下:

| 宽度 | 签名 | 无符号 | | 8 位 | int8_t | uint8_t | | 16 位 | int16_t | uint16_t | | 32 位 | int32_t | uint32_t |

指针大小还取决于体系结构。 开发人员通常需要寻址数组的元素,由于数组在内部表示为指针,因此偏移量表示取决于指针大小。 size_t是一种特殊的数据类型,以独立于体系结构的方式表示偏移量和数据大小。

处理硬件错误

嵌入式开发人员工作的一个重要部分就是处理硬件。 与大多数应用开发人员不同,嵌入式开发人员不能依赖硬件。 硬件故障有不同的原因,嵌入式开发人员必须区分纯粹的软件故障和由硬件故障或故障引起的软件故障。

硬件的早期版本

嵌入式系统基于为特定用例设计和制造的专用硬件。 这意味着在开发嵌入式系统的软件时,其硬件还没有稳定和良好的测试。 当软件开发人员在其代码行为中遇到错误时,并不一定意味着存在软件错误,但可能是硬件工作不正常所致。

很难对这类问题进行分类。 它们需要知识、直觉,有时还需要使用示波器来将问题的根源缩小到硬件。

硬件不可靠

硬件本质上是不可靠的。 每个硬件组件都有发生故障的可能性,开发人员应该意识到硬件随时可能出现故障。 存储在内存中的数据可能会因为内存故障而损坏。 通过通信信道传输的消息可能会因为外部噪声而改变。

嵌入式开发人员已经为这些情况做好了准备。 它们使用校验和或循环冗余校验(CRC)码来检测并在可能的情况下纠正损坏的数据。

环境条件的影响

高温、低温、高湿度、振动、灰尘等环境因素会显著影响硬件的性能和可靠性。 虽然开发人员设计他们的软件来处理所有潜在的硬件错误,但通常的做法是在不同的环境中测试系统。 此外,环境条件的知识可以在对问题的根本原因进行分析时提供重要线索。

使用 C++ 进行嵌入式开发

多年来,绝大多数嵌入式项目都是使用 C 编程语言开发的。 这种语言非常适合嵌入式软件开发人员的需求。 它提供了功能丰富且方便的语法,但同时,它的级别相对较低,并且不会对开发人员隐藏平台细节。

由于它的通用性、紧凑性和编译代码的高性能,它成为嵌入式世界事实上的标准开发语言。 C 语言的编译器适用于大多数(如果不是全部)体系结构;它们被优化以生成比手动编写的机器代码更高效的机器码。

随着时间的推移,嵌入式系统的复杂性增加了,开发人员面临着 C 语言的局限性,最显著的是容易出错的资源管理和缺乏高级抽象。 用 C 语言开发复杂的应用需要花费大量的精力和时间。

与此同时,C++ 也在不断发展,获得了新的功能并采用了编程技术,使其成为现代嵌入式系统开发人员的最佳选择。 这些新功能和新技术如下:

  • 你不用为你不用的东西付钱。
  • 面向对象的编程来计时代码的复杂性。
  • 资源获取是初始化(RAII)。
  • 例外。
  • 一个强大的标准库。
  • 线程和内存模型作为语言规范的一部分。

你不用为你不用的东西付钱

C++ 的座右铭之一是你不用为你不用的东西付费。 这种语言比 C 语言有更多的特性,但对于那些不使用的特性,它承诺零开销。

以虚拟函数为例:

#include <iostream>

class A {

public:

  void print() {

    std::cout << "A" << std::endl;

  }

};

class B: public A {

public:

  void print() {

    std::cout << "B" << std::endl;

  }

};

int main() {

  A* obj = new B;

  obj->print();

}

前面的代码将输出A,尽管obj指向B类的对象。 为了使其按预期工作,开发人员添加了一个关键字-virtual

#include <iostream>

class A {

public:

  virtual void print() {

    std::cout << "A" << std::endl;

  }

};

class B: public A {

public:

  void print() {

    std::cout << "B" << std::endl;

  }

};

int main() {

  A* obj = new B;

  obj->print();

}

在此更改之后,代码输出B,这是大多数开发人员期望得到的结果。 您可能会问,为什么 C++ 不强制每个方法在缺省情况下都是virtual。 Java 采用了这种方法,似乎没有任何缺点。

原因是virtual函数不是免费的。 函数解析在运行时通过虚拟表(函数指针数组)执行。 它稍微增加了函数调用时间的开销。 如果您不需要动态多态性,则无需付费。 这就是 C++ 开发人员添加virtual键盘的原因,明确同意增加性能开销的功能。

面向对象编程对代码复杂性进行计时

随着嵌入式程序的复杂性不断增加,使用 C 语言提供的传统过程化方法来管理它们变得越来越困难。 如果您看一看大型的 C 项目,比如 Linux 内核,您会发现它采用了面向对象编程的许多方面。

Linux 内核广泛使用封装,隐藏实现细节,并使用 C 结构提供对象接口。

虽然可以用 C 编写面向对象的代码,但用 C++ 编写要容易得多、方便得多,因为在 C++ 中,编译器可以为开发人员完成所有繁重的任务。

资源获取是初始化

嵌入式开发人员大量使用操作系统提供的资源:内存、文件和网络套接字。 C 开发人员使用 API 函数对获取和释放资源;例如,malloc声明一个内存块,free将其返回给系统。 如果开发人员出于某种原因忘记调用free,那么这块内存就会泄漏。 内存泄漏或资源泄漏通常是用 C:

#include <stdio.h>

#include <unistd.h>

#include <fcntl.h>

#include <string.h>

int AppendString(const char* str) {

  int fd = open("test.txt", O_CREAT|O_RDWR|O_APPEND);

 if (fd < 0) {

    printf("Can't open file\n");

    return -1;

  }

  size_t len = strlen(str);

  if (write(fd, str, len) < len) {

    printf("Can't append a string to a file\n");

    return -1;

  }

  close(fd);

  return 0;

}

前面的代码看起来是正确的,但它包含几个严重的问题。 如果write函数返回错误或写入的数据比请求的少(这是正确的行为),AppendString函数会记录错误并返回。 但是,如果它忘记关闭文件描述符,它就会泄漏。 随着时间的推移,越来越多的文件描述符泄漏,并且在某个时候,程序达到了打开文件描述符的极限,使得open函数的所有调用都失败。

C++ 提供了一个强大的编程习惯用法来防止资源泄漏:rai。 资源在对象构造函数中分配,在对象析构函数中释放。 这意味着仅在对象处于活动状态时才持有资源。 当对象被销毁时,它会自动释放:

#include <fstream>

void AppendString(const std::string& str) {

  std::ofstream output("test.txt", std::ofstream::app);

  if (!output.is_open()){

    throw std::runtime_error("Can't open file");

  }

  output << str;

}

请注意,此函数不会显式调用close。 文件在输出对象的析构函数中关闭,该函数在AppendString函数返回时自动调用。

例外情况

传统上,C 开发人员使用错误代码处理错误。 这种方法需要程序员的大量关注,并且是 C 程序中难以找到的错误的源泉。 忽略或忽略丢失的检查返回代码太容易了,从而掩盖了错误:

#include <stdio.h>

 #include <unistd.h>

 #include <fcntl.h>

 #include <iostream>

 #include <fstream>

 char read_last_byte(const char* filename) {

         char result = 0;

         int fd = open(filename, O_RDONLY);

         if (fd < 0) {

                printf("Can't open file\n");

                return -1;

       } 

         lseek(fd, -1, SEEK_END);

         size_t s = read(fd, &result, sizeof(result));

         if (s != sizeof(result)) {

                 printf("Can't read from file: %lu\n", s);

                 close(fd);

                 return -1;

        } 

         close(fd);

         return result;

 }

前面的代码至少有两个与错误处理相关的问题。 首先,不检查lseek函数调用的结果。 如果lseek返回错误,则函数将无法正常工作。 第二个问题更微妙,但更重要,也更难解决。 函数read_last_byte返回-1以指示错误,但它也是一个字节的有效值。 无法区分文件的最后一个字节是0xFF还是函数遇到错误。 要正确处理此情况,应按如下方式重新定义函数接口:

int read_last_byte(const char* filename, char* result);

该函数在出现错误时返回-1,否则返回0。 结果存储在通过引用传递的char变量中。 虽然这个界面是正确的,但对开发人员来说并不像原来的界面那么方便。

对于这类错误,最终随机崩溃的程序可能被认为是最好的结果。 如果它继续工作,悄悄地损坏数据或生成错误的结果,情况会更糟。

除此之外,实现逻辑的代码和负责错误检查的代码是交织在一起的。 代码变得难以阅读和理解,因此更容易出错。

尽管开发人员仍然可以继续使用返回代码,但在现代 C++ 中,推荐的错误处理方式是异常。 正确设计和正确使用异常可显著降低错误处理的复杂性,使代码具有可读性和健壮性。

使用异常使用 C++ 编写的相同函数看起来要干净得多:

char read_last_byte2(const char* filename) {

         char result = 0;

         std::fstream file;

         file.exceptions (

                 std::ifstream::failbit | std::ifstream::badbit );

         file.open(filename);

         file.seekg(-1, file.end);

         file.read(&result, sizeof(result));

         return result;

 }

强大的标准库

C++ 附带了一个功能丰富且功能强大的标准库。 许多需要 C 开发人员使用第三方库的函数现在都是标准 C++ 库的一部分。 这意味着更少的外部依赖,更稳定和可预测的行为,以及提高硬件架构之间的可移植性。

C++ 标准库附带了构建在最常用数据结构(如数组、二叉树和哈希表)之上的容器。 这些容器是通用的,可以有效地满足开发人员的大部分日常需求。 开发人员不需要花费时间和精力来创建他们自己的、往往容易出错的基本数据结构实现。

容器经过精心设计,最大限度地减少了对显式资源、分配或释放的需要,从而显著降低了内存或其他系统资源泄漏的可能性。

标准库还提供了许多标准算法,如findsortreplace、二进制搜索、集合运算和排列。 这些算法可以应用于任何公开集成器接口的容器。 与标准容器相结合,它们可以帮助开发人员专注于高级抽象,并用最少的额外代码将其构建在经过良好测试的功能之上。

线程和内存模型作为语言规范的一部分

C++ 11 标准引入了一个内存模型,该模型清楚地定义了多线程环境中 C++ 程序的行为。

对于 C 语言规范,内存模型超出了范围。 该语言本身并不知道线程或并行执行语义。 这取决于第三方库(如 pthread)为多线程应用提供所有必要的支持。

C++ 的早期版本遵循相同的原则。 多线程超出了语言规范的范围。 然而,具有支持指令重排序的多个流水线的现代 CPU 要求编译器的行为更具确定性。

因此,现代的 C++ 规范明确定义了线程类、各种类型的锁和互斥锁、条件变量和原子变量。 这为嵌入式开发人员提供了一个强大的工具包来设计和实现能够利用现代多核 CPU 的所有功能的应用。 由于工具包是语言规范的一部分,因此这些应用具有确定性行为,并且可移植到所有支持的体系结构。

远程部署软件

嵌入式系统的软件部署通常是一个复杂的过程,应该仔细设计、实现和测试。 有两个主要挑战:

  • 嵌入式系统通常部署在操作员难以进入或不切实际的地方。
  • 如果软件部署失败,系统可能无法运行。 这将需要一名熟练的技术人员进行干预,并需要额外的恢复工具。 这是昂贵的,而且往往是不可能的。

通过空中(OTA)更新的形式找到了连接到互联网的嵌入式系统的第一个挑战的解决方案。 系统定期连接到专用服务器并检查可用的更新。 如果找到软件的更新版本,则将其下载到设备并安装到永久存储器。

这种方法被智能手机、机顶盒(机顶盒)电器、智能电视和连接到互联网的游戏机制造商广泛采用。

在设计 OTA 更新时,系统架构师应该考虑影响整体解决方案可伸缩性和可靠性的许多因素。 例如,如果所有设备几乎同时检查更新,则会在更新服务器中产生较高的峰值负载,而使它们在所有其他时间都处于空闲状态。 随机化检查时间可以使负载均匀分布。 目标系统应设计为保留足够的永久内存,以便在应用更新映像之前下载完整的更新映像。 实现更新的软件映像下载的代码应该处理网络连接中断,并在连接恢复后恢复下载,而不是重新开始。 OTA 更新的另一个重要因素是安全性。 更新过程应该只接受正版更新图像。 更新由制造商进行加密签名,除非签名匹配,否则设备上运行的安装程序不会接受映像。

嵌入式系统的开发人员意识到,更新可能会因为不同的原因而失败;例如,更新过程中停电。 即使更新成功完成,新版本的软件也可能不稳定并在启动时崩溃。 预计即使在这种情况下,系统也能够恢复。

这是通过分离主要软件组件和引导加载程序来实现的。 引导加载器验证主要组件的一致性,例如操作系统内核和包含所有可执行文件、数据和脚本的根文件系统。 然后,它尝试运行操作系统。 如果出现故障,它将切换到以前的版本,该版本应该与新版本一起保存在永久存储器中。 硬件看门狗计时器用于检测和防止软件更新导致系统挂起的情况。

在软件开发和测试期间使用 OTA 或完全镜像重新刷新是不切实际的。 它大大减慢了开发过程。 工程师使用其他方式将他们的软件版本部署到开发系统,例如远程外壳或网络文件系统,允许在开发人员的工作站和目标板之间共享文件。

远程运行软件

嵌入式系统被设计为使用硬件和软件组件的特定组合来解决特定问题。 这就是为什么系统中的所有软件组件都是为实现这一目标而量身定做的。 所有不必要的功能都会被禁用,所有自定义软件都会集成到引导序列中。

用户不启动嵌入式程序;它们在系统引导时启动。 但是,在开发过程中,工程师需要在不重新启动系统的情况下运行他们的应用。

根据目标平台的类型,执行此操作的方式不同。 对于功能强大的基于 SoC 并运行抢占式多任务操作系统(如 Linux)的系统,可以使用远程 shell 来完成。

现代系统通常使用安全外壳(SSH)作为远程外壳。 目标系统运行 SSH 守护程序,等待传入连接。 开发人员使用客户端 SSH 程序(如 Linux 中的 SSH 或 Windows 中的 PuTTY)进行连接,以访问目标系统。 一旦连接,它们就可以像在本地计算机上一样使用嵌入式主板上的 Linux shell。

远程运行该程序的常见工作流程如下:

  1. 使用交叉编译工具包在本地系统中构建可执行程序。

  2. 使用scp工具将其复制到远程系统。

  3. 使用 SSH 连接到远程系统,并从命令行运行可执行文件。

  4. 使用相同的 SSH 连接,分析程序输出。

  5. 当程序终止或被开发人员中断时,将其日志取回到开发人员的工作站进行深入分析。

MCU 没有足够的资源用于远程外壳。 开发人员通常将编译后的代码直接上传到平台内存中,并从特定的内存地址启动代码执行。

日志记录和诊断

日志记录和诊断是任何嵌入式项目的一个重要方面。

在许多情况下,使用交互式调试器是不可能或不实用的。 硬件状态可能在几毫秒内发生变化。 程序在断点处停止后,开发人员没有足够的时间对其进行分析。 对于高性能、多线程、时间敏感的嵌入式系统,收集详细的日志数据并使用工具进行分析和可视化是一种更好的方法。

因为在大多数情况下资源是有限的,所以开发人员经常不得不做出权衡。 一方面,他们需要收集尽可能多的数据来确定故障的根本原因-无论是软件还是硬件、故障时硬件组件的状态以及系统处理的硬件和软件事件的准确计时。 另一方面,可用于日志的空间是有限的,每次写入日志都会影响整体性能。

解决方案是在设备上本地缓冲日志数据,然后将其发送到远程系统进行详细分析。

这种方法适用于嵌入式软件的开发。 然而,已部署系统的诊断需要更复杂的技术。

许多嵌入式系统离线工作,无法方便地访问内部日志。 开发人员需要仔细设计和实现其他诊断和报告方式。 如果系统没有显示器,则通常使用 LED 指示灯或嘟嘟声来编码各种错误情况。 它们足以提供有关故障类别的信息,但在大多数情况下无法提供必要的详细信息来确定根本原因。

嵌入式设备具有用于测试硬件组件的专用诊断模式。 通电后,几乎任何设备或设备都会执行开机自检(POST),这会对硬件运行快速测试。 这些测试应该是快速的,并不涵盖所有测试场景。 这就是为什么许多设备都有隐藏的服务模式,开发者或现场工程师可以激活这些模式来执行更彻底的测试。

简略的 / 概括的 / 简易判罪的 / 简易的

在本章中,我们讨论了嵌入式软件的高级概述,以及它的不同之处,并了解了为什么以及如何在这一领域有效地使用 C++。