☰
N32G457 + W5500 跑 RT-Thread:用 Mongoose 搭一个能看数据的 Webserver
2026/9/29 6:02:24 网站建设 项目流程

1. 从一块 N32G457 说起:为什么要在 RT-Thread 上跑 Mongoose Webserver

手里有块 N32G457 开发板,想让它把采集到的数据直接通过网页展示出来,这个需求在工业现场、实验室监测、设备调试里都很常见。N32G457 是国民技术基于 Cortex-M4 的 MCU,主频跑到 144MHz,带以太网 MAC,但很多开发板并没有板载 PHY,这时候外挂一颗 W5500 硬件 TCP/IP 芯片就是很自然的选择。W5500 把 TCP/IP 协议栈做进了芯片内部,MCU 只需要通过 SPI 读写寄存器就能收发网络数据,省去了在 MCU 里跑 LwIP 的内存开销。

RT-Thread 作为国产 RTOS,生态里有 W5500 的软件包,也有 Mongoose 这样的轻量级网络库。Mongoose 本身是一个单文件、事件驱动的网络库,支持 HTTP、WebSocket、MQTT 等协议,把它接到 RT-Thread 上,再配合 W5500 的 SPI 驱动,就能搭出一个能看数据的 Webserver。这篇文章聚焦的就是这个场景:N32G457 + W5500 + RT-Thread + Mongoose,从驱动接入到网页数据刷新,把可复制的配置骨架和验证动作讲清楚。

适合谁看?如果你手上有 N32G457 或者类似的 Cortex-M4 板子,想用 W5500 做以太网通信,又不想在 MCU 里塞完整的 LwIP,那 Mongoose 这条路值得试。下面按实际工程顺序展开,先讲环境准备,再讲驱动和 Mongoose 的接入,最后演示浏览器访问验证。

2. 前置准备:RT-Thread 工程与 W5500 软件包接入

在开始写代码之前,需要把 RT-Thread 的工程骨架搭好。我习惯用 RT-Thread 的 env 工具配合 scons 来编译,这样软件包的添加和配置都比较直观。如果你用的是 RT-Thread Studio,操作逻辑类似,只是界面不同。

2.1 打开 W5500 软件包与 SPI 驱动

在 env 里进入工程目录,执行menuconfig,找到RT-Thread online packages→IoT - internet of things→W5500: Ethernet network chip,把它选上。W5500 软件包依赖 SPI 驱动,所以还要确认RT-Thread Components→Device Drivers→Using SPI Bus/Device device drivers是打开的。

配置 W5500 的时候有几个参数要注意:SPI 设备名称要和你板子上实际挂载的 SPI 总线一致,比如spi1;CS 引脚、RST 引脚、IRQ 引脚按实际接线填。以我手头的板子为例,接线是这样的:

W5500 模块引脚N32G457 引脚说明
CSPA3SPI 片选
RSTPC4复位
IRQPA0中断
SCKPA5SPI 时钟
MISOPA6主入从出
MOSIPA7主出从入

这些引脚在menuconfig里对应填好,保存退出后执行pkgs --update拉取软件包,再scons编译。如果编译报错说找不到wizchip相关符号,多半是软件包没更新完整,重新pkgs --update一次。

2.2 把 Mongoose 源码放进工程

Mongoose 是单文件库,直接去官网下载mongoose.c和mongoose.h,放到工程目录下,比如applications/mongoose/。然后在 SConscript 里把这个目录加进编译源文件列表。Mongoose 7.x 版本对 RT-Thread 的适配需要自己写几个回调,主要是网络收发和文件系统接口。

这里有个坑要提前说:Mongoose 默认的文件接口用的是标准库的fopen/fread,在 RT-Thread 里如果文件存在 ROMFS 中,标准库对 ROMFS 的读支持可能不完整,导致读index.html失败。解决办法是基于 RT-Thread 的 DFS POSIX 接口自己实现一套文件读写回调,后面第 4 节会给出具体代码。

