COrnell Miniature ATmel Operating System (COMATOS)

Tutorial: A Digital Capacitance Meter


COMATOS has a lot of small details, making it difficult to absorb completely by simply reading the documentation. Perhaps the most useful illustration would be through example.

The system developed in this example is a digital capacitance meter (DCM). It is capable of reading capacitances in the range 1nF~200nF. A measurement is initiated whenever a key is pressed on the keyboard, and the output is sent to the PC through the UART. The circuit used is shown in figure 1. The capacitor to be measured is place in an RC circuit. When the voltage on the capacitor reaches

the analog comparator triggers an interrupt. The analog comparator is hooked to the input capture function of Timer1. The time needed to charge the capacitor is stored in the Timer 1 Capture Register. Since the charging of the capacitor follows the equation
it takes t=RC seconds to trigger the interrupt. At R=16k and a clock divider value of 64, one clock tick represents one nF.

Figure 1: Capacitance Meter Circuit


This COMATOS implementation of the DCM is divided into three tasks:

  1. Keyboard Task: This task runs every 30 ms. It contains a state machine which debounces key presses. When a new key is pressed, it sends a message to the measurement task.
  2. Measurement Task: Upon receiving a message from the keyboard task, this process begins by setting PortB pin 2 to 0 to discharge the capacitor. It runs 25 ms later to begin charging. It runs again when the input capture ISR signals that the capacitance value is ready. The measurement task then messages the display task with the capacitance value.
  3. Display Task: This task formats the capacitance sent by the measurement task and passes the result to the UART.

Constants, Includes, and Vector Tables

The code file begins with the required include directives for the OS, the interrupt vector table, and constant definitions for the keyboard scan table and output message. Note the inclusion of the required include files:

.include ""
; Contains the macro definitions used in the OS, must always be included
.include ""

; Task Number Definitions
.equ	KbdTask=0	
.equ	MeasureTask=1
.equ 	DisplayTask=2
.equ	CapIsr=3

; Measurement State Definitions
.equ 	keyWait 	=0
.equ	dischargeWait	=1
.equ	measureWait	=2

.def	key	=r17	;holds raw press value
.def	butnum	=r18	;final press value
.def	state	=r19	;debounce state
.def	lastKey	=r20	;last keypress
.def	Temp	=r21	;temporary register

.def	capacit =r19	;capacitance value

.equ	waitState=0
.equ	gotKeyState=1

.equ	TIMSKVal=0x08 ; Only use input capture
.equ	TCCR1BVal=0b01000011 ; Capture on rising edge, 64x clock div
.equ	ACSRVal=0b00000111 ; Interrupt enable, rising edge, input capture

.org $0000
	rjmp 	RESET	;reset entry vector
	rjmp	T1CapISR

; Contains the definitions of the subroutines used within the OS, 
; including the scheduler, timer 0 ISR, the OS call functions.
; Always included here
.include "" 

;Keyboard Scan Table
keytbl: .db 0b11101110, 0b11101101, 0b11101011, 0b11100111
	.db 0b11011110, 0b11011101, 0b11011011, 0b11010111
	.db 0b10111110, 0b10111101, 0b10111011, 0b10110111
	.db 0b01111110, 0b01111101, 0b01111011, 0b01110111

capMessage: .db "The capacitance in nF is:"


As usual, execution begins at the reset interrupt vector. After the stack point is initialized, OSInit must be called. In this case, the timeout counter is set to 30ms, which is the interval between executions of the keyboard task. Next, the relevent I/O registers are initialized to set up input capture, a 64x clock divider on timer 1, and the input/output settings on Port B. Following I/O port initialization, we create the three tasks with OSCreateTask as described in table 1. Note also that instead of passsing absolute addresses for the entry point of each task, we pass labels.

Table 1: Task Creation Settings
TaskTimeout ConditionMessage Mask
Keyboard30 - Task runs every 30 ms0xff - Not using messaging
Measurement0 - Not using timeout0x01 - Initially waiting on task 0 (keyboard)
Display0 - Not using timeout0x02 - Waiting on the measurement task
	ldi	r20, LOW(RAMEND) ;setup stack pointer
	out 	SPL, r20
	ldi	r20, HIGH(RAMEND)
	out	SPH, r20

	OSInit	30, OSDebug		; Specifies 30 ms timeout and debugging features
	; Setup timer 1
	ldi	r16, (TIMSKVal | TimerMask)
	out	TIMSK, r16
	; Setup analog comparator
	ldi	r16, ACSRVal
	out	ACSR, r16

	; Setup PortB for analog comparator
	ldi	r16, 0b11110011 ; Make most pins output except comparator
	out	DDRB, r16
	clr	r16
	out	PORTB, r16	; Start discharging
	OSCreateTask KbdTaskF, 30, 0xff	; Can't run unless timeout since
						; mask is 0xff
	OSCreateTask MeasureTaskF, 0x00, 0x01	; Initially can't run unless
						; it gets a message from the keyboard

	OSCreateTask DisplayTaskF, 0x00, 0x02   ; No timeout, wait for message from measurement
	OSStart					; Run scheduler
