云计算百科
云计算领域专业知识百科平台

FreeRTOS + STM32 + LwIP 超时网络 I/O 完整示例

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

下面是一个完整的、可直接参考移植的实现。核心思路:LwIP socket 默认是阻塞的,配合 SO_RCVTIMEO/SO_SNDTIMEO 或非阻塞 + select/poll 实现超时。这里给出基于 select() 的可移植方案(LwIP 支持 select),并处理 EINTR、部分收发等边界。


1. 头文件与配置

/* net_io_task.h */
#ifndef NET_IO_TASK_H
#define NET_IO_TASK_H

#include "FreeRTOS.h"
#include "task.h"
#include "lwip/sockets.h"
#include "lwip/inet.h"
#include "lwip/errno.h"

/* 任务配置 */
#define NET_TASK_STACK_SIZE (512 * 4) /* 字节,视 LwIP 配置放大 */
#define NET_TASK_PRIO (tskIDLE_PRIORITY + 3)
#define NET_SERVER_PORT 8080
#define NET_CONNECT_TIMEOUT_MS 5000
#define NET_RECV_TIMEOUT_MS 3000
#define NET_SEND_TIMEOUT_MS 3000
#define NET_RECV_BUF_LEN 512

void net_io_task(void *arg);

#endif


2. 核心工具函数:带超时的 send / recv

/* net_io_util.c */
#include "net_io_task.h"
#include <string.h>

/* 返回 >=0: 实际发送字节数; -1: 错误(含超时) */
static int sock_send_timeout(int s, const void *buf, size_t len, uint32_t timeout_ms)
{
const uint8_t *p = (const uint8_t *)buf;
size_t sent = 0;
uint32_t start = xTaskGetTickCount();

while (sent < len) {
fd_set wfds;
FD_ZERO(&wfds);
FD_SET(s, &wfds);

/* 计算剩余时间 */
uint32_t elapsed = (xTaskGetTickCount() – start) * portTICK_PERIOD_MS;
if (elapsed >= timeout_ms) {
errno = EWOULDBLOCK;
return -1;
}
struct timeval tv;
uint32_t remain_ms = timeout_ms – elapsed;
tv.tv_sec = remain_ms / 1000;
tv.tv_usec = (remain_ms % 1000) * 1000;

int rc = lwip_select(s + 1, NULL, &wfds, NULL, &tv);
if (rc < 0) {
if (errno == EINTR) continue; /* 被信号/其它中断打断,重试 */
return -1;
}
if (rc == 0) { /* 超时 */
errno = EWOULDBLOCK;
return -1;
}

ssize_t n = lwip_send(s, p + sent, len – sent, 0 /* 或 MSG_DONTWAIT */);
if (n < 0) {
if (errno == EINTR) continue;
if (errno == EWOULDBLOCK || errno == EAGAIN) continue; /* 再等 */
return -1;
}
sent += (size_t)n;
}
return (int)sent;
}

/* 返回 >0: 收到字节数; 0: 对端关闭; -1: 错误(含超时) */
static int sock_recv_timeout(int s, void *buf, size_t len, uint32_t timeout_ms)
{
uint32_t start = xTaskGetTickCount();

for (;;) {
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(s, &rfds);

uint32_t elapsed = (xTaskGetTickCount() – start) * portTICK_PERIOD_MS;
if (elapsed >= timeout_ms) { errno = EWOULDBLOCK; return -1; }

struct timeval tv;
uint32_t remain_ms = timeout_ms – elapsed;
tv.tv_sec = remain_ms / 1000;
tv.tv_usec = (remain_ms % 1000) * 1000;

int rc = lwip_select(s + 1, &rfds, NULL, NULL, &tv);
if (rc < 0) {
if (errno == EINTR) continue;
return -1;
}
if (rc == 0) { errno = EWOULDBLOCK; return -1; }

ssize_t n = lwip_recv(s, buf, len, 0);
if (n < 0) {
if (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)
continue;
return -1;
}
return (int)n; /* n==0 表示对端关闭 */
}
}

为什么要用 select 而不是单纯 SO_RCVTIMEO?
LwIP 的 SO_RCVTIMEO/SO_SNDTIMEO 在不同移植版本里行为不完全一致(部分版本 setsockopt 后仅对后续调用生效、且不同 lwipopts 下支持不同)。select 是 LwIP 标准支持、行为最稳的路径,且能同时统一管理收发超时。若你的 LwIP 版本 SO_RCVTIMEO 完整可用,也可用,见文末说明。


3. 带超时的 connect

/* 返回 0 成功, -1 失败/超时 */
static int sock_connect_timeout(const char *ip, uint16_t port, uint32_t timeout_ms)
{
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) return -1;

/* 先设为非阻塞以支持 connect 超时 */
int flags = lwip_fcntl(s, F_GETFL, 0);
lwip_fcntl(s, F_SETFL, flags | O_NONBLOCK);

struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);

int rc = lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr));
if (rc < 0 && errno != EINPROGRESS) {
lwip_close(s);
return -1;
}

if (rc < 0) { /* EINPROGRESS: 等待可写 */
fd_set wfds, efds;
FD_ZERO(&wfds); FD_ZERO(&efds);
FD_SET(s, &wfds); FD_SET(s, &efds);

struct timeval tv;
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;

rc = lwip_select(s + 1, NULL, &wfds, &efds, &tv);
if (rc <= 0) { /* 超时或错误 */
lwip_close(s);
return -1;
}
/* 检查 connect 是否真的成功 */
int soerr = 0;
socklen_t slen = sizeof(soerr);
lwip_getsockopt(s, SOL_SOCKET, SO_ERROR, &soerr, &slen);
if (soerr != 0) {
lwip_close(s);
return -1;
}
}

/* 恢复阻塞模式,便于后续 recv/send 语义清晰 */
lwip_fcntl(s, F_SETFL, flags);
return s;
}


4. 完整任务示例

/* net_io_task.c */
#include "net_io_task.h"
#include <string.h>
#include <stdio.h>

static const char *kServerIP = "192.168.1.100";

void net_io_task(void *arg)
{
(void)arg;
uint8_t rxbuf[NET_RECV_BUF_LEN];

for (;;) {
/* —- 1. 建连(带超时) —- */
int s = sock_connect_timeout(kServerIP, NET_SERVER_PORT, NET_CONNECT_TIMEOUT_MS);
if (s < 0) {
printf("[NET] connect failed/timeout, retry in 2s\\r\\n");
vTaskDelay(pdMS_TO_TICKS(2000));
continue;
}
printf("[NET] connected\\r\\n");

/* —- 2. 发送请求(带超时) —- */
const char *req = "GET /status HTTP/1.0\\r\\nHost: dev\\r\\n\\r\\n";
if (sock_send_timeout(s, req, strlen(req), NET_SEND_TIMEOUT_MS) < 0) {
printf("[NET] send timeout/err, close\\r\\n");
lwip_close(s);
vTaskDelay(pdMS_TO_TICKS(1000));
continue;
}

/* —- 3. 接收响应(带超时,循环收直到对端关闭或超时) —- */
int total = 0;
for (;;) {
int n = sock_recv_timeout(s, rxbuf, sizeof(rxbuf), NET_RECV_TIMEOUT_MS);
if (n > 0) {
/* 处理数据:此处仅打印长度,实际可解析/入队 */
total += n;
/* fwrite / 解析 … */
} else if (n == 0) {
printf("[NET] peer closed, total=%d\\r\\n", total);
break; /* 对端关闭,正常结束 */
} else {
if (errno == EWOULDBLOCK)
printf("[NET] recv timeout, total=%d\\r\\n", total);
else
printf("[NET] recv error\\r\\n");
break; /* 超时或错误,结束本次会话 */
}
}

/* —- 4. 关闭并延时重连 —- */
lwip_close(s);
vTaskDelay(pdMS_TO_TICKS(3000));
}
}


5. 创建任务(main 或初始化处)

#include "net_io_task.h"

void app_start(void)
{
/* 前置:网卡已 up,DHCP 完成或静态 IP 已设置,LwIP 已初始化 */
xTaskCreate(net_io_task,
"net_io",
NET_TASK_STACK_SIZE / sizeof(StackType_t), /* FreeRTOS 用 word */
NULL,
NET_TASK_PRIO,
NULL);
}

