jueves, 8 de febrero de 2018

GPIO a bajo nivel para Arduino Leonardo

Este artículo es una traducción de un artículo previo en el blog R6500.

Las placas Arduino proporcionan una entrada fácil en el mundo del microcontrolador (MCU). Prácticamente sin conocimiento de electrónica y en poco tiempo puede tener un pequeño proyecto en funcionamiento.La forma Arduino de hacer la codificación se basa en un amplio conjunto de bibliotecas precodificadas que oculta las complejidades de la interfaz de un MCU. Todo eso está bien si no necesitas ir al límite de lo que la MCU puede hacer o si no te importa saber cómo el MCU hace sus cosas. Pero si necesita apurar los tiempos o si deseas aprender sobre MCUs, debes intentar alejarte un poco del estilo Arduino.

Una cosa buena y mala de Arduinos es su uniformidad. Todos los arduinos usan las mismas funciones digitalWrite, digitalRead, analogRead y analogWrite. Y funcionan igual, más o menos, si está utilizando un Arduino UNO con un ATmega328 o un Arduino Galileo con un Intel Quark SoC X1000. 

Pero, como los MCU utilizados son diferentes, realmente no funcionan exactamente igual.Si deseas ir más allá del estilo Arduino, la uniformidad disminuye a medida que los métodos cambian según el MCU que utilice la placa. Si dos placas utilizan el mismo tipo de MCU, los procedimientos internos serán similares, aunque no siempre iguales. Si dos placas utilizan un MCU diferente, los procedimientos internos pueden ser bastante diferentes.

En este artículo apuntaré al Arduino Leonardo. Este es una placa bastante capaz que viene a sustituir al viejo Arduino UNO. Incluye un MCU ATmega 32u4. Este es parte de la familia megaAVR de MCUs. La mayoría de las placas arduino clásicas como la UNO (ATmega328), Mega 2560 (ATmega2560), Mini (ATmega168) pertenecen a esta familia. De hecho, los Arduinos Micro y Esplora usan el mismo MCU 32u4 que usa el Leonardo.

Arduino Leonardo

El Arduino Leonardo no necesita usar un chip adicional para proporcionar la comunicación USB porque el MCU que incluye puede administrarlo por sí mismo. Por la misma razón, la placa puede emular un periférico USB como un ratón o un teclado.

Usar un Arduino para aprender el interior del MCU ATmega 32u4 tiene ventajas e inconvenientes. Por un lado, Arduino IDE proporciona todo tipo de funciones y bibliotecas para realizar operaciones complejas, por lo que es posible implementar código de bajo nivel en solo una parte del sistema. Por ejemplo, puede dejar la comunicación serial sobre USB a las bibliotecas Arduino y hacer el control manual de las líneas GPIO. Por otro lado, Arduino IDE intenta ocultar el sintaxis de bajo nivel del MCU y el bootloader utilizado para cargar el programa no proporciona ninguna herramienta de depuración de hardware para el código.Para empeorar las cosas, las librerías Arduino hacen un uso de los recursos internos del MCU y no siempre es fácil saber qué bibliotecas fallará si das un paso fuera del camino normal de Arduino.

En este primer artículo sobre el interior del Arduino Leonardo, hablaré sobre cómo funciona el GPIO digital. Es decir, hablaré sobre cómo hacer operaciones de bajo nivel equivalentes, pero más rápidas, al uso de las funciones pinMode, digitalRead y digitalWrite.

Pines Arduino y Puertos Leonardo

El MCU ATmega32u4 que usa Arduino Leonardo presenta 5 puertos digitales GPIO (entrada / salida de propósito general). Cada uno está etiquetado con un leter, por lo que tenemos los puertos B, C, D, E y F.

Cada puerto puede tener hasta 8 líneas digitales (numeradas del 0 al 7) asociadas con los pines del encapsulado del MCU.

