news 2026/9/16 6:51:53

手写STM32链接脚本:从复位向量到main的启动机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写STM32链接脚本:从复位向量到main的启动机制

写嵌入式的人大概都有过这个经历:在 Keil 里点一下 Build 就下载跑起来,从来没想过编译器背后把代码“放”到了哪里;直到某一天换成 GCC 工具链,第一次打开.ld文件,满屏的 MEMORY、SECTIONS、ALIGN 看得一头雾水。我第一次用 STM32F411 从零搭 GCC 工程时,就栽在这份链接脚本上——程序烧进去完全不跑,PC 指针停在 0x08000000 附近不动,最后才发现是向量表没有被放到 Flash 开头。这让我彻底明白了一件事:链接脚本不是“编译器自动生成、与我无关”的配置,而是从复位到 main() 这条路上最关键的底层契约。

这篇文章就以 STM32F411 为例,把从 CPU 上电、复位向量取出、到 C 语言 main() 真正执行之间的每一个环节讲透,重点放在如何手写一份可用的链接脚本,以及我在实际调试中踩过的几个坑。内容不深奥,但足够扎实,适合刚转到 GCC 工具链的人、从 IDE 转过来的嵌入式工程师、以及那些想把启动流程彻底弄明白的同学。

1. 为什么链接脚本是一切的起点

1.1 被 IDE“藏起来”的幕后工作者

在 MDK、IAR 这类集成开发环境里,链接脚本通常是自动生成、自动匹配的,你只需要选择芯片型号,编译器就帮你把 Flash、RAM 的布局算好了。这不是没有代价——代价就是你不会去思考自己写的代码到底被安排在了哪里。换到 GCC 工具链之后,这一切都暴露出来了:你用的是arm-none-eabi-gcc,编译出来的是一个一个零散的.o目标文件,没有人告诉你这些文件该放到内存的什么位置、栈顶在哪里、堆开多大。如果没有一份正确的链接脚本,链接器只会报一堆让人摸不着头脑的错误。

链接脚本干的事情,说穿了就是三件事:决定代码段放在哪、决定变量段放在哪、决定栈和堆的位置和大小。对 STM32F411 来说,Flash 在 0x08000000,RAM 在 0x20000000,这个地址是硬件手册里写死的,链接脚本只是把这些信息翻译给链接器听。

1.2 你手里的“内存地图”与“搬家清单”

我习惯把链接脚本理解为两份东西的合体:一份是内存地图,告诉链接器哪里有可用空间、空间有多大、属性是只读还是可读写;另一份是搬家清单,告诉链接器哪些“块”(section)要放到地图的哪个位置。

C 语言源文件经过编译后,产生的不只是机器指令,还有各类带名字的数据块。.text是代码,.rodata是只读常量,.data是已初始化但需要写入 RAM 的变量,.bss是未初始化、启动时应该清零的变量。它们原本是各自独立的,链接脚本负责把它们按顺序铺进 Flash 和 RAM。如果这份清单写错了,程序能编译成功但运行必定跑飞,甚至根本编不过去。

1.3 从源头理解 .ld 文件的价值

有人可能觉得:“反正我用 Makefile 里写好的模板链接脚本就行,为什么要去读它?”我的体会是,如果你只是点点鼠标写写应用逻辑,确实可以不读;但只要你遇到过下面任何一个问题,就必须回去翻链接脚本:

  • 程序编译通过,烧录后完全不运行,或者一上电就进 HardFault
  • 想给固件做 OTA 升级,第二个 App 不知道该链接到哪个地址
  • 想把一个大数组放到外部 RAM
  • 想调整堆、栈的大小,却发现程序的崩溃问题可能和它有关
  • 移植 RTOS 后任务栈总是不对劲

这些问题的根因,往往都能追溯到链接脚本上。

2. 复位向量表:从零地址到 Reset_Handler 的第一次跳转

2.1 复位不是“从头执行”,而是“查表跳转”

STM32F411 使用的是 Cortex-M4 内核,它的启动方式和很多教科书里讲的 51、AVR 完全不同。51 单片机上电后 PC 直接指向 0x0000,从那里开始执行第一条指令;而 Cortex-M 系列是一个“查表”的过程——CPU 上电后会从地址 0x00000000 取出一个数作为主栈指针(MSP)的初始值,再从地址 0x00000004 取出一个数作为复位向量,也就是 Reset_Handler 的地址,然后跳过去执行。

