Unidad 2. Sensores y actuadores inteligentes

Introducción

En esta unidad vamos a estudiar dos protocolos de comunicación que permiten integrar sensores y actuadores más sofisticados a las aplicaciones: I2C y SPI.

Propósito de aprendizaje

Crear aplicaciones interactivas de tiempo real que integren sensores y actuadores mediante protocolos de comunicación I2C y SPI.

Temas

  • Protocolo I2C.
  • Protocolo SPI.

Trayecto de actividades

Ejercicio 1

Observa el siguiente video donde verás una introducción a las comunicaciones seriales, por I2C y por SPI.

Ejercicio 2

En este vas a poder profundizar un poco más sobre SPI.

Ejercicio 3

En base al material que leíste responde estas preguntas sobre el bus SPI:

  • ¿Cómo se conectan físicamente dos dispositivos?
  • ¿Qué debo hacer para conectar físicamente más de un sensor/actuador a un controlador?
  • ¿Qué se envía primero, el byte de mayor peso o el de menor peso?
  • ¿La señal de reloj descansa en alto o en bajo?
  • ¿Los datos se muestrean en el flanco de subida o en el flanco de bajada del clock?
  • ¿A qué velocidad se comunican dos dispositivos?
  • ¿Debe existir algún retardo entre transmisiones?

Ejercicio 4

Observa el siguiente fragmento de código típico al utilizar el framework de arduino:

1
2
 SPI.transfer(0xCD);
 byte x = SPI.transfer (0x00);

El valor almacenado en x corresponde a la transferencia 0xCD o 0x00?

Ejercicio 5

Ahora que ya entiendes cómo funciona el SPI, vamos a utilizar un sensor llamado el BME280 que puedes comprar aquí El BME280 es un sensor ambiental que permite medir humedad relativa, presión y temperatura. Como controlador, vamos a utilizar el ESP32 y el framework de arduino.

Ten presente este material de referencia:

La siguiente figura te muestra un diagrama del sensor:

../_images/BME280Pinout.jpeg

Las señales tienen la siguiente función:

  • VCC: alimentación a 3.3V.
  • GND: 0V.
  • SCL: Clock SPI.
  • SDA: MOSI SPI.
  • CSB: CS o SS (Chip Select) SPI.
  • SDO: MISO SPI.

Los puertos del controlador los verás aquí

Para conectar el sensor con el controlador se procede así:

DevKit32 BME280 SPI
3V3 VCC
GND GND
SCK/18 SCL CLOCK
MOSI/23 SDA MOSI
SS/5/LED CSB SS
MISO/19 SDO MISO

Ejercicio 6

En cuanto al software, necesitarás añadir las siguientes bibliotecas:

Ahora viene lo bueno. Vamos a probar que todo está bien conectado y que tienes las bibliotecas instaladas. Abre uno de los ejemplos de la biblioteca Adafruit BME280 llamado BME280test.ino.

Realiza las siguiente modificaciones:

Comenta el archivo de cabeceras Wire.h. Este archivo corresponde al API I2C (este sensor soporta los dos protocolos, pero por ahora estamos con el SPI).

Modificar el pinout del SPI:

24
25
26
27
28
 #include <SPI.h>
 #define BME_SCK 18
 #define BME_MISO 19
 #define BME_MOSI 23
 #define BME_CS 5

Comenta la línea que declara el objeto I2C y descomenta la correspondiente a SPI:

33
34
35
 //Adafruit_BME280 bme; // I2C
 Adafruit_BME280 bme(BME_CS); // hardware SPI
 //Adafruit_BME280 bme(BME_CS, BME_MOSI, BME_MISO, BME_SCK); // software SPI

