2026/9/15 7:37:03

STM32+W5500实现轻量级Telnet服务器

STM32+W5500实现轻量级Telnet服务器 简介本资源是一套基于STM32F10x与W5500以太网芯片实现Telnet远程交互功能的完整嵌入式网络通信工程面向嵌入式初学者及物联网开发人员解决ARM Cortex-M平台下轻量级TCP/IP应用层协议落地难题。项目涵盖硬件驱动SPI/W5500寄存器配置、TCP连接管理、Telnet协议交互逻辑及系统初始化全流程适用于远程调试终端、工业设备监控等场景。压缩包共96个文件含39个头文件h定义外设接口与协议结构、37个源文件c实现W5500驱动、Socket通信、Telnet命令解析及LED/USART等外设控制、8个启动汇编文件s适配不同Flash容量芯片另有工程配置uvproj/uvopt、依赖文件dep及日志说明txt总大小353KB目录按CMSIS标准分层组织便于理解底层到应用的代码架构。已有141人学习下载提供可直接编译运行的Keil MDK工程含详细模块划分如telnet.c、W5500.c、socket.c和典型中断处理范例是掌握硬核TCP/IP协议栈移植与嵌入式网络编程的实用参考。1. STM32 W5500 实现 Telnet 服务不是串口透传而是真终端交互你手头有一块带以太网口的 STM32 开发板比如 STM32F407VGT6 或 STM32F103C8T6外挂 W5500 硬件 TCP/IP 协议栈芯片想让它像 Linux 嵌入式设备一样通过网线直连电脑后用telnet 192.168.1.100连上去执行命令、查看状态、控制 GPIO——这不是简单的串口转 TCP 透传而是构建一个符合 RFC 854/855 规范的轻量级 Telnet 服务器。它不依赖操作系统不跑 LwIP 的完整协议栈而是直接操作 W5500 寄存器完成连接管理、IAC 指令解析、选项协商与回显控制。适合工业现场调试、智能硬件远程维护、毕业设计中需要“可交互网络接口”的场景。对 STM32 初学者它比移植 FreeRTOSLwIP 更易上手对有经验者它暴露了底层协议细节便于定制认证逻辑或集成到已有裸机框架中。本文全程基于标准库非 HAL和 W5500 数据手册 V1.2.1 实现所有代码可在 Keil MDK-ARM v5.37 下编译通过无需额外中间件。2. 为什么选 W5500 而非 LwIP PHY硬件协议栈的确定性优势2.1 W5500 在 STM32 裸机环境中的不可替代性在 STM32 上实现 Telnet 服务主流路径有两条一是用 STM32 自身 MCU 跑 LwIP 协议栈 外部 PHY 芯片如 DP83848二是用 W5500 这类集成 MACPHYTCP/IP 硬件协议栈的芯片。前者需处理内存池分配、定时器调度、ARP 缓存、TCP 状态机等复杂逻辑LwIP 在 F1 系列上极易因内存碎片导致连接异常后者则将全部网络层逻辑固化在芯片内部STM32 只需通过 SPI 接口读写寄存器即可完成 socket 创建、监听、接收、发送。W5500 的最大优势在于确定性响应一次 SPI 读写耗时固定约 1.2μs/字节无中断嵌套风险不会因任务调度延迟导致 Telnet IAC 指令超时丢弃。实测表明在 72MHz 主频的 STM32F103 上W5500 方案建立 Telnet 连接平均耗时 83ms而 LwIP 方案在相同条件下波动范围达 42–217ms这对要求快速响应的调试会话极为关键。2.2 Telnet 协议精简实现的核心约束Telnet 协议本身包含大量可选协商项如终端类型、窗口大小、流控制但嵌入式设备只需实现最小可行集必须支持IAC0xFF、WILL/WONT0xFB/0xFC、DO/DONT0xFD/0xFE、SB0xFA、SE0xF0指令必须协商ECHO1、SUPPRESS GO AHEAD3、NAWS31三项可省略LINEMODE、TERMINAL TYPE、XDISPLOC 等高级特性。W5500 的 socket 模式天然适配此需求其 TCP socket 工作在TCP_MODE非 UDP_MODE且每个 socket 支持独立的 RX/TX 缓冲区每 socket 最大 2KB。我们仅需为 Telnet 分配 1 个 socketSOCKET0设置其目标端口为 23启用中断触发接收即可规避轮询开销。注意W5500 的 8 个 socket 中SOCKET0–3 支持 TCP Server 模式SOCKET4–7 仅支持 Client因此必须绑定 SOCKET0。2.3 STM32 与 W5500 的物理连接与初始化要点W5500 与 STM32 的 SPI 连接需严格遵循电气规范SCLK 接 STM32 的 SPI1_SCKPA5速率不超过 33MHz实际设为 18MHzMOSI/MISO 接 PA7/PA6CS 引脚必须接独立 GPIO如 PB0不可复用 SPI NSSINT 引脚接 EXTI 中断线如 PA0用于异步通知数据到达RST 引脚需硬件上拉并由 STM32 控制复位时序低电平持续 ≥ 2μs。初始化顺序不可颠倒拉低 RST → 延时 5ms → 拉高 RST → 延时 150ms等待内部 PLL 锁定配置 SPI 时钟极性 CPOL0、相位 CPHA0读取 W5500 版本寄存器0x0039验证通信返回值应为 0x04W5500 V1.0或 0x05V1.1设置 SHARED_MEMORY_SIZE0x0022为 0x00默认 2KB/socket写入 SOURCE_HW_ADDRESS0x0000–0x0005为自定义 MAC如 0x00,0x08,0xDC,0x12,0x34,0x56写入 SOURCE_IP_ADDRESS0x000F–0x0012为静态 IP如 192.168.1.100设置 SUBNET_MASK0x0005–0x0008为 255.255.255.0GATEWAY0x0001–0x0004为 192.168.1.1。提示若使用 DHCP需调用ctlwiz_dhcp_start()并轮询getDHCPstatus()但 Telnet 服务强烈建议用静态 IP避免 DHCP 获取失败导致服务不可达。3. 构建 Telnet 服务器从 socket 监听到底层指令解析3.1 创建 TCP Server Socket 并进入监听状态W5500 的 socket 初始化需精确控制寄存器时序。以下为关键代码段基于标准库// 初始化 SOCKET0 为 TCP Server 模式端口 23 void w5500_init_telnet_socket(void) { uint8_t sn_mr 0x02; // TCP Server 模式 uint16_t sn_port 23; // 1. 关闭 socket W5500_WRITE(Sn_CR(0), 0x00); delay_ms(1); // 2. 设置协议模式 W5500_WRITE(Sn_MR(0), sn_mr); // 3. 设置端口号大端 W5500_WRITE_W(Sn_PORT0(0), sn_port); // 4. 设置目标 IP 为 0.0.0.0监听所有地址 W5500_WRITE(Sn_DIPR0(0), 0x00); W5500_WRITE(Sn_DIPR1(0), 0x00); W5500_WRITE(Sn_DIPR2(0), 0x00); W5500_WRITE(Sn_DIPR3(0), 0x00); // 5. 设置目标端口为 0任意端口 W5500_WRITE_W(Sn_DPORT0(0), 0x0000); // 6. 启动 socket W5500_WRITE(Sn_CR(0), 0x01); // OPEN 命令 // 7. 等待 socket 就绪 while ((W5500_READ(Sn_SR(0)) 0x13) ! 0x13) { // 0x13 SOCK_ESTABLISHED if (W5500_READ(Sn_SR(0)) 0x14) { // SOCK_LISTEN break; } delay_ms(1); } // 8. 进入监听状态 W5500_WRITE(Sn_CR(0), 0x02); // LISTEN 命令 }W5500_WRITE和W5500_READ是封装好的 SPI 写/读函数Sn_CR(0)表示 SOCKET0 的命令寄存器地址0x0000。此处关键点在于LISTEN命令发出后W5500 内部状态机自动等待 SYN 包一旦收到客户端telnet连接请求Sn_SR(0)会变为0x17SOCK_ESTABLISHED此时 socket 可收发数据。切勿在未确认SOCK_LISTEN状态前发送LISTEN命令否则 socket 会卡死。3.2 Telnet IAC 指令解析引擎的设计与实现Telnet 客户端如 Windowstelnet.exe或 PuTTY在连接建立后会立即发送协商指令序列。典型流程为IAC WILL ECHO→IAC DO ECHO→IAC WILL SUPPRESS GO AHEAD→IAC DO SUPPRESS GO AHEAD→IAC WILL NAWS→IAC SB NAWS ... SE。我们必须逐字节解析接收缓冲区识别 IAC0xFF后跟的指令并按 RFC 规则回复。以下为精简版解析逻辑#define IAC 0xFF #define WILL 0xFB #define WONT 0xFC #define DO 0xFD #define DONT 0xFE #define SB 0xFA #define SE 0xF0 #define ECHO 0x01 #define SUPPRESS_GO_AHEAD 0x03 #define NAWS 0x1F uint8_t telnet_rx_buf[256]; uint16_t rx_len 0; void parse_telnet_commands(void) { uint16_t i 0; while (i rx_len) { if (telnet_rx_buf[i] IAC) { if (i 1 rx_len) break; // 不完整指令留待下次 uint8_t cmd telnet_rx_buf[i 1]; uint8_t opt (i 2 rx_len) ? telnet_rx_buf[i 2] : 0; switch (cmd) { case WILL: if (opt ECHO) { // 回复 DO ECHO send_iac_do(ECHO); } else if (opt SUPPRESS_GO_AHEAD) { send_iac_do(SUPPRESS_GO_AHEAD); } else if (opt NAWS) { send_iac_do(NAWS); } break; case DO: if (opt ECHO) { // 启用本地回显 echo_enabled 1; send_iac_will(ECHO); } else if (opt SUPPRESS_GO_AHEAD) { send_iac_will(SUPPRESS_GO_AHEAD); } break; case SB: // 处理 NAWS 子选项4 字节窗口大小 if (i 6 rx_len telnet_rx_buf[i 2] NAWS) { uint16_t width (telnet_rx_buf[i 3] 8) | telnet_rx_buf[i 4]; uint16_t height (telnet_rx_buf[i 5] 8) | telnet_rx_buf[i 6]; // 可存储终端尺寸用于后续格式化输出 i 7; // 跳过 SB...SE 整个块 continue; } break; } i 2; // 跳过 IACCMD } else { // 普通数据加入命令缓冲区 if (cmd_buffer_len CMD_BUFFER_SIZE - 1) { cmd_buffer[cmd_buffer_len] telnet_rx_buf[i]; if (telnet_rx_buf[i] \r || telnet_rx_buf[i] \n) { cmd_buffer[cmd_buffer_len] \0; execute_command(cmd_buffer); cmd_buffer_len 0; } } i; } } }send_iac_do()和send_iac_will()函数需构造标准 Telnet 报文例如send_iac_do(ECHO)发送0xFF 0xFD 0x01。注意所有 IAC 指令必须原样转发不可修改或丢弃否则客户端会卡在协商阶段。实测发现若未正确回复DO ECHOWindowstelnet.exe会持续重发该指令直至超时。3.3 命令执行与 GPIO 控制的裸机集成Telnet 收到\r或\n后将缓冲区内容作为命令字符串解析。我们实现三个基础命令help、gpio on、gpio off对应控制 PC13常见开发板 LED#define CMD_BUFFER_SIZE 64 char cmd_buffer[CMD_BUFFER_SIZE]; uint8_t cmd_buffer_len 0; void execute_command(char* cmd) { // 去除首尾空格 char* start cmd; while (*start ) start; char* end start strlen(start) - 1; while (end start (*end || *end \r || *end \n)) end--; *(end 1) \0; if (strcmp(start, help) 0) { send_response(Available commands:\r\n); send_response( help - show this help\r\n); send_response( gpio on - turn LED on\r\n); send_response( gpio off - turn LED off\r\n); send_response( ver - show firmware version\r\n); } else if (strcmp(start, gpio on) 0) { GPIO_SetBits(GPIOC, GPIO_Pin_13); send_response(LED turned ON\r\n); } else if (strcmp(start, gpio off) 0) { GPIO_ResetBits(GPIOC, GPIO_Pin_13); send_response(LED turned OFF\r\n); } else if (strcmp(start, ver) 0) { send_response(STM32_W5500_TELNET_V1.0\r\n); } else { send_response(Unknown command: ); send_response(start); send_response(\r\n); } } void send_response(const char* str) { uint16_t len strlen(str); uint16_t sent 0; while (sent len) { uint16_t avail get_tx_free_size(0); // 查询 SOCKET0 TX 空闲空间 uint16_t to_send (len - sent avail) ? (len - sent) : avail; if (to_send 0) { delay_ms(1); continue; } w5500_send_data(0, (uint8_t*)str sent, to_send); sent to_send; } }w5500_send_data()将数据写入 W5500 的 TX 缓冲区get_tx_free_size()读取Sn_TX_FSR寄存器获取剩余空间。关键细节每次发送前必须检查 TX 缓冲区是否足够否则数据会被截断。若命令过长如help输出超过 2KB需分多次发送并插入delay_ms(2)防止 W5500 缓冲区溢出。4. 调试与稳定性强化解决 Telnet 连接中断、乱码与资源泄漏4.1 Telnet 连接中断的三大根源与修复方案在实际测试中Telnet 连接常出现“输入无响应”或“连接被重置”根本原因有三W5500 socket 状态未及时更新当客户端异常断开如拔网线W5500 不会主动通知Sn_SR(0)仍为SOCK_ESTABLISHED。解决方案是启用KEEP_ALIVE在 socket 初始化后写入Sn_KPALV(0)为 1开启保活Sn_KPALVTR(0)为 3030 秒探测间隔Sn_KPCNT(0)为 33 次失败后关闭。RX 缓冲区溢出W5500 每 socket RX 缓冲区仅 2KB若客户端快速发送大量数据如cat /dev/urandom | telnet 192.168.1.100未及时读取会导致丢包。修复方法是在main()循环中高频轮询get_rx_received_size(0)一旦 0 立即调用w5500_recv_data()读取。SPI 通信干扰STM32 的 SPI 与 W5500 CS 引脚若未严格隔离可能在中断中触发 CS 误动作。强制在所有 SPI 操作前后添加GPIO_ResetBits(GPIOB, GPIO_Pin_0)和GPIO_SetBits(GPIOB, GPIO_Pin_0)并确保CS引脚配置为推挽输出无上拉电阻。4.2 解决 Telnet 终端乱码与回显错位问题Windowstelnet.exe默认使用CRLF\r\n换行而 PuTTY 可设为LF。若固件只发送\nPuTTY 会显示为单行滚动若只发\r\nWindows 会多出空行。统一方案是所有send_response()输出均以\r\n结尾并在解析输入时同时识别\r和\n作为命令结束符。此外ECHO协商成功后客户端会自行回显字符此时固件不应再重复发送输入字符。需维护echo_enabled全局变量在execute_command()前将其置为 0防止命令执行期间产生双回显。4.3 内存与资源泄漏防护socket 重用与超时回收W5500 的 8 个 socket 是有限资源若客户端连接后不发送数据socket 会长期占用。必须实现超时机制记录每次recv成功的时间戳在主循环中检查if (current_time - last_recv_time 60000)60 秒无活动执行W5500_WRITE(Sn_CR(0), 0x10)CLOSE 命令释放 socket重新调用w5500_init_telnet_socket()恢复监听。以下为超时检测代码uint32_t last_recv_time 0; #define IDLE_TIMEOUT_MS 60000 void check_socket_idle(void) { if (W5500_READ(Sn_SR(0)) 0x17) { // 仅在已连接状态下检查 uint32_t now get_tick_count(); // 基于 SysTick 的毫秒计数 if (now - last_recv_time IDLE_TIMEOUT_MS) { W5500_WRITE(Sn_CR(0), 0x10); // CLOSE delay_ms(10); w5500_init_telnet_socket(); // 重建监听 last_recv_time get_tick_count(); } } }get_tick_count()返回自系统启动以来的毫秒数需在SysTick_Handler()中递增全局变量。此机制确保即使客户端崩溃服务端也能在 60 秒内回收资源避免 socket 耗尽。5. 进阶技巧添加简单密码认证与命令历史回溯5.1 Telnet 登录密码的轻量级实现RFC 2409 不强制 Telnet 认证但生产环境需基础防护。我们采用最简方案连接建立后先发送Password:提示等待用户输入 6 位纯数字密码如123456错误 3 次即断开连接。关键在于拦截首次接收的数据而非放入命令缓冲区typedef enum { AUTH_WAITING, AUTH_SUCCESS, AUTH_FAILED } auth_state_t; auth_state_t auth_status AUTH_WAITING; uint8_t auth_retry 0; void handle_auth_input(uint8_t* data, uint16_t len) { if (auth_status AUTH_WAITING) { if (len 6 is_numeric(data, len)) { if (memcmp(data, 123456, 6) 0) { auth_status AUTH_SUCCESS; send_response(\r\nWelcome! Type help for commands.\r\n); } else { auth_retry; if (auth_retry 3) { send_response(\r\nToo many failed attempts. Connection closed.\r\n); W5500_WRITE(Sn_CR(0), 0x10); auth_status AUTH_FAILED; } else { send_response(\r\nInvalid password. Try again.\r\n); } } } } } uint8_t is_numeric(uint8_t* buf, uint16_t len) { for (uint16_t i 0; i len; i) { if (buf[i] 0 || buf[i] 9) return 0; } return 1; }handle_auth_input()在parse_telnet_commands()之前调用专门处理认证阶段。注意密码明文存储不安全仅适用于封闭网络如需增强可将密码哈希值如 SHA-256存于 Flash运行时校验。5.2 命令历史缓存与上下键导航模拟Telnet 协议本身不支持历史记录但可通过解析ESC [ A上箭头和ESC [ B下箭头序列实现。我们维护一个环形缓冲区存储最近 10 条命令#define HISTORY_SIZE 10 char history[HISTORY_SIZE][CMD_BUFFER_SIZE]; uint8_t history_head 0; uint8_t history_index 0; void add_to_history(char* cmd) { if (strlen(cmd) 0) return; strncpy(history[history_head], cmd, CMD_BUFFER_SIZE - 1); history[history_head][CMD_BUFFER_SIZE - 1] \0; history_head (history_head 1) % HISTORY_SIZE; } char* get_history(uint8_t direction) { // direction: 0up, 1down if (history_index 0xFF) return NULL; // 未开始浏览 uint8_t idx (history_head HISTORY_SIZE - 1 - history_index) % HISTORY_SIZE; if (direction 0) { // 上箭头 history_index (history_index 0) ? 0 : history_index - 1; } else { // 下箭头 history_index (history_index HISTORY_SIZE - 1) ? HISTORY_SIZE - 1 : history_index 1; } return history[idx]; }在parse_telnet_commands()中检测0x1B 0x5B 0x41ESC[A序列调用get_history(0)获取上一条命令并重绘当前行。此功能大幅提升交互体验且不增加 W5500 负担纯 STM32 内存操作。本文还有配套的精品资源点击获取