(嵌入式八股)No.3 STM32(一个月左右)
3.1 STM32启动流程(熟悉)
3.1.1 先了解一下什么是MSP和PSP
MSP(Main Stack Pointer)主堆栈指针
•用途:默认堆栈指针,用于处理模式(Handler Mode),即中断、异常处理等系统级操作。
•特点:
◦系统复位后自动启用;
◦所有异常处理(如中断服务程序)都必须使用MSP;
◦始终在特权模式下使用;
◦是全局共享的堆栈指针,系统内核使用它来处理紧急事务。
PSP(Process Stack Pointer)进程堆栈指针
◦用途:用于线程模式(Thread Mode)下的应用程序代码(如用户任务)。
◦特点:
▪需要显式配置,常用于多任务系统(如RTOS);不同任务切换的时候PSP指针就会指向不同的任务,这个在RTOS调试中也会用到!
▪每个任务可拥有独立的PSP,实现堆栈隔离;
▪可在特权或非特权模式下使用(取决于配置);
▪若某个任务发生堆栈溢出,不会影响其他任务或系统内核。
3.1.2 启动流程(熟悉)
1️⃣硬件复位序列(芯片自动完成)
上电/外部复位(POR/PDR 电路释放后就是重启之后,内核处于已知状态):
◦PC = 0x0000 0000
◦所有寄存器为默认值,Flash 等待周期按出厂设定。
取第一个字——主堆栈顶(MSP)
内核从 0x0000 0000 读出 32 位数值(也就是4个字节),装入 MSP。
(该值实际存放在 Flash 0x0800 0000,通过地址别名映射到 0x0000 0000。)
取第二个字——复位向量
从 0x0000 0004 读出 Reset_Handler 地址,跳转到该地址,开始执行用户代码。
2️⃣启动文件运行(startup_stm32xxxx.s)
4. 建立 C 运行环境
•把 Flash 中的 .data 常量拷贝到 RAM(已初始化全局变量)
•把 .bss 段全部清零(未初始化全局变量)
•可选:把 _estack 重新装入 MSP,确保栈顶对齐
系统级初始化
◦调用 SystemInit():使能 FPU、配置 RCC 时钟树、重映射中断向量表(写 SCB->VTOR)
◦调用 __libc_init_array():为 C++ 全局对象调用构造函数(纯 C 工程亦会执行)
进入用户世界
◦bl main(这个是BL是汇编指令,先记下返回地址(把下一条指令地址写入 LR 寄存器),再跳转到目标函数。)
从此开始执行用户 main(),启动流程结束。
3️⃣总结:
上电/复位
→ 硬件自动:取 MSP → 取 Reset_Handler → 跳转
→ 软件汇编:copy .data / zero .bss → SystemInit() → __libc_init_array()
→ main()
3.2 ARM架构(了解即可,寄存器务必掌握)
在嵌入式开发领域,ARM架构的处理器占了90%以上的市场份额, 大多数人学习嵌入式都是从ARM开始的。如果时间足够的话可以去学习一下简单的汇编指令。如果时间不够的话就深入了解ARM体系结构和工作流程。
3.2.1 ARM处理器的工作模式:(了解)
应用程序正常运行时,ARM处理器工作在用户模式(User mode),当程序运行出错或有中断发生时,ARM处理器就会切换到对应的特权工作模式。用户模式属于普通模式,有些特权指令是运行不了的,需要切换到特权模式下才能运行。在ARM处理器中,除了用户模式是普通模式,剩下的几种工作模式都属于特权模式。
3.2.2 寄存器及其作用(务必掌握)
在ARM处理器内部,除了基本的算术运算单元、逻辑运算单元、浮点运算单元和控制单元,还有一系列寄存器,包括各种通用寄存器、状态寄存器、控制寄存器,用来控制处理器的运行,保存程序运行时的各种状态和临时结果。
a.通用寄存器
R0-R12(通用寄存器)
▪R0~R3通常用来传递函数参数,
▪R4~R11用来保存程序运算的中间结果或函数的局部变量等(一般都是硬件自动保存)
▪R12常用来作为函数调用过程中的临时寄存器。
R13(栈指针,SP)
▪用途:指向当前栈顶的地址。
▪特点:在函数调用和中断处理中,栈指针用于管理栈的入栈(PUSH)和出栈(POP)操作。
R14(链接寄存器,LR)
▪用途:存储函数调用返回地址。
▪特点:在函数调用时,ARM处理器会将返回地址存储到LR寄存器中。在函数返回时,从LR寄存器中获取返回地址。
R15(程序计数器,PC)
▪用途:存储当前指令的地址。
▪特点:PC寄存器始终指向当前正在执行的指令的地址。每次指令执行后,PC会自动更新到下一条指令的地址。CPU一条一条不停地取指令,程序也就源源不断地一直运行下去。
b.状态寄存器(了解)
CPSR(当前程序状态寄存器)
•用途:存储当前处理器的状态信息,包括条件标志、中断屏蔽位等。
•主要字段:
◦N(负标志)表示结果为负。
◦Z(零标志):表示结果为零。
◦C(进位标志):表示有进位或借位。
◦V(溢出标志):表示有溢出。
◦I(IRQ中断屏蔽位):1表示禁止IRQ中断,0表示允许IRQ中断。
◦F(FIQ中断屏位):1表示禁止FIQ中断,0表示允许FIQ中断。
◦M(模式位):表示当前处理器的工作模式(如用户模式、中断模式等)。
SPSR(保存的程序状态寄存器)
•用途:在中断或异常处理时,保存当前的CPSR值。
•特点:当处理器进入中断或异常处理模式时,CPSR的值会被保存到SPSR中。在中断或异常处理结束时,从SPSR中恢复CPSR的值。
3.3 中断嵌套
中断嵌套的核心在于中断优先级(Interrupt Priority)。当一个中断发生时,处理器会根据中断的优先级来决定是否中断当前正在执行的任务(包括普通任务或中断服务例程)。
•高优先级中断可以打断低优先级中断:如果一个高优先级的中断发生,而此时处理器正在执行一个低优先级的中断服务例程,处理器会暂停当前的中断服务例程,转而执行高优先级的中断服务例程。
•低优先级中断不能打断高优先级中断:如果一个低优先级的中断发生,而处理器正在执行一个高优先级的中断服务例程,低优先级的中断会被挂起,直到高优先级的中断服务例程执行完毕。
3.3.1 中断嵌套的实现
中断嵌套的实现依赖于处理器的中断控制器(Interrupt Controller)。中断控制器会根据中断的优先级来调度中断服务例程的执行。以下是中断嵌套的典型实现步骤:
中断请求(Interrupt Request):当一个中断源(如外部信号、定时器溢出等)触发中断时,中断控制器会记录该中断请求。
中断优先级判断:中断控制器会比较当前正在执行的任务(或中断服务例程)的优先级与新中断的优先级。
3.3.2 中断嵌套处理:
◦如果新中断的优先级高于当前正在执行的任务或中断服务例程的优先级,处理器会保存当前上下文(如寄存器状态),然后跳转到新中断的中断服务例程。
◦如果新中断的优先级低于或等于当前正在执行的任务或中断服务例程的优先级,新中断会被挂起,直到当前任务或中断服务例程执行完毕。
3.3.3 中断嵌套的注意事项
•中断服务例程的编写:中断服务例程应尽可能简短,避免长时间占用处理器。如果需要执行复杂任务,建议将任务放入队列,由主程序处理。
•优先级配置:合理配置中断优先级非常重要。高优先级的中断应处理紧急任务,低优先级的中断可以处理相对不紧急的任务。
•避免死锁:如果多个中断服务例程之间存在共享资源(如全局变量、硬件资源等),需要使用互斥机制(如禁用中断、使用信号量等)来避免死锁。
•中断嵌套深度:虽然中断嵌套可以提高系统的实时性,但过多的嵌套层次可能导致系统复杂度增加。建议限制中断嵌套的深度,一般不超过2 - 3层。
3.4 DMA(了解原理即可)
STM32微控制器的DMA(Direct Memory Access,直接存储器访问)是一种非常强大的功能,可以实现数据在存储器之间或存储器与外设之间的高效传输,而无需CPU干预。这不仅可以减轻CPU的负担,还可以提高系统的整体性能。
3.4.1 DMA的基本概念
DMA是一种硬件机制,允许数据在存储器和外设之间直接传输,而无需CPU的直接干预。在STM32中,DMA控制器可以管理多个数据传输请求,并根据配置的优先级和传输方向自动完成数据传输。
•传输方向:DMA支持多种传输方向,包括从外设到存储器、从存储器到外设、存储器到存储器等。
•数据源和目标:数据源和目标可以是外设的寄存器(如ADC、SPI、USART等)或存储器地址。
•传输大小:可以配置传输的数据量(如传输的字节数或字数)。
3.4.2 STM32 DMA的主要特性
•多通道支持:STM32的DMA控制器通常支持多个通道(如DMA1和DMA2),每个通道可以独立配置和使用。
•多种传输方向:支持从外设到存储器、从存储器到外设、存储器到存储器等多种传输方向。
•数据宽度:支持字节(8位)、半字(16位)、字(32位)等多种数据宽度。
•优先级配置:支持通道优先级配置,可以动态调整DMA通道的优先级。
•中断支持:支持多种中断类型,包括传输完成、传输错误、半传输完成等。
•循环传输:支持循环传输模式,可以在传输完成后自动重新开始传输。
3.5 Hardfault
这个最好自己根据自己的项目找一个问题,可以一直说的!这个问的几率超级无敌大!!!
“HardFault” 是 ARM Cortex-M 系列处理器(例如 STM32)中的一种异常类型,通常表示系统发生了严重错误。
3.5.1 定义:
HardFault 是一种不可屏蔽的异常,当系统发生某些严重错误时,处理器会触发 HardFault 异常。它通常表示系统已经无法正常运行,需要进行错误处理。
3.5.2 触发原因:HardFault 的触发原因有很多,常见的包括:
◦非法指令:执行了一条无效的指令(例如,指令地址错误或指令本身无效)。
◦堆栈溢出:堆栈空间不足,导致堆栈指针(SP)超出堆栈范围。
◦内存访问错误:访问了非法的内存地址(例如,未初始化的指针或越界的数组访问)。
◦特权级别错误:执行了特权指令,但当前运行在非特权模式下(例如,普通任务执行了只有操作系统才能执行的指令)。
◦中断优先级错误:中断优先级配置错误,导致高优先级中断被低优先级中断打断。
◦其他硬件错误:如总线错误、数据错误等。
3.5.3 调试方法:
1.通过查看这些寄存器的值,可以获取到触发异常时的指令地址(pc)、返回地址(lr)等信息,从而定位问题。前提是得了解各个寄存的作用!!!(ARM架构里面)
void HardFault_Handler(void)
{
// 获取异常上下文
uint32_t stacked_r0;
uint32_t stacked_r1;
uint32_t stacked_r2;
uint32_t stacked_r3;
uint32_t stacked_r12;
uint32_t stacked_lr;
uint32_t stacked_pc;
uint32_t stacked_psr;
asm volatile(
"MOV %0, r0\n"
"MOV %1, r1\n"
"MOV %2, r2\n"
"MOV %3, r3\n"
"MOV %4, r12\n"
"MOV %5, lr\n"
"MOV %6, pc\n"
"MOV %7, psr\n"
: "=r"(stacked_r0), "=r"(stacked_r1), "=r"(stacked_r2), "=r"(stacked_r3),
"=r"(stacked_r12), "=r"(stacked_lr), "=r"(stacked_pc), "=r"(stacked_psr)
:
: "memory"
);
// 打印上下文信息(可以通过串口或其他调试工具输出)
printf("HardFault: r0 = 0x%X\n", stacked_r0);
printf("HardFault: r1 = 0x%X\n", stacked_r1);
printf("HardFault: r2 = 0x%X\n", stacked_r2);
printf("HardFault: r3 = 0x%X\n", stacked_r3);
printf("HardFault: r12 = 0x%X\n", stacked_r12);
printf("HardFault: lr = 0x%X\n", stacked_lr);
printf("HardFault: pc = 0x%X\n", stacked_pc);
printf("HardFault: psr = 0x%X\n", stacked_psr);
// 进入死循环,等待调试
while (1);
}
(2)检查堆栈空间
堆栈溢出是常见的 HardFault 原因之一。可以通过以下方法检查堆栈空间是否足够:
•检查堆栈配置:确保堆栈大小配置合理。例如,在 FreeRTOS 中,可以通过 configTOTAL_HEAP_SIZE 配置堆栈大小。
•检查堆栈使用情况:使用调试工具(如 Keil、IAR 或 STM32CubeIDE)查看堆栈的使用情况。如果堆栈指针(SP)接近堆栈的起始地址或结束地址,说明堆栈可能不足。
(3)检查内存访问
内存访问错误也是 HardFault 的常见原因。可以通过以下方法检查内存访问是否正确:
•检查指针初始化:确保所有指针在使用前都已正确初始化。例如,避免使用未初始化的指针访问内存。
•检查数组访问:确保数组访问没有越界。例如,避免访问数组的负索引或超出数组范围的索引。
•使用内存保护单元(MPU):如果硬件支持,可以启用内存保护单元(MPU)来限制内存访问。MPU 可以检测非法的内存访问并触发异常。
(4)检查中断优先级
中断优先级配置错误可能导致 HardFault。可以通过以下方法检查中断优先级是否正确:
•检查中断优先级配置:确保中断优先级配置合理。例如,避免高优先级中断被低优先级中断打断。
•使用中断优先级分组:如果硬件支持,可以使用中断优先级分组来管理中断优先级。例如,在 STM32 中,可以通过 NVIC 的优先级分组寄存器(PRIGROUP)配置中断优先级分组。
(5)使用调试工具(这个调试一定要会!!!)
调试工具(如 Keil、IAR 或 STM32CubeIDE)提供了丰富的调试功能,可以帮助定位 HardFault 的原因。例如:
•设置断点:在 HardFault 异常处理函数中设置断点,当 HardFault 发生时,调试器会停在断点处,可以查看寄存器的值和调用栈信息。
•查看调用栈:通过调试器的调用栈窗口,可以查看触发 HardFault 时的调用栈信息,从而定位问题。
•单步调试:使用单步调试功能,逐步执行代码,观察变量的值和寄存器的变化,从而找到问题所在。
#八股##满分简历要如何准备?#从入门到上岸,一站式搞定求职! 本硕纯机械,无竞赛无论文,后转行嵌入式软件开发(因为课题组师哥转嵌入式拿到30Woffer之后狠狠心动),秋招最终收获35W+offer可以为27届或者28届的的UU们提供参考,可以关注一下!!!
