01signal.com

使用 Smart Zynq控制八个伺服电机

该网页属于一组探索 Smart Zynq 电路板功能的小项目

介绍

市场上有几种简单且低成本的直流伺服电机,例如 SG90、 MG90S、 MG995 和 MG996R。这些电机适用于业余项目,特别是用于构建简单的机器人。此类电机由单个 PWM 信号控制。

有些电机在有限的角度范围内旋转,通常为 180 度。对于此类电机, PWM 信号控制电机的角度位置。其他电机(通常称为“360 度电机”)可以连续旋转。对于此类电机, PWM 信号控制旋转的速度和方向。请注意,电机模型通常有两种变体: 一种是角度有限的变体,另一种是可以连续旋转的变体。购买此类电机时,注意这种差异非常重要。

本教程将介绍如何将最多 8 个伺服电机连接到 Smart Zynq 电路板,并借助简单的 Linux 命令控制这些电机。本教程还演示了如何使用 Xillybus的 seekable 数据流在 FPGA内实现寄存器(register)接口。

为了简化说明和图片,本教程中显示的电线仅适用于一台电机。只需向此处所示的相同组件添加电线即可添加更多电机。

电机电气接口

与相关类型的伺服电机的连接由三根电线组成:

PWM 信号需要具有 20ms (50 Hz) 的周期。脉冲(pulse)为高电平的持续时间控制电机的位置或角速度。

根据大多数数据手册(datasheets),脉冲的持续时间应在 1ms 和 2ms之间。对于某些电机,此信息不正确。例如, Tower Pro的 SG90 (具有 180 度旋转限制)的正确范围大约在 500μs 到 2450μs之间。此范围对应于 180 度旋转。使用高达 2560μs的脉冲可以将电机稍微旋转得更远。此电机忽略比这更长的脉冲。

因此,为了探索电机响应的脉冲持续时间范围,最好对电机进行实验,而不是依赖数据手册。由于这些电机用于业余项目,因此其规格数据可能不准确。

电气连接

首先,伺服电机需要单独的电源来为机械电机提供能量。此电源的电压为 +5V。不建议使用为 Smart Zynq 电路板供电的电源,因为电机经常会导致电压水平突然变化。如果 Smart Zynq 电路板的电源电压不稳定,其行为可能会不可靠。

当该信号为高电平时,橙色线上的 PWM 信号应具有与电源大致相同的电压。换句话说,橙色线和接地之间的电压应为 0 或大约为 +5V。

但是, Smart Zynq 电路板的输出电压(output voltage)只有在高电平时才是 3.3V 。如果将这样的输出(output)直接连接到电机的橙色线,电机很有可能会正确响应。原因是基于 5V 电源的数字电路通常将任何高于 2.5V 的电压视为逻辑 '1'(logic '1')。

话虽如此,但不确定电机是否能在 PWM 信号上使用 3.3V 电压可靠地工作。建议的解决方案是使用电压 level shifter (voltage level shifter),将 Smart Zynq 电路板转换为 0V 或 5V。在本教程中,使用带有 TXS0108E 芯片的小型电路板。这块电路板只是暴露了芯片的引脚,这样普通的电线就可以连接到芯片。

这张图显示了控制一台伺服电机的电气连接方式: 图片底部的 Smart Zynq 电路板借助杜邦线(Dupont wires)连接到 TXS0108E 电路板。该电路板的另一侧连接到伺服电机的 PWM 输入以及电机的单独电源(通过标准电源插头的适配器)。电机的红色和棕色电线也焊接到这块电路板上,以便为电机提供电源电压。

Connecting the Smart Zynq board with a servo motor with the help of a TXS0108E voltage level shifter

这张图可以更近距离地观察 TXS0108E 电路板及其连接:

Connections to the TXS0108E voltage level shifter, in detail

注意 OE 和 VA之间的红线。这确保芯片的输出使能(output enable)为高。

下图显示了 Smart Zynq和排针的连接。电机由此处连接的 J6/1 控制。

Dupont wire connections with the Smart Zynq board for controlling a servo motor

下表总结了所涉及的四个部分之间的联系:

TXS0108E Board 引脚 Smart Zynq 引脚 电机线 +5V 电源 评论
VA J6/37 (3.3V) -- -- 也连接到 OE
A4 J6/1 -- --
OE -- -- -- 已连接至 VA
GND J6/35 (GND) 棕色线(wire) GND
VB -- 红色线 +5V
B4 -- 橙色线 --

