【Embedded Development】嵌入式面试问题汇总(仅供参考)

前言

持续更新中

面试顺序:做笔试、开始面试、自我介绍、笔试题问答、技术栈问答、简历项目问答、咨询HR、反问HR

一、C/C++基础

1.1 自增自减的两种情况

取决于对于变量其值使用时的时机,比如:如果值(地址)要提前使用访问,然后再加1为下个地址。

1.2 strlen与sizeof的区别

strlen:C 语言库函数 ,专门计算字符串「实际有效长度」(仅统计 '\0' 之前的字符个数,不含 '\0');

sizeof:C 语言内置运算符 ,计算「数据 / 数据类型占用的内存总字节数」(包含 '\0' 或类型的固定内存大小),二者的本质、计算逻辑、适用场景完全不同。

注:sizeof(指针) 的结果只由操作系统的位数(寻址空间)决定,和指针指向的内容(字符串、数组、结构体等)、指向内容的长度完全无关:32 位系统(x86):sizeof(任意指针) = 4 字节(32 位地址总线,可寻址 2³²=4GB 内存,4 字节刚好存储一个内存地址);64 位系统(x86_64):sizeof(任意指针) = 8 字节(64 位地址总线,8 字节存储内存地址)。


1.3 结构体大小计算方法

(1)明确两个关键概念:

