OctOS: a simple tasking system for AVRs

Go To Last Post
3 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 1

I posted about this previously in this thread.  After that, I did go on to verify the code in SPIN and found errors.  I revamped the code to be more amenable to use in C programs.  Next step is to do more verification.  I have posted the code here. The code is covered by the so-called "MIT license".

 

The following is a working example for the Nano Curiosity 4809.  It runs the LED at brightness based on the executing task: main (TASK6) is dim, task3 brighter, task2 brightest.  Tasks 2 and 3 are woken every 3 seconds.

#ifdef F_CPU
#undef F_CPU
#endif
#define F_CPU 5000000

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>
#include <util/delay.h>
#include "octos.h"

/* Flash LED and wait for user button press to start application code. */
void nano_button_wait() {
  uint8_t st = 1;			/* switch state */
  uint8_t lc;				/* led counter */

  TCB0.CCMP = 333;			/* debounce time */
  TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm;
  PORTF.DIRSET = PIN5_bm;		/* LED output */

  while (st) {
    switch (st) {
    case 1: if ((PORTF.IN & PIN6_bm) == 0) st = 2; break;
    case 2: if ((PORTF.IN & PIN6_bm) != 0) st = 0; break;
    }
    if (lc++ == 0) PORTF.OUTTGL = PIN5_bm; /* toggle LED */
    while ((TCB0.INTFLAGS & 0x01) == 0);   /* wait for bounce */
    TCB0.INTFLAGS = 0x01;		   /* reset flag */
  }

  /* Restore MCU state. */
  PORTF.DIRCLR = PIN5_bm;
  TCB0.CTRLA = 0x00;
  TCB0.CCMP = 0;
  TCB0.INTFLAGS = 0x01;
}

void init_sys_clk() {
  _PROTECTED_WRITE(CLKCTRL.MCLKCTRLA, CLKCTRL_CLKSEL_OSC20M_gc);
  _PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, CLKCTRL_PDIV_4X_gc | CLKCTRL_PEN_bm);
  while ((CLKCTRL.MCLKSTATUS & 0x11) != 0x10); /* wait for stable OSC20M */
}

void init_wdt() {
  _PROTECTED_WRITE(WDT.CTRLA, WDT_PERIOD_8KCLK_gc); /* 8 sec watchdog */
}

void init_tca() {
  TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV256_gc | TCA_SINGLE_ENABLE_bm;
  TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_NORMAL_gc;
  TCA0.SINGLE.INTCTRL = TCA_SINGLE_OVF_bm;
  TCA0.SINGLE.PER = 3*9745;
}

ISR(TCA0_OVF_vect) {
  TCA0.SINGLE.INTFLAGS = TCA_SINGLE_ENABLE_bm;
  oct_isr_wake_task(OCT_TASK2 | OCT_TASK3);
}

#define T2_ITER 15000
#define T3_ITER 30000

#define STKSIZ2 0x80
uint8_t task2_stk[STKSIZ2];

void __attribute__((OS_task)) task2(void) {
  sei();
  while (1) {
    oct_idle_task(OCT_TASK2);
    for (int i = 0; i < T2_ITER; i++) {
      PORTF.OUTCLR = PIN5_bm;
      _delay_us(9);
      PORTF.OUTSET = PIN5_bm;
      _delay_us(1);
    }
  }
}

#define STKSIZ3 0x80
uint8_t task3_stk[STKSIZ3];

void __attribute__((OS_task)) task3(void) {
  sei();
  while (1) {
    oct_idle_task(OCT_TASK3);
    for (int i = 0; i < T3_ITER; i++) {
      PORTF.OUTCLR = PIN5_bm;
      _delay_us(2);
      PORTF.OUTSET = PIN5_bm;
      _delay_us(8);
    }
  }
}

#define STKSIZ7 0x80
uint8_t task7_stk[STKSIZ7];

void main(void) {
  nano_button_wait();			/* if non-sim, wait for button */

  oct_os_init(OCT_TASK6);
  oct_attach_task(OCT_TASK7, oct_spin, task7_stk, STKSIZ7);
  oct_attach_task(OCT_TASK3, task3, task3_stk, STKSIZ3);
  oct_attach_task(OCT_TASK2, task2, task2_stk, STKSIZ2);

  oct_wake_task(OCT_TASK3 | OCT_TASK2);	/* let tasks initialize */

  init_sys_clk();			/* init clock to 5 MHz */
  init_wdt();				/* init WDT to 8 sec */
  init_tca();				/* init TCA to 3 sec */

  PORTF.DIRSET = PIN5_bm;		/* LED output */
  PORTF.OUTSET = PIN5_bm;		/* LED off */

  sei();
  while (1) {
    wdt_reset();

    PORTF.OUTCLR = PIN5_bm;
    _delay_us(1);
    PORTF.OUTSET = PIN5_bm;
    _delay_us(20);
  }
}

OctOS consists if a single header file and a single assembly file.   Here they are:

#ifndef OCTOS_H_
#define OCTOS_H_

#define OCT_TASK0 0x01
#define OCT_TASK1 0x02
#define OCT_TASK2 0x04
#define OCT_TASK3 0x08
#define OCT_TASK4 0x10
#define OCT_TASK5 0x20
#define OCT_TASK6 0x40
#define OCT_TASK7 0x80

#ifndef __ASSEMBLER__
#include <stdint.h>

void oct_os_init(uint8_t id);
void oct_attach_task(uint8_t id, void (*fn)(void), uint8_t *sa, uint16_t sz);
void oct_detach_task(uint8_t id);

void oct_idle_task(uint8_t id_set);
void oct_wake_task(uint8_t id_set);

void oct_isr_idle_task_1(uint8_t id_set);
void oct_isr_wake_task_1(uint8_t id_set);

void oct_spin();
void oct_rest();

static inline void oct_isr_wake_task(uint8_t id_set) {
  asm volatile(" mov r24,%0\n"
	       " call oct_isr_wake_task_1\n"
	       : : "r"(id_set) : "r23", "r24", "r25");
}

static inline void oct_isr_idle_task(uint8_t id_set) {
  asm volatile(" mov r24,%0\n"
	       " call oct_idle_task_1\n"
	       : : "r"(id_set) : "r23", "r24", "r25");
}

static inline uint8_t oct_cur_task(void) {
  extern uint8_t oct_curmask;
  return (oct_curmask + 1);
}

#else /* __ASSEMBLER__ */

.global oct_init
.global oct_attach_task
.global oct_detach_task
.global oct_idle_task
.global oct_wake_task
.global oct_isr_idle_task_1
.global oct_isr_wake_task_1
.global oct_spin, oct_rest

#define oct_isr_wake_task oct_wake_task_1
#define oct_isr_idle_task oct_idle_task_1

#endif /* __ASSEMBLER__ */

#endif
#define OCT_TASK7 0x80

#define PUSH_CALLER_REGS \
	push r18 $ \
	push r19 $ \
	push r20 $ \
	push r21 $ \
	push r22 $ \
	push r23 $ \
	push r24 $ \
	push r25 $ \
	push r26 $ \
	push r27 $ \
	push r30 $ \
	push r31

#define POP_CALLER_REGS \
	pop r31 $ \
	pop r30 $ \
	pop r27 $ \
	pop r26 $ \
	pop r25 $ \
	pop r24 $ \
	pop r23 $ \
	pop r22 $ \
	pop r21 $ \
	pop r20 $ \
	pop r19 $ \
	pop r18

#define PUSH_CALLEE_REGS \
	push r0 $ \
	push r1 $ \
	push r2 $ \
	push r3 $ \
	push r4 $ \
	push r5 $ \
	push r6 $ \
	push r7 $ \
	push r8 $ \
	push r9 $ \
	push r10 $ \
	push r11 $ \
	push r12 $ \
	push r13 $ \
	push r14 $ \
	push r15 $ \
	push r16 $ \
	push r17 $ \
	push r28 $ \
	push r29

#define POP_CALLEE_REGS \
	pop r29 $ \
	pop r28 $ \
	pop r17 $ \
	pop r16 $ \
	pop r15 $ \
	pop r14 $ \
	pop r13 $ \
	pop r12 $ \
	pop r11 $ \
	pop r10 $ \
	pop r9 $ \
	pop r8 $ \
	pop r7 $ \
	pop r6 $ \
	pop r5 $ \
	pop r4 $ \
	pop r3 $ \
	pop r2 $ \
	pop r1 $ \
	pop r0

#define OCT_NSAVREG 20

	.equ SPL,0x3D
	.equ SPH,0x3E
	.equ SREG,0x3F
	.equ TCB_SIZE,2		; TCB holds stack pointer only

	;; octos functions
	.global oct_os_init
	.global oct_attach_task
	.global oct_detach_task
	.global oct_idle_task
	.global oct_wake_task
	.global oct_isr_idle_task_1
	.global oct_isr_wake_task_1
	.global oct_spin
	.global oct_rest

	.section .data
	.global oct_curmask
	.global oct_rdylist

