From b284cb4953e8b21e4995f6f48926182031fd5486 Mon Sep 17 00:00:00 2001 From: Wang Beihong Date: Sun, 1 Feb 2026 18:31:06 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=E4=B8=80=E6=AC=A1=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=EF=BC=9A=E5=AE=8C=E6=88=90=E4=BA=86=E7=BD=91=E5=85=B3=E7=9A=84?= =?UTF-8?q?=E5=8D=95=E8=B7=AF485=E6=95=B0=E6=8D=AE=E9=87=87=E9=9B=86?= =?UTF-8?q?=EF=BC=8C=E8=BF=98=E6=9C=89=E4=BB=A5=E5=A4=AA=E7=BD=91=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E5=92=8CMQTT=E9=85=8D=E7=BD=AE=EF=BC=8C=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E6=95=B0=E6=8D=AE=E4=B8=8A=E6=8A=A5=E5=92=8C=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E4=B8=8B=E5=8F=91=EF=BC=8C=E5=B7=AE=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E6=96=AD=E7=BD=91=E5=82=A8=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .clangd | 2 + .gitignore | 78 ++ .vscode/c_cpp_properties.json | 23 + .vscode/launch.json | 15 + .vscode/settings.json | 20 + CMakeLists.txt | 6 + README.md | 441 +++++++++ components/ETH_CH390H/CMakeLists.txt | 7 + components/ETH_CH390H/ETH_CH390H.c | 141 +++ components/ETH_CH390H/esp_eth_mac_ch390.c | 930 ++++++++++++++++++ components/ETH_CH390H/esp_eth_phy_ch390.c | 157 +++ components/ETH_CH390H/include/ETH_CH390H.h | 22 + components/ETH_CH390H/include/ch390.h | 236 +++++ .../ETH_CH390H/include/esp_eth_mac_ch390.h | 62 ++ .../ETH_CH390H/include/esp_eth_phy_ch390.h | 32 + components/MODBUS_ESP/CMakeLists.txt | 3 + components/MODBUS_ESP/MODBUS_ESP.c | 416 ++++++++ components/MODBUS_ESP/include/MODBUS_ESP.h | 136 +++ components/MQTT_ESP/CMakeLists.txt | 3 + components/MQTT_ESP/Kconfig.projbuild | 59 ++ components/MQTT_ESP/MQTT_ESP.c | 487 +++++++++ components/MQTT_ESP/include/MQTT_ESP.h | 25 + components/RS-485-SP3485EEN/CMakeLists.txt | 4 + .../RS-485-SP3485EEN/RS-485-SP3485EEN.c | 311 ++++++ .../include/RS-485-SP3485EEN.h | 66 ++ components/SNTP_ESP/CMakeLists.txt | 3 + components/SNTP_ESP/SNTP_ESP.c | 133 +++ components/SNTP_ESP/include/SNTP_ESP.h | 72 ++ components/STATUS_LED/CMakeLists.txt | 4 + components/STATUS_LED/STATUS_LED.c | 257 +++++ components/STATUS_LED/include/STATUS_LED.h | 63 ++ dependencies.lock | 21 + main/CMakeLists.txt | 4 + main/idf_component.yml | 18 + main/main.c | 81 ++ 35 files changed, 4338 insertions(+) create mode 100644 .clangd create mode 100644 .gitignore create mode 100644 .vscode/c_cpp_properties.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100755 CMakeLists.txt create mode 100644 README.md create mode 100644 components/ETH_CH390H/CMakeLists.txt create mode 100644 components/ETH_CH390H/ETH_CH390H.c create mode 100644 components/ETH_CH390H/esp_eth_mac_ch390.c create mode 100644 components/ETH_CH390H/esp_eth_phy_ch390.c create mode 100644 components/ETH_CH390H/include/ETH_CH390H.h create mode 100644 components/ETH_CH390H/include/ch390.h create mode 100644 components/ETH_CH390H/include/esp_eth_mac_ch390.h create mode 100644 components/ETH_CH390H/include/esp_eth_phy_ch390.h create mode 100644 components/MODBUS_ESP/CMakeLists.txt create mode 100644 components/MODBUS_ESP/MODBUS_ESP.c create mode 100644 components/MODBUS_ESP/include/MODBUS_ESP.h create mode 100644 components/MQTT_ESP/CMakeLists.txt create mode 100644 components/MQTT_ESP/Kconfig.projbuild create mode 100644 components/MQTT_ESP/MQTT_ESP.c create mode 100644 components/MQTT_ESP/include/MQTT_ESP.h create mode 100644 components/RS-485-SP3485EEN/CMakeLists.txt create mode 100644 components/RS-485-SP3485EEN/RS-485-SP3485EEN.c create mode 100644 components/RS-485-SP3485EEN/include/RS-485-SP3485EEN.h create mode 100644 components/SNTP_ESP/CMakeLists.txt create mode 100644 components/SNTP_ESP/SNTP_ESP.c create mode 100644 components/SNTP_ESP/include/SNTP_ESP.h create mode 100644 components/STATUS_LED/CMakeLists.txt create mode 100644 components/STATUS_LED/STATUS_LED.c create mode 100644 components/STATUS_LED/include/STATUS_LED.h create mode 100644 dependencies.lock create mode 100755 main/CMakeLists.txt create mode 100644 main/idf_component.yml create mode 100755 main/main.c diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..437f255 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Remove: [-f*, -m*] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7805557 --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Directory metadata +.directory + +# Temporary files +*~ +*.swp +*.swo +*.bak +*.tmp + +# Log files +*.log + +# Build artifacts and directories +**/build/ +build/ +*.o +*.a +*.out +*.exe # For any host-side utilities compiled on Windows + +# ESP-IDF specific build outputs +*.bin +*.elf +*.map +flasher_args.json # Generated in build directory +sdkconfig.old +sdkconfig + +# ESP-IDF dependencies +# For older versions or manual component management +/components/.idf/ +**/components/.idf/ +# For modern ESP-IDF component manager +managed_components/ +# If ESP-IDF tools are installed/referenced locally to the project +.espressif/ + +# CMake generated files +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +install_manifest.txt +CTestTestfile.cmake + +# Python environment files +*.pyc +*.pyo +*.pyd +__pycache__/ +*.egg-info/ +dist/ + +# Virtual environment folders +venv/ +.venv/ +env/ + +# Language Servers +.clangd/ +.ccls-cache/ +compile_commands.json + +# Windows specific +Thumbs.db +ehthumbs.db +Desktop.ini + +# User-specific configuration files +*.user +*.workspace # General workspace files, can be from various tools +*.suo # Visual Studio Solution User Options +*.sln.docstates # Visual Studio diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..b7d20b2 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,23 @@ +{ + "configurations": [ + { + "name": "ESP-IDF", + "compilerPath": "${config:idf.toolsPath}/tools/xtensa-esp-elf/esp-14.2.0_20251107/xtensa-esp-elf/bin/xtensa-esp32-elf-gcc", + "compileCommands": "${config:idf.buildPath}/compile_commands.json", + "includePath": [ + "${config:idf.espIdfPath}/components/**", + "${config:idf.espIdfPathWin}/components/**", + "${workspaceFolder}/**" + ], + "browse": { + "path": [ + "${config:idf.espIdfPath}/components", + "${config:idf.espIdfPathWin}/components", + "${workspaceFolder}" + ], + "limitSymbolsToIncludedHeaders": true + } + } + ], + "version": 4 +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2511a38 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "gdbtarget", + "request": "attach", + "name": "Eclipse CDT GDB Adapter" + }, + { + "type": "espidf", + "name": "Launch", + "request": "launch" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c9151e8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "C_Cpp.intelliSenseEngine": "default", + "idf.espIdfPath": "/home/beihong/esp/v5.5.2/esp-idf", + "idf.pythonInstallPath": "/usr/bin/python3", + "idf.openOcdConfigs": [ + "board/esp32s3-builtin.cfg" + ], + "idf.port": "/dev/ttyACM0", + "idf.toolsPath": "/home/beihong/esp/v5.5.2/esp-idf/tools", + "idf.customExtraVars": { + "IDF_TARGET": "esp32s3" + }, + "clangd.path": "/home/beihong/esp/v5.5.2/esp-idf/tools/tools/esp-clang/esp-19.1.2_20250312/esp-clang/bin/clangd", + "clangd.arguments": [ + "--background-index", + "--query-driver=**", + "--compile-commands-dir=/home/beihong/esp_projects/Distributed Collector Gateway/build" + ], + "idf.flashType": "UART" +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100755 index 0000000..a146726 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,6 @@ +# 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(Distributed Collector Gateway) diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e7f1fe --- /dev/null +++ b/README.md @@ -0,0 +1,441 @@ +# Distributed Collector Gateway - 使用说明 + +## 功能概述 + +本设备是一个分布式采集网关,支持通过 MQTT 远程控制 MODBUS RTU 轮询参数,并定期上报设备状态信息。 + +## SNTP 时间同步 + +设备内置 SNTP(Simple Network Time Protocol)时间同步功能,在网络连接成功后会自动同步系统时间。 + +### 时间同步特性 + +- **自动同步**:获取 IP 地址后自动启动 SNTP 客户端 +- **多服务器支持**:配置了中国地区多个 NTP 服务器 + - `cn.pool.ntp.org` - 中国 NTP 服务器 + - `ntp1.aliyun.com` - 阿里云 NTP 服务器 + - `ntp.tencent.com` - 腾讯云 NTP 服务器 +- **时区设置**:自动设置为北京时间(CST-8) +- **超时保护**:等待时间同步最长 10 秒,超时后使用本地时间继续运行 + +### 时间同步状态 + +设备会在启动日志中显示时间同步状态: + +``` +I (xxxx) SNTP_ESP: 初始化SNTP服务 +I (xxxx) SNTP_ESP: 时区设置为北京时间 (CST-8) +I (xxxx) SNTP_ESP: 当前时间: 1970-01-01 08:00:00 Thursday (同步前) +I (xxxx) SNTP_ESP: 时间同步成功: 2026-02-01 15:30:45 (同步后) +I (xxxx) SNTP_ESP: 时间同步完成 +``` + +### 时间在系统中的应用 + +同步后的时间会在以下场景中使用: + +1. **设备状态上报** - `update_time` 字段显示精确的同步时间 +2. **数据采集记录** - 可扩展用于记录数据采集时间戳 +3. **日志时间戳** - 方便调试和问题追踪 + +### 注意事项 + +- 需要网络连接正常才能进行时间同步 +- 首次启动时,时间同步可能需要几秒钟 +- 如果网络连接异常,设备会使用本地时间继续工作 +- 时间同步完成后,系统时间会持续由 SNTP 守护进程维护 + +## 设备状态上报 + +### 上报主题 + +`CONFIG_MQTT_PUB_TOPIC` (在 `sdkconfig` 中配置) + +### 上报类型 + +设备会定期上报以下类型的数据: + +1. **设备状态**(`message_type: "device_status"`) +2. **MODBUS 采集数据**(`function_code: 3`) + +### 设备状态上报格式 + +```json +{ + "message_type": "device_status", + "mac_address": "D0:CF:13:1B:C3:94", + "ip_address": "192.168.1.100", + "chip_model": "ESP32-S3", + "idf_version": "v5.5.2-dirty", + "uptime": 16, + "uptime_desc": "16秒", + "free_heap": 312996, + "status": "online", + "status_desc": "在线", + "update_time": "2026-02-01 15:30:45", + "led1_state": 1, + "led1_desc": "常亮", + "led1_function": "网络状态灯", + "led2_state": 4, + "led2_desc": "心跳", + "led2_function": "通信状态灯", + "modbus_enabled": 1, + "modbus_enabled_desc": "启用", + "modbus_channel": 0, + "modbus_channel_desc": "通道0 (UART0)", + "modbus_slave_addr": 1, + "modbus_interval": 1000, + "heap_status": "充足" +} +``` + +### 字段说明 + +#### 基本信息 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `message_type` | string | 消息类型:`"device_status"` | +| `mac_address` | string | 设备 WiFi MAC 地址(唯一标识) | +| `ip_address` | string | 设备 IP 地址 | +| `chip_model` | string | 芯片型号 | +| `idf_version` | string | ESP-IDF 版本 | + +#### 运行状态 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `uptime` | number | 设备运行时间(秒) | +| `uptime_desc` | string | 运行时间中文描述(如:"1天5小时30分") | +| `free_heap` | number | 剩余堆内存(字节) | +| `heap_status` | string | 内存状态:`"充足"` / `"一般"` / `"紧张"` | +| `status` | string | 设备状态:`"online"` | +| `status_desc` | string | 设备状态中文描述:`"在线"` | +| `update_time` | string | 状态更新时间(YYYY-MM-DD HH:MM:SS,通过 SNTP 同步) | + +#### LED 状态 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `led1_state` | number | LED1 状态:0=关闭, 1=常亮, 2=慢闪, 3=快闪, 4=心跳 | +| `led1_desc` | string | LED1 状态中文描述 | +| `led1_function` | string | LED1 功能:`"网络状态灯"` | +| `led2_state` | number | LED2 状态:同 LED1 | +| `led2_desc` | string | LED2 状态中文描述 | +| `led2_function` | string | LED2 功能:`"通信状态灯"` | + +#### MODBUS 轮询状态 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `modbus_enabled` | number | MODBUS 轮询是否启用(0=禁用, 1=启用) | +| `modbus_enabled_desc` | string | 轮询状态中文描述:`"启用"` / `"禁用"` / `"未配置"` | +| `modbus_channel` | number | 当前使用的 RS485 通道(0 或 1) | +| `modbus_channel_desc` | string | 通道中文描述 | +| `modbus_slave_addr` | number | 当前轮询的从机地址 | +| `modbus_interval` | number | 当前轮询间隔(毫秒) | + +### 中文提示字段说明 + +带有 `_desc` 后缀的字段是中文提示字段,设计用于: +- **直接显示在 Web 页面上**,无需额外转换 +- **不会被程序解析**,仅用于展示 +- **提升用户体验**,让状态更直观 + +Web 页面可以选择显示: +- 程序解析字段(如 `led1_state`)用于逻辑判断 +- 中文描述字段(如 `led1_desc`)用于界面显示 + +### 上报时机 + +- MQTT 订阅成功后立即上报一次 +- 之后每隔 10 秒上报一次(可在 main.c 中修改 `mqtt_start_device_status_task(10000)` 的参数) + +--- + +## MODBUS 控制说明 + +本设备是一个分布式采集网关,支持通过 MQTT 远程控制 MODBUS RTU 轮询参数。 + +## MQTT 控制指令 + +### 控制主题 + +发布控制指令到订阅主题:`CONFIG_MQTT_SUB_TOPIC` (在 `sdkconfig` 中配置) + +### 指令格式 (JSON) + +```json +{ + "command": "modbus_poll", + "channel": 0, + "slave_addr": 1, + "start_addr": 0, + "reg_count": 2, + "interval": 1000, + "enabled": true +} +``` + +### 参数说明 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `command` | string | 是 | 固定为 `"modbus_poll"` | +| `channel` | number | 是 | RS485 通道号 (0 或 1) | +| `slave_addr` | number | 是 | 从机地址 (1-247) | +| `start_addr` | number | 是 | 起始寄存器地址 (0-65535) | +| `reg_count` | number | 是 | 读取寄存器数量 (1-125) | +| `interval` | number | 是 | 轮询间隔(毫秒),最小 100ms | +| `enabled` | boolean | 否 | 是否启用轮询,默认 `true` | + +## 使用示例 + +### 示例 1:读取设备地址 1 的寄存器 + +```json +{ + "command": "modbus_poll", + "channel": 0, + "slave_addr": 1, + "start_addr": 0, + "reg_count": 2, + "interval": 1000 +} +``` + +设备会每 1 秒读取一次从机地址 1,从寄存器 0 开始的 2 个寄存器。 + +### 示例 2:读取设备地址 10 的寄存器 + +```json +{ + "command": "modbus_poll", + "channel": 0, + "slave_addr": 10, + "start_addr": 0, + "reg_count": 5, + "interval": 2000 +} +``` + +设备会每 2 秒读取一次从机地址 10,从寄存器 0 开始的 5 个寄存器。 + +### 示例 3:读取指定范围的寄存器 + +```json +{ + "command": "modbus_poll", + "channel": 0, + "slave_addr": 1, + "start_addr": 10, + "reg_count": 4, + "interval": 500 +} +``` + +设备会每 500ms 读取一次从机地址 1,从寄存器 10 开始的 4 个寄存器(地址 10, 11, 12, 13)。 + +### 示例 4:停止轮询 + +```json +{ + "command": "modbus_poll", + "channel": 0, + "slave_addr": 1, + "start_addr": 0, + "reg_count": 2, + "interval": 1000, + "enabled": false +} +``` + +设置 `"enabled": false` 可以暂停轮询任务。 + +### 示例 5:使用 RS485 通道 1 + +```json +{ + "command": "modbus_poll", + "channel": 1, + "slave_addr": 5, + "start_addr": 0, + "reg_count": 10, + "interval": 1000 +} +``` + +使用 RS485 通道 1 进行轮询。 + +## 数据上报 + +### 上报主题 + +`CONFIG_MQTT_PUB_TOPIC` (在 `sdkconfig` 中配置) + +### 上报数据格式 (JSON) + +```json +{ + "channel": "RS485-1", + "slave_addr": 1, + "function_code": 3, + "status": "success", + "byte_count": 4, + "register_count": 2, + "registers": [ + 556, + 998 + ] +} +``` + +### 字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `channel` | string | RS485 通道名称 | +| `slave_addr` | number | 从机地址 | +| `function_code` | number | MODBUS 功能码(始终为 3) | +| `status` | string | 状态:`"success"` 或 `"exception"` | +| `byte_count` | number | 数据字节数 | +| `register_count` | number | 寄存器数量 | +| `registers` | array | 寄存器值数组 | + +### 异常响应格式 + +```json +{ + "channel": "RS485-1", + "slave_addr": 1, + "function_code": 131, + "status": "exception", + "exception_code": 2 +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `exception_code` | number | 异常码:
1 - 非法功能
2 - 非法数据地址
3 - 非法数据值
4 - 服务器设备故障 | + +## 动态更新 + +每次发送 MQTT 控制指令都会立即更新轮询参数,无需重启设备。 + +### 更新流程 + +1. 发送新的控制指令 +2. 设备接收并解析 JSON +3. 立即更新轮询配置 +4. 下一次轮询使用新参数 + +## MQTT 控制指令 + +### 控制主题 + +发布控制指令到订阅主题:`CONFIG_MQTT_SUB_TOPIC` (在 `sdkconfig` 中配置) + +### 指令格式 (JSON) + +```json +{ + "command": "modbus_poll", + "channel": 0, + "slave_addr": 1, + "start_addr": 0, + "reg_count": 2, + "interval": 1000, + "enabled": true +} +``` + +### 参数说明 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `command` | string | 是 | 固定为 `"modbus_poll"` | +| `channel` | number | 是 | RS485 通道号 (0 或 1) | +| `slave_addr` | number | 是 | 从机地址 (1-247) | +| `start_addr` | number | 是 | 起始寄存器地址 (0-65535) | +| `reg_count` | number | 是 | 读取寄存器数量 (1-125) | +| `interval` | number | 是 | 轮询间隔(毫秒),最小 100ms | +| `enabled` | boolean | 否 | 是否启用轮询,默认 `true` | + +### 支持的指令类型 + +当前支持以下控制指令: + +#### 1. MODBUS 轮询控制 + +```json +{ + "command": "modbus_poll", + "channel": 0, + "slave_addr": 1, + "start_addr": 0, + "reg_count": 2, + "interval": 1000, + "enabled": true +} +``` + +#### 2. 设备状态上报间隔控制(可选扩展) + +可通过修改 `main.c` 中的参数来调整状态上报间隔: + +```c +mqtt_start_device_status_task(10000); // 10000ms = 10秒 +``` + +或运行时调用 API: + +```c +mqtt_update_report_interval(5000); // 5秒 +``` + +## 注意事项 + +1. **轮询间隔最小为 100ms**,设置更小的值会被拒绝 +2. **寄存器数量最大为 125**,MODBUS RTU 限制 +3. **从机地址范围 1-247**,0 为广播地址 +4. **通道号只能是 0 或 1** +5. 首次发送指令后会自动启动轮询任务 + +## 使用 mosquitto_cli 测试 + +### 发送控制指令 + +```bash +mosquitto_pub -h -p 1883 -t -m '{ + "command": "modbus_poll", + "channel": 0, + "slave_addr": 1, + "start_addr": 0, + "reg_count": 2, + "interval": 1000 +}' +``` + +### 监听数据上报 + +```bash +mosquitto_sub -h -p 1883 -t +``` + +## 硬件连接 + +### RS485 通道 0 (UART0) + +| 引脚 | GPIO | 功能 | +|------|------|------| +| RO (RX) | GPIO 41 | RS485 接收器输出 | +| DE/RE | GPIO 42 | 数据使能/接收器使能 | +| DI (TX) | GPIO 44 | RS485 驱动器输入 | + +### RS485 通道 1 (UART2) + +| 引脚 | GPIO | 功能 | +|------|------|------| +| RO (RX) | GPIO 43 | RS485 接收器输出 | +| DE/RE | GPIO 2 | 数据使能/接收器使能 | +| DI (TX) | GPIO 1 | RS485 驱动器输入 | diff --git a/components/ETH_CH390H/CMakeLists.txt b/components/ETH_CH390H/CMakeLists.txt new file mode 100644 index 0000000..3656e40 --- /dev/null +++ b/components/ETH_CH390H/CMakeLists.txt @@ -0,0 +1,7 @@ +idf_component_register(SRCS + "ETH_CH390H.c" + "esp_eth_mac_ch390.c" + "esp_eth_phy_ch390.c" + INCLUDE_DIRS "include" + REQUIRES esp_eth esp_netif driver esp_timer STATUS_LED + ) diff --git a/components/ETH_CH390H/ETH_CH390H.c b/components/ETH_CH390H/ETH_CH390H.c new file mode 100644 index 0000000..588a0cc --- /dev/null +++ b/components/ETH_CH390H/ETH_CH390H.c @@ -0,0 +1,141 @@ +/* + * 文件: ETH_CH390H.c + * 描述: CH390H SPI 以太网模块初始化与事件处理封装。 + * + * 功能: + * - 初始化 SPI 总线并配置 CH390H 设备 + * - 安装并启动 esp-eth 驱动 + * - 注册以太网事件回调(连接/断开/启动/停止)和获取 IP 回调 + * + * 用法: + * 1. 在 app_main() 中调用 eth_init() 完成初始化,例如: + * eth_init(); + * 2. 如需自定义 GPIO/SPI 配置,可修改本文件顶部的宏定义。 + * 3. 如需自定义事件处理,修改 eth_event_handler 或 got_ip_event_handler。 + * + * 注意: + * - 本模块使用 esp_netif 和 esp_event,调用前请确保没有重复初始化。 + * - 调试时可通过调整 mac_config.rx_task_stack_size 或 SPI_CLOCK_MHZ 优化性能。 + */ + + +#include "ETH_CH390H.h" +#include "STATUS_LED.h" + + +static const char *TAG = "eth_ch390h"; + + + +/* 事件处理函数 + * 处理 esp-eth 发出的以太网状态事件: + * - ETHERNET_EVENT_CONNECTED : 已连接(物理链路/链路层可用) + * - ETHERNET_EVENT_DISCONNECTED : 断开(物理链路丢失) + * - ETHERNET_EVENT_START : 以太网驱动启动 + * - ETHERNET_EVENT_STOP : 以太网驱动停止 + * 参数: + * arg - 注册时传入的参数(当前未使用) + * event_base - 事件基(ETH_EVENT) + * event_id - 事件 id + * event_data - 事件相关数据(视事件而定) + */ +static void eth_event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + switch (event_id) + { + case ETHERNET_EVENT_CONNECTED: + ESP_LOGI(TAG, "以太网连接成功"); + status_led_set(1, 1); // LED1 常亮:物理连接正常 + break; + case ETHERNET_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "以太网断开连接"); + status_led_blink_mode(1, 1); // LED1 快闪:网络断开 + break; + case ETHERNET_EVENT_START: + ESP_LOGI(TAG, "以太网开始工作"); + break; + case ETHERNET_EVENT_STOP: + ESP_LOGI(TAG, "以太网停止工作"); + break; + default: + break; + } +} + +/* 获取 IP 回调 + * 当网口获取到 IP(DHCP 或静态)时调用,打印分配到的 IP 信息。 + * 参数同上,event_data 可转换为 ip_event_got_ip_t* 来读取 ip 信息。 + */ +static void got_ip_event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; + ESP_LOGI(TAG, "获取到的IP: " IPSTR, IP2STR(&event->ip_info.ip)); + status_led_set(1, 1); // LED1 常亮:IP获取成功,网络就绪 +} + +/* eth_init + * 初始化并启动 CH390H 以太网设备的封装函数: + * 1. 初始化网络接口与默认事件循环 + * 2. 配置并初始化 SPI 总线(供 CH390H 使用) + * 3. 配置 CH390H 的 mac/phy,并安装 esp-eth 驱动 + * 4. 将 esp-netif 绑定到以太网驱动,并注册事件回调 + * 5. 启动以太网驱动 + * + * 注意: + * - 若需要修改引脚或 SPI 频率,可在文件顶部宏中调整 + * - 可根据需要调整 mac_config、phy_config 中的参数以优化性能 + */ +void eth_init(void) +{ + esp_netif_init(); + esp_event_loop_create_default(); + + esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH(); + esp_netif_t *eth_netif = esp_netif_new(&cfg); + + /* 设置以太网设备在 DHCP/路由器中的主机名(需在启动前设置) */ + esp_netif_set_hostname(eth_netif, "Distributed Collector Gateway"); + + gpio_install_isr_service(0); + + spi_bus_config_t buscfg = { + .mosi_io_num = ETH_MOSI_GPIO, + .miso_io_num = ETH_MISO_GPIO, + .sclk_io_num = ETH_SCLK_GPIO, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + }; + ESP_ERROR_CHECK(spi_bus_initialize(SPI_HOST, &buscfg, SPI_DMA_CH_AUTO)); + + spi_device_interface_config_t spi_devcfg = { + .mode = 0, + .clock_speed_hz = SPI_CLOCK_MHZ * 1000 * 1000, + .spics_io_num = ETH_CS_GPIO, + .queue_size = 20, + }; + + eth_ch390_config_t ch390_config = ETH_CH390_DEFAULT_CONFIG(SPI_HOST, &spi_devcfg); + ch390_config.int_gpio_num = ETH_INT_GPIO; + + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + mac_config.rx_task_stack_size = 4096; + + esp_eth_mac_t *mac = esp_eth_mac_new_ch390(&ch390_config, &mac_config); + + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + esp_eth_phy_t *phy = esp_eth_phy_new_ch390(&phy_config); + + esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy); + + esp_eth_handle_t eth_handle = NULL; + ESP_ERROR_CHECK(esp_eth_driver_install(ð_config, ð_handle)); + + ESP_ERROR_CHECK(esp_netif_attach(eth_netif, esp_eth_new_netif_glue(eth_handle))); + + esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, ð_event_handler, NULL); + esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &got_ip_event_handler, NULL); + + ESP_ERROR_CHECK(esp_eth_start(eth_handle)); +} diff --git a/components/ETH_CH390H/esp_eth_mac_ch390.c b/components/ETH_CH390H/esp_eth_mac_ch390.c new file mode 100644 index 0000000..d098136 --- /dev/null +++ b/components/ETH_CH390H/esp_eth_mac_ch390.c @@ -0,0 +1,930 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + * + * SPDX-FileContributor: 2024-2025 Sergey Kharenko + * SPDX-FileContributor: 2024-2025 Espressif Systems (Shanghai) CO LTD + */ + +#include +#include +#include +#include "driver/gpio.h" +#include "driver/spi_master.h" +#include "esp_attr.h" +#include "esp_log.h" +#include "esp_check.h" +#include "esp_eth_driver.h" +#include "esp_timer.h" +#include "esp_system.h" +#include "esp_intr_alloc.h" +#include "esp_heap_caps.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" + +#include "ch390.h" +#include "esp_eth_mac_ch390.h" + +/** @note ----------------------- RX Pack Structure --------------------------- + * | 4 Bytes Frame Head | Data Area(pass to lwip) | + * | Head | Status | Length(Low) | Length(High) | ........ | + * | | + * | | + * | | Should be the value of RSR( @ref CH390_RSR). We use @ref RSR_ERR_MASK to determine + * | | whether the pack has error. + * | |------------------------------------------------------------------------------------- + * | + * | Depends on RCSEN bit( @ref RCSCSR_RCSEN) of RCSCSR( @ref CH390_RCSCSR) + * | - RCSEN = 0, the Head should always be 0x01 + * | - RCSEN = 1, bit 7:2 of the Head is the same as that of RCSCSR; + * | This will affect the determination of the validity of the packet. Therefore, + * | we provide discriminant masks for both cases. + * | - RCSEN = 0 ---> @ref CH390_PKT_ERR + * | - RCSEN = 1 ---> @ref CH390_PKT_ERR_WITH_RCSEN + * |---------------------------------------------------------------------------------------------- +*/ + +static const char *TAG = "ch390.mac"; + +#define CH390_SPI_LOCK_TIMEOUT_MS (50) +#define CH390_MAC_TX_WAIT_TIMEOUT_US (1000) +#define CH390_PHY_OPERATION_TIMEOUT_US (1000) + +typedef struct { + uint8_t flag; + uint8_t status; + uint8_t length_low; + uint8_t length_high; +} ch390_rx_header_t; + +typedef struct { + spi_device_handle_t hdl; + SemaphoreHandle_t lock; +} eth_spi_info_t; + +typedef struct { + void *ctx; + void *(*init)(const void *spi_config); + esp_err_t (*deinit)(void *spi_ctx); + esp_err_t (*read)(void *spi_ctx, uint32_t cmd, uint32_t addr, void *data, uint32_t data_len); + esp_err_t (*write)(void *spi_ctx, uint32_t cmd, uint32_t addr, const void *data, uint32_t data_len); +} eth_spi_custom_driver_t; + +typedef struct { + esp_eth_mac_t parent; + esp_eth_mediator_t *eth; + eth_spi_custom_driver_t spi; + TaskHandle_t rx_task_hdl; + uint32_t sw_reset_timeout_ms; + int int_gpio_num; + esp_timer_handle_t poll_timer; + uint32_t poll_period_ms; + uint8_t addr[ETH_ADDR_LEN]; + bool flow_ctrl_enabled; + uint8_t *rx_buffer; + uint32_t rx_len; +} emac_ch390_t; + +static inline bool CH390_SPI_LOCK(eth_spi_info_t *spi) +{ + return xSemaphoreTake(spi->lock, pdMS_TO_TICKS(CH390_SPI_LOCK_TIMEOUT_MS)) == pdTRUE; +} + +static inline bool CH390_SPI_UNLOCK(eth_spi_info_t *spi) +{ + return xSemaphoreGive(spi->lock) == pdTRUE; +} + +static void *CH390_SPI_INIT(const void *spi_config) +{ + void *ret = NULL; + eth_ch390_config_t *ch390_config = (eth_ch390_config_t *)spi_config; + eth_spi_info_t *spi = calloc(1, sizeof(eth_spi_info_t)); + ESP_GOTO_ON_FALSE(spi, NULL, err, TAG, "no memory for SPI context data"); + + /* SPI device init */ + spi_device_interface_config_t spi_devcfg; + spi_devcfg = *(ch390_config->spi_devcfg); + if (ch390_config->spi_devcfg->command_bits == 0 && ch390_config->spi_devcfg->address_bits == 0) { + /* configure default SPI frame format */ + spi_devcfg.command_bits = 1; + spi_devcfg.address_bits = 7; + } else { + ESP_GOTO_ON_FALSE(ch390_config->spi_devcfg->command_bits == 1 && ch390_config->spi_devcfg->address_bits == 7, + NULL, err, TAG, "incorrect SPI frame format (command_bits/address_bits)"); + } + ESP_GOTO_ON_FALSE(spi_bus_add_device(ch390_config->spi_host_id, &spi_devcfg, &spi->hdl) == ESP_OK, + NULL, err, TAG, "adding device to SPI host #%d failed", ch390_config->spi_host_id + 1); + + /* create mutex */ + spi->lock = xSemaphoreCreateMutex(); + ESP_GOTO_ON_FALSE(spi->lock, NULL, err, TAG, "create lock failed"); + + ret = spi; + return ret; +err: + if (spi) { + if (spi->lock) { + vSemaphoreDelete(spi->lock); + } + free(spi); + } + return ret; +} + +static esp_err_t CH390_SPI_DEINIT(void *spi_ctx) +{ + esp_err_t ret = ESP_OK; + eth_spi_info_t *spi = (eth_spi_info_t *)spi_ctx; + + spi_bus_remove_device(spi->hdl); + vSemaphoreDelete(spi->lock); + + free(spi); + return ret; +} + +static inline esp_err_t CH390_SPI_WRITE(void *spi_ctx, uint32_t cmd, uint32_t addr, const void *value, uint32_t len) +{ + esp_err_t ret = ESP_OK; + eth_spi_info_t *spi = (eth_spi_info_t *)spi_ctx; + + spi_transaction_t trans = { + .cmd = cmd, + .addr = addr, + .length = 8 * len, + .tx_buffer = value + }; + if (CH390_SPI_LOCK(spi)) { + if (spi_device_polling_transmit(spi->hdl, &trans) != ESP_OK) { + ESP_LOGE(TAG, "%s(%d): spi transmit failed", __FUNCTION__, __LINE__); + ret = ESP_FAIL; + } + CH390_SPI_UNLOCK(spi); + } else { + ret = ESP_ERR_TIMEOUT; + } + return ret; +} + +static inline esp_err_t CH390_SPI_READ(void *spi_ctx, uint32_t cmd, uint32_t addr, void *value, uint32_t len) +{ + esp_err_t ret = ESP_OK; + eth_spi_info_t *spi = (eth_spi_info_t *)spi_ctx; + + spi_transaction_t trans = { + .cmd = cmd, + .addr = addr, + .length = 8 * len, + .rx_buffer = value + }; + if (CH390_SPI_LOCK(spi)) { + if (spi_device_polling_transmit(spi->hdl, &trans) != ESP_OK) { + ESP_LOGE(TAG, "%s(%d): spi transmit failed", __FUNCTION__, __LINE__); + ret = ESP_FAIL; + } + CH390_SPI_UNLOCK(spi); + } else { + ret = ESP_ERR_TIMEOUT; + } + return ret; +} + +/** + * @brief write value to ch390 internal register + */ +static esp_err_t ch390_io_register_write(emac_ch390_t *emac, uint8_t reg_addr, uint8_t value) +{ + return emac->spi.write(emac->spi.ctx, CH390_SPI_WR, reg_addr, &value, 1); +} + +/** + * @brief read value from ch390 internal register + */ +static esp_err_t ch390_io_register_read(emac_ch390_t *emac, uint8_t reg_addr, uint8_t *value) +{ + return emac->spi.read(emac->spi.ctx, CH390_SPI_RD, reg_addr, value, 1); +} + +/** + * @brief write buffer to ch390 internal memory + */ +static esp_err_t ch390_io_memory_write(emac_ch390_t *emac, uint8_t *buffer, uint32_t len) +{ + return emac->spi.write(emac->spi.ctx, CH390_SPI_WR, CH390_MWCMD, buffer, len); +} + +/** + * @brief read buffer from ch390 internal memory + */ +static esp_err_t ch390_io_memory_read(emac_ch390_t *emac, uint8_t *buffer, uint32_t len) +{ + return emac->spi.read(emac->spi.ctx, CH390_SPI_RD, CH390_MRCMD, buffer, len); +} + +IRAM_ATTR static void ch390_isr_handler(void *arg) +{ + emac_ch390_t *emac = (emac_ch390_t *)arg; + BaseType_t high_task_wakeup = pdFALSE; + /* notify ch390 task */ + vTaskNotifyGiveFromISR(emac->rx_task_hdl, &high_task_wakeup); + if (high_task_wakeup != pdFALSE) { + portYIELD_FROM_ISR(); + } +} + +static void ch390_poll_timer(void *arg) +{ + emac_ch390_t *emac = (emac_ch390_t *)arg; + xTaskNotifyGive(emac->rx_task_hdl); +} + +/** + * @brief read mac address from internal registers + */ +static esp_err_t ch390_get_mac_addr(emac_ch390_t *emac) +{ + esp_err_t ret = ESP_OK; + for (int i = 0; i < ETH_ADDR_LEN; i++) { + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_PAR + i, &emac->addr[i]), err, TAG, "read PAR failed"); + } + return ESP_OK; +err: + return ret; +} + +/** + * @brief set new mac address to internal registers + */ +static esp_err_t ch390_set_mac_addr(emac_ch390_t *emac) +{ + esp_err_t ret = ESP_OK; + for (int i = 0; i < 6; i++) { + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_PAR + i, emac->addr[i]), err, TAG, "write PAR failed"); + } + return ESP_OK; +err: + return ret; +} + +/** + * @brief clear multicast hash table + */ +static esp_err_t ch390_clear_multicast_table(emac_ch390_t *emac) +{ + esp_err_t ret = ESP_OK; + /* rx broadcast packet control by bit7 of MAC register 1DH */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_BCASTCR, 0x00), err, TAG, "write BCASTCR failed"); + for (int i = 0; i < 7; i++) { + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_MAR + i, 0x00), err, TAG, "write MAR failed"); + } + /* enable receive broadcast paclets */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_MAR + 7, 0x80), err, TAG, "write MAR failed"); + return ESP_OK; +err: + return ret; +} + +/** + * @brief software reset ch390 internal register + */ +static esp_err_t ch390_reset(emac_ch390_t *emac) +{ + esp_err_t ret = ESP_OK; + /* software reset */ + uint8_t ncr = NCR_RST; + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_NCR, ncr), err, TAG, "write NCR failed"); + uint32_t to = 0; + for (to = 0; to < emac->sw_reset_timeout_ms / 10; to++) { + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_NCR, &ncr), err, TAG, "read NCR failed"); + if (!(ncr & NCR_RST)) { + break; + } + vTaskDelay(pdMS_TO_TICKS(10)); + } + ESP_GOTO_ON_FALSE(to < emac->sw_reset_timeout_ms / 10, ESP_ERR_TIMEOUT, err, TAG, "reset timeout"); + + /* For CH390H/D, phy should be power on after software reset !*/ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_GPR, 0x00), err, TAG, "write GPR failed"); + /* mac and phy register won't be accessible within at least 1ms */ + vTaskDelay(pdMS_TO_TICKS(10)); + + return ESP_OK; +err: + return ret; +} + +/** + * @brief verify ch390 chip ID + */ +static esp_err_t ch390_verify_id(emac_ch390_t *emac) +{ + esp_err_t ret = ESP_OK; + uint8_t id[2]; + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_VIDL, &id[0]), err, TAG, "read VIDL failed"); + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_VIDH, &id[1]), err, TAG, "read VIDH failed"); + ESP_GOTO_ON_FALSE(0x1C == id[1] && 0x00 == id[0], ESP_ERR_INVALID_VERSION, err, TAG, "wrong Vendor ID"); + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_PIDL, &id[0]), err, TAG, "read PIDL failed"); + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_PIDH, &id[1]), err, TAG, "read PIDH failed"); + ESP_GOTO_ON_FALSE(0x91 == id[1] && 0x51 == id[0], ESP_ERR_INVALID_VERSION, err, TAG, "wrong Product ID"); + return ESP_OK; +err: + return ret; +} + +/** + * @brief default setup for ch390 internal registers + */ +static esp_err_t ch390_setup_default(emac_ch390_t *emac) +{ + esp_err_t ret = ESP_OK; + /* disable wakeup */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_NCR, 0x00), err, TAG, "write NCR failed"); + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_WCR, 0x00), err, TAG, "write WCR failed"); + /* stop transmitting, enable appending pad, crc for packets */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_TCR, 0x00), err, TAG, "write TCR failed"); + /* stop receiving, no promiscuous mode, no runt packet(size < 64bytes), receive all multicast packets */ + /* discard long packet(size > 1522bytes) and crc error packet, enable watchdog */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_RCR, RCR_DIS_CRC | RCR_ALL), err, TAG, "write RCR failed"); + /* retry late collision packet, at most two transmit command can be issued before transmit complete */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_TCR2, TCR2_RLCP), err, TAG, "write TCR2 failed"); + /* generate checksum for UDP, TCP and IPv4 packets */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_TCSCR, TCSCR_IPCSE | TCSCR_TCPCSE | TCSCR_UDPCSE), err, TAG, "write TCSCR failed"); + /* disable check sum for receive packets */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_RCSCSR, 0x00), err, TAG, "write RCSCSR failed"); + /* interrupt pin config: push-pull output, active high */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_INTCR, 0x00), err, TAG, "write INTCR failed"); + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_INTCKCR, 0x00), err, TAG, "write INTCKCR failed"); + /* set length limitation for rx packets to 1536(64*24)*/ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_RLENCR, RLENCR_RXLEN_EN | RLENCR_RXLEN_DEFAULT), err, TAG, "write RLENCR failed"); + /* clear network status: wakeup event, tx complete */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_NSR, NSR_WAKEST | NSR_TX2END | NSR_TX1END), err, TAG, "write NSR failed"); + return ESP_OK; +err: + return ret; +} + +static esp_err_t ch390_enable_flow_ctrl(emac_ch390_t *emac, bool enable) +{ + esp_err_t ret = ESP_OK; + if (enable) { + /* send jam pattern (duration time = 1.15ms) when rx free space < 3k bytes */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_BPTR, 0x3F), err, TAG, "write BPTR failed"); + /* flow control: high water threshold = 3k bytes, low water threshold = 8k bytes */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_FCTR, FCTR_HWOT(3) | FCTR_LWOT(8)), err, TAG, "write FCTR failed"); + /* enable flow control */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_FCR, FCR_FLOW_ENABLE), err, TAG, "write FCR failed"); + } else { + /* disable flow control */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_FCR, 0), err, TAG, "write FCR failed"); + } + return ESP_OK; +err: + return ret; +} + +static esp_err_t ch390_drop_frame(emac_ch390_t *emac, uint16_t length) +{ + esp_err_t ret = ESP_OK; + uint8_t mrrh, mrrl; + + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_MRRH, &mrrh), err, TAG, "read MDRAH failed"); + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_MRRL, &mrrl), err, TAG, "read MDRAL failed"); + + uint16_t addr = mrrh << 8 | mrrl; + /* include 4B for header */ + addr += length; + + addr = addr < 0x4000 ? addr : addr - 0x3400; + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_MRRH, addr >> 8), err, TAG, "write MDRAH failed"); + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_MRRL, addr & 0xFF), err, TAG, "write MDRAL failed"); +err: + return ret; +} + +/** + * @brief start ch390: enable interrupt and start receive + */ +static esp_err_t emac_ch390_start(esp_eth_mac_t *mac) +{ + esp_err_t ret = ESP_OK; + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + /* reset rx memory pointer */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_MPTRCR, MPTRCR_RST_RX), err, TAG, "write MPTRCR failed"); + /* clear interrupt status */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_ISR, ISR_CLR_STATUS), err, TAG, "write ISR failed"); + /* enable only Rx related interrupts as others are processed synchronously */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_IMR, IMR_PAR | IMR_PRI), err, TAG, "write IMR failed"); + /* enable rx */ + uint8_t rcr = 0; + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_RCR, &rcr), err, TAG, "read RCR failed"); + rcr |= RCR_RXEN; + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_RCR, rcr), err, TAG, "write RCR failed"); + return ESP_OK; +err: + return ret; +} + +/** + * @brief stop ch390: disable interrupt and stop receive + */ +static esp_err_t emac_ch390_stop(esp_eth_mac_t *mac) +{ + esp_err_t ret = ESP_OK; + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + /* disable interrupt */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_IMR, 0x00), err, TAG, "write IMR failed"); + /* disable rx */ + uint8_t rcr = 0; + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_RCR, &rcr), err, TAG, "read RCR failed"); + rcr &= ~RCR_RXEN; + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_RCR, rcr), err, TAG, "write RCR failed"); + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_ch390_set_mediator(esp_eth_mac_t *mac, esp_eth_mediator_t *eth) +{ + esp_err_t ret = ESP_OK; + ESP_GOTO_ON_FALSE(eth, ESP_ERR_INVALID_ARG, err, TAG, "can't set mac's mediator to null"); + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + emac->eth = eth; + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_ch390_write_phy_reg(esp_eth_mac_t *mac, uint32_t phy_addr, uint32_t phy_reg, uint32_t reg_value) +{ + esp_err_t ret = ESP_OK; + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + /* check if phy access is in progress */ + uint8_t epcr = 0; + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_EPCR, &epcr), err, TAG, "read EPCR failed"); + ESP_GOTO_ON_FALSE(!(epcr & EPCR_ERRE), ESP_ERR_INVALID_STATE, err, TAG, "phy is busy"); + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_EPAR, (uint8_t)(CH390_PHY | phy_reg)), err, TAG, "write EPAR failed"); + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_EPDRL, (uint8_t)(reg_value & 0xFF)), err, TAG, "write EPDRL failed"); + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_EPDRH, (uint8_t)((reg_value >> 8) & 0xFF)), err, TAG, "write EPDRH failed"); + /* select PHY and select write operation */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_EPCR, EPCR_EPOS | EPCR_ERPRW), err, TAG, "write EPCR failed"); + /* polling the busy flag */ + uint32_t to = 0; + do { + esp_rom_delay_us(100); + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_EPCR, &epcr), err, TAG, "read EPCR failed"); + to += 100; + } while ((epcr & EPCR_ERRE) && to < CH390_PHY_OPERATION_TIMEOUT_US); + ESP_GOTO_ON_FALSE(!(epcr & EPCR_ERRE), ESP_ERR_TIMEOUT, err, TAG, "phy is busy"); + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_ch390_read_phy_reg(esp_eth_mac_t *mac, uint32_t phy_addr, uint32_t phy_reg, uint32_t *reg_value) +{ + esp_err_t ret = ESP_OK; + ESP_GOTO_ON_FALSE(reg_value, ESP_ERR_INVALID_ARG, err, TAG, "can't set reg_value to null"); + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + /* check if phy access is in progress */ + uint8_t epcr = 0; + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_EPCR, &epcr), err, TAG, "read EPCR failed"); + ESP_GOTO_ON_FALSE(!(epcr & 0x01), ESP_ERR_INVALID_STATE, err, TAG, "phy is busy"); + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_EPAR, (uint8_t)(CH390_PHY | phy_reg)), err, TAG, "write EPAR failed"); + /* Select PHY and select read operation */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_EPCR, 0x0C), err, TAG, "write EPCR failed"); + /* polling the busy flag */ + uint32_t to = 0; + do { + esp_rom_delay_us(100); + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_EPCR, &epcr), err, TAG, "read EPCR failed"); + to += 100; + } while ((epcr & EPCR_ERRE) && to < CH390_PHY_OPERATION_TIMEOUT_US); + ESP_GOTO_ON_FALSE(!(epcr & EPCR_ERRE), ESP_ERR_TIMEOUT, err, TAG, "phy is busy"); + uint8_t value_h = 0; + uint8_t value_l = 0; + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_EPDRH, &value_h), err, TAG, "read EPDRH failed"); + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_EPDRL, &value_l), err, TAG, "read EPDRL failed"); + *reg_value = (value_h << 8) | value_l; + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_ch390_set_addr(esp_eth_mac_t *mac, uint8_t *addr) +{ + esp_err_t ret = ESP_OK; + ESP_GOTO_ON_FALSE(addr, ESP_ERR_INVALID_ARG, err, TAG, "can't set mac addr to null"); + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + memcpy(emac->addr, addr, 6); + ESP_GOTO_ON_ERROR(ch390_set_mac_addr(emac), err, TAG, "set mac address failed"); + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_ch390_get_addr(esp_eth_mac_t *mac, uint8_t *addr) +{ + esp_err_t ret = ESP_OK; + ESP_GOTO_ON_FALSE(addr, ESP_ERR_INVALID_ARG, err, TAG, "can't set mac addr to null"); + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + memcpy(addr, emac->addr, 6); + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_ch390_set_link(esp_eth_mac_t *mac, eth_link_t link) +{ + esp_err_t ret = ESP_OK; + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + switch (link) { + case ETH_LINK_UP: + ESP_GOTO_ON_ERROR(mac->start(mac), err, TAG, "ch390 start failed"); + if (emac->poll_timer) { + ESP_GOTO_ON_ERROR(esp_timer_start_periodic(emac->poll_timer, emac->poll_period_ms * 1000), + err, TAG, "start poll timer failed"); + } + break; + case ETH_LINK_DOWN: + ESP_GOTO_ON_ERROR(mac->stop(mac), err, TAG, "ch390 stop failed"); + if (emac->poll_timer) { + ESP_GOTO_ON_ERROR(esp_timer_stop(emac->poll_timer), + err, TAG, "stop poll timer failed"); + } + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_INVALID_ARG, err, TAG, "unknown link status"); + break; + } + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_ch390_set_speed(esp_eth_mac_t *mac, eth_speed_t speed) +{ + esp_err_t ret = ESP_OK; + switch (speed) { + case ETH_SPEED_10M: + ESP_LOGD(TAG, "working in 10Mbps"); + break; + case ETH_SPEED_100M: + ESP_LOGD(TAG, "working in 100Mbps"); + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_INVALID_ARG, err, TAG, "unknown speed"); + break; + } + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_ch390_set_duplex(esp_eth_mac_t *mac, eth_duplex_t duplex) +{ + esp_err_t ret = ESP_OK; + switch (duplex) { + case ETH_DUPLEX_HALF: + ESP_LOGD(TAG, "working in half duplex"); + break; + case ETH_DUPLEX_FULL: + ESP_LOGD(TAG, "working in full duplex"); + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_INVALID_ARG, err, TAG, "unknown duplex"); + break; + } + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_ch390_set_promiscuous(esp_eth_mac_t *mac, bool enable) +{ + esp_err_t ret = ESP_OK; + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + uint8_t rcr = 0; + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_RCR, &rcr), err, TAG, "read RCR failed"); + if (enable) { + rcr |= RCR_PRMSC; + } else { + rcr &= ~RCR_PRMSC; + } + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_RCR, rcr), err, TAG, "write RCR failed"); + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_ch390_enable_flow_ctrl(esp_eth_mac_t *mac, bool enable) +{ + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + emac->flow_ctrl_enabled = enable; + return ESP_OK; +} + +static esp_err_t emac_ch390_set_peer_pause_ability(esp_eth_mac_t *mac, uint32_t ability) +{ + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + // we want to enable flow control, and peer does support pause function + // then configure the MAC layer to enable flow control feature + if (emac->flow_ctrl_enabled && ability) { + ch390_enable_flow_ctrl(emac, true); + } + + else { + ch390_enable_flow_ctrl(emac, false); + ESP_LOGD(TAG, "Flow control not enabled for the link"); + } + return ESP_OK; +} + +static esp_err_t emac_ch390_transmit(esp_eth_mac_t *mac, uint8_t *buf, uint32_t length) +{ + esp_err_t ret = ESP_OK; + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + uint8_t tcr = 0; + + ESP_GOTO_ON_FALSE(length <= ETH_MAX_PACKET_SIZE, ESP_ERR_INVALID_ARG, err, + TAG, "frame size is too big (actual %lu, maximum %u)", + length, ETH_MAX_PACKET_SIZE); + + /* copy data to tx memory */ + ESP_GOTO_ON_ERROR(ch390_io_memory_write(emac, buf, length), err, TAG, + "write memory failed"); + + /* Check if last transmit complete */ + int64_t wait_time = esp_timer_get_time(); + do { + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_TCR, &tcr), err, TAG, + "read TCR failed"); + } while ((tcr & TCR_TXREQ) && ((esp_timer_get_time() - wait_time) < CH390_MAC_TX_WAIT_TIMEOUT_US)); + + if (tcr & TCR_TXREQ) { + ESP_LOGE(TAG, "last transmit still in progress, cannot send."); + return ESP_ERR_INVALID_STATE; + } + + /* set tx length */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_TXPLL, length & 0xFF), err, TAG, "write TXPLL failed"); + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_TXPLH, (length >> 8) & 0xFF), err, TAG, "write TXPLH failed"); + + /* issue tx polling command */ + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_TCR, &tcr), err, TAG, "read TCR failed"); + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_TCR, tcr | TCR_TXREQ), err, TAG, "write TCR failed"); + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_ch390_receive(esp_eth_mac_t *mac, uint8_t *buf, uint32_t *length) +{ + esp_err_t ret = ESP_OK; + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + + uint8_t ready; + /* dummy read, get the most updated data */ + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_MRCMDX, &ready), err, TAG, "read MRCMDX failed"); + ESP_GOTO_ON_ERROR(ch390_io_register_read(emac, CH390_MRCMDX, &ready), err, TAG, "read MRCMDX failed"); + + // if ready != 1 or 0 reset device + if (ready & CH390_PKT_ERR) { + emac_ch390_stop(mac); + esp_rom_delay_us(1000); + emac_ch390_start(mac); + + ESP_LOGE(TAG, "PACK ERR"); + return ESP_ERR_INVALID_RESPONSE; + } else { + __attribute__((aligned(4))) ch390_rx_header_t rx_header; // SPI driver needs the rx buffer 4 byte align + + if (ready & CH390_PKT_RDY) { + ESP_GOTO_ON_ERROR(ch390_io_memory_read(emac, (uint8_t *) & (rx_header), sizeof(rx_header)), + err, TAG, "peek rx header failed"); + *length = (rx_header.length_high << 8) + rx_header.length_low; + if (rx_header.status & RSR_ERR_MASK) { + ch390_drop_frame(emac, *length); + *length = 0; + return ESP_ERR_INVALID_RESPONSE; + } else if (*length > ETH_MAX_PACKET_SIZE) { + /* reset rx memory pointer */ + ESP_GOTO_ON_ERROR(ch390_io_register_write(emac, CH390_MPTRCR, MPTRCR_RST_RX), err, TAG, "reset rx pointer failed"); + return ESP_ERR_INVALID_RESPONSE; + } else { + ESP_GOTO_ON_ERROR(ch390_io_memory_read(emac, buf, *length), err, TAG, "read rx data failed"); + *length -= ETH_CRC_LEN; + } + } else { + *length = 0; + } + return ESP_OK; + } + +err: + *length = 0; + return ret; +} + +static esp_err_t emac_ch390_init(esp_eth_mac_t *mac) +{ + esp_err_t ret = ESP_OK; + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + esp_eth_mediator_t *eth = emac->eth; + if (emac->int_gpio_num >= 0) { + esp_rom_gpio_pad_select_gpio(emac->int_gpio_num); + gpio_set_direction(emac->int_gpio_num, GPIO_MODE_INPUT); + gpio_set_pull_mode(emac->int_gpio_num, GPIO_PULLDOWN_ONLY); + gpio_set_intr_type(emac->int_gpio_num, GPIO_INTR_POSEDGE); + gpio_intr_enable(emac->int_gpio_num); + gpio_isr_handler_add(emac->int_gpio_num, ch390_isr_handler, emac); + } + ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_LLINIT, NULL), err, TAG, "lowlevel init failed"); + /* reset ch390 */ + ESP_GOTO_ON_ERROR(ch390_reset(emac), err, TAG, "reset ch390 failed"); + /* verify chip id */ + ESP_GOTO_ON_ERROR(ch390_verify_id(emac), err, TAG, "verify chip ID failed"); + /* default setup of internal registers */ + ESP_GOTO_ON_ERROR(ch390_setup_default(emac), err, TAG, "ch390 default setup failed"); + /* clear multicast hash table */ + ESP_GOTO_ON_ERROR(ch390_clear_multicast_table(emac), err, TAG, "clear multicast table failed"); + /* get emac address from eeprom */ + ESP_GOTO_ON_ERROR(ch390_get_mac_addr(emac), err, TAG, "fetch ethernet mac address failed"); + return ESP_OK; +err: + if (emac->int_gpio_num >= 0) { + gpio_isr_handler_remove(emac->int_gpio_num); + gpio_reset_pin(emac->int_gpio_num); + } + eth->on_state_changed(eth, ETH_STATE_DEINIT, NULL); + return ret; +} + +static esp_err_t emac_ch390_deinit(esp_eth_mac_t *mac) +{ + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + esp_eth_mediator_t *eth = emac->eth; + mac->stop(mac); + if (emac->int_gpio_num >= 0) { + gpio_isr_handler_remove(emac->int_gpio_num); + gpio_reset_pin(emac->int_gpio_num); + } + if (emac->poll_timer && esp_timer_is_active(emac->poll_timer)) { + esp_timer_stop(emac->poll_timer); + } + eth->on_state_changed(eth, ETH_STATE_DEINIT, NULL); + return ESP_OK; +} + +static void emac_ch390_task(void *arg) +{ + emac_ch390_t *emac = (emac_ch390_t *)arg; + uint8_t status = 0; + uint8_t *buffer; + while (1) { + // check if the task receives any notification + if (emac->int_gpio_num >= 0) { // if in interrupt mode + if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(1000)) == 0 && // if no notification ... + gpio_get_level(emac->int_gpio_num) == 0) { // ...and no interrupt asserted + continue; // -> just continue to check again + } + } else { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + } + + /* clear interrupt status */ + ch390_io_register_read(emac, CH390_ISR, &status); + ch390_io_register_write(emac, CH390_ISR, status); + /* packet received */ + if (status & ISR_PR) { + do { + if (emac->parent.receive(&emac->parent, emac->rx_buffer, &emac->rx_len) == ESP_OK) { + if (emac->rx_len == 0) { + break; + } else { + ESP_LOGD(TAG, "receive len=%lu", emac->rx_len); + + /* allocate memory and check whether allocation failed */ + buffer = malloc(emac->rx_len); + if (buffer == NULL) { + ESP_LOGE(TAG, "no memory for receive buffer"); + continue; + } + + /* pass the buffer to stack (e.g. TCP/IP layer) */ + memcpy(buffer, emac->rx_buffer, emac->rx_len); + emac->eth->stack_input(emac->eth, buffer, emac->rx_len); + } + } else { + ESP_LOGE(TAG, "frame read from module failed"); + break; + } + } while (1); + } + } + vTaskDelete(NULL); +} + +static esp_err_t emac_ch390_del(esp_eth_mac_t *mac) +{ + emac_ch390_t *emac = __containerof(mac, emac_ch390_t, parent); + if (emac->poll_timer) { + esp_timer_delete(emac->poll_timer); + } + vTaskDelete(emac->rx_task_hdl); + emac->spi.deinit(emac->spi.ctx); + heap_caps_free(emac->rx_buffer); + free(emac); + return ESP_OK; +} + +esp_eth_mac_t *esp_eth_mac_new_ch390(const eth_ch390_config_t *ch390_config, const eth_mac_config_t *mac_config) +{ + esp_eth_mac_t *ret = NULL; + emac_ch390_t *emac = NULL; + ESP_GOTO_ON_FALSE(ch390_config, NULL, err, TAG, "can't set ch390 specific config to null"); + ESP_GOTO_ON_FALSE(mac_config, NULL, err, TAG, "can't set mac config to null"); + emac = calloc(1, sizeof(emac_ch390_t)); + ESP_GOTO_ON_FALSE(emac, NULL, err, TAG, "calloc emac failed"); + /* ch390 receive is driven by interrupt or timer signal */ + ESP_GOTO_ON_FALSE((ch390_config->int_gpio_num >= 0) != (ch390_config->poll_period_ms > 0), NULL, err, TAG, "invalid configuration argument combination"); + /* bind methods and attributes */ + emac->sw_reset_timeout_ms = mac_config->sw_reset_timeout_ms; + emac->int_gpio_num = ch390_config->int_gpio_num; + emac->poll_period_ms = ch390_config->poll_period_ms; + emac->parent.set_mediator = emac_ch390_set_mediator; + emac->parent.init = emac_ch390_init; + emac->parent.deinit = emac_ch390_deinit; + emac->parent.start = emac_ch390_start; + emac->parent.stop = emac_ch390_stop; + emac->parent.del = emac_ch390_del; + emac->parent.write_phy_reg = emac_ch390_write_phy_reg; + emac->parent.read_phy_reg = emac_ch390_read_phy_reg; + emac->parent.set_addr = emac_ch390_set_addr; + emac->parent.get_addr = emac_ch390_get_addr; + emac->parent.set_speed = emac_ch390_set_speed; + emac->parent.set_duplex = emac_ch390_set_duplex; + emac->parent.set_link = emac_ch390_set_link; + emac->parent.set_promiscuous = emac_ch390_set_promiscuous; + emac->parent.set_peer_pause_ability = emac_ch390_set_peer_pause_ability; + emac->parent.enable_flow_ctrl = emac_ch390_enable_flow_ctrl; + emac->parent.transmit = emac_ch390_transmit; + emac->parent.receive = emac_ch390_receive; + + if (ch390_config->custom_spi_driver.init != NULL && ch390_config->custom_spi_driver.deinit != NULL + && ch390_config->custom_spi_driver.read != NULL && ch390_config->custom_spi_driver.write != NULL) { + ESP_LOGD(TAG, "Using user's custom SPI Driver"); + emac->spi.init = ch390_config->custom_spi_driver.init; + emac->spi.deinit = ch390_config->custom_spi_driver.deinit; + emac->spi.read = ch390_config->custom_spi_driver.read; + emac->spi.write = ch390_config->custom_spi_driver.write; + /* Custom SPI driver device init */ + ESP_GOTO_ON_FALSE((emac->spi.ctx = emac->spi.init(ch390_config->custom_spi_driver.config)) != NULL, NULL, err, TAG, "SPI initialization failed"); + } else { + ESP_LOGD(TAG, "Using default SPI Driver"); + emac->spi.init = CH390_SPI_INIT; + emac->spi.deinit = CH390_SPI_DEINIT; + emac->spi.read = CH390_SPI_READ; + emac->spi.write = CH390_SPI_WRITE; + /* SPI device init */ + ESP_GOTO_ON_FALSE((emac->spi.ctx = emac->spi.init(ch390_config)) != NULL, NULL, err, TAG, "SPI initialization failed"); + } + + /* create ch390 task */ + BaseType_t core_num = tskNO_AFFINITY; + if (mac_config->flags & ETH_MAC_FLAG_PIN_TO_CORE) { + core_num = esp_cpu_get_core_id(); + } + BaseType_t xReturned = xTaskCreatePinnedToCore(emac_ch390_task, "ch390_tsk", mac_config->rx_task_stack_size, emac, + mac_config->rx_task_prio, &emac->rx_task_hdl, core_num); + ESP_GOTO_ON_FALSE(xReturned == pdPASS, NULL, err, TAG, "create ch390 task failed"); + + emac->rx_buffer = heap_caps_malloc(ETH_MAX_PACKET_SIZE, MALLOC_CAP_DMA); + ESP_GOTO_ON_FALSE(emac->rx_buffer, NULL, err, TAG, "RX buffer allocation failed"); + + if (emac->int_gpio_num < 0) { + const esp_timer_create_args_t poll_timer_args = { + .callback = ch390_poll_timer, + .name = "emac_spi_poll_timer", + .arg = emac, + .skip_unhandled_events = true + }; + ESP_GOTO_ON_FALSE(esp_timer_create(&poll_timer_args, &emac->poll_timer) == ESP_OK, NULL, err, TAG, "create poll timer failed"); + } + return &(emac->parent); + +err: + if (emac) { + if (emac->rx_task_hdl) { + vTaskDelete(emac->rx_task_hdl); + } + if (emac->spi.ctx) { + emac->spi.deinit(emac->spi.ctx); + } + heap_caps_free(emac->rx_buffer); + free(emac); + } + return ret; +} diff --git a/components/ETH_CH390H/esp_eth_phy_ch390.c b/components/ETH_CH390H/esp_eth_phy_ch390.c new file mode 100644 index 0000000..a89dfde --- /dev/null +++ b/components/ETH_CH390H/esp_eth_phy_ch390.c @@ -0,0 +1,157 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + * + * SPDX-FileContributor: 2024 Sergey Kharenko + * SPDX-FileContributor: 2024 Espressif Systems (Shanghai) CO LTD + */ + +#include +#include +#include +#include "esp_log.h" +#include "esp_check.h" +#include "esp_eth_phy_802_3.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "esp_eth_phy_ch390.h" + +#define CH390_INFO_OUI 0x1CDC64 + +#define CH390_INFO_MODEL 0x01 + +typedef struct { + phy_802_3_t phy_802_3; +} phy_ch390_t; + +static const char *TAG = "ch390.phy"; + +static esp_err_t ch390_update_link_duplex_speed(phy_ch390_t *ch390) +{ + esp_err_t ret = ESP_OK; + esp_eth_mediator_t *eth = ch390->phy_802_3.eth; + uint32_t addr = ch390->phy_802_3.addr; + eth_speed_t speed = ETH_SPEED_10M; + eth_duplex_t duplex = ETH_DUPLEX_HALF; + bmcr_reg_t bmcr; + bmsr_reg_t bmsr; + uint32_t peer_pause_ability = false; + anlpar_reg_t anlpar; + ESP_GOTO_ON_ERROR(eth->phy_reg_read(eth, addr, ETH_PHY_BMSR_REG_ADDR, &(bmsr.val)), err, TAG, "read BMSR failed"); + ESP_GOTO_ON_ERROR(eth->phy_reg_read(eth, addr, ETH_PHY_BMSR_REG_ADDR, &(bmsr.val)), err, TAG, "read BMSR failed"); + ESP_GOTO_ON_ERROR(eth->phy_reg_read(eth, addr, ETH_PHY_ANLPAR_REG_ADDR, &(anlpar.val)), err, TAG, "read ANLPAR failed"); + eth_link_t link = bmsr.link_status ? ETH_LINK_UP : ETH_LINK_DOWN; + /* check if link status changed */ + if (ch390->phy_802_3.link_status != link) { + /* when link up, read negotiation result */ + if (link == ETH_LINK_UP) { + ESP_GOTO_ON_ERROR(eth->phy_reg_read(eth, addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)), err, TAG, "read BMCR failed"); + if (bmcr.speed_select) { + speed = ETH_SPEED_100M; + } else { + speed = ETH_SPEED_10M; + } + if (bmcr.duplex_mode) { + duplex = ETH_DUPLEX_FULL; + } else { + duplex = ETH_DUPLEX_HALF; + } + ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_SPEED, (void *)speed), err, TAG, "change speed failed"); + ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_DUPLEX, (void *)duplex), err, TAG, "change duplex failed"); + /* if we're in duplex mode, and peer has the flow control ability */ + if (duplex == ETH_DUPLEX_FULL && anlpar.symmetric_pause) { + peer_pause_ability = 1; + } else { + peer_pause_ability = 0; + } + ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_PAUSE, (void *)peer_pause_ability), err, TAG, "change pause ability failed"); + } + ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_LINK, (void *)link), err, TAG, "change link failed"); + ch390->phy_802_3.link_status = link; + } + return ESP_OK; +err: + return ret; +} + +static esp_err_t ch390_get_link(esp_eth_phy_t *phy) +{ + esp_err_t ret = ESP_OK; + phy_ch390_t *ch390 = __containerof(esp_eth_phy_into_phy_802_3(phy), phy_ch390_t, phy_802_3); + /* Update information about link, speed, duplex */ + ESP_GOTO_ON_ERROR(ch390_update_link_duplex_speed(ch390), err, TAG, "update link duplex speed failed"); + return ESP_OK; +err: + return ret; +} + +static esp_err_t ch390_autonego_ctrl(esp_eth_phy_t *phy, eth_phy_autoneg_cmd_t cmd, bool *autonego_en_stat) +{ + esp_err_t ret = ESP_OK; + phy_802_3_t *phy_802_3 = esp_eth_phy_into_phy_802_3(phy); + esp_eth_mediator_t *eth = phy_802_3->eth; + if (cmd == ESP_ETH_PHY_AUTONEGO_EN) { + bmcr_reg_t bmcr; + ESP_GOTO_ON_ERROR(eth->phy_reg_read(eth, phy_802_3->addr, ETH_PHY_BMCR_REG_ADDR, &(bmcr.val)), err, TAG, "read BMCR failed"); + ESP_GOTO_ON_FALSE(bmcr.en_loopback == 0, ESP_ERR_INVALID_STATE, err, TAG, "Auto-negotiation can't be enabled while in loopback operation"); + } + return esp_eth_phy_802_3_autonego_ctrl(phy_802_3, cmd, autonego_en_stat); +err: + return ret; +} + +static esp_err_t ch390_loopback(esp_eth_phy_t *phy, bool enable) +{ + esp_err_t ret = ESP_OK; + phy_802_3_t *phy_802_3 = esp_eth_phy_into_phy_802_3(phy); + bool auto_nego_en = true; + ESP_GOTO_ON_ERROR(ch390_autonego_ctrl(phy, ESP_ETH_PHY_AUTONEGO_G_STAT, &auto_nego_en), err, TAG, "get status of autonegotiation failed"); + ESP_GOTO_ON_FALSE(!(auto_nego_en && enable), ESP_ERR_INVALID_STATE, err, TAG, + "Unable to set loopback while auto-negotiation is enabled. Disable it to use loopback"); + return esp_eth_phy_802_3_loopback(phy_802_3, enable); +err: + return ret; +} +static esp_err_t ch390_init(esp_eth_phy_t *phy) +{ + esp_err_t ret = ESP_OK; + phy_802_3_t *phy_802_3 = esp_eth_phy_into_phy_802_3(phy); + + /* Basic PHY init */ + ESP_GOTO_ON_ERROR(esp_eth_phy_802_3_basic_phy_init(phy_802_3), err, TAG, "failed to init PHY"); + + /* Check PHY ID */ + uint32_t oui; + uint8_t model; + ESP_GOTO_ON_ERROR(esp_eth_phy_802_3_read_oui(phy_802_3, &oui), err, TAG, "read OUI failed"); + ESP_GOTO_ON_ERROR(esp_eth_phy_802_3_read_manufac_info(phy_802_3, &model, NULL), err, TAG, "read manufacturer's info failed"); + ESP_GOTO_ON_FALSE(oui == CH390_INFO_OUI && model == CH390_INFO_MODEL, ESP_FAIL, err, TAG, "wrong chip ID"); + + return ESP_OK; +err: + return ret; +} + +esp_eth_phy_t *esp_eth_phy_new_ch390(const eth_phy_config_t *config) +{ + esp_eth_phy_t *ret = NULL; + phy_ch390_t *ch390 = calloc(1, sizeof(phy_ch390_t)); + ESP_GOTO_ON_FALSE(ch390, NULL, err, TAG, "calloc ch390 failed"); + ESP_GOTO_ON_FALSE(esp_eth_phy_802_3_obj_config_init(&ch390->phy_802_3, config) == ESP_OK, + NULL, err, TAG, "configuration initialization of PHY 802.3 failed"); + + // override functions which need to be customized for sake of ch390 + ch390->phy_802_3.parent.init = ch390_init; + ch390->phy_802_3.parent.get_link = ch390_get_link; + ch390->phy_802_3.parent.autonego_ctrl = ch390_autonego_ctrl; + ch390->phy_802_3.parent.loopback = ch390_loopback; + + return &ch390->phy_802_3.parent; +err: + if (ch390 != NULL) { + free(ch390); + } + return ret; +} diff --git a/components/ETH_CH390H/include/ETH_CH390H.h b/components/ETH_CH390H/include/ETH_CH390H.h new file mode 100644 index 0000000..51bc3ef --- /dev/null +++ b/components/ETH_CH390H/include/ETH_CH390H.h @@ -0,0 +1,22 @@ +#include +#include "esp_eth.h" +#include "esp_eth_driver.h" +#include "esp_eth_mac_ch390.h" +#include "esp_eth_phy_ch390.h" +#include "esp_netif.h" +#include "esp_event.h" +#include "driver/spi_master.h" +#include "esp_log.h" +#include "driver/gpio.h" + + +#define ETH_CS_GPIO (GPIO_NUM_10) +#define ETH_MOSI_GPIO (GPIO_NUM_12) +#define ETH_MISO_GPIO (GPIO_NUM_13) +#define ETH_SCLK_GPIO (GPIO_NUM_11) +#define ETH_INT_GPIO (GPIO_NUM_14) +#define SPI_HOST SPI2_HOST +#define SPI_CLOCK_MHZ 10 + + +void eth_init(void); diff --git a/components/ETH_CH390H/include/ch390.h b/components/ETH_CH390H/include/ch390.h new file mode 100644 index 0000000..9097cea --- /dev/null +++ b/components/ETH_CH390H/include/ch390.h @@ -0,0 +1,236 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + * + * SPDX-FileContributor: 2023-2024 NanjingQinhengMicroelectronics CO LTD + * SPDX-FileContributor: 2024 Sergey Kharenko + * SPDX-FileContributor: 2024-2025 Espressif Systems (Shanghai) CO LTD + */ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/******************************************************************** + * Register definition + */ + +#define CH390_NCR 0x00 // Network control reg +#define NCR_WAKEEN (1<<6) // Enable wakeup function +#define NCR_FDX (1<<3) // Duplex mode of the internal PHY +#define NCR_LBK_MAC (1<<1) // MAC loop-back +#define NCR_RST (1<<0) // Software reset + +#define CH390_NSR 0x01 // Network status reg +#define NSR_SPEED (1<<7) // Speed of internal PHY +#define NSR_LINKST (1<<6) // Link status of internal PHY +#define NSR_WAKEST (1<<5) // Wakeup event status +#define NSR_TX2END (1<<3) // Tx packet B complete status +#define NSR_TX1END (1<<2) // Tx packet A complete status +#define NSR_RXOV (1<<1) // Rx fifo overflow +#define NSR_RXRDY (1<<0) + +#define CH390_TCR 0x02 // Transmit control reg +#define TCR_TJDIS (1<<6) // Transmit jabber timer +#define TCR_PAD_DIS2 (1<<4) // PAD appends for packet B +#define TCR_CRC_DIS2 (1<<3) // CRC appends for packet B +#define TCR_PAD_DIS1 (1<<2) // PAD appends for packet A +#define TCR_CRC_DIS1 (1<<1) // CRC appends for packet A +#define TCR_TXREQ (1<<0) // Tx request + +#define CH390_TSRA 0x03 // Transmit status reg A +#define CH390_TSRB 0x04 // Transmit status reg B +#define TSR_TJTO (1<<7) // Transmit jabber time out +#define TSR_LC (1<<6) // Loss of carrier +#define TSR_NC (1<<5) // No carrier +#define TSR_LCOL (1<<4) // Late collision +#define TSR_COL (1<<3) // Collision packet +#define TSR_EC (1<<2) // Excessive collision + +#define CH390_RCR 0x05 // Receive control reg +#define RCR_DEFAULT 0x00 // Default settings +#define RCR_WTDIS (1<<6) // Disable 2048 bytes watch dog +#define RCR_DIS_CRC (1<<4) // Discard CRC error packet +#define RCR_ALL (1<<3) // Pass all multicast +#define RCR_RUNT (1<<2) // Pass runt packet +#define RCR_PRMSC (1<<1) // Promiscuous mode +#define RCR_RXEN (1<<0) // Enable RX + +#define CH390_RSR 0x06 // Receive status reg +#define RSR_RF (1<<7) // Rnt frame +#define RSR_MF (1<<6) // Multicast frame +#define RSR_LCS (1<<5) // Late collision seen +#define RSR_RWTO (1<<4) // Receive watchdog time-out +#define RSR_PLE (1<<3) // Physical layer error +#define RSR_AE (1<<2) // Alignment error +#define RSR_CE (1<<1) // CRC error +#define RSR_FOE (1<<0) // FIFO overflow error + +//Receive status error mask(default) +#define RSR_ERR_MASK (RSR_RF | RSR_LCS | RSR_RWTO | RSR_PLE | \ + RSR_AE | RSR_CE | RSR_FOE) + +#define CH390_ROCR 0x07 // Receive overflow count reg +#define CH390_BPTR 0x08 // Back pressure threshold reg +#define CH390_FCTR 0x09 // Flow control threshold reg +#define FCTR_HWOT(overflow_th) (( overflow_th & 0xf ) << 4) +#define FCTR_LWOT(overflow_th) ( overflow_th & 0xf ) + +#define CH390_FCR 0x0A // Transmit/Receive flow control reg +#define FCR_FLOW_ENABLE (0x39) // Enable Flow Control + +#define CH390_EPCR 0x0B // EEPROM or PHY control reg +#define EPCR_REEP (1<<5) // Reload EEPROM +#define EPCR_WEP (1<<4) // Write EEPROM enable +#define EPCR_EPOS (1<<3) // EEPROM or PHY operation select +#define EPCR_ERPRR (1<<2) // EEPROM or PHY read command +#define EPCR_ERPRW (1<<1) // EEPROM or PHY write command +#define EPCR_ERRE (1<<0) // EEPROM or PHY access status + +#define CH390_EPAR 0x0C // EEPROM or PHY address reg + +#define CH390_EPDRL 0x0D // EEPROM or PHY low byte data reg +#define CH390_EPDRH 0x0E // EEPROM or PHY high byte data reg + +#define CH390_WCR 0x0F // Wakeup control reg +#define WCR_LINKEN (1<<5) // Link status change wakeup +#define WCR_SAMPLEEN (1<<4) // Sample frame wakeup +#define WCR_MAGICEN (1<<3) // Magic packet wakeup +#define WCR_LINKST (1<<2) // Link status change event +#define WCR_SAMPLEST (1<<1) // Sample frame event +#define WCR_MAGICST (1<<0) // Magic packet event + +#define CH390_PAR 0x10 // MAC address reg +#define CH390_MAR 0x16 // Multicast address reg +#define CH390_GPCR 0x1E // General purpose control reg +#define CH390_GPR 0x1F // General purpose reg +#define CH390_TRPAL 0x22 // Transmit read pointer low byte address reg +#define CH390_TRPAH 0x23 // Transmit read pointer high byte address reg +#define CH390_RWPAL 0x24 // Receive write pointer low byte address reg +#define CH390_RWPAH 0x25 // Receive write pointer high byte address reg +#define CH390_VIDL 0x28 // Vendor ID low byte reg +#define CH390_VIDH 0x29 // Vendor ID high byte reg +#define CH390_PIDL 0x2A // Product ID low byte reg +#define CH390_PIDH 0x2B // Product ID high byte reg +#define CH390_CHIPR 0x2C // Chip reversion reg + +#define CH390_TCR2 0x2D // Transmit control reg II +#define TCR2_RLCP (1<<6) // Retry Late Collision Packet + +#define CH390_ATCR 0x30 // Auto-Transmit control reg +#define ATCR_AUTO_TX (1<<7) // Auto-Transmit Control + +#define CH390_TCSCR 0x31 // Transmit checksum and control reg +#define TCSCR_ALL 0x1F +#define TCSCR_IPv6TCPCSE (1<<4) // IPv6 TCP checksum generation +#define TCSCR_IPv6UDPCSE (1<<3) // IPv6 UDP checksum generation +#define TCSCR_UDPCSE (1<<2) // UDP checksum generation +#define TCSCR_TCPCSE (1<<1) // TCP checksum generation +#define TCSCR_IPCSE (1<<0) // IP checksum generation + +#define CH390_RCSCSR 0x32 // Receive checksum and control reg +#define RCSCSR_UDPS (1<<7) // UDP checksum status +#define RCSCSR_TCPS (1<<6) // TCP checksum status +#define RCSCSR_IPS (1<<5) // IP checksum status +#define RCSCSR_UDPP (1<<4) // UDP packet of current received packet +#define RCSCSR_TCPP (1<<3) // TCP packet of current received packet +#define RCSCSR_IPP (1<<2) // IP packet of current received packet +#define RCSCSR_RCSEN (1<<1) // Receive checksum checking enable +#define RCSCSR_DCSE (1<<0) // Discard checksum error packet + +#define CH390_MPAR 0x33 // MII PHY address reg +#define CH390_SBCR 0x38 // SPI bus control reg + +#define CH390_INTCR 0x39 // INT pin control reg +#define INCR_TYPE_OD 0x02 // Open drain output +#define INCR_TYPE_PP 0x00 // Push pull output +#define INCR_POL_L 0x01 // Low level positive +#define INCR_POL_H 0x00 // High level positive + +#define CH390_ALNCR 0x4A // SPI alignment error count reg +#define CH390_SCCR 0x50 // System clock control reg +#define CH390_RSCCR 0x51 // Recover system clock control reg + +#define CH390_RLENCR 0x52 // Receive data pack length control reg +#define RLENCR_RXLEN_EN 0x80 // Enable RX data pack length filter +#define RLENCR_RXLEN_DEFAULT 0x18 // Default MAX length of RX data(div by 64) + +#define CH390_BCASTCR 0x53 // Receive broadcast control reg +#define CH390_INTCKCR 0x54 // INT pin clock output control reg + +#define CH390_MPTRCR 0x55 // Memory pointer control reg +#define MPTRCR_RST_TX (1<<1) // Reset TX Memory Pointer +#define MPTRCR_RST_RX (1<<0) // Reset RX Memory Pointer + +#define CH390_MLEDCR 0x57 // More LED control reg +#define CH390_MRCMDX 0x70 // Memory read command without address increment reg +// Memory read command without data pre-fetch and address increment reg +#define CH390_MRCMDX1 0x71 +#define CH390_MRCMD 0x72 // Memory data read command with address increment reg +#define CH390_MRRL 0x74 // Memory read low byte address reg +#define CH390_MRRH 0x75 // Memory read high byte address reg +#define CH390_MWCMDX 0x76 // Memory write command without address increment reg +#define CH390_MWCMD 0x78 // Memory write command +#define CH390_MWRL 0x7A // Memory write low byte address reg +#define CH390_MWRH 0x7B // Memory write high byte address reg +#define CH390_TXPLL 0x7C // Transmit pack low byte length reg +#define CH390_TXPLH 0x7D // Transmit pack high byte length reg + +#define CH390_ISR 0x7E // Interrupt status reg +#define ISR_LNKCHG (1<<5) // Link status change +#define ISR_ROO (1<<3) // Receive overflow counter overflow +#define ISR_ROS (1<<2) // Receive overflow +#define ISR_PT (1<<1) // Packet transmitted +#define ISR_PR (1<<0) // Packet received +#define ISR_CLR_STATUS (ISR_LNKCHG | ISR_ROO | ISR_ROS | ISR_PT | ISR_PR) + +#define CH390_IMR 0x7F // Interrupt mask reg +#define IMR_NONE 0x00 // Disable all interrupt +#define IMR_ALL 0xFF // Enable all interrupt +#define IMR_PAR (1<<7) // Pointer auto-return mode +#define IMR_LNKCHGI (1<<5) // Enable link status change interrupt +#define IMR_UDRUNI (1<<4) // Enable transmit under-run interrupt +#define IMR_ROOI (1<<3) // Enable receive overflow counter overflow interrupt +#define IMR_ROI (1<<2) // Enable receive overflow interrupt +#define IMR_PTI (1<<1) // Enable packet transmitted interrupt +#define IMR_PRI (1<<0) // Enable packet received interrupt + +// SPI commands +#define OPC_REG_W 0x80 // Register Write +#define OPC_REG_R 0x00 // Register Read +#define OPC_MEM_DMY_R 0x70 // Memory Dummy Read +#define OPC_MEM_WRITE 0xF8 // Memory Write +#define OPC_MEM_READ 0x72 // Memory Read + +#define CH390_SPI_RD 0 +#define CH390_SPI_WR 1 + +// GPIO +#define CH390_GPIO1 0x02 +#define CH390_GPIO2 0x04 +#define CH390_GPIO3 0x08 + +// PHY registers +#define CH390_PHY 0x40 +#define CH390_PHY_BMCR 0x00 +#define CH390_PHY_BMSR 0x01 +#define CH390_PHY_PHYID1 0x02 +#define CH390_PHY_PHYID2 0x03 +#define CH390_PHY_ANAR 0x04 +#define CH390_PHY_ANLPAR 0x05 +#define CH390_PHY_ANER 0x06 +#define CH390_PHY_PAGE_SEL 0x1F + +// Packet status +#define CH390_PKT_NONE 0x00 /* No packet received */ +#define CH390_PKT_RDY 0x01 /* Packet ready to receive */ +#define CH390_PKT_ERR 0xFE /* Un-stable states mask */ +#define CH390_PKT_ERR_WITH_RCSEN 0xE2 /* Un-stable states mask when RCSEN = 1 */ + +#ifdef __cplusplus +} +#endif diff --git a/components/ETH_CH390H/include/esp_eth_mac_ch390.h b/components/ETH_CH390H/include/esp_eth_mac_ch390.h new file mode 100644 index 0000000..f165430 --- /dev/null +++ b/components/ETH_CH390H/include/esp_eth_mac_ch390.h @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + * + * SPDX-FileContributor: 2024 Sergey Kharenko + * SPDX-FileContributor: 2024 Espressif Systems (Shanghai) CO LTD + */ + +#pragma once + +#include "esp_eth_com.h" +#include "esp_eth_mac.h" + +#include "esp_idf_version.h" +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) +#include "esp_eth_mac_spi.h" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief CH390 specific configuration + * + */ +typedef struct { + int int_gpio_num; /*!< Interrupt GPIO number */ + uint32_t poll_period_ms; /*!< Period in ms to poll rx status when interrupt mode is not used */ + spi_host_device_t spi_host_id; /*!< SPI peripheral (this field is invalid when custom SPI driver is defined) */ + spi_device_interface_config_t *spi_devcfg; /*!< SPI device configuration (this field is invalid when custom SPI driver is defined) */ + eth_spi_custom_driver_config_t custom_spi_driver; /*!< Custom SPI driver definitions */ +} eth_ch390_config_t; + +/** + * @brief Default CH390 specific configuration + * + */ +#define ETH_CH390_DEFAULT_CONFIG(spi_host, spi_devcfg_p) \ + { \ + .int_gpio_num = 4, \ + .spi_host_id = spi_host, \ + .spi_devcfg = spi_devcfg_p, \ + .custom_spi_driver = ETH_DEFAULT_SPI, \ + } + +/** +* @brief Create CH390 Ethernet MAC instance +* +* @param ch390_config: CH390 specific configuration +* @param mac_config: Ethernet MAC configuration +* +* @return +* - instance: create MAC instance successfully +* - NULL: create MAC instance failed because some error occurred +*/ +esp_eth_mac_t *esp_eth_mac_new_ch390(const eth_ch390_config_t *ch390_config, const eth_mac_config_t *mac_config); + +#ifdef __cplusplus +} +#endif diff --git a/components/ETH_CH390H/include/esp_eth_phy_ch390.h b/components/ETH_CH390H/include/esp_eth_phy_ch390.h new file mode 100644 index 0000000..24f9a51 --- /dev/null +++ b/components/ETH_CH390H/include/esp_eth_phy_ch390.h @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + * + * SPDX-FileContributor: 2024 Sergey Kharenko + * SPDX-FileContributor: 2024 Espressif Systems (Shanghai) CO LTD + */ + +#pragma once + +#include "esp_eth_com.h" +#include "esp_eth_phy.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** +* @brief Create a PHY instance of CH390 +* +* @param[in] config: configuration of PHY +* +* @return +* - instance: create PHY instance successfully +* - NULL: create PHY instance failed because some error occurred +*/ +esp_eth_phy_t *esp_eth_phy_new_ch390(const eth_phy_config_t *config); + +#ifdef __cplusplus +} +#endif diff --git a/components/MODBUS_ESP/CMakeLists.txt b/components/MODBUS_ESP/CMakeLists.txt new file mode 100644 index 0000000..b445159 --- /dev/null +++ b/components/MODBUS_ESP/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "MODBUS_ESP.c" + INCLUDE_DIRS "include" + REQUIRES driver RS-485-SP3485EEN) diff --git a/components/MODBUS_ESP/MODBUS_ESP.c b/components/MODBUS_ESP/MODBUS_ESP.c new file mode 100644 index 0000000..931c00c --- /dev/null +++ b/components/MODBUS_ESP/MODBUS_ESP.c @@ -0,0 +1,416 @@ +#include +#include +#include "MODBUS_ESP.h" +#include "esp_log.h" +#include "RS-485-SP3485EEN.h" + +#define TAG "MODBUS_ESP" + +// CRC-16表(多项式0xA001) +static const uint16_t crc16_table[256] = { + 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, + 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, + 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, + 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, + 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, + 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, + 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, + 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, + 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, + 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, + 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, + 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, + 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, + 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, + 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, + 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, + 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, + 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, + 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, + 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, + 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, + 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, + 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, + 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, + 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, + 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, + 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, + 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, + 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, + 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, + 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, + 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 +}; + +uint16_t modbus_crc16(const uint8_t *data, size_t len) +{ + uint16_t crc = 0xFFFF; + + for (size_t i = 0; i < len; i++) { + crc = (crc >> 8) ^ crc16_table[(crc ^ data[i]) & 0xFF]; + } + + return crc; +} + +bool modbus_verify_crc(const uint8_t *data, size_t len) +{ + if (len < 3) { + ESP_LOGE(TAG, "Frame too short for CRC verification"); + return false; + } + + // 计算除最后2字节外的CRC + uint16_t calculated_crc = modbus_crc16(data, len - 2); + + // 获取帧中的CRC(小端序) + uint16_t frame_crc = ((uint16_t)data[len - 1] << 8) | data[len - 2]; + + if (calculated_crc != frame_crc) { + ESP_LOGE(TAG, "CRC mismatch: calculated=0x%04X, frame=0x%04X", calculated_crc, frame_crc); + return false; + } + + return true; +} + +bool modbus_build_read_holding_req(uint8_t slave_addr, uint16_t start_addr, uint16_t reg_count, + uint8_t *frame, size_t *frame_len) +{ + if (frame == NULL || frame_len == NULL) { + ESP_LOGE(TAG, "Invalid parameters"); + return false; + } + + // 参数检查 + if (reg_count == 0 || reg_count > 125) { + ESP_LOGE(TAG, "Invalid register count: %d", reg_count); + return false; + } + + // 构建请求帧(8字节) + frame[0] = slave_addr; // 从机地址 + frame[1] = MODBUS_FUNC_READ_HOLDING_REGISTERS; // 功能码 0x03 + frame[2] = (start_addr >> 8) & 0xFF; // 起始地址高字节 + frame[3] = start_addr & 0xFF; // 起始地址低字节 + frame[4] = (reg_count >> 8) & 0xFF; // 寄存器数量高字节 + frame[5] = reg_count & 0xFF; // 寄存器数量低字节 + + // 计算CRC + uint16_t crc = modbus_crc16(frame, 6); + frame[6] = crc & 0xFF; // CRC低字节 + frame[7] = (crc >> 8) & 0xFF; // CRC高字节 + + *frame_len = 8; + + ESP_LOGI(TAG, "Built request: slave=%d, start_addr=0x%04X, reg_count=%d", + slave_addr, start_addr, reg_count); + ESP_LOG_BUFFER_HEX(TAG, frame, 8); + + return true; +} + +bool modbus_parse_response(const uint8_t *frame, size_t frame_len, modbus_response_t *response) +{ + if (frame == NULL || response == NULL) { + ESP_LOGE(TAG, "Invalid parameters"); + return false; + } + + // 初始化响应结构体 + memset(response, 0, sizeof(modbus_response_t)); + response->registers = NULL; + + // 验证CRC + if (!modbus_verify_crc(frame, frame_len)) { + ESP_LOGE(TAG, "CRC verification failed"); + return false; + } + + // 解析基本字段 + response->slave_addr = frame[0]; + response->function_code = frame[1]; + + ESP_LOGI(TAG, "Parsing response: slave=%d, func=0x%02X", response->slave_addr, response->function_code); + + // 检查是否为异常响应(功能码最高位置1) + if (response->function_code & 0x80) { + response->is_exception = true; + response->exception_code = frame[2]; + ESP_LOGW(TAG, "Exception response: code=0x%02X", response->exception_code); + return true; // 异常响应也算解析成功 + } + + // 正常响应,仅处理功能码03 + if (response->function_code != MODBUS_FUNC_READ_HOLDING_REGISTERS) { + ESP_LOGE(TAG, "Unsupported function code: 0x%02X", response->function_code); + return false; + } + + response->is_exception = false; + + // 检查最小长度(地址+功能码+字节数+CRC = 5字节) + if (frame_len < 5) { + ESP_LOGE(TAG, "Frame too short: %d", frame_len); + return false; + } + + // 获取字节数 + response->byte_count = frame[2]; + + // 检查帧长度是否正确 + size_t expected_len = 3 + response->byte_count + 2; // 3字节头部 + 数据 + 2字节CRC + if (frame_len != expected_len) { + ESP_LOGE(TAG, "Frame length mismatch: expected=%d, actual=%d", expected_len, frame_len); + return false; + } + + // 计算寄存器数量 + response->register_count = response->byte_count / 2; + + if (response->register_count == 0) { + ESP_LOGW(TAG, "No registers in response"); + return true; + } + + // 分配内存存储寄存器数据 + response->registers = malloc(response->register_count * sizeof(uint16_t)); + if (response->registers == NULL) { + ESP_LOGE(TAG, "Failed to allocate memory for registers"); + return false; + } + + // 解析寄存器数据(大端序) + for (uint8_t i = 0; i < response->register_count; i++) { + response->registers[i] = ((uint16_t)frame[3 + i * 2] << 8) | frame[3 + i * 2 + 1]; + } + + ESP_LOGI(TAG, "Parsed successfully: %d registers", response->register_count); + + return true; +} + +void modbus_free_response(modbus_response_t *response) +{ + if (response != NULL && response->registers != NULL) { + free(response->registers); + response->registers = NULL; + } +} + +// ============================ +// MODBUS轮询任务相关 +// ============================ + +static TaskHandle_t poll_task_handle = NULL; +static modbus_poll_config_t current_config = {0}; +static SemaphoreHandle_t config_mutex = NULL; + +/** + * @brief MODBUS轮询任务函数 + */ +static void modbus_poll_task(void *arg) +{ + ESP_LOGI(TAG, "MODBUS poll task started"); + + // 分配发送帧缓冲区 + uint8_t request_frame[8]; + size_t frame_len; + + while (1) { + // 检查任务是否应该退出 + if (poll_task_handle == NULL) { + break; + } + + // 获取配置(互斥保护) + xSemaphoreTake(config_mutex, portMAX_DELAY); + bool enabled = current_config.enabled; + int channel_num = current_config.channel_num; + uint8_t slave_addr = current_config.slave_addr; + uint16_t start_addr = current_config.start_addr; + uint16_t reg_count = current_config.reg_count; + uint32_t poll_interval_ms = current_config.poll_interval_ms; + xSemaphoreGive(config_mutex); + + // 检查是否启用 + if (!enabled) { + vTaskDelay(pdMS_TO_TICKS(100)); + continue; + } + + // 验证通道号 + if (channel_num < 0 || channel_num >= NUM_CHANNELS) { + ESP_LOGE(TAG, "Invalid channel number: %d", channel_num); + vTaskDelay(pdMS_TO_TICKS(1000)); + continue; + } + + // 构建MODBUS请求帧 + if (modbus_build_read_holding_req(slave_addr, start_addr, reg_count, request_frame, &frame_len)) { + // 获取RS485通道 + rs485_channel_t *ch = &rs485_channels[channel_num]; + + // 发送请求 + ESP_LOGI(TAG, "Polling: channel=%s, slave=%d, addr=0x%04X, count=%d", + ch->name, slave_addr, start_addr, reg_count); + rs485_send(ch->uart_num, request_frame, frame_len); + } else { + ESP_LOGE(TAG, "Failed to build MODBUS request frame"); + } + + // 等待下一次轮询 + vTaskDelay(pdMS_TO_TICKS(poll_interval_ms)); + } + + ESP_LOGI(TAG, "MODBUS poll task exiting"); + poll_task_handle = NULL; + vTaskDelete(NULL); +} + +BaseType_t modbus_start_poll_task(modbus_poll_config_t *config, UBaseType_t priority, uint32_t stack_size) +{ + if (config == NULL) { + ESP_LOGE(TAG, "Invalid config parameter"); + return pdFALSE; + } + + // 参数验证 + if (config->channel_num < 0 || config->channel_num >= NUM_CHANNELS) { + ESP_LOGE(TAG, "Invalid channel number: %d", config->channel_num); + return pdFALSE; + } + + if (config->reg_count == 0 || config->reg_count > 125) { + ESP_LOGE(TAG, "Invalid register count: %d", config->reg_count); + return pdFALSE; + } + + if (config->poll_interval_ms < 100) { + ESP_LOGE(TAG, "Poll interval too short (minimum 100ms): %d", config->poll_interval_ms); + return pdFALSE; + } + + // 如果任务已存在,先停止 + if (poll_task_handle != NULL) { + modbus_stop_poll_task(); + vTaskDelay(pdMS_TO_TICKS(100)); + } + + // 创建互斥量 + if (config_mutex == NULL) { + config_mutex = xSemaphoreCreateMutex(); + if (config_mutex == NULL) { + ESP_LOGE(TAG, "Failed to create config mutex"); + return pdFALSE; + } + } + + // 保存配置 + xSemaphoreTake(config_mutex, portMAX_DELAY); + memcpy(¤t_config, config, sizeof(modbus_poll_config_t)); + current_config.enabled = true; + xSemaphoreGive(config_mutex); + + // 创建轮询任务 + BaseType_t ret = xTaskCreate(modbus_poll_task, "modbus_poll", stack_size, NULL, priority, &poll_task_handle); + + if (ret == pdPASS) { + ESP_LOGI(TAG, "MODBUS poll task started successfully"); + ESP_LOGI(TAG, "Config: channel=%d, slave=%d, start_addr=0x%04X, reg_count=%d, interval=%dms", + config->channel_num, config->slave_addr, config->start_addr, + config->reg_count, config->poll_interval_ms); + } else { + ESP_LOGE(TAG, "Failed to create MODBUS poll task"); + } + + return ret; +} + +void modbus_stop_poll_task(void) +{ + if (poll_task_handle != NULL) { + ESP_LOGI(TAG, "Stopping MODBUS poll task..."); + + // 禁用轮询 + xSemaphoreTake(config_mutex, portMAX_DELAY); + current_config.enabled = false; + xSemaphoreGive(config_mutex); + + // 等待任务退出 + vTaskDelay(pdMS_TO_TICKS(200)); + + // 删除任务 + TaskHandle_t temp_handle = poll_task_handle; + poll_task_handle = NULL; + vTaskDelete(temp_handle); + + ESP_LOGI(TAG, "MODBUS poll task stopped"); + } +} + +bool modbus_update_poll_config(modbus_poll_config_t *config) +{ + if (config == NULL) { + ESP_LOGE(TAG, "Invalid config parameter"); + return false; + } + + if (config->channel_num < 0 || config->channel_num >= NUM_CHANNELS) { + ESP_LOGE(TAG, "Invalid channel number: %d", config->channel_num); + return false; + } + + if (config->reg_count == 0 || config->reg_count > 125) { + ESP_LOGE(TAG, "Invalid register count: %d", config->reg_count); + return false; + } + + if (config->poll_interval_ms < 100) { + ESP_LOGE(TAG, "Poll interval too short (minimum 100ms): %d", config->poll_interval_ms); + return false; + } + + // 如果互斥量还没创建,直接更新配置 + if (config_mutex == NULL) { + memcpy(¤t_config, config, sizeof(modbus_poll_config_t)); + } else { + // 互斥保护 + xSemaphoreTake(config_mutex, portMAX_DELAY); + memcpy(¤t_config, config, sizeof(modbus_poll_config_t)); + xSemaphoreGive(config_mutex); + } + + ESP_LOGI(TAG, "MODBUS poll config updated: channel=%d, slave=%d, start_addr=0x%04X, reg_count=%d, interval=%dms, enabled=%d", + config->channel_num, config->slave_addr, config->start_addr, + config->reg_count, config->poll_interval_ms, config->enabled); + + // 如果轮询任务还没有启动,自动启动 + if (poll_task_handle == NULL && config->enabled) { + ESP_LOGI(TAG, "Auto-starting MODBUS poll task..."); + + // 先创建互斥量(如果还没创建) + if (config_mutex == NULL) { + config_mutex = xSemaphoreCreateMutex(); + if (config_mutex == NULL) { + ESP_LOGE(TAG, "Failed to create config mutex"); + return false; + } + } + + // 启动轮询任务 + BaseType_t ret = xTaskCreate(modbus_poll_task, "modbus_poll", 4096, NULL, 5, &poll_task_handle); + if (ret != pdPASS) { + ESP_LOGE(TAG, "Failed to auto-start MODBUS poll task"); + return false; + } + ESP_LOGI(TAG, "MODBUS poll task auto-started"); + } + + return true; +} + +modbus_poll_config_t* modbus_get_current_config(void) +{ + return ¤t_config; +} diff --git a/components/MODBUS_ESP/include/MODBUS_ESP.h b/components/MODBUS_ESP/include/MODBUS_ESP.h new file mode 100644 index 0000000..8c0141e --- /dev/null +++ b/components/MODBUS_ESP/include/MODBUS_ESP.h @@ -0,0 +1,136 @@ +#ifndef MODBUS_ESP_H +#define MODBUS_ESP_H + +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +/** + * @brief MODBUS功能码定义 + */ +typedef enum { + MODBUS_FUNC_READ_HOLDING_REGISTERS = 0x03, // 读取保持寄存器 +} modbus_function_code_t; + +/** + * @brief MODBUS异常码定义 + */ +typedef enum { + MODBUS_EXCEPTION_ILLEGAL_FUNCTION = 0x01, + MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS = 0x02, + MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE = 0x03, + MODBUS_EXCEPTION_SERVER_DEVICE_FAILURE = 0x04, +} modbus_exception_code_t; + +/** + * @brief MODBUS响应结构体 + */ +typedef struct { + uint8_t slave_addr; // 从机地址 + uint8_t function_code; // 功能码 + uint8_t byte_count; // 数据字节数 + uint16_t *registers; // 寄存器数据(需要调用者释放) + uint8_t register_count; // 寄存器数量 + bool is_exception; // 是否为异常响应 + uint8_t exception_code; // 异常码(如果是异常响应) +} modbus_response_t; + +/** + * @brief 计算CRC-16 (MODBUS RTU标准) + * + * @param data 数据指针 + * @param len 数据长度 + * @return uint16_t CRC校验值 + */ +uint16_t modbus_crc16(const uint8_t *data, size_t len); + +/** + * @brief 验证CRC校验 + * + * @param data 数据指针(包含CRC的完整帧) + * @param len 数据长度(包含CRC的2个字节) + * @return true CRC校验成功 + * @return false CRC校验失败 + */ +bool modbus_verify_crc(const uint8_t *data, size_t len); + +/** + * @brief 构建MODBUS RTU读取保持寄存器请求帧(功能码03) + * + * @param slave_addr 从机地址 + * @param start_addr 起始寄存器地址 + * @param reg_count 读取寄存器数量 + * @param frame 输出帧缓冲区(需要至少8字节) + * @param frame_len 输出帧长度 + * @return true 构建成功 + * @return false 构建失败(参数错误) + */ +bool modbus_build_read_holding_req(uint8_t slave_addr, uint16_t start_addr, uint16_t reg_count, + uint8_t *frame, size_t *frame_len); + +/** + * @brief 解析MODBUS RTU响应帧(仅支持功能码03) + * + * @param frame 接收到的帧数据 + * @param frame_len 帧长度 + * @param response 解析结果结构体 + * @return true 解析成功 + * @return false 解析失败(CRC错误、格式错误等) + */ +bool modbus_parse_response(const uint8_t *frame, size_t frame_len, modbus_response_t *response); + +/** + * @brief 释放MODBUS响应结构体中的寄存器内存 + * + * @param response MODBUS响应结构体 + */ +void modbus_free_response(modbus_response_t *response); + +/** + * @brief MODBUS轮询配置结构体 + */ +typedef struct { + int channel_num; // RS485通道号 (0 或 1) + uint8_t slave_addr; // 从机地址 + uint16_t start_addr; // 起始寄存器地址 + uint16_t reg_count; // 读取寄存器数量 + uint32_t poll_interval_ms; // 轮询间隔(毫秒) + bool enabled; // 是否启用 +} modbus_poll_config_t; + +/** + * @brief 启动MODBUS轮询任务 + * + * @param config 轮询配置指针 + * @param priority 任务优先级 + * @param stack_size 任务栈大小 + * @return BaseType_t pdTRUE 成功, pdFALSE 失败 + */ +BaseType_t modbus_start_poll_task(modbus_poll_config_t *config, UBaseType_t priority, uint32_t stack_size); + +/** + * @brief 停止MODBUS轮询任务 + * + * @return 无返回值 + */ +void modbus_stop_poll_task(void); + +/** + * @brief 更新轮询配置 + * + * @param config 新的轮询配置 + * @return true 更新成功 + * @return false 更新失败 + */ +bool modbus_update_poll_config(modbus_poll_config_t *config); + +/** + * @brief 获取当前轮询配置 + * + * @return modbus_poll_config_t* 当前配置指针 + */ +modbus_poll_config_t* modbus_get_current_config(void); + +#endif // MODBUS_ESP_H diff --git a/components/MQTT_ESP/CMakeLists.txt b/components/MQTT_ESP/CMakeLists.txt new file mode 100644 index 0000000..df05464 --- /dev/null +++ b/components/MQTT_ESP/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "MQTT_ESP.c" + PRIV_REQUIRES mqtt log STATUS_LED MODBUS_ESP SNTP_ESP json esp_wifi esp_system + INCLUDE_DIRS "include") diff --git a/components/MQTT_ESP/Kconfig.projbuild b/components/MQTT_ESP/Kconfig.projbuild new file mode 100644 index 0000000..dbe2c9a --- /dev/null +++ b/components/MQTT_ESP/Kconfig.projbuild @@ -0,0 +1,59 @@ +menu "MQTT连接配置" + + config BROKER_URI + string "MQTT服务器地址" + default "mqtt://mqtt.eclipseprojects.io:1883" + help + 要连接的MQTT Broker的完整URL。例如: + ws://broker.emqx.io:8083/mqtt (明文WebSocket) + wss://broker.emqx.io:8084/mqtt (加密WebSocket) + mqtt://192.168.1.100:1883 (明文TCP) + + config MQTT_CLIENT_ID + string "客户端标识符" + default "esp32_client_01" + help + MQTT协议中用于识别客户端的唯一ID。如果留空,部分服务器会自动生成。 + + config MQTT_USERNAME + string "用户名" + default "" + help + 用于连接MQTT服务器的用户名(如果需要认证)。如果无需认证,请留空。 + + config MQTT_PASSWORD + string "用户密码" + default "" + help + 用于连接MQTT服务器的密码(如果需要认证)。如果无需认证,请留空。 + + config MQTT_PUB_TOPIC + string "发布主题" + default "/device/esp32/pub" + help + ESP32将向此主题(Topic)发布(Publish)消息。 + + config MQTT_SUB_TOPIC + string "订阅主题" + default "/device/esp32/sub" + help + ESP32将订阅(Subscribe)此主题(Topic)以接收消息。 + + config BROKER_CERTIFICATE_OVERRIDE + string "服务器证书覆盖" + default "" + help + 如果服务器证书已从文本文件加载,请留空;否则请填写PEM格式证书的base64编码部分。 + + config BROKER_CERTIFICATE_OVERRIDDEN + bool + default y if BROKER_CERTIFICATE_OVERRIDE != "" + + + config BROKER_BIN_SIZE_TO_SEND + # This option is not visible and is used only to set parameters for example tests + # Here we configure the data size to send and to be expected in the python script + int + default 20000 + +endmenu diff --git a/components/MQTT_ESP/MQTT_ESP.c b/components/MQTT_ESP/MQTT_ESP.c new file mode 100644 index 0000000..e686f86 --- /dev/null +++ b/components/MQTT_ESP/MQTT_ESP.c @@ -0,0 +1,487 @@ +#include +#include "MQTT_ESP.h" +#include "STATUS_LED.h" +#include "MODBUS_ESP.h" +#include "SNTP_ESP.h" +#include "cJSON.h" +#include "esp_wifi.h" +#include "esp_mac.h" +#include "esp_system.h" +#include "esp_netif.h" +#include + +static const char *TAG = "mqtt_esp"; + + +// 全局MQTT客户端句柄 +static esp_mqtt_client_handle_t g_client = NULL; + +// ============================ +// 设备状态上报任务相关 +// ============================ + +static TaskHandle_t device_status_task_handle = NULL; +static uint32_t g_report_interval_ms = 10000; // 默认10秒上报一次 +static SemaphoreHandle_t report_interval_mutex = NULL; + +/** + * @brief 获取设备MAC地址字符串 + */ +static void get_device_mac(char *mac_str, size_t max_len) +{ + uint8_t mac[6]; + esp_read_mac(mac, ESP_MAC_WIFI_STA); + snprintf(mac_str, max_len, "%02X:%02X:%02X:%02X:%02X:%02X", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +} + +/** + * @brief 获取设备运行时间(秒) + */ +static uint32_t get_device_uptime(void) +{ + return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS / 1000); +} + +/** + * @brief 获取设备IP地址字符串 + */ +static void get_device_ip(char *ip_str, size_t max_len) +{ + esp_netif_t *netif = esp_netif_get_handle_from_ifkey("ETH_DEF"); + if (netif != NULL) { + esp_netif_ip_info_t ip_info; + if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) { + snprintf(ip_str, max_len, IPSTR, IP2STR(&ip_info.ip)); + } else { + strncpy(ip_str, "N/A", max_len); + } + } else { + strncpy(ip_str, "N/A", max_len); + } +} + +/** + * @brief 获取当前时间字符串 + */ +static void get_current_time(char *time_str, size_t max_len) +{ + // 使用SNTP组件获取格式化时间 + if (sntp_esp_get_formatted_time(time_str, max_len, "%Y-%m-%d %H:%M:%S") != ESP_OK) { + // 如果获取失败,使用本地时间 + time_t now; + time(&now); + struct tm timeinfo; + localtime_r(&now, &timeinfo); + strftime(time_str, max_len, "%Y-%m-%d %H:%M:%S", &timeinfo); + } +} + +/** + * @brief 获取LED状态描述(中文) + */ +static const char* get_led_state_desc(led_state_t state) +{ + switch (state) { + case LED_OFF: return "关闭"; + case LED_ON: return "常亮"; + case LED_BLINK_SLOW: return "慢闪"; + case LED_BLINK_FAST: return "快闪"; + case LED_HEARTBEAT: return "心跳"; + default: return "未知"; + } +} + +/** + * @brief 获取运行时间描述(中文) + */ +static void get_uptime_desc(uint32_t uptime_sec, char *desc, size_t max_len) +{ + uint32_t days = uptime_sec / 86400; + uint32_t hours = (uptime_sec % 86400) / 3600; + uint32_t minutes = (uptime_sec % 3600) / 60; + uint32_t seconds = uptime_sec % 60; + + if (days > 0) { + snprintf(desc, max_len, "%lu天%lu小时%lu分%lu秒", days, hours, minutes, seconds); + } else if (hours > 0) { + snprintf(desc, max_len, "%lu小时%lu分%lu秒", hours, minutes, seconds); + } else if (minutes > 0) { + snprintf(desc, max_len, "%lu分%lu秒", minutes, seconds); + } else { + snprintf(desc, max_len, "%lu秒", seconds); + } +} + +/** + * @brief 构建设备状态JSON + */ +static char* build_device_status_json(void) +{ + char mac_str[18]; + char ip_str[16]; + char time_str[32]; + char uptime_desc[64]; + + get_device_mac(mac_str, sizeof(mac_str)); + get_device_ip(ip_str, sizeof(ip_str)); + get_current_time(time_str, sizeof(time_str)); + get_uptime_desc(get_device_uptime(), uptime_desc, sizeof(uptime_desc)); + + cJSON *root = cJSON_CreateObject(); + if (root == NULL) { + return NULL; + } + + // 基本信息 + cJSON_AddStringToObject(root, "message_type", "device_status"); + cJSON_AddStringToObject(root, "mac_address", mac_str); + cJSON_AddStringToObject(root, "ip_address", ip_str); + cJSON_AddStringToObject(root, "chip_model", "ESP32-S3"); + + // IDF 版本 + cJSON_AddStringToObject(root, "idf_version", esp_get_idf_version()); + + // 运行状态 + cJSON_AddNumberToObject(root, "uptime", get_device_uptime()); + cJSON_AddStringToObject(root, "uptime_desc", uptime_desc); // 中文描述 + cJSON_AddNumberToObject(root, "free_heap", esp_get_free_heap_size()); + cJSON_AddStringToObject(root, "status", "online"); + cJSON_AddStringToObject(root, "status_desc", "在线"); // 中文描述 + cJSON_AddStringToObject(root, "update_time", time_str); // 更新时间 + + // LED 状态(带中文描述) + led_state_t led1_state = status_led_get_state(1); + led_state_t led2_state = status_led_get_state(2); + cJSON_AddNumberToObject(root, "led1_state", (int)led1_state); + cJSON_AddStringToObject(root, "led1_desc", get_led_state_desc(led1_state)); // LED1: 网络状态 + cJSON_AddNumberToObject(root, "led2_state", (int)led2_state); + cJSON_AddStringToObject(root, "led2_desc", get_led_state_desc(led2_state)); // LED2: 通信状态 + + // LED 功能说明(中文) + cJSON_AddStringToObject(root, "led1_function", "网络状态灯"); + cJSON_AddStringToObject(root, "led2_function", "通信状态灯"); + + // MODBUS 轮询状态 + modbus_poll_config_t *modbus_config = modbus_get_current_config(); + if (modbus_config != NULL) { + cJSON_AddNumberToObject(root, "modbus_enabled", modbus_config->enabled); + cJSON_AddStringToObject(root, "modbus_enabled_desc", modbus_config->enabled ? "启用" : "禁用"); + cJSON_AddNumberToObject(root, "modbus_channel", modbus_config->channel_num); + cJSON_AddStringToObject(root, "modbus_channel_desc", modbus_config->channel_num == 0 ? "通道0 (UART0)" : "通道1 (UART2)"); + cJSON_AddNumberToObject(root, "modbus_slave_addr", modbus_config->slave_addr); + cJSON_AddNumberToObject(root, "modbus_interval", modbus_config->poll_interval_ms); + } else { + cJSON_AddNumberToObject(root, "modbus_enabled", 0); + cJSON_AddStringToObject(root, "modbus_enabled_desc", "未配置"); + cJSON_AddNumberToObject(root, "modbus_channel", 0); + cJSON_AddStringToObject(root, "modbus_channel_desc", "N/A"); + cJSON_AddNumberToObject(root, "modbus_slave_addr", 0); + cJSON_AddNumberToObject(root, "modbus_interval", 0); + } + + // 内存使用情况 + uint32_t total_heap = esp_get_free_heap_size(); + cJSON_AddStringToObject(root, "heap_status", total_heap > 100000 ? "充足" : (total_heap > 50000 ? "一般" : "紧张")); + + char *json_str = cJSON_Print(root); + cJSON_Delete(root); + return json_str; +} + +/** + * @brief 设备状态上报任务 + */ +static void device_status_report_task(void *arg) +{ + ESP_LOGI(TAG, "Device status report task started"); + + while (1) { + // 检查是否应该退出 + if (device_status_task_handle == NULL) { + break; + } + + // 检查MQTT是否已连接 + if (g_client != NULL) { + // 构建设备状态JSON + char *status_json = build_device_status_json(); + if (status_json != NULL) { + // 发布到MQTT + int ret = esp_mqtt_client_publish(g_client, CONFIG_MQTT_PUB_TOPIC, + status_json, strlen(status_json), 0, 0); + if (ret >= 0) { + ESP_LOGI(TAG, "Device status published, msg_id=%d", ret); + ESP_LOGD(TAG, "Status: %s", status_json); + } else { + ESP_LOGW(TAG, "Failed to publish device status"); + } + free(status_json); + } + } + + // 获取上报间隔 + uint32_t interval; + xSemaphoreTake(report_interval_mutex, portMAX_DELAY); + interval = g_report_interval_ms; + xSemaphoreGive(report_interval_mutex); + + // 等待下一次上报 + vTaskDelay(pdMS_TO_TICKS(interval)); + } + + ESP_LOGI(TAG, "Device status report task exiting"); + device_status_task_handle = NULL; + vTaskDelete(NULL); +} + +BaseType_t mqtt_start_device_status_task(uint32_t report_interval_ms) +{ + // 如果任务已存在,先停止 + if (device_status_task_handle != NULL) { + mqtt_stop_device_status_task(); + vTaskDelay(pdMS_TO_TICKS(100)); + } + + // 创建互斥量 + if (report_interval_mutex == NULL) { + report_interval_mutex = xSemaphoreCreateMutex(); + if (report_interval_mutex == NULL) { + ESP_LOGE(TAG, "Failed to create report interval mutex"); + return pdFALSE; + } + } + + // 更新上报间隔 + xSemaphoreTake(report_interval_mutex, portMAX_DELAY); + g_report_interval_ms = report_interval_ms > 0 ? report_interval_ms : 10000; + xSemaphoreGive(report_interval_mutex); + + // 创建任务 + BaseType_t ret = xTaskCreate(device_status_report_task, "dev_status", + 4096, NULL, 5, &device_status_task_handle); + + if (ret == pdPASS) { + ESP_LOGI(TAG, "Device status report task started (interval=%dms)", g_report_interval_ms); + } else { + ESP_LOGE(TAG, "Failed to create device status report task"); + } + + return ret; +} + +void mqtt_stop_device_status_task(void) +{ + if (device_status_task_handle != NULL) { + ESP_LOGI(TAG, "Stopping device status report task..."); + + TaskHandle_t temp_handle = device_status_task_handle; + device_status_task_handle = NULL; + vTaskDelete(temp_handle); + + vTaskDelay(pdMS_TO_TICKS(200)); + ESP_LOGI(TAG, "Device status report task stopped"); + } +} + +void mqtt_update_report_interval(uint32_t report_interval_ms) +{ + if (report_interval_mutex != NULL) { + xSemaphoreTake(report_interval_mutex, portMAX_DELAY); + g_report_interval_ms = report_interval_ms > 0 ? report_interval_ms : 10000; + xSemaphoreGive(report_interval_mutex); + ESP_LOGI(TAG, "Device status report interval updated to %dms", g_report_interval_ms); + } +} + + +/** + * @brief MQTT事件处理函数 + * + * 处理MQTT客户端的各种事件,包括连接、订阅、发布、数据接收和错误处理。 + * + * @param handler_args 事件处理器参数 + * @param base 事件基础类型 + * @param event_id 事件ID + * @param event_data 指向MQTT事件数据的指针 + */ +static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%" PRIi32, base, event_id); + esp_mqtt_event_handle_t event = event_data; + esp_mqtt_client_handle_t client = event->client; + int msg_id; + switch ((esp_mqtt_event_id_t)event_id) { + case MQTT_EVENT_CONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED"); + // 订阅并取消订阅测试主题 + msg_id = esp_mqtt_client_subscribe(client, CONFIG_MQTT_SUB_TOPIC, 1); + ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); +status_led_blink_mode(2, 2); // LED2 心跳:MQTT连接正常 + break; + case MQTT_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED"); + status_led_blink_mode(2, 1); // LED2 快闪:MQTT断开,正在重连 + break; + + case MQTT_EVENT_SUBSCRIBED: + ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d, return code=0x%02x ", event->msg_id, (uint8_t)*event->data); + // 立即上报一次设备状态 + char *status_json = build_device_status_json(); + if (status_json != NULL) { + msg_id = esp_mqtt_client_publish(client, CONFIG_MQTT_PUB_TOPIC, + status_json, strlen(status_json), 0, 0); + if (msg_id >= 0) { + ESP_LOGI(TAG, "Device status published immediately, msg_id=%d", msg_id); + } + free(status_json); + } + break; + case MQTT_EVENT_UNSUBSCRIBED: + ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_PUBLISHED: + ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_DATA: + ESP_LOGI(TAG, "MQTT_EVENT_DATA"); + ESP_LOGI(TAG, "TOPIC=%.*s", event->topic_len, event->topic); + ESP_LOGI(TAG, "DATA=%.*s", event->data_len, event->data); + + // 解析MQTT控制指令(JSON格式) + if (event->data_len > 0 && event->data != NULL) { + // 创建临时缓冲区存储JSON数据 + char *json_str = malloc(event->data_len + 1); + if (json_str != NULL) { + memcpy(json_str, event->data, event->data_len); + json_str[event->data_len] = '\0'; + + // 解析JSON + cJSON *root = cJSON_Parse(json_str); + if (root != NULL) { + // 检查是否是MODBUS轮询控制指令 + cJSON *cmd = cJSON_GetObjectItem(root, "command"); + if (cmd != NULL && cJSON_IsString(cmd)) { + if (strcmp(cmd->valuestring, "modbus_poll") == 0) { + // 解析轮询配置 + cJSON *channel = cJSON_GetObjectItem(root, "channel"); + cJSON *slave_addr = cJSON_GetObjectItem(root, "slave_addr"); + cJSON *start_addr = cJSON_GetObjectItem(root, "start_addr"); + cJSON *reg_count = cJSON_GetObjectItem(root, "reg_count"); + cJSON *interval = cJSON_GetObjectItem(root, "interval"); + cJSON *enabled = cJSON_GetObjectItem(root, "enabled"); + + // 验证必要字段 + if (channel != NULL && cJSON_IsNumber(channel) && + slave_addr != NULL && cJSON_IsNumber(slave_addr) && + start_addr != NULL && cJSON_IsNumber(start_addr) && + reg_count != NULL && cJSON_IsNumber(reg_count) && + interval != NULL && cJSON_IsNumber(interval)) { + + // 构建轮询配置 + modbus_poll_config_t poll_config = { + .channel_num = channel->valueint, + .slave_addr = (uint8_t)slave_addr->valueint, + .start_addr = (uint16_t)start_addr->valueint, + .reg_count = (uint16_t)reg_count->valueint, + .poll_interval_ms = (uint32_t)interval->valueint, + .enabled = (enabled != NULL && cJSON_IsBool(enabled)) ? cJSON_IsTrue(enabled) : true + }; + + // 更新轮询配置 + if (modbus_update_poll_config(&poll_config)) { + ESP_LOGI(TAG, "MODBUS poll config updated via MQTT"); + } else { + ESP_LOGE(TAG, "Failed to update MODBUS poll config"); + } + } else { + ESP_LOGE(TAG, "Missing required fields in MODBUS poll command"); + } + } + } + cJSON_Delete(root); + } else { + ESP_LOGE(TAG, "Failed to parse JSON: %s", json_str); + } + free(json_str); + } + } + + break; + case MQTT_EVENT_ERROR: + ESP_LOGI(TAG, "MQTT_EVENT_ERROR"); + // 错误类型处理分支 + if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { + ESP_LOGI(TAG, "Last error code reported from esp-tls: 0x%x", event->error_handle->esp_tls_last_esp_err); + ESP_LOGI(TAG, "Last tls stack error number: 0x%x", event->error_handle->esp_tls_stack_err); + ESP_LOGI(TAG, "Last captured errno : %d (%s)", event->error_handle->esp_transport_sock_errno, + strerror(event->error_handle->esp_transport_sock_errno)); + } else if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) { + ESP_LOGI(TAG, "Connection refused error: 0x%x", event->error_handle->connect_return_code); + } else { + ESP_LOGW(TAG, "Unknown error type: 0x%x", event->error_handle->error_type); + } + status_led_blink_mode(2, 0); // LED2 慢闪:MQTT错误 + break; + default: + ESP_LOGI(TAG, "Other event id:%d", event->event_id); + break; + } +} + +/** + * @brief 启动MQTT客户端应用程序 + * + * 初始化MQTT客户端配置,注册事件处理程序,并启动MQTT连接 + * + * @param 无参数 + * @return 无返回值 + */ +void mqtt_app_start(void) +{ + // 配置MQTT客户端结构体,设置代理服务器地址和证书验证 + const esp_mqtt_client_config_t mqtt_cfg = { + .broker.address.uri = CONFIG_BROKER_URI, + // .broker.verification.certificate = (const char *)mqtt_eclipseprojects_io_pem_start, + .credentials.client_id = CONFIG_MQTT_CLIENT_ID, + .credentials.username = CONFIG_MQTT_USERNAME, + .credentials.authentication.password = CONFIG_MQTT_PASSWORD, + }; + + ESP_LOGI(TAG, "[APP] Free memory: %" PRIu32 " bytes", esp_get_free_heap_size()); + g_client = esp_mqtt_client_init(&mqtt_cfg); + /* The last argument may be used to pass data to the event handler, in this example mqtt_event_handler */ + esp_mqtt_client_register_event(g_client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL); + + esp_mqtt_client_start(g_client); +} + + +/** + * @brief 发布MQTT消息 + * + * 提供一个外部接口来发布MQTT消息 + * + * @param topic 发布的主题 + * @param data 要发布的数据 + * @param len 数据长度 + * @param qos QoS级别 (0, 1, 或 2) + * @param retain 是否保留消息 + * @return 消息ID,如果失败则返回负数 + */ +int mqtt_publish_message(const char* topic, const char* data, int len, int qos, int retain) +{ + if (g_client == NULL) { + ESP_LOGE(TAG, "MQTT client not initialized"); + return -1; + } + + int msg_id = esp_mqtt_client_publish(g_client, topic, data, len, qos, retain); + if (msg_id < 0) { + ESP_LOGE(TAG, "Failed to publish message to topic: %s", topic); + return -1; + } + + ESP_LOGI(TAG, "Published message to topic: %s, msg_id: %d", topic, msg_id); + return msg_id; +} diff --git a/components/MQTT_ESP/include/MQTT_ESP.h b/components/MQTT_ESP/include/MQTT_ESP.h new file mode 100644 index 0000000..237934a --- /dev/null +++ b/components/MQTT_ESP/include/MQTT_ESP.h @@ -0,0 +1,25 @@ +#include "mqtt_client.h" +#include "esp_log.h" + +void mqtt_app_start(void); +int mqtt_publish_message(const char* topic, const char* data, int len, int qos, int retain); + +/** + * @brief 启动设备状态上报任务 + * + * @param report_interval_ms 上报间隔(毫秒) + * @return pdTRUE 成功, pdFALSE 失败 + */ +BaseType_t mqtt_start_device_status_task(uint32_t report_interval_ms); + +/** + * @brief 停止设备状态上报任务 + */ +void mqtt_stop_device_status_task(void); + +/** + * @brief 更新设备状态上报间隔 + * + * @param report_interval_ms 新的上报间隔(毫秒) + */ +void mqtt_update_report_interval(uint32_t report_interval_ms); \ No newline at end of file diff --git a/components/RS-485-SP3485EEN/CMakeLists.txt b/components/RS-485-SP3485EEN/CMakeLists.txt new file mode 100644 index 0000000..fe76b47 --- /dev/null +++ b/components/RS-485-SP3485EEN/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "RS-485-SP3485EEN.c" + INCLUDE_DIRS "include" + REQUIRES nvs_flash driver MQTT_ESP mqtt STATUS_LED MODBUS_ESP json + ) diff --git a/components/RS-485-SP3485EEN/RS-485-SP3485EEN.c b/components/RS-485-SP3485EEN/RS-485-SP3485EEN.c new file mode 100644 index 0000000..22dd585 --- /dev/null +++ b/components/RS-485-SP3485EEN/RS-485-SP3485EEN.c @@ -0,0 +1,311 @@ +#include "RS-485-SP3485EEN.h" +#include +#include "MQTT_ESP.h" +#include "STATUS_LED.h" +#include "MODBUS_ESP.h" +#include "cJSON.h" +#define TAG "RS485_DRIVER" +// Timeout threshold for UART = number of symbols (~10 tics) with unchanged state on receive pin + +#define ECHO_READ_TOUT (3) // 3.5T * 8 = 28 ticks, TOUT=3 -> ~24..33 ticks + +// ---------------------------- +// 通道数组定义 +// ---------------------------- +rs485_channel_t rs485_channels[] = { + {RS_485_SP3485EEN_UART_PORT, RS_485_SP3485EEN_DI_PIN, RS_485_SP3485EEN_RO_PIN, RS_485_SP3485EEN_DE_RE_PIN, "RS485-1"}, + {RS_485_SP3485EEN_2_UART_PORT, RS_485_SP3485EEN_2_DI_PIN, RS_485_SP3485EEN_2_RO_PIN, RS_485_SP3485EEN_2_DE_RE_PIN, "RS485-2"}}; + +// ============================ +// UART 发送函数 +// ============================ +void rs485_send(uart_port_t uart_num, const uint8_t *data, size_t len) +{ + if (data == NULL || len == 0) + { + ESP_LOGW(TAG, "rs485_send: empty payload"); + return; + } + + // 清空 RX,防止残留帧污染 + uart_flush_input(uart_num); + + // 发送数据 + int written = uart_write_bytes(uart_num, (const char *)data, len); + if (written < 0) + { + ESP_LOGE(TAG, "UART%d TX write error (%d)", uart_num, written); + return; + } + if ((size_t)written != len) + { + ESP_LOGW(TAG, "UART%d TX partial (%d/%d)", uart_num, written, len); + } + + // RS485 半双工模式下,uart_write_bytes 会自动等待发送完成 + // 这里只需要等待一个额外的延时确保所有数据都已发送完成 + // 根据波特率和字节数计算传输时间(11位/字节:1起始位+8数据位+1停止位+1奇偶校验位) + uint32_t transmit_time_ms = (len * 11 * 1000) / (BAUD_RATE > 0 ? BAUD_RATE : 1); + vTaskDelay(pdMS_TO_TICKS(transmit_time_ms + 5)); + + // Modbus RTU 3.5T 帧间静默(保留短延时) + vTaskDelay(pdMS_TO_TICKS(5)); + + ESP_LOGI(TAG, "UART%d TX done (%d bytes)", uart_num, written); +} + +// ============================ +// UART 接收函数 +// ============================ +int rs485_receive(uart_port_t uart_num, uint8_t *buffer, size_t buf_size, uint32_t timeout_ms) +{ + if (buffer == NULL || buf_size == 0) + { + ESP_LOGW(TAG, "rs485_receive: invalid buffer"); + return -1; + } + + int len = uart_read_bytes(uart_num, buffer, buf_size, pdMS_TO_TICKS(timeout_ms)); + if (len > 0) + { + ESP_LOGI(TAG, "UART%d RX (%d bytes)", uart_num, len); + // 不在这里打印数据,交由调用者(task)处理,避免重复日志 + } + else if (len == 0) + { + ESP_LOGD(TAG, "UART%d RX timeout", uart_num); + } + else + { + ESP_LOGW(TAG, "UART%d RX error (%d)", uart_num, len); + } + + return len; +} +// ============================ +// RS485 初始化函数 +// ============================ +void RS_485_init(uart_port_t uart_num, int tx_pin, int rx_pin, int de_re_pin) +{ + uart_config_t uart_config = { + .baud_rate = BAUD_RATE, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, + .rx_flow_ctrl_thresh = 122, + }; + + ESP_ERROR_CHECK(uart_driver_install( + uart_num, + BUF_SIZE * 2, + BUF_SIZE * 2, + 0, + NULL, + 0)); + + ESP_ERROR_CHECK(uart_param_config(uart_num, &uart_config)); + ESP_ERROR_CHECK(uart_set_pin( + uart_num, + tx_pin, + rx_pin, + de_re_pin, + UART_PIN_NO_CHANGE)); + + ESP_ERROR_CHECK(uart_set_mode(uart_num, UART_MODE_RS485_HALF_DUPLEX)); + ESP_ERROR_CHECK(uart_set_rx_timeout(uart_num, ECHO_READ_TOUT)); + + ESP_LOGI(TAG, + "RS485 init OK: UART%d TX=%d RX=%d DE/RE=%d", + uart_num, tx_pin, rx_pin, de_re_pin); +} + +// ============================ +// 初始化指定RS485通道 +// ============================ +void init_specific_rs485_channel(int channel_num) +{ + if (channel_num < 0 || channel_num >= NUM_CHANNELS) + { + ESP_LOGE(TAG, "Invalid channel number: %d", channel_num); + return; + } + + rs485_channel_t *ch = &rs485_channels[channel_num]; + + // 初始化RS485硬件 + RS_485_init(ch->uart_num, ch->tx_pin, ch->rx_pin, ch->de_re_pin); + + ESP_LOGI(TAG, "Channel %d: %s (UART%d) initialized", + channel_num, ch->name, ch->uart_num); +} + +// ============================ +// 初始化所有RS485通道 +// ============================ +void init_all_rs485_channels(void) +{ + ESP_LOGI(TAG, "Initializing %d RS485 channels", NUM_CHANNELS); + + for (int i = 0; i < NUM_CHANNELS; i++) + { + init_specific_rs485_channel(i); + } +} + +/* 接收任务:参数为通道索引 (int cast via intptr_t) */ +static void rs485_rx_task(void *arg) +{ + int channel = (int)(intptr_t)arg; + if (channel < 0 || channel >= NUM_CHANNELS) + { + ESP_LOGE(TAG, "rs485_rx_task: invalid channel %d", channel); + vTaskDelete(NULL); + return; + } + + rs485_channel_t *ch = &rs485_channels[channel]; + + /* 使用堆分配,避免栈溢出 */ + uint8_t *buf = malloc(BUF_SIZE); + if (buf == NULL) + { + ESP_LOGE(TAG, "rs485_rx_task: malloc failed"); + vTaskDelete(NULL); + return; + } + + ESP_LOGI(TAG, "%s RX task started", ch->name); + + while (1) + { + int len = rs485_receive(ch->uart_num, buf, BUF_SIZE, 100); // 100ms timeout for Modbus + if (len > 0) + { + // 数据接收成功,LED2 短暂亮起表示有数据 + status_led_set(2, 1); + ESP_LOGI(TAG, "%s UART%d RX (%d bytes)", ch->name, ch->uart_num, len); + ESP_LOG_BUFFER_HEX(TAG, buf, len); + + // 尝试解析MODBUS响应 + modbus_response_t response; + if (modbus_parse_response(buf, len, &response)) + { + // MODBUS解析成功,构建JSON格式数据 + cJSON *root = cJSON_CreateObject(); + if (root != NULL) + { + // 添加基本信息 + cJSON_AddStringToObject(root, "channel", ch->name); + cJSON_AddNumberToObject(root, "slave_addr", response.slave_addr); + cJSON_AddNumberToObject(root, "function_code", response.function_code); + + if (response.is_exception) + { + // 异常响应 + cJSON_AddStringToObject(root, "status", "exception"); + cJSON_AddNumberToObject(root, "exception_code", response.exception_code); + ESP_LOGW(TAG, "Modbus exception response: code=0x%02X", response.exception_code); + } + else + { + // 正常响应 + cJSON_AddStringToObject(root, "status", "success"); + cJSON_AddNumberToObject(root, "byte_count", response.byte_count); + cJSON_AddNumberToObject(root, "register_count", response.register_count); + + // 添加寄存器数组 + cJSON *reg_array = cJSON_CreateArray(); + if (reg_array != NULL) + { + for (uint8_t i = 0; i < response.register_count; i++) + { + cJSON_AddItemToArray(reg_array, cJSON_CreateNumber(response.registers[i])); + } + cJSON_AddItemToObject(root, "registers", reg_array); + } + + ESP_LOGI(TAG, "Modbus response parsed: slave=%d, func=0x%02X, regs=%d", + response.slave_addr, response.function_code, response.register_count); + } + + // 转换为JSON字符串 + char *json_str = cJSON_Print(root); + if (json_str != NULL) + { + // 发布JSON数据到MQTT + int ret = mqtt_publish_message(CONFIG_MQTT_PUB_TOPIC, json_str, strlen(json_str), 0, 0); + if (ret < 0) + { + ESP_LOGW(TAG, "Failed to publish Modbus JSON to MQTT topic %s", CONFIG_MQTT_PUB_TOPIC); + } + else + { + ESP_LOGI(TAG, "Published Modbus JSON to MQTT topic %s, msg_id: %d", CONFIG_MQTT_PUB_TOPIC, ret); + ESP_LOGD(TAG, "JSON payload: %s", json_str); + } + free(json_str); + } + cJSON_Delete(root); + } + // 释放响应中的寄存器内存 + modbus_free_response(&response); + } + else + { + // MODBUS解析失败,原始数据直接上报 + ESP_LOGW(TAG, "Failed to parse Modbus frame, publishing raw data"); + int ret = mqtt_publish_message(CONFIG_MQTT_PUB_TOPIC, (char *)buf, len, 0, 0); + if (ret < 0) + { + ESP_LOGW(TAG, "Failed to publish RS485 data to MQTT topic %s", CONFIG_MQTT_PUB_TOPIC); + } + } + vTaskDelay(pdMS_TO_TICKS(50)); // 保持亮起50ms + status_led_blink_mode(2, 2); // 恢复心跳模式 + } + else if (len == 0) + { + vTaskDelay(pdMS_TO_TICKS(50)); // 无数据短延时 + } + else + { + // 接收错误,不记录日志避免刷屏 + vTaskDelay(pdMS_TO_TICKS(200)); + } + } + /* 永久任务通常不会到这里;若退出则释放 */ + free(buf); + vTaskDelete(NULL); +} + +/* 启动单个通道的接收任务 + 返回 pdPASS 或 pdFAIL */ +BaseType_t start_rs485_rx_task_for_channel(int channel_num, UBaseType_t priority, uint32_t stack_size) +{ + if (channel_num < 0 || channel_num >= NUM_CHANNELS) + { + ESP_LOGE(TAG, "start_rs485_rx_task_for_channel: invalid channel %d", channel_num); + return pdFAIL; + } + char tname[16]; + snprintf(tname, sizeof(tname), "rs485_rx_%d", channel_num); + return xTaskCreate(rs485_rx_task, tname, stack_size, (void *)(intptr_t)channel_num, priority, NULL); +} + +/* 可选:启动所有通道的接收任务(示例默认优先级与栈)*/ +void start_all_rs485_rx_tasks(UBaseType_t priority, uint32_t stack_size) +{ + for (int i = 0; i < NUM_CHANNELS; i++) + { + if (start_rs485_rx_task_for_channel(i, priority, stack_size) != pdPASS) + { + ESP_LOGW(TAG, "Failed to start RX task for channel %d", i); + } + else + { + ESP_LOGI(TAG, "Started RX task for channel %d", i); + } + } +} diff --git a/components/RS-485-SP3485EEN/include/RS-485-SP3485EEN.h b/components/RS-485-SP3485EEN/include/RS-485-SP3485EEN.h new file mode 100644 index 0000000..350ef9c --- /dev/null +++ b/components/RS-485-SP3485EEN/include/RS-485-SP3485EEN.h @@ -0,0 +1,66 @@ +// RS-485-SP3485EEN.h +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "nvs_flash.h" +#include "driver/uart.h" +#include "freertos/queue.h" +#include "esp_log.h" +#include "driver/gpio.h" + +typedef struct +{ + uart_port_t uart_num; + gpio_num_t tx_pin; + gpio_num_t rx_pin; + gpio_num_t de_re_pin; + char name[20]; +} rs485_channel_t; + +/** + * @brief RS-485-SP3485EEN.h + * DE和RE引脚接在一起使用,默认是下拉,通过控制DE_RE引脚的高低电平来控制发送和接收 + * DE_RE = HIGH 发送数据,接收器使能,低电平有效。低电平时使能接收器,高电平时禁用接收器 + * DE_RE = LOW 接收数据,驱动器(发送器)使能,高电平有效。高电平时使能发送器,低电平时禁用发送器 + * + * 所以默认是接收状态,发送数据时需要先将DE_RE引脚拉高,发送完数据后再拉低 + */ + +// 第一个模块的引脚配置 +#define RS_485_SP3485EEN_UART_PORT (UART_NUM_0) +#define RS_485_SP3485EEN_RO_PIN (GPIO_NUM_41) // 接收器输出。将总线上的差分信号转换为TTL电平,输出给 单片机RX +#define RS_485_SP3485EEN_DE_RE_PIN (GPIO_NUM_42) // 数据使能,接收器使能。控制发送器和接收器是否工作 +#define RS_485_SP3485EEN_DI_PIN (GPIO_NUM_44) // 驱动器输入。单片机TX 发送的TTL电平信号,转换为总线上的差分信号 + +// 第二个模块的引脚配置 +#define RS_485_SP3485EEN_2_UART_PORT (UART_NUM_2) +#define RS_485_SP3485EEN_2_RO_PIN (GPIO_NUM_43) // 接收器输出。将总线上的差分信号转换为TTL电平,输出给 单片机RX +#define RS_485_SP3485EEN_2_DE_RE_PIN (GPIO_NUM_2) // 数据使能,接收器使能。控制发送器和接收器是否工作 +#define RS_485_SP3485EEN_2_DI_PIN (GPIO_NUM_1) // 驱动器输入。单片机TX 发送的TTL电平信号,转换为总线上的差分信号 + +// 公共配置 +#define BUF_SIZE 256 +#define BAUD_RATE 115200 + +// 通道数量常量 +#define RS485_NUM_CHANNELS 2 + +// ---------------------------- +// 通道数组 +// ---------------------------- + +extern rs485_channel_t rs485_channels[]; +#define NUM_CHANNELS RS485_NUM_CHANNELS + +void RS_485_init(uart_port_t uart_num, int tx_pin, int rx_pin, int de_re_pin); +void init_specific_rs485_channel(int channel_num); // 新增函数声明 +void init_all_rs485_channels(void); + +void rs485_send(uart_port_t uart_num, const uint8_t *data, size_t len); +int rs485_receive(uart_port_t uart_num, uint8_t *buffer, size_t buf_size, uint32_t timeout_ms); + +BaseType_t start_rs485_rx_task_for_channel(int channel_num, UBaseType_t priority, uint32_t stack_size); + diff --git a/components/SNTP_ESP/CMakeLists.txt b/components/SNTP_ESP/CMakeLists.txt new file mode 100644 index 0000000..9089d9a --- /dev/null +++ b/components/SNTP_ESP/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "SNTP_ESP.c" + INCLUDE_DIRS "include" + REQUIRES lwip) diff --git a/components/SNTP_ESP/SNTP_ESP.c b/components/SNTP_ESP/SNTP_ESP.c new file mode 100644 index 0000000..8e1540a --- /dev/null +++ b/components/SNTP_ESP/SNTP_ESP.c @@ -0,0 +1,133 @@ +#include "SNTP_ESP.h" +#include "esp_log.h" +#include "esp_sntp.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include +#include + +#define TAG "SNTP_ESP" + +static bool time_synced = false; + +// 时间同步回调函数 +static void sntp_sync_time_callback(struct timeval *tv) +{ + time_t now = tv->tv_sec; + struct tm timeinfo; + localtime_r(&now, &timeinfo); + + char time_str[64]; + strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", &timeinfo); + + ESP_LOGI(TAG, "时间同步成功: %s", time_str); + time_synced = true; +} + +esp_err_t sntp_esp_set_timezone(void) +{ + // 设置中国标准时间(北京时间) + setenv("TZ", "CST-8", 1); + tzset(); + ESP_LOGI(TAG, "时区设置为北京时间 (CST-8)"); + return ESP_OK; +} + +time_t sntp_esp_get_current_time(void) +{ + // 使用POSIX函数获取时间 + return time(NULL); +} + +void sntp_esp_print_current_time(void) +{ + time_t now = sntp_esp_get_current_time(); + struct tm timeinfo; + char buffer[64]; + + localtime_r(&now, &timeinfo); + strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S %A", &timeinfo); + + ESP_LOGI(TAG, "当前时间: %s", buffer); +} + +esp_err_t sntp_esp_get_formatted_time(char *buffer, size_t size, const char *format) +{ + if (buffer == NULL || format == NULL) { + return ESP_FAIL; + } + + time_t now = sntp_esp_get_current_time(); + struct tm timeinfo; + localtime_r(&now, &timeinfo); + + strftime(buffer, size, format, &timeinfo); + return ESP_OK; +} + +bool sntp_esp_is_synced(void) +{ + time_t now = time(NULL); + // 检查时间是否已初始化(从1970年到现在) + return (now > 1000000000); // 2001年9月9日之后的任何时间都认为是有效时间 +} + +bool sntp_esp_wait_sync(uint32_t max_wait_ms) +{ + ESP_LOGI(TAG, "等待时间同步(最长等待 %lu ms)...", (unsigned long)max_wait_ms); + + uint32_t start_time = xTaskGetTickCount(); + uint32_t max_wait_ticks = pdMS_TO_TICKS(max_wait_ms); + + while ((xTaskGetTickCount() - start_time) < max_wait_ticks) { + if (sntp_esp_is_synced()) { + ESP_LOGI(TAG, "时间同步完成"); + return true; + } + vTaskDelay(pdMS_TO_TICKS(100)); // 每100毫秒检查一次 + } + + ESP_LOGW(TAG, "时间同步超时"); + return false; +} + +esp_err_t sntp_esp_init(void) +{ + ESP_LOGI(TAG, "初始化SNTP服务"); + + // 重置同步标志 + time_synced = false; + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + // 设置时间服务器(默认使用 pool.ntp.org) + esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); + + // 添加 NTP 服务器 + // esp_sntp_setservername(0, "pool.ntp.org"); // 默认服务器 + esp_sntp_setservername(0, "cn.pool.ntp.org"); // 中国 NTP 服务器 + esp_sntp_setservername(1, "ntp1.aliyun.com"); // 阿里云 NTP 服务器 + esp_sntp_setservername(2, "ntp.tencent.com"); // 腾讯云 NTP 服务器 + + // 设置时间同步回调 + esp_sntp_set_time_sync_notification_cb(sntp_sync_time_callback); + + // 初始化 SNTP + esp_sntp_init(); +#else + sntp_setoperatingmode(SNTP_OPMODE_POLL); + + // esp_sntp_setservername(0, "pool.ntp.org"); // 默认服务器 + sntp_setservername(0, "cn.pool.ntp.org"); // 中国 NTP 服务器 + sntp_setservername(1, "ntp1.aliyun.com"); // 阿里云 NTP 服务器 + sntp_setservername(2, "ntp.tencent.com"); // 腾讯云 NTP 服务器 + + sntp_set_time_sync_notification_cb(sntp_sync_time_callback); + sntp_init(); // 初始化 SNTP +#endif + + sntp_esp_set_timezone(); // 设置时区 + sntp_esp_print_current_time(); // 打印时间 + + ESP_LOGI(TAG, "SNTP服务初始化完成"); + return ESP_OK; +} diff --git a/components/SNTP_ESP/include/SNTP_ESP.h b/components/SNTP_ESP/include/SNTP_ESP.h new file mode 100644 index 0000000..6a20bb2 --- /dev/null +++ b/components/SNTP_ESP/include/SNTP_ESP.h @@ -0,0 +1,72 @@ +#ifndef SNTP_ESP_H +#define SNTP_ESP_H + +#include +#include +#include "esp_err.h" + +/** + * @brief 初始化SNTP服务 + * + * 初始化SNTP客户端,配置NTP服务器和时区 + * 必须在获得网络连接后调用 + * + * @return ESP_OK 成功 + * ESP_FAIL 失败 + */ +esp_err_t sntp_esp_init(void); + +/** + * @brief 设置时区 + * + * 设置本地时区,默认为北京时间(CST-8) + * + * @return ESP_OK 成功 + */ +esp_err_t sntp_esp_set_timezone(void); + +/** + * @brief 获取当前时间 + * + * @return time_t 当前时间戳 + */ +time_t sntp_esp_get_current_time(void); + +/** + * @brief 打印当前时间 + * + * 以格式化的方式打印当前时间到日志 + */ +void sntp_esp_print_current_time(void); + +/** + * @brief 获取格式化的时间字符串 + * + * @param buffer 存储时间字符串的缓冲区 + * @param size 缓冲区大小 + * @param format 时间格式(如 "%Y-%m-%d %H:%M:%S") + * @return ESP_OK 成功 + * ESP_FAIL 失败 + */ +esp_err_t sntp_esp_get_formatted_time(char *buffer, size_t size, const char *format); + +/** + * @brief 等待时间同步完成 + * + * 等待SNTP时间同步完成 + * + * @param max_wait_ms 最大等待时间(毫秒) + * @return true 同步成功 + * false 超时 + */ +bool sntp_esp_wait_sync(uint32_t max_wait_ms); + +/** + * @brief 检查时间是否已同步 + * + * @return true 已同步 + * false 未同步 + */ +bool sntp_esp_is_synced(void); + +#endif // SNTP_ESP_H diff --git a/components/STATUS_LED/CMakeLists.txt b/components/STATUS_LED/CMakeLists.txt new file mode 100644 index 0000000..ccd996e --- /dev/null +++ b/components/STATUS_LED/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "STATUS_LED.c" + INCLUDE_DIRS "include" + REQUIRES driver log + ) diff --git a/components/STATUS_LED/STATUS_LED.c b/components/STATUS_LED/STATUS_LED.c new file mode 100644 index 0000000..0e8da13 --- /dev/null +++ b/components/STATUS_LED/STATUS_LED.c @@ -0,0 +1,257 @@ +#include +#include "STATUS_LED.h" + +static const char *TAG = "status_led"; + +// LED 引脚定义 +#define STATUS_LED_1_PIN (GPIO_NUM_8) +#define STATUS_LED_2_PIN (GPIO_NUM_18) + + +// LED 控制结构体 +typedef struct { + gpio_num_t pin; + led_state_t state; + int blink_period_ms; + bool current_level; // 实际输出电平 + TaskHandle_t task_handle; + bool task_running; +} status_led_t; + +static status_led_t led1 = {.pin = STATUS_LED_1_PIN, .state = LED_OFF, .task_running = false}; +static status_led_t led2 = {.pin = STATUS_LED_2_PIN, .state = LED_OFF, .task_running = false}; + +/** + * @brief LED控制任务 + * + * @param param 指向LED结构体的指针 + */ +static void led_control_task(void *param) +{ + status_led_t *led = (status_led_t *)param; + + while(led->task_running) { + switch(led->state) { + case LED_OFF: + gpio_set_level(led->pin, 1); // 低电平点亮,所以高电平关闭 + vTaskDelay(100 / portTICK_PERIOD_MS); // 延迟100ms后再次检查状态 + break; + + case LED_ON: + gpio_set_level(led->pin, 0); // 低电平点亮 + vTaskDelay(100 / portTICK_PERIOD_MS); // 延迟100ms后再次检查状态 + break; + + case LED_BLINK_SLOW: + led->current_level = !led->current_level; + gpio_set_level(led->pin, !led->current_level); // 反转输出以适应低电平点亮 + vTaskDelay(500 / portTICK_PERIOD_MS); // 慢闪周期500ms + break; + + case LED_BLINK_FAST: + led->current_level = !led->current_level; + gpio_set_level(led->pin, !led->current_level); // 反转输出以适应低电平点亮 + vTaskDelay(100 / portTICK_PERIOD_MS); // 快闪周期100ms + break; + + case LED_HEARTBEAT: + // 心跳模式:快速亮两次,然后长灭 + gpio_set_level(led->pin, 0); // 点亮 + vTaskDelay(100 / portTICK_PERIOD_MS); + gpio_set_level(led->pin, 1); // 熄灭 + vTaskDelay(100 / portTICK_PERIOD_MS); + gpio_set_level(led->pin, 0); // 点亮 + vTaskDelay(100 / portTICK_PERIOD_MS); + gpio_set_level(led->pin, 1); // 熄灭 + vTaskDelay(800 / portTICK_PERIOD_MS); // 间隔800ms + break; + + default: + gpio_set_level(led->pin, 1); // 默认关闭 + vTaskDelay(100 / portTICK_PERIOD_MS); + break; + } + } + + // 任务结束前关闭LED(高电平关闭) + gpio_set_level(led->pin, 1); + vTaskDelete(NULL); +} + +/** + * @brief 初始化GPIO引脚为输出模式 + * + * @param led_pin LED使用的GPIO引脚 + */ +static void init_gpio_pin(gpio_num_t led_pin) +{ + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pin_bit_mask = (1ULL << led_pin); + io_conf.pull_down_en = 0; + io_conf.pull_up_en = 0; + gpio_config(&io_conf); + + // 初始状态设为高电平(LED熄灭,适用于低电平点亮的LED) + gpio_set_level(led_pin, 1); +} + +/** + * @brief 初始化状态LED + * + * 配置两个LED引脚为输出模式并设置初始状态 + */ +void status_led_init(void) +{ + init_gpio_pin(STATUS_LED_1_PIN); + init_gpio_pin(STATUS_LED_2_PIN); + + // 启动LED控制任务 + led1.task_running = true; + xTaskCreate(led_control_task, "led1_control", 2048, &led1, 5, &led1.task_handle); + + led2.task_running = true; + xTaskCreate(led_control_task, "led2_control", 2048, &led2, 5, &led2.task_handle); + + ESP_LOGI(TAG, "Status LEDs initialized on pins GPIO%d and GPIO%d", + STATUS_LED_1_PIN, STATUS_LED_2_PIN); +} + +/** + * @brief 控制指定LED的状态 + * + * @param led_num LED编号 (1 或 2) + * @param state LED状态 (0=关闭, 1=开启) + */ +void status_led_set(uint8_t led_num, uint8_t state) +{ + status_led_t *led = NULL; + + if (led_num == 1) { + led = &led1; + } else if (led_num == 2) { + led = &led2; + } else { + ESP_LOGE(TAG, "Invalid LED number: %d", led_num); + return; + } + + if (state == 1) { + led->state = LED_ON; + } else { + led->state = LED_OFF; + } + + ESP_LOGD(TAG, "LED %d set to %s", led_num, state ? "ON" : "OFF"); +} + +/** + * @brief 切换指定LED的状态 + * + * @param led_num LED编号 (1 或 2) + */ +void status_led_toggle(uint8_t led_num) +{ + status_led_t *led = NULL; + + if (led_num == 1) { + led = &led1; + } else if (led_num == 2) { + led = &led2; + } else { + ESP_LOGE(TAG, "Invalid LED number: %d", led_num); + return; + } + + if (led->state == LED_ON) { + led->state = LED_OFF; + } else if (led->state == LED_OFF) { + led->state = LED_ON; + } + + ESP_LOGD(TAG, "LED %d toggled", led_num); +} + +/** + * @brief 设置LED为闪烁模式 + * + * @param led_num LED编号 (1 或 2) + * @param mode 闪烁模式 (0=慢闪, 1=快闪, 2=心跳) + */ +void status_led_blink_mode(uint8_t led_num, uint8_t mode) +{ + status_led_t *led = NULL; + + if (led_num == 1) { + led = &led1; + } else if (led_num == 2) { + led = &led2; + } else { + ESP_LOGE(TAG, "Invalid LED number: %d", led_num); + return; + } + + switch(mode) { + case 0: // 慢闪 + led->state = LED_BLINK_SLOW; + break; + case 1: // 快闪 + led->state = LED_BLINK_FAST; + break; + case 2: // 心跳 + led->state = LED_HEARTBEAT; + break; + default: + ESP_LOGE(TAG, "Invalid blink mode: %d", mode); + return; + } + + ESP_LOGD(TAG, "LED %d set to blink mode %d", led_num, mode); +} + +/** + * @brief 销毁LED控制任务 + * + * @param led_num LED编号 (1 或 2) + */ +void status_led_deinit(uint8_t led_num) +{ + status_led_t *led = NULL; + + if (led_num == 1) { + led = &led1; + } else if (led_num == 2) { + led = &led2; + } else { + ESP_LOGE(TAG, "Invalid LED number: %d", led_num); + return; + } + + if (led->task_running) { + led->task_running = false; + if (led->task_handle) { + vTaskDelete(led->task_handle); + led->task_handle = NULL; + } + gpio_set_level(led->pin, 1); // 关闭LED(高电平关闭) + } +} + +/** + * @brief 获取LED当前状态 + * + * @param led_num LED编号 (1 或 2) + * @return 当前LED状态 + */ +led_state_t status_led_get_state(uint8_t led_num) +{ + if (led_num == 1) { + return led1.state; + } else if (led_num == 2) { + return led2.state; + } else { + ESP_LOGE(TAG, "Invalid LED number: %d", led_num); + return LED_OFF; + } +} diff --git a/components/STATUS_LED/include/STATUS_LED.h b/components/STATUS_LED/include/STATUS_LED.h new file mode 100644 index 0000000..711a77b --- /dev/null +++ b/components/STATUS_LED/include/STATUS_LED.h @@ -0,0 +1,63 @@ +#ifndef STATUS_LED_H +#define STATUS_LED_H + +#include "driver/gpio.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +// LED 状态枚举 +typedef enum { + LED_OFF = 0, + LED_ON = 1, + LED_BLINK_SLOW, + LED_BLINK_FAST, + LED_HEARTBEAT +} led_state_t; + +/** + * @brief 初始化状态LED + * + * 配置两个LED引脚为输出模式并启动控制任务 + */ +void status_led_init(void); + +/** + * @brief 控制指定LED的状态 + * + * @param led_num LED编号 (1 或 2) + * @param state LED状态 (0=关闭, 1=开启) + */ +void status_led_set(uint8_t led_num, uint8_t state); + +/** + * @brief 切换指定LED的状态 + * + * @param led_num LED编号 (1 或 2) + */ +void status_led_toggle(uint8_t led_num); + +/** + * @brief 设置LED为闪烁模式 + * + * @param led_num LED编号 (1 或 2) + * @param mode 闪烁模式 (0=慢闪, 1=快闪, 2=心跳) + */ +void status_led_blink_mode(uint8_t led_num, uint8_t mode); + +/** + * @brief 销毁LED控制任务 + * + * @param led_num LED编号 (1 或 2) + */ +void status_led_deinit(uint8_t led_num); + +/** + * @brief 获取LED当前状态 + * + * @param led_num LED编号 (1 或 2) + * @return 当前LED状态 + */ +led_state_t status_led_get_state(uint8_t led_num); + +#endif // STATUS_LED_H diff --git a/dependencies.lock b/dependencies.lock new file mode 100644 index 0000000..df3c466 --- /dev/null +++ b/dependencies.lock @@ -0,0 +1,21 @@ +dependencies: + espressif/mqtt: + component_hash: ffdad5659706b4dc14bc63f8eb73ef765efa015bf7e9adf71c813d52a2dc9342 + dependencies: + - name: idf + require: private + version: '>=5.3' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.0.0 + idf: + source: + type: idf + version: 5.5.2 +direct_dependencies: +- espressif/mqtt +- idf +manifest_hash: a05aed5660378334599ccf3fa66dcfc9fcf5deb3e651bd572aeb15a43840910d +target: esp32s3 +version: 2.0.0 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100755 index 0000000..f9a21e5 --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "main.c" + INCLUDE_DIRS "." + REQUIRES nvs_flash STATUS_LED RS-485-SP3485EEN ETH_CH390H MQTT_ESP SNTP_ESP + ) diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100644 index 0000000..d4b2985 --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,18 @@ +## IDF Component Manager Manifest File +dependencies: + ## Required IDF version + idf: + version: '>=4.1.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 + + espressif/mqtt: ^1.0.0 diff --git a/main/main.c b/main/main.c new file mode 100755 index 0000000..0ab6825 --- /dev/null +++ b/main/main.c @@ -0,0 +1,81 @@ +#include "RS-485-SP3485EEN.h" +#include "ETH_CH390H.h" +#include "MQTT_ESP.h" +#include "STATUS_LED.h" +#include "MODBUS_ESP.h" +#include "SNTP_ESP.h" + +#define TAG "main" +void app_main(void) +{ + // 1. 初始化LED(最先初始化,让用户知道设备已上电) + status_led_init(); + status_led_blink_mode(1, 1); // LED1 快闪:系统启动中 + status_led_blink_mode(2, 0); // LED2 慢闪:等待初始化 + + ESP_ERROR_CHECK(nvs_flash_init()); + + // 初始化以太网,这里包含了 esp_netif_init();和esp_event_loop_create_default(); + eth_init(); + + // 初始化RS485 + init_specific_rs485_channel(0); // 初始化通道0 + start_rs485_rx_task_for_channel(0, 5, 4096); // 为通道0启动接收任务 + + // 等待网络连接建立 + ESP_LOGI(TAG, "Waiting for network connection..."); + esp_netif_t *eth_netif = esp_netif_get_handle_from_ifkey("ETH_DEF"); // 获取默认以太网接口 + + + // 循环等待直到获得IP地址 + while (true) + { + esp_netif_ip_info_t ip_info; + if (esp_netif_get_ip_info(eth_netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) + { + ESP_LOGI(TAG, "Network connected with IP: " IPSTR, IP2STR(&ip_info.ip)); + break; + } + ESP_LOGI(TAG, "Waiting for IP address..."); + vTaskDelay(pdMS_TO_TICKS(1000)); // 等待1秒后重试 + } + + // 初始化SNTP时间同步服务 + ESP_LOGI(TAG, "Initializing SNTP time synchronization..."); + sntp_esp_init(); + + // 等待时间同步完成(最长等待10秒) + if (sntp_esp_wait_sync(10000)) { + ESP_LOGI(TAG, "Time synchronization completed successfully"); + } else { + ESP_LOGW(TAG, "Time synchronization timeout, using local time"); + } + + ESP_LOGI(TAG, "Starting MQTT client..."); + + // 启动MQTT客户端 + mqtt_app_start(); + + // 网络连接成功后 + status_led_set(1, 1); // LED1 常亮:网络正常 + + // MQTT 启动后 + status_led_blink_mode(2, 2); // LED2 心跳:系统运行正常 + + // ============================ + // 启动设备状态上报任务(每10秒上报一次) + // ============================ + if (mqtt_start_device_status_task(10000) == pdPASS) { + ESP_LOGI(TAG, "Device status report task started"); + } else { + ESP_LOGE(TAG, "Failed to start device status report task"); + } + + ESP_LOGI(TAG, "Waiting for MODBUS poll command via MQTT..."); + + for (;;) + { + vTaskDelay(1000 / portTICK_PERIOD_MS); + } +} +