FreeRTOS tickless idle on Atmega128

m128-zzzReal-time operating system (RTOS) makes embedded coding more elegant and maintainable if MCU has to take care of many things at the same time. FreeRTOS also supports many platform including  8-bit AVRs. I am using it because of both reasons. Problem is that while FreeRTOS supports low-power mode it does not provide such implementation for AVR. So I have implemented porting layer on my own.

FreeRTOS has tick suppression mode, which means when idling it can go sleep stopping active ticking. Since sleeping MCU consumes tens or hundreds time less power then in active mode, it is a game worth playing especially if the system has long idle periods. How long MCU should sleep is calculated by RTOS scheduler which knows when to wake up for next activity.

Tickless idle porting extension

To turn off most of Atmega128’s functional blocks, it can either go to “extended standby” or “power save” mode. They are almost identical except for that in extended standby main oscillator is still swinging, while in power save only low frequency (watch) crystal can work, which allows to balance wake-up speed versus power saving. The former takes wakes up in 6 clock cycles taking a bit more current to keep crystal working, the latter is much more power conserving at cost of precious time: wakeup takes from hundreds to several thousands clock cycles blocking MCU until main oscillator is stabilized.

My tickless implementation uses both modes and requires watch crystal to be connected to TOSC0-TOSC1 pins. If scheduler asks porting layer to sleep close to wake-up time of main crystal, it chooses to keep main crystal swinging in “extended standby” mode instead of going to “power save”. The prvCalculateDeepSleepMinTicks() can calculate number of ticks crystal needs to wake up as it combines fuse bits settings with configTICK_RATE_HZ, see below.

/*
 * Calculate minimum sleep time rationalizing deeper sleep
 * that requires more time to wake up.
 */
static portTickType prvCalculateDeepSleepMinTicks()
{
	/* Reading sys clock ticks to wake-up depending on oscillator used.
	   See atmega128a datasheet table 8-4 */
	uint8_t lfuse = boot_lock_fuse_bits_get( GET_LOW_FUSE_BITS );
	uint16_t wakeCK;
	if ( ~(lfuse & (1<<0)) & ( ~(lfuse & (1<<4)) | ~(lfuse & (1<<5)) ) ) {
		/* CKSEL0 (bit 0) is programmed and any of SUT0 (bit 4) or SUT1 (bit 5)
		   is programmed then 16K cycles. */
		wakeCK = 16000;
	} else if ( (lfuse & (1<<0)) & (lfuse & (1<<5)) ) {
		/* CKSEL0 is not-programmed and SUT1 is not-programmed */
		wakeCK = 258;
	} else {
		wakeCK = 1000;
	}
	/* Expected sleep time (in ticks) has to be N-times longer than wake time.
	   Factor of 10 is to lower error of sleep count.
	   Example: for 12MHz clock crystal needs 16k cycles = 1.33ms to work again
	   so at 1000Hz ticks freq, we need at least 13.3ms so 14 ticks. */
	return ((uint32_t)configTICK_RATE_HZ * 10) / (configCPU_CLOCK_HZ / wakeCK);
}

The Timer0 running asynchronously on watch crystal is the only timer that can be used for planned wake up. It is 8 bit only so it quickly counts to 255 and overflows, which limits its usage comparing to 16 (or 32) bit size of tick data type. Porting layer implementation is trying to find best settings of frequency prescaler per each sleep request so that it counts to numbers from 1 to 255.

/* Find the best Timer0 prescaler to sleep as long as possible with 8-bit
 * precision. This happen for maximum possible prescaled frequency with
 * overflow later than  requested sleep time i.e. when
 * 		max(fpre) < tick_rate*256/expected
 * Then counter should not exceed
 * 		countMax = floor(expected*fpre/ticks_rate)
 * Storing data in struct for further calculations.
 */
