Archive

Taller de programacion Bioloid: Primeros pasos en C

This entry is part 1 of 5 in the series (ES) Programación de CM-510

Taller de programación Bioloid: Primeros pasos en C

[This post is also in English]

Con este breve artículo comenzamos el taller de programación Bioloid con distintos lenguajes (C, C++ y C#) y en distintos entornos (ATMega, PC, SBC). Partiendo prácticamente de cero y hasta donde nos lleguen las fuerzas.

Los primeros pasos los daremos en C

C es un lenguaje sencillo, potente y extremadamente versátil con el que se desarrolla una gran cantidad de software para industrias tan diferentes como la del automóvil (enlace a traducción automática), el equipamiento médico o para la propia industria del software, desde Microsoft Office hasta sistemas operativos como Windows o Linux. (está en inglés pero se entiende fácilmente porque es una tabla de productos software bastante conocidos y lenguajes de programación utilizados).

Como va a ser un taller de programación muy práctico y dirigido a la programación de los servos Dynamixel de Robotis incluyo un enlace a un completo y popular libro Aprenda ANSI C como si estuviera en primero; si quieres una introducción más rápida descárgate esta presentación de 13 páginas, y aquí el documento con el estándar completo en inglés, pero ten en cuenta que es denso y no lo he encontrado en castellano.

{ Actualización:

He descubierto este estupendo y detallado curso de introducción a la programación con lenguaje C de la Universidad del País Vasco dirigido a personas que no tengan ningún conocimiento previo de programación.

Introducción a la programación con C, libro/curso de la UOC (Universidad abierta de Cataluña) práctico y muy fácil de seguir.

}

Uno de los programas más sencillos en C:

// Esta línea que empieza por dos barras inclinadas es un comentario

/*
Igual que éstas, que empiezan con una barra inclinada y un asterisco
y seguirá siendo un comentario que finaliza con otro asterisco y otra barra inclinada.

Los comentarios son muy útiles para realizar explicar qué vamos a hacer y,
especialmente, por qué lo hacemos así, ya que pasados unos meses no recordaremos los detalles.
*/

/*
Los includes nos sirven para anunciar al compilador que vamos a utilizar
funciones existentes en otros ficheros, como stdio.h, que en este ejempo
nos proporcionará la función printf para poder mostrar información en la pantalla.
(Esto no es exactamente así, pero ya lo veremos más adelante)
*/
#include

/*
Ésta es una de las formas de empezar un programa en C,
Creando la función principal (main) que todo programa en C necesita para empezar
*/
void main()

// El cuerpo o contenido de la función empieza con la siguiente llave
{

// ¿Adivinas qué hace la siguiente función?
printf ("Hola, Mundo");

// y, previsiblemente, la función acaba con esta otra llave
}

Prueba a hacer algunas modificaciones en esta web, “output” es lo que mostraría el programa en la pantalla, “submit” significa enviar y simula la ejecución del programa. Si te equivocas te indicará los errores, también te puede mostrar “warnings”, avisos.

Ahora realicemos el primer programa para el CM-510

Pero antes instalaremos el software necesario para programar el CM-510 (inglés). Si instalas WinAVR en “C:herramientasWinAVR-20100110” te podrás descargar un zip con todo preparado.

Cuando queramos volver a utilizar los programas RoboPlus Tasks, RoboPlus Motion y demás programas de Robotis deberemos cargar de nuevo el firmware de Robotis, restaurar firmware del CM-510 (inglés)

#include
#include "myCM510.h"

void ejecutarMovimiento1(int ax12Id)
{
dxl_write_word( ax12Id, P_GOAL_POSITION_L, 512);
}

void ejecutarMovimiento2(int ax12Id)
{
dxl_write_word( ax12Id, P_GOAL_POSITION_L, 600);
}

int main(void)
{
int ax12Id=6;

init();

printf("/r/n Un sencillo ejemplo");

printf("/r/n Realizar movimiento 1 con el AX-12 %i", ax12Id);
ejecutarMovimiento1(ax12Id);

printf("/r/n Pausa de medio segundo");
_delay_ms(500); // una pausa de medio segundo

printf("/r/n Pitido!");
buzzOn(100); // pitido

printf("/r/n Pausa de un segundo");
_delay_ms(1000); // una pausa de 1 segundo

printf("/r/n Realizar movimiento 2 con el AX-12 %i", ax12Id);
ejecutarMovimiento2(ax12Id);

printf("/r/n Fin");
}

Los carácteres “/r/n” se utilizan para saltar a la siguiente línea en Windows.

Si has instalado el software necesario (WinAVR debe estar instalado en “C:herramientasWinAVR-20100110”) y descomprimes el fichero TallerProgramacionBioloid_01.zip en el directorio raíz (C:) has de tener todo listo para poder modificar, compilar o simplemente cargar el ejecutable “hola_mundo.hex” en el CM-510. Has de ver algo similar a:

01_Hola_Mundo_Salida_RoboPlus_Terminal

01_Hola_Mundo_Salida_RoboPlus_Terminal

Algunas explicaciones
dxl_write_word( ax12Id, P_GOAL_POSITION_L, 600);

Es el comando incluído en las librerías de Robotis para CM-510 que nos permite enviar órdenes a un actuador Dynamixel de forma muy sencilla. Sólo le tenemos que indicar el ID del AX-12 a mover (en ax12Id), el código de la órden que el AX-12 ha de ejecutar, en este caso situarse en una posición determinada (P_GOAL_POSITION_L) y la posición en la que se ha de situar entre entre la 0 y la 1024 (600).

dx_series_goal

dx_series_goal

Puntos principales:

Descomponer el programa en distintas partes

  • Al haber creado previamente la función init() en myCM510.h/myCM510.c nos permite incluirla en este programa simplificandolo mucho.
  • Además de simplificar la programación permite reutilizar el mismo código en distintos programas. Lo cual nos evita tener que repetir el mismo código muchas veces y, sobre todo, tener que corregir los fallos o mejorarlo sólo en un único sitio, no en todos los programas que se ha repetido. Más adelante veremos cómo organizar los directorios e incluso cómo crear librerías.
  • También nos permite encapsular los detalles de forma que cuando el programa empiece a crecer podamos manejarlos con facilidad sin que nos veamos desbordados.

Mostrar qué está ejecutando el procesador

  • Mediante la función printf podemos enviar a la pantalla texto que nos permite saber qué es lo que está haciendo el programa (printf lo envía al puerto serie y “RoboPlus Terminal” lee de éste y lo muestra por pantalla. Aprenderemos a hacerlo cuando empecemos a programar Bioloid desde el PC)

¿Se te ocurre una forma sencilla de evitar tener dos funciones tan parecidas como “void ejecutarMovimiento1(int ax12Id)” y “void ejecutarMovimiento2(int ax12Id)”?

Programacion C con CM-510: leyendo y moviendo Dynamixel AX-12 (II)

This entry is part 2 of 5 in the series (ES) Programación de CM-510

Programación C con CM-510: leyendo y moviendo Dynamixel AX-12 (II)

En este programa utilizaremos un servo AX-12 para mover otro tal cual movamos el primero. Utilizaremos el ejemplo anterior con algunos cambios para incorporar esta nueva función, como habitualmente ocurre cuando se desarrolla software.

La función que realiza este control es void controlPorAX12(int parametroIdAX12Entrada, int parametroIdAX12Salida):

int esperaMientrasSeSitua(int idAX12)
{
  int estado=COMM_TXSUCCESS;
  boolean enMovimiento=true;

  while (enMovimiento==true)
  {
    enMovimiento = dxl_read_byte(idAX12, P_MOVING);
    estado = dxl_get_result();
    if (estado!=COMM_RXSUCCESS)
	  break;
  }

  return estado;
}

void controlPorAX12(int parametroIdAX12Entrada, int parametroIdAX12Salida)
{
  int posicion=512;

  dxl_write_word( parametroIdAX12Entrada, P_GOAL_POSITION_L, posicion);
  esperaMientrasSeSitua(parametroIdAX12Entrada);
  dxl_write_byte (parametroIdAX12Entrada, P_TORQUE_ENABLE, 0); // quitar toque

  dxl_write_word( parametroIdAX12Salida, P_GOAL_POSITION_L, posicion);
  esperaMientrasSeSitua(parametroIdAX12Salida);

  while(posicion>5 && posicion <1000)
  {
	  posicion = dxl_read_word( parametroIdAX12Entrada, P_PRESENT_POSITION_L );
	  dxl_write_word( parametroIdAX12Salida, P_GOAL_POSITION_L, posicion );
  }
  printf ("nFin controlPorAX12 idAXEntrada:%i, posicion: %in", parametroIdAX12Entrada, posicion);
}

Pero previamente hemos de entrar por RoboPlus Terminal el ID del servo AX-12 mando y del que queremos mover.

void datosDesdeOtroAX12()
{
 int idAX12Entrada=0;
 int idAX12Salida=0;

 idAX12Entrada=obtenerId("de entrada");
 if (idAX12Entrada!=0)
 {
   idAX12Salida=obtenerId("de salida");
   if (idAX12Salida!=0)
   {
     printf("nFinaliza llevando el AX12 %i de entrada a su posición inicial (0) o final (1023)n", idAX12Entrada);
     controlPorAX12(idAX12Entrada, idAX12Salida);
   }
 }
}

La función principal (main) quedaría así:

int main(void)
{
  init();

  // ordenesDesdeTerminal(); el ejemplo anterior refactorizado en una función
  datosDesdeOtroAX12();

  puts("The End");

  return 0;
}

Aquí te puedes descargar el ejemplo completo

Programacion C con CM-510: leyendo y moviendo Dynamixel AX-12 (I)

This entry is part 2 of 5 in the series (ES) Programación de CM-510

Programación C con CM-510: leyendo y moviendo Dynamixel AX-12 (I)

Como segunda entrega de este taller de programación de Bioloid vamos a realizar un programa que nos pedirá el identificador (ID) del AX-12+ a mover y la posición en la que lo situaremos.

Las explicaciones las he ido incluyendo en el código como comentarios, espero que los comentarios sean suficientes como para poderlo seguir, ya me diréis si es así.

El bucle principal es así de sencillo:


int main(void)
{
init();

while(true) // repetiremos eternamente este bucle
{
int id=obtenerId(); // obtenemos el ID del AX-12 a utilizar
int posicion=obtenerPosicion(); // obtenemos la posición en la que situar el AX-12 seleccionado
dxl_write_word( id, P_GOAL_POSITION_L, posicion); // enviamos la orden al Dynamixel
}

return 0;
}

Unas breves explicaciones sobre printf: La función printf es mucho más potente de lo que parece a primera vista, admitiendo bastantes parámetros que nos permiten mostrar una gran cantidad de tipos de datos y formatos diferentes. En el siguiente ejemplo %iindica que en dicha posición del mensaje se incluirá un entero que le pasaremos como parámetro a continuación de la cadena “ID del AX12:”. El carácter de control “n” indica que incluya un salto de línea.

Cada carácter de la cadena se almacena en un posición de memoria [I][D][ ][d][e][l]… siendo las cadenas un caso especial de vector o, en inglés, “array”.

Descárgate aquí una presentación en PDF con explicaciones muy detalladas sobre cadenas en C, de la Universidad del País Vasco.

Las funciones obtenerId() y obtenerPosicion() son también bastante sencillas, ¿verdad?


/*
La siguiente función solicita la entrada de un valor para seleccionar el ID del Dynamixel al que
enviar las órdenes, Comprobando que el que esté entre 1 y 18
*/

int obtenerId()
{
/*
Creamos una variable de un tamaño amplio, 256 bytes (posiciones de un carácter), aunque con 4
valdría, pero si se introdujeran más carácteres de los definidos provocaría un error.
*/
char cadena[256];

/*
Y otra variable de tipo entero, es recomendable asignar siempre un valor, en este caso
la inicializamos con el valor mínimo, 1
*/
int ax12Id=1;

// puts es muy similar a printf, muestra la cadena que recibe como parámetro por la pantalla
puts ("nnVamos a mover un Dynamixel a voluntad. V02");

do
{    // inicio del bucle
puts ("Introduce el ID del servo a mover, entre 1 y 18, ");
ax12Id=leerEnteroComoCadena(cadena); // llamamos a esta función  para entrar por pantalla el valor
//// la admiración (!) es el operador lógico NOT. Repetiremos el bucle mientras el rango NO se valido
}while(!enRangoValido(ax12Id, 1, 18));

// Mostramos el valor introducido
printf("ID del AX12: %in", ax12Id);

return ax12Id;
}

// Repetiremos prácticamente el mismo código que la función anterior, ¿sería fácil crear una función reutilizable para ambos, verdad?
int obtenerPosicion()
{
char cadena[256];
int posicion=0;

do
{
puts ("Introduce un valor entre 0 y 1023 para situarlo en esa posicion");
posicion=leerEnteroComoCadena(cadena);
}while(!enRangoValido(posicion, 0, 1023));

printf("nPosicion: %in", posicion);

return posicion;
}

Las funciones utilizadas para leer el texto de la consola son bastante interesantes,
ya que nos muestran la utilización de una cadena. También nos muestra como utilizar
los fichero .h (declaraciones) y .c (definiciones, el contenido de las funciones):

// Cuerpo (contenido) de las funciones de entrada de datos

/*
Recibimos una variable que tiene reservado espacio en memoria para almacenar
la cadena introducida
*/

void leerCadena(char parametroCadena[])
{
int i=0; // Utilizaremos esta variable como índice para ir almacenando los caracteres introducidos
do
{
parametroCadena[i]=getchar();  // almacenamos el carácter obtenido en la posición i de parametroCadena
putchar(parametroCadena[i]);   // lo mostramos por pantalla
if (parametroCadena[i]=='b')  // si se ha pulsado la tecla <Borrar> (justo encima de <Intro>)
i--;                           //    nos situamos en la posición anterior para escribir de nuevo sobre ella
else                           // si no
i++;                           //    escribiremos en la siguiente posición
}while(parametroCadena[i-1]!='n'); // mientras el último valor guardado NO sea INTRO. El símbolo ! representa el operador NOT
parametroCadena[i]=0;           // en la última posición de una cadena ha de estar el valor cero (NULO, null en Inglés)
}

/*
Leemos una cadena, la convertimos a entero y devolvemos el valor obtenido
*/
int leerEnteroComoCadena(char parametroCadena[])
{
leerCadena(parametroCadena);
return atoi(parametroCadena);
}

El código fuente entero os lo podéis descargar desde aquí.

El lenguaje C, también C++, tiene algunas características muy interesantes, como la inclusión de código dependiendo de determinadas condiciones. Esto permite utilizar el mismo código fuente para distintos procesadores, simplemente cambiando uno o varios parámetros.

A modo de ejemplo este ZIP contiene un proyecto preparado para Dev-CPP con una versión de los ficheros fuente válida para PC y para CM-510; simplemente comentando o descomentando una línea del fichero “myCM510.h” (para utilizarlo con AVR Studio se ha de crear el correspondiente proyecto e incluir estos fuentes).

En lugar de mover el servo AX-12 muestra en pantalla el texto “dxl_write_word” con los parámetros recibidos. Podemos practicar C y realizar distintas pruebas en el PC sin conectar ningún servo.


// Si no está comentada la siguiente línea podremos compilar el programa para PC
// Si está comentada la siguiente línea podremos compilar el programa para CM-510
#define __PC_TEST_

Programacion C con CM-510: leyendo y moviendo Dynamixel AX-12 (III y ultimo)

This entry is part 3 of 5 in the series (ES) Programación de CM-510

Programacion C con CM-510: leyendo y moviendo Dynamixel AX-12 (III y último)

Finalizaremos esta primera serie de programación del CM-510 en C moviendo un Dynamixel AX12 en función de los valores leídos con el sensor DMS.

DMS sensor

DMS sensor

La función “main” es así de sencilla:

int main(void)
{  	
  int idAX12=4;
  int puertoDelSensorDMS=3;

  init();

// los dos ejemplos anterior refactorizados en una función

  // ordenesDesdeTerminal(); 
  // datosDesdeOtroAX12();
  
  puts("Situaremos el AX12 en la posicion que nos indique el sensor DMS");
  ordenesDesdeSensoresPorPasos(idAX12, puertoDelSensorDMS);
  
  // una prueba cruda
  //ordenesDesdeSensoresCrudo();

  puts("The End");

  return 0;
}

Siendo “void ordenesDesdeSensoresPorPasos(int idAX12, int puertoDelSensor)” la función principal del ejemplo:

void ordenesDesdeSensoresPorPasos(int idAX12, int puertoDelSensor)
{       	
/*
    Para evitar las habituales fluctuaciones que sufren los sensores
	sólo tendremos en cuenta las diferencias superiores al siguiente valor 
	entre la anterior lectura y la actual
*/
	int minimaDiferencia=25; 
	int diferencia=0; // la diferencia entre el valor anterior y el actual

	int anteriorValor=0; // el anterior valor obtenido del sensor
	int valor=0; // el de la lectura actual	

	while (true)
	{
	  anteriorValor=valor; // guardamos el anterior valor leído del sensor

	  valor=leerSensor(puertoDelSensor); // leemos el actual

	  diferencia=anteriorValor-valor;
	  //printf("n%i", valor); // por si queremos ir viendo los valores que envía el sensor

	  _delay_ms(100); // una brevisima pausa

	  // diferencias menores que esta las consideramos fluctuaciones del sensor
	  if (abs(diferencia)>minimaDiferencia)
	  {
	    // utilizamos el valor leído del sensor como posición a situar el AX12
		dxl_write_word( idAX12, P_GOAL_POSITION_L, valor);
	  }
	}
}

Utiliza la función leerSensor que tiene cierta dificultad, ya que utiliza la conversión analógico-digital de los microcontroladores ATMega. De momento no entraremos en los detalles, creo que será mejor explicarlo en una próxima serie que trate cuestiones más avanzadas. De todas formas, por si os despierta la curiosidad, os la incluyo a continuación; está basada en los ejemplos de Robotis:

int leerSensor(unsigned char idPuerto)
{
	ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1);	// ADC Enable, Clock 1/64div.

	// printf( "nnIR example for CM-510nn" );

	asignarPuerto(idPuerto);
	//PORTA &= ~0x80;
	//PORTA &= ~0x20;

	//_delay_us(12);				// Short Delay for rising sensor signal
	_delay_us(24);
	ADCSRA |= (1 << ADIF);		// AD-Conversion Interrupt Flag Clear
	ADCSRA |= (1 << ADSC);		// AD-Conversion Start
		
	while( !(ADCSRA & (1 << ADIF)) );	// Wait until AD-Conversion complete
		
	PORTA = 0xFC;				// IR-LED Off
		
	//printf( "%drn", ADC); // Print Value on USART
					
	//_delay_ms(50);
	_delay_ms(esperaLeerSensorMS);

	return ADC;
}

