LED controller for aircraft model

Some time ago, I came across an article on how to make gates for an aircraft model . Actually, I didn’t really want to get involved in electronics, but for some reason I was hooked by the idea of ​​putting different blinkers on the model. Some do not see the point - it is better to cling to the model with LED strips from top to bottom, and it is beautiful and visible from afar. But I prefer copy models, which means that all bulbs, strobes, headlights and other lights should turn on and off the same way as the original.

For a number of reasons, the proposed option did not suit me. In this article, I described my version of the blinking and non-blinking LED controller for the aircraft model.

The controller is based on ATTiny13A, i.e. This article will also be useful to those who deal with AVR microcontrollers. I tried to chew everything and put it on the shelves, so the article will be of interest primarily to beginners.

The abilities of the piece of iron can be estimated from this video:



Introduction


When I started picking this question and even soldered the board proposed in that article, it turned out that this was not at all what I needed. Firstly, there are only two channels that can blink only in turn. You can adjust the timings, but the algorithm is wired in the firmware. Secondly, there are already 3 buttons on the board that add extra grams. I'm not going to rearrange the blinker from model to model and reprogram the LEDs after each flight, which means these buttons are unnecessary for me. I agree once to solder the wires directly to the controller and program the algorithm that is needed. And finally, thirdly, the firmware is only in binary form, without source codes, which means that it is impossible to modify anything.

Thinking about the next model, I immediately figured out how many LEDs I need and how they will blink. As a result of the “census”, it turned out that I needed 4 channels (in each channel there were 2-3 LEDs):
  • BANO (Airborne Aircraft Navigation Lights - green and red lights at the ends of the wings) - these things are always on
  • Landing lights - will turn on and off from the remote control, i.e. the circuit must respond to the PWM signal from the receiver
  • Gates - white lights that blink short and bright flashes from time to time
  • Flashing beacons - red lights that turn on and off smoothly, resembling a spinning old-school tube flashing beacon.


The model is large, it flies far. And that means that the LEDs should be visible, they must be powerful. On the previous model, I did BANO on single-watt LEDs - they are perfectly visible from a distance of 50m even on a bright sunny day in the evening. So this is my size.

It just turned out that powering powerful LEDs is not so simple. On board there is only power from a linear stabilizer (on the motor controller board). This means connecting there even one powerful LED (through a resistor, of course) we get very large losses in heat. So large that the shrink of the regulator melts into holes. More details with calculations here

A pulse voltage regulator is better, but as it turned out, the LEDs need to stabilize not the voltage, but the current. The benefit was found mikruha, which it does very well. This was the second part of the preparation that I described here .

Electronics


We dealt with the requirements. It's time to take on a soldering iron.

I'm in electronics, in general, a beginner. That’s why I creatively reworked the circuit from Acinonyx (which in turn borrowed it from the VETERAN SCOOTER ). I needed to change the following:
  • Throw out buttons
  • Get PWM input from the receiver
  • 3 legs ATTiny identify as outputs and connect LED drivers to them
  • Add a fourth driver that will always be enabled (for BANO)


In general, little is left of the original.

As a driver, the ZXLD1350 microcircuit was successfully used, which is precisely designed to power single-watt LEDs (current up to 350mA). Moreover, in each channel, you can put any LEDs in series, if only all together were invested in total supply voltage. Those. if I feed the circuit from a 3S (11.1V) battery, I can put up to 3 LEDs in each channel on each of which 3.2V drops.

I powered the microcontroller separately from the receiver, using the same wire as the PWM input.

image

Scheme. Each channel is built according to the scheme of datasheet. There are 4 such channels on the board (I drew only one). I drew 3 LEDs, but, as I said, you can put any number of LEDs in each channel. You can even put LEDs in different colors (different voltage drops on them), the main thing is that they would be designed for the same current. The driver itself will select such a voltage so that the current through the diodes does not exceed 350mA.

The ADJ input of three of the channels is connected to the controller output through a transistor. The ZXLD1350 driver has a special mechanism by which you can turn on and off the LED from the controller. Moreover, you can smoothly adjust the brightness by changing the input voltage or using PWM. Here are just the working input voltage from 0.3V to 2.5V, and it gives out 5V from the controller. Fortunately datasheet recommends a solution in the form of a transistor. It is only necessary to take into account that this transistor inverts the logical state - the zero on the foot of the controller will turn on the LED, and turn off the unit. However, this is not a problem to fix programmatically.

