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

第六章 模块 5:CAN 总线通信与中断应用

6.1 CAN 总线硬件原理

6.1.2 硬件组成(STM32 + TJA1050)​

CAN 总线通信需 “STM32 内置 CAN 控制器” 与 “TJA1050 收发器” 配合:​

  • STM32 CAN 控制器:实现 CAN 协议逻辑(帧组装、校验、仲裁),输出 TTL 电平信号;​
  • TJA1050 收发器:将 STM32 的 TTL 电平转换为 CAN 总线差分电平(抗干扰),并完成总线物理层驱动。​

TJA1050 核心引脚定义表:​

TJA1050 引脚​

功能描述​

电平特性​

连接说明​

VCC​

电源输入​

5V(±0.5V)​

接外部 5V 电源(不可接 3.3V,需与 STM32 共地)​

GND​

地​

-​

与 STM32、电源共地,减少地噪声干扰​

TXD​

接收控制器发送的信号​

TTL 电平(0V/3.3V)​

接 STM32 的 CAN_RX 引脚(注意:是 “接收” 控制器信号,故与 STM32 的接收端连接)​

RXD​

向控制器发送总线信号​

TTL 电平(0V/3.3V)​

接 STM32 的 CAN_TX 引脚(向控制器 “发送” 总线数据,故与 STM32 的发送端连接)​

CAN_H​

CAN 总线高电平线​

差分电平(1.7V~3.5V)​

接 CAN 总线的 CAN_H 线,需用双绞线(与 CAN_L 配对)​

CAN_L​

CAN 总线低电平线​

差分电平(0.5V~1.5V)​

接 CAN 总线的 CAN_L 线,与 CAN_H 绞合以抗干扰​

STB​

待机模式控制​

0V = 正常模式,3.3V = 待机​

接 GND(默认正常模式,无需待机时直接接地)​

​6.1.3 硬件接线表(STM32F103C8T6 + TJA1050 + CAN 总线)​​

设备 / 模块​

引脚​

目标设备引脚​

备注​

STM32F103​

PA11(CAN_RX)​

TJA1050 TXD​

STM32 接收 TJA1050 的总线数据(TTL 电平)​

STM32F103​

PA12(CAN_TX)​

TJA1050 RXD​

STM32 向 TJA1050 发送控制数据(TTL 电平)​

STM32F103​

GND​

TJA1050 GND​

强制共地,避免电平漂移导致通信失败​

TJA1050​

VCC​

5V 电源​

需单独 5V 供电(可从 STM32 开发板 5V 引脚取电,但需注意电流≥100mA)​

TJA1050​

CAN_H​

CAN 总线 CAN_H​

总线两端需各接 120Ω 终端电阻(小车端和接收端各一个,中间节点无需接)​

TJA1050​

CAN_L​

CAN 总线 CAN_L​

与 CAN_H 用双绞线绞合(每米绞合 30~50 次),减少电磁干扰​

TJA1050​

STB​

GND​

固定为正常模式,禁止待机​

CAN 总线终端​

120Ω 电阻​

CAN_H-CAN_L​

仅在总线 “两端” 节点(如小车和上位机适配器)的 CAN_H 与 CAN_L 之间并联电阻​

6.2 CAN 控制器配置(STM32 标准库)​

STM32F103 的 CAN 控制器挂载在 APB1 总线(时钟频率 36MHz),需先配置 “工作模式、波特率、过滤器、中断” 四部分,核心是波特率计算与过滤器配置(决定能否正确接收数据)。​

6.2.1 CAN 工作模式​

工作模式​

功能描述​

应用场景​

初始化模式(INIT)​

配置 CAN 控制器参数(波特率、过滤器),此时不参与总线通信​

初始化阶段(程序启动时)​

正常模式(NORMAL)​

正常参与总线通信,支持发送 / 接收数据,参与总线仲裁(优先级判断)​

小车正常运行时(发送转速、姿态数据,接收控制指令)​

静默模式(SILENT)​

仅接收数据,不发送数据,不参与总线仲裁(用于监听总线,不干扰其他节点)​

调试阶段(仅监听总线数据,不发送指令)​

回环模式(LOOPBACK)​

发送的数据直接回收到自身接收 FIFO,不经过物理总线(用于自测试,无需外部硬件)​

软件自测试(验证 CAN 控制器是否正常,无需连接 TJA1050 和总线)​

6.2.2 波特率计算(关键参数)​