3. 可复制配置:W5500 驱动适配与 Mongoose 接入骨架

这一节是核心,把 W5500 的 SPI 驱动适配、Mongoose 的网络回调、HTTP 事件处理都串起来。代码基于 RT-Thread 的 SPI 设备框架,Mongoose 版本是 7.4。

3.1 W5500 SPI 读写适配

W5500 软件包本身会调用wizchip_spi_readbyte和wizchip_spi_writebyte这类底层函数,需要我们自己实现。在 RT-Thread 里用rt_spi_send_then_recv和rt_spi_send来封装:

#include <rtthread.h> #include <rtdevice.h> static struct rt_spi_device *spi_dev_w5500; rt_err_t w5500_spi_init(void) { spi_dev_w5500 = (struct rt_spi_device *)rt_device_find("spi10"); if (spi_dev_w5500 == RT_NULL) { rt_kprintf("find spi10 failed\n"); return -RT_ERROR; } struct rt_spi_configuration cfg; cfg.data_width = 8; cfg.mode = RT_SPI_MASTER | RT_SPI_MODE_0 | RT_SPI_MSB; cfg.max_hz = 20 * 1000 * 1000; rt_spi_configure(spi_dev_w5500, &cfg); return RT_EOK; } void wizchip_spi_readbyte(unsigned char *buf, rt_uint32_t len) { rt_spi_recv(spi_dev_w5500, buf, len); } void wizchip_spi_writebyte(unsigned char *buf, rt_uint32_t len) { rt_spi_send(spi_dev_w5500, buf, len); }

这里有个细节:W5500 的 SPI 帧格式是地址段 + 控制段 + 数据段,软件包里的wizchip_spi_readbyte和wizchip_spi_writebyte是分开调用的,实际传输时 CS 要保持低电平。RT-Thread 的rt_spi_send_then_recv会自动处理 CS,但 W5500 软件包可能自己控制 CS,所以更稳妥的方式是用rt_spi_send_then_recv把地址和控制字节一起发出去,再读数据。我实测下来,直接用rt_spi_recv和rt_spi_send配合软件包的 CS 控制也能跑通,但要注意 SPI 总线上不要挂其他设备。

3.2 Mongoose 网络收发回调

Mongoose 需要两个底层回调:发送数据和接收数据。W5500 软件包提供了 socket API,类似 BSD socket,所以可以直接用send和recv:

#include "mongoose.h" static size_t mg_w5500_tx(const void *buf, size_t len, void *userdata) { int sock = *(int *)userdata; int ret = send(sock, buf, len, 0); return (ret > 0) ? ret : 0; } static size_t mg_w5500_rx(void *buf, size_t len, void *userdata) { int sock = *(int *)userdata; int ret = recv(sock, buf, len, 0); return (ret > 0) ? ret : 0; }

然后在 Mongoose 的mg_mgr初始化时,把这两个回调注册进去。Mongoose 7.x 的mg_mgr_init接受一个struct mg_mgr指针,网络接口通过mg_connect或mg_listen时指定。更直接的方式是用 Mongoose 的mg_mgr_poll驱动事件循环,在 RT-Thread 里单独开一个线程跑这个循环。

3.3 HTTP 事件处理与数据接口

Mongoose 的 HTTP 服务器通过mg_http_listen注册,事件回调里处理请求。下面是一个展示传感器数据的接口骨架:

