引言
在嵌入式开发领域,STM32 凭借其高性能、高性价比成为主流选择,而中断是 STM32 实现实时响应的核心技术 —— 它能让 CPU 在执行主程序时,及时响应外部 / 内部事件(如传感器数据就绪、按键触发、定时器溢出等),大幅提升系统实时性。阿克曼小车作为嵌入式实战的经典项目,集成了多种外设(MPU6050、舵机、编码器电机、PS2 手柄、CAN 总线),恰好能全方位覆盖 STM32 中断的应用场景。
本文将以 “理论 + 实战” 为核心,从 STM32 中断基础切入,逐一拆解各外设的硬件连接、中断配置、软件编程,最终实现阿克曼小车的完整控制逻辑。全文采用 确保零基础读者也能轻松上手,涵盖从底层驱动到上层应用的全流程。
第一章 STM32 中断基础:从概念到配置
要掌握中断应用,需先理解其核心原理。本章将梳理 STM32 中断的基本概念、分类、优先级机制及初始化流程,为后续外设开发奠定基础。
1.1 中断核心概念
中断是指 CPU 在执行当前程序时,因外部 / 内部事件触发,暂停当前程序,转而去执行 “中断服务函数(ISR)”,执行完后再返回原程序继续执行的机制。其核心价值是提升实时性(无需 CPU 轮询等待事件)和降低 CPU 负载。
|
术语 |
定义 |
作用 |
|
中断源 |
触发中断的事件 / 设备(如 GPIO 电平变化、定时器溢出、I2C 数据就绪) |
决定 “什么事件能打断 CPU” |
|
中断请求(IRQ) |
中断源向 CPU 发送的 “请求信号” |
触发 CPU 中断响应的 “导火索” |
|
中断服务函数(ISR) |
中断触发后 CPU 执行的专用函数 |
处理中断事件的 “核心逻辑”(如读取传感器数据、控制外设动作) |
|
中断优先级 |
多个中断同时触发时,CPU 优先处理的顺序 |
确保高优先级事件(如电机转速反馈)不被低优先级事件(如按键检测)阻塞 |
|
中断使能 |
允许 / 禁止某个中断源触发中断的开关 |
控制 “某个事件是否能触发中断” |
|
中断挂起 |
中断请求已产生,但 CPU 暂未处理(如被更高优先级中断阻塞)的状态 |
避免中断请求丢失 |
1.2 STM32 中断分类(以 STM32F103 为例)
STM32F103 属于 Cortex-M3 内核,支持 16 个可编程中断优先级,中断源分为 “内核中断” 和 “外设中断”,其中外设中断是嵌入式开发的重点。
|
中断类型 |
包含中断源 |
应用场景 |
|
内核中断 |
SysTick(系统滴答定时器)、NMI(不可屏蔽中断)、HardFault(硬件故障中断)等 |
系统定时(SysTick)、紧急故障处理(HardFault) |
|
外设中断 |
GPIO 外部中断、定时器中断(更新 / 比较 / PWM / 编码器模式)、I2C 中断、SPI 中断、CAN 中断等 |
传感器数据读取(GPIO/I2C)、电机 PWM 控制(定时器)、手柄通信(SPI)、总线通信(CAN) |
1.3 中断优先级机制
STM32 采用 “分组优先级” 机制,通过NVIC_PriorityGroupConfig()函数配置优先级分组,将优先级分为 “抢占优先级” 和 “子优先级”:
- 抢占优先级:高抢占优先级的中断可打断低抢占优先级的中断(“抢断”);
- 子优先级:抢占优先级相同时,子优先级高的中断先执行(无 “抢断” 能力)。
优先级分组配置表(Cortex-M3 内核)
|
优先级分组 |
抢占优先级位数 |
子优先级位数 |
抢占优先级等级数 |
子优先级等级数 |
函数配置代码 |
|
NVIC_PriorityGroup_0 |
0 |
4 |
1(仅 0 级) |
16(0-15 级) |
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0); |
|
NVIC_PriorityGroup_1 |
1 |
3 |
2(0-1 级) |
8(0-7 级) |
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); |
|
NVIC_PriorityGroup_2 |
2 |
2 |
4(0-3 级) |
4(0-3 级) |
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); |
|
NVIC_PriorityGroup_3 |
3 |
1 |
8(0-7 级) |
2(0-1 级) |
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_3); |
|
NVIC_PriorityGroup_4 |
4 |
0 |
16(0-15 级) |
1(仅 0 级) |
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); |
阿克曼小车中断优先级规划
小车需同时处理多个中断,需按 “实时性需求” 规划优先级(推荐使用分组 2):
|
中断源 |
功能 |
抢占优先级 |
子优先级 |
原因分析 |
|
编码器定时器更新中断 |
电机转速计算 |
0(最高) |
0 |
转速反馈需实时,否则电机控制精度下降 |
|
MPU6050 数据就绪中断 |
姿态数据采集 |
1 |
0 |
姿态控制需及时,避免小车跑偏 |
|
PS2 手柄 SPI 接收中断 |
控制指令接收 |
2 |
0 |
手柄指令需快速响应,否则操作延迟 |
|
CAN 总线接收中断 |
外设通信(如扩展模块) |
2 |
1 |
总线通信实时性低于手柄,但需优先于非关键中断 |
|
舵机 PWM 定时器更新中断 |
舵机角度微调 |
3 |
0 |
舵机响应速度较慢,低优先级不影响控制 |
1.4 STM32 中断配置通用流程
无论何种中断源,STM32 中断配置均遵循 “4 步流程”,以下以标准库为例(STM32F103):
|
步骤 |
核心操作 |
示例代码(以 GPIO 外部中断为例) |
|
1. 使能外设时钟 |
使能中断相关外设的时钟(如 GPIO 时钟、AFIO 时钟、NVIC 时钟) |
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |
|
2. 配置中断源 |
配置外设的中断触发条件(如 GPIO 电平触发、定时器溢出触发) |
GPIO_EXTILineConfig (GPIO_PortSourceGPIOA, GPIO_PinSource0); // 配置 PA0 为外部中断线EXTI_InitTypeDef EXTI_InitStruct;EXTI_InitStruct.EXTI_Line = EXTI_Line0;EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising; // 上升沿触发EXTI_InitStruct.EXTI_LineCmd = ENABLE;EXTI_Init(&EXTI_InitStruct); |
|
3. 配置 NVIC |
配置中断优先级、使能中断通道 |
NVIC_InitTypeDef NVIC_InitStruct;NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn; // 中断通道(EXTI0)NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级 2NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; // 子优先级 0NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;NVIC_Init(&NVIC_InitStruct); |
|
4. 编写 ISR |
实现中断服务函数,处理中断事件(如读取数据、置位标志位) |
void EXTI0_IRQHandler(void){ if (EXTI_GetITStatus (EXTI_Line0) != RESET) // 检查中断标志 { GPIO_SetBits (GPIOB, GPIO_Pin_5); // 示例:点亮 LED EXTI_ClearITPendingBit (EXTI_Line0); // 清除中断标志 }} |
注意:中断服务函数名需与 STM32 启动文件(如 startup_stm32f10x_md.s)中的中断向量表一致,否则无法触发。
第二章 模块 1:MPU6050(I2C)与中断应用
MPU6050 是常用的 6 轴传感器(3 轴加速度 + 3 轴角速度),通过 I2C 总线与 STM32 通信,其 “数据就绪中断” 可让 STM32 实时获取姿态数据,避免 CPU 轮询等待。
2.1 MPU6050 硬件原理
2.1.1 核心参数与引脚定义
|
参数 |
规格 |
|
通信接口 |
I2C(从机地址:0x68/0x69,取决于 AD0 引脚电平) |
|
加速度范围 |
±2g/±4g/±8g/±16g(可配置) |
|
角速度范围 |
±250°/s/±500°/s/±1000°/s/±2000°/s(可配置) |
|
中断输出 |
INT 引脚(数据就绪时输出高 / 低电平,可配置) |
|
供电电压 |
3.3V |
2.1.2 阿克曼小车硬件接线表(STM32F103C8T6 + MPU6050)
|
MPU6050 引脚 |
功能 |
STM32 引脚 |
备注 |
|
VCC |
电源 |
3.3V |
不可接 5V,否则烧毁传感器 |
|
GND |
地 |
GND |
共地,减少干扰 |
|
SCL |
I2C 时钟线 |
PB6 |
STM32F103 默认 I2C1_SCL 为 PB6(需使能 I2C1 时钟) |
|
SDA |
I2C 数据线 |
PB7 |
STM32F103 默认 I2C1_SDA 为 PB7 |
|
AD0 |
从机地址选择 |
GND |
接 GND 时地址为 0x68,接 VCC 时为 0x69(本文用 0x68) |
|
INT |
数据就绪中断 |
PA0 |
接 STM32 外部中断引脚,用于触发数据读取 |
2.1.3 中断触发原理
MPU6050 每次完成数据采集(加速度 / 角速度)后,会通过 INT 引脚输出触发信号,STM32 通过 “外部中断” 检测该信号,进而读取数据。触发方式可配置为 “上升沿”“下降沿” 或 “双边沿”,本文选择 “上升沿触发”。
2.2 MPU6050 软件实现(标准库)
2.2.1 软件模块划分
|
模块 |
功能 |
核心函数 |
|
I2C 驱动 |
实现 STM32 与 MPU6050 的 I2C 通信(读 / 写寄存器) |
I2C_Init()、I2C_WriteByte()、I2C_ReadByte() |
|
MPU6050 初始化 |
配置传感器的量程、采样率、中断使能 |
MPU6050_Init()、MPU6050_SetRange()、MPU6050_EnableINT() |
|
外部中断配置 |
配置 STM32 的 PA0 为外部中断,响应 MPU6050 的 INT 信号 |
EXTI_MPU6050_Init()、NVIC_MPU6050_Init() |
|
数据处理 |
读取原始数据、转换为实际物理量、滤波(滑动平均 / 卡尔曼) |
MPU6050_ReadData()、MPU6050_ConvertData()、MPU6050_Filter() |
|
中断服务函数 |
触发数据读取与处理 |
EXTI0_IRQHandler() |
2.2.2 核心代码实现
1. I2C 驱动初始化(I2C1)
#include "stm32f10x.h"
#include "mpu6050.h"
// I2C1初始化(MPU6050通信)
void I2C1_Init(void)
{
I2C_InitTypeDef I2C_InitStruct;
GPIO_InitTypeDef GPIO_InitStruct;
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); // PB口和AFIO时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // I2C1时钟(APB1总线,36MHz)
// 2. 配置SCL(PB6)和SDA(PB7)为开漏复用输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏复用(I2C必需)
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
// 3. 配置I2C1参数
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C; // I2C模式
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2; // 占空比2(1/2)
I2C_InitStruct.I2C_OwnAddress1 = 0x00; // STM32作为主机,无需设置自身地址
I2C_InitStruct.I2C_Ack = I2C_Ack_Enable; // 使能应答
I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // 7位地址模式
I2C_InitStruct.I2C_ClockSpeed = 400000; // 通信速率400kHz(快速模式)
I2C_Init(I2C1, &I2C_InitStruct);
// 4. 使能I2C1
I2C_Cmd(I2C1, ENABLE);
}
// I2C写一个字节到MPU6050寄存器
void I2C_WriteByte(uint8_t addr, uint8_t reg, uint8_t data)
{
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); // 等待总线空闲
// 发送起始信号
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 等待起始信号发送成功
// 发送MPU6050从机地址(写模式:地址+0)
I2C_Send7bitAddress(I2C1, addr, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 等待从机应答
// 发送要写入的寄存器地址
I2C_SendData(I2C1, reg);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 等待数据发送成功
// 发送要写入的数据
I2C_SendData(I2C1, data);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 等待数据发送成功
// 发送停止信号
I2C_GenerateSTOP(I2C1, ENABLE);
}
// I2C从MPU6050寄存器读一个字节
uint8_t I2C_ReadByte(uint8_t addr, uint8_t reg)
{
uint8_t data;
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); // 等待总线空闲
// 第一步:发送寄存器地址(写模式)
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, addr, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, reg);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
// 第二步:读取数据(读模式)
I2C_GenerateSTART(I2C1, ENABLE); // 重新发送起始信号(重复起始)
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, addr, I2C_Direction_Receiver); // 读模式:地址+1
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
// 禁止应答(最后一个字节无需应答)
I2C_AcknowledgeConfig(I2C1, DISABLE);
// 发送停止信号(提前发送,避免数据丢失)
I2C_GenerateSTOP(I2C1, ENABLE);
// 读取数据
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
data = I2C_ReceiveData(I2C1);
// 恢复应答使能(为下次通信做准备)
I2C_AcknowledgeConfig(I2C1, ENABLE);
return data;
}
2. MPU6050 初始化与中断配置
#define MPU6050_ADDR 0x68 // AD0接GND,从机地址0x68(8位地址:0xD0)
#define MPU6050_PWR_MGMT_1 0x6B // 电源管理寄存器1
#define MPU6050_INT_ENABLE 0x38 // 中断使能寄存器
#define MPU6050_INT_PIN_CFG 0x37 // 中断引脚配置寄存器
#define MPU6050_ACCEL_XOUT_H 0x3B // 加速度X轴高位寄存器
#define MPU6050_GYRO_XOUT_H 0x43 // 角速度X轴高位寄存器
// MPU6050数据结构体(原始数据+转换后数据)
typedef struct
{
// 原始数据(16位,高8位+低8位)
int16_t accel_x;
int16_t accel_y;
int16_t accel_z;
int16_t gyro_x;
int16_t gyro_y;
int16_t gyro_z;
// 转换后数据(物理量)
float accel_x_g; // 加速度X轴(g)
float accel_y_g; // 加速度Y轴(g)
float accel_z_g; // 加速度Z轴(g)
float gyro_x_dps; // 角速度X轴(°/s)
float gyro_y_dps; // 角速度Y轴(°/s)
float gyro_z_dps; // 角速度Z轴(°/s)
} MPU6050_Data;
MPU6050_Data mpu6050_data; // 全局数据变量
uint8_t mpu6050_int_flag = 0; // 中断标志位(1:数据就绪)
// MPU6050初始化(配置量程、采样率、中断)
uint8_t MPU6050_Init(void)
{
uint8_t id;
I2C1_Init(); // 初始化I2C1
// 1. 唤醒MPU6050(默认上电为睡眠模式)
I2C_WriteByte(MPU6050_ADDR, MPU6050_PWR_MGMT_1, 0x00); // 0x00:唤醒,禁用睡眠
delay_ms(10); // 等待传感器稳定
// 2. 读取WHO_AM_I寄存器(0x75),验证通信是否正常(返回0x68)
id = I2C_ReadByte(MPU6050_ADDR, 0x75);
if(id != 0x68)
{
return 1; // 通信失败
}
// 3. 配置加速度量程(±2g):ACCEL_CONFIG寄存器(0x1C),0x00=±2g
I2C_WriteByte(MPU6050_ADDR, 0x1C, 0x00);
// 配置角速度量程(±250°/s):GYRO_CONFIG寄存器(0x1B),0x00=±250°/s
I2C_WriteByte(MPU6050_ADDR, 0x1B, 0x00);
// 4. 配置中断:使能数据就绪中断
I2C_WriteByte(MPU6050_ADDR, MPU6050_INT_ENABLE, 0x01); // 0x01:使能数据就绪中断
// 配置中断引脚:INT引脚高电平触发,中断产生后保持高电平直到数据被读取
I2C_WriteByte(MPU6050_ADDR, MPU6050_INT_PIN_CFG, 0x10); // 0x10:高电平触发
// 5. 配置STM32外部中断(PA0)
EXTI_MPU6050_Init();
NVIC_MPU6050_Init();
return 0; // 初始化成功
}
// 配置MPU6050中断引脚(PA0)为外部中断
void EXTI_MPU6050_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
EXTI_InitTypeDef EXTI_InitStruct;
// 1. 使能GPIOA和AFIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
// 2. 配置PA0为浮空输入(外部中断引脚需为输入模式)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置外部中断线(PA0对应EXTI_Line0)
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
EXTI_InitStruct.EXTI_Line = EXTI_Line0;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising; // 上升沿触发(MPU6050 INT高电平)
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
}
// 配置NVIC(MPU6050中断通道)
void NVIC_MPU6050_Init(void)
{
NVIC_InitTypeDef NVIC_InitStruct;
// 配置优先级分组(全局仅需配置一次,建议在main函数开头配置)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 配置EXTI0中断通道(PA0对应EXTI0_IRQn)
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1(按之前规划)
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; // 子优先级0
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
3. 数据读取与处理
// 读取MPU6050原始数据(16位,高8位+低8位)
void MPU6050_ReadRawData(void)
{
uint8_t buf[14]; // 加速度(6字节)+角速度(6字节)=12字节,此处预留14字节
// 连续读取12字节数据(从ACCEL_XOUT_H开始)
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, MPU6050_ADDR, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, MPU6050_ACCEL_XOUT_H);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
// 切换为读模式
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, MPU6050_ADDR, I2C_Direction_Receiver);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
// 读取前11个字节(使能应答)
for(uint8_t i=0; i<11; i++)
{
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
buf[i] = I2C_ReceiveData(I2C1);
}
// 读取第12个字节(禁止应答,发送停止信号)
I2C_AcknowledgeConfig(I2C1, DISABLE);
I2C_GenerateSTOP(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
buf[11] = I2C_ReceiveData(I2C1);
I2C_AcknowledgeConfig(I2C1, ENABLE); // 恢复应答
// 组合高8位和低8位(大端模式)
mpu6050_data.accel_x = (buf[0] << 8) | buf[1];
mpu6050_data.accel_y = (buf[2] << 8) | buf[3];
mpu6050_data.accel_z = (buf[4] << 8) | buf[5];
mpu6050_data.gyro_x = (buf[8] << 8) | buf[9];
mpu6050_data.gyro_y = (buf[10] << 8) | buf[11];
mpu6050_data.gyro_z = (buf[12] << 8) | buf[13];
}
// 原始数据转换为物理量(基于量程配置)
void MPU6050_ConvertData(void)
{
// 加速度量程±2g:灵敏度系数=16384 LSB/g(2^15 / 2g = 16384)
mpu6050_data.accel_x_g = (float)mpu6050_data.accel_x / 16384.0f;
mpu6050_data.accel_y_g = (float)mpu6050_data.accel_y / 16384.0f;
mpu6050_data.accel_z_g = (float)mpu6050_data.accel_z / 16384.0f;
// 角速度量程±250°/s:灵敏度系数=131 LSB/(°/s)(2^15 / 250°/s ≈131)
mpu6050_data.gyro_x_dps = (float)mpu6050_data.gyro_x / 131.0f;
mpu6050_data.gyro_y_dps = (float)mpu6050_data.gyro_y / 131.0f;
mpu6050_data.gyro_z_dps = (float)mpu6050_data.gyro_z / 131.0f;
}
// 滑动平均滤波(减少数据抖动)
#define FILTER_DEPTH 5 // 滤波深度(5次采样)
float accel_x_filter[FILTER_DEPTH] = {0};
float accel_y_filter[FILTER_DEPTH] = {0};
float accel_z_filter[FILTER_DEPTH] = {0};
float gyro_x_filter[FILTER_DEPTH] = {0};
float gyro_y_filter[FILTER_DEPTH] = {0};
float gyro_z_filter[FILTER_DEPTH] = {0};
uint8_t filter_index = 0;
void MPU6050_Filter(void)
{
// 存入新数据
accel_x_filter[filter_index] = mpu6050_data.accel_x_g;
accel_y_filter[filter_index] = mpu6050_data.accel_y_g;
accel_z_filter[filter_index] = mpu6050_data.accel_z_g;
gyro_x_filter[filter_index] = mpu6050_data.gyro_x_dps;
gyro_y_filter[filter_index] = mpu6050_data.gyro_y_dps;
gyro_z_filter[filter_index] = mpu6050_data.gyro_z_dps;
// 计算平均值
float sum_accel_x = 0, sum_accel_y = 0, sum_accel_z = 0;
float sum_gyro_x = 0, sum_gyro_y = 0, sum_gyro_z = 0;
for(uint8_t i=0; i<FILTER_DEPTH; i++)
{
sum_accel_x += accel_x_filter[i];
sum_accel_y += accel_y_filter[i];
sum_accel_z += accel_z_filter[i];
sum_gyro_x += gyro_x_filter[i];
sum_gyro_y += gyro_y_filter[i];
sum_gyro_z += gyro_z_filter[i];
}
mpu6050_data.accel_x_g = sum_accel_x / FILTER_DEPTH;
mpu6050_data.accel_y_g = sum_accel_y / FILTER_DEPTH;
mpu6050_data.accel_z_g = sum_accel_z / FILTER_DEPTH;
mpu6050_data.gyro_x_dps = sum_gyro_x / FILTER_DEPTH;
mpu6050_data.gyro_y_dps = sum_gyro_y / FILTER_DEPTH;
mpu6050_data.gyro_z_dps = sum_gyro_z / FILTER_DEPTH;
// 更新滤波索引
filter_index = (filter_index + 1) % FILTER_DEPTH;
}
4. 中断服务函数(EXTI0_IRQHandler)
// MPU6050数据就绪中断服务函数
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0) != RESET) // 检查中断标志
{
MPU6050_ReadRawData(); // 读取原始数据
MPU6050_ConvertData(); // 转换为物理量
MPU6050_Filter(); // 滤波处理
mpu6050_int_flag = 1; // 置位中断标志(主程序可查询该标志做后续处理)
EXTI_ClearITPendingBit(EXTI_Line0); // 清除中断标志(必需,否则会重复触发)
}
}
2.2.3 数据验证与调试
通过串口将 MPU6050 数据打印到电脑(需配置 STM32 串口),验证数据是否正常:
#include "usart.h"
int main(void)
{
delay_init(); // 延时初始化
USART1_Init(115200); // 串口1初始化(波特率115200)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 优先级分组(全局配置)
// 初始化MPU6050
if(MPU6050_Init() != 0)
{
printf("MPU6050初始化失败!\r\n");
while(1); // 死循环,提示错误
}
printf("MPU6050初始化成功!\r\n");
while(1)
{
if(mpu6050_int_flag == 1) // 检测到数据就绪中断
{
// 打印转换后的物理量数据
printf("加速度:X=%.2fg, Y=%.2fg, Z=%.2fg\r\n",
mpu6050_data.accel_x_g,
mpu6050_data.accel_y_g,
mpu6050_data.accel_z_g);
printf("角速度:X=%.2f°/s, Y=%.2f°/s, Z=%.2f°/s\r\n",
mpu6050_data.gyro_x_dps,
mpu6050_data.gyro_y_dps,
mpu6050_data.gyro_z_dps);
printf("-------------------------\r\n");
mpu6050_int_flag = 0; // 清除标志位
}
delay_ms(100); // 降低打印频率,避免串口拥堵
}
}
2.2.4 常见问题与解决方案
|
问题现象 |
可能原因 |
解决方案 |
|
MPU6050 初始化失败(返回 1) |
1. I2C 引脚接线错误(SCL/SDA 接反或未接)2. MPU6050 供电异常(未接 3.3V)3. 从机地址错误(AD0 引脚电平配置错误) |
1. 重新核对接线(SCL=PB6,SDA=PB7)2. 测量 MPU6050 VCC 引脚电压,确保为 3.3V3. 确认 AD0 引脚电平,修改MPU6050_ADDR(接 VCC 时为 0x69) |
|
无中断触发(int_flag 始终为 0) |
1. INT 引脚接线错误(未接 PA0)2. MPU6050 中断未使能(INT_ENABLE寄存器配置错误)3. 外部中断触发方式错误(如配置为下降沿) |
1. 重新核对 INT 引脚接线(PA0)2. 检查I2C_WriteByte(MPU6050_ADDR, MPU6050_INT_ENABLE, 0x01)是否执行3. 修改外部中断触发方式为上升沿(EXTI_Trigger_Rising) |
|
数据抖动严重(波动大) |
1. 未进行滤波处理2. 硬件干扰(接线松动、未共地)3. 传感器安装不牢固(小车震动导致) |
1. 启用滑动平均滤波或卡尔曼滤波2. 检查接线是否牢固,确保 STM32 与 MPU6050 共地3. 将 MPU6050 固定在小车平稳位置,避免震动 |
第三章 模块 2:舵机 PWM 方向控制与中断
阿克曼小车的方向控制依赖舵机 —— 通过 STM32 定时器输出 PWM 信号控制舵机角度,结合中断可实现角度的实时微调(如根据 PS2 手柄指令动态调整方向)。
3.1 舵机硬件原理
3.1.1 舵机工作原理
舵机通过接收不同占空比的 PWM 信号,驱动内部电机转动到对应角度,核心参数如下:
- PWM 周期:固定为 20ms(频率 50Hz);
- 占空比与角度对应关系:
-
- 0.5ms 高电平(占空比 2.5%)→ 0°;
-
- 1.5ms 高电平(占空比 7.5%)→ 90°(中位);
-
- 2.5ms 高电平(占空比 12.5%)→ 180°。
3.1.2 常用舵机型号与参数
|
型号 |
角度范围 |
扭矩 |
响应速度 |
供电电压 |
适用场景 |
|
SG90 |
0°-180° |
1.8kg·cm(4.8V) |
0.12s/60° |
4.8V-6V |
小型阿克曼小车(重量轻) |
|
MG90S |
0°-180° |
2.2kg·cm(6V) |
0.08s/60° |
4.8V-6V |
中型阿克曼小车(重量中等) |
|
MG996R |
0°-180° |
13kg·cm(6V) |
0.15s/60° |
4.8V-7.2V |
大型阿克曼小车(重量大) |
本文选用 SG90 舵机(性价比高,适合入门)。
3.1.3 硬件接线表(STM32F103C8T6 + SG90)
|
舵机引脚 |
功能 |
STM32 引脚 |
备注 |
|
VCC |
电源 |
5V |
SG90 需 5V 供电(STM32 开发板 5V 引脚),不可接 3.3V(扭矩不足) |
|
GND |
地 |
GND |
与 STM32 共地,减少干扰 |
|
SIGNAL |
PWM 控制信号 |
PA8 |
接 STM32 定时器 1_CH1(TIM1_CH1),用于输出 PWM 信号 |
3.2 定时器 PWM 原理与中断配置
STM32 定时器可工作在 “PWM 模式”,通过配置 “自动重装寄存器(ARR)” 和 “比较寄存器(CCR)” 控制 PWM 周期和占空比:
- PWM 周期 = (ARR + 1) × (PSC + 1) / 定时器时钟频率;
- 占空比 = (CCR + 1) / (ARR + 1) × 100%。
3.2.1 定时器 PWM 参数计算(以 TIM1 为例)
目标:输出 20ms 周期(50Hz)的 PWM 信号(SG90 需求)。
- 定时器时钟频率:STM32F103 TIM1 挂载在 APB2 总线,时钟频率为 72MHz(默认);
- 预分频系数(PSC):设为 7199 → 定时器计数频率 = 72MHz / (7199 + 1) = 10kHz;
- 自动重装值(ARR):设为 199 → PWM 周期 = (199 + 1) × 1/10kHz = 20ms(符合要求);
- CCR 值计算(对应角度):
-
- 0°(0.5ms 高电平):CCR = (0.5ms × 10kHz) - 1 = 4 → 占空比 = (4+1)/(199+1) = 2.5%;
-
- 90°(1.5ms 高电平):CCR = (1.5ms × 10kHz) - 1 = 14 → 占空比 = 15/200 = 7.5%;
-
- 180°(2.5ms 高电平):CCR = (2.5ms × 10kHz) - 1 = 24 → 占空比 = 25/200 = 12.5%。
3.2.2 定时器中断配置
为实现舵机角度的实时微调(如每 10ms 检查一次控制指令),可配置定时器 “更新中断”(定时器溢出时触发),在中断服务函数中调整 CCR 值。
3.3 舵机 PWM 控制软件实现
3.3.1 软件模块划分
|
模块 |
功能 |
核心函数 |
|
定时器初始化 |
配置 TIM1 为 PWM 模式,设置周期、占空比 |
TIM1_PWM_Init() |
|
舵机角度控制 |
根据目标角度计算 CCR 值,控制舵机转动 |
Servo_SetAngle() |
|
定时器中断配置 |
配置 TIM1 更新中断,用于实时微调角度 |
NVIC_TIM1_Init() |
|
中断服务函数 |
响应定时器更新中断,调整舵机角度(如根据 PS2 指令) |
TIM1_UP_IRQHandler() |
3.3.2 核心代码实现
1. 定时器 1 PWM 初始化(PA8)
#include "stm32f10x.h"
#include "servo.h"
#define SERVO_TIM TIM1 // 舵机使用TIM1
#define SERVO_CHANNEL TIM_Channel_1 // 舵机使用TIM1_CH1(PA8)
#define SERVO_PSC 7199 // 预分频系数:72MHz/(7199+1)=10kHz
#define SERVO_ARR 199 // 自动重装值:(199+1)*1/10kHz=20ms(周期)
#define SERVO_MIN_CCR 4 // 0°对应的CCR值(0.5ms高电平)
#define SERVO_MAX_CCR 24 // 180°对应的CCR值(2.5ms高电平)
#define SERVO_MID_ANGLE 90 // 舵机中位角度(90°)
uint16_t target_angle = SERVO_MID_ANGLE; // 目标角度(默认中位)
// TIM1 PWM初始化(PA8)
void TIM1_PWM_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
TIM_OCInitTypeDef TIM_OCInitStruct;
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1, ENABLE); // TIM1在APB2总线
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 复用功能时钟
// 2. 配置PA8为复用推挽输出(TIM1_CH1)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽(PWM输出必需)
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置TIM1时基参数
TIM_TimeBaseStruct.TIM_Period = SERVO_ARR; // 自动重装值
TIM_TimeBaseStruct.TIM_Prescaler = SERVO_PSC; // 预分频系数
TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频:不分频
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
TIM_TimeBaseInit(SERVO_TIM, &TIM_TimeBaseStruct);
// 4. 配置TIM1 PWM模式(模式1:CNT < CCR时输出高电平)
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; // 使能输出
TIM_OCInitStruct.TIM_OutputNState = TIM_OutputNState_Disable; // 禁用互补输出(单通道无需)
TIM_OCInitStruct.TIM_Pulse = Servo_AngleToCCR(SERVO_MID_ANGLE); // 初始CCR值(中位)
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; // 输出极性:高电平有效
TIM_OCInitStruct.TIM_OCNPolarity = TIM_OCNPolarity_High; // 互补输出极性(禁用)
TIM_OCInitStruct.TIM_OCIdleState = TIM_OCIdleState_Reset; // 空闲状态(禁用)
TIM_OCInitStruct.TIM_OCNIdleState = TIM_OCNIdleState_Reset; // 互补空闲状态(禁用)
TIM_OC1Init(SERVO_TIM, &TIM_OCInitStruct); // 配置通道1
// 5. 使能PWM输出
TIM_OC1PreloadConfig(SERVO_TIM, TIM_OCPreload_Enable); // 使能CCR预装载
TIM_ARRPreloadConfig(SERVO_TIM, ENABLE); // 使能ARR预装载
// 6. 配置TIM1更新中断(用于实时调整角度)
TIM_ITConfig(SERVO_TIM, TIM_IT_Update, ENABLE); // 使能更新中断
NVIC_TIM1_Init(); // 配置NVIC
// 7. 启动定时器
TIM_Cmd(SERVO_TIM, ENABLE);
TIM_CtrlPWMOutputs(SERVO_TIM, ENABLE); // TIM1为高级定时器,需额外使能PWM输出
}
// 角度转换为CCR值(线性映射)
uint16_t Servo_AngleToCCR(uint16_t angle)
{
// 限制角度范围(0°-180°)
if(angle < 0) angle = 0;
if(angle > 180) angle = 180;
// 线性映射:angle → CCR(0°→4,180°→24)
return (uint16_t)((float)(angle) / 180.0f * (SERVO_MAX_CCR - SERVO_MIN_CCR) + SERVO_MIN_CCR);
}
// 设置舵机目标角度
void Servo_SetAngle(uint16_t angle)
{
target_angle = angle; // 更新目标角度
// 立即更新CCR值(可选:也可在中断中更新,实现平滑调整)
TIM_SetCompare1(SERVO_TIM, Servo_AngleToCCR(target_angle));
}
2. 定时器 1 中断配置(NVIC)
// 配置TIM1更新中断的NVIC
void NVIC_TIM1_Init(void)
{
NVIC_InitTypeDef NVIC_InitStruct;
// 优先级分组(全局已配置,此处无需重复)
// NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 配置TIM1更新中断通道(TIM1_UP_IRQn)
NVIC_InitStruct.NVIC_IRQChannel = TIM1_UP_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 3; // 抢占优先级3(按之前规划)
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; // 子优先级0
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
3. 定时器 1 中断服务函数(平滑调整角度)
为避免舵机角度突变(导致小车抖动),可在中断服务函数中实现 “平滑调整”—— 每次中断仅调整 1°,直到达到目标角度:
#define SERVO_ADJ_STEP 1 // 每次中断调整的角度步长(1°)
uint16_t current_angle = SERVO_MID_ANGLE; // 当前角度(默认中位)
// TIM1更新中断服务函数(每20ms触发一次,与PWM周期同步)
void TIM1_UP_IRQHandler(void)
{
if(TIM_GetITStatus(SERVO_TIM, TIM_IT_Update) != RESET) // 检查中断标志
{
// 平滑调整角度:当前角度 → 目标角度(每次调整1°)
if(current_angle < target_angle)
{
current_angle += SERVO_ADJ_STEP;
if(current_angle > target_angle) current_angle = target_angle; // 防止超调
}
else if(current_angle > target_angle)
{
current_angle -= SERVO_ADJ_STEP;
if(current_angle < target_angle) current_angle = target_angle; // 防止超调
}
// 更新CCR值,控制舵机转动
TIM_SetCompare1(SERVO_TIM, Servo_AngleToCCR(current_angle));
TIM_ClearITPendingBit(SERVO_TIM, TIM_IT_Update); // 清除中断标志
}
}
3.3.3 舵机控制测试代码
通过串口发送指令控制舵机角度(如发送 “ANGLE=45” 控制舵机转到 45°):
#include "usart.h"
#include "string.h"
#define BUF_LEN 32
char uart_buf[BUF_LEN]; // 串口接收缓冲区
uint8_t uart_buf_idx = 0; // 缓冲区索引
// 串口1中断服务函数(接收指令)
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) // 接收数据就绪
{
char data = USART_ReceiveData(USART1); // 读取接收数据
if(data == '\r' || data == '\n' || uart_buf_idx >= BUF_LEN-1) // 接收结束(回车/换行或缓冲区满)
{
uart_buf[uart_buf_idx] = '\0'; // 字符串结束符
uart_buf_idx = 0; // 重置索引
// 解析指令(如"ANGLE=45")
if(strstr(uart_buf, "ANGLE=") != NULL)
{
uint16_t angle = atoi(uart_buf + 6); // 提取"="后的数字
Servo_SetAngle(angle); // 设置舵机角度
printf("已设置舵机角度:%d°\r\n", angle);
}
}
else
{
uart_buf[uart_buf_idx++] = data; // 存入缓冲区
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清除中断标志
}
}
int main(void)
{
delay_init();
USART1_Init(115200);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 初始化舵机(默认中位90°)
TIM1_PWM_Init();
printf("舵机初始化完成,默认角度:%d°\r\n", SERVO_MID_ANGLE);
printf("发送指令格式:ANGLE=角度(如ANGLE=45)\r\n");
while(1)
{
// 主程序空闲,舵机控制由中断处理
delay_ms(100);
}
}
3.3.4 常见问题与解决方案
|
问题现象 |
可能原因 |
解决方案 |
|
舵机无反应(不转动) |
1. 舵机供电不足(未接 5V 或导线压降大)2. PWM 引脚接线错误(未接 PA8)3. 定时器未启动(TIM_Cmd未执行)4. 高级定时器未使能 PWM 输出(TIM_CtrlPWMOutputs未执行) |
1. 确保舵机 VCC 接 5V,使用粗导线减少压降2. 重新核对 PWM 引脚(PA8)3. 检查TIM_Cmd(SERVO_TIM, ENABLE)是否执行4. 检查TIM_CtrlPWMOutputs(SERVO_TIM, ENABLE)是否执行(TIM1 为高级定时器必需) |
|
舵机角度与指令不符(如指令 45° 实际转 30°) |
1. CCR 值计算错误(Servo_AngleToCCR函数映射关系错误)2. 舵机个体差异(不同舵机占空比 - 角度曲线不同) |
1. 重新校准 CCR 值:实测 0° 和 180° 对应的 CCR 值,修改SERVO_MIN_CCR和SERVO_MAX_CCR2. 针对个体舵机,调整Servo_AngleToCCR函数的映射系数 |
|
舵机抖动严重 |
1. 角度调整步长过大(SERVO_ADJ_STEP太大)2. PWM 信号干扰(接线松动、未共地)3. 舵机供电不稳定(5V 电源波动) |
1. 减小调整步长(如设为 1°)2. 检查接线是否牢固,确保 STM32 与舵机共地3. 在舵机 VCC 与 GND 之间并联 100uF 电容,稳定供电 |
第四章 模块 3:双编码器输入电机 PWM 转速控制与中断
阿克曼小车的动力来自两个电机(左 / 右),通过 “编码器 + 电机驱动 + STM32” 实现闭环转速控制 —— 编码器实时反馈电机转速(通过定时器中断计数),STM32 根据目标转速调整 PWM 占空比,确保电机稳定运行。
4.1 硬件原理
4.1.1 核心组件
|
组件 |
功能 |
选型建议 |
|
直流电机 |
提供动力 |
12V 直流减速电机(带减速箱,扭矩大,适合小车) |
|
编码器 |
实时反馈电机转速(增量式) |
600 线增量式编码器(每转输出 600 个脉冲,精度适中) |
|
电机驱动模块 |
接收 STM32 PWM 信号,驱动电机运转(放大电流) |
L298N(经典驱动,支持 12V 电机,电流 2A)或 TB6612FNG(小型化,电流 1.2A) |
本文选用 “12V 直流减速电机(带 600 线编码器)+ L298N 驱动模块”。
4.1.2 编码器工作原理
增量式编码器通过两个相位差 90° 的脉冲信号(A 相、B 相)反馈电机转动信息:
- 转速计算:单位时间内的脉冲数 → 电机转速(r/min);
- 转向判断:A 相超前 B 相 90°→正转,B 相超前 A 相 90°→反转。
STM32 定时器可工作在 “编码器模式”,自动对 A/B 相脉冲计数,无需 CPU 轮询。
4.1.3 硬件接线表
1. 电机 + 编码器 → L298N 驱动模块
|
电机 / 编码器引脚 |
功能 |
L298N 引脚 |
备注 |
|
电机正极 |
电机电源正极 |
OUT1(左电机)/ OUT3(右电机) |
左电机接 OUT1/OUT2,右电机接 OUT3/OUT4 |
|
电机负极 |
电机电源负极 |
OUT2(左电机)/ OUT4(右电机) | |
|
编码器 A 相 |
脉冲 A 相 |
PA0(左电机)/ PB0(右电机) |
接 STM32 定时器编码器模式引脚(左电机用 TIM2_CH1,右电机用 TIM3_CH1) |
|
编码器 B 相 |
脉冲 B 相 |
PA1(左电机)/ PB1(右电机) |
接 STM32 定时器编码器模式引脚(左电机用 TIM2_CH2,右电机用 TIM3_CH2) |
|
编码器 VCC |
编码器电源 |
5V |
编码器需 5V 供电(L298N 5V 引脚) |
|
编码器 GND |
编码器地 |
GND |
与 L298N 共地 |
2507

被折叠的 条评论
为什么被折叠?



