C code meets assembler - And falls down

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

Here is a snippet of my luverrly C code that doesn't work:

...
	uint8_t volatile *Ready = &PINE;
	uint8_t volatile *Bus = &PINB;
...
	while ((*Ready & 0x40) == 0x00) {}	// Until READY goes high
	Count[i] = *Bus;			// When gone high, get byte
	i++	;				// 
	PORTE ^= (1<<2);			// Toggle Read

	while ((*Ready & 0x40) == 0x40) {}	// Until READY goes low
	Count[i] = *Bus;			// When gone low, get byte
	i++	;				// 
	PORTE ^= (1<<2);			// Toggle Read
...

Here is a snippet of the assembler version I wrote some time ago to do the same thing. It does work, perfectly adequately:

...
BYTE_READ3:
	in temp, PINE
	andi temp, $40	; 
	breq BYTE_READ3	; 
			; 'READY' goes high

	in temp, PINB	; Grab bus
	st Z+, temp	; Stuff it into some handy SRAM

	sbi PINE, 2	; Toggle READ

BYTE_READ2:
	in temp, PINE
	andi temp, $40	;
	brne BYTE_READ2	;
			; 'READY' goes low

	in temp, PINB	; Grab bus
	st Z+, temp	; Stuff it into some handy SRAM

	sbi PINE, 2	; Toggle READ
...

Now, I'm trying to get all this new program written in C (GCC, with AVR Studio).

I'm declaring my pointers volatile, yet the GCC optimizer still does horrible things to the 'while {}' loops, and it still, when not optimizing at all, winds up putting various bytes in the wrong places in the 'Count' array.

The assembler code is neat and tidy and perfectly reliable. I've never seen it go wrong, and I was highly tempted to try inlining it, but didn't on the grounds that would be also a headache. And besides, I wanted it all in C.

Anyhow, what it is trying to do is communicate with a highly asychronous device (actually, another AVR).

It's setting one pin low, "Read", then waiting for the other to acknowledge having put valid data on the bus "Ready", reading the bus, then toggling "Read" to ask for "More data", and waiting for "Ready" to toggle again.

Note how they both toggle - "Ready" is expected to toggle at some point, and it happens to be pin 6 (0x40) of PortE.

For various reasons (Gone into upon request :wink: ) I think this is a much neater idea than a level-indicated "Data Valid" signal.

In assembler it works fine.

In C? Not so much. Despite making the useful and interesting variables all 'volatile', the GCC optimizer still puts data all wrong. Turning optimization off made for a far larger executable, but it still isn't putting the correct bytes into the correct positions in the array, although it's much closer to correct.

So. What did I do wrong this time? :?:
Any suggestions? Need more info? Let me know.

Scroungre

PS - The disassembler window shows me that what took me six instructions in assembler takes 27 instructions in C. C may be faster to write, but assembler gets it done a lot faster when written... :twisted:

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0
...
   while ((PINE & 0x40) == 0x00) {}   // Until READY goes high
   Count[i] = PINB;         // When gone high, get byte
   i++   ;            //
   PORTE |= (1<<2);         // Set Read HIGH

   while ((PINE & 0x40) == 0x40) {}   // Until READY goes low
   Count[i] = PINB;         // When gone low, get byte
   i++   ;            //
   PORTE &= ~(1<<2);         // Set Read LOW
...

Why are you messing with pointers?
If you want to use symbolic names for your ports, just say

#define READY PINE

I bet you will find the Compiler will be not too far different to your ASM. In another thread I despair of people writing C when they have knowledge of ASM. They try to micro-manage everything.

Search for danni + AVR + lcd to see how LCD code can be written in C. You will be amazed how clear it is!

David.

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

david note that his code general so by changing pointers he can do the same on an other port.

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

Yes. Of course he can. But how often have you changed the wiring of a HD44780 without re-compiling?

The OP's assembler code also depends on hard coding of the ports. So the comparable C version should do the same.

The original code uses exclusive-or for strobing the Enable line. This depends on the initial port value as to whether it does an active-high strobe or an active-low strobe. IMHO, this is unwise.

It also depends on the semantics of a volatile pointer variable or a pointer to volatile memory. Not all compilers distinguish the difference e.g. they read the memory regardless. This is one of the few occasions that I would inspect the generated code.

David.

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

Quote:
Anyhow, what it is trying to do is communicate with a highly asychronous device (actually, another AVR).
I don't see any mention of an LCD or an HD44780.

Don

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

He did not mention either.

I have a very special crystal ball!

I never said the crystal ball was very accurate. Now that you point this out, it probably is not an HD44780 since you read READY on bit 7 with that controller.

All the same, you should not translate SBI with an XOR instruction. You should translate it into an inclusive-or. Likewise you translate a CBI into an and.

David.

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

Silly question but if you already have Asm code to do the job then why not simply use that? avr-gcc has no problem with calls between C and Asm code held in .S files. The interface is documented in the manual:

http://www.nongnu.org/avr-libc/u...

The only thing you might have to "translate" (assuming the original code was for the Atmel assembler) are some of the assembler directives.

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

Quote:

Why are you messing with pointers?
If you want to use symbolic names for your ports, just say

Where's the register pointer use? I see he's loading in the *value* of the ports, but not the address of the port register.

- Dean :twisted:

Make Atmel Studio better with my free extensions. Open source and feedback welcome!

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

I originally wrote it with

while ((((volatile uint8_t) PINE) & 0x40) == 0x00) {}	// Until READY goes high
	Count[i] = ((volatile uint8_t) PINB);					// When gone high,

But that didn't work.

I wasn't sure all those qualifiers were being properly applied, so I extracted them into pointers to the I/O registers.

And no, no LCD is involved, but the board is hard-wired to use those particular pins. Future board revisions may not be, but I dunno. Portability wasn't as interesting to me as making it work.