In order to save weight, I decided to try to make a double-sided board. I never made friends with LUT, but with the photoresist everything worked out the first time. I also tried to play with the solder mask, but violated the technology and the mask fell ill (and in some places it fell off altogether). Errors are taken into account for the future, but I will leave this attempt as is. For the first time, it’ll roll anyway.

image

image

PCB layout. Crosses at the edges are docking marks. I cut out the textolite a bit with a margin, then in the places of the crosses I drilled holes through which then I combined the masks. I didn’t do the metallization of holes; I managed the jumpers. Well, the legs of the capacitor also work as a jumper between the sides.

image

Ready product. Excess textolite trimmed on the frame. The result was a 27x22mm shawl and a weight of 4g. Well, another 2g on the wires and connectors turned out. The device is connected to the receiver through a standard three-pin JR connector. LED drivers take power from the battery balancing connector.

image

image

To whom 1W can look a little at the ZXLD1360 chip. It is designed to power 3W LEDs (current 750mA). The wiring diagram and pinout are the same, so the wiring of the board is suitable. Only the values ​​of some parts need to be changed, smoke datasheet.

For those who have not yet pumped in the etching of double-sided boards, I also post several options for single-sided - for channels 2, 3 and 4.

LEDs bought from the Chinese on ebee. I bought it specifically without a radiator, which does not fit everywhere. I used pieces of aluminum strip as a radiator. The LED can be mounted using a special thermally conductive tape or glue. This is how it looks in the test version (the radiator is small here, it’s warming up)

image

And here it is on the previous model.

image

Firmware


Now you need to breathe life into this piece of iron. Since the source of the firmware from Scooter VETERAN was not found in the internet, I had to do everything myself. No, of course, I disassembled its firmware to see what's inside, but it was much more useful to just read the ATTiny13 specification.

Despite the fact that there is only 1KB of flash in the microcontroller, I decided to write in C. This is more convenient and visual. Arduino sketches, of course, will be easier in some way, but everything that I planned will not fit into the controller's memory. Therefore, I had to go down to a lower level and program the registers directly. To my surprise, the compiler (gcc 3.4.2 from Atmel Studio 6) generated pretty good code. True, there were a couple of places where the compiler acted suboptimal, but these places were able to be adjusted.

The architectural problem of the firmware is that I needed to do several conceptually different actions at the same time - blink here, do not blink here, wrap fish here, listen to PWM input, then generate PWM output.

Let me give you a classic example. What if we need to blink one LED? Well then, our program will look something like this:
while(1)
{
	led1(on);
	delay(500);
	led1(off);
	delay(500);
}


But what if we need to blink two LEDs, and even with a different frequency? Well, you can, of course, pervert and write something like this:

while(1)
{
	led1(on);
	delay(300);
	led2(on);
	delay(200);
	led1(off);
	delay(500);
	led2(off);
	delay(200);
}


But most likely it will be very difficult to choose the timings and the sequence of on-off. If possible, of course (of which I doubt). And if you need to blink with three diodes? And four?

The correct solution is to use timers. But there is a problem: the timer in the microcontroller is only 1, and even that is eight-bit.

In fact, where there is one timer, you can make a lot of software timers. It looks like this: the hardware timer is ticking with a high frequency. For each timer operation, the handler checks to see if the set time for the program timer has expired. If it turned out, then you need to call the handler.

Let's see how it will look in the code.

// Pointer to a timer handler
typedef void (*eventHandler)();
// Software timers list
typedef struct timer_t
{
	uint16_t timeout;
	eventHandler handler;
} timer_t;
#define TIMERS_LIST_SIZE 5
timer_t timersList[TIMERS_LIST_SIZE];


A program timer is a counter how many times the main (iron) timer must scroll before calling a handler. A pointer to the handler is attached. Three of these entries are sufficient for my tasks, but just in case, I made a list of program timers with a size of 5 elements.

The issue of setting the microcontroller timer I will describe a little later. And now about the implementation of software timers. The initialization function looks simple - we reset the list of timers

void setupEventQueue()
{
	// Clear timers list
	memset(timersList, 0, sizeof(timersList)); 
}


In order to add a program timer, we just look for an empty slot and enter the timeout values ​​in it plus a pointer to the handler. There is no error check in order to save space in the controller.

