Read the articles...
Can we use Scratch / Blockly to code Zig programs, the drag-n-drop way?
Let's create a Visual Programming Tool for Zig that will generate IoT Sensor Apps with Apache NuttX RTOS.
Why limit to IoT Sensor Apps?
-
Types are simpler: Only floating-point numbers will be supported, no strings needed
-
Blockly is Typeless. With Zig we can use Type Inference to deduce the missing Struct Types
-
Make it easier to experiment with various IoT Sensors: Temperature, Humidity, Air Pressure, ...
Blockly Source Code: lupyuen3/blockly-zig-nuttx
We start with the Sensor Test App (in C) from Apache NuttX RTOS: sensortest.c
Here are the steps for reading a NuttX Sensor...
// From https://lupyuen.github.io/articles/bme280#sensor-test-app
// Open the Sensor Device.
// devname looks like "/dev/uorb/sensor_baro0" or "/dev/uorb/sensor_humi0"
fd = open(devname, O_RDONLY | O_NONBLOCK);
// Set Standby Interval
ioctl(fd, SNIOC_SET_INTERVAL, interval);
// Set Batch Latency
ioctl(fd, SNIOC_BATCH, latency);
// If Sensor Data is available...
if (poll(&fds, 1, -1) > 0) {
// Read the Sensor Data
if (read(fd, buffer, len) >= len) {
// Cast buffer as Barometer Sensor Data
struct sensor_event_baro *event =
(struct sensor_event_baro *) buffer;
// Handle Pressure and Temperature
printf(
"%s: timestamp:%d value1:%.2f value2:%.2f\n",
name,
event.timestamp,
event.pressure,
event.temperature
);
}
}
// Close the Sensor Device and free the buffer
close(fd);
free(buffer);
NuttX compiles the Sensor Test App sensortest.c with this GCC command...
## App Source Directory
cd $HOME/nuttx/apps/testing/sensortest
## Compile sensortest.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/include" \
-Dmain=sensortest_main \
sensortest.c \
-o sensortest.c.home.user.nuttx.apps.testing.sensortest.o
(Observed from make --trace
)
Let's convert the Sensor Test App from C to Zig...
The Zig Compiler can auto-translate C code to Zig. (See this)
Here's how we auto-translate our Sensor App sensortest.c from C to Zig...
-
Take the GCC command from above
-
Change
riscv64-unknown-elf-gcc
tozig translate-c
-
Add the target
-target riscv32-freestanding-none -mcpu=baseline_rv32-d
-
Remove
-march=rv32imafc
-
Surround the C Flags by
-cflags
...--
Like this...
## App Source Directory
cd $HOME/nuttx/apps/testing/sensortest
## Auto-translate sensortest.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/include" \
-Dmain=sensortest_main \
sensortest.c \
>sensortest.zig
To fix the translation (Zig Translate doesn't support goto
) we need to insert this...
#ifdef __clang__ // Workaround for zig cc
#include <arch/types.h>
#include "../../nuttx/include/limits.h"
#define goto return ret;
#define name_err
#define open_err
#define ctl_err
#endif // __clang__
And change this...
#ifndef __clang__ // Workaround for NuttX with zig cc
ctl_err:
#endif // !__clang__
close(fd);
#ifndef __clang__ // Workaround for NuttX with zig cc
open_err:
#endif // !__clang__
free(buffer);
#ifndef __clang__ // Workaround for NuttX with zig cc
name_err:
#endif // !__clang__
Here's the original C code: sensortest.c
And the auto-translation from C to Zig: translated/sensortest.zig
We copy the relevant snippets from the Auto-Translation to create our Zig Sensor App: sensortest.zig
visual-zig-nuttx/sensortest.zig
Lines 32 to 417 in 0d3617d
Then we compile our Zig Sensor App...
## Download our Zig Sensor App for NuttX
git clone --recursive https://github.com/lupyuen/visual-zig-nuttx
cd visual-zig-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/include" \
sensortest.zig
## Patch the ELF Header of `sensortest.o` from Soft-Float ABI to Hard-Float ABI
xxd -c 1 sensortest.o \
| sed 's/00000024: 01/00000024: 03/' \
| xxd -r -c 1 - sensortest2.o
cp sensortest2.o sensortest.o
## Copy the compiled app to NuttX and overwrite `sensortest.o`
## TODO: Change "$HOME/nuttx" to your NuttX Project Directory
cp sensortest.o $HOME/nuttx/apps/testing/sensortest/sensortest*.o
## Build NuttX to link the Zig Object from `sensortest.o`
## TODO: Change "$HOME/nuttx" to your NuttX Project Directory
cd $HOME/nuttx/nuttx
make
For testing the Zig Sensor App, we connect the BME280 Sensor (Temperature / Humidity / Air Pressure) to Pine64's PineCone BL602 Board...
BL602 Pin | BME280 Pin | Wire Colour |
---|---|---|
GPIO 1 |
SDA |
Green |
GPIO 2 |
SCL |
Blue |
3V3 |
3.3V |
Red |
GND |
GND |
Black |
To read the BME280 Sensor, let's run the Zig Sensor App: sensortest.zig
First we read the Humidity with our Zig Sensor App...
NuttShell (NSH) NuttX-10.3.0
nsh> sensortest -n 1 humi0
Zig Sensor Test
bme280_fetch: temperature=30.520000 °C, pressure=1027.211548 mbar, humidity=72.229492 %
humi0: timestamp:109080000 value:72.23
Our Zig App returns the correct Humidity (value
): 72 %.
Next we read the Temperature and Air Pressure with our Zig Sensor App...
nsh> sensortest -n 1 baro0
Zig Sensor Test
bme280_fetch: temperature=30.520000 °C, pressure=1029.177490 mbar, humidity=72.184570 %
baro0: timestamp:78490000 value1:72.18 value2:72.18
This shows that the BME280 Driver has fetched the correct Temperature (30 °C), Pressure (1,029 mbar) and Humidity (72 %).
But our Zig App returns both Pressure (value1
) and Temperature (value2
) as 72. Which is incorrect.
Compare the above output with the original C Version of the Sensor App: sensortest.c
nsh> sensortest -n 1 humi0
bme280_fetch: temperature=30.690001 °C, pressure=1027.283447 mbar, humidity=72.280273 %
humi0: timestamp:26000000 value:72.28
nsh> sensortest -n 1 baro0
bme280_fetch: temperature=30.690001 °C, pressure=1029.177368 mbar, humidity=72.257813 %
baro0: timestamp:14280000 value1:1029.18 value2:30.69
We see that Humidity (value
), Pressure (value1
) and Temperature (value2
) are returned correctly by the C Version of the Sensor App.
Something got messed up in the Auto-Translation from C (sensortest.c) to Zig (sensortest.zig). Let's find out why...
(Note: We observed this issue with Zig Compiler version 0.10.0, it might have been fixed in later versions of the compiler)
Earlier we saw that our Zig Sensor App printed the incorrect Sensor Values for Pressure (value1
) and Temperature (value2
)...
nsh> sensortest -n 1 baro0
Zig Sensor Test
bme280_fetch: temperature=30.520000 °C, pressure=1029.177490 mbar, humidity=72.184570 %
baro0: timestamp:78490000 value1:72.18 value2:72.18
Zig seems to have a problem passing the Pressure and Temperature values (both f32
) to printf
, based on this Auto-Translated Zig Code...
fn print_valf2(buffer: [*c]const u8, name: [*c]const u8) void {
const event = @intToPtr([*c]c.struct_sensor_baro, @ptrToInt(buffer));
_ = printf("%s: timestamp:%llu value1:%.2f value2:%.2f\n",
name,
event.*.timestamp,
@floatCast(f64, event.*.pressure),
@floatCast(f64, event.*.temperature)
);
}
The workaround is to convert the Float values to Integer AND split into two calls to printf
...
fn print_valf2(buffer: [*c]const u8, name: [*c]const u8) void {
const event = @intToPtr([*c]c.struct_sensor_baro, @ptrToInt(buffer));
_ = printf("%s: timestamp:%llu ",
name,
event.*.timestamp,
);
_ = printf("value1:%d value2:%d\n",
@floatToInt(i32, event.*.pressure),
@floatToInt(i32, event.*.temperature)
);
}
Now our Zig Sensor App prints the correct values, but truncated as Integers...
nsh> sensortest -n 1 baro0
Zig Sensor Test
SensorTest: Test /dev/uorb/sensor_baro0 with interval(1000000us), latency(0us)
baro0: timestamp:42610000 value1:1003 value2:31
SensorTest: Received message: baro0, number:1/1
nsh> sensortest -n 1 humi0
Zig Sensor Test
SensorTest: Test /dev/uorb/sensor_humi0 with interval(1000000us), latency(0us)
humi0: timestamp:32420000 value:68
SensorTest: Received message: humi0, number:1/1
Since printf
works OK with Integers, let's print the Floats as Integers with 2 decimal places...
/// Print the float with 2 decimal places.
/// We print as integers because `printf` has a problem with floats.
fn print_float(f: f32) void {
const scaled = @floatToInt(i32, f * 100);
const f1 = @divTrunc(scaled, 100);
const f2 = @mod(scaled, 100);
_ = printf("%d.%02d", f1, f2);
}
Then we pass the Floats to print_float
for printing...
fn print_valf2(buffer: [*c]const u8, name: [*c]const u8) void {
const event = @intToPtr([*c]c.struct_sensor_baro, @ptrToInt(buffer));
_ = printf("%s: timestamp:%llu ",
name,
event.*.timestamp,
);
_ = printf("value1:"); print_float(event.*.pressure);
_ = printf(" value2:"); print_float(event.*.temperature);
_ = printf("\n");
}
Finally we see the Pressure (value1
), Temperature (value2
) and Humidity (value
) printed correctly with 2 decimal places!
nsh> sensortest -n 1 baro0
Zig Sensor Test
SensorTest: Test /dev/uorb/sensor_baro0 with interval(1000000us), latency(0us)
baro0: timestamp:17780000 value1:1006.12 value2:29.65
SensorTest: Received message: baro0, number:1/1
nsh> sensortest -n 1 humi0
Zig Sensor Test
SensorTest: Test /dev/uorb/sensor_humi0 with interval(1000000us), latency(0us)
humi0: timestamp:28580000 value:77.44
SensorTest: Received message: humi0, number:1/1
Have we tried other options for @floatCast
?
Yes we tested these options for @floatCast
...
_ = printf("value1 no floatCast: %f\n", event.*.pressure);
_ = printf("value1 floatCast f32: %f\n", @floatCast(f32, event.*.pressure));
_ = printf("value1 floatCast f64: %f\n", @floatCast(f64, event.*.pressure));
But the result is incorrect...
nsh> sensortest -n 1 baro0
Zig Sensor Test
baro0: timestamp:60280000 value1:1006.90 value2:30.79
value1 no floatCast: 0.000000
value1 floatCast f32: 0.000000
value1 floatCast f64: 30.790001
Because value1
(Pressure) is supposed to be 1006, not 30.
Robert Lipe suggests that we might have a problem with the RISC-V Calling Conventions for Floats and Doubles...
The calling conventions for RISC-V on small computers for floats/doubles is weird and the software/emulators/ libs are confusing. Verify that floats get promoted to doubles in the call and that they go to F0, F2, F4, etc. in the frame, properly aligned. Double check complr flags
See, e.g., https://github.com/riscv-non-isa/riscv-elf-psabi-doc/blob/master/riscv-cc.adoc and related. This is totally the kind of thing that small parts (enable FP on BL602; disable s/w libs) and x-language can get wrong.
Instead of printf
, why not call the Zig Debug Logger debug
?
debug("pressure: {}", .{ event.*.pressure });
debug("temperature: {}", .{ event.*.temperature });
This causes a Linker Error, as explained below...
(Note: We observed this issue with Zig Compiler version 0.10.0, it might have been fixed in later versions of the compiler)
When we call the Zig Debug Logger debug
to print Floating-Point Numbers (32-bit)...
var event = @intToPtr([*c]c.struct_sensor_baro, @ptrToInt(buffer));
debug("pressure: {}", .{ event.*.pressure });
debug("temperature: {}", .{ event.*.temperature });
It fails to link...
riscv64-unknown-elf-ld: nuttx/nuttx/staging/libapps.a(sensortest.c.home.user.nuttx.apps.testing.sensortest.o): in function `std.fmt.errol.errolInt':
zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:305:
undefined reference to `__fixunsdfti'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:305:
undefined reference to `__floatuntidf'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:315:
undefined reference to `__umodti3'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:316: undefined reference to `__udivti3'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:316:
undefined reference to `__umodti3'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:318:
undefined reference to `__umodti3'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:319:
undefined reference to `__udivti3'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:319:
undefined reference to `__umodti3'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:324:
undefined reference to `__udivti3'
riscv64-unknown-elf-ld: zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:335:
undefined reference to `__udivti3'
riscv64-unknown-elf-ld: nuttx/nuttx/staging/libapps.a(sensortest.c.home.user.nuttx.apps.testing.sensortest.o): in function `std.fmt.errol.fpeint':
zig-linux-x86_64-0.10.0-dev.2351+b64a1d5ab/lib/std/fmt/errol.zig:677:
undefined reference to `__ashlti3'
But printing as Integers with debug
works OK...
debug("pressure: {}",
.{ @floatToInt(i32, event.*.pressure) });
debug("temperature: {}",
.{ @floatToInt(i32, event.*.temperature) });
So we print Floats as Integers with the Debug Logger...
Earlier we saw that Zig Debug Logger debug
won't print Floating-Point Numbers. (Due to a Linker Error)
Let's convert Floating-Point Numbers to Fixed-Point Numbers (2 decimal places) and print as Integers instead...
/// Convert the float to a fixed-point number (`int`.`frac`) with 2 decimal places.
/// We do this because `debug` has a problem with floats.
pub fn floatToFixed(f: f32) struct { int: i32, frac: u8 } {
const scaled = @floatToInt(i32, f * 100.0);
const rem = @rem(scaled, 100);
const rem_abs = if (rem < 0) -rem else rem;
return .{
.int = @divTrunc(scaled, 100),
.frac = @intCast(u8, rem_abs),
};
}
This is how we print Floating-Point Numbers as Fixed-Point Numbers...
/// Print 2 floats
fn print_valf2(buffer: []const align(8) u8, name: []const u8) void {
_ = name;
const event = @ptrCast(*const c.struct_sensor_baro, &buffer[0]);
const pressure = floatToFixed(event.*.pressure);
const temperature = floatToFixed(event.*.temperature);
debug("value1:{}.{:0>2}", .{ pressure.int, pressure.frac });
debug("value2:{}.{:0>2}", .{ temperature.int, temperature.frac });
}
Originally we allocated the Sensor Data Buffer from the Heap via calloc
...
// Allocate buffer from heap
len = @bitCast(c_int, @as(c_uint, g_sensor_info[@intCast(c_uint, idx)].esize));
buffer = @ptrCast([*c]u8, @alignCast(std.meta.alignment(u8),
c.calloc(@bitCast(usize, @as(c_int, 1)), @bitCast(usize, len))));
...
// Read into heap buffer
if (c.read(fd, @ptrCast(?*anyopaque, buffer), @bitCast(usize, len)) >= len) {
g_sensor_info[@intCast(c_uint, idx)].print.?(buffer, name);
}
...
// Deallocate heap buffer
c.free(@ptrCast(?*anyopaque, buffer));
To make this a little safer, we switched to a Static Buffer...
/// Sensor Data Buffer
/// (Aligned to 8 bytes because it's passed to C)
var sensor_data align(8) = std.mem.zeroes([256]u8);
So that we no longer worry about deallocating the buffer...
// Use static buffer
len = @bitCast(c_int, @as(c_uint, g_sensor_info[@intCast(c_uint, idx)].esize));
assert(sensor_data.len >= len);
...
// Read into static buffer
if (c.read(fd, @ptrCast(?*anyopaque, &sensor_data), @bitCast(usize, len)) >= len) {
g_sensor_info[@intCast(c_uint, idx)].print.?(&sensor_data, name);
}
...
// No need to deallocate buffer
We also moved the Device Name from the Stack...
/// Main Function that will be called by NuttX. We read the Sensor Data from a Sensor.
pub export fn sensortest_main(...) {
var devname: [c.PATH_MAX]u8 = undefined;
To a Static Buffer...
/// Device Name, like "/dev/uorb/sensor_baro0" or "/dev/uorb/sensor_humi0"
/// (Aligned to 8 bytes because it's passed to C)
var devname align(8) = std.mem.zeroes([c.PATH_MAX]u8);
And we removed the Alignment from Device Name...
fd = c.open(
@ptrCast([*c]u8, @alignCast(std.meta.alignment(u8), &devname)),
c.O_RDONLY | c.O_NONBLOCK
);
So it looks like this...
fd = c.open(
&devname[0],
c.O_RDONLY | c.O_NONBLOCK
);
When we align the Sensor Data Buffer to 4 bytes...
/// Sensor Data Buffer
/// (Aligned to 4 bytes because it's passed to C)
var sensor_data align(4) = std.mem.zeroes([256]u8);
It triggers a Zig Panic due to Incorrect Aligment...
nsh> sensortest -n 1 baro0
Zig Sensor Test
SensorTest: Test /dev/uorb/sensor_baro0 with interval(1000000us), latency(0us)
!ZIG PANIC!
incorrect alignment
Stack Trace:
0x23014f4c
0x230161e2
RISC-V Disassembly shows that it's checking andi a0,a0,7
...
nuttx/visual-zig-nuttx/sensortest.zig:196
const event = @intToPtr([*c]c.struct_sensor_baro, @ptrToInt(buffer));
23014f2a: 85aa mv a1,a0
23014f2c: fcb42e23 sw a1,-36(s0)
23014f30: 891d andi a0,a0,7
23014f32: 4581 li a1,0
23014f34: 00b50c63 beq a0,a1,23014f4c <print_valf2+0x32>
23014f38: a009 j 23014f3a <print_valf2+0x20>
23014f3a: 23068537 lui a0,0x23068
23014f3e: 3c850513 addi a0,a0,968 # 230683c8 <__unnamed_6>
23014f42: 4581 li a1,0
23014f44: 00000097 auipc ra,0x0
23014f48: dd4080e7 jalr -556(ra) # 23014d18 <panic>
23014f4c: fdc42503 lw a0,-36(s0)
23014f50: fea42a23 sw a0,-12(s0)
(Last 3 bits of address must be 0)
Which means that the buffer address must be aligned to 8 bytes...
/// Sensor Data Buffer
/// (Aligned to 8 bytes because it's passed to C)
var sensor_data align(8) = std.mem.zeroes([256]u8);
Probably because struct_sensor_baro
contains a timestamp
field that's a 64-bit Integer.
The Auto-Translation from C to Zig looks super verbose and strips away the Macro Constants...
// Parse the Command-Line Options
g_should_exit = @as(c_int, 0) != 0;
while ((blk: {
const tmp = c.getopt(argc, argv, "i:b:n:h");
ret = tmp;
break :blk tmp;
}) != -@as(c_int, 1)) {
while (true) {
switch (ret) {
@as(c_int, 105) => {
interval = @bitCast(c_uint, @truncate(c_uint, c.strtoul(c.getoptargp().*, null, @as(c_int, 0))));
break;
},
@as(c_int, 98) => {
latency = @bitCast(c_uint, @truncate(c_uint, c.strtoul(c.getoptargp().*, null, @as(c_int, 0))));
break;
},
@as(c_int, 110) => {
count = @bitCast(c_uint, @truncate(c_uint, c.strtoul(c.getoptargp().*, null, @as(c_int, 0))));
break;
},
else => {
usage();
return ret;
},
}
break;
}
}
We clean up the Zig code like this...
// Parse the Command-Line Options
g_should_exit = false;
while (true) {
ret = c.getopt(argc, argv, "i:b:n:h");
if (ret == c.EOF) { break; }
switch (ret) {
'i' => { interval = c.strtoul(c.getoptargp().*, null, 0); },
'b' => { latency = c.strtoul(c.getoptargp().*, null, 0); },
'n' => { count = c.strtoul(c.getoptargp().*, null, 0); },
else => { usage(); return ret; },
}
}
So that it resembles the original C code...
g_should_exit = false;
while ((ret = getopt(argc, argv, "i:b:n:h")) != EOF)
{
switch (ret)
{
case 'i':
interval = strtoul(optarg, NULL, 0);
break;
case 'b':
latency = strtoul(optarg, NULL, 0);
break;
case 'n':
count = strtoul(optarg, NULL, 0);
break;
case 'h':
default:
usage();
goto name_err;
}
}
Our Zig Sensor App was originally this...
visual-zig-nuttx/sensortest.zig
Lines 35 to 170 in 4ccb0cd
After cleanup becomes this...
visual-zig-nuttx/multisensor.zig
Lines 15 to 150 in 692b5e5
We test again to be sure that the Zig Sensor App is still working OK...
NuttShell (NSH) NuttX-10.3.0
nsh> uname -a
NuttX 10.3.0 32c8fdf272 Jul 18 2022 16:38:47 risc-v bl602evb
nsh> sensortest -n 1 baro0
Zig Sensor Test
test_multisensor
SensorTest: Test /dev/uorb/sensor_baro0 with interval(1000000), latency(0)
value1:1007.65
value2:27.68
SensorTest: Received message: baro0, number:1/1
nsh> sensortest -n 1 humi0
Zig Sensor Test
test_multisensor
SensorTest: Test /dev/uorb/sensor_humi0 with interval(1000000), latency(0)
value:78.91
SensorTest: Received message: humi0, number:1/1
We also check that errors are handled correctly...
nsh> sensortest -n 1 baro
Zig Sensor Test
test_multisensor
Failed to open device:/dev/uorb/sensor_baro , ret:No such file or directory
nsh> sensortest -n 1 invalid
Zig Sensor Test
test_multisensor
The sensor node name:invalid is invalid
sensortest test
Test barometer sensor (/dev/uorb/sensor_baro0)
sensortest test2
Test humidity sensor (/dev/uorb/sensor_humi0)
sensortest [arguments...] <command>
[-h ] sensortest commands help
[-i <val>] The output data period of sensor in us
default: 1000000
[-b <val>] The maximum report latency of sensor in us
default: 0
[-n <val>] The number of output data
default: 0
Commands:
<sensor_node_name> ex, accel0(/dev/uorb/sensor_accel0)
nsh> sensortest
Zig Sensor Test
sensortest test
Test barometer sensor (/dev/uorb/sensor_baro0)
sensortest test2
Test humidity sensor (/dev/uorb/sensor_humi0)
sensortest [arguments...] <command>
[-h ] sensortest commands help
[-i <val>] The output data period of sensor in us
default: 1000000
[-b <val>] The maximum report latency of sensor in us
default: 0
[-n <val>] The number of output data
default: 0
Commands:
<sensor_node_name> ex, accel0(/dev/uorb/sensor_accel0)
Reading Sensor Data from a single sensor looks a lot simpler (because we don't need to cast the Sensor Data)...
// Omitted: Open the Sensor Device, enable the Sensor and poll for Sensor Data
...
// Define the Sensor Data Type
var sensor_data = std.mem.zeroes(c.struct_sensor_baro);
const len = @sizeOf(@TypeOf(sensor_data));
// Read the Sensor Data
if (c.read(fd, &sensor_data, len) >= len) {
// Convert the Sensor Data to Fixed-Point Numbers
const pressure = floatToFixed(sensor_data.pressure);
const temperature = floatToFixed(sensor_data.temperature);
// Print the Sensor Data
debug("pressure:{}.{:0>2}", .{
pressure.int,
pressure.frac
});
debug("temperature:{}.{:0>2}", .{
temperature.int,
temperature.frac
});
}
Here's the Air Pressure and Temperature read from the BME280 Barometer Sensor...
nsh> sensortest test
Zig Sensor Test
test_sensor
pressure:1007.66
temperature:27.70
To read a Humidity Sensor (like BME280), the code looks highly similar...
// Define the Sensor Data Type
var sensor_data = std.mem.zeroes(c.struct_sensor_humi);
const len = @sizeOf(@TypeOf(sensor_data));
// Read the Sensor Data
if (c.read(fd, &sensor_data, len) >= len) {
// Convert the Sensor Data to Fixed-Point Numbers
const humidity = floatToFixed(sensor_data.humidity);
// Print the Sensor Data
debug("humidity:{}.{:0>2}", .{
humidity.int,
humidity.frac
});
}
Here's the Humidity read from the BME280 Humidity Sensor...
nsh> sensortest test2
Zig Sensor Test
test_sensor2
humidity:78.81
With Zig Generics and comptime
, we can greatly simplify the reading of Sensor Data...
// Read the Temperature
const temperature: f32 = try sen.readSensor(
c.struct_sensor_baro, // Sensor Data Struct to be read
"temperature", // Sensor Data Field to be returned
"/dev/uorb/sensor_baro0" // Path of Sensor Device
);
// Print the Temperature
debug("temperature={}", .{ floatToFixed(temperature) });
Here's the implementation of readSensor
...
Lines 34 to 108 in 1bb1c69
Note that the Sensor Data Struct Type and the Sensor Data Field are declared as comptime
...
/// Read a Sensor and return the Sensor Data
pub fn readSensor(
comptime SensorType: type, // Sensor Data Struct to be read, like c.struct_sensor_baro
comptime field_name: []const u8, // Sensor Data Field to be returned, like "temperature"
device_path: []const u8 // Path of Sensor Device, like "/dev/uorb/sensor_baro0"
) !f32 { ...
Which means that the values will be substituted at Compile-Time. (Works like a C Macro)
We can then refer to the Sensor Data Struct sensor_baro
like this...
// Define the Sensor Data Type
var sensor_data = std.mem.zeroes(
SensorType
);
And return a field temperature
like this...
// Return the Sensor Data Field
return @field(sensor_data, field_name);
Thus this program...
Lines 27 to 61 in a7404ea
Produces this output...
NuttShell (NSH) NuttX-10.3.0
nsh> sensortest visual
Zig Sensor Test
Start main
...
temperature=30.18
pressure=1007.69
humidity=68.67
End main
Blockly will emit the Zig code below for a typical IoT Sensor App: visual.zig
// Read Temperature from BME280 Sensor
const temperature = try sen.readSensor( // Read BME280 Sensor
c.struct_sensor_baro, // Sensor Data Struct
"temperature", // Sensor Data Field
"/dev/uorb/sensor_baro0" // Path of Sensor Device
);
// Read Pressure from BME280 Sensor
const pressure = try sen.readSensor( // Read BME280 Sensor
c.struct_sensor_baro, // Sensor Data Struct
"pressure", // Sensor Data Field
"/dev/uorb/sensor_baro0" // Path of Sensor Device
);
// Read Humidity from BME280 Sensor
const humidity = try sen.readSensor( // Read BME280 Sensor
c.struct_sensor_humi, // Sensor Data Struct
"humidity", // Sensor Data Field
"/dev/uorb/sensor_humi0" // Path of Sensor Device
);
// Compose CBOR Message with Temperature, Pressure and Humidity
const msg = try composeCbor(.{
"t", temperature,
"p", pressure,
"h", humidity,
});
// Transmit message to LoRaWAN
try transmitLorawan(msg);
This reads the Temperature, Pressure and Humidity from BME280 Sensor, composes a CBOR Message that's encoded with the Sensor Data, and transmits the CBOR Message to LoRaWAN.
composeCbor
will work for a variable number of arguments? Strings as well as numbers?
Yep, here's the implementation of composeCbor
: visual.zig
/// TODO: Compose CBOR Message with Key-Value Pairs
/// https://lupyuen.github.io/articles/cbor2
fn composeCbor(args: anytype) !CborMessage {
debug("composeCbor", .{});
comptime {
assert(args.len % 2 == 0); // Missing Key or Value
}
// Process each field...
comptime var i: usize = 0;
var msg = CborMessage{};
inline while (i < args.len) : (i += 2) {
// Get the key and value
const key = args[i];
const value = args[i + 1];
// Print the key and value
debug(" {s}: {}", .{
@as([]const u8, key),
floatToFixed(value)
});
// Format the message for testing
var slice = std.fmt.bufPrint(
msg.buf[msg.len..],
"{s}:{},",
.{
@as([]const u8, key),
floatToFixed(value)
}
) catch { _ = std.log.err("Error: buf too small", .{}); return error.Overflow; };
msg.len += slice.len;
}
debug(" msg={s}", .{ msg.buf[0..msg.len] });
return msg;
}
/// TODO: CBOR Message
/// https://lupyuen.github.io/articles/cbor2
const CborMessage = struct {
buf: [256]u8 = undefined, // Limit to 256 chars
len: usize = 0,
};
Note that composeCbor
is declared as anytype
...
fn composeCbor(args: anytype) { ...
That's why composeCbor
accepts a variable number of arguments with different types.
To handle each argument, this inline
/ comptime
loop is unrolled at Compile-Time...
// Process each field...
comptime var i: usize = 0;
inline while (i < args.len) : (i += 2) {
// Get the key and value
const key = args[i];
const value = args[i + 1];
// Print the key and value
debug(" {s}: {}", .{
@as([]const u8, key),
floatToFixed(value)
});
...
}
What happens if we omit a Key or a Value when calling composeCbor
?
This comptime
assertion check will fail at Compile-Time...
comptime {
assert(args.len % 2 == 0); // Missing Key or Value
}
Next we customise Blockly to generate the Zig Sensor App.
Read the article...
To test the Zig Sensor App generated by Blockly, let's build an IoT Sensor App that will read Temperature, Pressure and Humidity from BME280 Sensor, and transmit the values to LoRaWAN...
lupyuen3.github.io/blockly-zig-nuttx/demos/code
The Blocks above will emit this Zig program...
/// Main Function
pub fn main() !void {
// Every 10 seconds...
while (true) {
const temperature = try sen.readSensor( // Read BME280 Sensor
c.struct_sensor_baro, // Sensor Data Struct
"temperature", // Sensor Data Field
"/dev/uorb/sensor_baro0" // Path of Sensor Device
);
debug("temperature={}", .{ temperature });
const pressure = try sen.readSensor( // Read BME280 Sensor
c.struct_sensor_baro, // Sensor Data Struct
"pressure", // Sensor Data Field
"/dev/uorb/sensor_baro0" // Path of Sensor Device
);
debug("pressure={}", .{ pressure });
const humidity = try sen.readSensor( // Read BME280 Sensor
c.struct_sensor_humi, // Sensor Data Struct
"humidity", // Sensor Data Field
"/dev/uorb/sensor_humi0" // Path of Sensor Device
);
debug("humidity={}", .{ humidity });
const msg = try composeCbor(.{ // Compose CBOR Message
"t", temperature,
"p", pressure,
"h", humidity,
});
// Transmit message to LoRaWAN
try transmitLorawan(msg);
// Wait 10 seconds
_ = c.sleep(10);
}
}
(composeCbor
is explained here)
Copy the contents of the Main Function and paste here...
The generated Zig code should correctly read the Temperature, Pressure and Humidity from BME280 Sensor, and transmit the values to LoRaWAN...
NuttShell (NSH) NuttX-10.3.0
nsh> sensortest visual
Zig Sensor Test
Start main
temperature=31.05
pressure=1007.44
humidity=71.49
composeCbor
t: 31.05
p: 1007.44
h: 71.49
msg=t:31.05,p:1007.44,h:71.49,
transmitLorawan
msg=t:31.05,p:1007.44,h:71.49,
temperature=31.15
pressure=1007.40
humidity=70.86
composeCbor
t: 31.15
p: 1007.40
h: 70.86
msg=t:31.15,p:1007.40,h:70.86,
transmitLorawan
msg=t:31.15,p:1007.40,h:70.86,
temperature=31.16
pressure=1007.45
humidity=70.42
composeCbor
t: 31.16
p: 1007.45
h: 70.42
msg=t:31.16,p:1007.45,h:70.42,
transmitLorawan
msg=t:31.16,p:1007.45,h:70.42,
temperature=31.16
pressure=1007.47
humidity=70.39
composeCbor
t: 31.16
p: 1007.47
h: 70.39
msg=t:31.16,p:1007.47,h:70.39,
transmitLorawan
msg=t:31.16,p:1007.47,h:70.39,
temperature=31.19
pressure=1007.45
humidity=70.35
composeCbor
t: 31.19
p: 1007.45
h: 70.35
msg=t:31.19,p:1007.45,h:70.35,
transmitLorawan
msg=t:31.19,p:1007.45,h:70.35,
temperature=31.20
pressure=1007.42
humidity=70.65
composeCbor
t: 31.20
p: 1007.42
h: 70.65
msg=t:31.20,p:1007.42,h:70.65,
transmitLorawan
msg=t:31.20,p:1007.42,h:70.65,
(Tested with NuttX and BME280 on BL602)
To test the Zig program above on Linux / macOS / Windows (instead of NuttX), add the stubs below to simulate a NuttX Sensor...
/// Import Standard Library
const std = @import("std");
/// Main Function
pub fn main() !void {
// TODO: Paste here the contents of Zig Main Function generated by Blockly
...
}
/// Aliases for Standard Library
const assert = std.debug.assert;
const debug = std.log.debug;
///////////////////////////////////////////////////////////////////////////////
// CBOR Encoding
/// TODO: Compose CBOR Message with Key-Value Pairs
/// https://lupyuen.github.io/articles/cbor2
fn composeCbor(args: anytype) !CborMessage {
debug("composeCbor", .{});
comptime {
assert(args.len % 2 == 0); // Missing Key or Value
}
// Process each field...
comptime var i: usize = 0;
var msg = CborMessage{};
inline while (i < args.len) : (i += 2) {
// Get the key and value
const key = args[i];
const value = args[i + 1];
// Print the key and value
debug(" {s}: {}", .{
@as([]const u8, key),
floatToFixed(value)
});
// Format the message for testing
var slice = std.fmt.bufPrint(
msg.buf[msg.len..],
"{s}:{},",
.{
@as([]const u8, key),
floatToFixed(value)
}
) catch { _ = std.log.err("Error: buf too small", .{}); return error.Overflow; };
msg.len += slice.len;
}
debug(" msg={s}", .{ msg.buf[0..msg.len] });
return msg;
}
/// TODO: CBOR Message
/// https://lupyuen.github.io/articles/cbor2
const CborMessage = struct {
buf: [256]u8 = undefined, // Limit to 256 chars
len: usize = 0,
};
///////////////////////////////////////////////////////////////////////////////
// Transmit To LoRaWAN
/// TODO: Transmit message to LoRaWAN
fn transmitLorawan(msg: CborMessage) !void {
debug("transmitLorawan", .{});
debug(" msg={s}", .{ msg.buf[0..msg.len] });
}
///////////////////////////////////////////////////////////////////////////////
// Stubs
const c = struct {
const struct_sensor_baro = struct{};
const struct_sensor_humi = struct{};
fn sleep(seconds: c_uint) c_int {
std.time.sleep(@as(u64, seconds) * std.time.ns_per_s);
return 0;
}
};
const sen = struct {
fn readSensor(comptime SensorType: type, comptime field_name: []const u8, device_path: []const u8) !f32
{ _ = SensorType; _ = field_name; _ = device_path; return 23.45; }
};
fn floatToFixed(f: f32) f32 { return f; }
(composeCbor
is explained here)
UPDATE: This crashing seems to be caused by our Zig Sensor App running out of Stack Space. We fixed iy by increasing "Sensor Driver Test Stack Size" from 2048 to 4096.
Calling the debug
logger inside print_valf2
causes weird crashes...
debug("timestamp: {}", .{ event.*.timestamp });
Possibly due to Memory Corruption inside our Zig Debug Logger...
/// Called by Zig for `std.log.debug`, `std.log.info`, `std.log.err`, ...
/// TODO: Support multiple threads
/// https://gist.github.com/leecannon/d6f5d7e5af5881c466161270347ce84d
pub fn log(...) void {
// Possible memory corruption here: Format the message
var slice = std.fmt.bufPrint(&log_buf, format, args)
catch { _ = puts("*** Error: log_buf too small"); return; };
...
Here's a crash log...
bme280_fetch: temperature=31.570000 °C, pressure=1025.396118 mbar, humidity=64.624023 %
name: baro0
timestamp: 27270000
value1: 1025
value2: 31
size: 16
SensorTest: Received message: �, number:1/1
decode_insn_compressed: Compressed: a783
riscv_exception: EXCEPTION: Load access fault. MCAUSE: 00000005
riscv_exception: PANIC!!! Exception = 00000005
up_assert: Assertion failed at file:common/riscv_exception.c line: 89 task: sensortest
backtrace| 3: 0x2300c698
riscv_registerdump: EPC: 2300c698
riscv_registerdump: A0: 4201b9a0 A1: 0000a80 A2: 4201bf48 A3: 00000000
riscv_registerdump: A4: 2307a5e8 A5: 00583000 A6: 2307a000 A7: 00000000
riscv_registerdump: T0: 000001ff T1: 23005830 T2: 0000002d T3: 00000068
riscv_registerdump: T4: 00000009 T5: 0000002a T6: 0000002e
riscv_registerdump: S0: 4201b9a0 S1: 2307a000 S2: 00000a80 S3: 4201bdef
riscv_registerdump: S4: 00000000 S5: 00000000 S6: 00000000 S7: 00000000
riscv_registerdump: S8: 00000000 S9: 00000000 S10: 00000000 S11: 00000000
riscv_registerdump: SP: 4201bef0 FP: 4201b9a0 TP: 00000000 RA: 2300c78e
Another crash...
up_assert: Assertion failed at file:common/riscv_doirq.c line: 78 task: sensortest
backtrace| 3: 0x2300bd9a
riscv_registerdump: EPC: 2300bd9a
riscv_registerdump: A0: 00000000 A1: 4201bc38 A2: 00000013 A3: 00000000
riscv_registerdump: A4: 00000000 A5: 0000000b A6: a0000000 A7: 2306a1b2
riscv_registerdump: T0: f0000000 T1: 80000000 T2: 00000000 T3: 00000000
riscv_registerdump: T4: 00000008 T5: 00010000 T6: 6d0cb600
riscv_registerdump: S0: 0000001b S1: 00000000 S2: 23079000 S3: 4201b8c8
riscv_registerdump: S4: 4201bc38 S5: 00000006 S6: 00000000 S7: 00000000
riscv_registerdump: S8: 00000000 S9: 00000000 S10: 00000000 S11: 00000000
riscv_registerdump: SP: 4201bbf0 FP: 0000001b TP: 00000000 RA: 2300bd78
Which happens when closing a file (console?)...
/home/user/nuttx/nuttx/fs/inode/fs_files.c:380
/* if f_inode is NULL, fd was closed */
if (!(*filep)->f_inode)
2300bd98: 441c lw a5,8(s0)
2300bd9a: c39d beqz a5,2300bdc0 <fs_getfilep+0xaa>
2300bd9c: 87a2 mv a5,s0
2300bd9e: 00fa2023 sw a5,0(s4)
And another crash...
up_assert: Assertion failed at file:mm_heap/mm_free.c line: 154 task: sensortest
riscv_registerdump: EPC: 230086b0
riscv_registerdump: A0: 00000000 A1: 4201bc38 A2: 00000000 A3: 00000000
riscv_registerdump: A4: 23078460 A5: 23078000 A6: 4201bdbc A7: 23078000
riscv_registerdump: T0: 000001ff T1: 23078460 T2: 0000002d T3: 00000068
riscv_registerdump: T4: 00000009 T5: 0000002a T6: 0000002e
riscv_registerdump: S0: 4201c1f0 S1: 23078000 S2: 4201b800 S3: 4201bed0
riscv_registerdump: S4: 42013000 S5: 23078000 S6: 00000000 S7: 00000000
riscv_registerdump: S8: 00000081 S9: 00000025 S10: 23068e25 S11: 4201bec4
riscv_registerdump: SP: 4201beb0 FP: 00000000 TP: 23001478 RA: 230080c2
This crashes inside free
when deallocating the Sensor Data Buffer, might be due to a Heap Problem.
For safety, we converted the Heap Buffer to a Static Buffer.