HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 技术面试完全指南

    • 技术面试完全指南
    • 8年面试官告诉你:90%的简历在第一轮就被刷掉了
    • 刷了500道LeetCode,终于明白大厂算法面试到底考什么
    • 高频算法题精讲-双指针与滑动窗口
    • 03-高频算法题精讲-二分查找与排序
    • 04-高频算法题精讲-树与递归
    • 05-高频算法题精讲-图与拓扑排序
    • 06-高频算法题精讲-动态规划
    • Go面试必问:一道GMP问题,干掉90%的候选人
    • 08-数据库面试高频题
    • 09-分布式系统面试题
    • 10-Kubernetes与云原生面试题
    • 11-系统设计面试方法论
    • 前端面试高频题
    • AI 与机器学习面试题
    • 行为面试与软技能

MCU架构

ARM Cortex-M系列

各型号对比

特性Cortex-M0Cortex-M3Cortex-M4Cortex-M7
架构ARMv6-MARMv7-MARMv7E-MARMv7E-M
主频50MHz120MHz180MHz400MHz+
流水线3级3级3级6级双发射
中断32240240240
FPU无无可选可选(DP)
DSP无无有增强
Cache无无无I/D Cache
MPU可选可选可选标配
性能0.9 DMIPS/MHz1.251.252.14
应用简单控制通用音频/电机高性能

指令集差异

// Cortex-M0:仅支持Thumb子集(56条指令)
uint32_t divide(uint32_t a, uint32_t b) {
    // 无硬件除法,编译器生成库调用
    return a / b;  // 调用__aeabi_uidiv
}

// Cortex-M3+:支持完整Thumb-2(含除法指令)
uint32_t divide_m3(uint32_t a, uint32_t b) {
    // UDIV指令,1个周期
    return a / b;
}

// Cortex-M4:支持DSP指令
int16_t saturate_add(int16_t a, int16_t b) {
    // QADD指令,饱和加法
    int32_t result = a + b;
    if (result > 32767) return 32767;
    if (result < -32768) return -32768;
    return result;
}

FPU浮点运算单元

// 使能FPU(Cortex-M4/M7)
void FPU_Enable(void) {
    // CPACR: Coprocessor Access Control Register
    SCB->CPACR |= (3UL << 20) | (3UL << 22);  // CP10, CP11 full access
}

// 浮点运算性能对比
void benchmark(void) {
    float a = 1.5f, b = 2.5f, c;

    // 无FPU:软件模拟,约100-200个周期
    // 有FPU:硬件执行,约3-10个周期
    c = a * b + sqrtf(a);
}

// FPU寄存器
// S0-S31: 单精度浮点寄存器(32bit)
// D0-D15: 双精度浮点寄存器(64bit, M7支持)
// FPSCR: 浮点状态和控制寄存器

寄存器架构

通用寄存器

R0-R12: 通用寄存器
R13(SP): 堆栈指针
  - MSP: 主堆栈指针(中断/特权模式)
  - PSP: 进程堆栈指针(任务模式)
R14(LR): 链接寄存器(保存返回地址)
R15(PC): 程序计数器
// 读取特殊寄存器
uint32_t get_MSP(void) {
    uint32_t result;
    __asm volatile ("MRS %0, MSP" : "=r" (result));
    return result;
}

uint32_t get_PSP(void) {
    uint32_t result;
    __asm volatile ("MRS %0, PSP" : "=r" (result));
    return result;
}

// 获取PC值
uint32_t get_PC(void) {
    uint32_t result;
    __asm volatile ("MOV %0, PC" : "=r" (result));
    return result;
}

程序状态寄存器(PSR)

typedef struct {
    uint32_t N : 1;  // Negative flag
    uint32_t Z : 1;  // Zero flag
    uint32_t C : 1;  // Carry flag
    uint32_t V : 1;  // Overflow flag
    uint32_t Q : 1;  // Saturation flag
    // ...
} PSR_Flags;

// 读取APSR(应用程序状态寄存器)
uint32_t get_APSR(void) {
    uint32_t result;
    __asm volatile ("MRS %0, APSR" : "=r" (result));
    return result;
}

外设寄存器映射

