<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>工具链 on Blog</title><link>https://b0weny-qwq.github.io/Blog/tags/%E5%B7%A5%E5%85%B7%E9%93%BE/</link><description>Recent content in 工具链 on Blog</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><lastBuildDate>Sun, 16 Aug 2026 01:42:00 +0800</lastBuildDate><atom:link href="https://b0weny-qwq.github.io/Blog/tags/%E5%B7%A5%E5%85%B7%E9%93%BE/index.xml" rel="self" type="application/rss+xml"/><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>传统开发和现代开发之间，差的不是会不会写代码</title><link>https://b0weny-qwq.github.io/Blog/p/traditional-development-modern-development-gap/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://b0weny-qwq.github.io/Blog/p/traditional-development-modern-development-gap/</guid><description>&lt;h1 id="传统开发和现代开发之间差的不是会不会写代码"&gt;传统开发和现代开发之间，差的不是会不会写代码
&lt;/h1&gt;&lt;p&gt;我最近越来越觉得，很多人学嵌入式，问题不在于不努力，而是一开始就被带进了一条很旧的路。&lt;/p&gt;
&lt;p&gt;最典型的情况就是：一入门就去 &lt;code&gt;B&lt;/code&gt; 站找视频。&lt;/p&gt;
&lt;p&gt;这件事本身没问题。视频对初学者很友好，能让人很快看到一个东西怎么跑起来，也能压住刚接触硬件、寄存器、外设、开发板时的恐惧感。问题在于，很多人找视频时不看年份，不看适合什么基础阶段，也不会判断这套教程背后的工程方式是不是已经落后。&lt;/p&gt;
&lt;p&gt;路径依赖就这么形成了。&lt;/p&gt;
&lt;p&gt;别人用什么软件，他就用什么软件；别人怎么建工程，他就怎么建工程；别人怎么点 &lt;code&gt;Keil&lt;/code&gt;，他就怎么点 &lt;code&gt;Keil&lt;/code&gt;；别人怎么复制例程，他就怎么复制例程。短期看起来是在学习，实际上很可能是在把自己训练成旧流程的执行者。&lt;/p&gt;
&lt;p&gt;再往后，这种依赖会把人和现代开发方式直接隔开。&lt;/p&gt;
&lt;h2 id="教程会教你怎么用但很少教你怎么看工程"&gt;教程会教你怎么用，但很少教你怎么看工程
&lt;/h2&gt;&lt;p&gt;很多教学视频最大的问题，是只告诉你“怎么用”。&lt;/p&gt;
&lt;p&gt;怎么新建工程，怎么点按钮，怎么配置外设，怎么把例程跑起来，怎么照着老师的步骤把灯点亮、把串口跑通、把屏幕刷出来。&lt;/p&gt;
&lt;p&gt;这些东西对入门有用，但远远不够。&lt;/p&gt;
&lt;p&gt;真正决定工程能力的，不是你能不能照着教程把某个外设跑起来，而是你能不能看懂一个项目为什么这样组织：目录为什么这么分，驱动为什么这样抽象，接口为什么这样暴露，模块之间为什么不能乱依赖。&lt;/p&gt;
&lt;p&gt;这些视野，普通入门视频很少会给。&lt;/p&gt;
&lt;p&gt;因为它的目标往往只是让你“跑起来”，而不是让你理解一个项目如何长期维护、如何多人协作、如何跨平台迁移、如何被工具链和 &lt;code&gt;agent&lt;/code&gt; 接管一部分重复流程。&lt;/p&gt;
&lt;p&gt;如果长期停留在这种学习方式里，工程能力和协作能力都会被压低。&lt;/p&gt;
&lt;p&gt;你会很熟悉某个视频里的步骤，但离开那个模板就不会做；你能改一个例程，却不知道怎么设计模块；你能把功能堆上去，却不知道怎么控制复杂度。最后写出来的东西能跑，但不能碰。&lt;/p&gt;
&lt;h2 id="很多传统习惯已经不适合现在了"&gt;很多传统习惯已经不适合现在了
&lt;/h2&gt;&lt;p&gt;我不是要全盘否定传统嵌入式开发。&lt;/p&gt;
&lt;p&gt;在过去，很多习惯是有现实原因的。比如反复仿真、在 &lt;code&gt;Keil&lt;/code&gt; 里盯寄存器、一步一步单步调试，这些在当时完全可以理解。早期开发板、芯片、下载器、调试环境都没现在这么方便，&lt;code&gt;Flash&lt;/code&gt; 擦写次数也确实是一个需要考虑的因素。&lt;/p&gt;
&lt;p&gt;那时候谨慎一点、慢一点、靠仿真多确认几遍，很正常。&lt;/p&gt;
&lt;p&gt;但现在是什么年代了？&lt;/p&gt;
&lt;p&gt;很多小工程里的静态错误、函数报错、类型问题、依赖缺失、拼写错误、配置问题，根本不需要人一直在 &lt;code&gt;Keil&lt;/code&gt; 里来回点。你完全可以让 &lt;code&gt;agent&lt;/code&gt; 看编译日志，让它跑静态检查，让它根据报错定位问题，再由人判断修改是否合理。&lt;/p&gt;
&lt;p&gt;构建、检查、修明显错误，本来就应该自动化。&lt;/p&gt;
&lt;p&gt;不是所有问题都需要上来就进仿真器，也不是所有问题都值得人肉盯寄存器。寄存器当然要会看，底层当然要懂，但不能把所有开发问题都退化成“我打开 IDE 手动调一遍”。&lt;/p&gt;
&lt;p&gt;现代开发要做的，是把重复动作交给工具，把人放回判断层。&lt;/p&gt;
&lt;p&gt;如果一个人一直停留在传统流程里，把大量时间消耗在低级重复劳动上，那他的工程能力提升会非常慢。因为他每天都在处理表层问题，而不是在训练自己理解结构、边界、抽象和系统。&lt;/p&gt;
&lt;h2 id="入门视频可以看但不能一直靠视频"&gt;入门视频可以看，但不能一直靠视频
&lt;/h2&gt;&lt;p&gt;入门视频可以看。&lt;/p&gt;
&lt;p&gt;刚开始学的时候，视频能帮你建立基本直觉：开发板长什么样，工程怎么打开，代码怎么烧进去，串口怎么打印，外设怎么验证。这些东西如果完全靠文档硬啃，确实会让很多人卡在门口。&lt;/p&gt;
&lt;p&gt;但视频只能当入口，不能当长期学习方式。&lt;/p&gt;
&lt;p&gt;到了一定阶段之后，必须从被动看教程，切换成主动读文档、主动搭工程、主动查资料、主动设计接口。&lt;/p&gt;
&lt;p&gt;尤其是嵌入式开发，真正重要的信息往往都在官方文档、芯片手册、SDK 说明、开源项目、构建脚本和代码结构里。只会看视频的人，很容易养成一种很危险的习惯：没有教程就不会写，没有例程就不会动，没有别人演示就不知道下一步。&lt;/p&gt;
&lt;p&gt;这不是学习，是依赖。&lt;/p&gt;
&lt;p&gt;现代工程师必须学会从文档开始工作。看到一个新芯片、新 SDK、新 RTOS、新库，应该能自己读说明、看示例、拆结构、建最小工程、验证接口，而不是第一反应去搜“有没有保姆级教程”。&lt;/p&gt;
&lt;p&gt;保姆级教程看多了，人也会被养成保姆级工程师。&lt;/p&gt;
&lt;h2 id="要从优秀开源工程里学工程"&gt;要从优秀开源工程里学工程
&lt;/h2&gt;&lt;p&gt;想提升工程能力，光看教程不够。优秀开源工程必须多看。&lt;/p&gt;
&lt;p&gt;不是只看它某个函数怎么写，而是要看完整结构：项目目录怎么组织，模块怎么分层，驱动怎么抽象，接口怎么命名，平台相关代码怎么隔离，配置系统怎么处理，测试怎么放，文档怎么写，构建系统怎么把这些东西串起来。&lt;/p&gt;
&lt;p&gt;工程能力不是单点技巧，而是一整套判断。&lt;/p&gt;
&lt;p&gt;为什么这个模块不应该依赖那个模块，为什么这个文件应该拆出去，为什么这个接口不能暴露太多细节，为什么这个实现要留出可移植空间，为什么一个功能不能随便塞到全局变量和宏定义里，这些东西都需要长期观察、模仿、实践和复盘。&lt;/p&gt;
&lt;p&gt;看优秀工程，本质上是在训练工程审美。&lt;/p&gt;
&lt;p&gt;你要逐渐建立一套自己的工程准则：代码要可读，结构要清晰，接口要稳定，依赖要克制，平台差异要隔离，重复逻辑要收束，文档要能解释边界，构建要能复现。&lt;/p&gt;
&lt;p&gt;写屎山绝对不应该被纵容。&lt;/p&gt;
&lt;p&gt;不是说代码一开始就要完美，而是你不能把“能跑”当成唯一标准。能跑只是底线，可维护、可扩展、可协作，才是工程的要求。&lt;/p&gt;
&lt;h2 id="软件和硬件都需要审美"&gt;软件和硬件都需要审美
&lt;/h2&gt;&lt;p&gt;我一直觉得，软件和硬件其实很像画画。&lt;/p&gt;
&lt;p&gt;刚开始照猫画虎当然有效。别人怎么画，你先跟着画；别人怎么写工程，你先模仿；别人怎么设计电路，你先照着理解。这个阶段很正常，因为人一开始都需要参照物。&lt;/p&gt;
&lt;p&gt;但如果想往高处走，迟早要有自己的创作和风格。&lt;/p&gt;
&lt;p&gt;你要知道什么样的结构是干净的，什么样的接口是克制的，什么样的布局是可读的，什么样的封装是有意义的，什么样的复杂度是危险的。&lt;/p&gt;
&lt;p&gt;这就是审美，不玄。&lt;/p&gt;
&lt;p&gt;工程审美不是玄学。它来自大量阅读、大量实践、大量踩坑，也来自你对系统边界和长期维护成本的敏感度。&lt;/p&gt;
&lt;p&gt;一个只会复制粘贴的人，也许能把功能堆出来，但他很难成为系统工程师。因为系统工程师要负责的不只是某一段代码能不能跑，而是整个系统能不能被理解、被维护、被扩展、被交给别人协作。&lt;/p&gt;
&lt;p&gt;这也是我区分 &lt;code&gt;CV&lt;/code&gt; 工程师和系统工程师的标准。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CV&lt;/code&gt; 工程师只关心哪里有现成代码，怎么复制过来，怎么让它先跑起来。系统工程师会关心这段代码放进来之后，项目结构会不会被污染，接口会不会变乱，依赖会不会失控，后面的人还能不能读懂。&lt;/p&gt;
&lt;p&gt;一个在拼功能，一个在建系统。&lt;/p&gt;
&lt;h2 id="现代开发要求人主动升级"&gt;现代开发要求人主动升级
&lt;/h2&gt;&lt;p&gt;传统开发不是完全错误，它只是有时代背景。&lt;/p&gt;
&lt;p&gt;但如果到了现在，还把所有经验都停留在旧 IDE、旧教程、旧流程、旧例程里，那就不是“基础扎实”，而是没有升级。&lt;/p&gt;
&lt;p&gt;现代嵌入式开发已经不只是写寄存器和调外设。它包含工具链、构建系统、文档开发、自动化测试、开源协作、架构设计、跨平台抽象，也包含 &lt;code&gt;agent&lt;/code&gt; 介入之后的人机协作方式。&lt;/p&gt;
&lt;p&gt;人不该被低级流程绑住。&lt;/p&gt;
&lt;p&gt;该看视频的时候看视频，该读手册的时候读手册，该用 &lt;code&gt;agent&lt;/code&gt; 的时候用 &lt;code&gt;agent&lt;/code&gt;，该自己判断架构的时候自己判断。不同阶段要用不同方法，而不是拿入门阶段的方法用一辈子。&lt;/p&gt;
&lt;p&gt;如果只满足于“我照着教程能跑”，那最多只能停留在执行层。&lt;/p&gt;
&lt;p&gt;真正的提升，是从会用工具，到理解工程；从复制例程，到设计结构；从修局部错误，到把握系统边界；从照猫画虎，到形成自己的工程风格。&lt;/p&gt;
&lt;p&gt;到了这一步，才能说自己不是单纯的 &lt;code&gt;CV&lt;/code&gt; 工程师，而是在往系统工程师的方向走。&lt;/p&gt;</description></item><item><title>Agent 做嵌入式，核心还是把硬件开发接成 API</title><link>https://b0weny-qwq.github.io/Blog/p/agent-embedded-toolchain-hardware-api/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://b0weny-qwq.github.io/Blog/p/agent-embedded-toolchain-hardware-api/</guid><description>&lt;h1 id="agent-做嵌入式核心还是把硬件开发接成-api"&gt;Agent 做嵌入式，核心还是把硬件开发接成 API
&lt;/h1&gt;&lt;p&gt;最近折腾嵌入式工具链，我越来越确定一件事：&lt;code&gt;agent&lt;/code&gt; 想真正参与嵌入式开发，光会写几行 C 代码没用。硬件开发里那些原本靠人手操作、靠 IDE 点按钮、靠经验猜的动作，得先变成它能调用的接口。&lt;/p&gt;
&lt;p&gt;说白了，要给 &lt;code&gt;agent&lt;/code&gt; 一套清楚的 &lt;code&gt;API&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里的 &lt;code&gt;API&lt;/code&gt; 不一定是传统软件接口。构建、烧录、调试、寄存器读写、日志抓取，这些命令本身就可以是接口。只要它们稳定、可复现、能组合，&lt;code&gt;agent&lt;/code&gt; 就能接进来。&lt;/p&gt;
&lt;p&gt;我现在看工具链，最看重这一点：硬件开发必须先 &lt;code&gt;CLI&lt;/code&gt; 化，然后才谈得上 &lt;code&gt;agent&lt;/code&gt; 化。&lt;/p&gt;
&lt;h2 id="把硬件能力暴露出来"&gt;把硬件能力暴露出来
&lt;/h2&gt;&lt;p&gt;传统嵌入式开发把很多能力藏起来了。&lt;/p&gt;
&lt;p&gt;烧录藏在 IDE 的按钮后面，调试藏在某个工程配置里，芯片连接状态藏在调试器窗口里，寄存器值藏在图形界面的某个面板里。人当然可以点开看，但这些东西对 &lt;code&gt;agent&lt;/code&gt; 来说几乎是黑箱。&lt;/p&gt;
&lt;p&gt;一个动作只能靠人点按钮，就很难自动化，也很难复盘。按钮按下去之后发生了什么，参数是什么，失败原因在哪里，日志去哪儿了，很多时候没人说得清。&lt;/p&gt;
&lt;p&gt;写成命令之后，事情就不一样了。&lt;/p&gt;
&lt;p&gt;比如通过 &lt;code&gt;DAPLink&lt;/code&gt; 和 &lt;code&gt;OpenOCD&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-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openocd -f interface/cmsis-dap.cfg -f target/stm32f1x.cfg
&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;telnet&lt;/code&gt; 或者 &lt;code&gt;gdb&lt;/code&gt; 接进去，执行 &lt;code&gt;mdw&lt;/code&gt;、&lt;code&gt;mww&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;telnet localhost &lt;span class="m"&gt;4444&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mdw 0x40021000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mww 0x40021018 0x00000004
&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;到这一步，寄存器就不只是 IDE 面板里给人看的值了。它可以被命令读出来，被日志记下来，被脚本检查，也能被 &lt;code&gt;agent&lt;/code&gt; 调用。&lt;/p&gt;
&lt;p&gt;我认为这才是 &lt;code&gt;agent&lt;/code&gt; 接入嵌入式的方向。&lt;/p&gt;
&lt;p&gt;不是让它隔着一层 GUI 猜你现在点到了哪里，而是给它明确的工程入口：怎么构建，怎么烧录，怎么连接芯片，怎么读寄存器，怎么判断失败，怎么把结果反馈回来。&lt;/p&gt;
&lt;h2 id="传统嵌入式很难自然接收到这些东西"&gt;传统嵌入式很难自然接收到这些东西
&lt;/h2&gt;&lt;p&gt;我现在回头看，很多传统嵌入式开发者不是能力不行，而是知识结构被旧开发方式卡住了。&lt;/p&gt;
&lt;p&gt;很多人学嵌入式，从单片机、寄存器、外设、手册、例程、IDE 开始。这条路没问题，底层基础也重要。但如果一直停在“我会配寄存器、我会调外设、我会移植库”，就会错过现代工程真正变掉的部分。&lt;/p&gt;
&lt;p&gt;现在的重点已经不只是会不会点亮一个外设，而是你能不能把这个动作组织成可维护的系统。&lt;/p&gt;
&lt;p&gt;工程结构怎么设计，构建系统怎么统一，调试链路怎么复现，文档怎么沉淀，接口怎么暴露给机器，流程怎么交给 &lt;code&gt;agent&lt;/code&gt; 执行，这些东西不是传统嵌入式课本会系统教你的。&lt;/p&gt;
&lt;p&gt;所以知识面必须往外扩。&lt;/p&gt;
&lt;p&gt;只盯着底层是不够的。你还得理解 &lt;code&gt;CLI&lt;/code&gt;、脚本、构建系统、自动化、日志、测试、文档、&lt;code&gt;agent&lt;/code&gt; 工作流，甚至要理解一点现代软件工程里的架构和协作方式。因为这些东西加在一起，才决定一个项目能不能长期扩展，而不是只决定某一块板子今天能不能跑起来。&lt;/p&gt;
&lt;h2 id="提升工程能力不是课本学习"&gt;提升工程能力不是课本学习
&lt;/h2&gt;&lt;p&gt;我现在越来越觉得，工程能力不是把课本从第一页啃到最后一页啃出来的。&lt;/p&gt;
&lt;p&gt;课本会给你基础，手册会告诉你寄存器，数据结构和操作系统会给你底层概念，这些当然都要学。但真正让人产生工程能力的，往往是你在真实项目里不断遇到混乱，然后逼自己把混乱整理成结构。&lt;/p&gt;
&lt;p&gt;不是所有东西都值得从最底层重新推一遍。&lt;/p&gt;
&lt;p&gt;现在有 &lt;code&gt;agent&lt;/code&gt;，很多重复工作完全可以交出去。查资料、补脚本、整理命令、定位明显错误、生成模板、做初步排查，这些事情如果还全靠人肉硬扛，本质上是在浪费生产力。&lt;/p&gt;
&lt;p&gt;你可以借助 &lt;code&gt;agent&lt;/code&gt; debug，也应该这么做。&lt;/p&gt;
&lt;p&gt;但这不代表人就可以不懂原理。恰恰相反，&lt;code&gt;agent&lt;/code&gt; 介入之后，人更应该把精力放到更高层、更关键的位置：判断它给出的方向对不对，判断模块边界合不合理，判断问题到底是工具链、驱动、时序、内存、任务调度，还是硬件本身。&lt;/p&gt;
&lt;p&gt;工具越强，人越不能只当执行者。&lt;/p&gt;
&lt;h2 id="agent-的边界也很明显"&gt;Agent 的边界也很明显
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;agent&lt;/code&gt; 很适合处理局部问题，尤其是上下文边界清楚的问题。&lt;/p&gt;
&lt;p&gt;一个模块报错，一个脚本跑不通，一个寄存器值异常，一个构建依赖缺失，只要你给它足够明确的命令、日志和背景，它确实可以很快帮你推进。&lt;/p&gt;
&lt;p&gt;但当项目规模变大，问题就不是这么简单了。&lt;/p&gt;
&lt;p&gt;一万行代码的时候，&lt;code&gt;agent&lt;/code&gt; 还能大概读懂项目结构，顺着日志和模块关系做一些推断。可当代码从一万行变成十万行，外设、驱动、协议、任务、状态机、硬件版本、上位机、测试脚本全都搅在一起的时候，它就很容易吃不下。&lt;/p&gt;
&lt;p&gt;它不是不会写代码，是上下文有限。&lt;/p&gt;
&lt;p&gt;一个复杂嵌入式项目真正难的地方，往往不在某一行代码怎么改，而在整体结构为什么会变成这样。哪个模块本来就不该这样依赖，哪个接口从一开始就设计错了，哪个状态机迟早会失控，哪个底层时序问题会被上层逻辑掩盖，这些判断很难只靠短上下文推出来。&lt;/p&gt;
&lt;p&gt;这就是人还不能退场的地方。更准确地说，是有工程经验、懂底层原理、也懂架构设计的人不能退场。&lt;/p&gt;
&lt;h2 id="人应该站在更高的位置"&gt;人应该站在更高的位置
&lt;/h2&gt;&lt;p&gt;所以我现在看 &lt;code&gt;agent&lt;/code&gt; 做嵌入式：它不是万能程序员，也不是底层工程师的替代品。它更像一个能执行大量明确工程动作的协作者。&lt;/p&gt;
&lt;p&gt;前提是人先把动作整理出来。&lt;/p&gt;
&lt;p&gt;你要给它命令，给它接口，给它文档，给它边界，给它可以验证的结果。&lt;code&gt;DAPLink&lt;/code&gt;、&lt;code&gt;OpenOCD&lt;/code&gt;、&lt;code&gt;CMake&lt;/code&gt;、&lt;code&gt;Ninja&lt;/code&gt;、&lt;code&gt;GDB&lt;/code&gt;、日志系统、测试脚本，这些都不是孤立工具，而是把硬件开发变成可调用流程的组成部分。&lt;/p&gt;
&lt;p&gt;真正有价值的工程师，不是守着旧 IDE 和旧流程证明自己会吃苦，而是能把这些东西组织成系统。&lt;/p&gt;
&lt;p&gt;该深耕原理的时候要深耕。总线、寄存器、启动流程、异常、中断、调度、内存、时序，这些东西决定你在关键问题上能不能看穿本质。&lt;/p&gt;
&lt;p&gt;但也不能只深耕底层。只会底层，不懂工程组织，不懂工具链，不懂现代协作方式，也会被时代甩开。&lt;/p&gt;
&lt;p&gt;最重要的还是紧跟潮流，多学一点，把知识面撑开。工程能力不是把自己锁在某个知识点里死磕，而是在真实变化里更新工具、方法和判断。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;agent&lt;/code&gt; 时代的嵌入式开发，不是人退场，而是人要站到更高的位置。&lt;/p&gt;
&lt;p&gt;底层能力要有，架构判断要有，工具链也要能被机器调用。只有这样，&lt;code&gt;agent&lt;/code&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>