1、宏定义printf()
问题提出
有时候我们想用宏定义来决定是编译debug版本的代码还是release的代码,dubug版本的代码会通过printf打印调试信息,release版本的代码则不会。我们总不能对每一条printf都这样写:
#if IS_DEBUG
printf("hello world!");
#endif
解决方法
我后来想到一个方法,可以使用宏定义代替printf函数,由于printf是可变参数的函数,这里就要用到变參宏(…和__VA_ARGS__)。
在头文件下写此代码
后面需要打印调试信息的时候使用PR宏就可以了,如果需要release版本,不打印调试信息,就把DEBUG设置为0,编译出来的程序就不会打印调试信息了。
#include "stdio.h"
#define IS_DEBUG 1 // 需要调试信息
#if IS_DEBUG
#define PR(...) printf(__VA_ARGS__)
#else
#define PR(...)
#endif
int main()
{
printf("debug test!\r\n"); //一定会打印
PR("hello world!\r\n"); // 如果IS_DEBUG 1 打印
PR("string:%s\r\n", "data");
PR("integer:%d\r\n", 100);
return 0;
}
2、static的作用
1、修饰局部变量–静态局部变量
被static修饰的局部变量 只会被初始化一次,不会被改变,下一次使用该变量的值就是上一次的值
void test(void)
{
static int a = 0;
printf("%d ",a++);
}
int main()
{
for(int i=0;i<10;i++)
{
test();
}
return 0;
}
// 0 1 2 3 4 5 6 7 8 9
2、修饰全局变量–静态全局变量
也是被初始化一次,在当前模块中使用,不可以被extern引用
3、修饰函数–静态函数
经static修饰后不可跨文件调用函数,并报错。
3、linux操作系统是怎么启动的
1、硬件初始化
首先进行的是硬件初始化。
- 检测并初始化 CPU:CPU 是计算机的核心部件,它的初始化工作主要包括设置 CPU 的工作模式、时钟频率等参数。
- 检测并初始化内存:内存是计算机的临时存储空间,它的初始化工作主要包括为内存分配物理地址空间、设置内存的工作模式等。
- 检测并初始化硬盘:硬盘是计算机的主要存储设备,它的初始化工作主要包括检测硬盘的状态、设置硬盘的工作模式等。
- 检测并初始化键盘、鼠标等输入设备:这些设备是用户与计算机交互的主要工具,它们的初始化工作主要包括检测设备的状态、设置设备的工作模式等。
2、内核启动
硬件初始化完成后,计算机会加载并启动内核。内核启动的过程主要包括以下步骤:
- 从磁盘读取内核映像:内核映像包含了操作系统的所有代码和数据,它是通过磁盘上的文件系统提供的。
- 解压内核映像:内核映像通常以压缩的形式提供,需要解压后才能被内核读取。
- 跳转到内核入口点:内核入口点是一个特殊的函数,它是内核运行的起始点。当内核启动时,它会跳转到这个入口点开始执行。
3、系统初始化
内核启动后,接下来会进行系统初始化。系统初始化主要包括以下任务:
- 创建进程0(即 init 进程):init 进程是系统的主进程,它的任务是启动其他所有的进程。
- 初始化各种系统设备和服务:这包括网络接口、文件系统、设备驱动等。
4、文件系统挂载
系统初始化完成后,接下来的工作就是挂载文件系统。文件系统挂载是将文件系统与计算机的文件系统中的某个目录关联起来,使用户可以访问到文件系统中的内容。这一阶段主要完成以下任务:
- 确定文件系统的类型:文件系统有多种类型,如 ext2、ext3、ntfs 等,需要根据文件系统的类型来确定如何挂载。
- 确定挂载点的设备和挂载选项:挂载点是一个目录,需要确定这个目录的设备号和挂载选项。设备号决定了文件系统在哪个设备上挂载,挂载选项决定了如何访问文件系统中的内容。
- 挂载文件系统:根据前面的信息,挂载文件系统到指定的设备和挂载点。
5、登录提示符显示
文件系统挂载完成后,计算机会显示一个登陆提示符,提示用户可以登录操作系统了。此时,用户可以输入用户名和密码来登录操作系统
4、进程与线程的区别
一个进程由进程控制块、数据段、代码段组成
进程与线程的区别:
- 本质区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
- 包含关系:一个进程至少有一个线程,线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
- 资源开销:每个进程都有独立的地址空间,进程之间的切换会有较大的开销;线程可以看做轻量级的进程,同一个进程内的线程共享进程的地址空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
- 影响关系:一个进程崩溃后,在保护模式下其他进程不会被影响,但是一个线程崩溃可能导致整个进程被操作系统杀掉,所以多进程要比多线程健壮。
优缺点总结:
1)一个进程死了不影响其他进程,一个线程崩溃很可能影响到它本身所处的整个进程。
2)创建多进程的系统花销大于创建多线程。
3)多进程通讯因为需要跨越进程边界,不适合大量数据的传送,适合小数据或者密集数据的传送。多线程无需跨越进程边界,适合各线程间大量数据的传送。并且多线程可以共享同一进程里的共享内存和变量。
进程间通讯:
(1)有名管道/无名管道(2)信号(3)共享内存(4)消息队列(5)信号量(6)socket
线程的通信:
(1)信号量(2)读写锁(3)条件变量(4)互斥锁(5)自旋锁
5、并行和并发的区别

