该网页属于一组探索 Smart Zynq 电路板功能的小项目。
介绍
市场上有几种简单且低成本的直流伺服电机,例如 SG90、 MG90S、 MG995 和 MG996R。这些电机适用于业余项目,特别是用于构建简单的机器人。此类电机由单个 PWM 信号控制。
有些电机在有限的角度范围内旋转,通常为 180 度。对于此类电机, PWM 信号控制电机的角度位置。其他电机(通常称为“360 度电机”)可以连续旋转。对于此类电机, PWM 信号控制旋转的速度和方向。请注意,电机模型通常有两种变体: 一种是角度有限的变体,另一种是可以连续旋转的变体。购买此类电机时,注意这种差异非常重要。
本教程将介绍如何将最多 8 个伺服电机连接到 Smart Zynq 电路板,并借助简单的 Linux 命令控制这些电机。本教程还演示了如何使用 Xillybus的 seekable 数据流在 FPGA内实现寄存器(register)接口。
为了简化说明和图片,本教程中显示的电线仅适用于一台电机。只需向此处所示的相同组件添加电线即可添加更多电机。
电机电气接口
与相关类型的伺服电机的连接由三根电线组成:
- 棕色线,接地。
- 红线,连接到 +5V 电源。
- 橙色线,连接控制信号(PWM)。
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 输入以及电机的单独电源(通过标准电源插头的适配器)。电机的红色和棕色电线也焊接到这块电路板上,以便为电机提供电源电压。
这张图可以更近距离地观察 TXS0108E 电路板及其连接:
注意 OE 和 VA之间的红线。这确保芯片的输出使能(output enable)为高。
下图显示了 Smart Zynq和排针的连接。电机由此处连接的 J6/1 控制。
下表总结了所涉及的四个部分之间的联系:
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 代码。
本教程还可以作为其他需要借助寄存器控制硬件的应用程序的基础。