oct_curmask: .skip 1		; current mask, e.g., 0x07 for id 0x08
oct_rdylist: .skip 1		; list of ready tasks, one per bit
do_swap: .skip 1		; task swap needed
swap_ip: .skip 1		; swap in progress
tcbtabl: .skip 8*TCB_SIZE	; TCB table (stack pointer only)

	.section .text

	;; idle tasks
	.type oct_spin,function
oct_spin:
0:	rjmp 0b

	.type oct_rest,function
oct_rest:
0:	sleep
	rjmp 0b

	;; void oct_os_init(uint8_t id)
	;; id: main task id (r24)
	.type oct_os_init,function
oct_os_init:
	push r29
	push r28
	mov r25,r24		; load task-id arg
	;; init TCB for TASK6..TASK0
	ldi r27,hi8(tcbtabl)
	ldi r26,lo8(tcbtabl)
	adiw X,7*TCB_SIZE
	ldi r24,OCT_TASK7	; r24 has loop task id
	ldi r23,OCT_TASK7-1	; r23 has loop task mask
	clr r20			; init SPH,SPL to zero
0:	lsr r23			; shift task mask
	lsr r24			; shift task id
	breq 2f			; done?
	cp r24,r25		; curr-id == caller-id ?
	brne 1f
	sts oct_curmask,r23	; log caller if so
1:	st -X,r20		; unallocated SPL
	st -X,r20		; unallocated SPH
	rjmp 0b
	;; log caller
	st -X,r20		; 0 => SPL in TCB
	st -X,r20		; 0 => SPH in TCB
	rjmp 0b
2:	;; update vars
	ldi r21,0x80
	or r21,r25		; add task-id arg
	sts oct_rdylist,r21	; updated ready list
	sts do_swap,r20		; for swap during
	sts swap_ip,r20		;   interrupts
	;; return
	pop r28
	pop r29
	ret
	.size oct_os_init, .-oct_os_init

	;; void oct_register_task(uint8_t id, void (*fn)(void),
	;;                        uint8_t *sp, uint16_t sz)
	;; id: task id (r24)
	;; fn: function (r23:r22)
	;; sp: stack memory pointer (r21:r20)
	;; sz: stack size (r19:r18)
	.type oct_attach_task,function
oct_attach_task:
	;; from task id, generate oct_curmask
	mov r25,r24
	subi r25,1
	;; load Z with TCB table end addr
	ldi r30,lo8(tcbtabl)
	ldi r31,hi8(tcbtabl)
0:	adiw Z,TCB_SIZE
	lsr r24
	brcc 0b
	;; stack top addr in r21:r20
	add r20,r18
	adc r21,r19
	;; load X with stack pointer
	movw r26,r20
	sbiw X,1
	;; save return address
	st -X,r22
	st -X,r23
	;; room for registers plus sreg
	sbiw X,OCT_NSAVREG+1
	;; save mask
	st -X,r25
	;; adjust for pop pre increment
	sbiw X,1
	;; save stack pointer to TCB
	st -Z,r27
	st -Z,r26
 	ret
	.size oct_attach_task, .-oct_attach_task

oct_detach_task:
	;; load Z with TCB table end addr
	ldi r30,lo8(tcbtabl)
	ldi r31,hi8(tcbtabl)
0:	adiw Z,TCB_SIZE
	lsr r24
	brcc 0b
	;; zero out the TCB
	st -Z,r24
	st -Z,r24
	ret
	.size oct_detach_task, .-oct_detach_task

	;; void oct_isr_wake_task_1(uint8_t id)
	;; id: OR'd ids of tasks to wake (r24)
	;; clobbers: r23,r24,r25
	.type oct_isr_wake_task_1,function
oct_isr_wake_task_1:
	in r23,SREG
	lds r25,oct_rdylist
	or r25,r24
	sts oct_rdylist,r25
	lds r24,oct_curmask
	and r25,r24
	breq 1f
	call isr_task_swap
1:	out SREG,r23
	ret
	.size oct_isr_wake_task_1, .-oct_isr_wake_task_1

	;; oct_isr_idle_task_1(uint8_t id)
	;; id: task ID to idle (r24)
	;; clobbers: r23,r24,r25
	.type oct_isr_idle_task_1,function
oct_isr_idle_task_1:
	in r23,SREG
	andi r24,~OCT_TASK7	; never idle task7
	com r24			; ones complement
	lds r25,oct_rdylist
	and r25,r24
	sts oct_rdylist,r25
	lds r24,oct_curmask
	rjmp isr_task_swap
	.size oct_isr_idle_task_1, .-oct_isr_idle_task_1

	.type isr_task_swap,function
