biojppm / c4core

C++ utilities

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add CI test for bare-metal arm

danngreen opened this issue · comments

I've been playing with this on a bare metal ARM target using the arm-none-eabi-gcc toolchain (in order to run rapidyml). I think rapidyml + c4core are a great combo for a small embedded system, being lightweight.

There is one issue, however, and that's regarding the use of <chrono>. This isn't well supported on bare-metal targets since getting the current time typically results in a call to the OS, or a SVC call. When compiling/linking rapidyml + c4core with -nostdlib, linking fails unless the user provides an implementation of std::chrono::high_resolution_clock::now() (ie std::chrono::_V2::system_clock::now()).

When compiling/linking with newlib (that is, omit the -nostdlib flag), it still doesn't work unless the user provides a custom implementation of `_gettimeofday'.

I'm not sure the best solution to this. Some ideas:

  • Add an additional #if defined check in currtime() something like:
#elif defined(__ARM_EABI__)
	return time_type{0};

But I'm not sure what symbol to check for, because __arm__ and __ARM_EABI__ could both be set even with embedded Linux or an RTOS.

  • Add a compilation flag such as "-DCOMPILE_BAREMETAL" which does the above (makes currtime() a stub returning 0 always)
  • Add a compilation flag to omit compiling and including time.cpp/hpp. It seems that these functions aren't used by c4core itself, so this shouldn't interfere.
  • Add compilations flags just for c4/time.cpp (or in-source __attribute__s to push flags for currtime()), so that currtime() will be optimized out before the linker sees it. Obviously, this fails if the user makes a call to currtime() or busy_wait() or exetime().

The other issue blocking usage on bare-metal systems was with fast-float, see this issue. It looks like will be addressed soon with #122 and #123

In any case, thanks for your hard work on this, and I've been enjoying using it on a small embedded system (with workarounds, which I'd love to get rid of).

I think you can check #ifdef __NEWLIB__

That works. __NEWLIB__ is defined even with -nostdlib and --specs=nosys.specs (I had to check to be sure).

commented

Thanks for the kind words.

I have been entertaining the idea of removing the time files. There's nothing special that they offer, and this problem seals the deal. I'll just remove these files.

As for the workarounds you mention, are there any things needed other than this and biojppm/rapidyaml#193? I'm going to set up a CI workflow for that toolchain with QEMU, but if there is anything else please let me know.

Great, a CI testing this toolchain with QEMU is a good idea. Besides the stdarg.h issue, there's nothing else preventing compilation for me. Also, I've had no runtime issues with it parsing yaml on a 32-bit ARM chip.
Thanks so much!

commented

@danngreen I'm having trouble running even a int main() { return 0; } with arm-eabi-none under QEMU, and am not experienced with bare-metal. Maybe I'm missing something obvious.

Can you show me how you are doing it? FWIW, I am building with cmake, with a eabi-none toolchain:

cat <<EOF > main.cpp
int main() { return 0; }
EOF
cat <<EOF > CMakeLists.txt
cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
project(c4core-quickstart LANGUAGES CXX)
add_executable(c4core-quickstart main.cpp)
EOF
wget https://raw.githubusercontent.com/jobroe/cmake-arm-embedded/master/toolchain-arm-none-eabi.cmake
cmake -S . -B build/arm-none --toolchain toolchain-arm-none-eabi.cmake
cmake --build build/arm-none/ -j --verbose

Now if I run with qemu I get a segfault:

$ qemu-arm -L /usr/arm-none-eabi build/arm-none/c4core-quickstart
Segmentation fault (core dumped)

Any idea?

Try this for main.cpp:

void main() { while(true); }

Depending on the startup code which calls main, returning from it could jump to an infinite loop, or start executing code from a non-executable section of memory, or other undefined behavior. That might explain the seg fault.
You may need to add -ffreestanding to the CXX build flags in the toolchain file in order for main to return void.

I'll try running it with qemu myself when I get a chance later.

[Edit: fixed the main() body]

Forget what I said in the previous comment, it turns out it's a lot more complex than that!

Bad news is I don't have a CI-friendly solution for using qemu. I'm not able to test qemu-arm -L /usr/arm-none-eabi because I don't know where to find a libc6-dev package that installs cross-compilation objects into /usr/arm-none-eabi. But I haven't used qemu in the static-user mode before, so I don't know really know much about this method.

Here's the way I've set up the project to get it running with qemu: https://github.com/danngreen/qemu-ryml-test

It's little much to paste it all in here, so I can just summarize.
I added a startup.s file which defines the Reset vector, sets up the stack and jumps to main().
I also add the linkerscript.ld in order to specify the memory layout. I setup the linkscript to use the addresses that a qemu machine called vexpress-a9 requires. These are pretty much boiler-plate files for a bare-metal project.

I had to add some build flags to make sure the linkscript is loaded and the generated machine code matches the emulated processor (cortex-a9). Finally, a post-build step converts the "executable" (which is an ELF file) into a .bin file (just raw code/data to load into the target's memory).

Then I use qemu-system-arm to launch the virtual machine and load the bin file.

git clone https://github.com/danngreen/qemu-ryml-test
cd qemu-ryml-test/examples/qemutest
cmake -S . -B build --toolchain toolchain-arm-none-eabi.cmake
cmake --build build -j --verbose
qemu-system-arm -S -M vexpress-a9 -m 512M -no-reboot -nographic -gdb tcp::2159 -monitor telnet:127.0.0.1:1234,server,nowait -kernel build/c4core-quickstart.bin

To monitor the system, you can do

telnet localhost 1234

or with gdb:

arm-none-eabi-gdb build/c4core-quickstart.elf

(gdb) target remote localhost:2159

There is something wrong and it crashes in the middle of ryml::parse_in_place. There are a few things I know might be causing this, possibly I defined the heap in the wrong place. But it's executing code properly within qemu, so I wanted to share that much at least.

commented

Thanks @danngreen . I will eventually return to add bare-metal QEMU in the CI, but this is too much work for me right now. So let's leave this issue open ; for now I will setup a PR with the fixes, and I will take your word that it works. If the bare-metal doesn't work or if it stops working, feel free to let it be known in here.

Makes sense. It looks like the CI uses doctest? If so, another hurdle may be getting doctest running on bare-metal, see: doctest/doctest#242 (comment)

In any case, I'll post if I run into any issues or find any solutions. Thanks

commented

Indeed, I had already seen the doctest problem and ended up exactly in the link you posted at doctest/doctest#242. I was resigned to just run a quickstart executable like the one in ryml, which uses no testing framework but still does some sort of testing. But first I must be able to run a hello world! :-P