diff --git a/include/circt/Dialect/ESI/ESIStdServices.td b/include/circt/Dialect/ESI/ESIStdServices.td
index 1bd034ba1c18..f4e494589304 100644
--- a/include/circt/Dialect/ESI/ESIStdServices.td
+++ b/include/circt/Dialect/ESI/ESIStdServices.td
@@ -102,3 +102,25 @@ def MMIOServiceDeclOp: ESI_Op<"service.std.mmio",
     std::optional<StringRef> getTypeName() { return "esi.service.std.mmio"; }
   }];
 }
+
+def HostMemServiceDeclOp: ESI_Op<"service.std.hostmem",
+          [HasParent<"::mlir::ModuleOp">, Symbol,
+           DeclareOpInterfaceMethods<ServiceDeclOpInterface>]> {
+  let summary = "Host memory service";
+  let description = [{
+    Declares a service to read/write host memory. Used for DMA services. Must be
+    implemented by a BSP.
+  }];
+
+  let arguments = (ins SymbolNameAttr:$sym_name);
+
+  let assemblyFormat = [{
+    $sym_name attr-dict
+  }];
+
+  let extraClassDeclaration = [{
+    ServicePortInfo readPortInfo();
+    ServicePortInfo writePortInfo();
+    std::optional<StringRef> getTypeName() { return "esi.service.std.hostmem"; }
+  }];
+}
diff --git a/lib/Dialect/ESI/ESIStdServices.cpp b/lib/Dialect/ESI/ESIStdServices.cpp
index c5b4dba24742..6eb2ca06ee4b 100644
--- a/lib/Dialect/ESI/ESIStdServices.cpp
+++ b/lib/Dialect/ESI/ESIStdServices.cpp
@@ -131,3 +131,56 @@ void MMIOServiceDeclOp::getPortList(SmallVectorImpl<ServicePortInfo> &ports) {
                           ChannelType::get(ctxt, IntegerType::get(ctxt, 64))}},
           /*resettable=*/UnitAttr())});
 }
+
+ServicePortInfo HostMemServiceDeclOp::writePortInfo() {
+  auto *ctxt = getContext();
+  auto addressType =
+      IntegerType::get(ctxt, 64, IntegerType::SignednessSemantics::Unsigned);
+
+  // Write port
+  hw::StructType writeType = hw::StructType::get(
+      ctxt,
+      {hw::StructType::FieldInfo{StringAttr::get(ctxt, "address"), addressType},
+       hw::StructType::FieldInfo{
+           StringAttr::get(ctxt, "tag"),
+           IntegerType::get(ctxt, 8,
+                            IntegerType::SignednessSemantics::Unsigned)},
+       hw::StructType::FieldInfo{StringAttr::get(ctxt, "data"),
+                                 AnyType::get(ctxt)}});
+  return createReqResp(
+      getSymNameAttr(), "write", "req", writeType, "ackTag",
+      IntegerType::get(ctxt, 8, IntegerType::SignednessSemantics::Unsigned));
+}
+
+ServicePortInfo HostMemServiceDeclOp::readPortInfo() {
+  auto *ctxt = getContext();
+  auto addressType =
+      IntegerType::get(ctxt, 64, IntegerType::SignednessSemantics::Unsigned);
+
+  hw::StructType readReqType = hw::StructType::get(
+      ctxt, {
+                hw::StructType::FieldInfo{StringAttr::get(ctxt, "address"),
+                                          addressType},
+                hw::StructType::FieldInfo{
+                    StringAttr::get(ctxt, "tag"),
+                    IntegerType::get(
+                        ctxt, 8, IntegerType::SignednessSemantics::Unsigned)},
+            });
+  hw::StructType readRespType = hw::StructType::get(
+      ctxt, {
+                hw::StructType::FieldInfo{
+                    StringAttr::get(ctxt, "tag"),
+                    IntegerType::get(
+                        ctxt, 8, IntegerType::SignednessSemantics::Unsigned)},
+                hw::StructType::FieldInfo{StringAttr::get(ctxt, "data"),
+                                          AnyType::get(ctxt)},
+            });
+  return createReqResp(getSymNameAttr(), "read", "req", readReqType, "resp",
+                       readRespType);
+}
+
+void HostMemServiceDeclOp::getPortList(
+    SmallVectorImpl<ServicePortInfo> &ports) {
+  ports.push_back(writePortInfo());
+  ports.push_back(readPortInfo());
+}
diff --git a/test/Dialect/ESI/services.mlir b/test/Dialect/ESI/services.mlir
index 6915b14981f7..31c6713bba1f 100644
--- a/test/Dialect/ESI/services.mlir
+++ b/test/Dialect/ESI/services.mlir
@@ -227,3 +227,25 @@ hw.module @MMIOManifest(in %clk: !seq.clock, in %rst: i1) {
   %dataChannel, %dataChannelReady = esi.wrap.vr %data, %valid: i64
   %cmdChannel = esi.bundle.unpack %dataChannel from %reqRW : !mmioRWReq
 }
+
+// CONN-LABEL:  esi.service.std.hostmem @hostmem
+// CONN-LABEL:  hw.module @HostmemRW(in %clk : !seq.clock, in %rst : i1, in %write : !esi.channel<!hw.struct<address: ui64, tag: ui8, data: i128>>, in %readAddress : !esi.channel<!hw.struct<address: ui64, tag: ui8>>, in %hostmemWrite : !esi.bundle<[!esi.channel<!hw.struct<address: ui64, tag: ui8, data: i128>> from "req", !esi.channel<ui8> to "ackTag"]>, in %hostmemRead : !esi.bundle<[!esi.channel<!hw.struct<address: ui64, tag: ui8>> from "req", !esi.channel<!hw.struct<tag: ui8, data: i64>> to "resp"]>, out readData : !esi.channel<!hw.struct<tag: ui8, data: i64>>, out writeDone : !esi.channel<ui8>) {
+// CONN-NEXT:     esi.manifest.req #esi.appid<"hostmemWrite">, <@hostmem::@write> std "esi.service.std.hostmem", !esi.bundle<[!esi.channel<!hw.struct<address: ui64, tag: ui8, data: i128>> from "req", !esi.channel<ui8> to "ackTag"]>
+// CONN-NEXT:     %ackTag = esi.bundle.unpack %write from %hostmemWrite : !esi.bundle<[!esi.channel<!hw.struct<address: ui64, tag: ui8, data: i128>> from "req", !esi.channel<ui8> to "ackTag"]>
+// CONN-NEXT:     esi.manifest.req #esi.appid<"hostmemRead">, <@hostmem::@read> std "esi.service.std.hostmem", !esi.bundle<[!esi.channel<!hw.struct<address: ui64, tag: ui8>> from "req", !esi.channel<!hw.struct<tag: ui8, data: i64>> to "resp"]>
+// CONN-NEXT:     %resp = esi.bundle.unpack %readAddress from %hostmemRead : !esi.bundle<[!esi.channel<!hw.struct<address: ui64, tag: ui8>> from "req", !esi.channel<!hw.struct<tag: ui8, data: i64>> to "resp"]>
+// CONN-NEXT:     hw.output %resp, %ackTag : !esi.channel<!hw.struct<tag: ui8, data: i64>>, !esi.channel<ui8>
+
+esi.service.std.hostmem @hostmem
+!hostmemReadReq = !esi.bundle<[!esi.channel<!hw.struct<address: ui64, tag: ui8>> from "req", !esi.channel<!hw.struct<tag: ui8, data: i64>> to "resp"]>
+!hostmemWriteReq = !esi.bundle<[!esi.channel<!hw.struct<address: ui64, tag: ui8, data: i128>> from "req", !esi.channel<ui8> to "ackTag"]>
+
+hw.module @HostmemRW(in %clk : !seq.clock, in %rst : i1, in %write : !esi.channel<!hw.struct<address: ui64, tag: ui8, data: i128>>, in %readAddress : !esi.channel<!hw.struct<address: ui64, tag: ui8>>, out readData : !esi.channel<!hw.struct<tag: ui8, data: i64>>, out writeDone : !esi.channel<ui8>) {
+  %writeBundle = esi.service.req <@hostmem::@write> (#esi.appid<"hostmemWrite">) : !hostmemWriteReq
+  %ackTag = esi.bundle.unpack %write from %writeBundle : !hostmemWriteReq
+
+  %readBundle = esi.service.req <@hostmem::@read> (#esi.appid<"hostmemRead">) : !hostmemReadReq
+  %readData = esi.bundle.unpack %readAddress from %readBundle : !hostmemReadReq
+
+  hw.output %readData, %ackTag: !esi.channel<!hw.struct<tag: ui8, data: i64>>, !esi.channel<ui8>
+}