STM32以太网开发详解:基于LwIP协议栈实现TCP/UDP通信(附网络摄像头案例)

发布于:2025-07-22 ⋅ 阅读:(28) ⋅ 点赞:(0)

前言:为什么STM32需要以太网?

在物联网和工业控制领域,设备联网已成为刚需。传统的串口、CAN总线等通信方式受限于距离和速率,而以太网凭借100Mbps/1Gbps的高速率、百米级传输距离和TCP/IP协议的通用性,成为设备接入互联网的首选方案。

STM32中高端型号(如F429、H743、F767等)集成了以太网MAC控制器,配合外部PHY芯片(如LAN8720)可实现完整的以太网通信功能。而LwIP(Lightweight IP) 协议栈的引入,让STM32能够轻松实现TCP、UDP、IP、ICMP等协议,无需从零开发复杂的网络协议。

本文将从硬件原理到软件实战,详细讲解STM32以太网开发流程:从MAC+PHY硬件配置,到LwIP协议栈移植,再到TCP/UDP通信实现,最后通过网络摄像头案例展示完整应用,帮助大家快速掌握STM32以太网开发。

一、STM32以太网硬件基础:MAC与PHY的协同工作

要实现以太网通信,STM32需要两个核心硬件组件:MAC控制器(内部集成)和PHY芯片(外部扩展),二者配合完成数据的编码、传输和接收。

1.1 以太网MAC:STM32内部的"数据调度中心"

MAC(Media Access Control,媒体访问控制)是STM32内部的以太网控制器,负责:

  • 实现以太网帧的组装与解析(添加帧头、帧尾、CRC校验);
  • 管理数据收发队列(支持DMA,减少CPU干预);
  • 支持全双工/半双工模式,速率可达10/100Mbps;
  • 提供MII(媒体独立接口)或RMII(简化媒体独立接口)与PHY芯片通信。

STM32不同系列的MAC特性略有差异:

  • F429/F767:支持RMII/MII,内置DMA控制器,最高100Mbps;
  • H743:支持千兆以太网(部分型号),增强型DMA,支持IEEE 1588精确时间协议;
  • L4系列:部分型号集成MAC,适合低功耗场景。

关键引脚(以RMII接口为例,最常用的简化接口):

  • 时钟:ETH_RMII_REF_CLK(50MHz,通常由PHY提供或外部晶振);
  • 数据:ETH_RMII_CRS_DV(载波侦听/数据有效)、ETH_RMII_RXD0/1(接收数据)、ETH_RMII_TX_EN(发送使能)、ETH_RMII_TXD0/1(发送数据);
  • 复位:ETH_RESET(控制PHY复位,可选);
  • 中断:ETH_INT(PHY中断,可选)。

1.2 PHY芯片:以太网的"物理层接口"

PHY(Physical Layer Transceiver,物理层收发器)是外部芯片,负责:

  • 将MAC输出的数字信号转换为以太网物理层的模拟信号(差分信号);
  • 实现信号的调制解调、噪声过滤和信号放大;
  • 支持自动协商(速率、双工模式);
  • 通过MDIO接口与MAC通信(MAC可配置PHY参数)。

常用PHY芯片

  • LAN8720:低成本、小封装(3.3V供电),支持RMII接口,性价比极高,适合入门;
  • DP83848:工业级,支持MII/RMII,抗干扰能力强,适合工业场景;
  • RTL8201:兼容性好,支持自动协商,常见于开发板。

PHY与STM32的连接(以LAN8720为例):

  • RMII信号线:与STM32的RMII引脚一一连接;
  • MDIO(管理接口):STM32的ETH_MDIO和ETH_MDC引脚连接到LAN8720的MDIO和MDC;
  • 电源:LAN8720需3.3V供电,注意电源稳定性(建议加100nF滤波电容);
  • 复位:LAN8720的RESET引脚接STM32的GPIO(如PA8),用于初始化复位;
  • 以太网接口:LAN8720的TX+/TX-、RX+/RX-接网络变压器,再连接到RJ45接口。

