yashi / module-sample

A sample Zephyr module
15 stars 4 forks source link

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.

** 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]].

+begin_example

 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

+end_example

Once this is done, you can test build the application with the following command:

+begin_example

 west build -b native_sim/native/64 module-app -t run

+end_example

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=.

+begin_src yaml

With this entry, West can now list module-sample as a module. You can check it with =west list=.

+begin_example

$ 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

+end_example

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=.

+begin_src cmake

add_subdirectory(.. build)

+end_src

This tells the build system

With this line, you see that libsample is built when you build your application. You see the number of the build steps increased.

+begin_example

$ west build -b native_sim/native/64 module-app : [95/95] Linking C executable zephyr/zephyr.elf

+end_example

+begin_example

$ west build -b native_sim/native/64 module-app : [97/97] Linking C executable zephyr/zephyr.elf

+end_example

** 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.

+begin_src cmake

if(CONFIG_LIBSAMPLE) add_subdirectory(.. build) endif()

+end_src

+begin_src kconfig

config LIBSAMPLE bool "Enable libsample" help This option enables the libsample as a Zephyr module.

+end_src

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.

+begin_example

Modules --->

,*** Available modules. ***
sample (.../module-workspace/modules/lib/sample)  --->

  [ ] Enable libsample

+end_example

+begin_example

$ west build -b native_sim/native/64 module-app -- -DCONFIG_LIBSAMPLE=y

+end_example

+begin_src conf

CONFIG_LIBSAMPLE=y

+end_src

** 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.

+begin_example

 $ 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 && :

+end_example

+begin_example

 $ 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 && :

+end_example

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.

** 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.

+begin_quote

"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.

+end_quote

All you have to do is to add the following line in your library's =zephyr/CMakeLists.txt=.

+begin_src cmake

 zephyr_include_directories(../include)

+end_src

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.

+begin_example

 -I.../module-workspace/modules/lib/sample/zephyr/../include

+end_example

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:

+begin_example

 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.

+end_example

It's obvious if you see how the macro is defined.

+begin_src cmake

 macro(zephyr_interface_library_named name)
   add_library(${name} INTERFACE)
   set_property(GLOBAL APPEND PROPERTY ZEPHYR_INTERFACE_LIBS ${name})
 endmacro()

+end_src

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:

+begin_src cmake

 add_subdirectory(.. build)
 set_property(GLOBAL APPEND PROPERTY ZEPHYR_INTERFACE_LIBS sample)

+end_src

We also need to change our =zephyr/Kconfig=:

+begin_src kconfig

 config APP_LINK_WITH_SAMPLE
     bool "Make libsample header file available to application"
     default y
     depends on LIBSAMPLE

+end_src

We need this because the Zephyr build system has the following check in =zephyr/cmake/app/boilerplate.cmake=:

+begin_src cmake

 target_link_libraries_ifdef(
   CONFIG_APP_LINK_WITH_${boilerplate_lib_upper_case}
   app
   PUBLIC
   ${boilerplate_lib}
   )

+end_src

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.