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

article/2025/11/5 16:07:36

程序下载链接:百度网盘 请输入提取码(提取码: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);elseHAL_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_Hextern 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 VectorENC28J60_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);// 数据包个数减1ENC28J60_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 Logicprintf("%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: PA6gpio.Mode = GPIO_MODE_INPUT;gpio.Pin = GPIO_PIN_1 | GPIO_PIN_6;gpio.Pull = GPIO_NOPULL;HAL_GPIO_Init(GPIOA, &gpio);// SPI1_NSS: PA4gpio.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: PA7gpio.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: PB9gpio.Mode = GPIO_MODE_OUTPUT_PP;gpio.Pin = GPIO_PIN_9;gpio.Speed = GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOB, &gpio);// 初始化SPIhspi1.Instance = SPI1;hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32; // 72MHz/32=2.25MHzhspi1.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=70msENC28J60_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);// 允许将收到的数据包放入bufferENC28J60_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);elseENC28J60_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;}elseENC28J60_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");elseprintf("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))
#endiferr_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_SIZEpbuf_remove_header(p, ETH_PAD_SIZE); /* drop the padding word */
#endifprintf("[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 atime. The size of the data in each pbuf is kept in the ->lenvariable. */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_SIZEpbuf_add_header(p, ETH_PAD_SIZE); /* reclaim the padding word */
#endifLINK_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_SIZElen += 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_SIZEpbuf_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_SIZEpbuf_add_header(p, ETH_PAD_SIZE); /* reclaim the padding word */
#endifLINK_STATS_INC(link.recv);} else {ENC28J60_EndReception(); // drop packetLINK_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地址elseip_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;}}elseip_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;}}elseip6_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网卡添加到lwipif (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){// 网络连接状态发生变化, 通知lwipENC28J60_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指定的名称:


http://chatgpt.dhexx.cn/article/r42MZb7e.shtml

相关文章

enc28j60是带SPI接口的独立以太网控制器(即网卡),兼容IEEE 802.3,集成MAC和10 BASE-T PHY.而KSZ8081只是PHY芯片和网口扫盲三:以太网芯片MAC和PHY的关系

百度百科中介绍以太网控制器也称以太网适配器&#xff0c;就是我们通常称的“网卡”。电脑中网卡通过PCI和CPU相连&#xff0c;网卡上RJ45插网线水晶头。教程中的ENC28J60通过SPI和单片机相连&#xff0c;ENC28J60模块的RJ45插网线的水晶头 以太网控制器_百度百科 1.概述 enc2…

基于enc28j60的学习心得

1.概述 enc28j60是带SPI接口的独立以太网控制器&#xff0c;兼容IEEE 802.3&#xff0c;集成MAC和10 BASE-T PHY&#xff0c;最高速度可达10Mb/s。基于enc28j60控制器的理解可阅读文章&#xff1a; ENC28J60学习笔记&#xff0c;在该文章内详细介绍控制器的使用方法&#xff0c…

单片机学习:手把手教你移植LWIP(ENC28J60)

这里只是移植&#xff0c;所以LWIP那么多的协议都不需要管&#xff0c;只要知道哪里需要我们修改&#xff0c;为什么修改就可以了。 上图就是整个移植的基本思路&#xff0c;非常清晰的三个层次。其实想想&#xff0c;本质上就是收发数据&#xff0c;只是LWIP协议通过对数据的…

单片机 STM32 HAL 网络模块 ENC28J60

文章目录 一、 简介二、特性三、示例代码 一、 简介 ENC28J60 是带有行业标准串行外设接口&#xff08;Serial Peripheral Interface&#xff0c;SPI&#xff09;的独立以太网控制器。它可作为任何配备有 SPI 的控制器的以太网接口。ENC28J60 符合IEEE 802.3的全部规范&#x…

ENC28J60 简介

单片机以太网方案 单片机想要使用以太网的话&#xff0c;通常有以下几种方案&#xff1a; 如果 MCU 内部集成 MAC 控制器&#xff0c;则只需外接一个 PHY 芯片就可以了如果 MCU 内部没有 MAC 控制器&#xff0c;需要外接 MAC 芯片和 PHY 芯片&#xff0c;这两颗芯片可以分立也…

c语言程序设计 国外教材,标准C程序设计(第7版国外计算机科学经典教材)

导语 内容提要 E.巴拉古路萨米著李周芳译的《标准C程序设计(第7版国外计算机科学经典教材)》专门用于满足渴望成为程序员的学生&#xff0c;最新版按照Bloom分类法所定的学习目标来呈现主题&#xff0c;支持基于学习的成果。本书解释了基本概念和高级内容&#xff0c;且主要关注…

学习C语言的教材

作者&#xff1a; 阮一峰 日期&#xff1a; 2011年9月18日 我的C语言是自学的&#xff0c;这些年看过不少教材。 下面&#xff0c;我对其中一些教材做个点评。 1. How to Think Like a Computer Scientist: C version 这是我读过最易懂的C语言教材。 虽然它只讲解最基本的语法&…

新概念c语言周二强07答案,新概念C语言能力教程(普通高等教育十二五规划教材)...

导语 内容提要 周二强编写的《新概念C语言能力教程(普通高等教育十二五规划教材)》以先进的教学理念为指导&#xff0c;以培养编程能力与学习能力为目标&#xff0c;从全新的角度解析了C语言&#xff0c;高屋建瓴地阐释了C语言学习中的诸多难点&#xff0c;对序列点、指针等概念…

计算机程序c语言教材,全国计算机等级考试二级C语言程序设计教材(2018年版)...

2018年计算机二级教材&#xff1a;C语言程序设计 简介 书名&#xff1a;全国计算机等级考试二级教程——C语言程序设计(2018年版) 作者&#xff1a;教育部考试中心 出版社&#xff1a;高等教育出版社 出版时间&#xff1a;2017年11月 ISBN&#xff1a;9787040488524 定价&#…

树莓派(0)C语言教材学习

学习日记的功能主要是记录学习C语言的知识还有难上手的地方&#xff0c;之后会记录配置树莓派环境还有设计linux环境下C语言的实验题目 目前在K&R的《C语言程序设计》教材学习&#xff0c;前面的内容没有难度&#xff0c;基本上和高级程序设计语言的学习思路一样&#xff0…

国内C语言教材中几种值得商榷的说法

作者&#xff1a;巨同升 “C语言程序设计”这门课程在国内高校普遍开设已有近三十年&#xff0c;课程的建设和研究取得了长足的进步&#xff0c;涌现出了数量众多、各具特色的C语言教材。尽管如此&#xff0c;在许多C语言教材中还或多或少地存在着一些不准确甚至是值得商榷的说…

既然谭浩强的C语言教材不好,那应该选什么书作C语言教材?

易道云学院C语言/C语法学习不在于你看了多少书&#xff0c;而在于你实实在在写了多少有效代码。易道云学院 回到这个问题&#xff0c;其实我个人认为&#xff0c;看什么样的书&#xff0c;也是需要应对不一样的场景易道云学院去有目的地涉猎。我暂时想到了几种情况&#xff0c;…

c语言课本答案解析宋士银,c语言教材

22.40定价&#xff1a;28.00(8折) /2007-02-01 根据教育部高等学校计算机科学与技术教学指导委员会提出的《关于进一步加强高等学校计算机基础教学的意见暨计算机基础课程教学基本要求》的有关要求&#xff0c;编者组织了一批多年工作在教学一线且有丰富教学经验的教师编写了《…

C语言的环境变量配置

一、编辑器选择与安装 这边选用的编辑器是比较常见的devc&#xff0c;当然vscode和vc也都可以用来学习C语言&#xff0c;要软件和C语言环境变量的话可以加QQ群&#xff1a;373270625 第一步全部默认勾选就行无脑下一步 第二步选择一个放软件的文件夹&#xff0c;我这边选择的…

Jmeter环境变量配置

解压后 电脑桌面----》“计算机”图标----》鼠标右键选择“属性”----》点击高级系统设置----》高级---》环境变量页面 1.在系统变量框&#xff0c;点击“新建”&#xff0c;建立一个变量&#xff1a;JMETER_HOME,值为你解压的jmeter安装路径 2.配置classpath变量&#xff0c…

java设置环境变量jre_JRE环境变量配置图解

JRE(Java Runtime Environment,Java运行环境),运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。如果大家需要查看JRE环境变量配置图解过程,看完本文你的问题也就迎刃而解了。 我们这里使用jre-7u67-windows-i586的32位JRE安装包,大小只有20多M,比JDK7小了…

Anaconda环境变量配置

Anaconda Anaconda环境变量配置 学习自用 解决问题 例如在VSCode中使用虚拟环境运行python代码导致的问题 *ImportError: DLL load failed while importing win32gui*新建系统变量&#xff0c;添加Anaconda安装路径 以后想用其他环境直接修改变量值就可以了 新建环境变量 变…

npm环境变量配置

NPM 使用介绍 NPM是随同NodeJS一起安装的包管理工具&#xff0c;能解决NodeJS代码部署上的很多问题&#xff0c;常见的使用场景有以下几种&#xff1a; 允许用户从NPM服务器下载别人编写的第三方包到本地使用。允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用。…

windows 10中R的环境变量配置

创建于&#xff1a;20221113 修改于&#xff1a;20221113 文章目录 1、情况介绍2、环境变量配置3、参考资料 1、情况介绍 win10 64bit系统中&#xff0c;已经安装好了R&#xff0c;Rtools &#xff0c;Rstudio&#xff0c;并且已经配置了java开发环境。 上述两个链接讲述的非…

SVN环境变量配置

1、环境变量配置 2、复制地址 3、环境变量配置&#xff1a; 步骤: 右击电脑属性、高级系统设置、高级、环境变量、 系统变量、找到Path、双击进入、新建粘贴到刚刚复制的地址&#xff0c;确定即可。