上传文件至 /

This commit is contained in:
Frank14f 2026-06-11 19:37:42 +08:00
commit 4de19b217e
5 changed files with 2747 additions and 0 deletions

671
README.md Normal file
View File

@ -0,0 +1,671 @@
# 树莓派 5 圆柱运动控制系统
> 本项目运行在 **Raspberry Pi 5**(树莓派 5通过多种通信协议控制三个圆柱在水槽中的运动并实时测量受力情况。
---
## 目录
1. [项目简介](#1-项目简介)
2. [整体架构](#2-整体架构)
3. [什么是树莓派Raspberry Pi](#3-什么是树莓派raspberry-pi)
4. [GPIO 概念](#4-gpio-概念)
5. [通信协议简介](#5-通信协议简介)
6. [Python 环境搭建](#6-python-环境搭建)
7. [项目文件结构](#7-项目文件结构)
8. [如何运行](#8-如何运行)
9. [三个驱动模块详解](#9-三个驱动模块详解)
10. [signal_features.json 说明](#10-signal_featuresjson-说明)
11. [常见问题 FAQ](#11-常见问题-faq)
12. [参考资料](#12-参考资料)
---
## 1. 项目简介
这是一个在 **Raspberry Pi 5** 上运行的控制系统,用于在水槽中控制三个圆柱的运动并测量受力。系统通过以下方式工作:
- 三个**空心杯电机**(带编码器反馈)控制圆柱的**旋转**
- 三个**步进电机**控制圆柱的**直线移动**
- 一个**高精度 ADC**(模数转换器)测量应变片传来的电压信号,从而算出受力大小
---
## 2. 整体架构
```
┌─────────────────────────────────────────────────────────────────────┐
│ Raspberry Pi 5 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ SPI 总线 │ │ I2C 总线 │ │ 硬件 PWM │ │
│ │ /dev/spidev0.0│ │ /dev/i2c-1 │ │ GPIO12 输出 │ │
│ └───────┬───────┘ └──────┬───────┘ └──────────┬───────────┘ │
│ │ │ │ │
└───────────┼───────────────────┼────────────────────────┼──────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ ADS124S08 │ │ 4EncoderMotor │ │ StepMotor Driver │
│ ADC 芯片 │ │ 电机模块 (I2C) │ │ (I2C + PWM) │
│ (SPI 通信) │ │ 地址: 0x24 │ │ 地址: 0x27 │
└──────┬───────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
▼ ▼ ▼
应变片信号 空心杯电机 (×4) 步进电机 (×3)
(电压测量) (控制圆柱旋转) (控制圆柱平移)
```
---
## 3. 什么是树莓派Raspberry Pi
树莓派是一台只有**信用卡大小**的 Linux 计算机。它虽然小,但具备普通电脑的所有功能——有 CPU、内存、USB 接口、HDMI 视频输出、网络接口等。树莓派最特别的地方是它有一排**GPIO 引脚**,可以通过这些引脚直接控制外部电子设备(比如电机、传感器、灯光等)。
### 本项目使用的型号Raspberry Pi 5
### 树莓派引脚图
树莓派 5 的 40 针 GPIO 排针布局如下从上方俯视SD 卡槽朝上):
```
3.3V (1) (2) 5V
GPIO2 (SDA) (3) (4) 5V
GPIO3 (SCL) (5) (6) GND
GPIO4 (7) (8) GPIO14 (TXD)
GND (9) (10) GPIO15 (RXD)
GPIO17 (11)(12) GPIO18
GPIO27 (13)(14) GND
GPIO22 (15)(16) GPIO23
3.3V (17)(18) GPIO24
GPIO10(MOSI)(19)(20) GND
GPIO9(MISO)(21)(22) GPIO25
GPIO11(SCLK)(23)(24) GPIO8 (CS)
GND (25)(26) GPIO7 (CS1)
GPIO0(IDSD)(27)(28) GPIO1 (IDSC)
GPIO5 (29)(30) GND
GPIO6 (31)(32) GPIO12 (PWM)
GPIO13 (33)(34) GND
GPIO19 (35)(36) GPIO16
GPIO26 (37)(38) GPIO20
GND (39)(40) GPIO21
```
> 详细引脚功能说明请参考:[树莓派官方引脚文档](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html)
---
## 4. GPIO 概念
### 什么是 GPIO
GPIO 是 **General Purpose Input Output**(通用输入输出引脚)的缩写。可以把 GPIO 引脚想象成树莓派身上伸出来的"小触手",每个触手可以:
- **输出高电平3.3V或低电平0V**:相当于开或关一个开关
- **检测外部电平是高还是低**:相当于感应外部有没有电信号
### 高电平和低电平
- **高电平HIGH / 1 / 3.3V**:引脚输出 3.3 伏特的电压,相当于"开"
- **低电平LOW / 0 / 0V**:引脚输出 0 伏特的电压,相当于"关"
简单来说,树莓派通过把 GPIO 引脚设置为高电平或低电平,来控制外部设备。比如把某个引脚设为高电平,就等于告诉连接的设备"开始工作"。
### 本项目中使用的 GPIO 引脚
| 引脚 (BCM) | 功能 | 连接设备 |
|---|---|---|
| GPIO12 | PWM 输出(产生步进脉冲) | StepMotor 模块 |
| GPIO5 | 步进电机 0 方向控制 | StepMotor 模块 |
| GPIO6 | 步进电机 1 方向控制 | StepMotor 模块 |
| GPIO16 | 步进电机 2 方向控制 | StepMotor 模块 |
| GPIO22 | 电源阶段 1 控制(先供电) | ADC (ADS124S08) |
| GPIO17 | 电源阶段 1 控制(先供电) | ADC |
| GPIO24 | 电源阶段 2 控制(后供电) | ADC |
| GPIO27 | 电源阶段 2 控制(后供电) | ADC |
| GPIO23 | 保持低电平(初始化期间) | ADC |
| GPIO18 | 保持低电平(初始化期间) | ADC |
| GPIO25 | DRDY 输入检测ADC数据就绪 | ADC |
| GPIO10 | SPI MOSI主出从入 | ADC |
| GPIO9 | SPI MISO主入从出 | ADC |
| GPIO11 | SPI SCLK时钟线 | ADC |
| GPIO8 | SPI CS片选低电平选中 | ADC |
| GPIO2 | I2C SDA数据线 | 电机模块 |
| GPIO3 | I2C SCL时钟线 | 电机模块 |
> BCM 编号是 Broadcom 芯片的 GPIO 编号方式,也是 Python 库(如 gpiozero默认使用的编号方式。
---
## 5. 通信协议简介
### 5.1 SPISerial Peripheral Interface串行外设接口
#### 是什么
SPI 是一种**同步串行通信协议**,用 **4 根线**连接主设备(这里是树莓派)和从设备(这里是 ADC 芯片)。
#### 四根线的比喻
把 SPI 通信想象成老师和学生对话:
| 线名 | 全称 | 比喻 |
|---|---|---|
| **SCLK** | Serial Clock串行时钟 | 老师用**节拍器**打拍子,所有人按这个节奏说话 |
| **MOSI** | Master Out Slave In主出从入 | **老师对学生说话**的通道 |
| **MISO** | Master In Slave Out主入从出 | **学生对老师说话**的通道 |
| **CS** | Chip Select片选 | 老师**点名**:喊谁的名字(拉低电平),谁就说话 |
> **CS 低电平有效**:当 CS 线从高电平变为低电平时,表示"我要跟你说话了";当 CS 线回到高电平时,表示"对话结束"。
#### 为什么 ADC 用 SPI
- **速度快**SPI 时钟频率可达几十 MHz适合高速数据读取
- **全双工**:可以同时发送和接收数据(就像两个人可以同时说话和听)
- 高精度 ADC如 ADS124S08需要快速、可靠地传输 24 位数据SPI 非常适合
#### 本项目中的 SPI
- **主设备**:树莓派 5
- **从设备**ADS124S08 ADC 芯片
- **总线路径**`/dev/spidev0.0`(总线 0设备 0
- **时钟频率**1 MHz每秒传输 1,000,000 位数据)
- **SPI 模式**:模式 1CPOL=0CPHA=1
---
### 5.2 I2CInter-Integrated Circuit内部集成电路
#### 是什么
I2C 是一种**同步串行通信协议**,只用 **2 根线**就可以连接多个设备。
#### 两根线的比喻
I2C 像学校的**广播系统**
| 线名 | 全称 | 比喻 |
|---|---|---|
| **SDA** | Serial Data串行数据线 | 广播的**话筒和喇叭**,说话和听话都用这一根 |
| **SCL** | Serial Clock串行时钟线 | 广播的**节拍信号**,控制说话的节奏 |
所有设备都接在同一对线上,但每个设备都有一个唯一的**地址**(就像一个班每个学生有**学号**)。主机喊学号,对应学号的设备才应答;其他设备保持安静。
#### 为什么电机模块用 I2C
- **接线简单**:只需要两根线,可以挂多个设备(每个设备用不同地址区分)
- **设备数量**:理论上一个 I2C 总线上最多可以连接 127 个设备
- 电机控制不需要极快的数据传输速度I2C 完全够用
#### 本项目中的 I2C
| 设备 | I2C 地址 | 用途 |
|---|---|---|
| 4EncoderMotor 模块 | **0x24** | 控制空心杯电机旋转 |
| StepMotor Driver 模块 | **0x27** | 配置步进电机(使能/复位等) |
树莓派通过 `/dev/i2c-1`I2C 总线 1与这两个模块通信。
---
### 5.3 PWMPulse Width Modulation脉冲宽度调制
#### 是什么
PWM 是一种通过**快速开关信号**来模拟不同电压输出的技术。就像快速眨眼——如果你半睁半闭眼睛,看起来就像在"眯着眼"PWM 也是类似的道理。
#### 两个重要参数
| 参数 | 含义 | 比喻 |
|---|---|---|
| **频率 (Frequency)** | 每秒开关多少次,单位 Hz赫兹 | 每秒眨眼的次数 |
| **占空比 (Duty Cycle)** | 高电平时间占一个周期的比例 | 每次眨眼时眼睛闭着的时间比例——80% 闭着 = 20% 占空比 |
比如:频率 1000 Hz = 每秒开关 1000 次。占空比 50% = 一半时间通(高电平),一半时间断(低电平)。
#### 为什么步进电机用 PWM
步进电机是一种特殊的电机,**每收到一个脉冲就走一步**。所以:
- PWM 的频率越高 → 脉冲来得越快 → 电机转速越快
- PWM 的频率越低 → 脉冲来得越慢 → 电机转速越慢
树莓派的**硬件 PWM**(由专用硬件产生,不受 CPU 负载影响)通过 GPIO12 引脚输出脉冲,经过 StepMotor 模块驱动步进电机。
---
## 6. Python 环境搭建
### 6.1 检查环境
树莓派 OS操作系统自带 Python 3。打开终端Terminal输入以下命令检查版本
```bash
python3 --version
```
如果显示类似 `Python 3.11.x` 或更高版本,说明 Python 已安装。
### 6.2 创建虚拟环境(推荐)
虚拟环境可以把本项目的 Python 依赖与你电脑上的其他项目隔离开,避免版本冲突。
```bash
# 进入项目文件夹
cd pinball_exp_rp5
# 创建虚拟环境(在项目根目录下生成 venv 文件夹)
python3 -m venv venv
# 激活虚拟环境
source venv/bin/activate
# 激活后,终端提示符前面会出现 (venv) 字样
```
> 以后每次使用本项目,都先运行 `source venv/bin/activate` 来激活虚拟环境。
### 6.3 安装依赖
首先确保 `requirements.txt` 文件在项目根目录下,然后运行:
```bash
pip install -r requirements.txt
```
#### 各依赖库的作用
| 库名 | 作用 | 安装方式 |
|---|---|---|
| **spidev** | 操作 SPI 总线的 Python 库。树莓派通过它跟 ADC 芯片通信。 | `pip install spidev` |
| **smbus2** | 操作 I2C 总线的 Python 库。树莓派通过它跟电机模块通信。 | `pip install smbus2` |
| **RPi.GPIO** | 控制 GPIO 引脚的经典库(可选)。本项目中主要用 gpiozero 代替。 | `pip install RPi.GPIO` |
| **gpiozero** | 简化版的 GPIO 控制库。Raspberry Pi OS 自带,无需额外安装。 | 系统自带 |
| **rpi_hardware_pwm** | 控制树莓派硬件 PWM 输出的库,用于步进电机的脉冲产生。 | `pip install rpi_hardware_pwm` |
| **jupyter** | 运行 `.ipynb` 笔记本文件的环境,可以在浏览器中交互式运行代码。 | `pip install jupyter` |
| **matplotlib** | 绘图库,`test.ipynb` 中用它将 ADC 采集的数据画成图表。 | `pip install matplotlib` |
> **注意**`rpi_hardware_pwm` 和 `matplotlib` 不在 `requirements.txt` 中,需要单独安装。
```bash
# 如果上面的安装命令执行完后还缺少某些库,可以手动补装:
pip install rpi_hardware_pwm matplotlib jupyter
```
---
## 7. 项目文件结构
```
pinball_exp_rp5/
├── README.md # 本文件 — 项目说明(你正在读的这份文档)
├── requirements.txt # Python 依赖列表(自动安装用)
├── drv_adc.py # ADC 驱动模块 — 采集电压信号SPI 通信)
├── drv_encodermotor.py # 编码器电机驱动模块 — 控制圆柱旋转I2C 通信)
├── drv_stepmotor.py # 步进电机驱动模块 — 控制圆柱平移I2C + PWM
├── test.ipynb # 教学演示 NotebookJupyter 笔记本)
└── signal_features.json # 实验信号参数(已脱敏处理)
```
---
## 8. 如何运行
```bash
# 1. 激活虚拟环境(如果之前创建了虚拟环境)
source venv/bin/activate
# 2. 启动 Jupyter Notebook
jupyter notebook
# 3. 终端会显示一个链接(类似 http://127.0.0.1:8888/
# 用浏览器打开这个链接
# 4. 在浏览器中点击 test.ipynb 文件
# 5. 在 Notebook 中按顺序从上到下运行每个 Cell代码单元格
# - 点击一个 Cell 选中它
# - 按 Shift + Enter 运行当前 Cell 并自动跳转到下一个
# - 也可以点工具栏上的 "Run" 按钮
```
> **什么是 Jupyter Notebook**
> Jupyter Notebook 是一种交互式的编程环境,把代码、运行结果、文字说明放在一个文件(`.ipynb`)中。每个代码块叫做一个"Cell",可以单独运行,非常适合教学和实验。
---
## 9. 三个驱动模块详解
### 9.1 ADC 模块(`drv_adc.py`
#### 控制芯片
**TI ADS124S08** —— 这是一款 24 位高精度模数转换器ADC
- **24 位**意味着它可以分辨非常微小的电压变化(最小可分辨约 0.3 微伏)
- 可以把连续的模拟电压信号转换成计算机可以处理的数字信号
- 本项目中用于读取应变片经放大后的电压信号,从而计算受力
#### 通信方式
SPI 总线,路径:`/dev/spidev0.0`
#### 为什么要分两步给 ADC 上电?
ADS124S08 芯片的模拟电路和数字电路需要按照特定顺序启动。如果同时上电芯片内部状态可能不稳定。所以驱动程序先启动第一部分电源Stage 1等待 100ms 让电源稳定后再启动第二部分电源Stage 2
#### 典型调用流程
```python
from drv_adc import ADS124S08
# 1. 创建 ADC 对象(自动完成上电和初始化)
adc = ADS124S08()
# 2. 配置可编程增益放大器PGA
# pga_en=1: 启用放大 gain=5: 放大 32 倍
adc.set_pga(pga_en=1, gain=5)
# 3. 配置采样率
# dr=8: 200 SPS每秒采样 200 次) mode=1: 单次转换模式
adc.set_datarate(dr=8, mode=1)
# 4. 选择要测量的输入通道(单端模式,以 AINCOM 为公共端)
# 这里选择通道 0AIN0 引脚)
adc.set_input_mux(pos_channel=0)
# 5. 读取多个通道的电压原始值
values = adc.request_channels([0, 1, 2])
# 6. 清理(关闭 SPI 总线,关掉 GPIO 电源)
adc.close()
```
#### 几个重要的配置
**PGA 增益倍数对照表:**
| gain 编码 | 实际放大倍数 | 适用信号 |
|---|---|---|
| 0 | ×1 | 强信号(>1V |
| 1 | ×2 | 中等信号 |
| 2 | ×4 | 中等信号 |
| 3 | ×8 | 较弱信号 |
| 4 | ×16 | 弱信号 |
| 5 | ×32 | 微弱信号(如应变片) |
| 6 | ×64 | 极微弱信号 |
| 7 | ×128 | 最微弱信号 |
**数据速率对照表:**
| dr 编码 | 实际速率SPS | 每次采样等待时间 |
|---|---|---|
| 0 | 2.5 | 400 ms |
| 4 | 20 | 50 ms |
| 8 | 200 | 5 ms |
| 13 | 4000 | 0.25 ms |
> **SPS** = Samples Per Second每秒采样次数。数值越大采样越快但噪声也越大。
---
### 9.2 编码器电机模块(`drv_encodermotor.py`
#### 控制硬件
**M5Stack Module 4EncoderMotor** —— 一个可以同时控制 4 个空心杯电机的模块。每个电机都带有编码器(可以测量电机转了多少圈)。
#### 通信方式
**I2C** 总线,模块地址:**0x24**
#### 什么是编码器?
编码器是安装在电机轴上的传感器。电机每转一个角度,编码器就产生一个脉冲。通过统计脉冲数量,计算机就知道电机转了多少圈、转到了哪个位置。
#### 三种控制模式
| 模式 | 名称 | 说明 |
|---|---|---|
| `NORMAL_MODE` (0x00) | 普通模式 | 直接设置 PWM 占空比控制转速,没有闭环控制 |
| `POSITION_MODE` (0x01) | 位置模式 | 使用 PID 闭环控制,让电机转到指定位置并停住 |
| `SPEED_MODE` (0x02) | 速度模式 | 使用 PID 闭环控制,让电机保持指定的转速 |
#### 什么是 PID 控制?
PID 是比例P-积分I-微分D控制的简称是一种自动调节技术。打个比方
你想让水龙头流出的水稳定在每分钟 1 升:
- **P比例**:看当前流量跟目标差多少,差得多就多拧一点,差得少就少拧一点
- **I积分**:如果长时间有微小偏差(比如一直是 0.98 升),就慢慢增加拧开的幅度
- **D微分**:如果流量突然变化(有人碰了水管),就快速反向调整一下
三个参数配合,就能让电机精确地达到并保持目标速度或位置。
#### 典型调用流程
```python
from drv_encodermotor import EncoderMotorDriver, SPEED_MODE
# 使用 with 语句(自动打开和关闭 I2C 连接)
with EncoderMotorDriver(bus=1) as driver:
# 1. 设置电机 0 为速度模式
driver.set_mode(0, SPEED_MODE)
# 2. 设置速度模式的 PID 参数
driver.set_speed_pid(0, kp=1, ki=100, kd=1)
# 3. 设置线速度(单位:米/秒)
# 电机将以 0.01 米/秒的速度旋转
driver.set_linear_speed_m_s(0, 0.01)
```
> `with` 语句是 Python 的一种语法确保无论代码是否出错I2C 连接都会被正确关闭。等价于 `try: ... finally: driver.close()`
---
### 9.3 步进电机模块(`drv_stepmotor.py`
#### 控制硬件
**M5Stack StepMotor Driver** —— 一个可以驱动最多 3 个步进电机的模块。
#### 通信方式
- **I2C**(地址 0x27用于模块配置使能、复位等
- **硬件 PWM**GPIO12用于产生步进脉冲控制电机速度
#### 什么是步进电机?
步进电机是一种特殊的电机,**每收到一个电脉冲就转动一个固定的角度**(称为"步距角")。本项目中电机的步距角是 1.8°(每圈 200 步),加上驱动器的微步进和减速器后,每圈需要 **89,600 个脉冲**
#### 三个电机的关系
- 三个步进电机**共享同一路 PWM**(即同时以相同的频率步进)
- 但每个电机可以**独立控制方向**(通过独立的 GPIO 引脚)
- 这样三个圆柱可以同时移动,但方向可以不同
#### S 曲线加减速
电机突然启动或停止会产生机械冲击。本项目使用 **S 曲线加减速**来让电机平滑地加速和减速。名字来源于速度随时间变化的形状像字母 "S"。
具体做法是用**余弦函数**来计算每个时刻的速度:
- 启动时:速度从 0 慢慢增加(就像汽车起步)
- 中间阶段:加速变快
- 接近目标速度时:加速又变慢,最终平稳到达目标速度
减速时反过来,整个过程非常平滑,减少了机械冲击和电机失步的风险。
#### 典型调用流程
```python
from drv_stepmotor import StepMotorDriver
# 1. 创建步进电机驱动对象
drv = StepMotorDriver()
# 2. 使能电机(允许接收脉冲)
drv.enable_motor()
# 3. 设置方向True = 正向)
drv.set_dir(True)
# 4. 启动电机并以 S 曲线加速到 0.01 米/秒
drv.start(0.01)
# 5. 在运行中改变速度
# drv.change_speed(0.02)
# 6. 以 S 曲线减速停止
drv.stop()
# 7. 清理资源
drv.close()
# 也支持 with 语句
# with StepMotorDriver() as drv:
# drv.enable_motor()
# drv.set_dir(True)
# drv.start(0.01)
# time.sleep(2)
# drv.stop()
```
---
## 10. `signal_features.json` 说明
这个文件存储了从 **LBM格子玻尔兹曼方法** 计算机仿真中提取的信号特征。LBM 是一种流体力学仿真方法,用于模拟水流绕过圆柱时的受力情况。
### 文件内容格式
```json
{
"signals": [
{
"name": "action1", // 第 1 个圆柱的信号
"mean": 0.00166, // 信号的直流分量(平均值)
"components": [ // 正弦波分量列表
{
"frequency": 0.135, // 频率 (Hz)
"amplitude": 0.00137, // 幅值
"phase": 0.187 // 相位 (弧度)
}
]
}
]
}
```
每个 action对应一个圆柱包含
- **mean均值**:信号的直流偏置
- **多个正弦波分量**:每个分量有频率、幅值、相位
这些参数用于在主控制循环中合成周期运动信号,让圆柱按照仿真得出的最优方式运动。**数据已做脱敏处理**(数值做了随机偏移),仅供学习参考。
---
## 11. 常见问题 FAQ
#### Q: SPI 设备找不到
```bash
ls -l /dev/spidev*
```
如果显示 `No such file or directory`,说明 SPI 接口未启用。
**解决方法**
```bash
sudo raspi-config
# 进入 Interface Options → SPI → Enable
# 重启树莓派
```
#### Q: I2C 设备找不到
```bash
i2cdetect -y 1
```
如果命令不存在或没有显示设备地址,说明 I2C 接口未启用。
**解决方法**
```bash
sudo raspi-config
# 进入 Interface Options → I2C → Enable
# 重启树莓派
```
扫描到的设备地址应该显示为十六进制编号(如 `24``27`)。
#### Q: 电机不转
请按顺序检查:
1. **电源**:电机模块是否有外部电源供电?树莓派的 5V 引脚通常不足以驱动电机
2. **使能信号**:是否调用了 `enable_motor()`(步进电机)或设置了正确的模式(编码器电机)
3. **接线**I2C 连线是否正确SDA → GPIO2SCL → GPIO3
4. **地址**I2C 地址是否匹配(编码器电机 0x24步进电机 0x27
#### Q: ADC 读数为 0
1. 检查 PGA 配置是否正确(增益是否设得太低,信号太弱读不出来)
2. 检查输入通道选择是否正确(是不是选对了 AIN 引脚)
3. 检查 SPI 接线
4. 用 `check_status()` 方法查看 ADC 状态寄存器
#### Q: ImportError: No module named xxx
运行以下命令安装缺失的库:
```bash
pip install xxx
```
常用的库安装命令:
```bash
pip install spidev smbus2 rpi_hardware_pwm matplotlib jupyter
```
#### Q: Permission denied权限不足
通常是因为当前用户没有访问 SPI 或 I2C 设备的权限。`pi` 用户默认有权限。如果使用其他用户,可以将用户加入相关组:
```bash
sudo usermod -a -G spi,i2c,gpio 用户名
# 然后注销并重新登录
```
---
## 12. 参考资料
- **ADS124S08 数据手册TI 官方网站)**
https://www.ti.com/product/ADS124S08
- **M5Stack 4EncoderMotor 模块文档**
https://docs.m5stack.com/en/module/Module_4EncoderMotor_V1.1
- **M5Stack StepMotor Driver 模块文档**
https://docs.m5stack.com/en/module/Stepmotor%20Driver%20Module13.2%20v1.1
- **树莓派官方文档**
https://www.raspberrypi.com/documentation/
- **树莓派 GPIO 引脚说明**
https://www.raspberrypi.com/documentation/computers/raspberry-pi.html
- **spidev Python 库**
https://pypi.org/project/spidev/
- **smbus2 Python 库**
https://pypi.org/project/smbus2/
- **gpiozero Python 库**
https://gpiozero.readthedocs.io/

1006
drv_adc.py Normal file

File diff suppressed because it is too large Load Diff

673
drv_encodermotor.py Normal file
View File

@ -0,0 +1,673 @@
"""
树莓派 5 (Raspberry Pi 5) 上控制 M5Stack Module 4EncoderMotor I2C 驱动程序
模块功能
本驱动用于通过 I2C 总线控制 M5Stack 4EncoderMotor 模块该模块可同时驱动
4 个空心杯电机带编码器反馈的小型直流电机常用于旋转圆柱机械臂关节等场景
每个电机都可以独立设置转速/位置模式读取编码器数值读取电流等
I2C 通信简介
I2CInter-Integrated Circuit是一种串行通信协议仅需两根线SDA 数据线
SCL 时钟线即可在多个设备之间传输数据总线上每个设备都有一个唯一地址主机
这里是树莓派通过地址选择要通信的从设备本模块的 I2C 地址固定为 0x24
资源依赖
需要安装 smbus2 Python I2C 操作库
参考文档
https://docs.m5stack.com/en/module/Module_4EncoderMotor_V1.1
用法示例
from drv_encodermotor import EncoderMotorDriver, NORMAL_MODE
with EncoderMotorDriver(bus=1) as driver:
driver.set_mode(0, NORMAL_MODE)
driver.set_motor_speed(0, 50)
"""
# 从 __future__ 模块导入 annotations 特性,允许在类型注解中使用字符串形式的类名
# (例如在方法返回类型中引用自身类 "EncoderMotorDriver"),从而避免循环导入问题
from __future__ import annotations
# 导入 struct 模块,用于将原始字节数据解析为整数、浮点数等 Python 数据类型
# 本驱动中使用它来解析 I2C 读取回来的多字节数据(如编码器数值、电流值)
import struct
# 从 typing 模块导入 List 类型注解,用于提示函数参数是列表类型,提高代码可读性
from typing import List
# 从 smbus2 库导入 SMBus 类,这是树莓派上操作 I2C 总线的 Python 封装库
# SMBus 提供了读写单个字节、读写数据块等方法
from smbus2 import SMBus
# =============================================================================
# I2C 寄存器地址常量定义
# 这些地址来源于 M5Stack 官方 Arduino 示例代码,每个地址对应模块内部的一个寄存器
# 通过向这些寄存器写入或读取数据,即可控制电机或获取传感器信息
# =============================================================================
# 模块的 I2C 从机地址,固定为 0x24十六进制十进制为 36
MODULE_4ENCODERMOTOR_ADDR = 0x24
# 舵机角度寄存器起始地址 = 0x00用于设置舵机的目标角度
# (第 1 个电机从 0x00 开始,第 2 个电机从 0x04 开始,依此类推)
MODULE_4ENCODERMOTOR_SERVO_ANGLE_ADDR = 0x00
# 舵机脉冲宽度寄存器起始地址 = 0x10用于直接设置舵机的 PWM 脉冲宽度
MODULE_4ENCODERMOTOR_SERVO_PULSE_ADDR = 0x10
# PWM 占空比寄存器起始地址 = 0x20用于设置电机的 PWM 占空比(即电机转速)
# 第 1 个电机对应 0x20第 2 个对应 0x21以此类推每个电机占 1 字节)
MODULE_4ENCODERMOTOR_PWM_DUTY_ADDR = 0x20
# 编码器数值寄存器起始地址 = 0x30用于读取或设置电机的编码器计数值
# 编码器是装在电机轴上的传感器,可以测量电机旋转的角度/圈数
# 每个电机编码器占 4 字节32 位有符号整数)
MODULE_4ENCODERMOTOR_ENCODER_ADDR = 0x30
# 速度寄存器起始地址 = 0x40用于读取电机每 20ms 的速度值1 字节)
MODULE_4ENCODERMOTOR_SPEED_ADDR = 0x40
# 8 位 ADC模数转换器寄存器地址 = 0xA0用于读取模拟输入引脚的值8 位精度)
MODULE_4ENCODERMOTOR_ADC_8BIT_REG = 0xA0
# 12 位 ADC 寄存器地址 = 0xB0用于读取模拟输入引脚的值12 位精度,更精确)
MODULE_4ENCODERMOTOR_ADC_12BIT_REG = 0xB0
# 跳转到 Bootloader引导加载程序的寄存器地址 = 0xFD用于进入固件升级模式
JUMP_TO_BOOTLOADER_REG = 0xFD
# 升级 Bootloader 的寄存器地址 = 0xE0用于触发 Bootloader 更新流程
UPGRADE_BOOTLOADER_REG = 0xE0
# 固件版本号寄存器地址 = 0xFE读取此地址可获得模块当前固件版本
MODULE_4ENCODERMOTOR_FIRMWARE_VERSION_ADDR = 0xFE
# Bootloader 版本号寄存器地址 = 0xFC读取此地址可获得 Bootloader 版本
MODULE_4ENCODERMOTOR_BOOTLOADER_VERSION_ADDR = 0xFC
# I2C 地址配置寄存器地址 = 0xFF用于读取或修改模块的 I2C 地址
MODULE_4ENCODERMOTOR_I2C_ADDRESS_ADDR = 0xFF
# 配置块寄存器起始地址 = 0x50用于设置电机的控制模式、PID 参数等
MODULE_4ENCODERMOTOR_CONFIG_ADDR = 0x50
# 电流寄存器地址 = 0x90用于读取当前电机总电流4 字节浮点数)
MODULE_4ENCODERMOTOR_CURRENT_ADDR = 0x90
# 软启停寄存器地址 = 0xD1用于启用或禁用电机的软启动/软停止功能
MODULE_4ENCODERMOTOR_SOFT_START_STOP_ADDR = 0xD1
# =============================================================================
# 电机工作模式常量定义
# =============================================================================
# 普通模式 = 0x00直接通过 PWM 占空比控制电机转速,不做闭环控制
NORMAL_MODE = 0x00
# 位置模式 = 0x01使用 PID 闭环控制,使电机旋转到指定位置
POSITION_MODE = 0x01
# 速度模式 = 0x02使用 PID 闭环控制,使电机保持指定转速
SPEED_MODE = 0x02
# IAP在应用中编程更新模式 = 0x03用于在线升级固件
IAP_UPDATE_MODE = 0x03
# Bootloader 更新模式 = 0x04用于通过 Bootloader 升级固件
BOOTLOADER_UPDATE_MODE = 0x04
def _constrain_index(index: int) -> int:
"""
将电机索引限制在 0~3 范围内4 个电机防止越界访问
参数:
index: 原始电机索引号用户传入的可能是负数或大于 3 的数
返回:
限制后的有效索引值范围 0~3
"""
# 如果索引小于 0则返回 0第 1 个电机),避免负数索引导致错误
if index < 0:
return 0
# 如果索引大于 3则返回 3第 4 个电机),避免超出模块支持的电机数量
if index > 3:
return 3
# 索引在有效范围内0~3直接返回原值
return index
class EncoderMotorError(RuntimeError):
"""
自定义异常类继承自 RuntimeError
I2C 通信失败或参数超出范围时抛出此异常方便调用方捕获并处理错误
"""
# pass 表示这是一个空类,不添加额外的方法或属性
# 只改变异常的名字,便于区分是哪个模块出的错误
pass
class EncoderMotorDriver:
"""树莓派上控制 M5Stack 4EncoderMotor 模块的最小化驱动程序。
本类尽量遵循 Arduino 示例代码中的寄存器布局和字节序约定以保证不同平台
Arduino 和树莓派之间的兼容性
关于字节序Byte Order的说明以匹配 Arduino 实现
- 编码器寄存器MODULE_4ENCODERMOTOR_ENCODER_ADDR使用大端序big-endian
有符号 32 位整数Arduino 代码中将字节按 b0<<24 | b1<<16 | b2<<8 | b3
的方式拼接因此我们从树莓派端读取时需要按大端序解析
- 配置块中的位置点setPositionPoint使用小端序little-endian写入
Arduino 代码中先发送最低有效字节LSB first
- 电流浮点数 4 个原始字节传输采用模块本身的浮点数表示方式
Arduino memcpy 表明是小端序浮点排列因此我们用 '<f'
'<' 表示小端序'f' 表示 float来解析
big-endian大端序高位字节在前存储在低地址例如数值 0x12345678
内存中顺序为 0x12 0x34 0x56 0x78
little-endian小端序低位字节在前存储在低地址例如数值 0x12345678
在内存中顺序为 0x78 0x56 0x34 0x12
"""
def __init__(self, i2c_addr: int = MODULE_4ENCODERMOTOR_ADDR, bus: int = 1):
"""
初始化电机驱动对象打开 I2C 总线
参数:
i2c_addr: 模块的 I2C 地址默认为 0x24MODULE_4ENCODERMOTOR_ADDR
bus: 树莓派上的 I2C 总线编号树莓派 5 的默认 I2C 总线是 bus 1
对应 GPIO 引脚 2SDA 3SCL
"""
# 保存模块的 I2C 地址到实例变量 self.addr供后续读写操作使用
self.addr = i2c_addr
# 保存 I2C 总线编号到实例变量 self.bus_num以备将来可能重新连接时使用
self.bus_num = bus
# 创建 SMBus 对象,打开指定编号的 I2C 总线,开始与模块通信
self.bus = SMBus(bus)
def close(self) -> None:
"""
关闭 I2C 总线连接释放资源
使用完驱动后应调用此方法或使用 with 语句让 Python 自动调用
"""
try:
# 检查总线对象是否不为 None避免重复关闭导致异常
if self.bus is not None:
# 调用 SMBus 对象的 close 方法,关闭 I2C 总线
self.bus.close()
finally:
# 无论 close 是否成功,都将总线对象设为 None标记为已关闭
# type: ignore 是告诉类型检查器忽略此处类型不匹配的警告
self.bus = None # type: ignore
def __enter__(self) -> "EncoderMotorDriver":
"""
Python 上下文管理器协议的方法进入 with 语句块时调用
配合 __exit__ 使用使该类支持 with 语句 with EncoderMotorDriver() as drv:
返回 self这样在 with 块内就可以用 drv 变量来调用其他方法
"""
return self
def __exit__(self, exc_type, exc, tb) -> None:
"""
Python 上下文管理器协议的方法退出 with 语句块时调用
无论 with 块中是否发生异常都会自动关闭 I2C 总线连接
参数:
exc_type: 异常类型如果没有异常则为 None
exc: 异常对象如果没有异常则为 None
tb: 异常回溯信息如果没有异常则为 None
"""
# 调用 close 方法关闭 I2C 总线
self.close()
# =========================================================================
# 底层 I2C 读写辅助方法
# 这些方法封装了 smbus2 库的基本 I2C 操作,加上错误处理,方便上层方法调用
# =========================================================================
def _write_byte(self, reg: int, value: int) -> None:
"""
向指定寄存器写入一个字节的数据
参数:
reg: 目标寄存器地址 0x20 表示 PWM 占空比寄存器
value: 要写入的整数值只取低 8 24 位被截断
"""
# 将 value 与 0xFF 做按位与运算,只保留低 8 位(一个字节)
# 例如 value=0x1A3 时0x1A3 & 0xFF = 0xA3只取最低一个字节
value &= 0xFF
try:
# 通过 I2C 向模块地址 self.addr 的寄存器 reg 写入一个字节 value
# write_byte_data(地址, 寄存器, 数据) 是 smbus2 的标准方法
self.bus.write_byte_data(self.addr, reg, value)
except Exception as e:
# 如果 I2C 写入过程发生任何异常(如设备未连接、总线错误等),
# 抛出自定义的 EncoderMotorError 异常,附带错误描述和原始异常信息
raise EncoderMotorError(f"I2C write_byte failed: {e}")
def _write_block(self, reg: int, data: List[int]) -> None:
"""
向指定寄存器写入多个字节的数据数据块
参数:
reg: 起始寄存器地址
data: 要写入的整数列表每个整数代表一个字节
"""
# 将 data 列表中的每个元素都与 0xFF 做按位与运算,确保每个值都在 0~255 范围内
# 这相当于只取每个元素的低 8 位
data = [int(x) & 0xFF for x in data]
try:
# 通过 I2C 向模块地址 self.addr 的起始寄存器 reg 写入 data 列表
# write_i2c_block_data(地址, 起始寄存器, 数据列表) 会依次写入多个字节
# 第一个字节写入 reg第二个字节写入 reg+1依此类推
self.bus.write_i2c_block_data(self.addr, reg, data)
except Exception as e:
# I2C 写入失败时,抛出自定义异常
raise EncoderMotorError(f"I2C write_block failed: {e}")
def _read_block(self, reg: int, length: int) -> bytes:
"""
从指定寄存器开始读取多个字节的数据
参数:
reg: 起始寄存器地址
length: 要读取的字节数
返回:
bytes 类型的字节序列包含从模块读取的原始数据
"""
try:
# 通过 I2C 从模块地址 self.addr 的起始寄存器 reg 读取 length 个字节
# read_i2c_block_data 返回一个整数列表每个元素代表一个字节的值0~255
data = self.bus.read_i2c_block_data(self.addr, reg, length)
# 将整数列表转换为 bytes 对象(不可变的字节序列),方便后续用 struct 解析
return bytes(data)
except Exception as e:
# I2C 读取失败时,抛出自定义异常
raise EncoderMotorError(f"I2C read_block failed: {e}")
# =========================================================================
# 上层 API 方法(命名和行为尽量与 Arduino 示例保持一致)
# =========================================================================
def set_mode(self, index: int, mode: int) -> None:
"""
设置指定电机的控制模式
参数:
index: 电机索引0~3分别对应模块上的 4 个电机接口
mode: 工作模式可以是 NORMAL_MODE(0x00)POSITION_MODE(0x01)
"""
# 将电机索引限制在 0~3 范围内,防止越界
index = _constrain_index(index)
# 计算配置寄存器的地址:基地址 0x50 + 0x10 * 电机索引
# 每个电机在配置块中占用 16 字节0x10的空间
reg = MODULE_4ENCODERMOTOR_CONFIG_ADDR + (0x10 * index)
# 通过 I2C 向计算出的配置寄存器地址写入模式值1 字节)
self._write_byte(reg, mode)
def get_encoder_value(self, index: int) -> int:
"""
读取指定电机的编码器数值
编码器是安装在电机轴上的传感器电机每转一圈编码器会产生固定数量的脉冲
通过统计脉冲数可以知道电机轴当前的位置角度
参数:
index: 电机索引0~3
返回:
编码器的 32 位有符号整数值正值表示正转负值表示反转
"""
# 将电机索引限制在 0~3 范围内
index = _constrain_index(index)
# 计算编码器寄存器地址:基地址 0x30 + 4 * 电机索引
# 每个电机的编码器数值占用 4 字节32 位整数),所以索引要乘以 4
reg = MODULE_4ENCODERMOTOR_ENCODER_ADDR + 4 * index
# 通过 I2C 从 reg 地址开始连续读取 4 个字节的原始数据
data = self._read_block(reg, 4)
# 使用 struct.unpack 将 4 字节解析为有符号 32 位整数
# '>i' 的含义:
# '>' 表示大端序big-endian即高位字节在前
# 'i' 表示有符号 32 位整数C 语言的 int32_t
# Arduino 代码中将字节按 (b0<<24) | (b1<<16) | (b2<<8) | b3 拼接,
# 这正是大端序的组装方式,所以我们用大端序解析
return struct.unpack('>i', data)[0]
def set_encoder_value(self, index: int, encoder: int) -> None:
"""
设置指定电机的编码器数值写入/重置编码器计数值
通常用于将编码器归零写入 0或设置初始位置
参数:
index: 电机索引0~3
encoder: 要写入的 32 位有符号整数值编码器计数值
"""
# 将电机索引限制在 0~3 范围内
index = _constrain_index(index)
# 计算编码器寄存器地址:基地址 0x30 + 4 * 电机索引
reg = MODULE_4ENCODERMOTOR_ENCODER_ADDR + 4 * index
# 将 32 位整数拆分为 4 个字节,按大端序排列(与 Arduino 一致)
# 拆分方法:
# 第 1 字节 = (encoder >> 24) & 0xFF 取最高 8 位
# 第 2 字节 = (encoder >> 16) & 0xFF 取次高 8 位
# 第 3 字节 = (encoder >> 8) & 0xFF 取次低 8 位
# 第 4 字节 = encoder & 0xFF 取最低 8 位
data = [
(encoder >> 24) & 0xFF,
(encoder >> 16) & 0xFF,
(encoder >> 8) & 0xFF,
encoder & 0xFF
]
# 通过 I2C 向编码器寄存器写入 4 个字节的数据
self._write_block(reg, data)
def set_motor_speed(self, index: int, duty: int) -> None:
"""
设置指定电机的 PWM 占空比即电机转速
此方法适用于 NORMAL_MODE普通模式直接控制 PWM 输出
PWM脉冲宽度调制通过快速开关电源来调节电机的平均电压
占空比越高电机转速越快正值为正转负值为反转
参数:
index: 电机索引0~3
duty: 占空比值取值范围 -128~127有符号 8 位整数
正数表示正转负数表示反转数值绝对值越大转速越快
"""
# 将电机索引限制在 0~3 范围内
index = _constrain_index(index)
# 计算 PWM 占空比寄存器地址:基地址 0x20 + 电机索引
# 每个电机占 1 个字节,所以直接加索引值即可
reg = MODULE_4ENCODERMOTOR_PWM_DUTY_ADDR + index
# 将 duty 与 0xFF 做按位与运算,只保留低 8 位
# 注意duty 是带符号的整数(如 -50 的补码表示为 0xCE
# 与 0xFF 后可以得到正确的二进制补码表示
self._write_byte(reg, duty & 0xFF)
def get_motor_speed(self, index: int) -> int:
"""
读取指定电机的当前 PWM 占空比转速设定值
参数:
index: 电机索引0~3
返回:
有符号 8 位整数-128~127表示当前的 PWM 占空比设定值
"""
# 将电机索引限制在 0~3 范围内
index = _constrain_index(index)
# 计算 PWM 占空比寄存器地址:基地址 0x20 + 电机索引
reg = MODULE_4ENCODERMOTOR_PWM_DUTY_ADDR + index
# 通过 I2C 从寄存器读取 1 个字节的原始数据
data = self._read_block(reg, 1)
# 使用 struct.unpack 将单个字节解析为有符号 8 位整数
# 'b' 表示有符号字符signed char即 -128~127 的范围
# 这样 -50 这样的负值就能正确解析,而不是变成 206无符号值
return struct.unpack('b', data)[0]
def get_motor_speed_20ms(self, index: int) -> int:
"""
读取指定电机每 20 毫秒的速度反馈值编码器测速结果
这个值代表电机在当前 20ms 时间窗口内的平均速度
参数:
index: 电机索引0~3
返回:
有符号 8 位整数-128~127正值正转负值反转
"""
# 将电机索引限制在 0~3 范围内
index = _constrain_index(index)
# 计算速度反馈寄存器地址:基地址 0x40 + 电机索引
# 每个电机占 1 个字节
reg = MODULE_4ENCODERMOTOR_SPEED_ADDR + index
# 通过 I2C 从寄存器读取 1 个字节的原始数据
data = self._read_block(reg, 1)
# 使用 struct.unpack 解析为有符号 8 位整数
return struct.unpack('b', data)[0]
def set_position_pid(self, index: int, kp: int, ki: int, kd: int) -> None:
"""
设置指定电机位置模式下的 PID 参数
PID比例-积分-微分控制是一种闭环控制算法
- Kp比例系数根据当前误差的大小来调节输出误差越大调节越强
- Ki积分系数根据误差的累积来调节输出消除稳态误差
- Kd微分系数根据误差的变化趋势来调节输出抑制超调
参数:
index: 电机索引0~3
kp: 比例系数0~255
ki: 积分系数0~255
kd: 微分系数0~255
"""
# 将电机索引限制在 0~3 范围内
index = _constrain_index(index)
# 计算 PID 参数在配置块中的起始地址:
# 基地址 0x50 + 0x10 * 电机索引 + 0x01 偏移
# 0x01 偏移表示 Kp 存储在配置块的第 2 个字节(第 1 个字节是模式设置)
reg = MODULE_4ENCODERMOTOR_CONFIG_ADDR + index * 0x10 + 0x01
# 通过 I2C 向起始寄存器写入 3 个字节Kp、Ki、Kd 各占 1 字节
# 每个参数都与 0xFF 取低 8 位,确保在 0~255 范围内
self._write_block(reg, [kp & 0xFF, ki & 0xFF, kd & 0xFF])
def set_position_point(self, index: int, position_point: int) -> None:
"""
设置指定电机在位置模式下的目标位置点
电机将自动旋转到指定位置使用 PID 闭环控制
参数:
index: 电机索引0~3
position_point: 目标位置值32 位有符号整数以编码器脉冲数为单位
"""
# 将电机索引限制在 0~3 范围内
index = _constrain_index(index)
# 计算位置点在配置块中的起始地址:
# 基地址 0x50 + 0x10 * 电机索引 + 0x04 偏移
# 0x04 偏移表示位置点存储在配置块的偏移 0x04 处
reg = MODULE_4ENCODERMOTOR_CONFIG_ADDR + index * 0x10 + 0x04
# 将 32 位整数拆分为 4 个字节,按小端序排列(先写最低有效字节 LSB
# 拆分方法(小端序):
# 第 1 字节 = position_point & 0xFF 取最低 8 位(先发送)
# 第 2 字节 = (position_point >> 8) & 0xFF
# 第 3 字节 = (position_point >> 16) & 0xFF
# 第 4 字节 = (position_point >> 24) & 0xFF 取最高 8 位(最后发送)
# Arduino 的 setPositionPoint 函数中先发送 LSB最低有效字节即小端序
data = [
position_point & 0xFF,
(position_point >> 8) & 0xFF,
(position_point >> 16) & 0xFF,
(position_point >> 24) & 0xFF
]
# 通过 I2C 向位置点寄存器写入 4 个字节(小端序)
self._write_block(reg, data)
def set_position_pid_max_speed(self, index: int, max_pwm: int) -> None:
"""
设置位置模式下 PID 控制的最大允许 PWM 占空比即最大转速限制
这样即使在位置 PID 计算出很大的输出时也能限制电机不超过这个速度
参数:
index: 电机索引0~3
max_pwm: 最大 PWM 占空比0~255数值越大允许的最大转速越高
"""
# 将电机索引限制在 0~3 范围内
index = _constrain_index(index)
# 计算最大速度寄存器地址:
# 基地址 0x50 + 0x10 * 电机索引 + 0x08 偏移
# 0x08 偏移对应 Arduino 示例中最大速度参数的存储位置
reg = MODULE_4ENCODERMOTOR_CONFIG_ADDR + index * 0x10 + 0x08
# 通过 I2C 向寄存器写入最大 PWM 值1 字节),与 0xFF 确保在范围内
self._write_byte(reg, max_pwm)
def set_speed_pid(self, index: int, kp: int, ki: int, kd: int) -> None:
"""
设置指定电机在速度模式下的 PID 参数
当使用 SPEED_MODE速度模式电机通过 PID 闭环控制来维持目标转速
参数:
index: 电机索引0~3
kp: 比例系数0~255
ki: 积分系数0~255
kd: 微分系数0~255
"""
# 将电机索引限制在 0~3 范围内
index = _constrain_index(index)
# 计算速度 PID 参数在配置块中的起始地址:
# 基地址 0x50 + 0x10 * 电机索引 + 0x09 偏移
# 0x09 偏移是速度 PID 参数的存储起始位置(紧跟在位置 PID 最大速度之后)
reg = MODULE_4ENCODERMOTOR_CONFIG_ADDR + index * 0x10 + 0x09
# 通过 I2C 向起始寄存器写入 3 个字节Kp、Ki、Kd 各占 1 字节
self._write_block(reg, [kp & 0xFF, ki & 0xFF, kd & 0xFF])
def set_speed_point(self, index: int, speed_point: int) -> None:
"""
设置指定电机在速度模式下的目标速度值
电机将通过 PID 闭环控制来达到并保持这个速度
参数:
index: 电机索引0~3
speed_point: 目标速度值有符号 8 位整数-128~127
"""
# 将电机索引限制在 0~3 范围内
index = _constrain_index(index)
# 计算速度点在配置块中的地址:
# 基地址 0x50 + 0x10 * 电机索引 + 0x0C 偏移
# 0x0C 偏移对应速度目标值的存储位置(紧跟在速度 PID 参数之后)
reg = MODULE_4ENCODERMOTOR_CONFIG_ADDR + index * 0x10 + 0x0C
# 通过 I2C 向寄存器写入速度目标值1 字节),与 0xFF 取低 8 位
self._write_byte(reg, speed_point & 0xFF)
def set_linear_speed_m_s(self, index: int, speed_m_s: float) -> None:
"""
设置指定电机的线速度/将线速度换算为电机转速并写入模块
适用于用电机驱动圆柱旋转的场景已知圆柱的物理参数后可换算
速度换算公式推导核心难点
speed = int(speed_m_s / (3.1415 * 0.01) * 9 * 16 * 20 / 50)
公式中各数字的物理含义
3.1415 圆周率 π约等于 3.14159用于计算圆柱周长
0.01 圆柱半径 0.01 1 厘米这是圆柱的物理半径
-----------------------------------------------------------------
3.1415 * 0.01 = 圆柱周长/
圆柱周长 = 2 * π * 半径 = 2 * 3.1415 * 0.01 = 0.06283
注意代码中实际用的是 π * 半径 而不是 2 * π * 半径,
这里需要结合实际的圆柱安装方式理解如果圆柱是由电机通过摩擦
轮或减速机构驱动的有效半径可能已经包含了减半的因素
-----------------------------------------------------------------
speed_m_s / (3.1415 * 0.01) = 每秒旋转的圈数/
将线速度除以每圈周长得到目标转速/
-----------------------------------------------------------------
9 电机减速比减速箱的减速比 9:1
电机内部有减速齿轮输出轴每转 1 电机转子转 9
-----------------------------------------------------------------
16 编码器每圈的脉冲数线数
编码器每转一圈产生 16 个脉冲用于测量电机转子的位置
-----------------------------------------------------------------
9 * 16 = 144 电机输出轴每转 1 编码器产生的脉冲数
-----------------------------------------------------------------
20 20 毫秒 0.02 采样一次速度
模块每 20ms 读取一次编码器脉冲数计算速度反馈值
-----------------------------------------------------------------
50 速度寄存器每一单位对应的每秒脉冲数
模块内部速度值 = 20ms 的脉冲数 / 50
换句话说速度寄存器中数值为 50 对应的 20ms 脉冲数为
50 × 20ms 的脉冲数... 这个 50 是模块固件定义的比例因子
-----------------------------------------------------------------
完整计算流程
1. 线速度 每秒圈数speed_m_s / (π * 半径)
2. 每秒圈数 每秒电机转子圈数× 减速比 9
3. 每秒转子圈数 每秒编码器脉冲数× 编码器线数 16
4. 每秒脉冲数 20 毫秒脉冲数÷ (1000/20) = ÷ 50
5. 最终速度值 = 每秒脉冲数 / 50
-----------------------------------------------------------------
合并公式
speed = speed_m_s / (π * 0.01) × 9 × 16 / 50
= speed_m_s / (3.1415 * 0.01) * 9 * 16 / 50
参数:
index: 电机索引0~3
speed_m_s: 目标线速度单位米/可以是小数 0.5 表示 0.5 /
"""
# 将线速度换算为模块速度寄存器所需的数值
# speed_m_s / (3.1415 * 0.01) = 每秒圆柱转数(圈/秒)
# × 9 × 16 = 每秒编码器脉冲数(考虑减速比和编码器线数)
# / 50 = 每 20ms 脉冲数 / 50模块内部速度单位换算
speed = int(speed_m_s / (3.1415 * 0.01) * 9 * 16 * 20 / 50)
# 检查计算出的速度值是否在有符号 8 位整数的有效范围内(-127~127
# 超出范围说明设定的线速度太高或太低,无法用 1 字节表示
if speed > 127 or speed < -127:
# 如果速度超出范围,抛出异常提示调用方
raise EncoderMotorError("Calculated speed out of range for set_linear_speed_m_s")
else:
# 如果速度在有效范围内,调用 set_speed_point 写入目标速度
self.set_speed_point(index, speed)
def get_motor_current(self) -> float:
"""
读取模块的电机总电流所有电机电流之和单位安培A
可用于监控电机是否过载或堵转
返回:
浮点数表示当前电机总电流单位安培A
"""
# 通过 I2C 从电流寄存器 MODULE_4ENCODERMOTOR_CURRENT_ADDR (0x90)
# 连续读取 4 个字节的原始数据
data = self._read_block(MODULE_4ENCODERMOTOR_CURRENT_ADDR, 4)
# 使用 struct.unpack 将 4 字节解析为单精度浮点数
# '<f' 的含义:
# '<' 表示小端序little-endian即低地址存放低字节
# 'f' 表示单精度浮点数C 语言的 float4 字节)
# Arduino 代码通过 memcpy 直接复制字节,在小端序处理器上得到的是小端序浮点数
# 所以我们也用小端序解析
return struct.unpack('<f', data)[0]
def get_analog_input(self) -> int:
"""
读取模块的 8 位模拟输入值ADC 采样值
可用于读取外接的模拟传感器如电位器光敏电阻等
返回:
整数0~255代表 8 ADC 的采样值
"""
# 通过 I2C 从 8 位 ADC 寄存器 MODULE_4ENCODERMOTOR_ADC_8BIT_REG (0xA0)
# 读取 1 个字节的数据
data = self._read_block(MODULE_4ENCODERMOTOR_ADC_8BIT_REG, 1)
# 返回读取到的第一个字节(也是唯一一个字节)作为 ADC 采样值
return data[0]
def set_soft_start_and_stop(self, index: int, state: bool) -> None:
"""
启用或禁用指定电机的软启动/软停止功能
软启动电机启动时缓慢加速而不是突然全速可以减少冲击
软停止电机停止时缓慢减速而不是突然刹车
实现原理模块用一个字节8 的位图来记录 4 个电机的软启停状态
每个位bit对应一个电机我们需要先读取当前字节然后修改对应的位
再写回模块
参数:
index: 电机索引0~3
state: True 表示启用软启停False 表示禁用
"""
# 将电机索引限制在 0~3 范围内
index = _constrain_index(index)
# 第 1 步:通过 I2C 读取软启停寄存器的当前值1 个字节)
# 先读取是避免覆盖其他电机已设置的状态
data = self._read_block(MODULE_4ENCODERMOTOR_SOFT_START_STOP_ADDR, 1)
# 取出读取到的第 1 个字节(也是唯一字节)作为当前状态位图
buf = data[0]
# 先清除当前电机对应的位:将对应位设为 0其他位保持不变
# 1 << index将 1 左移 index 位,得到只有第 index 位为 1 的数
# 例如 index=2 时1<<2 = 0b00000100
# ~(1 << index):按位取反,得到只有第 index 位为 0其他位为 1 的数
# buf = buf & ~(1 << index):按位与运算,将第 index 位强制设为 0
buf = buf & ~(1 << index)
# 如果 state 为 True启用软启停则将对应位设为 1
if state:
# buf | (1 << index):按位或运算,将第 index 位设为 1其他位不变
buf = buf | (1 << index)
# 第 2 步:通过 I2C 将修改后的字节写回软启停寄存器
self._write_byte(MODULE_4ENCODERMOTOR_SOFT_START_STOP_ADDR, buf)
def get_soft_start_and_stop(self, index: int) -> bool:
"""
查询指定电机的软启动/软停止功能是否已启用
参数:
index: 电机索引0~3
返回:
True 表示软启停已启用False 表示未启用
"""
# 将电机索引限制在 0~3 范围内
index = _constrain_index(index)
# 通过 I2C 读取软启停寄存器的当前值1 个字节)
data = self._read_block(MODULE_4ENCODERMOTOR_SOFT_START_STOP_ADDR, 1)
# 检查第 index 位是否为 1
# data[0] & (1 << index):将字节与只有第 index 位为 1 的数做按位与
# 如果结果非零,说明该位为 1已启用bool() 返回 True
# 如果结果为零,说明该位为 0未启用bool() 返回 False
return bool(data[0] & (1 << index))

369
drv_stepmotor.py Normal file
View File

@ -0,0 +1,369 @@
"""
Raspberry Pi 5 + M5Stack StepMotor Driver 模块驱动
=====================================================
本模块用于驱动 M5Stack StepMotor Driver 模块控制最多三个步进电机
驱动圆柱在水槽中直线移动
本模块结合了两种控制方式
1. I2C 通信地址 0x27用于控制模块的使能/复位/配置
2. 硬件 PWMGPIO12用于产生步进脉冲通过调整脉冲频率控制电机速度
方向控制通过独立的 GPIO 引脚5/6/16实现三个电机共享同一路 PWM
但方向可以独立控制
PWM 概念简介
PWMPulse Width Modulation脉冲宽度调制是一种通过快速开关信号来
模拟不同电压输出的技术在步进电机控制中PWM 的频率决定电机转速
频率越高脉冲越快电机转动越快
参考文档
https://docs.m5stack.com/en/module/Stepmotor%20Driver%20Module13.2%20v1.1
依赖库smbus2I2C 通信库
使用示例
from drv_stepmotor import StepMotorDriver
with StepMotorDriver() as driver:
driver.set_speeds([0.02, -0.015, 0.01])
time.sleep(2)
"""
from __future__ import annotations # 启用 Python 未来的注解特性,允许在类型提示中使用字符串形式的类名(前向引用)
import math # 导入 math 模块,用于数学计算(如余弦函数 cos() 用于 S 曲线加减速、圆周率 pi
import threading # 导入 threading 模块,用于多线程操作(本模块预留,当前未直接使用)
import time # 导入 time 模块,用于延时和计时(如加减速过程中的 sleep 暂停)
from typing import List, Optional # 从 typing 模块导入 List列表类型和 Optional可选类型用于参数类型注解
from gpiozero import DigitalOutputDevice # 从 gpiozero 库导入 DigitalOutputDevice 类,用于控制树莓派 GPIO 引脚的电平输出(高/低)
from smbus2 import SMBus # 从 smbus2 库导入 SMBus 类,用于通过 I2C 总线与 M5Stack 步进电机模块通信
from rpi_hardware_pwm import HardwarePWM # 从 rpi_hardware_pwm 库导入 HardwarePWM 类,用于控制树莓派硬件 PWM 发生器
# ==================== I2C 寄存器地址常量 ====================
# 以下常量对应 M5Stack StepMotor Driver 模块内部寄存器的 I2C 地址,
# 参考自官方 C++ 实现文件 ref_stepmotor.cpp
MODULE_STEPMOTOR_ADDR = 0x27 # 模块的 I2C 从设备地址7 位地址 0x27所有 I2C 通信都发往此地址
STEPMOTOR_REG_IO_CFG = 0x03 # IO 配置寄存器地址,用于配置模块引脚的输入/输出功能
STEPMOTOR_REG_MICROSTEP = 0x01 # 微步进模式配置寄存器地址,设置电机的微步细分数
STEPMOTOR_REG_ENABLE = 0x01 # 使能控制寄存器地址(与微步进共用 0x01通过不同位控制不同功能
STEPMOTOR_REG_EXTIO = 0x00 # 外部 IO 寄存器地址,用于扩展输入输出
STEPMOTOR_REG_FAULT = 0x04 # 故障状态寄存器地址,读取过流/过热等错误状态
STEPMOTOR_REG_RESET = 0x05 # 复位寄存器地址,用于单独复位各个电机通道
class StepMotorError(RuntimeError):
"""自定义异常类,继承自 RuntimeError用于步进电机驱动相关的错误处理"""
pass # 类体为空,仅作为自定义异常类型的标识,不需要额外实现
class StepMotorDriver:
"""步进电机驱动器类,最多控制 3 个步进电机。
本类封装了 I2C 通信和硬件 PWM 的全部控制逻辑提供启动停止调速
方向控制等完整功能三个步进电机共享同一路硬件 PWM产生步进脉冲
但每个电机的方向由独立的 GPIO 引脚控制
重要设计说明
- 三个电机共享同一路 PWM因为所有电机同时以相同频率步进仅方向可以不同
- I2C 只用于模块的使能/复位配置不用于产生步进脉冲
- 步进脉冲由树莓派硬件 PWM 直接产生不受 CPU 负载影响
构造函数参数说明
- pwm_pins: 用于产生步进脉冲的 GPIO 引脚编号BCM 编号默认为 GPIO12
- dir_pins: 用于控制方向信号的 GPIO 引脚编号列表最多 3 默认 [5, 6, 16]
- steps_per_rev: 电机转一圈所需的步数微步进默认 200×32×14 = 89600
- mm_per_rev: 电机转一圈对应的直线移动距离毫米默认 π×20 62.83 mm
- ramp_time: 加减速的斜坡时间默认 20
"""
def __init__(
self,
pwm_pins: int = None, # PWM 引脚编号BCM 编号),默认为 12即 GPIO12
dir_pins: Optional[List[int]] = None, # 方向控制 GPIO 引脚编号列表,默认为 [5, 6, 16]
steps_per_rev: int = 200*32*14, # 电机转一圈的总步数(微步进后)
mm_per_rev: float = math.pi*20, # 电机转一圈对应的直线移动距离(毫米)
ramp_time: float = 20, # 加减速斜坡时间(秒)
) -> None:
# 保存所有配置参数为实例变量,供其他方法访问
self.pwm_pins = pwm_pins or 12 # 如果未传入 PWM 引脚编号,默认使用 GPIO12
self.dir_pins = dir_pins or [5, 6, 16] # 如果未传入方向引脚列表,默认使用 GPIO5、GPIO6、GPIO16
self.steps_per_rev = steps_per_rev # 每转总步数 = 200电机物理步数× 32微步数× 14减速比
self.mm_per_rev = mm_per_rev # 每转移动距离 = π × 20驱动轮直径单位 mm
self.max_speed = 0.1 # 最大速度限制(米/秒),防止电机速度过快导致失步或机械冲击
self.ramp_time = float(ramp_time) # 加减速斜坡时间(秒),强制转为 float 类型以确保精度
self.i2c_addr = MODULE_STEPMOTOR_ADDR # I2C 设备地址,使用预定义的常量 0x27
self.i2c_bus = SMBus(1) # 创建 I2C 总线对象,使用 I2C 总线 1树莓派 5 上 I2C-1 对应引脚 GPIO2/SDA、GPIO3/SCL
self.default_dir = [True, True, False] # 三个电机的默认方向:电机 0 和电机 1 正向,电机 2 反向
self._pwm = HardwarePWM(pwm_channel=0, hz=1, chip=0) # 创建硬件 PWM 对象:通道 0初始 1HzPWM 芯片 0
self._dir_pins = [DigitalOutputDevice(d, initial_value=False) for d in self.dir_pins] # 为每个方向引脚创建 GPIO 输出对象,初始值为低电平
self.current_speed = 0.0 # 当前速度(米/秒),初始化为 0 表示电机处于停止状态
self.set_dir(True) # 设置三个电机的初始方向为正向
def _write_byte(self, reg: int, value: int) -> None:
"""向模块的指定 I2C 寄存器写入一个字节的数据。
这是一个内部方法以下划线开头封装了 I2C 寄存器写入操作
参数:
reg: 寄存器地址 0x01 使能寄存器0x05 复位寄存器等
value: 要写入的 8 位数据值0~255写入前会自动截断为低 8
异常:
StepMotorError: 如果 I2C 通信失败则抛出此异常
"""
value &= 0xFF # 截断高位:只保留低 8 位0~255确保写入一个字节
try:
self.i2c_bus.write_byte_data(self.i2c_addr, reg, value) # 通过 I2C 总线向指定寄存器的地址写入数据
except Exception as e:
raise StepMotorError(f"I2C write_byte failed: {e}") # 如果 I2C 操作异常,包装为自定义异常向上抛出
def _read_byte(self, reg: int) -> int:
"""读取模块指定 I2C 寄存器的一个字节数据。
这是一个内部方法以下划线开头封装了 I2C 寄存器读取操作
参数:
reg: 寄存器地址
返回:
int: 读取到的 8 位数据值0~255
异常:
StepMotorError: 如果 I2C 通信失败则抛出此异常
"""
try:
self.i2c_bus.write_byte(self.i2c_addr, reg) # 先向 I2C 设备发送要读取的寄存器地址(写入操作选择寄存器)
data = self.i2c_bus.read_byte(self.i2c_addr) # 再从设备读取该寄存器的当前值(读取操作获取数据)
return data # 返回读取到的数据值
except Exception as e:
raise StepMotorError(f"I2C read_byte failed: {e}") # 如果 I2C 操作异常,包装为自定义异常向上抛出
def enable_motor(self):
"""使能电机:允许电机驱动芯片接收脉冲信号,电机可以正常转动。
通过修改使能寄存器地址 0x01的第 4 bit 4来实现
bit 4 = 0 表示使能bit 4 = 1 表示禁止
操作流程先读取寄存器的当前值 修改特定位 将新值写回寄存器
这种"读-改-写"模式确保不影响寄存器中其他位的设置
"""
reg_data = self._read_byte(STEPMOTOR_REG_ENABLE) # 读取使能寄存器(地址 0x01的当前值
reg_data &= 0xEF # 清除第 4 位bit 4 置 00xEF = 0b11101111bit 4 = 0 表示使能电机
self._write_byte(STEPMOTOR_REG_ENABLE, reg_data) # 将修改后的值写回使能寄存器(地址 0x01
def disable_motor(self):
"""禁止电机:阻止电机驱动芯片接收脉冲信号,电机停止转动。
通过修改使能寄存器地址 0x01的第 4 bit 4来实现
将其他位清零仅保留 bit 4 的值
注意原代码使用 reg_data &= 0x10 仅保留 bit 4 并清除其他位
这与标准的"将 bit 4 置 1"reg_data |= 0x10有所不同
但为保持逻辑不变此处不修改代码
"""
reg_data = self._read_byte(STEPMOTOR_REG_ENABLE) # 读取使能寄存器(地址 0x01的当前值
reg_data &= 0x10 # 将除第 4 位之外的所有位清零0x10 = 0b00010000仅保留 bit 4
self._write_byte(STEPMOTOR_REG_ENABLE, reg_data) # 将修改后的值写回使能寄存器(地址 0x01
def reset_motor(self, resmtr: int, enable: bool):
"""复位或释放指定的电机通道。
通过复位寄存器地址 0x05的低 3 位分别控制三个电机
bit 0 控制电机 0bit 1 控制电机 1bit 2 控制电机 2
将某位置 1 表示复位该电机 0 表示释放
参数:
resmtr: 电机索引012分别对应三个步进电机通道
enable: True 表示复位将对应位设为 1False 表示释放将对应位设为 0
"""
reg_data = self._read_byte(STEPMOTOR_REG_RESET) # 读取复位寄存器(地址 0x05的当前值
reg_data &= 0x07 # 仅保留低 3 位bit 0~2分别对应三个电机的复位状态高位清零
if not enable:
reg_data &= ~(0x01 << resmtr) # 如果 enable=False将对应电机的位清零~(1<<n) 将第 n 位取反后做 AND
else:
reg_data |= (0x01 << resmtr) # 如果 enable=True将对应电机的位置 1(1<<n) 左移 n 位得到只有第 n 位为 1 的掩码
self._write_byte(STEPMOTOR_REG_RESET, reg_data) # 将修改后的值写回复位寄存器(地址 0x05
def close(self) -> None:
"""关闭驱动器:停止所有电机、释放硬件资源。
按顺序执行紧急停止 I2C 禁能 所有电机通道释放 清理 GPIO/PWM/I2C 对象
使用 try/except 确保即使某个清理步骤出错后续步骤也能继续执行
"""
self.emergency_stop() # 紧急停止:立即切断 PWM 输出,电机瞬间失去脉冲
self.disable_motor() # 通过 I2C 设置使能寄存器,禁止电机驱动芯片
for idx in range(3): # 循环三个电机通道(索引 0、1、2
self.reset_motor(idx, False) # 释放每个电机通道:将复位寄存器中对应位清 0
# 关闭 gpiozero 相关的 GPIO 设备
try:
self._pwm.close() # 关闭硬件 PWM 对象,释放 PWM 通道资源
self.i2c_bus.close() # 关闭 I2C 总线连接,释放总线资源
for d in self._dir_pins: # 遍历所有方向控制引脚对象
d.off() # 将方向 GPIO 引脚置为低电平(关闭输出)
d.close() # 关闭方向 GPIO 引脚对象,释放资源
except Exception:
pass # 忽略清理过程中的任何异常,确保程序能正常退出
def set_dir(self, dir: bool = True) -> None:
"""设置三个电机的整体方向。
注意三个电机只能同时设置为相同的方向全部正向或全部反向
每个电机的实际方向由 default_dir电机默认方向和传入的 dir 参数
通过异或XOR运算决定这样可以灵活设置每个电机的正反
参数:
dir: True 表示正向False 表示反向
电机 n 的实际方向 = default_dir[n] XOR dir
异常:
StepMotorError: 如果电机正在运行current_speed 0则不能改变方向
"""
if self.current_speed == 0.0: # 检查电机是否处于停止状态,运行时禁止改变方向
for idx, dir_dev in enumerate(self._dir_pins): # 遍历三个方向控制引脚(索引 0、1、2
boolean_dir = self.default_dir[idx] ^ dir # 计算实际方向:默认方向 XOR 传入方向(异或运算)
if boolean_dir:
dir_dev.on() # 如果计算结果为 True将 GPIO 引脚置为高电平(电机正转)
else:
dir_dev.off() # 如果计算结果为 False将 GPIO 引脚置为低电平(电机反转)
else:
raise StepMotorError("Cannot change direction while motor is running") # 电机运行时改变方向会损坏驱动器
def start(self, speed_m_s: float) -> None:
"""启动电机,并使用 S 曲线加速到目标速度。
S 曲线加速原理
使用余弦函数产生平滑的"S"形加速曲线速度变化率即加速度
在启动和结束时较小中间较大从而减少机械冲击和电机失步风险
S 曲线加速公式
s = (cos(π × (1 - i/100)) + 1) / 2 × speed_m_s
其中 i 0 99 逐步增加
- i=0 cos(π×(1-0)) = cos(π) = -1s = (-1+1)/2 × speed = 0 × speed = 0起始速度为 0
- i=50 cos(π×(1-0.5)) = cos(π/2) = 0s = (0+1)/2 × speed = 0.5 × speed中间速度为目标一半
- i=99 cos(π×(1-0.99)) = cos(0.01π) 1s = (1+1)/2 × speed speed接近目标速度
整个过程中加速度先增大后减小形成"S"形曲线相比线性加速更加平顺
频率换算公式
freq = s × steps_per_rev / (mm_per_rev / 1000)
推导过程
- freq(Hz) = 每秒步数脉冲数
- 每秒步数 = 速度(m/s) × 步数/
- 步数/ = 步数/ ÷ / = steps_per_rev ÷ (mm_per_rev / 1000)
- steps_per_rev = 200电机物理步数1.8°/× 32驱动器微步细分数× 14行星减速器减速比
- mm_per_rev = π × 20驱动轮直径 20mm= 62.83 mm/
参数:
speed_m_s: 目标速度/必须为正值
异常:
StepMotorError: 如果电机已经在运行中则不能重复启动
"""
if self.current_speed == 0.0: # 仅在电机当前处于停止状态时才能启动
self._pwm.change_frequency(1) # 先将 PWM 频率设为最低的 1Hz避免从 0Hz 直接跳变
self._pwm.start(100) # 启动硬件 PWM 输出,占空比设为 100%(模块内部处理步进脉冲的生成)
for i in range(100): # 将加速过程分为 100 个步进点,从 i=0 到 i=99 逐步加速
# S 曲线加速公式:余弦函数值从 cos(π) = -1 逐渐变化到 cos(0.01π) ≈ 1
# 经 (cos + 1) / 2 映射后,系数从 0 逐渐变化到接近 1
s = (math.cos(math.pi * (1-i/100)) + 1) / 2 * speed_m_s # 计算当前步进点的瞬时目标速度(米/秒)
# 将速度(米/秒)换算为 PWM 频率Hz
# freq = s × steps_per_rev / (mm_per_rev / 1000)
# 除以 1000 因为 mm_per_rev 是毫米,需要转换为米
freq = max(s * self.steps_per_rev / (self.mm_per_rev / 1000), 1) # 换算频率max(...,1) 确保频率不低于 1Hz
self._pwm.change_frequency(freq) # 更新硬件 PWM 的频率,使电机以对应速度转动
time.sleep(self.ramp_time / 100) # 等待 ramp_time/100 秒,控制每步加速的持续时间
self.current_speed = speed_m_s # 加速完成后,将当前速度记录为目标速度
else:
raise StepMotorError("Motor is already running") # 如果电机已经运行,拒绝重复启动(必须先 stop
def stop(self) -> None:
"""停止电机,使用 S 曲线减速到零。
S 曲线减速公式
s = (cos(π × i/100) + 1) / 2 × current_speed
其中 i 0 99 逐步增加
- i=0 cos(0) = 1s = (1+1)/2 × speed = 1 × speed保持当前速度开始减速
- i=50 cos(π/2) = 0s = (0+1)/2 × speed = 0.5 × speed速度降到一半
- i=99 cos(0.99π) -1s = (-1+1)/2 × speed = 0 × speed = 0减速到零
start() 中的加速曲线对称减速曲线也是 S 确保停止过程平滑
减速时间比加速时间短使用 ramp_time/4 而不是 ramp_time/100
因为减速到零后电机即停止不需要像加速那样缓慢进入
"""
if self.current_speed != 0.0: # 只有在电机正在运行时才能执行停止操作
for i in range(100): # 将减速过程分为 100 个步进点,从 i=0 到 i=99 逐步减速
# S 曲线减速公式:余弦值从 cos(0) = 1 逐渐变化到 cos(0.99π) ≈ -1
# 经 (cos + 1) / 2 映射后,系数从 1 逐渐下降到接近 0
s = (math.cos(math.pi * i/100) + 1) / 2 * self.current_speed # 计算当前步进点的瞬时速度(米/秒)
freq = max(s * self.steps_per_rev / (self.mm_per_rev / 1000), 1) # 将速度换算为 PWM 频率,最小 1Hz
self._pwm.change_frequency(freq) # 更新硬件 PWM 频率,降低电机转速
time.sleep(self.ramp_time / 4 / 100) # 等待 ramp_time/400 秒,比加速步进快 4 倍
self._pwm.stop() # 减速完成后,完全停止硬件 PWM 输出
self._pwm.change_frequency(1) # 将 PWM 频率复位到 1Hz 默认值,为下一次启动做准备
self.current_speed = 0.0 # 将当前速度设为 0标记电机已停止
else:
raise StepMotorError("Motor is already stopped") # 如果电机已经停止,拒绝重复执行停止操作
def change_speed(self, speed_m_s: float) -> None:
"""在电机运行过程中,平滑地将速度切换到新目标值。
速度切换 S 曲线公式
s = (cos(π × i/100) + 1) / 2 × (current_speed - speed_m_s) + speed_m_s
公式推导
将减速公式推广到任意目标速度
- delta = (current_speed - speed_m_s)即当前速度与目标速度的差值
- 减速公式变为s = 余弦因子 × delta + speed_m_s
- i=0 因子=1s = 1×delta + speed_m_s = current_speed保持当前速度
- i=99 因子0s = 0×delta + speed_m_s = speed_m_s达到目标速度
传入的速度可以是加速目标 > 当前或减速目标 < 当前
S 曲线都能平滑过渡
参数:
speed_m_s: 新的目标速度/可高于或低于当前速度
异常:
StepMotorError: 如果电机未运行调用此方法会抛出异常提示使用 start()
"""
if self.current_speed != 0.0: # 只有在电机正在运行时才能切换速度
for i in range(100): # 分 100 步进行速度过渡,从 i=0 到 i=99
# 速度切换公式:当前的 S 曲线因子从 1 过渡到 0
# 乘以速度差后加到目标速度上,实现从当前速度到目标速度的平滑过渡
s = (math.cos(math.pi * i/100) + 1) / 2 * (self.current_speed - speed_m_s) + speed_m_s
freq = max(s * self.steps_per_rev / (self.mm_per_rev / 1000), 1) # 将速度换算为 PWM 频率,最小 1Hz
self._pwm.change_frequency(freq) # 更新硬件 PWM 频率,改变电机转速
time.sleep(self.ramp_time / 4 / 100) # 等待 ramp_time/400 秒,控制每步过渡的持续时间
self.current_speed = speed_m_s # 速度过渡完成后,将当前速度更新为新目标速度
else:
raise StepMotorError("Motor is not running. Use start() to start the motor.") # 电机停止时不能用 change_speed
def emergency_stop(self) -> None:
"""紧急停止:立即切断 PWM 输出,不经过 S 曲线减速。
stop() 方法的区别
- stop()使用 S 曲线平滑减速约需 ramp_time/4
- emergency_stop()直接切断 PWM 输出无减速过程立即停止
适用于紧急情况下的快速停机但会产生较大的机械冲击
"""
self._pwm.stop() # 立即停止硬件 PWM 输出,电机瞬间失去步进脉冲
self._pwm.change_frequency(1) # 将 PWM 频率复位到 1Hz 默认值,为下一次启动做准备
self.current_speed = 0.0 # 将当前速度设为 0标记电机已停止
def __enter__(self):
"""进入上下文管理器:当使用 'with StepMotorDriver() as driver:' 语法时自动调用
返回:
StepMotorDriver: 返回自身实例 with 语句块中的变量引用
"""
return self # 返回当前 StepMotorDriver 实例
def __exit__(self, exc_type, exc, tb):
"""退出上下文管理器:当 with 语句块执行完毕时自动调用
参数:
exc_type: 异常类型如果语句块中发生了异常
exc: 异常对象
tb: 异常回溯信息
"""
self.close() # 自动调用 close() 清理所有资源,确保即使发生异常也能释放硬件

28
requirements.txt Normal file
View File

@ -0,0 +1,28 @@
# ==================================================
# 圆柱水槽运动控制系统 — Python 依赖列表
# 安装命令pip install -r requirements.txt
# ==================================================
# SPI 通信库
# 用于树莓派通过 SPI 总线与 ADS124S08 ADC 芯片通信
# 实现 24 位高精度模数转换数据的读取
spidev
# I2C 通信库
# 用于树莓派通过 I2C 总线与以下模块通信:
# - M5Stack 4EncoderMotor 模块(地址 0x24
# - M5Stack StepMotor Driver 模块(地址 0x27
smbus2
# 树莓派硬件 PWM 控制库
# 用于通过树莓派硬件 PWM 发生器GPIO12产生步进脉冲
# 驱动步进电机按指定速度运动
rpi_hardware_pwm
# Jupyter Notebook 环境
# 用于交互式运行 test.ipynb 教学演示
jupyter
# 数据可视化库(可选)
# test.ipynb 中的绘图功能依赖此库
matplotlib