前言

最近在做一个嵌入式项目,需要让STM32通过以太网实现数据通信。本来打算直接用lwIP,但考虑到项目对资源要求比较严格,而且功能相对简单,就萌生了自己写一个精简网络协议栈的想法。经过两周的折腾,总算把这个轮子造出来了,虽然功能不如lwIP全面,但胜在轻量够用。

这篇文章会详细记录整个实现过程,包括协议栈的分层设计、关键代码实现,以及我踩过的一些坑。代码基于STM32F407,使用的网卡芯片是ENC28J60,如果你用的是其他平台,核心思想也是通用的。

一、网络协议栈基础架构

1.1 为什么需要分层

网络协议栈采用分层设计并不是为了故意搞复杂,而是有实际意义的。每一层只关注自己的职责,上层不用管下层怎么传输,下层也不用管上层传什么数据。这种解耦设计让代码维护起来清晰很多。

我们实现的协议栈包含四层:

+-------------------+
|   应用层 (APP)     |  <- 用户数据
+-------------------+
|   传输层 (UDP)     |  <- 端口、校验和
+-------------------+
|   网络层 (IP)      |  <- IP地址、路由
+-------------------+
| 数据链路层 (MAC)   |  <- 物理地址、帧封装
+-------------------+
|   物理层 (PHY)     |  <- ENC28J60驱动
+-------------------+

1.2 数据封装过程

当应用层要发送数据时,数据会像穿衣服一样被一层层包装:

  1. 应用数据: “Hello”
  2. 加UDP头: [UDP头] + “Hello”
  3. 加IP头: [IP头] + [UDP头] + “Hello”
  4. 加MAC头: [MAC头] + [IP头] + [UDP头] + “Hello”

接收时正好相反,一层层拆包。每一层只关心自己的头部,剩下的数据直接传给上层处理。

二、硬件准备与SPI驱动

2.1 硬件连接

ENC28J60通过SPI与STM32通信,我的接线如下:

ENC28J60        STM32F407
---------       ---------
VCC     ----    3.3V
GND     ----    GND
CS      ----    PA4
SCK     ----    PA5
MOSI    ----    PA7
MISO    ----    PA6
RST     ----    PC4
INT     ----    PC5 (可选)

2.2 ENC28J60底层驱动

先实现基本的SPI读写函数:

// enc28j60.h
#ifndef __ENC28J60_H
#define __ENC28J60_H

#include "stm32f4xx.h"

// ENC28J60寄存器定义
#define ERDPTL      0x00
#define ERDPTH      0x01
#define EWRPTL      0x02
#define EWRPTH      0x03
#define ETXSTL      0x04
#define ETXSTH      0x05
#define ETXNDL      0x06
#define ETXNDH      0x07
#define ERXSTL      0x08
#define ERXSTH      0x09
#define ERXNDL      0x0A
#define ERXNDH      0x0B
#define ERXRDPTL    0x0C
#define ERXRDPTH    0x0D

// Bank 0寄存器
#define EIE         0x1B
#define EIR         0x1C
#define ESTAT       0x1D
#define ECON2       0x1E
#define ECON1       0x1F

// Bank 2寄存器
#define MACON1      0x00
#define MACON3      0x02
#define MACON4      0x03
#define MABBIPG     0x04
#define MAIPGL      0x06
#define MAIPGH      0x07
#define MAMXFLL     0x0A
#define MAMXFLH     0x0B

// Bank 3寄存器
#define MAADR1      0x00
#define MAADR2      0x01
#define MAADR3      0x02
#define MAADR4      0x03
#define MAADR5      0x04
#define MAADR6      0x05
#define MISTAT      0x0A
#define EREVID      0x12

// PHY寄存器
#define PHCON1      0x00
#define PHSTAT1     0x01
#define PHCON2      0x10
#define PHSTAT2     0x11

// ENC28J60操作码
#define ENC28J60_READ_CTRL_REG  0x00
#define ENC28J60_WRITE_CTRL_REG 0x40
#define ENC28J60_BIT_FIELD_SET  0x80
#define ENC28J60_BIT_FIELD_CLR  0xA0
#define ENC28J60_READ_BUF_MEM   0x3A
#define ENC28J60_WRITE_BUF_MEM  0x7A
#define ENC28J60_SOFT_RESET     0xFF

// 缓冲区定义
#define RXSTART_INIT    0x0000
#define RXSTOP_INIT     0x0BFF
#define TXSTART_INIT    0x0C00
#define TXSTOP_INIT     0x11FF
#define MAX_FRAMELEN    1518

void ENC28J60_Init(uint8_t *macaddr);
void ENC28J60_PacketSend(uint16_t len, uint8_t* packet);
uint16_t ENC28J60_PacketReceive(uint16_t maxlen, uint8_t* packet);

#endif
// enc28j60.c
#include "enc28j60.h"
#include "spi.h"
#include <string.h>

