海康BSP嵌入式开发实习面试经验

✅ IIC启动流程

I2C 启动流程概述

  1. 总线空闲状态
    在通信开始之前,I2C总线处于空闲状态,即SDA和SCL线都处于高电平。

  2. 发送启动信号(START)
    主设备在SCL线为高电平时,将SDA线从高电平拉低,形成启动信号。这一过程标志着通信的开始。

  3. 发送设备地址和读写位
    启动信号后,主设备发送目标设备的地址(通常为7位)和读写位。读写位为0表示写操作,为1表示读操作。

  4. 等待应答信号(ACK)
    从设备接收到地址和读写位后,返回一个应答信号(ACK)。如果从设备未响应,主设备可以根据需要进行重试或处理错误。

  5. 数据传输
    主设备根据读写位的设置,开始与从设备进行数据的读写操作。每传输一个字节后,接收方返回一个应答信号。

  6. 发送停止信号(STOP)
    数据传输完成后,主设备在SCL线为高电平时,将SDA线从低电平拉高,形成停止信号,标志着通信的结束。

🔹 特别注意
IIC信号在数据传输过程中,当SCL=1(高电平)时,数据线SDA必须保持稳定状态,不允许有电平跳变。只有在SCL为低电平期间,SDA的电平状态才允许变化。


✅ C语言关键字

volatile

🔹 防止编译器优化
编译器可能会将变量存储在寄存器中以提高访问效率。但如果该变量的值可能被外部因素(如硬件中断、其他线程或外部设备)改变,使用volatile修饰的变量会强制编译器每次从内存中读取最新值。

🔹 保证顺序性
volatile修饰的变量的读写操作不会被编译器重排序,确保操作顺序与源代码一致。

🔹 硬件寄存器访问
在嵌入式系统中,直接访问硬件寄存器时,使用volatile可确保每次访问都读取硬件的最新值。

🔹 中断服务例程(ISR)
中断处理程序修改的变量需用volatile修饰,确保主程序读取最新值。

🔹 多线程编程
多线程环境中,volatile可确保线程间变量的可见性(但需注意它不保证原子性)。

1
2
3
4
5
6
7
8
9
10
11
12
volatile int flag = 0;

void interrupt_handler() {
flag = 1; // 中断服务程序修改 flag 的值
}

int main() {
while (!flag) {
// 等待中断处理程序将 flag 设置为 1
}
return 0;
}

在上述代码中,flag 被 volatile 修饰,确保主程序每次访问 flag 时都从内存中读取其最新值,而非使用寄存器缓存的值。

static

🔹 修饰局部变量:保持变量的持久性
当 static 用于修饰局部变量时,该变量的生命周期将持续整个程序运行期间,但其作用域仍限于定义它的函数内部。​这意味着变量在函数调用之间保持其值,而不是每次调用时重新初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

void count_calls() {
static int count = 0;
printf("Function called %d times\n", ++count);
}

int main() {
count_calls();
count_calls();
count_calls();
return 0;
}

输出

1
2
3
4
Function called 1 times
Function called 2 times
Function called 3 times

在上述代码中,count 变量在函数 count_calls 的多次调用之间保持其值,实现了对函数调用次数的统计。

🔹 修饰全局变量和函数:限制作用域
当 static 用于修饰全局变量或函数时,它将限制该变量或函数的作用域仅限于定义它的源文件(即翻译单元)。​这意味着其他源文件无法通过 extern 关键字访问这些变量或函数,从而避免了命名冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// file1.c
#include <stdio.h>

static int secret = 42;

static void show_secret() {
printf("The secret is %d\n", secret);
}

int main() {
show_secret();
return 0;
}

在另一个源文件中,尝试访问 secret 或 show_secret 将导致链接错误,因为它们的作用域被限制在 file1.c 中。

🔹 修饰类成员(C++):实现类级别的共享
在 C++ 中,static 可以用于修饰类的成员变量和成员函数,使其成为类级别的成员,而非实例级别的成员。​这意味着所有类的实例共享同一份静态成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// file1.c
#include <iostream>

class Counter {
public:
static int count;

Counter() {
++count;
}
};

int Counter::count = 0;

int main() {
Counter a;
Counter b;
std::cout << "Number of Counter instances: " << Counter::count << std::endl;
return 0;
}


输出

1
2
Number of Counter instances: 2

在上述代码中,count 是一个静态成员变量,所有 Counter 类的实例共享同一份 count 变量。

🔹 修饰函数参数(C99):指定数组的最小大小

在 C99 标准中,static 可以用于修饰函数参数,指定数组参数的最小大小。​这有助于编译器进行更严格的类型检查。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

void process_data(int data[static 10]) {
// 处理数据
}

