使用PYNQ生成PWM波控制舵机/步进电机/机械臂

在开始这个工程之前,你需要PYNQ-Z2的板卡文件,约束文件,原理图作为参考,你可以在我上传的资源里下载

当然,这个工程也适用于PYNQ-Z1,只需要改一下板卡文件和约束文件。

我使用的舵机如下:

1. PWM 信号

脉宽调制(PWM)是一种调制技术,它生成可变宽度的脉冲来表示模拟输入信号的幅度。

我们关注两个参数:

信号频率(或周期)信号占空比

  • 周期是信号从低到高所需的时间

  • 占空比是高电平时间占信号周期的比例。

2. 用于 PWM 的 IP 核:Axi Timer

Xilinx 提供的唯一能够生成 PWM 信号的 IP 核是 Axi Timer。Axi Timer 由 S_AXI 接口控制的,它输出两种类型的输出:

  • 一个定时器输出,用于生成一个简单的时钟定时器

  • 一个 PWM,用于生成一个 PWM 信号

因此,根据这一点,我们将创建一个设计,该设计生成一个与 PYNQ Z2 的特定引脚相连的 PWM 信号。

3. PWM 的 Vivado 设计

Vivado 流程是通用的:

  • 首先,新建一个vivado工程,步骤如下:









  • 创建一个Block Design,步骤如下:





  • AXI Timer的添加也类似,想要驱动几个舵机,就添加几个AXI Timer,由于我控制的是四自由度的机械臂,所以这里用 4 个 AXI Timer,然后让它们自动连线:







  • 将 4 个 PWM 端口引出来,并且修改端口名字:



  • 可以点击这里验证自己的设计是否正确:

  • 在我们的设计中,我们定义了以下约束文件(对于4自由度的机械臂,我们需要4个PWM信号控制4个舵机,所以我们连接了 PMODB下面的 4 个引脚):







  • 约束文件内容如下:

    set_property -dict {PACKAGE_PIN W14 IOSTANDARD LVCMOS33} [get_ports {motor0}]
    set_property -dict {PACKAGE_PIN Y14 IOSTANDARD LVCMOS33} [get_ports {motor1}]
    set_property -dict {PACKAGE_PIN T11 IOSTANDARD LVCMOS33} [get_ports {motor2}]
    set_property -dict {PACKAGE_PIN T10 IOSTANDARD LVCMOS33} [get_ports {motor3}]
  • 现在讲解怎么确定管脚的绑定,根据PYNQ-Z2原理图,我们可以看到,PMODB下面四个管脚对应的序号为JB1P,JB1N,JB2P,JB2N,在IO口(BANK)寻找对应的序号,我们发现,这四个序号对应的管脚编号分别为W14,Y14,T11,T10,将 Block Design 中引出的 PWM 端口绑定到这四个管脚上,这样我们约束文件也就确定好了。



  • Create HDL Wrapper(将设计的顶层模块包装成一个HDL文件)

  • 最后,生成 bit 流,当看到右上角write_bitstream Complete说明比特流生成成功。



  • 比特流生成成功后,你将会在pwm_stepper_motor\pwm_stepper_motor.runs\impl_1看到生成的design_1_wrapper.bit文件,在pwm_stepper_motor\pwm_stepper_motor.srcs\sources_1\bd\design_1\hw_handoff文件夹中看到生成的design_1.hwh,这两个是pynq开发的必要文件,将它们改名为pwm_stepper_motor.bitpwm_stepper_motor.hwh注意!两个文件名要相同)。
  • 打开 Jupyter Notebook ,创建一个pwm_stepper_motor.ipynb文件,将它和pwm_stepper_motor.bitpwm_stepper_motor.hwh这两个文件放在同一个文件夹下。

4.生成 PWM 的 Pynq 代码

现在,我们有了比特流。我们如何使用它呢?

你必须阅读 Axi Timer 的文档 。

Axi Timer 的文档在这里:https://www.xilinx.com/support/documentation/ip_documentation/axi_timer/v2_0/pg079-axi-timer.pdf

与所有 ip 核类似,你需要控制下面显示的控制/状态寄存器:

因此,我们需要在这个寄存器的特定位上写入值,以激活 PWM 信号发生器。

特别地,我们需要以下配置:

  • TCSR0 的 PWMA0 位和 TCSR1 的 PWMB0 位必须设置为 1 以启用 PWM 模式。
  • 必须在 TCSR 中启用 GenerateOut 信号(GENT 位设置为 1)。PWM0 信号由 Timer 0 和 Timer 1 的 GenerateOut 信号生成,因此这些信号必须在两个定时器中都启用;
  • 计数器 UDT 可以设置为向上或向下计数(我更喜欢设置为 1);
  • 将 Autoreload 寄存器 ARHT0 设置为 1;
  • 使能定时器将 ENT0 设置为 1。
  • 完成后,我们可以设置生成的 PWM 信号的周期和占空比。

4.1 周期设置

AXI Timer IP通过将输入的计数值转换为实际的时间周期来工作。在这个例子中,AXI Timer的输入时钟频率是50MHz。这意味着每个计数单位表示20ns的时间(1 / 50MHz = 20ns)。

为了将计数值转换为实际的周期时间,我们需要考虑输入时钟的频率。在这个例子中,我们将_period_值设为10000,乘以100,得到周期计数值:

period = int((_period_ & 0x0ffff) * 100)

这里,我们将_period_值限制为16位(通过将其与0x0ffff进行位与操作),然后将其乘以100。这样,我们得到的period变量值为1000000,这表示1000000个20ns的计数单位,即20ms的实际周期时间。

接下来,我们将计算出的周期值写入AXI Timer的TLR0寄存器:

motor0.write(TLR0['address_offset'], period)

4.2 占空比设置

在这个示例中,计算占空比的方法是基于期望的百分比值。给定一个占空比的百分比,我们可以计算与之对应的脉冲宽度计数值。

首先,我们设置期望的占空比百分比:

_pulse_ = 50  # 50% 占空比

然后,我们计算占空比对应的脉冲宽度计数值。为此,我们首先将占空比限制为7位(通过将其与0x07f进行位与操作),然后将其乘以计算出的周期计数值(period)除以100。这将把占空比的百分比值转换为实际的脉冲宽度计数值:

pulse = int((_pulse_ & 0x07f) * period / 100)

在这个例子中,我们得到的pulse变量值为500000,这表示500000个20ns的计数单位,即10ms的实际脉冲宽度时间。因此,占空比为50%(10ms脉冲宽度 / 20ms周期 = 0.5)。

最后,我们将计算出的脉冲宽度值写入AXI Timer的TLR1寄存器:

motor0.write(TLR1['address_offset'], pulse)

这将配置AXI Timer,使其产生一个占空比为50%的PWM信号。这样,我们就可以根据需要调整PWM信号的周期和占空比,以便控制连接到AXI Timer的设备(例如舵机或其他类型的马达)。

好的,现在我们准备好控制 PWM 了!以下是一些用于生成 PWM 的 PYNQ 代码(在单个 Axi Timer 上):

from pynq import Overlay