Y aquí os podéis descargar el ejemplo completo.

Programacion C con CM-510: vectores (arrays), puertos y LEDs

This entry is part 5 of 5 in the series (ES) Programación de CM-510

Programanción C con Bioloid CM-510: “arrays”, puertos y LEDs

Cadenas y Vectores (“Strings y Arrays)

Una de las formas más habituales y fáciles de manejar un conjunto de datos en C, y en muchos otros lenguajes como C++, Java y C#,  es mediante cadenas y vectores.

Cadenas

Una cadena consiste en un conjunto de caracteres, como las que utilizamos en el programa “holaMundo.c” del anterior artículo:


int main(void)
{
int ax12Id=6;

init();

printf("rn Un sencillo ejemplo");

...
}

A la función printf le pasamos como parámetro la cadena rn Un sencillo ejemplo”

printf(“/r/n Un sencillo ejemplo”);

En este caso no hemos utilizado ninguna variable para almacenarla, sino que se la hemos pasado directamente, como un literal. Si quisiéramos utilizar dicha cadena en distintas partes la hubiéramos almacenado en una variable, aunque también existen otras razones para utilizar no utilizar literales en el código, como agruparlas en un único punto para facilitar su localización o su traducción).

char titulo[]=”Un sencillo ejemplo”;

