NOP0 / rustmatic

PLC programming in Rust!
Apache License 2.0
35 stars 2 forks source link

Process image turned upside down #26

Closed NOP0 closed 4 years ago

NOP0 commented 4 years ago

Still tinkering and pondering over this process image. Your're probably getting tired of this :smile:

Tried turning it upside down, that is;

If you try cargo run now, you can see that my_bool which is linked to an DummyInput is updated from the device driver and into userspace.

NOP0 commented 4 years ago

Just to clarify that this PR is too rough to be merged directly, will refine and polish if you agree with the general direction (and other points you might have). Then those other ProcessImage PR's will be closed.

NOP0 commented 4 years ago

I guess this is somewhat similar to memory mapped IO . But typesafe. Is it a name for this?

Michael-F-Bryan commented 4 years ago

From what I've seen, the way PLC programs use the Process Image for input and output is essentially identical to how a normal embedded system would interact with the outside world using memory mapped IO, so that's a good analogy.

How would it look for an end user to interact with the process image? For example, say I wanted to write a Structured Text program which reads from an analogue sensor at address 10 and writes to a digital output at address 20. The output is HIGH if the value is over 100, else LOW. What would this look like as a ST program? Then if we could transpile it to Rust code, how would that look?

Remember that the addresses are hard-coded because this code isn't involved in device registration (imagine it's a WASM program).

NOP0 commented 4 years ago

How would it look for an end user to interact with the process image? For example, say I wanted to write a Structured Text program which reads from an analogue sensor at address 10 and writes to a digital output at address 20. The output is HIGH if the value is over 100, else LOW. What would this look like as a ST program?

You could do something like

%QX20.0 := DWORD_TO_REAL(%ID10) > 100.0; // Hardcoded solution

As you can see, there's no bitmasking or anything to adress a single bit, that's nice. However, this is not typesafe so somewhere else I could do

%QX20.0 := %IX10.0; 
// Oops, reading a single bit inside a REAL and outputting this. Probably not what I wanted

I some environments you can map the offset to a typed "symbol" in userspace (this is quite similar to our Input<T> "handle") , If you only use this handle it's of course typesafe again.

MyBoolOutput := MyRealInput > 100.0; 

Since a wasm page is just an array of bytes I guess this fits nicely with a memorymap/process image. But how can we make a typesafe overlay of this in Rust. Do we need to resort to serialize/deserialize or some unsafe transmute?

The solution in this PR is "just" typesafe, and corresponds to

MyBoolOutput = MyRealInput > 100.0;  // This is Rust

but for flexibility and better fit with the 61131-3 code we might need to have an u8 array here also?

Thoughts?

Michael-F-Bryan commented 4 years ago

If we're going to make sure people don't do that sort of stuff (e.g. %QX20.0 := %IX10.0) each item in our Process Image will need to remember what type it is so we can emit some sort of runtime fault if it's accessed in an invalid way.

So previously, reading a bit from a particular location would be quite trivial:

struct ProcessImage {
  memory: Vec<u8>,
}

impl ProcessImage {
  fn read_bit(&self, address: usize, bit: usize) -> bool {
    let byte = self.memory[address];
    let mask = 1 << bit;
    (byte & mask) != 0
  }
}

Bit if we make it strongly-typed, every byte of IO gets an extra byte of overhead to remember what type it is.

struct ProcessImage {
  memory: Vec<IoCell>,
}

struct IoCell {
  value: u8,
  ty: Type,
}

enum Type {
  Int32,
  UInt32,
  U8,
  ...
}

How would normal PLC software handle this? I feel like it'd just let people write bad code and get garbage, and that's their fault for using the wrong address. That's not overly satisfying, but I feel like the IEC 61131-3 spec would deliberately leave this sort of stuff unspecified.

For the WASM code I'm thinking of adding memcpy-like intrinsics for reading inputs and writing to outputs. Because the data types you can use with the Process Image are all trivial (i.e. booleans, integers, and floats), type punning (e.g. accessing bits from a double) will just give you garbage values and not crash anything.

I can even see places where this sort of thing may even be desired, for example just read the sign bit from a float to see whether it's negative or positive. It's a stupid example, because the overhead of reading from the process image vastly outweighs the performance gain from reading 1 byte instead of 8 then checking if it's greater than 0, but people may still want to do it.

Michael-F-Bryan commented 4 years ago

If we're going to make sure people don't do that sort of stuff (e.g. %QX20.0 := %IX10.0) each item in our Process Image will need to remember what type it is so we can emit some sort of runtime fault if it's accessed in an invalid way.

So previously, reading a bit from a particular location would be quite trivial:

struct ProcessImage {
  memory: Vec<u8>,
}

impl ProcessImage {
  fn read_bit(&self, address: usize, bit: usize) -> bool {
    let byte = self.memory[address];
    let mask = 1 << bit;
    (byte & mask) != 0
  }
}

Bit if we make it strongly-typed, every input/output will need extra overhead to remember which type it is.

struct ProcessImage {
  memory: Vec<Value>,
}

enum Value {
  Int32(i32),
  UInt32(u32),
  U8(u8),
  ...
}

How would normal PLC software handle this? I feel like it'd just let people write bad code and get garbage, and that's their fault for using the wrong address. That's not overly satisfying, but I feel like the IEC 61131-3 spec would deliberately leave this sort of stuff unspecified.

For the WASM code I'm thinking of adding memcpy-like intrinsics for reading inputs and writing to outputs. Because the data types you can use with the Process Image are all trivial (i.e. booleans, integers, and floats), type punning (e.g. accessing bits from a double) will just give you garbage values and not crash anything.

