sábado, 8 de junio de 2013

PWM Síncrono con PCA9685 | Synchronous PWM with PCA9685

Gracias al auge de la iluminación LED han salido al mercado circuitos integrados capaces de generar varias salidas PWM. La mayoría de ellos son escalables permitiendo generar múltiples señales con un único bus de datos.

Por ejemplo el TLC5940, que cuenta con 16 salidas de 12 bits de resolución (4096 pasos). El problema que encontramos con este integrado es que cada una de las salidas tiene un delay de 20ns con respecto a la anterior (Pag. 14), así entre la salida 0 y la 15 hay 300ns de delay.

Otro integrado interesante es el PCA9685 que, al igual que el anterior, dispone de 16 salidas de 12 bits de resolución, y a diferencia del anterior, son sincrónicas. A demás este integrado se controla mediante el bus de datos I2C. El único problema que tiene es que la frecuencia máxima de señal utilizando el oscilador interno (25MHz) es de 1KHz. Usando un oscilador externo de 50MHz (máximo permitido), obtendríamos una frecuencia de 12KHz aproximadamente (Pag. 13).

     50Mhz / 4096 = 12207Hz
    

Hay otro de la misma familia que el anterior, se trata del PCA9635. Este cuenta con 16 salidas de 8 bits de resolución (256 pasos), son sincrónicas y con una frecuencia de 97KHz.

Para la prueba de hoy he comprado el segundo (PCA9685), y usando el oscilador interno, a una frecuencia de 1KHz he obtenido buenos resultados, lástima del zumbido audible que produce esta frecuencia en el motor.
With the rise of LED lighting, have arrived to market integrated circuits capable of generating several PWM outputs. Most of them are scalable allowing multiple signals on a single data bus.

For example TLC5940, which has 16 outputs 12-bit resolution (4096 steps). The problem we found with this chip is that each output has a delay of 20ns respect to previous output (p. 14), and there is 300ns delay between outputs 0 and 15.

Another interesting IC is PCA9685 which, as the previous one, has 16 outputs 12-bits resolution, but the outputs are synchronous. This integrated is controlled by I2C data bus. The only problem I have is that the maximum signal frequency using the internal oscillator (25MHz) is 1KHz. Using a 50MHz external oscillator (maximum allowed), we can obtain a frequency of about 12KHz (p. 13).

    50Mhz / 4096 = 12207Hz
    

Another of the same family as above, it's PCA9635. It has 16 outputs 8-bit resolution (256 steps), are synchronous with a frequency of 97KHz.

For testing today I bought the second one (PCA9685), and using the internal oscillator at a frequency of 1kHz I had good results, shame about the audible hum frequency produced by motor.



La verdad, pensaba que costaría más soldar los pins del encapsulado TSSOP28, pero no ha sido complicado, internet está lleno de manuales de cómo hacerlo. Aquí hay algunos interesantes de Sparkfun:

Surface Mount Soldering Tutorials:
https://www.sparkfun.com/tutorials/category/2

El siguiente esquema muestra la conexión entre PCA9685 y Arduino Nano. He usado también 2 transistores Darlington BD679 y la fuente de alimentación de Märklin Mini-Club. El potenciómetro de la fuente hay que ponerlo al máximo hacia la derecha, el conector rojo es el positivo y el marrón el negativo.
Actually, I thought it would be hard to solder TSSOP28 package pins, but it was not complicated, internet is full of how-to manuals. Here's an interesting one from Sparkfun:

Surface Mount Soldering Tutorials
https://www.sparkfun.com/tutorials/96

The following scheme shows the connections between PCA9685 and Arduino Nano. I used also 2 Darlington transistors BD679 and a Märklin Mini-Club power supply. The supply potentiometer must be put to maximum clockwise, the red connector is the positive and brown is the negative.



#include <Wire.h>

#define MODE1     0x00
#define MODE2     0x01
#define LED0      0x06
#define LED_SIZE  0x04
#define PRE_SCALE 0xFE

byte _address = B01000000;

