PIC18F56Q71 VCNL4020 Proximity Sensor with OLED

Curiosity Nano

In this example

The VCNL4020 proximity sensor provides a useful next step for the PIC18F56Q71 Curiosity Nano Explorer projects because it introduces an optical sensing device whose output can change rapidly as an object moves toward or away from the sensor.

The PIC18F56Q71 communicates with the VCNL4020 over I2C, reads the proximity measurement registers and converts the returned data into a value that can be displayed on the OLED.

Since the OLED interface is already established from the earlier display examples, the firmware can concentrate on configuring the sensor, requesting measurements and presenting the changing proximity data clearly.

Displaying the raw proximity value on the OLED makes it much easier to understand how the VCNL4020 responds to different targets, distances and surface conditions.

The readings can be observed while moving a hand or object in front of the sensor, providing an immediate view of the sensor’s usable range and response characteristics. This also creates a good foundation for later projects that add thresholds, object-detection indicators, alarms or simple gesture-style interactions.

The same I2C communication structure can be reused for other digital sensors, while the OLED provides a convenient way to verify measurements without relying on a connected serial terminal.

The VCNL4020 is a fully integrated proximity and ambient light sensor manufactured by Vishay that combines an infrared emitter, photo-pin-diodes, and signal processing IC in a single SMD-10 package.

It offers a proximity detection range of 0 to 200mm and an ambient light measurement range of 0.25 to 16,000 lux, utilizing a standard I²C interface for communication.

Key features include 16-bit resolution, programmable LED drive currents from 10mA to 200mA, and an interrupt function for efficient power management.

What you will see

VCNL4200 prox and OLED

PIC18F56Q71 VCNL4200 + SSD1306 Driver

Replace main.c with this 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 on Explorer Board
#define SSD1306_I2C_ADDR  0x3D
#define VCNL4200_I2C_ADDR 0x51  // VCNL4200 Address: 0x51

// VCNL4200 Registers
#define VCNL4200_REG_PS_CONF1 0x03
#define VCNL4200_REG_PS_DATA  0x08

#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 Driver 
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 Sensor Driver 
void VCNL4200_Init(void) 
{
    SW_I2C_Start();
    SW_I2C_Write((VCNL4200_I2C_ADDR << 1) | 0);
    SW_I2C_Write(VCNL4200_REG_PS_CONF1);
    SW_I2C_Write(0x00); // PS_CONF1: Enable IR LED & integration time
    SW_I2C_Write(0x00); // PS_CONF2: Enable PS power & 16-bit mode
    SW_I2C_Stop();
}

unsigned int VCNL4200_ReadProximity(void) 
{
    SW_I2C_Start();
    SW_I2C_Write((VCNL4200_I2C_ADDR << 1) | 0);
    SW_I2C_Write(VCNL4200_REG_PS_DATA);

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

    unsigned char lowByte  = SW_I2C_Read(1);
    unsigned char highByte = SW_I2C_Read(0);
    SW_I2C_Stop();

    return ((unsigned int)highByte << 8) | lowByte;
}

//  SSD1306 Display Engine 
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_Proximity_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) 
{
    ANSELCbits.ANSELC7 = 0;
    TRISCbits.TRISC7   = 0; // Heartbeat LED (RC7)

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

    SSD1306_DrawString("PIC18F56Q71 NANO", 1, 14);
    SSD1306_DrawString("VCNL4200 SENSOR", 3, 16);

    char proxStr[8];

    while (1) 
    {
        // 1Read raw 16-bit proximity count
        unsigned int proximity = VCNL4200_ReadProximity();

        // 2. Format integer count into string
        Format_Proximity_String(proximity, proxStr);

        // 3. Display raw count on Line 5
        SSD1306_DrawString("RAW: ", 5, 10);
        SSD1306_DrawString(proxStr, 5, 38);

        // 4. Evaluate count against thresholds and render status on Line 5
        if (proximity < 100) 
        {
            SSD1306_DrawString("[CLEAR]", 5, 80);
        } 
        else if (proximity < 2000) 
        {
            SSD1306_DrawString("[FAR]  ", 5, 80);
        } 
        else if (proximity < 15000) 
        {
            SSD1306_DrawString("[MID]  ", 5, 80);
        } 
        else 
        {
            SSD1306_DrawString("[CLOSE]", 5, 80);
        }

        HEARTBEAT_LED = !HEARTBEAT_LED; // Toggle heartbeat LED on RC7
        __delay_ms(200);
    }

    return 0;
}

Process

  1. Flash using MPLAB: Program Device.
  2. Heartbeat LED (RC7) will toggle rapidly (5 Hz).
  3. The OLED screen will display:
  • PIC18F56Q71 NANO (Line 1)
  • VCNL4200 SENSOR (Line 3)
  • PROX: 00124 (Line 5)
  1. Move your hand closer to or farther from the VCNL4200 sensor—the proximity integer count will scale dynamically in real time.

Video

Here is the use case with my big hand in view as an accurate test mechanism

Related posts

PIC18F56Q71 VCNL4020 Ambient Light Sensor with OLED

PIC18F56Q71 MCP9808 Temperature Sensor with OLED

PIC18F56Q71 OLED with Curiosity Nano Explorer

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