news 2026/9/20 9:33:05

STM32裸机启动流程详解:从复位向量到main函数执行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32裸机启动流程详解:从复位向量到main函数执行

1. 从“Hello World”到芯片引脚:一条被忽略的执行路径

你写过多少次int main() { printf("Hello World!\n"); return 0; }
——大一实验室里敲下第一行C代码时,它像一个仪式:编译、链接、运行,终端弹出那行字,世界就此点亮。
但你有没有想过:当main()被调用之前,你的代码其实在哪儿?
return 0;执行完之后,它又去了哪里?
在PC上,这个问题的答案是“操作系统接管”,但在STM32这类裸机(bare-metal)系统里,没有操作系统兜底,没有进程调度器收尾,没有shell等待下一条命令——你的main()不是程序的起点,也不是终点;它只是整个启动链条中一个被精心安排的中间节点。

这正是标题里那个看似简单的问号所承载的真实重量:“你的代码后来去了哪里?”
不是哲学发问,而是工程实证:从你保存.c文件那一刻起,代码就踏上了一条由编译器、链接器、启动文件、复位向量、内存映射共同编织的确定性路径。这条路径上每一步都可查、可测、可干预。而绝大多数初学者甚至中级开发者,只盯着main()函数体内部的逻辑,却对它前后发生的几十毫秒内发生的底层动作一无所知——直到某天main()根本没被执行,或者执行到一半卡死,连调试器都连不上,才猛然发现:原来我们一直站在冰山尖上编程,而冰山本身,从未被真正看见。

我带过三届嵌入式实训班,每次讲到启动流程,总有学生举手问:“老师,为什么Keil新建工程后,main()就能自动运行?它是不是被‘魔法’调用了?”
我的回答永远是:“不是魔法,是约定;不是自动,是硬编码;不是默认,是强制。”
这个“约定”,就是ARM Cortex-M系列芯片的启动规范;这个“硬编码”,是启动文件(startup_stm32f103xb.s)里那一段段汇编指令;这个“强制”,是链接脚本(startup_stm32f103xb.s)里.text段必须从地址0x08000000开始加载的铁律。
而所有这些,最终都服务于一个最朴素的目标:main()函数的地址,准确无误地加载进CPU的PC寄存器,并开始取指执行
这不是C语言的特性,而是你选择的芯片架构、工具链、启动方式共同定义的契约。一旦你脱离这个契约——比如手动修改了向量表偏移、误删了.data初始化代码、把全局变量放在了未使能的SRAM区域——main()就会成为一座孤岛:编译通过,烧录成功,但永远不会被调用。

所以,这篇文章不讲怎么点亮LED,不讲HAL库API怎么用,也不讲FreeRTOS任务怎么创建。它只做一件事:沿着代码从文本到机器指令的完整生命周期,一帧一帧拆解,带你亲眼看见main()是如何被“请”上CPU舞台的,以及它谢幕之后,系统又如何进入永恒静默或循环重启
适合谁读?

  • 正在用Keil/STM32CubeIDE写第一个GPIO控制程序,却搞不清“为什么main之前要初始化时钟”的人;
  • 遇到“程序烧进去没反应”“调试器连上但停在Reset_Handler”“全局变量初始值不对”等问题,查遍百度仍无头绪的人;
  • 已经能熟练使用HAL库,但想真正理解SystemInit()做了什么、__main符号是谁、_estack_sidata到底指向哪片物理内存的人;
  • 甚至包括那些正在设计Bootloader、做OTA升级、调试低功耗唤醒失败的工程师——因为所有这些高级功能,都建立在对启动流程绝对掌控的基础之上。
    接下来的内容,不会出现一句“你应该……”,而是直接告诉你:这一行汇编在做什么,这个链接地址为什么必须是0x08000000,这段C代码生成的机器码实际存放在Flash哪个扇区,以及当你按下复位键的瞬间,CPU的PC寄存器究竟从哪一行代码开始取指

2. 复位向量表:芯片上电后的第一份“寻址地图”

当你按下STM32开发板上的复位键,或者给VDD引脚施加电源,芯片内部的复位电路立刻响应:将CPU核心、总线矩阵、外设时钟全部拉回初始状态。此时,ARM Cortex-M内核(以F103为例)做的第一件事,不是执行任何C代码,甚至不是执行任何汇编指令——而是从固定地址读取两个32位字(Word)
这个地址,就是整个启动流程的绝对原点:0x00000000
但请注意:对于绝大多数STM32芯片(如F1/F4/H7系列),这个地址并不直接对应Flash的物理起始地址。它对应的是向量表(Vector Table)的基地址。而向量表的存放位置,由启动模式(BOOT0/BOOT1引脚状态)决定。最常见的配置是:BOOT0=0,BOOT1=x,此时芯片从主Flash启动,向量表实际位于Flash首地址0x08000000
于是,CPU上电后,硬件逻辑自动将0x08000000处的32位数据加载进栈指针SP(Stack Pointer),将0x08000004处的32位数据加载进程序计数器PC(Program Counter)。
这就是整个系统的“第一帧画面”。

我们来看一个真实的STM32F103CBT6最小系统向量表片段(十六进制dump):

Address: 0x08000000 Data: 0x20005000 // Initial Stack Pointer (SP) Address: 0x08000004 Data: 0x08000185 // Reset Handler Entry Point (PC) Address: 0x08000008 Data: 0x08000199 // NMI Handler Address: 0x0800000C Data: 0x080001AD // Hard Fault Handler ... Address: 0x0800007C Data: 0x080002A1 // SVC Handler Address: 0x08000080 Data: 0x080002B5 // PendSV Handler Address: 0x08000084 Data: 0x080002C9 // SysTick Handler

提示:向量表前两个字决定了系统能否启动。SP必须指向有效的RAM区域(如0x20000000~0x20005000),PC必须指向一个合法的、可执行的指令地址。如果SP指向非法地址(如0x00000000),CPU会在第一条指令执行前触发UsageFault;如果PC指向未编程Flash区域(全0xFF),CPU将执行0xFFFFFFFF指令,立即进入HardFault。

那么,0x08000004处的0x08000185是怎么来的?它指向哪里?
答案就在启动文件startup_stm32f103xb.s中。打开该文件,你会看到类似这样的汇编代码:

.section .isr_vector,"a",%progbits .type g_pfnVectors, %object .size g_pfnVectors, . - g_pfnVectors g_pfnVectors: .word _estack /* Top of Stack */ .word Reset_Handler /* Reset Handler */ .word NMI_Handler /* NMI Handler */ .word HardFault_Handler /* Hard Fault Handler */ ...

这里定义了一个名为g_pfnVectors的向量表,其中第一项.word _estack对应栈顶地址,第二项.word Reset_Handler对应复位处理函数入口。链接器会将这个向量表段(.isr_vector)精确放置在Flash的起始位置(即0x08000000)。而Reset_Handler这个符号,在同一文件中被定义为一段汇编函数:

Reset_Handler: ldr r0, =_estack mov sp, r0 /* Set stack pointer */ ldr r0, =__main /* Call C library initialization */ bl __main bx lr /* This should never be reached */

注意:Reset_Handler并不直接跳转到main(),而是先调用__main。这个__main不是你写的main(),而是ARM C库(ARMCC或GCC的libgcc)提供的一个标准初始化函数。它的职责,远比名字暗示的更重。

2.1__main:C运行时环境的隐形建筑师

__main是ARM编译器工具链(ARMCC / GCC)自动生成并链接的一个关键符号。它不是用户代码,而是连接C语言抽象语法与裸机硬件现实的桥梁。它的核心任务有三项,且严格按顺序执行:

  1. 初始化.data:将Flash中存储的全局/静态变量初始值,复制到RAM中对应的.data区域;
  2. 清零.bss:将RAM中.bss区域(未初始化的全局/静态变量)全部置零;
  3. 调用用户main()函数:完成所有C运行时准备后,才真正跳转至你的int main(void)

我们用一个具体例子说明其必要性。假设你写了这样一段代码:

uint32_t led_state = 0x00000001; // .data段:有初始值 uint32_t counter; // .bss段:无初始值 int main(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // 使能GPIOA时钟 GPIOA->CRH &= ~GPIO_CRH_MODE0; // 清除PA0模式位 GPIOA->CRH |= GPIO_CRH_MODE0_1; // 设置PA0为推挽输出(10MHz) while(1) { if (led_state) { GPIOA->BSRR = GPIO_BSRR_BR0; // 置位PA0(点亮LED) } else { GPIOA->BSRR = GPIO_BSRR_BS0; // 复位PA0(熄灭LED) } for(volatile int i=0; i<1000000; i++); led_state ^= 0x00000001; } }

编译后,链接器会将led_state的初始值0x00000001存放在Flash的.data段(例如0x08000200),而将counter的存储空间分配在RAM的.bss段(例如0x20000000)。但上电时,RAM是随机值!如果不执行__main的初始化:

  • led_state在RAM中的值是未知的(可能是0xCAFEBABE),导致LED初始状态不可预测;
  • counter的值也是随机的,如果后续代码依赖其为0,就会产生逻辑错误。

__main正是通过读取链接脚本中定义的符号地址,完成这两步“搬运”和“清零”。这些符号在链接脚本(如STM32F103C8Tx_FLASH.ld)中明确定义:

_estack = 0x20005000; /* Top of RAM */ _sidata = 0x08000200; /* Start address of .data section in Flash */ _sdata = 0x20000000; /* Start address of .data section in RAM */ _edata = 0x20000020; /* End address of .data section in RAM */ _sbss = 0x20000020; /* Start address of .bss section */ _ebss = 0x20000040; /* End address of .bss section */

__main内部伪代码逻辑如下:

// 伪代码,实际为汇编实现 void __main(void) { uint32_t *flash_ptr = (uint32_t*)_sidata; // Flash中.data起始地址 uint32_t *ram_ptr = (uint32_t*)_sdata; // RAM中.data起始地址 uint32_t size = _edata - _sdata; // .data段大小(字节) for(uint32_t i=0; i<size; i+=4) { *ram_ptr++ = *flash_ptr++; // 逐字复制 } uint32_t *bss_ptr = (uint32_t*)_sbss; uint32_t bss_size = _ebss - _sbss; for(uint32_t i=0; i<bss_size; i+=4) { *bss_ptr++ = 0; // 逐字清零 } // 最后,跳转到用户main函数 main(); }

注意:__main是编译器内置函数,你无法在源码中找到其实现,但可以通过反汇编.elf文件验证其存在。在Keil中,右键__main符号选择“Go To Definition”,会跳转到ARM库文档;在GCC+OpenOCD环境下,arm-none-eabi-objdump -d your_project.elf | grep "__main"可看到其汇编指令流。

2.2 向量表偏移:当你的代码不从0x08000000开始

在实际项目中,尤其是涉及Bootloader的场景,向量表往往不能放在Flash起始地址。例如,Bootloader通常占据前16KB(0x00000000~0x00003FFF),而Application则从0x00004000开始。此时,若仍按默认向量表位置加载,CPU会从Bootloader的向量表启动,而非你的Application。

解决方案是:动态重定位向量表基地址。Cortex-M内核提供了一个专用寄存器VTOR(Vector Table Offset Register),位于SCB(System Control Block)中,地址为0xE000ED08。你可以通过以下C代码,在main()开头手动设置:

// 假设Application的向量表位于0x08004000 #define APPLICATION_VECTOR_TABLE_BASE 0x08004000 SCB->VTOR = APPLICATION_VECTOR_TABLE_BASE;

但这必须在任何中断使能(__enable_irq())之前执行,否则中断发生时CPU仍会从旧VTOR地址取向量。更安全的做法是在Reset_Handler中完成:

Reset_Handler: ldr r0, =_estack mov sp, r0 ldr r0, =APPLICATION_VECTOR_TABLE_BASE ldr r1, =0xE000ED08 /* SCB->VTOR address */ str r0, [r1] /* Write new VTOR */ ldr r0, =__main bl __main bx lr

关键细节:VTOR寄存器的低7位(bit[6:0])必须为0,即向量表基地址必须是128字节对齐(2^7)。因此0x08004000是合法的(0x4000 & 0x7F == 0),而0x08004001则会导致HardFault。这是硬件强制要求,与链接脚本中.isr_vector段的ALIGN(128)属性直接对应。

3. 链接脚本与内存布局:代码如何被“分门别类”地塞进芯片