halt:	rjmp halt				; Not really necessary

Keyboard Task

The keyboard task acts as a state machine. During each run (every 30 ms) it uses OSGetState to determine what state it was in before. The task also stores the last key pressed in its own mailbox by sending a message to itself. The state machine works as follows:

; Pulls a key from the keyboard, debounces it, and sends it to the measurement
; task
	OSGetState state	; get the current state

	; Get the key press
	ldi	temp, 0x0f	;set lower four lines to output
	out 	DDRC, temp
	ldi	temp, 0xf0	;and turn on the pullups on the inputs
	out	PORTC, temp
	nop			;Need some time for the pullups to
	nop			;charge the port pins
	in	temp, PINC	;read the high nibble
	mov	key, temp	;and store it (with zeros in the low nibble)

 	ldi	temp, 0xf0	;set upper four lines to outputs
	out 	DDRC, temp
	ldi	temp, 0x0f	;and turn on pullups on the inputs
	out	PORTC, temp
	nop			;As before wait for the pin to charge
	in	temp, PINC	;read the low nibble
	or	key, temp	;combine to make key code
	;At the point the raw key code should have exactly one zero each in 
	;the lower and upper nibbles. Any other number of zeros indicates
	;either no-button pressed or multiple-button pressed.

	;Now search the table for a match to the raw key code
	;and exit with a button number
	ldi	ZL, low(keytbl*2)	;table pointer in FLASH
	ldi	ZH, high(keytbl*2)	;so convert from word to byte addr
	ldi	butnum, 0

tbllp: 	lpm			;get the table entry
	cp	key, r0		;match?
	breq	foundit
	inc	butnum		;if not, have we exhaused the
	cpi	butnum, 0x10	;table
	breq	illegal
	adiw	ZL, 1		;if not, get the next table entry
	rjmp	tbllp

foundit:subi	butnum, -1	;add one for display
	rjmp	TT1_statematch
illegal:clr	butnum

	OSGetMess	lastKey, 0	;Last key press is stored in our own mailbox
	tst	state			;Check the state
	breq	TT1_state_0
	OSSetState 0		; Return state to zero
	cp	lastKey, butnum	; Compare the key presses
	breq	TT1_debounce	; Key is debounced
	rjmp	TT1_false	; False alarm

	tst	butnum
	breq	TT1_release
	OSSendMess butnum, measureTask; Send the key press to the measurement task
	OSReturn 0

	cp	lastKey, butnum	; Compare the key presses
	brne	TT1_newKey
	OSReturn 0
	OSSetState gotKeyState ; New key press
	OSSendMess butnum, 0	; Store the last button press
	OSReturn 0
	OSSetState 0
	OSReturn 0

Measurement Task

This task is the meat of the DCM. It acts as a state machine as well, but it uses a different approach from the keyboard task. Instead of storing a state variable using OSSetState/OSGetState, it uses OSSetEntryPt to redefine where execution begins with each successive run. The states are defined as follows:

	; If we did not get a message from the keyboard
	; something is wrong
	OSGetMessageSrc Temp	; Get the list of who sent us messages
	andi	Temp, exp2(kbdTask)
	brne	MT_startDischarge
	OSReturn 1		;   the keyboard
	; If we're here, the keyboard sent a message
	sbi	DDRB, 2		; Discharge through analog comp 
	cbi	PORTB,2		
	ldi	Temp,25
	OSSetTimeout Temp	; Wait 25 ms
	OSSetEntryPt MT_startMeasure  ; Next time we start, we want to pick
	OSReturn 0		      ;  up at the start measurement point

	cbi	DDRB, 2		; Make pin 2 an input again

	clr	Temp
	out	TCNT1H, Temp	; Clear timer 1
	out	TCNT1L, Temp	

	sbi	PORTB, 0	; Raise Vcc to start charging  

	ldi	Temp, TCCR1BVal ; Start the timer waiting for charge
	out	TCCR1B, Temp

	OSSetMessMask exp2(CapISR)	; Now we wait on the completion of charging
	clr	Temp
	OSSetTimeout Temp
	OSSetEntryPt	MT_finishMeasure  ; Next time we start, we will be at
	OSReturn 0			  ; the measurement handling code
	;If we're here, the correct value is in the capture register
	OSGetMessageSrc Temp 	; Get the list of who sent us messages
	andi	Temp, exp2(capISR) ; If the capture ISR did not send it
	brne	MT_fM1
	OSReturn 1		; something's wrong, so return bad error code
	clr	Temp
	out	TCCR1B, Temp	; Shut off timer 1
	cbi	PORTB, 0	; Drop Vcc to discharge capacitor
	in	Temp, ICR1L ; Grab the capacitance value

	OSSendMess Temp, DisplayTask ; Send the value off to be displayed
	OSSetMessMask exp2(KbdTask) ; Wait for the next key press
	OSSetEntryPt MeasureTaskF   ; Start from the beginning next time	
	OSReturn 0		; Done
Display Task

The job of this task is to send the capacitance message to the UART. First, it copies the message from the program memory to the SRAM, since that is where OSUARTWait needs it to be. It sets the character count and begins transmission. The final step is to format the value of the capacitance as ASCII characters instead of a hexadecimal number, and then display that to the UART.

	; If we're here, there's a capacitance to print
	ldi	ZL, low(capMessage*2)	; Point to the first character
	ldi	ZH, high(capMessage*2)  ; of the output string
	ldi	Temp, 26
	sts	OSBase+OSUARTCharCnt, Temp	; Tell the UART to 26 characters

	ldi	YL, low(USRBase)		; Point at the beginning of memory
	ldi	YH, high(USRBase)

DT_0:	; Copy from program memory to SRAM
	adiw	ZL,1	
	st	Y+,r0
	dec	Temp
	brne	DT_0
	; Aim Z at the output string
	ldi	ZL, low(USRBase)
	ldi	ZH, high(USRBase)
	lds	Temp, OSBase+OSStatus
	cbr	Temp, exp2(txdone)	; Clear the bit indicating txdone
	sts	OSBase+OSStatus, Temp

	ld	Temp, Z+	; Put the first character in Temp
	OSUARTWait Temp		; Send the message

	; Now send the capacitance value itself
	OSGetMess Capacit, MeasureTask

	; Format the string
	ldi	ZL, low(USRBase)
	ldi	ZH, high(USRBase)
	clr	Temp
DT_1:	cpi	Capacit, 100
	brlo	DT_tens
	subi	Capacit, 100
	inc	Temp
	rjmp	DT_1
	subi	Temp, -ascii0
	st	Z+, Temp
	clr	Temp
	cpi	Capacit, 10
	brlo	DT_ones
	subi	Capacit, 10
	inc	Temp
	rjmp	DT_2
	subi	Temp, -ascii0
	st	Z+, Temp
	subi	Capacit, -ascii0
	st	Z+, Capacit
	ldi	Temp, 0x0d	; Newline character
	st	Z, Temp
; We've done the conversion, now send the value
	ldi	ZH, high(USRBase)
	ldi	ZL, low(USRBase)

	ldi	Temp, 4
	sts	OSBase+OSUARTCharCnt, Temp	; Tell the UART to 3 characters
	ld	Temp, Z+	; Put the first character in Temp
	OSUARTWait Temp

	OSReturn 0			
Capture ISR

Like any ISR, the capture ISR is intentionally kept short. It simply sends a message to the measurement task using ISRSendMess. This message does not contain any data and simply makes the measurement task eligible to run again.

; Analog comparator capture complete
; Capacitance in nF is in capture low register
	push	Temp
	push	Capacit
	in	Temp, SREG
	push	Temp
	ISRSendMess Temp, measureTask, CapISR ; Send a message to the measurement
					      ; task signalling the available value
	pop	Temp
	out	SREG, Temp
	pop	Capacit
	pop	Temp


This example leaves some room for optimization. For example, the ISR could send a message directly to the display task, completely eliminating the measurement task from the last step. Likewise, using OSSetEntryPt to develop state machines tends to involve simpler logic than using OSSetState/OSGetState. Please feel free to experiment with this example, which can be found in the source code section of this document.