Using an enc28j60 on the zero

I built a new kernel with enc28j60 as a module and enabled the overlay for SPI:

root@radxa-zero:/# cat /boot/uEnv.txt 
verbosity=7
console=ttyAML0,115200
overlay_prefix=meson
rootfstype=ext4
fdtfile=amlogic/meson-g12a-radxa-zero.dtb
overlays=meson-g12a-uart-ao-a-on-gpioao-0-gpioao-1 meson-g12a-spi-spidev
param_spidev_spi_bus=1
param_spidev_max_freq=10000000
rootuuid=dc5be088-dcd0-4a74-96e5-7879177587da
initrdsize=0xc16519
kernelversion=5.10.69-999-amlogic
initrdimg=initrd.img-5.10.69-999-amlogic
kernelimg=vmlinuz-5.10.69-999-amlogic
root@radxa-zero:/# lsmod | grep enc   
enc28j60               40960  0

Still, I don’t have any ethernet device pop up in dmesg. I tried both bus 0 and 1 for SPI.

However, even before I built the kernel with enc28j60 support, I had this in dmesg:

[    3.869559] Hardware name: Radxa Zero (DT)
[    3.873613] pstate: 60000005 (nZCv daif -PAN -UAO -TCO BTYPE=--)
[    3.879572] pc : spidev_probe+0x1c8/0x250 [spidev]
[    3.884318] lr : spidev_probe+0x1c8/0x250 [spidev]
[    3.889050] sp : ffff80001424b8f0
[    3.892325] x29: ffff80001424b8f0 x28: ffff800010039cb0 
[    3.897589] x27: ffff800008e0d4d0 x26: ffff8000116bfc10 
[    3.902848] x25: 0000000000000018 x24: ffff800008e0d058 
[    3.908108] x23: ffff8000117319a0 x22: 0000000000000000 
[    3.913370] x21: ffff800008e0d038 x20: ffff000000384800 
[    3.918632] x19: 0000000000000000 x18: 0000000000000030 
[    3.923893] x17: 0000000000000000 x16: 0000000000000000 
[    3.929154] x15: 0000006f8ea5a514 x14: 0000000000000290 
[    3.934415] x13: 00000000000002a2 x12: 0000000000000000 
[    3.939677] x11: 0000000000000000 x10: 0000000000000a20 
[    3.944938] x9 : ffff80001424b620 x8 : ffff00000090a780 
[    3.950199] x7 : ffff0000e47bbc40 x6 : 0000000000000001 
[    3.955459] x5 : 0000000000000000 x4 : 0000000000000028 
[    3.960718] x3 : ffff000000806210 x2 : 0000000000000000 
[    3.965979] x1 : 0000000000000000 x0 : ffff000000909d00 
[    3.971242] Call trace:
[    3.973661]  spidev_probe+0x1c8/0x250 [spidev]
[    3.978062]  spi_drv_probe+0x84/0xe4
[    3.981593]  really_probe+0xf0/0x510
[    3.985128]  driver_probe_device+0xf4/0x160
[    3.989268]  device_driver_attach+0xc0/0xcc
[    3.993407]  __driver_attach+0xa4/0x170
[    3.997204]  bus_for_each_dev+0x70/0xd0
[    4.000998]  driver_attach+0x24/0x30
[    4.004534]  bus_add_driver+0x140/0x234
[    4.008329]  driver_register+0x78/0x130
[    4.012124]  __spi_register_driver+0x4c/0x5c
[    4.016352]  spidev_init+0xa4/0x1000 [spidev]
[    4.020665]  do_one_initcall+0x50/0x1b0
[    4.024460]  do_init_module+0x54/0x250
[    4.028166]  load_module+0x1f20/0x2300
[    4.031875]  __do_sys_finit_module+0xbc/0x130
[    4.036188]  __arm64_sys_finit_module+0x24/0x30
[    4.040675]  el0_svc_common.constprop.0+0x78/0x1c4
[    4.045417]  do_el0_svc+0x24/0x8c
[    4.048696]  el0_svc+0x14/0x20
[    4.051712]  el0_sync_handler+0xe8/0xf0
[    4.055507]  el0_sync+0x180/0x1c0
[    4.058785] ---[ end trace a7ac2e45a09525ec ]---

Not sure what to do of that and if it may be related?

That overlay only enables SPI as a communication protocol so if you have a user space program that need to manually communicate to a SPI device that’s what you need. You need to instead have a custom device tree overlay telling the kernel that there is actually a device attached to it and you don’t want to manually communicate it with SPI protocol.

You can use meson-g12a-spi-spidev as a template and add something like this to enable the device. This is just the rough idea and you need to adjust some details to make it working.

Thanks, I more or less understands what needs to be done :wink:
Can’t say that I’m comfortable with it, but I’ll give it a try, maybe later today.

For others looking for the same information, I also found these which might additionally serve as templates:

OK, so I created a new .dts file.
Looks like this:

/dts-v1/;
/plugin/;

