SSD1306 I2C: Dibujar solo cambia, sin embargo, en una posición incorrecta, ¿cómo establecer la posición (comando)?

1

Creo una clase de pantalla OLED para el SSD1306 y su funcionamiento es bastante sencillo. La pantalla se almacena en búfer (método fuera de pantalla) y, al realizar una actualización, "descarga" el búfer a la pantalla a través de I2C, en este caso un total de 512 bytes (128x32 píxeles), pero también puede ser de 1024 bytes (128x64 píxeles).

El método fuera de pantalla es evitar el parpadeo y el rendimiento más rápido porque solo se actualiza cuando quiero que se actualice. Muy útil con animaciones por ejemplo.

Sin embargo, no es nada lujoso en muchas actualizaciones, es una operación costosa y en su mayoría no es necesario debido a pequeños cambios en la pantalla. También siento curiosidad por la velocidad de fotogramas, con menos actualizaciones necesarias, ¿funciona mejor?

Por eso quiero ser una clase lo suficientemente inteligente como para enviar solo los cambios, por lo que no necesito cuidar los problemas de velocidad / rendimiento. Esto ya está funcionando parcialmente (con un búfer doble), para detectar los cambios y enviar solo aquellos, sin embargo, aparecen en una posición incorrecta. Esto se debe a un método anterior, con un simple volcado, no es necesario especificar cada ubicación de pares de píxeles, simplemente bombea una matriz de valores.

¿Qué tipo de comando / instrucción del SSD1306 necesito para establecer la posición de destino o puedo omitir valores? No puedo resolverlo utilizando la hoja de datos .

Aquí está mi código (AVR-C):

// Send start address
  if (useRegisters)          // Send TWI Start
  {
    // Send start address
    TWCR = _BV(TWEN) | _BV(TWEA) | _BV(TWINT) | _BV(TWSTA);
    while(TOD_CHK_WAIT((TWCR & _BV(TWINT))) == 0) {};
    TWDR = twiAddress<<1;
    TWCR = _BV(TWEN) | _BV(TWINT) | _BV(TWEA);
    while (TOD_CHK_WAIT((TWCR & _BV(TWINT)) == 0)) {};
    TWDR = TOD_DATA_CONTINUE;
    TWCR = _BV(TWEN) | _BV(TWINT) | _BV(TWEA);
    while(TOD_CHK_WAIT((TWCR & _BV(TWINT)) == 0)) {};
  }
  else
  {
    pinSendStart( twiAddress<<1 );
    pinWaitForAck();
    pinWriteByte(TOD_DATA_CONTINUE);
    pinWaitForAck();
  }

 // Dump buffer
  for (uint16_t i=0; i < cacheSize; i++)    // Send data
  {  
     bool bUpdate = ((!doubleBuffer) || ( doubleBuffer && (displayCache[cacheSize-1+i] != displayCache[i]))); 

     if( doubleBufferFirstTime || bUpdate )
     {
      if( doubleBuffer )
       { displayCache[cacheSize-1+i]=displayCache[i]; }

      if( useRegisters )
      {
         if( !doubleBufferFirstTime && doubleBuffer )
          { 
            // Set location must be here
            // /* X */ set( TOD_SET_COLUMN_ADDR, x ); //width-1 );
            // /* Y */ set( TOD_SET_PAGE_ADDR  , y ); //height-1 );
          } 

         TWDR = displayCache[i];
         TWCR = _BV(TWEN) | _BV(TWINT) | _BV(TWEA);               // Clear TWINT to proceed
         while(TOD_CHK_WAIT((TWCR & _BV(TWINT)) == 0)) {};        // Wait for TWI to be ready
      }
      else { 
             pinWriteByte(displayCache[i]);
             pinWaitForAck();
           }
     }

  }

  doubleBufferFirstTime=false;

  if( useRegisters )                                            // Send TWI Stop
    { TWCR = _BV(TWEN)| _BV(TWINT) | _BV(TWSTO); }              // Send STOP
  else { pinSendStop(); }

Intenté usar COLUMN_ADDR (21h) y PAGE_ADDR (22h) para establecer el desplazamiento, el dispositivo no parece gustarle (se bloquea). ¿Qué puedo usar para cambiar la compensación u omitir una compensación específica?

Esta es la configuración de la pantalla:

  set( TOD_SET_DISPLAY_CLOCK_DIV_RATIO  , 0x80 );
  set( TOD_SET_MULTIPLEX_RATIO          , height-1 );
  set( TOD_SET_DISPLAY_OFFSET           , 0x0  );

  set( TOD_SET_START_LINE | 0x0 );

  set( TOD_CHARGE_PUMP                  , 0x14 );
  set( TOD_MEMORY_ADDR_MODE             , 0x00 );

  set( TOD_SET_SEGMENT_REMAP | 0x1 );
  set( TOD_COM_SCAN_DIR_DEC );

  set( TOD_SET_COM_PINS                 , ( height > 32 )?0x12:0x02 );
  set( TOD_SET_CONTRAST_CONTROL         , 0xCF );

  set( TOD_SET_PRECHARGE_PERIOD         , 0xF1 );

  set( TOD_SET_VCOM_DETECT              , 0x40 );

  set( TOD_DISPLAY_ALL_ON_RESUME );
  set( TOD_NORMAL_DISPLAY );

  set( TOD_DEACTIVATE_SCROLL );

  show();

Editar 21 Okt 2017

Ver mi respuesta publicada.

    
pregunta Codebeat

3 respuestas

0

Finalmente lo entendí, este sitio / artículo me dio algunas ideas para hacerlo bien: enlace Está en la función gotoxy. Creo que el cálculo en esta función no es correcto en el ejemplo, pero está bien, me di cuenta de la manera correcta.

La dirección de mi columna / página estaba equivocada y corríjala (cambiar la asignación en realidad en unos pocos días). También se modificó la forma de actualizar, ahora es de forma recursiva.

Funciona bastante bien (y rápido), por ejemplo, cuando un pequeño ícono cambia, solo se enviarán alrededor de 12 bytes / actualizaciones en lugar de los 512 bytes completos. Necesito calcular los bytes del bus de envío para ver si es realmente efectivo. Es un buen experimento, pero necesito hacer algunas pruebas para ver qué hará con el framerate, por ejemplo.

Gracias a todos por los comentarios. Aquí está mi nuevo código, creo que no se puede usar sin modificaciones / ediciones, solo está aquí como ejemplo y para mostrarle que es posible y para dar una idea de cómo hacerlo.

struct TODPoint
{
  uint8_t x;
  uint8_t y;
};


uint8_t TOLEDdisplay::getMemPageMax()
{ return (height > 16)?((height > 32)?7:3):1; }

uint8_t TOLEDdisplay::getMemPageCount()
{ return getMemPageMax()+1; }

uint8_t TOLEDdisplay::getMemColumnMax()
{
  uint16_t iSize     = (uint16_t)width*(uint16_t)height;
  uint16_t iPageSize = (uint16_t)getMemPageCount()*8;
  if( iSize >= iPageSize )
  {
    iSize/=iPageSize;
    return iSize-1;
  }
  return 0x7F;
}

uint8_t TOLEDdisplay::getMemColumnCount()
{ return getMemColumnMax()+1; }


void TOLEDdisplay::gotoMemXY( uint8_t memX, uint8_t memY )
{
  uint8_t iMaxX = getMemColumnCount();
  uint8_t iMaxY = getMemPageCount();

  // gotoXY routine to move the memory pointers to column, page.
  if( memX < iMaxX && memY < iMaxY )
  {
    TOD_DISABLE_INTERRUPTS();

    set( TOD_SET_COLUMN_ADDR, memX, iMaxX-1 );

    if( !cycleTimeout )
     { set( TOD_SET_PAGE_ADDR, memY, iMaxY-1 ); }

    TOD_ENABLE_INTERRUPTS();
  }
}