static uint8_t Enc28j60Bank;
static uint16_t NextPacketPtr;

#define ENC28J60_CS_LOW()   HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define ENC28J60_CS_HIGH()  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)

// SPI读写基础函数
static uint8_t SPI_ReadWrite(uint8_t data)
{
    uint8_t rx_data;
    HAL_SPI_TransmitReceive(&hspi1, &data, &rx_data, 1, 100);
    return rx_data;
}

// 读取控制寄存器
static uint8_t ENC28J60_ReadOp(uint8_t op, uint8_t address)
{
    uint8_t dat = 0;
    ENC28J60_CS_LOW();
    SPI_ReadWrite(op | (address & 0x1F));
    dat = SPI_ReadWrite(0xFF);
    // MAC和MII寄存器需要dummy read
    if(address & 0x80)
        dat = SPI_ReadWrite(0xFF);
    ENC28J60_CS_HIGH();
    return dat;
}

// 写入控制寄存器
static void ENC28J60_WriteOp(uint8_t op, uint8_t address, uint8_t data)
{
    ENC28J60_CS_LOW();
    SPI_ReadWrite(op | (address & 0x1F));
    SPI_ReadWrite(data);
    ENC28J60_CS_HIGH();
}

// 读取控制寄存器
static uint8_t ENC28J60_ReadReg(uint8_t address)
{
    return ENC28J60_ReadOp(ENC28J60_READ_CTRL_REG, address);
}

// 写入控制寄存器
static void ENC28J60_WriteReg(uint8_t address, uint8_t data)
{
    ENC28J60_WriteOp(ENC28J60_WRITE_CTRL_REG, address, data);
}

// 设置寄存器位
static void ENC28J60_SetBit(uint8_t address, uint8_t data)
{
    ENC28J60_WriteOp(ENC28J60_BIT_FIELD_SET, address, data);
}

// 清除寄存器位
static void ENC28J60_ClrBit(uint8_t address, uint8_t data)
{
    ENC28J60_WriteOp(ENC28J60_BIT_FIELD_CLR, address, data);
}

// 切换寄存器Bank
static void ENC28J60_SetBank(uint8_t address)
{
    if((address & 0x60) != Enc28j60Bank) {
        ENC28J60_ClrBit(ECON1, 0x03);
        ENC28J60_SetBit(ECON1, (address & 0x60) >> 5);
        Enc28j60Bank = (address & 0x60);
    }
}

// 读PHY寄存器
static uint16_t ENC28J60_ReadPhy(uint8_t address)
{
    ENC28J60_SetBank(MISTAT);
    ENC28J60_WriteReg(MIREGADR, address);
    ENC28J60_WriteReg(MICMD, 0x01);
    while(ENC28J60_ReadReg(MISTAT) & 0x01);
    ENC28J60_WriteReg(MICMD, 0x00);
    return (ENC28J60_ReadReg(MIRDH) << 8) | ENC28J60_ReadReg(MIRDL);
}

// 写PHY寄存器
static void ENC28J60_WritePhy(uint8_t address, uint16_t data)
{
    ENC28J60_SetBank(MIREGADR);
    ENC28J60_WriteReg(MIREGADR, address);
    ENC28J60_WriteReg(MIWRL, data);
    ENC28J60_WriteReg(MIWRH, data >> 8);
    while(ENC28J60_ReadReg(MISTAT) & 0x01);
}

// 读取缓冲区数据
static void ENC28J60_ReadBuffer(uint16_t len, uint8_t* data)
{
    ENC28J60_CS_LOW();
    SPI_ReadWrite(ENC28J60_READ_BUF_MEM);
    while(len--) {
        *data++ = SPI_ReadWrite(0xFF);
    }
    ENC28J60_CS_HIGH();
}

// 写入缓冲区数据
static void ENC28J60_WriteBuffer(uint16_t len, uint8_t* data)
{
    ENC28J60_CS_LOW();
    SPI_ReadWrite(ENC28J60_WRITE_BUF_MEM);
    while(len--) {
        SPI_ReadWrite(*data++);
    }
    ENC28J60_CS_HIGH();
}

