ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

GD32 TCP客户端:基于lwIP的以太网通信与调试

GD32 TCP客户端:基于lwIP的以太网通信与调试 简介这是一份基于GD32微控制器的TCP客户端示例工程面向嵌入式与物联网应用演示如何在GD32平台上实现TCP/IP通信与可靠数据传输。工程包含完整源码与配置覆盖TCP三次握手连接、数据收发、超时重传及四次挥手关闭连接等核心机制并集成lwIP轻量级协议栈与FreeRTOS支持帮助开发者快速理解并移植GD32网络应用。资源共1613个文件以C语言源码.c/.h为主辅以Keil、IAR工程文件、bin/hex固件及说明文档压缩包15.73MB目录清晰。目前已有523人学习适合入门GD32以太网通信或开发远程监控、数据采集设备的工程师参考。1. GD32 TCP客户端从一次数据采集系统的联调说起接手一台基于GD32F30x的工业数据采集设备时最常见的问题不是传感器采样而是数据怎么从单片机里稳定地送到上位机。串口速率不够USB协议栈又太重最合适的就是直接走以太网。GD32TCPdemo 正是这样一份工程模板它把 GD32 的 ENET 外设和 lwIP 协议栈串起来实现一个主动连接服务器的 TCP 客户端适合远程监控、数据采集网关、智能家居设备这类场景。工程里能看到 gd32f30x_enet.c、fsdata.c、GD32307C_EVAL.axf 等文件说明官方把底层驱动和上层协议都塞进了一个可编译的 Keil 工程。新手可以用它跳过寄存器细节直接跑通 TCP老手则能把它当成裁剪协议栈、调 DMA 描述符的起点。接下来按硬件底层、TCP 状态机、工程编译、联调验证的顺序拆开。2. 以太网驱动与描述符GD32F30x 的 MAC 底子2.1 从 ENET 库函数到寄存器映射GD32TCPdemo 中出现的 gd32f30x_enet.c 不是 lwIP 的一部分而是 GD32 固件库对以太网 MAC 和 DMA 外设的封装。它把所有寄存器操作打包成 enet_init、enet_descriptors_configure、enet_interrupt_enable 这一组函数。硬件上 GD32307C_EVAL 开发板使用 RMII 接口连接外部 PHY板上有 50MHz 有源晶振给 PHY 提供 RMII 参考时钟MCU 的 ETH_PHY_CLK 不需要复用 MCO这在移植时容易踩坑。使用库函数初始化时我一般这样组织参数enet_parameter_struct enet_init_param; enet_init_param.mac_forward_all DISABLE; enet_init_param.mac_checksum_offload ENABLE; enet_init_param.mac_vlan_filter DISABLE; enet_init_param.mac_rx_own_packet DISABLE; enet_init_param.mac_automatic_pad_crc_strip ENABLE; enet_init_param.mac_retry_transmission DISABLE; enet_init_param.mac_frame_length ENET_MAC_FRAME_LEN_MAX; enet_init_param.mac_retry_transmission_mode ENET_CRC_CHECK_DISABLE; enet_init_param.mac_retry_transmission_count 3; enet_init_param.mac_preamble_length ENET_PREAMBLE_LENGTH_7; enet_init(enet_init_param);这段代码的关键是把“是否校验 CRC、是否自动剥离填充字节、单帧最大长度”等 MAC 参数一次性写入寄存器。mac_checksum_offload启用后发送时硬件自动计算并填充帧尾的 FCS接收时硬件自动校验CPU 不需要参与。mac_automatic_pad_crc_strip让硬件把接收帧的填充和校验字段去掉lwIP 拿到的是净数据。如果这两个开关没配对应用层收到的 pbuf 会出现 6 到 8 个字节的偏移排查时很难发现。PHY 初始化是另一个重点。库函数enet_phy_config会向 PHY 写控制寄存器但前提是 PHY 地址正确。GD32307C_EVAL 板上的 PHY 地址是 0如果你改成自己的板子需要检查原理图中的 PHYAD[2:0] 引脚。地址不对时读回enet_phy_read返回 0xFFFF系统会一直卡在超时循环里。2.2 网络接收描述符的初始化与参数调整CPU 和 DMA 之间交换以太网帧依靠的是 DMA 描述符。GD32 的 ENET 使用描述符环每个接收描述符指向一个缓冲区。默认工程里通常只定义 4 个接收描述符和 4 个发送描述符在高吞吐场景下明显不够。我一般会先把这个数字调大#define ENET_RXBUF_NUM 8 #define ENET_TXBUF_NUM 8 #define ENET_RX_BUF_SIZE 1536 #define ENET_TX_BUF_SIZE 1536 static enet_descriptor_struct rx_desc_tab[ENET_RXBUF_NUM]; static enet_descriptor_struct tx_desc_tab[ENET_TXBUF_NUM]; static uint8_t rx_buffer[ENET_RXBUF_NUM][ENET_RX_BUF_SIZE]; static uint8_t tx_buffer[ENET_TXBUF_NUM][ENET_TX_BUF_SIZE]; void ethernet_descriptors_init(void) { uint16_t i; for (i 0; i ENET_RXBUF_NUM; i) { enet_descriptor_init(rx_desc_tab[i], rx_buffer[i][0], ENET_RX_BUF_SIZE); rx_desc_tab[i].control_status ENET_DESC_RX_OWN | ENET_DESC_RX_INT_ENABLE; } for (i 0; i ENET_TXBUF_NUM; i) { enet_descriptor_init(tx_desc_tab[i], tx_buffer[i][0], ENET_TX_BUF_SIZE); tx_desc_tab[i].control_status ENET_DESC_TX_OWN; /* 初始由CPU控制 */ } enet_descriptors_configure(rx_desc_tab[0], tx_desc_tab[0], ENET_RXBUF_NUM, ENET_TXBUF_NUM); }ENET_DESC_RX_OWN是描述符里最重要的位它表示 DMA 拥有这块缓冲区。接收开始前必须由 CPU 置 1DMA 收到帧后自动清零并写状态字段。中断处理程序里读取数据后需要重新设置这个位否则该描述符会被永久“占住”。初始化完成后调用enet_descriptors_configure告诉 MAC 描述符环的基地址和数量。描述符控制字的常用位如下位名方向作用OWN双向1 表示 DMA 拥有描述符0 表示 CPU 处理INT_ENABLE发送/接收置 1 时描述符完成后产生中断RX_FRAME_LEN接收记录接收帧长度CPU 据此知道缓冲区的有效数据TX_LAST_SEGMENT发送标识当前描述符是一个数据包的最后一个分段CHECKSUM_ERROR接收硬件校验失败时置位用于统计错误帧这里有一个容易忽略的细节接收缓冲区数组如果定义在普通 RAMDMA 能访问但 GD32F30x 没有独立 cache所以不需要做 cache 清理。但如果你换到带 cache 的 GD32F450 或 GD32F407 系列就必须在 DMA 写入后做 clean/invalidate 操作否则 CPU 可能从 cache 里读到脏数据。2.3 硬件层排错PHY 检测不到、RX 描述符死锁实验室里最常见的两个问题是enet_phy_read一直超时以及一段时间后收不到数据但网络连接还在。前者基本是 PHY 地址写错、RMII 时钟没起振、或 ETH_RESET 引脚没有正确拉高。用示波器测量 PHY 的 REF_CLK 是否稳定输出 50MHz 能很快定位。后者则是接收描述符死锁。死锁时中断标志已经置位但读取rx_desc_tab[i].control_status发现 OWN 位不是 0说明描述符没被 DMA 释放。常见原因是我方处理速度跟不上入帧速率描述符环被占满。解决思路不是盲目加大缓冲区而是先查中断服务程序里是否对每个描述符做了完整的数据搬移和 OWN 位重设。例如使用enet_common_interrupt_handler时如果只清中断标志而忘记重新挂接描述符就会出现丢帧后不再收包的假死现象。3. lwIP 的 TCP 客户端状态机connect、write、recv 回调3.1 tcp_connect 的三次握手与回调顺序lwIP 中 TCP 客户端首先要创建 PCBProtocol Control Block然后调用tcp_connect发起连接。GD32TCPdemo 使用的 lwIP 版本是经过裁剪的但 API 行为一致。连接流程如下struct tcp_pcb *client_pcb; ip_addr_t server_ip; client_pcb tcp_new(); if (client_pcb NULL) { /* 内存不足检查 lwipopts.h 中的 MEMP_NUM_TCP_PCB */ } IP4_ADDR(server_ip, 192, 168, 1, 100); tcp_arg(client_pcb, NULL); tcp_err(client_pcb, tcp_error_cb); err_t ret tcp_connect(client_pcb, server_ip, 8080, tcp_connected_cb); if (ret ! ERR_OK) { /* 连接发起失败释放 pcb */ tcp_abort(client_pcb); }tcp_connect不是同步建立连接它只是给 PCB 设置目标地址和端口然后让 lwIP 核心发送 SYN 报文。真正的连接成功通过回调通知。tcp_connected_cb在收到服务器的 SYN/ACK 并被确认后执行此时才能发送数据。回调函数签名如下static err_t tcp_connected_cb(void *arg, struct tcp_pcb *tpcb, err_t err) { if (err ! ERR_OK) { /* 连接失败等待重试 */ return ERR_OK; } tcp_sent(tpcb, tcp_sent_cb); /* 注册发送完成回调 */ tcp_recv(tpcb, tcp_recv_cb); /* 注册接收回调 */ const char *payload GD32 client online; tcp_write(tpcb, payload, strlen(payload), TCP_WRITE_FLAG_COPY); tcp_output(tpcb); return ERR_OK; }tcp_write把数据拷贝到 lwIP 内部发送队列TCP_WRITE_FLAG_COPY告诉协议栈必须复制数据不能引用栈上的指针。tcp_output立即触发一次发送。如果不调用tcp_outputlwIP 会等快速定时器到期后才发送通常有 200ms 左右的延迟。3.2 发送与接收的数据路径发送回调tcp_sent_cb的触发条件是协议栈已经确认对端收到了数据。注意 lwIP 的回调不是在中断里执行的它运行在tcpip_thread中所以回调代码里可以做耗时操作但不要调用可能阻塞 lwIP 核心的函数。接收回调是 TCP 客户端最核心的部分static err_t tcp_recv_cb(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { if (p NULL) { /* 对端发送了 FIN进入关闭流程 */ tcp_close(tpcb); return ERR_OK; } /* 处理接收到的数据 */ for (struct pbuf *q p; q ! NULL; q q-next) { process_payload(q-payload, q-len); } /* 告诉 lwIP 数据已经被应用层消费 */ tcp_recved(tpcb, p-tot_len); pbuf_free(p); return ERR_OK; }这里容易漏掉tcp_recved。lwIP 靠这个函数知道接收窗口可以向右滑动。如果应用层不调用它即使 pbuf 被释放协议栈也会认为数据仍在应用层接收窗口会逐渐缩小到 0最终导致服务器无法继续发送数据。tcp_recved的参数是已消费的字节数最好在数据处理完成后再调用而不是一进回调就调用。pbuf_free必须在tcp_recved之后还是之前都可以。因为tcp_recved只更新窗口pbuf_free释放内存。但必须在回调返回前完成否则 lwIP 内部跟踪的 pbuf 引用计数会混乱触发 LWIP_ASSERT。3.3 断线重连与内存回收TCP 连接不会永远存在。服务器重启、网线拔出都会导致连接异常。lwIP 对每个连接维护一个tcp_pcb如果对端 FIN接收回调会收到 NULL。如果是网络错误tcp_err回调会触发static void tcp_error_cb(void *arg, err_t err) { /* 连接异常终止此时 pcb 已经被 lwIP 释放不能再访问 tpcb */ reconnect_request 1; }错误回调触发的时刻 PCB 已被内部删除不能再调用tcp_close或tcp_abort。正确的做法是设置一个标志位在主循环或 poll 定时器里重新执行tcp_new和tcp_connect。重连时需要控制频率否则服务器未恢复时每次连接尝试都会在约 2 秒超时后失败并不断消耗内存。我习惯在主循环里加一个简单的退避逻辑第一次失败等 1 秒第二次等 2 秒最多等 10 秒。同时要检查tcp_new的返回值因为之前失败的 PCB 内存还没有被系统完全回收连续创建会导致内存耗尽。4. 从 Keil 到 Embedded Builder编译、烧录与 ST-Link 排错4.1 Keil MDK 工程与固件库版本对齐项目文件里有 test.uvguix.Administrator这是 Keil uVision 的窗口布局和用户配置不是工程主体。真正的工程入口是同名 .uvprojx 文件。GD32307C_EVAL.axf 是编译生成的 ARM 可执行文件GD32307C_EVAL_sct.Bak 是分散加载文件备份用于配置 Flash 和 RAM 区域地址。如果修改了内存大小定义比如把ENET_RXBUF_NUM从 4 改成 16接收缓冲区会增加约 24KB就可能导致链接时出现.bss超出 RAM 范围的错误。此时要查分散加载文件里 RW_IRAM1 的 size 是否足够或者把一些不再用的静态变量搬进外部 SRAM。Keil 工程里通常已经选好了 Device 为 GD32F307CC/C 选项里的 Define 一般包含USE_STDPERIPH_DRIVER、GD32F30X_HD和LWIP。如果你拿到工程后编译报错symbol enet_... undefined先检查是否把 gd32f30x_enet.c 和 gd32f30x_enet.h 分别加进了工程组和头文件路径。固件库版本不一致时enet_descriptor_struct的字段名可能不同需要对照官方固件库手册确认。4.2 GD32 Embedded Builder 迁移与 Linux 下的编译思路GD32 Embedded Builder 是兆易创新最近主推的 IDE 之一。它基于 Eclipse 和 GCC可以导入 Keil 工程但导入后需要重新配置启动文件、链接脚本和宏定义。注意 Keil 的分散加载文件不能直接用需要让 Embedded Builder 生成新的 .ld 链接脚本。内存布局参考原 Sct 文件即可。Linux 下编译这个 demo 的思路也很明确装好gcc-arm-none-eabi和make然后为工程写一个简单的 Makefile。关键是指定 GD32 固件库源码目录和 lwIP 源码目录。官方固件库的 Libraries 目录里已经包含 F30x_standard_peripheral 和 CMSISlwIP 则通常放在第三方目录下。一个最小命令序列如下sudo apt install gcc-arm-none-eabi make cd GD32TCPdemo make clean make all编译完成后会生成 .elf 和 .hex 文件。用烧录工具下载时注意 GD32F30x 的 Flash 起始地址是 0x08000000Sector 大小是 4KB。如果 Makefile 里LDFLAGS指定的内存地址和实际芯片不匹配烧进去后程序会跑飞。4.3 烧录工具选择串口下载与 ST-Link SWDGD32F30x 支持串口 ISP 下载。把 BOOT0 拉高、BOOT1 拉低复位后芯片进入 ISP 模式。使用 GD32 MCU ISP Programmer 或者官方串口烧录工具选择对应的串口和波特率烧录 .hex 文件。烧完后把 BOOT0 拉低再复位程序从 Flash 启动。这个方法不需要仿真器适合产线。使用 ST-Link 走 SWD 接口烧录更快还能在线调试。Keil 里配置 Debug 选择 ST-Link DebuggerFlash Download 里必须勾选 Reset and Run否则烧录后需要手动复位才执行。4.4 ST-Link internal command error 的排查步骤如果你在 Keil 里连接 ST-Link 到 GD32 时报internal command error不要急着换仿真器。这个错误通常是通信时序不稳定或 ST-Link 固件不识别 GD32 的 IDCODE。按下面顺序排查步骤操作可能原因1检查 ST-Link 与板子的 SWDIO/SWCLK/GND 是否连紧杜邦线接触不良2降低 SWD 频率到 1MHz频率过高、线缆过长3查看 ST-Link 固件版本旧固件不识别 GD32F30x4使用官方 ST-Link Upgrade 升级固件到最新版厂商更新了 IDCODE 列表5检查目标板 3.3V 供电是否稳定供电波动导致 SYNC 失败还有一个隐蔽点ST-Link 连接十二针 JTAG 接口时如果板子上的 JTAG 口和 ST-Link 的排针定义不同会导致 nRST 引脚冲突。建议只用 SWD 四线接口把 ST-Link 的 Mode 切换为 SWD并在 Keil 的 Debug 设置里清空 VTref 检测异常时的过保护。完成后重新插拔 USB 再试内部错误大部分都能解决。5. 验证与进阶用接收描述符和 C# 上位机把通信调到最稳5.1 Wireshark 抓包验证三次握手板子和服务器运行后用 Wireshark 在服务器网卡上抓包过滤条件直接写tcp.port 8080。可以看到标准的 client 到 server 的同步序列SYN、SYN/ACK、ACK 三个包。如果只有 SYN 重传而看不到 ACK说明服务器没监听端口或防火墙丢弃了入站包。如果三次握手成功但服务器端 Wireshark 里能看到发来的数据窗口不断变小就要检查tcp_recved是否及时调用。抓包时注意确认双方 IP 地址和端口没有被打错。GD32 端 IP 在ethernetif_init里通过 LWIP_DHCP 或静态配置指定。调试阶段最好用静态 IP避免 DHCP 响应延迟影响连接判断。5.2 C# TCP 客户端类与 GD32 服务端联调GD32 作为客户端主动连接上位机那么上位机通常要起一个 TCP 服务器。C# 的System.Net.Sockets.TcpListener最直接using System.Net.Sockets; TcpListener listener new TcpListener(System.Net.IPAddress.Parse(192.168.1.100), 8080); listener.Start(); TcpClient client listener.AcceptTcpClient(); NetworkStream stream client.GetStream(); byte[] buf new byte[1024]; int read stream.Read(buf, 0, buf.Length); Console.WriteLine(Received: System.Text.Encoding.ASCII.GetString(buf, 0, read));Read是阻塞的适合做单连接验证。注意上位机需要先启动监听GD32 才可能连上。接收到的数据可能不是一次 TCP 报文完整到达所以真实项目里要自己处理半包和粘包。最简单的方式是每个数据包以\r\n结尾上位机按行解析GD32 端发送时把行尾加上。5.3 描述符数量与中断频率的实测权衡最后一个值得动手调的参数是接收描述符数量。它会直接影响连续大量 TCP 分段时的丢包率。实测 GD32F30x 在 100Mbps 网速下MAINCLK 为 120MHzENET_RXBUF_NUM为 4 时持续接收 60KB 以上的数据会在中断处理期间发生 DMA 覆盖表现为p-tot_len出现随机值。把描述符增加到 8 后中断处理器可以延迟处理DMA 有更多缓冲可用。但代价是每个描述符 1536 字节的缓冲区8 个就是 12KB再加上发送描述符RAM 占用明显上升。表如下接收描述符数RAM 占用含缓冲区中断处理压力适用场景4约 6KB高低速率指令传输8约 12KB中常规数据采集16约 24KB低连续文件传输或高速上传调试时可以先从 8 开始观察 Wireshark 统计里的 TCP Dup ACK 和 Retransmission 数量。如果重传率没有下降就不是描述符不够而是 lwIP 的内存池大小问题。检查 lwipopts.h 里MEMP_NUM_PBUF和PBUF_POOL_SIZE这两个值决定了同时能缓冲多少个网络报文。把描述符调高但不能同步调大 pbuf 池高频通信时依然会在pbuf_alloc处返回 NULL。做完这组调整再抓包看就能看到重传消失、ACK 序列稳定。本文还有配套的精品资源点击获取
返回列表