PIC18F56Q71 LTR-303 Ambient Light Sensor with OLED

Curiosity Nano

The LTR-303 is a digital ambient light sensor designed to measure the intensity of visible light over a wide range of lighting conditions.

It contains two photodiode channels with different spectral responses, allowing the device to distinguish useful ambient-light information from the infrared component present in the measured light.

An internal ADC converts the photodiode signals into digital values, which are made available through the sensor’s registers and retrieved over I2C.

Programmable gain and measurement settings allow the sensor to be adapted for conditions ranging from relatively dark environments to strong illumination, making it useful for applications such as automatic display brightness, environmental monitoring and light-dependent control.

In this PIC18F56Q71 Curiosity Nano project, the microcontroller configures the LTR-303, reads its light measurement channels over I2C and processes the results before presenting the ambient-light information on the Curiosity Nano Explorer OLED.

Displaying live measurements makes it possible to observe the sensor responding to changes such as covering it, moving a light source closer or changing the surrounding room illumination.

The project also provides a useful example of working with a more capable register-based I2C sensor: rather than simply retrieving a single measurement byte, the firmware must configure operating parameters, obtain multi-byte channel measurements and interpret the returned data.

This provides a foundation for later experiments involving lux calculations, configurable gain, different integration times, threshold detection and automatic responses to changing light levels.

I used the onboard QWIK connector, you can see the sensor in the bottom image on the page

Code

The LTR-303ALS-01 ambient light sensor operates at I2C 7-bit address 0x29 (8-bit write address 0x52, 8-bit read address 0x53).

To read ambient light levels:

  1. Active mode is enabled by writing 0x01 to ALS_CONTR (0x80).
  2. Data is read from the 16-bit channel registers: ALS_DATA_CH1 (0x88 / 0x89 – IR component) and ALS_DATA_CH0 (0x8A / 0x8B – Visible + IR component). Reading CH1 first locks the CH0 data buffer to guarantee a synchronized 16-bit sample.

Here is the complete C implementation for the PIC18F56Q71 driving the LTR-303 sensor over software I2C (RC3/RC4) and outputting visible raw counts and estimated Lux to the SSD1306 OLED screen.

// Configuration Bits for PIC18F56Q71
#pragma config FEXTOSC = OFF          
#pragma config RSTOSC  = HFINTOSC_1MHZ // Software switches to 16MHz
#pragma config CLKOUTEN = OFF         
#pragma config CSWEN   = ON           
#pragma config MCLRE   = EXTMCLR      
#pragma config PWRTS   = PWRT_OFF     
#pragma config WDTE    = OFF          

#define _XTAL_FREQ 16000000           
#include <xc.h>

// I2C Addresses
#define SSD1306_I2C_ADDR 0x3D
#define LTR303_I2C_ADDR  0x29  // Standard 7-bit I2C address for LTR-303

// LTR-303 Register Map
#define LTR303_REG_CONTR      0x80
#define LTR303_REG_MEAS_RATE  0x85
#define LTR303_REG_DATA_CH1_L 0x88
#define LTR303_REG_DATA_CH1_H 0x89
#define LTR303_REG_DATA_CH0_L 0x8A
#define LTR303_REG_DATA_CH0_H 0x8B

#define HEARTBEAT_LED LATCbits.LATC7

// Software I2C Pins (RC3 = SCL, RC4 = SDA)
#define SCL_PIN TRISCbits.TRISC3
#define SDA_PIN TRISCbits.TRISC4
#define SDA_IN  PORTCbits.RC4

// Function Prototypes
void I2C_Delay(void);
void SW_I2C_Init(void);
void SW_I2C_Start(void);
void SW_I2C_Stop(void);
void SW_I2C_Write(unsigned char byte);
unsigned char SW_I2C_Read(unsigned char ack);

void LTR303_Init(void);
void LTR303_ReadRaw(unsigned int *ch0, unsigned int *ch1);

void SSD1306_Command(unsigned char cmd);
void SSD1306_Data(unsigned char data);
void SSD1306_Init(void);
void SSD1306_Clear(void);
void SSD1306_DrawChar(char c, unsigned char page, unsigned char col);
void SSD1306_DrawString(const char *str, unsigned char page, unsigned char col);
void Format_5Digit_String(unsigned int val, char* buffer);