// 初始化ENC28J60
void ENC28J60_Init(uint8_t *macaddr)
{
    ENC28J60_CS_HIGH();
    HAL_Delay(100);
    
    // 软件复位
    ENC28J60_WriteOp(ENC28J60_SOFT_RESET, 0, ENC28J60_SOFT_RESET);
    HAL_Delay(50);
    
    // 等待时钟就绪
    while(!(ENC28J60_ReadReg(ESTAT) & 0x01));
    
    NextPacketPtr = RXSTART_INIT;
    
    // 设置接收缓冲区
    ENC28J60_SetBank(ERXSTL);
    ENC28J60_WriteReg(ERXSTL, RXSTART_INIT & 0xFF);
    ENC28J60_WriteReg(ERXSTH, RXSTART_INIT >> 8);
    ENC28J60_WriteReg(ERXRDPTL, RXSTART_INIT & 0xFF);
    ENC28J60_WriteReg(ERXRDPTH, RXSTART_INIT >> 8);
    ENC28J60_WriteReg(ERXNDL, RXSTOP_INIT & 0xFF);
    ENC28J60_WriteReg(ERXNDH, RXSTOP_INIT >> 8);
    
    // 设置发送缓冲区
    ENC28J60_WriteReg(ETXSTL, TXSTART_INIT & 0xFF);
    ENC28J60_WriteReg(ETXSTH, TXSTART_INIT >> 8);
    ENC28J60_WriteReg(ETXNDL, TXSTOP_INIT & 0xFF);
    ENC28J60_WriteReg(ETXNDH, TXSTOP_INIT >> 8);
    
    // 配置接收过滤器
    ENC28J60_SetBank(ERXFCON);
    ENC28J60_WriteReg(ERXFCON, 0xA1);  // 单播+广播+CRC校验
    
    // 配置MAC
    ENC28J60_SetBank(MACON1);
    ENC28J60_WriteReg(MACON1, 0x0D);   // 使能接收
    ENC28J60_WriteReg(MACON3, 0x32);   // 全双工+填充+CRC
    ENC28J60_WriteReg(MACON4, 0x40);
    ENC28J60_WriteReg(MAMXFLL, MAX_FRAMELEN & 0xFF);
    ENC28J60_WriteReg(MAMXFLH, MAX_FRAMELEN >> 8);
    ENC28J60_WriteReg(MABBIPG, 0x15);  // 半双工间隙
    ENC28J60_WriteReg(MAIPGL, 0x12);
    ENC28J60_WriteReg(MAIPGH, 0x0C);
    
    // 设置MAC地址
    ENC28J60_SetBank(MAADR1);
    ENC28J60_WriteReg(MAADR1, macaddr[0]);
    ENC28J60_WriteReg(MAADR2, macaddr[1]);
    ENC28J60_WriteReg(MAADR3, macaddr[2]);
    ENC28J60_WriteReg(MAADR4, macaddr[3]);
    ENC28J60_WriteReg(MAADR5, macaddr[4]);
    ENC28J60_WriteReg(MAADR6, macaddr[5]);
    
    // 配置PHY
    ENC28J60_WritePhy(PHCON1, 0x0100); // 全双工
    ENC28J60_WritePhy(PHCON2, 0x0100); // 禁止环回
    
    // 使能接收
    ENC28J60_SetBit(EIE, 0xC0);        // 使能中断
    ENC28J60_SetBit(ECON1, 0x04);      // 使能接收
}

// 发送数据包
void ENC28J60_PacketSend(uint16_t len, uint8_t* packet)
{
    // 等待上一次发送完成
    while(ENC28J60_ReadOp(ENC28J60_READ_CTRL_REG, ECON1) & 0x08);
    
    // 设置写指针
    ENC28J60_SetBank(EWRPTL);
    ENC28J60_WriteReg(EWRPTL, TXSTART_INIT & 0xFF);
    ENC28J60_WriteReg(EWRPTH, TXSTART_INIT >> 8);
    
    // 设置发送结束指针
    ENC28J60_WriteReg(ETXNDL, (TXSTART_INIT + len) & 0xFF);
    ENC28J60_WriteReg(ETXNDH, (TXSTART_INIT + len) >> 8);
    
    // 写入控制字节
    uint8_t ctrl = 0x00;
    ENC28J60_WriteBuffer(1, &ctrl);
    
    // 写入数据
    ENC28J60_WriteBuffer(len, packet);
    
    // 开始发送
    ENC28J60_SetBit(ECON1, 0x08);
}

// 接收数据包
uint16_t ENC28J60_PacketReceive(uint16_t maxlen, uint8_t* packet)
{
    uint16_t rxstat;
    uint16_t len;
    
    // 检查是否有数据包
    if(ENC28J60_ReadReg(EPKTCNT) == 0)
        return 0;
    
    // 设置读指针
    ENC28J60_SetBank(ERDPTL);
    ENC28J60_WriteReg(ERDPTL, NextPacketPtr & 0xFF);
    ENC28J60_WriteReg(ERDPTH, NextPacketPtr >> 8);
    
    // 读取包头
    uint8_t header[6];
    ENC28J60_ReadBuffer(6, header);
    
    NextPacketPtr = header[0] | (header[1] << 8);
    len = header[2] | (header[3] << 8);
    rxstat = header[4] | (header[5] << 8);
    
    // 去除CRC
    len -= 4;
    
    // 读取数据
    if(len > maxlen)
        len = maxlen;
    ENC28J60_ReadBuffer(len, packet);
    
    // 更新ERXRDPT
    ENC28J60_WriteReg(ERXRDPTL, NextPacketPtr & 0xFF);
    ENC28J60_WriteReg(ERXRDPTH, NextPacketPtr >> 8);
    
    // 减少包计数
    ENC28J60_SetBit(ECON2, 0x40);
    
    return len;
}

