- 
                Notifications
    You must be signed in to change notification settings 
- Fork 931
Add LFS_STATICCFG and other config improvements #491
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
base: master
Are you sure you want to change the base?
Conversation
- Changed the name of the LFS_CONFIG macro to LFS_UTIL to avoid confusion with the lfs_config struct. This also hints that LFS_UTIL is related to lfs_util.h. LFS_UTIL allows the user to override lfs_util.h so they can provide their own system-level dependencies such as malloc, tracing, builtins, stdint definitions, string.h, and others. - Removed stdlib includes from lfs.h, these should all go through lfs_util.h to let users override these definitions if stdlib is unavailable on their system. - Moved error code definitions to lfs_util.h. This lets users override the error codes to replace them with their own error codes and avoid a translation layer in some situations. Note the error codes must still be in the range of a negative int. - Used proper stdint definitions in lfs_scmp.
As an embedded library, littlefs's configuration straddles two worlds.
In most cases the configuration is usually constant at build time, but
when integrated into OSs, the configuration needs to be dynamically
configurable.
To help with this, littlefs has a separate lfs_config struct that can be
placed into ROM when possible.
But you know what's better than ROM configuration? Truely inlinable
static configuration known at compile-time. In addition to avoiding the
RAM cost, compile-time configuration allows for additional compiler
optimizations, such as constexpr-elimination and removal of unused
code-paths.
So how to enable static configuration?
1. define LFS_STATICCFG
2. implement callbacks as global functions:
   - lfs_read
   - lfs_prog
   - lfs_erase
   - lfs_sync
2. define the now-required constants that configure littlefs:
   - LFS_READ_SIZE
   - LFS_PROG_SIZE
   - LFS_BLOCK_SIZE
   - LFS_BLOCK_COUNT
   - LFS_BLOCK_CYCLES
   - LFS_CACHE_SIZE
   - LFS_LOOKAHEAD_SIZE
   - LFS_READ_BUFFER (optional)
   - LFS_PROG_BUFFER (optional)
   - LFS_LOOKAHEAD_BUFFER (optional)
   - LFS_NAME_MAX (optional)
   - LFS_FILE_MAX (optional)
   - LFS_ATTR_MAX (optional)
Note, there is a separate configuration for the file configuration, this
can be enabled/disabled independently of LFS_STATICCFG. You will likely
want to define this as well if you are looking for the smallest code
size.
In order to avoid a mess of #ifdefs, the internals of littlefs use a
simple macro that redirects to either the dynamic or static config at
compile time:
    #ifdef LFS_STATICCFG
    #define LFS_CFG_READ_SIZE(lfs) ((void)lfs, LFS_READ_SIZE)
    #else
    #define LFS_CFG_READ_SIZE(lfs) lfs->cfg->read_size
    #endif
Unfortunately it does look like there still may be a lot of issues
related to warnings of comparisons against constants... If only C had
a way to ignore warnings on individual statements...
Original idea by apmorton
    Since this is already going to be a breaking API change, this renames structs/variables named _config -> _cfg. This is in order to be consistent with functions such as lfs_file_opencfg.
This is a style change to make littlefs's callbacks consistent with most callback declarations found in C. That is, taking in a user-provided `void*`. Previously, these callbacks took a pointer to the config struct itself, which indirectly contained a user provided context, and this gets the job done, but taking in a callback with a `void*` is arguably more expected, has a better chance of integrating with C++/OS-specific code, and is more likely to be optimized out by a clever compiler. --- As a part of these changes, the geometry for the test bds needed to be moved into bd specific configuration objects. This is a good change as it also allows for testing situations where littlefs's geometry does not match the underlying bd.
I have another branch where I tried exploring inlined config structs backed by RAM, but this did not work out due to additional RAM and ROM costs. Changing the bds to follow this was surprisingly annoying, as they had a lot of shared geometry that was previously in a single shared config object, and the way that testbd contains either of rambd and filebd made configuring all three of these a bit complicated. Ended up settling on a lfs_testbd_cfg that contains optional pointers to lfs_rambd_cfg and lfs_filebd_cfg. These can be NULL, but only if that bd goes unused.
This makes the block-device related operations more clearly block-device related operations, and helps avoid possible conflicts with user provided `lfs_bd_read/prog/erase` functions in the LFS_STATICCFG mode.
Added internal lineno tracking of generated test files so that test.py can reset the generated #line directives after a test case. This helps debug issues with the generated glue code itself, which would previously end up with invalid/unhelpful lineno information in error messages. Also changed suffix from .c.t to .tc
This makes littlefs's usage of the term "cache" an entirely internal concept and hopefully avoids some confusion about the usefulness of throwing RAM > block_size at these buffers. The term cache isn't entirely inaccurate, these buffers do act as single-line caches, however more often the term cache is used to describe multi-line caches. Maybe this will be added in littlefs's future, but the code-size cost makes this change not worth the overhead at the moment.
| Reducing code size and providing static configuration, in general that sounds good to me! | 
| #endif | ||
| #ifndef LFS_LOOKAHEAD_SIZE | ||
| #error "LFS_STATICCFG requires LFS_LOOKAHEAD_SIZE" | ||
| #endif | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Food for thought, but another pattern could have been something like:
struct lfs_config
{
...
#ifndef LFS_STATICCFG_READ_SIZE
    lfs_size_t read_size;
#endif
#ifndef LFS_STATICCFG_WRITE_SIZE
...
};
...
static inline lfs_size_t lfs_cfg_read_size(const struct lfs_config *c)
{
#ifndef LFS_STATICCFG_READ_SIZE
    return c->read_size;
#else
    return LFS_STATICCFG_READ_SIZE;
#endif
}
That would allow for:
- Partially optimized builds for any configuration. E.g. a user could declare what variables they'd never change for a given static configuration (but maybe would change a different parameter), and this could account for that.
- You could add min/max checks for the dynamic and/or static configurations that are used across the board. For instance, the block cycles need to be either off, or at least 2 IIRC.
- Having the functions in the header (not strictly required) allows a single point of entry for users to read a given configuration item regardless of if it's static or dynamic. Right now all of the helpers are placed into the lfs.c file, but it'd be potentially useful for a single point of retrieval.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Granted, doing the above, would require a cfg object to be always passed around to all functions instead of being able to remove it as in this patch; but this could get optimized out with link time optimization.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I'll have to see what this looks like. Partial optimizations would be nice, though it may be a bit difficult to track down all constant-related warnings.
Granted, doing the above, would require a cfg object to be always passed around to all functions
We could always do a big:
#ifdef LFS_STATICCFG_READ_SIZE && \
        LFS_STATICCFG_PROG_SIZE && \
        LFS_STATICCFG_BLOCK_SIZE && \
        LFS_STATICCFG_BLOCK_COUNT && \
        ...You could add min/max checks for the dynamic and/or static configurations that are used across the board. For instance, the block cycles need to be either off, or at least 2 IIRC.
Currently these are checked by runtime asserts in lfs_init. Unfortunately the math isn't always decidable at compile-time by C, but the runtime checks can be removed if you define LFS_NO_ASSERTS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went ahead and created something since I was curious. See here: tim-nordell@734de59
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interestingly, if the avoidance of division in the eviction logic is applied (#489) in conjunction with a static configuration using powers of 2 for all parameters, we end up with only 1 function performing division with a static configuration: lfs_ctz_index(...)
That function does a non div-by-power-of-2 since it divides by the block size less 2 * 4. It looks like that function calculates something integral to the design of LFS, so the - (2 * 4) can't be removed (to optimize for architectures without a hardware divider).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh! That is an interesting observation.
It doesn't surprise me that lfs_ctz_index gets in the way, it is the most complex math in littlefs. The division is effectively finding the number of blocks to iterate over, but each block has on average 2 pointers, so it doesn't work out that nicely.
Hmm, but the block_size is likely a power-of-two, and 2*4 is a power of two. Is there a shortcut for division by the sum of two powers of two?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed up a change to do this 👍 I believe the main difference is I'm using macro names without the LFS_STATICCFG_* prefix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:) Glad you liked the idea of partials.
Note that using MACROs like that loses the type safetyness of using the static inline functions, but it's probably just a stylistic thing here. There shouldn't be a difference in the compiled size between the two choices if the compiler is doing its job.
Also, I don't know what target C standard this is targeted at (C99, C11, etc), but if it was C11, you can use static asserts (https://en.cppreference.com/w/c/language/_Static_assert) for some of the configuration values statically specified.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that using MACROs like that loses the type safetyness of using the static inline functions
True, though in this case they are just rearranging arguments for another function. Also, if all of the macros are specified, we wouldn't emit the lfs_cfg struct, which would cause a type error if these were inline functions.
Also, I don't know what target C standard this is targeted at (C99, C11, etc), but if it was C11, you can use static asserts
Ah, littlefs sticks to C99, and even this has been a pain point for users. Embedded IDEs move pretty slowly.
Though one nice thing with static_asserts is that you can always have a macro that switches between nothing or a static assert if available.
The main downside would be figuring out what can be static asserted, LFS_ASSERT(LFS_CFG_BLOCK_SIZE(cfg) > LFS_CFG_PROG_SIZE(cfg)) may or may not be static assertable depending on what's defined. And it risks duplicating a lot of these checks.
Instead of 1. defining LFS_STATICCFG and 2. defining all LFS_READ_SIZE, LFS_PROG_SIZE, etc. Configuration can now be made static by defining LFS_READ_SIZE, LFS_PROG_SIZE, etc. Thanks to a really large ifdef, if all configurations are provided, LFS_STATICCFG will be defined and the RAM cost fully removed. Additionally, we can remove the ROM cost of each struct cfg member, allowing code savings when config is only partially defined, which is perhaps more common. This moves all of the configuration logic in lfs.h, which has the nice side effect of keeping all of the decision making in the same location. The only catch is that we need to differentiate between the cfg->*_max and *_MAX limits. To do this I've renamed the configuration *_max to *_limit. Note that these two are slightly different, with *_max indicating the maximum supported by the current driver, and *_limit the maximum supported by the specific instance of littlefs. However if you do define a *_LIMIT, I've added an override for the relevant *_MAX, since I can't think of a time where you _wouldn't_ want to do that. --- This also required some tweaks in scripts/test.py in order to populate the lfs_cfg struct correctly. This happens since the test defines overlap littlefs's configuration defines. This does change what is being tested a bit, but hopefully that's not a real issue. Suggested by tim-nordell-nimbelink
| Pushed up the changes suggested by @tim-nordell-nimbelink Now, instead of 1. defining LFS_STATICCFG and 2. defining all LFS_READ_SIZE, LFS_PROG_SIZE, etc. Configuration can now be made static by defining LFS_READ_SIZE, LFS_PROG_SIZE, etc. Thanks to a really large ifdef, if all configurations are provided, LFS_STATICCFG will be defined and the RAM cost fully removed. Additionally, we can remove the ROM cost of each struct cfg member, allowing code savings when config is only partially defined, which is perhaps more common. This moves all of the configuration logic in lfs.h, which has the nice side effect of keeping all of the decision making in the same location. The only catch is that we need to differentiate between the  | 
This is a big set of config changes that originally came from the idea in this comment: #158 (comment). This would be an API-breaking change (but not disk-breaking), so at the moment I am just looking for feedback.
The big change is the addition of static config, in which defining the relevant defines (LFS_READ_SIZE for example) will override the dynamic config found in
lfs_cfg(lfs->cfg->read_size for example). If you only have a single filesystem configuration in your build, this lets the compiler optimize out logic depending on this configuration.Added static configs which allow optimizing out the
lfs_cfgandlfs_file_cfgstructs 5f885f0Change the
*_maxconfigurations to*_limit, since they are slightly different than the*_MAXmacros 5f885f0Rename
lfs_(mount|format|migrate)->lfs_(mount|format|migrate)cfgif cfg is provided c44427fThis matches the existing
lfs_file_opencfg.In addition, this PR contains a number of changes/tweaks/improvements to the config API of littlefs:
Renamed the LFS_CONFIG macro to LFS_UTIL to avoid confusion with the lfs_cfg struct ef9ba2d
Removed lingering clib includes in lfs.h ef9ba2d
Moved error code definitions to lfs_util.h ef9ba2d
Used proper stdint definitions in lfs_scmp ef9ba2d
Rename
*_configstructs to*_cfgfor consistency a549413Changed callbacks to take user provided context directly a7cdd56
struct lfs_cfg { void *ctx; - int (*read)(const struct lfs_cfg *cfg, ...); + int (*read)(void *ctx, ...); ...Changed block device callbacks to use a
bd_*prefix 190eb83struct lfs_cfg { - void *ctx; - int (*read)(void *ctx, ...); + void *bd_ctx; + int (*bd_read)(void *ctx, ...); ...Renamed cache_size -> buffer_size to avoid confusion around the usefulness of >block_size buffer sizes 4990837
I also explored copying the dynamic configuration into the
lfs_tstruct at runtime. This would have avoided needing to persist the lfs_cfg struct for the lifetime of littlefs. This exists on this branch here:https://github.com/littlefs-project/littlefs/tree/config-improvements-mut
But I ended up dropping this change as it increased the code size and made the internals quite a bit more complicated in order to support both dynamic and static configurations. More info here: f9324d1. Though I am still open to bringing this in if it's believed to be a better user experience.
Open to thoughts or suggestions.
Supersedes #262
EDITed based on feedback, previously: