Running rust on an Analog Devices ADSP-SC589
I had some ideas for some audio effects and dusted off an old Analog Devices ADSP-SC589 dev board that’s been sitting idle in a drawer for the last few years. The ADSP-SC589 is a real-time processor with 2 SHARC+ DSP cores and an ARM Cortex-A5 processor. I’ve played with rust a bit, and wanted to try it out for something embedded, and this seemed like a great opportunity. Theres no LLVM backend for the SHARC architecture, so I’m stuck with C/C++ and Analog’s compiler there, but I figured I would write the code that runs on the ARM core and orchestrates the DSPs and peripherals in rust. I had hoped that I’d find examples of others who had gone down this path but after a bit of searching I quickly realized I was on my own. Given that there isn’t any info online that I could find, I figured I’d document the process here.
Writing a simple program
I started out with the embedonomicon, and figured I’d use the example in there as a starting point, and adapt it to the target architecture. The Cortex-A5 specs on the ARM site say the architecture is Armv7-a, so I added that as a target, rather than the thumbv7
architecture used in the example.
$ rustup target add armv7a-none-eabi
And set the target architecture in my .cargo/config.toml
:
[build]
target = "armv7a-none-eabi"
Its worth noting at this point that there is currently no std crate available for armv7a, although I’m just trying to get a no_std
program together. I wrote a pretty simple main
function. On the dev board I have, there are 3 LEDs wired up to outputs 0-2 on Port D. The main
function is what I think is the smallest amount of work I can do to turn some of them on: set those 3 pins as outputs, and set 2 of them high
#[no_mangle]
fn main() -> ! {
const PORTD_DIR: *mut u32 = 0x31004198 as *mut u32;
const PORTD_DATA: *mut u32 = 0x3100418C as *mut u32;
unsafe {
*PORTD_DIR = 0x07;
*PORTD_DATA = 0x05;
}
loop {};
}
One difference between the Cortex-M used as an example in the Embedonomicon and the Cortex-A5 I’m targeting is that the Cortex-A5 doesn’t expect a vector table at 0x00000000
. I played around with a generic ARMv7-A linker script I found somewhere for a bit and got something building. I built one of the example programs for the ARM core in the SC-589 in CrossCore Embedded Studio, and used objdump to find where a symbol named something like start
or _start
was. I wanted to see if it matched where my linker script was putting my Reset
function. Of course it did not. I started digging through the CCES install directory and found the linker scripts that it uses, adsp-sc58x-common.ld
and adsp-sc589.ld
. I copied those into the project root and tweaked the rustflags line in my .cargo/config.toml to use those, rather than the link.x
file from the example.
[target.armv7a-none-eabi]
rustflags = ["-C", "link-arg=-Tadsp-sc58x-common.ld", "-C", "link-arg=-Tadsp-sc589.ld"]
I changed the entrypoint in the linker script to Reset
from _start
, and made sure that the Reset
function in my runtime was decorated with #[link_section = ".init"]
so the program entry point would be placed in the same place the CCES project’s entry point was, which is in the ARM core’s L3 memory.
(not) Running on hardware
I figured at this point I could sidestep learning OpenOCD arcana and instead use an existing CrossCore project to flash my new binary onto the dev board. I’ve got an arm64 mac, and am running windows in parallels, and it turns out that is going to be a problem. The drivers for the analog devices debugger that I have (ICE-1000) use UsbDk and that doesn’t support ARM (yet). I spent a bit of time trying
(unsuccessfully) to build Analog Devices’ fork of openocd
(available from ftp.analog.com) for MacOS to try to run it natively, and eventually set that aside.
Emulator
The embedonomicon example has you run the program in Qemu which seemed like a good backup plan. I ran
$ qemu-system-arm -cpu help
Available CPUs:
...
cortex-a15
cortex-a7
cortex-a8
cortex-a9
cortex-m0
cortex-m3
cortex-m33
cortex-m4
cortex-m55
cortex-m7
cortex-r5
cortex-r52
cortex-r5f
...
and realized that cortex-a5 isn’t supported. We’ll add support for it. Some searching turns up a couple unmerged patches. That seems like a good starting point, but it turns out ADI has their own fork of qemu that they ship with CrossCore embedded studio. I grabbed the source for the latest available version of that and was eventually able to incorporate the bits related to the sc589 into the current HEAD of qemu, with a few changes, mostly related to the xml memory map parsing functionality which was not available in main. I ended up hardcoding a few memory sections based on the xml map from ADI’s forked qemu repo. The final diff of those changes against origin/master (97c872276)
is:
diff --git a/hw/arm/adsp_arm.c b/hw/arm/adsp_arm.c
new file mode 100644
index 0000000000..662c55c75f
--- /dev/null
+++ b/hw/arm/adsp_arm.c
@@ -0,0 +1,99 @@
+/*
+ * Analog Devices ARM processor emulation.
+ *
+ * Copyright (c) 2005-2007 CodeSourcery.
+ * Written by Paul Brook
+ *
+ * Portions Copyright (c) 2019-2021 Analog Devices, Inc. All Rights Reserved.
+ *
+ * This code is licensed under the GPL
+ */
+
+#include "qemu/osdep.h"
+#include "qapi/error.h"
+#include "cpu.h"
+#include "hw/sysbus.h"
+#include "migration/vmstate.h"
+#include "hw/boards.h"
+#include "hw/arm/boot.h"
+#include "hw/misc/arm_integrator_debug.h"
+#include "hw/net/smc91c111.h"
+#include "net/net.h"
+#include "exec/address-spaces.h"
+#include "sysemu/runstate.h"
+#include "sysemu/sysemu.h"
+#include "qemu/error-report.h"
+#include "hw/char/pl011.h"
+#include "hw/hw.h"
+#include "hw/irq.h"
+#include "qemu/datadir.h"
+
+
+static void add_adsp_arm_memory()
+{
+ MemoryRegion *sys_mem = get_system_memory();
+ Error *err = NULL;
+
+ MemoryRegion * ram_mem_reg = g_new(MemoryRegion, 1);
+ memory_region_init_ram(ram_mem_reg, NULL, "Boot ROM 0", 0x8000, &err);
+ memory_region_add_subregion(sys_mem, 0x0, ram_mem_reg);
+
+ ram_mem_reg = g_new(MemoryRegion, 1);
+ memory_region_init_ram(ram_mem_reg, NULL, "L2 RAM", 0x40000, &err);
+ memory_region_add_subregion(sys_mem, 0x20080000, ram_mem_reg);
+
+ ram_mem_reg = g_new(MemoryRegion, 1);
+ memory_region_init_ram(ram_mem_reg, NULL, "SMC Partition 0", 0x8000, &err);
+ memory_region_add_subregion(sys_mem, 0x40000000, ram_mem_reg);
+
+ ram_mem_reg = g_new(MemoryRegion, 1);
+ memory_region_init_ram(ram_mem_reg, NULL, "DMC1 (DDR-B)", 0x10000000, &err);
+ memory_region_add_subregion(sys_mem, 0xC0000000, ram_mem_reg);
+
+}
+
+static void adsp_arm_init(MachineState *machine)
+{
+ const char *cpu_model = machine->cpu_type;
+ const char *kernel_filename = machine->kernel_filename;
+ const char *kernel_cmdline = machine->kernel_cmdline;
+ const char *initrd_filename = machine->initrd_filename;
+ Object *cpuobj;
+ ARMCPU *cpu;
+ static struct arm_boot_info binfo;
+
+ cpuobj = object_new(cpu_model);
+ object_property_set_bool(cpuobj, "realized", true, &error_fatal);
+ cpu = ARM_CPU(cpuobj);
+
+ if (!cpu) {
+ fprintf(stderr, "qemu: could not find CPU definition for '%s'\n",
+ cpu_model);
+ exit(1);
+ }
+
+ add_adsp_arm_memory();
+
+ binfo.kernel_filename = kernel_filename;
+ binfo.kernel_cmdline = kernel_cmdline;
+ binfo.initrd_filename = initrd_filename;
+ binfo.ram_size = machine->ram_size;
+ arm_load_kernel(cpu, machine, &binfo);
+}
+
+static void adsp_arm_machine_init(MachineClass *mc)
+{
+ mc->desc = "Analog Devices ADSP-SC5xx ARM Core";
+ mc->init = adsp_arm_init;
+ mc->max_cpus = 1;
+ mc->ignore_memory_transaction_failures = true;
+ mc->default_ram_id = "adsp_arm.ram";
+#ifdef TARGET_AARCH64
+ mc->default_cpu_type = ARM_CPU_TYPE_NAME("adsp-sc598");
+#else
+ mc->default_cpu_type = ARM_CPU_TYPE_NAME("cortex-a5");
+#endif
+ mc->is_default = true;
+}
+
+DEFINE_MACHINE("adsp-arm", adsp_arm_machine_init);
diff --git a/hw/arm/meson.build b/hw/arm/meson.build
index 6808135c1f..11d2692ceb 100644
--- a/hw/arm/meson.build
+++ b/hw/arm/meson.build
@@ -60,6 +60,8 @@ arm_ss.add(when: 'CONFIG_FSL_IMX6UL', if_true: files('fsl-imx6ul.c', 'mcimx6ul-e
arm_ss.add(when: 'CONFIG_NRF51_SOC', if_true: files('nrf51_soc.c'))
arm_ss.add(when: 'CONFIG_XEN', if_true: files('xen_arm.c'))
+
+arm_ss.add(files('adsp_arm.c'))
system_ss.add(when: 'CONFIG_ARM_SMMUV3', if_true: files('smmu-common.c'))
system_ss.add(when: 'CONFIG_CHEETAH', if_true: files('palm.c'))
system_ss.add(when: 'CONFIG_COLLIE', if_true: files('collie.c'))
diff --git a/hw/arm/virt.c b/hw/arm/virt.c
index 3c93c0c0a6..807cfdfbde 100644
--- a/hw/arm/virt.c
+++ b/hw/arm/virt.c
@@ -2982,6 +2982,7 @@ static void virt_machine_class_init(ObjectClass *oc, void *data)
HotplugHandlerClass *hc = HOTPLUG_HANDLER_CLASS(oc);
static const char * const valid_cpu_types[] = {
#ifdef CONFIG_TCG
+ ARM_CPU_TYPE_NAME("cortex-a5"),
ARM_CPU_TYPE_NAME("cortex-a7"),
ARM_CPU_TYPE_NAME("cortex-a15"),
#ifdef TARGET_AARCH64
diff --git a/target/arm/tcg/cpu32.c b/target/arm/tcg/cpu32.c
index bdd82d912a..055fc2fea5 100644
--- a/target/arm/tcg/cpu32.c
+++ b/target/arm/tcg/cpu32.c
@@ -448,6 +448,49 @@ static const ARMCPRegInfo cortexa15_cp_reginfo[] = {
.access = PL1_RW, .type = ARM_CP_CONST, .resetvalue = 0 },
};
+
+static const ARMCPRegInfo cortexa5_cp_reginfo[] = {
+ { .name = "L2LOCKDOWN", .cp = 15, .crn = 9, .crm = 0, .opc1 = 1, .opc2 = 0, .access = PL1_RW, .type = ARM_CP_CONST, .resetvalue = 0 },
+ { .name = "L2AUXCR", .cp = 15, .crn = 9, .crm = 0, .opc1 = 1, .opc2 = 2, .access = PL1_RW, .type = ARM_CP_CONST, .resetvalue = 0},
+};
+static void cortex_a5_initfn(Object *obj)
+{
+ ARMCPU *cpu = ARM_CPU(obj);
+
+ cpu->dtb_compatible = "arm,cortex-a5";
+ set_feature(&cpu->env, ARM_FEATURE_V7);
+ set_feature(&cpu->env, ARM_FEATURE_NEON);
+ set_feature(&cpu->env, ARM_FEATURE_THUMB2EE);
+ set_feature(&cpu->env, ARM_FEATURE_DUMMY_C15_REGS);
+ cpu->midr = 0x410fc080;
+ cpu->reset_fpsid = 0x410330c0;
+ cpu->isar.mvfr0 = 0x11110222;
+ cpu->isar.mvfr1 = 0x00011100;
+ cpu->ctr = 0x82048004;
+ cpu->reset_sctlr = 0x00c50078;
+ cpu->isar.id_pfr0 = 0x00001031;
+ cpu->isar.id_pfr1 = 0x00000011;
+ cpu->isar.id_dfr0 = 0x00000400;
+ cpu->id_afr0 = 0x00000000;
+ cpu->isar.id_mmfr0 = 0x31100003;
+ cpu->isar.id_mmfr1 = 0x20000000;
+ cpu->isar.id_mmfr2 = 0x01202000;
+ cpu->isar.id_mmfr3 = 0x00000011;
+ cpu->isar.id_isar0 = 0x00101111;
+ cpu->isar.id_isar1 = 0x12112111;
+ cpu->isar.id_isar2 = 0x21232031;
+ cpu->isar.id_isar3 = 0x11112131;
+ cpu->isar.id_isar4 = 0x00111142;
+ cpu->isar.dbgdidr = 0x1203f001;
+ cpu->clidr = (1 << 27) | (2 << 24) | 3;
+ cpu->ccsidr[0] = 0xe007e01a;
+ cpu->ccsidr[1] = 0x2007e01a;
+ cpu->ccsidr[2] = 0xf0000000;
+ cpu->reset_auxcr = 2;
+ define_arm_cp_regs(cpu, cortexa5_cp_reginfo);
+}
+
+
static void cortex_a7_initfn(Object *obj)
{
ARMCPU *cpu = ARM_CPU(obj);
@@ -991,6 +1034,7 @@ static const ARMCPUInfo arm_tcg_cpus[] = {
{ .name = "arm1136", .initfn = arm1136_initfn },
{ .name = "arm1176", .initfn = arm1176_initfn },
{ .name = "arm11mpcore", .initfn = arm11mpcore_initfn },
+ { .name = "cortex-a5", .initfn = cortex_a5_initfn },
{ .name = "cortex-a7", .initfn = cortex_a7_initfn },
{ .name = "cortex-a8", .initfn = cortex_a8_initfn },
{ .name = "cortex-a9", .initfn = cortex_a9_initfn },
With that patch, we can run our program in qemu with gdb enabled:
$ /path/to/my/patched/qemu-system-arm \
-cpu cortex-a5 \
-machine adsp-arm \
-nographic \
-semihosting-config enable=on,target=native \
-kernel /target/armv7a-none-eabi/debug/cortex \
-gdb tcp::3333 -S
and connect to it from gdb:
$ arm-none-eabi-gdb -q target/armv7a-none-eabi/debug/cortex
Reading symbols from target/armv7a-none-eabi/debug/cortex...
(gdb) target remote :3333
Remote debugging using :3333
cortex::runtime::Reset () at src/runtime.rs:7
7 main()
(gdb) step
cortex::main () at src/main.rs:12
12 *PORTD_DIR = 0x07;
(gdb) disas
Dump of assembler code for function cortex::main:
0xc1000000 <+0>: movw r1, #16792 ; 0x4198
0xc1000004 <+4>: movt r1, #12544 ; 0x3100
0xc1000008 <+8>: mov r0, #7
=> 0xc100000c <+12>: str r0, [r1]
0xc1000010 <+16>: movw r1, #16780 ; 0x418c
0xc1000014 <+20>: movt r1, #12544 ; 0x3100
0xc1000018 <+24>: mov r0, #5
0xc100001c <+28>: str r0, [r1]
0xc1000020 <+32>: b 0xc1000024 <cortex::main+36>
0xc1000024 <+36>: b 0xc1000024 <cortex::main+36>
End of assembler dump.
and we’re in business! Up next is getting OpenOCD running with ICE1000 support so I can run this thing on the real hardware.