Pantalla ATMEGA328 I2C/TWI y OLED

Tengo ATMEGA328P y pantalla I2C (SSD1306). Estoy tratando de simplemente poner un solo píxel en una pantalla usando la menor cantidad de código posible para poder aprender de allí.

Pude hacer esto con raspberry usando el siguiente código:

// gcc ssd1306.c -lwiringPi -o ssd1306

#include <stdio.h>
#include <string.h>
#include <wiringPiI2C.h>

#define WIDTH 128
#define HEIGHT 64

int buffer[ WIDTH * HEIGHT / 8 ];
int i2cd;

void ssd1306_command(unsigned int c)
{
    unsigned int control = 0x00;
    wiringPiI2CWriteReg8( i2cd, control, c );
}

void ssd1306_byte(unsigned int c)
{
    unsigned int control = 0x40;
    wiringPiI2CWriteReg8( i2cd, control, c );
}

void drawPixel( int x, int y, unsigned int color )
{
    switch (color) 
    {
        case 1: // white
            buffer[x + ( y / 8 ) * WIDTH ] = 1;

            break;
        case 0: // black
            buffer[x + ( y / 8 ) * WIDTH ] = 0;
            break;
    }
}

void init()
{
    i2cd = wiringPiI2CSetup( 0x3C ); // address

    ssd1306_command(0xAE);          // 0xAE // display off
    ssd1306_command(0xD5);          // 0xD5 // set display clock division
    ssd1306_command(0x80);          // the suggested ratio 0x80
    ssd1306_command(0xA8);          // 0xA8 set multiplex
    ssd1306_command(63);            // set height
    ssd1306_command(0xD3);          // set display offset
    ssd1306_command(0x0);           // no offset
    ssd1306_command(64);            // line #0 setstartline
    ssd1306_command(0x8D);          // 0x8D // chargepump
    ssd1306_command(0x14);
    ssd1306_command(0x20);          // memory mode
    ssd1306_command(0x00);          // 0x0 act like ks0108
    ssd1306_command(161);           // segremap
    ssd1306_command(0xC8);          // comscandec
    ssd1306_command(0xDA);          // 0xDA set com pins
    ssd1306_command(0x12);
    ssd1306_command(0x81);          // 0x81 // set contract
    ssd1306_command(0xCF);
    ssd1306_command(0xD9);          // 0xd9 set pre-charge
    ssd1306_command(0xF1);
    ssd1306_command(0xDB);          // SSD1306_SETVCOMDETECT
    ssd1306_command(0x40);
    ssd1306_command(0xA4);          // 0xA4 // display all on resume
    ssd1306_command(0xA6);          // 0xA6 // normal display
    ssd1306_command(0x2E);          // deactivate scroll
    ssd1306_command(0xAF);          // --turn on oled panel
}

void renderBuffer(void)
{
    ssd1306_command(0x21);          // column address
    ssd1306_command(0);             // Column start address (0 = reset)
    ssd1306_command(127);           // Column end address (127 
    ssd1306_command(0x22);          // page address
    ssd1306_command(0x00);          // Page start address (0 = reset)
    ssd1306_command(7);             // Page end address

    int i;

    for (i = 0; i < ( 128 * 64 / 8 ); i++) 
    {
        ssd1306_byte( buffer[i] ); 
    }
}

void clearBuffer(void)
{
    memset( buffer, 0, ( 128 * 64 / 8 ) * sizeof( int ) );
}

void main() 
{
    init();
    clearBuffer();
    drawPixel( 10, 10, 1 );
    renderBuffer();
}

Todo funciona perfectamente hasta ahora cuando trato de hacer lo mismo usando i2c_master.c para mi ATMEGA: https://github.com/g4lvanix/rgbtime/tree/master/firmware

#ifndef F_CPU
    #define F_CPU 8000000UL
#endif

#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h>
#include "i2c_master.c"
#include "i2c_master.h"

#define  SSD1306_ADDRESS 0x3C

