Linux SPI驱动笔记

参考视频

【北京迅为】嵌入式学习之Linux驱动(第十六期SPI全新升级)基于RK3568开发板哔哩哔哩_bilibili

SPI基础知识

1. SPI介绍

基本概念

SPI(串行外设接口)是一种同步、全双工的串行通信协议,由摩托罗拉公司提出,广泛用于嵌入式系统中连接主控芯片(如MCU、SoC)与外围设备(如传感器、存储器、显示屏等)。其核心特点是通过主从架构实现高速数据传输。

核心特点

  • 高速传输:支持从几百Kbps到几十Mbps的速率,具体取决于硬件时钟频率。

  • 全双工通信:数据可同时在主设备和从设备之间双向传输。

  • 灵活性:通过配置时钟极性和相位,适配不同设备需求。

  • 简单性:协议层无复杂地址机制,完全依赖硬件片选(CS)控制。

  • 无流控与错误检测:需依赖应用层处理数据完整性问题。

硬件连接

SPI通常需要4根信号线(部分场景可简化):

信号线全称作用
SCKSerial Clock主设备输出的同步时钟信号。
MOSIMaster Out Slave In主设备发送数据,从设备接收数据。
MISOMaster In Slave Out从设备发送数据,主设备接收数据。
SS/CSSlave Select / Chip Select主设备控制从设备选通的片选信号(低电平有效)。
 主设备 (MCU)             从设备 (如EEPROM)
    SCK   ------------------->   SCK
    MOSI  ------------------->   MOSI
    MISO  <-------------------   MISO
    CS1   ------------------->   CS
  • 多从设备连接:每个从设备需独立片选线(CS1、CS2…),主设备通过拉低对应CS选择通信目标。

2.spi通信原理

SPI总线在进行数据传输时,默认先传输高位,后传输低位,数据线为高电平表示逻辑1,数据线为第电平表示逻辑0,一个字节传输完成后,无需应答信号即可开启下一个字节的传输。SPI总线采用同步方式,在时钟线的第一个或者第二个跳变沿采集数据(主机侧读数据),然后再紧接着的下一个跳变沿发数据,8个始终周期即可完成一个字节的传输。

SPI主设备和从设备都有一个串行移位寄存器,主设备通过向SPI串行寄存器写入一个字节来发起数据传输。

举例:

主设备从设备
数据0000 00001111 1111
第一个时钟周期0000 00011111 1110
第二个时钟周期0000 00111111 1100
.........
第八个时钟周期1111 11110000 0000

从上表中可以看出每次数据传输主设备和从设备进行数据交换,也就是说再一个SPI时钟周期内,收发同时进行。

主机通过MOSI线发送1bit数据,从机通过该线读取1bit数据。

从机通过MISO线发送1bit数据,主机通过该线读取1bit数据。

如果主设备给从设备传输数据,主设备只需要忽略从设备接收到的数据即可。如果主设备要从从设备接收数据,主设备向从设备发送数据,从设备忽略掉主设备接收的数据即可。

3.spi极性和相位

SPI通过时钟极性(CPOL)时钟相位(CPHA)定义四种工作模式:

模式CPOLCPHA描述
000时钟空闲为低电平,数据在上升沿采样,下降沿发数据。
101时钟空闲为低电平,数据在下降沿采样,上升沿发数据。
210时钟空闲为高电平,数据在下降沿采样,上升沿发数据。
311时钟空闲为高电平,数据在上升沿采样,下降沿发数据。

示例:模式0适用于大多数SPI设备(如NOR Flash、加速度传感器)。

SPI子系统框架

1.spi子系统框架讲解

介绍RK3568的SPI

SPI接口:

支持4路SPI控制器

支持1个片选输出和其它2个片选输出

支持主模式和从模式,软件可配置

iTOP-RK3568 开发板SPI使用情况:

SPI0:
	GPIO_B5_u0: SPI0_CLK_M0
	GPIO_B6_u0: SPI0_MOSI_M0
	GPIO_C5_d: SPI0_MISO_M0
	GPIO_C6_d: SPI0_CS0_M0
	GPIO_C4_d: SPI0_CS1_M0
	有引出独立的接口,开发板未使用:

	GPIO2_D0_d:	SPI0_MISO_M1
	GPIO2_D1_d:	SPI0_MOSI_M1
	GPIO2_D2_d:	SPI0_CS0_M1
	GPIO2_D3_d:	SPI0_CLK_M1
	被开发板的GPIO复用。

SPI1:
	GPIO2_C6_d: SPI1_CS1_M0
	GPIO2_C0_d: SPI1_CS0_M0
	GPIO2_B7_d: SPI1_MOSI_M0
	GPIO2_B6_d: SPI1_MISO_M0
	GPIO2_B5_d: SPI1_CLK_M0
	再开发板子上被网口复用
	
	GPIO3_A1_d: SPI1_CS0_M1
	GPIO3_C1_d: SPI1_MOSI_M1
	GPIO3_C2_d: SPI1_MISO_M1
	GPIO3_C3_d: SPI1_CLK_M1
	被开发板上的耳机麦克风和PCIE复用