printf(“rn %s”, titulo);

Unas breves explicaciones sobre printf: La función printf es mucho más potente de lo que parece a primera vista, admitiendo bastantes parámetros que nos permiten mostrar una gran cantidad de tipos de datos y formatos diferentes. En este caso %s indica que en dicha posición del mensaje se incluirá una cadena que le pasaremos como parámetro a continuación, “titulo” en este ejemplo.

Cada carácter de la cadena se almacena en un posición de memoria [U][n][ ][s][e][n]… siendo las cadenas un caso especial de vector o, en inglés, “array”.

Descárgate aquí una presentación en PDF con explicaciones muy detalladas sobre cadenas en C, de la Universidad del País Vasco.

Vectores

Un vector es probablemente la forma más sencilla de manejar o almacenar un conjunto de datos. En el ejemplo que viene a continuación utilizaremos el siguiente vector de enteros:

int pin[]={1,2,4,8,16,32,64};

Aunque quizás sea más claro si utilizamos la forma “completa”, en lugar de la abreviada, de cargar datos:

int pin[7] //  creamos un vector con 7 posiciones de memoria para almacenar un entero (int)

int[0]=1; // vamos asignando cada valor… ¡Ojo, que el primero es la posición 0 (cero)!

int[1]=2;

int[2]=4;

