AnkitSangwan[m] has quit [Quit: Idle timeout reached: 172800s]
crabbedhaloablut has joined #rust-embedded
jasperw has quit [Ping timeout: 252 seconds]
jasperw has joined #rust-embedded
IlPalazzo-ojiisa has joined #rust-embedded
duderonomy has quit [Ping timeout: 246 seconds]
morfertaw[m] has quit [Quit: Idle timeout reached: 172800s]
sourcebox[m] has joined #rust-embedded
<sourcebox[m]>
I'm trying to find a strategy for sharing peripherals in a multicore environment. I guess having them in one struct like most HALs do is not a good idea in that scenario. That translates to the next level, the board crate, in which things like UARTs, SPI etc has to be setup and serviced, but it should be on the application level to decide which core is using a certain peripheral. Any ideas?
starblue has quit [Ping timeout: 245 seconds]
starblue has joined #rust-embedded
IsaBang[m] has joined #rust-embedded
<IsaBang[m]>
Witch board?
<dirbaio[m]>
stm32mp1 I guess :)
<dirbaio[m]>
if it's a single firmware for both cores (both cores are same arch and share flash/ram, like in the rp2040 for example) then giving all peripherals to core0 and letting the user move them to core1 works well
<dirbaio[m]>
if they're separately built firmwares (I guess it's the case in tme MP1, different archs cortex-a vs cortex-m) then I don't think there's a good solution
<dirbaio[m]>
you could setup some funny bulid-time code generation: ask the user to write a "manifest" in some json/toml format specifying which core owns which periph, then have the cortex-a side read it at buildtime and codegen only the cortex-a periphs, and same for the cortex-m side
<dirbaio[m]>
but imo it's probably not worth it
<dirbaio[m]>
it's easier to just ask the user to manually take care of not conflicting peripherals
<dirbaio[m]>
perhaps gate obtaining each singleton behind an unsafe fn with a contract like "you must ensure the other core is not using this periph"
thomas25- has joined #rust-embedded
thomas25 has quit [Ping timeout: 260 seconds]
<whitequark[cis]>
ownership of peripherals in a multi-initiator system is a really interesting problem that we'd like to solve in Amaranth SoC
<whitequark[cis]>
we already plan to adopt Rust's ownership system in Amaranth SoC for reasoning about concurrent access to peripherals and explaining the invariants inherent in long-word register accesses to users, but right now there's nothing that specifically handles multi-initiator, other than "a peripheral's register block (like in SVD) MUST be accessed by only one initiator at a time"
<whitequark[cis]>
where the initiator could be a core or a thread running on the core
<whitequark[cis]>
considering that the accesses, from SoC perspective, look kinda samey whether they come from two threads or two cores, how about designating a "boot core" that takes all the peripherals (which should be Send right?) and then sends them to the other core?
<whitequark[cis]>
this way you retain the ability to swap things around at runtime and do not need any weird build system tricks
<dirbaio[m]>
whitequark[cis]: yeah that's what I suggested above. you can only do that if both cores run a single firmware
<whitequark[cis]>
ohhh I somehow completely missed that one message, sorry
* whitequark[cis]
didn't sleep this night and probably should avoid complex technical discussions <_<
<whitequark[cis]>
<dirbaio[m]> "if they're separately built..." <- you could represent the peripheral with a unit struct (or an integer) that has a method constructing/returning the actual underlying object?
<whitequark[cis]>
and then it reduces to the above
<whitequark[cis]>
assuming the problem here is that "you cannot send a struct from a cortex-a binary to a cortex-m binary due to differing ABI"
<dirbaio[m]>
if you have separately-built firmwares, how do you prove at compile time that both aren't obtaining the same periph?
<whitequark[cis]>
^ that
M9names[m] has joined #rust-embedded
<M9names[m]>
one firmware starts with all the peripherals, and just sends a serialization of one through to the other?
<dirbaio[m]>
yeah it could send the peripheral addr, or some id, as a u32
<dirbaio[m]>
that wouldn't be zero-cost though
<whitequark[cis]>
does it have to be?
<dirbaio[m]>
with the single-firmware case, you can "send" the singletons to the other core by letting a closure capture them
<dirbaio[m]>
but still proves at compile-time that there are no conflicting acceses
<whitequark[cis]>
this is functionally equivalent to using a global Cell to get your peripheral
<whitequark[cis]>
which is usually borrowed once
<whitequark[cis]>
that doesn't seem an especially high cost unless I'm missing something
<whitequark[cis]>
yes, it's non-zero, but if you have an MPSoC you can afford a few tens of instructions at startup, no?
<dirbaio[m]>
sure you can move that from compile-time to run-time, but then
<dirbaio[m]>
- it's not zero-cost
<dirbaio[m]>
- you can now have runtime crashes if core1 is expecting core0 to give it permission to use USART42 but core0 doesn't
<whitequark[cis]>
i think obsessing with zero-cost for this case is unproductive, again, unless i'm missing some downstream cost that is a big multiplier
<whitequark[cis]>
the cost of sending all the peripherals is in all likelihood less than one print! call
<whitequark[cis]>
I do kind of like the idea of manifest for codegen (or you could do a quick xslt transformation to filter SVD files for each core... I'll stop with the cursed) but it's less flexible in that it doesn't let you change assignment at runtime
<dirbaio[m]>
shared mem IPC across archs is not trivial :S
<whitequark[cis]>
which is something that you ordinarily can do
<dirbaio[m]>
* because shared mem
<whitequark[cis]>
my thought was that if you have an MPSoC there is a good chance you already have a queue
<whitequark[cis]>
this is not always the case, e.g. you could have the two cores handle disjoint tasks
<whitequark[cis]>
but in my experience it usually is
<dirbaio[m]>
for dynamic assignment I'd just let the user use an unsafe escape hatch
<whitequark[cis]>
our designs match different tradeoffs yea
<dirbaio[m]>
dynamic assignment seems very rare imo
<whitequark[cis]>
I think I would personally prefer to use the manifest solution in most cases
<whitequark[cis]>
but I can think of reasons to do dynamic assignment, such as field testing
IlPalazzo-ojiisa has quit [Quit: Leaving.]
fat_hamster[m] has quit [Quit: Idle timeout reached: 172800s]
emerent has quit [Ping timeout: 256 seconds]
emerent has joined #rust-embedded
IlPalazzo-ojiisa has joined #rust-embedded
<dngrsspookyvisio>
the absence of screaming after XSLT was mentioned means nature is healing and people are just memory holing that tech
<dirbaio[m]>
Catherine has the habit of bringing up cursed stuff
<dirbaio[m]>
after a while you get desensitized
<dirbaio[m]>
🤣
<sknebel>
I recently learned that there is a XSLT-based pipeline to turn .docx into LaTeX by some publishing house ...
<Lumpio->
I think I once wrote a simple 3D renderer kn XSLT that output SVG
<Lumpio->
in*
almindor[m] has quit [Quit: Idle timeout reached: 172800s]
diondokter[m] has quit [Quit: Idle timeout reached: 172800s]
<danielb[m]>
<Lumpio-> "I think I once wrote a simple 3D..." <- what does simple mean in this context? 😅
eldruin[m] has quit [Quit: Idle timeout reached: 172800s]
diondokter[m] has joined #rust-embedded
<diondokter[m]>
Bad idea of the evening: RTT over modbus 😁
crabbedhaloablut has quit []
IlPalazzo-ojiisa has quit [Quit: Leaving.]
GenTooMan has quit [Ping timeout: 256 seconds]
<JamesMunns[m]>
Isn't that just... logging?
<JamesMunns[m]>
like, do you mean bidirectional? Or do you just want to send your logs over modbus?
<JamesMunns[m]>
Because if so, defmt-bbq can enqueue messages and you can drain them from a modbus server
<JamesMunns[m]>
in mnemos we have a couple things multiplexed over serial links, including tracing as a binary protocol, it works pretty well, tho you have to be careful on how much you send, it's possible to overwhelm a low bandwidth link, we select the tracing level at runtime.
<JamesMunns[m]>
defmt is going to be a lot more compact because everything is interned, but also I don't know how often you plan to poll your modbus nodes for log info.
<JamesMunns[m]>
diondokter ^
<diondokter[m]>
<JamesMunns[m]> "Isn't that just... logging?" <- Well, afaik, modbus doesn't define any text or array data format. All you've got is an address space with u16s.
<diondokter[m]>
So I can reuse the idea of RTT with its read pointer and write pointer to bring across the data. This can be defmt too, will have to see if I need it.
<diondokter[m]>
For sending data I don't need text, so I can use the normal modbus registers for that.
<JamesMunns[m]>
Interesting. I wonder if that's easier/more efficient to read the ringbuffer like that (and translate addresses) rather than just draining a single "get the next two bytes in the log ring" that pops 0..=2 bytes and sends them as a u16.
<JamesMunns[m]>
if you use defmt/-bbq with rzcobs encoding, zero stuffing is fine, and 0x0000 means you have no logs left to drain :)
<diondokter[m]>
JamesMunns[m]: That's true. But the the modbus crate I'm planning on using doesn't have the proper callbacks for that I think. But that's not a bad idea either
<JamesMunns[m]>
* if you use defmt/-bbq with rzcobs encoding, zero stuffing is fine, and 0x0000 would mean you have no logs left to drain (because it's doublestuffed) :)
<JamesMunns[m]>
yeah, to emulate rtt fully you need to peek the ringbuf headers, then peek the ring contents, then poke the headers, rather than doing a "side-effectful read" of a fixed address
<JamesMunns[m]>
basically I'm suggesting you emulate most peripheral mapped uarts instead of RTT :D
<diondokter[m]>
Haha
<diondokter[m]>
Looks like this doesn't provide any callbacks:https://docs.rs/rmodbus/latest/rmodbus/server/index.html
<diondokter[m]>
Other than that it looks pretty decent
<diondokter[m]>
Technically I could intercept the messages and inspect them myself to see if the register has been read. Guess I need to do some experimenting
<JamesMunns[m]>
wouldn't you need callbacks for the client to poke the RTT header read position forward?
<diondokter[m]>
No the server does that for you.
<diondokter[m]>
Basically the way this crate is set up is that you have your context that contains all registers.
<diondokter[m]>
You then receive a message on your UART or whatever and parse it. You give the parsed message access to the context to modify or read it and it gives you a response back that you need to return on the UART.
<diondokter[m]>
So the client can just give a write command for the read pointer and it'll be changed in the context with the registers
<diondokter[m]>
It's a pretty decent way to write a crate like this. It works with any executor, sync and async
<JamesMunns[m]>
So your defmt impl would need to check the read pointer, then write a u16, then write the write pointer, for every two bytes you log?
<JamesMunns[m]>
(or can you do all three steps possibly repeated with one mutex lock of the context?)
duderonomy has joined #rust-embedded
<diondokter[m]>
No, synchronization is difficult. What I think I can do is letting the client control the read pointer and the device control the write pointer.
<diondokter[m]>
The difference between them is the amount of data available (keeping in mind the ring buffer wraparound). So the client can read all available data in one go and then update the read pointer to the end it has read.
<diondokter[m]>
The device knows where to write using the write pointer and it knows it can't go beyond the read pointer. With this I can implement the 'blockiffull' and 'skipiffull' modes RTT also has
<ian_rees[m]>
James Munns: I started using postcard recently, just wanted to say thanks!
<ian_rees[m]>
at work I do firmware mostly in C and C++, but am really enjoying doing this hobby project in Rust - it's just so nice to have all these coherent tools+documentation+libraries+communities
bpye has quit [Ping timeout: 246 seconds]
<ian_rees[m]>
one little puzzle I need to sort out: the serialized messages are moved through USB bulk transfers, so maximum 64B chunks meaning there is potential for a serialized object to be split across USB transfer chunks. Currently, I've got the USB OUT data going in to a bbqueue (another thanks!), so it seems possible that it will take two read grants to get a complete serialized message out
<dirbaio[m]>
maybe you want some kind of framing like COBS
<dirbaio[m]>
so you can treat the USB bulk transfers as an endless stream of bytes (like a serial port), ie not care about the packet boundaries.
<ian_rees[m]>
Is there a nice pattern to deserialize from a pair of slices? Current thinking is to replace the bbqueue with a Vec, and when a message is deserialized out of the front of the vec, shift any remaining data forward (with appropriate synchronization)
<dirbaio[m]>
would also allow sending multiple messages in a single transfer, maybe improving throughput
<ian_rees[m]>
I'll read up on COBS, thanks for the pointer!
<ian_rees[m]>
dirbaio[m]: current setup allows for that, I think, using `postcard::take_from_bytes()` to read out from the bbqueue
<ian_rees[m]>
it's not a super high performance thing - the firmware is basically managing timing and low-level interfacing for a voltage glitch attack, and the connection to the target MCU is over a UART so that will be the bottleneck well before USB
<dirbaio[m]>
ahh postcard itself can know when a message ends?
<dirbaio[m]>
then you don't need framing, ignore my suggestion 😂
<dirbaio[m]>
* message ends? from a stream of bytes of multiple messages concatenated?
<dngrsspookyvisio>
yeah COBS has a delimiter
<dirbaio[m]>
I mean raw postcard, no COBS
<dngrsspookyvisio>
but you have to use XX_from_bytes_cobs
<dngrsspookyvisio>
yeah, ok, then I think the answer is no
<dirbaio[m]>
🤔
<dngrsspookyvisio>
I might be wrong!
<dngrsspookyvisio>
I mean, you'll get a Result, so you can just call from_bytes until it Oks ... I have done that
<dngrsspookyvisio>
but explicit framing is probably more efficient
<ian_rees[m]>
I think the fundamental issue is that the deserializing methods take one slice, so they'll never succeed if the serialised message is split by the circular buffer wrapping
GenTooMan has joined #rust-embedded
<ian_rees[m]>
if the sender could guarantee only one message per transfer (it may just do that by accident, not sure) then I guess one could make the initial bbqueue producer grant big enough to contain the longest possible message, and preserve the grant (or some other state) through the individual USB chunks until a message is received. But that seems a bit ugly since the USB transfer layer shouldn't have to know about how the messages are
<ian_rees[m]>
encoded
<dngrsspookyvisio>
<ian_rees[m]> "I think the fundamental issue is..." <- hmmm, I think that's not supposed to happen if you're holding the write grant right. Question for James tho!
<dngrsspookyvisio>
ah, I responded to the wrong message
<dngrsspookyvisio>
<ian_rees[m]> "I think the fundamental issue is..." <- I meant to reply here
<dngrsspookyvisio>
element UI focus is weird sometimes
<dngrsspookyvisio>
oh wait, sorry
<dngrsspookyvisio>
we're not talking bbqueue
<ian_rees[m]>
currently, the USB is handled in an ISR, which gets a big enough write grant for the current chunk of USB data (so, 64B) and commits that before leaving
<ian_rees[m]>
well, it currently is using bbqueue, but I suspect it's not currently reliable if messages span the 64B USB chunks
<ian_rees[m]>
at the moment, I've only got some very small messages, but they'll need to be bigger than 64B before it's done
<dngrsspookyvisio>
Is there a reason for you not to use COBS? it's much more pleasant in my experience (esp. when you use the supplied accumulator)
<ian_rees[m]>
just wasn't aware of it
<dngrsspookyvisio>
aye!
<ian_rees[m]>
though after a few minutes of skimming, I'm not sure that I understand how COBS changes the picture - would the USB ISR look at the COBS-encoded data and use that to determine the grant size to request?
<JamesMunns[m]>
the idea is that you treat the data in and out as a "stream" of bytes
<ian_rees[m]>
<dngrsspookyvisio> "Is there a reason for you not to..." <- maybe the gap in my understanding is the accumulator... James just beat me to the punch ;)
<JamesMunns[m]>
and cobs can be used to figure out where one message stops and the next begins
<JamesMunns[m]>
it also means you can always use `0x00` to "fill" any incomplete 64B frames
<ian_rees[m]>
ah, I keep getting tripped up on docs.rs - had read through the functions and structs, but not modules...
<JamesMunns[m]>
<dirbaio[m]> "ahh postcard itself can know..." <- It knows how many bytes it took after deser, but that's only in the "everything is fine" case, not if there was a deser error or corrupted message
<dirbaio[m]>
ahhhh makes sense
<dirbaio[m]>
so you don't need COBS to know when a message starts/ends, but you do to recover from corruption
<dirbaio[m]>
in theory usb should have no corruption 🤔
<JamesMunns[m]>
Yeah, or so you don't start deserializing halfway through one message (and into the next) if you missed the first bytes of the first message
<JamesMunns[m]>
dirbaio[m]: It has a crc16 on frames iirc, it's unlikely but not impossible to have corruption
<JamesMunns[m]>
But cobs is also not an integrity check, just a basic "damage containment" as a side effect of framing.
<JamesMunns[m]>
Cobs forms a "linked list" through the data, so if any of *those* bytes are corrupted or shifted, deframing will fail, but if any of the bytes in the middle are changed, it won't notice.
<JamesMunns[m]>
But the zero bytes serve as a good "state reset" checkpoint, that's that "damage containment" part.
<JamesMunns[m]>
But cobs accumulator solves both "what if there are multiple messages in one frame" and "what if one message spans multiple frames".
Foxyloxy has quit [Read error: Connection reset by peer]
Foxyloxy has joined #rust-embedded
jasperw has quit [Read error: Connection reset by peer]
jasperw has joined #rust-embedded
<dngrsspookyvisio>
yeah without COBS it's really easy to desync and never come back
<dngrsspookyvisio>
one overrun and you're toast
<adamgreig[m]>
<dirbaio[m]> "in theory usb should have no..." <- Usb has the same problem as CAN where they specified an ok crc but it interacts poorly with bit stuffing so just a couple of physical bit flips get amplified to enough errors that the CRC passes invalid data
<adamgreig[m]>
But bit flips are pretty rare unless you have stupidly long cables so in practice you don't see many errors I think
<whitequark[cis]>
CRCs are so fascinating
<adamgreig[m]>
Their conditions for theoretical error detection guarantees just go through all your abstractions and wreck them lol
<adamgreig[m]>
Even for something like 8b10b encoding it's not entirely clear how good or bad the error amplification is for CRCs
<adamgreig[m]>
Once you combine it with COBS or something, who knows
<ian_rees[m]>
not really an "embedded" topic, but is there a good crate for talking to electronics test gear? Am looking for a slightly higher layer thing than LXI/VISA/USBTMC where there might be drivers for a particular oscilloscope or signal generator
<adamgreig[m]>
It seems pretty niche, there's libscopehal in c++ that supports some hardware but I think most people just end up sending scpi commands over a socket directly