/ {
	compatible = "radxa,zero", "amlogic,g12a";

	fragment@0 {
		target = <&spi0>;
		__overlay__ {

			#address-cells = <1>;
			#size-cells = <0>;

			spidev@0 {
				status = "disabled";
			};

			eth0: enc28j60@0 {
				compatible = "microchip,enc28j60";
				reg = <0>;
				pinctrl-0 = <&eth0_pins>;
				pinctrl-names = "default";
				interrupt-parent = <&gpio>;
				interrupts = <25 0x2>;	/* falling edge */
				spi-max-frequency = <12000000>;
				status = "okay";
			};
		};
	};

	fragment@1 {
		target = <&gpio>;
		__overlay__ {
			eth0_pins: eth0_pins {
				amlogic,pins = <25>;
				amlogic,function = <0>;	/* in */
				amlogic,pull = <0>; /* none */
			};
		};
	};

	__overrides__ {
		cs    = <&eth0>, "reg:0", <0>, "!0=1";
		irq   = <&eth0>, "interrupts:0", <&eth0_pins>, "amlogic,pins:0";
		speed = <&eth0>, "spi-max-frequency:0";
	};

};

I have found that I can build it like this:

dtc -I dts -O dtb -o arch/arm64/boot/dts/amlogic/overlay/meson-g12a-spi-enc28j60.dtbo arch/arm64/boot/dts/amlogic/overlay/meson-g12a-spi-enc28j60.dts 

But of course, it would be preferable to get the kernel build to include it in its build somehow…

I’ll post updates to this comment as I make progress (or not).

March 23rd

I’ve been reading through multiple other examples as well as through meson-g12a-radxa-zero.dts and I can’t really figure out what needs to go where. None of the terms used make any sense to me, nor do the values.

There are not enough examples of other peripherals for the radxa using SPI for me to infer what needs to go where unfortunately.

How does one get an understanding of this? How would I know which values even exist for e.g. interrupt-parent ? What do 25 and 0x2 mean in the context of interrupts and is that something Raspberry-Pi specific or not? I.e. I would think that the value that should go there would be something related to GPIOC_7 since that’s the pin on the radxa board that leads to the INT pin on the enc28j60 board.

I’ll take a step back and try to learn the basics on device trees, etc. Still help would be appreciated since I’m under some time pressure and any help will save me some sleep and hair.

But of course, it would be preferable to get the kernel build to include it in its build somehow…

Got that figured out from Thomas’ video. The path to the file goes into arch/ar4/boot/dts/amlogic/overlay/Makefile.

March 24th

Noon

Have watched more of Thomas Petazzoni’s talks… I have now switched from the fragment-based syntax for overlays to the &reference-based one as recommended by this Google guide for Android.

The resulting dts file now looks like this:

/dts-v1/;
/plugin/;

&spicc1 {

	#address-cells = <1>;
	#size-cells = <0>;

	eth1: enc28j60@0 {
		compatible = "microchip,enc28j60";
		reg = <0>;	/* for spi, this defines the chipselect */
		interrupt-parent = <&gpio_intc>;
		interrupts = <25 0x2>;	/* gpio_int has #interrupts-cell = <2>, falling edge */
		spi-max-frequency = <12000000>;
		status = "okay";
	};
};

I have also read through the dtsi file common to all meson platforms (arch/arm64/boot/dts/amlogic/meson-g12-common.dtsi), learning in the process that gpio_intc is an interrupt-controller on cbus: bus@ffd00000.
spicc1_pins however are defined below apb: bus@ff600000 > periphs: bus@34400 > periphs_pinctrl > gpio.

In any case, this heavily simplified dtbo results in the following dmesg output:

[    3.877628] enc28j60 spi1.0: Ethernet driver 1.02 loaded
[    3.883065] enc28j60 spi1.0: chip not found
[    3.884736] enc28j60: probe of spi1.0 failed with error -5

Afternoon

I believe this to be the correct interrupt configuration for my case (having wired the enc28j60 to SPI_B (pins 19, 21, 23, 25 and pin 22 as INT)):

/dts-v1/;
/plugin/;

#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/gpio/meson-g12a-gpio.h>

&spicc1 {

	#address-cells = <1>;
	#size-cells = <0>;

	eth1: enc28j60@0 {
		compatible = "microchip,enc28j60";
		reg = <0>;	/* for spi, this defines the chipselect */
		interrupt-parent = <&gpio_intc>;
		/* gpio_int has interrupts-cell = <2>, falling edge */
		interrupts = <GPIOC_7 IRQ_TYPE_EDGE_FALLING>;	// GPIOC_7
		spi-max-frequency = <12000000>;
		status = "okay";
	};
};

Nothing has changed about the output of dmesg for far, but I believe I am missing the pinctrl-0 stuff.

March 25th, 5pm

The dts has evolved a bit since I have changed the interrupt pin from pin 22 (GPIOC_7) to pin 28 (GPIOAO_2) given that pin 22 in an open drain as @linuxlion wrote. The file now looks like this:

 /dts-v1/;
/plugin/;

#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/gpio/meson-g12a-gpio.h>

&spicc1 {

	#address-cells = <1>;
	#size-cells = <0>;

	eth1: enc28j60@0 {
		compatible = "microchip,enc28j60";
		reg = <0>;	/* for spi, this defines the chipselect */
		interrupt-parent = <&gpio_intc>;
		/**
		 * gpio_int has interrupts-cells = <2>
		 * Pin#22 (GPIOC_7) and Pin#36 (GPIOH_8) are open drain pins.
		 * This means for input they need to connected to either GND or VCC (floating state is undefined),
		 * for output they will need external pull up
		 */
		// interrupts = <GPIOC_7 IRQ_TYPE_EDGE_FALLING>;	// <48, 2>
		interrupts = <GPIOAO_2 IRQ_TYPE_EDGE_FALLING>;	// <2, 2>

		pinctrl-names = "default";
		//pinctrl-0 = <&spicc1_pins &spicc1_ss0_pins>;
		pinctrl-0 = <&enc28j60_pins>;

		spi-max-frequency = <12000000>;
		status = "okay";
	};
};

