Skip to content
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

Add CompositeEMAC and use it to rewrite STM32 Ethernet MAC driver #438

Merged
merged 47 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
e2de1c0
Start on EMAC rewrite, but realized I can't do zero copy Tx
multiplemonomials Nov 1, 2024
9d65ab2
multicast test working now!
multiplemonomials Jan 20, 2025
9705115
Working with no hacky workarounds!!!11!11!
multiplemonomials Jan 20, 2025
80b2514
Update Nanostack memory manager, run formatter, make tests test nanos…
multiplemonomials Jan 22, 2025
dfcd9eb
Fix build
multiplemonomials Jan 22, 2025
60372ea
Keep workin' on CompositeEMAC
multiplemonomials Jan 30, 2025
2bdca41
Implement GenericEthPhy
multiplemonomials Feb 3, 2025
c849812
Implement Tx DMA
multiplemonomials Feb 10, 2025
693d111
Start on Rx DMA
multiplemonomials Feb 10, 2025
f9686ed
Finish Rx DMA and multicast subscribes
multiplemonomials Feb 11, 2025
599fc10
Progress on MAC driver
multiplemonomials Feb 13, 2025
8f30f73
Initial implementation of MAC driver complete!
multiplemonomials Feb 14, 2025
7b0d534
CompositeEMAC inits!
multiplemonomials Feb 16, 2025
1a2df8b
Phy task and Tx seem to work!
multiplemonomials Feb 16, 2025
26d4a22
Rx kinda working
multiplemonomials Feb 16, 2025
aeff333
Almost working! Just missing memory freed callback
multiplemonomials Feb 16, 2025
8ed9b44
Woring! Add initial support for STM32H5 as well
multiplemonomials Feb 16, 2025
733b1c0
Fix STM32H5 init
multiplemonomials Feb 18, 2025
c94b760
Ethernet working on STM32H5!
multiplemonomials Feb 18, 2025
5c65117
Run formatter
multiplemonomials Feb 18, 2025
eb03e12
Fix us ticker build error
multiplemonomials Feb 18, 2025
997ae9c
Start on MAC v1 driver
multiplemonomials Feb 20, 2025
0e78241
Working on v1 DMA
multiplemonomials Feb 24, 2025
34999a3
Initial implementation of v1 DMA
multiplemonomials Feb 24, 2025
56ad53a
Fix some bugs, Tx DMA working but no packets are getting transmitted
multiplemonomials Feb 24, 2025
baa5b76
Tx and Rx working!
multiplemonomials Feb 25, 2025
42eb490
Update F2 and F4 eth inits
multiplemonomials Feb 25, 2025
5e27b23
Try to fix bank test
multiplemonomials Feb 25, 2025
8080467
Disable Ethernet in HALs
multiplemonomials Feb 25, 2025
b4e04f8
Apparently old GCC can't do attribute packed + alignas
multiplemonomials Feb 25, 2025
df8ebd7
Add RMII watchdog, add power test, fix unittests build
multiplemonomials Feb 27, 2025
126bfd5
Debugging EMAC reboot test
multiplemonomials Mar 2, 2025
a0145e5
Fix deinit crash on STM32F7
multiplemonomials Mar 2, 2025
7deab95
Fix a couple issues with the power down test
multiplemonomials Mar 12, 2025
292d566
Start on docs
multiplemonomials Mar 12, 2025
0b76925
Oops missed diagram
multiplemonomials Mar 12, 2025
64a973b
Fix typo
multiplemonomials Mar 13, 2025
6b0411f
Bugfix, improve docs
multiplemonomials Mar 16, 2025
2ad785e
Fix PNG
multiplemonomials Mar 16, 2025
65be6a8
More docs, renames
multiplemonomials Mar 17, 2025
491c063
More renames, fix !=
multiplemonomials Mar 23, 2025
0776a75
Document Tx DMA
multiplemonomials Mar 23, 2025
6bc2ef3
Fix some DMA bugs:
multiplemonomials Mar 23, 2025
c8ae2e4
Make EMAC memory test smarter. It now disables pool allocations when …
multiplemonomials Mar 23, 2025
1dc339c
Document most of Rx DMA, do rename
multiplemonomials Mar 23, 2025
3a77ce0
Reformat, fix STM32 Eth v2 potential to have issues when the Tx ring …
multiplemonomials Mar 24, 2025
d359996
Finish documenting Rx DMA
multiplemonomials Mar 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion connectivity/drivers/emac/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ if(NOT "DEVICE_EMAC=1" IN_LIST MBED_TARGET_DEFINITIONS)
return()
endif()

