diff --git a/README.md b/README.md index bc4af87..d8bcdb6 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,23 @@ it), Ethernet via DHCP, and `172.32.0.93` over the USB-C gadget. The card is handed over read-only so `systemd-fsck-root` gets to run, then remounted `rw` from `/etc/fstab`; the partition and filesystem grow to fill the card. +The gadget is two functions on the one cable. `ssh root@172.32.0.93` over NCM, +and a second login prompt on the ACM port, which the host sees as +`/dev/ttyACM0`: + +```bash +tio /dev/ttyACM0 # or: screen /dev/ttyACM0, picocom /dev/ttyACM0 +``` + +The line settings do not matter: CDC ACM carries baud and parity as metadata +and nothing on the board acts on them. `/dev/ttyACM0` is whichever board +enumerated first, so with more than one plugged in, address them by the serial +the gadget reports, which is the SoC's own and does not change: + +```bash +tio /dev/serial/by-id/usb-Luckfox_Pico_Max_556abe2b7497589c-if02 +``` + Debian's desktop-sized housekeeping is masked, not deleted: `apt-daily`, `apt-daily-upgrade`, `e2scrub`, `fstrim` and `dpkg-db-backup` do not run on their own. `systemctl unmask` whichever you want back. @@ -198,15 +215,17 @@ Upstreams are pinned to exact commits in `scripts/lib.sh`. Nothing floats. Kernel boots cleanly, userspace starts green, NPU inference works right away. +The Ethernet MAC is `02:00:xx:xx:xx:xx`, derived from the SoC's OTP id, so it +is the same address on every boot and the same board keeps its DHCP lease. The +Type-C port is a peripheral: `luckfox-usb-gadget.service` binds NCM + ACM to it +and the board answers on `172.32.0.93`. Both of those took a devicetree change +and a kernel patch; `&gmac` and `&usbdrd_dwc3` in the DTS say why. + Wrong on a booted board, and not fixed yet: -- the Ethernet MAC is random on every boot (`rk_vendor_read eth mac address - failed`), so the board takes a new DHCP lease each time. -- the USB gadget never comes up: `phy ... illegal mode`, then `no UDC - available; is the controller in peripheral mode?`. The USB-C row in the - table above is what the hardware and the units are for, not something that - works today. - `Kernel memory protection not selected`. +- the Type-C port is peripheral-only. Host mode is a one-word DTS change, but + the port is also the power inlet, so it is the wrong default. ## Credits diff --git a/board/luckfox-pico-max/kernel/dts/rv1106g3-luckfox-pico-max.dts b/board/luckfox-pico-max/kernel/dts/rv1106g3-luckfox-pico-max.dts index 80ce326..9502312 100644 --- a/board/luckfox-pico-max/kernel/dts/rv1106g3-luckfox-pico-max.dts +++ b/board/luckfox-pico-max/kernel/dts/rv1106g3-luckfox-pico-max.dts @@ -163,8 +163,24 @@ /* ------------------------------------------------------------- ethernet --- */ -/* 100M, integrated PHY. MAC address comes from the OTP via nvmem. */ +/* + * 100M, integrated PHY. + * + * There is no MAC address anywhere on this board to read. dwmac-rk keeps one + * in Rockchip vendor storage, a reserved area at the end of the boot medium + * that the factory programs; nothing ever programmed this one, and an SD card + * written by mk-image.sh has no such area at all. The driver's answer to that + * was eth_random_addr(), so the board took a new MAC -- and a new DHCP lease + * -- on every boot. + * + * Name the OTP's 16-byte unique id, the same cell rockchip-cpuinfo hashes into + * /proc/cpuinfo's Serial, and the patched driver derives a locally administered + * address from it: stable for the life of the chip, distinct between chips. + * kernel/patches/0003 has the details. + */ &gmac { + nvmem-cells = <&otp_id>; + nvmem-cell-names = "soc-id"; status = "okay"; }; @@ -183,8 +199,27 @@ status = "okay"; }; +/* + * The Type-C port is the board's only USB port and its power inlet, and the + * connector has no ID pin -- there is no CC controller behind it either, just + * a socket wired to the SoC's one USB 2.0 OTG controller. + * + * dr_mode = "otg" therefore does not mean what it looks like it means. With no + * extcon to consult, dwc3 falls back to the OTG block's CONIDSTS bit, which + * follows the PHY's iddig input, which nothing on this board drives; it reads + * as grounded, dwc3 concludes it is the A-device and brings up xHCI. The port + * then has no VBUS to offer a downstream device and no UDC for a gadget to + * bind to, so it does nothing in either direction -- and usb_add_hcd() offers + * the PHY PHY_MODE_USB_HOST_SS on the way past, which + * rockchip_usb2phy_set_mode() does not handle, hence "illegal mode" in dmesg. + * + * Peripheral is what the hardware is, and is what Luckfox's own devicetree + * says. It gives the gadget in luckfox-usb-gadget.service a UDC to bind to. + * Set this to "host" instead if you power the board through the pin header and + * want to plug devices into the Type-C. + */ &usbdrd_dwc3 { - dr_mode = "otg"; + dr_mode = "peripheral"; status = "okay"; }; diff --git a/board/luckfox-pico-max/kernel/patches/0003-dwmac-rk-derive-a-stable-mac-address-from-the-soc-id.patch b/board/luckfox-pico-max/kernel/patches/0003-dwmac-rk-derive-a-stable-mac-address-from-the-soc-id.patch new file mode 100644 index 0000000..d19055a --- /dev/null +++ b/board/luckfox-pico-max/kernel/patches/0003-dwmac-rk-derive-a-stable-mac-address-from-the-soc-id.patch @@ -0,0 +1,110 @@ +From: luckfox-linux +Subject: [PATCH] net: stmmac: dwmac-rk: derive a stable MAC address from the SoC id + +rk_get_eth_addr() keeps the board's MAC address in Rockchip vendor +storage, a small reserved area at the end of the boot medium that the +factory programs and rk_vendor_storage.ko reads back. A board that was +never through Rockchip's provisioning, or that boots from a medium with +no vendor partition on it, has nothing there: + + rk_gmac-dwmac ffa80000.ethernet: rk_get_eth_addr: rk_vendor_read eth mac address failed (-1) + rk_gmac-dwmac ffa80000.ethernet: rk_get_eth_addr: generate random eth mac address: be:69:2e:54:8a:23 + rk_gmac-dwmac ffa80000.ethernet: rk_get_eth_addr: rk_vendor_write eth mac address failed (-1) + +The fallback is eth_random_addr() followed by an attempt to write the +result back, and on such a board the write fails for the same reason the +read did. So the address is new on every boot: a fresh DHCP lease, a +fresh entry in the server's table, a different address for anything that +tracked the last one, and no way to reserve one. + +Every one of these SoCs does have something unique and permanent in it, +though -- the 16-byte id in OTP that rockchip-cpuinfo already reads to +publish /proc/cpuinfo's Serial. Take a MAC from that when the board asks +for it, by naming the OTP id cell "soc-id" in the ethernet node: + + &gmac { + nvmem-cells = <&otp_id>; + nvmem-cell-names = "soc-id"; + }; + +The id is neither a MAC nor an OUI, so it is hashed into the low four +bytes and given a locally administered, unicast prefix rather than being +copied in. The result is stable for the life of the chip, differs +between chips, and differs between the MACs on one chip. Boards with +working vendor storage are unaffected: it is still tried first, and +boards that do not name the cell still get the old random address. + +Signed-off-by: luckfox-linux +--- + drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c | 44 ++++++++++++++++++++++++++ + 1 file changed, 44 insertions(+) + +diff --git a/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c b/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c +index 4561737aa..0249998d2 100644 +--- a/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c ++++ b/drivers/net/ethernet/stmicro/stmmac/dwmac-rk.c +@@ -10,6 +10,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -3254,6 +3255,42 @@ int dwmac_rk_get_phy_interface(struct stmmac_priv *priv) + } + EXPORT_SYMBOL(dwmac_rk_get_phy_interface); + ++/* ++ * Derive a MAC address from the SoC's OTP unique id, for boards that have no ++ * vendor storage to keep one in. The id is not a MAC and is not an OUI, so ++ * hash it down to the low four bytes and put the result under a locally ++ * administered prefix; the address is then stable for the life of the chip ++ * and distinct between chips, which is all the caller wanted. ++ */ ++static int rk_soc_id_eth_addr(struct device *dev, int id, unsigned char *addr) ++{ ++ struct nvmem_cell *cell; ++ unsigned char *soc_id; ++ size_t len; ++ u32 hash; ++ ++ cell = nvmem_cell_get(dev, "soc-id"); ++ if (IS_ERR(cell)) ++ return PTR_ERR(cell); ++ ++ soc_id = nvmem_cell_read(cell, &len); ++ nvmem_cell_put(cell); ++ if (IS_ERR(soc_id)) ++ return PTR_ERR(soc_id); ++ ++ hash = crc32(0, soc_id, len); ++ kfree(soc_id); ++ ++ addr[0] = 0x02; /* unicast, locally administered */ ++ addr[1] = id; /* one address per MAC on the SoC */ ++ addr[2] = hash >> 24; ++ addr[3] = hash >> 16; ++ addr[4] = hash >> 8; ++ addr[5] = hash; ++ ++ return 0; ++} ++ + static void rk_get_eth_addr(void *priv, unsigned char *addr) + { + struct rk_priv_data *bsp_priv = priv; +@@ -3274,6 +3311,13 @@ static void rk_get_eth_addr(void *priv, unsigned char *addr) + !is_valid_ether_addr(ðaddr[id * ETH_ALEN])) { + dev_err(dev, "%s: rk_vendor_read eth mac address failed (%d)\n", + __func__, ret); ++ ++ if (!rk_soc_id_eth_addr(dev, id, addr)) { ++ dev_info(dev, "%s: mac address from soc id: %pM\n", ++ __func__, addr); ++ goto out; ++ } ++ + eth_random_addr(ðaddr[id * ETH_ALEN]); + memcpy(addr, ðaddr[id * ETH_ALEN], ETH_ALEN); + dev_err(dev, "%s: generate random eth mac address: %pM\n", __func__, addr); diff --git a/board/luckfox-pico-max/kernel/patches/0004-inno-usb2-do-not-demand-a-parent-interrupt.patch b/board/luckfox-pico-max/kernel/patches/0004-inno-usb2-do-not-demand-a-parent-interrupt.patch new file mode 100644 index 0000000..a59de2f --- /dev/null +++ b/board/luckfox-pico-max/kernel/patches/0004-inno-usb2-do-not-demand-a-parent-interrupt.patch @@ -0,0 +1,43 @@ +From: luckfox-linux +Subject: [PATCH] phy: rockchip: inno-usb2: do not demand a parent interrupt + +The probe takes the muxed per-phy interrupt with platform_get_irq(), +which logs an error when the devicetree does not have one: + + rockchip-usb2phy ff3e0000.usb2-phy: error -ENXIO: IRQ index 0 not found + +Not having one is legitimate. SoCs that route every port's id, bvalid +and linestate events through a single line describe it on the usb2-phy +node; the rest, the RV1106 among them, give each port its own +interrupts, which rockchip_usb2phy_port_irq_init() picks up from the +port's own node. Both are correct, and the driver already treats the +muxed line as optional -- every use of rphy->irq is guarded by +`rphy->irq > 0`. + +Ask with platform_get_irq_optional(), which returns the same -ENXIO +without printing anything. + +Signed-off-by: luckfox-linux +--- + drivers/phy/rockchip/phy-rockchip-inno-usb2.c | 8 +++++++- + 1 file changed, 7 insertions(+), 1 deletion(-) + +diff --git a/drivers/phy/rockchip/phy-rockchip-inno-usb2.c b/drivers/phy/rockchip/phy-rockchip-inno-usb2.c +index 23c707be6..3dc434316 100644 +--- a/drivers/phy/rockchip/phy-rockchip-inno-usb2.c ++++ b/drivers/phy/rockchip/phy-rockchip-inno-usb2.c +@@ -2494,7 +2494,13 @@ static int rockchip_usb2phy_probe(struct platform_device *pdev) + rphy->chg_state = USB_CHG_STATE_UNDEFINED; + rphy->chg_type = POWER_SUPPLY_TYPE_UNKNOWN; + rphy->edev_self = false; +- rphy->irq = platform_get_irq(pdev, 0); ++ /* ++ * Optional: SoCs that mux every port's interrupts into one line put it ++ * here, the rest (RV1106 among them) describe them per port instead. ++ * Everything below tests rphy->irq before using it, so ask for it in a ++ * way that does not log an error when it is legitimately absent. ++ */ ++ rphy->irq = platform_get_irq_optional(pdev, 0); + platform_set_drvdata(pdev, rphy); + + if (!phy_cfgs) diff --git a/rootfs/overlay/usr/lib/systemd/system/luckfox-usb-gadget.service b/rootfs/overlay/usr/lib/systemd/system/luckfox-usb-gadget.service index b9ec200..dc880ea 100644 --- a/rootfs/overlay/usr/lib/systemd/system/luckfox-usb-gadget.service +++ b/rootfs/overlay/usr/lib/systemd/system/luckfox-usb-gadget.service @@ -3,6 +3,15 @@ Description=USB gadget (NCM network + ACM serial) on the Type-C port After=systemd-modules-load.service ConditionPathIsDirectory=/sys/kernel/config +# The ACM function on its own is a pipe with nothing on the far end: it gives +# the board a /dev/ttyGS0 and the host a /dev/ttyACM*, and a host that opens it +# sits looking at a blank screen. Put a login prompt there. Ordering matters -- +# serial-getty@.service is BindsTo=dev-ttyGS0.device, and the device node only +# exists once `up` has created the function, which is also why `down` takes the +# getty with it. +Wants=serial-getty@ttyGS0.service +Before=serial-getty@ttyGS0.service + [Service] Type=oneshot RemainAfterExit=yes diff --git a/rootfs/overlay/usr/local/sbin/luckfox-usb-gadget b/rootfs/overlay/usr/local/sbin/luckfox-usb-gadget index 11e7ff9..cc13444 100755 --- a/rootfs/overlay/usr/local/sbin/luckfox-usb-gadget +++ b/rootfs/overlay/usr/local/sbin/luckfox-usb-gadget @@ -9,10 +9,22 @@ set -eu G=/sys/kernel/config/usb_gadget/luckfox IFACE_ADDR=${USB_GADGET_ADDR:-172.32.0.93/24} -# Locally administered MACs, stable across reboots so the host does not invent -# a new interface name every time. -MAC_DEV=${USB_GADGET_MAC_DEV:-2a:66:6c:66:78:01} -MAC_HOST=${USB_GADGET_MAC_HOST:-2a:66:6c:66:78:02} + +# Everything identifying about this gadget hangs off the SoC's OTP unique id, +# which /proc/cpuinfo publishes as Serial. It has to be stable across reboots, +# or the host invents a new usb0 name and a new /dev/serial/by-id path every +# time, and it has to differ between boards, or two of these on one laptop are +# indistinguishable. /etc/machine-id is the fallback for a kernel that does not +# publish a serial. +soc_id=$(sed -n 's/^Serial[[:space:]]*:[[:space:]]*//p' /proc/cpuinfo | tr -dc '0-9a-f') +[ "${#soc_id}" -ge 8 ] || soc_id=$(tr -dc '0-9a-f' 2>/dev/null < /etc/machine-id || true) +[ "${#soc_id}" -ge 8 ] || soc_id=6c667801 +SERIAL=$(printf %s "$soc_id" | tail -c 16) + +# Locally administered, and the two ends differ in the second octet. +suffix=$(printf %s "$soc_id" | tail -c 8 | sed 's/../&:/g; s/:$//') +MAC_DEV=${USB_GADGET_MAC_DEV:-02:66:$suffix} +MAC_HOST=${USB_GADGET_MAC_HOST:-02:67:$suffix} udc_name() { ls /sys/class/udc 2>/dev/null | head -1; } @@ -31,11 +43,13 @@ up() { echo 0x0100 > "$G/bcdDevice" mkdir -p "$G/strings/0x409" - echo "Luckfox" > "$G/strings/0x409/manufacturer" - echo "Pico Max" > "$G/strings/0x409/product" - echo "$(cat /proc/sys/kernel/random/boot_id | tr -d - | cut -c1-16)" \ - > "$G/strings/0x409/serialnumber" + echo "Luckfox" > "$G/strings/0x409/manufacturer" + echo "Pico Max" > "$G/strings/0x409/product" + echo "$SERIAL" > "$G/strings/0x409/serialnumber" + # Link order below is interface order on the wire: NCM takes 0 and 1, so + # the ACM control interface is 2 and the host's stable name for the tty + # is /dev/serial/by-id/usb-Luckfox_Pico_Max_$SERIAL-if02. mkdir -p "$G/functions/ncm.usb0" echo "$MAC_DEV" > "$G/functions/ncm.usb0/dev_addr" echo "$MAC_HOST" > "$G/functions/ncm.usb0/host_addr"