// Targetting gpio, which is a node of periphs (periphs_pinctrl)
&gpio {
	enc28j60_pins:  enc28j60_pins@0 {
		amlogic,pins = <GPIOAO_2>;
		amlogic,function = <0>; /* in */
		amlogic,pull = <IRQ_TYPE_NONE>;
	};
};

The status however is still the same as yesterday morning:

[    3.932714] enc28j60 spi1.0: Ethernet driver 1.02 loaded
[    3.955366] enc28j60 spi1.0: chip not found
[    3.955544] enc28j60: probe of spi1.0 failed with error -5

March 25th, 7pm

I have found some mistakes in the dts and I have finally managed to at least get pinctrl to what I need. The dts now looks like this:

/dts-v1/;
/plugin/;

#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/gpio/meson-g12a-gpio.h>

&spicc1 {

	// See meson-g12a-spi-spidev.dts
	pinctrl-0 = <&spicc1_pins &spicc1_ss0_pins>;
	pinctrl-names = "default";
	status = "okay";

	// spicc1 defined in meson-g12-common.dtsi
	#address-cells = <1>;
	#size-cells = <0>;

	eth1: enc28j60@0 {
		compatible = "microchip,enc28j60";
		reg = <0>;	/* for spi, this defines the chipselect */
		interrupt-parent = <&gpio_intc>;
		/**
		 * gpio_int has interrupts-cells = <2>
		 * Pin#22 (GPIOC_7) and Pin#36 (GPIOH_8) are open drain pins.
		 * This means for input they need to connected to either GND or VCC (floating state is undefined),
		 * for output they will need external pull up.

		 How does this distinguish between e.g. GPIOAO_4 on the first chip and GPIOZ_4 on the second ?!
		 */
		// interrupts = <GPIOC_7 IRQ_TYPE_EDGE_FALLING>;	// <48, 2>
		// interrupts = <GPIOAO_2 IRQ_TYPE_EDGE_FALLING>;	// <2, 2>
		interrupts = <GPIOAO_4 IRQ_TYPE_EDGE_FALLING>;	// <4, 2>

		pinctrl-0 = <&enc28j60_pins>;
		pinctrl-names = "default";

		spi-max-frequency = <12000000>;
		status = "okay";
	};
};

// See meson-g12-common.dtsi
&periphs_pinctrl {
	enc28j60_pins:  enc28j60_pins@0 {
		amlogic,pins = <GPIOAO_4>;
		amlogic,function = <0>; /* in */
		amlogic,pull = <IRQ_TYPE_NONE>;
	};
};

// Not entirely sure this is correct, but I haven't found any other way to do this
// and this compiles
/ {
	__overrides__ {
		int_pin = <&eth1>, "interrupts:0",
				  <&enc28j60_pins>, "brcm,pins:0";
		speed   = <&eth1>, "spi-max-frequency:0";
	};
};

The state of the enc28j60 is almost unchanged, except for the fact that it now states that some default state is not properly mapped (I assume this refers to pinctrl-0 on the enc28j6@0 node):

[    3.901038] enc28j60 spi1.0: there is not valid maps for state default
[    3.902188] enc28j60 spi1.0: Ethernet driver 1.02 loaded
[    3.947474] enc28j60 spi1.0: chip not found
[    3.947564] enc28j60: probe of spi1.0 failed with error -5

And, as mentioned before, pinctrl handles now feature:

[…]
device: ffd15000.spi current state: default
  state: default
    type: MUX_GROUP controller pinctrl-meson group: spi1_mosi (262) function: spi1 (4)
    type: CONFIGS_GROUP controller pinctrl-meson group spi1_mosi (262)config 000fa00a
    type: MUX_GROUP controller pinctrl-meson group: spi1_miso (263) function: spi1 (4)
    type: CONFIGS_GROUP controller pinctrl-meson group spi1_miso (263)config 000fa00a
    type: MUX_GROUP controller pinctrl-meson group: spi1_clk (265) function: spi1 (4)
    type: CONFIGS_GROUP controller pinctrl-meson group spi1_clk (265)config 000fa00a
    type: MUX_GROUP controller pinctrl-meson group: spi1_ss0 (264) function: spi1 (4)
    type: CONFIGS_GROUP controller pinctrl-meson group spi1_ss0 (264)config 00000001
[…]

March 25th, 8:30pm

No progress. Just for fun, I connected a second enc28j60 to SPI-A with the exact same result:

[    3.892814] enc28j60 spi0.0: Ethernet driver 1.02 loaded
[    3.902207] enc28j60 spi0.0: chip not found
[    3.902642] enc28j60: probe of spi0.0 failed with error -5
[    3.906838] enc28j60 spi1.0: Ethernet driver 1.02 loaded
[    3.925501] enc28j60 spi1.0: chip not found
[    3.925591] enc28j60: probe of spi1.0 failed with error -5
1 Like

Hi @kwisatz, great work on this and much appreciated for sharing the resources you’re learning from and iterations of your project!

I’ve been on a quest to enable onewire (that was quick) and then to also allow parasitic power, which requires an increase in current output (from 500uA to 1500uA or more). This second goal has unfortunately been more of an uphill slog entering its third week.