CAN 波特率由 “APB1 时钟频率”“预分频系数(BRP)”“时间段 1(BS1)”“时间段 2(BS2)” 决定,公式如下:​

波特率 = APB1 时钟频率 / [BRP × ( BS1 + BS2 + 1) ]​

  • APB1 时钟频率:STM32F103 默认 36MHz(需确认系统时钟配置,无分频时 APB1=36MHz);​
  • BRP(预分频系数):取值 1~1024,决定时钟分频后的基础周期;​
  • BS1(同步段 + 传播段 + 相位缓冲段 1):取值 1~16(对应 CAN_BS1_1tq ~ CAN_BS1_16tq);​
  • BS2(相位缓冲段 2):取值 1~8(对应 CAN_BS2_1tq ~ CAN_BS2_8tq)。​

常用波特率配置表(APB1=36MHz)​

目标波特率​

BRP(预分频)​

BS1(时间段 1)​

BS2(时间段 2)​

计算验证(36MHz / [BRP×(BS1+BS2+1)])​

配置代码(标准库)​

250kbps​

8​

CAN_BS1_13tq​

CAN_BS2_2tq​

36e6 / [8×(13+2+1)] = 36e6/(8×16)=281.25kbps?→ 修正:BRP=9​

36e6/[9×(13+2+1)]=36e6/(9×16)=250kbps​

500kbps​

4​

CAN_BS1_13tq​

CAN_BS2_2tq​

36e6/[4×(13+2+1)]=36e6/(4×16)=562.5kbps?→ 修正 BRP=5​

36e6/[5×16]=450kbps?→ 最终调整:BRP=3,BS1=CAN_BS1_11tq,BS2=CAN_BS2_4tq → 36e6/[3×(11+4+1)]=36e6/(3×16)=750kbps?→ 正确配置:BRP=6,BS1=CAN_BS1_8tq,BS2=CAN_BS2_3tq → 36e6/[6×(8+3+1)]=36e6/(6×12)=500kbps​

1Mbps​

3​

CAN_BS1_8tq​

CAN_BS2_3tq​

36e6/[3×(8+3+1)]=36e6/(3×12)=1Mbps​

CAN_InitStruct.CAN_BRP=2;CAN_InitStruct.CAN_BS1=CAN_BS1_8tq;CAN_InitStruct.CAN_BS2=CAN_BS2_3tq;​

小车场景推荐250kbps:抗干扰能力强,适合 10 米内短距离通信,波特率配置代码以 250kbps 为例。​

6.2.3 CAN 过滤器配置(关键!不配置无法接收数据)​

STM32 CAN 控制器有 14 个过滤器,用于过滤无关 CAN 帧(仅接收需要的数据),支持 “列表模式” 和 “掩码模式”,小车场景推荐32 位掩码模式(接收所有标准帧,简化调试)。​

过滤器核心参数表:​

参数​

配置值 / 方式​

作用说明​

过滤器编号​

0(CAN_FilterNumber_0)​

选择第 0 个过滤器(共 14 个,0~13,可任选)​

过滤器模式​

CAN_FilterMode_IdMask(掩码模式)​

通过 “过滤器 ID + 掩码” 决定接收范围,灵活度高​

过滤器尺度​

CAN_FilterScale_32bit(32 位)​

同时过滤 ID 和掩码(标准帧 11 位 ID,扩展帧 29 位 ID,32 位兼容两种帧)​

过滤器 ID​

0x00000000​

接收所有标准帧时设为 0(若需指定 ID,需按标准帧格式设置前 11 位)​

过滤器掩码​

0x000007FF​

标准帧掩码:前 11 位为 1(精确匹配 ID),后 21 位为 0(忽略)→ 此处设 0x000007FF 表示接收所有标准帧​

过滤器 FIFO​

CAN_FilterFIFOAssignment_0(FIFO0)​

匹配的帧存入接收 FIFO0(共两个 FIFO,0 和 1,任选)​

过滤器使能​

ENABLE​

使能过滤器(不使能则无法过滤,接收不到数据)​

6.3 软件实现(标准库,250kbps,接收中断)​

6.3.1 软件模块划分​

模块​

功能​

核心函数​

CAN 初始化​

配置工作模式、波特率、过滤器,使能接收中断​

CAN_InitConfig()、CAN_FilterConfig()、NVIC_CAN_Init()​

CAN 数据发送​

组装 CAN 数据帧(ID、数据长度、数据),发送到总线​