void addTimer(eventHandler handler, uint16_t timeout)
{
	// Search through the timers list to find empty slot
	for(timer_t * timer = timersList; timer < timersList + TIMERS_LIST_SIZE; timer++)
	{
		if(timer->handler != NULL)
			continue;
		// Add the timer to the list
		timer->handler = handler;
		timer->timeout = timeout;
		break;
	}
}


The main cycle is as follows.

void runEventLoop()
{
	runTimer();
	// Set up sleep mode
	set_sleep_mode(SLEEP_MODE_IDLE);
	while(1) // Main event loop
	{
		wdt_reset();
		// Sleep until the timer event occurs
		sleep_enable();
		sleep_cpu();
		sleep_disable();
		//Iterate over timers
		for(timer_t * timer = timersList; timer < timersList + TIMERS_LIST_SIZE; timer++)
		{
			// Skip inactive timers
			if(timer->handler == NULL)
				continue;
			if(timer->timeout) // Decrement timeout value
			{
				timer->timeout--;
			}
			else // If it is already zero - execute handler
			{
				timer->handler();
				timer->handler = NULL;
			}
		}
	}	
}


First, start the timer (more on that below, it’s not so trivial there). Instead of waiting actively, I use sleep. At the beginning of each cycle, the processor enters Idle mode. This means that the CPU itself will fall asleep, but all the timers (ok, all, it is one!) Will continue to work. When the timer counts to the end and resets to zero, an interruption will occur that will wake the processor and the program will go further. Just what we need.

Yes, you can’t save a lot of electricity in an LED blinker, but in the future the same frame can be used in other applications where falling asleep can be very useful.

If we woke up, it means it's time to go through the list of software timers. In each record we decrease the counter value. If you have already reached zero, then we call the handler, after which we delete the timer from the list (by writing NULL to the pointer to the handler).

Since everything happens in one thread, no mutexes and locks are required.

In order to blink the LED with a fixed frequency, the processor will look like this: invert the state of the LED, ask the system to call the same processor again after a while.

#define LED_A_PIN		PORTB0
void toggleLedATask()
{
	PORTB ^= (1 << LED_A_PIN);
	addTimer(toggleLedATask, TIMEOUT_MS(300));
}


For this to work, you need the handler to somehow call for the first time. To do this, before starting the main loop, we just put the message in the queue that it is time to call the handler with a delay of 0 ms (i.e., immediately at the first opportunity).

int main(void)
{
	// Set up ports
	PORTB = 1 << LED_A_PIN; // LEDs switched off 
	DDRB = 1 << LED_A_PIN; // output mode for LED pins
	setupEventQueue();
	addTimer(toggleLedATask, TIMEOUT_MS(0));
	sei();
	runEventLoop();
}


First, a pin for output is configured here. I remind you that the LEDs are connected through an inverter. So in order to turn off the default LED, you need to write a unit to the port.

Well, blinking back and forth is not interesting. Could something be cooler? For example, flush once, then after a pause, blink twice, then three times, repeat, shake, do not stir. Well, this is also not difficult.

#define LED_B_PIN		PORTB1
uint8_t delayIndex = 0;
const uint16_t delays[] = 
{
	TIMEOUT_MS(100), //on
	TIMEOUT_MS(700), //off
	TIMEOUT_MS(100), //on
	TIMEOUT_MS(200), //off
	TIMEOUT_MS(100), //on
	TIMEOUT_MS(700), //off
	TIMEOUT_MS(100), //on
	TIMEOUT_MS(200), //off
	TIMEOUT_MS(100), //on
	TIMEOUT_MS(200), //off
	TIMEOUT_MS(100), //on
	TIMEOUT_MS(1200), //off
};
void complexLedTask()
{
	PORTB ^= (1 << LED_B_PIN);
	uint16_t delay = delays[delayIndex];
	delayIndex ++;
	if(delayIndex >= sizeof(delays)/sizeof(uint16_t)) //dim(delays)
		delayIndex = 0;
	addTimer(complexLedTask, delay);
}


Just make a table with timings. Each time, the handler changes the state of the LED and waits for the time indicated in the table.

In order to blink several LEDs simultaneously and independently, we simply add several similar handlers, without forgetting to configure the port and add to the list of handlers.