三、数据链路层实现(MAC/Ethernet)

3.1 以太网帧结构

以太网帧格式如下:

+----------------+----------------+--------+----------+-----+
| 目的MAC(6字节) | 源MAC(6字节)   | 类型(2) | 数据     | CRC |
+----------------+----------------+--------+----------+-----+

类型字段:

  • 0x0800: IPv4
  • 0x0806: ARP
  • 0x86DD: IPv6

3.2 MAC层代码实现

// ethernet.h
#ifndef __ETHERNET_H
#define __ETHERNET_H

#include <stdint.h>

#define ETH_TYPE_ARP    0x0806
#define ETH_TYPE_IP     0x0800

// 以太网帧头
typedef struct {
    uint8_t dest_mac[6];
    uint8_t src_mac[6];
    uint16_t type;
} __attribute__((packed)) eth_header_t;

// MAC地址
extern uint8_t local_mac[6];
extern uint8_t broadcast_mac[6];

void ethernet_init(void);
void ethernet_send(uint8_t *dest_mac, uint16_t type, uint8_t *data, uint16_t len);
void ethernet_input(uint8_t *frame, uint16_t len);

#endif
// ethernet.c
#include "ethernet.h"
#include "enc28j60.h"
#include "arp.h"
#include "ip.h"
#include <string.h>

uint8_t local_mac[6] = {0x00, 0x04, 0xA3, 0x12, 0x34, 0x56};
uint8_t broadcast_mac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

static uint8_t tx_buffer[1024];

void ethernet_init(void)
{
    ENC28J60_Init(local_mac);
}

// 发送以太网帧
void ethernet_send(uint8_t *dest_mac, uint16_t type, uint8_t *data, uint16_t len)
{
    eth_header_t *eth = (eth_header_t *)tx_buffer;
    
    memcpy(eth->dest_mac, dest_mac, 6);
    memcpy(eth->src_mac, local_mac, 6);
    eth->type = htons(type);
    
    memcpy(tx_buffer + sizeof(eth_header_t), data, len);
    
    ENC28J60_PacketSend(sizeof(eth_header_t) + len, tx_buffer);
}

// 处理接收到的以太网帧
void ethernet_input(uint8_t *frame, uint16_t len)
{
    if(len < sizeof(eth_header_t))
        return;
    
    eth_header_t *eth = (eth_header_t *)frame;
    uint16_t type = ntohs(eth->type);
    uint8_t *payload = frame + sizeof(eth_header_t);
    uint16_t payload_len = len - sizeof(eth_header_t);
    
    // 检查是否是发给我们的
    if(memcmp(eth->dest_mac, local_mac, 6) != 0 && 
       memcmp(eth->dest_mac, broadcast_mac, 6) != 0)
        return;
    
    // 根据类型分发
    switch(type) {
        case ETH_TYPE_ARP:
            arp_input(payload, payload_len);
            break;
        case ETH_TYPE_IP:
            ip_input(payload, payload_len);
            break;
        default:
            break;
    }
}

四、网络层实现(IP + ARP)

4.1 ARP协议实现

ARP用于IP地址和MAC地址的映射,这是必须实现的。

// arp.h
#ifndef __ARP_H
#define __ARP_H

#include <stdint.h>

#define ARP_HARDWARE_ETH    1
#define ARP_PROTOCOL_IP     0x0800
#define ARP_OP_REQUEST      1
#define ARP_OP_REPLY        2

typedef struct {
    uint16_t hw_type;
    uint16_t proto_type;
    uint8_t hw_len;
    uint8_t proto_len;
    uint16_t opcode;
    uint8_t sender_mac[6];
    uint32_t sender_ip;
    uint8_t target_mac[6];
    uint32_t target_ip;
} __attribute__((packed)) arp_packet_t;

// ARP缓存表项
typedef struct {
    uint32_t ip;
    uint8_t mac[6];
    uint32_t timestamp;
} arp_entry_t;

void arp_init(void);
void arp_input(uint8_t *data, uint16_t len);
uint8_t* arp_lookup(uint32_t ip);
void arp_request(uint32_t target_ip);

#endif
// arp.c
#include "arp.h"
#include "ethernet.h"
#include "ip.h"
#include <string.h>

#define ARP_CACHE_SIZE  10
#define ARP_TIMEOUT     300  // 5分钟

static arp_entry_t arp_cache[ARP_CACHE_SIZE];

void arp_init(void)
{
    memset(arp_cache, 0, sizeof(arp_cache));
}

// ARP缓存查找
uint8_t* arp_lookup(uint32_t ip)
{
    for(int i = 0; i < ARP_CACHE_SIZE; i++) {
        if(arp_cache[i].ip == ip) {
            return arp_cache[i].mac;
        }
    }
    return NULL;
}

