这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 旋转大师:让步进电机转出精确的角度

共1条 1/1 1 跳转至页

旋转大师:让步进电机转出精确的角度

菜鸟
2026-10-31 18:18:13     打赏

平台:NUCLEO-H533RE(STM32H533RET6)|语言:C++
工具链:STM32CubeMX + CMake + VSCode

点灯、蜂鸣器、加速度计,前面几篇要么在拨一个引脚,要么在跟一条总线对话:GPIO 只有高、低,PWM 只管频率,I²C 只管时序。步进电机不一样,它要四根线按固定顺序轮流导通,早一拍、晚一拍,转子都不买账。这一篇用 28BYJ-48 五线四相步进电机和 ULN2003 驱动板,从相序表一路讲到角度换算,最后让 NUCLEO-H533RE 精确转出 180°。链路还是那一条:建工程 → 读原理图 → 配 GPIO → 写驱动 → 下载验证。

一、新建 CubeMX 工程

1.1 选择芯片

打开 STM32CubeMX,在 MCU/MPU Selector 中搜索 STM32H533RET6,确认右侧的封装、Flash、RAM 等信息后,点击 Start Project。

image.png

图 1:在 MCU/MPU Selector 中选中 STM32H533RET6(对应 NUCLEO-H533RE)

1.2 处理 TrustZone 提示

STM32H5 系列带 Arm TrustZone,新建工程时会询问是否启用安全隔离。本例不涉及安全分区,选择 without TrustZone activated,点击 OK。

image.png

图 2:TrustZone 选择“不启用”

1.3 保留 ICACHE / MPU 默认配置

紧接着会提示是否为 ICACHE 和 MPU 访问 OTP / RO 区域应用默认配置,直接点 Yes 即可,省去手动配置,性能也更好。

image.png

图 3:建议直接 Yes,启用默认的 ICACHE 与 MPU 配置

二、先读原理图,再动配置

配置之前先弄清三件事:电机怎么接、四根控制线落在哪些引脚、外部晶振是多少。

2.1 电机与驱动:28BYJ-48 + ULN2003

28BYJ-48 是入门套件里最常见的步进电机:5 V 供电、五根线(一根公共端 + A/B/C/D 四相)、内部带 1:64 减速箱。资料里的步距角通常按输出轴给:八拍半步驱动下,输出轴转一圈要 4096 个半步,折合每步 360° / 4096 ≈ 0.088°。

参数取值
供电电压5 V
相数 / 线数四相五线(单极性)
减速比约 1:64
半步数 / 圈4096(输出轴)
保持转矩约 0.3 kg·cm
单相电流约 80 ~ 100 mA(5 V / 50 Ω,扣掉 ULN2003 的压降)

四相绕组不能直接挂到 MCU 上,原因有两个:

  • 一相就要接近 100 mA,而 GPIO 的绝对最大输出只有 25 mA 左右,正常设计得按 20 mA 留余量;

  • 线圈是感性负载,断电瞬间会产生反电动势,没有续流回路就会打穿引脚。

ULN2003 正好补上这两件事:七路达林顿阵列,输入高电平就把对应输出拉到地(灌电流),每路能扛 500 mA,板子上还集成了续流二极管。模块上那排小 LED 会跟着 IN1–IN4 亮灭,接线对不对,看灯最直观。

一句话:MCU 只出逻辑,电流由 ULN2003 和 5 V 电源出。

[!note] 单极性还是双极性28BYJ-48 是单极性(公共端接 +5 V,四相分别拉到地),所以能用 ULN2003 这种“只往下拉”的驱动。常见的 42 步进电机是双极性的,需要 A4988 / DRV8825 这类 H 桥驱动,相序和接线都不同,别混着用。

本文的相序表和 4096 都按这台电机写;换电机时,先确认步距角、减速比和相序,再改 kStepTable 与 kStepsPerRevolution。

2.2 控制引脚:PC8 / PB3 / PB4 / PB5

image.png