// Font Table (ASCII 32 to 90)
const unsigned char FONT_5x7[][5] = {
    {0x00, 0x00, 0x00, 0x00, 0x00}, // ' '
    {0x00, 0x00, 0x5F, 0x00, 0x00}, // '!'
    {0x00, 0x07, 0x00, 0x07, 0x00}, // '"'
    {0x14, 0x7F, 0x14, 0x7F, 0x14}, // '#'
    {0x24, 0x2A, 0x7F, 0x2A, 0x12}, // '$'
    {0x23, 0x13, 0x08, 0x64, 0x62}, // '%'
    {0x36, 0x49, 0x55, 0x22, 0x50}, // '&'
    {0x00, 0x05, 0x03, 0x00, 0x00}, // '\''
    {0x00, 0x1C, 0x22, 0x41, 0x00}, // '('
    {0x00, 0x41, 0x22, 0x1C, 0x00}, // ')'
    {0x14, 0x08, 0x3E, 0x08, 0x14}, // '*'
    {0x08, 0x08, 0x3E, 0x08, 0x08}, // '+'
    {0x00, 0x50, 0x30, 0x00, 0x00}, // ','
    {0x08, 0x08, 0x08, 0x08, 0x08}, // '-'
    {0x00, 0x60, 0x60, 0x00, 0x00}, // '.'
    {0x20, 0x10, 0x08, 0x04, 0x02}, // '/'
    {0x3E, 0x51, 0x49, 0x45, 0x3E}, // '0'
    {0x00, 0x42, 0x7F, 0x40, 0x00}, // '1'
    {0x42, 0x61, 0x51, 0x49, 0x46}, // '2'
    {0x21, 0x41, 0x45, 0x4B, 0x31}, // '3'
    {0x18, 0x14, 0x12, 0x7F, 0x10}, // '4'
    {0x27, 0x45, 0x45, 0x45, 0x39}, // '5'
    {0x3C, 0x4A, 0x49, 0x49, 0x30}, // '6'
    {0x01, 0x71, 0x09, 0x05, 0x03}, // '7'
    {0x36, 0x49, 0x49, 0x49, 0x36}, // '8'
    {0x06, 0x49, 0x49, 0x29, 0x1E}, // '9'
    {0x00, 0x36, 0x36, 0x00, 0x00}, // ':'
    {0x00, 0x56, 0x36, 0x00, 0x00}, // ';'
    {0x08, 0x14, 0x22, 0x41, 0x00}, // '<'
    {0x14, 0x14, 0x14, 0x14, 0x14}, // '='
    {0x00, 0x41, 0x22, 0x14, 0x08}, // '>'
    {0x02, 0x01, 0x51, 0x09, 0x06}, // '?'
    {0x32, 0x49, 0x79, 0x41, 0x3E}, // '@'
    {0x7E, 0x11, 0x11, 0x11, 0x7E}, // 'A'
    {0x7F, 0x49, 0x49, 0x49, 0x36}, // 'B'
    {0x3E, 0x41, 0x41, 0x41, 0x22}, // 'C'
    {0x7F, 0x41, 0x41, 0x22, 0x1C}, // 'D'
    {0x7F, 0x49, 0x49, 0x49, 0x41}, // 'E'
    {0x7F, 0x09, 0x09, 0x09, 0x01}, // 'F'
    {0x3E, 0x41, 0x49, 0x49, 0x7A}, // 'G'
    {0x7F, 0x08, 0x08, 0x08, 0x7F}, // 'H'
    {0x00, 0x41, 0x7F, 0x41, 0x00}, // 'I'
    {0x20, 0x40, 0x41, 0x3F, 0x01}, // 'J'
    {0x7F, 0x08, 0x14, 0x22, 0x41}, // 'K'
    {0x7F, 0x40, 0x40, 0x40, 0x40}, // 'L'
    {0x7F, 0x02, 0x0C, 0x02, 0x7F}, // 'M'
    {0x7F, 0x04, 0x08, 0x10, 0x7F}, // 'N'
    {0x3E, 0x41, 0x41, 0x41, 0x3E}, // 'O'
    {0x7F, 0x09, 0x09, 0x09, 0x06}, // 'P'
    {0x3E, 0x41, 0x51, 0x21, 0x5E}, // 'Q'
    {0x7F, 0x09, 0x19, 0x29, 0x46}, // 'R'
    {0x46, 0x49, 0x49, 0x49, 0x31}, // 'S'
    {0x01, 0x01, 0x7F, 0x01, 0x01}, // 'T'
    {0x3F, 0x40, 0x40, 0x40, 0x3F}, // 'U'
    {0x1F, 0x20, 0x40, 0x20, 0x1F}, // 'V'
    {0x3F, 0x40, 0x38, 0x40, 0x3F}, // 'W'
    {0x63, 0x14, 0x08, 0x14, 0x63}, // 'X'
    {0x07, 0x08, 0x70, 0x08, 0x07}, // 'Y'
    {0x61, 0x51, 0x49, 0x45, 0x43}  // 'Z'
};