自对齐值: 单个成员的对齐基准,默认等于该成员的类型大小(如char=1、int=4、double=8);若手动指定对齐值(如#pragma pack(n)),则取「成员类型大小」和n的较小值。
**最大对齐值:**结构体所有成员「自身对齐值」的最大值,是结构体总大小的对齐基准。

(2)通用规则:

首成员规则: 结构体第一个成员的偏移量(相对于结构体起始地址)为 0x00(就是0)。
成员偏移规则: 每个成员的偏移量(必须以0x00为起始,如果前面有其他变量,必须0x00加上前面的变量所占大小)必须是其「自身对齐值」的整数倍,不足则填充空白字节(Padding)。
总大小规则: 结构体总大小必须是「最大对齐值」的整数倍,不足则在末尾填充字节。

嵌套结构体规则:嵌套结构体的偏移量是其自身「最大对齐值」的整数倍;结构体整体的最大对齐值需包含嵌套结构体的最大对齐值。
**数组规则:**数组按「单个元素的类型」计算对齐值(如int arr[3]等价于 3 个int,对齐值仍为 4)。

(3)计算步骤(通用流程)

确定每个成员的「自身对齐值」;

按声明顺序计算每个成员的偏移量(不足则填充);

计算所有成员的总占用字节(最后一个成员的偏移 + 成员大小);

确保总大小是「最大对齐值」的整数倍(不足则末尾填充)。

cpp 复制代码
struct A {
    char a;   // 类型大小1,自身对齐值1,偏移0,占用1字节
    int b;    // 类型大小4,自身对齐值4;偏移需为4的倍数 → 补3字节(偏移0+1+3=4),占用4字节(4~7)
    double c; // 类型大小8,自身对齐值8;偏移8(4+4=8,刚好8的倍数),占用8字节(8~15)
};


成员总占用:1(a)+3(填充)+4(b)+8(c)= 16 字节;
最大对齐值:max (1,4,8)=8;
总大小:16(是 8 的整数倍)→ 最终大小 16。

cpp 复制代码
// 复用示例1的struct A(大小16,最大对齐值8)
struct C {
    short s;  // 类型大小2,自身对齐值2,偏移0,占用2字节
    struct A a; // 嵌套结构体的最大对齐值8 → 偏移需为8的倍数,补6字节(偏移0+2+6=8),占用16字节(8~23)
    char ch;  // 自身对齐值1,偏移24,占用1字节(24~24)
};


成员总占用:2+6+16+1=25 字节;
最大对齐值:max (2,8,1)=8;
总大小:25 不是 8 的倍数,补 7 字节 → 最终大小 32。
cpp 复制代码
手动指定对齐值(#pragma pack (n))
通过#pragma pack(n)可强制指定最大对齐值为n(n 通常为 1/2/4/8/16):
c
运行
#pragma pack(2) // 强制最大对齐值为2
struct D {
    char a;   // 自身对齐值min(1,2)=1,偏移0,占用1字节
    int b;    // 自身对齐值min(4,2)=2,偏移2(补1字节),占用4字节(2~5)
    double c; // 自身对齐值min(8,2)=2,偏移6,占用8字节(6~13)
};
#pragma pack() // 恢复默认对齐


成员总占用:1+1+4+8=14 字节;
最大对齐值:2;
总大小:14 是 2 的整数倍 → 最终大小 14。

1.4 队列和栈的区别

1.4.1 队列(Queue)

先进先出(FIFO, First In First Out)

仅在栈顶(单一端点)进行所有操作

空:队首指针 = 队尾指针;满:(队尾指针 + 1)% 数组长度 = 队首指针(循环队列)

实现方式:可以数组,可以链表

1.4.2 栈(Stack)

后进先出(LIFO, Last In First Out)

队尾入队队首出队(两个端点)操作

空:栈顶指针 =-1;满:栈顶指针 = 数组长度 - 1

实现方式:可以数组,可以链表,但在 FreeRTOS 中,队列(核心通信组件)任务栈(任务运行依赖的栈) 均基于连续内存(数组) 实现;而 FreeRTOS 内核的链表模块 (用于管理任务 / 定时器等节点)则基于双向循环链表 实现(并非栈 / 队列的实现,而是内核管理工具)。


1.5 冒泡排序与二分查找

1.5.1 冒泡排序

冒泡排序是一种简单的交换排序算法,核心思想是通过相邻元素的两两比较与交换,将最大(或最小)的元素逐步 "冒泡" 到数组的末尾,重复此过程直到整个数组有序。

1. 核心原理

遍历数组,依次比较相邻的两个元素,若顺序错误(如升序要求下前大后小)则交换;

每一轮遍历后,当前未排序部分的最大元素会被 "推" 到末尾,因此下一轮遍历的范围可缩小一位;

若某一轮遍历中没有发生任何交换,说明数组已完全有序,可提前终止(优化点)。

特性 说明
排序稳定性 稳定(相等元素的相对位置不变)
时间复杂度 最坏 / 平均 O (n²),最好 O (n)(已排序)
空间复杂度 O (1)(原地排序,无需额外空间)
适用场景 小规模数据、对稳定性有要求的简单场景

1.5.2 二分查找

二分查找是一种高效的查找算法 ,仅适用于有序的数组(升序 / 降序),核心思想是通过不断缩小查找范围(每次排除一半元素),快速定位目标值。

1. 核心原理
  • 定义查找区间的左边界 left 和右边界 right(初始为数组首尾);
  • 计算区间中间位置 mid = (left + right) // 2(避免溢出可写 left + (right - left) // 2);
  • 比较中间元素 arr[mid] 与目标值 target
    • arr[mid] == target:找到目标,返回 mid
    • arr[mid] > target:目标在左半区间,更新右边界 right = mid - 1
    • arr[mid] < target:目标在右半区间,更新左边界 left = mid + 1
  • 重复上述步骤,直到 left > right(说明目标不存在)。
2. 特点
特性 说明
适用场景 有序数组、静态数据(无需频繁增删)
时间复杂度 O (logn)(效率远高于顺序查找 O (n))
空间复杂度 迭代版 O (1),递归版 O (logn)(栈空间)
局限性 仅支持随机访问(数组),不支持链表

1.6 快速排序如何用在链表上

1.6.1 链表快速排序的核心思路

快速排序的核心是分治思想 :选基准值 → 分区(将元素分为小于 / 等于 / 大于基准的三部分)→ 递归处理子区间。链表与数组的核心差异是无法随机访问,因此需要调整:

  • 基准选择:优先选链表尾节点(或首节点),避免额外遍历找随机节点;
  • 分区操作:通过遍历链表、调整指针,将节点划分为三个子链表(小于 / 等于 / 大于基准);
  • 递归与拼接:递归排序 "小于" 和 "大于" 的子链表,再拼接三部分得到有序链表。

1.6.2 关键步骤拆解

1. 链表节点定义

以 Python 为例,定义单向链表节点:

python 复制代码
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
2. 分区函数(核心)

遍历原链表,将节点分配到 small(小于基准)、equal(等于基准)、large(大于基准)三个子链表,返回这三个链表的头和尾(方便后续拼接)。

3. 递归排序
  • 终止条件:链表为空或只有一个节点(无需排序);
  • 递归处理:对smalllarge子链表递归排序;
  • 拼接结果:small → equal → large
python 复制代码
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next


def quickSortLinkedList(head: ListNode) -> ListNode:
    # 终止条件:空链表或单个节点
    if not head or not head.next:
        return head

    # 步骤1:选择尾节点作为基准(避免随机访问,简化实现)
    pivot = get_tail(head)
    pivot_val = pivot.val

    # 步骤2:分区:small(<基准)、equal(=基准)、large(>基准)
    small_head = small_tail = ListNode(0)  # 哨兵节点,简化头节点处理
    equal_head = equal_tail = ListNode(0)
    large_head = large_tail = ListNode(0)

    curr = head
    while curr:
        if curr.val < pivot_val:
            small_tail.next = curr
            small_tail = small_tail.next
        elif curr.val == pivot_val:
            equal_tail.next = curr
            equal_tail = equal_tail.next
        else:
            large_tail.next = curr
            large_tail = large_tail.next
        # 断链:避免原链表的环,或子链表包含多余节点
        curr = curr.next

    # 收尾:将三个子链表的尾节点next置空
    small_tail.next = None
    equal_tail.next = None
    large_tail.next = None

    # 步骤3:递归排序small和large子链表
    sorted_small = quickSortLinkedList(small_head.next)  # 跳过哨兵节点
    sorted_large = quickSortLinkedList(large_head.next)

    # 步骤4:拼接结果:sorted_small → equal → sorted_large
    return concat(sorted_small, equal_head.next, sorted_large)


# 辅助函数:获取链表尾节点
def get_tail(head: ListNode) -> ListNode:
    curr = head
    while curr.next:
        curr = curr.next
    return curr


# 辅助函数:拼接三个链表(处理空链表情况)
def concat(small: ListNode, equal: ListNode, large: ListNode) -> ListNode:
    # 拼接small和equal
    dummy = ListNode(0)
    curr = dummy
    if small:
        curr.next = small
        curr = get_tail(curr)
    curr.next = equal
    if equal:
        curr = get_tail(curr)
    # 拼接large
    curr.next = large
    return dummy.next


# 测试代码:打印链表
def print_linked_list(head: ListNode):
    curr = head
    res = []
    while curr:
        res.append(str(curr.val))
        curr = curr.next
    print(" → ".join(res))


# 测试用例
if __name__ == "__main__":
    # 构建链表:4 → 2 → 5 → 1 → 3
    head = ListNode(4)
    head.next = ListNode(2)
    head.next.next = ListNode(5)
    head.next.next.next = ListNode(1)
    head.next.next.next.next = ListNode(3)

    print("原链表:")
    print_linked_list(head)

    sorted_head = quickSortLinkedList(head)
    print("排序后:")
    print_linked_list(sorted_head)

1.6.3 对比数组快速排序

特性 数组快速排序 链表快速排序
随机访问 支持(O (1)) 不支持(O (n))
元素交换 需临时空间 仅调整指针(无空间)
空间复杂度 O (log n)(递归)+ 临时空间 O (log n)(仅递归)
基准选择 灵活(首 / 尾 / 随机) 优先尾节点(无需遍历)

1.7 函数指针与指针函数,指针数组与数组指针的区别

(1)函数指针 vs 指针函数(核心:看本质是指针还是函数)

类型 本质 语法示例 核心特点
函数指针 指向函数的指针 int (*fp)(int) = &add; 指针变量,存函数入口地址
指针函数 返回指针的函数 int* func(int a); 函数,返回值为指针类型

(2)指针数组 vs 数组指针(核心:看本质是数组还是指针)

类型 本质 语法示例 核心特点
指针数组 数组(元素是指针) int* arr[3]; 3 个 int 指针的数组,arr [0] 是指针
数组指针 指针(指向数组) int (*p)[3] = &arr; 指向 "3 个 int 的数组" 的指针

1.8 static与普通变量的区别

维度 static 变量 普通变量(auto)
作用域 局部 static:当前函数;全局 static:当前文件 局部:当前函数;全局:整个程序
生命周期 程序全程(静态存储期) 局部:函数调用期;全局:程序全程
存储位置 数据段(静态区) 局部:栈;全局:数据段
初始化 仅初始化一次 局部:每次调用初始化;全局:启动时
多文件可见性 全局 static:其他文件不可见 全局:可通过 extern 访问

1.9 指针常量与常量指针的区别

口诀:const 靠近谁,谁就不可变;

  • 常量指针(指向常量的指针):const int* p / int const* p→ 指向的内容不可改(*p=10报错),指针本身可改(p=&b合法);
  • 指针常量(指针本身是常量):int* const p→ 指针本身不可改(p=&b报错),指向的内容可改(*p=10合法)。

1.10 extern关键字

核心作用:声明外部变量 / 函数,实现跨文件访问(仅声明,不分配内存);

  • 用法 1:跨文件访问全局变量文件 A:int g_val = 10;(定义),文件 B:extern int g_val;(声明,可访问);
  • 用法 2:跨文件访问函数(函数默认 extern,可省略);
  • 注意:局部变量不能用 extern,仅全局变量 / 函数可用;extern 不能初始化(如extern int g_val=10是定义,非法)。

1.11 static与const存放在内存的哪个区域

内存分区:栈、堆、数据段(静态区:.data/.bss)、代码段(常量区:.rodata);

  • static 变量:无论局部 / 全局,均存数据段
    • 初始化 static → .data 区;未初始化 static → .bss 区(启动时置 0);
  • const 变量:
    • 全局 const → 代码段(.rodata,只读);
    • 局部 const → 通常在栈(编译器优化可能入常量区);
    • static const → 仍在代码段,仅作用域限制在当前文件。

1.12 volatile关键字

volatile是 C/C++ 中核心的类型修饰符,核心作用是禁止编译器对变量做 "不必要的优化" ,强制程序每次访问该变量时都直接从内存(而非 CPU 寄存器) 读取 / 写入 ------ 这一特性在嵌入式开发(如 STM32、Linux 驱动)中至关重要,因为嵌入式场景中变量常被硬件、中断、多线程修改,编译器无法感知这类 "外部修改"。

需要的应用场景 原理说明 典型示例(STM32/Linux)
1. 硬件寄存器访问 硬件寄存器(如 GPIO/USART/ADC 寄存器)的值由硬件实时修改,编译器无法感知 volatile uint32_t *GPIOA_IDR = (volatile uint32_t*)0x40010800;(STM32 GPIO 输入寄存器)
2. 中断与主函数共享变量 中断服务函数(ISR)修改的变量,主函数读取时需避免编译器优化 主函数中volatile uint8_t flag = 0;,中断中flag = 1;,主函数循环检测flag
3. 多线程 / 多任务共享变量 RTOS(如 FreeRTOS)中,不同任务 / 线程共享的变量,需避免缓存优化 volatile int cnt = 0;,任务 1 修改cnt,任务 2 读取cnt
4. 延时函数(空循环) 编译器会优化掉 "无操作的空循环",加volatile强制执行循环 for(volatile int i=0; i<10000; i++);(STM32 简单延时,避免被优化为空)
5. 内存映射的外设地址 Linux 驱动中,外设地址映射到内存后,需volatile保证读写实时性 volatile unsigned int *uart_base = ioremap(0x12345678, 0x100);

注:编译器优化等级(-O0/-O1/-O2/-O3)越高,对变量的缓存优化越激进:

-O0(无优化):即使不加volatile,编译器也会每次读内存(调试模式可用,量产不推荐);

-O1/-O2(量产常用):必须为外部修改的变量加volatile,否则必出逻辑错误;

-O3(极致优化):甚至会优化掉 "看似无意义的 volatile 变量赋值",需结合代码逻辑调整。


1.13


二、嵌入式基础知识

2.1 4种输入4种输出

2.1.1 输入

模拟输入

上拉输入

下拉输入

浮空输入

2.1.2 输出

推挽输出

推挽复用输出

开漏输出

开漏复用输出


2.2 IIC的读操作

2.2.1 当前地址读:

起始+设备号7位+读1位+从机ACK1位+从机数据8位+主机NACK1位+结束

2.2.2指定地址读:

起始+设备号7位+写1位+从机ACK1位+主机写寄存器地址8位+从机ACK1位+起始+设备号7位+读1位+从机ACK1位+从机返回数据8位+主机NACK1位+结束

【Embedded Development】【IIC】IIC通信协议的时序学习过程记录-CSDN博客


2.3 为什么IIC要设置为开漏

主要为了符合IIC协议,规定IIC不能输出高电平,而IIC总线默认总是高电平由电路控制,信号控制就是开漏驱动出低电平拉低。

I²C(IIC)总线的 SDA(数据)和 SCL(时钟)引脚必须配置为开漏输出(Open Drain),核心原因是适配 I²C 总线的 "多主设备、线与逻辑、电平兼容" 特性,具体如下:

核心原因 详细说明
实现 "线与" 逻辑 开漏输出的引脚只能主动拉低电平,无法主动拉高,电平拉高依赖外部上拉电阻。当多个主设备同时驱动总线时,只要有一个设备拉低引脚,总线就为低;所有设备释放(输出高阻)时,总线被上拉电阻拉为高 ------ 这是 I²C 多主设备避免总线冲突的核心逻辑(若用推挽输出,多个设备同时输出高低电平会导致短路)。
兼容不同电平标准 上拉电阻可灵活匹配电压(如 3.3V/5V),实现不同电压域设备(如 3.3V MCU 和 5V 传感器)的通信,推挽输出无法做到电平灵活适配。
维持总线空闲态 I²C 总线空闲时要求 SDA/SCL 为高电平,开漏 + 上拉电阻可稳定维持高电平;若无开漏,引脚高阻态会导致电平浮动,总线状态不可控。
支持双向通信 开漏输出的引脚可在 "输出(拉低)" 和 "输入(高阻,检测总线电平)" 之间切换,满足 SDA/SCL 既作为输出(主设备发数据)又作为输入(从设备 / 其他主设备发数据)的双向通信需求。

补充:开漏输出需搭配上拉电阻(典型值 1.8kΩ~10kΩ),电阻值需平衡:阻值太小功耗大、总线上升沿太快易振铃;阻值太大上升沿太慢,限制 I²C 通信速率。


2.4 如何管理100个软件定时器

管理 100 个软件定时器的核心是基于 "定时基准 + 有序链表 / 时间轮" 设计,避免逐一遍历所有定时器导致的性能损耗,以下是工业级实现方案:

2.4.1 核心设计思路
  • 定时基准:用硬件定时器(如 SysTick、TIMx)产生固定周期中断(如 1ms),作为所有软件定时器的时间基准;
  • 定时器结构体:每个定时器包含「定时时长、剩余时长、状态(使能 / 暂停 / 单次 / 周期)、回调函数、优先级」等字段;
  • 高效管理容器:优先选择「按超时时间排序的单向链表」或「时间轮算法」(100 个定时器推荐链表,简单且高效)。
2.4.2 具体实现步骤
(1)定时器结构体定义
复制代码
typedef struct SoftTimer {
    uint32_t total_ticks;   // 总定时时长(ms)
    uint32_t remain_ticks;  // 剩余时长(ms)
    uint8_t state;          // 0:未使能,1:运行中,2:暂停
    uint8_t mode;           // 0:单次触发,1:周期触发
    void (*callback)(void); // 定时到期回调函数
    struct SoftTimer *next; // 链表节点指针
} SoftTimer_t;
(2)链表管理(按剩余时长升序)
  • 初始化:创建空链表,100 个定时器初始化为未使能状态;
  • 添加定时器:按remain_ticks插入链表(超时时间近的在前),避免每次遍历全部 100 个;
  • 定时中断处理(核心):
    1. 仅遍历链表头部节点,将其remain_ticks--
    2. remain_ticks == 0,触发回调函数;
    3. 若为周期模式,重置remain_ticks = total_ticks,并将节点重新插入链表;若为单次模式,标记为未使能并从链表移除;
    4. 仅当头部节点处理完成后,才处理下一个节点(100 个定时器仅需遍历 "即将超时的节点",而非全部)。
(3)批量管理优化
  • 提供统一接口:timer_create()(创建)、timer_start()(启动)、timer_stop()(暂停)、timer_delete()(删除);
  • 优先级处理:回调函数中避免耗时操作(如阻塞),若需高优先级处理,可通过消息队列触发任务;
  • 防重入:中断中仅标记 "定时器到期",回调函数在主循环执行(避免中断嵌套)。
3. 进阶方案:时间轮算法

若定时器数量进一步增加(如 > 500),可采用时间轮:将时间划分为 "轮盘槽位"(如 0~99 槽,对应 0~99ms),每个槽位挂载定时器;定时中断每 1ms 移动 "指针",仅处理当前槽位的定时器,遍历开销固定(与定时器数量无关)。


2.5 MODBUS的格式

MODBUS 是工业串行通信协议,分RTU 模式(主流)ASCII 模式,核心帧结构如下:

2.5.1 MODBUS-RTU(远程终端单元)

工业现场最常用,二进制传输,效率高,帧边界靠 "总线空闲时间" 区分(≥3.5 个字符时间),帧结构:

字段 字节数 说明
起始位 - 总线空闲≥3.5 字符时间(无实际字节,用于帧同步)
从机地址 1 0~247(0 为广播地址,1~247 为从机地址)
功能码 1 指示操作类型(如 03H 读保持寄存器、05H 写单线圈)
数据域 N 操作参数(如寄存器地址、寄存器数量、写入数据,长度随功能码变化)
CRC 校验 2 循环冗余校验(低字节在前,高字节在后,覆盖地址、功能码、数据域)
结束位 - 总线空闲≥3.5 字符时间(标记帧结束)

示例(读从机 1 的 0001~0002 寄存器):01 03 00 01 00 02 C4 0B

  • 01:从机地址;03:读保持寄存器;00 01:起始寄存器地址;00 02:读取数量;C4 0B:CRC 校验。
2.5.2 MODBUS-ASCII

文本格式传输,可读性强但效率低,帧结构:

字段 字符数 说明
起始符 1 冒号 ":"(标记帧开始)
从机地址 2 十六进制 ASCII(如 01→"01")
功能码 2 十六进制 ASCII(如 03→"03")
数据域 2N 十六进制 ASCII(每个字节转 2 个字符)
LRC 校验 2 纵向冗余校验(覆盖地址、功能码、数据域)
结束符 2 回车 + 换行(CR+LF,标记帧结束)

2.6 MODBUS中的遥控帧用在什么场景,功能码是多少

MODBUS "遥控帧" 本质是对离散输出线圈(Coil)的强制写操作,用于远程控制工业现场的开关量设备,核心功能码和场景如下:

2.6.1 核心功能码

功能码 名称 用途 遥控帧特性
05H 强制单线圈 控制单个开关量输出 最常用的遥控帧,单点操作
0FH 强制多线圈 批量控制多个开关量输出 多点批量遥控

2.6.2 应用场景

工业设备启停:遥控电机、泵体、风机的启停(线圈对应继电器吸合 / 断开);

阀门控制:远程控制电磁阀、气动阀的开关(如水管 / 气管阀门);

指示灯 / 报警器:遥控现场指示灯亮灭、声光报警器启停;

紧急操作:远程急停、复位等安全指令(优先级高,需搭配硬件冗余);

设备模式切换:遥控设备在 "自动 / 手动" 模式间切换(单个线圈对应模式)。

示例(05H 遥控帧):从机地址 1,强制 0001 线圈为 ON:01 05 00 01 FF 00 8C 3A

01:从机地址;05:强制单线圈;00 01:线圈地址;FF 00:置 ON(00 00 为置 OFF);8C 3A:CRC 校验。


2.7 在没有中断的情况下,该如何实现MODBUS-RTU的收发

无中断时需通过轮询(查询)方式 实现,核心是利用 "字符时间计算 + 总线空闲检测" 识别帧边界,缺点是 CPU 占用率高,适合对实时性要求低的场景,具体步骤:

2.7.1 前期准备
  • 配置 UART:波特率(如 9600)、8 位数据位、1 位停止位、无校验(8N1),禁用 UART 接收 / 发送中断;
  • 计算字符时间:每字节传输时间 = 10 / 波特率(含起始位 + 8 数据位 + 1 停止位),如 9600 波特率下字符时间≈1.04ms,3.5 字符时间≈3.64ms(帧边界判定阈值)。
2. 接收实现(核心:帧边界检测)
复制代码
uint8_t rcv_buf[32];  // 接收缓冲区
uint8_t rcv_len = 0;  // 接收长度
uint32_t last_rcv_tick = 0; // 最后一次接收字节的时间戳

void modbus_rtu_rcv(void) {
    // 1. 轮询UART接收寄存器,检测是否有字节可读
    if (UART_FLAG_RXNE) {
        rcv_buf[rcv_len++] = UART_DR; // 读取字节到缓冲区
        last_rcv_tick = get_sys_tick(); // 记录当前时间(ms)
        if (rcv_len >= 32) rcv_len = 0; // 防止缓冲区溢出
    }

    // 2. 检测总线空闲(超过3.5字符时间无字节接收),判定帧结束
    if (rcv_len > 0 && (get_sys_tick() - last_rcv_tick) > 3.64) {
        // 3. CRC校验(验证帧完整性)
        if (crc_check(rcv_buf, rcv_len)) {
            modbus_parse(rcv_buf, rcv_len); // 解析帧并处理
        }
        // 4. 重置接收状态
        rcv_len = 0;
        last_rcv_tick = 0;
    }
}
3. 发送实现
复制代码
void modbus_rtu_send(uint8_t *data, uint8_t len) {
    // 1. 等待总线空闲(避免发送冲突)
    while ((get_sys_tick() - last_rcv_tick) < 3.64);

    // 2. 轮询发送每一字节
    for (uint8_t i = 0; i < len; i++) {
        while (!UART_FLAG_TXE); // 等待发送寄存器为空
        UART_DR = data[i];      // 发送字节
    }
    // 3. 发送完成后,等待总线空闲
    last_rcv_tick = get_sys_tick();
}

2.8 解释一下优先级翻转

优先级翻转是实时操作系统(RTOS)中高优先级任务被低优先级任务阻塞,导致中等优先级任务优先执行的现象,是破坏 RTOS 实时性的典型问题,具体如下:

2.8.1 核心场景(三步触发)

  1. 低优先级任务(L)持有高优先级任务(H)所需的互斥锁(如串口、外设);
  2. 高优先级任务(H)就绪,抢占 CPU 但因等待互斥锁进入阻塞态;
  3. 中等优先级任务(M)就绪,抢占低优先级任务(L)的 CPU,导致高优先级任务(H)被 "卡" 在 M 之后执行 ------ 优先级发生 "翻转"。

2.8.2 危害

高优先级任务本应优先执行(如紧急中断处理、安全指令),但因优先级翻转导致响应超时,违反 RTOS "实时性" 核心诉求。

2.8.3 解决方法

  • 优先级继承(FreeRTOS 互斥锁默认实现):低优先级任务(L)持有互斥锁时,临时继承高优先级任务(H)的优先级,避免被中等优先级任务(M)抢占;释放锁后恢复原优先级;
  • 优先级天花板:给互斥锁设置 "天花板优先级"(所有使用该锁的任务中最高优先级),任务获取锁时直接提升到天花板优先级,释放后恢复。

2.8.4 比喻

没有加优先级继承:

家庭主妇妈妈给二儿子洗澡(低级任务),占用着厕所,而大儿子实在想上厕所(高级任务),可是厕所门锁了,同时来了一个电话需要接(中级任务),则抢占了了给二儿子洗澡(低级任务)的时间,大儿子还在门外等着妈妈打完电话(中级任务)和给二儿子洗澡(低级任务)的事件做完,大儿子才能上厕所(高级任务)。

注:但是(高级任务)不能等太久,否则会出问题

加优先级继承:

家庭主妇妈妈给二儿子洗澡(低级任务),占用着厕所,而大儿子实在想上厕所(高级任务),此时,妈妈给二儿子洗澡加快速度(低级任务暂时继承为高级任务的优先等级),即使,来了一个电话需要接(中级任务)也不接,给二儿子洗完就马上开门(释放互斥锁,同时,临时的高级任务变回原来的低级任务),大儿子开关门去上厕所,然后妈妈去打电话(中级任务)。


2.9 解释一下什么是死锁

死锁是多个任务 / 进程互相持有对方所需的资源,且均不释放已持有的资源,导致所有任务永久阻塞的现象,是多任务系统的致命问题,具体如下:

2.9.1 死锁的四大必要条件(缺一不可)

说明
互斥 资源同一时间只能被一个任务占用(如串口、互斥锁)
请求与保持 任务持有已获取的资源,同时请求新的资源
不可剥夺 资源只能由持有任务主动释放,其他任务无法强制剥夺
循环等待 任务 A 等待任务 B 的资源,任务 B 等待任务 C 的资源,...,任务 N 等待任务 A 的资源

2.9.2 典型场景

  • 任务 A 持有资源 1,请求资源 2;
  • 任务 B 持有资源 2,请求资源 1;
  • 两者均不释放已持有的资源,互相等待,永久阻塞。

2.9.3 解决 / 避免方法

  • 破坏 "循环等待":给资源编号,任务按编号升序申请资源(如先申请资源 1,再申请资源 2);
  • 破坏 "请求与保持":任务一次性申请所有所需资源,若申请失败则释放已持有的资源;
  • 超时释放:任务申请资源时设置超时,超时后放弃申请并释放已持有的资源;
  • 资源剥夺:内核强制剥夺低优先级任务的资源(仅适合非关键资源)。

2.10 六层板的层叠结构、ADC的输入阻抗匹配等硬件相关问题

2.10.1 六层板的层叠结构

六层板的层叠设计核心原则:电源 / 地平面紧邻、信号层与地 / 电源层相邻、模拟 / 数字层隔离、控制阻抗,工业级主流层叠方案如下(优先推荐):

层序 类型 作用
Top 信号层 走低速数字信号、接口信号(如 UART、IIC),禁止走高速 / 模拟信号
L2 地平面(GND) 作为 Top 层信号的参考平面,减少 EMI(电磁干扰),提供回流路径
L3 信号层 走高速信号(如 SPI、以太网)、核心控制信号(MCU 核心)
L4 电源平面(Power) 给核心器件(MCU、ADC/DAC)供电,紧邻地平面(L2/L5)减少电源噪声
L5 信号层 走模拟信号(如 ADC 输入、运放输出),远离数字信号层
Bottom 信号层 走电源接口、外设接口信号,可铺地增强屏蔽

其他可选方案(按需求调整):

  • Top→Power→GND→Signal→GND→Bottom(双地平面,适合强干扰场景);
  • Top→Signal→GND→Power→GND→Bottom(模拟信号走内层,屏蔽更好)。

设计要点

  • 模拟地 / 数字地单点连接,避免地环路干扰;
  • 高速信号(如 > 100MHz)走内层,与地平面紧邻,控制阻抗(如 50Ω/90Ω);
  • 电源平面分割(如 3.3V/5V / 模拟电源),分割处做隔离。

2.10.2 ADC 的输入阻抗匹配

ADC 输入阻抗匹配的核心是保证采样电容快速充电、减少信号反射 / 干扰,提升采样精度,具体要求和实现如下:

1. ADC 输入特性

ADC 内部包含 "采样保持电容(Cs)",采样瞬间需要从前端电路吸取大电流给 Cs 充电;若前端阻抗过高,Cs 充电不充分,采样值会失真。

2. 阻抗匹配核心要求
  • 驱动阻抗匹配:前端信号源 / 驱动电路的输出阻抗 ≤ ADC 输入阻抗的 1/10(ADC 输入阻抗通常为 MΩ 级,因此前端输出阻抗需≤100kΩ);
  • 采样时间匹配:前端电路给 Cs 充电的时间 ≤ ADC 的采样时间(如 12 位 ADC 采样时间通常为 1μs,需保证 Cs 在 1μs 内充电至 99% 以上);
  • 高频阻抗匹配:若信号含高频分量(>100kHz),需串联匹配电阻(如 50Ω)减少信号反射。
3. 实现方法
  • 低阻抗驱动:若信号源阻抗高(如传感器输出阻抗 1MΩ),加运放缓冲器(如 OPA2340)做阻抗变换(运放输出阻抗 < 100Ω);
  • RC 滤波匹配:ADC 输入端加 RC 低通滤波器(如 1kΩ 电阻 + 100nF 电容),电阻值需匹配前端输出阻抗,滤除高频噪声的同时保证充电速度;
  • 布线优化:模拟信号走线短、粗、直,远离数字信号,底层铺模拟地,减少干扰导致的阻抗变化;
  • 避免负载过重:ADC 输入引脚不挂载过多器件(如多个电阻分压),防止输入阻抗降低。

示例:12 位 ADC(输入阻抗 10MΩ,采样时间 1μs),前端传感器输出阻抗 10kΩ→加 1kΩ 串联电阻 + 100nF 电容,满足 "驱动阻抗≤1MΩ(10MΩ/10)",且 RC 充电时间(τ=1k×100n=100μs,实际采样时间内可充至 99% 以上)。


2.11 STM32哪种IO工作模式功耗最低

STM32 IO 口功耗最低的工作模式是 模拟输入模式(Analog Input) ;若 IO 口需保持数字特性,下拉 / 上拉输入模式 次之,而输出模式、复用功能模式功耗显著更高。


2.12 STM32有哪些低功耗模式

模式名称 内核状态 时钟状态 存储 / 寄存器保留 典型功耗(3.3V@25℃) 主要唤醒源 适用场景
睡眠模式(Sleep) 内核停止,外设运行 系统时钟(SYSCLK)仍运行,仅内核暂停 全部保留(SRAM / 寄存器) ~1-5mA(F4 系列) 中断(任意外设 / EXTI)、SysTick 中断 短延时等待(如传感器采样间隙)
停止模式(Stop) 内核 + 系统时钟停止 HSI/HSE/PLL 关闭,低速时钟(LSI/LSE)可选运行 SRAM / 寄存器保留 ~10-50μA(F4)/ ~1μA(L4 低功耗系列) EXTI 中断、RTC 闹钟、DMA、I2C/SPI 唤醒(部分系列) 中等时长休眠(如 1 分钟采集一次数据)
待机模式(Standby) 内核 + 电源域关闭 仅备份域时钟(LSE/LSI)可选运行 仅备份域(RTC / 备份寄存器)保留,SRAM / 寄存器丢失 ~0.1-1μA(F4)/ ~0.01μA(L4) WKUP 引脚、RTC 闹钟、NRST 复位、入侵检测 长时间休眠(如小时级唤醒)
关机模式(Shutdown) 所有电源域关闭 仅备份域(可选)运行 仅备份域保留 ~0.001μA(L4/H7) WKUP 引脚、RTC 闹钟、NRST 复位 极致低功耗(如天级唤醒)

STM32 低功耗模式功耗排序(从高到低):睡眠模式(Sleep);停止模式(Stop);待机模式(Standby); 关机模式(Shutdown)


2.13 信号量与互斥量的区别

维度 互斥量(Mutex) 信号量(Semaphore)
核心用途 临界区互斥(独占资源) 同步 / 有限资源访问(计数许可)
所有权 加锁线程必须解锁(有所有权) 无所有权(任意线程可解锁)
取值 二值(0/1) 计数型(≥0)
优先级反转 可通过优先级继承避免 无法避免
死锁风险 高(忘记解锁) 计数型低,二值型类似互斥

总结:互斥是 "独占",信号量是 "计数许可";互斥量解决 "谁先谁后",信号量解决 "同步 / 资源数"。

记住:互斥量(Mutex):核心解决公共资源的独占访问(临界区问题),保证同一时间只有一个任务访问资源;可通过优先级继承规避 "优先级反转" 问题。
信号量(Semaphore):核心解决任务同步(二值信号量) + 有限资源的计数访问(计数型信号量)(比如最多 3 个任务同时访问某资源,或计数已完成的任务数)。


2.14 流媒体的使用场景

核心是 "边传边播",典型场景:

  • 娱乐:直播、短视频、在线视频 / 音乐、游戏直播;
  • 安防:摄像头实时监控、NVR 回放;
  • 工业 / 物联网:流水线视频监控、无人机图传、设备远程运维视频流;
  • 教育医疗:在线直播课、远程手术直播、医疗影像实时传输;
  • 通信:视频通话、会议直播;
  • 车载:行车记录仪回传、车载大屏在线视频。

2.15 MQTT的主题订阅与发布机制

MQTT 是发布 / 订阅(Pub/Sub)模型,核心角色:发布者、订阅者、Broker(代理);

  • 发布:发布者向 Broker 指定 "主题"(层级化,/ 分隔)发消息,无需知道订阅者;
  • 订阅:订阅者向 Broker 订阅主题(支持通配符:+ 单层、# 多层),Broker 记录订阅关系;
  • 转发:Broker 接收消息后,向所有订阅匹配主题的订阅者转发,实现发布者与订阅者解耦。

注:必须有代理进行转发


2.16 STM32的BOOT的启动模式

2.16.1 BOOT 引脚电平与启动模式对照表

BOOT0 电平 BOOT1 电平 启动模式 启动源 核心作用与应用场景
低电平(0) 任意电平 主闪存启动(默认) 芯片内置的主 Flash 存储器 正常产品运行模式:程序固化在主 Flash 中,上电 / 复位后直接执行 Flash 中的代码(开发完成后的默认模式)
高电平(1) 低电平(0) 系统存储器启动 芯片内置的 Bootloader 区域 ISP 下载模式:通过串口(UART)、USB 等接口,向主 Flash 烧录程序(适用于程序丢失 / 首次烧录的场景)
高电平(1) 高电平(1) 内置 SRAM 启动 芯片的内置静态随机存储器 调试 / 测试模式:程序临时加载到 SRAM 中运行(无 Flash 擦写风险,适合调试大程序或临时验证代码)

2.16.2 各模式的关键说明

  1. 主闪存启动(默认模式)

    • 启动源是 STM32 的主 Flash(容量从几 KB 到几 MB 不等,因型号而异);
    • 是产品量产、正常运行的唯一模式,程序需提前通过下载工具(如 J-Link)烧录到主 Flash 中。
  2. 系统存储器启动(ISP 模式)

    • 启动源是 STM32 出厂时预烧录的内置 Bootloader(不可修改);
    • 无需外部下载器,仅需通过串口(如 USART1)连接电脑,用官方工具(如 STM32CubeProgrammer)烧录程序;
    • 适用于 "下载器损坏""首次烧录""程序崩溃后恢复" 等场景。
  3. 内置 SRAM 启动

    • 启动源是 STM32 的内置 SRAM(容量通常远小于 Flash,如 F103C8T6 的 SRAM 仅 20KB);
    • 程序不会固化(掉电丢失),仅用于调试阶段(如验证代码功能、测试大程序的部分模块);
    • 需在编译器中修改 "程序存储地址" 为 SRAM 的起始地址(如 F1 系列 SRAM 起始地址为0x20000000)。

2.16.3 注意事项

  • 部分低容量 STM32(如 STM32F030)仅保留BOOT0引脚,无BOOT1:此时BOOT0=1直接进入系统存储器启动模式
  • 上电 / 复位时,BOOT0/BOOT1的电平会被芯片采样,之后修改引脚电平不影响当前启动流程(需重新复位才会切换模式);
  • 若需切换启动模式,需在复位前将BOOT0/BOOT1拉到对应电平,复位后再恢复(如 ISP 下载完成后,需将BOOT0拉低再复位,回到主闪存模式)。

2.17 DMA的方向

传输方向 定义(源 → 目的) 核心原理 典型应用场景(STM32)
1. 外设 → 内存(Periph→Mem) 片上外设数据寄存器 → 内存(SRAM/Flash) 外设产生数据后,DMA 自动将数据从外设寄存器(如 USART_DR、ADC_DR)搬运到内存,无需 CPU 干预 - USART/UART 接收数据(串口数据自动存到内存缓冲区)- ADC 采集(采样值自动存到内存数组)- SPI/I2C 从设备接收数据
2. 内存 → 外设(Mem→Periph) 内存(SRAM/Flash)→ 片上外设数据寄存器 DMA 从内存读取数据,自动写入外设寄存器,外设再将数据发送出去 - USART/UART 发送数据(内存字符串自动通过串口发送)- SPI/I2C 主设备发送数据- DAC 输出(内存波形数据自动输出)- TIM PWM+DMA(输出固定占空比序列)
3. 内存 → 内存(Mem→Mem) 内存区域 A → 内存区域 B(SRAM/SRAM、Flash/SRAM) 纯软件触发,DMA 直接在内存间搬运数据(无需外设参与),效率远高于 CPU 循环拷贝 - 大容量数据拷贝(如 Flash 固件升级时,将接收的固件数据拷贝到指定 Flash 区域)- 内存数据格式转换(如批量字节转半字)- 缓冲区数据搬移(环形缓冲区读写)

2.18 内联函数与宏函数的区别

维度 宏函数(Macro) 内联函数(Inline Function)
实现阶段 预处理阶段(文本替换,无编译检查) 编译阶段(编译器优化,有完整编译检查)
类型安全 无类型检查,参数 / 返回值可能隐含类型错误 严格类型检查(同普通函数),参数 / 返回值类型不匹配会编译报错
参数处理 直接文本替换,易因参数副作用(如自增 / 自减)出问题 参数先求值再传递,无副作用(同普通函数)
作用域 全局生效(预处理无作用域概念),易命名冲突 遵循普通函数作用域(如局部内联函数仅在当前作用域有效)
函数特性 无函数语义(无栈帧、无返回值检查、不支持递归) 保留函数语义(支持递归、返回值检查、可调试)
编译器控制 强制替换(预处理阶段必须执行) 编译器自主决策(可忽略inline,如函数体过大时不内联)
重载 / 多态 不支持重载(宏名是文本,无法区分参数类型) 支持重载、类成员内联函数可支持多态(虚函数除外)
内存占用 替换越多,代码膨胀越严重(无编译器优化) 编译器可平衡内联与代码膨胀(如仅对高频小函数内联)
场景 推荐使用 原因
高频小函数(如数值计算、简单判断) 内联函数 类型安全、无副作用、编译器可灵活优化
需预处理阶段替换(如编译开关、常量) 宏函数 仅预处理阶段生效,支持条件编译等特性
类成员函数 内联函数 隐式 inline,结合类的封装性,类型安全
需要重载 / 多态 内联函数 宏不支持重载,内联函数兼容普通函数特性

注:内联函数就是会比宏函数直接替换的方式安全得多,会有语法检查,只不过比较适合较短/简单的函数替换来减小开销,必须看使用场景,否则就是负优化。


2.19


三、网络知识

3.1 TCP的连接与断开的过程

3.1.1 三次握手四次挥手


3.2 HTTP与MQTT的区别与使用场景

HTTP:「请求 - 响应」模式,适用于客户端主动获取数据、Web 交互、设备配置查询,开销较高,无实时性保障;

MQTT:「发布 - 订阅」模式,适用于物联网设备实时数据上报、远程控制、低带宽 / 低功耗场景,开销极低,支持 QoS 分级可靠传输。

对比维度 HTTP(1.1/2) MQTT(3.0)
通信模式 请求 - 响应(Client 发起请求,Server 被动响应) 发布 - 订阅(Broker 中转,多 Client 解耦通信)
连接方式 短连接(默认)/ 长连接(需配置 Keep-Alive) 长连接(默认,心跳包维持)
数据开销 高(头部冗余大,如 HTTP 头占数十 KB) 极低(固定头部 2 字节, payload 纯数据)
实时性 低(需客户端主动轮询,延迟取决于轮询周期) 高(消息实时推送,延迟毫秒级)
QoS(服务质量) 无原生支持(需自定义重试 / 确认) 三级 QoS:0(最多一次)、1(至少一次)、2(恰好一次)
功耗表现 高(频繁建立连接、头部传输消耗算力 / 带宽) 低(长连接减少握手开销,适合电池供电设备)
嵌入式实现难度 简单(LwIP 原生支持 HTTP 客户端 / 服务器) 中等(需集成 MQTT 客户端库,如 Paho-MQTT)
适用设备 算力 / 带宽充足的设备(如带屏终端、网关) 低算力 / 低带宽设备(如 STM32+GPRS/NB-IoT、传感器)
消息路由 点对点(Client→指定 Server) 多对多(发布者→Broker→所有订阅者)
离线消息 不支持(断开连接后无法接收消息) 支持(Broker 缓存消息,客户端重连后推送)
核心场景 设备配置查询、Web 界面交互、偶尔数据获取 传感器实时上报、设备远程控制、多设备协同

优先选 MQTT 的场景:

(1)传感器实时数据上报(如每秒 / 每分钟一次);

(2)设备远程控制(如开关、电机调速);

(3)低功耗设备(如电池供电的土壤传感器,NB-IoT/GPRS 模块);

(4)多设备协同(如智能家居、工业物联网多传感器联动)。

优先选 HTTP 的场景:

(1)设备配置查询 / 修改(如查询设备固件版本、设置 WiFi 参数);

(2)设备与 Web 服务器交互(如带 Web 界面的网关);

(3)低频数据传输(如每天一次的设备状态上报);

(4)无 MQTT Broker 部署条件(如简单单机设备)。

混合使用场景:

(1)设备上电后通过 HTTP 获取 MQTT Broker 地址、Topic 配置;

(2)实时数据通过 MQTT 上报,设备配置 / 固件更新通过 HTTP 实现。


3.3 TCP和UDP的区别

对比维度 TCP(传输控制协议) UDP(用户数据报协议)
核心定位 面向连接的可靠传输协议 无连接的轻量级传输协议
连接性 面向连接(需三次握手建立连接,四次挥手释放连接) 无连接(发送数据前无需建立连接,直接封装数据报发送)
可靠性保障 1. 确认应答(ACK);2. 超时重传;3. 序号 / 确认号(保证数据有序);4. 重传机制(ARQ);5. 校验和 仅提供校验和(可选,用于检测数据错误,无重传机制),不保证数据送达 / 有序
数据顺序 保证数据按发送顺序接收(通过序号排序、重传丢失报文) 不保证顺序(数据报独立传输,可能乱序、丢失、重复)
流量控制 有(滑动窗口机制,避免接收方缓冲区溢出) 无(发送方无节制发送,可能导致接收方拥塞丢失)
拥塞控制 有(慢启动、拥塞避免、快速重传、快速恢复,适应网络带宽变化) 无(不感知网络拥塞,持续发送数据,可能加剧网络拥堵)
头部开销 较大(固定头部 20 字节,可选选项字段,总长度可达 60 字节) 极小(固定头部 8 字节,无可选字段,开销仅为 TCP 的 1/3~1/4)
传输效率 较低(连接建立 / 释放、重传、拥塞控制等机制引入延迟) 极高(无额外控制机制,延迟低、实时性强)
数据边界 字节流服务(无数据边界,接收方需自行处理数据拆分) 数据报服务(每个数据报独立,接收方按数据报完整接收,保留发送方的数据边界)
端口与寻址 基于端口号,支持一对一通信(默认),不支持多播 / 广播 基于端口号,支持一对一、一对多、多播 / 广播通信
资源占用 高(需维护连接状态、滑动窗口、重传队列等,消耗 CPU / 内存资源多) 低(无连接状态维护,协议栈处理逻辑简单,适合资源受限设备)
嵌入式 / 物联网适用场景 1. 需可靠传输的控制指令(如设备配置、远程控制);2. 文件传输(固件升级、日志上传);3. 关键数据上报(如报警信息) 1. 实时性要求高的场景(如传感器数据采集、视频流传输);2. 低功耗 / 低带宽设备(如 LoRa 网关数据转发);3. 广播 / 多播场景(如设备发现、组播通知)
典型应用协议 HTTP/HTTPS、FTP、SSH、Telnet、MQTT(默认基于 TCP) DNS、DHCP、SNMP、RTP(音视频传输)、MQTT-SN(轻量级 MQTT,支持 UDP)、CoAP(物联网应用层协议,支持 UDP)

3.4


四、项目工程知识问答

4.1 PID算法核心

PID 算法是工业控制领域最核心的闭环控制算法,其核心可概括为:以 "目标值 - 实际值" 的误差为唯一驱动源,通过比例(P)、积分(I)、微分(D)三个维度的协同调控,在 "响应速度、稳态精度、系统稳定性" 三者间找到最优平衡,最终让被控对象的实际值快速、准确、稳定地跟踪目标值


4.2 MQTT分为哪几个等级

MQTT 核心等级为 3 个服务质量(QoS)等级,用于定义消息传输的可靠性

等级 名称 可靠性 开销 适用场景
QoS0 最多一次 可能丢失 最小 传感器心跳、实时温湿度
QoS1 至少一次 无丢失、可能重复 中等 设备控制指令
QoS2 恰好一次 无丢失无重复 最大 固件升级、金融交易等核心数据
QoS 等级 核心定义 传输机制 适用场景 嵌入式应用示例
QoS 0(最多一次) 消息仅发送一次,不确认、不重发 发送方→接收方(无反馈) 网络稳定、允许丢失的场景(如传感器周期性上报) STM32 采集温湿度,定时向网关发送数据(丢失可容忍)
QoS 1(至少一次) 消息确保送达,可能重复 发送方→接收方→发送方(PUBACK 确认) 需确保送达、允许重复的场景(如控制指令下发) FreeRTOS 任务向设备发送 "启动电机" 指令(必须收到,重复可通过逻辑去重)
QoS 2(恰好一次) 消息确保送达且仅一次,无重复 四步握手(PUB→PUBREC→PUBREL→PUBCOMP) 严格禁止重复、需精准传输的场景(如数据上报、指令确认) 嵌入式设备向云平台上报报警数据(避免重复计数)

4.3 OTA的大致分类,一般使用哪些OTA升级

分类名称 英文缩写 升级对象 核心特点 典型应用场景
固件升级 FOTA(Firmware OTA) 设备底层固件(如 MCU/SoC 的程序) 影响硬件核心功能,需重启生效,安全性要求高 物联网传感器、智能家居设备、汽车 ECU
软件升级 SOTA(Software OTA) 应用层软件(如 APP、操作系统) 不影响底层硬件,可动态更新,灵活性强 智能手机系统更新、车载娱乐系统、智能电视 APP
配置升级 COTA(Configuration OTA) 设备参数配置(如网络参数、工作模式) 无需重启,更新速度快,数据量小 工业设备远程参数调整、智能家居场景模式切换
诊断升级 DOTA(Diagnostics OTA) 设备诊断程序 / 日志系统 用于远程故障排查、性能优化 汽车远程诊断、工业设备预测性维护
升级方式 核心特点 存储空间需求 网络带宽需求 升级失败风险 适用设备类型
全量升级 完整镜像替换,不依赖旧版本 中(仅需目标版本存储空间) 所有设备,尤其首次升级、跨大版本更新
差分升级(增量升级) 仅传输新旧版本差异部分,需基础版本 低(基础版本空间 + 补丁包空间) 中高 物联网低功耗设备、频繁小版本迭代场景
分块升级 镜像分块下载,支持断点续传 中(目标版本空间 + 单块缓存空间) 中(弱网适应性强) 偏远地区设备、卫星通信设备、大镜像升级
非 A/B 单分区升级 单分区直接更新,升级中不可用 低(仅需目标版本空间) 8 位 MCU、小型传感器等资源受限低端设备
固件冗余升级 存储多版本(运行版 + 备份版 + 出厂版),支持快速回滚 高(多版本存储空间叠加) 极低 工业控制器、医疗设备、汽车安全系统等关键设备
拷贝升级 先下载到临时存储区,验证后拷贝至运行分区 中(临时存储区 + 目标版本空间) STM32 等 32 位 MCU 嵌入式设备
乒乓升级(A/B 系统升级) 双分区(A 区运行 + B 区更新),无缝切换 高(双倍目标版本存储空间) 极低 智能手机、自动驾驶汽车、服务器等高可用性要求设备

4.4


五、咨询反问

5.1 介绍公司情况

5.2 问当前的状态

5.3 问薪资要求

5.4 发展规划

5.5

相关推荐
吃着火锅x唱着歌1 小时前
LeetCode 3583.统计特殊三元组
算法·leetcode·职场和发展
小南家的青蛙3 小时前
LeetCode第773题 - 滑动谜题
算法·leetcode·职场和发展
野蛮人6号4 小时前
力扣热题100道之78子集
算法·leetcode·职场和发展
踏浪无痕5 小时前
从单体PHP到微服务:一个五年老项目的血泪重构史
后端·面试·架构
阿伟*rui5 小时前
互联网大厂Java面试:音视频场景技术攻防与系统设计深度解析
java·redis·websocket·面试·音视频·高并发·后端架构
Java天梯之路5 小时前
Spring AOP:面向切面编程的优雅解耦之道
java·spring·面试
北慕阳6 小时前
(1)八股文 算法
面试·职场和发展
小白程序员成长日记7 小时前
2025.12.07 力扣每日一题
算法·leetcode·职场和发展
测试秃头怪16 小时前
2026最新软件测试面试八股文(含答案+文档)
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展