CAN_SendData()​

CAN 接收中断服务函数​

响应 FIFO 非空中断,读取接收数据,存入缓冲区​

USB_LP_CAN1_RX0_IRQHandler()​

CAN 数据解析​

解析接收的 CAN 数据(如电机转速指令、姿态查询指令),映射到小车控制​

CAN_ParseData()​

小车数据封装​

封装电机转速、MPU6050 姿态数据为 CAN 帧,用于发送到总线​

CAN_PackCarData()​

6.3.2 核心代码实现​

1. 宏定义与全局变量​

#include "stm32f10x.h"

#include "can.h"

#include "motor.h"

#include "mpu6050.h"

// CAN参数(250kbps,标准帧,FIFO0接收中断)

#define CANx CAN1

#define CAN_CLK RCC_APB1Periph_CAN1

#define CAN_RX_PIN GPIO_Pin_11 // PA11(CAN_RX)

#define CAN_TX_PIN GPIO_Pin_12 // PA12(CAN_TX)

#define CAN_GPIO_PORT GPIOA

#define CAN_GPIO_CLK RCC_APB2Periph_GPIOA

// CAN接收缓冲区(存储接收的8字节数据)

uint8_t can_rx_data[8] = {0};

uint8_t can_rx_flag = 0; // 接收完成标志(1:有新数据)

// CAN发送数据结构体(小车状态:电机转速+MPU6050姿态)

typedef struct

{

int16_t left_motor_speed; // 左电机转速(r/min,16位)

int16_t right_motor_speed;// 右电机转速(r/min,16位)

float accel_x; // 加速度X轴(g,32位float)

float gyro_z; // 角速度Z轴(°/s,32位float)

} CAN_CarState;

CAN_CarState can_car_state; // 小车状态变量

2. CAN GPIO 与控制器初始化​

// CAN GPIO初始化(PA11=CAN_RX,PA12=CAN_TX)

void CAN_GPIO_Init(void)

{

GPIO_InitTypeDef GPIO_InitStruct;

// 使能GPIOA和CAN时钟

RCC_APB2PeriphClockCmd(CAN_GPIO_CLK, ENABLE);

RCC_APB1PeriphClockCmd(CAN_CLK, ENABLE);

// 配置CAN_RX(PA11)为浮空输入(接收TJA1050数据)

GPIO_InitStruct.GPIO_Pin = CAN_RX_PIN;

GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入(避免悬空干扰)

GPIO_Init(CAN_GPIO_PORT, &GPIO_InitStruct);

// 配置CAN_TX(PA12)为复用推挽输出(发送数据到TJA1050)

GPIO_InitStruct.GPIO_Pin = CAN_TX_PIN;

GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽

GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_Init(CAN_GPIO_PORT, &GPIO_InitStruct);

}

// CAN控制器初始化(250kbps,正常模式)

void CAN_InitConfig(void)

{

CAN_InitTypeDef CAN_InitStruct;

CAN_GPIO_Init(); // 先初始化GPIO

// 1. 进入初始化模式(配置参数前必须进入INIT模式)

CAN_InitStruct.CAN_Mode = CAN_Mode_Normal; // 正常模式(可发送/接收)

// CAN_InitStruct.CAN_Mode = CAN_Mode_LoopBack; // 回环模式(自测试用)

// 2. 配置波特率:250kbps(APB1=36MHz)

CAN_InitStruct.CAN_SJW = CAN_SJW_1tq; // 同步跳转宽度:1个时间量子

CAN_InitStruct.CAN_BS1 = CAN_BS1_13tq; // 时间段1:13个时间量子

CAN_InitStruct.CAN_BS2 = CAN_BS2_2tq; // 时间段2:2个时间量子

CAN_InitStruct.CAN_Prescaler = 9; // 预分频系数:9(36MHz / (9*(13+2+1)) = 250kbps)

// 3. 其他配置(默认)

CAN_InitStruct.CAN_TTCM = DISABLE; // 禁止时间触发通信模式

CAN_InitStruct.CAN_ABOM = ENABLE; // 自动离线管理(异常时自动恢复)

CAN_InitStruct.CAN_AWUM = DISABLE; // 禁止自动唤醒(需手动唤醒)

CAN_InitStruct.CAN_NART = DISABLE; // 使能自动重发(发送失败时重发)

CAN_InitStruct.CAN_RFLM = DISABLE; // 禁止接收FIFO锁定(满时覆盖旧数据)

CAN_InitStruct.CAN_TXFP = DISABLE; // 禁止发送优先级由ID决定(按发送顺序)

// 4. 初始化CAN控制器

while(CAN_Init(CANx, &CAN_InitStruct) == ERROR); // 等待初始化成功

}

