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

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

硬件接口

GPIO通用输入输出

GPIO工作模式

GPIO是单片机最基础的外设,支持多种工作模式:

模式描述应用场景
输入浮空高阻态,易受干扰外部有上下拉电阻
输入上拉内部上拉到VDD按键检测(按下接地)
输入下拉内部下拉到GND检测高电平信号
推挽输出可输出强高/低电平驱动LED、小负载
开漏输出只能输出低电平I2C总线、电平转换
复用推挽外设控制引脚UART TX、SPI MOSI
复用开漏外设控制引脚I2C SDA/SCL
模拟模式禁用数字功能ADC输入、DAC输出

GPIO寄存器配置(STM32)

// 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;     // 位设置/复位寄存器
} GPIO_TypeDef;

// 配置PA5为推挽输出
void GPIO_Init_PA5_Output(void) {
    // 1. 使能GPIOA时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

    // 2. 配置模式为输出(01)
    GPIOA->MODER &= ~(3U << (5 * 2));   // 清零
    GPIOA->MODER |= (1U << (5 * 2));    // 设置为01

    // 3. 配置为推挽输出
    GPIOA->OTYPER &= ~(1U << 5);

    // 4. 配置输出速度为高速
    GPIOA->OSPEEDR |= (3U << (5 * 2));

    // 5. 不使用上拉下拉
    GPIOA->PUPDR &= ~(3U << (5 * 2));
}

// 高效的IO操作
#define LED_ON()   (GPIOA->BSRR = (1U << 5))
#define LED_OFF()  (GPIOA->BSRR = (1U << (5 + 16)))
#define LED_TOGGLE() (GPIOA->ODR ^= (1U << 5))

按键检测与消抖

#define KEY_PIN  0
#define KEY_PRESSED  0  // 按键按下为低电平

typedef enum {
    KEY_IDLE,
    KEY_DEBOUNCE,
    KEY_PRESSED_STATE
} KeyState;

uint8_t key_scan(void) {
    static KeyState state = KEY_IDLE;
    static uint32_t debounce_timer = 0;

    uint8_t key_val = (GPIOA->IDR >> KEY_PIN) & 1;

    switch (state) {
        case KEY_IDLE:
            if (key_val == KEY_PRESSED) {
                debounce_timer = HAL_GetTick();
                state = KEY_DEBOUNCE;
            }
            break;

        case KEY_DEBOUNCE:
            if (HAL_GetTick() - debounce_timer > 20) {  // 20ms消抖
                if (key_val == KEY_PRESSED) {
                    state = KEY_PRESSED_STATE;
                    return 1;  // 按键有效
                } else {
                    state = KEY_IDLE;
                }
            }
            break;

        case KEY_PRESSED_STATE:
            if (key_val != KEY_PRESSED) {
                state = KEY_IDLE;
            }
            break;
    }
    return 0;
}

UART串口通信

UART协议格式

起始位(1bit) + 数据位(5-9bit) + 校验位(0-1bit) + 停止位(1-2bit)

典型配置:8N1 (8数据位,无校验,1停止位)

时序图:
   空闲(高)  起始  D0  D1  D2  D3  D4  D5  D6  D7  停止  空闲
      ___    __    _____    _____       _____    ___    ____
    _|   |__|  |__|     |__|     |_____|     |__|   |__|

波特率计算

// 波特率 = fCK / (16 * USARTDIV)
// 例: 72MHz时钟, 115200波特率
// USARTDIV = 72000000 / (16 * 115200) = 39.0625
// BRR = 39 + 0.0625*16 = 39 + 1 = 40(十进制) = 0x27

void UART1_Init(uint32_t baudrate) {
    // 1. 使能时钟
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

    // 2. 配置GPIO(PA9-TX, PA10-RX)
    GPIOA->MODER |= (2U << (9*2)) | (2U << (10*2));  // 复用功能
    GPIOA->AFR[1] |= (7U << ((9-8)*4)) | (7U << ((10-8)*4));  // AF7

    // 3. 配置波特率
    uint32_t apb2_clk = 72000000;
    USART1->BRR = apb2_clk / baudrate;

    // 4. 使能发送和接收
    USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
}

