目录
- 一、前言
- 二、接收时的状态检测
- [三、w800_get_status 实现](#三、w800_get_status 实现)
- 四、发送时的状态检测
- [五、C 语言小技巧:块作用域](#五、C 语言小技巧:块作用域)
- 六、断线重连机制
- 七、完整任务函数
- 八、总结
- 九、结尾
一、前言
大家好,这里是 Hello_Embed。上篇修复了首次连接超时的竞态问题,本篇继续完善整个通信流程的健壮性。
实测中会遇到两类典型场景:
- 上位机主动断开连接:此时开发板的收/发接口会持续返回错误,若不处理,整个 Modbus 任务就会卡死。
- 网络波动导致丢包 :若
recvfrom无限等待信号量,遇到断线就会永久阻塞。
本篇要解决的就是这两个问题------在收发接口 里增加 socket 状态检测,并在 Modbus 主循环里增加断线自动重连逻辑。
二、接收时的状态检测
原来 recvfrom 的逻辑是:先尝试从队列里读数据,读不到就等信号量。问题在于,如果 socket 已经断开,信号量永远不会被触发,函数就会无限阻塞。
原始逻辑(仅读数据,无状态检测):
c
/* 先读取数据 */
for (i = 0; i < len; i++)
{
if (pdPASS != xQueueReceive(recv_queue, &buf[i], 0))
break;
}
/* 读取到数据的话设置 from */
if (i)
{
if (from)
*from = ptDev->sockets[socket].remote;
if (fromlen)
*fromlen = sizeof(*from);
}
改进:当队列为空时,不无限等待,而是定期检查 socket 状态:
c
if (i == 0)
{
/* 不要无限等待
* 每隔 1000ms 检查一次 socket 状态
* 如果 socket 已断开,立即返回错误
*/
while (pdPASS != xSemaphoreTake(ptDev->sockets[socket].at_packet_sem, 1000))
{
status = w800_get_status(socket);
if (status != 2)
{
closesocket(socket);
return -1;
}
}
}
解析 :
xSemaphoreTake设置了 1000ms 超时,而不是portMAX_DELAY。每次超时未拿到信号量,就主动查询一次 socket 状态(通过 AT 命令)。若状态不是2(已连接),则关闭 socket 并返回-1,让上层感知到连接已断开。
三、w800_get_status 实现
w800_get_status 是本篇新增的辅助函数,功能是向 W800 发送 AT+SKSTT 命令查询指定 socket 的连接状态。
c
static int w800_get_status(int socket)
{
int8_t buf[100];
int err;
PAT_Device ptDev = get_netdev();
uint32_t local_port, remote_port;
char remote_ip[20];
uint32_t resp_len;
int status;
int hw_socket;
int new_socket;
int rx_data;
int i;
PAT_Socket ptSocket;
struct sockaddr_in *sin;
/* 构造 AT 命令(查询 socket 状态) */
sprintf((char *)buf, "AT+SKSTT=%d\r", (int)ptDev->sockets[socket].user_data);
/* 执行 AT 命令,等待响应 */
err = at_exec_cmd(ptDev, (int8_t *)buf, (uint8_t *)buf, sizeof(buf), &resp_len, AT_TIMEOUT);
if (err)
return -1;
/* 返回的数据格式:
* +OK=<socket>,<status>,[host],[HostPort],[LocalPort],[rx_data]<CR><LF>
*
* status: 0 - 断开, 1 - 监听, 2 - 连接
* host : 对端 IP
* HostPort : 对端端口
* LocalPort: 本地端口
* rx_data: 接收 buffer 中数据长度
*/
sscanf((const char *)buf, "+OK=%d,%d,\"%[^\"]\",%d,%d,%d",
&hw_socket, &status, remote_ip, &remote_port, &local_port, &rx_data);
return status;
}
解析 :通过
sscanf解析+OK=响应,提取第二个字段status。返回值含义:0= 断开,1= 监听,2= 已连接。只要返回值不是2,就认为连接异常。
四、发送时的状态检测
同样的状态检测逻辑也要加到 sendto 中------在数据发送完毕后,立即检查 socket 是否仍然在线:
c
int w800_sendto(int socket, const void *data, size_t size, int flags,
const struct sockaddr *to, socklen_t tolen)
{
PAT_Device ptDev = get_netdev();
struct sockaddr_in *old_dest = (struct sockaddr_in *)&ptDev->sockets[socket].remote;
struct sockaddr_in *new_dest = (struct sockaddr_in *)to;
int err;
int8_t buf[100];
uint32_t resp_len;
/* 对于 TCP 连接,参数 to 没有意义,必须为 NULL */
if (ptDev->sockets[socket].type == SOCK_STREAM && to)
return -1;
/* 对于 UDP,必须指定参数 to */
if (ptDev->sockets[socket].type == SOCK_DGRAM && !to)
return -1;
/* 对于 UDP,可能会多次调用 sendto 给不同目标发送数据
* 目标发生变化时,需要先用 AT 命令重新创建 socket
*/
if (ptDev->sockets[socket].type == SOCK_DGRAM)
{
if ((ptDev->sockets[socket].user_data == NULL) ||
(old_dest->sin_addr.s_addr != new_dest->sin_addr.s_addr ||
old_dest->sin_port != new_dest->sin_port))
{
if (ptDev->sockets[socket].user_data)
w800_closesocket(socket);
err = w800_connect(socket, to, tolen);
if (err)
return -1;
}
}
/* 发送数据:
* 先发命令:AT+SKSND=<socket>,<size><CR>
* 再发送 size 个字节的数据
*/
sprintf((char *)buf, "AT+SKSND=%d,%d\r", (int)ptDev->sockets[socket].user_data, size);
err = at_exec_cmd(ptDev, (int8_t *)buf, NULL, 0, NULL, AT_TIMEOUT);
if (err)
return -1;
err = at_send_datas(ptDev, (uint8_t *)data, size, AT_TIMEOUT);
if (err)
return -1;
/* 发送完毕后检查 socket 状态 */
{
int status = w800_get_status(socket);
if (status != 2)
{
closesocket(socket);
return -1;
}
}
return size;
}
解析 :发送成功后不能就此"高枕无忧"------对端可能在我们发完的瞬间断开了连接。通过
w800_get_status进行一次确认,若状态异常则主动关闭并返回错误,避免上层误以为发送成功。
五、C 语言小技巧:块作用域
上面代码里用了一个不太常见的写法------单独的花括号 {} 块:
c
/* 写法一:使用块限制变量作用域 */
{
int status = w800_get_status(socket);
if (status != 2) { closesocket(socket); return -1; }
}
/* 写法二:直接声明变量 */
int status = w800_get_status(socket);
if (status != 2) { closesocket(socket); return -1; }
解析 :两种写法功能完全相同,但写法一中
status的生命周期被限制在{}内,不会污染外部作用域。在大函数中,这个小技巧能有效减少变量名冲突,让代码意图更清晰。
六、断线重连机制
有了收/发时的状态检测,上层 Modbus 任务就能通过返回值感知到断线事件。modbus_receive 返回负值时,说明 socket 已经出错,此时的处理逻辑如下:
c
if (rc < 0)
{
/* Socket 出错,等待重连 */
Draw_String(0, 80, "wait re-connect ...", 0xff0000, 0);
while (1)
{
socket_client = modbus_tcp_accept(ctx, &socket_server);
if (socket_client >= 0)
break;
}
Draw_String(0, 96, "Modbus client re-connected", 0xff0000, 0);
continue;
}
解析 :这里不重新调用
modbus_tcp_listen,是因为socket_server(监听 socket)始终没有被关闭,仍在监听端口。只需重新调用modbus_tcp_accept等待新的客户端连入即可。整个循环是非阻塞友好的------只要上位机重新连接,就能立刻恢复通信。
七、完整任务函数
以下是加入断线重连后的完整 LibmodbusServerTask,供参考:
c
void LibmodbusServerTask(void *pvParameters)
{
uint8_t *query;
modbus_t *ctx;
int rc;
modbus_mapping_t *mb_mapping;
PChannelInfo ptChannelInfo;
int channel;
int err;
int header_length;
int socket_server, socket_client;
/* 显示当前运行模式 */
if (isBootloader())
Draw_String(150, 0, "Bootloader", 0xff0000, 0);
else
Draw_String(150, 0, "Application", 0xff0000, 0);
/* 初始化 AT 层 */
at_init("uart1");
/* 连接 WiFi 热点,失败则 1s 后重试 */
while (1)
{
err = at_connect_ap("Programmers", "100asktech");
if (!err)
break;
vTaskDelay(1000);
}
/* 创建 Modbus TCP 上下文 */
#if 0
ctx = modbus_new_st_rtu("usb", 115200, 'N', 8, 1);
modbus_set_slave(ctx, 1);
query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);
#else
ctx = modbus_new_tcp(NULL, 1502);
query = pvPortMalloc(MODBUS_TCP_MAX_ADU_LENGTH);
#endif
header_length = modbus_get_header_length(ctx);
/* 分配寄存器映射表 */
mb_mapping = modbus_mapping_new_start_address(0, 500, 0, 500, 0, 500, 0, 500);
memset(mb_mapping->tab_bits, 0, mb_mapping->nb_bits);
memset(mb_mapping->tab_registers, 0, mb_mapping->nb_registers * 2);
/* 开始监听 */
Draw_String(0, 48, "Waiting connect ...", 0xff0000, 0);
while (1)
{
socket_server = modbus_tcp_listen(ctx, 1);
if (socket_server >= 0)
break;
}
/* 等待第一次客户端连接 */
while (1)
{
socket_client = modbus_tcp_accept(ctx, &socket_server);
if (socket_client >= 0)
break;
}
Draw_String(0, 64, "Modbus client connected", 0xff0000, 0);
/* 初始化通道信息,创建业务任务 */
for (channel = 1; channel < MAX_CHANNEL_NUMBER; channel++)
{
ptChannelInfo = &g_tChannelInfos[channel];
ptChannelInfo->mb_mapping = mb_mapping;
ptChannelInfo->xMutex = xSemaphoreCreateMutex();
}
if (!isBootloader())
{
xTaskCreate(CH0_Task, "CH0_Task", 400, mb_mapping, osPriorityNormal, NULL);
for (channel = 1; channel < MAX_CHANNEL_NUMBER; channel++)
{
ptChannelInfo = &g_tChannelInfos[channel];
xTaskCreate(CHn_Task, ptChannelInfo->uart_name, 400,
ptChannelInfo, osPriorityNormal, NULL);
}
}
/* 主循环:接收请求 → 处理 → 回复 */
for (;;)
{
do {
rc = modbus_receive(ctx, query);
/* 过滤查询返回 0,继续等待 */
} while (rc == 0);
if (rc < 0)
{
/* Socket 出错,等待重连 */
Draw_String(0, 80, "wait re-connect ...", 0xff0000, 0);
while (1)
{
socket_client = modbus_tcp_accept(ctx, &socket_server);
if (socket_client >= 0)
break;
}
Draw_String(0, 96, "Modbus client re-connected", 0xff0000, 0);
continue;
}
/* 处理紧急命令 */
err = process_emergency_cmd(ctx, &query[header_length - 1], rc, mb_mapping);
if (err)
{
modbus_reply_exception(ctx, query, MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY);
continue;
}
/* 处理文件记录 */
err = process_file_record(&query[header_length - 1], rc);
if (err)
{
modbus_reply_exception(ctx, query, MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY);
continue;
}
/* 正常回复 */
rc = modbus_reply(ctx, query, rc, mb_mapping);
}
/* 清理资源(正常情况下不会执行到这里) */
modbus_mapping_free(mb_mapping);
vPortFree(query);
modbus_close(ctx);
modbus_free(ctx);
vTaskDelete(NULL);
}
八、总结
| 改进点 | 位置 | 方案 |
|---|---|---|
| 接收阻塞问题 | w800_recvfrom |
信号量等待改为带超时的轮询,超时后检查 socket 状态 |
| 发送后状态确认 | w800_sendto |
发送完毕后主动调用 w800_get_status 验证连接 |
| 断线重连 | LibmodbusServerTask |
rc < 0 时重新调用 modbus_tcp_accept 等待新连接 |
经过本篇的改进,系统已经能够:
- 在上位机主动断开时,开发板感知到断线并自动进入重连等待
- 在网络波动时,接收接口不会无限阻塞,而是定期"自检"
九、结尾
本篇完成了收发接口的 socket 状态检测与断线自动重连机制。至此,整个 Modbus TCP 通信已经具备了基本的容错能力。
下一篇将继续改进程序,实现写文件功能,敬请期待~
Hello_Embed 继续带你从原理到实践,掌握嵌入式上位机开发的核心技能,敬请关注~