// STM32外设基地址
#define PERIPH_BASE      0x40000000UL
#define APB1PERIPH_BASE  PERIPH_BASE
#define APB2PERIPH_BASE  (PERIPH_BASE + 0x00010000UL)
#define AHB1PERIPH_BASE  (PERIPH_BASE + 0x00020000UL)

// GPIO寄存器结构
typedef struct {
    volatile uint32_t MODER;
    volatile uint32_t OTYPER;
    volatile uint32_t OSPEEDR;
    volatile uint32_t PUPDR;
    volatile uint32_t IDR;
    volatile uint32_t ODR;
    volatile uint32_t BSRR;
    volatile uint32_t LCKR;
    volatile uint32_t AFR[2];
} GPIO_TypeDef;

#define GPIOA_BASE  (AHB1PERIPH_BASE + 0x0000UL)
#define GPIOA       ((GPIO_TypeDef *) GPIOA_BASE)

// 使用示例
void set_PA5_high(void) {
    GPIOA->BSRR = (1 << 5);  // 原子操作
}

时钟树

STM32时钟系统

                       [PLL] ×9
HSE(8MHz) ─┬─────────────┴──────→ SYSCLK(72MHz)
           │                         │
HSI(8MHz) ─┘                         ├→ AHB Prescaler(/1) → HCLK(72MHz)
                                     │    ├→ Cortex系统定时器
                                     │    ├→ DMA
                                     │    └→ GPIO
                                     │
                                     ├→ APB1 Prescaler(/2) → PCLK1(36MHz)
                                     │    ├→ TIM2-7
                                     │    ├→ USART2-3
                                     │    └→ I2C1-2
                                     │
                                     └→ APB2 Prescaler(/1) → PCLK2(72MHz)
                                          ├→ TIM1,8
                                          ├→ USART1
                                          ├→ ADC1-2
                                          └→ SPI1

时钟配置代码

void SystemClock_Config(void) {
    // 1. 使能HSE
    RCC->CR |= RCC_CR_HSEON;
    while (!(RCC->CR & RCC_CR_HSERDY));  // 等待HSE就绪

    // 2. 配置Flash延迟(72MHz需要2等待周期)
    FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2;

    // 3. 配置PLL:HSE×9 = 72MHz
    RCC->CFGR = RCC_CFGR_PLLSRC |        // PLL源=HSE
                RCC_CFGR_PLLMULL9 |      // PLL倍频×9
                RCC_CFGR_HPRE_DIV1 |     // AHB不分频
                RCC_CFGR_PPRE1_DIV2 |    // APB1÷2
                RCC_CFGR_PPRE2_DIV1;     // APB2不分频

    // 4. 使能PLL
    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY));

    // 5. 切换系统时钟到PLL
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
}

// 获取系统时钟频率
uint32_t SystemCoreClock = 72000000;

void SystemCoreClockUpdate(void) {
    uint32_t pll_mul, pll_src;

    switch (RCC->CFGR & RCC_CFGR_SWS) {
        case RCC_CFGR_SWS_HSI:  // HSI
            SystemCoreClock = HSI_VALUE;
            break;

        case RCC_CFGR_SWS_HSE:  // HSE
            SystemCoreClock = HSE_VALUE;
            break;

        case RCC_CFGR_SWS_PLL:  // PLL
            pll_mul = ((RCC->CFGR & RCC_CFGR_PLLMULL) >> 18) + 2;
            pll_src = (RCC->CFGR & RCC_CFGR_PLLSRC) ? HSE_VALUE : HSI_VALUE/2;
            SystemCoreClock = pll_src * pll_mul;
            break;
    }
}

电源管理

低功耗模式

模式功耗唤醒时间保留内容唤醒源
Run正常-全部-
Sleep低快(us)全部任何中断
Stop很低中(us)SRAM/寄存器EXTI/RTC/IWDG
Standby极低慢(ms)仅备份寄存器WKUP/RTC/IWDG
// Sleep模式
void enter_sleep_mode(void) {
    // WFI: Wait For Interrupt
    __WFI();
    // 任何中断唤醒,继续执行
}

// Stop模式
void enter_stop_mode(void) {
    // 清除唤醒标志
    PWR->CR |= PWR_CR_CWUF;

    // 进入Stop模式
    PWR->CR |= PWR_CR_LPDS;  // 低功耗深睡眠
    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
    __WFI();

    // 唤醒后需重新配置时钟
    SystemClock_Config();
}