1.3 硬件设计注意事项

  1. 阻抗匹配:以太网差分线(TX+/TX-、RX+/RX-)需控制阻抗为100Ω±10%,布线时尽量短且平行,避免过孔和直角;
  2. 网络变压器:必须在PHY与RJ45之间串联网络变压器(如HR911105A),用于隔离共模干扰、提高抗雷击能力;
  3. 时钟稳定性:RMII参考时钟(50MHz)的抖动需控制在±50ppm以内,建议由PHY提供(LAN8720可输出50MHz时钟);
  4. 复位时序:PHY复位时间需满足芯片要求(LAN8720至少10ms),复位后再初始化MDIO接口。

二、LwIP协议栈:嵌入式以太网的"灵魂"

TCP/IP协议栈复杂且庞大(完整实现需数十KB内存),而嵌入式设备资源有限(STM32F429的RAM通常为256KB),LwIP(轻量级IP)应运而生——它是专为嵌入式设计的开源TCP/IP协议栈,以内存占用小(最小仅几十KB)、代码精简(核心代码约150KB)为特点,完美适配STM32。

2.1 LwIP的核心特性

  • 支持核心协议:IP(IPv4/IPv6)、ICMP(ping)、TCP、UDP、ARP、DHCP;
  • 内存管理:采用内存池(memp)和堆(heap)结合的方式,高效利用有限内存;
  • API接口:提供两种API:
    • RAW API:无操作系统(bare-metal)时使用,基于回调函数,实时性高;
    • Socket API:类似POSIX的socket接口,需配合操作系统(如FreeRTOS),易用性好;
  • 可裁剪:可根据需求关闭不需要的协议(如IPv6、DHCP),减少资源占用。

2.2 LwIP在STM32上的移植

STM32Cube生态已集成LwIP协议栈,无需手动移植,通过CubeMX配置即可生成适配代码。移植的核心是实现底层网卡驱动(low-level driver),包括:

  • 初始化MAC和PHY;
  • 实现数据发送函数(将LwIP的数据包发送到物理层);
  • 实现数据接收函数(从物理层接收数据并提交给LwIP);
  • 中断处理(PHY中断、DMA中断)。

CubeMX生成的代码已包含这些驱动,用户只需关注应用层逻辑。

三、开发环境搭建:STM32CubeMX配置以太网与LwIP

本节以STM32F429IGT6(带以太网MAC)和LAN8720为例,详解通过CubeMX配置以太网和LwIP的步骤。

3.1 硬件准备

  • 开发板:STM32F429 Discovery或自制板(需带以太网接口);
  • PHY模块:LAN8720(带RMII接口和网络变压器);
  • 软件:STM32CubeMX 6.6.0 + Keil MDK 5.36;
  • 工具:网线(连接开发板与路由器/PC)、串口调试助手(查看日志)。

3.2 CubeMX配置步骤

步骤1:新建工程,选择芯片

打开CubeMX,搜索"STM32F429IGT6",创建新工程。

步骤2:配置系统时钟

以太网MAC需要特定的时钟源(ETH_CLK),配置步骤:

  1. 配置RCC:HSE选择"Crystal/Ceramic Resonator"(8MHz);
  2. 配置PLL:
    • PLL_M = 8,PLL_N = 336,PLL_P = 2 → 系统时钟=8×336/2=1344/2=168MHz;
    • PLL_Q = 7 → 使USB_OTG_FS时钟=168/7=24MHz(不影响以太网,但需配置);
  3. 以太网时钟:ETH_CLK由PLL输出,需确保HSE使能,且PLL48CLK(用于PHY时钟)正确。
步骤3:配置以太网外设
  1. 引脚配置:

    • 点击"Connectivity"→"ETH",选择"RMII"模式;
    • 自动分配引脚(或手动指定):
      • ETH_RMII_REF_CLK:PA1(或PHY提供的50MHz时钟,如PB1);
      • ETH_RMII_CRS_DV:PA7;
      • ETH_RMII_RXD0:PC4;
      • ETH_RMII_RXD1:PC5;
      • ETH_RMII_TX_EN:PB11;
      • ETH_RMII_TXD0:PB12;
      • ETH_RMII_TXD1:PB13;
      • ETH_MDIO:PA2;
      • ETH_MDC:PC1;
    • 配置PHY复位引脚:如PA8(输出模式,用于复位LAN8720)。
  2. MAC配置:

    • 模式:“Full-Duplex”(全双工);
    • 速率:“100Mbps”;
    • 自动协商:使能(Auto-negotiation);
    • DMA配置:使能"ETH DMA TX/RX Interrupt"(DMA中断)。