Observa como queda el código completo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
 /***************************************************************************
 This is a library for the BME280 humidity, temperature & pressure sensor

 Designed specifically to work with the Adafruit BME280 Breakout
 ----> http://www.adafruit.com/products/2650

 These sensors use I2C or SPI to communicate, 2 or 4 pins are required
 to interface. The device's I2C address is either 0x76 or 0x77.

 Adafruit invests time and resources providing this open source code,
 please support Adafruit andopen-source hardware by purchasing products
 from Adafruit!

 Written by Limor Fried & Kevin Townsend for Adafruit Industries.
 BSD license, all text above must be included in any redistribution
 ***************************************************************************/

 //#include <Wire.h>

 #include <Adafruit_Sensor.h>
 #include <Adafruit_BME280.h>


 #include <SPI.h>
 #define BME_SCK 18
 #define BME_MISO 19
 #define BME_MOSI 23
 #define BME_CS 5


 #define SEALEVELPRESSURE_HPA (1013.25)

 //Adafruit_BME280 bme; // I2C
 Adafruit_BME280 bme(BME_CS); // hardware SPI
 //Adafruit_BME280 bme(BME_CS, BME_MOSI, BME_MISO, BME_SCK); // software SPI

 unsigned long delayTime;

 void setup() {
 Serial.begin(9600);
 Serial.println(F("BME280 test"));

 bool status;

 // default settings
 // (you can also pass in a Wire library object like &Wire2)
 //status = bme.begin(0x76);ç
 status = bme.begin();
 if (!status) {
     Serial.println("Could not find a valid BME280 sensor, check wiring!");
     while (1);
 }

 Serial.println("-- Default Test --");
 delayTime = 1000;

 Serial.println();
 }


 void loop() {
 printValues();
 delay(delayTime);
 }


 void printValues() {
 Serial.print("Temperature = ");
 Serial.print(bme.readTemperature());
 Serial.println(" *C");

 Serial.print("Pressure = ");

 Serial.print(bme.readPressure() / 100.0F);
 Serial.println(" hPa");

 Serial.print("Approx. Altitude = ");
 Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA));
 Serial.println(" m");

 Serial.print("Humidity = ");
 Serial.print(bme.readHumidity());
 Serial.println(" %");

 Serial.println();
 }

Una vez ejecutes el código este será el resultado:

Temperature = 25.44 *C
Pressure = 850.51 hPa
Approx. Altitude = 1452.61 m
Humidity = 51.67 %S

Temperature = 25.43 *C
Pressure = 850.43 hPa
Approx. Altitude = 1453.42 m
Humidity = 51.67 %

Temperature = 25.43 *C
Pressure = 850.47 hPa
Approx. Altitude = 1453.03 m
Humidity = 51.67 %

La temperatura se reporta como un número en punto flotante en grados centígrados. La presión se reporta como un número en punto flotante en Pascales. Nota que el valor de presión se divide por el literal 100.0F (constante en punto flotante) para convertir a hecto Pascales el resultado. Para el cálculo de la altitud aproximada, es necesario pasar la presión sobre el nivel del mar de la ciudad al día y hora de la prueba en unidades de hecto Pascales. Finalmente, se reporta la humedad relativa en punto flotante.

Ejercicio 7

Ahora te recomiendo que analices a fondo el código fuente de la biblioteca. De hecho, este es uno de los mejores ejercicios para aprender a programar.

El código fuente lo encuentras aquí

Piensa en estas preguntas:

  • Analiza el código del constructor de la clase. ¿Qué estrategia utilizan para diferenciar el SPI por hardware al SPI por software?
  • ¿En qué parte del código se inicializa el objeto SPI?
  • Haciendo la lectura del código fuente, ¿Qué bit se envía primero, el de mayor peso o el de menor peso?
  • ¿Cuál modo de SPI utiliza el sensor?
  • ¿Cuál es la velocidad de comunicación?
  • El sensor soporta dos modos SPI. Leyendo la información en la hoja de datos, cómo sería posible configurar el modo?
  • ¿Cómo es el protocolo para escribir información en el sensor?
  • ¿Cómo es el protocolo para leer información del sensor?
  • Busque en el código fuente de la biblioteca, ¿Dónde se lee el chip-ID del sensor BME280?
  • Muestra y explica detalladamente los pasos y el código para identificar el chip-ID. No olvide apoyarse de la hoja de datos
  • ¿Qué otros pasos se requieren para inicializar el sensor?

Ejercicio 8

¿Qué hacer si quieres transmitir información del sensor a una plataforma interactiva? Deberás decidir qué tipo de protocolo vas a usar: ASCII o binario.

¿Repasamos un poco?

Para transmitir información de variables usando un protocolo binario necesitas obtener los bytes que componen una variable.

¿Cómo conseguir cada uno de los bytes que componen la variable?

Considera este código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void setup() {
  Serial.begin(115200);

}

