基于Linux内核SPI子系统工作机制

来源:互联网 发布:拉丁美洲知乎 编辑:程序博客网 时间:2024/05/24 07:10
基本思路,需要了解的知识点。
硬件方面:
 1:什么是SPI
 2:三星的ARM9 s3c2440 的SPI寄存器的使用
 3:什么是CAN总线
 4:CAN总线的传输及特点
 5:MCP2515 CAN控制器的使用
软件方面:
 1:了解并会使用linux内核 2.6.30.9的SPI子系统的框架及如何实现
 2:掌握platform device(平台设备)的驱动写法
 3:掌握MCP2515在内核中驱动的写法。
 4:掌握一定的调试方法
 先从硬件说起:
1.      什么是SPI?
白话的理解就是高速同步串行口,是一种标准的四线同步双向串行总线,英文全称是
Serial Peripheral interface。SPI的通信原理很简单,它以主从方式工作,这种模式通常有一个主设备和一个或多个从设备,需要至少4根线,事实上3根也可以(用于单向传输时,也就是半双工方式)。也是所有基于SPI的设备共有的,它们是SDI(数据输入),SDO(数据输出),SCLK(时钟),CS(片选)。
  (1)MOSI – SPI 总线主机输出/ 从机输入(SPI Bus Master Output/Slave Input)
  (2)MISO – SPI 总线主机输入/ 从机输出(SPI Bus Master Input/Slave Output)
  (3)SCLK – 时钟信号,由主设备产生
  (4)CS – 从设备使能信号,由主设备控制
  其中CS是控制芯片是否被选中的,也就是说只有片选信号为预先规定的使能信号时(高电位或低电位),对此芯片的操作才有效。这就允许在同一总线上连接多个SPI设备成为可能。
  接下来就负责通讯的3根线了。通讯是通过数据交换完成的,这里先要知道SPI是串行通讯协议,也就是说数据是一位一位的传输的。这就是SCLK时钟线存在的原因,由SCK提供时钟脉冲,SDI,SDO则基于此脉冲完成数据传输。数据输出通过 SDO线,数据在时钟上升沿或下降沿时改变,在紧接着的下降沿或上升沿被读取。完成一位数据传输,输入也使用同样原理。这样,在至少8次时钟信号的改变(上沿和下沿为一次),就可以完成8位数据的传输。
要注意的是,SCLK信号线只由主设备控制,从设备不能控制信号线。同样,在一个基于SPI的设备中,至少有一个主控设备。这样传输的特点:这样的传输方式有一个优点,与普通的串行通讯不同,普通的串行通讯一次连续传送至少8位数据,而SPI允许数据一位一位的传送,甚至允许暂停,因为SCLK时钟线由主控设备控制,当没有时钟跳变时,从设备不采集或传送数据。也就是说,主设备通过对SCLK时钟线的控制可以完成对通讯的控制。SPI还是一个数据交换协议:因为SPI的数据输入和输出线独立,所以允许同时完成数据的输入和输出。不同的SPI设备的实现方式不尽相同,主要是数据改变和采集的时间不同,在时钟信号上沿或下沿采集有不同定义,具体请参考相关器件的文档。 在点对点的通信中,SPI接口不需要进行寻址操作,且为全双工通信,显得简单高效。在多个从设备的系统中,每个从设备需要独立的使能信号,硬件上比I2C系统要稍微复杂一些。
2.       三星公司的ARM9 s3c2440 其实有俩个SPI,下面介绍其中一个SPI0。
寄存器手册可以在这款CPU的说明书中看到,SPI寄存器并不多,只有6个寄存器。分别为 SPI控制寄存器SPCON,SPI状态寄存器SPSTA,SPI引脚控制寄存器SPPIN,
SPI波特率寄存器SPPRE,SPI发送数据寄存器SPTDAT,SPI接收数据寄存器SPRDAT。
首先说下SPI控制寄存器 SPCON。
寄存器地址 0x59000000 复位值0x00
未用
SPI MODE SELECT
SCK EN
Master/slave
CPOL
CPHA
TAGD
0
0
0
0
0
0
0
0
        