如果说向量表是CPU启动的“导航图”,那么链接脚本(Linker Script)就是整个程序在芯片内存中的“城市规划图”。它精确规定了:.text(代码)放哪儿、.data(已初始化数据)放哪儿、.bss(未初始化数据)放哪儿、堆(heap)和栈(stack)的边界在哪。没有它,编译器生成的.o目标文件只是一堆零散的二进制块,无法形成可执行的固件镜像。

以STM32F103C8T6(64KB Flash, 20KB RAM)为例,其典型链接脚本STM32F103C8Tx_FLASH.ld结构如下:

/* Highest address of the user mode stack */ _estack = 0x20005000; /* Top of RAM */ /* Generate a link error if heap and stack don't fit into RAM */ _Min_Heap_Size = 0x200; /* Required amount of heap */ _Min_Stack_Size = 0x400; /* Required amount of stack */ /* Memories definition */ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K } SECTIONS { .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) /* Startup code */ . = ALIGN(4); } >FLASH .text : { . = ALIGN(4); *(.text) /* .text sections (code) */ *(.text*) /* .text* sections (code) */ *(.rodata) /* .rodata sections (constants) */ *(.rodata*) /* .rodata* sections (constants) */ . = ALIGN(4); _etext = .; /* Define a symbol for the end of text */ } >FLASH .data : { . = ALIGN(4); _sdata = .; /* Create a symbol for the start of .data */ *(.data) /* .data sections (initialized data) */ *(.data*) /* .data* sections (initialized data) */ . = ALIGN(4); _edata = .; /* Define a symbol for the end of .data */ } >RAM AT> FLASH .bss : { . = ALIGN(4); _sbss = .; /* Create a symbol for the start of .bss */ *(.bss) /* .bss sections (uninitialized data) */ *(.bss*) /* .bss* sections (uninitialized data) */ *(COMMON) /* COMMON sections (uninitialized data) */ . = ALIGN(4); _ebss = .; /* Define a symbol for the end of .bss */ } >RAM /* User_heap_stack section, used by startup code to initialize heap/stack */ ._user_heap_stack : { . = ALIGN(4); . = . + _Min_Heap_Size; . = . + _Min_Stack_Size; . = ALIGN(4); } >RAM }

这份脚本的核心逻辑,可以用一张表格清晰呈现:

