# Zig on RISC-V BL602 with Apache NuttX RTOS and LoRaWAN Read the articles... - ["Zig on RISC-V BL602: Quick Peek with Apache NuttX RTOS"](https://lupyuen.github.io/articles/zig) - ["Build an IoT App with Zig and LoRaWAN"](https://lupyuen.github.io/articles/iot) To build the Hello Zig App for NuttX on BL602... ```bash ## Enable Zig App in NuttX menuconfig make menuconfig ## TODO: Select "Application Configuration > Examples > Hello Zig Example" ## Save the configuration and exit menuconfig. ## Build Nuttx make ## NuttX Build fails with Undefined Reference to `hello_zig_main` ## That's OK, here's the fix... ## Download our modified Zig App for NuttX git clone --recursive https://github.com/lupyuen/zig-bl602-nuttx cd zig-bl602-nuttx ## Compile the Zig App for BL602 (RV32IMACF with Hardware Floating-Point) zig build-obj \ -target riscv32-freestanding-none \ -mcpu sifive_e76 \ hello_zig_main.zig ## Dump the ABI for the compiled app riscv64-unknown-elf-readelf -h -A hello_zig_main.o ## Shows "Flags: 0x1, RVC, soft-float ABI" ## Which is Software Floating-Point. ## This won't link with NuttX because NuttX is compiled with Hardware Floating-Point ## We change Software Floating-Point to Hardware Floating-Point... ## Edit hello_zig_main.o in a Hex Editor, change byte 0x24 from 0x01 to 0x03 ## (See https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header) ## Dump the ABI for the compiled app riscv64-unknown-elf-readelf -h -A hello_zig_main.o ## Shows "Flags: 0x3, RVC, single-float ABI" ## Which is Hardware Floating-Point and will link with NuttX ## Copy the compiled app to NuttX and overwrite `hello.o` ## TODO: Change "$HOME/nuttx" to your NuttX Project Directory cp hello_zig_main.o $HOME/nuttx/apps/examples/hello/*hello.o ## Build NuttX to link the Zig Object from `hello.o` ## TODO: Change "$HOME/nuttx" to your NuttX Project Directory cd $HOME/nuttx/nuttx make ## NuttX build should now succeed ``` Boot NuttX and enter this at the NuttX Shell... ```text NuttShell (NSH) NuttX-10.3.0-RC2 nsh> hello_zig Hello, Zig! ``` For the LoRaWAN Zig App, see this... - ["Convert LoRaWAN App to Zig"](#convert-lorawan-app-to-zig) To compile the LoRa SX1262 Library in C with Zig Compiler, see this... - ["Zig Compiler as Drop-In Replacement for GCC"](#zig-compiler-as-drop-in-replacement-for-gcc) Here's how we made Zig and LoRaWAN run on BL602 NuttX... # Zig App for NuttX Apache NuttX RTOS is bundled with a simple Zig App ... Let's run this on BL602: [hello_zig_main.zig](hello_zig_main.zig) ```zig // Import the Zig Standard Library const std = @import("std"); // Import printf() from C pub extern fn printf( _format: [*:0]const u8 ) c_int; // Main Function pub export fn hello_zig_main( _argc: c_int, _argv: [*]const [*]const u8 ) c_int { _ = _argc; _ = _argv; _ = printf("Hello, Zig!\n"); return 0; } ``` We fixed the last 2 lines to make the Zig compiler happy... ```zig // Previously: printf("Hello, Zig!\n"); // Zig needs us to use the returned value from printf()... _ = printf("Hello, Zig!\n"); // Previously this was missing. // Zig needs us to return a value... return 0; ``` Original version is here: [hello_zig_main.zig](https://github.com/apache/incubator-nuttx-apps/blob/master/examples/hello_zig/hello_zig_main.zig) # Enable Zig App To enable the Zig App in NuttX... ```bash make menuconfig ``` Select "Application Configuration > Examples > Hello Zig Example" Save the configuration and exit menuconfig. # Build Fails on NuttX When we build NuttX... ```bash make ``` We see this error... ```text LD: nuttx riscv64-unknown-elf-ld: nuttx/staging/libapps.a(builtin_list.c.home.user.nuttx.apps.builtin.o):(.rodata.g_builtins+0xbc): undefined reference to `hello_zig_main' ``` [(Source)](https://gist.github.com/lupyuen/497c90b862aef48b57ff3124f2ea94d8) Which looks similar to this issue... https://github.com/apache/incubator-nuttx/issues/6219 This seems to be caused by the NuttX Build not calling the Zig Compiler. But no worries! Let's compile the Zig App ourselves and link into NuttX. # Compile Zig App Here's how we compile our Zig App for RISC-V BL602 and link it with NuttX... ```bash ## Download our modified Zig App for NuttX git clone --recursive https://github.com/lupyuen/zig-bl602-nuttx cd zig-bl602-nuttx ## Compile the Zig App for BL602 (RV32IMACF with Hardware Floating-Point) zig build-obj \ -target riscv32-freestanding-none \ -mcpu sifive_e76 \ hello_zig_main.zig ## Copy the compiled app to NuttX and overwrite `hello.o` ## TODO: Change "$HOME/nuttx" to your NuttX Project Directory cp hello_zig_main.o $HOME/nuttx/apps/examples/hello/*hello.o ## Build NuttX to link the Zig Object from `hello.o` ## TODO: Change "$HOME/nuttx" to your NuttX Project Directory cd $HOME/nuttx/nuttx make ``` # Zig Target _Why is the target `riscv32-freestanding-none`?_ Zig Targets have the form `--`... `riscv32`: Because BL602 is a 32-bit RISC-V processor `freestanding`: Because embedded targets don't need an OS `none`: Because embedded targets don't specify the ABI _Why is the target CPU `sifive_e76`?_ BL602 is designated as RV32IMACF... | Designation | Meaning | |:---:|:---| | __`RV32I`__ | 32-bit RISC-V with Base Integer Instructions | __`M`__ | Integer Multiplication + Division | __`A`__ | Atomic Instructions | __`C`__ | Compressed Instructions | __`F`__ | Single-Precision Floating-Point [(Source)](https://en.wikipedia.org/wiki/RISC-V#ISA_base_and_extensions) Among all Zig Targets, only `sifive_e76` has the same designation... ```bash $ zig targets ... "sifive_e76": [ "a", "c", "f", "m" ], ``` [(Source)](https://gist.github.com/lupyuen/09d64c79e12b30e5eebc7d0a9c3b20a4) Thus we use `sifive_e76` as our CPU Target. Alternatively we may use `baseline_rv32-d` as our CPU Target... ```bash ## Compile the Zig App for BL602 (RV32IMACF with Hardware Floating-Point) zig build-obj \ -target riscv32-freestanding-none \ -mcpu=baseline_rv32-d \ hello_zig_main.zig ``` Because... - `baseline_rv32` means RV32IMACFD (D for Double-Precision Floating-Point) - `-d` means remove the Double-Precision Floating-Point (D) (But keep the Single-Precision Floating-Point) [(More about RISC-V Feature Flags. Thanks Matheus!)](https://github.com/lupyuen/zig-bl602-nuttx/issues/1) # Floating-Point ABI When linking the Compiled Zig App with NuttX, we see this error... ```bash $ make ... riscv64-unknown-elf-ld: nuttx/staging/libapps.a(hello_main.c.home.user.nuttx.apps.examples.hello.o): can't link soft-float modules with single-float modules ``` That's because NuttX was compiled for (Single-Precision) __Hardware Floating-Point__ ABI (Application Binary Interface)... ```bash ## Do this BEFORE overwriting hello.o by hello_zig_main.o. ## "*hello.o" expands to something like "hello_main.c.home.user.nuttx.apps.examples.hello.o" $ riscv64-unknown-elf-readelf -h -A $HOME/nuttx/apps/examples/hello/*hello.o ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: RISC-V Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 4528 (bytes into file) Flags: 0x3, RVC, single-float ABI Size of this header: 52 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 40 (bytes) Number of section headers: 26 Section header string table index: 25 Attribute Section: riscv File Attributes Tag_RISCV_stack_align: 16-bytes Tag_RISCV_arch: "rv32i2p0_m2p0_a2p0_f2p0_c2p0" ``` [(Source)](https://gist.github.com/lupyuen/5c090dead49eb50751578f28c15cecd5) [(NuttX was compiled with the GCC Flags `-march=rv32imafc -mabi=ilp32f`)](https://gist.github.com/lupyuen/288c980fdef75c334d32e669a921e623) Whereas Zig Compiler produces an Object File with __Software Floating-Point__ ABI... ```bash $ riscv64-unknown-elf-readelf -h -A hello_zig_main.o ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: RISC-V Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 11968 (bytes into file) Flags: 0x1, RVC, soft-float ABI Size of this header: 52 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 40 (bytes) Number of section headers: 24 Section header string table index: 22 Attribute Section: riscv File Attributes Tag_RISCV_stack_align: 16-bytes Tag_RISCV_arch: "rv32i2p0_m2p0_a2p0_f2p0_c2p0" ``` [(Source)](https://gist.github.com/lupyuen/f04386a0b94ed1fb42a94d671edb1ba7) GCC won't allow us to link object files with Software Floating-Point and Hardware Floating-Point ABIs! (Why did the Zig Compiler produce an Object File with Software Floating-Point ABI, when `sifive_e76` supports Hardware Floating-Point? [See this](https://www.reddit.com/r/Zig/comments/v2zgvh/comment/iavw5xp/?utm_source=share&utm_medium=web2x&context=3)) # Patch ELF Header Zig Compiler generates an Object File with __Software Floating-Point__ ABI (Application Binary Interface)... ```bash ## Dump the ABI for the compiled app $ riscv64-unknown-elf-readelf -h -A hello_zig_main.o ... Flags: 0x1, RVC, soft-float ABI ``` This won't link with NuttX because NuttX is compiled with Hardware Floating-Point ABI. We fix this by modifying the ELF Header... - Edit `hello_zig_main.o` in a Hex Editor [(Like VSCode Hex Editor)](https://marketplace.visualstudio.com/items?itemName=ms-vscode.hexeditor) - Change byte `0x24` (Flags) from `0x01` (Soft Float) to `0x03` (Hard Float) [(See this)](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header) We verify that the Object File has been changed to __Hardware Floating-Point__ ABI... ```bash ## Dump the ABI for the compiled app $ riscv64-unknown-elf-readelf -h -A hello_zig_main.o ... Flags: 0x3, RVC, single-float ABI ``` This is now Hardware Floating-Point ABI and will link with NuttX. Now we link the modified Object File with NuttX... ```bash ## Copy the compiled app to NuttX and overwrite `hello.o` ## TODO: Change "$HOME/nuttx" to your NuttX Project Directory cp hello_zig_main.o $HOME/nuttx/apps/examples/hello/*hello.o ## Build NuttX to link the Zig Object from `hello.o` ## TODO: Change "$HOME/nuttx" to your NuttX Project Directory cd $HOME/nuttx/nuttx make ``` The NuttX Build should now succeed. _Is it really OK to change the ABI like this?_ Well technically the __ABI is correctly generated__ by the Zig Compiler... ```bash ## Dump the ABI for the compiled Zig app $ riscv64-unknown-elf-readelf -h -A hello_zig_main.o ... Flags: 0x1, RVC, soft-float ABI Tag_RISCV_arch: "rv32i2p0_m2p0_a2p0_f2p0_c2p0" ``` The last line translates to __RV32IMACF__, which means that the RISC-V Instruction Set is indeed targeted for __Hardware Floating-Point__. We're only editing the __ELF Header__, because it didn't seem to reflect the correct ABI for the Object File. _Is there a proper fix for this?_ In future the Zig Compiler might allow us to specify the __Floating-Point ABI__ as the target... ```bash ## Compile the Zig App for BL602 ## ("ilp32f" means Hardware Floating-Point ABI) zig build-obj \ -target riscv32-freestanding-ilp32f \ ... ``` [(See this)](https://github.com/ziglang/zig/issues/9760#issuecomment-991738757) Stay Tuned! _Can we patch the ELF Header via Command Line?_ Yep we may patch the ELF Header via __Command Line__... ```bash xxd -c 1 hello_zig_main.o \ | sed 's/00000024: 01/00000024: 03/' \ | xxd -r -c 1 - hello_zig_main2.o ``` # Zig Runs OK! The NuttX Build succeeds. Zig runs OK on NuttX BL602! ```text NuttShell (NSH) NuttX-10.3.0-RC2 nsh> hello_zig Hello, Zig! ``` # Hello App Remember that we overwrote `hello.o` with our Zig Compiled Object File. NuttX Build will fail unless we provide the `hello_main` function... ```text riscv64-unknown-elf-ld: nuttx/staging/libapps.a(builtin_list.c.home.user.nuttx.apps.builtin.o):(.rodata.g_builtins+0xcc): undefined reference to `hello_main' ``` That's why we define `hello_main` in our Zig App... ```zig pub export fn hello_main( _argc: c_int, _argv: [*]const [*]const u8 ) c_int { _ = _argc; _ = _argv; _ = printf("Hello, Zig!\n"); return 0; } ``` [(Source)](hello_zig_main.zig) Which means that the `hello` app will call our Zig Code too... ```text NuttShell (NSH) NuttX-10.3.0-RC2 nsh> hello Hello, Zig! ``` ![Pine64 PineCone BL602 Board (right) connected to Semtech SX1262 LoRa Transceiver (left) over SPI](https://lupyuen.github.io/images/spi2-title.jpg) [_Pine64 PineCone BL602 Board (right) connected to Semtech SX1262 LoRa Transceiver (left) over SPI_](https://lupyuen.github.io/articles/spi2) # Zig Compiler as Drop-In Replacement for GCC _Will Zig Compiler work as [Drop-In Replacement for GCC](https://lupyuen.github.io/articles/zig#why-zig) for compiling NuttX Libraries?_ Let's test it on the [LoRa SX1262 Library](https://lupyuen.github.io/articles/sx1262) for Apache NuttX RTOS! Here's how NuttX compiles the [LoRa SX1262 Library](https://lupyuen.github.io/articles/sx1262) with GCC... ```bash ## LoRa SX1262 Source Directory cd $HOME/nuttx/nuttx/libs/libsx1262 ## Compile radio.c with GCC riscv64-unknown-elf-gcc \ -c \ -fno-common \ -Wall \ -Wstrict-prototypes \ -Wshadow \ -Wundef \ -Os \ -fno-strict-aliasing \ -fomit-frame-pointer \ -fstack-protector-all \ -ffunction-sections \ -fdata-sections \ -g \ -march=rv32imafc \ -mabi=ilp32f \ -mno-relax \ -isystem "$HOME/nuttx/nuttx/include" \ -D__NuttX__ \ -DNDEBUG \ -DARCH_RISCV \ -pipe src/radio.c \ -o src/radio.o ## Compile sx126x.c with GCC riscv64-unknown-elf-gcc \ -c \ -fno-common \ -Wall \ -Wstrict-prototypes \ -Wshadow \ -Wundef \ -Os \ -fno-strict-aliasing \ -fomit-frame-pointer \ -fstack-protector-all \ -ffunction-sections \ -fdata-sections \ -g \ -march=rv32imafc \ -mabi=ilp32f \ -mno-relax \ -isystem "$HOME/nuttx/nuttx/include" \ -D__NuttX__ \ -DNDEBUG \ -DARCH_RISCV \ -pipe src/sx126x.c \ -o src/sx126x.o ## Compile sx126x-nuttx.c with GCC riscv64-unknown-elf-gcc \ -c \ -fno-common \ -Wall \ -Wstrict-prototypes \ -Wshadow \ -Wundef \ -Os \ -fno-strict-aliasing \ -fomit-frame-pointer \ -fstack-protector-all \ -ffunction-sections \ -fdata-sections \ -g \ -march=rv32imafc \ -mabi=ilp32f \ -mno-relax \ -isystem "$HOME/nuttx/nuttx/include" \ -D__NuttX__ \ -DNDEBUG \ -DARCH_RISCV \ -pipe src/sx126x-nuttx.c \ -o src/sx126x-nuttx.o ``` We make these changes... - Change `riscv64-unknown-elf-gcc` to `zig cc` - Add the target `-target riscv32-freestanding-none -mcpu=baseline_rv32-d` - Remove `-march=rv32imafc` And we run this... ```bash ## LoRa SX1262 Source Directory cd $HOME/nuttx/nuttx/libs/libsx1262 ## Compile radio.c with zig cc zig cc \ -target riscv32-freestanding-none \ -mcpu=baseline_rv32-d \ -c \ -fno-common \ -Wall \ -Wstrict-prototypes \ -Wshadow \ -Wundef \ -Os \ -fno-strict-aliasing \ -fomit-frame-pointer \ -fstack-protector-all \ -ffunction-sections \ -fdata-sections \ -g \ -mabi=ilp32f \ -mno-relax \ -isystem "$HOME/nuttx/nuttx/include" \ -D__NuttX__ \ -DNDEBUG \ -DARCH_RISCV \ -pipe src/radio.c \ -o src/radio.o ## Compile sx126x.c with zig cc zig cc \ -target riscv32-freestanding-none \ -mcpu=baseline_rv32-d \ -c \ -fno-common \ -Wall \ -Wstrict-prototypes \ -Wshadow \ -Wundef \ -Os \ -fno-strict-aliasing \ -fomit-frame-pointer \ -fstack-protector-all \ -ffunction-sections \ -fdata-sections \ -g \ -mabi=ilp32f \ -mno-relax \ -isystem "$HOME/nuttx/nuttx/include" \ -D__NuttX__ \ -DNDEBUG \ -DARCH_RISCV \ -pipe src/sx126x.c \ -o src/sx126x.o ## Compile sx126x-nuttx.c with zig cc zig cc \ -target riscv32-freestanding-none \ -mcpu=baseline_rv32-d \ -c \ -fno-common \ -Wall \ -Wstrict-prototypes \ -Wshadow \ -Wundef \ -Os \ -fno-strict-aliasing \ -fomit-frame-pointer \ -fstack-protector-all \ -ffunction-sections \ -fdata-sections \ -g \ -mabi=ilp32f \ -mno-relax \ -isystem "$HOME/nuttx/nuttx/include" \ -D__NuttX__ \ -DNDEBUG \ -DARCH_RISCV \ -pipe src/sx126x-nuttx.c \ -o src/sx126x-nuttx.o ## Link Zig Object Files with NuttX after compiling with `zig cc` ## TODO: Change "$HOME/nuttx" to your NuttX Project Directory cd $HOME/nuttx/nuttx make ``` Zig Compiler shows these errors... ```text In file included from src/sx126x-nuttx.c:3: In file included from nuttx/include/debug.h:39: In file included from nuttx/include/sys/uio.h:45: nuttx/include/sys/types.h:119:9: error: unknown type name '_size_t' typedef _size_t size_t; ^ nuttx/include/sys/types.h:120:9: error: unknown type name '_ssize_t' typedef _ssize_t ssize_t; ^ nuttx/include/sys/types.h:121:9: error: unknown type name '_size_t' typedef _size_t rsize_t; ^ nuttx/include/sys/types.h:174:9: error: unknown type name '_wchar_t' typedef _wchar_t wchar_t; ^ In file included from src/sx126x-nuttx.c:4: In file included from nuttx/include/stdio.h:34: nuttx/include/nuttx/fs/fs.h:238:20: error: use of undeclared identifier 'NAME_MAX' char parent[NAME_MAX + 1]; ^ ``` We fix this by including the right header files... ```c #if defined(__NuttX__) && defined(__clang__) // Workaround for NuttX with zig cc #include #include "../../nuttx/include/limits.h" #endif // defined(__NuttX__) && defined(__clang__) ``` Into these source files... - [radio.c](https://github.com/lupyuen/lora-sx1262/blob/lorawan/src/radio.c#L23-L26) - [sx126x-nuttx.c](https://github.com/lupyuen/lora-sx1262/blob/lorawan/src/sx126x-nuttx.c#L4-L7) - [sx126x.c](https://github.com/lupyuen/lora-sx1262/blob/lorawan/src/sx126x.c#L23-L26) [(See the changes)](https://github.com/lupyuen/lora-sx1262/commit/8da7e4d7cc8f1455d750bc51d75c640eea221f41) We insert this code to tell us (at runtime) whether it was compiled with Zig Compiler or GCC... ```c void SX126xIoInit( void ) { #ifdef __clang__ #warning Compiled with zig cc puts("SX126xIoInit: Compiled with zig cc"); #else #warning Compiled with gcc puts("SX126xIoInit: Compiled with gcc"); #endif // __clang__ ``` [(Source)](https://github.com/lupyuen/lora-sx1262/blob/lorawan/src/sx126x-nuttx.c#L119-L127) Compiled with `zig cc`, the LoRa SX1262 Library runs OK on NuttX yay! ```text nsh> lorawan_test SX126xIoInit: Compiled with zig cc ... ###### =========== MLME-Confirm ============ ###### STATUS : OK ###### =========== JOINED ============ ###### OTAA DevAddr : 000E268C DATA RATE : DR_2 ... ###### =========== MCPS-Confirm ============ ###### STATUS : OK ###### ===== UPLINK FRAME 1 ===== ###### CLASS : A TX PORT : 1 TX DATA : UNCONFIRMED 48 69 20 4E 75 74 74 58 00 DATA RATE : DR_3 U/L FREQ : 923400000 TX POWER : 0 CHANNEL MASK: 0003 ``` [(See the complete log)](https://gist.github.com/lupyuen/ada7f83a96eb36ad1b9fe09da4527003) # LoRaWAN Library for NuttX Let's compile the huge [LoRaWAN Library](https://lupyuen.github.io/articles/lorawan3) with Zig Compiler. NuttX compiles the LoRaWAN Library like this... ```bash ## LoRaWAN Source Directory cd $HOME/nuttx/nuttx/libs/liblorawan ## Compile mac/LoRaMac.c with GCC riscv64-unknown-elf-gcc \ -c \ -fno-common \ -Wall \ -Wstrict-prototypes \ -Wshadow \ -Wundef \ -Os \ -fno-strict-aliasing \ -fomit-frame-pointer \ -fstack-protector-all \ -ffunction-sections \ -fdata-sections \ -g \ -march=rv32imafc \ -mabi=ilp32f \ -mno-relax \ -isystem "$HOME/nuttx/nuttx/include" \ -D__NuttX__ \ -DNDEBUG \ -DARCH_RISCV \ -pipe src/mac/LoRaMac.c \ -o src/mac/LoRaMac.o ``` We switch to the Zig Compiler... ```bash ## LoRaWAN Source Directory cd $HOME/nuttx/nuttx/libs/liblorawan ## Compile mac/LoRaMac.c with zig cc zig cc \ -target riscv32-freestanding-none \ -mcpu=baseline_rv32-d \ -c \ -fno-common \ -Wall \ -Wstrict-prototypes \ -Wshadow \ -Wundef \ -Os \ -fno-strict-aliasing \ -fomit-frame-pointer \ -fstack-protector-all \ -ffunction-sections \ -fdata-sections \ -g \ -mabi=ilp32f \ -mno-relax \ -isystem "$HOME/nuttx/nuttx/include" \ -D__NuttX__ \ -DNDEBUG \ -DARCH_RISCV \ -pipe src/mac/LoRaMac.c \ -o src/mac/LoRaMac.o ## Link Zig Object Files with NuttX after compiling with `zig cc` ## TODO: Change "$HOME/nuttx" to your NuttX Project Directory cd $HOME/nuttx/nuttx make ``` We include the right header files into [LoRaMac.c](https://github.com/lupyuen/LoRaMac-node-nuttx/blob/master/src/mac/LoRaMac.c#L33-L36)... ```c #if defined(__NuttX__) && defined(__clang__) // Workaround for NuttX with zig cc #include #include "../../nuttx/include/limits.h" #endif // defined(__NuttX__) && defined(__clang__) ``` [(See the changes)](https://github.com/lupyuen/LoRaMac-node-nuttx/commit/e36b54ea3351fc80f03d13a131527bf6733410ab) [LoRaMac.c](https://github.com/lupyuen/LoRaMac-node-nuttx/blob/master/src/mac/LoRaMac.c) compiles OK with Zig Compiler. TODO: Compile the other files in the LoRaWAN Library with `build.zig` https://ziglang.org/documentation/master/#Zig-Build-System TODO: Test the LoRaWAN Library # LoRaWAN App for NuttX Now we compile the LoRaWAN App [lorawan_test_main.c](https://github.com/lupyuen/lorawan_test/blob/main/lorawan_test_main.c) with Zig Compiler. NuttX compiles the LoRaWAN App [lorawan_test_main.c](https://github.com/lupyuen/lorawan_test/blob/main/lorawan_test_main.c) like this... ```bash ## App Source Directory cd $HOME/nuttx/apps/examples/lorawan_test/lorawan_test_main.c ## Compile lorawan_test_main.c with GCC riscv64-unknown-elf-gcc \ -c \ -fno-common \ -Wall \ -Wstrict-prototypes \ -Wshadow \ -Wundef \ -Os \ -fno-strict-aliasing \ -fomit-frame-pointer \ -fstack-protector-all \ -ffunction-sections \ -fdata-sections \ -g \ -march=rv32imafc \ -mabi=ilp32f \ -mno-relax \ -isystem "$HOME/nuttx/nuttx/include" \ -D__NuttX__ \ -DNDEBUG \ -DARCH_RISCV \ -pipe \ -I "$HOME/nuttx/apps/graphics/lvgl" \ -I "$HOME/nuttx/apps/graphics/lvgl/lvgl" \ -I "$HOME/nuttx/apps/include" \ -Dmain=lorawan_test_main lorawan_test_main.c \ -o lorawan_test_main.c.home.user.nuttx.apps.examples.lorawan_test.o ``` We switch to Zig Compiler... ```bash ## App Source Directory cd $HOME/nuttx/apps/examples/lorawan_test ## Compile lorawan_test_main.c with zig cc zig cc \ -target riscv32-freestanding-none \ -mcpu=baseline_rv32-d \ -c \ -fno-common \ -Wall \ -Wstrict-prototypes \ -Wshadow \ -Wundef \ -Os \ -fno-strict-aliasing \ -fomit-frame-pointer \ -fstack-protector-all \ -ffunction-sections \ -fdata-sections \ -g \ -mabi=ilp32f \ -mno-relax \ -isystem "$HOME/nuttx/nuttx/include" \ -D__NuttX__ \ -DNDEBUG \ -DARCH_RISCV \ -pipe \ -I "$HOME/nuttx/apps/graphics/lvgl" \ -I "$HOME/nuttx/apps/graphics/lvgl/lvgl" \ -I "$HOME/nuttx/apps/include" \ -Dmain=lorawan_test_main lorawan_test_main.c \ -o *lorawan_test.o ## Link Zig Object Files with NuttX after compiling with `zig cc` ## TODO: Change "$HOME/nuttx" to your NuttX Project Directory cd $HOME/nuttx/nuttx make ``` We include the right header files into [lorawan_test_main.c](https://github.com/lupyuen/lorawan_test/blob/main/lorawan_test_main.c#L20-L23)... ```c #if defined(__NuttX__) && defined(__clang__) // Workaround for NuttX with zig cc #include #include "../../nuttx/include/limits.h" #endif // defined(__NuttX__) && defined(__clang__) ``` [(See the changes)](https://github.com/lupyuen/lorawan_test/commit/3d4a451d44cf36b19ef8d900281a2f8f9590de62) Compiled with `zig cc`, the LoRaWAN App runs OK on NuttX yay! ```text nsh> lorawan_test lorawan_test_main: Compiled with zig cc ... ###### =========== MLME-Confirm ============ ###### STATUS : OK ###### =========== JOINED ============ ###### OTAA DevAddr : 00DC5ED5 DATA RATE : DR_2 ... ###### =========== MCPS-Confirm ============ ###### STATUS : OK ###### ===== UPLINK FRAME 1 ===== ###### CLASS : A TX PORT : 1 TX DATA : UNCONFIRMED 48 69 20 4E 75 74 74 58 00 DATA RATE : DR_3 U/L FREQ : 923400000 TX POWER : 0 CHANNEL MASK: 0003 ``` [(See the complete log)](https://gist.github.com/lupyuen/477982242d897771d7a5780c8a9b0910) # Auto-Translate LoRaWAN App to Zig The Zig Compiler can auto-translate C code to Zig. [(See this)](https://ziglang.org/documentation/master/#C-Translation-CLI) Here's how we auto-translate our LoRaWAN App [lorawan_test_main.c](https://github.com/lupyuen/lorawan_test/blob/main/lorawan_test_main.c) from C to Zig... - Change `zig cc` to `zig translate-c` - Surround the C Flags by `-cflags` ... `--` Like this... ```bash ## App Source Directory cd $HOME/nuttx/apps/examples/lorawan_test ## Auto-translate lorawan_test_main.c from C to Zig zig translate-c \ -target riscv32-freestanding-none \ -mcpu=baseline_rv32-d \ -cflags \ -fno-common \ -Wall \ -Wstrict-prototypes \ -Wshadow \ -Wundef \ -Os \ -fno-strict-aliasing \ -fomit-frame-pointer \ -fstack-protector-all \ -ffunction-sections \ -fdata-sections \ -g \ -mabi=ilp32f \ -mno-relax \ -- \ -isystem "$HOME/nuttx/nuttx/include" \ -D__NuttX__ \ -DNDEBUG \ -DARCH_RISCV \ -I "$HOME/nuttx/apps/graphics/lvgl" \ -I "$HOME/nuttx/apps/graphics/lvgl/lvgl" \ -I "$HOME/nuttx/apps/include" \ -Dmain=lorawan_test_main \ lorawan_test_main.c \ >lorawan_test_main.zig ``` Here's the original C code: [lorawan_test_main.c](https://github.com/lupyuen/lorawan_test/blob/main/lorawan_test_main.c) And the auto-translation from C to Zig: [translated/lorawan_test_main.zig](translated/lorawan_test_main.zig) Here's a snippet from the original C code... ```c int main(int argc, FAR char *argv[]) { #ifdef __clang__ puts("lorawan_test_main: Compiled with zig cc"); #else puts("lorawan_test_main: Compiled with gcc"); #endif // __clang__ // If we are using Entropy Pool and the BL602 ADC is available, // add the Internal Temperature Sensor data to the Entropy Pool init_entropy_pool(); // Compute the interval between transmissions based on Duty Cycle TxPeriodicity = APP_TX_DUTYCYCLE + randr( -APP_TX_DUTYCYCLE_RND, APP_TX_DUTYCYCLE_RND ); const Version_t appVersion = { .Value = FIRMWARE_VERSION }; const Version_t gitHubVersion = { .Value = GITHUB_VERSION }; DisplayAppInfo( "lorawan_test", &appVersion, &gitHubVersion ); // Init LoRaWAN if ( LmHandlerInit( &LmHandlerCallbacks, &LmHandlerParams ) != LORAMAC_HANDLER_SUCCESS ) { printf( "LoRaMac wasn't properly initialized\n" ); // Fatal error, endless loop. while ( 1 ) {} } // Set system maximum tolerated rx error in milliseconds LmHandlerSetSystemMaxRxError( 20 ); // The LoRa-Alliance Compliance protocol package should always be initialized and activated. LmHandlerPackageRegister( PACKAGE_ID_COMPLIANCE, &LmhpComplianceParams ); LmHandlerPackageRegister( PACKAGE_ID_CLOCK_SYNC, NULL ); LmHandlerPackageRegister( PACKAGE_ID_REMOTE_MCAST_SETUP, NULL ); LmHandlerPackageRegister( PACKAGE_ID_FRAGMENTATION, &FragmentationParams ); IsClockSynched = false; IsFileTransferDone = false; // Join the LoRaWAN Network LmHandlerJoin( ); // Set the Transmit Timer StartTxProcess( LORAMAC_HANDLER_TX_ON_TIMER ); // Handle LoRaWAN Events handle_event_queue(NULL); // Never returns return 0; } ``` [(Source)](https://github.com/lupyuen/lorawan_test/blob/main/lorawan_test_main.c#L271-L323) And the auto-translated Zig code... ```zig pub export fn lorawan_test_main(arg_argc: c_int, arg_argv: [*c][*c]u8) c_int { var argc = arg_argc; _ = argc; var argv = arg_argv; _ = argv; _ = puts("lorawan_test_main: Compiled with zig cc"); init_entropy_pool(); TxPeriodicity = @bitCast(u32, @as(c_int, 40000) + randr(-@as(c_int, 5000), @as(c_int, 5000))); const appVersion: Version_t = Version_t{ .Value = @bitCast(u32, @as(c_int, 16908288)), }; const gitHubVersion: Version_t = Version_t{ .Value = @bitCast(u32, @as(c_int, 83886080)), }; DisplayAppInfo("lorawan_test", &appVersion, &gitHubVersion); if (LmHandlerInit(&LmHandlerCallbacks, &LmHandlerParams) != LORAMAC_HANDLER_SUCCESS) { _ = printf("LoRaMac wasn't properly initialized\n"); while (true) {} } _ = LmHandlerSetSystemMaxRxError(@bitCast(u32, @as(c_int, 20))); _ = LmHandlerPackageRegister(@bitCast(u8, @truncate(i8, @as(c_int, 0))), @ptrCast(?*anyopaque, &LmhpComplianceParams)); _ = LmHandlerPackageRegister(@bitCast(u8, @truncate(i8, @as(c_int, 1))), @intToPtr(?*anyopaque, @as(c_int, 0))); _ = LmHandlerPackageRegister(@bitCast(u8, @truncate(i8, @as(c_int, 2))), @intToPtr(?*anyopaque, @as(c_int, 0))); _ = LmHandlerPackageRegister(@bitCast(u8, @truncate(i8, @as(c_int, 3))), @ptrCast(?*anyopaque, &FragmentationParams)); IsClockSynched = @as(c_int, 0) != 0; IsFileTransferDone = @as(c_int, 0) != 0; LmHandlerJoin(); StartTxProcess(@bitCast(c_uint, LORAMAC_HANDLER_TX_ON_TIMER)); handle_event_queue(@intToPtr(?*anyopaque, @as(c_int, 0))); return 0; } ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/main/translated/lorawan_test_main.zig#L4535-L4565) We'll refer to this auto-translated Zig Code when we manually convert our LoRaWAN App [lorawan_test_main.c](https://github.com/lupyuen/lorawan_test/blob/main/lorawan_test_main.c) from C to Zig in the next section... ![Pine64 PineDio Stack BL604 (left) talking LoRaWAN to RAKwireless WisGate (right)](https://lupyuen.github.io/images/lorawan3-title.jpg) [_Pine64 PineDio Stack BL604 (left) talking LoRaWAN to RAKwireless WisGate (right)_](https://lupyuen.github.io/articles/lorawan3) # Convert LoRaWAN App to Zig Finally we convert the LoRaWAN App [lorawan_test_main.c](https://github.com/lupyuen/lorawan_test/blob/main/lorawan_test_main.c) from C to Zig, to show that we can build Complex IoT Apps in Zig. The LoRaWAN App runs on PineDio Stack BL604 (RISC-V). The app connects to a LoRaWAN Gateway (like ChirpStack or The Things Network) and sends a Data Packet at regular intervals. Here's the original C code: [lorawan_test_main.c](https://github.com/lupyuen/lorawan_test/blob/main/lorawan_test_main.c) (700 lines of C code) And our converted LoRaWAN Zig App: [lorawan_test.zig](lorawan_test.zig) (673 lines of Zig code) ```zig /// Import the LoRaWAN Library from C const c = @cImport({ // NuttX Defines @cDefine("__NuttX__", ""); @cDefine("NDEBUG", ""); @cDefine("ARCH_RISCV", ""); // Workaround for "Unable to translate macro: undefined identifier `LL`" @cDefine("LL", ""); @cDefine("__int_c_join(a, b)", "a"); // Bypass zig/lib/include/stdint.h // NuttX Header Files @cInclude("arch/types.h"); @cInclude("../../nuttx/include/limits.h"); @cInclude("stdio.h"); // LoRaWAN Header Files @cInclude("firmwareVersion.h"); @cInclude("../libs/liblorawan/src/apps/LoRaMac/common/githubVersion.h"); @cInclude("../libs/liblorawan/src/boards/utilities.h"); @cInclude("../libs/liblorawan/src/mac/region/RegionCommon.h"); @cInclude("../libs/liblorawan/src/apps/LoRaMac/common/Commissioning.h"); @cInclude("../libs/liblorawan/src/apps/LoRaMac/common/LmHandler/LmHandler.h"); @cInclude("../libs/liblorawan/src/apps/LoRaMac/common/LmHandler/packages/LmhpCompliance.h"); @cInclude("../libs/liblorawan/src/apps/LoRaMac/common/LmHandler/packages/LmhpClockSync.h"); @cInclude("../libs/liblorawan/src/apps/LoRaMac/common/LmHandler/packages/LmhpRemoteMcastSetup.h"); @cInclude("../libs/liblorawan/src/apps/LoRaMac/common/LmHandler/packages/LmhpFragmentation.h"); @cInclude("../libs/liblorawan/src/apps/LoRaMac/common/LmHandlerMsgDisplay.h"); }); // Main Function that will be called by NuttX pub export fn lorawan_test_main( _argc: c_int, _argv: [*]const [*]const u8 ) c_int { // Call the LoRaWAN Library to set system maximum tolerated rx error in milliseconds _ = c.LmHandlerSetSystemMaxRxError(20); // TODO: Call the LoRaWAN Library to Join LoRaWAN Network // and send a Data Packet ``` To compile the LoRaWAN Zig App [lorawan_test.zig](lorawan_test.zig)... ```bash ## Download our LoRaWAN Zig App for NuttX git clone --recursive https://github.com/lupyuen/zig-bl602-nuttx cd zig-bl602-nuttx ## Compile the Zig App for BL602 (RV32IMACF with Hardware Floating-Point) zig build-obj \ --verbose-cimport \ -target riscv32-freestanding-none \ -mcpu=baseline_rv32-d \ -isystem "$HOME/nuttx/nuttx/include" \ -I "$HOME/nuttx/apps/examples/lorawan_test" \ lorawan_test.zig ## Patch the ELF Header of `lorawan_test.o` from Soft-Float ABI to Hard-Float ABI xxd -c 1 lorawan_test.o \ | sed 's/00000024: 01/00000024: 03/' \ | xxd -r -c 1 - lorawan_test2.o cp lorawan_test2.o lorawan_test.o ## Copy the compiled app to NuttX and overwrite `lorawan_test.o` ## TODO: Change "$HOME/nuttx" to your NuttX Project Directory cp lorawan_test.o $HOME/nuttx/apps/examples/lorawan_test/*lorawan_test.o ## Build NuttX to link the Zig Object from `lorawan_test.o` ## TODO: Change "$HOME/nuttx" to your NuttX Project Directory cd $HOME/nuttx/nuttx make ``` Our LoRaWAN Zig App [lorawan_test.zig](lorawan_test.zig) compiles OK with Zig Compiler after making the following fixes... # Refer to Auto-Translated Zig Code Some parts of the LoRaWAN Zig App [lorawan_test.zig](lorawan_test.zig) can get tricky to convert from C to Zig, like this C code... ```c // Original C code... #define APP_TX_DUTYCYCLE 40000 #define APP_TX_DUTYCYCLE_RND 5000 uint32_t TxPeriodicity = APP_TX_DUTYCYCLE + randr( -APP_TX_DUTYCYCLE_RND, APP_TX_DUTYCYCLE_RND ); ``` [(Source)](https://github.com/lupyuen/lorawan_test/blob/main/lorawan_test_main.c#L283-L286) Which has conflicting signed (`randr`) and unsigned (`APP_TX_DUTYCYCLE`) types. We get help by referring to the auto-translated Zig Code: [translated/lorawan_test_main.zig](translated/lorawan_test_main.zig) ```zig // Converted from C to Zig... const APP_TX_DUTYCYCLE: c_int = 40000; const APP_TX_DUTYCYCLE_RND: c_int = 5000; // Cast to u32 because randr() can be negative var TxPeriodicity: u32 = @bitCast(u32, APP_TX_DUTYCYCLE + c.randr( -APP_TX_DUTYCYCLE_RND, APP_TX_DUTYCYCLE_RND ) ); ``` Which resolves the conflicting types by casting the signed result to become unsigned. # Opaque Type Error When we reference `LmHandlerCallbacks` in our LoRaWAN Zig App [lorawan_test.zig](lorawan_test.zig)... ```zig _ = &LmHandlerCallbacks; ``` Zig Compiler will show this Opaque Type Error... ```text zig-cache/o/d4d456612514c342a153a8d34fbf5970/cimport.zig:1353:5: error: opaque types have unknown size and therefore cannot be directly embedded in unions Fields: struct_sInfoFields, ^ zig-cache/o/d4d456612514c342a153a8d34fbf5970/cimport.zig:1563:5: note: while checking this field PingSlot: PingSlotInfo_t, ^ zig-cache/o/d4d456612514c342a153a8d34fbf5970/cimport.zig:1579:5: note: while checking this field PingSlotInfo: MlmeReqPingSlotInfo_t, ^ zig-cache/o/d4d456612514c342a153a8d34fbf5970/cimport.zig:1585:5: note: while checking this field Req: union_uMlmeParam, ^ zig-cache/o/d4d456612514c342a153a8d34fbf5970/cimport.zig:2277:5: note: while checking this field OnMacMlmeRequest: ?fn (LoRaMacStatus_t, [*c]MlmeReq_t, TimerTime_t) callconv(.C) void, ^ ``` Opaque Type Error is explained here... - ["Extend a C/C++ Project with Zig"](https://zig.news/kristoff/extend-a-c-c-project-with-zig-55di) - ["Translation failures"](https://ziglang.org/documentation/master/#Translation-failures) Let's trace through our Opaque Type Error... ```zig export fn OnMacMlmeRequest( status: c.LoRaMacStatus_t, mlmeReq: [*c]c.MlmeReq_t, nextTxIn: c.TimerTime_t ) void { c.DisplayMacMlmeRequestUpdate(status, mlmeReq, nextTxIn); } ``` Our function `OnMacMlmeRequest` has a parameter of type `MlmeReq_t`, auto-imported by Zig Compiler as... ```zig pub const MlmeReq_t = struct_sMlmeReq; pub const struct_sMlmeReq = extern struct { Type: Mlme_t, Req: union_uMlmeParam, ReqReturn: RequestReturnParam_t, }; ``` Which contains another auto-imported type `union_uMlmeParam`... ```zig pub const union_uMlmeParam = extern union { Join: MlmeReqJoin_t, TxCw: MlmeReqTxCw_t, PingSlotInfo: MlmeReqPingSlotInfo_t, DeriveMcKEKey: MlmeReqDeriveMcKEKey_t, DeriveMcSessionKeyPair: MlmeReqDeriveMcSessionKeyPair_t, }; ``` Which contains an `MlmeReqPingSlotInfo_t`... ```zig pub const MlmeReqPingSlotInfo_t = struct_sMlmeReqPingSlotInfo; pub const struct_sMlmeReqPingSlotInfo = extern struct { PingSlot: PingSlotInfo_t, }; ``` Which contains a `PingSlotInfo_t`... ```zig pub const PingSlotInfo_t = union_uPingSlotInfo; pub const union_uPingSlotInfo = extern union { Value: u8, Fields: struct_sInfoFields, }; ``` Which contains a `struct_sInfoFields`... ```zig pub const struct_sInfoFields = opaque {}; ``` But the fields of `struct_sInfoFields` are not known by the Zig Compiler! If we refer to the original C code... ```c typedef union uPingSlotInfo { /*! * Parameter for byte access */ uint8_t Value; /*! * Structure containing the parameters for the PingSlotInfoReq */ struct sInfoFields { /*! * Periodicity = 0: ping slot every second * Periodicity = 7: ping slot every 128 seconds */ uint8_t Periodicity : 3; /*! * RFU */ uint8_t RFU : 5; }Fields; }PingSlotInfo_t; ``` [(Source)](https://github.com/lupyuen/LoRaMac-node-nuttx/blob/master/src/mac/LoRaMac.h#L312-L333) We see that `sInfoFields` contains Bit Fields, that the Zig Compiler is unable to translate. # Fix Opaque Type Earlier we saw that this fails to compile in our LoRaWAN Zig App [lorawan_test.zig](lorawan_test.zig)... ```zig _ = &LmHandlerCallbacks; ``` That's because `LmHandlerCallbacks` references the auto-imported type `MlmeReq_t`, which contains Bit Fields and can't be translated by the Zig Compiler. Let's convert `MlmeReq_t` to an Opaque Type, since we won't be accessing the fields anyway... ```zig /// We use an Opaque Type to represent MLME Request, because it contains Bit Fields that can't be converted by Zig const MlmeReq_t = opaque {}; ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/main/lorawan_test.zig#L695-L696) We convert `LmHandlerCallbacks` to use our Opaque Type `MlmeReq_t`... ```zig /// Handler Callbacks. Adapted from /// https://github.com/lupyuen/zig-bl602-nuttx/blob/main/translated/lorawan_test_main.zig#L2818-L2833 pub const LmHandlerCallbacks_t = extern struct { GetBatteryLevel: ?fn () callconv(.C) u8, GetTemperature: ?fn () callconv(.C) f32, GetRandomSeed: ?fn () callconv(.C) u32, OnMacProcess: ?fn () callconv(.C) void, OnNvmDataChange: ?fn (c.LmHandlerNvmContextStates_t, u16) callconv(.C) void, OnNetworkParametersChange: ?fn ([*c]c.CommissioningParams_t) callconv(.C) void, OnMacMcpsRequest: ?fn (c.LoRaMacStatus_t, [*c]c.McpsReq_t, c.TimerTime_t) callconv(.C) void, /// Changed `[*c]c.MlmeReq_t` to `*MlmeReq_t` OnMacMlmeRequest: ?fn (c.LoRaMacStatus_t, *MlmeReq_t, c.TimerTime_t) callconv(.C) void, OnJoinRequest: ?fn ([*c]c.LmHandlerJoinParams_t) callconv(.C) void, OnTxData: ?fn ([*c]c.LmHandlerTxParams_t) callconv(.C) void, OnRxData: ?fn ([*c]c.LmHandlerAppData_t, [*c]c.LmHandlerRxParams_t) callconv(.C) void, OnClassChange: ?fn (c.DeviceClass_t) callconv(.C) void, OnBeaconStatusChange: ?fn ([*c]c.LoRaMacHandlerBeaconParams_t) callconv(.C) void, OnSysTimeUpdate: ?fn (bool, i32) callconv(.C) void, }; ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/main/lorawan_test.zig#L675-L693) We change all auto-imported `MlmeReq_t` references from... ```zig [*c]c.MlmeReq_t ``` To our Opaque Type... ```zig *MlmeReq_t ``` We also change all auto-imported `LmHandlerCallbacks_t` references from... ```zig [*c]c.LmHandlerCallbacks_t ``` To our converted `LmHandlerCallbacks_t`... ```zig *LmHandlerCallbacks_t ``` Which means we need to import the affected LoRaWAN Functions ourselves... ```zig /// Changed `[*c]c.MlmeReq_t` to `*MlmeReq_t`. Adapted from /// https://github.com/lupyuen/zig-bl602-nuttx/blob/main/translated/lorawan_test_main.zig#L2905 extern fn DisplayMacMlmeRequestUpdate( status: c.LoRaMacStatus_t, mlmeReq: *MlmeReq_t, nextTxIn: c.TimerTime_t ) void; /// Changed `[*c]c.LmHandlerCallbacks_t` to `*LmHandlerCallbacks_t`. Adapted from /// https://github.com/lupyuen/zig-bl602-nuttx/blob/main/translated/lorawan_test_main.zig#L2835 extern fn LmHandlerInit( callbacks: *LmHandlerCallbacks_t, handlerParams: [*c]c.LmHandlerParams_t ) c.LmHandlerErrorStatus_t; ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/main/lorawan_test.zig#L707-L720) After fixing the Opaque Type, Zig Compiler successfully compiles our LoRaWAN Test App [lorawan_test.zig](lorawan_test.zig) yay! # Macro Error While compiling our LoRaWAN Test App [lorawan_test.zig](lorawan_test.zig), we see this Macro Error... ```text zig-cache/o/23409ceec9a6e6769c416fde1695882f/cimport.zig:2904:32: error: unable to translate macro: undefined identifier `LL` pub const __INT64_C_SUFFIX__ = @compileError("unable to translate macro: undefined identifier `LL`"); // (no file):178:9 ``` According to the Zig Docs, this means that the Zig Compiler failed to translate a C Macro... - ["C Macros"](https://ziglang.org/documentation/master/#C-Macros) So we define `LL` ourselves... ```zig /// Import the LoRaWAN Library from C const c = @cImport({ // Workaround for "Unable to translate macro: undefined identifier `LL`" @cDefine("LL", ""); ``` (`LL` is the "long long" suffix for C Constants, which is probably not needed when we import C Types and Functions into Zig) Then Zig Compiler emits this error... ```text zig-cache/o/83fc6cf7a78f5781f258f156f891554b/cimport.zig:2940:26: error: unable to translate C expr: unexpected token '##' pub const __int_c_join = @compileError("unable to translate C expr: unexpected token '##'"); // /home/user/zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/include/stdint.h:282:9 ``` Which refers to this line in `stdint.h`... ```c #define __int_c_join(a, b) a ## b ``` The `__int_c_join` Macro fails because the `LL` suffix is now blank and the `##` Concatenation Operator fails. We redefine the `__int_c_join` Macro without the `##` Concatenation Operator... ```zig /// Import the LoRaWAN Library from C const c = @cImport({ // Workaround for "Unable to translate macro: undefined identifier `LL`" @cDefine("LL", ""); @cDefine("__int_c_join(a, b)", "a"); // Bypass zig/lib/include/stdint.h ``` Now Zig Compiler successfully compiles our LoRaWAN Test App [lorawan_test.zig](lorawan_test.zig) # Struct Initialisation Error Zig Compiler crashes when it tries to initialise the Timer Struct at startup... ```zig /// Timer to handle the application data transmission duty cycle var TxTimer: c.TimerEvent_t = std.mem.zeroes(c.TimerEvent_t); // Zig Compiler crashes with... // TODO buf_write_value_bytes maybe typethread 11512 panic: // Unable to dump stack trace: debug info stripped ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/main/lorawan_test.zig#L679-L684) So we initialise the Timer Struct in the Main Function instead... ```zig /// Timer to handle the application data transmission duty cycle. /// Init the timer in Main Function. var TxTimer: c.TimerEvent_t = undefined; /// Main Function pub export fn lorawan_test_main( _argc: c_int, _argv: [*]const [*]const u8 ) c_int { // Init the Timer Struct at startup TxTimer = std.mem.zeroes(c.TimerEvent_t); ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/main/lorawan_test.zig#L90-L101) # LoRaWAN Zig App Runs OK! After fixing the above issues, we test the LoRaWAN Zig App on NuttX: [lorawan_test.zig](lorawan_test.zig) ```text nsh> lorawan_test Application name : Zig LoRaWAN Test ... ###### =========== MLME-Confirm ============ ###### STATUS : OK ###### =========== JOINED ============ ###### OTAA DevAddr : 00D803AB DATA RATE : DR_2 ... PrepareTxFrame: Transmit to LoRaWAN (9 bytes): Hi NuttX ###### =========== MCPS-Confirm ============ ###### STATUS : OK ###### ===== UPLINK FRAME 1 ===== ###### CLASS : A TX PORT : 1 TX DATA : UNCONFIRMED 48 69 20 4E 75 74 74 58 00 DATA RATE : DR_3 U/L FREQ : 923200000 TX POWER : 0 CHANNEL MASK: 0003 ``` [(See the complete log)](https://gist.github.com/lupyuen/0871ac515b18d9d68d3aacf831fd0f5b) LoRaWAN Zig App [lorawan_test.zig](lorawan_test.zig) successfully joins the LoRaWAN Network (ChirpStack on RAKwireless WisGate) and sends a Data Packet to the LoRaWAN Gateway yay! # Safety Checks The Zig Compiler reveals interesting insights when auto-translating our C code to Zig. This C code copies an array, byte by byte... ```c static int8_t FragDecoderWrite( uint32_t addr, uint8_t *data, uint32_t size ) { ... for(uint32_t i = 0; i < size; i++ ) { UnfragmentedData[addr + i] = data[i]; } return 0; // Success } ``` [(Source)](https://github.com/lupyuen/lorawan_test/blob/main/lorawan_test_main.c#L539-L550) Here's the auto-translated Zig code... ```zig pub fn FragDecoderWrite(arg_addr: u32, arg_data: [*c]u8, arg_size: u32) callconv(.C) i8 { ... var size = arg_size; var i: u32 = 0; while (i < size) : (i +%= 1) { UnfragmentedData[addr +% i] = data[i]; } return 0; } ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/main/translated/lorawan_test_main.zig#L4335-L4349) Note that the Array Indexing in C... ```c // Array Indexing in C... UnfragmentedData[addr + i] ``` Gets translated to this in Zig... ```zig // Array Indexing in Zig... UnfragmentedData[addr +% i] ``` `+` in C becomes `+%` in Zig! _What's `+%` in Zig?_ That's the Zig Operator for [__Wraparound Addition__](https://ziglang.org/documentation/master/#Wrapping-Operations). Which means that the result wraps back to 0 (and beyond) if the addition overflows the integer. But this isn't what we intended, since we don't expect the addition to overflow. That's why in our final converted Zig code, we revert `+%` back to `+`... ```zig export fn FragDecoderWrite(addr: u32, data: [*c]u8, size: u32) i8 { ... var i: u32 = 0; while (i < size) : (i += 1) { UnfragmentedData[addr + i] = data[i]; } return 0; // Success } ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/main/lorawan_test.zig#L407-L416) _What happens if the addition overflows?_ We'll see a Runtime Error... ```text panic: integer overflow ``` [(Source)](https://ziglang.org/documentation/master/#Integer-Overflow) Which is probably a good thing, to ensure that our values are sensible. _What if our Array Index goes out of bounds?_ We'll get this Runtime Error... ```text panic: index out of bounds ``` [(Source)](https://ziglang.org/documentation/master/#Index-out-of-Bounds) Here's the list of __Safety Checks__ done by Zig at runtime... - ["Undefined Behavior"](https://ziglang.org/documentation/master/#Undefined-Behavior) If we prefer to live recklessly, this is how we disable the Safety Checks... - ["@setRuntimeSafety"](https://ziglang.org/documentation/master/#setRuntimeSafety) # Panic Handler _Some debug features don't seem to be working? Like `unreachable`, `std.debug.assert` and `std.debug.panic`?_ That's because for Embedded Platforms we need to implement our own Panic Handler... - ["Using Zig to Provide Stack Traces on Kernel Panic for a Bare Bones Operating System"](https://andrewkelley.me/post/zig-stack-traces-kernel-panic-bare-bones-os.html) - [Default Panic Handler: `std.debug.default_panic`](https://github.com/ziglang/zig/blob/master/lib/std/builtin.zig#L763-L847) With our own Panic Handler, this Assertion Failure... ```zig // Create a short alias named `assert` const assert = std.debug.assert; // Assertion Failure assert(TxPeriodicity != 0); ``` Will show this Stack Trace... ```text !ZIG PANIC! reached unreachable code Stack Trace: 0x23016394 0x23016ce0 ``` According to our RISC-V Disassembly, the first address `23016394` doesn't look interesting, because it's inside the `assert` function... ```text /home/user/zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/debug.zig:259 pub fn assert(ok: bool) void { 2301637c: 00b51c63 bne a0,a1,23016394 23016380: a009 j 23016382 23016382: 2307e537 lui a0,0x2307e 23016386: f9850513 addi a0,a0,-104 # 2307df98 <__unnamed_4> 2301638a: 4581 li a1,0 2301638c: 00000097 auipc ra,0x0 23016390: f3c080e7 jalr -196(ra) # 230162c8 if (!ok) unreachable; // assertion failure 23016394: a009 j 23016396 ``` But the second address `23016ce0` reveals the assertion that failed... ```text /home/user/nuttx/zig-bl602-nuttx/lorawan_test.zig:95 assert(TxPeriodicity != 0); 23016ccc: 42013537 lui a0,0x42013 23016cd0: fbc52503 lw a0,-68(a0) # 42012fbc 23016cd4: 00a03533 snez a0,a0 23016cd8: fffff097 auipc ra,0xfffff 23016cdc: 690080e7 jalr 1680(ra) # 23016368 /home/user/nuttx/zig-bl602-nuttx/lorawan_test.zig:100 TxTimer = std.mem.zeroes(c.TimerEvent_t); 23016ce0: 42016537 lui a0,0x42016 ``` This is our implementation of the Zig Panic Handler... ```zig /// Called by Zig when it hits a Panic. We print the Panic Message, Stack Trace and halt. See /// https://andrewkelley.me/post/zig-stack-traces-kernel-panic-bare-bones-os.html /// https://github.com/ziglang/zig/blob/master/lib/std/builtin.zig#L763-L847 pub fn panic( message: []const u8, _stack_trace: ?*std.builtin.StackTrace ) noreturn { // Print the Panic Message _ = _stack_trace; _ = puts("\n!ZIG PANIC!"); _ = puts(@ptrCast([*c]const u8, message)); // Print the Stack Trace _ = puts("Stack Trace:"); var it = std.debug.StackIterator.init(@returnAddress(), null); while (it.next()) |return_address| { _ = printf("%p\n", return_address); } // Halt while(true) {} } ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/main/lorawan_test.zig#L501-L522) # Logging We have implemented Debug Logging `std.log.debug` that's described here... - ["A simple overview of Zig's std.log"](https://gist.github.com/leecannon/d6f5d7e5af5881c466161270347ce84d) Here's how we call `std.log.debug` to print a log message... ```zig // Create a short alias named `debug` const debug = std.log.debug; // Message with 8 bytes const msg: []const u8 = "Hi NuttX"; // Print the message debug("Transmit to LoRaWAN ({} bytes): {s}", .{ msg.len, msg }); // Prints: Transmit to LoRaWAN (8 bytes): Hi NuttX ``` `.{ ... }` creates an [__Anonymous Struct__](https://ziglearn.org/chapter-1/#anonymous-structs) with a variable number of arguments that will be passed to `std.log.debug` for printing. Below is our implementation of `std.log.debug`... ```zig /// Called by Zig for `std.log.debug`, `std.log.info`, `std.log.err`, ... /// https://gist.github.com/leecannon/d6f5d7e5af5881c466161270347ce84d pub fn log( comptime _message_level: std.log.Level, comptime _scope: @Type(.EnumLiteral), comptime format: []const u8, args: anytype, ) void { _ = _message_level; _ = _scope; // Format the message var buf: [100]u8 = undefined; // Limit to 100 chars var slice = std.fmt.bufPrint(&buf, format, args) catch { _ = puts("*** log error: buf too small"); return; }; // Terminate the formatted message with a null var buf2: [buf.len + 1 : 0]u8 = undefined; std.mem.copy( u8, buf2[0..slice.len], slice[0..slice.len] ); buf2[slice.len] = 0; // Print the formatted message _ = puts(&buf2); } ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/main/lorawan_test.zig#L519-L546) This implementation calls `puts()`, which is supported by Apache NuttX RTOS since it's [__POSIX-Compliant__](https://nuttx.apache.org/docs/latest/introduction/inviolables.html#strict-posix-compliance). # Compare C and Zig The Original C Code and the Converted Zig Code for our LoRaWAN App look highly similar. Here's the Main Function from our Original C Code... ```c /// Main Function that will be called by NuttX. We call the LoRaWAN Library /// to Join a LoRaWAN Network and send a Data Packet. int main(int argc, FAR char *argv[]) { // If we are using Entropy Pool and the BL602 ADC is available, // add the Internal Temperature Sensor data to the Entropy Pool init_entropy_pool(); // Compute the interval between transmissions based on Duty Cycle TxPeriodicity = APP_TX_DUTYCYCLE + randr( -APP_TX_DUTYCYCLE_RND, APP_TX_DUTYCYCLE_RND ); const Version_t appVersion = { .Value = FIRMWARE_VERSION }; const Version_t gitHubVersion = { .Value = GITHUB_VERSION }; DisplayAppInfo( "lorawan_test", &appVersion, &gitHubVersion ); // Init LoRaWAN if ( LmHandlerInit( &LmHandlerCallbacks, &LmHandlerParams ) != LORAMAC_HANDLER_SUCCESS ) { printf( "LoRaMac wasn't properly initialized\n" ); // Fatal error, endless loop. while ( 1 ) {} } // Set system maximum tolerated rx error in milliseconds LmHandlerSetSystemMaxRxError( 20 ); // The LoRa-Alliance Compliance protocol package should always be initialized and activated. LmHandlerPackageRegister( PACKAGE_ID_COMPLIANCE, &LmhpComplianceParams ); LmHandlerPackageRegister( PACKAGE_ID_CLOCK_SYNC, NULL ); LmHandlerPackageRegister( PACKAGE_ID_REMOTE_MCAST_SETUP, NULL ); LmHandlerPackageRegister( PACKAGE_ID_FRAGMENTATION, &FragmentationParams ); IsClockSynched = false; IsFileTransferDone = false; // Join the LoRaWAN Network LmHandlerJoin( ); // Set the Transmit Timer StartTxProcess( LORAMAC_HANDLER_TX_ON_TIMER ); // Handle LoRaWAN Events handle_event_queue(NULL); // Never returns return 0; } ``` [(Source)](https://github.com/lupyuen/lorawan_test/blob/main/lorawan_test_main.c#L271-L323) And the Main Function from our Converted Zig Code (after some scrubbing)... ```zig /// Main Function that will be called by NuttX. We call the LoRaWAN Library /// to Join a LoRaWAN Network and send a Data Packet. pub export fn lorawan_test_main( _argc: c_int, _argv: [*]const [*]const u8 ) c_int { _ = _argc; _ = _argv; // Init the Timer Struct at startup TxTimer = std.mem.zeroes(c.TimerEvent_t); // If we are using Entropy Pool and the BL602 ADC is available, // add the Internal Temperature Sensor data to the Entropy Pool // TODO: init_entropy_pool(); // Compute the interval between transmissions based on Duty Cycle TxPeriodicity = @bitCast(u32, // Cast to u32 because randr() can be negative APP_TX_DUTYCYCLE + c.randr( -APP_TX_DUTYCYCLE_RND, APP_TX_DUTYCYCLE_RND ) ); // Show the Firmware and GitHub Versions const appVersion = c.Version_t { .Value = c.FIRMWARE_VERSION, }; const gitHubVersion = c.Version_t { .Value = c.GITHUB_VERSION, }; c.DisplayAppInfo("Zig LoRaWAN Test", &appVersion, &gitHubVersion); // Init LoRaWAN if (LmHandlerInit(&LmHandlerCallbacks, &LmHandlerParams) != c.LORAMAC_HANDLER_SUCCESS) { std.log.err("LoRaMac wasn't properly initialized", .{}); // Fatal error, endless loop. while (true) {} } // Set system maximum tolerated rx error in milliseconds _ = c.LmHandlerSetSystemMaxRxError(20); // The LoRa-Alliance Compliance protocol package should always be initialized and activated. _ = c.LmHandlerPackageRegister(c.PACKAGE_ID_COMPLIANCE, &LmhpComplianceParams); _ = c.LmHandlerPackageRegister(c.PACKAGE_ID_CLOCK_SYNC, null); _ = c.LmHandlerPackageRegister(c.PACKAGE_ID_REMOTE_MCAST_SETUP, null); _ = c.LmHandlerPackageRegister(c.PACKAGE_ID_FRAGMENTATION, &FragmentationParams); // Init the Clock Sync and File Transfer status IsClockSynched = false; IsFileTransferDone = false; // Join the LoRaWAN Network c.LmHandlerJoin(); // Set the Transmit Timer StartTxProcess(LmHandlerTxEvents_t.LORAMAC_HANDLER_TX_ON_TIMER); // Handle LoRaWAN Events handle_event_queue(); // Never returns return 0; } ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/main/lorawan_test.zig#L90-L158) TODO: Clean up names of Types, Functions and Variables TODO: Read the Internal Temperature Sensor TODO: Encode the Temperature Sensor Data with TinyCBOR and transmit to The Things Network https://lupyuen.github.io/articles/cbor2 TODO: Monitor sensor data with Prometheus and Grafana https://lupyuen.github.io/articles/prometheus TODO: Add new code with `@import()` https://zig.news/mattnite/import-and-packages-23mb TODO: Do we need to align buffers to 32 bits when exporting to C? ```zig /// User application data /// (Aligned to 32-bit because it's exported to C) var AppDataBuffer: [LORAWAN_APP_DATA_BUFFER_MAX_SIZE]u8 align(4) = std.mem.zeroes([LORAWAN_APP_DATA_BUFFER_MAX_SIZE]u8); ``` # Zig Type Reflection Zig Type Reflection ... Can we use it to generate a Structured Call Graph for C Libraries ... Like for the LoRaWAN Library? 🤔 This Zig Program imports the LoRaWAN Library from C and dumps out the LoRaWAN Types and Functions... ```zig // Do Type Reflection on the imported C functions fn reflect() void { // We run this at Compile-Time (instead of Runtime)... comptime { // Allow Zig Compiler to loop up to 100,000 times (Default is 1,000) @setEvalBranchQuota(100_000); // Get the Type Info of the C Namespace const T = @typeInfo(c); // Show the Type Info of the C Namespace (Struct) @compileLog("@typeInfo(c): ", T); // Shows | *"@typeInfo(c): ", std.builtin.Type { .Struct = (struct std.builtin.Type.Struct constant)} // Show the number of Fields in the C Namespace (0) @compileLog("T.Struct.fields.len: ", T.Struct.fields.len); // Shows | *"T.Struct.fields.len: ", 0 // Show the number of Declarations in the C Namespace (4743) @compileLog("T.Struct.decls.len: ", T.Struct.decls.len); // Shows | *"T.Struct.decls.len: ", 4743 // Show the first Declaration in the C Namespace (__builtin_bswap16) @compileLog("T.Struct.decls[0].name: ", T.Struct.decls[0].name); // Shows | *"T.Struct.decls[0].name: ", "__builtin_bswap16" // For every C Declaration... for (T.Struct.decls) |decl, i| { // If the C Declaration starts with "Lm" (LoRaMAC)... if (std.mem.startsWith(u8, decl.name, "Lm")) { // Dump the C Declaration var T2 = @typeInfo(c); @compileLog("decl.name: ", T2.Struct.decls[i].name); // Strangely we can't do this... // @compileLog("decl.name: ", decl.name); // Because it shows... // *"decl.name: ", []const u8{76,109,110,83,116,97,116,117,115,95,116} } } } // End of Compile-Time Code } ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/d30cc1e8714f053ad5e417087f5685a62f9df10e/reflect.zig#L825-L867) [(`@typeInfo` is explained here)](https://ziglang.org/documentation/master/#typeInfo) When Zig Compiler compiles the code above, we see this at Compile-Time... ```text $ zig build-obj --verbose-cimport -target riscv32-freestanding-none -mcpu=baseline_rv32-d -isystem /Users/Luppy/pinecone/nuttx/nuttx/include -I /Users/Luppy/pinecone/nuttx/apps/examples/lorawan_test reflect.zig info(compilation): C import output: zig-cache/o/e979b806463a36dcecc2ef773bd2d2ad/cimport.zig | *"@typeInfo(c): ", std.builtin.Type { .Struct = (struct std.builtin.Type.Struct constant)} | *"T.Struct.fields.len: ", 0 | *"T.Struct.decls.len: ", 4744 | *"T.Struct.decls[0].name: ", "__builtin_bswap16" | *"decl.name: ", "LmnStatus_t" | *"decl.name: ", "LmHandlerAdrStates_t" | *"decl.name: ", "LmHandlerFlagStatus_t" ... | *"decl.name: ", "LmHandlerInit" | *"decl.name: ", "LmHandlerIsBusy" | *"decl.name: ", "LmHandlerProcess" | *"decl.name: ", "LmHandlerGetDutyCycleWaitTime" | *"decl.name: ", "LmHandlerSend" | *"decl.name: ", "LmHandlerJoin" ... ./reflect.zig:836:9: error: found compile log statement @compileLog("@typeInfo(c): ", T); ^ ./reflect.zig:840:9: error: found compile log statement @compileLog("T.Struct.fields.len: ", T.Struct.fields.len); ^ ./reflect.zig:844:9: error: found compile log statement @compileLog("T.Struct.decls.len: ", T.Struct.decls.len); ^ ./reflect.zig:848:9: error: found compile log statement @compileLog("T.Struct.decls[0].name: ", T.Struct.decls[0].name); ^ ./reflect.zig:857:17: error: found compile log statement @compileLog("decl.name: ", T2.Struct.decls[i].name); ^ ``` [(Source)](https://gist.github.com/lupyuen/ff225b4208f03ac012f48b130fd3ddd1) Which is a list of C Types and Functions from the LoRaWAN Library. Let's use this to visualise the Call Graph (by module) for the LoRaWAN Library. We'll render the Call Graph with Mermaid.js... https://mermaid-js.github.io/mermaid/#/./flowchart?id=flowcharts And we'll group LoRaWAN Functions in Call Graph by LoRaWAN Module (Subgraph), so we can see the calls across LoRaWAN Modules. # Heisenbug _In the code above, why did we use `T2.Struct.decls[i].name` instead of `decl.name`?_ ```zig // For every C Declaration... for (T.Struct.decls) |decl, i| { // If the C Declaration starts with "Lm" (LoRaMAC)... if (std.mem.startsWith(u8, decl.name, "Lm")) { // Dump the C Declaration var T2 = @typeInfo(c); // Can't use decl.name here... @compileLog("decl.name: ", T2.Struct.decls[i].name); } } ``` We expect this code to print the name of the declaration... ```zig @compileLog("decl.name: ", decl.name); ``` Like so... ```text "decl.name: ", "LmnStatus_t" ``` But strangely it prints the bytes... ```text "decl.name: ", []const u8{76,109,110,83,116,97,116,117,115,95,116} ``` Zig Compiler seems to interpret the name differently after we have referenced the name earlier... ```zig // If the C Declaration starts with "Lm" (LoRaMAC)... if (std.mem.startsWith(u8, decl.name, "Lm")) { ... ``` So we use this workaround instead... ```zig // Get a fresh reference to the Type Info var T2 = @typeInfo(c); // This works OK @compileLog("decl.name: ", T2.Struct.decls[i].name); ``` Which produces the result we need... ```text "decl.name: ", "LmnStatus_t" ``` # Group by C Header Files Can we automatically identify the LoRaWAN Module for every LoRaWAN Function, by analysing the `HEADER_NAME_H__` C Header Macros? Let's try this to dump out all C Declarations and Macros imported from C... ```zig // Get the Type Info of the C Namespace const T = @typeInfo(c); // Remember the C Header var header: []const u8 = ""; // For every C Declaration... for (T.Struct.decls) |decl, i| { var T2 = @typeInfo(c); // If the C Declaration ends with "_H"... if ( std.mem.endsWith(u8, decl.name, "_H") or std.mem.endsWith(u8, decl.name, "_H_") or std.mem.endsWith(u8, decl.name, "_H__") ) { // Dump the C Header and remember it var name = T2.Struct.decls[i].name; @compileLog("-----", name); header = name; } else { // Dump the C Declaration var name = T2.Struct.decls[i].name; @compileLog("decl.name:", name); } } // End of C Declaration ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/293ccb37ec6fdd6b4ac40da5410bdf4c97f12eea/reflect.zig#L825-L885) We get this list of LoRaWAN Functions and LoRaWAN Macros... ```text | *"decl.name:", "LmHandlerInit" | *"decl.name:", "LmHandlerIsBusy" | *"decl.name:", "LmHandlerProcess" | *"decl.name:", "LmHandlerGetDutyCycleWaitTime" | *"decl.name:", "LmHandlerSend" | *"decl.name:", "LmHandlerJoin" ... | *"-----", "__LORAMAC_HANDLER_H__" | *"-----", "__LORAMAC_HANDLER_TYPES_H__" | *"decl.name:", "LMH_SYS_TIME_UPDATE_NEW_API" | *"decl.name:", "__LMHP_COMPLIANCE__" ``` [(Source)](https://gist.github.com/lupyuen/7f0058c982b958a245123714fccd2289) Which isn't useful. `LmHandlerInit` is actually declared inside the C Header File for `__LORAMAC_HANDLER_H__`. But somehow the Zig Type Reflection moves `LmHandlerInit` up to the top, before `__LORAMAC_HANDLER_H__` appears. So it seems we need to manually group the LoRaWAN Functions into LoRaWAN Modules. The LoRaWAN Functions appear to be sequenced according to the C Header File, so we only need to manually tag the first LoRaWAN Function for each LoRaWAN Module. Like this... ```text LmHandlerInit → __LORAMAC_HANDLER_H__ LoRaMacInitialization → __LORAMAC_H__ RegionCommonValueInRange → __REGIONCOMMON_H__ SX126xInit → __SX126x_H__ SX126xIoInit → __SX126x_BOARD_H__ ``` # Self Type Reflection Our Zig App can do Type Reflection on itself to discover its Types, Constants, Variables and Functions... ```zig // Show the Type Info for our Zig Namespace const ThisType = @typeInfo(@This()); @compileLog("ThisType: ", ThisType); @compileLog("ThisType.Struct.decls.len: ", ThisType.Struct.decls.len); @compileLog("ThisType.Struct.decls[0].name: ", ThisType.Struct.decls[0].name); @compileLog("ThisType.Struct.decls[1].name: ", ThisType.Struct.decls[1].name); @compileLog("ThisType.Struct.decls[2].name: ", ThisType.Struct.decls[2].name); // Shows... // | *"ThisType: ", std.builtin.Type { .Struct = (struct std.builtin.Type.Struct constant)} // | *"ThisType.Struct.decls.len: ", 66 // | *"ThisType.Struct.decls[0].name: ", "std" // | *"ThisType.Struct.decls[1].name: ", "c" // | *"ThisType.Struct.decls[2].name: ", "ACTIVE_REGION" ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/293ccb37ec6fdd6b4ac40da5410bdf4c97f12eea/reflect.zig#L886-L898) We'll use this to plot the Function Calls from our Zig Functions to the C Functions in the LoRaWAN Library. # Import Call Log Earlier we have captured this Call Log: A log of calls to the C Functions in the LoRaWAN Library... ```text init_event_queue TimerInit: 0x4201c76c SX126xIoInit: Compiled with gcc init_gpio ... RadioSetChannel: freq=923200000 RadioSetTxConfig: modem=1, power=13, fdev=0, bandwidth=0, datarate=10, coderate=1, preambleLen=8, fixLen=0, crcOn=1, freqHopOn=0, hopPeriod=0, iqInverted=0, timeout=4000 RadioSetTxConfig: SpreadingFactor=10, Bandwidth=4, CodingRate=1, LowDatarateOptimize=0, PreambleLength=8, HeaderType=0, PayloadLength=255, CrcMode=1, InvertIQ=0 RadioStandby RadioSetModem SX126xSetTxParams: power=13, rampTime=7 SX126xSetPaConfig: paDutyCycle=4, hpMax=7, deviceSel=0, paLut=1 ``` [(Source)](https://gist.github.com/lupyuen/0871ac515b18d9d68d3aacf831fd0f5b) Each line of the Call Log contains the LoRaWAN Function Name. We'll match this with the info from Zig Type Reflection to plot the Call Graph. We import the Call Log into our Zig App like so... ```zig /// Call Log captured for this app. From /// https://gist.github.com/lupyuen/0871ac515b18d9d68d3aacf831fd0f5b const call_log = \\init_event_queue \\TimerInit: 0x4201c76c \\SX126xIoInit: Compiled with gcc \\init_gpio ... \\RadioSetChannel: freq=923200000 \\RadioSetTxConfig: modem=1, power=13, fdev=0, bandwidth=0, datarate=10, coderate=1, preambleLen=8, fixLen=0, crcOn=1, freqHopOn=0, hopPeriod=0, iqInverted=0, timeout=4000 \\RadioSetTxConfig: SpreadingFactor=10, Bandwidth=4, CodingRate=1, LowDatarateOptimize=0, PreambleLength=8, HeaderType=0, PayloadLength=255, CrcMode=1, InvertIQ=0 \\RadioStandby \\RadioSetModem \\SX126xSetTxParams: power=13, rampTime=7 \\SX126xSetPaConfig: paDutyCycle=4, hpMax=7, deviceSel=0, paLut=1 \\SecureElementRandomNumber: 0xbc9f21c2 \\RadioSend: size=23 \\00 00 00 00 00 00 00 00 00 5b b1 7b 37 e7 5e c1 4b c2 21 9c 04 48 1b \\RadioSend: PreambleLength=8, HeaderType=0, PayloadLength=23, CrcMode=1, InvertIQ=0 \\TimerStop: 0x4201b86c \\TimerStart2: 0x4201b86c, 4000 ms \\callout_reset: evq=0x420131a8, ev=0x4201b86c \\ \\###### =========== MLME-Request ============ ###### \\###### MLME_JOIN ###### \\###### ===================================== ###### \\STATUS : OK \\StartTxProcess \\handle_event_queue \\DIO1 add event \\handle_event_queue: ev=0x4201b894 \\RadioOnDioIrq \\RadioIrqProcess \\IRQ_TX_DONE \\TimerStop: 0x4201b86c \\TODO: RtcGetCalendarTime \\TODO: RtcBkupRead \\RadioOnDioIrq \\RadioIrqProcess \\ProcessRadioTxDone: RxWindow1Delay=4988 \\RadioSleep ... ; ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/293ccb37ec6fdd6b4ac40da5410bdf4c97f12eea/reflect.zig#L916-L2409) Zig Compiler can process the Call Log line by line like so... ```zig // Show the first line of the Call Log var call_log_split = std.mem.split(u8, call_log, "\n"); const line = call_log_split.next(); @compileLog("line:", line); // Shows | *"line:", []const u8{103,112,108,104,95,101,110,97,98,108,101,58,32,87,65,82,78,73,78,71,58,32,112,105,110,57,58,32,65,108,114,101,97,100,121,32,100,101,116,97,99,104,101,100} ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/293ccb37ec6fdd6b4ac40da5410bdf4c97f12eea/reflect.zig#L900-L904) To iterate through all lines of the Call Log we do this... ```zig // For every line in the Call Log... var call_log_split = std.mem.split(u8, call_log, "\n"); while (call_log_split.next()) |line| { // TODO: Process the line @compileLog("line:", line); ... } // End of Call Log ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/0c480450bcffca353948fc24f41258316c95b8e1/reflect.zig#L906-L928) # Match Call Log Let's match the Call Log with the Function Names from our LoRaWAN Library... ```zig // For every line in the Call Log... var call_log_split = std.mem.split(u8, call_log, "\n"); while (call_log_split.next()) |line| { // For every C Declaration... for (T.Struct.decls) |decl, i| { if (std.mem.eql(u8, decl.name, "Radio")) { continue; } // Skip Radio var T2 = @typeInfo(c); // If the C Declaration matches the Call Log... if (std.mem.startsWith(u8, line, decl.name)) { // Dump the C Declaration var name = T2.Struct.decls[i].name; @compileLog("Found call log", name); break; } } // End of C Declaration } // End of Call Log ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/0c480450bcffca353948fc24f41258316c95b8e1/reflect.zig#L906-L928) The code above produces this result... ```text | *"Found call log", "LoRaMacInitialization" | *"Found call log", "TimerInit" | *"Found call log", "SX126xIoInit" | *"Found call log", "SX126xSetTx" | *"Found call log", "SX126xSetPaConfig" | *"Found call log", "TimerInit" | *"Found call log", "TimerInit" | *"Found call log", "RadioSetModem" | *"Found call log", "RadioSetModem" | *"Found call log", "RadioSetPublicNetwork" | *"Found call log", "RadioSleep" | *"Found call log", "RadioSetModem" | *"Found call log", "RadioSetPublicNetwork" ... ``` [(Source)](https://gist.github.com/lupyuen/e423de95105708223cf5f1b6455e07d7) Which is a list of LoRaWAN Functions and the sequence they were called. One step closer to rendering our Structured Call Graph! # Draw The Graph Let's extend the code above and render a Naive Call Graph... - For every LoRaWAN Function that we identify in the Call Log... - We plot a line between the LoRaWAN Function and the next LoRaWAN Function in the Call Log - So we're just plotting the connected sequence of Function Calls (Yep it's super naive!) We do it like this... ```zig // Draw the graph for all functions in the Call Log var call_log_split = std.mem.split(u8, call_log, "\n"); var prev_name: []const u8 = "Start"; @compileLog("flowchart TD;"); // Top-Down Flowchart // For every line in the Call Log... while (call_log_split.next()) |line| { // For every C Declaration... for (T.Struct.decls) |decl, i| { if (std.mem.eql(u8, decl.name, "Radio")) { continue; } // Skip Radio var T2 = @typeInfo(c); // If the C Declaration matches the Call Log... if (std.mem.startsWith(u8, line, decl.name)) { // Draw the graph: [previous function]-->[current function] var name = T2.Struct.decls[i].name; @compileLog(" ", prev_name, "-->", name, ";"); prev_name = name; break; } } // End of C Declaration } // End of Call Log @compileLog(" ", prev_name, "-->", "End", ";"); ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/5c7989894566cdc0d1852789bc68621c531e459e/reflect.zig#L906-L932) Zig Compiler produces this result at Compile-Time... ```text | *"flowchart TD;" | *" ", "Start", *"-->", "LoRaMacInitialization", *";" | *" ", "LoRaMacInitialization", *"-->", "TimerInit", *";" | *" ", "TimerInit", *"-->", "TimerInit", *";" | *" ", "TimerInit", *"-->", "TimerInit", *";" | *" ", "TimerInit", *"-->", "TimerInit", *";" | *" ", "TimerInit", *"-->", "TimerInit", *";" | *" ", "TimerInit", *"-->", "TimerInit", *";" | *" ", "TimerInit", *"-->", "TimerInit", *";" | *" ", "TimerInit", *"-->", "TimerInit", *";" | *" ", "TimerInit", *"-->", "SX126xIoInit", *";" | *" ", "SX126xIoInit", *"-->", "SX126xSetTx", *";" | *" ", "SX126xSetTx", *"-->", "SX126xSetPaConfig", *";" | *" ", "SX126xSetPaConfig", *"-->", "TimerInit", *";" | *" ", "TimerInit", *"-->", "TimerInit", *";" | *" ", "TimerInit", *"-->", "RadioSetModem", *";" ... ``` [(Source)](https://gist.github.com/lupyuen/056340b299495682c0c2fbc61b30f203) We manually remove the delimiters from the Zig Compiler Log above like this... ```text flowchart TD; Start-->LoRaMacInitialization; LoRaMacInitialization-->TimerInit; TimerInit-->TimerInit; TimerInit-->TimerInit; TimerInit-->TimerInit; TimerInit-->TimerInit; TimerInit-->TimerInit; TimerInit-->TimerInit; TimerInit-->TimerInit; TimerInit-->SX126xIoInit; SX126xIoInit-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->TimerInit; TimerInit-->TimerInit; TimerInit-->RadioSetModem; ... ``` To get this [Mermaid.js Flowchart](https://mermaid-js.github.io/mermaid/#/./flowchart?id=flowcharts)... ```mermaid flowchart TD; Start-->LoRaMacInitialization; LoRaMacInitialization-->TimerInit; TimerInit-->TimerInit; TimerInit-->TimerInit; TimerInit-->TimerInit; TimerInit-->TimerInit; TimerInit-->TimerInit; TimerInit-->TimerInit; TimerInit-->TimerInit; TimerInit-->SX126xIoInit; SX126xIoInit-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->TimerInit; TimerInit-->TimerInit; TimerInit-->RadioSetModem; RadioSetModem-->RadioSetModem; RadioSetModem-->RadioSetPublicNetwork; RadioSetPublicNetwork-->RadioSleep; RadioSleep-->RadioSetModem; RadioSetModem-->RadioSetPublicNetwork; RadioSetPublicNetwork-->TimerInit; TimerInit-->TimerInit; TimerInit-->TimerInit; TimerInit-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerInit; TimerInit-->TimerSetValue; TimerSetValue-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->IRQ_TX_DONE; IRQ_TX_DONE-->TimerStop; TimerStop-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->IRQ_HEADER_VALID; IRQ_HEADER_VALID-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->IRQ_RX_DONE; IRQ_RX_DONE-->TimerStop; TimerStop-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->IRQ_TX_DONE; IRQ_TX_DONE-->TimerStop; TimerStop-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->IRQ_RX_TX_TIMEOUT; IRQ_RX_TX_TIMEOUT-->TimerStop; TimerStop-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerStop; TimerStop-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->IRQ_TX_DONE; IRQ_TX_DONE-->TimerStop; TimerStop-->RadioOnDioIrq; RadioOnDioIrq-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->IRQ_RX_TX_TIMEOUT; IRQ_RX_TX_TIMEOUT-->TimerStop; TimerStop-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerStop; TimerStop-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->IRQ_TX_DONE; IRQ_TX_DONE-->TimerStop; TimerStop-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->IRQ_RX_TX_TIMEOUT; IRQ_RX_TX_TIMEOUT-->TimerStop; TimerStop-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerStop; TimerStop-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->IRQ_TX_DONE; IRQ_TX_DONE-->TimerStop; TimerStop-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->IRQ_RX_TX_TIMEOUT; IRQ_RX_TX_TIMEOUT-->TimerStop; TimerStop-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerStop; TimerStop-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->IRQ_TX_DONE; IRQ_TX_DONE-->TimerStop; TimerStop-->RadioOnDioIrq; RadioOnDioIrq-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; ``` # Super Naive Call Graph _What's wrong with the super-naive Call Graph that we have produced?_ ```text flowchart TD; Start-->LoRaMacInitialization; LoRaMacInitialization-->TimerInit; TimerInit-->TimerInit; TimerInit-->SX126xIoInit; SX126xIoInit-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->TimerInit; TimerInit-->RadioSetModem; ... ``` [(Source)](https://gist.github.com/lupyuen/056340b299495682c0c2fbc61b30f203) Our Call Graph lacks __Structure__. Remember we're only plotting the sequence of Function Calls... - `TimerInit → TimerInit` doesn't make sense (because we don't have recursive functions) - `TimerInit → RadioSetModem` is totally wrong because `TimerInit` is a Low-Level Function (NimBLE Multithreading Library), whereas `RadioSetModem` is an High-Level Function (SX1262 Library) _Can we add some Structure to improve the Call Graph?_ To produce a Structured Call Graph we'll group the functions into Modules, from High-Level to Low-Level... 1. Zig App (Highest Level) 1. LoRaWAN Library 1. SX1262 Library 1. NimBLE Multithreading Library (Lowest Level) And we'll ensure that we never draw a line from a Low-Level Function to a High-Level Function. This is a snippet of what we want to achieve... ```mermaid flowchart TD; subgraph LoRaWAN; Start; LoRaMacInitialization; end; subgraph SX1262; SX126xIoInit; SX126xSetTx; SX126xSetPaConfig; end; subgraph NimBLE; TimerInit; TimerInit; end; Start-->LoRaMacInitialization; LoRaMacInitialization-->TimerInit; SX126xIoInit-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->TimerInit; ``` # Group Functions by Modules Let's add some Structure to our Call Graph by grouping the C Functions into Modules. This is how we define the Modules, from High Level to Low Level... ```zig // Define the Modules and the First / Last Functions in each Module. // We order the Modules from High-Level to Low-Level. var all_modules = [_]Module { // LoRaMAC Handler is the Top Level Module that drives the LoRaWAN Stack Module { .name = "LMHandler", .first_function = "LmHandlerInit", .last_function = "DisplayAppInfo", .first_index = undefined, .last_index = undefined, }, // LoRaMAC Module is the implementation of the LoRaWAN Driver Module { .name = "LoRaMAC", .first_function = "LoRaMacInitialization", .last_function = "LoRaMacDeInitialization", .first_index = undefined, .last_index = undefined, }, // Radio Module is the abstract interface for LoRa Radio Transceivers Module { .name = "Radio", .first_function = "RadioInit", .last_function = "RadioAddRegisterToRetentionList", .first_index = undefined, .last_index = undefined, }, // SX1262 Module is the LoRa Driver for Semtech SX1262 Radio Transceiver Module { .name = "SX1262", .first_function = "SX126xInit", .last_function = "SX126xSetOperatingMode", .first_index = undefined, .last_index = undefined, }, // NimBLE is the Bottom Level Module that contains Multithreading Functions like Timers and Event Queues Module { .name = "NimBLE", .first_function = "TimerInit", .last_function = "TimerGetElapsedTime", .first_index = undefined, .last_index = undefined, }, }; ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/69565443d4b9afb5a2153473b831d7188ff266ef/reflect.zig#L838-L876) Note that we specify only the First and Last Functions for each Module. That's because functions from the same Module are clustered together in the Zig Type Reflection. Then we render each Module as a Mermaid.js Subgraph... ```zig /// Render all Modules and their Functions as Subgraphs fn render_modules(all_modules: []Module) void { comptime { // Render every Module for (all_modules) |module, m| { @compileLog(" subgraph ", module.name, ";"); // For every line in the Call Log... var call_log_split = std.mem.split(u8, call_log, "\n"); while (call_log_split.next()) |line| { var T = @typeInfo(c); // If the Call Log matches a C Declaration... if (get_decl_by_name_filtered(all_modules, line)) |decl_index| { // Get the Module Index for the C Declaration if (get_module_by_decl(all_modules, decl_index)) |m2| { // If the C Declaration matches our Module Index... if (m == m2) { // Print the Function Name var name = T.Struct.decls[decl_index].name; @compileLog(" ", name, ";"); } } else { // Missing Declaration var name = T.Struct.decls[decl_index].name; @compileLog("Missing Decl:", name); } } } // End of Call Log @compileLog(" end;"); } // End of Module } } ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/69565443d4b9afb5a2153473b831d7188ff266ef/reflect.zig#L914-L952) Each Subgraph contains a list of Functions that belong to the Module... ```text flowchart TD; subgraph LMHandler; LmHandlerSend; end; subgraph LoRaMAC; LoRaMacInitialization; end; subgraph Radio; RadioSetModem; RadioSetPublicNetwork; RadioSleep; RadioSetChannel; RadioSetTxConfig; RadioStandby; RadioSend; RadioIrqProcess; ... end; subgraph SX1262; SX126xIoInit; SX126xSetTx; SX126xSetPaConfig; end; subgraph NimBLE; TimerInit; TimerStop; TimerStart; TimerSetValue; end; Start-->LoRaMacInitialization; LoRaMacInitialization-->TimerInit; TimerInit-->SX126xIoInit; ... ``` [(Source)](https://gist.github.com/lupyuen/a7fa7b9973ade3c4565f21e20ef7f88b) When we render the output with Mermaid.js, we get a Structured Call Graph that looks more meaningful... ```mermaid flowchart TD; subgraph LMHandler; LmHandlerSend; LmHandlerSend; LmHandlerSend; LmHandlerSend; LmHandlerSend; LmHandlerSend; LmHandlerSend; LmHandlerSend; LmHandlerSend; LmHandlerSend; LmHandlerSend; LmHandlerSend; end; subgraph LoRaMAC; LoRaMacInitialization; end; subgraph Radio; RadioSetModem; RadioSetModem; RadioSetPublicNetwork; RadioSleep; RadioSetModem; RadioSetPublicNetwork; RadioSetChannel; RadioSetTxConfig; RadioSetTxConfig; RadioStandby; RadioSetModem; RadioSend; RadioSend; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioStandby; RadioSetChannel; RadioSetRxConfig; RadioStandby; RadioSetModem; RadioSetRxConfig; RadioRx; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioSetChannel; RadioSetTxConfig; RadioSetTxConfig; RadioStandby; RadioSetModem; RadioSend; RadioSend; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioStandby; RadioSetChannel; RadioSetRxConfig; RadioStandby; RadioSetModem; RadioSetRxConfig; RadioRx; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioIrqProcess; RadioSetChannel; RadioSetTxConfig; RadioSetTxConfig; RadioStandby; RadioSetModem; RadioSend; RadioSend; RadioIrqProcess; RadioSleep; RadioStandby; RadioSetChannel; RadioSetRxConfig; RadioStandby; RadioSetModem; RadioSetRxConfig; RadioRx; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioIrqProcess; RadioSetChannel; RadioSetTxConfig; RadioSetTxConfig; RadioStandby; RadioSetModem; RadioSend; RadioSend; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioStandby; RadioSetChannel; RadioSetRxConfig; RadioStandby; RadioSetModem; RadioSetRxConfig; RadioRx; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioIrqProcess; RadioSetChannel; RadioSetTxConfig; RadioSetTxConfig; RadioStandby; RadioSetModem; RadioSend; RadioSend; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioStandby; RadioSetChannel; RadioSetRxConfig; RadioStandby; RadioSetModem; RadioSetRxConfig; RadioRx; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioIrqProcess; RadioSetChannel; RadioSetTxConfig; RadioSetTxConfig; RadioStandby; RadioSetModem; RadioSend; RadioSend; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioStandby; RadioSetChannel; RadioSetRxConfig; RadioStandby; RadioSetModem; RadioSetRxConfig; RadioRx; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioIrqProcess; RadioSetChannel; RadioSetTxConfig; RadioSetTxConfig; RadioStandby; RadioSetModem; RadioSend; RadioSend; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioStandby; RadioSetChannel; RadioSetRxConfig; RadioStandby; RadioSetModem; RadioSetRxConfig; RadioRx; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioIrqProcess; RadioSetChannel; RadioSetTxConfig; RadioSetTxConfig; RadioStandby; RadioSetModem; RadioSend; RadioSend; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioStandby; RadioSetChannel; RadioSetRxConfig; RadioStandby; RadioSetModem; RadioSetRxConfig; RadioRx; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioIrqProcess; RadioSetChannel; RadioSetTxConfig; RadioSetTxConfig; RadioStandby; RadioSetModem; RadioSend; RadioSend; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioStandby; RadioSetChannel; RadioSetRxConfig; RadioStandby; RadioSetModem; RadioSetRxConfig; RadioRx; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioIrqProcess; RadioSetChannel; RadioSetTxConfig; RadioSetTxConfig; RadioStandby; RadioSetModem; RadioSend; RadioSend; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioStandby; RadioSetChannel; RadioSetRxConfig; RadioStandby; RadioSetModem; RadioSetRxConfig; RadioRx; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioIrqProcess; RadioSetChannel; RadioSetTxConfig; RadioSetTxConfig; RadioStandby; RadioSetModem; RadioSend; RadioSend; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioStandby; RadioSetChannel; RadioSetRxConfig; RadioStandby; RadioSetModem; RadioSetRxConfig; RadioRx; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioIrqProcess; RadioSetChannel; RadioSetTxConfig; RadioSetTxConfig; RadioStandby; RadioSetModem; RadioSend; RadioSend; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioStandby; RadioSetChannel; RadioSetRxConfig; RadioStandby; RadioSetModem; RadioSetRxConfig; RadioRx; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioIrqProcess; RadioSetChannel; RadioSetTxConfig; RadioSetTxConfig; RadioStandby; RadioSetModem; RadioSend; RadioSend; RadioIrqProcess; RadioIrqProcess; RadioSleep; RadioStandby; RadioSetChannel; RadioSetRxConfig; RadioStandby; RadioSetModem; RadioSetRxConfig; RadioRx; RadioIrqProcess; RadioIrqProcess; RadioIrqProcess; RadioSleep; end; subgraph SX1262; SX126xIoInit; SX126xSetTx; SX126xSetPaConfig; SX126xSetTx; SX126xSetPaConfig; SX126xSetTx; SX126xSetPaConfig; SX126xSetTx; SX126xSetPaConfig; SX126xSetTx; SX126xSetPaConfig; SX126xSetTx; SX126xSetPaConfig; SX126xSetTx; SX126xSetPaConfig; SX126xSetTx; SX126xSetPaConfig; SX126xSetTx; SX126xSetPaConfig; SX126xSetTx; SX126xSetPaConfig; SX126xSetTx; SX126xSetPaConfig; SX126xSetTx; SX126xSetPaConfig; SX126xSetTx; SX126xSetPaConfig; SX126xSetTx; SX126xSetPaConfig; end; subgraph NimBLE; TimerInit; TimerInit; TimerInit; TimerInit; TimerInit; TimerInit; TimerInit; TimerInit; TimerInit; TimerInit; TimerInit; TimerInit; TimerInit; TimerStop; TimerStart; TimerInit; TimerSetValue; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStop; TimerStart; TimerStop; TimerStop; TimerStop; TimerStart; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStop; TimerStart; TimerStop; TimerStop; TimerStop; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStart; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStop; TimerStart; TimerStop; TimerStop; TimerStop; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStart; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStop; TimerStart; TimerStop; TimerStop; TimerStop; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStart; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStop; TimerStart; TimerStop; TimerStop; TimerStop; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStart; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStop; TimerStart; TimerStop; TimerStop; TimerStop; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStart; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStop; TimerStart; TimerStop; TimerStop; TimerStop; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStart; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStop; TimerStart; TimerStop; TimerStop; TimerStop; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStart; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStop; TimerStart; TimerStop; TimerStop; TimerStop; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStart; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStop; TimerStart; TimerStop; TimerStop; TimerStop; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStart; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStop; TimerStart; TimerStop; TimerStop; TimerStop; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStart; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStop; TimerStart; TimerStop; TimerStop; TimerStop; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStart; TimerStop; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerSetValue; TimerStart; TimerStop; TimerStart; TimerStop; TimerStop; TimerStart; TimerStop; TimerStop; TimerStop; end; Start-->LoRaMacInitialization; LoRaMacInitialization-->TimerInit; TimerInit-->SX126xIoInit; SX126xIoInit-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->TimerInit; TimerInit-->RadioSetModem; RadioSetModem-->RadioSetPublicNetwork; RadioSetPublicNetwork-->RadioSleep; RadioSleep-->RadioSetModem; RadioSetModem-->RadioSetPublicNetwork; RadioSetPublicNetwork-->TimerInit; TimerInit-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerInit; TimerInit-->TimerSetValue; TimerSetValue-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->LmHandlerSend; LmHandlerSend-->RadioSetChannel; RadioSetChannel-->RadioSetTxConfig; RadioSetTxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->SX126xSetTx; SX126xSetTx-->SX126xSetPaConfig; SX126xSetPaConfig-->RadioSend; RadioSend-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerSetValue; TimerSetValue-->TimerStart; TimerStart-->TimerStop; TimerStop-->TimerStart; TimerStart-->TimerStop; TimerStop-->RadioStandby; RadioStandby-->RadioSetChannel; RadioSetChannel-->RadioSetRxConfig; RadioSetRxConfig-->RadioStandby; RadioStandby-->RadioSetModem; RadioSetModem-->RadioSetRxConfig; RadioSetRxConfig-->RadioRx; RadioRx-->TimerStop; TimerStop-->TimerStart; TimerStart-->RadioIrqProcess; RadioIrqProcess-->TimerStop; TimerStop-->RadioIrqProcess; RadioIrqProcess-->RadioSleep; RadioSleep-->TimerStop; TimerStop-->End; ``` # Constrain the Call Graph TODO # Out Of Memory Zig Compiler crashes when we run this code to group the C Functions by Module... https://github.com/lupyuen/zig-bl602-nuttx/blob/f5a5c824e07e5fc984136069a1769edda4967c77/reflect.zig#L825-L1086 Here is the Output Log... https://gist.github.com/lupyuen/5738abefa9d4c138e9d731e22d01500f On macOS, Zig Compiler consumes over 34 GB of memory and crashes... ![Too Much Memory](https://lupyuen.github.io/images/Screenshot%202022-06-21%20at%208.03.46%20AM.png) (On WSL, Zig Compiler hangs the WSL process when it consumes over 4 GB of memory) This happens because our code loops repeatedly over __4,700 C Declarations__ while processing __1,500 lines of Raw Call Logs__. Let's optimise our code. # Fix Out Of Memory Our code searches for a C Declaration by looping repeatedly over __4,700 C Declarations__ like so... ```zig /// Return the C Declaration Index for the Function Name. /// We match the C Declaration Name against the start of the Function Name. /// This is the slower, unfiltered version that searches all C Declarations. fn get_decl_by_name(name: []const u8) ?usize { comptime { const T = @typeInfo(c); // For every C Declaration... for (T.Struct.decls) |decl, i| { if (std.mem.eql(u8, decl.name, "Radio")) { continue; } // Skip Radio // If the C Declaration starts with the Function Name... if (std.mem.startsWith(u8, name, decl.name)) { // Return the C Declaration Index return i; } } // End of C Declaration return null; // Not found } } ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/6d428422bf83b45ffe33e4ea9ce2919f812ad3bb/reflect.zig#L1024-L1044) Which causes Zig Compiler to crash with Out Of Memory. But we don't actually need to loop through all the C Declarations! According to our list of Modules, we call only __173 Functions__. Let's fix the above function so that we loop over the 173 Functions instead... ```zig /// Return the C Declaration Index for the Function Name. /// We match the C Declaration Name against the start of the Function Name. /// This is the faster, filtered version that searches C Declarations by Modules. fn get_decl_by_name_filtered(all_modules: []Module, name: []const u8) ?usize { comptime { const T = @typeInfo(c); // Search all Modules for the Function Name for (all_modules) |module| { // For every C Declaration in the Module... var decl_index = module.first_index; while (decl_index <= module.last_index) { // Get the C Declaration var decl = T.Struct.decls[decl_index]; if (std.mem.eql(u8, decl.name, "Radio")) { continue; } // Skip Radio // If the C Declaration starts with the Function Name... if (std.mem.startsWith(u8, name, decl.name)) { // Return the C Declaration Index return decl_index; } decl_index += 1; } // End of C Declaration } // End of Module return null; // Not found } } ``` [(Source)](https://github.com/lupyuen/zig-bl602-nuttx/blob/6d428422bf83b45ffe33e4ea9ce2919f812ad3bb/reflect.zig#L1046-L1075) And our code runs OK with Zig Compiler! (macOS, not WSL though) # Wishlist for Zig Compiler Building a Call Graph at Compile-Time with Zig Compiler looks pretty challenging. I hope that future versions of the Zig Compiler will support the following... - Type Reflection for C Declarations will provide the full path of source files (Makes it easier to identify the module for each declaration) - `@compileLog` will emit any kind of strings (So that `@compileLog` can generate proper Mermaid.js strings like "`Start-->LoRaMacInitialization;`")