As my expertise is still growing, please take my ideas with a grain of salt. I’m wondering if your needed pin group is
pinctrl-0 = <&spicc0_x_pins &spicc0_ss0_x_pins>; # for SPI-A
or
pinctrl-0 = <&spicc1_pins &spicc1_ss0_pins>; # for SPI-B
rather than
pinctrl-0 = <&eth0_pins>;

My thinking is the spi pins are defined for the board and include the pads specific to how you could connect your ethernet interface. I did find eth0_pins however they aren’t defined for the Zero pads/pins unless you’re still using some form of the code from your March 22nd example.

The tools I’ve found helpful include searching through the kernel source for existing definitions, such as looking for all pinctrl-0 entries in device tree source files specific to the Zero:
cd /path/to/kernel/source
find . -type f -iname '*g12*.dts' | xargs grep pinctrl-0 | more

Changing your search criteria or the path or cd to a the directory you indicate (arch/arm64/boot/dts/amlogic) could also broaden or focus your search with cd simplifying the resulting output.

That’s helped me compare existing code, then on the device side, @RadxaYuntian pointed me to sysfs, so I’ve been similarly searching there by becoming root and digging around with something like:
find /sys -iname '*spi*'

The sys file structure includes debug & firmware which may help indicate the status of your device such as what’s been recognized from your code or not.

Another caveat is in using GPIOC_7 as this is an open drain requiring Vcc or GND for use as input as detailed in the More details about 40-pin Header below the GPIO Table.

I created a blob to use that pin as its current output already met my use case needs, however onewire wasn’t accessible while connected to Vcc (or GND or nothing).

Anyway, hope this helps and your are successful in your goal!

@linuxlion, first of all, thanks for your feedback and encouragement :wink:

I can’t say that I yet understand what open drain means, as a term or for this project. The reason I picked this pin is simple that the GPIO header of the zero is (to the best of my knowledge) pin-compatible to the raspberry pi zero and I have followed this guide where they connect the enc28j60s INT pin to pin 22 (what is GPIO25 on a raspi).

Yes, I learned that from one of Thomas Petazzoni’s talks, however, I can’t say that it helped me a lot so far. I’m not seeing anything that I wouldn’t have previously defined myself in the dts.

And as I said, astonishingly, there are no other devices that make use of SPI, which means that looking for examples of pinctrl-0 on other device tree nodes isn’t giving me anything useful.

Coming back to the pins then. What I found in meson-g12-common.dtsi regarding spi pinctrl is the following:

spicc0_x_pins: spicc0-x {
        mux {
                groups = "spi0_mosi_x",
                       "spi0_miso_x",
                       "spi0_clk_x";
                function = "spi0";
                drive-strength-microamp = <4000>;
                bias-disable;
        };
};
spicc0_ss0_x_pins: spicc0-ss0-x {
        mux {
                groups = "spi0_ss0_x";
                function = "spi0";
                drive-strength-microamp = <4000>;
                bias-disable;
        };
};
spicc0_c_pins: spicc0-c {
        mux {
                groups = "spi0_mosi_c",
                       "spi0_miso_c",
                       "spi0_ss0_c",
                       "spi0_clk_c";
                function = "spi0";
                drive-strength-microamp = <4000>;
                bias-disable;
        };
};
spicc1_pins: spicc1 {
        mux {
                groups = "spi1_mosi",
                       "spi1_miso",
                       "spi1_clk";
                function = "spi1";
                drive-strength-microamp = <4000>;
        };
};
spicc1_ss0_pins: spicc1-ss0 {
        mux {
                groups = "spi1_ss0";
                function = "spi1";
                drive-strength-microamp = <4000>;
                bias-disable;
        };
};

I have not idea why there are x and c variants for SPI-A, but I can see that this differs a lot from what @RadxaYuntian linked to where we have the following:

enc28j60_pins: enc28j60_pins@0 {
    reg = <0>;
    fsl,pinmux-ids = <
            MX28_PAD_AUART0_RTS__GPIO_3_3    /* Interrupt */
    >;
    fsl,drive-strength = <MXS_DRIVE_4mA>;
    fsl,voltage = <MXS_VOLTAGE_HIGH>;
    fsl,pull-up = <MXS_PULL_DISABLE>;
};

What bothers me in this is that the syntax is so much different. Not only are the properties different, but the i.MX28 version also uses the vendor as a prefix?

Actually, if we look at a slightly different part of the above, we can see that &spi2_pins_b (which is similar to &spicc1_pins) is specified at the spi level, not at the client device of spi level (so enc28j60). Which makes me think that what pinctrl-0 requires (and what heuristics proved) is not <&spicc1_pins &spicc1_ss0_pins>.

