基本思路,需要了解的知识点。
硬件方面:
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总线的传输特点
在总线中传送的报文,每帧由7部分组成。CAN协议支持两种报文格式,其唯一的不同是标识符(ID)长度不同,标准格式为11位,扩展格式为29位。
在标准格式中,报文的起始位称为帧起始(SOF),然后是由11位标识符和远程发送请求位(RTR)组成的仲裁场。RTR位标明是数据帧还是请求帧,在请求帧中没有数据字节。
控制场包括标识符扩展位(IDE),指出是标准格式还是扩展格式。它还包括一个保留位(ro),为将来扩展使用。它的最后四个字节用来指明数据场中数据的长度(DLC)。数据场范围为0~8个字节,其后有一个检测数据错误的循环冗余检查(CRC)。
应答场(ACK)包括应答位和应答分隔符。发送站发送的这两位均为隐性电平(逻辑1),这时正确接收报文的接收站发送主控电平(逻辑0)覆盖它。用这种方法,发送站可以保证网络中至少有一个站能正确接收到报文。
报文的尾部由帧结束标出。在相邻的两条报文间有一很短的间隔位,如果这时没有站进行总线存取,总线将处于空闲状态。
远程帧
远程帧由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()函数会被“睡眠掉“,等待一旦有数据时会被唤醒。
这样基本上就完成了数据的读取,还有一些其他的细节要看源代码了。
前一篇文章主要讲述了SPI子系统的传输,没有具体分析具体代码的含义与片选的处理。要深刻体会SPI子系统的思想和精髓。
首先在SPI子系统中分成俩部分驱动。
第一部分SPI主控制端驱动,(Linux内核已经做好的驱动,需要自己配置和使用)从SPI协议我们可以得知,SPI支持主从模式,在arm cpu s3c2440中也留有作为从设备的片选脚,但内核2.6.30中的驱动只是把SPI作为控制端来写的。即该ARM芯片上的SPI作为主机(产生SPI时钟信号)来控制其他SPI接口的从机,这部分驱动功能是负责产生符合SPI格式的数据(暂时先这么理解)。
第二部分就是具有SPI接口的从机得驱动,(这部分需要自己来编写),这部分驱动通过前一个驱动在SPI总线上发送/接收SPI数据,来解释自己的命令。例如编辑一个SPI接口的CAN控制器的驱动,我们需要在SPI总线上发送CAN控制器可以解释的命令,这样CAN控制器可以明白我们要它做什么,并做出相应动作。这部分驱动的功能和你的SPI接口设备关系很大,接口设备不同驱动内容也不同。
下面以实例讲解在SPI子系统同一个控制端下分别挂载CAN控制器MCP2515和移位寄存器74SL595的情况。
Linux下的驱动模式是driver和device分别注册到“某个地方”,然后根据名称是否一致进行匹配。一致的话你这个设备就和这个驱动匹配,那驱动就可以使用这个设备的资源,这个设备的动作需要这个驱动来进行。其次对于平台设备(platform_device)信息会在系统上电时被初始化,对于有子系统包含的设备和其他设备有点不一样的初始化。
1:追踪SPI主控制端的初始化。首先SPI资源会在devs.c这个文件夹里面定义,定义包括名称,资源。(资源指寄存器地址还有些标志)
static u64 s3c_device_spi0_dmamask = 0xffffffffUL;
struct platform_device s3c_device_spi0 = {
.name = "s3c2410-spi",
.id = 0,//-1表示只有一个这样的设备。否则设备的真正名称是name+.+id
.num_resources = ARRAY_SIZE(s3c_spi0_resource),
.resource = s3c_spi0_resource,
.dev = {
.dma_mask = &s3c_device_spi0_dmamask,
.coherent_dma_mask = 0xffffffffUL
}
};
EXPORT_SYMBOL(s3c_device_spi0);//全局变量输出这样在系统中我们就知道有这个设备了。但是我们还需要再填充点信息进去,要把这个设备作为子系统处理,应该有总线的概念,所以还需要总线信息。在mach-smdk2440.c里面有这个定义
static struct s3c2410_spi_info s3c2410_spi0_platdata = {
// .pin_cs = S3C2410_GPF0, //含义待考虑
.num_cs = 2, //表示总线挂载设备的数量
.bus_num = 0, //总线的名称
.set_cs =&setcs, //设置片选函数
.gpio_setup=&gpiocs_setup,//GPIO设置函数配置SPI相关引脚。
};
s3c_device_spi0.dev.platform_data=&s3c2410_spi0_platdata;
这样设备信息就比较全面了,小结下,在s3c_device_spi0中就包含了设备的寄存器地址,设备名称,设备所产生的总线号,总线挂载的数目,及各种配置函数。
然后由函数platform_add_devices(smdk2440_devices, ARRAY_SIZE(smdk2440_devices));
统一把2440所有设备进行注册。
然后看下这个platform_add_devices注册函数主要干了什么事情。在linux/drivers/base /platform.c中105行定义了这个函数。函数调用platform_device_register()来进行注册。然后在platform_device_regisrer中调用device_initialize(pdev->dev)和platform_device_add(pdev)这俩个函数,从函数名称上我们推断一个是初始化设备信息中的dev结构体,另一个是把这个设备增加到什么地方去。
首先看初始化dev结构体。初看下初始化了kobj相关东西,初始化链表,同步锁,还有相关标志。
然后看platform_device_add里面内容。把其中一个pdev->dev.bus=& platform_bus_type (全局变量)至此我们基本可以确定了,这个设备属于platform_bus_type。所以这个设备的总线信息就知道了,但是总线还不知道这个设备,不过放心,在接下来的初始化过程中有一个函数bus_add_device,会让总线知道这个函数。这样至此我们就把一个设备注册完毕,初始化了一些我们能初始化的东西。结果之一是设备在总线上可以找到。
2:追踪SPI接口的设备的注册
在SPI系统中使用mach-smdk2440.c中的
static struct spi_board_info s3c2410_spi0_board[] = {
[0]= {
.modalias = "mcp2515",
.chip_select = 0,
//.controller_data = &S3C2410_GPF0
.irq = IRQ_EINT4,
.platform_data = &mcp2515_data,
.max_speed_hz = 3000 * 1000,
.bus_num = 0,
.mode = SPI_MODE_0,
},
[1]= {
.modalias = "ls595",
.chip_select = 1,
//.controller_data = &S3C2410_GPF0
//.irq = IRQ_EINT4,
//.platform_data = &mcp2515_data,
.max_speed_hz = 3000 * 1000,
.bus_num = 0,
.mode = SPI_MODE_0,
},
};
上面结构体来填充SPI接口的设备信息,然后通过函数
spi_register_board_info(s3c2410_spi0_board,ARRAY_SIZE(s3c2410_spi0_board));注册。下面来跟踪下这个函数干了些什么事情。
在这个函数里面,是把s3c2410_spi0_board的信息都拷贝到结构体
struct boardinfo {
struct list_head list;
unsigned n_board_info;
struct spi_board_info board_info[0];
};这里使用编程技巧定义个元素为0的数组,目的是接收s3c2410_spi0_borad里面的不确定元素,因为事先不知道元素的多少。然后在系统编译的时候会把board_info的内存默认为0,所以赋值的时候还要自动申请内存。
memcpy(bi->board_info, info, n * sizeof *info);
然后定义了同步锁,创建了链表。
mutex_lock(&board_lock);
list_add_tail(&bi->list, &board_list);
mutex_unlock(&board_lock);
这部分好像就到这个地方了,系统把信息保存到一块内存中,我们可以通过全局变量board_list找到这块地方。
3:追踪SPI驱动的注册
在spi_s3c24xx.c文件中
static int __init s3c24xx_spi_init(void)
{
return platform_driver_probe(&s3c24xx_spi_driver, s3c24xx_spi_probe);
}
开始驱动的注册,主要配置工作在s3c24xx_spi_probe函数里面。当然在执行s3c24xx_spi_probe函数,系统会在总线上进行匹配。
先看platform_driver_probe完成什么功能?
函数开头首先调用
platform_driver_register(drv);
在这个函数的第一句就是
drv->driver.bus = &platform_bus_type;
和device的一样,总线也是全局变量platform_bus_type.当然还有其他其他的工作,这里就不再跟踪了,基本可以吻合以前的推断。Driver和device都要注册到同一个总线上,总线再根据名称一样来进行匹配。
4:那spi接口的设备如何匹配的呢,我们来看下probe函数。函数原型为
static int __init s3c24xx_spi_probe(struct platform_device *pdev)
如果匹配成功,那么这个pdev就是前面说到的SPI设备s3c_device_spi0。
上面有SPI总线信息。
在代码
s3c24xx_spi_initialsetup(hw);
/* register our spi controller */
err = spi_bitbang_start(&hw->bitbang);
if (err) {
dev_err(&pdev->dev, "Failed to register SPI master\n");
goto err_register;
}
这个之前主要是填充了
struct s3c2410_spi_info *pdata;
struct s3c24xx_spi *hw;
struct spi_master *master;
三个结构体
Pdata就是SPI总线信息结构体,hw就是保罗SPI所有情况的一个结构体,mastr则偏重于SPI传输的结构体。
/* register our spi controller */
err = spi_bitbang_start(&hw->bitbang);
这个就开始进行spi controller的注册了,即所谓的SPI接口函数的注册。
跟踪spi_bitbang_start干什么事情。
除了一些初始化工作外,进而进行调用下面的函数。
/* driver may get busy before register() returns, especially
* if someone registered boardinfo for devices
*/
status = spi_register_master(bitbang->master);
在函数spi_register_master()里面
有一函数调用scan_boardinfo(master);用来扫描设备。
在scan_boardinfo(master)里面
static void scan_boardinfo(struct spi_master *master)
{
struct boardinfo *bi;
mutex_lock(&board_lock);
list_for_each_entry(bi, &board_list, list) {
struct spi_board_info *chip = bi->board_info;
unsigned n;
for (n = bi->n_board_info; n > 0; n--, chip++) {
if (chip->bus_num != master->bus_num)
continue;
/* NOTE: this relies on spi_new_device to
* issue diagnostics when given bogus inputs
*/
(void) spi_new_device(master, chip);
}
}
mutex_unlock(&board_lock);
}
会通过全局变量board_list遍历链表,取得设备信息,然后调用
spi_new_device(master, chip);
然后看这个函数里面做了什么东西。
主要是初始化及填充struct spi_device *proxy结构体,这个结构体就是将来SPI接口的设备驱动要找的设备。
首先是下面函数的调用
struct spi_device *spi_alloc_device(struct spi_master *master)
{
struct spi_device *spi;
struct device *dev = master->dev.parent;
if (!spi_master_get(master))
return NULL;
spi = kzalloc(sizeof *spi, GFP_KERNEL);
if (!spi) {
dev_err(dev, "cannot alloc spi_device\n");
spi_master_put(master);
return NULL;
}
spi->master = master;
spi->dev.parent = dev;
spi->dev.bus = &spi_bus_type;
spi->dev.release = spidev_release;
device_initialize(&spi->dev);
return spi;
}
从红色代码那我们很清楚看到,这样设备就挂在全局变量spi_bus_type总线上,原理和platform_bus_type类似。
然后调用函数status = spi_add_device(proxy);
if (status < 0) {
spi_dev_put(proxy);
return NULL;
}
Spi_add_device()函数完成了什么事情呢?
if(bus_find_device_by_name(&spi_bus_type,NULL,dev_name(&spi->dev))!= NULL) {
dev_err(dev, "chipselect %d already in use\n",
spi->chip_select);
status = -EBUSY;
goto done;
}
首先检查&spi_bus_type,总线是否已经存在这个设备等代码的其他部分暂不分析了。
小结:目的我们已经看到,最终SPI接口的设备注册到了spi_bus_type上了,如果把SPI接口设备的驱动也注册到这个总线上,然后根据名称进行匹配则device和driver就配对成功。
5:以MCP2515的驱动为例,来追踪该驱动的注册。
在驱动文件代码中有
static struct spi_driver mcp251x_driver = {
.driver = {
.name = DRIVER_NAME,
.bus = &spi_bus_type,
.owner = THIS_MODULE,
},
.probe = mcp251x_probe,
.remove = __devexit_p(mcp251x_remove),
#ifdef CONFIG_PM
.suspend = mcp251x_suspend,
.resume = mcp251x_resume,
#endif
};
static int __init mcp251x_init(void)
{
return spi_register_driver(&mcp251x_driver);
}
Bus类型就是spi_bus_type。后面如何注册及查找就不分析了。
总结:对linux内核设备和驱动的匹配了解加深!
上次报告主要讲解了SPI子系统数据的传输,最重要的函数是
static void bitbang_work(struct work_struct *work)
{
struct spi_bitbang *bitbang =
container_of(work, struct spi_bitbang, work);
unsigned long flags;
spin_lock_irqsave(&bitbang->lock, flags);
bitbang->busy = 1;
while (!list_empty(&bitbang->queue)) {
struct spi_message *m;
struct spi_device *spi;
unsigned nsecs;
struct spi_transfer *t = NULL;
unsigned tmp;
unsigned cs_change;
int status;
int (*setup_transfer)(struct spi_device *,
struct spi_transfer *);
m = container_of(bitbang->queue.next, struct spi_message,
queue);
list_del_init(&m->queue);
spin_unlock_irqrestore(&bitbang->lock, flags);
/* FIXME this is made-up ... the correct value is known to
* word-at-a-time bitbang code, and presumably chipselect()
* should enforce these requirements too?
*/
nsecs = 100;
spi = m->spi;
tmp = 0;
cs_change = 1;
status = 0;
setup_transfer = NULL;
list_for_each_entry (t, &m->transfers, transfer_list) {
/* override or restore speed and wordsize */
if (t->speed_hz || t->bits_per_word) {
setup_transfer = bitbang->setup_transfer;
if (!setup_transfer) {
status = -ENOPROTOOPT;
break;
}
}
if (setup_transfer) {
status = setup_transfer(spi, t);
if (status < 0)
break;
}
/* set up default clock polarity, and activate chip;
* this implicitly updates clock and spi modes as
* previously recorded for this device via setup().
* (and also deselects any other chip that might be
* selected ...)
*/
if (cs_change) {
bitbang->chipselect(spi, BITBANG_CS_ACTIVE);
ndelay(nsecs);
}
cs_change = t->cs_change;
if (!t->tx_buf && !t->rx_buf && t->len) {
status = -EINVAL;
break;
}
/* transfer data. the lower level code handles any
* new dma mappings it needs. our caller always gave
* us dma-safe buffers.
*/
if (t->len) {
/* REVISIT dma API still needs a designated
* DMA_ADDR_INVALID; ~0 might be better.
*/
if (!m->is_dma_mapped)
t->rx_dma = t->tx_dma = 0;
status = bitbang->txrx_bufs(spi, t);
}
if (status > 0)
m->actual_length += status;
if (status != t->len) {
/* always report some kind of error */
if (status >= 0)
status = -EREMOTEIO;
break;
}
status = 0;
/* protocol tweaks before next transfer */
if (t->delay_usecs)
udelay(t->delay_usecs);
if (!cs_change)
continue;
if (t->transfer_list.next == &m->transfers)
break;
/* sometimes a short mid-message deselect of the chip
* may be needed to terminate a mode or command
*/
ndelay(nsecs);
bitbang->chipselect(spi, BITBANG_CS_INACTIVE);
ndelay(nsecs);
}
m->status = status;
m->complete(m->context);
/* restore speed and wordsize */
if (setup_transfer)
setup_transfer(spi, NULL);
/* normally deactivate chipselect ... unless no error and
* cs_change has hinted that the next message will probably
* be for this chip too.
*/
if (!(status == 0 && cs_change)) {
ndelay(nsecs);
bitbang->chipselect(spi, BITBANG_CS_INACTIVE);
ndelay(nsecs);
}
spin_lock_irqsave(&bitbang->lock, flags);
}
bitbang->busy = 0;
spin_unlock_irqrestore(&bitbang->lock, flags);
}
蓝色部分是对SPI传输的初始化,每次传输都要根据设备的配置进行初始化。
红色部分是片选的操作。
bitbang->chipselect(spi, BITBANG_CS_ACTIVE);其实是个函数指针的调用,具体函数实现在spi_s3c24xx.c文件内德probe函数里面被指定。
hw->bitbang.chipselect = s3c24xx_spi_chipsel;
看下s3c24xx_spi_chipsel的定义
static void s3c24xx_spi_chipsel(struct spi_device *spi, int value)
{
struct s3c24xx_spi *hw = to_hw(spi);
unsigned int cspol = spi->mode & SPI_CS_HIGH ? 1 : 0;
unsigned int spcon;
switch (value) {
case BITBANG_CS_INACTIVE:
hw->set_cs(hw->pdata, spi->chip_select, cspol^1);
break;
case BITBANG_CS_ACTIVE:
spcon = readb(hw->regs + S3C2410_SPCON);
if (spi->mode & SPI_CPHA)
spcon |= S3C2410_SPCON_CPHA_FMTB;
else
spcon &= ~S3C2410_SPCON_CPHA_FMTB;
if (spi->mode & SPI_CPOL)
spcon |= S3C2410_SPCON_CPOL_HIGH;
else
spcon &= ~S3C2410_SPCON_CPOL_HIGH;
spcon |= S3C2410_SPCON_ENSCK;
//write new configration
writeb(spcon, hw->regs + S3C2410_SPCON);
hw->set_cs(hw->pdata, spi->chip_select, cspol);
break;
}
}
如果要激活片选其实调用的是hw->set_cs函数。
这个函数也是个函数指针的调用,其实函数实现也在probe内指定。
hw->pdata = pdata = pdev->dev.platform_data;//把在smdk2440.c定义的s3c2410_spi_info赋值给他们
Pdata就是前面说到的s3c_device_spi0的platform_data。
if (!pdata->set_cs) {
if (pdata->pin_cs < 0) {
dev_err(&pdev->dev, "No chipselect pin\n");
goto err_register;
}
err = gpio_request(pdata->pin_cs, dev_name(&pdev->dev));
if (err) {
dev_err(&pdev->dev, "Failed to get gpio for cs\n");
goto err_register;
}
hw->set_cs = s3c24xx_spi_gpiocs;
gpio_direction_output(pdata->pin_cs, 0);
} else
hw->set_cs = pdata->set_cs;//如果自己给出片选函数就使用自己
//否则使用s3c24xx_spi_gpiocs
然后看下这个片选函数传递进来的参数
void (*set_cs)(struct s3c2410_spi_info *spi, int cs, int pol);
这个函数是在函数static void s3c24xx_spi_chipsel(struct spi_device *spi, int value)
中调用的,所以可以使用spi_device类型spi的资源,spi的资源是通过我们定义在
static struct spi_board_info s3c2410_spi0_board[]{
[0]= {
.modalias = "mcp2515",
.chip_select = 0,
//.controller_data = &S3C2410_GPF0
.irq = IRQ_EINT4,
.platform_data = &mcp2515_data,
.max_speed_hz = 3000 * 1000,
.bus_num = 0,
.mode = SPI_MODE_0,
},
[1]= {
.modalias = "ls595",
.chip_select = 1,
//.controller_data = &S3C2410_GPF0
//.irq = IRQ_EINT4,
//.platform_data = &mcp2515_data,
.max_speed_hz = 3000 * 1000,
.bus_num = 0,
.mode = SPI_MODE_0,
},
}
然后通过一系列的处理得到的。但是看原始数据,没有片选脚的定义。反而在
static struct s3c2410_spi_info s3c2410_spi0_platdata = {
// .pin_cs = S3C2410_GPF0, //含义待考虑
.num_cs = 2, //表示总线挂载设备的数量
.bus_num = 0, //总线的名称
.set_cs =&setcs, //设置片选函数
.gpio_setup=&gpiocs_setup,//GPIO设置函数配置SPI相关引脚。
};
总线信息时有片选脚的定义,所以我在处理单设备挂载的时候,就使用这个引脚。但是多设备挂载显然不可以这么做了。首先查看源代码是否有其他地方进行这方面的设置以及验证。发现没有这方面的设置。
综合考虑,片选可以不要定义,而根据chip_select的数值不同,选用不同的引脚。所以函数void (*set_cs)(struct s3c2410_spi_info *spi, int cs, int pol);
的第二个参数应该传进来spi设备的chip_select。然后在这个函数里面根据这个参数进行选择不同的引脚,进行拉低和拉高处理。