int main(void)
{
	// Set up ports
	PORTB = 1 << LED_A_PIN | 1 << LED_B_PIN | 1 << LED_C_PIN; // LEDs switched off
	DDRB = 1 << LED_A_PIN | 1 << LED_B_PIN | 1 << LED_C_PIN; // output mode for LED pins
	setupEventQueue();
	addTimer(toggleLedATask, TIMEOUT_MS(0));
 	addTimer(complexLedTask, TIMEOUT_MS(0));
 	addTimer(blinkLedCTask, TIMEOUT_MS(0));
	sei();
	runEventLoop();
}


Of course, you still need to get used to this style of programming, but in general, the approach works well. Remember, if we are writing a multi-threaded application for a large computer, usually each thread has an eternal loop and, possibly, some sort of sleep or wait. Consider that the handlers presented above are the body of that same eternal loop, and the addTimer () call is the same sleep.

How often should the main timer tick? If it ticks infrequently, this will reduce the accuracy of the measured time intervals. On the other hand, for each timer cycle, you will need to do a certain number of useful actions. And these actions need to be completed before the next timer cycle. So the timer should tick and not very often the same.

Those. not often and not rarely. But how exactly? Ok, for the previous task, the range of possible values ​​is quite large. But you also need to remember the task of “listening to PWM input”. More specifically, there are pulses with a duration of 800-2200 μs and we will have to measure this length. For our task, turn on / off the LED by command from the remote control, we will consider this: if the pulse is shorter than 1500 µs, the LED is off, if longer, it is on.

Translated into the language of microcontrollers and timers, we will count how many ticks of the timer fit in the measured length of time. The problem arises when the pulse duration is approximately equal to the threshold. Then false alarms are possible and the LED will blink when the pulse length changes. To reduce the likelihood of blinking, we need to more accurately measure the pulse length. I think the resolution of the timer should be in the region of 1-2 μs - such a resolution will provide sufficient accuracy of measurements.

Since we are talking about specific numbers, you need to figure out the frequency of the microcontroller. The controller can be clocked from the internal and from the external generator. An external generator is more accurate, but these are additional details and weight. Yes, and we do not really need accuracy. 128kHz, 4.8MHz and 9.6MHz are available from internal generators. 128kHz will not be enough, we will choose between two other options.

The timer, in turn, can have the same frequency as the microcontroller, or it can use a frequency divider of 8, 64, 256 or 1024. The timer itself counts from 0 to 255 and then is reset to 0. If the divider does not use one tick the timer corresponds to one processor tick, which in most cases corresponds to one command. We were going to do useful work every complete timer cycle. But if we need to do this work every 256 teams, then we simply will not have time to do this work (or it should be very very small).

So, you need to choose between 4.8 MHz and 9.6 MHz, and dividers 8, 64 and 256. As for me, the 4.8 MHz variant with a divider 8 is pretty good. The timer will tick with a frequency of 4.8 MHz / 8 = 600 kHz. This means that one tick will take 1.666mks. It just fits into the desired 1-2mks. A full timer cycle will take 1,666 * 256 = 426.66 μs. As a program timer, we use a 16-bit variable, which means we are able to measure time intervals 65536 * 426.66 μs = 27.96 s (with the accuracy of the same 426.66 μs)

Timer start code:

void runTimer()
{
	// Reset timer counter
	TCNT0 = 0;
	// Run timer at 4.8MHz/8 = 600 kHz
	// This gives 1.667 uSec timer tick, 426.667 uSec timer interval
	// Almost 28 seconds with additional 16bit SW timer value
	TCCR0A = 0; // Normal mode
	TCCR0B = 0 << CS02 | 1 << CS01 | 0 << CS00; // run timer with prescailer f/8
}


In the code above, I used the cryptic TIMEOUT_MS macro. It's time to decrypt it.

#define TIMEOUT_MS(t)  ((uint32_t)t  * 600 / 256)    //4.8MHz / (8 prescailer * 256 full timer cycle * 1000 since we are counting in ms)


This macro determines the number of cycles by 426.6 μs required to measure the given number of milliseconds. Unfortunately, when I bumped into the full formula (the one in the commentary), the compiler began to generate terrible vornings that I could not handle. I had to recount the formula to now incomprehensible 600/256.

