Skip to content

Example: build a GL5528 photoresistor

The GL5528 is the 5 mm photoresistor most light-sensing projects start with: a cadmium sulphide film between two leads, whose resistance falls as the light on it rises. This page builds one from its datasheet.

It’s a resistor and nothing else, so it only needs circuit logic: one conductance, decided once per tick from the light falling on its face. It has no component logic.

  1. On the Home screen, open the Component Editor and press New Component.

  2. Press Import 3D Model and choose a model of the part (.glb, .gltf or .fbx): a small disc on two legs.

  3. Under CONNECTIVITY, press Add Pin twice, and name the first A and the second B. Select each in turn and move it to the end of a leg with the Translate tool in the 3D view’s toolbar, or type its Position (mm) in the inspector. A photoresistor has no polarity, so either can go to the supply.

  4. Under PHYSICS, press Add Receiver for the film’s face, and set it up:

    • Identifier Window, the name the code reads it by.
    • Field Name Lux.
    • Shape Cone, with a Cone Half-Angle of 89.5. A cone that wide is a flat face: it sees what’s in front of it and nothing behind, so a lamp level with the part or behind it adds nothing.
    • Extends To Infinity on, so the face can see the sun.

    Move it to the top of the face with Translate, and turn it to point straight up with Rotate.

  5. Under PROGRAMMING, tick Has Circuit Logic. Select the circuit logic, and press Add Property once for each property below. Leave each Property Type on Double, and set its default:

    • Lux = 300. The light in lux when nothing on the bench says otherwise: no environment zone around the part and no lamp in reach. 300 is an ordinary lit room.
    • ResistanceAt10Lux = 10000. Ohms at 10 lux, the point the datasheet quotes. The GL5528 is 10 to 20 kΩ, so 10 kΩ is the low end. With Gamma, it sets the whole curve.
    • Gamma = 0.6. The slope of the curve on log paper. At 0.6, ten times the light divides the resistance by four.
    • DarkResistance = 1000000. The ceiling in the dark: at least 1 MΩ.
    • RiseTime = 0.02. Seconds the film takes to respond when the light comes on.
    • DecayTime = 0.03. Seconds it takes when the light goes off, slower than the rise.
    • MaxPower = 0.1. The watts the film survives. 0 makes a part that can’t burn out.
  6. Put the code below into the circuit logic’s code editor, replacing the code it starts with, then press Save. The part goes into your library with your own parts.

Other photoresistors in the GL55 family differ only in their numbers: make a copy with Save As… and change ResistanceAt10Lux, Gamma and DarkResistance to match their datasheet.

The blocks below are pieces of one circuit logic. Put them in the editor one after another, in this order. There’s no class around them (see What you write).

What the part remembers from tick to tick:

private double _gLatched; // the conductance this tick, decided once in PreStep
private double _logOhms; // log10 of the resistance the film has reached
private bool _seeded; // false until the first tick has placed the film on its curve
private double _voltsAcross;
private double _overstressSeconds;
private int _overstressTicks;

On log paper, resistance against light is a straight line with slope −γ, anchored at the datasheet’s 10 lux point, and the film stops at its dark resistance:

R = R₁₀ × (lux / 10)−γ

private double TargetOhms(double lux)
{
double r10 = Properties.ResistanceAt10Lux < 1.0 ? 1.0 : Properties.ResistanceAt10Lux;
double dark = Properties.DarkResistance < r10 ? r10 : Properties.DarkResistance;
if (lux <= 0.0) return dark;
double ohms = r10 * Math.Pow(lux / 10.0, -Properties.Gamma);
if (ohms > dark) ohms = dark;
if (ohms < 10.0) ohms = 10.0; // never quite a short; the matrix wants a floor
return ohms;
}

Both stamps go in once. The dynamic conductance carries the value, and a 1 GΩ leak keeps the two nodes tied before the film has been read.

public override void StampInitial()
{
DeclareDynamicConductance(Connections.A, Connections.B);
Simulation.StampResistor(ConnectionNodes[Connections.A], ConnectionNodes[Connections.B], 1e9);
}
public override void Step()
{
Simulation.StampConductanceDynamic(ConnectionNodes[Connections.A], ConnectionNodes[Connections.B], _gLatched);
}
public override void CalculateCurrent()
{
double v = ConnectionVoltages[Connections.A] - ConnectionVoltages[Connections.B];
SetCurrent(0, v * _gLatched);
}

Everything is decided in PreStep and held for the whole tick. The light comes from, in order of preference:

  1. A component logic that wrote its own reading into UserData[0] and set UserData[1] to 1.
  2. The field around the part, sampled through the Window receiver.
  3. The part’s own Lux property, when nothing on the bench says anything about light.

The film is slow, and it brightens faster than it darkens. The resistance chases its target through a first-order lag on its logarithm, which is how a real film behaves.

public override void PreStep()
{
double around = SampleEnvironment("Lux", double.NaN);
double lux = UserData[1] > 0.5 ? UserData[0]
: double.IsNaN(around) ? Properties.Lux
: SampleReceiver("Window", around);
double target = Math.Log10(TargetOhms(lux));
if (!_seeded)
{
_logOhms = target; // a part placed on a lit bench is already at that light
_seeded = true;
}
else
{
// Brightening means the resistance is falling, which is the faster direction.
double tau = target < _logOhms ? Properties.RiseTime : Properties.DecayTime;
double dt = Simulation != null ? Simulation.TimeStep : 0.0;
if (tau <= 0.0 || dt <= 0.0) _logOhms = target;
else _logOhms += (target - _logOhms) * (1.0 - Math.Exp(-dt / tau));
}
_gLatched = 1.0 / Math.Pow(10.0, _logOhms);
_voltsAcross = ConnectionVoltages[Connections.A] - ConnectionVoltages[Connections.B];
}

Over its rated power the film burns, judged over 10 ms of simulated time rather than a single tick:

public override bool CheckFailure()
{
if (Properties.MaxPower <= 0.0) return false;
double watts = _voltsAcross * _voltsAcross * _gLatched;
if (AccumulateOverstress(ref _overstressSeconds, ref _overstressTicks, watts > Properties.MaxPower, 0.010))
{
Fail("over-power: exceeded the " + Properties.MaxPower + " W rating");
return true;
}
return false;
}
public override void PowerCycle()
{
base.PowerCycle();
_overstressSeconds = 0.0;
_overstressTicks = 0;
_seeded = false;
}

Build a divider: the GL5528 from 5 V to a midpoint, and a 10 kΩ resistor from the midpoint to ground. Put the multimeter on DCV across the 10 kΩ resistor and run.

  • At the default 300 lux the midpoint reads about 4.4 V. Set the part’s Lux to 10 and it falls to 2.5 V, and at 0 it reads almost nothing.
  • Put an environment zone around it with Lux: 10000 in its Extra Fields for daylight, or bring a lit LED close to its face.
  • To see it burn out, take the resistor away and put it straight across the 5 V supply in that 10,000 lux zone. It drops to about 160 Ω and takes about 160 mW, over its 100 mW rating.