-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Take ownership of SPI/CS? #13
Comments
I am still not sure which of those paths I prefer. On the one hand, when having references, dropping a Taking the ownershhip of the What do you think? |
The syntax of the language poses ownership as the default path (ex: let cs = ...;
let spi = ...
let inactive_ethernet = W5500::with_initialization(cs, ...);
let ethernet = inactive_ethernet.activate(spi);// inactive_ethernet is owned by ethernet
// do something on the network
let (inactive_ethernet, spi) = ethernet.deactivate(spi);// or `release`
// spi available for something else
let cs = inactive_ethernet.release();
// cs available for something else In this example, even the W5500 instance is owned by ActiveW5500. With this approach, things are more explicit, and an argument could be made that additional code simply isn't worth it. However I feel it's more idiomatically aligned with how Rust is designed. Just my $0.02. Take it with a grain of salt, I've started learning rust about 4 weeks ago. |
I've put together my vision as an informal state machine diagram. https://docs.google.com/drawings/d/1Yq0XQ_DKhvvv8n4GElfYEy7dBVHKeKAfOQJQfE9o1cA/edit The lines show the methods that can be called from each state to transition the state. For example, calling struct Bus {
cs: OutputPin,
}
struct ActiveBus {
spi: FullDuplex<u8>,
bus: Bus,
}
struct UninitializedW5500 {
bus: ActiveBus,
}
impl UninitializedW5500 {
fn initialize(self, WakeOnLan, OnPingRequest, ...) -> (W5500, Socket0, Socket1, Socket2, Socket3, Socket4, Socket5, Socket7, Socket8) {
// config chip
(W5500 { self.bus }, Socket0 {}, Socket1 {}, Socket2 {}, Socket3 {}, Socket4 {}, Socket5 {}, Socket6 {}, Socket7 {})
}
fn deactivate(self) -> FullDuplex {
self.bus.spi
}
}
struct W5500 {
bus: ActiveBus,
}
impl W5500 {
fn reset(self) -> UninitializedW5500 {
// reset chip
UninitializedW5500 { bus: self.bus }
}
fn deactivate(self) -> (InactiveW5500, FullDuplex) {
(InactiveW5500 { bus: self.bus.bus }, self.bus.spi)
}
fn take_udp_socket(self, socket: Socket, port: u16) -> UdpSocket {
// set up socket for UDP mode
UdpSocket { socket, chip: self }
}
}
struct InactiveW5500 {
bus: Bus
}
impl InactiveW5500 {
fn activate(self, spi: FullDuplex) -> W5500 {
W5500 { bus: ActiveBus { spi, bus: self.bus } }
}
}
struct UdpSocket {
socket: Socket,
chip: W5500,
}
impl UdpSocket {
fn release(self) -> (W5500, Socket) {
// reset socket
(self.chip, self.socket)
}
fn deactivate(self) -> (W5500, InactiveUdpSocket) {
(self.chip, InactiveUdpSocket { socke})
}
}
struct InactiveUdpSocket {
socket: Socket,
}
impl InactiveUdpSocket {
fn activate(self, chip: W5500) -> UdpSocket {
UdpSocket { socket: self.socket, chip }
}
}
// TcpSocket
// InactiveTcpSocket |
That sounds quite neat. pub struct Socket0;
pub struct Socket1;
pub struct Socket2;
pub struct Socket3;
pub struct Socket4;
pub struct Socket5;
pub struct Socket6;
pub struct Socket7;
pub trait Socket {
const ID: u8;
}
impl Socket for Socket0 {
const ID: u8 = 0;
}
impl Socket for Socket1 {
const ID: u8 = 1;
}
// ...
// maybe Udp instead of UdpSocket when using generics?
// The full name would then be for example Udp<Socket0>,
// instead of UdpSocket<Socket0>, so no 'Socket' repetition
struct Udp<S: Socket> {
socket: S,
// ...
} Two notes:
All in all, I quite like this approach |
Thanks, I'm glad you think so 😄 Yes, I totally agree on your socket ID idea.
Good point. What about
Excellent point. You're right if we don't clean up those sockets, they would be invalid. Accepting them as args would be a way to do that. And even though that would be pretty verbose, it wouldn't be necessary for most programs. A basic program might look something like this. let inactive_w5500 = InactiveW5500::new(spi, cs);
let (w5500, socket0, ..) = inactive_w5500.initialize(/* settings */).unwrap();
let udp_socket = w5500.open_udp_socket(socket0).unwrap();
// send/receive on udp_socket The only problem with this that I can think of so far is that if you had two W5500s and two driver instances, you could interchange their Sockets to bypass the compile-time ownership checking. I need to think some more about how to get the compiler to only accept the instances it instantiated itself. |
Idea: if the W5500 instantiates a symbol struct that it owns, it could instantiate each socket with a ref to that symbol. That would require
It wouldn't be compile-time, but at least it would be very fast. The only way I can think of to make the sockets unique per driver at compile time is to get tricky with generics, so that the compiler treats every W5500 instance as a different type. |
See my progress here: https://github.com/jonahbron/w5500/tree/restructure |
@kellerkindt Just some updates on my progress thus far. I've created a trait called There are also three other main structs: The initialize method accepts a struct of I've realized that there needs to be a trait for Also still need to work on socket management and the UDP implementation. |
I've come to the belief that it is impossible to guarantee the consistency of the sockets at compile-time. The only way for the compiler to ensure that the socket it receives is a socket it created is for every driver instance to have a unique type. But this is not possible because the number of driver instances isn't necessarily known at compile time. The user could theoretically run a loop of indeterminate length, producing unique driver instances not known at compile-time. Thus I've pivoted the concept for Socket management. As of the current state of the branch, the W5500 struct now holds ownership of all 8 sockets, and will produce mutable references upon request. These mutable references can be passed to a new UdpSocket. I believe this way, the borrow checker should be able to guarantee that the socket references leave scope before the W5500 is uninitialized or leaves scope. I haven't actually written a program that uses the library yet, so I'm not 100% sure this will work properly in practice. The library does compile though. If you have some time to review, I'd greatly appreciate it. |
Woah, quite a lot of things you fixed in your restructure branch. |
@kellerkindt I took a break for about a week and a half, but I'm back at it. At this point, the code is to the point where it can actually initialize a socket for use in UDP mode. Next thing to work on is the ability to actually send UDP packets. Again this compiles, but I have not yet tested on hardware. I'll probably do that after packet send/receive is theoretically working. |
@kellerkindt Would you mind explaining this code?
It appears that it keeps checking the received size until it stops changing. Is that explicitly the design of the chip, that you don't know exactly when the packet has completed until it stops coming Is it possible for the packet to get delayed such that it thinks the packet is fully loaded but isn't? I've been poring over the datasheet and see no mention of that behavior. I'd like to make sure that I fully understand it before I move it over. Edit: I see this is also the behavior of the Arduino Ethernet library |
In short, the doc(?) mentioned that when two reads of the size register return the same value, a new message / chunk has been received. If the two reads are not equal, it is still receiving data. |
Great news. Take your time :) |
Can you refer me to that documentation? I was unable to find it in the datasheet, perhaps it's somewhere else?
I'm also encountering some confusion here. I see no info in the datasheet concerning the structure of the socket RX buffer. This structure described in your code comments does not line up with what the docs say about an actual UDP packet header: https://erg.abdn.ac.uk/users/gorry/course/inet-pages/udp.html My only guess is that the W5500 does some sort of parsing on the UDP packet itself (assumedly verifying the checksum, since it's not present in the comment). Can you refer me to the documentation that describes this as well? |
@kellerkindt Never mind about the two-reads of the size thing, my datasheet must have been out-of-date. Found one on the WizNet website that mentions that necessity. http://wizwiki.net/wiki/lib/exe/fetch.php?media=products:w5500:w5500_ds_v109e.pdf page 55 "Sn_RX_RSR" I'm still unable to find any mention of the structure of the socket buffer however. |
All righty, I found it. A bit of googling revealed this info is missing from the W5500 docs, but is present in the W5200 docs. https://forum.wiznet.io/t/topic/691 W5200 datasheet, page 5.2.2 |
@kellerkindt Tell me what you think of this, but I've added a new struct called I think we should still keep a method called |
UDP packet receipt is fleshed out. Not tested, but I'm going to move on to packet sending before testing. |
@kellerkindt I modified the design, there's now an |
Upon attempting to integrate the library into an application, I've finally verified there's no way to do the kind of socket enforcement I've been trying to do. Will need to come up with a more run-time approach 😢
|
@kellerkindt Okay I was able to get a simple packet-sending script to compile after some alterations to the driver. let uninitialized_w5500 = UninitializedW5500::new(FourWire::new(cs).activate(spi));
let w5500 = uninitialized_w5500.initialize_manual(MacAddress::new(0, 1, 2, 3, 4, 5), IpAddress::new(192, 168, 86, 30), Mode::default());// handle error
if let Ok((w5500, (socket, ..))) = w5500 {
let udp_socket = w5500.open_udp_socket(8000, socket);
if let Ok(udp_socket) = udp_socket {
let packet = udp_socket.send(IpAddress::new(192, 168, 86, 35), 4000);
if let Ok(mut packet) = packet {
packet.write(&mut [104, 101, 108, 108, 111]);// ASCII "hello"
packet.send();
}
}
} The good news is that this compiles. The bad news, is that I'm now getting the same error as described in #6 . The gcc-linker step fails. It will succeed if I remove the |
All right after adding compiler-builtins to my project, I can now compile and upload a program to the Arduino. With the program I've been able to read/write from/to some registers of the chip. However, I've run into some trouble with the process of actually sending a UDP packet. I got a dump of the Socket0 register to show what's being written correctly and what's not.
What's very odd is that no matter what I do, writing to anything past the source port (port 0) doesn't seem to stick. That dump of the register above is after the program writes several registers including destination IP (DIPR). But they still read as though they were never written to. |
Sorry for the lack of responses 😢 |
I got sidetracked for a while working on an issue on a lower layer. I should be able to pick this back up again and try to figure it out. |
Hey, great news! But don't stress yourself. I'll be happy to accept your PRs when they are ready! |
Excited to announce that now that AVR-Rust has been upstreamed, I can finally (once again) compile this library to my target platform. I will be resuming development. |
Finally have everything back up and running. I can talk to the chip again (in my https://github.com/jonahbron/w5500/blob/restructure/src/udp/mod.rs#L29 |
Super late to this party: Just an FYI, but from an embedded rust perspective, it's very idiomatic for drivers to take ownership of the communication interface (e.g. SPI in this case). There are possibilities where SPI may be used by multiple devices, but there's actually other crates that facilitate this, so each driver can still own the SPI peripheral. Ref: https://crates.io/crates/shared-bus and https://crates.io/crates/shared-bus-rtic The gist is that these crates provide a "proxy" to the SPI bus, which also implements the associated traits. This allows multiple drivers to "own" the SPI bus at the same time. Those crates facilitate the resource sharing aspect of it. |
@ryan-summers That's good to know, thank you. @kellerkindt I've got some great news. After much gnashing of teeth, I found just a few little (ba78c6d) bugs that were preventing my tests from working. I now officially have a test program that can send a UDP packet 🎉 390f87d has the working library version as of this comment, and here is the full test program code: #![no_std]
#![no_main]
extern crate panic_halt;
use arduino_uno::spi::{Spi, Settings, DataOrder, SerialClockRate};
use arduino_uno::prelude::*;
use embedded_hal::spi;
use nb::block;
use w5500::bus::FourWire;
use w5500::bus::ActiveBus;
use w5500::register;
use w5500::socket::Socket0;
use w5500::socket::Socket;
use w5500::IpAddress;
use w5500::uninitialized_w5500::{UninitializedW5500,InitializeError};
use w5500::MacAddress;
use w5500::Mode;
#[arduino_uno::entry]
fn main() -> ! {
let dp = arduino_uno::Peripherals::take().unwrap_or_else(|| panic!());
let mut delay = arduino_uno::Delay::new();
let mut pins = arduino_uno::Pins::new(dp.PORTB, dp.PORTC, dp.PORTD);
let mut serial = arduino_uno::Serial::new(
dp.USART0,
pins.d0,
pins.d1.into_output(&mut pins.ddr),
57600,
);
let cs = pins.d10.into_output(&mut pins.ddr);
let mut spi = arduino_uno::spi::Spi::new(
dp.SPI,
pins.d13.into_output(&mut pins.ddr),
pins.d11.into_output(&mut pins.ddr),
pins.d12.into_pull_up_input(&mut pins.ddr),
Settings {
data_order: DataOrder::MostSignificantFirst,
clock: SerialClockRate::OscfOver4,
mode: spi::Mode {
polarity: spi::Polarity::IdleLow,
phase: spi::Phase::CaptureOnFirstTransition,
}
},
);
let mut bus = FourWire::new(cs).activate(spi);
let uninitialized_w5500 = UninitializedW5500::new(bus);
let w5500 = uninitialized_w5500.initialize_manual(MacAddress::new(0, 1, 2, 3, 4, 5), IpAddress::new(192, 168, 86, 79), Mode::default());// handle error
if let Ok((w5500, (socket, ..))) = w5500 {
let udp_socket = w5500.open_udp_socket(8001, socket);// source port gets set correctly
if let Ok(mut udp_socket) = udp_socket {
let packet = udp_socket.send(IpAddress::new(192, 168, 86, 38), 8000);
if let Ok(mut packet) = packet {
let wrote = packet.write(&mut [104, 101, 108, 108, 111, 10]);
if let Ok(()) = wrote {
let sent = packet.send();
if let Ok(mut udp_socket) = sent {
ufmt::uwriteln!(&mut serial, "packet sent\r").unwrap();
} else {
ufmt::uwriteln!(&mut serial, "packet send failed\r").unwrap();
}
} else {
ufmt::uwriteln!(&mut serial, "packet write failed\r").unwrap();
}
} else {
ufmt::uwriteln!(&mut serial, "packet start failed\r").unwrap();
}
}
} else if let Err(InitializeError::ChipNotConnected) = w5500 {
ufmt::uwriteln!(&mut serial, "chip not connected\r").unwrap();
} else {
ufmt::uwriteln!(&mut serial, "not initialized\r").unwrap();
}
ufmt::uwriteln!(&mut serial, "DONE\r").unwrap();
loop {}
} There is still a bit of clean-up to do on packet sending, but it actually working at all is an exciting threshold. I also need to put together a working test of packet receiving. Following that, I'll need to re-implement some of the newer features introduced upstream. |
@jonahbron what are you thoughts about @ryan-summers PR which introduces embedded-nal support? |
I think that's an ideal direction. I haven't heard about embedded-nal until now; I need to read through the docs and see how much it conflicts with my design. |
Resolved by #26 |
What do you think about changing the interface so that the
W5500
andActiveW5500
structs take ownership of the SPI/CS chips instead of accepting mutable references to them? Taking ownership of them seems like the more obvious approach to me, but maybe there are factors to use &mut instead that I'm not aware of.The text was updated successfully, but these errors were encountered: