Add support for MCP39XX in Linux kernel

Posted by Marcus Folkesson on Friday, August 4, 2023

Add support for MCP39XX in Linux kernel

I've maintained the MCP3911 driver in the Linux kernel for some time and continuously add support for new features [1] upon requests from people and companies.

Microchip has several IC:s in this series of ADC:s that works similar to MCP3911. Actually, all other IC:s are register compatible but MCP3911. The IC:s I've extended support for is MCP3910, MCP3912, MCP3913, MCP3914, MCP3918 and MCP3919.

The main difference between these IC:s from the driver perspective is the number of channels ranging from 1 to 8 channels and that the register map is not the same for all devices.

/media/mcp39xx.png

Implementation

This is a rather small patch without any fanciness, but just to show how to do this without the macro-magic you find in Zephyr [2].

Add compatible strings

The Linux driver infrastructure binds a certain device to a driver by a string (or other unique identifies such as VID/PID for USB). When, for example, the compatible property of a device tree node matches a device driver, a device is instantiated and the probe function is called.

As a single device driver could handle multiple similar IC:s where some of their properties may differ, we have to differentiate those somehow. This is done by provide device specific data to each instance of the device. This data is called "driver data" or "private data" and is part of the device lookup table.

E.g. the driver_data field of the struct spi_device_id:

1struct spi_device_id {
2	char name[SPI_NAME_SIZE];
3	kernel_ulong_t driver_data;	/* Data private to the driver */
4};

Or the data field of the struct of_device_id:

1/*
2 * Struct used for matching a device
3 */
4struct of_device_id {
5	char	name[32];
6	char	type[32];
7	char	compatible[128];
8	const void *data;
9};