6、说明什么是上下文切换?
就是保存当前状态,切换任务,然后回到保存的状态
1、进程上下文
一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
2、中断上下文
一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
7、strcat、strncat、strcmp、strcpy 哪些函数会导致内存越界?如何改进?
strcpy函数会导致内存越界。
strcpy拷贝函数不安全,他不做任何的检查措施,也不判断拷贝大小,不判断目的地址内存是否够用。
char *strcpy(char *strDest,const char *strSrc)
strncpy拷贝函数,虽然计算了复制的大小,但是也不安全,没有检查目标的边界。
strncpy(dest, src, sizeof(dest));
strncpy_s是安全的。
strcmp(str1,str2),是比较函数,若str1=str2,则返回零;若str1<str2,则返回负数;若str1>str2,则返回正数。(比较字符串)
strncat()主要功能是在字符串的结尾追加n个字符。
char * strncat(char *dest, const char *src, size_t n);
strcat()函数主要用来将两个char类型连接。例如:
char d[20]="Golden";
char s[20]="View";
strcat(d,s);
//打印d
printf("%s",d);
//输出 d 为 GoldenView (中间无空格)
8、strcpy和memcpy的区别
memcpy拷贝函数,它与strcpy的区别就是memcpy可以拷贝任意类型的数据,strcpy只能拷贝字符串类型。
memcpy 函数用于把资源内存(src所指向的内存区域)拷贝到目标内存(dest所指向的内存区域);有一个size变量控制拷贝的字节数;
void *memcpy(void *dest, void *src, unsigned int count);
就是说,memcpy是把整个内存拷过去,strcpy只可以拷贝字符串类型
9、const的用法
我记得有一个人说,不能把const当成常量,可以把他说成是只读。
- 用const修饰常量:不能被更改,相当于常量
- 用const修饰形参:func(const int a){};在当前函数中不能被更改,只能读不能写
- 用const修饰类成员函数:该函数对成员变量只能进行只读操作,就是const类成员函数是不能修改成员变量的数值的。
const int *temp; // 指向常整型数的指针 只能操作指针,不能写值
int * const temp; // 指向整型数的常指针 只能操作值,不能改变指针指向
10、volatile作用和用法
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量在内存中的值,而不是使用保存在寄存器里的备份(虽然读写寄存器比读写内存快)。
简单的来说,设置了volatile之后,编译器就不会对该变量进行优化。
这些情况一般会使用volatile:
- 并行设备的硬件寄存器(如:状态寄存器)
- 一个中断服务子程序中会访问到的非自动变量
- 多线程应用中被几个任务共享的变量
一般就是对寄存器的状态值、中断和多线程中的共享数据变量。
11、内存四区,什么变量分别存储在什么区域,堆上还是栈上。