int main() {
int arr[10] = {0};
process_data(arr);
return 0;
}

在上述代码中,data 参数被声明为至少包含 10 个元素的数组,编译器将检查传递给 process_data 函数的数组是否满足这一要求。


✅ C语言编程题

内存对齐

以下两个结构体分别占用多少内存

1
2
3
4
5
6
7
8
9
10
11
struct student {
int no; //学号
char name; //姓名
short sex; //性别
};
struct teach {
char no; //学号
int name; //姓名
short sex; //性别
};

🔹 student内存布局分析
no(int):占 0-3 字节(对齐到 4 字节)。

name(char):占 4 字节(对齐到 1 字节,无需填充)。

sex(short):需要对齐到 2 字节,所以必须在地址 6 开始(因为地址 5 未对齐到 2)。因此,在 name(地址 4)后插入 1 字节填充(地址 5)。sex 占 6-7 字节。

总大小:0-7 共 8 字节,且 8 是最大成员 int(4 字节)的整数倍,无需末尾填充。

🔹 teach内存布局分析
no(char):占 0 字节(对齐到 1 字节)。

name(int):需要对齐到 4 字节,所以必须在地址 4 开始。因此:在 no(地址 0)后插入 3 字节填充(地址 1-3)。name 占 4-7 字节。

sex(short):占 8-9 字节(对齐到 2 字节,地址 8 已对齐)。

总大小:当前已用 0-9 共 10 字节,但结构体总大小必须是最大成员 int(4 字节)的整数倍。因此,在 sex(地址 9)后插入 2 字节填充(地址 10-11),使总大小为 12 字节(12 是 4 的倍数)。

malloc(记不太清了,感觉不会这么简单)

以下代码输出结果

1
2
3
4
5
6
7
8
9
int* p = (int*)malloc(sizeof(int));
if (p== NULL) {
printf("Memory allocation failed\n");
return 1;//返回非零值表示错误
}
*p= 12345;
printf("result = %d\n", *p);
free(p);//使用完毕后释放内存
return 0;

输出 result =12345


✅ FreeRTOS任务调度

FreeRTOS是怎么进行调度的

FreeRTOS 的任务调度遵循优先级调度的原则,其中高优先级的任务优先执行。当任务调度器决定哪个任务运行时,它会选择优先级最高的就绪任务。
任务优先级
每个任务都具有一个优先级,优先级数值较大的任务优先级较高。任务的优先级是在创建任务时指定的,可以在运行时通过 vTaskPrioritySet() 函数动态修改任务的优先级。
调度器行为
🔹 抢占式调度
如果高优先级任务变为就绪状态,调度器会立即中断当前正在运行的低优先级任务,切换到高优先级任务执行。
🔹 协作式调度
任务在执行时不会被抢占,除非它显式地调用 taskYIELD() 或其他类似的 API 来让出 CPU 给其他任务。
FreeRTOS 中,任务可以通过时间片轮转的方式调度,除非某个任务被标记为“挂起”或“阻塞”,此时它不会被调度。

高优先级任务一直运行会不会霸占CPU资源

如果 FreeRTOS 中的高优先级任务长时间占用 CPU 而不主动让出控制权,低优先级任务将无法获得执行机会,导致 CPU 资源被高优先级任务“霸占”。​
🔹 为什么高优先级任务会“霸占”CPU?**
在 FreeRTOS 中,任务调度遵循优先级原则:优先级高的任务优先执行。如果高优先级任务没有被挂起、阻塞或主动让出 CPU,它将持续占用 CPU,导致其他任务无法执行。
🔹 可能导致的问题
任务饥饿:​低优先级任务长时间无法获得 CPU 时间,可能导致系统响应迟缓或实时性差。​
系统不稳定:​某些低优先级任务可能负责关键功能(如通信、数据采集等),长时间无法执行可能导致系统功能异常。
🔹 如何避免“霸占”CPU
使用 vTaskDelay() 或 vTaskDelayUntil():​让任务主动延时,释放 CPU 给其他任务。​
示例:

1
2
vTaskDelay(pdMS_TO_TICKS(10));  // 延时 10 毫秒

使用 taskYIELD():​在任务中适当位置调用,主动让出 CPU 给同优先级的其他任务。​
示例:

1
taskYIELD();  // 主动让出 CPU

合理设计任务优先级:​确保高优先级任务只处理紧急或实时性要求高的任务,其他任务使用较低优先级。​
使用互斥机制:​避免高优先级任务长时间占用共享资源,导致低优先级任务无法访问


海康BSP嵌入式开发实习面试经验
https://chrisy0618.github.io/2025/04/15/hello-world/
作者
Chris.Y
发布于
2025年4月15日
许可协议