Scroungre

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

clawson wrote:
Silly question but if you already have Asm code to do the job then why not simply use that? avr-gcc has no problem with calls between C and Asm code held in .S files. The interface is documented in the manual:

Thanks. I had read that, and concluded that because my assembler code also monkeys around in the SRAM (with the Z pointer) that it would be a zoo trying to sort out where all the return variables went.

I did try inlining a 'nop' instruction in the while {} loop to get the optimizer to behave itself, and my compiler (GCC, AVRStudio 4.16) just flipped, insisting I 'define' 'asm' before using it, despite it being recognized (turning blue) as a keyword...

	while ((*Ready & 0x40) == 0x40) { 
	  asm volatile("nop"::); }

There's sample code out there, but all I could find just concerns itself with modifying registers in assembler, not accessing SRAM.

If you know of any (or would like to write some! :) ) Let me know!

Scroungre

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

Surely I translated your ASM into C just fine !

I agree that my crystal ball was wildly inaccurate about your purpose.

Compile my version and compare with your ASM. I guess it will be almost identical.

Once you are satisfied that C can be written without obfuscation, invest in a large quantity of cider, vodka, cannabis.

After drinking / smoking yourself into oblivion, your brain cells should be wiped clear of any knowledge of ASM.

Life will then be simpler.

David.

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

Um, what? 8)

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

Quote:

my compiler (GCC, AVRStudio 4.16) just flipped, insisting I 'define' 'asm' before using it, despite it being recognized (turning blue) as a keyword...


The highlighting in the editor is not done by the compiler. It is a simple function of the editor, based on a list of words and how to highlight the.

As of January 15, 2018, Site fix-up work has begun! Now do your part and report any bugs or deficiencies here

No guarantees, but if we don't report problems they won't get much of  a chance to be fixed! Details/discussions at link given just above.

 

"Some questions have no answers."[C Baird] "There comes a point where the spoon-feeding has to stop and the independent thinking has to start." [C Lawson] "There are always ways to disagree, without being disagreeable."[E Weddington] "Words represent concepts. Use the wrong words, communicate the wrong concept." [J Morin] "Persistence only goes so far if you set yourself up for failure." [Kartman]

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

Quote:

I did try inlining a 'nop' instruction in the while {} loop to get the optimizer to behave itself, and my compiler (GCC, AVRStudio 4.16) just flipped, insisting I 'define' 'asm' before using it, despite it being recognized (turning blue) as a keyword...

You need to set the compiler into GNU99 standards mode, and not the default C89 standard (or pure C99 standard). To do that, pass --std=gnu99 on the command line to avr-gcc when compiling.

- Dean :twisted:

Make Atmel Studio better with my free extensions. Open source and feedback welcome!

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

Y'all know that a thread title such as this one will get my interest. Aaa--a dreary early Spring day; nothing like a little C vs. ASM to pick up one's spirits.

But a great disappointment after going through the "challenge" code. The model ASM code uses direct I/O addressing and the pin-toggle function to avoid RMW races. The C code is crippled on both accounts. Why?

Then there is some discussion of "bytes in the wrong place", though we didn't see the compiler output. I'll suggest now that this is more "Compiler Wars"--the choice of toolchain.

And here I thought this was going to be fun.

Lee

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

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

abcminiuser wrote:
To do that, pass --std=gnu99 on the command line to avr-gcc when compiling.

- Dean :twisted:

Urfle. Okay, I can try that, but I'd rather just get the C working. I think I'm going to have to put this board under the 'scope to make sure all the signals and bus assertions are happening at the right time.

Oh, and I'm not using the command line - Perhaps I should have told you all this before - The whole reason I'm writing this in C is to put it atop a LUFA USB communication chip.

Any inline assembler is going to have to play nice with LUFA.

The compiler command line options are set in the makefile (I think...?)

Scroungre

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

theusch wrote:
And here I thought this was going to be fun.

Lee

I was told that writing code in C would be quicker than writing code in assembler.

I was wrong too. :-P

The C code, you see, doesn't work. What's wrong with it? The assembler works fine.

Now, tell me what I didn't do in C that I did do in assembler.

Scroungre

PS - It's not a speed challenge. The assembler works. The C just DOES NOT WORK. Speed's relevant, I want it to be snappy, but it would be nice if it JUST WORKED. S.

PPS - "Bytes in the wrong place" refers to acquired data, ie. what the whole purpose of the gadget is to do, not arbitrary ideas of what should go where. The output is wrong. S.

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

Quote:

but it would be nice if it JUST WORKED

Then why didn't you translate it directly?
Quote:

The model ASM code uses direct I/O addressing and the pin-toggle function to avoid RMW races. The C code is crippled on both accounts. Why?

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

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

theusch wrote:
Then why didn't you translate it directly?

That's what I was trying to do. It didn't work.

What did I do wrong? :?:

S.

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

Quote:

I was told that writing code in C would be quicker than writing code in assembler.

That only works if you are equally proficient in both C and Asm.

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

clawson wrote:
Quote:

I was told that writing code in C would be quicker than writing code in assembler.

That only works if you are equally proficient in both C and Asm.

That's unquestionably not the case in my case. I can run rings around C in assembler, but I'm trying to break out of my mold and get on with real life here.

For me, assembler is a high-level language. I bought this house on laying down AND and OR gates in hardware. Yes, I am an olde farte. But I'm trying to get with the new. Have pity on me... ;-P

Now, will someone please tell me what's wrong with my C so I can fix it and get on with making this gadget work?

Scroungre

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

Quote:
The C code, you see, doesn't work. What's wrong with it? The assembler works fine.

I cannot understand what part of any C generated code will fail.

Your 'working ASM' uses the SBI instruction, so I see no problem wih C generating exactly the same instruction.

If you are worried about RMW race conditions, you would never use XOR in C. You would choose a 'modern' AVR that allows writes to the PINx registers. And you would code that in C too!

I still maintain tha you will get on better by 'forgetting' all knowledge of ASM. After all, you are only waiting for an i/o bit and setting / clearing an i/o bit.

Note that many AVRs that contain a PORTE will map it out of the SBI addressable area.

Whether you use ASM or C, if you want to avoid RMW you do an OUT or STS to the PINx register. You really do not want to ever use SBI.

David.

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

Or, alternately, give me lots more hints on inlining assembler code that accesses the SRAM.

S.

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

david.prentice wrote:
I cannot understand what part of any C generated code will fail.

I can. It gives crap results where the assembler code works. Your understanding hasn't met the experience of mine.

S.

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

Of course you can make your code dependent on one particular toolchain.

It will tie you project down to gobbledygook experts. This may be important to you. After all it will mean that younger staff will have difficulty understanding or maintaining the code.

Incidentally, mixing ASM and C does not need to be gobbledygook. But using gobbledygook does add a certain mystery that appeals to some people.

David.

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

Hey, if the chicks with the plastic tits dig it, I'm all for obfuscation.

I'd like to ask you nicely - be helpful, or go away.

Scroungre.

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

uint8_t i, Count[20];


void foo(void)
{
   while ((PINE & 0x40) == 0x00) {}   // Until READY goes high
   Count[i] = PINB;         // When gone high, get byte
   i++   ;            //
   PORTE |= (1<<2);         // Set Read HIGH

   while ((PINE & 0x40) == 0x40) {}   // Until READY goes low
   Count[i] = PINB;         // When gone low, get byte
   i++   ;            //
   PORTE &= ~(1<<2);         // Set Read LOW
}

void bar(void)
{
   while ((PINE & 0x40) == 0x00) {}   // Until READY goes high
   Count[i] = PINB;         // When gone high, get byte
   i++   ;            //
   PINE = (1<<2);         // Toggle Read

   while ((PINE & 0x40) == 0x40) {}   // Until READY goes low
   Count[i] = PINB;         // When gone low, get byte
   i++   ;            //
   PINE = (1<<2);         // Toggle Read
}

You have never mentioned which AVR you are using. I compiled the above functions for an at90usb1287.

Note that a mega128 cannot use SBI for its PORTE port. And it does not allow writing to PINx either.

You should find CodeVision, ImageCraft, IAR will all compile to similar accesses of the PORTx and PINx sfrs.

I am trying to be helpful. Does the polarity of your 'toggle' matter to you? Are you concerned about RMW or execution speed?

David.

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

david.prentice wrote:
I am trying to be helpful. Does the polarity of your 'toggle' matter to you? Are you concerned about RMW or execution speed? David.

I would like to say that I do appreciate your help.

But I don't see where your code differs from mine in any significant way. My code compiles. It works. It even gets most of the bytes into the right places - It just doesn't get all of them there.

And it has to get all of them there.

Polarity of 'toggle' matters only inasmuch that both source and destination chip expect a certain polarity to start with, and both know how many bytes to send/expect (Five, incidentally).

The chip I'm using is an ATmega16u4, and it has running underneath the LUFA communication library. All C and assembler code I write has to be layered atop that.

I like help.

Scroungre

PS - The LUFA stuff works wonderfully (except when I screw it up :D ) so more kudoes to Dean for that. S.

Last Edited: Mon. Apr 4, 2011 - 04:41 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Quote:
It even gets most of the bytes into the right places - It just doesn't get all of them there.
Surely this has something to do with what you do with i and count outside of these functions.

Regards,
Steve A.

The Board helps those that help themselves.

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

I'm in exactly the same boat as Scroungre, (including the olde farte part). I am more comfortable in ASM by far, as my present understanding of C is minimal. Just like Scroungre, I am trying to make that transition.

Various here have said "don't micromanage" and "trust the compiler", but it seems to me that no matter how clunky the code is, the result should be the same, albeit longer and/or slower, providing the code is logically and syntactically valid.

I don't mean to set the cat amongst the pigeons, but this implies to me that the compiler "expects" a certain approach and can be confused by unorthodox code.

I really appreciate the advice here on proven and recommended styles as well as why other ways can lead to grief but it avoids an obvious question. It's all very well to advise "no, do it this way", but it doesn't explain why the "bad" approach doesn't physically work.

As I blunder into the C world, the idea that my less than clean code will cause the compiler to throw me a curve ball gives me the heebie jeebies.

A compiler is another layer of abstraction, with productivity expected in return. This requires a level of trust.

Being an engineer, I don't trust much, and only after a prolonged relationship. :-)

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

Quote:

but this implies to me that the compiler "expects" a certain approach and can be confused by unorthodox code.

No, it doesn't. It means to me that if you are going to translate a piece of ASM to C and expect the same results, you should write C that on its face will do the same operations.

It is still unclear after repeated requests for generated code listings what is deficient about the code generated by the C compiler. We also don't see the rest of the app, and we don't know what "same" means.

First, in the ASM fragment there was a "SBI PINE, ...". Fine. But in the C code it was translated not a set of a bit in PINE (that is atomic), but rather an explicit XOR toggle and a RMW situation that is not atomic. We don't see the rest of the app but (as I said earlier) that is where I threw in the towel. All the follow-on ragging about this and that is moot without seeing the whole app and whether this matters.

On the indirect access to the ports, it should be OK. But why do it, when it is of little use, and OP said that efficiency is important?

Also as I implied earlier, don't blame C for the way one toolchain generates code for a particular construct.

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

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

I'm also in this boat having done fairly complex assembly programs for decades. I'm suppose to "trust the compiler" but have spent many hours stepping through low-level code to see what's wrong with something. A perfect example of that is my AVR Treasure chest project I did awhile back. I decided to give C another chance and fired up win-avr for the task. There's a an entire thread in the GCC forum about if my code size exceeded a certain size (8041 bytes?) the program would go off into wonderland before all the init routines completed. No errors, no warnings, code size well below the Mega64's capacity. Without JTAG or ICE I was at the mercy of this compiler which I was suppose to trust. Problem never was resolved, I simply kept the code size down below the magic number and all was fine. I came very close to converting the whole thing to assembly just to get it working.

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

Quote:

Various here have said "don't micromanage" and "trust the compiler", but it seems to me that no matter how clunky the code is, the result should be the same, albeit longer and/or slower, providing the code is logically and syntactically valid.

I don't mean to set the cat amongst the pigeons, but this implies to me that the compiler "expects" a certain approach and can be confused by unorthodox code.


Not really. As long as optimisers are enabled the code generation of a C compiler generally follows a pattern where it initiallly generates some horrendous code that does lots of LDS, op, STS but then the optimiser will recognize where (known!) target addresses are in range of IN,op,OUT or even CBI,SBI (if single bits change) and replace the long winded sequences during peep-hole optimization. So if I write:

PORTB |= (1<<5);

or

PORTB |= 0x20 + 0x10 + 0x08 + 0x08;

or however complicated I want to make it look I should get:

	PORTB |= (1<<5);
  70:	c5 9a       	sbi	0x18, 5	; 24
	PORTB |= 0x20 + 0x10 + 0x08 + 0x08;
  72:	c6 9a       	sbi	0x18, 6	; 24

What wouldn't be efficient is if the bit-mask were not known at compile time. If I had something like:

void mask(uint8_t msk) {
  PORTB |= msk;
}

the compiler generates:

void mask(uint8_t msk) {
  PORTB |= msk;
  6c:	98 b3       	in	r25, 0x18	; 24
  6e:	98 2b       	or	r25, r24
  70:	98 bb       	out	0x18, r25	; 24
}

cos it cannot know if msk is going to change 1 bit or lots of bits and it cannot predict which one (which the hard coded bit number in a SBI would require).

So you get what you ask for.

BTW as I'm doing here there's nothing to stop you looking at the generated code after the build if you are in any doubt as to whether the compiler is doing a good job or not. Just study the .s, .lst or .s files or load the .elf or .hex into the Studio disassembler. My examples above use excerpts from the .lss generated by avr-gcc but I think all the compilers have a mechanism to see their generated Asm.

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

theusch wrote:

No, it doesn't. It means to me that if you are going to translate a piece of ASM to C and expect the same results, you should write C that on its face will do the same operations.

Well put and I agree if the purpose is to make an "all C" app, but I wouldn't expect moderate diversions from the original ASM to blow out in the compiler. I don;t have enough experience to say that is happening here.

theusch wrote:

It is still unclear after repeated requests for generated code listings what is deficient about the code generated by the C compiler. We also don't see the rest of the app, and we don't know what "same" means.

Fair enough, there may be something outside the snippet that is causing grief, but the code shown appears to be fairly isolated and, as yet, none of the many experienced eyes have said "aha" or "doh!".

theusch wrote:

Also as I implied earlier, don't blame C for the way one toolchain generates code for a particular construct.

Agreed. It's one thing to argue the merits of C and another thing to discuss the implementation in one compiler or another.

For that very reason, if there are weaknesses or deficiencies in a particular product, they should be aired, and understood. Nothing's perfect.

e.g. Firefox is terrible at handling scripts; I know this, but I still use Firefox

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

clawson wrote:

Not really. As long as optimisers are enabled the code generation of a C compiler generally follows a pattern where it initiallly generates some horrendous code that does lots of LDS, op, STS but then the optimiser will recognize where (known!) target addresses are in range of IN,op,OUT or even CBI,SBI (if single bits change) and replace the long winded sequences during peep-hole optimization

Here's where I get out of my depth. I can only assume, for portability, that a function written in C will produce "end-to-end" results that are identical, regardless of the compiler. If not, it's an outright bug.

So, it comes down to optimization. Are there noticeable strengths and weaknesses between mainstream compilers, or are algorithms fairly consistent?

clawson wrote:

BTW as I'm doing here there's nothing to stop you looking at the generated code after the build if you are in any doubt as to whether the compiler is doing a good job or not.

Yes, I knew that. I suspect I will be doing a fair bit of that, just to get a feel for the compiler.

With complex drivers, like graphics, which are highly hardware dependent and cycle hungry, I suspect that I will revert to ASM. These are microcontroller applications, after all, so moving up to the next processor to solve performance or memory issues is self-defeating IMO.

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

I really struggle to see the problem(s).

Controlling peripherals involves reading, writing, testing bits. And straightforward reading and writing bytes.

There is absolutely no difference between what language has produced "LDI r24,0x12 ; STS 0x3456,r24"

Yes. There are semantics with reading / writing some 16 bit registers, or with 4-cycle timing. The first applies with any language. The second does require some ASM assistance to ensure conformance. But most compiler libraries look after these niceties.

If people describe their intentions carefully, there is a lot of help out there. Solutions tend to be both concise and elegant. But no-one can help if they do not know the problem.

I have only ever seen any need for ASM in functions for video timing.

David.

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

david.prentice wrote:

I have only ever seen any need for ASM in functions for video timing.

I agree with everything you said, so were are not really at odds. There are two points though:

- Everything is fine as long as you have a pre-existing library.

- On your last point, I would apply that to any kind of timing. If there is a big speed differential between the uC and the peripheral, you either want the CPU to run as fast as possible, or if the device is slow and you must bit bash, there are other things you can be doing while waiting (Begone! cursed NOP).