// CAN过滤器配置(32位掩码模式,接收所有标准帧)

void CAN_FilterConfig(void)

{

CAN_FilterInitTypeDef CAN_FilterInitStruct;

// 1. 使能过滤器(必须先使能才能配置)

CAN_FilterInitStruct.CAN_FilterNumber = 0; // 选择第0个过滤器

CAN_FilterInitStruct.CAN_FilterMode = CAN_FilterMode_IdMask; // 掩码模式

CAN_FilterInitStruct.CAN_FilterScale = CAN_FilterScale_32bit; // 32位尺度

// 2. 配置过滤器ID和掩码(接收所有标准帧)

CAN_FilterInitStruct.CAN_FilterIdHigh = 0x0000; // 32位ID高16位(标准帧ID前11位为0)

CAN_FilterInitStruct.CAN_FilterIdLow = 0x0000; // 32位ID低16位

CAN_FilterInitStruct.CAN_FilterMaskIdHigh = 0x0000; // 32位掩码高16位

CAN_FilterInitStruct.CAN_FilterMaskIdLow = 0x07FF; // 32位掩码低16位(前11位为1,接收所有标准帧)

// 3. 配置FIFO和使能

CAN_FilterInitStruct.CAN_FilterFIFOAssignment = 0; // 匹配帧存入FIFO0

CAN_FilterInitStruct.CAN_FilterActivation = ENABLE; // 使能过滤器

CAN_FilterInitStruct.CAN_FilterBank = 0; // 过滤器组(F103只有1组,固定为0)

// 4. 初始化过滤器

CAN_FilterInit(&CAN_FilterInitStruct);

}

// CAN接收中断NVIC配置(FIFO0非空中断)

void NVIC_CAN_Init(void)

{

NVIC_InitTypeDef NVIC_InitStruct;

// 配置CAN接收中断通道(USB_LP_CAN1_RX0_IRQn:FIFO0非空中断)

NVIC_InitStruct.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;

NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级2(按之前规划)

NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; // 子优先级1

NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;

NVIC_Init(&NVIC_InitStruct);

// 使能CAN FIFO0非空中断(接收到数据时触发)

CAN_ITConfig(CANx, CAN_IT_FMP0, ENABLE);

}

// CAN初始化总函数(调用一次即可)

void CAN_AllInit(void)

{

CAN_InitConfig();

CAN_FilterConfig();

NVIC_CAN_Init();

}

3. CAN 数据发送(小车状态上传)​

将电机转速、MPU6050 姿态数据封装为 CAN 帧(标准帧 ID=0x001,数据长度 8 字节),发送到总线:​

// 封装小车状态为CAN发送数据(8字节,适配CAN数据帧最大长度)

void CAN_PackCarData(void)

{

// 1. 填充小车状态数据(从全局变量读取)

can_car_state.left_motor_speed = motor_speed_left;

can_car_state.right_motor_speed = motor_speed_right;

can_car_state.accel_x = mpu6050_data.accel_x_g;

can_car_state.gyro_z = mpu6050_data.gyro_z_dps;

// 2. 将16位/32位数据拆分为8位字节(小端模式,兼容大多数接收端)

uint8_t send_buf[8] = {0};

// 左电机转速(int16_t,2字节)

send_buf[0] = (uint8_t)(can_car_state.left_motor_speed & 0xFF); // 低8位

send_buf[1] = (uint8_t)((can_car_state.left_motor_speed >> 8) & 0xFF); // 高8位

// 右电机转速(int16_t,2字节)

send_buf[2] = (uint8_t)(can_car_state.right_motor_speed & 0xFF);

send_buf[3] = (uint8_t)((can_car_state.right_motor_speed >> 8) & 0xFF);

// 加速度X(float,4字节)

memcpy(&send_buf[4], &can_car_state.accel_x, 4); // 直接内存拷贝(float占4字节)

// 角速度Z(float,4字节)→ 此处仅用8字节,故暂存前4字节,实际需扩展为12字节(分两帧发送)

// 注:CAN数据帧最大8字节,若数据超8字节需分帧,此处简化为只发加速度X

}

// CAN发送数据帧(标准帧,ID=0x001,数据长度=8字节)

