From 933433f40f78ccbae06185029d6493e0d83bd7aa Mon Sep 17 00:00:00 2001 From: Ondrej Kosta Date: Mon, 3 Jun 2024 17:10:21 +0200 Subject: [PATCH] feat(phy_tester): Added new Application to test custom ETH boards --- phy_tester/CMakeLists.txt | 12 + phy_tester/LICENSE | 201 ++++++++++++ phy_tester/README.md | 52 +++ phy_tester/eth_error_msg.py | 110 +++++++ phy_tester/idf_component.yml | 8 + phy_tester/main/CMakeLists.txt | 2 + phy_tester/main/cmd_ethernet.c | 511 ++++++++++++++++++++++++++++++ phy_tester/main/cmd_ethernet.h | 18 ++ phy_tester/main/eth_common.c | 208 ++++++++++++ phy_tester/main/eth_common.h | 49 +++ phy_tester/main/idf_component.yml | 20 ++ phy_tester/main/phy_tester.c | 49 +++ phy_tester/main/test_functions.c | 374 ++++++++++++++++++++++ phy_tester/main/test_functions.h | 22 ++ phy_tester/pytest_eth_phy.py | 468 +++++++++++++++++++++++++++ 15 files changed, 2104 insertions(+) create mode 100644 phy_tester/CMakeLists.txt create mode 100644 phy_tester/LICENSE create mode 100644 phy_tester/README.md create mode 100644 phy_tester/eth_error_msg.py create mode 100644 phy_tester/idf_component.yml create mode 100644 phy_tester/main/CMakeLists.txt create mode 100644 phy_tester/main/cmd_ethernet.c create mode 100644 phy_tester/main/cmd_ethernet.h create mode 100644 phy_tester/main/eth_common.c create mode 100644 phy_tester/main/eth_common.h create mode 100644 phy_tester/main/idf_component.yml create mode 100644 phy_tester/main/phy_tester.c create mode 100644 phy_tester/main/test_functions.c create mode 100644 phy_tester/main/test_functions.h create mode 100644 phy_tester/pytest_eth_phy.py diff --git a/phy_tester/CMakeLists.txt b/phy_tester/CMakeLists.txt new file mode 100644 index 0000000..4395e21 --- /dev/null +++ b/phy_tester/CMakeLists.txt @@ -0,0 +1,12 @@ +# For more information about build system see +# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(phy_tester) + +# to be able to set esp_eth verbosity to "VERBOSE" +idf_component_get_property(esp_eth_lib esp_eth COMPONENT_LIB) +target_compile_definitions(${esp_eth_lib} PUBLIC "-DLOG_LOCAL_LEVEL=ESP_LOG_VERBOSE") diff --git a/phy_tester/LICENSE b/phy_tester/LICENSE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/phy_tester/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/phy_tester/README.md b/phy_tester/README.md new file mode 100644 index 0000000..abbff71 --- /dev/null +++ b/phy_tester/README.md @@ -0,0 +1,52 @@ +# Ethernet PHY Tester + +**!!!WORK IN PROGRESS!!!** + +The Ethernet PHY tester is a tool to help you debug issues with Ethernet PHY. It's especially handy when you're in process of bringing up a custom board with Ethernet and you face issues. + +## Features + +* PHY Registry dump +* PHY Registry write +* Near-end loopback test +* Far-end loopback test +* Ethernet L2 loopback server +* Dummy Ethernet frames transmitter + +## How to use + +1) Configure PHY and Ethernet component based on your actual board's needs in sdkconfig. +2) Build & Flash. +3) Connect to the board and run commands from console (type `help` to list available commands). + +or run automatic test to identify Ethernet related issue of your board: + +1) Configure PHY and Ethernet component based on your actual board's needs in sdkconfig. +2) Build. +3) Find your network interface (NIC) name the DUT is connected to (e.g. use `ip` command). +4) Run `pytest --target=esp32 --eth_nic=YOUR_NIC_NAME` + +Note: you need `pytest` installed, see [pytest in ESP-IDF](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/contribute/esp-idf-tests-with-pytest.html) for more information. + +## Known Limitations + +* The `pytest_eth_phy.py` can be run on Linux only and you need to: + * have root permission to run the test app (since RAW socket is used) + * or set correct capacity through ``sudo setcap 'CAP_NET_RAW+eip' $(readlink -f $(which python))`` + +## Example of pytest Output +``` +loopback server: PASS +. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +. DUT . +. RMII Tr. . +. +------------------+ +---------------+ +-----------+ . +---------------+ +. | +---| | | 8|8 | | . | | +. | .-<-| M |-----Rx-----|-----------<---|------- 8|8 ------| | . | | +. | ESP32 | | A | | PHY | | RJ45 |===========| Test PC | +. | '->-| C |-----Tx-----|----------->---|------- 8|8 ------| | . | | +. | +---| | | 8|8 | | . | | +. +------------------+ +---------------+ +-----------+ . +---------------+ +. . +. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +``` \ No newline at end of file diff --git a/phy_tester/eth_error_msg.py b/phy_tester/eth_error_msg.py new file mode 100644 index 0000000..1fae7f2 --- /dev/null +++ b/phy_tester/eth_error_msg.py @@ -0,0 +1,110 @@ +# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 + +norm = '\033[0m' +red = '\033[31m' +green = '\033[32m' +italics = '\033[3m' +yellow = '\033[33m' + + +class EthFailMsg(object): + @classmethod + def print_yellow(cls, msg: str) -> None: + print(yellow + msg + norm) + + @classmethod + def print_eth_init_fail_msg(cls) -> None: + cls.print_yellow('=================================================================================') + cls.print_yellow(f'Ethernet initialization failed!') + cls.print_yellow(f'------------------------------') + cls.print_yellow(f'Possible root causes:') + cls.print_yellow(f'1) RMII REF CLK mode incorrectly configured if emac initialization timeouts. Does ESP32 outputs the RMII CLK? Or is RMII CLK ' + 'provided externally by PHY or oscillator?') + cls.print_yellow(f'2) If external RMII CLK is used, measure the clock signal at ESP32 REF RMII CLK input pin using oscilloscope with sufficient ' + 'bandwidth. There must be 50 MHz square wave signal.') + # TODO make it target dependent + cls.print_yellow(f'3) Make sure programmer/monitor device correctly handles “nDTR”/”nRST” and associated transistors which are connected to GPIO0.') + + cls.print_yellow(f'4) If PHY error reported: check if bootstrap (PHY address) is set correctly or set to "auto" in the code.') + cls.print_yellow('=================================================================================') + + @classmethod + def print_link_up_fail_msg(cls) -> None: + cls.print_yellow('=================================================================================') + cls.print_yellow(f'Link failed to up!') + cls.print_yellow(f'------------------') + cls.print_yellow(f'Possible root causes:') + cls.print_yellow(f'1) DUT not connected. Check if the Ethernet cable from your test PC is properly connected to DUT.') + cls.print_yellow(f'2) PHY is not able to negotiate a link:') + cls.print_yellow(f' a) incorrect configuration of test PC NIC') + cls.print_yellow(f' b) HW problem in DUT at path between the PHY and RJ45') + cls.print_yellow('=================================================================================\n') + + @classmethod + def print_link_stability_fail_msg(cls) -> None: + cls.print_yellow('=================================================================================') + cls.print_yellow(f'Ethernet link is not stable!') + cls.print_yellow(f'---------------------------') + cls.print_yellow('=================================================================================') + # TODO crate a test + # - Something connected to GPIO0 (like a programmer via wires) can cause signal integrity issues. Link oscillate UP/Down. + # - if ESP32 is configured as source of RMII REF CLK and Wi-Fi is used at the same time TBD!!! + + @classmethod + def print_rmii_fail_msg(cls, is_rx: bool, is_tx: bool) -> None: + cls.print_yellow('=================================================================================') + cls.print_yellow(f'The RMII data pane non functional!') + cls.print_yellow(f'----------------------------------') + receive = '' + rx_gpio = '' + transmit = '' + tx_gpio = '' + if is_rx: + receive = 'receive' + rx_gpio = 'RXD[0,1]' + + if is_tx: + transmit = 'transmit' + tx_gpio = 'TXD[0,1]' + + _and = '' + if is_tx and is_rx: + _and = ' and ' + + cls.print_yellow(f'The DUT is not able to {receive}{_and}{transmit} Ethernet frames. The issue needs to be probed on the board.') + cls.print_yellow(f'* Identify location of {rx_gpio}{_and}{tx_gpio} pins on your board at ESP32 side and at PHY side.') + cls.print_yellow(f'* Go over all identified pins. Connect the first oscilloscope probe at the ESP32 pin side and the other probe at PHY side (expect ' + 'signal with frequency of 25 MHz when in 100mbps mode).') + if is_tx: + cls.print_yellow(f'\n-- Transmit path steps: --') + cls.print_yellow(f' * Flash `phy_tester` tool to DUT and connect to it using `monitor`') + cls.print_yellow(f' * Run `dummy-tx` command in DUT console. Appropriately configure the interval and number of transmission so you have enough ' + 'time to perform measurement.') + cls.print_yellow(f' * The DUT now transmits frames and you should see the same signal at both ESP32 and PHY sides. If you do not see it at PHY ' + 'side, the trace is corrupted. It could be missing or having incorrect value of series termination resistor or incorrect ' + 'tracing.') + cls.print_yellow(f' * If signal at {tx_gpio} is OK, check TX_EN signal path.') + + if is_rx: + cls.print_yellow(f'\n-- Receive path steps: --') + cls.print_yellow(f' * Run `python pytest_eth_phy.py --eth_nic=NIC_NAME dummy-tx` command in test PC console. Appropriately configure the interval ' + 'and number of transmission so you have enough time to perform measurement.') + cls.print_yellow(f' * The Test PC now transmits frames and you should see the same signal at both PHY and ESP32 sides. If you do not see it at ' + 'ESP32 side, the trace is corrupted. It could be missing or having incorrect value of series termination resistor or incorrect ' + 'tracing.') + + if is_rx and is_tx: + cls.print_yellow(f'Since both receive and transmit is not possible, check also CRS_DV signal path if data signals look good.') + + cls.print_yellow('=================================================================================') + + @classmethod + def print_rj45_fail_msg(cls, is_rx: bool, is_tx: bool) -> None: + cls.print_yellow('=================================================================================') + cls.print_yellow(f'Path between PHY and RJ45 non functional!') + cls.print_yellow(f'----------------------------------------') + cls.print_yellow(f'* Double check the design checklist if you used correct components and the design is correct.') + cls.print_yellow(f'* Check the trace is not corrupted.') + cls.print_yellow(f'* Check the components are correctly soldered and correct Part Numbers (PNs) are mounted.') + cls.print_yellow('=================================================================================') diff --git a/phy_tester/idf_component.yml b/phy_tester/idf_component.yml new file mode 100644 index 0000000..46860e2 --- /dev/null +++ b/phy_tester/idf_component.yml @@ -0,0 +1,8 @@ +version: "0.1.0" +targets: + - esp32 + - esp32p4 +description: Ethernet PHY Tester +url: https://github.com/espressif/esp-eth-drivers/tree/master/phy_tester +dependencies: + idf: ">=5.0.6" \ No newline at end of file diff --git a/phy_tester/main/CMakeLists.txt b/phy_tester/main/CMakeLists.txt new file mode 100644 index 0000000..9958582 --- /dev/null +++ b/phy_tester/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "phy_tester.c" "cmd_ethernet.c" "test_functions.c" "eth_common.c" + INCLUDE_DIRS ".") diff --git a/phy_tester/main/cmd_ethernet.c b/phy_tester/main/cmd_ethernet.c new file mode 100644 index 0000000..b215fb4 --- /dev/null +++ b/phy_tester/main/cmd_ethernet.c @@ -0,0 +1,511 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_log.h" +#include "esp_console.h" +#include "esp_bit_defs.h" +#include "argtable3/argtable3.h" +#include "esp_event.h" +#include "esp_err.h" +#include "esp_check.h" +#include "test_functions.h" +#include "eth_common.h" +#include "ethernet_init.h" + +static const char *TAG = "eth_phy_tester_cmd"; + +static esp_eth_handle_t *s_eth_handles; +static uint8_t s_eth_port_cnt; + +static char *supported_phys[PHY_ID_END] = { + "IP101", /* PHY_IP101, */ + "LAN87XX", /* PHY_LAN87XX */ + "KSZ80XX", /* PHY_KSZ80XX */ + "RTL8201", /* PHY_RTL8201 */ + "DP83848" /* PHY_DP83848 */ +}; + +static struct { + struct arg_str *info; + struct arg_lit *read; + struct arg_lit *write; + struct arg_int *addr; + struct arg_int *decimal; + struct arg_str *hex; + struct arg_end *end; +} phy_control_args; + +/* "dump_regs" command */ +static struct { + struct arg_lit *dump_802_3; + struct arg_int *dump_range_start; + struct arg_int *dump_range_stop; + struct arg_end *end; +} phy_dump_regs_args; + +static struct { + struct arg_lit *enable; + struct arg_lit *disable; + struct arg_end *end; +} phy_farend_loopback_args; + +static struct { + struct arg_lit *enable; + struct arg_lit *disable; + struct arg_end *end; +} phy_nearend_loopback_args; + +static struct { + struct arg_int *length; + struct arg_int *count; + struct arg_int *interval; + struct arg_lit *verbose; + struct arg_end *end; +} phy_nearend_loopback_test_args; + +static struct { + struct arg_int *length; + struct arg_int *count; + struct arg_int *interval; + struct arg_lit *verbose; + struct arg_end *end; +} dummy_transmit_args; + +static struct { + struct arg_int *timeout_ms; + struct arg_str *eth_type_filter; + struct arg_lit *verbose; + struct arg_end *end; +} loop_server_args; + +static struct { + struct arg_int *verbosity; + struct arg_end *end; +} verbosity_args; + +// TODO +static esp_err_t check_eth_state(void) +{ + if (s_eth_handles == NULL) { + ESP_LOGW(TAG, "Ethernet init failed, command is not available"); + return ESP_ERR_INVALID_STATE; + } + return ESP_OK; +} + +static esp_err_t check_phy_reg_addr_valid(int addr) +{ + if (addr >= 0 && addr <= 31) { + return ESP_OK; + } + ESP_LOGE(TAG, "invalid PHY register address range"); + return ESP_FAIL; +} + +static int phy_cmd_control(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&phy_control_args); + if (nerrors != 0) { + arg_print_errors(stderr, phy_control_args.end, argv[0]); + return 1; + } + + if (!strncmp(phy_control_args.info->sval[0], "info", 4)) { + // Get the device information of the ethernet handle + eth_dev_info_t eth_info = ethernet_init_get_dev_info(s_eth_handles[0]); + + printf("--- PHY Chip under Test ---\n"); + printf("Name: %s\n", eth_info.name); + printf("MCD pin: %u\n", eth_info.pin.eth_internal_mdc); + printf("MDIO pin: %u\n", eth_info.pin.eth_internal_mdio); + } + + if (phy_control_args.read->count != 0) { + if (phy_control_args.addr->count != 0) { + if (check_phy_reg_addr_valid(phy_control_args.addr->ival[0]) != ESP_OK) { + return 1; + } + dump_phy_regs(s_eth_handles[0], phy_control_args.addr->ival[0], phy_control_args.addr->ival[0]); + } else { + ESP_LOGE(TAG, "register address is missing"); + } + } + + if (phy_control_args.write->count != 0) { + if (phy_control_args.addr->count != 0) { + if (check_phy_reg_addr_valid(phy_control_args.addr->ival[0]) != ESP_OK) { + return 1; + } + int data; + if (phy_control_args.decimal->count != 0 && phy_control_args.hex->count == 0) { + data = phy_control_args.decimal->ival[0]; + } else if (phy_control_args.decimal->count == 0 && phy_control_args.hex->count != 0) { + data = strtol(phy_control_args.hex->sval[0], NULL, 16); + } else { + ESP_LOGE(TAG, "invalid combination of register data formats"); + return -1; + } + write_phy_reg(s_eth_handles[0], phy_control_args.addr->ival[0], data); + } else { + ESP_LOGE(TAG, "register address is missing"); + } + } + return 0; +} + +static int phy_dump(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&phy_dump_regs_args); + if (nerrors != 0) { + arg_print_errors(stderr, phy_dump_regs_args.end, argv[0]); + return 1; + } + + if (phy_dump_regs_args.dump_802_3->count != 0) { + dump_phy_regs(s_eth_handles[0], 0, 15); + } else if (phy_dump_regs_args.dump_range_start->count != 0 && phy_dump_regs_args.dump_range_stop->count != 0) { + dump_phy_regs(s_eth_handles[0], phy_dump_regs_args.dump_range_start->ival[0], phy_dump_regs_args.dump_range_stop->ival[0]); + } else if (phy_dump_regs_args.dump_range_start->count != 0) { + dump_phy_regs(s_eth_handles[0], phy_dump_regs_args.dump_range_start->ival[0], phy_dump_regs_args.dump_range_start->ival[0]); + } else { + ESP_LOGE(TAG, "invalid arguments"); + } + + return 0; +} + +static int nearend_loopback_test(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&phy_nearend_loopback_test_args); + if (nerrors != 0) { + arg_print_errors(stderr, phy_nearend_loopback_test_args.end, argv[0]); + return 1; + } + + // default configuration + uint32_t length = 256; + uint32_t count = 10; + uint32_t interval = 1000000; // us => 1000 ms + bool verbose = false; + + if (phy_nearend_loopback_test_args.length->count != 0) { + length = phy_nearend_loopback_test_args.length->ival[0]; + } + + if (phy_nearend_loopback_test_args.count->count != 0) { + count = phy_nearend_loopback_test_args.count->ival[0]; + } + + if (phy_nearend_loopback_test_args.interval->count != 0) { + if (phy_nearend_loopback_test_args.interval->ival[0] >= 10000) { + interval = phy_nearend_loopback_test_args.interval->ival[0]; + } else { + ESP_LOGW(TAG, "currently, 10ms is the smallest interval to be set"); + interval = 10000; // 10 ms + } + } + + if (phy_nearend_loopback_test_args.verbose->count != 0) { + verbose = true; + } + loopback_near_end_test(s_eth_handles[0], verbose, length, count, interval); + + return 0; +} + +static int dummy_transmit(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&dummy_transmit_args); + if (nerrors != 0) { + arg_print_errors(stderr, dummy_transmit_args.end, argv[0]); + return 1; + } + + // default configuration + uint32_t length = 256; + uint32_t count = 10; + uint32_t interval = 1000000; // us => 1000 ms + bool verbose = false; + + if (dummy_transmit_args.length->count != 0) { + length = dummy_transmit_args.length->ival[0]; + } + + if (dummy_transmit_args.count->count != 0) { + count = dummy_transmit_args.count->ival[0]; + } + + if (dummy_transmit_args.interval->count != 0) { + if (dummy_transmit_args.interval->ival[0] >= 10000) { + interval = dummy_transmit_args.interval->ival[0]; + } else { + ESP_LOGW(TAG, "currently, 10ms is the smallest interval to be set"); + interval = 10000; // 10 ms + } + } + + if (dummy_transmit_args.verbose->count != 0) { + verbose = true; + } + transmit_to_host(s_eth_handles[0], verbose, length, count, interval); + + return 0; +} + +static int nearend_loopback_enable(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&phy_nearend_loopback_args); + if (nerrors != 0) { + arg_print_errors(stderr, phy_nearend_loopback_args.end, argv[0]); + return 1; + } + + phy_id_t phy_id; + eth_dev_info_t eth_info = ethernet_init_get_dev_info(s_eth_handles[0]); + for (phy_id = 0; phy_id < PHY_ID_END; phy_id++) { + if (strcmp(eth_info.name, supported_phys[phy_id]) == 0) { + break; + } + } + if (phy_id >= PHY_ID_END) { + ESP_LOGE(TAG, "unsupported PHY"); + return 1; + } + + // TODO add check that only one option at a time + if (phy_nearend_loopback_args.enable->count != 0) { + loopback_near_end_en(s_eth_handles[0], 0, true); + } + + if (phy_nearend_loopback_args.disable->count != 0) { + loopback_near_end_en(s_eth_handles[0], 0, false); + } + + return 0; +} + +static int farend_loopback_enable(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&phy_farend_loopback_args); + if (nerrors != 0) { + arg_print_errors(stderr, phy_farend_loopback_args.end, argv[0]); + return 1; + } + + phy_id_t phy_id; + eth_dev_info_t eth_info = ethernet_init_get_dev_info(s_eth_handles[0]); + for (phy_id = 0; phy_id < PHY_ID_END; phy_id++) { + if (strcmp(eth_info.name, supported_phys[phy_id]) == 0) { + break; + } + } + if (phy_id >= PHY_ID_END) { + ESP_LOGE(TAG, "unsupported PHY"); + return 1; + } + + if (phy_farend_loopback_args.enable->count != 0) { + loopback_far_end_en(s_eth_handles[0], phy_id, true); + } + + if (phy_farend_loopback_args.disable->count != 0) { + loopback_far_end_en(s_eth_handles[0], phy_id, false); + } + + return 0; +} + +static int loop_server_start(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&loop_server_args); + if (nerrors != 0) { + arg_print_errors(stderr, loop_server_args.end, argv[0]); + return 1; + } + + // default values + uint32_t timeout_ms = 5000; + uint16_t eth_type_filter = 0xFFFF; // don't filter + bool verbose = false; + + if (loop_server_args.timeout_ms->count != 0) { + timeout_ms = loop_server_args.timeout_ms->ival[0]; + } + if (loop_server_args.eth_type_filter->count != 0) { + eth_type_filter = strtol(loop_server_args.eth_type_filter->sval[0], NULL, 16); + } + if (loop_server_args.verbose->count != 0) { + verbose = true; + } + + loop_server(s_eth_handles[0], verbose, eth_type_filter, timeout_ms); + return 0; +} + +static int set_verbosity(int argc, char **argv) +{ + int nerrors = arg_parse(argc, argv, (void **)&verbosity_args); + if (nerrors != 0) { + arg_print_errors(stderr, verbosity_args.end, argv[0]); + return 1; + } + + if (verbosity_args.verbosity->count != 0) { + esp_log_level_t verbosity_level = verbosity_args.verbosity->ival[0]; + if (verbosity_level >= ESP_LOG_NONE && verbosity_level <= ESP_LOG_VERBOSE) { + esp_log_level_set("*", verbosity_level); + } else { + ESP_LOGE(TAG, "invalid range of ESP log verbosity level"); + } + } + + return 0; +} + +void register_ethernet(void) +{ + // Create default event loop that running in background + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // Initialize Ethernet driver + esp_err_t ret = ethernet_init_all(&s_eth_handles, &s_eth_port_cnt); + + if (ret == ESP_OK) { + ESP_LOGI(TAG, "Ethernet init successful!"); + } else { + ESP_LOGE(TAG, "Ethernet init failed!"); + goto err; + } + + if (s_eth_port_cnt > 1) { + ESP_LOGE(TAG, "only one PHY can be tested"); + goto err; + } + // Get the device information of the ethernet handle + eth_dev_info_t eth_info = ethernet_init_get_dev_info(s_eth_handles[0]); + if (eth_info.type == ETH_DEV_TYPE_SPI) { + ESP_LOGE(TAG, "test of SPI modules is not supported"); + goto err; + } + + phy_control_args.info = arg_str0(NULL, NULL, "", "Get info of Ethernet"); + phy_control_args.read = arg_lit0(NULL, "read", "read PHY register"); + phy_control_args.write = arg_lit0(NULL, "write", "write PHY register"); + phy_control_args.addr = arg_int0("a", NULL, "
", "register address (used in combination with read/write)"); + phy_control_args.decimal = arg_int0("d", NULL, "", "register data in dec format (used in combination with write)"); + phy_control_args.hex = arg_str0("h", NULL, "", "register data in hex format (used in combination with write)"); + phy_control_args.end = arg_end(1); + const esp_console_cmd_t cmd = { + .command = "phy", + .help = "Ethernet PHY control", + .hint = NULL, + .func = phy_cmd_control, + .argtable = &phy_control_args + }; + ESP_ERROR_CHECK(esp_console_cmd_register(&cmd)); + + phy_dump_regs_args.dump_802_3 = arg_lit0("a", "all", "Dump IEEE 802.3 registers"); + phy_dump_regs_args.dump_range_start = arg_int0(NULL, NULL, "", "Dump a range of registers start addr"); + phy_dump_regs_args.dump_range_stop = arg_int0(NULL, NULL, "", "Dump a range of registers end addr"); + phy_dump_regs_args.end = arg_end(1); + const esp_console_cmd_t dump_cmd = { + .command = "dump", + .help = "Dump PHY registers", + .hint = NULL, + .func = phy_dump, + .argtable = &phy_dump_regs_args + }; + ESP_ERROR_CHECK(esp_console_cmd_register(&dump_cmd)); + + phy_nearend_loopback_args.enable = arg_lit0("e", "enable", "enable near-end loopback"); + phy_nearend_loopback_args.disable = arg_lit0("d", "disable", "disable near-end loopback"); + phy_nearend_loopback_args.end = arg_end(1); + const esp_console_cmd_t nearend_loopback_cmd = { + .command = "near-loop-en", + .help = "Enables near-end loopback, frames are looped at R/MII PHY back to ESP32", + .hint = NULL, + .func = nearend_loopback_enable, + .argtable = &phy_nearend_loopback_args + }; + ESP_ERROR_CHECK(esp_console_cmd_register(&nearend_loopback_cmd)); + + phy_farend_loopback_args.enable = arg_lit0("e", "enable", "enable far-end loopback"); + phy_farend_loopback_args.disable = arg_lit0("d", "disable", "disable far-end loopback"); + phy_farend_loopback_args.end = arg_end(1); + const esp_console_cmd_t farend_loopback_cmd = { + .command = "farend-loop-en", + .help = "Enables far-end loopback, frames are looped at PHY back to host", + .hint = NULL, + .func = farend_loopback_enable, + .argtable = &phy_farend_loopback_args + }; + ESP_ERROR_CHECK(esp_console_cmd_register(&farend_loopback_cmd)); + + phy_nearend_loopback_test_args.length = arg_int0("s", "size", "", "size of the frame"); + phy_nearend_loopback_test_args.count = arg_int0("c", "count", "", "number of frames to be loopedback"); + phy_nearend_loopback_test_args.interval = arg_int0("i", "interval", "", "microseconds between sending each frame"); + phy_nearend_loopback_test_args.verbose = arg_lit0("v", "verbose", "enable verbose test output"); + phy_nearend_loopback_test_args.end = arg_end(1); + const esp_console_cmd_t nearend_loopback_test_cmd = { + .command = "loop-test", + .help = "Runs Loopback test, frames are looped by PHY back to ESP32 (near-end loopback)", + .hint = NULL, + .func = nearend_loopback_test, + .argtable = &phy_nearend_loopback_test_args + }; + ESP_ERROR_CHECK(esp_console_cmd_register(&nearend_loopback_test_cmd)); + + dummy_transmit_args.length = arg_int0("s", "size", "", "size of the frame"); + dummy_transmit_args.count = arg_int0("c", "count", "", "number of frames to be transmitted"); + dummy_transmit_args.interval = arg_int0("i", "interval", "", "microseconds between sending each frame"); + dummy_transmit_args.verbose = arg_lit0("v", "verbose", "enable verbose test output"); + dummy_transmit_args.end = arg_end(1); + const esp_console_cmd_t dummy_transmit_cmd = { + .command = "dummy-tx", + .help = "Transmits dummy test frames", + .hint = NULL, + .func = dummy_transmit, + .argtable = &dummy_transmit_args + }; + ESP_ERROR_CHECK(esp_console_cmd_register(&dummy_transmit_cmd)); + + loop_server_args.timeout_ms = arg_int0("t", "timeout", "", "receive timeout (if no message is received, loop is closed)"); + loop_server_args.eth_type_filter = arg_str0("f", "filter", "", "Ethertype in hex to be filtered at recv function (FFFF to not filter)"); + loop_server_args.verbose = arg_lit0("v", "verbose", "enable verbose test output"); + loop_server_args.end = arg_end(1); + const esp_console_cmd_t loop_server_cmd = { + .command = "loop-server", + .help = "Start a Ethernet loop `server`", + .hint = NULL, + .func = loop_server_start, + .argtable = &loop_server_args + }; + ESP_ERROR_CHECK(esp_console_cmd_register(&loop_server_cmd)); + + verbosity_args.verbosity = arg_int0("l", "level", "<0-6>", "set ESP logs verbosity level"); + verbosity_args.end = arg_end(1); + const esp_console_cmd_t verbosity_cmd = { + .command = "verbosity", + .help = "set ESP log verbosity level", + .hint = NULL, + .func = set_verbosity, + .argtable = &verbosity_args + }; + ESP_ERROR_CHECK(esp_console_cmd_register(&verbosity_cmd)); + + return; +err: + if (s_eth_handles) { + ethernet_deinit_all(s_eth_handles); + } + esp_event_loop_delete_default(); + return; +} diff --git a/phy_tester/main/cmd_ethernet.h b/phy_tester/main/cmd_ethernet.h new file mode 100644 index 0000000..023dedb --- /dev/null +++ b/phy_tester/main/cmd_ethernet.h @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +// Register test functions +void register_ethernet(void); + +#ifdef __cplusplus +} +#endif diff --git a/phy_tester/main/eth_common.c b/phy_tester/main/eth_common.c new file mode 100644 index 0000000..2c35bad --- /dev/null +++ b/phy_tester/main/eth_common.c @@ -0,0 +1,208 @@ +/* + * SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_event.h" +#include "esp_check.h" +#include "esp_log.h" +#include "sdkconfig.h" +#include "eth_common.h" + +static const char *TAG = "ethernet_fncs"; + +/** Event handler for Ethernet events */ +static void eth_event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + EventGroupHandle_t eth_event_group = (EventGroupHandle_t)arg; + switch (event_id) { + case ETHERNET_EVENT_CONNECTED: + xEventGroupSetBits(eth_event_group, ETH_CONNECT_BIT); + break; + case ETHERNET_EVENT_DISCONNECTED: + break; + case ETHERNET_EVENT_START: + xEventGroupSetBits(eth_event_group, ETH_START_BIT); + break; + case ETHERNET_EVENT_STOP: + xEventGroupSetBits(eth_event_group, ETH_STOP_BIT); + break; + default: + break; + } +} + +/** Event handler for IP_EVENT_ETH_GOT_IP */ +void got_ip_event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + EventGroupHandle_t eth_event_group = (EventGroupHandle_t)arg; + ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; + const esp_netif_ip_info_t *ip_info = &event->ip_info; + ESP_LOGI(TAG, "Ethernet Got IP Address"); + ESP_LOGI(TAG, "~~~~~~~~~~~"); + ESP_LOGI(TAG, "ETHIP:" IPSTR, IP2STR(&ip_info->ip)); + ESP_LOGI(TAG, "ETHMASK:" IPSTR, IP2STR(&ip_info->netmask)); + ESP_LOGI(TAG, "ETHGW:" IPSTR, IP2STR(&ip_info->gw)); + ESP_LOGI(TAG, "~~~~~~~~~~~"); + xEventGroupSetBits(eth_event_group, ETH_GOT_IP_BIT); +} + +void delete_eth_event_group(EventGroupHandle_t eth_event_group) +{ + esp_event_handler_unregister(ETH_EVENT, ESP_EVENT_ANY_ID, eth_event_handler); + if (eth_event_group != NULL) { + vEventGroupDelete(eth_event_group); + } +} + +EventGroupHandle_t create_eth_event_group(void) +{ + EventGroupHandle_t eth_event_group = xEventGroupCreate(); + if (eth_event_group == NULL) { + return NULL; + } + if (esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, ð_event_handler, eth_event_group) != ESP_OK) { + ESP_LOGE(TAG, "event handler registration failed"); + goto err; + } + + return eth_event_group; +err: + delete_eth_event_group(eth_event_group); + return NULL; +} + +esp_err_t dump_phy_regs(esp_eth_handle_t *eth_handle, uint32_t start_addr, uint32_t end_addr) +{ + esp_eth_phy_reg_rw_data_t reg; + uint32_t reg_val; + reg.reg_value_p = ®_val; + + printf("--- PHY Registers Dump ---\n"); + for (uint32_t curr_addr = start_addr; curr_addr <= end_addr; curr_addr++) { + reg.reg_addr = curr_addr; + ESP_RETURN_ON_ERROR(esp_eth_ioctl(eth_handle, ETH_CMD_READ_PHY_REG, ®), TAG, "ioctl read PHY register failed"); + printf("Addr: 0x%02lx, value: 0x%04lx\n", curr_addr, reg_val); + } + printf("\n"); + + return ESP_OK; +} + +esp_err_t write_phy_reg(esp_eth_handle_t *eth_handle, uint32_t addr, uint32_t data) +{ + esp_eth_phy_reg_rw_data_t reg; + uint32_t reg_val; + reg.reg_value_p = ®_val; + + reg.reg_addr = addr; + reg_val = data; + ESP_RETURN_ON_ERROR(esp_eth_ioctl(eth_handle, ETH_CMD_WRITE_PHY_REG, ®), TAG, "ioctl write PHY register data failed"); + return ESP_OK; +} + +esp_err_t loopback_near_end_en(esp_eth_handle_t *eth_handle, phy_id_t phy_id, bool enable) +{ + esp_err_t ret = ESP_OK; + static bool nego_en = true; + + bool loopback_en; + if (enable) { + loopback_en = true; + if ((ret = esp_eth_ioctl(eth_handle, ETH_CMD_S_PHY_LOOPBACK, &loopback_en)) != ESP_OK) { + ESP_GOTO_ON_ERROR(esp_eth_ioctl(eth_handle, ETH_CMD_G_AUTONEGO, &nego_en), err, TAG, "get auto-negotiation failed"); + if (nego_en == false) { + return ret; + } + ESP_LOGW(TAG, "some PHY requires to disable auto-negotiation => disabling and trying to enable loopback again..."); + nego_en = false; + ESP_GOTO_ON_ERROR(esp_eth_ioctl(eth_handle, ETH_CMD_S_AUTONEGO, &nego_en), err, TAG, "auto-negotiation configuration failed"); + eth_speed_t speed = ETH_SPEED_100M; + ESP_GOTO_ON_ERROR(esp_eth_ioctl(eth_handle, ETH_CMD_S_SPEED, &speed), err, TAG, "speed configuration failed"); + eth_duplex_t duplex = ETH_DUPLEX_FULL; + ESP_GOTO_ON_ERROR(esp_eth_ioctl(eth_handle, ETH_CMD_S_DUPLEX_MODE, &duplex), err, TAG, "speed configuration failed"); + ESP_GOTO_ON_ERROR(esp_eth_ioctl(eth_handle, ETH_CMD_S_PHY_LOOPBACK, &loopback_en), err, TAG, "loopback configuration failed"); + ESP_LOGW(TAG, "loopback configuration succeeded at the second attempt, you can ignore above errors"); + } + } else { + // configure the PHY back to default setting + loopback_en = false; + ESP_GOTO_ON_ERROR(esp_eth_ioctl(eth_handle, ETH_CMD_S_PHY_LOOPBACK, &loopback_en), err, TAG, "loopback configuration failed"); + if (nego_en == false) { + nego_en = true; + ESP_GOTO_ON_ERROR(esp_eth_ioctl(eth_handle, ETH_CMD_S_AUTONEGO, &nego_en), err, TAG, "auto-negotiation configuration failed"); + } + } + return ESP_OK; +err: + return ret; +} + +esp_err_t loopback_far_end_en(esp_eth_handle_t *eth_handle, phy_id_t phy_id, bool enable) +{ + esp_err_t ret = ESP_OK; + esp_eth_phy_reg_rw_data_t reg; + uint32_t reg_val; + uint32_t reg_val_exp; + reg.reg_value_p = ®_val; + + switch (phy_id) { + case PHY_IP101: + reg.reg_addr = 20; // page control reg. + reg_val = 1; // page 1 + esp_eth_ioctl(eth_handle, ETH_CMD_WRITE_PHY_REG, ®); + reg.reg_addr = 23; // UTP PHY Specific Control Register + esp_eth_ioctl(eth_handle, ETH_CMD_READ_PHY_REG, ®); + if (enable) { + reg_val |= 1 << 13; + } else { + reg_val &= ~(1 << 13); + } + break; + case PHY_LAN87XX: + // TODO add check that 100BASE-TX, see datasheet + reg.reg_addr = 17; // Mode Control Register + esp_eth_ioctl(eth_handle, ETH_CMD_READ_PHY_REG, ®); + if (enable) { + reg_val |= 1 << 9; + } else { + reg_val &= ~(1 << 9); + } + break; + case PHY_KSZ80XX: + reg.reg_addr = 0x1e; // PHY Control 1 Register + esp_eth_ioctl(eth_handle, ETH_CMD_READ_PHY_REG, ®); + if (enable) { + reg_val |= 1 << 7; + } else { + reg_val &= ~(1 << 7); + } + break; + case PHY_RTL8201: + case PHY_DP83848: // TODO DP83848 offers BIST => investigate if could be used instead + ESP_LOGE(TAG, "far-end loopback is not supported by selected PHY"); + return ESP_ERR_NOT_SUPPORTED; + default: + + return ESP_FAIL; + } + esp_eth_ioctl(eth_handle, ETH_CMD_WRITE_PHY_REG, ®); + reg_val_exp = reg_val; + esp_eth_ioctl(eth_handle, ETH_CMD_READ_PHY_REG, ®); + if (reg_val_exp != reg_val) { + ESP_LOGE(TAG, "error to configure far-end loopback"); + return ESP_FAIL; + } + + if (enable) { + ESP_GOTO_ON_ERROR(esp_eth_start(eth_handle), err, TAG, "failed to start Ethernet"); // just to see link status + } else { + ESP_GOTO_ON_ERROR(esp_eth_stop(eth_handle), err, TAG, "failed to stop Ethernet"); + } +err: + return ret; +} diff --git a/phy_tester/main/eth_common.h b/phy_tester/main/eth_common.h new file mode 100644 index 0000000..ef219a0 --- /dev/null +++ b/phy_tester/main/eth_common.h @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#pragma once + +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_eth.h" + +#define ETH_START_BIT BIT(0) +#define ETH_STOP_BIT BIT(1) +#define ETH_CONNECT_BIT BIT(2) +#define ETH_GOT_IP_BIT BIT(3) + +#define ETH_START_TIMEOUT_MS (1000) +#define ETH_CONNECT_TIMEOUT_MS (10000) +#define ETH_STOP_TIMEOUT_MS (1000) +#define ETH_GET_IP_TIMEOUT_MS (60000) + +typedef enum { + PHY_IP101, + PHY_LAN87XX, + PHY_KSZ80XX, + PHY_RTL8201, + PHY_DP83848, + PHY_ID_END +} phy_id_t; + +typedef struct { + uint8_t dest[ETH_ADDR_LEN]; + uint8_t src[ETH_ADDR_LEN]; + uint16_t proto; + uint8_t data[]; +} __attribute__((__packed__)) emac_frame_t; + +EventGroupHandle_t create_eth_event_group(void); +void delete_eth_event_group(EventGroupHandle_t eth_event_group); + +/** Event handler for IP_EVENT_ETH_GOT_IP */ +void got_ip_event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data); + +esp_err_t write_phy_reg(esp_eth_handle_t *eth_handle, uint32_t addr, uint32_t data); +esp_err_t dump_phy_regs(esp_eth_handle_t *eth_handle, uint32_t start_addr, uint32_t end_addr); + +esp_err_t loopback_near_end_en(esp_eth_handle_t *eth_handle, phy_id_t phy_id, bool enable); +esp_err_t loopback_far_end_en(esp_eth_handle_t *eth_handle, phy_id_t phy_id, bool enable); diff --git a/phy_tester/main/idf_component.yml b/phy_tester/main/idf_component.yml new file mode 100644 index 0000000..7cebe50 --- /dev/null +++ b/phy_tester/main/idf_component.yml @@ -0,0 +1,20 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/ethernet_init: "^0.0.8" + ## Required IDF version + idf: + version: ">=5.2.0" + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true + + cmd_system: + path: ${IDF_PATH}/examples/system/console/advanced/components/cmd_system diff --git a/phy_tester/main/phy_tester.c b/phy_tester/main/phy_tester.c new file mode 100644 index 0000000..116a83f --- /dev/null +++ b/phy_tester/main/phy_tester.c @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include "esp_log.h" +#include "esp_event.h" +#include "esp_err.h" +#include "esp_check.h" + +#include "esp_console.h" +#include "esp_vfs_fat.h" +#include "cmd_system.h" +#include "cmd_ethernet.h" + + +void app_main(void) +{ + // Increase logging level of Ethernet related + esp_log_level_set("*", ESP_LOG_VERBOSE); + + esp_console_repl_t *repl = NULL; + esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); + esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT(); + /*#if CONFIG_EXAMPLE_STORE_HISTORY // TODO add history + initialize_filesystem(); + repl_config.history_save_path = HISTORY_PATH; + #endif*/ + repl_config.prompt = "eth_phy>"; + // init console REPL environment + ESP_ERROR_CHECK(esp_console_new_repl_uart(&uart_config, &repl_config, &repl)); + + /* Register commands */ + register_system_common(); + register_ethernet(); + + printf("\n =======================================================\n"); + printf(" | Steps to Test Ethernet PHY |\n"); + printf(" | |\n"); + printf(" | 1. Enter 'help', check all supported commands |\n"); + printf(" | 2. Connect DUT Ethernet directly to test PC |\n"); + printf(" | 3. Execute any command |\n"); + printf(" | |\n"); + printf(" =======================================================\n\n"); + + // start console REPL + ESP_ERROR_CHECK(esp_console_start_repl(repl)); +} diff --git a/phy_tester/main/test_functions.c b/phy_tester/main/test_functions.c new file mode 100644 index 0000000..6c31776 --- /dev/null +++ b/phy_tester/main/test_functions.c @@ -0,0 +1,374 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "arpa/inet.h" // for ntohs, etc. +#include "esp_log.h" +#include "esp_event.h" +#include "esp_err.h" +#include "esp_check.h" +#include "esp_eth_driver.h" +#include "sdkconfig.h" +#include "eth_common.h" +#include "test_functions.h" +#include "freertos/queue.h" +#include // TODO: use different source for seed? + +#define TX_TASK_PRIO (8) +#define TX_TASK_STACK_SIZE (4096) +#define DEFAULT_TX_MESSAGE "ESP32 HELLO" + +#define TX_QUEUE_SIZE (10) +#define RX_QUEUE_SIZE (10) + +#define ETH_TYPE (0x3300) + +typedef struct { + QueueHandle_t rx_frame_queue; + uint16_t eth_type_filter; + atomic_bool reset_rx_cnt; + bool verbose; +} eth_recv_config_t; + +typedef struct { + esp_eth_handle_t eth_handle; + TaskHandle_t calling_task; + uint16_t frame_len; + uint32_t count; + uint32_t period_us; + QueueHandle_t control_frame_queue; + bool verbose; + bool randomize; +} tx_task_config_t; + +typedef struct { + uint16_t frame_len; + uint8_t *frame; +} frame_info_t; + +static const char *TAG = "eth_phy_test_fncs"; +static SemaphoreHandle_t s_verbose_mutex; + +static esp_err_t print_frame(emac_frame_t *eth_frame, uint32_t length, uint32_t seq, bool is_recv) +{ + esp_err_t ret = ESP_OK; + + if (!s_verbose_mutex) { + s_verbose_mutex = xSemaphoreCreateMutex(); + ESP_GOTO_ON_FALSE(s_verbose_mutex, ESP_ERR_NO_MEM, err, TAG, "create frame print lock failed"); + } + ESP_GOTO_ON_FALSE(xSemaphoreTake(s_verbose_mutex, pdMS_TO_TICKS(100)) == pdTRUE, ESP_ERR_TIMEOUT, err, TAG, + "print frame timeout"); + if (is_recv) { + printf("Received frame #%lu:\n", seq); + } else { + printf("Transmitted frame #%lu:\n", seq); + } + ESP_LOG_BUFFER_HEXDUMP("", eth_frame, length, ESP_LOG_INFO); + xSemaphoreGive(s_verbose_mutex); +err: + return ret; +} + +static esp_err_t eth_input_cb(esp_eth_handle_t hdl, uint8_t *buffer, uint32_t length, void *priv) +{ + eth_recv_config_t *recv_config = (eth_recv_config_t *)priv; + emac_frame_t *rx_eth_frame = (emac_frame_t *)buffer; + static uint32_t recv_cnt = 0; + + bool exp_reset_rx_cnt = true; + if (atomic_compare_exchange_strong(&recv_config->reset_rx_cnt, &exp_reset_rx_cnt, false)) { + recv_cnt = 0; + } + + if (recv_config->rx_frame_queue && (recv_config->eth_type_filter == 0xFFFF || + recv_config->eth_type_filter == ntohs(rx_eth_frame->proto))) { + recv_cnt++; + if (recv_config->verbose == true) { + print_frame(rx_eth_frame, length, recv_cnt, true); + } + frame_info_t frame_info = { + .frame_len = length, + .frame = buffer + }; + if (xQueueSend(recv_config->rx_frame_queue, &frame_info, pdMS_TO_TICKS(50)) != pdTRUE) { + ESP_LOGE(TAG, "Rx queue full"); + free(buffer); + } + } else { + free(buffer); + } + return ESP_OK; +} + +static void free_queue(QueueHandle_t frame_queue) +{ + frame_info_t frame_info; + while (xQueueReceive(frame_queue, &frame_info, pdMS_TO_TICKS(10)) == pdTRUE) { + free(frame_info.frame); + } + vQueueDelete(frame_queue); +} + +static void randomize_frame_payload(uint8_t *frame, uint32_t frame_length) +{ + srand(time(NULL)); + for (int i = ETH_HEADER_LEN + 1; i < frame_length; i++) { // +1 for sequence number offset + frame[i] = rand() & 0xff; + } +} + +static void tx_task(void *arg) +{ + tx_task_config_t *tx_task_config = (tx_task_config_t *)arg; + + uint8_t *tx_buffer = calloc(sizeof(uint8_t), tx_task_config->frame_len); + if (!tx_buffer) { + ESP_LOGE(TAG, "no memory for TX frame buffer"); + goto err; + } + + // prepare header of Ethernet frame + emac_frame_t *tx_eth_frame = (emac_frame_t *)tx_buffer; + memset(tx_eth_frame->dest, 0xFF, ETH_ADDR_LEN); // broadcast + esp_eth_ioctl(tx_task_config->eth_handle, ETH_CMD_G_MAC_ADDR, tx_eth_frame->src); + tx_eth_frame->proto = htons(ETH_TYPE); + if ((tx_task_config->frame_len - ETH_HEADER_LEN) > sizeof(DEFAULT_TX_MESSAGE)) { + strcpy((char *)&tx_eth_frame->data[1], DEFAULT_TX_MESSAGE); + } else { + ESP_LOGW(TAG, "Ethernet frame len is too small to fit default Tx message"); + } + + ESP_LOGI(TAG, "starting ETH broadcast transmissions with Ethertype: 0x%x", ETH_TYPE); + for (uint32_t frame_id = 0; frame_id < tx_task_config->count; frame_id++) { + tx_eth_frame->data[0] = frame_id & 0xff; + // Randomize frame content if enabled + if (tx_task_config->randomize) { + randomize_frame_payload(tx_buffer, tx_task_config->frame_len); + // TODO randomize length too?? + } + // Queue control sample frame if enabled + if (tx_task_config->control_frame_queue) { + uint8_t *ctrl_sample_frame = malloc(tx_task_config->frame_len); + if (!tx_buffer) { + ESP_LOGE(TAG, "no memory for TX control sample frame"); + goto err; // it doesn't make sense to continue since the control samples are missing and so control check will fail + } + memcpy(ctrl_sample_frame, tx_eth_frame, tx_task_config->frame_len); + frame_info_t frame_info = { + .frame_len = tx_task_config->frame_len, + .frame = ctrl_sample_frame + }; + if (xQueueSend(tx_task_config->control_frame_queue, &frame_info, pdMS_TO_TICKS(50)) != pdTRUE) { + ESP_LOGE(TAG, "control Tx queue full"); + free(ctrl_sample_frame); + goto err; // it doesn't make sense to continue since the control samples are missing and so control check will fail + } + } + // Transmit the frame + if (esp_eth_transmit(tx_task_config->eth_handle, tx_eth_frame, tx_task_config->frame_len) != ESP_OK) { + ESP_LOGE(TAG, "transmit failed"); + } else if (tx_task_config->verbose) { + print_frame(tx_eth_frame, tx_task_config->frame_len, frame_id, false); + } + vTaskDelay(pdMS_TO_TICKS(tx_task_config->period_us / 1000)); + } +err: + free(tx_buffer); + // notify calling task that transmitting has been finished + if (tx_task_config->calling_task) { + xTaskNotifyGive(tx_task_config->calling_task); + } + vTaskDelete(NULL); +} + +esp_err_t loop_server( + esp_eth_handle_t *eth_handle, + bool verbose, + uint16_t eth_type, + uint32_t timeout_ms) +{ + esp_err_t ret = ESP_OK; + EventGroupHandle_t eth_event_group = NULL; + QueueHandle_t rx_frame_queue = NULL; + + ESP_GOTO_ON_FALSE(eth_handle, ESP_ERR_INVALID_ARG, err, TAG, "invalid Ethernet handle"); + + eth_event_group = create_eth_event_group(); + ESP_GOTO_ON_FALSE(eth_event_group != NULL, ESP_FAIL, err, TAG, "event init failed"); + + rx_frame_queue = xQueueCreate(RX_QUEUE_SIZE, sizeof(frame_info_t)); + ESP_GOTO_ON_FALSE(rx_frame_queue, ESP_FAIL, err, TAG, "create rx queue failed"); + eth_recv_config_t recv_config = { + .rx_frame_queue = rx_frame_queue, + .eth_type_filter = eth_type, + .verbose = verbose + }; + atomic_store(&recv_config.reset_rx_cnt, true); + ESP_GOTO_ON_ERROR(esp_eth_update_input_path(eth_handle, eth_input_cb, &recv_config), err, TAG, "ethernet input function configuration failed"); + + ESP_GOTO_ON_ERROR(esp_eth_start(eth_handle), err, TAG, "failed to start Ethernet"); + EventBits_t bits = xEventGroupWaitBits(eth_event_group, ETH_CONNECT_BIT, true, true, pdMS_TO_TICKS(ETH_CONNECT_TIMEOUT_MS)); + ESP_GOTO_ON_FALSE(bits & ETH_CONNECT_BIT, ESP_ERR_TIMEOUT, err_stop, TAG, "link connect timeout"); + + frame_info_t rx_frame_info; + while (xQueueReceive(rx_frame_queue, &rx_frame_info, pdMS_TO_TICKS(timeout_ms)) == pdTRUE) { + emac_frame_t *eth_frame = (emac_frame_t *)rx_frame_info.frame; + memcpy(eth_frame->dest, eth_frame->src, ETH_ADDR_LEN); + esp_eth_ioctl(eth_handle, ETH_CMD_G_MAC_ADDR, eth_frame->src); + if (esp_eth_transmit(eth_handle, eth_frame, rx_frame_info.frame_len) != ESP_OK) { + ESP_LOGE(TAG, "transmit failed"); + } + } +err_stop: + ESP_GOTO_ON_ERROR(esp_eth_stop(eth_handle), err, TAG, "failed to stop Ethernet"); +err: + esp_eth_update_input_path(eth_handle, NULL, NULL); + delete_eth_event_group(eth_event_group); + return ret; +} + +esp_err_t transmit_to_host( + esp_eth_handle_t *eth_handle, + bool verbose, + uint32_t frame_length, + uint32_t count, + uint32_t period_us) +{ + esp_err_t ret = ESP_OK; + EventGroupHandle_t eth_event_group = NULL; + ESP_GOTO_ON_FALSE(eth_handle, ESP_ERR_INVALID_ARG, err, TAG, "invalid Ethernet handle"); + + eth_event_group = create_eth_event_group(); + ESP_GOTO_ON_FALSE(eth_event_group != NULL, ESP_FAIL, err, TAG, "event init failed"); + + ESP_GOTO_ON_ERROR(esp_eth_start(eth_handle), err, TAG, "failed to start Ethernet"); + EventBits_t bits = xEventGroupWaitBits(eth_event_group, ETH_CONNECT_BIT, true, true, pdMS_TO_TICKS(ETH_CONNECT_TIMEOUT_MS)); + ESP_GOTO_ON_FALSE(bits & ETH_CONNECT_BIT, ESP_ERR_TIMEOUT, err_stop, TAG, "link connect timeout"); + + tx_task_config_t tx_task_config = { + .eth_handle = eth_handle, + .calling_task = xTaskGetCurrentTaskHandle(), + .frame_len = frame_length, + .count = count, + .period_us = period_us, + .control_frame_queue = NULL, + .verbose = verbose, + .randomize = false + }; + + TaskHandle_t task_hndl; + BaseType_t xReturned = xTaskCreate(tx_task, "eth_tx_task", TX_TASK_STACK_SIZE, &tx_task_config, TX_TASK_PRIO, &task_hndl); + ESP_GOTO_ON_FALSE(xReturned == pdPASS, ESP_FAIL, err_stop, TAG, "create emac_rx task failed"); + + if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(period_us / 1000 * count * 120 / 100)) == 0) { // + 20% + ESP_LOGE(TAG, "transmit task hasn't finished in expected timeout"); + }; +err_stop: + ESP_GOTO_ON_ERROR(esp_eth_stop(eth_handle), err, TAG, "failed to stop Ethernet"); +err: + delete_eth_event_group(eth_event_group); + return ret; +} + +esp_err_t loopback_near_end_test( + esp_eth_handle_t *eth_handle, + bool verbose, + uint32_t frame_length, + uint32_t count, + uint32_t period_us) +{ + esp_err_t ret = ESP_OK; + EventGroupHandle_t eth_event_group = NULL; + QueueHandle_t rx_frame_queue = NULL; + QueueHandle_t tx_frame_queue = NULL; + + ESP_GOTO_ON_FALSE(eth_handle, ESP_ERR_INVALID_ARG, err, TAG, "invalid Ethernet handle"); + + eth_event_group = create_eth_event_group(); + ESP_GOTO_ON_FALSE(eth_event_group != NULL, ESP_FAIL, err, TAG, "event init failed"); + + // Enable PHY near end loopback + loopback_near_end_en(eth_handle, 0, true); + + rx_frame_queue = xQueueCreate(RX_QUEUE_SIZE, sizeof(frame_info_t)); + ESP_GOTO_ON_FALSE(rx_frame_queue, ESP_FAIL, err, TAG, "create rx queue failed"); + eth_recv_config_t recv_config = { + .rx_frame_queue = rx_frame_queue, + .eth_type_filter = 0xFFFF, // don't filter + .verbose = verbose + }; + atomic_store(&recv_config.reset_rx_cnt, true); + ESP_GOTO_ON_ERROR(esp_eth_update_input_path(eth_handle, eth_input_cb, &recv_config), err, TAG, "ethernet input function configuration failed"); + + ESP_GOTO_ON_ERROR(esp_eth_start(eth_handle), err, TAG, "failed to start Ethernet"); + EventBits_t bits = xEventGroupWaitBits(eth_event_group, ETH_CONNECT_BIT, true, true, pdMS_TO_TICKS(ETH_CONNECT_TIMEOUT_MS)); + ESP_GOTO_ON_FALSE(bits & ETH_CONNECT_BIT, ESP_ERR_TIMEOUT, err_stop, TAG, "link connect timeout"); + + tx_frame_queue = xQueueCreate(TX_QUEUE_SIZE, sizeof(frame_info_t)); + ESP_GOTO_ON_FALSE(tx_frame_queue, ESP_FAIL, err, TAG, "create tx queue failed"); + + tx_task_config_t tx_task_config = { + .eth_handle = eth_handle, + .calling_task = NULL, + .frame_len = frame_length, + .count = count, + .period_us = period_us, + .control_frame_queue = tx_frame_queue, + .verbose = verbose, + .randomize = true + }; + + TaskHandle_t task_hndl; + BaseType_t xReturned = xTaskCreate(tx_task, "eth_tx_task", TX_TASK_STACK_SIZE, &tx_task_config, TX_TASK_PRIO, &task_hndl); + ESP_GOTO_ON_FALSE(xReturned == pdPASS, ESP_FAIL, err_stop, TAG, "create emac_rx task failed"); + + uint32_t rx_err_cnt = 0; + uint32_t rx_cnt = 0; + frame_info_t rx_frame_info; + frame_info_t tx_frame_info; + // go over received frames and compare them with control samples + while (xQueueReceive(rx_frame_queue, &rx_frame_info, pdMS_TO_TICKS(period_us / 1000 * 2)) == pdTRUE) { + if (xQueueReceive(tx_frame_queue, &tx_frame_info, pdMS_TO_TICKS(10)) == pdTRUE) { + if (rx_frame_info.frame_len == tx_frame_info.frame_len) { + if (memcmp(rx_frame_info.frame, tx_frame_info.frame, tx_frame_info.frame_len) == 0) { + rx_cnt++; + } else { + ESP_LOGE(TAG, "unexpected content of received frame"); + rx_err_cnt++; + // TODO: try to find the same frame => traverse over Tx queue + } + } else { + ESP_LOGE(TAG, "unexpected length of received frame"); + rx_err_cnt++; + } + free(tx_frame_info.frame); + } + free(rx_frame_info.frame); + } + // TODO add wait for Tx task end + ESP_LOGI(TAG, "looped frames: %lu, rx errors: %lu", rx_cnt, rx_err_cnt); +err_stop: + ESP_GOTO_ON_ERROR(esp_eth_stop(eth_handle), err, TAG, "failed to stop Ethernet"); +err: + esp_eth_update_input_path(eth_handle, NULL, NULL); + if (rx_frame_queue) { + free_queue(rx_frame_queue); + } + if (tx_frame_queue) { + free_queue(tx_frame_queue); + } + if (eth_handle) { + loopback_near_end_en(eth_handle, 0, false); + } + delete_eth_event_group(eth_event_group); + return ret; +} diff --git a/phy_tester/main/test_functions.h b/phy_tester/main/test_functions.h new file mode 100644 index 0000000..46bb930 --- /dev/null +++ b/phy_tester/main/test_functions.h @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#pragma once + +#include +#include "esp_err.h" +#include "esp_eth_driver.h" + +#ifdef __cplusplus +extern "C" { +#endif + +esp_err_t loopback_near_end_test(esp_eth_handle_t *eth_handle, bool verbose, uint32_t frame_length, uint32_t count, uint32_t period_us); +esp_err_t transmit_to_host(esp_eth_handle_t *eth_handle, bool verbose, uint32_t frame_length, uint32_t count, uint32_t period_us); +esp_err_t loop_server(esp_eth_handle_t *eth_handle, bool verbose, uint16_t eth_type, uint32_t timeout_ms); + +#ifdef __cplusplus +} +#endif diff --git a/phy_tester/pytest_eth_phy.py b/phy_tester/pytest_eth_phy.py new file mode 100644 index 0000000..d6537d6 --- /dev/null +++ b/phy_tester/pytest_eth_phy.py @@ -0,0 +1,468 @@ +# SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import argparse +import contextlib +import logging +import os +import re +import socket +import sys +import signal +from multiprocessing import Pipe, Process, connection +from time import sleep +from typing import Iterator + +import pexpect +import pytest +from eth_error_msg import EthFailMsg +from pytest_embedded import Dut +from scapy.all import Ether, raw + +ETH_TYPE = 0x3300 +LINK_UP_TIMEOUT = 6 + +norm = '\033[0m' +red = '\033[31m' +green = '\033[32m' +italics = '\033[3m' +yellow = '\033[33m' + + +class EthTestIntf(object): + def __init__(self, eth_type: int, my_if: str = ''): + self.target_if = '' + self.eth_type = eth_type + self.find_target_if(my_if) + + def find_target_if(self, my_if: str = '') -> None: + # try to determine which interface to use + netifs = os.listdir('/sys/class/net/') + logging.info('detected interfaces: %s', str(netifs)) + + for netif in netifs: + # if no interface defined, try to find it automatically + if my_if == '': + if netif.find('eth') == 0 or netif.find('enp') == 0 or netif.find('eno') == 0: + self.target_if = netif + break + else: + if netif.find(my_if) == 0: + self.target_if = my_if + break + if self.target_if == '': + raise RuntimeError('network interface not found') + logging.info('Use %s for testing', self.target_if) + + @contextlib.contextmanager + def configure_eth_if(self, eth_type:int=0) -> Iterator[socket.socket]: + if eth_type == 0: + eth_type = self.eth_type + try: + so = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(eth_type)) + except PermissionError: + EthFailMsg.print_yellow('\n\n=================================================================================') + EthFailMsg.print_yellow('Insufficient permission to create raw socket') + EthFailMsg.print_yellow('Either run as root or set appropriate capability to python by executing:') + EthFailMsg.print_yellow(" sudo setcap 'CAP_NET_RAW+eip' $(readlink -f $(which python))") + EthFailMsg.print_yellow('=================================================================================') + raise RuntimeError('Insufficient permission to create raw socket') + + so.bind((self.target_if, 0)) + try: + yield so + finally: + so.close() + + def get_mac_addr(self) -> str: + with self.configure_eth_if() as so: + mac_bytes = so.getsockname()[4] + mac = ':'.join(format(x, '02x') for x in mac_bytes) + return mac + + def send_eth_frame(self, mac: str, payload_len: int=1010) -> None: + with self.configure_eth_if() as so: + so.settimeout(10) + payload = bytearray(payload_len) + for i, _ in enumerate(payload): + payload[i] = i & 0xff + eth_frame = Ether(dst=mac, src=so.getsockname()[4], type=self.eth_type) / raw(payload) + try: + so.send(raw(eth_frame)) + except Exception as e: + raise e + + def send_recv_cmp(self, mac: str, payload_len: int=1010) -> bool: + with self.configure_eth_if() as so: + so.settimeout(5) + payload = bytearray(payload_len) + for i, _ in enumerate(payload): + payload[i] = i & 0xff + eth_frame = Ether(dst=mac, src=so.getsockname()[4], type=self.eth_type) / raw(payload) + try: + so.send(raw(eth_frame)) + except Exception as e: + raise e + + try: + rx_eth_frame = Ether(so.recv(1522)) + except Exception as e: + logging.error('recv: %s', e) + return False + if rx_eth_frame.load != eth_frame.load: + logging.error("doesn't match!") + return False + return True + + def traffic_gen(self, mac: str, count: int, payload_len: int, data_pattern_hex: str, interval: float, pipe_rcv:connection.Connection) -> None: + with self.configure_eth_if() as so: + payload = bytes.fromhex(data_pattern_hex) * int((payload_len + 1) / len(bytes.fromhex(data_pattern_hex))) + eth_frame = Ether(dst=mac, src=so.getsockname()[4], type=self.eth_type) / raw(payload[0:payload_len]) + i = 0 + try: + while pipe_rcv.poll() is not True: + so.send(raw(eth_frame)) + sleep(interval) + if count != -1: + i += 1 + if i >= count: + break + except Exception as e: + raise e + + +def test_loopback_server(dut: Dut, eth_if: EthTestIntf, mac: str) -> str: + ret = 'PASS' + eth_type_hex = hex(eth_if.eth_type) + dut.write(f'loop-server -f {eth_type_hex} -t 2000\n') + try: + dut.expect_exact('Link Up', timeout=LINK_UP_TIMEOUT) + except pexpect.exceptions.TIMEOUT: + EthFailMsg.print_link_up_fail_msg() + return 'FAIL' + + if eth_if.send_recv_cmp(mac) is False: + ret = 'FAIL' + + dut.expect_exact('Link Down') + return ret + + +def test_farend_loopback(dut: Dut, eth_if: EthTestIntf, mac: str) -> str: + ret = 'PASS' + dut.write('farend-loop-en -e\n') + try: + dut.expect_exact('Link Up', timeout=LINK_UP_TIMEOUT) + except pexpect.exceptions.TIMEOUT: + # it could be caused by PHY does not support far-end loopback + try: + dut.expect_exact('far-end loopback is not supported by selected PHY', timeout=1) + except: # noqa + pass # far-end loopback is supported, do nothing + else: + logging.warning('DUT PHY does not support far-end loopback!') + return 'NOT SUPPORTED' + EthFailMsg.print_link_up_fail_msg() + dut.write('farend-loop-en -d\n') + return 'FAIL' + + if eth_if.send_recv_cmp(mac) is False: + ret = 'FAIL' + + dut.write('farend-loop-en -d\n') + dut.expect_exact('Link Down') + return ret + + +def test_nearend_loopback(dut: Dut, count: int) -> str: + ret = 'PASS' + dut.write(f'loop-test -s 640 -c {count}\n') + try: + dut.expect_exact('Link Up') + dut.expect_exact(f'looped frames: {count}, rx errors: 0', timeout=4) + except: # noqa + logging.error('near-end loop back failed') + ret = 'FAIL' + return ret + + +def test_dut_rx(dut: Dut, eth_if: EthTestIntf, mac: str) -> str: + ret = 'PASS' + eth_type_hex = hex(eth_if.eth_type) + dut.write(f'loop-server -v -f {eth_type_hex} -t 2000\n') + try: + dut.expect_exact('Link Up', timeout=LINK_UP_TIMEOUT) + except pexpect.exceptions.TIMEOUT: + EthFailMsg.print_link_up_fail_msg() + return 'FAIL' + + payload_len = 50 + eth_if.send_eth_frame(mac, payload_len) + len = payload_len + 6 + 6 + 2 # + dest mac + src mac + ethtype + try: + dut.expect_exact(f'esp.emac: receive len= {len}', timeout=4) + rx_frame = dut.expect(r'([0-9A-Fa-f]{2} [0-9A-Fa-f]{2} [0-9A-Fa-f]{2} [0-9A-Fa-f]{2} [0-9A-Fa-f]{2} [0-9A-Fa-f]{2}) ' + + r'([0-9A-Fa-f]{2} [0-9A-Fa-f]{2} [0-9A-Fa-f]{2} [0-9A-Fa-f]{2} [0-9A-Fa-f]{2} [0-9A-Fa-f]{2}) ' + + r'([0-9A-Fa-f]{2} [0-9A-Fa-f]{2})', timeout=4) + except pexpect.exceptions.TIMEOUT: + logging.error('expected frame was not received by DUT') + return 'FAIL' + + rx_dst_mac = rx_frame.group(1).decode('utf-8').replace(' ', ':') + rx_src_mac = rx_frame.group(2).decode('utf-8').replace(' ', ' ').replace(' ', ':') + rx_eth_type = rx_frame.group(3).decode('utf-8').replace(' ','') + + mac.lower() + host_mac = eth_if.get_mac_addr() + if rx_dst_mac != mac or rx_src_mac != host_mac or rx_eth_type not in eth_type_hex: + ret = 'FAIL' + + dut.expect_exact('Link Down') + return ret + + +def test_dut_tx(dut: Dut, eth_if: EthTestIntf, count: int) -> str: + ret = 'PASS' + with eth_if.configure_eth_if() as so: + so.settimeout(5) + dut.write(f'dummy-tx -s 256 -c {count}\n') + try: + dut.expect_exact('Link Up', timeout=LINK_UP_TIMEOUT) + except pexpect.exceptions.TIMEOUT: + EthFailMsg.print_link_up_fail_msg() + return 'FAIL' + + try: + rx_eth_frame = Ether(so.recv(1522)) + decoded_rx_msg = rx_eth_frame.load[1:].decode('utf-8') # the first byte is seq. number + if 'ESP32 HELLO' not in decoded_rx_msg: + logging.error('recv. frame does not contain expected string') + ret = 'FAIL' + except Exception as e: + logging.error('recv: %s', e) + ret = 'FAIL' + + dut.expect_exact('Link Down') + return ret + + +def draw_result(near_tx_color: str, near_rx_color: str, far_tx_color: str, far_rx_color: str, phy_loop: bool, esp_loop: bool) -> None: + rmii_rx = near_rx_color + rmii_tx = near_tx_color + if phy_loop is True: + near_rx = f'{near_rx_color}-<-.{norm}' + near_tx = f"{near_tx_color}->-'{norm}" + near_loop = f'{near_tx_color}|{norm}' + + far_rx = f'{far_rx_color}.-<-{norm}' + far_tx = f"{far_tx_color}'->-{norm}" + far_loop = f'{far_tx_color}|{norm}' + phy_rx_path = f' ' + phy_tx_path = f' ' + else: + near_rx = f'{far_rx_color}----{norm}' + near_tx = f'{near_tx_color}----{norm}' + near_loop = f'{near_tx_color} {norm}' + far_rx = f'{far_rx_color}<---{norm}' + far_tx = f'{far_tx_color}>---{norm}' + far_loop = f'{far_tx_color} {norm}' + phy_rx_path = f'{far_rx_color}-------{norm}' + phy_tx_path = f'{far_tx_color}-------{norm}' + + if esp_loop is True: + esp_rx = f'{near_rx_color}.-<-{norm}' + esp_tx = f"{near_tx_color}'->-{norm}" + loop_esp = f'{near_tx_color}|{norm}' + else: + esp_rx = f'{near_rx_color} {norm}' + esp_tx = f'{near_tx_color} {norm}' + loop_esp = f'{near_tx_color} {norm}' + + if (far_tx_color == far_rx_color): + pc_conn = far_rx_color + else: + pc_conn = norm + + print(f'. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .') + print(f'. DUT .') + print(f'. RMII Tr. .') + print(f'. +------------------+ +---------------+ +-----------+ . +---------------+') + print(f'. | +---| | | 8|8 | | . | |') + print(f'. | {esp_rx}| M |{rmii_rx}-----Rx-----{norm}|{near_rx}{phy_rx_path}{far_rx}|{far_rx_color}-------{norm} 8|8 {far_rx_color}------{norm}| | . | |') # noqa + print(f'. | ESP32 {loop_esp} | A | | {near_loop} PHY {far_loop} | | RJ45 |{pc_conn}==========={norm}| Test PC |') # noqa + print(f'. | {esp_tx}| C |{rmii_tx}-----Tx-----{norm}|{near_tx}{phy_tx_path}{far_tx}|{far_tx_color}-------{norm} 8|8 {far_tx_color}------{norm}| | . | |') # noqa + print(f'. | +---| | | 8|8 | | . | |') + print(f'. +------------------+ +---------------+ +-----------+ . +---------------+') + print(f'. .') + if phy_loop is True: + print(f'. {italics}(near-end loopback) (far-end loopback){norm} .') + print(f'. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .') + + +def ethernet_phy_test(dut: Dut, test_pc_nic: str) -> None: + target_if = EthTestIntf(ETH_TYPE, test_pc_nic) + try: + dut.expect_exact('Ethernet init successful!') + except pexpect.exceptions.TIMEOUT: + EthFailMsg.print_eth_init_fail_msg() + raise RuntimeError() + + dut.expect_exact('Steps to Test Ethernet PHY') + + res_dict = {'loopback server': test_loopback_server(dut, target_if, 'ff:ff:ff:ff:ff:ff')} + # Loopback server results + if res_dict['loopback server'] == 'PASS': + tx_path = green + rx_path = green + else: + tx_path = red + rx_path = red + print('\n\n') + print(f'loopback server: {tx_path}{res_dict["loopback server"]}{norm}') + draw_result(tx_path, rx_path, tx_path, rx_path, False, True) + + if res_dict['loopback server'] == 'FAIL': + logging.error('loopback server test failed!') + logging.info('Running additional tests to try to isolate the problem...') + + res_dict.update({'DUT tx': test_dut_tx(dut, target_if, 1)}) + res_dict.update({'DUT rx': test_dut_rx(dut, target_if, 'ff:ff:ff:ff:ff:ff')}) + + res_dict.update({'far-end loopback': test_farend_loopback(dut, target_if, 'ff:ff:ff:ff:ff:ff')}) + res_dict.update({'near-end loopback': test_nearend_loopback(dut, 2)}) + + # DUT Tx/Rx results + if res_dict['DUT tx'] == 'PASS': + tx_path = green + else: + tx_path = red + + if res_dict['DUT rx'] == 'PASS': + rx_path = green + else: + rx_path = red + + print('\n\n') + print(f'DUT tx: {tx_path}{res_dict["DUT tx"]}{norm}') + print(f'DUT rx: {rx_path}{res_dict["DUT rx"]}{norm}') + draw_result(tx_path, rx_path, tx_path, rx_path, False, False) + + # PHY loopback results + if res_dict['near-end loopback'] == 'PASS': + near_loop = green + else: + near_loop = red + + if res_dict['far-end loopback'] == 'PASS': + far_loop = green + elif res_dict['far-end loopback'] == 'NOT SUPPORTED': + far_loop = yellow + else: + far_loop = red + + print('\n\n') + print(f'Near-end loopback: {near_loop}{res_dict["near-end loopback"]}{norm}') + print(f'Far-end loopback: {far_loop}{res_dict["far-end loopback"]}{norm}') + draw_result(near_loop, near_loop, far_loop, far_loop, True, False) + + # Final isolation + rx_near = green + tx_near = green + rmii_rx_fail = False + rmii_tx_fail = False + if near_loop != green: + if near_loop == red: + if rx_path == red: + rx_near = red + rmii_rx_fail = True + if tx_path == red: + tx_near = red + rmii_tx_fail = True + else: + rx_near = near_loop + tx_near = near_loop + + tx_far = green + rx_far = green + rj45_rx_fail = False + rj45_tx_fail = False + if far_loop != green: + if far_loop == red: + if rx_path == red: + rx_far = red + rj45_rx_fail = True + if tx_path == red: + tx_far = red + rj45_tx_fail = True + else: + tx_far = far_loop + rx_far = far_loop + + print('\n\n') + print('==============================') + print('Final problem isolation') + print('==============================') + draw_result(tx_near, rx_near, tx_far, rx_far, True, False) + if rmii_rx_fail or rmii_tx_fail: + EthFailMsg.print_rmii_fail_msg(rmii_rx_fail, rmii_tx_fail) + if rj45_rx_fail or rj45_tx_fail: + EthFailMsg.print_rj45_fail_msg(rj45_rx_fail, rj45_tx_fail) + + print('\nThe test finished! `Final problem isolation` should show you the most probable location of issue. However, go over the full log to see ' + 'additional details and to fully understand each tested scenario.') + + print('\nScript run:') + + +@pytest.mark.esp32 +@pytest.mark.esp32p4 +def test_esp_ethernet(dut: Dut, + eth_nic: str) -> None: + ethernet_phy_test(dut, eth_nic) + + +if __name__ == '__main__': + signal.signal(signal.SIGINT, lambda s, f : sys.exit()) + + logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO) + + # Add arguments + parser = argparse.ArgumentParser(description='Ethernet PHY tester helper script') + parser.add_argument('--eth_nic', default='', + help='Name of the test PC Ethernet NIC connected to DUT. If the option ' + 'is omitted, script uses the first identified Ethernet interface.') + + subparsers = parser.add_subparsers(help='Commands', dest='command') + + tx_parser = subparsers.add_parser('dummy-tx', + help='Generates dummy Ethernet transmissions') + + tx_parser.add_argument('-i', type=float, default=0.5, dest='interval', + help='seconds between sending each frame') + tx_parser.add_argument('-c', type=int, default=-1, dest='count', + help='number of Ethernet frames to send') + tx_parser.add_argument('-t', type=str, default=hex(ETH_TYPE), dest='eth_type', + help='Ethertype in hex') + tx_parser.add_argument('-s', type=int, default=512, dest='size', + help='payload size in B') + tx_parser.add_argument('-a', type=str, default='ff:ff:ff:ff:ff:ff', dest='mac', + help='destination MAC address') + tx_parser.add_argument('-p', type=str, default='ff 00', dest='pattern', + help='payload pattern. For example if you define "ff 00", the pattern will be repeated to the end of frame size.') + + args = parser.parse_args() + + target_if = EthTestIntf(ETH_TYPE, args.eth_nic) + if args.command == 'dummy-tx': + if args.mac: + if re.fullmatch('(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}', args.mac) is None: + logging.error('MAC is not in expected format') + sys.exit() + + pipe_rcv, pipe_send = Pipe(False) + tx_proc = Process(target=target_if.traffic_gen, args=(args.mac, args.count, args.size, args.pattern, args.interval, pipe_rcv, )) + tx_proc.start() + # pipe_send.send(0) # just send some dummy data to stop the process + tx_proc.join() + if tx_proc.exitcode is None: + tx_proc.terminate()