int[3]=8;

int[4]=16;

int[5]=32;

int[6]=64;

Descárgate aquí una presentación en PDF con explicaciones muy detalladas sobre vectores en C, también de la Universidad del País Vasco.

Los puertos de los microcontroladores

Probablemente el elemento más característico de los microcontroladores son los puertos de Entrada/Salida E/S (Input/Output, I/O en inglés), con los cuales podemos enviar y recibir información para controlar distintos elementos electrónicos como sensores, actuadores y LEDs.

El controlador de Robotis CM-510 nos ofrece 6 conexiones donde podremos utilizar los puertos de entrada salida que el microcontrolador ATMega 2561 (página en inglés del fabricante ATMEL) incorpora.

Controlador CM-510

Controlador CM-510

Cada puerto se controla mediante tres registros (un registro es básicamente una zona de memoria):

  • DDRx: donde se indica si se enviarán o recibirán datos
  • PINx: aquí se reciben los datos
  • PORTx: y desde aquí se envían los datos al exterior del microcontrolador

Para manejar los puertos se han de utilizar los operadores de C a nivel de bits para activar y desactivar cada uno de los bits que representan los puntos de conexión (PIN) que componen los puertos. Estas operaciones utilizan los mismos operadores booleanos que las puertas lógicas y tablas de verdad