SPI2:
	GPIO2_C1_d: SPI2_CS1_M0
	GPIO2_C2_d: SPI2_CS0_M0
	GPIO2_B3_d: SPI2_MOSI_M0
	GPIO2_B4_d: SPI2_MISO_M0
	GPIO2_B5_d: SPI2_CLK_M0
	再开发板子上被网口复用
	
	GPIO2_D4_d: SPI2_CS1_M1
	GPIO2_D5_d: SPI2_CS0_M1
	GPIO2_D6_d: SPI2_MOSI_M1
	GPIO2_D7_d: SPI2_MISO_M1
	GPIO3_A0_d: SPI2_CLK_M1
	被开发板上的PCIE复用	

SPI3:
	GPIO4_B2_d: SPI3_MOSI_M0
	GPIO4_B0_d: SPI3_MISO_M0
	GPIO4_A7_d: SPI3_CS1_M0
	GPIO4_A6_d: SPI3_CS0_M0
	GPIO4_B3_d: SPI3_CLK_M0
	再开发板子上被网口复用
	
	GPIO4_C2_d: SPI3_CLK_M1
	GPIO4_C3_d: SPI3_MOSI_M1
	GPIO4_C5_d: SPI3_MISO_M1
	GPIO4_C6_d: SPI3_CS0_M1
	被开发板上被串口,sata等接口复用

iTOP-RK3588 开发板SPI使用情况:

SPI0:
	GPIO0_C6_u0: SPI0_CLK_M0
	GPIO_B6_u0: SPI0_MOSI_M0
	GPIO0_C0_d: SPI0_MOSI_M0: pin23
	
	GPIO4_B1_u: SPI0_CS1_M1: pin40
	GPIO4_A2_d: SPI0_CLK_M1: pin80
	GPIO4_A0_d: SPI0_MISO_M1: pin67
	GPIO4_A1_d: SPI0_MOSI_M1: pin69
	
	GPIO1_B3_d: SPI0_CLK_M2
	GPIO1_B2_d: SPI0_MOSI_M2
	GPIO1_B1_d: SPI0_MISO_M2
	GPIO1_B5_u: SPI0_CS1_M2
	GPIO1_B4_u: SPI0_CS0_M2
	
	GPIO3_D1_d: SPI0_MISO_M3: pin35
	GPIO3_D2_d: SPI0_MOSI_M3: pin37
	GPIO3_D3_d: SPI0_CLK_M3: pin39
	GPIO3_D4_d: SPI0_CS0_M3: pin41
	GPIO3_D5_d: SPI0_CS1_M3: pin71

 

2.编写通用spi外设驱动框架

1.device部分

查看设备树文件:kernel\arch\arm64\boot\dts\rockchip\rk3588s.dtsi

	spi4: spi@fecb0000 {
		compatible = "rockchip,rk3066-spi";
		reg = <0x0 0xfecb0000 0x0 0x1000>;
		interrupts = <GIC_SPI 330 IRQ_TYPE_LEVEL_HIGH>;
		#address-cells = <1>;
		#size-cells = <0>;
		clocks = <&cru CLK_SPI4>, <&cru PCLK_SPI4>;
		clock-names = "spiclk", "apb_pclk";
		dmas = <&dmac2 13>, <&dmac2 14>;
		dma-names = "tx", "rx";
		pinctrl-names = "default";
		//pinctrl-0 = <&spi4m0_cs0 &spi4m0_cs1 &spi4m0_pins>;
		pinctrl-0 = <&spi4m2_cs0 &spi4m2_pins>;
		num-cs = <2>;
		status = "disabled";
	};

找打spi0对应的控制器的设备树,使用compatible的“rockchip,rk3066-spi” 即可搜索到对应的spi控制器的驱动程序:drivers\spi\spi-rockchip.c

讯为RK3588对应板子的设备树:kernel\arch\arm64\boot\dts\rockchip\topeet_rk3588_config.dtsi

描述SPI外设:

&spi4 {
	status = "okay";

	mcp2515:mcp2515@0 {
		compatible = "my-mcp2515";
		reg = <0>;
		spi-max-frequency = <24000000>;
	};
};

SPI外设解析代码流程:

rockchip_spi_probe()
	--> devm_spi_register_controller()
		--> spi_register_controller()
			--> of_register_spi_devices()
				--> of_register_spi_device()
					--> of_spi_parse_dt()
						{
							...;
							//关键点,描述SPI外设的设备树,得有“reg”配置,否则返回错误
							rc = of_property_read_u32(nc, "reg", &value);
							if (rc) {
								dev_err(&ctlr->dev, "%pOF has no valid 'reg' property (%d)\n", nc, rc);
								return rc;
							}
						}
2. driver部分

驱动代码如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>

int mcp2515_probe(struct spi_device *spi)
{
    printk("This is mcp2515 probe\n");
    return 0;
}

int mcp2515_remove(struct spi_device *spi)
{
    return 0;
}

const struct of_device_id mcp2515_of_match_table[] = {
    {.compatible = "my-mcp2515"},
    {}
};

const struct spi_device_id mcp2515_id_table[] = {
    {"mcp2515"},
    {}
};

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 = spi_register_driver(&spi_mcp2515);
    if( ret < 0 ) {
        printk("spi_register_driver return error.\n");
        return ret;
    }

    return ret;
}