Depending on the AVR device, you may not have the luxury of events or DMA, so IMO, getting creative is necessary for max performance.

When it involves dependence on the CPU clock freq, it is not for the fainthearted, but major performance gains with ASM can be achieved.

Perhaps a C guru can prove me wrong, I'm always keen to learn.

Cheers

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

Count me as another who started with assembly and found it to be extremely easy, logical and efficient. I also found it took 2x as long to just get simple things done in C as I was learning, but now both languages are staples of my diet!

Can I beat the compiler? Hell yeah just about every single time for both speed and code size, even when half asleep. But with C I can get an entire complex project up and running in a few hours as compared to days with assembly.

So when I need to count every cycle and know exactly what is going on, it's assembly, but when cycle accurate code is not important, it's CodeVision.

For a current project that has a ton of user IO on an LCD menu, I did the main program in C and then dropped inline assembly for things like RC5 timing and video output.

It really did hurt to learn C after doing assembly for a year, but I like both languages equally (==) now.

Brad

I Like to Build Stuff : http://www.AtomicZombie.com

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

Quote:
I can only assume, for portability, that a function written in C will produce "end-to-end" results that are identical, regardless of the compiler. If not, it's an outright bug.

Not entirely sure what the meaning of "end-to-end" here, but: You should only assume that a C compiler will produce code that adheres to the semantics of the C programming language. Anything else is a bug.

You should not assume that two compilers, given the same C code will produce the same machine code. Compilers vary in quality in this respect. Is the difference between them big enough to be substantial for you? IMO that depends on the situation. In most cases I personally gain in speed of coding what I might loose in code efficiency.

I am fairly confident that

while (some-expression)
{
   some statements;
}

will be handled by the compiler to produce optimal, or near optimal code for such a loop construct. If the condition (some-expression) is intricate, the compiler might not "see" some very smart way to evaluate it as the skilled assembler programmer in full know of all aspects of the application. But I will be relieved of making up possibly two labels (for the "loop" and "exit" locations), and writing assembly for a half-complex expression gets involved and takes me much longer.

And I would expect of any decent compiler that e.g.

PORTx |= 1<<pin_number;

would generate a single instruction SBI. There are no guarantees, though. A compiler that does not do that is crappy, for sure, but it does not necessarily violate the C standard.

Programming in C is not like programming in assembler. There is no magic path-of-no-resistance into C programming if you come from assembler. You need to learn not only the syntax, but also the semantics of the language. You need to accept to give up absolute control of the generated code. The absoluteness is the C language syntax and semantics. When that is not enough, you either need to fall back to assembler or to inspect the generated code.

And you need to do that inspection "every time" - do not trust the compiler to generate the same code for the same code snippet when e.g. it's surroundings have changed. The compiler might "suddenly" decide that a variable that it allocated in a register will be allocated in RAM, because there is more to gain now by allocating another variable in a register. If you can not live with this then C is simply not for you. Period. This is the control you will have to give up.

And when you get into stuff that is not standardized, the case cited most often here at AVRfreaks is interrupt handlers, you need to rely on extensions implemented by a specific compiler.

As of January 15, 2018, Site fix-up work has begun! Now do your part and report any bugs or deficiencies here

No guarantees, but if we don't report problems they won't get much of  a chance to be fixed! Details/discussions at link given just above.

 

"Some questions have no answers."[C Baird] "There comes a point where the spoon-feeding has to stop and the independent thinking has to start." [C Lawson] "There are always ways to disagree, without being disagreeable."[E Weddington] "Words represent concepts. Use the wrong words, communicate the wrong concept." [J Morin] "Persistence only goes so far if you set yourself up for failure." [Kartman]

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

Quote:
getting creative is necessary for max performance.
This has come up many times. People obsess with maximum performance when in reality only fast enough performance is necessary. While there are some applications where better performance yields better results, the vast majority of applications simply do not benefit from it.

Regards,
Steve A.

The Board helps those that help themselves.

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

Yes, Steve. Concur.

I'll refrain from my usual rant about the three rules of optimization. For now.

As of January 15, 2018, Site fix-up work has begun! Now do your part and report any bugs or deficiencies here

No guarantees, but if we don't report problems they won't get much of  a chance to be fixed! Details/discussions at link given just above.

 

"Some questions have no answers."[C Baird] "There comes a point where the spoon-feeding has to stop and the independent thinking has to start." [C Lawson] "There are always ways to disagree, without being disagreeable."[E Weddington] "Words represent concepts. Use the wrong words, communicate the wrong concept." [J Morin] "Persistence only goes so far if you set yourself up for failure." [Kartman]

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

From OP's description of the algorithm,
RMW should not be an issue.
Sequence matters, precise timing does not.
My guess is that the description is wrong.
The other AVR is probably running code for which precise timing does matter.
Said code is tuned for OP's assembler, but breaks on his C.

"Demons after money.
Whatever happened to the still beating heart of a virgin?
No one has any standards anymore." -- Giles

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

Phew! Busy thread.

JohanEkdahl wrote:

Not entirely sure what the meaning of "end-to-end" here, but: You should only assume that a C compiler will produce code that adheres to the semantics of the C programming language. Anything else is a bug.

I should have been more specific, perhaps "input to output" in terms of values entered and values returned would be better.

I appreciate such a detailed response; you confirm what I thought.

For the most part, I don't have a problem with relinquishing control to a compiler. I have used some VERY high level languages and had to put a great deal of faith in whatever was "under the hood". Keep in mind that this was all PC based, where bloatware reigns supreme and all problems are solved with a faster processor or more memory. (and of course, an animated paper clip stealing cycles is essential to the job, isn't it?)