TXS0108E 芯片从其 A4 输入到其 B4 输出执行电压 level shift (voltage level shift)。芯片上还有七对引脚。本教程中任意选择了 A4/B4 对。为了控制八个电机,可以使用此芯片的所有八个引脚对。这里没有显示这一点,因为控制八个电机所需的电线数量会使图片难以理解。

为了找到 J6 排针,请在 Smart Zynq 电路板背面寻找写有“Bank 33 VCCIO Vadj”的位置。靠近此标记的引脚行是本教程中使用的排针。因此, J6/1 是最靠近 HDMI 连接器的引脚。

请注意, Smart Zynq 电路板通过其 USB 端口之一连接到单独的电源。还请注意, J6 排针的最后一个引脚是 5V,因此不要将电线连接到此引脚。

准备 Vivado 项目

从演示包的(demo bundle)的 zip 文件(启动 partition kit(boot partition kit))创建一个新的 Vivado 项目。在文本编辑器中打开 verilog/src/xillydemo.v 。删除代码(code)中标记为“PART 4”的部分。插入以下内容代替该部分:

assign  user_r_mem_8_empty = 0;
   assign  user_r_mem_8_eof = 0;
   assign  user_w_mem_8_full = 0;

   reg [7:0] reg0, reg1, reg2, reg3, reg4, reg5, reg6, reg7;

   always @(posedge bus_clk)
     if (user_w_mem_8_wren)
       case (user_mem_8_addr[2:0])
	 0: reg0 <= user_w_mem_8_data;
	 1: reg1 <= user_w_mem_8_data;
	 2: reg2 <= user_w_mem_8_data;
	 3: reg3 <= user_w_mem_8_data;
	 4: reg4 <= user_w_mem_8_data;
	 5: reg5 <= user_w_mem_8_data;
	 6: reg6 <= user_w_mem_8_data;
	 7: reg7 <= user_w_mem_8_data;
       endcase

   reg [7:0] mem_8_data_reg;
   assign user_r_mem_8_data = mem_8_data_reg;

   always @(posedge bus_clk)
     if (user_r_mem_8_rden)
       case (user_mem_8_addr[2:0])
	 0: mem_8_data_reg <= reg0;
	 1: mem_8_data_reg <= reg1;
	 2: mem_8_data_reg <= reg2;
	 3: mem_8_data_reg <= reg3;
	 4: mem_8_data_reg <= reg4;
	 5: mem_8_data_reg <= reg5;
	 6: mem_8_data_reg <= reg6;
	 7: mem_8_data_reg <= reg7;
       endcase

   servo_pwm servo_pwm_i [7:0]
     (
      .clk(bus_clk),
      .rst(quiesce),
      .pwm_signal(J6[7:0]),
      .pwm_width( { reg7, reg6, reg5, reg4, reg3, reg2, reg1, reg0 } )
      );

也可以从此链接下载更新的 xillydemo.v 。

在项目的 verilog/src/ 目录中创建一个新文件 servo_pwm.v ,包含以下内容(使用复制-粘贴(copy-paste)或从此链接下载)。

module servo_pwm
  (
   input clk,
   input rst,

   output reg pwm_signal,
   input [7:0] pwm_width
  );

   reg [9:0]  enable_count;
   reg 	      enable;

   reg [10:0] pwm_count;
   reg [8:0]  threshold;

   always @(posedge clk)
     begin
	if (enable_count == 999)
	  begin
	     enable_count <= 0;
	     enable <= 1;
	  end
	else
	  begin
	     enable_count <= enable_count + 1;
	     enable <= 0;
	  end

	threshold <= pwm_width + 50; // Add 0.5 ms to PWM width

	if (enable)
	  begin
	     if (pwm_count == 1999)
	       pwm_count <= 0;
	     else
	       pwm_count <= pwm_count + 1;

	     pwm_signal <= (pwm_count < threshold);
	  end

	if (rst)
	  begin
	     enable_count <= 0;
	     pwm_count <= 0;
	     pwm_signal <= 0;
	  end
     end
endmodule

然后将该文件添加到 Vivado 项目中: 点击 File > Add Sources… ,选择“Add or create design sources”。然后点击 Next。点击 "Add Files" 按钮,在 verilog/src/ 目录中选择名为“servo_pwm.v”的文件。然后点击 "Finish" 按钮。

按照与为演示包的创建比特流(bitstream)文件相同的方式从更新的项目创建比特流文件。同样以相同的方式将比特流文件复制到 TF 卡(用此项目创建的文件覆盖旧的 xillydemo.bit 文件)。

控制伺服电机

本节所示的这些步骤应在 Smart Zynq 电路板上的 shell 提示符上执行。

首先,将目录(directory)更改为 Xillybus,并执行编译(compilation)的演示应用程序:

# cd ~/xillybus/demoapps/
# make
gcc  -g -Wall -O3 memwrite.c -o memwrite
gcc  -g -Wall -O3 memread.c -o memread
gcc  -g -Wall -O3 streamread.c -o streamread
gcc  -g -Wall -O3 streamwrite.c -o streamwrite
gcc  -g -Wall -O3 -pthread fifo.c -o fifo

FPGA 中有八个寄存器(registers),用于控制伺服电机。初始时它们的值都是零。这是如何以十进制格式打印出这些值的。

# hexdump -v -n 8 -e '8/1 "%u " "\n" ' /dev/xillybus_mem_8
0 0 0 0 0 0 0 0

可以使用更简单的 hexdump 命令以十六进制格式打印出相同的值:

# hexdump -v -n 8 -C /dev/xillybus_mem_8
00000000  00 00 00 00 00 00 00 00                           |........|
00000008

memwrite 程序(位于 xillybus/demoapps/ 目录内部)可用于控制电机。例如,此命令将寄存器 0 (register 0)中的值更改为 120:

# ./memwrite /dev/xillybus_mem_8 0 120

这会改变连接到 J6/1 的电机的旋转位置或速度(即如上图所示)。

PWM 脉冲的宽度取决于寄存器的值,具体公式如下:

t = 500 + (x * 10)

在这个公式中, t 以微秒为单位, x 是相关寄存器的值。因此,脉冲的默认宽度为 500μs (即 x=0)。上面的命令将寄存器的值更改为 120。因此,脉冲的宽度更改为 1700μs。

每个寄存器由一个字节组成,因此其值范围从 0 到 255。因此,每个电机的脉冲可以修改为 500μs 和 3050μs之间的宽度。请注意,大多数电机认为此值范围的一部分是非法的。最好对每个电机尝试不同的可能性,看看它如何反应。

为了控制连接到另一个引脚的电机,请写入不同的寄存器。例如,如果电机连接到 Smart Zynq的 J6/4,则可以使用此命令,例如:

# ./memwrite /dev/xillybus_mem_8 3 50

这个命令把脉冲的宽度改成了 1000μs (也就是 1 ms)。

这两个命令之后,可以通过读回值来看到变化:

# hexdump -v -n 8 -e '8/1 "%u " "\n" ' /dev/xillybus_mem_8
120 0 0 50 0 0 0 0

这将以十进制格式显示值。对于十六进制格式:

# hexdump -v -n 8 -C /dev/xillybus_mem_8
00000000  78 00 00 32 00 00 00 00                           |x..2....|
00000008

memwrite 的工作原理

memwrite 中的源代码(source code)是 ~/xillybus/demoapps/ 目录中名为 memwrite.c 的文件。这个程序(program)展示了如何借助 Xillybus的 seekable 数据流访问寄存器。如果你想了解这个程序是如何工作的,我建议看看源代码。下面我只会介绍两个重要的部分。

程序(program)打开第一 argument中指定名称的文件。该文件的 file descriptor 存储在变量 @fd(variable @fd)中。

然后,程序对 lseek() 执行如下函数调用:

if (lseek(fd, address, SEEK_SET) < 0) {
    perror("Failed to seek");
    exit(1);
  }

变量 @address (variable @address)包含程序的第二 argument。在上面使用 memwrite 的第一个例子中,这是 0。在第二个例子中,它是 3。通常, lseek() 用于移动到文件内的特定位置。在这种情况下,文件中的位置与我们要访问的寄存器的编号相同。

接下来,程序对 allwrite()进行函数调用:

allwrite(fd, &data, 1);

这会将一个字节写入文件。 allwrite() 在 memwrite.c中定义,与众所周知的函数 write()(function write())类似。因此,不使用 allwrite(),这几乎是一样的:

write(fd, &data, 1);

不同之处在于 write() 不确保数据被写入。而函数 allwrite()则保证数据被写入文件。

在本例中,对 allwrite() 的函数调用会写入程序的第三 argument的值。换句话说,这是寄存器的新值。

有关 Xillybus的应用程序接口(API)对于主机(host)端的更多详细信息,请参阅有关此主题的文档,特别是 6.1部分。

关于 Verilog 代码的说明

我将首先展示 xillydemo.v 中的 Verilog 代码如何实现 FPGA中的寄存器。接下来是 PWM 脉冲的实现。

对于实现硬件寄存器的Xillybus和应用程序接口,在Xillybus FPGA designer's guide中有详细的说明。