I can even see places where this sort of thing may even be desired, for example just read the sign bit from a float to see whether it's negative or positive. It's a stupid example, because the overhead of reading from the process image vastly outweighs the performance gain from reading 1 byte instead of 8 then checking if it's greater than 0, but people may still want to do it.

NOP0 commented 4 years ago

I think we're definitely at the core of my struggles with the ProcessImage here. There's a classic conflict between safety vs. control.

How would normal PLC software handle this? I feel like it'd just let people write bad code and get garbage, and that's their fault for using the wrong address. That's not overly satisfying, but I feel like the IEC 61131-3 spec would deliberately leave this sort of stuff unspecified.

Yeah, I think they would allow this. Bring your footguns and all that. I think in some environments your're only allowed to pun "untyped" types though, that is bit, bool, word, doubleword" as an extra safety.

I can even see places where this sort of thing may even be desired

I agree that there should be some way to manipulate on this level if we can find a good solution for this. Maybe default can be typesafe, but with option to drop into this sort of stuff.

enum Value {
  Int32(i32),
  UInt32(u32),
  U8(u8),
  ...
}

I contemplated this solution earlier also, but thought it had too much memory overhead. That is in an u8 you can pack 8 bits/bools. If we need 64 bits to store one bool, that's getting real in an application with say 2000 bools.

And should the user be able to put his own types in the ProcessImage (this requirement is of course up for discussion), then we can't go down the enum route, since that is like a closed form of generics(?) (If we dont put an escape hatch like Box<T> in the enum?)

Maybe if there was some crate that let you borrow a slice of u8 like a type.

Michael-F-Bryan commented 4 years ago

Maybe if there was some crate that let you borrow a slice of u8 like a type.

You could do that using pointer arithmetic and casting, but it'd have just as many footguns because it's possible to have unaligned pointers. Say you're trying to read an i32 (alignment = 4) but the pointer's address ends in 3, trying to read 4 bytes into a register from an address which isn't a multiple of 4 will make your application crash on certain processors (ARM I think, you can read more here).

I'm tempted to take the easy route for now and omit any checks. A lot of this will be hidden behind a simple fn read_input(address: usize, buffer: &mut [u8]) -> Result<(), Error> function, so implementing it later won't break anything. That's the interface I'll be providing to the WASM code any way.

NOP0 commented 4 years ago

Question: What is the relation between the rust runtime in this pr and the different use cases; will the runtime be used when e.g. compiling IEC to webassembly? Should I continue working on the runtime or has this become obsolete when we target wasm?

NOP0 commented 4 years ago

This was the Rust I came up with that is most similar to what I've seen on the plc side. You did pub fn ix(&self, offset, bit) -> bool yourself in a previous post in this thread. I see that "adress" might be better than "offset", but you get the idea :smile:

It's a good deal simpler than the anymap/denseslotmap solution and maps 1-1 to IEC.

Thoughts?

use std::convert::{TryFrom};

struct PI{
    pub image : [u8; 128],
}

impl PI {
    pub fn ib(&self, offset: usize) -> u8 {
        self.image[offset]
    }

    pub fn iw(&self, offset: usize) -> u16 {
        u16::from_le_bytes(<[u8;2]>::try_from(&self.image[offset..offset+2]).unwrap())
    }

    pub fn id(&self, offset: usize) -> u32 {
        u32::from_le_bytes(<[u8;4]>::try_from(&self.image[offset..offset+4]).unwrap())
    } 

}

fn main() {

    let mut my_pi = PI{
        image : [0;128],
    };

    my_pi.image[5] = 0xFF;

    println!("{}", my_pi.ib(5)); // 255
    println!("{}", my_pi.iw(5)); // 255
    println!("{}", my_pi.id(5)); // 255

    let my_float : f32 = 1.0;

 // this offset is *really* a float, be sure to leave your footgun at home  
    println!("{}", my_float + my_pi.id(5) as f32);  // 256

}
Michael-F-Bryan commented 4 years ago

I don't think it would become obsolete.

My thoughts are that a WASM program would implement rustmatic_core::Process and poll()ed repeatedly. That way you can write programs in IEC and compile it to WASM while still being able to write code in Rust that can directly interact with the System or do things like TCP or interact with the file system.

I don't imagine most people would want to write Rust when they can use ladder logic, but it gives us a way to implement housekeeping tasks or maybe some sort of debugger or UI.

NOP0 commented 4 years ago

Ok, that is clarifying :+1:

What do you think about the example in my previous post, should I go for this simpler solution?

Note; If it's premature to take a decision on this, I can also work on parsing and stdlib. I think you see the big picture with wasm and all more clearly than me right now.

Michael-F-Bryan commented 4 years ago

I'm tempted to go with the dumbed down version you showed above. It's loads simpler than using slotmap or anymap, and as long as we keep things loosely coupled we can easily change it later on.

We may need better names than ib(), iw(), and id() though... It took me a couple seconds to realise they mean read_byte_input(), read_word_input(), and read_double_word_input(), respectively :stuck_out_tongue_winking_eye:

NOP0 commented 4 years ago

I'm tempted to go with the dumbed down version you showed above. It's loads simpler than using slotmap or anymap, and as long as we keep things loosely coupled we can easily change it later on.

Let's do it! I'll close the current PR's on ProcessImage and implement this simpler approach in a new PR. It's probably one of those things we gain experience with from usage anyway. +1:

We may need better names than ib(), iw(), and id() though... It took me a couple seconds to realise they mean read_byte_input(), read_word_input(), and read_double_word_input(), respectively stuck_out_tongue_winking_eye

Lol, yeah I said it was 1-1 with IEC :wink: I'll go with your names. :+1:

NOP0 commented 4 years ago

Closed because of change of approach.