uint8_t CAN_SendData(uint32_t id, uint8_t *data, uint8_t len)

{

CAN_TxHeaderTypeDef CAN_TxHeader; // 发送帧头

uint8_t tx_mailbox; // 发送邮箱(0~2,CAN有3个发送邮箱)

uint32_t timeout = 1000; // 超时时间(防止死等)

// 1. 配置发送帧头(标准帧)

CAN_TxHeader.StdId = id; // 标准帧ID(0x001~0x7FF)

CAN_TxHeader.ExtId = 0x00; // 扩展帧ID(不用,设为0)

CAN_TxHeader.RTR = CAN_RTR_Data; // 数据帧(非远程帧)

CAN_TxHeader.IDE = CAN_ID_STD; // 标准帧

CAN_TxHeader.DLC = len; // 数据长度(1~8字节)

CAN_TxHeader.TransmitGlobalTime = DISABLE; // 不包含时间戳

// 2. 等待发送邮箱空闲,并发送数据

while(CAN_GetTxMailBoxFreeLevel(CANx) == 0 && timeout--);

if(timeout == 0)

return 1; // 超时,发送失败

// 3. 发送数据(标准库函数:CAN_AddTxMessage)

CAN_AddTxMessage(CANx, &CAN_TxHeader, data, &tx_mailbox);

// 4. 等待发送完成(可选,非必需,因CAN支持自动重发)

timeout = 1000;

while(CAN_GetFlagStatus(CANx, CAN_FLAG_TXOK0 + tx_mailbox) == RESET && timeout--);

if(timeout == 0)

return 2; // 发送未完成,可能总线错误

return 0; // 发送成功

}

// 小车状态上传函数(每500ms调用一次)

void CAN_UploadCarState(void)

{

uint8_t send_buf[8];

CAN_PackCarData(); // 封装数据

// 发送数据帧(ID=0x001,数据长度8字节)

uint8_t ret = CAN_SendData(0x001, send_buf, 8);

if(ret == 0)

printf("CAN发送成功:左速=%d,右速=%d,accel_x=%.2fg\r\n",

motor_speed_left, motor_speed_right, mpu6050_data.accel_x_g);

else if(ret == 1)

printf("CAN发送超时(邮箱满)\r\n");

else

printf("CAN发送失败(总线错误)\r\n");

}

4. CAN 接收中断服务函数(处理控制指令)​

接收到 CAN 指令(如 ID=0x002 的电机转速指令)后,解析数据并控制小车:​

// CAN FIFO0非空中断服务函数(接收到数据时触发)

void USB_LP_CAN1_RX0_IRQHandler(void)

{

CAN_RxHeaderTypeDef CAN_RxHeader; // 接收帧头

uint8_t rx_len; // 接收数据长度

// 检查FIFO0是否有数据(FMP0:FIFO0消息 pending 数量)

if(CAN_GetITStatus(CANx, CAN_IT_FMP0) != RESET)

{

// 1. 读取接收帧头和数据

CAN_GetRxMessage(CANx, CAN_FIFO0, &CAN_RxHeader, can_rx_data, &rx_len);

// 2. 根据帧ID判断指令类型(仅处理ID=0x002的电机控制指令)

if(CAN_RxHeader.StdId == 0x002 && rx_len == 4)

{

// 解析电机控制指令(4字节:左电机转速2字节,右电机转速2字节)

int16_t cmd_left_speed = (int16_t)((can_rx_data[1] << 8) | can_rx_data[0]); // 高8位+低8位

int16_t cmd_right_speed = (int16_t)((can_rx_data[3] << 8) | can_rx_data[2]);

// 更新目标转速(控制电机)

target_speed_left = cmd_left_speed;

target_speed_right = cmd_right_speed;

printf("CAN接收电机指令:左速=%d,右速=%d\r\n", cmd_left_speed, cmd_right_speed);

}

// 处理舵机控制指令(ID=0x003,数据长度2字节)

else if(CAN_RxHeader.StdId == 0x003 && rx_len == 2)

{

uint16_t cmd_servo_angle = (uint16_t)((can_rx_data[1] << 8) | can_rx_data[0]);

Servo_SetAngle(cmd_servo_angle);

printf("CAN接收舵机指令:角度=%d°\r\n", cmd_servo_angle);

}

// 3. 置位接收标志(主程序可查询)

can_rx_flag = 1;

// 4. 释放FIFO0(读取后必须释放,否则无法接收下一帧)

CAN_FIFORelease(CANx, CAN_FIFO0);

}

}