void setup()
{
  
  Wire.begin();

  SetReg(MODE1, B00001001); // Sleep
  SetReg(PRE_SCALE, 0);     // Prescale. Minimum is 3 (Pag. 24)
  SetReg(MODE1, B00000001); // Reset
  
  SetReg(MODE1, B10100001); // Auto-Increment
  SetReg(MODE2, B00000100); // Totem Pole

  for(int i=0; i<16; i++)
    SetPwm(i, 700);

}

void loop()
{}

void SetReg(byte reg, byte data)
{
  Wire.beginTransmission(_address);  // Address
  Wire.write(reg);                   // Register
  Wire.write(data);                  // Data
  Wire.endTransmission();
}

void SetPwm(int led, unsigned int pwm)
{
  Wire.beginTransmission(_address);    // Address
  Wire.write(LED0 + (led * LED_SIZE)); // Output
  Wire.write(0);                       // LEDn_ON_L
  Wire.write(0);                       // LEDn_ON_H
  Wire.write(pwm);                     // LEDn_OFF_L
  Wire.write(pwm>>8);                  // LEDn_OFF_H
  Wire.endTransmission();
}



Las flechas señalan los puntos de aislamiento
Arrows indicates the isolation points






Descargas:
Downloads:

ArduinoPCA9685.zip




Enlaces:
Links:

TLC5940 (EN): http://www.ti.com/lit/ds/symlink/tlc5940.pdf
PCA9685 (EN): http://www.nxp.com/documents/data_sheet/PCA9685.pdf
PCA9635 (EN): http://www.nxp.com/documents/data_sheet/PCA9635.pdf
I2C (ES): http://es.wikipedia.org/wiki/I2C
I2C (EN): http://en.wikipedia.org/wiki/I%C2%B2C
Surface Mount Soldering Tutorials (EN): https://www.sparkfun.com/tutorials/category/2




jueves, 30 de mayo de 2013

PWM síncrono | Synchronous PWM

Ya sabemos cómo controlar la velocidad y el sentido de la marcha de un tren con Arduino, ahora vamos a ver cómo aplicarlo a una maqueta analógica. El principal problema que se nos presenta es el de las transiciones entre cantones de vía alimentados con diferentes fuentes PWM. No voy a extenderme explicándolo ya que hay dos blogs que lo hacen estupendamente, mejor de lo que yo lo haría.

El primero es “AGP SchwarzWaldBahn 220” de A. G. Pino, dónde plantea el problema que ocurre cuando un tren toma la corriente de dos tramos de vía alimentados cada uno por una fuente PWM diferente:

El segundo es “Mi Maqueta Marklin Z”de Ignacio de la Fuente, donde explica una posible solución al problema planteado:
    PWM (y III): Solución bidireccional.
    Cab Control: Varios reguladores que pueden asignarse a cualquier cantón aislado del circuito.

Bueno, pues yo propongo otra solución. Una solución que sólo funciona en teoría pero no en la práctica ya que el ancho de los pulsos PWM dependerá de la tolerancia de los componentes utilizados.

Mi idea es la de sincronizar un número n de generadores PWM alineados a la izquierda. ¿Y cómo se consigue esto? Pues muy sencillo, con una señal de reloj y n generadores de un único pulso de una duración menor o igual que la frecuencia de dicho reloj. Veamos la siguiente imagen:
We know how to control the speed and direction of a train with Arduino, now let's see how to apply it to an analog layout. The main issue we have to overcome are transitions between isolated blocks of track using different PWM sources. I will not dwell explaining it as there are two blogs that do this great, better than I would do (Sorry, these blogs are only in spanish)

The first is “AGP Schwarzwaldbahn 220” by A. G. Pino, where the problem happens when a train is powered from two different sections of track, each with a different PWM source:

The second one is “Mi Maqueta Marklin Z” by Ignacio de la Fuente, explaining a possible solution to the problem:

Well, I propose another solution. A solution which works only in theory but not in practice since the PWM pulse widths depend on the tolerances of the components used.

My idea is to synchronize n left-aligned PWM sources. And how is this achieved? Very simple, with a clock signal and n single pulse sources with a pulse duration less or equal than clock frequency.
Consider the following image:


Teoría de PWM síncrono
Synchronous PWM theory

Cada una de las fuentes de pulso genera un único pulso cada vez que el reloj pasa de estado alto a bajo. Si el reloj es constante, se generarán pulsos constantes. Para construir estos circuitos se emplea el circuito integrado 555 en configuración monoestable y para el reloj una salida PWM de Arduino con un valor de entre 240 y 250 (sobre 255). Para evitar disparos erróneos se coloca al final del reloj una resistencia pullup de 10K.
Each of the pulse sources produces a single pulse each time clock goes from high to low. If clock is constant, constant pulses are generated. To make these circuits we have to use 555 ic in monostable configuration and an Arduino PWM output for generating clock, with a PWM value of between 240 and 250 (about 255). To prevent erroneous triggering, a 10K pullup resistor must be placed at the end of the clock line.

555 monoestable
Monostable 555
La frecuencia que he elegido para el reloj es de 62500Hz, o lo que es lo mismo un pulso cada 16us. En Arduino UNO, por ejemplo, esta frecuencia sólo se puede conseguir con los pins 5 y 6 que son los que están conectados al TIMER0.

Para calcular los valores de los componentes en configuración monoestable se utiliza la siguiente función:
I've chosen 62500Hz as clock frequency, or a pulse every 16us. In Arduino UNO, for example, this frequency can only be achieved using pins 5 and 6, which are currently connected to TIMER0.

Use the following function in order to calculate the values for the components in monostable configuration:
    T = 1.1 * R * C
Dónde los valores son en unidades:
    T en segundos. R en ohmios. C en faradios.
Where the values are in units:
    T in seconds. R in ohms. C in farads.
Para obtener 16us usando un potenciómetro de 10K tenemos que:
To obtain a 16us pulse using a 10K potentiometer we have:
    16us = 1.1 * 10K * C C = 16us / (1.1 * 10K) C = 1.45e-9 C ~ 1500pF
Resolviendo T con C = 1500pF:
Solving T with C = 1500pF:
    T = 1.1 * 10K * 1500pF T = 0.0000165s T = 16.5us
Obtenemos un tiempo superior al del reloj, por lo tanto el valor máximo que usaremos en el potenciómetro será:
We get a time higher than the clock time, therefore the maximum value that we will use in the pot will be:
    R = T / (1.1 * C) R = 16us / (1.1 * 1500pF) R ~ 9K70
A partir del 97% del potenciómetro tendremos una señal PWM del 100%.

Para la primera prueba he utilizado 3 circuitos monoestables con resistencias fijas de 4K7 para obtener aproximadamente la mitad del ciclo PWM en las tres salidas:
With a pot value upper than 97% we will have a 100% PWM signal.

For the first test I used three monostable circuit with 4K7 fixed resistors, to get about half of the PWM cycle at all three outputs:





Se puede observar como el inicio del pulso de las tres señales está perfectamente sincronizado, no ocurre lo mismo con el final, ya que ni las tres resistencias ni los tres condensadores son exactamente iguales entre sí. Las resistencias tienen una tolerancia de 5%, por lo tanto sus valores pueden variar entre 4465 y 4935 ohmios. Los condensadores que he utilizado también tienen una tolerancia del 5%, por lo tanto sus valores oscilan entre 1425 y 1757 picofaradios.

Para la segunda prueba he sustituido las resistencias fijas de 4K7 por un potenciómetro digital de 10K de Microchip, el MCP4131.
It can be seen that the pulse start of all three signals are perfectly synchronized, not so with the signal end, as neither the three resistors and three capacitors are exactly equal. The resistors have a tolerance of 5%, so their values may vary between 4465 and 4935 ohms. The capacitors I've used also have a tolerance of 5%, so their values range are between 1425 and 1757 pF.

For the second test I replaced the 4K7 fixed resistors by a digital 10K potentiometer from Microchip, the MCP4131.






Se puede apreciar una mayor diferencia en la salida ya que estos potenciómetros tienen una tolerancia del 20%.

Como ya he dicho antes, esta es una solución teórica, una solución que sólo funcionaría si los componentes tuviesen tolerancia 0%.

Para finalizar, me gustaría recomendar la lectura de los dos blogs a los que he hecho referencia al inicio de este artículo:

We can see a major difference at the outputs, as these potentiometers have a tolerance of 20%.

As I said before, this is a theoretical solution, a solution that would only work if all of the components had a tolerance of 0%.

Finally, I'd recommend reading the two blogs to which I referred at the beginning of this article:






Descargas:
Downloads:

TestFixRes.zip

TestMCP4131.zip





Enlaces:
Links:

Cantón ferroviario (ES): http://es.wikipedia.org/wiki/Cantón_ferroviario

Model RR blocks (EN): http://modeltrains.about.com/od/bmodelrailroadterms/g/block.htm

Circuito integrado 555 (ES): http://es.wikipedia.org/wiki/Circuito_integrado_555

555 integrated circuit (EN): http://en.wikipedia.org/wiki/555_IC

NE555 (EN): http://www.ti.com/lit/ds/symlink/ne555.pdf

MCP4131 (EN): http://ww1.microchip.com/downloads/en/DeviceDoc/22060b.pdf


AGP SchwarzWaldBahn 220 (ES): http://agp-schwarzwaldbahn.blogspot.com.es
Mi Maqueta Marklin Z (ES): http://mimaquetaz.blogspot.com.es




domingo, 19 de mayo de 2013

Mejorando la lectura analógica | Improving analog reading

En la anterior versión del analizador lógico cometí un error, deshabilitaba las interrupciones mientras leía el puerto analógico, esto producía errores en el reloj encargado de calcular el tiempo total.
In the previous version of the logic analyzer I made a mistake, I disabled the interrupts while reading the analog port, it causes errors in the clock that calculates the total time.


...

Serial.end();
noInterrupts();  
BeginRead();
interrupts();
Serial.begin(115200);
Serial.write(_data, DATA);

...

void ReadAnalog()
{

  t = micros();
  for(i=0; i<DATA; i+=2) // 2 cycles
  {

    /*** From wiring_analog.c ***/

    // start the conversion
    sbi(ADCSRA, ADSC); // 2 Cycles

    // ADSC is cleared when the conversion finishes
    while (bit_is_set(ADCSRA, ADSC)); // 13 * 16 Cycles

    // we have to read ADCL first; doing so locks both ADCL
    // and ADCH until ADCH is read.  reading ADCL second would
    // cause the results of each conversion to be discarded,
    // as ADCL and ADCH would be locked when it completed.
    _data[i+1] = ADCL; // 2 cycles
    _data[i]   = ADCH; // 1 cycle

    /****************************/
    
  }
  
  t = micros() - t;
  _data[DATA-4] = (byte)(t >> 24);
  _data[DATA-3] = (byte)(t >> 16);
  _data[DATA-2] = (byte)(t >> 8);
  _data[DATA-1] = (byte)(t);


}


Se puede calcular el error de la siguiente manera:

Tiempo de cada iteración:

    for: 2 ciclos
    sbi: 2 ciclos
    leer analógico: 208 ciclos (13 ciclos ADC * 16 prescaler)
    leer ADCL : 2 ciclos (1 para incrementar el índice y 1 para asignar al array)
    leer ADCH: 1 ciclo
    Total: 215 ciclos

Teníamos 898 iteraciones:


    898 iteraciones * 215 ciclos = 193070 ciclos
    193070 ciclos * 62.5 ns = 12.07 ms


Al deshabilitar las interrupciones y calcular el tiempo con la función "micros()" obteníamos aproximadamente 1200 us, unas mil veces menos de lo esperado.

Así que al final, he decidido no calcular el tiempo y leer 900 veces (1800 bytes), y actuar de la misma forma que en la lectura digital.
Error can be calculated as follows:

Time per iteration:

    for: 2 cycles
    sbi: 2 cycles
    analog read: 208 cycles (13 ADC cycles * 16 prescaler)
    read ADCL: 2 cycles (1 for increasing index and 1 to assign the array)
    read ADCH: 1 cycle
    Total: 215 cycles