// Standby模式
void enter_standby_mode(void) {
    // 使能电源时钟
    RCC->APB1ENR |= RCC_APB1ENR_PWREN;

    // 清除唤醒标志
    PWR->CR |= PWR_CR_CWUF;

    // 使能WKUP引脚唤醒
    PWR->CSR |= PWR_CSR_EWUP;

    // 进入Standby模式
    PWR->CR |= PWR_CR_PDDS;  // Power Down Deep Sleep
    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
    __WFI();

    // 唤醒后从Reset Handler重新启动
}

// RTC唤醒
void RTC_WakeUp_Config(uint16_t seconds) {
    // 配置RTC闹钟
    // ...
    // 进入低功耗模式,RTC闹钟唤醒
}

低功耗设计技巧

// 1. 关闭未使用的外设时钟
void disable_unused_peripherals(void) {
    RCC->APB1ENR &= ~(RCC_APB1ENR_TIM2EN |
                      RCC_APB1ENR_TIM3EN |
                      RCC_APB1ENR_TIM4EN);
}

// 2. 降低系统时钟频率
void reduce_clock_speed(void) {
    // 切换到HSI 8MHz
    RCC->CFGR &= ~RCC_CFGR_SW;
    RCC->CFGR |= RCC_CFGR_SW_HSI;
}

// 3. 使用DMA减少CPU唤醒次数
void setup_dma_uart(void) {
    // DMA自动传输,CPU可保持睡眠
}

// 4. 配置GPIO为模拟输入减少漏电流
void configure_unused_gpio(void) {
    GPIOC->MODER = 0xFFFFFFFF;  // 所有引脚为模拟输入
}

DMA直接内存访问

DMA传输模式

// DMA传输方向
typedef enum {
    DMA_PERIPH_TO_MEMORY = 0,  // 外设到内存
    DMA_MEMORY_TO_PERIPH = 1,  // 内存到外设
    DMA_MEMORY_TO_MEMORY = 2   // 内存到内存
} DMA_Direction;

// DMA配置结构
typedef struct {
    uint32_t PeriphAddr;      // 外设地址
    uint32_t MemoryAddr;      // 内存地址
    uint32_t Direction;       // 传输方向
    uint32_t BufferSize;      // 缓冲区大小
    uint32_t PeriphInc;       // 外设地址递增
    uint32_t MemoryInc;       // 内存地址递增
    uint32_t PeriphDataSize;  // 外设数据宽度
    uint32_t MemoryDataSize;  // 内存数据宽度
    uint32_t Mode;            // 正常/循环模式
    uint32_t Priority;        // 优先级
} DMA_InitTypeDef;

// ADC+DMA连续采集
#define ADC_BUFFER_SIZE 100
uint16_t adc_buffer[ADC_BUFFER_SIZE];

void ADC_DMA_Init(void) {
    // 配置DMA通道1
    DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR;  // 外设地址
    DMA1_Channel1->CMAR = (uint32_t)adc_buffer; // 内存地址
    DMA1_Channel1->CNDTR = ADC_BUFFER_SIZE;     // 传输数量

    DMA1_Channel1->CCR = DMA_CCR_MINC |    // 内存地址递增
                         DMA_CCR_CIRC |    // 循环模式
                         DMA_CCR_TCIE |    // 传输完成中断
                         DMA_CCR_EN;       // 使能DMA

    // 使能ADC DMA请求
    ADC1->CR2 |= ADC_CR2_DMA;
}

// DMA传输完成中断
void DMA1_Channel1_IRQHandler(void) {
    if (DMA1->ISR & DMA_ISR_TCIF1) {
        DMA1->IFCR = DMA_IFCR_CTCIF1;  // 清除标志

        // 处理采集到的数据
        process_adc_data(adc_buffer, ADC_BUFFER_SIZE);
    }
}

内存到内存传输

