Esta página web pertenece a un grupo de pequeños proyectos que exploran las características de la placa Smart Zynq.
Introducción
Hay varios servomotores de corriente continua (DC) sencillos y de bajo coste en el mercado, por ejemplo SG90, MG90S, MG995 y MG996R. Estos motores están pensados para proyectos de aficionados, en particular para construir robots sencillos. Estos motores se controlan con una única señal PWM.
Algunos motores giran dentro de un rango limitado de ángulos, normalmente 180 grados. En estos motores, la señal PWM controla la posición angular del motor. Otros motores (a menudo denominados «motores de 360 grados») pueden girar de forma continua. Con estos motores, la señal PWM controla la velocidad y la dirección del giro. Nótese que un modelo concreto suele estar disponible en dos variantes: una variante con ángulo limitado y una segunda variante que puede girar de forma continua. Es importante prestar atención a esta diferencia al comprar un motor de este tipo.
Este tutorial muestra cómo conectar hasta ocho servomotores a la placa Smart Zynq y controlarlos con la ayuda de un simple comando de Linux. Este tutorial también demuestra cómo usar los flujos con posibilidad de posicionamiento (seekable streams) de Xillybus para implementar una interfaz de registros dentro de la FPGA.
El cableado eléctrico que se muestra en este tutorial solo es adecuado para un motor, con el fin de mantener sencillas las descripciones y las fotos. Se pueden añadir más motores simplemente añadiendo cables a los mismos componentes que se muestran aquí.
Interfaz eléctrica del motor
La conexión a los servomotores de este tipo consta de tres cables:
- Cable marrón, conectado a tierra.
- Cable rojo, conectado a una fuente de alimentación de +5 V.
- Cable naranja, conectado a la señal de control (PWM).
La señal PWM debe tener un periodo de 20 ms (50 Hz). La duración durante la cual el pulso está en alto controla la posición o la velocidad angular del motor.
Según la mayoría de las hojas de datos, la duración del pulso debería estar entre 1 ms y 2 ms. Esta información es incorrecta para algunos motores. Por ejemplo, el rango correcto para el SG90 de Tower Pro (con límite de rotación de 180 grados) está aproximadamente entre 500 μs y 2450 μs. Este rango corresponde a una rotación de 180 grados. Es posible hacer girar el motor algo más allá con un pulso de hasta 2560 μs. Este motor ignora un pulso más largo que ese.
Por tanto, a la hora de explorar el rango de duraciones de pulso a las que responde el motor, es mejor experimentar con el motor que fiarse de la hoja de datos. Como estos motores están pensados para proyectos de aficionados, sus datos de especificación pueden no ser precisos.
Conexiones eléctricas
En primer lugar, el servomotor necesita una fuente de alimentación independiente para suministrar energía al motor mecánico. La tensión de esta fuente debe ser de +5 V. No se recomienda usar la fuente que alimenta la placa Smart Zynq, porque los motores suelen provocar cambios bruscos en el nivel de tensión. La placa Smart Zynq puede comportarse de forma poco fiable si la tensión de su alimentación es inestable.
La señal PWM del cable naranja debería tener aproximadamente la misma tensión que la fuente de alimentación cuando esta señal está en alto. En otras palabras, la tensión entre el cable naranja y tierra debe ser 0 o aproximadamente +5 V.
Sin embargo, la tensión de salida de la placa Smart Zynq es de solo 3.3 V cuando está en alto. Si esa salida se conecta directamente al cable naranja del motor, hay bastantes posibilidades de que el motor responda correctamente. La razón es que los circuitos digitales alimentados con 5 V suelen considerar cualquier tensión superior a 2.5 V como un «1» lógico.
Dicho esto, no está claro si el motor funcionará de forma fiable con una tensión de 3.3 V en la señal PWM. La solución recomendada es usar un adaptador de nivel de tensión que transforme la salida de la placa Smart Zynq en 0 V o 5 V. En este tutorial se usa una pequeña placa con el circuito integrado TXS0108E. Esta placa simplemente expone los pines del integrado, de modo que se pueden conectar cables ordinarios.
Esta imagen muestra cómo están dispuestas las conexiones eléctricas para controlar un servomotor: la placa Smart Zynq, en la parte inferior de la imagen, está conectada a la placa TXS0108E con la ayuda de cables Dupont. El otro lado de esta placa se conecta tanto a la entrada PWM del servomotor como a la fuente de alimentación independiente para los motores (a través de un adaptador para un conector de alimentación estándar). Los cables rojo y marrón del motor también están soldados a esta placa, para proporcionar al motor su tensión de alimentación.
Esta imagen ofrece una vista más cercana de la placa TXS0108E y de sus conexiones:
Preste atención al cable rojo entre OE y VA. Esto garantiza que la habilitación de salida del integrado esté en alto.
Las conexiones al conector de pines de la Smart Zynq se muestran en la imagen siguiente. El motor se controla mediante J6/1 tal como está conectado aquí.
Esta tabla resume las conexiones entre las cuatro partes implicadas:
| Pin de la placa TXS0108E | Pin de Smart Zynq | Cable del motor | Fuente de +5 V | Comentario |
|---|---|---|---|---|
| VA | J6/37 (3.3V) | -- | -- | También conectado a OE |
| A4 | J6/1 | -- | -- | |
| OE | -- | -- | -- | Conectado a VA |
| GND | J6/35 (GND) | Cable marrón | GND | |
| VB | -- | Cable rojo | +5V | |
| B4 | -- | Cable naranja | -- |
El integrado TXS0108E realiza una conversión de nivel de tensión desde su entrada A4 hasta su salida B4. El integrado tiene otros siete pares de pines adicionales. El par A4/B4 se eligió arbitrariamente para este tutorial. Para controlar ocho motores, se pueden usar los ocho pares de pines del integrado. Esto no se muestra aquí, porque la cantidad de cables necesaria para controlar ocho motores haría que las imágenes fueran difíciles de entender.
Para encontrar el conector de pines J6, busque en la parte trasera de la placa Smart Zynq la inscripción «Bank 33 VCCIO Vadj». La fila de pines cercana a esa marca es el conector de pines que se usa en este tutorial. Por tanto, J6/1 es el pin más cercano al conector HDMI.
Nótese que la placa Smart Zynq está conectada a una fuente de alimentación independiente a través de uno de sus puertos USB. Nótese también que el último pin del conector de pines J6 es de 5 V, así que no conecte ningún cable a ese pin.
Preparación del proyecto Vivado
Cree un nuevo proyecto Vivado a partir del archivo zip del paquete de demostración (demo bundle), es decir, el kit de la partición de arranque. Abra verilog/src/xillydemo.v en un editor de texto. Elimine la parte del código etiquetada como «PART 4». En lugar de esa parte, inserte esto:
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 } )
);
También puede descargar el xillydemo.v actualizado desde este enlace.
Cree un archivo nuevo, servo_pwm.v, en el directorio verilog/src/ del proyecto, que contenga lo siguiente (use copiar y pegar, o descárguelo desde este enlace).
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
Después añada el archivo al proyecto Vivado: haga clic en File > Add Sources… y elija «Add or create design sources». Luego haga clic en Next. Pulse el botón «Add Files» y elija el archivo llamado «servo_pwm.v» del directorio verilog/src/. A continuación pulse el botón «Finish».
Cree un archivo de flujo de bits (bitstream) a partir del proyecto actualizado, igual que hizo con el paquete de demostración. Copie también el archivo de flujo de bits a la tarjeta TF de la misma manera (sobrescriba el antiguo xillydemo.bit con el archivo que se haya generado con este proyecto).
Control del servomotor
Los pasos que se muestran en esta sección deben realizarse en un indicador de shell de la placa Smart Zynq.
Primero, cambie de directorio y compile las aplicaciones de demostración de Xillybus:
# 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
En la FPGA hay ocho registros para controlar el servomotor. Inicialmente, todos sus valores son cero. Así se imprimen esos valores en formato decimal:
# hexdump -v -n 8 -e '8/1 "%u " "\n" ' /dev/xillybus_mem_8 0 0 0 0 0 0 0 0
También se puede usar un comando hexdump más sencillo para imprimir los mismos valores en formato hexadecimal:
# hexdump -v -n 8 -C /dev/xillybus_mem_8 00000000 00 00 00 00 00 00 00 00 |........| 00000008
El programa memwrite (dentro del directorio xillybus/demoapps/) puede usarse para controlar el motor. Por ejemplo, este comando cambia el valor del registro 0 a 120:
# ./memwrite /dev/xillybus_mem_8 0 120
Esto cambia la posición de giro o la velocidad del motor conectado a J6/1 (es decir, como se muestra en las fotos de arriba).
La anchura del pulso PWM depende del valor de este registro según esta fórmula:
t = 500 + (x * 10)
En esta fórmula, t está en microsegundos y x es el valor del registro correspondiente. Por tanto, la anchura por defecto del pulso es 500 μs (es decir, para x=0). El comando anterior cambió el valor del registro a 120. Como resultado, la anchura del pulso cambió a 1700 μs.
Cada registro consta de un byte, por lo que su valor está entre 0 y 255. En consecuencia, el pulso de cada motor puede tener una anchura entre 500 μs y 3050 μs. Nótese que la mayoría de los motores considera ilegal parte de ese rango de valores. Lo mejor es probar distintas posibilidades con cada motor y ver cómo reacciona.
Para controlar un motor conectado a otro pin, escriba en un registro distinto. Por ejemplo, si el motor está conectado a J6/4 de la Smart Zynq, puede usarse este comando:
# ./memwrite /dev/xillybus_mem_8 3 50
Este comando cambia la anchura del pulso a 1000 μs (es decir, 1 ms).
Después de estos dos comandos, los cambios pueden comprobarse leyendo los valores:
# hexdump -v -n 8 -e '8/1 "%u " "\n" ' /dev/xillybus_mem_8 120 0 0 50 0 0 0 0
Esto muestra los valores en formato decimal. Para el formato hexadecimal:
# hexdump -v -n 8 -C /dev/xillybus_mem_8 00000000 78 00 00 32 00 00 00 00 |x..2....| 00000008
Cómo funciona memwrite
El código fuente de memwrite es el archivo memwrite.c del directorio ~/xillybus/demoapps/. Este programa muestra cómo acceder a los registros con la ayuda de los flujos direccionables de Xillybus. Si quiere entender cómo funciona este programa, le sugiero que mire el código fuente. A continuación solo repasaré las dos partes importantes.
El programa abre el archivo cuyo nombre se da en el primer argumento. El descriptor de archivo de este archivo se guarda en la variable @fd.
El programa hace entonces una llamada a la función lseek() como sigue:
if (lseek(fd, address, SEEK_SET) < 0) {
perror("Failed to seek");
exit(1);
}
La variable @address contiene el segundo argumento del programa. En el primer ejemplo de uso de memwrite de arriba, este era 0. En el segundo ejemplo, era 3. En general, lseek() sirve para desplazarse a una posición concreta dentro de un archivo. En este caso, la posición en el archivo es la misma que el número del registro al que queremos acceder.
A continuación, el programa hace una llamada a la función allwrite():
allwrite(fd, &data, 1);
Esto escribe un byte en el archivo. allwrite() está definida en memwrite.c y es similar a la conocida función write(). Así que, en lugar de usar allwrite(), esto sería casi lo mismo:
write(fd, &data, 1);
La diferencia es que write() no garantiza que los datos se escriban. En cambio, la función allwrite() garantiza que los datos se escriben en el archivo.
En este caso, la llamada a allwrite() escribe el valor del tercer argumento del programa. En otras palabras, es el nuevo valor del registro.
Para más detalles sobre la API de Xillybus en el lado del host, consulte la documentación sobre este tema, en particular la sección 6.1.
Explicación del código Verilog
Primero mostraré cómo el código Verilog de xillydemo.v implementa los registros dentro de la FPGA. Después vendrá la implementación del pulso PWM.
La API de Xillybus para implementar registros de hardware se explica en detalle en la guía para diseñadores FPGA de Xillybus.
Los cables relacionados con los registros ya estaban definidos en el archivo xillydemo.v original:
// 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;
Estos cables están conectados al núcleo IP (IP core) de Xillybus como parte de su instanciación (instantiation):
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)
);
A continuación, veamos el código Verilog que se añadió específicamente para este proyecto. Empieza con esta parte, que se asegura de que no se aplique ningún control de flujo (flow control) al intercambio de datos con el host:
assign user_r_mem_8_empty = 0;
assign user_r_mem_8_eof = 0;
assign user_w_mem_8_full = 0;
A continuación se declaran los ocho registros:
reg [7:0] reg0, reg1, reg2, reg3, reg4, reg5, reg6, reg7;
La siguiente parte implementa las operaciones de escritura en los registros: cuando @user_w_mem_8_wren está en alto, se escribe un valor nuevo en uno de los registros. @user_mem_8_addr selecciona el registro afectado. Tanto @user_w_mem_8_wren como @user_mem_8_addr son salidas del núcleo IP de Xillybus.
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
Después se implementa la posibilidad de leer valores de estos registros: cuando @user_r_mem_8_rden está en alto, @user_r_mem_8_data se actualiza para que contenga el valor de uno de los registros. @user_mem_8_addr selecciona de qué registro se lee el valor.
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
Por último, tiene lugar la instanciación del módulo 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 } )
);
Nótese que esta instanciación crea ocho copias idénticas de servo_pwm: la primera copia está conectada a reg0 y J6[0], la segunda copia está conectada a reg1 y J6[1], etc.
Ahora veamos el módulo servo_pwm. Empieza con la declaración de los puertos y registros del módulo:
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;
Recordemos que @pwm_width recibe el valor del registro, que se actualiza con la ayuda del comando memwrite en el host. @pwm_signal está conectado al pin de la placa Smart Zynq, de modo que es la señal que llega al motor por el cable naranja.
La siguiente parte de este módulo implementa una señal de strobe (strobe signal): @enable está en alto una vez cada 1000 ciclos de reloj. La frecuencia de @clk es 100 MHz. Por tanto, @enable está en alto una vez cada 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
Como ya se ha dicho, el valor del registro, entre 0 y 255, corresponde a una anchura de pulso PWM entre 500 μs y 3050 μs (la expresión para la anchura del pulso en microsegundos era 500 + x * 10).
Por tanto, esta es la expresión para @threshold:
threshold <= pwm_width + 50; // Add 0.5 ms to PWM width
Este registro contiene el número de veces que @enable está en alto durante el pulso PWM (es decir, cuando la señal PWM está en alto). Esta expresión refleja el hecho de que el valor del registro es la longitud del pulso PWM en unidades de 10 μs. Sumar 50 al valor del registro corresponde a la anchura mínima del pulso de 500 μs.
Ahora se usa @enable como habilitación de reloj (clock enable) para esta parte:
if (enable)
begin
if (pwm_count == 1999)
pwm_count <= 0;
else
pwm_count <= pwm_count + 1;
pwm_signal <= (pwm_count < threshold);
end
@pwm_count cuenta de 0 a 1999 y vuelve a empezar desde cero. Este contador solo cambia una vez cada 10 μs, por lo que completa un ciclo cada 20000 μs = 20 ms. En otras palabras, el ciclo de repetición del pulso PWM es de 20 ms, como se requiere.
@pwm_signal está en alto mientras @pwm_count sea menor que @threshold. Así es como la anchura del pulso queda controlada por el registro.
La parte final de este módulo consiste en poner a cero algunos registros:
if (rst)
begin
enable_count <= 0;
pwm_count <= 0;
pwm_signal <= 0;
end
end
endmodule
Cuando @rst está en alto, esta parte se impone a cualquier cosa escrita anteriormente. Por tanto, @rst tiene la función de una señal de reinicio.
La relación entre el código Verilog y los pines reales
El código Verilog anterior usa el puerto inout llamado J6, pero, ¿cómo llegan las conexiones de este puerto al conector de pines? La respuesta se encuentra en xillydemo.xdc. Este archivo forma parte del proyecto Vivado que crea el flujo de bits (en el directorio «vivado-essentials»).
xillydemo.xdc contiene diversa información necesaria para que la FPGA funcione correctamente como componente electrónico. Entre otras cosas, este archivo contiene estas líneas:
[ ... ]
## 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
[ ... ]
La primera línea dice que la señal J6[0] debe conectarse a U22. Esta es una posición en el encapsulado físico de la FPGA. Según los esquemas de Smart Zynq, este pin de la FPGA está conectado al primer pin del conector de pines. Las posiciones de los demás puertos se definen de la misma manera.
Resumen
Este tutorial ha mostrado cómo controlar servomotores con la ayuda de la placa Smart Zynq y de la API de registros de Xillybus. Se han presentado tanto la aplicación del host como el código Verilog de esta aplicación.
Este tutorial también puede servir de base para otras aplicaciones que necesiten controlar hardware con la ayuda de registros.