图 4:NUCLEO-H533RE 原理图:PC8(红圈)经 Arduino 排针 CN9 的 D2 引出,PB3 / PB4 / PB5 分别对应 D3 / D5 / D4

从原理图能读出:

  • 四根控制线走 Arduino 数字排针 CN9,丝印分别是 D2(PC8)、D3(PB3)、D5(PB4)、D4(PB5);

  • PB3 / PB4 的复位复用功能是 JTDO / NJTRST,把调试口选成 Serial Wire 后 JTAG 不再占用它们,才能当普通 GPIO 用;

  • 电机电源和公共地分别从 Arduino 电源排针 CN6 的 5V / GND 取。

接线按下表来:

ULN2003 板NUCLEO 引脚排针丝印对应相
IN1PC8CN9 D2A
IN2PB3CN9 D3B
IN3PB4CN9 D5C
IN4PB5CN9 D4D
VCC5VCN6 5V电机电源
GNDGNDCN6 GND公共地

电机那端的五芯插头直接插到 ULN2003 板的电机座上,座子有防呆,插反一般插不进;四根 IN 才是需要自己接的。

[!warning] 别按 D2→D5 的顺序插 四根线在排针上确实是相邻的,但丝印顺序是 D2、D3、D5、D4:D2 = IN1、D3 = IN2、D5 = IN3、D4 = IN4。如果图省事把 IN3 插到 D4、IN4 插到 D5,相序就从 A-AB-B-BC-C-CD-D-DA 变成了 A-AB-B-BD-D-DC-C-CA,中间会出现两相同时导通在相反方向的“死点”,电机多半只会抖、不转。先对丝印,再上电。

[!warning] 5 V 从哪来 NUCLEO 的 5 V 由 ST-Link 的 USB 供出,板子上还有限流器件。单台 28BYJ-48 的工作电流在 100 ~ 200 mA,点动演示没问题;如果同时带多台电机,或者电机一转板子就复位,就换外部 5 V 电源,把 ULN2003 的 VCC 改接外部 5 V,并且一定要和 NUCLEO 共地。外部电源不要直接顶到 NUCLEO 的 5 V 排针上,避免和 USB 的 5 V 打架。

ULN2003 板上的 VCC 是电机电源,不是逻辑电源;它的输入端串的是达林顿管的基极电阻,3.3 V 逻辑可以直接驱动,不需要电平转换。但 GND 必须和 NUCLEO 连在一起,否则 IN1–IN4 没有参考地,电机可能乱抖。

2.3 外部晶振:24 MHz

image.png

图 5:HSE 使用 24 MHz 晶振(X1),LSE 为 32.768 kHz(本例不使用)

HSE 接的是 24 MHz 晶振(X1,OSC_IN / OSC_OUT),后面的时钟树就以此为输入。

三、CubeMX 外设配置

3.1 调试接口:Serial Wire

在左侧 Trace and Debug → DEBUG(旧版本在 System Core → SYS)中,把 Debug 设为 Serial Wire。

image.png

图 6:Debug 选择 Serial Wire,保留 PA13 / PA14 的 SWD 功能

[!tip] 为什么第一步就配调试口 一是把 PA13 / PA14 留给 SWD,避免下载、调试连不上;二是 PB3 / PB4 默认是 JTAG 的 JTDO / NJTRST,把 Debug 选成 Serial Wire 后 JTAG 不启用,这两个脚才归 GPIO。要是选了 JTAG,CubeMX 不会允许把它们配成输出。这个实验的四根控制线正好占了两根 JTAG 脚,调试口必须先定下来。

3.2 时钟树:24 MHz → 250 MHz

在 System Core → RCC 中,把 High Speed Clock (HSE) 设为 Crystal/Ceramic Resonator,Low Speed Clock (LSE) 保持 Disable;再切到 Clock Configuration,将 HSE 选为 PLL1 的输入源,按图设置分频与倍频。

image.png

