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不通?