QEMU User-mode Emulation is a practical solution for testing algorithms directly without having to set up a microcontroller system and boot it every time you test. Unfortunately, QEMU only supports full system emulation of Aurix microcontrollers with TriCore instruction set. User-mode emulation is not available for TriCore.

Since Infineon’s Tricore instruction set simulator TSIM has a virtual I/O feature that can be used for user-mode emulation.

Compile for Tricore and TSIM

Download and install the Tricore GNU toolchain (including GCC, binutils, GDB and newlib) with

git clone --recursive https://github.com/NoMore201/tricore-gcc-toolchain
cd tricore-gcc-toolchain
mkdir build
cd build
../configure --prefix=~/.local/share/tricore-gcc
make -j$(nproc) stamps/build-gcc-stage

or directly use the less experimental upstream repository from EEESlab

git clone --recursive git@github.com:EEESlab/tricore-gcc-toolchain-11.3.0.git
./build-toolchain --all

Compile foo.c to foo.elf with

tricore-elf-gcc -mcpu=tc39xx -g -nostartfiles -T tsim.ld tsim_startup.c \
    -o foo.elf" foo.c

Two additional files cannot be avoided: the internal linker script of gcc does not define memory regions and the internal startup code does access an invalid memory mapped register which causes an exception.

Minimal Linker Script

The linker script starts with the declaration of the target architecture:

OUTPUT_FORMAT("elf32-tricore")
OUTPUT_ARCH(tricore)

TSIM comes with config scripts for the Aurix TC39xx family of microcontrollers. We use two memory regions: 240 KiByte data scratchpad RAM at 0x70000000 and 4MiByte program flash ROM at 0x80000000:

MEMORY {
    data_scratchpad_ram (w!xp): org = 0x70000000, len = 240k
    program_flash_rom   (rx!p): org = 0x80000000, len = 4M
}

Most sections are automatically assigned by the linker. Only the .heap section must be defined, because newlib expects the symbols __HEAP and __HEAP_END to point at the start and the end of the heap memory area.

HEAP_SIZE  = 64k;
SECTIONS {
    .heap  : FLAGS(aw) {
        . = ALIGN(4);
        __HEAP = .;
        . += HEAP_SIZE;
        __HEAP_END = .;
    } > data_scratchpad_ram
}

Complete file: tsim.ld

Minimal Startup Code

Program entry is at the _start function. At this point the stack is not initialised yet. Therefore the function is of pure assembly and only sets the stack pointer a10 to 0x7003A000 and jumps to the C code in __startup.

void _start( void ) __attribute__((used,noinline)) ;
void _start(void)
{
    asm("movh.a   %a10, hi:(0x7003A000)           \n\t" \
        "lea      %a10, [%a10]lo:(0x7003A000)     \n\t" \
        "dsync                                    \n\t" \
        "movh.a   %a15,  hi:(__startup)           \n\t" \
        "lea      %a15, [%a15]lo:(__startup)      \n\t" \
        "ji %a15");
}

Main task of __startup is to create the singly linked list of CSAs. Each CSA is 64 bytes long and the first 4 byte word points to the next entry. Core register FCX points to the first free entry in the CSA list. LCX points to one of the last entries, but not the last. When FCX reaches LCX a trap is taken and there should be some CSA remaining for the execution of the trap. The pointers in FCX, LCX and the next field are not direct addresses, but compressed ones that can only address the first 4 MiByte of each segment.

void __startup() __attribute__((used,noinline,noreturn)) ;
void __startup()
{
    /* Setup the context save area linked list. */
    unsigned int csa_addr  = 0x7003A000;
    unsigned int csa_end   = 0x7003C000;
    unsigned int next_pcxi = ((csa_addr & 0xF0000000) >> 12) | /* segment */
                             ((csa_addr & 0X003FFFC0) >> 6);   /* offset */

    _mtcr(0xFE38/*FCX*/, next_pcxi); /* store 1st PCXI value in FCX */

    while (csa_addr < csa_end) {
        next_pcxi++;
        *(unsigned int *)csa_addr = next_pcxi;
        csa_addr += 64;
    }
    *(unsigned int *)(csa_end - 64) = 0; /* mark end of CSA list */

    _mtcr(0xFE3C/*LCX*/, next_pcxi - 3);
    _dsync();

    exit(main());
}

Complete file: tsim_startup.c

Run with TSIM

After registration, TSIM can be downloaded from the Infineon website. Install with

sudo apt install ./tsim_1.18.196_linux_x64.deb

TSIM will be installed to a fixed path under /opt/Tools/.... Emulate foo.elf with

tsim_path=/opt/Tools/TSIM-Tricore-instruction-set-simulator/1.18.196
${tsim_path}/bin/tsim16p_e -e -h -s -H -z \
    -MConfig ${tsim_path}/config/tc162/tc39xx/MConfig \
    -OConfig ${tsim_path}/config/tc162/tc39xx/OConfig \
    -o foo.elf -trace-instr-file foo.tsim

Appendix: TSIM Virtual I/O Interface

A system call to the virtual I/O is done by the debug instruction. The parameters are passed in registers according to the Tricore calling convention. The syscall number is passed in register D12. After the syscall, D11 is set to the return value and D12 to errno if an error occured. The following syscall numbers are supported:

no function
1 int open(char *filename, int flags, int mode)
2 close(int fd)
3 int lseek(int fd, int offset, int whence)
4 int read(int fd, void *buf, int len)
5 int write(int fd, void *buf, int len)
6 int creat(char *filename, int mode)
7 int unlink(char *filename)
8 int stat(char *filename, void *statbuf)
9 int fstat(int fd, void *statbuf)
10 gettimeofday() ?
11 int ftruncate(int fd, int len)
13 int rename(char *oldname, char *newname)