- 代码区(text segment):存放程序执行代码的区域,通常是只读的。该区域的内容在程序执行时不能被修改。
- 数据区(data segment):存放已经初始化的全局变量和静态变量(包括全局和静态变量的指针)的区域。
- BSS区(bss segment):存放未初始化的全局变量和静态变量的区域,该区域的值默认初始化为0。
- 栈区(stack segment):存放函数调用时的局部变量、函数参数和返回地址等信息。栈空间是由操作系统自动分配和回收的,它的大小通常是固定的,不能随意增加。栈空间是向下增长的,也就是说,栈顶的地址是越来越小的。
- 堆区(heap segment):存放由程序员手动申请的内存空间,大小可以动态增加或减少。堆空间是由程序员手动管理的,程序员需要负责在使用完毕后将其释放。堆空间是向上增长的,也就是说,堆顶的地址是越来越大的。
12、c语言编译的四个阶段
- 预处理阶段(Preprocessing):编译器会处理源文件,包括展开宏定义、头文件的展开、条件编译等,生成一个经过预处理后的文本文件。此阶段的结果是一个以 .i 为扩展名的文件。
- 编译阶段(Compilation):编译器将经过预处理的文本文件翻译成汇编代码。汇编代码是一种低级的、与机器相关的语言。此阶段的结果是一个以 .s 为扩展名的文件。
- 汇编阶段(Assembly):汇编器将汇编代码转换成机器可以执行的指令。此阶段的结果是一个以 .o 为扩展名的文件。
- 链接阶段(Linking):连接器将目标文件以及一些必要的库文件进行链接,生成可执行文件。此阶段的结果是一个没有扩展名的可执行文件。
13、请解释一下什么是中断,以及中断服务程序是如何工作的
就是打断当前正在执行的程序,去执行另一个程序,称为中断服务程序。中断可以由硬件设备或软件发起。
当一个中断被触发时,CPU会立即停止正在执行的程序,并保存当前的上下文信息。然后,CPU会跳转到中断服务程序的入口地址开始执行。中断服务程序会处理中断请求,并根据需要执行相应的操作,例如读取键盘输入、处理磁盘读写等。当中断服务程序执行完毕后,CPU会恢复之前保存的上下文信息,继续执行被打断的程序。
中断服务程序的工作流程包括以下几个步骤:
1、中断请求:硬件设备或软件发起中断请求。
2、中断响应:CPU立即停止正在执行的程序,保存当前的上下文信息,并跳转到中断服务程序的入口地址。
3、中断处理:中断服务程序处理中断请求,并根据需要执行相应的操作。
4、中断返回:中断服务程序执行完毕后,CPU恢复之前保存的上下文信息,继续执行被打断的程序。
14、TCP/IP协议
15、IIC和SPI协议
1、IIC协议
16 、请解释arr和psc和ccr的区别与使用
arr 是自动重装载值,用于定时器的计数和自动重装载。
PSC 是预分频器 分频器寄存器,用于定时器时钟的分频。
ccr 捕获/比较器 用于定时器的捕获和比较。PWM
17、PWM计算公式

18、内存,RAM,ROM,Cache的区别与联系

