Fan controller project (also, no more Arduino)

December 2, 2015

A while ago I started on a rather fun project to make a temperature controlled fan that we needed so our consoles will not melt in their enclosed media cabinets under the TV ( never mind the creepy cat).

11949364_701999709944238_1937537779589436090_n

Sure, I could’ve bought some but sometimes it’s nice to build stuff with your own hands ( and head ). After a few prototypes using various Arduino boards I decided to make my own board based on Atmega32u4 from Atmel in order to have the small form factor I was going for.

After some quality time spent in the lab the hardware design was done.

12046838_722167881260754_763966236515822514_n

First prototype was proto-board and that sucked bad… i mean baaaad..  sparks between solder points bad… so on to make my own board with the laser printer and a bunch of nasty chemicals….

12039207_709400759204133_7728226878182741216_n

Yeaaa… that’s not gonna happen… Hopeless! Finally went for my trusted friends at https://oshpark.com for help, and here’s how it looks today (after i mounted some components on it )

IMG_0130

Now that the hardware is done, time to do the software. Burn the bootloader and on we go with the Arduino IDE.

What can I say… turns out it’s a bit slow… so I got Atmel Studio and gave that a try. It’s really awesome! And.. it just imports Arduino sketches as native projects. But then you’re still running arduino code. Don’t get me wrong, Arduino code is just like the Arduino boards: great for prototyping, but if you wanna do native stuff it’s better to write native code.

Here’s a simple example of Arduino digitalWrite disassembly:

00000806 <digitalWrite>:
     806:	1f 93       	push	r17
     808:	cf 93       	push	r28
     80a:	df 93       	push	r29
     80c:	28 2f       	mov	r18, r24
     80e:	30 e0       	ldi	r19, 0x00	; 0
     810:	f9 01       	movw	r30, r18
     812:	ec 50       	subi	r30, 0x0C	; 12
     814:	ff 4f       	sbci	r31, 0xFF	; 255
     816:	84 91       	lpm	r24, Z
     818:	f9 01       	movw	r30, r18
     81a:	ee 5e       	subi	r30, 0xEE	; 238
     81c:	fe 4f       	sbci	r31, 0xFE	; 254
     81e:	d4 91       	lpm	r29, Z
     820:	f9 01       	movw	r30, r18
     822:	e0 5d       	subi	r30, 0xD0	; 208
     824:	fe 4f       	sbci	r31, 0xFE	; 254
     826:	c4 91       	lpm	r28, Z
     828:	cc 23       	and	r28, r28
     82a:	c9 f0       	breq	.+50     	; 0x85e <digitalWrite+0x58>
     82c:	16 2f       	mov	r17, r22
     82e:	81 11       	cpse	r24, r1
     830:	0e 94 8a 03 	call	0x714	; 0x714 <_ZL10turnOffPWMh>
     834:	ec 2f       	mov	r30, r28
     836:	f0 e0       	ldi	r31, 0x00	; 0
     838:	ee 0f       	add	r30, r30
     83a:	ff 1f       	adc	r31, r31
     83c:	e4 5a       	subi	r30, 0xA4	; 164
     83e:	fe 4f       	sbci	r31, 0xFE	; 254
     840:	a5 91       	lpm	r26, Z+
     842:	b4 91       	lpm	r27, Z
     844:	8f b7       	in	r24, 0x3f	; 63
     846:	f8 94       	cli
     848:	11 11       	cpse	r17, r1
     84a:	05 c0       	rjmp	.+10     	; 0x856 <digitalWrite+0x50>
     84c:	9c 91       	ld	r25, X
     84e:	ed 2f       	mov	r30, r29
     850:	e0 95       	com	r30
     852:	e9 23       	and	r30, r25
     854:	02 c0       	rjmp	.+4      	; 0x85a <digitalWrite+0x54>
     856:	ec 91       	ld	r30, X
     858:	ed 2b       	or	r30, r29
     85a:	ec 93       	st	X, r30
     85c:	8f bf       	out	0x3f, r24	; 63
     85e:	df 91       	pop	r29
     860:	cf 91       	pop	r28
     862:	1f 91       	pop	r17
     864:	08 95       	ret

