Control de velocidad tipo PID para un motor DC 
Uno de los controladores más utilizados es el tipo PID (Proporcional Integral Derivativo). A lo largo de este post se abordará la implementación de uno en Arduino para controlar la velocidad de un motor DC.

Un poco de teoría

Cuando se quiere controlar una planta (en nuestro caso un motor DC), lo más habitual es plantear un lazo de control estándar:

La señal que entra al controlador es la medida que queremos que alcance la planta (llamada "consigna" en teoría del control) menos la medida de salida de la planta o, lo que es lo mismo, el error. El objetivo del controlador será siempre minimizar el valor absoluto del error (que tienda a cero) actuando sobre la entrada de la planta.

Para profundizar bien en el estudio del control habría que ver las transformadas de Laplace, los polos y los ceros del sistema y, para el caso discreto, lo ideal sería un estudio basado en la transformada Z estudiando también la ubicación de los polos y los ceros. Sin embargo me centraré en el estudio y la implementación de un controlador estándar: el PID.

PID

Los controladores PID son un tipo especial de controlador que combinan la acción proporcional (P), la acción integral (I) y la acción derivativa (D) sobre el error. Si a la entrada del controlador (el error) la llamamos e(t) y a la salida del controlador (la entrada a la planta, en nuestro caso la entrada al motor DC) la llamamos u(t). Podemos definir un PID de la siguiente manera:
$$u(t)=K_pe(t)+K_i\int_0^t e(\tau)d\tau+K_d{de(t) \over dt}$$
Como se puede apreciar, la acción proporcional vendrá determinada por la constante $K_p$, la acción integral por la constante $K_i$ y la acción derivativa por la constante $K_d$.

1. La acción proporcional $K_p$ hace que el error en estado estacionario tienda a cero.

2. La acción integral $K_i$, al ir sumando los errores en el tiempo (integral), tiende a eliminar el error estacionario generado por la acción proporcional.

3. La acción derivativa $K_d$ tiende a suavizar las variaciones en el error.

Para determinar los mejores valores de cada una de las constantes, lo ideal es realizar un estudio mediante la transformada de Laplace y buscar la mejor ubicación de los polos y los ceros del controlador PID para que se obtenga el comportamiento deseado.

En este caso se ha optado por realizar pruebas empíricas con valores bajos e ir probando diferentes combinaciones.

Implementación a nivel hardware

En este caso la planta es un motor DC del que vamos a controlar su velocidad mediante la salida PWM de 8 bits (0 a 255) y 5 voltios. La salida PWM la conectamos a la base de un transistor NPN de potencia (en este caso un BD139) montado en configuración de emisor común.

La lectura de la velocidad angular la hacemos utilizando un disco pintado (mitad blanco y mitad negro) conectado al eje de rotación (para que gire) y un sensor reflexivo de infrarrojos CNY70 (del que se utiliza en los robots sigue líneas).



Polarizando el fototransistor y el led infrarrojo y acondicionando la señal con una puerta inversora de tipo schmitt, ya tenemos un flanco de subida o de bajada por cada vuelta que da el disco.


Implementación a nivel software

Lectura de la velocidad

Para obtener la velocidad de rotación lo más eficiente es conectar la salida del inversor schmitt a una entrada del microcontrolador que permita disparar interrupciones internas en cada flanco de bajada o en cada flanco de subida. El pseudocódigo sería como sigue:
rpm = 0
anterior_t = 0

cada vez que haya un flanco de subida hacer:
t = microsegundos
incremento = t - anterior_t
rpm = (1 / incremento) * 60000000
anterior_t = t
fin interrupción

De esta forma tenemos los rpm a los que va el motor. Nótese que esta implementación no detecta la velocidad de 0 rpm. Para detectar la velocidad de 0 rpm habría que incluir un timer que, pasado un tiempo determinado, si no se produce la interrupción, asuma que el disco se ha parado (rpm = 0). En este caso no se ha implementado esta funcionalidad por simplicidad.

Implementación del PID

Para implementar el controlador PID en el Arduino (o en cualquier otro microcontrolador) tenemos que discretizar la ecuación diferencial que relaciona u(t) con e(t). Separamos primero dicha ecuación diferencial en partes:
$$u(t)=u_p(t)+u_i(t)+u_d(t)$$
Siendo:
$$u_p(t)=K_pe(t)$$
$$u_i(t)=K_i\int_0^t e(\tau)d\tau$$
$$u_d(t)=K_d{de(t) \over dt}$$
La discretización de $u_p(t)$ es trivial:
$$u_p[k]=K_pe[k]$$
La discretización de $u_i(t)$ asumiento un período de muestreo de $T$ lo suficientemente bajo la podemos calcular aproximando la integral mediante una suma de áreas de rectángulos de base $T$ y altura $e[k]$:
$$u_i[k]=K_i\sum_{n=0}^kTe[n]=K_iT\sum_{n=0}^ke[n]$$
De la misma manera, la discretización de $u_d(t)$ asumiento un período de muestreo $T$ lo suficientemente bajo la podemos calcular aproximando la derivada mediante el cálculo de la pendiente de la recta que une $e[k-1]$ con $e[k]$:
$$u_d[k]=K_d{e[k]-e[k-1] \over T}$$
El PID discretizado nos quedaría, por tanto, de la siguiente manera:
$$u[k]=K_pe[k]+K_iT\sum_{n=0}^ke[n]+K_d{e[k]-e[k-1] \over T}$$
Esta ecuación en diferencias finitas sí es fácilmente implementable en cualquier sistema. En el caso de Arduino podríamos realizar la siguiente implementación:

struct pid_controller {
  float kp, ki, kd;
  float delta;
  float sum;
  float prevError;
};

void pid_controller_init(struct pid_controller &pid, float delta, float kp, float ki, float kd) {
  pid.delta = delta;
  pid.kp = kp;
  pid.ki = ki;
  pid.kd = kd;
  pid.sum = 0;
  pid.prevError = 0;
}

float pid_controller_run(struct pid_controller &pid, float error) {
  float p = pid.kp * error;
  pid.sum += error;
  float i = pid.ki * pid.delta * pid.sum;
  float d = pid.kd * (error - pid.prevError) / pid.delta;
  pid.prevError = error;
  return p + i + d;
}

Las pruebas empíricas realizadas han dado muy buenos resultados para:
$$K_p=K_i=K_d=0.5$$
Con un período de muestreo $T=0.01$. La inicialización, por tanto, quedaría así:

void setup() {
    ...
    pid_controller_init(motor_pid_controller, 0.01, 0.5, 0.5, 0.5);
    ...
}

Mientras que cada 10 milisegundos ($T=0.01$) habrá que calcular el PID:

const float SET_POINT = 1600;   // consigna en rpm
unsigned long last_t = 0;

void loop() {
    unsigned long t = millis();
    if ((t - last_t) >= 10) {
        float error = SET_POINT - current_rpm;
        float u = pid_controller_run(motor_pid_controller, error);
        analogWrite(PWM_OUTPUT, (int) u);
        last_t = t;
    }
}


Pruebas realizadas

Para una consigna de 1000 rpm, la velocidad angular medida utilizando el PID es la siguiente (100ms entre medida y medida):
1013.99 rpm
1025.57 rpm
1013.03 rpm
1019.02 rpm
986.13 rpm
1003.95 rpm
1002.00 rpm
1013.65 rpm
999.07 rpm
977.64 rpm
1013.99 rpm
1037.49 rpm
1018.26 rpm
998.14 rpm
986.71 rpm
1006.64 rpm
1017.29 rpm
1017.43 rpm

Mientras que para una consigna de 1600 rpm, la velocidad angular medida fue la siguiente (100ms entre medida y medida):
1632.03 rpm
1591.01 rpm
1602.56 rpm
1583.28 rpm
1608.92 rpm
1578.28 rpm
1599.66 rpm
1583.61 rpm
1586.29 rpm
1616.21 rpm
1619.35 rpm
1594.39 rpm
1601.54 rpm
1581.11 rpm
1601.37 rpm
1606.68 rpm
1570.52 rpm
1602.39 rpm

El código fuente para Arduino puede descargarse de la sección soft.



[ añadir comentario ] ( 3402 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 4206 )
Detección de tonos en sistemas embebidos 
Cuando pensamos en detectar determinadas frecuencias o tonos en una señal lo primero que se nos viene a la cabeza suele ser la FFT, en concreto la implementación de Cooley-Tukey con N potencia de 2. La FFT está muy bien si lo que queremos es todo el espectro de una señal, pero si lo que necesitamos es detectar un único tono en una frecuencia concreta podemos recurrir al algoritmo de Goertzel, más rápido y muy fácil de implementar en sistemas embebidos.

El algoritmo de Goertzel permite calcular un coeficiente aislado de la DFT sobre un conjunto de N muestras con una complejidad temporal de O(n) y una complejidad espacial de O(1), además N no tiene por qué ser potencia de 2. Este algoritmo se basa en la aplicación de una ecuación en diferencias finitas (un filtro IIR).

A mayor N, mayor resolución en frecuencia y también mayor latencia en la detección de los tonos. Es necesario, por tanto encontrar un compromiso. La resolución en hercios del algoritmo de Goertzel viene dada por:
$$R = {f_s \over N}$$
Siendo $f_s$ la frecuencia de muestreo y $N$ el número de muestras que se evalúan en cada pasada.

1. Se calcula el índice del coeficiente correspondiente

Lo primero que hay que hacer es calcular el índice del coeficiente asociado a la DFT a partir de la frecuencia que queremos detectar:
$$k = N{f \over f_s}$$
Siendo $f$ la frecuencia que queremos detectar, $f_s$ la frecuencia de muestreo y $N$ el número de muestras que procesamos cada vez. La máxima frecuencia que podemos detectar será la mitad de la frecuencia de muestreo.

2. Para cada conjunto de N muestras

2.1. Se aplica la ecuación en diferencias

Para cada una de las muestras que van llegando, vamos aplicando la siguiente ecuación en diferencias finitas:
$$s[n] = x[n] + 2\cos\left({2 \pi k \over N}\right)s[n-1] - s[n-2]$$
$$n = 0..N$$
Con codiciones iniciales $s[-1] = s[-2] = 0$. Se trata, como se puede ver, de un sencillo filtro IIR de segundo orden.

2.2. Se obtiene del coeficiente k-esimo de la DFT

Se puede demostrar que el coeficiente k-ésimo de la DFT de tamaño N es:
$$X(k) = s[N] - W_N^ks[N-1]$$
Siendo:
$$W_N = e^{-j\left({2 \pi \over N}\right)}$$
Por tanto:
$$X(k) = s[N] - e^{-j\left({2 \pi \over N}\right)k}s[N-1]$$
Si desarrollamos la exponencial compleja mediante la fórmula de Euler tenemos que:
$$e^{-j\left({2 \pi \over N}\right)k} = \cos\left({2 \pi \over N}\right) - j \sin\left({2 \pi k \over N}\right)$$
Y, por tanto:
$$X(k)_{real} = s[N] - \cos\left({2 \pi k \over N}\right)s[N-1]$$
$$X(k)_{imag} = \sin\left({2 \pi k \over N}\right)s[N-1]$$
Para calcular la magnitud de la banda de frecuencia correspondiente calculamos el módulo de $X(k)$:
$$M^2 = \left|X(k)\right|^2 = X(k)_{real}^2 + X(k)_{imag}^2$$
De esta forma podemos medir la magnitud de la banda de frecuencia correspondiente al coeficiente k-ésimo de la DFT o, lo que es lo mismo, la magnitud de la banda de frecuencia correspondiente a la frecuencia $f$.
$$k = N{f \over f_s} \Rightarrow f = {k f_s \over N}$$

