16-對part14-spi-ethernet的翻譯和搬運

非黑色字體均為我自己添加
圖均為原文中的圖
原文放在文章末尾
10英鎊以內(nèi)的裸機(jī)以太網(wǎng)
????????建立你自己OS令人興奮,但是直到你給它與外界通信的能力,你的可能性都被限制了.實際上,我們簡單的藍(lán)牙通訊建立起來并且運行 -- 但是如果我們要做任何有意義的事,我們就要是當(dāng)?shù)木W(wǎng)絡(luò).
????????在這個教程中,我們將要連接到一個格外的以太網(wǎng)控制器(如果你喜歡,一張網(wǎng)卡也行),通過使用樹莓派串行外設(shè)接口(SPI).
????????你需要的東西:
一個?ENC28J60 以太網(wǎng)模塊(https://www.amazon.co.uk/dp/B00DB76ZSK) -- 它花了我不到6英鎊并且每一分都值(注意,代碼只在這個型號上測試過)
一些公對公的跳線 -- 花了我不到2.5英鎊
一根以太網(wǎng)網(wǎng)線來連接到我的路由器上
連接ENC28J60以太網(wǎng)模塊
????????我跟隨這里的非常有用的指示(https://www.instructables.com/Super-Cheap-Ethernet-for-the-Raspberry-Pi/)來把我的ENC28J60掛載到樹莓派的SPI0接口上.
????????我們不會現(xiàn)在就連接中斷線,所以現(xiàn)在有6個接線頭(我建議過顏色)用來連接.


這里有一個(可能不太有用的)正確連接我的樹莓派的照片

SPI庫
????????讓我們通過看如何實現(xiàn)SPI來開始.
????????我不打算寫一篇關(guān)于SPI如何工作并且我們?yōu)槭裁葱枰拈L文,因為這在其他地方已經(jīng)有詳細(xì)文檔了(https://learn.sparkfun.com/tutorials/serial-peripheral-interface-spi/).推薦它作為背景閱讀,但是如果只是想讓某些東西順利運轉(zhuǎn)起來那就并不重要.
????????看到 lib/spi.c.它使用一些你會記得來自之前的教程的 lib/spi.c 里現(xiàn)存的函數(shù).實際上,我們往 include/io.h 中添加了兩個函數(shù),這樣我們就可以從我們的SPI庫里調(diào)用它.
void gpio_setPinOutputBool(unsigned int pin_number, unsigned int onOrOff);
void gpio_initOutputPinWithPullNone(unsigned int pin_number);
????????特別的, spi_init() 設(shè)置GPIO 7,9,10和11使用ALT0函數(shù).使用?BCM2711 ARM外設(shè)文檔(https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf),第77頁,你就會發(fā)現(xiàn)這個把SPI0映射到GPIO頭文件.GPIO8被映射為一個輸出引腳,因為我們打算使用這個向ENC28J60發(fā)出我們想要通信的信號.實際上, spi_chip_select() 函數(shù)接收一個 true/false (布爾值) 參數(shù),它設(shè)置或者清除這個引腳.
????????看到136頁的 SPI0 的映射寄存器,我們發(fā)現(xiàn)這個反應(yīng)了我們的?REGS_SPI0 結(jié)構(gòu)體.它給予我們方便的訪問 SPI0 外設(shè)的內(nèi)存映射寄存器.
????????我們的 spi_send_recv() 函數(shù)然后為了通信進(jìn)行一些設(shè)置:
設(shè)置?DLEN?寄存器中需要傳輸?shù)淖止?jié)數(shù)量(我們需要傳進(jìn)函數(shù)的長度)
清楚 RX 和 TX 的FIFO緩沖
設(shè)置傳輸活動(TA)標(biāo)志
????????然后當(dāng)數(shù)據(jù)讀寫到時候(我們并沒有讀寫比我們所要求的更多的數(shù)據(jù)),我們使用我們傳進(jìn)去的緩沖來讀寫FIFO.一旦我們認(rèn)為完成了,我們等待直到SPI接口同意,即CS寄存器中的?DONE 標(biāo)志被設(shè)置.如果有額外的字節(jié)需要閱讀,我們就僅僅拋棄它們(好吧,現(xiàn)在把它們放到屏幕上,因為這不應(yīng)該發(fā)生).
????????最后,為了絕對的確定,我們清零 TA 標(biāo)志.
????????我已經(jīng)設(shè)置好了兩個方便的函數(shù) --?spi_send() 和?spi_recv() -- 它運行 spi_send_recv(),主要是讓將來的代更加易讀.
ENC28J60的驅(qū)動
????????我們現(xiàn)在看到 net/ 子文件夾.
????????enc28j60.c 和 enc28j60.h 都組成了?ENC28J60以太網(wǎng)模塊的驅(qū)動代碼.當(dāng)我們可能勞動數(shù)月來基于以太網(wǎng)模塊的的手冊(http://ww1.microchip.com/downloads/en/devicedoc/39662c.pdf)編寫我們自己的驅(qū)動代碼的時候,我選擇利用別人的幸苦工作.這感覺贏了,我可以毫不費力的把其他人的優(yōu)秀代碼帶到我的OS中!然而,我確實理解了每次這個代碼都在做什么(可選的!).
????????感謝這個GitHub代碼庫(https://github.com/wolfgangr/enc28j60)保存了我?guī)讉€月的工作.我對這個代碼做了寫改動,但是并沒有值得寫進(jìn)文檔的.如果你十分愿意看看我做出了多小的改變,克隆這個代碼庫并且用好diff命令.
????????我需要做的事是寫一些連接驅(qū)動和樹莓派4硬件的橋接代碼.主要的,我說的是把我們的SPI庫掛載到驅(qū)動上 --?encspi.c 的全部原因.
????????它定義了4個驅(qū)動需要的函數(shù)(在 enc28j60.h 文件中有詳細(xì)的文檔):
void ENC_SPI_Select(unsigned char truefalse) {
? ? spi_chip_select(!truefalse); // If it's true, select 0 (the ENC), if false, select 1 (i.e. deselect the ENC)
}
void ENC_SPI_SendBuf(unsigned char *master2slave, unsigned char *slave2master, unsigned short bufferSize) {
? ? spi_chip_select(0);
? ? spi_send_recv(master2slave, slave2master, bufferSize);
? ? spi_chip_select(1); // De-select the ENC
}
void ENC_SPI_Send(unsigned char command) {
? ? spi_chip_select(0);
? ? spi_send(&command, 1);
? ? spi_chip_select(1); // De-select the ENC
}
void ENC_SPI_SendWithoutSelection(unsigned char command) {
? ? spi_send(&command, 1);
}
????????可能最令人困惑的就是芯片選擇了.通過一些嘗試和錯誤,我發(fā)現(xiàn)當(dāng)GPIO08被清除時,設(shè)備被選中,當(dāng)它被設(shè)置時,設(shè)備被取消選中.它說明這是因為ENC28J60的芯片選擇引腳拉低時活動,并且板子上名沒有翻轉(zhuǎn)器(可能為了省錢),所以GPIO8必須拉低來使能IC.
一些更多的定時器函數(shù)
????????我們的?ENC28J60 驅(qū)動需要的唯一的另一間東西是訪問一對定義良好的定時器函數(shù):
HAL_GetTick() -- 返回從開始到現(xiàn)在的時鐘滴答數(shù)
HAL_Delay() -- 延遲特定的毫秒數(shù)
????????它們在?kernel/kernel.c 中快速實現(xiàn)了并且在part13-interrupts后并不需要很多力氣:
unsigned long HAL_GetTick(void) {
? ? unsigned int hi = REGS_TIMER->counter_hi;
? ? unsigned int lo = REGS_TIMER->counter_lo;
? ? //double check hi value didn't change after setting it...
? ? if (hi != REGS_TIMER->counter_hi) {
? ? ? ? hi = REGS_TIMER->counter_hi;
? ? ? ? lo = REGS_TIMER->counter_lo;
? ? }
? ? return ((unsigned long)hi << 32) | lo;
}
void HAL_Delay(unsigned int ms) {
? ? unsigned long start = HAL_GetTick();
? ? while(HAL_GetTick() < start + (ms * 1000));
}
讓我們開始連接
????????所以我們有了能工作的驅(qū)動,它與我們的硬件通過 net/encspi.c 和?kernel/kernel.c 中一些函數(shù)進(jìn)行交互.現(xiàn)在怎么辦?
????????我們的內(nèi)核網(wǎng)絡(luò)設(shè)計目標(biāo)將會是:
證明我們將能與硬件溝通
成功帶起網(wǎng)絡(luò)
證明我們能通過網(wǎng)絡(luò)連上某些東西并且得到回應(yīng)
????????我完成這個目標(biāo)的計劃是:
證明我們可以探測到這個網(wǎng)絡(luò)在物理層面上是否建立連接,(CAT5線插入并且連接到一個工作的交換機(jī)上)
依賴ENC28J60的驅(qū)動來告訴我們成功連接
手工發(fā)送ARP請求并且等待從我的互聯(lián)網(wǎng)路由器的ARP響應(yīng)(設(shè)備從零知識的角度觸發(fā)來在網(wǎng)絡(luò)設(shè)備上"發(fā)現(xiàn)對方"的傳統(tǒng)方法)
????? ?看到?kernel/arp.c.首先我們創(chuàng)建了一個句柄來引用我們的驅(qū)動實例?ENC_HandleTypeDef handle.然后我們初始化 init_network() 中的結(jié)構(gòu)體:
handle.Init.DuplexMode = ETH_MODE_HALFDUPLEX;
handle.Init.MACAddr = myMAC;
handle.Init.ChecksumMode = ETH_CHECKSUM_BY_HARDWARE;
handle.Init.InterruptEnableBits = EIE_LINKIE | EIE_PKTIE;
????????這使得模塊以半雙工的模式啟動(不能同時收發(fā)),設(shè)置MAC地址(我的最愛:?C0:FF:EE:C0:FF:EE),告訴硬件去添加自己包的校驗和(我們不想不得不在軟件中創(chuàng)造它),并且使能"連接上/沒連接上"和"包接收到"的中斷信息.
????????然后我們調(diào)用驅(qū)動實例?ENC_Start(&handle) 并且檢查它返回 true(它完成了設(shè)計要求2 -- 驅(qū)動告訴我們已經(jīng)正確啟動).我們繼續(xù)使用 ENC_SetMacAddr(&handle) 設(shè)置 MAC 地址.
????????這行等待直到物理網(wǎng)絡(luò)連接已經(jīng)被建立(完成設(shè)計要求1)
while (!(handle.LinkStatus & PHSTAT2_LSTAT)) ENC_IRQHandler(&handle);
????????當(dāng)中斷被觸發(fā)后刷新驅(qū)動狀態(tài)的標(biāo)志時,驅(qū)動的?ENC_IRQHandler(&handle) 會被調(diào)用.因為我們并沒有連接中斷線,并且為了保持事情簡單,我們只是在軟件中不斷的輪詢.當(dāng)我們看見 handle.LinkStatus 標(biāo)志的?PHSTAT2_LSTAT 位被設(shè)置,我們知道連接上了(在模塊的數(shù)據(jù)手冊的第24頁有寫).
????????在我們完成之前,我們必須重新使能以太網(wǎng)中斷( ENC_IRQHandler() 禁止了它們,但沒有重新使能它們 -- 我通過讀代碼的時候發(fā)現(xiàn)的).
收發(fā)ARP
????????為了在以太網(wǎng)上傳輸,我們需要正確的格式化我們的包.ENC28J60 處理物理層(包括校驗和,因為我么要求它了),所以我們只需要關(guān)注數(shù)據(jù)傳輸層 -- 由一個頭文件和有效載荷組成.
????????頭文件(我們的?EtherNetII 結(jié)構(gòu)體)只是一個目的和源的MAC地址,加上一個16-bit的包類型(https://en.wikipedia.org/wiki/EtherType).舉個例子,ARP包,類型是0x0806.你會注意到我們的?#define ARPPACKET 包含了2字節(jié).這是因為大端序是網(wǎng)絡(luò)協(xié)議的主要序列,但是樹莓派是一個小端序架構(gòu)(這里可能需要一些閱讀!)我們必須全面的展開這項工作.
????????有效載荷是一個完全的定義在ARP結(jié)結(jié)構(gòu)體中的ARP包(https://en.wikipedia.org/wiki/Address_Resolution_Protocol).SendArpPacket() 函數(shù)在結(jié)構(gòu)體中設(shè)置了我們需要的數(shù)據(jù)(在注釋中有文檔)并且使用驅(qū)動調(diào)用來傳輸這些包.
// Send the packet
if (ENC_RestoreTXBuffer(&handle, sizeof(ARP)) == 0) {
? ?debugstr("Sending ARP request.");
? ?debugcrlf();
? ?ENC_WriteBuffer((unsigned char *)&arpPacket, sizeof(ARP));
? ?handle.transmitLength = sizeof(ARP);
? ?ENC_Transmit(&handle);
}
????????ENC_RestoreTXBuffer() 只是簡單的準(zhǔn)備傳輸緩沖區(qū)并且如果成功就返回0.ENC_WriteBuffer() 通過SPI把包送往?ENC28J60.然后我們在驅(qū)動狀態(tài)標(biāo)志里設(shè)置傳輸緩沖區(qū)長度并且調(diào)用?ENC_Transmit() 來告訴 ENC 來把包發(fā)往網(wǎng)絡(luò).
????????你可以看到?arp_test() 函數(shù)使用這種方法發(fā)送了第一個ARP.我們告訴它我們路由器的IP(我的例子是 192.168.0.1),但我們不知道它的MAC地址 -- 這就是我們想找的東西.一旦ARP被發(fā)送, arp_test() 等待接收以太網(wǎng)的包,檢查它們是否是為我們,并且如果它們是來自路由器的IP地址(因此有可能是對我們的請求的ARP應(yīng)答),我們打印出路由器的MAC地址.
????????這完成了設(shè)計要求3,因此我們完成了!我們所需要做的就是確保??kernel/kernel.c 以正確的順序調(diào)用我們的網(wǎng)絡(luò)例程.我選擇使用一些非常容易跟隨的在part13-interrupts的基礎(chǔ)上改動來達(dá)成它.基本上這是所有的我們需要的調(diào)用:
spi_init();
init_network();
arp_test();
????????想象一下,當(dāng)我看到我的路由器(正確!)MAC地址出現(xiàn)在屏幕上——一個生命的標(biāo)志,并證明我的操作系統(tǒng)實際上是聯(lián)網(wǎng)的!

原文如下

Bare metal Ethernet for under £10
It's exciting to build your own OS, but until you give it the ability to communicate with the outside world, your possibilities are limited. Indeed, our simple Bluetooth comms got us up and running - but if we're to do anything meaningful then we need proper networking.
In this tutorial, we're going to connect to an external Ethernet controller (a network card, if you like) using the RPi4's Serial Peripheral Interface (SPI).
Things you'll need:
?* An [ENC28J60 Ethernet module](https://www.amazon.co.uk/dp/B00DB76ZSK) - it cost me less than £6 and was worth every penny (n.b. code only tested on this exact model)
?* Some [female-to-female jumper cables](https://www.amazon.co.uk/dp/B072LN3HLG) - cost me less than £2.50
?* An Ethernet cable to connect to your Internet router
Connecting up the ENC28J60 Ethernet module
I followed the very helpful instructions [here](https://www.instructables.com/Super-Cheap-Ethernet-for-the-Raspberry-Pi/) to hook up the ENC28J60 to the RPi4's SPI0 interface.
We won't be connecting the interrupt line for now, so there are just six jumper leads (I've suggested colours) that need connecting:
?| Pi pin | Pi GPIO? ? ?| Jumper colour | ENC28J60 pin |
?| ------ | ----------- | ------------- | ------------ |
?| Pin 17 | +3V3 power? | Red? ? ? ? ? ?| VCC? ? ? ? ? |
?| Pin 19 | GPIO10/MOSI | Green? ? ? ? ?| SI? ? ? ? ? ?|
?| Pin 20 | GND? ? ? ? ?| Black? ? ? ? ?| GND? ? ? ? ? |
?| Pin 21 | GPIO09/MISO | Yellow? ? ? ? | SO? ? ? ? ? ?|
?| Pin 23 | GPIO11/SCLK | Blue? ? ? ? ? | SCK? ? ? ? ? |
?| Pin 24 | GPIO08/CE0? | Green? ? ? ? ?| CS? ? ? ? ? ?|

Here's a (not very useful) photo of my RPi4 connected correctly:

The SPI library
Let's start by looking at how we implement SPI communication.
I'm not going to write a long paper on how SPI works and why we need it, because it's [very well documented elsewhere](https://learn.sparkfun.com/tutorials/serial-peripheral-interface-spi/). It's recommended background reading, but not essential if all you want to do is get something working.
Look at _lib/spi.c_. It uses some of existing functions in _lib/io.c_ that you'll remember from earlier tutorials. In fact, I've added two functions to the _include/io.h_ header file so we can call them from our SPI library:
```c
void gpio_setPinOutputBool(unsigned int pin_number, unsigned int onOrOff);
void gpio_initOutputPinWithPullNone(unsigned int pin_number);
```
Specifically, `spi_init()` sets GPIO 7, 9, 10, and 11 to use the ALT0 function. Cross-referencing with the [BCM2711 ARM Peripherals document](https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf), page 77, you'll see that this maps SPI0 to the GPIO header. GPIO 8 is mapped as an output pin, since we'll use this to signal to the ENC28J60 that we want to talk. In fact, the `spi_chip_select()` function takes a true/false (boolean) parameter which either sets or clears this pin.
Looking at the SPI0 register map on page 136, we see this reflected in our `REGS_SPI0` structure. This gives us handy access to the SPI0 peripheral's memory-mapped registers.
Our `spi_send_recv()` function then sets us up for some communcation:
?* Sets the DLEN Register to the number of bytes to transfer (a length we passed into the function)
?* Clears the RX & TX FIFOs
?* Sets the Transfer Active (TA) flag
Then, whilst there's either data to write or data to read (and we haven't written/read more bytes than we asked for), we write to/read from the FIFO using the buffers we passed in. Once we think we're done, we wait until the SPI interface agrees i.e. the DONE flag in the CS Register is set. If there are extraneous bytes to read, we just throw them away (well, dump them to the screen for now because this shouldn't happen).
Finally, to be absolutely sure, we clear the TA flag.
I've then set up two convenient functions - `spi_send()` and `spi_recv()` - which exercise `spi_send_recv()`, mainly to make future code more readable.
The ENC28J60 drivers
Let's now look into the _net/_ subdirectory.
Both _enc28j60.c_ and _enc28j60.h_ make up the driver code for the ENC28J60 Ethernet module. Whilst we could have laboured for months writing our own driver based on [the module's datasheet](http://ww1.microchip.com/downloads/en/devicedoc/39662c.pdf), I chose to leverage somebody else's hard work instead. It felt like a win that I could effortlessly bring somebody else's good code into my own OS! I did, however, make sure I understood what the code was doing at every turn (optional!).
Thanks to [this Github repository](https://github.com/wolfgangr/enc28j60) for saving me months of work. I made a very few changes to the code, but nothing worth documenting here. If you're keen to see how little I needed to change, clone the repo and make good use of the `diff` command.
What I did need to do is write some bridging code between the driver and the RPi4 hardware. Essentially, I'm talking about hooking up our SPI library to the driver - the whole reason for _encspi.c_.
It defines four functions that the driver requires (well documented in the _enc28j60.h_ file):
```c
void ENC_SPI_Select(unsigned char truefalse) {
? ? spi_chip_select(!truefalse); // If it's true, select 0 (the ENC), if false, select 1 (i.e. deselect the ENC)
}
void ENC_SPI_SendBuf(unsigned char *master2slave, unsigned char *slave2master, unsigned short bufferSize) {
? ? spi_chip_select(0);
? ? spi_send_recv(master2slave, slave2master, bufferSize);
? ? spi_chip_select(1); // De-select the ENC
}
void ENC_SPI_Send(unsigned char command) {
? ? spi_chip_select(0);
? ? spi_send(&command, 1);
? ? spi_chip_select(1); // De-select the ENC
}
void ENC_SPI_SendWithoutSelection(unsigned char command) {
? ? spi_send(&command, 1);
}
```
Perhaps the most confusing aspect is the chip selection. Through a bit of trial & error I discovered that when GPIO08 is clear, the device is selected, and when it's set, the device is deselected. It turns out this is because the Chip Select pin on the ENC28J60 is active LOW and there's no invertor on the board (presumably to save costs), so GPIO08 must be LOW to enable the IC.
Some more timer functions
The only other thing our ENC28J60 driver requires is access to a couple of well-defined timer functions:
?* `HAL_GetTick()` - returns the current number of timer ticks since start
?* `HAL_Delay()` - delays by a specified number of milliseconds
These are quickly implemented in _kernel/kernel.c_ and weren't too much of a stretch after _part13-interrupts_:
```c
unsigned long HAL_GetTick(void) {
? ? unsigned int hi = REGS_TIMER->counter_hi;
? ? unsigned int lo = REGS_TIMER->counter_lo;
? ? //double check hi value didn't change after setting it...
? ? if (hi != REGS_TIMER->counter_hi) {
? ? ? ? hi = REGS_TIMER->counter_hi;
? ? ? ? lo = REGS_TIMER->counter_lo;
? ? }
? ? return ((unsigned long)hi << 32) | lo;
}
void HAL_Delay(unsigned int ms) {
? ? unsigned long start = HAL_GetTick();
? ? while(HAL_GetTick() < start + (ms * 1000));
}
```
Let's connect!
So we have a working driver that's interfacing with our hardware via _net/encspi.c_ and a few timer functions in _kernel/kernel.c_. Now what?
The design goals of our kernel's networking demo will be to:
?1. Prove we can talk to the hardware
?2. Bring the network up successfully
?3. Prove we can connect to something else on the network and get a response
My proposals for how we fulfil these goals are:
?1. Prove we can detect whether a network link has been established at a physical level (CAT5 cable plugged in and connected to a working switch)
?2. Rely on the ENC28J60 driver to tell us that we've started up successfully
?3. Handcraft and send an [ARP](https://en.wikipedia.org/wiki/Address_Resolution_Protocol) request and await an ARP response from my Internet router (the traditional way devices "find each other" on a network from a point of zero knowledge)
Look at _kernel/arp.c_. First we create a handle to reference our driver instance `ENC_HandleTypeDef handle`. We then initialise this structure in `init_network()`:
```c
handle.Init.DuplexMode = ETH_MODE_HALFDUPLEX;
handle.Init.MACAddr = myMAC;
handle.Init.ChecksumMode = ETH_CHECKSUM_BY_HARDWARE;
handle.Init.InterruptEnableBits = EIE_LINKIE | EIE_PKTIE;
```
This starts the module in half duplex mode (can't transmit & receive simultaneously), sets its MAC address (my favourite: `C0:FF:EE:C0:FF:EE`), tells the hardware to add its own packet checksums (we don't want to have to create them in software), and enables interrupt messages for "link up/down" and "packet received".
We then call the driver routine `ENC_Start(&handle)` and check it returns true (this fulfils design requirement 2 - the driver tells us we've started correctly). We go on to set the MAC address using `ENC_SetMacAddr(&handle)`.
This line waits until a physical network link has been established (fulfilling design requirement 1):
```c
while (!(handle.LinkStatus & PHSTAT2_LSTAT)) ENC_IRQHandler(&handle);
```
The driver's `ENC_IRQHandler(&handle)` routine would ordinarily be called when an interrupt was raised to refresh the driver status flags. Because we didn't hook up the interrupt line and to keep things simple, we're just polling in the software for now. When we see the `handle.LinkStatus` flag has the `PHSTAT2_LSTAT` bit set, we know the link is up (documented on page 24 of the module's datasheet).
Before we're done, we have to re-enable Ethernet interrupts (`ENC_IRQHandler()` disables them, but doesn't re-enable them - something I discovered by reading the code).
Sending/receiving an ARP
To transmit on an Ethernet network, we need to format our packets correctly. The ENC28J60 deals with the physical layer (including the checksum, as we asked it to), so we only need concern ourselves with the data link layer - made up of a header, and a payload.
The header (our `EtherNetII` struct) is simply a destination and source MAC address, as well as a [16-bit packet type](https://en.wikipedia.org/wiki/EtherType). ARP packets, for example, have type `0x0806`. You'll note in our `#define ARPPACKET` that we've swapped the two bytes. This is because big-endianness is the dominant ordering in network protocols, and the RPi4 is a little-endian architecture (some reading may be required here!). We've had to do this across the board.
The payload is the full [ARP packet](https://en.wikipedia.org/wiki/Address_Resolution_Protocol) defined in the `ARP` struct. The `SendArpPacket()` function sets up the data we need in the structure (documented in code comments) and then uses driver calls to transmit the packet:
```c
// Send the packet
if (ENC_RestoreTXBuffer(&handle, sizeof(ARP)) == 0) {
? ?debugstr("Sending ARP request.");
? ?debugcrlf();
? ?ENC_WriteBuffer((unsigned char *)&arpPacket, sizeof(ARP));
? ?handle.transmitLength = sizeof(ARP);
? ?ENC_Transmit(&handle);
}
```
`ENC_RestoreTXBuffer()` simply prepares the transmit buffer and returns 0 if successful. `ENC_WriteBuffer()` sends the packet to the ENC28J60 over the SPI. We then set the transmit buffer length in the driver status flags and call `ENC_Transmit()` to tell the ENC to send the packet across the network.
You'll see that the `arp_test()` function sends our first ARP this way. We tell it the IP of our router (`192.168.0.1` in my case), but we don't know its MAC address - that's what we want to find out. Once the ARP is sent, `arp_test()` then waits for received Ethernet packets, checks whether they're for us and, if they come from the router's IP address (therefore likely to be the ARP response to our request), we print out the router's MAC address.
This fulfils design requirement 3, and therefore we're done! All we need to do is ensure that _kernel/kernel.c_ calls our networking routines in the right order. I've chosen to do this on core 3 with a few easy-to-follow changes from where we left off in _part13-interrupts_. Essentially these are all the calls we need:
```c
spi_init();
init_network();
arp_test();
```
_Imagine how happy I was to see my router's (correct!) MAC address appear on-screen - a sign of life, and proof that my OS is actually networking!_
