lwip-2.1.3在STM32F103ZE+ENC28J60有线网卡上无操作系统移植(使用STM32 HAL库)

程序下载链接:百度网盘 请输入提取码(提取码:k6tz)

【重要说明】

连接方式一(推荐):
电脑有线网卡断开,无线网卡连无线路由器,无线网卡配置成自动获取IP地址。
板子的ENC28J60也连无线路由器,main函数的net_config参数为1(自动获取IP地址)。

连接方式二:
电脑的有线网卡连板子的ENC28J60,无线网卡必须断开
Windows系统默认情况下不支持多网卡路由,除非自己在命令行里面用命令配置好路由表。如果此时无线网卡连了无线路由器,那么所有的数据包都会从无线网卡出去,不会经过有线网卡,导致电脑ping不通板子。
板子main函数里面net_config参数为0(在程序里面配置IP地址)。
电脑的有线网卡也要手工配置IP地址,不能自动获取IP地址。手工配置的IP地址必须和板子是一个网段。

单片机的串口打印非常重要,是跟踪程序流程的重要手段。
硬件没有调通之前,一定不能舍弃串口!!!!!

一、概述

以太网芯片简介

ENC28J60是一款10Mbps速率的以太网MAC+PHY芯片,和单片机的通信接口为SPI,SPI最高时钟频率为20MHz
ENC28J60支持半双工和全双工模式,但是不支持自动协商。在支持自动协商的网络环境中,ENC28J60默认的工作模式是半双工模式。
另外,STM32本身有一个ETH外设,这个外设采用的接口是MII或RMII,不是SPI,所以不能连接ENC28J60芯片,这次我们用不到这个ETH外设。
STM32本身的ETH外设相当于MAC,通常要外接一个PHY芯片(如DP83848)。DP83848是一款100Mbps速率的以太网PHY芯片(同时也支持10Mbps速率模式),支持半双工和全双工模式,而且支持自动协商,插在支持自动协商的网络环境中可以协商到100Mbps全双工模式。
ENC28J60芯片内部集成了MAC和PHY,所以不再需要单片机的MAC了,直接用SPI通信就能搞定了。

芯片封装

ENC28J60有四种封装:两列直插(ENC28J60-I/SP)、SOIC型贴片(ENC28J60-I/SO)、SSOP型贴片(ENC28J60/SS)、QFN型贴片(ENC28J60-I/ML)。
请注意SOIC型和SSOP型虽然都是两列贴片, 但是一个尺寸大,一个尺寸小,不要搞错了!

电路图

 以下6个引脚要和单片机I/O口相连。前四个接单片机的SPI接口,后面两个为普通I/O口可以任接。

引脚I/O口
SPI1_NSSPA4
SPI1_SCKPA5
SPI1_MISOPA6
SPI1_MOSIPA7
ENC28J60_INT(中断)PA1
ENC28J60_RST(复位)

PB9

网口的型号是HanRun HR911105A。请注意网口灯的颜色。LED_LINK要接绿色的灯,灯亮表示插了网线,灯灭表示没有插网线。LED_ACT要接黄色的灯,闪烁一次表示传输了一个数据包,平时灯不亮。
正常情况下,即使单片机没有写任何程序,插上网线后绿灯也要亮,并且收到数据包时黄灯也要闪烁。如果灯不亮,就说明电路有问题。注意一下Y4晶振是一个25MHz的无源晶振,不要接错接成有源的了。

二、建立基于HAL库的Keil工程 

去ST官网下载STM32F1的HAL库源码包:STM32CubeF1 - STM32Cube MCU Package for STM32F1 series (HAL, Low-Layer APIs and CMSIS, USB, TCP/IP, File system, RTOS, Graphic - and examples running on ST boards) - STMicroelectronics
下载STM32CubeF1 1.8.0和Patch_CubeF1 1.8.4(补丁)两个压缩包。
下载下来后,解压en.stm32cubef1_v1.8.0.zip,然后再解压en.patch_cubef1_v1-8-4_v1.8.4.zip。
两个压缩包要解压到同一个目录,遇到相同文件名时选择覆盖文件,使两个STM32Cube_FW_F1_V1.8.0文件夹合并在一起:

在一个空白文件夹中建立新的Keil工程,选择STM32F103ZE芯片,在弹出的Manage Run-Time Environment窗口直接点击OK。
在建好的Keil工程里面新建一个STM32F1xx_HAL_Driver文件夹:

将解压出来的STM32Cube_FW_F1_V1.8.0/Drivers/STM32F1xx_HAL_Driver/Inc文件夹复制到工程中的STM32F1xx_HAL_Driver文件夹:

将工程里面的STM32F1xx_HAL_Driver/Inc中凡是以_template后缀结尾的h文件,全部重命名,删掉_template后缀:
stm32_assert_template.h重命名为stm32_assert.h。
stm32f1xx_hal_conf_template.h重命名为stm32f1xx_hal_conf.h。

在工程里面建立STM32F1xx_HAL_Driver/Src文件夹,将STM32Cube_FW_F1_V1.8.0/Drivers/STM32F1xx_HAL_Driver/Src里面需要用到的外设的C文件复制过去。请注意STM32Cube_FW_F1_V1.8.0/Drivers/STM32F1xx_HAL_Driver/Src/Legacy不用复制,这是用于兼容旧版HAL库代码的文件。

复制以下文件到工程的STM32F1xx_HAL_Driver文件夹中:
STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Device/ST/STM32F1xx/Include/stm32f1xx.h
STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Device/ST/STM32F1xx/Include/stm32f103xe.h(因为STM32F103ZE属于STM32F103xE这一类,是High Density器件)
STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Device/ST/STM32F1xx/Include/system_stm32f1xx.h
STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/system_stm32f1xx.c
STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/arm/startup_stm32f103xe.s
STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Core/Include/cmsis_armcc.h
STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Core/Include/cmsis_compiler.h
STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Core/Include/cmsis_version.h
STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Core/Include/core_cm3.h

在Keil工程中新建一个STM32F1xx_HAL_Driver组,将STM32F1xx_HAL_Driver/Src中的所有c文件,STM32F1xx_HAL_Driver中的所有c文件和s文件添加到组里面:

在工程属性的C/C++选项卡中定义STM32F103xE USE_FULL_ASSERT USE_HAL_DRIVER这三个宏,头文件包含路径添加STM32F1xx_HAL_Driver和STM32F1xx_HAL_Driver/Inc这两个文件夹。
请不要在Target选项卡中勾选Use MicroLIB。

在Source Group 1下添加main.c和common.c两个文件,内容如下。
main.c:

#include <stdio.h>
#include <stm32f1xx.h>
#include "common.h"

int main(void)
{
  HAL_Init();
  
  clock_init();
  usart_init(115200);
  printf("STM32F103ZE ENC28J60\n");
  printf("SystemCoreClock=%u\n", SystemCoreClock);
  
  while (1)
  {
  }
}

common.c:

#include <stdio.h>
#include <stdlib.h>
#include <stm32f1xx.h>
#include "common.h"

#pragma import(__use_no_semihosting) // 禁用半主机模式 (不然调用printf就会进HardFault)

FILE __stdout = {1};
FILE __stderr = {2};
UART_HandleTypeDef huart1;

/* main函数返回时执行的函数 */
void _sys_exit(int returncode)
{
  printf("Exited! returncode=%d\n", returncode);
  while (1);
}

void _ttywrch(int ch)
{
  if (ch == '\n')
    HAL_UART_Transmit(&huart1, (uint8_t *)"\r\n", 2, HAL_MAX_DELAY);
  else
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
}

/* HAL库参数错误警告 */
#ifdef USE_FULL_ASSERT
void assert_failed(uint8_t *file, uint32_t line)
{
  printf("%s: file %s on line %d\r\n", __FUNCTION__, file, line);
  abort();
}
#endif

/* 配置系统时钟 */
void clock_init(void)
{
  HAL_StatusTypeDef status;
  RCC_ClkInitTypeDef clk = {0};
  RCC_OscInitTypeDef osc = {0};
  
  // 启动HSE晶振, 并用PLL倍频9倍
  osc.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  osc.HSEState = RCC_HSE_ON;
  osc.PLL.PLLMUL = RCC_PLL_MUL9;
  osc.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  osc.PLL.PLLState = RCC_PLL_ON;
  status = HAL_RCC_OscConfig(&osc);
  
  // 若HSE启动失败, 则改用HSI, 并用PLL倍频8倍
  if (status != HAL_OK)
  {
    osc.HSEState = RCC_HSE_OFF;
    osc.PLL.PLLMUL = RCC_PLL_MUL16;
    osc.PLL.PLLSource = RCC_PLLSOURCE_HSI_DIV2;
    HAL_RCC_OscConfig(&osc);
  }
  
  // 设置ADC时钟分频系数
  __HAL_RCC_ADC_CONFIG(RCC_ADCPCLK2_DIV6);
  
  // 将PLL设为系统时钟
  clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
  clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  clk.AHBCLKDivider = RCC_SYSCLK_DIV1;
  clk.APB1CLKDivider = RCC_HCLK_DIV2;
  clk.APB2CLKDivider = RCC_HCLK_DIV1;
  HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_2);
}

/* 显示数据块的十六进制内容 */
void dump_data(const void *data, int len)
{
  const uint8_t *p = data;
  
  while (len--)
    printf("%02X", *p++);
  printf("\n");
}

/* printf和perror重定向到串口 */
int fputc(int ch, FILE *fp)
{
  if (fp->handle == 1 || fp->handle == 2)
  {
    _ttywrch(ch);
    return ch;
  }
  return EOF;
}

/* 初始化串口 */
void usart_init(int baud_rate)
{
  GPIO_InitTypeDef gpio;
  
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_USART1_CLK_ENABLE();
  
  gpio.Mode = GPIO_MODE_AF_PP;
  gpio.Pin = GPIO_PIN_9;
  gpio.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &gpio);
  
  huart1.Instance = USART1;
  huart1.Init.BaudRate = baud_rate;
  huart1.Init.Mode = UART_MODE_TX_RX;
  HAL_UART_Init(&huart1);
}

void HardFault_Handler(void)
{
  printf("Hard Error!\n");
  while (1);
}

void SysTick_Handler(void)
{
  HAL_IncTick();
}

其中包含的头文件common.h的内容如下:

#ifndef _COMMON_H
#define _COMMON_H

extern UART_HandleTypeDef huart1;

struct __FILE
{
  int handle;
};

void clock_init(void);
void dump_data(const void *data, int len);
void usart_init(int baud_rate);

#endif

三、编写ENC28J60初始化和收发数据包的函数

ENC28J60.h:

#ifndef _ENC28J60_H
#define _ENC28J60_H

/* 公共寄存器 */
#define ENC28J60_EIE 0x1b
#define ENC28J60_EIR 0x1c
#define ENC28J60_ESTAT 0x1d
#define ENC28J60_ECON2 0x1e
#define ENC28J60_ECON1 0x1f
#define ENC28J60_IS_COMMON_REG(n) ((n) >= ENC28J60_EIE && (n) <= ENC28J60_ECON1)
 
/* Bank0寄存器 */
#define ENC28J60_ERDPTL 0x00
#define ENC28J60_ERDPTH 0x01
#define ENC28J60_EWRPTL 0x02
#define ENC28J60_EWRPTH 0x03
#define ENC28J60_ETXSTL 0x04
#define ENC28J60_ETXSTH 0x05
#define ENC28J60_ETXNDL 0x06
#define ENC28J60_ETXNDH 0x07
#define ENC28J60_ERXSTL 0x08
#define ENC28J60_ERXSTH 0x09
#define ENC28J60_ERXNDL 0x0a
#define ENC28J60_ERXNDH 0x0b
#define ENC28J60_ERXRDPTL 0x0c
#define ENC28J60_ERXRDPTH 0x0d
#define ENC28J60_ERXWRPTL 0x0e
#define ENC28J60_ERXWRPTH 0x0f
#define ENC28J60_EDMASTL 0x10
#define ENC28J60_EDMASTH 0x11
#define ENC28J60_EDMANDL 0x12
#define ENC28J60_EDMANDH 0x13
#define ENC28J60_EDMADSTL 0x14
#define ENC28J60_EDMADSTH 0x15
#define ENC28J60_EDMACSL 0x16
#define ENC28J60_EDMACSH 0x17
 
/* Bank1寄存器 */
#define ENC28J60_EHT0 0x20
#define ENC28J60_EHT1 0x21
#define ENC28J60_EHT2 0x22
#define ENC28J60_EHT3 0x23
#define ENC28J60_EHT4 0x24
#define ENC28J60_EHT5 0x25
#define ENC28J60_EHT6 0x26
#define ENC28J60_EHT7 0x27
#define ENC28J60_EPMM0 0x28
#define ENC28J60_EPMM1 0x29
#define ENC28J60_EPMM2 0x2a
#define ENC28J60_EPMM3 0x2b
#define ENC28J60_EPMM4 0x2c
#define ENC28J60_EPMM5 0x2d
#define ENC28J60_EPMM6 0x2e
#define ENC28J60_EPMM7 0x2f
#define ENC28J60_EPMCSL 0x30
#define ENC28J60_EPMCSH 0x31
#define ENC28J60_EPMOL 0x34
#define ENC28J60_EPMOH 0x35
#define ENC28J60_ERXFCON 0x38
#define ENC28J60_EPKTCNT 0x39
 
/* Bank2寄存器 */
#define ENC28J60_MACON1 0x40
#define ENC28J60_MACON3 0x42
#define ENC28J60_MACON4 0x43
#define ENC28J60_MABBIPG 0x44
#define ENC28J60_MAIPGL 0x46
#define ENC28J60_MAIPGH 0x47
#define ENC28J60_MACLCON1 0x48
#define ENC28J60_MACLCON2 0x49
#define ENC28J60_MAMXFLL 0x4a
#define ENC28J60_MAMXFLH 0x4b
#define ENC28J60_MICMD 0x52
#define ENC28J60_MIREGADR 0x54
#define ENC28J60_MIWRL 0x56
#define ENC28J60_MIWRH 0x57
#define ENC28J60_MIRDL 0x58
#define ENC28J60_MIRDH 0x59
 
/* Bank3寄存器 */
#define ENC28J60_MAADR5 0x60
#define ENC28J60_MAADR6 0x61
#define ENC28J60_MAADR3 0x62
#define ENC28J60_MAADR4 0x63
#define ENC28J60_MAADR1 0x64
#define ENC28J60_MAADR2 0x65
#define ENC28J60_EBSTSD 0x66
#define ENC28J60_EBSTCON 0x67
#define ENC28J60_EBSTCSL 0x68
#define ENC28J60_EBSTCSH 0x69
#define ENC28J60_MISTAT 0x6a
#define ENC28J60_EREVID 0x72
#define ENC28J60_ECOCON 0x75
#define ENC28J60_EFLOCON 0x77
#define ENC28J60_EPAUSL 0x78
#define ENC28J60_EPAUSH 0x79
 
/* PHY寄存器 */
#define ENC28J60_PHCON1 0x80
#define ENC28J60_PHSTAT1 0x81
#define ENC28J60_PHID1 0x82
#define ENC28J60_PHID2 0x83
#define ENC28J60_PHCON2 0x90
#define ENC28J60_PHSTAT2 0x91
#define ENC28J60_PHIE 0x92
#define ENC28J60_PHIR 0x93
#define ENC28J60_PHLCON 0x94
 
