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

第四章 模块 3:双编码器输入电机 PWM 转速控制与中断

4.1.3 硬件接线表
2. L298N → STM32F103C8T6

L298N 引脚

L298N 引脚

功能

STM32 引脚

备注

IN1左电机转向控制 1PB12GPIO 输出(高 / 低电平控制转向)

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 轮询。核心配置要点包括:

  1. GPIO 配置:编码器 A/B 相引脚设为 “浮空输入”(避免外部电平干扰);
  1. 定时器模式:选择 “编码器模式 3”(A 相和 B 相的上升沿、下降沿均计数,实现 4 倍频,提高精度);
  1. 计数器配置:计数器设为 16 位自动重装(最大值 65535),支持正反转计数(正转递增,反转递减);
  1. 中断配置:使能定时器 “更新中断”,定期(如 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Ω 电阻

帧类型

数据帧(发送数据)、远程帧(请求数据)、错误帧(报错)、过载帧(忙)

小车常用数据帧(发送电机转速、姿态等数据)

6.1.2 硬件组成(STM32 + TJA1050)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值