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

Add generic serial bus driver #1507

Merged
merged 26 commits into from Nov 23, 2018

Conversation

andrzej-kaczmarek
Copy link
Contributor

@andrzej-kaczmarek andrzej-kaczmarek commented Nov 6, 2018

This PR adds initial proposed API and implementation of generic serial bus driver.

The goal is to create abstraction for using serial buses like I2C and SPI in a generic way where bus state is managed automatically to allow access from multiple tasks and to multiple devices concurrently in an easy way [1]. It will be also good place to add special handling for known issues in existing devices if they are attached to bus so we can recover from errors [2].

[1] currently we use global mutexes which are shared via sensor/charger/etc. interface with each driver and each driver needs to lock it manually - we should have lock defined and managed automatically on bus level
[2] currently we already have some workaround for faulty device in hal_i2c implementation of hw/mcu/nordic/nrf52xxx - HAL is not a good place to workaround problems not related to particular I2C controller and this workaround will also not work when platform other than nrf52xxx is used

There are two kind of objects managed by bus driver:

  • bus_dev = bus device which is a representation of I2C or SPI bus instance; should be created by MCU (or BSP) code and effectively replace existing init calls to HAL since this is done now internally by bus driver,
  • bus_node = bus node which is a representation of an actual device connected to bus; should be created by driver or even an application in case application wants to access some "loose" object on the bus without dedicated driver.

It may be a bit counter-intuitive to use bus_dev for bus and not for an actual device, but it originated from os_dev which is used internally and honestly I don't have any better names for both so any other ideas are welcome. I eventually got used to these names after a week of coding ;-)

The setup to use bus driver for I2C controller is as follows:

  1. MCU creates i2c0 bus_dev which initializes hardware (similar to what hal_xxx_init call did previously)
  2. for each device attached to bus, driver or application needs to create bus_node which specifies:
  • bus_dev (by name) where node is attached
  • bus configuration used for communication with each device (clock frequency for I2C, will be also data mode for SPI)
  • optional quirks for device; this allows to define some known issues for a device and allow bus driver to automatically apply some workarounds in case such device is used

Application which wants to talk to one of created bus_nodes should do as follows:

  1. call os_dev_open() to open bus_node with specified name
  2. on 1st open, driver can configure device before 1st use
  3. os_dev obtained from os_dev_open() call can now used to talk to device using bus_node APIs

Each operation on bus_node does as follows:

  1. parent bus_dev is locked for exclusive access
  2. bus configuration is adjusted if new device requires different configuration that device previously accessed on the same bus (for I2C this probably won't be used too much since we can run all devices with the same speed, but it's easily possible to use different devices with different speeds)
  3. operation is executed via appropriate HAL
  4. bus_dev is unlocked

So effectively what users of bus_nodes need to do is just call single bus_node_xxx API and parse the result. They don't need to care about current bus state. The goal is to be able to use a simple call to make most of bus accesses.

APIs are split into two main parts:

  • bus/bus.h is what is enough to access bus_node as opened by os_dev_open() and this is what driver and application will mostly use
  • bus/bus_driver.h is what is necessary to create bus_dev and bus_node from MCU/BSP/driver so more low-level stuff that needs to be only done once to create os_devs and not required to access bus from apps
    (there's also bus/bus_debug.h but it's just some helper for debug code, so not really useful from "outside")

Current API allows to do following operations:

  1. read data from bus_node
  2. write data to bus_node
  3. write data from and then read data to the same bus_node (with bus lock held during entire transaction) which is probably the most commonly used transaction
  4. lock and unlock bus_dev to allow combine the above simple ops into compound transactions
  5. (already deprecated) obtain direct access to bus_dev lock

1/2/3 are available as blocking operations; non-blocking versions of these APIs will be also available and they will quite similar to blocking calls except the result will be returned via supplied callback
4 I considered to provide API to define and execute compound transactions (other than most common write->read) but abandoned this for now since it will be quite complicated and I am not sure if it will be used too much, so probably not worth the effort, but we can still add this later
5 is to provide some compatibility with existing code so we can share mutex with other existing ways of locking, at least for now

In addition to new hw/bus and hw/bus/i2c packages which implements generic bus driver and I2C bus driver, there are few other commits included which were either necessary or just useful to implement this:

  1. hal_i2c is extended with some calls that already exist in hal_spi which are separate calls to enable, disable and configure interface
  2. hw/mcu/nordic/nrf52xxx is extended to create bus_devs for enabled I2C instances when hw/bus is included in build
  3. apps/bus_test is provided which includes two simple implementation of bus_nodes which it can then talk to (they are running at different speeds just to see if things are reconfigured properly on-the-fly)

Finally, there are few things that are not finished yet which I'll try to update in upcoming days:

  • doxygen doc is a bit sparse in some places,
  • error handling is barely implemented,
  • device quirks are not implemented
  • SPI bus driver is missing

UPDATE 2018-11-08:

  • SPI driver is working now
  • sensors interface adjusted to use bus driver if enabled (all drivers need to support bus driver also)
  • BME280 driver modified to use bus driver as an example; as a bonus it can work with both I2C and SPI, just need to create proper node using helper function
  • test app can talk to BME280 via sensors interface using either I2C or SPI
  • some issues fixes

hw/bus/include/bus/bus.h Outdated Show resolved Hide resolved
hw/bus/src/bus.c Outdated Show resolved Hide resolved
assert(measurement);

rc = bme280_set_sensor_mode(BME280_FORCED_MODE, &bme280_node->bme280_dev);
assert(rc == 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asserting on access may be to drastic. Maybe rc could be returned on errors.
In release build when assert is not present function returns 0 event if data is wrong.

static void
close_node_cb(struct bus_node *node)
{
console_printf("%s: node %p\n", __func__, node);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess dependency to console is needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's already dependency in bus_test app and since this is sub-package not meant to be used outside I think this is enough

apps/bus_test/bme280_node/src/bme280_node.c Outdated Show resolved Hide resolved
#endif

struct lis2dh_node_pos {
uint16_t x;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it maybe signed data

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I just used this a raw data since it has no other purpose than demo ;-)

hw/bus/spi/include/bus/spi.h Outdated Show resolved Hide resolved
hw/bus/spi/include/bus/spi.h Outdated Show resolved Hide resolved
hw/hal/include/hal/hal_spi.h Outdated Show resolved Hide resolved
@andrzej-kaczmarek
Copy link
Contributor Author

Changed:

  • updated review comments
  • rebased
  • fixed problem with bus node creation for use with sensors (sample BME280 worked only because it did not use data from sensor_itf)


/* XXX this is only required for SPI, but apparently device has no problem
* with this being set also for I2C so let's leave it for now since there's
* no API now to figure out bus type for node
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For devices who need to know this, they should be able to keep track of it themselves. Maybe no need for such an API.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a good point, I did not think about it

hw/bus/src/bus.c Outdated

err = os_mutex_release(&bdev->lock);
if (err == OS_BAD_MUTEX) {
return SYS_EACCES;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect people will ignore return codes from this routine, so I would assert on OS_BAD_MUTEX. Probably should allow OS_NOT_STARTED.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, will fix

hw/bus/src/bus.c Outdated
return SYS_ETIMEOUT;
}

assert(err == OS_OK);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should allow OS_NOT_STARTED.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will fix

hw/bus/src/bus.c Outdated

assert((void *)bdev == (void *)bnode->parent_bus);

if (bdev->configured_for == bnode) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good. No need to configure if it already is.

hw/bus/src/bus.c Outdated
return rc;
}

rc = bus_dev_configure_for(bdev, bnode);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking you'll want to combine bus_dev_lock_by_node() with bus_dev_configure_for(). The latter is always preceded by latter. Would be less calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, configuring bus for device while locking it sounds reasonable.

hw/bus/src/bus.c Outdated
return 0;
}

static inline int
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend dropping inline. I think we care more about space rather than speed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point

#include "bus/bus_driver.h"

static int
bus_node_open_func(struct os_dev *odev, uint32_t wait, void *arg)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguments wait nor arg are not used. Do you intend these to be used at some point?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably not (I don't really see how I could use those parameters here), but this is callback from os_dev so it needs to be like this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes. Of course.

.configure = bus_spi_configure,
.read = bus_spi_read,
.write = bus_spi_write,
.disable = bus_spi_disable,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For SPI devices combined read/write is occasionally needed. I see there's an call API matching combined read/write semantic in bus_node(), but no such thing here?

I think there's 2 options here; either add combined read/write to allow that for SPI, or make the bus_node_XXX interface such that the driver writer can take advantage of the locking/reconfig for bus-sharing done there, and then they can call hal_XXX routines of their choice while the device is locked/configured.

Including the non-blocking read/write version. Or were you going to roll those as separate interface within bus_dev_ops()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bus_node_write_read_transact does write-then-read to simplify perhaps one of most common transaction for peripheral devices. I also had write-and-read API prototype but removed it as I did not implement it since I don't really know device which would require it (I've only seen ones that can use it, but not require). This can be added later as a bus_node API and as a new op here (like write_read). My original idea was that this would work as expected for SPI bus and return SYS_ENOTSUP for I2C bus. For now, as you mentioned, one can just lock bus manually via bus_dev_lock_by_node (btw, I don't like this name, thinking of changing it) and use HAL if really necessary.

Once I add async APIs which can use non-blocking HAL calls I think it will require to add non-blocking versions of read, write (and read-write) ops as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that it is a public API, I'm all for changing it. bus_node_lock()/bus_node_unlock() ?

I suspect it's ok to leave the full-duplex operation out of the bus_node interface.

Copy link
Contributor

@mkiiskila mkiiskila left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Only needs new name for locking routine.

@andrzej-kaczmarek
Copy link
Contributor Author

Fixed latest comments.

Also had to update device creation since when adjusting other drivers it turned out that some of them need custom arg to be passed to init function - the one we have for os_dev is already used by bus driver so need to have some other way to pass extra arg directly to driver. This is possible via modified node creation APIs.

Finally, BME280 sample node is removed since it did not pass RAT check as it uses driver from Bosch.

If current API is good enough I'd prefer to merge this now and fix/update with subsequent PRs - I guess before release we can even adjust some APIs if necessary since they are not frozen.
I also will push some drivers adjusted to new interface in separate PR.

@andrzej-kaczmarek andrzej-kaczmarek changed the title [RFC] Add generic serial bus driver Add generic serial bus driver Nov 22, 2018
Copy link
Contributor

@rymanluk rymanluk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. Couple of minor comments issues and one more important in bme280.c

}

/**
* Read data from node
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be write

* This is simple version of bus_node_write() with default timeout and no flags.
*
* @param node Node device object
* @param buf Buffer to read data into
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

}

/**
* Read data from node
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy paste issue

int (* enable)(struct bus_dev *bus);
/* Configure bus for node */
int (* configure)(struct bus_dev *bus, struct bus_node *node);
/* Write data to node */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be read

/* Write data to node */
int (* read)(struct bus_dev *dev, struct bus_node *node, uint8_t *buf,
uint16_t length, os_time_t timeout, uint16_t flags);
/* Read data from node */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and write here

rc = bus_node_simple_write(dev, payload, len);

done:
rc = bus_dev_unlock_by_node(dev);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lost previous rc here

This adds initial implementation of generic serial bus driver.

General bus driver is an abstraction for using serial buses like I2C or
SPI in a generic way where bus state is managed automatically to handle
access form multiple threads and to multiple devices concurrently in a
seamless way. This aims to replace direct calls to HAL in drivers etc.

Internally bus driver works with two main object types:
- bus_dev = bus device, a representation of I2C or SPI bus instance
- bus_node = bus node, a representation of device connected to bus_dev

In general, bus_dev object creation should be managed by MCU (or BSP)
code and replace existing calls HAL init APIs since it will initialize
these HALs internally. bus_node objects should be created by drivers
or by an application in case application wants to access some "loose"
device on the bus without dedicated driver.

Externally both objects are passed around as simple os_dev so basically
application needs to call os_dev_open() to get os_dev object and then
just pass it to bus APIs.
This adds init function which gets generic structure as a parameter as
opposed to hal_spi_init which requires MCU-specific structure. This is
to make it compatible with latest similar addition to I2C HAL.
With bus driver enabled sensor interface does not need to manage locks
and device configuration so let's just remove this part in such case.
This means we needs all sensors used by sensor interface to be either
bus-driver-compatible or legacy-compatible depending on what we use.
This modified driver to be compatible with bus driver, when enabled.

As a bonus the driver supports now both I2C and SPI when used with bus
driver. Helper functions are added to register bus node for both I2C
and SPI (only one shall be registered for single device).
This is a simple app for evaluating hw/bus driver.
GPIOs are int everywhere.
To create os_dev which is a bus node for sensor device we need to pass
both node configuration and sensor interface. To make this easier,
there's now sensor_node_cfg struc provided which contains both data and
can be passed when creating os_dev.
Since bus nodes are os_dev, they may need some extra argument to be
passed in init function. However, os_dev custom argument is already
used by bus driver thus we need other way to pass driver-specific arg.

To make this possible, bus_XXX_node_create functions have extra argument
which is stored inside node and passed to init function later.

Hack warning: to avoid using extra 4 bytes of memory for each node to
store extra arg for a brief time we'll just reuse the same pointer (via
union) as used by parent_bus. It's ok to make it like this since we need
to store arg until init function is called and also parent_bus is used
1st time in init function so we can just "exchange" contents there.
Bus locking APIs names were changed to more friendly bus_node_lock()
and bus_node_unlock().

Also locking bus will automatically configure bus for given node since
we lock the bus in order to talk to given node so can have extra call
here.

bus_node_unlock() will not return an error since it's a bit useless -
probably no one cares about it and it can only fail if things are really
broken already, so we'll just assert internally to aid debugging.
BME280 sample node used driver from Bosch which I'm not sure if we can
include in Mynewt repository. Let's remove it - we have sample node for
LIS2DH written from scratch and BME280 is supported via sensors
interface.
@andrzej-kaczmarek
Copy link
Contributor Author

@rymanluk fixed

I have some more fixes ready for this but would prefer to put them in separate PR as it becomes more and more time consuming to rebase all of those patches :-)

@andrzej-kaczmarek andrzej-kaczmarek merged commit 3a2a4db into apache:master Nov 23, 2018
@andrzej-kaczmarek andrzej-kaczmarek deleted the bus-dev branch November 23, 2018 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants