diff --git a/src/main/scala/uk/ac/soton/ecs/can/core/CanCore.scala b/src/main/scala/uk/ac/soton/ecs/can/core/CanCore.scala
new file mode 100644
index 0000000000000000000000000000000000000000..3a644e0b4ebc7eca6a5841ccd89e39ed35c468d2
--- /dev/null
+++ b/src/main/scala/uk/ac/soton/ecs/can/core/CanCore.scala
@@ -0,0 +1,168 @@
+// SPDX-FileCopyrightText: 2021 Minyong Li <ml10g20@soton.ac.uk>
+// SPDX-License-Identifier: CERN-OHL-W-2.0
+
+package uk.ac.soton.ecs.can.core
+
+import chisel3._
+import scala.math.{ceil, log}
+
+class CanCore(
+    programMemoryWords: Int,
+    dataMemoryWords: Int,
+    syncReadMemory: Boolean = true,
+    regAfterBlockInitializer: Boolean = true,
+    regBetweenRounds: Boolean = true,
+    regAfterAdder: Boolean = true
+) extends MultiIOModule {
+
+  // ========== Calculated Parameters ========== //
+
+  private val programMemoryAddressWidth = ceil(
+    log(programMemoryWords) / log(2)
+  ).toInt
+  private val controlWordWidth = ControlWord(programMemoryAddressWidth).getWidth
+  private val dataMemoryAddressWidth = ceil(
+    log(dataMemoryWords) / log(2)
+  ).toInt
+  private val blockWidth = 512
+
+  // ========== External I/O Ports ========== //
+
+  val io = IO(new Bundle {
+    val programMemory = new Bundle {
+      val read = new Bundle {
+        val addr = Input(UInt(programMemoryAddressWidth.W))
+        val data = Output(UInt(controlWordWidth.W))
+      }
+      val write = new Bundle {
+        val en = Input(Bool())
+        val addr = Input(UInt(programMemoryAddressWidth.W))
+        val data = Input(UInt(controlWordWidth.W))
+      }
+    }
+    val dataMemory = new Bundle {
+      val take = Input(Bool())
+      val read = new Bundle {
+        val addr = Input(UInt(dataMemoryAddressWidth.W))
+        val data = Output(UInt(blockWidth.W))
+      }
+      val write = new Bundle {
+        val en = Input(Bool())
+        val addr = Input(UInt(dataMemoryAddressWidth.W))
+        val data = Input(UInt(blockWidth.W))
+      }
+    }
+  })
+
+  // ========== Modules ========== //
+
+  private val programMemory = Module(
+    new ProgramMemory(
+      programMemoryAddressWidth,
+      controlWordWidth,
+      programMemoryWords,
+      syncReadMemory
+    )
+  )
+  private val dataMemory = Module(
+    new DataMemory(
+      dataMemoryAddressWidth,
+      blockWidth,
+      dataMemoryWords,
+      syncReadMemory
+    )
+  )
+  private val blockInitializer = Module(new BlockInitializer)
+  private val columnarRound = Module(ChaChaRound.columnar)
+  private val diagonalRound = Module(ChaChaRound.diagonal)
+  private val adder = Module(new Adder)
+  private val xorer = Module(new Xorer)
+
+  // ========== Non-Module Components ========== //
+
+  private val afterBlockInitializer =
+    if (regAfterBlockInitializer)
+      Reg(Vec(16, UInt(32.W)))
+    else
+      Wire(Vec(16, UInt(32.W)))
+  private val betweenRounds =
+    if (regBetweenRounds)
+      Reg(Vec(16, UInt(32.W)))
+    else
+      Wire(Vec(16, UInt(32.W)))
+  private val afterRounds = Reg(Vec(16, UInt(32.W)))
+  private val afterAdder =
+    if (regAfterAdder)
+      Reg(Vec(16, UInt(32.W)))
+    else
+      Wire(Vec(16, UInt(32.W)))
+  private val afterXorer = Wire(Vec(16, UInt(32.W)))
+
+  // ========== Buses (Port Aliases) ========== //
+
+  private val ctrl =
+    programMemory.cw.asTypeOf(ControlWord(programMemoryAddressWidth))
+  private val data =
+    dataMemory.read.map(_.data.asTypeOf(Vec(16, UInt(32.W))))
+
+  // ========== Multiplexers ========== //
+
+  private val muxRoundLoop =
+    Mux(ctrl.roundLoop, afterRounds, afterBlockInitializer)
+  private val muxAddFrom = Mux(ctrl.addFrom, data(0), data(1))
+  private val muxXorFrom = Mux(ctrl.xorFrom, data(0), data(1))
+  private val muxWbFromRound =
+    Mux(ctrl.writeBackFromRound, afterAdder, afterXorer)
+  private val muxWbFromInit =
+    Mux(ctrl.writeBackFromInit, blockInitializer.out, muxWbFromRound)
+
+  // ========== Wiring Between Modules / Components ========== //
+
+  programMemory.br.abs := ctrl.absoluteBranch
+  programMemory.br.rel := ctrl.relativeBranch
+  programMemory.br.addr := ctrl.immediate
+  programMemory.read <> io.programMemory.read
+  programMemory.write <> io.programMemory.write
+
+  dataMemory.read(0).addr := ctrl.ramReadAddress(0)
+  dataMemory.read(1).addr := Mux(
+    io.dataMemory.take,
+    io.dataMemory.read.addr,
+    ctrl.ramReadAddress(1)
+  )
+  io.dataMemory.read.data := dataMemory.read(1).data
+  dataMemory.write.en := Mux(
+    io.dataMemory.take,
+    io.dataMemory.write.en,
+    ctrl.ramWriteEnable
+  )
+  dataMemory.write.addr := Mux(
+    io.dataMemory.take,
+    io.dataMemory.write.addr,
+    ctrl.ramWriteAddress
+  )
+  dataMemory.write.data := Mux(
+    io.dataMemory.take,
+    io.dataMemory.write.data,
+    muxWbFromInit.asUInt
+  )
+
+  blockInitializer.fillConstants := ctrl.fillConstants
+  blockInitializer.incrementBlockCount := ctrl.incrementBlockCount
+  blockInitializer.in := data(0)
+  afterBlockInitializer := blockInitializer.out
+
+  diagonalRound.in := muxRoundLoop
+  betweenRounds := diagonalRound.out
+  columnarRound.in := betweenRounds
+  afterRounds := columnarRound.out
+
+  adder.lhs := afterRounds
+  adder.rhs := muxAddFrom
+  afterAdder := adder.out
+
+  xorer.lhs := afterAdder
+  xorer.rhs := muxXorFrom
+  afterXorer := xorer.out
+
+}