Pines de ATmega 32u4
Como se puede ver en el pinout anterior, la mayoría de los pines del integrado incluyen referencias como PE6 (línea 6 del puerto E) y PB0 (línea 0 del puerto B). Como el encapsulado del MCU tiene 44 pines y como cada uno de los 5 puertos podría tener hasta 8 líneas, es fácil ver que te quedarías sin pines si incluyes fuentes de alimentación, conexiones Xtal y USB. Observando el pinout se ve, por ejemplo, que no hay PD5 y que el puerto E incluye solo dos líneas. De hecho, solo los puertos B y D están completos (8 líneas cada uno).

La distribución de líneas en los puertos del MCU es:

  •     Puerto B: 8 líneas PB0 a PB7
  •     Puerto C: 2 líneas PC6 y PC7
  •     Puerto D: 8 líneas PD0 a PD7
  •     Puerto E: 2 líneas PE2 y PE6
  •     Puerto F: 6 líneas PF0, PF1 y PF4 a PF7

Por tanto, tenemos 26 líneas GPIO en total. También se puede ver esta distribución de líneas en la descripción del diagrama de bloques interno del MCU.

Estructura interna de ATmega 32u4


Las líneas GPIO, de las que hay 26, están reetiquetadas, en las funciones Arduino, como líneas 0 a 23.  Es posible ver que faltan dos líneas. Ello es porque PD5 y PE2 no están disponibles como líneas digitales o analógicas en la placa de Leonardo. Si miras la placa, solo verás los números de 14 de las líneas (0 a 13). Las líneas 14 a 17 están ubicadas en el conector ISP / SPI macho de 6 pines y las líneas 18 a 23 son nombres de alias para las líneas analógicas A0 a A5 que también se pueden usar como E/S digitales.

Leonardo: Vista superior


El ATmega 32u4 cuenta con 12 entradas analógicas. Eso es más que los 6 A0 a A5 marcados en la placa. Esas entradas están disponibles como las líneas 4, 6, 8, 9, 10 y 12 y es posible usar los alias A6, A7, A8, A9, A10 y A11 para esas líneas.

¿Por qué no todas las líneas analógicas adicionales están marcadas como A6 a A11?Bueno, porque el Arduino original, con MCU en encapsulado PDIP, solo tenía 6 líneas analógicas y mantener el factor el pinout de la placa es importante para proporcionar la compatibilidad placa a placa.En otras palabras, Leonardo, con su MCU 32u4 es diferente del UNO con su 328 MCU, pero la placa está marcada para usar los mismos nombres de pin.

Arduino UNO: Vista superior


Las diferentes líneas de los puertos no están relacionadas con los nombres de pin estándar de Arduino. Esto se debe a que, para mantener la compatibilidad con el pinout de la placa, las líneas que pueden proporcionar señales PWM con la función analogWrite deben estar asociadas a las líneas 3, 5, 6, 9, 10, 11 y 13.En la placa UNO, la numeración de los pines es fácil, los pines digitales 0 a 7 van a las líneas 0 a 7 del puerto D, las líneas 8 a 13 van a las líneas 0 a 5 del puerto B y las analógicas 0 a 5 van a las líneas 0 a 5 del puerto C.En Leonardo, para mantener las cosas compatibles, hay un lío de líneas. Las líneas digitales de 0 en adelante están conectadas a PD2, PD3, PD1, PD0, PD4, PC6, PD7, PE6 ... Todo enredado.

La única manera de saber con la línea Arduino se relaciona con qué línea de puerto en Leonardo es usando una lista escrita ya que no hay distribución lógica como en Arduino UNO.
Si deseas ver qué línea va con qué, puedes usar la Hoja de cálculo de Arduino Leonardo que escribí en Google Drive disponible el siguiente enlace.


Variables y registros bit a bit

Cada puerto digital B, C, D, E y F tiene tres registros de 8 bits asociados. Es posible acceder a esos registros de forma similar al acceso de las variables de tipo byte. Solo hay que saber sus nombres. Cada registro tiene un bit asociado a cada línea del puerto. Entonces, por ejemplo, el bit 3 (con peso 2 ^ 3 = 8) está relacionado con la línea 3 del puerto. Es, en general, buena idea pensar en registros de puertos como conjuntos de 8 valores booleanos.

Si se desea establecer los bits 0, 4 y 6 de una variable de 8 bits, se pueden agregar sus pesos:

VARIABLE = 2^0 + 2^4 + 2^6;