I realize that uCs have exploded in capabilities so using C vs asm is not unreasonable, but at the same time, when hardware meets hardware or when algorithms get tricky, you can't beat assembler. Yes, I know inappropriate optimization can be a real time vampire. I am a newbie so I haven't heard your optimization rules yet, please feel free to rant away.

As for trusting extensions, (or drivers etc.) that trust has to be earned. I appreciate the support from OP here about various products that bit them in their behind instead of helping them succeed. For example, I was dissatisfied with the update speed of an LCD display on a vendor supplied dev kit. Less than two hours work resulted in well over a 10x speed improvement.

I'm not a fundamentalist zealot. I will pay my dues and try to master C. (I even bought a book :-) ) I think it will be a core part of any but the smallest of projects (and processors).

I'm glad no one mentioned structure or documentation as a reason. The structure of a program is only as good as the coder and NO language is self-documenting.

That said, keeping ASM in my back pocket for specific tasks only makes sense, as does examining the compiler output.

Koshchi wrote:

This has come up many times. People obsess with maximum performance when in reality only fast enough performance is necessary. While there are some applications where better performance yields better results, the vast majority of applications simply do not benefit from it.

I understand your point and I tend to agree, with some reservations. I rarely do anything too fancy in the main program. It's mostly calls, branches and user interface. When I am writing drivers and subroutines, that's a different story. Anything that is recursive, iterative or frequently called gets my full attention. The optimization may be overkill on a 25MHn Xmega, but what if later I want to use that same routine on a 8 MHz ATtiny?

Once I am satisfied that it is optimized and solid, it goes into that special box for future use. I have made some good money, not because I am the world's greatest coder, but because I look at every function as another tool to put in the box. I catalog every one and document the heck out of it. This means I can crank out code fast when the customer wants it yesterday.

So, C vs ASM? I'd say different horses for different courses, but it doesn't do much for Scroungre's original post.

Cheers :)

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

Pragma wrote:
As for trusting extensions, (or drivers etc.) that trust has to be earned.
So far as I can tell,
OP isn't using any extensions.
All the code, including that in system header files, is standard C.

"Demons after money.
Whatever happened to the still beating heart of a virgin?
No one has any standards anymore." -- Giles

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

OP here again, if I'm still relevant... 8)

1st) Keep in mind the whole thing is a heavily edited version of the 'DualVirtualSerial' device from LUFA. I've whacked out one serial port, and the way I can tell my arrays are out-of-order is because I convert them to hexadecimal ASCII and flush them down the USB plumbing to a PC.

Incidentally, USB "That works"(TM) is the whole and entire reason for getting into C. My assembler attempts at USB drivers were a failure, so I stopped trying to re-invent the wheel and went with LUFA.

2nd) I know the unoptimized output of the compiler is atrocious. It's going to be atrocious. But turning the optimization on munges the data far worse, despite declaring various things volatile.

3rd) And now, I have at least an idea to test. It's quite possible that my array index [i] is getting mangled by USB interrupts. Perhaps I should make it 'volatile' as well, or just use some bizarre variable name that isn't going to be used anywhere else.

Thanks. I go figure.

Scroungre

PS - If you want more code you can have more code, but I didn't want to paste the whole silly thing (including some fairly scatological //comments :shock: ) into a forum post. Privately, perhaps. Just ask... S.

PPS - The assembler code I posted was within a cli/sei loop, so it didn't have to worry about interrupts. But the entire 'toggle the pin' idea came about because the other end may be interrupted, and now this end can be too... S.

PPPS - Yes, I have tested flushing the array down the USB plumbing preloaded with various values, and the flushing code works fine (well, it does now. Running that test exposed another bug, but I fixed that one on my own. 8) ). S.

PPPPS - Thankx to Koschi who pointed out that Count or [i] were at fault. Obvious, once someone hits you over the head with it! :lol:

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

And just to add to the philosophical arguments, I'll throw in this hypothesis:

The higher-level the language is, the easier it is to write really atrocious code.

Laying down 74xxx chips it's usually pretty obvious if you're using wildly redundant logic. In assembler, it's still fairly obvious when you call a computationally expensive function. In C I can just write (double float x = a / b) and not even notice how many processor cycles, registers, RAM, etc., that took.

Don't get me started on Javascripting websites with database backends... :)

Scroungre

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

I would assume that you are controlling some sort of hardware by reading / writing bits in PORTB and PORTE.

The C that I posted should be the equivalent of the ASM that you said 'worked'. And yes, modern AVR C compilers generate pretty good code.

But bear in mind that you need to give an AND statement for C to produce an AND instruction. The same applies to OR, XOR, SBI, CBI, ...

You posted 'sufficient' ASM in your first posting for anyone to 'translate' into C. Your attempt at translation was NOT a 'translation'.

You have never said what goes wrong with the hardware. Or if you have even tried any suggestions from your readers.

Explain 'what you expect'
And 'what you get'

This applies to all questions posed here. You will get excellent help.

You only have to read the progress of the threads that say 'my AVR does not work' or 'this code is crap'. They spread over days (sometimes weeks) and tend to go off-topic.

Compare that to : "This ASM code works with a ATmega32u2 to control chip ABC1234 connected to these pins" ... "what is wrong with this C code?"
These questions are often answered and solved within an hour.

Yes. Anyone can devise inappropriate code with a HLL. It is not uncommon for students to use pow() and floating point to shift an integer one bit to the left.

David.

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

david.prentice wrote:
I would assume that you are controlling some sort of hardware by reading / writing bits in PORTB and PORTE.

I am. I'm controlling another AVR. I mentioned this in another post.

Quote:
The C that I posted should be the equivalent of the ASM that you said 'worked'. And yes, modern AVR C compilers generate pretty good code.

Yep. And it was equivalent to the C I had written. But the C didn't work.

Quote:
You posted 'sufficient' ASM in your first posting for anyone to 'translate' into C. Your attempt at translation was NOT a 'translation'.

Obviously. It didn't work.

Quote:
You have never said what goes wrong with the hardware. Or if you have even tried any suggestions from your readers.

Explain 'what you expect'
And 'what you get'

What I expect is certain bytes in a certain order from the external hardware. I get those bytes (most of them) in order shambolic.

Quote:
This applies to all questions posed here. You will get excellent help.

I usually do. This is why I like y'all.

Quote:
You only have to read the progress of the threads that say 'my AVR does not work' or 'this code is crap'. They spread over days (sometimes weeks) and tend to go off-topic.

You don't say. :)

Quote:
Compare that to : "This ASM code works with a ATmega32u2 to control chip ABC1234 connected to these pins" ... "what is wrong with this C code?"
These questions are often answered and solved within an hour.

Yes. Anyone can devise inappropriate code with a HLL. It is not uncommon for students to use pow() and floating point to shift an integer one bit to the left.

David.

I know you're trying to help. And I'm trying to interpret what you're saying into useful software fixes. But there are occasionally "Failures to communicate", and this may be one of those.

Scroungre.

PS - It doesn't help to say what chip I'm interacting with when it too is a programmed microprocessor. I could, if you wanted me to, paste several more pages of assembler code, but I don't think it's help. S.

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

Scroungre wrote:
david.prentice wrote:
You have never said what goes wrong with the hardware. Or if you have even tried any suggestions from your readers.

Explain 'what you expect'
And 'what you get'

What I expect is certain bytes in a certain order from the external hardware. I get those bytes (most of them) in order shambolic.


You devise a test suite.
You devise a set of input data.
You specify the expected output.

You run the test suite.
You discover 'where' and 'how' your code fails.

More importantly, your readers would have some idea what 'order shambolic' might mean.

Quote:

PS - It doesn't help to say what chip I'm interacting with when it too is a programmed microprocessor. I could, if you wanted me to, paste several more pages of assembler code, but I don't think it's help. S.

Yes. It requires you to reduce your code down to a minimal set that reproduces your problem.

If you describe things properly, it might be that someone chooses to run your code. Or even just read it.

Life is considerably easier with a hardware chip ABC1234. The reader can simply look at the data sheet.
With another microcontroller, you have to specify what it is doing too.

You would often debug an SPI slave implemented on another AVR by first debugging the Master with a hardware SPI slave. It is worth conforming to an existing protocol such as SPI, I2C, UART ...

If you are using custom protocols, you have to devise even more test suites.

David.

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

Quote:

can only assume, for portability, that a function written in C will produce "end-to-end" results that are identical, regardless of the compiler. If not, it's an outright bug.

So, it comes down to optimization. Are there noticeable strengths and weaknesses between mainstream compilers, or are algorithms fairly consistent?


Not a good assumption to make. Some of the C compilers are far better than others at "tight" code. The one I use, avr-gcc, is about half way up the scale. There's also one (possibly 2) that I wouldn't touch as they are consistently shown to regularly produce sub-optimal code.
Quote:

While there are some applications where better performance yields better results, the vast majority of applications simply do not benefit from it.

Agree entirely. I think the only "regular" topic here where Asm seems the only choice is the already mentioned video generation - where every single cycle counts (the faster you can do it the more rows and columns you can have). But I'd be hard pushed to think of any application where "C performance" is otherwise not acceptable.
Quote:
have some idea what 'order shambolic' might mean

I wonder if it would help for anyone confused to read:

Optimization and the importance of volatile in GCC

(which may have some relevance to other C compilers too). It explains that the optimizer may reorder statements or reuse some opcode(s) for more than one line of C making the flow appear potentially "shambolic". The guarantee you get from C is that it will do what's written but there are no particular guarantees about the ordering or speed (except in conditions explained by the C standard)

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

Quote:
The guarantee you get from C is that it will do what's written but there are no particular guarantees about the ordering or speed (except in conditions explained by the C standard)

I agree 100%.

Yes. Some compilers may be more clever in optimising. All compilers will 'do what you ask'.

Yes. You can ensure the ordering of code to satisfy some hardware timing order.

In all honesty, you may have no guarantee of efficiency but you can have a high confidence that most compilers are pretty good.

David.

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

As you know I'm a ASM person.
All apps. that go to sleep/idle to save power will benefit from a fast program, and if that is important ASM will allways be in the picture!

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

david.prentice wrote:

You devise a test suite.
You devise a set of input data.
You specify the expected output.

You run the test suite.
You discover 'where' and 'how' your code fails.

More importantly, your readers would have some idea what 'order shambolic' might mean.

It means they come out in a shambles. A mess. I have prepared test suites and I have prepared test data and I have flushed it down the USB plumbing. The test data worked (eventually). The live interaction with external hardware did not.

Quote:
Yes. It requires you to reduce your code down to a minimal set that reproduces your problem.

I don't know how to make it any simpler. It has to fetch the data, and get it right.

How can I simplify that?

Scroungre

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

clawson wrote:

Agree entirely. I think the only "regular" topic here where Asm seems the only choice is the already mentioned video generation - where every single cycle counts (the faster you can do it the more rows and columns you can have). But I'd be hard pushed to think of any application where "C performance" is otherwise not acceptable.

In this case, the other AVRs have interrupt routines running that really do care about every cycle. I completely rewrote an effective 'switch' statement in assembler so that the fall-through would be the last desired result and would lead into the subroutine without a branch or jump because that saved three cycles in a worst-case scenario.

Sometimes it is important. This is why I'm using six AVRs in parallel on the input data side...

Scroungre

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

You claim that your ASM function 'works'.

I suggested a C equivalent. It will compile to give you very similar ASM sequences to your original ASM.

I cannot believe that your AVR is going to behave any differently.

I am sceptical however of a 'strobe' with an unspecified polarity. And if your Slave AVR has any particular timing considerations.

I would also be suspicious of a C newbie that uses pointers inappropriately. Likewise using global variables that may share name spaces. In other words, there is a whole lot of other things that can corrupt data.

Design an ASM routine that uses local variables and checks array bounds.
Design a C routine that uses local variables and checks array bounds.

The C will probably use more cycles and instructions. I doubt that the functionality will be affected at all.

Anyway, as suggested days ago, you can always leave your function coded in ASM. Simply add the appropriate "asmfunction.S" to your project.

David.

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

david.prentice wrote:
You claim that your ASM function 'works'.

I do, and it did. It worked fine talking to the other AVRs - What it didn't do was speak USB.

Quote:
I am sceptical however of a 'strobe' with an unspecified polarity. And if your Slave AVR has any particular timing considerations.

That's the point of toggling. It gets around the whole timing thingy. Yes, both chips could just go away - And still meet the spec - and come back an hour later. More details available upon request.

Quote:
I would also be suspicious of a C newbie that uses pointers inappropriately. Likewise using global variables that may share name spaces. In other words, there is a whole lot of other things that can corrupt data.

I am a C newbie. Be suspicious all you want.

Quote:
Anyway, as suggested days ago, you can always leave your function coded in ASM. Simply add the appropriate "asmfunction.S" to your project.
...

David.

And if my assembler monkeys with the SRAM you think the rest of the program is just going to play nice with it? Heh.

I don't think so. I could be wrong.

Scroungre

PS - Since I wrote both the chips, I can specify the initial state of the "read" and "ready" strobes, and I did. They also both know exactly how many bytes are supposed to be transmitted. That part works fine... In assembler. S.

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

Incidentally, Skeeve got it right some time ago, and I didn't give suitable credit. The other AVR is doing time-sensitive stuff. S.

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

Can you wind back and post the generated Asm both for your original C attempt and also for David's "this should be like the Asm C code". Can you not see how (a) the original code was not written like the Asm and (b) that David's code produces something very close to the Asm you are trying to mimic? In what way doesn't David's code deliver what you are looking for? When we know that suggestions can be made as to how the C might be changed to be even closer.

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

I programmed earlier micros using assembler (such as the 8080,80186 families) because the PC compilers (C, pascal, etc) were hard to use for embedded targets. I found myself structuring my code and data in ways to emulate what the C language itself would do.

The GNU GCC compiler and libraries for the AVR are well suited for embedded use, and there are many functions in AVR LibC that I would not care to re-create in assembler for myself (some of these ARE written in assembler anyway). You can of course call a C library function from Assembler, but it's easier to just write the calling code in C.

Often when I look at the output of the compiler I see a similar pattern, the compiler will copy the source operands to specific registers, perform the operations, and copy the results back to the original locations EVEN IF THE ORIGINAL LOCATIONS WERE REGISTERS! OTOH the compiler will often find code segments that are common to several sections of the C code and generate only one copy of this in assembler inserting the required calls or jumps to access it from the various sections to save code space. That's something I might miss in coding it myself in assembler and certainly in C.

abcminiuser wrote:

You need to set the compiler into GNU99 standards mode, and not the default C89 standard (or pure C99 standard). To do that, pass --std=gnu99 on the command line to avr-gcc when compiling.
- Dean :twisted:

That's the first time I've heard about this GCC switch. What exactly does it do, and why isn't it the default?

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

Quote:

That's the first time I've heard about this GCC switch. What exactly does it do, and why isn't it the default?

http://gcc.gnu.org/onlinedocs/gc...

I forget all the GNU dialect extensions but one that springs to mind is named struct initialisers using : rather than . Another very popular one is anonymous fields in structs.

A list of all the GCC dialect extensions is here:

http://gcc.gnu.org/onlinedocs/gc...

BTW the reason most people switch to 99 rather than the default 89 is for the:

for (int i=0; i<10; i++) {

construct. (there are many other reasons but that's the most common one I guess). If you use that in -std=c89 or -std=gun89 you get:

test.c:6: error: 'for' loop initial declaration used outside C99 mode

Both Mfile and AVR Studio (well the old one at least! ;-)) pass -std=gnu99 by default.

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

Yay! OP here. I believe I have fixed it, although there's a few things I still don't quite understand.

Anyhow, there were two interacting problems, which added to the confusion. Problem #1 was indeed [i] - Making it volatile fixed most of the trouble.

#2 was a bit more fundamental, and involved a bit of arcane USB business, as it seems to like considering eight bytes as two 32-bit integers and shipping them out little-endian, whereas I really want them big-endian.

What I don't understand is why it didn't do that with test arrays. Initializing thus:

static uint8_t Count[24] = { 0x11, 0x22, 0x33, 0x44,0x55,0x66,0x77,0x88,0x99,0xAA,0xBB,0xCC,
0xF1, 0xE2, 0xD3, 0xC4,0xB5,0xA6,0x97,0x88,0x79,0x6A,0x5B,0x4C };

worked fine, providing all the bytes in initialized order, but once I've gotten my 'Count' array from the outside world, as it is, I have to do this:

	temp = Count[i];					// Swap two bytes
	Count[i] = Count[(i - 2)];			//
	Count[(i - 2)] = temp;				//
	temp = Count[(i - 1)];				// And do it again. 
	Count[(i - 1)] = Count[(i - 3)];	// 
	Count[(i - 3)] = temp;				// 

I'm not sure why that should be - I'm rather surprised, actually - but it works now.

Obviously, it could be neater, faster, etc., and it would be nice to be able to turn the optimization back on (at the moment, that still throws a huge wrench into the works) but hey. It works! Yay! :)

Thanks for all your comments. Even those I didn't end up using were still educational.

Thanks again!

Scroungre