static void __exit mcp2515_exit(void)
{
    spi_unregister_driver(&spi_mcp2515);
}

module_init(mcp2515_init);
module_exit(mcp2515_exit);
MODULE_LICENSE("GPL");

接下来扫描内核,并在板子上加载驱动文件。

查看对应spi0是否包含my-mcp2515这个字符串

cd /sys/firmware/devicetree/base/spi@fecb0000
ls
'#address-cells'  '#size-cells'   clock-names   clocks   compatible   dma-names   dmas   interrupts   mcp2515@0   name   num-cs   pinctrl-0   pinctrl-names   reg   status

接着查看rk3588引脚复用是否有问题:

cd /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl
cat pinmux-pins

...
pin 32 (gpio1-0): fecb0000.spi (GPIO UNCLAIMED) function spi4 group spi4m2-pins
pin 33 (gpio1-1): fecb0000.spi (GPIO UNCLAIMED) function spi4 group spi4m2-pins
pin 34 (gpio1-2): fecb0000.spi (GPIO UNCLAIMED) function spi4 group spi4m2-pins
pin 35 (gpio1-3): fecb0000.spi (GPIO UNCLAIMED) function spi4 group spi4m2-cs0
...

spi外设mcp2515 驱动编写

1.mcp2515芯片介绍

MCP2515 是 Microchip 公司推出的一款独立 CAN 控制器,支持 CAN 2.0A/B 协议,广泛应用于汽车电子、工业控制、机器人等需要可靠通信的领域。它通过 SPI 接口与主控芯片(如 STM32、ESP32、树莓派等)通信,简化了 CAN 总线协议的硬件实现。

1. 核心特性
特性说明
协议支持CAN 2.0A/B,兼容标准帧(11位标识符)和扩展帧(29位标识符)。
通信速率最高支持 1 Mbps 的 CAN 总线速率。
SPI 接口支持标准 SPI 模式 0,0 和 1,1,时钟频率最高 10 MHz
工作电压2.7V - 5.5V,兼容 3.3V 和 5V 系统。
工作温度工业级:-40°C 至 +125°C。
封装常见为 PDIP-18SOIC-18
中断支持提供多种中断源(如接收完成、发送完成、错误中断)。
滤波功能内置 6 个接收缓冲区,支持 2 个全帧滤波器4 个掩码滤波器

2. 引脚功能

SOIC-18 封装为例,关键引脚说明如下:

引脚名称功能
1TXCANCAN 发送引脚,需外接 CAN 收发器(如 TJA1050)。
2RXCANCAN 接收引脚,连接 CAN 收发器。
3CLKOUT可配置时钟输出引脚(可选晶振分频输出)。
4TX0RTS发送缓冲区 0 请求发送控制引脚(可选功能)。
5-8TXnRTS发送缓冲区 1/2 请求发送引脚(部分型号支持)。
9OSC1晶振输入或外部时钟输入(4-24 MHz)。
10OSC2晶振输出(若使用外部时钟,此引脚悬空)。
11VSS地(GND)。
12VDD电源(2.7V - 5.5V)。
13RESET复位引脚(低电平有效)。
14CSSPI 片选引脚(低电平有效)。
15SCKSPI 时钟输入。
16SISPI 数据输入(主控输出,MCP2515 输入)。
17SOSPI 数据输出(MCP2515 输出,主控输入)。
18INT中断输出引脚(低电平有效,用于通知主控事件)。

3. 典型应用电路
(1) 硬件连接示意图
主控(如 STM32)          MCP2515          CAN 收发器(如 TJA1050)
   SPI_SCK  ------> SCK                      |
   SPI_MOSI ------> SI                       |
   SPI_MISO <------ SO                       |
   SPI_CS   ------> CS                       |
   GPIO     <------ INT                      |
   RESET    ------> RESET                    |
                                           TXCAN ------> CAN_H
                                           RXCAN ------> CAN_L
  • CAN 收发器:MCP2515 需外接 CAN 收发器(如 TJA1050、MCP2551)以连接 CAN 总线。

  • 终端电阻:CAN 总线两端需加 120Ω 终端电阻

(2) 晶振电路
  • 若使用外部晶振,需在 OSC1 和 OSC2 之间连接 4-24 MHz 晶振 并并联负载电容(如 22pF)。

 

2. 编写mcp2515驱动

1.字符设备
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>

dev_t dev_num;
struct cdev mcp2515_cdev;
struct class *mcp2515_class;
struct device *mcp2515_device;

int mcp2515_open(struct inode *inode, struct file *file)
{
    return 0;
}

ssize_t mcp2515_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
    return 0;
}

ssize_t mcp2515_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
    return 0;
}

int mcp2515_release(struct inode *inode, struct file *file)
{
    return 0;
}

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

int mcp2515_probe(struct spi_device *spi)
{
    int ret;
    printk("This is mcp2515 probe\n");

    ret = alloc_chrdev_region(&dev_num, 0, 1, "mcp2515");
    if(ret < 0)
    {
        printk("alloc_chrdev_region error\n");
        return -1;
    }

    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 error!\n");
        return -1;
    }

    mcp2515_class = class_create(THIS_MODULE, "spi_to_can");
    if( IS_ERR(mcp2515_class) )
    {
        printk("class_create error!\n");
        return PTR_ERR(mcp2515_class);
    }

    mcp2515_device = device_create(mcp2515_class, NULL, dev_num, NULL, "mcp2515");
    if( IS_ERR(mcp2515_device) )
    {
        printk("device_create error!\n");
        return PTR_ERR(mcp2515_device);
    }

    return 0;
}

int mcp2515_remove(struct spi_device *spi)
{
    return 0;
}

const struct of_device_id mcp2515_of_match_table[] = {
    {.compatible = "my-mcp2515"},
    {}
};

const struct spi_device_id mcp2515_id_table[] = {
    {"mcp2515"},
    {}
};

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 = spi_register_driver(&spi_mcp2515);
    if( ret < 0 ) {
        printk("spi_register_driver return error.\n");
        return ret;
    }

    return ret;
}

static void __exit mcp2515_exit(void)
{
    device_destroy(mcp2515_class, dev_num);
    class_destroy(mcp2515_class);
    cdev_del(&mcp2515_cdev);
    unregister_chrdev_region(dev_num, 1);
    spi_unregister_driver(&spi_mcp2515);
}

module_init(mcp2515_init);
module_exit(mcp2515_exit);
MODULE_LICENSE("GPL");

重新编译内核,烧写内核,加载驱动后,内核打印如下,说明驱动加载正常:

[  299.975414] This is mcp2515 probe

// 查看dev下是否有mcp2515
root@topeet:~# ls /dev/mcp2515
/dev/mcp2515

// 查看sys下是否有class spi_to_can
ls /sys/class/spi_to_can/mcp2515/
2.对寄存器的介绍和操作
mcp2515复位操作

mcp2515在正常运行之前必须进行初始化,只有在配置模式下,才能对期间进行初始化,在商店或者复位时,器件或自动进入复位模式,或通过CANTRL,REQOP位设置位“100”也可使器件从任何模式进入配置模式。

修改设备树,spi配置kernel\arch\arm64\boot\dts\rockchip\topeet_rk3588_config.dtsi:

&spi4 {
	status = "okay";

	mcp2515:mcp2515@0 {
		compatible = "my-mcp2515";
		reg = <0>;
		//spi-cpha; //如果spi-cpha和spi-cpol都设置,那么就是将spi工作模式设置位11
		//spi-cpol; //如果spi-cpha和spi-cpol都没设置,那么就是将spi工作模式设置位00
		//spi-lsb-first; //如果spi-lsb-first表示spi先发低位;如果没有设置表示spi先发高位
		//spi-cs-high; 如果这个字段设置,那么高电平选中SPI设备,否则低电平选中SPI设备
		spi-max-frequency = <10000000>;
		status = "okay";
	};
};

修改驱动程序,代码如下所示:

...;
#include <linux/spi/spi.h>

...;
struct device *mcp2515_device;
truct spi_device *spi_dev;
...;

void mcp2515_reset(void)
{
    //reset command:0xc0
    char write_buf[] = {0xc0};
    int ret;

    ret = spi_write(spi_dev, write_buf, sizeof(write_buf));
    if( ret < 0 )
    {
        printk("spi_write return error.\n");
    }
}

int mcp2515_probe(struct spi_device *spi)
{
	...;
	printk("This is mcp2515 probe\n");
	
	spi_dev = spi;
	...;
}
探究:SPI通信流程

读寄存器函数

通过读取mcp2515状态寄存器得值,即可知道mcp2515是否复位成功。

对SPI设备进行读取操作,先发送读取操作指令: 0000 0011 (0x03),然后是对应寄存器得地址。步骤:先写指令,后读取。

新增函数mcp2515_read_reg(),用于读取SPI设备寄存器值。

char mcp2515_read_reg(char reg)
{
    char write_buf[] = {0x03, reg}; //0x03 为MCP2515的写指令
    char read_buf;
    int ret;

    ret = spi_write_then_read(spi_dev, write_buf, sizeof(write_buf), &read_buf, sizeof(read_buf));
    if( ret < 0 )
    {
        printk("spi_write_then_read return error.\n");
        return ret;
    }

    return read_buf;
}

...;
int mcp2515_probe(struct spi_device *spi)
{
    char value;
    ...;
    mcp2515_reset();
    value = mcp2515_read_reg(0x0e);  //0x0e 为mcp2515状态寄存器值
    printk("mcp2515 reg:0x0e value is %d\n", value);
    
    return 0;
}
迅为RK3588 邮孔版本与mcp2515硬件连接

等拿到mcp2515模块后,更新这个章节。

配置cn1,cn2,cn3寄存器

位时间就是指一个(二进制)位在总线传输所需要的时间,一个位时间分为4段,这些段又由称为Time Quantum(缩写Tg)的最小时间单位组成:

CNF1,CNF2,CNF3 就是对位时间中的段 进行所占多少TQ配置的寄存器。

例子:Fosc=20Mhz 时欲实现125Khz的can波特率 (Fosc位晶振的频率)

步骤1:换算成周期(换成周期比较容易计算)

