.. _uhal-quick-tutorial: uHAL quick tutorial =================== uHAL is the Hardware Access Library (HAL) that provides an end-user C++/Python API for IPbus reads, writes and RMW transactions. .. admonition:: API reference The doxygen pages containing an exhaustive list of all uHAL classes and functions can be found at https://ipbus.web.cern.ch/ipbus/sw/release/2.7/api/html/ **N.B. If you're a new user** it would be best to read the relevant sections of this tutorial at least once before looking at the doxygen pages. Pre-requisites -------------- Before building/running the source code examples from the following sections, you should: * :ref:`Install the IPbus suite ` * Set the environment: .. code-block:: sh export LD_LIBRARY_PATH=/opt/cactus/lib:$LD_LIBRARY_PATH export PATH=/opt/cactus/bin:$PATH * Create a :ref:`connection file ` that describes what protocol (UDP, PCIe, ...) and IP address or device file should be used to communicate with each IPbus endpoint (i.e. each hardware device - typically an FPGA - containing a control bus master). * Create an :ref:`address table ` describing the address layout of your IPbus endpoints. * (*optional*) If you want to work with dummy hardware, then just check the :ref:`working-with-dummy-hardware` section. * (*optional*) If multiple clients/threads/processes have to simulatenously access an IPbus UDP endpoint, then you will need to use the :ref:`Control Hub ` Connecting to the hardware IP endpoint with a connection file ------------------------------------------------------------- Once you have a :ref:`connection file ` describing the location of the hardware endpoint and protocol, and an :ref:`address table ` describing its memory layout, then you can create the ``HwInterface`` by: .. code-block:: c++ #include "uhal/uhal.hpp" using namespace uhal; ConnectionManager manager ( "file://path/to/connection/file/connections.xml" ); HwInterface hw=manager.getDevice ( "hcal.crate1.slot1" ); ValWord< uint32_t > mem = hw.getNode ( "REG" ).read(); hw.dispatch(); std::cout << "REG = " << reg.value() << std::endl; You can see a working example in :download:`read_write_single_register.cxx ` **Tip**: You can inspect the connection file programmatically by means of the ``vector ConnectionManager::getDevices()`` and ``vector ConnectionManager::getDevice(const string& regex)`` methods. Connecting to the hardware IP endpoint without a connection file ---------------------------------------------------------------- If you do not want to use a connection file (e.g. debugging purposes), then you can instantiate a ``HwInterface`` using the ``ConnectionManager::getDevice`` factory method: .. code-block:: c++ HwInterface hw=ConnectionManager::getDevice ( id, uri, address_file ); where the three parameters are strings: * ``id``: String to identify the device. * ``uri``: String with the protocol and location of the hardware endpoint in URI format (e.g. ``ipbusudp-2.0://localhost:50001``). * ``address_table``: String with the location of the file containing the address file. For example: .. code-block:: c++ HwInterface hw=ConnectionManager::getDevice( "on.the.fly","chtcp-2.0://localhost:10203?target=127.0.0.1:50001","file:///path/to/address_file.xml" ); You can see a working example in :download:`read_write_single_register_without_connection_file.cxx ` Reading and writing a register ------------------------------ Let's suppose that in your address table you have a register named ``REG`` in address 0x0001: .. code-block:: xml Each entry in the address table is represented by an instance of the ``uhal::Node`` class. You can read and write to the 32-bit register as follows: .. code-block:: c++ ConnectionManager manager("file://path/to/connections.xml"); HwInterface hw = manager.getDevice("dummy.udp.0"); //write 1 in the address 0x0001 hw.getNode ("REG").write(1); //read back ValWord< uint32_t > reg = hw.getNode ("REG").read(); //send the IPbus transactions hw.dispatch(); std::cout << "REG = " << reg.value() << std::endl; You can see a working example in :download:`read_write_single_register.cxx ` Reading and writing memory blocks --------------------------------- Let's suppose that in your address table you have a 1 MByte memory block (i.e. ``mode="block"`` and ``size=262144`` words) named ``MEM`` in address 0x0001: .. code-block:: xml The usage is pretty similar that the single register. You can write and read back 256 words (i.e. 1 kByte): .. code-block:: c++ ConnectionManager manager("file://path/to/connections.xml"); HwInterface hw = manager.getDevice("dummy.udp.0"); //fill a vector with random information const size_t N=256; std::vector xx; for(size_t i=0; i!= N; ++i) xx.push_back(static_cast(rand())); //write hw.getNode ("MEM").writeBlock(xx); //read back the information ValVector< uint32_t > mem = hw.getNode ( "MEM" ).readBlock (N); hw.dispatch(); //If there is a single client we should read back the same informaiton for(size_t i=0; i!= N; ++i) assert(xx[i] == mem[i]) You can see a working example in :download:`read_write_block_or_fifo.cxx ` Reading and writing to FIFOs ---------------------------- The C++ API for reading a FIFO (aka non-incremental memory or port) and for reading a memory block is identical. The only difference is in the corresponding address table entry: .. code-block:: xml You can see a working example in :download:`read_write_block_or_fifo.cxx ` How to get the node attributes? ------------------------------- You can retrieve programmatically all the attributes from a given node. The following methods are available in the ``Node`` API: * ``uint32_t getAddress()``: Address of the node. * ``uint32_t getMask()``: Mask of the node. * ``const string& getId()``: Id of the node. * ``defs::BlockReadWriteMode getMode()``: One out of ``defs::SINGLE`` (default), ``defs::INCREMENTAL``, and ``defs::NON_INCREMENTAL``, or ``defs::HIERARCHICAL`` for top-level nodes nesting other nodes. * ``defs::NodePermission getPermission()``: One out of ``defs::READ``, ``defs::WRITE``, and ``defs::READWRITE`` (default). * ``uint32_t getSize()``: Size of the node. All the single register access and FIFOs have a default size of 1 (i.e. 32 bits). * ``const string& getTags()``: User definable attribute with in principle a comma separated list of values. * ``const boost::unordered_map& getParameters ()``: Set of "parameter=value" pairs, specified with a semi-colon delimeted "param=value" list in the xml address table "parameters" attribute. You can see a working example in :download:`print_node_attributes.cxx ` How to traverse the address table tree? --------------------------------------- The traversal of the address table tree is pretty straight forward using the methods ``Node::getNode()``. For example, if we apply the following code to :download:`this address table ` (which references :download:`uhal-example/example-address-componentA.xml` and :download:`uhal-example/example-address-componentB.xml`) : .. code-block:: c++ ConnectionManager manager ( connection ); HwInterface hw=manager.getDevice ( id ); std::vector ids = hw.getNodes(); std::cout << "getNodes(): "; std::copy(ids.begin(), ids.end(), std::ostream_iterator(std::cout,", ")); std::cout << std::endl << std::endl; ids = hw.getNodes(".*mem.*"); std::cout << "getNodes(\".*mem.*\").getNodes(): "; std::copy(ids.begin(), ids.end(), std::ostream_iterator(std::cout,", ")); std::cout << std::endl << std::endl; ids = hw.getNode("componentA").getNodes(); std::cout << "getNode("componentA").getNodes(): "; std::copy(ids.begin(), ids.end(), std::ostream_iterator(std::cout,", ")); std::cout << std::endl; We will get the following result:: getNodes(): componentB.reg1, componentB.reg2, componentB.mem1, componentB, componentA.mem1, mem2, mem1, componentA.fifo, componentA.reg1, componentB.mem2, fifo, reg3.fieldB, componentA.mem2, componentB.fifo, reg3.reset, reg3.fieldC, reg3, componentA.reg2, componentA, reg2, reg1, reg3.fieldA, getNodes(".*mem.*"): componentA.mem1, componentA.mem2, componentB.mem1, componentB.mem2, mem1, mem2, getNode("componentA").getNodes(): mem2, mem1, fifo, reg2, reg1, You can see a working example in :download:`print_node_attributes.cxx ` How to insert multiple ``HwInterface`` objects in the same STL container? ------------------------------------------------------------------------- Imagine that you want to create once all the ``HwInterface`` objects and then use them. You could store and use them in a ``std::vector`` as follows: .. code-block:: c++ ConnectionManager manager ( connection ); //get all the hcal endpoints std::vector hws; std::vector ids=manager.getDevices ( "hcal\..*" ); for(std::vector::const_iterator i(ids.begin()); i!=ids.end(); ++i) hws.push_back(manager.getDevice(*i)); //Request the "FIRMWARE_REVISION" register for all of them for(std::vector::iterator hw(hws.begin()); hw != hws.end(); ++hw) { ValWord ver = hw->getNode("FIRMWARE_REVISION").read(); hw->dispatch(); std::cout << hw->id() << " FIRMWARE REVISION: " << std::hex << ver << std:endl; } How do I create an ``HwInterface`` object on the heap? ------------------------------------------------------ It is not recommended that you attempt to manually construct an ``HwInterface`` object - you should always use a ``ConnectionManager`` class to construct the ``HwInterface`` objects for you. So... what do you do if you want to create an ``HwInterface`` object on the heap? If you want to use "new" to create an ``HwInterface`` object, you have to do it via the ``HwInterface`` copy constructor like so: .. code-block:: c++ uhal::ConnectionManager connectionManager("file://path/to/connections.xml"); uhal::HwInterface * hw = new uhal::HwInterface(connectionManager.getDevice("MyBoard")); How to disable the logging? --------------------------- You can disable the logging by executing ``uhal::disableLogging()`` in your program. Or, alternatively, raise the log threshold to something quite high like this: ``uhal::setLogLevelTo(uhal::Error());`` .. _creating-a-connection-file: Creating a connection file -------------------------- Before we can use uHAL to talk to a board, we need to create a configuration file telling uHAL which board to talk to. A configuration file is an XML file like: .. code-block:: xml Each of the ``connection`` tags contain three attributes that describe a device and the protocol used to access to it: * ``id``: String identifier. The standard nomenclature for id's in the CMS trigger upgrade project will be ``subsystem.crate.slot``. * ``uri``: Protocol and location to access a target device in URI format. There are 8 protocols currently available: * For IPbus 1.3 hardware : ``chtcp-1.3``, ``ipbusudp-1.3``, and ``ipbustcp-1.3`` * For IPbus 2.0 hardware : ``chtcp-2.0``, ``ipbusudp-2.0``, ``ipbustcp-2.0``, ``ipbuspcie-2.0`` and ``ipbusmmap-2.0`` * ``address_file``: Location of the :ref:`address table file ` which describes the register space of the target device. The URI can be absolute or relative to the connection file in the local file system (e.g. ``file://my_adress_file.xml``) You can check a working example in :download:`this connections file ` .. _creating-an-address-table: Creating an address table ------------------------- **Note!** *In these examples it is worth remembering that IPbus is an A32/D32 bus, that is, it supports addresses up to 32 bits wide and data spaces up to 32 bits wide. Any particular device/firmware may, however, chose to use only a restricted subset of the total address space.* You can check a working example in :download:`this address table ` (which references :download:`uhal-example/example-address-componentA.xml` and :download:`uhal-example/example-address-componentB.xml`) Single register address table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The simplest address table that we may consider is an XML file like: .. code-block:: xml This address table can be read as follows: *The 32-bit memory space at address 0x00000000 can be accessed with the id "A".* This means that you will be able to read this memory location with the following C++ code: .. code-block:: c++ ConnectionManager manager("file://path/to/connections.xml"); HwInterface hw = manager.getDevice("x.y.z"); ValWord< uint32_t > reg = hw.getNode ("A").read(); hw.dispatch(); std::cout << "A = " << reg.value() << std::endl; Single register in a hierarchical address table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ uHAL provides a hierarchical address table structure that is designed to have a one-to-one correspondence with the hierarchical structure of firmware modules. A simple example of an address table with a hierarchical structure is: .. code-block:: xml This address table can be read as follows: *The firmware module "B" contains a 32-bit memory location at address 0x00000100 with ID "A".* This means that you will be able to read this memory location with the following C++ code: .. code-block:: c++ ConnectionManager manager("file://path/to/connections.xml"); HwInterface hw = manager.getDevice("x.y.z"); //This is equivalent to getNode("B.A") ValWord< uint32_t > reg = hw.getNode("B").getNode("A").read(); hw.dispatch(); std::cout << "B.A = " << reg.value() << std::endl; Multiple modules with absolute and relative addresses ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you have two firmware modules in different addresses you could have an address table like: .. code-block:: xml In this example, the modules "C1" and "C2" refer to different firmware modules. For the memory locations in "C1" we have explicitly specified the absolute address, whereas in "C2" we have specified the relative address of the memory locations with respect to the parent module. For example, the address of the "C2.A2" node is calculated as follows:: AbsoluteAddress( C2.A2 ) = Address( C2 ) + RelativeAddress( C2.A2 ) = 0x00000300 + 0x001 = 0x00000301 Multiple modules with identical structure ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Let's suppose that we have two instances of the same firmware module accessible from a single IPbus device (e.g. "D1" and "D2"). The internal address table structure of the modules will be identical, and therefore it would be a waste of time (and error prone) to type the same structure several times. Instead we can avoid duplication by creating two address tables: 1. A higher-level address table, containing two nodes (with different base addresses) that correspond to the two instances of the firwmare module: .. code-block:: xml 2. The ``mymodule.xml`` file, that specifies the layout of registers, block RAMs, FIFOs etc within the duplicated module, and specifies relative addresses with respect to the parent node: .. code-block:: xml In order to access memory location "A2" within module "D1", you would then write in C++: .. code-block:: c++ ValWord< uint32_t > reg = hw.getNode("D1.A2").read(); Setting read and write access to memory locations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The access permissions (e.g. read-only, read-write, etc) of a node can be set using the ``permission`` attribute: .. code-block:: xml The meanings of these attributes are hopefully self-evident: ``r`` and ``read`` indicate a read-only IPbus endpoint whilst ``w`` and ``write`` indicate a write-only endpoint. The other four options indicate that the IPbus endpoint is both readable and writeable. IPbus endpoints are assumed, by default, to be both readable and writeable. Permissions are currently not inherited from parent to child and so setting the permissions on a branch, rather than to a final memory location, has no effect. Memory blocks and FIFOs ^^^^^^^^^^^^^^^^^^^^^^^ Access to memory blocks and FIFOs can be configured using the ``mode`` attribute: .. code-block:: xml ``single`` indicates that the node refers to a single word (i.e. 32 bits) register (e.g. "F.A1" and "F.A2"). ``single`` is the default mode if no mode is explicitly declared. This can be used, for example, for a bunch crossing zero (BC0) counter. ``block``, ``incremental`` and ``inc`` indicate that the given address is the base address of a block of registers with a continuous address space. In this case, the ``size`` attribute is mandatory (e.g. "F.A3", "F.A4", and "F.A5"). This can be used, for example, for a capture RAM. ``port``, ``non-incremental`` and ``non-inc`` indicate that the given address is an access port which may receive or provide a stream of data but whose address is fixed (e.g. "F.A6", "F.A7", and "F.A8"). This can be used, for example, for a JTAG access port. In order to read from and write to a memory block or FIFO, you could then use the following C++ code: .. code-block:: c++ //read ValVector< uint32_t > mem = hw.getNode("F.A3").readBlock(16); ValVector< uint32_t > fifo = hw.getNode("F.A6").readBlock(16); //write std::vector x; //fill x... hw.getNode("F.A4").writeBlock(x); hw.getNode("F.A7").writeBlock(x); Single register read and write access with bit-masks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Even though IPbus is word-oriented, it is nevertheless convenient to be able to label individual bits. This is done as shown below: .. code-block:: xml Performing an IPbus read or write on, for instance, "G.G3", will perform all of the necessary bit-shifting under-the-hood. For example, reading a value from the node with id "G.G4" will actually perform the appropriate operations and return a value from 0x0 to 0xF, rather than from 0x00 to 0xF0. It should be noted that performing many operations on bit-masked nodes may add a not insignificant overhead when compared to operating on full 32-bit registers. This is due to the nature of IPbus instructions, rather than a software overhead. There is no way for the software to know if it can merge multiple bit-masked operations into a single instruction and it is up to the user to consider the structure of their code with respect to this. Documenting your address table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A convenient way of documenting the address table is to use ``description`` attribute, for example: .. code-block:: xml Much like the :ref:`tags attribute ` discussed below, the content of ``description`` attributes is completely user-definable. An advantage of using the description attribute for address table documentation is that - unlike using XML comments - the content of the attribute can be accessed by your application, through the ``Node::getDescription`` method. .. _what-are-the-tags-and-parameters-attributes: What are the "tags" and "parameters" attributes? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``tags`` and ``parameters`` attributes allow the user to specify a custom list and/or map of properties that are associated with a particular node; the values of these attributes can then be accessed from C++/Python through methods of the ``Node`` class. These attributes **do not have to be set** for any nodes within the address table, and *the values of these attributes do not change the behaviour of any of the uHAL classes/functions* in any way. The value of the ``tags`` attribute can be accessed from C++/Python using the ``Node::getTags()`` method. The ``parameters`` attribute is parsed as a semicolon-delimited map of 'parameter=value' pairs, producing a ``std::map`` that can be accessed from C++/Python using the ``Node::getParameters()`` method. For example, ``parameters="tx0=0x29;tx1=0x2b"`` would result in a map containing 2 entries: key ``tx0`` with value ``0x29``, and key ``tx1`` with value ``0x2b``. Building your code ------------------ Here is an :download:`example Makefile ` for building your project. Once you've downloaded the file, rename it to "Makefile" and then follow the documentation found in the section at the top of the file. Note: The example makefile assumes that the software is installed under ``/opt/cactus`` (e.g. using the `YUM installation instructions `). ---- Using IPbus in Python --------------------- Since release 1.0.6, the IPbus software suite also includes Python-bindings for uHAL. After following the :ref:`IPbus installation instructions `, just import the ``uhal`` module in Python and you can get going. The Python API (i.e. function and class names) is basically the same as in C++, but with all ``std::vector`` instances replaced with Python lists. Several example use-cases are given below. They all assume that you have already done: .. code-block:: python import uhal # Or if using uHAL v1.0.6 (now an old version) import pycohal as uhal Getting a ``HwInterface`` object ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``HwInterface`` objects form the basic interface to your hardware ... * Creating from an :ref:`xml connection file `: .. code-block:: python manager = uhal.ConnectionManager("file://path/to/connection/file/connections.xml") hw = manager.getDevice("hcal.crate1.slot1") * Without a connection file: .. code-block:: python hw = uhal.getDevice( "id_for_device" , uri, address_table ) where: * ``uri`` = String with the protocol and location of the hardware endpoint in URI format (e.g. ``ipbusudp-2.0://localhost:50001``). Syntax explained :ref:`here ` * ``address_table`` = String with location of :ref:`xml address table ` for this hardware device * The attributes of a ``HwInterface`` object can then be accessed later on if needed ... .. code-block:: python # Grab device's id, or print it to screen device_id = hw.id() print hw # Grab the device's URI device_uri = hw.uri() Reading and writing to a single register ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For a single register named ``REG`` in your address table: .. code-block:: python # Queue a write to register hw.getNode("REG").write(1) # Read the value back. # NB: the reg variable below is a uHAL "ValWord", not just a simple integer reg = hw.getNode("REG").read() # Send IPbus transactions hw.dispatch() # Print the register value to screen as a decimal: print " REG =", reg # Print the register value to screen in hex: print " REG =", hex(reg) # Get the underlying integer value from the reg "ValWord" value = int(reg) # Equivalent to reg.value() A similar working example can be found in :download:`read_write_single_register.py ` and :download:`read_write_single_register_without_connection_file.py ` Reading/writing to a memory block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For a 1MByte memory block named ``MEM``: .. code-block:: python N=256 # Fill list with random info xx = [] for i in range(N): xx.append( rand_uint32() ) # Write hw.getNode("MEM").writeBlock(xx) # Read back values mem = hw.getNode("MEM").readBlock(N) hw.dispatch() # Use these values - either by array indexing, in a for loop, or print to screen print " All values:", mem firstValue = mem[0] for x in mem: # Do something with each value A similar working example can be found in :download:`read_write_block_or_fifo.py ` Traversing the address table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The hierarchy of nodes within your hardware's address table can be traversed easily using the ``hw.getNodes(regex_string)`` and ``node.getNodes(regex_string)`` methods. For example: .. code-block:: python # Ids of all nodes list_of_ids = hw.getNodes() # Use regular expression to select sub-set of nodes mem_ids = hw.getNodes(".*MEM.*") # Get list of sub-nodes of "SUBSYSTEM1" print "The nodes within SUBSYSTEM1 are:" for id in hw.getNode("SUBSYSTEM1").getNodes(): print id A working example of traversing the node tree can be found in :download:`print_node_attributes.py ` Print node attributes ^^^^^^^^^^^^^^^^^^^^^ All of a node's attributes can be accessed through 'getter' methods ... .. code-block:: python print "Node attributes ..." print node # Prints id. Equivalent to print node.getId() print "Address:", node.getAddress() print "Mask:", node.getMask() # Bit-mask of node print "Mode:", node.getMode() # Mode enum - one of uhal.BlockReadWriteMode.SINGLE (default), INCREMENTAL and NON_INCREMENTAL, or HIERARCHICAL for top-level nodes nesting other nodes. print "Read/write permissions:", node.getPermission() # One of uhal.NodePermission.READ, WRITE and READWRITE print "Size (in units of 32-bits):", node.getSize() # In units of 32-bits. All single registers and FIFOs have default size of 1 print "Tags:", node.getTags() # User-definable string from address table - in principle a comma separated list of values print "Parameters:", node.getParameters() # Map of user-definable, semicolon-delimited parameter=value pairs specified in the "parameters" xml address file attribute. Some example code that accesses these variables can be found in :download:`print_node_attributes.py ` Logging ^^^^^^^ By default all log levels are printed to screen. There are 6 log levels of increasing severity: ``DEBUG``, ``INFO``, ``NOTICE``, ``WARNING``, ``ERROR`` and ``FATAL``. The log threshold can be controlled at runtime one of several different ways: .. code-block:: python # Only show log messages with levels Warning and above uhal.setLogLevelTo( uhal.LogLevel.WARNING ) # Don't show any log messages uhal.disableLogging() # Use environment variable to set displayed log level uhal.setLogLevelFromEnvironment( env_variable_name ) ---- .. _working-with-dummy-hardware: Working with dummy hardware --------------------------- UDP dummy hardware ^^^^^^^^^^^^^^^^^^ Start the UDP server emulating the IPbus protocol on ``localhost:50001``: .. code-block:: sh /opt/cactus/bin/uhal/tests/DummyHardwareUdp.exe -p 50001 -v 2 In this case the connection file will have to contain a URI equal to ``ipbusudp-2.0://localhost:50001"``. ControlHub accessing the UDP dummy hardware ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ First, start the UDP server emulating the IPbus protocol on ``localhost:50001``: .. code-block:: sh /opt/cactus/bin/uhal/tests/DummyHardwareUdp.exe -p 50001 -v 2 Second, start the Control Hub to serialize the access to the UDP server on port ``10203``: .. code-block:: sh controlhub_start In this case the connection file will have to contain a URI equal to ``chtcp-2.0://localhost:10203?target=127.0.0.1:50001``. TCP dummy hardware ^^^^^^^^^^^^^^^^^^ Start a TCP server emulating the IPbus protocol on ``localhost:50002``: .. code-block:: sh /opt/cactus/bin/uhal/tests/DummyHardwareTcp.exe -p 50002 -v 2 In this case the connection file will have to contain a URI equal to ``ipbustcp-1.3://localhost:50002``. ---- .. _working-with-the-controlhub: Working with the ControlHub --------------------------- The Control Hub is required when multiple clients/threads/processes are simultaneously communicating with the same IPbus UDP endpoint. Using the Control Hub does not require any modification of the source code. Connection file ^^^^^^^^^^^^^^^ If initially you were using a connection like: .. code-block:: xml Then in order to serialize the access to the same endpoint using the Control Hub, you will need a connection like: .. code-block:: xml Usage ^^^^^ The Control Hub is entirely configuration free and can be considered akin to a network switch that you turn on and forget about. Once switched on, the Control Hub listens for TCP connections on port 10203, so you should ensure that the firewall is configured to allow connections on this port if you wish to connect to the Control Hub from an external machine (i.e. not just over localhost). Further information on the ControlHub can be found :ref:`here `