ENC28J60基于AVRNET修改ENC28J60驱动过程(STM32+ CubeMx + ENC28J60)

背景(一些没用的话,建议跳过)

想给自己的MCU接入网络,在某宝上入手了一块网口模块(ENC28J60),第一次接触SPI接口,信心满满的以为和以往的TTL、RS485、RS232没什么区别,链接到电脑也是一个COM接口,可以通过串口调试工具发送指令、接收指令。所以在买网口的同时还买了SPI转USB模块,实时证明这个模块白买了,SPI根本不能通过串口工具调试!SPI链接电脑后也不是串口!

MCU: stm32f103c6t6

网络模块: ENC28J60

因为尽量照顾对一些概念性东西不熟悉的同学,本次写的非常啰嗦,请见谅。

如果第一次接触ENC28J60,请准备好,它可能没想象中那么容易,但当调通后,一定也会有也没有想象中那么难的感觉!

主要介绍内容

本章不会讨论原理,只希望开始接触enc28j60的同学更快入门,写出第一个hello world!。

网上介绍ENC28J60设备的文章主要是CSDN 的xukai871105: https://blog.csdn.net/xukai871105/article/details/13931833  

写的很好,非常适合想搞嵌入式的朋友学习,理解。但对于一些初学者, 想快速入手的同学来说,确实有些头大,不知道该从哪里入手的感觉。

主要介绍以下内容

1. 简单介绍SPI

2. ENC28J60驱动获取、修改SPI读写操作位置、HAL库的SPI读写、GPIO读写、MCU接入

3. 测试ENC28J60通讯是否正常,写一个最简单的测试驱动通讯是否正常小程序,类似软件的Hello World,通过Wireshark监控,能监控到对应消息表示驱动通讯正常!

4. 遇见的一些简单问题,以及问题原因

接入ENC28J60前准备

SPI相关介绍

如果有兴趣,建议简单了解一下SPI接口,至少知道SPI接口的基本通讯四根线(MISO、MOSI、SCK、CS),ENC28J60要和MCU通讯,也需要这四根线。

个人感觉SPI比TTL要底层,通讯效率要比TTL要高,速度当然也比TTL快。

为方便理解,MISO和MOSI,防止搞混,全名是:

MISO: Master(主机) Input(接收) Slave(从机) Output(发送)

MOSI:Master(主机) Output(发送) Slave(从机) Input(接收)

SCK: 控制主、从设备通讯频率,由主机控制

CS: 片选信号,由主机选择那个从设备进行通讯

关于CS(片选信号)的一些自己的理解

可以把它理解成一个单纯的GPIO 输出,一般情况下是低电平有效,默认高电平状态, 在和从机(ENC28J60)通讯前,第一步就是拉低电平,在SPI通讯(发送指令、发送数据.....) 最后拉高点平.

SPI设计的就是一对多的情况,一个主机,可以连接多个从机,但同时只能跟一个从机进行通讯。

最后,SPI在数据通讯时, 是有读有写的。要想读入数据,需要先向从机发送数据!

在操作ENC28J60前,需要掌握SPI的读写函数、控制GPIO 高低电平(控制CS针脚高低电平)

HAL库SPI读写函数

此处只介绍HAL库的SPI读写函数,其他的读写方式请自行百度,因为我不会O-O。

非常重要,这是操作enc28j60设备的第一步,也是后续所有操作的基础,如果你也是使用STM32+HAL库(CubeMx)+Keil5,可以跳过所有,下载最下边提到的keil5 helloworld项目,尝试跑通"第一个程序"。

HAL库对SPI做了封装,只需要调用HAL_SPI_TransmitReceive函数即可:

HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size,
                                          uint32_t Timeout)

hspi: SPI实例指针, CubeMX已声明SPI示例,实例中包括SPIMISO、MOSI、SCK指针位置、和一些其他参数

pTxData: 要发送的数据

pRxData: 要接收的数据

Size: 发送/接收 数据长度,因为SPI读写是同时进行的,如果不理解请自行百度具体介绍

