Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Sensor Channel Specifiers #63830

Closed
teburd opened this issue Oct 11, 2023 · 16 comments
Closed

RFC: Sensor Channel Specifiers #63830

teburd opened this issue Oct 11, 2023 · 16 comments
Assignees
Labels
area: Sensor Subsystem area: Sensors Sensors RFC Request For Comments: want input from the community

Comments

@teburd
Copy link
Collaborator

teburd commented Oct 11, 2023

Introduction

Sensor channels today are solely identified by the type of information they contain using a single enum. Many sensors provide multiple channels of the same measurement type with varying need for a sub type or index.

Example devices of varying kinds

Device Channels Channel Type Product URL
TI FDC1004 4 Capacitance https://www.ti.com/product/FDC1004
AD MAX6581 8 Temperature https://www.analog.com/en/products/max6581.html
AMS AS7261 6 Light Spectra https://ams.com/as7262
ST VL53L5CX 8x8 ToF Array https://www.st.com/en/imaging-and-photonics-solutions/vl53l5cx.html

Problem description

Selecting channels simply by their type of measurement isn't flexible enough for such array like sensor devices that exist and are used widely in the real world. Subtypes and indexes are encoded in enum suffixes.

Some examples...

SENSOR_TEMP_AMBIENT
SENSOR_TEMP_DIE
SENSOR_GYRO_X
SENSOR_GYRO_Y
SENSOR_GYRO_Z
SENSOR_GYRO_XYZ

In multi-channel array like devices custom channel defines are needed

FDC2X1X

Proposed change

Define sensor channels as a triple (type, optional subtype, optional idx)

Proposed change (Detailed)

Sensor channels become defined as a uint64_t unioned with a struct to define a structured channel identifier with a copy friendly size.

struct sensor_chan_spec {
    union {
        uint64_t spec;
        struct {
            uint16_t idx;
            uint16_t subtype;
            uint32_t type;
        };
    };
};

A pair of conversion functions are available, perhaps infallible and which assert on unknown legacy values or channel specs that cannot be converted back to a legacy enum.

struct sensor_chan_spec sensor_chan_spec_from_legacy(enum sensor_channel legacy);
enum sensor_channel sensor_chan_spec_to_legacy(struct sensor_chan_spec chan);

Some samples of how channel specs might be defined...

struct sensor_chan_spec accel_xyz_0 = {
  .idx = 0
  .subtype = SENSOR_CHAN_SUBTYPE_XYZ,
  .type = SENSOR_CHAN_TYPE_ACCEL,
};

struct sensor_chan_spec temp_die_0 = {
  .idx = 0,
  .subtype = SENSOR_CHAN_SUBTYPE_DIE,
  .type = SENSOR_CHAN_TYPE_TEMP,
};

struct sensor_chan_spec temp_ambient_4 = {
  .idx = 4,
  .subtype = SENSOR_CHAN_SUBTYPE_AMBIENT,
  .type = SENSOR_CHAN_TYPE_TEMP,
};

struct sensor_chan_spec light_spectra_0 = {
   .idx = 0,
   .subtype = SENSOR_CHAN_SUBTYPE_SPECTRA,
   .type = SENSOR_CHAN_TYPE_LIGHT,
};

All newer APIs, e.g. sensor_read and the sensor_stream API would be updated to only support the newer struct sensor_chan_spec type as arrays. It is up to the backwards compatibility layer of this new API then to translate the sensor_chan_spec to a legacy enum sensor_channel when used with existing drivers.

A new pair of attribute set/get functions to act on a chan spec rather than a channel type
would be available for drivers to implement. Some mappings from old to new or vice versa could be provided at the sensor API layer.

Introspection of available channels could optionally be provided with a Kconfig and relevant DT entries perhaps as a future improvement.

Related PRs/Issues/Drivers

@teburd teburd added the RFC Request For Comments: want input from the community label Oct 11, 2023
@teburd teburd changed the title RFC: Sensor Channel Identifiers RFC: Sensor Channel Specifiers Oct 11, 2023
@microbuilder
Copy link
Member

microbuilder commented Oct 12, 2023

If index here for the spectrometer examples is analogous to the wavelength in nm (500 nm here), a 16-bit integer should be sufficient for most use cases from UV to IR, but is there a mechanism to programmatically determine what index values are exposed by the driver? Ex: 285, 315, 360, 405, 452, 505, 580, 670, 745, 780 (random numbers) might be a sensible UV to visible to IR sensor range in nm, but how do I know what's available as a developer?

Otherwise, would a linear index 0..9 make more sense with per channel/index attributes to indicate the center wavelength in nm, potentially with more precision since it's unlikely to be a clean integer? I could then just scan the index range until I hit an exception, and probably also set a precise value like 524.6 nm via an attribute?

Sorry if the answer is obvious ... on my phone and I've been away from the sensor API for a long time.

@teburd
Copy link
Collaborator Author

teburd commented Oct 12, 2023

If index here for the spectrometer examples is analogous to the wavelength in nm (500 nm here), a 16-bit integer should be sufficient for most use cases from UV to IR, but is there a mechanism to programmatically determine what index values are exposed by the driver? Ex: 285, 315, 360, 405, 452, 505, 580, 670, 745, 780 (random numbers) might be a sensible UV to visible to IR sensor range in nm, but how do I know what's available as a developer?

Otherwise, would a linear index 0..9 make more sense with per channel/index attributes to indicate the center wavelength in nm, potentially with more precision since it's unlikely to be a clean integer? I could then just scan the index range until I hit an exception, and probably also set a precise value like 524.6 nm via an attribute?

yeah I changed it in realizing this exact issue, the index is just the channel index into the sensor channel array now. An attribute of the channel could be the wavelength.

I think some of them also let you maybe select which wavelength in some channels to use? I swore I saw one that did that. Which makes this even more dubious to try and use the index as a wavelength.

Sorry if the answer is obvious ... on my phone and I've been away from the sensor API for a long time.

No, you were spot on, and I've corrected it.

@teburd teburd moved this to Todo in Sensor Working Group Oct 23, 2023
@teburd teburd added this to the v3.6.0 milestone Oct 23, 2023
@jilaypandya
Copy link
Contributor

Hi @teburd, This is a very relevant use-case for one of the sensors i am working on. +1 from me and would be happy to collaborate on this.

@henrikbrixandersen
Copy link
Member

Hi @teburd, This is a very relevant use-case for one of the sensors i am working on. +1 from me and would be happy to collaborate on this.

Same here. As mentioned in the Sensors WG meeting yesterday, my only concern is adding a new API and adding "legacy" wrappers instead of just modifying the old API. At minimum, I think we need to have a migration plan for all in-tree drivers to use the new sensor channel specifiers.

One option could be to go with the legacy wrappers for the initial PR and create a meta issue for assigning/tracking driver conversions.

@kartben
Copy link
Collaborator

kartben commented Nov 13, 2023

Another candidate would be for particulate matter and particle count sensors, where the subtype could be used to specify the particle size, among the common values 1.0, 2.5, etc.

@teburd
Copy link
Collaborator Author

teburd commented Nov 13, 2023

Another candidate would be for particulate matter and particle count sensors, where the subtype could be used to specify the particle size, among the common values 1.0, 2.5, etc.

The more I keep looking at this, the more I wonder if what we want is to put channel descriptors into DT and solely use an index into this channel descriptor array as the identifier, with of course some functions to find the correct channel index.

uint32_t pm25_idx = DT_SENSOR_CHANNEL_IDX(mysensornode, SENSOR_CHAN_PM, SENSOR_CHAN_ST_2_5, 0);

Kind of thing perhaps...

This way we can at runtime also iterate over all potential channels a sensor has for things like the sensor shell, or maybe a sensor tracing app (dump the data to a visualizer on a host PC)

I think there's some nice reasons to go this route and maybe worth revamping the RFC a little to account for the need to introspect channels at runtime.

@dolence
Copy link

dolence commented Mar 21, 2024

Since it was pointed as something relevant to this, I would like to explain my use case.

I have a custom board with two DS2482-800, an 8 channels IC used to handle the communication with multiple DS18B20 and other one-wire devices. On my project I will have up to 32 sensors per channel on a long cable (one sensor each 1.5m), to the max of 16*32=512 sensors. Another board will periodically send a packet through Modbus or LoRaWAN asking for a temperature reading on all the active channels, one by one.

This is how I have things set at this moment:

&i2c2 {
	eeprom0: eeprom@50 {
		compatible = "atmel,at24";
		reg = <0x50>;
		status = "okay";
		size = <256>;
		pagesize = <8>;
		address-width = <8>;
		timeout = <5>;
	};

	w1@19 {
		compatible = "maxim,ds2482-800";
		reg = <0x19>;

		#address-cells = <1>;
		#size-cells = <0>;

		w1_a_ch0: ch@0 {
			compatible = "maxim,ds2482-800-channel";
			reg = <7>;
		};
		w1_a_ch1: ch@1 {
			compatible = "maxim,ds2482-800-channel";
			reg = <6>;
		};
		w1_a_ch2: ch@2 {
			compatible = "maxim,ds2482-800-channel";
			reg = <5>;
		};
		w1_a_ch3: ch@3 {
			compatible = "maxim,ds2482-800-channel";
			reg = <4>;
		};
		w1_a_ch4: ch@4 {
			compatible = "maxim,ds2482-800-channel";
			reg = <3>;
		};
		w1_a_ch5: ch@5 {
			compatible = "maxim,ds2482-800-channel";
			reg = <2>;
		};
		w1_a_ch6: ch@6 {
			compatible = "maxim,ds2482-800-channel";
			reg = <1>;
		};
		w1_a_ch7: ch@7 {
			compatible = "maxim,ds2482-800-channel";
			reg = <0>;
		};
	};

	w1@18 {
		compatible = "maxim,ds2482-800";
		reg = <0x18>;

		#address-cells = <1>;
		#size-cells = <0>;

		w1_b_ch8: ch@8 {
			compatible = "maxim,ds2482-800-channel";
			reg = <7>;
		};
		w1_b_ch9: ch@9 {
			compatible = "maxim,ds2482-800-channel";
			reg = <6>;
		};
		w1_b_ch10: ch@10 {
			compatible = "maxim,ds2482-800-channel";
			reg = <5>;
		};
		w1_b_ch11: ch@11{
			compatible = "maxim,ds2482-800-channel";
			reg = <4>;
		};
		w1_b_ch12: ch@12 {
			compatible = "maxim,ds2482-800-channel";
			reg = <3>;
		};
		w1_b_ch13: ch@13 {
			compatible = "maxim,ds2482-800-channel";
			reg = <2>;
		};
		w1_b_ch14: ch@14 {
			compatible = "maxim,ds2482-800-channel";
			reg = <1>;
		};
		w1_b_ch15: ch@15 {
			compatible = "maxim,ds2482-800-channel";
			reg = <0>;
		};
	};
};

Shell devices list ouput
Screenshot from 2024-03-19 17-35-39

w1 seach on a short cable with 7 devices
Screenshot from 2024-03-19 17-38-43

w1 search on a much longer cable with 32 devices
Screenshot from 2024-03-20 17-44-50

Since I don't know how many channels will be in use or how many sensors will be connected to these channels (and their ROM ID) at the building time, I will need to save somewhere the ROM of each sensor alongside the channel and position on cable. It could be done inside the sensor own two register (1 byte each) or to an external eeprom.

