I have a custom device that can be connected to various SoCs using two SPIs and a couple of GPIO wires (one of them functioning as an interrupt request wire). It would be acceptable to expect both SPIs to be connected to the same bus/controller. The operation of the device calls for a single Linux kernel device driver to control both SPI (sub)devices.
Which possibilities are there to achieve this and which one is recommended? I'm interested in how the device tree layout could look like and which kernel APIs could be used to not make this look like a dirty hack.
First variant:
Have two entries in the controller's device tree node like (let's call the device "bar")
...
bar_a@0 {
compatible = "foo,bar-function-a-1.0";
id = "primary";
...
}
bar_b@1 {
compatible = "foo,bar-function-b-1.0";
id = "primary";
...
}
and to have the driver match with both foo,bar-function-[ab]-1.0
, collecting and registering spi_device
structures along with their associated id
in the probe()
function. If the one just encountered is the second one for a given id
(both have been found), the probe()
function continues.
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);