与寄存器相关的线已经在原始 xillydemo.v 文件中定义:

// Wires related to /dev/xillybus_mem_8
  wire  user_r_mem_8_rden;
  wire  user_r_mem_8_empty;
  wire [7:0] user_r_mem_8_data;
  wire  user_r_mem_8_eof;
  wire  user_r_mem_8_open;
  wire  user_w_mem_8_wren;
  wire  user_w_mem_8_full;
  wire [7:0] user_w_mem_8_data;
  wire  user_w_mem_8_open;
  wire [4:0] user_mem_8_addr;
  wire  user_mem_8_addr_update;

这些线作为例化(instantiation)的一部分连接到 Xillybus IP core :

xillybus xillybus_ins (

    // Ports related to /dev/xillybus_mem_8
    // FPGA to CPU signals:
    .user_r_mem_8_rden(user_r_mem_8_rden),
    .user_r_mem_8_empty(user_r_mem_8_empty),
    .user_r_mem_8_data(user_r_mem_8_data),
    .user_r_mem_8_eof(user_r_mem_8_eof),
    .user_r_mem_8_open(user_r_mem_8_open),

    // CPU to FPGA signals:
    .user_w_mem_8_wren(user_w_mem_8_wren),
    .user_w_mem_8_full(user_w_mem_8_full),
    .user_w_mem_8_data(user_w_mem_8_data),
    .user_w_mem_8_open(user_w_mem_8_open),

    // Address signals:
    .user_mem_8_addr(user_mem_8_addr),
    .user_mem_8_addr_update(user_mem_8_addr_update),

[ ... ]

    .quiesce(quiesce)
  );

接下来,我们来看看专门为这个项目添加的 Verilog 代码。它从这部分开始,确保在与主机进行数据交换时不应用任何 flow control :

assign  user_r_mem_8_empty = 0;
   assign  user_r_mem_8_eof = 0;
   assign  user_w_mem_8_full = 0;

接下来声明八个寄存器:

reg [7:0] reg0, reg1, reg2, reg3, reg4, reg5, reg6, reg7;

接下来的部分实现对寄存器的写操作: 当 @user_w_mem_8_wren 为高电平时,会将新值写入寄存器之一。 @user_mem_8_addr 选择受影响的寄存器。 @user_w_mem_8_wren 和 @user_mem_8_addr 都是 Xillybus IP core的输出。

always @(posedge bus_clk)
     if (user_w_mem_8_wren)
       case (user_mem_8_addr[2:0])
	 0: reg0 <= user_w_mem_8_data;
	 1: reg1 <= user_w_mem_8_data;
	 2: reg2 <= user_w_mem_8_data;
	 3: reg3 <= user_w_mem_8_data;
	 4: reg4 <= user_w_mem_8_data;
	 5: reg5 <= user_w_mem_8_data;
	 6: reg6 <= user_w_mem_8_data;
	 7: reg7 <= user_w_mem_8_data;
       endcase

此后,就可以从这些寄存器读取值了: 当 @user_r_mem_8_rden 为高电平时, @user_r_mem_8_data 会更新,因此它包含寄存器之一的值。 @user_mem_8_addr 选择读取哪个寄存器的值。

reg [7:0] mem_8_data_reg;
   assign user_r_mem_8_data = mem_8_data_reg;

   always @(posedge bus_clk)
     if (user_r_mem_8_rden)
       case (user_mem_8_addr[2:0])
	 0: mem_8_data_reg <= reg0;
	 1: mem_8_data_reg <= reg1;
	 2: mem_8_data_reg <= reg2;
	 3: mem_8_data_reg <= reg3;
	 4: mem_8_data_reg <= reg4;
	 5: mem_8_data_reg <= reg5;
	 6: mem_8_data_reg <= reg6;
	 7: mem_8_data_reg <= reg7;
       endcase

最后, servo_pwm 模块的例化发生。

servo_pwm servo_pwm_i [7:0]
     (
      .clk(bus_clk),
      .rst(quiesce),
      .pwm_signal(J6[7:0]),
      .pwm_width( { reg7, reg6, reg5, reg4, reg3, reg2, reg1, reg0 } )
      );

请注意,此例化创建了八个相同的 servo_pwm副本: 第一个副本连接到 reg0 和 J6[0],第二个副本连接到 reg1 和 J6[1] 等。

现在我们来看看 servo_pwm 模块。首先声明模块的端口和寄存器:

module servo_pwm
  (
   input clk,
   input rst,

   output reg pwm_signal,
   input [7:0] pwm_width
  );

   reg [9:0]  enable_count;
   reg 	      enable;

   reg [10:0] pwm_count;
   reg [8:0]  threshold;

