网上搜罗的,侵删
1关键字
字符串颠倒 字符串闭合
数组出现次数大于某个值的元素
链表
项目经历,项目细节 技术深度,基础能力
C语言
volatile
volatile关键字是一个类型修饰符,用于告诉编译器该变量的值可能会在程序的控制或监视之外被改变。这意味着编译器在优化代码时,不能假设这个变量的值在两次读取之间不会改变,因此每次访问该变量时都必须直接从内存中读取其值,而不是使用可能已经存储在寄存器中的值(这是编译器为了提高访问速度而常用的优化手段)。
如果没有使用volatile,编译器可能会优化掉对变量的多次读取,因为它认为在两次读取之间变量的值不会改变。
1. 内存映射的硬件寄存器访问:在嵌入式系统编程中,硬件寄存器通常通过特定的内存地址访问。这些寄存器的值可能会因为硬件的操作或外部事件(如中断)而改变,因此必须使用volatile来确保每次访问都直接从硬件读取。
2. 多线程或中断服务程序中的共享变量:在多线程程序中,或者在使用中断服务程序(ISR)时,一个变量可能在一个线程或ISR中被修改,而在另一个线程或主程序中被读取。在这种情况下,使用volatile可以确保每次读取都能获取到最新的值。但请注意,volatile并不保证操作的原子性或内存访问的顺序,对于复杂的同步问题,可能需要使用更高级的同步机制(如互斥锁)。
3. 信号量或其他同步机制:虽然对于高级的同步机制(如信号量),通常会使用专门的库或操作系统功能来管理,但在某些情况下,可能需要手动实现或使用简单的变量作为标志。在这些情况下,volatile可以确保这些变量的值被正确读取
static的用法(定义和用途)
- 用static修饰局部变量:使其变为静态存储方式(静态数据区),那么这个局部变量在函数执行完成之后不会被释放,而是继续保留在内存中。
- 用static修饰全局变量:使其只在本文件内部有效,而其他文件不可连接或引用该变量。
- 用static修饰函数:对函数的连接方式产生影响,使得函数只在本文件内部有效,对其他文件是不可见的(这一点在大工程中很重要很重要,避免很多麻烦,很常见)。这样的函数又叫作静态函数。使用 静态函数的好处是,不用担心与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。
extern
声明一个定义在外部文件的变量(引入)。如果extern声明全局变量,编译器会自动到本工程其他.c源文件引入该变量; extern也可以引入外部定义的函数,相当于包含对应的头文件。(引入外部变量、外部函数)
const
const修饰的变量,只能在初始化时赋初始值,后面就不能通过该变量来修改,防止变量被意外的修改
const离谁近,谁就不能被修改
const int *ConstPtr = &Value; //常量指针,不能*ConstPtr改变值
int *const PtrConst = &Value; //指针常量,不能PtrConst改变地址
typedef
使用typedef关键字为复杂的数据类型另外定义一个简单的别名
typedef int int32_t
typedef与#define的区别
- #define是预处理指令,预处理阶段,typedef是关键字,在编译阶段
- #define是替换,typedef是别名
union
①结构体(struct)中所有变量是“共存”的——优点是“有容乃大”,全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。
②而联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”;但优点是内存使用更为精细灵活,也节省了内存空间。
联合体允许你在相同的内存位置存储不同的数据类型,但一次只能使用其中一种类型。这意味着联合体的所有成员共享同一块内存空间,因此联合体的大小是其最大成员的大小(也考虑到对齐和填充)。
浮点数-float/double
单精度浮点数(float型)在32位计算机中存储占用4字节,有效位数为7位,小数点后6位。
双精度浮点数(double型)在32位计算机中存储占用8字节,有效位数为16位,小数点后15位。
变量声明和定义的区别:
变量声明只是变量的说明,在程序中不需要为其分配内存空间;而变量定义是为变量分配内存空间并初始化
程序编译过程
预处理(Preprocessing) 展开头文件/宏替换/去掉注释/条件编译(test.i main .i),当源代码中包含#include <stdio.h>时,预处理器会将stdio.h头文件中的内容插入到源代码中,使得编译器能够识别并处理stdio.h中定义的函数和宏
gcc -E test.c -o test.i
参数E是告诉gcc命令只进行预编译,不做其他处理,参数o是用来指明输出的文件名为test.i
编译(Compilation) 检查语法,生成汇编 ( test.s main .s)
gcc -S test.i -o test.s
汇编(Assembly) 汇编代码转换机器码(test.o main.o)。
gcc -c test.s -o test.o
链接(Linking) 将多个目标文件及所需要的库连接成最终的可执行目标文件
gcc test.o -o test
C语言编译后的内存分布
栈: 由编译器进行管理,自动分配和释放,用于存放函数调用过程中的各种参数、局部变量、返回值以及函数的返回地址。
堆: 用于程序动态申请分配和释放空间
(C中的malloc()和free(),C++中的new和delete均是在堆中进行的。)一般情况下,程序员在编写程序时应该将申请的空间在使用后释放,若没有主动释放空间,则在程序运行结束时系统自动回收释放。
全局(静态)存储区:分为DATA段和BSS段,该区域的生命周期为整个程序,即从程序第一行开始到main函数结束。
该区域在程序运行结束时,自动释放内存空间。
- BSS段(全局未初始化区):未初始化或初始化为0的全局变量和静态变量,具体体现为 一个占位符,不占用.exe文件空间,这个占位符告诉操作系统或链接器在程序加载到内存时,需要为这些变量分配多少空间,并确保这些空间被初始化为0。这也就是为什么说声明不需要分配内存地址,定义才需要
- DATA段(全局初始化区):存放初始化后的全局变量和静态变量。
内存泄漏
静态区的创建和消失是在什么时候
静态区的创建和消失是在程序启动时创建,在程序结束时销毁
静态区是在程序运行时由操作系统分配的一块内存空间,它的创建和消失与程序的生命周期有关。静态局部变量在程序运行时只会被初始化一次,它们的内存空间会一直存在直到程序结束。因此,。
连续调用同一个函数两次,它的局部变量初始化结果是否会一致?
每次函数调用时,局部变量的内存地址都是全新的
函数中的局部变量没有被显式地初始化,那么它们的值将是未定义的,可能会导致不同的结果。
函数中的局部变量被显式地初始化为相同的值,那么它们的值将是一致的。
真随机数还是个伪随机数?
在C语言中,如果你需要生成伪随机数,你应该使用标准库中的随机数生成函数,如rand()函数,该函数定义在<stdlib.h>头文件中。rand()函数返回一个伪随机数,每次调用时通常返回不同的值(但序列是可预测的,除非你改变随机数生成的种子)。
使用rand()函数时,通常会先调用srand()函数来设置随机数生成的种子。种子是一个起点值,用于随机数生成算法的初始化。如果你每次程序运行时都使用相同的种子值(例如,总是调用srand(1)),那么rand()函数将生成相同的随机数序列。为了每次程序运行时都得到不同的随机数序列,你可以使用当前时间(通过time()函数获取,定义在<time.h>中)作为种子。
2.1 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
#define SEC_YEAR (365*24*60*60)UL
2.2 写一个“标准”宏MIN ,这个宏输入两个参数并返回较小的一个
#define MIN(a,b) ((a)<=(b)?(a):(b))
2.3 预处理器标识#error的目的是什么?
编译程序时,只要遇到 #error 就会跳出一个编译错误
2.4 嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢?
while(1){}
for(;;){}
loop:... goto loop;
2.5 用变量a给出下面的定义
1、一个整型数(An integer)
int a;
2、一个指向整型数的指针( A pointer to an integer)
int *a;
3、一个指向指针的的指针,它指向的指针是指向一个整型数( A pointer to a pointer to an intege)
int **a;
4、一个有10个整型数的数组( An array of 10 integers)
int a[10]
5、一个有10个指针的数组,该指针是指向一个整型数的。(An array of 10 pointers to integers)
int *a[10]
6、一个指向有10个整型数数组的指针( A pointer to an array of 10 integers)
int (*a)[10]
7、一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)
int (*a)(int a)
8、一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数 ( An array of ten pointers to functions that take an integer argument and return an integer )
int (*a[10])(int)
2.9 野指针
野指针:指向已被释放或无效的内存地址的指针
产生野指针的情况:
- 内存释放后未置空指针
int *ptr = (int*)malloc(sizeof(int)); free(ptr); *ptr = 10; // 这里ptr成为了野指针
- 返回局部变量的指针
int* getIntPointer() {
int num = 5;
return # // 返回局部变量的指针
}
int *ptr = getIntPointer();
*ptr = 10; // getIntPointer返回一个野指针
- 未初始化的指针
int* a
2.11 栈和队列的区别
- 栈是先进后出的数据结构,队列是先进先出的数据结构
- 栈用于表达式求值、函数调用的调用栈、括号匹配等需要后进先出的场景。队列常用于任务调度、缓冲区管理、消息传递等需要先进先出的场景。
2.14 如何判断链表是否有环
使用快慢指针
- 创建两个指针(快慢指针),初始时都指向链表的头节点
- 快指针每次移动两个节点,慢指针每次移动一个节点
- 如果存在环,快慢指针最终会相遇
- 如果不存在环,快指针会优先到达链表尾部
2.16 冒泡排序
bubble sort 是一种简单直观的排序算法,属于比较算法的一种。通过多次比较将较大(或较小)的元素逐渐浮出来
void bubbleSort(int* array, int size)
{
for (int i = 0; i < size - 1; i++)
{
for (int j = 0; j < size - i - 1; j++)
{
// 比较相邻的两个元素,如果顺序错误就交换它们
if (array[j] > array[j + 1])
{
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
2.17 选择排序
selection sort 属于比较算法的一种,基本思想是从未排序部分的数组中选择最小(或最大)的元素,然后将其放置在已排序部分的末尾,不断重复这个过程,直到整个数组排序完成。
void selectionSort(int* array, int size)
{
for (int i = 0; i < size - 1; i++)
{
int minIndex = i;
for (int j = i + 1; j < size; j++)
{
// 寻找未排序部分的最小元素的索引
if (array[j] < array[minIndex])
{
minIndex = j;
}
}
// 将最小元素与当前位置交换
int temp = array[i];
array[i] = array[minIndex];
array[minIndex] = temp;
}
}
2.18 插入排序
Insertion Sort 属于比较排序算法的一种。它的基本思想是将数组分为已排序部分和未排序部分,初始时已排序部分包含第一个元素,然后逐步将未排序部分的元素插入到已排序部分的合适位置,直到整个数组排序完成。
void insertionSort(int arr[], int n) {
int i, key, j;
for (i = 1; i < n; i++) {
key = arr[i]; // 当前需要排序的元素
j = i - 1;
// 将当前元素与其之前的元素依次比较,如果之前的元素更大,则将之前的元素向后移动一位
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key; // 插入正确的位置
}
}
2.19 大小端
大小端(Endian)指的是在计算机存储和处理多字节数据时,字节的存储顺序
大端序(Big Endian): 高位字节存储在低地址,低位字节存储在高地址。类似于将数字的高位放在左边,低位放在右边的表示方式。例如整数值 0x12345678 在内存中按照大端序排列为 12 34 56 78。
小端序(Little Endian): 低位字节存储在低地址,高位字节存储在高地址。类似于将数字的低位放在左边,高位放在右边的表示方式。例如整数值 0x12345678 在内存中按照小端序排列为 78 56 34 12。
计算机知识
RISC和CISC区别
这两个是描述处理器(CPU)指令集特性的
RISC(Reduced Instruction Set Computing,精简指令集计算机):强调增强指令的能力、减少目标代码的数量,但指令复杂且指令周期长。ARM、MIPS架构的计算机
CISC(Complex Instruction Set Computing,复杂指令集计算机):RISC强调尽量减少指令集、指令单周期执行,但目标代码会更大。X86架构的计算机
共性:
执行存储在内存中的指令来处理数据。
- 指令执行:无论是复杂指令还是精简指令,最终都需要被CPU解析并执行。
- 数据处理:对存储在内存或寄存器中的数据进行运算和处理。
- 程序控制:支持程序的顺序执行、循环、条件跳转等控制流程。
区别:
1. 指令集复杂度
- RISC:指令集相对简单,每条指令通常只执行一项基本操作,如加法、减法、逻辑运算等。指令长度固定,通常为固定的字节数(如4字节)。
- CISC:指令集较为复杂,每条指令能完成多项操作,如乘法、除法、字符串处理等。指令长度可变,根据不同的指令和操作数数量,长度可能从1到多个字节不等。
2. 指令执行效率 - RISC:由于指令简单且固定长度,RISC处理器能够更快地解析和执行指令。此外,RISC处理器通常采用流水线技术,能够并行处理多条指令,从而提高执行效率。
- CISC:由于指令复杂且长度可变,CISC处理器的指令解析和执行速度相对较慢。然而,CISC处理器能够通过复杂的指令来减少程序中的指令数量,从而在某种程度上提高程序的执行效率(尽管这通常是以牺牲硬件复杂度和功耗为代价的)。
3. 功耗和成本 - RISC:由于指令集简单且执行效率高,RISC处理器通常具有较低的功耗和成本。这使得RISC处理器在嵌入式系统、移动设备等对功耗和成本有严格要求的领域具有优势。
- CISC:CISC处理器的复杂指令集和较高的功耗使得其成本相对较高。然而,在需要高性能和复杂功能的场合(如服务器、工作站等),CISC处理器仍然具有广泛的应用。
4. 编程难度和灵活性 - RISC:由于指令集简单且固定长度,RISC处理器的编程难度相对较低。但是,这也意味着在某些情况下可能需要编写更多的指令来完成相同的任务。
- CISC:CISC处理器的复杂指令集使得编程更加灵活和方便。程序员可以使用较少的指令来完成复杂的任务,从而提高了编程效率。然而,这也增加了程序的复杂性和维护难度。
存储器结构模型–冯诺依曼结构和哈弗架构的异同
哈弗架构(Harvard architecture):
- 是一种将程序指令存储和数据存储分开,分别具有独立的地址空间和总线,允许指令和数据的同时访问的计算机体系结构。
- 允许CPU在一个机器周期内同时从指令存储器和数据存储器中读取信息,提高了执行速度和数据的吞吐率。
- 由于指令和数据分开存储,它们的宽度可以不同,这为系统设计提供了更大的灵活性。
- 哈弗架构广泛应用于数字信号处理器(DSP)、微控制器和某些嵌入式系统中,如ARM Cortex-M系列芯片。在改进型哈弗架构中,还引入了指令缓存(Instruction Cache)和数据缓存(Data Cache),以进一步提高处理器性能和效率。
冯诺依曼架构(Von Neumann Architecture):
- 将程序指令存储器和数据存储器合并在一起的存储器结构。程序指令和数据共享同一个存储空间,并使用同一条总线进行访问。
- 这种架构采用顺序执行的方式,即CPU按照指令的顺序依次从存储器中读取指令和数据。
- 由于指令和数据通过同一条总线传输,CPU无法同时读取指令和数据,可能导致“冯诺依曼瓶颈”,限制了系统的并行处理能力和整体性能。
- 总线冲突:指令和数据共享总线可能导致总线冲突,影响系统效率。
- 冯诺依曼架构广泛应用于个人计算机、服务器和嵌入式系统等通用计算设备中。它的简洁性和灵活性使其成为许多计算场景的首选架构。
ARM/x86/MIPS/DSP
- ARM架构具有功耗低、性能高、体积小、成本低等诸多优点,ARM处理器具有灵活的架构设计,可根据不同应用需求进行定制,满足特定应用场景的要求;再次,ARM处理器具有广泛的软件生态系统支持,有大量的开源和商业软件可用,开发者可以快速构建应用,缩短产品上市周期
- x86架构是一种通用性较强的处理器架构,用于个人电脑,也广泛运用于嵌入式系统。x86处理器采用了高度集成的设计,能够提供出色的计算能力,适合进行复杂的计算任务,如图像处理、数据分析等;x86架构还具有高度的兼容性,支持多种操作系统和编程语言,易于迁移和集成;x86架构的功耗较高,体积较大
- MIPS架构是一种经典的32位RISC指令集架构,它具有指令简洁、执行效率高的特点,适用于诸如路由器、交换机、多媒体设备等嵌入式领域。MIPS架构的优点有:首先,MIPS处理器具有指令简单、流水线执行效率高等特点,能够提供出色的计算能力和响应速度;其次,MIPS处理器的功耗较低,适合要求长时间运行的设备,如路由器、交换机等;再次,MIPS架构具有较强的内存管理和异常处理能力,能够提供良好的系统稳定性和安全性。MIPS处理器的软件生态系统相对较弱,支持的操作系统和开发工具较少,不利于开发和调试;其次,MIPS架构的市场份额较小,相关设备和技术的支持相对有限;再次,MIPS处理器的体系结构较为复杂,对硬件设计和软件开发的要求较高。
- DSP(数字信号处理器)架构是一种专门用于处理数字信号的嵌入式微处理器架构,具有高性能、低功耗、丰富的并行计算能力等特点,适用于音频、图像、视频等数字信号的处理和压缩。DSP架构的优点有:首先,DSP处理器的指令集和硬件设计专注于数字信号处理,具有高效的算法执行能力,能够提供优秀的信号处理效果;其次,DSP架构具有丰富的并行计算能力,支持高性能的数据交换和并行计算操作;再次,DSP处理器的功耗较低,适合要求节能环保的应用场合。DSP处理器较为专用,适用范围相对较窄,在通用计算方面的表现相对差
内存分配函数,如果系统内存只有10K,我要申请12K,可以成功吗
在大多数现代操作系统和编程环境中,如果你尝试申请的内存大小(比如12K)超过了系统可用内存(比如只有10K,这里假设的是一个非常极端的简化情况),内存分配函数(如C语言中的malloc)通常不会成功分配内存,并会返回一个空指针(在C中为NULL)。
这是因为操作系统管理内存时,会跟踪哪些内存区域是可用的,哪些已经被其他程序或进程占用。当程序请求分配内存时,操作系统会检查是否有足够的连续可用内存来满足请求。如果没有足够的连续内存空间可用,请求就会失败。
处于一个只有很少量内存的环境中(比如嵌入式系统或非常老旧的计算机),那么内存分配失败的情况确实可能发生。
此外,还需要考虑操作系统的内存管理机制,如虚拟内存。虚拟内存允许操作系统将磁盘空间用作内存的一部分,以扩展可用内存。但是,即使在这种情况下,如果物理内存和虚拟内存的总和也不足以满足分配请求,分配仍然会失败。
STM32相关
STM32的启动过程
STM32 启动文件由 ST 官方提供,在官方的固件包里。
启动文件由汇编编写,是系统上电复位后第一个执行的程序。
启动文件主要做了以下工作:
1、初始化堆栈指针 SP = _initial_sp
2、初始化程序计数器指针 PC = Reset_Handler
3、设置堆和栈的大小
4、初始化中断向量表
5、配置外部 SRAM 作为数据存储器(可选)
6、配置系统时钟,通过调用 SystemInit 函数(可选)
7、调用 C 库中的 _main 函数初始化用户堆栈,最终调用 main 函数
https://blog.youkuaiyun.com/m0_56694518/article/details/135032170
51单片机和STM32架构的区别
51单片机基于传统的哈佛总线结构,采用 CISC 架构
STM32 基于 ARM Cortex-M 系列的32位处理器核心,采用 RISC 架构.
MCU可以运行Linux吗
MCU可以运行Linux。
要在MCU上运行Linux,需要选择一个适合的处理器和内存,以及一个支持Linux的开发板或芯片。这些开发板或芯片通常具有足够的硬件资源来支持Linux操作系统的运行。接下来,需要将Linux内核和根文件系统(rootfs)烧录到开发板或芯片中。由于MCU的资源有限,因此需要选择轻量级的Linux发行版,如嵌入式Linux发行版(如Buildroot、OpenWrt、BusyBox等),这些发行版可以根据应用的需求进行裁剪,以减小系统的体积和资源占用。
还需要考虑处理器架构、处理器速度、内存容量、存储容量、网络接口等因素。
STM32 中的中断机制
中断是CPU对I/O设备或其他事件发来的信号的一种响应。
当这类事件发生时,CPU会暂停正在执行的程序,保留当前的环境信息后自动转去执行相应的处理程序,这个过程称为中断处理。
首先,当一个中断请求发生时,相应的中断标志位会被设置。然后,处理器会自动保存当前的现场到堆栈里,包括PC、xPSR、R0-R3、R12和LR等寄存器的值。一旦入栈结束,ISR(中断服务例程)就可以开始执行。
在处理完中断后,需要手动清除中断标志位,以便重新使能该中断。最后,被打断的指令会在退出中断后继续执行。这个过程也可以被打断,使得随时可以响应新的中断。
STM32F0中断的体系结构中,NVIC(嵌套向量中断控制器)是主要的部分,它负责中断管理,支持异常及中断向量化处理,以及支持嵌套中断。CM3内核支持256个中断,其中包含了16个内核中断和240个外部中断,并且具有256级的可编程中断设置。此外,STM32的每一个IO口都可以作为外部中断的中断输入口。
中断上下文
中断上下文(Interrupt Context)是指当 CPU 接收到硬件设备的中断请求时,CPU 会暂停当前运行的进程或线程,然后立即跳转到预先定义好的中断处理函数去执行。在这个过程中,由于操作系统需要保存和恢复一些关键的现场信息,所以就会产生一个新的上下文环境,称为中断上下文。
在 Linux 操作系统内核中,中断上下文通常包括以下内容:
中断号:标识触发中断事件的硬件设备。
上下文标志位:表示当前是否处于中断上下文。
用户空间堆栈指针和内核空间堆栈指针:用于保存用户空间和内核空间的栈帧。
线程状态:表示被打断前 CPU 正在执行哪个进程或线程。
CPU 寄存器状态:记录所有寄存器(如通用寄存器、程序计数器、堆栈指针等)的值。
总之,每次处理完一个硬件设备发出的中断请求后,操作系统都会恢复先前的运行状态,并继续执行之前被打断的进程或线程
stm32使用浮点会对中断效率产生什么影响?
浮点运算:浮点运算是指对浮点数(即带有小数点的数)进行的数学运算,如加、减、乘、除等。STM32系列中的某些型号(如基于Cortex-M4或M7核心的型号)内置了硬件浮点运算单元(FPU),可以加速浮点运算
STM32使用浮点对中断效率的影响:
浮点运算的硬件支持:
如果STM32型号内置了FPU,那么浮点运算将由硬件直接支持,执行速度相对较快,对中断效率的影响较小,因为FPU可以独立于CPU核心进行运算,不会直接阻塞中断处理。
如果STM32型号没有内置FPU或FPU被禁用,那么浮点运算将通过软件模拟实现,这会导致运算速度大幅下降,并可能增加CPU的负担。如果中断服务程序中包含大量的浮点运算,那么中断的响应时间和处理效率都会受到影响。
中断处理中的浮点运算:
如果中断服务程序中需要执行浮点运算,那么这些运算的执行时间将直接影响中断的处理效率。特别是在没有FPU支持或FPU性能有限的情况下,浮点运算可能成为中断处理中的瓶颈。
为了减少这种影响,可以优化中断服务程序中的浮点运算部分,例如通过减少不必要的浮点运算、使用更高效的算法或预先计算结果等方式来降低计算复杂度。
STM32存储器映射
对于32位的处理器,可寻址的范围为232字节,即232= 4 × 1024 × 1024 × 1024 = 4GB,也就是0x00000000至0xFFFFFFFF。ARM将这4G空间从低地址到高地址依次划分为代码区(Code)、片上SRAM区(SRAM)、片上外设(Peripheral)、片外RAM(External RAM)、片外外设(External Device)和系统级(System level),程序存储器、数据存储器、寄存器和 I/O 端口排列在同一个顺序的 4 GB 地址空间内
通过把片上外设的寄存器映射到外设区,就可以简单地以访问内存的方式来访问这些外设的寄存器,从而控制外设的工作,片上外设可以使用C语言来操作。这种预定义的映射关系,也使得对访问速度可以做高度的优化,而且对于片上系统的设计而言 更易集成
嵌入式系统中的存储器类型有哪些?
嵌入式系统中的存储器类型主要有随机存取存储器(RAM)、只读存储器(ROM)、闪存(Flash Memory)、硬盘驱动器(Hard Disk Drive)、光盘(Compact Disc)等。
其中,RAM和ROM是最常用的存储器类型。 RAM(手机中的运行内存)是一种易失性存储器,其运行速度比较快,常用于存储系统运行时的程序和数据。而ROM(非手机中的外存(外部存储器),则是一种非易失性存储器,即使断电也不会丢失存储的信息,通常用于存储程序和固定数据。
Flash Memory。 闪存是一种非易失性存储器,具有电可擦除和重新编程的特性,同时它还具有较快的读写速度、较低的功耗以及较长的寿命。
此外,根据存储器所处的物理位置,嵌入式系统的存储器还可以分为片内存储器和片外存储器以及外部存储设备。
微控制器内部通常会有一块RAM和ROM(就是为什么MCU可以控制boot引脚来控制程序烧录在内部ROM,还是外部的Flash。从而让程序在ROM或者Flash启动),用于存储执行中的程序和数据;而在微控制器外部,可能还会连接有外部的Flash存储器、硬盘驱动器或者光盘等存储设备。
嵌入式系统中的闪存和EEPROM的区别。
Flash和EEPROM都是非易失性存储器,意味着即使在断电后,它们也能保持存储的数据。然而,在它们的操作和应用方面存在一些主要的区别:
读取方式:Flash和EEPROM都支持随机读取,可以通过地址直接访问存储器中的数据
写入方式:Flash和EEPROM的写入方式不同。Flash通常需要按块进行写入,而EEPROM则可以按字节进行写入。
这是由于FLASH的电路结构较简单,同样容量占芯片面积较小,成本自然比EEPROM低,因而适合用作程序存储器;而EEPROM则更多的用作非易失的数据存储器。
擦除方式:虽然Flash和EEPROM都属于电擦除的ROM,但是Flash改进了擦除的方式,不再是以字节为单位,而是以块为单位,这样简化了电路,数据密度更高,降低了成本。
擦写次数:EEPROM的擦写次数一般超过10万次,而FLASH则较少,一般一万次。这也是为什么我们通常使用FLASH来存储不需要经常改变的程序,而使用EEPROM来存储需要反复读写的数据。
价格:由于EEPROM的电路结构和操作复杂度高于Flash,因此其单位容量的价格可能会更高。
用途:Flash和EEPROM通常在不同的应用场合中使用。例如,Flash常用于存储程序,而EEPROM则更多地用于存储设备配置、用户设置等数据
IIC为什么要加上拉电阻,为什么使用开漏输出
上拉电阻:
- 当IIC总线在空闲状态,SDA和SCL需要处于高电平状态
- 开漏输出无法输出高电平,使用上拉电阻可以完成高低电平之间的转换。
开漏输出:
- IIC的SDA脚即要作为输出,又要作为输入,用开漏输出模式,很好实现输出输入共用,避免IO模式频繁切换带来的麻烦。
- 实现线与功能
I2C和SPI总线协议。
IIC:是一种同步半双工通讯协议 通信总是由主机启动,每个通信过程由起始信号开始,由停止信号结束,一个数据包有八位,每个数据包后有一个应答位和非应答位
SCL是时钟线 SDA是数据线
IIC协议的起始信号:SCL为高电平时候,SDA上升沿。 停止信号:SCL为高电平时候,SDA下降沿 数据传输:SCL为第电平,SDA开始传输数据
SPI:是一种同步全双工总线协议
SS:片选线 (默认高电平,选中时拉低片选) SCLK:时钟线 MISO:数据接收线 MOSI:数据输出线
时钟极性:决定时钟信号在空闲状态默认高还是低电平
时钟极性为0(CPOL=0):时钟信号在空闲状态下为低电平,即时钟信号开始时为低电平。
时钟极性为1(CPOL=1):时钟信号在空闲状态下为高电平,即时钟信号开始时为高电平。
时钟相性:决定数据在时钟信号的下降沿还是上升沿进行采样。
时钟相位为0(CPHA=0):数据在时钟信号的下降沿进行采样,即数据稳定在时钟的上升沿之前。
时钟相位为1(CPHA=1):数据在时钟信号的上升沿进行采样,即数据稳定在时钟的下降沿之后。
数据传输:
写数据:当SS拉低,SCLK上升沿采样(具体要看时钟相性的设置)
读数据:当SS拉低,SCLK上升沿采样。(先发送要读寄存器的位置,再接收寄存器设备发送的数据)
固件和驱动程序有什么区别?
在嵌入式系统中,固件和驱动程序是两种不同的软件形态,但它们都是代码,分别服务于硬件和操作系统。固件,也被称为Firmware,是一种被写入只读存储器(ROM)中的程序,担任着一个系统最基础最底层的工作,直接控制硬件。比如计算机上电后第一个启动的程序BIOS,它可以与硬件交互,并检查它是否有任何错误。
驱动程序则是操作系统和硬件组件之间的中间人,它充当了硬件设备和应用软件之间的桥梁,使得应用程序可以间接地操作硬件。因此,在开发驱动程序时,除了必须了解新设备的硬件特性外,还需要让驱动程序符合操作系统的规定。
数字信号处理(DSP)。
DSP(Digital Signal Processing),全称为数字信号处理器
数字信号处理是将连续时间或连续幅度的信号转换为离散时间或离散幅度的数字信号,然后对这些数字信号进行运算处理的技术。
包含但不限于信号变换、滤波、检测、估值、调制解调以及快速算法等。
PWM(脉宽调制)。
PWM:脉宽调制 和可实现控制高电平的占比(占空比)
脉冲宽度调制(PWM)是一种模拟控制方式,其工作原理是通过对一系列脉冲的宽度进行调制,从而等效出所需要的波形,包括波形的形状和幅值。
在嵌入式系统中,PWM的应用非常广泛,例如电机控制、LED亮度调节等。
波特率
比特率:每秒钟传送的比特数,单位bit/s
波特率:每秒钟传送的码元数,单位Baud
比特率 = 波特率 * log2M,M表示每个码元承载的信息
二进制系统中,波特率数值上等于比特率
嵌入式系统中的功耗管理技术
串口数据帧格式
起始位 数据位 校验位 停止位
操作系统
进程和线程
运行多个进程,其中一个进程执行完了,它的静态区会如何处理
当一个进程执行完毕后,操作系统会回收该进程的资源,包括其地址空间。在回收过程中,操作系统会释放该进程所占用的内存空间,包括静态区。释放静态区的具体处理方式取决于操作系统的实现。
一般情况下,操作系统会将静态区的内存空间标记为可用,以便其他进程可以重新使用。但是,静态区中的数据并不会立即被清除,而是等待下一个进程使用该内存空间时进行覆盖。因此,在下一个进程使用该静态区之前,原来进程的数据可能仍然存在于内存中。
互斥锁和信号量的区别
- 互斥量用于线程的互斥,信号量用于线程的同步;
- 互斥量值只能为0或1,信号量值可以为非负整数;
- 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
死锁的必要条件
互斥条件:
指进程对所分配到的资源进行排他性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其他进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。这一条件确保了资源在任意时刻只被一个进程使用,从而防止了多个进程同时访问同一资源可能导致的混乱。
请求和保持条件:
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。这一条件使得进程在等待新资源的同时继续占有已分配的资源,增加了死锁的风险。
不可剥夺条件:
进程所获得的资源在未使用完之前,不能被其他进程强行夺走,只能由获得资源的进程自己释放。这一条件确保了进程在完成其任务前可以持续使用其已获得的资源,但同时也阻止了其他进程在资源未被释放时获取该资源。
循环等待条件:
在发生死锁时,必然存在一个进程-资源的环形链,即进程P1等待由进程P2占用的资源,P2等待由进程P3占用的资源,依此类推,直到Pn等待由进程P0占用的资源。这一条件表明存在一个循环的等待关系,使得每个进程都在等待其他进程释放资源,从而导致死锁。
对于临界区的数据,使用互斥锁和自己使用条件判断,哪个好
使用互斥锁的优势:
强制互斥:互斥锁能确保在任何时刻,只有一个线程能进入临界区,从而保护共享资源不被多个线程同时访问,有效防止数据竞争和不一致问题。
自动管理:互斥锁提供了一套完整的加锁和解锁机制,由操作系统或运行时库管理,程序员只需在合适的位置调用加锁和解锁函数,无需手动处理复杂的同步逻辑。
跨平台兼容性:互斥锁在多种操作系统和编程环境中都有良好的支持,易于实现跨平台的线程同步。
使用条件判断的优势(在某些场景下):
性能优化:在某些特定场景下,如果临界区的访问非常频繁且持续时间极短,使用简单的条件判断(如原子操作)可能比互斥锁更高效,因为互斥锁的加锁和解锁操作本身需要一定的时间开销。
避免死锁:在复杂的多线程程序中,不当的互斥锁使用可能会导致死锁。相比之下,简单的条件判断在某些情况下可能更容易避免死锁问题。但需要注意的是,即使使用条件判断,也需要谨慎设计同步逻辑以防止竞争条件。
优先级反转及解决方法
任务调度器,总是去激活某个,在所有任务中优先级是最高的,且处于就绪状态的任务,即让其去执行
任何任务,都可能由于需要某种资源,而该资源被别的任务占用,而无法继续运行下去,此时任务状态就变成:挂起 –> 等待其所需要的资源被释放
示例:
当某个最高优先级的任务A,由于其所需要的某个资源被某个低优先级的任务C占用了(还没使用完,还没释放),所以高优先级任务A就被阻塞了。按照调度规则:此高优先级的任务A,必须等到低优先级任务C,把其所占用的资源释放掉后,才能继续运行。但是在高优先级任务A执行的这段时间内,某个中优先级的任务B,已经处于就绪状态了。
结果就是当高优先级的任务A,由于所需资源被占用而挂起,然后中优先级的任务B,由于比(本来打算去调度执行的)低优先级任务C的优先级高,所以被调度执行,然后B去一直执行,直到结束。
一个具有中等优先级的任务(B),却比一个更高优先级的任务(A)先执行—优先级反转
如何解决:
优先级继承:给低优先任务级C,暂时设置成和A相同的优先级
优先级天花板:将申请某资源的任务的优先级提升到可能访问该资源的所有任务中最高优先级任务的优先级。
RTOS的实时性?为什么它很重要?
嵌入式系统的实时性是指在系统接收到外部事件或信号后,能够及时地做出响应并处理这些事件或信号的能力。实时性对于许多嵌入式系统应用(如航空航天控制、医疗设备和交通控制系统等)至关重要,因为它们需要迅速响应外部事件以确保安全和准确性。如果系统的实时性不足,可能会导致延迟、错误或系统失败。
RTOS任务调度算法有哪些?
优先级调度算法:高优先级的任务先执行,执行完之后执行低优先级的
抢占调度:低优先级任务执行过程中,若有高优先级的任务发生,则高优先级的执行
时间片轮转调度:在这种算法中,即使所有任务具有相同的优先级,它们也会轮流执行一个固定的时间片(时间片长度可以是可变的或固定的)。时间片结束时,当前任务将CPU控制权交给下一个任务,无论其是否完成。这种算法保证了每个任务都能获得公平的执行时间,避免了优先级反转的问题。
RTOS的任务间通信机制。
队列 信号量 互斥量 事件组 任务通知
RTOS的SysTick优先级低,影响RTOS时钟精度怎么办
uC/OS、FreeRTOS都是这么设置的
假如SysTick的优先级高于IRQ的优先级,所以会导致某些情况下IRQ的响应速度变慢,执行时间变长
SysTick的优先级为最低,并且将任务调度放到了PendSV,可以最大限度的减少SysTick中断服务程序消耗时间,IRQ不会被中断,并且能够最快的时间去响应
OS的代码中,一般在SysTick中会关中断,这个时候不照样会关闭IRQ吗? 想到这块的时候,我们就要考虑一下,提到这个问题,其实就是过于追求完美了,没有十全十美的解决方案,使用SysTick + PendSV的目的就是为了最大限度的减少任务调度 对于IRQ的影响。做不到完全不影响,做到最少的影响的方案就是最好的方案.
外部中断一般是硬件中断,优先级都需要很高。
操作系统的心跳节拍本质上讲,就是一个保证操作系统正常运行的节拍而已
嵌入式OS的时钟节拍,压根就不是精准的,也不需要太准,大概准就可以了,因为只要有周期性的节拍,就能保证周期性的调度。软件定时器也不是特别准的,一般用于【短时间】、对时间要求不严苛的场景。如果非要对时间要求的特别准,还是需要用硬件定时器实现的。
SysTick的优先级设置为最低,不是完美的解决方案,是一种对外部IRQ影响最小的解决方案,是最合适的方案。
RTOS的PendSV功能,为什么需要PendSV
PendSV的特点就是支持【缓期执行】,进行任务调度过程的上下文切换。简单的说就是任何嵌入式RTOS,都需要尽量不打断外设中断。
将SysTick的优先级设置为最低,然后在SysTick中进行上下文切换,然后任务调度呢?
OS在调度任务时,会关闭中断,也就是进入临界区,而OS任务调度是要耗时的,这就会出现一种情况:
在任务调度期间,如果新的外部IRQ发生,CPU将不能够快速响应处理。
- PendSV是可悬起系统中断,具有【缓期执行】特征。
- PendSV一般被嵌入式OS用于 任务调度的上下文切换。
- 使用PendSV时,一般会将PendSV的优先级设置为最低,让外部IRQ先执行,等到没有外部中断后,在执行上下文切换。
FreeRTOS内存管理算法
Linux阻塞和非阻塞 IO
这里的“IO”并不是我们学习 STM32 或者其他单片机的时候所说的“GPIO”(也就是引脚)。
这里的 IO 指的是 Input/Output,也就是输入/输出,是应用程序对驱动设备的输入/输出操作。当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程
序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃
Linux 中断的原理和开发方式
Linux中断分为两部分
上半部:中断处理函数,用来处理哪些处理比较快的,对时间敏感、执行速度快、不希望被其他中断打断、和硬件有关的操作,
下半部:如果中断处理比较耗时,将耗时的代码提取出来,交给下半部执行,比如上半部将数据拷贝到内存中,关于数据的处理就可以放在下半部执行,这样中断处理函数就会快进快出
Linux软件中断和硬件中断的区别
- 软件中断是由特殊指定触发,硬件中断是由外部设备或处理器内部产生的信号触发的
- 软件中断可以是同步的也可以是异步的,硬件中断是异步的
- 软件中断的响应时间较长,硬件中断响应时间非常短
Linux 用户空间与内核通信方式有哪些
用户空间发起的有系统调用、proc、虚拟文件系统等。
内核空间主动发起的有get_user/put_user、信号、netlink等。
系统调用:
open(),read(),write(), ioctl(), mmap()
get_user(x,ptr):在内核中被调用,获取用户空间指定地址的数值并保存到内核变量x中
put_user(x,ptr):在内核中被调用,将内核空间变量x的数值保存到到用户空间指定地址处
copy_from_user() / copy_to_user():主要应用于设备驱动读写函数中,通过系统调用触发
虚拟文件系统 :
● proc文件系统 proc文件系统实现用户空间与内核空间的数据通信_proc通信
● sysfs文件系统 linux sysfs文件系统
● debugfs文件系统 debugfs使用指南
很多内核程序细节,如中断等,都在proc/目录下有所体现,虚拟文件系统提供了一种便捷的用户空间和内核空间的交互方式;
netlink
netlink socket提供了一组类似于BSD风格的API,用于用户态和内核态的IPC。相比于其他的用户态和内核态IPC机制,netlink有几个好处:1.使用自定义一种协议完成数据交换,不需要添加一个文件等。2.可以支持多点传送。3.支持内核先发起会话。4.异步通信,支持缓存机制。
内存映像
mmap共享内存。Linux通过mmap的把内核中特定部分的内存空间映射到用户级程序的内存空间去,从而提供了用户程序对内存直接访问的能力。该方式尤其适合在那些内核和用户空间需要快速大量交互数据的情况下。
内核程序使用信号通知应用程序
信号在内核里的用途主要集中在通知用户程序出现重大错误,强行杀死当前进程,这是内核通过发送SIGKILL信号通知进程终止。
信号发送必须要事先知道进程序号(pid),所以要想从内核中通过发信号的方式异步通知用户进程执行某项任务,那么必须事先知道用户进程的进程号才可以(可以让应用程序通过ioctl函数,把自己的PID主动告诉驱动程序)。而一般内核运行时搜索特定进程的进程号是个费事的工作,可能要遍历整个进程控制块链表。所以用信号通知特定用户进程的方法很糟糕,一般在内核不会使用。内核中使用信号的情形只出现在通知当前进程(可以从current变量中方便获得pid)做某些通用操作,如终止操作等。因此对内核开发者该方法用处不大。类似情况还有消息操作。
文件
应该说这是一种比较笨拙的做法,不过确实可以这样用。当处于内核空间的时候,直接操作文件,
将想要传递的信息写入文件,然后用户空间可以读取这个文件便可以得到想要的数据了。
Linux中怎么实现同步
互斥锁/信号量/条件变量
Linux wakelock机制
https://zhuanlan.zhihu.com/p/163518775
Linux 休眠模式
# echo standby >/sys/power/state //CPU和RAM在运行
# echo mem > /sys/power/state //挂起到内存(待机),关闭硬 盘、外设等设备
# echo disk > /sys/power/state //挂起到硬盘(休眠),关闭硬盘、外设等设备,进入关机状态。此时计算机
完全关闭,不耗电
# echo on > /sys/power/state //退出休眠
# echo +10 > /sys/class/rtc/rtc0/wakealarm //10s后rtc唤醒cpu
- On(on) S0 - Working
- Standby (standby) S1 - CPU and RAM are powered but not executed
- Suspend to RAM(mem) S3 - RAM is powered and the running content is saved to RAM
- Suspend to Disk,Hibernation(disk) S4 - All content is saved to Disk and power down
2.查看电源状态
//查看当前睡眠情况
# cat /sys/power/state
//查看当前那些占用了阻止深度睡眠的wake lock
# cat /sys/power/wake_lock
//查看wake lock状态情况
# cat /sys/proc/wakelocks
//手动释放一次名称为“PowerManagerSerivce”的wake lock引用次数-1,为0时进入睡眠
#cat "PowerManagerService" > /sys/power/wake_unlock