C语言
static 关键字
- 用途:
- 限制函数、变量的作用域
- 延续变量的生命周期
- 对局部变量的作用:
- 将局部变量的存储区域由栈变为静态区,生命周期变为程序结束时终止,但不改变变量的作用域。
- 对全局变量的作用:
- 将全局变量的作用域由整个程序变为当前文件,变量的生命周期不变。
- 对函数的作用:
- 将函数的作用域从整个程序变为当前文件。
volatile 关键字
- 用途:防止编译器去过度优化。每次读取数据都是从内存中读取,而不是从寄存器或缓存中读取变量的值。
- 使用场景:
- 在嵌入式中,操作并行设备的硬件寄存器和存储器映射的硬件寄存器。
- 在 ISR 中,变量需要加 volatile(编译器可能认为主函数没有修改,该变量就从内存或 cache 读副本)。
- 多任务环境下各任务间共享的变量。
位操作
#define BIT3 (0x1 << 3)
static int a = 0;
void setBit3(void){
a |= BIT3;
}
void clearBit3(void){
a &= ~BIT3;
}
函数指针与指针函数
- 函数指针是指返回指针类型的函数,使用场景:
- 用于实现多态、回调、封装等等。
- 用来优化代码结构。
- 指针函数是指向函数的指针,使用场景:
- 动态分配内存,返回一个指向整型数组的指针。
- 方便对字符串操作。
- 方便对数据结构操作,例如链表、树。
静态内存与动态内存
- 静态内存是指在程序开始时由编译器分配的内存,不占用 CPU 资源,在 stack 上分配。
- 动态内存是运行中分配的,释放分配都会占用 CPU,在 heap 上分配。
七、内存溢出与内存泄漏
内存溢出与内存泄漏
内存溢出是指程序在运行过程中,因为申请内存超过了系统所能提供的范围或者申请的内存没有及时释放,导致内存不足,无法为其他应用程序提供服务。
可能的原因有:
- 程序中存在死循环或递归调用,导致栈区不断增加,直到超出系统限制;
- 程序中使用大量的全局变量或静态变量,导致全局区占用过多的内存;
- 程序中动态申请了大量的堆内存,但没有及时释放或者释放不完全,导致堆区不断扩展,直到耗尽系统资源。
内存泄漏是指程序在申请内存后,无法释放已经不再使用的内存空间,导致被占用的内存越来越多,影响程序的性能和稳定性,可以用valgrind检测内存泄漏。
sizeof与strlen
sizeof是操作符 strlen是库函数
char a[] = "12345 54321";
char *b = "12345 54321";
char c[20] = "12345 54321";
Output:
a[] | strlen | 11 |
a[] | Sizeof | 12 |
*b | strlen | 11 |
*b | sizeof | 4 |
c[20] | strlen | 11 |
c[20] | sizeof | 20 |
值传递与引用传递
#include <stdio.h>
#include <stdlib.h>
void fun(int *p1, int *p2, int *s) {
s = (int *)malloc(sizeof(int));
*s = *p1 + *(p2++);
}
int main() {
int a[2] = {1, 2}, b[2] = {10, 20}, *s = a;
fun(a, b, s);
printf("%d \n", *s);
}
答案是:1
这是值传参,s传进去之后产生一个s的拷贝,属于局部变量,里面存有原来的指针地址,
让这个局部变量s指向新的地址并不会改变原来的外面的s,
所以原来的s指向的地址不变,而里面也没有对原来的s指向的内容进行修改,自然内容也不变。
指针与括号
int main()
{
int arr[5]={1,3,5,7,9};
int* p = arr; // 每句话都要初始化
printf("%d \n",*++p); // Output:3
p = arr;
printf("%d \n",++*p); // Output:2
p = arr;
printf("%d \n",*p++); // Output:2
p = arr;
printf("%d \n",(*p)++); // Output:2
p = arr;
printf("%d \n",*(p++)); // Output:3
}
char * 与 char []
int main()
{
char *a= "abcedfg"; // 相等 char * b; b = "abcedfg";
printf("%c \n",*a); //*a 输出第一个字符
printf("%c \n",*(a+1)); //*(a+1) 输出第二个字符
printf("%s \n",a); //a 输出整个字符串
char b[10]="abcedfg"; // 相等 a[0]='a' ....... 报错 b = "abcedfg";
printf("%c \n",*b); //*a 输出第一个字符
printf("%c \n",*(b+1)); //*(a+1) 输出第二个字符
printf("%s \n",b); //a 输出整个字符
return 0;
}
/*
Output:
a
b
abcedfg
a
b
Abcedfg
*/
'\0' 与NULL
'\0' 主要应用场景字符串的结束标志,帮助程序识别字符串的边界,以便进行正确的处理和操作
NULL可以用于指针的初始化,也可以用来帮我们解决野指针的问题
enum
red默认为0,green为1,black为-2,blue为-1,pink为0
C语言小题目
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6) ? puts("> 6") : puts("<= 6");
}
有符号型会转变为无符号型 output: > 6
设char型变量中x中的值为10100111,则表达式(2+x)^(~3)的值是()
X+2 = 10101001 ~3 = ~(0000 0011)
1010 1001 ^ 1111 1100 = 0101 0101
GCC编译器编译的四个流程
gcc –E hello.c –o hello.i 预处理
gcc –S hello.i –o hello.s 编译
gcc –c hello.s –o hello.o 汇编
gcc hello.o –o hello 链接
Preprocesing -> compilation -> Assemble ->Linking
通信协议
UART
UART是一种异步、全双工的串行通信协议,需要2根线(RX和TX)进行数据传输。
数据帧格式包括起始位、数据位、奇偶校验位和停止位,从低位开始传输。
IIC
IIC是一种同步、半双工、主从式接口。需要2根线(SDA和SCL)进行数据传输。
ISCL与SDA都必须是开漏输出+上拉电阻 (空闲时候输出高电平,开漏输出无法主动输出高电平),上拉电阻一般都是1K欧,具体取决电源,上拉电阻的作用是提高IO的驱动能力,使得IO能够输出高电平。
IIC开漏输出目的
实现线与能够多个设备通信,如果不用开漏输出,在多个主设备和多个从设备在一条总线上时候,会出现主设备之间的短路情况。
单片机的推挽输出和开漏输出有什么区别?
开漏输出只能输出低电平,如果输出高电平需要外接上拉电阻
推挽输出既能输出高电平,又能输出低电平
IIC总线仲裁
IIC理论可以搭载多少从设备
1.按照7位模式,最多可以支持127个从设备,10位模式最多可以支持1023个从设备,挂载从设备数量会受到总线电容影响
IIC时序
IIC写入一个字节
IIC读取一个字节
空闲状态
SDA = 1 SCL = 1
起始状态 / 停止状态
IIC数据有效性
在时钟的低电平期间可以改变数据,高电平保持数据
I2C通讯速率有几种?
- 在标准模式下,I2C的传输速率为100Kbps
- 在快速模式下,I2C的传输速率为400Kbps
- 在高速模式下,I2C的传输速率为3.4Mbps
SPI
SPI是一种同步、全双工、主从式接口。需要4根线(CS,MOSI,MISO,CLK)进行数据传输。
- 数据传输
要开始SPI通信,主机必须发送时钟信号,并通过 使能CS信号选择从机 。片选通常是低电平有效信号。因此,主机必须在该信号上发送逻辑0以选择从机。
- 时钟极性和时钟相位
时钟极性(CPOL):表示空闲时是高电平为1,低电平为0
时钟相位(CPHA):表示从第一个跳变开始采样为0,从第二个跳变开始采样为1
SPI有4种工作模式,主要根据时钟极性(CPOL)和时钟相位(CPHA)的不同组合而定义:
- 模式0(CPOL = 0,CPHA = 0):时钟空闲状态为低电平,数据在第一个时钟沿上采样。
- 模式1(CPOL = 0,CPHA = 1):时钟空闲状态为低电平,数据在第二个时钟沿上采样。
- 模式2(CPOL = 1,CPHA = 0):时钟空闲状态为高电平,数据在第一个时钟沿上采样。
- 模式3(CPOL = 1,CPHA = 1):时钟空闲状态为高电平,数据在第二个时钟沿上采样。
模式0:
主机发送0xA5会收到从机发来的0xBA,主机会自动忽略掉0xBA
阻抗匹配的电阻大小是33欧
SPI的传输速率?
SPI没有固定的传输速率,根据实际元器件决定的
IIC与SPI的区别?
总线结构:SPI是一种同步的、全双工的协议,需要4或更多个导线来建立通信。典型的SPI包括一个主设备和一个或多个从设备,主设备通过时钟信号和片选信号与从设备进行通信。而I2C是一种双线制、半双工的协议,仅需要两根线来传输数据:一根是串行数据线(SDA),另一根是串行时钟线(SCL)。在I2C总线上,可以有多个主设备和多个从设备。
传输速率:SPI的传输速率通常比I2C高,可以达到几百MHz。相比之下,I2C的传输速率较低,一般在几十Kbps到几百Kbps之间。
硬件成本:由于SPI需要较多的引脚和硬件资源来建立连接,通常占用更多的硬件资源。相对而言,I2C只需要两根线,并且可以通过电平转换器等简单的外部电路与MCU连接。
CAN通信
CAN通信是异步半双工通信,使用差分信号进行数据传输。根据CAN_H和CAN_L上的电位差来判断总线电平,分为显性电平(逻辑0)和隐性电平(逻辑1)。CAN有低速和高速两种通信速率,低速通信速率范围为10k-125kbps,高速通信速率范围为125kbps-1Mbps
终端电阻
低速CAN总线在CANH和CANL上分别串接2.2kΩ的电阻
高速CAN总线在CANH和CANL之间串接120Ω的电阻
数据链路层
CAN-bus规定了5种通信帧——数据帧、远程帧、错误帧、过载帧、帧间隔
数据帧又分成标准帧和扩展帧,主要体现在仲裁段和控制段
数据帧由帧起始 + 仲裁段 + 控制段 + 数据段 + crc段 + ack段 + 帧结束
CAN仲裁
CAN驱动
波特率=预分频系数×(1+BS1时间量子数+BS2时间量子数)APB1总线频率
RS232和RS485:
- 传输方式:RS485采用差分传输方式,而RS232采用单端传输方式。
- 信号线:RS485组成的半双工网络,一般只需二根信号线。RS-232口一般只使用RXD、TXD、GND三条线。
- 抗干扰性:RS485接口是采用平衡驱动器和差分接收器的组合,抗噪声干扰性好。RS232接口使用一根信号线和一根信号返回线而构成共地的传输形式,这种共地传输容易产生共模干扰。
- 传输距离:RS485接口的最大传输距离标准值为1200米(9600bps时),实际上可达3000米。RS232传输距离有限,最大传输距离标准值为50米,实际上也只能用在15米左右。
- 通信能力:RS485接口在总线上是允许连接多达128个收发器,用户可以利用单一的RS485接口方便地建立起设备网络。RS232只允许一对一通信。
- 传输速率:RS232传输速率较低,在异步传输时,波特率为20Kbps。RS485的数据最高传输速率为10Mbps。
- 电气电平值:RS485的逻辑“1”以两线间的电压差为+(2-6)V表示;逻辑“0”以两线间的电压差为-(2-6)V表示。在RS-232中任何一条信号线的电压均为负逻辑关系。即:逻辑“1”,-(5-15)V;逻辑“0 “ +(5- 15)V 。
计算机组成
大小端
小端模式就是数据的低位存放内存低位,数据高位存放在内存高位
大端模式就是数据的低位存放内存高位,数据高位存放在内存低位
32位数据 0x12345678
0x8000H 0x8001H 0x8002H 0x8003H
小端模式 78 56 34 12
大端模式 12 34 56 78
示例代码:
int d = 0x12345678;
union checkBigorSmall
{
int a;
char b;
};
int main(){
union checkBigorSmall c;
c.a = d;
(c.b == 0x78) ? printf("small") : printf("big");
return 0;
}
c语言内存分配
栈区 | RAM |
堆区 | |
静态区(全局区) | |
常量区 | ROM |
代码区 |
-
栈区:
- 临时创建的局部变量和const定义的局部变量存放在此区域。
- 函数调用和返回时,其入口参数和返回值存放在此区域。
-
堆区:
- 存放程序运行过程中动态分配出来的内存。
-
全局区:
- 分成.bss区和.data区。
-
.bss区:
- 未初始化的全局变量和未初始化的静态变量存放在此段。
- 初始化为0的全局变量和初始化为0的静态变量也存放在此段。
-
.data区:
- 已初始化的全局变量存放在此段。
- 已初始化的静态变量也存放在此段。
-
常量区:
- 字符串、数字等常量存放在此区域。
- const修饰的全局变量也存放在此区域。
-
代码区:
- 程序执行代码存放在此区域,其值不能修改(若修改则会出现错误)。
- 字符串常量和define定义的常量也有可能存放在此区域。
中断
使用__interrupt定义一个ISR,评论代码有什么不妥
__interrupt double compute_area (double radius)
{
double area = PI * radius * radius;
printf(" Area = %f", area);
return area;
}
- ISR不能有返回值。
- ISR不能传递参数。
- ISR应该是短而高效的。
- 不建议在ISR中进行浮点运算。
- printf()经常有重入和性能方面上的
RTOS
需要了解线程互斥,线程通信,信号量,内核知识,单片机启动等等基本概念与知识,直接看到rtthread文档中心
MCU
STM32启动流程
(1) 上电复位,硬件设置 SP、PC 的值 (0x0800 0000读取sp的值 0x0800 0004读取pc的值)
(2) 找到了 Reset_Handler 的地址后,CPU 就从这里开始取指令运行程序;
(3)Reset_Handler调用SystemInit完成时钟、中断向量偏移的初始化工作,然后跳转到__main,__main函数会完成RW、ZI数据段的重定位工作,即将ROM中的RW数据拷贝到RAM中,将ZI段清零,然后跳转到_rt_entry进行Stack和Heap的初始化。
ROM占用大小等于RO SIZE + RW SIZE
RO SIZE 等于 code + RO data
RW SIZE 等于 RW data + ZI data
RW data 是初始化不为0的全局变量和静态变量
ZI data 是没初始化/初始化为0的全局变量和静态变量 + 堆区(0.5kb) + 栈区(1kb)
STM32中startup.s文件
- 设置栈的大小 默认为1kb
- 设置堆的大小,默认为0.5kb
- 设置中断向量表
- 复位程序
- 中断服务函数
- 堆栈初始化
定义了一个名为HEAP的内存区域,大小为0x00000200字节(512字节),并且没有初始化,可读可写,8字节对齐。然后定义了两个符号__heap_base和__heap_limit,分别表示堆的起始地址和结束地址。最后分配了堆大小的空间给Heap_Mem。
定义了两个符号__stack_limit和__initial_sp,分别表示堆栈的起始地址和结束地址。最后分配了堆栈大小的空间给Stack_Mem。
RAM起始地址 0x2000 0000 ROM起始地址0x0800 0000
ARM R13 R14 R15分别是什么?
R13属于是SP寄存器 R14属于是LR寄存器 R15属于是PC寄存器
STM32有几个时钟源?
STM32有五个时钟源:HSI,HSE,LSI,LSE,PLL
①、HSI是高速内部时钟,RC振荡器,频率为8MHz,上电后默认的系统时时钟 SYSCLK = 8MHz,Flash编程时钟。
①、HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~16MHz。
③、LSI是低速内部时钟,RC振荡器,频率为40kHz,可用于独立看门狗IWDG、实时时钟RTC。
④、LSE是低速外部时钟,接频率为32.768kHz的石英晶体。
HSE:外部晶振STM32F103C8T6默认是8Mhz (4Mhz~16Mhz)
HSI内部最大是64Mhz,一般使用外部晶振HSE,PLL设置为9,分频设置为1,8Mhz*9=72Mhz
中断流程
中断请求-中断响应-保护断点-中断服务-中断返回
异常和中断
异常是可以程序控制的事件。当异常发生时,处理器不会执行程序,而是暂停当前任务,并转而执行一段名为异常处理的程序代码。在异常处理完成后,处理器会继续执行之前的任务。
异常的种类很多,中断是异常的一个子集。
Cortex M3内核支持256个中断,支持256优先级。(厂商会进行剪裁)
在stm32f103中有10个内核中断和60个外部中断,中断优先级有16个。(其他有空缺)
stm32将中断优先级分成5个组,分别为组0-4,分成抢占优先级和响应优先级(还有自然优先级)
DMA定义:
DMA用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU的干预,通过DMA数据可以快速移动。
DMA传输方式:
外设到内存
内存到外设
内存到内存
外设到外设
DMA流程
1.外设对DMA控制器发出请求信号
2.DMA控制器收到请求后,发送一个应答信号,触发DMA工作
3.DMA控制器从AHB总线获取ADC采集数据,存储DMA通道中
4.DMA控制器的DMA总线与总线矩阵协调,使用AHB把外设ADC采集的数据经由DMA通道存放到SRAM中,这个数据的传输过程中,完全不需要内核的参与,也就是不需要CPU的参与。
单片机死机、跑飞的原因是什么?
软件导致单片机死机的原因
1、指针异常
指针未初始化或者野指针导致正常数据被篡改。如果程序区被修改,会导致程序直接跑飞;如果数据区被修改,会导致数据异常引起程序运行错误。
2、缓冲区溢出
实际接受的数据超过了缓冲区长度,导致后续正常数据被篡改。
或者操作数组的时候下标溢出
3、等待标志位
没有增加超时判断,正常情况下很快就能出来,但实际运行时标志位一直满足while条件,导致程序一直死循环等待标志位。处理方法是增加超时判断,超过一定时间主动报错退出。
4、堆栈溢出
常见于容量小的单片机,重复中断、函数调用导致超出堆栈空间,正常数据被改写。该问题最难查,有一定特殊性,很难稳定复现
5、中断异常
打开了某个中断但是没有编写中断响应函数导致进入fault,或者没有清除中断标志导致重复进入中断。
硬件导致单片机死机的原因
1、电源不稳定
主要表现为纹波过大、电压过高、过低
2、晶振失振
晶振电路设计有问题,导致温度变化后,晶振失振
3、外部干扰
外部干扰导致导线上电平变化,或者直接导致单片机内部模块运行异常。常见于一些干扰较为严重的场合,可通过电磁干扰性的实验复现。现象是在特定环境下容易出现,实验室条件下很难复现。
海兴电力固件面试
你的计量芯片参考电压是多少?
-
1.25V
-
典型场景:低功耗设计、电池供电设备(如电能计量芯片、便携式仪表)。
-
优势:低电压可降低芯片功耗,适合对功耗敏感的应用。
-
-
2.5V
-
典型场景:工业测量、高精度ADC/DAC(如电能表、传感器信号调理)。
-
优势:较高的电压可提升信噪比(SNR),减少噪声影响。
-
你如何处理大量串口数据?
我们公司方案:
1.“中断触发接收 + 队列缓冲 + Polling + 数据解析”
中断触发接收:每当UART接收到一个字节时,触发中断服务程序(ISR),在ISR中将该字节添加到队列中。
队列缓冲:使用环形缓冲区来缓存接收到的数据,确保数据不会丢失,并提供线程安全的操作。
轮询(Polling):在主循环中定期检查队列,查看是否有新的数据需要处理。
数据解析:从队列中取出数据后进行协议解析和业务逻辑处理。
面试官应该想听到的:
2.DMA + 中断 +队列缓冲 + Polling + 数据解析
串口超时机制
RS485是一种物理层的串行通信标准,常用于工业设备间的数据传输。它的通信是基于数据帧的,但没有内置的协议层(如TCP的可靠性保证),因此超时机制需要手动实现:
数据包超时(Packet Timeout)
-
场景:设备以固定频率发送数据包(例如每20ms一包),但接收方无法预知一包数据的准确结束位置。
-
解决方法:
如果在5ms
内没有收到新数据,则认为当前包接收完成。例如:-
使用定时器,每次收到一个字节时重置计时器。
-
若超时未收到新字节,触发回调函数处理已接收的数据。
-
数据帧超时(Frame Timeout)
-
场景:协议规定帧长100字节,但只收到98字节后通信中断。
-
解决方法:
设置一个更大的超时(如10ms
),若在超时时间内未收齐完整帧,则丢弃或重发请求。
实现方式
-
中断+DMA:通过硬件DMA自动接收数据,用定时器判断超时。
-
纯中断:每个字节触发中断,软件维护超时计时器。
厦门佳因特
c语言字符串用什么结束?
c语言字符串用'\0'结束。在数组里面判断一个字符串,如果对方字符串发送没有用'\0',在获取strlen的长度和使用memcpy时候可能会死机。
声明和定义的区别
声明是告诉编译器某个变量或函数的存在及其类型信息,但并不分配内存空间。它的主要目的是让编译器知道如何使用这个变量或函数,而不需要知道其具体实现细节。
定义不仅包含了声明的所有功能,还会为变量或函数分配实际的存储空间。对于变量来说,这意味着为其分配内存;对于函数来说,这意味着提供其实现代码。