嵌入式软件面试小结
本文章是我面试了几家公司后的嵌入式软件面试小结,也为我以后面试其他嵌入式岗位打下基础。分为C语言、ARM开发等。有纰漏请指出,转载请说明。学习交流请发邮件。
前言:本文章是我面试了几家公司后的嵌入式软件面试小结,也为我以后面试其他嵌入式岗位打下基础。分为C语言、ARM开发等。
有纰漏请指出,转载请说明。
学习交流请发邮件 1280253714@qq.com
1 C语言
1.1函数指针和指针函数
函数指针 int (*fun)(int x,int y);
函数指针的实质是指针,简单理解为指向函数的指针,占4个字节(32位OS)。函数的实质是一段代码,这一段代码在内存连续分布,函数地址即函数第一句代码的地址。所以,函数指针是一个普通变量,它的值为函数的地址。
#include <stdio.h>
void printOut(void){
printf("I am fun");
}
int main(void){
void (*fun)(void);
fun=printOut;
fun();
return 0;
}
#include <stdio.h>
int add(int a, int b){
return a+b;
}
int main(void){
int (*fun)(int x, int y);
fun=add;
printf("%d \n",fun(1,2)); //3
return 0;
}
指针函数 int * fun(int x,int y);
指针函数的实质是函数,简单理解为返回指针的函数
#include <stdio.h>
// 返回两个变量中较大的一个
int *compareInt(int *var1, int *var2)
{
if(*var1 >= *var2)
{
return var1;
}else{
return var2;
}
}
int main()
{
int a=2;
int b=3;
int *fun = compareInt(&a, &b);
printf("%d \n",*fun); //3
return 0;
}
1.2 指针数组和数组指针
指针数组 int *p[5]
指针数组是数组,数组里存放着变量的地址,即数组的各元素指向某变量
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
int *p[2];
p[0] = &a;
p[1] = &b;
printf("%p\n", p[0]); //a的地址
printf("%p\n", &a); //a的地址
printf("%d\n", *p[0]); //p[0]表示a的地址,则*p[0]表示a的值
return 0;
}
数组指针 int (*p)[5]
数组指针是指针,指针指向数组的首地址,其实可以理解为数组换了个名字,其用法跟数组一样(因为数组名其实也是一组变量的首地址)
#include <stdio.h>
int main()
{
int a[2]={1,2};
int (*p)[2];
p=&a;
printf("%p\n", p); //a的地址
printf("%p\n", p[0]); //a的地址
printf("%p\n", &a); //a的地址
printf("%d\n", *a); //a[0]的值
printf("%d\n", a[0]); //a[0]的值
printf("%d\n", *p[0]); //p[0]表示a的地址,则*p[0]表示a[0]的值
return 0;
}
1.3 结构体字节对齐问题
结构体元素的偏移量要考虑元素的对齐访问,结构体实际占用的字节数与所有成员占用的字节数的总和不一定相等。
32位系统的内存,每次按照4个字节对齐访问,效率是最高的。对齐访问牺牲了内存空间,换取速度性能。
//编译器默认4字节对齐
#include <stdio.h>
/*
#pragma pack(1) //手动1字节对齐
*/
typedef struct
{ //1字节对齐 4字节对齐
int a; // 4 4
char b; // 1 2
long long c; // 8 8
}MyStruct;
int main(void){
MyStruct s1;
printf("%d \n",sizeof(s1)); //如果是1字节对齐的话,s1占16个字节
//printf("%d \n",sizeof(s1)); //如果是1字节对齐的话,s1占13个字节
return 0;
}
1.4 共同体
共同体:同一个内存空间的多种解析方式,共同体各变量互斥,所有成员占用同一块地址空间,
#include <stdio.h>
union MyUnion
{
int a;
char b;
};
int main(void){
union MyUnion u1;
printf("%p \n",&u1.a); //u1.a的地址
printf("%p \n",&u1.b); //u1.b的地址
u1.a=1;
printf("%d \n",u1.a); //1
u1.b=2;
printf("%d \n",u1.a); //2
printf("%d \n",u1.b); //2
return 0;
}
1.5 static关键字
各种变量(static、局部、全局、动态内存等等)的生命周期、链接属性、作用域、内存区域、默认值
可简单理解如下:
局部变量:用static修饰局部变量,可看成全局变量
全局变量:用static修饰全局变量,静态全局变量只能被本.c文件引用
函数:用static修饰函数,静态函数只能被本.c文件引用
1.6 C语言常用库函数
strcpy strlen strcat strcmp malloc calloc abs printf scanf sprintf fopen fclose
1.7 map文件
简单来说,map文件就是通过编译器编译之后,生成的程序、数据及IO空间信息的一种映射文件,里面包含函数大小,入口地址等一些重要信息。
1.8 内存泄露
1.8.1 什么是内存泄露?
内存泄漏指的是程序在运行时动态分配的内存空间,在使用完毕后未及时释放,导致系统的可用内存越来越少,最终耗尽系统所有的内存资源。
1.8.2 内存泄露的原因
-
未释放的堆内存:程序在运行过程中,通过malloc、calloc、realloc等动态分配内存的函数申请了内存空间,但在使用完毕后没有及时释放,导致系统的可用内存越来越少。
-
未关闭的文件句柄:程序在打开文件后没有及时关闭文件,导致文件句柄一直存在于系统中,占用系统资源,最终导致内存泄露。
-
循环引用:在使用面向对象编程时,如果两个对象之间互相引用,即形成了循环引用,就会导致内存泄漏。因为两个对象之间的引用计数器永远都不会变为0,导致它们所占用的内存空间得不到释放。
-
缓存未清理:程序中使用了缓存,但在使用完毕后没有及时清理,导致缓存中的数据一直占用内存空间,最终导致内存泄漏。
1.8.3 如何避免内存泄露?
-
正确地使用内存分配和释放函数:程序在使用malloc、calloc、realloc等动态分配内存的函数时,一定要记得在使用完毕后及时调用free函数释放内存空间。
-
正确地使用文件句柄:程序在打开文件后,一定要记得在使用完毕后及时关闭文件,释放文件句柄所占用的资源。
-
避免循环引用:在使用面向对象编程时,要避免对象之间的循环引用,可以使用弱引用等技术来解决这个问题。
-
及时清理缓存:程序中使用缓存时,要及时清理缓存中的数据,避免数据一直占用内存空间。可以使用LRU等算法来管理缓存。
2 STM32
2.1 GPIO口的8种模式
2.2 72MHz时钟配置的过程
-
选择8MHz的外部晶振时钟作为时钟源
-
不分频输入锁相环
-
锁相环9倍频后输出72MHz作为系统时钟
2.3 固件库文件
-
1-startup_stm32f10x_hd.s
设置堆栈指针、设置PC指针、初始化中断向量表、配置系统时钟,调用c库函数_main最终去到c的世界
栈:变量(全局,局部),函数调用
堆:动态内存分配,malloc()
-
2-时钟配置文件
system_stm32f10x.c: 把外部时钟HSE=8M,经过PLL倍频为72M
-
3-外设相关的
stm32f10x.h:实现了内核之外的外设的寄存器映射
(XXX: GPIO、USRAT、 I2C、 SPI、 FSMC)
stm32f10x_xx.c:外设的驱动函数库文件
stm32f10x_xx.h: 存放外设的初始化结构体,外设初始化结构体成员的参数列表,外设固件库函数的声明
-
4-内核相关的CMSIS - Cortex 微控制器软件接口标准
core_cm3.h、core_cm3.c:实现了内核里面外设的寄存器映射
misc.h、misc.c : NVIC(嵌套向量中断控制器)、sysTick(系统滴答定时器)
-
5-头文件的配置文件
stm32f10x conf.h:头文件的头文件
//stm32f10x_usart.h
//stm32f10x_i2c.h
//stm32f10x_spi.h
//stm32f10x_adc.h
//stm32f10x_fsmc.h
-
6-专门存放中断服务函数的c文件
stm32f10x_it.c
stm32f10x_it.h
中断服务函数你可以随意放在其他的地方,并不是一定要放在stm32f10x_ it.c
2.4 STM32F103VET6的资源
512K FLASH
64K SRAM
72MHz 时钟频率
4G 存储器映射
2.4 常用外设及区别
2.4.1 UART
通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),可全双工传输
假设波特率为115200,问串口一秒传输多少数据?
115200比特率,1秒传输115200bit数据
常用8bit数据位,1bit起始,1bit停止
115200/10/1024=11.25K
其实RS232和RS485只是改变了物理层,数据帧格式和普通的串口通信没有区别
RS232就是把常用的串口TTL信号通过MAX232转换为-15~+15的电平范围,增强了抗干扰能力
RS485就是把常用的串口TTL信号通过MAX485转换为差分信号,抗干扰能力更强
2.4.2 IIC
有SCL时钟线和SDA数据线,用GPIO口的开漏输出模式,并由分立元器件为总线提供上拉电流
100kbps为标准速度,400kbps为高速
2.4.3 SPI
SPI是串行外设接口(Serial Peripheral Interface)的缩写
SPI总线一般应用于ADC、FLASH、EEPROM、LCD/OLED显示屏
SPI一般是一主多从模式,没有向IIC那样的起始、结束、应答信号,直接CS引脚拉低,即为选中某个从设备进行通信。
2.4.4 DMA
DMA属于Cortex-M3内核外设,全称为direct memory access,直接存储器存取。不需要CPU的参与,即可进行数据通信。
具体细节写在我的博文里
2.4.5 ADC
ADC全称Analog-to-Digital Converter,即将模拟信号转换为数字信号的器件,我用过的AD转换芯片有PCF8591,而STM32集成了3个ADC,12位的精度。
AD模式有以下几种:并联转换型、逐次逼近型、双积分型。
并联转换型,例如电压范围为0~8V,用8个相同阻值的电阻串联在一起,每个电阻均分得1V的电压,比较器一端接在均匀的1、2、3、4、5、6、7、8压降,另一端接需要比较的电压,当Vin为6.5V时,此时输出0xef,即01111111B,表示7V,用8个比较器得出的结果放在刚好8bit的寄存器,转换结果瞬间完成。当电压范围为0-5V,分成2^12份,每一份为0.0012V,那么该转换电路需要4096个串联在一起的电阻,需要4096个比较器,此时虽然转换速度快,但电路规模太大。
逐次逼近型,可理解为老式的秤,假如此时秤砣有1、2、4、8、16KG,我放一个13KG的重物,此时先放16KG的秤砣,游标往下,那么把16KG的拿下,换成8KG的,游标翘起,再加4KG的秤砣,游标还是翘起,再加2KG的秤砣,游标往下,把2KG的换成1KG的,此时刚好秤砣总重为13KG。
也可以理解为数学上的二分法。
这样,当电压范围为0-5V,分成2^12份时,只需要最多比较12次即可得出结果,相较于并联转换型,逐次逼近型牺牲了速度,简化了电路。
2.4.6 定时器
B站这位UP主讲定时器工作原理 讲得很好
定时器的具体细节写在我的博文里
stm32有3种定时器,分别是基本定时器、通用定时器、高级定时器
基本定时器是一个 16 位的只能向上计数的定时器,只能定时,没有外部IO
通用定时器是一个 16 位的可以向上/下计数的定时器,可以定时,可以输出比较,可以输入捕捉,每个定时器有四个外部 IO
高级定时器 是一个 16 位的可以向上/下计数的定时器,可以定时,可以输出比较,可以输入捕捉,还可以有三相电机互补输出信号,每个定时器有 8 个外部 IO
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)