STM32 开发基础知识入门2:STM32 中断应用,基于阿克曼小车编程全解析(上)

引言

在嵌入式开发领域,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 共地

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值