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 |
|---|---|
|
|
|
|
|
|
|
|
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.
Listing - Listing 12.1 Memory control block data structure
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];
Listing - Listing 12.2 Creating a memory partition
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].
Listing - Listing 12.3
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().
Listing - Listing 12.4 - 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.