介绍一下SPI时序
- 硬件信号
- SCLK(Serial Clock):主机提供时钟。
- MOSI(Master Out Slave In):主发从收。
- MISO(Master In Slave Out):从发主收。
- CS/SS(Chip Select):低电平有效,选中某个从机。
- CPOL / CPHA 定义
SPI 的“模式”由 CPOL(时钟极性) 和 CPHA(时钟相位) 决定。
-
CPOL:时钟空闲电平
- 0 → 空闲时为低
- 1 → 空闲时为高
-
CPHA:在哪个边沿采样
- 0 → 第一个有效边沿采样
- 1 → 第二个有效边沿采样(第一个用于“预热”)
模式 | CPOL | CPHA | 空闲电平 | 采样沿 | 改变数据的沿 |
---|---|---|---|---|---|
0 | 0 | 0 | 低 | 上升沿采样 | 下降沿改变 |
1 | 0 | 1 | 低 | 下降沿采样 | 上升沿改变 |
2 | 1 | 0 | 高 | 下降沿采样 | 上升沿改变 |
3 | 1 | 1 | 高 | 上升沿采样 | 下降沿改变 |
-
数据传输过程
- 主机拉低 CS → 从机被选中。
- 主从机都在时钟驱动下,把各自移位寄存器的数据逐位推出,同时采样对方数据。
- 每个 SCLK 周期传输 1 bit,通常是 MSB 先行。
- 常见帧长度为 8/16/32 位。
-
为什么要这样设计
- 不同芯片习惯不同:有的逻辑在上升沿采样,有的在下降沿采样。
- 稳定性考虑:有些器件需要半个时钟周期才能稳定数据 → CPHA=1 就给了“半拍缓冲”。
- 电平兼容:有的电路要求空闲为高电平,有的要求低电平 → CPOL 提供灵活性。
- 结果:提供 4 种模式(0/1/2/3)保证主机和从机能对齐时序。
介绍一下IIC协议
好的,我们来系统地讲一遍 I²C 协议 (Inter-Integrated Circuit)。它是嵌入式面试的常客,理解了“电气层 + 协议层 + 仲裁/应答”,就能对答如流。
I²C 协议介绍
- 硬件层特点
-
两根线:
- SCL:时钟线,由主机驱动(多主模式下可仲裁)。
- SDA:数据线,双向。
- 开漏/开集电极:器件只能拉低(输出 0),不能主动输出高电平,逻辑 1 依赖上拉电阻。
- 有线与逻辑(wired-AND):多设备同时驱动时,0 胜 1。
- 因此可支持 多主多从,且仲裁不会破坏总线。
- 基本时序规则
- 起始条件 (START):在 SCL 高电平时,SDA 从高→低。
- 停止条件 (STOP):在 SCL 高电平时,SDA 从低→高。
- 数据有效性:在 SCL 高电平时,SDA 必须保持稳定;只有在 SCL 低电平时才允许变化。
- 帧结构
一个完整传输由以下部分组成:
START → [7位地址 + R/W] → ACK → [数据字节] → ACK/NACK → ... → STOP
- 地址:7 位常用,后跟 1 位读/写位。
-
应答 (ACK/NACK):
- 接收方在第 9 个时钟拉低 SDA 表示 ACK;
- 若保持高电平表示 NACK(通常表示没有设备响应或者数据接收结束)。
- 重复起始 (Repeated START):主机在一次会话中不释放总线,而是发新的 START,常用于先写寄存器地址再读数据。
- 速度等级
- 标准模式:100 kHz
- 快速模式:400 kHz
- 快速模式 Plus:1 MHz
- 高速模式:3.4 MHz
- 超高速:5 MHz(很少用)
- 仲裁与时钟同步
- 仲裁:多主机同时发起时,比较 SDA 在高电平时的值:谁发 1 却读到 0,就输掉仲裁,立即退出。
- 时钟同步:任何设备都可以拉低 SCL,延长低电平时间(称 Clock Stretching),以此“慢一点”传输。
一句话总结
I²C 用两根线、开漏与上拉,依靠 START/STOP 和 ACK/NACK 来分隔字节,支持多主仲裁与时钟拉伸,最常见速率是 100 kHz 与 400 kHz。
介绍一下CAN协议,仲裁机制
物理层特性
- 两根线:CAN_H 和 CAN_L,差分传输,抗干扰强。
-
两种电平:
- 显性 (dominant):逻辑 0,CAN_H≈3.5V、CAN_L≈1.5V,差分≈2V。
- 隐性 (recessive):逻辑 1,CAN_H≈2.5V、CAN_L≈2.5V,差分≈0。
-
总线逻辑:有线与(wired-AND),即只要有一个节点发 0,总线上就是 0。
帧结构 (经典 CAN)
一帧包含:
- SOF (Start of Frame):1 bit 显性 0,所有节点同步。
- 仲裁段:标识符 (11 位或 29 位) + 控制位 (RTR、IDE 等)。
- 数据段:0–8 字节(CAN-FD 可到 64 字节)。
- CRC 段:校验。
- ACK 段:接收方拉低确认位。
- EOF:结束。
仲裁机制原理
- 总线是 有线与,所以 0(显性) 胜过 1(隐性)。
-
多个节点同时开始发帧,在 仲裁段逐位比较:
- 如果一个节点发的是 1,但总线被拉成了 0,就说明有别的节点在发 0,它立刻停止发送,转为接收。
- 胜利的节点继续发,整个报文不会损坏,叫 无破坏仲裁。
优先级规则
- ID 数字越小,优先级越高。 因为 ID 从最高位开始比较,谁先出现 0,谁就赢。
- 数据帧优先于远程帧:数据帧的 RTR 位=0(显性),远程帧的 RTR=1(隐性)。
- 标准帧优先于扩展帧(当 11 位前缀相同):标准帧在 IDE 位发 0,扩展帧在 SRR/IDE 位发 1,所以扩展帧会输。
举例
假设两个节点同时发报文:
- A:ID =
0x080
→ 二进制0000 1000 0000
- B:ID =
0x100
→ 二进制0001 0000 0000
逐位仲裁:
- 前 3 位相同。
- 第 4 位:A 发
0
,B 发1
,结果总线为 0。 - B 发现自己发的是 1,却读到 0 → 输掉仲裁。
- A 获胜继续发送。
所以 ID 越小,优先级越高。
工程意义
- 实时性高:紧急报文(如刹车命令)给低 ID,保证一定能赢仲裁。
- 效率高:失败的节点不用重传一堆损坏帧,而是自然等待。
- 可扩展:多主机共用总线,自动决定谁发。
一句话总结
CAN 总线用显性(0)压制隐性(1),在仲裁段逐位比较 ID,谁先发 1 却读到 0 就输,结果是 ID 数值小的报文优先级最高。
有用过FreeRTOS吗,有了解优先级和调度的策略吗?
FreeRTOS 的优先级和调度策略
- 调度方式
-
基于优先级的抢占式调度 (Preemptive Scheduling)
- 配置
configUSE_PREEMPTION = 1
时启用。 - 内核时钟(SysTick)或事件触发调度器检查任务优先级。
- 就绪队列中 最高优先级的任务 必定运行。
- 配置
-
协作式调度 (Co-operative Scheduling)
- 配置
configUSE_PREEMPTION = 0
。 - 只有当任务调用
taskYIELD()
或进入阻塞态,才会切换。 - 更简单,但实时性差。
- 配置
- 优先级策略
- 每个任务创建时指定一个优先级(0 ~
configMAX_PRIORITIES-1
)。 - 数字越大,优先级越高。
- 调度器总是选择就绪态中 优先级最高的任务。
- 同优先级任务的调度
-
如果
configUSE_TIME_SLICING = 1
:- 同优先级任务在时钟节拍中 轮转执行(时间片调度)。
-
如果
configUSE_TIME_SLICING = 0
:- 一个任务只要不主动让出 CPU(比如
vTaskDelay
、taskYIELD
),就会一直跑下去。
- 一个任务只要不主动让出 CPU(比如
- 阻塞与唤醒
- 任务可能因为 延时 (
vTaskDelay
)、等待队列/信号量、事件通知 而进入阻塞态。 - 一旦条件满足,它被放回就绪队列,并按优先级重新竞争 CPU。
- 特殊机制
-
优先级继承 (Priority Inheritance):
- 当低优先级任务占用互斥锁,高优先级任务等待时,低优先级任务会临时“继承”高优先级,防止 优先级反转。
-
抢占阈值 (configMAX_PRIORITIES):
- 可以设定不同优先级的分布,保证关键任务一定能调度。
- 回答要点(简洁版,适合面试)
“FreeRTOS 的调度是基于优先级的抢占式调度,最高优先级就绪任务总能运行;同优先级任务可以配置是否时间片轮转;任务可以因延时或等待事件而阻塞,再由中断或内核唤醒。为防止优先级反转,FreeRTOS 的互斥锁支持优先级继承机制。”
static
- 在函数外(全局变量/函数前面加
static
)
- 效果:把符号的链接属性改成“内部链接”(internal linkage)。
- 意思:这个变量/函数 只在当前编译单元可见,别的
.c/.cpp
文件看不到。 -
用途:
- 避免命名冲突。
- 做“模块私有变量/函数”。
👉 示例:
// file1.c
static int counter = 0; // 只能在 file1.c 使用
static void helper() { // 只能在 file1.c 调用
counter++;
}
- 在函数内部(局部变量前加
static
)
- 效果:变量存储在 静态区,不是栈。
- 生命周期:从程序开始到结束都存在,但作用域只在函数内。
- 特点:只初始化一次,后续调用保留上次的值。
👉 示例:
void foo() {
static int cnt = 0; // 只初始化一次
cnt++;
printf("%d\n", cnt);
}
调用 foo()
三次,输出:
1
2
3
- 在类内部(C++)
- static 成员变量:属于整个类,不属于某个对象。所有对象共享这一份。
- static 成员函数:不依赖对象实例调用,不能访问非静态成员。
👉 示例:
class A {
public:
static int count; // 类变量
static void show() { // 类函数
cout << count << endl;
}
};
int A::count = 0; // 必须在类外定义(C++17 起可用 inline static 省略)
static
的总结
- 存储期:使变量进入静态存储区,生命周期贯穿整个程序。
-
作用域:
- 函数外 → 只在当前文件可见(内部链接)。
- 函数内 → 值跨调用保留。
- 类内 → 类成员/方法,不依赖对象。
- 与 const 区别:
const
是“值不变”,static
是“存储期/可见性”。
手撕:链表插入
struct ListNode { int val; ListNode* next; ListNode(int v): val(v), next(nullptr) {} };
struct DNode { int val; DNode* prev; DNode* next; DNode(int v): val(v), prev(nullptr), next(nullptr) {} };
进程和线程的区别
进程 (Process) 操作系统分配资源的基本单位。每个进程有独立的 地址空间、代码段、数据段、堆、栈等。
线程 (Thread) CPU 调度的最小单位,是进程中的“执行流”。一个进程可以有多个线程,它们共享进程的地址空间和资源。
进程:创建/切换时需要切换 虚拟内存页表、文件描述符表 等,开销大。
线程:切换时只改寄存器、栈指针,上下文切换轻量。
进程间通信 (IPC):管道、消息队列、共享内存、socket 等。需要操作系统内核支持。
线程间通信:更简单,直接共享内存 + 同步原语(互斥锁、条件变量、信号量)。
CAN总线中的采样点
采样点就是 CAN 控制器在比特周期内“读取总线电平”的时刻,通常设置在比特时间的 80% ~ 87.5%,通过调节 PROP_SEG 和 PHASE_SEG1 实现。它的位置影响 CAN 总线对抖动、延迟和噪声的容忍度。