0
\$\begingroup\$

I have ATMEGA328P and I2C display (SSD1306). I'm trying to simply put a single pixel on a screen using the least code possible so I can learn from there.

I was able to do this with raspberry using the following code:

// 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();
}

Everything is working perfectly so far now when I try to do the same using i2c_master.c for my 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;
}

but the pixel is not drawn..

I use the following fuse settings (I use 8Mhz internal) lfuse:w:0xe2:m -U hfuse:w:0xd9:m

I have the following hardware setup:

-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)

I also tried to remove resistors and 'the extra vcc connection', and connected it the same way I did raspberry, but made no difference. Any hints on what am I doing wrong? I've been stuck on this for days.. Thanks!

Additional information:

(a) Please supply photo of the h/w.

enter image description here

enter image description here

(b) Please supply datasheet for the OLED module.

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

(c) Have you measured whether the OLED module has I2C pull-ups built-in and enabled, or are your 15k pull-ups the only ones on the bus?

I believe there are no built-in pull-ups. Here's a quote from the datasheet:

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

(d) Do you have access to an oscilloscope and experience in using it?

Unfortunately I don't have an oscilloscope..

(e) If so, can you provide traces showing the rise & fall of a small sample of both I2C signals?

-

(f) Although less useful than a 'scope at this stage, do you have access to a logic analyser, even a cheap one?

Unfortunetelly I don't have a logic analyser as well..

\$\endgroup\$
  • 1
    \$\begingroup\$ Thanks for the update. Unfortunately that link is the datasheet for the controller not the module - that controller is used on different modules, and it cannot tell us whether there are I2C resistors on the module. I looked on the Adafruit website (based on the URL for that datasheet), but I didn't find an OLED I2C module with what looks like the same pinout. Therefore I can't review info on the module as it is still unidentified. Without that info, nor a 'scope or LA, I can only suggest trying lower value pull-ups e.g. 5k6 or even 2k2. (FYI the RPi has on-board 1k8 I2C pull-ups.) \$\endgroup\$ – SamGibson Dec 8 '16 at 1:00
  • 1
    \$\begingroup\$ [continued] Without 'scope or LA to view its behaviour, it will be difficult for you to confirm I2C operation at the basic level. The reason for my suggestion to lower value I2C pull-ups is because your 15k is quite high. Seeing your cables are short (low bus capacitance) 15k might work (depending on exact slave requirements) but it is higher than I would use. As I mentioned, RPi has built-in 1k8 I2C pull-ups, which may explain why your RPi setup still worked without external pull-ups (though the module itself might also have pull-ups). If I think of anything else, I'll add another comment. \$\endgroup\$ – SamGibson Dec 8 '16 at 1:11
  • 1
    \$\begingroup\$ @SamGibson thank you very much for the response. I will try to use a lower level pull-up's as you suggested. I'm sorry, I'm a bit lost on module vs controller. As far as I understood SSD1306 is a smaller component that is mounted to the Chinese 'module' I have, correct? I checked ebay, but I can't find link to data sheets to any of these. If it gives any clue, it's called "0.96" I2C IIC SPI Serial 128X64 White OLED", thank you for your time! \$\endgroup\$ – 0x29a Dec 8 '16 at 1:33
  • 1
    \$\begingroup\$ @SamGibson oh damn, looks like without oscilloscope/logic analyzer it will be like shooting in the dark. I believe I could ignore the missing ack from the slave, but honestly I don't understand how the code in I2C library works. I guess I have no other choice than to study and write my own code to understand the process in depth. \$\endgroup\$ – 0x29a Dec 8 '16 at 2:44
  • 1
    \$\begingroup\$ @SamGibson (continued) I will try to connect a simpler device (MPU-6050) and try to get it working. I'm basically trying to get TWI to work with any sensor, but the ultimate glory would have been this display. When I will get different device working, then it will be easier to debug this further without extra tools and I will get back to you with the results, because now I'm not totally sure if the I2C library I'm using has a working code in the first place, even for non-broken I2C implementation. Thanks! \$\endgroup\$ – 0x29a Dec 8 '16 at 2:45
2
\$\begingroup\$

The first byte transmitted in a I2C transaction is the slave address (7 bit) plus the single read/write bit. So if the slave address is 0x3c, the following is transmitted as the first byte:

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

That's 0x3c shifted by 1 bit plus - for read transactions - the LSB set.

The Raspberry library seems to handle this automatically. The ATMEGA328P library seems to require that you do it manually.

So use 0x78 as the address and try again.

Update

There are additional problems in your code: You need to indicate to the display controller if you send commands (and command parameters) or data. Basically, you have to prefix 0x80 before each command byte and 0x40 before you send data. Once you use 0x40, all bytes until the stop condition will be treated as data bytes. See figure 8-7 and chapter 8.1.5.2 lit. 5 in the datasheet.

That's why there is ssd1306_command and ssd1306_byte in the Raspberry code. (Obviously, 0x00 also works instead of 0x80.) So you have to add 0x80 before each initialization byte as well as before the the address commands when rendering a buffer. And you have to add 0x40 before you send the first data byte.

Note that you also have a difference with the transactions. (A transaction starts with the START condition and ends with the STOP condition.) In the Raspberry code, each command byte and each data byte is sent in a separate transaction. In the ATMEGA328P code, you combine it into far fewer but bigger transaction. Both approaches work. The latter one is more efficient. This might be relevant for a few minor differences such as 0x00 vs 0x80.

Just as a reference: You can find my reference code for the OLED display in Swift and C#. It's test code for Wirekite – an open-source solution for hooking I/Os including I2C to your Mac or PC using an inexpensive Teensy board and a USB cable. The application specific code is then run on your Mac or PC; the code on the Teensy is fixed.

\$\endgroup\$
1
\$\begingroup\$

I cannot be exactly sure and since I am not privileged yet to comment,

Don't you think that you are supposed to start or restart TWI communication after every command or else send a stop and issue a fresh start to it?

If that is your own library, you could read the TWI or I2C status code from the TWSR register and that would help you in debugging this problem. You could really check which part of your instructions fails by comparing with the status code given in the Datasheet.

EDIT : No guesses anymore, the first part of answer is wrong. No, you don't need start and stop bit after ever read and write. So to send a command to your Chip, you need to send 16 bit data , of which first 8 bits from MSB determine whether you are sending the data or command. Are you doing that?

\$\endgroup\$
  • \$\begingroup\$ Thanks for the response! I expected the library to handle it automatically, let me verify this, thanks. \$\endgroup\$ – 0x29a Dec 8 '16 at 0:47
  • \$\begingroup\$ @0x29a do put your i2c_write function as well in the question itself. It might help people in helping you. \$\endgroup\$ – MaNyYaCk Dec 8 '16 at 0:51
  • \$\begingroup\$ I attached a link to the library so that post doesn't get too long, thanks ) \$\endgroup\$ – 0x29a Dec 8 '16 at 1:25
  • \$\begingroup\$ @0x29a You need to do following \$\endgroup\$ – MaNyYaCk Dec 8 '16 at 1:38
  • \$\begingroup\$ @0x29a You need changed your code and make a function that sends a start byte first, then writes slave address, then writes a byte to show whether the next byte is command/data , then write the 'Command or Data' and then it send the stop byte. \$\endgroup\$ – MaNyYaCk Dec 8 '16 at 1:44
-1
\$\begingroup\$

The simplest way to port is to write your own version ofwiringPiI2CWriteReg8 and the corresponding setup function.

Recompile and you should be ready to go.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.