// 发送单字节
void UART1_SendByte(uint8_t data) {
    while (!(USART1->SR & USART_SR_TXE));  // 等待发送寄存器空
    USART1->DR = data;
}

// 接收单字节
uint8_t UART1_RecvByte(void) {
    while (!(USART1->SR & USART_SR_RXNE));  // 等待接收完成
    return USART1->DR;
}

DMA收发(高效方式)

void UART_DMA_Send(const uint8_t *data, uint16_t len) {
    // 配置DMA通道
    DMA2_Stream7->CR &= ~DMA_SxCR_EN;  // 禁用DMA
    while (DMA2_Stream7->CR & DMA_SxCR_EN);  // 等待禁用完成

    DMA2_Stream7->PAR = (uint32_t)&USART1->DR;  // 外设地址
    DMA2_Stream7->M0AR = (uint32_t)data;         // 内存地址
    DMA2_Stream7->NDTR = len;                    // 传输长度

    // 配置:内存递增,外设到内存,8位数据
    DMA2_Stream7->CR = DMA_SxCR_CHSEL_2 |  // 通道4
                       DMA_SxCR_MINC |      // 内存递增
                       DMA_SxCR_DIR_0 |     // 内存到外设
                       DMA_SxCR_TCIE;       // 传输完成中断

    DMA2_Stream7->CR |= DMA_SxCR_EN;  // 使能DMA
    USART1->CR3 |= USART_CR3_DMAT;    // 使能UART DMA发送
}

void DMA2_Stream7_IRQHandler(void) {
    if (DMA2->HISR & DMA_HISR_TCIF7) {
        DMA2->HIFCR = DMA_HIFCR_CTCIF7;  // 清除中断标志
        // 传输完成处理
    }
}

SPI总线

SPI特性

SPI是全双工、主从式同步串行通信协议:

  • MOSI: Master Out Slave In(主机输出)
  • MISO: Master In Slave Out(主机输入)
  • SCK: Serial Clock(时钟,主机产生)
  • CS/SS: Chip Select(片选,低电平有效)

SPI四种模式

模式CPOLCPHA时钟空闲采样边沿
000低电平第一个边沿
101低电平第二个边沿
210高电平第一个边沿
311高电平第二个边沿
void SPI1_Init(void) {
    // 使能时钟
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

    // 配置GPIO: PA5-SCK, PA6-MISO, PA7-MOSI
    GPIOA->MODER |= (2U << 10) | (2U << 12) | (2U << 14);
    GPIOA->AFR[0] |= (5U << 20) | (5U << 24) | (5U << 28);  // AF5

    // 配置SPI
    SPI1->CR1 = SPI_CR1_MSTR |      // 主模式
                SPI_CR1_BR_2 |      // 波特率分频256
                SPI_CR1_SSM |       // 软件NSS管理
                SPI_CR1_SSI |       // 内部NSS
                SPI_CR1_SPE;        // 使能SPI
}

uint8_t SPI1_TransferByte(uint8_t data) {
    while (!(SPI1->SR & SPI_SR_TXE));  // 等待发送缓冲区空
    SPI1->DR = data;
    while (!(SPI1->SR & SPI_SR_RXNE)); // 等待接收完成
    return SPI1->DR;
}

// 读取W25Q128 Flash ID
void W25Q128_ReadID(uint8_t *id) {
    CS_LOW();  // 片选拉低
    SPI1_TransferByte(0x9F);  // JEDEC ID命令
    id[0] = SPI1_TransferByte(0xFF);  // Manufacturer ID
    id[1] = SPI1_TransferByte(0xFF);  // Device ID 1
    id[2] = SPI1_TransferByte(0xFF);  // Device ID 2
    CS_HIGH();  // 片选拉高
}

I2C总线

I2C协议基础

I2C是半双工、多主从式同步串行通信协议:

  • SDA: Serial Data(数据线,双向)
  • SCL: Serial Clock(时钟线)
  • 需要上拉电阻(典型4.7kΩ)
  • 支持多主机、多从机

I2C时序

起始条件: SCL高电平时,SDA下降沿
停止条件: SCL高电平时,SDA上升沿

数据传输: SCL低电平时改变SDA,SCL高电平时采样