ssp2: ssp@80014000 {
    compatible = "fsl,imx28-spi";
    pinctrl-names = "default";
    pinctrl-0 = <&spi2_pins_b &spi2_sck_cfg>;

    enc28j60: ethernet@0 {
            compatible = "microchip,enc28j60";
            pinctrl-names = "default";
            pinctrl-0 = <&enc28j60_pins>;

In any case, I tried what you suggested but the result is still the same, enc28j60 spi1.0: chip not found.

Hi @kwisatz, I read over the links you shared and want to provide some thoughts.

I think of an open drain like a sink without a stopper - the water can just flow out. In the case of a SBC, this means the electricity will just dissipate without powering anything.

In your shoes, I would add the standard Zero spi overlay to my uEnv.txt and use the associated GPIOs. So from the RaspberryPi link you sent, I’d connect the following from the enc28j60 to the Zero:
CS to GPIOX_10 (SPI_A_SS0) pin 16
SI to GPIOX_8 (SPI_A_MOSI) pin 18
SCK to GPIOX_11 (SPI_A_SCLK) pin 13
SO to GPIOX_9 (SPI_A_MISO) pin 12

For the interrupt, let’s use: GPIOAO_3 (pin 7) or really any GPIO that is neither defined for SPI use nor an open drain.

The RPi link you provided indicates requiring its specific enc28j60 driver overlay as well, which looks like a solid basis to extrapolate your driver for the Zero.

I think the challenge here is to take existing code for different boards and transmute that into a functional driver on the Zero.

I’m in a similar position with my project as all the pieces are in front of me but the nuance of coding for this board is still too new to completely gel yet.

Again, hope this helps!

I can indeed try and connect the enc28j60 to SPI-A instead of B, but I don’t understand what you mean by

I would add the standard Zero spi overlay to my uEnv.txt

As @RadxaYuntian wrote,

That overlay only enables SPI as a communication protocol so if you have a user space program that need to manually communicate to a SPI device that’s what you need. You need to instead have a custom device tree overlay telling the kernel that there is actually a device attached to it and you don’t want to manually communicate it with SPI protocol.

So I’ll still have to find a working dt overlay configuration to make the chip work.

Hi @kwisatz, pardon my previous “long way round” reply. My primary points are:
a) use an alternate GPIO for the interrupt
b) focus on the spi pins for the R0 rather than the RPiZ

Whether you use SPI-A or -B shouldn’t matter.

My thinking was with the standard R0 spi overlay, you’ll know you have the communication necessary to then implement the card driver:

The idea being to breakout the larger problem into two smaller solutions :wink:

TLDR: switch the interrupt GPIO to another pin with your current solution and see if your enc28j60 overlay is then successful in recognizing and communicating with the card.

March 29th

No progress, the problems remain the same.
Without pinctrl-0, the error remains that the driver cannot find the chip:

[    3.977949] enc28j60 spi1.0: Ethernet driver 1.02 loaded
[    3.987722] enc28j60 spi1.0: chip not found
[    3.987786] enc28j60: probe of spi1.0 failed with error -5

With the pinctrl-0 set to what is outlined above, the error includes

[    3.965837] enc28j60 spi1.0: there is not valid maps for state default
[    3.982755] enc28j60 spi1.0: Ethernet driver 1.02 loaded
[    3.987920] enc28j60 spi1.0: chip not found
[    3.988003] enc28j60: probe of spi1.0 failed with error -5

Questions that I have that I don’t know how to find answers for:

  1. Looking at the dts for the allwinner platform, we can see that the pinctrl mapping is optional and only required if internal pull-up is used for the IRQ pin. What does that mean and how can I know whether the chosen pin requires internal pull-up?
  2. Is the pinctrl-0 definition required at all or is it optional and the chip should work without it?

Questions I have found answers to

  1. Q: Why can’t I find pinctrl definitions for the pins used in the mux-group for spicc1_pins? I can find them getting defined for rockchip or tegra, but not for amlogic, how come?
    A: The definitions of those pins are actually part of driver code, not the device tree and can be found in kernel/drivers/pinctrl/pinctrl-meson-g12a.c.

For other SoCs, these definitions can be part of the device tree, e.g. the mosi pin definition on the px30:

spi1_mosi: spi1-mosi {
    rockchip,pins =
        <3 RK_PB4 4 &pcfg_pull_up_4ma>;
};

RasPi

To make sure my wiring is correct, I connected the module to a raspberry pi 3B, et voilà:

root@raspberrypi:~# dmesg | grep enc28
[   11.817637] enc28j60 spi0.0: Ethernet driver 1.02 loaded
[   14.048971] enc28j60 spi0.0 eth1: link down
[   14.049020] enc28j60 spi0.0 eth1: multicast mode
[   14.049599] enc28j60 spi0.0 eth1: multicast mode
[   14.066658] enc28j60 spi0.0 eth1: multicast mode
[   14.067083] enc28j60 spi0.0 eth1: multicast mode
[   14.092572] enc28j60 spi0.0 eth1: multicast mode
[   14.092988] enc28j60 spi0.0 eth1: multicast mode
[  195.615691] enc28j60 spi0.0 eth1: link up - Half duplex
[  195.636032] enc28j60 spi0.0 eth1: multicast mode
[  195.637509] enc28j60 spi0.0 eth1: multicast mode
[  201.858352] enc28j60 spi0.0 eth1: multicast mode

pi@raspberrypi:~ $ ip ad
[…]
4: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether fe:74:ac:48:ce:95 brd ff:ff:ff:ff:ff:ff
    inet 10.24.1.105/24 brd 10.24.1.255 scope global dynamic noprefixroute eth1
       valid_lft 259732sec preferred_lft 227257sec
    inet6 fe80::1ab2:7e57:69a9:3e7d/64 scope link 
       valid_lft forever preferred_lft forever

So, the wiring (except perhaps for the interrupt pin) is correct, the module works.

End of day.

The dts overlay currently looks like this:

/dts-v1/;
/plugin/;

#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/gpio/meson-g12a-gpio.h>

