Need Help with AVR-based UV Exposure Unit

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

Right now, I'm building a UV Exposure Unit using ATTiny2313 on 16MHz crystal. The exposure unit is meant to have a timer edit feature that uses a rotary encoder, and when in expose mode, it will turn off the uv light automatically after the timer runs out. However, I'm stuck with a timer problem when turning on the UV. The logic for the timer causes interrupts for rotary encoder and other buttons to break. Not all input get registered by the CPU. So, if I edit the clock using the rotary encoder, the rotation doesn't increase nor decrease the timer value responsively, only occasionally.

 

I know that the runtime() function is causing the problem. It behaves like millis() in arduino, disabling interrupt before putting out the value. However, if I don't use this function (e.g. using sys_runtime variable directly), the timer will skip a second or two randomly, that it ends up turning off the UV light earlier by 10 seconds or more. I think it is caused by the interrupt changes the value of sys_runtime when last_tick is being assigned with sys_runtime that somehow, causes the assignment to skip.

 

I suspect there's other way to handle the 1ms tick of the timer more effectively, but I just couldn't think other way. Any help is very appreciated.

 

Below is the code I write:

 

/*
* P17006-UV Exposure Unit.c
*
* Created: 18/10/2017 07:07:14
* Author : Feryanlie
*
* Microcontroller	: ATTiny2313A
* Clock Frequency	: 16MHz
* Clock Divider		: 1
* Clock Source		: Crystal
* Pin Configuration:
*	- Input
*		1. START	: PB3
*		2. ROT_A	: PB0
*		3. ROT_B	: PB1
*		4. ROT_SW	: PB2
*	- Output
*		1. BUZZER	: PD2
*		2. UV_LIGHT	: PD3
*		3. SEG7_CLK	: PD4
*		4. SEG7_DIO	: PD5
*
* Input Logic: Active LOW
*/

//---------MACRO DEFINITIONS----------
#define F_CPU 16000000UL

#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdbool.h>
#include <util/delay.h>
#include <string.h>

#define START PB3
#define ROT_A PB0
#define ROT_B PB1
#define ROT_SW PB2
#define BUZZER PD2
#define UV_LIGHT PD3
#define SEG7_CLK PD4
#define SEG7_DIO PD5

#define TM_COLON 0x80
#define TM_START_ADDR 0xC0
//------END OF MACRO DEFINITIONS-------

//---------GLOBAL VARIABLES------------------------
bool rot_left, rot_right, rot_enabled, rot_pressed, rot_sw_enabled, start_pressed, start_enabled; //For marking interrupt results, and current system mode.
uint8_t visr_laststate;//Global variable for ISR to record the last state of PINB
unsigned long sys_runtime; //Variable to record how many time has passed since the system starts. It counts in 1ms tick.
//--------END OF GLOBAL VARIABLES-------------------

//---------------GENERAL FUNCTIONS------------------------

/* Function to produce beep with several defined modes
* Modes:
*	0->Single Short Beep
*	1->Single Long Beep
*	2->Double Short Beeps
*	3->Double Long Beeps
*	4->1 Short Beep and 1 Long Beep
*	5->1 Long Beep and 1 Short Beep
* If calling wrong mode, it automatically defaults to Mode 0 (single short beep).
*/
void beep(uint8_t mode)
{
	switch(mode)
	{
		case 0:
			PORTD |= (1<<BUZZER);
			_delay_ms(50);
			PORTD &= ~(1<<BUZZER);
			_delay_ms(50);
			break;
		case 1:
			PORTD |= (1<<BUZZER);
			_delay_ms(200);
			PORTD &= ~(1<<BUZZER);
			_delay_ms(50);
			break;
		case 2:
			beep(0);
			beep(0);
			break;
		case 3:
			beep(1);
			beep(1);
			break;
		case 4:
			beep(0);
			beep(1);
			break;
		case 5:
			beep(1);
			beep(0);
			break;
		default:
			beep(0);
	}
}

//Converting from display digits to integer value to be used in the timer function (measured in seconds)
int convDigitsInt (char* digits)
{
	int val = (digits[0]-'0') * 60 * 10 + (digits[1]-'0') * 60 + (digits[2]-'0') * 10 + (digits[3]-'0');
	return val;
}