标准模式:100kHz  快速模式:400kHz  高速模式:3.4MHz

时序图:
      起始    地址+W    ACK   数据    ACK   停止
SDA:  ___X__X_X_X_X_X_X_X__X__X_X_X_X_X_X_X__X___
       \___/ 7位地址+R/W \___/ 8位数据  \___/
SCL:  _____|‾|_|‾|_|‾|_|‾|____|‾|_|‾|_|‾|_|‾|_____

I2C驱动实现

void I2C1_Init(void) {
    // 使能时钟
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;

    // 配置GPIO: PB6-SCL, PB7-SDA
    GPIOB->MODER |= (2U << 12) | (2U << 14);  // 复用功能
    GPIOB->OTYPER |= (1U << 6) | (1U << 7);   // 开漏输出
    GPIOB->PUPDR |= (1U << 12) | (1U << 14);  // 上拉
    GPIOB->AFR[0] |= (4U << 24) | (4U << 28); // AF4

    // 复位I2C
    I2C1->CR1 = I2C_CR1_SWRST;
    I2C1->CR1 = 0;

    // 配置时钟频率(36MHz)
    I2C1->CR2 = 36;

    // 配置CCR(100kHz标准模式)
    I2C1->CCR = 180;  // 36MHz / (2 * 100kHz)
    I2C1->TRISE = 37; // (1000ns / 28ns) + 1

    // 使能I2C
    I2C1->CR1 = I2C_CR1_PE;
}

// 发送起始信号
void I2C1_Start(void) {
    I2C1->CR1 |= I2C_CR1_START;
    while (!(I2C1->SR1 & I2C_SR1_SB));  // 等待起始条件完成
}

// 发送地址
void I2C1_SendAddr(uint8_t addr, uint8_t dir) {
    I2C1->DR = (addr << 1) | dir;
    while (!(I2C1->SR1 & I2C_SR1_ADDR));  // 等待地址发送完成
    (void)I2C1->SR2;  // 读SR2清除ADDR标志
}

// 写BMP280传感器寄存器
void BMP280_WriteReg(uint8_t reg, uint8_t data) {
    I2C1_Start();
    I2C1_SendAddr(0x76, 0);  // 写模式

    I2C1->DR = reg;
    while (!(I2C1->SR1 & I2C_SR1_TXE));

    I2C1->DR = data;
    while (!(I2C1->SR1 & I2C_SR1_TXE));

    I2C1->CR1 |= I2C_CR1_STOP;
}

SPI vs I2C对比

特性SPII2C
线数4线(MOSI/MISO/SCK/CS)2线(SDA/SCL)
速度快(可达几十MHz)慢(100kHz-3.4MHz)
硬件复杂度简单需要上拉电阻
多从机每个从机需独立CS地址区分,最多128设备
全双工是否
应用Flash、LCD、SD卡传感器、EEPROM、RTC

ADC模数转换

ADC关键参数

// ADC配置示例(12位分辨率)
void ADC1_Init(void) {
    // 使能时钟
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

    // 配置PA0为模拟输入
    GPIOA->MODER |= (3U << 0);

    // ADC配置
    ADC1->CR2 = 0;
    ADC1->CR1 = 0;  // 12位分辨率
    ADC1->SQR1 = 0;  // 1个转换
    ADC1->SQR3 = 0;  // 通道0
    ADC1->SMPR2 = 7;  // 采样时间480周期

    ADC->CCR = 0;  // 分频2
    ADC1->CR2 = ADC_CR2_ADON;  // 使能ADC
}

uint16_t ADC1_Read(void) {
    ADC1->CR2 |= ADC_CR2_SWSTART;  // 启动转换
    while (!(ADC1->SR & ADC_SR_EOC));  // 等待转换完成
    return ADC1->DR;
}

// 电压计算: V = (ADC_Value / 4095) * VREF
float ADC_ToVoltage(uint16_t adc_val) {
    return (adc_val / 4095.0f) * 3.3f;
}

PWM脉宽调制