/* 常用的寄存器位 */
// EIE: 0x1b
#define ENC28J60_EIE_INTIE 0x80
#define ENC28J60_EIE_PKTIE 0x40
#define ENC28J60_EIE_DMAIE 0x20
#define ENC28J60_EIE_LINKIE 0x10
#define ENC28J60_EIE_TXIE 0x08
#define ENC28J60_EIE_TXERIE 0x02
#define ENC28J60_EIE_RXERIE 0x01
// EIR: 0x1c
#define ENC28J60_EIR_PKTIF 0x40
#define ENC28J60_EIR_DMAIF 0x20
#define ENC28J60_EIR_LINKIF 0x10
#define ENC28J60_EIR_TXIF 0x08
#define ENC28J60_EIR_TXERIF 0x02
#define ENC28J60_EIR_RXERIF 0x01
// ESTAT: 0x1d
#define ENC28J60_ESTAT_CLKRDY 0x01
// ECON2: 0x1e
#define ENC28J60_ECON2_PKTDEC 0x40
// ECON1: 0x1f
#define ENC28J60_ECON1_TXRST 0x80
#define ENC28J60_ECON1_RXRST 0x40
#define ENC28J60_ECON1_DMAST 0x20
#define ENC28J60_ECON1_CSUMEN 0x10
#define ENC28J60_ECON1_TXRTS 0x08
#define ENC28J60_ECON1_RXEN 0x04
#define ENC28J60_ECON1_BSEL 0x03
#define ENC28J60_ECON1_BSEL_Pos 0
// ERXFCON: 0x38
#define ENC28J60_ERXFCON_UCEN 0x80
#define ENC28J60_ERXFCON_ANDOR 0x40
#define ENC28J60_ERXFCON_CRCEN 0x20
#define ENC28J60_ERXFCON_PMEN 0x10
#define ENC28J60_ERXFCON_MPEN 0x08
#define ENC28J60_ERXFCON_HTEN 0x04
#define ENC28J60_ERXFCON_MCEN 0x02
#define ENC28J60_ERXFCON_BCEN 0x01
// MACON1: 0x40
#define ENC28J60_MACON1_TXPAUS 0x08
#define ENC28J60_MACON1_RXPAUS 0x04
#define ENC28J60_MACON1_MARXEN 0x01
// MACON3: 0x42
#define ENC28J60_MACON3_PADCFG 0xe0
#define ENC28J60_MACON3_PADCFG_Pos 5
#define ENC28J60_MACON3_TXCRCEN 0x10
#define ENC28J60_MACON3_FRMLNEN 0x02
#define ENC28J60_MACON3_FULDPX 0x01
// MICMD: 0x52
#define ENC28J60_MICMD_MIISCAN 0x02
#define ENC28J60_MICMD_MIIRD 0x01
// MISTAT: 0x6a
#define ENC28J60_MISTAT_BUSY 0x01
// PHCON1: 0x80
#define ENC28J60_PHCON1_PDPXMD 0x0100
// PHSTAT1: 0x81
#define ENC28J60_PHSTAT1_LLSTAT 0x04
#define ENC28J60_PHSTAT1_JBSTAT 0x02
// PHCON2: 0x90
#define ENC28J60_PHCON2_HDLDIS 0x0100
// PHSTAT2: 0x91
#define ENC28J60_PHSTAT2_LSTAT 0x0400
// PHIE: 0x92
#define ENC28J60_PHIE_PLNKIE 0x10
#define ENC28J60_PHIE_PGEIE 0x02
// PHIR: 0x93
#define ENC28J60_PHIR_PLNKIF 0x10
#define ENC28J60_PHIR_PGIF 0x02
// PHLCON: 0x94
#define ENC28J60_PHLCON_LACFG_Pos 8
#define ENC28J60_PHLCON_LBCFG_Pos 4
#define ENC28J60_PHLCON_LFRQ_Pos 2
#define ENC28J60_PHLCON_STRCH 0x02
 
/* 指令集 */
#define ENC28J60_READ_CTRL_REG 0x00 // 读控制寄存器
#define ENC28J60_READ_BUF_MEM 0x3a // 读缓冲区
#define ENC28J60_WRITE_CTRL_REG 0x40 // 写控制寄存器
#define ENC28J60_WRITE_BUF_MEM 0x7a // 写缓冲区
#define ENC28J60_BIT_FIELD_SET 0x80 // 位域置一
#define ENC28J60_BIT_FIELD_CLR 0xa0 // 位域清零
#define ENC28J60_SOFT_RESET 0xff // 系统复位
 
/* 选项 */
#define ENC28J60_MAXPACKETLEN 1518 // 以太网数据包最大长度
#define ENC28J60_RXSTART 0x0000 // 接收缓冲区起始地址
#define ENC28J60_RXEND 0x1a0d // 接收缓冲区结束地址
#define ENC28J60_TXSTART 0x1a0e // 发送缓冲区起始地址, 缓冲区大小为1514+8字节 (发送时不用填写CRC)
#define ENC28J60_TXEND 0x1fff // 发送缓冲区结束地址

#define ENC28J60_ClearRegisterBits(addr, bits) ENC28J60_SetRegister(addr, bits, 0)
#define ENC28J60_GetPacketCount() ENC28J60_ReadRegister(ENC28J60_EPKTCNT) // 获取收到的数据包个数
#define ENC28J60_ReadMemory(buffer, size) ENC28J60_Execute(ENC28J60_READ_BUF_MEM, 0, buffer, -(size))
#define ENC28J60_SetRegisterBits(addr, bits) ENC28J60_SetRegister(addr, bits, 1)
#define ENC28J60_WriteMemory(buffer, size) ENC28J60_Execute(ENC28J60_WRITE_BUF_MEM, 0, (void *)(buffer), size)

int ENC28J60_BeginReception(void);
int ENC28J60_BeginTransmission(int len);
void ENC28J60_EndReception(void);
void ENC28J60_EndTransmission(void);
void ENC28J60_Execute(uint8_t opcode, uint8_t argument, void *data, int len);
int ENC28J60_GetITStatus(void);
int ENC28J60_GetNextPacketPointer(void);
void ENC28J60_Init(uint8_t mac_addr[6]);
uint16_t ENC28J60_ReadRegister(uint8_t addr);
void ENC28J60_SelectBank(uint8_t bank);
void ENC28J60_SetRegister(uint8_t addr, uint8_t bits, uint8_t value);
void ENC28J60_WriteRegister(uint8_t addr, uint16_t value);

#endif

ENC28J60内部缓冲区大小为8KB。前面的部分用来接收数据,后面的部分用来发送数据。正常情况下以太网数据包的最大长度为1518字节(含CRC)。发送数据包时不需要填写CRC(占4字节),但需要预留1字节的控制字节和7字节的Status Vector,加起来发送缓冲区占用的空间就是(1518-4)+(1+7)=1522字节,地址范围为0x1a0e(偶数)~0x1fff(奇数)。其余部分是接收缓冲区,地址范围为0x0000~0x1a0d(奇数)。

ENC28J60.c:

#include <stdio.h>
#include <stdlib.h>
#include <stm32f1xx.h>
#include "ENC28J60.h"

#define CS_0 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define CS_1 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)
#define INT (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_SET)
#define RST_0 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET)
#define RST_1 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET)

SPI_HandleTypeDef hspi1;
static uint8_t enc28j60_bank;
static uint16_t enc28j60_next_packet;

/* 开始接收数据包 */
// 返回收到的数据包的长度
int ENC28J60_BeginReception(void)
{
  int len;
  uint8_t info[6];
  
  // 设置缓冲区读指针
  ENC28J60_WriteRegister(ENC28J60_ERDPTL, enc28j60_next_packet & 0xff);
  ENC28J60_WriteRegister(ENC28J60_ERDPTH, enc28j60_next_packet >> 8);
  
  // 读取Next Packet Pointer和Receive Status Vector
  ENC28J60_ReadMemory(info, sizeof(info));
  enc28j60_next_packet = info[0] | (info[1] << 8); // 下一个数据包的位置
  len = info[2] | (info[3] << 8); // 数据包的长度
  len -= 4; // 去掉CRC的长度
  return len;
}

