profirust

2023-12-28 rust profibus industrial-automation embedded 10 min read

I want to announce the release of a project that I have been working on in the past months: profirust is a PROFIBUS-DP compatible communication stack written in Rust!

On my quest to dive deeper and deeper into the industrial automation world, it was inevitable for me to explore the various fieldbus protocols at some point. Especially for supporting my increasingly complex machinery, I wanted to look into a robust and scalable fieldbus solution. PROFIBUS uses purple cabling so naturally I felt like this was the option I had to choose.

As there are few to no existing open-source projects implementing a fully capable PROFIBUS communication stack, this looked like a promising niche to fill and an interesting challenge to attempt solving.

GitHub

crates.io

Documentation

PROFIBUS

If you haven't heard about PROFIBUS or other fieldbusses before, I've written an introduction in my PROFIBUS Primer blog post. I highly encourage you to read that first to get yourself familiar with PROFIBUS.

But TL;DR:

Announcing profirust

With a rough idea what PROFIBUS is, let's now turn to profirust. That's the PROFIBUS-DP compatible communication stack I've been working on. It is written in pure Rust, hence the name.

At this time, profirust targets applications which intend to communicate with PROFIBUS peripherals. I want to include peripheral-side support at some point, but for now, only the master side is implemented.

profibus is designed to have minimal hardware requirements. While a lot of PROFIBUS masters implement a part of the communication in hardware, profirust only requires a UART peripheral and an RS-485 transceiver. This allows for a wider choice of hardware platforms but of course more CPU time is needed for communication. Also some form of real-time scheduling is required, to ensure compliance with bus timing constraints. The higher the baudrate, the stricter the latency requirements get (max time between polls is TSLOT/2).

Image of profirust running on an RP2040

profirust running on an RP2040 test setup with a MAX485. Connected to a SIEMENS diagnostics repeater and a WAGO remote I/O station.

Right now, profirust ships with physical layer implementations for embedded Linux (non-realtime) and the RP2040 MCU. More will follow, of course. Especially a PHY implementation based on embedded-hal as soon as e-h 1.0 is released.

The usage on Linux is a bit controversial because the realtime requirements of PROFIBUS cannot be guaranteed. In practice, you can make it work at low baudrates by carefully selecting conservative timing parameters. But I would refrain from using this for anything mission critical. I do plan on making profirust compatible with realtime Linux at some point.

gsd-parser and gsdtool

To do anything useful with PROFIBUS you need to be able to make use of GSD files (Learn more about them in my PROFIBUS Primer). That's why profirust comes with a gsd-parser for those files and a small CLI tool called gsdtool.

gsdtool can be used to interactively generate appropriate configuration and parameter data for a peripheral. It spits out the necessary code block for setting up a peripheral using your desired configuration:

 gsdtool config-wizard FRAB4711.gsd 
Welcome to the station configuration wizard!
Station: "FRABA Encoder" from "FRABA"
Ident:   0x4711

Selecting modules (maximum 1): Select module 1/1 (ESC to stop): Class 2 Multiturn Code sequence: Increasing clockwise (0) Class 2 functionality: Enable Scaling function control: Enable Measuring units per revolution (0 - 8192): 4096 Total measuring range (high) (0 - 512): 256 Total measuring range (low) (0 - 65535): 0

Peripheral Configuration:

// Options generated by `gsdtool` using "FRAB4711.gsd"
let options = profirust::dp::PeripheralOptions {
    // ........
};
let mut buffer_inputs = [0u8; 4];
let mut buffer_outputs = [0u8; 4];
let mut buffer_diagnostics = [0u8; 57];

(You can also watch a live gsdtool session on asciinema)

In the other direction, gsdtool also provides facilities for interpreting diagnostic data reported by a peripheral using the information from the GSD file:

 gsdtool diagnostics si0380a7.gsd 
Diagnostics Data (as fmt::Debug slice): [160, 0, 0, 32, 72, 100, 255, 255, 255, 255, 255, 255, 0, 41, 0, 128, 0, 0]

Bit 29: ====== Segment DP2 ====== Bit 127: A/B shorted, too much resist. Area 40-47: 100 = Reflection error rate: 100%

Using profirust

So, what can you do with it? For development of profirust, I've built myself a little playground using a WAGO 750-343 remote I/O station and a FRABA58 multi-turn rotary encoder. The remote I/O controls a pneumatic solenoid valve which in turn moves a pneumatic rotary cylinder. The cylinder then rotates the encoder, providing feedback to the control system.

PROFIBUS playground setup

WAGO 750-343 (bottom left), FRABA58 (top right), pneumatic valve and rotary cylinder (center right), PROFIBUS Diagnostics Repeater (bottom right)

To program this playground, we start by setting up the PROFIBUS master:

use profirust::{dp, fdl, phy};

const MASTER_ADDRESS: u8 = 3;
const BUS_DEVICE: &'static str = "/dev/ttyUSB0";
const BAUDRATE: profirust::Baudrate = profirust::Baudrate::B500000;

