بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيم

Manajemen Memori di Sistem Embedded

Manajemen memori (RAM) dalam pemrograman sangatlah penting, apalagi dalam sistem embedded yang memang memori sangatlah terbatas, sebagai contoh keluarga STM32 ukuran RAM terbesar adalah 1MB, walaupun memang bisa ditambah RAM eksternal. Di sistem embedded, khususnya ketika memprogram menggunakan C/C++, RAM ini akan dibagi menjadi 3 blok: Stack, Heap dan statik.

Pembagian Memori

Ukuran blok stack dan heap biasanya dilakukan melalui linker script, sedangkan blok statis, ukurannya akan diketahui setalah program dikompail, karena memang ukuran blok statis akan ditentukan banyaknya variabel-variabel program yang dideklarasikan secara global atau statis.

Pengaturan Memori Stack dan Heap di STM32CubeMX
/* Highest address of the user mode stack */
_estack = ORIGIN(RAM) + LENGTH(RAM);	/* end of "RAM" Ram type memory */

_Min_Heap_Size = 0x200 ;	/* required amount of heap  */
_Min_Stack_Size = 0x400 ;	/* required amount of stack */

/* Memories definition */
MEMORY
{
  CCMRAM    (xrw)    : ORIGIN = 0x10000000,   LENGTH = 64K
  RAM    (xrw)    : ORIGIN = 0x20000000,   LENGTH = 128K
  FLASH    (rx)    : ORIGIN = 0x8000000,   LENGTH = 1024K
}

Memori stack digunakan untuk menyimpan variabel lokal. Di sisi pemrogram bahasa assembly, memori stack ini digunakan untuk menyimpan akumulator, data pointer/program counter (PC) dan register internal, ketika memanggil (call) fungsi lain atau ketika terjadi interupsi. Jadi ketika call atau interupsi terjadi, maka sebelum mengeksekusi fungsi yang dipanggil atau ISR, maka data-data yang ada di akumulator, register, PC harus disimpan dulu di memori stack. Ketika ISR atau fungsi yang dipanggil telah selesai dikerjakan, data-data tersebut kemudian akan dibaca kembali sehingga ketika kembali ke fungsi di mana interupsi terjadi, kondisi fungsi tersebut sama dengan kondisi saat ditinggalkan. Kita mengenal instruksi push (simpan) dan pop (ambil) dalam bahasa assembly untuk melakukan proses ini.

Memori stack biasanya ditempatkan diujung alamat RAM dan sifatnya adalah LIFO (Last-In-First-Out) artinya data yang terakhir disimpan adalah data yang akan pertama diambil, sehingga alamat stack akan menurun.

Sedangkan memori heap digunakan untuk menyimpan variabel atau buffer (variabel array) yang saat program dijalankan ukurannya bisa berubah (dynamic allocation). Bahasa C mengenal fungsi malloc() untuk mengalokasikan memori kepada variabel ketika pogram sedang berjalan. Perlu diperhatikan, ketika variabel tersebut sudah tidak digunakan lagi, sebaiknya alokasi memori untuk varibel tersebut harus dibebaskan, agar tidak menyebabkan program kekurangan memori atau bahkan merusak memori itu sendiri (karena menimpa alamat memori lain).

Manajemen Memori di FreeRTOS

Di RTOS, di mana berlaku konsep menjalankan beberapa program/task secara bersamaan, maka secara sederhana bisa dinyatakan bahwa setiap task harus mempunyai memori stacknya masing-masing. Memori stack ini digunakan untuk menyimpan semua variabel lokal yang digunakan oleh task yang bersangkutan. Selain itu memori stack juga digunakan untuk menyimpan informasi dari setiap task yang dinamakan dengan TCB (Task Control Block). Memori stack juga digunakan untuk menyimpan objek-objek kernel RTOS seperti Queue, MutexI, Semaphore, Event, dan Timer.