注意:xTaskCreate 的栈大小在多数 FreeRTOS 移植里以 word(4 字节) 为单位;上面 NET_TASK_STACK_SIZE 我按字节给了宏,创建时除以 sizeof(StackType_t)。若你直接习惯用 word 数,把宏改成 word 数并去掉除法即可。


6. lwipopts.h 关键配置

/* lwipopts.h 片段 —— 必须确保 select 与 socket 相关开启 */
#define LWIP_SOCKET 1
#define LWIP_NETCONN 1
#define LWIP_SO_RCVTIMEO 1 /* 若想用 SO_RCVTIMEO 方式 */
#define LWIP_SO_SNDTIMEO 1
#define LWIP_SO_RCVBUF 1
#define LWIP_SELECT 1 /* LwIP 1.4+ 内部由 LWIP_SOCKET 提供 */

#define MEMP_NUM_NETCONN 8 /* 每 socket 占一个 netconn */
#define MEMP_NUM_NETBUF 16
#define MEMP_NUM_TCP_SEG 32
#define TCP_SND_BUF (4 * TCP_MSS)
#define TCP_WND (4 * TCP_MSS)

/* 若用 select,确保以下开启(部分版本) */
#define LWIP_TIMEVAL_PRIVATE 0 /* 用系统的 struct timeval,或 1 用 LwIP 自带 */


7. 边界与常见坑(务必看)

场景处理
select 返回值 0=超时,>0=就绪数,<0=错误。超时要显式判定,不能当错误一律关闭
EINTR FreeRTOS 下 LwIP select 一般不会返回 EINTR(无信号机制),但保留重试逻辑无害;EWOULDBLOCK/EAGAIN 必须重试而非直接失败
部分收发 TCP 是字节流,send 可能只发一部分、recv 可能少于请求长度,必须用循环累计(上面已处理)
recv 返回 0 表示对端有序关闭,是正常终止,不是错误
connect 超时 必须用非阻塞 + select + SO_ERROR 检查,否则 connect 可能长时间阻塞任务
任务优先级 网络任务优先级不要高于 LwIP 的 tcpip_thread(通常 LWIP_TCPIP_CORE_LOCKING 或独立线程,一般为中高优先级);过低可能饿死
栈大小 LwIP + 字符串处理 + printf 很吃栈,建议 ≥ 2 KB,含 printf 时更大
printf 阻塞 若底层 UART 阻塞,长时间打印会影响超时精度;量产建议用 RTT/队列
超时精度 基于 xTaskGetTickCount(),受 configTICK_RATE_HZ 限制;1 ms tick 下毫秒级足够,若要求更精确用 xTaskGetTickCountFromISR/高精度定时器

8. 备选方案:直接用 SO_RCVTIMEO / SO_SNDTIMEO

若你的 LwIP 版本完整支持这两个选项(LWIP_SO_RCVTIMEO=1, LWIP_SO_SNDTIMEO=1),代码可更短:

struct timeval tv = { .tv_sec = NET_RECV_TIMEOUT_MS / 1000,
.tv_usec = (NET_RECV_TIMEOUT_MS % 1000) * 1000 };
lwip_setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

ssize_t n = lwip_recv(s, buf, len, 0);
if (n < 0 && (errno == EWOULDBLOCK || errno == EAGAIN)) {
/* 超时 */
}

权衡:SO_*TIMEO 简洁,但部分 LwIP 移植版本对 send 超时不支持或语义不一致;select 方案跨版本最稳,且能统一管理多 socket。量产/多连接建议用 select 方案。


9. 流程总结

text

connect(非阻塞) → select 等可写(超时) → SO_ERROR 校验
│
send(循环) → select 等可写(每轮重新算剩余超时) → 累计字节
│
recv(循环) → select 等可读(超时) → n>0 处理 / n==0 关闭 / n<0 超时或错误
│
close → 延时 → 重连

整个任务内部没有任何无限阻塞调用,每个网络操作都有明确上限,满足实时系统对任务响应性的要求。

赞(0)
未经允许不得转载:网硕互联帮助中心 » FreeRTOS + STM32 + LwIP 超时网络 I/O 完整示例
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!