今天设计的同事突然找我帮忙,说有个产品需要旋转着拍摄各个角度的效果,问我有没有合适的展示盘,实际上没有~ 他说能否做一个。我说:

于是中午午饭和咖啡就到位了。
为了快速实现,我赶紧翻找了一下步进电机,还真有个之前测试用的。
于是,翻了翻百宝箱,找到st-link和一个已经开始锈蚀的stm32f103c8t6,开始接线。
这里省略10000字,特别的简单待会儿文档里面写。
下面就是整个配置和调试的过程。全程半小时,同时让3D设计的同事设计了一个圆盘。
年轻人动作就是快,不到一个小时就给我送过来了。
在模型还在打印的时候,我就开始搭建项目框架了。
顺便简单记录了一下操作过程中需要注意的内容,顺便回顾一下以前学习stm32的时候的一些小技巧。
下面开始:
01 - 环境搭建(从零开始)
本章目标:在犀牛派x1上的 Linux 系统上装好三样东西——交叉编译器、烧录工具、Make,
并验证它们都能正常工作。
本教程以 Ubuntu/Debian 系(包括 AidLux)为例。整个过程只需要一条 apt 命令,
不需要访问 ST 官网注册下载任何东西。
1. 理解我们需要什么
在电脑上给 STM32 写程序,和给电脑本身写程序不一样:
-
你的电脑是 ARM64/x86 处理器,STM32F103 是 Cortex-M3 处理器,
两者的机器码不通用。 -
所以我们需要一个"交叉编译器":在电脑上运行、但生成 STM32 机器码的
gcc,名字叫arm-none-eabi-gcc。arm= 目标 CPU 架构none= 目标上没有操作系统(裸机)eabi= 嵌入式应用二进制接口(一套调用规范)
-
编译出来的
.bin文件要送进芯片,需要烧录工具通过 ST-Link 写进去,
我们用开源的stlink-tools(提供st-flash、st-info等命令)。
2. 一键安装
sudo apt update
sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi stlink-tools make
各包的作用:
| 软件包 | 提供的命令 | 用途 |
|---|---|---|
| gcc-arm-none-eabi | arm-none-eabi-gcc | 交叉编译器 |
| binutils-arm-none-eabi | arm-none-eabi-objcopy 等 | 把 ELF 转成 BIN/HEX |
| stlink-tools | st-flash, st-info, st-util | 通过 ST-Link 烧录和调试 |
| make | make | 读取 Makefile 自动完成编译 |
注意:
gcc-arm-none-eabi会同时安装一个比较大的 newlib 运行库
(约几百 MB),属于正常现象,不要panic哈哈。
3. 验证安装
逐个运行下面的命令,都应该能打印出版本号:
arm-none-eabi-gcc --version # 预期输出类似 10.3.1 或更高
make --version # 预期 GNU Make 4.x
st-flash --version # 预期 v1.7.0 或更高
如果某条命令提示 command not found,说明对应软件包没装上,
回到第 2 步检查 apt 的输出。
4. 验证 ST-Link 被系统识别
把 ST-Link 插上 USB,运行:
lsusb | grep -i st
预期看到一行:
Bus 001 Device 005: ID 0483:3748 STMicroelectronics ST-LINK/V2
0483:3748 就是 ST-Link V2 的 USB 厂商/产品 ID。看不到的话检查 USB 线
(很多便宜 USB 线只有电源线没有数据线!)和 USB 口。
5. 验证 ST-Link 与 STM32 芯片的通信
按 02-硬件接线.md 接好 SWD 四根线后运行:
st-info --probe
正常会输出:
Found 1 stlink programmers
version: V2J27S6 <- ST-Link 固件版本
serial: 130057... <- 序列号
flash: 65536 (pagesize: 1024) <- 识别到 64KB Flash
sram: 20480 <- 识别到 20KB SRAM
chipid: 0x0410 <- 0x0410 = STM32F103 中容量型
descr: F1xx Medium-density
看到 chipid: 0x0410 就说明电脑 → ST-Link → STM32 整条链路完全正常,
可以直接进入烧录环节了。
6.(可选)openocd —— 另一个烧录工具
stlink-tools 之外还有一个更通用的开源工具 OpenOCD,功能更强(支持
GDB 单步调试),本项目不需要它,但装上备用无妨:
sudo apt install openocd
用 openocd 烧录本项目的等价命令:
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \
-c "program build/stepper.elf verify reset exit"
7.(可选)GDB 在线调试
stlink-tools 自带的 st-util 可以开一个 GDB 服务器,
配合 arm-none-eabi-gdb 就能单步调试芯片上正在跑的程序:
# 终端 1:
st-util # 监听 4242 端口
# 终端 2:
arm-none-eabi-gdb build/stepper.elf
(gdb) target extended-remote localhost:4242
(gdb) load # 烧录
(gdb) break main
(gdb) continue # 运行到 main 停下
初学者暂时用不到,先混个眼熟即可。
02 - 硬件接线详解
这里需要把每一块硬件、每一根线搞清楚。接线错误是初学者烧板子的第一大原因,
请务必逐条对照检查后再上电。
1. 认识你的硬件
STM32F103C8T6 最小系统板
板子一侧有 4 根一组的排针(通常在短边),丝印从上往下一般是:
3.3V DIO(或SWDIO) DCLK(或SWCLK) GND
这就是 SWD 调试口,烧录就走这里。
ST-Link V2
金属小U盘或小白条形状,正面 10 个焊盘/排针,丝印通常是两排:
RST SWCLK SWIM SWDIO GND
3.3V 5V 5V 3.3V GND (不同卖家顺序可能不同,以你手上的丝印为准!)
市面上 ST-Link 外壳至少有三四种引脚排列,一定以实物丝印为准,
不要照抄任何一张网图。
ULN2003 驱动板 + 28BYJ-48 电机
驱动板上有:IN1~IN4 四个信号输入针、+/- 两个电源针、
4 个 LED 指示灯、一个白色电机插座。电机插头有防呆,直接插到白色插座。
2. 第一部分:ST-Link ↔ STM32(烧录/调试用)
| ST-Link | STM32 板 | 说明 |
|---|---|---|
| SWDIO | DIO(SWDIO) | 数据线 |
| SWCLK | DCLK(SWCLK) | 时钟线 |
| 3.3V | 3.3V | 给开发板供电 |
| GND | GND | 共地 |
要点:
- SWDIO 和 SWCLK 不要接反。接反的典型症状是
st-info --probe识别不到
芯片(chipid 0x0000 或直接报错),对调这两根线即可,一般不会烧东西。 - 用 ST-Link 的 3.3V 给板子供电时,不要再同时用 USB 线给 STM32 板供电,
两路电源选一个。 - 线尽量短(15cm 以内最好),SWD 对线长不敏感但太长会不稳定。
3. 第二部分:STM32 ↔ ULN2003 驱动板(电机控制用)
| STM32 | ULN2003 驱动板 | 说明 |
|---|---|---|
| PA1 | IN1 | 信号线 1 |
| PA2 | IN2 | 信号线 2 |
| PA3 | IN3 | 信号线 3 |
| PA4 | IN4 | 信号线 4 |
| GND | 电源 - |
必须共地! |
驱动板电源:
| 电源 | 接法 |
|---|---|
驱动板 + |
接 5V(可用 ST-Link 的 5V 引脚,或独立 USB 5V) |
驱动板 - |
接 GND,并且要和 STM32 的 GND 连在一起 |
"共地"是最容易被忽略也最关键的一条:IN1~IN4 的 3.3V 信号电平
是以 STM32 的 GND 为参考的,如果驱动板和 STM32 不共地,驱动板无法正确
识别信号,电机会乱抖或不转。
28BYJ-48 额定电压是 5V。不要图省事把驱动板 + 接到 3.3V——
能转但力矩很小、容易失步。
电机插头插到驱动板白色插座(防呆设计,插反插不进)。
4. 完整接线图
USB
│
┌────┴─────┐
│ ST-Link │
│ V2 │
└─┬──┬──┬──┘
│ │ │ 3.3V ──────────────► STM32 板 3.3V
│ │ └ SWDIO ─────────────► STM32 板 DIO
│ └─── SWCLK ─────────────► STM32 板 DCLK
└────── GND ──┬────────────► STM32 板 GND
│
STM32 板 PA1 ────┼────────────► 驱动板 IN1
STM32 板 PA2 ────┼────────────► 驱动板 IN2
STM32 板 PA3 ────┼────────────► 驱动板 IN3
STM32 板 PA4 ────┼────────────► 驱动板 IN4
└────────────► 驱动板 电源- (共地)
ST-Link 5V ──────────────────► 驱动板 电源+
│
28BYJ-48 电机(白色插座)
5. 上电自检顺序
- 先不接电机,只接 ST-Link ↔ STM32,运行
st-info --probe,
确认能识别芯片(说明烧录链路 OK)。 - 烧录程序(
make flash)。 - 接上驱动板和电机上电。此时驱动板上 4 个 LED 应该按顺序轮流闪烁,
说明信号输出正常;电机应正转一圈、停一秒、反转一圈。 - 如果 LED 正常轮流闪但电机不转/乱抖 → 问题在电机侧供电或插头
03 - 编译与烧录
这里只要搞明白 make 和 make flash 背后发生了什么,以及每个命令的用法,就可以顺利开始折腾了。
1. 编译:make
在项目根目录(Makefile 所在目录)运行:
make
预期输出:
mkdir -p build
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -O2 -g ... -c src/main.c -o build/main.o
arm-none-eabi-gcc ... -c startup_stm32f103c8tx.s -o build/startup.o
arm-none-eabi-gcc ... -T STM32F103C8Tx_FLASH.ld ... -o build/stepper.elf
arm-none-eabi-objcopy -O binary build/stepper.elf build/stepper.bin
text data bss dec hex filename
628 0 4 632 278 build/stepper.elf
发生了什么?
编译分四步:
- 编译 C 源码:
main.c→main.o(目标文件,机器码但还没拼起来) - 编译汇编启动文件:
startup_stm32f103c8tx.s→startup.o - 链接:把所有
.o按链接脚本STM32F103C8Tx_FLASH.ld规定的地址
拼成完整的stepper.elf(含调试信息) - 转换格式:
objcopy把 ELF 转成纯二进制stepper.bin——
烧录进 Flash 的就是它
最后一行 size 输出告诉你程序占了多少空间:
text(代码+只读数据)628 字节 → 占 Flash,64KB 绰绰有余data(有初始值的全局变量)0 字节 → Flash 存初值、RAM 存值bss(无初始值的全局变量)4 字节 → 只占 RAM
编译产物
| 文件 | 用途 |
|---|---|
build/stepper.elf |
完整固件,调试(GDB)时用 |
build/stepper.bin |
纯二进制,st-flash 烧录用 |
build/stepper.map |
每个函数/变量被放在了哪个地址,排查问题用 |
修改代码后
改完 src/main.c 直接再 make 即可,Makefile 会自动判断哪些文件变了、
只重新编译变化的部分。想彻底重来:
make clean && make
2. 烧录:make flash
make flash
它实际执行的是:
st-flash write build/stepper.bin 0x08000000
两个关键概念:
0x08000000:STM32 的 Flash 起始地址,这是芯片硬件规定的,
所有 STM32 程序都从这个地址烧起。链接脚本里的ORIGIN = 0x08000000
必须和它一致。- 烧录流程:st-flash 会自动 擦除目标页 → 写入 → 校验,
最后看到Flash written and verified!就是成功,芯片自动复位运行。
成功输出示例:
st-flash 1.7.0
INFO common.c: F1xx Medium-density: 20 KiB SRAM, 64 KiB flash ...
INFO common.c: Attempting to write 628 bytes to stm32 address: 134217728 (0x8000000)
INFO common.c: Flash page at addr: 0x08000000 erased
INFO common.c: Starting Flash write for VL/F0/F3/F1_XL
1/1 pages written
INFO common.c: Starting verification of write complete
INFO common.c: Flash written and verified! jolly good!
3. 其他常用命令
擦除芯片
make erase # 等价于 st-flash erase
把整块 Flash 清空(程序消失,芯片上电什么都不干)。
复位
make reset # 等价于 st-flash reset
让芯片从头重新运行(相当于按板子上的 RESET 键)。
检查连接
st-info --probe
任何时候怀疑线松了、板子没上电,先跑这个。能读出 chipid: 0x0410
就说明硬件链路没问题,问题在别处。
4. 烧不进去?按顺序排查
st-info --probe能否识别芯片?不能 → 查接线去,有时候杜邦线很坑的,尽量用好的杜邦线,例如硅胶那种,5毛-1块钱一根的那种awg 系列的线缆。- 报
Couldn't open ST-Link/ 权限错误 → 去检查“权限问题"。 - 提示芯片受保护(
read protection)→ 遇到读保护的话看后面内容。 - 接线全对但时好时坏 → 换短杜邦线、确认板子供电正常
(板子上的电源 LED 是否亮)。
5.(进阶)用 OpenOCD 烧录
如果装了 openocd(sudo apt install openocd),等价烧录命令:
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \
-c "program build/stepper.elf verify reset exit"
-f interface/stlink.cfg:告诉 openocd 用的是 ST-Link-f target/stm32f1x.cfg:目标是 STM32F1 系列program ... verify reset exit:写入 → 校验 → 复位 → 退出
效果和 make flash 完全一样,二选一即可。
我肯定选择简单有效的咯。
04 - 代码讲解与步进电机原理
这里我们两部分来讲解:先讲电机为什么会转,再逐段讲解 src/main.c,
最后教你怎么改代码实现自己的想法。
第一部分:步进电机原理
28BYJ-48 是什么电机
它是五线四相减速步进电机,内部是"电机本体 + 减速齿轮组":
- 电机本体有 4 个线圈(相),对应驱动板的 IN1~IN4
- 内部齿轮减速比约 64:1,所以本体转 64 圈,输出轴才转 1 圈
- 本体一圈 32 个半步 → 输出轴一圈 = 32 × 64 = 2048 步(半步模式下
常用资料写作 4096,因为 8 拍序列里每"拍"是半步,实际按 4096 拍一圈计算,
本项目代码取 4096 拍/圈)
市面上的 28BYJ-48 减速比略有公差,实际一圈可能在 4076±若干拍,
对于我同事的这个需求完全够了,所以不纠结了。
它怎么转起来
普通直流电机通电就转,步进电机不同——必须按固定顺序给线圈轮流通电,
电机内部的永磁转子才会一步一步跟着磁场走。每切换一次通电组合,转子走一小步。
8 拍(半步)励磁顺序(本项目采用,力矩和分辨率兼顾):
| 拍 | IN1 | IN2 | IN3 | IN4 | 十六进制 |
|---|---|---|---|---|---|
| 1 | 0x1 | ||||
| 2 | 0x3 | ||||
| 3 | 0x2 | ||||
| 4 | 0x6 | ||||
| 5 | 0x4 | ||||
| 6 | 0xC | ||||
| 7 | 0x8 | ||||
| 8 | 0x9 |
- 正转:按 1→2→3→…→8→1 的顺序循环输出
- 反转:按 8→7→6→…→1→8 的顺序循环输出
- 转速:每拍之间停多久。停 2ms → 一圈约 4096×2ms ≈ 8.2 秒
- 停转:四路全部断电(0x0)。断电后转子不锁死,用手可以拨动
ULN2003 驱动板的作用
STM32 引脚最多输出约 20mA,推不动电机线圈(需要上百 mA)。
ULN2003 是 7 路达林顿管阵列,相当于 7 个"开关放大器":
STM32 给一个小信号,它就接通大电流回路让线圈得电。板上 4 个 LED
指示哪一路正在通电——调程序时看 LED 就能知道输出对不对。
第二部分:main.c 逐段讲解
寄存器地址定义
#define RCC_BASE 0x40021000UL
#define RCC_APB2ENR (*(volatile uint32_t *)(RCC_BASE + 0x18))
STM32 的所有外设(GPIO、定时器、串口……)都通过内存映射寄存器控制:
芯片手册(RM0008)规定某个固定地址对应某个控制开关,C 代码里把这个地址
强制转成指针再解引用,就能读写硬件。
volatile:告诉编译器"这个内存随时会被硬件改变,不许优化掉读写"。
写寄存器不带 volatile 是新手最常见的 bug 之一。- 这些地址不是背出来的,查 **RM0008《STM32F103 参考手册》**的
“Memory map” 和外设章节即可发现操作如此简单~
GPIO 初始化
static void gpio_init(void)
{
RCC_APB2ENR |= RCC_APB2ENR_IOPAEN; /* 给 GPIOA 开时钟 */
for (int pin = 1; pin <= 4; pin++) {
GPIOA_CRL &= ~(0xFUL << (pin * 4));
GPIOA_CRL |= (0x2UL << (pin * 4));
}
}
两条 STM32 的铁律都在这里:
- 用任何外设前必须先开它的时钟。STM32 为了省电,所有外设上电默认
没有时钟、处于"冻结"状态,写它的寄存器没有任何效果。
RCC_APB2ENR的第 2 位控制 GPIOA 的时钟。 - 引脚功能靠配置寄存器选择。
GPIOx_CRL管理 PA0~PA7,每个引脚 4 位:- MODE[1:0]:输入还是输出、输出多快(我们选
10= 输出 2MHz) - CNF[1:0]:输出的话是推挽还是开漏(我们选
00= 通用推挽) - 合起来
0b0010=0x2
- MODE[1:0]:输入还是输出、输出多快(我们选
输出一步(BSRR 寄存器)
static void motor_write(uint8_t pattern)
{
uint32_t bits = (uint32_t)(pattern & 0xF) << 1;
uint32_t mask = (uint32_t)(~pattern & 0xF) << 1;
GPIOA_BSRR = bits | (mask << 16);
}
GPIOx_BSRR 是个巧妙而且骚气的寄存器:
- 低 16 位:写 1 → 对应引脚变高电平(置位 Set)
- 高 16 位:写 1 → 对应引脚变低电平(复位 Reset)
- 写 0 的位:引脚保持不变
好处是一次写操作同时完成"点亮该亮的、熄灭该灭的",不会出现
"先清再置"中间瞬间全灭的状态,电机运行更平滑,最后再录个视频给你们看。
延时(SysTick)
SYSTICK_RVR = (SYSCLK_HZ / 1000UL) - 1UL; /* 8MHz / 1000 = 8000,计 8000 个数 = 1ms */
SysTick 是 Cortex-M3 内核自带的一个 24 位倒计时定时器:装入重载值后
每个时钟周期减 1,减到 0 时把标志位(COUNTFLAG)置 1。代码不停读标志位
(这叫"轮询"),读一次标志就过去 1ms,循环 ms 次就延时了 ms 毫秒。
STM32F103 上电后默认用内部 8MHz 晶振(HSI)作为系统时钟,
不需要任何配置,所以SYSCLK_HZ = 8000000。
(最大能跑到 72MHz,但要配置 PLL,入门阶段用不上。)
主循环
while (1) {
motor_rotate(STEPS_PER_REV, +1); /* 正转一圈 */
motor_stop(); delay_ms(1000);
motor_rotate(STEPS_PER_REV, -1); /* 反转一圈 */
motor_stop(); delay_ms(1000);
}
motor_rotate 里有个细节:static int idx 记住当前停在励磁序列的第几拍,
这样多次调用 motor_rotate 之间磁场是连续的,不会跳步。
第三部分:动手改
改转速
#define STEP_DELAY_MS 2 /* 每步 2ms → 一圈约 8.2 秒 */
- 改大(如 5)→ 变慢,力矩更稳
- 改小(如 1)→ 变快。28BYJ-48 空载极限大约 1ms/步,再小会失步
(转子跟不上磁场,表现为抖动、异响、不走)
转指定角度
8 拍模式一圈 4096 拍,转 N 度就是 4096 * N / 360 拍:
#define STEPS_FOR_ANGLE(deg) ((int32_t)(4096L * (deg) / 360))
motor_rotate(STEPS_FOR_ANGLE(90), +1); /* 正转 90 度 */
用板载 LED(PC13)做心跳指示
最小系统板上 PC13 接了一个 LED(低电平点亮)。在 gpio_init() 里加:
RCC_APB2ENR |= (1UL << 4); /* 使能 GPIOC 时钟 */
GPIOC_CRH &= ~(0xFUL << 20); /* PC13 在 CRH(管 8~15 脚的寄存器)的 (13-8)*4=20 位 */
GPIOC_CRH |= (0x2UL << 20); /* 推挽输出 */
(GPIOC 基地址 0x40011000,CRH 偏移 0x04,同样查 RM0008。)
然后在主循环里翻转 PC13 就能看到 LED 一闪一闪,说明程序活着。
同款代码下载
为了不占用这里的空间,我把代码传到github上了,有需要直接去取。免费的哦。
下一步就是如何把步进电机和3D打印圆固定到一起。
3D设计同事刚夸完他,就发现没脑子,连个孔也不给我怎么固定呢?
于是聪明如我就拆了他的玩具车,用联轴器固定,并用轮子作为托盘。哈哈
【【漂移菌】阿加犀犀牛派X1构建STM32编程环境控制步进电机完成同事拍摄器材的简易搭建~哈哈】 【漂移菌】阿加犀犀牛派X1构建STM32编程环境控制步进电机完成同事拍摄器材的简易搭建~哈哈_哔哩哔哩_bilibili






