This document describes the steps to configure, compile, run, and debug the modified QEMU project to support the NXP S32K3 board.
Before starting, make sure you have the following dependencies installed. Run this command from your terminal:
sudo apt update
sudo apt upgrade
sudo apt install git libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev ninja-buildNote: If you encounter issues with ninja-build, ensure your system is fully updated by first running sudo apt update and sudo apt upgrade.
Follow these steps to correctly download the source code and compile it.
Clone the repository and initialize the necessary submodules:
git clone <YOUR_REPOSITORY_URL>The ./configure command prepares the build environment. You can customize it with specific flags to enable debug logs for certain modules.
Generic configuration with debug enabled:
./configure --target-list=arm-softmmu --enable-debugConfiguration with debug for LPUART:
CFLAGS="-g -O0 -DNXP_LPUART_DEBUG=2" CXXFLAGS="-g -O0 -DNXP_LPUART_DEBUG=2" ./configure --target-list=arm-softmmu --enable-debugConfiguration with debug for LPSPI:
CFLAGS="-g -O0 -DNXP_LPSPI_ERR_DEBUG=2" CXXFLAGS="-g -O0 -DNXP_LPSPI_ERR_DEBUG=2" ./configure --target-list=arm-softmmu --enable-debugAfter configuration, start the compilation using all available CPU cores:
make -j$(nproc)To check the machines (boards) supported by your QEMU build, run:
./build/qemu-system-arm -M helpBelow are several examples for testing the emulator with different firmwares.
To run our FreeRTOS demo which uses LPUART3:
./build/qemu-system-arm -M nxps32k358evb -nographic -kernel /path/to/your/project/DEBUG_QEMU/Demo_FreeRTOS.elf -serial none -serial none -serial none -serial mon:stdio -d guest_errorsExplanation of the -serial flags: The emulated board has 16 LPUART interfaces. Since our DEMO project uses LPUART3 (the fourth interface, starting from 0), we disable the first three (-serial none) and connect the fourth to the terminal's standard input/output (-serial mon:stdio).
For interactive debugging of the firmware running on QEMU, use GDB in combination with the -S -s flags.
Open two terminals.
Terminal 1: Start QEMU Run QEMU. The emulator will start and wait for a GDB connection.
./build/qemu-system-arm \
-M nxps32k358evb \
-nographic \
-kernel /path/to/your/project/DEBUG_QEMU/Demo_FreeRTOS.elf \
-serial none -serial none -serial none -serial mon:stdio \
-d guest_errors \
-S -s-S: Freezes the CPU at startup.-s: Opens a GDB server on localhost:1234.
Terminal 2: Start GDB
Launch gdb-multiarch to connect to QEMU.
gdb-multiarch /path/to/your/project/DEBUG_QEMU/Demo_FreeRTOS.elfOnce GDB has started, run these commands:
# Connect to QEMU listening on port 1234target remote localhost:1234# Set the source file paths to allow GDB to find them# (adjust the paths for your environment)directory /mnt/c/Users/vitoc/Desktop/workspace_group7/Demo_FreeRTOS
set substitute-path ../ /mnt/c/Users/vitoc/Desktop/workspace_group7/Demo_FreeRTOS/
# Now you can use standard GDB commands:# b main (set a breakpoint at main)# c (continue execution)# n (next, execute the next line)# p my_variable (print the value of a variable)To view memory addresses and the disassembly of the ELF file, you can use objdump. This is useful for verifying the correct compilation and for low-level debugging.
arm-none-eabi-objdump -d /path/to/your/project/DEBUG_QEMU/Demo_FreeRTOS.elf > disassembly.txtThis command will save the entire code disassembly into a disassembly.txt file for easy reference.
During development, some issues were identified when running code generated by S32 Design Studio on QEMU. The solutions are described below.
- Symptom: The program gets stuck in a
WaitForClockloop during the initialization of theMC_MEmodule. - Cause: The startup code generated by NXP contains a wait loop for clock stability that cannot be satisfied in the QEMU simulation environment. This block of code is protected by the preprocessor directive
#ifndef SIM_TYPE_VDK. - Solution: You need to create a specific build configuration for QEMU that defines the
SIM_TYPE_VDKsymbol, thus excluding the problematic code from compilation.
- Create a New Build Configuration:
- Go to
Project -> Build Configurations -> Manage.... - Select your "Debug" configuration and click
New.... - Name it
Debug_QEMUand click OK. - Activate the new configuration:
Project -> Build Configurations -> Set Active -> Debug_QEMU.
- Go to
- Add the Preprocessor Symbol:
- Right-click on your project and go to
Properties. - Navigate to
C/C++ Build -> Settings -> Tool Settings. - Under
Standard S32DS C Compiler -> Preprocessor, click the "Add" icon (+) in the "Defined symbols (-D)" section. - Enter
SIM_TYPE_VDK. - Important: Repeat the same step under
S32 Assembler -> Preprocessor.
- Right-click on your project and go to
- Rebuild the Project:
- Clean and rebuild the project (
Project -> Clean...thenProject -> Build Project). The output will be generated in the newDebug_QEMUfolder.
- Clean and rebuild the project (
- The Problem: The standard firmware for NXP S32K3xx attempts to enable the Instruction Tightly Coupled Memory (ITCM) and Data Tightly Coupled Memory (DTCM) by writing to the
ITCMCR(offset0xF90) andDTCMCR(offset0xF94) control registers. The default QEMU model for the ARMv7-M NVIC did not implement handlers for these addresses, causing an "unimplemented memory access" error and boot failure. - The Solution: A two-part solution was implemented:
- NVIC Patch: The
qemu/hw/intc/armv7m_nvic.cfile was modified to intercept and handle accesses to these registers, preventing the error and allowing the firmware to proceed. - Memory Region Emulation: In the SoC model (
hw/arm/nxps32k358_soc.c), the memory regions for ITCM (at address0x00000000) and DTCM (at address0x20000000) were declared, initialized, and mapped into the system memory map.
- NVIC Patch: The
- Implementation Status: It is important to note that the NVIC patch is a "dummy" implementation. It acknowledges the register writes but does not use the value to dynamically enable or disable the memory regions. As a result, ITCM and DTCM are always enabled in the current state of the emulation.
Enabling the MPU allows for task memory isolation, increasing system robustness and security. The configuration requires a two-level approach, both of which are mandatory.
This setting activates the hardware initialization of the MPU before the FreeRTOS scheduler starts.
- Where: In the project properties in S32 Design Studio:
Properties -> C/C++ Build -> Settings -> Standard S32 Compiler -> Preprocessor - What to do: Ensure that the MPU enable option is checked.
- Purpose: This option adds a compiler directive (e.g.,
-D__MPU_ENABLE=1) that is used by the NXP startup code to configure basic memory regions (Flash, SRAM) at microcontroller startup.
This setting tells FreeRTOS to use MPU features for task management.
- Where: In the
FreeRTOSConfig.hconfiguration file. - What to do: Add or verify the presence of the following macros:
/* Enable MPU support in FreeRTOS */#defineconfigENABLE_MPU 1 /* Enable modern MPU wrappers, simplifying task management */#defineportUSING_MPU_WRAPPERS 1 /* Static allocation is strongly recommended when using the MPU */#defineconfigSUPPORT_STATIC_ALLOCATION 1 #defineconfigSUPPORT_DYNAMIC_ALLOCATION 1
This is the command that permits to check that the MPU is working, because in FreeRTOS implementation is present a function TestMPU that try to write on SRAM but the program crash. For this purpose we have builded another elf file.
```bash
./qemu-system-arm -M nxps32k358evb -nographic -kernel ../../Demo_FreeRTOS_MPU/Demo_FreeRTOS.elf -serial none -serial none -serial none -serial mon:stdio -d guest_errors
```
Think of two levels that must work together:
- S32 DS Compiler Flag (
__MPU_ENABLE):This is the hardware level. Enabling it activates code in the NXP startup files that performs the very first MPU initialization at boot, setting up basic memory regions to allow the code to run before FreeRTOS starts. Without this, the MPU would remain off. - FreeRTOS Flag (
configENABLE_MPU):This is the operating system level. Enabling it tells FreeRTOS to use the MPU APIs to manage task memory protection, save/restore their regions during context switches, and create "restricted" tasks.
In conclusion, you must enable both for correct operation.
The purpose of the project is to test a FreeRTOS application that manages sensors without needing the physical board. To achieve this, QEMU was extended to simulate the necessary hardware components.
The interaction is based on a master-slave system:
- Master: The FreeRTOS application running on the emulated processor.
- Slave: A virtual sensor device (
motor_speed) created specifically within QEMU.
QEMU is an emulator and virtualizer that allows us to run code compiled for our NXP microcontroller directly on a PC, without needing the physical board. To do this, QEMU must simulate not only the CPU but also all the hardware peripherals.
In this project, we virtualized a complete master-slave system: the FreeRTOS application acts as the master, and a custom sensor device within QEMU acts as the slave.
The Lpuart_Uart_Ip driver is used to initialize and control the simulated serial port. In our project, its sole purpose is to provide a debug channel. The print() function uses it to send status messages from the microcontroller to the QEMU console, allowing us to monitor the application's behavior in real-time.
The UART_send_byte function transmits a single byte of data over a UART interface. It serves as a simplified wrapper for a more complex driver function, Lpuart_Uart_Ip_SyncSend.
When called, it passes the following parameters to the driver:
UART_LPUART_INTERNAL_CHANNEL: A constant that specifies which LPUART hardware peripheral to use.&byte: A pointer to the single byte of data that needs to be sent.1: The number of bytes to transmit.100: A timeout value (likely in milliseconds) that the function will wait for the transmission to complete before failing.
To test our firmware's logic realistically, we created a complete virtual SPI communication system composed of two main parts: the virtual hardware (the master controller and the slave sensor) and the software driver that the application uses to interact with them.
nxps32k358_lpspi.c(The Master Controller): This file, which we debugged and corrected, implements the model of the LPSPI peripheral inside QEMU. It acts as the master controller, simulating the hardware registers and behavior. It receives commands from our FreeRTOS application (via theLpspi_Ipdriver) and manages the data flow on the simulated SPI bus.motor_speed.c(The Slave Device): This file defines a new virtual device for QEMU that behaves like a motor speed sensor. It is the slave in our system. It's programmed to listen on the SPI bus and respond to a specific command (CMD_GET_SPEED, defined as0xAA) by sending back a random numerical value, simulating a real-world sensor.
The Lpspi_Ip is the high-level software driver provided by NXP that our FreeRTOS application uses to control the LPSPI hardware. We interact with it primarily through the function Lpspi_Ip_SyncTransmit.
The Role of
Lpspi_Ip_SyncTransmitThis function is the bridge between our application logic and the SPI hardware. The
Sync(Synchronous) part is crucial: it means that when a task calls this function, it stops and waits (it is "blocked") until the entire SPI data exchange is complete.In our
Motor_Sensor_ReadValuefunction, we use it like this:Lpspi_Ip_SyncTransmit(&MASTER_EXTERNAL_DEVICE, &cmd, rx_buff, 1, 1000);
Here is a breakdown of each parameter:
&MASTER_EXTERNAL_DEVICE: A pointer to a configuration structure defining the slave device we want to talk to. It tells the driver which Chip Select (CS) pin to use and other specific settings for that slave.&cmd: A pointer to the data we want to send. In our case, this is the command0xAA.rx_buff: A pointer to the buffer where the received data will be stored. While the master sends the command, the slave simultaneously sends a byte back, which is stored here.1: The length of the transfer. This tells the driver to send one byte and receive one byte.1000: A timeout value in milliseconds. This is a safety feature to prevent the application from freezing if the hardware gets stuck.
A task is a function that runs as an independent mini-program. Our system is composed of three main tasks that drive all the hardware interactions:
ReadSpeedTask: This is the primary active task. It is responsible for callingMotor_Sensor_ReadValue(), which in turn usesLpspi_Ip_SyncTransmitto communicate with the virtual sensor and read the speed.CheckSpeedTask: This task waits forReadSpeedTaskto finish. It then analyzes the speed value and uses theLPUART(via theprint()function) to report the system's status.TaskCodeC: An auxiliary task activated by a software timer to demonstrate asynchronous execution, independent of the main sensor-reading loop.