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’sigmp_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!