diff --git a/requirements.txt b/requirements.txt
index a2247fea500d67f6dfb762f0214ddbfb921d1b00..daefe6b51c395e2707d63246ccba62d40ad34fd7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
+bitstring
 docrep
 future
 numpy
diff --git a/src/finn/backend/fpgadataflow/utils.py b/src/finn/backend/fpgadataflow/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..b0268237739476e3a8fb99377fda9f76a6a8c5c2
--- /dev/null
+++ b/src/finn/backend/fpgadataflow/utils.py
@@ -0,0 +1,51 @@
+import sys
+
+import numpy as np
+
+from finn.core.datatype import DataType
+from finn.core.utils import pack_innermost_dim_as_hex_string
+
+
+def numpy_to_hls_code(ndarray, dtype, hls_var_name, pack_innermost_dim=True):
+    """Return C++ code representation of a numpy ndarray with FINN DataType
+    dtype, using hls_var_name as the resulting C++ variable name. If
+    pack_innermost_dim is specified, the innermost dimension of the ndarray
+    will be packed into a hex string using array2hexstring.
+    """
+    hls_dtype = dtype.get_hls_datatype_str()
+    if type(ndarray) != np.ndarray or ndarray.dtype != np.float32:
+        # try to convert to a float numpy array (container dtype is float)
+        ndarray = np.asarray(ndarray, dtype=np.float32)
+    if pack_innermost_dim:
+        idimlen = ndarray.shape[-1]
+        idimbits = idimlen * dtype.bitwidth()
+        ndarray = pack_innermost_dim_as_hex_string(ndarray, dtype, idimbits)
+        hls_dtype = "ap_uint<%d>" % idimbits
+    ndims = ndarray.ndim
+    # add type string and variable name
+    # e.g. "const ap_uint<64>" "weightMem0"
+    ret = "%s %s" % (hls_dtype, hls_var_name)
+    # add dimensions
+    for d in range(ndims):
+        ret += "[%d]" % ndarray.shape[d]
+    orig_printops = np.get_printoptions()
+    np.set_printoptions(threshold=sys.maxsize)
+
+    # define a function to convert a single element into a C++ init string
+    # a single element can be a hex string if we are using packing
+    def elem2str(x):
+        if type(x) == str or type(x) == np.str_ or type(x) == np.str:
+            return '%s("%s", 16)' % (hls_dtype, x)
+        elif type(x) == np.float32:
+            if dtype == DataType.FLOAT32:
+                return str(x)
+            else:
+                return str(int(x))
+        else:
+            raise Exception("Unsupported type for numpy_to_hls_code")
+
+    strarr = np.array2string(ndarray, separator=", ", formatter={"all": elem2str})
+    np.set_printoptions(**orig_printops)
+    strarr = strarr.replace("[", "{").replace("]", "}")
+    ret = ret + " = \n" + strarr + ";"
+    return ret
diff --git a/src/finn/core/datatype.py b/src/finn/core/datatype.py
index 42a366aafcc002a433d0e03c03ef6a9bed6adede..34eb5abc247c2d44d33a6a57406fba6887316b98 100644
--- a/src/finn/core/datatype.py
+++ b/src/finn/core/datatype.py
@@ -130,3 +130,13 @@ class DataType(Enum):
         """Return whether this DataType represents integer values only."""
         # only FLOAT32 is noninteger for now
         return self != DataType.FLOAT32
+
+    def get_hls_datatype_str(self):
+        """Return the corresponding Vivado HLS datatype name."""
+        if self.is_integer():
+            if self.signed():
+                return "ap_int<%d>" % self.bitwidth()
+            else:
+                return "ap_uint<%d>" % self.bitwidth()
+        else:
+            return "float"
diff --git a/src/finn/core/utils.py b/src/finn/core/utils.py
index 00dad9655036e496870d60adaa358e2156859599..19c51f65990902898683a492811b83c495d73b38 100644
--- a/src/finn/core/utils.py
+++ b/src/finn/core/utils.py
@@ -3,6 +3,9 @@ import string
 
 import numpy as np
 import onnx
+from bitstring import BitArray
+
+from finn.core.datatype import DataType
 
 
 def valueinfo_to_tensor(vi):
@@ -35,3 +38,72 @@ def random_string(stringLength=6):
     """Randomly generate a string of letters and digits."""
     lettersAndDigits = string.ascii_letters + string.digits
     return "".join(random.choice(lettersAndDigits) for i in range(stringLength))
+
+
+def array2hexstring(array, dtype, pad_to_nbits):
+    """
+    Pack given one-dimensional NumPy array with FINN DataType dtype into a hex
+    string.
+    Any BIPOLAR values will be converted to a single bit with a 0 representing
+    -1.
+    pad_to_nbits is used to prepend leading zeros to ensure packed strings of
+    fixed width. The minimum value for pad_to_nbits is 4, since a single hex
+    digit is four bits.
+
+    Examples:
+    array2hexstring([1, 1, 1, 0], DataType.BINARY, 4) = "e"
+    array2hexstring([1, 1, 1, 0], DataType.BINARY, 8) = "0e"
+    """
+    if pad_to_nbits < 4:
+        pad_to_nbits = 4
+    # ensure input is a numpy array with float values
+    if type(array) != np.ndarray or array.dtype != np.float32:
+        # try to convert to a float numpy array (container dtype is float)
+        array = np.asarray(array, dtype=np.float32)
+    # ensure one-dimensional array to pack
+    assert array.ndim == 1
+    if dtype == DataType.BIPOLAR:
+        # convert bipolar values to binary
+        array = (array + 1) / 2
+        dtype = DataType.BINARY
+    lineval = BitArray(length=0)
+    bw = dtype.bitwidth()
+    for val in array:
+        # ensure that this value is permitted by chosen dtype
+        assert dtype.allowed(val)
+        if dtype.is_integer():
+            if dtype.signed():
+                lineval.append(BitArray(int=int(val), length=bw))
+            else:
+                lineval.append(BitArray(uint=int(val), length=bw))
+        else:
+            lineval.append(BitArray(float=val, length=bw))
+    if pad_to_nbits >= lineval.len:
+        # extend to the desired output width (a minimum of 4 bits)
+        lineval.prepend(BitArray(length=pad_to_nbits - lineval.len))
+    else:
+        raise Exception("Number of bits is greater than pad_to_nbits")
+    # represent as hex
+    return lineval.hex
+
+
+def pack_innermost_dim_as_hex_string(ndarray, dtype, pad_to_nbits):
+    """Pack the innermost dimension of the given numpy ndarray into hex
+    strings using array2hexstring. Examples:
+
+    A = [[1, 1, 1, 0], [0, 1, 1, 0]]
+    eA = ["0e", "06"]
+    pack_innermost_dim_as_hex_string(A, DataType.BINARY, 8) == eA
+    B = [[[3, 3], [3, 3]], [[1, 3], [3, 1]]]
+    eB = [[ "0f", "0f"], ["07", "0d"]]
+    pack_innermost_dim_as_hex_string(B, DataType.UINT2, 8) == eB
+    """
+
+    if type(ndarray) != np.ndarray or ndarray.dtype != np.float32:
+        # try to convert to a float numpy array (container dtype is float)
+        ndarray = np.asarray(ndarray, dtype=np.float32)
+
+    def fun(x):
+        return array2hexstring(x, dtype, pad_to_nbits)
+
+    return np.apply_along_axis(fun, ndarray.ndim - 1, ndarray)
diff --git a/tests/test_npy2hls.py b/tests/test_npy2hls.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea54d900a8e3a06b53cb4074072b15747c6c598c
--- /dev/null
+++ b/tests/test_npy2hls.py
@@ -0,0 +1,43 @@
+import numpy as np
+
+from finn.backend.fpgadataflow.utils import numpy_to_hls_code
+from finn.core.datatype import DataType
+from finn.core.utils import array2hexstring, pack_innermost_dim_as_hex_string
+
+
+def test_array2hexstring():
+    assert array2hexstring([1, 1, 1, 0], DataType.BINARY, 4) == "e"
+    assert array2hexstring([1, 1, 1, 0], DataType.BINARY, 8) == "0e"
+    assert array2hexstring([1, 1, 1, -1], DataType.BIPOLAR, 8) == "0e"
+    assert array2hexstring([3, 3, 3, 3], DataType.UINT2, 8) == "ff"
+    assert array2hexstring([1, 3, 3, 1], DataType.UINT2, 8) == "7d"
+    assert array2hexstring([1, -1, 1, -1], DataType.INT2, 8) == "77"
+    assert array2hexstring([1, 1, 1, -1], DataType.INT4, 16) == "111f"
+    assert array2hexstring([-1], DataType.FLOAT32, 32) == "bf800000"
+    assert array2hexstring([17.125], DataType.FLOAT32, 32) == "41890000"
+
+
+def test_pack_innermost_dim_as_hex_string():
+    A = [[1, 1, 1, 0], [0, 1, 1, 0]]
+    eA = np.asarray(["0e", "06"])
+    assert (pack_innermost_dim_as_hex_string(A, DataType.BINARY, 8) == eA).all()
+    B = [[[3, 3], [3, 3]], [[1, 3], [3, 1]]]
+    eB = np.asarray([["0f", "0f"], ["07", "0d"]])
+    assert (pack_innermost_dim_as_hex_string(B, DataType.UINT2, 8) == eB).all()
+
+
+def test_numpy_to_hls_code():
+    def remove_all_whitespace(s):
+        return "".join(s.split())
+
+    A = [[1, 1, 1, 0], [0, 1, 1, 0]]
+    ret = numpy_to_hls_code(A, DataType.BINARY, "test", True)
+    eA = """ap_uint<4> test[2] =
+    {ap_uint<4>("e", 16), ap_uint<4>("6", 16)};"""
+    assert remove_all_whitespace(ret) == remove_all_whitespace(eA)
+    B = [[[3, 3], [3, 3]], [[1, 3], [3, 1]]]
+    ret = numpy_to_hls_code(B, DataType.UINT2, "test", True)
+    eB = """ap_uint<4> test[2][2] =
+    {{ap_uint<4>("f", 16), ap_uint<4>("f", 16)},
+     {ap_uint<4>("7", 16), ap_uint<4>("d", 16)}};"""
+    assert remove_all_whitespace(ret) == remove_all_whitespace(eB)