参考视频
【北京迅为】嵌入式学习之Linux驱动(第十六期SPI全新升级)基于RK3568开发板哔哩哔哩_bilibili
SPI基础知识
1. SPI介绍
基本概念
SPI(串行外设接口)是一种同步、全双工的串行通信协议,由摩托罗拉公司提出,广泛用于嵌入式系统中连接主控芯片(如MCU、SoC)与外围设备(如传感器、存储器、显示屏等)。其核心特点是通过主从架构实现高速数据传输。
核心特点
-
高速传输:支持从几百Kbps到几十Mbps的速率,具体取决于硬件时钟频率。
-
全双工通信:数据可同时在主设备和从设备之间双向传输。
-
灵活性:通过配置时钟极性和相位,适配不同设备需求。
-
简单性:协议层无复杂地址机制,完全依赖硬件片选(CS)控制。
-
无流控与错误检测:需依赖应用层处理数据完整性问题。
硬件连接
SPI通常需要4根信号线(部分场景可简化):
信号线 | 全称 | 作用 |
---|---|---|
SCK | Serial Clock | 主设备输出的同步时钟信号。 |
MOSI | Master Out Slave In | 主设备发送数据,从设备接收数据。 |
MISO | Master In Slave Out | 从设备发送数据,主设备接收数据。 |
SS/CS | Slave 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 0000 | 1111 1111 |
第一个时钟周期 | 0000 0001 | 1111 1110 |
第二个时钟周期 | 0000 0011 | 1111 1100 |
... | ... | ... |
第八个时钟周期 | 1111 1111 | 0000 0000 |
从上表中可以看出每次数据传输主设备和从设备进行数据交换,也就是说再一个SPI时钟周期内,收发同时进行。
主机通过MOSI线发送1bit数据,从机通过该线读取1bit数据。
从机通过MISO线发送1bit数据,主机通过该线读取1bit数据。
如果主设备给从设备传输数据,主设备只需要忽略从设备接收到的数据即可。如果主设备要从从设备接收数据,主设备向从设备发送数据,从设备忽略掉主设备接收的数据即可。
3.spi极性和相位
SPI通过时钟极性(CPOL)和时钟相位(CPHA)定义四种工作模式:
模式 | CPOL | CPHA | 描述 |
---|---|---|---|
0 | 0 | 0 | 时钟空闲为低电平,数据在上升沿采样,下降沿发数据。 |
1 | 0 | 1 | 时钟空闲为低电平,数据在下降沿采样,上升沿发数据。 |
2 | 1 | 0 | 时钟空闲为高电平,数据在下降沿采样,上升沿发数据。 |
3 | 1 | 1 | 时钟空闲为高电平,数据在上升沿采样,下降沿发数据。 |
示例:模式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-18、SOIC-18。 |
中断支持 | 提供多种中断源(如接收完成、发送完成、错误中断)。 |
滤波功能 | 内置 6 个接收缓冲区,支持 2 个全帧滤波器 和 4 个掩码滤波器。 |
2. 引脚功能
以 SOIC-18 封装为例,关键引脚说明如下:
引脚 | 名称 | 功能 |
---|---|---|
1 | TXCAN | CAN 发送引脚,需外接 CAN 收发器(如 TJA1050)。 |
2 | RXCAN | CAN 接收引脚,连接 CAN 收发器。 |
3 | CLKOUT | 可配置时钟输出引脚(可选晶振分频输出)。 |
4 | TX0RTS | 发送缓冲区 0 请求发送控制引脚(可选功能)。 |
5-8 | TXnRTS | 发送缓冲区 1/2 请求发送引脚(部分型号支持)。 |
9 | OSC1 | 晶振输入或外部时钟输入(4-24 MHz)。 |
10 | OSC2 | 晶振输出(若使用外部时钟,此引脚悬空)。 |
11 | VSS | 地(GND)。 |
12 | VDD | 电源(2.7V - 5.5V)。 |
13 | RESET | 复位引脚(低电平有效)。 |
14 | CS | SPI 片选引脚(低电平有效)。 |
15 | SCK | SPI 时钟输入。 |
16 | SI | SPI 数据输入(主控输出,MCP2515 输入)。 |
17 | SO | SPI 数据输出(MCP2515 输出,主控输入)。 |
18 | INT | 中断输出引脚(低电平有效,用于通知主控事件)。 |
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