let mut dp_master = dp::DpMaster::new(vec![]);
// Peripherals are added to `dp_master` here later on...

let mut fdl_master = fdl::FdlMaster::new(
    fdl::ParametersBuilder::new(MASTER_ADDRESS, BAUDRATE)
        // We use a rather large T_slot time because USB-RS485 converters
        // can induce large delays at times.
        .slot_bits(2500)
        .watchdog_timeout(profirust::time::Duration::from_secs(2))
        .build_verified(&dp_master),
);

let mut phy = phy::LinuxRs485Phy::new(BUS_DEVICE, fdl_master.parameters().baudrate);

fdl_master.set_online();
dp_master.enter_operate();
loop {
    let now = profirust::time::Instant::now();
    let events = fdl_master.poll(now, &mut phy, &mut dp_master);

    if events.cycle_completed {
        // Control logic goes here...
    }

    // We must not poll() too often or to little. T_slot / 2 seems to be a good compromise.
    let sleep_time: std::time::Duration = (fdl_master.parameters().slot_time() / 2).into();
    std::thread::sleep(sleep_time);
}

Then, we use gsdtool to generate the configuration and parameterization data blocks for each peripheral, as was demonstrated above. The output from gsdtool is simply added to the DP master initialization:

const IO_STATION_ADDRESS: u8 = 8;

let mut dp_master = dp::DpMaster::new(vec![]);

// Options generated by `gsdtool` using "wagob757.gsd"
let options = profirust::dp::PeripheralOptions {
    // "WAGO 750-343" by "WAGO Kontakttechnik GmbH"
    ident_number: 0xb757,

    // ... many more things output by `gsdtool` ...
};
let mut buffer_inputs = [0u8; 10];
let mut buffer_outputs = [0u8; 7];
let mut buffer_diagnostics = [0u8; 64];

let io_handle = dp_master.add(
    dp::Peripheral::new(
        IO_STATION_ADDRESS,
        options,
        &mut buffer_inputs,
        &mut buffer_outputs,
    )
    .with_diag_buffer(&mut buffer_diagnostics),
);

The io_handle returned by dp_master.add() will later allow us to access the peripheral where we need it. For example, to access the inputs and outputs as part of the control logic:

loop {
    let now = profirust::time::Instant::now();
    let events = fdl_master.poll(now, &mut phy, &mut dp_master);

    // `cycle_completed` fires whenever one full cycle of data-exchange with
    // all peripherals was performed.
    if events.cycle_completed {
        let remote_io = dp_master.get_mut(io_handle);
        if remote_io.is_running() {
            // Print the status of all inputs of the remote I/O station
            println!("Inputs: {:?}", remoteio.pi_i());
        }
    }

    // We must not poll() too often or to little. T_slot / 2 seems to be a good compromise.
    let sleep_time: std::time::Duration = (fdl_master.parameters().slot_time() / 2).into();
    std::thread::sleep(sleep_time);
}

For more elaborate logic, I'll shamelessly plug my process-image crate. It provides convenient macros for accessing individual bits in the input and output process images of your peripherals:

use process_image as pi;

loop {
    let now = profirust::time::Instant::now();
    let events = fdl_master.poll(now, &mut phy, &mut dp_master);

    let remoteio = dp_master.get_mut(io_handle);
    if events.cycle_completed && remoteio.is_running() {
        // Get access to both the inputs and outputs process images.
        let (pi_i, pi_q) = remoteio.pi_both();

        // Limit sensor at address %IX0.3 (3rd bit of the 0th byte of inputs)
        let sensor_forwards = pi::tag!(pi_i, X, 0, 3);
        // Limit sensor at address %IX0.4
        let sensor_backwards = pi::tag!(pi_i, X, 0, 4);
        // Supply air pressure sensor at address %IX1.2
        let pressure_ok = pi::tag!(pi_i, X, 1, 2);

        // Valve solenoid at address %QX0.7
        let mut valve_open = pi::tag_mut!(pi_q, X, 0, 7);
        // Update valve state
        *valve_open =
            (*valve_open || sensor_backwards)
            && !sensor_forwards
            && pressure_ok;
    }

    // We must not poll() too often or to little. T_slot / 2 seems to be a good compromise.
    let sleep_time: std::time::Duration = (fdl_master.parameters().slot_time() / 2).into();
    std::thread::sleep(sleep_time);
}

And there you have it - all the pieces needed to build a simple control application using profirust. For your satisfaction, here is a short video of my playground setup in action. The outputs of the WAGO remote I/O are used to indicate the encoder position.

Conclusion

So as you can see, profirust can already handle some (arguably staged) real world applications. But of course, there is still lot's to do. If you are interested in the project, head over to GitHub to check out the code.

I'd also love to hear from you if you have any ideas for profirust or if you are missing any important features.

Let's get Rust expanding more into the industrial automation world!