// 添加ARP缓存
static void arp_cache_add(uint32_t ip, uint8_t *mac)
{
    // 查找空闲位置或最老的条目
    int oldest = 0;
    for(int i = 0; i < ARP_CACHE_SIZE; i++) {
        if(arp_cache[i].ip == 0) {
            oldest = i;
            break;
        }
        if(arp_cache[i].timestamp < arp_cache[oldest].timestamp) {
            oldest = i;
        }
    }
    
    arp_cache[oldest].ip = ip;
    memcpy(arp_cache[oldest].mac, mac, 6);
    arp_cache[oldest].timestamp = HAL_GetTick() / 1000;
}

// 发送ARP请求
void arp_request(uint32_t target_ip)
{
    arp_packet_t arp;
    
    arp.hw_type = htons(ARP_HARDWARE_ETH);
    arp.proto_type = htons(ARP_PROTOCOL_IP);
    arp.hw_len = 6;
    arp.proto_len = 4;
    arp.opcode = htons(ARP_OP_REQUEST);
    
    memcpy(arp.sender_mac, local_mac, 6);
    arp.sender_ip = local_ip;
    memset(arp.target_mac, 0, 6);
    arp.target_ip = target_ip;
    
    ethernet_send(broadcast_mac, ETH_TYPE_ARP, (uint8_t*)&arp, sizeof(arp));
}

// 处理ARP数据包
void arp_input(uint8_t *data, uint16_t len)
{
    if(len < sizeof(arp_packet_t))
        return;
    
    arp_packet_t *arp = (arp_packet_t *)data;
    
    // 检查是否是以太网和IP
    if(ntohs(arp->hw_type) != ARP_HARDWARE_ETH ||
       ntohs(arp->proto_type) != ARP_PROTOCOL_IP)
        return;
    
    // 更新ARP缓存
    arp_cache_add(arp->sender_ip, arp->sender_mac);
    
    uint16_t opcode = ntohs(arp->opcode);
    
    if(opcode == ARP_OP_REQUEST) {
        // 如果目标是我们,发送ARP应答
        if(arp->target_ip == local_ip) {
            arp_packet_t reply;
            
            reply.hw_type = htons(ARP_HARDWARE_ETH);
            reply.proto_type = htons(ARP_PROTOCOL_IP);
            reply.hw_len = 6;
            reply.proto_len = 4;
            reply.opcode = htons(ARP_OP_REPLY);
            
            memcpy(reply.sender_mac, local_mac, 6);
            reply.sender_ip = local_ip;
            memcpy(reply.target_mac, arp->sender_mac, 6);
            reply.target_ip = arp->sender_ip;
            
            ethernet_send(arp->sender_mac, ETH_TYPE_ARP, 
                         (uint8_t*)&reply, sizeof(reply));
        }
    }
}

4.2 IP层实现

// ip.h
#ifndef __IP_H
#define __IP_H

#include <stdint.h>

// IP头部
typedef struct {
    uint8_t version_ihl;      // 版本(4bit) + 首部长度(4bit)
    uint8_t tos;              // 服务类型
    uint16_t total_len;       // 总长度
    uint16_t id;              // 标识
    uint16_t flags_offset;    // 标志(3bit) + 片偏移(13bit)
    uint8_t ttl;              // 生存时间
    uint8_t protocol;         // 协议
    uint16_t checksum;        // 首部校验和
    uint32_t src_ip;          // 源IP
    uint32_t dest_ip;         // 目的IP
} __attribute__((packed)) ip_header_t;

#define IP_PROTO_ICMP   1
#define IP_PROTO_TCP    6
#define IP_PROTO_UDP    17

extern uint32_t local_ip;
extern uint32_t gateway_ip;
extern uint32_t netmask;

void ip_init(uint32_t ip, uint32_t gw, uint32_t mask);
void ip_input(uint8_t *data, uint16_t len);
void ip_send(uint32_t dest_ip, uint8_t protocol, uint8_t *data, uint16_t len);
uint16_t ip_checksum(uint8_t *data, uint16_t len);

// 字节序转换
#define htons(x) ((uint16_t)((((x) & 0x00FF) << 8) | (((x) & 0xFF00) >> 8)))
#define ntohs(x) htons(x)
#define htonl(x) ((uint32_t)((((x) & 0x000000FF) << 24) | \
                             (((x) & 0x0000FF00) << 8) | \
                             (((x) & 0x00FF0000) >> 8) | \
                             (((x) & 0xFF000000) >> 24)))
#define ntohl(x) htonl(x)

#endif
// ip.c
#include "ip.h"
#include "ethernet.h"
#include "arp.h"
#include "udp.h"
#include "icmp.h"
#include <string.h>

uint32_t local_ip = 0;
uint32_t gateway_ip = 0;
uint32_t netmask = 0;

