test: master bcast request

This commit is contained in:
aleks
2026-07-08 15:18:10 +02:00
parent 1af46133fa
commit 624b566c7e
11 changed files with 408 additions and 2 deletions
+15
View File
@@ -0,0 +1,15 @@
# This is the project CMakeLists.txt file for the test subproject
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(EXTRA_COMPONENT_DIRS "../test_common")
# The workaround for the test_utils under ESP-IDF v6.0
if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER "5.5")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/test_apps/components")
else()
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/unit-test-app/components")
endif()
project(test_broadcast)
+4
View File
@@ -0,0 +1,4 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-S2 | ESP32-S3 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- |
This test app is used to test modbus interface feature.
@@ -0,0 +1,18 @@
set(PROJECT "test_bcast")
set(srcs "test_app_main.c"
"${PROJECT}_serial.c"
)
idf_component_register(SRCS ${srcs}
PRIV_REQUIRES test_utils test_common unity
)
# The workaround for WHOLE_ARCHIVE which is absent in v4.4
set_property(TARGET ${COMPONENT_LIB} APPEND PROPERTY INTERFACE_LINK_LIBRARIES "-u mb_test_include_bcast_serial")
# Workaround to avoid static analysis false positives for some components.
if(CONFIG_FMB_COMPILER_STATIC_ANALYZER_ENABLE AND CMAKE_C_COMPILER_ID STREQUAL "GNU")
target_compile_options(${COMPONENT_LIB} PRIVATE "-fanalyzer")
message(STATUS "Static analyzer build for ${PROJECT_NAME}.")
endif()
@@ -0,0 +1,45 @@
menu "Modbus Test Configuration"
config MB_PORT_ADAPTER_EN
bool "Enable Modbus port adapter to substitute hardware layer for test."
default n
help
When option is enabled the port communication layer is substituted by
port adapter layer to allow testing of higher layers without access to physical layer.
config MB_TEST_SLAVE_TASK_PRIO
int "Modbus master test task priority"
range 4 23
default 4
help
Modbus master task priority for the test.
config MB_TEST_MASTER_TASK_PRIO
int "Modbus slave test task priority"
range 4 23
default 4
help
Modbus slave task priority for the test.
config MB_TEST_COMM_CYCLE_COUNTER
int "Modbus test communication cycle counter"
range 10 1000
default 10
help
Modbus communication cycle counter for test.
config MB_TEST_LEAK_WARN_LEVEL
int "Modbus test leak warning level"
range 4 256
default 32
help
Modbus test leak warning level.
config MB_TEST_LEAK_CRITICAL_LEVEL
int "Modbus test leak critical level"
range 4 1024
default 64
help
Modbus test leak critical level.
endmenu
@@ -0,0 +1,5 @@
dependencies:
idf: ">=5.0"
espressif/esp-modbus:
version: "^2"
override_path: "../../../"
@@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include "unity.h"
#include "test_common.h"
void app_main(void)
{
#if !CONFIG_LOG_DEFAULT_LEVEL_DEBUG
esp_log_level_set("mbc_tcp.slave", ESP_LOG_DEBUG);
esp_log_level_set("mbc_serial.slave", ESP_LOG_DEBUG);
esp_log_level_set("mb_object.slave", ESP_LOG_DEBUG);
#else
// Disable VFS logs as they are too verbose
esp_log_level_set("vfs_calls", ESP_LOG_NONE);
#endif
printf("Modbus RS485 multi-device test cases/n");
unity_run_menu();
}
@@ -0,0 +1,210 @@
/*
* SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include "unity.h"
#include "sdkconfig.h"
#include "test_common.h"
#include "test_utils.h"
#if __has_include("unity_test_utils.h")
// unity test utils are used
#include "unity_test_utils.h"
#else
// Unit_test_app utils from test_utils ("test_utils.h"), v4.4
#define unity_utils_task_delete test_utils_task_delete
#endif
#define TEST_SER_PORT_NUM (1)
#define TEST_TASK_TIMEOUT_MS (160000)
#define TEST_SEND_TOUT_US (30000)
#define TEST_RESP_TOUT_MS (1000)
#define TEST_BAUD_RATE (115200)
#if CONFIG_IDF_TARGET_ESP32
#define TEST_SER_PIN_RX (22)
#define TEST_SER_PIN_TX (23)
// RTS for RS485 Half-Duplex Mode manages DE/~RE
#define TEST_SER_PIN_RTS (18)
#else
#define TEST_SER_PIN_RX (4)
#define TEST_SER_PIN_TX (5)
#define TEST_SER_PIN_RTS (10)
#endif
#define TEST_MASTER_RESPOND_TOUT_MS (CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND)
// The workaround to statically link the whole test library
__attribute__((unused)) bool mb_test_include_bcast_serial = true;
#define TAG "MODBUS_SERIAL_COMM_TEST"
#if (CONFIG_FMB_COMM_MODE_RTU_EN || CONFIG_FMB_COMM_MODE_ASCII_EN)
#define MB_DEVICE_ADDR0 0
// Example Data (Object) Dictionary for Modbus parameters
static const mb_parameter_descriptor_t descriptors[] = {
{
CID_DEV_REG0, STR("MB_hold_reg-0"), STR("Data"), MB_DEVICE_ADDR0, MB_PARAM_HOLDING, 0, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER
},
{
CID_DEV_REG1, STR("MB_hold_reg-1"), STR("Data"), MB_DEVICE_ADDR0, MB_PARAM_HOLDING, 1, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER
},
{
CID_DEV_REG2, STR("MB_hold_reg-2"), STR("Data"), MB_DEVICE_ADDR0, MB_PARAM_HOLDING, 2, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER
},
{
CID_DEV_REG3, STR("MB_hold_reg-3"), STR("Data"), MB_DEVICE_ADDR0, MB_PARAM_HOLDING, 3, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER
},
{
CID_DEV_REG_COUNT, STR("CYCLE_COUNTER"), STR("Data"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 4, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 0, 0), PAR_PERMS_READ_WRITE_TRIGGER
}
};
// The number of parameters in the table
const uint16_t num_descriptors = (sizeof(descriptors) / sizeof(descriptors[0]));
static void test_modbus_rs485_rtu_slave(void)
{
mb_communication_info_t slave_config1 = {
.ser_opts.port = TEST_SER_PORT_NUM,
.ser_opts.mode = MB_RTU,
.ser_opts.uid = MB_DEVICE_ADDR1,
.ser_opts.data_bits = UART_DATA_8_BITS,
.ser_opts.stop_bits = UART_STOP_BITS_1,
.ser_opts.baudrate = TEST_BAUD_RATE,
.ser_opts.parity = UART_PARITY_DISABLE,
.ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS,
.ser_opts.test_tout_us = TEST_SEND_TOUT_US
};
TaskHandle_t slave_task_handle = test_common_slave_serial_create(&slave_config1, 0);
// Set driver mode to Half Duplex
TEST_ESP_OK(uart_set_mode(slave_config1.ser_opts.port, UART_MODE_RS485_HALF_DUPLEX));
TEST_ESP_OK(uart_set_pin(slave_config1.ser_opts.port, TEST_SER_PIN_TX,
TEST_SER_PIN_RX, TEST_SER_PIN_RTS, UART_PIN_NO_CHANGE));
ESP_LOGI(TAG, "Slave RTU is started. (%s).", __func__);
unity_send_signal("Slave_ready");
unity_wait_for_signal("Master_started");
test_common_task_start(slave_task_handle, 1);
TEST_ASSERT_TRUE(test_common_task_wait_done(slave_task_handle, pdMS_TO_TICKS(TEST_TASK_TIMEOUT_MS)));
}
static void test_modbus_rs485_rtu_master(void)
{
ESP_LOGI(TAG, "Master RTU is started (%s).", __func__);
unity_wait_for_signal("Slave_ready");
unity_send_signal("Master_started");
// Initialize and start Modbus controller
mb_communication_info_t master_config = {
.ser_opts.port = TEST_SER_PORT_NUM,
.ser_opts.mode = MB_RTU,
.ser_opts.data_bits = UART_DATA_8_BITS,
.ser_opts.stop_bits = UART_STOP_BITS_1,
.ser_opts.baudrate = TEST_BAUD_RATE,
.ser_opts.parity = UART_PARITY_DISABLE,
.ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS,
.ser_opts.test_tout_us = TEST_SEND_TOUT_US
};
TaskHandle_t master_task_handle = test_common_master_serial_create(&master_config, 0, &descriptors[0], num_descriptors);
// Set driver mode to Half Duplex
TEST_ESP_OK(uart_set_mode(master_config.ser_opts.port, UART_MODE_RS485_HALF_DUPLEX));
TEST_ESP_OK(uart_set_pin(master_config.ser_opts.port, TEST_SER_PIN_TX,
TEST_SER_PIN_RX, TEST_SER_PIN_RTS, UART_PIN_NO_CHANGE));
void *handle = test_common_task_get_instance(master_task_handle);
uint16_t reg = 0;
uint8_t type = 0;
esp_err_t err = mbc_master_get_parameter(handle, CID_DEV_REG0, (uint8_t *)&reg, &type);
TEST_ESP_ERR(ESP_ERR_NOT_SUPPORTED, err); // Broadcast read request is not supported
test_common_task_start(master_task_handle, 1);
TEST_ASSERT_TRUE(test_common_task_wait_done(master_task_handle, pdMS_TO_TICKS(TEST_TASK_TIMEOUT_MS)));
}
/*
* Modbus RS485 RTU multi device test case
*/
TEST_CASE_MULTIPLE_DEVICES("Modbus RS485 RTU multi device broadcast case.", "[modbus][test_env=multi_dut_modbus_serial]", test_modbus_rs485_rtu_slave, test_modbus_rs485_rtu_master);
static void test_modbus_rs485_ascii_slave(void)
{
mb_communication_info_t slave_config1 = {
.ser_opts.port = TEST_SER_PORT_NUM,
.ser_opts.mode = MB_ASCII,
.ser_opts.uid = MB_DEVICE_ADDR1,
.ser_opts.data_bits = UART_DATA_8_BITS,
.ser_opts.stop_bits = UART_STOP_BITS_1,
.ser_opts.baudrate = TEST_BAUD_RATE,
.ser_opts.parity = UART_PARITY_DISABLE,
.ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS,
.ser_opts.test_tout_us = TEST_SEND_TOUT_US
};
TaskHandle_t slave_task_handle = test_common_slave_serial_create(&slave_config1, 0);
TEST_ESP_OK(uart_set_pin(slave_config1.ser_opts.port, TEST_SER_PIN_TX,
TEST_SER_PIN_RX, TEST_SER_PIN_RTS, UART_PIN_NO_CHANGE));
// Set driver mode to Half Duplex
TEST_ESP_OK(uart_set_mode(slave_config1.ser_opts.port, UART_MODE_RS485_HALF_DUPLEX));
ESP_LOGI(TAG, "Slave ASCII is started. (%s).", __func__);
unity_send_signal("Slave_ready");
unity_wait_for_signal("Master_started");
test_common_task_start(slave_task_handle, 1);
TEST_ASSERT_TRUE(test_common_task_wait_done(slave_task_handle, pdMS_TO_TICKS(TEST_TASK_TIMEOUT_MS)));
};
static void test_modbus_rs485_ascii_master(void)
{
ESP_LOGI(TAG, "Master ASCII is started (%s).", __func__);
unity_wait_for_signal("Slave_ready");
// Initialize and start Modbus controller
mb_communication_info_t master_config = {
.ser_opts.port = TEST_SER_PORT_NUM,
.ser_opts.mode = MB_ASCII,
.ser_opts.data_bits = UART_DATA_8_BITS,
.ser_opts.stop_bits = UART_STOP_BITS_1,
.ser_opts.baudrate = TEST_BAUD_RATE,
.ser_opts.parity = UART_PARITY_DISABLE,
.ser_opts.response_tout_ms = TEST_MASTER_RESPOND_TOUT_MS,
.ser_opts.test_tout_us = TEST_SEND_TOUT_US
};
TaskHandle_t master_task_handle = test_common_master_serial_create(&master_config, 0, &descriptors[0], num_descriptors);
// Set driver mode to Half Duplex
TEST_ESP_OK(uart_set_mode(master_config.ser_opts.port, UART_MODE_RS485_HALF_DUPLEX));
TEST_ESP_OK(uart_set_pin(master_config.ser_opts.port, TEST_SER_PIN_TX,
TEST_SER_PIN_RX, TEST_SER_PIN_RTS, UART_PIN_NO_CHANGE));
unity_send_signal("Master_started");
test_common_task_start(master_task_handle, 1);
TEST_ASSERT_TRUE(test_common_task_wait_done(master_task_handle, pdMS_TO_TICKS(TEST_TASK_TIMEOUT_MS)));
}
/*
* Modbus RS485 ASCII multi device test case
*/
TEST_CASE_MULTIPLE_DEVICES("Modbus RS485 ASCII multi device broadcast case.", "[modbus][test_env=multi_dut_modbus_serial]", test_modbus_rs485_ascii_slave, test_modbus_rs485_ascii_master);
#endif
@@ -0,0 +1,17 @@
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import pytest
@pytest.mark.parametrize("target", ["esp32"], indirect=True)
@pytest.mark.parametrize("count, config", [(2, "serial")], indirect=True)
@pytest.mark.multi_dut_modbus_serial
def test_modbus_comm_bcast_serial(case_tester) -> None: # type: ignore
for case in case_tester.test_menu:
if (
case.attributes.get("test_env", "multi_dut_modbus_serial")
== "multi_dut_modbus_serial"
):
print(f"Test case: {case.name}")
case_tester.run_multi_dev_case(case=case, reset=True)
@@ -0,0 +1,25 @@
#
# Default configuration options
#
CONFIG_FMB_TIMER_USE_ISR_DISPATCH_METHOD=y
CONFIG_FMB_MASTER_DELAY_MS_CONVERT=300
CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND=2000
CONFIG_FMB_PORT_TASK_STACK_SIZE=4096
CONFIG_FMB_PORT_TASK_PRIO=10
# Avoid CI issues "Warning: The smallest app partition is nearly full (5% free space left)!"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_MB_PORT_ADAPTER_EN=n
CONFIG_MB_TEST_SLAVE_TASK_PRIO=5
CONFIG_MB_TEST_MASTER_TASK_PRIO=5
CONFIG_MB_TEST_COMM_CYCLE_COUNTER=30
CONFIG_ESP_TASK_WDT_EN=n
CONFIG_ESP_TASK_WDT_INIT=n
# Enable debug logging
CONFIG_LOG_DEFAULT_LEVEL_DEBUG=n
CONFIG_LOG_MAXIMUM_LEVEL_DEBUG=y
CONFIG_MB_CONSOLE_HELPER_ENABLED=y
@@ -0,0 +1,15 @@
#
# Serial-specific configuration
#
CONFIG_MB_TEST_SLAVE_TASK_PRIO=5
CONFIG_MB_TEST_MASTER_TASK_PRIO=5
CONFIG_FMB_COMM_MODE_RTU_EN=y
CONFIG_FMB_COMM_MODE_ASCII_EN=y
CONFIG_FMB_COMM_MODE_TCP_EN=n
CONFIG_FMB_TCP_UID_ENABLED=n
CONFIG_MB_TEST_COMM_CYCLE_COUNTER=10
CONFIG_MB_PORT_ADAPTER_EN=n
CONFIG_MB_TEST_LEAK_CRITICAL_LEVEL=128
CONFIG_MB_TEST_LEAK_WARN_LEVEL=128
CONFIG_UNITY_ENABLE_FIXTURE=y
+32 -2
View File
@@ -7,6 +7,7 @@
#include "freertos/portmacro.h"
#include "freertos/queue.h"
#include "esp_modbus_common.h"
#include "port_adapter.h"
#include "mb_common.h"
#include "mbc_slave.h"
@@ -139,6 +140,27 @@ void test_task_add_entry(TaskHandle_t task_handle, void *inst)
xSemaphoreTake(new_entry->task_sema_handle, 1);
}
static void *test_task_find_object(bool is_master)
{
task_entry_t *it, *pfound = NULL;
if (LIST_EMPTY(&s_task_list)) {
return NULL;
}
portENTER_CRITICAL(&s_list_spinlock);
LIST_FOREACH(it, &s_task_list, entries) {
if (it) {
mb_controller_common_t *pobj = (mb_controller_common_t *)it->inst_handle;
if (pobj && pobj->mb_base->descr.is_master == is_master) {
pfound = (void *)pobj;
break;
}
}
}
portEXIT_CRITICAL(&s_list_spinlock);
return pfound;
}
static task_entry_t *test_task_find_entry(TaskHandle_t task_handle)
{
TEST_ASSERT_NOT_NULL(task_handle);
@@ -438,8 +460,16 @@ esp_err_t test_common_read_modbus_parameter(void *handle, uint16_t cid, uint16_t
uint8_t type = 0;
if (param_descriptor->mb_slave_addr) {
err = mbc_master_get_parameter(handle, cid, (uint8_t *)par_data, &type);
} else { // if the address is broadcast, try to read from first slave intentionally
err = mbc_master_get_parameter_with(handle, cid, param_descriptor->mb_slave_addr + 1, (uint8_t *)par_data, &type);
} else {
// if the address is broadcast, try to read from first registered slave intentionally
void *pobj = test_task_find_object(false);
uint8_t uid = 0x01;
if (pobj) {
mbs_controller_iface_t *pctrl_obj = ((mbs_controller_iface_t *)pobj);
uid = pctrl_obj->opts.comm_opts.common_opts.uid;
}
ESP_LOGD(TAG, "Use slave object: %p, UID:%u", pobj, uid);
err = mbc_master_get_parameter_with(handle, cid, uid, (uint8_t *)par_data, &type);
}
if (err == ESP_OK) {
ESP_LOGI(TAG, "%p, CHAR #%u %s (%s) value = (0x%04x) parameter read successful.",