# 用于位操作的实用函数
def set_bit(value, bit):
return value | (1 << bit) def clear_bit(value, bit):
return value & ~(1 << bit) def get_bit(value, bit):
return (value >> bit) & 1 ol = Overlay("pwm_stepper_motor.bit")
motor0 = ol.axi_timer_0 # 提取寄存器地址(每个Axi Timer的寄存器地址都相同)
TCSR0 = ol.ip_dict['axi_timer_0']['registers']['TCSR0']
TCSR1 = ol.ip_dict['axi_timer_0']['registers']['TCSR1']
TCSR0_address = TCSR0['address_offset']
TCSR1_address = TCSR1['address_offset']
TCSR0_register = TCSR0['fields'] # bit_offset for address
TCSR1_register = TCSR1['fields']
TLR0 = ol.ip_dict['axi_timer_0']['registers']['TLR0']
TLR1 = ol.ip_dict['axi_timer_0']['registers']['TLR1']
TLR0_address = TLR0['address_offset']
TLR1_address = TLR1['address_offset'] # 创建控制寄存器的配置值
temp_val_0 = 0
temp_val_1 = 0 # 要启用PWM模式,必须将TCSR0中的PWMA0位和TCSR1中的PWMB0位设置为1
temp_val_0 = set_bit(temp_val_0, TCSR0_register['PWMA0']['bit_offset'])
temp_val_1 = set_bit(temp_val_1, TCSR1_register['PWMA1']['bit_offset']) # 在TCSR中必须启用GenerateOut信号(将GENT位设置为1)
# PWM0信号是由Timer 0和Timer 1的GenerateOut信号生成的
# 因此必须在两个定时器中启用这些信号
temp_val_0 = set_bit(temp_val_0, TCSR0_register['GENT0']['bit_offset'])
temp_val_1 = set_bit(temp_val_1, TCSR1_register['GENT1']['bit_offset']) # 计数器可以设置为向上计数或向下计数
temp_val_0 = set_bit(temp_val_0, TCSR0_register['UDT0']['bit_offset'])
temp_val_1 = set_bit(temp_val_1, TCSR1_register['UDT1']['bit_offset']) # 将ARHT0位设置为1
temp_val_0 = set_bit(temp_val_0, TCSR0_register['ARHT0']['bit_offset'])
temp_val_1 = set_bit(temp_val_1, TCSR1_register['ARHT1']['bit_offset']) # 要启用定时器,可以将ENT0位设置为1
temp_val_0 = set_bit(temp_val_0, TCSR0_register['ENT0']['bit_offset'])
temp_val_1 = set_bit(temp_val_1, TCSR1_register['ENT1']['bit_offset']) # 这里您应该会看到两次"0b1010010110"
print(bin(temp_val_0))
print(bin(temp_val_1)) # 现在我们可以生成PWM信号了
_period_ = 10000 # 50Hz, 20ms
_pulse_ = 50 # 50% 占空比
period = int((_period_ & 0x0ffff) *100);
pulse = int((_pulse_ & 0x07f)*period/100);
print("period 20ms", period)
print("pulse 50%", pulse) motor0.write(TCSR0['address_offset'], temp_val_0)
motor0.write(TCSR1['address_offset'], temp_val_1)
motor0.write(TLR0['address_offset'], period)
motor0.write(TLR1['address_offset'], pulse)

运行该代码,如果你将示波器连接到PMOD B的引脚,您将会看到一个PWM信号!

如果您想使用图形界面更改PWM的周期,您可以使用此Jupyter Notebook代码:

from ipywidgets import interact, interactive, HBox, VBox
import ipywidgets as widgets def clicked(P=None, Torque=None): _period_ = P
_pulse_ = 50 # 50% 占空比
period = int((_period_ & 0x0ffff) *100);
pulse = int((_pulse_ & 0x07f)*period/100);
print("period 20ms", period)
print("pulse 50%", pulse) motor0.write(TCSR0['address_offset'], temp_val_0)
motor0.write(TCSR1['address_offset'], temp_val_1)
motor0.write(TLR0['address_offset'], period)
motor0.write(TLR1['address_offset'], pulse) w = interactive(clicked,
P=widgets.IntSlider(min=4000, max=30000, step=1, value=4000),
Torque=widgets.IntSlider(min=-400, max=400, step=1, value=0))
VBox([w.children[0], w.children[1]])

5. 伺服电机是如何工作的

我们知道你也很好奇,让我们来了解一下!



这个特定的伺服电机包含一个无刷电机、一些减速齿轮以减少电机的转速但增加扭矩、一个电路来将外部信号解释为旋转轴的特定位置,以及一个“H桥”以允许顺时针和逆时针旋转。

180°舵机:

角度 脉冲宽度 占空比
0 0.5ms 2.5%
45 1ms 5.0%
90 1.5ms 7.5%
135 2ms 10.0%
180 2.5ms 12.5%

因此,这种特定的伺服电机不能仅仅使用给定的电压或电流旋转,而是需要一个特定的命令来旋转到一个角度。该命令由一个PWM信号组成,改变信号的占空比,算法如下:

pulse = 2.5 + (angle/180)*10

最后,将周期和脉冲值发送到Axi Timer的相应寄存器中。

PYNQ代码如下:

def set_servo_angle(servo, angle):

    _period_ = 10000
_pulse_ = 2.5 + (angle/180)*10
period = int((_period_ & 0x0ffff) *100);
pulse = int((_pulse_ & 0x07f)*period/100); servo.write(TCSR0['address_offset'], temp_val_0)
servo.write(TCSR1['address_offset'], temp_val_1)
servo.write(TLR0['address_offset'], period)
servo.write(TLR1['address_offset'], pulse)

