9/22/2020

Primeros pasos con Arduino nano 33 Sense BLE

 

 



 

 

Saludos, estamos vivos y en este tiempo es bueno poder decir esto. Bien ha sido un periodo muy extraño que por suerte nos ha permitido seguir trabajando desde casa en horarios no euclidianos.

Entre otras muchas cosas me llego un proyecto para trabajar con un Arduino nano 33 sense con Ble. La placa por menos de 35 €  ofrece una cantidad de sensores:

·        Sensor de inercia de 9 ejes.

·        Sensor de humedad y temperatura.

·        Sensor barométrico

·        Sensor de gestos, proximidad, color, luz

·        Micrófono

·        BLE: bluetooth de bajo consumo

Sin daros mucho la chapa, como comenzar a trabajar con el esta muy bien explicado en el tutorial de arduino.

Quizá tengáis algo de trabajo en instalar todas las librerías, porque no están agrupadas en una sola.

 

#include <ArduinoBLE.h>                //Bluetooth ble

#include <Arduino_LSM9DS1.h>      // IMU //

#include <PDM.h>                            // Digital microphone

#include <Arduino_APDS9960.h>    // Gesture sensor //

#include <Arduino_LPS22HB.h>     // Pressure //

#include <Arduino_HTS221.h>      // Relative humidity and temperature //

 

Casi todas fácilmente instalables desde el administrador de bibliotecas.

 

La gracia es tratar de extraer la información de los sensores y enviarlos por bluetooth. Pero para esto que con un bluetooth típico HC-06  el funcionamiento es muy similar al Serial. Pero con el ble es otro mundo y este post es ayudar a solucionar algunos problemas que me he topado.

Para explicarlo vamos a usar un simple código donde enviaremos el valor de la IMU y recibiremos un char que nos variara el estado del led (Pin 13). El Ble su uso esta muy extendido en un montón de periféricos y su uso no debería ser problemático… Ahora bien, intentar capturar los datos mencionados en el ejemplo anterior en un ordenador con Windows.  Sin la aplicación específica del fabricante es complicado.





Topología del modo connected

 

Insisto no os voy a dar la chapa, aquí tenéis una buena explicación de como funciona el bluetooth low energy BLE. Resumiendo, nosotros en el Arduino vamos a tener que crear una serie de servicios con unas propiedades, de lectura, escritura o notificación. Y desde la central (programa Python en Windows) nos conectaremos cuando necesitemos y obtendremos los datos.

 

 

Cabecera del programa

 

#include <ArduinoBLE.h>

#include <Arduino_LSM9DS1.h> //IMU

 float Ax, Ay, Az, Gx, Gy, Gz, Mx, My, Mz;

const int BUFFER_SIZE = 64;

char msgprint[BUFFER_SIZE];

 

 const char* uuid_service="00001101-0000-1000-8000-00805f9b34fb";

const char* uuid_string="00001143-0000-1000-8000-00805f9b34fb";

const char* uuid_char="00001150-0000-1000-8000-00805f9b34fb";

 

BLEService customService(uuid_service);

BLECharacteristic Send_string(uuid_string,BLERead | BLENotify ,BUFFER_SIZE,false);

BLECharCharacteristic led_control(uuid_char, BLERead | BLEWrite);// 0,1

 

 

Las direcciones uuid tiene un formato de 16 bits o 128, en principio pensé que podían ser inventadas, y pueden pero el primer valor 0x1101 o 0x1143, representa el GATT, que en función del valor que sean que se está enviando, aquí podéis ver una lista de perfiles existentes. Esto esta pensado para hacer el protocolo más estable entre fabricantes. Si ponéis un perfil  especifico, en las aplicaciones de Android o ios os aparecerá ese nombre en el servicio. Si no aparecerá toda la dirección.

 

Definimos tres, la principal es la que contendrá el resto de los servicios vendría siendo el “profile”.

La segunda es la dirección donde enviaremos el string, como características se puede hacer una orden de lectura o recibir notificaciones de cambios.

Y la tercera es donde leeremos y escribiremos en un char que emplearemos para activar el led.

 

 



Servicios y caracteristicas

 

 

Setup del programa

void setup() {

  Serial.begin(9600); //to debug

  if (!IMU.begin()) {    // init IMU

    Serial.println("Failed to initialize IMU!");

    while (1);

  }

  if (!BLE.begin()) {   //init  BLE

    Serial.println("starting BLE failed!");

    while (1);

  }

  BLE.setLocalName("IMU_test"); // name of Bluetooth

  BLE.setAdvertisedService(customService.uuid());

  customService.addCharacteristic(Send_string); // add characteristic

  customService.addCharacteristic(led_control); // add characteristic

  BLE.addService(customService);

 

 

  led_control.setValue(‘0’); // initial values 0x30 o 0 en Asccii

  

  // Events

  led_control.setEventHandler(BLEWritten,led_Update); // event to write

  BLE.setEventHandler(BLEConnected, onBLEConnected); // event to connect optional

  BLE.setEventHandler(BLEDisconnected, onBLEDisconnected); // event to disconned optional

   

  BLE.advertise();

  Serial.println(BLE.address());// Print the mac address

  pinMode(13,OUTPUT);

}

 

Inicializamos el Serial, el IMU y la conexión BLE.

Añadimos el nombre al bluetooth, activamos el profile y le añadimos las características que hemos defino anteriormente. Si uno de estos servicios necesita inicializarlo “led_control.setValue(‘0’)”.

El valor inicial va en función del tipo de característica definido, en nuestro caso un Char porque lo cambiaremos mediante la introducción de teclado y para no andar con conversiones de char a int lo dejamos como char.

Los eventos, “setEventHandler” definen funciones que se llaman cuando se da la condición propiamente descrita, en este caso cuando queramos escribir un char o cuando el bluetooth se conecte o se desconecte.

 

Funciones

void led_Update(BLEDevice central, BLECharacteristic characteristic) {

   char aux=led_control.value();

   if (aux=='1') digitalWrite(13,HIGH); // '1' o 0x31

   else  digitalWrite(13,LOW);

}

 

void onBLEConnected(BLEDevice central) {

  Serial.print("Connected event, central: ");

  Serial.println(central.address());

 }

 

void onBLEDisconnected(BLEDevice central) {

  Serial.print("Disconnected event, central: ");

  Serial.println(central.address()); // util to  know which mac address is your board

 }

  

 

 

Y por último tenemos el loop, donde gestionamos que hacer una vez nos conectemos a la central, en nuestro caso leeremos los valores de la IMU, los agruparemos en un string que enviaremos. Podríamos haber hecho un evento para la lectura de los valores, pero así podemos definir qué hacer.

Loop

