
快速入门 $ O4 a- @) |+ S0 C 可以去官方有相关文档说明; F7 @; J; ^2 |7 N 首先将KEIL下载下来,官网上有相关例程 工程说明, y3 X( j6 u4 y3 ? # |. G* L5 ~( O K5 p3 m ![]() ![]() 3 x( }- I1 Q5 Z' l7 i( b ![]() 调试命令6 @# q5 N; W0 H* |( U* P 打开调试公共点击运行(F5)代码,打开串口UART#1,点击Tab键或者help+回车,查看系统支持的命令。5 ^8 g7 ^7 u% |$ D+ J ![]() 8 r# ]! `# b7 U) C ![]() % u0 q# K8 y* O, e4 v 系统启动步骤 以 MDK-ARM 为例,MDK-ARM 的用户程序入口为 main() 函数,位于 main.c 文件中。系统启动后先从汇编代码 startup_stm32f103xe.s 开始运行,然后跳转到 C 代码,进行 RT-Thread 系统功能初始化,最后进入用户程序入口 main()。2 o7 Q8 `) F) I% O# R4 ~+ E ![]() 2 T+ X$ [( h, a4 V5 m 如果使用到了ARM内核则重新定义启动当时,最终跳到了rtthread_startup(); 是RT-Thread规定的统一启动入口。 启动程序函数初始化如下图所示: ![]() 启动代码分为四部分: ![]() 用户入口代码# P- ]( T) V+ w0 t0 x3 l 为了在进入 main 程序之前,完成系统功能初始化,可以使用 $sub$$ 和 $super$$ 函数标识符在进入主程序之前调用另外一个例程,这样可以让用户不用去管 main() 之前的系统初始化操作。, o0 r8 J) c7 p3 o% G4 m0 s3 t 内核基础 内核的组成部分、系统如何启动、内存分布情况以及内核配置方法。. S: a @6 `" s* q+ g/ u 7 B9 s1 t% h+ `, K9 S' N; C ![]() 1 p, y! K3 ~/ u& |6 a 实时内核的实现包括:对象管理、线程管理及调度器、线程间通信管理、时钟管理及内存管理等等,内核最小的资源占用情况是 3KB ROM,1.2KB RAM。- R5 ^- z3 i: W: i 0 q8 |# P( D9 {8 J7 ~" o 线程调度# S% W% ^ H2 d) P6 }: t 线程是 RT-Thread 操作系统中最小的调度单位,线程调度算法是基于优先级的全抢占式多线程调度算法,即在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。 : P: Q2 u' U2 \( N5 Y! k 时钟管理6 G9 F6 K, K6 Z3 |' ], f7 V) M" o& Z RT-Thread 的时钟管理以时钟节拍为基础,时钟节拍(滴答始终)是 RT-Thread 操作系统中最小的时钟单位。RT-Thread 的定时器提供两类定时器机制:第一类是单次触发定时器,这类定时器在启动后只会触发一次定时器事件,然后定时器自动停止。第二类是周期触发定时器,这类定时器会周期性的触发定时器事件,直到用户手动的停止定时器否则将永远持续执行下去。/ ?$ P( ~/ }# C5 F/ [4 W ' j: L" E9 i7 K 另外,根据超时函数执行时所处的上下文环境,RT-Thread 的定时器可以设置为 HARD_TIMER 模式或者 SOFT_TIMER 模式。 通常使用定时器定时回调函数(即超时函数),完成定时服务。用户根据自己对定时处理的实时性要求选择合适类型的定时器。% D# B; J8 K: W7 i; B9 m 6 O w9 E7 ~; s; c . Q4 v# I* F+ [" c: q: b 线程间同步 RT-Thread 采用信号量、互斥量与事件集实现线程间同步。线程通过对信号量、互斥量的获取与释放进行同步;互斥量采用优先级继承的方式解决了实时系统常见的优先级翻转问题,信号量会导致线程阻塞即当低优先级持有信号量时,高优先级无法获取信号量。 ![]() ( a( o" |) S7 Q3 [6 g3 L" [ 线程间通信/ F7 X) o$ r- @$ n- Q RT-Thread 支持邮箱和消息队列等通信机制。邮箱中一封邮件的长度固定为 4 字节大小;消息队列能够接收不固定长度的消息,并把消息缓存在自己的内存空间中。邮箱效率较消息队列更为高效。邮箱和消息队列的发送动作可安全用于中断服务例程中。通信机制支持线程按优先级等待或按先进先出方式获取。 9 A" C/ H2 [% o+ Q5 l4 |7 \& `- t: N i ' ]& ~$ V1 P% E/ j! P 内存管理+ F: G7 a( O6 W RT-Thread 支持静态内存池管理及动态内存堆管理。当静态内存池具有可用内存时,系统对内存块分配的时间将是恒定的;当静态内存池为空时,系统将申请内存块的线程挂起或阻塞掉 (即线程等待一段时间后仍未获得内存块就放弃申请并返回,或者立刻返回。等待的时间取决于申请内存块时设置的等待时间参数),当其他线程释放内存块到内存池时,如果有挂起的待分配内存块的线程存在的话,则系统会将这个线程唤醒。 ) s) x* Q& m6 |- V$ q. k6 ? 动态内存堆管理模块在系统资源不同的情况下,分别提供了面向小内存系统的内存管理算法及面向大内存系统的 SLAB 内存管理算法。( l$ Q) f) M, s* k. o3 e S( S1 J) @3 w( B7 t 还有一种动态内存堆管理叫做 memheap,适用于系统含有多个地址且不连续的内存堆。使用 memheap 可以将多个内存堆 “粘贴” 在一起,让用户操作起来像是在操作一个内存堆。: z! Y( r) C/ F0 j6 i. ` I/O设备管理# p( p) K4 P% v$ D: L* G RT-Thread 将 PIN、I2C、SPI、USB、UART 等作为外设设备,统一通过设备注册完成。实现了按名称访问的设备管理子系统,可按照统一的 API 界面访问硬件设备。在设备驱动接口上,根据嵌入式系统的特点,对不同的设备可以挂接相应的事件。当设备事件触发时,由驱动程序通知给上层的应用程序。" W* E2 p8 P. h8 s2 u : G# j" D/ n' J0 {+ F+ H 程序内存分布7 G+ r1 @) a4 H1 n 一般MCU包含的存储空间有:片内Flash(硬盘)与片内RAM(内存)。编译器会将一个程序分类为好几个部分,分别存储在 MCU 不同的存储区。* I5 V% e/ d: ? Keil工程在编译完成后,会相应的程序提示占用空间, A' a& n; s5 h ![]() 7 a+ ^* Q( _& v* v* K% a6 h 1.Code:代码段,存放程序的代码部分; 2.RO-data:只读数据段,存放程序中定义的常量;9 n( z- E8 w4 l/ f+ Y, B 3.RW-data:读写数据段,存放初始化为非 0 值的全局变量;+ F0 N9 u6 ^) J/ z 4.ZI-data:0 数据段,存放未初始化的全局变量及初始化为 0 的变量; 编译完后工程会生成一个.map文件,说明了各个函数占用的吃尺寸和地址,在文件最后几行与场面的关系。: \8 ?0 u6 n6 E7 F7 i) V' M+ o 3 C$ P8 u6 `( A; L ![]() 1.RO Size 包含了 Code 及 RO-data,表示程序占用 Flash 空间的大小; 2.RW Size 包含了 RW-data 及 ZI-data,表示运行时占用的 RAM 的大小; 3.ROM Size 包含了 Code、RO-data 以及 RW-data,表示烧写程序所占用的 Flash 空间的大小; Z/ {5 i" D+ ]: H- l J $ u2 N9 b; j/ ~9 u( ?6 _5 x% v 5 S' ^' C t% G- u( r$ J+ S STM32 在上电启动之后默认从 Flash 启动,启动之后会将 RW 段中的 RW-data(初始化的全局变量)搬运到 RAM 中,但不会搬运 RO 段,即 CPU 的执行代码从 Flash 中读取,另外根据编译器给出的 ZI 地址和大小分配出 ZI 段,并将这块 RAM 区域清零。% E, `1 T& i& p- Z6 L & V5 s# u7 r2 ?4 t$ ]) \3 Q N ![]() 动态内存的申请:msg_ptr 指针指向的 128 字节内存空间位于动态内存堆空间中。, _) b# v3 Z; j( T# p; t' e ![]() 而一些全局变量则是存放于 RW 段和 ZI 段中,RW 段存放的是具有初始值的全局变量(而常量形式的全局变量则放置在 RO 段中,是只读属性的),ZI 段存放的系统未初始化的全局变量。8 N1 d; D3 i N% M1 u1 w) Y$ g0 r 1 T. P% F# U" K4 y! v1 d " F/ F/ j; J; h" {0 ?2 z 自动初始化机制 自动初始化机制是指初始化函数不需要被显式调用,只需要在函数定义处通过宏定义的方式进行申明,就会在系统启动过程中被执行。 ![]() ![]() 内核对象模型 静态与动态对象 3 i) ^9 t; ~3 P/ q# r- D) b5 W% K 静态内核对象通常放在 RW 段和 ZI 段中,在系统启动后在程序中初始化; 4 y W4 V# D- h7 C( H 动态内核对象则是从内存堆中创建的,而后手工做初始化,最后需要释放。 静态对象会占用 RAM 空间,不依赖于内存堆管理器,内存分配时间确定。动态对象则依赖于内存堆管理器,运行时申请 RAM 空间,当对象被删除后,占用的 RAM 空间被释放。这两种方式各有利弊,可以根据实际环境需求选择具体使用方式。: Q2 ^- i, b- X1 {; J $ P& i G ~) [6 q/ k4 c0 @ 内核对象管理架构 静态对象会占用 RAM 空间,不依赖于内存堆管理器,内存分配时间确定。动态对象则依赖于内存堆管理器,运行时申请 RAM 空间,当对象被删除后,占用的 RAM 空间被释放。这两种方式各有利弊,可以根据实际环境需求选择具体使用方式。 8 {4 F4 v3 r, v) j, K$ \, K ![]() , L" X5 v% D$ [" c' F ![]() 对象控制块
遍历内核对象
2 B: S+ s. _( I) Y+ Z
内核配置与裁剪 配置主要是通过修改工程目录下的 rtconfig.h 文件来进行,用户可以通过打开 / 关闭该文件中的宏定义来对代码进行条件编译,最终达到系统配置和裁剪的目的4 D/ Z. l; L+ p W L& r4 n8 m9 D6 s 注:在实际应用中,系统配置文件 rtconfig.h 是由配置工具自动生成的,无需手动更改。 8 g; b2 r2 e& C) } 9 Q0 s0 D) ?$ r6 p2 v 第8章 线程的定义与线程的切换& G: j% k0 B8 r" | 定义线程栈:+ m$ M; F! K" Q4 d- z a ![]() rt_uint8_t* e) Y1 q) [3 k 这些经过重定义的数据类型放在rtdef.h(rtdef.h 第一次使用需要在 include 文件夹下面新建然后添加到工程 rtt/source 这个组文件)这个头文件。 ALIGN 是一个带参宏,也在rtdef.h中定义 5 H5 x( c, E v _2 ~ ![]() RT_ALIGN_SIZE" x; r7 w8 w3 k8 F& I3 C6 {8 V 是一个在 rtconfifig.h ( rtconfifig.h 第一次使用需要在 User 文件夹下面新建然后添加到工程 user" y9 O3 s! w1 x) \ 这个组文件)中定义的宏 ![]() ![]() 1 J7 w3 \3 d* E0 T6 m1 u8 d 遵循 RT-Thread 中的函数命名规则,以小写的 rt 开头,表示这是一个外部函数,可以由用户调用,以 _rt 开头的函数表示内部函数,只能由 RT-Thread 内部使用。紧接着是文件名,表示该函数放在哪个文件,最后是函数功能名称。 thread 是线程控制块指针。 entry 是线程函数名,表示线程的入口。 parameter 是线程形参,用于传递线程参数。( E0 p0 Y* e {- u stack_start 用于指向线程栈的起始地址。! \, F, l. e$ ^* ?4 q' [: S2 z stack_size 表示线程栈的大小,单位为字节。 链表 1 C4 R9 w( u1 O6 g* c S ![]() $ c* e* o' v; `4 m# f/ ~ ![]() 这些函数均在 rtservice.h 中实现,rtservice.h 第一次使用需要自行在 rtthread/3.0.3/include 文件夹下新建,然后添加到工程的 rtt/source 组中。9 o6 r; I4 `! |4 e% H6 ?) X2 B! _ % F$ V1 e& C8 s & j' @1 e6 u& {/ h2 A 初始化链表节点1 O- D$ |% [1 V% c# X ![]() 5 C X( |( f# w( s/ ~ 双向链表表头后面插入一个节点 ![]() ![]() 双向链表表头前面插入一个节点 * ^* L) D! L, n; y Z1 e" B" K ![]() ![]() 双向链表删除一个节点' w: b& w6 q3 T I ![]() $ T7 ^7 k% K8 [$ n7 h$ z0 ? ![]() 线程栈初始化:rt_hw_stack_init() 函数( M$ k9 {0 w7 ~, Q4 Z5 X! i /* 获取栈顶指针 rt_hw_stack_init 在调用的时候,传给 stack_addr 的是 ( 栈顶指针 )*3 l; J) N0 {" u+ s# ? stk = stack_addr + sizeof (rt_uint32_t); " U* Q0 F1 ?3 [0 _" c7 W ![]() stk = (rt_uint8_t * )RT_ALIGN_DOWN((rt_uint32_t)stk, 8 );+ ~7 f; Q& O5 I3 _3 \- T; I6 _ 让 stk 这个指针向下 8 个字节对齐,确保 stk 是 8 字节对齐的地址。在 Cortex-M3 ( Cortex-M4 或 Cortex-M7 )内核的单片机中,因为总线宽度是 32 位的,通常只要栈保持 4 字节对齐就行,可这样为啥要 8 字节?难道有哪些操作是 64 位的? 确实有,那就是浮点运算,所以要 8 字节对齐(但是目前我们都还没有涉及到浮点运算,只是为了后续兼容浮点运行的考虑) 。如果栈顶指针是 8 字节对齐的,在进行向下 8 字节对齐的时候,指针不会移动,如果不是 8 字节对齐的,在做向下 8 字节对齐的时候,就会空出几个字节,不会使用,比如当 stk 是 33 ,明显不能整除 8 ,进行向下 8 字节对齐就是 32,那么就会空出一个字节不使用。 ![]() ' j3 O6 E6 Q$ S0 x% Z 线程第一次运行的时候,加载到 CPU 寄存器的环境参数我们要预先初始化好。从栈顶开始,初始化的顺序固定,首先是异常发生时自动保存的 8 个寄存器,即 xPSR 、 R15 、 R14 、 R12 、 R3 、 R2 、 R1 和 R0 。 其中 xPSR 寄存器的位 24 必须是1,R15 PC 指针必须存的是线程的入口地址,R0 必须是线程形参,剩下的 R14、R12、R3、R2 和 R1 我们初始化为 0。. T. V' N( Q1 K' l ![]() " y3 [2 A( U: n0 J9 ]5 T 将线程插入到双向就绪列表 ! v) d# a7 ~ t 线程创建好之后,我们需要把线程添加到就绪列表里面,表示线程已经就绪,系统随时可以调度。就绪列表在 scheduler.c 中定义(scheduler.c 第一次使用需要在 rtthread3.0.3src 目录下新建,然后添加到工程的 rtt/source 组中)。 * ]7 ?0 H# T3 P( p' D ![]() ![]() 实现调度器: l; H6 V4 ~- v; [ 调度器是操作系统的核心,其主要功能就是实现线程的切换,即从就绪列表里面找到优先级最高的线程,然后去执行该线程。从代码上来看,调度器无非也就是由几个全局变量和一些可以实现线程切换的函数组成,全部都在 scheduler.c 文件中实现。" K! |+ D' B# f" l) R3 X T " z: ^! }0 f3 e4 T5 g# B8 A ![]() / f M5 I5 `) G# p7 i/ |! v 定义一个局部变量,用 C 语言关键词 register 修饰,防止被编译器优化。我们把调度器初始化放在硬件初始化之后,线程创建之前2 G8 ~+ |$ A6 P3 h% R' Y; k 调度器启动由函数 rt_system_scheduler_start() 来完成2 Q5 o4 d5 R1 \" {& K! [' E9 I3 i1 M: U 系统调度 系统调度就是在就绪列表中寻找优先级最高的就绪线程,然后去执行该线程。但是目前我们还不 支持优先级,仅实现两个线程轮流切换,系统调度函数 rt_schedule。0 C2 q6 _3 f" Z S- X 6 B5 E% p0 `0 g( p# _, |7 } main函数4 {- t q F9 S- u/ F; @ 线程的创建,就绪列表的实现,调度器的实现均已经讲完,现在我们把全部的测试代码都放到 main.c 里面 ![]() . E9 h, i+ z" H; }2 h 第9章 临界段的保护4 d3 g6 s2 x v# n 临界段用一句话概括就是一段在执行的时候 不能被中断的代码段 。在 RT-Thread 里面,这个临界段最常出现的就是对全局变量的操作。) o$ ^$ u3 u( r. B4 c* Y" ^2 U 那么什么情况下临界段会被打断?一个是系统调度,还有一个就是外部中断。在 RT-Thread,系统调度,最终也是产生 PendSV 中断,在 PendSV Handler 里面实现线程的切换,所以还是可以归结为中断。既然这样, RT-Thread 对临界段的保护就处理的很干脆了,直接把中断全部关了,NMIFAULT 和硬 FAULT 除外。 + u: q6 f5 a8 [( r6 {/ N) ? ![]() ![]() . ?7 w3 v, v5 J 第10章 对象容器的实现 在 RT-Thread 中,所有的数据结构都称之为对象。7 j: s+ J: {$ n L# S8 u8 r ![]() % J, T* V6 C) x4 O, D3 M ![]() # P) U" Z- I* k8 z; T 在 RT-Thread 中,每个对象都会有对应的一个结构体,这个结构体叫做该对象的控制块。; \- Z" |. H) k( q7 ?/ y + W- \! {) F% P; D ![]() " M$ u/ | [6 P- I/ z 在 rtt 中,每当用户创建一个对象,如线程,就会将这个对象放到一个叫做容器的地方。 ( S2 @) i$ P3 z( c* w& k4 ] $ \3 K0 N% u9 L. E 那什么是容器,从代码上看,容器就是一个数组,是一个全局变量,数据类型为 struct rt_object_information ,在 object.c 中定义 ![]() ![]() ! D1 {) Y& `6 a$ y6 J 1 j5 ?) m6 D0 ?( N* E( C8 V0 y) M ———————————————— 版权声明:追逐者-桥 如有侵权请联系删除7 p% N* s2 j) C5 [( X3 N( M 0 b7 m0 @1 \* m% o+ m6 h0 i$ c" p |
【2025·STM32峰会】GUI解决方案实训分享1-对LVGL咖啡机例程的牛刀小试以及问题排查
OpenBLT移植到STM32F405开发板
为什么要先开启STM32外设时钟?
【STM32MP157】从ST官方例程中分析RPMsg-TTY/SDB核间通信的使用方法
【经验分享】STM32实例-RTC实时时钟实验④-获取RTC时间函数与中断服务函数
STM32 以太网 MAC Loopback 的实现
STM32功能安全设计包,助力产品功能安全认证
基于STM32启动过程startup_xxxx.s文件经验分享
HRTIM 指南
ST 微控制器电磁兼容性 (EMC) 设计指南