TCB ini akan menyimpan alamat dari awal stack (pxStack), alamat stack yang sedang terpakai(pxTopOfStack), dan juga alamat akhir stack (pxEndOfStack) yang bisa digunakan untuk mengecek kondisi apakah ada overflow di memori stack. Selain itu TCB juga menyimpan informasi task seperti prioritas, nama task dan informasi lain seperti terlihat di deklarasi TCB di bawah ini.

/*
 * Task control block.  A task control block (TCB) is allocated for each task,
 * and stores task state information, including a pointer to the task's context
 * (the task's run time environment, including register values)
 */
typedef struct tskTaskControlBlock 			/* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
	volatile StackType_t	*pxTopOfStack;	/*< Points to the location of the last item placed on the tasks stack.  THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */

	#if ( portUSING_MPU_WRAPPERS == 1 )
		xMPU_SETTINGS	xMPUSettings;		/*< The MPU settings are defined as part of the port layer.  THIS MUST BE THE SECOND MEMBER OF THE TCB STRUCT. */
	#endif

	ListItem_t			xStateListItem;	/*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */
	ListItem_t			xEventListItem;		/*< Used to reference a task from an event list. */
	UBaseType_t			uxPriority;			/*< The priority of the task.  0 is the lowest priority. */
	StackType_t			*pxStack;			/*< Points to the start of the stack. */
	char				pcTaskName[ configMAX_TASK_NAME_LEN ];/*< Descriptive name given to the task when created.  Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */

	#if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
		StackType_t		*pxEndOfStack;		/*< Points to the highest valid address for the stack. */
	#endif

	#if ( portCRITICAL_NESTING_IN_TCB == 1 )
		UBaseType_t		uxCriticalNesting;	/*< Holds the critical section nesting depth for ports that do not maintain their own count in the port layer. */
	#endif

	#if ( configUSE_TRACE_FACILITY == 1 )
		UBaseType_t		uxTCBNumber;		/*< Stores a number that increments each time a TCB is created.  It allows debuggers to determine when a task has been deleted and then recreated. */
		UBaseType_t		uxTaskNumber;		/*< Stores a number specifically for use by third party trace code. */
	#endif

	#if ( configUSE_MUTEXES == 1 )
		UBaseType_t		uxBasePriority;		/*< The priority last assigned to the task - used by the priority inheritance mechanism. */
		UBaseType_t		uxMutexesHeld;
	#endif

	#if ( configUSE_APPLICATION_TASK_TAG == 1 )
		TaskHookFunction_t pxTaskTag;
	#endif

	#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
		void			*pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
	#endif

	#if( configGENERATE_RUN_TIME_STATS == 1 )
		uint32_t		ulRunTimeCounter;	/*< Stores the amount of time the task has spent in the Running state. */
	#endif

	#if ( configUSE_NEWLIB_REENTRANT == 1 )
		/* Allocate a Newlib reent structure that is specific to this task.
		Note Newlib support has been included by popular demand, but is not
		used by the FreeRTOS maintainers themselves.  FreeRTOS is not
		responsible for resulting newlib operation.  User must be familiar with
		newlib and must provide system-wide implementations of the necessary
		stubs. Be warned that (at the time of writing) the current newlib design
		implements a system-wide malloc() that must be provided with locks.

		See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
		for additional information. */
		struct	_reent xNewLib_reent;
	#endif

	#if( configUSE_TASK_NOTIFICATIONS == 1 )
		volatile uint32_t ulNotifiedValue;
		volatile uint8_t ucNotifyState;
	#endif

	/* See the comments in FreeRTOS.h with the definition of
	tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE. */
	#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /*lint !e731 !e9029 Macro has been consolidated for readability reasons. */
		uint8_t	ucStaticallyAllocated; 		/*< Set to pdTRUE if the task is a statically allocated to ensure no attempt is made to free the memory. */
	#endif

	#if( INCLUDE_xTaskAbortDelay == 1 )
		uint8_t ucDelayAborted;
	#endif

	#if( configUSE_POSIX_ERRNO == 1 )
		int iTaskErrno;
	#endif

} tskTCB;

