Linux内核编程(十九)SPI子系统驱动
本文目录
对于SPI基础知识这里不做过多讲解,详情查看:SPI基础知识实践讲解-STM32版。
一、 SPI驱动框架图
本框图中spi核心层和spi适配器驱动层不需要我们去关心,如果未来要去原厂工作的话,可以深入了解其工作原理和内容,这里我们不做过多介绍。
二、编写SPI驱动device框架
- 查看开发板中可用的SPI引脚。
- 修改设备树文件。
修改完成设备树后重新编译设备树源码生成出来的boot.img烧录至开发板。
(1)修改spi控制器源码设备树文件。
将控制器的pinctrl-0复用引脚的spi0m0修改为我们所使用的spi0m1。由于原厂工程师已经写完spi控制器的引脚复用功能,所以我们只需要修改即可。
spi0: spi@fe610000 {
compatible = "rockchip,rk3066-spi";
reg = <0x0 0xfe610000 0x0 0x1000>; // SPI 控制器寄存器基地址
interrupts = <GIC SPI 103 IRQ TYPE LEVEL HIGH>; // 中断配置
#address-cells = <1>; // 地址单元数量
#size-cells = <0>; // 大小单元数量
clocks = <&cru CLK_SPI>, <&cru PCLK_SPIO>; // SPI 和 APB 时钟
clock-names = "spiclk", "apb pclk"; // 时钟名称
dmas = <&dmac0 20>, <&dmac0 21>; // DMA 通道配置(TX, RX)
dma-names = "tx", "rx"; // DMA 通道名称
pinctrl-names = "default", "high-speed"; // 引脚控制模式
// pinctrl-0 = <&spi0m0_cs0 &spi0m0_csl &spi0m0_pins>; // 默认引脚控制配置
pinctrl-0 = <&spi0m1_cs0 &spi0m1_pins>; // 默认引脚控制配置
// pinctrl-1 = <&spi0m0_cs0 &spi0m0_csl &spi0m0_pins_high_speed>; // 高速模式引脚配置
pinctrl-1 = <&spi0m1_cs0 &spi0m1_pins_high_speed>; // 高速模式引脚配置
status = "disabled"; // 当前 SPI 控制器状态,禁用状态
};
(2)添加引用spi控制器
在开发板设备树根节点下添加以下节点。如果设备树中没有reg 和 spi-max-freguengy这俩个属性就会返回错误。返回错误设备注册不成功。从而就不会和驱动匹配上(具体分析spi控制器的注册流程)。以下的SPI工作模式以及传输模式等都需要根据具体的SPI外设来设置!!
&spi0 {
status ="okay";
mcp2515:mcp2515@0{
status ="okay";
compatible="my-mcp2515";
reg = <0>; /* 1. 选择片选0 */
spi-max-frequency = <24000000>; /* 2. 设置SPI时钟最大频率为24MHz,不要超过50M */
//3. 设置工作模式,如果不写,则默认为0,0
spi-cpha = <1>; // 设置时钟相位
spi-cpol = <0>; // 设置时钟极性
//4. 设置传输模式,高位先传还是低位先传。默认为高位先传。
//spi-lsb-first = <1>; // 设置为 LSB 优先
//5. 选择片选信号为高电平/低电平选中。默认为低电平选中。
// spi-cs-high = <1>; // 设置片选信号为高电平有效
}
}
reg
属性用于设置 SPI 设备的片选引脚,而 spi-max-frequency
属性则用于设置 SPI 总线的最大时钟频率。通常,SPI 设备的片选和时钟频率都需要在设备树中进行配置,以便正确地初始化 SPI 设备。
三、编写SPI驱动driver框架
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/kernel.h>
static int mcp2515_probe(struct spi_device *spi)
{
return 0;
}
static int mcp2515_remove(struct spi_device *spi)
{
return 0;
}
static const struct of_device_id mcp2515_of_match[] = {
{ .compatible = "my-mcp2515", },
{ }
};
static struct spi_driver mcp2515_driver = {
.driver = {
.name = "mcp2515",
.owner = THIS_MODULE,
.of_match_table = mcp2515_of_match,
},
.probe = mcp2515_probe,
.remove = mcp2515_remove,
};
static int __init mcp2515_init(void)
{
return spi_register_driver(&mcp2515_driver);
}
static void __exit mcp2515_exit(void)
{
spi_unregister_driver(&mcp2515_driver);
}
module_init(mcp2515_init);
module_exit(mcp2515_exit);
MODULE_LICENSE("GPL");
四、实验一编写mcp2515驱动
MCP2515 是由 Microchip 提供的一款独立的 CAN (Controller Area Network) 控制器,它通过 SPI 接口与主机通信。即SPI转CAN的一个模块。
1. 注册字符设备或杂项设备框架
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
static ssize_t myread(struct file *fp, char __user *ubuf, size_t size, loff_t *loft)
{
return 0;
}
static int my_open(struct inode *node, struct file *fp)
{
return 0;
}
ssize_t my_write(struct file *fp, const char __user *ubuf, size_t size, loff_t *loft)
{
// 若不需要实际的写操作,可以直接返回0或-EINVAL
return 0;
}
static const struct file_operations myfops = {
.read = myread,
.open = my_open,
.write = my_write,
};
static struct miscdevice mcp2515_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "mcp2515",
.fops = &myfops,
};
static int mcp2515_probe(struct spi_device *spi)
{
int ret;
ret = misc_register(&mcp2515_misc);
if (ret) {
pr_err("Failed to register misc device\n");
return ret;
}
return 0;
}
static int mcp2515_remove(struct spi_device *spi)
{
pr_info("MCP2515 SPI device removed\n");
misc_deregister(&mcp2515_misc);
return 0;
}
static const struct of_device_id mcp2515_of_match[] = {
{ .compatible = "my-mcp2515", },
{ }
};
static struct spi_driver mcp2515_driver = {
.driver = {
.name = "mcp2515", //不会与之匹配
.owner = THIS_MODULE,
.of_match_table = mcp2515_of_match,
},
.probe = mcp2515_probe,
.remove = mcp2515_remove,
};
static int __init mcp2515_init(void)
{
pr_info("MCP2515 driver loading\n");
return spi_register_driver(&mcp2515_driver);
}
static void __exit mcp2515_exit(void)
{
pr_info("MCP2515 driver unloading\n");
spi_unregister_driver(&mcp2515_driver);
}
module_init(mcp2515_init);
module_exit(mcp2515_exit);
MODULE_LICENSE("GPL");
2. MCP2515相关配置
MCP2515提供了一系列的SPI指令,通过向MCP2515发送SPI指令就可以完成复位、读、写等操作,具体的SPI指令如下所示:
(1)SPI写数据
成功时:返回发送的字节数,通常是 len。
失败时:返回负的错误代码,通常为 -EINVAL(无效参数)或 -EIO(输入/输出错误)等。
注意:我们要将写入的数据一次性的按照写入的先后顺序存放在一个数组中。
int spi_write(struct spi_device *spi, const void *buf, unsigned len);
/*
spi:指向目标 SPI 设备的指针。这是你要向其发送数据的 SPI 设备。
buf:指向包含要发送的数据的缓冲区的指针。这些数据将被通过 SPI 总线发送。
len:要发送的数据的字节数。表示从 buf 中发送的字节数。
*/
●示例: 我们通过mcp2515手册可知,对其进行写操作需要写入0x02。使用spi_write
来进行写操作。
#include <linux/spi/spi.h>
struct spi_device *spi;
void mcp2515_write_reg(char reg, char value)
{
int ret;
char write_buf[] = {0x02, reg, value}; // MCP2515 写寄存器的指令
// 向 MCP2515 发送数据
ret = spi_write(spi, write_buf, sizeof(write_buf));
if (ret < 0) {
printk(KERN_ERR "MCP2515 write reg failed: %d\n", ret);
}
}
(2)SPI读寄存器数据
这里有以下两个API函数。
(1)spi_read
(直接读)
成功时:返回接收的字节数(通常是 len)。失败时:返回负的错误代码。
int spi_read(struct spi_device *spi, void *buf, unsigned len);
/*
spi:指向 SPI 设备的指针。
buf:指向存放接收数据的缓冲区的指针。接收到的数据将存放在这个缓冲区中。
len:要接收的数据的字节数。
*/
(2)spi_write_then_read
(先写后读)
这个函数用于 SPI 设备的 “write then read” 操作,即先发送数据,然后读取数据。成功时:返回传输的字节数,通常等于 n_tx 或 n_rx,这取决于设备的特性。失败时:返回负的错误代码(如 -EINVAL 或 -EIO 等)。
int spi_write_then_read(struct spi_device *spi, const void *txbuf, unsigned n_tx, void *rxbuf, unsigned n_rx);
/*
spi:指向 SPI 设备的指针,表示我们将要操作的 SPI 设备。
txbuf:指向要发送的缓冲区的指针。它包含我们要发送的数据。
n_tx:要发送的字节数。
rxbuf:指向接收数据的缓冲区的指针。读取到的数据会存放在这个缓冲区中。
n_rx:要接收的字节数。
*/
●示例: mcp2515读寄存器的值,我们查看其手册发现,要想读寄存器的值需要先写入0x03命令,然后发送要读取寄存器的地址,再进行读取值。所以我们需要先写后读。
#include <linux/spi/spi.h>
struct spi_device *spi;
char mcp2515_read_reg(char reg)
{
int ret;
u8 write_buf[] = { 0x03, reg }; // 0x03 是读取寄存器命令,具体命令值请参考 MCP2515 数据手册
u8 read_buf; // 用于存储读取到的数据
// SPI write then read 操作
ret = spi_write_then_read(spi, write_buf, sizeof(write_buf), &read_buf, sizeof(read_buf));
if (ret < 0) {
printk(KERN_ERR "SPI write then read error: %d\n", ret);
return ret; // 返回错误代码
}
printk(KERN_INFO "Read value from MCP2515 register 0x%02X: 0x%02X\n", reg, read_buf);
return read_buf; // 返回读取到的寄存器值
}
(3)复位操作(进入配置模式)
通过查看器件手册来获取复位命令,当复位器件后,器件会进入配置模式(具体看器件手册,每个器件不一样,这里只针对MCP2515)。
struct spi_device *spi;
static int mcp2515_reset()
{
int ret;
u8 write_buf[] = { 0xc0 }; // 假设 0xc0 是复位命令,具体命令根据 datasheet 确定
// 向 MCP2515 设备写入复位命令
ret = spi_write(spi, write_buf, sizeof(write_buf));
if (ret < 0) {
printk(KERN_ERR "SPI write error: %d\n", ret);
return ret;
}
printk(KERN_INFO "MCP2515 reset command sent successfully.\n");
return 0;
}
如何来查看我们设置的复位模式是否成功呢?可查看器件的状态寄存器(只读,地址0x0e),使用上面的读命令来获取该寄存器的值,从而来判断是否配置成功。
(4)配置波特率
MCP2515发送SPI复位指令之后就会进入配置模式下,只有在配置模式才可以修改其他寄存器来配置功能。
CAN总线上的所有器件都必须使用相同的波特率,而CNF1、CNF2和CNF3三个寄存器就是用来配置CAN总线波特率的,更具体的说明为CNF1、CNF2和CNF3三个寄存器通过对位时间各个段的配置,进而设置CAN总线的波特率。对于位时间的具体讲解,请查看CAN总线驱动这篇文章。
每个 CAN 报文的二进制数据都通过这些位时间段进行传输。位时间的配置对于 CAN 总线的正确通信至关重要,它影响总线的传输速率、同步和误差处理能力,TQ是每个段持续时间的最小单位。对于mcp2515手册上给出了TQ的计算公式,如下:
其中FOSC为MCP2515外设晶振频率8MHZ(每秒钟有 8,000,000 次周期),BRP为cnf1寄存器设置的预分频系数,可以任意设置,确保最后的TQ为整数即可,这里设置为0x01,然后对TQ进行计算(2×2/8000000)得到TQ=500ns,这时候已经可以确定最小单位TQ的值为500ns。
根据下面的公式,假设需要设置CAN的波特率为125k,则位时间为:1/125000=8000ns。则总的位时间所占8000/500=16个TQ。所以我们需要配置CNF1、CNF2和CNF3三个寄存器来设置相应位时间段的TQ数量,从而来配置为指定的CAN波特率值。
而在MCP2515的数据手册中要求时间段的设定必须满足以下要求:
(1)传播段 + 相位缓冲段 PS1 >= 相位缓冲段 PS2
(2)传播段 + 相位缓冲段 PS1 >= TDELAY (TDELAY 典型值为 1-2 TQ)
(3)相位缓冲段 PS2 > 同步跳转宽度 SJW(SJW 最大值为 4 TQ,一般取1 TQ即可满足要求)
最终确定各个段的值位:同步段1个TQ,传播段2个TQ,相位缓冲段PS1 7个TQ,相位缓冲段PS2 6个TQ,然后来根据各个段的值确定CNF1、CNF2、CNF3的取值。
寄存器 | 寄存器地址 | 设置值 |
---|---|---|
CNF1 | 0x2a | 0x01 |
CNF2 | 0x29 | 0xb1 |
CNF3 | 0x28 | 0x05 |
原文地址:https://blog.csdn.net/qq_48361010/article/details/142879766
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!