步骤4:配置LwIP协议栈
  1. 点击"Middleware"→"LwIP",启用LwIP:

    • 模式:“Standalone”(无OS)或"With RTOS"(如FreeRTOS,推荐后者);
    • 勾选"Enable LwIP Debug"(调试日志,可选)。
  2. 配置IP参数:

    • 选择"DHCP"(自动获取IP)或"Static"(静态IP,如192.168.1.100);
    • 静态IP示例:
      • IP地址:192.168.1.100;
      • 子网掩码:255.255.255.0;
      • 网关:192.168.1.1(路由器IP)。
  3. 配置协议:

    • 勾选"TCP"、“UDP”、“ICMP”(支持ping);
    • 其他参数保持默认(如TCP窗口大小、超时重传次数)。
步骤5:配置FreeRTOS(可选,推荐)

为提高实时性和多任务处理能力,建议配合FreeRTOS:

  1. 点击"Middleware"→"FreeRTOS",选择"CMSIS_V1"或"CMSIS_V2";
  2. 创建任务:如"eth_task"(处理以太网通信)、“app_task”(应用逻辑)。
步骤6:生成代码

设置工程路径和IDE(Keil),点击"Generate Code"生成初始化代码。

3.3 生成代码结构解析

CubeMX生成的以太网和LwIP代码主要位于以下文件:

文件路径 功能描述
Core/Src/eth.c 以太网MAC和PHY初始化驱动
Core/Src/lwip.c LwIP协议栈初始化
Core/Src/lwip_app.c LwIP应用层示例(TCP/UDP)
Middlewares/Third_Party/LwIP/src LwIP协议栈核心代码(IP/TCP/UDP等)
Middlewares/Third_Party/LwIP/system STM32适配层(网卡驱动对接)

核心初始化流程:

  1. MX_ETH_Init():初始化以太网MAC和PHY;
  2. MX_LWIP_Init():初始化LwIP协议栈(IP、TCP、UDP等);
  3. ethernetif_init():初始化网络接口(绑定MAC与LwIP);
  4. 启动LwIP主循环(lwip_periodic_handle()):处理超时、ARP缓存等。

四、LwIP通信实战:TCP与UDP的实现

4.1 TCP通信:可靠的数据传输

TCP(Transmission Control Protocol)是面向连接的可靠协议,适用于对数据完整性要求高的场景(如文件传输、控制指令)。

(1)TCP服务器:等待客户端连接并收发数据

实现一个TCP服务器,端口号为8080,流程:

  1. 创建TCP监听套接字(socket);
  2. 绑定IP和端口(bind);
  3. 监听连接(listen);
  4. 接受客户端连接(accept);
  5. 与客户端收发数据(recv/send)。

代码示例(FreeRTOS任务中)

#include "lwip/sockets.h"
#include <string.h>

#define TCP_SERVER_PORT 8080
#define MAX_TCP_BUF_LEN 1024