1、内存
内存在电脑中起着举足轻重的作用。内存一般采用半导体存储单元,包括随机存储器(RAM),只读存储器(ROM),以及高速缓存(CACHE)。
2、RAM
RAM:随机存取存储器(random access memory),又称作“随机存储器”,是与CPU直接交换数据的内部存储器,也叫主存(内存)。可以随时读写,而且速度很快,通常作为操作系统或其他正在运行中的程序的临时数据存储媒介。当电源关闭时RAM不能保留数据。如果需要保存数据,就必须把它们写入一个长期的存储设备中(例如硬盘)。我们通常购买或升级的内存条就是用作电脑的内存,内存条(SIMM)就是将RAM集成块集中在一起的一小块电路板,它插在计算机中的内存插槽上,以减少RAM集成块占用的空间。
3、ROM
ROM:只读存储器。在制造ROM的时候,信息(数据或程序)就被存入并永久保存。这些信息只能读出,一般不能写入,即使机器掉电,这些数据也不会丢失。ROM一般用于存放计算机的基本程序和数据,如BIOS ROM。ROM所存数据,一般是装入整机前事先写好的,整机工作过程中只能读出,而不像随机存储器那样能快速地、方便地加以改写。ROM所存数据稳定,断电后所存数据也不会改变, 其物理外形一般是双列直插式(DIP)的集成块。
4、Cache
Cache:高速缓冲存储器,也是我们经常遇到的概念,它位于CPU与内存之间,是一个读写速度比内存更快的存储器。当CPU向内存中写入或读出数据时,这个数据也被存储进高速缓冲存储器中。当CPU再次需要这些数据时,CPU就从高速缓冲存储器读取数据,而不是访问较慢的内存,当然,如需要的数据在Cache中没有,CPU会再去读取内存中的数据。
5、RAM和ROM的区别
一般来说会比较难以理解RAM与ROM和平时所说的运行内存和硬盘容量有什么关系,其实从一般意义上来说是一样的,但从计算机和手机的角度来说又有一些区别:
从电脑来说一般比较好理解,RAM就是我们平时所说的运行内存,它的确是随时可读写的。因为CPU处理的数据都是以运行内存为中介的。断电后信息是不保存的。那么对于ROM来说,是不是就是硬盘呢?不是说ROM只可以读吗?硬盘却是可以修改的。的确,必须明确一点,RAM与ROM都是内存,而硬盘是外存,所以ROM不等于硬盘。计算机中的ROM主要是用来存储一些系统信息,或者启动程序BIOS程序,这些都是非常重要的,只可以读一般不能修改,断电也不会消失。
RAM和ROM相比,两者的最大区别是RAM在断电以后保存在上面的数据会自动消失,而ROM不会自动消失,可以长时间断电保存。
在手机里面,RAM就是跟电脑一样的运行内存一样;而ROM就跟硬盘挂上钩了,手机中的ROM有一部分用来存储系统信息,还有一些装机软件,剩余的大部分容量都是就是拿来作为硬盘用的,可读可写。
6、硬盘与内存的区别与联系
硬盘与内存的区别是很大的,最主要的三点:
一、内存是计算机的工作场所,硬盘用来存放暂时不用的信息。
二、内存是半导体材料制作,硬盘是磁性材料制作。
三、内存中的信息会随掉电而丢失,硬盘中的信息可以长久保存。
内存与硬盘的联系:
内存与硬盘的联系也非常密切,这里只提一点:硬盘上的信息永远是暂时不用的,要用吗?请装入内存!CPU与硬盘不发生直接的数据交换,CPU只是通过控制信号指挥硬盘工作,硬盘上的信息只有在装入内存后才能被处理。内存就是存储程序以及数据的地方,比如当我们在使用WPS处理文稿时,当你在键盘上敲入字符时,它就被存入内存中,当你选择存盘时,内存中的数据才会被存入硬(磁)盘。
内存与储存的差别:
内存与储存的差别:大多数人常将内存 (Memory)与储存空间 (Storage) 两个名字混为一谈,尤其是在谈到两者的容量的时候。内存是指 (Memory) 计算机中所安装的随机存取内存的容量,储存 (Storage) 是指计算机内硬盘的容量。从计算机的体系结构来讲,硬盘应当是计算机的“外存”。内存应当是计算机内部(在主板上)的一些存储器,用来保存CPU运算使用过程中的中间数据和计算结果,当不用这些数据时,它们被保存在硬盘上。在计算机业界,内存这个名词被广泛用来称呼 RAM( 随机存取内存 )。
以上就是关于ROM,RAM,运行内存,硬盘的一些区别
19、野指针和空指针、万能指针 void*、const 与指针
1、野指针
任意数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。
#include <stdio.h>
int main()
{
int a = 100;
int* p;
// 给指针变量p赋值,p为野指针
p = a;
// 给指针变量p赋值,p为野指针
p = 0x12345678;
// 对野指针指向的未知区域进行写操作,内存出问题,error
*p = 1000;
return 0;
}
2、空指针
就是内存空间上的一个地址,这个地址是0x0
int a;
int *p =NULL; ->p就会指向安全区域。 p = &a;
3、万能指针 void*
void* 是一个特殊的指针类型,用来表示一个指向未知类型的指针。它可以存储任何类型的地址,但无法直接解引用或操作其指向的数据。
它可以用于在没有明确类型信息的情况下表示指针。例如,当你需要在函数中传递一个指针,但不确定指针所指向的数据类型时,可以使用 void* 作为参数类型。
使用 void* 类型时,需要注意的是,在使用 void 指针进行操作之前,必须将其转换为适当的指针类型,以便进行正确的解引用和操作。这是因为 void 指针在不确定指向的具体类型时无法进行类型推断。
#include <stdio.h>
int main()
{
void* p = NULL;
int a = 10;
// 指向变量时,最好转换为void *
p = (void*)&a;
//使用指针变量指向的内存时,转换为int *
*((int*)p) = 11;
printf("a = %d\n", a);
return 0;
}
4、const 与指针
在定义指针的时候可以添加const关键字, 根据const关键字的位置可以用其修饰指针本身也可以用来修饰指针指向的值:
-
常量指针:const 关键字在 * 左边,常量指针的本质是指针,表示指针所指向的地址可变,但是地址中的数据不能被修改。例如:
C++ const int* ptr; // ptr 是一个指向 int 类型常量的指针 int const *ptr; // 等价于 const int* ptr;在这个例子中,
ptr是一个指向int类型常量的指针。这意味着不能通过ptr修改它所指向的整数值,但该指针指向的地址是可以改变的。 -
指针常量: const 关键字在 * 右边,表示指针指向的地址不能被修改,但是地址中的值可以被修改。例如:
C++ int value = 10; // ptr 是一个常量指针,指向 int 类型的变量 value, 不能被重新赋值 int* const ptr = &value;在这个例子中,
ptr是一个指针常量,指向了变量value。这意味着不能通过ptr修改指针的值,即不能将ptr指向其它地址,但可以通过*ptr来修改所指向的变量的值。注意:指针常量在定义时要赋初值。
下面有几句口诀,方便大家记忆常量指针和指针常量:
const (*号)左边放,我是指针变量指向常量 - 常量指针const (*号)右边放,我是指针常量指向变量 - 指针常量const (*号)两边放,我是指针常量指向常量 - 常量指针常量
指针变量能改指向,指针常量不能转向,要是全部变成常量,锁死了,我不能转向,你也甭想变样!
#include <stdio.h>
int main()
{
// 常量指针
int value1 = 100;
int value2 = 200;
const int* ptr = &value1; // 初始化
ptr = &value2; // ok, 可以修改指针指向的内存地址
*ptr = 300; // error, 不能修改指针指向的地址中的值
// 指针常量
int value = 10;
int* const ptr1 = &value; // 初始化
*ptr1 = 20; // ok, 可以修改指针指向的地址中的值
ptr1 = &value1; // error, 不能修改指针指向的内存地址
// 指向常量的指针常量
const int* const ptr2 = &value2;
*ptr2 = 99; // error, 不能修改指针指向的地址中的值
ptr2 = &value; // error, 不能修改指针指向的内存地址
return 0;
}
const 修饰的指针变量可以帮助确保数据的不可变性和程序的安全性,特别是在函数参数传递、返回值和常量数据的处理方面有广泛的应用。在使用 const 修饰的指针时,需要注意遵守 const 修饰符的规则,以便正确使用和理解指针的行为。
20、STM32中systick延时毫秒和微秒
系统定时器为内核中的一个外设。24位,只能递减,存在于内核嵌套在NVIC中,所有的Cortex-M内核的单片机都具有这个定时器。

系统定时器 是一个 24bit 的向下递减的计数器,计数器每计数一次的时间为 1/SYSCLK us,一般我们设置 系统时钟 SYSCLK 等于 180M。当重装载数值寄存器的值递减到 0 的时候,系统定时器就 产生一次中断,以此循环往复。
分析代码(正点原子)节选一部分
没有操作系统的
#include "delay.h"
static uint32_t fac_us=0; //us延时倍乘数
//初始化延迟函数
//当使用ucos的时候,此函数会初始化ucos的时钟节拍
//SYSTICK的时钟固定为AHB时钟
//SYSCLK:系统时钟频率
void delay_init(uint8_t SYSCLK)
{
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);//SysTick频率为HCLK
fac_us=SYSCLK; //不论是否使用OS,fac_us都需要使用
}
//延时nus
//nus为要延时的us数.
//nus:0~190887435(最大值即2^32/fac_us@fac_us=22.5)
void delay_us(uint32_t nus)
{
uint32_t ticks;
uint32_t told,tnow,tcnt=0;
uint32_t reload=SysTick->LOAD; //LOAD的值 就是重装载寄存器
ticks=nus*fac_us; //需要的节拍数 就是要先计算延时多少us对应的这个节拍数
told=SysTick->VAL; //刚进入时的计数器值 保存进入函数的systick的节拍数,就是取出当前systick的寄存器的值
while(1)
{
tnow=SysTick->VAL; //不断获取这个systick的节拍书
if(tnow!=told) // 判断这个当前的节拍和我之前记录的节拍数对比,因为是向下梯减所以看你会出现当前的节拍大于我记录的节拍
{
if(tnow<told)tcnt+=told-tnow; //这里注意一下SYSTICK是一个递减的计数器就可以了.
else tcnt+=reload-tnow+told; // 如果大于就是这样算,可以参考如果我当前的节拍为150、我刚进入的节拍told=100,重装载值为200,按道理计算是100+(200-150)
told=tnow;
if(tcnt>=ticks)break; //时间超过/等于要延迟的时间,则退出.
}
};
}
//延时nms
//nms:要延时的ms数
void delay_ms(uint16_t nms)
{
uint32_t i;
for(i=0;i<nms;i++) delay_us(1000);
}
21、FREERTOS的任务状态和优先级

优先级数字越低表示任务的优先级越低, 0 的优先级最低, configMAX_PRIORITIES-1 的优先级最高。空闲任务的优先级最低,为 0。
22、PendSV 中断的作用
23、在FreeRTOS中,二值信号量和互斥量的区别?
- 互斥型信号量必须是同一个任务申请,同一个任务释放,其他任务释放无效。同一个任务可以递归申请。
- 二进制信号量,一个任务申请成功后,可以由另一个任务释放。
24、优先级反转
当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级, 这个过程就是优先级继承。优先级继承尽可能的降低了高优先级任务处于阻塞态的时间,并且将已经出现的“优先级翻转”的影响降到最低。
25、freertos的任务通知
任务通知在 FreeRTOS 中是一个可选的功能,要使用任务通知的话就需要将宏configUSE_TASK_NOTIFICATIONS 定义为 1
任务通知的发送使用函数 xTaskNotify()或者 xTaskNotifyGive()(还有此函数的中断版本)来完 成 , 这 个 通 知 值 会 一 直 被 保 存 着 , 直 到 接 收 任 务 调 用 函 数 xTaskNotifyWait() 或 者ulTaskNotifyTake()来获取这个通知值。假如接收任务因为等待任务通知而阻塞的话那么在接收到任务通知以后就会解除阻塞态。任务通知虽然可以提高速度,并且减少 RAM 的使用,但是任务通知也是有使用限制的:
- FreeRTOS 的任务通知只能有一个接收任务,其实大多数的应用都是这种情况。
- 接收任务可以因为接收任务通知而进入阻塞态,但是发送任务不会因为任务通知发送失败而阻塞。
1
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,uint32_t ulValue,eNotifyAction eAction )
参数:
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
ulValue: 任务通知值。
eAction: 任务通知更新的方法, eNotifyAction 是个枚举类型,在文件 task.h 中有如下
typedef enum
{
eNoAction = 0,
eSetBits, //更新指定的 bit
eIncrement, //通知值加一
eSetValueWithOverwrite, //覆写的方式更新通知值
eSetValueWithoutOverwrite //不覆写通知值
} eNotifyAction;
BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t * pxHigherPriorityTaskWoken );
参数:
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
ulValue: 任务通知值。
eAction: 任务通知更新的方法。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换,这个变量的值函数会自动
设置的,用户不用进行设置,用户只需要提供一个变量来保存这
个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之
前一定要进行一次任务切换。
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
参数:
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的
void vTaskNotifyGiveFromISR( TaskHandle_t xTaskHandle,
BaseType_t * pxHigherPriorityTaskWoken );
参数:
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换,这个变量的值函数会自动
设置的,用户不用进行设置,用户只需要提供一个变量来保存这
个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之
前一定要进行一次任务切换。
BaseType_t xTaskNotifyAndQuery ( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction
uint32_t * pulPreviousNotificationValue);
参数:
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。ALIENTEK 阿波罗 FreeRTOS 开发教程
319
STM32F429 FreeRTOS 开发手册
ulValue: 任务通知值。
eAction: 任务通知更新的方法。
pulPreviousNotificationValue:用来保存更新前的任务通知值。
BaseType_t xTaskNotifyAndQueryFromISR ( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t * pulPreviousNotificationValue
BaseType_t * pxHigherPriorityTaskWoken );
参数:
xTaskToNotify: 任务句柄,指定任务通知是发送给哪个任务的。
ulValue: 任务通知值。
eAction: 任务通知更新的方法。
pulPreviousNotificationValue:用来保存更新前的任务通知值。
pxHigherPriorityTaskWoken: 记退出此函数以后是否进行任务切换,这个变量的值函数会自动
设置的,用户不用进行设置,用户只需要提供一个变量来保存这
个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之
前一定要进行一次任务切换。

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit,TickType_t xTicksToWait );
参数:
xClearCountOnExit: 参数为 pdFALSE 的话在退出函数 ulTaskNotifyTake()的时候任务通知值减一,类似计数型信号量。当此参数为 pdTRUE 的话在退出函数的时候任务任务通知值清零,类似二值信号量。
xTickToWait: 阻塞时间
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,uint32_t ulBitsToClearOnExit,uint32_t * pulNotificationValue,TickType_t xTicksToWait );
参数:
ulBitsToClearOnEntry: 当没有接收到任务通知的时候将任务通知值与此参数的取反值进行按位与运算,当此参数为 0xffffffff 或者 ULONG_MAX 的时候就会将任务
通知值清零。
ulBitsToClearOnExit: 如果接收到了任务通知,在做完相应的处理退出函数之前将任务通知值与此参数的取反值进行按位与运算,当此参数为 0xffffffff 或者
ULONG_MAX 的时候就会将任务通知值清零。
pulNotificationValue: 此参数用来保存任务通知值。
xTickToWait: 阻塞时间。
1、任务通知模拟二值信号量实验
二值信号量就是值最大为 1 的信号量,这也是名字中“二值”的来源。当任务通知用于替代二值信号量的时候任务通知值就会替代信号量值,函数 ulTaskNotifyTake()就可以替代信号量获取函数 xSemaphoreTake(),函数 ulTaskNotifyTake()的参数 xClearCountOnExit 设置为 pdTRUE。这样在每次获取任务通知的时候模拟的信号量值就会清零。函数 xTaskNotifyGive()和vTaskNotifyGiveFromISR()用于替代函数 xSemaphoreGive()和 xSemaphoreGiveFromISR()。接下来我们通过一个实验来演示一下任务通知是如何用作二值信号量的。
2、任务通知模拟计数型信号量实验
当任务通知用作计数型信号量的时候获取信号量相当于获取任务通知值,使用函数ulTaskNotifyTake()来替代函数 xSemaphoreTake()。函数 ulTaskNotifyTake()的参数 xClearOnExit要设置为 pdFLASE,这样每次获取任务通知成功以后任务通知值就会减一。使用任务通知发送函 数 xTaskNotifyGive() 和 vTaskNotifyGiveFromISR() 来 替 代 计 数 型 信 号 量 释 放 函 数xSemaphoreGive()和 xSemaphoreGiveFromISR()。下面通过一个实验来演示一下任务通知是如何用作计数型信号量。
26、GPIO的常见寄存器的配置
1、库函数
1、BRR
拉低电平
#define LED_RB_LOW (LED_RB_PORT->BRR = LED_RB_PIN)
2、BSRR
就是设置成高电平
#define IO_UART10_RXD_HIG (IO_UART10_RXD_PORT->BSRR = IO_UART10_RXD_PIN)
SRR寄存器的作用是允许你同时设置或清除GPIO引脚的输出状态,而不会影响其他引脚的状态。这是通过将寄存器中的特定位组合设置为1(置位)或0(清零)来实现的。
在BSRR寄存器中,低16位用于设置要置位的引脚,高16位用于设置要清零的引脚。通过在寄存器中写入数据,你可以同时对多个引脚进行操作,这种并行设置或清零的方式可以提高GPIO引脚控制的效率。
GPIOA->BSRR = GPIO_PIN_0 | GPIO_PIN_1; // 置位引脚0和引脚1
GPIOA->BSRR = GPIO_PIN_2 << 16 | GPIO_PIN_3 << 16; // 清零引脚2和引脚3

