This is sample and template code for Zephyr's module system. This repository aims to support developers who want to integrate an existing code as a Zephyr module.
For anyone wanting to work with Zephyr modules, please read [[https://docs.zephyrproject.org/latest/guides/modules.html][the official document]] first.
Given
This repository has a sample library, libsample, which we want to integrate to Zephyr as a module.
An application using this sample is in [[https://github.com/yashi/module-app][a separate repository]].
libsample has two functions
sample_pure() does not use any Zephyr facility at all. It does its own calculation and return. If this is the case for all functions in your code, it's simple.
sample_zephyr() uses Zephyr facility and you need to take care a few more bits.
libsample is buildable for both Linux and Zephyr and we want to target both native_sim and qemu_cortex_m3.
The build system for this repository is CMake. A Zephyr module must have a working CMake build script. If code is based on Meson, GNU Make, or any other build system, you must first port it to CMake.
We use the [[https://docs.zephyrproject.org/latest/guides/west/workspaces.html#t2-star-topology-application-is-the-manifest-repository][T2 Star topology]] for our workspace.
Build as a library on Linux Since it's a very simple library, building it for Linux is easy:
git clone https://github.com/yashi/module-sample cd module-sample make -B builddir -G Ninja ninja -C builddir
Build as a Zephyr module
At first, you should make libsample buildable by the Zephyr's build system. You need to tell the build system libsample as a module, and tell how to build it.
This allows us to test building the library with your target compilers.
** Setup West workspace
As noted, we are using T2 star topology. Run the following commands to setup a workspace. This assume you have [[https://pipenv.pypa.io/en/latest/][Pipenv]].
mkdir module-workspace
cd module-workspace
pipenv shell
pip3 install west
west init -m https://github.com/yashi/module-app
west update --narrow
pip3 install -r zephyr/scripts/requirements-base.txt
Once this is done, you can test build the application with the following command:
west build -b native_sim/native/64 module-app -t run
You need to =C-c= to kill the application.
If you don't know about West workspace, please read the [[https://docs.zephyrproject.org/latest/guides/west/basics.html][Basic]] and the [[https://docs.zephyrproject.org/latest/guides/west/workspaces.html][Workspaces]].
** Add as a module
To do so, you must add an entry about the library in the application's =west.yml=.
With this entry, West can now list module-sample as a module. You can check it with =west list=.
$ west list manifest module-app HEAD N/A zephyr zephyr main https://github.com/zephyrproject-rtos/zephyr cmsis modules/hal/cmsis b0612c97c1401feeb4160add6462c3627fe90fc7 https://github.com/zephyrproject-rtos/cmsis module-sample modules/lib/sample main https://github.com/yashi/module-sample
West now recognize it as a module but it doesn't build it at all because it's not told to do so.
Zephyr's build system will load module's =CMakeLists.txt=, which is =zephyr/CMakeLists.txt= as the default. Please note that the build system will search for =zephyr/= directroy in a module directory. This is a fixed location and hard coded in =zephyr/scripts/zephyr_module.py=.
You can, on the other hand, change the location of the =zephyr/CMakeLists.txt=. We'll talk about this in a later section.
** Tell the build system to build
If you just want to bulid the library, add the following line in the =zephyr/CMakeLists.txt=.
add_subdirectory(.. build)
This tells the build system
CMake Error at .../module-workspace/modules/lib/sample/zephyr/CMakeLists.txt:1 (add_subdirectory): add_subdirectory not given a binary directory but the given source directory ".../module-workspace/modules/lib/sample" is not a subdirectory of ".../module-workspace/modules/lib/sample/zephyr". When specifying an out-of-tree source a binary directory must be explicitly specified.
With this line, you see that libsample is built when you build your application. You see the number of the build steps increased.
$ west build -b native_sim/native/64 module-app : [95/95] Linking C executable zephyr/zephyr.elf
$ west build -b native_sim/native/64 module-app : [97/97] Linking C executable zephyr/zephyr.elf
** Conditional compilation with Kconfig
We just built libsample using the Zephyr build system but we want to control when to build it or not just like any other features in Zephyr. To do so, we'll use =if(CONFIG_LIBSAMPLE)= and =Kconfig= constructs.
The default location of =Kconfig= is =zephyr/Kconfig= under a module directory. You can change the location of =Kconfig= as well as =CMakeLists.txt=. This will be discussed in the later section.
if(CONFIG_LIBSAMPLE) add_subdirectory(.. build) endif()
config LIBSAMPLE bool "Enable libsample" help This option enables the libsample as a Zephyr module.
With these changes, libsample will show up in the menuconfig, you can build it with =-DCONFIG_LIBSAMPLE=y=, or you can control the build with =prj.conf= as usual.
Modules --->
,*** Available modules. ***
sample (.../module-workspace/modules/lib/sample) --->
[ ] Enable libsample
$ west build -b native_sim/native/64 module-app -- -DCONFIG_LIBSAMPLE=y
CONFIG_LIBSAMPLE=y
** Build it with your target compilers
Now we can test buliding libsample with your target board and target compilers. We'll use =qemu_cortex_m3= and =native_sim/native/64= as examples, but you should make sure your library is built by your configuraiton.
To see how the library is built, you should use =-v= option to =west= command.
$ west -v build -b native_sim/native/64 module-app -- -DCONFIG_LIBSAMPLE=y
:
[2/135] ccache .../zephyr-sdk-0.13.1/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc -I.../module-workspace/modules/lib/sample/include -Wall -Wextra -std=gnu11 -MD -MT modules/sample/build/CMakeFiles/sample.dir/src/plain.c.obj -MF modules/sample/build/CMakeFiles/sample.dir/src/plain.c.obj.d -o modules/sample/build/CMakeFiles/sample.dir/src/plain.c.obj -c .../module-workspace/modules/lib/sample/src/plain.c
[3/135] : && ccache /usr/bin/cmake -E rm -f modules/sample/build/libsample.a && ccache .../zephyr-sdk-0.13.1/arm-zephyr-eabi/bin/arm-zephyr-eabi-ar qc modules/sample/build/libsample.a modules/sample/build/CMakeFiles/sample.dir/src/plain.c.obj && ccache .../zephyr-sdk-0.13.1/arm-zephyr-eabi/bin/arm-zephyr-eabi-ranlib modules/sample/build/libsample.a && :
$ west -v build -b native_sim/native/64 module-app -- -DCONFIG_LIBSAMPLE=y
[1/97] ccache /usr/lib/ccache/gcc -I.../module-workspace/modules/lib/sample/include -Wall -Wextra -std=gnu11 -MD -MT modules/sample/build/CMakeFiles/sample.dir/src/plain.c.obj -MF modules/sample/build/CMakeFiles/sample.dir/src/plain.c.obj.d -o modules/sample/build/CMakeFiles/sample.dir/src/plain.c.obj -c .../module-workspace/modules/lib/sample/src/plain.c
[2/97] cd .../module-workspace/build/zephyr && /usr/bin/cmake -E echo
[3/97] : && ccache /usr/bin/cmake -E rm -f modules/sample/build/libsample.a && ccache /usr/bin/ar qc modules/sample/build/libsample.a modules/sample/build/CMakeFiles/sample.dir/src/plain.c.obj && ccache /usr/bin/ranlib modules/sample/build/libsample.a && :
An experienced user might notice that the built timing is way too early, before the essential builds in the build system. This will be a problem if your library depends on Zephyr proper. We'll cover that later.
Make sure your library is built with compiler options you want to use. You should also make sure that your library is not using any compiler options and flags a Zephyr application would normally built with. This is because we haven't tell to do so. If your library doesn't depend on Zephyr, you don't need any compiler option from Zephyr. If it uses and depends on Zephyr, that is your library uses Zephyr semaphore or logging subsystem, you must tell additional flags while building your library. We'll cover this later.
Header-only library
A header-only library is a rare but does exists. If you want to integrate such a library, you have to tell the bulid system how to find your header file. Usually, your application is the one to use the header file.
We'll use the following line to integrate libsample to the application.
Just adding this line to your Zephyr application yeilds
.../module-workspace/module-app/src/main.c:2:10: fatal error: libsample.h: No such file or directory
2 | #include ~~~~
compilation terminated.
ninja: build stopped: subcommand failed.
If you see the compilation with =-v= it's obvious that compiler doesn't specify libsample's include directroy.
To tell include directory with CMake? It's =target_include_directories=. This function tells include directries to the given target. But we want to tell our application the libsample include directroy.
We have to ways to do so.
** zephyr_interface
One way to do so is to use =zephyr_interface=, which is a target Zephyr's build system has. This target collects all compiler options the build system needs.
"zephyr_interface" is a source-less library that encapsulates all the global compiler options needed by all source files. All zephyr libraries, including the library named "zephyr" link with this library to obtain these flags.
All you have to do is to add the following line in your library's =zephyr/CMakeLists.txt=.
zephyr_include_directories(../include)
This does get job done. But if you check the build commands, you will see that almost all the compilations gets the libsample's include directory.
-I.../module-workspace/modules/lib/sample/zephyr/../include
This is needed only if Zephyr proper depends on a library, such as the CMSIS module. However, that is not our case.
** ZEPHYR_INTERFACE_LIBS
Another way to specify is to use =ZEPHYR_INTERFACE_LIBS=. It has a similar name with =zephyr_interface=, but these two are different. In fact, =ZEPHYR_INTERFACE_LIBS= is only used by =zephyr_interface_library_named()= as of this writing. The macro is defined in =zephyr/cmake/extensions.cmake=.
It'd be easier if we could use =zephyr_interface_library_named()= in our libsample but if you do you get the following error:
CMake Error at .../module-workspace/zephyr/cmake/extensions.cmake:619 (add_library):
add_library cannot create target "sample" because another target with the
same name already exists. The existing target is a static library created
in source directory ".../module-workspace/modules/lib/sample".
See documentation for policy CMP0002 for more details.
It's obvious if you see how the macro is defined.
macro(zephyr_interface_library_named name)
add_library(${name} INTERFACE)
set_property(GLOBAL APPEND PROPERTY ZEPHYR_INTERFACE_LIBS ${name})
endmacro()
libsample already declare it as =sample= by calling =add_library(sample)= in the top level =CMakeLists.txt= and you are now trying to re-declare =sample= with this macro and CMake doesn't like it.
If libsample is only for Zephyr, it's easier to just use this macro in the top level =CMakeLists.txt= and done with it. It's also possible to do it with a separete branch, overwriting the top level =CMakeLists.txt=.
But here we want to keep as much the original CMake build system for libsample as possible and keep the Zephyr module construct in a separate =zephyr/= directory. So, we'll use =ZEPHYR_INTERFACE_LIBS= directly. Our =zephyr/CMakeLists.txt= will become this:
add_subdirectory(.. build)
set_property(GLOBAL APPEND PROPERTY ZEPHYR_INTERFACE_LIBS sample)
We also need to change our =zephyr/Kconfig=:
config APP_LINK_WITH_SAMPLE
bool "Make libsample header file available to application"
default y
depends on LIBSAMPLE
We need this because the Zephyr build system has the following check in =zephyr/cmake/app/boilerplate.cmake=:
target_link_libraries_ifdef(
CONFIG_APP_LINK_WITH_${boilerplate_lib_upper_case}
app
PUBLIC
${boilerplate_lib}
)
This also explains why the name of the option is =APP_LINK_WITH_SAMPLE=.
You might ask, "We are talking about include directories, why does it use =target_link_libraries_ifdef=, which uses [[https://cmake.org/cmake/help/latest/command/target_link_libraries.html][=target_link_libraries=]], instead of =target_include_directories_ifdef= or [[https://cmake.org/cmake/help/latest/command/target_include_directories.html][=target_include_directories=]]?" With CMake, if a library already knows include directories for applications, your application can just link against it with =target_link_libraries()=.
You can learn about this in more detail in the [[https://cmake.org/cmake/help/latest/guide/tutorial/index.html][CMake Tutorial]], the [[https://cmake.org/cmake/help/latest/guide/tutorial/Adding%20a%20Library.html][step 2]] and [[https://cmake.org/cmake/help/latest/guide/tutorial/Adding%20Usage%20Requirements%20for%20a%20Library.html][step 3]] are the ones you should check.
ToDo