<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>RTOS on Blog</title><link>https://b0weny-qwq.github.io/Blog/tags/rtos/</link><description>Recent content in RTOS on Blog</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><lastBuildDate>Mon, 17 Aug 2026 15:52:00 +0800</lastBuildDate><atom:link href="https://b0weny-qwq.github.io/Blog/tags/rtos/index.xml" rel="self" type="application/rss+xml"/><item><title>RTOS 正传（一）：FreeRTOS 的目录、移植层和配置文件</title><link>https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/</link><pubDate>Mon, 17 Aug 2026 15:52:00 +0800</pubDate><guid>https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/</guid><description>&lt;h1 id="rtos-正传一freertos-的目录移植层和配置文件"&gt;RTOS 正传（一）：FreeRTOS 的目录、移植层和配置文件
&lt;/h1&gt;&lt;p&gt;上一篇把启动文件、链接脚本和 CMake 都搭好了，这一篇就是正传，开始把 FreeRTOS 搬进来。&lt;/p&gt;
&lt;p&gt;学习 RTOS，注重适配层而不是内核，这一点很关键。这种大型开源项目，本来就是一个组织、一群人在维护，想把里面所有东西全部理解，本身就很困难。&lt;/p&gt;
&lt;p&gt;当然，这不是说你不能成为贡献者。真想走这条路，也可以学阿酥在 Coding 那样一路混到大厂 Offer。但如果眼前只是想把 F407 上的系统跑起来，那就先把下面几个问题弄明白：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哪些 FreeRTOS 文件要编进工程？&lt;/li&gt;
&lt;li&gt;F407 + GCC 应该选哪个端口？&lt;/li&gt;
&lt;li&gt;堆实现到底选哪个？&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FreeRTOSConfig.h&lt;/code&gt; 里一大堆宏，哪些真的要管？&lt;/li&gt;
&lt;li&gt;哪些中断可以调用 &lt;code&gt;xxxFromISR()&lt;/code&gt;？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些问题先弄明白，系统就不再是“复制一个 Demo，能亮就算成功”的黑盒。&lt;/p&gt;
&lt;h2 id="rtos-到底适合什么场景"&gt;RTOS 到底适合什么场景
&lt;/h2&gt;&lt;p&gt;我们接下来就依照 FreeRTOS 来介绍。OS 本质上就是调度器，就是多任务处理，所以它明显更适合业务逻辑复杂、控制频率没到 us 级的场景，比如智能穿戴和飞控。&lt;/p&gt;
&lt;p&gt;没错，我这里把飞控也算作低频率控制系统。一是因为它的数据处理很复杂，二是因为电调本身的控制频率也不高，PX4 就是跑在 NuttX 上的。&lt;/p&gt;
&lt;p&gt;但你说我搞高精度伺服、工业电机驱动，上个 RTOS，这就不符合应用场景了。这是很关键的问题。&lt;/p&gt;
&lt;h2 id="先把内核仓库拉下来"&gt;先把内核仓库拉下来
&lt;/h2&gt;&lt;p&gt;这次直接看官方的 &lt;a class="link" href="https://github.com/FreeRTOS/FreeRTOS-Kernel" target="_blank" rel="noopener"
 &gt;FreeRTOS-Kernel&lt;/a&gt; 仓库。它只放内核源码、头文件和各种 CPU/编译器端口，比把 FreeRTOS-Plus、TCP、MQTT 全带上的聚合仓库清爽得多。&lt;/p&gt;
&lt;p&gt;&lt;img alt="FreeRTOS-Kernel 官方仓库" class="gallery-image" data-flex-basis="384px" data-flex-grow="160" height="1600" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/image/index/1786953157804.png" srcset="https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/image/index/1786953157804_hu_48e7d92945518e22.png 800w, https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/image/index/1786953157804_hu_637c4f36668a6f24.png 1600w, https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/image/index/1786953157804_hu_f5c5639d26902f27.png 2400w, https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/image/index/1786953157804.png 2560w" width="2560"&gt;&lt;/p&gt;
&lt;p&gt;只学习当前版本的话，浅克隆就够了：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone --depth &lt;span class="m"&gt;1&lt;/span&gt; --branch V11.3.0 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://github.com/FreeRTOS/FreeRTOS-Kernel.git
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;官方文档在 &lt;a class="link" href="https://www.freertos.org/" target="_blank" rel="noopener"
 &gt;www.freertos.org&lt;/a&gt;。内容肯定最全，但第一次打开确实像面对一本全英文词典：每个 API 都能查，问题是根本不知道应该先查哪个。&lt;/p&gt;