Implementación

A continuación se puede ver una implementación del algoritmo de Goertzel en Arduino:
const int ANALOG_INPUT = A0;
const int SAMPLE_RATE_HZ = 3000;
const int SAMPLE_PERIOD_US = 1000000 / SAMPLE_RATE_HZ;
const int F_HZ = 440;
const int N = 3000;
const int K = 440;  // N * F_HZ / SAMPLE_RATE_HZ;
unsigned long tPrev;

struct goertzelFilter {
  float s1, s2;
  int k, n;
  float sinv, cosv, cosv2;
  int nextIteration;
};

struct goertzelFilter filter;

void goertzelFilterReset(struct goertzelFilter &f) {
  f.s1 = 0;
  f.s2 = 0;
  f.nextIteration = 0;
}

void goertzelFilterInit(struct goertzelFilter &f, int k, int n) {
  f.k = k;
  f.n = n;
  f.cosv = cos(2 * PI * k / n);
  f.sinv = sin(2 * PI * k / n);
  f.cosv2 = 2 * f.cosv;
  goertzelFilterReset(f);
}

boolean goertzelFilterFinished(struct goertzelFilter &f) {
  return (f.nextIteration == (f.n + 1));
}

void goertzelFilterRun(struct goertzelFilter &f, float input) {
  float s = input + (f.cosv2 * f.s1) - f.s2;
  f.s2 = f.s1;
  f.s1 = s;
  f.nextIteration++;
}

float goertzelFilterGetMagnitude(struct goertzelFilter &f) {
  float real = f.s1 - (f.cosv * f.s2);
  float imag = f.sinv * f.s2;
  return ((real * real) + (imag * imag));
}

void setup() {
  Serial.begin(9600); 
  tPrev = micros();
  goertzelFilterInit(filter, K, N);
  goertzelFilterReset(filter);
}

void loop() {
  unsigned long t = micros();
  if ((t - tPrev) >= SAMPLE_PERIOD_US) {
    int v = analogRead(ANALOG_INPUT);
    if (goertzelFilterFinished(filter)) {
      float magnitude = goertzelFilterGetMagnitude(filter);
      Serial.println(magnitude);
      goertzelFilterReset(filter);
    }
    else
      goertzelFilterRun(filter, ((float) v - 512) / 512);
    tPrev = t;
  }
}

Se ha elegido una frecuencia de muestreo baja (3000Hz) para poder trabajar cómodamente con tipos float. Utilizando aritmética de punto fijo podríamos incremenentar la frecuencia de muestreo y que la detección sea más precisa.

La entrada de audio se toma de la entrada analógica A0 a la que se conecta un sencillo circuito amplificador:


Elegir el valor de N

A la hora de elegir la N lo ideal es escogerla lo más grande posible, que nos permita una latencia razonable y que se cumpla que:
$$k = N{f \over f_s} \in \mathbb{N}$$
Por ejemplo, para detectar un tono de 1Khz sobre una señal muestreada a 6KHz lo ideal sería que la N valiese: 96, 102, 114… Ya que para todos estos valores se cumple que $k$ es número natural.

Magnitudes medidas para diferentes tonos

En ausencia de señal de entrada: 0.30, 0.37, 0.30, 0.35, 0.44, 0.32, 0.37, 0.28...

Con una señal de entrada de 200Hz: 2.56, 1.73, 3.56, 0.81, 0.58, 1.67, 4.71, 6.81...

Con una señal de entrada de 440Hz (la frecuencia del detector): 138.41, 87.29, 441.14, 185.20, 233.03, 762.27, 80.62, 330.98...

Con una señal de entrada de 500Hz: 25.70, 9.60, 22.50, 2.76, 16.62, 18.75, 23.56, 35.58...



[ añadir comentario ] ( 2534 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3.1 / 3204 )
Implementación a bajo nivel de un dispositivo USB sobre Arduino Leonardo. Prueba de concepto 
El protocolo USB es uno de los protocolos de comunicación más utilizado y, a la vez, uno de los más complicados y esotéricos para una gran mayoría de programadores. Lo habitual cuando nos enfrentamos a un desarrollo embebido con USB es tirar de librerías o frameworks ya existentes para el microcontrolador que estemos usando (dentro del ecosistema de microcontroladores AVR de 8 bits una de las mejores librerías es, sin duda, LUFA). Sin embargo, en este caso he optado por una implementación desde cero para, por un lado, aprender más sobre el protocolo y, por otro lado, desmitificar la programación de dispositivos USB.

Breve introducción al protocolo USB

El protocolo USB es un protocolo serie basado en tramas de paquetes y con una arquitectura muy sencilla. En un bus USB hay un único host, cero o más dispositivos y cero o más hubs. En el protocolo USB es siempre el host el que lleva la voz cantante: un dispositivo nunca envía de forma asíncrona datos a su host, es el host el que interroga al dispositivo si tiene datos para él (esta forma de hacer las cosas, a priori enrevesada, facilita luego mucho la implementación del protocolo).

Cada conexión USB entre un host y un dispositivo está formada por varios “endpoints”, un endpoint es un canal de comunicación entre el host y el dispositivo y cada dispositivo puede tener hasta 16 endpoints (o canales) de comunicación con el host. Cada endpoint es unidireccional, esto es, cada endpoint debe ser definido como de entrada o de salida (siempre desde el punto de vista del host, IN es dispositivo->host, y OUT es host->dispositivo, siempre, aunque estemos en el contexto del dispositivo).

Hay cuatro tipos de endpoints: de control (estos son especiales y pueden ser bidireccionales), bulk, interrupt e isócronos:

Control: Endpoints usados para configurar el dispositivo.
Bulk: Endpoints para transferencias “estándar” de datos. No tienen latencia máxima garantizada.
Interrupt: Endpoints destinados a envíos asíncronos. Tienen una latencia máxima fija garantizada.
Isócronos: Endpoints para transferencias con alto ancho de banda, con detección de errores pero sin reintento de envío de paquetes. No se garantiza la entrega de los paquetes.

Cada dispositivo USB tiene una serie de tablas (normalmente alojadas en flash o en ROM) denominadas descriptores, que son transferidas al host en el arranque (mediante un endpoint de control) y que identifican de forma precisa y estándar la funcionalidad y los endpoints disponibles (tipo, dirección, etc.) en el dispositivo.



Cada dispositivo tiene un descriptor de dispositivo, dentro de cada descriptor de dispositivo podrá haber diferentes configuraciones (descriptores de configuración), a su vez dentro de cada configuración habrá descriptores de interfaces y dentro de éstos habrá descriptores de endpoints. Lo más habitual es que cada dispositivo sólo tenga un único descriptor de configuración. Hay un descriptor de endpoint por cada endpoint en uso por parte del dispositivo. No existe descriptor para el endpoint 0 ya que es un endpoint que siempre debe estar disponible y sólo puede ser de control.

Inicialización

La secuencia de inicialización es como sigue:

1. Cuando el host detecta un dispositivo conectado a un puerto USB realiza una secuencia de reset sobre dicho puerto.

2. A continuación, el host envía un paquete SETUP a través del endpoint de control 0 con el comando GET_DESCRIPTOR(device)

3. El dispositivo, como respuesta, devuelve su descriptor de dispositivo.

4. El host solicita a través del endpoint de control 0 los descriptores de cada una de las configuraciones con el comando GET_DESCRIPTOR(configuration). Normalmente cada dispositivo sólo tiene una única configuración.

5. El dispositivo envía como respuesta también a través del endpoint de control 0 los descriptores de configuración (normalmente uno). Cuando un dispositivo envía el descriptor de configuración, envía dentro de él los descriptores de interface y de endpoints asociados a dicha configuración (ver imagen anterior).

6. Cuando el host (el ordenador) ya “sabe quién es” el dispositivo lo “enumera”, esto es, le asigna una dirección en el bus USB y envía al dispositivo el comando SET_ADDRESS(dirección).

7. El dispositivo queda enumerado y pasa a escuchar en la dirección indicada por el host.

8. El host envía al dispositivo el comando SET_CONFIGURATION(configuracion) para decirle al dispositivo que quiere trabajar con una configuración en concreto (como vimos antes, normalmente sólo hay una configuración por cada dispositivo).

A partir de este momento el dispositivo queda listo para usar y para que se empiecen a usar el resto de sus endpoints.

El protocolo es mucho más complejo y es necesario tener en cuenta algunos comandos más (SET_FEATURE, GET_STATUS, etc.) pero a grosso modo así es como funciona el invento.

Recomiendo a todo aquel interesado en el tema, dos lecturas de referencia:
- USB in a Nutshell
- USB Made Simple
Son muy sencillas de leer y mucho más asequibles que el aterrador tocho de 650 páginas de la especificación oficial.

Implementación en el ATmega32u4

Este microcontrolador, incluido en el Arduino Leonardo, puede ser configurado como dispositivo USB. Debido a que los tiempos USB son extremadamente cortos, es necesario realizar una implementación orientada a interrupciones. Como prueba de concepto he decidido implementar un puerto serie virtual (al estilo de los cables conversores USB a RS232) basado en dos endpoints de tipo bulk, uno de entrada y otro de salida.

El pseudocódigo para implementar el dispositivo USB quedaría así:
interrupción de reset de bus USB
configurar endpoints 0 como control, 1 como bulk in y 2 como bulk out
inicializar buffers
habilitar interrupción de endpoints
fin interrupción

interrupción de endpoints
si ha llegado un paquete de setup por el endpoint 0 entonces
si es de tipo GET_DESCRIPTOR entonces
devolver el descriptor correspondiente
en otro caso, si es de tipo SET_ADDRESS entonces
configurar el módulo USB del chip para usar la dirección indicada
en otro caso, si es de tipo GET_STATUS entonces
devolver 0 (todo ok)
en otro caso, si es de tipo GET_CONFIGURATION entonces
devolver un 1 (la configuración activa es siempre la 1)
en otro caso, si es de tipo SET_CONFIGURATION entonces
no hacer nada (sólo hay una configuración y es la que siempre está activa)
fin si
en otro caso, si ha llegado un token bulk out por el endpoint 2 entonces
transferir bytes del buffer del endpoint 2 al buffer de usuario
en otro caso, si ha llegado un token bulk in por el endpoint 1 entonces
transferir bytes del buffer de usuario al buffer del endpoint 1
fin si
fin interrupción
Los descriptores se encuentran definidos en USBDescriptor.H:
class USBSerialDescriptorContainer {
    public:
        USBDeviceDescriptor deviceDescriptor;
        USBConfigurationDescriptor configurationDescriptor;
        USBInterfaceDescriptor interfaceDescriptor;
        USBEndpointDescriptor inEndpointDescriptor;
        USBEndpointDescriptor outEndpointDescriptor;
} __attribute__ ((packed));

const USBSerialDescriptorContainer MyUSBSerialDescriptorContainer = {
    {  // device descriptor
        0x12,    // descriptor size
        0x01,    // descriptor type (device)
        0x0100,  // usb protocol version
        0x00,
        0x00,
        0x00,
        0x40,    // bMaxPacketSize0 = 64  (for endpoint 0)
        0xA4F6,  // idVendor
        0x5678,  // idProduct
        0x0100,  // product version
        0x00,
        0x00,
        0x00,
        0x01     // one available configuration
    },
    {  // configuration descriptor
        0x09,    // descriptor size
        0x02,    // descriptor type (configuration)
        0x0020,  // total size of this descriptor and rest of descriptors inside this configuration 9 + 9 + 7 + 7 = 32 bytes
        0x01,    // num interfaces = 1
        0x01,    // this configuration number = 1
        0x00,
        0x80,    // not self powered
        0x04     // max power in units of 5 mA (4 * 5 = 20 mA)
    },
    {  // interface descriptor
        0x09,    // descriptor size
        0x04,    // descriptor type (interface)
        0x00,    // interface number (zero based)
        0x00,
        0x02,    // num endpoints = 2
        0xFF,    // class = vendor specific
        0x00,    // subclass
        0x00,
        0x00
    },
    {  // in endpoint descriptor
        0x07,    // descriptor size
        0x05,    // descriptor type (endpoint)
        0x81,    // in endpoint 1
        0x02,    // bulk endpoint
        0x0040,  // max packet size = 64
        0x0A     // 10ms for interval polling
    },
    {  // out endpoint descriptor
        0x07,    // descriptor size
        0x05,    // descriptor type (endpoint)
        0x02,    // out endpoint 2
        0x02,    // bulk endpoint
        0x0040,  // max packet size = 64
        0x0A     // 10ms for interval polling
    }
};

Para hacer la prueba de concepto lo he implementado todo, por ahora, en una única clase “USB” a modo de utility class (con atributos y métodos estáticos):
ISR(USB_GEN_vect) {
    USB::__general_isr();
}

void USB::__general_isr() {
    if (UDINT & (1 << EORSTI))
        USB::configureEndpoint0();
    UDINT = 0;
}

ISR(USB_COM_vect) {
    USB::__endpoint_isr();
}

void USB::sendDataToEndpoint0() {
    while (USB::toSendSize > 0) {
        // wait for host ready for in packet
        while (!(UEINTX & ((1 << TXINI) | (1 << RXOUTI))))
            ;
        if (UEINTX & (1 << RXOUTI))
            break;
        // send in packet
        uint16_t n = (USB::toSendSize >= 64) ? 64 : USB::toSendSize;
        USB::writeFromBuffer(n, USB::toSend);
        USB::toSend += n;
        USB::toSendSize -= n;
        UEINTX = ~(1 << TXINI);
    }
}

void USB::__endpoint_isr() {
    if (USB::status == USB::STATUS_IDLE) {
        UENUM = 0;
        uint8_t aux = UEINTX;
        if (aux & (1 << RXSTPI)) {
            // setup packet received
            USB::readOnBuffer(8, USB::buffer);
            USB::setupPacketReceived = (USBSetupPacket *) USB::buffer;
            UEINTX = ~((1 << RXSTPI) | (1 << RXOUTI) | (1 << TXINI));
            if (USB::setupPacketReceived->bRequest == USBSetupPacket::REQUEST_GET_DESCRIPTOR) {
                uint8_t descriptorType = USB::setupPacketReceived->wValue >> 8;
                USB::toSendSize = 0;
                if (descriptorType == 1) {
                    // get the device descriptor
                    USB::toSend = (uint8_t *) &MyUSBSerialDescriptorContainer.deviceDescriptor;
                    USB::toSendSize = sizeof(USBDeviceDescriptor);
                    USB::toSendSize = (USB::toSendSize > USB::setupPacketReceived->wLength) ? USB::setupPacketReceived->wLength : USB::toSendSize;
                }
                else if (descriptorType == 2) {
                    // get the configuration descriptor
                    USB::toSend = (uint8_t *) &MyUSBSerialDescriptorContainer.configurationDescriptor;
                    USB::toSendSize = sizeof(USBSerialDescriptorContainer) - sizeof(USBDeviceDescriptor);
                    USB::toSendSize = (USB::toSendSize > USB::setupPacketReceived->wLength) ? USB::setupPacketReceived->wLength : USB::toSendSize;
                }
                USB::sendDataToEndpoint0();
            }
            else if (USB::setupPacketReceived->bRequest == USBSetupPacket::REQUEST_SET_ADDRESS) {
                UEINTX = ~(1 << TXINI);
                USB::address = USB::setupPacketReceived->wValue;
                UDADDR = USB::address;
                while (!(UEINTX & (1 << TXINI)))
                    ;
                UDADDR |= (1 << ADDEN);
                UEINTX = ~(1 << TXINI);
            }
            else if (USB::setupPacketReceived->bRequest == USBSetupPacket::REQUEST_GET_STATUS) {
                USB::buffer[0] = 0;
                USB::buffer[1] = 0;
                USB::toSend = (uint8_t *) USB::buffer;
                USB::toSendSize = 2;
                USB::sendDataToEndpoint0();
            }
            else if (USB::setupPacketReceived->bRequest == USBSetupPacket::REQUEST_GET_CONFIGURATION) {
                USB::buffer[0] = 1;
                USB::toSend = (uint8_t *) USB::buffer;
                USB::toSendSize = 1;
                USB::sendDataToEndpoint0();
            }
            else if (USB::setupPacketReceived->bRequest == USBSetupPacket::REQUEST_SET_CONFIGURATION) {
                UEINTX = ~(1 << TXINI);
            }
        }
        // check for bulk out transfer
        UENUM = 2;
        if (UEINTX & (1 << RXOUTI)) {
            UEINTX &= ~(1 << RXOUTI);
            uint16_t numBytesReceived = (((uint16_t) UEBCHX) << 8) | ((uint16_t) UEBCLX);
            numBytesReceived = (numBytesReceived > 64) ? 64 : numBytesReceived;
            USB::readOnBuffer(numBytesReceived, USB::rxBuffer);
            USB::rxBufferFull = true;
            UEINTX &= ~(1 << FIFOCON);
        }
        // check for bulk in transfer
        UENUM = 1;
        if (UEINTX & (1 << TXINI)) {
            UEINTX &= ~(1 << TXINI);
            // TODO: write data to buffer
            UEINTX &= ~(1 << FIFOCON);
        }
    }
    else if (USB::status == USB::STATUS_HALT) {
        UENUM = 0;
        UEINTX = ~((1 << RXSTPI) | (1 << RXOUTI) | (1 << TXINI));
        UECONX = (1 << STALLRQ) | (1 << EPEN);   // stall
    }
}

