欢迎光临散文网 会员登陆 & 注册

16-对part14-spi-ethernet的翻译和搬运

2023-08-20 15:57 作者:Xdz-2333  | 我要投稿

非黑色字体均为我自己添加

图均为原文中的图

原文放在文章末尾

10英镑以内的裸机以太网

        建立你自己OS令人兴奋,但是直到你给它与外界通信的能力,你的可能性都被限制了.实际上,我们简单的蓝牙通讯建立起来并且运行 -- 但是如果我们要做任何有意义的事,我们就要是当的网络.

        在这个教程中,我们将要连接到一个格外的以太网控制器(如果你喜欢,一张网卡也行),通过使用树莓派串行外设接口(SPI).

        你需要的东西:

  • 一个 ENC28J60 以太网模块(https://www.amazon.co.uk/dp/B00DB76ZSK) -- 它花了我不到6英镑并且每一分都值(注意,代码只在这个型号上测试过)

  • 一些公对公的跳线 -- 花了我不到2.5英镑

  • 一根以太网网线来连接到我的路由器上

连接ENC28J60以太网模块

        我跟随这里的非常有用的指示(https://www.instructables.com/Super-Cheap-Ethernet-for-the-Raspberry-Pi/)来把我的ENC28J60挂载到树莓派的SPI0接口上.

        我们不会现在就连接中断线,所以现在有6个接线头(我建议过颜色)用来连接.

原文中给出的表格,我以图片形式粘贴在这
原文中对树莓派引脚的说明图

这里有一个(可能不太有用的)正确连接我的树莓派的照片

作者连接树莓派原图

SPI库

        让我们通过看如何实现SPI来开始.

        我不打算写一篇关于SPI如何工作并且我们为什么需要它的长文,因为这在其他地方已经有详细文档了(https://learn.sparkfun.com/tutorials/serial-peripheral-interface-spi/).推荐它作为背景阅读,但是如果只是想让某些东西顺利运转起来那就并不重要.

        看到 lib/spi.c.它使用一些你会记得来自之前的教程的 lib/spi.c 里现存的函数.实际上,我们往 include/io.h 中添加了两个函数,这样我们就可以从我们的SPI库里调用它.

void gpio_setPinOutputBool(unsigned int pin_number, unsigned int onOrOff);

void gpio_initOutputPinWithPullNone(unsigned int pin_number);

        特别的, spi_init() 设置GPIO 7,9,10和11使用ALT0函数.使用 BCM2711 ARM外设文档(https://datasheets.raspberrypi.com/bcm2711/bcm2711-peripherals.pdf),第77页,你就会发现这个把SPI0映射到GPIO头文件.GPIO8被映射为一个输出引脚,因为我们打算使用这个向ENC28J60发出我们想要通信的信号.实际上, spi_chip_select() 函数接收一个 true/false (布尔值) 参数,它设置或者清除这个引脚.

        看到136页的 SPI0 的映射寄存器,我们发现这个反应了我们的 REGS_SPI0 结构体.它给予我们方便的访问 SPI0 外设的内存映射寄存器.

        我们的 spi_send_recv() 函数然后为了通信进行一些设置:

  • 设置 DLEN 寄存器中需要传输的字节数量(我们需要传进函数的长度)

  • 清楚 RX 和 TX 的FIFO缓冲

  • 设置传输活动(TA)标志

        然后当数据读写到时候(我们并没有读写比我们所要求的更多的数据),我们使用我们传进去的缓冲来读写FIFO.一旦我们认为完成了,我们等待直到SPI接口同意,即CS寄存器中的 DONE 标志被设置.如果有额外的字节需要阅读,我们就仅仅抛弃它们(好吧,现在把它们放到屏幕上,因为这不应该发生).

        最后,为了绝对的确定,我们清零 TA 标志.

        我已经设置好了两个方便的函数 -- spi_send() 和 spi_recv() -- 它运行 spi_send_recv(),主要是让将来的代更加易读.

ENC28J60的驱动

        我们现在看到 net/ 子文件夹.

        enc28j60.c 和 enc28j60.h 都组成了 ENC28J60以太网模块的驱动代码.当我们可能劳动数月来基于以太网模块的的手册(http://ww1.microchip.com/downloads/en/devicedoc/39662c.pdf)编写我们自己的驱动代码的时候,我选择利用别人的幸苦工作.这感觉赢了,我可以毫不费力的把其他人的优秀代码带到我的OS中!然而,我确实理解了每次这个代码都在做什么(可选的!).

        感谢这个GitHub代码库(https://github.com/wolfgangr/enc28j60)保存了我几个月的工作.我对这个代码做了写改动,但是并没有值得写进文档的.如果你十分愿意看看我做出了多小的改变,克隆这个代码库并且用好diff命令.

        我需要做的事是写一些连接驱动和树莓派4硬件的桥接代码.主要的,我说的是把我们的SPI库挂载到驱动上 -- encspi.c 的全部原因.

        它定义了4个驱动需要的函数(在 enc28j60.h 文件中有详细的文档):

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);

}

        可能最令人困惑的就是芯片选择了.通过一些尝试和错误,我发现当GPIO08被清除时,设备被选中,当它被设置时,设备被取消选中.它说明这是因为ENC28J60的芯片选择引脚拉低时活动,并且板子上名没有翻转器(可能为了省钱),所以GPIO8必须拉低来使能IC.

一些更多的定时器函数

        我们的 ENC28J60 驱动需要的唯一的另一间东西是访问一对定义良好的定时器函数:

  • HAL_GetTick() -- 返回从开始到现在的时钟滴答数

  • HAL_Delay() -- 延迟特定的毫秒数

        它们在 kernel/kernel.c 中快速实现了并且在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));

}

让我们开始连接

        所以我们有了能工作的驱动,它与我们的硬件通过 net/encspi.c 和 kernel/kernel.c 中一些函数进行交互.现在怎么办?

        我们的内核网络设计目标将会是:

  1. 证明我们将能与硬件沟通

  2. 成功带起网络

  3. 证明我们能通过网络连上某些东西并且得到回应

        我完成这个目标的计划是:

  1. 证明我们可以探测到这个网络在物理层面上是否建立连接,(CAT5线插入并且连接到一个工作的交换机上)

  2. 依赖ENC28J60的驱动来告诉我们成功连接

  3. 手工发送ARP请求并且等待从我的互联网路由器的ARP响应(设备从零知识的角度触发来在网络设备上"发现对方"的传统方法)

       看到 kernel/arp.c.首先我们创建了一个句柄来引用我们的驱动实例 ENC_HandleTypeDef handle.然后我们初始化 init_network() 中的结构体:

handle.Init.DuplexMode = ETH_MODE_HALFDUPLEX;

handle.Init.MACAddr = myMAC;

handle.Init.ChecksumMode = ETH_CHECKSUM_BY_HARDWARE;

handle.Init.InterruptEnableBits = EIE_LINKIE | EIE_PKTIE;

        这使得模块以半双工的模式启动(不能同时收发),设置MAC地址(我的最爱: C0:FF:EE:C0:FF:EE),告诉硬件去添加自己包的校验和(我们不想不得不在软件中创造它),并且使能"连接上/没连接上"和"包接收到"的中断信息.

        然后我们调用驱动实例 ENC_Start(&handle) 并且检查它返回 true(它完成了设计要求2 -- 驱动告诉我们已经正确启动).我们继续使用 ENC_SetMacAddr(&handle) 设置 MAC 地址.

        这行等待直到物理网络连接已经被建立(完成设计要求1)

while (!(handle.LinkStatus & PHSTAT2_LSTAT)) ENC_IRQHandler(&handle);

        当中断被触发后刷新驱动状态的标志时,驱动的 ENC_IRQHandler(&handle) 会被调用.因为我们并没有连接中断线,并且为了保持事情简单,我们只是在软件中不断的轮询.当我们看见 handle.LinkStatus 标志的 PHSTAT2_LSTAT 位被设置,我们知道连接上了(在模块的数据手册的第24页有写).

        在我们完成之前,我们必须重新使能以太网中断( ENC_IRQHandler() 禁止了它们,但没有重新使能它们 -- 我通过读代码的时候发现的).

收发ARP

        为了在以太网上传输,我们需要正确的格式化我们的包.ENC28J60 处理物理层(包括校验和,因为我么要求它了),所以我们只需要关注数据传输层 -- 由一个头文件和有效载荷组成.

        头文件(我们的 EtherNetII 结构体)只是一个目的和源的MAC地址,加上一个16-bit的包类型(https://en.wikipedia.org/wiki/EtherType).举个例子,ARP包,类型是0x0806.你会注意到我们的 #define ARPPACKET 包含了2字节.这是因为大端序是网络协议的主要序列,但是树莓派是一个小端序架构(这里可能需要一些阅读!)我们必须全面的展开这项工作.

        有效载荷是一个完全的定义在ARP结结构体中的ARP包(https://en.wikipedia.org/wiki/Address_Resolution_Protocol).SendArpPacket() 函数在结构体中设置了我们需要的数据(在注释中有文档)并且使用驱动调用来传输这些包.

// 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() 只是简单的准备传输缓冲区并且如果成功就返回0.ENC_WriteBuffer() 通过SPI把包送往 ENC28J60.然后我们在驱动状态标志里设置传输缓冲区长度并且调用 ENC_Transmit() 来告诉 ENC 来把包发往网络.

        你可以看到 arp_test() 函数使用这种方法发送了第一个ARP.我们告诉它我们路由器的IP(我的例子是 192.168.0.1),但我们不知道它的MAC地址 -- 这就是我们想找的东西.一旦ARP被发送, arp_test() 等待接收以太网的包,检查它们是否是为我们,并且如果它们是来自路由器的IP地址(因此有可能是对我们的请求的ARP应答),我们打印出路由器的MAC地址.

        这完成了设计要求3,因此我们完成了!我们所需要做的就是确保  kernel/kernel.c 以正确的顺序调用我们的网络例程.我选择使用一些非常容易跟随的在part13-interrupts的基础上改动来达成它.基本上这是所有的我们需要的调用:

spi_init();

init_network();

arp_test();

        想象一下,当我看到我的路由器(正确!)MAC地址出现在屏幕上——一个生命的标志,并证明我的操作系统实际上是联网的!

作者联网后的效果图


原文如下

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           |


![GPIO location](../part3-helloworld/images/3-helloworld-pinloc.png)


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


![ENC28J60 connections](images/14-spi-ethernet-photo.jpg)


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!_


![ARP response](images/14-spi-ethernet-arp.jpg)



16-对part14-spi-ethernet的翻译和搬运的评论 (共 条)

分享到微博请遵守国家法律