Linux驱动-SPI-mcp2515 复位函数


前言

这里只简单写一个复位函数,为什么要写复位函数是通过外设数据手册来的,所以要先搞懂数据手册,通过数据手册来分析驱动业务流程,然后一步一步进行外设的驱动的业务 无外乎读写外设寄存器进行控制。

一、参考文档

Linux驱动-初步认识SPI
Linux驱动-SPI外设代码框架编写
Linux驱动-SPI-框架中添加字符设备

Linux 驱动-I2C输入子系统相关知识点

二、读懂原理图和操作手册

原理图分析

Can 模块

mcp2515 模块有很多功能的,spi 转can,spi 转485. 如下 spi 如何转换的呢? 就是通过ic 芯片进行转换了一次而已。
在这里插入图片描述

我们直接看一下spi 转can 的原理图。
在这里插入图片描述

看U1 部分,左边不就是SPI 相关的内容吗? pin 脚标记的16 、15/14/13/12 这些就是SPI 的四根线,右边看 RXCAN 、TXCAN 输出的就是can 信号。

目标:就是你要看懂原理图,这个太基本了。 或者通过相关资料来读懂原理图。

这里有特别容易忽略的问题,就是IC 的原理图和实际的物理要对应上,千万不要搞错。
在这里插入图片描述

GPIO/CAN/485 Interfcae 原理图

如如下红丝标记的J1 , 原理图要和实际的对上,这样才能正确接线。
在这里插入图片描述

数据手册分析

收据手册本身要读懂,要搞明白机制,每一块的内容都特别重要,下面初步看一下目录有哪些内容,本身就是一个说明使用书:
在这里插入图片描述

器件概述

在这里插入图片描述

工作模式

在这里插入图片描述
所以驱动连接外设后,在驱动第一步就是复位 进入配置模式。

SPI接口

这里特别重要,数据手册里面都说了支持0,0 和 1,1 两种模式,所以在实际设备树配置的时候要配置好,不然无法通信。
在这里插入图片描述
在这里插入图片描述
同时要关注一下数据手册里面有误发送数据介绍说明,默认优先高位。

在这里插入图片描述

三、设备树配置

我们看看我们之前的设备树基本配置、必备配置: 参考 Linux驱动-SPI外设代码框架编写

&spi0 {
   status = "okay";
   pinctrl-0 = <&spi0m1_cs0 &spi0m1_pins>;
   pinctrl-1 = <&spi0m1_cs0 &spi0m1_pins_hs>;
   mcp2515:mcp2515@0 {
        compatible = "my-mcp2515";
        reg = <0>;
        spi-max-frequency = <10000000>;
        status = "okay";
    };
};

说一下 设备树配置外设时候,针对SPI 关注几个点:

  • compatible : 匹配外设驱动
  • reg : 片选信号
  • spi-max-frequency : 始终频率 也就是传输速度,去手册里面找。(10MHz)
  • spi-cpha; spi-cpol; : 极性和相位,外设默认是0,0 支持的就是0,0 或者 1,1 。 就按默认的来匹配,不要配置这两个属性。
  • spi-lsb-first; : 低位优先传输,默认是高位优先传输,所有不要配置这个属性。
  • spi-cs-high : 片选极性,高电平有效。 mcp2515 默认是低电平有效,所有不要写这个属性。

两点特别注意:

  • 你自己要学会看懂操作手册,如果有特别的说明 那么对应的就要在设备树上体现
  • 如果没有特别体现就用默认,同时不要写不必要的属性。

四、SPI 复位函数编写

1、复位函数编写

定义一个SPI设备指针

static struct spi_device *g_mcp2515_spi = NULL;

定义复位指令

#define MCP2515_CMD_RESET    0xC0  // 复位指令 1100 0000

SPI设备指针 赋值

mcp2515_probe回调函数中赋值

