January 10, 2023

Kyu networking -- H3 network PHY and the linux driver

After a lot of searching I tracked down the linux driver for the Allwinner H3. It is:
drivers/net/ethernet/stmicro/stmmac/dwmac-sun8i.c
Also absolutely vital to look at are the device tree files:
arch/arm/boot/dts
arch/arm/boot/dts/sunxi-h3-h5.dtsi
I originally tracked these down in a snapshot of the 5.4.43 sources I have on disk, but I see the C driver (with about 100 extra lines) in the latest git as well, and I will focus my study on the latest git.

Apparently the current source has been reworked by various linux people. Originally source was contributed by "sunxi", but that code has been polished up for whatever reasons in the current linux sources.

It is curious that this is in the ST micro subdirectory. I have not idea what the connection there might be given that this is an Allwinner chip, but this remains a mystery for the time being.

I have learned several things just skimming this driver:

Kyu experimentation

I plan to launch a new file to hold the mdio/phy stuff for the H3. I may well call it h3_phy.c, though it might make more sense to call it emac_phy.c so it will sit right alongside emac.c. I may well also split out emac_regs.h since certain emac registers will need to be shared by emac.c and emac_phy.c

The internal/external MUX business

Here we are again trying to sort out inscrutable linux kernel code. A good part of the trouble is that linux has organized things into subsystems that I am not closely familiar with, not the least of which is the device tree.

The crucial action is in mdio_mux_syscon_switch_fn(). The code I need to understand is:

		regmap_field_read(gmac->regmap_field, ®);
                switch (desired_child) {
                case DWMAC_SUN8I_MDIO_MUX_INTERNAL_ID:
                        dev_info(priv->device, "Switch mux to internal PHY");
                        val = (reg & ~H3_EPHY_MUX_MASK) | H3_EPHY_SELECT;
                        need_power_ephy = true;

		regmap_field_write(gmac->regmap_field, val);
                if (need_power_ephy)
                        ret = sun8i_dwmac_power_internal_phy(priv);

                /* After changing syscon value, the MAC need reset or it will
                 * use the last value (and so the last PHY set).
                 */
                ret = sun8i_dwmac_reset(priv);

Linux device driver abstractions

So we are fooling with some register. The question is what this "regmap" business is all about. It is a substantial linux facility that can be found in "drivers/base/regmap/regmap.c" My hope is that I can observe how the H3 driver utilizes it and avoid digging into the internals. Indeed, a search on "linux regmap" indicates that this is a major linux facility that aims to make device driers more generic. Regmap fields are bit fields within registers.

This regmap business is set up in the sun8i_dwmac_probe() function. The following is the code with as much error handling and such removed.

    static int sun8i_dwmac_probe(struct platform_device *pdev)
	{
	struct regmap *regmap;
	struct sunxi_priv_data *gmac;

	gmac = devm_kzalloc(dev, sizeof(*gmac), GFP_KERNEL);
        gmac->variant = of_device_get_match_data(&pdev->dev);

	regmap = sun8i_dwmac_get_syscon_from_dev(pdev->dev.of_node);
        if (IS_ERR(regmap))
                regmap = syscon_regmap_lookup_by_phandle(pdev->dev.of_node, "syscon");

	gmac->regmap_field = devm_regmap_field_alloc(dev, regmap, *gmac->variant->syscon_field);
	}
When we dig into the sun8i_dwmac_get_syscon_from_dev() routine above, we see various references to "of_" routines. Here "of_" denotes "open firmware" and this is the linux device tree API.
The device tree files are:
arch/arm/boot/dts
arch/arm/boot/dts/sunxi-h3-h5.dtsi
This includes many other files from "dt-bindings/reset/sun8i-h3-ccu.h" and such like.
/u1/linux/linux-git/include/dt-bindings/clock/sun8i-h3-ccu.h
/u1/linux/linux-git/include/dt-bindings/reset/sun8i-h3-ccu.h
The h3-h5 file is included from:
sun8i-h3.dtsi
This file has a lot more useful addresses and such, including:
	    syscon: system-control@1c00000 {
                        compatible = "allwinner,sun8i-h3-system-control";
                        reg = <0x01c00000 0x1000>;
                        #address-cells = <1>;
                        #size-cells = <1>;
                        ranges;
It would certainly be worth our while to spend a day or two getting familiar with the linux device tree framework, along with the regmap scheme. But I am going to take a break from all that and look at the H3 datasheet.

H3 syscon register

The "syscon" (System Control) is documented starting at page 152. The Emac control register pops up right away with bits controlling internal and external MII and PHY. This is called the "EMAC-EPHY Clock Register" and apart from a version register is essentially the only thing in syscon and at offset 0x30.

While we are at it, we take a look at the CCU beginning at page 87. The clock diagram shows the emac hung on the AHB2 bus.
Offset 0x5c is the AHB2 config register, but just controls the bus clock in general.
Offset 0x60 is the bus clock gate register 0, and has a bit for the EMAC.
Offset 0x70 is the bus clock gate register 4, and has a bit for the EPHY.
Offset 0x2c0 is the bus reset register 0, and has a bit for the EMAC.
Offset 0x2c8 is the bus reset register 2, and has a bit for the EPHY.

I made the guess initially that "EPHY" denoted "external PHY". This is entirely wrong, it is the internal PHY. Look at these definitions from the top of dwmac-sun8i.c

/* H3 specific bits for EPHY */
#define H3_EPHY_ADDR_SHIFT      20
#define H3_EPHY_CLK_SEL         BIT(18) /* 1: 24MHz, 0: 25MHz */
#define H3_EPHY_LED_POL         BIT(17) /* 1: active low, 0: active high */
#define H3_EPHY_SHUTDOWN        BIT(16) /* 1: shutdown, 0: power up */
#define H3_EPHY_SELECT          BIT(15) /* 1: internal PHY, 0: external PHY */
#define H3_EPHY_MUX_MASK        (H3_EPHY_SHUTDOWN | H3_EPHY_SELECT)
#define DWMAC_SUN8I_MDIO_MUX_INTERNAL_ID        1
#define DWMAC_SUN8I_MDIO_MUX_EXTERNAL_ID        2
When we switch to the internal PHY we do this:
val = (reg & ~H3_EPHY_MUX_MASK) | H3_EPHY_SELECT;
Note also that these bits (15 and 16) are in the syscon register (offset 0x30)


Have any comments? Questions? Drop me a line!

Kyu / [email protected]