这个向量表是整个启动流程的起点。程序烧录进 STM32F411 的 Flash 后,由于 Flash 的首地址 0x08000000 会被 Cortex-M 内核映射到地址 0x00000000,所以向量表必须出现在 Flash 的最前面。因此,链接脚本里第一段放的一定是.isr_vector段。

2.2 向量表长什么样

向量表本质上是一串 4 字节对齐的函数指针数组。STM32F411 的启动文件startup_stm32f411xe.s里会定义这样一个表:

偏移内容说明
0x00__initial_sp复位后的主栈指针初始值,通常指向 RAM 末尾
0x04Reset_Handler复位后执行的第一段代码
0x08NMI_Handler不可屏蔽中断
0x0CHardFault_Handler硬件错误中断
0x10MemManage_Handler内存管理错误
0x14BusFault_Handler总线错误
0x18UsageFault_Handler未定义指令/异常
......其余外设中断按顺序排

第 0 项特别关键,它是栈指针的初始值。如果这里写错了,CPU 一复位就有问题;如果链接脚本没有把向量表放在 Flash 开头,CPU 查表时读到的就是一个随机数,后果不用多说。

2.3 复位的几类来源与“异步复位同步释放”

讲启动流程绕不开“复位”这件事。STM32F411 的复位来源大致分四类:上电复位(POR)、外部复位(NRST 引脚拉低)、看门狗复位(IWDG/WWDG)、软件复位(NVIC_SystemReset())。

这里值得多说一句“异步复位同步释放”。复位信号本身是异步的,但释放时如果恰好和时钟沿靠得很近,内部触发器可能出现亚稳态——也就是说 CPU 收到的复位信号是“犹犹豫豫”的,一会儿高一会儿低,最终状态不确定。芯片内部会把人眼看到的“复位消失”信号再同步到时钟域里,让内核从确定的、干净的复位状态开始运行。这个概念和链接脚本没有直接关系,但理解了它,你就知道为什么不能只靠外部 RC 复位电路就指望 CPU 每次都在同一个状态醒来。

回到地址上:无论哪种复位,CPU 都会回到向量表,重新取 MSP、重新取 PC,所以链接脚本对向量表的安排是绝对不能被破坏的。

2.4 BOOT 引脚决定“从哪个地址映射”

STM32F411 的 BOOT0 引脚决定复位后从哪个存储器启动。BOOT0 拉低,从主 Flash 启动,地址 0x08000000 被映射到 0x00000000;BOOT0 拉高,从系统存储器启动,那里是芯片出厂自带的 bootloader(可以通过串口等方式下载固件)。系统存储器模式下,向量表跑到 bootloader 所在的地址,而不是你写的 Flash 地址。这个知识在调试“程序不跑”时需要特别注意——很多时候不是链接脚本错了,而是 BOOT0 引脚接错。

3. MEMORY 与 SECTIONS:手写 STM32F411 链接脚本的核心骨架

3.1 先给一份可以直接用的脚本

下面是一份针对 STM32F411RE 的精简链接脚本,512KB Flash、128KB RAM,配合startup_stm32f411xe.s和 GCC 工具链可以正常工作。我建议你先复制到工程里跑通,再跟着后面的讲解一点点改。

/* stm32f411re.ld */ ENTRY(Reset_Handler) _estack = ORIGIN(RAM) + LENGTH(RAM); _Min_Heap_Size = 0x200; _Min_Stack_Size = 0x400; MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K } SECTIONS { .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) . = ALIGN(4); } >FLASH .text : { . = ALIGN(4); *(.text) *(.text*) *(.glue_7) *(.glue_7t) *(.eh_frame) . = ALIGN(4); } >FLASH .rodata : { . = ALIGN(4); *(.rodata) *(.rodata*) . = ALIGN(4); } >FLASH _sidata = LOADADDR(.data); .data : { . = ALIGN(4); _sdata = .; *(.data) *(.data*) . = ALIGN(4); _edata = .; } >RAM AT> FLASH .bss : { . = ALIGN(4); _sbss = .; *(.bss) *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; } >RAM .heap : { . = ALIGN(8); _sheap = .; . = . + _Min_Heap_Size; . = ALIGN(8); _eheap = .; } >RAM .stack : { . = ALIGN(8); _sstack = .; . = . + _Min_Stack_Size; . = ALIGN(8); } >RAM /DISCARD/ : { libc.a ( * ) libm.a ( * ) libgcc.a ( * ) } }