Es decir:


VARIABLE = 1 + 16 + 64;


O, en un estilo más "C":


VARIABLE = 1|(1<<4)|(1<<6)


Para facilitar el cálculo de los pesos de los bits, se puede usar la macro bit () que está definida en Arduino.h. 
 
Usando esta macro se puede usar una asignación de bits más fácil:
 VARIABLE = bit(0) + bit(4) + bit(6);

Para establecer un bit de una variable,  se puede usar el operador OR bit a bit:


VARIABLE = VARIABLE | bit(3); // Establece el bit 3 de VARIABLE

Para borrar un bit de una variable, se puede usar los operadores bit AND y complemento ~.


VARIABLE = VARIABLE & (~bit(3)); // Borra el bit 3 de VARIABLE


Para facilitar el borrado y el conjunto de variables se pueden definir dos macros:


#define SET_FLAG(REGISTER,FLAG)     REGISTER|=(FLAG)
#define CLEAR_FLAG(REGISTER,FLAG)   REGISTER&=~(FLAG)


Entonces se puede usar:


SET_FLAG(VARIABLE, bit(3));     // Establece el bit 3 de VARIABLECLEAR_FLAG(VARIABLE, bit(3); // Borra el bit 3 de VARIABLE


También puede establecer varios bits al mismo tiempo:SET_FLAG(VARIABLE, bit(2) | bit(3));   // Establece los bits 2 y 3 de VARIABLE

CLEAR_FLAG(VARIABLE, bit(2) | bit(3));
// Borra los bits 2 y 3 de VARIABLE



Si se desea, es posible usar las macros bitRead, bitSet, bitClear y bitWrite definidas en Arduino.h:

 
Es importante saber cuáles son las macros Arduino, ya que se expanden en el lugar que se usan, debido a ello, no tienen la sobrecarga asociada a una llamada a función.
Observese que, utilizando las macros Arduino originales, no se debe usar la macro bit () en las llamadas ya que se ejecuta internamente a partir de los argumentos de las macros:

if (bitRead(VARIABLE, 3))
       {
       // Hacer algo si se establece el bit 3 de VARIABLE
       }
bitSet(VARIABLE, 3); // Establece el bit 3 de VARIABLE
bitClear(VARIABLE, 3); // Borra el bit 3 de VARIABLE
bitWrite(VARIABLE, 3, valor); // Establece o borra el bit 3 de
                              // VARIABLE dependiendo del valor

También se ha de tener en cuenta que no pueden usar las macros bitSet y bitClear para activar o borrar varios bits al mismo tiempo.





E / S digital de bajo nivel


Los tres registros asociados a cada puerto controlan la dirección del puerto (similar a pinMode) la salida del puerto (similar a digitalWrite) y la entrada del puerto (similar a digitalRead). Veamos el uno por uno.


Registros de dirección del puerto (DDRB, DDRC, DDRD, DDRE, DDRF)

 Esos registros indican, para cada línea, si es una entrada o una salida. Cada bit en el registro solo puede tener dos valores:

    "0" indica que la línea es una entrada
    
"1" indica que la línea es una salida

Así, por ejemplo, para establecer los pines Arduino 10 y 11, asociadas a PB6 y PB7 en modo de salida, puede escribir:

SET_FLAG(DDRB,bit(6)|bit(7));

De esta manera, la línea anterior es equivalente a:

pinMode(10,OUTPUT);
pinMode(11,OUTPUT);

Pero ocupa menos espacio del programa y requiere menos tiempo para ejecutarse.


Registros de salida del puerto (PORTB, PORTC, PORTD, PORTE, PORTF)

Cuando una línea de puerto está en modo de salida, mediante un registro DDRx, este registro mantiene el valor que se verá en la línea de salida.

    "0" indica un valor bajo (próximo a 0 voltios)
    
"1" indica un valor alto (junto a 5 voltios)


Por ejemplo:

SET_FLAG(PORTB, bit(6));


Es equivalente a:


digitalWrite(10, HIGH);


Los valores de salida altos y bajos se pueden obtener de la hoja de datos del ATmega 42u4:

Datos de continua ATmega 42u4