/* 开始发送数据包 */
int ENC28J60_BeginTransmission(int len)
{
  int capacity = ENC28J60_TXEND - ENC28J60_TXSTART + 1;
  uint8_t data = 0;
  
  // 判断发送缓冲区的容量是否足够
  if (len <= 0)
    return -1;
  else if (len + 8 > capacity) // Control和Status Vector要占8字节
  {
    printf("%s: packet is too big!\n", __FUNCTION__);
    return -1;
  }
  
  // 设置发送缓冲区起始地址
  ENC28J60_WriteRegister(ENC28J60_ETXSTL, ENC28J60_TXSTART & 0xff);
  ENC28J60_WriteRegister(ENC28J60_ETXSTH, ENC28J60_TXSTART >> 8);
  
  // 设置发送缓冲区结束地址
  ENC28J60_WriteRegister(ENC28J60_ETXNDL, (ENC28J60_TXSTART + len) & 0xff);
  ENC28J60_WriteRegister(ENC28J60_ETXNDH, (ENC28J60_TXSTART + len) >> 8);
  
  // 设置缓冲区写指针
  ENC28J60_WriteRegister(ENC28J60_EWRPTL, ENC28J60_TXSTART & 0xff);
  ENC28J60_WriteRegister(ENC28J60_EWRPTH, ENC28J60_TXSTART >> 8);
  
  // 写入控制字节(Control): 控制字节为0x00, 表示使用MACON3寄存器的设置
  ENC28J60_WriteMemory(&data, 1);
  return 0;
}

/* 结束接收当前数据包 */
void ENC28J60_EndReception(void)
{
  // 移动接收缓冲区读指针
  ENC28J60_WriteRegister(ENC28J60_ERXRDPTL, enc28j60_next_packet & 0xff);
  ENC28J60_WriteRegister(ENC28J60_ERXRDPTH, enc28j60_next_packet >> 8);
  
  // 数据包个数减1
  ENC28J60_SetRegisterBits(ENC28J60_ECON2, ENC28J60_ECON2_PKTDEC);
}

/* 结束发送当前数据包 */
void ENC28J60_EndTransmission(void)
{
  uint32_t ticks;
  
  // 请求发送
  ENC28J60_SetRegisterBits(ENC28J60_ECON1, ENC28J60_ECON1_TXRTS);
  
  // 等待发送完毕
  ticks = HAL_GetTick();
  while (ENC28J60_ReadRegister(ENC28J60_ECON1) & ENC28J60_ECON1_TXRTS)
  {
    if (HAL_GetTick() - ticks > 1000)
    {
      // 超时, 取消发送并复位发送逻辑
      // 见ENC28J60勘误手册的10. Module: Transmit Logic
      printf("%s: timeout!\n", __FUNCTION__);
      ENC28J60_ClearRegisterBits(ENC28J60_ECON1, ENC28J60_ECON1_TXRTS);
      ENC28J60_SetRegisterBits(ENC28J60_ECON1, ENC28J60_ECON1_TXRST);
      ENC28J60_ClearRegisterBits(ENC28J60_ECON1, ENC28J60_ECON1_TXRST);
      break;
    }
  }
}

/* 执行SPI命令 */
// len>0: 发送数据; len<0: 接收数据; len=0: 无数据
void ENC28J60_Execute(uint8_t opcode, uint8_t argument, void *data, int len)
{
  uint8_t byte0 = opcode | (argument & 0x1f);
  
  CS_0;
  HAL_SPI_Transmit(&hspi1, &byte0, 1, HAL_MAX_DELAY);
  if (data != NULL)
  {
    if (len > 0)
      HAL_SPI_Transmit(&hspi1, data, len, HAL_MAX_DELAY);
    else if (len < 0)
      HAL_SPI_Receive(&hspi1, data, -len, HAL_MAX_DELAY);
  }
  CS_1;
}

/* 判断中断引脚的电平是否有效 */
int ENC28J60_GetITStatus(void)
{
  return !INT;
}

/* 获取下一个数据包在缓冲区中的位置 */
int ENC28J60_GetNextPacketPointer(void)
{
  return enc28j60_next_packet;
}

/* 初始化 */
void ENC28J60_Init(uint8_t mac_addr[6])
{
  uint16_t regs[3];
  GPIO_InitTypeDef gpio;
  
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();
  __HAL_RCC_SPI1_CLK_ENABLE();
  
  // ENC28J60_INT: PA1, SPI1_MISO: PA6
  gpio.Mode = GPIO_MODE_INPUT;
  gpio.Pin = GPIO_PIN_1 | GPIO_PIN_6;
  gpio.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOA, &gpio);
  
  // SPI1_NSS: PA4
  gpio.Mode = GPIO_MODE_OUTPUT_PP;
  gpio.Pin = GPIO_PIN_4;
  gpio.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &gpio);
  
  // SPI1_SCK: PA5, SPI1_MOSI: PA7
  gpio.Mode = GPIO_MODE_AF_PP;
  gpio.Pin = GPIO_PIN_5 | GPIO_PIN_7;
  gpio.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &gpio);
  
  // ENC28J60_RST: PB9
  gpio.Mode = GPIO_MODE_OUTPUT_PP;
  gpio.Pin = GPIO_PIN_9;
  gpio.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOB, &gpio);
  
  // 初始化SPI
  hspi1.Instance = SPI1;
  hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32; // 72MHz/32=2.25MHz
  hspi1.Init.Mode = SPI_MODE_MASTER;
  hspi1.Init.NSS = SPI_NSS_SOFT;
  HAL_SPI_Init(&hspi1);
  
  // 复位
  RST_0;
  enc28j60_bank = 0;
  enc28j60_next_packet = ENC28J60_RXSTART;
  HAL_Delay(10);
  RST_1;
  while ((ENC28J60_ReadRegister(ENC28J60_ESTAT) & ENC28J60_ESTAT_CLKRDY) == 0); /* 6.4 Waiting for OST */
  
  // 读PHY寄存器, 和手册上标明的默认值对比
  // 如果不正确, 说明SPI时钟频率太高
  regs[0] = ENC28J60_ReadRegister(ENC28J60_PHID1);
  regs[1] = ENC28J60_ReadRegister(ENC28J60_PHID2);
  regs[2] = ENC28J60_ReadRegister(ENC28J60_PHLCON);
  printf("ENC28J60 ID: 0x%04x 0x%04x\n", regs[0], regs[1]);
  if (regs[0] != 0x83 || regs[1] != 0x1400 || (regs[2] & 0xfffe) != 0x3422)
  {
    printf("Failed to read ENC28J60 registers!\n");
    printf("SPI frequency may be too high.\n");
    abort();
  }
  
  /* 6.1 Receive Buffer */
  // 设置接收缓冲区起始地址
  ENC28J60_WriteRegister(ENC28J60_ERXSTL, ENC28J60_RXSTART & 0xff);
  ENC28J60_WriteRegister(ENC28J60_ERXSTH, ENC28J60_RXSTART >> 8);
  
  // 设置接收缓冲区读指针
  ENC28J60_WriteRegister(ENC28J60_ERXRDPTL, ENC28J60_RXSTART & 0xff);
  ENC28J60_WriteRegister(ENC28J60_ERXRDPTH, ENC28J60_RXSTART >> 8);
  
  // 设置接收缓冲区结束地址
  ENC28J60_WriteRegister(ENC28J60_ERXNDL, ENC28J60_RXEND & 0xff);
  ENC28J60_WriteRegister(ENC28J60_ERXNDH, ENC28J60_RXEND >> 8);
  
  /* 6.3 Receive Filters */
  // 接收目的MAC地址和本机匹配的单播帧
  // 丢弃CRC校验不通过的帧
  // 接收所有多播帧和广播帧
  ENC28J60_WriteRegister(ENC28J60_ERXFCON, ENC28J60_ERXFCON_UCEN | ENC28J60_ERXFCON_CRCEN | ENC28J60_ERXFCON_MCEN | ENC28J60_ERXFCON_BCEN);
  
  /* 6.5.1 打开MAC接收 */
  // 允许发送暂停控制帧
  // 当接收到暂停控制帧时停止发送
  ENC28J60_WriteRegister(ENC28J60_MACON1, ENC28J60_MACON1_TXPAUS | ENC28J60_MACON1_RXPAUS | ENC28J60_MACON1_MARXEN);
  
  /* 6.5.2 */
  // 填充所有VLAN短帧至64字节, 填充所有其他帧至60字节
  // 发送时自动添加CRC校验值
  // 不允许收发长度超过MAMXFL值的帧
  // 收发时自动检查type/length字段的值
  // MAC配置为全双工模式
  ENC28J60_WriteRegister(ENC28J60_MACON3, (5 << ENC28J60_MACON3_PADCFG_Pos) | ENC28J60_MACON3_TXCRCEN | ENC28J60_MACON3_FRMLNEN | ENC28J60_MACON3_FULDPX);
  // PHY配置为全双工模式
  // 请注意: PDPXMD位的默认值取决于LEDB的接法
  ENC28J60_WriteRegister(ENC28J60_PHCON1, ENC28J60_PHCON1_PDPXMD);
  
  /* 6.5.4 配置最大帧长度 */
  ENC28J60_WriteRegister(ENC28J60_MAMXFLL, ENC28J60_MAXPACKETLEN & 0xff);
  ENC28J60_WriteRegister(ENC28J60_MAMXFLH, ENC28J60_MAXPACKETLEN >> 8);
  
  /* 6.5.5 配置Back-to-Back Inter-Packet Gap */
  ENC28J60_WriteRegister(ENC28J60_MABBIPG, 0x15);
  
  /* 6.5.6 配置Non-Back-to-Back Inter-Packet Gap */
  ENC28J60_WriteRegister(ENC28J60_MAIPGL, 0x12);
  ENC28J60_WriteRegister(ENC28J60_MAIPGH, 0x0c);
  
  /* 6.5.9 配置MAC地址 */
  assert_param((mac_addr[0] & 1) == 0); // MAC地址必须为单播地址
  ENC28J60_WriteRegister(ENC28J60_MAADR1, mac_addr[0]);
  ENC28J60_WriteRegister(ENC28J60_MAADR2, mac_addr[1]);
  ENC28J60_WriteRegister(ENC28J60_MAADR3, mac_addr[2]);
  ENC28J60_WriteRegister(ENC28J60_MAADR4, mac_addr[3]);
  ENC28J60_WriteRegister(ENC28J60_MAADR5, mac_addr[4]);
  ENC28J60_WriteRegister(ENC28J60_MAADR6, mac_addr[5]);
  
  /* 其他配置 */
  // LEDA(绿灯)显示是否插了网线
  // LEDB(黄灯)显示数据包收发状态
  // LED闪烁时长为tMSTRCH=70ms
  ENC28J60_WriteRegister(ENC28J60_PHLCON, (4 << ENC28J60_PHLCON_LACFG_Pos) | (7 << ENC28J60_PHLCON_LBCFG_Pos) | (1 << ENC28J60_PHLCON_LFRQ_Pos) | ENC28J60_PHLCON_STRCH);
  // 禁止半双工回环
  ENC28J60_WriteRegister(ENC28J60_PHCON2, ENC28J60_PHCON2_HDLDIS);
  // 打开中断: 全局中断, 接收中断, 网线插拔中断, 发送出错中断, 接收出错中断
  ENC28J60_WriteRegister(ENC28J60_EIE, ENC28J60_EIE_INTIE | ENC28J60_EIE_PKTIE | ENC28J60_EIE_LINKIE | ENC28J60_EIE_TXERIE | ENC28J60_EIE_RXERIE);
  ENC28J60_WriteRegister(ENC28J60_PHIE, ENC28J60_PHIE_PLNKIE | ENC28J60_PHIE_PGEIE);
  // 允许将收到的数据包放入buffer
  ENC28J60_WriteRegister(ENC28J60_ECON1, ENC28J60_ECON1_RXEN); 
}