void tcp_server_task(void const *argument)
{
  int server_fd, new_socket;
  struct sockaddr_in address;
  int addrlen = sizeof(address);
  char buffer[MAX_TCP_BUF_LEN] = {0};

  // 1. 创建TCP套接字(IPv4,流式套接字)
  if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
  {
    printf("TCP socket创建失败\r\n");
    vTaskDelete(NULL);
  }

  // 2. 配置服务器地址
  address.sin_family = AF_INET;
  address.sin_addr.s_addr = INADDR_ANY; // 监听所有本地IP
  address.sin_port = htons(TCP_SERVER_PORT); // 端口号(主机字节序转网络字节序)

  // 3. 绑定套接字与地址
  if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0)
  {
    printf("TCP bind失败\r\n");
    closesocket(server_fd);
    vTaskDelete(NULL);
  }

  // 4. 监听连接(最大等待队列长度为5)
  if (listen(server_fd, 5) < 0)
  {
    printf("TCP listen失败\r\n");
    closesocket(server_fd);
    vTaskDelete(NULL);
  }

  printf("TCP服务器启动,端口:%d,等待连接...\r\n", TCP_SERVER_PORT);

  while (1)
  {
    // 5. 接受客户端连接(阻塞等待)
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0)
    {
      printf("TCP accept失败\r\n");
      continue;
    }

    printf("客户端已连接,IP:%s,端口:%d\r\n", 
           inet_ntoa(address.sin_addr), ntohs(address.sin_port));

    // 6. 与客户端通信
    while (1)
    {
      // 接收客户端数据
      int recv_len = recv(new_socket, buffer, MAX_TCP_BUF_LEN-1, 0);
      if (recv_len <= 0)
      {
        printf("客户端断开连接\r\n");
        closesocket(new_socket);
        break;
      }
      buffer[recv_len] = '\0';
      printf("收到TCP数据:%s\r\n", buffer);

      // 发送响应数据
      char *resp = "收到数据:";
      send(new_socket, resp, strlen(resp), 0);
      send(new_socket, buffer, recv_len, 0);
    }
  }
}
(2)TCP客户端:主动连接服务器并通信

TCP客户端主动连接服务器,流程:

  1. 创建TCP套接字;
  2. 配置服务器IP和端口;
  3. 连接服务器(connect);
  4. 收发数据(send/recv)。

代码示例

#define TCP_SERVER_IP "192.168.1.101" // 服务器IP
#define TCP_CLIENT_PORT 8080

void tcp_client_task(void const *argument)
{
  int sockfd;
  struct sockaddr_in serv_addr;
  char buffer[MAX_TCP_BUF_LEN] = {0};

  while (1)
  {
    // 1. 创建TCP套接字
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
      printf("TCP客户端socket创建失败\r\n");
      vTaskDelay(1000);
      continue;
    }

    // 2. 配置服务器地址
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(TCP_CLIENT_PORT);
    // 将字符串IP转为网络字节序
    if (inet_pton(AF_INET, TCP_SERVER_IP, &serv_addr.sin_addr) <= 0)
    {
      printf("无效的服务器IP\r\n");
      closesocket(sockfd);
      vTaskDelay(1000);
      continue;
    }

    // 3. 连接服务器
    if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
    {
      printf("连接TCP服务器失败\r\n");
      closesocket(sockfd);
      vTaskDelay(1000);
      continue;
    }

    printf("已连接到TCP服务器:%s:%d\r\n", TCP_SERVER_IP, TCP_CLIENT_PORT);

    // 4. 发送数据
    char *msg = "Hello TCP Server!";
    send(sockfd, msg, strlen(msg), 0);
    printf("发送TCP数据:%s\r\n", msg);

    // 5. 接收响应
    int recv_len = recv(sockfd, buffer, MAX_TCP_BUF_LEN-1, 0);
    if (recv_len > 0)
    {
      buffer[recv_len] = '\0';
      printf("收到服务器响应:%s\r\n", buffer);
    }

    // 6. 关闭连接(实际应用可保持连接)
    closesocket(sockfd);
    vTaskDelay(5000); // 5秒后重新连接
  }
}

4.2 UDP通信:无连接的快速传输

UDP(User Datagram Protocol)是无连接的不可靠协议,适用于对实时性要求高、可容忍少量丢包的场景(如视频流、传感器数据)。

(1)UDP服务器:绑定端口并接收数据

UDP服务器流程:

  1. 创建UDP套接字;
  2. 绑定IP和端口;
  3. 接收数据(recvfrom,同时获取发送方地址);
  4. 发送响应(sendto)。

代码示例

#define UDP_SERVER_PORT 8081
#define MAX_UDP_BUF_LEN 1024