好的,让我们把所有的东西连接在一起,并创建一个简单的PYNQ代码来测试一下:

from pynq import Overlay

def set_servo_angle(servo, angle):

    _period_ = 10000
_pulse_ = 2.5 + (angle/180)*10
period = int((_period_ & 0x0ffff) *100);
pulse = int((_pulse_ & 0x07f)*period/100); servo.write(TCSR0['address_offset'], temp_val_0)
servo.write(TCSR1['address_offset'], temp_val_1)
servo.write(TLR0['address_offset'], period)
servo.write(TLR1['address_offset'], pulse) ol = Overlay("pwm.bit")
motor0 = ol.axi_timer_0
TCSR0 = ol.ip_dict['axi_timer_0']['registers']['TCSR0']
TCSR1 = ol.ip_dict['axi_timer_0']['registers']['TCSR1']
TCSR0_address = TCSR0['address_offset']
TCSR1_address = TCSR1['address_offset']
TCSR0_register = TCSR0['fields']
TCSR1_register = TCSR1['fields']
TLR0 = ol.ip_dict['axi_timer_0']['registers']['TLR0']
TLR1 = ol.ip_dict['axi_timer_0']['registers']['TLR1']
TLR0_address = TLR0['address_offset']
TLR1_address = TLR1['address_offset'] temp_val_0 = 0
temp_val_1 = 0 temp_val_0 = set_bit(temp_val_0, TCSR0_register['PWMA0']['bit_offset'])
temp_val_1 = set_bit(temp_val_1, TCSR1_register['PWMA1']['bit_offset']) temp_val_0 = set_bit(temp_val_0, TCSR0_register['GENT0']['bit_offset'])
temp_val_1 = set_bit(temp_val_1, TCSR1_register['GENT1']['bit_offset']) temp_val_0 = set_bit(temp_val_0, TCSR0_register['UDT0']['bit_offset'])
temp_val_1 = set_bit(temp_val_1, TCSR1_register['UDT1']['bit_offset']) temp_val_0 = set_bit(temp_val_0, TCSR0_register['ARHT0']['bit_offset'])
temp_val_1 = set_bit(temp_val_1, TCSR1_register['ARHT1']['bit_offset']) temp_val_0 = set_bit(temp_val_0, TCSR0_register['ENT0']['bit_offset'])
temp_val_1 = set_bit(temp_val_1, TCSR1_register['ENT1']['bit_offset']) set_servo_angle(motor0, 90)

如果一切正常,您可以看到电机旋转到90度。很酷,对吧?