Tosc=1/20Mhz=50ns

步骤2:选择BRP的值,计算TQ,例子中选择的为0x4,根据计算公式TQ=2 * (BRP+1)/Fosc 得到TQ=2*5 * 50ns = 500ns

步骤3:计算欲达到125Khz需要多少TQ,将125Khz换算成周期为8000ns,8000/500 = 16TQ,即位时间一共占用16TQ.

SPI转can模块中用的晶振大小位8Mhz欲实现125Khz的can波特率。

步骤1:换算成周期

Tosc = 1 / 8Mhz = 125ns

步骤2:选择BRP的值,计算TQ

选择BRP位0x1,根据公式计算TQ=2 * (BRP + 1)/ Fosc 得到TQ=2 * 2 * 125ns = 500

步骤3:计算欲达到125Khz需要多少个TQ

将125Khz换算成周期8000ns,8000 / 500 = 16TQ

对时间段的编程

对时间段的编程必须要满足要求:

传播段 + 相位缓冲段 PS1 >= 相位缓冲段PS2

传播段 + 相位缓冲段 PS1 >= Tdelay

相位缓冲段 PS2 > 同步跳转宽度SJW,SJW最大可以设置位4个TQ,所以16个TQ可以这样分配:

同步段:1个TQ

传播段:2个TQ

相位缓冲段PS1: 7个TQ

相位缓冲段PS2:6个TQ

CNF1


CNF2

同步段:1个TQ

传播段:2个TQ

相位缓冲段ps1: 7个TQ

传播段:2个TQ,对应bit2-0 设置001

相位缓冲段PS1:7个TQ,对应bit5-3 设置为110

采样点设置进行一次采样,bit6设置为0

相位缓冲段PS2的时间长度为bit7 设置为1

CNF2寄存器的值应该设置为0xB1


CNF3

相位缓冲段PS2:6个TQ

相位缓冲段PS2:6个TQ bit2-0 设置为101

其他为设置为0,所以CNF3寄存器值为0x05

驱动代码修改如下所示:

...;

#define CNF1 0x2a
#define CNF2 0x29
#define CNF3 0x28
...;

void mcp2515_write_reg(char reg, char value)
{
    int ret;
    char write_buf[] = {0x02, reg, value}; //0x02 为mcp2515的写指令

    ret = spi_write(spi_dev, write_buf, sizeof(write_buf));
    if( ret < 0 )
    {
        printk("spi_write return error.\n");
    }
}

int mcp2515_probe(struct spi_device *spi)
{
    ...;
    
    mcp2515_reset();
    value = mcp2515_read_reg(0x0e);
    printk("mcp2515 reg:0x0e value is %d\n", value);

    mcp2515_write_reg(CNF1, 0x01);
    mcp2515_write_reg(CNF2, 0xb1);
    mcp2515_write_reg(CNF3, 0x05);

    return 0;
}

位修改指令和屏蔽字节

代码修改

...;
#define CNF1 0x2a
#define CNF2 0x29
#define CNF3 0x28
#define RXB0CTRL 0x60
#define CANINTE 0x2b
...;

void mcp2515_change_regbit(char reg, char mask, char value)
{
    int ret;
    char write_buf[] = {0x05, reg, mask, value};

    ret = spi_write(spi_dev, write_buf, sizeof(write_buf));
    if( ret < 0 )
    {
        printk("mcp2515_change_regbit return error.\n");
    }
}

int mcp2515_probe(struct spi_device *spi)
{
    ...;
    
    mcp2515_write_reg(CNF1, 0x01);
    mcp2515_write_reg(CNF2, 0xb1);
    mcp2515_write_reg(CNF3, 0x05);
    mcp2515_change_regbit(RXB0CTRL, 0x64, 0x60);
    mcp2515_write_reg(CANINTE, 0x05); //使能mcp2515对应终端

    return 0;
}
修改工作模式

mcp2515有五种工作模式,分别为:

1.配置模式

2.正常模式

3.休眠模式

4.仅监听模式

5.回环模式

将mcp2515 设置位回环模式

代码修改如下所示:

...;
#define CANCTRL 0xf
#define CANSTAT 0xe
...;

int mcp2515_probe(struct spi_device *spi)
{
    ...;
    
    mcp2515_change_regbit(CANCTRL, 0xe0, 0x40);  //设置mcp2515为回环模式
    value = mcp2515_read_reg(CANSTAT);
    printk("mcp2515 reg:CANSTAT value is %d\n", value);

    return 0;
}

实验现象

等拿到mcp2515模块后,补充这个章节。


编写write()函数

代码修改:

size_t mcp2515_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
    char w_kbuf[13] = {0};
    int ret;
    int i;

    mcp2515_change_regbit(TXB0CTRL, 0x30, 0x30); //设置最高报文发送优先级

    ret = copy_from_user(w_kbuf, buf, size);
    if( ret )
    {
        printk("copy_from_user w_kbuf is error\n");
        return -1;
    }

    for(i=0; i<sizeof(w_kbuf); i++)
    {
        mcp2515_write_reg(0x31+i, w_kbuf[i]); //设置发送缓冲寄存器
    }

    mcp2515_change_regbit(TXB0CTRL, 0x08, 0x08); //将寄存器TXB0CTRL的bit3设置为1,缓冲器等待发送
    
    while( !(mcp2515_read_reg(CANINTF) && (1 << 2)) ); //检查CANINTF的bit2是否被置1,置1表示发送成功
    
    mcp2515_change_regbit(CANINTF, 0x04, 0x00); //发送成功后,将CANINTF的bit2 清0

    return 0;
}
编写read()函数

ssize_t mcp2515_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
    char r_kbuf[13] = {0};
    int i;
    int ret;

    while ( !(mcp2515_read_reg(CANINTE) && (1 << 0)) );  //接收缓冲区曲中是否被置位

    for(i=0; i<sizeof(r_kbuf); i++)
    {
        r_kbuf[i] = mcp2515_read_reg(0x61+i);     //接收缓冲寄存器接收数据
    }

    mcp2515_change_regbit(CANINTF, 0x01, 0x00);   //将CANINTF的bit0 RX0IF清0

    ret = copy_to_user(buf, r_kbuf, size);   //接收到的数据copy到用户空间
    if( ret )
    {
        printk("copy_to_user is error.\n");
        return -1;
    }
    
    return 0;
}
驱动完整代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/spi/spi.h>
#include <linux/uaccess.h>

dev_t dev_num;
struct cdev mcp2515_cdev;
struct class *mcp2515_class;
struct device *mcp2515_device;
struct spi_device *spi_dev;

#define CNF1 0x2a
#define CNF2 0x29
#define CNF3 0x28
#define RXB0CTRL 0x60
#define CANINTE 0x2b
#define CANCTRL 0xf
#define CANSTAT 0xe
#define TXB0CTRL 0x30
#define CANINTF 0x2c

void mcp2515_reset(void)
{
    //reset command:0xc0
    char write_buf[] = {0xc0};
    int ret;

    ret = spi_write(spi_dev, write_buf, sizeof(write_buf));
    if( ret < 0 )
    {
        printk("spi_write return error.\n");
    }
}

char mcp2515_read_reg(char reg)
{
    char write_buf[] = {0x03, reg};
    char read_buf;
    int ret;

    ret = spi_write_then_read(spi_dev, write_buf, sizeof(write_buf), &read_buf, sizeof(read_buf));
    if( ret < 0 )
    {
        printk("spi_write_then_read return error.\n");
        return ret;
    }

    return read_buf;
}

void mcp2515_write_reg(char reg, char value)
{
    int ret;
    char write_buf[] = {0x02, reg, value};

    ret = spi_write(spi_dev, write_buf, sizeof(write_buf));
    if( ret < 0 )
    {
        printk("mcp2515_write_reg return error.\n");
    }
}

void mcp2515_change_regbit(char reg, char mask, char value)
{
    int ret;
    char write_buf[] = {0x05, reg, mask, value};

    ret = spi_write(spi_dev, write_buf, sizeof(write_buf));
    if( ret < 0 )
    {
        printk("mcp2515_change_regbit return error.\n");
    }
}

int mcp2515_open(struct inode *inode, struct file *file)
{
    return 0;
}

ssize_t mcp2515_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
    char r_kbuf[13] = {0};
    int i;
    int ret;

    while ( !(mcp2515_read_reg(CANINTE) && (1 << 0)) );

    for(i=0; i<sizeof(r_kbuf); i++)
    {
        r_kbuf[i] = mcp2515_read_reg(0x61+i);
    }

    mcp2515_change_regbit(CANINTF, 0x01, 0x00);

    ret = copy_to_user(buf, r_kbuf, size);
    if( ret )
    {
        printk("copy_to_user is error.\n");
        return -1;
    }
    

    return 0;
}

ssize_t mcp2515_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
    char w_kbuf[13] = {0};
    int ret;
    int i;

    mcp2515_change_regbit(TXB0CTRL, 0x30, 0x30);

    ret = copy_from_user(w_kbuf, buf, size);
    if( ret )
    {
        printk("copy_from_user w_kbuf is error\n");
        return -1;
    }

    for(i=0; i<sizeof(w_kbuf); i++)
    {
        mcp2515_write_reg(0x31+i, w_kbuf[i]);
    }

    mcp2515_change_regbit(TXB0CTRL, 0x08, 0x08);

    while( !(mcp2515_read_reg(CANINTF) && (1 << 2)) );

    mcp2515_change_regbit(CANINTF, 0x04, 0x00);

    return size;
}

int mcp2515_release(struct inode *inode, struct file *file)
{
    return 0;
}

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

int mcp2515_probe(struct spi_device *spi)
{
    int ret;
    char value;
    printk("This is mcp2515 probe\n");

    spi_dev = spi;

    ret = alloc_chrdev_region(&dev_num, 0, 1, "mcp2515");
    if(ret < 0)
    {
        printk("alloc_chrdev_region error\n");
        return -1;
    }

    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 error!\n");
        return -1;
    }

    mcp2515_class = class_create(THIS_MODULE, "spi_to_can");
    if( IS_ERR(mcp2515_class) )
    {
        printk("class_create error!\n");
        return PTR_ERR(mcp2515_class);
    }

    mcp2515_device = device_create(mcp2515_class, NULL, dev_num, NULL, "mcp2515");
    if( IS_ERR(mcp2515_device) )
    {
        printk("device_create error!\n");
        return PTR_ERR(mcp2515_device);
    }

    mcp2515_reset();
    value = mcp2515_read_reg(CANSTAT);
    printk("mcp2515 reg:CANSTAT value is %d\n", value);

    mcp2515_write_reg(CNF1, 0x01);
    mcp2515_write_reg(CNF2, 0xb1);
    mcp2515_write_reg(CNF3, 0x05);
    mcp2515_change_regbit(RXB0CTRL, 0x64, 0x60);
    mcp2515_write_reg(CANINTE, 0x05);

    mcp2515_change_regbit(CANCTRL, 0xe0, 0x40);
    value = mcp2515_read_reg(CANSTAT);
    printk("mcp2515 reg:CANSTAT value is %d\n", value);

    return 0;
}

int mcp2515_remove(struct spi_device *spi)
{
    return 0;
}

const struct of_device_id mcp2515_of_match_table[] = {
    {.compatible = "my-mcp2515"},
    {}
};

const struct spi_device_id mcp2515_id_table[] = {
    {"mcp2515"},
    {}
};

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 = spi_register_driver(&spi_mcp2515);
    if( ret < 0 ) {
        printk("spi_register_driver return error.\n");
        return ret;
    }

    return ret;
}

static void __exit mcp2515_exit(void)
{
    device_destroy(mcp2515_class, dev_num);
    class_destroy(mcp2515_class);
    cdev_del(&mcp2515_cdev);
    unregister_chrdev_region(dev_num, 1);
    spi_unregister_driver(&spi_mcp2515);
}

module_init(mcp2515_init);
module_exit(mcp2515_exit);
MODULE_LICENSE("GPL");

Makefile:

export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-none-linux-gnu-#交叉编译器前缀
obj-m += mcp2515.o    #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/rk3588-linux/kernel    #这里是你的内核目录                                                                                                                            
PWD ?= $(shell pwd)
all:
	make -C $(KDIR) M=$(PWD) modules    #make操作
clean:
	make -C $(KDIR) M=$(PWD) clean    #make clean操作

 


编写APP应用测试代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>


int main(int argc, char *argv[])
{
    int fd;
    int i;

    char w_buf[13] = {0x66, 0x08, 0x22, 0x33, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
    char r_buf[13] = {0};

    fd = open("/dev/mcp2515", O_RDWR);
    if( fd < 0 )
    {
        printf("open /dev/mcp2515 error\n");
        return -1;
    }

    write(fd, w_buf, sizeof(w_buf));

    read(fd, r_buf, sizeof(r_buf));

    for(i=0; i<13; i++)
    {
        printf("r_buf[%d] is %d\n", i, r_buf[i]);
    }

    close(fd);

    return 0;
}
实验现象

缺mcp2515模块, 这部分内容后续补充。


扩展知识

1. linux中spi通用驱动的使用

在用户空间中如何使用spi呢?

步骤1.在linux中有一个通用spi驱动程序,在内核源码目录下使用make menuconfig图形化中将这个驱动程序编译进内核。

	Device Drivers

​		[*] SPI support --->

​			<*> User mode SPI device driver support  //选中

通用SPI驱动源码路径:drivers\spi\spidev.c

步骤2:在设备树中将compatible属性修改位rockchip,spidev; kernel\arch\arm64\boot\dts\rockchip\topeet_rk3588_config.dtsi

&spi4 {
	status = "okay";

	mcp2515:mcp2515@0 {
		compatible = "rockchip,spidev";
		reg = <0>;
		//spi-cpha;
		//spi-cpol;
		//spi-lsb-first;
		//spi-cs-high;
		spi-max-frequency = <10000000>;
		status = "okay";
	};
};

步骤3:步骤1和步骤2完成后,在/dev/路径下就会有/dev/spidev4.0 设备, spidev4.0 这里的0表示spi4,后面1个0表示第一个SPI设备。

2. spidev_test和Linux中通用spi驱动使用

spidev_test使用

源码路径:spidev_test工具可以在我们的RK3588的Linux内核源码tools/spi下找到。

编译代码命令:

make CC=aarch64-none-linux-gnu-gcc LD=aarch64-none-linux-gnu-ld
ls
spidev_test spidev_fdx

将编译号的spidev_test和spidev_fdx拷贝到开发板上即可。

使用命令:

//将spi设备spidev0.0 配置位回环模式,并显示信息,这个命令可以检查spi配置是否有问题。
./spidev_test -D /dev/spidev0.0 -l -v

3. 在应用程序中如何使用spi

#include <stdio.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define RESET 0xc0
#define CANSTAT 0x0e
#define READ 0x03
#define CANCTRL 0x0f
#define WRITE 0x02

int mode = SPI_MODE_0;
int fd;
int bits = 8;
int speed = 10000000;
int delay;

int spi_init(void)
{
    int ret;
    fd = open("/dev/spidev4.0", O_RDWR);
    if( fd < 0 )
    {
        printf("open dev/spidev4.0 erroe\n");
        return -1;
    }

    /*
	 * spi mode
	 */
	ret = ioctl(fd, SPI_IOC_WR_MODE32, &mode);
	if (ret == -1)
        printf("can't set spi mode\n");

	ret = ioctl(fd, SPI_IOC_RD_MODE32, &mode);
	if (ret == -1)
        printf("can't get spi mode\n");

	/*
	 * bits per word
	 */
	ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
	if (ret == -1)
        printf("can't set bits per word\n");

	ret = ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits);
	if (ret == -1)
        printf("can't get bits per word\n");

	/*
	 * max speed hz
	 */
	ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
	if (ret == -1)
        printf("can't set max speed hz\n");

	ret = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed);
	if (ret == -1)
        printf("can't get max speed hz\n");

    printf("spi mode: 0x%x\n", mode);
    printf("bits per word: %u\n", bits);
    printf("max speed: %u Hz (%u kHz)\n", speed, speed/1000);

    return 0;
}

int transfer(int fd, char *tx, char *rx, int len)
{
    int ret;
    struct spi_ioc_transfer tr = {
		.tx_buf = (unsigned long)tx,
		.rx_buf = (unsigned long)rx,
		.len = len,
		.delay_usecs = delay,
		.speed_hz = speed,
		.bits_per_word = bits,
	};

	ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
	if (ret < 1)
    {
        printf("can't send spi message\n");
        return -1;
    }

    return 0;
    
}

int main(int argc, char *argv[])
{
    char reset_cmd[1] = {RESET};
    char rd_canstat[2] = {READ, CANSTAT};
    char canstat[4] = {0};
    char wr_canctrl[] = {WRITE, CANCTRL, 0x00};

    spi_init();

    transfer(fd, reset_cmd, NULL, sizeof(reset_cmd)); //Send Reset Cmd
    transfer(fd, rd_canstat, canstat, sizeof(canstat));

    printf("canstat is %x\n", canstat[2]);

    memset(canstat, 0, sizeof(canstat));

    transfer(fd, wr_canctrl, NULL, sizeof(wr_canctrl));
    transfer(fd, rd_canstat, canstat, sizeof(canstat));

    printf("canstat is %x\n", canstat[3]);

    return 0;
}

4. Linux 中如何使用模拟spi

模拟 SPI
1. 把模拟 spi 的驱动编译进内核
模拟 spi 驱动位置:drivers/spi/spi-gpio.c
在 make menuconfig 图形化配置界面选中:
Device Drivers --->
[*] SPI support --->
    <*> GPIO-based bitbanging SPI Master //选中

cp .config arch/arm64/configs/rockchip_linux_defconfig  //这步骤一定要操作,否则修改后的配置,就被还原了

2. 在设备树中编写 spi 控制器节点
arch/arm64/boot/dts/rockchip/rk3568.dtsi
spi5:spi@gpiof {
    compatible = "spi-gpio";
    #address-cells = <1>;
    gpio-sck = <&gpio0 RK_PB0 GPIO_ACTIVE_LOW>;
    gpio-miso = <&gpio1 RK_PB0 GPIO_ACTIVE_LOW>;
    gpio-mosi = <&gpio1 RK_PB1 GPIO_ACTIVE_LOW>;
    cs-gpios = <&gpio1 RK_PB2 GPIO_ACTIVE_LOW>;
    num-chipselects = <1>;
    pinctrl-names = "default";
    pinctrl-0 = <&spi5_gpios>;
    status = "disabled";
};

实践课程

移植官方mcp2515驱动

1. 把驱动编译进内核  
Linux 内核中默认有 MCP2515 的驱动程序,所以不需要和供应商要驱动程序和自己写 Kconfig 文件。只需要在 make menuconfig 图形化配置界面中选中即可。

[*] Networking support --->  
<*> CAN bus subsystem support --->  
    CAN Device Drivers --->  
    <*> Platform CAN drivers with Netlink support //选中  
    CAN SPI interfaces --->  
    <*> Microchip MCP251x and MCP25625 SPI CAN controllers //选中  

MCP2515 官方驱动在Linux kernel路径: kernel/drivers/net/can/spi/mcp251x.c

修改设备树文件:

&spi0{
	status = "okay";
	mcp2515:mcp2515@0{
    	compatible = "microchip,mcp2515";
    	reg = <0>;
    	spi-max-frequency = <10000000>;
    	interrupt-parent = <&gpio0>;
    	interrupts = <RK_PB0IRQ_TYPE_EDGE_FALLING>;
    	pinctrl-names = "default";
    	pinctrl-0 = <&mcp2515_int>;
    	clocks = <&clk8m>;
    	status = "okay";
	};
	clk8m:clk8 {
		compatible = "fixed-clock";
		#clock-cells = <0>;
		clock-frequency = <8000000>
	};
};

mcp2515-gpios {
	mcp2515_int:mcp2515-int{
		rockchip,pins = <0 RK_PB0 &pcfg_pnull_none>;
	};
};

回环测试

ip link set can1 down  
ip link set can1 type can bitrate 250000  
ip link set can1 type can loopback on  
ip link set up can1  
candump can1 -L &  
cansend can1 123#1122334455667788 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yan12368

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值