/* 读寄存器 */
uint16_t ENC28J60_ReadRegister(uint8_t addr)
{
  uint16_t value = 0;
  
  if ((addr & 0x80) == 0)
  {
    // 读控制寄存器
    if (!ENC28J60_IS_COMMON_REG(addr))
      ENC28J60_SelectBank(addr >> 5);
    ENC28J60_Execute(ENC28J60_READ_CTRL_REG, addr & 0x1f, &value, -1);
  }
  else
  {
    // 读PHY寄存器
    ENC28J60_WriteRegister(ENC28J60_MIREGADR, addr & 0x1f);
    ENC28J60_WriteRegister(ENC28J60_MICMD, ENC28J60_MICMD_MIIRD);
    while (ENC28J60_ReadRegister(ENC28J60_MISTAT) & ENC28J60_MISTAT_BUSY);
    ENC28J60_WriteRegister(ENC28J60_MICMD, 0);
    value = ENC28J60_ReadRegister(ENC28J60_MIRDL);
    value |= ENC28J60_ReadRegister(ENC28J60_MIRDH) << 8;
  }
  return value;
}

/* 选择寄存器区域 */
void ENC28J60_SelectBank(uint8_t bank)
{
  uint16_t value;
  
  assert_param(bank < 4);
  if (enc28j60_bank != bank)
  {
    value = ENC28J60_ReadRegister(ENC28J60_ECON1);
    assert_param((value & ENC28J60_ECON1_BSEL) == (enc28j60_bank << ENC28J60_ECON1_BSEL_Pos));
    
    value &= ~ENC28J60_ECON1_BSEL;
    value |= (bank << ENC28J60_ECON1_BSEL_Pos) & ENC28J60_ECON1_BSEL;
    ENC28J60_WriteRegister(ENC28J60_ECON1, value);
  }
}

/* 将寄存器的某些位置1或清0 */
// 请注意: 只有名称以E开头的寄存器才能使用这个函数
void ENC28J60_SetRegister(uint8_t addr, uint8_t bits, uint8_t value)
{
  assert_param((addr & 0x80) == 0); // 不允许为PHY寄存器
  if (!ENC28J60_IS_COMMON_REG(addr))
    ENC28J60_SelectBank(addr >> 5);
  
  if (value == 0)
    ENC28J60_Execute(ENC28J60_BIT_FIELD_CLR, addr & 0x1f, &bits, 1);
  else
    ENC28J60_Execute(ENC28J60_BIT_FIELD_SET, addr & 0x1f, &bits, 1);
  
  if (addr == ENC28J60_ECON1)
  {
    bits = ENC28J60_ReadRegister(ENC28J60_ECON1);
    enc28j60_bank = (bits & ENC28J60_ECON1_BSEL) >> ENC28J60_ECON1_BSEL_Pos;
  }
}

/* 写寄存器 */
void ENC28J60_WriteRegister(uint8_t addr, uint16_t value)
{
  if ((addr & 0x80) == 0)
  {
    // 写控制寄存器
    if (ENC28J60_IS_COMMON_REG(addr))
    {
      if (addr == ENC28J60_ECON1)
        enc28j60_bank = (value & ENC28J60_ECON1_BSEL) >> ENC28J60_ECON1_BSEL_Pos;
    }
    else
      ENC28J60_SelectBank(addr >> 5);

    ENC28J60_Execute(ENC28J60_WRITE_CTRL_REG, addr & 0x1f, &value, 1);
  }
  else
  {
    // 写PHY寄存器
    ENC28J60_WriteRegister(ENC28J60_MIREGADR, addr & 0x1f);
    ENC28J60_WriteRegister(ENC28J60_MIWRL, value & 0xff);
    ENC28J60_WriteRegister(ENC28J60_MIWRH, value >> 8);
    while (ENC28J60_ReadRegister(ENC28J60_MISTAT) & ENC28J60_MISTAT_BUSY);
  }
}

在main.c里面测试接收数据包、检测网线插拔:

#include <stdio.h>
#include <stm32f1xx.h>
#include "common.h"
#include "ENC28J60.h"

int main(void)
{
  uint8_t mac[] = {0x00, 0x12, 0x34, 0x56, 0x78, 0x90};
  uint16_t status;
  
  HAL_Init();
  
  clock_init();
  usart_init(115200);
  printf("STM32F103ZE ENC28J60\n");
  printf("SystemCoreClock=%u\n", SystemCoreClock);
  
  ENC28J60_Init(mac);
  while (1)
  {
    if (ENC28J60_GetITStatus())
    {
      status = ENC28J60_ReadRegister(ENC28J60_EIR);
      if (status & ENC28J60_EIR_PKTIF)
      {
        printf("[Recv] len=%d, ", ENC28J60_BeginReception());
        printf("next=%d\n", ENC28J60_GetNextPacketPointer());
        ENC28J60_EndReception();
      }
      if (status & ENC28J60_EIR_LINKIF)
      {
        ENC28J60_ReadRegister(ENC28J60_PHIR);
        if (ENC28J60_ReadRegister(ENC28J60_PHSTAT2) & ENC28J60_PHSTAT2_LSTAT)
          printf("Link is up!\n");
        else
          printf("Link is down!\n");
      }
      if (status & ENC28J60_EIR_TXERIF)
      {
        printf("ENC28J60 Tx error!\n");
        ENC28J60_ClearRegisterBits(ENC28J60_EIR, ENC28J60_EIR_TXERIF);
      }
      if (status & ENC28J60_EIR_RXERIF)
      {
        printf("ENC28J60 Rx error!\n");
        ENC28J60_ClearRegisterBits(ENC28J60_EIR, ENC28J60_EIR_RXERIF);
      }
    }
  }
}

运行程序,能正确响应网线插拔, 并且还能看到收到的每个数据包的大小(len),next是下一个数据包在缓冲区中的位置。

四、移植lwip-2.1.3协议栈

添加lwip库文件

lwip官网下载lwip-2.1.3的压缩包:lwip-2.1.3.zip和contrib-2.1.0.zip。
下载好了之后,在工程里面建立一个lwip-2.1.3文件夹,然后将lwip-2.1.3.zip里面的以下文件复制到工程的lwip-2.1.3文件夹中:
lwip-2.1.3/src/include/*
lwip-2.1.3/src/core/*
lwip-2.1.3/src/netif/ethernet.c
lwip-2.1.3/src/apps/http/*
lwip-2.1.3/src/apps/netbiosns/*
contrib-2.1.0.zip里面需要复制的文件是contrib-2.1.0/examples/ethernetif/ethernetif.c,复制到工程的lwip-2.1.3/src/netif里面去。
现在把工程lwip-2.1.3文件夹里面所有的*.c文件都添加到工程中。注意lwip-2.1.3/src/apps/http目录下只添加fs.c和httpd.c这两个文件,其他的不添加,如下图所示。

将lwip-2.1.3/include添加到头文件包含路径中:

为了避免warning:  #2532-D: support for trigraphs is disabled这个警告,应该在Misc Controls栏填入--trigraphs。

编写lwip-2.1.3/include/arch/cc.h,内容如下:

#ifndef LWIP_ARCH_CC_H
#define LWIP_ARCH_CC_H

#define LWIP_RAND() ((u32_t)rand())
#define PACK_STRUCT_BEGIN __packed // struct前的__packed

#endif

编写lwip-2.1.3/include/lwipopts.h,内容如下:

#ifndef LWIP_LWIPOPTS_H
#define LWIP_LWIPOPTS_H

#define NO_SYS 1 // 无操作系统
#define SYS_LIGHTWEIGHT_PROT 0 // 不进行临界区保护

#define LWIP_NETCONN 0
#define LWIP_SOCKET 0

#define MEM_ALIGNMENT 4 // STM32单片机是32位的单片机, 因此是4字节对齐的
#define MEM_SIZE 10240 // lwip的mem_malloc函数使用的堆内存的大小

// 配置TCP
#define TCP_MSS 1500
#define LWIP_TCP_SACK_OUT 1 // 允许选择性确认

// 配置DHCP
#define LWIP_DHCP 1
#define LWIP_NETIF_HOSTNAME 1

// 配置DNS
#define LWIP_DNS 1

// 广播包过滤器
// 如果打开了这个过滤器, 那么就需要在套接字上设置SOF_BROADCAST选项才能收发广播数据包
//#define IP_SOF_BROADCAST 1
//#define IP_SOF_BROADCAST_RECV 1

// 配置IPv6
#define LWIP_IPV6 1
#define LWIP_ND6_RDNSS_MAX_DNS_SERVERS LWIP_DNS // 允许SLAAC获取DNS服务器的地址

#endif

为了避免以下两个编译错误,应该在common.c中实现fflush函数,这个函数是由LWIP_ASSERT调用的。
.\Objects\enc28j60.axf: Error: L6200E: Symbol __stdout multiply defined (by stdio_streams.o and common.o).
.\Objects\enc28j60.axf: Error: L6200E: Symbol __stderr multiply defined (by stdio_streams.o and common.o).

/* 刷新输出缓冲区 */
// LWIP_ASSERT会调用此函数
int fflush(FILE *stream)
{
  return 0;
}

另外还需要实现sys_now函数,否则会出现下面的编译错误:
.\Objects\enc28j60.axf: Error: L6218E: Undefined symbol sys_now (referred from timeouts.o).

/* 获取系统时间毫秒数 (lwip协议栈要求实现的函数) */
// 该函数必须保证: 除非定时器溢出, 否则后获取的时间必须大于先获取的时间
uint32_t sys_now(void)
{
  return HAL_GetTick();
}

实现后在common.h中声明一下:uint32_t sys_now(void);

与ENC28J60网络接口绑定

网口初始化函数

将enc28j60和lwip绑定是由lwip-2.1.3/netif/ethernetif.c完成的。
打开这个文件后,首先要将
#if 0 /* don't build, this is only a skeleton, see previous comment */
改为
#if 1

然后修改low_level_init函数,在里面指定网卡MAC地址00:12:34:56:78:90,然后调用刚才我们编写的ENC28J60_Init初始化函数。请注意MAC地址的第一个字节必须为偶数,因为如果是奇数的话这个MAC地址就是一个多播地址,这是不允许的!
netif->flags要去掉NETIF_FLAG_LINK_UP选项,使网口的初始状态为未连接。增加NETIF_FLAG_MLD6选项,这样电脑才能ping通板子的IPv6地址。
netif->mtu=1500指的是网络层数据的最大长度,而前面说的1518字节是以太网数据包的最大大小。以太网数据包=目的地址(6字节)+源地址(6字节)+类型(2字节)+网络层数据(n字节)+CRC校验码(4字节),18+n≤1518,所以n≤1500。

// 包含头文件
#include <netif/ethernetif.h>
#include "../ENC28J60.h"

static void
low_level_init(struct netif *netif)
{
  //struct ethernetif *ethernetif = netif->state;

  /* set MAC hardware address length */
  netif->hwaddr_len = ETHARP_HWADDR_LEN;

  /* set MAC hardware address */
  // 指定网卡MAC地址
  netif->hwaddr[0] = 0x00;
  netif->hwaddr[1] = 0x12;
  netif->hwaddr[2] = 0x34;
  netif->hwaddr[3] = 0x56;
  netif->hwaddr[4] = 0x78;
  netif->hwaddr[5] = 0x90;
  printf("MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n", netif->hwaddr[0], netif->hwaddr[1], netif->hwaddr[2], netif->hwaddr[3], netif->hwaddr[4], netif->hwaddr[5]);

  /* maximum transfer unit */
  netif->mtu = 1500;

  /* device capabilities */
  /* don't set NETIF_FLAG_ETHARP if this device is not an ethernet one */
  netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP; // 网卡默认状态为: 未连接
  netif->flags |= NETIF_FLAG_MLD6; // 启用IPv6多播 (必须要启用这个选项, 才能ping通IPv6地址)

#if LWIP_IPV6 && LWIP_IPV6_MLD
  /*
   * For hardware/netifs that implement MAC filtering.
   * All-nodes link-local is handled by default, so we must let the hardware know
   * to allow multicast packets in.
   * Should set mld_mac_filter previously. */
  if (netif->mld_mac_filter != NULL) {
    ip6_addr_t ip6_allnodes_ll;
    ip6_addr_set_allnodes_linklocal(&ip6_allnodes_ll);
    netif->mld_mac_filter(netif, &ip6_allnodes_ll, NETIF_ADD_MAC_FILTER);
  }
#endif /* LWIP_IPV6 && LWIP_IPV6_MLD */

  /* Do whatever else is needed to initialize interface. */
  ENC28J60_Init(netif->hwaddr); // 初始化网口
}

新建lwip-2.1.3/include/netif/ethernetif.h头文件,内容如下:

#ifndef ETHERNETIF_H
#define ETHERNETIF_H

#ifndef _BV
#define _BV(n) (1ull << (n))
#endif

err_t ethernetif_init(struct netif *netif);
void ethernetif_input(struct netif *netif);

#endif

里面声明了ethernetif_init和ethernetif_input函数,这两个函数将会在main.c中使用,所以必须在头文件中声明。
回到刚才的ethernetif.c文件,我们需要将下面这句话的static关键字去掉,文件里面一共有两处