void loop() {
  static uint16_t x = 0;

  if (Serial.available()) {
    if (Serial.read() == 0x73) {
      Serial.write((uint8_t)( x & 0x00FF ));
      Serial.write( (uint8_t)(x >> 8 ));
    }
  }
}

Nota cómo la operación (x >> 8 ) permite conseguir el byte de mayor peso del entero no signado de 16 bits x.

Abre el programa ScriptCommunicator e interactúa con la aplicación anterior:

  • ¿Qué debo hacer para que el ESP32 me responda?
  • ¿Qué significan los datos que estoy recibiendo?
  • Ahora intentemos la misma técnica para conseguir los bytes de un número en punto flotante.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void setup() {
    Serial.begin(115200);
}

void loop() {
    float num = 1.1;

    if (Serial.available()) {
        if (Serial.read() == 0x73) {
            Serial.write((uint8_t)( num ));
            Serial.write( (uint8_t)(num >> 8 ));
            Serial.write( (uint8_t)(num >> 16 ));
            Serial.write( (uint8_t)(num >> 32 ));
        }
    }
}

El 1.1 en punto flotante será el 3f 8c cc cd

  • ¿Pudiste compilar el programa?

Nota que al intentar compilar, el compilador te dirá que no es posible aplicar el operador >> al tipo float.

  • Debemos entonces aplicar una técnica diferente para obtener los bytes del float:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void setup() {
    Serial.begin(115200);
}

void loop() {
    // 45 60 55 d5
    // https://www.h-schmidt.net/FloatConverter/IEEE754.html
    static float num = 3589.3645;

    static uint8_t arr[4] = {0};

    if(Serial.available()){
        if(Serial.read() == 0x73){
            memcpy(arr,(uint8_t *)&num,4);
            Serial.write(arr,4);
        }
    }
}

En este caso estamos guardando los 4 bytes que componen el float en un arreglo, arr, para luego transmitir dicho arreglo.

  • ¿En qué orden estamos transmitiendo los bytes, en bigEndian o en littleEndian?

Ejercicio 9

Ahora si, vamos a conectarnos a Unity, pero aún sin sensor. Vamos paso a paso. Simularemos el sensor con un programa de prueba en el ESP32.

Nuestro sensor simulado enviará tres números de 16 bits sin signo que modificarán la escala x, y, z de un GameObject en Unity. Este envío solo lo realizará cuando Unity solicite datos.

Unity solicitará los datos enviando el byte 0x73 y recibirá 6 bytes (2 por cada número) en little endian con los valores de la escala.

Implementa el siguiente código y analiza parte por parte:

El código del ESP32 simulando el sensor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void setup() {
  Serial.begin(115200);

}

void loop() {
  static uint16_t x = 0;
  static uint16_t y = 0;
  static uint16_t z = 0;
  static bool countUp = true;


  if (Serial.available()) {
    if (Serial.read() == 0x73) {
      Serial.write((uint8_t)( x & 0x00FF));
      Serial.write( (uint8_t)( x >> 8 ));
      Serial.write((uint8_t)( y & 0x00FF ));
      Serial.write((uint8_t)(y >> 8 ));
      Serial.write((uint8_t)( z & 0x00FF ));
      Serial.write((uint8_t)(z >> 8 ));

      if (countUp == true) {
        if (x < 1000) {
          x = x + 1;
          y = y + 1;
          z = z + 1;
        }
        else countUp = false;
      }

      if (countUp == false)
      {
        if (x > 0) {
          x = x - 1;
          y = y - 1;
          z = z - 1;
        }
        else countUp = true;
      }
    }
  }
}

Prueba este código con ScriptCommunicator antes de continuar.

El código de Unity tendrá la misma arquitectura de Ardity: un controlador, la implementación del protocolo y la aplicación como tal.

El código para el protocolo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO.Ports;

using System.Text;

public class Protocol : AbstractSerialThread
{
    // Buffer where a single message must fit
    private byte[] buffer = new byte[1024];
    private int bufferUsed = 0;

    public Protocol(string portName,
                                      int baudRate,
                                      int delayBeforeReconnecting,
                                      int maxUnreadMessages)
        : base(portName, baudRate, delayBeforeReconnecting, maxUnreadMessages, false)
    {

    }

    protected override void SendToWire(object message, SerialPort serialPort)
    {
        byte[] binaryMessage = (byte[])message;
        serialPort.Write(binaryMessage, 0, binaryMessage.Length);
    }