Una vez conectado el Arduino Leonardo y enumerado el dispositivo podemos, desde un ordenador con Linux, reconocer el nuevo dispositivo como interface serie:

1. Instalamos el driver usbserial:
modprobe usbserial vendor=0xa4f6 product=0x5678
Esto instala el driver genérico usbserial y lo asocia a nuestro dispositivo creando el dispositivo /dev/ttyUSB0 en el sistema de archivos.

2. Emitimos una cadena de caracteres de ejemplo para hacer la prueba:
echo -en "Prueba desde Linux\n\0" > /dev/ttyUSB0
3. Voilà:





El código es compatible también con el microcontrolador ATmega16u4 y es muy fácil de adaptar al resto de microcontroladores USB de AVR. Puede descargarse de la sección soft.

[ 2 comentarios ] ( 6570 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 2941 )
Vúmetro LCD 
La posibilidad de redefinir una parte del juego de caracteres en los displays LCD alfanuméricos, en combinación con el uso de una de las entradas analógicas del AVR y un pequeño circuito analógico, nos va a permitir la implementación de un sencillo vúmetro en el Arduino.

Un vúmetro no es más que un medidor gráfico de intensidad de señal de audio. En este caso vamos a abordar una implementación muy sencilla y lineal (no logarítmica).



Entrada analógica

La entrada analógica del microcontrolador AVR en el Arduino permite medir entre 0 y 5 voltios con una precisión de 10 bits, de 0 a 1023. 0 se corresponde con 0 voltios y 1023 se corresponde con 5 voltios. Para obtener la intensidad de señal en una de las entradas analógicas, realizamos los siguientes pasos:

1. Amplificación
2. Detección de envolvente

La amplificación se ha implementado construyendo una sencilla etapa estándar de amplificación basada en un transistor NPN con configuración de emisor común:



La señal de entrada es una señal de audio en alterna (en este caso la he conectado a la salida de auriculares de mi móvil). Dicha señal, al pasar por el circuito de amplificación se invierte en fase (no nos importa) y, lo más importante, se amplifica. La señal de salida es también en alterna (el condensador de salida elimina la componente de continua) y pasa la siguiente parte del circuito: el detector de envolvente.



Un detector de envolvente es un circuito que, a partir de una señal de entrada, determina su envolvente:



(imagen extraida de Wikimedia Commons)

El circuito detector de envolvente es muy sencillo: La señal alterna, al hacerla pasar por un diodo, se rectifica y sólo deja pasar los semiciclos positivos. En cada uno de los semiciclos positivos de la señal se carga el condensador y, durante las pausas entre semiciclos, en ausencia de corriente que pase por el diodo, el condensador se descarga lentamente a través de la resistencia, así hasta el siguiente ciclo, que se repite el proceso. El resultado en la salida es una señal que “sigue” de forma aproximada a los picos de la señal de audio que hay en la entrada.

Hay que elegir correctamente los valores del condensador y la resistencia: Un valor de condensador muy bajo hará que se descargue rápidamente mientras que un valor de condensador muy alto hará que tarde excesivamente en cargarse. En el caso de la resistencia de descarga del condensador un valor muy alto hará que el condensador apenas se descargue en las pausas entre ciclos (lo que puede provocar que los ciclos se “sumen” a medida que llegan) y un valor muy bajo hará que el condensador se descargue muy rápido, perdiendo el efecto de seguimiento de envolvente.

Display LCD

En post anteriores de este blog publiqué varias clases C++ para la gestión de displays LCD. En este caso he reutilizado la clase “Lcd20x4” de proyectos anteriores, redefiniendo de forma estática 5 de los caracteres definibles por el usuario.

Para implementar una barra horizontal hay que tener en cuenta que tenemos 20 columnas y que cada carácter tiene 5 columnas de puntos: En total tenemos 100 pixels en horizontal para un display LCD de 20x4. Si definimos los caracteres de la siguiente forma:
* . . . .
* . . . .
* . . . .
* . . . . --> 1
* . . . .
* . . . .
* . . . .

* * . . .
* * . . .
* * . . .
* * . . . --> 2
* * . . .
* * . . .
* * . . .

...

* * * * *
* * * * *
* * * * *
* * * * * --> 5
* * * * *
* * * * *
* * * * *

Utilizando este método de visualización, la barra LCD podrá adoptar valores entre 0 y 100. El pseudocódigo para visualizar un valor “v” será:

procedimiento mostrar_valor(v)     // 0 <= v <= 100
numCaracteresTotalmenteLlenos := parte entera de (v / 5)
valorCaracterParcial := (v mod 5)
ir_coordenada(0, 0)
para x := 1 hasta numCaracteresTotalmenteLlenos hacer
escribir_carácter(5)
fin para
escribir_carácter(valorCaracterParcial)
para x := (numCaracteresTotalmenteLlenos + 2) hasta 20 hacer
escribir_carácter(0)
fin para
fin procedimiento

Como se puede ver, para cada valor “v” que se quiera visualizar en la barra, se escribe toda una fila horizontal del display LCD. La visualización puede optimizarse si partimos de la base de que la diferencia entre un valor “v” enviado en un instante “t” y el valor “v” enviado al display en un instante “t + d” no va a variar mucho si “d” es lo suficientemente pequeño. En nuestro caso vamos a hacer un muestreo de la entrada analógica cada 50ms por lo que la “v” no va a variar excesivamente entre un instante de muestreo y el siguiente.

Si asumimos que la “v” no va a variar mucho podemos enviar al LCD sólo los cambios:

barraLogica[20]
barraFisica[20]

procedimiento mostrar_valor(v)
numCaracteresTotalmenteLlenos := parte entera de (v / 5)
valorCaracterParcial := (v mod 5)
j := 0
para x := 1 hasta numCaracteresTotalmenteLlenos hacer
barraLogica[j] := 5
j := j + 1
fin para
barraLogica[j] := valorCaracterParcial
j := j + 1
para x := (numCaracteresTotalmenteLlenos + 2) hasta 20 hacer
barraLogica[j] := 0
j := j + 1
fin para
fin procedimiento

procedimiento actualiza
dentroCambio := NO
inicioCambio := -1
para j := 0 hasta 19 hacer
si (dentroCambio) entonces
si (barraFisica[j] = barraLogica[j]) entonces
escribir_barra_fisica(inicioCambio, j)
dentroCambio := NO
fin si
en otro caso
si (barraFisica[j] <> barraLogica[j]) entonces
inicioCambio := j
dentroCambio := SI
fin si
fin si
barraFisica[j] := barraLogica[j]
fin para
si (dentroCambio) entonces
escribir_barra_fisica(inicioCambio, 19)
fin si
fin procedimiento

El procedimiento “actualiza” se ejecutará de forma periódica y, como se puede ver, sólo envía al display LCD los caracteres que cambien. Esta forma de refresco del display LCD es más eficiente y nos permite tasas de muestreo mayores.

En el siguiente vídeo puede verse el circuito en acción:



El código en C++ puede descargarse de la sección soft.

[ añadir comentario ] ( 1843 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 14939 )
Control de display LCD con soporte para caracteres en español 
El control de displays LCD alfanuméricos desde microcontroladores es un tópico ampliamente abordado en muchas webs y tutoriales. El juego de caracteres utilizado por este tipo de displays es de tipo ASCII con algunos símbolos adicionales, sobre todo asiáticos, y se echan en falta varios de los símbolos propios del español (tildes, diéresis, etc.). A lo largo de este post se plantea un sencillo algoritmo de gestión del display LCD que permite la utilización de texto en español de forma transparente al usuario.

Características del display

Los displays LCD alfanuméricos estándar almacenan los mapas de bits (los dibujos) de su juego de caracteres en una ROM interna, con la excepción de los 8 primeros caracteres (del 0 al 7). Los mapas de bits de estos 8 primeros caracteres se almacenan en una parte de la RAM del LCD denominada CGRAM (Character Generator RAM). Es esta parte de la RAM la que hay que utilizar para representar los caracteres extendidos no ASCII del español en nuestro display.

El mapa de bits de cada carácter está formado por una matriz de 5x7 puntos que se almacena en la CGRAM como 8 bytes consecutivos: 1 byte por cada línea horizontal (de la cual sólo son significativos los 5 bits más bajos) y un 1 byte adicional para la línea de cursor que siempre se pone a 0 para displays de 5x7. Como son configurables sólo los 8 primeros caracteres del juego de caracteres, esto nos da 8 * 8 = 64 bytes para la CGRAM.
        . . . x .       0 0 0 1 0
. . x . . 0 0 1 0 0
. x x x . 0 1 1 1 0
á --> . . . . x --> 0 0 0 0 1
. x x x x 0 1 1 1 1
x . . . x 1 0 0 0 1
. x x x x 0 1 1 1 1
. . . . . 0 0 0 0 0

Gestión de los caracteres

En español tenemos 16 caracteres que se salen de la simbología ASCII:
á é í ó ú ü ñ Á É Í Ó Ú Ü Ñ ¿ ¡

Como no es posible cargar en la CGRAM los mapas de bits de estos 16 caracteres de forma simultánea, es necesario implementar algún tipo de gestión a nivel software que cargue en CGRAM sólo los caracteres que necesitamos en cada momento.

Para la gestión de los caracteres se han utilizado las siguientes estructuras de datos (en pseudocódigo):
EntradaTabla {
caracterLatin1
mapaDeBits
caracterLcd
caracterLcdDefecto
vecesUsado
marcaCarga
}

EntradaTabla tabla[16]
Cola caracteresLcdDisponibles

Cada entrada de la tabla de caracteres especiales incluye el carácter “ISO-8859-1” o “latin1” correspondiente y el mapa de bits que lo dibuja en el display. caracterLcd es el carácter del LCD (1 al 7, no vamos a usar la entrada 0 por si acaso) que está mapeando a este carácter especial. caracterLcdDefecto es el carácter de la ROM que mapeará a este carácter latin1 en caso de que no esté disponible ningún hueco en la CGRAM (por ejemplo ‘á’ tiene como carácter por defecto ‘a’).

vecesUsado indica cuántas veces está siendo usado esa entrada de la tabla de caracteres extendidos: cada vez que se utiliza un carácter especial, se incrementa este contador de la entrada correspondiente y cada vez que se deja de utilizar en alguna parte de la pantalla (se borra o se sustituye por otro), se decrementa este contador de la entrada correspondiente. Cuando un contador llega a 0, el hueco que ocupaba esa entrada en la CGRAM es marcado como vacío (metido en la cola de caracteres LCD disponibles.

marcaCarga indica cuando una entrada de la tabla debe ser cargada en la CGRAM. La carga de los bitmaps de las entradas marcadas se realiza a posteiori para no influir en la escritura de los caracteres. Primero se escribe en la DDRAM (la memoria de pantalla) y al final, si es necesario enviar bitmaps de caracteres no ASCII, se escribe en la CGRAM.
inicializar(EntradaTabla e)
e.caracterLcd = 0
e.vecesUsado = 0
e.marcaCarga = NO
fin

inicializarCola
meter(caracteresLcdDisponibles, 1)
meter(caracteresLcdDisponibles, 2)
meter(caracteresLcdDisponibles, 3)
meter(caracteresLcdDisponibles, 4)
meter(caracteresLcdDisponibles, 5)
meter(caracteresLcdDisponibles, 6)
meter(caracteresLcdDisponibles, 7)
fin

buscarEntrada(c) {
Devuelve la entrada en “tabla” que cumpla “caracterLatin1 == c”, o NULL en caso de no haber ninguna entrada.
fin

reemplazarCaracter(nuevo, viejo)
EntradaTabla e = buscarEntrada(viejo)
si (e != NULL) {
e.vecesUsado = e.vecesUsado - 1
si (e.vecesUsado == 0) {
meter(caracteresLcdDisponibles, e.caracterLcd)
e.caracterLcd = 0
fin si
fin si
e = buscarEntrada(nuevo);
si (e == NULL)
devolver nuevo
en otro caso
si (e.caracterLcd > 0)
e.vecesUsado = e.vecesUsado + 1
en otro caso
si (caracteresLcdDisponibles está vacía)
devolver e.caracterLcdDefecto
e.caracterLcd = sacar(caracteresLcdDisponibles)
e.vecesUsado = 1
e.marcaCarga = SI
fin si
devolver e.caracterLcd
fin si
fin

procesar
para todos los segmentos de texto que haya que cambiar en el display
para todos los caracteres del segmento de texto
nuevo = nuevo carácter
viejo = actual carácter
v = reemplazarCaracter(nuevo, viejo)
emitir(v)
fin para
fin para
para todas las entradas e de la tabla con marcaCarga = SI hacer
cargar e.mapaDeBits en la CGRAM correspondiente al carácter e.caracterLcd
e.marcaCarga = NO
fin para
fin

Ejemplo de traza

Imaginemos que tenemos la pantalla en blanco (recién inicializada): Todas las entradas de la tabla las tenemos inicializadas (caracterLcd=0, vecesUsado=0, marcaCarga=NO) y la cola “caracteresLcdDisponibles” inicializada con los 7 huecos.

Para escribir la palabra “Máquina”, el proceso irá llamando a “reemplazarCaracter” por cada nueva letra:
    reemplazarCaracter(‘M’, ‘’)
No hay entrada en la tabla para el carácter ‘’
No hay entrada en la tabla para el carácter ‘M’
devuelve ‘M’
reemplazarCaracter(‘á’, ‘’)
No hay entrada en la tabla para el carácter ‘’
Hay una entrada e para el carácter ‘á’
como e.caracterLcd = 0, entonces
la cola de caracteres disponibles no está vacía
e.caracterLcd = 1
e.vecesUsado = 1
e.marcarCarga = SI
devuelve e.caracterLcd (1)
reemplazarCaracter(‘q’, ‘’)
No hay entrada en la tabla para el carácter ‘’
No hay entrada en la tabla para el carácter ‘q’
devuelve ‘q’
reemplazarCaracter(‘u’, ‘’)
No hay entrada en la tabla para el carácter ‘’
No hay entrada en la tabla para el carácter ‘u’
devuelve ‘u’
...

Al final del proceso de escritura de la cadena ‘Máquina’ tenemos que la cola de caracteres disponibles está así: [2, 3, 4, 5, 6, 7] y que la entrada de la tabla de caracteres correspondiente a la letra ‘á’ tiene caracterLcd=1, vecesUsado=1, el resto de entradas siguen como al principio.



De esta forma se va alojando espacio en la CGRAM del display LCD en función de los caracteres especiales que necesitamos en todo momento. Nótese que, en caso de que ya no nos queden huecos libres (la cola de huecos esté vacía), devolvemos el carácter por defecto.

Políticas alternativas de sustitución de caracteres

Una política alternativa podría ser establecer una prioridad por cada carácter de la tabla: en caso de vaciado de la cola de huecos, se sacrifica el carácter con menor prioridad de los que estén siendo usados en ese momento. Un criterio de prioridad podría ser en función del valor de “vecesUsado”. De esta forma se sacrificarían los caracteres menos usados.

Nótese que esta política de “sacrificado” de caracteres menos prioritarios obligaría a refrescar los caracteres a sacrificar y cambiarlos por los caracteres por defecto correspondientes. En todas las posiciones donde se encuentren.

En esta implementación no se lleva a cabo ninguna política de sacrificado de caracteres. Cuando la cola de caracteres disponibles se acaba, se imprime el carácter por defecto.

Implementación

Este algoritmo de gestión de caracteres extendidos para displays LCD se ha implementado sobre un Arduino Leonardo en C++.



Se ha optado por una anchura de bus de 4 bits para minimizar el número de cables. El código fuente puede descargarse de la sección soft.

[ añadir comentario ] ( 1904 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 1960 )

<< <Anterior | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Siguiente> >>