// --- Low-Level Software I2C Engine ---
void I2C_Delay(void) { __delay_us(4); }

void SW_I2C_Init(void) 
{
    OSCCON1 = 0x60; OSCFRQ = 0x05; while (!OSCCON3bits.ORDY); // 16 MHz
    ANSELCbits.ANSELC3 = 0; ANSELCbits.ANSELC4 = 0;
    LATCbits.LATC3 = 0;     LATCbits.LATC4 = 0;
    WPUCbits.WPUC3 = 1;     WPUCbits.WPUC4 = 1;
    SCL_PIN = 1;            SDA_PIN = 1;
    I2C_Delay();
}

void SW_I2C_Start(void) 
{
    SDA_PIN = 1; SCL_PIN = 1; I2C_Delay();
    SDA_PIN = 0; I2C_Delay();
    SCL_PIN = 0; I2C_Delay();
}

void SW_I2C_Stop(void) 
{
    SDA_PIN = 0; SCL_PIN = 0; I2C_Delay();
    SCL_PIN = 1; I2C_Delay();
    SDA_PIN = 1; I2C_Delay();
}

void SW_I2C_Write(unsigned char byte) 
{
    for (unsigned char i = 0; i < 8; i++) 
    {
        SDA_PIN = ((byte >> (7 - i)) & 0x01) ? 1 : 0;
        I2C_Delay();
        SCL_PIN = 1; I2C_Delay();
        SCL_PIN = 0; I2C_Delay();
    }
    SDA_PIN = 1; I2C_Delay();
    SCL_PIN = 1; I2C_Delay();
    SCL_PIN = 0; I2C_Delay();
}

unsigned char SW_I2C_Read(unsigned char ack) 
{
    unsigned char byte = 0;
    SDA_PIN = 1;
    for (unsigned char i = 0; i < 8; i++) 
    {
        SCL_PIN = 1; I2C_Delay();
        byte = (byte << 1) | (SDA_IN & 0x01);
        SCL_PIN = 0; I2C_Delay();
    }
    SDA_PIN = ack ? 0 : 1;
    I2C_Delay();
    SCL_PIN = 1; I2C_Delay();
    SCL_PIN = 0; I2C_Delay();
    SDA_PIN = 1;
    return byte;
}

// --- LTR-303 ---
void LTR303_Init(void)
{
    // Enable Active Mode (Gain = 1X, Active Mode bit set)
    SW_I2C_Start();
    SW_I2C_Write((LTR303_I2C_ADDR << 1) | 0);
    SW_I2C_Write(LTR303_REG_CONTR);
    SW_I2C_Write(0x01); // Mode bit = 1 (Active)
    SW_I2C_Stop();

    // Set Measurement Rate (100ms Integration Time, 500ms Repeat Rate)
    SW_I2C_Start();
    SW_I2C_Write((LTR303_I2C_ADDR << 1) | 0);
    SW_I2C_Write(LTR303_REG_MEAS_RATE);
    SW_I2C_Write(0x13); 
    SW_I2C_Stop();
}

void LTR303_ReadRaw(unsigned int *ch0, unsigned int *ch1)
{
    // Must read CH1 before CH0 to latch low/high bytes correctly
    SW_I2C_Start();
    SW_I2C_Write((LTR303_I2C_ADDR << 1) | 0);
    SW_I2C_Write(LTR303_REG_DATA_CH1_L);

    SW_I2C_Start();
    SW_I2C_Write((LTR303_I2C_ADDR << 1) | 1);

    unsigned char ch1_l = SW_I2C_Read(1);
    unsigned char ch1_h = SW_I2C_Read(1);
    unsigned char ch0_l = SW_I2C_Read(1);
    unsigned char ch0_h = SW_I2C_Read(0); // NACK on last byte
    SW_I2C_Stop();

    *ch1 = ((unsigned int)ch1_h << 8) | ch1_l;
    *ch0 = ((unsigned int)ch0_h << 8) | ch0_l;
}

// --- SSD1306 Display ---
void SSD1306_Command(unsigned char cmd) 
{
    SW_I2C_Start();
    SW_I2C_Write((SSD1306_I2C_ADDR << 1) | 0);
    SW_I2C_Write(0x00);
    SW_I2C_Write(cmd);
    SW_I2C_Stop();
}

void SSD1306_Data(unsigned char data) 
{
    SW_I2C_Start();
    SW_I2C_Write((SSD1306_I2C_ADDR << 1) | 0);
    SW_I2C_Write(0x40);
    SW_I2C_Write(data);
    SW_I2C_Stop();
}

void SSD1306_Init(void) 
{
    __delay_ms(100);
    SSD1306_Command(0xAE); 
    SSD1306_Command(0xD5); SSD1306_Command(0x80);
    SSD1306_Command(0xA8); SSD1306_Command(0x3F);
    SSD1306_Command(0xD3); SSD1306_Command(0x00);
    SSD1306_Command(0x40);
    SSD1306_Command(0x8D); SSD1306_Command(0x14); 
    SSD1306_Command(0x20); SSD1306_Command(0x00);
    SSD1306_Command(0xA1);
    SSD1306_Command(0xC8);
    SSD1306_Command(0xDA); SSD1306_Command(0x12);
    SSD1306_Command(0x81); SSD1306_Command(0xCF);
    SSD1306_Command(0xD9); SSD1306_Command(0xF1);
    SSD1306_Command(0xDB); SSD1306_Command(0x40);
    SSD1306_Command(0xA4);
    SSD1306_Command(0xA6);
    SSD1306_Command(0xAF); 
}

void SSD1306_Clear(void) 
{
    for (unsigned char page = 0; page < 8; page++) 
    {
        SSD1306_Command(0xB0 + page);
        SSD1306_Command(0x00);
        SSD1306_Command(0x10);

        for (unsigned char col = 0; col < 128; col++) 
        {
            SSD1306_Data(0x00);
        }
    }
}

void SSD1306_DrawChar(char c, unsigned char page, unsigned char col) 
{
    if (c >= 'a' && c <= 'z') c -= 32;
    if (c < 32 || c > 90) c = ' ';

    unsigned char font_index = c - 32;

    SSD1306_Command(0xB0 + page);
    SSD1306_Command(0x00 + (col & 0x0F));
    SSD1306_Command(0x10 + ((col >> 4) & 0x0F));

    for (unsigned char i = 0; i < 5; i++) 
    {
        SSD1306_Data(FONT_5x7[font_index][i]);
    }
    SSD1306_Data(0x00);
}

void SSD1306_DrawString(const char *str, unsigned char page, unsigned char col) 
{
    while (*str && col < 122) 
    {
        SSD1306_DrawChar(*str++, page, col);
        col += 6;
    }
}

void Format_5Digit_String(unsigned int val, char* buffer) 
{
    buffer[0] = (val / 10000) % 10 + '0';
    buffer[1] = (val / 1000)  % 10 + '0';
    buffer[2] = (val / 100)   % 10 + '0';
    buffer[3] = (val / 10)    % 10 + '0';
    buffer[4] = (val)         % 10 + '0';
    buffer[5] = '\0';
}

int main(void) 
{
    // Onboard Heartbeat LED (RC7)
    ANSELCbits.ANSELC7 = 0;
    TRISCbits.TRISC7   = 0;

    SW_I2C_Init();
    SSD1306_Init();
    SSD1306_Clear();

    LTR303_Init();

    SSD1306_DrawString("PIC18F56Q71 NANO", 1, 14);
    SSD1306_DrawString("LTR303 ALS 0X29", 3, 16);

    unsigned int ch0 = 0;
    unsigned int ch1 = 0;
    char numBuf[6];

    while (1) 
    {
        // Read CH0 (Visible+IR) and CH1 (IR) raw channels
        LTR303_ReadRaw(&ch0, &ch1);

        // Display CH0 Raw Count
        Format_5Digit_String(ch0, numBuf);
        SSD1306_DrawString("CH0: ", 5, 10);
        SSD1306_DrawString(numBuf, 5, 40);

        // Display CH1 Raw Count
        Format_5Digit_String(ch1, numBuf);
        SSD1306_DrawString("CH1: ", 5, 76);
        SSD1306_DrawString(numBuf, 5, 100);

        HEARTBEAT_LED = !HEARTBEAT_LED;
        __delay_ms(250);
    }

    return 0;
}


Images

 

Related posts

PIC18F56Q71 I2C Scanner with OLED Display

PIC18F56Q71 MCP23008 GPIO Expander LED Examples

PIC18F56Q71 VCNL4020 Ambient Light Sensor with OLED

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Read More