目录
一. 设备树
1.1 驱动的设备树匹配
1.2 驱动的Probe函数
1.3 驱动的Remove函数
1.4 驱动结构体
二. 老版字符设备驱动框架
2.1 文件操作函数的实现
三. 新字符设备驱动框架
3.1 设备模型
3.2 设备树
3.3 kobject 和 sysfs
3.4 fasync 和 poll
3.5 file_operations的改进
四. plateform驱动框架
五. SPI驱动框架和编写流程
5.1 定义服务接口
5.2 实现服务提供者
5.3 配置服务提供者
5.4 加载服务
5.5 部署和测试
六. SPI 总线接口
七. IIC总线接口
八. UART
把这个词分开其实就是"设备"和"树", 树的主干就是系统总线,分支则是IIC,SPI等各硬件。而之前没有设备树的时候,linux是如何描述板级信息的?
通过硬编码在内核源代码中描述板级信息,直接写在内核源码中。
而dts用于描述板级信息,如设备节点,属性及配置;dtsi用于描述SOC信息,
设备树的语法结构:
主要的语法元素包含:节点和属性
节点的定义:
每个节点以一个标签和花括号开始,如
cpus {
cpu@0 {
compatible = "arm,cortex-a53";
reg = <0>;
clock-frequency = <1200000000>; /* 1.2 GHz */
};
cpu@1 {
compatible = "arm,cortex-a53";
reg = <1>;
clock-frequency = <1200000000>; /* 1.2 GHz */
};
};
在这个例子中,cpus
是一个节点,cpu@0
和 cpu@1
是它的子节点
设备树的标准属性:
compatible
指定设备的兼容性字符串,帮助操作系统或驱动程序确定如何处理该设备。
reg
描述设备寄存器的地址和大小,通常用于内存映射的设备。
interrupts
定义设备的中断信息,包括中断号和中断触发方式。
那么我们驱动到底是怎么使用设备树的呢?
那么我们驱动到底是怎么使用设备树的呢?
那么我们驱动到底是怎么使用设备树的呢?
比如,我们有在设备树中有下面的定义:
led_device: led@0
{
compatible = "my_led";
reg = <0x0 0x0 0x4>
led-color = "red"
}
驱动需要在'of_match_table'中指定设备树节点的兼容性信息,以便内核可以将设备树中的节点与驱动匹配起来
#include
#include
#include
#include
struct led_device {
void __iomem *base_addr;
const char *color;
};
static const struct of_device_id led_of_match[] = {
{ .compatible = "my_led", },
{ /* sentinel */ }
};
// 一个用于定义设备树的匹配表,以便内核可以根据设备树中的信息识别和加载驱动
MODULE_DEVICE_TABLE(of, led_of_match);
在'probe'函数中,驱动通过设备树API获取设备的配置信息。
static int led_probe(struct platform_device *pdev)
{
//定义一个指向'led_device'结构体的指针,用于保存LED设备的相关信息
struct led_device *led;
//用于获取设备的内存资源
struct resource *res;
//从 platform_device 结构体中获取设备树节点信息,用于访问设备树中定义的属性。
struct device_node *node = pdev->dev.of_node;
// 分配内存给 led_device 结构体,并初始化为零。devm_kzalloc 函数确保在设备释放时自动释放内存。
led = devm_kzalloc(&pdev->dev, sizeof(*led), GFP_KERNEL);
// 检查内存分配是否成功。如果失败,返回 -ENOMEM 表示内存不足。
if (!led)
return -ENOMEM;
// 获取寄存器的资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENODEV;
// 映射寄存器地址
led->base_addr = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(led->base_addr))
return PTR_ERR(led->base_addr);
// 从设备树中获取 LED 颜色信息
if (of_property_read_string(node, "led-color", &led->color)) {
dev_err(&pdev->dev, "Failed to get led-color from device tree\n");
return -EINVAL;
}
dev_info(&pdev->dev, "LED color is %s\n", led->color);
// 其他初始化代码...
platform_set_drvdata(pdev, led);
return 0;
}
在'remove'函数中,你可以执行清理操作,例如取消映射的内存
static int led_remove(struct platform_device *pdev)
{
struct led_device *led = platform_get_drvdata(pdev);
// 释放资源等操作...
return 0;
}
最后,将'probe'和'remove'函数与设备树匹配表一起定义,并注册驱动
static struct platform_driver led_driver = {
// 指定当平台设备匹配驱动时调用的'probe'函数,在这里它指向'ledn_porbe'函数
// 该函数负责初始化和配置设备
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "led_driver",
.of_match_table = led_of_match,
},
};
module_platform_driver(led_driver);
MODULE_AUTHOR("Author");
MODULE_DESCRIPTION("LED Driver");
MODULE_LICENSE("GPL");
主要指的就是传统的设备驱动模型,通常包含文件操作函数如open
、read
、write
和close
。他们通过设备号和设备文件进行交互,实现数据的读写和设备控制。
static int my_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "mychardev: Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "mychardev: Device closed\n");
return 0;
}
static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *offset) {
ssize_t ret = 0;
if (*offset >= BUFFER_SIZE)
return 0;
if (len > BUFFER_SIZE - *offset)
len = BUFFER_SIZE - *offset;
if (copy_to_user(buf, device_buffer + *offset, len) != 0)
return -EFAULT;
*offset += len;
ret = len;
printk(KERN_INFO "mychardev: Read %zu bytes\n", len);
return ret;
}
static ssize_t my_write(struct file *file, const char __user *buf, size_t len, loff_t *offset) {
ssize_t ret = 0;
if (*offset >= BUFFER_SIZE)
return 0;
if (len > BUFFER_SIZE - *offset)
len = BUFFER_SIZE - *offset;
if (copy_from_user(device_buffer + *offset, buf, len) != 0)
return -EFAULT;
*offset += len;
ret = len;
printk(KERN_INFO "mychardev: Wrote %zu bytes\n", len);
return ret;
}
新添加的关键特性有:
struct device
和 struct device_driver
::新版内核引入了设备模型,提供了一种统一的方式来管理设备和驱动程序,这些结构体提供了设备和驱动程序之间的关联和通信机制
设备树用于描述硬件设备的结构,内核通过设备树来初始化和配置设备,而不是通过硬编码的方式
kobject
和 sysfs
kobject是一个内核对象,用于管理内核中的对象;sysfs是一个文件系统,通过它可以在用户空间中暴露内核对象的信息
可以通过sysfs创建和管理设备的属性,从而使用户空间能够访问和修改设备的状态
fasync
和 poll
这两个机制提供了异步事件通知的能力,可以用来处理字符设备的事件驱动操作
file_operations
的改进新版本内核在file_operations
结构体中添加了一些新的方法,如unlocked_ioctl
,支持无锁的IO控制操作。
平台驱动(Platform Driver)框架在Linux内核中是一种专门用于处理平台设备的机制。平台设备通常指的是那些直接与系统硬件紧密耦合的设备,例如嵌入式系统中的设备,或那些通过设备树(Device Tree)描述的设备。
创建一个接口或抽象类,定义服务的标准方法和功能
public interface MyService {
void execute();
}
编写一个或多个服务实现类,实现服务接口
public class MyServiceImpl implements MyService {
@Override
public void execute() {
System.out.println("Service Executed!");
}
}
META-INF/services
目录下,文件名为服务接口的完全限定名。com.example.MyServiceImpl
使用ServiceLoader
类动态加载并使用服务提供者。
ServiceLoader loader = ServiceLoader.load(MyService.class);
for (MyService service : loader) {
service.execute();
}
META-INF/services
目录中。SPI主要包括以下4条信号线:MOSI,MISO,SS/CS,SCK
通信模式主要是4种,由时钟极性和时钟相位决定
它是一种全双工,串行总线,时钟同步
主要包括两条信号线:SCL,SDA
时序主要包括:开始信号,应答信号,地址信号,停止信号
它是一种半双工,串行总线,时钟同步,但它支持多主机
起始位:标识数据传输的开始,起始位通常是逻辑低电平,在数据传输开始时,数据线会从高电平(空闲状态)变为低电平
数据位:通常是5到9位,通常是8位
奇偶校验位:用于错误检测。奇偶校验位用于检测数据传输过程中是否发生了错误。
停止位:止位的数量可以是1位、1.5位或2位,具体取决于通信协议的配置。