
En esta entrada vamos a ver como conectar nuestro microcontrolador con otro dispositivo muy popular, un servomotor. Un servomotor es un motor DC, similar a los usados en las entradas que dedicamos al control de motores. Sin embargo, al contrario que los motores que manejamos allí, los servomotores incluyen su propio hardware de control, por lo que no tendremos que preocuparnos de esa parte.
En la
primera parte de esta entrada describiremos los fundamentos de cómo controlar
un servo con un
microcontrolador, con una mínima sobrecarga para el PIC mediante el uso de un timer y sus interrupción asociada.
En la
segunda parte extenderemos este enfoque para el caso de que necesitemos controlar muchos
servos con un único PIC. Veremos como podemos controlar hasta 8/10 servos dedicando
sólo un TIMER del
microcontrolador.
--------------------------------------------------------------------------------------
Hardware :

Aunque
menos comunes, podemos encontrarnos servos cuyo parámetro controlado es la
velocidad. En este caso, nuestras ordenes causan que el servo gire
uniformemente a la velocidad indicada.
En esta
entrada nos centraremos en los servos de posición. Típicamente constan de un
motor DC normal con una gran reductora entre el eje del motor y el eje de
salida. A este eje de salida está conectado algún tipo de sensor (normalmente
un potenciómetro) a través de cuya medida el servo conoce que ha llegado a la
posición deseada y puede detener el motor.
La mayoría
de los servos están limitados a moverse dentro de una fracción de vuelta, por
lo que en vez de posiciones hablaremos del ángulo del servo. Un parámetro
importante de un servo es el rango máximo que puede girar ("throw" en
ingles). Así tendremos servos de media vuelta (180º), una vuelta completa
(360º) o incluso un cuarto (90º) de vuelta.
Los servos
de media vuelta, con un rango máximo de 180º, son muy comunes y en ellos basaré
la explicación de los conceptos
básicos.
Un servo
suele contar con 3 hilos. Dos de ellos, típicamente rojo (V+) y negro (GND), le
proporcionan la alimentación requerida, así como un nivel de referencia común
(masa) con el micro. El tercer
hilo (naranja es una opción muy usada para este hilo) es el que el micro usará
para control, es decir, para decirle al servo a que posición (ángulo) debe
girar.
La forma de
enviar esa información en muy sencilla. Técnicamente se denomina modulación por
ancho de pulso y consiste en que el PIC debe mandar una serie de pulsos al
servo con una frecuencia constante (del orden de 50 Hz). La
separación entre pulsos por lo tanto es siempre de unos 20 msec. Las ordenes se
las damos a través del ancho de los pulsos, que suele oscilar entre 1 y 2
milisegundos.
En la
figura adjunta podemos ver gráficamente esta situación:
El caso
ilustrado corresponde a un servo con un rango de ½ vuelta o 180º. Un ancho del
pulso de 1.5 milisegundos le dice al
servo que debe ir a su posición central o neutra. Si acortamos o alargamos el
pulso giraremos a la izquierda o a la derecha. En el ejemplo mostrado el servo
alcanza sus extremos (0 y 180º) para 1 y 2 milisegundos.
Como se
observa, independientemente del ancho de los pulsos, la separación entre pulsos
es constante y debe ser del orden
de 20 milisegundos, lo que corresponde a la frecuencia antes mencionada de 50
Hz. Esta frecuencia no es crítica: frecuencias entre 40 y 65 Hz funcionarán
posiblemente (periodos entre 15-25 msec).
Es
importante resaltar que los valores anteriores son aproximados y deben
considerarse un punto de partida.
Es
fundamental no sobrepasar los valores máximo y mínimo, porque el servo
intentará ir más allá y sus "topes" se lo impedirán. El resultado,
dependiendo de la fuerza del servo y la calidad de los engranajes podría ser un
servo quemado o con sus engranajes "pulidos" (sobre todo si son de plástico,
como ocurre en muchos servos baratos).
Debido a
esto, es muy importante verificar los servos con los que vamos a trabajar antes
de usarlos en nuestra aplicación. Para ello podemos ir
incrementando/decrementando el ancho del pulso hasta encontrar sus límites. El
punto de partida será la posición neutra "nominal": un ancho de 1.5
msec. Puede que no corresponda al punto central exactamente pero seguro que cae
en la zona permitida.

