diff --git a/board/aarch64/bananapi-bpi-r3/Config.in b/board/aarch64/bananapi-bpi-r3/Config.in
index c7cccf3fc..4801e6eff 100644
--- a/board/aarch64/bananapi-bpi-r3/Config.in
+++ b/board/aarch64/bananapi-bpi-r3/Config.in
@@ -5,6 +5,7 @@ config BR2_PACKAGE_BANANAPI_BPI_R3
select BR2_PACKAGE_LINUX_FIRMWARE
select BR2_PACKAGE_LINUX_FIRMWARE_MEDIATEK
select BR2_PACKAGE_LINUX_FIRMWARE_MEDIATEK_MT7986
+ select BR2_PACKAGE_LINUX_FIRMWARE_AIROHA_EN8811H
select SDCARD_AUX
help
Build Banana PI R3 support
diff --git a/board/aarch64/bananapi-bpi-r3/README.md b/board/aarch64/bananapi-bpi-r3/README.md
index 6d503b36e..fa1a95379 100644
--- a/board/aarch64/bananapi-bpi-r3/README.md
+++ b/board/aarch64/bananapi-bpi-r3/README.md
@@ -1,20 +1,29 @@
-# Banana Pi BPI-R3
+# Banana Pi BPI-R3 / BPI-R3 Mini
## Overview
-The Banana Pi R3 is a high-performance networking board with full Infix
-support for all enabled features including switched Ethernet ports, WiFi,
-and SFP interfaces.
+The Banana Pi BPI-R3 and BPI-R3 Mini are high-performance networking
+boards with full Infix support for all enabled features including
+Ethernet, WiFi, and SFP interfaces.
+
+Both boards share the same SoC and most peripherals. The key
+differences are:
+
+| Feature | BPI-R3 | BPI-R3 Mini |
+|-----------------|--------------------|-------------------------|
+| Ethernet switch | Yes (4x LAN + WAN) | No (WAN/LAN ports only) |
+| SD card slot | Yes | No |
### Hardware Features
- MediaTek MT7986 ARM Cortex-A53 quad-core processor @ 2.0 GHz
- 2 GB DDR4 RAM
-- 8 GB eMMC storage + microSD card slot
-- 5x Gigabit Ethernet ports (4x LAN, 1x WAN)
-- 2x SFP cages for fiber connectivity (1G/2.5G)
+- 8 GB eMMC storage
+- microSD card slot (BPI-R3 only)
+- 5x Gigabit Ethernet ports with switch core (BPI-R3 only)
+- 2x SFP cages for fiber connectivity (1G/2.5G) (BPI-R3 only)
- Dual-band WiFi (2.4 GHz + 5 GHz)
- USB 3.0 port
- Mini PCIe slot
@@ -23,16 +32,16 @@ and SFP interfaces.
Infix comes preconfigured with:
-- **LAN ports** (lan1-lan4): Bridged for internal networking
+- **LAN ports** (lan1-lan4): Bridged for internal networking (BPI-R3 only)
- **WAN port**: DHCP client enabled for internet connectivity
-- **SFP ports** (sfp1, sfp2): Available for configuration
+- **SFP ports** (sfp1, sfp2): Available for configuration (BPI-R3 only)
- **WiFi interfaces** (wifi0, wifi1): Available for configuration
## Getting Started
-### Quick Start with SD Card
+### BPI-R3: Quick Start with SD Card
-The easiest way to get started is using an SD card:
+The easiest way to get started with the BPI-R3 is using an SD card:
> [!NOTE]
> SD card boot works but we have observed stability issues. For production
@@ -49,6 +58,12 @@ The easiest way to get started is using an SD card:
- Connect to LAN port or console (115200 8N1)
- Default login: `admin` / `admin`
+### BPI-R3 Mini: Getting Started
+
+The BPI-R3 Mini does not have an SD card slot, so you must install
+directly to eMMC. See [Installing to eMMC — BPI-R3
+Mini](#bpi-r3-mini) below.
+
### Boot Switch Reference
The BPI-R3 has a 4-position DIP switch that controls boot media:
@@ -62,60 +77,44 @@ The BPI-R3 has a 4-position DIP switch that controls boot media:
| 1010 | SPI NAND | Boot from SPI NAND (advanced users) |
> [!NOTE]
-> Switch position is read from left to right: "0" = OFF, "1" = ON.
+> Switch position is read from left to right: "0" = OFF, "1" = ON.
> When the DIP switch is in the "UP" position it is OFF(0).
-## Advanced: Installing to eMMC
+## Installing to eMMC
For production deployments or better performance, you can install Infix
-to the internal eMMC storage. This is more complex but provides faster
-boot times and eliminates the external SD card.
+to the internal eMMC storage.
> [!IMPORTANT]
-> While Infix boots on both SD card and eMMC, we have observed stability
-> issues with SD cards on this platform. **eMMC is recommended** for
-> reliable operation.
-
-### Why Use eMMC?
-
-**Advantages:**
-
-- More reliable than SD card (stability issues observed with SD cards)
-- Faster boot and better performance
-- No external SD card to manage
-- More robust for industrial/embedded deployments
-
-**Disadvantages:**
-
-- More complex installation process
-- Requires intermediate NAND boot step
-- Harder to recover from errors
+> While Infix boots on both SD card and eMMC on the BPI-R3, we have
+> observed stability issues with SD cards on this platform. **eMMC is
+> recommended** for reliable operation.
### Prerequisites
- FTDI USB-to-serial cable (3.3V) for console access
-- microSD card with Infix (for initial boot)
- USB flash drive (FAT32 formatted)
+- microSD card with Infix, for initial boot (BPI-R3 only)
- Downloaded files (see below)
### Required Files
Download and place these files on a FAT32-formatted USB drive:
-1. **Intermediate NAND bootloader** (from Frank-W's U-Boot):
- - [bpi-r3_spim-nand_bl2.img][5] (BL2 loader)
- - [bpi-r3_spim-nand_fip.bin][6] (FIP image)
-2. **Infix eMMC image:**
+1. **Infix eMMC image:**
- [infix-bpi-r3-emmc.img][3] (Complete system image)
-3. **eMMC bootloader** (extracted from):
+2. **eMMC bootloader** (extracted from):
- [bpi-r3-emmc-boot-2025.01-latest.tar.gz][4]
- Extract `bl2.img` from the tarball to your USB drive
+3. **Intermediate NAND bootloader** (BPI-R3 only, from Frank-W's U-Boot):
+ - [bpi-r3_spim-nand_bl2.img][5] (BL2 loader)
+ - [bpi-r3_spim-nand_fip.bin][6] (FIP image)
> [!WARNING]
> The following process involves multiple boot mode changes. Take your
-> time verify each step carefully.
+> time and verify each step carefully.
-### Installation Steps
+### BPI-R3
#### Step 1: Boot from SD card
@@ -177,13 +176,48 @@ mmc partconf 0 1 1 0
Your BPI-R3 should now boot Infix from internal eMMC storage!
+### BPI-R3 Mini
+
+The BPI-R3 Mini does not have an SD card slot, so you have to go
+through the factory-installed Linux (OpenWRT) to flash the Infix
+bootloader and OS to eMMC.
+
+#### Step 1: Boot from NAND (factory default)
+
+
+
+1. Ensure boot switches are set to NAND mode (factory default)
+2. Place `infix-bpi-r3-emmc.img` and `bl2.img` on a USB drive
+3. Power on and boot into Linux
+4. Mount the USB drive to `/mnt`
+
+#### Step 2: Flash Infix to eMMC
+
+From the Linux shell:
+
+```
+echo 0 > /proc/sys/kernel/printk
+dd if=/mnt/infix-bpi-r3-emmc.img of=/dev/mmcblk0
+echo 0 > /sys/block/mmcblk0boot0/force_ro
+dd if=/mnt/bl2.img of=/dev/mmcblk0boot0
+sync
+```
+
+#### Step 3: Boot from eMMC
+
+
+
+1. Power off the board
+2. Set boot switches to eMMC mode (see image above)
+3. Power on
+
## Troubleshooting
### Board won't boot
- Verify boot switch positions (check twice!)
- Ensure power supply provides adequate current (12V/2A recommended)
-- Try booting from SD card with switches at **0000**
+- Try booting from SD card with switches at **0000** (BPI-R3 only)
### Can't break into U-Boot
@@ -195,9 +229,9 @@ Your BPI-R3 should now boot Infix from internal eMMC storage!
- Boot from NAND (**1010**) and verify eMMC image was written
- Check USB drive contents - ensure all files are present
-- Re-run Step 5 (eMMC boot configuration)
+- Re-run the eMMC boot configuration step
-### Reverting to SD card
+### Reverting to SD card (BPI-R3 only)
Simply set boot switches back to **0000** and boot from SD card. The
eMMC installation does not affect SD card functionality.
diff --git a/board/aarch64/bananapi-bpi-r3/bananapi-bpi-r3.mk b/board/aarch64/bananapi-bpi-r3/bananapi-bpi-r3.mk
index 93b20786c..14caf570c 100644
--- a/board/aarch64/bananapi-bpi-r3/bananapi-bpi-r3.mk
+++ b/board/aarch64/bananapi-bpi-r3/bananapi-bpi-r3.mk
@@ -9,6 +9,7 @@ define BANANAPI_BPI_R3_LINUX_CONFIG_FIXUPS
$(call KCONFIG_ENABLE_OPT,CONFIG_MEDIATEK_WATCHDOG)
$(call KCONFIG_ENABLE_OPT,CONFIG_MEDIATEK_GE_PHY)
$(call KCONFIG_ENABLE_OPT,CONFIG_REALTEK_PHY)
+ $(call KCONFIG_SET_OPT,CONFIG_AIR_EN8811H_PHY,m)
$(call KCONFIG_ENABLE_OPT,CONFIG_NET_VENDOR_MEDIATEK)
$(call KCONFIG_ENABLE_OPT,CONFIG_NET_MEDIATEK_SOC)
$(call KCONFIG_SET_OPT,CONFIG_NET_DSA_MT7530,m)
diff --git a/board/aarch64/bananapi-bpi-r3/dts/Makefile b/board/aarch64/bananapi-bpi-r3/dts/Makefile
index 974394efb..aa34a9795 100644
--- a/board/aarch64/bananapi-bpi-r3/dts/Makefile
+++ b/board/aarch64/bananapi-bpi-r3/dts/Makefile
@@ -1 +1 @@
-dtb-y += mediatek/mt7986a-bananapi-bpi-r3-sd.dtb mediatek/mt7986a-bananapi-bpi-r3-emmc.dtb
+dtb-y += mediatek/mt7986a-bananapi-bpi-r3-sd.dtb mediatek/mt7986a-bananapi-bpi-r3-emmc.dtb mediatek/mt7986a-bananapi-bpi-r3-mini.dtb
diff --git a/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3-emmc.dts b/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3-emmc.dts
index dafbe2451..573285c8c 100644
--- a/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3-emmc.dts
+++ b/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3-emmc.dts
@@ -1,2 +1,4 @@
+#include
+
#include "mt7986a-bananapi-bpi-r3.dtsi"
#include "mt7986a-bananapi-bpi-r3-emmc.dtsi"
diff --git a/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3-mini.dts b/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3-mini.dts
new file mode 100644
index 000000000..bcb323e6b
--- /dev/null
+++ b/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3-mini.dts
@@ -0,0 +1,12 @@
+#include
+
+#include "mt7986a-bananapi-bpi-r3.dtsi"
+#include "mt7986a-bananapi-bpi-r3-emmc.dtsi"
+
+&phy0 {
+ compatible = "ethernet-phy-id03a2.a411";
+};
+&phy1 {
+ compatible = "ethernet-phy-id03a2.a411";
+};
+
diff --git a/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3-mini.dtsi b/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3-mini.dtsi
new file mode 100644
index 000000000..dafbe2451
--- /dev/null
+++ b/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3-mini.dtsi
@@ -0,0 +1,2 @@
+#include "mt7986a-bananapi-bpi-r3.dtsi"
+#include "mt7986a-bananapi-bpi-r3-emmc.dtsi"
diff --git a/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3-sd.dts b/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3-sd.dts
index 725f7b4e2..640586fc7 100644
--- a/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3-sd.dts
+++ b/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3-sd.dts
@@ -1,2 +1,4 @@
+#include
+
#include "mt7986a-bananapi-bpi-r3.dtsi"
#include "mt7986a-bananapi-bpi-r3-sd.dtsi"
diff --git a/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3.dtsi b/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3.dtsi
index f4637d603..355afd163 100644
--- a/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3.dtsi
+++ b/board/aarch64/bananapi-bpi-r3/dts/mediatek/mt7986a-bananapi-bpi-r3.dtsi
@@ -1,5 +1,3 @@
-#include
-
/ {
chosen {
infix {
diff --git a/board/aarch64/bananapi-bpi-r3/genimage.cfg.in b/board/aarch64/bananapi-bpi-r3/genimage.cfg.in
index cbb345f69..f9fc6fccf 100644
--- a/board/aarch64/bananapi-bpi-r3/genimage.cfg.in
+++ b/board/aarch64/bananapi-bpi-r3/genimage.cfg.in
@@ -51,6 +51,11 @@ image #INFIX_ID##VERSION#-bpi-r3-#TARGET#.img {
size = 4096s
}
+ partition en8811h_fw {
+ size = 10M
+ image = "en8811h-fw.bin"
+ }
+
partition aux {
partition-uuid = D4EF35A0-0652-45A1-B3DE-D63339C82035
image = "aux.ext4"
diff --git a/board/aarch64/bananapi-bpi-r3/r3mini_emmcboot.png b/board/aarch64/bananapi-bpi-r3/r3mini_emmcboot.png
new file mode 100644
index 000000000..05a7a2c3f
Binary files /dev/null and b/board/aarch64/bananapi-bpi-r3/r3mini_emmcboot.png differ
diff --git a/board/aarch64/bananapi-bpi-r3/r3mini_nandboot2.png b/board/aarch64/bananapi-bpi-r3/r3mini_nandboot2.png
new file mode 100644
index 000000000..cbc5796a4
Binary files /dev/null and b/board/aarch64/bananapi-bpi-r3/r3mini_nandboot2.png differ
diff --git a/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3mini/etc/factory-config.cfg b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3mini/etc/factory-config.cfg
new file mode 100644
index 000000000..19068393f
--- /dev/null
+++ b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3mini/etc/factory-config.cfg
@@ -0,0 +1,445 @@
+{
+ "ieee802-dot1ab-lldp:lldp": {
+ "infix-lldp:enabled": true
+ },
+ "ietf-hardware:hardware": {
+ "component": [
+ {
+ "name": "USB",
+ "class": "infix-hardware:usb",
+ "state": {
+ "admin-state": "unlocked"
+ }
+ },
+ {
+ "class": "infix-hardware:usb",
+ "name": "USB2",
+ "state": {
+ "admin-state": "unlocked"
+ }
+ },
+ {
+ "name": "radio0",
+ "class": "infix-hardware:wifi",
+ "infix-hardware:wifi-radio": {
+ "country-code": "DE",
+ "band": "2.4GHz",
+ "channel": "auto"
+ }
+ },
+ {
+ "name": "radio1",
+ "class": "infix-hardware:wifi",
+ "infix-hardware:wifi-radio": {
+ "country-code": "DE",
+ "band": "5GHz",
+ "channel": "auto"
+ }
+ }
+ ]
+ },
+ "ietf-interfaces:interfaces": {
+ "interface": [
+ {
+ "name": "br0",
+ "type": "infix-if-type:bridge",
+ "ietf-ip:ipv4": {
+ "address": [
+ {
+ "ip": "192.168.0.1",
+ "prefix-length": 24
+ }
+ ]
+ }
+ },
+ {
+ "name": "lan",
+ "type": "infix-if-type:ethernet",
+ "ietf-ip:ipv6": {},
+ "infix-interfaces:bridge-port": {
+ "bridge": "br0"
+ }
+ },
+ {
+ "name": "lo",
+ "type": "infix-if-type:loopback",
+ "ietf-ip:ipv4": {
+ "address": [
+ {
+ "ip": "127.0.0.1",
+ "prefix-length": 8
+ }
+ ]
+ },
+ "ietf-ip:ipv6": {
+ "address": [
+ {
+ "ip": "::1",
+ "prefix-length": 128
+ }
+ ]
+ }
+ },
+ {
+ "name": "wan",
+ "type": "infix-if-type:ethernet",
+ "ietf-ip:ipv4": {
+ "infix-dhcp-client:dhcp": {
+ "option": [
+ {
+ "id": "ntp-server"
+ },
+ {
+ "id": "broadcast"
+ },
+ {
+ "id": "domain"
+ },
+ {
+ "id": "hostname"
+ },
+ {
+ "id": "dns-server"
+ },
+ {
+ "id": "router"
+ },
+ {
+ "id": "netmask"
+ },
+ {
+ "id": "vendor-class",
+ "value": "Banana Pi BPI-R3"
+ }
+ ]
+ }
+ },
+ "ietf-ip:ipv6": {
+ "infix-dhcpv6-client:dhcp": {
+ "option": [
+ {
+ "id": "ntp-server"
+ },
+ {
+ "id": "client-fqdn"
+ },
+ {
+ "id": "domain-search"
+ },
+ {
+ "id": "dns-server"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "name": "wifi0-ap",
+ "type": "infix-if-type:wifi",
+ "infix-interfaces:wifi": {
+ "radio": "radio0",
+ "access-point": {
+ "ssid": "Infix",
+ "security": {
+ "secret": "wifi"
+ }
+ }
+ },
+ "infix-interfaces:bridge-port": {
+ "bridge": "br0"
+ }
+ },
+ {
+ "name": "wifi1-ap",
+ "type": "infix-if-type:wifi",
+ "infix-interfaces:wifi": {
+ "radio": "radio1",
+ "access-point": {
+ "ssid": "Infix5Ghz",
+ "security": {
+ "secret": "wifi"
+ }
+ }
+ },
+ "infix-interfaces:bridge-port": {
+ "bridge": "br0"
+ }
+ }
+ ]
+ },
+ "ietf-keystore:keystore": {
+ "asymmetric-keys": {
+ "asymmetric-key": [
+ {
+ "name": "genkey",
+ "public-key-format": "infix-crypto-types:ssh-public-key-format",
+ "public-key": "",
+ "private-key-format": "infix-crypto-types:rsa-private-key-format",
+ "cleartext-private-key": "",
+ "certificates": {}
+ }
+ ]
+ },
+ "symmetric-keys": {
+ "symmetric-key": [
+ {
+ "name": "wifi",
+ "cleartext-symmetric-key": "aW5maXhpbmZpeA==",
+ "key-format": "infix-crypto-types:passphrase-key-format"
+ }
+ ]
+ }
+ },
+ "ietf-netconf-acm:nacm": {
+ "enable-nacm": true,
+ "read-default": "permit",
+ "write-default": "permit",
+ "exec-default": "permit",
+ "groups": {
+ "group": [
+ {
+ "name": "admin",
+ "user-name": [
+ "admin"
+ ]
+ },
+ {
+ "name": "operator",
+ "user-name": []
+ },
+ {
+ "name": "guest",
+ "user-name": []
+ }
+ ]
+ },
+ "rule-list": [
+ {
+ "name": "admin-acl",
+ "group": [
+ "admin"
+ ],
+ "rule": [
+ {
+ "name": "permit-all",
+ "module-name": "*",
+ "access-operations": "*",
+ "action": "permit",
+ "comment": "Allow 'admin' group complete access to all operations and data."
+ }
+ ]
+ },
+ {
+ "name": "operator-acl",
+ "group": [
+ "operator"
+ ],
+ "rule": [
+ {
+ "name": "permit-system-rpcs",
+ "module-name": "ietf-system",
+ "rpc-name": "*",
+ "access-operations": "exec",
+ "action": "permit",
+ "comment": "Operators can reboot, shutdown, and set system time."
+ }
+ ]
+ },
+ {
+ "name": "guest-acl",
+ "group": [
+ "guest"
+ ],
+ "rule": [
+ {
+ "name": "deny-all-write+exec",
+ "module-name": "*",
+ "access-operations": "create update delete exec",
+ "action": "deny",
+ "comment": "Guests cannot change anything or exec rpcs."
+ }
+ ]
+ },
+ {
+ "name": "default-deny-all",
+ "group": [
+ "*"
+ ],
+ "rule": [
+ {
+ "name": "deny-password-access",
+ "path": "/ietf-system:system/authentication/user/password",
+ "access-operations": "*",
+ "action": "deny",
+ "comment": "No user except admins can access password hashes."
+ },
+ {
+ "name": "deny-keystore-access",
+ "module-name": "ietf-keystore",
+ "access-operations": "*",
+ "action": "deny",
+ "comment": "No user except admins can access cryptographic keys."
+ },
+ {
+ "name": "deny-truststore-access",
+ "module-name": "ietf-truststore",
+ "access-operations": "*",
+ "action": "deny",
+ "comment": "No user except admins can access trust store."
+ }
+ ]
+ }
+ ]
+ },
+ "ietf-netconf-server:netconf-server": {
+ "listen": {
+ "endpoints": {
+ "endpoint": [
+ {
+ "name": "default-ssh",
+ "ssh": {
+ "tcp-server-parameters": {
+ "local-bind": [
+ {
+ "local-address": "::"
+ }
+ ]
+ },
+ "ssh-server-parameters": {
+ "server-identity": {
+ "host-key": [
+ {
+ "name": "default-key",
+ "public-key": {
+ "central-keystore-reference": "genkey"
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
+ "ietf-system:system": {
+ "hostname": "bpi-%m",
+ "ntp": {
+ "server": [
+ {
+ "name": "default",
+ "udp": {
+ "address": "pool.ntp.org"
+ }
+ }
+ ]
+ },
+ "authentication": {
+ "user": [
+ {
+ "name": "admin",
+ "password": "$factory$",
+ "infix-system:shell": "bash"
+ }
+ ]
+ },
+ "infix-system:motd-banner": "Li0tLS0tLS0uCnwgIC4gLiAgfCBJbmZpeCBPUyDigJQgSW1tdXRhYmxlLkZyaWVuZGx5LlNlY3VyZQp8LS4gdiAuLXwgaHR0cHM6Ly9rZXJuZWxraXQub3JnCictJy0tLSctJwo="
+ },
+ "infix-dhcp-server:dhcp-server": {
+ "option": [
+ {
+ "id": "ntp-server",
+ "address": "auto"
+ },
+ {
+ "id": "dns-server",
+ "address": "auto"
+ },
+ {
+ "id": "router",
+ "address": "auto"
+ }
+ ],
+ "subnet": [
+ {
+ "subnet": "192.168.0.0/24",
+ "pool": {
+ "start-address": "192.168.0.100",
+ "end-address": "192.168.0.250"
+ }
+ }
+ ]
+ },
+ "infix-firewall:firewall": {
+ "default": "wan",
+ "zone": [
+ {
+ "name": "lan",
+ "action": "accept",
+ "interface": [
+ "br0"
+ ]
+ },
+ {
+ "name": "wan",
+ "action": "drop",
+ "interface": [
+ "wan"
+ ],
+ "service": [
+ "dhcpv6-client"
+ ]
+ }
+ ],
+ "policy": [
+ {
+ "name": "lan-to-wan",
+ "action": "accept",
+ "ingress": [
+ "lan"
+ ],
+ "egress": [
+ "wan"
+ ],
+ "masquerade": true
+ }
+ ]
+ },
+ "infix-meta:meta": {
+ "version": "1.7"
+ },
+ "infix-services:mdns": {
+ "enabled": true
+ },
+ "infix-services:ssh": {
+ "enabled": true,
+ "hostkey": [
+ "genkey"
+ ],
+ "listen": [
+ {
+ "name": "ipv4",
+ "address": "0.0.0.0",
+ "port": 22
+ },
+ {
+ "name": "ipv6",
+ "address": "::",
+ "port": 22
+ }
+ ]
+ },
+ "infix-services:web": {
+ "enabled": true,
+ "console": {
+ "enabled": true
+ },
+ "netbrowse": {
+ "enabled": true
+ },
+ "restconf": {
+ "enabled": true
+ }
+ }
+}
diff --git a/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3mini/etc/udev/rules.d/90-bpi-r3-mini-rename-ethernet.rules b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3mini/etc/udev/rules.d/90-bpi-r3-mini-rename-ethernet.rules
new file mode 100644
index 000000000..23366a8e4
--- /dev/null
+++ b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3mini/etc/udev/rules.d/90-bpi-r3-mini-rename-ethernet.rules
@@ -0,0 +1,2 @@
+ACTION=="add", SUBSYSTEM=="net", DEVPATH=="/devices/platform/soc/15100000.ethernet/net/eth0", NAME="lan"
+ACTION=="add", SUBSYSTEM=="net", DEVPATH=="/devices/platform/soc/15100000.ethernet/net/eth1", NAME="wan"
diff --git a/board/aarch64/bananapi-bpi-r3/uboot/extras.config b/board/aarch64/bananapi-bpi-r3/uboot/extras.config
index 70fa5ab64..364c6c92b 100644
--- a/board/aarch64/bananapi-bpi-r3/uboot/extras.config
+++ b/board/aarch64/bananapi-bpi-r3/uboot/extras.config
@@ -3,6 +3,9 @@ CONFIG_BOOTDELAY=2
# CONFIG_MMC_PCI is not set
CONFIG_ENV_IS_NOWHERE=y
# CONFIG_ENV_IS_IN_MMC is not set
+CONFIG_MULTI_DTB_FIT=y
+CONFIG_OF_LIST="mt7986a-bpi-r3-sd mt7986a-bpi-r3-emmc mt7986a-bpi-r3-mini"
+
CONFIG_USB=y
CONFIG_USB_XHCI_HCD=y
CONFIG_USB_XHCI_MTK=y
@@ -26,8 +29,17 @@ CONFIG_MTD_SPI_NAND=y
CONFIG_MTK_SPIM=y
CONFIG_MTK_SNOR=y
+CONFIG_DM_MDIO=y
+CONFIG_DM_ETH_PHY=y
+CONFIG_PHY_ETHERNET_ID=y
+
+CONFIG_PHY_AIROHA=y
+CONFIG_PHY_AIROHA_EN8811H=y
+CONFIG_PHY_AIROHA_FW_IN_MMC=y
+
CONFIG_CMD_SF=y
CONFIG_CMD_USB=y
CONFIG_CMD_MTD=y
+CONFIG_CMD_MDIO=y
CONFIG_CMD_MTDPARTS=y
CONFIG_CMD_DM=y
diff --git a/board/aarch64/bananapi-bpi-r3/uboot/mt7986-env.dtsi b/board/aarch64/bananapi-bpi-r3/uboot/mt7986-env.dtsi
index 20a532f22..6964340f8 100644
--- a/board/aarch64/bananapi-bpi-r3/uboot/mt7986-env.dtsi
+++ b/board/aarch64/bananapi-bpi-r3/uboot/mt7986-env.dtsi
@@ -15,6 +15,10 @@
scriptaddr = "0x48000000";
ramdisk_addr_r = "0x4A000000";
+ en8811h_fw_part = "0#en8811h_fw";
+ en8811h_fw_dm_dir = "EthMD32.dm.bin";
+ en8811h_fw_dsp_dir = "EthMD32.DSP.bin";
+
/* This is a development platform, keep
* developer mode statically enabled.
*/
diff --git a/configs/bpi_r3_emmc_boot_defconfig b/configs/bpi_r3_emmc_boot_defconfig
index 184febd2c..387ed369e 100644
--- a/configs/bpi_r3_emmc_boot_defconfig
+++ b/configs/bpi_r3_emmc_boot_defconfig
@@ -11,6 +11,8 @@ BR2_SSP_NONE=y
BR2_INIT_NONE=y
BR2_SYSTEM_BIN_SH_NONE=y
# BR2_PACKAGE_BUSYBOX is not set
+BR2_PACKAGE_LINUX_FIRMWARE=y
+BR2_PACKAGE_LINUX_FIRMWARE_AIROHA_EN8811H=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
# BR2_TARGET_ROOTFS_TAR is not set
BR2_TARGET_ARM_TRUSTED_FIRMWARE=y
diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md
index e32f763fc..2d55e23a8 100644
--- a/doc/ChangeLog.md
+++ b/doc/ChangeLog.md
@@ -12,11 +12,16 @@ All notable changes to the project are documented in this file.
- Upgrade Buildroot to 2025.02.11 (LTS)
- Add support for Microchip SAMA7G54-EK Evaluation Kit, Arm Cortex-A7
- Add GPS/GNSS receiver support with NTP reference clock integration
+- Add support for [Banana Pi R3 Mini][BPI-R3-MINI], a 2 port router with 2 WiFi chip,
+ uses the same bootloader as BPI-R3 (eMMC-version)
### Fixes
N/A
+[BPI-R3-MINI]: https://wiki.banana-pi.org/Banana_Pi_BPI-R3_Mini
+
+
[v26.01.0][] - 2026-02-03
-------------------------
diff --git a/patches/uboot/2025.01/0001-hush-Remove-Ctrl-C-detection-in-loops.patch b/patches/uboot/2025.01/0001-hush-Remove-Ctrl-C-detection-in-loops.patch
index 0e1c08801..ce9970206 100644
--- a/patches/uboot/2025.01/0001-hush-Remove-Ctrl-C-detection-in-loops.patch
+++ b/patches/uboot/2025.01/0001-hush-Remove-Ctrl-C-detection-in-loops.patch
@@ -1,8 +1,11 @@
-From 989ef8436df2ee48d308981098c44b1b8257c23b Mon Sep 17 00:00:00 2001
+From 9a9885324c70d607c9c79814441dbc829e212e41 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz
Date: Mon, 10 Jun 2024 13:25:31 +0200
-Subject: [PATCH 8/8] hush: Remove Ctrl-C detection in loops
-Organization: Addiva Elektronik
+Subject: [PATCH 1/7] hush: Remove Ctrl-C detection in loops
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Organization: Wires
Assume that the original intent was to emulate SIGINT to a shell. This
only works as expected if the loop in question is the ouermost, and
@@ -14,15 +17,16 @@ Disable this behavior and delegate the problem of loop termination to
the writer of the script instead.
Signed-off-by: Tobias Waldekranz
+Signed-off-by: Mattias Walström
---
common/cli_hush.c | 7 -------
1 file changed, 7 deletions(-)
diff --git a/common/cli_hush.c b/common/cli_hush.c
-index 171069f5f4..d6d487893f 100644
+index a6a8edce1f4..0711e05a779 100644
--- a/common/cli_hush.c
+++ b/common/cli_hush.c
-@@ -1796,13 +1796,6 @@ static int run_list_real(struct pipe *pi)
+@@ -1792,13 +1792,6 @@ static int run_list_real(struct pipe *pi)
for (; pi; pi = (flag_restore != 0) ? rpipe : pi->next) {
if (pi->r_mode == RES_WHILE || pi->r_mode == RES_UNTIL ||
pi->r_mode == RES_FOR) {
@@ -37,5 +41,5 @@ index 171069f5f4..d6d487893f 100644
if (!rpipe) {
flag_rep = 0;
--
-2.34.1
+2.43.0
diff --git a/patches/uboot/2025.01/0002-add-rpidisplay-check.patch b/patches/uboot/2025.01/0002-cmd-new-command-rpidisplay.patch
similarity index 86%
rename from patches/uboot/2025.01/0002-add-rpidisplay-check.patch
rename to patches/uboot/2025.01/0002-cmd-new-command-rpidisplay.patch
index 5629d150f..9594f4433 100644
--- a/patches/uboot/2025.01/0002-add-rpidisplay-check.patch
+++ b/patches/uboot/2025.01/0002-cmd-new-command-rpidisplay.patch
@@ -1,3 +1,20 @@
+From 7efc3aa61fbc84b2c0c959e737257d2de2e8020c Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?=
+Date: Sat, 6 Sep 2025 22:18:27 +0200
+Subject: [PATCH 2/7] cmd: new command rpidisplay
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Organization: Wires
+
+Signed-off-by: Mattias Walström
+---
+ cmd/Kconfig | 14 ++++++++
+ cmd/Makefile | 2 +-
+ cmd/rpi_display.c | 81 +++++++++++++++++++++++++++++++++++++++++++++++
+ 3 files changed, 96 insertions(+), 1 deletion(-)
+ create mode 100644 cmd/rpi_display.c
+
diff --git a/cmd/Kconfig b/cmd/Kconfig
index 1d7ddb4ed36..8572dc4353e 100644
--- a/cmd/Kconfig
@@ -130,3 +147,6 @@ index 00000000000..07abdc08552
+ "Returns: 0=connected, 1=not detected, 2=no video\n"
+ "Usage: if testrpidisplay; then echo DSI found; fi"
+);
+--
+2.43.0
+
diff --git a/patches/uboot/2025.01/0003-arm-dts-at91-sama7g5ek-increase-clock-for-sdmmc-from.patch b/patches/uboot/2025.01/0003-arm-dts-at91-sama7g5ek-increase-clock-for-sdmmc-from.patch
new file mode 100644
index 000000000..0e96b9214
--- /dev/null
+++ b/patches/uboot/2025.01/0003-arm-dts-at91-sama7g5ek-increase-clock-for-sdmmc-from.patch
@@ -0,0 +1,48 @@
+From c646eef3ee304ae49ad7e3f998ed08bfa0efb7b9 Mon Sep 17 00:00:00 2001
+From: Mihai Sain
+Date: Fri, 5 May 2023 13:28:31 +0300
+Subject: [PATCH 3/7] arm: dts: at91: sama7g5ek: increase clock for sdmmc from
+ 25 MHz to 50 MHz
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Organization: Wires
+
+Current clock for sdmmc0 and sdmmc1 is 25 MHz because of the caps forced
+by the mainline kernel.
+
+Remove the caps in order to increase the clock to 50 MHz. This will
+improve the boot time when reading the kernel binary. Tested on
+sama7g5ek rev 5 using mmcinfo command.
+
+Signed-off-by: Mihai Sain
+Signed-off-by: Mattias Walström
+---
+ arch/arm/dts/at91-sama7g5ek.dts | 4 ----
+ 1 file changed, 4 deletions(-)
+
+diff --git a/arch/arm/dts/at91-sama7g5ek.dts b/arch/arm/dts/at91-sama7g5ek.dts
+index 9b247fcaf66..5bbbdd8ed76 100644
+--- a/arch/arm/dts/at91-sama7g5ek.dts
++++ b/arch/arm/dts/at91-sama7g5ek.dts
+@@ -776,8 +776,6 @@
+ &sdmmc0 {
+ bus-width = <8>;
+ non-removable;
+- no-1-8-v;
+- sdhci-caps-mask = <0x0 0x00200000>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pinctrl_sdmmc0_default>;
+ status = "okay";
+@@ -785,8 +783,6 @@
+
+ &sdmmc1 {
+ bus-width = <4>;
+- no-1-8-v;
+- sdhci-caps-mask = <0x0 0x00200000>;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pinctrl_sdmmc1_default>;
+ status = "okay";
+--
+2.43.0
+
diff --git a/patches/uboot/2025.01/0004-arm-dts-at91-sama7g5ek-supports-high-speed-on-mmc0-e.patch b/patches/uboot/2025.01/0004-arm-dts-at91-sama7g5ek-supports-high-speed-on-mmc0-e.patch
new file mode 100644
index 000000000..6bbef0563
--- /dev/null
+++ b/patches/uboot/2025.01/0004-arm-dts-at91-sama7g5ek-supports-high-speed-on-mmc0-e.patch
@@ -0,0 +1,37 @@
+From 5a530e4eebf64887fd6b449dd45df04eccd07cad Mon Sep 17 00:00:00 2001
+From: Joachim Wiberg
+Date: Thu, 12 Feb 2026 10:00:02 +0100
+Subject: [PATCH 4/7] arm: dts: at91: sama7g5ek supports high-speed on mmc0
+ (eMMC)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Organization: Wires
+
+- eMMC high-speed timing is supported
+- eMMC hardware reset is supported
+
+Tested on sama7g5ek rev 5.
+
+Signed-off-by: Joachim Wiberg
+Signed-off-by: Mattias Walström
+---
+ arch/arm/dts/at91-sama7g5ek.dts | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/arch/arm/dts/at91-sama7g5ek.dts b/arch/arm/dts/at91-sama7g5ek.dts
+index 5bbbdd8ed76..d2d16bd5f1f 100644
+--- a/arch/arm/dts/at91-sama7g5ek.dts
++++ b/arch/arm/dts/at91-sama7g5ek.dts
+@@ -776,6 +776,8 @@
+ &sdmmc0 {
+ bus-width = <8>;
+ non-removable;
++ cap-mmc-highspeed;
++ cap-mmc-hw-reset;
+ pinctrl-names = "default";
+ pinctrl-0 = <&pinctrl_sdmmc0_default>;
+ status = "okay";
+--
+2.43.0
+
diff --git a/patches/uboot/2025.01/0005-net-phy-Add-the-Airoha-EN8811H-PHY-driver.patch b/patches/uboot/2025.01/0005-net-phy-Add-the-Airoha-EN8811H-PHY-driver.patch
new file mode 100644
index 000000000..81e8eccc5
--- /dev/null
+++ b/patches/uboot/2025.01/0005-net-phy-Add-the-Airoha-EN8811H-PHY-driver.patch
@@ -0,0 +1,980 @@
+From be250151366460ee318ca47bb15ec23e5e322a84 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?=
+Date: Wed, 18 Feb 2026 17:00:09 +0100
+Subject: [PATCH 5/7] net: phy: Add the Airoha EN8811H PHY driver
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Organization: Wires
+
+Add the driver for the Airoha EN8811H 2.5 Gigabit PHY. The PHY supports
+100/1000/2500 Mbps with auto negotiation only.
+
+The driver uses two firmware files, for which updated versions are added to
+linux-firmware already.
+
+Locating the AIROHA FW within the filesystem at the designated partition
+and path will trigger its automatic loading and writing to the PHY via MDIO.
+If need board specific loading override,
+please override the en8811h_read_fw function on board or architecture level.
+
+Linux upstream AIROHA EN8811H driver commit:
+71e79430117d56c409c5ea485a263bc0d8083390
+
+Based on the Linux upstream AIROHA EN8811H driver code(air_en8811h.c),
+I have modified the relevant process to align with the U-Boot boot sequence.
+and have validated this on Banana Pi BPI-R3 Mini.
+
+Signed-off-by: Lucien.Jheng
+Signed-off-by: Mattias Walström
+---
+ drivers/net/phy/Kconfig | 25 +
+ drivers/net/phy/Makefile | 1 +
+ drivers/net/phy/air_en8811h.c | 887 ++++++++++++++++++++++++++++++++++
+ 3 files changed, 913 insertions(+)
+ create mode 100644 drivers/net/phy/air_en8811h.c
+
+diff --git a/drivers/net/phy/Kconfig b/drivers/net/phy/Kconfig
+index 13e73810ad6..4f7451bf0c1 100644
+--- a/drivers/net/phy/Kconfig
++++ b/drivers/net/phy/Kconfig
+@@ -83,6 +83,31 @@ config PHY_ADIN
+ help
+ Add support for configuring RGMII on Analog Devices ADIN PHYs.
+
++menuconfig PHY_AIROHA
++ bool "Airoha Ethernet PHYs support"
++
++config PHY_AIROHA_EN8811H
++ bool "Airoha Ethernet EN8811H support"
++ depends on PHY_AIROHA
++ help
++ AIROHA EN8811H supported.
++
++choice
++ prompt "Location of the Airoha PHY firmware"
++ default PHY_AIROHA_FW_IN_MMC
++ depends on PHY_AIROHA_EN8811H
++
++config PHY_AIROHA_FW_IN_MMC
++ bool "Airoha firmware in MMC partition"
++ help
++ Airoha PHYs use firmware which can be loaded automatically
++ from storage directly attached to the PHY, and then loaded
++ via MDIO commands by the boot loader. The firmware is loaded
++ from a file specified by the U-Boot environment variables
++ en8811h_fw_part, en8811h_fw_dm_dir, and en8811h_fw_dsp_dir.
++
++endchoice
++
+ menuconfig PHY_AQUANTIA
+ bool "Aquantia Ethernet PHYs support"
+ select PHY_GIGE
+diff --git a/drivers/net/phy/Makefile b/drivers/net/phy/Makefile
+index 2487f366e1c..87dee3c15b9 100644
+--- a/drivers/net/phy/Makefile
++++ b/drivers/net/phy/Makefile
+@@ -11,6 +11,7 @@ obj-$(CONFIG_MV88E6352_SWITCH) += mv88e6352.o
+ obj-$(CONFIG_PHYLIB) += phy.o
+ obj-$(CONFIG_PHYLIB_10G) += generic_10g.o
+ obj-$(CONFIG_PHY_ADIN) += adin.o
++obj-$(CONFIG_PHY_AIROHA_EN8811H) += air_en8811h.o
+ obj-$(CONFIG_PHY_AQUANTIA) += aquantia.o
+ obj-$(CONFIG_PHY_ATHEROS) += atheros.o
+ obj-$(CONFIG_PHY_BROADCOM) += broadcom.o
+diff --git a/drivers/net/phy/air_en8811h.c b/drivers/net/phy/air_en8811h.c
+new file mode 100644
+index 00000000000..d856d48b4a5
+--- /dev/null
++++ b/drivers/net/phy/air_en8811h.c
+@@ -0,0 +1,887 @@
++// SPDX-License-Identifier: GPL-2.0+
++/*
++ * Driver for the Airoha EN8811H 2.5 Gigabit PHY.
++ *
++ * Limitations of the EN8811H:
++ * - Only full duplex supported
++ * - Forced speed (AN off) is not supported by hardware (100Mbps)
++ *
++ * Source originated from linux air_en8811h.c
++ *
++ * Copyright (C) 2025 Airoha Technology Corp.
++ */
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++
++#define EN8811H_PHY_ID 0x03a2a411
++
++#define AIR_FW_ADDR_DM 0x00000000
++#define AIR_FW_ADDR_DSP 0x00100000
++
++#define EN8811H_MD32_DM_SIZE 0x4000
++#define EN8811H_MD32_DSP_SIZE 0x20000
++
++ #define EN8811H_FW_CTRL_1 0x0f0018
++ #define EN8811H_FW_CTRL_1_START 0x0
++ #define EN8811H_FW_CTRL_1_FINISH 0x1
++ #define EN8811H_FW_CTRL_2 0x800000
++ #define EN8811H_FW_CTRL_2_LOADING BIT(11)
++
++ /* MII Registers */
++ #define AIR_AUX_CTRL_STATUS 0x1d
++ #define AIR_AUX_CTRL_STATUS_SPEED_MASK GENMASK(4, 2)
++ #define AIR_AUX_CTRL_STATUS_SPEED_100 0x4
++ #define AIR_AUX_CTRL_STATUS_SPEED_1000 0x8
++ #define AIR_AUX_CTRL_STATUS_SPEED_2500 0xc
++
++#define AIR_EXT_PAGE_ACCESS 0x1f
++#define AIR_PHY_PAGE_STANDARD 0x0000
++#define AIR_PHY_PAGE_EXTENDED_4 0x0004
++
++/* MII Registers Page 4*/
++#define AIR_BPBUS_MODE 0x10
++#define AIR_BPBUS_MODE_ADDR_FIXED 0x0000
++#define AIR_BPBUS_MODE_ADDR_INCR BIT(15)
++#define AIR_BPBUS_WR_ADDR_HIGH 0x11
++#define AIR_BPBUS_WR_ADDR_LOW 0x12
++#define AIR_BPBUS_WR_DATA_HIGH 0x13
++#define AIR_BPBUS_WR_DATA_LOW 0x14
++#define AIR_BPBUS_RD_ADDR_HIGH 0x15
++#define AIR_BPBUS_RD_ADDR_LOW 0x16
++#define AIR_BPBUS_RD_DATA_HIGH 0x17
++#define AIR_BPBUS_RD_DATA_LOW 0x18
++
++/* Registers on MDIO_MMD_VEND1 */
++#define EN8811H_PHY_FW_STATUS 0x8009
++#define EN8811H_PHY_READY 0x02
++
++/* Registers on MDIO_MMD_VEND2 */
++#define AIR_PHY_LED_BCR 0x021
++#define AIR_PHY_LED_BCR_MODE_MASK GENMASK(1, 0)
++#define AIR_PHY_LED_BCR_TIME_TEST BIT(2)
++#define AIR_PHY_LED_BCR_CLK_EN BIT(3)
++#define AIR_PHY_LED_BCR_EXT_CTRL BIT(15)
++
++#define AIR_PHY_LED_DUR_ON 0x022
++
++#define AIR_PHY_LED_DUR_BLINK 0x023
++
++#define AIR_PHY_LED_ON(i) (0x024 + ((i) * 2))
++#define AIR_PHY_LED_ON_MASK (GENMASK(6, 0) | BIT(8))
++#define AIR_PHY_LED_ON_LINK1000 BIT(0)
++#define AIR_PHY_LED_ON_LINK100 BIT(1)
++#define AIR_PHY_LED_ON_LINK10 BIT(2)
++#define AIR_PHY_LED_ON_LINKDOWN BIT(3)
++#define AIR_PHY_LED_ON_FDX BIT(4) /* Full duplex */
++#define AIR_PHY_LED_ON_HDX BIT(5) /* Half duplex */
++#define AIR_PHY_LED_ON_FORCE_ON BIT(6)
++#define AIR_PHY_LED_ON_LINK2500 BIT(8)
++#define AIR_PHY_LED_ON_POLARITY BIT(14)
++#define AIR_PHY_LED_ON_ENABLE BIT(15)
++
++#define AIR_PHY_LED_BLINK(i) (0x025 + ((i) * 2))
++#define AIR_PHY_LED_BLINK_1000TX BIT(0)
++#define AIR_PHY_LED_BLINK_1000RX BIT(1)
++#define AIR_PHY_LED_BLINK_100TX BIT(2)
++#define AIR_PHY_LED_BLINK_100RX BIT(3)
++#define AIR_PHY_LED_BLINK_10TX BIT(4)
++#define AIR_PHY_LED_BLINK_10RX BIT(5)
++#define AIR_PHY_LED_BLINK_COLLISION BIT(6)
++#define AIR_PHY_LED_BLINK_RX_CRC_ERR BIT(7)
++#define AIR_PHY_LED_BLINK_RX_IDLE_ERR BIT(8)
++#define AIR_PHY_LED_BLINK_FORCE_BLINK BIT(9)
++#define AIR_PHY_LED_BLINK_2500TX BIT(10)
++#define AIR_PHY_LED_BLINK_2500RX BIT(11)
++
++#define EN8811H_FW_VERSION 0x3b3c
++
++#define EN8811H_POLARITY 0xca0f8
++#define EN8811H_POLARITY_TX_NORMAL BIT(0)
++#define EN8811H_POLARITY_RX_REVERSE BIT(1)
++
++#define EN8811H_CLK_CGM 0xcf958
++#define EN8811H_CLK_CGM_CKO BIT(26)
++#define EN8811H_HWTRAP1 0xcf914
++#define EN8811H_HWTRAP1_CKO BIT(12)
++
++#define air_upper_16_bits(n) ((u16)((n) >> 16))
++#define air_lower_16_bits(n) ((u16)((n) & 0xffff))
++#define clear_bit(bit, bitmap) __clear_bit(bit, bitmap)
++
++/* Led definitions */
++#define EN8811H_LED_COUNT 3
++
++struct led {
++ unsigned long rules;
++ unsigned long state;
++};
++
++enum {
++ AIR_PHY_LED_STATE_FORCE_ON,
++ AIR_PHY_LED_STATE_FORCE_BLINK,
++};
++
++enum {
++ AIR_PHY_LED_DUR_BLINK_32MS,
++ AIR_PHY_LED_DUR_BLINK_64MS,
++ AIR_PHY_LED_DUR_BLINK_128MS,
++ AIR_PHY_LED_DUR_BLINK_256MS,
++ AIR_PHY_LED_DUR_BLINK_512MS,
++ AIR_PHY_LED_DUR_BLINK_1024MS,
++};
++
++enum {
++ AIR_LED_DISABLE,
++ AIR_LED_ENABLE,
++};
++
++enum {
++ AIR_ACTIVE_LOW,
++ AIR_ACTIVE_HIGH,
++};
++
++enum {
++ AIR_LED_MODE_DISABLE,
++ AIR_LED_MODE_USER_DEFINE,
++};
++
++/* Trigger specific enum */
++enum air_led_trigger_netdev_modes {
++ AIR_TRIGGER_NETDEV_LINK = 0,
++ AIR_TRIGGER_NETDEV_LINK_10,
++ AIR_TRIGGER_NETDEV_LINK_100,
++ AIR_TRIGGER_NETDEV_LINK_1000,
++ AIR_TRIGGER_NETDEV_LINK_2500,
++ AIR_TRIGGER_NETDEV_LINK_5000,
++ AIR_TRIGGER_NETDEV_LINK_10000,
++ AIR_TRIGGER_NETDEV_HALF_DUPLEX,
++ AIR_TRIGGER_NETDEV_FULL_DUPLEX,
++ AIR_TRIGGER_NETDEV_TX,
++ AIR_TRIGGER_NETDEV_RX,
++ AIR_TRIGGER_NETDEV_TX_ERR,
++ AIR_TRIGGER_NETDEV_RX_ERR,
++
++ /* Keep last */
++ __AIR_TRIGGER_NETDEV_MAX,
++};
++
++/* Default LED setup:
++ * GPIO5 <-> LED0 On: Link detected, blink Rx/Tx
++ * GPIO4 <-> LED1 On: Link detected at 2500 and 1000 Mbps
++ * GPIO3 <-> LED2 On: Link detected at 2500 and 100 Mbps
++ */
++#define AIR_DEFAULT_TRIGGER_LED0 (BIT(AIR_TRIGGER_NETDEV_LINK) | \
++ BIT(AIR_TRIGGER_NETDEV_RX) | \
++ BIT(AIR_TRIGGER_NETDEV_TX))
++#define AIR_DEFAULT_TRIGGER_LED1 (BIT(AIR_TRIGGER_NETDEV_LINK_2500) | \
++ BIT(AIR_TRIGGER_NETDEV_LINK_1000))
++#define AIR_DEFAULT_TRIGGER_LED2 (BIT(AIR_TRIGGER_NETDEV_LINK_2500) | \
++ BIT(AIR_TRIGGER_NETDEV_LINK_100))
++
++#define AIR_PHY_LED_DUR_UNIT 781
++#define AIR_PHY_LED_DUR (AIR_PHY_LED_DUR_UNIT << AIR_PHY_LED_DUR_BLINK_64MS)
++
++struct en8811h_priv {
++ int firmware_version;
++ bool mcu_needs_restart;
++ struct led led[EN8811H_LED_COUNT];
++};
++
++static int air_phy_read_page(struct phy_device *phydev)
++{
++ return phy_read(phydev, MDIO_DEVAD_NONE, AIR_EXT_PAGE_ACCESS);
++}
++
++static int air_phy_write_page(struct phy_device *phydev, int page)
++{
++ return phy_write(phydev, MDIO_DEVAD_NONE, AIR_EXT_PAGE_ACCESS, page);
++}
++
++int air_phy_select_page(struct phy_device *phydev, int page)
++{
++ int ret, oldpage;
++
++ oldpage = air_phy_read_page(phydev);
++ if (oldpage < 0)
++ return oldpage;
++
++ if (oldpage != page) {
++ ret = air_phy_write_page(phydev, page);
++ if (ret < 0)
++ return ret;
++ }
++
++ return oldpage;
++}
++
++int air_phy_restore_page(struct phy_device *phydev, int oldpage, int ret)
++{
++ int r;
++
++ if (oldpage < 0)
++ return oldpage;
++
++ r = air_phy_write_page(phydev, oldpage);
++ if (ret >= 0 && r < 0)
++ ret = r;
++
++ return ret;
++}
++
++static int air_buckpbus_reg_write(struct phy_device *phydev,
++ u32 pbus_address, u32 pbus_data)
++{
++ int ret, saved_page;
++
++ saved_page = air_phy_select_page(phydev, AIR_PHY_PAGE_EXTENDED_4);
++ if (saved_page < 0)
++ return saved_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_MODE,
++ AIR_BPBUS_MODE_ADDR_FIXED);
++ if (ret < 0)
++ goto restore_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_WR_ADDR_HIGH,
++ air_upper_16_bits(pbus_address));
++ if (ret < 0)
++ goto restore_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_WR_ADDR_LOW,
++ air_lower_16_bits(pbus_address));
++ if (ret < 0)
++ goto restore_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_WR_DATA_HIGH,
++ air_upper_16_bits(pbus_data));
++ if (ret < 0)
++ goto restore_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_WR_DATA_LOW,
++ air_lower_16_bits(pbus_data));
++ if (ret < 0)
++ goto restore_page;
++
++restore_page:
++ if (ret < 0)
++ printf("%s 0x%08x failed: %d\n", __func__,
++ pbus_address, ret);
++
++ return air_phy_restore_page(phydev, saved_page, ret);
++}
++
++static int air_buckpbus_reg_read(struct phy_device *phydev,
++ u32 pbus_address, u32 *pbus_data)
++{
++ int pbus_data_low, pbus_data_high;
++ int ret = 0, saved_page;
++
++ saved_page = air_phy_select_page(phydev, AIR_PHY_PAGE_EXTENDED_4);
++ if (saved_page < 0)
++ return saved_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_MODE,
++ AIR_BPBUS_MODE_ADDR_FIXED);
++ if (ret < 0)
++ goto restore_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_RD_ADDR_HIGH,
++ air_upper_16_bits(pbus_address));
++ if (ret < 0)
++ goto restore_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_RD_ADDR_LOW,
++ air_lower_16_bits(pbus_address));
++ if (ret < 0)
++ goto restore_page;
++
++ pbus_data_high = phy_read(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_RD_DATA_HIGH);
++ if (pbus_data_high < 0) {
++ ret = pbus_data_high;
++ goto restore_page;
++ }
++
++ pbus_data_low = phy_read(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_RD_DATA_LOW);
++ if (pbus_data_low < 0) {
++ ret = pbus_data_low;
++ goto restore_page;
++ }
++
++ *pbus_data = pbus_data_low | (pbus_data_high << 16);
++
++restore_page:
++ if (ret < 0)
++ printf("%s 0x%08x failed: %d\n", __func__,
++ pbus_address, ret);
++
++ return air_phy_restore_page(phydev, saved_page, ret);
++}
++
++static int air_buckpbus_reg_modify(struct phy_device *phydev,
++ u32 pbus_address, u32 mask, u32 set)
++{
++ int pbus_data_low, pbus_data_high;
++ u32 pbus_data_old, pbus_data_new;
++ int ret = 0, saved_page;
++
++ saved_page = air_phy_select_page(phydev, AIR_PHY_PAGE_EXTENDED_4);
++ if (saved_page < 0)
++ return saved_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_MODE,
++ AIR_BPBUS_MODE_ADDR_FIXED);
++ if (ret < 0)
++ goto restore_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_RD_ADDR_HIGH,
++ air_upper_16_bits(pbus_address));
++ if (ret < 0)
++ goto restore_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_RD_ADDR_LOW,
++ air_lower_16_bits(pbus_address));
++ if (ret < 0)
++ goto restore_page;
++
++ pbus_data_high = phy_read(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_RD_DATA_HIGH);
++ if (pbus_data_high < 0)
++ return pbus_data_high;
++
++ pbus_data_low = phy_read(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_RD_DATA_LOW);
++ if (pbus_data_low < 0)
++ return pbus_data_low;
++
++ pbus_data_old = pbus_data_low | (pbus_data_high << 16);
++ pbus_data_new = (pbus_data_old & ~mask) | set;
++ if (pbus_data_new == pbus_data_old)
++ return 0;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_WR_ADDR_HIGH,
++ air_upper_16_bits(pbus_address));
++ if (ret < 0)
++ goto restore_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_WR_ADDR_LOW,
++ air_lower_16_bits(pbus_address));
++ if (ret < 0)
++ goto restore_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_WR_DATA_HIGH,
++ air_upper_16_bits(pbus_data_new));
++ if (ret < 0)
++ goto restore_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_WR_DATA_LOW,
++ air_lower_16_bits(pbus_data_new));
++ if (ret < 0)
++ goto restore_page;
++
++restore_page:
++ if (ret < 0)
++ printf("%s 0x%08x failed: %d\n", __func__,
++ pbus_address, ret);
++
++ return air_phy_restore_page(phydev, saved_page, ret);
++}
++
++static int air_write_buf(struct phy_device *phydev, unsigned long address,
++ unsigned long array_size, const unsigned char *buffer)
++{
++ unsigned int offset;
++ int ret, saved_page;
++ u16 val;
++
++ saved_page = air_phy_select_page(phydev, AIR_PHY_PAGE_EXTENDED_4);
++ if (saved_page < 0)
++ return saved_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_MODE,
++ AIR_BPBUS_MODE_ADDR_INCR);
++ if (ret < 0)
++ goto restore_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_WR_ADDR_HIGH,
++ air_upper_16_bits(address));
++ if (ret < 0)
++ goto restore_page;
++
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_WR_ADDR_LOW,
++ air_lower_16_bits(address));
++ if (ret < 0)
++ goto restore_page;
++
++ for (offset = 0; offset < array_size; offset += 4) {
++ val = get_unaligned_le16(&buffer[offset + 2]);
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_WR_DATA_HIGH, val);
++ if (ret < 0)
++ goto restore_page;
++
++ val = get_unaligned_le16(&buffer[offset]);
++ ret = phy_write(phydev, MDIO_DEVAD_NONE, AIR_BPBUS_WR_DATA_LOW, val);
++ if (ret < 0)
++ goto restore_page;
++ }
++
++restore_page:
++ if (ret < 0)
++ printf("%s 0x%08lx failed: %d\n", __func__,
++ address, ret);
++
++ return air_phy_restore_page(phydev, saved_page, ret);
++}
++
++static int en8811h_wait_mcu_ready(struct phy_device *phydev)
++{
++ int ret, reg_value;
++
++ /* Because of mdio-lock, may have to wait for multiple loads */
++ ret = phy_read_mmd_poll_timeout(phydev, MDIO_MMD_VEND1,
++ EN8811H_PHY_FW_STATUS, reg_value,
++ reg_value == EN8811H_PHY_READY,
++ 20000, 7500000, true);
++ if (ret) {
++ printf("MCU not ready: 0x%x\n", reg_value);
++ return -ENODEV;
++ }
++
++ return 0;
++}
++
++__weak int en8811h_read_fw(void **addr)
++{
++ const char *fw_dm, *fw_dsp, *fw_part;
++ u32 ca_crc32;
++ void *buffer;
++ loff_t read;
++ int ret;
++
++ if (!IS_ENABLED(CONFIG_PHY_AIROHA_FW_IN_MMC))
++ return -EOPNOTSUPP;
++
++ /* Allocate memory to store both DM and DSP firmware */
++ buffer = malloc(EN8811H_MD32_DM_SIZE + EN8811H_MD32_DSP_SIZE);
++ if (!buffer) {
++ printf("Failed to allocate memory for firmware\n");
++ return -ENOMEM;
++ }
++
++ /* Get the partition name where the firmware is stored */
++ fw_part = env_get("en8811h_fw_part");
++ if (!fw_part) {
++ printf("Error: env var en8811h_fw_part not set.\n");
++ ret = -EINVAL;
++ goto err_free;
++ }
++
++ /* Get the DM firmware file path */
++ fw_dm = env_get("en8811h_fw_dm_dir");
++ if (!fw_dm) {
++ printf("Error: env var en8811h_fw_dm_dir not set.\n");
++ ret = -EINVAL;
++ goto err_free;
++ }
++
++ /* Get the DSP firmware file path */
++ fw_dsp = env_get("en8811h_fw_dsp_dir");
++ if (!fw_dsp) {
++ printf("Error: env var en8811h_fw_dsp_dir not set.\n");
++ ret = -EINVAL;
++ goto err_free;
++ }
++
++ /* Load DM firmware */
++ ret = fs_set_blk_dev("mmc", fw_part, FS_TYPE_ANY);
++ if (ret < 0)
++ goto err_free;
++
++ /* Read DM firmware file into the start of buffer */
++ ret = fs_read(fw_dm, (ulong)buffer, 0, EN8811H_MD32_DM_SIZE, &read);
++ if (ret < 0) {
++ printf("Failed to read DM firmware: %s\n", fw_dm);
++ goto err_free;
++ }
++
++ /* Calculate CRC32 of DM firmware for verification */
++ ca_crc32 = crc32(0, (unsigned char *)buffer, EN8811H_MD32_DM_SIZE);
++ debug("DM crc32: 0x%x\n", ca_crc32);
++
++ /* Load DSP firmware */
++ ret = fs_set_blk_dev("mmc", fw_part, FS_TYPE_ANY);
++ if (ret < 0)
++ goto err_free;
++
++ /* Read DSP firmware file into buffer after DM section */
++ ret = fs_read(fw_dsp, (ulong)buffer + EN8811H_MD32_DM_SIZE, 0,
++ EN8811H_MD32_DSP_SIZE, &read);
++ if (ret < 0) {
++ printf("Failed to read DSP firmware: %s\n", fw_dsp);
++ goto err_free;
++ }
++
++ /* Calculate CRC32 of DSP firmware for verification */
++ ca_crc32 = crc32(0, (unsigned char *)buffer + EN8811H_MD32_DM_SIZE,
++ EN8811H_MD32_DSP_SIZE);
++ debug("DSP crc32: 0x%x\n", ca_crc32);
++
++ *addr = buffer;
++ debug("Found Airoha Firmware.\n");
++
++ return 0;
++
++err_free:
++ free(buffer);
++ return ret;
++}
++
++static int en8811h_load_firmware(struct phy_device *phydev)
++{
++ struct en8811h_priv *priv = phydev->priv;
++ void *buffer = NULL;
++ int ret;
++
++ if (!IS_ENABLED(CONFIG_PHY_AIROHA_FW_IN_MMC)) {
++ printf("Airoha EN8811H firmware loading not implemented\n");
++ return -EOPNOTSUPP;
++ }
++
++ ret = en8811h_read_fw(&buffer);
++ if (ret < 0)
++ goto en8811h_load_firmware_out;
++
++ ret = air_buckpbus_reg_write(phydev, EN8811H_FW_CTRL_1,
++ EN8811H_FW_CTRL_1_START);
++ if (ret < 0)
++ goto en8811h_load_firmware_out;
++
++ ret = air_buckpbus_reg_modify(phydev, EN8811H_FW_CTRL_2,
++ EN8811H_FW_CTRL_2_LOADING,
++ EN8811H_FW_CTRL_2_LOADING);
++ if (ret < 0)
++ goto en8811h_load_firmware_out;
++
++ ret = air_write_buf(phydev, AIR_FW_ADDR_DM, EN8811H_MD32_DM_SIZE,
++ (unsigned char *)buffer);
++ if (ret < 0)
++ goto en8811h_load_firmware_out;
++
++ ret = air_write_buf(phydev, AIR_FW_ADDR_DSP, EN8811H_MD32_DSP_SIZE,
++ (unsigned char *)buffer + EN8811H_MD32_DM_SIZE);
++ if (ret < 0)
++ goto en8811h_load_firmware_out;
++
++ ret = air_buckpbus_reg_modify(phydev, EN8811H_FW_CTRL_2,
++ EN8811H_FW_CTRL_2_LOADING, 0);
++ if (ret < 0)
++ goto en8811h_load_firmware_out;
++
++ ret = air_buckpbus_reg_write(phydev, EN8811H_FW_CTRL_1,
++ EN8811H_FW_CTRL_1_FINISH);
++ if (ret < 0)
++ goto en8811h_load_firmware_out;
++
++ ret = en8811h_wait_mcu_ready(phydev);
++
++ air_buckpbus_reg_read(phydev, EN8811H_FW_VERSION,
++ &priv->firmware_version);
++ printf("MD32 firmware version: %08x\n",
++ priv->firmware_version);
++
++en8811h_load_firmware_out:
++ free(buffer);
++ if (ret < 0)
++ printf("Firmware loading failed: %d\n", ret);
++
++ return ret;
++}
++
++static int en8811h_restart_mcu(struct phy_device *phydev)
++{
++ int ret;
++
++ ret = phy_write_mmd(phydev, 0x1e, 0x8009, 0x0);
++ if (ret < 0)
++ return ret;
++
++ ret = air_buckpbus_reg_write(phydev, EN8811H_FW_CTRL_1,
++ EN8811H_FW_CTRL_1_START);
++ if (ret < 0)
++ return ret;
++
++ return air_buckpbus_reg_write(phydev, EN8811H_FW_CTRL_1,
++ EN8811H_FW_CTRL_1_FINISH);
++}
++
++static int air_led_hw_control_set(struct phy_device *phydev, u8 index,
++ unsigned long rules)
++{
++ struct en8811h_priv *priv = phydev->priv;
++ u16 on = 0, blink = 0;
++ int ret;
++
++ if (index >= EN8811H_LED_COUNT)
++ return -EINVAL;
++
++ priv->led[index].rules = rules;
++
++ if (rules & BIT(AIR_TRIGGER_NETDEV_FULL_DUPLEX))
++ on |= AIR_PHY_LED_ON_FDX;
++
++ if (rules & (BIT(AIR_TRIGGER_NETDEV_LINK_10) | BIT(AIR_TRIGGER_NETDEV_LINK)))
++ on |= AIR_PHY_LED_ON_LINK10;
++
++ if (rules & (BIT(AIR_TRIGGER_NETDEV_LINK_100) | BIT(AIR_TRIGGER_NETDEV_LINK)))
++ on |= AIR_PHY_LED_ON_LINK100;
++
++ if (rules & (BIT(AIR_TRIGGER_NETDEV_LINK_1000) | BIT(AIR_TRIGGER_NETDEV_LINK)))
++ on |= AIR_PHY_LED_ON_LINK1000;
++
++ if (rules & (BIT(AIR_TRIGGER_NETDEV_LINK_2500) | BIT(AIR_TRIGGER_NETDEV_LINK)))
++ on |= AIR_PHY_LED_ON_LINK2500;
++
++ if (rules & BIT(AIR_TRIGGER_NETDEV_RX)) {
++ blink |= AIR_PHY_LED_BLINK_10RX |
++ AIR_PHY_LED_BLINK_100RX |
++ AIR_PHY_LED_BLINK_1000RX |
++ AIR_PHY_LED_BLINK_2500RX;
++ }
++
++ if (rules & BIT(AIR_TRIGGER_NETDEV_TX)) {
++ blink |= AIR_PHY_LED_BLINK_10TX |
++ AIR_PHY_LED_BLINK_100TX |
++ AIR_PHY_LED_BLINK_1000TX |
++ AIR_PHY_LED_BLINK_2500TX;
++ }
++
++ if (blink || on) {
++ /* switch hw-control on, so led-on and led-blink are off */
++ clear_bit(AIR_PHY_LED_STATE_FORCE_ON,
++ &priv->led[index].state);
++ clear_bit(AIR_PHY_LED_STATE_FORCE_BLINK,
++ &priv->led[index].state);
++ } else {
++ priv->led[index].rules = 0;
++ }
++
++ ret = phy_modify_mmd(phydev, MDIO_MMD_VEND2, AIR_PHY_LED_ON(index),
++ AIR_PHY_LED_ON_MASK, on);
++
++ if (ret < 0)
++ return ret;
++
++ return phy_write_mmd(phydev, MDIO_MMD_VEND2, AIR_PHY_LED_BLINK(index),
++ blink);
++};
++
++static int air_led_init(struct phy_device *phydev, u8 index, u8 state, u8 pol)
++{
++ int val = 0;
++ int err;
++
++ if (index >= EN8811H_LED_COUNT)
++ return -EINVAL;
++
++ if (state == AIR_LED_ENABLE)
++ val |= AIR_PHY_LED_ON_ENABLE;
++ else
++ val &= ~AIR_PHY_LED_ON_ENABLE;
++
++ if (pol == AIR_ACTIVE_HIGH)
++ val |= AIR_PHY_LED_ON_POLARITY;
++ else
++ val &= ~AIR_PHY_LED_ON_POLARITY;
++
++ err = phy_write_mmd(phydev, 0x1f, AIR_PHY_LED_ON(index), val);
++ if (err < 0)
++ return err;
++
++ return 0;
++}
++
++static int air_leds_init(struct phy_device *phydev, int num, u16 dur, int mode)
++{
++ struct en8811h_priv *priv = phydev->priv;
++ int ret, i;
++
++ ret = phy_write_mmd(phydev, MDIO_MMD_VEND2, AIR_PHY_LED_DUR_BLINK,
++ dur);
++ if (ret < 0)
++ return ret;
++
++ ret = phy_write_mmd(phydev, MDIO_MMD_VEND2, AIR_PHY_LED_DUR_ON,
++ dur >> 1);
++ if (ret < 0)
++ return ret;
++
++ switch (mode) {
++ case AIR_LED_MODE_DISABLE:
++ ret = phy_modify_mmd(phydev, MDIO_MMD_VEND2, AIR_PHY_LED_BCR,
++ AIR_PHY_LED_BCR_EXT_CTRL |
++ AIR_PHY_LED_BCR_MODE_MASK, 0);
++ break;
++ case AIR_LED_MODE_USER_DEFINE:
++ ret = phy_modify_mmd(phydev, MDIO_MMD_VEND2, AIR_PHY_LED_BCR,
++ AIR_PHY_LED_BCR_EXT_CTRL |
++ AIR_PHY_LED_BCR_CLK_EN,
++ AIR_PHY_LED_BCR_EXT_CTRL |
++ AIR_PHY_LED_BCR_CLK_EN);
++ if (ret < 0)
++ return ret;
++ break;
++ default:
++ printf("LED mode %d is not supported\n", mode);
++ return -EINVAL;
++ }
++
++ for (i = 0; i < num; ++i) {
++ ret = air_led_init(phydev, i, AIR_LED_ENABLE, AIR_ACTIVE_HIGH);
++ if (ret < 0) {
++ printf("LED%d init failed: %d\n", i, ret);
++ return ret;
++ }
++ air_led_hw_control_set(phydev, i, priv->led[i].rules);
++ }
++
++ return 0;
++}
++
++static int en8811h_config(struct phy_device *phydev)
++{
++ struct en8811h_priv *priv = phydev->priv;
++ ofnode node = phy_get_ofnode(phydev);
++ u32 pbus_value = 0;
++ int ret = 0;
++
++ /* If restart happened in .probe(), no need to restart now */
++ if (priv->mcu_needs_restart) {
++ ret = en8811h_restart_mcu(phydev);
++ if (ret < 0)
++ return ret;
++ } else {
++ ret = en8811h_load_firmware(phydev);
++ if (ret) {
++ printf("Load firmware fail.\n");
++ return ret;
++ }
++ /* Next calls to .config() mcu needs to restart */
++ priv->mcu_needs_restart = true;
++ }
++
++ ret = phy_write_mmd(phydev, 0x1e, 0x800c, 0x0);
++ if (ret < 0)
++ return ret;
++ ret = phy_write_mmd(phydev, 0x1e, 0x800d, 0x0);
++ if (ret < 0)
++ return ret;
++ ret = phy_write_mmd(phydev, 0x1e, 0x800e, 0x1101);
++ if (ret < 0)
++ return ret;
++ ret = phy_write_mmd(phydev, 0x1e, 0x800f, 0x0002);
++ if (ret < 0)
++ return ret;
++
++ /* Serdes polarity */
++ pbus_value = 0;
++ if (ofnode_read_bool(node, "airoha,pnswap-rx"))
++ pbus_value |= EN8811H_POLARITY_RX_REVERSE;
++ else
++ pbus_value &= ~EN8811H_POLARITY_RX_REVERSE;
++ if (ofnode_read_bool(node, "airoha,pnswap-tx"))
++ pbus_value &= ~EN8811H_POLARITY_TX_NORMAL;
++ else
++ pbus_value |= EN8811H_POLARITY_TX_NORMAL;
++ ret = air_buckpbus_reg_modify(phydev, EN8811H_POLARITY,
++ EN8811H_POLARITY_RX_REVERSE |
++ EN8811H_POLARITY_TX_NORMAL, pbus_value);
++ if (ret < 0)
++ return ret;
++
++ ret = air_leds_init(phydev, EN8811H_LED_COUNT, AIR_PHY_LED_DUR,
++ AIR_LED_MODE_USER_DEFINE);
++ if (ret < 0) {
++ printf("Failed to disable leds: %d\n", ret);
++ return ret;
++ }
++
++ return 0;
++}
++
++static int en8811h_parse_status(struct phy_device *phydev)
++{
++ int ret = 0, reg_value;
++
++ phydev->duplex = DUPLEX_FULL;
++
++ reg_value = phy_read(phydev, MDIO_DEVAD_NONE, AIR_AUX_CTRL_STATUS);
++ if (reg_value < 0)
++ return reg_value;
++
++ switch (reg_value & AIR_AUX_CTRL_STATUS_SPEED_MASK) {
++ case AIR_AUX_CTRL_STATUS_SPEED_2500:
++ phydev->speed = SPEED_2500;
++ break;
++ case AIR_AUX_CTRL_STATUS_SPEED_1000:
++ phydev->speed = SPEED_1000;
++ break;
++ case AIR_AUX_CTRL_STATUS_SPEED_100:
++ phydev->speed = SPEED_100;
++ break;
++ default:
++ printf("Auto-neg error, defaulting to 100M/FD\n");
++ phydev->speed = SPEED_100;
++ break;
++ }
++
++ return ret;
++}
++
++static int en8811h_startup(struct phy_device *phydev)
++{
++ int ret = 0;
++
++ ret = genphy_update_link(phydev);
++ if (ret)
++ return ret;
++
++ return en8811h_parse_status(phydev);
++}
++
++static int en8811h_probe(struct phy_device *phydev)
++{
++ struct en8811h_priv *priv;
++
++ priv = malloc(sizeof(*priv));
++ if (!priv)
++ return -ENOMEM;
++ memset(priv, 0, sizeof(*priv));
++
++ priv->led[0].rules = AIR_DEFAULT_TRIGGER_LED0;
++ priv->led[1].rules = AIR_DEFAULT_TRIGGER_LED1;
++ priv->led[2].rules = AIR_DEFAULT_TRIGGER_LED2;
++
++ /* mcu has just restarted after firmware load */
++ priv->mcu_needs_restart = false;
++
++ phydev->priv = priv;
++
++ return 0;
++}
++
++U_BOOT_PHY_DRIVER(en8811h) = {
++ .name = "Airoha EN8811H",
++ .uid = EN8811H_PHY_ID,
++ .mask = 0x0ffffff0,
++ .config = &en8811h_config,
++ .probe = &en8811h_probe,
++ .startup = &en8811h_startup,
++ .shutdown = &genphy_shutdown,
++};
+--
+2.43.0
+
diff --git a/patches/uboot/2025.01/0006-Add-bpi-r3-mini-device-tree.patch b/patches/uboot/2025.01/0006-Add-bpi-r3-mini-device-tree.patch
new file mode 100644
index 000000000..1811ce3c6
--- /dev/null
+++ b/patches/uboot/2025.01/0006-Add-bpi-r3-mini-device-tree.patch
@@ -0,0 +1,275 @@
+From a987f92a768291669eec1a8c5d413190bd9241ec Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?=
+Date: Wed, 18 Feb 2026 17:00:42 +0100
+Subject: [PATCH 6/7] Add bpi-r3-mini device tree
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Organization: Wires
+
+Signed-off-by: Mattias Walström
+---
+ arch/arm/dts/Makefile | 1 +
+ arch/arm/dts/mt7986a-bpi-r3-mini.dts | 238 +++++++++++++++++++++++++++
+ 2 files changed, 239 insertions(+)
+ create mode 100644 arch/arm/dts/mt7986a-bpi-r3-mini.dts
+
+diff --git a/arch/arm/dts/Makefile b/arch/arm/dts/Makefile
+index 6ad59aeed5f..3561e4c3651 100644
+--- a/arch/arm/dts/Makefile
++++ b/arch/arm/dts/Makefile
+@@ -1199,6 +1199,7 @@ dtb-$(CONFIG_ARCH_MEDIATEK) += \
+ mt7981-sd-rfb.dtb \
+ mt7986a-bpi-r3-sd.dtb \
+ mt7986a-bpi-r3-emmc.dtb \
++ mt7986a-bpi-r3-mini.dtb \
+ mt7986a-rfb.dtb \
+ mt7986b-rfb.dtb \
+ mt7986a-sd-rfb.dtb \
+diff --git a/arch/arm/dts/mt7986a-bpi-r3-mini.dts b/arch/arm/dts/mt7986a-bpi-r3-mini.dts
+new file mode 100644
+index 00000000000..b8d0a01c9ed
+--- /dev/null
++++ b/arch/arm/dts/mt7986a-bpi-r3-mini.dts
+@@ -0,0 +1,238 @@
++// SPDX-License-Identifier: GPL-2.0-or-later
++
++/dts-v1/;
++#include "mt7986.dtsi"
++#include
++#include
++
++/ {
++ #address-cells = <1>;
++ #size-cells = <1>;
++ model = "Bananapi BPi-R3 Mini";
++ compatible = "mediatek,mt7986", "mediatek,mt7986-rfb";
++
++ chosen {
++ stdout-path = &uart0;
++ tick-timer = &timer0;
++ };
++
++ memory@40000000 {
++ device_type = "memory";
++ reg = <0x40000000 0x80000000>;
++ };
++
++ gpio-keys {
++ compatible = "gpio-keys";
++
++ button-reset {
++ label = "reset";
++ linux,code = ;
++ gpios = <&gpio 7 GPIO_ACTIVE_LOW>;
++ };
++ };
++
++ leds {
++ compatible = "gpio-leds";
++
++ status_led: led-0 {
++ label = "green:status";
++ gpios = <&gpio 19 GPIO_ACTIVE_HIGH>;
++ };
++
++ led-1 {
++ label = "blue:wlan2g";
++ gpios = <&gpio 1 GPIO_ACTIVE_HIGH>;
++ };
++
++ led-2 {
++ label = "blue:wlan5g";
++ gpios = <&gpio 2 GPIO_ACTIVE_HIGH>;
++ };
++ };
++
++ reg_1p8v: regulator-1p8v {
++ compatible = "regulator-fixed";
++ regulator-name = "fixed-1.8V";
++ regulator-min-microvolt = <1800000>;
++ regulator-max-microvolt = <1800000>;
++ regulator-boot-on;
++ regulator-always-on;
++ };
++
++ reg_3p3v: regulator-3p3v {
++ compatible = "regulator-fixed";
++ regulator-name = "fixed-3.3V";
++ regulator-min-microvolt = <3300000>;
++ regulator-max-microvolt = <3300000>;
++ regulator-boot-on;
++ regulator-always-on;
++ };
++};
++
++ð {
++ status = "okay";
++ pinctrl-names = "default";
++ pinctrl-0 = <&mdio_pins>;
++
++ mediatek,gmac-id = <0>;
++ phy-mode = "2500base-x";
++ phy-handle = <&phy14>;
++
++ phy14: eth-phy@e {
++ compatible = "ethernet-phy-id03a2.a411";
++ reg = <14>;
++
++ airoha,pnswap-rx;
++
++ reset-gpios = <&gpio 49 GPIO_ACTIVE_LOW>;
++ reset-assert-us = <10000>;
++ reset-deassert-us = <20000>;
++ };
++};
++
++&mmc0 {
++ pinctrl-names = "default";
++ pinctrl-0 = <&mmc0_pins_default>;
++ bus-width = <8>;
++ max-frequency = <200000000>;
++ cap-mmc-highspeed;
++ cap-mmc-hw-reset;
++ vmmc-supply = <®_3p3v>;
++ vqmmc-supply = <®_1p8v>;
++ non-removable;
++ status = "okay";
++};
++
++&pinctrl {
++ mdio_pins: mdio-pins {
++ mux {
++ function = "eth";
++ groups = "mdc_mdio";
++ };
++
++ conf-en8811-pwr-a {
++ pins = "GPIO_11";
++ drive-strength = ;
++ bias-pull-down = ;
++ output-low;
++ };
++
++ conf-en8811-pwr-b {
++ pins = "GPIO_12";
++ drive-strength = ;
++ bias-pull-down = ;
++ output-low;
++ };
++ };
++
++ mmc0_pins_default: mmc0default {
++ mux {
++ function = "flash";
++ groups = "emmc_51";
++ };
++
++ conf-cmd-dat {
++ pins = "EMMC_DATA_0", "EMMC_DATA_1", "EMMC_DATA_2",
++ "EMMC_DATA_3", "EMMC_DATA_4", "EMMC_DATA_5",
++ "EMMC_DATA_6", "EMMC_DATA_7", "EMMC_CMD";
++ input-enable;
++ drive-strength = ;
++ bias-pull-up = ;
++ };
++
++ conf-clk {
++ pins = "EMMC_CK";
++ drive-strength = ;
++ bias-pull-down = ;
++ };
++
++ conf-dsl {
++ pins = "EMMC_DSL";
++ bias-pull-down = ;
++ };
++
++ conf-rst {
++ pins = "EMMC_RSTB";
++ drive-strength = ;
++ bias-pull-up = ;
++ };
++ };
++
++ spi_flash_pins: spi0-pins-func-1 {
++ mux {
++ function = "flash";
++ groups = "spi0", "spi0_wp_hold";
++ };
++
++ conf-pu {
++ pins = "SPI2_CS", "SPI2_HOLD", "SPI2_WP";
++ drive-strength = ;
++ bias-pull-up = ;
++ };
++
++ conf-pd {
++ pins = "SPI2_CLK", "SPI2_MOSI", "SPI2_MISO";
++ drive-strength = ;
++ bias-pull-down = ;
++ };
++ };
++
++ pwm_pins: pwm0-pins-func-1 {
++ mux {
++ function = "pwm";
++ groups = "pwm0";
++ };
++ };
++};
++
++&pwm {
++ pinctrl-names = "default";
++ pinctrl-0 = <&pwm_pins>;
++ status = "okay";
++};
++
++&spi0 {
++ #address-cells = <1>;
++ #size-cells = <0>;
++ pinctrl-names = "default";
++ pinctrl-0 = <&spi_flash_pins>;
++ status = "okay";
++ must_tx;
++ enhance_timing;
++ dma_ext;
++ ipm_design;
++ support_quad;
++ tick_dly = <1>;
++ sample_sel = <0>;
++
++ spi_nand@0 {
++ compatible = "spi-nand";
++ reg = <0>;
++ spi-max-frequency = <20000000>;
++
++ partitions {
++ compatible = "fixed-partitions";
++ #address-cells = <1>;
++ #size-cells = <1>;
++
++ partition@0 {
++ label = "bl2";
++ reg = <0x0 0x200000>;
++ };
++
++ partition@200000 {
++ label = "ubi";
++ reg = <0x200000 0x7e00000>;
++ };
++ };
++ };
++
++};
++
++&uart0 {
++ status = "okay";
++};
++
++&watchdog {
++ status = "disabled";
++};
+--
+2.43.0
+
diff --git a/patches/uboot/2025.01/0007-bpi-r3-r4-Add-probe-for-specific-model.patch b/patches/uboot/2025.01/0007-bpi-r3-r4-Add-probe-for-specific-model.patch
new file mode 100644
index 000000000..a3e1bbe7a
--- /dev/null
+++ b/patches/uboot/2025.01/0007-bpi-r3-r4-Add-probe-for-specific-model.patch
@@ -0,0 +1,562 @@
+From 57b3cd8612a8da852f0796d97e6a7526cc4742cc Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?=
+Date: Wed, 18 Feb 2026 17:02:23 +0100
+Subject: [PATCH 7/7] bpi-r3/r4: Add probe for specific model
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Organization: Wires
+
+Probe for bpi-r3 and bpi-r3-mini and select correct device tree,
+prepare for bpi-r4 (not supported yet, but soon), for bpi-r3,
+look if there are phys connected, if so it is a mini, else it is
+regular bpi-r3.
+
+Signed-off-by: Mattias Walström
+---
+ arch/arm/mach-mediatek/Kconfig | 3 +
+ board/mediatek/mt7986/Kconfig | 10 ++
+ board/mediatek/mt7986/Makefile | 1 +
+ board/mediatek/mt7986/bpir3.c | 229 +++++++++++++++++++++++++++
+ board/mediatek/mt7988/Kconfig | 9 ++
+ board/mediatek/mt7988/Makefile | 1 +
+ board/mediatek/mt7988/bpir4.c | 33 ++++
+ configs/mt7986a_bpir3_emmc_defconfig | 2 +
+ configs/mt7986a_bpir3_mini_defconfig | 66 ++++++++
+ configs/mt7986a_bpir3_sd_defconfig | 2 +
+ configs/mt7988a_bpir4_defconfig | 85 ++++++++++
+ 11 files changed, 441 insertions(+)
+ create mode 100644 board/mediatek/mt7986/Kconfig
+ create mode 100644 board/mediatek/mt7986/bpir3.c
+ create mode 100644 board/mediatek/mt7988/Kconfig
+ create mode 100644 board/mediatek/mt7988/bpir4.c
+ create mode 100644 configs/mt7986a_bpir3_mini_defconfig
+ create mode 100644 configs/mt7988a_bpir4_defconfig
+
+diff --git a/arch/arm/mach-mediatek/Kconfig b/arch/arm/mach-mediatek/Kconfig
+index ff1fdee5c8d..def8a564d90 100644
+--- a/arch/arm/mach-mediatek/Kconfig
++++ b/arch/arm/mach-mediatek/Kconfig
+@@ -153,4 +153,7 @@ config MTK_BROM_HEADER_INFO
+ default "media=snand;nandinfo=2k+64" if TARGET_MT7981 || TARGET_MT7986 || TARGET_MT7988
+ default "lk=1" if TARGET_MT7623
+
++source "board/mediatek/mt7986/Kconfig"
++source "board/mediatek/mt7988/Kconfig"
++
+ endif
+diff --git a/board/mediatek/mt7986/Kconfig b/board/mediatek/mt7986/Kconfig
+new file mode 100644
+index 00000000000..cea150b52d8
+--- /dev/null
++++ b/board/mediatek/mt7986/Kconfig
+@@ -0,0 +1,10 @@
++if TARGET_MT7986
++
++config BOARD_BPI_R3
++ bool "BananaPi BPI-R3 / BPI-R3-mini board support"
++ select BOARD_LATE_INIT
++ help
++ Enable runtime variant detection for BananaPi BPI-R3 and BPI-R3-mini,
++ selecting the correct device tree from a FIT image.
++
++endif
+diff --git a/board/mediatek/mt7986/Makefile b/board/mediatek/mt7986/Makefile
+index 7bb84fa2f4e..b006ee150bd 100644
+--- a/board/mediatek/mt7986/Makefile
++++ b/board/mediatek/mt7986/Makefile
+@@ -1,3 +1,4 @@
+ # SPDX-License-Identifier: GPL-2.0
+
+ obj-y += mt7986_rfb.o
++obj-$(CONFIG_BOARD_BPI_R3) += bpir3.o
+diff --git a/board/mediatek/mt7986/bpir3.c b/board/mediatek/mt7986/bpir3.c
+new file mode 100644
+index 00000000000..01eef55bb74
+--- /dev/null
++++ b/board/mediatek/mt7986/bpir3.c
+@@ -0,0 +1,229 @@
++// SPDX-License-Identifier: GPL-2.0
++/*
++ * BPI-R3 / BPI-R3-mini board variant detection
++ */
++
++#include
++#include
++#include
++#include
++#include
++#include
++#include
++
++DECLARE_GLOBAL_DATA_PTR;
++
++enum bpir3_variant {
++ BPIR3,
++ BPIR3_MINI,
++};
++
++/*
++ * Detect BPI-R3 vs BPI-R3-mini by probing for the Airoha EN8811H PHY.
++ *
++ * BPI-R3-mini: EN8811H is directly on the MDIO bus at addr 14.
++ * GPIO49 (active-low) is its reset; PHYSID1 = 0x03a2.
++ * BPI-R3: MT7531AE switch via SGMII; nothing at MDIO addr 14 → 0xffff.
++ *
++ * NOTE: I2C 0x50 is NOT usable — BPI-R3 SFP cage EEPROMs occupy 0x50–0x5b.
++ */
++
++/*
++ * MT7986 GPIO — gpio_base = 0x1001f000
++ *
++ * DIR register: base + 0x000 + (pin/32)*0x10, 1 bit/pin (1=output)
++ * DOUT register: base + 0x100 + (pin/32)*0x10, 1 bit/pin
++ * MODE register: base + 0x300 + (pin*4/32)*0x10, 4 bits/pin (0=GPIO, 1=eth…)
++ */
++#define MT7986_GPIO_BASE 0x1001f000UL
++#define GPIO_DIR_REG(p) (MT7986_GPIO_BASE + 0x000 + ((p) / 32) * 0x10)
++#define GPIO_DOUT_REG(p) (MT7986_GPIO_BASE + 0x100 + ((p) / 32) * 0x10)
++#define GPIO_MODE_REG(p) (MT7986_GPIO_BASE + 0x300 + (((p) * 4) / 32) * 0x10)
++#define GPIO_MODE_SHIFT(p) (((p) * 4) % 32)
++
++/* EN8811H PHY reset: GPIO49, active-low */
++#define EN8811H_RST_GPIO 49
++/* EN8811H power control lines: active-low enables on some BPI-R3-mini dts */
++#define EN8811H_PWR_A_GPIO 11
++#define EN8811H_PWR_B_GPIO 12
++/* Alternative EN8811H power control lines used by other mini trees */
++#define EN8811H_PWR2_A_GPIO 16
++#define EN8811H_PWR2_B_GPIO 17
++
++/* MDC/MDIO pins: GPIO67 = MDC, GPIO68 = MDIO, both on function 1 ("eth") */
++#define MDC_GPIO 67
++#define MDIO_GPIO 68
++
++/*
++ * MT7986 Frame Engine: ethernet@15100000, GMAC at +0x10000
++ *
++ * PPSC (+0x0000): MDC clock config — bits[29:24] = divider, bit[4] = turbo
++ * PIAC (+0x0004): PHY indirect access — write then poll PHY_ACS_ST clear
++ */
++#define GMAC_BASE 0x15110000UL
++#define GMAC_PPSC (GMAC_BASE + 0x0000)
++#define GMAC_PIAC (GMAC_BASE + 0x0004)
++
++#define PPSC_MDC_CFG_MASK GENMASK(29, 24)
++#define PPSC_MDC_CFG(d) (((d) & 0x3f) << 24)
++#define PPSC_MDC_TURBO BIT(4)
++/* divider = 10 → MDC ≈ 2.5 MHz from 25 MHz reference */
++#define MDC_DIVIDER 10
++
++#define PIAC_ACS_ST BIT(31)
++#define PIAC_REG_S 25
++#define PIAC_PHY_S 20
++#define PIAC_CMD_S 18
++#define PIAC_ST_S 16
++#define PIAC_DATA_MASK 0xffffU
++#define PIAC_CMD_READ 2
++#define PIAC_ST_C22 1
++
++/* EN8811H PHY ID register values */
++#define EN8811H_PHYSID1 0x03a2
++#define EN8811H_PHYSID2_MASK 0xfff0
++#define EN8811H_PHYSID2 0xa410
++#define MII_PHYSID1 0x02 /* IEEE MII register 2 */
++#define MII_PHYSID2 0x03 /* IEEE MII register 3 */
++
++/* Set a GPIO pin's mode field (0 = GPIO, 1 = eth function, …) */
++static void gpio_set_mode(unsigned int pin, unsigned int func)
++{
++ clrsetbits_le32(GPIO_MODE_REG(pin),
++ 0xf << GPIO_MODE_SHIFT(pin),
++ (func & 0xf) << GPIO_MODE_SHIFT(pin));
++}
++
++/* Configure a GPIO pin as output and drive it high or low */
++static void gpio_out(unsigned int pin, int val)
++{
++ setbits_le32(GPIO_DIR_REG(pin), BIT(pin % 32));
++ if (val)
++ setbits_le32(GPIO_DOUT_REG(pin), BIT(pin % 32));
++ else
++ clrbits_le32(GPIO_DOUT_REG(pin), BIT(pin % 32));
++}
++
++/*
++ * Clause-22 MDIO read via the MT7986 GMAC PHY indirect access register.
++ * Returns the 16-bit register value, or -1 on timeout.
++ */
++static int mdio_read_c22(unsigned int phy, unsigned int reg)
++{
++ u32 val;
++ int i;
++
++ val = (PIAC_ST_C22 << PIAC_ST_S) |
++ (PIAC_CMD_READ << PIAC_CMD_S) |
++ (phy << PIAC_PHY_S) |
++ (reg << PIAC_REG_S);
++ writel(val | PIAC_ACS_ST, GMAC_PIAC);
++
++ for (i = 0; i < 5000; i++) {
++ val = readl(GMAC_PIAC);
++ if (!(val & PIAC_ACS_ST))
++ return (int)(val & PIAC_DATA_MASK);
++ udelay(1);
++ }
++
++ return -1; /* timeout */
++}
++
++static void enable_mini_power_lines(unsigned int gpio_a, unsigned int gpio_b)
++{
++ gpio_set_mode(gpio_a, 0); /* mode 0 = GPIO */
++ gpio_set_mode(gpio_b, 0); /* mode 0 = GPIO */
++ gpio_out(gpio_a, 0); /* active-low enable */
++ gpio_out(gpio_b, 0); /* active-low enable */
++}
++
++static void pulse_mini_phy_reset(void)
++{
++ gpio_set_mode(EN8811H_RST_GPIO, 0); /* mode 0 = GPIO */
++ gpio_out(EN8811H_RST_GPIO, 0); /* assert reset */
++ mdelay(10);
++ gpio_out(EN8811H_RST_GPIO, 1); /* deassert reset */
++ mdelay(20);
++}
++
++static bool is_en8811_id(int id1, int id2)
++{
++ return id1 == EN8811H_PHYSID1 &&
++ (id2 & EN8811H_PHYSID2_MASK) == EN8811H_PHYSID2;
++}
++
++static int probe_en8811_addr(unsigned int phy_addr)
++{
++ int id1, id2;
++
++ id1 = mdio_read_c22(phy_addr, MII_PHYSID1);
++ id2 = mdio_read_c22(phy_addr, MII_PHYSID2);
++
++ if (id1 < 0 || id2 < 0)
++ return 0;
++
++ return is_en8811_id(id1, id2);
++}
++
++static enum bpir3_variant detect_bpir3_variant(void)
++{
++ unsigned int pwr_a[] = { EN8811H_PWR_A_GPIO, EN8811H_PWR2_A_GPIO };
++ unsigned int pwr_b[] = { EN8811H_PWR_B_GPIO, EN8811H_PWR2_B_GPIO };
++ unsigned int i;
++
++ /* Switch GPIO67 (MDC) and GPIO68 (MDIO) to eth function */
++ gpio_set_mode(MDC_GPIO, 1);
++ gpio_set_mode(MDIO_GPIO, 1);
++
++ /* Configure MDC clock: enable turbo + set safe divider (~2.5 MHz) */
++ setbits_le32(GMAC_PPSC, PPSC_MDC_TURBO);
++ clrsetbits_le32(GMAC_PPSC, PPSC_MDC_CFG_MASK, PPSC_MDC_CFG(MDC_DIVIDER));
++
++ /*
++ * Enable EN8811H power rails and probe; some mini boards use
++ * GPIO11/12, others GPIO16/17.
++ */
++ for (i = 0; i < ARRAY_SIZE(pwr_a); i++) {
++ enable_mini_power_lines(pwr_a[i], pwr_b[i]);
++ pulse_mini_phy_reset();
++
++ if (probe_en8811_addr(14) || probe_en8811_addr(15)) {
++ /* Assert reset; DM eth-phy will deassert with proper delays */
++ gpio_out(EN8811H_RST_GPIO, 0);
++ return BPIR3_MINI;
++ }
++ }
++
++ return BPIR3;
++}
++
++int board_fit_config_name_match(const char *name)
++{
++ static int variant = -1;
++
++ if (variant < 0)
++ variant = detect_bpir3_variant();
++
++ switch (variant) {
++ case BPIR3_MINI:
++ return strcmp(name, "mt7986a-bpi-r3-mini") ? -1 : 0;
++ case BPIR3:
++ default:
++ /*
++ * Accept both sd and emmc variants of BPI-R3;
++ * storage type is determined by spl_boot_device(), not here.
++ */
++ return (strcmp(name, "mt7986a-bpi-r3-sd") == 0 ||
++ strcmp(name, "mt7986a-bpi-r3-emmc") == 0) ? 0 : -1;
++ }
++}
++
++int board_late_init(void)
++{
++ const char *model = fdt_getprop(gd->fdt_blob, 0, "model", NULL);
++
++ if (model && strstr(model, "Mini"))
++ env_set("fdtfile", "mediatek/mt7986a-bananapi-bpi-r3-mini.dtb");
++
++ return 0;
++}
+diff --git a/board/mediatek/mt7988/Kconfig b/board/mediatek/mt7988/Kconfig
+new file mode 100644
+index 00000000000..24c70417ec5
+--- /dev/null
++++ b/board/mediatek/mt7988/Kconfig
+@@ -0,0 +1,9 @@
++if TARGET_MT7988
++
++config BOARD_BPI_R4
++ bool "BananaPi BPI-R4 / BPI-R4-2g5 board support"
++ help
++ Enable runtime variant detection for BananaPi BPI-R4 and BPI-R4-2g5,
++ selecting the correct device tree from a FIT image.
++
++endif
+diff --git a/board/mediatek/mt7988/Makefile b/board/mediatek/mt7988/Makefile
+index f1249ab3715..157ee371eae 100644
+--- a/board/mediatek/mt7988/Makefile
++++ b/board/mediatek/mt7988/Makefile
+@@ -1,3 +1,4 @@
+ # SPDX-License-Identifier: GPL-2.0
+
+ obj-y += mt7988_rfb.o
++obj-$(CONFIG_BOARD_BPI_R4) += bpir4.o
+diff --git a/board/mediatek/mt7988/bpir4.c b/board/mediatek/mt7988/bpir4.c
+new file mode 100644
+index 00000000000..46e22d38135
+--- /dev/null
++++ b/board/mediatek/mt7988/bpir4.c
+@@ -0,0 +1,33 @@
++// SPDX-License-Identifier: GPL-2.0
++/*
++ * BPI-R4 / BPI-R4-2g5 board variant detection
++ */
++
++#include
++#include
++
++enum bpir4_variant {
++ BPIR4,
++ BPIR4_2G5,
++};
++
++/*
++ * Detect which BPI-R4 variant this board is.
++ * TODO: implement real hardware detection
++ */
++static enum bpir4_variant detect_bpir4_variant(void)
++{
++ /* Stub: always report BPI-R4 */
++ return BPIR4;
++}
++
++int board_fit_config_name_match(const char *name)
++{
++ switch (detect_bpir4_variant()) {
++ case BPIR4_2G5:
++ return strcmp(name, "mt7988a-bananapi-bpi-r4-2g5") ? -1 : 0;
++ case BPIR4:
++ default:
++ return strcmp(name, "mt7988a-bananapi-bpi-r4") ? -1 : 0;
++ }
++}
+diff --git a/configs/mt7986a_bpir3_emmc_defconfig b/configs/mt7986a_bpir3_emmc_defconfig
+index ef6a4822c18..193acb4a9b4 100644
+--- a/configs/mt7986a_bpir3_emmc_defconfig
++++ b/configs/mt7986a_bpir3_emmc_defconfig
+@@ -9,6 +9,8 @@ CONFIG_ENV_SIZE=0x80000
+ CONFIG_ENV_OFFSET=0x300000
+ CONFIG_DEFAULT_DEVICE_TREE="mt7986a-bpi-r3-emmc"
+ CONFIG_TARGET_MT7986=y
++CONFIG_BOARD_BPI_R3=y
++CONFIG_SPL_LOAD_FIT=y
+ CONFIG_SYS_LOAD_ADDR=0x46000000
+ CONFIG_DEBUG_UART_BASE=0x11002000
+ CONFIG_DEBUG_UART_CLOCK=40000000
+diff --git a/configs/mt7986a_bpir3_mini_defconfig b/configs/mt7986a_bpir3_mini_defconfig
+new file mode 100644
+index 00000000000..39e44555c55
+--- /dev/null
++++ b/configs/mt7986a_bpir3_mini_defconfig
+@@ -0,0 +1,66 @@
++CONFIG_ARM=y
++CONFIG_SYS_HAS_NONCACHED_MEMORY=y
++CONFIG_POSITION_INDEPENDENT=y
++CONFIG_ARCH_MEDIATEK=y
++CONFIG_TEXT_BASE=0x41e00000
++CONFIG_SYS_MALLOC_F_LEN=0x4000
++CONFIG_NR_DRAM_BANKS=1
++CONFIG_ENV_SIZE=0x80000
++CONFIG_ENV_OFFSET=0x300000
++CONFIG_DEFAULT_DEVICE_TREE="mt7986a-bpi-r3-mini"
++CONFIG_TARGET_MT7986=y
++CONFIG_BOARD_BPI_R3=y
++CONFIG_SPL_LOAD_FIT=y
++CONFIG_SYS_LOAD_ADDR=0x46000000
++CONFIG_DEBUG_UART_BASE=0x11002000
++CONFIG_DEBUG_UART_CLOCK=40000000
++CONFIG_DEBUG_UART=y
++# CONFIG_EFI_LOADER is not set
++# CONFIG_AUTOBOOT is not set
++CONFIG_DEFAULT_FDT_FILE="mt7986a-bpi-r3-mini"
++CONFIG_SYS_CBSIZE=512
++CONFIG_SYS_PBSIZE=1049
++CONFIG_LOGLEVEL=7
++CONFIG_LOG=y
++CONFIG_SYS_PROMPT="BPI-R3> "
++# CONFIG_BOOTM_NETBSD is not set
++# CONFIG_BOOTM_PLAN9 is not set
++# CONFIG_BOOTM_RTEMS is not set
++# CONFIG_BOOTM_VXWORKS is not set
++# CONFIG_CMD_ELF is not set
++# CONFIG_CMD_UNLZ4 is not set
++# CONFIG_CMD_UNZIP is not set
++CONFIG_CMD_GPIO=y
++CONFIG_CMD_GPT=y
++CONFIG_CMD_GPT_RENAME=y
++CONFIG_CMD_LSBLK=y
++CONFIG_CMD_MMC=y
++CONFIG_CMD_PART=y
++CONFIG_CMD_READ=y
++CONFIG_CMD_PING=y
++CONFIG_CMD_SMC=y
++CONFIG_CMD_FAT=y
++CONFIG_CMD_FS_GENERIC=y
++CONFIG_PARTITION_TYPE_GUID=y
++CONFIG_ENV_OVERWRITE=y
++CONFIG_ENV_IS_IN_MMC=y
++CONFIG_ENV_VARS_UBOOT_RUNTIME_CONFIG=y
++CONFIG_NET_RANDOM_ETHADDR=y
++CONFIG_REGMAP=y
++CONFIG_SYSCON=y
++CONFIG_CLK=y
++CONFIG_MMC_HS200_SUPPORT=y
++CONFIG_MMC_MTK=y
++CONFIG_PHY_FIXED=y
++CONFIG_MEDIATEK_ETH=y
++CONFIG_PINCTRL=y
++CONFIG_PINCONF=y
++CONFIG_PINCTRL_MT7986=y
++CONFIG_POWER_DOMAIN=y
++CONFIG_MTK_POWER_DOMAIN=y
++CONFIG_DM_REGULATOR=y
++CONFIG_DM_REGULATOR_FIXED=y
++CONFIG_DM_SERIAL=y
++CONFIG_MTK_SERIAL=y
++CONFIG_FAT_WRITE=y
++CONFIG_HEXDUMP=y
+diff --git a/configs/mt7986a_bpir3_sd_defconfig b/configs/mt7986a_bpir3_sd_defconfig
+index 3d971f5c313..e498b58a4a5 100644
+--- a/configs/mt7986a_bpir3_sd_defconfig
++++ b/configs/mt7986a_bpir3_sd_defconfig
+@@ -9,6 +9,8 @@ CONFIG_ENV_SIZE=0x80000
+ CONFIG_ENV_OFFSET=0x300000
+ CONFIG_DEFAULT_DEVICE_TREE="mt7986a-bpi-r3-sd"
+ CONFIG_TARGET_MT7986=y
++CONFIG_BOARD_BPI_R3=y
++CONFIG_SPL_LOAD_FIT=y
+ CONFIG_SYS_LOAD_ADDR=0x46000000
+ CONFIG_DEBUG_UART_BASE=0x11002000
+ CONFIG_DEBUG_UART_CLOCK=40000000
+diff --git a/configs/mt7988a_bpir4_defconfig b/configs/mt7988a_bpir4_defconfig
+new file mode 100644
+index 00000000000..e2e128ec29d
+--- /dev/null
++++ b/configs/mt7988a_bpir4_defconfig
+@@ -0,0 +1,85 @@
++CONFIG_ARM=y
++CONFIG_SYS_HAS_NONCACHED_MEMORY=y
++CONFIG_POSITION_INDEPENDENT=y
++CONFIG_ARCH_MEDIATEK=y
++CONFIG_TEXT_BASE=0x41e00000
++CONFIG_SYS_MALLOC_F_LEN=0x4000
++CONFIG_NR_DRAM_BANKS=1
++CONFIG_DEFAULT_DEVICE_TREE="mt7988a-bananapi-bpi-r4"
++CONFIG_TARGET_MT7988=y
++CONFIG_BOARD_BPI_R4=y
++CONFIG_SPL_LOAD_FIT=y
++CONFIG_SYS_LOAD_ADDR=0x46000000
++CONFIG_DEBUG_UART_BASE=0x11000000
++CONFIG_DEBUG_UART_CLOCK=40000000
++CONFIG_DEBUG_UART=y
++# CONFIG_EFI_LOADER is not set
++# CONFIG_AUTOBOOT is not set
++CONFIG_DEFAULT_FDT_FILE="mt7988a-bananapi-bpi-r4"
++CONFIG_SYS_CBSIZE=512
++CONFIG_SYS_PBSIZE=1049
++CONFIG_LOGLEVEL=7
++CONFIG_LOG=y
++CONFIG_SYS_PROMPT="BPI-R4> "
++# CONFIG_BOOTM_NETBSD is not set
++# CONFIG_BOOTM_PLAN9 is not set
++# CONFIG_BOOTM_RTEMS is not set
++# CONFIG_BOOTM_VXWORKS is not set
++# CONFIG_CMD_ELF is not set
++CONFIG_CMD_CLK=y
++CONFIG_CMD_DM=y
++CONFIG_CMD_GPIO=y
++CONFIG_CMD_PWM=y
++CONFIG_CMD_MMC=y
++CONFIG_CMD_MTD=y
++CONFIG_CMD_PING=y
++CONFIG_CMD_SMC=y
++CONFIG_DOS_PARTITION=y
++CONFIG_EFI_PARTITION=y
++CONFIG_PARTITION_TYPE_GUID=y
++CONFIG_ENV_VARS_UBOOT_RUNTIME_CONFIG=y
++CONFIG_USE_IPADDR=y
++CONFIG_IPADDR="192.168.1.1"
++CONFIG_USE_NETMASK=y
++CONFIG_NETMASK="255.255.255.0"
++CONFIG_USE_SERVERIP=y
++CONFIG_SERVERIP="192.168.1.2"
++CONFIG_PROT_TCP=y
++CONFIG_NET_RANDOM_ETHADDR=y
++CONFIG_REGMAP=y
++CONFIG_SYSCON=y
++CONFIG_CLK=y
++CONFIG_MMC_HS200_SUPPORT=y
++CONFIG_MMC_MTK=y
++CONFIG_MTD=y
++CONFIG_DM_MTD=y
++CONFIG_MTD_SPI_NAND=y
++CONFIG_DM_SPI_FLASH=y
++CONFIG_SPI_FLASH_SFDP_SUPPORT=y
++CONFIG_SPI_FLASH_EON=y
++CONFIG_SPI_FLASH_GIGADEVICE=y
++CONFIG_SPI_FLASH_ISSI=y
++CONFIG_SPI_FLASH_MACRONIX=y
++CONFIG_SPI_FLASH_SPANSION=y
++CONFIG_SPI_FLASH_STMICRO=y
++CONFIG_SPI_FLASH_WINBOND=y
++CONFIG_SPI_FLASH_XMC=y
++CONFIG_SPI_FLASH_XTX=y
++CONFIG_SPI_FLASH_MTD=y
++CONFIG_PHY_FIXED=y
++CONFIG_MEDIATEK_ETH=y
++CONFIG_PINCTRL=y
++CONFIG_PINCONF=y
++CONFIG_PINCTRL_MT7988=y
++CONFIG_POWER_DOMAIN=y
++CONFIG_MTK_POWER_DOMAIN=y
++CONFIG_DM_PWM=y
++CONFIG_PWM_MTK=y
++CONFIG_RAM=y
++CONFIG_DM_SERIAL=y
++CONFIG_MTK_SERIAL=y
++CONFIG_SPI=y
++CONFIG_DM_SPI=y
++CONFIG_MTK_SPIM=y
++CONFIG_LZO=y
++CONFIG_HEXDUMP=y
+--
+2.43.0
+
diff --git a/utils/mkimage.sh b/utils/mkimage.sh
index 98eace9de..41698182b 100755
--- a/utils/mkimage.sh
+++ b/utils/mkimage.sh
@@ -259,6 +259,50 @@ download_bootloader()
log "Bootloader ready in temporary directory"
}
+# Build EN8811H firmware partition image as an ext4 filesystem containing
+# EthMD32.dm.bin and EthMD32.DSP.bin. Returns 0 when created, 1 when source
+# files are missing.
+prepare_en8811h_fw_image()
+{
+ src_dir="$1"
+ out_img="$2"
+ dm_fw="${src_dir}/EthMD32.dm.bin"
+ dsp_fw="${src_dir}/EthMD32.DSP.bin"
+ fw_staging=""
+
+ [ -f "$dm_fw" ] && [ -f "$dsp_fw" ] || return 1
+
+ fw_staging=$(mktemp -d)
+ cp "$dm_fw" "$fw_staging/EthMD32.dm.bin"
+ cp "$dsp_fw" "$fw_staging/EthMD32.DSP.bin"
+
+ rm -f "$out_img"
+ truncate -s 2M "$out_img"
+
+ if command -v mke2fs >/dev/null 2>&1; then
+ mke2fs -q -t ext4 -L en8811h_fw -d "$fw_staging" "$out_img"
+ elif command -v mkfs.ext4 >/dev/null 2>&1; then
+ mkfs.ext4 -q -L en8811h_fw -d "$fw_staging" "$out_img"
+ else
+ rm -rf "$fw_staging"
+ die "mke2fs/mkfs.ext4 not found, cannot create en8811h-fw.bin"
+ fi
+
+ rm -rf "$fw_staging"
+ return 0
+}
+
+maybe_prepare_en8811h_fw_image()
+{
+ out_img="${BINARIES_DIR}/en8811h-fw.bin"
+
+ [ -f "$out_img" ] && return 0
+
+ prepare_en8811h_fw_image "${ROOT_DIR}/images/airoha" "$out_img" || \
+ prepare_en8811h_fw_image "${ROOT_DIR}/airoha" "$out_img" || \
+ prepare_en8811h_fw_image "${BINARIES_DIR}/airoha" "$out_img" || return 0
+}
+
# Discover boot files for Raspberry Pi boot partition
# Scans rpi-firmware directory and builds file list for genimage
discover_rpi_boot_files()
@@ -519,6 +563,11 @@ if [ -n "$DOWNLOAD_BOOT" ]; then
fi
fi
+if [ "$BOARD" = "bananapi-bpi-r3" ]; then
+ maybe_prepare_en8811h_fw_image
+ [ -f "${BINARIES_DIR}/en8811h-fw.bin" ] || die "Missing EN8811H firmware image: ${BINARIES_DIR}/en8811h-fw.bin"
+fi
+
# Template expansion
log "Generating genimage configuration for $BOARD..."