Alokasi memori stack untuk tiap task ini tidak mengambil alokasi memori stack yang dideklarasikan di linker script, tergantung metode/skema alokasi memori seperti yang akan diterangkan. Alokasi memori ini bisa dilakukan secara dinamik, statik atau dimanik dan statik secara bersamaan. Alokasi memori statik baru diperkenalkan di FreeRTOS veri 9.

Manajemen Memori

Skema Alokasi Memori Dinamik

Dalam alokasi memori dinamik, ukuran memori yang nanti akan dipakai oleh masing-masing task harus didefinisikan dalam parameter TOTAL_HEAP_SIZE (dalam byte). Ukuran heap ini minimal harus sama dengan jumlah semua stack yang dipakai oleh semua task, sebagai contoh jika ada 4 task dan masing-masing menggunakan 1KB task, maka total heap minimal harus 4KB. Heap ini akan terlihat sebagai penggunaan RAM saat program di-compile. Alokasi memori ini menggunakan fungsi pvPortMalloc() yang berfungsi mirip dengan fungsi umum C malloc(). Ketika sebuah task dihapus, karena mungkin task tersebut sudah tidak diperlukan, alokasi memori untuk task tersebut bisa dibebaskan. FreeRTOS menggunakan fungsi vPortFree(), fungsi ini mirip dengan fungsi free(). Alokasi memori dinamik mengenal 5 skema yang dinamakan dengan heap_1, heap_2, heap_3, heap_4 dan heap_5. Penggunaan skema ini akan membutuhkan file heap_1.c, heap_2.c dan seterusnya.

Skema Memori Dinamik

Heap_1

Heap_1 merupakan implementasi skema yang paling sederhana. Pada umumnya, sebuah task akan dibuat sebelum kernel RTOS mulai bekerja, dalam hal ini memori akan dialokasikan sebelum kernel bekerja dan alokasi ini akan bertahan selama aplikasi bertahan. Dan tidak akan ada perubahan selama aplikasi berjalan, seperti tidak akan ada penghapusan task atau penghapusan atau pembuatan objek lain, seperti mutex, semaphore dan lain-lain. Aplikasi yang seperti ini cocok menggunakan skema heap_1 ini. Aplikasi yang melarang alokasi memori saat berjalan juga bisa menggunakan heap_1.

Skema Heap_1

Keterangan gambar:
A. Alokasi memori TOTAL_HEAP_SIZE
B. Alokasi memori setelah 1 task terbentuk (Stack+TCB)
C. Alokasi memori setelah 3 task dibentuk

Heap_2

Skema Heap_2 sebenarnya sudah tidak direkomendasikan. Namun untuk menjaga kompabilitas, oleh FreeRTOS heap_2.c tetap didistribusikan. Tidak sperti Heap_1, Heap_2 mendukung untuk membebaskan memori, sehingga bisa menghapus task atau objek lain atau membuat task atau objek baru saat program berjalan.

Skema Heap_2

Di skema Heap_2, blok memori yang dibebaskan tidak akan digabungkan menjadi satu. Terlihat dari gambar

  1. A Kondisi stack setelah 3 stack dibuat.
  2. B Salah satu task dihapus. Maka akan terbentuk memori yang bebas, namun blok untuk Stack dan TCB tidak akan digabung menjadi 1. Sehingga akan ada 2 blok memori yang bebas di antara task 1 dan task 3.
  3. C, kemudian ada task baru yang dibuat, ukuran stacknya sama, maka akan menempati blok memori tersebut. Karena ukurannya sama, maka tidak akan terbentuk fragmentasi.

Heap_2 ini cocok untuk aplikasi di mana ada satu task yang dibuat kemudian dihapus kemudian dibuat lagi saat program berjalan, sehingga ukuran memorinya tetap sama. Tetapi sekali lagi Heap_2 ini tidak direkomendasikan.

Heap_3

