从零开始在STM32上实现轻量级网络协议栈
本文介绍了基于STM32F407和ENC28J60芯片实现精简网络协议栈的过程。协议栈采用四层架构设计(应用层/UDP/IP/MAC),通过分层封装实现数据收发。硬件方面详细说明了SPI接口连接和ENC28J60驱动实现,包括寄存器定义和底层读写函数。该方案相比lwIP更加轻量,适合资源受限的嵌入式应用场景,完整代码已提供关键部分的实现。
前言
最近在做一个嵌入式项目,需要让STM32通过以太网实现数据通信。本来打算直接用lwIP,但考虑到项目对资源要求比较严格,而且功能相对简单,就萌生了自己写一个精简网络协议栈的想法。经过两周的折腾,总算把这个轮子造出来了,虽然功能不如lwIP全面,但胜在轻量够用。
这篇文章会详细记录整个实现过程,包括协议栈的分层设计、关键代码实现,以及我踩过的一些坑。代码基于STM32F407,使用的网卡芯片是ENC28J60,如果你用的是其他平台,核心思想也是通用的。
一、网络协议栈基础架构
1.1 为什么需要分层
网络协议栈采用分层设计并不是为了故意搞复杂,而是有实际意义的。每一层只关注自己的职责,上层不用管下层怎么传输,下层也不用管上层传什么数据。这种解耦设计让代码维护起来清晰很多。
我们实现的协议栈包含四层:
+-------------------+
| 应用层 (APP) | <- 用户数据
+-------------------+
| 传输层 (UDP) | <- 端口、校验和
+-------------------+
| 网络层 (IP) | <- IP地址、路由
+-------------------+
| 数据链路层 (MAC) | <- 物理地址、帧封装
+-------------------+
| 物理层 (PHY) | <- ENC28J60驱动
+-------------------+
1.2 数据封装过程
当应用层要发送数据时,数据会像穿衣服一样被一层层包装:
- 应用数据: “Hello”
- 加UDP头: [UDP头] + “Hello”
- 加IP头: [IP头] + [UDP头] + “Hello”
- 加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抓包分析是最直接的调试方法:
- 设置过滤器:
ip.addr == 192.168.1.100 - 观察数据包结构是否正确
- 检查校验和是否计算正确
- 分析时序问题
九、性能优化与改进建议
9.1 当前实现的局限性
- 没有数据包缓冲: 发送ARP请求时直接丢弃数据包
- 单线程轮询: 效率不高,可以改用中断驱动
- 没有错误重传: UDP本身不保证可靠性
- 内存使用固定: 可以改用内存池管理
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 收不到数据包
检查点:
- SPI时钟是否正确(不要超过10MHz)
- MAC地址是否设置正确
- 接收过滤器配置
- 网线是否连接(查看PHSTAT2寄存器)
10.2 发送失败
可能原因:
- 没有等待上次发送完成
- 发送缓冲区溢出
- 帧长度错误
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
欢迎大家提出改进建议!
更多推荐
所有评论(0)