add_library(mbed-emac STATIC EXCLUDE_FROM_ALL)
add_library(mbed-emac STATIC EXCLUDE_FROM_ALL
sources/CompositeEMAC.cpp
sources/GenericEthPhy.cpp
sources/PhyDrivers.cpp)

target_include_directories(mbed-emac PUBLIC include)

if("ARM_FM" IN_LIST MBED_TARGET_LABELS)
add_subdirectory(TARGET_ARM_FM)
Expand Down
97 changes: 97 additions & 0 deletions connectivity/drivers/emac/CompositeEMAC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Composite EMAC

The original Mbed OS EMAC API was added in Mbed OS 5.9. That API grouped all EMAC functionality into a single abstract class that targets/vendors had to implement, the `EMAC` class. Since then, dozens of Mbed targets have received EMAC drivers, and the strengths and weaknesses of this API have become clear. The general structure is good, and the idea of abstracting the memory manager and the network stack from the EMAC driver works well.

However, the [EMAC interface](https://github.com/mbed-ce/mbed-os/blob/0553c111850997d847dc6a3189ac0b7048304e57/connectivity/netsocket/include/netsocket/EMAC.h#L33) is difficult to implement, especially for people not intimately familiar with Mbed and its IP stacks. It requires EMAC implementations to implement the specifics of memory management, MAC address tracking, and DMA ring usage themselves, even though quite a bit of this logic is common to all MAC drivers. This has led to duplicated code, and quite often to half-assed code as well as chip vendors have struggled to conform to the (in some ways not very well defined) EMAC API.

Couple that with inconsistent testing, and you have a recipe for inconsistent and buggy Ethernet drivers across the breadth of Mbed devices. For instance, Mbed supports zero-copy EMACs, where buffers can be passed directly to and from the Ethernet peripheral without being copied. This saves both memory and CPU time. However, this was only ever implemented for a few targets, because it's very difficult to get right. Even more egregiously, the EMAC driver implemented for STM32H7 for the past 6+ years has ignored the memory manager API and used LwIP directly, making it impossible to even test it with the EMAC tests (and hoo boy, were there a lot of things that would have failed). For extra fun, this driver also ignored the DMA functionality and sent packets _synchronously_, meaning the application is blocked while a packet is being transmitted!

To address this situation, Mbed CE is implementing a new layer in the EMAC driver stack: CompositeEMAC. CompositeEMAC is a class which implements the `EMAC` API and breaks up the functionality into several subclasses.

![Overview diagram](./doc/cemac-overview.svg)

By implementing these four subclasses for each target MCU and board, high-performance Ethernet can be ported to any Mbed target much more easily than before.

## Embedded Ethernet - An Overview

Before we can get into the details of how CompositeEMAC works, we need to go over how embedded Ethernet works in general.

![Embedded Ethernet diagram](./doc/embedded-ethernet.svg)

To run an ethernet connection, two chips need to work together*: the microcontroller and an external Ethernet PHY. The microcontroller sends and receives logic level Ethernet packets, while the PHY transforms those into Ethernet signals, which are decidedly *not* logic level (and actually have a lot in common with radio signals). The Ethernet signals, called MDI (Media Dependent Interface) pairs, are sent through an isolation transformer, which removes common mode interference and provides electrical isolation (e.g. so that the two ends of the connection can have different ground voltage levels).

The PHY and the MCU are connected via a standard called [Reduced Media Independent Interface](https://en.wikipedia.org/wiki/Media-independent_interface#RMII) (RMII), which transfers the Ethernet packets as serialized bytes. This is an 8-wire bus with a 50MHz clock, four receive lines, and three transmit lines. The clock is traditionally either supplied by the PHY or by a dedicated clock generator chip, though some MCUs support supplying this clock as well. In addition to RMII, there's also a two-wire command and control bus called [Management Data IO](https://en.wikipedia.org/wiki/Management_Data_Input/Output) (MDIO) (though it can also be referred to Station Management Interface (SMI) or even "MiiM"). MDIO is used for talking directly to the PHY, not for sending Ethernet packets. MDIO is an open-drain bus similar to I2C, but with 16-bit words instead of bytes and a specific frame format (referred to as "Clause 22"). Unlike RMII, MDIO is a multi-drop bus, so you can actually connect up to 15 PHYs or other devices to one set of MDIO lines as long as they have different addresses!

Inside the microcontroller, the bridge between the CPU and Ethernet is a peripheral called the Ethernet MAC. MAC stands for "Media Access Control" and refers to the second layer of the Ethernet protocol stack, the logic which encodes Ethernet packets and decides when to send them across the wire. The MAC has a number of moving parts inside. The simplest is the block of configuration registers, which is accessible at a specific memory address and sets up operation of the MAC (e.g. what MAC addresses the hardware should accept and which checksums should be inserted/checked by the MAC). There is also an MDIO master interface, which controls the MDIO lines to talk to the PHY.

Every Ethernet MAC I've seen also has DMA functionality. This means that the Ethernet peripheral can transmit and receive packets without direct CPU intervention. This is very important because it means your device can hit high network speeds without needing to have your CPU blocked for lots of time waiting on Ethernet packets to move through the hardware! For transmit, there will be a Tx DMA module which fetches data from the main RAM, and then enqueues the packet bytes plus control information into a FIFO (which is usually at least a couple thousand bytes long). Then, another block in the MAC, sometimes called the MTL (MAC Translation Layer) takes these bytes, applies any needed Ethernet framing, and shifts them out of the RMII Tx port.

For reception, the process works the same but in reverse: the decoder and shifter block takes in packets and enqueues their bytes into the Rx FIFO. Then, the Rx DMA dequeues the packets and stores them into RAM at the right location.

How does the DMA know where in RAM to read and write packets, though? On every embedded MAC I have seen so far, this is done through a ring of "DMA descriptors". Here's a diagram (that I stole from the STM32F2 datasheet):

![DMA descriptor ring](doc/stm32f2-eth-dma-descriptors.png)

A descriptor is a structure in memory that contains control information and one or more pointers to memory buffers (which contain the actual packet data). For Tx, the DMA will fetch the descriptor, then transmit the data in the buffers. For Rx, the DMA will fetch the descriptor, then write the packet to the descriptor's buffers. Either way, when the MAC is done with the descriptor, the DMA will write back status information (e.g. whether the checksum passed, or what timestamp the packet was sent at) to the descriptor, set a "done" flag, and then interrupt the CPU to tell it it has something to process.

But we don't want the DMA to have to wait for the CPU, do we? To avoid this, each descriptor also specifies a "next descriptor", either via an offset or a pointer. The DMA can move to this next descriptor and start processing it right away to send or receive the next packet. The CPU will process the completed descriptor on its own time and give it back to the DMA. In this manner, as long as your ring of descriptors is big enough and your CPU can keep up with the processing them, the CPU and MAC never have to wait for each other!

## Components of the Composite EMAC
### MAC Driver

The MAC driver (which must be implemented as a subclass of `CompositeEMAC::MACDriver`) is usually fairly simple. It provides an interface between Mbed and the MAC's configuration register block and MDIO master interface. Its responsibilities include:
- Initializing and muxing the RMII and MDIO pins
- Initializing all needed clocks
- Configuring all settings needed for MAC operation
- Configuring the unicast MAC address (as in, the MAC address that the device uses on the network)
- Adding and removing multicast subscriptions
- Configuring interrupts
- Talking to the PHY over MDIO

### PHY Driver

The PHY driver must be a subclass of `CompositeEMAC::PHYDriver`. It must:
- Confirm the existence of the PHY chip and initialize it
- Configure the selected Ethernet settings (autonegotiation, speed, duplex) into the PHY
- Check if link has been established and, if so, what kind

Unlike the MAC driver and the DMA, the PHY driver does not need to be subclassed for each target device. Thankfully, the Ethernet standard imposes some order on the chaotic sea of PHY parts, and it mandates that the lower 16 registers are standardized and must work the same way on each part. Using this standard behavior, we have implemented the `mbed::GenericEthPhy` class, which should function as a driver for any 802.3u standard compliant PHY. All it needs is configuration, like the PHY's part number and its address on the MDIO bus. When porting to a new target, all you need to do is indicate the PHY model in `mbed-os/connectivity/netsocket/mbed_lib.json` like so:

```json5
"MY_TARGET": {
"nsapi.emac-phy-model": "LAN8742",
"nsapi.emac-phy-mdio-address": 0
}
```

This will work out of the box, as long as `LAN8742` names a PHY driver defined in PhyDrivers.cpp. Individual PHY models will generally need their own drivers, since often PHYs have errata that need to be worked around or need other configuration that isn't defined in the standard. However, GenericEthPhy allows implementing the absolute minimum amount of logic per-phy as possible!

Since user boards may want to use a different ethernet PHY, the driver can be customized in an application by overriding the `mbed::get_eth_phy_driver` weak function. This might look something like

```c++
namespace MY_PHY {
inline constexpr GenericEthPhy::Config Config = {
// These are found in the PHY datasheet. See GenericEthPhy::Config for documentation.
.OUI = 0x123,
.model = 0x45,
.address = 0,
};

class Driver : public GenericEthPhy {
public:
explicit Driver(GenericEthPhy::Config const & config = DefaultConfig):
GenericEthPhy(config)
{}

// You may override/replace any functions of `GenericEthPhy` here
};
}

namespace mbed {
CompositeEMAC::PHYDriver * get_eth_phy_driver()
{
static MY_PHY::Driver(MY_PHY::Config) phyDriver;
return &phyDriver;
}
}
```
22 changes: 18 additions & 4 deletions connectivity/drivers/emac/TARGET_STM/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,28 @@ elseif("STM32F7" IN_LIST MBED_TARGET_LABELS)
add_subdirectory(TARGET_STM32F7)
elseif("STM32H7" IN_LIST MBED_TARGET_LABELS)
add_subdirectory(TARGET_STM32H7)
elseif("STM32H5" IN_LIST MBED_TARGET_LABELS)
add_subdirectory(TARGET_STM32H5)
endif()

target_include_directories(mbed-emac
PUBLIC
.
)

target_sources(mbed-emac
PRIVATE
stm32xx_emac.cpp
)
if("STM32H7" IN_LIST MBED_TARGET_LABELS OR "STM32H5" IN_LIST MBED_TARGET_LABELS)
target_sources(mbed-emac
PRIVATE
STM32EthMACv2.cpp
)
endif()

if("STM32F2" IN_LIST MBED_TARGET_LABELS OR "STM32F4" IN_LIST MBED_TARGET_LABELS OR "STM32F7" IN_LIST MBED_TARGET_LABELS)
target_sources(mbed-emac
PRIVATE
STM32EthMACv1.cpp
)
endif()


target_compile_options(mbed-emac PRIVATE -Wno-packed-bitfield-compat)
84 changes: 84 additions & 0 deletions connectivity/drivers/emac/TARGET_STM/STM32EthMACCommon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* Copyright (c) 2025 Jamie Smith
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#pragma once

#include "device.h"
#include "CompositeEMAC.h"
#include "MbedCRC.h"

// Figure out the Ethernet IP version in use
#if defined(TARGET_STM32H5) || defined(TARGET_STM32H7)
#define ETH_IP_VERSION_V2
#else
#define ETH_IP_VERSION_V1
#endif

namespace mbed
{
constexpr auto MDIO_TRANSACTION_TIMEOUT = std::chrono::milliseconds(1); // used by STMicro HAL

inline constexpr size_t NUM_PERFECT_FILTER_REGS = 3;
static const std::pair<volatile uint32_t *, volatile uint32_t *> MAC_ADDR_PERF_FILTER_REGS[NUM_PERFECT_FILTER_REGS] = {
{&ETH->MACA1HR, &ETH->MACA1LR},
{&ETH->MACA2HR, &ETH->MACA2LR},
{&ETH->MACA3HR, &ETH->MACA3LR}
};

/// Write a MAC address into the given registers with the needed encoding
static inline void writeMACAddress(const CompositeEMAC::MACAddress & mac, volatile uint32_t *addrHighReg, volatile uint32_t *addrLowReg)
{
/* Set MAC addr bits 32 to 47 */
*addrHighReg = (static_cast<uint32_t>(mac[5]) << 8) | static_cast<uint32_t>(mac[4]) | ETH_MACA1HR_AE_Msk;
/* Set MAC addr bits 0 to 31 */
*addrLowReg = (static_cast<uint32_t>(mac[3]) << 24) | (static_cast<uint32_t>(mac[2]) << 16) |
(static_cast<uint32_t>(mac[1]) << 8) | static_cast<uint32_t>(mac[0]);
}

/// Add a MAC address to the multicast hash filter.
void addHashFilterMAC(ETH_TypeDef * base, const CompositeEMAC::MACAddress & mac) {
#if defined(ETH_IP_VERSION_V2)
uint32_t volatile * hashRegs[] = {
&base->MACHT0R,
&base->MACHT1R
};
#else
uint32_t volatile * hashRegs[] = {
&base->MACHTLR,
&base->MACHTHR
};
#endif

// Note: as always, the datasheet description of how to do this CRC was vague and slightly wrong.
// This forum thread figured it out: https://community.st.com/t5/stm32-mcus-security/calculating-ethernet-multicast-filter-hash-value/td-p/416984
// What the datasheet SHOULD say is:
// Compute the Ethernet CRC-32 of the MAC address, with initial value of 1s, final XOR of ones, and input reflection on but output reflection off
// Then, take the upper 6 bits and use that to index the hash table.

mbed::MbedCRC<POLY_32BIT_ANSI> crcCalc(0xFFFFFFFF, 0xFFFFFFFF, true, false);

// Compute Ethernet CRC-32 of the MAC address
uint32_t crc;
crcCalc.compute(mac.data(), mac.size(), &crc);

// Take upper 6 bits
uint32_t hashVal = crc >> 26;

// Set correct bit in hash filter
*hashRegs[hashVal >> 5] |= (1 << (hashVal & 0x1F));
}

}
Loading