平台: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。

图 1:在 MCU/MPU Selector 中选中 STM32H533RET6(对应 NUCLEO-H533RE)
1.2 处理 TrustZone 提示
STM32H5 系列带 Arm TrustZone,新建工程时会询问是否启用安全隔离。本例不涉及安全分区,选择 without TrustZone activated,点击 OK。

图 2:TrustZone 选择“不启用”
1.3 保留 ICACHE / MPU 默认配置
紧接着会提示是否为 ICACHE 和 MPU 访问 OTP / RO 区域应用默认配置,直接点 Yes 即可,省去手动配置,性能也更好。

图 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

图 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 取。
接线按下表来:
| IN1 | PC8 | CN9 D2 | A |
| IN2 | PB3 | CN9 D3 | B |
| IN3 | PB4 | CN9 D5 | C |
| IN4 | PB5 | CN9 D4 | D |
| VCC | 5V | CN6 5V | 电机电源 |
| GND | GND | CN6 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

图 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。

图 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 的输入源,按图设置分频与倍频。

图 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 level | Low |
| GPIO mode | Output Push Pull |
| GPIO Pull-up/Pull-down | No pull-up and no pull-down |
| Maximum output speed | Low |
| User Label | IN1 / IN2 / IN3 / IN4 |

图 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 Name | motor |
| Project Location | 全英文路径,例如 E:\stm32cubemx\todo |
| Application Structure | Advanced |
| Toolchain / IDE | CMake |
| Code Generator | Copy 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 |

图 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:
| 1 | 1 | 0 | 0 | 0 | A |
| 2 | 1 | 1 | 0 | 0 | A + B |
| 3 | 0 | 1 | 0 | 0 | B |
| 4 | 0 | 1 | 1 | 0 | B + C |
| 5 | 0 | 0 | 1 | 0 | C |
| 6 | 0 | 0 | 1 | 1 | C + D |
| 7 | 0 | 0 | 0 | 1 | D |
| 8 | 1 | 0 | 0 | 1 | D + 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 LibXRRotate() 把有符号步数拆成“方向 + 绝对值”。int64_t count = steps 是为了处理 INT32_MIN:-(-2147483648) 在 32 位里有符号溢出,先升到 64 位就安全了。走完调用 Stop()。
RotateAngle() 做四件事:
std::isfinite() 挡掉 NaN / inf,避免后面算出无意义的步数;
角度乘 4096 / 360 换成半步数;
超出 int32_t 范围就钳位——把越界的浮点数直接 static_cast 成整数是未定义行为;
加 ±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 下载并启动调试。

图 10:在 VSCode 的运行和调试中选择 ST-Link GDB Server
6.2 看现象
下载成功后,应该看到:
上电后电机先转半圈,耗时约 4 秒;
ULN2003 板上的小 LED 一路或两路依次亮灭,节奏均匀;
转完 Stop() 释放线圈,用手能拧动输出轴,电机不烫;
随后板载绿色 LED 继续以 1 Hz 闪烁,表示主循环还活着。
听到均匀细密的“嗡嗡”声、看到轴在转,说明相序、接线、延时都对上了。
6.3 算一算:角度、步数与时间
4096 个半步对应一圈,所以角度和步数的关系是 steps = angle × 4096 / 360。按 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 的时序要求又抬高了一截:
CubeMX 选型建工程,处理 TrustZone 与 ICACHE 默认配置;
从原理图确认四根控制线是 PC8 / PB3 / PB4 / PB5,对应 CN9 的 D2 / D3 / D5 / D4,5 V 与地取自 CN6;
配置 SWD(顺便解放 PB3 / PB4)、HSE 24 MHz → 250 MHz 时钟树,四个引脚设为推挽输出、初始低电平;
生成 CMake 工程,用 StepperMotor 收拢八拍相序、单步、连续步进和角度换算;
VSCode + ST-Link 下载,电机转出 180°(约 4.1 s),随后释放线圈,心跳灯继续闪。
我要赚赏金