But back to listening to the PWM input. To make it a little clearer, I’ll tell you again how everything works, but in other words. The main 8-bit timer ticks from 0 to 255. Each complete timer cycle, we process the list of program timers and run handlers if necessary. In addition, the value of the 8-bit timer itself is used to measure the pulse length at the input. This is done very simply: if the impulse starts, we remember the value of the timer. As the pulse goes, the timer continues to tick. By the time the pulse ends, the timer dips to some new value. Accordingly, by the difference in values, we can calculate the pulse length simply by multiplying by the time of one tick (1,666 μs)

Stop! We have an 8-bit timer, which means that only pulses up to 256 * 1.66 = 426.66 microseconds long can be measured in this way, while incoming pulses up to 2200 microseconds long. No problem! You can artificially expand the timer counter by adding as many high bytes as needed. Normal binary math works - when the low byte overflows, increment the high bytes.

// Additional high byte for 8bit timer value
volatile uint8_t tcnth; 
void runTimer()
{
	// Reset timer counters
	tcnth = 0;
	TCNT0 = 0;
	// Run timer at 4.8MHz/8 = 600 kHz
	// This gives 1.667 uSec timer tick, 426.667 uSec timer interval
	// Almost 28 seconds with additional 16bit SW timer value
	TCCR0A = 0; // Normal mode
	TCCR0B = 0 << CS02 | 1 << CS01 | 0 << CS00; // run timer with prescailer f/8
	TIMSK0 = 1 << TOIE0;
}


Almost everything is the same. Only the tcnth variable was added - the "high" byte in addition to the low byte inside the timer. The last line is also important - it includes a timer overflow interrupt. This interrupt will increment the high byte:

ISR(TIM0_OVF_vect)
{
	// Increment high byte of the HW counter
	tcnth++;
}


Note that the tcnth variable is declared volatile. Without this keyword, the compiler in another part of the program might think that the variable does not change and optimize the excess. He is not aware that the variable changes in the interrupt (in fact, in another thread).

In order to catch the beginning and end of the pulse, you can use the pin change interrupt, specially designed for this, - an interrupt that will be called just when the value at the input changes. T.O. no need to constantly poll the input - the microcontroller will do all the work. We can only write a handler for this interrupt

uint16_t pwmPulseStartTime;
#define PWM_THRESHOLD	900	 // number of pulses in 1500 uS at 4.8MHz with /8 prescailer = 1500 * 4.8 / 8 = 900
// Pin Change interrupt
ISR(PCINT0_vect)
{
	/*
	// Get the current time stamp
	uint16_t curTime = (tcnth << 8) + TCNT0;
	Unfortunately gcc generates plenty of code when constructing 16 bit value from 2 bytes. Let's do it ourselves	
	*/
	union
	{
		struct
		{
			uint8_t l;
			uint8_t h;
		};
		uint16_t val;
	} curTime;
	// Get the current time stamp
	curTime.h = tcnth;
	curTime.l = TCNT0;
	// It may happen that Pin Change Interrupt occurs at the same time as timer overflow
	// Since timer overflow interrupt has lower priority let's do its work here (increment tcnth)
	if(TIFR0 & (1 << TOV0))
	{
		curTime.h = tcnth+1;
		curTime.l = TCNT0;
	}
	if(PINB & (1 << PWM_INPUT_PIN)) // On raising edge just capture current timer value
	{
		pwmPulseStartTime = curTime.val;
	}
	else // On failing edge calculate pulse length and turn on/off LED depending on time
	{
		uint16_t pulseLen = curTime.val - pwmPulseStartTime;
		if(pulseLen >= PWM_THRESHOLD)
			PORTB |= (1 << LED_C_PIN);
		else	
			PORTB &= ~(1 << LED_C_PIN);
	}
}


The first part of the handler is devoted to pulling the value of the timer counter (extended by an additional external byte). Unfortunately, reading the value in the forehead did not work - from time to time the LED spontaneously blinked. This was because 2 interrupts occurred at about the same time. And since timer interruption has lower priority, the handler sometimes was not called when it should. As a result, the high byte was not increased, which means the total value was 256 units less. This is essential.

The solution is quite simple - check if a timer overflow has occurred and if it does, do the same work as the processor of this overflow - do +1 to the high byte.

At this point, I came across the non-optimality of code generated by a gnat. The code (tcnth << 8) + TCNT0 compiled like this, with shifts and additions. And this despite the included optimization (-O1). In this place, I just need to interpret 2 bytes as a 16-bit number. I had to fence a garden with unions.

The second part of the handler actually does a useful job. If we caught the beginning of the impulse, just remember the time stamp in the variable pwmPulseStartTime. If you caught the end of the pulse - we consider the difference in time stamps and turn on / off the LED depending on the value. The response threshold is 1500ms, or 900 ticks of a timer of 1.66 µs each.

What is missing here is the initialization of this pin change interrupt:

#define PWM_INPUT_PIN	PCINT3
void setupPWMInput()
{
	// Initialize the timestamp value
	pwmPulseStartTime = 0;
	// Set up pin configuration
	PORTB |= 1 << PWM_INPUT_PIN; // pull-up for PCINT3
	DDRB &= ~(1 << PWM_INPUT_PIN); // output mode for LED pins, input mode for PCINT3 pin
	// Use PCINT3 pin as input
	PCMSK = 1 << PWM_INPUT_PIN; 
	// Enable Pin Change interrupt
	GIMSK |= 1 << PCIE;
}


Almost everything is ready. Of the requirements, only smooth blinking was not implemented. In fact, the controller can only turn the LED on and off. There are no intermediate values. But if you quickly turn on and off quickly with a given duty cycle (the ratio of the time when the LED is on to the time when it is not on), then it seems to a person that the LED still shines smoothly, only with a lower brightness. Well, if you change the duty cycle little by little, it will seem that the brightness of the LED changes smoothly.

You can write code that will turn the LED on and off. The benefit of timers can now be done as much as you want. But why, if PWM generation is already built into the controller? Moreover, it is possible to independently control as many as two generation channels - on the legs OC0A and OC0B (they are also PB0 and PB1).

It works like this. A single timer spins as usual at a given speed. At the beginning of the cycle, one is set on the foot; upon reaching a certain certain value (set by the registers OCR0A and OCR0B), zero is set on the foot. Next, the cycle repeats. The larger the value of the register, the greater the duty cycle and the brighter the diode glows. This is called Non-Inverting Mode. Since the LEDs are connected through the inverter, Inverting mode is more suitable for us - we turn on the value in the register, turn it off when the timer reaches the end and resets.

// Current PWM value
volatile uint8_t pwmAValue = 1;
volatile uint8_t pwmBValue = 1;
void runTimer()
{
	// Reset counter counters
	tcnth = 0;
	TCNT0 = 0;
	OCR0A = pwmAValue;
	OCR0B = pwmBValue;
	// Run timer at 4.8MHz/8 = 600 kHz
	// This gives 1.667 uSec timer tick, 426.667 uSec timer interval
	// Almost 28 seconds with additional 16bit SW timer value
	//TCCR0A = 1 << COM0A1 | 1 << COM0A0 | 1 << COM0B1 | 1 << COM0B0 | 1 << WGM01 | 1 << WGM00; // Fast PWM on OC0A and OC0B pins, inverting mode
	TCCR0A = 1 << COM0A1 | 1 << COM0A0 | 1 << WGM01 | 1 << WGM00; // Fast PWM on OC0A pin, inverting mode
	TCCR0B = 0 << CS02 | 1 << CS01 | 0 << CS00; // run timer with prescailer f/8
	TIMSK0 = 1 << TOIE0;
}


I had to slightly adjust the initialization of the timer. The WGM00 and WGM01 bits enable Fast-PWM generation mode. Bits COM0A0, COM0A1, COM0B0 and COM0B1 enable Inverting mode in channels A and B. More precisely, the commented line includes both uncommented for OC0A only.

Since the values ​​in the pwmAValue variables change from time to time, you need to somehow let the timer know about it. This is best done in the overflow handler.

ISR(TIM0_OVF_vect)
{
	// Update the PWM values
	OCR0A = pwmAValue;
	OCR0B = pwmBValue;
	// Increment high byte of the HW counter
	tcnth++;
}


Of course, you can directly directly push values ​​into the OCR0A and OCR0B registers, but this is not recommended by the datasheet. This can lead to a “slip” when the pin value changes earlier or later than it should. Visually, this would be manifested in an undesirable sharp and short-term change in brightness.

The brightness itself can be changed in the already familiar software timers. For example, like this:

uint8_t directionA = 0; 
void pwmLedATask()
{
	if(directionA) // Incrementing
	{
		pwmAValue += 2;
		if(pwmAValue == 255)
			directionA = 0;
	}
	else //decrementing
	{
		pwmAValue -= 2;
		if(pwmAValue == 1)
			directionA = 1;
	}
	addTimer(pwmLedATask, TIMEOUT_MS(2));
}


Just increment or decrement the value of the pwmAValue variable, which will then be entered in the appropriate register. Although, to emulate a real flashing beacon, you will have to come up with something prettier. For example, like this:

typedef struct complexPWM
{
	uint8_t step;
	uint8_t maxValue;
	uint16_t delay;
} complexPWM;
complexPWM pwmItems[] =
{
	{0, 1, TIMEOUT_MS(1000)},
	{2, 127, TIMEOUT_MS(2)},
	{-2, 33, TIMEOUT_MS(2)},
	{2, 255, TIMEOUT_MS(2)},
	{-2, 1, TIMEOUT_MS(2)}
};
uint8_t pwmTableIndex = 0;
void complexPWMTask()
{
	complexPWM * curItem = pwmItems + pwmTableIndex;
	pwmAValue += curItem->step;
	if(curItem->maxValue == pwmAValue)
		pwmTableIndex++;
	if(pwmTableIndex == sizeof(pwmItems)/sizeof(complexPWM)) //dim(pwmItems)
		pwmTableIndex = 0;
	addTimer(complexPWMTask, curItem->delay);
}


I’m not sure that it looks like a flashing beacon, but in this piece of code, a preflash is done to a brightness of 127, then we decrease the brightness to 33 and make a full flash (up to 255).

That's probably all for the firmware. With all the giblets and morgulki, everything interferes with 500-600 bytes - even the reserve still remains. It remains to highlight one important point - Fuse bits. They are equal to hfuse = 0xff, lfuse = 0x79. For decoding, I’ll ask for a datasheet. In a nutshell, a couple of bits in these bytes force the controller to operate at 4.8 MHz. The remaining bits are left in the default state.

How it looks in reality can be estimated from the video in the header of the article.

Conclusion


In this article, I described my version of the controller for various aircraft model bulbs - BANO, landing lights and gates. All that remains is to pick the firmware a bit and your copy models will look like real ones.

Moreover, such a blinker can be put on a car. And you can, for example, highlight an advertising sign. All in your hands.

But not single planes. In addition to blinking lights, I covered several other points in the article related to programming weak controllers:

  • How to simulate several timers on one timer
  • How to choose timer options
  • How to set a timer in PWM generation mode
  • How to listen to PWM input and do useful work based on read values


If we ignore the task of blinking LEDs, it turned out to be a good frame for "multitasking" ("multithreaded"?) Applications on the microcontroller. Of course, this is not RTOS, but it already eliminates a whole bunch of routine operations. In the firmware, this framework I put in a separate module EventQueue.c / .h. Use health.

The nice thing is that we got 3 completely independent tasks (counting long periods of time, measuring the pulse width at the input and generating the PWM at the output) we managed to hang on a single 8-bit timer. Well, on the added software timers, you can still do a lot of useful things.

The code, however, turned out to be not very structured. Different tasks are solved in the same functions, while each task is spread out in several places. But this is a fee for a compact size. As well as the lack of buttons and other means of entering information. But, again, I’m not tired of soldering the programmer wires directly to the controller once, configure the desired blink option in the code and pour it all in the form of firmware.

This article is not a reference - you do not care to get into the datasheet for an explanation of certain bits and registers. This is just an example of how you can make some useful stuff using the meager means of a junior controller in the AVR line.

Initially, I did not plan to attach the final compiled hex file to the article. The fact is that all models are different. Somewhere you need a different number of channels, somewhere you need to blink differently, maybe you need to add a couple of inputs, or do something else. Instead, I would suggest that you try to change the firmware yourself so that it fully matches your idea. Everything is simple there!

However, not all aircraft modelers are friends with the compiler. So I nevertheless compiled some middle version: one channel blinks every 2 seconds (strobe), the PWM channel blinks a little more often with double flashes, the third channel is switched on by a command from the remote control, the fourth one, as before, always shines. This firmware will be a starting point for further dopilivaniya. The examples presented in the article I also left in the code, only commented on the calls.

Good luck!

Firmware sources and PCB layout .

Also popular now: