diff --git a/target/arm/cpu.h b/target/arm/cpu.h
index 3841359d0f..6b3cb8e70e 100644
--- a/target/arm/cpu.h
+++ b/target/arm/cpu.h
@@ -30,6 +30,8 @@
 #include "qapi/qapi-types-common.h"
 #include "target/arm/multiprocessing.h"
 #include "target/arm/gtimer.h"
+#include "chardev/char.h"
+#include "chardev/char-fe.h"
 
 #ifdef TARGET_AARCH64
 #define KVM_HAVE_MCE_INJECTION 1
@@ -523,6 +525,11 @@ typedef struct CPUArchState {
 
         /* NV2 register */
         uint64_t vncr_el2;
+        /*
+         * Debug Trace regsiters
+         */
+        uint32_t dbgdtr_tx;
+        uint32_t dbgdtr_rx;
     } cp15;
 
     struct {
@@ -1097,6 +1104,9 @@ struct ArchCPU {
 
     /* Generic timer counter frequency, in Hz */
     uint64_t gt_cntfrq_hz;
+
+    /* dcc chardev */
+    CharBackend dcc;
 };
 
 typedef struct ARMCPUInfo {
@@ -2388,6 +2398,7 @@ enum arm_features {
      * CPU types added in future.
      */
     ARM_FEATURE_BACKCOMPAT_CNTFRQ, /* 62.5MHz timer default */
+    ARM_FEATURE_DCC,
 };
 
 static inline int arm_feature(CPUARMState *env, int feature)
diff --git a/target/arm/internals.h b/target/arm/internals.h
index 11b5da2562..2fa797c5df 100644
--- a/target/arm/internals.h
+++ b/target/arm/internals.h
@@ -1778,4 +1778,8 @@ uint64_t gt_get_countervalue(CPUARMState *env);
  * and CNTVCT_EL0 (this will be either 0 or the value of CNTVOFF_EL2).
  */
 uint64_t gt_virt_cnt_offset(CPUARMState *env);
+/*
+ * Initialise Coresight Debug interface
+ */
+void arm_dcc_init(ARMCPU *cpu);
 #endif
diff --git a/target/arm/debug-dcc.c b/target/arm/debug-dcc.c
new file mode 100644
index 0000000000..9144b54994
--- /dev/null
+++ b/target/arm/debug-dcc.c
@@ -0,0 +1,99 @@
+#include "qemu/osdep.h"
+#include "cpu.h"
+#include "internals.h"
+#include "cpregs.h"
+#include "chardev/char-fe.h"
+
+#define MDCCSR_EL0_RXFULL_MASK (1 << 30)
+#define MDCCSR_EL0_TXFULL_MASK (1 << 29)
+
+static void debug_dcc_write(CPUARMState *env, const ARMCPRegInfo *ri,
+                        uint64_t value)
+{
+    ARMCPU *cpu = ri->opaque;
+    env->cp15.dbgdtr_tx = value;
+
+    if (qemu_chr_fe_get_driver(&cpu->dcc)) {
+        /*
+         * Usually dcc is used for putc/getc calls which expect only
+         * 1 byte from external debugger.
+         * TODO: This needs to be generalized for other use-cases.
+         */
+        qemu_chr_fe_write_all(&cpu->dcc, (uint8_t *)&env->cp15.dbgdtr_tx, 1);
+    }
+}
+
+static uint64_t debug_dcc_read(CPUARMState *env, const ARMCPRegInfo *ri)
+{
+    uint32_t ret = 0;
+    ARMCPU *cpu = ri->opaque;
+
+    if (env->cp15.mdscr_el1 & MDCCSR_EL0_RXFULL_MASK) {
+        ret = env->cp15.dbgdtr_rx;
+        env->cp15.dbgdtr_rx = 0;
+        env->cp15.mdscr_el1 &= ~MDCCSR_EL0_RXFULL_MASK;
+        qemu_chr_fe_accept_input(&cpu->dcc);
+    }
+    return ret;
+}
+
+static const ARMCPRegInfo dcc_cp_reginfo[] = {
+    /* DBGDTRTX_EL0/DBGDTRRX_EL0 depend on direction */
+    { .name = "DBGDTR_EL0", .state = ARM_CP_STATE_AA64,
+      .opc0 = 2, .opc1 = 3, .crn = 0, .crm = 5, .opc2 = 0,
+      .access = PL0_RW, .writefn = debug_dcc_write,
+      .readfn = debug_dcc_read,
+      .type = ARM_CP_OVERRIDE, .resetvalue = 0 },
+    /* DBGDTRTXint/DBGDTRRXint depend on direction */
+    { .name = "DBGDTRint", .state = ARM_CP_STATE_AA32, .cp = 14,
+      .opc1 = 0, .crn = 0, .crm = 5, .opc2 = 0,
+      .access = PL0_RW, .writefn = debug_dcc_write,
+      .readfn = debug_dcc_read,
+      .type = ARM_CP_OVERRIDE, .resetvalue = 0 },
+};
+
+
+static int dcc_chr_can_read(void *opaque)
+{
+    ARMCPU *cpu = opaque;
+    CPUARMState *env = &cpu->env;
+
+    if (!(env->cp15.mdscr_el1 & MDCCSR_EL0_RXFULL_MASK)) {
+        /*
+         * Usually dcc is used for putc/getc calls which expect only
+         * 1 byte from external debugger.
+         * TODO: This needs to be generalized for other use-cases.
+         */
+        return 1;
+    }
+
+    return 0;
+}
+
+static void dcc_chr_read(void *opaque, const uint8_t *buf, int size)
+{
+    ARMCPU *cpu = opaque;
+    CPUARMState *env = &cpu->env;
+
+    env->cp15.dbgdtr_rx = *buf;
+    env->cp15.mdscr_el1 |= MDCCSR_EL0_RXFULL_MASK;
+}
+
+void arm_dcc_init(ARMCPU *cpu)
+{
+    Chardev *chr;
+    char *dcc_name;
+    CPUState *p = CPU(cpu);
+
+    dcc_name = g_strdup_printf("dcc%d", p->cpu_index);
+    chr = qemu_chr_find(dcc_name);
+    define_arm_cp_regs_with_opaque(cpu, dcc_cp_reginfo, cpu);
+    if (chr) {
+        qemu_chr_fe_init(&cpu->dcc, chr, NULL);
+        qemu_chr_fe_set_handlers(&cpu->dcc,
+                      dcc_chr_can_read,
+                      dcc_chr_read,
+                      NULL, NULL, cpu, NULL, true);
+    }
+    g_free(dcc_name);
+}
diff --git a/target/arm/helper.c b/target/arm/helper.c
index ce31957235..2b594f91cb 100644
--- a/target/arm/helper.c
+++ b/target/arm/helper.c
@@ -9268,6 +9268,9 @@ void register_cp_regs_for_features(ARMCPU *cpu)
         }
     }
 
+    if (arm_feature(&cpu->env, ARM_FEATURE_DCC)) {
+        arm_dcc_init(cpu);
+    }
     /*
      * Register the base EL2 cpregs.
      * Pre v8, these registers are implemented only as part of the
diff --git a/target/arm/meson.build b/target/arm/meson.build
index 2e10464dbb..3ee38c6b45 100644
--- a/target/arm/meson.build
+++ b/target/arm/meson.build
@@ -5,6 +5,7 @@ arm_ss.add(files(
   'gdbstub.c',
   'helper.c',
   'vfp_helper.c',
+  'debug-dcc.c',
 ))
 arm_ss.add(zlib)
 