//Converting from timer seconds to display digits
void convIntDigits(int time_value, char* digits)
{
	digits[0] = (time_value / 600) + '0';
	digits[1] = ((time_value / 60) % 10) +'0';
	digits[2] = ((time_value % 60) / 10) + '0';
	digits[3] = ((time_value % 60) % 10) + '0';
}

//Get the runtime since system start
unsigned long runtime()
{
	uint8_t oldSREG = SREG;
	unsigned long m;

	cli(); //Disabling interrupt
	m = sys_runtime;
	SREG = oldSREG; //Enabling interrupt

	return m;
}

//---------------TM1637 INTERFACE FUNCTIONS----------------
//The starting bit for communication with TM1637
void tm_start()
{
	PORTD |= (1<<SEG7_DIO)|(1<<SEG7_CLK);
	_delay_us(2);
	PORTD &= ~(1<<SEG7_DIO);
	_delay_us(2);
}

//The ending bit for communication with TM1637
void tm_stop()
{
	PORTD &= ~((1<<SEG7_DIO)|(1<<SEG7_CLK));
	_delay_us(2);
	PORTD |= (1<<SEG7_CLK);
	_delay_us(2);
	PORTD |= (1<<SEG7_DIO);
	_delay_us(2);
}

//Sending a byte of data to TM1637
void tm_wrdata(uint8_t data)
{
	for(int i = 0; i < 8; i++)
	{
		PORTD &= ~(1<<SEG7_CLK);
		_delay_us(1);
		if(data & 0x01)
		{
			PORTD |= (1<<SEG7_DIO);
		}
		else 
		{
			PORTD &= ~(1<<SEG7_DIO);
		}
		_delay_us(1);
		PORTD |= (1<<SEG7_CLK);
		_delay_us(2);
		data >>=1;
	}
}

//Waiting for a respond from TM1637
void tm_ack()
{
	PORTD &= ~(1<<SEG7_CLK);
	DDRD &= ~(1<<SEG7_DIO);
	_delay_us(2);
	while(PINB & (1<<SEG7_DIO));
	_delay_us(2);
	PORTD |= (1<<SEG7_CLK);
	PORTD &= ~(1<<SEG7_DIO);
	DDRD |= (1<<SEG7_DIO);
	_delay_us(2);
}

//Constants of digits in TM1637
const static char tm_digits[10] = 
{
	0x3f, 0x06, 0x5b, 0x4f, 0x66,
	0x6d, 0x7d, 0x07, 0x7f, 0x6f
};

//Convert from an ordinary character to TM1637 compatible digits
static char tm_convertChar(char ch, bool colon)
{
	char val = 0;
	if(((uint8_t)'0'<= ch) && ((uint8_t)'9' >= ch))
		val = tm_digits[(ch-'0')];

	if(colon) val |= TM_COLON;

	return val;
}

//Set the TM1637 to write the display either in Fixed Address Mode or Auto Increment Mode
//If fixed_addr is true, the TM1637 display is in Fixed Address Mode, else in Auto Increment Mode
void tm_settings(bool fixed_addr)
{
	uint8_t settings = 0x40;
	if (fixed_addr) settings |= 4;

	tm_start();
	tm_wrdata(settings);
	tm_ack();
	tm_stop();
}

//Command for configuring TM1637 display: turning on or off, and the intensity of the display
void tm_display(bool displayon, uint8_t intensity)
{
	uint8_t settings = 0x80|(displayon<<3)|intensity;

	tm_start();
	tm_wrdata(settings);
	tm_ack();
	tm_stop();
}

//Set the whole 4 display digits in Auto Increment Mode
//IMPORTANT: Make sure the display is in Auto Increment Mode first before calling the function!
void tm_writeDigits(char* digits, bool colon)
{
	tm_start();
	tm_wrdata(TM_START_ADDR); //Specify the starting digit
	tm_ack();
	for(int i = 0; i < strlen(digits); i++)
	{
		tm_wrdata(tm_convertChar(digits[i],colon));
		tm_ack();
	}
	tm_stop();
}