3.2 ENTRY 与 MEMORY:先圈出地盘

第一行ENTRY(Reset_Handler)是告诉链接器,整个程序的入口符号是Reset_Handler。对 Cortex-M 来说,真正的入口其实由向量表里的第二项决定,ENTRY主要影响调试器对“程序入口”的判断,但写上是规范习惯。

MEMORY 命令是整张内存地图的框架:

  • FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K表示 Flash 起始地址 0x08000000,大小为 512KB,属性是r(只读)和x(可执行)
  • RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K表示 SRAM 起始地址 0x20000000,大小为 128KB,属性是xrw(可读可写可执行)

属性不是摆设。如果你试图把一个可执行代码段放进只有rw属性的区域,链接器会有意见;而把只读数据段放到 RAM 里,也不算合理的方案,只是不会报错而已。对于 F411 全系,Flash/RAM 地址都一样,区别只在容量:F411CE/RC/VE 这类后缀 C 的型号 Flash 是 256KB,后缀 E 的型号是 512KB,RAM 全部都是 128KB。所以换型号时,最常改的就是 MEMORY 里的 LENGTH。

3.3 SECTIONS:把段按顺序铺进地盘

SECTIONS 命令内部从上到下就是最终内存布局的顺序。对于 STM32F411 这种从 Flash 启动的 MCU,顺序通常是:

  1. .isr_vector:向量表必须放在 Flash 偏移 0 的位置
  2. .text:所有代码
  3. .rodata:只读常量,如const修饰的全局变量、字符串字面量
  4. .data:已初始化全局变量,运行时在 RAM,初始值在 Flash
  5. .bss:未初始化/零初始化全局变量,运行时在 RAM
  6. .heap.stack:堆与栈区域

每个段后面的>FLASH>RAM表示这个段的 VMA(运行时地址)落在哪个内存区域。.data那行的特殊写法>RAM AT> FLASH在下一章细说。

约等于 C 语言里的“定义”:*(.text)表示把所有.o文件里的.text段收集起来,*(.text*)表示匹配所有以.text开头的段。链接器按顺序填充地址,前面一个段结束的位置就是后一个段开始的位置。

3.4 ALIGN 和 KEEP 这两个“小东西”不能省

ALIGN(4)表示把当前位置向上对齐到 4 字节边界。Cortex-M 的向量表要求 4 字节对齐,ARM 指令也不是 2 字节就是 4 字节对齐,所以每个段之间都补一句ALIGN(4)是非常稳妥的习惯。

KEEP(*(.isr_vector))的意思是:即使编译时使用了--gc-sections做垃圾回收,也要保留这个段。汇编启动文件里的向量表本质上只是一段数据,链接器不知道它有用,不强制保留的话,它很可能被当垃圾回收掉。这个坑我踩过,后面专门讲。

4. 从 Flash 到 RAM 的“搬家公司”:启动代码与 LMA/VMA 的配合

4.1 为什么 .data 非要“搬一次家”

这里要引入两个概念:VMA(Virtual Memory Address,运行时地址)和 LMA(Load Memory Address,加载地址)。

代码段.text在 Flash 里,CPU 直接从 Flash 取指令执行,所以它的 VMA 和 LMA 都是 Flash 地址,不需要搬家。但.data段不一样。

全局变量的特点是有初值、需要在运行时能读能写。Flash 掉电不丢数据,但只能读;RAM 可以随意读写,但掉电清零。这两者结合的结果就是:.data段的初始值必须躺在 Flash 里(LMA 在 Flash),但运行时要被搬运到 RAM 里(VMA 在 RAM)。链接脚本里这句:

.data : { ... } >RAM AT> FLASH

>RAM是 VMA,AT> FLASH是 LMA。链接器会把初始值放在 Flash 的某个位置,同时把运行时地址安在 RAM 的某个位置。搬运任务交给启动代码。

用一个生活类比:Flash 是仓库,RAM 是工作台。你有一批乐高零件(已初始化变量),平时装在仓库箱子里(Flash),开工的时候要把箱子搬到工作台上拆开(RAM),才方便拼装。关灯下班后工作台上的零件会消失,但仓库里的箱子还在,第二天重新搬一次就行。

4.2 谁负责执行搬运?

在 ARMCC/Keil 的默认工程里,这个搬运由 C 运行时库的__main完成;在 GCC 工具链中,通常由启动汇编文件startup_stm32f411xe.s里的Reset_Handler完成。逻辑上等价于下面这段 C 代码:

