diff --git a/doc/tick_sources.md b/doc/tick_sources.md new file mode 100644 index 0000000..299de89 --- /dev/null +++ b/doc/tick_sources.md @@ -0,0 +1,190 @@ +# Scheduler tick sources + +## Configuration +Tick source is selected by (un)defining values `portUSE_WDTO` and `portUSE_TIMER0` in file `FreeRTOSVariant.h`. Default in Arduino_FreeRTOS is Watchdog timer (WDT), it contains all code needed for this and works out-of-the-box. + +For alternative tick source, pieces of code must be provided by the application. Arduino_FreeRTOS expects you to provide function `void prvSetupTimerInterrupt(void)` responsible for the initialization of your tick source. This function is called after the Arduino's initialization and before the FreeRTOS scheduler is launched. + +NOTE: Reconfiguring Timer0 for FreeRTOS will break Arduino `millis()` and `micros()`, as these functions rely on Timer0. Functions relying on these Arduino features need to be overridden. + + + +## WDT (default) +Time slices can be selected from 15ms up to 500ms. Slower time slicing can allow the Arduino MCU to sleep for longer, without the complexity of a Tickless idle. + +Watchdog period options: +* `WDTO_15MS` (default) +* `WDTO_30MS` +* `WDTO_60MS` +* `WDTO_120MS` +* `WDTO_250MS` +* `WDTO_500MS` +* `WDTO_1S` +* `WDTO_2S` + +### WDT precision limitations +The frequency of the Watchdog Oscillator is voltage and temperature dependent as shown in “Typical Characteristics” on corresponding figures: + +![WDT limitations](https://user-images.githubusercontent.com/35344069/224619444-3c0b634c-f460-40d2-8a73-256bad0d5ba1.png) + +Timing consistency may vary as much as 20% between two devices in same setup due to individual device differences, or between a prototype and production device due to setup differences. + +## Alternative tick sources +For applications requiring high precision timing, the Ticks can be sourced from a hardware timer or external clock. + +First, you switch it in `FreeRTOSVariant.h` header by removing or undefining `portUSE_WDTO` and defining `portUSE_TIMER0`. +```cpp +#undef portUSE_WDTO +#define portUSE_TIMER0 +#define portTICK_PERIOD_MS 16 +``` + +Next, in your app you provide two pieces of code: the initialization function and the ISR hook. Their implementation depends of what is your tick source. + + +## Hardware timer Timer0 +### Timer initialization function +_NOTE: This code snippet is verified to work on Atmega2560. Full code avaialable [here](./tick_sources_timer0.cpp)._ +```cpp +// Formula for the frequency is: +// f = F_CPU / (PRESCALER * (1 + COUNTER_TOP) +// +// Assuming the MCU clock of 16MHz, prescaler 1024 and counter top 249, the resulting tick period is 16 ms (62.5 Hz). +// +#define TICK_PERIOD_16MS 249 +#define PRESCALER 1024 +#if (portTICK_PERIOD_MS != (PRESCALER * (1 + TICK_PERIOD_16MS) * 1000 / F_CPU)) + #warning portTICK_PERIOD_MS defined in FreeRTOSVariant.h differs from your timer configuration +#endif + +// For register TCCR0A: +#define NO_PWM (0 << COM0A1) | (0 << COM0A0) | (0 << COM0B1) | (0 << COM0B0) +#define MODE_CTC_TCCR0A (1 << WGM01) | (0 << WGM00) + +// For register TCCR0B: +#define MODE_CTC_TCCR0B (0 << WGM02) +#define PRESCALER_1024 (1 << CS02) | (0 << CS01) | (1 << CS00) + +// For register TIMSK0: +#define INTERRUPT_AT_TOP (1 << OCIE0A) + +extern "C" +void prvSetupTimerInterrupt( void ) +{ + // In case Arduino platform has pre-configured the timer, + // disable it before re-configuring here to avoid unpredicted results: + TIMSK0 = 0; + + // Now configure the timer: + TCCR0A = NO_PWM | MODE_CTC_TCCR0A; + TCCR0B = MODE_CTC_TCCR0B | PRESCALER_1024; + OCR0A = TICK_PERIOD_16MS; + + // Prevent missing the top and going into a possibly long wait until wrapping around: + TCNT0 = 0; + + // At this point the global interrupt flag is NOT YET enabled, + // so you're NOT starting to get the ISR calls until FreeRTOS enables it just before launching the scheduler. + TIMSK0 = INTERRUPT_AT_TOP; +} +``` + +Though Timer0 is given as example here, any timer can be used. A 16-bit timer (e.g., Timer1) is needed for time slices longer than ~20 milliseconds. + +### ISR hook +For **preemptive** scheduler use `naked` attribute to reduce the call overhead: +```cpp +ISR(TIMER0_COMPA_vect, ISR_NAKED) __attribute__ ((hot, flatten)); +ISR(TIMER0_COMPA_vect) { + portSchedulerTick(); + __asm__ __volatile__ ( "reti" ); +} +``` + +The context is saved at the start of `portSchedulerTick()`, then the tick count is incremented, finally the new context is loaded - so no dirtying occurs. + + +For **cooperative** scheduler, the context is not saved because no switching is intended; therefore `naked` attribute cannot be applied because cooperative `portSchedulerTick()` dirties the context. +```cpp +ISR(TIMER0_COMPA_vect) __attribute__ ((hot, flatten)); +ISR(TIMER0_COMPA_vect) { + portSchedulerTick(); +} +``` + +Use ISR_NOBLOCK where there is an important timer running, that should preempt the scheduler: +```cpp +ISR(portSCHEDULER_ISR, ISR_NAKED ISR_NOBLOCK) __attribute__ ((hot, flatten)); +``` + +Attributes `hot` and `flatten` help inlining all the code found inside your ISR thus reducing the call overhead. + + +## External clock +### Input configuration function +_NOTE: This code snippet was not verified on actual MCU._ + +Assuming the external clock is connected to data pin 21 (function INT0): +```cpp +// For register EICRA: +#define TICK_ON_RISING_EDGE_D21 (1 << ISC01) | (1 << ISC00) + +// For register EIMSK: +#define TICK_INPUT_PIN_D21 (1 << INT0) + +extern "C" +void prvSetupTimerInterrupt( void ) +{ + EICRA = TICK_ON_RISING_EDGE_D21; + + // At this point the global interrupt flag is NOT YET enabled, + // so you're NOT starting to get the ISR calls until FreeRTOS enables it just before launching the scheduler. + EIMSK = TICK_INPUT_PIN_D21; + + // Configure the pin + pinMode(21, INPUT); +} +``` + + +### ISR hook +Similar to Timer0 ISR, for **preemptive** scheduler: +```cpp +ISR(INT0_vect, ISR_NAKED) __attribute__ ((hot, flatten)); +ISR(INT0_vect) { + portSchedulerTick(); + __asm__ __volatile__ ( "reti" ); +} +``` + +For **cooperative** scheduler: +```cpp +ISR(INT0_vect) __attribute__ ((hot, flatten)); +ISR(INT0_vect) { + portSchedulerTick(); +} +``` + + + +## Performance considerations +When selecting the duration for the time slice, the following should be kept in mind. + +### Granularity +Note that Timer resolution (or granularity) is affected by integer math division and the time slice selected. For example, trying to measure 50ms using a 120ms time slice won't work. + +### Context switching +In preemptive mode, tasks which are actively executing (i.e., those not waiting for a semaphore or queue) might be switched every time tick, depending on their priority. Switching the context involves pushing all CPU's registers of old task and poping all registers of new task. The shorter your time slice is, the bigger of overhead this becomes. + +In cooperative mode, context overhead is not a factor. + +### Calculations +On MCUs lacking the hardware division operation like AVR, a special care should be taken to avoid division operations. Where unavoidable, operations with divisor of power of 2 work best because they are performed with bitwise shifting, whereas an arbitrary value results in a software division operation taking ~200 clock cycles (for a uint16 operand). + +You might encounter a division when calculating delays, e.g. converting milliseconds to ticks: +```cpp + TickType_t ticks = delay_millis / portTICK_PERIOD_MS +``` + +If your application needs to do this sort of conversion a lot, consider making your time slice a power-of-2 value (16 ms, 32 ms, 64 ms etc.). + diff --git a/doc/tick_sources_timer0.cpp b/doc/tick_sources_timer0.cpp new file mode 100644 index 0000000..0043bc4 --- /dev/null +++ b/doc/tick_sources_timer0.cpp @@ -0,0 +1,52 @@ +#include + +/* + * Formula for the frequency is: + * f = F_CPU / (PRESCALER * (1 + COUNTER_TOP) + * + * Assuming the MCU clock of 16MHz, prescaler 1024 and counter top 249, the resulting tick period is 16 ms (62.5 Hz). + */ +#define TICK_PERIOD_16MS 249 +#define PRESCALER 1024 +#if (portTICK_PERIOD_MS != (PRESCALER * (1 + TICK_PERIOD_16MS) * 1000 / F_CPU)) + #warning portTICK_PERIOD_MS defined in FreeRTOSVariant.h differs from your timer configuration +#endif + +// For register TCCR0A: +#define NO_PWM (0 << COM0A1) | (0 << COM0A0) | (0 << COM0B1) | (0 << COM0B0) +#define MODE_CTC_TCCR0A (1 << WGM01) | (0 << WGM00) + +// For register TCCR0B: +#define MODE_CTC_TCCR0B (0 << WGM02) +#define PRESCALER_1024 (1 << CS02) | (0 << CS01) | (1 << CS00) + +// For register TIMSK0: +#define INTERRUPT_AT_TOP (1 << OCIE0A) + +extern "C" +void prvSetupTimerInterrupt( void ) +{ + // In case Arduino platform has pre-configured the timer, + // disable it before re-configuring here to avoid unpredicted results: + TIMSK0 = 0; + + // Now configure the timer: + TCCR0A = NO_PWM | MODE_CTC_TCCR0A; + TCCR0B = MODE_CTC_TCCR0B | PRESCALER_1024; + OCR0A = TICK_PERIOD_16MS; + + // Prevent missing the top and going into a possibly long wait until wrapping around: + TCNT0 = 0; + + // At this point the global interrupt flag is NOT YET enabled, + // so you're NOT starting to get the ISR calls until FreeRTOS enables it just before launching the scheduler. + TIMSK0 = INTERRUPT_AT_TOP; +} + + +ISR(TIMER0_COMPA_vect, ISR_NAKED) __attribute__ ((hot, flatten)); +ISR(TIMER0_COMPA_vect) +{ + portSchedulerTick(); + __asm__ __volatile__ ( "reti" ); +} diff --git a/readme.md b/readme.md index 8faa94f..c285aed 100644 --- a/readme.md +++ b/readme.md @@ -21,21 +21,9 @@ Over the past few years freeRTOS development has become increasingly 32-bit orie FreeRTOS has a multitude of configuration options, which can be specified from within the FreeRTOSConfig.h file. To keep commonality with all of the Arduino hardware options, some sensible defaults have been selected. Feel free to change these defaults as you gain experience with FreeRTOS. -The AVR Watchdog Timer is used to generate 15ms time slices (Ticks), but Tasks that finish before their allocated time will hand execution back to the Scheduler. +Normally, the AVR Watchdog Timer is used to generate 15ms time slices (Ticks). For applications requiring high precision timing, the Ticks can be sourced from a hardware timer or external clock. See chapter [Scheduler Tick Sources](./doc/tick_sources.md) for the configuration details. -Time slices can be selected from 15ms up to 500ms. Slower time slicing can allow the Arduino MCU to sleep for longer, without the complexity of a Tickless idle. - -Watchdog period options: -* `WDTO_15MS` -* `WDTO_30MS` -* `WDTO_60MS` -* `WDTO_120MS` -* `WDTO_250MS` -* `WDTO_500MS` -* `WDTO_1S` -* `WDTO_2S` - -Note that Timer resolution (or granularity) is affected by integer math division and the time slice selected. Trying to measure 50ms, using a 120ms time slice for example, won't work. +Tasks that finish before their allocated time will hand execution back to the Scheduler. The Arduino `delay()` function has been redefined to automatically use the FreeRTOS `vTaskDelay()` function when the delay required is one Tick or longer, by setting `configUSE_PORT_DELAY` to `1`, so that simple Arduino example sketches and tutorials work as expected. If you would like to measure a short millisecond delay of less than one Tick, then preferably use [`millis()`](https://www.arduino.cc/reference/en/language/functions/time/millis/) (or with greater granularity use [`micros()`](https://www.arduino.cc/reference/en/language/functions/time/micros/)) to achieve this outcome (for example see [BlinkWithoutDelay](https://docs.arduino.cc/built-in-examples/digital/BlinkWithoutDelay)). However, when the delay requested is less than one Tick then the original Arduino `delay()` function will be automatically selected. diff --git a/src/FreeRTOSVariant.h b/src/FreeRTOSVariant.h index 61416d5..e92802c 100644 --- a/src/FreeRTOSVariant.h +++ b/src/FreeRTOSVariant.h @@ -53,6 +53,92 @@ extern "C" { /* Watchdog Timer is 128kHz nominal, but 120 kHz at 5V DC and 25 degrees is actually more accurate, from data sheet. */ #define configTICK_RATE_HZ ( (TickType_t)( (uint32_t)128000 >> (portUSE_WDTO + 11) ) ) // 2^11 = 2048 WDT scaler for 128kHz Timer +/*-----------------------------------------------------------*/ +// To switch to an alternative tick source, uncomment this block: +// #undef portUSE_WDTO +// #define portUSE_TIMER0 +// #define portTICK_PERIOD_MS 16 + +/* + * When a tick source other than WDT is used, configuring the tick source becomes the user's responsibility. + * E.g., when using Timer0 for the tick source, you can use the following snippet: + * + * // Formula for the frequency is: + * // f = F_CPU / (PRESCALER * (1 + COUNTER_TOP) + * // + * // Assuming the MCU clock of 16MHz, prescaler 1024 and counter top 249, the resulting tick period is 16 ms (62.5 Hz). + * // + * #define TICK_PERIOD_16MS 249 + * #define PRESCALER 1024 + * #if (portTICK_PERIOD_MS != (PRESCALER * (1 + TICK_PERIOD_16MS) * 1000 / F_CPU)) + * #warning portTICK_PERIOD_MS defined in FreeRTOSVariant.h differs from your timer configuration + * #endif + * + * // For register TCCR0A: + * #define NO_PWM (0 << COM0A1) | (0 << COM0A0) | (0 << COM0B1) | (0 << COM0B0) + * #define MODE_CTC_TCCR0A (1 << WGM01) | (0 << WGM00) + * + * // For register TCCR0B: + * #define MODE_CTC_TCCR0B (0 << WGM02) + * #define PRESCALER_1024 (1 << CS02) | (0 << CS01) | (1 << CS00) + * + * // For register TIMSK0: + * #define INTERRUPT_AT_TOP (1 << OCIE0A) + * + * extern "C" + * void prvSetupTimerInterrupt( void ) + * { + * // In case Arduino platform has pre-configured the timer, + * // disable it before re-configuring here to avoid unpredicted results: + * TIMSK0 = 0; + * + * // Now configure the timer: + * TCCR0A = NO_PWM | MODE_CTC_TCCR0A; + * TCCR0B = MODE_CTC_TCCR0B | PRESCALER_1024; + * OCR0A = TICK_PERIOD_16MS; + * + * // Prevent missing the top and going into a possibly long wait until wrapping around: + * TCNT0 = 0; + * + * // At this point the global interrupt flag is NOT YET enabled, + * // so you're NOT starting to get the ISR calls until FreeRTOS enables it just before launching the scheduler. + * TIMSK0 = INTERRUPT_AT_TOP; + * } + */ +void prvSetupTimerInterrupt( void ); + +/* + * When a tick source other than WDT is used, calling the scheduler becomes the user's responsibility. + * E.g., when using Timer0 for the tick source, you'll have a timer compare IRS wherein you call portSCHEDULER_ISR(). + * + * Implement your ISR handler efficiently: + * + * For PREEMPTIVE scheduler use a `naked` attribute to reduce the call overhead: + * + * ISR(portSCHEDULER_ISR, ISR_NAKED) __attribute__ ((hot, flatten)) { + * portSchedulerTick(); + * __asm__ __volatile__ ( "reti" ); + * } + * + * The context is saved at the start of `portSchedulerTick()`, then the tick count is incremented, finally + * the new context is loaded - so no dirtying occurs. + * + * + * For COOPERATIVE scheduler, the context is not saved because no switching is intended; therefore `naked` attribute + * cannot be applied because cooperative `portSchedulerTick()` dirties the context. + * + * ISR(portSCHEDULER_ISR) __attribute__ ((hot, flatten)) { + * portSCHEDULER_ISR(); + * } + * + * Use ISR_NOBLOCK where there is an important timer running, that should preempt the scheduler: + * + * ISR(portSCHEDULER_ISR, ISR_NAKED ISR_NOBLOCK) __attribute__ ((hot, flatten)); + * + * Attributes `hot` and `flatten` help inlining all the code found inside your ISR thus reducing the call overhead. + */ +void portSchedulerTick( void ) __attribute__ ((hot, flatten)); + /*-----------------------------------------------------------*/ #ifndef INC_TASK_H diff --git a/src/port.c b/src/port.c index f15270b..2b9a664 100644 --- a/src/port.c +++ b/src/port.c @@ -50,17 +50,6 @@ #elif defined( portUSE_TIMER0 ) /* Hardware constants for Timer0. */ #warning "Timer0 used for scheduler." - #define portSCHEDULER_ISR TIMER0_COMPA_vect - #define portCLEAR_COUNTER_ON_MATCH ( (uint8_t) _BV(WGM01) ) - #define portPRESCALE_1024 ( (uint8_t) (_BV(CS02)|_BV(CS00)) ) - #define portCLOCK_PRESCALER ( (uint32_t) 1024 ) - #define portCOMPARE_MATCH_A_INTERRUPT_ENABLE ( (uint8_t) _BV(OCIE0A) ) - #define portOCRL OCR0A - #define portTCCRa TCCR0A - #define portTCCRb TCCR0B - #define portTIMSK TIMSK0 - #define portTIFR TIFR0 - #else #error "No Timer defined for scheduler." #endif @@ -524,12 +513,6 @@ volatile TickType_t ticksRemainingInSec; #endif /*-----------------------------------------------------------*/ -/* - * Perform hardware setup to enable ticks from relevant Timer. - */ -static void prvSetupTimerInterrupt( void ); -/*-----------------------------------------------------------*/ - /* * See header file for description. */ @@ -653,6 +636,7 @@ void vPortEndScheduler( void ) extern void delay ( unsigned long ms ); void vPortDelay( const uint32_t ms ) __attribute__ ((hot, flatten)); +#if defined( portUSE_WDTO ) void vPortDelay( const uint32_t ms ) { if ( ms < portTICK_PERIOD_MS ) @@ -665,6 +649,10 @@ void vPortDelay( const uint32_t ms ) delay( (unsigned long) (ms - portTICK_PERIOD_MS) % portTICK_PERIOD_MS ); } } +#elif defined( portUSE_TIMER0 ) +// The user is responsible to provide function `vPortDelay` +extern void vPortDelay( const uint32_t ms ) __attribute__ ((hot, flatten)); +#endif /*-----------------------------------------------------------*/ /* @@ -732,45 +720,13 @@ void prvSetupTimerInterrupt( void ) } #elif defined( portUSE_TIMER0 ) -/* - * Setup Timer0 compare match A to generate a tick interrupt. - */ -static void prvSetupTimerInterrupt( void ) -{ -uint32_t ulCompareMatch; -uint8_t ucLowByte; - - /* Using 8bit Timer0 to generate the tick. Correct fuses must be - selected for the configCPU_CLOCK_HZ clock.*/ - - ulCompareMatch = configCPU_CLOCK_HZ / configTICK_RATE_HZ; - - /* We only have 8 bits so have to scale 1024 to get our required tick rate. */ - ulCompareMatch /= portCLOCK_PRESCALER; - - /* Adjust for correct value. */ - ulCompareMatch -= ( uint32_t ) 1; - - /* Setup compare match value for compare match A. Interrupts are disabled - before this is called so we need not worry here. */ - ucLowByte = ( uint8_t ) ( ulCompareMatch & ( uint32_t ) 0xff ); - portOCRL = ucLowByte; - - /* Setup clock source and compare match behaviour. */ - portTCCRa = portCLEAR_COUNTER_ON_MATCH; - portTCCRb = portPRESCALE_1024; - - - /* Enable the interrupt - this is okay as interrupt are currently globally disabled. */ - ucLowByte = portTIMSK; - ucLowByte |= portCOMPARE_MATCH_A_INTERRUPT_ENABLE; - portTIMSK = ucLowByte; -} - +// The user is responsible to provide function `prvSetupTimerInterrupt` +extern void prvSetupTimerInterrupt( void ); #endif /*-----------------------------------------------------------*/ +#if defined( portUSE_WDTO ) #if configUSE_PREEMPTION == 1 /* @@ -786,7 +742,7 @@ uint8_t ucLowByte; */ ISR(portSCHEDULER_ISR) { - vPortYieldFromTick(); + portSchedulerTick(); __asm__ __volatile__ ( "reti" ); } #else @@ -803,6 +759,17 @@ uint8_t ucLowByte; */ ISR(portSCHEDULER_ISR) { - xTaskIncrementTick(); + portSchedulerTick(); } #endif +#endif +/*-----------------------------------------------------------*/ + +void portSchedulerTick( void ) +{ +#if configUSE_PREEMPTION == 1 + vPortYieldFromTick(); +#else + xTaskIncrementTick(); +#endif +} diff --git a/src/portmacro.h b/src/portmacro.h index 8d298cc..cdd3c61 100644 --- a/src/portmacro.h +++ b/src/portmacro.h @@ -93,7 +93,7 @@ typedef uint8_t UBaseType_t; #if defined( portUSE_WDTO ) #define portTICK_PERIOD_MS ( (TickType_t) _BV( portUSE_WDTO + 4 ) ) #else -#define portTICK_PERIOD_MS ( (TickType_t) 1000 / configTICK_RATE_HZ ) +// Variant configuration must define portTICK_PERIOD_MS as macro like `( (TickType_t) 1000 / configTICK_RATE_HZ )` or a constant value like `16` #endif #define portBYTE_ALIGNMENT 1