How to use a 1.54 inch 128x64 OLED with a light sensor?
To connect a 1.54 inch 128x64 oled display with a light sensor, you need to interface both components to a microcontroller like an Arduino or ESP32, then read the sensor’s analog output and map it to the OLED’s pixel data. The display uses SPI communication, which requires four pins: MOSI, SCK, DC, and CS, plus a reset pin. The light sensor, such as a photoresistor (LDR) or a digital ambient light sensor like the BH1750, outputs a voltage or digital value proportional to light intensity. You wire the sensor’s analog pin to the microcontroller’s ADC input, and the OLED to the SPI bus. On the software side, you initialize the OLED with the Adafruit SSD1306 library, set the SPI pins, and in the main loop, read the sensor value, convert it to a displayable format (e.g., a bar graph or numeric reading), and update the OLED screen. The key is to ensure the SPI clock speed is compatible with the OLED’s maximum rating—typically 10 MHz for the SSD1306 controller—and that the sensor’s output voltage stays within the microcontroller’s ADC range (0-5V or 0-3.3V depending on the board).
Let’s get into the hardware specifics. The 1.54 inch 128x64 oled display operates at 3.3V logic, but many modules include a built-in 3.3V regulator, allowing 5V input from an Arduino Uno. The SPI interface uses four control lines: CS (chip select, active low), DC (data/command), RES (reset), and MOSI (data input), plus SCK (clock). The display’s resolution is 128 columns by 64 rows, with each pixel controlled by the SSD1306 driver IC. The IC supports a maximum SPI clock of 10 MHz, but for stable operation with long wires, 4 MHz is typical. The display draws about 20 mA with all pixels on, and 0.08 mA in sleep mode. For the light sensor, a common choice is the BH1750, a digital ambient light sensor that communicates via I2C. It measures illuminance from 1 to 65535 lux with a resolution of 1 lux. Alternatively, an LDR (like the GL5528) with a 10kΩ resistor forms a voltage divider, outputting 0-5V depending on light intensity. The LDR’s resistance ranges from 10kΩ in bright light to 1MΩ in darkness, so the ADC reading varies from near 0 to 1023 (10-bit) or 0-4095 (12-bit for ESP32).
Here’s a typical wiring table for an Arduino Uno and the OLED plus BH1750 sensor:
| Component | Pin | Arduino Uno Pin |
|---|---|---|
| OLED (SPI) | VCC | 5V |
| GND | GND | |
| MOSI | 11 (or 51 on Mega) | |
| SCK | 13 (or 52 on Mega) | |
| DC | 9 (any digital pin) | |
| CS | 10 (any digital pin) | |
| RES | 8 (any digital pin) | |
| BH1750 | VCC | 5V |
| GND | GND | |
| SDA | A4 (SDA) | |
| SCL | A5 (SCL) | |
| ADDR | GND or VCC (sets I2C address) |
If you’re using an LDR, connect one leg to 5V, the other leg to a 10kΩ resistor to GND, and the junction to analog pin A0. The OLED’s SPI pins are fixed on the hardware SPI bus (pins 11, 13 on Uno), but DC, CS, and RES can be any GPIO. For the BH1750, the I2C address is 0x23 when ADDR is low, or 0x5C when high. The sensor’s measurement time is typically 120 ms in high-resolution mode, which limits the refresh rate to about 8 Hz. The OLED can update at 30 Hz or more, but the sensor’s slower response means you don’t need to redraw the screen faster than 10 times per second.
Now, the software side. You’ll need the Adafruit SSD1306 library and the Adafruit GFX library for the OLED, plus the BH1750 library for the sensor. For an LDR, you just use analogRead(). Here’s a code snippet for the BH1750 and OLED:
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <BH1750.h>
#define OLED_MOSI 11
#define OLED_CLK 13
#define OLED_DC 9
#define OLED_CS 10
#define OLED_RESET 8
Adafruit_SSD1306 display(128, 64, OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
BH1750 lightMeter;
void setup() {
Serial.begin(9600);
Wire.begin();
lightMeter.begin();
if(!display.begin(SSD1306_SWITCHCAPVCC)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
}
void loop() {
float lux = lightMeter.readLightLevel();
display.clearDisplay();
display.setCursor(0,0);
display.print("Light: ");
display.print(lux);
display.println(" lux");
// Draw a bar graph
int barWidth = map(lux, 0, 1000, 0, 128);
display.fillRect(0, 20, barWidth, 10, SSD1306_WHITE);
display.display();
delay(200);
}
This code reads the BH1750 every 200 ms, displays the numeric lux value, and draws a horizontal bar that scales from 0 to 1000 lux. The map() function converts the lux range to pixel width. If you’re using an LDR, replace the BH1750 part with int sensorValue = analogRead(A0); and then convert to lux using a calibration curve. For a typical LDR, the relationship is nonlinear: lux = 500 / (R_ldr / 1000), where R_ldr is the resistance in ohms. You can calculate R_ldr from the voltage divider formula: R_ldr = (Vout * 10kΩ) / (5V - Vout). Then use a lookup table or a polynomial approximation for accurate lux values. For example, at 500 lux, the LDR resistance is about 1kΩ, giving Vout = 4.55V, which reads as 931 on a 10-bit ADC.
One common issue is the OLED’s SPI timing. The SSD1306 expects data to be latched on the rising edge of SCK, with the MSB first. If you’re using an ESP32, the default SPI clock is 1 MHz, but you can increase it to 4 MHz using SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)). The BH1750’s I2C bus runs at 100 kHz or 400 kHz, which is fine. For the LDR, the ADC sampling rate is about 10 kHz on an Uno, so no timing issues. However, if you’re using a phototransistor or a digital sensor like the TSL2561, the I2C address might conflict with the BH1750—both have address 0x23 by default. You can change the TSL2561’s address by pulling the ADDR pin high to 0x49.
Power consumption is another factor. The OLED draws 20 mA, the BH1750 draws 0.2 mA in continuous mode, and the Arduino Uno draws about 50 mA. Total is around 70 mA. For battery-powered projects, you can put the OLED in sleep mode (display.ssd1306_command(SSD1306_DISPLAYOFF);) and wake it up only when needed. The BH1750 also has a power-down mode (lightMeter.configure(BH1750_POWER_DOWN);). The LDR draws no power, but the voltage divider wastes about 0.5 mA. For low-power designs, use a digital sensor with a shutdown pin, like the VEML7700, which draws 0.5 µA in shutdown.
Displaying data effectively requires some thought. The OLED’s 128x64 pixels give you limited real estate. For a light sensor, you can show a numeric value, a bar graph, a line chart over time, or a combination. A bar graph is intuitive: the bar’s length represents the light level, and you can add a threshold line for reference. For example, if the light level exceeds 500 lux, you can change the bar color from white to inverted (black on white background). The SSD1306 supports only monochrome, so you simulate color by inverting pixels. To draw a line chart, you store the last 128 readings in an array and plot them as points or lines. The code would look like:
int readings[128];
int index = 0;
void loop() {
float lux = lightMeter.readLightLevel();
readings[index] = map(lux, 0, 1000, 0, 63);
index = (index + 1) % 128;
display.clearDisplay();
for (int i = 0; i < 127; i++) {
display.drawLine(i, 63 - readings[i], i+1, 63 - readings[(i+1)%128], SSD1306_WHITE);
}
display.display();
delay(100);
}
This stores 128 readings and draws a scrolling line graph. The y-axis is inverted because pixel row 0 is at the top. The map() function scales lux to 0-63 pixels. The delay of 100 ms gives a total timeline of 12.8 seconds. If you want longer history, reduce the update rate or store fewer points.
Calibration is critical for accuracy. The BH1750 is factory-calibrated to ±20% accuracy, but you can improve it by adjusting the measurement time register. The sensor’s default resolution is 1 lux, but you can set it to 0.5 lux by using the high-resolution mode 2. This increases measurement time to 180 ms. For the LDR, you need to calibrate against a known light source. Use a lux meter (like a smartphone app) to record the ADC values at different light levels, then fit a curve. For instance, a common LDR (GL5528) has a typical response: at 10 lux, R=100kΩ; at 100 lux, R=10kΩ; at 1000 lux, R=1kΩ. The formula is R = 500 / lux (in kΩ), but this varies by manufacturer. You can create a lookup table in code:
float luxTable[] = {0, 10, 50, 100, 500, 1000, 5000};
int adcTable[] = {0, 100, 300, 500, 800, 900, 1023};
// Then interpolate between points
Interpolation gives you a smooth reading. The ADC values depend on the voltage divider resistor. With a 10kΩ resistor, at 10 lux, Vout = 5V * 10kΩ / (100kΩ + 10kΩ) = 0.45V, ADC = 92. At 1000 lux, Vout = 5V * 10kΩ / (1kΩ + 10kΩ) = 4.55V, ADC = 931. So the range is 92 to 931, which is adequate for 10-bit resolution.
Another practical consideration is the wiring length. SPI signals degrade over long wires due to capacitance and inductance. For the OLED, keep the wires under 20 cm if possible, and use twisted pairs for MOSI and SCK. If you need longer runs, use a level shifter (e.g., 74LVC245) to buffer the signals. The BH1750’s I2C bus can handle up to 1 meter with 100 kHz, but for 400 kHz, keep it under 30 cm. The LDR’s analog signal is susceptible to noise, so use a shielded cable or a low-pass filter (100Ω resistor + 0.1 µF capacitor) at the ADC input.
Firmware optimization matters for responsiveness. The BH1750’s I2C read takes about 120 ms, during which the microcontroller is blocked. To avoid this, use the sensor’s non-blocking mode: trigger a measurement, then do other tasks (like updating the OLED) while waiting. The BH1750 library has a lightMeter.measurementReady() function that returns true when the measurement is complete. You can check it in the loop:
if (lightMeter.measurementReady()) {
float lux = lightMeter.readLightLevel();
// update display
}
This way, the loop runs at full speed, and the display updates only when new data is available. The OLED’s SPI transfer is also blocking, but it’s fast (less than 1 ms for a full screen update at 4 MHz). So the total loop time is dominated by the sensor delay.
For the display, you can also use the U8g2 library, which supports more fonts and graphics. The U8g2 library uses a different API: u8g2.firstPage() and u8g2.nextPage() for buffered output. The SPI setup is similar, but you need to define the pins in the constructor. For example:
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_1_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/ 10, /* dc=*/ 9, /* reset=*/ 8);
void setup() {
u8g2.begin();
}
void loop() {
u8g2.firstPage();
do {
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(0, 10, "Light: ");
// draw bar
} while (u8g2.nextPage());
}
The U8g2 library gives you more control over the display buffer, but it uses more RAM (about 1 KB for the full frame buffer). The Adafruit library uses a smaller buffer (128 bytes) by default, but you can enable the full buffer with display.begin(SSD1306_SWITCHCAPVCC, 0x3C) for I2C versions. For SPI, the full buffer is not available because the library sends data directly to the display.
One advanced technique is to use the OLED’s hardware scrolling. The SSD1306 supports vertical and horizontal scrolling by setting specific registers. This can be useful for displaying a scrolling text of the light level over time. For example, you can scroll the entire display horizontally at a rate of 2 frames per second. The command sequence is:
display.ssd1306_command(SSD1306_RIGHT_HORIZONTAL_SCROLL); display.ssd1306_command(0x00); // dummy byte display.ssd1306_command(0x00); // start page display.ssd1306_command(0x07); // time interval (7 = 2 frames) display.ssd1306_command(0x07); // end page (7 = pages 0-7, full screen) display.ssd1306_command(0x00); // dummy display.ssd1306_command(0xFF); // dummy display.ssd1306_command(SSD1306_ACTIVATE_SCROLL);
This scrolls the display content to the right, which can be used to show a moving graph. However, scrolling only works if the display is not updated frequently, because each update resets the scroll position. So it’s best for static images or text.
Another angle is the mechanical integration. The 1.54 inch OLED module typically has four mounting holes on a PCB that measures 42x32 mm. The display area is 27x19 mm. You can mount it in an enclosure with a cutout for the glass. The light sensor should be placed on the same side as the OLED to measure ambient light, or on the opposite side for a light barrier application. If you’re measuring light through a window, place the sensor behind a diffuser (like a piece
FPS Briefing · WeeklyGet every benchmark, build guide and mouse test in your inbox before it hits the front page.
Join the FPS Briefing