If the custom device was using two I2C devices instead of two SPI devices (we will get to those later), the custom device could be a "platform" device and its devicetree node could have a property that refers to the nodes of the I2C client devices (that do not have their own driver) that it wants to consume. The platform driver for the custom device could then use the of_parse_phandle(), of_node_put(), and of_find_i2c_device_by_node() functions in its "probe" handler to retrieve pointers to struct i2c_device objects. (For Linux kernel v6.4 onwards, it could instead use fwnode_find_reference(), fwnode_handle_put(), and i2c_find_device_by_fwnode() which are more bus-agnostic than the of_ functions.)
In its "remove" handler, it can call put_device() to release each I2C device. Note that if of_find_i2c_device_by_node() (or i2c_find_device_by_fwnode()) returns NULL it is probably because the I2C device is not ready to be probed yet. In that case the custom device's "probe" handler should clean up and return -EPROBE_DEFER so that the custom device's "probe" handler can be called again later.
That's all well and good for using I2C devices, but we want to use SPI devices, and there is no equivalent of_find_spi_device_by_node() or spi_find_device_by_fwnode() function that we can use. But fear not, the building blocks for creating our own of_find_spi_device_by_node() (that I will call my_of_find_spi_device_by_node()) and/or our own spi_find_device_by_fwnode() (that I will call my_spi_find_device_by_fwnode()) are there. We want to give it a pointer to the struct device_node (or struct fwnode_handle) associated with the SPI device that we have found using of_parse_phandle() (or fwnode_find_property()), and we want it to return a pointer to the corresponding struct spi_device, or NULL if the device has not been probed yet.
There follows an example DTS snippet to describe the SPI devices we want to use. They need to have a "compatible" string property even though the devices will not be used by an SPI device driver. I have chosen to use the same "compatible" value for both of SPI devices, but they do not have to be the same. The platform driver for the custom device will have a struct device_node pointer for the SPI device nodes, so it can use that to check the "compatible" properties are as expected if it wants. Also, I have made them children of the same SPI controller (&spi0) in this example, but they could be on separate controllers. Their devicetree nodes need labels that can be referenced by the node for the custom device.
&spi0 {
status = "okay";
bar_a: spi@0 {
compatible = "vendor,foo-bar-1.0";
reg = <0>;
spi-max-frequency = <1000000>;
};
bar_b: spi@1 {
compatible = "vendor,foo-bar-1.0";
reg = <1>;
spi-max-frequency = <1000000>;
};
};
For the devicetree node for the custom platform device, I have chosen to use a property called spis storing an array of phandle values for the SPI device nodes, but it would also be possible to use separate properties for each SPI device node phandle if desired. I have created the custom device node in the root of the devicetree, but it can be placed somewhere else in the devicetree if necessary.
/ {
foo {
compatible = "vendor,foo-1.0";
spis = <&bar_a &bar_b>;
};
};
The following code is for an example kernel mode driver for the custom device. It does not do very much apart from retrieving pointers to the SPI devices during "probe" and releasing them during "remove". It incorporates the my_of_find_spi_device_by_node() and my_spi_find_device_by_fwnode() functions mentioned earlier to get the pointers to the SPI devices, and calls spi_dev_put() to release them.
This version should work on Linux kernel v4.12 or later.
/* SPDX-Licence-Identifier: GPL-2.0+ */
#include <linux/version.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/err.h>
#include <linux/errno.h>
struct foo_private {
struct device *hwdev;
struct spi_device *bar[2];
};
#ifdef CONFIG_OF
static const struct of_device_id foo_of_ids[] = {
{
.compatible = "vendor,foo-1.0"
},
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, foo_of_ids);
#endif
/* Must call spi_dev_put() when done with returned spi_device */
static struct spi_device *
my_spi_find_device_by_fwnode(struct fwnode_handle *fwnode)
{
struct device *dev;
if (!fwnode) {
return NULL;
}
dev = bus_find_device_by_fwnode(&spi_bus_type, fwnode);
if (!dev) {
return NULL;
}
return to_spi_device(dev);
}
/* Must call spi_dev_put() when done with returned spi_device */
static struct spi_device *
my_of_find_spi_device_by_node(struct device_node *node)
{
return my_spi_find_device_by_fwnode(of_fwnode_handle(node));
}
static int foo_platform_probe(struct platform_device *pdev)
{
struct device *hwdev = &pdev->dev;
struct foo_private *foo;
struct device_node *np = hwdev->of_node;
unsigned int i;
int rc;
foo = kzalloc(sizeof(*foo), GFP_KERNEL);
if (!foo) {
dev_err(hwdev, "failed to allocate private data\n");
rc = -ENOMEM;
goto fail_alloc_private;
}
foo->hwdev = hwdev;
dev_set_drvdata(hwdev, foo);
/* Look for our SPI devices. */
for (i = 0; i < 2; i++) {
struct device_node *spinode =
of_parse_phandle(np, "spis", i);
if (!spinode) {
dev_err(hwdev, "missing spi phandle[%u]\n", i);
rc = -ENOENT;
goto fail_find_spi;
}
foo->bar[i] = my_of_find_spi_device_by_node(spinode);
of_node_put(spinode);
if (!foo->bar[i]) {
/* SPI device not ready yet. Try again later. */
rc = -EPROBE_DEFER;
goto fail_find_spi;
}
}
dev_info(hwdev, "probed OK\n");
rc = 0;
goto out;
fail_find_spi:
for (i = 0; i < 2; i++) {
spi_dev_put(foo->bar[i]);
}
kfree(foo);
fail_alloc_private:
out:
return rc;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(6,11,0)
typedef int remove_ret_t;
#define REMOVE_RETURN return 0
#else
typedef void remove_ret_t;
#define REMOVE_RETURN return
#endif
static remove_ret_t foo_platform_remove(struct platform_device *pdev)
{
struct foo_private *foo = dev_get_drvdata(&pdev->dev);
unsigned int i;
for (i = 0; i < 2; i++) {
spi_dev_put(foo->bar[i]);
}
kfree(foo);
REMOVE_RETURN;
}
static struct platform_driver foo_platform_driver = {
.probe = foo_platform_probe,
.remove = foo_platform_remove,
.driver = {
.name = "foo",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(foo_of_ids),
},
};
/* Module info. */
MODULE_LICENSE("GPL");
module_platform_driver(foo_platform_driver);
For Linux kernel v5.3 or later, we can make the code less OF-specific by making use of the fwnode_find_reference() function. The my_of_find_spi_device_by_node() function has been removed because it nothing is using it in this version. The my_spi_find_device_by_fwnode() function is called instead. The only OF-specific code in this version is the module device table.
/* SPDX-Licence-Identifier: GPL-2.0+ */
#include <linux/version.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/err.h>
#include <linux/errno.h>
struct foo_private {
struct device *hwdev;
struct spi_device *bar[2];
};
#ifdef CONFIG_OF
static const struct of_device_id foo_of_ids[] = {
{
.compatible = "vendor,foo-1.0"
},
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, foo_of_ids);
#endif
/* Must call spi_dev_put() when done with returned spi_device */
static struct spi_device *
my_spi_find_device_by_fwnode(struct fwnode_handle *fwnode)
{
struct device *dev;
if (!fwnode) {
return NULL;
}
dev = bus_find_device_by_fwnode(&spi_bus_type, fwnode);
if (!dev) {
return NULL;
}
return to_spi_device(dev);
}
static int foo_platform_probe(struct platform_device *pdev)
{
struct device *hwdev = &pdev->dev;
struct foo_private *foo;
struct fwnode_handle *np = dev_fwnode(hwdev);
unsigned int i;
int rc;
foo = kzalloc(sizeof(*foo), GFP_KERNEL);
if (!foo) {
dev_err(hwdev, "failed to allocate private data\n");
rc = -ENOMEM;
goto fail_alloc_private;
}
foo->hwdev = hwdev;
dev_set_drvdata(hwdev, foo);
/* Look for our SPI devices. */
for (i = 0; i < 2; i++) {
struct fwnode_handle *spinode =
fwnode_find_reference(np, "spis", i);
if (IS_ERR(spinode)) {
dev_err(hwdev, "missing spi fwnode[%u]\n", i);
rc = PTR_ERR(spinode);
goto fail_find_spi;
}
foo->bar[i] = my_spi_find_device_by_fwnode(spinode);
fwnode_handle_put(spinode);
if (!foo->bar[i]) {
/* SPI device not ready yet. Try again later. */
rc = -EPROBE_DEFER;
goto fail_find_spi;
}
}
dev_info(hwdev, "probed OK\n");
rc = 0;
goto out;
fail_find_spi:
for (i = 0; i < 2; i++) {
spi_dev_put(foo->bar[i]);
}
kfree(foo);
fail_alloc_private:
out:
return rc;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(6,11,0)
typedef int remove_ret_t;
#define REMOVE_RETURN return 0
#else
typedef void remove_ret_t;
#define REMOVE_RETURN return
#endif
static remove_ret_t foo_platform_remove(struct platform_device *pdev)
{
struct foo_private *foo = dev_get_drvdata(&pdev->dev);
unsigned int i;
for (i = 0; i < 2; i++) {
spi_dev_put(foo->bar[i]);
}
kfree(foo);
REMOVE_RETURN;
}
static struct platform_driver foo_platform_driver = {
.probe = foo_platform_probe,
.remove = foo_platform_remove,
.driver = {
.name = "foo",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(foo_of_ids),
},
};
/* Module info. */
MODULE_LICENSE("GPL");
module_platform_driver(foo_platform_driver);
<name>@<n>wherenis the corresponding CS), having two of them be controlled by the same driver instance does not come naturally, hence the question.