Se puede ver que el voltaje de salida de bajo nivel Vol se garantiza que está por debajo de 0.7V hasta un drenaje de 10mA de corriente cuando la alimentación es de 5V. De manera similar, se puede ver que se garantiza que el voltaje de alto nivel superará los 4.2 voltios hasta los 10 mA de corriente.
Todo lo anterior es válido para puertos configurados en modo salida, sin embargo, cuando una línea de puerto está en modo de entrada, el registro de PORTx indica si el puerto tendrá o no una resistencia de pull-up.
    "0" indica que no se usará pull-up
    
"1" indica que se utilizará un pull-up


Es importante tener en cuenta que cuando configura un puerto desde la salida al modo de entrada, el valor anterior en el Registro de salida de puerto determina si tendrá o no un pull-up.

Datos de la resistencia de Pull-Up

Como se puede ver en los datos obtenidos de la hoja de datos del MCU, las resistencias Pull-Up en los puertos de E/S tienen un valor garantizado entre 20kOhm y 50kOhm.


De esa manera:

CLEAR_FLAG(DDRB, bit(6));CLEAR_FLAG(PORTB, bit(6));

Configura el pin 10 de Arduino, asociado a PB6, en modo de entrada.Y es equivalente a:

pinMode (10, INPUT);

 y
 
CLEAR_FLAG(DDRB, bit(6));SET_FLAG(PORTB, bit(6));
Configura el pin 10 de Arduino, en modo de entrada con una resistencia pull-up a Vdd.Y es equivalente a:

pinMode (10, INPUT_PULLUP);

Registros de entrada de puerto (PINB, PINC, PIND, PINE, PINF)

Este registro siempre devuelve, cuando se lee, el estado lógico en el pin del puerto relacionado. Esto es independiente de la configuración de Entrada o Salida del puerto.

Este registro normalmente se lee, para los pines de entrada, con objeto de determinar el estado de las líneas relacionadas. Pero también se puede leer, para los pines de salida, para verificar que ningún elemento externo esté forzando un estado lógico diferente al que configuramos.

Cada bit indica:


    "0" Si la línea relacionada está en nivel bajo (Voltaje inferior a Vil_max)
    
"1" Si la línea relacionada está en un nivel alto (Voltaje superior a Vih_min)


Por ejemplo:



if (PINB & bit(6)) {  // Hacer algo
                   };

Es equivalente a:

if (digitalRead(10)) {// Hacer algo
                     
};



Es posible encontrar los umbrales Vil_max y Vih_min en la tabla que se muestra anteriormente para la hoja de datos ATmega 32u4. Como se puede ver, son:

    
Vil max = 0,2 Vcc - 0,1 V = 0,9 V         para  Vcc = 5 V
    
Voh min = 0,2 Vcc + 0,9 V = 1,9 V       para  Vcc = 5 V

Escribir en los registros PINx no tiene relación con leerlos. De hecho, escribir en un bit de registro PINx en "1" afecta al bit PORTx del Registro de salida correspondiente alternando su valor. Escribir un  "0" no tiene ningún efecto.

Por ejemplo, la línea:

PINB = BIT (6); // Cambia el bit 6 de PORTB

Es mucho más rápido que la línea equivalente:

digitalWrite (10,! digitalRead(10)); // Cambia el pin 10 de Arduino


El siguiente código implementa dos funciones: highLevelToggle y lowLevelTogge. El primero usa llamadas Arduino estándar y el segundo utiliza accesos a puertos de bajo nivel.


Al probar las funciones, se puede ver que el nivel alto da una frecuencia de salida de aproximadamente 43 kHz, mientras que el nivel bajo da una frecuencia de aproximadamente 2,5 MHz. 


La función de bajo nivel es 58 veces más rápida que la de alto nivel, por lo tanto, cuando es necesario obtener toda la velocidad disponible de un MCU, nada supera la programación de bajo nivel. El siguiente paso será usar ensamblador, pero la ganancia no será tan grande en este caso.   

 

Código en Github (11/02/2018)


El código está ahora disponible en Github:

https://github.com/R6500/Leonardo/blob/master/fastToggleTest.ino

No hay comentarios:

Publicar un comentario