    protected override object ReadFromWire(SerialPort serialPort)
    {
        if(serialPort.BytesToRead >= 6)
        {

            bufferUsed = serialPort.Read(buffer, 0, 6);
            byte[] returnBuffer = new byte[bufferUsed];
            System.Array.Copy(buffer, returnBuffer, bufferUsed);
/*
            StringBuilder sb = new StringBuilder();
            sb.Append("Packet: ");
            foreach (byte data in buffer)
            {
                sb.Append(data.ToString("X2") + " ");
            }
            sb.Append("Checksum fails");
            Debug.Log(sb);
*/

            return returnBuffer;
        }
        else
        {
            return null;
        }
    }

}

El código del controlador:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


using System.Threading;

public class Controller : MonoBehaviour
{
    [Tooltip("Port name with which the SerialPort object will be created.")]
    public string portName = "/dev/ttyUSB0";

    [Tooltip("Baud rate that the serial device is using to transmit data.")]
    public int baudRate = 57600;

    [Tooltip("Reference to an scene object that will receive the events of connection, " +
            "disconnection and the messages from the serial device.")]
    public GameObject messageListener;

    [Tooltip("After an error in the serial communication, or an unsuccessful " +
            "connect, how many milliseconds we should wait.")]
    public int reconnectionDelay = 1000;

    [Tooltip("Maximum number of unread data messages in the queue. " +
            "New messages will be discarded.")]
    public int maxUnreadMessages = 1;

    // Internal reference to the Thread and the object that runs in it.
    protected Thread thread;
    protected Protocol serialThread;


    // ------------------------------------------------------------------------
    // Invoked whenever the SerialController gameobject is activated.
    // It creates a new thread that tries to connect to the serial device
    // and start reading from it.
    // ------------------------------------------------------------------------
    void OnEnable()
    {
        serialThread = new Protocol(portName,
                                                      baudRate,
                                                      reconnectionDelay,
                                                      maxUnreadMessages);
        thread = new Thread(new ThreadStart(serialThread.RunForever));
        thread.Start();
    }

    // ------------------------------------------------------------------------
    // Invoked whenever the SerialController gameobject is deactivated.
    // It stops and destroys the thread that was reading from the serial device.
    // ------------------------------------------------------------------------
    void OnDisable()
    {
        // If there is a user-defined tear-down function, execute it before
        // closing the underlying COM port.
        if (userDefinedTearDownFunction != null)
            userDefinedTearDownFunction();

        // The serialThread reference should never be null at this point,
        // unless an Exception happened in the OnEnable(), in which case I've
        // no idea what face Unity will make.
        if (serialThread != null)
        {
            serialThread.RequestStop();
            serialThread = null;
        }

        // This reference shouldn't be null at this point anyway.
        if (thread != null)
        {
            thread.Join();
            thread = null;
        }
    }

    // ------------------------------------------------------------------------
    // Polls messages from the queue that the SerialThread object keeps. Once a
    // message has been polled it is removed from the queue. There are some
    // special messages that mark the start/end of the communication with the
    // device.
    // ------------------------------------------------------------------------
    void Update()
    {
        // If the user prefers to poll the messages instead of receiving them
        // via SendMessage, then the message listener should be null.
        if (messageListener == null)
            return;

        // Read the next message from the queue
        byte[] message = ReadSerialMessage();
        if (message == null)
            return;

        // Check if the message is plain data or a connect/disconnect event.
        messageListener.SendMessage("OnMessageArrived", message);
    }

    // ------------------------------------------------------------------------
    // Returns a new unread message from the serial device. You only need to
    // call this if you don't provide a message listener.
    // ------------------------------------------------------------------------
    public byte[] ReadSerialMessage()
    {
        // Read the next message from the queue
        return (byte[]) serialThread.ReadMessage();
    }

    // ------------------------------------------------------------------------
    // Puts a message in the outgoing queue. The thread object will send the
    // message to the serial device when it considers it's appropriate.
    // ------------------------------------------------------------------------
    public void SendSerialMessage(byte[] message)
    {
        serialThread.SendMessage(message);
    }

