第六章 模块 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 调试)、串口助手(调试);
2491

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