TODPoint TOLEDdisplay::getCachePosToMemPos( uint16_t iAddress )
{
  TODPoint xy;
  xy.x = getMemColumnCount();
  xy.y = (iAddress > 0)?iAddress / (uint16_t)xy.x:0;
  xy.x = (iAddress > (xy.x-1))?iAddress % xy.x:iAddress;
  return xy;
}

void TOLEDdisplay::gotoMemXY( uint16_t iAddress )
{
  if( iAddress < cacheSize )
  {
    //iAddress = (cacheSize-1)-iAddress;
    TODPoint memXY = getCachePosToMemPos(iAddress);
    gotoMemXY( memXY.x, memXY.y );
  }
}


bool TOLEDdisplay::isCacheAddressChanged( uint16_t iAddress, bool bUpdateWhenChanged /* default = false */  )
{
  bool bFound = ( doubleBuffer && !doubleBufferFirstTime && ( iAddress < cacheSize ) );
  if( bFound )
   {
     uint16_t idbAddress = cacheSize-1+iAddress;
     bFound = displayCache[ cacheSize-1+iAddress ]!=displayCache[ iAddress ];

     if( bUpdateWhenChanged )
      { displayCache[ idbAddress ]=displayCache[ iAddress ]; }
   }

  return bFound;
}

uint8_t TOLEDdisplay::getMemPageMax()
{ return (height > 16)?((height > 32)?7:3):1; }

uint8_t TOLEDdisplay::getMemPageCount()
{ return getMemPageMax()+1; }

uint8_t TOLEDdisplay::getMemColumnMax()
{
  uint16_t iSize     = (uint16_t)width*(uint16_t)height;
  uint16_t iPageSize = (uint16_t)getMemPageCount()*8;
  if( iSize >= iPageSize )
  {
    iSize/=iPageSize;
    return iSize-1;
  }
  return 0x7F;
}

uint8_t TOLEDdisplay::getMemColumnCount()
{ return getMemColumnMax()+1; }


TODMinax TOLEDdisplay::getCacheChangeRange()  // get min max boundary
{
  TODMinax result;
  bool bFound = ( doubleBuffer && !doubleBufferFirstTime );

  if( bFound )
  {
    result.min  = 0;
    result.max  = cacheSize;
    bFound = false;

    // Don't count zero, will be drawed when needed
    while( !bFound && (++result.min < result.max ) )
     { bFound = displayCache[result.max+result.min]!=displayCache[result.min]; }

    if( bFound )
    {
      bFound=false;
      while( !bFound && (--result.max > result.min) )
       { bFound = displayCache[cacheSize-1+result.max]!=displayCache[result.max]; }
    }
 }

 if( !bFound )
  { result.min = result.max = 0; }

  return result;
}