// 使用定时器3通道1产生PWM(PA6)
void PWM_Init(uint16_t freq, uint16_t duty) {
    // 使能时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

    // 配置PA6为复用功能
    GPIOA->MODER |= (2U << 12);
    GPIOA->AFR[0] |= (2U << 24);  // AF2

    // 定时器配置
    TIM3->PSC = 72 - 1;  // 预分频器(1MHz)
    TIM3->ARR = 1000000 / freq - 1;  // 自动重装载值
    TIM3->CCR1 = (TIM3->ARR + 1) * duty / 100;  // 占空比

    // PWM模式1
    TIM3->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1PE;
    TIM3->CCER = TIM_CCER_CC1E;  // 使能输出
    TIM3->CR1 = TIM_CR1_ARPE | TIM_CR1_CEN;  // 使能计数器
}

// 设置占空比(0-100)
void PWM_SetDuty(uint16_t duty) {
    TIM3->CCR1 = (TIM3->ARR + 1) * duty / 100;
}

// 呼吸灯效果
void breathing_led(void) {
    static uint8_t duty = 0;
    static int8_t step = 1;

    PWM_SetDuty(duty);
    duty += step;

    if (duty >= 100 || duty <= 0) {
        step = -step;
    }
}

中断系统

NVIC配置

// 配置EXTI0中断(PA0外部中断)
void EXTI0_Config(void) {
    // 使能SYSCFG时钟
    RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;

    // 配置PA0为中断源
    SYSCFG->EXTICR[0] = 0;  // EXTI0连接到PA

    // 配置EXTI
    EXTI->IMR |= EXTI_IMR_MR0;   // 使能中断
    EXTI->FTSR |= EXTI_FTSR_TR0; // 下降沿触发

    // 配置NVIC
    NVIC_SetPriority(EXTI0_IRQn, 2);
    NVIC_EnableIRQ(EXTI0_IRQn);
}

// 中断服务程序
void EXTI0_IRQHandler(void) {
    if (EXTI->PR & EXTI_PR_PR0) {
        EXTI->PR = EXTI_PR_PR0;  // 清除中断标志

        // 中断处理(尽量简短)
        button_pressed_flag = 1;
    }
}

高频面试题

1. SPI和I2C的主要区别是什么?如何选择?

答案: 见上方对比表格。选择原则:

  • SPI: 需要高速传输(LCD、SD卡)、全双工通信、从机数量少
  • I2C: 引脚资源紧张、需要连接多个设备、速度要求不高(传感器)

2. 什么是GPIO的开漏输出?为什么I2C要用开漏?

答案: 开漏输出只能输出低电平,高电平需要外部上拉电阻。I2C使用开漏的原因:

  1. 多主机支持: 任何设备都可以将线拉低,实现线与功能
  2. 电平转换: 通过不同电压的上拉实现电平匹配(如3.3V单片机与5V设备通信)
  3. 总线仲裁: 冲突检测机制

3. UART的波特率误差如何计算?多大误差可接受?

答案:

// 实际波特率 = 时钟频率 / (16 * BRR)
// 误差 = |实际波特率 - 目标波特率| / 目标波特率 * 100%

// 例: 8MHz时钟,目标115200
float actual = 8000000.0 / (16 * 4);  // 125000
float error = fabs(actual - 115200) / 115200 * 100%;  // 8.5%

一般误差<2.5%可正常工作,<5%能工作但可能偶尔出错。建议使用晶振提高精度。

4. ADC采样时间如何选择?

答案: 采样时间取决于源阻抗和精度要求:

  • 低阻抗(<1kΩ): 3-15个周期
  • 中阻抗(1-10kΩ): 15-56个周期
  • 高阻抗(>10kΩ): 56-480个周期

公式: t_conv = 采样时间 + 12.5周期 (12位ADC)

5. 如何实现PWM控制舵机?

答案: 舵机控制协议:20ms周期,0.5-2.5ms脉宽对应0-180度

// 50Hz频率(20ms周期)
void Servo_Init(void) {
    PWM_Init(50, 0);  // 50Hz
}

// angle: 0-180度
void Servo_SetAngle(uint8_t angle) {
    // 0.5ms=2.5%, 2.5ms=12.5%
    uint16_t duty = 25 + angle * 75 / 180;  // 2.5-12.5%
    PWM_SetDuty(duty);
}