In this post, we are going to talk about creating modular code. We take up a simple example of an application-network interface and discuss how to make codes that are easy to write, read and hopefully, easy to maintain.
We all talk about code modularity - especially in the case of embedded systems. What does modularity mean? And what is this layered architecture that has everyone abuzz? Well, it is much like Lewis Carroll's Alice in Wonderland. As you go down the rabbit hole, the deeper it gets. Looks simple on the outside, but intricate on the inside. Plus, it might take some painstaking effort to design one in the first place, something that at the end of design is actually layered. So how do we write modular code after all?
Here is a start!
While writing any module, let's take for example any driver module sitting under a Firmware Application, we generally follow the following few principles:
- All that the module needs to know from the Application should be given to the module during initialization, along with an argument to serve as an identifier in the module.
- The module doesn't have to know, and should never know who initialized it or who is going to use it.
- The Application and the module always pass the identifier to each other in all subsequent interactions.
So, what I mean is that the module and the application should know nothing about each other and all interactions are defined by the argument illustrated in the point 1 above. This argument can be thought of as a key to the interactions between the module and the application using it. This often comes in as an 'interface specification' - which is essentialy the code of conduct which must be followed in full in order to achieve the desired function. It is usually a well-formed document optimized for developer-readability - this essentially means that it can be a small text document, or a header file.
To explain the concept further, let us take for example a Module that manages TCP connections, sending and receiving of packets on the connections for the application:
The points to be noted here are:
- The Application neither sends nor receives anything to or from the actual connection directly.
- The Application always talks to the underlying connections via the TCP Module.
- The Application does not know what the underlying connection mechanism is or anything else from inside the driver, except that the interfacing module provides such functionality of TCP connections.
Big Talk! How do we do this?
The first thing is to find out what all information does the module need from the application upfront for the entire life cycle of the software. For example, when the Application would request the TCP Module to create a new connection it would need to tell the TCP Module the following essential things:
- The type of connection - whether we want to initiate a connection (Client) or want to listen for incoming connections (Server)
- The type of IP Address of the Server - IPv4 or IPv6. This specifies the length of the next field. 4 bytes(octets) for IPv4, 16 bytes(octets) for IPv6.
- The IP Address of the Server - The actual IP Address. In the case of a server, we will want to listen on this IP and in the case of a client, we'll need to send a connection request to this IP.
- The TCP Port of the Server
- If we're creating a Server connection, the maximum number of connection requests from the clients that are allowed to queue up.
- The maximum size of the message the Module should expect on the connection when packets first arrive. As a simple example, it means that the Application is saying to the TCP Module, "if the message received is not at least 'X' bytes long, don't bother telling me about it".
- The argument to this TCP Connection (or more simply, the identifier).
- A callback function which must be called when a packet (greater than the size specified in point 6) is received on the connection.
- A callback function to be called when/if some error occurs on the connection.
I think the first 6 pieces of information make intuitive sense. Do they not? But what is this argument
after all? It is the Application telling the TCP Module, "Hey, I want this connection made, and I'll refer to this connection as 'John Doe'. So, if you want to talk about it with me, use this name." It is up to the module how it uses this information, as long as it is able to function as desired. Imagine the Application had requested 10 TCP connections from the TCP Module. So, the Application would have supplied 10 unique identifiers, one for each connection request. Both the Application and the Module identify each of the connection with a unique identifier (Note that the identifier can be anything. I've used "John Doe", but in a real implementation scenario, it makes more sense to give them numbered names like 1,2,3. It is easier for the computers). When the application wants the module to send some packet or carry out any activity on a particular TCP connection, it sends in the "argument" of that TCP Connection to the module and the module automatically understands which connection to send the packet on, or to carry out the activity. Similarly, for the packet receive call back, the module would pass back the "argument" that represents the connection on which the packet was received to the Application. Makes Sense? Let's see this visually:
In the case shown above, the application requests for two TCP Connections and identifies them with arguments 0x01 and 0x02 respectively. Now when the Application wants to send a packet on the Second TCP Connection, it would call the Send Packet API of the TCP Module with the pointer to the packet data structure and the argument value as 0x02. The TCP Module would understand which connection the packet needs to be sent on and will take care of the rest (unless an error occurs, which it would notify using the error notification callback). This way the application need not actively keep track of the TCP connection and everything is maintained by the TCP Module in its stead. It still needs to keep a reference to the connection names, or arguments for use though. Similarly, when a packet is received on the TCP connection created for the first connection, the TCP Module calls the callback for receiving the packet that was passed by Application during init request with the pointer to the received packet and the argument filled as 0x01.
Points 8 and 9 are function pointers. These are populated by the Application when requesting for a TCP Connection from the TCP Module. This shows the flexibility that the TCP Module provides to the Application in case of Asynchronous Events. I have given example of two such events here:
- A Packet of appropriate length is received on a connection.
- An error occurred
Now for each connection, the Application might want to handle these Asynchronous Events in a different way and hence would either want to call the same function for a particular event across multiple connections or call different functions for different connections. So it needs to populate the callbacks for each connection separately while registering the connection. A definite argument to these functions would be the "argument" to the TCP connection for which the callback was called.
'Nough said, show me some code! So in the case of such module design, the first thing to do would be to 'freeze' the interface and expose some init or register kind of API to the Application. In the example cited above the API would look something like this:
/*----------------------------------------------------------------------------*/ /*!@brief function pointers to receive and error application call backs */ typedef void ( * tcp_packet_receive_cb_t ) ( uint32 argument, uint8 * packet_data, uint32 packet_size ); typedef void ( * tcp_packet_error_cb_t ) ( uint32 argument, tcp_error_type_e error_type ); /*----------------------------------------------------------------------------*/ /*!@brief structure to define the params required to initialize tcp sockets */ typedef struct { /* type of the tcp socket */ tcp_socket_type_e socket_type; /* TCP Socket identifier */ uint32 argument; /* TCP IP Address Type */ tcp_ip_type_e ip_type; /* TCP Server IP Address : server_ip_byte_0.server_ip_byte_1.server_ip_byte_2.server_ip_byte_3 server_ip_byte_0.server_ip_byte_1.server_ip_byte_2.server_ip_byte_3.server_ip_byte_4.server_ip_byte_5 */ BYTE server_ip_byte_0; BYTE server_ip_byte_1; BYTE server_ip_byte_2; BYTE server_ip_byte_3; BYTE server_ip_byte_4; BYTE server_ip_byte_5; /* TCP Socket Port */ uint32 port; /* Max queued connection requests to Listen to */ uint32 max_queued_connection_requests_to_listen; /* Max Message size to receive at once */ uint32 max_message_size_to_receive; /* Call back for receive */ tcp_packet_receive_cb_t tcp_packet_receive_cb; /* Call back for error */ tcp_packet_error_cb_t tcp_packet_error_cb; }tcp_socket_req_s; /*----------------------------------------------------------------------------*/ tcp_status_e tcp_socket_request ( tcp_socket_req_s * tcp_socket_req, uint32 num_sockets );
When the Application calls the request API tcp_socket_request()
exposed by the TCP Module, it expects the TCP Module to take care of everything that is required for the connection to be made which in this case includes:
- Threading
- Creation of a socket descriptor based on if the connection is Client Type or Server Type
- Bind with the IP
- Listen on the Socket if Server
- Accept on the Socket if Server
- Connect on the Socket if Client
- Keep Re-establishing in case of disconnection
- Receive and Send
- Other Tid Bits
When the Application would like to send a packet on a particular connection, the Module would have exposed an API of this kind to the application:
/*----------------------------------------------------------------------------*/ tcp_status_e tcp_socket_send ( uint32 argument, BYTE * buffer_pointer, uint32 send_size );
and rest should be taken care of by the module!
Finally the error and receive call backs populated by the application should look like these:
/*----------------------------------------------------------------------------*/ /*!@brief function pointers to receive and error application call backs */ typedef void ( * tcp_packet_receive_cb_t ) ( uint32 argument, uint8 * packet_data, uint32 packet_size ); typedef void ( * tcp_packet_error_cb_t ) ( uint32 argument, tcp_error_type_e error_type );
Proceeeding in this way, one could build a full-fledged application that makes the upper layers agnostic of what underlying connection is. Actually, the POSIX specification of sockets does a fine job at achieving this. But sometimes, (often) we're just too lazy to care. Besides, if someone is a GUI expert, they don't necessarily have to be a TCP-IP wizard either. So, this kind of method of creating black boxes can help when working on larger projects. Also, one could just swap out one TCP module with the other as long as the interface specifications are followed.