void TOLEDdisplay::update( bool bForce /* default = false */, uint16_t iAddress /* default = 0 */ )
{
  if( !isInit )
   { return; }

  if( !bForce )
  {
   if( !enabled )
    { return; }

   if( cycleTimeout )
    {
      reset();
      if( cycleTimeout )
       { return; }
    }
  }

   // Set loop range iAddress-iMaxAddress, set iMaxAddress to total cache size
  uint16_t iMaxAddress = cacheSize;

  /*
   // Prints cache to mem position table
  while( iAddress < iMaxAddress )
  {
    TODPoint memXY = getCachePosToMemPos(iAddress);
    Serial.print( iAddress );
    Serial.print( ": Column " );
    Serial.print( memXY.x );
    Serial.print( " - Page " );
    Serial.println( memXY.y );
    ++iAddress;
  }

  return;
  */

  // When doublebuffer is enabled, we check individual parts of the
  // cache are changed, to only draw the changes. When doublebuffer is
  // disabled or an update is forced, we send the whole cache to the
  // display.
  if( !bForce && doubleBuffer && !doubleBufferFirstTime && (iAddress == 0) )
  {
     //Serial.println( "Check changes" );

     // Check for changes, get min and max range of cache changes.
     // It is possible that not all entries between this range are changed,
     // this just to limit the loop to check for changes, see code below.
     // NOTICE: The function getCacheChangeRange() skips element 0 in
     //         the cache array, it needs to be checked manually, see also
     //         code below.
     TODMinax changeRange = getCacheChangeRange();

     //Serial.println( "-----" );
     //Serial.println( changeRange.min );
     //Serial.println( changeRange.max );

     // Something changed?
     if( changeRange.min == changeRange.max )
     {
        // Check if address 0 has been changed and reset change
       iMaxAddress = (uint8_t)isCacheAddressChanged( 0, true );

        // Function sets ranges to zero when no change is found,
        // when value > 0 then doublecheck entry has been changed
       if( changeRange.min != 0 && isCacheAddressChanged( changeRange.min, true ) )
       {
           // Recursive call to this function with different address
           update( false, changeRange.min );
       }

        // Cache element 0 changed?
       if( !iMaxAddress )
       {
           // No, exit function
          return;
       }

       // At this point proceed normal, draw changes detected at element 0
       // In this case, loop max iMaxAddress is set to 1
     }
     else {
              // Changed range found
             adjustBusSpeed(true);
             TOD_DISABLE_INTERRUPTS();
             //uint16_t iUpdates = 0;

             while( changeRange.min <= changeRange.max )
             {
                 if( isCacheAddressChanged( changeRange.min, true ) )
                  {
                     // Recursive call to this function with different address
                    update( false, changeRange.min );
                    //++iUpdates;
                  }

                 ++changeRange.min;
             }

             //Serial.println( "-----------" );
             //Serial.print( "bytes updated : " );
             //Serial.println( iUpdates );

              // Check if address 0 has been changed
             if( !isCacheAddressChanged( iAddress, true ) )
             {
                  TOD_ENABLE_INTERRUPTS();
                  adjustBusSpeed(false);
                  return;
             }

             // otherwise proceed, draw iAddress 0
             iMaxAddress=1;
          }
  }

  if( cycleTimeout )
   {
     doubleBufferFirstTime=true;
     return;
   }

  if( iAddress == 0 )
   {
      // Update all unless iMaxAddress has been changed to 1,
      // If this the case, we draw only address 0
     doubleBufferFirstTime=false;
     gotoMemXY(0,0);
   }
  else {
         // Update only requested address
         gotoMemXY( iAddress );
         //gotoMemXY(0,0);
         iMaxAddress=iAddress+1;
       }

  if( cycleTimeout )
  {
    doubleBufferFirstTime=true;
    return;
  }

 if( iAddress == 0 )
  {
    TOD_DISABLE_INTERRUPTS();
    adjustBusSpeed(true);
  }

  if (useRegisters)          // Send TWI Start
  {
    // Send start address
    TWCR = _BV(TWEN) | _BV(TWEA) | _BV(TWINT) | _BV(TWSTA);
    while(TOD_CHK_WAIT((TWCR & _BV(TWINT))) == 0) {};
    TWDR = twiAddress<<1;
    TWCR = _BV(TWEN) | _BV(TWINT) | _BV(TWEA);
    while (TOD_CHK_WAIT((TWCR & _BV(TWINT)) == 0)) {};
    TWDR = TOD_DATA_CONTINUE;
    //TWDR = TOD_DATA;
    TWCR = _BV(TWEN) | _BV(TWINT) | _BV(TWEA);
    while(TOD_CHK_WAIT((TWCR & _BV(TWINT)) == 0)) {};
  }
  else
  {
    pinSendStart( twiAddress<<1 );
    pinWaitForAck();
    pinWriteByte(TOD_DATA_CONTINUE);
    pinWaitForAck();
  }


  for (uint16_t i=iAddress; i < iMaxAddress; i++)    // Send data
  {
      if( useRegisters )
      {
         TWDR = displayCache[i];
         TWCR = _BV(TWEN) | _BV(TWINT) | _BV(TWEA);               // Clear TWINT to proceed
         while(TOD_CHK_WAIT((TWCR & _BV(TWINT)) == 0)) {};        // Wait for TWI to be ready
      }
      else {
             pinWriteByte(displayCache[i]);
             pinWaitForAck();
           }
  }

  if( useRegisters )                                            // Send TWI Stop
    { TWCR = _BV(TWEN)| _BV(TWINT) | _BV(TWSTO); }              // Send STOP
  else { pinSendStop(); }


 if( iAddress == 0 )
 {
   adjustBusSpeed(false);
   TOD_ENABLE_INTERRUPTS();
 }

}
    