&lt;p&gt;&lt;img alt="FreeRTOS 官方文档" class="gallery-image" data-flex-basis="384px" data-flex-grow="160" height="1600" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/image/index/1786951997138.png" srcset="https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/image/index/1786951997138_hu_300ea6c776a6f6d2.png 800w, https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/image/index/1786951997138_hu_4fc0aa649574de11.png 1600w, https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/image/index/1786951997138_hu_22d038d6ca2931a9.png 2400w, https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/image/index/1786951997138.png 2560w" width="2560"&gt;&lt;/p&gt;
&lt;p&gt;我还是那个观点，没有什么比上手更重要。应用本来就比原理简单，设计岗薪资更高这一点事实足以佐证。所以这篇不准备逐行做源码解析，只介绍文件作用、怎么裁剪，以及核心移植层是怎么做的。源码主要还得靠自己慢慢啃，工程先跑起来再说。&lt;/p&gt;
&lt;h2 id="克隆下来为什么这么吓人"&gt;克隆下来为什么这么吓人
&lt;/h2&gt;&lt;p&gt;&lt;img alt="FreeRTOS-Kernel V11.3.0 文件结构" class="gallery-image" data-flex-basis="192px" data-flex-grow="80" height="1026" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/image/index/1786952115881.png" srcset="https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/image/index/1786952115881_hu_851493b892a5f754.png 800w, https://b0weny-qwq.github.io/Blog/p/stm32f407-freertos-kernel-port-config/image/index/1786952115881.png 821w" width="821"&gt;&lt;/p&gt;
&lt;p&gt;点开目录第一眼确实容易晕：一个 &lt;code&gt;tasks.c&lt;/code&gt; 就有 8872 行，旁边还躺着一堆不认识的配置文件。别慌，真正要烧进 F407 的内容并没有这么多。&lt;/p&gt;
&lt;p&gt;这个仓库里的东西大致分成四类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;tasks.c&lt;/code&gt;、&lt;code&gt;queue.c&lt;/code&gt;、&lt;code&gt;list.c&lt;/code&gt; 这些通用内核源码；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;portable/&lt;/code&gt; 下面针对不同 CPU 和编译器准备的移植代码；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;include/&lt;/code&gt; 里的公共 API 和内核头文件；&lt;/li&gt;
&lt;li&gt;GitHub、合规、发布和文档文件。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;后面不再给每个文件单独截一张图了。最好把仓库放在旁边，对着目录往下看，会比只看文章顺得多。&lt;/p&gt;
&lt;h2 id="先分清自己站在哪一层"&gt;先分清自己站在哪一层
&lt;/h2&gt;&lt;p&gt;这里最关键的不是记文件名，而是先搞清楚自己现在扮演什么角色。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;应用开发者&lt;/strong&gt;主要使用 &lt;code&gt;task.h&lt;/code&gt;、&lt;code&gt;queue.h&lt;/code&gt;、&lt;code&gt;semphr.h&lt;/code&gt; 这些 API，再根据业务配置 &lt;code&gt;FreeRTOSConfig.h&lt;/code&gt;。任务怎么分、栈给多少、优先级怎么排，这些才是日常工作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;移植者&lt;/strong&gt;要把内核接到具体 CPU 和工具链上。F407 已经有官方 ARM_CM4F 端口，我们只需要选对、编进来，再接好 SysTick、PendSV、SVC 和中断优先级，不用自己从零写上下文切换。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内核维护者&lt;/strong&gt;才需要深挖 &lt;code&gt;tasks.c&lt;/code&gt;、&lt;code&gt;queue.c&lt;/code&gt;、&lt;code&gt;list.c&lt;/code&gt;，研究就绪链表、阻塞链表、优先级继承和调度细节。&lt;/p&gt;
&lt;p&gt;还有一类是&lt;strong&gt;仓库维护者&lt;/strong&gt;。&lt;code&gt;.github/&lt;/code&gt;、MISRA、SPDX、拼写检查这些文件主要服务于 FreeRTOS 项目本身。它们不是垃圾，只是不会进入我们的固件。&lt;/p&gt;
&lt;h2 id="顶层目录先过一遍"&gt;顶层目录先过一遍
&lt;/h2&gt;&lt;p&gt;把截图里的内容整理一下，大概就是这样：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FreeRTOS-Kernel/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── .github/ # CI、Issue/PR 模板、贡献规范
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── examples/ # CMake 示例和配置模板
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── include/ # 内核头文件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── portable/ # CPU、编译器和堆的移植实现
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── tasks.c # 任务和调度器
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── queue.c # 队列、信号量、互斥锁
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── list.c # 内核链表
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── timers.c # 软件定时器
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── event_groups.c # 事件组
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── stream_buffer.c # Stream/Message Buffer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── croutine.c # 旧式协程
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── CMakeLists.txt # 官方 CMake 入口
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── README.md # 仓库说明
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── History.txt # 版本历史
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── LICENSE.md # MIT 许可证
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── MISRA.md # MISRA 合规与偏离说明
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── manifest.yml # 发布元数据
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── cspell.config.yaml # 英文拼写检查
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── .gitattributes # Git 文件属性
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── .gitmodules # 第三方端口子模块
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── .git-blame-ignore-revs # blame 时忽略格式化提交
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── *SPDX2.3.spdx* # 多种格式的 SBOM
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── GitHub-FreeRTOS-Kernel-Home.url
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── Quick_Start_Guide.url # 网页快捷方式
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;看起来还是不少，但真正的内核主体其实就是根目录那几个 &lt;code&gt;.c&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="根目录的-7-个-c"&gt;根目录的 7 个 &lt;code&gt;.c&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;tasks.c&lt;/code&gt; 是调度器主体。任务创建、删除、延时、优先级、就绪与阻塞状态，最后都绕不开它。应用开发者先理解任务状态怎么变化就够了；真要维护内核，再进去研究各条链表怎么转。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;queue.c&lt;/code&gt; 不只实现 Queue。Semaphore、Mutex、Recursive Mutex、Queue Set 也复用了它的队列结构。应用层最值得先搞懂的是阻塞超时和 Mutex 的优先级继承。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;list.c&lt;/code&gt; 只有两百多行，却是内核调度的地基。就绪任务、延时任务、挂起任务都要挂到链表上。应用不会直接调用它，内核维护者则必须把它看明白。&lt;/p&gt;
&lt;p&gt;上面三个是所有端口都要带的通用内核文件。剩下四个按功能看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;timers.c&lt;/code&gt;：软件定时器和 Timer Service Task；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;event_groups.c&lt;/code&gt;：事件组；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stream_buffer.c&lt;/code&gt;：Stream Buffer 和 Message Buffer；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;croutine.c&lt;/code&gt;：旧式协程，新项目基本不用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;官方 CMake 会把这 7 个文件都放进内核目标，再靠配置宏和链接器裁掉没启用的部分。所以这里说“按需使用”，不是让人刚入门就随手删源码。&lt;/p&gt;
&lt;h2 id="include平时调用的-api-都在这儿"&gt;&lt;code&gt;include/&lt;/code&gt;：平时调用的 API 都在这儿
&lt;/h2&gt;&lt;p&gt;这个目录大部分是声明和宏，不是另一份内核实现。应用最常碰到的是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FreeRTOS.h 总入口，读取 FreeRTOSConfig.h
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;task.h 任务、延时、通知、调度器 API
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;queue.h 队列和 Queue Set
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;semphr.h 信号量和互斥锁
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;timers.h 软件定时器
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event_groups.h 事件组
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stream_buffer.h Stream Buffer
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;message_buffer.h Message Buffer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;下面这些就更偏内核和移植：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;list.h 内核链表，应用不要直接依赖
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;portable.h 内核和端口层之间的接口
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;projdefs.h 通用类型、返回值和基础定义
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stack_macros.h 栈溢出检查宏
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;atomic.h 原子操作宏
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;剩下的 &lt;code&gt;mpu_*.h&lt;/code&gt; 是 MPU 端口用的，&lt;code&gt;newlib-freertos.h&lt;/code&gt;、&lt;code&gt;picolibc-freertos.h&lt;/code&gt; 负责 C 库适配；&lt;code&gt;StackMacros.h&lt;/code&gt; 和 &lt;code&gt;deprecated_definitions.h&lt;/code&gt; 主要照顾旧工程。现在知道它们大概在哪儿就行，不用每个都点开拜读。&lt;/p&gt;
&lt;p&gt;应用层可以正常包含公共 API，但不要为了“适配板子”去改内核头文件。芯片和板级差异应该留在端口、中断入口和自己的 &lt;code&gt;FreeRTOSConfig.h&lt;/code&gt; 里。&lt;/p&gt;
&lt;h2 id="portable真正和芯片接上的地方"&gt;&lt;code&gt;portable/&lt;/code&gt;：真正和芯片接上的地方
&lt;/h2&gt;&lt;p&gt;这个目录最大不是因为它最复杂，而是 FreeRTOS 要同时照顾 GCC、IAR、Keil，以及 Cortex-M、RISC-V、AVR、PIC 等一大群组合。基本规则就是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;portable/&amp;lt;编译器&amp;gt;/&amp;lt;CPU 架构&amp;gt;/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;我们现在用的是 &lt;code&gt;STM32F407VET6 + arm-none-eabi-gcc&lt;/code&gt;。F407 是带 FPU 的 Cortex-M4，所以选：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;portable/GCC/ARM_CM4F/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── port.c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── portmacro.h
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;port.c&lt;/code&gt; 负责初始任务栈、启动第一个任务，以及 SysTick、PendSV、SVC 这些和上下文切换有关的东西。&lt;code&gt;portmacro.h&lt;/code&gt; 则定义栈类型、字节对齐、临界区和任务切换宏。&lt;/p&gt;
&lt;p&gt;其他 CPU 和编译器目录都不用编译。你不是在做一个全架构发行版，没必要把整个 &lt;code&gt;portable/&lt;/code&gt; 一锅端进工程。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;portable/MemMang/&lt;/code&gt; 下面还有 5 个堆实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;heap_1.c&lt;/code&gt;：只能分配，不能释放，最简单；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;heap_2.c&lt;/code&gt;：能释放但不合并相邻空闲块，新项目一般不选；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;heap_3.c&lt;/code&gt;：包装 C 库的 &lt;code&gt;malloc()&lt;/code&gt; 和 &lt;code&gt;free()&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;heap_4.c&lt;/code&gt;：能释放，也会合并空闲块，普通 MCU 工程最常用；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;heap_5.c&lt;/code&gt;：能力类似 &lt;code&gt;heap_4.c&lt;/code&gt;，但支持多个不连续内存区。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;动态分配时只能选一个。这里先用 &lt;code&gt;heap_4.c&lt;/code&gt;，别把五个一起编进去开会。&lt;/p&gt;
&lt;h2 id="那些不会进固件的文件"&gt;那些不会进固件的文件
&lt;/h2&gt;&lt;p&gt;根目录剩下的东西也简单过一下，免得每次看到都怀疑是不是漏编了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.github/&lt;/code&gt;：CI、测试、PR 模板和贡献流程；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;examples/&lt;/code&gt;：CMake 示例、Coverity/MISRA 示例和 &lt;code&gt;FreeRTOSConfig.h&lt;/code&gt; 模板；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CMakeLists.txt&lt;/code&gt;：官方构建入口；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;README.md&lt;/code&gt;、&lt;code&gt;History.txt&lt;/code&gt;、&lt;code&gt;LICENSE.md&lt;/code&gt;：说明、历史和许可证；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MISRA.md&lt;/code&gt;：MISRA C:2012 的合规范围和偏离理由；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;manifest.yml&lt;/code&gt;：发布系统读取的版本和许可证信息；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cspell.config.yaml&lt;/code&gt;：英文拼写检查；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.gitattributes&lt;/code&gt;、&lt;code&gt;.gitmodules&lt;/code&gt;、&lt;code&gt;.git-blame-ignore-revs&lt;/code&gt;：Git 自己用的配置；&lt;/li&gt;
&lt;li&gt;4 个 &lt;code&gt;SPDX2.3&lt;/code&gt; 文件：同一份 SBOM 的文本、JSON、XML、YAML 版本；&lt;/li&gt;
&lt;li&gt;两个 &lt;code&gt;.url&lt;/code&gt;：官方主页和快速入门的网页快捷方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它们都不会被编译器变成 MCU 指令。想给 FreeRTOS 官方仓库提 PR 时再认真研究，现在知道不用塞进 F407 工程就够了。&lt;/p&gt;
&lt;h2 id="f407-工程最后到底拿哪些"&gt;F407 工程最后到底拿哪些
&lt;/h2&gt;&lt;p&gt;绕了一圈，真正需要的东西可以缩成这样：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FreeRTOS-Kernel/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── tasks.c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── queue.c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── list.c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── timers.c # 开软件定时器时使用
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── event_groups.c # 开事件组时使用
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── stream_buffer.c # 开 Stream/Message Buffer 时使用
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── include/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── portable/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── GCC/ARM_CM4F/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ ├── port.c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ └── portmacro.h
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── MemMang/heap_4.c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;自己的工程/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── FreeRTOSConfig.h
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;边界也就清楚了：内核维护者去深究 &lt;code&gt;tasks.c + queue.c + list.c&lt;/code&gt;；做新端口的人研究 &lt;code&gt;port.c&lt;/code&gt;、&lt;code&gt;portmacro.h&lt;/code&gt; 和异常机制；我们现在主要负责选对 ARM_CM4F 端口，再把任务、内存和中断规则写进 &lt;code&gt;FreeRTOSConfig.h&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这个配置文件不在内核根目录，因为它本来就属于具体应用。官方只在 &lt;code&gt;examples/template_configuration/FreeRTOSConfig.h&lt;/code&gt; 放了一份模板。接下来就开始拆它。&lt;/p&gt;
&lt;h2 id="freertosconfigh-配置分类"&gt;&lt;code&gt;FreeRTOSConfig.h&lt;/code&gt; 配置分类
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FreeRTOSConfig.h
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 1. 时钟与时间
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 2. 调度器与任务
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 3. 任务附加能力
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 4. 任务通信与同步
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 5. 软件定时器
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 6. 内存、堆和任务栈
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 7. 中断与临界区
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 8. Hook 回调
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 9. 故障检测、断言、调试统计
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 10. C 库、POSIX 兼容
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 11. 旧协程
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 12. MPU、TrustZone、安全隔离
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 13. 多核 SMP
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── 14. API 裁剪开关
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;先说好，这 14 类不是让你全背下来。第一版真能把人折磨疯的，其实就时钟、中断优先级、任务栈、堆和几个调试开关。剩下那些 MPU、TrustZone、多核，看见名字很高级，但和现在这颗 F407 没啥关系，先别给自己加戏。&lt;/p&gt;
&lt;p&gt;我们还是一组一组过，不然后面报错了连该骂谁都不知道。&lt;/p&gt;
&lt;h3 id="1-时钟与时间"&gt;1. 时钟与时间
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;configCPU_CLOCK_HZ&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;configSYSTICK_CLOCK_HZ&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;configTICK_RATE_HZ&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;configTICK_TYPE_WIDTH_IN_BITS&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;configUSE_TICKLESS_IDLE&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这组就是告诉 FreeRTOS：CPU 到底跑多快，一秒钟产生多少个 Tick。&lt;/p&gt;
&lt;p&gt;最容易写错的是 &lt;code&gt;configCPU_CLOCK_HZ&lt;/code&gt;。它填的是&lt;strong&gt;启动调度器时真正生效的 HCLK&lt;/strong&gt;，不是芯片宣传页上的最高主频。假如时钟还没配，现在实际跑的是 16 MHz，那就老老实实填 16 MHz。你代码根本没切到 168 MHz，却在这里写个 168 MHz，后面的延时自然全是歪的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;configSYSTICK_CLOCK_HZ&lt;/code&gt; 只有在 SysTick 和 CPU 时钟不同源时才需要单独写。现在两边一样就别重复定义，省得以后改了一个忘了另一个。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;configTICK_RATE_HZ&lt;/code&gt; 第一版用 &lt;code&gt;1000&lt;/code&gt; 就行，也就是 1 Tick = 1 ms。写延时的时候别偷懒：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;vTaskDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pdMS_TO_TICKS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;直接写 &lt;code&gt;vTaskDelay(10)&lt;/code&gt; 看起来很爽，但哪天 Tick 频率一改，这个 10 就不一定还是 10 ms 了。&lt;/p&gt;
&lt;p&gt;F407 用 32 位 Tick：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configTICK_TYPE_WIDTH_IN_BITS TICK_TYPE_WIDTH_32_BITS
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;configUSE_TICKLESS_IDLE&lt;/code&gt; 是低功耗用的，空闲时把周期 Tick 停掉。第一版先关，系统都还没跑稳就别急着省那点电。&lt;/p&gt;
&lt;h3 id="2-调度器与任务"&gt;2. 调度器与任务
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_PREEMPTION 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_TIME_SLICING 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configMAX_PRIORITIES 5
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configIDLE_SHOULD_YIELD 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;configUSE_PREEMPTION = 1&lt;/code&gt; 就是抢占式调度。高优先级任务一旦就绪，不需要等低优先级任务自己良心发现让出 CPU，直接抢过来运行。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;configUSE_TIME_SLICING = 1&lt;/code&gt; 是让同优先级任务轮流跑。两个任务优先级一样，又都不阻塞，那就每个 Tick 换一次。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;configMAX_PRIORITIES&lt;/code&gt; 也不是越大越牛逼。第一版 5 级其实就够分了：控制、采样通信、普通业务、显示日志、Idle。你一上来开几十个优先级，最后只会得到一张自己都看不懂的任务关系网。&lt;/p&gt;
&lt;p&gt;ARM_CM4F 可以打开 &lt;code&gt;configUSE_PORT_OPTIMISED_TASK_SELECTION&lt;/code&gt;，让内核用 Cortex-M 的指令快速找最高优先级任务。不过开了以后 &lt;code&gt;configMAX_PRIORITIES&lt;/code&gt; 不能超过 32，这点记住就行。&lt;/p&gt;
&lt;h3 id="3-任务附加能力"&gt;3. 任务附加能力
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configMAX_TASK_NAME_LEN 16
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configMINIMAL_STACK_SIZE 128
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configTASK_NOTIFICATION_ARRAY_ENTRIES 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_APPLICATION_TASK_TAG 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;任务名长度设 16，调试器里能看清名字就行。通知槽第一版一个也够用，线程局部指针和 Task Tag 暂时都关掉。&lt;/p&gt;
&lt;p&gt;重点还是 &lt;code&gt;configMINIMAL_STACK_SIZE&lt;/code&gt;。这个 &lt;code&gt;128&lt;/code&gt; 不是 128 字节。ARM_CM4F 的 &lt;code&gt;StackType_t&lt;/code&gt; 是 32 位，所以这里实际是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;128 word × 4 byte = 512 byte
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;而且它主要是 Idle Task 的栈大小，不代表所有任务都照着 128 抄。任务里要是有大数组、浮点计算、&lt;code&gt;printf&lt;/code&gt;，栈很快就能给你吃干净。后面一定要看栈水位，不能凭感觉拍一个数字。&lt;/p&gt;
&lt;h3 id="4-任务通信与同步"&gt;4. 任务通信与同步
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_TASK_NOTIFICATIONS 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_MUTEXES 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_RECURSIVE_MUTEXES 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_COUNTING_SEMAPHORES 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_QUEUE_SETS 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_EVENT_GROUPS 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_STREAM_BUFFERS 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这一组看着多，其实就是任务之间怎么说话。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Task Notification：一对一通知，快而且省内存，能用它就先用它。&lt;/li&gt;
&lt;li&gt;Queue：真的有数据要传，比如传一帧传感器结构体，就用队列。&lt;/li&gt;
&lt;li&gt;Semaphore：做事件同步或者资源计数。&lt;/li&gt;
&lt;li&gt;Mutex：保护共享资源，它有优先级继承，不要拿二值信号量硬顶。&lt;/li&gt;
&lt;li&gt;Event Group：用几个 bit 同时等多个事件。&lt;/li&gt;
&lt;li&gt;Stream Buffer：连续字节流，串口接收这类场景比较常见。&lt;/li&gt;
&lt;li&gt;Message Buffer：底层还是 Stream Buffer，但是会保留消息边界。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Queue、Semaphore、Mutex 都主要在 &lt;code&gt;queue.c&lt;/code&gt; 里；Event Group 需要 &lt;code&gt;event_groups.c&lt;/code&gt;；Stream 和 Message Buffer 需要 &lt;code&gt;stream_buffer.c&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;中断里又是另一套规矩，只能调用带 &lt;code&gt;FromISR&lt;/code&gt; 的接口：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;BaseType_t&lt;/span&gt; &lt;span class="n"&gt;xHigherPriorityTaskWoken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdFALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;vTaskNotifyGiveFromISR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xTaskHandle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;xHigherPriorityTaskWoken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;portYIELD_FROM_ISR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xHigherPriorityTaskWoken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;最后这个 &lt;code&gt;portYIELD_FROM_ISR()&lt;/code&gt; 不是装饰品。中断唤醒了更高优先级任务，就应该让它在退出中断后马上运行。&lt;/p&gt;
&lt;h3 id="5-软件定时器"&gt;5. 软件定时器
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;configUSE_TIMERS&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;configTIMER_TASK_PRIORITY&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;configTIMER_TASK_STACK_DEPTH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;configTIMER_QUEUE_LENGTH&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;软件定时器开启以后，FreeRTOS 会多创建一个 Timer Service Task。所有软件定时器到期，最终都是这个任务来执行回调。&lt;/p&gt;
&lt;p&gt;所以它和硬件定时器完全不是一回事。回调不是在某个神秘的精准时刻凭空执行，它得排队，还会受 Timer Task 优先级和其他回调耗时影响。&lt;/p&gt;
&lt;p&gt;第一版不用就直接：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_TIMERS 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;要用就把 &lt;code&gt;timers.c&lt;/code&gt; 编进来，再配置任务优先级、栈和命令队列长度。回调里别延时，别疯狂打印，更别塞一大坨计算。一个回调卡住，后面的定时器全跟着排队。&lt;/p&gt;
&lt;h3 id="6-内存堆和任务栈"&gt;6. 内存、堆和任务栈
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configSUPPORT_STATIC_ALLOCATION 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configSUPPORT_DYNAMIC_ALLOCATION 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configTOTAL_HEAP_SIZE (32 * 1024)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configAPPLICATION_ALLOCATED_HEAP 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configSTACK_ALLOCATION_FROM_SEPARATE_HEAP 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configENABLE_HEAP_PROTECTOR 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configHEAP_CLEAR_MEMORY_ON_FREE 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里先按 &lt;code&gt;heap_4.c + 动态分配&lt;/code&gt; 来。&lt;code&gt;heap_4.c&lt;/code&gt; 能释放，也会合并相邻空闲块，对这种普通 MCU 工程够用了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;configTOTAL_HEAP_SIZE&lt;/code&gt; 是 FreeRTOS 自己的堆，动态创建出来的任务栈、TCB、队列和信号量都从这里拿内存。它和链接脚本里的 C 库堆不是一回事，也不会自动接管普通 &lt;code&gt;malloc()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;上面写 32 KB 只是一个起步例子，不是说所有工程都必须 32 KB。最后到底要多少，得把每个任务栈、队列长度、对象数量都算进去，再配合：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;xPortGetFreeHeapSize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;xPortGetMinimumEverFreeHeapSize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;uxTaskGetStackHighWaterMark&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里还有个经典坑：&lt;code&gt;configTOTAL_HEAP_SIZE&lt;/code&gt; 的单位是&lt;strong&gt;字节&lt;/strong&gt;，但是 &lt;code&gt;xTaskCreate()&lt;/code&gt; 的栈深度在 ARM_CM4F 上通常是 32 位 &lt;strong&gt;word&lt;/strong&gt;。两个数字放在一起很像，单位完全不一样。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;configAPPLICATION_ALLOCATED_HEAP = 0&lt;/code&gt; 表示 &lt;code&gt;ucHeap[]&lt;/code&gt; 还是由 &lt;code&gt;heap_4.c&lt;/code&gt; 自己定义；设成 1 以后就得由应用提供。任务栈单独放进另一块内存、释放时清零、堆指针保护这些东西第一版也先关，真有对应需求再开。&lt;/p&gt;
&lt;p&gt;静态分配以后再单独讲。它不是把开关改成 1 就结束了，Idle Task 的静态内存得自己提供；如果开了软件定时器，Timer Task 的内存也一样。&lt;/p&gt;
&lt;h3 id="7-中断与临界区"&gt;7. 中断与临界区
&lt;/h3&gt;&lt;p&gt;这一组真得认真看。前面那些东西配错，大不了功能不对；中断优先级配错，经常是平时能跑，一上负载就随机爆炸，最恶心。&lt;/p&gt;
&lt;p&gt;STM32F407 有 4 个 NVIC 优先级位。这里按 4 位全部用于抢占优先级、不分子优先级来配：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configPRIO_BITS 4
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configKERNEL_INTERRUPT_PRIORITY \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; (configLIBRARY_LOWEST_INTERRUPT_PRIORITY &amp;lt;&amp;lt; (8 - configPRIO_BITS))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configMAX_SYSCALL_INTERRUPT_PRIORITY \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY &amp;lt;&amp;lt; (8 - configPRIO_BITS))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configCHECK_HANDLER_INSTALLATION 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Cortex-M 的优先级数字越小，实际优先级反而越高，这个反人类设计先接受。&lt;/p&gt;
&lt;p&gt;按上面这套配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;优先级 0 ~ 4 不会被 FreeRTOS 临界区挡住，但禁止调用 FreeRTOS API
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;优先级 5 ~ 15 可以调用对应的 xxxFromISR() API
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;优先级 15 给 SysTick 和 PendSV，放到最低
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;configMAX_SYSCALL_INTERRUPT_PRIORITY&lt;/code&gt; 千万不能填 0。还有，CMSIS 里写的优先级 5 是没左移的逻辑值，FreeRTOS 这里要的是左移后写进寄存器的值。网上很多教程两个混着写，抄完还能跑只能说命硬。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;configCHECK_HANDLER_INSTALLATION = 1&lt;/code&gt; 会检查 SVC 和 PendSV 有没有正确挂进向量表，但它依赖 &lt;code&gt;configASSERT&lt;/code&gt;。这两个要一起开，不然新版内核会直接编译报错。&lt;/p&gt;
&lt;h3 id="8-hook-回调"&gt;8. Hook 回调
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_IDLE_HOOK 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_TICK_HOOK 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_MALLOC_FAILED_HOOK 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_DAEMON_TASK_STARTUP_HOOK 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_SB_COMPLETED_CALLBACK 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Hook 就是 FreeRTOS 在几个固定时机回调你写的函数。&lt;/p&gt;
&lt;p&gt;Idle Hook 会跟着 Idle Task 跑，Tick Hook 每个 Tick 都会进，而且是在中断上下文里。第一版都先关，尤其别在 Tick Hook 里打印日志，那是在给自己制造新问题。&lt;/p&gt;
&lt;p&gt;但是 &lt;code&gt;configUSE_MALLOC_FAILED_HOOK&lt;/code&gt; 我建议调试阶段直接打开：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;vApplicationMallocFailedHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;taskDISABLE_INTERRUPTS&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(;;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;内存分配失败以后停在这里，至少调试器能告诉你是堆没了。不开的话，错误可能继续往后传，最后在一个毫不相干的地方炸掉，到时候又要查半天。&lt;/p&gt;
&lt;h3 id="9-故障检测断言与统计"&gt;9. 故障检测、断言与统计
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configCHECK_FOR_STACK_OVERFLOW 2
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configGENERATE_RUN_TIME_STATS 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_TRACE_FACILITY 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_STATS_FORMATTING_FUNCTIONS 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configQUEUE_REGISTRY_SIZE 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;configCHECK_FOR_STACK_OVERFLOW = 2&lt;/code&gt; 会检查任务栈末尾的填充值有没有被踩。开了以后还要实现：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;vApplicationStackOverflowHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TaskHandle_t&lt;/span&gt; &lt;span class="n"&gt;xTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pcTaskName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后是 &lt;code&gt;configASSERT&lt;/code&gt;，调试阶段别关：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configASSERT(x) \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; do \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; { \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; if ((x) == 0) \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; { \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; taskDISABLE_INTERRUPTS(); \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; for (;;) \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; { \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; } \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; } \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; } while (0)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;FreeRTOS 很多错误不是靠返回一个 &lt;code&gt;error&lt;/code&gt; 告诉你，而是靠断言把程序钉在现场。中断优先级配错、参数不合法、Handler 没挂好，都可能从这里抓出来。&lt;/p&gt;
&lt;p&gt;运行时间统计、Trace、格式化任务列表先关。它们当然有用，但运行时间统计还需要额外的高频计数器，格式化函数又会把字符串处理带进来。先把系统跑起来，后面需要分析 CPU 占用时再开。&lt;/p&gt;
&lt;h3 id="10-c-库与-posix-兼容"&gt;10. C 库与 POSIX 兼容
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_NEWLIB_REENTRANT 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_POSIX_ERRNO 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configENABLE_BACKWARD_COMPATIBILITY 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这一组第一版全关。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;configUSE_POSIX_ERRNO&lt;/code&gt; 只是让每个任务有自己的 &lt;code&gt;FreeRTOS_errno&lt;/code&gt;，不是说打开以后 FreeRTOS 就突然变成 Linux 了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;configUSE_NEWLIB_REENTRANT&lt;/code&gt; 会给每个任务塞一份 Newlib 重入状态，内存开销也会跟着涨。而且它不等于“从此 &lt;code&gt;printf&lt;/code&gt; 随便多线程调用都安全”。串口日志最好还是统一丢给一个日志任务，或者至少用互斥锁保护。&lt;/p&gt;
&lt;p&gt;新工程也没必要兼容一堆旧 API 名字，所以 &lt;code&gt;configENABLE_BACKWARD_COMPATIBILITY&lt;/code&gt; 关掉。&lt;/p&gt;
&lt;h3 id="11-旧协程"&gt;11. 旧协程
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_CO_ROUTINES 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configMAX_CO_ROUTINE_PRIORITIES 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这个东西是给 RAM 极小的老设备准备的。它没有普通任务那样的独立栈，走的还是合作式调度。&lt;/p&gt;
&lt;p&gt;F407 又不是穷到这点内存都没有，没必要为了省一点 RAM 同时养两套调度模型。关掉，也不用编译 &lt;code&gt;croutine.c&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="12-mputrustzone-与安全隔离"&gt;12. MPU、TrustZone 与安全隔离
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;configENABLE_MPU
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;configENABLE_TRUSTZONE
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;configRUN_FREERTOS_SECURE_ONLY
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;configENABLE_FPU
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这几个宏出现在通用模板的 ARMv8-M 配置区里。F407 是 ARMv7-M Cortex-M4F，没有 TrustZone，所以不要照着模板把它们全抄进来。&lt;/p&gt;
&lt;p&gt;F407 自己有 MPU，但我们现在选的是标准 &lt;code&gt;portable/GCC/ARM_CM4F&lt;/code&gt;，不是 &lt;code&gt;ARM_CM4_MPU&lt;/code&gt;。只定义一个 &lt;code&gt;configENABLE_MPU&lt;/code&gt; 没用，真正做 MPU 隔离还得换端口、划内存区域、区分特权任务和非特权任务，这已经是下一阶段的东西了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;configTOTAL_MPU_REGIONS&lt;/code&gt; 和 &lt;code&gt;configUSE_MPU_WRAPPERS_V1&lt;/code&gt; 也都是 MPU 端口才会认真处理的东西。标准 ARM_CM4F 端口下先不用管。&lt;/p&gt;
&lt;p&gt;至于 FPU，选中 &lt;code&gt;ARM_CM4F&lt;/code&gt; 端口并使用正确的浮点编译参数后，端口会负责浮点上下文；这里也不用额外定义 &lt;code&gt;configENABLE_FPU&lt;/code&gt;。这个宏主要是给模板里对应的 ARMv8-M 端口准备的。&lt;/p&gt;
&lt;h3 id="13-多核-smp"&gt;13. 多核 SMP
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configNUMBER_OF_CORES 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configRUN_MULTIPLE_PRIORITIES 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define configUSE_CORE_AFFINITY 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;STM32F407 就一个 Cortex-M4F 核心，这组没什么好纠结的。核心数设 1，亲和性关掉，定时器任务绑哪个核这种问题也和我们没关系。&lt;/p&gt;
&lt;p&gt;所以 &lt;code&gt;configTASK_DEFAULT_CORE_AFFINITY&lt;/code&gt;、&lt;code&gt;configTIMER_SERVICE_TASK_CORE_AFFINITY&lt;/code&gt; 这些宏可以直接忽略，它们是多核开启以后才有意义。&lt;/p&gt;
&lt;p&gt;官方模板是给所有端口看的，所以里面出现 SMP 不代表 F407 突然长出了第二个核。&lt;/p&gt;
&lt;h3 id="14-api-裁剪开关"&gt;14. API 裁剪开关
&lt;/h3&gt;&lt;p&gt;最后这组 &lt;code&gt;INCLUDE_*&lt;/code&gt; 不是在选择调度模式，它只是决定某个 API 要不要编进来。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define INCLUDE_vTaskDelay 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;开了以后才能用 &lt;code&gt;vTaskDelay()&lt;/code&gt;。第一版我会先保留这些：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define INCLUDE_vTaskDelay 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define INCLUDE_xTaskDelayUntil 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define INCLUDE_vTaskDelete 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define INCLUDE_xTaskGetSchedulerState 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define INCLUDE_xTaskGetCurrentTaskHandle 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define INCLUDE_uxTaskGetStackHighWaterMark 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;周期任务尽量用 &lt;code&gt;xTaskDelayUntil()&lt;/code&gt;，它是按固定时间点唤醒，比“运行完再延时一段时间”更适合做稳定周期。&lt;/p&gt;
&lt;p&gt;下面这些没有明确需求就先关：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define INCLUDE_vTaskSuspend 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define INCLUDE_eTaskGetState 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define INCLUDE_xTaskAbortDelay 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define INCLUDE_xTaskGetHandle 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define INCLUDE_xTimerPendFunctionCall 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define INCLUDE_xTaskResumeFromISR 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;尤其别把 &lt;code&gt;vTaskSuspend()&lt;/code&gt; 当成万能同步工具。任务之间有事件关系，就老老实实用通知、信号量或者队列，不然代码跑着跑着就不知道是谁把谁挂起了。&lt;/p&gt;
&lt;h2 id="总结一下"&gt;总结一下
&lt;/h2&gt;&lt;p&gt;这么一大串看完，其实真正的优先级就几条：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先确认 CPU 真实时钟和 Tick 频率，不然所有时间都是假的。&lt;/li&gt;
&lt;li&gt;把 NVIC 分组和 FreeRTOS 中断优先级配对，搞清楚哪些中断能调用 &lt;code&gt;FromISR&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;打开 &lt;code&gt;configASSERT&lt;/code&gt;、栈溢出检查和 Malloc Failed Hook，让程序死也死得明白。&lt;/li&gt;
&lt;li&gt;算任务栈和 FreeRTOS 堆，别拿“现在还没崩”当内存够用的证据。&lt;/li&gt;
&lt;li&gt;软件定时器、事件组、Stream Buffer 用到哪个再开哪个。&lt;/li&gt;
&lt;li&gt;协程、MPU、TrustZone、SMP 暂时全部放一边。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以 &lt;code&gt;FreeRTOSConfig.h&lt;/code&gt; 不是官方宏大全，更不是把所有功能都设成 1 就完事。它应该老老实实记录这块板子现在跑多快、任务怎么调度、中断能做到什么程度、内存又准备拿出多少。这个文件配明白了，FreeRTOS 才算真的进了自己的工程，而不是从别人的 Demo 里复制过来勉强亮机。&lt;/p&gt;</description></item><item><title>RTOS 前传：先把IDE的编译黑盒拆了</title><link>https://b0weny-qwq.github.io/Blog/p/stm32f407-rtos-prequel-build-from-scratch/</link><pubDate>Sun, 16 Aug 2026 01:42:00 +0800</pubDate><guid>https://b0weny-qwq.github.io/Blog/p/stm32f407-rtos-prequel-build-from-scratch/</guid><description>&lt;h1 id="rtos-前传先把ide的编译黑盒拆了"&gt;RTOS 前传：先把IDE的编译黑盒拆了
&lt;/h1&gt;&lt;p&gt;之前其实学过 &lt;code&gt;RTOS&lt;/code&gt;，但那时候纯纯把它当黑盒来用：会创建任务、会调度，能跑起来就完事了。&lt;code&gt;CMake&lt;/code&gt; 也差不多，自己甚至没怎么手写过。&lt;/p&gt;
&lt;p&gt;我发现自己每次开学，工程能力都会 &lt;code&gt;++&lt;/code&gt;；每次一放长假，又会再进化一次。原因可能也很简单：假期里有时间把那些最基础、最底层的东西重新磨一遍。刀磨了一整个假期，开学当然砍得更快。&lt;/p&gt;
&lt;p&gt;所以接下来准备更新一个新篇章，重新学 &lt;code&gt;RTOS&lt;/code&gt;。这次以 &lt;code&gt;STM32F407VET6&lt;/code&gt; 为例，不急着上操作系统，先从最基础的工程开始：补齐启动文件、系统初始化文件和链接脚本，再亲手写好 &lt;code&gt;CMake&lt;/code&gt;，让它真的编译起来。&lt;/p&gt;
&lt;p&gt;这一篇就是前传。看懂它最好有一点汇编语言和微机原理基础，不然中间可能会有点吃力。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;本系列均使用编辑器 + CLI，拒绝一切丑陋的 IDE，比如 Keil CubeMX。用 Keil 的你一辈子没法成长 ,就等着找底薪工作吧。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="先把官方仓库拉下来"&gt;先把官方仓库拉下来
&lt;/h2&gt;&lt;p&gt;先克隆 ST 官方的 &lt;a class="link" href="https://github.com/STMicroelectronics/STM32CubeF4" target="_blank" rel="noopener"
 &gt;STM32CubeF4&lt;/a&gt; 仓库。里面有一堆库、驱动和例程，也有我们需要的启动文件。&lt;/p&gt;
&lt;p&gt;&lt;img alt="STM32CubeF4 官方仓库" class="gallery-image" data-flex-basis="410px" data-flex-grow="171" height="1454" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://b0weny-qwq.github.io/Blog/p/stm32f407-rtos-prequel-build-from-scratch/image/index/github%E4%BB%93%E5%BA%93cubef4.png" srcset="https://b0weny-qwq.github.io/Blog/p/stm32f407-rtos-prequel-build-from-scratch/image/index/github%E4%BB%93%E5%BA%93cubef4_hu_3e452eec8af28110.png 800w, https://b0weny-qwq.github.io/Blog/p/stm32f407-rtos-prequel-build-from-scratch/image/index/github%E4%BB%93%E5%BA%93cubef4_hu_59e2e166f862b0b4.png 1600w, https://b0weny-qwq.github.io/Blog/p/stm32f407-rtos-prequel-build-from-scratch/image/index/github%E4%BB%93%E5%BA%93cubef4_hu_d41ac23b509940bb.png 2400w, https://b0weny-qwq.github.io/Blog/p/stm32f407-rtos-prequel-build-from-scratch/image/index/github%E4%BB%93%E5%BA%93cubef4.png 2487w" width="2487"&gt;&lt;/p&gt;
&lt;p&gt;这个仓库其实挺大，只想拿来搭工程的话，浅克隆就够了：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gh repo clone STMicroelectronics/STM32CubeF4 -- --depth &lt;span class="m"&gt;1&lt;/span&gt; --recursive
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;相比完整克隆，体积能小一半以上，快多了。没必要为了三个文件把整个 Git 历史请回家供着。&lt;/p&gt;
&lt;p&gt;接下来把工具配齐：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;arm-none-eabi-gcc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gdb-multiarch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cmake&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ninja&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openocd&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我自己都安装过了，这里就不演示了。&lt;/p&gt;
&lt;h2 id="一个最小工程需要什么"&gt;一个最小工程需要什么
&lt;/h2&gt;&lt;p&gt;&lt;img alt="STM32F407 最小工程" class="gallery-image" data-flex-basis="248px" data-flex-grow="103" height="952" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://b0weny-qwq.github.io/Blog/p/stm32f407-rtos-prequel-build-from-scratch/image/index/%E6%9C%80%E5%B0%8F%E5%B7%A5%E7%A8%8B.png" srcset="https://b0weny-qwq.github.io/Blog/p/stm32f407-rtos-prequel-build-from-scratch/image/index/%E6%9C%80%E5%B0%8F%E5%B7%A5%E7%A8%8B_hu_da6a6d5165f331d9.png 800w, https://b0weny-qwq.github.io/Blog/p/stm32f407-rtos-prequel-build-from-scratch/image/index/%E6%9C%80%E5%B0%8F%E5%B7%A5%E7%A8%8B.png 985w" width="985"&gt;&lt;/p&gt;
&lt;p&gt;先从 CubeF4 里复制三个关键文件：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;F407_RTOS_EXAMPLE/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── Startup/startup_stm32f407xx.s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── Platform/system_stm32f4xx.c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── linker/STM32F407VETX_FLASH.ld
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;官方仓库里比较容易找到的是 &lt;code&gt;STM32F407VGTX_FLASH.ld&lt;/code&gt;。它也能拿来改，但一定要注意：&lt;code&gt;F407VGT6&lt;/code&gt; 有 &lt;code&gt;1 MB Flash&lt;/code&gt;，这里使用的 &lt;code&gt;F407VET6&lt;/code&gt; 只有 &lt;code&gt;512 KB Flash&lt;/code&gt;。复制之后要把链接脚本改对，最好顺手重命名，免得哪天把自己也骗了。&lt;/p&gt;
&lt;p&gt;最基础的文件结构大概这样：&lt;/p&gt;
&lt;p&gt;&lt;img alt="最小工程必备文件" class="gallery-image" data-flex-basis="358px" data-flex-grow="149" height="354" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://b0weny-qwq.github.io/Blog/p/stm32f407-rtos-prequel-build-from-scratch/image/index/%E5%BF%85%E5%A4%87%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84.png" width="529"&gt;&lt;/p&gt;
&lt;p&gt;这几个文件到底是干啥的？这就要从编译开始说了。之前说用 IDE 的永远不会成长，原因就在这里：一点 &lt;code&gt;Build&lt;/code&gt;，后面的东西全黑了。你只知道没报错，牛逼，可以烧录；或者突然蹦出一个 &lt;code&gt;error&lt;/code&gt;，卧槽，牛魔的，快修改。&lt;/p&gt;
&lt;p&gt;但我们写的一堆 &lt;code&gt;.c&lt;/code&gt;、&lt;code&gt;.h&lt;/code&gt;，到底是怎么变成 MCU 能执行的机器码的？大致流程其实就这一条：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;C / 汇编源码
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ 编译、汇编
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;.o 目标文件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ 链接脚本安排地址
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ELF
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ objcopy
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BIN / HEX
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ OpenOCD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;STM32 Flash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="arm-none-eabi-这一家子"&gt;&lt;code&gt;arm-none-eabi&lt;/code&gt; 这一家子
&lt;/h2&gt;&lt;p&gt;先简单认一下工具链，后面报错时至少知道该骂谁：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;工具&lt;/th&gt;
 &lt;th&gt;作用&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;arm-none-eabi-gcc&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;编译 C，也可以统一调用汇编器和链接器&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;arm-none-eabi-as&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;把汇编代码变成&lt;code&gt;.o&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;arm-none-eabi-ld&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;把一堆&lt;code&gt;.o&lt;/code&gt; 链接成 ELF&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;arm-none-eabi-objcopy&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;把 ELF 转成 BIN 或 HEX&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;arm-none-eabi-objdump&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;看段信息和反汇编&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;arm-none-eabi-nm&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;看符号和地址&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;arm-none-eabi-size&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;看 Flash 和 RAM 占用&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;arm-none-eabi-gdb&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;调试正在运行的程序&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;arm-none-eabi&lt;/code&gt; 也不是什么神秘咒语：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;arm → 目标架构是 ARM
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;none → 不针对某个操作系统
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;eabi → 使用嵌入式应用二进制接口
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;至于 MCU 是哪个内核，则由编译参数告诉 GCC，比如 F407 是 &lt;code&gt;Cortex-M4&lt;/code&gt;，F1 常见型号是 &lt;code&gt;Cortex-M3&lt;/code&gt;。&lt;code&gt;-mcpu=cortex-m4 -mthumb&lt;/code&gt; 这类参数就该出现在工具链或 CMake 配置里。&lt;/p&gt;
&lt;p&gt;真正决定“代码放到 Flash 哪儿、变量放到哪块 RAM”的，是下面这个文件。&lt;/p&gt;
&lt;h2 id="1-链接脚本告诉链接器这个家有多大"&gt;1. 链接脚本：告诉链接器这个家有多大
&lt;/h2&gt;&lt;p&gt;文件：&lt;code&gt;linker/STM32F407VETX_FLASH.ld&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;链接脚本干的事很直接：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;老子是什么型号？RAM、Flash 多大？每一段程序到底放哪里？&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;首先是程序入口：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ENTRY(Reset_Handler)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这表示固件的入口符号是启动文件里的 &lt;code&gt;Reset_Handler&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后计算栈顶：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;_estack = ORIGIN(RAM) + LENGTH(RAM);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;再描述物理内存。以 &lt;code&gt;STM32F407VET6&lt;/code&gt; 为例：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MEMORY
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里必须和手上的芯片对得上。写小一点一般只是浪费空间，写大了却可能把程序链接到根本不存在的 Flash 里，到时候就不是“为什么没亮”这么温柔了。&lt;/p&gt;
&lt;p&gt;所以 IDE 里选择芯片型号，不只是让界面左上角多一串字；它背后也在替你选启动文件、链接脚本和编译参数。IDE 帮你做了不代表这些东西不存在，只是以前它们被藏起来了。&lt;/p&gt;
&lt;p&gt;一句话总结：&lt;code&gt;arm-none-eabi-ld&lt;/code&gt; 负责链接，&lt;code&gt;.ld&lt;/code&gt; 负责告诉它怎样安排 STM32 的内存。&lt;/p&gt;
&lt;h2 id="2-startup_stm32f407xxs上电后第一棒"&gt;2. &lt;code&gt;startup_stm32f407xx.s&lt;/code&gt;：上电后第一棒
&lt;/h2&gt;&lt;p&gt;第二个关键文件是：&lt;code&gt;Startup/startup_stm32f407xx.s&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;它有两重身份。构建时，它先被汇编成目标文件，再和 &lt;code&gt;main.o&lt;/code&gt;、&lt;code&gt;system_stm32f4xx.o&lt;/code&gt; 一起链接：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;startup_stm32f407xx.s
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ 汇编
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;startup_stm32f407xx.o
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ 和其他 .o 一起链接
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;app.elf
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ 烧录
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;STM32 Flash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;它主要贡献几块内容：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;.isr_vector 中断向量表
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;.text.Reset_Handler 复位启动代码
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;.text.Default_Handler 默认中断处理代码
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;链接器再根据 &lt;code&gt;.ld&lt;/code&gt; 文件，把向量表和代码放进 Flash。&lt;/p&gt;
&lt;p&gt;不过，CPU 并不是从 &lt;code&gt;.s&lt;/code&gt; 文件第一行开始往下读。像 &lt;code&gt;.syntax&lt;/code&gt;、&lt;code&gt;.cpu&lt;/code&gt;、&lt;code&gt;.section&lt;/code&gt;、&lt;code&gt;.word&lt;/code&gt; 这些，大多是给汇编器看的指令或者数据，不是 CPU 上电后逐句执行的代码。&lt;/p&gt;
&lt;p&gt;假设 &lt;code&gt;BOOT0 = 0&lt;/code&gt;，芯片从内部 Flash 启动，硬件会先读取向量表前两项：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;0x08000000 的 32 位数 → 装进 MSP，成为初始栈顶
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;0x08000004 的 32 位数 → 装进 PC，跳到复位入口
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;对应到启动文件里就是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-asm" data-lang="asm"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nl"&gt;g_pfnVectors:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;.word&lt;/span&gt; &lt;span class="no"&gt;_estack&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="na"&gt;.word&lt;/span&gt; &lt;span class="no"&gt;Reset_Handler&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;于是整个过程变成：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;上电 / 复位
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CPU 读取 _estack
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CPU 读取 Reset_Handler 地址
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;跳进 Reset_Handler
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SystemInit → 初始化 .data/.bss → main
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;所以更准确的说法不是“CPU 从 startup 第一行开始执行”，而是：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;CPU 根据 startup 生成的向量表找到 &lt;code&gt;Reset_Handler&lt;/code&gt;，再开始执行它生成的复位代码。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;到这里，&lt;code&gt;.s&lt;/code&gt; 文件也没那么吓人了。它本质上就是在 C 世界开门之前，先把栈、内存和入口准备好。&lt;/p&gt;
&lt;h2 id="3-system_stm32f4xxc终于回到舒适区"&gt;3. &lt;code&gt;system_stm32f4xx.c&lt;/code&gt;：终于回到舒适区
&lt;/h2&gt;&lt;p&gt;最后一个关键文件是 &lt;code&gt;Platform/system_stm32f4xx.c&lt;/code&gt;。唯一一个 &lt;code&gt;.c&lt;/code&gt;，是不是瞬间亲切多了？&lt;/p&gt;
&lt;p&gt;这个文件来自 CMSIS，处在 startup 和 &lt;code&gt;main()&lt;/code&gt; 之间：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Reset_Handler
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SystemInit() ← system_stm32f4xx.c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;初始化 .data/.bss
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;main()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;启动文件里会调用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-asm" data-lang="asm"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;bl&lt;/span&gt; &lt;span class="no"&gt;SystemInit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;但 &lt;code&gt;startup.o&lt;/code&gt; 自己没有这个函数。链接时，&lt;code&gt;ld&lt;/code&gt; 会去 &lt;code&gt;system_stm32f4xx.o&lt;/code&gt; 里把同名符号找出来。之前那一大家子工具到这里终于串上了。&lt;/p&gt;
&lt;p&gt;这个文件里最值得先认识的是三个东西：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SystemInit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SystemCoreClockUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;SystemCoreClock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="systeminit"&gt;&lt;code&gt;SystemInit()&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;它会在复位后、进入 &lt;code&gt;main()&lt;/code&gt; 前运行，主要负责把 RCC 恢复到一个确定的基础状态、打开 HSI、处理 FPU 访问权限，以及按配置设置向量表或外部存储器。&lt;/p&gt;
&lt;p&gt;它一般不会替你完成这些事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置最终的 168 MHz 主频&lt;/li&gt;
&lt;li&gt;初始化 GPIO 和串口&lt;/li&gt;
&lt;li&gt;初始化 HAL&lt;/li&gt;
&lt;li&gt;初始化 FreeRTOS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些还是后面的板级初始化代码来干。&lt;/p&gt;
&lt;p&gt;这时候就有看到这里的吴彦祖、彭于晏困惑了：我不是已经在 CubeMX 的时钟树里把主频改成 168 MHz 了吗，怎么这里还是 16 MHz？&lt;/p&gt;
&lt;p&gt;因为 CubeMX 生成的时钟配置，是进入后续初始化阶段才执行的。&lt;code&gt;SystemInit()&lt;/code&gt; 只是先把芯片送回一个大家都认识的起点，然后你的代码再去开 HSE、配 PLL、切系统时钟。&lt;/p&gt;
&lt;h3 id="systemcoreclock"&gt;&lt;code&gt;SystemCoreClock&lt;/code&gt;
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;SystemCoreClock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;它只是一个记录当前 HCLK 的软件变量，很多地方会拿它计算 &lt;code&gt;SysTick&lt;/code&gt;、RTOS Tick、串口波特率和定时器参数。&lt;/p&gt;
&lt;p&gt;注意，它不是许愿池。你直接写：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;SystemCoreClock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;168000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;并不会让 MCU 原地超频到 168 MHz，只会让软件以为它已经到了。真正的时钟还是得改 RCC 寄存器，然后调用 &lt;code&gt;SystemCoreClockUpdate()&lt;/code&gt; 重新读取并计算。&lt;/p&gt;
&lt;h3 id="hse_value"&gt;&lt;code&gt;HSE_VALUE&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;HSE_VALUE&lt;/code&gt; 表示软件认为外部晶振是多少 Hz：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#ifndef HSE_VALUE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define HSE_VALUE 25000000U
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这不代表程序已经启用了 25 MHz HSE，更不代表你板子上真的焊了 25 MHz 晶振。板子是 8 MHz，就应该通过编译宏传入正确的值：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-cmake" data-lang="cmake"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;target_compile_definitions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt; &lt;span class="s"&gt;PRIVATE&lt;/span&gt; &lt;span class="s"&gt;HSE_VALUE=8000000U&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;要不然硬件跑它的，软件算软件的，最后两边各过各的。&lt;/p&gt;
&lt;p&gt;还有一个容易漏掉的点：这份启动文件会在 &lt;code&gt;.data&lt;/code&gt; 复制和 &lt;code&gt;.bss&lt;/code&gt; 清零之前调用 &lt;code&gt;SystemInit()&lt;/code&gt;。所以它不能随便依赖已经初始化好的普通全局变量、堆或者完整的 C 运行库，主要还是直接操作硬件寄存器。&lt;/p&gt;
&lt;p&gt;一句话总结：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;startup 负责把 C 程序的运行环境搭起来，&lt;code&gt;system_stm32f4xx.c&lt;/code&gt; 负责把 Cortex-M4 和 STM32F4 拉回一个确定的基础状态，后面的 Platform 代码再配置这块板真正需要的时钟和外设。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="最后交给-cmake"&gt;最后交给 CMake
&lt;/h2&gt;&lt;p&gt;必备文件当然还有 &lt;code&gt;main.c&lt;/code&gt;，这我就懒得说了。剩下主要是两个和 CMake 有关的文件：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;arm-none-eabi-gcc.cmake # 指定交叉编译工具链和 CPU 参数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CMakeLists.txt # 描述源文件、头文件、链接脚本和产物
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;工具链文件告诉 CMake 去哪里找 &lt;code&gt;arm-none-eabi-gcc&lt;/code&gt;，目标是 &lt;code&gt;Cortex-M4&lt;/code&gt;，要使用什么浮点 ABI；&lt;code&gt;CMakeLists.txt&lt;/code&gt; 再把 startup、system、main 和链接脚本串起来，最后顺手生成 &lt;code&gt;.elf&lt;/code&gt;、&lt;code&gt;.bin&lt;/code&gt;、&lt;code&gt;.hex&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;具体 CMake 我这里就不展开了。了解基本逻辑之后完全可以让 AI 写，再自己检查参数和路径。这里还要顺便吐槽一下 py：这种东西简单到极致，有啥难度，用的时候学就行了，我完全不知道刻意学习的意义在哪里。&lt;/p&gt;
&lt;p&gt;写好之后，工程结构如下：&lt;/p&gt;
&lt;p&gt;&lt;img alt="完整工程结构" class="gallery-image" data-flex-basis="159px" data-flex-grow="66" height="843" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://b0weny-qwq.github.io/Blog/p/stm32f407-rtos-prequel-build-from-scratch/image/index/%E5%B7%A5%E7%A8%8B%E7%BB%93%E6%9E%84.png" width="559"&gt;&lt;/p&gt;
&lt;p&gt;然后：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmake -S . -B build -G Ninja
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmake --build build
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;哦也，能编译了。&lt;/p&gt;
&lt;p&gt;恭喜你亲手拆开了 IDE 的编译黑盒。下一步再往里面塞 &lt;code&gt;RTOS&lt;/code&gt;，至少这次知道它到底站在什么东西上面了。牛逼吧。&lt;/p&gt;</description></item><item><title>Zephyr 入门（二）：设备树是什么，应用怎样使用它</title><link>https://b0weny-qwq.github.io/Blog/p/zephyr-lesson-2-devicetree/</link><pubDate>Fri, 10 Jul 2026 00:40:00 +0800</pubDate><guid>https://b0weny-qwq.github.io/Blog/p/zephyr-lesson-2-devicetree/</guid><description>&lt;h1 id="lesson-2-设备树是什么应用怎样使用它"&gt;Lesson 2: 设备树是什么，应用怎样使用它
&lt;/h1&gt;&lt;p&gt;上一课的闪灯程序里有这样一行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;GPIO_DT_SPEC_GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;DT_ALIAS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;led0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;gpios&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;它看起来没有写 GPIO 端口和引脚号，却能找到板上的 LED。这一课就把这条线拆开：&lt;code&gt;led0&lt;/code&gt; 从哪里来，它怎样变成具体硬件，以及为什么换一块兼容的板后，应用代码有机会不改。&lt;/p&gt;
&lt;h2 id="设备树是在替应用保管硬件细节"&gt;设备树是在替应用保管硬件细节
&lt;/h2&gt;&lt;p&gt;假设一块板把用户 LED 接在 &lt;code&gt;gpio0&lt;/code&gt; 的 12 号引脚，另一块板把它接在 &lt;code&gt;gpio1&lt;/code&gt; 的 7 号引脚。如果在应用中写死端口和引脚：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;HAL_GPIO_TogglePin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GPIOC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO_PIN_13&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这段代码就把“闪灯逻辑”和“这块板的接线”绑在了一起。换板时，功能本身没变，仍然只是闪灯，却要回到业务代码里找引脚号。&lt;/p&gt;
&lt;p&gt;Zephyr 的做法是把接线关系放进设备树。应用只说“我想操作默认 LED”或“我想使用这个 I2C 传感器”；设备树负责回答它接在哪个控制器、占用哪个引脚、地址和中断是什么。可以把它看成开发板附带的一张硬件清单：应用不必记住仓库里每个零件的位置，只需要按清单取用。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;应用 C 代码
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 使用 led0、uart0、传感器节点等逻辑入口
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;设备树
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 记录控制器、引脚、总线、地址、中断和连接关系
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;驱动
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 根据这些参数操作实际外设
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;设备树不会凭空让所有程序跨板运行。目标板需要有对应硬件描述和驱动；但当两块板都提供相同的逻辑入口时，应用不再需要关心 LED 实际接到 P0.13 还是 P1.7。&lt;/p&gt;
&lt;h2 id="一份设备树从哪里拼出来"&gt;一份设备树从哪里拼出来
&lt;/h2&gt;&lt;p&gt;以一块 STM32 开发板为例，最终设备树通常不是单个文件，而是几层描述叠加的结果。以下路径都相对于 &lt;code&gt;zephyr/&lt;/code&gt; 源码根目录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;boards/&amp;lt;厂商&amp;gt;/&amp;lt;开发板&amp;gt;/&amp;lt;开发板&amp;gt;.dts
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 板级硬件：板载 LED、按键、晶振、默认串口、外接器件等。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dts/&amp;lt;架构&amp;gt;/&amp;lt;厂商&amp;gt;/&amp;lt;SoC 系列&amp;gt;/*.dtsi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; SoC 硬件：片内 UART、GPIO、I2C、SPI、时钟、中断和内存地址。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dts/bindings/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; binding YAML：说明 compatible 对应的设备类型、属性和参数格式。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;可以这样理解它们的分工：SoC 的 &lt;code&gt;.dtsi&lt;/code&gt; 先说明“芯片里有几路 I2C、每路在哪个地址”；开发板的 &lt;code&gt;.dts&lt;/code&gt; 再说明“这块板把 LED 接到了哪个引脚”；应用 overlay 最后补充“这次项目要在 I2C0 上接一个传感器”。&lt;/p&gt;
&lt;p&gt;应用不应该直接改 Zephyr 自带的板级 DTS，否则升级 Zephyr 或切换版本时很难维护。通常在应用目录里添加 overlay：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;application&amp;gt;/app.overlay
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;application&amp;gt;/boards/&amp;lt;board&amp;gt;.overlay
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;构建后要看的不是某一层输入，而是最终合并结果：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;build/zephyr/zephyr.dts
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;build/zephyr/include/generated/devicetree_generated.h
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;如果你在 overlay 中改了 LED 引脚，实际却没有变化，先打开 &lt;code&gt;build/zephyr/zephyr.dts&lt;/code&gt; 搜索 &lt;code&gt;led0&lt;/code&gt; 或节点标签。它才是本次构建真正采用的硬件清单。&lt;/p&gt;
&lt;h2 id="先分清四个容易混淆的名字"&gt;先分清四个容易混淆的名字
&lt;/h2&gt;&lt;p&gt;同一个设备在 DTS 中可能有几种名字，刚开始很容易看花：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;节点名 DTS 树中的名字，例如 led_0、serial@4000xxxx。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;节点标签 供 DTS 内部引用的名字，例如 led0:、uart0:，引用时写 &amp;amp;led0。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;属性 节点的参数，例如 gpios、reg、status、compatible。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;alias /aliases 下给应用的逻辑名字，例如 led0、sw0。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;可以把节点标签理解为设备树内部的“书签”，而 alias 更像留给应用的快捷入口。两者名字可以相同，但不是同一个概念。尤其要记住：&lt;code&gt;DT_ALIAS(led0)&lt;/code&gt; 查的是 &lt;code&gt;/aliases&lt;/code&gt; 中的 &lt;code&gt;led0&lt;/code&gt;，不是节点名，也不是节点标签。&lt;/p&gt;
&lt;h2 id="例子从-led0-找到真实引脚"&gt;例子：从 &lt;code&gt;led0&lt;/code&gt; 找到真实引脚
&lt;/h2&gt;&lt;p&gt;下面是一段简化的板级 DTS。它把真实 LED 映射为通用别名：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dts" data-lang="dts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;/&lt;/span&gt;&lt;span class="cm"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;leds&lt;/span&gt;&lt;span class="cm"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;compatible&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gpio-leds&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;user_led&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;led_0&lt;/span&gt;&lt;span class="cm"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;gpios&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;&amp;amp;gpio0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GPIO_ACTIVE_HIGH&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;aliases&lt;/span&gt;&lt;span class="cm"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;led0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;&amp;amp;user_led&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里的 &lt;code&gt;user_led&lt;/code&gt; 是节点标签，&lt;code&gt;led_0&lt;/code&gt; 是节点名。真正描述接线的是 &lt;code&gt;gpios&lt;/code&gt;：LED 连到 &lt;code&gt;gpio0&lt;/code&gt; 的 12 号引脚，高电平有效。&lt;code&gt;aliases&lt;/code&gt; 则给它起了一个跨板更好使用的名字 &lt;code&gt;led0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;应用只要取这个别名：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;zephyr/drivers/gpio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define LED_NODE DT_ALIAS(led0)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;gpio_dt_spec&lt;/span&gt; &lt;span class="n"&gt;led&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GPIO_DT_SPEC_GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LED_NODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gpios&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这段代码并不是运行时去读取一个配置文件。宏会在编译期一路展开：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DT_ALIAS(led0)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; /aliases 中的 led0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; user_led 节点
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; gpios 属性
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; GPIO 控制器、12 号引脚、有效电平
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; struct gpio_dt_spec
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;随后就可以调用 &lt;code&gt;gpio_pin_configure_dt()&lt;/code&gt;、&lt;code&gt;gpio_pin_set_dt()&lt;/code&gt; 或 &lt;code&gt;gpio_pin_toggle_dt()&lt;/code&gt;。若另一块板把默认 LED 定义为 &lt;code&gt;led0&lt;/code&gt;，它可以接在完全不同的引脚上，而这段应用逻辑不必知道。&lt;/p&gt;
&lt;h2 id="例子给项目接一个-i2c-传感器"&gt;例子：给项目接一个 I2C 传感器
&lt;/h2&gt;&lt;p&gt;再看一个更接近项目的场景：开发板已经有 &lt;code&gt;i2c0&lt;/code&gt;，但板级 DTS 没有写你的 BME280 传感器。此时不需要复制整份板级 DTS，只要在应用的 &lt;code&gt;app.overlay&lt;/code&gt; 中补上一段：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-dts" data-lang="dts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nl"&gt;&amp;amp;i2c0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;okay&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;bme280&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;sensor&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="mi"&gt;76&lt;/span&gt;&lt;span class="cm"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;compatible&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;bosch,bme280&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;reg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mh"&gt;0x76&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这段话可以顺着读：找到已有的 &lt;code&gt;i2c0&lt;/code&gt;；确保它启用；在这条总线上声明一个地址是 &lt;code&gt;0x76&lt;/code&gt; 的设备；它遵循 &lt;code&gt;bosch,bme280&lt;/code&gt; 这份 binding 的规则。&lt;code&gt;bme280:&lt;/code&gt; 是节点标签，因此 C 代码可以按标签取到设备：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;zephyr/device.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sensor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;DEVICE_DT_GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;DT_NODELABEL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bme280&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;设备树到这里解决的是“传感器接在哪里、是什么类型”。它还不保证传感器一定可用：对应的驱动必须被 Kconfig 编译进固件，运行前也应该用 &lt;code&gt;device_is_ready(sensor)&lt;/code&gt; 检查设备是否准备好。下一课的 Kconfig 正好解释这个条件。&lt;/p&gt;
&lt;h2 id="dtsdtsioverlay-和-binding-各自负责什么"&gt;DTS、DTSI、overlay 和 binding 各自负责什么
&lt;/h2&gt;&lt;p&gt;回看上面的两个例子，这四类文件的边界就比较清楚了：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SoC .dtsi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 描述芯片内部已有的外设资源，好比芯片的数据手册。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;板级 .dts
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 描述开发板怎样使用这些资源，例如 LED 和按键接在哪里。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;应用 overlay
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 描述当前应用新增、启用或修改的硬件，例如接入自己的传感器。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;binding YAML
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 定义某种 compatible 允许哪些属性、数据格式和约束，像一份填写规则。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;构建系统把它们合并成 &lt;code&gt;build/zephyr/zephyr.dts&lt;/code&gt;，再生成 C 宏头文件。设备树不是运行时配置文件，所有宏都在编译期展开；改了 overlay 后必须重新构建，固件里的描述才会更新。&lt;/p&gt;
&lt;h2 id="本课结论"&gt;本课结论
&lt;/h2&gt;&lt;p&gt;设备树回答“硬件有什么、怎样连接、参数是什么”，应用代码再用设备树宏把这些信息交给驱动 API。读设备树时，先从板级 &lt;code&gt;.dts&lt;/code&gt; 看这块板怎样接线；需要了解片内资源时再追到 SoC &lt;code&gt;.dtsi&lt;/code&gt;；不清楚某个属性怎么写时查 binding；改完后始终以 &lt;code&gt;build/zephyr/zephyr.dts&lt;/code&gt; 为准。&lt;/p&gt;
&lt;p&gt;下一课会看 Kconfig：设备树已经说清“这里接着一颗 BME280”，Kconfig 还要决定“它的驱动是否真的进入这次固件”。&lt;/p&gt;</description></item><item><title>Zephyr 入门（一）：认识 Zephyr 工程</title><link>https://b0weny-qwq.github.io/Blog/p/zephyr-lesson-1-project-overview/</link><pubDate>Fri, 10 Jul 2026 00:30:00 +0800</pubDate><guid>https://b0weny-qwq.github.io/Blog/p/zephyr-lesson-1-project-overview/</guid><description>&lt;p&gt;这一系列想从 Zephyr 入门开始，补上之前一直想写却没动笔的内容。&lt;/p&gt;
&lt;h1 id="lesson-1-认识-zephyr-工程"&gt;Lesson 1: 认识 Zephyr 工程
&lt;/h1&gt;&lt;p&gt;Zephyr 是一个面向资源受限设备的开源实时操作系统（RTOS）。它有线程、调度和同步这些内核能力，但它想管的不只是“任务怎么调度”：驱动、硬件描述、配置、板级支持和构建流程也被放进了同一套体系。&lt;/p&gt;
&lt;p&gt;先想一个最常见的练习：每隔一秒翻转一次 LED。用 FreeRTOS 时，任务里的延时可以写成 &lt;code&gt;vTaskDelay()&lt;/code&gt;；但 LED 在哪个引脚、GPIO 时钟怎样开、引脚怎样初始化，仍要由 STM32 HAL、TI DriverLib 或其他厂商库处理。任务代码可以不变，换一块板后，&lt;code&gt;HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13)&lt;/code&gt; 这类外设代码和初始化往往还是得跟着改。&lt;/p&gt;
&lt;p&gt;Zephyr 希望把这部分差异挪到应用代码之外。应用更接近下面这种写法：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;gpio_dt_spec&lt;/span&gt; &lt;span class="n"&gt;led&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GPIO_DT_SPEC_GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;DT_ALIAS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;led0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;gpios&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;gpio_pin_toggle_dt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;led&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里没有出现 &lt;code&gt;GPIOC&lt;/code&gt; 或 &lt;code&gt;PIN_13&lt;/code&gt;。应用只说“切换这块板的默认 LED”；&lt;code&gt;led0&lt;/code&gt; 到底接在哪个 GPIO 控制器、哪个引脚，以及应该选用哪一个 GPIO 驱动，由开发板的设备树和 Kconfig 配置决定。换到另一块已支持、也提供 &lt;code&gt;led0&lt;/code&gt; 的板时，这段闪灯逻辑可以保持不动，只需要重新选择目标板构建。&lt;/p&gt;
&lt;p&gt;这不是魔法，也不是硬件差异消失了。可以把设备树看成一张接线和设备清单：它把“LED 接在 P0.13”这样的事实放在板级描述里，而不是散落在应用业务代码中。DMA、低功耗、特殊时钟等功能仍然要面对具体芯片的能力和限制；只是普通应用不必反复触碰同一组寄存器和引脚号。&lt;/p&gt;
&lt;p&gt;这也是 Zephyr 值得学习、同时需要先熟悉工程体系的原因。它适合 MCU、传感器节点、可穿戴设备等需要实时性和较小资源占用的场景，也覆盖了从小型开发板到部分带 MMU 的平台。刚接触时，最容易困惑的往往不是 API，而是应用、配置、设备树和驱动究竟怎样配合。&lt;/p&gt;
&lt;p&gt;这一课先不急着读内核。先建立整体印象：一份闪灯应用从哪些文件得到“这块板有什么”，又如何变成可以烧录的固件。理解了这条线，再回头读线程和调度会顺得多。&lt;/p&gt;
&lt;h2 id="先看整个工作区"&gt;先看整个工作区
&lt;/h2&gt;&lt;p&gt;用 West 初始化后的工作区通常长这样：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;workspace/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── .west/ West 工作区配置
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── zephyr/ Zephyr RTOS 源码
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── modules/ 第三方模块，例如 CMSIS、芯片 HAL
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── my-application/ 自己的应用工程
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;可以把这个目录想成一个项目工地：&lt;code&gt;my-application&lt;/code&gt; 是你正在写的产品代码，&lt;code&gt;zephyr&lt;/code&gt; 是公共框架和基础设施，&lt;code&gt;modules&lt;/code&gt; 放着芯片厂商和第三方提供的零件。Zephyr SDK 不在工地里，它安装在开发电脑上，提供交叉编译器、链接器等工具；它不属于 Zephyr 源码，也不会被烧录进开发板。&lt;/p&gt;
&lt;p&gt;刚开始最值得盯住的是两处：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;my-application/ 应用代码、应用配置、设备树覆盖
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zephyr/ RTOS、驱动、设备树、板级支持和构建规则
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;west&lt;/code&gt;、SDK 和 &lt;code&gt;modules/hal/&amp;lt;vendor&amp;gt;/&lt;/code&gt; 当然都重要，但前期不需要急着读实现。它们的作用分别是管理多个仓库、提供工具链，以及承载厂商 HAL；应用通常会通过 Zephyr 的驱动间接使用这些 HAL。&lt;/p&gt;
&lt;h2 id="west-到底负责什么"&gt;West 到底负责什么
&lt;/h2&gt;&lt;p&gt;Git 管理单个仓库的历史，West 管理的是一组彼此有依赖关系的仓库。可以把它理解为项目的“装配清单”：&lt;code&gt;zephyr/west.yml&lt;/code&gt; 记录 CMSIS、芯片 HAL 等模块该放在哪里、要使用哪个版本。执行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;west update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;West 会把工作区同步到清单指定的状态。比如同一个 Zephyr 版本需要某个版本的 STM32 HAL，&lt;code&gt;west update&lt;/code&gt; 会按清单取回匹配版本，避免每个人手动拼一套不兼容的仓库。它还把构建、烧录和调试入口统一起来：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;west build -b &amp;lt;board&amp;gt; &amp;lt;application-directory&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;west flash
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;west debug
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;不过 West 本身不编译 C 代码。真正干活的是 Zephyr 的 CMake 构建系统、构建工具和交叉编译器；West 更像是把这些步骤组织到同一个命令入口下。&lt;/p&gt;
&lt;h2 id="一份应用怎样变成固件"&gt;一份应用怎样变成固件
&lt;/h2&gt;&lt;p&gt;还是以闪灯程序为例。&lt;code&gt;main.c&lt;/code&gt; 只写“翻转 LED”，但编译器还不知道这颗 LED 接在哪里、GPIO 驱动要不要编进去。应用代码并不是直接丢给编译器，而是先经过配置和硬件描述的合并：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;src/main.c
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CMakeLists.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; find_package(Zephyr ...): 引入 Zephyr 构建系统
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; target_sources(...): 将应用源文件加入 app 目标
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;prj.conf
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 选择需要的 Kconfig 功能，例如 GPIO、日志、线程
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;板级 DTS + SoC DTSI + 应用 overlay
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 描述目标硬件，以及应用对硬件描述的修改
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;生成最终设备树、最终 Kconfig 和设备树 C 宏
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;交叉编译并链接
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zephyr.elf / zephyr.bin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;构建目录不是可以完全忽略的临时垃圾，它像是构建系统交卷前留下的草稿。比如你把 &lt;code&gt;led0&lt;/code&gt; 指到另一个引脚，却发现灯没有变化；这时先看最终合并出的设备树，而不是靠猜。遇到“明明改了配置却没生效”这类问题，也先来这里确认：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;build/zephyr/zephyr.dts
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 最终合并后的设备树。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;build/zephyr/.config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 最终生效的 Kconfig 配置。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;build/zephyr/include/generated/devicetree_generated.h
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 设备树生成的 C 宏。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="zephyr-和-zephyr-sdk-别混在一起"&gt;Zephyr 和 Zephyr SDK 别混在一起
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Zephyr： 会被编译进目标设备的 RTOS、驱动和板级支持源码。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Zephyr SDK： 在开发主机上执行编译、汇编、链接和二进制转换的工具链。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;它们之间的关系可以简单理解为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;应用源码 + Zephyr 源码
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | Zephyr SDK 提供交叉编译器、链接器等工具
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zephyr.elf / zephyr.bin
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;烧录到目标开发板并运行
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;所以日常开发主要修改应用源码、&lt;code&gt;prj.conf&lt;/code&gt; 和设备树；SDK 通常只在安装、升级或排查工具链问题时才需要关注。&lt;/p&gt;
&lt;h2 id="zephyr-目录该从哪儿看"&gt;&lt;code&gt;zephyr/&lt;/code&gt; 目录该从哪儿看
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kernel/ 线程、调度、定时器、同步机制
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;drivers/ GPIO、UART、I2C、SPI 等驱动框架
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;boards/ 开发板定义
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;soc/ 芯片和 SoC 支持
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dts/ 通用设备树定义与 bindings
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;include/zephyr/ 应用可包含的 API 头文件
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmake/、scripts/ 构建和代码生成规则
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;第一次打开 &lt;code&gt;zephyr/&lt;/code&gt; 时很容易被目录数量劝退。不建议马上扎进 &lt;code&gt;kernel/&lt;/code&gt;：先从自己的 &lt;code&gt;main.c&lt;/code&gt; 出发，找到它使用的 &lt;code&gt;led0&lt;/code&gt;，再看它在最终设备树里落到了哪个 GPIO。等这条“代码如何落到硬件上”的路走通，再读内核时，看到的就不再是一堆孤立文件。&lt;/p&gt;
&lt;h2 id="zephyr-能支持哪些硬件"&gt;Zephyr 能支持哪些硬件
&lt;/h2&gt;&lt;p&gt;Zephyr 不是给任意 MCU 都能直接编译运行的通用库。把一个 STM32 的应用拿去给一颗新的 Cortex-M 芯片编译，并不因为两者都是 Cortex-M 就会成功。要支持一颗芯片，架构、SoC、开发板定义、设备树 binding、驱动和工具链必须一起就位：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;应用代码
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Zephyr 通用 API
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;驱动 + 设备树 binding
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SoC 支持 + 启动代码 + 中断/时钟/内存配置
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;具体 MCU 硬件
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这有点像两辆车都使用同一款发动机，并不代表仪表、线束和控制器可以直接互换。判断某个 MCU 是否受支持，不能只看它是不是 Cortex-M；外设、时钟、中断控制器、内存布局、启动方式和所需驱动是否完成适配，同样关键。&lt;/p&gt;
&lt;h3 id="移植一颗新-mcu-为什么费工夫"&gt;移植一颗新 MCU 为什么费工夫
&lt;/h3&gt;&lt;p&gt;移植不只是让 &lt;code&gt;main()&lt;/code&gt; 跑起来，通常还要补齐下面这些环节：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CPU 架构层 异常入口、上下文切换、中断模型
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;启动与内存 启动代码、向量表、链接脚本、Flash/RAM 布局
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SoC 基础设施 时钟、复位、中断控制器、系统定时器
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;硬件描述 DTS、pinmux/pinctrl、设备树 bindings
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;外设驱动 GPIO、UART、SPI、I2C、DMA 等驱动及其 Kconfig
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;构建与调试 交叉工具链、烧录、调试器配置
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;同一 CPU 架构的异常和上下文切换部分可能可以复用，但真正花时间的通常是 SoC 基础设施、设备树和外设驱动。厂商 HAL 也不能自动解决问题：它只是提供了操作硬件的底层能力，还需要接入 Zephyr 的驱动模型、Kconfig 和设备树体系，应用才能通过统一 API 使用它。&lt;/p&gt;
&lt;h2 id="zephyr-怎样把-api-接到硬件上"&gt;Zephyr 怎样把 API 接到硬件上
&lt;/h2&gt;&lt;p&gt;Zephyr 希望应用表达“我要控制这块板定义的 LED”，而不是直接写寄存器地址或厂商专属的 GPIO 端口。比如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;gpio_pin_toggle_dt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;led&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这句 API 最终能改变引脚电平，靠的是下面几层接力：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;应用： 使用 GPIO API
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;设备树： 指定 LED 对应哪个 GPIO 控制器和引脚
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GPIO 驱动： 将 API 调用转换为该控制器的操作
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SoC/厂商 HAL： 访问具体外设寄存器
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;硬件： 改变引脚电平
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;其中三件事尤其重要。统一 API 是应用看到的操作面板，例如 GPIO、UART、I2C、SPI API；设备树像硬件清单，描述“板上有什么、连在哪里”；Kconfig 像装配选项，决定哪些功能和驱动会被编译进固件。&lt;/p&gt;
&lt;p&gt;抽象并不意味着硬件差异消失。目标板已经有相应 DTS 描述和驱动时，同一份应用代码才可能少改甚至不改地迁移；DMA、低功耗、特殊时钟和厂商私有外设功能仍然需要结合具体 SoC 的限制来处理。&lt;/p&gt;
&lt;h2 id="与-freertos-的应用开发模型对比"&gt;与 FreeRTOS 的应用开发模型对比
&lt;/h2&gt;&lt;p&gt;Zephyr 和 FreeRTOS 都能做实时嵌入式应用，区别主要不在“有没有线程和队列”，而在于平台覆盖的范围。前者更像一套带标准接口和装配规则的平台，后者的 Kernel 则专注把实时内核做好：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FreeRTOS Kernel
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 重点提供任务调度、队列、信号量、软件定时器等内核能力。
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Zephyr
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 除了内核，还把驱动模型、设备树、Kconfig、板级支持和构建流程
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 作为同一平台的一部分。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这并不代表 FreeRTOS 不能用驱动或厂商库。实际项目里，它常和厂商的启动代码、HAL、手写或厂商提供的外设驱动一起使用，只是这些组件不由 FreeRTOS Kernel 统一定义和管理。若项目只固定在一块板上，这种方式完全合理；当要支持多块板、维护多种芯片，或希望复用驱动和配置时，Zephyr 试图用一套接口、配置和硬件描述把这些部分串起来。&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;问题&lt;/th&gt;
 &lt;th&gt;FreeRTOS 项目中常见做法&lt;/th&gt;
 &lt;th&gt;Zephyr 项目中常见做法&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;创建任务/线程&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;xTaskCreate()&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;K_THREAD_DEFINE()&lt;/code&gt; 或 &lt;code&gt;k_thread_create()&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;延时&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;vTaskDelay()&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;k_sleep()&lt;/code&gt; 或 &lt;code&gt;k_msleep()&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;队列与同步&lt;/td&gt;
 &lt;td&gt;Queue、Semaphore、Event Group&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;k_msgq&lt;/code&gt;、&lt;code&gt;k_sem&lt;/code&gt;、&lt;code&gt;k_event&lt;/code&gt; 等对象&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;外设选择&lt;/td&gt;
 &lt;td&gt;厂商 HAL、手写寄存器配置或项目代码&lt;/td&gt;
 &lt;td&gt;通用驱动 API + DTS + Kconfig&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;引脚和外设实例&lt;/td&gt;
 &lt;td&gt;通常在 BSP、初始化代码或应用中配置&lt;/td&gt;
 &lt;td&gt;主要由设备树描述&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;编译开关&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;FreeRTOSConfig.h&lt;/code&gt; 加上工程配置&lt;/td&gt;
 &lt;td&gt;Kconfig，例如 &lt;code&gt;prj.conf&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;换一块板&lt;/td&gt;
 &lt;td&gt;常需要调整 BSP、时钟、引脚和驱动初始化&lt;/td&gt;
 &lt;td&gt;若目标板已被支持，通常通过板选择和 DTS/overlay 调整&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;API 名称不能机械地一一替换，两者的对象模型、内存管理方式和配置选项并不完全相同。学习入口也不同：FreeRTOS 往往从任务和调度开始；Zephyr 更适合先弄清应用如何经过配置、设备树和驱动连接到硬件，再进入内核。&lt;/p&gt;
&lt;h2 id="推荐的阅读顺序"&gt;推荐的阅读顺序
&lt;/h2&gt;&lt;p&gt;别从目录树里盲目挑文件。继续沿着“闪灯”这个小目标往下追，读完每一步都能马上验证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;应用的 &lt;code&gt;CMakeLists.txt&lt;/code&gt;：应用如何进入 Zephyr 构建系统。&lt;/li&gt;
&lt;li&gt;应用的 &lt;code&gt;prj.conf&lt;/code&gt;：应用请求启用哪些功能。&lt;/li&gt;
&lt;li&gt;目标板的 &lt;code&gt;.dts&lt;/code&gt;：板上有哪些硬件资源。&lt;/li&gt;
&lt;li&gt;应用的 &lt;code&gt;src/main.c&lt;/code&gt;：应用如何调用 Zephyr API。&lt;/li&gt;
&lt;li&gt;构建生成的 &lt;code&gt;zephyr.dts&lt;/code&gt;、&lt;code&gt;.config&lt;/code&gt; 和 &lt;code&gt;devicetree_generated.h&lt;/code&gt;：这次构建最终做了什么。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一课会把 &lt;code&gt;led0&lt;/code&gt; 这条线拆开，看看它如何一路找到真实引脚：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DT_ALIAS(led0) -&amp;gt; LED 节点 -&amp;gt; gpios 属性 -&amp;gt; GPIO 控制器与引脚 -&amp;gt; GPIO 驱动
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;顺着这条链路读下去，再进入驱动、初始化和内核，关系会清楚得多。&lt;/p&gt;</description></item><item><title>工具链搭建完成</title><link>https://b0weny-qwq.github.io/Blog/p/toolchain-bringup-stm32f1-mspm0/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://b0weny-qwq.github.io/Blog/p/toolchain-bringup-stm32f1-mspm0/</guid><description>&lt;h1 id="工具链搭建完成"&gt;工具链搭建完成
&lt;/h1&gt;&lt;p&gt;这段时间主要在折腾自己的嵌入式工具链。到 &lt;code&gt;5.18&lt;/code&gt; 这一天，基本可以算是阶段性搭建完成了。&lt;/p&gt;
&lt;p&gt;以前做嵌入式，很多流程其实是被厂商 &lt;code&gt;IDE&lt;/code&gt;、图形配置工具和各种零散脚本拖着走。能跑是能跑，但每次换平台、换板子、换环境，都像是在重新和工具链打一架。现在我更想把这些东西统一到一套可复现、可调用、可交给 &lt;code&gt;agent&lt;/code&gt; 执行的流程里：构建、烧录、检查、日志、错误定位，都尽量走明确的命令和工程结构。&lt;/p&gt;
&lt;p&gt;这次最重要的成果，是测试成功了两条 &lt;code&gt;ARM&lt;/code&gt; 架构相关的烧录链路。&lt;/p&gt;
&lt;p&gt;一条是经典的 &lt;code&gt;STM32F1&lt;/code&gt;。这基本算是我最熟悉的一类 &lt;code&gt;MCU&lt;/code&gt;，也是很多嵌入式开发者绕不开的起点。能把它从传统开发方式里抽出来，用现代一点的工程结构完成构建和烧录，对我来说像是把老朋友重新接进新的工作流。&lt;/p&gt;
&lt;p&gt;另一条是 &lt;code&gt;TI MSPM0&lt;/code&gt;。相比 &lt;code&gt;STM32F1&lt;/code&gt;，它更像是一个新的切入点：同样是 &lt;code&gt;ARM&lt;/code&gt; 生态，但厂商工具、SDK 组织方式、烧录链路和调试习惯都不完全一样。把 &lt;code&gt;MSPM0&lt;/code&gt; 也跑通，说明这套工具链不是只为某一个芯片临时拼出来的，而是有机会扩展成更通用的嵌入式开发底座。&lt;/p&gt;
&lt;p&gt;从结果看，&lt;code&gt;STM32F1&lt;/code&gt; 和 &lt;code&gt;MSPM0&lt;/code&gt; 两边都能完成烧录验证，这件事让我很兴奋。工程流开始从“某个项目能跑”变成“某类流程能复用”。只要构建、烧录、调试这些基础动作足够稳定，后面再接入更多芯片、板卡和模板，就不用每次都从一片混乱里重新开始。&lt;/p&gt;
&lt;h2 id="下一步认识-zephyr"&gt;下一步：认识 Zephyr
&lt;/h2&gt;&lt;p&gt;工具链跑通之后，我开始对 &lt;code&gt;Zephyr&lt;/code&gt; 这个 &lt;code&gt;RTOS&lt;/code&gt; 产生兴趣。&lt;/p&gt;
&lt;p&gt;以前接触 &lt;code&gt;RTOS&lt;/code&gt;，更多是从裸机工程往上加：任务、调度、信号量、队列、驱动抽象，一层一层把东西堆起来。这样学很扎实，但也容易停留在“我会移植一个系统”或者“我会用一套接口”的阶段。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Zephyr&lt;/code&gt; 给我的感觉不太一样。它不像只是一个单纯的内核，更像一整套面向现代嵌入式工程的系统：设备树、配置系统、构建系统、驱动模型、板级支持、跨平台抽象，这些东西组合在一起，才接近我想理解的“工程化嵌入式”。&lt;/p&gt;
&lt;p&gt;我想认识它，不只是为了会用一个新的 &lt;code&gt;RTOS&lt;/code&gt;，而是想看看成熟的嵌入式系统到底怎么组织复杂性。为什么它能支持那么多板子，为什么驱动可以被抽象成统一模型，为什么配置和构建能被系统化管理，这些问题都值得拆开看。&lt;/p&gt;
&lt;p&gt;工具链搭好只是第一步。烧录跑通代表入口打通了，有意思的事情还在后面：把底层硬件、构建系统、&lt;code&gt;RTOS&lt;/code&gt; 思想和 &lt;code&gt;agent&lt;/code&gt; 工作流接到一起，形成一套自己能长期维护、长期扩展的嵌入式工程体系。&lt;/p&gt;
&lt;p&gt;这是我接下来想继续推进的方向。&lt;/p&gt;</description></item></channel></rss>