//Set individual digits of the display in Fixed Address Mode
//IMPORTANT: Make sure the display is in Fixed Address Mode first before calling the function!
//Tips: to turn off individual digit, use any non-numerical characters, or a byte that isn't equal to the ASCII code of numerical characters
void tm_setdigit(char digit, uint8_t digitnum, bool colon)
{
	uint8_t targetaddr = TM_START_ADDR + (digitnum-1);
	tm_start();
	tm_wrdata(targetaddr);
	tm_ack();
	tm_wrdata(tm_convertChar(digit,colon));
	tm_ack();
	tm_stop();
}
//---------------END OF TM1637 INTERFACE FUNCTIONS------------------
//---------------END OF GENERAL FUNCTIONS---------------------------

//------------------INTERRUPT---------------------------------------
//Interrupt to handle rotary encoder input
ISR(PCINT_vect)
{	
	//Get current state and find which pin changes state
	uint8_t visr_curstate = PINB;
	uint8_t visr_changed_pin = visr_curstate ^ visr_laststate;
	
	//Check if rotary encoder switch pin changes state
	if((visr_changed_pin & (1<<ROT_SW))  && rot_sw_enabled)
	{
		//Check if it is a rising edge
		if(visr_curstate & (1<<ROT_SW))
			rot_pressed = true;
	}
	
	//Check if start button changes state
	if((visr_changed_pin & (1<<START))  && start_enabled)
	{
		//Check if it is a rising edge
		if((visr_curstate & (1<<START)))
			start_pressed = true;
	}
	
	//Check if rotary encoder rotation pins (ROT_A and ROT_B) change state
	if((visr_changed_pin & ((1<<ROT_A)|(1<<ROT_B))) && rot_enabled)
	{
		//If current state of rotary encoder is the starting state (0x03) and
		//last state is 0x02 then it rotates to the right.
		//But, if the last state is 0x01, then it rotates to the left.
		//Else, if current state is not the starting state, then copy current state to the last state.
		uint8_t visr_rot_curstate = visr_curstate & ((1<<ROT_A)|(1<<ROT_B));
		uint8_t visr_rot_laststate = visr_laststate & ((1<<ROT_A)|(1<<ROT_B));
		if(visr_rot_curstate == ((1<<ROT_A)|(1<<ROT_B)))
		{
			if(visr_rot_laststate == ((1<<ROT_B)|(0<<ROT_A)))
				rot_right = true;
			else if(visr_rot_laststate == ((1<<ROT_A)|(0<<ROT_B)))
				rot_left = true;
		}	
	}
	visr_laststate = visr_curstate;			
}

//Interrupt to handle timer compare match (CTC)
//Tick every 1ms
ISR(TIMER0_COMPA_vect)
{
	sys_runtime++;
	PORTD ^= (1<<PD6); //DEBUG
}
//-------------------------END OF INTERRUPT-----------------------------