/* Forward declarations. */
static void  ethernetif_input(struct netif *netif);
static void
ethernetif_input(struct netif *netif)

文件最底部的ethernetif_init函数里面有一句netif->hostname = "lwip",这个设置的是板子在路由器管理页面中的显示名称。可以设置成STM32F103ZE_ENC28J60,或者其他自己喜欢的名字。

#if LWIP_NETIF_HOSTNAME
  /* Initialize interface hostname */
  netif->hostname = "STM32F103ZE_ENC28J60";
#endif /* LWIP_NETIF_HOSTNAME */

数据包发送函数

先调用ENC28J60_BeginTransmission函数指定要发送的数据包的大小,如果数据包太大发送不了的话,函数的返回值ret就会等于-1,然后下方return就要改成ERR_MEM。
如果ret=0那么就用ENC28J60_WriteMemory函数将p里面的数据拷贝到ENC28J60的发送缓冲区,然后用ENC28J60_EndTransmission函数发送出去。

static err_t
low_level_output(struct netif *netif, struct pbuf *p)
{
  //struct ethernetif *ethernetif = netif->state;
  struct pbuf *q;
  int ret;

#if ETH_PAD_SIZE
  pbuf_remove_header(p, ETH_PAD_SIZE); /* drop the padding word */
#endif
  
  printf("[Send] len=%u\n", p->tot_len);
  ret = ENC28J60_BeginTransmission(p->tot_len);
  if (ret == 0) {
    for (q = p; q != NULL; q = q->next) {
      /* Send the data from the pbuf to the interface, one pbuf at a
         time. The size of the data in each pbuf is kept in the ->len
         variable. */
      ENC28J60_WriteMemory(q->payload, q->len);
    }
    ENC28J60_EndTransmission();

    MIB2_STATS_NETIF_ADD(netif, ifoutoctets, p->tot_len);
    if (((u8_t *)p->payload)[0] & 1) {
      /* broadcast or multicast packet*/
      MIB2_STATS_NETIF_INC(netif, ifoutnucastpkts);
    } else {
      /* unicast packet */
      MIB2_STATS_NETIF_INC(netif, ifoutucastpkts);
    }
    /* increase ifoutdiscards or ifouterrors on error */
  }

#if ETH_PAD_SIZE
  pbuf_add_header(p, ETH_PAD_SIZE); /* reclaim the padding word */
#endif

  LINK_STATS_INC(link.xmit);

  return (ret == 0) ? ERR_OK : ERR_MEM;
}

数据包接收函数

先调用ENC28J60_BeginReception函数获取收到的数据包的大小,然后开辟内存,用ENC28J60_ReadMemory函数从ENC28J60的接收缓冲区读取数据,读完之后调用ENC28J60_EndReception函数结束读取。
如果内存开辟失败,则不读取数据,直接调用ENC28J60_EndReception函数结束。

static struct pbuf *
low_level_input(struct netif *netif)
{
  //struct ethernetif *ethernetif = netif->state;
  struct pbuf *p, *q;
  u16_t len;
  int next;

  /* Obtain the size of the packet and put it into the "len"
     variable. */
  len = ENC28J60_BeginReception();
  next = ENC28J60_GetNextPacketPointer();
  printf("[Recv] len=%u, next=%d\n", len, next);

#if ETH_PAD_SIZE
  len += ETH_PAD_SIZE; /* allow room for Ethernet padding */
#endif

  /* We allocate a pbuf chain of pbufs from the pool. */
  p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);

  if (p != NULL) {

#if ETH_PAD_SIZE
    pbuf_remove_header(p, ETH_PAD_SIZE); /* drop the padding word */
#endif

    /* We iterate over the pbuf chain until we have read the entire
     * packet into the pbuf. */
    for (q = p; q != NULL; q = q->next) {
      /* Read enough bytes to fill this pbuf in the chain. The
       * available data in the pbuf is given by the q->len
       * variable.
       * This does not necessarily have to be a memcpy, you can also preallocate
       * pbufs for a DMA-enabled MAC and after receiving truncate it to the
       * actually received size. In this case, ensure the tot_len member of the
       * pbuf is the sum of the chained pbuf len members.
       */
      ENC28J60_ReadMemory(q->payload, q->len);
    }
    ENC28J60_EndReception();

    MIB2_STATS_NETIF_ADD(netif, ifinoctets, p->tot_len);
    if (((u8_t *)p->payload)[0] & 1) {
      /* broadcast or multicast packet*/
      MIB2_STATS_NETIF_INC(netif, ifinnucastpkts);
    } else {
      /* unicast packet*/
      MIB2_STATS_NETIF_INC(netif, ifinucastpkts);
    }
#if ETH_PAD_SIZE
    pbuf_add_header(p, ETH_PAD_SIZE); /* reclaim the padding word */
#endif

    LINK_STATS_INC(link.recv);
  } else {
    ENC28J60_EndReception(); // drop packet
    LINK_STATS_INC(link.memerr);
    LINK_STATS_INC(link.drop);
    MIB2_STATS_NETIF_INC(netif, ifindiscards);
  }

  return p;
}

修改主函数

现在我们可以修改main.c里面的main函数,初始化lwip,设置板子IP地址了。

#include <lwip/apps/httpd.h>
#include <lwip/apps/netbiosns.h>
#include <lwip/dhcp.h>
#include <lwip/dns.h>
#include <lwip/init.h>
#include <lwip/netif.h>
#include <lwip/timeouts.h>
#include <netif/ethernetif.h>
#include <stdio.h>
#include <stm32f1xx.h>
#include "common.h"
#include "ENC28J60.h"

static struct netif netif_enc28j60;

/* 显示板子获取到的IP地址 */
static void display_ip(void)
{
  const ip_addr_t *addr;
  static uint8_t ip_displayed = 0;
  static uint8_t ip6_displayed = 0;
  int i, ip_present;
  int dns = 0;

  if (netif_dhcp_data(&netif_enc28j60) == NULL)
    ip_present = 1; // 使用静态IP地址
  else if (dhcp_supplied_address(&netif_enc28j60))
    ip_present = 2; // 使用DHCP获得IP地址, 且已成功获取到IP地址
  else
    ip_present = 0; // 使用DHCP获得IP地址, 且还没有获取到IP地址

  // 显示IPv4地址
  if (ip_present)
  {
    if (ip_displayed == 0)
    {
      ip_displayed = 1;

      if (ip_present == 2)
        printf("DHCP supplied address!\n");
      printf("IP address: %s\n", ipaddr_ntoa(&netif_enc28j60.ip_addr));
      printf("Subnet mask: %s\n", ipaddr_ntoa(&netif_enc28j60.netmask));
      printf("Default gateway: %s\n", ipaddr_ntoa(&netif_enc28j60.gw));
      dns = 1;
    }
  }
  else
    ip_displayed = 0;

  // 显示IPv6地址
  for (i = 1; i < LWIP_IPV6_NUM_ADDRESSES; i++) // 0号地址是本地链路地址, 不需要显示
  {
    if (ip6_addr_isvalid(netif_ip6_addr_state(&netif_enc28j60, i)))
    {
      if ((ip6_displayed & _BV(i)) == 0)
      {
        ip6_displayed |= _BV(i);
        printf("IPv6 address %d: %s\n", i, ipaddr_ntoa(netif_ip_addr6(&netif_enc28j60, i)));
        dns = 1;
      }
    }
    else
      ip6_displayed &= ~_BV(i);
  }

  // 显示DNS服务器地址
  // 在lwip中, IPv4 DHCP和IPv6 SLAAC获取到的DNS地址会互相覆盖
  if (dns)
  {
    addr = dns_getserver(0);
    if (ip_addr_isany(addr))
      return;
    printf("DNS Server: %s", ipaddr_ntoa(addr));

    addr = dns_getserver(1);
    if (!ip_addr_isany(addr))
      printf(" %s", ipaddr_ntoa(addr));
    printf("\n");
  }
}

/* 配置板子的IP地址 */
// use_dhcp=0: 静态配置
// use_dhcp=1: 自动从路由器获取
static void net_config(int use_dhcp)
{
  ip4_addr_t ipaddr, netmask, gw;

  // 将ENC28J60网卡添加到lwip
  if (use_dhcp)
    netif_add_noaddr(&netif_enc28j60, NULL, ethernetif_init, netif_input); // 添加网卡, 但不配置IP地址
  else
  {
    IP4_ADDR(&ipaddr, 192, 168, 1, 20); // IP地址
    IP4_ADDR(&netmask, 255, 255, 255, 0); // 子网掩码
    IP4_ADDR(&gw, 192, 168, 1, 1); // 默认网关
    netif_add(&netif_enc28j60, &ipaddr, &netmask, &gw, NULL, ethernetif_init, netif_input); // 添加网卡
  }
  netif_set_default(&netif_enc28j60); // 设为默认网卡
  netif_set_up(&netif_enc28j60); // 启用网卡

  // 启动DHCP服务器
  if (use_dhcp)
    dhcp_start(&netif_enc28j60);

  // 创建IPv6本地链路地址, 并从路由器获取公网IPv6地址
  netif_create_ip6_linklocal_address(&netif_enc28j60, 1);
  printf("IPv6 link-local address: %s\n", ipaddr_ntoa(netif_ip_addr6(&netif_enc28j60, 0)));
  netif_set_ip6_autoconfig_enabled((struct netif *)(uintptr_t)&netif_enc28j60, 1);
}

int main(void)
{
  uint16_t status;
  
  HAL_Init();
  
  clock_init();
  usart_init(115200);
  printf("STM32F103ZE ENC28J60\n");
  printf("SystemCoreClock=%u\n", SystemCoreClock);
  
  lwip_init();
  net_config(1);
  
  httpd_init(); // 启动网页服务器
  netbiosns_init();
  netbiosns_set_name("STM32F103ZE"); // 设置设备名
  
  while (1)
  {
    if (ENC28J60_GetITStatus())
    {
      status = ENC28J60_ReadRegister(ENC28J60_EIR);
      if (status & ENC28J60_EIR_LINKIF)
      {
        // 网络连接状态发生变化, 通知lwip
        ENC28J60_ReadRegister(ENC28J60_PHIR);
        if (ENC28J60_ReadRegister(ENC28J60_PHSTAT2) & ENC28J60_PHSTAT2_LSTAT)
        {
          // 已插入网线
          printf("Link is up!\n");
          netif_set_link_up(&netif_enc28j60);
        }
        else
        {
          // 已拔出网线
          printf("Link is down!\n");
          netif_set_link_down(&netif_enc28j60);
        }
      }
      if (status & ENC28J60_EIR_PKTIF)
      {
        // 处理收到的数据包
        while (ENC28J60_GetPacketCount() != 0)
          ethernetif_input(&netif_enc28j60);
      }
      if (status & ENC28J60_EIR_TXERIF)
      {
        // 发送出错
        printf("ENC28J60 Tx error!\n");
        ENC28J60_ClearRegisterBits(ENC28J60_EIR, ENC28J60_EIR_TXERIF);
      }
      if (status & ENC28J60_EIR_RXERIF)
      {
        // 接收出错 (通常是因为接收缓冲区不够了)
        printf("ENC28J60 Rx error!\n");
        ENC28J60_ClearRegisterBits(ENC28J60_EIR, ENC28J60_EIR_RXERIF);
      }
    }
    
    // 如果获取到了IP地址就显示
    display_ip();
    
    // lwip内部定时处理
    sys_check_timeouts();
  }
}

一个struct netif变量代表一个网络接口。注意一下netif_set_up/down和netif_set_link_up/down的区别,netif_set_up/down指的是启用或禁用网络接口,而netif_set_link_up/down指的是通知lwip网络连接已连上或断开。
sys_check_timeouts函数是lwip内部的定时处理函数,只要sys_now()函数正常工作,后调用的返回值永远大于先调用的返回值(除非32位数溢出),就没有问题。
裸机环境下,初始化lwip的函数是lwip_init()。netif_add()添加网卡时最后一个参数填的是netif_input,也可以填ethernet_input,是一样的。netif_input只是多了一个网络接口类型的判断,是以太网网络接口的话最终还是会调用ethernet_input,否则如果是PPP点对点接口则调用的是ip_input。
netif_add的倒数第二个参数是ethernetif.c里面定义的网口初始化函数ethernetif_init。倒数第三个参数是给网口初始化函数传递的自定义参数,可传递任意数据,在网口初始化函数中可通过netif->state读取到。

如果是带操作系统的环境下,初始化lwip的函数就必须换成tcpip_init(),netif_add()的最后一个参数必须换成tcpip_input。所有的raw API函数(包括像netif_add这样的函数)在非tcpip_thread线程外使用,使用前都必须调用LOCK_TCPIP_CORE(),使用后必须调用UNLOCK_TCPIP_CORE()。

我们之前在ethernetif.c的low_level_init函数中去掉了netif->flags的NETIF_FLAG_LINK_UP选项,所以网卡默认状态是未连接状态。
netif_add添加网卡后,只要netif_set_up启用了网卡,就可以马上调用dhcp_start启动DHCP服务器,不需要等到连上网络后netif_set_link_up再启动DHCP。
此外,netif_set_link_up函数内部会调用dhcp_network_changed,进而调用dhcp_reboot,所以网络断开后再连接,DHCP会自动重新获取IP地址,不需要自己再去调用dhcp_start。
需要注意的是,dhcp_start只是启动DHCP服务器,函数返回时,IP地址还没有获取到。在裸机环境下,用dhcp_supplied_address判断是否获取到IP地址,应该在main函数的while(1)主循环里面进行。dhcp_start后马上用dhcp_supplied_address判断,判断结果肯定是没有获取到。

五、最终效果

板子能正常收发数据,并从路由器获取到IPv4地址、IPv6地址和DNS服务器的地址:

电脑能ping通板子的IPv4地址和设备名:

电脑能ping通板子的IPv6本地链路地址和公网地址:

电脑能用浏览器访问板子上的网页服务器,并且一直按住F5刷新也没问题:

网线拔出后又重新插上,板子也能自动识别到,DHCP也能重新获取IP地址。
获取到的DNS服务器的地址也有可能是IPv6地址。(想要禁止获取IPv6 DNS地址,只允许获取IPv4 DNS地址的话就需要在lwipopts.h里面去掉LWIP_ND6_RDNSS_MAX_DNS_SERVERS选项)

在路由器管理页面中看到的设备名是ethernetif.c的ethernetif_init函数的netif->hostname指定的名称:

  • 8
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
STM32F1是意法半导体公司推出的一款32位ARM Cortex-M3单片机系列产品,具有良好的性能和扩展能力。它采用了低功耗技术,集成了丰富的外设和内存,适用于广泛的应用领域。 寄存器是计算机体系结构中的重要组成部分,用于存储和操作数据。STM32F1芯片内部集成了大量的寄存器,包括通用寄存器、特殊功能寄存器和外设寄存器等。通过对寄存器的读写操作,可以实现对芯片内部各种功能的配置和控制。 LwIP-2.1.2是一个开源的轻量级网络协议栈,适用于嵌入式系统。它提供了TCP/IP协议栈的实现,支持各种网络协议和服务,例如IP、TCP、UDP、ARP、DHCP、DNS等。LwIP-2.1.2具有较小的内存占用和高性能的特点,适用于资源有限的嵌入式环境。 ENC28J60是一款低成本的SPI以太网控制器芯片,由微芯科技(Microchip Technology)公司推出。它支持10Mbps以太网通信,采用硬件SPI接口和内部缓存,能够有效减少主控制器的负担。ENC28J60STM32F1可以通过SPI总线进行连接,用于实现嵌入式设备与以太网的通信。 综上所述,STM32F1是一款强大的单片机系列产品,具备丰富的外设和可编程寄存器,可以灵活配置和控制芯片内部功能。LwIP-2.1.2是一个轻量级的网络协议栈,用于实现嵌入式系统的网络通信。ENC28J60是一款低成本的以太网控制器芯片,可以与STM32F1通过SPI总线进行连接。这些技术的结合可以实现嵌入式设备的网络功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

巨大八爪鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值