首页/学习资料/CVTE视源股份嵌入式单片机方向一面面经/
CVTE视源股份嵌入式单片机方向一面面经
2024-04-22 16:12:41697浏览
CVTE视源股份嵌入式单片机方向一面面经,个人经历回顾版

CVTE嵌入式一面(单片机方向)

1volatile关键字的作用

在C和C++中,volatile关键字用于告诉编译器,该变量的值可能会在程序的控制之外被改 变。这可以防止编译器对该变量的读取和写入进行优化,以确保在编译器优化时正确的访问该变量。

volatile关键字通常用于以下情况:

1、多线程环境下的共享变量:当一个变量被多个线程共享时,且这些线程中有些线程可能会修改该变量的值,而其他线程可能会读取该变量的值时,需要使用volatile关键字。这可以确保每次访问该变量时都会重新从内存中读取其值,而不是使用之前缓存的值。

2、中断服务子程序:在嵌入式系统中,当一个变量被中断服务子程序(ISR)和主程序共享时,该变量通常会被声明为volatile。因为ISR可以在任何时候修改该变量的值,而主程序可能在任何时候读取该变量的值。

3、访问硬件:存储器映射的硬件寄存器通常加volatile,由于硬件可能在程序控制之外修改寄存器的值,因此需要使用volatile关键字。这确保了编译器不会对与硬件通信的代码进行优化,以免导致意外。

实例:

#include <stdio.h> #include <unistd.h>

volatile int flag = 0;


void *threadFunction(void *arg) { sleep(1);
flag = 1; // 在子线程中修改 flag 的值
return NULL;
}


int main() {
pthread_t tid;
pthread_create(&tid, NULL, threadFunction, NULL);

// 在主线程中检查 flag 的值,由于 flag 是 volatile 的,确保每次读取都从内存中读取最新值
while (!flag) {
printf("Waiting for flag to become true...\n"); usleep(100000); // 等待100毫秒
}


printf("Flag has become true!\n"); pthread_join(tid, NULL);

return 0;


}

解析:函数中,主线程不断检查flag的值是否为1(true),如果不是,则打印一条消息并等待一段时间。同时,在子线程中,等待一段时间后将flag的值设置为1.由于flag是volatile的,主线程中对flag的每次读取都会从内存中读取最新的值,而不会使用缓存的值,因此,当子线程将flag设置为1时。主线程能够立刻感知到。


2、堆和栈的区别