图 7:HSE 24 MHz 作为 PLL 源,SYSCLK 250 MHz,AHB 不分频

STM32H533 的最高主频为 250 MHz,这里直接拉满:SYSCLK = HCLK = 250 MHz,AHB Prescaler = /1。若右上角出现 Resolve Clock Issues 红标,按提示逐项检查未使能的时钟源即可。

GPIO 本身不挑时钟,但“多久走一步”挑:Thread::Sleep(2) 的 2 ms 来自工程的毫秒时基,而时基由时钟树分频得到。主频或分频配错,2 ms 就不是 2 ms,转速会整体偏。所以先把 250 MHz 定下来,再谈步进间隔。

3.3 GPIO:PC8 / PB3 / PB4 / PB5 推挽输出

回到 Pinout & Configuration,在芯片图上依次点击 PC8、PB3、PB4、PB5,都选 GPIO_Output;再在 System Core → GPIO 里按下表统一配置,并把 User Label 改成 IN1–IN4:

参数取值
GPIO output levelLow
GPIO modeOutput Push Pull
GPIO Pull-up/Pull-downNo pull-up and no pull-down
Maximum output speedLow
User LabelIN1 / IN2 / IN3 / IN4

image.png

图 8:PC8 / PB3 / PB4 / PB5 分别标为 IN1–IN4,全部设为推挽输出,初始低电平

几个取值都是有理由的:

  • 初始电平 Low:ULN2003 输入低,对应输出关断,四相都不通电。上电不会自己转,也不会发热;

  • 无上下拉:推挽输出由内部 MOS 直接驱动,不需要上下拉,留空即可,少一个可能影响复位后电平的因素;

  • Low 速度档:步进节拍是毫秒级,低速档的边沿完全够用,还能少一点开关噪声;

  • User Label:CubeMX 会生成 IN1_Pin / IN1_GPIO_Port 这样的宏,后面写代码不用记引脚号。

[!note] 上电时电机可能抖一下PB4 复位后是 NJTRST,内部带上拉;在 motor.Initialize() 把四相拉低之前,ULN2003 的某一路可能短暂导通。听到上电瞬间“咯噔”一声是正常的,初始化完成后就释放了。

四、生成 CMake 工程

4.1 Project Manager 设置

切到 Project Manager,按下表填写:

配置项取值
Project Namemotor
Project Location全英文路径,例如 E:\stm32cubemx\todo
Application StructureAdvanced
Toolchain / IDECMake
Code GeneratorCopy only the necessary library files
Generated files勾选 Generate peripheral initialization as a pair of '.c/.h' files per peripheral
勾选 Keep User Code when re-generating

image.png

图 9:工程名、路径、CMake 工具链与代码生成选项

[!warning] 路径不要带中文 CubeMX 生成的工程路径请保持全英文,中文路径在 CMake / GCC 环节很容易出现难以排查的报错。

4.2 生成代码

点击右上角的 GENERATE CODE,等待生成完成后选择 Open Project,即可用 VSCode 打开工程。

如果是在加速度计那篇的工程上继续,PA5 心跳灯、TIM3_CH2、I2C1 都已经在,跳过新建,直接从 3.3 节补四个 GPIO 即可。

五、用 C++ 封装步进电机

5.1 先定“拍”:八拍相序

步进电机的“相序”就是四根线的通电顺序。28BYJ-48 是四相单极性,常见两种驱动方式:

  • 整步(4 拍):A → B → C → D,每次只给一相通电,控制最简单,但步距大、顿挫明显,单相导通的转矩也偏小;

  • 半步(8 拍):A → AB → B → BC → C → CD → D → DA,单相、双相交替,步距减半,双相拍把转矩补回来,运行平顺得多。

本文用八拍。把 IN1–IN4 的高低电平排成表,就是代码里的 kStepTable:

拍IN1IN2IN3IN4导通相
11000A
21100A + B
30100B
40110B + C
50010C
60011C + D
70001D
81001D + A

八拍的首尾必须接上:第 8 拍 DA 之后回到第 1 拍 A,循环才是连续的;不能在第 7 拍 D 之后直接跳回 A,那样中间少一个半步,转到这一格会顿一下。相序反过来走(8 → 1),电机就反转;两相拍转矩大、单相拍转矩小,所以八拍会有轻微力矩波动,但比整步平顺得多。

5.2 类的设计

驱动只依赖 GPIO 抽象,不碰 HAL 寄存器;四根线的“拍”做成一张编译期常量表。头文件如下:

#pragma once

#include <array>
#include <cstddef>
#include <cstdint>

#include "gpio.hpp"

namespace LibXR
{

/**
 * @brief Four-phase stepper motor driven with an eight-step half-step sequence.
 */
class StepperMotor final
{
 public:
  static constexpr size_t kPhaseCount = 8;
  static constexpr size_t kCoilCount = 4;
  static constexpr int32_t kStepsPerRevolution = 4096;

  using Phase = std::array<uint8_t, kCoilCount>;
  using PhaseTable = std::array<Phase, kPhaseCount>;

  /**
   * @brief The phase order is IN1, IN2, IN3, IN4.
   */
  inline static constexpr PhaseTable kStepTable = {{{{0x01, 0x00, 0x00, 0x00}},
                                                     {{0x01, 0x01, 0x00, 0x00}},
                                                     {{0x00, 0x01, 0x00, 0x00}},
                                                     {{0x00, 0x01, 0x01, 0x00}},
                                                     {{0x00, 0x00, 0x01, 0x00}},
                                                     {{0x00, 0x00, 0x01, 0x01}},
                                                     {{0x00, 0x00, 0x00, 0x01}},
                                                     {{0x01, 0x00, 0x00, 0x01}}}};

  StepperMotor(GPIO& in1, GPIO& in2, GPIO& in3, GPIO& in4);

  /** @brief Configure the four coil pins as push-pull outputs and release them. */
  ErrorCode Initialize();

  /** @brief Apply one phase from the eight-step table. */
  void SetPhase(uint8_t index);

  /**
   * @brief Move one half-step.
   * @param direction Positive for forward, negative for reverse.
   * @param delay_ms Delay after the phase is applied.
   */
  void Step(int direction, uint32_t delay_ms);

  /** @brief Release all four coils. */
  void Stop();

  /**
   * @brief Move a signed number of half-steps, then release the coils.
   */
  void Rotate(int32_t steps, uint32_t delay_ms);

  /**
   * @brief Move by a signed angle in degrees, then release the coils.
   * @note The motor is treated as 4096 half-steps per output-shaft revolution.
   */
  void RotateAngle(float angle, uint32_t delay_ms);

  uint8_t StepIndex() const { return step_index_; }

 private:
  std::array<GPIO*, kCoilCount> coils_;
  uint8_t step_index_ = 0;
};

}  // namespace LibXR

几点说明:

  • 构造函数收 GPIO& 而不是 STM32GPIO&,驱动只认抽象接口;换平台、换引脚实现,这个类不用动;

  • kStepsPerRevolution = 4096 是输出轴一圈的半步数,换电机只改这个常量(以及相序表);

  • kStepTable 用 inline static constexpr,编译期展开,不占 RAM;

  • 线圈用指针数组保存,StepperMotor 不拥有 GPIO 对象,所以四个 GPIO 必须比它活得久;

  • Initialize() 返回 ErrorCode,配置失败时先 Stop() 再上报,不会留下半开的线圈;

  • StepIndex() 留给调试器看当前走到第几拍。

5.3 初始化与换相

#include "stepper_motor.hpp"

#include <cmath>
#include <limits>

#include "thread.hpp"

namespace LibXR
{

StepperMotor::StepperMotor(GPIO& in1, GPIO& in2, GPIO& in3, GPIO& in4)
    : coils_{&in1, &in2, &in3, &in4}
{
}

ErrorCode StepperMotor::Initialize()
{
  const GPIO::Configuration output_config{
      GPIO::Direction::OUTPUT_PUSH_PULL,
      GPIO::Pull::NONE,
  };

  for (GPIO* coil : coils_)
  {
    const ErrorCode result = coil->SetConfig(output_config);
    if (result != ErrorCode::OK)
    {
      Stop();
      return result;
    }
  }

  Stop();
  return ErrorCode::OK;
}

void StepperMotor::SetPhase(uint8_t index)
{
  step_index_ = static_cast<uint8_t>(index % kPhaseCount);
  const Phase& phase = kStepTable[step_index_];
  for (size_t coil_index = 0; coil_index < kCoilCount; ++coil_index)
  {
    coils_[coil_index]->Write(phase[coil_index] != 0);
  }
}

void StepperMotor::Step(int direction, uint32_t delay_ms)
{
  if (direction > 0)
  {
    step_index_ = static_cast<uint8_t>((step_index_ + 1U) % kPhaseCount);
  }
  else if (direction < 0)
  {
    step_index_ = static_cast<uint8_t>((step_index_ + kPhaseCount - 1U) %
                                       kPhaseCount);
  }
  else
  {
    return;
  }

  SetPhase(step_index_);
  Thread::Sleep(delay_ms);
}

void StepperMotor::Stop()
{
  for (GPIO* coil : coils_)
  {
    coil->Write(false);
  }
}

Initialize() 把四根线都配成推挽、无上下拉,任何一根失败就先把已经配好的线圈关掉再返回。CubeMX 其实已经配过一遍,这里再配一次,是为了让驱动自己保证状态——换到别的工程、或者别人改了 .ioc,这个类仍然能工作。

SetPhase() 只做一件事:取表里的一拍,逐根写电平。% kPhaseCount 让调用者可以传任意下标。

Step() 是核心:+1 正转、-1 反转,取模回绕;0 直接返回。先换相、再 Thread::Sleep(delay_ms),让线圈有时间把转子吸到位;如果把延时放在换相之前,第一步会停在上一拍,看起来就像“反应慢半拍”。

Stop() 把四根线全部拉低,ULN2003 关断,线圈不再通电。

5.4 走角度:Rotate 与 RotateAngle

文件后半部分仍在上面的 namespace LibXR 里,补上两个“走角度”的函数:

void StepperMotor::Rotate(int32_t steps, uint32_t delay_ms)
{
  const int direction = steps >= 0 ? 1 : -1;
  int64_t count = steps;
  if (count < 0)
  {
    count = -count;
  }

  for (int64_t index = 0; index < count; ++index)
  {
    Step(direction, delay_ms);
  }

  Stop();
}

void StepperMotor::RotateAngle(float angle, uint32_t delay_ms)
{
  if (!std::isfinite(angle))
  {
    return;
  }

  const float steps_float = angle * static_cast<float>(kStepsPerRevolution) /
                            360.0F;
  int32_t steps = 0;
  if (steps_float >= static_cast<float>(std::numeric_limits<int32_t>::max()))
  {
    steps = std::numeric_limits<int32_t>::max();
  }
  else if (steps_float <= static_cast<float>(std::numeric_limits<int32_t>::min()))
  {
    steps = std::numeric_limits<int32_t>::min();
  }
  else
  {
    const float rounded = steps_float + (steps_float >= 0.0F ? 0.5F : -0.5F);
    steps = static_cast<int32_t>(rounded);
  }

  Rotate(steps, delay_ms);
}

}  // namespace LibXR

Rotate() 把有符号步数拆成“方向 + 绝对值”。int64_t count = steps 是为了处理 INT32_MIN:-(-2147483648) 在 32 位里有符号溢出,先升到 64 位就安全了。走完调用 Stop()。

RotateAngle() 做四件事:

  1. std::isfinite() 挡掉 NaN / inf,避免后面算出无意义的步数;

  2. 角度乘 4096 / 360 换成半步数;

  3. 超出 int32_t 范围就钳位——把越界的浮点数直接 static_cast 成整数是未定义行为;

  4. 加 ±0.5 再截断,实现“四舍五入、远离零”,最后交给 Rotate()。

180° 正好是 2048 步,没有舍入误差;任意角度最多差半步,约 0.044°。当然这是理论分辨率,减速箱的背隙和丢步才是实际误差的大头。

[!note] 为什么最后要 Stop() 步进电机静止时如果保持某一相导通,会有保持转矩,但线圈一直通着约 100 mA,电机和驱动板都会发热。Rotate() 结束就 Stop(),输出轴用手能拧动,适合“转到位就停”的场合。如果要抵抗外力(云台、阀门),把 Stop() 去掉,让最后一拍保持——代价是发热和耗电。

5.5 在 app_main 里转 180°

先把头文件放进用户代码区,再把四个 GPIO 和电机对象定义出来:

/* User Code Begin 1 */
#include "stepper_motor.hpp"
/* User Code End 1 */
// GPIO
static STM32GPIO PA5(GPIOA, GPIO_PIN_5);
static STM32GPIO IN1(IN1_GPIO_Port, IN1_Pin);
static STM32GPIO IN2(IN2_GPIO_Port, IN2_Pin);
static STM32GPIO IN3(IN3_GPIO_Port, IN3_Pin);
static STM32GPIO IN4(IN4_GPIO_Port, IN4_Pin);

static StepperMotor motor(IN1, IN2, IN3, IN4);

初始化并转半圈:

/* User Code Begin 3 */
PA5.SetConfig({.direction = GPIO::Direction::OUTPUT_PUSH_PULL,
               .pull = GPIO::Pull::NONE});   // 心跳灯,沿用前几篇

if (motor.Initialize() != ErrorCode::OK)
{
  Error_Handler();
}

motor.RotateAngle(180.0F, 2);   // 2 ms 一拍,转半圈

几个细节:

  • IN1_Pin 这些宏来自 3.3 的 User Label,CubeMX 生成在 main.h;如果标签没改,宏名会是 PC8_Pin 之类,按实际生成的改;

  • StepperMotor 只存指针,四个 GPIO 对象要定义成 static,活到程序结束;

  • 2 是每个半步的延时(ms);180° 是 2048 步,约 4.1 s;

  • 这一句是阻塞的:转完才往下走,所以心跳灯要等电机停稳之后才开始闪。

[!tip] 代码写在 USER CODE 区app_main.cpp 由 libxr gen 生成,只有夹在 /* USER CODE BEGIN ... */ 与 /* USER CODE END ... */ 之间的内容才会保留;改 .ioc 重新生成时,电机对象和调用都别写到区域外面。