static uint16_t ip_id = 0;

void ip_init(uint32_t ip, uint32_t gw, uint32_t mask)
{
    local_ip = ip;
    gateway_ip = gw;
    netmask = mask;
}

// 计算IP校验和
uint16_t ip_checksum(uint8_t *data, uint16_t len)
{
    uint32_t sum = 0;
    uint16_t *ptr = (uint16_t *)data;
    
    while(len > 1) {
        sum += *ptr++;
        len -= 2;
    }
    
    if(len == 1) {
        sum += *(uint8_t *)ptr;
    }
    
    while(sum >> 16) {
        sum = (sum & 0xFFFF) + (sum >> 16);
    }
    
    return ~sum;
}

// 发送IP数据包
void ip_send(uint32_t dest_ip, uint8_t protocol, uint8_t *data, uint16_t len)
{
    uint8_t buffer[1500];
    ip_header_t *ip = (ip_header_t *)buffer;
    
    // 填充IP头
    ip->version_ihl = 0x45;  // IPv4, 20字节头部
    ip->tos = 0;
    ip->total_len = htons(sizeof(ip_header_t) + len);
    ip->id = htons(ip_id++);
    ip->flags_offset = 0;
    ip->ttl = 64;
    ip->protocol = protocol;
    ip->checksum = 0;
    ip->src_ip = local_ip;
    ip->dest_ip = dest_ip;
    
    // 计算校验和
    ip->checksum = ip_checksum((uint8_t *)ip, sizeof(ip_header_t));
    
    // 拷贝数据
    memcpy(buffer + sizeof(ip_header_t), data, len);
    
    // 查找目标MAC地址
    uint32_t next_hop = dest_ip;
    if((dest_ip & netmask) != (local_ip & netmask)) {
        next_hop = gateway_ip;  // 不在同一网段,发给网关
    }
    
    uint8_t *dest_mac = arp_lookup(next_hop);
    if(dest_mac == NULL) {
        // 发送ARP请求
        arp_request(next_hop);
        // 实际应用中应该缓存这个包,等收到ARP应答再发送
        return;
    }
    
    ethernet_send(dest_mac, ETH_TYPE_IP, buffer, sizeof(ip_header_t) + len);
}

// 处理接收到的IP数据包
void ip_input(uint8_t *data, uint16_t len)
{
    if(len < sizeof(ip_header_t))
        return;
    
    ip_header_t *ip = (ip_header_t *)data;
    
    // 检查版本
    if((ip->version_ihl >> 4) != 4)
        return;
    
    // 检查目的IP
    if(ip->dest_ip != local_ip && ip->dest_ip != 0xFFFFFFFF)
        return;
    
    // 验证校验和
    uint16_t checksum = ip->checksum;
    ip->checksum = 0;
    if(ip_checksum((uint8_t *)ip, sizeof(ip_header_t)) != checksum)
        return;
    ip->checksum = checksum;
    
    // 获取数据部分
    uint8_t header_len = (ip->version_ihl & 0x0F) * 4;
    uint8_t *payload = data + header_len;
    uint16_t payload_len = ntohs(ip->total_len) - header_len;
    
    // 根据协议分发
    switch(ip->protocol) {
        case IP_PROTO_ICMP:
            icmp_input(ip->src_ip, payload, payload_len);
            break;
        case IP_PROTO_UDP:
            udp_input(ip->src_ip, payload, payload_len);
            break;
        default:
            break;
    }
}

五、传输层实现(UDP)

UDP比TCP简单很多,适合资源受限的嵌入式系统。

// udp.h
#ifndef __UDP_H
#define __UDP_H

#include <stdint.h>

typedef struct {
    uint16_t src_port;
    uint16_t dest_port;
    uint16_t length;
    uint16_t checksum;
} __attribute__((packed)) udp_header_t;

// UDP回调函数
typedef void (*udp_callback_t)(uint32_t src_ip, uint16_t src_port, 
                               uint8_t *data, uint16_t len);

void udp_init(void);
void udp_input(uint32_t src_ip, uint8_t *data, uint16_t len);
void udp_send(uint32_t dest_ip, uint16_t src_port, uint16_t dest_port, 
              uint8_t *data, uint16_t len);
void udp_register_handler(uint16_t port, udp_callback_t callback);

#endif
// udp.c
#include "udp.h"
#include "ip.h"
#include <string.h>

#define MAX_UDP_HANDLERS  5

typedef struct {
    uint16_t port;
    udp_callback_t callback;
} udp_handler_t;

static udp_handler_t udp_handlers[MAX_UDP_HANDLERS];

void udp_init(void)
{
    memset(udp_handlers, 0, sizeof(udp_handlers));
}

// 注册UDP端口处理函数
void udp_register_handler(uint16_t port, udp_callback_t callback)
{
    for(int i = 0; i < MAX_UDP_HANDLERS; i++) {
        if(udp_handlers[i].callback == NULL) {
            udp_handlers[i].port = port;
            udp_handlers[i].callback = callback;
            break;
        }
    }
}