&spicc1 {
	// spicc1 defined in meson-g12-common.dtsi

	// See meson-g12a-spi-spidev.dts
	pinctrl-0 = <&spicc1_pins &spicc1_ss0_pins>;
	pinctrl-names = "default";
	// handled by `param_spidev_spi_bus` and meson-fixup.scr-cmd for the spi-spidev overlay
	status = "okay";

	#address-cells = <1>;
	#size-cells = <0>;

	eth1: enc28j60@0 {
		compatible = "microchip,enc28j60";
		reg = <0>;	/* for spi, this defines the chipselect */
		/**
		 * gpio_int has interrupts-cells = <2>
         *
		 * How does this distinguish between e.g. GPIOAO_4 on the first chip and GPIOZ_4 on the second ?!
		 */
		interrupt-parent = <&gpio_intc>;
		interrupts = <GPIOX_8 IRQ_TYPE_EDGE_FALLING>;

		// causes "enc28j60 spi1.0: there is not valid maps for state default"
		pinctrl-0 = <&enc28j60_int_pin>;
		pinctrl-names = "default";

		spi-max-frequency = <12000000>;
		status = "okay";
		debug = <1>;
	};
};

// See meson-g12-common.dtsi
// I think what happens here is that we define this pin as the interrupt pin (something must
// tell the chip that we want this pin to receive interrupts) and then "interrupts" in &eth1
// actually uses it
&periphs_pinctrl {
	enc28j60_int_pin: enc28j60_int_pin@0 {
		amlogic,pins = <&gpio GPIOX_8>;
		amlogic,function = <0>; /* interrupt - not sure how to verify this is correct */
		amlogic,pull = <IRQ_TYPE_NONE>;
	};
};

// Not entirely sure this is correct, but I haven't found any other way to do this
// and this compiles
/ {
	__overrides__ {
		irq   = <&eth1>, "interrupts:0", <&enc28j60_int_pin>, "brcm,pins:0";
		speed = <&eth1>, "spi-max-frequency:0";
	};
};

Output is still

[    3.882872] enc28j60 spi1.0: there is not valid maps for state default
[    3.884022] enc28j60 spi1.0: Ethernet driver 1.02 loaded
[    3.896540] enc28j60 spi1.0: chip not found
[    3.896686] enc28j60: probe of spi1.0 failed with error -5

@RadxaYuntian if you could give this a quick glance and point out any mistakes obvious to you, that would be much appreciated. I really don’t know where to go from here.

1 Like

Without the device it is hard for me to test the overlay but I’d start with something like below:

/dts-v1/;
/plugin/;

/ {
    compatible = "radxa,zero", "amlogic,g12a";

    fragment@0 {
        target = <&spicc1>;
        __overlay__ {
            pinctrl-0 = <&spicc1_pins &spicc1_ss0_pins>;
            pinctrl-names = "default";
            #address-cells = <1>;
            #size-cells = <0>;
            status = "okay";
            enc28j60: spidev@0 {
                    compatible = "microchip,enc28j60";
                    reg = <0>;
                    interrupt-parent = <&gic>;
                    interrupts = <GIC_SPI 82 IRQ_TYPE_EDGE_FALLING>;
                    spi-max-frequency = <12000000>;
            };
        };
    };
};

spicc1 was using interrupt 81 so I put 82 here which seems unused, but that may not be needed so you can try without those 2 interrupt lines as well.

Thanks for the feedback @RadxaYuntian

  • How does one know to use &gic as interrupt-parent and not &gpio_intc ? Both are tagged as interrupt controller and it was my understanding that I’d pick the one that is closest (in the tree) to the device node I’m describing.
  • You mention interrupts 81 and 82. spicc1 actually uses 90 and since the value 90 is not anywhere within meson-g12a-gpio.h I am confused as to how interrupt lines map to GPIO pins? So even if I could put 91 in this case, I still don’t know which pin to connect the interrupt line to.
  • What do these numbers stand for and how does one learn which is which?
  • What would you be able to do/check if you had the hardware module?

Without defining the interrupt-parent or interrupts, the results stay the same:

[    3.843415] enc28j60 spi1.0: Ethernet driver 1.02 loaded
[    3.849474] enc28j60 spi1.0: chip not found
[    3.849597] enc28j60: probe of spi1.0 failed with error -5

Maybe what isn’t entirely clear here is that the enc28j60 module has an interrupt pin and from what I read elsewhere, the module won’t work without this pin being connected.
So, the interrupt-line must IMHO map to a GPIO pin. I do not currently see how this is done.

My hypothesis is thus that the correct interrupt-parent is actually the gpio_intc and not gic directly.

gpio_intc: interrupt-controller@f080 {
	compatible = "amlogic,meson-g12a-gpio-intc",
				"amlogic,meson-gpio-intc";
	reg = <0x0 0xf080 0x0 0x10>;
	interrupt-controller;
	#interrupt-cells = <2>;
	amlogic,channel-interrupts = <64 65 66 67 68 69 70 71>;
};

If this hypothesis is correct, I still need to figure out how to map physical GPIO pins to register indices within gpio_intc.

For reference, the raspberry-pi dts has this:

fragment@2 {
        target = <&spi0>;
        __overlay__ {
                /* needed to avoid dtc warning */
                #address-cells = <1>;
                #size-cells = <0>;

                status = "okay";

                eth1: enc28j60@0{
                        compatible = "microchip,enc28j60";
                        reg = <0>; /* CE0 */
                        pinctrl-names = "default";
                        pinctrl-0 = <&eth1_pins>;
                        interrupt-parent = <&gpio>;
                        interrupts = <25 0x2>; /* falling edge */
                        spi-max-frequency = <12000000>;
                        status = "okay";
                };
        };
};

fragment@3 {
        target = <&gpio>;
        __overlay__ {
                eth1_pins: eth1_pins {
                        brcm,pins = <25>;
                        brcm,function = <0>; /* in */
                        brcm,pull = <0>; /* none */
                };
        };
};