void initDisplay()
{
    i2c_start(SSD1306_ADDRESS);

    i2c_write(0xAE);          // 0xAE // display off
    i2c_write(0xD5);          // 0xD5 // set display clock division
    i2c_write(0x80);          // the suggested ratio 0x80
    i2c_write(0xA8);          // 0xA8 set multiplex
    i2c_write(63);            // set height
    i2c_write(0xD3);          // set display offset
    i2c_write(0x0);           // no offset
    i2c_write(64);            // line #0 setstartline
    i2c_write(0x8D);          // 0x8D // chargepump
    i2c_write(0x14);
    i2c_write(0x20);          // memory mode
    i2c_write(0x00);          // 0x0 act like ks0108
    i2c_write(161);           // segremap
    i2c_write(0xC8);          // comscandec
    i2c_write(0xDA);          // 0xDA set com pins
    i2c_write(0x12);
    i2c_write(0x81);          // 0x81 // set contract
    i2c_write(0xCF);
    i2c_write(0xD9);          // 0xd9 set pre-charge
    i2c_write(0xF1);
    i2c_write(0xDB);          // SSD1306_SETVCOMDETECT
    i2c_write(0x40);
    i2c_write(0xA4);          // 0xA4 // display all on resume
    i2c_write(0xA6);          // 0xA6 // normal display
    i2c_write(0x2E);          // deactivate scroll
    i2c_write(0xAF);          // --turn on oled panel

    i2c_stop();
}

void drawPixel()
{
    i2c_start( SSD1306_ADDRESS );

    i2c_write(0x21);          // column address
    i2c_write(0);             // Column start address (0 = reset)
    i2c_write(127);           // Column end address (127 
    i2c_write(0x22);          // page address
    i2c_write(0x00);          // Page start address (0 = reset)
    i2c_write(7);             // Page end address

    int i;

    int z=0;

    for ( i = 0; i < ( 128 * 64 / 8 ); i++ ) 
    {
        if ( z == 0 )
        {
            i2c_write( 0xff ); 
            z = 1;
        }
        else
        {
            i2c_write( 0x00 ); 
            z = 0;
        }
    }

    i2c_stop();
}

int main(void){

    i2c_init();
    initDisplay();
    drawPixel();

    return 0;
}

pero el píxel no se dibuja ..

Uso las siguientes configuraciones de fusibles (uso 8Mhz interno) lfuse:w:0xe2:m -U hfuse:w:0xd9:m

Tengo la siguiente configuración de hardware:

-ATMEGA328P connected to a 4.0v power source
-ADC5 (SCL) connected to OLED's SCL (with additional line to VCC with 15k resistor in between) 
-ADC4 (SDA) connected to OLED's SDA (with additional line to VCC with 15k resistor in between)

También traté de eliminar las resistencias y 'la conexión vcc adicional', y la conecté de la misma manera que lo hice con la frambuesa, pero no hizo ninguna diferencia. ¿Alguna pista sobre lo que estoy haciendo mal? He estado atascado en esto durante días. ¡Gracias!

Información adicional:

(a) Proporcione una foto del h/w.

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

(b) Proporcione la hoja de datos del módulo OLED.

https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf

(c) ¿Ha medido si el módulo OLED tiene pull-ups I2C incorporados y habilitados, o si sus pull-ups de 15k son los únicos en el bus?

Creo que no hay pull-ups incorporados. Aquí hay una cita de la hoja de datos:

Both the data line (SDA) and the clock line (SCL) should be pulled up by external resistors

(d) ¿Tiene acceso a un osciloscopio y experiencia en su uso?

Lamentablemente no tengo osciloscopio..

(e) Si es así, ¿puede proporcionar trazas que muestren el aumento y la caída de una pequeña muestra de ambas señales I2C?

-

(f) Aunque menos útil que un 'visor en esta etapa, ¿tiene acceso a un analizador lógico, aunque sea barato?

Desafortunadamente, tampoco tengo un analizador lógico ...