tabla y puerta or

tabla y puerta or

tabla y puerta and

tabla y puerta and

Mediante los puertos podremos controlar los LEDs y podremos comprobar si está pulsado alguno de los botones. Por ejemplo:

// El puerto se inicializa con los valores a 1, 1111 1111 ó 0x7F en hexadecimal. OJO 1 (uno) es LED apagado, 0 (cero) es encendido.

// Al ejecutar:
PORTC &= ~0x01; // el complementario de 0000 0001 es 1111 1110

/* la operación AND entre
1111 1111       el puerto C
1111 1110       y el valor complementario de 0000 0001
–—- ——
1111 1110       el resultado es que sólo el LED 1 estará encencido

Son operaciones sencillas pero en las cuales es fácil equivocarse. Las funciones son uno de los elementos fundamentales de C para encapsular los detalles y, una vez comprobado que funcionan perfectamente, “olvidarnos” de ellos:

int pin[]={1,2,4,8,16,32,64};

void encenderYapagarLEDs()
{
	for (int i=0;i<=6;i++)
	{
		ledOn(pin[i]); //enciende LED
	 	_delay_ms(500); // una pausa de medio segundo
		ledOff(pin[i]); // apaga LED
	}
}

teniendo definido previamente, claro:

void ledOn(unsigned char ledId)
{
	PORTC &= ~ledId;
}

void ledOff(unsigned char ledId)
{
	PORTC |= ledId;
}

Habitualmente en lugar de funciones se utiliza #define, pero creo que ahora es mucho más claro utilizar funciones. Por cierto, ¿por qué utilizo al array “int pin[]={1,2,4,8,16,32,64};”?, ¿de qué otras formas se puede hacer?

%d bloggers like this: