文章目录
-
- 一、项目背景与原理概述
-
- [1.1 核心原理拆解](#1.1 核心原理拆解)
- [1.2 硬件选型(零基础友好)](#1.2 硬件选型(零基础友好))
- 二、硬件电路设计与接线
-
- [2.1 电路拓扑(Mermaid流程图展示)](#2.1 电路拓扑(Mermaid流程图展示))
- [2.2 详细接线步骤(零基础友好)](#2.2 详细接线步骤(零基础友好))
- 三、软件环境搭建
-
- [3.1 开发环境配置](#3.1 开发环境配置)
- [3.2 STM32CubeMX配置步骤](#3.2 STM32CubeMX配置步骤)
- 四、核心代码编写(详细注释)
-
- [4.1 头文件定义与全局变量](#4.1 头文件定义与全局变量)
- [4.2 PID算法实现](#4.2 PID算法实现)
- [4.3 PWM输出与电机控制](#4.3 PWM输出与电机控制)
- [4.4 ADC电流采集](#4.4 ADC电流采集)
- [4.5 编码器转速采集(可选)](#4.5 编码器转速采集(可选))
- [4.6 串口调试函数](#4.6 串口调试函数)
- [4.7 main函数主逻辑](#4.7 main函数主逻辑)
- [4.8 时钟配置函数](#4.8 时钟配置函数)
- 五、调试与参数整定
-
- [5.1 硬件调试](#5.1 硬件调试)
- [5.2 PID参数整定](#5.2 PID参数整定)
- [5.3 常见问题与解决](#5.3 常见问题与解决)
- 六、项目扩展与优化
- 总结
一、项目背景与原理概述
你想要实现的是基于STM32F1系列单片机的直流电机闭环调速系统,核心是通过PWM输出控制电机转速,ADC采集电机工作电流,再结合PID算法实现精准的转速调节,相比开环控制,闭环控制能有效抵抗负载变化、电压波动等干扰,让电机转速保持稳定。
1.1 核心原理拆解
- PWM调速:通过改变STM32输出的PWM波占空比,调整施加在直流电机两端的平均电压,从而改变电机转速。
- ADC电流采集:利用采样电阻将电机的工作电流转换为电压信号,通过STM32的ADC通道采集该电压,再换算为实际电流值,用于监测电机运行状态和参与PID调节。
- PID闭环控制:将电机实际转速(可通过编码器或电流间接反映,本文以电流辅助+转速反馈为例)与目标转速对比,通过比例(P)、积分(I)、微分(D)算法计算出补偿量,动态调整PWM占空比,使实际转速逼近目标转速。
1.2 硬件选型(零基础友好)
| 硬件模块 | 型号/规格 | 作用 |
|---|---|---|
| 主控芯片 | STM32F103C8T6 | 核心控制,输出PWM、采集ADC等 |
| 直流电机 | 12V直流减速电机 | 被控对象 |
| 电机驱动 | L298N(或TB6612) | 放大STM32的PWM信号,驱动电机 |
| 采样电阻 | 0.1Ω/2W精密电阻 | 将电机电流转换为电压信号 |
| 电源 | 12V/2A直流电源 | 给电机供电;5V给STM32供电 |
| 编码器(可选) | 霍尔编码器(AB相) | 采集电机实际转速(精准反馈) |
| 滤波电容 | 100uF+0.1uF | 稳定ADC采集电压 |
| 杜邦线/面包板 | 通用款 | 电路连接 |
二、硬件电路设计与接线
2.1 电路拓扑(Mermaid流程图展示)
12V直流电源
IN1/IN2引脚
5V降压模块
STM32F103C8T6
PWM输出通道TIM1_CH1
GPIO输出
直流电机
0.1Ω采样电阻
GND
RC滤波电路
STM32 ADC1_CH0
霍尔编码器
STM32 TIM2_CH1/CH2
PID算法计算
USART串口
上位机
2.2 详细接线步骤(零基础友好)
-
电源接线:
- 12V电源正极接L298N的12V引脚,负极接L298N的GND引脚;
- L298N的5V引脚可对外供电(若电流不足,建议单独接5V降压模块),接STM32的VCC引脚;
- 所有GND(STM32、L298N、采样电阻、电源)必须共地,否则ADC采集会有误差。
-
STM32与L298N接线:
- STM32的PA8(TIM1_CH1)接L298N的ENA引脚(PWM调速);
- STM32的PA0、PA1接L298N的IN1、IN2引脚(控制电机正反转);
- L298N的OUT1、OUT2接直流电机的两端。
-
电流采集电路接线:
- 在电机负极回路中串联0.1Ω采样电阻;
- 采样电阻两端并联100uF电解电容+0.1uF陶瓷电容(RC滤波);
- 滤波后的电压信号接STM32的PA0(ADC1_CH0),注意电压范围不超过3.3V(STM32 ADC量程)。
-
编码器接线(可选):
- 编码器VCC接5V,GND接共地;
- 编码器A相接STM32的PA0(TIM2_CH1),B相接PA1(TIM2_CH2)(需注意引脚复用)。
-
串口接线(调试用):
- STM32的PA9(TX)接USB转TTL的RX,PA10(RX)接USB转TTL的TX,GND共地。
三、软件环境搭建
3.1 开发环境配置
- 下载并安装STM32CubeMX(版本6.0以上):用于图形化配置STM32外设;
- 下载并安装Keil MDK-ARM(版本5.30以上):用于代码编译和下载;
- 安装STM32F103C8T6的器件库(在Keil中添加对应的Device Pack);
- 配置J-Link/ST-Link调试器驱动,确保能正常下载程序到STM32。
3.2 STM32CubeMX配置步骤
-
新建工程:
- 打开STM32CubeMX,选择"Access to MCU Selector";
- 搜索"STM32F103C8T6",选择对应芯片,点击"Start Project"。
-
系统配置:
- 点击"System Core"→"RCC",选择"HSE"(外部高速时钟)为"Crystal/Ceramic Resonator";
- 点击"System Core"→"SYS",调试接口选择"Serial Wire"(SWD,节省引脚)。
-
PWM配置(TIM1):
- 点击"Timers"→"TIM1",模式选择"PWM Generation CH1";
- 配置预分频器(PSC):72-1=71(系统时钟72MHz,分频后1MHz);
- 自动重装值(ARR):999(PWM频率=1MHz/(999+1)=1kHz,适合电机驱动);
- PWM模式选择"PWM Mode 1",使能CH1输出。
-
ADC配置(ADC1):
- 点击"Analog"→"ADC1",模式选择"Independent Mode";
- 启用"IN0"通道,采样时间设置为"239.5 Cycles"(提高采集精度);
- 开启ADC连续转换模式,数据对齐方式选择"Right alignment"。
-
定时器配置(编码器用TIM2):
- 点击"Timers"→"TIM2",模式选择"Encoder Mode",选择"Encoder Mode TI1 and TI2";
- 预分频器设为0,自动重装值设为65535(最大计数范围)。
-
GPIO配置:
- 配置PA0、PA1为"GPIO_Output"(IN1、IN2引脚),默认电平设为低;
- 其他引脚保持默认。
-
串口配置(USART1):
- 点击"Connectivity"→"USART1",模式选择"Asynchronous"(异步通信);
- 波特率设为115200,数据位8,停止位1,无校验。
-
时钟树配置:
- 点击"Clock Configuration",将HCLK设为72MHz(STM32F103最大主频);
- 确认各外设时钟分频正确(如APB1=36MHz,APB2=72MHz)。
-
生成代码:
- 点击"Project Manager",设置工程名称、保存路径,工具链选择"MDK-ARM";
- 点击"Code Generator",选择"Generate peripheral initialization as a pair of '.c/.h' files per peripheral";
- 点击"Generate Code",生成工程后用Keil打开。
四、核心代码编写(详细注释)
4.1 头文件定义与全局变量
c
#include "main.h"
#include "adc.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"
/* PID参数定义 */
typedef struct {
float Target; // 目标值(转速/电流)
float Actual; // 实际值
float Kp; // 比例系数
float Ki; // 积分系数
float Kd; // 微分系数
float Error; // 当前误差
float LastError; // 上一次误差
float SumError; // 误差积分
float Output; // PID输出(PWM占空比)
float MaxOutput; // 输出上限
float MinOutput; // 输出下限
} PID_TypeDef;
/* 全局变量 */
PID_TypeDef MotorPID; // 电机PID结构体
uint16_t ADC_Value = 0; // ADC原始采集值
float Motor_Current = 0.0f; // 电机电流(单位:A)
uint16_t Motor_Speed = 0; // 电机转速(单位:rpm,编码器采集)
uint16_t Target_Speed = 1000; // 目标转速
uint32_t Encoder_Count = 0; // 编码器计数值
/* 函数声明 */
void PID_Init(PID_TypeDef *pid, float kp, float ki, float kd, float max_out, float min_out);
float PID_Calculate(PID_TypeDef *pid, float target, float actual);
void Motor_Set_PWM(uint16_t duty);
void Motor_Set_Dir(uint8_t dir); // 0-正转,1-反转
void ADC_Get_Current(void);
void Encoder_Get_Speed(void);
void USART_Send_Data(void); // 串口发送调试数据
4.2 PID算法实现
c
/**
* @brief PID参数初始化
* @param pid: PID结构体指针
* @param kp: 比例系数
* @param ki: 积分系数
* @param kd: 微分系数
* @param max_out: 输出上限
* @param min_out: 输出下限
* @retval 无
*/
void PID_Init(PID_TypeDef *pid, float kp, float ki, float kd, float max_out, float min_out)
{
pid->Kp = kp;
pid->Ki = ki;
pid->Kd = kd;
pid->MaxOutput = max_out;
pid->MinOutput = min_out;
pid->Target = 0.0f;
pid->Actual = 0.0f;
pid->Error = 0.0f;
pid->LastError = 0.0f;
pid->SumError = 0.0f;
pid->Output = 0.0f;
}
/**
* @brief PID计算
* @param pid: PID结构体指针
* @param target: 目标值
* @param actual: 实际值
* @retval PID输出值
*/
float PID_Calculate(PID_TypeDef *pid, float target, float actual)
{
// 更新目标值和实际值
pid->Target = target;
pid->Actual = actual;
// 计算当前误差
pid->Error = pid->Target - pid->Actual;
// 积分项(积分限幅,防止积分饱和)
pid->SumError += pid->Error;
if(pid->SumError > pid->MaxOutput / pid->Ki)
{
pid->SumError = pid->MaxOutput / pid->Ki;
}
else if(pid->SumError < pid->MinOutput / pid->Ki)
{
pid->SumError = pid->MinOutput / pid->Ki;
}
// PID公式计算
pid->Output = pid->Kp * pid->Error + pid->Ki * pid->SumError + pid->Kd * (pid->Error - pid->LastError);
// 输出限幅
if(pid->Output > pid->MaxOutput)
{
pid->Output = pid->MaxOutput;
}
else if(pid->Output < pid->MinOutput)
{
pid->Output = pid->MinOutput;
}
// 更新上一次误差
pid->LastError = pid->Error;
return pid->Output;
}
4.3 PWM输出与电机控制
c
/**
* @brief 设置电机PWM占空比
* @param duty: 占空比(0-999,对应0-100%)
* @retval 无
*/
void Motor_Set_PWM(uint16_t duty)
{
// 限幅,防止超出PWM范围
if(duty > 999)
{
duty = 999;
}
else if(duty < 0)
{
duty = 0;
}
// TIM1_CH1的PWM值更新(CCR1寄存器)
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, duty);
}
/**
* @brief 控制电机正反转
* @param dir: 0-正转,1-反转
* @retval 无
*/
void Motor_Set_Dir(uint8_t dir)
{
if(dir == 0)
{
// 正转:IN1=高,IN2=低
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);
}
else
{
// 反转:IN1=低,IN2=高
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET);
}
}
4.4 ADC电流采集
c
/**
* @brief 获取电机电流值
* @param 无
* @retval 无
*/
void ADC_Get_Current(void)
{
// 启动ADC转换
HAL_ADC_Start(&hadc1);
// 等待转换完成
if(HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK)
{
// 读取ADC原始值(12位ADC,范围0-4095)
ADC_Value = HAL_ADC_GetValue(&hadc1);
// 转换为电压值:V = (ADC_Value / 4095) * 3.3V
float Voltage = (float)ADC_Value * 3.3f / 4095.0f;
// 转换为电流值:I = V / R(R=0.1Ω)
Motor_Current = Voltage / 0.1f;
}
// 停止ADC转换
HAL_ADC_Stop(&hadc1);
}
4.5 编码器转速采集(可选)
c
/**
* @brief 获取电机转速
* @param 无
* @retval 无
*/
void Encoder_Get_Speed(void)
{
// 读取编码器计数值(TIM2的CNT寄存器)
Encoder_Count = __HAL_TIM_GET_COUNTER(&htim2);
// 转速计算:假设编码器每转脉冲数为11,减速比为30
// 转速(rpm) = (计数值 * 60) / (脉冲数 * 减速比 * 采样时间)
// 采样时间设为100ms(0.1s)
Motor_Speed = (Encoder_Count * 60) / (11 * 30 * 0.1f);
// 清零计数值,准备下一次采样
__HAL_TIM_SET_COUNTER(&htim2, 0);
}
4.6 串口调试函数
c
/**
* @brief 串口发送调试数据(电流、转速、PWM占空比)
* @param 无
* @retval 无
*/
void USART_Send_Data(void)
{
char buf[100];
sprintf(buf, "Target_Speed: %d rpm, Actual_Speed: %d rpm, Current: %.2f A, PWM_Duty: %d\r\n",
Target_Speed, Motor_Speed, Motor_Current, (uint16_t)MotorPID.Output);
HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 100);
}
4.7 main函数主逻辑
c
int main(void)
{
/* 初始化HAL库 */
HAL_Init();
/* 配置系统时钟 */
SystemClock_Config();
/* 初始化外设 */
MX_GPIO_Init();
MX_ADC1_Init();
MX_TIM1_Init();
MX_TIM2_Init();
MX_USART1_UART_Init();
/* 启动PWM输出 */
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
/* 启动编码器模式 */
HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL);
/* PID参数初始化(需根据实际电机调试) */
// Kp=1.2, Ki=0.5, Kd=0.1, 输出上限999, 下限0
PID_Init(&MotorPID, 1.2f, 0.5f, 0.1f, 999.0f, 0.0f);
/* 设置电机正转 */
Motor_Set_Dir(0);
/* 主循环 */
while (1)
{
/* 采集电流(10ms一次) */
ADC_Get_Current();
/* 采集转速(100ms一次) */
Encoder_Get_Speed();
/* PID计算,调整PWM占空比 */
PID_Calculate(&MotorPID, Target_Speed, Motor_Speed);
/* 设置PWM占空比 */
Motor_Set_PWM((uint16_t)MotorPID.Output);
/* 串口发送调试数据(200ms一次) */
USART_Send_Data();
/* 延时10ms */
HAL_Delay(10);
}
}
4.8 时钟配置函数
c
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
/* 错误处理函数 */
void Error_Handler(void)
{
__disable_irq();
while (1)
{
// 可添加LED闪烁等错误提示
}
}
五、调试与参数整定
5.1 硬件调试
- 电源检查:上电后,测量STM32的VCC引脚是否为3.3V,L298N的12V引脚是否为12V,确保无短路;
- PWM输出检查:用示波器测量PA8引脚,查看是否有1kHz的PWM波输出,占空比是否随PID调节变化;
- ADC采集检查:用万用表测量采样电阻两端电压,对比STM32串口输出的电流值,验证采集精度;
- 编码器检查:手动转动电机,查看串口输出的Actual_Speed是否有变化,确保编码器计数正常。
5.2 PID参数整定
PID参数是闭环控制的核心,需耐心调试,步骤如下:
- 先调P参数:将Ki=0、Kd=0,逐渐增大Kp,直到电机转速接近目标转速但有小幅波动;
- 再调I参数:保持Kp不变,逐渐增大Ki,消除静态误差(转速稳定在目标值);
- 最后调D参数:小幅增大Kd,抑制转速波动,提高响应速度(D参数过大易导致震荡);
- 微调优化:反复调整Kp、Ki、Kd,使电机在负载变化时转速仍能快速稳定。
5.3 常见问题与解决
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 电机不转 | PWM无输出/电机驱动板故障/接线错误 | 检查PWM引脚电平/更换驱动板/核对接线 |
| 电流采集值不准 | 采样电阻误差/滤波电路缺失/共地问题 | 更换精密电阻/添加RC滤波/确保共地 |
| 电机转速震荡 | PID参数过大(尤其是P/D) | 减小Kp/Kd参数 |
| 转速无法达到目标值 | 电源功率不足/PWM占空比已到上限 | 更换更大功率电源/调整PID输出上限 |
六、项目扩展与优化
- 添加过流保护:在ADC_Get_Current函数中,当电流超过阈值(如2A)时,关闭PWM输出,保护电机和驱动板;
- 添加LCD显示:外接1602/12864 LCD,实时显示目标转速、实际转速、电流等参数;
- 上位机控制:通过串口接收上位机指令,动态修改目标转速;
- 加入速度环+电流环双闭环:在转速环基础上,增加电流环,提高电机动态响应和过载保护能力;
- 低功耗优化:在电机停转时,关闭不必要的外设,降低STM32功耗。
总结
- 直流电机闭环控制的核心是PWM输出(调速)+ ADC/编码器采集(反馈)+ PID算法(调节),三者结合实现精准调速;
- 硬件接线需注意共地 和电压范围,软件需重点关注PID参数整定(先调P、再调I、最后调D);
- 本教程的代码可直接移植到STM32F103C8T6开发板,零基础用户按接线、配置、编译、调试的步骤操作,即可完成闭环调速系统的落地。