使用PYNQ生成PWM波控制舵机/步进电机/机械臂的更多相关文章

  1. PWM波控制舵机总结

    文章转自:http://www.geek-workshop.com/thread-70-1-1.html 一.关于舵机: 舵机(英文叫Servo):它由直流电机.减速齿轮组.传感器和控制电路组成的一套 ...

  2. 使用引脚模拟PWM波控制引脚

    /********************************* 代码功能:输出PWM波控制引脚 使用函数: 创作时间:2016*10*07 作者邮箱:jikexianfeng@outlook.c ...

  3. 进阶之路(基础篇) - 005 模拟PWM波控制引脚

    /********************************* 代码功能:输出PWM波控制引脚 使用函数: 创作时间:2016*10*07 作者邮箱:jikexianfeng@outlook.c ...

  4. STM32F103定时器输出PWM波控制直流电机

    这个暑假没有回家,在学校准备九月份的电子设计竞赛.今天想给大家分享一下STM32高级定时器输出PWM波驱动直流电机的问题.. 要想用定时器输出的PWM控制直流电机,,首先要理解“通道”的概念..一个定 ...

  5. STM32用PWM波控制呼吸灯代码

    pwm.c #include "pwm.h" //TIM3-CH3 //PB0 void PWM_Config(void) { GPIO_InitTypeDef GPIO_Init ...

  6. 【.NET 与树莓派】控制舵机

    不管是小马达,还是大马达,嗯,也就是电机,相信大伙伴们也不会陌生.四驱车是一种很优秀的玩具,从老周小时候就开始流行(动画片<四驱兄弟>估计很多大朋友都看过),直到现在还能看到很多卖四驱车的 ...

  7. 一步步制作下棋机器人之 coppeliasim进行Scara机械臂仿真与python控制

    稚晖君又发布了新的机器人,很是强大. 在编写时看到了稚晖君的招聘信息,好想去试试啊! 小时候都有一个科幻梦,如今的职业也算与梦想有些沾边了.但看到稚晖君这种闪着光芒的作品,还是很是羡慕. 以前就想做一 ...

  8. [ZigBee] 13、ZigBee基础阶段性回顾与加深理解——用定时器1产生PWM来控制LED亮度(七色灯)

    引言:PWM对于很多软件工程师可能又熟悉又陌生,以PWM调节LED亮度为例,其本质是在每个周期都偷工减料一些,整体表现出LED欠压亮度不同的效果.像大家看到的七色彩灯其原理也类似,只是用3路PWM分别 ...

  9. yield和python(如何生成斐波那契數列)

    您可能听说过,带有 yield 的函数在 Python 中被称之为 generator(生成器),何谓 generator ? 我们先抛开 generator,以一个常见的编程题目来展示 yield ...

  10. stm32cube--通用定时器--产生pwm波

    看了通用定时器的资料,发现内容挺多,挺难看懂,现在还是先掌握使用方法,以后再多看几遍吧. ① ② ③生成mdk工程后,在main.c的while(1)前面加上HAL_TIM_PWM_Start(&am ...

随机推荐

  1. [天线原理及设计>基本原理] 2. 细线天线上的电流分配

    2. 细线天线上的电流分配 为了说明线性偶极子上电流分布的产生及其随后的辐射,让我们首先从无损双线传输线的几何形状开始,如图1.15(a)所示. 电荷的运动沿每条导线产生幅度为I0/2的行波电流.当电 ...

  2. AvaloniaChat-v0.0.2:兼容智谱AI 快速使用指南

    智谱AI介绍 北京智谱华章科技有限公司(简称"智谱AI")致力于打造新一代认知智能大模型,专注于做大模型的中国创新.公司合作研发了中英双语千亿级超大规模预训练模型GLM-130B, ...

  3. Echarts 5 动态按需引入图表

    官网提供的按需引入方法为全量按需引入,在打包分离中,仍旧存在使用不到的图表被打包进去. 例如:组件A使用了折线图.柱状图,组件B只用到了折线图,但是打包组件B的时候,柱状图也就被打包进去. 本文提供一 ...

  4. js_for循环的错误

    本段代码实现的效果是遍历数组中的每个元素,给每个元素插入一个类名 for (var i = 0; i < dropdownLi.length; i++) { if(i == 1){ contin ...

  5. Go 必知必会:探索 Go 语言中的数组和切片深入理解顺序集合

    文末有面经共享群 在 Go 语言的丰富数据类型中,数组和切片是处理有序数据集合的强大工具,它们允许开发者以连续的内存块来存储和管理相同类型的多个元素.无论是在处理大量数据时的性能优化,还是在实现算法时 ...

  6. C# Dynamic 转换成 Dictionary,Dynamic 转换成 DataTable

    部分软件开发的时候用到了 dynamic 类型,这个类型的数据不需要做其他处理的时候非常好用,但是需要对其中的数据调整的时候就不是那么好用了,这里提供两个常见的转换方式 Dynamic To Dict ...

  7. verilog vscode 与AI 插件

    Verilog 轻量化开发环境 背景 笔者常用的开发环境 VIAVDO, 体积巨大,自带编辑器除了linting 能用,编辑器几乎不能用,仿真界面很友好,但是速度比较慢. Sublime Text, ...

  8. CSS – Display block, inline, inline-block

    前言 之前 W3Schools 学习笔记就有提到了 CSS Layout - The display Property 这篇做更多的解释. 参考: CSS Display FLEX vs Block, ...

  9. MQ核心作用异步&削峰&解耦使用场景详解

    说在前面 在如今的高并发互联网应用中,如何确保系统在巨大的流量冲击下还能稳稳当当运转,是每个技术团队都会遇到的挑战.说到这,消息队列(MQ) 就是背后的"大功臣"了. 无论是异步处 ...

  10. 深入理解HDFS 错误恢复

    我们从动态的角度来看 hdfs 先从场景出发,我们知道 hdfs 的写文件的流程是这样的: 数据以 pipeline 的方式写入 hdfs ,然后对于读取操作,客户端选择其中一个保存块副本的 Data ...