把 stepper_motor.hpp 和 stepper_motor.cpp 放进 User/:本系列的工程模板会自动把 User/*.cpp glob 进构建,不用手动改 CMakeLists.txt;如果用的是纯 CubeMX 工程,记得把 stepper_motor.cpp 加到 target_sources() 里。

想让电机来回扫,可以把主循环写成这样:

while (true)
{
  motor.RotateAngle(180.0F, 2);
  Thread::Sleep(500);
  motor.RotateAngle(-180.0F, 2);
  Thread::Sleep(500);
}

代价是主循环被电机独占,转动期间心跳灯和别的任务都得等着。这也是后面要把换相交给定时器中断的原因。

六、编译、下载与验证

6.1 编译与下载

在 VSCode 中装好 CMake Tools 与 STM32 扩展,打开工程后先 Configure、再 Build(终端里等价于 cmake --preset Debug 加 cmake --build --preset Debug)。随后打开 运行和调试,选择 STM32Cube: STM32 Launch STLink GDB Server(NUCLEO 板载 ST-Link),按 F5 下载并启动调试。

image.png

图 10:在 VSCode 的运行和调试中选择 ST-Link GDB Server

6.2 看现象

下载成功后,应该看到:

  1. 上电后电机先转半圈,耗时约 4 秒;

  2. ULN2003 板上的小 LED 一路或两路依次亮灭,节奏均匀;

  3. 转完 Stop() 释放线圈,用手能拧动输出轴,电机不烫;

  4. 随后板载绿色 LED 继续以 1 Hz 闪烁,表示主循环还活着。

听到均匀细密的“嗡嗡”声、看到轴在转,说明相序、接线、延时都对上了。

6.3 算一算:角度、步数与时间

4096 个半步对应一圈,所以角度和步数的关系是 steps = angle × 4096 / 360。按 2 ms 一拍算:

角度半步数耗时(2 ms/步)
30°341≈ 0.68 s
45°512≈ 1.02 s
90°1024≈ 2.05 s
180°2048≈ 4.10 s
360°4096≈ 8.19 s

一圈 8.19 s,折算转速约 7.3 rpm。实测会比理论值略长,因为每一步还有 GPIO 写入和函数调用开销;要精确计时,用逻辑分析仪量 IN1 的周期,或者干脆用定时器中断代替 Thread::Sleep()。

28BYJ-48 的可用转速通常不超过 15 rpm,2 ms 已经比较稳妥。把延时降到 1 ms 转速翻倍,但转矩下降、容易丢步,带负载时更明显。

6.4 转不动、转不准怎么查

现象优先检查
完全不动,ULN2003 灯也不亮四根 IN 是否接对;motor.Initialize() 是否返回 OK;5V / GND 是否接上
灯亮但电机不转电机五芯插头是否插反、插歪;公共端是否接到 5V;程序是否一直停在 Stop()
原地抖动、嗡嗡响相序错(重点查 IN3 / IN4 是否插成 D4 / D5);某一相断线;延时太短
转向相反交换 IN1 / IN2,或把角度写成负数
转一会儿丢步、位置对不上负载太大;2 ms 太快;5 V 跌落;减速箱背隙
电机或驱动板发烫、输出轴拧不动Stop() 没调用,线圈长期通电;只调了 SetPhase() 没有 Step(),也会停在某一拍
下载 / 调试连不上Debug 没选 Serial Wire,PB3 / PB4 被 JTAG 占用;或代码里把 PA13 / PA14 配成了普通 GPIO

6.5 想更准、更快、更稳

  • 更准:理论上每半步 0.088°,但 28BYJ-48 的减速箱背隙在 1° 量级,正反转会有回差,开环丢步还会累积。要绝对位置,得加限位开关或编码器,上电先回零;

  • 更快:不要一上来就用最高速。起步慢、匀速快、到点慢,做一条加减速曲线,丢步会少很多;

  • 更稳:ULN2003 的 VCC 并一个 100 µF 电解加 0.1 µF 瓷片,电机线绞在一起,电源线和信号线分开走,能减少复位和丢步;

  • 更细:ULN2003 只能做整步 / 半步,想要细分(microstep)得换 A4988 / DRV8825 加双极步进电机。

七、小结

这次从“开关一个引脚”走到“按节拍指挥四根线”,步进电机把 GPIO 的时序要求又抬高了一截:

  1. CubeMX 选型建工程,处理 TrustZone 与 ICACHE 默认配置;

  2. 从原理图确认四根控制线是 PC8 / PB3 / PB4 / PB5,对应 CN9 的 D2 / D3 / D5 / D4,5 V 与地取自 CN6;

  3. 配置 SWD(顺便解放 PB3 / PB4)、HSE 24 MHz → 250 MHz 时钟树,四个引脚设为推挽输出、初始低电平;

  4. 生成 CMake 工程,用 StepperMotor 收拢八拍相序、单步、连续步进和角度换算;

  5. VSCode + ST-Link 下载,电机转出 180°(约 4.1 s),随后释放线圈,心跳灯继续闪。





关键词: 2026年第     基于     边缘     电机     状态     检测     ---    

共1条 1/1 1 跳转至页

回复

匿名不能发帖!请先 [ 登陆 注册 ]