ich habe ein Problem mit einem ESP32, dem Entwicklungsframework ESP-IDF v3 und einem KSZ8863RLL Ethernet-Phy.
Kurzfassung: Das ganze Funktioniert nicht.
Langfassung mit Kontext:
Ich habe ein eigenes Board für [url=https://kno.wled.ge/]WLED[/url] gebaut (siehe Anhang). Da drauf ist ein ESP32-WROVER-E und ein KSZ8863RLL 3Port Ethernetswitch / Phy.
Ziel der Übung ist es Lampen zu bauen, die man auf Ethernet-Seite kaskadieren kann ohne einen zentralen Switch zu benötigen, sowas ich üblich bei Bühnentechnik.
WLED ist ein PlatformIO-Projekt und basiert auf dem Arduino-Framework. Intern basiert das Arduino-Framework auf dem ESP-IDF v3. ESP-IDF v3 (und damit Arduino) in der Version unterstützt leider nur sehr wenige Phys, keiner davon mit integriertem Switch.
Im ESP-IDF v5 gibt es ein Beispiel wie man einen KSZ8863 anspricht, das Beispiel lässt sich bauen und funktioniert auf meinem Board -> die Hardware ist in Ordnung. Leider sind die Unterschiede zwischen v3 und v5 enorm, der Treiber lässt sich quasi nicht portieren. WLED/Arduino lässt sich auch nicht auf das ESP-IDF v5 hochziehen, da gibt es politische Verwerfungen zwischen PlatformIO und Espressif und alles stinkt zum Himmel. Wenn man das versucht explodiert das PlatformIO-Buildsystem und kotzt einem Python Stacktraces vor die Füße.
Einfach einen anderen Phy angeben funktioniert nicht, die Register vom KSZ8863 sind zu unterschiedlich, außerdem lässt sich der Chip über SMI nochtmal komplett ansprechen. Nur über I2C oder SPI kann man alle Register erreichen.
Der KSZ8863RLL hat drei Ports, Port 1+2 sind normale 100Mbps Ethernet-Ports, Port 3 ist über RMII zu erreichen und hängt an meinem ESP32.
ESP-IDF v3 hat auch eine Schnittstelle um eigene Treiber zu integrieren, also will ich mich da reinhängen. Da das nicht direkt in WLED ist sondern noch der Arduino-Core dazwischen ist habe ich Angefangen den Arduino-Core zu modifizierne, genauer gesagt die ETHClass.
In der Methode ETHClass::begin gibt es den folgenden Code der die konfigurierbaren Phys durchgeht, dort haben ich meinen KSZ8863 reingehängt:
Ab Zeile 17 gehen sie die verschiedenen Phy-Typen durch. "phy_lan8720_default_ethernet_config" sind vorgefertige Strukturen vom IDF-Framework.
Ab zeile 28 fängt mein Code an, ich nehme erst die Default-Config vom ip101, dann überschreibe ich die ganzen Funktionspointer die die Struktur hat.
Am Ende landet alles in eth_config, dann kommen noch die dynamischen Teile von WLED dazu (Zeile 46-49).
In Zeile 56 wird der TcpIP-Stack initialisiert (das ist wieder Arduino-Code, an dem habe ich nicht rumgefummelt, in Zeile 57 wird die EMAC-Hardware vom ESP32 initialisiert.
Das ganze läuft ohne Fehlermeldung durch.
Hier sind noch die Funktionen, die ich dem IDF-Framework unterschiebe:
1
uint8_tksz_read_reg(uint8_treg)
2
{
3
Wire.beginTransmission(KSZ8863_ADR);
4
Wire.write(reg);//register
5
Wire.endTransmission();
6
Wire.requestFrom(KSZ8863_ADR,1);
7
uint8_treg1=Wire.read();// read that byte into 'slaveByte2' variable
8
9
returnreg1;
10
}
11
12
13
14
esp_err_tksz_phy_init(void)
15
{
16
phy_mii_enable_flow_ctrl();
17
returnESP_OK;
18
}
19
20
boolksz_phy_check_link(void)
21
{
22
uint8_tport1_status_0=ksz_read_reg(30);
23
uint8_tport2_status_0=ksz_read_reg(46);
24
25
if(port1_status_0&(1<<5))
26
Serial.printf("Port 1 Link good\n");
27
else
28
Serial.printf("Port 1 Link bad\n");
29
30
if(port2_status_0&(1<<5))
31
Serial.printf("Port 2 Link good\n");
32
else
33
Serial.printf("Port 2 Link bad\n");
34
35
36
// for(int i=0; i < 240; i++)
37
// Serial.printf("Read register %d: 0x%02X\n", i, ksz_read_reg(i));
38
39
returntrue;
40
}
41
42
voidksz_phy_check_init(void)
43
{
44
45
}
46
47
eth_speed_mode_tksz_phy_get_speed_mode(void)
48
{
49
returnETH_SPEED_MODE_100M;
50
}
51
52
eth_duplex_mode_tksz_phy_get_duplex_mode(void)
53
{
54
returnETH_MODE_FULLDUPLEX;
55
}
56
57
boolksz_phy_get_partner_pause_enable(void)
58
{
59
returnfalse;
60
}
61
62
voidksz_phy_power_enable(boolenable)
63
{
64
65
}
Wie man sieht sehr spartanisch. Zum Beispiel kann der Link gar nicht abbrechen da er ja direkt mit einem Switch verbunden ist, außerdem habe ich erstmal die Geschwindigkeit auf 100Mbps gepinnt.
Ich hab den Sourcecode für die im IDF-Framework enthaltenen Phys durchgeschaut, aber die konfigurieren wirklich nur den Phy über SMI und drehen offenbar nicht an der ESP32-EMAC-Hardware rum.
Außerdem habe ich einen Registerdump gemacht und per Hand mit dem KSZ8863-Datenblatt abgeglichen, dabei habe ich keine "Verkonfigurierte" Option gefunden. Ich gehe daher davon aus, dass die EMAC-Config und nicht die Phy-Config das Problem ist.
Jetzt das beobachtete Verhalten:
Switching zwischen Port 1 und Port 2 funktioniert, ich kann auf Port 1 einen DHCP-Server anklemmen und auf Port 2 einen Client, das sieht sich und funktioniert.
Dem ESP32 habe ich eine feste IP gegeben, das funktioniert scheinbar auch.
Laptop am Port 1 (192.168.69.10), ESP32 fixe IP 192.168.69.100)
Ping vom Laptop zum PC, monitor mit Wireshark zeigt: Keine Antwort auf den ARP Request.
Mit dem Oszi das RMII Interface anschauen:
RMII Clock läuft (kommt aus GPIO0 raus, in den PHY)
Die Datenleitungen vom Phy zum ESP32 zeigen aktivität
Auf den Datenleitungen vom ESP32 zum Phy ist tote Hose
Der TCPIP-Stack wird nicht aufgerufen.
Das ganze Baubar zu Verfügung zu stellen ist leider nicht ganz einfach weil es über mehrere Repos (WLED, Arduino) verstreut ist, das müsste ich erst Forken und selber Hosten...
Ich bin nicht sicher ob es gut ist auf 100 MBit zu fixieren. Habe ich auch mal mit einem einfach PHY gemacht, da musste der Switch aber auch auf 100 Mbit fix eingestellt werden, mit Auto haben die sich nicht verstanden.
Ich habe jetzt mal nochmal zum Testen das ganze WLED/Arduino-Theater weggelassen und direkt das Espressif Beispiel angepasst.
Jetzt passt das ganze in 300 Zeilen (im Anhang), baubar mit ESP-IDF v3.3.6
Darin habe ich jetzt auch get_speed_mode() und get_duplex_mode() richtig implementiert. Geändert hat sich am Verhalten leider gar nichts.
Im log sehe ich
1
I (465) KSZ: ksz_phy_init
2
W (475) KSZ: Except next write to fail because of SW reset
3
I (575) I2C: i2c_master_cmd_begin failed
4
E (575) I2C: I2C_read failed
5
I (575) KSZ: ChipID ok
6
I (575) emac: emac reset done
7
I (575) KSZ: Port 3 speed mode: 100M
8
I (575) eth_example: Ethernet Started
9
I (2575) eth_example: Port 1 Link good
10
11
I (2575) eth_example: Port 2 Link good
12
13
I (2585) KSZ: Port 3 speed mode: Full Duplex
14
I (2585) KSZ: Port 3 speed mode: 100M
15
I (2595) KSZ: Port 3 speed mode: Full Duplex
16
I (2595) KSZ: Port 3 speed mode: 100M
17
I (2605) KSZ: Port 3 speed mode: Full Duplex
18
I (2605) KSZ: Port 3 speed mode: 100M
19
I (2615) KSZ: Port 3 speed mode: Full Duplex
20
I (2615) KSZ: Port 3 speed mode: 100M
21
I (2625) KSZ: Port 3 speed mode: Full Duplex
22
I (2625) KSZ: Port 3 speed mode: 100M
23
I (2635) KSZ: Port 3 speed mode: Full Duplex
24
I (2635) KSZ: Port 3 speed mode: 100M
25
I (2635) eth_example: Ethernet Link Up
26
I (4575) eth_example: Port 1 Link good
27
28
I (4575) eth_example: Port 2 Link good
Die wichtigen Zeilen hier sind:
I (575) eth_example: Ethernet Started
I (2635) eth_example: Ethernet Link Up
Danach würde ich eigentlich ein SYSTEM_EVENT_ETH_GOT_IP event erwarten (Ausgabe "Ethernet Got IP Addr"), das kommt aber nie. Die RMII-Leitungen mit der Datenrichtung ESP32 -> PHY bleiben tot.
Circa 1x pro Sekunde fragt er den Status der beiden Ports ab.
ja ok, jetzt habe ich es auch verstanden.
Nach dem PHY init sollte der Emac Code aber nichts mehr mit dem PHY zu tun haben, bis auf das Pollen des Linkstatus. Und das liefert ja Link up.
Der KSZ benutzt ja sicher auch so strapping bits, wird der wirklich mit Autonegotation initialsiert? Oder andere Einstellungen durch diese Straps falsch gesetzt?
Kannst du den Inhalt von tcpip_adapter_eth_input zwischen v3 / v5 vergleichen? Das ist ja die Routine die vom Interrupt aufgerufen wird wenn Daten angekommen sind. Oder ist die Interrupt Aktivierung jetzt anders oder fehlt?
Oder eine Funktion dazwischensetzen,
config.tcpip_input = my_tcpip_adapter_eth_input;
my_tcpip_adapter_eth_input() dann einen Portpin toggeln lassen und dann die richtige Funktion aufrufen um zu sehen ob es bis dahin kommt.
Der KSZ benutzt ja sicher auch so strapping bits, wird der wirklich mit
Autonegotation initialsiert? Oder andere Einstellungen durch diese
Straps falsch gesetzt?
Hmmm, ja, da wird einiges mit strapping bits gemacht, aber nur für Port 1/2 (die, an die RJ45 Buchsen kommen).
Für Port3 sind z.b. die ganzen Autonegotiation-Bits "Reserved, Not Applicable to Port 3"
Macht ja auch Sinn (aus Switch-Sicht), Port3 hat ja nur RMII und an keiner Stelle physikalische Ethernet-Pegel.
Kannst du den Inhalt von tcpip_adapter_eth_input zwischen v3 / v5
vergleichen?
tcpip_adapter_eth_input wird in meinem Fall nie aufgerufen.
Interrupt-Aktivierung... guter Punkt, da muss ich wohl direkt mal im ESP32 auf die Register schauen. Eigentlich (tm) hatte ich erwartet, dass das in Beispiel drin ist (entweder in tcpip_adapter_init(), esp_eth_init() oder esp_eth_enable().
Dann bin ich da mit meinem Halbwissen zum ESP auch am Ende.
D.h. sowas wie
eth_config_t config;
gibt es in der STM HAL ja auch reichlich. Da bin ich mal darauf reingefallen das die Struktur geändert wurde und in meinem Code nicht alle Member gesetzt wurden. Wenn die Struktur auf dem Stack liegt, dann hat die zufälligen Inhalt. Gibt es eine init Funktion um die Struktur zu löschen? Sonst mit memset auf 0 setzen.
edit:
Das wird es auch nicht sein, sieht so aus das die Struktur in v3 gleich ist. in v5 ist einiges umbenannt.
Es aktiviert im emac die Bits TIE, RIE, RBUE und NISE
(Transmit interrupt, Receive Interrupt, Receive Buffer unavailable interrupt und normal interrupts enable).
Leider geht noch immer kleine Kommunikation, aber jetzt wird zumindest der TCP/IP-Stack aufgerufen und ich sehe Daten aufm dem Oszi in beide Richtungen.
Ich konnte das Problem lösen, es bekommt jetzt zumindest eine IP :)
Ich verstehe nur noch nicht warum es funktioniert.
Die Lösung war das Bit 3 im Register 197, "P3 RMII Clock Selection: Internal".
Nicht funktionierend stand es auf "External". Meiner Meinung nach ist "External" auch richtig, schließlich habe ich dem ESP gesagt, dass er den Takt ausgeben soll. (mode GPIO0_OUT).
GPIO0_OUT ist auch der richtige Modus für den ESP, wenn ich das testweise auf "IN" umstelle geht es nicht mehr.
also "GPIO00_OUT" und "RMII CLock selection Internal" ist die richtige Kombination. Auch vorher war aber ein Takt messbar...
Jetzt bekommt es eine IP und das Webinterface lädt auch, aber irgendwas hat noch ziemlich Packagedrop :<
Ich hoffe, dass das nicht an der Signalintegrität aufm RMII-Interface liegt.
also "GPIO00_OUT" und "RMII CLock selection Internal" ist die richtige
Kombination. Auch vorher war aber ein Takt messbar...
Und auch dieses Rätsel ist gelöst, es war noch ein Hardwarefehler mit dem Takt - in der Kombination haben ESP32 und Phy den RMII Takt generiert, natürlich nicht synchron - so kommt man auf (temperaturabhängige) 5-25% Paketverlust.