    // ------------------------------------------------------------------------
    // Executes a user-defined function before Unity closes the COM port, so
    // the user can send some tear-down message to the hardware reliably.
    // ------------------------------------------------------------------------
    public delegate void TearDownFunction();
    private TearDownFunction userDefinedTearDownFunction;
    public void SetTearDownFunction(TearDownFunction userFunction)
    {
        this.userDefinedTearDownFunction = userFunction;
    }

}

El código de la clase AbstractSerialThread

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
/**
* Ardity (Serial Communication for Arduino + Unity)
* Author: Daniel Wilches <dwilches@gmail.com>
*
* This work is released under the Creative Commons Attributions license.
* https://creativecommons.org/licenses/by/2.0/
*/

using UnityEngine;

using System;
using System.IO;
using System.IO.Ports;
using System.Collections;
using System.Threading;

/**
* This class contains methods that must be run from inside a thread and others
* that must be invoked from Unity. Both types of methods are clearly marked in
* the code, although you, the final user of this library, don't need to even
* open this file unless you are introducing incompatibilities for upcoming
* versions.
*/
public abstract class AbstractSerialThread
{
    // Parameters passed from SerialController, used for connecting to the
    // serial device as explained in the SerialController documentation.
    private string portName;
    private int baudRate;
    private int delayBeforeReconnecting;
    private int maxUnreadMessages;

    // Object from the .Net framework used to communicate with serial devices.
    private SerialPort serialPort;

    // Amount of milliseconds alloted to a single read or connect. An
    // exception is thrown when such operations take more than this time
    // to complete.
    private const int readTimeout = 100;

    // Amount of milliseconds alloted to a single write. An exception is thrown
    // when such operations take more than this time to complete.
    private const int writeTimeout = 100;

    // Internal synchronized queues used to send and receive messages from the
    // serial device. They serve as the point of communication between the
    // Unity thread and the SerialComm thread.
    private Queue inputQueue, outputQueue;

    // Indicates when this thread should stop executing. When SerialController
    // invokes 'RequestStop()' this variable is set.
    private bool stopRequested = false;

    private bool enqueueStatusMessages = false;


    /**************************************************************************
    * Methods intended to be invoked from the Unity thread.
    *************************************************************************/

    // ------------------------------------------------------------------------
    // Constructs the thread object. This object is not a thread actually, but
    // its method 'RunForever' can later be used to create a real Thread.
    // ------------------------------------------------------------------------
    public AbstractSerialThread(string portName,
                                int baudRate,
                                int delayBeforeReconnecting,
                                int maxUnreadMessages,
                                bool enqueueStatusMessages)
    {
        this.portName = portName;
        this.baudRate = baudRate;
        this.delayBeforeReconnecting = delayBeforeReconnecting;
        this.maxUnreadMessages = maxUnreadMessages;
        this.enqueueStatusMessages = enqueueStatusMessages;

        inputQueue = Queue.Synchronized(new Queue());
        outputQueue = Queue.Synchronized(new Queue());
    }

    // ------------------------------------------------------------------------
    // Invoked to indicate to this thread object that it should stop.
    // ------------------------------------------------------------------------
    public void RequestStop()
    {
        lock (this)
        {
            stopRequested = true;
        }
    }

    // ------------------------------------------------------------------------
    // Polls the internal message queue returning the next available message
    // in a generic form. This can be invoked by subclasses to change the
    // type of the returned object.
    // It returns null if no message has arrived since the latest invocation.
    // ------------------------------------------------------------------------
    public object ReadMessage()
    {
        if (inputQueue.Count == 0)
            return null;

        return inputQueue.Dequeue();
    }

    // ------------------------------------------------------------------------
    // Schedules a message to be sent. It writes the message to the
    // output queue, later the method 'RunOnce' reads this queue and sends
    // the message to the serial device.
    // ------------------------------------------------------------------------
    public void SendMessage(object message)
    {
        outputQueue.Enqueue(message);
    }


    /**************************************************************************
    * Methods intended to be invoked from the SerialComm thread (the one
    * created by the SerialController).
    *************************************************************************/

