Now we take a look at the second function of the VCNL4020 – ambient light
The ambient light capability of the VCNL4020 can be used with the PIC18F56Q71 to measure changes in surrounding illumination and present the readings directly on the Curiosity Nano Explorer OLED.
The firmware periodically obtains the sensor’s ambient light measurement over I2C and updates the display as lighting conditions change.
Covering the sensor, moving it toward a window or changing the artificial lighting around the board produces an immediately visible response, making this a useful experiment for examining real sensor behaviour rather than simply confirming that an I2C transaction succeeds.
Ambient light measurement also introduces different considerations from proximity sensing even though both functions are provided by the same VCNL4020.
The firmware must select and configure the appropriate ambient-light measurement function, retrieve the corresponding result and decide how frequently the OLED should be refreshed.
Observing the readings under several lighting conditions provides a useful basis for later projects involving automatic display brightness, light/dark detection, environmental monitoring or threshold-based control.
It also demonstrates how a single I2C device can expose several independent measurement functions through its internal register interface.
To measure ambient light with the VCNL4200, you read the ALS_DATA register (0x09).
Unlike proximity (which uses the IR LED emitter), the Ambient Light Sensor (ALS) measures natural or room light in raw counts. Multiplying the raw 16-bit ALS value by 0.024 converts the reading directly into Lux (lx).
Image

VCNL4200 ALS and OLED
PIC18F56Q71 VCNL4200 Ambient Light (ALS) Code
// 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 VCNL4200_I2C_ADDR 0x51
// VCNL4200 Registers
#define VCNL4200_REG_ALS_CONF 0x00
#define VCNL4200_REG_ALS_DATA 0x09
#define HEARTBEAT_LED LATCbits.LATC7
// Software I2C Pin Definitions (RC3 = SCL, RC4 = SDA)
#define SCL_PIN TRISCbits.TRISC3
#define SDA_PIN TRISCbits.TRISC4
#define SDA_IN PORTCbits.RC4
// Font Table (ASCII 32 to 90: ' ' through 'Z')
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'
};
// Software I2C
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;
}
// VCNL4200 ALS
void VCNL4200_ALS_Init(void)
{
SW_I2C_Start();
SW_I2C_Write((VCNL4200_I2C_ADDR << 1) | 0);
SW_I2C_Write(VCNL4200_REG_ALS_CONF);
// ALS_CONF: Integration time = 100ms, ALS power ON (Bit 0 = 0)
SW_I2C_Write(0x00);
SW_I2C_Write(0x00);
SW_I2C_Stop();
}
unsigned int VCNL4200_ReadALS(void)
{
SW_I2C_Start();
SW_I2C_Write((VCNL4200_I2C_ADDR << 1) | 0);
SW_I2C_Write(VCNL4200_REG_ALS_DATA);
SW_I2C_Start();
SW_I2C_Write((VCNL4200_I2C_ADDR << 1) | 1);
unsigned char lowByte = SW_I2C_Read(1); // Read Low byte with ACK
unsigned char highByte = SW_I2C_Read(0); // Read High byte with NACK
SW_I2C_Stop();
return ((unsigned int)highByte << 8) | lowByte;
}
// 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_Lux_String(float lux, char* buffer)
{
int whole = (int)lux;
int decimal = (int)((lux - (float)whole) * 10.0f);
if (decimal < 0) decimal = -decimal;
// Convert whole Lux (0-9999)
buffer[0] = (whole / 1000) % 10 + '0';
buffer[1] = (whole / 100) % 10 + '0';
buffer[2] = (whole / 10) % 10 + '0';
buffer[3] = (whole) % 10 + '0';
buffer[4] = '.';
buffer[5] = decimal + '0';
buffer[6] = ' ';
buffer[7] = 'L';
buffer[8] = 'X';
buffer[9] = '\0';
}
int main(void)
{
// Heartbeat LED
ANSELCbits.ANSELC7 = 0;
TRISCbits.TRISC7 = 0;
SW_I2C_Init();
SSD1306_Init();
SSD1306_Clear();
VCNL4200_ALS_Init();
SSD1306_DrawString("PIC18F56Q71 NANO", 1, 14);
SSD1306_DrawString("LIGHT SENSOR ALS", 3, 14);
char luxStr[11];
while (1)
{
// Read raw 16-bit ALS value
unsigned int rawALS = VCNL4200_ReadALS();
// Convert raw count to Lux (1 count = 0.024 Lux)
float lux = (float)rawALS * 0.024f;
// Format into ASCII string
Format_Lux_String(lux, luxStr);
// Render on OLED
SSD1306_DrawString("LUX: ", 5, 16);
SSD1306_DrawString(luxStr, 5, 46);
HEARTBEAT_LED = !HEARTBEAT_LED; // Toggle heartbeat LED
__delay_ms(250);
}
return 0;
}
Results
- Flash using MPLAB: Program Device.
- Heartbeat LED (RC7) toggles every 250 ms.
- The OLED screen displays:
- PIC18F56Q71 NANO (Line 1)
- LIGHT SENSOR ALS (Line 3)
- LUX: 0045.2 LX (Line 5)
- Covering the sensor lowers the reading toward 0000.0 LX, while shining a flashlight over it scales the Lux reading up dynamically.
You can see this in this demo video