I also don’t know exactly how, but the value 25 here refers to their GPIO25 which is pin 22.
The same value is used below in the definition of the pins as well as above when specifying interrupts.

After reading the code again I think you are right.

IRQ number is different from GPIO number, but I can’t figure out how those numbers are calculated so I just put an unused one there. For example spicc0 can be mapped to different pins but the IRQ number is the same 81.

@kwisatz @RadxaYuntian did you ever figure this out?

We also need to use enc28j60 with Radxa Zero so just wondered :slight_smile:

Hi @shawaj,

In a nutshell: No

But let me post a quick summary of what is new since my last update. You can go from there and together we might get to a solution quicker.

I’ll split this in two:

  1. SPI communication
  2. The IRQ pin and pinmuxing/pinctrl

SPI communication

We spent over 6 hours on this using an oscilloscope to analyse what is happening on the wires between the radxa zero and the module, but also between the module and a raspberry pi where this works flawlessly.
The first thing I have to say is that the signals from the radxa zero are very noisy. It’s a little better using a power connector to USB-C rather than feeding it from a notebook, but it’s still very noisy. Especially looking at the clock signal, it looks more like a mountain range than a skyline.

The first thing we did was to reduce the SPI frequency. Although we can see a max-spi-frequency value of 12000000 being used for the raspberry pi and other SoCs, this will not work with the radxa. Setting it to 10000000 or lower was necessary to make any communication with the module work.

The initial problem we encountered was that the module never responded to anything from the radxa. What the enc28j60 driver does, is to poll the module’s revision ID via the SPI bus. If there is no answer, then we get chip not found as seen above. Reading the specs of the enc28j60 chip, we tried several things and what finally led to us getting the driver to recognize the module was to add a pull-up on the MISO connection.

Now, this may also be possible using a pinctrl section, specifying that the pin in question (here pin 21) needs a pull-up. In our case, we did it using a 1 kOhm resistor on the MISO connection. But since knowing how to define pinctrl in general is a problem with the meson chip (e.g. I wouldn’t know by which value to refer to it in the dts), we have not yet attempted to do this.

The IRQ pin

Using guesswork and this commit from Xingyu Chen on the LKML we finally came up with the following configuration for the interrupt pin:

interrupt-parent = <&gpio_intc>;
interrupts = <85 IRQ_TYPE_EDGE_FALLING>;

Here, the value 85 is derived from the following statement from that commit:

The Meson-G12A SoC uses the same GPIO interrupt controller IP block as the
other Meson SoCs, A totle of 100 pins can be spied on, which is the sum of:

  • 223:100 undefined (no interrupt)
    […]
  • 96:77 20 pins on bank GPIOX
    […]

and thus 77 + 8 (GPIOX_8) = 85

Having this, we can at least see some interrupts in cat /proc/interrupts for the driver by applying external stimulus to the pin (but not from the module itself it would appear).

 38:          0          0          0          0  meson-gpio-irqchip  85 Edge      enc28j60

Note that the interrupt pin is not required for the driver to detect the chip, but it is required in order to get an operational interface.

Where we’re currently stuck is getting the interface up. There are two symptoms that we can see (other than the interface simply not getting an IP address from e.g. a DHCP server):

  1. The devicetree code is still complaining about an invalid map for state default: enc28j60 spi1.0: there is not valid maps for state default
  2. The interface is and stays in state link down, no matter what.
[    3.867504] enc28j60 spi1.0: there is not valid maps for state default
[    3.869939] enc28j60 spi1.0: Ethernet driver 1.02 loaded
[    6.248128] enc28j60 spi1.0 eth0: link down
[    6.248819] enc28j60 spi1.0 eth0: multicast mode
[    6.257138] enc28j60 spi1.0 eth0: multicast mode

As for the invalid map, the pinctrl configuration that I have is still the following:

&periphs_pinctrl {
	enc28j60_int_pin: enc28j60_int_pin@0 {
		amlogic,pins = <85>;	/* or is it <&gpio 85> ? */
		amlogic,function = "irq"; /* in - whether "out" (1) or "in" (0) */
		// amlogic,function = <0>; /* in - whether "out" (1) or "in" (0) */
		amlogic,pull = <IRQ_TYPE_NONE>;
	};
};

As you can see, I experimented with using "irq" as the function and providing one or two cells for pins since I’ve seen similar things being done. However, neither combination results in a working network interface. And I have found no information (and no examples of other SPI peripherals being used with the meson g12a) that would specify how many cells are expected here.

Another person had a similar issue on the tegra/jetson platform using the same chip. They have added debugging information to the driver to get more insight into what is failing and that might be what we’re going to try next.

Others with a similar experience/problem:

I was able to get some more information from the driver by raising its debug level. But the output (0xfe) leads me to believe that what we’re seeing is not actually the module but simply the effect of the pull-up on the MISO connection:

[ 3221.882849] enc28j60 spi1.0: there is not valid maps for state default
[ 3221.883836] enc28j60 spi1.0: Ethernet driver 1.02 loaded
[ 3221.889042] enc28j60 spi1.0: enc28j60_hw_init() - HalfDuplex
[ 3221.891781] enc28j60 spi1.0: chip RevID: 0xfe
[ 3221.896839] enc28j60 spi1.0:  Hw initialized.
               HwRevID: 0xfe
               Cntrl: ECON1 ECON2 ESTAT  EIR  EIE
                      0xfe  0xfe  0xfe  0xfe  0xfe
               MAC  : MACON1 MACON3 MACON4
                      0x00   0x00   0x00
               Rx   : ERXST  ERXND  ERXWRPT ERXRDPT ERXFCON EPKTCNT MAMXFL
                      0xfefe 0xfefe 0xfefe  0xfefe  0xfe    0xfe    0x0000
               Tx   : ETXST  ETXND  MACLCON1 MACLCON2 MAPHSUP
                      0xfefe 0xfefe 0x00     0x00     0x00