Recalcar
que es necesario que la masa del PIC y la del servo estén conectadas para que
tengan una referencia común de voltaje.
Respecto
al voltaje de alimentación para el servo habrá que consultar las especificaciones, pero muchos de ellos
funcionarán razonablemente con una alimentación entre 4 y 8V. Si nos quedamos
cortos lo único que puede pasar es que el servo no se mueva o gire más
lentamente.
Como 5V es
un voltaje adecuado para muchos servos podríamos estar tentados de usar la
misma alimentación para el servo y el PIC. Esto es una mala idea, ya que las
demandas de corriente del servo al moverse pueden hacer caer el voltaje de la
alimentación, con el consiguiente riesgo de reseteo del PIC.

Finalmente,
en el programa usaremos un par de pulsadores (conectados a RA1 y RA2) para
variar el ancho del pulso (y por lo tanto mover el servo). Vemos en el esquema
adjunto cómo cada pin está atado a tierra con una resistencia de pull-down. Su estado natural es LOW (0). Sólo al pulsar
pasan a un nivel alto.
Software :
Nuestro
principal objetivo es programar una señal de 50 Hz con un ancho de pulso que
podamos variar en el rango indicado.
Lo primero
que podemos pensar es que la señal esperada por el servo (periodo fijo, ancho
del pulsi variable) se parece
mucho a una señal PWM. De hecho eso es exactamente lo que es (antes la hemos
llamado codificación por ancho de pulso, pero eso en inglés es justamente PWM,
Pulse Width Modulation).
Nuestra
primera idea sería usar una línea PWM para controlar un servo. Se trataría de
conseguir una frecuencia PWM del orden de 50 Hz y variar el ancho del pulso con
el duty_cicle. Esto es factible, pero en mi opinión, no es una buena idea por
varias razones:
1. Las
líneas PWM son escasas (típicamente sólo 2 en un PIC). ¿Qué pasa si queremos
controlar más servos? Además, las líneas PWM tienen otras funciones (modo de
CAPTURE o COMPARE) y pueden no estar disponibles.
2. Aunque
no hubiese problemas de disponibilidad, las características del PWM de un PIC
no se prestan a su uso como control de un servo. Veamos las razones.
Si
recordamos la entrada de introducción al PWM la frecuencia PWM que puede
obtenerse para una cierta frecuencia del oscilador es:
Fpwm (mínima) = Fosc / 16384
Para poder
llegar a la frecuencia deseada de 50 Hz deberíamos bajar a un oscilador de 1
MHz o menor, lo que posiblemente haga muy lento el sistema para otras tareas.
Además PWM
está diseñado para variar el ciclo de trabajo (% on) entre 0 y 100%. En cambio,
para controlar un servo el tiempo on (ancho del pulso) oscila como hemos visto entre
1 y 2 msec dentro del periodo de 20 msec. Traducido a ciclo de trabajo
correspondería a movernos entre un 5% (1/20) y un 10% (2/20). Perderemos mucha
"resolución" si estamos limitados a un intervalo tan pequeño.
En vez de usar
el módulo PWM es mucho mejor usar un simple TIMER (y su correspondiente
interrupción) para crear la señal de control. Se nos puede criticar que estamos
cambiando un recurso (PWM) por otro (TIMER) pero eso no sería correcto, ya que
usar el PWM conlleva también el gasto del timer asociado (TMR2).
Otra
posible crítica es que si necesitamos controlar muchos servos se nos acabarán
los timers. Lo bueno de este enfoque es que, como explicaremos en esta entrada,
podemos controlar hasta 8 servos con un único
timer (el número exacto depende de las características de los servos
usados, podrían ser alguno más o menos).
En esta entrada veremos dos programas: en el primero presentaremos los fundamentos de la interfaz con el servo usando un timer. En el segundo ampliaremos el concepto al uso de múltiples servos sin que necesitemos más recursos del PIC.
PROGRAMA1 (servo_calib.c):
Ilustrará los conceptos básicos sobre como configurar un TIMER para generar la señal de control sin apenas sobrecarga para el PIC. Usaremos un par de pulsadores para aumentar/reducir el ancho del pulso.
Este programa también puede servirnos como calibración de un servo cuyas especificaciones detalladas desconozcamos. Podemos partir de un ancho medio (1500 msec) correspondiente a la posición neutra e ir subiendo/bajando hasta notar (chirridos, vibraciones) que hemos alcanzado el tope. De esta forma podemos obtener el ancho mínimo y máximo del pulso para ese servo y su posición neutra. Nos vendrá bien además para verificar el verdadero rango del servo (su movimiento en grados). Es fácil adquirir servos baratos supuestamente con un rango de 180º, cuyo verdadero rango está más cerca de ¼ de vuelta (90º).
El
programa es muy sencillo. Veamos el código de la rutina principal.
{
T0CON = 0b00000000;
enable_global_ints; enable_TMR0_int;
start_TMR0;
ADCON1=0x0F;
// No PORTS used as AD
TRISA=0b00000110; // RA1,RA2 inputs
TRISB=0; PORTB=0; TRISC=0; PORTC=0; TRISD=0; PORTD=0; // All pins down to 0
while(1)
{
if (PORTAbits.RA1==1) if (pulse<2100) pulse++; // Increments pulse (max 2.1 msec)
if (PORTAbits.RA2==1) if (pulse>500) pulse--; // Decrements
pulse (min 0.5 msec)
PORTB = pulse/10; // Show
current pulse in PORTB
Delay10TCYx(100); // Wait
a while
}
}
Algunos
comentarios:
- La inicialización a 0 de T0CON pone al timer TMR0 en modo de 16 bits, contando ciclos de instrucción y con un pre-scaler 1:2. En este proyecto estoy usando un cristal de 8 MHz, por lo que la frecuencia de instrucción es de Fosc/4 = 2 MHz. Con un preescaler 1:2 hago que el contador del TMR0 se incremente cada microsegundo, lo que resulta muy conveniente para "traducir" los delay programados.
- Habilitamos las interrupciones globales y la interrupción asociada a TMR0.
- Usaremos todos los pines como de entrada/salida digital (ADCON1=0x0F)
- Declaramos RA1 y RA2 entradas (bits correspondientes de TRISA = 1).
Previamente
hemos declarado un par de macros para arrancar el timer TMR0 y resetera su
contador (16 bits):
#define set_TMR0(x) {TMR0H=(x>>8);
TMR0L=(x&0x00FF);}
#define start_TMR0 T0CONbits.TMR0ON=1;
El resto
del main() es simplemente un bucle infinito donde monitorizamos si se han
pulsado los pulsadores asociados a RA1 y RA2. Si es así se incrementa o
decrementa la variable pulse (uint16) que gobernará el ancho del pulso de
control. El valor de dicha variable puede variar entre 0.5 y 2.1 milisegundos,
que son los extremos que he detectado para el servo usado.
Evidentemente
toda la acción sucede durante la interrupción del TMR0. Veamos su código:
#define SERVO_LINE LATCbits.LATC0
uint16 TMR0_ini,pulse=1325; //
in microsec
// High priority
interruption
#pragma interrupt high_ISR
void high_ISR (void)
{
if (TMR0_flag) // ISR de la interrupcion de TMR0
{
SERVO_LINE=1-SERVO_LINE;
if (SERVO_LINE) TMR0_ini= 25-pulse; //65536-(pulse-25);
else TMR0_ini= 45536+pulse+25; //65536-(20000-(pulse-25))
set_TMR0(TMR0_ini);
TMR0_flag=0;
}
}
- Con el define #SERVO_LINE indico que pin voy a usar para controlar el servo. En este caso es RC0.
- Declaro la variable pulse (uint16) que guardará el ancho del pulso deseado, y la inicializo con 1325, que corresponde a 1.325 milisegundos (que resulta ser la posición neutra de mi servo).
Examinemos
ahora con más detalle el código de la interrupción:
1. Lo
primero que hacemos es cambiar el estado de la línea. Si estaba baja se levanta
y viceversa.
2. Si
la línea está alta (tras la operación anterior) es que estamos en la fase del
pulso, que debe durar el ancho del pulso (valor de pulse en microsegundos).
Como los contadores de los timers en los PIC se incrementan siempre y la
interrupción se producirá al llegar a 65536, debo programar un valor nominal
inicial para TMR0:
TMR0_ini
= 65536 – pulse
para que
pasados pulse microsegundos rebose el contador y se produzca la siguiente
interrupción (donde se cambiará el
estado de la línea, acabándose el pulso.
Sin embargo,
debido a los retrasos introducidos en la llamada a la interrupción y a los
cálculos previos he comprobado que
es más exacto inicializar el contador un poco más adelante de su valor nominal. En particular, una corrección de
25 ciclos es muy adecuada:
TMR0_ini
= 65536 – pulse + 25 = (para un uint16) = 25 - pulse
y ese es el
valor que introduzco en la variable TMR0_ini.
3. Si
tras el cambio la línea esta baja es que estamos en la fase de
"descanso" hasta el siguiente pulso. La duración de esa fase será de
20 msec – ancho_pulso = 20000 – pulse. Inicializaremos TMR0_ini con su
complementario:
TMR0_ini = 65536 – 20000 +
pulse = 45536 + pulse.
Al igual que
antes para compensar retrasos en la ejecución podemos adelantar un poco dicho
contador. Si introducimos la misma
corrección de antes tendremos:
TMR0_ini = 45536 + pulse + 25
4. Tras
hacer los cálculos anteriores pongo en hora el contador TMR0 con set_TMR0(TMR0_ini).
Y ya está
todo. La interrupción saltará alternadamente cada pulse (en msec) y cada
20-pulse (msec). Trás el intervalo largo (20-pulso) se subirá la línea y tras
el corto (pulse) se bajará, obteniéndose así la señal de control deseada.
Como pulse
es una variable puede cambiarse en cualquier momento (en nuestro caso a través
de los pulsadores en el bucle principal) modificando el ancho del pulso y la
posición del servo.
En el video adjunto se muestra el resultado cuando conectamos RC0 a la línea de control de un servo:
Una vez entendidos los fundamentos para controlar un servo, ampliaremos el concepto al caso de
querer gobernar múltiples servos.
PROGRAMA 2 (servo_n.c)
En este segundo ejemplo ilustraremos como usar múltiples servos con un único TIMER del PIC.
Además del usoi de un timer necesitaremos (en principio) 8 líneas de control, una para cada servo. Sin embargo veremos que con este enfoque en cada instante sólo tenemos activa una línea de un servo, por lo que sería posible usar un demultiplexador (tipo 74HC237) para gobernar 8 servos con sólo 3 pines del PIC. En este código no se ha usado esta opción pero sería fácil de adaptar.
Podemos controlar varios servos sin usar timers adicionales porque el tiempo que cada servo esta alto (1-2 msec) es muy pequeño
comparado con el periodo (20 msec). La idea es aprovechar el tiempo en que la
línea de un servo está baja para dedicarnos al resto de los servos.
Veamos el código, que está escrito suponiendo 5 servos a controlar, aunque es fácilmente modificable para potro número. Empezaremos con la declaración
de los defines y variables globales a usar:
#define N_SERVO 5
#define SERVO_0 LATCbits.LATC0
#define SERVO_1 LATCbits.LATC1
#define SERVO_2 LATCbits.LATC2
#define SERVO_3 LATCbits.LATC3
#define SERVO_4 LATCbits.LATC4
uint8 servo_active=0;
uint16
pulse[N_SERVO]={600,900,1200,1500,1800};
// in microsec
uint16 TMR0_ini;
Muy similar
al caso anterior, solo que ahora tenemos 5 servos (N_SERVO) a controlar, por lo que tenemos que declarar no 1 sino 5 pines de control, desde RC0 a RC4.
Tenemos que tener también una nueva variable (servo_activo) que nos indicará de que servo se está
ocupando el timer.
Finalmente
tenemos nuestras conocidas variables TMR0_ini y pulse, solo que ahora pulse es
un array de tamaño 5, ya que tenemos 5 servos y cada uno puede tener un ancho
de pulso (posción) diferente. Los pulsos para los cinco servos se inicializan a
0.6, 0.9 , 1.2, 1.5 y 1.8 msec.
Dentro de
la función main() tenemos un sistema
parecido al anterior para poder cambiar los anchos de los pulsos:
ADCON1=0x0F; // No PORTS used as AD
TRISA=0b00000111;
while(1)
{
if (PORTAbits.RA0==1) {k++; if (k==N_SERVO) k=0; PORTB=k;}
if (PORTAbits.RA1==1) if (pulse[k]<2100) pulse[k]++;
if (PORTAbits.RA2==1) if (pulse[k]>500) pulse[k]--;
Delay10TCYx(100);
}
Además de
los pulsadores RA1 y RA2 de antes para cambiar el tamaño de un pulso, tengo que tener una variable (k, inicializada a 0) que me indica cuál de los servos estoy modificando. Dicha variable cambia al pulsar RA0, por lo que puedo modificar el ancho de pulso de cualquiera de las
señales de control. Para saber qué servo estoy modificando el programa me
muestra el valor actual de k en PORTB.
Ya sólo
queda ver el código de la interrupción donde se hace el trabajo:
#define slot
(20000/N_SERVO) // Time slot allocated to each servo
#define c_slot (65536-slot+30) // Complementary
(16 bits) of c_slot
// High priority
interruption
#pragma interrupt high_ISR
void high_ISR (void)
{
PORTDbits.RD0=1;
if (TMR0_flag) //
ISR de la interrupcion de TMR1
{
switch (servo_active)
{
case 0:
SERVO_0=1-SERVO_0; // Changes
states of SERVO_0
// if HIGH program pulse[0]
delay, else // else (slot-pulse[0]) delay
if (SERVO_0)
TMR0_ini= 25-pulse[0];
else {TMR0_ini=
c_slot+pulse[0]; servo_active++;}
break;
case 1:
SERVO_1=1-SERVO_1;
if (SERVO_1) TMR0_ini= 25-pulse[1];
else {TMR0_ini=
c_slot+pulse[1]; servo_active++; }
break;
case 2:
SERVO_2=1-SERVO_2;
if (SERVO_2) TMR0_ini= 25-pulse[2];
else {TMR0_ini= c_slot+pulse[2];
servo_active++; }
break;
case 3:
SERVO_3=1-SERVO_3;
if (SERVO_3) TMR0_ini= 25-pulse[3];
else {TMR0_ini= c_slot+pulse[3];
servo_active++; }
break;
case 4:
SERVO_4=1-SERVO_4;
if (SERVO_4) TMR0_ini= 25-pulse[4];
else {TMR0_ini= c_slot+pulse[4]; servo_active=0;
}
break;
}
set_TMR0(TMR0_ini);
TMR0_flag=0;
}
}
A través de servo_active podemos ver que servo está
"activo". Dentro
de cada servo, el código es muy similar al de antes:
SERVO_0=1-SERVO_0; // Changes states of SERVO_0
// if
HIGH program delay = pulse[0] else =(slot-pulse[0])
if (SERVO_0)
TMR0_ini= 25-pulse[0];
else {TMR0_ini=
c_slot+pulse[0]; servo_active++;}
break;
Como antes cambiamos el estado de la línea. Si tras el cambio está
alta es que estamos en el periodo del pulso y programamos el timer para que salte dentro de pulse[0]
microsegundos.
La
diferencia es que ahora, si la línea está baja en vez de esperar 20 msec (–
pulso), esperamos 20 msec/5 (- pulso). Es lo que en el programa defino como un
slot. La idea es que ahora los N servos deben repartirse los 20 milisegundos de
los que disponemos, por lo que cada uno sólo cuenta con 20/5 = 4 msec antes de
ceder el paso al siguiente pulso.
Eso se
hace incrementando la variable servo_active, salvo en el último caso (case 4:)
donde se resetea a 0 para volver a empezar el ciclo.
Si
conectasemos la salida de las 5 líneas a un analizador lógico veríamos algo así
como lo mostrado en la figura adjunta.
En cada segmento de 20/5 = 4 msec la
variable servo_active toma un valor distinto y los comandos de levantar/bajar
la línea afectan a distintos pines. Una vez que ha acabado el pulso y el pin ha vuelto
a su nivel bajo no hay nada más que hacer para ese servo (hasta dentro de 20 msec), por lo que puedo
dedicarme al siguiente.
Solo falta
ver cuantos servos podríamos llegar a manejar. La respuesta es sencilla. El
problema de tener muchos servos es que puede que todavía estuviesemos haciendo
la ronda cuando se cumplieran los 20 milisegundos. En ese momento tendríamos
que volver a ocuparnos de preparar el pulso del 1er servo.
Si
nuestros servos tienen un ancho máximo de unos 2 msec con asignar un slot de
2.5 msec a cada servo tendríamos suficiente. 20 msec entre 2.5msec dan un total
de 8 slots. Incluso podríamos acomodar alguno más usando el hecho de que la
frecuencia de 50 Hz no es crítica. De ser necesario podríamos seguramente
incrementar el periodo de los pulsos a 25 msec (40 Hz), lo que nos daría
espacio para 2 slots adicionales.
Otro
consideración es que % del PIC queda libre para otras tareas. Y la respuesta es
que prácticamente todo. Si monitorizamos cuanto nos demoramos en la
interrupción veremos que es del orden de 25 usec (justo la compensación que
tuvimos que usar para ajustar más exactamente los timings).
Por cada
timer que tengamos TMR0 salta 2 veces, una para subir y otra para bajar la
línea. En el caso máximo de tener 8 timers tendríamos 8 x 2 = 16 interrupciones
durante el periodo de 20 msec. 16 interrupciones nos ocuparán
16 x 25 = 400
usec = 0.4 msec. El tanto por ciento que se come la interrupción es por lo
tanto de:
0.4/20 = 2%
Luego el
PIC tiene libre un 98% de sus ciclos para dedicarlos a sus otras tareas.
Me ha sido de gran ayuda, ahora intentaré aplicarlo. Aprendí micros con una STM32F3 y es una bestia, tiene varios Timers con hasta 4 canales con PWM, es una maravilla, sin embargo ahora que ando aprendiendo PIC veo que es dificil contar con tantos recursos en hardware, así que veo muy útil utilizar soft, claro, habrá aplicaciones que requieran alta precición y por lo tanto requieran puro PWD por hard, sin embargo si así lo amerita la implementación pues mejor invertir en un micro mas grande, el pwm por soft nos permite llevar a cabo la gran mayoría de los proyectos "de nivel medio"
ResponderEliminarhola. muy bueno todo el proyecto, se agradece. estoy aprendiendo a prog pics y me ha sido de gran ayuda. lo único que no tengo claro es la ultima parte referente a "% que se come la interrupción =2%". ¿quiere decir esto que si yo quiero ocupar el pic con otras funciones (por ej: leer un pin, leer un canal analógico, manejar un lcd, etc) tengo un 98% disponible?....de ser asi, ¿el programa se interrumpe incondicionalmente con el valor cargado en el timer ?. y la ultima pregunta y la mas importante para mi es ¿se puede utilizar paralelamente los timers del pic ? saludos y gracias.
ResponderEliminarGracias por todos los aportes, me he leído la mayoría de tus archivos tratando de aprender a programar mi pic32.
ResponderEliminarY he aprendido mucho con tu Información, GRACIAS!!
hola , una pregunta , la alimentación del pic y de los servos son independientes, pero cuando dices que tengan las mismas masas , es que todas las tierras vayan a un solo lugar , osea que compartan las tierras el pic y los servos ?
ResponderEliminarBro tus enlaces de descarga están mal.. los podrías arreglarlos porfa..!!
ResponderEliminarHice un codigo en ccs pero controlar la dirección de los motores pero solo funciona con uno ya que al conectar otro y oprimir cualquier push button (derecha o izquieda) siempre se dirige hacia un mismo lado ¿alguien puede decirme porque sucede esto?
ResponderEliminarAlguien ya logró hacer funcionar el código ?
ResponderEliminarHola son nuevo en programar pic ¿el codigo se puede adaptar para un 16f88?
ResponderEliminarHola yo estoy esperando a una niña para Julio y tengo que manejar dos servos de forma independiente con dos potenciometros. Estoy manejando un 16F628A, pero el codigo no me vale para PCW de CCS. En que esta programado este ejemplo??
ResponderEliminarAlguien ya tiene el codigo bien?
ResponderEliminarcomo puedo hacer esto con los 2 pulsadores, 1 servo y arduino???
ResponderEliminarmuchas gracias
Excelente información con la explicación que da el buen amigo se puede programar en cualquier microcontrolador solo hace falta tener logica. Saludos desde Mexico.
ResponderEliminarMuy bien explicado amigo, la verdad muchas gracias logre hacer mi libreria para un PIC18F2550 con tu blog, le hice mejores y reduci el codigo, la verdad muchas gracias por la explicacion, Saludos desde Peru
ResponderEliminar