{ "cells": [ { "cell_type": "markdown", "id": "cell_1_markdown", "metadata": {}, "source": [ "## 项目概述\n", "\n", "本项目是一个**水槽中圆柱运动控制实验系统**,用于在充满水的实验水槽中控制三个\n", "圆柱体的旋转和平移运动,并实时采集圆柱表面的受力信号。\n", "\n", "### 三个核心驱动模块\n", "\n", "| 模块文件 | 功能 |\n", "| --- | --- |\n", "| `drv_adc.py` | **ADC 模块** — 通过 SPI 总线控制 ADS124S08 模数转换器芯片,采集应变片输出的微弱电压信号,用于测量圆柱受到的流体力 |\n", "| `drv_encodermotor.py` | **编码器电机模块** — 通过 I2C 总线控制 M5Stack 4EncoderMotor 模块,驱动空心杯电机(带编码器反馈)使圆柱旋转 |\n", "| `drv_stepmotor.py` | **步进电机模块** — 通过 I2C + PWM + GPIO 控制 M5Stack StepMotor Driver 模块,驱动步进电机使圆柱在水槽中平移 |\n", "\n", "### 本 Notebook 的目的\n", "\n", "本 Notebook 是一个**面向初学者的教学演示**,逐模块演示每个驱动的基本用法。\n", "即使你完全不懂 Python,也可以按照下面的步骤逐步了解每个设备是如何控制的。\n", "\n", "> ⚠️ **注意**:以下所有涉及硬件的代码段都需要连接真实的树莓派和相应硬件模块才能运行。\n", "> 如果没有硬件,代码旁边的注释会特别说明。" ] }, { "cell_type": "code", "execution_count": null, "id": "cell_2_imports", "metadata": {}, "outputs": [], "source": [ "# 导入编码器电机驱动模块,用于控制圆柱旋转的空心杯电机\n", "from drv_encodermotor import EncoderMotorDriver, NORMAL_MODE, SPEED_MODE, POSITION_MODE\n", "\n", "# 导入步进电机驱动模块,用于控制圆柱平移\n", "from drv_stepmotor import StepMotorDriver\n", "\n", "# 导入 ADC 驱动模块,用于采集应变片电压信号\n", "from drv_adc import ADS124S08\n", "\n", "# 导入 time 模块,用于延时和计时\n", "import time\n", "\n", "# 导入 math 模块,用于数学计算(如正弦、余弦、圆周率等)\n", "import math" ] }, { "cell_type": "markdown", "id": "cell_3_markdown", "metadata": {}, "source": [ "## EncoderMotor(编码器电机)使用示例\n", "\n", "本示例演示如何控制一个空心杯电机旋转,包括:设置速度模式、配置 PID 参数、\n", "启用软启停(减少机械冲击)、设定目标速度、读取编码器和电流值等。\n", "\n", "> 涉及 I2C 总线通信,**需要连接 M5Stack 4EncoderMotor 模块才能运行**。" ] }, { "cell_type": "code", "execution_count": null, "id": "cell_4_encodermotor", "metadata": {}, "outputs": [], "source": [ "# 使用 with 语句创建 EncoderMotorDriver 实例,连接 I2C 总线 1\n", "# with 语句会在代码块结束后自动关闭设备,确保资源被正确释放\n", "with EncoderMotorDriver(bus=1) as driver:\n", "\n", " # ========== 第一步:工作模式设置 ==========\n", " # 设置通道 0(第一个电机)为速度模式(SPEED_MODE)\n", " # 在速度模式下,电机通过 PID 闭环控制保持指定的转速\n", " driver.set_mode(0, SPEED_MODE)\n", " print(\"已将通道 0 设为速度模式\")\n", "\n", " # ========== 第二步:PID 参数配置 ==========\n", " # 设置速度模式下的 PID 参数\n", " # kp(比例系数)= 1:根据当前速度误差大小调节输出\n", " # ki(积分系数)= 100:消除稳态误差(让实际速度更接近目标)\n", " # kd(微分系数)= 1:抑制速度的快速变化(减少震荡)\n", " driver.set_speed_pid(0, kp=1, ki=100, kd=1)\n", " print(\"已配置速度 PID 参数:kp=1, ki=100, kd=1\")\n", "\n", " # ========== 第三步:软启停设置 ==========\n", " # 启用软启动/软停止功能(True=启用)\n", " # 电机启动时会缓慢加速,停止时会缓慢减速,减少机械冲击\n", " driver.set_soft_start_and_stop(0, True)\n", " print(\"已启用软启停功能\")\n", "\n", " # ========== 第四步:设定目标速度 ==========\n", " # 设置目标线速度为 0.02 米/秒(正值为正转)\n", " # 内部会自动将线速度换算为电机转速并写入模块\n", " driver.set_linear_speed_m_s(0, 0.02)\n", " print(\"已设定目标速度:0.02 m/s\")\n", "\n", " # 等待 2 秒,让电机在实际硬件上运行一段时间\n", " time.sleep(2)\n", "\n", " # ========== 第五步:读取编码器数值 ==========\n", " # 编码器可以测量电机实际旋转的角度/圈数\n", " # 返回值是一个整数,表示编码器的脉冲计数值\n", " encoder_val = driver.get_encoder_value(0)\n", " print(f\"当前编码器数值:{encoder_val}\")\n", "\n", " # ========== 第六步:读取电机电流 ==========\n", " # 读取所有电机的总电流,单位安培(A)\n", " # 可用于监控电机是否过载或堵转\n", " current = driver.get_motor_current()\n", " print(f\"当前电机总电流:{current:.4f} A\")\n", "\n", " # ========== 第七步:停止电机 ==========\n", " # 将目标速度设为 0,电机停止旋转\n", " # 由于启用了软停止,电机会缓慢减速到零\n", " driver.set_linear_speed_m_s(0, 0.0)\n", " print(\"已将速度设为 0,电机停止\")\n", "\n", " # 等待 1 秒让停止动作完成\n", " time.sleep(1)\n", "\n", "# 退出 with 语句块时,driver.close() 会被自动调用,释放 I2C 总线资源\n", "print(\"EncoderMotor 示例运行完毕,资源已自动释放\")\n", "\n", "# ⚠️ 以上代码需要连接真实的 M5Stack 4EncoderMotor 模块和树莓派才能运行" ] }, { "cell_type": "markdown", "id": "cell_5_markdown", "metadata": {}, "source": [ "## StepMotor(步进电机)使用示例\n", "\n", "本示例演示如何控制步进电机使圆柱平移,包括:使能电机、复位通道、设置方向、\n", "S 曲线加速启动、运行中变速、减速停止和急停等。\n", "\n", "> **涉及 I2C + PWM + GPIO,需要连接 M5Stack StepMotor Driver 模块和树莓派才能运行**。\n", "> 注意:`start`/`stop` 的 S 曲线运行时间很短(`ramp_time=0.1` 左右),不要跑太久。" ] }, { "cell_type": "code", "execution_count": null, "id": "cell_6_stepmotor", "metadata": {}, "outputs": [], "source": [ "# 创建 StepMotorDriver 实例\n", "# 注意:这里没有使用 with 语句,因此需要在最后手动调用 close()\n", "drv = StepMotorDriver()\n", "\n", "try:\n", " # ========== 第一步:使能电机 ==========\n", " # 允许电机驱动芯片接收脉冲信号,电机可以正常转动\n", " drv.enable_motor()\n", " print(\"已使能电机\")\n", "\n", " # ========== 第二步:复位所有通道 ==========\n", " # 依次复位三个电机通道(索引 0, 1, 2)\n", " # True 表示复位(将电机通道恢复到初始状态)\n", " for i in range(3):\n", " drv.reset_motor(i, True)\n", " print(\"已复位所有电机通道\")\n", "\n", " # ========== 第三步:设置方向 ==========\n", " # True = 正向,False = 反向\n", " # 三个电机同时设置为同一个方向\n", " drv.set_dir(True)\n", " print(\"已设置电机方向为正向\")\n", "\n", " # ========== 第四步:S 曲线加速启动 ==========\n", " # 使用余弦 S 曲线从零平滑加速到目标速度 0.02 m/s\n", " # ramp_time 参数控制加速时间,这里用短时间(0.1 秒)演示\n", " drv.ramp_time = 0.1 # 将加减速时间设为 0.1 秒(仅用于演示)\n", " drv.start(0.02) # 目标速度 0.02 米/秒\n", " print(\"电机已启动,正在以 0.02 m/s 运行\")\n", "\n", " # 让电机运行一小段时间\n", " time.sleep(1)\n", "\n", " # ========== 第五步:运行中改变速度 ==========\n", " # 在电机不停机的情况下,将速度切换到 0.04 m/s\n", " # 同样使用 S 曲线平滑过渡\n", " drv.change_speed(0.04)\n", " print(\"已将速度切换到 0.04 m/s\")\n", "\n", " time.sleep(1)\n", "\n", " # ========== 第六步:减速停止 ==========\n", " # 使用 S 曲线平滑减速到零(机械冲击小,但不紧急)\n", " drv.stop()\n", " print(\"电机已停止(S 曲线减速)\")\n", "\n", " # ========== 第七步:急停演示 ==========\n", " # 先再次启动电机,然后展示紧急停止\n", " drv.start(0.02)\n", " time.sleep(0.5)\n", "\n", " # 紧急停止:直接切断 PWM 输出,电机立即停止\n", " # 与 stop() 不同,emergency_stop() 不做平滑减速\n", " # 适用于紧急情况,但会产生较大的机械冲击\n", " drv.emergency_stop()\n", " print(\"紧急停止完成\")\n", "\n", "finally:\n", " # ========== 第八步:清理资源 ==========\n", " # close() 会自动执行:紧急停止 → I2C 禁能 → 释放通道 → 清理 GPIO/PWM/I2C\n", " drv.close()\n", " print(\"StepMotor 示例运行完毕,资源已释放\")\n", "\n", "# ⚠️ 以上代码需要连接真实的 M5Stack StepMotor Driver 模块和树莓派才能运行" ] }, { "cell_type": "markdown", "id": "cell_7_markdown", "metadata": {}, "source": [ "## ADC(模数转换器)使用示例\n", "\n", "本示例演示如何通过 SPI 总线控制 ADS124S08 ADC 芯片采集电压信号。\n", "ADC 用于读取应变片输出的微弱电压变化,从而推算圆柱受到的流体力。\n", "\n", "> **涉及 SPI 通信和 GPIO 控制,需要连接 ADS124S08 ADC 模块和树莓派才能运行**。" ] }, { "cell_type": "code", "execution_count": null, "id": "cell_8_adc", "metadata": {}, "outputs": [], "source": [ "# 创建 ADS124S08 ADC 实例\n", "# 构造函数自动完成 SPI 初始化、分步上电、寄存器验证等操作\n", "adc = ADS124S08()\n", "\n", "try:\n", " # ========== 第一步:配置 PGA(可编程增益放大器)==========\n", " # PGA 可以在 ADC 转换之前放大微弱的输入信号\n", " # pga_en=1:启用前置放大\n", " # gain=5:增益编码 5 对应 ×32 倍放大\n", " # 应变片输出信号通常只有毫伏级,需要放大才能被 ADC 准确测量\n", " adc.set_pga(pga_en=1, gain=5)\n", " print(\"已配置 PGA:已启用,增益 ×32\")\n", "\n", " # ========== 第二步:配置数据速率和转换模式 ==========\n", " # dr=8:数据速率编码 8 对应 200 SPS(每秒采样 200 次)\n", " # 数值越大采样越快,但噪声也越大\n", " # mode=1:单次转换模式(每次转换完成后停止,需要重新触发)\n", " # 适合逐个通道依次测量的场景\n", " adc.set_datarate(dr=8, mode=1)\n", " print(\"已配置数据速率:200 SPS,单次转换模式\")\n", "\n", " # ========== 第三步:设置输入通道 ==========\n", " # 设置输入多路复用器,选择要测量的模拟输入引脚\n", " # pos_channel=0:正输入端接 AIN0 引脚(连接第一个应变片)\n", " # neg_channel=12:负输入端接 AINCOM(公共端,单端测量方式)\n", " adc.set_input_mux(pos_channel=0, neg_channel=12)\n", " print(\"已设置输入通道:AIN0(正)↔ AINCOM(负)\")\n", "\n", " # ========== 第四步:读取单次数据 ==========\n", " # 同时测量 3 个通道(0, 1, 2)的电压\n", " # request_channels 会逐个切换通道、启动转换、等待完成、读取结果\n", " # 返回值是 24 位有符号整数的列表,代表 ADC 原始采样值\n", " channels = [0, 1, 2]\n", " raw_values = adc.request_channels(channels=channels)\n", " print(f\"ADC 原始采样值:{raw_values}\")\n", "\n", " # ========== 第五步:转换为电压值 ==========\n", " # 将 ADC 原始数值转换为实际的电压值(单位:伏特)\n", " # 转换公式:电压 = 原始值 × Vref / (Gain × 2^23)\n", " # 其中 Vref=2.5V(内部参考电压),Gain 为当前 PGA 增益\n", " for ch, raw in zip(channels, raw_values):\n", " voltage = adc.convert_to_voltage(raw)\n", " print(f\"通道 {ch}:ADC={raw:8d},电压={voltage:.6f} V\")\n", "\n", "finally:\n", " # ========== 第六步:清理 ==========\n", " # 关闭 SPI 总线,将所有 GPIO 引脚置为低电平并释放\n", " adc.close()\n", " print(\"ADC 示例运行完毕,资源已释放\")\n", "\n", "# ⚠️ 以上代码需要连接真实的 ADS124S08 ADC 模块和树莓派才能运行" ] }, { "cell_type": "markdown", "id": "cell_9_markdown", "metadata": {}, "source": [ "## 完整控制循环演示(简化版 — 模拟运行)\n", "\n", "本示例演示一个简单的闭环控制流程:生成一个正弦波形的目标速度信号,\n", "通过 EncoderMotor 发送速度指令,同时通过 ADC 采集数据,并打印实时结果。\n", "\n", "> **注意**:这个循环只是一个教学演示,不要求真实硬件运行。\n", "> 它演示了控制逻辑的结构,实际使用时需要连接硬件并调整参数。" ] }, { "cell_type": "code", "execution_count": null, "id": "cell_10_control_loop", "metadata": {}, "outputs": [], "source": [ "# ========== 定义正弦信号生成函数 ==========\n", "# 这个函数生成一个简单的正弦波速度信号\n", "# 参数:\n", "# t: 当前时间(秒)\n", "# 返回值:目标速度(米/秒)\n", "def generate_sine_speed(t):\n", " \"\"\"生成一个正弦波形的目标速度信号\"\"\"\n", " # 速度 = 0.02 × sin(2 × π × 0.2 × t)\n", " # 0.02:速度幅值(米/秒)\n", " # 0.2:频率(Hz),每 5 秒完成一个完整正弦周期\n", " return 0.02 * math.sin(2 * math.pi * 0.2 * t)\n", "\n", "# ========== 控制循环参数 ==========\n", "DURATION = 3.0 # 总运行时长 3 秒\n", "INTERVAL = 0.5 # 每 0.5 秒执行一次控制\n", "\n", "print(\"=\" * 60)\n", "print(\"控制循环演示(虚拟运行,不依赖硬件)\")\n", "print(\"=\" * 60)\n", "\n", "# 记录起始时间\n", "t_start = time.time()\n", "# 记录上次执行的时间\n", "t_last = t_start\n", "# 步数计数器\n", "step = 0\n", "\n", "# 使用 with 语句创建硬件驱动实例,保证资源自动释放\n", "with EncoderMotorDriver(bus=1) as driver:\n", " # 预先配置电机参数(只需要配置一次)\n", " driver.set_mode(0, SPEED_MODE) # 设为速度模式\n", " driver.set_speed_pid(0, kp=1, ki=100, kd=1) # 配置 PID 参数\n", " driver.set_soft_start_and_stop(0, True) # 启用软启停\n", "\n", " # 进入主循环\n", " while True:\n", " # 获取当前时间\n", " t_now = time.time()\n", " # 计算从启动开始的经过时间\n", " elapsed = t_now - t_start\n", "\n", " # 检查是否到达运行时长\n", " if elapsed >= DURATION:\n", " print(f\"\\n运行完成,共执行 {step} 步\")\n", " break\n", "\n", " # 每 INTERVAL 秒执行一次\n", " if t_now - t_last >= INTERVAL:\n", " # 生成当前时刻的目标速度(正弦波)\n", " target_speed = generate_sine_speed(elapsed)\n", "\n", " # ==== 通过 EncoderMotor 发送速度指令 ====\n", " # 将生成的目标速度写入电机驱动\n", " driver.set_linear_speed_m_s(0, target_speed)\n", "\n", " # ==== 通过 ADC 采集数据(如果有硬件)====\n", " # 此处用模拟数据代替(实际使用时取消下面注释)\n", " # adc_values = adc.request_channels(channels=[0])\n", " # adc_voltage = adc.convert_to_voltage(adc_values[0])\n", " adc_voltage = 0.0 # 模拟值,无硬件时返回 0\n", "\n", " # 打印实时结果\n", " print(f\"步 {step:2d} | 时间 {elapsed:4.1f}s | \"\n", " f\"目标速度 {target_speed:+.4f} m/s | \"\n", " f\"ADC 电压 {adc_voltage:.6f} V\")\n", "\n", " # 更新步数计数和上次执行时间\n", " step += 1\n", " t_last = t_now\n", "\n", " # 短暂休眠,减少 CPU 占用\n", " time.sleep(0.01)\n", "\n", " # 循环结束后,将速度设为 0,让电机停止\n", " driver.set_linear_speed_m_s(0, 0.0)\n", "\n", "# 退出 with 语句块,EncoderMotor 驱动自动关闭\n", "print(\"控制循环演示结束\")\n", "\n", "# ⚠️ 要实际驱动硬件,需要连接 M5Stack 4EncoderMotor 模块和 ADS124S08 ADC 模块" ] }, { "cell_type": "markdown", "id": "cell_11_markdown", "metadata": {}, "source": [ "## 资源清理总结\n", "\n", "在使用硬件驱动时,**正确释放资源**非常重要,否则可能会导致后续程序无法正常访问设备。\n", "\n", "Python 提供了两种主要方式来确保资源释放:\n", "\n", "### 方式一:`with` 语句(推荐)\n", "\n", "所有三个驱动模块都支持 `with` 语句。当代码块执行完毕(无论是否发生异常),\n", "驱动对象会自动调用 `close()` 方法释放资源。\n", "\n", "### 方式二:`try/finally` 语句\n", "\n", "在不使用 `with` 的情况下,必须在 `finally` 块中手动调用 `close()` 方法。\n", "\n", "下面给出三种设备完整的清理示例。" ] }, { "cell_type": "code", "execution_count": null, "id": "cell_12_cleanup", "metadata": {}, "outputs": [], "source": [ "# ========== 方式一:使用 with 语句(推荐)==========\n", "# EncoderMotor 使用 with 语句自动清理\n", "print(\"--- 示例:with 语句自动释放 ---\")\n", "with EncoderMotorDriver(bus=1) as driver:\n", " # 在这里进行各种操作\n", " driver.set_mode(0, SPEED_MODE)\n", " driver.set_speed_pid(0, kp=1, ki=100, kd=1)\n", " print(\"EncoderMotor 已配置完成(with 语句块内)\")\n", "# 退出 with 语句块后,I2C 资源已被自动关闭\n", "print(\"EncoderMotor 资源已自动释放(退出 with 语句块)\\n\")\n", "\n", "# ========== 方式二:使用 try/finally 手动清理 ==========\n", "# StepMotor 和 ADC 如果没有用 with,则需手动清理\n", "print(\"--- 示例:try/finally 手动清理 ---\")\n", "drv = StepMotorDriver()\n", "try:\n", " # 执行各种控制操作\n", " drv.enable_motor()\n", " for i in range(3):\n", " drv.reset_motor(i, True)\n", " print(\"StepMotor 已配置完成(try 块内)\")\n", "finally:\n", " # 无论 try 块中是否发生异常,都会执行清理\n", " drv.close()\n", " print(\"StepMotor 资源已释放(finally 块中)\")\n", "\n", "print()\n", "\n", "# ========== StepMotor 也支持 with 语句 ==========\n", "print(\"--- 示例:StepMotor 也可以用 with 语句 ---\")\n", "with StepMotorDriver() as drv:\n", " drv.enable_motor()\n", " print(\"StepMotor 操作中(with 语句块内)\")\n", "print(\"StepMotor 资源已自动释放(退出 with 语句块)\\n\")\n", "\n", "# ========== ADC 使用 try/finally 清理 ==========\n", "print(\"--- 示例:ADC 手动清理 ---\")\n", "adc = ADS124S08()\n", "try:\n", " # 配置并读出一些数据\n", " adc.set_pga(pga_en=1, gain=5)\n", " adc.set_datarate(dr=8, mode=1)\n", " print(\"ADC 已配置完成(try 块内)\")\n", "finally:\n", " # 关闭 SPI 总线和 GPIO 引脚\n", " adc.close()\n", " print(\"ADC 资源已释放(finally 块中)\")\n", "\n", "print(\"\\n所有资源清理演示完成\")\n", "\n", "# ⚠️ 以上代码需要连接真实硬件才能成功创建设备实例" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.10.0" } }, "nbformat": 4, "nbformat_minor": 5 }