5. 主程序整合(CAN + 小车控制)​

将 CAN 模块与电机、PS2、MPU6050 整合,实现 “手柄控制 + CAN 上传状态 + CAN 接收指令”:​

#include "delay.h"

#include "usart.h"

#include "ps2.h"

int main(void)

{

// 1. 基础初始化

delay_init();

USART1_Init(115200);

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

// 2. 外设初始化(按依赖顺序)

MPU6050_Init(); // MPU6050(姿态)

Motor_GPIO_Init(); // 电机GPIO

TIM2_Encoder_Init(); // 电机编码器

TIM3_Encoder_Init();

NVIC_Encoder_Init();

TIM4_PWM_Init(); // 电机PWM

Motor_PID_Init(); // 电机PID

TIM1_PWM_Init(); // 舵机

PS2_CS_Init(); // PS2手柄

SPI2_Init();

NVIC_SPI2_Init();

CAN_AllInit(); // CAN总线

printf("阿克曼小车全功能初始化完成!\r\n");

printf("功能:1.PS2手柄控制 2.CAN上传状态 3.CAN接收指令\r\n");

ps2_parse_flag = 1;

uint32_t can_upload_tick = 0; // CAN上传定时器(500ms一次)

while(1)

{

// 3. PS2手柄控制(每10ms接收一次)

PS2_StartReceive();

if(ps2_parse_flag == 1)

{

PS2_ParseData();

PS2_MapToCarCtrl();

}

delay_ms(10);

// 4. CAN状态上传(每500ms一次,避免总线拥堵)

if(GetTickCount() - can_upload_tick > 500)

{

CAN_UploadCarState();

can_upload_tick = GetTickCount();

}

// 5. 处理CAN接收数据(主程序查询,可选)

if(can_rx_flag == 1)

{

can_rx_flag = 0; // 清除标志

// 此处可添加额外数据处理逻辑(如打印接收数据)

}

}

}

注:GetTickCount()需自行实现(基于 SysTick 定时器,记录系统运行毫秒数),代码如下:​

// SysTick定时器初始化(1ms中断)

void SysTick_Init(void)

{

SysTick_Config(SystemCoreClock / 1000); // 72MHz/1000=72000,1ms中断一次

}

volatile uint32_t tick_count = 0;

// SysTick中断服务函数(1ms加1)

void SysTick_Handler(void)

{

tick_count++;

}

// 获取系统运行毫秒数

uint32_t GetTickCount(void)

{

return tick_count;

}

6.3.3 CAN 通信测试(硬件 + 软件)​

需准备USB-CAN 适配器(如周立功 USBCAN-II)和CAN 分析仪软件(如 CANoe、USBCAN-III 软件),测试步骤如下:​

测试步骤​

操作内容​

验证点​

1. 硬件连接​

1. 小车 TJA1050 的 CAN_H/CAN_L 接 USB-CAN 适配器的 CAN_H/CAN_L;2. 小车和适配器均接 120Ω 终端电阻;3. 适配器 USB 接电脑,小车上电。​

1. TJA1050 电源灯亮(VCC 正常);2. 适配器 CAN 灯闪烁(总线有信号)。​

2. 软件配置​

1. 打开 CAN 分析仪软件,选择 “USB-CAN 设备”;2. 配置波特率 = 250kbps,数据位 = 8,校验 = 无,停止位 = 1;3. 启动 CAN 总线(“开始接收”)。​

软件显示 “总线已连接”,无错误帧(Error Frame)。​

3. 发送测试​

1. 在软件中发送 “标准帧 ID=0x002,数据 = 0x32,0x00,0x32,0x00”(左速 = 50,右速 = 50);2. 观察小车动作。​

1. 小车电机前进(转速 50r/min);2. 软件显示 “发送成功”,无重发。​

4. 接收测试​

1. 用 PS2 手柄控制小车(前进 / 转向);2. 在软件中观察接收数据(ID=0x001)。​

1. 软件接收区显示 8 字节数据,解析后与小车实际转速 / 姿态一致;2. 每 500ms 接收一帧。​

6.3.4 常见问题与解决方案​

问题现象​

可能原因​

解决方案​

CAN 总线无数据(软件显示无接收)​

1. 终端电阻未接(总线阻抗不匹配,信号反射)2. CAN_H/CAN_L 接反(差分信号错误)3. TJA1050 供电不足(5V 电压 < 4.5V)​

1. 在小车和适配器两端各接 120Ω 电阻(CAN_H 与 CAN_L 之间)2. 交换 CAN_H/CAN_L 接线3. 测量 TJA1050 VCC 电压,确保≥4.5V(更换电源)​

发送失败(返回超时)​

1. 波特率不匹配(小车 250kbps,软件 500kbps)2. 过滤器配置错误(未接收目标 ID)3. 总线忙(多个节点同时发送)​

1. 统一所有节点波特率(需重新编译小车程序)2. 检查过滤器 ID 和掩码(确保接收目标 ID)3. 增加发送间隔(如从 100ms 改为 500ms)​

接收数据乱码(解析值错误)​

1. 数据字节序错误(小端 / 大端不匹配)2. 数据长度错误(发送 8 字节,接收 4 字节)3. 浮点数据解析错误(字节数不足)​

1. 统一字节序(小车用小端,软件也设为小端)2. 确保发送和接收的数据长度一致(如均为 8 字节)3. 浮点数据需 4 字节完整接收(分帧时标记帧序号)​

CAN 控制器初始化失败​

1. APB1 时钟频率错误(非 36MHz,导致波特率计算错误)2. GPIO 复用配置错误(PA12 未设为复用推挽)​

1. 检查系统时钟配置(确保 APB1 无分频,频率 = 36MHz)2. 确认 CAN_TX 引脚(PA12)模式为GPIO_Mode_AF_PP​

第七章 阿克曼小车系统整合与优化​

前面章节已实现单个模块的功能,本章将整合所有模块(MPU6050 + 舵机 + 电机 + PS2+CAN),并针对 “稳定性、实时性、抗干扰” 进行优化,最终实现工业级小车控制系统。​

7.1 系统架构与模块交互​

7.1.1 硬件架构图(分层设计)​

7.1.2 模块交互时序(中断优先级主导)​​

中断源​

优先级​

触发周期​

交互模块​

编码器定时器更新中断​

0(最高)​

100ms​

电机模块(计算转速→PID 调节→PWM 输出)​

MPU6050 数据就绪中断​

1​

10ms​

姿态模块(读取姿态→滤波→CAN 上传)​

PS2 SPI 接收中断​

2​

10ms​

控制模块(接收摇杆指令→映射为电机 / 舵机目标值)​

CAN 接收中断​

2​

不定时​

通信模块(接收外部指令→覆盖 PS2 目标值,优先级低于 PS2)​

舵机定时器更新中断​

3​

20ms​

执行模块(平滑调整舵机角度→PWM 输出)​

规则:高优先级中断可打断低优先级中断(如编码器中断可打断舵机中断),同优先级按触发顺序执行(PS2 和 CAN 同优先级,先触发先执行)。​

7.2 系统优化方案​

7.2.1 实时性优化(减少 CPU 负载)​

  • 中断服务函数轻量化:仅做 “数据读取 + 标志置位”,复杂处理(如滤波、PID)放到主程序循环(例:MPU6050 中断仅读取数据,滤波在主程序执行);​
  • 定时器合并:用一个定时器(如 TIM6)管理多个周期性任务(如 PS2 接收 10ms、CAN 上传 500ms),减少定时器数量;​
  • 禁用无用中断:如未使用 CAN 发送中断,需关闭CAN_IT_TX_MAILBOX_EMPTY,避免空中断占用 CPU。​

7.2.2 稳定性优化(抗干扰)​

硬件抗干扰:​

  • 电源端并联电容(100uF 电解电容 + 0.1uF 陶瓷电容),滤除电源噪声;​
  • 编码器 / 舵机信号线套屏蔽层(一端接地),减少电磁干扰;​
  • 电机电源线与信号线分开布线(避免电机电流干扰信号)。​

软件抗干扰:​

  • 数据校验:CAN 发送时添加校验和(如最后 1 字节为前 7 字节异或),接收时验证;​
  • 超时处理:PS2 手柄 3 秒无数据时,自动停止电机(避免失控);​
  • 姿态补偿:用 MPU6050 的 Z 轴角速度修正舵机角度(小车跑偏时自动回正)。​