    // ------------------------------------------------------------------------
    // Enters an almost infinite loop of attempting connection to the serial
    // device, reading messages and sending messages. This loop can be stopped
    // by invoking 'RequestStop'.
    // ------------------------------------------------------------------------
    public void RunForever()
    {
        // This 'try' is for having a log message in case of an unexpected
        // exception.
        try
        {
            while (!IsStopRequested())
            {
                try
                {
                    AttemptConnection();

                    // Enter the semi-infinite loop of reading/writing to the
                    // device.
                    while (!IsStopRequested())
                        RunOnce();
                }
                catch (Exception ioe)
                {
                    // A disconnection happened, or there was a problem
                    // reading/writing to the device. Log the detailed message
                    // to the console and notify the listener.
                    Debug.LogWarning("Exception: " + ioe.Message + " StackTrace: " + ioe.StackTrace);
                    if (enqueueStatusMessages)
                        inputQueue.Enqueue("__Disconnected__");

                    // As I don't know in which stage the SerialPort threw the
                    // exception I call this method that is very safe in
                    // disregard of the port's status
                    CloseDevice();

                    // Don't attempt to reconnect just yet, wait some
                    // user-defined time. It is OK to sleep here as this is not
                    // Unity's thread, this doesn't affect frame-rate
                    // throughput.
                    Thread.Sleep(delayBeforeReconnecting);
                }
            }

            // Before closing the COM port, give the opportunity for all messages
            // from the output queue to reach the other endpoint.
            while (outputQueue.Count != 0)
            {
                SendToWire(outputQueue.Dequeue(), serialPort);
            }

            // Attempt to do a final cleanup. This method doesn't fail even if
            // the port is in an invalid status.
            CloseDevice();
        }
        catch (Exception e)
        {
            Debug.LogError("Unknown exception: " + e.Message + " " + e.StackTrace);
        }
    }

    // ------------------------------------------------------------------------
    // Try to connect to the serial device. May throw IO exceptions.
    // ------------------------------------------------------------------------
    private void AttemptConnection()
    {

        Debug.Log("Openening the serial port");
        serialPort = new SerialPort(portName, baudRate);
        serialPort.ReadTimeout = readTimeout;
        serialPort.WriteTimeout = writeTimeout;
        serialPort.DtrEnable = true;
        serialPort.Open();

        if (enqueueStatusMessages)
            inputQueue.Enqueue("__Connected__");
    }

    // ------------------------------------------------------------------------
    // Release any resource used, and don't fail in the attempt.
    // ------------------------------------------------------------------------
    private void CloseDevice()
    {
        if (serialPort == null)
            return;

        try
        {
            serialPort.Close();
        }
        catch (IOException)
        {
            // Nothing to do, not a big deal, don't try to cleanup any further.
        }

        serialPort = null;
    }

    // ------------------------------------------------------------------------
    // Just checks if 'RequestStop()' has already been called in this object.
    // ------------------------------------------------------------------------
    private bool IsStopRequested()
    {
        lock (this)
        {
            return stopRequested;
        }
    }

    // ------------------------------------------------------------------------
    // A single iteration of the semi-infinite loop. Attempt to read/write to
    // the serial device. If there are more lines in the queue than we may have
    // at a given time, then the newly read lines will be discarded. This is a
    // protection mechanism when the port is faster than the Unity progeram.
    // If not, we may run out of memory if the queue really fills.
    // ------------------------------------------------------------------------
    private void RunOnce()
    {
        try
        {
            // Send a message.
            if (outputQueue.Count != 0)
            {
                SendToWire(outputQueue.Dequeue(), serialPort);
            }

            // Read a message.
            // If a line was read, and we have not filled our queue, enqueue
            // this line so it eventually reaches the Message Listener.
            // Otherwise, discard the line.
            object inputMessage = ReadFromWire(serialPort);
            if (inputMessage != null)
            {
                if (inputQueue.Count < maxUnreadMessages)
                {
                    inputQueue.Enqueue(inputMessage);
                }
                else
                {
                    Debug.LogWarning("Queue is full. Dropping message: " + inputMessage);
                }
            }
        }
        catch (TimeoutException)
        {
            // This is normal, not everytime we have a report from the serial device
        }
    }

    // ------------------------------------------------------------------------
    // Sends a message through the serialPort.
    // ------------------------------------------------------------------------
    protected abstract void SendToWire(object message, SerialPort serialPort);

    // ------------------------------------------------------------------------
    // Reads and returns a message from the serial port.
    // ------------------------------------------------------------------------
    protected abstract object ReadFromWire(SerialPort serialPort);
}

Finalmente el código de la aplicación

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Text;

public class App : MonoBehaviour
{
    public Controller serialController;
    private float timer = 0.0f;
    private float waitTime = 0.005f;