Timeout: 超时时间

 比如我想读取1字节数据,要想读1字节,需要先写1字节:

//接收的数据
uint8_t Rxdata = 0x00;
//发送的数据
uint8_t Txdata;
//hspi1 cubemx生成的SPI实例
//1000为超时时间,毫秒
HAL_SPI_TransmitReceive(&hspi1, &TxData, &Rxdata,sizeof(TxData), 1000)

HAL库GPIO Output 高低点平控制

 相对上边介绍的SPI,高低电平控制就简单的多:

HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)

GPIOx: 针脚所在区

GPIO_Pin: 针脚所在位置

PinState: 状态枚举, 高电平: GPIO_PIN_SET,   低电平: GPIO_PIN_RESET

详细GPIO 高低点平控制介绍,可以看我之前记录的随笔: https://www.cnblogs.com/GengMingYan/p/15614068.html

通过CubeMx创建项目

具体详细暂不介绍,这里只介绍SPI配置信息,SPI参数全部默认即可:

 

 

 

PDF电子书文档:

最后,通过CubeMx生成Keil5项目,生成前记得勾选Generate peripheral initialzation as a pair of '.c/.h' fiels per peripheral:

 

 

 

开始修改ENC28J60驱动

如果不想看修改过程,可以直接跳过,直接看SPI实现和GPIO实现部分,通过AVR修改好的驱动文件(enc28j60.h和enc28j60.c)已放在文章最后。

如果是STM32 + HAL库,那可以不看SPI实现和GPIO实现部分,直接下载我修改好的,只关注代码中用到的MCU针脚就可以。

首先,ENC28J60驱动是从国外的一个项目(AVRNET)中获取的,因为操作ENC28J60过程非常复杂(至少对于我来说是这样),如果从0开始实现需要了解很多概念性东西,并且也没有必要重复造轮子。

从gitee或github上拉取或打包下载AVRNET项目,AVRNET项目地址已写在本章最后。

项目目录如下,我们只需要用项目中的enc28j60.c和enc28j60.h:

 

 

 由于AVRNET项目使用的是AVR类型单片机,有很多数据类型、SPI、GPIO 操作方式和STM32不一样,但最终效果都一样,SPI读写,GPIO高低点平控制。

把enc28j60.h和enc28j60.c文件放入开发工具中,这里我使用的Keil5,具体怎么添加头文件和源文件请自行百度。

需要自己修改/实现的地方:

1. SPI读写

  需要实现通过SPI读写一字节数据,所有ENC28J60操作都需要基于实现的读写函数

2. GPIO 高低点平控制

  实现高低电平控制,控制CS片选信号,ENC28J60操作数据前置低电平,操作完后置高点平

3. 类型定义

  AVR有自己的类型BYTE(1字节)、WORD_BYTES(2字节)等一些其他类型,在STM32中并没有,需要用unsigned char(1字节)和unsigned short(2字节)代替

4. 睡眠函数实现(ms毫秒)

   HAL提供了线程的睡眠函数:HAL_Delay函数,实现睡眠指定毫秒,有一些操作,是需要等待几十毫秒到几百毫秒的等待响应时间

5. 删除一些AVR初始化GPIO、SPI操作

GPIO高低点平控制CS

主要实现操作:  CSACTIVE(置低电平,使能ENC28J60)和CSPASSIVE(置高电平,释放ENC28J60使能,低电平有效)

如下,CS针脚我在MCU中用的是A4,CS针脚不一定是A4,只要是可以GPIO 高低电平输出的针脚,都可以用来当SPI 的CS片选:

enc28j60.h

//新增头
//gpio.h 主要用在操作CS高低点平
#include "gpio.h"
//置A4针脚低电平,激活从机,APIOA A区   GPIO_PIN_4  4针脚
#define CSACTIVE HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
//置A4针脚高电平,释放从机
#define CSPASSIVE HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)

SPI读写在驱动中实现

在AVRNET提供的ENC28J60驱动中,有一个全局变量(1字节),临时存储要写入的数据和读出的数据:

变量名为: SPDR

enc28j60.h声明SPDR类型:

用unsigned char代替1字节的全局变量

//要读写的数据,设置后立即调用waitspi(),这是AVR的读写方式,在waitspi中实现SPI读写
#define SPDR_TYPE unsigned char

enc28j60.c中定义SPDR变量:

...
//SPDR变量
SPDR_TYPE SPDR;
...

所以读写一字节的操作流程是(伪代码):

SPDR = 0x01//要写入的内容置到SPDR中
waitspi()//调用waitspi向enc28j60(寄存器)发送(0x01)并读取1字节数据(读取内容到SPDR中),读到的内容存在SPDR变量中
print(SPDR)//打印读取的1字节数据

 SPI读写在驱动中实现

调用HAL_SPI_TransmitReceive函数,需要引入"spi.h"头文件

enc28j60.h中声明:

void waitspi()

enc28j60.c:

void waitspi() {
    //hspi1 为cubemx生成好的SPI实例,存储MISO、MOSI、SCK针脚位置信息和一些SPI的其他参数
    if(HAL_SPI_TransmitReceive(&hspi1, &SPDR, &SPDR, sizeof(SPDR), 1000) != HAL_OK) {
        //读写错误
        print("error!");//可去掉,仅打印调试信息到串口,方便排查问题
        return ;
    }
    //读写正确
}

LOW和HIGH函数实现

在项目中一些地方用了LOW和HIGH函数,并没有明白具体用途:

//不明白...
BYTE LOW(int d) {
    return d & 0xFF;
}
//不明白...
BYTE HIGH(int d) {
    return d >> 8;
}

类型定义

AVR项目驱动中用到的STM32中没有的一些类型代替声明:

//新增的定义
#define BYTE unsigned char //1字节
#define WORD unsigned short  //2字节
#define WORD_BYTES unsigned short  //2字节

睡眠函数实现(ms毫秒)

这里使用HAL库带的HAL_Delay

//睡指定毫秒
void _delay_ms(int sleep_ms) {
    HAL_Delay(sleep_ms);
}

 

 删除、修改处,比较复杂的地方

 1. enc28j60.h删除AVR GPIO初始化

enc28j60.h

....
#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

//删除的地方 AVR高低点平、SPI读写
// set CS to 0 = active
//#define CSACTIVE PORTB &= ~_BV(PB4)
// set CS to 1 = passive
//#define CSPASSIVE PORTB |= _BV(PB4)
//#define waitspi() while(!(SPSR&(1<<SPIF)))
//删除的地方 AVR高低点平、SPI读写 END

// The RXSTART_INIT should be zero. See Rev. B4 Silicon Errata
// buffer boundaries applied to internal 8K ram
// the entire available packet buffer space is allocated
//
#define MAX_TX_BUFFER    1500
#define MAX_RX_BUFFER    1500
....

2. 修改每包接收数据最大为常量1500

.....
#define
TXSTART_INIT (8192-1500) #define TXSTOP_INIT 8192 // // max frame length which the conroller will accept: //修改为常量 1500,每包可接受数据最大长度 //#define MAX_FRAMELEN (1500+sizeof(ETH_HEADER)+4) // maximum ethernet frame length #define MAX_FRAMELEN 1500 #define ENC28J60_RESET_PIN_DDR DDD3 #define ENC28J60_INT_PIN_DDR DDD2
....

3. 修改操作enc28j60PhyWritePHY位置

enc28j60.c中:

void enc28j60PhyWrite(BYTE address, WORD_BYTES data)
{
    // set the PHY register address
    enc28j60Write(MIREGADR, address);
    // write the PHY data
    //修改地方...
    //enc28j60Write(MIWRL, data.byte.low);
    //enc28j60Write(MIWRH, data.byte.high);
    //修改为
    enc28j60Write(MIWRL, data);
    enc28j60Write(MIWRH, data>>8);
    // wait until the PHY write completes
    while(enc28j60Read(MISTAT) & MISTAT_BUSY)
    {
        //睡15微秒,是否添加在自己,不睡也没太大的问题个人感觉
        //目前并不知道怎么睡15微秒...所以此处注释
        //_delay_us(15);
    }
}

4. 初始化enc28j60_init一些操作修改

enc28j60.c中:

void enc28j60_init( BYTE *avr_mac)
{
    // initialize I/O
    //DDRB |= _BV( DDB4 );
    //打开注释,CS置高电平
    CSPASSIVE;
    //AVR初始化SPI的一些操作,注释
    /*
    // enable PB0, reset as output 
    ENC28J60_DDR |= _BV(ENC28J60_RESET_PIN_DDR);

    // enable PD2/INT0, as input
    ENC28J60_DDR &= ~_BV(ENC28J60_INT_PIN_DDR);
    ENC28J60_PORT |= _BV(ENC28J60_INT_PIN);

    // set output to gnd, reset the ethernet chip
    ENC28J60_PORT &= ~_BV(ENC28J60_RESET_PIN);
    _delay_ms(10);

    // set output to Vcc, reset inactive
    ENC28J60_PORT |= _BV(ENC28J60_RESET_PIN);
    _delay_ms(200);

    //    
    DDRB  |= _BV( DDB4 ) | _BV( DDB5 ) | _BV( DDB7 ); // mosi, sck, ss output
    //DDRB &= ~_BV( DDB6 ); // MISO is input

    CSPASSIVE;
    PORTB &= ~(_BV( PB5 ) | _BV( PB7 ) );
    //
    // initialize SPI interface
    // master mode and Fosc/2 clock:
    SPCR = _BV( SPE ) | _BV( MSTR );
    SPSR |= _BV( SPI2X );
    
    */
    //AVR初始化SPI的一些操作,注释 END
    // perform system reset
    enc28j60WriteOp(ENC28J60_SOFT_RESET, 0, ENC28J60_SOFT_RESET);
    _delay_ms(50);

    // check CLKRDY bit to see if reset is complete
    // The CLKRDY does not work. See Rev. B4 Silicon Errata point. Just wait.
    //打开注释,如果初始化不成功将进入死循环,直到初始化成功
    while(!(enc28j60Read(ESTAT) & ESTAT_CLKRDY));
    // do bank 0 stuff
    // initialize receive buffer
    // 16-bit transfers, must write low byte first
    // set receive buffer start address
    
    //去掉.word,下一包数据开始指针
    //next_packet_ptr.word = RXSTART_INIT;
    next_packet_ptr = RXSTART_INIT;
    // Rx start
    enc28j60Write(ERXSTL, RXSTART_INIT&0xFF);
    enc28j60Write(ERXSTH, RXSTART_INIT>>8);
        ....
}

5. 修改包接收函数一些操作,相对其他来说最复杂的地方

enc28j60.c中:

WORD enc28j60_packet_receive ( BYTE *rxtx_buffer, WORD max_length )
{
    WORD_BYTES rx_status, data_length;
    
    // check if a packet has been received and buffered
    // if( !(enc28j60Read(EIR) & EIR_PKTIF) ){
    // The above does not work. See Rev. B4 Silicon Errata point 6.
    if( enc28j60Read(EPKTCNT) == 0 )
    {
        return 0;
    }
    //修改最多的地方,不懂最多的地方, 把所有  <WORD_BYTES类型变量>.word 都改为 <WORD_BYTES类型变量>
    //中文注释下的代码,是需要修改的地方
    
    // Set the read pointer to the start of the received packet
    //设置读取指针为接收数据包开头?? ↑
    //enc28j60Write(ERDPTL, next_packet_ptr.bytes[0]);
    //enc28j60Write(ERDPTH, next_packet_ptr.bytes[1]);
    enc28j60Write(ERDPTL, (next_packet_ptr));
    enc28j60Write(ERDPTH, (next_packet_ptr)>>8);
    
    // read the next packet pointer
    //读取下一包指针??? ↑
    //next_packet_ptr.bytes[0] = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0);
    //next_packet_ptr.bytes[1] = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0);
    next_packet_ptr = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0);
    next_packet_ptr |= enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0) << 8;
    

    // read the packet length (see datasheet page 43)
    // 读取数据包长度
    //data_length.bytes[0] = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0);
    //data_length.bytes[1] = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0);
    data_length = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0);
    data_length |= enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0) << 8;
    
    //删除CRC 计数..还是不懂
    //data_length.word -=4; //remove the CRC count
    data_length -= 4;
    
    // read the receive status (see datasheet page 43)
    //读取接收状态
    //rx_status.bytes[0] = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0);
    //rx_status.bytes[1] = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0);
    rx_status = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0);
    rx_status |= enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0)<<8;
    
    //应该是防止数据越界,做的一些判断
    /*if ( data_length.word > (max_length-1) )
    {
        data_length.word = max_length-1;
    }
    */
    if ( data_length > (max_length-1) )
    {
        data_length = max_length-1;
    }
    
    
    // check CRC and symbol errors (see datasheet page 44, table 7-3):
    // The ERXFCON.CRCEN is set by default. Normally we should not
    // need to check this.
    //判断接收状态,如果接收到数据,读出来
    /*
    if ( (rx_status.word & 0x80)==0 )
    {
        // invalid
        data_length.word = 0;
    }
    else
    {
        // read data from rx buffer and save to rxtx_buffer
        rx_status.word = data_length.word;
        CSACTIVE;
        // issue read command
        SPDR = ENC28J60_READ_BUF_MEM;
        waitspi();
        while(rx_status.word)
        {
            rx_status.word--;
            SPDR = 0x00;
            waitspi();
            *rxtx_buffer++ = SPDR;
        }
        CSPASSIVE;
    }
    */
    if ( (rx_status & 0x80)==0 )
    {
        //未读到数据
        // invalid
        data_length = 0;
    }
    else
    {
        //成功读到数据
        //临时变量,要操作包索引
        WORD_BYTES data_length_index = data_length;
        //开始读
        CSACTIVE;
        //发送读指令
        SPDR = ENC28J60_READ_BUF_MEM;
        waitspi();
        while(data_length_index) {
            data_length_index--;
            SPDR = 0x00;
            waitspi();
            *rxtx_buffer++ = SPDR;
        }
        CSPASSIVE;
        //开始读 END
    }
    // Move the RX read pointer to the start of the next received packet
    // This frees the memory we just read out
    //还是不懂...
    //enc28j60Write(ERXRDPTL, next_packet_ptr.bytes[0]);
    //enc28j60Write(ERXRDPTH, next_packet_ptr.bytes[1]);
    enc28j60Write(ERXRDPTL, (next_packet_ptr));
    enc28j60Write(ERXRDPTH, (next_packet_ptr)>>8);
    // decrement the packet counter indicate we are done with this packet
    enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON2, ECON2_PKTDEC);
    //这懂,发送接收到的数据包长度
    //return( data_length.word );
    return( data_length);
}

 

到此,驱动应该可以在STM32中编译了。

MCU和ENC28J60接线

 

 

MCU和ENC28J60链接,需要(最少)4根线:

ENC28J60      MCU(STM32F103C6T6)

CS   <--->    GPIO OUTPUT CS针脚,本篇介绍为PA4

SCK      <--->    SCK 时钟信号,由频率主机控制, 本篇介绍为 PA5

MISO  <--->    主机接收,从机发送,本篇介绍为 PA6

MOSI  <--->    主机发送,从机接收,本篇介绍为 PA7

调试ENC28J60需要循序渐进,慢慢来。

可以先调通初始化函数,enc28j60_init

在调通enc28j60_packet_send函数,电脑端成功收到发送的数据

在接入UIP 、LWIP框架,实现ICMP(ping)、TCP、UDP协议通讯

开始调试ENC28J60

驱动文件可以成功通过编译后,在main.c中引入enc28j60.h,开始写第一个Hello World:

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "spi.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "string.h"
#include "enc28j60.h"
/* USER CODE END Includes */