void DMA_MemCopy(uint32_t *src, uint32_t *dst, uint32_t len) {
    // 禁用DMA通道
    DMA2_Channel1->CCR &= ~DMA_CCR_EN;

    // 配置源和目标
    DMA2_Channel1->CPAR = (uint32_t)src;
    DMA2_Channel1->CMAR = (uint32_t)dst;
    DMA2_Channel1->CNDTR = len;

    // 配置传输参数
    DMA2_Channel1->CCR = DMA_CCR_MEM2MEM |  // 内存到内存
                         DMA_CCR_MINC |     // 内存地址递增
                         DMA_CCR_PINC |     // 外设地址递增
                         DMA_CCR_PSIZE_1 |  // 32位
                         DMA_CCR_MSIZE_1 |  // 32位
                         DMA_CCR_EN;

    // 等待传输完成
    while (!(DMA2->ISR & DMA_ISR_TCIF1));
    DMA2->IFCR = DMA_IFCR_CTCIF1;
}

Flash与EEPROM

Flash操作

// Flash编程步骤
void Flash_Write(uint32_t addr, uint16_t data) {
    // 1. 解锁Flash
    if (FLASH->CR & FLASH_CR_LOCK) {
        FLASH->KEYR = 0x45670123;
        FLASH->KEYR = 0xCDEF89AB;
    }

    // 2. 等待上一次操作完成
    while (FLASH->SR & FLASH_SR_BSY);

    // 3. 使能编程
    FLASH->CR |= FLASH_CR_PG;

    // 4. 写入半字(16bit)
    *((volatile uint16_t*)addr) = data;

    // 5. 等待完成
    while (FLASH->SR & FLASH_SR_BSY);

    // 6. 检查错误
    if (FLASH->SR & FLASH_SR_EOP) {
        FLASH->SR = FLASH_SR_EOP;  // 清除标志
    }

    // 7. 禁用编程
    FLASH->CR &= ~FLASH_CR_PG;
}

// Flash擦除(页擦除)
void Flash_ErasePage(uint32_t page_addr) {
    FLASH->CR |= FLASH_CR_PER;       // 页擦除模式
    FLASH->AR = page_addr;           // 页地址
    FLASH->CR |= FLASH_CR_STRT;      // 开始擦除
    while (FLASH->SR & FLASH_SR_BSY);
    FLASH->CR &= ~FLASH_CR_PER;
}

// 保存配置参数
#define CONFIG_ADDR  0x0801F000  // 最后一页
typedef struct {
    uint16_t magic;         // 0xA55A
    uint16_t brightness;
    uint16_t volume;
    uint16_t checksum;
} Config_t;

void save_config(Config_t *config) {
    config->magic = 0xA55A;
    config->checksum = calculate_checksum(config);

    Flash_ErasePage(CONFIG_ADDR);
    uint16_t *p = (uint16_t*)config;
    for (int i = 0; i < sizeof(Config_t)/2; i++) {
        Flash_Write(CONFIG_ADDR + i*2, p[i]);
    }
}

void load_config(Config_t *config) {
    memcpy(config, (void*)CONFIG_ADDR, sizeof(Config_t));
    if (config->magic != 0xA55A) {
        // 使用默认配置
        config->brightness = 50;
        config->volume = 80;
    }
}

IAP在线升级

// 内存分区
// 0x08000000 - 0x08003FFF: Bootloader (16KB)
// 0x08004000 - 0x0801FFFF: Application (112KB)

#define APP_START_ADDR  0x08004000

// Bootloader跳转到应用程序
void jump_to_app(void) {
    uint32_t app_stack = *((uint32_t*)APP_START_ADDR);
    uint32_t app_entry = *((uint32_t*)(APP_START_ADDR + 4));

    // 检查栈指针有效性
    if ((app_stack & 0x2FFE0000) != 0x20000000) {
        return;  // 无效的应用程序
    }

    // 关闭全局中断
    __disable_irq();

    // 关闭SysTick
    SysTick->CTRL = 0;

    // 设置向量表偏移
    SCB->VTOR = APP_START_ADDR;

    // 设置栈指针
    __set_MSP(app_stack);

    // 跳转到应用程序
    void (*app_reset_handler)(void) = (void*)app_entry;
    app_reset_handler();
}

启动流程

// 1. 复位向量表(startup_stm32.s)
__Vectors:
    DCD     __initial_sp               // 栈顶地址
    DCD     Reset_Handler              // 复位处理函数
    DCD     NMI_Handler
    DCD     HardFault_Handler
    // ...

