直流电机闭环控制:STM32F1 PWM+ADC电流采集,PID调速实战

文章目录

    • 一、项目背景与原理概述
      • [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 详细接线步骤(零基础友好)

  1. 电源接线

    • 12V电源正极接L298N的12V引脚,负极接L298N的GND引脚;
    • L298N的5V引脚可对外供电(若电流不足,建议单独接5V降压模块),接STM32的VCC引脚;
    • 所有GND(STM32、L298N、采样电阻、电源)必须共地,否则ADC采集会有误差。
  2. STM32与L298N接线

    • STM32的PA8(TIM1_CH1)接L298N的ENA引脚(PWM调速);
    • STM32的PA0、PA1接L298N的IN1、IN2引脚(控制电机正反转);
    • L298N的OUT1、OUT2接直流电机的两端。
  3. 电流采集电路接线

    • 在电机负极回路中串联0.1Ω采样电阻;
    • 采样电阻两端并联100uF电解电容+0.1uF陶瓷电容(RC滤波);
    • 滤波后的电压信号接STM32的PA0(ADC1_CH0),注意电压范围不超过3.3V(STM32 ADC量程)。
  4. 编码器接线(可选)

    • 编码器VCC接5V,GND接共地;
    • 编码器A相接STM32的PA0(TIM2_CH1),B相接PA1(TIM2_CH2)(需注意引脚复用)。
  5. 串口接线(调试用)

    • STM32的PA9(TX)接USB转TTL的RX,PA10(RX)接USB转TTL的TX,GND共地。

三、软件环境搭建

3.1 开发环境配置

  1. 下载并安装STM32CubeMX(版本6.0以上):用于图形化配置STM32外设;
  2. 下载并安装Keil MDK-ARM(版本5.30以上):用于代码编译和下载;
  3. 安装STM32F103C8T6的器件库(在Keil中添加对应的Device Pack);
  4. 配置J-Link/ST-Link调试器驱动,确保能正常下载程序到STM32。

3.2 STM32CubeMX配置步骤

  1. 新建工程

    • 打开STM32CubeMX,选择"Access to MCU Selector";
    • 搜索"STM32F103C8T6",选择对应芯片,点击"Start Project"。
  2. 系统配置

    • 点击"System Core"→"RCC",选择"HSE"(外部高速时钟)为"Crystal/Ceramic Resonator";
    • 点击"System Core"→"SYS",调试接口选择"Serial Wire"(SWD,节省引脚)。
  3. 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输出。
  4. ADC配置(ADC1)

    • 点击"Analog"→"ADC1",模式选择"Independent Mode";
    • 启用"IN0"通道,采样时间设置为"239.5 Cycles"(提高采集精度);
    • 开启ADC连续转换模式,数据对齐方式选择"Right alignment"。
  5. 定时器配置(编码器用TIM2)

    • 点击"Timers"→"TIM2",模式选择"Encoder Mode",选择"Encoder Mode TI1 and TI2";
    • 预分频器设为0,自动重装值设为65535(最大计数范围)。
  6. GPIO配置

    • 配置PA0、PA1为"GPIO_Output"(IN1、IN2引脚),默认电平设为低;
    • 其他引脚保持默认。
  7. 串口配置(USART1)

    • 点击"Connectivity"→"USART1",模式选择"Asynchronous"(异步通信);
    • 波特率设为115200,数据位8,停止位1,无校验。
  8. 时钟树配置

    • 点击"Clock Configuration",将HCLK设为72MHz(STM32F103最大主频);
    • 确认各外设时钟分频正确(如APB1=36MHz,APB2=72MHz)。
  9. 生成代码

    • 点击"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 硬件调试

  1. 电源检查:上电后,测量STM32的VCC引脚是否为3.3V,L298N的12V引脚是否为12V,确保无短路;
  2. PWM输出检查:用示波器测量PA8引脚,查看是否有1kHz的PWM波输出,占空比是否随PID调节变化;
  3. ADC采集检查:用万用表测量采样电阻两端电压,对比STM32串口输出的电流值,验证采集精度;
  4. 编码器检查:手动转动电机,查看串口输出的Actual_Speed是否有变化,确保编码器计数正常。

5.2 PID参数整定

PID参数是闭环控制的核心,需耐心调试,步骤如下:

  1. 先调P参数:将Ki=0、Kd=0,逐渐增大Kp,直到电机转速接近目标转速但有小幅波动;
  2. 再调I参数:保持Kp不变,逐渐增大Ki,消除静态误差(转速稳定在目标值);
  3. 最后调D参数:小幅增大Kd,抑制转速波动,提高响应速度(D参数过大易导致震荡);
  4. 微调优化:反复调整Kp、Ki、Kd,使电机在负载变化时转速仍能快速稳定。

5.3 常见问题与解决

问题现象 可能原因 解决方法
电机不转 PWM无输出/电机驱动板故障/接线错误 检查PWM引脚电平/更换驱动板/核对接线
电流采集值不准 采样电阻误差/滤波电路缺失/共地问题 更换精密电阻/添加RC滤波/确保共地
电机转速震荡 PID参数过大(尤其是P/D) 减小Kp/Kd参数
转速无法达到目标值 电源功率不足/PWM占空比已到上限 更换更大功率电源/调整PID输出上限

六、项目扩展与优化

  1. 添加过流保护:在ADC_Get_Current函数中,当电流超过阈值(如2A)时,关闭PWM输出,保护电机和驱动板;
  2. 添加LCD显示:外接1602/12864 LCD,实时显示目标转速、实际转速、电流等参数;
  3. 上位机控制:通过串口接收上位机指令,动态修改目标转速;
  4. 加入速度环+电流环双闭环:在转速环基础上,增加电流环,提高电机动态响应和过载保护能力;
  5. 低功耗优化:在电机停转时,关闭不必要的外设,降低STM32功耗。

总结

  1. 直流电机闭环控制的核心是PWM输出(调速)+ ADC/编码器采集(反馈)+ PID算法(调节),三者结合实现精准调速;
  2. 硬件接线需注意共地电压范围,软件需重点关注PID参数整定(先调P、再调I、最后调D);
  3. 本教程的代码可直接移植到STM32F103C8T6开发板,零基础用户按接线、配置、编译、调试的步骤操作,即可完成闭环调速系统的落地。
相关推荐
Y1rong3 小时前
STM32之MQTT
stm32
Zeku3 小时前
TCP交错传输多通道实现原理
stm32·freertos·linux应用开发
z20348315204 小时前
如何使用Micropython进行单片机开发(一)
单片机·嵌入式硬件·micropython
嵌入式×边缘AI:打怪升级日志8 小时前
C语言算术赋值运算复习笔记
c语言·stm32·单片机·算法·51单片机·proteus·代码
7yewh9 小时前
AM57X Processor SDK Linux - run Installer
linux·嵌入式硬件·硬件架构·嵌入式
LCG元10 小时前
智能农业灌溉:STM32+NB-IoT+土壤湿度传感器,自动控制实战
stm32·物联网·mongodb
光子物联单片机10 小时前
STM32传感器模块编程实践(十八)DIY电子游戏机模型
stm32·单片机·嵌入式硬件
古译汉书10 小时前
【IoT死磕系列】Day 3:学习HTTP!实战:STM32手写GET请求获取天气实战(附源码+八股文)
数据结构·stm32·物联网·网络协议·学习·算法·http
風清掦11 小时前
【江科大STM32学习笔记-06】TIM 定时器 - 6.2 定时器的输出比较功能
笔记·stm32·单片机·嵌入式硬件·学习