We had 898 iterations:

    898 iterations * 215 cycles = 193070 cycles
    193070 cycles * 62.5 ns = 12.07 ms


By disabling interrupts and calculate the time using "micros()" we're getting about 1200 us, a thousand times less than expected.

So in the end, I decided not calculate the time and read 900 times (1800 bytes), and act at the same way as digital reading.





Descargas:
Downloads:

LogicAnalyzer.zip



miércoles, 8 de mayo de 2013

Analizador lógico (analógico) | Logic analyzer (analog)

Para leer un canal analógico la cosa cambia. No podemos leer el puerto completo, tenemos que leer un pin analógico y esperar a que el microcontrolador haga la conversión del voltaje de entrada a un valor numérico legible. Por defecto, Arduino utiliza el prescaler del reloj del conversor analógico-digital con un valor de 128, que significa que el reloj ADC trabajará a una frecuencia de 125KHz.

    16 MHz / 128 = 125 KHz

Esto lo podemos ver en el archivo "Arduino \hardware\arduino\cores\arduino\wiring.c"
To read an analog channel things change. We can't read the full port, we have to read an analog pin and wait for the microcontroller do the conversion of the input voltage to a numerical value. By default, Arduino uses ADC clock prescaler with a value of 128, which means that the ADC clock work at a frequency of 125KHz.

    16 MHz / 128 = 125 KHz

This can be seen in the "Arduino\hardware\arduino\cores\arduino\wiring.c"

#if defined(ADCSRA)
 // set a2d prescale factor to 128
 // 16 MHz / 128 = 125 KHz, inside the desired 50-200 KHz range.
 // XXX: this will not work properly for other clock speeds, and
 // this code should use F_CPU to determine the prescale factor.
 sbi(ADCSRA, ADPS2);
 sbi(ADCSRA, ADPS1);
 sbi(ADCSRA, ADPS0);

 // enable a2d conversions
 sbi(ADCSRA, ADEN);
#endif


Si miramos el datasheet de ATmega328, en la página 263, en el punto 23.9.2 podemos ver la siguiente tabla:
Looking at ATmega328 datasheet, on page 263, in section 23.9.2 we can see the following table:


7 6 5 4 3 2 1 0
ADEN ADSC ADATE ADIF ADIE ADPS2 ADPS1 ADPS0


Donde ADPSx son los 3 bits que corresponden al valor del prescaler del reloj. En la página siguiente (264), en la tabla 23-5, podemos ver qué valor debemos asignar a cada bit para establecer el factor de división:
Where ADPSx are 3 bits corresponding to the clock prescaler value. On the next page (264), Table 23-5, we can see what value we have to assign each bit to set the division factor:


ADPS2 ADPS1 ADPS0 Division factor
0 0 0 2
0 0 1 2
0 1 0 4
0 1 1 8
1 0 0 16
1 0 1 32
1 1 0 64
1 1 1 128


Una lectura ADC tarda aproximadamente 13 ciclos (pag 263), necesitamos un valor de prescaler que nos asegure velocidad a la vez que fiabilidad en el resultado. Si usamos un prescaler de 16, tenemos 76923 lecturas por segundo:
ADC read takes about 13 cycles (p 263), we need a prescaler value that assures us speed and reliability in the result. If we use a prescaler of 16, we have 76923 scans per second:


    16000000 / 16 = 1000000
    1000000 / 13 = 76923

 sbi(ADCSRA, ADPS2);
 cbi(ADCSRA, ADPS1);
 cbi(ADCSRA, ADPS0);


El tiempo total de la ejecución de lectura puede ser variable, así que lo mejor es leer el tiempo total y enviarlo también por puerto serie. Como en el modo digital hemos utilizado un buffer de 1800 bytes y sabiendo que la lectura analógica retorna un valor entero de 2 bytes y que el tiempo en microsegundos es un entero largo sin signo, sabemos que podremos llegar a leer 898 valores:
The total execution time for reading can be variable, so the best is to read the total spend time and send it also via serial port. As digital mode we used a buffer of 1800 bytes and knowing that analog reading returns a 2-byte integer value and the time in microseconds is an unsigned long, we know we can get to read 898 values:


    1800 - 4 = 1796
    1796 / 2 = 898


#define DATA 1800
#define DATAA 1796

byte _data[DATA];
unsigned long t;

void setup()
{

  sbi(ADCSRA, ADPS2);
  cbi(ADCSRA, ADPS1);
  cbi(ADCSRA, ADPS0);

  // DEFAULT Analog reference + A0 input pin
  ADMUX = (DEFAULT << 6); // | (pin & 0x07);

}

void ReadAnalog()
{

  t = micros();

  for(i=0; i<DATAA; i+=2)
  {

    /*** From wiring_analog.c ***/

    // start the conversion
    sbi(ADCSRA, ADSC);

    // ADSC is cleared when the conversion finishes
    while (bit_is_set(ADCSRA, ADSC));

    // we have to read ADCL first; doing so locks both ADCL
    // and ADCH until ADCH is read.  reading ADCL second would
    // cause the results of each conversion to be discarded,
    // as ADCL and ADCH would be locked when it completed.
    _data[i+1] = ADCL;
    _data[i]   = ADCH;

    /****************************/
    
  }

  t = micros() - t;
  _data[DATA-4] = (byte)(t >> 24);
  _data[DATA-3] = (byte)(t >> 16);
  _data[DATA-2] = (byte)(t >> 8);
  _data[DATA-1] = (byte)(t);

}







PRUEBAS:
TESTS:


Al igual que en el artículo anterior, he programado otra placa Arduino para que genere los datos a leer. Se trata de una onda sinusoidal que oscila entre 0 y 5V.

Si miramos el enlace de la Wikipedia podemos ver la siguiente expresión. Tomamos la fase inicial como 0 (φ). Sabemos que si utilizamos una salida PWM para generar la onda podemos representar 256 valores de 0V a 5V. Así que la amplitud (A) corresponderá a la mitad, 128 (2.5V) y el periodo (T) a 255:
As in the previous article, I have programed another Arduino to generate the data to read. It is a sine wave ranging between 0 and 5V.

If we look at the Wikipedia link we can see the following expression. We take the initial phase as 0 (φ). We know that if we use a PWM output to generate the wave we can represent 256 values from 0V to 5V. So that the amplitude (A) correspond to half, 128 (2.5V) and the period (T) of 255:





Ahora tenemos un valor entre -2.5V y 2.5V, para obtener un valor entre 0V y 5V debemos sumar 2.5V al resultado final. Sabemos que el valor numérico en bytes para 2.5V es 128.
Now we have a value between -2.5V and 2.5V, to obtain a value between 0V and 5V, must add 2.5V to the final result. We know that the numeric value in bytes is 128 for 2.5V.




Para convertir la señal PWM en una señal analógica se utiliza un filtro RC, yo he elegido unos valores un poco a ojo, 1K y 0.47uF.
To convert the PWM signal into an analog signal, must use an RC filter, I have chosen values a little by eye, 1K and 0.47uF.


Ejemplo de conexiones
Example of connections

Esquema
Schematic

Onda sinusoidal
Sene wave
Cambiando 1K por 10K
Changing 1K for 10K





Descargas:
Downloads:

LogicAnalyzer.zip




Enlaces:
Links:

8-bit AVR® Instruction Set (EN): http://www.atmel.com/Images/doc0856.pdf
ATmega 328 (EN): http://www.atmel.com/Images/doc8161.pdf
Sinusoide (ES): https://es.wikipedia.org/wiki/Sinusoide
Sine wave (EN): http://en.wikipedia.org/wiki/Sine_wave




lunes, 29 de abril de 2013

Analizador lógico (digital) | Logic analyzer (digital)

Una buena herramienta para trabajar con la lógica digital es el analizador lógico. Como necesito uno y ahora no es un buen momento para gastar dinero he decidido construir uno casero con Arduino.

Lo primero que necesito es saber cuántas lecturas puedo llegar a hacer en un determinado tiempo. Lo más fácil sería leer el estado de un pin de entrada y enviarlo por el puerto serie y luego controlar el tiempo de lectura desde un software de PC, pero esto sería demasiado lento y podríamos perder muchos datos. Lo óptimo sería leer un determinado número de veces, guardarlo en la memoria SRAM de Arduino y luego volcarlo todo al puerto serie. Sabiendo el tiempo empleado para leer una vez, podemos calcular la gráfica a pintar en pantalla. La forma más precisa es trabajar con instrucciones de ensamblador. Para ello hay que echar un vistazo al set de instrucciones de los chips ATmega y determinar que instrucciones usar y los ciclos de reloj que consume cada una de estas instrucciones. Además, si queremos leer varios pines a la vez, debemos acceder a ellos mediante los puertos internos del microcontrolador. Si utilizamos un Arduino UNO o un Arduino Nano (como en mi caso) tendremos el siguiente mapeado de puertos:
A good tool for working with digital logic is the logic analyzer. As I need one and now is not a good time to spend money I decided to build a homemade one using Arduino.

The first thing I need to know is how many readings I can get in a determinate time. The easiest way would be to read the state of an input pin and send it via the serial port and then control the reading time from a PC software, but this would be too slow and I could lose a lot of data. The optimum would be to read a certain number of times, save it in Arduino SRAM and then bounce everything to the serial port. Knowing the time used to read one time, we can calculate the graph displayed on the screen. The most accurate way is to work with assember instructions. This requires taking a look at the instruction set of the ATmega chip and determining which instructions to use and how many clock cycles consume each of these instructions. Furthermore, if we read several pins at once, we have to access to the internal ports of the microcontroller. If we use an Arduino UNO or Arduino Nano (as in my case) we have the following port mapping:



BIT 7 6 5 4 3 2 1 0
PORTB XTAL XTAL 13 12 11 10 9 8
PORTC NC RST A5 A4 A3 A2 A1 A0
PORTD 7 6 5 4 3 2 TX 1 RX 0



Si utilizamos el puerto B para leer las entradas, debemos utilizar los bits 0..5 ya que los bits 6 y 7 están conectados al cristal. Si utilizamos el puerto C, usaremos los bits 0..5 ya que en el bit 6 está conectado el reset y el bit 7 no está conectado. Y si usamos el puerto D, usaremos los bits 2..7 porque los bits 0 y 1 se utilizan para la comunicación por el puerto serie.

Yo, concretamente, he decidido utilizar el puerto B para hacer las lecturas de lógica, así que podré leer 6 canales.

Para leer los 8 bits de un puerto con Arduino utilizamos la siguiente instrucción:
    byte b = PINB;

Para leerlos con instrucciones a bajo nivel (ASM) lo leemos de la siguiente forma:
    IN Rd, A

Dónde Rd es el registro de memoria de destino y A es el puerto de entrada. Esta instrucción consume un ciclo de reloj (Pag 13).

Para guardar la lectura en memoria y pasar a la siguiente posición de memoria usaremos la siguiente instrucción:
    ST z+, Rr

Dónde z es la posición de memoria donde vamos a guardar el dato y Rr es el registro donde previamente hemos leído el puerto de entrada. ST consume un ciclo de reloj y el incremento de posición de memoria otro ciclo más.
Así, tenemos el siguiente bloque de código ensamblador:
    in r0, A
    st z+,r0

A parte de esto tenemos que saber de cuanta memoria SRAM disponemos para almacenar lecturas. ATmega 328 dispone de 2Kb (2048 bytes), de los cuales una cantidad la consume el bootloader de Arduino. Haciendo unas pruebas he visto que puedo utilizar un buffer de 1800 bytes. Si sabemos que estamos consumiendo 3 ciclos por cada lectura podemos deducir lo siguiente:

If we use the port B to read inputs, we use the bits 0..5 as bits 6 and 7 are connected to the xtal. If we use the port C, we will use the bits 0..5 as in the bit 6 is connected to the reset and bit 7 isn't connected. And if we use the port D, we use bits 2..7 because bits 0 and 1 are used for serial port communication.

I, specifically, I decided to use port B to make logical readings, so I can read 6 channels.

To read the 8 bits of a port with Arduino we use the following statement:
    byte b = PINB;

To read with low-level instructions (ASM) we read as follows:
    IN Rd, A

Where Rd is the destination memory register and A is the input port. This instruction consumes one clock cycle (Pag 13).
To save the reading in memory and move to the next memory location we will use the following instruction:
    ST z+, Rr

Where z is the memory location where we will store the data and Rr is the register where we have already read the input port. ST consumes one clock cycle and increasing memory location another cycle.
Thus, we have the following assembler code block:
    in r0, A
    st z+,r0

Apart from this we have to know how much SRAM have to store readings. ATmega328 has 2K (2048 bytes), of which consumes an amount Arduino Bootloader. After doing some tests I have seen that I can use a buffer of 1800 bytes. If we know that we are consuming 3 cycles for each reading we can deduce the following:

    16 MHz / 3 Cycles = 5.33 MHz
    3 Cycles * 1800 Times = 5400 Cycles
    16 MHz = 62.5 ns per cycle
    5400 Cycles * 62.5 ns = 337.5 us

Tenemos una frecuencia de lectura de 5.33 Mhz. El tiempo total de lectura es de 337.5 us.

Para hacer las 1800 lecturas he decidido no utilizar un bucle porque perdería tiempo incrementando variables y saltando al inicio del bucle en cada vuelta.

We have a 5.33 MHz reading frequency. The total reading time is 337.5 us.

To make 1800 readings I decided not to use a loop, because it can make lose time increasing the index and looping back.



#define DATA 1800

byte _data[DATA];

void ReadDigital()
{
  __asm__ __volatile(
    "in r0,%[port]\n\t"
    "st z+,r0\n\t"            // 1
    "in r0,%[port]\n\t"
    "st z+,r0\n\t"            // 2
.
.
.
    "in r0,%[port]\n\t"
    "st z+,r0\n\t"            // 1800
    :
    : [port] "i" (_SFR_IO_ADDR(PINB)), "z" (_data)
    :
  );
}






PRUEBAS:
TESTS:


He programado también una aplicación en VB.Net 4 para simular por pantalla los datos recogidos por el analizador. Más adelante, cuando esté perfeccionado, subiré el código, por ahora he subido el ejecutable.

Para probar el funcionamiento del analizador, he programado otra placa Arduino como un generador de señales PWM, de forma que cada una de las salidas genera una señal PWM del 50% a su máxima frecuencia. Pins 3, 9, 10 y 11 a 31250 Hz y pins 5 y 6 a 62500 Hz. He conectado estas salidas a los pins de entrada del analizador y este ha sido el resultado:
I have also programmed an application in VB.Net 4 to simulate on screen the data collected by the analyzer. Later, when it is perfected, I'll upload the code, now I uploaded the executable.

To test the performance of the analyzer, I have programmed another Arduino board as a PWM signal generator, so that each of the outputs generates a PWM signal of 50% at maximum frequency. Pins 3, 9, 10 and 11 are 31250 Hz and pins 5 and 6 are 62500 Hz. I connected these to the analyzer input pins and this is the result:


Ejemplo de conexiones
Example of connections

Esquema
Schematic

6 Señales PWM
6 PWM signals



#include "PwmFrequency.h"

int pins[] = {3, 5, 6, 9, 10, 11};

void setup()
{
  
  for(int i=0; i<6; i++)
  {
    setPwmFrequency(pins[i], 1);
    pinMode(pins[i], OUTPUT);
    analogWrite(pins[i], 127);
  }

}

void loop()
{}







Descargas:
Downloads:


LogicAnalyzer.zip




Enlaces:
Links:


Analizador lógico (ES): http://es.wikipedia.org/wiki/Analizador_lógico
Logic analyzer (EN): http://en.wikipedia.org/wiki/Logic_analyzer
8-bit AVR® Instruction Set (EN): http://www.atmel.com/Images/doc0856.pdf
Arduino registro PORT (ES): http://arduino.cc/es/Reference/PortManipulation
Arduino port registers (EN): http://arduino.cc/en/Reference/PortManipulation
ATmega port manipulation (EN): http://playground.arduino.cc/Learning/PortManipulation