isr_task_swap:
	PUSH_CALLER_REGS
	rcall task_swap
	POP_CALLER_REGS
	ret

	;; void oct_wake_task(uint8_t id_set)
	;; id: or'd ids of tasks to wake (r24)
	.type oct_wake_task,function
oct_wake_task:
	in r23,SREG
	cli
	lds r25,oct_rdylist
	or r25,r24
	sts oct_rdylist,r25
	lds r24,oct_curmask
	and r25,r24
	brne task_swap
	out SREG,r23
	ret
	.size oct_wake_task, .-oct_wake_task

	;; oct_idle_task(uint8_t id)
	;; id: task ID to idle (r24)
	.type oct_idle_task,function
oct_idle_task:
	in r23,SREG
	andi r24,~OCT_TASK7	; never idle task7
	com r24			; ones complement
	cli
	lds r25,oct_rdylist
	and r25,r24
	sts oct_rdylist,r25
	lds r24,oct_curmask
	rjmp task_swap
	.size oct_idle_task, .-oct_idle_task

	;; void task_swap(void)
	;; Get here via jmp from wake_task or idle_task only, with:
	;; 1) interrupts disabled
	;; 2) SREG in r23
	;; 3) curmask in r24
	;; TODO: FIXME? should keep curmask and SP consistent : SPIN check
	.type task_swap,function
task_swap:
	;; if running, flag and return
	lds r25,swap_ip
	tst r25
	breq swap_body
	sts do_swap,r25		; flag to re-run
	out SREG,r23
	ret
swap_body:
	ldi r25,1
	sts swap_ip,r25
	;; interrupts enabled in this section
	sei
	push r23		; push SREG
	PUSH_CALLEE_REGS	; save registers
	push r24		; push curmask
	;; incoming TCB addr => Y (TODO: if Y is zero, abort)
	lds r25,oct_rdylist
	ldi r28,lo8(tcbtabl)
	ldi r29,hi8(tcbtabl)
0:	lsr r25
 	brcs 1f
	adiw Y,TCB_SIZE
	rjmp 0b
1:	ld r22,Y+		; load incoming SPL from TCB
	ld r23,Y+		; load incoming SPH from TCB
	;; outgoing TCB addr => Y
	ldi r28,lo8(tcbtabl)
	ldi r29,hi8(tcbtabl)
0:	lsr r24
	brcc 1f
	adiw Y,TCB_SIZE
	rjmp 0b
	;; swap stack pointers
1:	cli
	in r24,SPL
	in r25,SPH
	out SPL,r22
	out SPH,r23
	sei
	st Y+,r24		; save outgoing SPL to TCB
	st Y+,r25		; save outgoing SPH to TCB
	pop r24			; load curmask
	sts oct_curmask,r24	;   and restore
	POP_CALLEE_REGS		; restore registers
	pop r23			; load SREG, restore later
	;; trailer
	cli
	lds r25,do_swap
	tst r25
	breq 1f
	;;
	;; From SPIN testing: (otherwise infinite loop IIRC)
	;; 1) done if (rdylist & curmask) == 0
	lds r25,oct_rdylist
	and r25,r24
	breq 1f
	;; 2) done if rdylist & (curmask+1) != 0
	lds r25,oct_rdylist
	inc r24
	and r25,r24
	brne 1f
	;;
	;; rerun task swap
	sts do_swap,r1
	rjmp swap_body		; redo w/ r23=SREG, r24=curmask
1:	;; done
	sts swap_ip,r1		; r1 == 0 here
	out SREG,r23		; restore SREG
	ret
	.size task_swap, .-task_swap

 

Last Edited: Sat. Apr 3, 2021 - 02:21 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I'm sure I don't know completely what is going on in your code, but this may be something you need to keep in mind since you have stacks/preemption- you can sei all you want inside an isr (for an avr0/1/Dx), but it is meaningless since these have a LVLnEX bit. Once inside an isr, the LVL0EX bit is set (and hardware does not touch sreg I, as it needs to allow preemption by a LVL1 irq), and as long as a LVLnEX bit is set you cannot get any other irq to fire of the same level (can ignore LVL1 for now, which for a 4809 may have a problem/errata). The only way to clear a LVLnEX bit is with the 'reti' instruction, and since I see no reti in your code, I assume this is not handled so I'm not sure how you can get to preemption.

 

To allow for a sei replacement inside an isr, the simplest solution is to just create a reti function (in asm, or a naked c function with asm("reti"); ), and to use it just call it which will then clear the LVLnEX bit (and sreg I is set, which it most likely will be already). Any other reti that normally occurs later with no LVLnEX bit set will be harmless, although may still require some thought to make sure that is always the case.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

curtvm,   Noted in github issue #1.   Thanks.