void loop() {

  BLEDevice central = BLE.central();

  if (central) {

    while (central.connected()) {

      if (IMU.accelerationAvailable()&& IMU.gyroscopeAvailable() && IMU.magneticFieldAvailable()) {

        IMU.readAcceleration(Ax, Ay, Az);

        IMU.readGyroscope(Gx, Gy, Gz);

        IMU.readMagneticField(Mx, My, Mz);

        sprintf(msgprint, "%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,", Ax, Ay, Az, Gx, Gy, Gz, Mx,             My, Mz);

        Serial.println(msgprint);

        Send_string.writeValue(msgprint, sizeof(msgprint));  // guardamos en el servicio Send_string

         }

  }

}

 

Mientras estemos conectados imprimirá por el serial el valor de la IMU, y enviará estos valores al servicio.

Código completo.

 

El Ide de Arduino tarda en compilar y luego esta placa tiene otra particularidad, tiene dos puertos un para monitorear y otro para programar, pulsando 2 veces seguidas el reset entra en modo programar, selecciona ese puerto para enviar el programa. Una vez hecho tendrás que cambiar al otro puerto para mirar el serial.

 

Vamos al ordenador.

Con Python 3.7 he empleado la librería Bleak (“pip install bleak”) para comunicación con el bluetooth low energy y aiconsole (“pip install aioconsole” ), para introducir comandos en la consola

La lecturas que se van a realizar son asíncronas, y el programa se detendrá para esperar la instrucción del control del led, si se le envía ‘x’ romperá el bucle, se desconectara y saldrá del programa.

 

Inicio

 

import logging

import asyncio

import datetime

 

from aioconsole import ainput

from bleak import BleakClient, BleakError

 

read_string_uuid = "00001143-0000-1000-8000-00805f9b34fb" #servicio de lectura de string

led_control_uuid = "00001150-0000-1000-8000-00805f9b34fb" #servicio control del led

 

 

#definimos variable globales

Ax=0.0;Ay=0.0;Az=0.0;Gx=0.0;Gy=0.0;Gz=0.0;Mx=0.0;My=0.0;Mz=0.0

 

Definimos los servicios con el mismo  valor que los servicios del Arduino

 

 

Funcion principal

 

async with BleakClient(address, loop=loop) as client:

        while(1):

            x = await client.is_connected() #  Connect to address

            log.info("Connected: {0}".format(x))

            log.info("Starting notifications/indications...")

           

# Start receiving notifications, which are sent to the `data_handler method`

            await client.start_notify(indication_characteristic_uuid, data_handler)# read the string

           

            # wait for instruction to crontrol the led

            keyboard_input = await ainput("Control Led [0,1]: ")

            if keyboard_input== 'x':

                break

            bytes_to_send = bytearray(map(ord, keyboard_input))

            await client.write_gatt_char(led_control_uuid, bytes_to_send, response=True) # Sino es True no lo envia

 

            # Send a request to the peripheral to stop sending notifications.

            await client.stop_notify(indication_characteristic_uuid)

            log.info("Stopping notifications/indications...")

 

 

Una vez conectado, la data contiene 64 bytes, que es como hemos definido el buffer y en data_handler procesamos esos datos. Después el sistema se detiene a la espera de una entrada de teclado. Con un 1 leído como un char se enciende el led del Arduino, x cierra la aplicación y cualquier otra cosa apaga el led.

 

def data_handler

def data_handler(sender, data):

        global Ax,Ay,Az,Gx,Gy,Gz,Mx,My,Mz

        #log.info("{0}: {1}".format(sender, data))

        data_storage.append((datetime.datetime.utcnow().timestamp(), data))

        aux = bytearray(data)

        datos = stringdata = aux.decode('utf-8')

        msg = datos.split(",")

        Ax=float(msg[0])

        Ay=float(msg[1])

        Az=float(msg[2])

        Gx=float(msg[3])

        Gy=float(msg[4])

        Gz=float(msg[5])

        Mx=float(msg[6])

        My=float(msg[7])

        Mz=float(msg[8])

        print("Accelerometro [xyz]:"+str(Ax)+", "+str(Ay)+", "+str(Az))

        print("Gyroscope [xyz]:"+str(Gx)+", "+str(Gy)+", "+str(Gz))

        print("Magnetometre [xyz]:"+str(Mx)+", "+str(My)+", "+str(Mz))

 

 

Recibimos 64 bytes en un bytearray, salvo en MAC que hay que convertirlo a bytearry de 64 bytes, mediante un Split separamos por las comas y el valor msg[9] es descartable son todo cero. El resto lo convertimos a valores float en este caso. Y visualizamos con el print

 

 

Código python



Anakleto...

 

2/20/2020

Driver 4 Servomotores por i2c Remade


Saludos,

Edito: 21/6/2020

Pues no ha llovido ni nada. A las buenas, vengo a citarme yo mismo, el anterior post es incorrecto de ahí de añadirle el FAIL en grandote el motivo es que no funciona. Para empezar el caso de no funcione es que las salidas del PWM no van a 50 hz que es la premisa para que funcione un servo. Por ello y sin borrar el anterior porque creo que de los errores se aprende más de lo que uno cree. Voy a rehacer el post.

Continuando con mis prototipos, hoy os presento una placa para controlar 4 servomotores, vendría a substituir la placa que os mostré en la entrada anterior , pero con otro tipo de motores. Empleamos el PIC16F1509 el modelo superior del PIC16F1503, más memoria más pines más tamaño. y la oportunidad de emplear los 4 PWM para obtener un control de los servomotores.



Figura 1 placa del prototipo


Los servomotores funcionan a una frecuencia de 50 Hz , eso equivale a un ciclo de reloj de 20 ms, y el régimen de trabajo se desarrolla entre 1 y  2 ms. Este dato puede variar en función de los Servomotores que empleemos.  La placa dispone de un conector de alimentación extra para los servos, para los test y las pruebas he empleado la tensión propia del i2c y los servomotores DFR0399 un micrometal de 75:1 con control de servo. En este caso en vez de controlar la posición, controlamos la velocidad y la dirección.


Conexiones


Pic
Función
RC5
Servo01 à pwm1 en el pic
RC3
Servo02 àpwm2 en el pic
RB7
LED
RA0
I/O o ANALOGICA
RA1
I/O o ANALOGICA
RA2
Servo03 à pwm3 en el pic
RC1
Servo04 à pwm4 en el pic
RB4
SDA
RB6
SCL

Control de los ServoMotores


Al emplear el modelo PICF1509 podemos usar los cuatro PWM del pic para que nos generen un pulso con una frecuencia de 50Hz, o lo que es lo mismo 20 ms. Que el tiempo que necesitan la mayoría de los servos para funcionar.

Esto lo conseguimos:
  • Para conseguir este intervalo de tiempo en el Timer 2, es necesario poner el reloj interno del pic a 8Mhz en vez de los 16Mhz que permite.

setup_timer_2(T2_DIV_BY_16,250,10);      //2,0 ms overflow, 20,0 ms interrupt



Nota: Es incorrecto la explicación, para empezar vamos a explicar que es cada parte :

T2_DIV_BY_16 es el Preescaler en este caso 16 y puede ser 16,4,1.

PR2 o carga del  Timer2 es el segundo valor en este caso 250.


Postscale es el tercer valor en nuestro caso 10.

Si fuéramos a emplear una interrupción en el Timer2 se daría cada 20ms y tendría una frecuencia de 50HZ, pero para el PWM debemos fijarnos en el dato del overflow que nos indica que  es cada 2,0 ms eso es un frecuencia de 500H, y eso es lo que no va a dar las salidas de PWM.  Como conseguimos los 50HZ, luego lo explico.

  • .Vamos a hacer algunos números.


Nota: volvemos al mismo error anterior la frecuencia de salida es 500Hz no 50Hz, otra forma de detectar el error es que la resolución máxima que dispone el pic es de 10 bits es decir 1024.

Estos valores se adecuan mas a los 10 bits de resolución, pero nos dejan un margen de maniobra muy reducido 90 valores para determinar 180 grados. Dependiendo de la aplicación podría funcionarnos.


Pero la cuestión es que no es fácil que nos funcione, vamos a resolver el primer problema conseguir 50hz en las salidas. Para conseguirlo tenemos que poner el oscilador interno a funcionar a 500KHZ.

setup_timer_2(T2_DIV_BY_16,155,1);      //20,0 ms overflow, 20,0 ms interrupt

Y pensamos Bueno toda ira más lento, pero si funciona…

La comunicación I2C no va a esa velocidad a no ser que la bajes con lo que perjudicas al resto de sistemas. 

Controlar servos con PWM es una mala alternativa, el control se simplifica, pero te quedas sin proceso.

Entonces como controlamos 4 servos con un pic mediante I2C


Este es un tema que llevo mucho tiempo plantándome y que no hallaba fácil solución, utilizando una interrupción en timer2 a 20ms.

setup_timer_2(T2_DIV_BY_16,250,10);      //2,0 ms overflow, 20,0 ms interrupt


1 servo, pan comido
#INT_TIMER2
void  TIMER2_isr(void) {
  output_high(PIN_C3);
  delay_us(pulse_time);
  output_low(PIN_C3);
}

2 servos, no hay problema, miramos quien tiene el pulso mas corto.
#INT_TIMER2
void  TIMER2_isr(void){
   output_high(PIN_C3);
   output_high(PIN_C5);
   if(pr1>pr2){
      delay_us(pr2);
      output_low(PIN_C5);
      delay_us(pr1-pr2);
      output_low(PIN_C3);
   }else if (pr1<pr2){
      delay_us(pr1);
      output_low(PIN_C3);
      delay_us(pr2-pr1);
      output_low(PIN_C5);
   }else{
   delay_us(pr1);
   output_low(PIN_C3);
   output_low(PIN_C5);
   }
}
3 servos ya se complican un poco mediante este sistema, el tiempo de ordenar cual va antes puede hacerse mas alto que la misma interrupción. Pero se podría hacer…




Con cuatro ya lo tengo probado que no va, entonces como hacerlo. Pues de una manera absurdamente simple. Gracias por abrirme los ojos.







Si tengo 20 ms de interrupción y 4 servos, porque no reservo 5 ms de la interrupción para cada servo. A efectos practico son 4 señales de 50Hz desfasada 5 ms la una de la otra pero en cada ciclo. Si me apuras y marcas un tiempo de 2,5 valido para casi la mayoría de servos, se podría controlar 8 servos diferentes.El código:



#INT_TIMER2
void  TIMER2_isr(void) {
   // cada 3 ms actua un servo empezamos por RC5
   output_high(PIN_C5);
   delay_us(servo[0]);
   output_low(PIN_C5);
   delay_us(3000-servo[0]);
   /////////////////////////
   output_high(PIN_C3);
   delay_us(servo[1]);
   output_low(PIN_C3);
   delay_us(3000-servo[1]);
   /////////////////////////
   output_high(PIN_C1);
   delay_us(servo[2]);
   output_low(PIN_C1);
   delay_us(3000-servo[2]);
   //////////////////////////
   output_high(PIN_A2);
   delay_us(servo[3]);
   output_low(PIN_A2);
  
}


Las duraciones del pulso se guardan en un vector int16_t servo[4]={1500,1500,1500,1500};
Se podría enviar la posición en un int16_t  y tener un control total y no un valor de conversión pero no todo el mundo lleva bien lo de trabajar con microsegundos.

Para el nuevo control los valores max=2200, min=800 y cero =1500.

 

Modo individual


El control se simplifica mucho. Convertimos en valor de ángulo en su equivalente para el ancho del pulso del pwm.



  •         Posición del servomotor àservo[0..3], ángulo[0..180]
  •         Posición del servomotor con delay predefinido àservo[0..3], ángulo[0..180]
  •         Posición del servomotor con delay àservo[0..3], ángulo[0..180], delay(int16)

 Ejemplo de código  control de cada servo individual

int16_t servo[4];

void servo_individual(int nServo, int angle)
{
  servo[nServo]=angle;
}

Modo Coordinado


En este modo va en función de la aplicación que vamos a realizar. Si empleamos en ejemplo de los motores, el control de un vehículo con tracción a las cuatro ruedas. En este sentido el ángulo se interpreta como velocidad.

0°
90°
180°
Máxima velocidad ßß
Stop
Máxima velocidad àà


Dirección àForward, Backward, Lef, Right, Turn_left, Turn_right, Stop


·        Modo coordinado        
o   Instrucción àPalabra de control, ángulo (0..180)
§  0001 xxxx à Stop
§  0000 0011 à Forward
§  0000 1100 à Backward
§  0000 1010 à Left
§  0000 0101 à Right
§  0000 1111 à Turn_left
§  0000 0000 à Turn_right
o   Instrucción con delay predefinido à Palabra de control, ángulo (0..180)
o   Instrucción con delayà Palabra de control, ángulo (0..180),delay(int16)

Ejemplo de función en Modo coordinado

void servomotor_con_all( int control,int angle)
{
   if(!bit_test(control,4))  //non STOP
   {
      if(!bit_test(control,3)) servo[0]=angle;     
      else  servo[0]=max-angle;
      if(!bit_test(control,2)) servo[1]=angle;   
      else servo[1]=max-angle;
      if(!bit_test(control,1)) servo[2]=angle;    
      else servo[2]=max-angle;
      if(!bit_test(control,0)) servo[3]=angle;    
      else servo[3]=max-angle;
      
   }else   stop_all();  

}



El control mediante PWM simplifica mucho el tipo de control, tanto el individual como el coordinado.

Instrucciones I2c para controlar el driver


cmd
Datos
Función
0x01
--
Led on
0x02
--
Led off
0x10
ServoMotor, ángulo
control motor individual
0x11
ServoMotor,ángulo
control motor individual con delay por defecto
0x12
ServoMotor,ángulo,delayH,delayL
control motor individual con delay enviado
0x15
Control,ángulo
Control Servomotores
0x16
Control,ángulo
Control Servomotores con delay por defecto
0x17
Control,ángulo, delayH,delayL
Control Servomotores con delay enviado
0x20
delayH,delayL
Cambio delay
0x30
--
Leer la entrada analógica A0
0x31
--
Leer la entrada analógica A1
0x32
--
Leer variable de control 4660



Esta placa esta interesante pues ofrece poder controlar diferentes servos sin tener que saturar a la cpu, con varias placas de este tipo conectadas a un máster se podría controlar un hexapodo mediante mensajes de i2c. Mi obsesión por este protocolo es alta.


Anakleto.