Porting new families
This document briefly outlines what needs to be done, in order to port a new chip family to LibreTiny.
Base framework + builders
The base framework is the core part, that provides little functionality and a small HAL (over some things like OTA or sys control). It also includes a builder script for the vendor SDK.
Here's what has to be done to make that work:
- Find vendor SDK - should be self-explanatory. We can't work without a working SDK (yet).
-
Test vendor SDK - compile a sample program "as it was meant to be done".
- Most SDKs provide some example programs (like Hello World, WiFi scanning, etc.) that can usually be compiled by running a single "make" command.
- Sometimes you need to configure your environment in a weird and complicated way. For me, using Cygwin on Windows was usually enough, though.
- You need to flash this to the chip as well. The SDK usually bundles some flashing tools.
- This step is crucial to understand the vendor build system, and to have working binaries to compare out results against.
-
"Clean up" vendor SDK.
- SDKs usually bundle entire compiler toolchains, which can take up hundreds of megabytes. We want to keep the downloaded PlatformIO packages as small as possible.
- On existing families, GitHub Workflows produce the packages by removing some files and adding
package.jsonto them. See framework-beken-bdk/.github/workflows/platformio-package.yml for an example.
-
Write base family and board definitions.
families.jsonneeds to have the new family added to it.platform.jsonneeds to know the vendor SDK repository.- Add any boards and base JSONs to the
boards/directory. It's easiest to start with generic boards. - Use
boardgen ltcito generate variant sources (.c and .h).
-
Add base core code.
lt_defs.h,lt_family.handlt_api.cfiles need to be created, and initialized with (even empty) functions and definitions.- The list of family functions can be found here.
- Make the SDK call
lt_main()as the entrypoint. If needed, use fixups.
-
Write a binary manipulation tool.
- While this step could be optional, as these tools are provided in the SDK, they're usually platform-specific (i.e. Windows-only) and use proprietary executables, with no source code nor documentation. This is unacceptable for LibreTiny, as we need to support multiple architectures & platforms (Windows, Linux, Raspberry Pi, etc.). Naturally, doing that in Python seems to be the best choice.
- All binary tools are currently in ltchiptool/soc/.../binary.py. The
elf2bin()function is what takes an .ELF file, and generates a set of binaries that can be flashed to the chip. - It's best to test if the generation is correct, by taking an .ELF compiled by vendor SDK, running it through ltchiptool and checking if the resulting binaries are identical.
- Ghidra/IDA Pro is your friend here; you can decompile the SDK tools.
-
Write a flashing tool.
- mostly the same as above. Refer to the existing tools for examples. It's useful to make the flasher class "standalone", i.e. a class that is then wrapped by ltchiptool, like in
realtek-ambz2.
- mostly the same as above. Refer to the existing tools for examples. It's useful to make the flasher class "standalone", i.e. a class that is then wrapped by ltchiptool, like in
-
Write builder scripts.
builder/family/xxx.pyfiles are builders, which contain all SDK sources and include paths. Write the script, based on the existing families, and any Makefiles or other scripts from the SDK.- Make sure not to make a mess in the
CCFLAGS/CPPDEFINES, and only include what's needed there. Some flags are project-wide (family-independent) inbuilder/frameworks/base.py. - Use a pure PlatformIO project - not ESPHome!. Pass one of the generic boards you created before, and
framework = baseinplatformio.ini. Generally, try to get the thing to compile. - Use a simple Hello World program - C, not C++. Only add
main()function with aprintf()and awhile(1)loop. - I've noticed that using
nano.specsinstead ofnosys.specsproduces smaller binaries.
-
When you get it to link successfully, build a UF2 file.
- UF2 packages are for flashing and for OTA.
- Add
UF2OTAto the env, to provide binaries that will go to the UF2. Some understanding of the chip's partition and flash layout will be needed.
-
Flash it, test if it works!
- It probably won't. You may need to remove
__libc_init_array()fromcores/common/base/lt_api.cso that it doesn't crash. Most SDKs don't support C++ properly.
- It probably won't. You may need to remove
Making it actually work
-
Write
flashdbandprintfports.- The ports are in
cores/.../base/port/. It's a simple flash access layer, and a character printing function. Not a lot of work, but it needs to be done first.
- The ports are in
-
Add fixups so that string & memory stdlib functions are not from SDK.
- Refer to stdlib.md to find functions that need to be wrapped.
- SDK should not define them, you have to figure out a way to remove them from headers. Fixups can mess with includes and trick the SDK into using our own functions.
-
Clean up FreeRTOS.
- FreeRTOS' headers usually include some SDK headers, which pull in a lot of macros and typedefs, which usually break lots of non-SDK code, which doesn't expect these macros.
- library-freertos repo contains some FreeRTOS versions, adapted for SDKs. Basically, copy a clean (straight from FreeRTOS github) version to the repo, commit it. Then copy the version from SDK and compare the differences.
- Try to make it look as "stock" as possible. Discard any formatting differences (and backports).
- Annotate any parts that can't be removed with
#ifdef FREERTOS_PORT_REALTEK_AMB1. - Put the FreeRTOS vendor-specific port in library-freertos-port.
- Remove all FreeRTOS sources from builder scripts. Replace with:
-
Do the same with lwIP - later.
-
Write LibreTiny C APIs - in
lt_api.c. -
At this point, your Hello World code should work fine.
Porting Arduino Core - C++ support
-
Add main.cpp and write wiring_*.c ports. GPIOs and stuff should work even without proper C++ support.
-
Port Serial library first. This should already show whether C++ works fine or if it doesn't. For example, calling
Serial.println()refers to the virtual functionPrint::write, which will probably crash the chip if C++ is not being linked properly.