01signal.com

用 Smart Zynq 控制八个舵机

本页面属于一个小项目系列,这些项目用于探索 Smart Zynq 开发板的各种功能。

简介

市面上有几种结构简单、价格低廉的直流舵机,例如 SG90、MG90S、MG995 和 MG996R。这些舵机用于业余爱好项目,特别是制作简易机器人。这类舵机通过单一的 PWM 信号控制。

有些舵机只能在有限的角度范围内旋转,通常是 180 度。对这种舵机,PWM 信号控制舵机的角位置。另一些舵机(常被称为“360 度舵机”)可以连续旋转。对这种舵机,PWM 信号控制旋转的速度和方向。注意,同一个型号的舵机常常有两种版本:一种限制转动角度,另一种可以连续旋转。购买这类舵机时,留意这一区别很重要。

本教程展示如何将最多八个舵机连接到 Smart Zynq 开发板,并借助一条简单的 Linux 命令控制这些舵机。本教程还演示了如何利用 Xillybus 的可寻址数据流(seekable streams)来实现 FPGA 内部的寄存器接口。

为了让描述和照片保持简单,本教程中的电气接线只针对一个舵机。要增加更多舵机,只需按图上所示,在同样的元器件上增加导线即可。

舵机的电气接口

这类舵机的接线共有三根导线:

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

按照大多数数据手册,脉冲持续时间应为 1 ms 到 2 ms。对某些舵机来说,这个信息并不正确。例如,Tower Pro 的 SG90(限制在 180 度转动)的正确范围大约是 500 μs 到 2450 μs。这个范围对应 180 度的转动。如果脉冲最长到 2560 μs,还能让舵机略微转得更远一些。SG90 会忽略比这更长的脉冲。

因此,为了探索舵机实际响应的脉冲宽度范围,最好亲自用舵机做实验,而不要只依赖数据手册。由于这类舵机面向业余爱好项目,其规格数据未必精确。

电气连接

首先,舵机需要独立的 +5V 电源来为内部的电机供电。不建议使用给 Smart Zynq 开发板供电的电源,因为舵机经常会引起电压的突然波动。如果供电电压不稳定,Smart Zynq 开发板可能会工作不可靠。

橙色线上的 PWM 信号在高电平时,其电压应大致等于电源电压。换句话说,橙色线与地之间的电压要么是 0,要么大约是 +5V。

然而,Smart Zynq 开发板的输出高电平电压只有 3.3V。如果把这个输出直接接到舵机的橙色线,舵机很有可能仍能正常响应。原因是,基于 5V 电源供电的数字电路通常把任何高于 2.5V 的电压都视为逻辑 1

话虽如此,PWM 信号电压为 3.3V 时,舵机能否可靠工作还不确定。推荐的解决方案是使用电压电平转换器(voltage level shifter),把 Smart Zynq 开发板的输出转换成 0V 或 5V。本教程使用了一块带有 TXS0108E 芯片的小板。这块小板只是把芯片的引脚引出来,便于用普通导线连接到芯片。

这张图展示了控制单个舵机时的接线方式:图中底部的 Smart Zynq 开发板通过杜邦线连接到 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 小板引脚 Smart Zynq 引脚 舵机导线 +5V 电源 说明
VA J6/37(3.3V) -- -- 还与 OE 相连
A4 J6/1 -- --
OE -- -- -- 与 VA 相连
GND J6/35(GND) 棕色线 GND
VB -- 红色线 +5V
B4 -- 橙色线 --

TXS0108E 芯片负责把电压电平从它的 A4 输入转换到 B4 输出。芯片上还有另外七对引脚。A4/B4 这一对是本教程随意选择的。要控制八个舵机,可以使用这个芯片的全部八对引脚。这里没有展示这种方式,因为控制八个舵机所需的导线数量会让照片难以看懂。

要找到 J6 排针,请在 Smart Zynq 开发板背面找到印有 “Bank 33 VCCIO Vadj” 的位置。靠近这个标记的那一排引脚就是本教程使用的排针。因此,J6/1 就是最靠近 HDMI 连接器的那个引脚。

注意,Smart Zynq 开发板通过它的一个 USB 端口连接到另一路电源。还要注意,J6 排针的最后一个引脚是 5V,所以不要把导线接到这个引脚上。

