It’s week number 11 and the weekly driver #4 is out! Last time, I did drivers 1 and 2 so you may be wondering where’s driver 3? Driver #3, the MCP3008 (8 channel 10-bit ADC with SPI interface), was covered by @pcein in their blog. Also, as of now there are at least 14 (!) drivers being worked on by the community.
This week I’m releasing a driver for the ENC28J60, an Ethernet controller with SPI interface. This IC lets you connect your microcontroller, if it has a SPI interface, to a Local Area Network or, with more work, to the internet. Apart from the IC you need a RJ45 connector and a few other components so I’m using this module which has the ENC28J60 and all the required components on a single board.
enc28j60
The driver crate, the enc28j60
, that lets you interface this chip is kind of boring – as all
drivers should be: boring and with no surprises in them.
To initialize a driver you pass something that implements the SPI traits from the embedded-hal
crate plus a nCS (Clock Select) pin. You can optionally pass the INT and RESET pins; if you pass the
INT (interrupt) pin you can make use of the interrupt API; if you pass the RESET pin then
initialization will use that to reset the ENC28J60 instead of using a software reset. You also need
to pass something that provides delay functionality; a delay is needed in the initialization because
silicon bugs are a thing 1. Finally, you have to pass the size of the internal RX (reception)
buffer and the MAC address that the device will use.
let mut enc28j60 = Enc28j60::new(spi, ncs, int, reset, &mut delay, 7 * KB, MAC)?;
The SPI interface usually runs at a lower rate (e.g. 1 Mbps) than the Ethernet interface (10 Mbps) so it’s not possible to move the incoming data into the microcontroller memory as it arrives. That’s why the ENC28J60 has 8 KB of RAM; in that memory it stores (buffers) all the incoming data until the microcontroller has a chance to read it out. This memory is also used to store the data to transmit so it’s necessary to split the 8 KB in two regions: one for transmission (TX) and one for reception (RX). That’s what the 7 KB in the code snippet is all about: it’s the size of the RX part.
To send out data you use the transmit
method. This method copies (in a blocking fashion) the
specified bytes
into the ENC28J60 memory and starts a transmission.
enc28j60.transmit(bytes)?;
transmit
won’t block until the transmission is finished though. For that you can use the flush
method.
enc28j60.flush()?;
But note that the current implementation of transmit
will flush
any in progress transmission.
This may be lifted in the future to let you queue several frames to send in the ENC28J60 memory.
The bytes
you transmit should be a valid Ethernet frame otherwise the recipient is likely to
discard your data. In the current API bytes
has type &[u8]
, which means that it’s up to the
caller to ensure that the data is a valid Ethernet frame. The driver doesn’t demand any more
elaborated (new)type to let you use it with any network stack you want.
As per the spec Ethernet frames must include a frame check sequence (a CRC) at their end. The
ENC28J60 takes care of computing that and appending it to the frame so the microcontroller doesn’t
have to deal with it. The ENC28J60 will also take care of padding bytes
so the frame meets the
minimum length of 64 bytes.
To check if there’s new data available you have the pending_packets
method which returns the
number of packets that are stored in the ENC28J60 memory and that still need to be processed (read
out).
let pending_packets = enc28j60.pending_packets()?;
Once you have confirmed that there are packets that still need to be processed you can read them out
using the receive
method.
let buf = [0; 256];
while enc28j60.pending_packets()? > 0 {
let n = enc28j60.receive(&mut buf)?;
let frame = &buffer[..n as usize];
// ..
}
receive
pretty much mimics the API of std::io::Read::read
but returns the number of bytes read
as a u16
value because that’s the smallest integer type that makes sense in this case (remember:
only 8 KB of memory).
Note that the ENC28J60 contains a receiver filter and that, by default, will filter out (ignore) packets with invalid CRC, unicast packets that are not addressed to the MAC of the ENC28J60 and packets that are not broadcasts.
That’s the description of the boring driver now let’s look at some demos!
Demos
(All these demos were tested on the Blue Pill development board.)
ping
The first demo is a “pong server” (code here). Basically, it’s a program that responds to
the ping
command.
If you ping the hardcoded IP address of the microcontroller you’ll see this:
$ # remove the IP and MAC address of the microcontroller from the ARP cache
$ _ arp -d 192.168.1.33
$ ping -c3 192.168.1.33
PING 192.168.1.33 (192.168.1.33) 56(84) bytes of data.
64 bytes from 192.168.1.33: icmp_seq=1 ttl=64 time=28.4 ms
64 bytes from 192.168.1.33: icmp_seq=2 ttl=64 time=15.6 ms
64 bytes from 192.168.1.33: icmp_seq=3 ttl=64 time=15.6 ms
--- 192.168.1.33 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 15.674/19.950/28.497/6.043 ms
For comparison here’s the output of ping
ing my router:
$ ping -c3 192.168.1.1
PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.
64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=2.24 ms
64 bytes from 192.168.1.1: icmp_seq=2 ttl=64 time=2.22 ms
64 bytes from 192.168.1.1: icmp_seq=3 ttl=64 time=2.18 ms
--- 192.168.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 2.188/2.219/2.240/0.022 ms
The Round Trip Time (RTT) is 86% smaller in the case of the router.
The microcontroller will also log a bunch of stuff to the ITM. Here are the logs that were
generated during the execution of the first ping
command:
$ itmdump -f /dev/ttyUSB0
Rx(60)
* ether::Frame { destination: mac::Addr([0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), source: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), type: Arp }
** arp::Packet { oper: Request, sha: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), spa: ipv4::Addr([192, 168, 1, 11]), tha: mac::Addr([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), tpa: ipv4::Addr([192, 168, 1, 33]) }
** arp::Packet { oper: Reply, sha: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), spa: ipv4::Addr([192, 168, 1, 33]), tha: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), tpa: ipv4::Addr([192, 168, 1, 11]) }
* ether::Frame { destination: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), source: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), type: Arp }
Tx(42)
Rx(98)
* ether::Frame { destination: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), source: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), type: Ipv4 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 84, identification: 4374, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Icmp, checksum: 0xa616, source: ipv4::Addr([192, 168, 1, 11]), destination: ipv4::Addr([192, 168, 1, 33]) }
*** icmp::Packet { type: EchoRequest, code: 0, checksum: 0x5638, id: 22953, seq_no: 1 }
*** icmp::Packet { type: EchoReply, code: 0, checksum: 0x5e38, id: 22953, seq_no: 1 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 84, identification: 4374, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Icmp, checksum: 0xa616, source: ipv4::Addr([192, 168, 1, 33]), destination: ipv4::Addr([192, 168, 1, 11]) }
* ether::Frame { destination: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), source: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), type: Ipv4 }
Tx(98)
Rx(98)
* ether::Frame { destination: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), source: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), type: Ipv4 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 84, identification: 5023, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Icmp, checksum: 0xa38d, source: ipv4::Addr([192, 168, 1, 11]), destination: ipv4::Addr([192, 168, 1, 33]) }
*** icmp::Packet { type: EchoRequest, code: 0, checksum: 0x1531, id: 22953, seq_no: 2 }
*** icmp::Packet { type: EchoReply, code: 0, checksum: 0x1d31, id: 22953, seq_no: 2 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 84, identification: 5023, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Icmp, checksum: 0xa38d, source: ipv4::Addr([192, 168, 1, 33]), destination: ipv4::Addr([192, 168, 1, 11]) }
* ether::Frame { destination: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), source: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), type: Ipv4 }
Tx(98)
Rx(98)
* ether::Frame { destination: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), source: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), type: Ipv4 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 84, identification: 5092, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Icmp, checksum: 0xa348, source: ipv4::Addr([192, 168, 1, 11]), destination: ipv4::Addr([192, 168, 1, 33]) }
*** icmp::Packet { type: EchoRequest, code: 0, checksum: 0x2c29, id: 22953, seq_no: 3 }
*** icmp::Packet { type: EchoReply, code: 0, checksum: 0x3429, id: 22953, seq_no: 3 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 84, identification: 5092, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Icmp, checksum: 0xa348, source: ipv4::Addr([192, 168, 1, 33]), destination: ipv4::Addr([192, 168, 1, 11]) }
* ether::Frame { destination: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), source: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), type: Ipv4 }
Tx(98)
There are four exchanges in these logs: 1 ARP exchange and 3 ICMP exchanges. Let’s look at them in more detail.
ARP
The first exchange is this ARP exchange.
Rx(60)
* ether::Frame { destination: mac::Addr([0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), source: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), type: Arp }
** arp::Packet { oper: Request, sha: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), spa: ipv4::Addr([192, 168, 1, 11]), tha: mac::Addr([0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), tpa: ipv4::Addr([192, 168, 1, 33]) }
** arp::Packet { oper: Reply, sha: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), spa: ipv4::Addr([192, 168, 1, 33]), tha: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), tpa: ipv4::Addr([192, 168, 1, 11]) }
* ether::Frame { destination: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), source: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), type: Arp }
Tx(42)
In these logs Rx($N)
indicates that $N
bytes were received – the 4 bytes of the CRC are not
included in this number. The lines below the Rx($N)
line correspond to the headers found in the
received data. As we are dealing with Ethernet frames the first header will always be an Ethernet
frame. In this case, the payload of the Ethernet frame is an ARP packet.
The Tx($N)
in the logs indicate that the $N
bytes were sent to the ENC28J60 for transmission –
this number doesn’t include the CRC or the zero padding that the ENC28J60 appends to the frame. The
lines above the Tx($N)
line indicate the headers included in the transmitted data.
So, what’s this ARP thing?
My laptop wants to ping
the microcontroller and knows its IP address: 192.168.1.33 (that’s the
first argument of the ping
command) but it doesn’t know its MAC address, which is required to send
an Ethernet frame.
Before actually ping
ing the microcontroller the laptop will first broadcast (MAC address =
ff:ff:ff:ff:ff:ff) an ARP request. The request basically asks everyone on the LAN: “what’s the MAC
address (THA: Target Hardware Address) of the machine with IP address (TPA: Target Protocol Address)
192.168.1.33?”
When the microcontroller sees its IP in this request it will answer with another ARP packet indicating that its MAC address (SHA: Sender Hardware Address) is 20:18:03:01:00:00 and that its IP address (SPA: Sender Protocol Address) is 192.168.1.33.
From this exchange the microcontroller also learns the MAC address and IP address of my laptop: this information is in the SHA and SPA fields of the received ARP packet.
So, the Address Resolution Protocol (ARP) is used to find out how Protocol Addresses, like IPv4 addresses, map to Hardware Addresses, like MAC addresses – at least within a LAN and when using IPv4 as the data link layer.
ICMP
What the ping
command does under the hood is send ICMP packets of the EchoRequest type to the
specified IP address. Machines that receive this kind of ICMP packet must respond with ICMP
packets of the EchoReply type. The ping
program processes these responses and shows some
statistics about the exchange like the Round Trip Time and the hop distance between the nodes (cf.
ttl).
Back to the exchange, once my laptop learned the MAC address of the microcontroller it started sending ICMP packets. The first ICMP exchange is shown below:
Rx(98)
* ether::Frame { destination: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), source: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), type: Ipv4 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 84, identification: 4374, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Icmp, checksum: 0xa616, source: ipv4::Addr([192, 168, 1, 11]), destination: ipv4::Addr([192, 168, 1, 33]) }
*** icmp::Packet { type: EchoRequest, code: 0, checksum: 0x5638, id: 22953, seq_no: 1 }
*** icmp::Packet { type: EchoReply, code: 0, checksum: 0x5e38, id: 22953, seq_no: 1 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 84, identification: 4374, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Icmp, checksum: 0xa616, source: ipv4::Addr([192, 168, 1, 33]), destination: ipv4::Addr([192, 168, 1, 11]) }
* ether::Frame { destination: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), source: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), type: Ipv4 }
Tx(98)
The first thing to note is that this time the destination MAC address specified in the received Ethernet frame is the MAC address of the microcontroller, and not the broadcast address. The payload of the Ethernet frame this time is an IPv4 packet and the payload of that packet is an ICMP packet.
As expected the ICMP packet is of the EchoRequest type. Its id
(identifier) field indicates the
PID of the ping
command, and the seq_no
(sequence number) field tracks the number of packets
send by the ping
command. If you look at the full log you’ll see that id
remains constant across
all the ICMP exchanges whereas seq_no
monotonically increases.
The microcontroller sends back a EchoReply packet in response to this EchoRequest packet. Most of
the information in the headers, like the id
and seq_no
fields, as well as the payload of the
request are preserved in the reply.
Benchmark
The pong server I showed works fine but it’s wasteful because it busy waits for new packets so I partially rewrote it to be reactive: now it sleeps most of the time and only wakes up to process newly received packets. It does this using the INT (interrupt) pin as a source of interrupts: the ENC28J60 notifies the microcontroller about new packets by driving the INT pin low and this wakes up the microcontroller.
To this version I also added a CPU monitor with the goal of being able to benchmark the performance
of the pong server. Then I benchmarked the final version by spawning several parallel instances of
the ping
command. The results are shown below:
ping s |
CPU usage during one second (worst of 10 samples) |
---|---|
1 | 0.6591% |
2 | 0.8111% |
4 | 1.8581% |
8 | 3.2091% |
16 | 6.3993% |
32 | 12.8010% |
I should note that the CPU was operating at 8 MHz, that logs were disabled during the collection of these statistics and that the driver only exposes a blocking API 2 at the moment so CPU usage could actually be reduced in the future.
UDP
The second demo is a UDP echo server (the code is the same as the first demo’s). This program will send back all the received UDP datagrams, regardless of what their destination port is.
You can test this demo using netcat:
$ nc -u 192.168.1.33 1337
hello
hello
Rustaceans
Rustaceans
The server will echo back everything you send to it.
Here are the logs captured during that UDP exchange:
$ itmdump -f /dev/ttyUSB0
Rx(60)
* ether::Frame {destination: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), source: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), type: Ipv4 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 34, identification: 3907, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Udp, checksum: 0xa80b, source: ipv4::Addr([192, 168, 1, 11]), destination: ipv4::Addr([192, 168, 1, 33]) }
*** udp::Packet { source: 58248, destination: 1337, length: 14, checksum: 20407 }
*** udp::Packet { source: 1337, destination: 58248, length: 14, checksum: 0 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 34, identification: 3907, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Udp, checksum: 0xa80b, source: ipv4::Addr([192, 168, 1, 33]), destination: ipv4::Addr([192, 168, 1, 11]) }
* ether::Frame { destination: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), source: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x0, 0x00]), type: Ipv4 }
Tx(48)
Rx(60)
* ether::Frame { destination: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), source: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), type: Ipv4 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 39, identification: 4839, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Udp, checksum: 0xa462, source: ipv4::Addr([192, 168, 1, 11]), destination: ipv4::Addr([192, 168, 1, 33]) }
*** udp::Packet { source: 58248, destination: 1337, length: 19, checksum: 36455 }
*** udp::Packet { source: 1337, destination: 58248, length: 19, checksum: 0 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 39, identification: 4839, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Udp, checksum: 0xa462, source: ipv4::Addr([192, 168, 1, 33]), destination: ipv4::Addr([192, 168, 1, 11]) }
* ether::Frame { destination: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), source: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), type: Ipv4 }
Tx(53)
This time we have UDP datagrams, instead of ICMP packets, inside the IPv4 packets. I should note that the echo server doesn’t bother with updating the checksum of the UDP datagrams and just zeroes it 3; that’s why you see that all the responses have their UDP checksum set to zero.
I think this is a good time to show the binary size of the demo program:
$ arm-none-eabi-size enc28j60
text data bss dec hex filename
7158 0 4 7162 1bfa enc28j60
This size is with the logging functionality removed.
CoAP
The third and final demo (code here) is a simple CoAP server.
If you are not familiar with the Constrained Application Protocol (CoAP) it’s, more or less, a simplified version of HTTP that runs on top of UDP (HTTP uses TCP as its transport layer). In CoAP you also have GET, PUT, POST and DELETE methods that you can use to implement RESTful APIs.
The difference between HTTP and CoAP is that CoAP has been designed to run on resource constrained nodes; its RFC explicitly mentions “8-bit microcontrollers with small amount of ROM and RAM” as an example of the environments it targets.
In this demo the CoAP server exposes a single resource: an LED at path /led
. The state of the LED
can be queried / modified using GET / PUT requests, respectively.
The jnet
crate provides a simple CoAP client that you can use to interact with the CoAP server.
This is how a GET request looks like:
$ coap GET coap://192.168.1.33/led
-> coap::Message { version: 1, type: Confirmable, code: Method::Get, message_id: 0, options: {UriPath: "led"} }
<- coap::Message { version: 1, type: Acknowledgement, code: Response::Content, message_id: 0, payload: "on" }
on
And this is how a PUT request looks like:
$ coap PUT coap://192.168.1.33/led off
-> coap::Message { version: 1, type: Confirmable, code: Method::Put, message_id: 0, options: {UriPath: "led"}, payload: "off" }
<- coap::Message { version: 1, type: Acknowledgement, code: Response::Changed, message_id: 0 }
Here’s a video where I interact with the CoAP server to control the LED:
And here are the logs collected during the first two CoAP requests:
$ itmdump -f /dev/ttyUSB0
Rx(60)
* ether::Frame { destination: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), source: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), type: Ipv4 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 37, identification: 20643, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Udp, checksum: 0x66a8, source: ipv4::Addr([192, 168, 1, 11]), destination: ipv4::Addr([192, 168, 1, 33]) }
*** udp::Packet { source: 11983, destination: 5683, length: 17, checksum: 57209 }
**** coap::Message { version: 1, type: Confirmable, code: Method::Get, message_id: 0, options: {UriPath: "led"} }
**** coap::Message { version: 1, type: Acknowledgement, code: Response::Content, message_id: 0, payload: "off" }
*** udp::Packet { source: 5683, destination: 11983, length: 16, checksum: 0 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 36, identification: 20643, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Udp, checksum: 0x66a9, source: ipv4::Addr([192, 168, 1, 33]), destination: ipv4::Addr([192, 168, 1, 11]) }
* ether::Frame { destination: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), source: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), type: Ipv4 }
Tx(50)
Rx(60)
* ether::Frame { destination: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), source: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), type: Ipv4 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 39, identification: 22193, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Udp, checksum: 0x6098, source: ipv4::Addr([192, 168, 1, 11]), destination: ipv4::Addr([192, 168, 1, 33]) }
*** udp::Packet { source: 53402, destination: 5683, length: 19, checksum: 53048 }
**** coap::Message { version: 1, type: Confirmable, code: Method::Put, message_id: 0, options: {UriPath: "led"}, payload: "on" }
**** coap::Message { version: 1, type: Acknowledgement, code: Response::Changed, message_id: 0 }
*** udp::Packet { source: 5683, destination: 53402, length: 13, checksum: 0 }
** ipv4::Packet { version: 4, ihl: 5, dscp: 0, ecn: 0, total_length: 33, identification: 22193, df: true, mf: false, fragment_offset: 0, ttl: 64, protocol: Udp, checksum: 0x609e, source: ipv4::Addr([192, 168, 1, 33]), destination: ipv4::Addr([192, 168, 1, 11]) }
* ether::Frame { destination: mac::Addr([0x9c, 0xb6, 0xd0, 0xed, 0xad, 0xff]), source: mac::Addr([0x20, 0x18, 0x03, 0x01, 0x00, 0x00]), type: Ipv4 }
Tx(47)
This is binary size of the CoAP demo with logging functionality disabled:
$ arm-none-eabi-size enc28j60-coap
text data bss dec hex filename
9186 0 4 9190 23e6 enc28j60-coap
I swear that at some point the binary size of the CoAP demo was about the size of the UDP echo server. I, somehow, seem to have made some change that regressed the binary size by around 2 KB sigh. This is why I should commit more often.
Conclusion
There you go: Ethernet functionality for all devices that have a SPI interface via the ENC28J60. The
driver have been kept as simple as possible to let you use it with any network stack. I’ve been
doing my own network experiments in the jnet
crate but you should definitively check out the
smoltcp
crate (I haven’t tested it myself) which is a mature network stack with actual socket
abstractions – it would be great to have an example of enc28j60
+ smoltcp
in the
stm32f103xx-hal
crate!
Thank you patrons! ❤️
I want to wholeheartedly thank:
Iban Eguia, Aaron Turon, Geoff Cant, Harrison Chin, Brandon Edens, whitequark, James Munns, Fredrik Lundström, Kjetil Kjeka, Kor Nielsen, Alexander Payne, Dietrich Ayala, Kenneth Keiter, Hadrien Grasland, vitiral and 54 more people for supporting my work on Patreon.
Let’s discuss on reddit.
-
Vendors document silicon bugs in a document called Silicon Errata. Here is the Silicon Errata for the ENC28J60; I had to work around 5 (!) silicon bugs in the driver to make it work. ↩︎
-
Methods like
transmit
andreceive
could be made asynchronous / non-blocking with the help of the DMA but we don’t have traits for DMA based I/O inembedded-hal
at the moment. ↩︎ -
This is allowed by the spec when using IPv4 as the data link layer. ↩︎