diff --git a/grc/CMakeLists.txt b/grc/CMakeLists.txt
index adfaa90b8ae8174de1914afa6f3bcbc02a4b8eb7..690c1455bb6339f02df6040a603ab8d449b969c9 100644
--- a/grc/CMakeLists.txt
+++ b/grc/CMakeLists.txt
@@ -10,5 +10,6 @@ install(FILES
     elen90089_corr_est_cc.block.yml
     elen90089_moe_symbol_sync_cc.block.yml
     elen90089_symbol_mapper_c.block.yml
+    elen90089_header_format_cdc.block.yml
     DESTINATION share/gnuradio/grc/blocks
 )
diff --git a/grc/elen90089_header_format_cdc.block.yml b/grc/elen90089_header_format_cdc.block.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0bee652f6d423161eb42cba630cbd3bde1c584c2
--- /dev/null
+++ b/grc/elen90089_header_format_cdc.block.yml
@@ -0,0 +1,24 @@
+id: header_format_cdc
+label: CDC Header Format
+category: '[elen90089]'
+flags: [ show_id ]
+
+templates:
+  imports: import elen90089
+  make: elen90089.header_format_cdc(${access_code}, ${threshold}, ${bps})
+
+parameters:
+- id: access_code
+  label: Access Code
+  dtype: string
+  default: '11001010'
+- id: threshold
+  label: Threshold
+  dtype: int
+  default: 3
+- id: bps
+  label: Bits per symbol
+  dtype: int
+  default: 1
+  
+file_format: 1
diff --git a/include/elen90089/CMakeLists.txt b/include/elen90089/CMakeLists.txt
index e47a60c06608643b7f30bf5d5de8147b3f53e85c..01411424550ddb7307cb581024c5b8d59e98eb84 100644
--- a/include/elen90089/CMakeLists.txt
+++ b/include/elen90089/CMakeLists.txt
@@ -14,5 +14,6 @@ install(FILES
     corr_est_cc.h
     moe_symbol_sync_cc.h
     symbol_mapper_c.h
+    header_format_cdc.h
     DESTINATION include/elen90089
 )
diff --git a/include/elen90089/header_format_cdc.h b/include/elen90089/header_format_cdc.h
new file mode 100644
index 0000000000000000000000000000000000000000..42d4e3144c4ee83713f153005ece883790ea965c
--- /dev/null
+++ b/include/elen90089/header_format_cdc.h
@@ -0,0 +1,108 @@
+/* -*- c++ -*- */
+/*
+ * Copyright 2022 University of Melbourne.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#ifndef INCLUDED_ELEN90089_HEADER_FORMAT_CDC_H
+#define INCLUDED_ELEN90089_HEADER_FORMAT_CDC_H
+
+#include <elen90089/api.h>
+#include <gnuradio/digital/header_format_default.h>
+#include <pmt/pmt.h>
+
+namespace gr {
+namespace elen90089 {
+
+/*!
+ * \brief Header formatter for CDC packets that adds payload bits/symbol
+ * format and a packet number counter
+ *
+ * \details
+ *
+ * Same as digital::header_format_counter but updates \p bps field of header
+ * based on value found in packet metadata dictionary. Packet structure is:
+ *
+ *   | access_code | hdr | payload |
+ *
+ * Where the access_code is <= 64 bits and hdr is:
+ *
+ *   |  0 -- 15 | 16 -- 31 |   
+ *   | pkt len  | pkt len  |
+ *   | bits/sym | counter  |
+ *
+ * Access code and header are formatted for network byte order (big endian)
+ *
+ */
+class ELEN90089_API header_format_cdc : public digital::header_format_default
+{
+public:
+    typedef std::shared_ptr<header_format_cdc> sptr;
+
+    header_format_cdc(const std::string& access_code,
+                      int threshold,
+                      int bps);
+    
+    ~header_format_cdc() override;
+
+    
+    /*!
+     * Creates a header from the access code and packet length to build an
+     * output packet in the form:
+     *
+     *   | access code | pkt len | pkt len | bps | counter |
+     *
+     * \param nbytes        The length (in bytes) of the \p input payload
+     * \param input         An array of unsigned chars of the packet payload
+     * \param output        A pmt::u8vector with the new header prepended
+     *                      onto the input data.
+     * \param info          A pmt::dict containing meta data and info about
+     *                      the PDU (generally from the metadata portion of the
+     *                      input PDU). Data can be extracted from this for the
+     *                      header formatting or inserted.
+     */
+    bool format(int nbytes,
+                const unsigned char* input,
+                pmt::pmt_t& output,
+                pmt::pmt_t& info) override;
+
+    /*!
+     * Returns the length of the formatted header in bits.
+     */
+    size_t header_nbits() const override;
+
+    /*!
+     * Make a CDC header formatter object.
+     *
+     * \param access_code   Frame access code, string of 0s and 1s (<= 64 bits)
+     * \param threshold     Bit error threshold in access code detection.
+     * \param bps           Default payload bits per symbol. Updated based on
+     *                      bps field in metadata.
+     */
+    static sptr make(const std::string& access_code,
+                     int threshold,
+                     int bps);
+
+protected:
+    uint16_t d_counter; //!< keeps track of the number of packets transmitted
+
+    //! Verify that the header is valid
+    bool header_ok() override;
+
+    /*! Get info from the header; return payload length and package
+     *  rest of data in d_info dictionary.
+     *
+     * Extracts the header of the form:
+     *
+     * \verbatim
+     *   | access code | pkt len | pkt len | bps | counter | payload |
+     * \endverbatim
+     */
+    int header_payload() override;
+};
+
+} // namespace elen90089
+} // namespace gr
+
+#endif /* INCLUDED_ELEN90089_HEADER_FORMAT_CDC_H */
diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt
index 8b8158b74b647b76cec672be3b5720e3e577d60b..e6470911aa94bebf40d3c57e3d4d1cb2aca035f7 100644
--- a/lib/CMakeLists.txt
+++ b/lib/CMakeLists.txt
@@ -15,6 +15,7 @@ list(APPEND elen90089_sources
     corr_est_cc_impl.cc
     moe_symbol_sync_cc_impl.cc
     symbol_mapper_c_impl.cc
+    header_format_cdc.cc
 )
 
 set(elen90089_sources "${elen90089_sources}" PARENT_SCOPE)
@@ -64,6 +65,7 @@ include(GrTest)
 #include_directories()
 # List all files that contain Boost.UTF unit tests here
 list(APPEND test_elen90089_sources
+    qa_header_format_cdc.cc
 )
 # Anything we need to link to for the unit tests go here
 list(APPEND GR_TEST_TARGET_DEPS gnuradio-elen90089)
diff --git a/lib/header_format_cdc.cc b/lib/header_format_cdc.cc
new file mode 100644
index 0000000000000000000000000000000000000000..568f63f14cc1809cc75f018c2a8dc57a22fbb4bb
--- /dev/null
+++ b/lib/header_format_cdc.cc
@@ -0,0 +1,94 @@
+/* -*- c++ -*- */
+/*
+ * Copyright 2022 University of Melbourne.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include <gnuradio/io_signature.h>
+#include <volk/volk_alloc.hh>
+#include <elen90089/header_format_cdc.h>
+
+namespace gr {
+namespace elen90089 {
+
+header_format_cdc::sptr
+header_format_cdc::make(const std::string& access_code, int threshold, int bps)
+{
+    return header_format_cdc::sptr(
+        new header_format_cdc(access_code, threshold, bps));
+}
+
+header_format_cdc::header_format_cdc(const std::string& access_code,
+                                     int threshold,
+                                     int bps)
+    : header_format_default(access_code, threshold, bps)
+{
+    d_counter = 0;
+}
+
+header_format_cdc::~header_format_cdc() {}
+
+bool header_format_cdc::format(int nbytes_in,
+                               const unsigned char* input,
+                               pmt::pmt_t& output,
+                               pmt::pmt_t& info)
+
+{
+    // update bps from metadata if present
+    int bps = d_bps;
+    if (pmt::is_dict(info) && pmt::dict_has_key(info, pmt::mp("bps"))) {
+        pmt::pmt_t val = pmt::dict_ref(info, pmt::mp("bps"), pmt::PMT_NIL);
+        bps = pmt::to_long(val);
+    }
+    assert(bps > 0 && bps < 5);
+
+    // Creating the output pmt copies data; free our own here when done.
+    volk::vector<uint8_t> bytes_out(header_nbytes());
+
+    digital::header_buffer header(bytes_out.data());
+    header.add_field64(d_access_code, d_access_code_len);
+    header.add_field16((uint16_t)(nbytes_in));
+    header.add_field16((uint16_t)(nbytes_in));
+    header.add_field16((uint16_t)(bps));
+    header.add_field16((uint16_t)(d_counter));
+
+    // Package output data into a PMT vector
+    output = pmt::init_u8vector(header_nbytes(), bytes_out.data());
+
+    d_counter++;
+
+    return true;
+}
+
+size_t header_format_cdc::header_nbits() const
+{
+    return d_access_code_len + 8 * 4 * sizeof(uint16_t);
+}
+
+bool header_format_cdc::header_ok()
+{
+    // confirm that two copies of header info are identical
+    uint16_t len0 = d_hdr_reg.extract_field16(0);
+    uint16_t len1 = d_hdr_reg.extract_field16(16);
+    return (len0 ^ len1) == 0;
+}
+
+int header_format_cdc::header_payload()
+{
+    uint16_t len = d_hdr_reg.extract_field16(0);
+    uint16_t bps = d_hdr_reg.extract_field16(32);
+    uint16_t counter = d_hdr_reg.extract_field16(48);
+
+    d_bps = bps;
+
+    d_info = pmt::make_dict();
+    d_info = pmt::dict_add(
+        d_info, pmt::intern("payload symbols"), pmt::from_long(8 * len / d_bps));
+    d_info = pmt::dict_add(d_info, pmt::intern("bps"), pmt::from_long(bps));
+    d_info = pmt::dict_add(d_info, pmt::intern("counter"), pmt::from_long(counter));
+    return static_cast<int>(len);
+}
+
+} /* namespace elen90089 */
+} /* namespace gr */
diff --git a/lib/qa_header_format_cdc.cc b/lib/qa_header_format_cdc.cc
new file mode 100644
index 0000000000000000000000000000000000000000..4896e160e9247aea970650db4b27cabf18585e16
--- /dev/null
+++ b/lib/qa_header_format_cdc.cc
@@ -0,0 +1,60 @@
+
+
+#include <elen90089/header_format_cdc.h>
+#include <gnuradio/attributes.h>
+#include <boost/test/unit_test.hpp>
+
+namespace gr {
+namespace elen90089 {
+
+BOOST_AUTO_TEST_CASE(test_header_format_cdc_bps)
+{
+    static const int N = 4;
+    std::string ac = "10110010"; // 0xB2
+    std::vector<unsigned char> data{ 0x01, 0x02, 0x03, 0x04 };
+    int threshold = 3;
+    int bps = 1;
+    header_format_cdc::sptr hdr_format;
+    hdr_format = header_format_cdc::make(ac, threshold, bps);
+
+    pmt::pmt_t output;
+    pmt::pmt_t info = pmt::make_dict();
+    info = pmt::dict_add(info, pmt::mp("bps"), pmt::from_long(2));
+
+    bool ret = hdr_format->format(N, data.data(), output, info);
+    size_t length = pmt::length(output);
+
+    BOOST_REQUIRE(ret);
+    BOOST_REQUIRE_EQUAL(length, hdr_format->header_nbytes());
+    BOOST_REQUIRE_EQUAL(8 * length, hdr_format->header_nbits());
+
+    // Test access code
+    unsigned char h0 = pmt::u8vector_ref(output, 0);
+    BOOST_REQUIRE_EQUAL(0xB2, (int)h0);
+
+    // Test packet length - 0x0404
+    unsigned char h1 = pmt::u8vector_ref(output, 1);
+    unsigned char h2 = pmt::u8vector_ref(output, 2);
+    unsigned char h3 = pmt::u8vector_ref(output, 3);
+    unsigned char h4 = pmt::u8vector_ref(output, 4);
+    BOOST_REQUIRE_EQUAL(0x00, (int)h1);
+    BOOST_REQUIRE_EQUAL(0x04, (int)h2);
+    BOOST_REQUIRE_EQUAL(0x00, (int)h3);
+    BOOST_REQUIRE_EQUAL(0x04, (int)h4);
+
+    // Test bits/symbol - 0x0002
+    unsigned char h5 = pmt::u8vector_ref(output, 5);
+    unsigned char h6 = pmt::u8vector_ref(output, 6);
+    BOOST_REQUIRE_EQUAL(0x00, (int)h5);
+    BOOST_REQUIRE_EQUAL(0x02, (int)h6);
+
+    // Test counter - 0x0000
+    unsigned char h7 = pmt::u8vector_ref(output, 7);
+    unsigned char h8 = pmt::u8vector_ref(output, 8);
+    BOOST_REQUIRE_EQUAL(0x00, (int)h7);
+    BOOST_REQUIRE_EQUAL(0x00, (int)h8);
+}
+
+} /* namespace elen90089 */
+} /* namespace gr */
+
diff --git a/python/bindings/CMakeLists.txt b/python/bindings/CMakeLists.txt
index 6c6cd4df39a1d296aa4aaaf1bdd78be62e903cdf..95bcf56bf41e4fec2d69113d490e0eee00815ebd 100644
--- a/python/bindings/CMakeLists.txt
+++ b/python/bindings/CMakeLists.txt
@@ -32,6 +32,7 @@ list(APPEND elen90089_python_files
     corr_est_cc_python.cc
     moe_symbol_sync_cc_python.cc
     symbol_mapper_c_python.cc
+    header_format_cdc_python.cc
     python_bindings.cc)
 
 GR_PYBIND_MAKE_OOT(elen90089
diff --git a/python/bindings/docstrings/header_format_cdc_pydoc_template.h b/python/bindings/docstrings/header_format_cdc_pydoc_template.h
new file mode 100644
index 0000000000000000000000000000000000000000..48b65de9b9f19a1c26e9a6c68473c8209367f104
--- /dev/null
+++ b/python/bindings/docstrings/header_format_cdc_pydoc_template.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 Free Software Foundation, Inc.
+ *
+ * This file is part of GNU Radio
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+#include "pydoc_macros.h"
+#define D(...) DOC(gr,elen90089, __VA_ARGS__ )
+/*
+  This file contains placeholders for docstrings for the Python bindings.
+  Do not edit! These were automatically extracted during the binding process
+  and will be overwritten during the build process
+ */
+
+
+ 
+ static const char *__doc_gr_elen90089_header_format_cdc = R"doc()doc";
+
+
+ static const char *__doc_gr_elen90089_header_format_cdc_header_format_cdc_0 = R"doc()doc";
+
+
+ static const char *__doc_gr_elen90089_header_format_cdc_header_format_cdc_1 = R"doc()doc";
+
+
+ static const char *__doc_gr_elen90089_header_format_cdc_format = R"doc()doc";
+
+
+ static const char *__doc_gr_elen90089_header_format_cdc_header_nbits = R"doc()doc";
+
+
+ static const char *__doc_gr_elen90089_header_format_cdc_make = R"doc()doc";
+
+  
diff --git a/python/bindings/header_format_cdc_python.cc b/python/bindings/header_format_cdc_python.cc
new file mode 100644
index 0000000000000000000000000000000000000000..5ac77bc8d69570f12fc385cec9ac1e688d562f87
--- /dev/null
+++ b/python/bindings/header_format_cdc_python.cc
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2022 Free Software Foundation, Inc.
+ *
+ * This file is part of GNU Radio
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+/***********************************************************************************/
+/* This file is automatically generated using bindtool and can be manually edited  */
+/* The following lines can be configured to regenerate this file during cmake      */
+/* If manual edits are made, the following tags should be modified accordingly.    */
+/* BINDTOOL_GEN_AUTOMATIC(0)                                                       */
+/* BINDTOOL_USE_PYGCCXML(0)                                                        */
+/* BINDTOOL_HEADER_FILE(header_format_cdc.h)                                        */
+/* BINDTOOL_HEADER_FILE_HASH(a83bb767baf65cce828e12e815183c6a)                     */
+/***********************************************************************************/
+
+#include <pybind11/complex.h>
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+
+namespace py = pybind11;
+
+#include <elen90089/header_format_cdc.h>
+// pydoc.h is automatically generated in the build directory
+#include <header_format_cdc_pydoc.h>
+
+void bind_header_format_cdc(py::module& m)
+{
+
+    using header_format_cdc = ::gr::elen90089::header_format_cdc;
+
+    py::class_<header_format_cdc,
+               gr::digital::header_format_default,
+               std::shared_ptr<header_format_cdc>>(m, "header_format_cdc", D(header_format_cdc))
+
+        .def(py::init(&header_format_cdc::make),
+             py::arg("access_code"),
+             py::arg("threshold"),
+             py::arg("bps"),
+             D(header_format_cdc, make))
+
+        .def("format",
+             &header_format_cdc::format,
+             py::arg("nbytes"),
+             py::arg("input"),
+             py::arg("output"),
+             py::arg("info"),
+             D(header_format_cdc, format))
+
+        .def("header_nbits",
+             &header_format_cdc::header_nbits,
+             D(header_format_cdc, header_nbits));
+
+}
+
diff --git a/python/bindings/python_bindings.cc b/python/bindings/python_bindings.cc
index a6f73a6a9618aff5ff266d5f636df852b18314a9..d46dcdfb7afb3b71c81cfa25552e3060e5807ed1 100644
--- a/python/bindings/python_bindings.cc
+++ b/python/bindings/python_bindings.cc
@@ -24,6 +24,7 @@ namespace py = pybind11;
     void bind_corr_est_cc(py::module& m);
     void bind_moe_symbol_sync_cc(py::module& m);
     void bind_symbol_mapper_c(py::module& m);
+    void bind_header_format_cdc(py::module& m);
 // ) END BINDING_FUNCTION_PROTOTYPES
 
 
@@ -45,6 +46,7 @@ PYBIND11_MODULE(elen90089_python, m)
 
     // Allow access to base block methods
     py::module::import("gnuradio.gr");
+    py::module::import("gnuradio.digital");
 
     /**************************************/
     // The following comment block is used for
@@ -55,5 +57,6 @@ PYBIND11_MODULE(elen90089_python, m)
     bind_corr_est_cc(m);
     bind_moe_symbol_sync_cc(m);
     bind_symbol_mapper_c(m);
+    bind_header_format_cdc(m);
     // ) END BINDING_FUNCTION_CALLS
-}
\ No newline at end of file
+}