For this driver, the driver data to these ID tables looks as follows:

 1static const struct of_device_id mcp3911_dt_ids[] = {
 2-       { .compatible = "microchip,mcp3911" },
 3+       { .compatible = "microchip,mcp3910", .data = &mcp3911_chip_info[MCP3910] },
 4+       { .compatible = "microchip,mcp3911", .data = &mcp3911_chip_info[MCP3911] },
 5+       { .compatible = "microchip,mcp3912", .data = &mcp3911_chip_info[MCP3912] },
 6+       { .compatible = "microchip,mcp3913", .data = &mcp3911_chip_info[MCP3913] },
 7+       { .compatible = "microchip,mcp3914", .data = &mcp3911_chip_info[MCP3914] },
 8+       { .compatible = "microchip,mcp3918", .data = &mcp3911_chip_info[MCP3918] },
 9+       { .compatible = "microchip,mcp3919", .data = &mcp3911_chip_info[MCP3919] },
10    { }
11};
12MODULE_DEVICE_TABLE(of, mcp3911_dt_ids);
13
14static const struct spi_device_id mcp3911_id[] = {
15-       { "mcp3911", 0 },
16+       { "mcp3910", (kernel_ulong_t)&mcp3911_chip_info[MCP3910] },
17+       { "mcp3911", (kernel_ulong_t)&mcp3911_chip_info[MCP3911] },
18+       { "mcp3912", (kernel_ulong_t)&mcp3911_chip_info[MCP3912] },
19+       { "mcp3913", (kernel_ulong_t)&mcp3911_chip_info[MCP3913] },
20+       { "mcp3914", (kernel_ulong_t)&mcp3911_chip_info[MCP3914] },
21+       { "mcp3918", (kernel_ulong_t)&mcp3911_chip_info[MCP3918] },
22+       { "mcp3919", (kernel_ulong_t)&mcp3911_chip_info[MCP3919] },
23    { }

The driver data is then reachable in the probe function via spi_get_device_match_data():

1    adc->chip = spi_get_device_match_data(spi);

Driver data

The driver data is used to distinguish between different devices and provide enough information to make it possible for the driver to handle all differences between the IC:s in a common way.

The driver data for these devices looks as follows:

 1+struct mcp3911_chip_info {
 2+       const struct iio_chan_spec *channels;
 3+       unsigned int num_channels;
 4+
 5+       int (*config)(struct mcp3911 *adc);
 6+       int (*get_osr)(struct mcp3911 *adc, int *val);
 7+       int (*set_osr)(struct mcp3911 *adc, int val);
 8+       int (*get_offset)(struct mcp3911 *adc, int channel, int *val);
 9+       int (*set_offset)(struct mcp3911 *adc, int channel, int val);
10+       int (*set_scale)(struct mcp3911 *adc, int channel, int val);
11+};
12+

Description of the structure members:

  • .channels is a a pointer to struct iio_chan_spec where all ADC and timestamp channels are specified.
  • .num_channels is the number of channels
  • .config is a function pointer to configure the device
  • .get_* and .set_* is function pointers used to get/set certain registers

A struct mcp3911_chip_info is created for each type of supported IC:

 1+static const struct mcp3911_chip_info mcp3911_chip_info[] = {
 2+       [MCP3910] = {
 3+               .channels = mcp3910_channels,
 4+               .num_channels = ARRAY_SIZE(mcp3910_channels),
 5+               .config = mcp3910_config,
 6+               .get_osr = mcp3910_get_osr,
 7+               .set_osr = mcp3910_set_osr,
 8+               .get_offset = mcp3910_get_offset,
 9+               .set_offset = mcp3910_set_offset,
10+               .set_scale = mcp3910_set_scale,
11+       },
12+       [MCP3911] = {
13+               .channels = mcp3911_channels,
14+               .num_channels = ARRAY_SIZE(mcp3911_channels),
15+               .config = mcp3911_config,
16+               .get_osr = mcp3911_get_osr,
17+               .set_osr = mcp3911_set_osr,
18+               .get_offset = mcp3911_get_offset,
19+               .set_offset = mcp3911_set_offset,
20+               .set_scale = mcp3911_set_scale,
21+       },
22+       [MCP3912] = {
23+               .channels = mcp3912_channels,
24+               .num_channels = ARRAY_SIZE(mcp3912_channels),
25+               .config = mcp3910_config,
26+               .get_osr = mcp3910_get_osr,
27+               .set_osr = mcp3910_set_osr,
28+               .get_offset = mcp3910_get_offset,
29+               .set_offset = mcp3910_set_offset,
30+               .set_scale = mcp3910_set_scale,
31+       },
32+       [MCP3913] = {
33+               .channels = mcp3913_channels,
34+               .num_channels = ARRAY_SIZE(mcp3913_channels),
35+               .config = mcp3910_config,
36+               .get_osr = mcp3910_get_osr,
37+               .set_osr = mcp3910_set_osr,
38+               .get_offset = mcp3910_get_offset,
39+               .set_offset = mcp3910_set_offset,
40+               .set_scale = mcp3910_set_scale,
41+       },
42+       [MCP3914] = {
43+               .channels = mcp3914_channels,
44+               .num_channels = ARRAY_SIZE(mcp3914_channels),
45+               .config = mcp3910_config,
46+               .get_osr = mcp3910_get_osr,
47+               .set_osr = mcp3910_set_osr,
48+               .get_offset = mcp3910_get_offset,
49+               .set_offset = mcp3910_set_offset,
50+               .set_scale = mcp3910_set_scale,
51+       },
52+       [MCP3918] = {
53+               .channels = mcp3918_channels,
54+               .num_channels = ARRAY_SIZE(mcp3918_channels),
55+               .config = mcp3910_config,
56+               .get_osr = mcp3910_get_osr,
57+               .set_osr = mcp3910_set_osr,
58+               .get_offset = mcp3910_get_offset,
59+               .set_offset = mcp3910_set_offset,
60+               .set_scale = mcp3910_set_scale,
61+       },
62+       [MCP3919] = {
63+               .channels = mcp3919_channels,
64+               .num_channels = ARRAY_SIZE(mcp3919_channels),
65+               .config = mcp3910_config,
66+               .get_osr = mcp3910_get_osr,
67+               .set_osr = mcp3910_set_osr,
68+               .get_offset = mcp3910_get_offset,
69+               .set_offset = mcp3910_set_offset,
70+               .set_scale = mcp3910_set_scale,
71+       },
72+};

Thanks to this, all differences between the IC:s is in one place and the driver code is common for all devices. See the code below how oversampling ration is set. The differences between IC:s is handled by the callback function:

 1        case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
 2                for (int i = 0; i < ARRAY_SIZE(mcp3911_osr_table); i++) {
 3                        if (val == mcp3911_osr_table[i]) {
 4-                               val = FIELD_PREP(MCP3911_CONFIG_OSR, i);
 5-                               ret = mcp3911_update(adc, MCP3911_REG_CONFIG, MCP3911_CONFIG_OSR,
 6-                                               val, 2);
 7+                               ret = adc->chip->set_osr(adc, i);
 8                                break;
 9                        }
10                }