[ 3221.896855] enc28j60 spi1.0: eth%d: Setting MAC address to 2e:c0:49:97:57:a0
[ 3221.987152] enc28j60 spi1.0: enc28j60_hw_init() - HalfDuplex
[ 3221.989799] enc28j60 spi1.0: chip RevID: 0xfe
[ 3221.994188] enc28j60 spi1.0:  Hw initialized.
               HwRevID: 0xfe
               Cntrl: ECON1 ECON2 ESTAT  EIR  EIE
                      0xfe  0xfe  0xfe  0xfe  0xfe
               MAC  : MACON1 MACON3 MACON4
                      0x00   0x00   0x00
               Rx   : ERXST  ERXND  ERXWRPT ERXRDPT ERXFCON EPKTCNT MAMXFL
                      0xfefe 0xfefe 0xfefe  0xfefe  0xfe    0xfe    0x0000
               Tx   : ETXST  ETXND  MACLCON1 MACLCON2 MAPHSUP
                      0xfefe 0xfefe 0x00     0x00     0x00
[ 3221.994205] enc28j60 spi1.0: eth0: Setting MAC address to 2e:c0:49:97:57:a0
[ 3221.995895] enc28j60 spi1.0: enc28j60_hw_enable() enabling interrupts.
[ 3221.997191] enc28j60 spi1.0: enc28j60_check_link_status() PHSTAT1: 0000, PHSTAT2: 0000
[ 3221.997202] enc28j60 spi1.0 eth0: link down
[ 3221.999727] enc28j60 spi1.0 eth0: multicast mode

After further looking into this with people more skilled in this than I am, we have found further issues with the SPI implementation on the radxa zero. (We have actually come to question why this has not been a subject much earlier. Is nobody using SPI with the meson g12a?).

The two problems we have identified are:

The pins used for SPI on the radxa zero are electrically floating and must have a pull-down applied for the communication to make any sense in the first place. This can be seen nicely in this first screenshot on the left. Here, no pull-down has been applied to the MISO pin and the signal is thus interpreted as 1 for most of the time, unless a signal is applied on either the SCK or MOSI pins, in which case they influence the signal on the MISO pin:

We finally applied a 10kOhm resistor to the MISO pin to pull it down to 0V. Again, maybe it is possible to define this pull-down in the device tree overlay but we wouldn’t know how to.

The second issue is that the CS isn’t correctly implemented on the g12a. As Neil Armstrong, the developer of the SPI driver for meson (spi-meson-spicc.c) put it (in the last paragraph):

/*
 * The Meson SPICC controller could support DMA based transfers, but is not
 * implemented by the vendor code, and while having the registers documentation
 * it has never worked on the GXL Hardware.
 * The PIO mode is the only mode implemented, and due to badly designed HW :
 * - all transfers are cutted in 16 words burst because the FIFO hangs on
 *   TX underflow, and there is no TX "Half-Empty" interrupt, so we go by
 *   FIFO max size chunk only
 * - CS management is dumb, and goes UP between every burst, so is really a
 *   "Data Valid" signal than a Chip Select, GPIO link should be used instead
 *   to have a CS go down over the full transfer
 */

This can clearly be seen on this second screenshot:

Radxa on the left, Raspberry Pi 3B on the right.
One can clearly see that the CS only goes to 0 when a a signal is applied to SCK. The enc28j60 thus does not interpret the message as a 16 bit message but as two separate 8-bit messages. We can see (again, on the left) that the enc28j60 does, for that reason, not reply with its revision id (as it does on the right, 0x06)

In summary, I think we’ll have to say that for us, the SPI implementation of the Radxa is too broken to be useful. On top of that pinctrl seems to have been implemented in a way that is fundamentally different from other platforms and it’s not sufficiently transparent to us how to tweak it to make it work for us. We’ll probably try to revert to e.g. the Raspberry Pi platform which may be less available, but better supported.

@RadxaYuntian
@linuxlion

Seeing this image:

We see on the left right side the last packet of the Raspberry in the initialization sequence, asking for the revision ID. The first 8 clock bytes are the OP code asking for Rev ID (obviously), the next 8 clock bytes are for the enc28j60 to reply on the MISO channel (you can see the reply).

On the left side is the Radxa, which interrupts the CS between the two “clock bytes”.
Because the Radxa interrupts CS after the OP code, enc28j60 will not reply.
Here is a screenshot of the datasheet how a communication like this MUST happen:
54

Thus, the CS pin of the SPI controller of the Radxa Zero is unuseable (at least for this use-case).

It is necessary to implement the CS pin via GPIO.
The SPI linux kernel implementation looks like it is possible to do this via Device Tree (cs-gpios) like the example here:

Can we have an example, how to change the CS Pin to be done via cs-gpios instead of using the CS pin of the Radxa SPI controller?

Thanks for your investigation. In that case you can try using spi-gpio binding and define SPI entirely with GPIO. You can use this dtbo as a template and define GPIO in the same style specified in that file.