inline static presc_t prvCalculatePrescaler( portTickType xExpectedIdleTime )
{
	presc_t pr;
	uint32_t fexp = ( (uint32_t)configTICK_RATE_HZ * 0xFF ) / xExpectedIdleTime;
	uint16_t fpre[] = {TIMER0_FOSC/1, TIMER0_FOSC/8, TIMER0_FOSC/32,
			TIMER0_FOSC/64, TIMER0_FOSC/128, TIMER0_FOSC/256, 
			TIMER0_FOSC/1024};
	uint8_t size = sizeof(fpre)/sizeof(fpre[0]);
	pr.prescalerMask=0;
	for (uint8_t i=0; i<size; i++) {
		pr.prescalerMask++;
		if (fpre[i] < fexp) break;
	}
	/* Count maximum */
	pr.count = ((uint32_t)xExpectedIdleTime * fpre[pr.prescalerMask-1])
			/ configTICK_RATE_HZ;
	return pr;
}

For very long sleep periods it eventually hits the roof – largest prescaler is 1024, so counting to 255 on 32768Hz quartz takes 8 seconds at maximum. It is not a big deal as FreeRTOS sleeping may be interrupted anyway and it has to recover. In case MCU cannot sleep longer, porting layer after wake-up recalculates how long it has slept and reports back to OS. For example if it is planned to sleep 20 seconds, it will first go sleep for 8 seconds reporting only number of ticks it slept over this duration, then FreeRTOS ask to sleep for 12 seconds more, and after 8 seconds again for remaining 4 seconds. If external interrupt occurs meantime it will break scheduled sleep process of course and if ISR unblocks any task, idling/sleeping is not continued.

Adaptive prescaler approach leads to significant inaccuracy due to low counter resolution and coarse granularity of prescaled timer, especially for fine grained tick frequency. Aware of this limitation I have chosen this strategy as power saving in solar-powered data logger is more relevant than strict timing.

Low-power mode and I/O.

Sleeping RTOS brings benefits as well as some traps. Imagine a session with external device like GSM or BT module with USART interface. The command is TX-ed and MCU waits for RX. In normal tick mode it works, then you switch to low-power mode and … no response comes back. Chances are that your tasks somehow wait for response, probably with interrupts and queues, but RTOS scheduler detected idle moment and eagerly put MCU to sleep. But sleeping Atmega cannot wake up on RX pin activity (in fact only a few AVRs can do it – tiny 1634 and 828 have USART with Start Frame Detector, some AtXmegas also have it, Atmegas do not). I would not try to use RX pin to manually detect external interrupt; extra overhead with own code increases possibility to miss first bit or two and then communication is out of sync.

To make USART session working MCU must stay awaken. I see two options:

  1. USART code manipulation can be rewriten with loops (wait for data actively) instead interrupts and paying attention to not let RTOS go sleep (no delays etc). Code becomes less elegant at least.
  2. The other option is to have block of code representing the session marked as non-sleeping. While FreeRTOS support for low-power mode is compile-time, we cannot disable/enable sleeping in idle in runtime. However the configEXPECTED_IDLE_TIME_BEFORE_SLEEP parameter holds scheduler off sleeping for too short delays. Clever suggestion I got was to replace static value with macro. I did it this way and it works, details below.

The FreeRTOSConfig.h has static value replaced with evaluate one, like this:

uint8_t g_keepActive;
#define USE_TICKLESS_IDLE(enabled) 		{ g_keepActive = !enabled; }
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP	( g_keepActive ? 0xFFFFFFFF : 2)

and then in task code I simply disable sleeping for a hypothetical block of operations:

USE_TICKLESS_IDLE(false);
vPowerUp(GSM);
vUsartSendCommand(GSM, GSM_SEND_SMS);
if (xUsartWaitForResponse(GSM) != GSM_OK) {
...
vPowerDown(GMS);
USE_TICKLESS_IDLE(true);

Download

Creative Commons License

The detailed description is in the source code shared hereby. Implementation relies on WinAVR / avr-gcc toolchain. My part of work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Download file: FreeRTOS-ATMega128-tickless-sleep.zip

See also other related articles:
[posts_by_tag tag=”WLS”]

This entry was posted in Electronics and tagged , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.