Treck Real-Time TCP/IP Systems
Treck TCP/IP is designed to be easy to integrate into your environment. It is also designed to be scalable to your needs. In other words it is designed to allow you to use as much or as little of Treck TCP/IP as you need. For example, if you decide to make an int type 16 bits, you can use up to 32000 sockets with very little penalty in performance. When using the Treck TCP/IP system we want you to think of it as a "Black Box". We believe that there should be no reason for you as an integrator to be intimately familiar with the internals of Treck TCP/IP. We have designed a series of API's that you can use to hook Treck to any environment. These API's include a kernel interface, timer interface, driver interface, sockets interface, and miscellaneous API's to allow you to configure Treck TCP/IP to your environment. Built inside of Treck is a locking system, buffer system, and timer system. These different systems are described in this chapter so that you know how Treck uses the API's that are described in the next two chapters. You will also need to know what the requirements are to integrate Treck Real-Time TCP/IP into your environment.
Treck Real-Time TCP/IP can be used in many different models. You do not need an operating system in order to use Treck in your environment. However, if you have an operating system, we will of course use that operating system to allow us to work best for you. Operating systems seem to come in two different types. The simplest operating system is a non-preemptive operating system. The most common variety of an operating system is a preemptive operating system. The difference between the two types is crucial when you decide to integrate Treck Real-Time TCP/IP into your environment. Preemption is when a task is interrupted by another task of higher priority, or when a fixed time interval has occurred. With a non-preemptive operating system, the user does not need to set up any critical sections. A critical section is a section of code that is protected from preemption by disabling interrupts while working on a shared data area or a shared resource. Since a non-preemptive operating system is by definition non-preemptive, we do not require that protection in this environment. If you have other real-time needs, this may not be the operating system for you.
In a preemptive operating system environment, we do need to concern ourselves with the possibility that we may be preempted or swapped out by another task or interrupt service routine (ISR). In embedded environments we cannot have long critical sections; otherwise this would inhibit our ability to operate in real time. This is what our locking system is concerned with. With our locking system, we are able to protect a section of code over long periods of time without having the interrupts disabled on the system for long periods of time.
With networking it is impossible to achieve 100 percent reentrancy of the code without locking. An example of this would be an application task trying to receive data at the same time as a higher priority task is trying to store data in the receive queue. We are forced to protect the application task while it is updating its pointers to the receive queue, otherwise the user may be returned a corrupt pointer or even worse the corrupt pointer is put into the socket entry. For a non-preemptive operating system this is never an issue because each task is operating at the same priority. One task completes everything it needs to before another task is swapped in.
Another important item to consider when operating in embedded environments is that care must be taken with how we call a networking stack from an interrupt service routine. If we were to receive a packet inside of an interrupt service routine and pass that packet to the networking stack, it is possible that the packet may generate a response to the network. This would increase the time that we stay inside of our interrupt service routine. Most embedded systems have a requirement that the interrupt service routine time is kept to a minimum. You will find with the Treck TCP/IP, a very small set of function calls are supported within an interrupt service routine. These calls are limited in the amount of work that they do. One of these would be to update the timer. However, this is not a recommended method for updating the timer system. We would in fact prefer that the update and execute of the timer system be done in either a main line loop or a separate timer task. Another call from an ISR that is supported is notification of a send complete or a received packet. Note that we do not support passing a packet back to the stack from an ISR. All we do is notify the stack that a packet has been received by the network hardware.
As we can see from what we have discussed thus far, we only need the locking system when we are using Treck with a preemptive operating system and we are using Treck as a shared library in this environment. What we have done with the locking system is to use the most atomic feature found in most embedded operating systems to achieve single threaded access to a data area. We use a counting semaphore to guarantee that only one thread or task of a system can access certain shared data areas at one time. By using a counting semaphore we allow the operating system to properly context switch to a higher priority task at the end of the locked section.
In Treck TCP/IP we use many different locks. By operating in this fashion we avoid stopping other tasks that have no interest in our protected data area. If there is no contention for the same data area, tasks will still operate without blocking. We do not call the operating system unless there is contention for a shared data area.
There is a large difference between a critical section and locking in Treck TCP/IP. A critical section is used to protect a shared data area in a very small amount of code. This code is typically less than five assembly instructions in most instances. On the other hand, a lock is used to protect a shared data area or resource from reentrancy for longer periods. This shared data area could be a socket entry, routing entry, or ARP entry. Even with locks we attempt to keep the time as short as possible to avoid having contention for the shared data area or resource. Counting semaphores are also used to allow us to wait for a resource to become available, such as received data. When we the call socket function recv and we are allowing blocking to occur, if there is no data to receive, we will pend on a counting semaphore until there is data for us to receive or the socket has been closed.
Blocking means that we will wait until some event has occurred that we want to happen. A task that is blocked will allow other tasks to still run. By default, Sockets operate in blocking mode. In order to achieve blocking when it needs to occur, we rely on a counting semaphore. By using counting semaphores for blocking, we do not need to call the operating system in a critical section. This is extremely important for embedded systems. In fact, some operating systems will not allow you to call them inside of a critical section. If we did not use a counting semaphore, then there could be a window of opportunity where a task never exits the blocking state should the event that we are waiting for occurred during that small window.
Most operating systems have the concept of a counting semaphore. Not all operating systems offer a counting semaphore. If your RTOS does not provide a counting semaphore, but does have an event flag, then use the counting semaphore implementation in 'kernel\trcousem.c'.
A counting semaphore is a mechanism that allows you to wait for a resource indefinitely. It also allows you to release the resource before you actually wait.
If the event occurs before you wait, your task will not wait for the resource. This mechanism is very important to the operation of Treck TCP/IP.
Protocol stacks need this mechanism in order to work in an asynchronous mode properly. Now there is another question that you must be asking yourself "Do I actually need a counting semaphore?" The answer is simple. If you are not using a preemptive operating system, and you do not make any blocking calls, then you do not need a counting semaphore. We only use the counting semaphore for blocking and locking.
Fig. 3-1 RTOS Allows Task 1 to Continue
In Treck TCP/IP we have an internal buffer management system for performance reasons. We have found that if we need to call the operating system every time that we need a buffer and again have to call the operating system in order to free that buffer, there is a severe penalty on performance. What we do with Treck TCP/IP is allow the system to dynamically allocate memory as needed. This means the memory pool may grow over time. After Treck TCP/IP is used actively over a period of about one-minute, the maximum pool that Treck TCP/IP will be using is already allocated from the operating system.
The pool that Treck is using will only grow as more performance demands are placed on the networking system. By allocating buffers in this fashion, Treck eliminates the guesswork that is involved in trying to set up a system.
We allow the protocol stack to only use the memory that is required for your performance demands. You do not need to set aside an area of memory of a fixed size. We have found that if you are forced to set aside a fixed size memory block that will be used by the TCP/IP stack to dynamically allocate memory from, you will be forced to set aside more memory than you will use in order to prevent "out of memory" errors from occurring. We dynamically allocate almost all of our data structures. This is done to keep the static memory area as small as possible. Also by doing this, we allow you to change the configuration of the protocol stack without recompiling.
By this time you must be asking yourself the question "So, how much RAM will I use?" We have found that a single TCP socket implementation with a small number of receive buffers will use less than 20k bytes of RAM when the highest performance demands are placed upon the protocol stack. The amount of RAM used in your system will depend on many different factors. These factors include the size of your sockets send queue and receive queue as well as the number of pre-allocated receive buffers for the device. Most of the RAM is used for your data. The internal data structures are very small. For example: a single UDP socket entry requires less than 200 bytes for the internal data structures. If we are using TCP on a socket, then the internal data structure grows to less than 400 bytes. A socket entry is only allocated between the socket call and the close call. This means that the socket data structures are not in use when the socket is not active. This allows you to have a high number of sockets defined on a system and use a small amount of RAM while they are not in use. Maximum RAM usage is also dependent on the maximum number of concurrent sockets that you have open at any given time. The interface to your operating system or "C" compiler from the buffer system is a simple malloc() and free() call.
The timer system is the most simple of the systems to understand. We need a fixed time interval notification into the protocol stack. This is very similar to the crystal that is attached to your microprocessor. We use this fixed time notification to let us know how much time has elapsed. This is very important for a protocol stack. We must know how long a TCP packet takes to get to a remote system. We must also know how much time an ARP entry has been in the ARP queue. In addition, the timer system also measures routing entries and is used for Treck PPP.
You don't deal with all of the individual timers that Treck TCP/IP manages. Since we only use a single notification to let us know how much time is elapsed, the timer system will manage all of the different timers used by the Treck TCP/IP system.
Treck TCP/IP is flexible in the way that you provide this notification. You have the option of using an ISR or a task to do the timer notification. The notification is done in a single function call. You can then decide when you wish to execute the timers. The execution of the timers is done with a single function call as well. Many users elect to have a simple task under their operating system that will both update the timers (this is the notification), and execute the timers. After performing both of these functions, the timer task will wait for a fixed amount of time before looping back to the update. If you do not have an embedded operating system then you may opt to update the timers from an ISR and execute them from a main line loop.