内存段存储位置物理地址范围内容来源初始化方式典型用途
.isr_vectorFlash0x08000000~0x0800007Fstartup_stm32f103xb.s编译时固化复位向量、中断向量
.textFlash0x08000080~0x0800xxxx*.c编译生成的机器码编译时固化所有函数体、常量字符串、const变量
.rodataFlash.text连续const int x = 5;编译时固化只读数据,如字符串字面量、const数组
.dataFlash (源) → RAM (运行时)Flash:0x0800yyyy~0x0800zzzz; RAM:0x20000000~0x2000001Fint x = 10;__main运行时复制已初始化的全局/静态变量
.bssRAM0x20000020~0x2000003Fint y;__main运行时清零未初始化的全局/静态变量
heapRAM0x20000040~0x200001FFmalloc()动态分配运行时管理动态内存分配(如malloc,calloc
stackRAM0x20000200~0x200005FF(向下增长)函数调用、局部变量复位时由_estack设定函数参数、返回地址、局部变量

提示:.data段的“AT> FLASH”属性是关键。它告诉链接器:.data段的内容(初始值)存储在Flash中(AT表示“stored at”),但其运行时地址(LOADADDR)在RAM中。这正是__main需要执行复制操作的根本原因——链接器无法在运行前将Flash内容“搬”到RAM,只能靠启动代码完成。

理解这个布局,能直接解决大量“玄学”问题。例如:

  • 问题:“为什么我在main()里给全局数组赋初值,烧录后数组内容还是乱码?”
    根因:该数组被错误地放在了.bss段(如声明为static int arr[10];且未赋初值),而.bss段在__main中被清零,覆盖了你后续的赋值。正确做法是显式初始化:static int arr[10] = {0};,使其进入.data段。

  • 问题:“为什么启用printf后程序跑飞,或者串口打印出乱码?”
    根因printf依赖_sbrk系统调用分配堆内存,而默认链接脚本中_Min_Heap_Size可能过小(如仅0x200字节)。当printf尝试分配缓冲区时,_sbrk返回的地址超出RAM范围,导致写入非法内存。解决方案是增大_Min_Heap_Size,或禁用浮点格式化(--specs=nano.specs)以减小printf体积。

  • 问题:“为什么我把一个大数组uint8_t buffer[64*1024];声明为全局变量,程序就无法启动?”
    根因:该数组被放入.bss段,大小64KB,远超RAM容量(20KB)。链接器虽能通过,但运行时__main试图清零64KB内存,会越界写入,破坏栈或其他关键数据。正确做法是将其声明为static const uint8_t buffer[64*1024] = {...};,放入.rodata段(Flash),或使用malloc动态分配(需确保heap足够)。

3.1 手动验证:用objdumpnm看透你的固件

理论终需实践验证。以下是在Windows CMD或Linux终端中,用GNU工具链快速分析固件的方法(假设已安装arm-none-eabi-gcc):

  1. 查看符号表,确认关键地址

    arm-none-eabi-nm your_project.elf | grep -E "_estack|_sidata|_sdata|_edata|_sbss|_ebss|main"

    输出示例:

    20005000 A _estack 08000200 A _sidata 20000000 A _sdata 20000020 A _edata 20000020 A _sbss 20000040 A _ebss 08000185 T main

    这里清晰显示:main函数地址为0x08000185.data段在RAM中从0x20000000开始,到0x20000020结束(32字节),其初始值存于Flash的0x08000200

  2. 反汇编Reset_Handler,看启动流程

    arm-none-eabi-objdump -d your_project.elf | sed -n '/<Reset_Handler>/,/^$/p'

    输出关键片段:

    08000185 <Reset_Handler>: 8000185: 4807 ldr r0, [pc, #28] ; (80001a4 <Reset_Handler+0x1f>) 8000187: 4685 mov sp, r0 8000189: 4806 ldr r0, [pc, #24] ; (80001a4 <Reset_Handler+0x1f>) 800018b: f000 f81e bl 80001ca <__main> 800018f: 4770 bx lr
  3. 查看内存映射,确认段分布

    arm-none-eabi-objdump -h your_project.elf

    输出示例:

    Sections: Idx Name Size VMA LMA File off Algn 0 .isr_vector 00000080 08000000 08000000 00008000 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 1 .text 00000180 08000080 08000080 00008080 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 2 .data 00000020 20000000 08000200 00008200 2**2 CONTENTS, ALLOC, LOAD, DATA 3 .bss 00000020 20000020 20000020 00008220 2**2 CONTENTS, ALLOC, NOLOAD, DATA

    这里VMA(Virtual Memory Address)是运行时地址,LMA(Load Memory Address)是加载地址。.data段的VMA=0x20000000(RAM),LMA=0x08000200(Flash),完美印证了链接脚本的AT> FLASH指令。

这些命令不是炫技,而是你在调试“程序不启动”、“变量值异常”、“内存越界”等问题时,最直接、最权威的证据来源。它让你摆脱“猜”和“试”,进入“看”和“证”的工程阶段。

4.main()之后:当return 0;执行完毕,CPU去哪了?

在PC上,main()返回后,控制权交还给C运行时库(CRT),CRT调用exit()函数终止进程,操作系统回收资源。但在STM32裸机环境中,没有exit(),没有进程概念,没有资源回收机制。那么,当你的main()函数执行完最后一行代码(无论是return 0;还是自然结束),CPU会做什么?

答案是:执行main()函数末尾自动生成的bx lr指令,将返回地址(即__main的下一条指令)加载进PC,然后继续取指执行。而__main函数的末尾,正是bx lr——它会将控制权返回给Reset_Handlerbl __main指令的下一条,即bx lr

此时,Reset_Handler也执行完毕,CPU会从当前PC(即Reset_Handler末尾)继续取指。由于Reset_Handler之后的内存是.text段的其他代码(如SystemInitmain等),CPU会开始执行这些指令。但main()已经结束,SystemInit早已执行过,接下来的指令很可能是未定义的垃圾数据,或者跳转到某个非法地址,最终触发HardFault。

然而,在绝大多数STM32工程中,你永远不会看到这个HardFault,因为你的main()函数里几乎必然包含一个无限循环

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); while(1) { // ← 这个while(1)是关键! HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); HAL_Delay(500); } }

这个while(1),本质上是一个主动的、可控的“停机指令”。它让CPU在一个固定的地址(0x08000185附近)不断执行b.n(无条件跳转到自身)指令,进入一种稳定、可预测的低功耗等待状态。此时,CPU不再取新指令,功耗降至最低(相对于运行状态),且随时可以被中断唤醒。

注意:while(1)并非C语言标准要求,而是嵌入式开发的工程惯例。如果你真的写了一个没有while(1)main(),比如:

int main(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); return 0; // 或者直接结束 }

那么程序行为将是:点亮LED →main返回 →__main返回 →Reset_Handler返回 → CPU执行垃圾指令 → HardFault → 进入HardFault_Handler → 如果该Handler未实现,CPU将死锁在HardFault向量地址(0x0800000C),表现为“程序跑飞后彻底无响应”。

4.1main()的“返回值”:一个被忽略的接口契约

C语言标准规定,main()函数的返回类型为int,其返回值用于向“宿主环境”报告程序退出状态。在POSIX系统中,这个值会被父进程通过wait()获取。但在STM32上,“宿主环境”是什么?答案是:不存在。因此,return 0;return 1;的数值本身没有任何语义,既不会被读取,也不会影响系统行为。

然而,这个int返回类型,却在底层汇编层面产生了真实影响。我们对比两种声明方式:

// 方式A:标准int main(void) int main(void) { return 0; } // 方式B:void main(void) —— 非标准,但某些旧教程使用 void main(void) { }

编译后,方式A生成的汇编会包含明确的返回值设置:

main: movs r0, #0 // r0 = 0 (return value) bx lr // return to caller

而方式B则没有movs r0, #0指令。虽然这对STM32运行毫无影响(因为没人读取r0),但它违反了ARM AAPCS(ARM Architecture Procedure Call Standard)调用约定:函数返回时,整数返回值必须放在r0寄存器。这意味着,如果你在main()中调用了其他遵循AAPCS的函数(如HAL_GPIO_ReadPin()),而main()本身不遵守该约定,理论上可能导致调用链混乱(尽管实践中极少发生)。

更重要的是,void main(void)在现代编译器(如GCC 10+)中会产生警告:

warning: 'main' function returns 'void' [-Wmain]

因为它明确违背了ISO C标准(C11 5.1.2.2.1)。坚持使用int main(void),不仅是遵循标准,更是向工具链发出明确信号:这是一个符合规范的C程序入口,所有优化、链接、调试信息都将按标准流程处理。

4.2 主动停机与低功耗:超越while(1)的优雅退出

while(1)是简单粗暴的有效方案,但它让CPU持续运行,消耗不必要的电流。在电池供电或对功耗敏感的应用中(如无线传感器节点),我们需要更优雅的“退出”方式:让CPU进入睡眠模式,等待中断唤醒

Cortex-M内核提供了多种低功耗模式,其中最常用的是Sleep模式(WFI - Wait For Interrupt)。修改main()循环如下:

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); // 配置一个外部中断(如按键)作为唤醒源 HAL_NVIC_EnableIRQ(EXTI0_IRQn); HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0); while(1) { HAL_GPIO_T
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 9:29:19

AI率太高?从检测原理到复述改写,一套降低AIGC检测率的实战指南

先说我自己的结论&#xff1a;AI率高不高&#xff0c;其实从你按下回车让ChatGPT写第一段话的时候就已经注定了。后面所有降AI率的操作&#xff0c;都是在给前面偷的懒买单。这篇文章我会把自己从"ChatGPT生成初稿"到"通过学校AIGC检测"的全过程拆开讲&…

作者头像 李华
网站建设 2026/9/20 9:28:30

直流直流变换器设计入门:从Buck/Boost到拓扑选型与调试

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

作者头像 李华
网站建设 2026/9/20 9:26:37

GetQzonehistory完整教程:一键备份QQ空间历史说说

GetQzonehistory完整教程&#xff1a;一键备份QQ空间历史说说 【免费下载链接】GetQzonehistory 获取QQ空间发布的历史说说 项目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory GetQzonehistory 是一个免费开源的 Python 工具&#xff0c;用来备份 QQ 空…

作者头像 李华
网站建设 2026/9/20 9:25:26

显卡驱动卸载神器DDU:从蓝屏到干净重装完全指南

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

作者头像 李华