// UDP校验和计算(包含伪首部)
static uint16_t udp_checksum(uint32_t src_ip, uint32_t dest_ip, 
                             uint8_t *udp_packet, uint16_t len)
{
    uint32_t sum = 0;
    
    // 伪首部
    sum += (src_ip >> 16) & 0xFFFF;
    sum += src_ip & 0xFFFF;
    sum += (dest_ip >> 16) & 0xFFFF;
    sum += dest_ip & 0xFFFF;
    sum += htons(IP_PROTO_UDP);
    sum += htons(len);
    
    // UDP数据
    uint16_t *ptr = (uint16_t *)udp_packet;
    while(len > 1) {
        sum += *ptr++;
        len -= 2;
    }
    
    if(len == 1) {
        sum += *(uint8_t *)ptr;
    }
    
    while(sum >> 16) {
        sum = (sum & 0xFFFF) + (sum >> 16);
    }
    
    return ~sum;
}

// 发送UDP数据包
void udp_send(uint32_t dest_ip, uint16_t src_port, uint16_t dest_port,
              uint8_t *data, uint16_t len)
{
    uint8_t buffer[1500];
    udp_header_t *udp = (udp_header_t *)buffer;
    
    udp->src_port = htons(src_port);
    udp->dest_port = htons(dest_port);
    udp->length = htons(sizeof(udp_header_t) + len);
    udp->checksum = 0;
    
    memcpy(buffer + sizeof(udp_header_t), data, len);
    
    // 计算校验和
    udp->checksum = udp_checksum(local_ip, dest_ip, buffer, 
                                 sizeof(udp_header_t) + len);
    
    ip_send(dest_ip, IP_PROTO_UDP, buffer, sizeof(udp_header_t) + len);
}

// 处理接收到的UDP数据包
void udp_input(uint32_t src_ip, uint8_t *data, uint16_t len)
{
    if(len < sizeof(udp_header_t))
        return;
    
    udp_header_t *udp = (udp_header_t *)data;
    uint16_t dest_port = ntohs(udp->dest_port);
    uint16_t src_port = ntohs(udp->src_port);
    
    uint8_t *payload = data + sizeof(udp_header_t);
    uint16_t payload_len = ntohs(udp->length) - sizeof(udp_header_t);
    
    // 查找注册的处理函数
    for(int i = 0; i < MAX_UDP_HANDLERS; i++) {
        if(udp_handlers[i].port == dest_port && 
           udp_handlers[i].callback != NULL) {
            udp_handlers[i].callback(src_ip, src_port, payload, payload_len);
            break;
        }
    }
}

六、ICMP实现(可选但建议添加)

ICMP主要用于ping功能,调试时很有用。

// icmp.c
#include "icmp.h"
#include "ip.h"
#include <string.h>

#define ICMP_TYPE_ECHO_REPLY    0
#define ICMP_TYPE_ECHO_REQUEST  8

typedef struct {
    uint8_t type;
    uint8_t code;
    uint16_t checksum;
    uint16_t id;
    uint16_t sequence;
} __attribute__((packed)) icmp_header_t;

void icmp_input(uint32_t src_ip, uint8_t *data, uint16_t len)
{
    if(len < sizeof(icmp_header_t))
        return;
    
    icmp_header_t *icmp = (icmp_header_t *)data;
    
    if(icmp->type == ICMP_TYPE_ECHO_REQUEST) {
        // 回复ping请求
        uint8_t buffer[1500];
        memcpy(buffer, data, len);
        
        icmp_header_t *reply = (icmp_header_t *)buffer;
        reply->type = ICMP_TYPE_ECHO_REPLY;
        reply->checksum = 0;
        reply->checksum = ip_checksum(buffer, len);
        
        ip_send(src_ip, IP_PROTO_ICMP, buffer, len);
    }
}

七、应用层示例

7.1 主程序集成

// main.c
#include "stm32f4xx_hal.h"
#include "ethernet.h"
#include "ip.h"
#include "udp.h"
#include "arp.h"
#include <stdio.h>
#include <string.h>

// 将IP地址字符串转换为uint32_t
static uint32_t str_to_ip(const char *ip_str)
{
    uint8_t ip[4];
    sscanf(ip_str, "%hhu.%hhu.%hhu.%hhu", &ip[0], &ip[1], &ip[2], &ip[3]);
    return (ip[0] << 24) | (ip[1] << 16) | (ip[2] << 8) | ip[3];
}

// UDP数据接收回调
void udp_data_handler(uint32_t src_ip, uint16_t src_port, 
                     uint8_t *data, uint16_t len)
{
    printf("收到来自 %d.%d.%d.%d:%d 的数据: %.*s\n",
           (src_ip >> 24) & 0xFF,
           (src_ip >> 16) & 0xFF,
           (src_ip >> 8) & 0xFF,
           src_ip & 0xFF,
           src_port, len, data);
    
    // 回复数据
    char reply[] = "Hello from STM32!";
    udp_send(src_ip, 8888, src_port, (uint8_t*)reply, strlen(reply));
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    
    // 初始化各层协议
    ethernet_init();
    arp_init();
    ip_init(str_to_ip("192.168.1.100"),    // 本地IP
            str_to_ip("192.168.1.1"),      // 网关
            str_to_ip("255.255.255.0"));   // 子网掩码
    udp_init();
    
    // 注册UDP端口8888的处理函数
    udp_register_handler(8888, udp_data_handler);
    
    printf("网络协议栈初始化完成\n");
    printf("本地IP: 192.168.1.100\n");
    
    uint8_t rx_buffer[1500];
    uint32_t last_send = 0;
    
    while(1)
    {
        // 接收处理
        uint16_t len = ENC28J60_PacketReceive(sizeof(rx_buffer), rx_buffer);
        if(len > 0) {
            ethernet_input(rx_buffer, len);
        }
        
        // 定时发送测试数据
        if(HAL_GetTick() - last_send > 5000) {
            last_send = HAL_GetTick();
            
            char msg[] = "STM32 heartbeat";
            udp_send(str_to_ip("192.168.1.10"), 8888, 9999, 
                    (uint8_t*)msg, strlen(msg));
        }
        
        HAL_Delay(1);
    }
}

八、测试与调试

8.1 网络配置

确保你的电脑和STM32在同一网段:

  • STM32: 192.168.1.100
  • 电脑: 192.168.1.10
  • 子网掩码: 255.255.255.0

8.2 Ping测试

在电脑上ping STM32:

ping 192.168.1.100

如果ICMP实现正确,应该能收到响应。

8.3 UDP测试

用Python写个简单的UDP测试工具:

import socket

# 创建UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('192.168.1.10', 9999))

print("等待数据...")

while True:
    data, addr = sock.recvfrom(1024)
    print(f"收到来自 {addr} 的数据: {data.decode()}")
    
    # 回复
    sock.sendto(b"Hello from PC!", (addr[0], 8888))

8.4 Wireshark抓包

用Wireshark抓包分析是最直接的调试方法:

  1. 设置过滤器: ip.addr == 192.168.1.100
  2. 观察数据包结构是否正确
  3. 检查校验和是否计算正确
  4. 分析时序问题

九、性能优化与改进建议

9.1 当前实现的局限性

  1. 没有数据包缓冲: 发送ARP请求时直接丢弃数据包
  2. 单线程轮询: 效率不高,可以改用中断驱动
  3. 没有错误重传: UDP本身不保证可靠性
  4. 内存使用固定: 可以改用内存池管理

9.2 优化方向

添加数据包队列:

typedef struct {
    uint32_t dest_ip;
    uint8_t data[1500];
    uint16_t len;
} packet_queue_t;

packet_queue_t tx_queue[10];

使用DMA提高SPI传输效率:

HAL_SPI_TransmitReceive_DMA(&hspi1, tx_data, rx_data, len);

添加TCP支持(这就是另一个大工程了):

  • 需要实现状态机
  • 滑动窗口
  • 重传机制
  • 拥塞控制

十、常见问题与解决

10.1 收不到数据包

检查点:

  1. SPI时钟是否正确(不要超过10MHz)
  2. MAC地址是否设置正确
  3. 接收过滤器配置
  4. 网线是否连接(查看PHSTAT2寄存器)

10.2 发送失败

可能原因:

  1. 没有等待上次发送完成
  2. 发送缓冲区溢出
  3. 帧长度错误

10.3 校验和错误

这个我踩过坑:

  • IP/UDP校验和要注意字节序
  • 注意奇数长度的处理
  • UDP伪首部别忘了

总结

自己实现网络协议栈确实比直接用lwIP麻烦,但收获也很多。通过这个过程,我对TCP/IP协议族的理解加深了不少,特别是各层之间的交互关系。

代码总共不到2000行,编译后占用Flash大约15KB,RAM使用3KB左右(不包括接收发送缓冲区),对于资源受限的项目来说还是很实用的。

当然,这个实现还很基础,距离生产环境使用还有距离。如果项目允许,还是建议用成熟的协议栈。但如果你想深入理解网络原理,或者资源确实有限,自己实现一个也是不错的选择。


参考资料:

  • RFC 768 (UDP)
  • RFC 791 (IP)
  • RFC 826 (ARP)
  • ENC28J60 Datasheet
  • STM32F407 Reference Manual

环境说明:

  • MCU: STM32F407VET6
  • IDE: STM32CubeIDE 1.10.0
  • 网卡: ENC28J60
  • 编译器: ARM-GCC 10.3

欢迎大家提出改进建议!

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