第四章 模块 3:双编码器输入电机 PWM 转速控制与中断
4.1.3 硬件接线表
2. L298N → STM32F103C8T6
L298N 引脚
|
L298N 引脚 |
功能 |
STM32 引脚 |
备注 |
| IN1 | 左电机转向控制 1 | PB12 | GPIO 输出(高 / 低电平控制转向) |
|
IN2 |
左电机转向控制 2 |
PB13 |
GPIO 输出(与 IN1 配合控制转向,如 IN1=1、IN2=0→正转;IN1=0、IN2=1→反转) |
|
ENA |
左电机 PWM 使能 |
PB6 |
接 TIM4_CH1(通用定时器 4 通道 1),输出 PWM 控制左电机转速(ENA 低电平禁用) |
|
IN3 |
右电机转向控制 1 |
PB14 |
GPIO 输出(与 IN4 配合控制转向,逻辑同左电机) |
|
IN4 |
右电机转向控制 2 |
PB15 |
GPIO 输出 |
|
ENB |
右电机 PWM 使能 |
PB7 |
接 TIM4_CH2(通用定时器 4 通道 2),输出 PWM 控制右电机转速 |
|
12V |
电机电源输入 |
12V 电池 |
需外接 12V 电池,为电机提供动力(L298N 电机电源接口) |
|
5V |
逻辑电源输出 |
无需接 |
L298N 可从 12V 电源降压输出 5V,可为编码器供电(已在 “电机 + 编码器→L298N” 接线中使用) |
|
GND |
地 |
GND |
必须与 STM32、电池共地,否则驱动紊乱 |
4.1.4 电机驱动原理(L298N)
L298N 基于 “H 桥电路” 实现电机正反转与转速控制,核心逻辑通过 “转向控制引脚(IN1/IN2/IN3/IN4)” 和 “PWM 使能引脚(ENA/ENB)” 配合实现:
电机转向控制逻辑表(左电机为例)
|
IN1 电平 |
IN2 电平 |
电机状态 |
原理说明 |
|
1 |
0 |
正转(前进) |
H 桥左侧上管、右侧下管导通,电流从 OUT1 流入电机,OUT2 流出 |
|
0 |
1 |
反转(后退) |
H 桥左侧下管、右侧上管导通,电流从 OUT2 流入电机,OUT1 流出 |
|
1 |
1 |
刹车(急停) |
H 桥同侧上下管导通,电机两端短接,利用反电动势快速制动 |
|
0 |
0 |
停止(自由转) |
H 桥所有管子截止,电机无电流,可自由转动(无制动) |
右电机转向控制逻辑与左电机一致,仅需替换为 IN3(对应 IN1)、IN4(对应 IN2)。
电机转速控制原理
L298N 的 ENA/ENB 为 “PWM 使能端”,仅当 ENA/ENB 为高电平时,电机才能响应转向控制;PWM 占空比直接决定电机转速:
- 占空比 0% → ENA/ENB 持续低电平 → 电机停止;
- 占空比 10%-90% → 电机转速随占空比增大而提高(占空比过高易导致电机发热);
- 占空比 100% → ENA/ENB 持续高电平 → 电机满速运行。
4.2 编码器模式配置(STM32 定时器)
STM32 通用定时器(如 TIM2、TIM3)支持 “编码器模式”,可自动采集编码器 A/B 相脉冲,无需 CPU 轮询。核心配置要点包括:
- GPIO 配置:编码器 A/B 相引脚设为 “浮空输入”(避免外部电平干扰);
- 定时器模式:选择 “编码器模式 3”(A 相和 B 相的上升沿、下降沿均计数,实现 4 倍频,提高精度);
- 计数器配置:计数器设为 16 位自动重装(最大值 65535),支持正反转计数(正转递增,反转递减);
- 中断配置:使能定时器 “更新中断”,定期(如 100ms)读取计数器值计算转速。
编码器模式核心参数表(以左电机 TIM2 为例)
|
参数 |
配置值 / 方式 |
作用说明 |
|
定时器 |
TIM2(挂载 APB1 总线,时钟频率 36MHz) |
左电机编码器 A 相(PA0=TIM2_CH1)、B 相(PA1=TIM2_CH2) |
|
GPIO 模式 |
浮空输入(GPIO_Mode_IN_FLOATING) |
编码器脉冲为外部信号,浮空输入可准确采集高低电平变化 |
|
编码器模式 |
TIM_EncoderMode_TI12(模式 3) |
A 相(TI1)和 B 相(TI2)的上升沿、下降沿均触发计数(4 倍频) |
|
滤波配置 |
TIM_ICFilter_6(6 个时钟周期滤波) |
滤除编码器脉冲的高频干扰(如机械抖动产生的杂波) |
|
计数器方向 |
自动判断(正转递增,反转递减) |
通过 A/B 相相位差自动识别转向,无需额外 GPIO 判断 |
|
更新中断周期 |
100ms(每 100ms 计算一次转速) |
周期过短易导致计算频繁(CPU 负载高),过长易导致转速反馈滞后 |
|
四倍频系数 |
4(模式 3 实现) |
600 线编码器每转产生 600×4=2400 个脉冲(提高转速计算精度) |
4.3 软件实现(标准库)
4.3.1 软件模块划分
|
模块 |
功能 |
核心函数 |
|
GPIO 初始化 |
配置电机转向控制引脚(IN1~IN4)为推挽输出 |
Motor_GPIO_Init() |
|
编码器定时器初始化 |
配置 TIM2(左电机)、TIM3(右电机)为编码器模式,使能更新中断 |
TIM2_Encoder_Init()、TIM3_Encoder_Init()、NVIC_Encoder_Init() |
|
PWM 定时器初始化 |
配置 TIM4(ENA/ENB)为 PWM 模式,输出转速控制信号 |
TIM4_PWM_Init() |
|
转速计算 |
每 100ms 读取编码器计数,转换为电机实际转速(r/min) |
Motor_CalcSpeed() |
|
PID 控制 |
基于 “目标转速 - 实际转速” 的误差,计算 PWM 占空比,实现闭环控制 |
PID_Init()、PID_Calculate() |
|
电机控制 |
控制电机转向与使能,更新 PWM 占空比 |
Motor_SetDir()、Motor_SetPWM() |
|
中断服务函数 |
响应编码器定时器更新中断,触发转速计算与 PID 调节 |
TIM2_IRQHandler()、TIM3_IRQHandler() |
4.3.2 核心代码实现
1. 宏定义与全局变量
#include "stm32f10x.h"
#include "motor.h"
#include "pid.h"
// 电机转向控制引脚定义
#define IN1_PIN GPIO_Pin_12
#define IN2_PIN GPIO_Pin_13
#define IN3_PIN GPIO_Pin_14
#define IN4_PIN GPIO_Pin_15
#define MOTOR_GPIO_PORT GPIOB
#define MOTOR_GPIO_CLK RCC_APB2Periph_GPIOB
// 编码器定时器参数(左电机TIM2,右电机TIM3)
#define ENCODER_TIM_LEFT TIM2
#define ENCODER_TIM_RIGHT TIM3
#define ENCODER_CLK_LEFT RCC_APB1Periph_TIM2
#define ENCODER_CLK_RIGHT RCC_APB1Periph_TIM3
#define ENCODER_PPR 600 // 编码器线数(每转脉冲数)
#define ENCODER_4X 4 // 4倍频系数(模式3)
#define ENCODER_SAMPLE_MS 100 // 转速采样周期(100ms)
#define ENCODER_SAMPLE_MIN (ENCODER_SAMPLE_MS / 60000.0f) // 采样周期(分钟):100ms=100/60000 min
// PWM定时器参数(TIM4,ENA=PB6=TIM4_CH1,ENB=PB7=TIM4_CH2)
#define PWM_TIM TIM4
#define PWM_CLK RCC_APB1Periph_TIM4
#define PWM_PSC 71 // 预分频系数:72MHz/(71+1)=1MHz
#define PWM_ARR 999 // 自动重装值:(999+1)/1MHz=1ms(PWM周期1ms,频率1kHz)
#define PWM_MAX 900 // PWM最大占空比(90%,避免电机过热)
#define PWM_MIN 100 // PWM最小占空比(10%,避免电机堵转)
// 电机转速与转向变量
int16_t motor_speed_left = 0; // 左电机实际转速(r/min,正为前进,负为后退)
int16_t motor_speed_right = 0; // 右电机实际转速(r/min)
int16_t target_speed_left = 0; // 左电机目标转速(r/min)
int16_t target_speed_right = 0; // 右电机目标转速(r/min)
typedef enum {MOTOR_STOP, MOTOR_FORWARD, MOTOR_BACKWARD} Motor_Dir; // 电机转向枚举
Motor_Dir motor_dir_left = MOTOR_STOP; // 左电机转向
Motor_Dir motor_dir_right = MOTOR_STOP; // 右电机转向
// PID实例(左电机、右电机各一个)
PID_HandleTypeDef pid_left;
PID_HandleTypeDef pid_right;
2. GPIO 初始化(电机转向控制)
// 电机转向控制GPIO初始化(IN1~IN4)
void Motor_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
// 使能GPIOB时钟
RCC_APB2PeriphClockCmd(MOTOR_GPIO_CLK, ENABLE);
// 配置IN1~IN4为推挽输出
GPIO_InitStruct.GPIO_Pin = IN1_PIN | IN2_PIN | IN3_PIN | IN4_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(MOTOR_GPIO_PORT, &GPIO_InitStruct);
// 初始状态:电机停止(IN1~IN4均为低电平)
GPIO_ResetBits(MOTOR_GPIO_PORT, IN1_PIN | IN2_PIN | IN3_PIN | IN4_PIN);
}
// 设置电机转向(左电机)
void Motor_SetDir_Left(Motor_Dir dir)
{
motor_dir_left = dir;
switch(dir)
{
case MOTOR_STOP:
GPIO_ResetBits(MOTOR_GPIO_PORT, IN1_PIN);
GPIO_ResetBits(MOTOR_GPIO_PORT, IN2_PIN);
break;
case MOTOR_FORWARD:
GPIO_SetBits(MOTOR_GPIO_PORT, IN1_PIN);
GPIO_ResetBits(MOTOR_GPIO_PORT, IN2_PIN);
break;
case MOTOR_BACKWARD:
GPIO_ResetBits(MOTOR_GPIO_PORT, IN1_PIN);
GPIO_SetBits(MOTOR_GPIO_PORT, IN2_PIN);
break;
default:
break;
}
}
// 设置电机转向(右电机)
void Motor_SetDir_Right(Motor_Dir dir)
{
motor_dir_right = dir;
switch(dir)
{
case MOTOR_STOP:
GPIO_ResetBits(MOTOR_GPIO_PORT, IN3_PIN);
GPIO_ResetBits(MOTOR_GPIO_PORT, IN4_PIN);
break;
case MOTOR_FORWARD:
GPIO_SetBits(MOTOR_GPIO_PORT, IN3_PIN);
GPIO_ResetBits(MOTOR_GPIO_PORT, IN4_PIN);
break;
case MOTOR_BACKWARD:
GPIO_ResetBits(MOTOR_GPIO_PORT, IN3_PIN);
GPIO_SetBits(MOTOR_GPIO_PORT, IN4_PIN);
break;
default:
break;
}
}
3. 编码器定时器初始化(TIM2、TIM3)
// 左电机编码器定时器初始化(TIM2)
void TIM2_Encoder_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
TIM_ICInitTypeDef TIM_ICInitStruct;
GPIO_InitTypeDef GPIO_InitStruct;
// 1. 使能时钟
RCC_APB1PeriphClockCmd(ENCODER_CLK_LEFT, ENABLE); // TIM2时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA0、PA1时钟
// 2. 配置编码器A相(PA0=TIM2_CH1)、B相(PA1=TIM2_CH2)为浮空输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置定时器时基参数
TIM_TimeBaseStruct.TIM_Period = 65535; // 16位计数器,最大值65535
TIM_TimeBaseStruct.TIM_Prescaler = 0; // 无预分频(计数器频率=定时器时钟频率)
TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1; // 不分频
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up; // 初始向上计数(转向自动调整)
TIM_TimeBaseInit(ENCODER_TIM_LEFT, &TIM_TimeBaseStruct);
// 4. 配置编码器模式(模式3:TI1、TI2上升沿+下降沿计数)
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1; // 通道1(A相)
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising; // 初始上升沿(模式3会自动调整)
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI; // 直接连接到TI1
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 无分频
TIM_ICInitStruct.TIM_ICFilter = 6; // 6个时钟周期滤波
TIM_ICInit(ENCODER_TIM_LEFT, &TIM_ICInitStruct);
TIM_ICInitStruct.TIM_Channel = TIM_Channel_2; // 通道2(B相)
TIM_ICInit(ENCODER_TIM_LEFT, &TIM_ICInitStruct); // 配置与通道1一致
// 5. 使能编码器模式
TIM_EncoderInterfaceConfig(ENCODER_TIM_LEFT,
TIM_EncoderMode_TI12, // 模式3
TIM_ICPolarity_Rising,
TIM_ICPolarity_Rising);
// 6. 使能定时器更新中断(每100ms触发一次,用于计算转速)
TIM_ITConfig(ENCODER_TIM_LEFT, TIM_IT_Update, ENABLE);
// 配置更新中断周期(100ms):计数器溢出时间 = (ARR+1)*PSC/定时器时钟 = 65536*0/36MHz → 需通过预分频调整?
// 修正:通过定时器预分频实现100ms中断:PSC=3599,ARR=999 → (999+1)*(3599+1)/36MHz = 1000*3600/36e6 = 0.1s=100ms
TIM_TimeBaseStruct.TIM_Prescaler = 3599;
TIM_TimeBaseStruct.TIM_Period = 999;
TIM_TimeBaseInit(ENCODER_TIM_LEFT, &TIM_TimeBaseStruct);
// 7. 启动定时器
TIM_SetCounter(ENCODER_TIM_LEFT, 0); // 计数器清零
TIM_Cmd(ENCODER_TIM_LEFT, ENABLE);
}
// 右电机编码器定时器初始化(TIM3,与TIM2逻辑一致)
void TIM3_Encoder_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
TIM_ICInitTypeDef TIM_ICInitStruct;
GPIO_InitTypeDef GPIO_InitStruct;
// 1. 使能时钟
RCC_APB1PeriphClockCmd(ENCODER_CLK_RIGHT, ENABLE); // TIM3时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // PB0=TIM3_CH1、PB1=TIM3_CH2时钟
// 2. 配置编码器A相(PB0)、B相(PB1)为浮空输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOB, &GPIO_InitStruct);
// 3. 配置定时器时基参数(100ms更新中断)
TIM_TimeBaseStruct.TIM_Period = 999;
TIM_TimeBaseStruct.TIM_Prescaler = 3599;
TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(ENCODER_TIM_RIGHT, &TIM_TimeBaseStruct);
// 4. 配置编码器模式3
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStruct.TIM_ICFilter = 6;
TIM_ICInit(ENCODER_TIM_RIGHT, &TIM_ICInitStruct);
TIM_ICInitStruct.TIM_Channel = TIM_Channel_2;
TIM_ICInit(ENCODER_TIM_RIGHT, &TIM_ICInitStruct);
TIM_EncoderInterfaceConfig(ENCODER_TIM_RIGHT,
TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising,
TIM_ICPolarity_Rising);
// 5. 使能更新中断
TIM_ITConfig(ENCODER_TIM_RIGHT, TIM_IT_Update, ENABLE);
// 6. 启动定时器
TIM_SetCounter(ENCODER_TIM_RIGHT, 0);
TIM_Cmd(ENCODER_TIM_RIGHT, ENABLE);
}
// 编码器定时器NVIC配置(TIM2、TIM3中断)
void NVIC_Encoder_Init(void)
{
NVIC_InitTypeDef NVIC_InitStruct;
// 左电机编码器TIM2中断
NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级0(最高,按之前规划)
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
// 右电机编码器TIM3中断
NVIC_InitStruct.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; // 子优先级1(低于左电机)
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
4. PWM 定时器初始化(TIM4,ENA/ENB)
// TIM4 PWM初始化(ENA=PB6=TIM4_CH1,ENB=PB7=TIM4_CH2)
void TIM4_PWM_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
TIM_OCInitTypeDef TIM_OCInitStruct;
// 1. 使能时钟
RCC_APB1PeriphClockCmd(PWM_CLK, ENABLE); // TIM4时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // PB6、PB7时钟
// 2. 配置PB6、PB7为复用推挽输出(PWM输出)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
// 3. 配置TIM4时基参数(PWM周期1ms,频率1kHz)
TIM_TimeBaseStruct.TIM_Period = PWM_ARR; // 999
TIM_TimeBaseStruct.TIM_Prescaler = PWM_PSC; // 71
TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(PWM_TIM, &TIM_TimeBaseStruct);
// 4. 配置PWM模式(模式1:CNT < CCR时输出高电平)
// 通道1(ENA,左电机)
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStruct.TIM_Pulse = 0; // 初始占空比0%(电机停止)
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; // 高电平有效
TIM_OC1Init(PWM_TIM, &TIM_OCInitStruct);
TIM_OC1PreloadConfig(PWM_TIM, TIM_OCPreload_Enable); // 使能CCR1预装载
// 通道2(ENB,右电机)
TIM_OCInitStruct.TIM_Channel = TIM_Channel_2;
TIM_OCInit(PWM_TIM, &TIM_OCInitStruct);
TIM_OC2PreloadConfig(PWM_TIM, TIM_OCPreload_Enable); // 使能CCR2预装载
// 5. 使能ARR预装载
TIM_ARRPreloadConfig(PWM_TIM, ENABLE);
// 6. 启动定时器
TIM_Cmd(PWM_TIM, ENABLE);
}
// 设置左电机PWM占空比(0~PWM_MAX)
void Motor_SetPWM_Left(uint16_t pwm)
{
if(pwm > PWM_MAX) pwm = PWM_MAX;
if(pwm < PWM_MIN && pwm != 0) pwm = PWM_MIN; // 非停止时,PWM不低于最小值
TIM_SetCompare1(PWM_TIM, pwm);
}
// 设置右电机PWM占空比(0~PWM_MAX)
void Motor_SetPWM_Right(uint16_t pwm)
{
if(pwm > PWM_MAX) pwm = PWM_MAX;
if(pwm < PWM_MIN && pwm != 0) pwm = PWM_MIN;
TIM_SetCompare2(PWM_TIM, pwm);
}
5. PID 控制实现(闭环转速控制)
转速控制需通过 PID 算法实现 “目标转速→实际转速” 的闭环调节,避免电机负载变化导致转速漂移。此处采用 “位置式 PID”,适合慢响应系统(如小车电机)。
// PID结构体定义
typedef struct
{
float Kp; // 比例系数
float Ki; // 积分系数
float Kd; // 微分系数
float target; // 目标值(转速,r/min)
float actual; // 实际值(转速,r/min)
float error; // 当前误差(target - actual)
float error_prev; // 上一次误差
float integral; // 积分项
float integral_limit; // 积分限幅(防止积分饱和)
float output; // PID输出(PWM占空比)
float output_limit; // 输出限幅(0~PWM_MAX)
} PID_HandleTypeDef;
// PID初始化
void PID_Init(PID_HandleTypeDef *pid, float Kp, float Ki, float Kd,
float integral_limit, float output_limit)
{
pid->Kp = Kp;
pid->Ki = Ki;
pid->Kd = Kd;
pid->target = 0;
pid->actual = 0;
pid->error = 0;
pid->error_prev = 0;
pid->integral = 0;
pid->integral_limit = integral_limit;
pid->output_limit = output_limit;
pid->output = 0;
}
// PID计算(位置式)
float PID_Calculate(PID_HandleTypeDef *pid)
{
// 1. 计算当前误差
pid->error = pid->target - pid->actual;
// 2. 计算积分项(积分限幅,防止饱和)
pid->integral += pid->error;
if(pid->integral > pid->integral_limit)
pid->integral = pid->integral_limit;
else if(pid->integral < -pid->integral_limit)
pid->integral = -pid->integral_limit;
// 3. 计算PID输出(位置式公式:output = Kp*error + Ki*integral + Kd*(error - error_prev))
pid->output = pid->Kp * pid->error +
pid->Ki * pid->integral +
pid->Kd * (pid->error - pid->error_prev);
// 4. 输出限幅
if(pid->output > pid->output_limit)
pid->output = pid->output_limit;
else if(pid->output < 0)
pid->output = 0; // 输出为负时,设为0(转向由单独函数控制)
// 5. 保存当前误差为上一次误差
pid->error_prev = pid->error;
return pid->output;
}
// 电机PID参数初始化(需根据实际电机调试,此处为参考值)
void Motor_PID_Init(void)
{
// 左电机PID参数(Kp=2.0,Ki=0.1,Kd=0.05,积分限幅=500,输出限幅=PWM_MAX)
PID_Init(&pid_left, 2.0f, 0.1f, 0.05f, 500.0f, (float)PWM_MAX);
// 右电机PID参数(与左电机一致,若电机有差异可微调)
PID_Init(&pid_right, 2.0f, 0.1f, 0.05f, 500.0f, (float)PWM_MAX);
}
6. 转速计算与中断服务函数
通过编码器定时器更新中断(每 100ms)读取脉冲数,转换为实际转速,并触发 PID 调节。
// 左电机转速计算(根据编码器脉冲数)
void Motor_CalcSpeed_Left(void)
{
int16_t encoder_cnt; // 编码器计数(16位,支持正负)
float pulse_total; // 总脉冲数(含4倍频)
// 1. 读取编码器计数器值(TIM2为16位,需强制转换为int16_t处理正负)
encoder_cnt = (int16_t)TIM_GetCounter(ENCODER_TIM_LEFT);
// 2. 清零计数器,准备下一次采样
TIM_SetCounter(ENCODER_TIM_LEFT, 0);
// 3. 计算总脉冲数(4倍频)
pulse_total = (float)encoder_cnt * ENCODER_4X;
// 4. 计算实际转速(r/min):转速 = 总脉冲数 / (编码器线数 * 采样周期(min))
motor_speed_left = (int16_t)(pulse_total / (ENCODER_PPR * ENCODER_SAMPLE_MIN));
// 5. 修正转速方向(与电机转向一致:前进为正,后退为负)
if(motor_dir_left == MOTOR_BACKWARD)
motor_speed_left = -motor_speed_left;
else if(motor_dir_left == MOTOR_STOP)
motor_speed_left = 0;
}
// 右电机转速计算(与左电机一致)
void Motor_CalcSpeed_Right(void)
{
int16_t encoder_cnt;
float pulse_total;
encoder_cnt = (int16_t)TIM_GetCounter(ENCODER_TIM_RIGHT);
TIM_SetCounter(ENCODER_TIM_RIGHT, 0);
pulse_total = (float)encoder_cnt * ENCODER_4X;
motor_speed_right = (int16_t)(pulse_total / (ENCODER_PPR * ENCODER_SAMPLE_MIN));
if(motor_dir_right == MOTOR_BACKWARD)
motor_speed_right = -motor_speed_right;
else if(motor_dir_right == MOTOR_STOP)
motor_speed_right = 0;
}
// 左电机编码器TIM2中断服务函数(每100ms触发)
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(ENCODER_TIM_LEFT, TIM_IT_Update) != RESET)
{
// 1. 计算左电机实际转速
Motor_CalcSpeed_Left();
// 2. 更新PID实际值,计算输出PWM
pid_left.actual = (float)motor_speed_left;
pid_left.target = (float)target_speed_left;
uint16_t pwm_left = (uint16_t)PID_Calculate(&pid_left);
// 3. 根据目标转速设置转向,并更新PWM
if(target_speed_left > 0)
Motor_SetDir_Left(MOTOR_FORWARD);
else if(target_speed_left < 0)
{
Motor_SetDir_Left(MOTOR_BACKWARD);
pid_left.target = -pid_left.target; // 目标转速取绝对值,PID计算用正值
}
else
{
Motor_SetDir_Left(MOTOR_STOP);
pwm_left = 0;
}
Motor_SetPWM_Left(pwm_left);
// 4. 清除中断标志
TIM_ClearITPendingBit(ENCODER_TIM_LEFT, TIM_IT_Update);
}
}
// 右电机编码器TIM3中断服务函数(每100ms触发)
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(ENCODER_TIM_RIGHT, TIM_IT_Update) != RESET)
{
// 1. 计算右电机实际转速
Motor_CalcSpeed_Right();
// 2. 更新PID实际值,计算输出PWM
pid_right.actual = (float)motor_speed_right;
pid_right.target = (float)target_speed_right;
uint16_t pwm_right = (uint16_t)PID_Calculate(&pid_right);
// 3. 根据目标转速设置转向,并更新PWM
if(target_speed_right > 0)
Motor_SetDir_Right(MOTOR_FORWARD);
else if(target_speed_right < 0)
{
Motor_SetDir_Right(MOTOR_BACKWARD);
pid_right.target = -pid_right.target;
}
else
{
Motor_SetDir_Right(MOTOR_STOP);
pwm_right = 0;
}
Motor_SetPWM_Right(pwm_right);
// 4. 清除中断标志
TIM_ClearITPendingBit(ENCODER_TIM_RIGHT, TIM_IT_Update);
}
}
4.3.3 电机控制测试代码
通过串口发送指令设置目标转速(如 “LEFT=50” 表示左电机目标转速 50r/min,“RIGHT=-30” 表示右电机目标转速 - 30r/min),并打印实际转速:
#include "usart.h"
#include "string.h"
#define UART_BUF_LEN 64
char uart_buf[UART_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 >= UART_BUF_LEN-1)
{
uart_buf[uart_buf_idx] = '\0';
uart_buf_idx = 0;
// 解析左电机转速指令(如"LEFT=50")
if(strstr(uart_buf, "LEFT=") != NULL)
{
target_speed_left = atoi(uart_buf + 5);
printf("左电机目标转速:%d r/min\r\n", target_speed_left);
}
// 解析右电机转速指令(如"RIGHT=-30")
else if(strstr(uart_buf, "RIGHT=") != NULL)
{
target_speed_right = atoi(uart_buf + 6);
printf("右电机目标转速:%d r/min\r\n", target_speed_right);
}
// 停止指令("STOP")
else if(strcmp(uart_buf, "STOP") == 0)
{
target_speed_left = 0;
target_speed_right = 0;
printf("电机已停止\r\n");
}
}
else
{
uart_buf[uart_buf_idx++] = data;
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
// 主函数测试
int main(void)
{
delay_init(); // 延时初始化
USART1_Init(115200); // 串口1初始化(波特率115200)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 优先级分组
// 1. 初始化电机相关模块
Motor_GPIO_Init(); // 转向控制GPIO
TIM2_Encoder_Init(); // 左电机编码器
TIM3_Encoder_Init(); // 右电机编码器
NVIC_Encoder_Init(); // 编码器中断NVIC
TIM4_PWM_Init(); // PWM定时器
Motor_PID_Init(); // PID参数初始化
printf("电机控制模块初始化完成!\r\n");
printf("指令格式:\r\n");
printf(" LEFT=转速(如LEFT=50)→ 左电机目标转速\r\n");
printf(" RIGHT=转速(如RIGHT=-30)→ 右电机目标转速\r\n");
printf(" STOP → 电机停止\r\n");
printf("-------------------------\r\n");
while(1)
{
// 每500ms打印一次实际转速
printf("左电机实际转速:%d r/min,右电机实际转速:%d r/min\r\n",
motor_speed_left, motor_speed_right);
delay_ms(500);
}
}
4.3.4 PID 参数调试指南
PID 参数直接影响转速控制精度,需通过实际测试调试,调试步骤如下:
|
调试步骤 |
目标 |
操作方法 |
现象判断 |
|
1. 调 Kp |
快速响应目标转速 |
先将 Ki、Kd 设为 0,逐步增大 Kp(每次 + 0.5),直到电机转速接近目标值 |
- 若 Kp 过小:转速响应慢,误差大;- 若 Kp 过大:转速超调严重(超过目标后回落),甚至震荡 |
|
2. 调 Ki |
消除静态误差 |
在 Kp 合适的基础上,逐步增大 Ki(每次 + 0.05),直到静态误差(目标 - 实际)<5% |
- 若 Ki 过小:静态误差无法消除;- 若 Ki 过大:转速震荡加剧,稳定性下降 |
|
3. 调 Kd |
抑制超调与震荡 |
在 Kp、Ki 合适的基础上,逐步增大 Kd(每次 + 0.01),直到超调量 < 10% |
- 若 Kd 过小:超调严重;- 若 Kd 过大:转速响应变慢,对负载变化不敏感 |
参考调试值:12V 减速电机(600 线编码器)通常 Kp=1.5~2.5,Ki=0.08~0.15,Kd=0.03~0.08。
4.3.5 常见问题与解决方案
|
问题现象 |
可能原因 |
解决方案 |
|
编码器计数为 0(转速始终 0) |
1. 编码器 A/B 相接线错误(接反或未接)2. 编码器模式配置错误(非模式 3)3. 定时器未启动 |
1. 重新核对接线(左电机 PA0=A 相、PA1=B 相;右电机 PB0=A 相、PB1=B 相)2. 确认TIM_EncoderInterfaceConfig参数为TIM_EncoderMode_TI123. 检查TIM_Cmd(ENCODER_TIM_LEFT, ENABLE)是否执行 |
|
电机转速与目标值偏差大(静态误差 > 10%) |
1. PID 参数不合适(Ki 过小)2. 电机负载过大(如小车超重)3. PWM 最小占空比过低 |
1. 增大 Ki 值(参考调试指南)2. 减轻小车负载或更换扭矩更大的电机3. 提高PWM_MIN(如从 100 改为 150) |
|
电机转速震荡严重(忽高忽低) |
1. PID 参数不合适(Kp/Ki 过大,Kd 过小)2. 编码器脉冲干扰(未滤波)3. 供电不稳定 |
1. 减小 Kp/Ki,增大 Kd2. 增大编码器滤波系数(如TIM_ICFilter_8)3. 在 L298N 12V 电源端并联 1000uF 电容 |
|
电机只能正转,无法反转 |
1. 转向控制 GPIO 接线错误(IN2/IN4 未接)2. Motor_SetDir函数逻辑错误 |
1. 重新核对 IN2(左电机)、IN4(右电机)接线2. 检查Motor_SetDir_Left中 IN1/IN2 的电平逻辑 |
第五章 模块 4:PS2 手柄(SPI 接口)与中断应用
PS2 手柄通过 SPI 接口与 STM32 通信,可实时发送 “摇杆、按键” 指令控制小车(如左摇杆控制转速、右摇杆控制舵机角度)。结合 SPI 中断,可实现指令的快速接收,避免主程序轮询等待。
5.1 PS2 手柄硬件原理
5.1.1 核心参数与接口定义
|
参数 |
规格 |
|
通信接口 |
SPI(同步串行通信,STM32 作为主机,手柄作为从机) |
|
工作电压 |
3.3V(部分手柄支持 5V,需查看手册) |
|
指令频率 |
100kHz~1MHz(推荐 500kHz,兼容性好) |
|
数据格式 |
8 位数据,高位先传,CPOL=1(空闲时 SCLK 高电平),CPHA=1(第二个边沿采样) |
|
控制指令 |
包含 2 个摇杆(X/Y 轴,10 位精度)、12 个按键(如 SELECT、START、方向键) |
5.1.2 PS2 手柄引脚定义(常见 2.4G 无线手柄接收器)
|
接收器引脚 |
功能 |
备注 |
|
VCC |
电源 |
3.3V(不可接 5V,否则烧毁接收器) |
|
GND |
地 |
与 STM32 共地 |
|
CS |
片选信号 |
低电平有效(STM32 输出,选择手柄作为从机) |
|
SCK |
SPI 时钟线 |
STM32 输出时钟信号 |
|
MOSI |
主机发送数据 |
STM32 向手柄发送指令(如读取数据指令) |
|
MISO |
从机发送数据 |
手柄向 STM32 返回数据(摇杆、按键状态) |
|
NC |
空引脚 |
无需连接 |
5.1.3 硬件接线表(STM32F103C8T6 + PS2 手柄接收器)
|
PS2 接收器引脚 |
功能 |
STM32 引脚 |
备注 |
|
VCC |
电源 |
3.3V |
严格 3.3V 供电 |
|
GND |
地 |
GND |
共地减少干扰 |
|
CS |
片选信号 |
PB12 |
GPIO 输出(低电平有效) |
|
SCK |
SPI 时钟线 |
PB13 |
SPI2_SCK(STM32F103 默认 SPI2 时钟引脚) |
|
MOSI |
主机发送数据 |
PB15 |
SPI2_MOSI(STM32F103 默认 SPI2 主机发送引脚) |
|
MISO |
从机发送数据 |
PB14 |
SPI2_MISO(STM32F103 默认 SPI2 从机接收引脚) |
选择 SPI2 的原因:SPI1 引脚(PA5=SCK、PA6=MISO、PA7=MOSI)可能与其他模块冲突(如舵机 PA8),SPI2 引脚(PB13~PB15)独立性更强。
5.2 SPI 通信原理与中断配置
5.2.1 SPI 通信时序(PS2 手柄要求)
PS2 手柄 SPI 通信需满足特定时序(CPOL=1,CPHA=1):
- CPOL=1:SCLK 空闲时为高电平;
- CPHA=1:数据在 SCLK 的第二个边沿(高→低)采样;
- 数据传输:每次通信 STM32 发送 1 个字节指令,手柄返回 1 个字节数据,共需发送 9 个字节(前 1 个为同步指令,后 8 个为数据)。
5.2.2 中断配置要点
为避免主程序轮询 SPI 接收状态,配置 SPI“接收缓冲区非空中断(RXNE)”:
- 当手柄返回 1 个字节数据时,触发 SPI 中断;
- 在中断服务函数中读取数据,存入缓冲区;
- 当接收完 9 个字节后,解析数据(摇杆 / 按键状态)。
5.3 软件实现(标准库)
5.3.1 软件模块划分
|
模块 |
功能 |
核心函数 |
|
SPI 初始化 |
配置 SPI2 为主机模式,设置时序与中断 |
SPI2_Init()、NVIC_SPI2_Init() |
|
PS2 数据接收 |
在 SPI 中断中接收 9 字节数据,完成后置位解析标志 |
SPI2_IRQHandler()、PS2_ReceiveData() |
|
PS2 数据解析 |
将接收的原始数据转换为 “摇杆值(0~1023)” 和 “按键状态(按下 / 未按下)” |
PS2_ParseData() |
|
小车控制映射 |
将摇杆 / 按键值映射为 “电机目标转速” 和 “舵机目标角度” |
PS2_MapToCarCtrl() |
5.3.2 核心代码实现
1. 宏定义与全局变量
#include "stm32f10x.h"
#include "ps2.h"
#include "motor.h"
#include "servo.h"
// SPI2参数(PS2手柄)
#define PS2_SPI SPI2
#define PS2_SPI_CLK RCC_APB1Periph_SPI2
#define PS2_CS_PIN GPIO_Pin_12
#define PS2_CS_PORT GPIOB
#define PS2_CS_CLK RCC_APB2Periph_GPIOB
// PS2数据接收参数
#define PS2_DATA_LEN 9 // 每次通信接收9字节数据
uint8_t ps2_rx_buf[PS2_DATA_LEN] = {0}; // 接收缓冲区
uint8_t ps2_rx_idx = 0; // 接收索引
uint8_t ps2_parse_flag = 0; // 数据解析标志(1:接收完成,可解析)
// PS2控制状态(摇杆+按键)
typedef struct
{
// 左摇杆(控制电机转速):X轴(0~1023,512为中位),Y轴(未使用)
uint16_t left_joy_x;
// 右摇杆(控制舵机角度):X轴(0~1023,512为中位),Y轴(未使用)
uint16_t right_joy_x;
// 按键状态(1:按下,0:未按下)
uint8_t key_select; // SELECT键
uint8_t key_start; // START键
uint8_t key_up; // 方向键上
uint8_t key_down; // 方向键下
uint8_t key_left; // 方向键左
uint8_t key_right; // 方向键右
} PS2_CtrlState;
PS2_CtrlState ps2_ctrl; // PS2控制状态变量
2. SPI2 初始化与 CS 引脚配置
// PS2 CS引脚初始化(PB12,推挽输出)
void PS2_CS_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(PS2_CS_CLK, ENABLE); // 使能GPIOB时钟
GPIO_InitStruct.GPIO_Pin = PS2_CS_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(PS2_CS_PORT, &GPIO_InitStruct);
GPIO_SetBits(PS2_CS_PORT, PS2_CS_PIN); // 初始高电平(未选中从机)
}
// SPI2初始化(主机模式,CPOL=1,CPHA=1)
void SPI2_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
SPI_InitTypeDef SPI_InitStruct;
// 1. 使能时钟
RCC_APB1PeriphClockCmd(PS2_SPI_CLK, ENABLE); // SPI2时钟(APB1,36MHz)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // PB13~PB15时钟
// 2. 配置SPI2引脚(SCK=PB13,MISO=PB14,MOSI=PB15)
// SCK(输出)、MOSI(输出):推挽复用
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
// MISO(输入):浮空输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOB, &GPIO_InitStruct);
// 3. 配置SPI2参数
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 全双工
SPI_InitStruct.SPI_Mode = SPI_Mode_Master; // 主机模式
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b; // 8位数据
SPI_InitStruct.SPI_CPOL = SPI_CPOL_High; // CPOL=1(空闲高电平)
SPI_InitStruct.SPI_CPHA = SPI_CPHA_2Edge; // CPHA=1(第二个边沿采样)
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; // 软件控制NSS(CS引脚)
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64; // 波特率:36MHz/64=562.5kHz(符合PS2要求)
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先传
SPI_InitStruct.SPI_CRCPolynomial = 7; // CRC多项式(默认7,无需修改)
SPI_Init(PS2_SPI, &SPI_InitStruct);
// 4. 使能SPI2接收中断(RXNE)
SPI_I2S_ITConfig(PS2_SPI, SPI_I2S_IT_RXNE, ENABLE);
// 5. 启动SPI2
SPI_Cmd(PS2_SPI, ENABLE);
}
// SPI2 NVIC配置
void NVIC_SPI2_Init(void)
{
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = SPI2_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级2(按之前规划)
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; // 子优先级0
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
// SPI发送1个字节
void SPI_SendByte(SPI_TypeDef* SPIx, uint8_t data)
{
while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) == RESET); // 等待发送缓冲区为空
SPI_I2S_SendData(SPIx, data);
}
// SPI接收1个字节
uint8_t SPI_ReceiveByte(SPI_TypeDef* SPIx)
{
while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) == RESET); // 等待接收缓冲区非空
return SPI_I2S_ReceiveData(SPIx);
}
3. PS2 数据接收与中断服务函数
PS2 手柄通信需先发送 “0x01” 同步指令,再发送 8 个 “0x42” 指令,同时接收 9 字节数据:
// 启动PS2数据接收(主程序中调用,每10ms一次)
void PS2_StartReceive(void)
{
if(ps2_parse_flag == 0) // 上一次数据未解析完成,不启动新接收
return;
ps2_rx_idx = 0; // 接收索引清零
ps2_parse_flag = 0; // 清除解析标志
GPIO_ResetBits(PS2_CS_PORT, PS2_CS_PIN); // 拉低CS,选中PS2手柄
// 发送第一个同步指令(0x01)
SPI_SendByte(PS2_SPI, 0x01);
}
// SPI2中断服务函数(接收PS2数据)
void SPI2_IRQHandler(void)
{
if(SPI_I2S_GetITStatus(PS2_SPI, SPI_I2S_IT_RXNE) != RESET)
{
// 1. 接收1字节数据存入缓冲区
ps2_rx_buf[ps2_rx_idx++] = SPI_ReceiveByte(PS2_SPI);
// 2. 根据接收索引发送下一个指令
if(ps2_rx_idx < PS2_DATA_LEN)
{
// 第2~9字节指令为0x42
SPI_SendByte(PS2_SPI, 0x42);
}
else
{
// 3. 接收完成(9字节),拉高CS,置位解析标志
GPIO_SetBits(PS2_CS_PORT, PS2_CS_PIN);
ps2_parse_flag = 1;
}
// 4. 清除中断标志(SPI接收中断无需手动清除,读取数据后自动清除)
}
}
4. PS2 数据解析(摇杆与按键)
PS2 接收的 9 字节数据中,关键信息分布如下:
- 第 2 字节:按键状态高位(方向键、SELECT、START 等);
- 第 3 字节:按键状态低位;
- 第 4 字节:左摇杆 X 轴原始值(0~255);
- 第 5 字节:左摇杆 Y 轴原始值;
- 第 6 字节:右摇杆 X 轴原始值;
- 第 7 字节:右摇杆 Y 轴原始值。
需将原始值(0~255)映射为 10 位精度(0~1023),按键状态通过位判断:
// PS2数据解析(将原始数据转换为控制状态)
void PS2_ParseData(void)
{
if(ps2_parse_flag == 0)
return;
// 1. 解析左摇杆X轴(控制电机转速:0~1023,512为中位)
ps2_ctrl.left_joy_x = (uint16_t)ps2_rx_buf[3] * 4; // 0~255 → 0~1020(近似0~1023)
// 中位校准(若偏移,可调整±20)
if(ps2_ctrl.left_joy_x > 492 && ps2_ctrl.left_joy_x < 532)
ps2_ctrl.left_joy_x = 512;
// 2. 解析右摇杆X轴(控制舵机角度:0~1023,512为中位)
ps2_ctrl.right_joy_x = (uint16_t)ps2_rx_buf[5] * 4;
if(ps2_ctrl.right_joy_x > 492 && ps2_ctrl.right_joy_x < 532)
ps2_ctrl.right_joy_x = 512;
// 3. 解析按键状态(1:按下,0:未按下;PS2按键为低电平有效)
uint8_t key_high = ~ps2_rx_buf[1]; // 第2字节取反(高位)
uint8_t key_low = ~ps2_rx_buf[2]; // 第3字节取反(低位)
ps2_ctrl.key_select = (key_high >> 0) & 0x01; // SELECT键(高位第0位)
ps2_ctrl.key_start = (key_high >> 3) & 0x01; // START键(高位第3位)
ps2_ctrl.key_up = (key_low >> 4) & 0x01; // 方向键上(低位第4位)
ps2_ctrl.key_down = (key_low >> 6) & 0x01; // 方向键下(低位第6位)
ps2_ctrl.key_left = (key_low >> 7) & 0x01; // 方向键左(低位第7位)
ps2_ctrl.key_right = (key_low >> 5) & 0x01; // 方向键右(低位第5位)
ps2_parse_flag = 0; // 清除解析标志
}
// 打印PS2控制状态(调试用)
void PS2_PrintState(void)
{
printf("左摇杆X:%d,右摇杆X:%d\r\n",
ps2_ctrl.left_joy_x, ps2_ctrl.right_joy_x);
printf("按键:SELECT=%d,START=%d,UP=%d,DOWN=%d,LEFT=%d,RIGHT=%d\r\n",
ps2_ctrl.key_select, ps2_ctrl.key_start,
ps2_ctrl.key_up, ps2_ctrl.key_down,
ps2_ctrl.key_left, ps2_ctrl.key_right);
printf("-------------------------\r\n");
}
5. PS2 控制映射(摇杆→小车动作)
将摇杆值映射为 “电机转速” 和 “舵机角度”,实现直观控制:
- 左摇杆 X 轴:0~511→小车后退(转速从 - 50 到 0),512→停止,513~1023→小车前进(转速从 0 到 50);
- 右摇杆 X 轴:0~511→舵机左转(角度从 45° 到 90°),512→中位(90°),513~1023→舵机右转(角度从 90° 到 135°)。
// PS2控制映射到小车(电机转速+舵机角度)
void PS2_MapToCarCtrl(void)
{
int16_t target_speed;
uint16_t target_angle;
// 1. 左摇杆X轴→电机目标转速(-50~50 r/min)
if(ps2_ctrl.left_joy_x < 512)
{
// 后退:0~511 → -50~0 r/min
target_speed = (int16_t)((ps2_ctrl.left_joy_x - 512) * 50.0f / 512.0f);
}
else if(ps2_ctrl.left_joy_x > 512)
{
// 前进:513~1023 → 0~50 r/min
target_speed = (int16_t)((ps2_ctrl.left_joy_x - 512) * 50.0f / 511.0f);
}
else
{
// 中位→停止
target_speed = 0;
}
// 左右电机转速一致(阿克曼小车直线行驶时)
target_speed_left = target_speed;
target_speed_right = target_speed;
// 2. 右摇杆X轴→舵机目标角度(45°~135°,避免超出舵机范围)
if(ps2_ctrl.right_joy_x < 512)
{
// 左转:0~511 → 45°~90°
target_angle = 45 + (uint16_t)((512 - ps2_ctrl.right_joy_x) * 45.0f / 512.0f);
}
else if(ps2_ctrl.right_joy_x > 512)
{
// 右转:513~1023 → 90°~135°
target_angle = 90 + (uint16_t)((ps2_ctrl.right_joy_x - 512) * 45.0f / 511.0f);
}
else
{
// 中位→90°
target_angle = 90;
}
Servo_SetAngle(target_angle);
// 3. 按键控制(如START键停止)
if(ps2_ctrl.key_start == 1)
{
target_speed_left = 0;
target_speed_right = 0;
Servo_SetAngle(90);
printf("START键按下,小车停止,舵机回中位\r\n");
}
}
5.3.3 小车完整控制主程序
整合 PS2、电机、舵机模块,实现手柄控制小车:
#include "delay.h"
#include "usart.h"
int main(void)
{
// 1. 基础初始化
delay_init();
USART1_Init(115200);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 2. 外设初始化
Motor_GPIO_Init();
TIM2_Encoder_Init();
TIM3_Encoder_Init();
NVIC_Encoder_Init();
TIM4_PWM_Init();
Motor_PID_Init();
TIM1_PWM_Init(); // 舵机初始化
PS2_CS_Init();
SPI2_Init();
NVIC_SPI2_Init(); // PS2初始化
printf("阿克曼小车系统初始化完成!\r\n");
printf("左摇杆X:控制前进/后退,右摇杆X:控制转向\r\n");
printf("START键:紧急停止\r\n");
ps2_parse_flag = 1; // 初始允许接收数据
while(1)
{
// 每10ms启动一次PS2数据接收
PS2_StartReceive();
delay_ms(10);
// 接收完成后解析数据,并映射到小车控制
if(ps2_parse_flag == 1)
{
PS2_ParseData();
PS2_MapToCarCtrl();
// 每500ms打印一次状态(调试用)
// PS2_PrintState();
// printf("电机转速:左=%d,右=%d;舵机角度:%d\r\n",
// motor_speed_left, motor_speed_right, current_angle);
}
}
}
5.3.4 常见问题与解决方案
|
问题现象 |
可能原因 |
解决方案 |
|
PS2 接收数据全为 0xFF |
1. SPI 时序错误(CPOL/CPHA 配置错误)2. CS 引脚未拉低(未选中从机)3. 手柄未配对(无线手柄) |
1. 确认SPI_CPOL_High和SPI_CPHA_2Edge2. 检查PS2_StartReceive中GPIO_ResetBits(PS2_CS_PIN)是否执行3. 重新配对无线手柄(按接收器配对键) |
|
摇杆值漂移(中位不在 512) |
1. 手柄硬件漂移(劣质手柄)2. 原始值未校准 |
1. 更换手柄;2. 在PS2_ParseData中扩大中位校准范围(如 480~544) |
|
按键无响应(始终 0) |
1. 按键数据位映射错误2. 原始数据未取反(PS2 按键低电平有效) |
1. 核对按键位映射(如key_high >> 0对应 SELECT 键)2. 确认key_high = ~ps2_rx_buf[1](取反操作) |
第六章 模块 5:CAN 总线通信与中断应用
CAN 总线是工业级现场总线,具有高可靠性、抗干扰能力强的特点,可用于阿克曼小车扩展模块通信(如多小车协同、远程数据上传)。STM32 通过 CAN 控制器实现总线通信,结合接收中断可实时处理总线数据。
6.1 CAN 总线硬件原理
6.1.1 核心概念与参数
|
术语 |
定义 |
规格(STM32F103) |
|
CAN 控制器 |
STM32 内置的 CAN 协议控制器,支持 CAN 2.0A/B 标准 |
支持 11 位(标准帧)/29 位(扩展帧)ID,波特率最高 1Mbps |
|
CAN 收发器 |
实现 CAN 控制器与物理总线的电平转换(TTL→CAN 总线电平) |
常用 TJA1050(5V 供电,支持高速 CAN) |
|
波特率 |
CAN 总线通信速率 |
推荐 250kbps(小车场景,抗干扰强)或 500kbps(高速场景) |
|
总线拓扑 |
两端需接 120Ω 终端电阻,总线长度≤1000m(250kbps 时) |
小车场景:CAN_H、CAN_L 两根双绞线,两端接 120Ω 电阻 |
|
帧类型 |
数据帧(发送数据)、远程帧(请求数据)、错误帧(报错)、过载帧(忙) |
小车常用数据帧(发送电机转速、姿态等数据) |
2492

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



