ESP32 IDFv3 Ethernet-Phy KSZ8863 funktioniert nicht

OP #7732159
Lesenswert?

Hi,

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:

1
bool ETHClass::begin(uint8_t phy_addr, int power, int mdc, int mdio, eth_phy_type_t type, eth_clock_mode_t clock_mode)
2
{
3
    esp_err_t err;
4
    if(initialized){
5
        err = esp_eth_enable();
6
        if(err){
7
            Serial.printf("esp_eth_enable error: %d", err);
8
            return false;
9
        }
10
        started = true;
11
        return true;
12
    }
13
    _eth_phy_mdc_pin = mdc;
14
    _eth_phy_mdio_pin = mdio;
15
    _eth_phy_power_pin = power;
16

17
    if(type == ETH_PHY_LAN8720){
18
        eth_config_t config = phy_lan8720_default_ethernet_config;
19
        memcpy(&eth_config, &config, sizeof(eth_config_t));
20
    } else if(type == ETH_PHY_TLK110){
21
        eth_config_t config = phy_tlk110_default_ethernet_config;
22
        memcpy(&eth_config, &config, sizeof(eth_config_t));
23
    } else if(type == ETH_PHY_IP101) {
24
      eth_config_t config = phy_ip101_default_ethernet_config;
25
      memcpy(&eth_config, &config, sizeof(eth_config_t));
26
    } else if(type == ETH_PHY_KSZ8863) {
27

28
        eth_config_t config = phy_ip101_default_ethernet_config;
29
        
30
        config.phy_init = ksz_phy_init;
31
        config.phy_check_link = ksz_phy_check_link;
32
        config.phy_check_init = ksz_phy_check_init;
33
        config.phy_get_speed_mode = ksz_phy_get_speed_mode;
34
        config.phy_get_duplex_mode = ksz_phy_get_duplex_mode;
35
        config.phy_get_partner_pause_enable = ksz_phy_get_partner_pause_enable;
36
        config.phy_power_enable = ksz_phy_power_enable;
37
        
38

39
        memcpy(&eth_config, &config, sizeof(eth_config_t));
40

41
    } else {
42
        Serial.printf("Bad ETH_PHY type: %u", (uint8_t)type);
43
        return false;
44
    }
45

46
    eth_config.phy_addr = (eth_phy_base_t)phy_addr;
47
    eth_config.clock_mode = clock_mode;
48
    eth_config.gpio_config = ksz_phy_config_gpio; //_eth_phy_config_gpio;
49
    eth_config.tcpip_input = tcpip_adapter_eth_input;
50

51
    if(_eth_phy_power_pin >= 0){
52
        _eth_phy_power_enable_orig = eth_config.phy_power_enable;
53
        eth_config.phy_power_enable = _eth_phy_power_enable;
54
    }
55

56
    tcpipInit();
57
    err = esp_eth_init(&eth_config);
58
    if(!err){
59
        initialized = true;
60
        err = esp_eth_enable();
61
        if(err){
62
            Serial.printf("esp_eth_enable error: %d", err);
63
        } else {
64
            Serial.printf("esp_eth_enabled done!");
65
            started = true;
66
            return true;
67
        }
68
    } else {
69
        Serial.printf("esp_eth_init error: %d", err);
70
    }
71
    return false;
72
}

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_t ksz_read_reg(uint8_t reg)
2
{
3
    Wire.beginTransmission(KSZ8863_ADR);
4
    Wire.write(reg); //register
5
    Wire.endTransmission();
6
    Wire.requestFrom(KSZ8863_ADR,1);
7
    uint8_t reg1 = Wire.read();        // read that byte into 'slaveByte2' variable
8

9
    return reg1;
10
}
11

12

13

14
esp_err_t ksz_phy_init(void)
15
{
16
    phy_mii_enable_flow_ctrl();
17
    return ESP_OK;
18
}
19

20
bool ksz_phy_check_link(void)
21
{
22
    uint8_t port1_status_0 = ksz_read_reg(30);
23
    uint8_t port2_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
    return true;
40
}
41

42
void ksz_phy_check_init(void)
43
{
44

45
}
46

47
eth_speed_mode_t ksz_phy_get_speed_mode(void)
48
{
49
    return ETH_SPEED_MODE_100M;
50
}
51

52
eth_duplex_mode_t ksz_phy_get_duplex_mode(void)
53
{
54
    return ETH_MODE_FULLDUPLEX;
55
}
56

57
bool ksz_phy_get_partner_pause_enable(void)
58
{
59
    return false;
60
}
61

62
void ksz_phy_power_enable(bool enable)
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...

Hat jemand eine Idee?

Angehängte Dateien:
OP #7732202
Lesenswert?

Monk schrieb:

an, insbesondere Kapitel 4.2, weil mir da eine Abweichung aufgefallen ist.

Das stimmt, allerdings habe ich überstehende Boards bei ESPs noch nie gesehen - aber wenn man WLAN benutzen möchte bestimmt ein guter Tipp.

