【漂移菌】阿加犀犀牛派X1构建STM32编程环境控制步进电机

今天设计的同事突然找我帮忙,说有个产品需要旋转着拍摄各个角度的效果,问我有没有合适的展示盘,实际上没有~ 他说能否做一个。我说:
image
于是中午午饭和咖啡就到位了。
为了快速实现,我赶紧翻找了一下步进电机,还真有个之前测试用的。
于是,翻了翻百宝箱,找到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-flashst-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   (不同卖家顺序可能不同,以你手上的丝印为准!)

:warning: 市面上 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 连在一起

:warning: "共地"是最容易被忽略也最关键的一条: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. 上电自检顺序

  1. 先不接电机,只接 ST-Link ↔ STM32,运行 st-info --probe
    确认能识别芯片(说明烧录链路 OK)。
  2. 烧录程序(make flash)。
  3. 接上驱动板和电机上电。此时驱动板上 4 个 LED 应该按顺序轮流闪烁
    说明信号输出正常;电机应正转一圈、停一秒、反转一圈。
  4. 如果 LED 正常轮流闪但电机不转/乱抖 → 问题在电机侧供电或插头

03 - 编译与烧录

这里只要搞明白 makemake 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

发生了什么?

编译分四步:

  1. 编译 C 源码main.cmain.o(目标文件,机器码但还没拼起来)
  2. 编译汇编启动文件startup_stm32f103c8tx.sstartup.o
  3. 链接:把所有 .o 按链接脚本 STM32F103C8Tx_FLASH.ld 规定的地址
    拼成完整的 stepper.elf(含调试信息)
  4. 转换格式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. 烧不进去?按顺序排查

  1. st-info --probe 能否识别芯片?不能 → 查接线去,有时候杜邦线很坑的,尽量用好的杜邦线,例如硅胶那种,5毛-1块钱一根的那种awg 系列的线缆。
  2. Couldn't open ST-Link / 权限错误 → 去检查“权限问题"。
  3. 提示芯片受保护(read protection)→ 遇到读保护的话看后面内容。
  4. 接线全对但时好时坏 → 换短杜邦线、确认板子供电正常
    (板子上的电源 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 :check_mark: 0x1
2 :check_mark: :check_mark: 0x3
3 :check_mark: 0x2
4 :check_mark: :check_mark: 0x6
5 :check_mark: 0x4
6 :check_mark: :check_mark: 0xC
7 :check_mark: 0x8
8 :check_mark: :check_mark: 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 的铁律都在这里:

  1. 用任何外设前必须先开它的时钟。STM32 为了省电,所有外设上电默认
    没有时钟、处于"冻结"状态,写它的寄存器没有任何效果。
    RCC_APB2ENR 的第 2 位控制 GPIOA 的时钟。
  2. 引脚功能靠配置寄存器选择GPIOx_CRL 管理 PA0~PA7,每个引脚 4 位:
    • MODE[1:0]:输入还是输出、输出多快(我们选 10 = 输出 2MHz)
    • CNF[1:0]:输出的话是推挽还是开漏(我们选 00 = 通用推挽)
    • 合起来 0b0010 = 0x2

输出一步(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上了,有需要直接去取。免费的哦。

>GitHub

下一步就是如何把步进电机和3D打印圆固定到一起。

3D设计同事刚夸完他,就发现没脑子,连个孔也不给我怎么固定呢?
于是聪明如我就拆了他的玩具车,用联轴器固定,并用轮子作为托盘。哈哈


【【漂移菌】阿加犀犀牛派X1构建STM32编程环境控制步进电机完成同事拍摄器材的简易搭建~哈哈】 【漂移菌】阿加犀犀牛派X1构建STM32编程环境控制步进电机完成同事拍摄器材的简易搭建~哈哈_哔哩哔哩_bilibili

1 个赞


咖啡走起,周五的下午要开始浪了

1 个赞

经典开局小故事

2 个赞

时间地点,人物,冲突,高潮,低谷。。。然后故事浑然天成,这还真不是故事,今天的真事儿。哈哈主打一个真实。。

1 个赞

漂移菌老师,其实b站视频是可以在论坛上播放的:backhand_index_pointing_down:

教程:功能上新—社区论坛可上传视频了~ - 视频专区 - 端侧AI开发者社区