respondido por el Codebeat
0

Supongo que entiende cómo crear la secuencia de bytes I2C para el SSD1306 pero lo repetiré de todos modos: El SSD1306 distingue entre comandos (incluidos los parámetros del comando) y datos (datos de píxeles). Con SPI, utiliza un pin de entrada dedicado para distinguir comandos y datos.

Con I2C, 0x80 se debe añadir a cada byte de comando. 0x40 cambiar al modo de datos. El modo de datos continúa hasta el final de la transacción I2C (indicada por una condición de PARADA).

Para actualizar una parte de la pantalla, se debe configurar la dirección de inicio de la esquina superior izquierda y luego se pueden enviar los datos. Una secuencia de bytes válida para comenzar en las coordenadas (20, 16) para xey se ve así:

0x80, 0xb1,  // page start address: 0xb0 | (y >> 3)
0x80, 0x04,  // lower nibble of column: 0x00 | (x & 0x0f)
0x80, 0x11,  // upper nibble of column: 0x10 | ((x >> 4) & 0x0f)
0x40, // switch to data mode
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, ... // pixel data

La memoria está dividida en páginas. Cada página cubre 8 filas de píxeles. Por lo tanto, solo puede actualizar franjas de 8 píxeles verticales y las franjas deben estar alineadas a múltiplos de 8. Como puede ver en la primera línea de la secuencia de bytes, los 3 bits inferiores de y simplemente se descartan .

La posición de inicio horizontal se proporciona en dos partes: el mordisco superior e inferior. Un mordisco es de cuatro bits, es decir, medio byte. Ver las líneas 2 y 3 anteriores.

Las dos líneas restantes cambian al modo de datos y envían los datos de píxeles. Con cada byte, la dirección avanza en 1, es decir, avanza horizontalmente de izquierda a derecha y cada byte escrito afecta a una pieza vertical de 8 píxeles.

Con los diferentes modos de direccionamiento (comando 0x20 a 0x22), puede determinar cómo avanza la dirección al final de la página, al final de su área de actualización, etc. El enfoque más simple es escribir en cada página por separado y explícitamente establece la dirección al comienzo de cada página.

Tenga en cuenta que hay clones de los chips SSD1306 que no admiten los diferentes modos de direccionamiento.

    
respondido por el Codo
0

El número de bytes a transferir (1 KB) es tan bajo que este medio para actualizar la pantalla completa es mucho más fácil. No necesita gastar tiempo en el área que desea actualizar. Simplemente mantenga el framebuffer en SRAM y actualice solo esto y luego envíe el búfer completo a la pantalla.

Personalmente uso STM32F0 (sí, es mucho más rápido que AVR y también usé DMA para la transferencia), pero el primer intento fue la transferencia con bitbanging y la velocidad aún era muy rápida. Obtuve una tasa de fotogramas total con esta configuración sobre SPI más de 1000 fps en caso de que la CPU dedique tiempo solo al enviar datos a la pantalla (una transferencia de todo el búfer tomó menos de 1 ms).

Por lo tanto, mi propuesta es mantener el programa simple y siempre actualizar la pantalla completa, porque es demasiado pequeña.

    
respondido por el vlk

Lea otras preguntas en las etiquetas