文章介绍

本文主要是基于 T507 Android 10 Linux 4.9 的源代码,对 Linux W1 总线框架的分析记录,便于了解整个框架的基本实现机制。

驱动框架

这张图基本上将内部的各个源文件相互关系表述出来了。

在这里插入图片描述

简析说明

master

linux-4.9/drivers/w1/masters/

  • 对应的是 SOC 内部具体的 W1 控制器,也可以是通过 GPIO 模拟,还可以是外部 W1 转换芯片,如 DS2490 USB 转单总线芯片。

  • 实现具体的硬件控制器数据发送接收,通过实现 struct w1_bus_master 里面的回调接口,通过 w1_init.h->w1_add_master_device() 提供给 core 层使用。

  • 最简单的一个 master ,只需实现 read_bit()\write_bit() 两个接口即可,core 层的 w1_io.c 发现其它接口没有实现,会通过这两个接口模拟读写时序。

  • 可以参考 drivers/w1/masters/w1-gpio.c 来实现自己的 master 驱动程序。

// linux-4.9/drivers/w1/w1.h
struct w1_bus_master
{
	void *data;
	u8 (*read_bit)(void *data); 
	void (*write_bit)(void *data, u8 bit); 
	u8 (*touch_bit)(void *data, u8 bit); 
	u8 (*read_byte)(void *data);
	void (*write_byte)(void *data, u8 byte);
	u8 (*read_block)(void *data, u8 *buf, int len);
	void (*write_block)(void *data, const u8 *buf, int len);
	u8 (*triplet)(void *data, u8 dbit);	
	u8 (*reset_bus)(void *data);
	u8 (*set_pullup)(void *data, int delay);
	void (*search)(void *, struct w1_master *, u8, w1_slave_found_callback); 
};

core

linux-4.9/drivers/w1/

  • 与具体硬件无关的功能逻辑实现,目的是将硬件控制器与外设解耦,使实现 master 还是 slave ,都变得极其简单。
  • 会为每个 master 创建内核线程,周期性发起 0xF0 搜索设备指令,通过读取到的 family id 来创建 slave,关联 master。

linux-4.9/drivers/w1/w1_init.c
根据 bus_master 添加删除来创建和销毁 master,并通过链表维护,为每个 master 创建内核线程用于探测设备和关联 slave 。

linux-4.9/drivers/w1/w1.c
内核线程的具体实现,实现周期性探测设备和关联 slave,通过 slist 链表来维护所有的 slave,便于被用户态使用,为 master 创建 sys 接口实现可动态配置。

linux-4.9/drivers/w1/w1_io.c
提供数据读写接口给 w1.c 使用,当某些接口并未被 master 实现时,会基于 master 基础的 read_bit()/write_bit() 接口通过模拟实现。

linux-4.9/drivers/w1/w1_family.c
当一个 family 驱动通过 w1_register_family() 注册进来,就会被加入到 w1_families 链表内,用于探测设备时匹配关联,从而得到一个 slave。

linux-4.9/drivers/w1/w1_netlink.c
基于 netlink 实现与用户态交互,可参考:https://github.com/bioothod/w1/blob/master/w1d.c。

变量配置 功能说明
w1_search_cout 指定搜索设备的次数,即内核线程搜索设备的次数。
w1_enable_pullup 是否启用内部上拉电阻。
w1_timeout、w1_timeout_us 每一次搜索设备的时间间隔。
w1_max_slave_count 每次搜索设备的数量。
w1_max_slave_ttl 设备生存时间(时间间隔 x TTL),当设备超过多少 ttl 次没有响应则认为设备离线。

slave

linux-4.9/drivers/w1/slaves/

  • 具体的外设器件,不同的外设可能会有不同指令,这些都是与外设强关联的,如 DS18B20 温度传感器。
  • 主要填充 struct w1_family 结构体的 fid 和 fops,前者用于适配 family id,后者用于拿到 slave 对象用于操作外设。
  • 最简单的一个 slave 只需要设置 fid 成员,并通过 w1_family.c->w1_register_family() 注册到 core 层即可,core 层会有一个默认的 fops 可用。
  • 可以参考 drivers/w1/slaves/w1_bq27000.c 或者 drivers/w1/slaves/w1_ds2780.c 实现 slave 设备驱动程序。
// linux-4.9/drivers/w1/w1.h
struct w1_family
{
	struct list_head family_entry;
	u8 fid;
	struct w1_family_ops *fops;
	atomic_t refcnt;
};

基本用法

内核配置

使用 GPIO 模拟 W1,因此选择 GPIO 1-wire busmaster。

make ARCH=arm64 menuconfig
	Device Drivers  --->
		<*> Connector - unified userspace <-> kernelspace linker  --->
			[*]   Report process events to userspace (NEW)
		<*> Dallas's 1-wire support  --->
			[*]   Userspace communication over connector (NEW)
			1-wire Bus Masters  --->
				<*> GPIO 1-wire busmaster

全志平台可以在 sys_config.fex 增加如下配置,其它平台修改对应的 dts 即可(可参考 Documentation\devicetree\bindings\w1\w1-gpio.txt)。

[w1_master]
compatible = "w1-gpio"
gpios = port:PH8<1><default><default><default>

设备驱动

  • 这是最简单的设备驱动,主要填充 struct w1_family 的两个 .fid 和 .fops 成员。
  • .fid 适配的是 family 不同的设备的 family 不一样,多个设备可以挂接同一条 w1 总线上。
  • .fops 指定当总线驱动检测到有新设备,或设备不在线时的回调处理,会传入一个 struct w1_slave。
  • struct w1_family_ops 的 .groups 可以指定 sys 接口属性,会在适配到 family 的时候由 core 创建。
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/types.h>
#include <linux/mutex.h>

#include "../w1.h"
#include "../w1_int.h"
#include "../w1_family.h"

static int w1_test_add_slave(struct w1_slave *sl)
{
	printk("w1_test_add_slave.......\n");
    mutex_lock(&sl->master->bus_mutex);
	if (w1_reset_select_slave(sl) == 0) {
		w1_write_block(sl->master, ...);
		w1_read_block(sl->master, ...);
	}
	mutex_unlock(&sl->master->bus_mutex);
    
	return 0;
}

static void w1_test_remove_slave(struct w1_slave *sl)
{
	printk("w1_test_remove_slave.......\n");
}

static struct w1_family_ops w1_test_fops = {
	.add_slave	= w1_test_add_slave,
	.remove_slave	= w1_test_remove_slave,
};

static struct w1_family w1_test_family = {
	.fid = 1,
	.fops = &w1_test_fops,
};

static int __init w1_test_init(void)
{
	return w1_register_family(&w1_test_family);
}

static void __exit w1_test_exit(void)
{
	w1_unregister_family(&w1_test_family);
}

module_init(w1_test_init);
module_exit(w1_test_exit);
MODULE_LICENSE("GPL");

应用程序

基于 Netlink
W1 的驱动框架中已经基于 Netlink Connector 实现了与用户空间交互的命令接口,可以参考 https://github.com/bioothod/w1/blob/master/w1d.c。

基于 Sys 节点

  • 也可以在自己的 slave 驱动中通过指定 struct w1_family.fops.groups,会在匹配成功后由 core 自动创建文件。
  • 如果检测到的 family 没有被 w1_register_family(),会有一个默认的 w1_default_family 指定二进制读写属性。
    可以通过二进制读写的方式来控制总线发出读写指令:/sys/bus/w1/devices/{family}-{serial}/rw
// linux-4.9/drivers/w1/w1.c
int w1_attach_slave_device(struct w1_master *dev, struct w1_reg_num *rn)
{
	......
	spin_lock(&w1_flock);
	f = w1_family_registered(rn->family);
	if (!f) {
		f= &w1_default_family;
		dev_info(&dev->dev, "Family %x for %02x.%012llx.%02x is not registered.\n",
			  rn->family, rn->family,
			  (unsigned long long)rn->id, rn->crc);
	}
	__w1_family_get(f);
	spin_unlock(&w1_flock);
	......
	return 0;
}

高效用法

在一些比较特殊的应用场景,例如需要读取 TM DS1990A 电子钥匙的场景。不像 DS18B20 器件会直接焊接在主板上,电子钥匙只在需要开锁的时候, 触碰探头来连接 gnd 和 data,才能实现数据通信。但何时开锁具有不确定性,且又需要相对较高的实时性,可以通过以下方式绕开原有的检测机制,直接控制总线读写。

导出接口

diff --git a/longan/kernel/linux-4.9/drivers/w1/w1.c b/longan/kernel/linux-4.9/drivers/w1/w1.c
--- a/longan/kernel/linux-4.9/drivers/w1/w1.c
+++ b/longan/kernel/linux-4.9/drivers/w1/w1.c
@@ -840,6 +840,7 @@ struct w1_master *w1_search_master_id(u32 id)
 
        return (found)?dev:NULL;
 }
+EXPORT_SYMBOL(w1_search_master_id);
 
 struct w1_slave *w1_search_slave(struct w1_reg_num *id)
 {

使用方法

// 通过 id 找到对应的 master , 并停止周期性搜索设备
static struct w1_master *get_master(int w1_master_id)
{
    struct w1_master *master;
    if((master = w1_search_master_id(1))){
        mutex_lock(&master->mutex);
        master->search_count = 0;
        mutex_unlock(&master->mutex);  
        return master;
    }
    return NULL;
}

// 通过指定的 master 发出读写指令
static unsigned long long tm_ds1990a_read_serial_number(struct w1_master *master)
{
	u64 rn_le = 0x00, data = 0x00;
	struct w1_reg_num *tmp = NULL;
	unsigned long long id = 0x00;
	
	mutex_lock(&master->bus_mutex);
	w1_reset_bus(master);
	w1_write_8(master, W1_READ_ROM);
	if(w1_read_block(master, (u8 *)&data, sizeof(data)) == sizeof(data)){
		rn_le = cpu_to_le64(data);
		tmp = (struct w1_reg_num *)&data;
		if(data && tmp->crc == w1_calc_crc8((u8 *)&rn_le, sizeof(data) - 1)){
			id = (unsigned long long)tmp->id;
		}
	}
	mutex_unlock(&master->bus_mutex);
	return id;
}