The Burnt Couch Robot Blog
AVRs and Linux are the stuff of the Universe...Dr. Tyson just don' wanna admit it yet....
Sunday, October 9, 2016
The five minute Fitbit hack
And this is what you get for bragging...
My 40-year running buddy JM, distressed and beleaguered former Iraq vet that he is, was recently beset with a Fitbit...corporate America has burdened him with the task of proving his manhood by walking at least 10,000 steps a day.
After listening to his combination of guilty-conscienctiousness and frustrated bitching for a few weeks, I begin to brag...'I can, in five minutes, write-a -program/wire-up-some-hardware that will obviate your physical participation in this ridiculous encumberation.'
And after 10,000 legitimate steps on the river walk, several beers on the porch, and a fantastically unusual Oregon Indian summer Sunday afternoon, I realized I would be forced to walk the walk. Fair enough! Results are below. My hardware setup consisted of an Arduino Uno, three jumper wires to the servo (Radio Shack 'standard'), a 5" long 1/8" wood dowel, some cellophane tape...and the logger module from JM's fitness torture device.
And, voila...the quintessential 19-line Arduino program that will allow you to win that case of Gatorade at the company pool next week.
/* servo slew 1 */
//
//
#include <Servo.h>
Servo myservo;
void setup() {
pinMode(13, OUTPUT);
myservo.attach(3);
}
void loop() {
int x, y;
digitalWrite(13, HIGH);
x = random(650, 1200);
myservo.writeMicroseconds(x);
y = random(350, 600);
delay(y);
digitalWrite(13, LOW);
x = random(1000, 2050);
myservo.writeMicroseconds(x);
y = random(350, 600);
delay(y);
}
The blinks of the LED on pin 13 aren't strictly necessary, just there for debugging purposes. I randomized the delays as well as the right- and left- slew to simulate the uneven gait of most walkers. My first attempt was even shorter, simply slewing between 20 and 120 degrees and back, once per second, but the Fitbit module didn't log near as many steps that way...
Forthwith, JM and I adjourned to the local brew pub for some quality hours with our favorite blend of mere Irishness and outright beligerence; two hours and ten minutes, four 22-ounce pints, and several thousand jokes in questionable taste later....
11,500 steps!
We do NOT feel guilty about this. It is as if Everest were in our back yard, and Sir Edmund Hillary had asked us if we would like to tag along for a little hike. JM ended up with 25,000 for the day and quite a bit of good will with the waitresses...
Tuesday, October 4, 2016
avra if you REALLY want it painless
Assembler wars continue...
At this point, if you are as frustrated with all assembler gyrations as I am, you would wonder if all this pain over the best and most pious AVR assembler is really worth the trouble. avr-as and the GNU toolchian are all very well, but after wrangling with EEPROM programming for another few days, I was beginning to worry about my sanity.
While doing some more futile googling to figure out what I was doing wrong...I discovered that there were at least two more AVR assemblers for Linux beyond the GNU toolchain: 'Tom's AVR Assembler' and 'avra'. They are both a bit old; 'tavrasm' was last maintained in 2005, and the last activity with 'avra' on Sourceforge was in 2010. But the big difference: BOTH are intended specifically to assemble Atmel's avrasm-formatted files, and both include a superset of Atmel's assembler directives. The latter of which, as it turns out, I am pretty familiar.
In my last post I documented my frustrations with the 'AVR-libc' header file gibberish, and demonstrated how to adapt Atmel's include files to work with avr-as, so that using the other garbage wasn't strictly necessary. With a lot of trial-and-error, I managed to get almost everything to work...except a fully useful way to define EEPROM data and link addresses that would point to it. I could handcode a .hex file (ech). Or create a separate EEPROM file (faking in the data as if it was in program space), assemble it, then look at the list file and add constants to the main program file that refer to the right addresses (double ech).
Ugly. Non-standard. Not very portable.
Back in Atmel land...
And then I discovered tavrasm, and realized that I could return to a more sensible assembler syntax that was AVR-specific, use the standard Atmel *.inc files...errrrrr...sort of. But then the honeymoon ended, the romance was over....
Yeah. Tom Mortensen last worked on his assembler in 2005. His 'macro in macro' expansion of the assembler directives was just a BIT undocumented. I spent half a day looking vainly for code examples, experimenting with conditional assembly directives that didn't work, wondering why there weren't any command line options to generate a .list file even though there were documentation examples and directives for it.....The list of devices supported by tavrasm is/was VERY limited: it did NOT include any ATtiny chips, among other things...and there are a few instructions on the ATtiny (like MOVW, LPM <register>, Z+ and, more critically, SPM) that are too good to give up.
To add the basic support for the ATtinys (sizes of the various memory spaces), I edited the device header file and recompiled, but there wasn't any easy way, short of learning yacc and lex again, then hacking through Tom's code and Atmel's part docs, to add support for the newer instructions. Arrrrrrr....another afternoon wasted.
So I turned to avra, and all my problems went away, including the vast frustrations I was beginning to have with Tom's macros.
Here's the 'avra' version of Blink26.S:
--------------------------------------------------------
;
; blink26.S
;
; ** LOAD IO DEFS FOR THE RIGHT DEVICE
;
.nolist
.include "avrinct/avrcommon.inc"
.include "avrinct/tn26def.inc"
;
; ** GET ME SOME HANDY MACROS **
;
.include "avrinct/macros.inc"
;
; *** DEFINES HERE ***
;
.list
.equ F_CPU = 8000 ; 8Mhz clock, for msec_delay define in 'macro.inc'
.equ OUTPIN = PA7 ; for t26
.equ IO_OS = 0x20 ; for lds/sts with i/o ports, e.g 'lds r2, (IO_OS + TCNT0)'
;
; *** SRAM RESERVES HERE ***
;
.dseg
; FOR example:
;<datalabel>: .byte 1 ; to reserve 1 byte in SRAM for <datalabel>
;
; *** RESETS, IVECTORS, AND MAIN PROGRAM ***
;
.cseg
.org 0x0000
reset:
rjmp main; *** tn26 interrupt vector names, just fer handy ***
reti ; 0001 INT0_vect
reti ; 0002 PCI0_vect
reti ; 0003 TIMER1_OC1A_vect
reti ; 0004 TIMER1_OC1B_vect
reti ; 0005 TIMER1_OVF_vect
reti ; 0006 TIMER0_OVF_vect
reti ; 0007 USI_START_vect
reti ; 0008 USI_OVF_vect
reti ; 0009 EERDY_vect EEPROM Ready
reti ; 000A ACOMP_vect Analog Comparator
reti ; 000B ADC_vect ADC Conversion Complete
;
;
main:
spsetup r16, RAMEND ; use the macro to set up the stack
sbi DDRA, OUTPIN ; set pin 'OUTPIN' on port A to high for OUTPUT
ldi ZL, low(array * 2) ; to calc address right for LPM later
ldi ZH, high(array * 2) ; ditto
blink:
sbi PORTA, OUTPIN ; then drive it HIGH
lpm r20, Z
b2:
msec_delay 1, r17, r18, r19 ; 1 ms wait * r20 times
dec r20
brne b2
cbi PORTA, OUTPIN ; then LOW...
lpm r20, Z
b3:
msec_delay 1, r17, r18, r19 ; 1 ms wait * r20 times
dec r20
brne b3
adiw Z, 1
lpm r20, Z
tst r20
breq end
rjmp blink;
;
end:
rjmp end;
; *** MAIN PROGRAM END ***
;
; PROGRAM MEMORY DATA HERE
;
array: .db 10,250,20,240,30,230,40,220,50,210,60,200,70,190,80,180,90,170,100,160,110,150,120,140,130,0
;
;
;
--------------------------------------------------------
'avra' has excellent documentation, particularly the macros; there are many, many code examples and the expansion from Atmel's basic functionality allows
Things in the above code are just clear, not near as much mud. We have .dseg, .cseg, and .eseg directives; a very clear difference between register (.def) labels and symbolic constants (.equ); runtime constant expansions using .set, excellent conditional compilation directives; .list and .nolist for disassembly and automatic generation of Intel hex code, list and EEPROM hex files.
Now I can use the straight Atmel *.inc files with almost no customization, or even use the conditional directives to make them even more handy. The inclusion of 'avrcommon.inc' takes care of some of the universal constants and macros, including stack initialization.
Macros are a bit easier, and a lot more powerful; here's part of my rewritten utility file:
--------------------------------------------------------
;*
;* macros.inc file
;*
;*
;
...
...
;
;
; ***
; *** inline millisec delay loop
; ***
; *** max is about 11000
; *** !!and be sure to define F_CPU in main program for both like below!!
;
;.equ F_CPU, <CPU mhz * 1000> (e.g. for 8Mhz use 8000)
;
;
; delay = @0, ra = @1, rb = @2, rc = @3
.macro msec_delay
ldi @1, ((((@0 * F_CPU - 1) % 327680) % 1280) / 5)
ldi @2, (((@0 * F_CPU - 1) % 327680) / 1280)
ldi @3, ((@0 * F_CPU - 1) / 327680)
m_%:
subi @1, 0x01
sbci @2, 0x00
sbci @3, 0x00
brne m_%
nop
.endmacro
; ***
--------------------------------------------------------
Since avra is AVR-specific, the Makefile is REALLY streamlined. avra doesn't need the one-size-fits-all command-line options and it doesn't have to do any linking; directives in the assembled file handle almost everything:
<prompt>:avra -I <librarypath> test.S -o test.hex -l test.lst
...literally does the work of four separate functions in my previous Makefile, so you get all these files from one command:
test.hex - the uploadable file, already in Intel hex
test.eep.hex - the EEPROM file, if needed
test.lst - disassembled list file for sanity checking
test.obj - whether you need the object file or not
My current assembly Makefile, still generic but without C code support:
------------------------------------------------------------------------------------------------------
P=test
UC=t26
UPNUM=0
APRG=arduino
BUPATH=./back
VERSION=01
TS=`date +%H%M%S-%Y`
ifdef VER
TS:=$(VER)
endif
ifdef PRG
P:=$(PRG)
endif
ifdef MCU
UC:=$(MCU)
endif
ifdef UPN
UPNUM:=$(UPN)
endif
UPORT=/dev/ttyACM$(UPNUM)
AFLAGS=-I /home/zenas/mylib/
AS=avra
DUDE=avrdude
$(P).hex: $(P).S
cp $(P).S $(BUPATH)/$(P)_$(TS).S; $(AS) $(AFLAGS) $(P).S -o $(P).hex -l $(P).lst
hex: $(P).hex
$(P): $(P).hex
toucha:
touch *.S
clean:
rm *.hex; rm *.obj; rm *.cof; rm *.lst
upload: $(P).hex
$(DUDE) -P $(UPORT) -p $(UC) -c $(APRG) -b 19200 -U flash:w:$(P).hex
get:
$(DUDE) -P $(UPORT) -p $(UC) -c $(APRG) -b 19200 -U flash:r:$(UC)-$(TS)-get.hex:i
--------------------------------------------------------
With all that streamlining, I went ahead and added the 'upload' and 'get' routines using avrdude. Both of those functions assume you are using an Arduino connected to a USB port as a programmer. You could probably also use it to put assembly code directly on an Arduino, if you really wanted to do that...
Since the whole idea isn't to go blind, or broke, or murder my cat before I get a real project finished...I think I've found something tolerable to use for those functions where assembly on the bare metal is the best solution. avra meets all the qualifications; we can always use AVR-libc for C stuff, as it was intended, and look at the disassembly listings occasionally if we want to poach some of the tricks gcc might use to optimize AVR code; the 'msec_delay' macro above was specifically stolen from gcc output, for instance.
At this point, if you are as frustrated with all assembler gyrations as I am, you would wonder if all this pain over the best and most pious AVR assembler is really worth the trouble. avr-as and the GNU toolchian are all very well, but after wrangling with EEPROM programming for another few days, I was beginning to worry about my sanity.
While doing some more futile googling to figure out what I was doing wrong...I discovered that there were at least two more AVR assemblers for Linux beyond the GNU toolchain: 'Tom's AVR Assembler' and 'avra'. They are both a bit old; 'tavrasm' was last maintained in 2005, and the last activity with 'avra' on Sourceforge was in 2010. But the big difference: BOTH are intended specifically to assemble Atmel's avrasm-formatted files, and both include a superset of Atmel's assembler directives. The latter of which, as it turns out, I am pretty familiar.
In my last post I documented my frustrations with the 'AVR-libc' header file gibberish, and demonstrated how to adapt Atmel's include files to work with avr-as, so that using the other garbage wasn't strictly necessary. With a lot of trial-and-error, I managed to get almost everything to work...except a fully useful way to define EEPROM data and link addresses that would point to it. I could handcode a .hex file (ech). Or create a separate EEPROM file (faking in the data as if it was in program space), assemble it, then look at the list file and add constants to the main program file that refer to the right addresses (double ech).
Ugly. Non-standard. Not very portable.
Back in Atmel land...
And then I discovered tavrasm, and realized that I could return to a more sensible assembler syntax that was AVR-specific, use the standard Atmel *.inc files...errrrrr...sort of. But then the honeymoon ended, the romance was over....
Yeah. Tom Mortensen last worked on his assembler in 2005. His 'macro in macro' expansion of the assembler directives was just a BIT undocumented. I spent half a day looking vainly for code examples, experimenting with conditional assembly directives that didn't work, wondering why there weren't any command line options to generate a .list file even though there were documentation examples and directives for it.....The list of devices supported by tavrasm is/was VERY limited: it did NOT include any ATtiny chips, among other things...and there are a few instructions on the ATtiny (like MOVW, LPM <register>, Z+ and, more critically, SPM) that are too good to give up.
To add the basic support for the ATtinys (sizes of the various memory spaces), I edited the device header file and recompiled, but there wasn't any easy way, short of learning yacc and lex again, then hacking through Tom's code and Atmel's part docs, to add support for the newer instructions. Arrrrrrr....another afternoon wasted.
So I turned to avra, and all my problems went away, including the vast frustrations I was beginning to have with Tom's macros.
Here's the 'avra' version of Blink26.S:
--------------------------------------------------------
;
; blink26.S
;
; ** LOAD IO DEFS FOR THE RIGHT DEVICE
;
.nolist
.include "avrinct/avrcommon.inc"
.include "avrinct/tn26def.inc"
;
; ** GET ME SOME HANDY MACROS **
;
.include "avrinct/macros.inc"
;
; *** DEFINES HERE ***
;
.list
.equ F_CPU = 8000 ; 8Mhz clock, for msec_delay define in 'macro.inc'
.equ OUTPIN = PA7 ; for t26
.equ IO_OS = 0x20 ; for lds/sts with i/o ports, e.g 'lds r2, (IO_OS + TCNT0)'
;
; *** SRAM RESERVES HERE ***
;
.dseg
; FOR example:
;<datalabel>: .byte 1 ; to reserve 1 byte in SRAM for <datalabel>
;
; *** RESETS, IVECTORS, AND MAIN PROGRAM ***
;
.cseg
.org 0x0000
reset:
rjmp main; *** tn26 interrupt vector names, just fer handy ***
reti ; 0001 INT0_vect
reti ; 0002 PCI0_vect
reti ; 0003 TIMER1_OC1A_vect
reti ; 0004 TIMER1_OC1B_vect
reti ; 0005 TIMER1_OVF_vect
reti ; 0006 TIMER0_OVF_vect
reti ; 0007 USI_START_vect
reti ; 0008 USI_OVF_vect
reti ; 0009 EERDY_vect EEPROM Ready
reti ; 000A ACOMP_vect Analog Comparator
reti ; 000B ADC_vect ADC Conversion Complete
;
;
main:
spsetup r16, RAMEND ; use the macro to set up the stack
sbi DDRA, OUTPIN ; set pin 'OUTPIN' on port A to high for OUTPUT
ldi ZL, low(array * 2) ; to calc address right for LPM later
ldi ZH, high(array * 2) ; ditto
blink:
sbi PORTA, OUTPIN ; then drive it HIGH
lpm r20, Z
b2:
msec_delay 1, r17, r18, r19 ; 1 ms wait * r20 times
dec r20
brne b2
cbi PORTA, OUTPIN ; then LOW...
lpm r20, Z
b3:
msec_delay 1, r17, r18, r19 ; 1 ms wait * r20 times
dec r20
brne b3
adiw Z, 1
lpm r20, Z
tst r20
breq end
rjmp blink;
;
end:
rjmp end;
; *** MAIN PROGRAM END ***
;
; PROGRAM MEMORY DATA HERE
;
array: .db 10,250,20,240,30,230,40,220,50,210,60,200,70,190,80,180,90,170,100,160,110,150,120,140,130,0
;
;
;
--------------------------------------------------------
'avra' has excellent documentation, particularly the macros; there are many, many code examples and the expansion from Atmel's basic functionality allows
Things in the above code are just clear, not near as much mud. We have .dseg, .cseg, and .eseg directives; a very clear difference between register (.def) labels and symbolic constants (.equ); runtime constant expansions using .set, excellent conditional compilation directives; .list and .nolist for disassembly and automatic generation of Intel hex code, list and EEPROM hex files.
Now I can use the straight Atmel *.inc files with almost no customization, or even use the conditional directives to make them even more handy. The inclusion of 'avrcommon.inc' takes care of some of the universal constants and macros, including stack initialization.
Macros are a bit easier, and a lot more powerful; here's part of my rewritten utility file:
--------------------------------------------------------
;*
;* macros.inc file
;*
;*
;
...
...
;
;
; ***
; *** inline millisec delay loop
; ***
; *** max is about 11000
; *** !!and be sure to define F_CPU in main program for both like below!!
;
;.equ F_CPU, <CPU mhz * 1000> (e.g. for 8Mhz use 8000)
;
;
; delay = @0, ra = @1, rb = @2, rc = @3
.macro msec_delay
ldi @1, ((((@0 * F_CPU - 1) % 327680) % 1280) / 5)
ldi @2, (((@0 * F_CPU - 1) % 327680) / 1280)
ldi @3, ((@0 * F_CPU - 1) / 327680)
m_%:
subi @1, 0x01
sbci @2, 0x00
sbci @3, 0x00
brne m_%
nop
.endmacro
; ***
--------------------------------------------------------
Since avra is AVR-specific, the Makefile is REALLY streamlined. avra doesn't need the one-size-fits-all command-line options and it doesn't have to do any linking; directives in the assembled file handle almost everything:
<prompt>:avra -I <librarypath> test.S -o test.hex -l test.lst
...literally does the work of four separate functions in my previous Makefile, so you get all these files from one command:
test.hex - the uploadable file, already in Intel hex
test.eep.hex - the EEPROM file, if needed
test.lst - disassembled list file for sanity checking
test.obj - whether you need the object file or not
My current assembly Makefile, still generic but without C code support:
------------------------------------------------------------------------------------------------------
P=test
UC=t26
UPNUM=0
APRG=arduino
BUPATH=./back
VERSION=01
TS=`date +%H%M%S-%Y`
ifdef VER
TS:=$(VER)
endif
ifdef PRG
P:=$(PRG)
endif
ifdef MCU
UC:=$(MCU)
endif
ifdef UPN
UPNUM:=$(UPN)
endif
UPORT=/dev/ttyACM$(UPNUM)
AFLAGS=-I /home/zenas/mylib/
AS=avra
DUDE=avrdude
$(P).hex: $(P).S
cp $(P).S $(BUPATH)/$(P)_$(TS).S; $(AS) $(AFLAGS) $(P).S -o $(P).hex -l $(P).lst
hex: $(P).hex
$(P): $(P).hex
toucha:
touch *.S
clean:
rm *.hex; rm *.obj; rm *.cof; rm *.lst
upload: $(P).hex
$(DUDE) -P $(UPORT) -p $(UC) -c $(APRG) -b 19200 -U flash:w:$(P).hex
get:
$(DUDE) -P $(UPORT) -p $(UC) -c $(APRG) -b 19200 -U flash:r:$(UC)-$(TS)-get.hex:i
--------------------------------------------------------
With all that streamlining, I went ahead and added the 'upload' and 'get' routines using avrdude. Both of those functions assume you are using an Arduino connected to a USB port as a programmer. You could probably also use it to put assembly code directly on an Arduino, if you really wanted to do that...
Since the whole idea isn't to go blind, or broke, or murder my cat before I get a real project finished...I think I've found something tolerable to use for those functions where assembly on the bare metal is the best solution. avra meets all the qualifications; we can always use AVR-libc for C stuff, as it was intended, and look at the disassembly listings occasionally if we want to poach some of the tricks gcc might use to optimize AVR code; the 'msec_delay' macro above was specifically stolen from gcc output, for instance.
Sunday, September 25, 2016
Using and abusing the GNU assembler for ATtiny programming...
One of the big troubles with programming AVRs on Linux, as you might have gathered from previous posts, is that you have to use the GNU compiler family and the AVR-libc library, along with your favorite editor and avrdude and/or the ArduinoISP to finish the job.
ALL this is tolerable, even comfortable, if you are a Linux geek and a command line junkie. At least if you are writing in C; you can even use the Arduino IDE and libraries, if your target μc is supported in it. Or you can do it the 'hard way', with straight AVR-libc, gcc, and the command line. Not too painful, as I've covered previously....
GNU 'as' docs are sh****
Once you decide you want to start doing some straight assembly, though...not so much fun. I have spent the last week just reading The GNU Assembler documentation and various forum posts, looking for 'best practice' or 'just plain works' code examples. Because of the vagaries of FSF 'free software' license policies (which apply to the GNU compiler docs, fortunately or unfortunately as Alice might say), the support for avr-as is stark, at best, and there is VERY little code to examine out in the wild.
The attitude seems to be....you REALLY should be programming in C, or at worst using the (awful) inline assembler. REAL assembler is just there in case someone like to torture themselves with AVR-libc's comprehensive but extremely obscure symbol/macro naming conventions and complex header file hierarchy.
Atmel's attitude toward support of other assemblers is worse, as is to be expected. They would rather you use AVR Studio and avrasm, which has entirely different preprocessor syntax. Atmel does assert a minimum of support for the GNU AVR toolchain, and even have some documentation on using it...but it is even sketchier than the GNU docs, outright wrong about certain preprocessor directives, and completely out to sea on code and data sectioning among other things.
Even though I despise coding under Windows, I LIKE avrasm and Atmel's take on assembly. I have ALWAYS liked it. The syntax was VERY well documented, it is very clear where everything goes, and when, and why wouldn't it be, since Atmel had a lot to do with developing it? Unlike AVR-libc, Atmel took a more incremental approach, assuming that anyone doing assembly would be more likely to have a 'bag-of-tricks' set of macros for common tasks (like timed delays) and would generally write software toward a specific subset of the AVR family (in my case the tiny26, tinyx4, tinyx5, and tinyx61 devices) rather than everything.
The Atmel/avrasm device 'include' files are particularly clearly put together; it occurred to me right away that, with a bit of search and replace, I could convert them to avr-as syntax and get the best of both worlds. Or the best of THREE worlds, since I wouldn't have to rely on the AVR-libc header files either, which are really oriented toward C rather than AVR assembler anyway.
And the last hurdle, therefore...
AVR-libc header files are gibberish...
The latter is only a personal opinion, but....I do NOT plan on using assembler for the big ATmega's and the Arduinos. Those devices have a LOT of memory and peripheral support, so why not be comfortable and do it all in C? For the kind of tasks it makes sense to implement on an Arduino, we can sit back and just include the libraries we need and hope the magic works, not worrying too much about what's going on in there unless we run into serious problems.
But there's a good reason that Arduino C and the Arduino IDE exist in the first place; bare-metal C programming on an AVR is not for the faint of heart. Despite the abundance of excellent code examples out there and the well-intentioned effort by the maintainers to create a device-agnostic environment, even the the simplest tasks (such as the 'demo.c' program in the AVR-libc docs) require a knowledge of the hardware that few beginners are likely to have.
With assembly, using the AVR-libc envirnoment is even more painful. In assembly, you can't ignore the device-compatibility gibberish in all those files. The gyrations needed to get one set of header files to work with the dozens of different AVR's, even in C, require a LOT of esoteric macro witchcraft that I may understand...eventually. In the next year or so. Or maybe when I retire.
But...we aren't talking about ATmegas, anyway, and why not? The AVR ATtinys might not be able to do a hardware multiply in four clock cycles, but do we really need that to start with? The whole idea behind using a microcontroller for these tasks is that they are more space- and resource- and power-efficient than a large TTL or analog network, in addition to being more flexible, programming-wise. But going too far with this, making an MCU do everything, can defeat the purpose of using them in the first place. With 32K of memory there is little incentive (beyond the limitations of the hardware itself) to code efficiently. More, there are only so many pins and so many counters and so many processor cycles available on an AVR. Why not develop a sensible multiprocessor control system, with good communications between sub-systems as a priority?
IN other words...why not use ATmegas or small Linux platforms like the Pi to do the supervisory tasks that require space, flexibility and horsepower, and the small devices like the excellent stable of ATtinys to do the low-end sensor and motor control gruntwork? C is perfectly fine for the big picture baton-waving...but assembly might be a very handy tool to have if we want the best results out at the ragged edge of reality, where every clock cycle and bit counts.
If we want all this, and Linux too, we need a simple set of programming tools that work fairly seamlessly with the Arduino and AVR-libc environments, but don't inherit the narrow device support of the former or the one-size-fits all limitations of the latter. We want a set of device-centric headers, some powerful macros, and a functional code base that will allow us to easily write EXACTLY to the device and the purpose we need. And we want as much automation as we can devise so that we CAN concentrate on those goals.
In short:
1. Device-specific include files, not generic ones.
2. Familiarity with the minimum subset of assembler directives that the GNU assembler furnishes to get the job done, so that our code is clear and easy to adapt.
3. A set of rules and scripts that allows us to deploy our code quickly.
Firstly...
To get started, I needed to convert a subset of the Atmel '<device>def.inc' files to a format avr-as likes. AVR Studio/avrasm use now use an XML format for newer versions of the Atmel programming environment, but I found a set of older files here.
With some chopping, cutting, search-and-replace, I was is business. Below, ferinstance, are some snips from my massaged 'tn26def.inc' include file:
-----------------
... (other stuff)
...
.equ PINA, 0x19
.equ PORTB, 0x18
.equ DDRB, 0x17
.equ PINB, 0x16
.equ USIDR, 0x0f
.equ USISR, 0x0e
.equ USICR, 0x0d
.equ ACSR, 0x08
.equ ADMUX, 0x07
.equ ADCSRA, 0x06
.equ ADCH, 0x05
.equ ADCL, 0x04
; ***** BIT DEFINITIONS **************************************************
; ***** AD_CONVERTER *****************
; ADMUX - The ADC multiplexer Selection Register
.equ MUX0, 0 ; Analog Channel and Gain Selection Bits
.equ MUX1, 1 ; Analog Channel and Gain Selection Bits
.equ MUX2, 2 ; Analog Channel and Gain Selection Bits
.equ MUX3, 3 ; Analog Channel and Gain Selection Bits
.equ MUX4, 4 ; Analog Channel and Gain Selection Bits
.equ ADLAR, 5 ; Left Adjust Result
.equ REFS0, 6 ; Reference Selection Bit 0
.equ REFS1, 7 ; Reference Selection Bit 1
...
... (and so on...)
...
; ***** CPU REGISTER DEFINITIONS *****************************************
.equ XH, r27
.equ XL, r26
.equ YH, r29
.equ YL, r28
.equ ZH, r31
.equ ZL, r30
; ***** DATA MEMORY DECLARATIONS *****************************************
.equ FLASHEND, 0x03ff ; Note: Word address
.equ IOEND, 0x003f
.equ SRAM_START, 0x0060
.equ SRAM_SIZE, 128
.equ RAMEND, 0x00df
.equ XRAMEND, 0x0000
.equ E2END, 0x007f
.equ EEPROMEND, 0x007f
.equ EEADRBITS, 7
--------
All relatively clear, as far as mud goes. In Atmel's assembler, the '.equ' directive uses an '=' as a delimiter rather than a comma, so a simple search-and-replace fixed 95% of the file; I removed all the # (C-style) directives and fixed some tabs for cosmetic reasons. All of the dozen files I needed for the ATtinys took me a total of an hour to fix up for avr-as.
So at this point, I could at least have all the shortcut symbols I would need to get at the I/O ports and registers, plus some handy constants. So what's next?
Of the dozens of preprocessor directives that the GNU docs describe (sort of), you really only need a small subset:
.equ - to define symbolic constants
.include - to include other files
.section - to define non-code sections with non-standard names, like...
.section .bss - to reserve space in the SRAM data
.text - to define the code area
.global - to make sure an external program can see a label
.macro/.endm - to define fancy code shortcuts
.if/.endif - more fancy stuff in macros, condtional assembly, and....
...and a gaggle of data description things like:
.byte, .word, .quad, .ascii, .asciz, .octa, .space
...for space reservation in data areas. See below for a working example; a simple tiny26 program that uses a macro to generate an inline 'microsecond_delay' function, and reads a array of bytes from the tail end of program memory to sequentially blink an LED in a set pattern:
The blink26.S program
;
; blink26.S
;
; ** LOAD IO DEFS FOR THE RIGHT DEVICE
;
.include "avrinc/tn26def.inc"
;
; ** GET ME SOME HANDY MACROS **
;
.include "avrinc/macro.inc"
;
; *** DEFINES HERE ***
;
.equ F_CPU, 8000 ; 8Mhz clock, for msec_delay define in 'macro.inc'
.equ OUTPIN, PA7 ; for t26
.equ IO_OS, 0x20 ; for lds/sts with i/o ports, e.g 'lds r2, (IO_OS + TCNT0)'
;
; *** SRAM RESERVES HERE ***
;
.section .bss
; FOR example:
;<datalabel>: .byte 1 ; to reserve 1 byte in SRAM for <datalabel>
;
; *** RESETS, IVECTORS, AND MAIN PROGRAM ***
;
.text
.global reset ; don't know if '.global' needed but...
reset:
rjmp main; *** tn26 interrupt vector names, just fer handy ***
reti ; 0001 INT0_vect
reti ; 0002 PCI0_vect
reti ; 0003 TIMER1_OC1A_vect
reti ; 0004 TIMER1_OC1B_vect
reti ; 0005 TIMER1_OVF_vect
reti ; 0006 TIMER0_OVF_vect
reti ; 0007 USI_START_vect
reti ; 0008 USI_OVF_vect
reti ; 0009 EERDY_vect EEPROM Ready
reti ; 000A ACOMP_vect Analog Comparator
reti ; 000B ADC_vect ADC Conversion Complete
;
;
.global main
main:
setupsp r16, RAMEND ; use the macro to set up the stack
sbi DDRA, OUTPIN ; set pin 'OUTPIN' on port A to high for OUTPUT
ldi ZL, lo8(array)
ldi ZH, hi8(array)
blink:
sbi PORTA, OUTPIN ; then drive it HIGH
lpm r20, Z
b2:
msec_delay 1, r17, r18, r19 ; 1 ms wait * r20 times
dec r20
brne b2
cbi PORTA, OUTPIN ; then LOW...
lpm r20, Z
b3:
msec_delay 1, r17, r18, r19 ; 1 ms wait * r20 times
dec r20
brne b3
adiw Z, 1
lpm r20, Z
tst r20
breq end
rjmp blink;
;
end:
rjmp end;
; *** MAIN PROGRAM END ***
;
; *** SUBROUTINES OR INTERRUPT SERVICE ROUTINES HERE ***
;
; (don' have none o' that at the moment)
;
; *** PROGRAM MEMORY DATA HERE ***
;
array: .byte 10,250,20,240,30,230,40,220,50,210,60,200,70,190,80,180,90,170,100,160,110,150,120,140,130,0
;
; end of test.s
--------------------
Everything you really need is right there, unless you are going to start moving code blocks around and deal with a boot loader; if you need to load something into EEPROM you could either hand-code a hex file that would work, or...more when I know more.
Among other things, note the very tail-end of the program, where the 'array:' label is used to mark off a list of byte values. Nothing special is done with directives (other than .byte to make it clear what size and type the data are) to reserve this area; it's just slapped right to the end of the program after the last of the code.
You can even use '.org <addr>' before the label to move the memory location this label is pointing at around, but....why not just leave it where it is?
A few minor caveats with putting data in program memory....among other things you can only use the Z register (r31:r32 jammed together as a 16-bit register) to access it indirectly, using the LPM instructions. LPM also has a very limited number of addressing modes, depending on the device, and takes three cycles to get the data to the register. Use the 'lo8()' and 'hi8()' built-in macros to carve the 16-bit program address into something you can load into the two halves (ZL and ZH) of the Z register.
A more interesting problem...program memory is organized 'word-wise', in other words 16 bits at a time, so any data you put out there in program space has to align with word boundaries; if you have an odd number of bytes in a chunk of data, you may need to add a '.space 1' directive before or after it to line the label address up with right...avr-as will complain if you don't.
----------------
Here is a snip from my 'macro' file, just so you get a taste of how those work:
;
; define SPL if only SP is defined
;
.ifndef SPL
.equ SPL, SP
.endif
;
; setup stack pointer
;
.macro setupsp r,x
ldi \r, \x % 256
out SPL, \r
.if \x > 256
ldi \r, \x / 256
out SPH, \r
.endif
.endm
;
;
;
;***
; 8-bit hex to ascii-coded hex (subroutine)
;
; use 4 registers above r15!!
;
.macro hex2ascii_m in, tmp, o1, o2
hex2ascii:
mov \tmp, \in
andi \tmp, 0x0f ; get bottom nybble
ldi \o1, 48
add \o1, \tmp
cpi \o1, 58
brlo h2ach1
subi \o1, (-7)
h2ach1:
mov \tmp, \in
swap \tmp
andi \tmp, 0x0f ; get top nybble
ldi \o2, 48
add \o2, \tmp
cpi \o2, 58
brlo h2ach2
ldi \tmp, 7
subi \o2, (-7)
h2ach2:
ret
.endm
;
;
; ***
; *** inline millisec delay loop
; ***
; *** max is about 11000
; *** !!and be sure to define F_CPU in main program for both like below!!
;
;.equ F_CPU, <CPU mhz * 1000> (e.g. for 8Mhz use 8000)
;
; NOTE: label '1' is necessary if you are going to use this macro more than
; once in a give program. The '1b' branch is shorthand for 'branch to the
; '1' label BEFORE this instruction'. '1f' would mean to go to the '1' label
; FOLLOWING the branch.
;
.macro msec_delay delay, ra, rb, rc
ldi \ra, ((((\delay * F_CPU - 1) % 327680) % 1280) / 5)
ldi \rb, (((\delay * F_CPU - 1) % 327680) / 1280)
ldi \rc, ((\delay * F_CPU - 1) / 327680)
1:
subi \ra, 0x01
sbci \rb, 0x00
sbci \rc, 0x00
brne 1b ; see note above
nop
.endm
----------------------------
There are a few different things going on here...
'as' macros are actually quite sophisticated, and I've used them above in two different ways. If you want to just to define a subroutine:
; 8-bit hex to ascii-coded hex (subroutine)
;
; use 4 registers above r15!!
;
.macro hex2ascii_m in, tmp, o1, o2
hex2ascii:
mov \tmp, \in
andi \tmp, 0x0f ; get bottom nybble
ldi \o1, 48
add \o1, \tmp
...
...shows the way. First, notice that this macro has four parameters; and it is pretty obvious from the way the 'mov' instruction is used below that 'tmp' and 'in' refer to two of the AVR registers. And it should also be pretty obvious that to get at a parameter inside the macro you need to put a slash in front of it, e.g. '\tmp'.
But you can also pass constants, and use C-style expressions to calculate new ones:
;
.macro msec_delay delay, ra, rb, rc
ldi \ra, ((((\delay * F_CPU - 1) % 327680) % 1280) / 5)
ldi \rb, (((\delay * F_CPU - 1) % 327680) / 1280)
ldi \rc, ((\delay * F_CPU - 1) / 327680)
'delay' is passed in, and then a BUNCH of math is done to figure out how to set the three registers used for the cascading countdown.
'msec_delay' generates inline code, rather than a subroutine...and therfore you may notice some weirdness farther down:
... (continued from above)
...
ldi \rc, ((\delay * F_CPU - 1) / 327680)
1:
subi \ra, 0x01
sbci \rb, 0x00
sbci \rc, 0x00
brne 1b ; see note above
nop
.endm
How the hell does the branch get to the '1' label if it refers to it as '1b'? What the hell does THAT mean? Hmmm...
Okay. We are going to use this 'msec_delay' more than once, presumably, in a fairly complicated program, so when this macro gets expanded by the compiler...it AIN'T going to like seeing multiple '1:' labels. SOOO, the rule is that, if you use a digits only label in a macro, you reference either the 'next' or the 'previous' instance of that label, and when the macro is expanded the actual labels are removed and numerical offsets put in on the branches and jumps in their place. Therefore...
brne 1b
..means 'branch if not equal to the first label '1:' BEFORE this location (thus the 'b'). If you need to branch or jump FORWARD, you would say 'brne 1f', ferinstance.
And what about conditional things? Try this out:
; setup stack pointer
;
.macro setupsp r,x
ldi \r, \x % 256
out SPL, \r
.if \x > 256
ldi \r, \x / 256
out SPH, \r
.endif
.endm
;
Almost self explanatory. The macro is intended to set up the stack pointer. It is named 'setupsp' and has two parameters, 'r' (which should be a register), and 'x', which looks to be used as a constant. This line:
.if \x > 256
...looks an awful lot like it is CHECKING that constant to see if it is just so large. If RAMEND is plugged into 'x', you are probably guessing that we are trying to see if the SRAM size for the device needs a 16-bit or only an 8-bit register to store the stack pointer...and that is what this does. If RAMEND is larger than 256, then two extra instructions are generated to init SPH as well as SPL, with the high bytes of RAMEND.
One of the few things that the GNU docs DO include are a VERY few examples of macros, so those pages are worth a perusal; and the many variations of '.if' are also worth a look.
Make? Ugh? Ahh....
Lastly...it would be nice to have an automated way to compile/assemble our programs, so we aren't stuck memorizing a lot of stupid and easy-to-screw-up compiler options...how about a nice Makefile?
--------------------
P=test MCU=attiny26
BUPATH=./back
TS=`date +%H%M%S-%Y`
ifdef VER
TS:=$(VER)
endif
ifdef UC
MCU:=$(UC)
endif
ifdef PRG
P:=$(PRG)
endif
CFLAGS=-g -mmcu=$(MCU) -Os
AFLAGS=-g -mmcu=$(MCU)
LDLIBS=-I/home/zenas/mylib/
CC=avr-gcc
OC=avr-objcopy
AS=avr-as
ASL=avr-ld
DIS=avr-objdump
$(P).ao: $(P).S
cp $(P).S $(BUPATH)/$(P)_$(TS).S; $(AS) $(AFLAGS) $(LDLIBS) $(P).S -o $(P).ao
$(P).elf: $(P).ao
$(ASL) -o $(P).elf $(P).ao
$(P).o: $(P).c
cp $(P).c $(BUPATH)/$(P)_$(TS).c; $(CC) $(CFLAGS) $(LDLIBS) $(P).c -o $(P).o
$(P).ahex: $(P).elf
$(OC) -j .text -j .data -O ihex $(P).elf $(P).ahex
$(P).hex: $(P).o
$(OC) -j .text -j .data -O ihex $(P).o $(P).hex
$(P)-eeprom.hex: $(P).o
$(OC) -j .eeprom --change-section-lma .eeprom=0 -O ihex $(P).o $(P)-eeprom.hex
dump: $(P).o
$(DIS) -h -S $(P).o > $(P).lst
dumpa: $(P).elf
$(DIS) -h -S $(P).elf > $(P).lst
ahex: $(P).ahex
hex: $(P).hex
eeprom: $(P)-eeprom.hex
touchc:
touch *.c
toucha:
touch *.S
clean:
rm *.o; rm *.ao; rm *.ahex; rm *.hex; rm *.elf;
--------------------
Just plug in PRG, UC, and VER variables on the command line if you want to use this file generically; by default it compiles/assembles 'test.S' or 'test.c' for the ATtiny26.
Note that there is an extra 'linker' stage (invoked with avr-ld) needed to get the final uploadable code! One of my early mistakes took a very long time to fix: I was generating both the final '.ahex' file (to burn to the device) and the disassembly '.lst' file from the output object '.ao' file from avr-as, NOT the '.elf' file that the linker produces....while all of the symbolic constants and macros unrolled right, none of the jumps or branches or labels did anything useful.
The 'make dumpa' routine is particularly handy (once you are dumping the right thing anyway); you can see exactly what the assembler has done after all the macros and constants have been resolved, and where everything will end up when you upload to your device. It will save you an immense amount of headache if you can grit your teeth and familiarize yourself with the disassembled product of avr-gcc and avr-as. Among the bonuses: you can discover some very handy code shortcuts by poaching the output of avr-gcc...just write up something simple in C, let avr-gcc have it, then use the 'make dump' routine and see what the final product looks like. avr-objdump very helpfully includes the C code segment used to generate each piece of object code, and as long as the task isn't too complicated you can tease out the jewels.
Incidentally, the 'hex' and 'ahex' routines backup the .c/.S file before it's compiled, shoving a time stamp on it in case you want to go back and see what worked before...the backed up files ends up in the 'back' folder by default.
The makefile works with both avr-libc 'C' files and assembler 'S' files:
make clean --- gets everything ready
make hex PRG=cblah --- compiles, links, and genrates a .hex file of cblah.c
make ahex PRG=ablah --- assembles, links, and genrates a .hex file of ablah.S
make dumpa PRG=ablah --- creates a disassembled version of ablah.S
make dump PRG=cblah --- ditto for object file of cblah.c
..and so on. I still upload stuff to the device by hand with 'avrdude',
avrdude -p t26 -P /dev/ttyACM0 -c arduino -b 19200 -U flash:w:ablah.ahex
...but that could probably go in there too, eventually.
Wednesday, August 17, 2016
tiny45 Programming through an Arduino, Part 3
...and the last lap? We would like to use an Arduino as a generic AVR programming device: leverage the low-end tools that come with Linux and the Arduino IDE to do stuff the gods of high-level programming never intended.
---------------
And there are other benefits of programming on the bare metal this way, if one's willing to do a little homework. It is certainly cheaper: a bare Mega328 costs $5 or less, as compared to $10 for the cheapest possible Arduino; and the tiny45 can be bought in volume at $0.99 a device! And knowing a little bit more about how AVRs work, and how the programming tools do their job, can't hurt.
One of the problems with the Arduino IDE is it DOES bloat the code a bit; this isn't a problem on most Arduinos, which have a relatively large amount of flash and static RAM compared to the size of the bootloader and the more routine sample programs. On the older and smaller devices, though, program and static RAM space is a major issue.
/*
Blink for tiny45
*/
//
// NOTES for uploading: use the ATtinyCore Universal library
// (http://drazzy.com/package_drazzy.com_index.json)
// with ATtiny x5 series, then the chip, speed, etc. and use the
// Arduino/Leo as ISP (ATtiny) as the programmer.
//
// You can also change the internal clock, B.O.D., voltage
// standard fuses by choosing the option
// you want and choosing 'Burn Bootloader'. Be Careful!
//
//
#define BLINKPIN 1
// the setup function runs once when you press reset or power the board
void setup() {
// initialize ATtiny45 PB1 as an output.
pinMode(BLINKPIN, OUTPUT);
}
// the loop function runs over and over again forever
void loop() {
delay(500);
digitalWrite(BLINKPIN, HIGH); // turn the LED on (HIGH is the voltage level)
delay(500); // wait for a second
digitalWrite(BLINKPIN, LOW);
}
This is about as simple a program as you can write on an AVR, no matter how you do it....and with the IDE you still end up using 808 bytes of program space and 9 bytes of RAM. I suspect that the delay() function takes up the bulk of that.
-----------
A few days earlier, I'd spent about half a day installing and cramming the WinAVR package on an old Dell Inspiron 7000 (Windows 2000, 128 MB of RAM, 4GB HD!), because it was one of the few PC's my junk pile furnished with a useful parallel port. Up until I discovered the Arduino I had been using AVR assembler, Atmel's AVR Studio on a PC, and an old homebuilt SP12 programmer for all my ancient AVR devices. I have several AT90S2313's and 8515's, a mega16, a mega8515, and a number of tiny26's (ancestor of the 261/461/861), all of which I wanted to continue to use.
I didn't have too much trouble working with WinAVR, and in the process managed to muddle through the rather complicated 'demo.c' program on the AVR-libc support site.....which example DOES (more or less), implement the 'fader' program in my previous post on this subject. I modified the demo, working over the various macros embedded in the 'iocompat.h' file, and got it to work with the PWM stuff on the tiny26. All good....
But...I didn't want to lug out that old clunk programmer and yet another computer when I wanted to use my old uC's, dammit! And I wanted to use Linux as well...so what next?
While using the WinAVR stuff, I used the command line, as illustrated in the AVR-libc tutorials for 'demo.c' above, to do everything by hand, like so:
Insp7000 c:\code\> avr-gcc -g -Os -mmcu=attiny26 demo.c -o demo.o
Insp7000 c:\code\> avr-objcopy -j .text -j .data -O ihex demo.o demo.hex
This built me a 300+ byte file in 'demo.hex', and with the SP12 dongle I uploaded it like so:
Insp7000 c:\code\> avrdude -p t26 -c sp12 -U flash:w:demo.hex
(Notice that avr-gcc and AVR-libc know the ATtiny26 as 'atttiny26', while avrdude believes it is called 't26'. Gotta keep our wits about us, kids...)
Which was probably no more that 70-80 bytes on the tiny26....Impressive! And more to the point, it worked. And note that the much simpler 'blink' program for the tiny45 above, letting the Arduino IDE do the heavy lifting, was 10 times the size.
Size matters; the tiny26 has 2K of flash, while the Mega328P (the core of the Arduino Uno) has 32K to waste. We can do better, for not much in the way of pain.
--------------
So, this last example should give us some proper hackerly ideas, right? Avr-gcc and avrdude are already there with the Arduino IDE installed; and unlike the IDE, avrdude has the full pin descriptions and functions defined for EVERY AVR clear back to the microcontroller stone age. In theory, at least, if we have the ArduinoISP software loaded up on the Uno, we should be able to make the Arduino program ANY AVR device, as long as AVR-libc and avrdude know the device exists.
First, let's look in 'programmers.txt' in the Arduino IDE install folder (<folder>/hardware/arduino/avr/) and see what kind of directives are sent to avrdude for the 'Arduino as ISP' programmer defined there:
arduinoasisp.name=Arduino as ISP
arduinoasisp.communication=serial
arduinoasisp.protocol=stk500v1
arduinoasisp.speed=19200
arduinoasisp.program.protocol=stk500v1
arduinoasisp.program.speed=19200
arduinoasisp.program.tool=avrdude
arduinoasisp.program.extra_params=-P{serial.port} -b{program.speed}
And so we see. Unlike when hitting the Arduino directly through the IDE, we must talk DIRECTLY to the serial.port (not with the pseudodevice 'usb'), as well as define a baud rate to talk through the Arduino (as if it was a serial port itself rather than the destination for the code we want to write).
So that's one piece of the pie. The other piece is....which programmer do we use on the avrdude command line if we aren't going out on the parallel port with the SP12 dongle? Entering 'avrdude -c ?' gives us a list of programmers that it knows about:
Valid programmers are:
2232HIO = FT2232H based generic programmer
4232h = FT4232H based generic programmer
89isp = Atmel at89isp cable
abcmini = ABCmini Board, aka Dick Smith HOTCHIP
alf = Nightshade ALF-PgmAVR, http://nightshade.homeip.net/
arduino = Arduino
arduino-ft232r = Arduino: FT232R connected to ISP
atisp = AT-ISP V1.1 programming cable for AVR-SDK1 from <http://micro-research.co.th/>
atmelice = Atmel-ICE (ARM/AVR) in JTAG mode
atmelice_dw = Atmel-ICE (ARM/AVR) in debugWIRE mode
atmelice_isp = Atmel-ICE (ARM/AVR) in ISP mode
atmelice_pdi = Atmel-ICE (ARM/AVR) in PDI mode
avr109 = Atmel AppNote AVR109 Boot Loader
avr910 = Atmel Low Cost Serial Programmer
avr911 = Atmel AppNote AVR911 AVROSP
avrftdi = FT2232D based generic programmer
...... (and so on for a page or two) ......
The only one the looks even close is, well 'arduino', so let's use the command line options we found above in combination with what we've got here, and just try talking to the gadget and see:
mymachine:$avrdude -p t45 -c arduino -P /dev/ttyACM0 -b 19200
avrdude: AVR device initialized and ready to accept instructions
Reading | ################################################## | 100% 0.01s
avrdude: Device signature = 0x1e9206
avrdude: safemode: Fuses OK (E:FF, H:DF, L:E2)
avrdude done. Thank you.
So wow, it just works! That is, indeed, the correct sig for the ATtiny45. The Arduino Uno passes our request right through using the ArduinoISP software (with the jumpered on capacitor across the Reset line) to get to our 'legacy' device.
So now, how about a new version of the blink program? Let's see here:
/*
test blink program to make sure know what am doing with
avr-gcc and such.
for tiny45 through Arduino as programmer?
*/
#include <inttypes.h>
#include <avr/io.h>
#define F_CPU 8000000UL // 8Mhz on t45 - this needs to be defined BEFORE including util/delay.h
#include <util/delay.h> // for _delay_us(double) and _delay_ms(double)
// handy stuff from Arduino.h
#define HIGH 0x1
#define LOW 0x0
#define INPUT 0x0
#define OUTPUT 0x1
#define INPUT_PULLUP 0x2
#define lowByte(w) ((uint8_t) ((w) & 0xff))
#define highByte(w) ((uint8_t) ((w) >> 8))
#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
#define bitWrite(value, bit, bitvalue) (bitvalue ? bitSet(value, bit) : bitClear(value, bit))
#define BLINKPIN 0x01 // assume port B for t45
// sorta like setup()
void ioinit(void) {
bitSet(DDRB, BLINKPIN); // set B1 as OUTPUT -- like DDRB |= (1 << BLINKPIN);
}
// and can more or less pretend this is loop()
int main(void) {
ioinit();
for (;;) {
_delay_ms(500.0);
bitClear(PORTB, BLINKPIN); //..or PORTB &= ~(1 << BLINKPIN);
_delay_ms(500.0);
bitSet(PORTB, BLINKPIN); //..or PORTB |= (1 << BLINKPIN);
}
}
// end testblink.c
Compile it with avr-gcc and use avr-objcopy to create the hex file, then avrdude can write it. 100 bytes used total!
So how did I concoct the above? Among other things, I looked for (and at) the main header files that the IDE loads when you create a new program...i.e. at the Arduino.h file, which is stored at <IDE install folder>/hardware/arduino/avr/cores/. LOTS of handy macros in there, and a nice list of other files that are included in all IDE builds by default. Since we are trying to program on the bare metal, making as few assumptions as possible about the device, we can muck and pare at that list a bit, drill down through a few #includes to find what they do and what we need, guess what we don't need. Eventually we can just go with:
#include <inttypes.h>
#include <avr/io.h>
#define F_CPU 8000000UL // 8Mhz on t45 - this needs to be defined BEFORE including util/delay.h
#include <util/delay.h> // for _delay_us(double) and _delay_ms(double
There was one gotcha in my first attempt at this: avr-gcc bitched incessantly that F_CPU was NOT something it knew about, thus the #define for the CPU frequency there....I tried just including util/delay.h without it, and immediate had a wake up call. OBVIOUSLY (right? right?) the delay functions wouldn't work right if they were based on the clock speed of the device, and didn't know what that number WAS. An arbitrary AVR device can't be assumed to have an RTC or a PLL clock built-in, so the only other way to have a time base is to run it off the CPU clock, and....
IN any case...paring the #includes down to a bare minimum shows us what we can safely leave out, while still giving us some nice short cuts, and a handy template for building more complicated stuff later on.
---------------
And there are other benefits of programming on the bare metal this way, if one's willing to do a little homework. It is certainly cheaper: a bare Mega328 costs $5 or less, as compared to $10 for the cheapest possible Arduino; and the tiny45 can be bought in volume at $0.99 a device! And knowing a little bit more about how AVRs work, and how the programming tools do their job, can't hurt.
One of the problems with the Arduino IDE is it DOES bloat the code a bit; this isn't a problem on most Arduinos, which have a relatively large amount of flash and static RAM compared to the size of the bootloader and the more routine sample programs. On the older and smaller devices, though, program and static RAM space is a major issue.
/*
Blink for tiny45
*/
//
// NOTES for uploading: use the ATtinyCore Universal library
// (http://drazzy.com/package_drazzy.com_index.json)
// with ATtiny x5 series, then the chip, speed, etc. and use the
// Arduino/Leo as ISP (ATtiny) as the programmer.
//
// You can also change the internal clock, B.O.D., voltage
// standard fuses by choosing the option
// you want and choosing 'Burn Bootloader'. Be Careful!
//
//
#define BLINKPIN 1
// the setup function runs once when you press reset or power the board
void setup() {
// initialize ATtiny45 PB1 as an output.
pinMode(BLINKPIN, OUTPUT);
}
// the loop function runs over and over again forever
void loop() {
delay(500);
digitalWrite(BLINKPIN, HIGH); // turn the LED on (HIGH is the voltage level)
delay(500); // wait for a second
digitalWrite(BLINKPIN, LOW);
}
This is about as simple a program as you can write on an AVR, no matter how you do it....and with the IDE you still end up using 808 bytes of program space and 9 bytes of RAM. I suspect that the delay() function takes up the bulk of that.
-----------
A few days earlier, I'd spent about half a day installing and cramming the WinAVR package on an old Dell Inspiron 7000 (Windows 2000, 128 MB of RAM, 4GB HD!), because it was one of the few PC's my junk pile furnished with a useful parallel port. Up until I discovered the Arduino I had been using AVR assembler, Atmel's AVR Studio on a PC, and an old homebuilt SP12 programmer for all my ancient AVR devices. I have several AT90S2313's and 8515's, a mega16, a mega8515, and a number of tiny26's (ancestor of the 261/461/861), all of which I wanted to continue to use.
I didn't have too much trouble working with WinAVR, and in the process managed to muddle through the rather complicated 'demo.c' program on the AVR-libc support site.....which example DOES (more or less), implement the 'fader' program in my previous post on this subject. I modified the demo, working over the various macros embedded in the 'iocompat.h' file, and got it to work with the PWM stuff on the tiny26. All good....
But...I didn't want to lug out that old clunk programmer and yet another computer when I wanted to use my old uC's, dammit! And I wanted to use Linux as well...so what next?
While using the WinAVR stuff, I used the command line, as illustrated in the AVR-libc tutorials for 'demo.c' above, to do everything by hand, like so:
Insp7000 c:\code\> avr-gcc -g -Os -mmcu=attiny26 demo.c -o demo.o
Insp7000 c:\code\> avr-objcopy -j .text -j .data -O ihex demo.o demo.hex
This built me a 300+ byte file in 'demo.hex', and with the SP12 dongle I uploaded it like so:
Insp7000 c:\code\> avrdude -p t26 -c sp12 -U flash:w:demo.hex
(Notice that avr-gcc and AVR-libc know the ATtiny26 as 'atttiny26', while avrdude believes it is called 't26'. Gotta keep our wits about us, kids...)
Which was probably no more that 70-80 bytes on the tiny26....Impressive! And more to the point, it worked. And note that the much simpler 'blink' program for the tiny45 above, letting the Arduino IDE do the heavy lifting, was 10 times the size.
Size matters; the tiny26 has 2K of flash, while the Mega328P (the core of the Arduino Uno) has 32K to waste. We can do better, for not much in the way of pain.
--------------
So, this last example should give us some proper hackerly ideas, right? Avr-gcc and avrdude are already there with the Arduino IDE installed; and unlike the IDE, avrdude has the full pin descriptions and functions defined for EVERY AVR clear back to the microcontroller stone age. In theory, at least, if we have the ArduinoISP software loaded up on the Uno, we should be able to make the Arduino program ANY AVR device, as long as AVR-libc and avrdude know the device exists.
First, let's look in 'programmers.txt' in the Arduino IDE install folder (<folder>/hardware/arduino/avr/) and see what kind of directives are sent to avrdude for the 'Arduino as ISP' programmer defined there:
arduinoasisp.name=Arduino as ISP
arduinoasisp.communication=serial
arduinoasisp.protocol=stk500v1
arduinoasisp.speed=19200
arduinoasisp.program.protocol=stk500v1
arduinoasisp.program.speed=19200
arduinoasisp.program.tool=avrdude
arduinoasisp.program.extra_params=-P{serial.port} -b{program.speed}
And so we see. Unlike when hitting the Arduino directly through the IDE, we must talk DIRECTLY to the serial.port (not with the pseudodevice 'usb'), as well as define a baud rate to talk through the Arduino (as if it was a serial port itself rather than the destination for the code we want to write).
So that's one piece of the pie. The other piece is....which programmer do we use on the avrdude command line if we aren't going out on the parallel port with the SP12 dongle? Entering 'avrdude -c ?' gives us a list of programmers that it knows about:
Valid programmers are:
2232HIO = FT2232H based generic programmer
4232h = FT4232H based generic programmer
89isp = Atmel at89isp cable
abcmini = ABCmini Board, aka Dick Smith HOTCHIP
alf = Nightshade ALF-PgmAVR, http://nightshade.homeip.net/
arduino = Arduino
arduino-ft232r = Arduino: FT232R connected to ISP
atisp = AT-ISP V1.1 programming cable for AVR-SDK1 from <http://micro-research.co.th/>
atmelice = Atmel-ICE (ARM/AVR) in JTAG mode
atmelice_dw = Atmel-ICE (ARM/AVR) in debugWIRE mode
atmelice_isp = Atmel-ICE (ARM/AVR) in ISP mode
atmelice_pdi = Atmel-ICE (ARM/AVR) in PDI mode
avr109 = Atmel AppNote AVR109 Boot Loader
avr910 = Atmel Low Cost Serial Programmer
avr911 = Atmel AppNote AVR911 AVROSP
avrftdi = FT2232D based generic programmer
...... (and so on for a page or two) ......
The only one the looks even close is, well 'arduino', so let's use the command line options we found above in combination with what we've got here, and just try talking to the gadget and see:
mymachine:$avrdude -p t45 -c arduino -P /dev/ttyACM0 -b 19200
avrdude: AVR device initialized and ready to accept instructions
Reading | ################################################## | 100% 0.01s
avrdude: Device signature = 0x1e9206
avrdude: safemode: Fuses OK (E:FF, H:DF, L:E2)
avrdude done. Thank you.
So wow, it just works! That is, indeed, the correct sig for the ATtiny45. The Arduino Uno passes our request right through using the ArduinoISP software (with the jumpered on capacitor across the Reset line) to get to our 'legacy' device.
So now, how about a new version of the blink program? Let's see here:
/*
test blink program to make sure know what am doing with
avr-gcc and such.
for tiny45 through Arduino as programmer?
*/
#include <inttypes.h>
#include <avr/io.h>
#define F_CPU 8000000UL // 8Mhz on t45 - this needs to be defined BEFORE including util/delay.h
#include <util/delay.h> // for _delay_us(double) and _delay_ms(double)
// handy stuff from Arduino.h
#define HIGH 0x1
#define LOW 0x0
#define INPUT 0x0
#define OUTPUT 0x1
#define INPUT_PULLUP 0x2
#define lowByte(w) ((uint8_t) ((w) & 0xff))
#define highByte(w) ((uint8_t) ((w) >> 8))
#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
#define bitWrite(value, bit, bitvalue) (bitvalue ? bitSet(value, bit) : bitClear(value, bit))
#define BLINKPIN 0x01 // assume port B for t45
// sorta like setup()
void ioinit(void) {
bitSet(DDRB, BLINKPIN); // set B1 as OUTPUT -- like DDRB |= (1 << BLINKPIN);
}
// and can more or less pretend this is loop()
int main(void) {
ioinit();
for (;;) {
_delay_ms(500.0);
bitClear(PORTB, BLINKPIN); //..or PORTB &= ~(1 << BLINKPIN);
_delay_ms(500.0);
bitSet(PORTB, BLINKPIN); //..or PORTB |= (1 << BLINKPIN);
}
}
// end testblink.c
Compile it with avr-gcc and use avr-objcopy to create the hex file, then avrdude can write it. 100 bytes used total!
So how did I concoct the above? Among other things, I looked for (and at) the main header files that the IDE loads when you create a new program...i.e. at the Arduino.h file, which is stored at <IDE install folder>/hardware/arduino/avr/cores/. LOTS of handy macros in there, and a nice list of other files that are included in all IDE builds by default. Since we are trying to program on the bare metal, making as few assumptions as possible about the device, we can muck and pare at that list a bit, drill down through a few #includes to find what they do and what we need, guess what we don't need. Eventually we can just go with:
#include <inttypes.h>
#include <avr/io.h>
#define F_CPU 8000000UL // 8Mhz on t45 - this needs to be defined BEFORE including util/delay.h
#include <util/delay.h> // for _delay_us(double) and _delay_ms(double
There was one gotcha in my first attempt at this: avr-gcc bitched incessantly that F_CPU was NOT something it knew about, thus the #define for the CPU frequency there....I tried just including util/delay.h without it, and immediate had a wake up call. OBVIOUSLY (right? right?) the delay functions wouldn't work right if they were based on the clock speed of the device, and didn't know what that number WAS. An arbitrary AVR device can't be assumed to have an RTC or a PLL clock built-in, so the only other way to have a time base is to run it off the CPU clock, and....
IN any case...paring the #includes down to a bare minimum shows us what we can safely leave out, while still giving us some nice short cuts, and a handy template for building more complicated stuff later on.
Tuesday, August 9, 2016
Tiny45 programming with the Arduino IDE - ver 2
This post is Round Two of my quest to use my older AVR chips using the Arduino IDE as a programming platform and an Arduino Uno as a programmer ..
Originally, I was using a library (written originally for Arudino IDE 1.0.x) by David Ellis, from back in 2005. This DID work, for the fader program below and in the previous post, but that library only supported a few chips, and I was worried that as I tried to do more advanced things (using the ADC, SPI, or I2C stuff) I would run into problems. And I would like to buy/use the tiny44/84, 2313/4313, 861, and others eventually as well.
In any case...Spence Konde has created a 'modern' ATtiny library that has a lot more chips and functional support. To get it, open the Arduino IDE and then hit File/Preferences. Click on the little 'edit' button to the right of the 'Additional Boards Manager URLs:' field, and cut and paste the following...
...onto the list of sources for third-party board descriptions. Hit OK, then OK again on the Preferences dialog. Then go to the Tools/Board.../Board Manager (at the very top of the list)....scroll around until you find the ATtinyCore Modern package by Spencer Konde (ver 1.1.1 is the latest). Highlight and install it, and then quit the Board Manager.
Now, BEFORE you start writing a program for an ATtiny, make sure the Ardiuno ISP software is loaded on the Arduino, which is Example 11 in the File/Examples menu. Burn that, and then open/write your ATtiny program. When you are ready to burn your ATtiny....
You'll now have LOTS of new options under Tools/Board below the standard Arduinos....Choose the board series (ATtinyx5 in my case) off from the ATtiny list, and then you will have the B.O.D./power options (disable if yer not sure), Timer 1 Clock (not sure so I dint mess with it), Chip selection (tiny45), and Clock (lots of new options, but mine was already set on 8Mhz internal).
To burn a program (like 'fader' below)...wire up the ISP pins from the Arduino to the chip in question. Spencer Konde also recommends a 0.1 uF capacitor across the VCC/GND on the chip just for safety sake. Next, make sure you choose the Arduino/Leo as ISP (ATtiny) off the programmer list. Other options may work, but I know that one does the job.
Lastly...if you need to know all about this library, Spencer Konde has an excellent README for it here:
ATTinyCore/README.md at master · SpenceKonde/ATTinyCore · GitHub
/*
LED Fader for tiny45
*/
//
// NOTES for uploading: use the ATtinyCore Universal library
// (http://drazzy.com/package_drazzy.com_index.json)
// with ATtiny x5 series, then the chip, speed, etc. and use the
// Arduino/Leo as ISP (ATtiny) as the programmer.
//
// You can also change the internal clock, B.O.D., voltage
// standard fuses by choosing the option
// you want and choosing 'Burn Bootloader'. Be Careful!
//
//
#define FADEPIN 1
unsigned char fcnt = 0;
char fpause = 7;and
char updn = 1;
// the setup function runs once when you press reset or power the board
void setup() {
// initialize digital pin 13 as an output.
pinMode(FADEPIN, OUTPUT);
}
// the loop function runs over and over again forever
void loop() {
fcnt += updn;
analogWrite(FADEPIN, fcnt); // turn the LED on (HIGH is the voltage level)
delay(fpause); // wait for a second
if (fcnt > 254) updn = -1;
if (fcnt < 1) updn = 1;
}
Originally, I was using a library (written originally for Arudino IDE 1.0.x) by David Ellis, from back in 2005. This DID work, for the fader program below and in the previous post, but that library only supported a few chips, and I was worried that as I tried to do more advanced things (using the ADC, SPI, or I2C stuff) I would run into problems. And I would like to buy/use the tiny44/84, 2313/4313, 861, and others eventually as well.
In any case...Spence Konde has created a 'modern' ATtiny library that has a lot more chips and functional support. To get it, open the Arduino IDE and then hit File/Preferences. Click on the little 'edit' button to the right of the 'Additional Boards Manager URLs:' field, and cut and paste the following...
...onto the list of sources for third-party board descriptions. Hit OK, then OK again on the Preferences dialog. Then go to the Tools/Board.../Board Manager (at the very top of the list)....scroll around until you find the ATtinyCore Modern package by Spencer Konde (ver 1.1.1 is the latest). Highlight and install it, and then quit the Board Manager.
Now, BEFORE you start writing a program for an ATtiny, make sure the Ardiuno ISP software is loaded on the Arduino, which is Example 11 in the File/Examples menu. Burn that, and then open/write your ATtiny program. When you are ready to burn your ATtiny....
You'll now have LOTS of new options under Tools/Board below the standard Arduinos....Choose the board series (ATtinyx5 in my case) off from the ATtiny list, and then you will have the B.O.D./power options (disable if yer not sure), Timer 1 Clock (not sure so I dint mess with it), Chip selection (tiny45), and Clock (lots of new options, but mine was already set on 8Mhz internal).
To burn a program (like 'fader' below)...wire up the ISP pins from the Arduino to the chip in question. Spencer Konde also recommends a 0.1 uF capacitor across the VCC/GND on the chip just for safety sake. Next, make sure you choose the Arduino/Leo as ISP (ATtiny) off the programmer list. Other options may work, but I know that one does the job.
Lastly...if you need to know all about this library, Spencer Konde has an excellent README for it here:
ATTinyCore/README.md at master · SpenceKonde/ATTinyCore · GitHub
/*
LED Fader for tiny45
*/
//
// NOTES for uploading: use the ATtinyCore Universal library
// (http://drazzy.com/package_drazzy.com_index.json)
// with ATtiny x5 series, then the chip, speed, etc. and use the
// Arduino/Leo as ISP (ATtiny) as the programmer.
//
// You can also change the internal clock, B.O.D., voltage
// standard fuses by choosing the option
// you want and choosing 'Burn Bootloader'. Be Careful!
//
//
#define FADEPIN 1
unsigned char fcnt = 0;
char fpause = 7;and
char updn = 1;
// the setup function runs once when you press reset or power the board
void setup() {
// initialize digital pin 13 as an output.
pinMode(FADEPIN, OUTPUT);
}
// the loop function runs over and over again forever
void loop() {
fcnt += updn;
analogWrite(FADEPIN, fcnt); // turn the LED on (HIGH is the voltage level)
delay(fpause); // wait for a second
if (fcnt > 254) updn = -1;
if (fcnt < 1) updn = 1;
}
Sunday, August 7, 2016
i2c 2 Pi v0.6 - Arduino i2c Slave
i2c 2 Pi - Arduino i2c Slave
(Go back to i2c article)
// new i2c to Pi transceiver
//
// 8/2/16:0837 - version 0.51
// 8/3/16:0832 - version 0.6
//
#include <Wire.h>
#include <Servo.h>
#include <string.h>
#define SLAVE_ADDR 0x08
#define END_OF_STR '\x00'
#define SIG_BASE 0xA0
#define MAXSTR 20
#define CMD 1
#define CEND 2
#define CGET 4
#define CDATA 5
#define CDEND 6
#define WSLCT 7
#define WDATA 8
#define WDEND 9
#define DEBUG 1
const int flashpin = 13;
char recvStr[MAXSTR + 1];
char cdat[MAXSTR + 1];
int ccnt = 0;
unsigned char cmode = CMD;
//
// pin/function maps
// make sure pins are excluded from operations that are NOT good
// there MUST be a -1 at the end of each list unless ALL possible pins are included
//
// note: can really only control three analog devices at once!
// if pins 5 and 6 are used for PWM, the delay() and millis() functions won't work right.
// ...so pins 5,6 must be same freq; 9,10 same; 3,11 same
//
//Timer output Arduino Uno pins
//OC0A 6
//OC0B 5
//OC1A 9
//OC1B 10
//OC2A 11
//OC2B 3
//
const unsigned char alimit = 6, dlimit = 14;
unsigned char algList[6] = {0, 1, 2, 3, -1}; // can be pins 0-3 (avoid SCL/SDA)
unsigned char digiList[14] = {2, 4, 7, 8, 10, 12, -1};// can be pins 2-13 (avoid serial ports)
unsigned char digoList[14] = {13, -1}; // can be pins 2-13 (avoid serial ports)
unsigned char mtrList[6] = {6, -1}; // can be any of 3,5,6,9,10,11 but see above
unsigned char srvList[6] = {9, -1}; // can be any of 3,5,6,9,10,11 but see above
unsigned char tonList[6] = {11, -1}; // can be any of 3,5,6,9,10,11 but see above
// ...but no pwm on 3 or ll if tone is on
Servo servo[alimit];
void setup() {
if (DEBUG == 1) {
Serial.begin(9600);
}
// setup I2C stuff
Wire.begin(SLAVE_ADDR);
Wire.onRequest(sendData);
Wire.onReceive(receiveData);
strcpy(recvStr, "K");
cdat[0] = '\0';
// init servos and wake them up
for (int i = 0; i < alimit; i++) {
if (srvList[i] == -1) break;
else {
servo[i].attach(srvList[i]);
servo[i].write(5);
delay(500);
servo[i].write(150);
delay(500);
servo[i].write(90);
}
}
//init signal LED
pinMode(flashpin, OUTPUT);
digitalWrite(flashpin, LOW);
delay(200);
digitalWrite(flashpin, HIGH);
delay(200);
digitalWrite(flashpin, LOW);
}
void loop() {
//
// see if there is a command to process
//
if (strcmp(recvStr, "K") != 0) {
// let us know somethings happening
//
DebugOut("Data rcvd:");
DebugOut(recvStr);
//
//do something with received command
if (recvStr[0] == 'S' && strlen(recvStr) >= 4 ) {
char rtmp[6];
int svnum = ((int) (recvStr[1])) - 48;
unsigned char pinOK = pinCheck('s', svnum);
if (pinOK != -1) {
int slew = 90, mflag = 0;
for (int i = 0; i < 6; i++) {
rtmp[i] = recvStr[i+3];
if (rtmp[i] == '\0') break;
if (rtmp[i] == 'm' || rtmp[i] == 'M') {
rtmp[i+1] = '\0';
mflag = 1;
break;
}
}
rtmp[5] = '\0';
slew = atoi(rtmp);
if (mflag && slew > 500 && slew < 2100 ) servo[svnum].writeMicroseconds(slew);
else if (!mflag && slew >= 5 && slew <= 160) servo[svnum].write(slew);
}
} // end of servo slew
if (recvStr[0] == 'M' && strlen(recvStr) >= 4 ) {
char rtmp[6];
int mtnum = ((int) (recvStr[1])) - 48;
unsigned char pinOK = pinCheck('m', mtnum);
if (pinOK != -1) {
int mspeed = 0;
for (int i = 0; i < 6; i++) {
rtmp[i] = recvStr[i+3];
if (rtmp[i] == '\0') break;
}
rtmp[5] = '\0';
mspeed = atoi(rtmp);
if (mspeed >= 0 && mspeed <= 255) {
pinMode(pinOK, OUTPUT);
analogWrite(pinOK, mspeed);
}
}
} // end of motor speed
if (recvStr[0] == 'T' && strlen(recvStr) >= 4 ) {
char rtmp[7];
int newtpin, freq = -2;
unsigned char i = 1, j = 0;
while (j != 99) {
switch (freq) {
case -2:
rtmp[j++] = recvStr[i++];
break;
case -1:
rtmp[j] = '\0';
newtpin = atoi(rtmp);
freq = 0; j = 0;
break;
case 0:
if (recvStr[i] == '\0') {
rtmp[j] = '\0';
if (strlen(rtmp) > 0) freq = atoi(rtmp);
j = 99;
} else rtmp[j++] = recvStr[i++];
break;
}
if (recvStr[i] == '-') {i++; freq = -1;}
}
unsigned char pinOK = pinCheck('t', newtpin);
if (pinOK != -1) {
char dummy[10];
DebugOut("pin:");
DebugOut(itoa(newtpin, dummy, 10));
DebugOut("freq:");
DebugOut(itoa(freq, dummy, 10));
if (freq == 0) { pinMode(newtpin, OUTPUT); noTone(newtpin); }
else { pinMode(newtpin, OUTPUT); tone(newtpin, freq); }
}
} // end of write to digital pin
if (recvStr[0] == 'F' && strlen(recvStr) >= 4 ) {
char rtmp[6];
int newfpin;
for (int i = 0; i < 6; i++) {
rtmp[i] = recvStr[i+3];
if (rtmp[i] == '\0') break;
}
newfpin = atoi(rtmp);
unsigned char pinOK = pinCheck('o', newfpin);
if (pinOK != -1) {
pinMode(newfpin, OUTPUT);
if (recvStr[1] == '0') digitalWrite(newfpin, LOW);
else digitalWrite(newfpin, HIGH);
}
} // end of write to digital pin
// and reset the data for next time 'round..
strcpy(recvStr, "K");
}
}
void DebugOut(char *dbgstr) {
if (DEBUG == 1) {
Serial.println(dbgstr);
}
}
unsigned char pinCheck(char ptype, unsigned char thepin) {
unsigned char res = -1;
switch (ptype) {
case 'i': // digital read
for (int i = 0; i < dlimit; i++) {
if (digiList[i] == -1) break;
else if (digiList[i] == thepin) { res = i; break; }
}
break;
case 'o': // digital write
for (int i = 0; i < dlimit; i++) {
if (digoList[i] == -1) break;
else if (digoList[i] == thepin) { res = i; break; }
}
break;
case 't': // tone output
for (int i = 0; i < dlimit; i++) {
if (tonList[i] == -1) break;
else if (tonList[i] == thepin) { res = i; break; }
}
break;
case 'a': // analog read
for (int i = 0; i < alimit; i++) {
if (algList[i] == -1) break;
else if (algList[i] == thepin) { res = i; break; }
}
break;
case 'm': // pwm output
for (int i = 0; i < alimit; i++) {
if (mtrList[i] == -1) break;
else if (thepin == i) {res = mtrList[i]; break; }
}
break;
case 's': // servo output
for (int i = 0; i < alimit; i++) {
if (srvList[i] == -1) break;
else if (thepin == i) { res = i; break; }
}
break;
default:
res = -1; break;
}
return res;
}
int getSensor(int isens, char sdata[MAXSTR+1]) {
char tstr[5];
// debugging
itoa(isens, tstr, 10);
DebugOut("Data req:");
DebugOut(tstr);
//
if (isens > 96 && isens < 103) {
// read analog pin senscode - 97
unsigned char pinOK = pinCheck('a', isens - 97);
if (pinOK != -1) {
char szdat[MAXSTR + 1] = "\0";
strcpy(sdata, "AV:");
int isdat = analogRead(algList[pinOK]);
itoa(isdat, szdat, 10);
strcat(sdata, szdat);
}
} else if (isens > 64 && isens < 85) {
unsigned char pinOK = pinCheck('i', isens - 65);
if (pinOK != -1) {
int isdat;
char szdat[2];
strcpy(sdata, "DV:");
pinMode(digiList[pinOK], INPUT);
isdat = digitalRead(digiList[pinOK]);
itoa(isdat, szdat, 10);
strcat(sdata, szdat);
}
} else {
strcpy(sdata, "NOP");
}
return strlen(sdata);
}
void receiveData(int thebytes) {
char tstr[2];
tstr[0] = (char) (48 + cmode);
tstr[1] = '\0';
// debugging
DebugOut("I2C recv:");
DebugOut(tstr);
//
while(Wire.available()) {
char c = Wire.read();
int ic = ((unsigned char) c);
switch (cmode) {
case CMD:
switch (c) {
case '1':
cmode = CMD; // check status, sort of a NOP
break;
case '4':
cmode = CGET; // data is coming
break;
case '7':
cmode = WSLCT; // data is wanted from a pin or something
break;
}
break;
case WSLCT:
getSensor(ic, cdat);
ccnt = 0;
cmode = WDATA;
break;
case CGET:
cdat[0] = c;
cdat[1] = '\0';
ccnt = 1;
cmode = CDATA;
break;
case CDATA: {
if (c == '\0') {
ccnt = 0;
strcpy(recvStr, cdat);
cmode = CDEND;
/* flash so we know something worked
digitalWrite(flashpin, LOW);
delay(100);
digitalWrite(flashpin, HIGH);
delay(100);
digitalWrite(flashpin, LOW);
*/
} else {
cdat[ccnt++] = c;
cdat[ccnt] = '\0';
}
break; }
default:
break;
}
}
}
void sendData() {
char tstr[2];
tstr[0] = (char) (48 + cmode);
tstr[1] = '\0';
// debugging
DebugOut("I2C send:");
DebugOut(tstr);
//
switch (cmode) {
case WSLCT:
case CDATA:
case CGET:
case CMD:
Wire.write(cmode + SIG_BASE);
break;
case WDATA: {
int clen = strlen(cdat);
Wire.write(cdat[ccnt]);
ccnt++;
if (ccnt == clen) {
cmode = WDEND;
strcpy(cdat, "\0");
ccnt = 0;
}
break; }
case CDEND:
case WDEND:
Wire.write(cmode + SIG_BASE);
cmode = CMD;
break;
}
}
(Go back to i2c article)
// new i2c to Pi transceiver
//
// 8/2/16:0837 - version 0.51
// 8/3/16:0832 - version 0.6
//
#include <Wire.h>
#include <Servo.h>
#include <string.h>
#define SLAVE_ADDR 0x08
#define END_OF_STR '\x00'
#define SIG_BASE 0xA0
#define MAXSTR 20
#define CMD 1
#define CEND 2
#define CGET 4
#define CDATA 5
#define CDEND 6
#define WSLCT 7
#define WDATA 8
#define WDEND 9
#define DEBUG 1
const int flashpin = 13;
char recvStr[MAXSTR + 1];
char cdat[MAXSTR + 1];
int ccnt = 0;
unsigned char cmode = CMD;
//
// pin/function maps
// make sure pins are excluded from operations that are NOT good
// there MUST be a -1 at the end of each list unless ALL possible pins are included
//
// note: can really only control three analog devices at once!
// if pins 5 and 6 are used for PWM, the delay() and millis() functions won't work right.
// ...so pins 5,6 must be same freq; 9,10 same; 3,11 same
//
//Timer output Arduino Uno pins
//OC0A 6
//OC0B 5
//OC1A 9
//OC1B 10
//OC2A 11
//OC2B 3
//
const unsigned char alimit = 6, dlimit = 14;
unsigned char algList[6] = {0, 1, 2, 3, -1}; // can be pins 0-3 (avoid SCL/SDA)
unsigned char digiList[14] = {2, 4, 7, 8, 10, 12, -1};// can be pins 2-13 (avoid serial ports)
unsigned char digoList[14] = {13, -1}; // can be pins 2-13 (avoid serial ports)
unsigned char mtrList[6] = {6, -1}; // can be any of 3,5,6,9,10,11 but see above
unsigned char srvList[6] = {9, -1}; // can be any of 3,5,6,9,10,11 but see above
unsigned char tonList[6] = {11, -1}; // can be any of 3,5,6,9,10,11 but see above
// ...but no pwm on 3 or ll if tone is on
Servo servo[alimit];
void setup() {
if (DEBUG == 1) {
Serial.begin(9600);
}
// setup I2C stuff
Wire.begin(SLAVE_ADDR);
Wire.onRequest(sendData);
Wire.onReceive(receiveData);
strcpy(recvStr, "K");
cdat[0] = '\0';
// init servos and wake them up
for (int i = 0; i < alimit; i++) {
if (srvList[i] == -1) break;
else {
servo[i].attach(srvList[i]);
servo[i].write(5);
delay(500);
servo[i].write(150);
delay(500);
servo[i].write(90);
}
}
//init signal LED
pinMode(flashpin, OUTPUT);
digitalWrite(flashpin, LOW);
delay(200);
digitalWrite(flashpin, HIGH);
delay(200);
digitalWrite(flashpin, LOW);
}
void loop() {
//
// see if there is a command to process
//
if (strcmp(recvStr, "K") != 0) {
// let us know somethings happening
//
DebugOut("Data rcvd:");
DebugOut(recvStr);
//
//do something with received command
if (recvStr[0] == 'S' && strlen(recvStr) >= 4 ) {
char rtmp[6];
int svnum = ((int) (recvStr[1])) - 48;
unsigned char pinOK = pinCheck('s', svnum);
if (pinOK != -1) {
int slew = 90, mflag = 0;
for (int i = 0; i < 6; i++) {
rtmp[i] = recvStr[i+3];
if (rtmp[i] == '\0') break;
if (rtmp[i] == 'm' || rtmp[i] == 'M') {
rtmp[i+1] = '\0';
mflag = 1;
break;
}
}
rtmp[5] = '\0';
slew = atoi(rtmp);
if (mflag && slew > 500 && slew < 2100 ) servo[svnum].writeMicroseconds(slew);
else if (!mflag && slew >= 5 && slew <= 160) servo[svnum].write(slew);
}
} // end of servo slew
if (recvStr[0] == 'M' && strlen(recvStr) >= 4 ) {
char rtmp[6];
int mtnum = ((int) (recvStr[1])) - 48;
unsigned char pinOK = pinCheck('m', mtnum);
if (pinOK != -1) {
int mspeed = 0;
for (int i = 0; i < 6; i++) {
rtmp[i] = recvStr[i+3];
if (rtmp[i] == '\0') break;
}
rtmp[5] = '\0';
mspeed = atoi(rtmp);
if (mspeed >= 0 && mspeed <= 255) {
pinMode(pinOK, OUTPUT);
analogWrite(pinOK, mspeed);
}
}
} // end of motor speed
if (recvStr[0] == 'T' && strlen(recvStr) >= 4 ) {
char rtmp[7];
int newtpin, freq = -2;
unsigned char i = 1, j = 0;
while (j != 99) {
switch (freq) {
case -2:
rtmp[j++] = recvStr[i++];
break;
case -1:
rtmp[j] = '\0';
newtpin = atoi(rtmp);
freq = 0; j = 0;
break;
case 0:
if (recvStr[i] == '\0') {
rtmp[j] = '\0';
if (strlen(rtmp) > 0) freq = atoi(rtmp);
j = 99;
} else rtmp[j++] = recvStr[i++];
break;
}
if (recvStr[i] == '-') {i++; freq = -1;}
}
unsigned char pinOK = pinCheck('t', newtpin);
if (pinOK != -1) {
char dummy[10];
DebugOut("pin:");
DebugOut(itoa(newtpin, dummy, 10));
DebugOut("freq:");
DebugOut(itoa(freq, dummy, 10));
if (freq == 0) { pinMode(newtpin, OUTPUT); noTone(newtpin); }
else { pinMode(newtpin, OUTPUT); tone(newtpin, freq); }
}
} // end of write to digital pin
if (recvStr[0] == 'F' && strlen(recvStr) >= 4 ) {
char rtmp[6];
int newfpin;
for (int i = 0; i < 6; i++) {
rtmp[i] = recvStr[i+3];
if (rtmp[i] == '\0') break;
}
newfpin = atoi(rtmp);
unsigned char pinOK = pinCheck('o', newfpin);
if (pinOK != -1) {
pinMode(newfpin, OUTPUT);
if (recvStr[1] == '0') digitalWrite(newfpin, LOW);
else digitalWrite(newfpin, HIGH);
}
} // end of write to digital pin
// and reset the data for next time 'round..
strcpy(recvStr, "K");
}
}
void DebugOut(char *dbgstr) {
if (DEBUG == 1) {
Serial.println(dbgstr);
}
}
unsigned char pinCheck(char ptype, unsigned char thepin) {
unsigned char res = -1;
switch (ptype) {
case 'i': // digital read
for (int i = 0; i < dlimit; i++) {
if (digiList[i] == -1) break;
else if (digiList[i] == thepin) { res = i; break; }
}
break;
case 'o': // digital write
for (int i = 0; i < dlimit; i++) {
if (digoList[i] == -1) break;
else if (digoList[i] == thepin) { res = i; break; }
}
break;
case 't': // tone output
for (int i = 0; i < dlimit; i++) {
if (tonList[i] == -1) break;
else if (tonList[i] == thepin) { res = i; break; }
}
break;
case 'a': // analog read
for (int i = 0; i < alimit; i++) {
if (algList[i] == -1) break;
else if (algList[i] == thepin) { res = i; break; }
}
break;
case 'm': // pwm output
for (int i = 0; i < alimit; i++) {
if (mtrList[i] == -1) break;
else if (thepin == i) {res = mtrList[i]; break; }
}
break;
case 's': // servo output
for (int i = 0; i < alimit; i++) {
if (srvList[i] == -1) break;
else if (thepin == i) { res = i; break; }
}
break;
default:
res = -1; break;
}
return res;
}
int getSensor(int isens, char sdata[MAXSTR+1]) {
char tstr[5];
// debugging
itoa(isens, tstr, 10);
DebugOut("Data req:");
DebugOut(tstr);
//
if (isens > 96 && isens < 103) {
// read analog pin senscode - 97
unsigned char pinOK = pinCheck('a', isens - 97);
if (pinOK != -1) {
char szdat[MAXSTR + 1] = "\0";
strcpy(sdata, "AV:");
int isdat = analogRead(algList[pinOK]);
itoa(isdat, szdat, 10);
strcat(sdata, szdat);
}
} else if (isens > 64 && isens < 85) {
unsigned char pinOK = pinCheck('i', isens - 65);
if (pinOK != -1) {
int isdat;
char szdat[2];
strcpy(sdata, "DV:");
pinMode(digiList[pinOK], INPUT);
isdat = digitalRead(digiList[pinOK]);
itoa(isdat, szdat, 10);
strcat(sdata, szdat);
}
} else {
strcpy(sdata, "NOP");
}
return strlen(sdata);
}
void receiveData(int thebytes) {
char tstr[2];
tstr[0] = (char) (48 + cmode);
tstr[1] = '\0';
// debugging
DebugOut("I2C recv:");
DebugOut(tstr);
//
while(Wire.available()) {
char c = Wire.read();
int ic = ((unsigned char) c);
switch (cmode) {
case CMD:
switch (c) {
case '1':
cmode = CMD; // check status, sort of a NOP
break;
case '4':
cmode = CGET; // data is coming
break;
case '7':
cmode = WSLCT; // data is wanted from a pin or something
break;
}
break;
case WSLCT:
getSensor(ic, cdat);
ccnt = 0;
cmode = WDATA;
break;
case CGET:
cdat[0] = c;
cdat[1] = '\0';
ccnt = 1;
cmode = CDATA;
break;
case CDATA: {
if (c == '\0') {
ccnt = 0;
strcpy(recvStr, cdat);
cmode = CDEND;
/* flash so we know something worked
digitalWrite(flashpin, LOW);
delay(100);
digitalWrite(flashpin, HIGH);
delay(100);
digitalWrite(flashpin, LOW);
*/
} else {
cdat[ccnt++] = c;
cdat[ccnt] = '\0';
}
break; }
default:
break;
}
}
}
void sendData() {
char tstr[2];
tstr[0] = (char) (48 + cmode);
tstr[1] = '\0';
// debugging
DebugOut("I2C send:");
DebugOut(tstr);
//
switch (cmode) {
case WSLCT:
case CDATA:
case CGET:
case CMD:
Wire.write(cmode + SIG_BASE);
break;
case WDATA: {
int clen = strlen(cdat);
Wire.write(cdat[ccnt]);
ccnt++;
if (ccnt == clen) {
cmode = WDEND;
strcpy(cdat, "\0");
ccnt = 0;
}
break; }
case CDEND:
case WDEND:
Wire.write(cmode + SIG_BASE);
cmode = CMD;
break;
}
}
Subscribe to:
Posts (Atom)