Comenzaría verificando el bus I2C en su configuración ATMega328 "que no funciona" a nivel de hardware : (a) Proporcione una foto del h / w. (b) Proporcione la hoja de datos del módulo OLED. (c) ¿Ha medido si el módulo OLED tiene pull-ups I2C incorporados y habilitados, o si sus pull-ups de 15k son los únicos en el bus? (d) ¿Tiene acceso a un osciloscopio y experiencia en su uso? (e) Si es así, ¿puede proporcionar trazas que muestren el aumento y la caída de una pequeña muestra de ambas señales I2C? (f) Aunque menos útil que un 'visor en esta etapa, ¿tiene acceso a un analizador lógico, aunque sea barato?
@SamGibson ¡Gracias por la respuesta! Actualicé mi pregunta con la información adicional que solicitaste.
Gracias por la actualización. Desafortunadamente, ese enlace es la hoja de datos del controlador , no del módulo : ese controlador se usa en diferentes módulos y no puede decirnos si hay resistencias I2C en el módulo . Busqué en el sitio web de Adafruit (basado en la URL de esa hoja de datos), pero no encontré un módulo OLED I2C con lo que parece ser el mismo pinout. Por lo tanto, no puedo revisar la información sobre el módulo , ya que aún no está identificado. Sin esa información, ni un alcance o LA, solo puedo sugerir probar pull-ups de menor valor, por ejemplo, 5k6 o incluso 2k2. (Para su información, el RPi tiene pull-ups 1k8 I2C integrados).
[continuación] Sin 'alcance o LA para ver su comportamiento, será difícil para usted confirmar el funcionamiento de I2C en el nivel básico. La razón por la que sugiero bajar el valor de los pull-ups I2C es porque su 15k es bastante alto. Al ver que sus cables son cortos (capacitancia de bus baja), 15k podría funcionar (dependiendo de los requisitos exactos del esclavo), pero es más alto de lo que usaría. Como mencioné, RPi tiene pull-ups 1k8 I2C incorporados, lo que puede explicar por qué su configuración de RPi aún funcionaba sin pull-ups externos (aunque el módulo en sí también podría tener pull-ups). Si se me ocurre algo más, añadiré otro comentario.
@SamGibson muchas gracias por la respuesta. Trataré de usar un pull-up de nivel más bajo como usted sugirió. Lo siento, estoy un poco perdido en el módulo vs controlador. Por lo que entendí, SSD1306 es un componente más pequeño que está montado en el "módulo" chino que tengo, ¿correcto? Revisé eBay, pero no puedo encontrar un enlace a las hojas de datos de ninguno de estos. Si da alguna pista, se llama "0.96" I2C IIC SPI Serial 128X64 White OLED", ¡gracias por su tiempo!
"SSD1306 es un componente más pequeño que está montado en el 'módulo' chino que tengo, ¿correcto?" Sí. Se podrían haber agregado al módulo componentes como resistencias pull-up , por lo que no se mencionarían en la hoja de datos del controlador SSD1306 . Personalmente, solo seguiría los rastros de PCB del módulo desde los pines SCL y SDA, para ver si se agregaron pull-ups I2C. En una búsqueda rápida, encontré varios módulos que coincidían con ese "título" en Ebay. Sin embargo, la mayoría tuvo un comentario (parcialmente incomprensible) de que su interfaz I2C no envía ACK y, por lo tanto, necesita una biblioteca I2C especial, pero los enlaces a esa biblioteca están muertos :-(
@SamGibson oh maldición, parece que sin osciloscopio/analizador lógico será como disparar en la oscuridad. Creo que podría ignorar el reconocimiento faltante del esclavo, pero honestamente no entiendo cómo funciona el código en la biblioteca I2C. Supongo que no tengo más remedio que estudiar y escribir mi propio código para entender el proceso en profundidad.
@SamGibson (continuación) Intentaré conectar un dispositivo más simple (MPU-6050) e intentaré que funcione. Básicamente, estoy tratando de hacer que TWI funcione con cualquier sensor, pero la máxima gloria habría sido esta pantalla. Cuando haga funcionar un dispositivo diferente, entonces será más fácil depurarlo más sin herramientas adicionales y le responderé con los resultados, porque ahora no estoy totalmente seguro de si la biblioteca I2C que estoy usando funciona. código en primer lugar, incluso para la implementación de I2C no rota. ¡Gracias!
@SamGibson Estoy a la mitad de lograr que el OLED funcione con ATMEGA. El problema al que me enfrento ahora es que, por alguna razón, tengo un parpadeo muy extraño y fuerte, aunque probé resistencias de diferentes valores para los pull-on, pero eso no cambió nada. El parpadeo incluso ocurre cuando no 'dibujo' nada. Todos los comandos como cambiar el contraste, etc. funcionan perfectamente. ¿Alguna pista de por qué esto podría estar sucediendo? ¡Gracias!
Recomiendo obtener un Logic Sniffer : me ha ahorrado mucho al depurar las comunicaciones I2C. Es el analizador lógico apropiado más barato que pude encontrar.

Respuestas (3)

El primer byte transmitido en una transacción I2C es la dirección del esclavo (7 bits) más el único bit de lectura/escritura. Entonces, si la dirección del esclavo es 0x3c, lo siguiente se transmite como el primer byte:

0x78  // for starting a write transaction
0x79  // for starting a read transaction

Eso es 0x3c desplazado por 1 bit más, para transacciones de lectura, el conjunto LSB.

La biblioteca Raspberry parece manejar esto automáticamente. La biblioteca ATMEGA328P parece requerir que lo haga manualmente.

Así que utilízalo 0x78como la dirección e inténtalo de nuevo.

Actualizar

Hay problemas adicionales en su código: debe indicarle al controlador de pantalla si envía comandos (y parámetros de comando) o datos. Básicamente, debe prefijar 0x80 antes de cada byte de comando y 0x40 antes de enviar datos. Una vez que use 0x40, todos los bytes hasta la condición de parada se tratarán como bytes de datos. Consulte la figura 8-7 y el capítulo 8.1.5.2 lit. 5 en la hoja de datos.

Es por eso que existe ssd1306_commandy ssd1306_byteen el código de Raspberry. (Obviamente, 0x00 también funciona en lugar de 0x80). Por lo tanto, debe agregar 0x80 antes de cada byte de inicialización, así como antes de los comandos de dirección al representar un búfer. Y debe agregar 0x40 antes de enviar el primer byte de datos.

Tenga en cuenta que también tiene una diferencia con las transacciones. (Una transacción comienza con la condición START y termina con la condición STOP). En el código Raspberry, cada byte de comando y cada byte de datos se envía en una transacción separada. En el código ATMEGA328P, lo combina en muchas menos transacciones pero más grandes. Ambos enfoques funcionan. Este último es más eficiente. Esto podría ser relevante para algunas diferencias menores, como 0x00 frente a 0x80.

Solo como referencia: puede encontrar mi código de referencia para la pantalla OLED en Swift y C# . Es un código de prueba para Wirekite , una solución de código abierto para conectar E/S, incluido I2C, a su Mac o PC mediante una placa Teensy económica y un cable USB. Luego, el código específico de la aplicación se ejecuta en su Mac o PC; el código en Teensy está arreglado.

No puedo estar exactamente seguro y dado que aún no tengo el privilegio de comentar,

¿No cree que se supone que debe iniciar o reiniciar la comunicación TWI después de cada comando o enviar una parada y emitir un nuevo comienzo?

Si esa es su propia biblioteca, podría leer el código de estado TWI o I2C del registro TWSR y eso lo ayudaría a depurar este problema. Realmente podría verificar qué parte de sus instrucciones falla comparándolas con el código de estado que se proporciona en la hoja de datos.

EDITAR: Ya no hay conjeturas, la primera parte de la respuesta es incorrecta. No, no necesita bit de inicio y parada después de leer y escribir. Entonces, para enviar un comando a su Chip, debe enviar datos de 16 bits, de los cuales los primeros 8 bits de MSB determinan si está enviando los datos o el comando. ¿Estás haciendo eso?

¡Gracias por la respuesta! Esperaba que la biblioteca lo manejara automáticamente, déjame verificar esto, gracias.
@ 0x29a ponga su función i2c_write también en la pregunta misma. Podría ayudar a la gente a ayudarte.
Adjunté un enlace a la biblioteca para que la publicación no se alargue demasiado, gracias)
@ 0x29a Tienes que hacer lo siguiente
@ 0x29a Necesita cambiar su código y hacer una función que envíe un byte de inicio primero, luego escriba la dirección del esclavo, luego escriba un byte para mostrar si el siguiente byte es comando/datos, luego escriba el 'Comando o Datos' y luego envíe el byte de parada.

La forma más sencilla de portar es escribir su propia versión dewiringPiI2CWriteReg8 y la función de configuración correspondiente.

Vuelva a compilar y debería estar listo para comenzar.