海能达嵌入式面经
1.volatile 和 const 的含义
const(只读约束):修饰变量表示该变量是“只读”的,不可被程序直接修改。常用于函数参数、只读数组或固定地址映射(如const uint8_t table[])。
volatile(易变约束):告诉编译器该变量可能被编译器未知的因素(如硬件中断、RTOS任务切换、DMA后台改写)随时改变,禁止编译器对该变量的访问进行优化(即强制每次从内存地址重新读取,不缓存到寄存器)。经典用法:硬件寄存器地址、中断服务程序中的全局标志位、RTOS中的共享变量。
2.STM32上UART如何实现收发
发送(TX):
轮询模式:HAL_UART_Transmit,等待TC(发送完成)或TXE(发送数据寄存器空)标志,CPU死等,浪费资源。
中断模式:HAL_UART_Transmit_IT,使能TXEIE中断,每发完一个字节触发中断,直到FIFO/缓冲区全部发完。
DMA模式:HAL_UART_Transmit_DMA,配置DMA把内存数据搬运到DR寄存器,CPU零干预,适合大数据块传输(如日志打印)。
接收(RX):
轮询/中断:HAL_UART_Receive_IT,接收一个字节触发RXNE中断。
空闲中断(IDLE):工程中最推荐,利用__HAL_UART_ENABLE_IT(&huart, UART_IT_IDLE),配合DMA接收不定长数据(无需固定帧尾,能准确判断一帧结束)。
核心流程:初始化时钟/GPIO(AF复用)→ 配置波特率/数据位/停止位 → 使能NVIC → 实现回调函数(HAL_UART_RxCpltCallback)。
3.I^2C时序
IIC是半双工同步串行总线,包含SCL(时钟线)和SDA(数据线),空闲态均为高电平(上拉电阻拉高):
起始信号(START):SCL高电平时,SDA发生下降沿。
停止信号(STOP):SCL高电平时,SDA发生上升沿。
数据有效性:SCL高电平期间,SDA必须稳定;SDA跳变只能发生在SCL低电平期间。
应答信号(ACK):每传输8位数据后,第9个SCL脉冲,主机释放SDA(拉高),若从机拉低SDA则表示应答(ACK),否则为非应答(NACK)。
速率:标准模式100kbps,快速模式400kbps,高速模式3.4Mbps。
4.I^2C的代码实现封装
优秀的工程封装应分为3层:
硬件抽象层(HAL_Driver):
直接调用STM32 HAL库的HAL_I2C_Master_Transmit/Receive,或使用GPIO模拟(精准延迟)。
传输层(I2C_Bus):封装基础的起始/停止/发送字节/接收字节/检查应答,并提供超时保护机制(防止挂死)。
设备应用层(Device_Driver):针对具体设备(如AT24C02、MPU6050),封装成xxx_WriteByte(addr, data)、xxx_ReadBytes(addr, buf, len),内部调用传输层接口。
补充:模拟IIC代码要点是时序严格,尤其是在高速MCU上需插入__NOP()或delay调整SCL高低电平宽度以满足从机建立/保持时间。
5.IIC总线上地址重复导致多个设备同时响应会发生什么?
硬件设计错误。当主机发送该重复地址时,多个从机都会在应答位(第9个SCL脉冲)同时拉低SDA线。
后果:总线冲突(Bus Contention)。由于IIC是开漏输出+上拉电阻,强拉低是“线与”逻辑。虽然从逻辑电平上看可能仍是低电平(ACK),但多个设备的输出级会争夺电流,导致信号边沿变缓、波形畸变,甚至损坏从机IO口。对于数据位,若一个从机输出1(释放总线),另一个输出0(拉低),则总线被强制拉低,导致数据位错误(主机读到的全是乱码)。
解决方案:硬件上必须保证地址唯一(通过引脚配置不同电平或选用不同型号芯片);软件上若不可避免,可增加片选使能引脚(CS)或使用IIC多路复用器(如TCA9548A)。
6.Socket如何创建和使用(流程)
以TCP流式套接字为例(面向连接):
服务端流程:
socket(int domain, int type, int protocol) →创建套接字描述符。
bind(int sockfd, struct sockaddr *addr, socklen_t addrlen) → 绑定IP和端口。
listen(int sockfd, int backlog) → 监听端口,backlog为最大等待队列长度。
accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) → 阻塞等待客户端连接,返回新的通信套接字(new_fd)。
read(new_fd, buf, len) / write(new_fd, buf, len) → 收发数据。
close(new_fd) / close(sockfd) → 关闭连接。
客户端流程:
socket() → 创建套接字。
connect(int sockfd, struct sockaddr *addr, socklen_t addrlen) → 发起三次握手连接。
write() / read() → 收发数据。
close() → 关闭连接。
注:UDP无连接,服务端无需listen/accept,直接使用sendto/recvfrom。
7.Linux下利用字符串查找文件的命令
最常用:grep -rnw "指定路径" -e "要查找的字符串"。
-r:递归子目录;-n:显示行号;-w:精确匹配全词(避免部分匹配);
例:grep -rnw ./src/ -e "main_function"。
查找特定类型文件:find /path -name "*.c" | xargs grep "字符串"。
只显示文件名:grep -rl "字符串" /path。
8.父类两个虚函数,子类继承父类,求子类的内存占用(经典陷阱题)
分情况讨论(以64位系统,字节对齐为8为例):
情况A(子类无新增成员变量):
子类对象大小 = 8 字节(仅含继承自父类的虚函数指针 __vptr,64位下占8字节)。
注意:无论父类有几个虚函数(2个或200个),只要不是多重继承,对象内存中只有1个虚函数指针。虚函数表(vtbl)是类级别的,对象不存储函数代码。
情况B(子类新增了1个int成员变量):
大小 = 虚表指针(8) + int(4) = 12,对齐到8的倍数 → 16 字节。
情况C(多重继承,且父类都有虚函数):
子类会继承多个虚函数指针(每个有虚函数的父类各一个),大小相应叠加。
9.虚函数表(vtbl)、虚函数指针(vptr)、虚函数存放的位置(硬核内存分区)

10. volatile 在多线程环境下的工作原理
volatile 的核心作用是告诉编译器:“该变量可能会在程序流程之外被改变,禁止对该变量的访问做任何优化”。
工作原理(编译器层面):
- 强制每次访问都从内存地址中读取,绝不会将值缓存到CPU寄存器中重复使用。
- 禁止编译器将该变量相关的指令进行重排序(优化重排)。
在多线程下的致命误区:
volatile 不提供任何原子性,也不提供内存屏障(Memory Barrier)。
在多核CPU上,即使禁止了编译器优化,CPU硬件级乱序执行和Cache写回延迟依然存在。线程A修改了volatile变量,线程B可能过很久才在Cache中看到新值(除非你是单核裸机)。
结论:在Linux/POSIX多线程中,volatile几乎无用。必须使用std::atomic(C++11)或互斥锁,它们同时提供编译器屏障和CPU硬件屏障。
更多推荐


所有评论(0)