Dado que llega Navidad, voy a aprovechar para añadir un circuito para controlar un belén.
La tecnología ha cambiado en cuanto a la iluminación y ahora se pueden hacer cosas que antes eran impensables, como por ejemplo iluminar en RGB, o utilizar solo tensiones de seguridad.
Características del circuito
Alimentación circuito y salidas 1A. 6-12V
Alimentación salidas 2A 5-48V
8 Salidas 2A.
8 salidas 1A.
Conector ICSP
Conector Serire TTL para futuras ampliaciones.
Conector SPI. para futuras ampliaciones.
Uso de las salidas con el firmware actual
En esta versión inicial he implementado en el firmware unas cuantas características básicas para el belén. Eso no implica que no se puedan usar las salidas transistorizadas para otros usos en otro belén.
En el caso que propongo actualmente el belén dispondrá de :
2 Salidas de 2A con posibilidad de alimentar a 48V, para 2 conjuntos de LEDs RGB. Uno para generar el atardecer y otro para generar el amanecer
2 Salidas de 2A con posibilidad de alimentar a 48V, para colocar LEDs de potencia cálidos, para la iluminación central y la transición del amanecer al atardecer.
4 salidas de 1A 12V para el encedido de estrellas en 4 grupos diferentes.
3 Salidas de 1A 12V para el encendido de casas en 4 grupos diferentes.
1 Salida de 1A 12V para el encendido de antorchas y hogueras.
Todo está debe de ser realizado con LEDs.
El atardecer y el amanecer con leds RGB de alta potencia (10W) (20W).
La mañana y la tarde con LEDS cálidos de potenia (20W)
Las estrellas con leds alto brillo agrupados en series de 4 leds o 5 leds.
Las casas igualmente con leds alto brillo agrupados en serie de 4 leds
La hogueras y antorchas con leds rojos alto brillo agrupados en series de 5 leds.
Esquema
Como se puede ver el esquema no puede ser más simple. Todo se hace con un Pic16F73 al que se le amplifica la potencia de las salidas mediante ULN2003 y transistores.
Las salidas que van diréctamente a ULN2003, tendrán 1A máximo de salida, y funcionarán cerrando el circuito a masa. Es decir el + es el común y la salida debe de conectarse al negativo del diodo.
En cambio las salidas conectadas a los transistores funcionan al revés. Es decir, el común es el negativo y las salidas deberán ser conectadas al + del diodo.
Implementaciones en el Firmware actual
El programa del pic está dividido en tres partes.
Control de las estrellas
Control de los fundidos
Control del ciclo.
Control de Estrellas
El control de las estrellas, su tintineo, está implementado en la interrupción RTCC, de forma que periódicamente se entra en esta interrupción y se valora si se deben o no apagar momentaneamente las estrellas. El tiempo que tardará entre dos tintineos es aleatorio, y diferente para cada grupo de estrellas, por lo que no se verán tintinear todas a la vez, sino por grupos. como se ve en el programa he llamado a las estrellas ESTRELLAS1 y ESTRELLAS2 y para cada una hay H y L.
¿Qué significa esto? Que hay cuatro salidas de estrellas. Los grupos 1 y 2 están pensados para ser puestos por el lado del amanecer y por el lado del atardecer, y los H y L son partes del mismo conjunto de estrellas, pero que serán calculadas para que den diferente luminosidad.
Es decir para formar la constelación de Orión, usamos Estellas1_H para las estrellas de primera magnitud y Estrellas1_L para las de segunda magnitud. Para conseguir mayor iluminación en la primera magnitud con respecto de la segunda, bastará colocar una resistencia limitadora en las de segunda magnitud.
Así podremos emular mejor una constelación. Además en el ciclo encenderé antes las de 1ª magnitud que las de la segunda durante el atardecer. igualmente apagaré antes las de 2ª magnitud que las de 1ª magnitud durante el amanecer.
Que haya dos grupos, 1 y 2, nos permite colocar un grupo en el amanecer y otro en el atardecer del belén, de forma que podemos encender antes las del lado de amanecer cuando atardece y apagar antes las del lado del atardecer cuando amanece.
Control de fundidos
Para conseguir realizar el amanecer y la transición del amanecer al atardecer, he usado fundidos, más o menos complejos.
Para gestionar esos fundidos uso PWM. Este PWM está programado en la interrupción del TIMER1.
En ella, según el valor hayamos dado a la intensidad de luz, 0 a 255, se generará un PWM para controlar la potencia de la salida.
En este caso las salidas controladas por este método son los tres colores del atardecer, y los tres del amanecer, la mañana y la tarde. En total 8 salidas, que podrían ser usadas para otras cosas en el futuro.
Para mayor comodidad en la gestión de los valores he creado dos estructuras
La priemera Tipo_crepusculo, permite definir los colores el atardecer y el amanecer, para conseguir pasar del blanco del día al azul de la noche, a través de un rojo del atardecer.
typedef struct { //estructura para control rgb del
int rojo;
int verde;
int azul;
}Tipo_crepusculo;
Esta estrucutra es usada a su vez para crear unas variables de nivel de potencia de las luces, de forma que sean fáciles de identificar y manejar en el programa.
struct Niveles_Str {
Tipo_crepusculo LUZ_Amanecer, LUZ_atardecer;
int LUZ_manana;
int LUZ_tarde;
} Nivel, Nivel_tmp;
Control de ciclo
Una vez que tenemos la estructura de mando del sistema, solo queda pensar en el ciclo.
El ciclo, en este caso es muy complejo. Por pasos sería
Subimos la Nivel.LUZ_Amanecer pasándolo del azul de la noche al blanco
Antes incluso de alcanzar el blanco en el amanecer, comenzamos a subir la mañana, que es luces colocada a 1/3 de la longitud del belén.
Cuando se llega a un nivel en la mañana comenzamos a subir la tarde, y luego subimos el Nivel.LUZ_Atardecer, con todos los colores igual ,para subir en blanco.
Esto nos ilumina completamente el belén desde el amanecer a toda la escena de forma progresiva.
Una vez alcanzado el día, esperamos un tiempo y comenzamos el atardecer.
Para ello vamos bajando la luz del amanecer del blanco al azul, pasando por el rojo.
De forma sincronizada, vamos apagando las luces de la mañana,y la tarde. De forma que vayan en escala descendente.
Cuando el amanecer esta en rojo y está apagada casi la mañana y ya ha ido bajando la tarde, comienzo el descenso del atardecer del blanco al rojo.
Para finalizar, el amanecer queda en azul, la mañana y la tarde se apagan y el rojo del atardecer pasa finalmente a azul. Se ha hecho la noche.
Durante este proceso, deberemos ir encendiendo y apagando las estrellas, las casas y las antorchas y fuego.
Como en el caso anterior lo iremos haciendo progresivamente de forma sincronizada con las luces del atardecer y el amanecer.
Cuando comienza a caer la tarde, vamos encendiendo las antorchas, algunas casas y las estrellas del la de del amanecer , y cuando aparece el crepúsculo se van encendiendo las estrellas del lado del atardecer. Todas ellas en dos fases, Primero las más intensas y luego las débiles.
La noche queda como se desee, alternando casas y dejando encendidas las estrellas. El fuego y las casas se apaga al cabo de un tiempo.
Llegamos al amanecer. Entonces, vuelvo a encender las casa y el fuego. Y volvemos a ir subiendo el amanecer del azul al blanco. Volviendo al paso 1. Durante un tiempo, mientras amanece, permanecerán las hogueras y algunas casas encendidas.
Para implementar este ciclo, por lo tanto, en el programa he creado cuatro rutinas. Amanecer, Atardecer, Día y Noche, en las cuales se irá colocando el ciclo decada parte. Igualmente he creado constantes de tiempo para el fundido y los retardos principales, para poder variarlos de forma rápida.
Mejoras
En otra entrega usaré lasa salidas de comunicación reservadas RS-232 y SPI.
Con el SPI controlarré tarjetas adicionales, tales como figuras con servocontroles, o motorrizadas, las cuales se crearán con su propio contról de ciclo y serán gestionadas mediante esta tarjeta mediante comandos.
Igualmente en el RS-232, podremos conectar un Bluetooth o un ESP8266, para gestionar el ciclo, tiempos, puesta en marcha u otras características del belén mediante un móvil o internet.
Pero eso queda para otro día.
Tenía pensado espera a tener acabado una rutina para controlar el robor en wifi, pero debido a la falta de tiempo, no puedo pasar el código inicial que hice, y que está aquí a la versión posterior con bluetooth que he puesto en la entrad anterior.
Por ello, debido a que me han pdido un código para mover un robot mandado por ESP8266, pongo el código, tal como lo tenía en el vido de youtube, para que pueda ampliarlo y terminarlo el que lo desee.
Igualmente coloco el MainActivity del Android del programa que está en el video.
Clase Robot Cabecera clase // *********************************************** // Clase Robot // Por Jos� Angel Moneo // Gestiona el Robot creado por m�- // LLeva dos ruedas mediante servos continuos // Dos sensores de distancia ultras�nicos, uno adelante y oro atras // Un sensor de audio // Un m�dulo ESP8266 wifi // No necesitaremos calibrado de motores, pues cada motor se calibra independientemente al ser servos // ****************************************************
//D2 y D3 reservadas para encoder //Reservadas SPI 10,11,12,13 //Reservadas I2C A4-A5 //Reservada Analogicas A7 //Libres Generales 5,6,A0,A1,A2,A3
class Robot { public:
int PosMI,PosMD; //posiciones de los motores boolean _Autonomo; private: int _VD,_VI; int _Velocidad; //Velocidad del robot int _Direccion; int _Nataques; //cuenta ataques double cntEncoderD; double cntEncoderI; boolean _Marcha; boolean Marcha; //Robot en marcha //Servos Servo _MotorI; Servo _MotorD;
void Robot::Move()
{ //Da orden de velocidades para movimiento conituno
if(_Direccion>90) //Direccion a derecha
{
_VD=_Velocidad;
if (_Velocidad<90) _VI=90-(90-_Velocidad)/(_Direccion-90);
else _VI=(_Velocidad-90)/(_Direccion-90)+90;
}
if(_Direccion<90) //Dirección a Izquierda
{
_VI=_Velocidad;
if (_Velocidad<90) _VD=90-(90-_Velocidad)/(90-_Direccion);
else _VD=(_Velocidad-90)/(90-_Direccion)+90;
void Robot::Move(boolean sent,int Dist)
{ //invertimos el sentido si es necesario
if (sent==Av) _VI=_VD=abs(_Velocidad-90)+90;
if (sent==Rt) _VI=_VD=90-abs(_Velocidad-90);
_MotorI.write(180-_VI); _MotorD.write(_VD);
delay(Dist*10);
Stop();
_Marcha=false;
}
//Al clickear en conectar
btconect.setOnClickListener(new OnClickListener() {
@Override
// conectar
public void onClick(View v) {
//Nos conectamos y obtenemos el estado de la conexion
boolean conectstatus = Connect();
//si nos pudimos conectar
if (conectstatus) {//mostramos mensaje
Set_txtstatus("Conexion OK ", 1);
Change_leds(true);//camiamos img a verde
} else {//error al conectarse
Change_leds(false);//camiamos img a rojo
//mostramos msg de error
Set_txtstatus("Esconectado.. ", 0);
}
}
});
//Al clickear en desconectar
btdisconect.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
boolean conectstatus=Disconnect();
if (conectstatus) {//mostramos mensaje
Set_txtstatus(".. En espera ..", 1);
Change_leds(false);//camiamos img a verde
}
else {//error al conectarse
Change_leds(false);//camiamos img a rojo
//mostramos msg de error
Set_txtstatus("Desconectado.. ", 0);
}
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
//Enviamos mensaje de accion segun el boton q presionamos
public void Snd_Action(int bt) {
String msg="";
//no hay texto
//seteo en el valor action el numero de accion
switch (bt) {
case 0:
msg = "$C0";
break;
case 1:
msg = "$C1";
break;
case 2:
msg = "$C2";
break;
case 3:
msg = "$C3";
break;
case 4:
msg = "$C4";
break;
case 5:
msg = "$C5";
break;
case 6:
msg = "$C6";
break;
case 10:
msg = "$C10";
break;
}
//mando msg
boolean val_acc = Snd_Msg(msg);
//error al enviar
if (!val_acc) {
Set_txtstatus(" Error ", 0);
Change_leds(false);
Log.e("Snd_Action() -> ", "!ERROR!");
}
if (!socket.isConnected())
Change_leds(false);
}
/*Metodo para enviar mensaje por socket
*recibe como parmetro un objeto Mensaje_data
*retorna boolean segun si se pudo establecer o no la conexion
*/
public boolean Snd_Msg(String msg) {
try {
//Accedo a flujo de salida
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true);
if (socket.isConnected())// si la conexion continua
{
//Envio mensaje por flujo
out.println(msg);
//envio ok
return true;
} else {//en caso de que no halla conexion al enviar el msg
Set_txtstatus("Error...", 0);//error
return false;
}
//cambia el imageview segun status
public void Change_leds(boolean status) {
if (status)
leds.setImageResource(R.drawable.on);
else
leds.setImageResource(R.drawable.off);
}
/*Cambiamos texto de txtstatus segun parametro flag_status
* flag_status 0 error, 1 ok*/
public void Set_txtstatus(String txt, int flag_status) {
// cambiel color
switch (flag_status){
case 0:
txtstatus.setTextColor(Color.RED);
break;
case 1:
txtstatus.setTextColor(Color.GREEN);
break;
case 2:
txtstatus.setTextColor(Color.BLACK);
break;
}
txtstatus.setText(txt);
}
//Metodo de desconexion
public boolean Disconnect() {
//Prepramos mensaje de desconexion
//avisamos al server que cierre el canal
try {
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return false;
}
if (socket.isConnected())
return false;
return true;
}
Hoy pongo aquí una pequeña prueba para usar el bluetouth para comandar un robot.
Para facilitar las cosas he utilizado un programa de control ya diseñado para el android.
Entre todos los programas de control de robots que están disponibles en google play, he elegido el Arduino Joystick Controller. Es muy completo y sencillo a la vez. En enste caso nos sobran opciones, pues para empezar solo moveremos el robot. Pero nos queda preparado para futuras ampliaciones. La parte mñas interesante del progrma es el hecho de que pueda ya hayan implementado la visualización de dcatos, y el hecho de que podamos parametrizar los límites y valores de los mandos, de forma que podremos ajustar la respuesta de los motores sin tener que reprogramar el arduino. Simplemente variaremos los límites y la posición cero de los slider.
El robot lleva solo dos ruedas, pero controladas por servocontroles paralax de movimiento continuo de alta velocidad. Pueden verse sus características aquí.
Los servos son más fáciles de controlar que los motores de continua, sobre todo a velocidades bajas. Este servo puede ir de 0 a 60hz. Las señales de control son mucho menos, pero un poco más difíciles de gestionar a nivel de soft. Pero para mí son ventajas.
Otra ventaja del servo es su facil colocación. Aunque en la foto no se vea, no están atornillados. Simplemente están pegados con cinta adesiva doble cara al chasis.
Como hemos dicho el hard se simplifica, pues solo necesitamos una salida por servo paracontrolar su sentido y velocidad. La salida funciona en PWM de forma que da un pulso de duracion definida cada 20ms. La duración de ese pulso definirá la velocidad y sentido del servo.
En este punto encontraremos un problema con el arduino. La librería servo del arduino, genera el PWM mediante una interrupción del Timer1. Esta interrupción que se usa para medir tiempos y determinar cuando encender y apagar la salida resulta ser la misma que se utiliza para softserial. Es decir está compartida. Esto es un problema, pues si deseamos leer y enviar muchos datos por el puerto serie y a la vez hacer PWM, es muy posible que en el momento que se deba bajar la señal del PWM el micro esté ocupado gestionando el puerto serie. Eso hará que se alargue el tiempo del pulso PWM y se trasmita una velocidad indebida. Esto se traduce en un tembleque de los servos durante el control.
Para evitarlo solo hay dos maneras. O usamos PWM por hardware, o usamos puerto serie por hardware. El segundo caso no podemos pues ya está ocupado para el usb, y la primera forma no es facil, pues el arduino ya ha implementado la función servo y manejar el PWM a perlo del micro solo nos serviría para 1 servo, pues no tiene mas que una señal de salida PWM por hard. Al menos el uno y el leonardo.
Entonces la mejor solución, y además preparada para ampliaciones del robot, es compar una tarjeta de 16 PWM contolada por I2C.
De esta forma dispondremos de 14 salidas de servo en reserva. El único problema es que debido a la duración el ciclo PWM del servo son 20ms, y en cambio el pusso debe de ser de 1,5ms, la resolución efectiva del PWM para controlar el servo es muy pequeña. En concreto 24 puntos. Pero suficiente, ya que nos da 24 velocidades.
Inicialmente solo le pondré quí un movimiento comandado remoto. Mediante este modo, guiaremos el robot como un coche teledirigido, pudiendo parar, ir marcha atras y adelante y girar. Mas adelante añadiré otras funciones.
Para poder hacer mejoras futuras en mi robot, he construido una clase específica para el robot, en la que podré ir incluyendo las funciones que vaya implementando, o derivarla en futuros proyectos.
La rutina de control de comunicaciones la he dejado dentro del bucle principal del programa, pues es la parte que quiero testear..
Al robot le he implementado un sensor ultrasonico por delante y otro por detrás para futuras funcione. En este caso simplemente pasaré la medida de distancia a través del bluetooth al Joystick Controller.
Como es de enteneder la comunicación entre el robot y el android se hará por bluetooth, conectando este a los pines 10 y 11 del arduino.
Programa principal
/************************************************************
* Programa de control de Robot
* Basado en la clas Robot, integra la comunicación y comandos
* Por José Ángel Moneo
* Escrito para Arduino 09-09-15
* Se integra la comuniciación con bluethooth
* Con los comandos compatibles con la aplicacion
* Arduino Joystick Controller
* https://play.google.com/store/apps/details?id=com.andico.control.joystick
* Explicacion comandos en
* https://sites.google.com/site/bluetoothrccar/home/6-Joystick-Control
*
* Estructura del Mensjase mando 4 bytes
* Byte 0 Comando 243=0xF3 parado, 241=0xF1 avance, 242=0xF2 retroceso, 244=0xF4 cabeza 245=0xF5 Apagar todo
* Byte 1 Velocidad
* Byte 2 Dirección
* Byte 3 Banderas
* bit 0 E
* bit 1 D
* bit 2 C
* bit 3 B
* bit 4 A
* bit 5 claxon
* bit 6 Freno
* bit 7 Luces
*
* Estructura recepción sensores 14 bytes
* Byte 0 Inicio datos = 0xee
* Byte 1-4 Dato 1 coma flotante
* Byte 5-8 Dato 2 coma flotante
* Byte 9 Dato 3 byte
* Byte 10 Dato 4 Byte
* Byte 11 Dato 5 Byte
* Byte 12 Dato 6 Byte
* Byte 13 Fin datos = 0xcc
*
***********************************************************/
// bytes recibidos del mando bluetooth
int mando=0,velocidad,direccion,botones;
unsigned int id=0; //Bandera de inicio de comandos
unsigned long timer0; //sobrepaso tiempo datos recibidos
unsigned long timer1; //Tiempo para siguiente envío
//Lectura comando
void mando_read()
{ byte n;
//Si pasa demasiado tiempo sin respuesta para el robot y vuelve a esperar primer comando
if((millis() - timer0)>400){
MyRobot.Stop();
id=0;
mando=0;
}
if (blue.available() > 0 ) {
n=blue.read();
if (n==241||n==242||n==243||n==244||n==245) {
while (blue.available() <3); //Espera el resto de los datos
mando=n;
velocidad=blue.read(); //SE enviará
direccion=blue.read();
botones=blue.read();
timer0 = millis();
}
}
}
void mando_write()
{ byte three[14] = {0xee,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xcc};
if((millis() - timer1)>=477){ //Check if it has been 477ms since sensor reading were sent
//Break the sensor0 float into four bytes for transmission
three[1] = Sensor0.a[0];
three[2] = Sensor0.a[1];
three[3] = Sensor0.a[2];
three[4] = Sensor0.a[3];
//Break the sensor1 float into four bytes for transmission
three[5] = Sensor1.a[0];
three[6] = Sensor1.a[1];
three[7] = Sensor1.a[2];
three[8] = Sensor1.a[3];
//Get the remaining reading from the analog inputs
three[9] = MyRobot.DistFront(1); //Muesta la distancia frontal
three[10] = Sensor3;
three[11] = Sensor4;
three[12] = Sensor5;
//Send the six sensor readings
blue.write(three,14);
//Store the time when the sensor readings were sent
timer1 = millis();
//Lectura de comandos
mando_read();
//ShowComand();
//////////////////Gestión de comandos
switch(mando) {
case 0xF1: //Avanzar
// velocidad adelante
//recodificamos la velocidad para usar signo para el sentido. El
programa de android está configurado para enviar de 2 a 24 con centro
en 0
MyRobot.SetVel(velocidad);
MyRobot.SetDireccion(direccion);
MyRobot.Move();
break;
case 0xF2: //Retroceder
// velocidad Atras
MyRobot.SetVel(-velocidad);
MyRobot.SetDireccion(direccion);
MyRobot.Move();
break;
case 0xF3: // parado
MyRobot.Stop();
break;
case 0xF5: //detener todo
MyRobot.Stop();
break;
}
Wire.begin(); //inicialización I2C
//Inicialización Driver PWM
pwm->begin();
pwm->setPWMFreq(50); //Pulsos cada 20ms como indica elmanual de paralax (50Hz)
//Punto parada del servo 1,5ms. Maxima velociad atras en 1,3ms y adelante en 1,7ms
Stop();
}
//asigna velociad al servo -180 a 180 dado que el sevo alcanza 180rpm en cada sentido
// -180 rpm son 1,3ms, 0 pulso 1,5ms, 180 rpm de 1,7ms
// por lo tanto 180 rpm equivale a 0,2ms indicando el signo sumar o restar
// 20 ms del periodo son 4096 pulsos
// Debemos generar el pwm a 50Hz, 20ms según el manual del servo.
//Dado que la resolución del pca9658 es 12 bits (4096) tendremos 4096/20=204 pulsos por milisegundo
//Como el margen de regulación del servo está entre 1,3 y 1,7ms nos deja 0,4ms pararegular
// con la resolución del PCA9658 a esta frecuenca nos deja 0,4ms*204puslos por ms= 122 pulsos para definir todo el rango, 61 para cada lado
// Como el punto central son 1,5ms 1,5*204,8=307,2 pulsos sería el punto de parada.
// Como el servo tiene una cierta regulación provando se ve el punto central en 310ms, por lo que el rango de movimiento va de 250-310-370
// Experimentalmente se ha visto que parece estar mas bien en 260-310-360
//*********Por todo esto dejo Centro en 310 pulsos. La velocidad se codificará: -24,0,24
void Robot::setServoVel(uint8_t nservo, double Velocidad)
{
pwm->setPWM(nservo, 0, 310+Velocidad); //recodifica la velocidad
}
void Robot::Move()
{ int vi,vd;
if(_Velocidad==_OldVelocidad && _Direccion==_OldDireccion) return;
vi=vd=_Velocidad;
if (_Direccion<50) vd=_Velocidad*(_Direccion)/50;
if (_Direccion>50) vi=_Velocidad*(100-_Direccion)/50;
_VI=vi;_VD=-vd;
// ***********************************************
// Clase Robot
// Por Jos� Angel Moneo
// Gestiona el Robot creado por m�-
// LLeva dos ruedas mediante servos continuos
// Tarjeta de control de servos PWMServoDriver de 12 bits 16 canales Canales 0 y 1 para las ruedas
// Dos sensores de distancia ultras�nicos, uno adelante y oro atras
// Un m�dulo BLUETOOTH
// No necesitaremos calibrado de motores, pues cada motor se calibra independientemente al ser servos
// ****************************************************
void Move();
void Stop();
boolean EnMarcha(){return _Marcha;} // test estado robot
// sensores
int DistFront(boolean medicion);
int DistBack(boolean medicion);
private:
void setServoVel(uint8_t nservo, double rpm); //Asigna la velocidad rpm en pulsos pwm al servo servo
};
En esta entrada voy a resolver un típico problema que nos surge al leer sensores e intentar mostrar o tomar los datos.
Normalmente los sensores, tienen un error suficientemente grande como para distorsionarnos la lectura. Por ejemplo, un termopar tipo k puede tener un error de +-2,2ºC. Suficiente, para volvernos locos la lectura.
Pero eso no significa que mida mal, sino que va a estar oscilando sobre la medida real 2,2 grados.
En el caso de otros sensores como son presostatos podremos encontrar igualmente un error sobre el fondo de escala de un 1%.
Posíblemente el error de la sonda y la precisión de la medida no sean iguales. Si leemos un sensor con un 1% de error con un ADC de más de 7 bits estaremos viendo oscilaciones en la medida que no son realmente oscilaciones de presión sino errores en las medidas del sensor.
Por lo tanto según esto ¿Para qué usar entradas analógicas de 16 bits para leer un sensor, si todo lo vamos a leer error?.
Cuando leamos con 10 o 16 bits, vemos unas oscilaciones como ruido, que nos impedirán determinar exactamente donde se encuentra la sonda. Si visualizamos el dato en un display y lo hacemos rápido, veremos oscilar constantemente el dígito de menos peso, y si lo hacemos cada segundo estaremos leyendo datos que aunque nos parezcan correctos, tienen un error.
Planteamiento
¿ Entonces que podemos hacer?, ¿Usar solo resoluciones bajas?. Pues no.
Aunque la sonda tiene un error, por ejemplo 2,2º en el termopar. Este error siempre oscila alrededor de la medida real. por lo tanto el error es como un ruido blanco aleatorio en radio.
Por ello podremos promediarlo con un filtro de MEDIA MÓVIL, y obtener el valor real, con precisión.
Si las medidas se realizan de forma que se procesen posteriormente, lo más correcto es tomar las medidas reales y almacenarlas tal cual, para poder aplicar el filtro posteriormente con un programa, de forma que pueda variarse las condiciones del filtro según se desee e incluso probar con varias opciones.
Pero si lo que queremos es mostrarlo en un display, tenemos un pequeño problema.
En un filtro de media móvil de largo M, la salida actual consiste en el promedio de las últimas M muestras de la entrada.
La función de transferencia de este filtro es:
M> significa más suavizado, y M< menos suavizado
Para tomar el valor filtrado de un punto de la onda debemos conocer el valor real de la últimas M medidas.
Solución
Para que podamos utilizar los datos del display estos deben refrescarse con una cierta cadencia. Es decir debemos mostrar el dato con unos 200ms mínimo de cadencia. Más rápido es tontería, porque no veremos nada.
Aprovechando este tiempo, normalmente se realiza un bucle de M lecturas para calcular la media y mostrarla en el display, Esto realmente genera un retraso de M lecturas y si el sensor es lento , como es el caso de un sensor de temperatura DS18B20, o un sensor MAX6675, la respuesta en el visor es muy lenta.
Por ejemplo el MAX6675 debemos leerlo cada 200ms si queremos asegurarnos que de la lectura sea correcta. Si queremos un filtro de orden 5 (M=5), esto nos dará un bucle de 1000 ms. Por lo que no podremos refrescar la pantalla en intetervalos inferiores a un segundo. Si el orden es mayor el refresco está más comprometido aún.
Para evitar este retraso voy a hacer una función que realice la meda sobre una cola. Así podré refrescar el dato en cada lectura del sensor, aun aplicando el filtro. Dado que los datos están en una pila, solo iré añadiendo la última lectura real a la pila en cada lectura al tiempo que realizaré la media con las M-1 lecturas anteriores que ya están en la pila. Así puedo conseguir un refresco de los datos cada 200ms independientemente del número de muestras que tome.
El único retraso que veré al aumentar M será el retraso a la respuesta a un escalón, que irá aumentando al aumentar M.
Operatividad
Para realizar el filtro, vamos a realizar una pila de M datos. Iremos empujando los datos que vayamos leyendo sobre la pila. En cada lectura realizaremos el cálculo del valor medio a mostrar. Trasladando los valores de la formula a la pila, y considerando que siempre almaceno el nuevo dato en x[0] empujando los demás hacia el fondo, el valor más antiguo está en x[M] y el más actual en x[0].
Hay que tener en cuenta que un filtro genera un retraso. El filtro provoca una respuesta más lenta ante escalones bruscos. Esto es una desventaja y una ventaja. El fin precisamente del filtro es eliminar los picos y escalones de frecuencia alta, que no dan datos, sino ruido y tomar una media con ellos para mostrar un valor más real. Pero si necesitamos mostrar datos en una señal con rápidos cambios, puede que el filtro nos genere un retraso adicional. Este retraso se verá en el encendido inicial o cuando la sonda se conecte o desconecte, pues en estos casos el salto es muy elevado.
Como el valor mostrado es la media, esta variará de forma progresiva desde el valor inicial al valor final al producirse un escalón. Si El escalón resultase muy alto, aparecerán medidas de valores intermedias durante algunos segundos. Pero esto ya es algo que estamos acostumbrados a ver en los sistemas comerciales.
En cambio veremos la medida con mucha más precisión y estabilidad, eliminando reduciendo drásticamente el error del sensor.
Por lo tanto, hay que valorar el parámetro M del filtro. Cuanto mayor sea más estable se verá la medida, y mejor filtrará el ruido, pero solo servirá para sistemas con alta inercia. Si el sistema puede variar sus parámetros de forma rápida habrá que reducir M a 3 o menos, para que el sistema tenga una respuesta más adecuada.
En este gráfico puede verse la diferencia entre M altos y bajos.
Gráfico tomado de "Introducción a los Filtros Digitales clase 10"
Realizaré el código en C para Pic.
Código 1
////////////////////////////////////////////////////////////////////
// Librer¡a Filtro Media Movil //
// compilador PIC CCS //
// Realizado por Jose Angel Moneo //
// Funciones: //
// float MediaMovil(int ntabla,float valor)
// devuelve el valor medio calculado al almacenar un nuevo dato //
// El valor se calcula y maneja con la tabla ntabla //
////////////////////////////////////////////////////////////////////
//*********************** Definición de pila*********************
//Estas constantes deben ser definidas en el programa principal, pues podrá manejarse varias pilas por el programa.
//#define NSensor_MediaMovil 1 //defineimos el número de sensores
//#define Muestras_MediaMovil 10 //definimos el número de muestras de la media movil
//En caso de no haber definidio las tablas se define 1 con 10 muestras
//Pilas de datos para el cálculo
float PilaSensor[NSensor_MediaMovil][Muestras_MediaMovil];
//Devuelve un nuevo valor de la media al añadir un nuevo dato a la pila
float MediaMovil(int ntabla,float valor)
{ int i;
float Ynuevo;
Ynuevo=0;
//Hace hueco moviendo los datos hacia el fondo y suma los datos para la media
for(i=Muestras_MediaMovil-1;i>0;i--)
{PilaSensor[ntabla][i] = PilaSensor[ntabla][i-1];
Ynuevo+=PilaSensor[ntabla][i];}
//Empuja el nuevo dato en la pila y suma el nuevo dato
PilaSensor[ntabla][0]=valor;
Ynuevo+=PilaSensor[ntabla][0];
//Calcula el nuevo valor medio
Ynuevo/=Muestras_MediaMovil; //calcula la media
return Ynuevo;
}
Código 2. El mismo algoritmo pero con cola circular mediante punteros (más rapido)
////////////////////////////////////////////////////////////////////
// Librer¡a Filtro Media Movil V2 //
// compilador PIC CCS //
// Realizado por Jose Angel Moneo //
// Funciones: //
// float MediaMovil(int ntabla,float valor)
// devuelve el valor medio calculado al almacenar un nuevo dato //
// El valor se calcula y maneja con la tabla ntabla //
////////////////////////////////////////////////////////////////////
//*********************** Definición de pila*********************
//Estas constantes deben ser definidas en el programa principal, pues podrá manejarse varias pilas por el programa.
//#define NSensor_MediaMovil 1 //defineimos el número de sensores
//#define Muestras_MediaMovil 3 //definimos el número de muestras de la media movil
//En caso de no haber definidio las tablas se define 1 con 10 muestras
//Pilas de datos para el cálculo
int16 ColaSensor[NSensor_MediaMovil][Muestras_MediaMovil]; //Tabla historico últimos valores
int16 VADC[NSensor_MediaMovil]; //Suma Valores de las entradas analógicas pasadas por filtro media movil
int antiguo=0;
int nuevo=0;
//Devuelve un nuevo valor de la media al añadir un nuevo dato a la pila
int16 MediaMovil(int ntabla,int16 valor)
{ // Calcula la nueva suma
VADC[NSensor_MediaMovil]-=VADC[ntabla]-ColaSensor[ntabla][antiguo]; //Resta el valor más antiguo
VADC[NSensor_MediaMovil]+=valor; //Calcula la nueva suma
ColaSensor[ntabla][nuevo]=valor; //Almacena el nuevo valor en la cola
//Actualiza los punteros de cola
if(antiguo++>Muestras_MediaMovil) antiguo=0;//Hace la cola circular
if(nuevo++>Muestras_MediaMovil) nuevo=0;//Hace la cola circular
//Devuelve el nuevo valor medio
return (VADC[NSensor_MediaMovil]/Muestras_MediaMovil);
}
Implementación
Para implementarlo en el programa principal donde se toman los datos, basta con llamar a la función con la lectura actual.
Ejemplo de implementación.
Voy a usar el filtro para estabilizar y mejorar la lectura de tres sensor termopar tipo k leído con un MAX6675 con PIC.
Para leer el sensor a través del MAX6675 lo haremos por SPI por lo que la instrucción de lectura usaremos la función SPI_XFER
Configuraremos la lectura de SPI de los tres sensores mediante
Cambiando el ENABLE de cada MAX6675 y el STREAM por el nombre que deseemos.
Para leer el valor del sensor usaríamos SPI_XFER(Sensor1,0)>>3, despreciando los bits de control, y para obtener su valor en grados lo multiplicaríamos por 0.25.
Por lo tanto, leer su valor en grados sería (SPI_XFER(Sensor1,0)>>3)*0.25.
Esto lo podríamos mostrar ya en el display, pero veremos como el valor puede oscilar más de un grado en cada lectura.
Para corregirlo vamos a implementar la función de media móvil.
Primero incluimos la función en el programa y definimos los parámetros que deseamos.
Esto nos leerá el sensor, lo convertirá en grados y lo almacenará en la tabla de la media móvil ya en coma flotante. Así mismo la función MediaMovil, nos devolverá el valor filtrado en coma flotante, el cual almacenamos en una variable en coma flotante llamada T_Sensor1. que posteriormente usaremos es para mostrar en el display.