/ Low Level

Emulating the VEX Cortex

What better to spend my time on than emulate decade old deprecated hardware?

Here is a rough block diagram of what the Cortex looks like:

cortex--2-

At first I wanted to do full emulation including both the supervisor and user SoCs to make it as realisic as possible, but quickly realized how difficult that would be as the supervisor's pinout is not documented and the protocol it uses to communicate with VEXNet keys is not documented.

The user SoC is a STM32F103VD with 384K flash, 64k ram, and a Cortex-M3 armv7-m processor. QEMU itself supports Cortex-M3 but only a very limited number of SoCs and development boards not including anything stm32.

Support for stm32 specifically is extremely important because timers, interrupt control, dma, and most other mmio functionality is vendor specific.

Turns out there is a qemu fork that adds stm32 support for the stm32f103xx SoCs here.
This fork includes enough to get PROS running out of the box and even print messages to UART without any modifications to the kernel.

This is great but unfortunately I2C and SPI aren't implemented and the ADC is incomplete (lacks continuous conversion mode). I need SPI for the supervisor or user code won't even run, I2C is needed to simulate IMEs, and the ADC is needed to simulate analog sensors.

To work around these issues instead of rewriting everything I opted to modify the pros kernel to do all I/O through custom hardware.

First step was to make a custom qemu device for the vex cortex implemented here.

Pretty straight forward, just a stripped down version of the stm32f103c8 device.

Next I needed to write qemu hardware here to communicate with the kernel, to do this i needed the following:

  1. A timer to update supervisor every couple ms
  2. The irq list for the cortex so I can poke them from qemu
  3. A memory region that I can control read and writes from on the qemu side

Sadly the qemu internals aren't too well documented so I had to take reference from some other hardware implementaitons (dma, adc, etc.)

Timers are simple:

s->circular_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, (QEMUTimerCB *)vex_mgr_stream_circular_timer, s)

This creates a nanosecond timer with vex_mgr_stream_circular_timer as a callback when it gets triggered.

uint64_t curr_time = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
timer_mod(s->circular_timer, curr_time + 10000000);

This schedules the timer 10ms in the future

Next we need the irq list, which is returned by stm32_init and can just be passed to vex_mgr_create in vex_cortex_init

Once you have the list (named pic) you can just trigger them with qemu_irq_pulse. The stm32 header conveniently provides definitions for irqs so the following example triggers the SPI1 interrupt as the SPI controller would do when there is data in the SPI buffer sent from the supervisor:

qemu_irq_pulse(s->pic[STM32_SPI1_IRQ]);

Finally the memory region, I picked some reserved space in the stm32 memory map that's at 0x40021400 and 0xC00 (3072) bytes wide. The following code maps it to functions that handle reading and writing:

static const MemoryRegionOps vex_mgr_ops = {
	.read = vex_mgr_read,
	.write = vex_mgr_write,
	.endianness = DEVICE_NATIVE_ENDIAN,
	.impl = {
		.min_access_size = 1,
		.max_access_size = 4,
	}
};

sysbus_mmio_map(SYS_BUS_DEVICE(dev), 0, 0x40021400);
// s->iomem is a MemoryRegion struct
memory_region_init_io(&s->iomem, OBJECT(s), &vex_mgr_ops, s, "vex_mgr", 0xC00);
sysbus_init_mmio(dev, &s->iomem);

Now if the cortex ever tries to read or write from that memory region qemu will immediately call the handlers allowing two way communication.

As with most MMIO on the cortex, in the kernel you define the device as a convenient struct with volatile fields:

typedef struct {
  // Control register
  __IO uint32_t CR;
  // Args
  __IO uint32_t A1;
  __IO uint32_t A2;
  __IO uint32_t A3;
  // Results
  __IO uint32_t R1;
  __IO uint32_t R2;
  __IO uint32_t R3;
  __IO uint32_t R4;
} EMULATOR_TypeDef;

#define EMULATOR ((EMULATOR_TypeDef*)0x40021400)

With the struct and EMULATOR macro we can access the hardware registers by name instead of directly reading and writing from memory.

The emuCall helper function i wrote loads arguments and signals the control register to call a specific function on the qemu side provided a 16 bit module ID and 16 bit function ID. It is this this way instead of passing a function name as a string i.e. "Motor_set" for simplicity as there are only a few dozen functions.

inline int emuCall(uint16_t mod, uint16_t func, uint32_t a1, uint32_t a2, uint32_t a3) {
  _enterCritical();
  EMULATOR->A1 = a1;
  EMULATOR->A2 = a2;
  EMULATOR->A3 = a3;
  EMULATOR->CR = func | (mod << 16);
  int out = EMULATOR->R1;
  _exitCritical();
  return out;
}

The enter and exit critical ensure interrupts are disabled while registers are being accessed, as an interrupt inbetween any of these reads and writes will most likely clobber the argument and return registers.

After that I refactored a good chunk of the kernel to do all I/O through emuCall, you can see the huge commit here.

Here are all the functions that needed to be implemented to remove dependency on standard cortex gpio:

int EmuSerial_init(int port, int baud, int flags);
int EmuSerial_shutdown(int port);
int EmuSerial_putc(int port, int c);

int EmuFS_programOn();
int EmuFS_programOff();
int EmuFS_erasePage(int page);

int EmuI2C_startRead(int addr, void* data, int count);
int EmuI2C_startWrite(int addr, void* data, int count);
int EmuI2C_setAddr(int addr);

int EmuGPIO_ADCInit(uint32_t data);
int EmuGPIO_setDir(int port, int mode);
int EmuGPIO_getInput(int port);
int EmuGPIO_getOutput(int port);
int EmuGPIO_setOutput(int port, int value);
int EmuGPIO_setInterrupt(int port, int edges);

int EmuMotor_get(int channel);
int EmuMotor_set(int channel, int value);
int EmuMotor_stop();

int EmuComp_init();
int EmuComp_enableStandalone();
int EmuComp_setName(char* name);
int EmuComp_getStatus(void* buff);

int EmuSystem_exit();
int EmuSystem_break();

Header where these are defined

On the qemu side most of this will remain stubbed until I have an actual robot simulation to send and recieve all the data from.

For now only UART and the supervisor is fully implemented but that's enough to get the PROS kernel and user code fully running in an emulator!

Selection_589

And that's all I have for simulating the cortex until my virtual vex field in Unreal Engine is ready, once that is complete I can merge the two and have something you can drive around, test autons and sensors using actual robot code!

Saucy: QEMU Fork, PROS Fork

Emulating the VEX Cortex
Share this