面灵AI→

正浩创新嵌入式软件一面:static 全局变量、结构体对齐与串口 DMA

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

  1. C 语言中全局变量和局部变量有什么区别?分别放在哪里?
  2. static 修饰全局变量后会发生什么变化?其他文件想访问这个 static 全局变量有什么方法?
  3. 如何把全局变量放到自己指定的内存区域?
  4. 项目中用过函数指针吗?
  5. 32 位 MCU 上,依次包含 uint8_t、uint16_t、uint32_t 的结构体占多少字节?
  6. 高优先级任务正在运行,怎样让低优先级任务获得执行机会?
  7. 中断产生数据,多个任务都要读取,应该怎么设计?如果任务还会修改原始数据呢?
  8. 请完整介绍串口 DMA 接收的处理流程。

《参考解析》

存放位置:全局变量(含 static 全局)在静态存储区的 .data(已初始化)或 .bss(未初始化/零初始化);局部变量在栈上;malloc 的在堆上;字符串常量与 const 全局通常在 .rodata;代码在 .text。局部变量如果加了 static 就移到静态区,生命周期变成整个程序。

static 修饰全局变量:把链接属性从外部改为内部,符号不再导出,其他编译单元无法用 extern 引用,避免命名冲突。其他文件想访问只有两条路:本文件提供一个访问函数(getter/setter,推荐),或者通过指针在初始化时把地址传出去。直接 extern 是不行的——这是面试官想听到的结论。

指定内存区域:靠链接脚本(.ld / scatter file)定义段,再用编译器扩展把变量放进该段——GCC 用 __attribute__((section(".my_section"))),Keil/ARMCC 用 __attribute__((section(".ARM.__at_0x20000000"))) 或 #pragma arm section,IAR 用 @ 或 #pragma location;然后在链接脚本里把该段定位到目标地址(如 CCM RAM、备份域、外部 SDRAM)。注意该区域是否被启动代码清零、是否需要手动初始化。

函数指针:用于回调(注册中断/事件处理)、状态机跳转表、驱动接口抽象(同一套 API 对应不同硬件)、以及把命令字映射到处理函数。要能写出声明与调用:typedef void (*cb_t)(int); cb_t cb = &handler; cb(1);。

结构体对齐:默认按成员自然对齐,uint8_t 占 1 字节,后面 uint16_t 要对齐到 2 的倍数,所以 1 字节后补 1 字节;uint32_t 对齐到 4,此时偏移已是 4,无需再补;总大小要对齐到最大成员(4),1+1(补)+2+4 = 8,已经是 4 的倍数,答案 8 字节。若顺序改成 32/16/8 也是 8。想紧凑就用 __attribute__((packed)) 或 #pragma pack(1),但会带来非对齐访问的性能损失甚至硬件异常(Cortex-M0 不支持非对齐访问)。

让低优先级任务获得执行机会:同优先级时间片轮转、taskYIELD() 主动让出、在低优先级任务里等信号量/队列(阻塞即让出 CPU)、把高优先级任务改成周期性阻塞(vTaskDelay)、以及调低高优先级任务优先级。要说明「高优先级任务如果不阻塞,低优先级永远饿死」——这是 RTOS 调度器按优先级抢占的必然后果。

中断数据被多任务共享:读多写少且只读快照用「双缓冲 + 指针切换」或「环形缓冲区 + 读写索引原子更新」;需要一致性时用临界区(关中断)或 RTOS 的互斥量(但中断里不能用互斥量,只能用 FromISR 版本 API 发信号量通知任务)。如果任务还要修改数据,最好把「修改」收敛到单一任务(数据所有者),其他任务通过消息队列请求修改,避免多写者竞争;确需共享则用互斥量 + 拷贝出去再改,缩短临界区。

串口 DMA 接收:初始化 GPIO 与 USART,配置 DMA 通道(外设到内存、循环或普通模式),开 DMA 传输完成/半传输中断与串口空闲中断(IDLE)。数据到来时 DMA 自动搬进缓冲区,不占 CPU;用「空闲中断 + 已传输计数」判断一帧结束(变长帧的关键),把数据拷到队列或置标志交给任务处理;配合环形缓冲与双缓冲避免处理期间被覆盖。错误处理要覆盖溢出(ORE)、帧错误、DMA 传输错误,并做超时保护。