SPI MODE SELECT : [6-5]  决定SPTDAT的收发。
  00=查询模式  01=中断模式 10=DMA模式   11=reserverd
根据SPI的特性,在读取数据时发送也在进行,(作为主设备而言),然后TAGD控制了读取数据时发送的模式。中断模式是指在向SPTDAT寄存器写下数据并发送完毕后,会产生一个中断。
SCK ENABLE:[4]    0=无效  1=使能
决定SCK是否使能,仅对主机有效,决定发送的时候必须使能这位,否则即使往发送寄存器写入了数据,也无数据输出。
Master/Slave:[3]   0=从    1=主
在从模式时,应该留有时间给主机初始化!
CLOCK POLARITY:[2]  0 = 高态有效 1 = 低态有效
     决定了时钟的有效态
CLOCK PHASE:[1]      0 = 格式A  1 = 格式B
     和时钟态一起组成了4种传输模式,在控制某个特定SPI接口的从设备时,需要知道该从设备支持的模式,有的从设备不一定全部支持这4种模式。
TX auto Garbage data mode enable [0]:   0 =normal mode    1 =TAGD
  决定是否需要正在接收的数据,在正常模式下,如果仅想接收数据,可以改传输无效数据0XFF。如果是TAGD模式,在读取数据时,即读取SPRDAT寄存器时,会自动发送数据来产生时钟来进行读取。
下面的一幅图描述了SPI的四种传输模式的物理表现形式。
 
SPI状态寄存器 SPSTA地址0x59000004 复位值 0x01
保留 [7:3]
Data collision error flag[2]
Multi master error flag[1]
Transfer ready flag  [0]
0
0
0
1
 
 Data collision error:[2]  0 = 不检测 1= 冲突检测
               在传输过程中写SPTDAT或者读SPRDAT会置位该标志,读取SPSTA该标志后会自动清除 ..
 Multi master error flag :[1]  0 = 不检测  1 = 多主机错误     
          当SPI配置为主机时nss信号为低态有效,该标志置位。
Transfer ready flag :[0]   0 = 不准备   1 = 数据接收发送准备
     该位是指SPTDAT 或者SPRDAT 准备发送或者接收。 写数据到SPTDAT该位自动清除。
 
SPI 引脚控制寄存器 SPPIN 地址 0x59000008 复位值 0x00
保留[7:3]
Multi master error detect enable[2]
保留[1]
Master out keep
0
0
0
1
 Multi error detect enable :[2]  0 = 无效 1 = 多主机错误侦测使能
          当SPI为主设备时nss作为输入来侦测多主机错误
 Master out keep :[0]  0 = 释放 1= 驱动先前电平
         决定MOSI引脚驱动先前电平或者在发送完一个字节后释放
SPI波特率寄存器SPPRE 地址 0x5900000c 复位值 0x00
Prescaler value [7:0]
波特率=PCLK/2/(寄存器的值+1)一般不超过25MHZ
 
SPI 发送寄存器SPTDAT 地址 0x59000010 复位值 0x00
TX data register
包含发送的数据
 
SPI接收寄存器SPRDAT 地址  0x59000014 复位值 0xff
Rx data register
包含接收的数据
 
3.      什么是CAN总线
  CAN是控制器局域网络(Controller Area Network, CAN)的简称,是由研发和生产汽车电子产品著称的德国BOSCH公司开发了的,并最终成为国际标准(ISO118?8)。是国际上应用最广泛的现场总线之一。 在北美和西欧,CAN总线协议已经成为汽车计算机控制系统和嵌入式工业控制局域网的标准总线,并且拥有以CAN为底层协议专为大型货车和重工机械车辆设计的J1939协议。近年来,其所具有的高可靠性和良好的错误检测能力受到重视,被广泛应用于汽车计算机控制系统和环境温度恶劣、电磁辐射强和振动大的工业环境。
4.      CAN总线的传输特点
  
