N32G457 + W5500 跑 RT-Thread:用 Mongoose 搭一个能看数据的 Webserver
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 总线一致比如spi1CS 引脚、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 charsetutf-8 titleN32G457 数据展示/title script srchttps://d3js.org/d3.v7.min.js/script script srchttps://cdn.jsdelivr.net/npm/axios/dist/axios.min.js/script /head body h2传感器数据/h2 div idchart/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_KEEPALIVEW5500 软件包的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 Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc模型对话https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentchatCoding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding_plan最后说一个实际调试的小技巧W5500 的 IRQ 引脚可以接到 MCU 的外部中断但 Mongoose 是轮询驱动的IRQ 用不上。如果你发现mg_mgr_poll的轮询间隔太长导致响应慢把间隔从 10ms 调到 5ms 试试但别调太小否则 CPU 占用会上去。数据刷新的流畅度最终取决于 W5500 的 SPI 速率和 socket 回收速度20MHz 的 SPI 时钟下1 秒刷新一次完全够用。