My problem relies on how to set this to make use of sensors API. 512 node entries for each sensor would be impractical for obvious reasons.
This is something that would work, for reading only one sensor at a time (#55743 (comment)) but I would lose the ability to multidrop and convert an entire channel at once.

I would like to hear your opinions and would be nice to know if my case could be take in account or if it has relevance to this discussion.

@teburd
Copy link
Collaborator Author

teburd commented May 17, 2024

We have channel specifiers in now with #71093 now with the new read+decode API and I'm considering this complete.

@teburd
Copy link
Collaborator Author

teburd commented May 22, 2024

A small addendum here, I think its still great if we could have a channel descriptor of some kind available, but doing it in DT turns out to be somewhat painful, either we have an array with modulo dependent data (e.g. position 0 is type, position 1 is index, etc) or better would be to have a sub node representing what really is a subset of sensor channels typically, e.g. accel x,y,z and gyro x,y,z with their own independent properties and configuration (this is often possible).

So that's where #71235 comes in and hopefully resolves the issue of describing sensors in a way that could be converted to C code useful to zephyr.

@dolence
Copy link

dolence commented May 22, 2024

I still have a problem understanding how would a fetch an entire bus of sensors wired to the channels of a DS2482-800. I have many sensors per channel and need to send a skip rom command to convert all and after the conversion ends I would need to read one by one by rom ID.

@teburd
Copy link
Collaborator Author

teburd commented May 22, 2024

assuming you describe the device layout property with dt as you noted above you could setup a single reader that asks for all the temperature sensors hanging off the all of the i2c to 1 wire converters. It would require a sensor device driver representing the i2c to 1wire converter that understands your setup though (the w1, w2, etc in this case)

E.g. something like...

SENSOR_DT_READ_IODEV(w1, DT_NODELABEL(w1),
    { SENSOR_CHAN_TEMP, 0 },
    { SENSOR_CHAN_TEMP, 1 },
    ...
    { SENSOR_CHAN_TEMP, 8 },
);
SENSOR_DT_READ_IODEV(w2, DT_NODELABEL(w2),
    { SENSOR_CHAN_TEMP, 0 },
    { SENSOR_CHAN_TEMP, 1 },
    ...
    { SENSOR_CHAN_TEMP, 8 },
);
...

/* put all the iodev pointers in an array for ease of use */
struct rtio_iodev *temp_devices[TEMP_DEVS] = LISTIFY(...);

/* put a matching decoder for each sensor into an array for ease of use */
struct sensor_decoder *decoders[TEMP_DEVS] = LISTIFY(...);

/* create an async executor context with a mempool and an exactly matching number of inflight requests/completions as the number of sensors */
RTIO_MEMPOOL_DEFINE(r, TEMP_DEVS, TEMP_DEVS, 64, 64);


int main() {
    struct rtio_sqe *sqe;
    struct rtio_cqe *cqe;
    uint8_t *buf;
    size_t buf_len;

    while(true) {
        /* setup read requests */
        for (int i = 0; i < TEMP_DEVS; i++) {
            sqe = rtio_sqe_acquire(r);
            rtio_prep_read_with_pool(sqe, temp_devs[i], temp_devs[i]);
        }

        /* wait for all temp sensors to read, may happen concurrently if multiple i2c buses are used */
        rtio_submit(r, TEMP_DEVS); 
        
        for (int i = 0; i < TEMP_DEVS; i++) {
            /* get the completion queue entry for each read request to the sensors */
            cqe = rtio_cqe_consume(r);

	    /* contains the result of the read request, 0 success, -errno for error as usual */
	    rc = cqe->result;

	    /* is a pointer to one of the temp devs, letting you determine *which* sensor the completion came from */
	    userdata = cqe->userdata; 

            /* since we used the builtin memory pool to stash the readings, we need to get the pointer to the associated buffer */
	    rtio_cqe_get_mempool_buffer(ctx, cqe, &buf, &buf_len);

	    /* Release the CQE */
	    rtio_cqe_release(ctx, cqe);

	    /* Decode the buffer with decoder->decode() into a vector of fixed point values, optional! */
            /* decoders[i]->decode(...);
	    
	    /* Release the memory back to the mempool */
	    rtio_release_buffer(ctx, buf, buf_len);
        }

        /* Poll once a second */
        k_msleep(1000);

    }
}

@dolence
Copy link

dolence commented May 22, 2024

assuming you describe the device layout property with dt as you noted above you could setup a single reader that asks for all the temperature sensors hanging off the all of the i2c to 1 wire converters. It would require a device driver that understands your setup though (the w1, w2, etc in this case)

As someone who is beginning on my journey through learning Zephyr and RTOS I must say this is really nice. I didn't knew about RTIO API and it seemed to be exactly what I need. Thank you for providing this example. I have a lot more to learn now. May I ask some questions? What you mean by "a device driver that understands my setup"? I guess ds2482/w1 doesn't support RTIO API by default, is this the case? Would you please elaborate a little more the concept behind it, considering I'm still getting the general idea behind this?

@teburd
Copy link
Collaborator Author

teburd commented May 22, 2024

assuming you describe the device layout property with dt as you noted above you could setup a single reader that asks for all the temperature sensors hanging off the all of the i2c to 1 wire converters. It would require a device driver that understands your setup though (the w1, w2, etc in this case)

As someone who is beginning on my journey through learning Zephyr and RTOS I must say this is really nice. I didn't knew about RTIO API and it seemed to be exactly what I need. Thank you for providing this example. I have a lot more to learn now. May I ask some questions? What you mean by "a device driver that understands my setup"? I guess ds2482/w1 doesn't support RTIO API by default, is this the case? Would you please elaborate a little more the concept behind it, considering I'm still getting the general idea behind this?

Absolutely, the RTIO API from a sensor driver needs to implement the submit and decoder APIs (decoder is specific to sensors, for interpreting the readings).

The idea here is that the driver gets submit called whenever requests (read requests in this case) come in, the read request comes with a buffer you can directly read into.

For sensors the idea would be to directly read the register readings into the buffer without manipulation, allowing for the raw data to be directly used by the application if desired. A small prefix or suffix can be used to stash some data for the decoder (the decoder is stateless!).

so for example.. something like the following

void ds2482_consume(const struct device *dev, bool completion);

void ds2482_read_complete(....)
{
   /* get completions from our driver's internal async context (data->r); */

   /* note that our current request is *done* /
   rtio_iodev_sqe_ok(data->r, data->sqe);

   /* continue working through the queue */
   ds2482_consume(dev, true);
}


void ds2482_start(const struct device *dev) 
{
   struct ds2482_data *data = dev->data;

   /* start async i2c request with rtio */
   struct rtio_mpsc_node *node = rtio_mpsc_pop(&data->io_q);
   struct rtio_iodev_sqe *sqe = CONTAINER_OF(struct rtio_iodev_sqe, q, node);
   
   __ASSERT(sqe->op == RTIO_SQE_OP_RX, "ds2482 only accepts read requests");

   /* the iodev associated with the sqe can be looked at to determine the channels to read here, not shown though */
   uint8_t *buf;
   size_t buf_len;
   
   /* get a reference to the read buffer (or allocate if using pools) from the incoming request
    * this can validate that the buffer is large enough for our needs 
    */
   int rc = rtio_sqe_rx_buf(sqe, min_buf_len, max_buf_len, &buf, &buf_len);

   __ASSERT(rc == 0, "expected a valid read buffer");

   /* we have our io task descriptor (sqe), our array of channels (from the iodev) request the appropriate reads from i2c peripheral asynchronously */
   struct rtio_sqe *i2c_sqe;

    i2c_sqe = rtio_sqe_acquire(data->r);
    /* write the register value to read... */
    rtio_sqe_prep_write(i2c_sqe,  i2c_iodev, RTIO_PRIO_NORM, (uint8_t*)&reg_addr, sizeof(reg_addr));
    /* mark the request with a flag signaling the next one is part of this transaction */
    i2c_sqe->flags |= RTIO_SQE_TRANSACTION;

    i2c_sqe = rtio_sqe_acquire(data->r);
    /* note we read *directly* into the buffer provided by the application here */
    rtio_sqe_prep_read(i2c_sqe, i2c_iodev, buf, buf_len);
    /* we want the next sqe after this one to be chained to it (requires this one completes first). */
    i2c_sqe->flags |= RTIO_SQE_CHAIN;
   
    i2c_sqe = rtio_sqe_acquire(data->r);
    /* finally we want a callback when the transaction completes */
    rtio_sqe_prep-callback(i2c_sqe, ds2482_read_complete);
 
    /* don't wait here, submit and return */
    rtio_submit(data->r, 0);
}

void ds2482_consume(const struct device *dev, bool completion)
{
 /* attempt to kick off async interrupt driven tasks if not already doing things 
    * since the tasks are asynchronous, this really amounts to a few register writes
    * typically and should be just fine in a spin lock
    */
   k_spinlock_key_t key = k_spin_lock(&data->lock);

   if ((data->sqe == NULL && !completion) || (completion)) {
      data->sqe = rtio_mpsc_pop(data->io_q);
   }

   if (data->sqe) {
          ds2482_start(dev);
   }

   k_spin_unlock(&data->lock, key);
}

void ds2482_submit(const struct device *dev, struct rtio_iodev_sqe *sqe) 
{
    struct ds2482_data *data = dev->data;

   /* put sqe into atomic queue, no lock needed here, the "multiple producer" side */
   rtio_mpsc_push(&data->io_q, &sqe->q);

   ds2482_consume(dev, false);
}

@dolence
Copy link

dolence commented May 22, 2024

Thank you very much for your time. This is a very nice explanation! Now I have plenty of things to work with.

@bryceschober
Copy link

The comments in this thread sound like they could be fleshed out a bit more and become a good reference example for RTIO usage. It seems like there are important exemplary details here that aren't in the existing samples/subsys/rtio/sensor_batch_processing sample, which doesn't even have a README...

@teburd
Copy link
Collaborator Author

teburd commented May 23, 2024

The comments in this thread sound like they could be fleshed out a bit more and become a good reference example for RTIO usage. It seems like there are important exemplary details here that aren't in the existing samples/subsys/rtio/sensor_batch_processing sample, which doesn't even have a README...

Fair! I have on my TODO list updating the docs... which I was working on and then got side tracked trying to make building docs not take so long 💻 🦥

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: Sensor Subsystem area: Sensors Sensors RFC Request For Comments: want input from the community
Projects
Status: Done
Development

No branches or pull requests

8 participants