//--------------------------MAIN PROGRAM--------------------------------
int main(void)
{
	//Set input output
	DDRB &= ~((1<<ROT_A)|(1<<ROT_B)|(1<<ROT_SW)|(1<<START));
	DDRD |= (1<<PD6)|(1<<BUZZER)|(1<<UV_LIGHT)|(1<<SEG7_CLK)|(1<<SEG7_DIO); //DEBUG
	//DDRD |= (1<<BUZZER)|(1<<UV_LIGHT)|(1<<SEG7_CLK)|(1<<SEG7_DIO);
	PORTB = 0;
	PORTD = (1<<SEG7_CLK)|(1<<SEG7_DIO);//Defaulting PORTD state
	GIMSK |= (1<<PCIE); //Enabling pin change interrupt
	PCMSK |= (1<<PCINT0)|(1<<PCINT1)|(1<<PCINT2)|(1<<PCINT3); //Enabling pin change interrupt for the rotary encoder and its switch
	TCCR0A = (1<<WGM01);//Setting up timer in CTC mode
	TCCR0B = (1<<CS01)|(1<<CS00); //Set up timer prescaler: 64
	OCR0A = 249; //The value comes from this formula: freq = sys_clock / (timer_prescaler*(1+OCR0A)).
	TIMSK |= (1<<OCIE0A);//Enabling timer interrupt
	
	//Initializing the rotary encoder, switch, and system mode state
	rot_left = false;
	rot_right = false;
	rot_enabled = false;
	rot_sw_enabled = true;
	rot_pressed = false;
	start_pressed = false;
	start_enabled = true;
	visr_laststate = PINB;
	
	//Initializing system run time
	sys_runtime = 0;

	//Set blinking parameter and initial timer value
	char* seg7_digits = "0200";
	bool edit_mode = false;
	bool expose_mode = false;
	const long seg7_blinkinterval = 250; //250ms
	const long uv_interval = 1000; //1s
	bool seg7_blink_state = false;
	uint8_t seg7_seldigit = 1;
	int timer_set = 120, timer_run = timer_set;
	unsigned long last_blink = sys_runtime, last_tick = sys_runtime;

	//Initializing the display digits
	tm_settings(false);
	tm_writeDigits(seg7_digits,true);
	tm_display(true,7);

	sei(); //Enabling global interrupt

	beep(2); //2 short beeps indicating system is ready
	while (1)
	{
		if(edit_mode)
		{
			//Blinking the selected digit according to blink interval
			if((runtime()-last_blink)>= seg7_blinkinterval)
			{
				last_blink = runtime();
				if(seg7_blink_state)
					tm_setdigit(0,seg7_seldigit,true);
				else
					tm_setdigit(seg7_digits[seg7_seldigit-1],seg7_seldigit,true);
				seg7_blink_state = !seg7_blink_state;
			}
			
			//If rotary encoder is rotated to the right, increase the number
			if(rot_right)
			{
				if(seg7_digits[seg7_seldigit-1] == '9')
					seg7_digits[seg7_seldigit-1] = '0';
				else
					seg7_digits[seg7_seldigit-1] +=1;
				beep(0);
				rot_right = false;
			}
			
			//If rotary encoder is rotated to the left, decrease the number
			if(rot_left)
			{
				if(seg7_digits[seg7_seldigit-1] == '0')
					seg7_digits[seg7_seldigit-1] = '9';
				else
					seg7_digits[seg7_seldigit-1] -=1;
				beep(0);
				rot_left = false;
			}
			
			//If rotary encoder switch is pressed, check if it is the last digit.
			//If it is, then validate the entered number, then quit edit mode
			//Else, move the selected digit to the next digit
			if(rot_pressed)
			{
				tm_setdigit(seg7_digits[seg7_seldigit-1],seg7_seldigit,true);
				seg7_blink_state = true;
				if(seg7_seldigit == 4)
				{
					edit_mode = false;
					seg7_seldigit = 1;
					rot_enabled = false;
					start_enabled = true;
					timer_set = convDigitsInt(seg7_digits); //Convert input to seconds
					convIntDigits(timer_set,seg7_digits); //Correct display numbers (ensuring the seconds to less than 60)
					tm_settings(false);
					tm_writeDigits(seg7_digits,true);
					tm_display(false,7); //Blink the display once
					_delay_ms(200);
					tm_display(true,7);
					beep(2);
				}
				else
					seg7_seldigit++;
				beep(0);
				rot_pressed = false;
			}
		}
		else
		{			
			if(expose_mode)
			{
				//Update the TM1637 display to reflect current timer value every 1s
				if((runtime() - last_tick) >= uv_interval)
				{
					last_tick = runtime();
					timer_run--;
					convIntDigits(timer_run,seg7_digits);
					tm_writeDigits(seg7_digits,true);
				}

				//If timer runs out or start button is pressed, then turn off the light and quit expose mode
				if(timer_run == 0 || start_pressed)
				{
					PORTD &= ~(1<<UV_LIGHT);
					expose_mode = false;
					rot_sw_enabled = true;
					beep(3);					
					tm_settings(false);
					if(timer_run == 0) //Give blinking effect when timer runs out
					{
						tm_writeDigits("0000",true);
						_delay_ms(200);
						tm_display(false,7);//Blink the display once
						_delay_ms(200);
						tm_display(true,7);
						_delay_ms(200);
					}
					convIntDigits(timer_set,seg7_digits); //Write the display back to the original value
					tm_writeDigits(seg7_digits,true);
					start_pressed = false;
				}
			}
			else
			{
				//If rotary switch is pressed, enter edit mode
				if(rot_pressed)
				{
					edit_mode = true;
					rot_enabled = true;
					beep(0);
					rot_pressed = false;
					start_enabled = false;
					tm_settings(true);
				}
				//If start button is pressed, enter expose mode
				if(start_pressed)
				{
					tm_settings(false);
					timer_run = timer_set;
					expose_mode = true;
					start_pressed = false;
					rot_sw_enabled = false;
					PORTD |= (1<<UV_LIGHT);
					last_tick = sys_runtime;
					beep(1);
				}	
			}
		}
	}
}
//---------------------END OF MAIN PROGRAM-----------------------------

 

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