static void http_ev_handler(struct mg_connection *c, int ev, void *ev_data) { if (ev == MG_EV_HTTP_MSG) { struct mg_http_message *hm = (struct mg_http_message *)ev_data; if (mg_match(hm->uri, mg_str("/api/data"), NULL)) { char json[128]; int temp = get_temperature(); int humi = get_humidity(); int len = rt_snprintf(json, sizeof(json), "{\"temp\":%d,\"humi\":%d}", temp, humi); mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%.*s", len, json); } else if (mg_match(hm->uri, mg_str("/"), NULL)) { struct mg_http_serve_opts opts = { .root_dir = "/romfs", .fs = &mg_fs_posix }; mg_http_serve_dir(c, hm, &opts); } } }

get_temperature和get_humidity是你自己的数据采集函数,可以读 ADC、读传感器,或者像原项目那样先用随机数模拟。mg_http_serve_dir负责把 ROMFS 里的静态文件发出去,mg_fs_posix是 Mongoose 自带的 POSIX 文件系统接口,如果 ROMFS 读有问题,就换成自己实现的 DFS 接口。

3.4 线程划分与主循环

整个工程用两个线程就够了:一个跑 Mongoose 的 HTTP 服务器主循环,一个跑数据采集。Mongoose 线程里调用mg_mgr_poll,数据线程里定时更新全局变量:

static void mongoose_thread_entry(void *param) { struct mg_mgr mgr; mg_mgr_init(&mgr); mg_http_listen(&mgr, "http://0.0.0.0:80", http_ev_handler, NULL); while (1) { mg_mgr_poll(&mgr, 10); } } static void info_thread_entry(void *param) { while (1) { update_sensor_data(); rt_thread_mdelay(1000); } }

两个线程的优先级:Mongoose 线程可以设低一点,数据线程设高一点,保证数据更新及时。栈大小方面,Mongoose 线程建议给 4096 字节以上,因为 HTTP 解析和 JSON 拼接会用到不少栈空间。

4. 验证请求:浏览器访问与数据刷新演示

代码编译烧录之后,把 W5500 模块插上网线,接到和电脑同一个路由器下。N32G457 上电后,W5500 会通过 DHCP 获取 IP,如果没有 DHCP 服务器,就需要在代码里设静态 IP。串口终端里会打印获取到的 IP 地址,比如192.168.1.100。

打开浏览器,输入这个 IP,应该能看到index.html页面。页面里用 d3.js 画动态图表,用 axios 定时请求/api/data接口,拿到 JSON 数据后更新图表。下面是一个简化的index.html骨架:

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>N32G457 数据展示</title> <script src="https://d3js.org/d3.v7.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> </head> <body> <h2>传感器数据</h2> <div id="chart"></div> <script> const svg = d3.select("#chart").append("svg") .attr("width", 600).attr("height", 300); const data = []; function fetchData() { axios.get("/api/data").then(res => { data.push(res.data); if (data.length > 20) data.shift(); render(); }); } function render() { svg.selectAll("rect").remove(); svg.selectAll("rect") .data(data) .enter().append("rect") .attr("x", (d, i) => i * 30) .attr("y", d => 300 - d.temp * 2) .attr("width", 25) .attr("height", d => d.temp * 2) .attr("fill", "steelblue"); } setInterval(fetchData, 1000); </script> </body> </html>

把这个文件放到 ROMFS 的根目录,编译进固件。浏览器访问后,每秒会请求一次/api/data,图表柱子会动态变化。如果数据不刷新,先看串口有没有打印请求日志,再看/api/data单独访问能不能返回 JSON。

实测下来,Firefox 浏览器表现最稳定,因为它只会创建 2 个连接。Google 系浏览器(Chrome、Edge)会一次性发起多个连接,W5500 的 socket 数量有限(8 个),容易被耗尽,导致index.html加载失败甚至宕机。解决办法有两个:一是限制 Mongoose 的最大连接数,二是用 Firefox 测试。如果非要用 Chrome,可以在 Mongoose 里设置mgr.connection_limit,或者在 HTTP 响应头里加Connection: close,让浏览器不要保持长连接。

5. 本篇常见错排查:SPI 错位、W5500 选项、ROMFS 读失败

这一节把开发过程中容易踩的坑集中列一下,都是实际调试时遇到的。

5.1 SPI 回读数据错位

自己写 SPI 驱动的时候,如果在填充数据之前发现SPI_I2S_RNE_FLAG已经置位,回读的数据就会错位或缺失。原因是 SPI 接收寄存器里还有上一次的残留数据。解决办法是在每次读写之前,先做一次 dummy 读,把 RNE 标志清掉:

while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_RNE_FLAG) == SET) { SPI_I2S_ReceiveData(SPI1); }

用 RT-Thread 的 SPI 框架时,这个问题一般不会出现,因为框架内部会处理。但如果你直接操作寄存器,就要注意这个细节。

5.2 W5500 软件包缺少 SO_REUSEADDR 和 SO_KEEPALIVE

W5500 软件包的wiz_setsockopt函数没有处理SO_REUSEADDR和SO_KEEPALIVE选项,导致 Mongoose 启动时设置 socket 选项失败,服务器起不来。解决办法是在wiz_setsockopt里增加这两个选项的处理,或者直接在 Mongoose 初始化时跳过这两个选项的设置。我选择的是改软件包,在wiz_setsockopt里加分支:

case SO_REUSEADDR: case SO_KEEPALIVE: return 0;

这样 Mongoose 调用时直接返回成功,不影响功能。

5.3 ROMFS 文件读失败

Mongoose 默认的mg_file用标准库fopen/fread实现,在 RT-Thread 里读 ROMFS 可能失败。解决办法是基于 DFS 的 POSIX 接口自己实现一套文件操作回调:

static void *dfs_fopen(const char *path, int flags) { int fd = open(path, flags); return (fd >= 0) ? (void *)(rt_base_t)fd : NULL; } static size_t dfs_fread(void *fp, void *buf, size_t len) { return read((int)(rt_base_t)fp, buf, len); } static void dfs_fclose(void *fp) { close((int)(rt_base_t)fp); } struct mg_fs mg_fs_dfs = { .st = NULL, .ls = NULL, .open = dfs_fopen, .read = dfs_fread, .write = NULL, .close = dfs_fclose, };

然后在mg_http_serve_opts里把.fs指向mg_fs_dfs,这样就能正常读 ROMFS 里的文件了。

5.4 浏览器连接数耗尽

前面提过,Google 系浏览器会一次性发起多个连接,W5500 只有 8 个 socket,容易被占满。除了换 Firefox,还可以在 Mongoose 里限制每个 IP 的最大连接数,或者在 HTTP 响应里加Connection: close。另外,W5500 的 socket 回收需要时间,如果频繁请求,建议把数据刷新间隔设成 1 秒以上,给 socket 留出回收窗口。

6. 接入与调试资源:API Key、文档与模型对话

如果你在调试过程中需要验证 HTTP 接口的返回格式,或者想用模型对话快速生成 JSON 解析代码,可以走 TaoToken 的模型对话入口,把接口返回的原始数据贴进去,让它帮你写解析逻辑。对于长期做嵌入式网络开发的场景,Coding Plan 更适合,可以把 Mongoose 的事件回调、W5500 的寄存器操作这些重复代码交给它补全。

接入文档里有 RT-Thread 环境下网络库的配置说明,API Keys 页面可以生成调试用的 Key。实际调试时,先用模型对话验证/api/data的 JSON 结构,再回到板子上跑,能省不少来回烧录的时间。

排障相关的入口:

  • API Keys:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api_keys
  • 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc
  • 模型对话:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=chat
  • Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding_plan

最后说一个实际调试的小技巧:W5500 的 IRQ 引脚可以接到 MCU 的外部中断,但 Mongoose 是轮询驱动的,IRQ 用不上。如果你发现mg_mgr_poll的轮询间隔太长导致响应慢,把间隔从 10ms 调到 5ms 试试,但别调太小,否则 CPU 占用会上去。数据刷新的流畅度,最终取决于 W5500 的 SPI 速率和 socket 回收速度,20MHz 的 SPI 时钟下,1 秒刷新一次完全够用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询