Memory Management
Your application can allocate and free dynamic memory using any ANSI C compiler’s malloc()
and free()
functions, respectively. However, using malloc()
and free()
in an embedded real-time system is dangerous because, eventually, you may not be able to obtain a single contiguous memory area due to fragmentation. Fragmentation is the development of a large number of separate free areas (i.e., the total free memory is fragmented into small, non-contiguous pieces). Execution time of malloc() and free() are also generally nondeterministic because of the algorithms used to locate a contiguous block of free memory.
Memory Management Configuration
µC/OS-II provides an alternative to malloc()
and free()
by allowing your application to obtain fixed-sized memory blocks from a partition made of a contiguous memory area, as illustrated in Figure 12.1. All memory blocks are the same size and the partition contains an integral number of blocks. Allocation and deallocation of these memory blocks is done in constant time and is deterministic.
As shown in Figure 12.2, more than one memory partition can exist, so your application can obtain memory blocks of different sizes. However, a specific memory block must be returned to the partition from which it came. This type of memory management is not subject to fragmentation.
To enable µC/OS-II memory management services, you must set configuration constants in OS_CFG.H
. Specifically, table 12.1 shows which services are compiled based on the value of configuration constants found in OS_CFG.H
. You should note that NONE of the memory management services are enabled when OS_MEM_EN is set to 0. To enable specific features (i.e., service) listed in Table 12.1, simply set the configuration constant to 1. You will notice that OSMemCreate()
, OSMemGet()
and OSMemPut()
cannot be individually disabled like the other services. That’s because they are always needed when you enable µC/OS-II memory management.
µC/OS-II Memory Service | Enabled when set to 1 in OS_CFG.H |
---|---|
OSMemCreate() | |
OSMemGet() | |
OSMemPut() | |
OSMemQuery() | OS_MEM_QUERY_EN |
Memory Control Blocks
µC/OS-II keeps track of memory partitions through the use of a data structure called a memory control block (Listing 12.1). Each memory partition requires its own memory control block.
typedef struct { void *OSMemAddr; void *OSMemFreeList; INT32U OSMemBlkSize; INT32U OSMemNBlks; INT32U OSMemNFree; } OS_MEM;
.OSMemAddr
is a pointer to the beginning (base) of the memory partition from which memory blocks will be allocated. This field is initialized when you create a partition [see section 12.01, Creating a Partition, OSMemCreate()
] and is not used thereafter.
.OSMemFreeList
is a pointer used by µC/OS-II to point to either the next free memory control block or to the next free memory block. The use depends on whether the memory partition has been created or not (see section 12.01).
.OSMemBlkSize
determines the size of each memory block in the partition and is a parameter you specify when the memory partition is created (see section 12.01).
.OSMemNBlks
establishes the total number of memory blocks available from the partition. This parameter is specified when the partition is created (see section 12.01).
.OSMemNFree
is used to determine how many memory blocks are available from the partition.
µC/OS-II initializes the memory manager if you configure OS_MEM_EN to 1 in OS_CFG.H
. Initialization is done by OS_MemInit()
[called by OSInit()
] and consists of creating a linked list of memory control blocks, as shown in Figure 12.3. You specify the maximum number of memory partitions with the configuration constant OS_MAX_MEM_PART (see OS_CFG.H
), which must be set at least to 2.
As you can see, the OSMemFreeList field of the control block is used to chain the free control blocks.
Creating a Partition, OSMemCreate()
Your application must create each partition before it can be used and is this done by calling OSMemCreate()
. Listing 12.2 shows how you could create a memory partition containing 100 blocks of 32 bytes each. Some processors like to have memory aligned on either 16 or 32-bit boundaries. To accommodate these processors, you could declare the memory partitions as:
INT16U CommTxPart[100][16];
or,
INT32U CommTxPart[100][8];
OS_MEM *CommTxBuf; INT8U CommTxPart[100][32]; void main (void) { INT8U err; OSInit(); . . CommTxBuf = OSMemCreate(CommTxPart, 100, 32, &err); . . OSStart(); }
The code to create a memory partition is shown in Listing 12.3. OSMemCreate()
requires four arguments: the beginning address of the memory partition, the number of blocks to be allocated from this partition, the size (in bytes) of each block, and a pointer to a variable that contains an error code. OSMemCreate()
returns a NULL pointer if OSMemCreate()
fails. On success, OSMemCreate()
returns a pointer to the allocated memory control block. This pointer must be used in subsequent calls to memory management services [see OSMemGet()
, OSMemPut()
, and OSMemQuery()
in sections 12.02 through 12.04].
OS_MEM *OSMemCreate (void *addr, INT32U nblks, INT32U blksize, INT8U *err) { #if OS_CRITICAL_METHOD == 3 OS_CPU_SR cpu_sr; #endif OS_MEM *pmem; INT8U *pblk; void **plink; INT32U i; #if OS_ARG_CHK_EN > 0 if (addr == (void *)0) { (1) *err = OS_MEM_INVALID_ADDR; return ((OS_MEM *)0); } if (nblks < 2) { (2) *err = OS_MEM_INVALID_BLKS; return ((OS_MEM *)0); } if (blksize < sizeof(void *)) { (3) *err = OS_MEM_INVALID_SIZE; return ((OS_MEM *)0); } #endif OS_ENTER_CRITICAL(); pmem = OSMemFreeList; (4) if (OSMemFreeList != (OS_MEM *)0) { OSMemFreeList = (OS_MEM *)OSMemFreeList->OSMemFreeList; } OS_EXIT_CRITICAL(); if (pmem == (OS_MEM *)0) { (5) *err = OS_MEM_INVALID_PART; return ((OS_MEM *)0); } plink = (void **)addr; (6) pblk = (INT8U *)addr + blksize; for (i = 0; i < (nblks - 1); i++) { *plink = (void *)pblk; plink = (void **)pblk; pblk = pblk + blksize; } *plink = (void *)0; OS_ENTER_CRITICAL(); pmem->OSMemAddr = addr; (7) pmem->OSMemFreeList = addr; pmem->OSMemNFree = nblks; pmem->OSMemNBlks = nblks; pmem->OSMemBlkSize = blksize; OS_EXIT_CRITICAL(); *err = OS_NO_ERR; return (pmem); (8) }
(1) You must pass a valid pointer to the memory allocated that will be used as a partition.
(2) Each memory partition must contain at least two memory blocks.
(3) Each memory block must be able to hold the size of a pointer because a pointer is used to chain all the memory blocks together.
(4) Next, OSMemCreate()
obtains a memory control block from the list of free memory control blocks. The memory control block contains run-time information about the memory partition.
(5) OSMemCreate()
cannot create a memory partition unless a memory control block is available.
(6) If a memory control block is available and all the previous conditions are satisfied, the memory blocks within the partition are linked together in a singly linked list. A singly linked list is used because insertion and removal of elements in the list is always done from the head of the list.
(7) When all the blocks are linked, the memory control block is filled with information about the partition.
(8) OSMemCreate()
returns the pointer to the memory control block so it can be used in subsequent calls to access the memory blocks from this partition.
Figure 12.4 shows how the data structures look when OSMemCreate()
completes successfully. Note that the memory blocks are shown linked one after the other. At run time, as you allocate and deallocate memory blocks, the blocks will most likely not be in the same order.
Obtaining a Memory Block, OSMemGet()
Your application can get a memory block from one of the created memory partitions by calling OSMemGet()
. You must use the pointer returned by OSMemCreate()
in the call to OSMemGet()
to specify which partition the memory block will come from. Obviously, your application needs to know how big the memory block obtained is so that it doesn’t exceed its storage capacity. In other words, you must not use more memory than is available from the memory block. For example, if a partition contains 32-byte blocks, then your application can use up to 32 bytes. When you are done using the block, you must return it to the proper memory partition [see section 12.03, Returning a Memory Block, OSMemPut()
].
Listing 12.4 shows the code for OSMemGet()
.
void *OSMemGet (OS_MEM *pmem, INT8U *err) (1) { #if OS_CRITICAL_METHOD == 3 OS_CPU_SR cpu_sr; #endif void *pblk; #if OS_ARG_CHK_EN > 0 if (pmem == (OS_MEM *)0) { (2) *err = OS_MEM_INVALID_PMEM; return ((OS_MEM *)0); } #endif OS_ENTER_CRITICAL(); if (pmem->OSMemNFree > 0) { (3) pblk = pmem->OSMemFreeList; (4) pmem->OSMemFreeList = *(void **)pblk; (5) pmem->OSMemNFree--; (6) OS_EXIT_CRITICAL(); *err = OS_NO_ERR; return (pblk); (7) } OS_EXIT_CRITICAL(); *err = OS_MEM_NO_FREE_BLKS; return ((void *)0); }
(1) The pointer passed to OSMemGet()
specifies the partition from which you want to get a memory block.
(2) If you enabled argument checking (i.e. OS_ARG_CHK_EN is set in OS_CFG.H
) then OSMemGet()
makes sure that you didn’t pass a NULL pointer instead of a pointer to a partition. Unfortunately, OSMemGet()
doesn’t know whether a non-NULL is actually pointing to a valid partition (pmem could point to anything).
(3) OSMemGet()
checks to see if there are free blocks available.
(4) If a block is available, it is removed from the free list.
(5)
(6) The free list is then updated so that it points to the next free memory block, and the number of blocks is decremented, indicating that it has been allocated.
(7) The pointer to the allocated block is finally returned to your application.
Note that you can call this function from an ISR because, if a memory block is not available, there is no waiting and the ISR simply receives a NULL pointer.
Returning a Memory Block, OSMemPut()
When your application is done with a memory block, it must be returned to the appropriate partition. This is accomplished by calling OSMemPut()
. You should note that OSMemPut()
has no way of knowing whether the memory block returned to the partition belongs to that partition. In other words, if you allocate a memory block from a partition containing blocks of 32 bytes, then you should not return this block to a memory partition containing blocks of 120 bytes. The next time an application requests a block from the 120-byte partition, it will only get 32 valid bytes; the remaining 88 bytes may belong to some other task(s). This could certainly make your system crash.
Listing 12.5 shows the code for OSMemPut()
.
INT8U OSMemPut (OS_MEM *pmem, void *pblk) (1) { #if OS_CRITICAL_METHOD == 3 OS_CPU_SR cpu_sr; #endif #if OS_ARG_CHK_EN > 0 if (pmem == (OS_MEM *)0) { (2) return (OS_MEM_INVALID_PMEM); } if (pblk == (void *)0) { return (OS_MEM_INVALID_PBLK); } #endif OS_ENTER_CRITICAL(); if (pmem->OSMemNFree >= pmem->OSMemNBlks) { (3) OS_EXIT_CRITICAL(); return (OS_MEM_FULL); } *(void **)pblk = pmem->OSMemFreeList; (4) pmem->OSMemFreeList = pblk; pmem->OSMemNFree++; (5) OS_EXIT_CRITICAL(); return (OS_NO_ERR); }
(1) You pass OSMemPut()
the address of the memory control block (pmem) to which the memory block belongs (pblk).
(2) OSMemPut()
then checks that the pointers being passed to the function are non-NULL. Unfortunately, OSMemPut()
doesn’t know for whether the block returned actually belongs to the partition. It is assumed that your application will be returning the block to its proper place.
(3) Next, we check to see that the memory partition is not already full. This situation would certainly indicate that something went wrong during the allocation/deallocation process. Indeed, you are returning a block to a partition which ‘thinks’ it has all of its blocks already returned to it.
(4) If the memory partition can accept another memory block, it is inserted into the linked list of free blocks.
(5) Finally, the number of memory blocks in the memory partition is incremented.
Obtaining Status of a Memory Partition, OSMemQuery()
OSMemQuery()
is used to obtain information about a memory partition. Specifically, your application can determine how many memory blocks are free, how many memory blocks have been used (i.e., allocated), the size of each memory block (in bytes), etc. This information is placed in a data structure called OS_MEM_DATA, as shown in Listing 12.6.
typedef struct { void *OSAddr; /* Points to beginning address of memory partition */ void *OSFreeList; /* Points to beginning of free list of memory blocks */ INT32U OSBlkSize; /* Size (in bytes) of each memory block */ INT32U OSNBlks; /* Total number of blocks in the partition */ INT32U OSNFree; /* Number of memory blocks free */ INT32U OSNUsed; /* Number of memory blocks used */ } OS_MEM_DATA;
The code for OSMemQuery()
is shown in Listing 12.7.
INT8U OSMemQuery (OS_MEM *pmem, OS_MEM_DATA *pdata) { #if OS_CRITICAL_METHOD == 3 OS_CPU_SR cpu_sr; #endif #if OS_ARG_CHK_EN > 0 if (pmem == (OS_MEM *)0) { (1) return (OS_MEM_INVALID_PMEM); } if (pdata == (OS_MEM_DATA *)0) { return (OS_MEM_INVALID_PDATA); } #endif OS_ENTER_CRITICAL(); pdata->OSAddr = pmem->OSMemAddr; (2) pdata->OSFreeList = pmem->OSMemFreeList; pdata->OSBlkSize = pmem->OSMemBlkSize; pdata->OSNBlks = pmem->OSMemNBlks; pdata->OSNFree = pmem->OSMemNFree; OS_EXIT_CRITICAL(); pdata->OSNUsed = pdata->OSNBlks - pdata->OSNFree; (3) return (OS_NO_ERR); }
(1) As usual, we start off by checking the arguments passed to the function.
(2) All the fields found in OS_MEM are copied to the OS_MEM_DATA data structure with interrupts disabled. This ensures that the fields will not be altered until they are all copied.
(3) You should also notice that computation of the number of blocks used is performed outside of the critical section because it’s done using the local copy of the data.
Using Memory Partitions
Figure 12.5 shows an example of how you can use the dynamic memory allocation feature of µC/OS-II, as well as its message-passing capability (see Chapter 11). Also, refer to Listing 12.8 for the pseudocode of the two tasks shown. The numbers in parenthesis in Figure 12.5 correspond to the appropriate action in Listing 12.8.
The first task reads and checks the value of analog inputs (pressures, temperatures, voltages) and sends a message to the second task if any of the analog inputs exceed a threshold. The message sent contains a time stamp, information about which channel had the error, an error code, an indication of the severity of the error, and any other information you can think of.
Error handling in this example is centralized. This means that other tasks, or even ISRs, can post error messages to the error-handling task. The error-handling task could be responsible for displaying error messages on a monitor (a display), logging errors to a disk, or dispatching other tasks that could take corrective actions based on the error.
AnalogInputTask() { for (;;) { for (all analog inputs to read) { Read analog input; (1) if (analog input exceeds threshold) { Get memory block; (2) Get current system time (in clock ticks); (3) Store the following items in the memory block: (4) System time (i.e. a time stamp); The channel that exceeded the threshold; An error code; The severity of the error; Etc. Post the error message to error queue; (5) (A pointer to the memory block containing the data) } } Delay task until it's time to sample analog inputs again; } } ErrorHandlerTask() { for (;;) { Wait for message from error queue; (6) (Gets a pointer to a memory block containing information about the error reported) Read the message and take action based on error reported; (7) Return the memory block to the memory partition; (8) } }
Waiting for Memory Blocks from a Partition
Sometimes it’s useful to have a task wait for a memory block in case a partition runs out of blocks. µC/OS-II doesn’t support “pending” on partitions, but you can support this requirement by adding a counting semaphore (see Chapter 7, Semaphores) to guard the memory partition. To obtain a memory block, simply obtain a semaphore then call OSMemGet()
. To release a block, simply return the block back to its partition and post to the semaphore. The whole process is shown in Listing 12.9.
OS_EVENT *SemaphorePtr; (1) OS_MEM *PartitionPtr; INT8U Partition[100][32]; OS_STK TaskStk[1000]; void main (void) { INT8U err; OSInit(); (2) . . SemaphorePtr = OSSemCreate(100); (3) PartitionPtr = OSMemCreate(Partition, 100, 32, &err); (4) . OSTaskCreate(Task, (void *)0, &TaskStk[999], &err); (5) . OSStart(); (6) } void Task (void *pdata) { INT8U err; INT8U *pblock; for (;;) { OSSemPend(SemaphorePtr, 0, &err); (7) pblock = OSMemGet(PartitionPtr, &err); (8) . . /* Use the memory block */ . OSMemPut(PartitionPtr, pblock); (9) OSSemPost(SemaphorePtr); (10) } }
(1) First, declare your system objects. Note that I used hard-coded constants for clarity. You would certainly create #define constants in a real application.
(2)
(3) Initialize µC/OS-II by calling OSInit()
then create a semaphore with an initial count corresponding to the number of blocks in the partition.
(4) Next, create the partition and one of the tasks that will be accessing the partition.
(5) By now, you should be able to figure out what you need to do to add the other tasks. It would obviously not make much sense to use a semaphore if only one task is using memory blocks — there would be no need to ensure mutual exclusion! In fact, it wouldn’t even make sense to use partitions unless you intend to share memory blocks with other tasks.
(6) Multitasking is then started by calling OSStart()
.
(7)
(8) When the task executes, it obtains a memory block only if a semaphore is available. Once the semaphore is available, the memory block is obtained. There is no need to check for an error code from OSSemPend()
because the only way µC/OS-II will return to this task is if a memory block is released because a timeout of 0 is specified. Also, you don’t need the error code from OSMemGet()
for the same reason — you must have at least one block in the partition in order for the task to resume.
(9)
(10) When the task is finished with a memory block, it simply returns it to the partition and signals the semaphore.