CAN的报文格式
  在总线中传送的报文,每帧由7部分组成。CAN协议支持两种报文格式,其唯一的不同是标识符(ID)长度不同,标准格式为11位,扩展格式为29位。
  在标准格式中,报文的起始位称为帧起始(SOF),然后是由11位标识符和远程发送请求位 (RTR)组成的仲裁场。RTR位标明是数据帧还是请求帧,在请求帧中没有数据字节。
  控制场包括标识符扩展位(IDE),指出是标准格式还是扩展格式。它还包括一个保留位 (ro),为将来扩展使用。它的最后四个字节用来指明数据场中数据的长度(DLC)。数据场范围为0~8个字节,其后有一个检测数据错误的循环冗余检查(CRC)。
  应答场(ACK)包括应答位和应答分隔符。发送站发送的这两位均为隐性电平(逻辑1),这时正确接收报文的接收站发送主控电平(逻辑0)覆盖它。用这种方法,发送站可以保证网络中至少有一个站能正确接收到报文。
  报文的尾部由帧结束标出。在相邻的两条报文间有一很短的间隔位,如果这时没有站进行总线存取,总线将处于空闲状态。
CAN数据帧的组成
  远程帧
  远程帧由6个场组成:帧起始、仲裁场、控制场、CRC场、应答场和帧结束。远程帧不存在数据场。
  远程帧的RTR位必须是隐位。
  DLC的数据值是独立的,它可以是0~8中的任何数值,为对应数据帧的数据长度。
  出错帧
  出错帧由两个不同场组成,第一个场由来自各站的错误标志叠加得到,第二个场是出错界定符
  错误标志具有两种形式:
  活动错误标志(Active error flag),由6个连续的显位组成
  认可错误标志(Passive error flag),由6个连续的隐位组成
  出错界定符包括8个隐位
  超载帧
  超载帧包括两个位场:超载标志和超载界定符
  发送超载帧的超载条件:
  要求延迟下一个数据帧或远程帧
  在间歇场检测到显位
  超载标志由6个显位组成
  超载界定符由8个隐位组成
 
以上只是简单的CAN的特点介绍,如果要应用还的参考其他资料。
5.      MCP2515 CAN控制器的使用
网上下载MCP2515的datasheet去查看相关资料。
 注意点:
MCP2515最高支持SPI速率为10MHZ,本身CAN速率最高支持1MHZ(传输距离缩短),只支持SPI倆种传输模式 00,11。上电必须进行初始化,可以选择硬件初始化和软件初始化。RESET引脚不能空接。有些寄存器支持位修改,有些不可以。有些寄存器只能在配置模式修改,其他模式修改无效。
 
 
 
 
 
6.      Linux 内核2.6.30.9 SPI子系统实现的部分解析
 
子系统的概念这样理解挂载在s3c2440上面的SPI接口的所有设备都可以通过它进行控制,大家共享SPI资源,采用分时复用的原理使用SPI资源。这样的好处是扩展了SPI可以连接设备的数量,方便管理,避免了一些重复劳动,共享同样的驱动等。为了完成这个目标SPI子系统把设备信息抽象成一个个结构体,把子系统模拟成总线的形式,然后把这样结构体注册到总线上,这样就可以知道总线上包括哪些设备。
 设备信息的结构体数据如下所示:include/linux/spi.h定义这个结构体,不受具体的CPU的影响。
struct spi_board_info {
       char        modalias[32]; //设备的名称用来和驱动进行匹配
       const void      *platform_data; //设备的具体信息,是个空指针,可以指向任何结构的结构体
       void        *controller_data;
       int           irq; //设备的中断号
       u32        max_speed_hz; //SPI的最大速率
        * chip_select reflects how this chip is wired to that master;
        * it's less than num_chipselect.
        */
       u16        bus_num;  //总线的编号,实际指对应的SPI寄存器
       u16        chip_select; //反映了这个芯片是不是被连接到SPI上
 
       /* mode becomes spi_device.mode, and is essential for chips
        * where the default of SPI_CS_HIGH = 0 is wrong.
        */
       u8          mode; //设备的一些模式,例如片选的高低,SPI连接方式
};
 arch\arm\mach-s3c2410\include\mach\spi.h中定义了这个结构体,受你所使用的CPU的影响,我使用的是ARM CPU 型号s3c2440。因为和s3c2410寄存器大部分一样,使用了共同的部分。