准备 Vivado 工程

从演示包(demo bundle)的 zip 文件(boot partition kit)创建新的 Vivado 工程。用文本编辑器打开 verilog/src/xillydemo.v。删除代码中标有 “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,内容如下(可以复制粘贴,也可以从这个链接下载):

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 提示符下执行。

首先,切换目录并对 Xillybus 的演示应用程序(demoapps)进行编译:

# 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 中有八个用于控制舵机的寄存器。初始时,它们的值全部为零。下面这条命令以十进制格式打印这些值:

# 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

可以使用位于 xillybus/demoapps/ 目录下的 memwrite 程序来控制舵机。例如,下面的命令把寄存器 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 的源代码就是 ~/xillybus/demoapps/ 目录下的 memwrite.c 文件。这个程序展示了如何借助 Xillybus 的可寻址数据流(seekable streams)来访问寄存器。如果你想理解这个程序的工作原理,建议直接查看其源代码。下面我只讲解两个重要部分。

程序会打开第一个参数指定的文件。这个文件对应的文件描述符存放在变量 @fd 中。

然后,程序调用 lseek(),具体如下:

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

@address 变量保存程序的第二个参数。在前面的第一个 memwrite 示例中,它是 0;在第二个示例中,它是 3。通常,lseek() 用来移动到文件内的某个特定位置。在本例中,文件中的位置就是我们要访问的寄存器的编号。

接下来,程序调用 allwrite():

  allwrite(fd, &data, 1);

这段代码向文件写入一个字节。allwrite() 定义在 memwrite.c 中,它与众所周知的 write() 函数相似。因此,把 allwrite() 换成下面这样也差不多:

  write(fd, &data, 1);

两者的区别是,write() 不保证数据一定写入成功。而 allwrite() 则保证数据会被写入文件。

在这里,调用 allwrite() 写入的是程序第三个参数的值。换句话说,这就是寄存器的新值。

关于 Xillybus 主机端 API 的更多细节,请参阅相关文档,特别是其中 6.1 节。

Verilog 代码解读

我先介绍 xillydemo.v 中的 Verilog 代码如何实现 FPGA 内部的寄存器,然后再介绍 PWM 脉冲的实现。

关于如何用 Xillybus API 实现硬件寄存器的详细说明,参见Xillybus FPGA 设计指南

与寄存器相关的连线在原始 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;

这些连线在 Xillybus IP 核实例化时连接到该 IP 核:

  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 核的输出。

   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 模块进行实例化(instantiation):

   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_width 接收寄存器的值,这个值由主机上的 memwrite 命令更新。@pwm_signal 连接到 Smart Zynq 开发板的引脚,因此这就是通过橙色线到达舵机的信号。

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

   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(脉冲宽度以微秒为单位时的表达式是 500 + x * 10)。

因此,@threshold 的表达式如下:

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

这个寄存器保存 PWM 脉冲(即 PWM 信号为高电平期间)中 @enable 为高的次数。这个表达式反映了一个事实:寄存器的值就是 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 脉冲的重复周期是 20 ms,满足要求。

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

模块的最后一部分用于复位某些寄存器:

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

当 @rst 为高时,这段逻辑会覆盖上面任何赋值。因此,@rst 起复位(reset)信号的作用。

Verilog 代码与实际引脚的对应关系

上面的 Verilog 代码使用名为 J6 的 inout 端口,但这些连接是如何到达排针的呢?答案可以在 xillydemo.xdc 中找到。这个文件是生成比特流(bitstream)的 Vivado 工程的一部分(位于 vivado-essentials 目录)。

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] 应连接到 U22,这是 FPGA 物理封装上的一个位置。根据 Smart Zynq 的原理图,这个 FPGA 引脚连接排针的第一个引脚。其他端口的位置也以同样方式定义。

总结

本教程展示了如何在 Smart Zynq 开发板和 Xillybus 寄存器 API 的帮助下控制舵机。文中给出了主机应用程序以及相应的 Verilog 代码。

对于其他需要通过寄存器控制硬件的应用,本教程也可以作为基础参考。

本页由机器从英文翻译而来。如有疑问,请参阅原文
Copyright © 2021-2026. All rights reserved. (dcc38493)