extern uint32_t _sidata; extern uint32_t _sdata; extern uint32_t _edata; extern uint32_t _sbss; extern uint32_t _ebss; void Reset_Handler(void) { // 1. 从 Flash 拷贝 .data 初始值到 RAM uint32_t *src = &_sidata; uint32_t *dst = &_sdata; while (dst < &_edata) { *dst++ = *src++; } // 2. 将 .bss 段清零 for (dst = &_sbss; dst < &_ebss; dst++) { *dst = 0; } // 3. 进入 C 世界 main(); }

注意到_sidata_sdata_edata这些符号并不是真正的 C 变量,它们只是链接脚本里定义出来的“地址标签”。C 代码里对它们取地址,得到的才是真实地址。链接脚本和启动文件就是靠这些符号“握手”的。

4.3 .bss 段为什么要清零

C 语言标准规定,未初始化的全局变量和静态变量的初始值应为 0。问题是这些变量没有初始值躺在 Flash 里,直接给它们在 RAM 里划一块空间装上即可,不需要 Flash 占地方。但 RAM 上电后的内容是随机垃圾,所以启动过程中必须把它们清零。

如果哪一天_sbss_ebss符号对不上,或者启动代码里漏掉了清零循环,你会看到全局变量一开始是乱数,而且这个乱数是随编译环境变化而变化的,非常难查。

4.4 栈指针是谁给的

栈指针初始值来自向量表第一项__initial_sp。链接脚本里:

_estack = ORIGIN(RAM) + LENGTH(RAM);

定义了一个符号_estack,值等于 RAM 的最末尾地址。启动汇编文件则把这个符号写进向量表第一项:

g_pfnVectors: .word _estack .word Reset_Handler ...

也就是说,RAM 末尾是栈顶,生长方向向下。这个初始值由链接脚本给出,再由启动文件放进向量表,最后 CPU 复位时从向量表读出栈指针。一条链路贯穿下来,少了任何一环都不行。

5. 藏在链接脚本里的握手符号:_estack、PROVIDE、KEEP 与对齐规则

5.1 启动代码与链接脚本之间的“暗号”

链接脚本不只是给链接器看的内存地图,它还定义了一批特殊符号,专门供启动文件、库函数和用户代码引用。常见的有:

符号含义
_estack栈顶地址,等于 RAM 最末尾
_Min_Stack_Size栈最小尺寸
_Min_Heap_Size堆最小尺寸
_sdata/_edata.data 段的起始/结束地址
_sidata.data 初始值在 Flash 里的起始地址,即 LMA
_sbss/_ebss.bss 段的起始/结束地址
_sheap/_eheap堆的起始/结束地址

这些符号看起来像变量,但你用 C 语言访问它们时,标准姿势是取地址:

extern uint32_t _estack; uint32_t stack_top = (uint32_t)&_estack;

如果写成uint32_t stack_top = (uint32_t)_estack;,运行时会把“_estack 符号所在地址里存放的值”取出来,那完全是另一回事了。这是我见过很多初学者容易晕的一个点。

5.2 PROVIDE:默认值还是自定义值?

有些链接脚本里会用PROVIDE(_estack = ...)这样的写法。PROVIDE的含义是:如果在某个目标文件中已经定义了同名强符号,就使用目标文件里的;否则才用链接脚本里提供的这个值。

这样设计的好处是:基础脚本给出默认值,但你可以在某个 C/汇编文件中重新定义,实现“按需覆盖”。比如你在启动文件里手动给过一个_estack定义,且它足够满足应用,那么链接脚本里的PROVIDE就不生效。但大多数工程都不会去覆盖它,维持默认就好。

5.3 KEEP 为什么救了我一命

前面说过,KEEP(*(.isr_vector))是防止链接器在垃圾回收时删掉向量表。GCC 工具链中,如果开启了-ffunction-sections -fdata-sections配合--gc-sections,链接器会按 section 粒度剔除“没被引用”的段。向量表在这种机制下很容易被误伤。

我实际遇到的情况是:用arm-none-eabi-size看固件体积一切正常,烧录后程序一动不动,调试器里 PC 停在 0xFFFFFFFF 附近。后来用arm-none-eabi-objdump -s firmware.elf一看,文件开头根本不是向量表。补上KEEP之后,程序首次上电就跑起来了。那次之后我每次写链接脚本都会看一眼.isr_vector段有没有 KEEP。

5.4 对齐:4 字节与 8 字节的门道

处理器对对齐的容忍度各不相同。Cortex-M4 在硬件上支持非对齐访问,但外设寄存器、中断向量表这类特殊场景依旧要求严格对齐,优异地采用非对齐访问也会降低性能。链接脚本里给每个段都补ALIGN(4),就是为了让段与段之间不出现错位的“缝隙”,也让每个符号都落在 4 字节边界上。

栈和堆用ALIGN(8)是另一个原因:AAPCS(ARM 架构过程调用标准)要求在进行函数调用时,栈要满足 8 字节对齐,这样库函数中使用 LDRD、STRD、SIMD 等指令时不会触发对齐异常。把.stack段的起始地址用 8 字节对齐,再配合_estack为 RAM 末尾,就能保证“栈顶 8 字节对齐”这条约束被满足。

6. 我在这块板子上踩过的链接坑位与排查流程

6.1 坑位一:RAM 容量写小一截,程序随机崩溃

有段时间我怀疑 F411 的 RAM 不够用,把链接脚本里的LENGTH = 128K随手改成了96K,抱着“反正我用不到那么多内存”的心态继续开发。结果固件正常编译、正常下载,跑简单任务没事,一旦运行到某个大数组比较密集的逻辑就随机死机。

原因很简单:把 RAM 区域声明成 96K,并不意味着物理 RAM 只剩 96K,链接器只是把前 96K“分配”出去,后面的 32K 不在内存管理范围内。如果某个变量访问越过了所谓的“内存末端”,就是纯裸奔,读写行为完全不可预期。

排查方法:打开.map文件,查所有变量地址,如果发现有数组的地址扎在 0x20018000 附近(即 96K 边界),再去对照物理 RAM 大小,问题就浮现了。老实说,这类问题不一定是链接脚本写错,也可能是你估算的内存需求和实际差距太大,但无论如何先把容量写对。

6.2 坑位二:忘记 KEEP,向量表被垃圾回收

这是最坑的一个。加了-Os -ffunction-sections -fdata-sections --gc-sections之后,整个.isr_vector段被链接器当成垃圾删掉了。现象:程序烧录后复位无反应,调试器全 0xFFFFFFFF,单步都进不去。

排查方式:用arm-none-eabi-objdump -s firmware.elf看文件开头是否有完整的向量表,或者用arm-none-eabi-objdump -h看 ELF 中 section 是否存在。如果发现.isr_vector段不存在,十有八九是漏了KEEP。这是链接脚本和编译选项相互作用的结果,纯靠“看代码”很难发现,必须走工具链排查流程。

6.3 坑位三:.bss 段漏掉*(COMMON),全局变量带垃圾值

早期 GCC 工具链中,某些编译器选项下未初始化全局变量会被放进COMMON块,而不一定放进.bss。如果链接脚本里只写了*(.bss)*(.bss*),就漏掉了COMMON,导致那部分全局变量在启动时没有被清零,程序一运行就是随机初值。

现在的主流 GCC 默认-fno-common,把常见变量都放进.bss,问题不太容易触发。但为了兼容不同工具链版本、不同库实现,标准链接脚本里一般都会写上*(COMMON)。这个习惯值得保留。

6.4 坑位四:链接脚本符号名与启动文件对不上

有时候你会遇到这种链接错误:undefined symbol: _sdata,或者反过来undefined symbol: __initial_sp。这通常不是链接脚本本身写错了,而是启动汇编文件的符号名和链接脚本不一致。不同版本工具链的启动文件可能使用不同的命名风格(比如带不带下划线),所以从网上复制来的.ldstartup文件必须配套使用,尽量用同一套模板。

6.5 排查流程:不要瞎猜,按步骤来

如果你也遇到了“上电不跑、复位之后没反应”的情况,我建议按这个顺序排查:

  1. 先用arm-none-eabi-objdump -h firmware.elf查看所有 section 是否存在、VMA 和 LMA 是否合理
  2. arm-none-eabi-objdump -s -j .isr_vector firmware.elf确认向量表确实在 Flash 开头,第一项是不是_estack的值
  3. arm-none-eabi-nm firmware.elf | grep _sdata检查_sdata_sbss_estack这些符号是否都被定义
  4. arm-none-eabi-size firmware.elf查看各段总大小,确认没有超出 Flash/RAM
  5. 最后再上调试器,单步观察复位后 SP 和 PC 是否从向量表正确加载

这五步基本能定位绝大多数与链接脚本相关的启动异常问题。我把它们单列出来,是因为我在实战中发现,很多人一遇到“程序不跑”就习惯性怪芯片坏了、晶振没起、调试器没接好,结果折腾半天,问题往往就藏在一份不起眼的.ld文件里。工具链提供者不会替你背这个锅,链接器给的信息也够多了,只是要做的是静下心去读它。

6.6 链接脚本的“进阶改装”场景

明白基础之后,链接脚本就是你在嵌入式工程里“画地图”的笔。常见的高级玩法包括:

  • 加外部 RAM:如果板子上外扩了 SDRAM/SRAM,在 MEMORY 里加一块区域,再新增一个 section 把它分配过去即可
  • 改 Flash 偏移做 OTA 固件升级:做 YMODEM 这类串口升级方案时,App 固件通常要链接到0x08020000这类偏移地址,把 MEMORY 中的 FLASH ORIGIN 改成对应偏移,同时注意设置 SCB->VTOR 重定位中断向量表
  • 双区 A/B 升级:两份固件分别链接到不同 Flash 区域,靠 bootloader 决定从哪个区启动,链接脚本里给两个 App 分别准备一套 ORIGIN 参数
  • 调整堆栈大小:跑复杂协议栈或 RTOS 时,需要扩大_Min_Stack_Size_Min_Heap_Size,直接把数值改大,再配合 map 文件确认不会撞到.bss

这些改动的底层逻辑,都是“改内存地图”,而地图的骨架就是这篇文章里讲的 MEMORY 和 SECTIONS。把基础打牢,后面的扩展会顺手得多。


最后说一点自己的体会。我刚接触 GCC 工具链时,总觉得链接脚本是一堆晦涩的符号和花括号,能跑就行,不想管。直到有一次程序复位不跑,我耐着性子把链接脚本、启动文件、map 文件三者对着看了一遍,才发现问题不是出在“代码逻辑”,而是出在“代码不知道被放到了哪里”。从那以后,我看待.ld文件的视角就变了:它不是配置,也不是模板,而是 CPU、启动代码、C 运行时和你写的代码之间的一份契约。下次再有程序上电跑飞的情况,不妨先别急着怀疑硬件,打开 ELF 文件看一眼向量表还在不在——很多时候,答案就藏在那份你从未认真看过的链接脚本里。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 6:51:37

Linux开机卡在fsck exited with status code 4:原因定位与安全修复指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 6:50:31

大模型训练四阶段详解:从基础到RLHF实战

1. 大模型训练四阶段全景图上周在调试Llama 3时突然意识到&#xff0c;很多同行对大模型训练阶段的理解还停留在"预训练微调"的二分法。这就像把汽车制造简单分为"造零件"和"装整车"一样粗糙。经过与Anthropic和Meta工程师的多次交流&#xff0c…

作者头像 李华
网站建设 2026/9/16 6:48:55

基于TMP144与外部晶振的温度监测信号传递方案设计

1. 为什么是 TMP144 晶振的组合&#xff1a;系统架构与选型思路1.1 先从“信号传递”这个需求倒推系统架构提到温度监测&#xff0c;大多数人第一反应是 DHT22、DS18B20 这类现成模块。但如果仔细读一下标题里的需求——“监测和信号传递温度变化”&#xff0c;会发现这里面的…

作者头像 李华
网站建设 2026/9/16 6:48:28

U8固定资产月末结账报错BOF/EOF的排查与修复

U8固定资产模块到了月末结账这一步&#xff0c;财务那边点了“固定资产-处理-月末结账”&#xff0c;系统没给任何缓冲&#xff0c;直接弹出一个报错框&#xff1a;“BOF或EOF中有一个是真”。我第一次遇到的时候&#xff0c;客户财务主管就站在旁边&#xff0c;等着结账之后出…

作者头像 李华
网站建设 2026/9/16 6:48:24

STM32图书馆环境监测系统:CO₂温湿度PM2.5实时监控开源方案

1. 项目概述&#xff1a;为什么一个图书馆环境监测系统值得开源&#xff1f;你有没有在图书馆待过一整个下午&#xff0c;突然觉得喉咙发干、眼睛酸胀、脑袋昏沉&#xff1f;不是你状态不好&#xff0c;很可能是那台老旧的空调没调好新风量&#xff0c;CO₂浓度悄悄爬到了1200p…

作者头像 李华
网站建设 2026/9/16 6:48:14

Git Worktree + AI Coding Agent:并行开发隔离工作区实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华