You really shouldn’t use interrupts for your encoder or switch. You can simply poll and debounce these in your 1ms isr.
I can’t make much sense of your code as i’m reading it on a mobile, but it does look a bit complex.
Your handling of the system timer looks reasonable, so i wouldn’t be putting the blame there.

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

I highly recommend https://www.avrprogrammers.com/howto/quad-encoder as a read on how to improve your handling of the rotary encoders.

I used the pin change interrupt one together with 2 geared motors, worked really well after struggling for some time getting accurate handling of the motors.

If you add more encoders you will have to shift the next encoders down to bit 1,0 for the state table to work.

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

There are encoders and there are encoders! A manual encoder (as in the twisty knob type) are unlikely to generate 1000 pulses per second, thus interrupts are unnecessary and add to the unreliabilty of the system.

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

Kartman wrote:
There are encoders and there are encoders! A manual encoder (as in the twisty knob type) are unlikely to generate 1000 pulses per second, thus interrupts are unnecessary and add to the unreliabilty of the system.

 

While I agree to a certain degree to what you are saying, I can't see where I told him explicitly to use interrupts. Maybe I should have said "take a look at the state table" instead.

 

Either way, using a working example to get something up and running before rolling their own is IMO a lot easier.

I generally use pin change interrupts when using rotary encoders for human input also, I think its easier, and snappier.

When using cheap rotary encoders from eBay, like I do, they always work best for me that way.

 

Can't say I ever have seen unreliability because of using interrupts on the rotary encoders, but that's not to say it cannot happen.

 

Would you say using INT0,INT1 is worse than using pin change interrupts, or are they equally bad?

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

You mentioned using pcints. Use interrupts when you need microsecond response. As well, when using external interrupts, be they pcints or int0,1, you want to ensure that you limit the rate of potential interrupts otherwise your system can be crippled if it is trying to service too many interrupts per second. Pushbuttons bounce - so you get a burst of fast interrupts. Not really what we want.

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

Kartman wrote:

You mentioned using pcints. Use interrupts when you need microsecond response. As well, when using external interrupts, be they pcints or int0,1, you want to ensure that you limit the rate of potential interrupts otherwise your system can be crippled if it is trying to service too many interrupts per second. Pushbuttons bounce - so you get a burst of fast interrupts. Not really what we want.

 

Yes, I mentioned I used the pcint code on two geared motors.

Then again, a rotary encoder isn't really comparable to a pushbutton is it? I understand the button bounce, but I have never encountered anything like it on a rotary encoder.

Of course I don't have the button on an interrupt pin, but really, what is the problem having the phases on interrupt pins together with appropriate pullup?

 

I used pcint together with rotary encoders in a very noisy environment, fluorescents, HPS ballasts, contactors etc.

Never did I see anything that makes me rethink my way of doing this, so can you please be more specific to what can actually cause these problems?

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

You seem to be confusing encoders! The twisty knob type are usually mechanical contacts - so they bounce. The encoder you’re talking about is probably optical. Two very different beasts. Nevertheless, what happens if your encoder rotates too fast? Does your processing grind to a halt?

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

Kartman wrote:

 Pushbuttons bounce - so you get a burst of fast interrupts. Not really what we want.

That is why you need to do debouncing in the end. so after the first interrupt there is a "dead" time before the next interrupt can occur on that pin. (in case of switches/buttons....)

Do not do that and you may get very interesting button response indeed.