Atenuación de LED multiplexados

Estoy multiplexando 32 LED en una configuración 4: 8 en un ATMega328 y estoy tratando de atenuarlos con lo que probablemente sea una comprensión completamente ingenua de PWM. Nota: los estoy multiplexando directamente con 12 pines de ATMega, no se usan otros chips en absoluto. Básicamente, de cada 10 ciclos de actualización de toda la pantalla (lo que toma 8 de la interrupción ISR), solo apago todo para 2 de ellos si quiero mostrarlo con un brillo del 80%. Pero, en la práctica, esto solo me da un montón de LED pulsantes.

Al principio pensé que simplemente no estaba llamando a ISR lo suficiente, así que cambié la preescala de 8 a 1, pero eso no cambió.

El código aproximado está debajo. Cualquier sugerencia apreciada. Estoy seguro de que estoy haciendo esto COMPLETAMENTE de manera incorrecta: P La multiplexación funciona bien y creo que obtengo una frecuencia de actualización de pantalla completa de 244 Hz, que parecía lo suficientemente buena. Parece que no puede atenuarse.

Nota: la variable "datos" se cambia en el método loop() (que no se muestra).

uint8_t pwmStep = 0;
uint8_t pwmMax = 10;
float pwmLevel = 0.8f; //range 0.5 - 1.0 (50% - 100%)

void setup()
{
  //port inits and other stuff here...

  // timer 1 setup, prescaler 8
  TCCR1B |= (1 << CS10);

  // enable timer 1 interrupt
  TIMSK1 = _BV(TOIE1);
}

uint8_t col, row = 0;
ISR(TIMER1_OVF_vect) 
{
  //Turn all columns off 
  PORTB |= (_BV(PB0) | _BV(PB1) | _BV(PB2) | _BV(PB3) | _BV(PB4) | _BV(PB5));
  PORTD |= (_BV(PD6) | _BV(PD7));

  if(pwmStep < pwmLevel*pwmMax)
  {
    //set the 4 rows
    for(row=0; row<4; row++)
    {
      if(data & (1UL << (row + (col * 4))))
        PORTC |= _BV(row);
      else
        PORTC &= ~_BV(row);
    } 

    //Enable the current column
    if(col < 6)
      PORTB &= ~_BV(col);
    else
      PORTD &= ~_BV(col);
  }
  col++;
  if(col == 8)
  { 
    col = 0;
    pwmStep++;
    if(pwmStep == pwmMax)
      pwmStep = 0;
  }
}

* Actualización: * Intentando hacer esto con un ATTiny85, pero sin obtener los resultados que sospechaba. Lo atenúa, pero solo en una cantidad muy pequeña.

Vea el código a continuación. Incluso con OCR0B configurado en MUY pequeño, solo obtengo una pequeña cantidad de atenuación.

#define PRESCALE_1 _BV(0)
#define PRESCALE_8 _BV(1)
#define PRESCALE_64 (_BV(0) | _BV(1))
#define PRESCALE_256 _BV(2)
#define PRESCALE_1024 (_BV(2) | _BV(0))

void setup(){
  DDRB |= _BV(PINB0);

  PRR &= ~_BV(PRTIM0); //Enable Timer 0
  TCCR0A = 0x00; //Outputs disconnected

  // turn on CTC mode, 64 prescaler
  TCCR0B |= _BV(WGM01) | PRESCALE_64;


  OCR0A = 200; //~1200 Hz
  OCR0B = 50;  //~4800 Hz

  //
  TIMSK = _BV(OCIE0B) | _BV(OCIE0A); //Interrupts Enabled
}

ISR(TIM0_COMPA_vect){
  PORTB |= _BV(PINB0); //Set PINB0 on
}

ISR(TIM0_COMPB_vect){
  PORTB &= ~_BV(PINB0); //Set PINB0 off
}

Respuestas (3)