//打印调试信息
void print(char *data) {
    
    HAL_UART_Transmit(&huart1, (uint8_t*)data, strlen(data), 1000);
}
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 *//* USER CODE END 1 */
  

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();//GPIO初始化cubemx自动生成
  MX_SPI1_Init();//SPI初始化自动生成
  MX_USART1_UART_Init();//UART初始化自动生成
  /* USER CODE BEGIN 2 */
    
  MX_USART1_UART_Init();//自动生成
    //等待一些时间,准备初始化ENC28J60
  /* USER CODE BEGIN 2 */
    for(int i = 0;i < 20;i++) {
        //enc28j60PhyWrite(PHLCON,0x7a4);    
        HAL_Delay(500);
        print("begin init enc28j60...");
    }
    //ENC28J60网卡地址
    unsigned char my_mac[6] = {0x29, 0x7C, 0x07, 0x37, 0x24, 0x63};
    //开始初始化,如果初始化不成功会阻塞
    enc28j60_init(my_mac);
    //表示初始化成功,说明接线正常
    print("init enc28j60 success!!!");
    
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
    
  while (1)
  {
    /* USER CODE END WHILE */
        //向网口发送物理网卡地址,直连电脑,通过Wireshark看是否能收到物理网卡地址,并且对比是否发送正确
enc28j60_packet_send(my_mac, 6); HAL_Delay(1000); /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }

通过网线直连电脑,电脑不用配置网关、IP地址等一些信息。

发送内容不一定必须是物理网卡地址,可以发送0xAA, 0xBB, 0xCC等,看电脑端Wireshark能否正常接收消息,接收消息是否和发送消息一致

通过Wireshark监控对应网口,如果发送成功,会在列表中发现如下灰色项目:

双击打开:

 

 

 如图,0x29  0x7c  0x07  0x37  0x24  0x63 为成功接收到的消息,并且消息接收正确。

 到此,MCU和ENC28J60成功通讯! 

 

ENC28J60相关资料

已修改好的STM32驱动(根据AVRNET,cubemx+keil5   HAL库):https://wwb.lanzouw.com/i9bnmy0cuja

  代码中用到的针脚是: CS: A4  SCK: A5  MISO: A6   MOSI: A7,不是所有针脚都能当MISO、MOSI、SCK!!!大部分针脚能当CS (GPIO 输出)

cubemx+keil5 已测试成功helloworld项目,每秒发送一次网卡地址: https://wwb.lanzouw.com/i0Euey0czef

  代码中用到的针脚是: CS: A4  SCK: A5  MISO: A6   MOSI: A7,不是所有针脚都能当MISO、MOSI、SCK!!!大部分针脚能当CS (GPIO 输出)

PDF中文电子书: https://wwb.lanzouw.com/iUihexzikri 密码:bvqf

gitee AVRNET项目地址: https://gitee.com/liming2019/AVRNET

github AVRNET 项目地址: https://github.com/JonTian/AVRNET

某宝中ENC28J60 驱动: https://wwb.lanzouw.com/i981lxzz2kf

  从某宝上找到的资料中的ENC28J60驱动,也是从AVRNET项目中改过来的,这个驱动文件给我改AVRNET提供了很好的参考(手抄)信息。

结尾

从了解SPI,到找ENC28J60驱动,到成功通讯,到使用UIP ping通,前前后后花了15天左右时间(不是15天每天都在搞,也是要上班赚钱的o-o),在这里特别感谢CSDN的qllaoda帮助,遇到问题在CSDN提问后,给我解答,和私聊指导。

CSDN 提问: enc28j60 + UIP + STM32F103C6T6 电脑端接收数据错误,导致不能通讯问题

CSDN 提问: enc28j60 可以正常正常接收ARP消息,但是ping不通?

 

posted @ 2021-12-26 18:16  耿明岩  阅读(1545)  评论(1编辑  收藏  举报
希望能帮助到你,顺利解决问题! ...G(^_−)☆