Zu Ethernet / RMII findet sich jedoch nix.

J. S. schrieb:

Ich bin nicht sicher ob es gut ist auf 100 MBit zu fixieren.

Hm, ich sehe da kein Problem, weil der Switch ja noch direkt mit auf dem Board ist. Außerdem habe ich die Register des Phys gechecked, da steht

1
Register 63 fürs RMII Interface:
2
-> TX flow control active
3
-> RX flow control active
4
-> link speed 100Mbps
5
-> Full-Duplex operation

Also auch wenn ich das return 100Mbps; durch eine Register-Abfrage ersetze wird trotzdem 100Mbps raus kommen. Ich baue es gleich mal um

OP #7732274
Lesenswert?

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.

Angehängte Dateien:
#7732288
Lesenswert?

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?

#7732292
Lesenswert?

Torben H. schrieb:

Monk schrieb:

an, insbesondere Kapitel 4.2, weil mir da eine Abweichung aufgefallen ist.

Das stimmt, allerdings habe ich überstehende Boards bei ESPs noch nie gesehen - aber wenn man WLAN benutzen möchte bestimmt ein guter Tipp.

Wer wlan braucht könnte ja auch das Trägerboard an der Antenne vom ESP ausklinken, dann steht nichts über.

#7732297
Lesenswert?

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.

OP #7732300
Lesenswert?

J. S. schrieb:

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.

J. S. schrieb:

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().

OP #7732305
Lesenswert?

Jap, wirds aber nicht, ist tot. Daher funktioniert der IP-Stack nicht...

Ich hab gerade mal eine eigene Funktion dazwischen gesetzt:

1
esp_err_t my_tcpip_adapter_eth_input(void *buffer, uint16_t len, void *eb)
2
{
3
    ESP_LOGI("TCP", "TCP daten!");
4
    return tcpip_adapter_eth_input(buffer, len, eb);
5
}
6

7
[...]
8
config.tcpip_input = my_tcpip_adapter_eth_input;

Die Ausgabe "TCP Daten!" kommt nie, also wird der TCP/IP-Stack nie gerufen. Was dazu passt, dass die emac-Config im Eimer ist.

#7732310
Lesenswert?

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.

Aber in emac_start werden die Ints aktiviert, in https://github.com/espressif/esp-idf/blob/b64b3752342a23469ada0188d4838a4fb96fe172/components/ethernet/emac_main.c#L834

Das nutzt das REG_WRITE Macro was ja wahrscheinlich SMI benutzt, SMI muss also auch funktionieren.

OP #7732316
Lesenswert?

J. S. schrieb:

Das nutzt das REG_WRITE Macro was ja wahrscheinlich SMI benutzt, SMI muss also auch funktionieren.

Ne, REG_WRITE schreibt ein Hardwareregister vom ESP32, in diesem Fall DMAIN_EN_REG (Seite https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf )

Es aktiviert im emac die Bits TIE, RIE, RBUE und NISE (Transmit interrupt, Receive Interrupt, Receive Buffer unavailable interrupt und normal interrupts enable).

J. S. schrieb:

Das wird es auch nicht sein,

Jap, leider nicht. Ich hab direkt die eth_config_t -Struktur ausm Header kopiert und dann Zeile für Zeile gefüllt.

OP #7732368
Lesenswert?

Ha! Erkenntnis!

Ich hab das IO-Mapping ausversehen rausgeworfen. So waren die RMII-Pins nicht mehr mit dem emac verbunden und alles ging ins leere:

1
    // CRS_DRV to GPIO27
2
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO27_U, FUNC_GPIO27_EMAC_RX_DV);
3
    // TXD0 to GPIO19
4
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO19_U, FUNC_GPIO19_EMAC_TXD0);
5
    // TX_EN to GPIO21
6
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO21_U, FUNC_GPIO21_EMAC_TX_EN);
7
    // TXD1 to GPIO22
8
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO22_U, FUNC_GPIO22_EMAC_TXD1);
9
    // RXD0 to GPIO25
10
    gpio_set_direction(25, GPIO_MODE_INPUT);
11
    // RXD1 to GPIO26
12
    gpio_set_direction(26, GPIO_MODE_INPUT);
13
    // RMII CLK to GPIO0
14
    gpio_set_direction(0, GPIO_MODE_INPUT);

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.

Fortschritt! :)

OP #7733465
Lesenswert?

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.

OP #7743522
Lesenswert?

Torben H. schrieb:

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.

Jetzt funktioniert alles, die Software gibts hier: Einmal der modifizierte Arduino-Core mit KSZ8863-Treiber: https://github.com/Bauteiltoeter/arduino-esp32-ksz8863

Und einmal das modifizierte WLED, verlinkt auf den oben genannten Arduino-Core und mit "KSZ8863"-Option im Webinterface: https://github.com/Bauteiltoeter/WLED/tree/add-ksz8863-support

Vielen Dank für die Hilfe hier, Projekt erfolgreich!

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren