1.cubemx生成文件的时候可以勾选三种保存文件的方式
将HAL库的所有.C和.H都复制到所建工程中
优点:这样如果后续需要新增其他外设又可能不再用STM32CubeMX的时候便会很方便
缺点: 体积大,编译时间长(很长)
只复制所需要的.C和.H (推荐)
优点:体积相对小,编译时间短,并且工程可复制拷贝
缺点: 新增外设时需要重新用STM32CubeMX导入
不复制文件,直接从软件包存放位置导入.C和.H
优点:体积小,比较节约硬盘空间
缺点: 复制到其他电脑上或者软件包位置改变,就需要修改相对应的路径
自行选择方式即可
看门狗:看门狗实际上就是一种定时计数器,窗口看门狗,独立看门狗。
看门狗是能之后就一直计数,每次喂狗相当于重新计数,如果看门狗计数值超过了一个预定值,也就是一段时间没有喂狗,mcu就挂了,这时候发送复位信号让mcu复位重启。
3.中断
打断原来正在执行的任务,转而执行紧急任务,紧急任务执行完之后返回来继续执行之前没有执行完的任务。
作用:高效处理紧急程序,并且不会占用CPU资源。
main函数正常使用cpu资源,当中断发生时,cpu使用权被强占,main函数的优先级最低。
优先级:
抢占优先级
响应优先级
自然优先级
抢占优先级高的先进入中断,抢占优先级相同的任务,响应优先级高的先进入中断,但是不能相互打断;抢占优先级和响应优先级都相同的任务,自然优先级越高的先执行。自然优先级是中断向量表里的优先级。
中断(NVIC)里的寄存器
主要用下面四个
中断使能寄存器ISER、中断失能寄存器ICER、应用程序中断及复位控制寄存器AIRCR、中断优先级寄存器IPR
中断使能寄存器ISER、中断失能寄存器ICER这两个寄存器控制中断的打开和关闭,应用程序中断及复位控制寄存器AIRCR这个寄存器控制中断优先级的分组,也就是设定中断优先级寄存器中的高四位里有几位是抢占优先级哪几位是相应优先级。中断优先级寄存器IPR这个寄存器用于控制中断的优先级。
4.单片机启动流程
1.初始化堆栈指针 SP=_initial_sp
2.初始化 PC 指针=Reset_Handler
3.初始化中断向量表
4.配置系统时钟
5.调用 C 库函数_main 初始化用户堆栈,然后进入 main 函数。
5.
ram内的结构
RAM分为静态常量区、栈区和堆区
STACK
存放函数内局部变量,形参,函数运行结束后自动释放
HEAP
存放由malloc/new开辟的空间,自由使用,但必须通过free/delete释放
静态常量区
内存在程序编译的时候已经分配好,主要存放全局数据(初始化&未初始化)和常量
7.flash ram code rodata rwdata zidata
flash存储code、rodata和rwdata
ram存储rwdata zidata
flash断电后数据依旧存在,因此适合保存需要长时间存储的数据,比如编译后的代码(这里的代码中,被调用的函数才会被放入code里,没有调用的函数不会放入code),常量(定义的全局常量只有被调用后才会加入到RO-data中)。ram断电后不会保留数据,适合存储需要频繁使用的数据,已赋值的变量rwdata(RW-data(读写数据)代表被调用的已经初始且初始化不为0的全局变量,RW-data会先被存储到flash里面,等待程序运行的时候,再被复制到RAM中,所以RW-data既占用一份flash,也会占用一份RAM。)和未初始化的变量zidata。初始化时会把rwdata从flash拷贝到ram里。
8.threadx初始化流程
- 设置堆栈
- 初始化中断向量表
- 进入复位入口程序(首先进行系统级的初始化,再进入__main函数)
- 链接c库中的_main,进入main函数
9.用户如果自定义threadx,在tx_user.h有一些配置选项可供选择。
thread_ptr:线程控制块指针
name_ptr:指向线程名称的指针
entry_function:线程的入口函数,当线程从此入口函数返回时,它将处于完成状态并无限期挂起。
entry_input:一个32位值,该值在首次执行时传递给线程的入口函数。此输入的使用完全由应用程序确定。
preempt_threshold:禁用的抢占的最高优先级(0到(TX_MAX_PRIORITIES-1))。只有高于此级别的优先级才可以抢占该线程。该值必须小于或等于指定的优先级。等于线程优先级的值将禁用抢占阈值。
time_slice:允许在同一优先级的其他就绪线程运行之前,允许该线程运行的计时器计数。请注意,使用抢占阈值将禁用时间片。合法的时间片值范围是1到0xFFFFFFFF(包括0)。值为TX_NO_TIME_SLICE(值为0)禁用此线程的时间切片。\
auto_start:指定线程是立即启动还是处于挂起状态。有效选项为TX_AUTO_START (0x01)和TX_DONT_START(0x00)。如果指定了TX_DONT_START,则应用程序以后必须调用tx_thread_resume才能运行线程。
在tx_application_defination中创建线程,也可以在运行时创建线程
cpp
UINT tx_thread_create(
TX_THREAD *thread_ptr,
CHAR *name_ptr,
VOID (*entry_function)(ULONG),
ULONG entry_input,
VOID *stack_start,
ULONG stack_size,
UINT priority,
UINT preempt_threshold,
ULONG time_slice,
UINT auto_start);
在线程内部删除线程,只能删除处于终止状态或者完成状态的线程
cpp
UINT tx_thread_delete(TX_THREAD *thread_ptr);
14.线程进入或者退出时通知应用程序
这个服务需要调用一个通知回调函数,当进入或者退出这个函数的时候调用这个通知回调函数
cpp
UINT tx_thread_entry_exit_notify(
TX_THREAD *thread_ptr,
VOID (*entry_exit_notify)(TX_THREAD *, UINT));
15.查询当前正在执行的应用程序
cpp
TX_THREAD* tx_thread_identify(VOID);
如果从ISR调用此服务,则返回值表示在执行中断处理程序之前运行的线程。
16.获取指定线程信息
-
参数
-
thread_ptr指向线程控制块的指针。
-
name指向目标的指针,该目标指向线程名称。
-
state指向线程当前执行状态的目标的指针。可能的值如下。
- TX_READY(0x00)
- TX_COMPLETED(0x01)
- TX_TERMINATED(0x02)
- TX_SUSPENDED(0x03)
- TX_SLEEP(0x04)
- TX_QUEUE_SUSP(0x05)
- TX_SEMAPHORE_SUSP(0x06)
- TX_EVENT_FLAG(0x07)
- TX_BLOCK_MEMORY(0x08)
- TX_BYTE_MEMORY(0x09)
- TX_MUTEX_SUSP(0x0D)
-
run_count指向目标的线程运行计数,也就是被调用的次数。
-
**priority **指向目标的线程优先级。
-
preemption_threshold指向线程的抢占阈值的指针。
-
time_slice指向线程时间切片的指针。
-
next_thread指向下一个创建的线程指针的指针。
-
suspend_thread 指向目标的指针,该指针指向指向暂挂列表中下一个线程的指针,如果线程当前被挂起,这个指针将指向它自己,否则为NULL。
注意:为任何参数提供TX_NULL表示该参数不是必需的。
-
cpp
UINT tx_thread_info_get(
TX_THREAD *thread_ptr,
CHAR **name,
UINT *state,
ULONG *run_count,
UINT *priority,
UINT *preemption_threshold,
ULONG *time_slice,
TX_THREAD **next_thread,
TX_THREAD **suspended_thread);
17.更改线程的抢占阈值
抢占阈值可防止等于或小于抢占阈值的线程抢占指定线程。
使用抢占阈值会禁用指定线程的时间片。
cpp
UINT tx_thread_priority_change(
TX_THREAD *thread_ptr,
UINT new_priority,
UINT *old_priority);
18.更改线程的优先级
该服务更改指定线程的优先级。有效优先级的范围是0到(TX_MAX_PRIORITES-1),其中0表示最高优先级。
注意:指定线程的抢占阈值将自动设置为新优先级。如果需要新的阈值,则必须在此调用之后使用 tx_thread_preemption_change*服务
cpp
UINT tx_thread_priority_change(
TX_THREAD *thread_ptr,
UINT new_priority,
UINT *old_priority);
19.释放线程控制权
-
此服务放弃处理器的控制权,转交给其他相同或更高优先级的处于就绪状态的线程
-
除了将控制权放弃给相同优先级的线程之外,此服务还将控制权放弃给由于当前线程的抢占阈值设置而阻止执行的最高优先级线程。
cpp
VOID tx_thread_relinquish(VOID);
20.重置线程
此服务将指定的线程重置为在线程创建时定义的入口点执行。线程必须处于TX_COMPLETED 或TX_TERMINATED状态才能重置。
必须恢复(resume)该线程以使其再次执行 ,也就是说如果想让处于终止状态或者完成状态的线程重新执行,需要先重置在唤醒。
cpp
UINT tx_thread_reset(TX_THREAD *thread_ptr);
- 恢复线程
该服务将恢复或准备执行先前由tx_thread_suspend调用暂停的线程。此外,此服务将恢复在没有自动启动的情况下创建的线程。
thread_ptr指向挂起的应用程序线程的指针。
cpp
UINT tx_thread_resume(TX_THREAD *thread_ptr);
22.线程挂起
此服务挂起指定的应用程序线程。线程可以调用此服务来挂起自身。
挂起后,必须由tx_thread_resume恢复该线程才能再次执行。
注意: 在ThreadX中,如果一个线程已经因为某种原因(如等待信号量、事件标志或是因为调用了tx_thread_suspend
)被挂起,那么在它被恢复之前,任何尝试再次挂起它的操作都不会改变它的状态。这是因为线程的挂起状态是一个布尔值,一旦设置,就表示线程不能执行,直到它被明确地恢复。重复的挂起请求在逻辑上没有意义,因为线程已经不能执行了。只有当线程恢复并尝试重新调度时,系统才会检查是否有进一步的挂起请求。如果存在,线程将再次被挂起。
cpp
UINT tx_thread_suspend(TX_THREAD *thread_ptr);
23.线程挂起指定时间
此服务使调用线程在指定的计时器刻度数内挂起。与计时器刻度相关的物理时间量是特定于应用程序的。只能从应用程序线程调用此服务。
cpp
UINT tx_thread_sleep(ULONG timer_ticks);
24.中止某些线程的挂起状态(不是显式挂起)
描述
此服务中止睡眠或指定线程的任何其他对象挂起。如果等待被中止,则线程正在等待的服务将返回TX_WAIT_ABORTED值。
注意:该服务不会释放由tx_thread_suspend服务进行的显式挂起。
cpp
UINT tx_thread_wait_abort(TX_THREAD *thread_ptr);
25.抢占阈值是threadx独有的特性
裸机编程的时候,也就是单任务的时候,常常会遇到紧急事件等待非紧急事件执行的情况,这也是裸机编程的缺点,因此要用操作系统调度实现多任务系统。
26.任务栈和系统栈
任务栈是每个线程定义的时候作为参数的那个栈,系统栈是在启动函数中和堆一起设置大小的栈。任务栈不使用系统栈中的大小。
27.threadx的空闲任务
threadx中没有空闲任务,但是我们可以自己创建一个
作用:threadx操作系统不能总是执行应用任务,这样的话会一直处在一个超负荷运行的状态,为了降低功耗,在空闲任务中停机、睡眠等。
28.系统节拍
每个操作系统都需要一个系统节拍,在threadx中,系统节拍在low_level.s低级初始编译文件中进行定义,和系统节拍相关的是系统时钟,系统节拍就是系统周期性中断的频率,根据这个周期性中断的频率,操作系统就能处理有关时间的操作,比如延时比如超时。
29.任务之间的通讯与同步机制
时间标志组:
为什么要用事件标志组不用全局变量
- 时间标志组可以解决中断服务函数和任务之间的同步问题
- 时间标志组可以解决线程之间的访问冲突问题
- 事件标志组可以有效的管理任务,超时等机制不需要用户额外编写
互斥信号量(互斥锁):
二值信号量解决不了优先级翻转的问题,互斥信号量可以通过优先级继承来解决优先级翻转的问题。
互斥信号量仅在threadx的任务中可以使用,不能再中断函数中使用。
获取互斥信号量会让所有权计数器+1;释放信号量所有权会让所有权计数器-1。
互斥锁挂起列表:
当一个互斥锁被获取,其他线程现在想获取这个互斥锁,就会按照先入先出的顺序进入互斥锁挂起列表表,等到当前拥有互斥锁的线程释放互斥锁所有权,按照先入先出的顺序调用挂起列表的第一个线程获得互斥锁所有权。
threadx还提供了另一种方式确定互斥锁挂起顺序,按照优先级由高到低的顺序互斥锁挂起顺序。
死锁:线程都在等待对方释放互斥锁,就是死锁。
避免死锁的方法:
保证线程按照相同的顺序获取互斥锁;
设置互斥锁最大等待时间
尽量让互斥锁的数量少,减少复杂度
让每个线程最多拥有一个互斥锁
到互斥锁被删除时,所有之前因为等待互斥锁所有权释放而在互斥锁挂起列表中的函数都会从挂起状态回到就绪状态,之前的互斥锁获取函数都返回一个互斥锁删除返回值TX_DELETED。
内存管理:
threadx有两种内存分配模式,内存池和内存块池,内存池可以根据需求分配内存大小,但是会产生内存碎片,内存块池则避免碎片,但固定大小限制了灵活性。
内存池挂起列表:申请的内存如果内存池大小不足时,需要释放空间,因此有内存池挂起列表,等待申请的内存被释放后,腾出新的空间之后根据内存池挂起列表逐步分配内存。
字节池大小申请的大小和实际能够使用的大小有些出入,这是因为实际申请的内存大小里还包含了对内存管理的大小,两个指针大小,一般包括所有者指针和下一个内存块指针。所有者指针,这通常指的是指向当前拥有或使用该内存块的实体(如线程、进程或对象)的指针。这个指针允许内存管理器快速确定哪个线程或进程正在使用特定的内存块,这对于内存的分配和回收非常重要。
申请的内存池被分成了两个部分,一个是大的空闲块,一个是小的末端内存块,这个末端内存块可以避免内存合并时的内存池末端检查。
随着池的碎片化,开销也会增加。
内存池删除时,内存池挂起的所有线程都会恢复,返回Tx_Deleted状态。
碎片整理:当一个线程需要使用一部分内存时,如果找到的第一个空闲内存块大小不足,会继续向后搜索空闲内存块大小,并合并内存块。
内存块池优于字节池
内存块池为了避免过度的内存浪费,可以申请好几种内存块大小不同的内存块池。
系统节拍和应用程序计时器
应用程序计时器可根据设定的时间来执行任务,应用程序计时器的线程优先级是一个默认的最高优先级(0),因此需要降低时间到期函数之中的函数复杂度,也不能在中断到期函数中挂起任何任务,这些都会导致系统时钟节拍的不准确。
信号量:
有两种操作可以影响信号量的值,put增加1,get减少1。如果信号量已经是0,再get就会失败。
二值信号量:
二值信号量类似互斥锁,(但是互斥锁可以解决优先级反转的问题),二值信号量也是只有0和1。1表示资源可用,0表示资源被占用了。和互斥锁一样,二值信号量可以处理资源访问冲突问题,如果有一个线程要访问一个资源,必须先执行p操作,二值信号量从1变成0。访问结束后,会执行v操作,二值信号量从0变成1。当一个线程尝试访问一个值为0的二值信号量控制的资源,这时候会阻塞,知道二值信号量变为1。
二值信号量和互斥锁:
二值信号量速度快于互斥锁
二值信号量没有互斥锁的所有权概念,互斥锁是谁获取资源谁才拥有自己解锁的权利,如果自己不解锁,别的线程永远不能获得这个资源。二值信号量是每个线程都有权利执行v操作。也就是因为这一点的不同,二值信号量无法处理优先级翻转。
利用信号量实现同步
现在有一个线程1和线程2,要实现线程同步,现在线程1运行过程中,因为等待线程2的中断函数释放对二值信号量的控制权而挂起,线程2释放了控制权后获得二值信号量,这时候线程1就可以继续运行。实现了同步。
为什么使用消息队列不用全局数组
- 使用消息队列可以有效管理线程,而全局数组无法做到,线程的超时机制需要用户自己去实现。
- 使用全局数组要防止多线程的访问冲突,而使用消息队列则处理好了这个问题,用户无需担心。
- 使用消息队列可以有效地解决中断服务程序与线程之间消息传递的问题。
- FIFO 机制更有利于数据的处理。