Skema Heap_3 ini alokasi memorinya tidak menggunakan fungsi pvPortMalloc() dan fungsi vPortFree(), melainkan menggunakan fungsi standar C (malloc() dan free()). Alokasi memori ini akan menggunakan alokasi memori heap yang didefinisikan oleh linker script. Oleh karena itu perlu diperhatikan ketika menggunakan skema ini agar menentukan ukuran heap sesuai dengan stack setiap task yang dibuat. Setelah di-compile juga, compiler tidak akan menunjukan penggunaan RAM yang dialokasikan oleh skema ini. Hati-hati agar memperhatikan ukuran RAM dari jenis MCU yang digunakan.

Heap_4

Heap_4, skema ini yang sering saya pakai ketika membuat aplikai dengan FreeRTOS, dan juga STM32CubeMX membuat heap_4 ini sebagai piliha default ketika alokasi dinamik yang dipilih.

Skema Heap_4

Seperti halnya Heap_1 dan Heap_2, Heap_4 akan membagi alokasi Heap menjadi blok-blok memori yang berukuran lebih kecil sesuai alokasi setiap task. Namun tidak seperti Heap_2 yang tidak akan menggabungkan blok-blok memori ketika fungsi vPortFree() dipanggil karena ada task yang dihapus misalnya, Heap_4 akan menggabungkan blok-blok tersebut menjadi satu blok yang lebih besar.

  1. A, tidak task terbentuk.
  2. B, task 2 dihapus, jadi akan ada 2 blok memori yang dibebaskan (TCB dan Stack), kedua blok ini kemudian digabung menjadi 1 blok memori
  3. C, sebuah objek (Queue) dibentuk, menempati blok memori tadi. Ukurannya lebih kecil, sehingga akan ada blok yang masih kosong.
  4. D, kemudian ada alokasi lagi (User), dan ditempatkan juga di blok memori yang kosong tadi. Karena ukurannya lebih kecil, masih ada blok memori yang kosong.
  5. E dan F, kedua objek (Queue dan User) dihapus, dan blok-blok memori tersebut kemudian akan digabung menjadi satu blok seperti kondisi B.

Heap_5

Alokasi memori menggunakan skema Heap_5 hampir sama dengan skema Heap_4, bedanya kalap Heap_4 alokasi akan membuat sebuah memori array tunggal, sementara Heap_5 bisa mengalokasikan memori ke blok-blok yang berbeda. Sebagai contoh STM32F407 selain mempunyai RAM 128 KB dengan alamat awal 0x2000000, juga mempunyai CCMRAM 64 KB dengan alamat awal 0x1000000. Nah dengan menggunakan Heap_5 memungkinkan untuk menempatkan heap FreeRTOS di dua area tersebut, misal 2KB di RAM, 32 KB di CCMRAM.

Heap_5 juga memungkinkan kita sebagai programmer menentukan sendiri penempatan alamat memori untuk alokasi ini, misal walaupun dalam satu area (misal CCMRAM), tapi dibagi-bagi menjadi blok-blok heap yang berbeda-beda.

STM32F407 RAM

Alokasi Memori Statik

Sejak FreeRTOS versi 9 alokasi memori secara statik. Dengan alokasi memori statik kita tidak perlu mendefinisikan TOTAL_HEAP yang harus dialokasikan untuk masing-masing task. Fungsi-fungsi untuk membuat task pun berbeda dengan fungsi yang digunakan dalam alokasi memori dinamik.

Dalam alokasi statik, alokasi memori saat proses link, setelah compile. Compiler akan mengalokasikan memori untuk setiap task, sehingga kita tidak perlu khawatir heap memori yang kurang atau memori yang terbuang percuma karena mengalokasikan secara berlebih. Mungkin alokasi statik bisa dibilang lebih baik dari pada alokasi dinamik. Selama ini saya selalu memakai alokasi dinamik, jadi belum bisa membandingkan. Untuk sementara silakan merujuk ke artikel berikut.

Semoga bermanfaat.

One thought on “Bermain-main dengan FreeRTOS (Bagian 5): Manajemen Memori”

Leave a Reply

Your email address will not be published.