这个结构体描述了SPI寄存器的一些信息
struct s3c2410_spi_info {
       int                  pin_cs;  /* simple gpio cs */
       unsigned int           num_cs;       //所以的片选信号
       int                  bus_num;  //SPI多对应的总线编号
 
       void (*gpio_setup)(struct s3c2410_spi_info *spi, int enable);//引脚设置函数
       void (*set_cs)(struct s3c2410_spi_info *spi, int cs, int pol);//片选操作函数
};
SPI寄存器的其他信息在\arch\arm\plat-s3c24xx\devs.c中定义
定义了SPI0的寄存器地址和数据读取方式
 
 
static struct resource s3c_spi0_resource[] = {
       [0] = {
              .start = S3C24XX_PA_SPI,
              .end   = S3C24XX_PA_SPI + 0x1f,
              .flags = IORESOURCE_MEM,
       },
       [1] = {
              .start = IRQ_SPI0,
              .end   = IRQ_SPI0,
              .flags = IORESOURCE_IRQ,
       }
 
};
static u64 s3c_device_spi0_dmamask = 0xffffffffUL;
 
struct platform_device s3c_device_spi0 = {
       .name               = "s3c2410-spi",
       .id             = 0,
       .num_resources       = ARRAY_SIZE(s3c_spi0_resource),
       .resource   = s3c_spi0_resource,
        .dev              = {
                .dma_mask = &s3c_device_spi0_dmamask,
                .coherent_dma_mask = 0xffffffffUL
        }
};
然后通过
s3c_device_spi0.dev.platform_data= &s3c2410_spi0_platdata;
       spi_register_board_info(s3c2410_spi0_board, ARRAY_SIZE(s3c2410_spi0_board));
把信息整合到一起。(s3c2410_spi0_platdata是struct s3c2410_spi_info的对象
s3c2410_spi0_board是struct spi_board_info的对象)
从上面的信息我们就可以知道,SPI寄存器地址和片选信号和设备总数,这个SPI寄存器对应的总线编号,总线上面的设备由struct spi_board_info来描述,提供总线信息,这样一个总线上有几个总线编号(是实际SPI),每个总线编号上有几个设备,每个设备的SPI速率等信息就知道了。
然后需要进行实际运行和操作,需要注册设备device和驱动driver!linux下的设备和驱动注册是分开进行的,即设备是设备,驱动是驱动,然后进行匹配。
Linux下和SPI有关系的主要有下面几个文件 spi.c, spidev.c , spibitbang.c, spi_s3cxx.c , spi.h, spidev.h, spibitbang.h,等其他相关头文件。
l        Spi.c文件里面主要是封装了一些SPI函数,扫描总线信息,SPI驱动探测,注册,申请内核内存等,属于比较上层的函数。
l        Linux/Spi.h封装了一些SPI传输函数,如spi_write(),spi_read()等,需要提供SPI设备和传输buff和len即可。
l        在下面一层的是spibitbang.c也是传输函数和spi的一些设置函数,需要提供SPI设备和spi_message信息。通过调用驱动层得传输函数s3c24xx_spi_txrx(struct spi_device *spi, struct spi_transfer *t)来实现
l        驱动层 spi_s3cxx.c包含了最基础的传输函数和一些设置。
l        Spidev.c就是一个直接的SPI的驱动函数,封装了SPI的设置和传输函数,      可以直接在应用层使用。
然后描述下spi的子系统的传输过程,如果需要传输数据,那么在底层就需要把结构体struct spi_transfer填充好,这个是描述进行一次SPI传输/接收的全部信息。这个结构体其实是链表的形式进行传输的,结构体最后部分创建了一个链表。
struct spi_transfer {
       const void      *tx_buf; //传输Buff
       void        *rx_buf; //接收Buff
       unsigned len;  //字节长度
       dma_addr_t   tx_dma; //DMA传输的地址信息
       dma_addr_t   rx_dma;
       unsigned cs_change:1; //是否进行片选改变的标志
       u8          bits_per_word; //每字传输的位数,有8位和12位等区别。
       u16        delay_usecs; //每次启动传输/接收的延时时间
       u32        speed_hz; //传输的速度即波特率
       struct list_head transfer_list; //传输结构体链表头包含了前一个链表指针和后一个链表指针
};
还有消息结构体指针
struct spi_message {
       struct list_head      transfers; //spi_message包含的传输结构体的链表头
       struct spi_device   *spi;
       unsigned        is_dma_mapped:1; //表示是否是DMA传输方式
       void               (*complete)(void *context);
       void               *context;
       unsigned        actual_length;
       int                  status;
       struct list_head      queue; //工作队列指针包含前一个spi_message和后一个spi_message
       void               *state;
};
这俩个是传输时重要的结构体!
然后大概说下传输流程
  数据的发送:
 1:用户层发送使用write()函数。
2:然后调用内核驱动的write()函数。//由具体的设备写相应的发送函数
3:内核的驱动write函数再调用spi_write()(在linux/spi.h中定义的函数)
4: spi_write()调用spi_sync(spi, &m);(在spi_bitbang.c中定义)这个是一个阻塞的同步传输函数,可以睡眠。
5: spi_sync()调用spi_bitbang_transfer(struct spi_device *spi, struct spi_message *m)函数,这个是在spi_bitbang.c中定义,其实这个调用的原代码是
return spi->master->transfer(spi, message),调用了自己SPI结构体中的一个函数指针,在spi驱动中spi_s3c24xx.c 的probe()函数中被赋值,err = spi_bitbang_start(&hw->bitbang),这条语句把spi的master结构体注册成功。Master结构体的transfer()函数指针被赋予真正的函数值,所以这个时候使用这个调用语句调用的才是spi_bitbang.c中的spi_bitbang_transfer(struct spi_device *spi, struct spi_message *m)函数。
6:在transfer()函数的主要作用是把这个spi_message加到一个工作队列里面
queue_work(bitbang->workqueue, &bitbang->work);然后以后的事情就由系统调度来完成。这样就形成了一个管子状的,串行的先进先出的通信管道。然后就把一次传输制作成一次”work”,“work”的具体工作内容在spi_bitbang.c中定义,由函数void bitbang_work(struct work_struct *work) 完成。
由这个函数看出每一次的对struct spi_transfer 传输都要调用传输初始化函数
int s3c24xx_spi_setupxfer(struct spi_device *spi,struct spi_transfer *t),功能是设置传输的波特率和传输字节。如果这个结构体的对象设置了速度就选用这个对象的波特率,如果没有就选用spi_board_info()结构体得波特率。初始化结束后开启对应设备的片选,延时,间接调用驱动程序spi_s3c24xx.c中的s3c24xx_spi_txrx(struct spi_device *spi, struct spi_transfer *t)函数,原代码:
if (!m->is_dma_mapped){
                                 t->rx_dma = t->tx_dma = 0;
                          status = bitbang->txrx_bufs(spi, t);
}
这个类似master结构体得transfer()函数调用,在spi_s3c24xx.c的probe()函数中被赋值 ,源代码是
 hw->bitbang.txrx_bufs      = s3c24xx_spi_txrx;
这样一套调用流程就很清晰的摆在我们面前,我们也就知道用户层的数据怎么通过内核传输出去的。我们可以把他们进行形象的比喻,例如可以把要发送的设备比喻成一个人,发送的消息比喻成一封信。设备要发送消息”spi_message”,人要写信。Spi_messgae可能只有一个spi_transfer,也可能有很多spi_transfer,人写的信可以有一句话,也可以写很多话。在SPI子系统中要发送的消息会被丢给调度程序给你排序发送,调度可以也已经接收到其他的发送要求了,然后按照绝对的先后顺序进行发送,对于设备来说一次发送就算完成。而在人类社会,你的这些信会丟给邮递员,邮递员会收好多人的信,一般会按实际送信的时间来给你发送,人就可以不要管事情了。所以在SPI子系统要理解数据封装,统一分发的好处,到底有什么好处,你可以想象下如果一个人都需要一个邮递员的是个什么情况就知道了,其实linux系统的设计会参照人类社会的一些哲理进行设计,因为本身这个操作系统就是人类自己写的,所以一点也不奇怪。
 
数据的接收:这个要联系具体设备,因为SPI的读写都是一个函数。
  1:用户层使用read()函数
  2: 内核态的read()函数,根据具体设备情况来写read函数
  3:read()把数据复制到用户层,使用函数copy_to_user()。
  4:用户层得到数据。
这样看貌似read()函数比write()简单不少,其实不然,真正的读取函数是在内核态完成的。读取函数主要看spi驱动的写法。根据SPI特性,对于SPI主控制器的读操作不需要中断,只有你SPI主控制器想读的时候就可以读,不需要中断进行提示。所以在具体应用中一般还会有另外一个中断,例如与MCP2515 CAN控制器的通信中,需要MCP2515的中断来提示需要读取数据了。这样这部分内容需要在MCP2515的驱动中进行叙述。
7.      平台设备的驱动一般写法
首先先要理解什么是平台设备(platform),平台设备指的是SOC系统上带的设备,
SOC片上系统是指把一些电路包括CPU集成到一块芯片上,顾名思义 system on the chip。
然后说下linux下的驱动和设备是分开的,驱动需要注册,设备也需要注册。有些时候系统会把外部设备模拟成指定的类型,例如对于串口设备,网口设备,CAN控制寄存器,还有按键设备,都可以模拟成字符型设备,块设备,网络设备。这样把一些不同的设备按照一些规则就可以归为一类,例如串口,CAN控制器,按键设备都可以归为字符设备驱动,他们都是一个字符一个字符进行传输的。所以在写这些设备驱动时,需要组成设备就可以组成成符合自己属性的类型。平台设备驱动其实我个人理解是提供了一个操作平台,具体的流通还是需要这些字符,块,和网络设备驱动来驱动,在这些驱动里面,调用的函数了等内容由这个“平台设备驱动“提供,所以这是平台设备的意义,也是和一般驱动的不同点。说白了,要让一些东西工作只依靠一块驱动很难,考虑到通用性和可共享性,避免重复劳动,就需要那么一块驱动让需要的人都可以使用,如果没有这些需要的人那这个驱动其实也没干什么事情。这个就像一个工具,甲可以用,乙也可以用,他们完成的工作可以不一样,但如果他们不用这个工具的意义就会大打折扣。
  基于这个思想就出现了所谓的平台设备的驱动,在操作系统中我们可以直接读取这些设备的寄存器进而直接进行控制,然后通过这个设备再控制其他的设备(外部设备)。所以在操作系统中已经包含了这个设备的信息,例如在devs.c这个文件夹里面就罗列出来了s3c2400 ARM中的平台设备的信息,然后通过不同于一般设备的注册方法进行注册,具体什么流程以后再说(我也不太明白)。这个只是我个人的理解有不正确之处可以予以指正和批评。
8.      MCP2515在内核中驱动的写法
这部分内容我只说内核如何完成接收的,在发送方面可以参照SPI的子系统的发送思想。
既然MCP2515驱动可以读取了,就看下如何接收吧。上面也提到过,对于SPI主设备不会被动的读,想读的时候会读,至于这个“想”如何实现是根据MCP2515提供的中断来实现的。就是说你如果不发送数据这个SPI平时就在“睡大觉“,除非你把它“打醒”,它才知道你要它接收数据了。负责“打醒”这个工作的任务就是MCP2515的INT引脚,只有在MCP2515正确接收到数据后,会把这个引脚拉低,这样触发了一个中断函数,在中断函数里面你可以调用SPI的读函数了,这样就完成了对MCP2515的读取工作。大体框架就是这样,具体实现看下面介绍。
先看下这个结构体
 
现在的操作系统一般是多任务多进程的操作系统,在宏观上看一秒内在干很多事情,其实是由内部调度系统进行操作时间分片,给每个进程一个时间片,这个时间片非常短,当这个进程的时间片消耗完之后再调用另一个进程,至于这个时间片如何产生如何分配就涉及到调度的算法问题,不做细究,所以在操作系统中你在做一件事情的时候,有可能是穿插进行的,即做一下停一下,在有些重要的事情的时候你不允许别人打断,就可以使用锁锁住,这个就是锁得由来吧,当然系统中的锁也分很多种,在这也不细究。
 
 
 
struct mcp251x {
    struct cdev cdev;
    struct semaphore lock;   /
    struct semaphore rxblock;     /提供了一些锁
    struct semaphore txblock;     /
    uint8_t *spi_transfer_buf;     /* temp buffer for spi bus transfer. */
    struct can_frame rxb[MCP251X_BUF_LEN]; /* ring buffer for receive. */
    struct can_frame txb[MCP251X_BUF_LEN]; /* ring buffer for send. */
    int txbin;                /* pos of in for ring buffer of sned. */
    int txbout;                     /* pos of out for ring buffer of send. */
    int rxbin;               /* pos of in for ring buffer of receive. */
    int rxbout;                    /* pos of out for ring buffer of receive. */
    int bit_rate;            /* save bit rate of current set. */
    int count;               /* count of the device opened. */
    wait_queue_head_t wq;  /* queue for read process. */
    struct work_struct irq_work; /* bottom half of interrupt task. */
    struct spi_device *spi;   /* save the point of struce spi_device. */
    struct can_filter filter;    /* save the filter data of current set. */
};
还有在操作系统中大部分设备都被模拟成文件的形式,对文件的读写就是对设备的操作。前面的创建字符设备驱动最后是创建了一个字符设备型的文件。
在中断函数中我们可以知道是什么设备产生的中断,
 
static irqreturn_t mcp251x_irq(int irq, void *dev_id)
{
    struct spi_device *spi = dev_id;
    struct mcp251x *chip = dev_get_drvdata(&spi->dev);
 
    /* Can't do anything in interrupt context so fire of the interrupt
     * handling workqueue. */
    schedule_work(&chip->irq_work);
 
    return IRQ_HANDLED;
}
为什么可以知道,那就是在申请中断时,在MCP2515.c驱动文件的
Probe()函数中申请中断的代码
ret = request_irq(spi->irq,mcp251x_irq, IRQF_DISABLED, DRIVER_NAME, spi);
这个dev_id参数是在probe()中传输进去的,probe()传入就是SPI是设备的信息,匹配的是名词一样的驱动。意思是我这个驱动程序可以取得到和我驱动程序名词一样的设备的信息。在中断函数中我们不宜占用太多时间,所以只是调用了一个调度函数把该做的“工作”放到队列里,至于这个工作是什么,就看下这个工作的具体定义。
定义在probe()的函数里面
 
 
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,20))
    INIT_WORK(&chip->irq_work, mcp251x_irq_handler);
#else
    INIT_WORK(&chip->irq_work, mcp251x_irq_handler, spi);
这样就把这个工作和mcp251x_irq_handler联系上了。
那这个工作具体干了什么,就可以看下mcp251x_irq_handler()这个函数
这个函数的主要功能是先判断MCP2515的中断是什么中断,读取它的中断标志寄存器,根据相应中断再调用相应函数,如果是接收中断的话,根据接收寄存器的值
,MCP2515有俩个接收寄存器,调用相关接收函数。代码如下
Intf就是读取的MCP2515的中断状态寄存器的值
if (intf & CANINTF_RX0IF) /* If received data, copy data to ring buffer. */
        mcp251x_hw_rx(spi, 0);
     if (intf & CANINTF_RX1IF)
         mcp251x_hw_rx(spi, 1);
然后如果是接收寄存器0的中断就调用函数mcp251x_hw_rx(spi, 0);
看下mcp251x_hw_rx();这个函数干了什么,可以查看相关源代码,这里我只说下功能,主要是一些接收准备工作,例如环形缓冲区的操作,struct spi_transfer 传输结构体得填充,因为SPI发送和接收是同时进行的,所以接收也需要发送一些无用数据0xff(TAGD=0)的情况下,然后调用在linux/spi.h封装的函数spi_write_then_read()
读取出数据,然后复制到环形缓冲区里。这样在内核的某块地方我们就已经接收到了MCP2515的一些数据,所以在应用层read()的时候会读到数据,如果还没有接收到数据,这个read()函数会被“睡眠掉“,等待一旦有数据时会被唤醒。
这样基本上就完成了数据的读取,还有一些其他的细节要看源代码了。
原创粉丝点击