【连载】【FPGA黑金开发板】NIOS II那些事儿SPI实验 (八)

声明:本文为原创作品,版权归本博文作者所有,如需转载,请注明出处http://www.cnblogs.com/kingst/

 fpga

简介

      这一节,我们来讲讲NIOS II中的SPI总线的用法。首先,我们来简单介绍一下SPI总线吧,SPI是英文Serial Peripheral Interface的缩写,中文意思是串行外围设备接口,是Motorola公司推出的一种同步串行通讯方式,是一种四线同步总线,因其硬件功能很强,与SPI有关的软件就相当简单,使CPU有更多的时间处理其他事务。

      SPI的通信原理很简单,它以主从方式工作,这种模式通常有一个主设备和一个或多个从设备,需要至少4根线,事实上3根也可以(用于单向传输时,也就是半双工方式)。也是所有基于SPI的设备共有的,它们是MISO(主入从出),MOSI(主出从入),SCK(时钟),CS(片选)。

(1)MISO – 主设备数据输出,从设备数据输入

(2)MOSI – 主设备数据输入,从设备数据输出

(3)SCK – 时钟信号,由主设备产生

(4)CS – 从设备使能信号,由主设备控制

      其中CS是控制芯片是否被选中的,也就是说只有片选信号为预先规定的使能信号时(高电位或低电位),对此芯片的操作才有效。这就允许在同一总线上连接多个SPI设备成为可能。

      SPI总线的理论知识就介绍这么多,想要看具体点的去网上百度一下吧。下面我们就开始SPI总线的开发旅程吧。

硬件开发

      在我们开发板中网口部分是用SPI总线实现的,网络芯片是MICROCHIP公司的ENC28J60,我们先看一下这部分的电路,如下图所示,其中与SPI总线相关的有,LAN_MISO,LAN_MOSI,LAN_SCK这三个根线,其余的都是通过PIO模块实现的。而且有些线还用不到,比如LAN_nWOL。

clip_image002

我们这一节主要是教大家如何来实现SPI总线的功能,对于ENC28J60的原理相对复杂,在这里我就不详细讲解了,大家有兴趣的可以自己研究一下。

下      面我们就来构建SPI模块,进入SOPC BUILDER后,我们如下图所示,点击红圈处(SPI)

clip_image004

点击后,如下图所示,在这里面,我们有5个地方需要注意,

红圈1处是主从模式选择,我们选择主模式(Master);

红圈2处是从设备的个数,我们选择1;

红圈3处是SPI时钟速率,我们选择10M,这个地方需要注意一下,我们设置的频率与实际的频率有时候是不一致的(下面显示的是实际频率),例如,我们输入50MHz,实际的频率只有25MHz。

红圈4处是数据的位数,我们选择8;

红圈5处是移位的方向,就是说串行数据过来时,是最高位先来还是最低位先来,我们选择MSB first。

clip_image006

处理好这些以后,点击Finish,完成构建。

      接下来,我们还要构建两个PIO模块,一个用作CS信号控制,一个用作中断信号。之所以没有用SPI总线本身的CS,是由程序处理本身决定的。中断信号的PIO模块,构建过程需要注意一下内容,首先作为中断信号,是输入信号,所以在选择过程中,如下图所示,红圈1处选择为1,红圈2处选择Input ports only,仅作为输入端口,点击Next,进行下一步

clip_image008

点击后,进入下一步,如下图所示,外部中断要求电平触发,所以按红圈处选择方式。

然后,我们点击Finish,完成构建。

clip_image010

完成上述内容以后,我们需要对模块进行改名,如下图所示,

clip_image012

一切就绪,别忘了自动地址分配和中断分配。哦了,我们开始编译吧,等待……

编译好以后,我们回到Quartus界面,根据TCL脚本文件进行管脚分配,如下图所示

clip_image014

接下来我们运行脚本文件,进行编译,又一次漫长的等待……

编译成功以后,我们开始进行软件部分的开发

软件开发

      打开NIOS II 9.0 IDE,然后进行编译,快捷键Ctrl+b,等待编译成功后,我们来看看system.h中多了些什么,如下表所示,

/*
 * LAN configuration
 *
 */
#define LAN_NAME "/dev/LAN"
#define LAN_TYPE "altera_avalon_spi"
#define LAN_BASE 0x00201020
……
/*
 * LAN_CS configuration
 *
 */
#define LAN_CS_NAME "/dev/LAN_CS"
#define LAN_CS_TYPE "altera_avalon_pio"
#define LAN_CS_BASE 0x00201060
……
/*
 * LAN_nINT configuration
 *
 */
#define LAN_NINT_NAME "/dev/LAN_nINT"
#define LAN_NINT_TYPE "altera_avalon_pio"
#define LAN_NINT_BASE 0x00201070
……

我们需要以下内容

#define LAN_BASE 0x00201020
#define LAN_CS_BASE 0x00201060
#define LAN_NINT_BASE 0x00201070

接下来,我们需要对sopc.h进行修改,在其中加入以下代码

typedef struct{
    volatile unsigned long int RXDATA;
    volatile unsigned long int TXDATA;
    union{
        struct{
            volatile unsigned long int NC           :3;
            volatile unsigned long int ROE          :1;
            volatile unsigned long int TOE          :1;
            volatile unsigned long int TMT          :1;
            volatile unsigned long int TRDY         :1;
            volatile unsigned long int RRDY         :1;
            volatile unsigned long int E             :1;
            volatile unsigned long int NC1          :23;        
        }BITS;
        volatile unsigned long int WORD;
    }STATUS;

    union{
        struct{
            volatile unsigned long int NC           :3;
            volatile unsigned long int IROE         :1;
            volatile unsigned long int ITOE         :1;
            volatile unsigned long int NC1          :1;
            volatile unsigned long int ITRDY        :1;
            volatile unsigned long int IRRDY        :1;
            volatile unsigned long int IE           :1;
            volatile unsigned long int NC2          :1;
            volatile unsigned long int SSO          :21;
        }BITS;
        volatile unsigned long int CONTROL;
    }CONTROL;

    unsigned long int RESERVED0;
    unsigned long int SLAVE_SELECT;
}SPI_STR;

这部分代码是根据《n2cpu_Embedded Peripherals.pdf》的第7-10页,如下表所示,结构体的顺序是根据下表的排列顺序进行设计的,与串口中结构体的道理相同。

clip_image016

除了上述结构体以外,我们还要在sopc.h中加入以下代码

#ifdef _LAN
#define LAN          ((SPI_STR *) LAN_BASE)
#define LAN_CS       ((PIO_STR *) LAN_CS_BASE)       
#endif /*_LAN */

修改好sopc.h以后,我们需要在inc文件夹下建立一个enc28j60.h,在其中加入以下内容,(这只是enc28j60.h文件中的一部分,还有很大一部分宏定义没有写出)

/*-----------------------------------------------------------
 *  Data Struct
 *----------------------------------------------------------*/
typedef const struct{
    unsigned char (* read_control_register)(unsigned char address);
    void (* initialize)(void);
    void (* packet_send)(unsigned short len,unsigned char * packet);
    unsigned int (* packet_receive)(unsigned short maxlen,unsigned char * packet);
}ENC28J60;

/*----------------------------------------------------------
 *  external variable
 *----------------------------------------------------------*/
extern ENC28J60 enc28j60;

大家可以看出,在我们的程序中,这样的结构体随处可见,在之前的串口程序,还是这个SPI程序,我们都在用。它的好处就在于,可以将零散的函数和变量整合在一起,通过结构体的形式来处理,大大提高了程序的可读性,也增强了程序的可维护性和可移植性。

处理好上述内容后,我们开始编写enc28j60的驱动程序,内容很多,我们截取其中一部分有关SPI的内容来进行讲解

/*
 * ==============================================================
*       Filename:  enc28j60.c
*    Description:  enc28j60 device driver
*        Version:  1.0.0
 *        Created:  2009-8-7 13:05:54
 *       Revision:  none
 *       Compiler:  Nios II IDE
*         Author:  AVIC
 *        Company:  金沙滩工作室
 *
 * ==============================================================
 */

/*---------------------------------------------------------------
 *  Include
 *---------------------------------------------------------------*/
#include "../inc/enc28j60.h"
#include "../inc/sopc.h"
#include <stdio.h>

/*--------------------------------------------------------------
 *  Function Prototype
 *-------------------------------------------------------------*/
static unsigned char enc28j60_read_control_register(
unsigned char address);
static void enc28j60_initialize(void);
static void enc28j60_packet_send(unsigned short len,
unsigned char * packet);
static unsigned int enc28j60_packet_receive(
unsigned short maxlen,unsigned char * packet);

/*------------------------------------------------------------
 *  Variable
 *------------------------------------------------------------*/
//结构体初始化,注意初始化的写法
ENC28J60 enc28j60={
    .read_control_register = enc28j60_read_control_register,               
    .initialize        = enc28j60_initialize,                  
    .packet_send       = enc28j60_packet_send,                 
    .packet_receive    = enc28j60_packet_receive                   
};

static unsigned char enc28j60_bank = 1;
static unsigned short next_packet_pointer;
/* 
 * ===  FUNCTION  ==============================================================
 *         Name:  set_cs
 *  Description:  
 * ============================================================
 */
static void set_cs(unsigned char level)          
{
    if(level)    
        LAN_CS->DATA = 1;     
    else        
        LAN_CS->DATA = 0;      
}

/* 
 * ===  FUNCTION  ===============================================
 *         Name:  enc28j60_write_operation
 *  Description:  ENC28J60的写操作
 * =============================================================
 */


static void enc28j60_write_operation(unsigned char op,
 unsigned char address, unsigned char data)
{       
    //首先将CS置低,CS低电平有效
set_cs(0);
              
    //首先是写命令,等待状态状态寄存器的TMT位,当该位为0,说明正在发送数据,当该位//为1时,说明发送完毕,此时寄存器为空
    LAN->TXDATA = (op | (address & 0x1F)); 
while(!(LAN->STATUS.BITS.TMT));

//写数据,等待状态状态寄存器的TMT位,当该位为0,说明正在发送数据,当该位
//为1时,说明发送完毕,此时寄存器为空
    LAN->TXDATA = data;          
    while(!(LAN->STATUS.BITS.TMT));
    //发送完毕以后,将CS置高
    set_cs(1);
}

/* 
 * ===  FUNCTION  ================================================
 *         Name:  enc28j60_read_operation
 *  Description:  ENC28J60的读操作
 * ==============================================================
 */
static unsigned char enc28j60_read_operation(unsigned char op,
unsigned char address)
{ 
unsigned char data;

//首先将CS置低,CS低电平有效
set_cs(0);

    //首先是写命令,等待状态状态寄存器的TMT位,当该位为0,说明正在发送数据,当该位//为1时,说明发送完毕,此时寄存器为空
    LAN->TXDATA = op|(address&0x1f);
    while(!(LAN->STATUS.BITS.TMT));
    
    //写数据,发送0x00,0x00是个随机数,为了使能时钟,发送的数据与硬件有关系
    LAN->TXDATA = 0x00;  //0x00 is random number ,to enable clock 
    while(!(LAN->STATUS.BITS.TMT));
    
    //MAC和MII寄存器读的第一个字节是无效的,所以他们需要写两次
    if(address&0x80){
        LAN->TXDATA = 0x00;
        while(!(LAN->STATUS.BITS.TMT));   
    }
    //开始读数据
    data = LAN->RXDATA;
//读完以后,将CS置高
    set_cs(1);

    return data;
}

对于网口这部分程序,需要结合TCP/IP协议才能进行通信,所以,这部分主函数就暂时不写了,等讲到TCP/IP协议那部分的时候,我们再进行讲解。

      我们这一节主要是讲解SPI总线的使用方法,对于数据的收发都有所涉及,希望大家能够充分的理解其中的编程方法。有关ENC28J60的完整驱动,我将以附件形式提供给大家,这一节到此结束了,如果大家对这部分内容有疑问,可以加入我们的NIOS技术群,或者通过邮件形式与我沟通,谢谢大家。

原文地址:https://www.cnblogs.com/kingst/p/1705550.html