1、申请方式:栈的空间由操作系统自动分配和释放,堆的空间手动分配和释放
2、申请大小的限制
①栈的空间有限。在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存区
域。在Windows中,栈的大小是2M(也有的说1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间,将提示超出界限。
②堆是很大的自由存储区,堆是向高地址扩展的数据结构,是不连续的内存区域,这是由于系统是用链表来存储的空闲内存地址,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
3、申请效率
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是new分配的内存,一般速度比较慢,容易产生内存碎片,但使用更加方便。


3、栈的作用,什么是栈溢出现象,以及如何解决

栈的作用:

1、C语言中栈用来存储临时变量,临时变量包括函数参数和函数内部定义的临时变量。函数调用中和函数调用相关的函数返回地址,函数中的临时变量,寄存器等均保存在栈中,函数调动返回后从栈中恢复寄存器和临时变量等函数运行场景。

2、多线程编程的基础是栈,栈是多线程编程的基石,每一个线程都最少有一个自己专属的栈,用来存储本线程运行时各个函数的临时变量和维系函数调用和函数返回时的函数调用关系和函数运行场景。操作系统最基本的功能是支持多线程编程,支持中断和异常处理,每个线程都有专属的栈,中断和异常处理也具有专属的栈,栈是操作系统多线程管理的基石。

栈溢出现象:是指在使用栈内存时,写入了超出栈内存分配范围的数据,通常会导致程序崩溃或产生不可预测的行为。

栈溢出的主要现象

①程序崩溃:当栈溢出时,程序可能会崩溃,并且在错误日志中显示堆栈溢出的消息。

②未定义的行为:栈溢出可能会导致程序进入未定义的状态,产生不可预测的行为,可能会导致程序错误、数据损坏等问题。

实例:

#include <stdio.h>


void recursiveFunction(int n) {
char buffer[1024]; // 定义一个大小为1024的局部数组 printf("Current stack frame at address: %p\n", &buffer); recursiveFunction(n + 1); // 递归调用自身
}


int main() {
recursiveFunction(1); // 初始调用递归函数
return 0;
}

解析:在实例中,recursiveFunction()是一个递归函数,每次调用时都会在栈上分配一个大小为1024字节的局部数组buffer。由于递归调用没有终止条件,因此会导致栈上的局部变量buffer不断增加,最终超出栈的容量,触发栈溢出

当程序运行时,每次递归调用recursiveFunction()函数时,都会打印当前栈帧的地址,直到栈溢出发生。当栈溢出发生时,程序可能会崩溃,并在错误日志中显示栈溢出的消息。
如何解决栈溢出问题,可以采取以下措施:
1、优化递归算法:如果使用递归算法导致栈溢出,可以尝试优化算法,使用迭代或非递归的方式实现。避免不必要的递归调用,减少栈的深度。
2、减少局部变量的使用:局部变量会占用栈内存,如果函数中定义了大量的局部变量,容易导致栈溢出。可以考虑减少局部变量的数量,或者将一些局部变量改为全局变量。
3、增大栈的大小:有些编译器允许设置栈的大小,可以通过调整编译器的参数或者在程序启动时设置栈大小来增大栈的容量。但是这种方法可能会导致内存浪费,并且不是一个通用的解决方案。
4、使用动态内存分配:将大量的数据存储在堆上而不是栈上,可以使用动态内存分配函数
(如malloc() calloc() 等)在堆上分配内存,从而避免栈溢出问题。但是要注意及时释放动态分配的内存,防止内存泄漏。
5、使用栈保护技术:一些操作系统和编译器提供了栈保护技术,如栈溢出检测、栈随机化等,可以在一定程度上防止栈溢出攻击。


4STM32有哪些基本外设以及如何使用

STM32微控制器具有丰富的外设,用于连接和控制各种外部设备。以下是一些常见的外设及其使用方法:
1. GPIO(通用输入输出):GPIO用于连接和控制外部设备的数字输入和输出。通过配置寄存器和引脚设置,可以将GPIO用于输入、输出、中断等功能。
2. UART(通用异步收发器):UART用于串行通信,可以与其他设备进行数据传输。通过配置串口时钟和数据位率,可以使用UART进行数据的发送和接收。
3. SPI(串行外围接口):SPI用于连接外部设备,如传感器、存储器和显示器等。通过配置时钟极性、相位和数据帧长度等参数,可以使用SPI进行全双工的串行数据传输。
4. I2C(两线制串行接口):I2C用于连接多个外设,通过总线共享数据。通过配置时钟速度、寄存器地址和数据传输等,可以使用I2C进行串行数据传输。
5. ADC(模数转换器):ADC用于将模拟信号转换为数字信号。通过配置采样时间、分辨率和触发模式等参数,可以使用ADC进行模拟信号的转换和采样。
6. PWM(脉冲宽度调制器):PWM用于产生可调节占空比的脉冲信号,用于控制电机、LED亮度调节和音频输出等。通过配置定时器和通道,可以使用PWM生成特定频率和占空比的脉冲信号。
7. Timers(定时器):定时器用于生成精确的时间延迟


5、熟悉UARTI2CSPI协议么,它们有什么异同点

I2C协议:
I²C(Inter-Integrated Circuit)是一种串行通信接口,用于在微控制器或其他集成电路之间进行通信。以下是I²C的运行方式及配置从机地址的方法:
物理连接:
I2C 使用两条线进行通信:
1. SDA(Serial Data):串行数据线,用于发送和接收数据。
2. SCL(Serial Clock):串行时钟线,用于同步数据传输。
通信方式:

  • I2C 是一种双向、半双工的通信协议,意味着主机和从机都可以发送和接收数据,但不能同时进行。
  • 主机负责发起通信和生成时钟信号,从机负责响应主机的请求。
    通信过程:

1. 起始条件(Start Condition):

  • 通信开始时,主机发送一个起始条件,即 SDA 从高电平变为低电平时,SCL 保持高电平。

2. 地址和读/写位传输:

  • 主机发送一个7位地址(包括一个从机地址和一个读/写位),告诉从机它要进行读取还是写入操作。
  • 从机在收到地址后会发送应答位,告知主机它是否存在并准备好进行通信。

3. 数据传输:

  • 一旦从机确认,主机和从机之间的数据传输就可以开始。主机发送数据到从机(如果是写入操作),或者从机发送数据给主机(如果是读取操
    作)。

4. 停止条件(Stop Condition):

  • 数据传输完成后,主机发送一个停止条件,即 SDA 从低电平变为高电平时,SCL 保持高电平。

数据格式:
数据通过 I2C 被分成一系列的字节,每个字节由一个起始位、8位数据位、一个可选的校验位(通常是一个奇偶校验位)和一个停止位组成。

从机地址的配置:

从机的地址通常由厂商预定义或者在从机硬件上配置。每个从机都有一个唯一的7位地址,范围从 0x00 0x7F(二进制形式为 0000 000 1111 111)。在 I2C 总线上,从机地址是静态的,不会动态更改。

I2C传输数据的格式:

1、写操作

刚开始主芯片要发出一个start信号,然后发出一个(用来确定是往哪一个芯片写数据),方向(读/写,0表示写,1表示读)。回应(用来确定这个设备是否存在),然后就可以传输数据,传输数据之后,要有一个回应信号(确定数据是否接收完成),然后再传输下一个数据。每传输一个数据,接收方都会有一个回应信号,数据发送完之后,主芯片就会发送一个停止信号

2、读操作

刚开始主芯片要发出一个start信号,然后发出一个设备地址(用来确定是从哪一个芯片读取数据),方向(读/写,0表示写,1表示读)。回应(用来确定这个设备是否存在),然后就可以传输数据,传输数据之后,要有一个回应信号(确定数据是否接受完成),然后在传输下一个数据。每传输一个数据,接收方都会有一个回应信号,数据发送完之后,主芯片就会发送一个停止信号

SPI协议:
SPI协议是由摩托罗拉公司提出的通讯协议,即串行外围设备接口,是一种高速全双工的通信总线。SPI总线系统是一种同步串行外设接口,它可以使MCU与各种外围设备以串行方式进行通信以交换信息。SPI总线可直接与各个厂家生产的多种标准外围器件相连,包括 FLASH、RAM、网络控制器、LCD显示驱动器、A/D转换器和MCU等。

1 主从架构

SPI通信通常以主从(Master-Slave)架构进行,其中一个设备作为主设备控制通信,而其他设备作为从设备响应主设备的指令

2 信号线

①SCLK(Serial Clock):时钟线,主设备通过它向从设备发送时钟信号,同步数据传输。

②MOSI(Master Out Slave In):主设备输出、从设备输入,主设备通过它向从设备发送数据。

③MISO(Master In Slave Out):从设备输出、主设备输入,从设备通过它向主设备发送数据。

④SS(Slave Select):从设备选择线,用于选择要与主设备通信的从设备,通常每个从设备都有一个对应的SS线。

I2C协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而SPI协议中没有设备地址,它使用NSS信号线来寻址,当主机要选择从设备时,把该从设备的NSS信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行SPI通讯。所以SPI通讯以NSS线置低电平为开始信号,以NSS线被拉高作为结束信号

3 数据传输方式

SPI通信是全双工的,意味着数据可以同时在两个方向上传输。通信开始时,主设备产生时钟信号(SCLK),在每个时钟周期上,主设备将一个比特数据发送到MOSI线上,并且同时接收来自从设备的数据(如果有)。4 传输过程

①、主机先将NSS信号拉低,这样保证开始接收数据;

②、当接收端检测到时钟边沿信号时,它将立即读取数据线上的信号,这样就得到了一位数据(1bit);

③、主机发送到从机时:主机产生相应的时钟信号,然后数据一位一位地将从MOSI信号线上进行发送到从机;

④、主机接收从机数据:如果从机需要将数据发送回主机,则主机将继续生成预定数量的时钟信号,并且从机会将数据通过MISO信号线发送;

5 SPI模式

在SPI协议中,有两个值来确定SPI的模式。

时钟极性(CPOL):表示SPICLK的初始电平,0为低电平,1为高电平

时钟相位(CPHA):表示相位,即第一个还是第二个时钟沿采样数据,0为第一个时钟沿,

1为第二个时钟沿

我们常用的是模式0和模式3,因为它们都是在上升沿采样数据,不用去在乎时钟的初始电平是什么,只要在上升沿采集数据就行。
CPOL初始电平:指它的初始电平
CPHA相位:指我们在采集数据是从一个时钟沿采集,还是从第二个时钟沿采集

相位0-->第一个时钟沿(上升沿或下降沿)

相位1-->第二个时钟沿

UART协议:

UART (Universal Asynchronous Receiver-Transmitter) 是一个常用于串行通信的硬件设备或协议。典型的串口通信使用3根线完成,分别是:发送线(TX)、接收线(RX)和地 线(GND)。它不需要时钟信号来同步数据传输,因此称为异步。以下是 UART 通信的主要特点和描述,特别是起始位和终止位:
1. 起始位 (Start Bit):当不传输数据时, UART 数据传输线通常保持高电压电平。 若要开始数据传输,发送UART 会将传输线从高电平拉到低电平并保持1 个时钟周期。当接收 UART 检测到高到低电压跃迁时,便开始以波特率对应的频率读取数据帧中的位。每次传输数据前都会先发送一个起始位,以标志一个新的数据帧的开始。通常,起始位是逻辑“0”
2. 数据位 (Data Bits):这是数据帧的主体部分,其中包含实际要传输的数据。UART
通信可以配置为5、6、7、8、9或10个数据位。通常,大多数应用使用8个数据
位。
3. 奇偶校验位 (Parity Bit):这是一个可选的位,用于错误检查。如果设置了奇偶校验,UART 会根据设置添加一个奇偶校验位,以确保数据帧中逻辑“1”的数量是奇数或偶数。
4. 停止位 (Stop Bits):为了表示数据包结束,发送 UART 将数据传输线从低电压驱动到高电压并保持1 到 2 位时间。在数据和任何奇偶校验位后,会发送一个或多个停止位,以标志数据帧的结束。停止位是逻辑“1”。常见的配置是1个或2个停止位。
5. 波特率 (Baud Rate):波特率定义了每秒传输的符号数。例如,9600波特率表示每秒传输9600个符号(这包括起始位、数据位、奇偶校验位和停止位)。
UART 通信中的发送设备和接收设备必须事先达成协议,以确保它们都使用相同的设置(如波特率、 数据位数、 奇偶校验和停止位数)。只有当两个设备的设置相同时,它们之间的通信才能正确进行。
起始位和终止位在 UART 通信中尤为重要,因为它们标志着数据帧的开始和结束。起始位 让接收器知道新的数据帧即将开始,而停止位则给予接收器时间来准备接收下一个数据帧。
1 异步通信
UART是一种异步通信协议,意味着发送端和接收端之间没有共享的时钟信号。相反,数据通过预先协商好的波特率(通常是固定的)进行传输,接收端根据波特率进行数据的同步和解析。
2 数据帧结构
UART通信的数据帧结构包括以下几个部分:
①起始位(Start Bit):表示数据帧的开始,通常为逻辑低电平(0)。
②数据位:包含实际的数据,通常为8位,但也可能是其他值。

③可选的奇偶校验位:用于数据的完整性检查,以确保数据的准确性。

④停止位(Stop Bit):表示数据帧的结束,通常为逻辑高电平(1)。3 波特率

波特率是指每秒钟传输的比特数,它决定了数据传输的速率。在UART通信中,发送端和接收端必须使用相同的波特率进行通信,以确保数据的正确传输。常见的波特率包括9600、 19200、38400等。

4 数据传输流程

在UART通信中,发送端将数据按照数据帧结构逐位发送到传输线上,接收端通过接收这些位并重新组装成完整的字节来接收数据。接收端需要根据起始位、数据位、校验位和停止位的设备来解析数据。

⭐在STM32使用UART通信时,通常会制定通信协议,确保数据传输的准确性

异同点:


6、中断函数可不可以有参数、返回值,在中断中可以进行printf函数打印么?

中断服务函数应该注意的四大点:

  • 1.中断服务函数不能传入参数
  • 2.中断服务函数不能有返回值
  • 3.中断服务函数应做到短小精悍
  • 4.不推荐在中断函数中使用printf函数,printf函数可能会引入较大的代码体积和运行时开销,而在中断服务程序中不应该执行过多的复杂操作,以免影响实时性和系统性能。

解析:首先,中断源连接到硬件,由硬件来触发产生中断,即:中断源提出中断申请,而中断申请一般由硬件电路产生,申请提出时间是随机的,因此中断的产生是随机的,也就是 说,中断服务函数的调用是硬件级别的。当中断产生时,pc指针强制跳转到相应的中断服务函数入口执行中断服务函数。

1) 关于返回值,需要进行入栈出栈操作。由于中断是随机的,且由硬件告知,并不是由某段代码调用,所以,如果有返回值那么返回值将返回给谁?显然,这是毫无意义的!也就是说,如果中断服务函数有返回值,返回时将返回值压入栈中后,那么何时出栈、如何出栈?

2) 关于传入参数,这和返回值类似,也需要入栈出栈,那么什么时候入栈,谁给它传递参数?

3) 至于ISR要短小精悍就更好理解了。假设存在一个中断,其中断产生的次数较为频繁,而它对应ISR耗时较长,那么对于中断的响应就会无限的延迟,从而会错过很多中断请求;

4) 关于printf函数带来的重入性和性能问题,需要了解像printf这样的glibc函数,其采用的是缓冲机制,该缓冲区域是共享的,可以理解为全局变量。当某个中断发生时,向缓冲区中写入某些内容,而恰巧此时来了一个更高级的中断,它同样调用了printf函数,也向缓冲区中写入某些内容,此时,缓冲区中的内容就混乱了。

 _interrupt double compute_area (double radius)
{

double area = PI * radius * radius; printf("/nArea = %f", area);
return area;

}

解析:

第一行,中断函数传入参数了;第二行,在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额外的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算;第三行,使用printf()函数带来了重入性能问题;第四行,ISR不能有返回值;整体上,ISR应该时短小精悍的,所以在ISR中做浮点运算是不明智的。

友情链接: