线性自抗扰控制算法(LADRC)与PID控制算法相比,在某些方面展现出一定的优势。以下是这些优势及其原因的详细分析:
ADRC(自抗扰控制算法)
优势
- 更强的抗干扰能力:
- LADRC:通过其内置的扩张状态观测器(ESO),能够实时估计并补偿系统的总扰动(包括内部不确定性和外部扰动),从而显著增强系统的抗干扰能力。这种机制使得LADRC在面对复杂、多变的工业环境时,能够保持较好的控制性能。
- PID:虽然PID也具有一定的抗干扰能力,但其主要通过比例、积分和微分三个环节来调节系统,对于大幅度或快速变化的扰动,其响应速度和调节能力可能相对有限。
- 更广的适用性:
- LADRC:不依赖于被控对象的精确数学模型,仅需一个相对简单的线性化模型即可进行设计。这使得LADRC在处理非线性、时变和不确定性系统时具有更强的适应性和灵活性。
- PID:PID控制器的设计和调参通常需要较为精确的数学模型支持。在模型不准确或系统特性复杂的情况下,PID控制的性能可能受到限制。
- 更好的动态性能:
- LADRC:通过合理的参数设计和调整,LADRC能够实现较小的超调量、较快的响应速度和更小的稳态误差。这些特性使得LADRC在跟踪快速变化的参考信号或应对突发扰动时表现出色。
- PID:PID控制器的动态性能受到其参数设置和系统特性的影响。在某些情况下,PID可能无法同时满足快速响应和小超调量的要求。
原因
- 控制机制的不同:
- LADRC:采用了自抗扰控制的思想,通过估计和补偿系统内外部的扰动来实现对系统的精确控制。这种机制使得LADRC在面对复杂和变化环境时具有更强的适应性和鲁棒性。
- PID:基于误差的反馈控制机制,通过不断调整控制量来减小误差。虽然这种机制简单有效,但在处理复杂系统和强扰动时可能显得力不从心。
- 参数设置的灵活性:
- LADRC:其参数设置相对灵活,可以根据系统的实际需求和特性进行调整。这种灵活性使得LADRC能够适应不同工况下的控制需求。
- PID:PID控制器的参数设置相对固定,且需要一定的调试经验。在某些情况下,PID参数的调整可能较为困难且耗时。
- 算法设计的创新性:
- LADRC:作为一种新型的控制算法,LADRC在设计上充分吸收了传统PID控制的优点,并结合现代控制理论的研究成果进行了创新。这种创新性使得LADRC在多个方面展现出比PID更优越的性能。
- PID:虽然PID控制算法历史悠久且应用广泛,但其在面对复杂系统和强扰动时的局限性也较为明显。随着控制理论的不断发展,PID控制算法也需要不断创新和改进以适应新的控制需求。
进程与线程
一、基本概念
- 进程:进程是操作系统进行资源分配和调度的一个独立单元,是应用程序的运行实例。每个进程都有自己独立的内存空间和系统资源,它可以包含多个线程。进程是程序的一次执行过程,是动态的、有生命周期的。
- 线程:线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的独立运行的单位。线程一般不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但它可与同属一个进程的其他线程共享进程所拥有的全部资源。
二、主要区别
- 资源占用
- 进程:进程是资源分配的基本单位,每个进程拥有独立的内存空间、文件描述符等资源。进程之间的地址空间是相互隔离的,一个进程的崩溃通常不会影响到其他进程。
- 线程:线程是处理器调度的基本单位,线程共享其所属进程的地址空间和资源,如内存、文件句柄等。因此,线程之间的通信和资源共享更加高效和方便。
- 执行开销
- 进程:由于进程拥有独立的资源,因此进程的创建、销毁和切换的开销相对较大。进程之间的通信也需要通过显式的机制,如管道、消息队列等,这些通信方式通常具有较大的开销。
- 线程:由于线程共享进程的资源,线程的创建、销毁和切换的开销相对较小。线程之间的通信可以通过直接读写共享内存的方式进行,因此通信开销也较小。
- 并发性
- 进程:进程可以独立执行,它们之间的并发执行是通过操作系统的调度机制来实现的。多进程系统可以充分利用多核处理器的计算能力,提高系统的整体性能。
- 线程:线程是进程内的一个执行单元,一个进程可以包含多个线程。这些线程可以并发执行,实现多任务处理。多线程可以提高程序的并发性和响应速度。
- 独立性
- 进程:进程是相对独立的执行环境,每个进程都有自己的代码、数据和堆栈空间。进程之间的通信需要通过显式的机制来实现。
- 线程:线程是进程内的一个实体,它不能独立于进程而存在。线程之间的通信和资源共享更加方便和高效。
- 管理复杂性
- 进程:进程的管理相对复杂,包括进程的创建、销毁、调度、通信等。操作系统需要为每个进程分配独立的资源,并管理这些资源的访问和共享。
- 线程:线程的管理相对简单,因为线程共享进程的资源。线程之间的切换和通信也更加高效和方便。然而,线程同步和线程安全是多线程编程中需要特别注意的问题。
三、总结
线程和进程是实现并发和并行的基本单位,它们在资源占用、执行开销、并发性、独立性和管理复杂性等方面存在显著差异。了解这些差异对于编写高效、安全和可靠的程序至关重要。在实际应用中,可以根据具体的需求和场景选择合适的并发和并行方式。线程和进程的区别:1、线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小;2、进程相对独立,需要通过显式机制进行通信,切换开销较大;而线程的管理更为灵活,进程的管理相对复杂。
freeRTOS相关的
问题1、为什么不能使用System tick 分析
这里需要知道,使用了FreeRTOS以后,FreeRTOS 会强制使用systick作为自己的心跳,这个os_tick的优先级是最低的,他主要是处理OS任务调度,时间片查询等工作。在图中这里设置的Timebase是HAL库所使用的基础时钟 hal_tick, 如果设置成也设置成systick,优先级最低,那么
当在高优先级(优先级高于systick)中断服务中调用 HAL_Delay()就会导致错误,当然我们也经常强调不要在中断中使用延时!不要在中断中使用延时!不要在中断中使用延时!
TCB和栈
TCB(Task Control Block)
TCB,即任务控制块,是FreeRTOS中用于保存任务状态、优先级、堆栈信息等关键数据的数据结构。每个任务在FreeRTOS中都会被分配一个唯一的TCB。TCB中包含了任务调度的核心信息,如:
- 栈顶指针(pxTopOfStack):指向任务栈的顶部,用于任务切换时的压栈和出栈操作。
- 状态列表项(xStateListItem):记录TCB在就绪列表、当前运行列表或等待列表中的位置,用于任务调度。
- 优先级(uxPriority):任务的优先级,决定了任务在调度时的优先级顺序。
- 堆栈起始地址(pxStack):指向任务堆栈的起始位置。
- 任务名称(pcTaskName):任务的名称,主要用于调试。
此外,TCB还可能包含其他辅助信息,如互斥信号量个数、临界区嵌套层数等,具体取决于FreeRTOS的配置和版本。
栈(Stack)
栈是任务执行时用于存储临时数据、局部变量和函数调用信息的区域。在FreeRTOS中,每个任务都有自己的栈空间,这个栈空间在任务创建时由系统分配。栈的大小可以根据任务的需求进行配置,但需要注意避免栈溢出,因为这可能导致系统崩溃。
在静态创建任务时,开发者需要在全局范围内为任务分配一个固定大小的栈空间。这个栈空间将用于存储任务执行过程中的所有临时数据和函数调用信息。
总结
在FreeRTOS中,静态创建任务时需要事先分配好TCB和栈。TCB是任务控制块,包含了任务调度的核心信息;栈是任务执行时用于存储临时数据和函数调用信息的区域。通过合理配置TCB和栈,可以确保任务在FreeRTOS中的稳定执行。
内存管理
- 堆,heap,就是一块空闲的内存,需要提供管理函数
- malloc:从堆里划出一块空间给程序使用
- free:用完后,再把它标记为"空闲"的,可以再次使用
- 栈,stack,函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中
- 可以从堆中分配一块空间用作栈
-
文件 优点 缺点 heap_1.c 分配简单,时间确定 只分配、不回收 heap_2.c 动态分配、最佳匹配 碎片、时间不定 heap_3.c 调用标准库函数 速度慢、时间不定 heap_4.c 相邻空闲内存可合并,首次适应算法 可解决碎片问题、时间不定 heap_5.c 在heap_4基础上支持分隔的内存块 可解决碎片问题、时间不定
-
Heap_2之所以还保留,只是为了兼容以前的代码。新设计中不再推荐使用Heap_2。建议使用Heap_4来替代Heap_2,更加高效。
Heap_2也是在数组上分配内存,跟Heap_1不一样的地方在于:
- Heap_2使用最佳匹配算法(best fit)来分配内存
- 它支持vPortFree
-
最佳匹配算法:
- 假设heap有3块空闲内存:5字节、25字节、100字节
- pvPortMalloc想申请20字节
- 找出最小的、能满足pvPortMalloc的内存:25字节
- 把它划分为20字节、5字节
- 返回这20字节的地址
- 剩下的5字节仍然是空闲状态,留给后续的pvPortMalloc使用
-
与Heap_4相比,Heap_2不会合并相邻的空闲内存,所以Heap_2会导致严重的"碎片化"问题。
-
Heap_4使用 首次适应算法(first fit)来分配内存 。它还会把相邻的空闲内存合并为一个更大的空闲内存,这有助于较少内存的碎片问题。
-
Heap_5分配内存、释放内存的算法跟Heap_4是一样的。
相比于Heap_4,Heap_5并不局限于管理一个大数组:它可以管理多块、分隔开的内存。
在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用Heap_5。
既然内存是分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:
- 在使用pvPortMalloc之前,必须先指定内存块的信息
- 使用vPortDefineHeapRegions来指定这些信息
-
阻塞
- 阻塞状态的任务,它在等待"事件",当事件发生时任务就会进入就绪状态。事件分为两类:时间相关的事件、同步事件。
-
-
两个延时函数
-
- 使用vTaskDelay(n)时,进入、退出vTaskDelay的时间间隔至少是n个Tick中断
- 使用xTaskDelayUntil(&Pre, n)时,前后两次退出xTaskDelayUntil的时间至少是n个Tick中断
- 退出xTaskDelayUntil时任务就进入的就绪状态,一般都能得到执行机会
- 所以可以使用xTaskDelayUntil来让任务周期性地运行
-
空闲任务及其钩子函数
- 空闲任务(Idle任务)的作用之一:释放被删除的任务的内存。如果使用vTaskDelete()来删除任务,那么你就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。
- 空闲任务优先级为0:它不能阻碍用户任务运行
- 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
-
们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:
执行一些低优先级的、后台的、需要连续执行的函数测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。空闲任务的钩子函数的限制:不能导致空闲任务进入阻塞状态、暂停状态如果你会使用vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。 -
调度算法
- 调度器永远都是挑选最高优先级的就绪态任务并让它进入运行状态。
- 调度算法的行为主要体现在两方面:高优先级的任务先运行、同优先级的就绪态任务如何被选中。调度算法要确保同优先级的就绪态任务,能"轮流"运行,策略是"轮转调度"(Round Robin Scheduling)。
- 分为可抢占调度模式和合作调度模式、
-
配置项 A B C D E configUSE_PREEMPTION 1 1 1 1 0 configUSE_TIME_SLICING 1 1 0 0 x configIDLE_SHOULD_YIELD 1 0 1 0 x 说明 常用 很少用 很少用 很少用 几乎不用 - A:可抢占+时间片轮转+空闲任务让步
- B:可抢占+时间片轮转+空闲任务不让步
- C:可抢占+非时间片轮转+空闲任务让步
- D:可抢占+非时间片轮转+空闲任务不让步
- E:合作调度
同步与互斥
在团队活动里,同事A先写完报表,经理B才能拿去向领导汇报。经理B必须等同事A完成报表,AB之间有依赖,B必须放慢脚步,被称为同步。在团队活动中,同事A已经使用会议室了,经理B也想使用,即使经理B是领导,他也得等着,这就叫互斥。经理B跟同事A说:你用完会议室就提醒我。这就是使用"同步"来实现"互斥"。
能实现同步、互斥的内核方法有:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)。
内核对象 | 生产者 | 消费者 | 数据/状态 | 说明 |
---|---|---|---|---|
队列 | ALL | ALL | 数据:若干个数据 谁都可以往队列里扔数据, 谁都可以从队列里读数据 | 用来传递数据, 发送者、接收者无限制, 一个数据只能唤醒一个接收者 |
事件组 | ALL | ALL | 多个位:或、与 谁都可以设置(生产)多个位, 谁都可以等待某个位、若干个位;不能传递数据,有广播效果 | 用来传递事件, 可以是N个事件, 发送者、接受者无限制, 可以唤醒多个接收者:像广播 |
信号量 | ALL | ALL | 数量:0~n 谁都可以增加一个数量, 谁都可消耗一个数量,也不能传递数据,核心是“计数值” | 用来维持资源的个数, 生产者、消费者无限制, 1个资源只能唤醒1个接收者 |
任务通知 | ALL | 只有我 | 数据、状态都可以传输, 使用任务通知时, 必须指定接受者,核心是任务的TCB里的数值 | N对1的关系: 发送者无限制, 接收者只能是这个任务 |
互斥量 | 只能A开锁 | A上锁 | 位:0、1 我上锁:1变为0, 只能由我开锁:0变为1,谁获得互斥量就由谁释放互斥量 | 就像一个空厕所, 谁使用谁上锁, 也只能由他开锁 |
队列
使用队列传输数据时有两种方法:
- 拷贝:把数据、把变量的值复制进队列里
- 引用:把数据、把变量的地址复制进队列里
某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。
既然读取队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?
- 优先级最高的任务
- 如果大家的优先级相同,那等待时间最久的任务会进入就绪态
跟读队列类似,一个任务要写队列时,如果队列满了,该任务也可以进入阻塞状态:还可以指定阻塞的时间。如果队列有空间了,则该阻塞的任务会变为就绪态。如果一直都没有空间,则时间到之后它也会进入就绪态。
既然写队列的任务个数没有限制,那么当多个任务写"满队列"时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的空间。当队列中有空间时,哪个任务会进入就绪态?
- 优先级最高的任务
- 如果大家的优先级相同,那等待时间最久的任务会进入就绪态
创建队列:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数 | 说明 |
---|---|
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:以字节为单位 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为内存不足 |
静态创建队列
QueueHandle_t xQueueCreateStatic(*
UBaseType_t uxQueueLength,*
UBaseType_t uxItemSize,*
uint8_t *pucQueueStorageBuffer,*
StaticQueue_t *pxQueueBuffer*
);
参数 | 说明 |
---|---|
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:以字节为单位 |
pucQueueStorageBuffer | 如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组, 此数组大小至少为"uxQueueLength * uxItemSize" |
pxQueueBuffer | 必须执行一个StaticQueue_t结构体,用来保存队列的数据结构 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为pxQueueBuffer为NULL |
写队列:
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
参数 | 说明 |
---|---|
xQueue | 队列句柄,要写哪个队列 |
pvItemToQueue | 数据指针,这个数据的值会被复制进队列, 复制多大的数据?在创建队列时已经指定了数据大小 |
xTicksToWait | 如果队列满则无法写入新数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法写入数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写 |
返回值 | pdPASS:数据成功写入了队列 errQUEUE_FULL:写入失败,因为队列满了。 |
读队列:使用 xQueueReceive() 函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);
参数 | 说明 |
---|---|
xQueue | 队列句柄,要读哪个队列 |
pvBuffer | bufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小 |
xTicksToWait | 果队列空则无法读出数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写 |
返回值 | pdPASS:从队列读出数据入 errQUEUE_EMPTY:读取失败,因为队列空了。 |
队列集
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength )
参数 | 说明 |
---|---|
uxQueueLength | 队列集长度,最多能存放多少个数据(队列句柄) |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列NULL:失败,因为内存不足 |
读取队列集
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,
TickType_t const xTicksToWait );
参数 | 说明 |
---|---|
xQueueSet | 队列集句柄 |
xTicksToWait | 如果队列集空则无法读出数据,可以让任务进入阻塞状态,xTicksToWait表示阻塞的最大时间(Tick Count)。如果被设为0,无法读出数据时函数会立刻返回;如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写 |
返回值 | NULL:失败,队列句柄:成功 |
信号量
- 信号:起通知作用
- 量:还可以用来表示资源的数量
- 当"量"没有限制时,它就是"计数型信号量"(Counting Semaphores)
- 当"量"只有0、1两个取值时,它就是"二进制信号量"(Binary Semaphores)
- 支持的动作:"give"给出资源,计数值加1;"take"获得资源,计数值减1
-
差异列表如下:
队列 信号量 可以容纳多个数据, 创建队列时有2部分内存: 队列结构体、存储数据的空间 只有计数值,无法容纳其他数据。 创建信号量时,只需要分配信号量结构体 生产者:没有空间存入数据时可以阻塞 生产者:用于不阻塞,计数值已经达到最大时返回失败 消费者:没有数据时可以阻塞 消费者:没有资源时可以阻塞
两种信号量的对比
信号量的计数值都有限制:限定了最大值。如果最大值被限定为1,那么它就是二进制信号量;如果最大值不是1,它就是计数型信号量。
差别列表如下:
二进制信号量 | 技术型信号量 |
---|---|
被创建时初始值为0 | 被创建时初始值可以设定 |
其他操作是一样的 | 其他操作是一样的 |
创建
使用信号量之前,要先创建,得到一个句柄;使用信号量时,要使用句柄来表明使用哪个信号量。 对于二进制信号量、计数型信号量,它们的创建函数不一样:
二进制信号量 | 计数型信号量 | |
---|---|---|
动态创建 | xSemaphoreCreateBinary 计数值初始值为0 | xSemaphoreCreateCounting |
vSemaphoreCreateBinary(过时了) 计数值初始值为1 | ||
静态创建 | xSemaphoreCreateBinaryStatic | xSemaphoreCreateCountingStatic |
give/take
在任务中使用 | 在ISR中使用 | |
---|---|---|
give | xSemaphoreGive | xSemaphoreGiveFromISR |
take | xSemaphoreTake | xSemaphoreTakeFromISR |
pxHigherPriorityTaskWoken的函数原型如下:
BaseType_t xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);
xSemaphoreGiveFromISR函数的参数与返回值列表如下:
参数 | 说明 |
---|---|
xSemaphore | 信号量句柄,释放哪个信号量 |
pxHigherPriorityTaskWoken | 如果释放信号量导致更高优先级的任务变为了就绪态, 则*pxHigherPriorityTaskWoken = pdTRUE |
返回值 | pdTRUE表示成功, 如果二进制信号量的计数值已经是1,再次调用此函数则返回失败; 如果计数型信号量的计数值已经是最大值,再次调用此函数则返回失败 |
互斥量
它的核心在于:谁上锁,就只能由谁开锁。
在多任务系统中,任务A正在使用某个资源,还没用完的情况下任务B也来使用的话,就可能导致问题。比如对于串口,任务A正使用它来打印,在打印过程中任务B也来打印,客户看到的结果就是A、B的信息混杂在一起。
- 对变量的非原子化访问
修改变量、设置结构体、在16位的机器上写32位的变量,这些操作都是非原子的。也就是它们的操作过程都可能被打断,如果被打断的过程有其他任务来操作这些变量,就可能导致冲突。
- 函数重入
"可重入的函数"是指:多个任务同时调用它、任务和中断同时调用它,函数的运行也是安全的。可重入的函数也被称为"线程安全"(thread safe)。
上述问题的解决方法是:任务A访问这些全局变量、函数代码时,独占它,就是上个锁。这些全局变量、函数代码必须被独占地使用,它们被称为临界资源。
互斥量也被称为互斥锁,使用过程如下:
- 互斥量初始值为1
- 任务A想访问临界资源,先获得并占有互斥量,然后开始访问
- 任务B也想访问临界资源,也要先获得互斥量:被别人占有了,于是阻塞
- 任务A使用完毕,释放互斥量;任务B被唤醒、得到并占有互斥量,然后开始访问临界资源
- 任务B使用完毕,释放互斥量
正常来说:在任务A占有互斥量的过程中,任务B、任务C等等,都无法释放互斥量。 但是FreeRTOS未实现这点:任务A占有互斥量的情况下,任务B也可释放互斥量。
要注意的是,互斥量不能在ISR中使用。
优先级继承:
- 假设持有互斥锁的是任务A,如果更高优先级的任务B也尝试获得这个锁
- 任务B说:你既然持有宝剑,又不给我,那就继承我的愿望吧
- 于是任务A就继承了任务B的优先级
- 这就叫:优先级继承
- 等任务A释放互斥锁时,它就恢复为原来的优先级
- 互斥锁内部就实现了优先级的提升、恢复
事件组
事件组可以简单地认为就是一个整数:
- 的每一位表示一个事件
- 每一位事件的含义由程序员决定,比如:Bit0表示用来串口是否就绪,Bit1表示按键是否被按下
- 这些位,值为1表示事件发生了,值为0表示事件没发生
- 一个或多个任务、ISR都可以去写这些位;一个或多个任务、ISR都可以去读这些位
- 可以等待某一位、某些位中的任意一个,也可以等待多位
事件组用一个整数来表示,其中的高8位留给内核使用,只能用其他的位来表示事件。那么这个整数是多少位的?
- 如果configUSE_16_BIT_TICKS是1,那么这个整数就是16位的,低8位用来表示事件
- 如果configUSE_16_BIT_TICKS是0,那么这个整数就是32位的,低24位用来表示事件
- configUSE_16_BIT_TICKS是用来表示Tick Count的,怎么会影响事件组?这只是基于效率来考虑
- 如果configUSE_16_BIT_TICKS是1,就表示该处理器使用16位更高效,所以事件组也使用16位
- 如果configUSE_16_BIT_TICKS是0,就表示该处理器使用32位更高效,所以事件组也使用32位
-
事件组和队列、信号量等不太一样,主要集中在2个地方:
- 唤醒谁?
- 队列、信号量:事件发生时,只会唤醒一个任务
- 事件组:事件发生时,会唤醒所有符号条件的任务,简单地说它有"广播"的作用
- 是否清除事件?
- 队列、信号量:是消耗型的资源,队列的数据被读走就没了;信号量被获取后就减少了
- 事件组:被唤醒的任务有两个选择,可以让事件保留不动,也可以清除事件
-
任务通知
- 我们使用队列、信号量、事件组等等方法时,并不知道对方是谁。使用任务通知时,可以明确指定:通知哪个任务。
-
任务通知的优势:
- 效率更高:使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都有大的优势。
- 更节省内存:使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。
-
任务通知的限制:
- 不能发送数据给ISR:
- ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知的功能,发数据给任务。
- 数据只能给该任务独享
- 使用队列、信号量、事件组时,数据保存在这些结构体中,其他任务、ISR都可以访问这些数据。使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。
- 在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把一个数据源的数据发给多个任务。
- 无法缓冲数据
- 使用队列时,假设队列深度为N,那么它可以保持N个数据。
- 使用任务通知时,任务结构体中只有一个任务通知值,只能保持一个数据。
- 无法广播给多个任务
- 使用事件组可以同时给多个任务发送事件。
- 使用任务通知,只能发个一个任务。
- 如果发送受阻,发送方无法进入阻塞状态等待
- 假设队列已经满了,使用 xQueueSendToBack() 给队列发送数据时,任务可以进入阻塞状态等待发送完成。
- 使用任务通知时,即使对方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。
软件定时器
在FreeRTOS里,我们也可以设置无数个"软件定时器",它们都是基于系统滴答中断(Tick Interrupt)。
- 指定类型,定时器有两种类型:
- 一次性(One-shot timers): 这类定时器启动后,它的回调函数只会被调用一次; 可以手工再次启动它,但是不会自动启动它。
- 自动加载定时器(Auto-reload timers ): 这类定时器启动后,时间到之后它会自动启动它; 这使得回调函数被周期性地调用。
- 指定要做什么事,就是指定回调函数
- 我们自己编写的任务函数要使用定时器时,是通过"定时器命令队列"(timer command queue)和守护任务交互
中断管理
ISR是在内核中被调用的,ISR执行过程中,用户的任务无法执行。ISR要尽量快,否则:
- 其他低优先级的中断无法被处理:实时性无法保证
- 用户任务无法被执行:系统显得很卡顿
如果这个硬件中断的处理,就是非常耗费时间呢?对于这类中断的处理就要分为2部分:
- ISR:尽快做些清理、记录工作,然后触发某个任务
- 任务:更复杂的事情放在任务中处理
- 所以:需要ISR和任务之间进行通信
要在FreeRTOS中熟练使用中断,有几个原则要先说明:
- FreeRTOS把任务认为是硬件无关的,任务的优先级由程序员决定,任务何时运行由调度器决定
- ISR虽然也是使用软件实现的,但是它被认为是硬件特性的一部分,因为它跟硬件密切相关
- 何时执行?由硬件决定
- 哪个ISR被执行?由硬件决定
- ISR的优先级高于任务:即使是优先级最低的中断,它的优先级也高于任务。任务只有在没有中断的情况下,才能执行。
为什么要引入两套API函数?
- 很多API函数会导致任务计入阻塞状态:
- 运行这个函数的 任务 进入阻塞状态
- 比如写队列时,如果队列已满,可以进入阻塞状态等待一会
- ISR调用API函数时,ISR不是"任务",ISR不能进入阻塞状态
- 所以,在任务中、在ISR中,这些函数的功能是有差别的
- 在任务、ISR中调用时,需要的参数不一样,比如:
- 在任务中调用:需要指定超时时间,表示如果不成功就阻塞一会
- 在ISR中调用:不需要指定超时时间,无论是否成功都要即刻返回
两套API函数列表
类型 | 在任务中 | 在ISR中 |
---|---|---|
队列(queue) | xQueueSendToBack | xQueueSendToBackFromISR |
xQueueSendToFront | xQueueSendToFrontFromISR | |
xQueueReceive | xQueueReceiveFromISR | |
xQueueOverwrite | xQueueOverwriteFromISR | |
xQueuePeek | xQueuePeekFromISR | |
信号量(semaphore) | xSemaphoreGive | xSemaphoreGiveFromISR |
xSemaphoreTake | xSemaphoreTakeFromISR | |
事件组(event group) | xEventGroupSetBits | xEventGroupSetBitsFromISR |
xEventGroupGetBits | xEventGroupGetBitsFromISR | |
任务通知(task notification) | xTaskNotifyGive | vTaskNotifyGiveFromISR |
xTaskNotify | xTaskNotifyFromISR | |
软件定时器(software timer) | xTimerStart | xTimerStartFromISR |
xTimerStop | xTimerStopFromISR | |
xTimerReset | xTimerResetFromISR | |
xTimerChangePeriod | xTimerChangePeriodFromISR |
嵌入式通信协议相关的
波特率
波特率是指单位时间内传送二进制数据的位数,单位用bps(位/秒)表示,记作波特。
一个字节多少位
1字节(byte) = 8位(bit)
- 在16位的系统中(比如8086微机) 1字 (word)= 2字节(byte)= 16(bit)
- 在32位的系统中(比如win32) 1字(word)= 4字节(byte)=32(bit)
- 在64位的系统中(比如win64)1字(word)= 8字节(byte)=64(bit)
另外,只有char类型是被规定为8位,其他数据类型都没有被强制规定具体位数
C类型 | 32 | 64 |
---|---|---|
char | 1 | 1 |
short int | 2 | 2 |
int | 4 | 4 |
long int | 4 | 8 |
long long int | 8 | 8 |
char* | 4 | 8 |
float | 4 | 4 |
double | 8 | 8 |