When working with Xilinx’s Vitis tools to build projects targeting ARM Cortex-M1 processors, there are some common configuration mistakes that are easy to make. These misconfigurations can lead to hard-to-debug issues or problems getting your application to run properly on the target hardware. In this article, we’ll go over some of the most common pitfalls and how to avoid them.
Not Selecting the Correct Target Processor
One of the first things you’ll do when creating a new Vitis project is select the target processor or hardware platform. For Cortex-M1 designs, you’ll want to choose either the MicroBlaze or the Cortex-M1 Streaming processor options. Selecting the wrong architecture can lead to incompatible and mismatched settings down the line.
Double check that you have the right processor selected in the project creation wizard before moving forward. If you’ve already created your project with the wrong selection, you may need to delete it and start over to avoid configuration problems.
Incorrect Compiler Options
There are some key compiler options that need to be set correctly for Cortex-M1 projects in Vitis. One common mistake is using the wrong instruction set architecture (ISA). Make sure this is set to “-mthumb” in your compiler settings to target the M1’s Thumb ISA.
You also need to select the correct hardware floating point unit option. Set this to “-mfloat-abi=soft” if your Cortex-M1 implementation does not contain a hardware FPU. Using the wrong settings here can lead to illegal instruction errors during compilation or runtime crashes.
Not Linking Appropriate Libraries
To keep your application size small on a Cortex-M1, you typically need to avoid linking in unnecessary libraries. A common mistake is keeping the default standard C/C++ libraries linked, which brings in substantial code you probably don’t need.
Pay close attention to only link the bare minimum requirements for your application. For example, linking the libc.a for newlib C runtime environment is usually sufficient rather than the much larger libstdc++. Review the libraries list and remove any unnecessary options.
Incorrect Memory Regions
One of the key steps with any embedded processor project is defining the proper memory regions. This specifies where the code and data will be placed within the system memory map. For Cortex-M1 designs, you’ll usually have separate regions for the instruction and data buses.
It’s important to get these regions right based on your specific hardware, otherwise you can end up with memory access crashes or unavailable RAM. Double check the start addresses, sizes, and bus connections for the memory sections match the hardware specifications.
Not Enabling the MPU
The Cortex-M1 contains an optional memory protection unit (MPU) that provides additional safety by restricting memory access. Forgetting to configure and enable this can leave your system vulnerable to buffer overflows or other code accessing restricted regions.
Make sure you add defines like __MPU_PRESENT to enable MPU code in the appropriate library files. Also initialize and configure the MPU regions properly to activate it during runtime. Leaving the MPU disabled removes an important layer of memory safety.
Boot Sequence Errors
To get your Cortex-M1 application actually running on the hardware, you need to properly set up the boot sequence and loading of the executable. If this isn’t configured right, you’ll just end up with the processor stalled at reset or somewhere else unexpected.
Carefully review the boot sequence process for your specific system. Make sure you have things like the vector table, reset handler, and bootloader properly set up. Test the boot process early on with a simple application to catch any issues here.
Invalid Interrupt Handlers
Handling interrupts properly is key for any embedded application. On the Cortex-M1, you need to ensure your interrupt service routines (ISRs) match the hardware peripherals and are configured correctly.
Double check that the number and type of ISRs match the target system. Use the right syntax for defining an ISR properly. Handle both the interrupt request and clearing in the handler. Prioritize interrupts appropriately by priority level. Test ISRs thoroughly under real interrupt conditions.
Incorrect Wait State Settings
Since the Cortex-M1 is a relatively fast processor, you need to take care that your memory can keep up in terms of access timing. This is configured through wait states, which insert extra cycles for each memory access.
Too few wait states will lead to unreliable operation, crashes, or corrupted data. Too many impacts performance unnecessarily. Carefully calibrate the wait state values for your specific memory chips based on their timings to avoid issues here.
Not Handling Processor Exceptions
By default, processor exceptions on the Cortex-M1 like divide by zero, unaligned access, etc. will cause a system crash. You need exception handlers set up to catch these and either correct the issue or restart gracefully.
Go through the list of possible processor exceptions and add handlers for any you anticipate could legitimately occur. Having these in place makes the system much more robust and avoids frustrating debugging sessions tracking down crashes.
With the very limited memory available on Cortex-M1 systems, carefully managing stack and heap usage is critical. Runaway stack usage can easily overwrite other memory areas and crash the application.
Be sure to allocate a sufficient fixed stack size and guard area. Check for infinite loops, deeply nested function calls, and recursion that may overflow the stack. Use stack canary values to detect overwrites early. Allocate heap carefully and check for memory leaks.
Incorrect Vector Table
The vector table defines the initial stack pointer value and sets up the reset and exception handlers called by the processor. Errors in this table will lead to a broken system right from the start.
Double check the stack pointer start address is correct. Make sure the reset handler and other exception vectors point to valid functions. Verify the linker script properly places the vector table at the very beginning of the application image.
Poor Code Optimization
Optimizing your C/C++ code effectively is critical for good performance on a Cortex-M1. The compiler can’t always pick the best optimizations automatically.
Use commands like -Os to optimize for size. Leverage compiler pragmas to inline or unroll key loops. Refactor code to take advantage of auto-vectorization. Be careful using floats. Review the generated assembly for hot spots to optimize.
Bad Single-Core Assumptions
While Cortex-M1 systems are single core, many still use multicore SoC designs. That means assumptions about timing or exclusive access to hardware may not hold.
Carefully review initialization code that accesses registers early on. Use mutexes or flags to protect shared hardware access. Make no assumptions about timing between code sections. Consider using the MPU to protect critical memory regions.
Incorrect Build Settings
Embedded projects have many build settings around the toolchain, macros, source files, etc. that need to be configured correctly. If your build settings are wrong, you’ll end up with problems compiling or getting a flawed binary.
Double check your toolchain path, selected BSP, compiler flags, include paths, and defined macros. Make sure source files are all included properly. It pays off to carefully validate your build settings initially to avoid later issues.
Assert statements are extremely helpful for validating assumptions during development. But the standard assert macro includes substantial debugging code you don’t want in a release build.
Make sure to use a macro like NDEBUG or your own custom slim NOASSERT version to remove asserts in the release application. This avoids both increased code size and the performance cost of evaluating the assertions.
Initialization Order Errors
For complex embedded projects, getting the proper initialization order correct can be tricky. If done incorrectly, you may have code using hardware that is not yet set up properly.
Review startup code and main application flow closely to catch any use of peripherals before they are initialized. Leverage constructor functions to ensure objects are set up before use. Be careful of priority issues around multiple chip enables.
Unaligned Data Access
The Cortex-M1 does not support unaligned memory accesses which can occur when using packed structs or byte arrays. But this is not checked by default.
Prefer 32-bit aligned data structures. Use compiler attributes like __packed to alert of any unaligned access. Or enable the unaligned access traps on the MPU to catch these errors. Undetected, unaligned accesses can lead to subtle data corruption.
With everything happening on a single core in a Cortex-M1 system, timing between different operations can become sensitive.
Avoid making assumptions about timing in different code sections. Use timer delays or flags where precise timing is required. Increase stack guard regions in case of timing variability. Consider using an RTOS for better timing control.
Improper Register Saving
Forgotten compiler pragmas or incorrect ISR coding can lead to issues with registers not being properly saved and restored during interrupt handling.
Use pragmas like save, preserve, and restore around function declarations. Verify assembly shows registers being pushed and popped. Enable the stack overflow detector. Check for corrupted global variables that may indicate register problems.
Fixed Hardware Assumptions
Embedded software often makes assumptions about the target hardware that may not be true, like memory sizes, clocks, or presence of chips.
Parameterize key configuration constants rather than hard-coding. Use chip selects and clocks strategically to minimize dependencies. Detect chips/buses at runtime if possible. Validate against the hardware spec frequently as the design evolves.
Resource Constraint Issues
Cortex-M1 systems are highly resource constrained environments with tight limits on factors like code size, processing, and memory.
Understand your resource budget and track closely against it. Use fixed size data types like uint32_t. Avoid dynamic memory allocation. Disable unused library features and compiler optimizations. Keep monitoring for resource usage regressions.
Use of Floating Point Code
The Cortex-M1 typically does not contain an FPU, so emitting floating point code can be problematic. But the compiler will do so implicitly which is easy to miss.
Avoid direct float usage and prefer integers wherever practical. Enable compiler flags to treat floats as illegal to detect use. Check assembly listing for FP instructions which will fault at runtime without an FPU. Use software floating point libraries if needed.
Fixed Endianness Assumptions
It’s common to assume a fixed CPU endianness during development, but some Cortex-M1 systems can actually configure this at boot time.
Use endian-agnostic code like htons()/ntohs() for network and communications. Detect endianness at runtime if needed. Use unions to interpret raw data vs bytes. Avoid pointer type casting that may depend on endian order.
Compiler Version Incompatibility
Toolchain components like compilers and linkers need to be version matched or you can end up with weird issues in the compiled output.
Always use a matched toolchain bundle like Xilinx Vitis when possible. Check that compiler and linker versions align across projects. Rebuild all code cleanly when upgrading toolchain versions across the board.
Excessive Debug Features
It’s easy to leave debug features enabled in release builds that waste space and cycles, like debug symbols, asserts, logs, etc.
Review compiler options to remove debug symbols in release build. Disable logging code with global compiler defines. Omit debug functionality like console I/O. Remove debug only driver code and utilities. Check the map file for any debug code/data.
Improper HW Debug Access
Debugging hardware like JTAG needs to be initialized and accessed properly to work correctly. Issues here will make development and post-mortem analysis challenging.
Review the board and processor documentation on debug unit connectivity and pin states. Verify the debug unit is reset properly at start up before use. Check for SWD/JTAG signal integrity issues on long routes or low pull-ups.
Targeting Cortex-M1 processors in Vitis comes with a variety of potential configuration pitfalls. Watch out for the common mistakes like incorrect compiler settings, memory region issues, boot sequence problems, and missing initialization code. Pay close attention to linker directives, boot up timing, and the start up code flow. Validate the tool chain compatibility and build settings closely. And don’t forget about endianness, alignment, and resource constraints in this embedded environment. Following the recommendations outlined here will help you avoid some frustrating issues and get your embedded application running smoothly on a Cortex-M1.