// MCP2515设备初始化函数
static int mcp2515_probe(struct spi_device *spi)
{
    int ret;
    printk("This is mcp2515 probe\n");
    // 保存spi设备指针
    g_mcp2515_spi = spi;
    // ==========【上电首先执行MCP2515软件复位】==========
    ret = mcp2515_hw_reset(spi);
    if (ret) {
        printk("mcp2515 hw reset fail!\n");
        return ret;
    }
    // =================================================
    // 后续可以在这里继续添加寄存器初始化代码
    return 0;
}

MCP2515 SPI软件复位函数

/* MCP2515 SPI软件复位函数 */
static int mcp2515_hw_reset(struct spi_device *spi)
{
    u8 cmd = MCP2515_CMD_RESET;
    int ret;
    if (!spi) {
        printk("mcp2515 spi dev is NULL!\n");
        return -EINVAL;
    }
    // 发送单字节复位命令
    ret = spi_write(spi, &cmd, sizeof(cmd));
    if (ret < 0) {
        printk("mcp2515 reset cmd send failed, ret:%d\n", ret);
        return ret;
    }
    printk("mcp2515 send reset command(0xC0) success\n");
    msleep(10);  // 等待MCP2515内部复位完成,参考数据手册要求
    return 0;
}

对比两种复位操作-写入函数

如上是一种写操作,写了复位,如下给出另外一种方案:

// MCP2515芯片复位函数
void mcp2515_reset(void){
    int ret;
    char write_buf[] = {0xc0}; // 复位指令0x11000000即0xc0
    ret = spi_write(spi_dev, write_buf, sizeof(write_buf)); // 发送复位命令
    if(ret < 0){
        printk("spi_write is error\n"); // 打印错误信息
    }
}

这两种实现方案的底层逻辑其实是一摸一样的,一个是标准写法一个是极简写法,推荐第一套写法。

其实 这里最核心特别注意的一点是如何声明的问题:

char vs u8
char write_buf[]={0xc0} 在 ARM 平台可能是有符号 char,规范驱动代码统一使用内核<linux/types.h>u8无符号字节类型,避免符号扩展带来的隐性问题。

2、带复位函数完整驱动编写

#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/delay.h>   // 新增:msleep 头文件

static dev_t dev_num;
static int major;
static int minor;
static struct cdev mcp2515_cdev;
struct class *mcp2515_class;
struct device *mcp2515_device;
static struct spi_device *g_spi_dev;  // 新增:全局保存spi设备指针

/* MCP2515 SPI复位函数 */
static int mcp2515_reset(void)
{
    int ret;
    uchar cmd = 0xC0;  // MCP2515 复位指令 11000000 = 0xC0

    if (!g_spi_dev) {
        printk("mcp2515_reset: spi dev pointer is NULL!\n");
        return -ENODEV;
    }

    ret = spi_write(g_spi_dev, &cmd, sizeof(cmd));
    if (ret < 0) {
        printk("mcp2515_reset spi_write failed, ret=%d\n", ret);
        return ret;
    }
    msleep(10);  // MCP2515复位后等待芯片稳定(手册建议延时)
    printk("mcp2515 send reset cmd success\n");
    return 0;
}

/*打开设备函数*/
static int mcp2515_open(struct inode *inode, struct file *file)
{
    printk("This is mcp2515_open \n");
    return 0;
}

static ssize_t mcp2515_read(struct file *file,char __user *buf, size_t size, loff_t *off)
{
    printk("This is mcp2515_cdev read\r\n");
    return 0;
}

/*向设备写入数据函数*/
static ssize_t mcp2515_write(struct file *file,const char __user *buf,size_t size,loff_t *off)
{
    printk("This is mcp2515_cdev_write\r\n");
    return 0;
}

static int mcp2515_release(struct inode *inode, struct file *file)
{
    printk("This is chrdev_release\r\n");
    return 0;
}

static struct file_operations mcp2515_fops = {
    .owner=THIS_MODULE,
    .open = mcp2515_open,
    .read = mcp2515_read,
    .write = mcp2515_write,
    .release = mcp2515_release,
};

// MCP2515 设备初始化函数
static int mcp2515_probe (struct spi_device *spi)
{
    printk ("This is mcp2515 probe\n");

    g_spi_dev = spi;          // 保存spi设备指针
    mcp2515_reset();          // ✅ 进入probe后执行芯片复位

    return 0;
}

// MCP2515 设备移除函数
static int mcp2515_remove (struct spi_device *spi)
{
    printk ("This is mcp2515 mcp2515_remove\n");
    g_spi_dev = NULL;         // 移除时清空全局指针,防止野指针
    return 0;
}

// MCP2515 设备匹配表,用于设备树匹配
static const struct of_device_id mcp2515_of_match_table [] = {
    {.compatible = "my-mcp2515"},
    { /* sentinel */ }
};

// MCP2515 设备 ID 匹配表,用于总线匹配
static const struct spi_device_id mcp2515_id_table [] = {
    {"mcp2515", 0},
    { /* sentinel */ }
};

// MCP2515 SPI 驱动结构体
static struct spi_driver spi_mcp2515 = {
    .probe = mcp2515_probe,
    .remove = mcp2515_remove,
    .driver = {
        .name = "mcp2515",
        .owner = THIS_MODULE,
        .of_match_table = mcp2515_of_match_table,
    },
    .id_table = mcp2515_id_table,
};

// 驱动初始化函数
static int __init mcp2515_init (void)
{
    int ret;

    ret=alloc_chrdev_region (&dev_num,0,1,"mcp2515");
    if(ret < 0){
        printk("alloc_chrdev_region is error\n");
        goto err_out;
    }
    printk ("alloc_chrdev_region is ok\n");
    major=MAJOR (dev_num);
    minor=MINOR (dev_num);
    printk ("major is %d\n",major);
    printk ("minor is %d\n",minor);

    cdev_init (&mcp2515_cdev,&mcp2515_fops);
    mcp2515_cdev.owner = THIS_MODULE;

    ret= cdev_add(&mcp2515_cdev,dev_num,1);
    if(ret < 0 ){
        printk("cdev_add is error\n");
        goto err_unreg_devnum;
    }
    printk("cdev_add is ok\n");

    mcp2515_class  = class_create (THIS_MODULE,"spi_to_can");
    if (IS_ERR(mcp2515_class)) {
        ret = PTR_ERR(mcp2515_class);
        printk("class_create failed!\n");
        goto err_cdev_del;
    }

    mcp2515_device = device_create(mcp2515_class, NULL, dev_num, NULL, "mcp2515");
    if (IS_ERR(mcp2515_device)) {
        ret = PTR_ERR(mcp2515_device);
        printk("device_create failed!\n");
        goto err_class_destroy;
    }

    // 注册 SPI 驱动
    ret = spi_register_driver (&spi_mcp2515);
    if (ret < 0) {
        printk ("spi_register_driver error\n");
        goto err_dev_destroy;
    }

    printk("mcp2515 module load complete\n");
    return 0;

    // ========== 错误回滚分支(新增,防止资源泄漏oops)==========
err_dev_destroy:
    device_destroy(mcp2515_class, dev_num);
err_class_destroy:
    class_destroy(mcp2515_class);
err_cdev_del:
    cdev_del(&mcp2515_cdev);
err_unreg_devnum:
    unregister_chrdev_region(dev_num,1);
err_out:
    return ret;
}

// 驱动退出函数
static void __exit mcp2515_exit (void)
{
    spi_unregister_driver (&spi_mcp2515);
    device_destroy (mcp2515_class,dev_num);
    class_destroy (mcp2515_class);
    cdev_del (&mcp2515_cdev);
    unregister_chrdev_region (dev_num,1);
    printk ("module exit \n");
}

module_init(mcp2515_init);
module_exit(mcp2515_exit);

MODULE_LICENSE("GPL");

总结

本篇内容核心知识点:

  • 会看使用手册、原理图、通过原理图接线
  • 设备树的配置相关技能,必须根据使用手册来针对性配置
  • 复位函数,通过spi 的 相关方法spi_write 进行写操作
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