介绍
创建工程
使用ESPIDF创建S3系列芯片的工程,选择空模板,进行开发
在设置里将FLASH改实际大小,我是16M的FLASH。选择下载方式和端口,编译下载。
测试半双工RS-485收发器
SP3485EEN是一款应用广泛的3.3V半双工RS-485收发器,用于单片机和差分总线(A、B线)间的电平转换。
| 引脚编号 | 引脚名称 | 功能描述 |
|---|---|---|
| 1 | RO (Receiver Output) | 接收器输出。将总线上的差分信号转换为TTL电平,输出给单片机RX。 |
| 2 | /RE (Receiver Enable) | 接收器使能,低电平有效。低电平时使能接收器,高电平时禁用。 |
| 3 | DE (Driver Enable) | 驱动器(发送器)使能,高电平有效。高电平时使能发送器,低电平时禁用。 |
| 4 | DI (Driver Input) | 驱动器输入。将单片机的TX引脚TTL电平输入给芯片。 |
| 5 | GND | 电源地。 |
| 6 | A | 驱动器同相输出 / 接收器同相输入。接RS-485总线的A线。 |
| 7 | B | 驱动器反相输出 / 接收器反相输入。接RS-485总线的B线。 |
| 8 | VCC | 电源引脚,接3.3V电压。 |

