硬件接口
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四种模式
| 模式 | CPOL | CPHA | 时钟空闲 | 采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低电平 | 第一个边沿 |
| 1 | 0 | 1 | 低电平 | 第二个边沿 |
| 2 | 1 | 0 | 高电平 | 第一个边沿 |
| 3 | 1 | 1 | 高电平 | 第二个边沿 |
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对比
| 特性 | SPI | I2C |
|---|---|---|
| 线数 | 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使用开漏的原因:
- 多主机支持: 任何设备都可以将线拉低,实现线与功能
- 电平转换: 通过不同电压的上拉实现电平匹配(如3.3V单片机与5V设备通信)
- 总线仲裁: 冲突检测机制
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);
}