程序下载链接:百度网盘 请输入提取码(提取码: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_NSS PA4
SPI1_SCK PA5
SPI1_MISO PA6
SPI1_MOSI PA7
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指定的名称:

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

  1. linux kernel有线网卡驱动enc28j60分析 一

    1.为了更好低学习linux的网络驱动架构,本文选择分析linux kernel下的有线网卡驱动enc28j60来学习网络驱动架构. enc28j60是一个10/100Mb的有线网卡,适用于嵌入式设备 ...

  2. linux 有线网卡,linux下有线网卡出现ADDRCONF(NETDEV_UP): eth0: link is not ready的解决方法...

    一.背景 2018年5月24日,笔者的pc已经连续运转两天了,突然要使用有线网卡,却发现有线网卡无法正常工作,于是查看了一下内核日志: r8169 0000:05:00.0 eth0: link do ...

  3. 有线网卡和无线网卡同时上网 优先级切换的设置方法

    2019独角兽企业重金招聘Python工程师标准>>> 默认有线网卡优先权高,如果你想改为无线高,那么: 1,进入网络属性的有线网卡的连接属性,选择TCP/IP属性,点"高 ...

  4. 关于ubuntu对有线网卡I219-V和无线网卡RTL8723DE 802.11b/g/n的支持问题

    输入lspci得到 00:1f.6 Ethernet controller: Intel Corporation Ethernet Connection (11) I219-V 05:00.0 Net ...

  5. ubuntu 网卡双网口 配置_无线网卡m2 ngff keya keye、minipcie接口改转多口有线网卡实现软路...

    小型主板及笔记本中的无线网卡m2ngffkeyakeye接口(CNVI除外)通过m2ngffkeyae转接pcie1x转接板,或者无线网卡的minipcie接口,通过minipcie转接pcie1x转 ...

  6. LwIP应用开发笔记之一:LwIP无操作系统基本移植

    现在,TCP/IP协议的应用无处不在.随着物联网的火爆,嵌入式领域使用TCP/IP协议进行通讯也越来越广泛.在我们的相关产品中,也都有应用,所以我们结合应用实际对相关应用作相应的总结. 1.技术准备 ...

  7. 免驱 usb有线网卡_Type-C转千兆有线网卡,MacBook也能接网线

    纵观整个笔记本电脑行业,苹果推出的MacBook系列笔记本电脑,凭借着出色的性能.轻薄便携的机身.以及强悍的硬件配置备受用户关注.对于职场办公人士而言,在选择笔记本电脑的过程中,它都会成为用户重点关注 ...

  8. 有线网卡驱动_WDS如何为boot.wim或install.wim添加驱动

    问题描述: 使用WDS为某些机型部署系统时,可能会出现无法引导的情况,比如某一款笔记本PXE引导后会弹出提示"WdsClient: 从 DHCP 服务器获取 IP 地址时出错.请检查以确保在 ...

  9. LwIP应用笔记(二):无操作系统支持下的RAW API移植

    欢迎来我的个人博客转转:https://www.codinglover.top 写在前面 由于从这篇博客开始要涉及代码编写了,为此笔者自行画板搭建了一个实验平台,以后的所有代码与步骤都会在此实验平台上 ...

  10. 电脑无线网卡和有线网卡同时使用

    一.概述 1.需要 由于笔者的无线联网比有线的速度快,所以连接互联网要用无线网卡. 我的虚拟机桥接与主机沟通,所以需要有线网卡连接局域网. 2.问题 如果同时连接有线网卡和无线网卡,因为有线网卡的优先 ...

最新文章

  1. LeetCode 213 House Robber II Python
  2. c语言srand函数怎么用_C语言的main函数到底该怎么写
  3. 录像带转存电脑的方法_误删微信记录别着急,教你几招可靠的恢复微信记录方法...
  4. dataframe的drop無效
  5. Ubuntu 16.04 安装 Matlab2016a
  6. Java里氏转换_详解Java设计模式编程中的里氏替换原则
  7. 信天游机票电子行程单_4月1日起,取得火车票、飞机票等咋抵扣?权威解答来了...
  8. AndroidStudio_gradle依赖相关错误的处理_Minimum supported Gradle version is 6.5. Current等---Android原生开发工作笔记228
  9. Android 设置闹铃步骤和基础代码
  10. 机器学习Normal Equation的推导(不要求矩阵求导)
  11. 2021年下软考高项信息系统项目管理师真题试卷答案解析
  12. 工业上服务器无线投屏到电视,台式电脑支持无线投屏吗 如何投屏到电视上
  13. 【SDOI2015】临行前的夜
  14. Android 通过AlarmClock设置系统闹钟
  15. 大数据发展和就业前景好吗大数据人才缺口有多大?
  16. android系统时间获取方式
  17. python 制作刷题程序
  18. 图像处理基本库的学习笔记5--公共数据集,PASCAL VOC数据集,NYUD V2数据集的简介与提取,COCO2017,医学影像数据集汇总
  19. 云堡垒机和传统堡垒机对比
  20. asp生成带参数的二维码并合成推广海报图片,asp合并合成推广海报图片asp代码

热门文章

  1. opensuse 安装网易云音乐(tumbleweed)(leap)
  2. STS安装lombok插件
  3. zemax操作例子_光学软件使用实例:从Zemax导入光学系统
  4. 双11,立减¥3554!戴尔官网撩客服砍价带走高性能电脑,速来!
  5. 固若金汤 - PostgreSQL pgcrypto加密插件
  6. linux添加模块设备,linux采用模块方法,添加一个新的设备
  7. 大话数据结构Java版第一节
  8. BP神经网络公式推导
  9. windows配置端口映射
  10. 新型智能搜索引擎,挑战google