    private Transform objTransform;
    private Vector3 scaleChange;


    // Initialization
    void Start()
    {
        serialController = GameObject.Find("SerialController").GetComponent<Controller>();
        objTransform = GetComponent<Transform>();
        scaleChange = new Vector3(0f, 0f, 0f);
    }

    // Executed each frame
    void Update()
    {

      //---------------------------------------------------------------------
        // Send data
        //---------------------------------------------------------------------
        if (Input.GetKeyUp(KeyCode.Q))
        {
            //Debug.Log("Get data 0x73 ");
            serialController.SendSerialMessage(new byte[] { 0x73});
        }

        timer += Time.deltaTime;
        if (timer > waitTime)
        {
            timer = timer - waitTime;

            serialController.SendSerialMessage(new byte[] { 0x73});
        }



        //---------------------------------------------------------------------
        // Receive data
        //---------------------------------------------------------------------

        byte[] message = serialController.ReadSerialMessage();

        if (message == null)
            return;

        float x = ((float)System.BitConverter.ToUInt16(message, 0) ) / 500F;
        float y = ((float)System.BitConverter.ToUInt16(message, 2) ) / 500F;
        float z = ((float)System.BitConverter.ToUInt16(message, 4) ) / 500F;
        scaleChange.Set(x,y,z);

        objTransform.localScale =  scaleChange;

/*         StringBuilder sb = new StringBuilder();
        sb.Append("Packet: ");
        foreach (byte data in message)
        {
            sb.Append(data.ToString("X2") + " ");
        }
        Debug.Log(sb); */
    }
}

La configuración del proyecto queda como se muestra en la figura:

proyecto en Unity

Ejercicio 10

Ahora le toca el turno a un sensor I2C. Observa de nuevo este video. Luego profundiza un poco más aquí.

Ejercicio 11

En base al material que leíste responde estas preguntas sobre el bus I2C:

  • ¿Cómo se conectan físicamente dos dispositivos?
  • ¿Qué debo hacer para conectar físicamente más de un sensor/actuador a un controlador?
  • ¿Cómo sabemos si tenemos un dispositivo particular en el bus?
  • ¿Para qué sirven las resistencias de pullup?
  • ¿Cómo se transmite un CERO en I2C?
  • ¿Cómo se transmite un UNO en I2C?
  • ¿Puede un esclavo enviar datos sin que un maestro lo solicite?
  • ¿Puedo tener dos sensores iguales en el mismo bus I2C?
  • ¿Qué y cómo es un ACK en I2C?
  • ¿Qué y cómo es un NACK en I2C?

Ejercicio 12

¿Qué debe hacer un maestro para acceder un esclavo?

  • La transferencia de datos únicamente es posible cuando el bus esté IDLE (SDA y SCL están en alto luego de una condición de STOP).
  • Si el maestro quiere enviar datos al esclavo:
    • El maestro-tx envía la condición de START y direcciona al esclavo-rx
    • El maestro-tx envía datos al esclavo-rx
    • El maestro-tx termina la comunicación enviando la condición de STOP.
  • Si el maestro quiere recibir datos del esclavo:
    • El maestro-rx envía la condición de START y direcciona al esclavo-tx
    • El maestro-rx envía una petición del registro que desea leer del esclavo-tx
    • El maestro-rx recibe los datos del esclavo-tx
    • El maestro-rx termina la transferencia con una condición de STOP.

Ejercicio 13

¿Qué cosas pueden generar un NACK?

  • El receptor genera un NACK porque no puede enviar o transmitir información porque no está listo. Posiblemente esté procesando algo.
  • El receptor no entendió el byte que le transmitieron
  • El receptor no puede recibir más datos.
  • Un Master-rx le indica a un esclavo que ya terminó de recibir datos mediante un NACK.

Ejercicio 14

Considera estas dos figuras:

condición de arranque y parada transmisión de un byte

Al transferir datos ¿Por qué la señal de SDA debe estar estable mientras SCL está en alto?

Ejercicio 14

En la siguiente figura podrás ver un ejemplo de un Nack:

nack

¿Cómo sería en este caso un ACK?

Ejercicio 15

Observa la siguiente figura:

un maestro escribe un esclavo

En este caso vemos que un maestro está escribiendo un esclavo.