void udp_server_task(void const *argument)
{
  int sockfd;
  struct sockaddr_in serv_addr, cli_addr;
  int len = sizeof(cli_addr);
  char buffer[MAX_UDP_BUF_LEN] = {0};

  // 1. 创建UDP套接字
  if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
  {
    printf("UDP socket创建失败\r\n");
    vTaskDelete(NULL);
  }

  // 2. 配置服务器地址
  memset(&serv_addr, 0, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_addr.s_addr = INADDR_ANY;
  serv_addr.sin_port = htons(UDP_SERVER_PORT);

  // 3. 绑定端口
  if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
  {
    printf("UDP bind失败\r\n");
    closesocket(sockfd);
    vTaskDelete(NULL);
  }

  printf("UDP服务器启动,端口:%d\r\n", UDP_SERVER_PORT);

  while (1)
  {
    // 4. 接收数据(获取发送方地址)
    int recv_len = recvfrom(sockfd, buffer, MAX_UDP_BUF_LEN-1, 0, 
                           (struct sockaddr *)&cli_addr, (socklen_t*)&len);
    if (recv_len < 0)
    {
      printf("UDP接收失败\r\n");
      continue;
    }
    buffer[recv_len] = '\0';
    printf("收到UDP数据(来自%s:%d):%s\r\n",
           inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buffer);

    // 5. 发送响应
    char *resp = "收到UDP数据";
    sendto(sockfd, resp, strlen(resp), 0, 
           (struct sockaddr *)&cli_addr, len);
  }
}
(2)UDP客户端:发送数据到目标地址

UDP客户端无需连接,直接发送数据:

  1. 创建UDP套接字;
  2. 配置目标服务器地址;
  3. 发送数据(sendto);
  4. 接收响应(recvfrom)。

代码示例

#define UDP_SERVER_IP "192.168.1.101"
#define UDP_CLIENT_PORT 8081

void udp_client_task(void const *argument)
{
  int sockfd;
  struct sockaddr_in serv_addr;
  char buffer[MAX_UDP_BUF_LEN] = {0};

  // 1. 创建UDP套接字
  if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
  {
    printf("UDP客户端socket创建失败\r\n");
    vTaskDelete(NULL);
  }

  // 2. 配置服务器地址
  memset(&serv_addr, 0, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(UDP_CLIENT_PORT);
  inet_pton(AF_INET, UDP_SERVER_IP, &serv_addr.sin_addr);

  while (1)
  {
    // 3. 发送数据
    char *msg = "Hello UDP Server!";
    sendto(sockfd, msg, strlen(msg), 0, 
           (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    printf("发送UDP数据到%s:%d:%s\r\n", UDP_SERVER_IP, UDP_CLIENT_PORT, msg);

    // 4. 接收响应(超时等待1秒)
    struct timeval tv;
    tv.tv_sec = 1;
    tv.tv_usec = 0;
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

    int len = sizeof(serv_addr);
    int recv_len = recvfrom(sockfd, buffer, MAX_UDP_BUF_LEN-1, 0, 
                           (struct sockaddr *)&serv_addr, (socklen_t*)&len);
    if (recv_len > 0)
    {
      buffer[recv_len] = '\0';
      printf("收到UDP响应:%s\r\n", buffer);
    }

    vTaskDelay(2000); // 2秒发送一次
  }
}

五、实战案例:网络摄像头(通过UDP传输图像)

网络摄像头是以太网的典型应用,流程:

  1. 摄像头采集图像(如OV7670);
  2. 图像数据压缩(如JPEG,减少数据量);
  3. 通过UDP协议发送到PC端;
  4. PC端软件(如VLC、Python脚本)接收并显示。

5.1 硬件与软件准备

OV7670

  • 摄像头:OV7670(VGA分辨率640×480,支持JPEG输出);
  • 接口:OV7670通过DCMI(数字摄像头接口)连接STM32F429;
  • PC工具:Python脚本(用socket接收UDP数据并显示)。

5.2 图像采集与传输流程

(1)初始化摄像头与DCMI

通过CubeMX配置DCMI接口,初始化OV7670为JPEG模式:

void MX_DCMI_Init(void)
{
  hdcmi.Instance = DCMI;
  hdcmi.Init.SynchroMode = DCMI_SYNCHRO_HARDWARE; // 硬件同步
  hdcmi.Init.PCKPolarity = DCMI_PCKPOLARITY_RISING;
  hdcmi.Init.VSPolarity = DCMI_VSPOLARITY_HIGH;
  hdcmi.Init.HSPolarity = DCMI_HSPOLARITY_HIGH;
  hdcmi.Init.CaptureRate = DCMI_CR_ALL_FRAME; // 捕获所有帧
  hdcmi.Init.ExtendedDataMode = DCMI_EXTEND_DATA_8B; // 8位数据
  if (HAL_DCMI_Init(&hdcmi) != HAL_OK)
  {
    Error_Handler();
  }
}

// 初始化OV7670为JPEG模式(具体配置需参考摄像头 datasheet)
void ov7670_init(void)
{
  // 复位摄像头
  HAL_GPIO_WritePin(OV7670_RST_GPIO_Port, OV7670_RST_Pin, GPIO_PIN_RESET);
  HAL_Delay(100);
  HAL_GPIO_WritePin(OV7670_RST_GPIO_Port, OV7670_RST_Pin, GPIO_PIN_SET);
  
  // 配置寄存器:设置分辨率为QVGA(320×240)、JPEG格式
  ov7670_write_reg(0x12, 0x04); // 复位
  HAL_Delay(10);
  ov7670_write_reg(0x11, 0x00); // 输出格式:RGB565(后续转为JPEG)
  // ... 其他寄存器配置(略)
}
(2)UDP图像传输任务

采集JPEG数据并通过UDP发送:

#define CAMERA_UDP_PORT 5000
#define JPEG_BUF_SIZE 32768 // 32KB缓冲区

uint8_t jpeg_buf[JPEG_BUF_SIZE];
uint32_t jpeg_len = 0;
int udp_cam_sockfd;
struct sockaddr_in cam_serv_addr;

// 初始化UDP发送套接字
void udp_camera_init(void)
{
  // 创建UDP套接字
  if ((udp_cam_sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
  {
    printf("摄像头UDP socket创建失败\r\n");
    return;
  }

  // 配置PC端地址(PC的IP和端口)
  memset(&cam_serv_addr, 0, sizeof(cam_serv_addr));
  cam_serv_addr.sin_family = AF_INET;
  cam_serv_addr.sin_port = htons(CAMERA_UDP_PORT);
  inet_pton(AF_INET, "192.168.1.102", &cam_serv_addr.sin_addr); // PC的IP
}

// DCMI回调函数:接收摄像头数据
void HAL_DCMI_FrameEventCallback(DCMI_HandleTypeDef *hdcmihandle)
{
  // 一帧数据采集完成,标记长度
  jpeg_len = JPEG_BUF_SIZE; // 实际长度需根据摄像头输出调整
}

// 图像传输任务
void camera_transfer_task(void const *argument)
{
  udp_camera_init();
  ov7670_init();
  MX_DCMI_Init();

  // 启动DCMI DMA采集(循环模式)
  HAL_DCMI_Start_DMA(&hdcmihandle, DCMI_MODE_CONTINUOUS, 
                    (uint32_t)jpeg_buf, JPEG_BUF_SIZE/4);

  while (1)
  {
    if (jpeg_len > 0)
    {
      // 发送JPEG数据(分块发送,避免超过UDP最大包长)
      uint32_t sent = 0;
      while (sent < jpeg_len)
      {
        uint32_t send_len = (jpeg_len - sent) > 1400 ? 1400 : (jpeg_len - sent);
        sendto(udp_cam_sockfd, &jpeg_buf[sent], send_len, 0,
               (struct sockaddr *)&cam_serv_addr, sizeof(cam_serv_addr));
        sent += send_len;
        vTaskDelay(1); // 避免网络拥塞
      }
      printf("发送一帧图像,长度:%d字节\r\n", jpeg_len);
      jpeg_len = 0; // 重置
    }
    vTaskDelay(100);
  }
}
(3)PC端Python接收与显示

用Python的socket和OpenCV接收并显示图像:

import socket
import cv2
import numpy as np

UDP_IP = "0.0.0.0"  # 监听所有IP
UDP_PORT = 5000
BUF_SIZE = 1400

# 创建UDP套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))

print(f"等待图像数据...(端口:{UDP_PORT})")

frame_data = b''
while True:
    data, addr = sock.recvfrom(BUF_SIZE)
    if not data:
        continue
    frame_data += data
    
    # 简单判断:JPEG结束标志为0xFFD9
    if b'\xff\xd9' in frame_data:
        # 转换为图像并显示
        nparr = np.frombuffer(frame_data, np.uint8)
        img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
        if img is not None:
            cv2.imshow('STM32 Camera', img)
            if cv2.waitKey(1) & 0xFF == ord('q'):
                break
        frame_data = b''  # 重置缓冲区

cv2.destroyAllWindows()
sock.close()

5.3 测试结果

STM32采集OV7670的JPEG图像,通过UDP发送到PC,Python脚本接收后实时显示,实现网络摄像头功能。实际应用中可优化:

  • 增加帧头帧尾(如标记帧长度),避免数据粘连;
  • 降低分辨率(如QVGA)或压缩率,减少带宽占用;
  • 实现多客户端连接(通过维护客户端地址列表)。

六、常见问题与调试技巧

6.1 以太网初始化失败(PHY无法识别)

现象MX_ETH_Init()返回错误,HAL_ETH_Init()失败。

原因

  • PHY地址错误(LAN8720默认地址为0或1,由引脚A0/A1决定);
  • MDIO接口连接错误(ETH_MDIO和ETH_MDC接反);
  • PHY未复位或复位时间不足;
  • 电源问题(PHY未供电或电压不稳)。

解决方案

  1. 检查PHY地址:通过HAL_ETH_ReadPHYRegister()读取PHY ID(如LAN8720的ID为0x0007C0F1),确认地址正确;
  2. 用示波器测量MDIO和MDC信号,确认有波形(MDC为50MHz以下时钟,MDIO为数据);
  3. 延长PHY复位时间(至少10ms);
  4. 测量PHY的3.3V供电,确保稳定。

6.2 能ping通但TCP/UDP无法通信

现象:PC能ping通STM32,但socket连接失败或数据收发异常。

原因

  • 防火墙拦截(PC防火墙阻止了目标端口);
  • IP地址冲突(多个设备使用同一IP);
  • 端口被占用(LwIP未正确释放套接字);
  • 数据长度超过MTU(默认1500字节,UDP包过大需分片)。

解决方案

  1. 关闭PC防火墙或添加端口例外;
  2. 用arp -a命令查看IP与MAC绑定,确认无冲突;
  3. 确保closesocket正确调用,释放资源;
  4. 限制UDP包大小(建议≤1400字节,避免分片)。

6.3 图像传输卡顿或花屏

现象:PC接收的图像卡顿、有撕裂或花屏。

原因

  • 网络带宽不足(图像分辨率过高);
  • 摄像头采集速度慢于传输速度;
  • UDP丢包(未处理网络拥塞);
  • 数据缓冲区溢出。

解决方案

  1. 降低图像分辨率(如320×240)或帧率(如10fps);
  2. 用DMA双缓冲采集摄像头数据,避免缓冲区溢出;
  3. 实现简单的流量控制(如接收方反馈丢包率,动态调整发送速率);
  4. 在PC端增加数据校验(如CRC),丢弃错误帧。

七、总结与扩展学习

本文详细讲解了STM32以太网开发的核心流程:从MAC+PHY硬件原理,到LwIP协议栈配置,再到TCP/UDP通信实现和网络摄像头案例,核心要点:

  • STM32以太网需要MAC(内部)和PHY(外部)配合,RMII接口是简化设计的首选;
  • LwIP协议栈通过CubeMX可快速集成,提供Socket API简化TCP/UDP开发;
  • TCP适合可靠通信,UDP适合实时传输,需根据场景选择;
  • 网络摄像头等大数据量应用需注意带宽控制和数据分片。

扩展学习方向

  1. HTTP服务器:基于LwIP实现Web服务器,通过浏览器控制设备;
  2. MQTT协议:实现物联网设备与云平台通信(如连接阿里云、MQTT.fx);
  3. 网络诊断工具:实现ICMP(ping)、DHCP客户端、DNS解析等功能;
  4. 以太网唤醒(WoL):通过网络远程唤醒STM32(需PHY支持)。

STM32以太网开发是嵌入式设备联网的基础,掌握LwIP协议栈的使用,能为工业物联网、智能家居等领域的开发打开大门。建议结合实际硬件多做测试,尤其是网络异常场景的处理,才能开发出稳定可靠的以太网应用。


网站公告

今日签到

点亮在社区的每一天
去签到