ATtiny85 & TSSP4038 IR Barrier

I’ve been playing around with my model trains, but I need to prevent collisions.
electronics
model-railway
ir
code
Author

Alaisdair West

Published

April 8, 2026

I recently bought some HO & OO scale DC model trains, I run them on 3 separate loops and there are level crossings where one loop crosses two others. I thought it would be fun to try and automate my railway to prevent collisions and even add stops at stations. Fundamentally, I need a way to detect the presence of a train and I don’t want to make any modifications to the trains themselves. This is where infrared (IR) light presence detection comes in.

IR Presence Detection Techniques

Presence detection is used when our only concern is whether an object is in a specified area or not. We don’t care how far away it is or if it is moving. There are two common methods for presence detection using IR light.

The first method is by reflection, in this configuration the IR emitter is located adjacent to the receiver. When something is in front of the sensor the light is reflected from the emitter onto the receiver, as depicted in Figure 1. Common examples of this include automatic hand dryers and soap dispensers used in public bathrooms. Schäfer (2024) is a handy application note from VISHAY which discusses these applications and how to design your own. While simple to construct, some tuning is required to control the detection distance.

Figure 1: Reflective presence sensor, image from Schäfer (2024).

The second method is to create an IR Barrier or break-beam sensor. Unlike the reflection sensor, the emitter and receiver are set up to face each other. As the name suggests we detect presence when the continuous beam between the emitter and receiver is broken. This is the opposite logic to the reflection setup, we’re interested in when there is no light. These are useful for applications like burglar alarms (Vishay 2024).

Neither of these methods is generally superior and you should always consider what is appropriate for your application. From preliminary testing with no-name IR LEDs and TSSP4038 I found that the reflection setup had too much range. In a small model railway setup, precise control over the detection area is important. As the title suggests I decided to use an IR barrier, but the code I use here can easily be adapted for reflection.

Choosing an IR Detector

When choosing your IR receiver it’s best to avoid sensors with automatic gain control (AGC). IR receivers with AGC (such as the TSOP series or the common VS1838B) are designed for remote control systems (like TV remotes) and filter out continuous beams. It is possible to use these sensors but it requires complex emitter-receiver code to time pulses (see Pons 2025). The TSSP series lack AGC and are designed for these applications.

I also recommend getting a receiver that requires a modulated signal. The TSSP4038 I’m using requires 38kHz modulation, meaning that the IR emitter must be turned on and off at 38,000 times per second. By requiring a modulated signal external sources of IR can be ignored (like the Sun) (Pons 2025).

The setup

As a simple project I made a simple IR barrier that would turn on an LED when blocked, this allowed me to experiment with different trains and lighting conditions.

The full code and schematic are in my Codeberg repository for the project, but I will go through it all here.

Figure 2: Schematic for the ATtiny85 & TSSP4038 IR Barrier

Of particular interest in Figure 2 are C1 and R1 which act as a low-pass noise filter on the receiver, without it I found the receiver output to be useless.

C2 was needed to smooth the power supply for the ATtiny85 and filter the electrical noise from the railway. Without it, I found that the ATtiny85 would be constantly switching off and on.

The code

In this setup the ATtiny85 has two tasks, it needs to control both the emitter and receiver ends of the barrier. In setups requiring longer range these tasks would be handled by separate circuits as running wires between each would be impractical.

Emission

To generate the required 38kHz square wave signal I used the ATtiny85’s Timer0 peripheral in CTC mode. In CTC mode Timer0 can be configured to flip an output pin once the relevant comparison register overflows. This is limited to two output pins OC0A and OC0B, with corresponding registers OCR0A and OCR0B. I only need one of them for this project and chose output A which is shared with PB0.

The trick now is finding the right value for OCR0A, but luckily it’s not that hard thanks to page 72 of the ATtiny85 datasheet (Atmel 2013). \[ f_\text{OC0A} = \frac{f_\text{clk\_I/O}}{2N(1+\text{OCR0A})} \tag{1}\] We can rearrange the equation at the bottom of page 72 (Equation 1) to solve for the value of OCR0A that will get us the desired output frequency: \[ \text{OCR0A} = \frac{f_\text{clk\_I/O}}{2Nf_\text{OC0A}}-1 \tag{2}\]

Where:

  • \(f_\text{clk\_I/O}\) is the clock frequency in Hz (I stuck with the default 1MHz)
  • \(f_\text{OCR0A}\) is the desired output frequency (38kHz)
  • \(N \in \{1,8,64,256,1024\}\) is the prescale factor which divides the clock cycles giving us some more room for adjustment.

If I use \(N=1\) with Equation 2 I get \(\text{OCR0A}=12.16\), and \(\text{OCR0A}=0.64\) with \(N=8\). OCR0A is an 8-bit unsigned integer so we’ll need to round these two values to \(12\) and \(1\), respectively. To see what frequency we will actually get we plug these numbers back into Equation 1. This yields \(f_\text{OC0A}=38.46\text{kHz}\) for \(N=1\) and \(f_\text{OC0A}=31.25\text{kHz}\) for \(N=8\). \(N=1\) with \(\text{OCR0A}=12\), is the best we can do with the 1Mhz clock. Luckily this frequency is within the \(\pm5\%\) tolerance of the TSSP4038 (Vishay 2025).

In the C code I configured Timer0 in the init_timer0 function:

#include <avr/io.h>

void init_timer0(void)
{
    TCCR0A |= (1 << WGM01) // Put Timer0 into CTC mode
        | (1 << COM0A0); // Toggle OC0A each cycle (blink the IR LED)
    TCCR0B |= (1 << CS00); // no prescaler
    OCR0A = 12; // Oscillate at approx 38.46kHz
}

I call this at the start of my main function and that’s all there is to generating the emitter output. This is called in the setup part of my main function:

int main(void)
{
    DDRB |= (1 << PIN_IR_LED);
    init_timer0();
    // ...

Detection

The detection code is fairly simple, the TSSP4038 is active LOW. This means that the output pin on the TSSP4038 will be LOW when there is an IR signal and HIGH when there is none. In other words when the sensor is blocked the output will be HIGH. I wrapped this in the update_indicator function:

void update_indicator(void)
{
    // Sensor pin will be high when blocked
    if (PINB & (1 << PIN_SENSOR))
        PORTB |= (1 << PIN_INDICATOR);
    else
        PORTB &= ~(1 << PIN_INDICATOR);
}
TipUse as a reflective sensor

The code can easily be adapted to work as a reflective sensor, simply invert the logic in update_indicator:

void update_indicator(void)
{
    // Sensor pin will be low when train present
    if (PINB & (1 << PIN_SENSOR))
        PORTB &= ~(1 << PIN_INDICATOR);
    else
        PORTB |= (1 << PIN_INDICATOR);
}

To handle detection I use a pin change interrupt, in particular INT0 which is the highest priority interrupt and is mapped to PB2. By using an interrupt we keep the CPU free to handle other tasks (or idle) instead of constantly polling the PINB register. The interrupt was configured as such:

#include <avr/interrupt.h>

ISR(INT0_vect)
{
    update_indicator();
}

void init_interrupt0(void)
{
    GIMSK |= (1 << INT0); // Enable INT0
    MCUCR |= (1 << ISC00); // Interrupt on any change
    sei();
}

Now, you might be wondering why I even bothered to split the barrier checking logic into a separate function if I’m only going to call it in the ISR. It actually needs to be called on startup as well to poll the initial state. Since the ISR is only called on a change if there is already a train blocking the barrier it will not be registered.

Finally, since the TSSP4038 is active LOW we need to enable the internal pullup resistor on that pin to prevent a floating state. This results in this final main function:

int main(void)
{
    DDRB |= (1 << PIN_IR_LED) | (1 << PIN_INDICATOR);
    PORTB |= (1 << PIN_SENSOR); // Pullup the IR sensor
    init_interrupt0();
    init_timer0();

    // Since the interrupt will only fire on a change,
    // check the initial state of the barrier incase
    // it is already blocked
    update_indicator();

    while (1) {} // Loop forever
    return 0;
}

The full code for this project is on Codeberg.

Results

With all of that set up here’s a photo of my simple IR barrier in action:

Figure 3: The IR Barrier in action (TSSP4038 is hidden by the blue LED and IR LED is on the other side of the train)

The IR barrier worked great at detecting solid objects (like the Mallard), however light could travel through carriages with windows. A possible solution is to move the sensor to be above and below the train. With some fine tuning I may change this to a reflective sensor either mounted under the track or on some overhead structure.

This is a part of an ongoing project to automate my railway.

References

Atmel. 2013. ATtiny25/v/ATtiny45/v/ATtiny85/v. https://ww1.microchip.com/downloads/aemDocuments/documents/OTH/ProductDocuments/DataSheets/Atmel-2586-AVR-8-bit-Microcontroller-ATtiny25-ATtiny45-ATtiny85_Datasheet.pdf.
Pons, Giulio. 2025. How to Build an Infrared Barrier with IR Led and VS1838B Receiver and Arduino. https://www.barattalo.it/making/how-to-build-infrared-barrier-vs1838b-arduino/.
Schäfer, Sebastian. 2024. Vishay Infrared Receivers for Presence Sensor Applications. https://www.vishay.com/docs/82904/irreceiversprecencesensorapps.pdf.
Vishay. 2024. RECEIVERS AND EMITTERS FOR LIGHT BARRIERS. https://www.vishay.com/docs/49650/pt0402-lightbarriers.pdf.
Vishay. 2025. TSSP40.. https://www.vishay.com/docs/82458/tssp40.pdf.