Analicemos cada uno de los pasos que observamos en el diagrama:

  1. El maestro direcciona al esclavo. Como el esclavo si está presente responde con un ACK. Adicionalmente el maestro le está indicando al esclavo que va a escribir un dato (R/W)
  2. El maestro envía un dato y el esclavo responde con un ACK. El dato enviado corresponde a la dirección de un registro interno del esclavo que el maestro quiere escribir.
  3. Finalmente, el maestro envía el datos a escribir en el registro. El esclavo responde con ACK.
  4. El maestro envía la condición de parada.

Ejercicio 16

Observa la siguiente figura:

un maestro lee datos de un esclavo

En este caso vemos que un maestro está leyendo dato del esclavo.

Analicemos cada uno de los pasos que observamos en el diagrama:

  1. El maestro direcciona al esclavo. Como el esclavo si está en el bus responde con ACK. El maestro indica además que va a escribir al esclavo. ¿QUÉ? ¿No lo iba a leer pues? Si, lo que desea hacer el maestro es leer un REGISTRO interno del esclavo, pero primero debe decirle al esclavo qué registro va a leer.
  2. El maestro escribe la dirección del registro a leer y el esclavo envía un ACK.
  3. El maestro envía de nuevo una condición de ARRANQUE, pero al no enviar previamente la condición de parada se marca como un SR o repeated START. El maestro hace esto para indicarle al esclavo que ahora lo va a leer. Nota que el esclavo manda el ACK.
  4. Luego el esclavo devuelve el dato almacenado en el registro que el maestro deseaba leer.
  5. El maestro le responde al esclavo con un NACK y luego una condición de parada indicando de esta manera que ya tiene el dato y que se termina la transacción en el bus.

Ejercicio 17: RETO

Primero vamos a conectar:

Ahora vamos a practicar todo lo anterior conectando a un controlador un sensor I2C. En este caso será este reloj de tiempo real.

Aquí está la hoja de datos del dispositivo.

La biblioteca de arduino es esta.

Los planos del sensor está aquí

Para conectar el sensor al ESP32 necesitarás ser muy cuidados con los voltajes de alimentación del sensor. El sensor funciona a 5 voltios y el ESP32 a 3.3 voltios. Afortunadamente, el sensor cuenta con un convertidor de voltaje que permite conectar de manera segura ambos dispositivos. En los planos se puede ver un circuito convertidor bidireccional de 3.3V a 5V similar a este

Ten mucho cuidado al alimentar el sensor, este necesitará que conectes: 5V, 3.3V, GND.

Las resistencias de pullup ya están en el sensor como puedes observar en los planos.

Ejercicio 18: RETO

¿Cómo probamos si los dispositivos quedaron bien conectados?

Debes hacer un programa en el controlador que detecte si el dispositivo está o no en el bus I2C. ¿Recuerdas las figuras con los diagramas de tiempo? ¿Qué era lo primero que hacía el maestro cuando deseaba leer o escribir? Si el esclavo está en el bus ¿Qué le responde al maestro?

Ejercicio 19: RETO

Construye una aplicación para el ESP32 que:

  • Detecte si el sensor está en el bus I2C.
  • Detecte si el sensor se desconecta del bus.
  • Configura la hora, minutos, segundos y el formato 12H o 24H.
  • Configura el día, mes, año y día de la semana.
  • Lee la hora completa (horas, minutos, segundos).
  • Lee la fecha completa (día, mes, año y día de la semana).

Puedes utilizar el programa monitor de arduino para verificar todas las características anteriores.

Ejercicio 20: RETO

Construye una biblioteca para Arduino con todo lo que aprendiste. Te puedes basar en esta referencia para construir tu propia biblioteca.

Ejercicio 21: proyecto

Ahora piensa que quieres hacer de proyecto; sin embargo, ten presente estos elementos mínimos:

  • Debes incluir al menos un dispositivo I2C y otro SPI. Ambos pueden estar controlados por el mismo ESP32.
  • Conecta el ESP32 a Unity usando comunicaciones seriales mediante un protocolo binario.
  • La configuración (puerto serial, velocidad, etc) y el control de tu aplicación interactiva debe realizarse mediante una interfaz de usuario gráfica.

Recuerda que antes de comenzar el proyecto debes reunirte con tu profesor para discutir los conceptos de la unidad y obtener luz verde para comenzar a trabajar en tu proyecto.