硬件连接方式(单引脚控制DE和RE)是UART_MODE_RS485_HALF_DUPLEX模式的标准物理电路。
控制逻辑核心:
发送数据时:将DE和/RE引脚置于发送模式(DE=1, /RE=1)。此时驱动器使能,接收器关闭,单片机从TX经DI发出的数据被转换为差分信号送到A、B总线。
接收数据时:将DE和/RE引脚置于接收模式(DE=0, /RE=0)。此时驱动器关闭,接收器使能,总线A、B上的差分信号经芯片转换为TTL电平,从RO传给单片机的RX。
程序设计
TTL转RS-485
不知为什么,我的另一个485转换芯片总是有问题(怀疑是坏了),所以现在先初始化一个通道使用。
定义头文件
// RS-485-SP3485EEN.h
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#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
// ----------------------------
// 通道数组
// ----------------------------
extern rs485_channel_t rs485_channels[];
#define NUM_CHANNELS (sizeof(rs485_channels) / sizeof(rs485_channels[0]))
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);
编写源文件
#include "RS-485-SP3485EEN.h"
#include <esp_log.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);
}
// 等待 TX 完成(根据字节数计算超时,带一点余量)
uint32_t timeout_ms = 50 + (len * 11 * 1000) / (BAUD_RATE > 0 ? BAUD_RATE : 1);
esp_err_t ret = uart_wait_tx_done(uart_num, pdMS_TO_TICKS(timeout_ms));
if (ret != ESP_OK)
{
ESP_LOGW(TAG, "UART%d wait tx done failed: %d", uart_num, ret);
}
// 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);
uint8_t data[] = "RS485-";
rs485_send(ch->uart_num, data, sizeof(data));
}
// ============================
// 初始化所有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;
}
while (1)
{
int len = rs485_receive(ch->uart_num, buf, BUF_SIZE, 20); // 20ms timeout
if (len > 0)
{
ESP_LOGI(TAG, "%s UART%d RX (%d bytes)", ch->name, ch->uart_num, len);
ESP_LOG_BUFFER_HEX(TAG, buf, len);
//ESP_LOGI(TAG, "UART%d RX: %s", ch->uart_num, buf);
}
else if (len == 0)
{
vTaskDelay(pdMS_TO_TICKS(50)); // 无数据短延时
}
else
{
ESP_LOGW(TAG, "%s UART%d RX error (%d)", ch->name, ch->uart_num, len);
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);
}
}
}主程序调用
#include "RS-485-SP3485EEN.h"
#define TAG "main"
void app_main(void)
{
// 初始化NVS(如果工程中需要)
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
// 初始化RS485
init_specific_rs485_channel(0); // 初始化通道0
start_rs485_rx_task_for_channel(0, 5, 4096); // 为通道0启动接收任务
for (;;)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
测试结果:

所以这个功能是可以用的。
移植espressif/ch390
我的以太网芯片使用的是CH390H-CH390H官方介绍,刚好在乐鑫有组件库,可以试着移植,节省时间。
下载地址:组件下载地址
CH390是一款工业级以太网控制器芯片,集成10/100M以太网介质传输层(MAC)和物理层收发器(PHY),支持CAT3、4、5及CAT5、6线缆连接,兼容10BASE-T和100BASE-TX标准,具备HP Auto-MDIX功能,采用低功耗设计,遵循IEEE 802.3u规范。CH390内置16K字节SRAM,支持3.3V或2.5V并行接口以及SPI串行接口。

将下载的组件copy到自己的组件,然后记得开启SPI-Ethernet模块的支持。

编写头文件
在这个文件包含需要的文件,然后定义使用的引脚
#include <stdio.h>
#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);编写头文件
这个文件负责初始化SPI和使用以太网连接,并且显示以太网连接状态
/*
* 文件: 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"
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, "以太网连接成功");
break;
case ETHERNET_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "以太网断开连接");
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));
}
/* 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));
}注意cmake文件
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
)
不要忘记包含!
测试结果
在app_main中调用eth_init(); // 初始化以太网就可以正常连上。(记得插网线)。
这个是连接结果:
在路由器后台可以看见,设备成功连接:

通过MQTT协议连接自建EMQx
创建自定义组件
使用一下命令,下载使用这个组件,espressif/mqtt: ^1.0.0,在工程中添加MQTT组件,加入mqtt
idf_component_register(SRCS "MQTT_ESP.c"
PRIV_REQUIRES mqtt
INCLUDE_DIRS "include")创建Kconfig文件以便配置连接参数
参考教程:ESP32S3项目配置 和 ESP-IDF Kconfig文件配置
填写属于MQTT配置的Kconfig.projbuild文件
在ESP-IDF的menuconfig配置中,所有显示给用户的字符串都可以替换为中文,而内部的配置变量名和逻辑关键字必须保持英文不变。
menu "MQTT连接配置"
config BROKER_URI
string "MQTT服务器地址"
default "wss://mqtt.eclipseprojects.io:443/mqtt"
help
本示例要连接的MQTT服务器的URL地址。
config BROKER_CERTIFICATE_OVERRIDE
string "服务器证书覆盖"
default ""
help
如果服务器证书已从文本文件加载,请留空;否则请填写PEM格式证书的base64编码部分。
config BROKER_CERTIFICATE_OVERRIDDEN
bool
default y if BROKER_CERTIFICATE_OVERRIDE != ""
endmenu保存,清理构建,重新编译。

然后打开menuconfig就可以看见刚才写的内容:

使用定义的变量
定义好的变量在主目录的sdkconfig中,打开搜索自己的菜单名字(如:MQTT连接配置 )就可快速找到。

然后就可以直接使用这个变量了!!!!
可以丰富一下,添加一些常用的参数:
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编写连接MQTT的程序
在menuconfig填写连接参数之后,就可以开始写程序了。这里我使用1883端口连接我的服务器,这样就不用部署证书了。

这些是源文件的代码:
#include <stdio.h>
#include "MQTT_ESP.h"
static const char *TAG = "mqtt_esp";
/**
* @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);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
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);
// 发布测试消息到主题
msg_id = esp_mqtt_client_publish(client, CONFIG_MQTT_PUB_TOPIC, "I am comming!", 0, 0, 0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
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");
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);
}
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());
esp_mqtt_client_handle_t 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(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
esp_mqtt_client_start(client);
}在头文件声明mqtt_app_start();这个函数,然后在main函数调用,就可以启动这个mqtt连接,并且测试发送,但是,为了避免在还没有连接网络成功的情况下就进行连接,需要在初始化网络连接之后,mqtt启动前,增加对网络连接状态的判断。
// 等待网络连接建立
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秒后重试
}
ESP_LOGI(TAG, "Starting MQTT client...");接着就可以初始化mqtt,这是main的全部代码:
void app_main(void)
{
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秒后重试
}
ESP_LOGI(TAG, "Starting MQTT client...");
// 启动MQTT客户端
mqtt_app_start();
for (;;)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}在终端查看日志,是正常的顺序:

服务器也看见设备上线并且订阅成功:


这样就成功完成了MQTT部分的编写。
将消息发送到服务器
增加可供外部调用的消息发布函数
- 在源文件加上全局的客户端句柄:
// 全局MQTT客户端句柄
static esp_mqtt_client_handle_t g_client = NULL;- 更改启动中的句柄:
/**
* @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;
}- 在main中测试功能,间隔1s发送消息(后续记得删掉):
for (;;)
{
mqtt_publish_message(CONFIG_MQTT_PUB_TOPIC, "Hello from ESP32!", 18, 1, 0);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}- 测试结果

在MQTTx中可以监视到所有上传的消息:

功能正常!!!!!!!!!!!
建立转发任务
把#include "MQTT_ESP.h" // 包含MQTT头文件 在RS-485-SP3485EEN.c中包含。将原有的接收任务进行用MQTT协议转发至服务器
idf_component_register(SRCS "RS-485-SP3485EEN.c"
INCLUDE_DIRS "include"
REQUIRES nvs_flash driver MQTT_ESP mqtt
)
代码改动:
/* 接收任务:参数为通道索引 (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;
}
while (1)
{
int len = rs485_receive(ch->uart_num, buf, BUF_SIZE, 20); // 20ms timeout
if (len > 0)
{
ESP_LOGI(TAG, "%s UART%d RX (%d bytes)", ch->name, ch->uart_num, len);
ESP_LOG_BUFFER_HEX(TAG, buf, len);
// 发布数据到MQTT
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);
} else {
ESP_LOGI(TAG, "Published RS485 data to MQTT topic %s, msg_id: %d", CONFIG_MQTT_PUB_TOPIC, ret);
}
}
else if (len == 0)
{
vTaskDelay(pdMS_TO_TICKS(50)); // 无数据短延时
}
else
{
ESP_LOGW(TAG, "%s UART%d RX error (%d)", ch->name, ch->uart_num, len);
vTaskDelay(pdMS_TO_TICKS(200));
}
}
/* 永久任务通常不会到这里;若退出则释放 */
free(buf);
vTaskDelete(NULL);
}
这样就可以将接收到的数据转发到服务器,这是为后续网关转发功能做基础。


增加状态指示灯
我的灯是低电平点亮,细节不说,看全部代码:
头文件:
#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源文件:
#include <stdio.h>
#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;
}
}在main中使用,用于指示状态(简陋版本,待优化)
void app_main(void)
{
status_led_init();
status_led_blink_mode(1, 1); // LED 1 快闪
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秒后重试
}
status_led_blink_mode(1, 0); // LED 1 慢闪
status_led_blink_mode(2, 1); // LED 2 快闪
ESP_LOGI(TAG, "Starting MQTT client...");
// 启动MQTT客户端
mqtt_app_start();
// 等待MQTT连接建立
ESP_LOGI(TAG, "Waiting for MQTT connection...");
vTaskDelay(pdMS_TO_TICKS(2000)); // 等待2秒,确保MQTT连接建立
status_led_blink_mode(2, 0); // LED 2 慢闪
for (;;)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}==后续需要集成在真这个的状态监测中!==
评论
暂无评论