// 2. Reset Handler
void Reset_Handler(void) {
    // 复制.data段从Flash到RAM
    extern uint32_t _sdata, _edata, _sidata;
    uint32_t *src = &_sidata;
    uint32_t *dst = &_sdata;
    while (dst < &_edata) {
        *dst++ = *src++;
    }

    // 清零.bss段
    extern uint32_t _sbss, _ebss;
    dst = &_sbss;
    while (dst < &_ebss) {
        *dst++ = 0;
    }

    // 调用SystemInit
    SystemInit();

    // 调用C++全局对象构造函数
    __libc_init_array();

    // 调用main函数
    main();

    // main不应返回
    while (1);
}

// 3. SystemInit(初始化时钟等)
void SystemInit(void) {
    // 配置FPU
    #if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
        SCB->CPACR |= (3UL << 20) | (3UL << 22);
    #endif

    // 设置向量表位置
    SCB->VTOR = FLASH_BASE;

    // 配置系统时钟
    SystemClock_Config();
}

高频面试题

1. Cortex-M3和M4的主要区别是什么?

答案:

核心区别在于DSP和FPU:

  • M3: 无DSP指令,无FPU
  • M4: 有DSP指令集(饱和运算、SIMD、MAC),可选FPU

应用场景:

  • M3: 通用控制(工控、智能家居)
  • M4: 音频处理、电机控制、数字滤波

性能对比:

// FFT运算:M4(带FPU)比M3快10倍以上
// 浮点乘加:M4只需3周期,M3需要软件模拟(约100周期)

2. 如何降低MCU的功耗?

答案:

综合策略:

  1. 时钟优化: 降低主频,关闭未用外设时钟
  2. 睡眠模式: 空闲时进入Sleep/Stop模式
  3. GPIO配置: 未使用引脚配置为模拟输入
  4. 外设优化: 使用DMA减少CPU唤醒,使用RTC定时唤醒
  5. 电源域: 关闭未用电源域
  6. 硬件设计: 外部上拉/下拉改用内部,选择低功耗器件

实测数据(STM32L系列):

  • Run(32MHz): 10mA
  • Sleep: 3mA
  • Stop: 10uA
  • Standby: 1uA

3. DMA和CPU搬运数据的优缺点?

答案:

特性CPUDMA
CPU占用100%0%
速度受限于指令执行直接访问总线
灵活性高(可处理数据)低(仅搬运)
功耗高低
适用场景需要处理的数据大块数据传输

使用原则:

  • 大量数据(>64字节)使用DMA
  • 需要处理/转换数据使用CPU
  • 低功耗场景优先DMA

4. Flash为什么要先擦除后编程?

答案:

Flash存储原理:

  • 编程: 只能将1变为0(电子注入浮栅)
  • 擦除: 将0变为1(电子移出浮栅)
  • 无法直接将0变1,必须整块擦除

擦除单位:

  • NOR Flash: 扇区擦除(4KB-64KB)
  • NAND Flash: 块擦除(128KB-256KB)

编程注意事项:

  1. 擦除次数有限(1万-10万次)
  2. 擦除耗时长(毫秒级)
  3. 编程时需禁用中断
  4. 掉电保护(使用双备份+校验)

5. 如何实现Bootloader跳转?

答案:

关键步骤:

void jump_to_app(uint32_t app_addr) {
    // 1. 检查栈顶地址有效性
    uint32_t sp = *(uint32_t*)app_addr;
    if ((sp & 0x2FFE0000) != 0x20000000) {
        return;  // 无效
    }

    // 2. 关闭所有外设和中断
    HAL_DeInit();
    __disable_irq();
    SysTick->CTRL = 0;

    // 3. 重置向量表
    SCB->VTOR = app_addr;

    // 4. 设置栈指针
    __set_MSP(sp);

    // 5. 跳转
    uint32_t pc = *(uint32_t*)(app_addr + 4);
    void (*app_entry)(void) = (void*)pc;
    app_entry();
}

注意事项:

  • App链接脚本需设置正确的起始地址
  • App启动代码需设置SCB->VTOR
  • 可通过按键/超时/固件标志触发跳转