背景(一些沒用的話,建議跳過)
想給自己的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不通?