For my master’s thesis, I work with STM32F767
Nulceo-144 boards. We are using
ChibiOS as operating system and
needed support for IP Multicast. This blog post describes what is
necessary to add Multicast support to this setup. Overall, it was a lot
of fun and I learned a lot!
First of all, let’s take a quick look at how Multicast works. Multicast
means that we address a single packet to several peers. This works by
using special addresses. For IPv4 these are 224.0.0.0
to
239.255.255.255
(224.0.0.0/4
). If a host wants
to receive packets for a Multicast address, it must join the respective
Multicast Group. It does so using IGMP.
Then, the router in our network knows that the host wants to receive
traffic for the respective Multicast Group and will start forwarding
traffic.
IP Multicast over Ethernet
In an Ethernet Collision Domain, multiple hosts may want to receive
Multicast traffic. If we would just send traffic to the host’s regular
MAC address, the router would have to duplicate packets, if there is
more than one host receiving multicast traffic. To avoid this, there are
special Multicast MAC addresses. Similarily to a Broadcast MAC address,
they address multiple hosts. These MAC addresses are
01-00-5E-00-00-00
to 01-00-5E-7F-FF-FF
. As we
have 23 Bits for in the Multicast MAC range, but 28 Bits in the
Multicast IP range, this is not a one-to-one mapping. Instead, multiple
Multicast IPs map to a single Multicast MAC address. The mapping works
by OR-ing the lower 23 Bits of the Multicast IP with the Multicast base
MAC address (01-00-5E-00-00-00
).
When the Ethernet MAC of our microcontroller (µC) receives a Ethernet
frame, it takes a look at the destination MAC address of the frame. If
the frame is not addressed to the µC, i.e. its destination MAC address
does not match the MAC address of the µC, it is simply discarded by the
MAC. This saves resources, as the microcontroller can continue working
on other tasks and no CPU time must be spent handling packets that are
not meant for us.
For Multicast, this is of course not what we want: the frames are not
sent to our MAC address, but to the Multicast MAC address! Thus, the MAC
in our µC will simply discard the frame and it will never be seen by
software. This means we have to tell hardware that it should accept
frames for the MAC address of the Multicast group we want to join.
LwIP and Multicast
Fortunately, LwIP already comes with Multicast support. You can join a
multicast group using the netconn_join_leave_group()
function. It will take care of IGMP for you. But, with the stock ChibiOS
MAC drivers, it will not take care of configuring your MAC to accept
packets for the Multicast MAC address! So, the MAC driver has to be
notified when we join a Multicast Group. For this, LwIP allows you to
register a callback using the netif_set_igmp_mac_filter()
function. This callback can then take care of configuring the MAC
appropriately.
Configuring the MAC to accept frames for the Multicast MAC address
Now to the fun low-level part! Let’s take a look at the STM32F76xxx
Reference Manual, section 42.5.5 “MAC filtering”. We have several
options here:
- Set the Pass All Multicast (PAM) bit in the
MACFFR
register. Then, the MAC will pass all Multicast
frames to software. This is of course very easy to implement. We do not
even have to use LwIP’s igmp_mac_filter
. We can just set
the respective bits when the MAC is initialized. This is, however, a
quick and dirty solution, as all Multicast frames then have to go
through the LwIP stack to decide whether we are interested in them. This
means we will waste quite some CPU cycles just for this. Thus, I did not
choose this solution.
- Use perfect filtering and write the Multicast address to one of the
four MAC address registers. However, this limits the number of Multicast
groups we are able to join. As the device itself needs a MAC address, we
can join at most three Multicast groups.
- Use imperfect filtering using the Hash Table. This is a good
compromise between aoproach 1 and 2: We can join as many Multicast
Groups as we want, but we will only waste CPU cycles if there has been a
collision in the hash table. I chose this approach, even though it is
probably the most difficult to implement.
The MAC hash table
Let’s take a look at how the MAC hash table works. When the MAC receives
a frame, it extracts the destination address. It will then compute the
CRC-32 of this MAC address, which results in a 32-bit value. Only the
upper six bits are then used for the hash table.
The hash table consists of two registers: the Ethernet MAC hash table
low register (ETH_MACHTLR) and the Ethernet MAC hash table high
register (ETH_MACTHR). Each of these registers is 32 Bits, which
means that the hash table is 64 Bits in total.
The most significant bit of the calculated CRC value indicates whether
the ETH_MACTLR or the ETH_MACTHR is consulted. The next five bits of the
CRC then indicate the index of the bit in the register that is checked.
If the respective bit is one, the frame passes the MAC filter, otherwise
it does not.
So, in our igmp_mac_filter
callback, we have to do the
following steps:
- From LwIP, we receive an IP address as a parameter to the callback.
We first have to convert this to a MAC address, as explained above.
- We then have to calculate the CRC-32 of this MAC address.
- Finally we set the appropriate bits in the ETH_MACTHR or ETH_MACTLR
registers.
Keep in mind that Ethernet transmits the least significant
bit first. Weirdly, this also applies to the CRC
calculation above. The most significant bit of the CRC is actually the
least significant bit and so on.
I will publish the code as soon as I’ve tested my implementation
thoroughly and all the bugs are fixed. Second blog post with a deep dive
into the code will follow!