Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile

How to Port Free RTOS to Atmega128

4.31/5 (10 votes)
22 Jun 2011CPOL4 min read 53K   1.6K  
How to port Free RTOS to Atmega128

Introduction

I have successfully ported Free RTOS to atmega128, although I did not write all the port functions, but I will explain how they work.

Using the Code

Free RTOS has a port to GCC ATMega323, not Atmega128, but surprisingly, all the port functions written for atmega323 work for Atmega128, as it is told by those who have successfully ported.

I am using AVR Studio 4 for Windows, and use Microsoft Studio 2008 as my source code editor, as you can see in my uploaded projects and source code.

First, you need to download AVR Studio 4 from Atmel web site, then download WinAVR from their web site as well as this project will use avr-gcc compiler to compile all the source code.

You can download my ports, then open the ".aps" file from AVR Studio 4, then run it straightaway, it should compile. After it is compiled, you will see a ".hex" created, you can use this file program your PCB board, I am using Ponyprog2000.

My led is at PORTE pin 2, I can see it flashes my PCB board, in your case might be different.

Now I will explain how RTOS works, why we need a "port" actions.

RTOS only provides us the kernel source code, like the task scheduler, semaphore, mutex, etc. And their implementations. We want to have multi tasks in our embedded system, RTOS will be able to do this for us. But in different processors, things can be different.

To schedule a task, we need to save all the registers in CPU, different architectures, different registers, and different assembly instructions, like the following lines in port.c file:

C++
#define portSAVE_CONTEXT()         \
 asm volatile ( "push r0      \n\t" \
     "in  r0, __SREG__   \n\t" \
     "cli       \n\t" \
     "push r0      \n\t" \
     "push r1      \n\t" \
     "clr r1      \n\t" \
     "push r2      \n\t" \
     "push r3      \n\t" \
     "push r4      \n\t" \        

It saves SREG enable interrupt flags, clears interrupt flags, and general registers (32 in atmega128).

Then it initializes stack space for task, in most CPUs stack grows from top to bottom (0xffff to 0). But some are different.

Then we need to setup our OS tick functionality, it is in "prvSetupTimerInterrupt" function:

C++
OCR1AH = ucHighByte;
OCR1AL = ucLowByte;
/* Setup clock source and compare match behaviour. */
ucLowByte = portCLEAR_COUNTER_ON_MATCH | portPRESCALE_64;
TCCR1B = ucLowByte;
/* Enable the interrupt - this is okay as interrupt are currently globally
disabled. */
ucLowByte = TIMSK;
ucLowByte |= portCOMPARE_MATCH_A_INTERRUPT_ENABLE;
TIMSK = ucLowByte;

When OS ticks, we can reschedule our tasks, this can be done by calling "vPortYieldFromTick",

As you can see this from the following line in OS tick interrupt in port.c:

C++
 #if configUSE_PREEMPTION == 1
 /*
  * Tick ISR for preemptive scheduler.  We can use a naked attribute as
  * the context is saved at the start of vPortYieldFromTick().  The tick
  * count is incremented after the context is saved.
  */
 void SIG_OUTPUT_COMPARE1A( void ) __attribute__ ( ( signal, naked ) );
 void SIG_OUTPUT_COMPARE1A( void )
 {
  vPortYieldFromTick();
  asm volatile ( "reti" );
 }
#else
 /*
  * Tick ISR for the cooperative scheduler.  All this does is increment the
  * tick count.  We don't need to switch context, this can only be done by
  * manual calls to taskYIELD();
  */
 void SIG_OUTPUT_COMPARE1A( void ) __attribute__ ( ( signal ) );
 void SIG_OUTPUT_COMPARE1A( void )
 {
  vTaskIncrementTick();
 }
#endif    

Most of the times, we don't need to know what's behind the RTOS, for us we only need to create a few tasks, or setup interrupt handlers according to RTOS documents.

In my case, I create 3 tasks, one is to flash led at 400ms intervals, another is to reset external watch dog pin by 50ms intervals, 3rd task is refresh LCD.

C++
 portSHORT main(void) {
  //Start Tasks
 Startflash_led(tskIDLE_PRIORITY + 1);
 StartReset_watchdog(tskIDLE_PRIORITY + 2);
 StartLCD_task(tskIDLE_PRIORITY + 1);
  //RunSchedular
  vTaskStartScheduler();
  return 0;
}

As you can see, the reset watch dog has higher priority than flash led, I want to reset external watch dog as quickly as possible, therefore it has higher priority than others.

RTOS will allocate some memory from internal RAM or external RAM for us, to use as stack space for individual tasks.

Simple task like flash led, their stack size only needs less than 50 bytes, these bytes are used to save 32 registers, function pointer address, etc. When RTOS will need to schedule tasks, it will save current running task to stack, this includes saving 32 registers, saving local variables that are created by current task, etc.

My flash led task is created by using the following function:

C++
xTaskCreate(flash_led, (signed portCHAR *)"flash_led",
	configMINIMAL_STACK_SIZE, NULL, Priority, NULL ); 

At FreeRTOSConfig.h file, you will see the following line:

C++
#define configMINIMAL_STACK_SIZE ( ( unsigned portSHORT ) 85 )

So our flash led task stack used 85 bytes.

If a task like drawing LCD screen is taking too long time, it will be interrupted many times by other tasks, then its stack needs to be big enough to save registers and local variables many times.

In my case, I create an lcd task like the following:

C++
xTaskCreate(lcd_task, (signed portCHAR *)"lcd_task",
   configMINIMAL_STACK_SIZE*2, NULL, Priority, NULL );

In my case, I need to tell RTOS my heap size, Atmega128 has 4k bytes internal memory, in my FreeRTOSConfig.h file, I have the following line: 

C++
#define configTOTAL_HEAP_SIZE  ( (size_t ) ( 2800 ) )

Leave some space to AVR libc function calls, as they might use some heap memories.

Each one of our tasks is an infinite loop, flash led task is like the following. We have to create our task this way:

C++
void flash_led() {
  portTickType last_start_time;
 const portTickType ticks = 400; // 1 KHz tick -> 400ms delay
 DDRE |= 4;
 for(;;)
 {
  while( xSemaphoreTake( xSemaphore, ( portTickType ) 254 ) == pdFALSE ); //wait for it
  if (PORTE & _BV(PE2)) PORTE&=(~_BV(PE2));
  else PORTE |= _BV(PE2);
  xSemaphoreGive( xSemaphore ); //Done with the , release the semaphore
  last_start_time = xTaskGetTickCount(); // Last time where task was blocked
  vTaskDelayUntil(&last_start_time, ticks);
 }
}   

The above code used semaphore, we want to access the PORTE exclusively, without other tasks like "reset watchdog" interference.

Then it waits for 400ms flash led again, during this period, RTOS will yield our flash led task to some other tasks.

The next step is adding receive data from RS485 connection, after we have received data we need to put them into a queue, then our receiving data task can handle it.

First, we initialize our RS485 port:

C++
portENTER_CRITICAL();
{
  /* Create the queues used by the com test task. */
    xRxedChars = xQueueCreate( uxQueueLength, ( unsigned portBASE_TYPE )
	sizeof( signed char ) );
    xCharsForTx = xQueueCreate( uxQueueLength/2,
	( unsigned portBASE_TYPE ) sizeof( signed char ) );
      //9600bps
      MASTER_UBRRH=0;
      MASTER_UBRRL = 25;
      MASTER_UCSRB = (COMM_RX_INT_ENABLE|COMM_RX_ENABLE|COMM_9_BITS_SIZE|COMM_TX_ENABLE);
      MASTER_UCSRC = (0x6);
      cbi(PORTD,4);//turn on receive line, half duplex
 }
 portEXIT_CRITICAL(); 

Next, we enable interrupt routine:

C++
SIGNAL(SIG_UART1_RECV )
{
    signed char cChar;
     signed portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
     cChar=MASTER_UCSRB;
     if (cChar & COMM_ADDR_BIT) //this byte is address
     {
          cChar = MASTER_UDR;
          if (cChar & BROADCAST_ADDRESS)
          {
               data_for_me=true;
               need_to_reply=(cChar == stComState.addr)?true:false;
               cChar=ESCAPE;//indicate an address coming
               xQueueSendFromISR( xRxedChars, &cChar, &xHigherPriorityTaskWoken );
          }
          else
          {
               stComState.got_data =false;
               data_for_me=false;
          }
     }
     else if (data_for_me)
     {
          /* Get the character and post it on the queue of Rxed characters.
          If the post causes a task to wake force a context switch as the woken task
          may have a higher priority than the task we have interrupted. */
          cChar = MASTER_UDR;
          if (need_to_reply) //master board may need quick reply from us
          {
          }
          if (cChar==ESCAPE)
               xQueueSendFromISR( xRxedChars, &cChar, &xHigherPriorityTaskWoken );
          xQueueSendFromISR( xRxedChars, &cChar, &xHigherPriorityTaskWoken );
          if( xHigherPriorityTaskWoken != pdFALSE )
          {
               taskYIELD();
          }
     }
} 

Then, at receiving task routine, we handle what we have received:

C++
 if (!xSerialGetChar(NULL,&ch,portMAX_DELAY)) return;
 if (ch==ESCAPE)
 {
      if (is_cmd)
           is_cmd=false;
      else
      {
           is_cmd=true;//next char will be command
           return;
      }
 }
if (is_cmd)
 {
      current_cmd=ch;
      count=0;
      memset(buffer,0,BUFFER_SIZE);
      is_cmd=false;//next char will be data
 }
 if (is_display_line(current_cmd))
 {
      stComState.got_data =true;
      buffer[count++]=ch;
      if (can_display(count)) display_line(buffer);
 }

Free RTOS supports interprocess communication by message queue.

RTOS on atmega128 is a good start of more complicated applications. Atmega128 only got 4k internal RAM, if you have external RAM installed, then you can have more tasks running in your system. It's more easy or convenient than the old style of single while loop doing everything.

We used Atmega128 as a processor to do all the LCD displays, electric circuit checking, simple digital communications, etc.

At this stage, this article is finished.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)