回想一下, @pwm_signal 接收寄存器的值,该值在主机上的 memwrite 命令的帮助下进行更新。 @pwm_signal 连接到 Smart Zynq 电路板的引脚,因此这是通过橙色线到达电机的信号。

这个模块的下一部分实现了 strobe 信号: 每 1000个时钟周期(clock cycles)周期, @enable 为高电平一次。 @clk的频率为 100 MHz。因此,每 10μs周期, @enable 为高电平一次。

always @(posedge clk)
     begin
	if (enable_count == 999)
	  begin
	     enable_count <= 0;
	     enable <= 1;
	  end
	else
	  begin
	     enable_count <= enable_count + 1;
	     enable <= 0;
	  end

前面提到过,寄存器的取值范围从 0 到 255 对应着 PWM 脉冲的宽度在 500μs 和 3050μs 之间(脉冲的微秒宽度对应的表达式(expression)是 500 + x * 10)。

因此这是 @threshold的表达式。

threshold <= pwm_width + 50; // Add 0.5 ms to PWM width

这个寄存器包含 PWM 脉冲期间 @enable 为高的次数(即当 PWM signal 为高时)。这个表达式(expression)反映了寄存器的值是 PWM 脉冲的长度,以 10μs为单位。将 50 添加到寄存器的值中对应于 500μs的最小脉冲宽度。

@enable 现在用作此部件的时钟使能(clock enable):

if (enable)
	  begin
	     if (pwm_count == 1999)
	       pwm_count <= 0;
	     else
	       pwm_count <= pwm_count + 1;

	     pwm_signal <= (pwm_count < threshold);
	  end

@pwm_count 从 0 计数到 1999 并重新开始。这个计数器在每 10μs中只改变一次,因此它在每 20000μs = 20 ms中完成一个完整的循环。换句话说, PWM 脉冲的重复周期是 20ms,正如所要求的那样。

只要 @pwm_count 小于 @threshold,@pwm_signal 就为高。这就是脉冲的宽度受寄存器控制的方式。

此模块的最后一部分包括重置一些寄存器:

if (rst)
	  begin
	     enable_count <= 0;
	     pwm_count <= 0;
	     pwm_signal <= 0;
	  end
     end
endmodule

当 @rst 为高电平时,该部分将取代上述内容。因此, @rst 具有复位信号(reset signal)的功能。

Verilog 代码和真实引脚之间的关系

上面的 Verilog 代码使用了名为 J6的 inout 端口,但是与这个端口的连接如何到达排针?答案可以在 xillydemo.xdc中找到。此文件是创建比特流(在 "vivado-essentials" 目录中)的 Vivado 项目的一部分。

xillydemo.xdc 包含 FPGA 作为电子元件正常工作所需的各种信息。其中,该文件包含以下行:

[ ... ]

## J6 on board (BANK33 VADJ)
set_property PACKAGE_PIN U22  [get_ports {J6[0]}];   #J6/1  = IO_B33_LN2
set_property PACKAGE_PIN T22  [get_ports {J6[1]}];   #J6/2  = IO_B33_LP2
set_property PACKAGE_PIN W22  [get_ports {J6[2]}];   #J6/3  = IO_B33_LN3
set_property PACKAGE_PIN V22  [get_ports {J6[3]}];   #J6/4  = IO_B33_LP3
set_property PACKAGE_PIN Y21  [get_ports {J6[4]}];   #J6/5  = IO_B33_LN9
set_property PACKAGE_PIN Y20  [get_ports {J6[5]}];   #J6/6  = IO_B33_LP9
set_property PACKAGE_PIN AB22 [get_ports {J6[6]}];   #J6/7  = IO_B33_LN7
set_property PACKAGE_PIN AA22 [get_ports {J6[7]}];   #J6/8  = IO_B33_LP7

[ ... ]

第一行表示端口 J6[0] (port J6[0])应该连接到 U22。这是 FPGA物理封装上的位置。根据 Smart Zynq的原理图(schematics),这个 FPGA 引脚连接到排针的第一个引脚。其他端口的位置定义方式相同。

概括

本教程展示了如何借助 Smart Zynq 电路板和 Xillybus的寄存器应用程序接口(register API)来控制伺服电机。介绍了用于此应用的主机应用(host application)以及 Verilog 代码。

本教程还可以作为其他需要借助寄存器控制硬件的应用程序的基础。

此页面由英文自动翻译。 如果有不清楚的地方,请参考原始页面
Copyright © 2021-2024. All rights reserved. (38a9d8fd)