That is atrocious! Let alone the number of cycles but man… branches… function calls.. .AAAAARGH!

Time to write my own…

For contrast, here’s my generated code for digitalWrite:

1f6:	2d 9a       	sbi	0x05, 5	; 5

Yes! That’s right. One bloody line. Remember though, that’s *generated* code. The source code handles every case just like arduino does. It supports PWM, supports setting the mode to input, input_pullup, output, all the good things. It’s just that the generated code is better by a many orders of magnitude.

It’s good to realize compilers are pretty smart nowadays and it’s sad to see people don’t take advantage of that. I’m not saying trust the compilers, but once you verify it does what you expect it to do then use them! That’s why they were invented for in the first place !

The key to make this super optimal is proper use of inlines and consts.

Example:

static FORCE_INLINE  volatile uint8_t* GetDDRRegFromPin(const Pin& pin)
{
	if ( &PORTB == pin.port )
		return &DDRB;
	else if ( &PORTC == pin.port )
		return &DDRC;
	else if ( &PORTD == pin.port )
		return &DDRD;
	else if ( &PORTE == pin.port )
		return &DDRE;
	//else if ( &PORTF == pin.port )
	
	return &DDRF;
}

static FORCE_INLINE  volatile uint8_t* GetPINRegFromPin(const Pin& pin)
{
	if ( &PORTB == pin.port )
		return &PINB;
	else if ( &PORTC == pin.port )
		return &PINC;
	else if ( &PORTD == pin.port )
		return &PIND;
	else if ( &PORTE == pin.port )
		return &PINE;
	//else if ( &PORTF == pin.port )
	
	return &PINF;
}

struct Pin
{
 volatile uint8_t* const port; // pin's port physical address
 uint8_t mask; // pin's bit
 volatile uint8_t* const timer; // pin's timer physical address
 uint8_t timerBit; // timer's bit
 volatile uint16_t* const pwmReg;// PWM register address
 ...
 void FORCE_INLINE SetHigh() const { *port |= mask; }
 void FORCE_INLINE SetLow() const { *port &= ~mask; }
 void FORCE_INLINE SetDDRHigh() const { *GetDDRRegFromPin(*this) |= mask; }
 void FORCE_INLINE SetDDRLow() const { *GetDDRRegFromPin(*this) &= ~mask; }
 uint8_t FORCE_INLINE Get() const { return *GetPINRegFromPin(*this) & mask;}
 ...
};

Now… you might say ‘WTF is with all the branches ?!?!?! this is stupid!’

Well, not quite. If you look carefully, the input arguments are const. in most cases people setup pins by using literal consts ( as in numbers ).
What the compiler does, it will pass that literal const along in the preprocessing stage and then it will optimize all the branches away. And that’s exactly what it does.

This only works if the consts are allowed to propagate properly. This means that the initialization of the pins has to be done in a certain way. Having them members of a struct that you init in the constructor won’t do the trick. And if they’re not properly propagated you’ll end up generating all those branches from above and ain’t nobody got time for that… literally… you’re running on a 8-16MHz MCU…

So. This is how I initialize my LCD thingy:

static const HD44780::Config lcdContext =
{
	PINDESC_D12,
	PINDESC_D4,
	PINDESC_D14,
	PINDESC_D16,
	PINDESC_D15,
	PINDESC_D17,
};

For PINDESC_Dxx I took advantage of the compiler/preprocessor by making these crafty struct initializing macros for each of the pins.

#define PINDESC_D0   { &PORTD, (1 << 2), 0, 0, 0} // example PIN without PWM support
...
#define PINDESC_D5   { &PORTC, (1 << 6), &TCCR3A, 1 << COM3A1, &OCR3A} // PIN with PWM support
...

So now that the constants are properly propagated, the inlines are properly inlined and everything is fast and dandy, I’m happy!

Don’t you just love a happy ending? I know I do!

 

P.S.

You can download the library from GitHub here. Keep in mind that it is *not* a fully tested released product. It’s just a little something I’m using for my own projects. Use at your own risk.

Share this:

#c0decafe #Electronics

Leave a Reply

Your email address will not be published. Required fields are marked *


*


This site uses Akismet to reduce spam. Learn how your comment data is processed.