基于MCP2515的Linux CAN总线驱动程序设计(三)
作者:李老师,华清远见嵌入式学院讲师。
1.前言
上篇文章介绍了使用SPI子系统设计的基于MCP2515的Linux CAN总线驱动程序,这篇文章主要介绍MCP2515的字符设备驱动功能函数的实现。
2.硬件设计
MCP2515与S3C2416的硬件连接图如图3所示。如硬件原理图可知MCP2515芯片连接在S3C2416芯片的SPI0上,中断接在GPF1上;MCP2515输出连接SN65HVD230 CAN总线收发器,SN65HVD230是德州仪器公司生产的3.3V CAN收发器。为了节省功耗,缩小电路体积,MCP2515 CAN总线控制器的逻辑电平采用LVTTL,SN65HVD230就是与其配套的收发器。
图1 MCP2515硬件连接图
3.MCP2515功能函数的实现
3.1.MCP2515设备结构体
首先需要定义MCP2515的设备结构体,包含MCP2515驱动所需要的相关属性。
struct mcp2515_chip
{
canid_t own_id; // CAN ID
canid_t broadcast_id; // Broadcase ID
CanBandRate bandrate; // 波特率
struct cdev cdev; // 字符设备结构体
struct spi_device *spi; // SPI 设备结构体
struct class *class; // Class类
struct work_struct irq_work; // 工作队列
uint32_t count; // CAN 设备计数
uint8_t *spi_transfer_buf; // SPI传输缓冲区
/* SPI输入输出缓冲区 */
struct can_frame spi_tx_buf[MCP2515_BUF_LEN];
struct can_frame spi_rx_buf[MCP2515_BUF_LEN];
uint32_t rxbin; // 接收报文计数
uint32_t rxbout; // 读报文计数
uint32_t txbin; // 发送报文计数
uint32_t txbout; // 发送报文计数
wait_queue_head_t rwq; // 读报文等待队列头
};
在整个驱动中我们都需要使用这个设备结构体,所以在驱动注册成功后在probe函数中要创建这个设备结构体,并且对这个结构体相应的初始化。
static int __devinit mcp2515_probe(struct spi_device *spi)
{
struct mcp2515_chip *chip;
int ret;
/* 为设备结构体申请空间 */
chip = kmalloc(sizeof(struct mcp2515_chip), GFP_KERNEL);
if (!chip)
{
ret = -ENOMEM;
goto error_alloc;
}
/* 初始化设备结构体 */
dev_set_drvdata(&spi->dev, chip);
……
/* 初始化工作队列 */
INIT_WORK(&chip->irq_work, mcp2515_irq_handler);
/* 申请中断 */
ret = request_irq(IRQ_EINT(1), mcp2515_irq,IRQF_DISABLED | IRQF_TRIGGER_FALLING, DEVICE_NAME, spi);
if (ret < 0)
{
printk("MCP2515: Request_irq() Error!\n");
goto error_irq;
}
/* 初始化等待队列头 */
init_waitqueue_head(&chip->rwq);
/* 注册设备 */
……
printk ("MCP2515: MCP2515 Can Device Driver.\n");
}
使用dev_set_drvdata函数把spi与设备结构体chip关联起来,在只有spi参数传入的情况下,可以使用dev_get_drvdata获取设备结构体chip。
3.2 MCP2515中断函数
根据芯片手册可知,MCP2515有8个中断源,当中断发生时,INT引脚将被MCP2515拉低为低电平,并且保持低电平状态直至MCU清除中断。
由原理图可知MCP2515芯片外部中断接到S3C2416的外部中断1口上,所以在probe时使用request_irq申请外部中断1的中断。我们可以通过判断中断标志位的方式来接收数据。然而我们使用request_irq函数注册的中断实际是中断上半部,在Linux中把中断分为两个部分,在上半部不能有中断发生,尽可能使上半部处理少的工作。在MCP2515中断函数中,我们要判断中断标志,和接收数据,所以需要下半部机制。下面引自《Linux设备驱动开发详解》第10章第2节,详细介绍了中断的半部机制。
设备中断会打断内核中进程的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务尽可能的短小精悍。但是,在大多数真实的系统中,当中断到来时,要完成的工作往往并不会是短小的,它可能要进行较大量的耗时处理。
在Linux内核中,为了在中断执行时间尽可能短和中断处理需完成大量工作之间找到一个平衡点,Linux将中断处理程序分为两个部分:上半部(top half)和下半部(bottom half)。中断处理程序的上半部在接收到一个中断时就立即执行,但只做比较紧急的工作,这些工作都是在所有中断被禁止的情况下完成的,所以要快,否则其它的中断就得不到及时的处理。那些耗时又不紧急的工作被推迟到下半部去。中断处理程序的下半部分(如果有的话)几乎做了中断处理程序所有的事情。它们最大的不同是上半部分不可中断,而下半部分可中断。在理想的情况下,最好是中断处理程序上半部分将所有工作都交给下半部分执行,这样的话在中断处理程序上半部分中完成的工作就很少,也就能尽可能快地返回。但是,中断处理程序上半部分一定要完成一些工作,例如,通过操作硬件对中断的到达进行确认,还有一些从硬件拷贝数据等对时间比较敏感的工作。剩下的其他工作都可由下半部分执行。
对于上半部分和下半部分之间的划分没有严格的规则,靠驱动程序开发人员自己的编程习惯来划分,不过还是有一些习惯供参考:
* 如果该任务对时间比较敏感,将其放在上半部中执行。
* 如果该任务和硬件相关,一般放在上半部中执行。
* 如果该任务要保证不被其他中断打断,放在上半部中执行(因为这是系统关中断)。
* 其他不太紧急的任务, 一般考虑在下半部执行。
下半部分并不需要指明一个确切时间,只要把这些任务推迟一点,让它们在系统不太忙并且中断恢复后执行就可以了。通常下半部分在中断处理程序一返回就会马上运行。内核中实现下半部的手段不断演化,目前已经从最原始的BH(bottom half)衍生出BH(在2.5中去除)、软中断(softirq在2.3引入)、tasklet(在2.3引入)、工作队列(work queue在2.5引入)。稍后笔者将介绍后两种方式。
尽管上半部和下半部的结合能够改善系统的响应能力,但是,Linux设备驱动中的中断处理并不一定要分成两个半部。如果中断要处理的工作本身就很少,则完全可以直接在上半部全部完成。
所以我们只需要在中断函数中把当前任务加入到工作队列中,使用下半部机制来完成中断判断和数据的读取。
static irqreturn_t mcp2515_irq(int irq, void *dev_id)
{
struct spi_device *spi = dev_id;
struct mcp2515_chip *chip = dev_get_drvdata(&spi->dev);
schedule_work(&chip->irq_work);
return IRQ_HANDLED;
}
3.3 MCP2515中断下半部
要使用下半部机制,首先要建立一个工作队列,这个工作队列在设备结构体中已经创建,然后就可以在probe函数中初始化这个工作队列。
1 INIT_WORK(&chip->irq_work, mcp2515_irq_handler);
这样,我们就可以在下半部中来判断中断类型,做出相应的处理。
01 static void mcp2515_irq_handler(struct work_struct *work)
{
struct mcp2515_chip *chip = container_of(work, struct mcp2515_chip, irq_work);
struct spi_device *spi = chip->spi;
int intf;
while (1)
{
intf = mcp2515_read_reg(spi, CANINTF);
if (!intf)
break;
//if (intf & CANINTF_WAKIF)
if (intf & CANINTF_MERRF)
mcp2515_hw_reset(spi);
if (intf & CANINTF_ERRIF)
mcp2515_write_reg(spi, EFLG, 0x00);
if (intf & CANINTF_TX2IF)
mcp2515_tx(spi, 2);
if (intf & CANINTF_TX1IF)
mcp2515_tx(spi, 1);
if (intf & CANINTF_TX0IF)
mcp2515_tx(spi, 0);
if (intf & CANINTF_RX1IF)
mcp2515_rx(spi, 1);
if (intf & CANINTF_RX0IF)
mcp2515_rx(spi, 0);
mcp2515_write_bits(spi, CANINTF, intf, 0x00);
if (chip->rxbin != chip->rxbout)
wake_up_interruptible(&chip->rwq);
}
}
在下半部中,需要根据不同的中断类型来做出不同的处理,这里对除了唤醒中断之外的所有中断进行了相应的处理。后面会结合发送和接收函数详细介绍上面的代码。
3.4 MCP2515设置
根据芯片手册,MCP2515支持CAN2.0B技术规范所定义的标准数据帧、扩展数据帧和远程帧,详细帧格式如图2、图3和图4所示。
图2 CAN标准数据帧
图3 CAN扩展数据帧
图4 CAN远程帧
三种不同的帧格式分别有不同的工作场合,但扩展帧因为有更多的地址位,所以应用场合比较广泛,下面详细介绍了如何设置MCP2515 ID的程序。
static long mcp2515_unlocked_ioctl (struct file *filp,unsigned int cmd, unsigned long arg)
{
struct mcp2515_chip *chip = filp->private_data;
struct spi_device *spi = chip->spi;
int ret = 0;
switch(cmd)
{
case CAN_Set_Own_ID:
mcp2515_set_id(spi, (canid_t)arg, Own_ID);
break;
case CAN_Set_Broadcast_ID:
mcp2515_set_id(spi, (canid_t)arg, Broadcast_ID);
break;
case CAN_Set_Bandrate:
ret = mcp2515_set_bandrate(spi, arg);
break;
......
default:
break;
}
return ret;
}
MCP2515可以设置滤波器,以便有选择的接受总线上的数据,来减少MCU的工作工作负荷。根据芯片手册可以查到MCP2515相关的滤波器,本驱动在设置本地ID后会自动装载到屏蔽滤波器。
static void mcp2515_set_id(struct spi_device *spi, canid_t id, int8_t flag)
{
struct mcp2515_chip *chip = dev_get_drvdata(&spi->dev);
if (flag == Own_ID)
{
mcp2515_write_can_id(spi, (uint32_t)&chip->own_id, id, 1, 1);
mcp2515_write_can_id(spi, RXFSIDH(0), id, 1, 0);
mcp2515_write_can_id(spi, RXFSIDH(3), id, 1, 0);
mcp2515_write_can_id(spi, RXFSIDH(4), id, 1, 0);
mcp2515_write_can_id(spi, RXFSIDH(5), id, 1, 0);
}
if (flag == Broadcast_ID)
{
mcp2515_write_can_id(spi, (uint32_t)&chip->broadcast_id, id, 1, 1);
mcp2515_write_can_id(spi, RXFSIDH(1), id, 1, 0);
mcp2515_write_can_id(spi, RXFSIDH(2), id, 1, 0);
}
}
3.5 MCP2515发送报文
MCP2515报文发送有相应的缓冲机制,它共有3个报文发送缓冲器,驱动接收到报文后首先要装载到发送缓冲器中,当检测到报文装载完毕后,驱动会执行报文发送指令,这时报文才会发送到总线上。下图是SPI装载缓冲器的时序。
图5 MCP2515装载缓存器SPI时序图
所以我们首先要检测发送缓冲器的状态,当检测到发送缓冲器为空(没有准备发送的数据)时,才会将报文装载到相应的发送缓冲器中,否则会检测其他的缓冲器,如果所有缓冲器都为忙,则返回发送报文失败。
static ssize_t mcp2515_write (struct file *filp, const char __user *buf,size_t count, loff_t *lof)
{
struct mcp2515_chip *chip = filp->private_data;
struct spi_device *spi = chip->spi;
struct can_frame *frame;
int txreq;
int nbytes = 0;
int ret;
if (count != sizeof(struct can_frame))
return -EINVAL;
frame = &chip->spi_tx_buf[chip->txbin];
ret = copy_from_user(frame, buf, sizeof(struct can_frame));
if (0 != ret)
return -EFAULT;
chip->txbin++;
if (chip->txbin >= MCP2515_BUF_LEN)
chip->txbin = 0;
nbytes = frame->can_dlc & 0x0F;
txreq = mcp2515_read_state(spi, INSTRUCTION_CAN_STATE);
if (!(txreq & CAN_STATE_TX0REQ))
mcp2515_tx(spi, 0);
if (!(txreq & CAN_STATE_TX1REQ))
mcp2515_tx(spi, 1);
if (!(txreq & CAN_STATE_TX2REQ))
mcp2515_tx(spi, 2);
return nbytes;
}
报文缓冲器准备完毕,就可以执行请求发送的SPI时序,相应的SPI时序如下图所示。
图6 MCP2515请求发送SPI时序图
3.6 MCP2515接收报文
此驱动的报文接收使用的是阻塞的方式,报文接收实际上是在中断中完成的。当总线收到报文数据后,会把报文保存到接收缓冲器中。当我们在程序中调用read函数时,才会把报文从缓冲器中读出来,传到用户空间,如果缓冲器没有报文数据时,函数会阻塞,直到有缓冲器中有报文或者用户结束程序。
当中断中接收到报文数据,装载到接受缓冲器后,会唤醒等待队列头,如果此时等待队列正在睡眠,则睡眠会被唤醒,来接收数据。
static ssize_t mcp2515_read(struct file *filp, char __user *buf, size_t count, loff_t * lof)
{
struct mcp2515_chip *chip = filp->private_data;
int nbytes = 0;
int ret;
struct can_frame *frame;
if(count != sizeof(struct can_frame))
return -EINVAL;
while (chip->rxbin == chip->rxbout)
{
if (filp->f_flags & O_NONBLOCK)
return -EAGAIN;
if (wait_event_interruptible(chip->rwq, (chip->rxbin != chip->rxbout)))
return -ERESTARTSYS;
}
frame = &chip->spi_rx_buf[chip->rxbout];
ret = copy_to_user(buf, frame, sizeof(struct can_frame));
if (0 != ret)
return -EFAULT;
chip->rxbout++;
if (chip->rxbout >= MCP2515_BUF_LEN)
chip->rxbout = 0;
nbytes = frame->can_dlc & 0x0F;
return nbytes;
}
read函数被唤醒后,要把报文从接收缓冲器中读出来,MCP2515提供了读缓冲器的SPI时序,如下图所示。
图7 MCP2515读缓冲区SPI时序图
4. 测试
4.1 硬件连接
环境使用FS2416开发板,根据原理图可知“H”和“L”为CAN总线引脚,连接两个开发板的CAN总线。
图8 CAN总线测试连接图
4.2 插入模块驱动
板子启动后,加载编译好的MCP2515驱动程序。
$ insmod mcp2515.ko
图9 模块插入成功
4.3 测试程序
① 接收端运行测试程序;
$ ./CANtest -r
图10 选择波特率
② 选择通讯的波特率,然后等待发送端数据;
图11 接收端等待数据
③ 发送端加载驱动,并且运行测试程序;
$ insmod mcp2515.ko
$ ./CANtest –s
图12 发送端选择波特率
④ 选择波特率(相互通讯收发端应该使用相同的波特率);
图13 准备发送数据
⑤ 发送端输入数据;
图14 发送端发送数据
⑥ 接收端接收数据;
图15 接收端接收到数据
5. 总结
至此,使用SPI子系统注册MCP2515 CAN总线驱动详细设计就介绍完了。因为使用了SPI子系统的方式,由SPI子系统屏蔽了不同平台的差异,在拿到一个新平台后,我们只需要让其SPI正常工作,我们就可以把之前挂在SPI上的设备上设备的驱动拿到新平台上,相应的设备驱动在新平台上不需要改一行代码就可以正常工作了。这种做法充分体现了Linux分层的思想,这也是子系统设计的初衷。