Para empezar, no uso ni he usado Arduino, pero estoy muy familiarizado con los chips AVR y el ATmega328p en particular. Si lo entiendo correctamente, está tratando de atenuar una matriz de LED de 4x8. Toda la matriz debe atenuarse de una vez, pero no todos los LED estarán siempre encendidos, lo que significa un control de encendido/apagado individual con atenuación colectiva. Esto es realmente una cosa muy simple de hacer. Permítanme comenzar explicando el control PWM, ya que mencionó que es posible que no lo esté haciendo correctamente.

Si tengo un LED y una resistencia en serie y conecto 5 V, brillará con cierto brillo, un factor de la corriente a través del LED, establecido por la resistencia en serie. Si bajo el voltaje, la corriente también bajará y el LED se atenuará. Si envío un pulso al LED, el voltaje efectivo del LED será un promedio de los estados de encendido y apagado del pulso. Este porcentaje de tiempo se conoce como el ciclo de trabajo. La frecuencia del pulso en sí es la frecuencia con la que se repite. Por ejemplo, para crear un pulso de 100 Hz con un ciclo de trabajo del 50 %, me gustaría encender una señal durante 5 ms y luego apagarla durante 5 ms. El período total es de 10 ms. Frecuencia = inversa de Período = 1/10ms = 100Hz. El ciclo duy es 5ms/10ms = 50%.

Los LED pueden encenderse y apagarse muy rápidamente, pero el ojo humano no puede distinguir estos cambios por encima de cierta frecuencia; este valor es diferente para diferentes personas. Teniendo en cuenta que un televisor se actualiza a 60 Hz en los EE. UU. (50 Hz en otros lugares), podemos decir con seguridad que 50 Hz es un buen mínimo, aunque muchos estudios han demostrado que con los LED, ciertas frecuencias pueden hacer que el LED parezca más brillante con el mismo ciclo de trabajo. . Un número común es 100Hz.

Controlar un solo LED e incluso grupos de esta manera es muy simple usando un temporizador. El siguiente código permitirá que el temporizador 1 se ejecute a 125 Hz (período de 8 ms) con Fcpu = 16 MHz.

  #define _BV(FOO) (1<<FOO)
  // Set up Timer 1 for 125Hz LED pulse to control brightness
  PRR &= ~_BV(PRTIM1);                  // Enable Timer1 Clock
  TCCR1A = 0x00;                        // Outputs Disconnected
  TCCR1B = _BV(WGM12) |                 // CTC Mode, Top = OCR1A
           _BV(CS11) | _BV(CS10);       // Prescaler = 64
  OCR1A = 1999;                         // Top = (16MHz * 8ms / 64)-1
  OCR1B = LED_DUTY_CYCLE;               // LED PUlse Width
  TIMSK1 = _BV(OCIE1B) | _BV(OCIE1A);   // Interrupts Enabled

En este código, la coincidencia de comparación A ocurrirá cada 8 ms, creando un pulso de 125 Hz. La coincidencia de comparación B ocurrirá en cualquier valor que se defina como "LED_DUTY_CYCLE". Para un ciclo de trabajo del 80 %, como mencionó, configure OCR1B en 1600. Este valor también se puede cambiar en el código, como cuando un usuario presiona un botón de función de atenuación.

El control de LED tendrá lugar en el ISR para las dos coincidencias de comparación. La variable "salidas" se actualiza en el programa principal cada vez que un LED debe estar encendido o apagado. Cada bit de esta variable se asigna a un LED. Por ejemplo, para encender los LED 0 y 5, las salidas deben establecerse en 0b00100001 en principal. La variable "brillo" se puede actualizar en principal para controlar el ciclo de trabajo de los LED. En el COMPA ISR se encenderán los LEDs que estén habilitados por “salidas”. Luego, todos los LED deben estar apagados en el COMPB ISR.

ISR(TIMER1_COMPA_vect){
  PORTD = (outputs & 0xFF);         // Turn On LEDs Q0 - Q7
  OCR1B = brightness;               // Set the pulse width
}
ISR(TIMER1_COMPB_vect){
  PORTD = 0x00;                     // Turn Off LEDs Q0 - Q7
}

En este ejemplo, hay 8 LED conectados a cada uno de los 8 pines de PORTD. Podrían colocarse en cualquier lugar, esto solo hace que el ejemplo de código sea más fácil de leer. Si los LED están dispersos, deberá hacer algo más como esto:

if(outputs & 0x01) PORTD |= LED0; 
if(outputs & 0x02) PORTD |= LED1;
if(outputs & 0x04) PORTC |= LED2;
//...
if(outputs & 0x40) PORTB |= LED6;
if(outputs & 0x80) PORTB |= LED7;

Tenga en cuenta que cada LED está asignado a un bit en "salidas", pero los propios LED residen en varios puertos IO. Cualquier LED que esté habilitado se encenderá.

Controlar una matriz es un poco más complejo ya que solo una columna estará activa a la vez. Con eso en mente, el ciclo de trabajo más alto posible que puede lograr es del 25 %, incluso si las filas de LED estuvieran encendidas todo el tiempo. Eso es porque cada columna solo estaría en 1/4 del tiempo total. Si hay más de una columna encendida a la vez, perderá por completo la capacidad de encender y apagar los LED individuales. Otra cosa a tener en cuenta con una matriz es el consumo actual. Dependiendo de su definición de fila y columna, tendrá 4 bancos de 8 u 8 bancos de 4 LED paralelos. Si todos los cátodos están unidos, entonces ese pin IO está hundiendo la corriente a través de todos los LED. El ATmega328p tiene una corriente máxima de 40 mA por pin y un total de 200 mA en cualquier momento. El problema de los pines individuales podría evitarse fácilmente hundiendo los LED a través de un "

Matriz LED 4x8

Por supuesto, todo se puede girar 90 grados para adaptarse a sus necesidades. En cualquier caso, las 8 líneas "CTRL" encenderán un LED siempre que la señal "COLUMNA" apropiada también sea alta. El control de la columna debe ser simple y se puede realizar en la interrupción principal o del temporizador, pero la frecuencia PWM debe ser aproximadamente 4 veces más rápida que la frecuencia de conmutación de la columna para garantizar que la atenuación de los LED aún funcione correctamente. En ese caso, cada columna se pulsaría cuatro veces antes de que se encienda la siguiente columna. Pero, como dije, con 4 columnas, cada LED solo estará encendido el 25 % del tiempo como máximo, por lo que si su función PWM está configurada en 80 %, el LED está realmente solo en .8 * .25 = 20 % de el tiempo. Además, no olvide que a medida que la columna activa cambia, el control cambia de un banco de LED al siguiente, por lo que las "salidas"

También es de destacar que no importa lo que pulse para atenuar los LED. En el ejemplo anterior, estaba pulsando las filas porque es fácil habilitar un LED específico de esa manera. Pulsar la señal de control de la columna a la puerta FET también funcionaría. Por último, dado que solo hay una columna activa a la vez, cada fila compartida puede compartir una resistencia. Cada columna no puede compartir una resistencia porque el brillo individual de los LED cambiaría dependiendo de cuántos estén encendidos o apagados.

Estoy bastante seguro de que entiendo esto. Desconocía por completo que pudieras manejar un ciclo de trabajo así. Lo que todavía estoy tratando de entender es cómo hacer esta atenuación con múltiples "columnas". Actualmente estoy configurado con un cátodo común y 8 grupos de 4. Básicamente, cada pin de E/S solo proporciona energía para 1 LED en un momento dado. Solo uso otros 8 pines y tierra y solo configuro 1 a tierra a la vez para encender su columna. Ha funcionado muy bien hasta ahora. Aunque ahora me pregunto si estoy excediendo la corriente en el pin del fregadero...
Dependería de cuántos amperios estén pasando por cada LED a la vez. En el peor de los casos: con los 8 LED de un banco encendidos, podrían tener un máximo de 5 mA cada uno (5 * 8 = 40 mA máx.) si todos están hundidos por un pin. Es por eso que mencioné usar el controlador FET para hundirlos. Entonces, es solo una cuestión de la corriente total generada. El máximo del chip es de 200 mA, por lo que podría generar de forma segura 20 mA por LED... 160 mA en total si los 8 están encendidos.
Extraño ... He estado suministrando 4 LED a la vez a un pin de disipador y cada LED tiene una potencia nominal de aproximadamente 20 mA. No creo que en realidad estén funcionando tan alto, pero me imagino que más de 10 mA. Pero he tenido esto funcionando continuamente durante varias semanas sin problemas. Por supuesto, cada grupo de 4 LED solo se enciende durante 1/6400 segundos antes de pasar al siguiente grupo de 4. Dados 8 grupos, está actualizando toda la pantalla a 800 Hz.
Mi temporizador de actualización actual está configurado de la siguiente manera: TCCR1A = 0; // establecer todo el registro TCCR1A en 0 TCCR1B = 0; // lo mismo para TCCR1B TCNT1 = 0; // inicializar el valor del contador en 0 // establecer el registro de coincidencia de comparación para 6400 Hz ( actualización de pantalla de 800 Hz) incrementa OCR1A = 2500;// = (16*10^6) / (1*6400) - 1 // activa el modo CTC TCCR1B |= _BV(WGM12); TCCR1B |= PREESCALA_1; // habilita la interrupción de comparación del temporizador TIMSK1 |= _BV(OCIE1A);
El temporizador está configurado correctamente para un ISR de 6400 Hz, pero ¿para qué lo usa: para pulsar los LED o para cambiar el control de un banco de LED al siguiente? En cuanto a la corriente del LED, no importa cuál sea su valor nominal, importa qué resistencia coloque en serie. Los diferentes LED arrojarán diferentes voltajes, la fuente menos el voltaje del LED es el voltaje de la resistencia, y el voltaje de la resistencia dividido por la resistencia es igual a la corriente: (Vs - V_LED)/R = I_LED. Por ejemplo, un LED rojo común generalmente cae 2V. Desde una fuente de 5 V, una resistencia de 300 ohmios en serie produciría 10 mA a través del LED.
Ambos. Mantengo un contador de pasos. A medida que aumenta, enciendo la siguiente columna y apago el resto. Y luego, enciendo cualquiera de los 4 LED en esa columna que deben estar. Entonces, toda la pantalla se actualiza a 800 Hz, de manera efectiva. Vea el código aquí: pastebin.com/PW2mFEqc ¿Pensamientos? Eh... imagínense. HICE los cálculos actuales: P. Estoy usando un LED de 1.9 Vf y una resistencia de 330 ohmios, todo a 5 V, por un poco menos de 10 mA de corriente por. x4 LED por pin disipador <~40mA... no es de extrañar que no lo haya quemado.
Debería comentar su código mucho más (como cada línea) para que alguien más pueda entender lo que está tratando de hacer. En proyectos más grandes, si no trabajo en algo durante unos días, me lleva una eternidad darme cuenta de lo que estaba haciendo. Dado que está utilizando 8 columnas en lugar de las 4 de mi ejemplo, sus LED están encendidos solo 1/8 del tiempo sin atenuación. Le sugiero que extraiga la mayor parte de ese código del ISR para ponerlo en main. Además, no veo cómo está usando ningún PWM, ¡pero tal vez porque no está comentado!
Ahora mismo el único que necesitaba saberlo era yo... pero se comentará antes de hacerlo público. Me doy cuenta de que solo están en 1/8 del tiempo, pero tuve que minimizar la corriente por columna. ¿Por qué debería ponerlo en el principal? Entonces no tengo control sobre la frecuencia de actualización... No estoy haciendo el PWM adecuado, ese era el problema... pero lo estoy fingiendo con esta línea: if(pwmStep < pwmLevel); Incremento pwmStep cada vez que completo un escaneo a través de las 8 columnas. Hasta 10. Entonces, si pwm step es 5 y pwmLevel es 5, omite las últimas 5 actualizaciones y la luz solo está encendida el 50% del tiempo... ¿tiene sentido?
¿Hay alguna posibilidad de que sepa cómo hacer su código de ejemplo anterior para ATTiny85? Estoy tratando de adaptarlo a eso, pero no tengo suerte para que actúe correctamente.
Debería funcionar exactamente igual. Todavía tiene un temporizador 1 de 16 bits, sin embargo, los nombres de registro son diferentes para esa MCU. Eche un vistazo a la hoja de datos del ATtinyx5. En particular, debe consultar la sección 12.3 - Descripciones del registro del temporizador 1 en la página 88: atmel.com/Images/… Por lo general, usaría el temporizador 0 de 8 bits para todo, ya que usa menos energía (deshabilitando los otros temporizadores con el registro de reducción de energía). , PRR), pero creo que Arduino ya usa timer0 para varias cosas.
Además, el tiny25 es más económico que el 85 y tiene mucho espacio para la mayoría de los programas pequeños. Esa es parte de la razón por la que mencioné mover mucho de su código de ISR a main. Desea la menor cantidad de código posible en su ISR, lo que hace que el código sea mucho más pequeño. Consulte las excelentes notas de esta aplicación sobre cómo escribir código C eficiente: atmel.com/Images/doc1497.pdf y atmel.com/Images/doc8453.pdf
En realidad, estoy bastante seguro de que la serie ATTinyx5 solo tiene temporizadores de 8 bits. Parece que no puedo establecer los nombres de registro correctos ya que todos son diferentes: P
Sin embargo, en el núcleo arduino-tiny que estoy usando, usa Timer1 para millis () de todos modos, así que probablemente debería usar Timer0 ... ¿sugerencias? Parece que no puedo hacer coincidir los registros ATTiny85 Timer0 con el ejemplo que tenía.
Recibió una puñalada que funciona en el ATTiny85. Consulte el código agregado en la pregunta anterior. ¿Pensamientos?
Lo siento, tienes razón. Solo temporizadores de 8 bits. En ese caso, no puede usar valores superiores a 255, por lo que la resolución máxima es menor. Lo único que veo mal es que debería ser: TCCR0A = _BV(WGM01); Intentaste configurarlo usando TCCR0B. Aparte de eso, debería funcionar bien. Para ver si el LED realmente se está atenuando, intente aumentar/disminuir su valor OCR0B principal entre 0 y 200 cada 100 ms. Debería ver que el LED se atenúa o se vuelve más brillante cada 100 ms cuando cambia el valor de OCR0B.
Ahhh... eso funciona mucho mejor. El único problema que tengo es el tiempo entre cambiar el valor del ciclo de trabajo. Intentando usar el arduino-tiny core y aunque creo que estoy usando el temporizador correcto para no interferir con la llamada millis() o delay(), parece que la configuración de mi temporizador está interfiriendo con los que están trabajando.
Estás solo con eso. Como dije, no he usado Arduino ni me ha interesado nunca. Diseño mis propios sistemas integrados en torno a cualquier chip que quiera usar. Me da mucha más libertad y control. Personalmente, creo que Arduino es una sobrecarga inútil que solo evita que sus usuarios aprendan cómo funcionan realmente las cosas.
Bueno, ¿hay alguna manera de hacer un retraso de tiempo en el código AVR directo? Prefiero usar eso también.
¡Si muchos! Y creo que quieres decir "código c". Los lenguajes de programación más comunes utilizados con AVR son C y Assembly. Creo que Arduino usa un C ++ modificado, que es una versión avanzada de C. Hay un archivo de encabezado que se puede usar para retrasos. Consulte la documentación aquí: nongnu.org/avr-libc/user-manual/group__util__delay.html O puede crear su propia rutina ISR para controlar el flujo de su programa. Casi siempre uso el temporizador 0 para crear un reloj de 1 ms, y luego lo uso para cronometrar todo en el programa: retrasos, PWM, controles de botones, etc.

Comenzaría eliminando las operaciones de punto flotante dentro de una interrupción en un AVR porque pueden hacer un uso intensivo de la CPU y, dependiendo de la velocidad del reloj, tal vez la interrupción no se complete cuando el temporizador esté listo para dispararse a continuación. Por ejemplo cambiar:

uint8_t pwmMax = 10;
float pwmLevel = 0.8f;
....
if(pwmStep < pwmLevel*pwmMax)

a:

uint8_t pwmMax = 100;
uint8_8 pwmLevel = 80;
if(pwmStep < pwmLevel)

Idealmente, para PWM, desearía intercalar la conmutación, por lo que, por ejemplo, en este momento, digamos más de 10 interrupciones, estará haciendo lo siguiente:

1 1 1 1 1 1 1 1 0 0

Mientras que la siguiente secuencia sería ideal:

1 1 1 1 0 1 1 1 1 0

Sin embargo, eso complicaría las cosas y, a más de 50 Hz, no esperaría que el parpadeo fuera visible, por lo que no parece ser su problema real.

Discrepo respetuosamente con PeterJ sobre el intercalado: si bien es cierto que esto es ideal, ningún atenuador LED comercial hace esto, AFAIK. Todos los que he visto no intercalan los ciclos de encendido y apagado de la manera que sugieres. Y dado un PWM lo suficientemente rápido, realmente no importa.

Una cosa a tener en cuenta es que el mapeo de PWM a intensidad no es lineal, sino exponencial. Usando potencias de dos, el número de ciclos que el LED necesita para encenderse es 2^(intensidad) o 2^(intensidad)-1. Mi preferencia es por lo último, así que iremos con eso. Esto significa que para 4 intensidades (niveles de intensidad 0-3) necesita dividir su período PWM en 7 ciclos y sus LED deben estar encendidos durante 0, 1, 3 y 7 ciclos respectivamente.

Por supuesto, esto tampoco resuelve tu problema. ¿Necesita atenuar los LED de forma individual o colectiva? Si es lo último, puede usar las instalaciones PWM de AVR y no codificar las suyas propias. Si se trata de un chip AVR listo para usar, tiene el divisor 8x, por lo que está ejecutando 1 mhz, y podría ser el caso de que su código pwm sea demasiado lento. Puede apagarlo sin quemar fusibles de la siguiente manera:

CLKPR = (1<<CLKPCE);  // CLKPCE bit must be set immediately before CLKPS bits
CLKPR = 0;  // System clock divider = 1

Esto al menos debería hacer que sus LED parpadeen más rápido :) Pero en cualquier caso, un diagrama de circuito sería útil.

Estoy ejecutando una configuración estándar de arduino ATMega328, por lo que funciona a 16 MHz. Sin embargo, no estoy muy seguro de cómo usaría el PWM incorporado, ya que el chip solo tiene 3 pines PWM y creo que necesitaría al menos 4 (ya que tengo 4 cátodos comunes para los LED). ¿Correcto? Incluso si hubiera suficientes, ¿cómo haría eso?
Para aclarar los pinouts, estos son los que importan: //Configurar cátodos comunes como salidas DDRC = _BV(PC0) | _BV(PC1) | _BV(PC2) | _BV(PC3); //Configurar filas como salidas DDRB = _BV(PB0) | _BV(PB1) | _BV(PB2) | _BV(PB3) | _BV(PB4) | _BV(PB5); DDRD = _BV(PD6) | _BV(PD7);
Y... necesito atenuar los LED colectivamente. No todos están siempre encendidos, pero es solo una pantalla completa que se oscurece. Saber cómo hacerlos individualmente sería genial para futuros proyectos, pero no es lo que necesito ahora.
El brillo aparente es logarítmico en lugar de lineal, pero si se utilizan LED de colores, el tono aparente de, por ejemplo, rojo 10 % azul 20 % será aproximadamente el mismo que el de rojo 5 % azul 10 %.