7.2.3 功能扩展(基于 CAN 总线)​

  • 多小车协同:多个小车接入同一 CAN 总线,通过不同 ID 区分(小车 1=0x001,小车 2=0x002),上位机发送广播指令控制所有小车;​
  • 远程监控:CAN 总线连接 WiFi 模块(如 ESP8266-CAN),将小车状态上传到云端(需适配 MQTT 协议);​
  • 传感器扩展:通过 CAN 总线添加超声波传感器(测距)、红外传感器(避障),无需修改小车主程序(仅需在接收端解析新 ID 指令)。​

7.3 完整系统测试用例​

测试用例​

测试步骤​

预期结果​

1. 上电初始化​

1. 小车上电;2. 打开串口助手(115200 波特率)。​

1. 串口打印 “全功能初始化完成”;2. 舵机自动回中位(90°),电机无动作。​

2. PS2 手柄控制​

1. 左摇杆向前推(X=1023);2. 右摇杆向左推(X=0);3. 按 START 键。​

1. 电机前进(转速 50r/min);2. 舵机左转到 45°;3. 电机停止,舵机回中位。​

3. CAN 指令控制​

1. 软件发送 CAN 指令(ID=0x002,数据 = 0x1E,0x00,0x1E,0x00);2. 发送 ID=0x003,数据 = 0x5A,0x00。​

1. 电机前进(转速 30r/min);2. 舵机转到 90°(0x5A=90)。​

4. 姿态上传​

1. 手动倾斜小车;2. 观察 CAN 软件接收数据(ID=0x001)。​

1. CAN 接收数据中的 accel_x 值随倾斜角度变化;2. 无丢帧(每 500ms 一帧)。​

5. 抗干扰测试​

1. 电机满速运行(50r/min);2. 观察编码器转速与目标值偏差。​

1. 转速偏差≤5%(PID 稳定);2. 无明显抖动(滤波生效)。​

​7.4 常见系统级问题与解决方案​​

问题现象​

可能原因​

解决方案​

小车失控(电机乱转)​

1. PS2 手柄未配对(无指令,目标值异常)2. 编码器接线错误(转速计算错误)3. PID 参数过大(震荡)​

1. 重新配对 PS2 手柄,3 秒无数据自动停车2. 检查编码器 A/B 相接线(PA0/PA1)3. 减小 PID 的 Kp(如从 2.0 改为 1.5)​

CAN 上传数据频率不稳定​

1. 主程序循环阻塞(如 delay_ms (100) 过长)2. SysTick 定时器被中断打断(未优先级保护)​

1. 用定时器中断替代 delay_ms(非阻塞延时)2. 配置 SysTick 为最高优先级(抢占优先级 0)​

舵机与电机动作不同步​

1. 舵机调整步长过大(SERVO_ADJ_STEP=5)2. 电机 PID 响应过快(Kp 过大)​

1. 减小舵机步长(设为 1°)2. 降低电机 PID 响应速度(Kp 减小,Ki 增大)​

​第八章 总结与扩展学习​

8.1 学习总结​

本文通过阿克曼小车实战,覆盖 STM32 中断核心应用场景,关键知识点总结如下:​

  • 中断本质:通过优先级管理实现 “实时响应”,避免 CPU 轮询(如编码器 100ms 中断确保转速反馈及时);​
  • 外设共性:无论 I2C(MPU6050)、SPI(PS2)、CAN,均需 “硬件接线→时钟使能→外设配置→中断使能→数据处理” 五步流程;​
  • 闭环控制:电机转速控制需 “编码器反馈(感知)+PID 调节(控制)+PWM 输出(执行)”,缺一不可;​
  • 抗干扰设计:硬件(屏蔽层、电容)+ 软件(校验、超时)结合,是工业级系统的关键。​

8.2 扩展学习方向​

  • 操作系统移植:将裸机程序移植到 RT-Thread/FreeRTOS,用任务管理替代中断优先级(更适合多任务场景);​
  • 高级姿态控制:用卡尔曼滤波替代滑动平均,结合 PID 实现小车自平衡(需增加陀螺仪权重);​
  • 视觉融合:添加摄像头(如 OV7725),通过 OpenCV 识别路径,自动控制舵机(视觉导航);​
  • CAN FD 升级:用 CAN FD(灵活数据速率)替代传统 CAN,支持 64 字节数据帧(无需分帧,适合多传感器数据上传)。​

8.3 资料推荐​

  • 手册:《STM32F103xx 参考手册》(重点看 CAN、TIM、SPI 章节)、《TJA1050 数据手册》;​
  • 工具:Keil MDK(编程)、CANoe(CAN 调试)、串口助手(调试);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值