Optimisation breaks debug code.

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

I'm using a little header file to output some debug info to a logic analyzer. The macro's in that file unroll a statement such as DEBUG(23) into a bunch of sbi and cbi instructions which output that number in an async format. This way I can track which code is executed and when it's executed without changing the overall timing too much.

The problem is however that if the optimisation level is higher than -O1 the timing of the macro breaks.
Is there a way to group these sbi / cbi statements together so the optimiser won't touch them?

#ifdef		DEBUG_PORT

	// Just to make sure the user knows the debug code is inserted.
	#warning Debug code inserted.

	// 3 little Macro's for internal use only.
	#define _SET					(DEBUG_PORT |= DEBUG_BIT)
	#define _CLEAR					(DEBUG_PORT &= ~DEBUG_BIT)
	#define _BIT(X,Y)				((X) &(0x01 << (Y)) ? (_SET) : (_CLEAR))

	// Use this macro to enable the debug output pin.
	#define DEBUG_OUTPUT_ENABLE		(DEBUG_DDR |= DEBUG_BIT)

	// Startbit, a bunch of data bits and a stop bit.
	#define DEBUG(X)	(_CLEAR,\
			_BIT(X,0), _BIT(X,1), _BIT(X,2),_BIT(X,3),\
			_BIT(X,4), _BIT(X,5), _BIT(X,6), \
			_SET)

#else
	// Preprocessor removes debug statements.
	#define		DEBUG_OUTPUT_ENABLE
	#define		DEBUG(x)

#endif

In this example below the debug bit stream is interrupted by the cpse and rjmp instructions because of optimisation.

ISR (USART_UDRE_vect) {
// Uart Data Register Empty interrupt routine.
// Sends the Next byte of A Packet to the UART. If the last bye is put in UDR
// this interrupt disables itself and enables the transmission complete interrupt
// to clean up.
	if(TxDBytes){			// if there's more data to be sent...
		DEBUG(0x37);
		UCSRB &= ~(1<<TXB8);		// Send databytes from now on.
		//	Bug: Can possibly be eliminated (do in MumarSend)
		UDR = *pTxd; 		// write next byte to data buffer.
		TxDBytes--;				// A byte has been transmitted, so decrement...
		pTxd++;
	}
	else {
		DEBUG(0x38);
		UCSRB = (1<<RXEN)|(1<<TXEN)|	// No change.
		(1<<TXCIE)|						// Enable Transmission Complete,
		(0<<UDRIE)|						// Disable UDRE interrupt.
		(1<<UCSZ2)|(0<<TXB8);			// No change.
	}
}
		DEBUG(0x37);
     3ba:	a9 98       	cbi	0x15, 1	; 21
ISR (USART_UDRE_vect) {
	if(TxDBytes){			// if there's more data to be sent...
     3bc:	81 11       	cpse	r24, r1
     3be:	13 c0       	rjmp	.+38     	; 0x3e6 <__vector_12+0x42>
		UDR = *pTxd; 		// write next byte to data buffer.
		TxDBytes--;				// A byte has been transmitted, so decrement...
		pTxd++;
	}
	else {
		DEBUG(0x38);
     3c0:	a9 98       	cbi	0x15, 1	; 21
     3c2:	a9 98       	cbi	0x15, 1	; 21
     3c4:	a9 98       	cbi	0x15, 1	; 21
     3c6:	a9 9a       	sbi	0x15, 1	; 21
     3c8:	a9 9a       	sbi	0x15, 1	; 21
     3ca:	a9 9a       	sbi	0x15, 1	; 21
     3cc:	a9 98       	cbi	0x15, 1	; 21
     3ce:	a9 9a       	sbi	0x15, 1	; 21

Doing magic with a USD 7 Logic Analyser: https://www.avrfreaks.net/comment/2421756#comment-2421756

Bunch of old projects with AVR's: http://www.hoevendesign.com

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

You're not showing enough of either the source or the .lss file .
E.g. TxDBytes is not declared. I suspect that it is not volatile.
Non-volatile accesses can be moved around volatile accesses.

Note that getting precise or even consistent timing from C source can be difficult.

"SCSI is NOT magic. There are *fundamental technical
reasons* why it is necessary to sacrifice a young
goat to your SCSI chain now and then." -- John Woods

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

Paulvdh wrote:
Optimisation breaks (sic) debug code

It would be more accurate to say, "Optimisation breaks faulty code" - the fault being trying to rely upon the timing of compiler-generated code.

skeeve wrote:
Note that getting precise or even consistent timing from C source can be difficult.

Absolutely!
That is in the very nature of using a High-Level Language!

http://www.8052.com/forum/read/1...

Top Tips:

  1. How to properly post source code - see: https://www.avrfreaks.net/comment... - also how to properly include images/pictures
  2. "Garbage" characters on a serial terminal are (almost?) invariably due to wrong baud rate - see: https://learn.sparkfun.com/tutorials/serial-communication
  3. Wrong baud rate is usually due to not running at the speed you thought; check by blinking a LED to see if you get the speed you expected
  4. Difference between a crystal, and a crystal oscillatorhttps://www.avrfreaks.net/comment...
  5. When your question is resolved, mark the solution: https://www.avrfreaks.net/comment...
  6. Beginner's "Getting Started" tips: https://www.avrfreaks.net/comment...
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The C compiler can't guarantee any timing.

However, your SBI/CBI implies that you are writing to a 'lower' PORT. Since the SFRs are declared as volatile, the Optimiser is not allowed to remove the access.

Why not use a real peripheral. e.g. a spare USART_MSPI can blit asynchronous data on a single pin at F_CPU/2. i.e. faster than your SBI/CBI sequence and without using any CPU cycles. (apart from 2 cycles: LDI reg,N / OUT UDR,reg)

And your Logic Analyser will get accurate timing.

DAvid.

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

@skeeve: TxDBytes is being declared as:

static volatile uint8_t TxDBytes;	// ByteCounter for outgoing data

@awneil: I'm not blaming the compiler. I know I'm ordering it to shuffly my code around and mess with timing when I'm using optimisation. I was just wondering if there was an easy fix for the quick hack of these little debug macro's.

@David.prentice:
For the project I'm working on both the UART and SPI are being used. TWI won't get ack's from the logic analyser, I could use a spare timer for pwm output.

Using hardware is not faster, sbi & cbi also result in a bitrate of F_CPU/2 but you are right about lower overhead and less impact on the rest of the timing.

The message is clear. I'ts just not worth my (or your) time to put much effort in this. Suppose it's getting time to invest a bit in more hardware and/or use bigger cpu's just for debugging.

I'm also afraid i'm a bit stuck in te '90s. Haven't looked into debugwire yet and I've read some rumors avrstudio should be working under Linux nowadays.

Doing magic with a USD 7 Logic Analyser: https://www.avrfreaks.net/comment/2421756#comment-2421756

Bunch of old projects with AVR's: http://www.hoevendesign.com

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

Quote:

I've read some rumors avrstudio should be working under Linux nowadays

Not unless you have some Windows-emulating layer on that Lunix system..

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

I realised that a SPI or UART was unlikely to be going spare.

Actually, a timing wiggle on a spare pin is very handy for a Logic Analyser. In practice, a single or double blip is good enough to identify which code caused it.

I have no idea how critical your timing is.

Yes, debugWIRE + LA is an excellent combination. The dW will let you view memory, registers, ... and the LA will record and analyse the hardware signals.

Rowley will work natively on Linux.
You would need to run Windows in a VirtualBox for AS6.

Some people use Eclipse or Code::Blocks
As far as I know, there are no 'wash-and-go' versions of either.

At least AS6, Rowley, C-SPY, ... will run out of the box on Windows. (Rowley runs out of the box on Linux, Mac as well)

David.

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

Quote:

As far as I know, there are no 'wash-and-go' versions of either.

C::B is about as close as you get - all you need do is basically tell it the directory where the compiler and binutils are located.

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

So you can install C::B and it will understand debugWIRE, AVR special function registers, ...

If it will only 'build' your program, it does not seem much more useful than a regular Makefile.

There are any number of IDEs that can group together a set of source files, search paths, Symbols, ...

David.

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

Quote:

So you can install C::B and it will understand debugWIRE, AVR special function registers, ...

Oh no, sorry I thought you meant in the sense "is there a C::B complete with avr-gcc?". I was simply saying there isn't but telling it where to find it is about as simple as it was for AS4. But, no, there's no easy solution for debugging. Sure C::B will easily invoke avr-gdb but the setting up of avraice to make the link between that and the Atmel debugger is left as an easy exercise for the reader and, no, it knows nothing about SFR names (apart from the obvious C source annotation when you read and write SFRs by name) but the debugger itself does not label I/O location 0x05 as DDRC or whatever it is.

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

This kind of thing is why one uses assembly.
If the time available allows for a standard function call,
you can write a separate assembly function in a .S file.
If little time is available, in-line assembly will be necessary.
Use .if pseudo-ops and a pair of "I" constraints.

"SCSI is NOT magic. There are *fundamental technical
reasons* why it is necessary to sacrifice a young
goat to your SCSI chain now and then." -- John Woods

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

Surely, this is the kind of thing where you use the most appropriate tools at your disposal.

e.g. Logic Analyser. --- already owned by OP
e.g. Debugger ---- I bet the OP can beg, steal, borrow a Windows licence and run AS6

If the OP intends serious development, his employer would buy the relevant licences for the proven tools.

Incidentally, his macros could be made 'timing-safe'.
You can avoid ASM. Simply disable interrupts and avoid -Os making subroutines.

David.

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

david.prentice wrote:
Incidentally, his macros could be made 'timing-safe'.
You can avoid ASM. Simply disable interrupts and avoid -Os making subroutines.
If there is time for a subroutine call,
OP can probably avoid ASM.
He will need optimization if he expects to get cbi and sbi instructions.
The code is for an ISR. Interrupts are already disabled.

"SCSI is NOT magic. There are *fundamental technical
reasons* why it is necessary to sacrifice a young
goat to your SCSI chain now and then." -- John Woods

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

Ok, you don't have to worry about IRQ.

Depending on your Optimisation strategy, you should be pretty safe. The cost-benefit of code compression is clearly bad for timing.

Since this is a "Debug" macro, I would just try it and see. I would put money on it producing a good sequence of SBI/CBI. Only the brain-dead -O0 will produce bad code. But hey-ho, isn't that what it is supposed to do.
Anyway, the Release is never going to see the effect of the Debug macro.

There are always alternatives. e.g. an AVR with more GPIO. A LA with more channels. A debugger that runs on your chosen operating system. ...

From the look of the example, you could debug it with pencil and paper.

David.

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

OP seems to want to debug code with the release level of optimization.
A sequence of cbi's and sbi's should not affect the compiler's register strategy too much
A call will.
Even in-line assembly could affect it a little.
In-line assembly implicitly clobbers R0.

"SCSI is NOT magic. There are *fundamental technical
reasons* why it is necessary to sacrifice a young
goat to your SCSI chain now and then." -- John Woods

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

A solution could be to put the entire sequence of instructions in inline assembly, using conditional assembly to choose between sbi and cbi.

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

It seems as though the compiler is 'too smart' for you in this case.

Looks like it saw that the _CLEAR for the start bit of the DEBUG(0x37) (which is not taken) is the same as the start bit of the DEBUG(0x38) (which is taken):

		DEBUG(0x37);
     3ba:	a9 98       	cbi	0x15, 1	; 21
.
.
.
		DEBUG(0x38);
     3c0:	a9 98       	cbi	0x15, 1	; 21
     3c2:	a9 98       	cbi	0x15, 1	; 21
     3c4:	a9 98       	cbi	0x15, 1	; 21
     3c6:	a9 9a       	sbi	0x15, 1	; 21
     3c8:	a9 9a       	sbi	0x15, 1	; 21
     3ca:	a9 9a       	sbi	0x15, 1	; 21
     3cc:	a9 98       	cbi	0x15, 1	; 21
     3ce:	a9 9a       	sbi	0x15, 1	; 21

Perhaps you can 'outsmart' the compiler (although it may get smarter in a future release):

   // Startbit, a bunch of data bits and a stop bit.
   #define DEBUG(X) (DEBUG_((X<<1) | 0x0100))
   #define DEBUG_(X)   (_BIT(X,0), \
         _BIT(X,1), _BIT(X,2), _BIT(X,3),_BIT(X,4),\
         _BIT(X,5), _BIT(X,6), _BIT(X,7), \
         _BIT(X,8))

If the compiler is still 'too smart', you could always use a host program to generate 128 separate snippets of inline assembler:

#include 

int main(void) {

  int byte, bit;
  
  fputs("#define DEBUG(X) \\\n", stdout);
  fputs("  switch (X) { \\\n", stdout);
  for (byte=0; byte<128; byte++) {
    printf("    case 0x%02X: \\\n", byte);
    fputs ("      __asm__ __volatile__ ( \\\n", stdout);
    // Start bit
    fputs("          \"cbi %[port], %[pin] \\n\\t\" \\\n", stdout);
    // Data bits
    for (bit=0; bit<7; bit++) {
      if (byte & (1<<bit)) {
        fputs("          \"sbi %[port], %[pin] \\n\\t\" \\\n", stdout);
      }
      else {
        fputs("          \"cbi %[port], %[pin] \\n\\t\" \\\n", stdout);
      }
    }
    // Stop bit
    fputs("          \"sbi %[port], %[pin] \\n\\t\" \\\n", stdout);
    fputs("       :: [port] \"I\" (_SFR_IO_ADDR(DEBUG_PORT)), \\\n", stdout);
    fputs("          [pin]  \"I\" (DEBUG_BIT)); \\\n", stdout);
    fputs("      break; \\\n", stdout);
  }
  fputs("  }\n\n", stdout);

  return 0;
  
}

Build and run that on a host PC, push the output into a .h, and include that in your AVR project. Add whatever #ifdef wrappers you like as before.

Of course, that will have the effect (as already noted) of clobbering r0.

///////////////////////////////////////////////////////////////////////////////

"Experience is what enables you to recognise a mistake the second time you make it."

"Good judgement comes from experience.  Experience comes from bad judgement."

"Wisdom is always wont to arrive late, and to be a little approximate on first possession."

"When you hear hoofbeats, think horses, not unicorns."

"Fast.  Cheap.  Good.  Pick two."

"We see a lot of arses on handlebars around here." - [J Ekdahl]

 

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

Here is the inline assembly:

#define DEBUG(X) \
    asm (" cbi %[dport], %[dbit]\n" \
         " .irpc bit,01234567\n" \
         "   .if %[X] & (1<<\bit)\n" \
         "      sbi %[dport], %[dbit]\n" \
         "   .else\n" \
         "      cbi %[dport], %[dbit]\n" \
         "   .endif\n" \
         " .endr\n" \
         " sbi %[dport], %[dbit]" \
         : : [dport] "I"(_SFR_IO_ADDR(DEBUG_PORT)), \
              [dbit] "I"(DEBUG_BIT), \
                 [X] "M"(X) ) ;

If one threatens the compiler or its children,
one might be able to get what is desired from "plain" C,
but I recommend against it.
If one is going for a precise sequence of machine instructions,
one should specify them.

Note that even with volatile, asm statements with no outputs are subject to code motion.
To nail it down between two uses of a variable, one can add it to the io list:

#define DEBUG(X, nail) \
    asm (" cbi %[dport], %[dbit]\n" \
         " .irpc bit,01234567\n" \
         "   .if %[X] & (1<<\bit)\n" \
         "      sbi %[dport], %[dbit]\n" \
         "   .else\n" \
         "      cbi %[dport], %[dbit]\n" \
         "   .endif\n" \
         " .endr\n" \
         " sbi %[dport], %[dbit]" \
         : "+r"(nail) \
         : [dport] "I"(_SFR_IO_ADDR(DEBUG_PORT)), \
            [dbit] "I"(DEBUG_BIT), \
               [X] "M"(X) ) ;

"SCSI is NOT magic. There are *fundamental technical
reasons* why it is necessary to sacrifice a young
goat to your SCSI chain now and then." -- John Woods

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

"Experience is what enables you to recognise a mistake the second time you make it."

"Good judgement comes from experience.  Experience comes from bad judgement."

"Wisdom is always wont to arrive late, and to be a little approximate on first possession."

"When you hear hoofbeats, think horses, not unicorns."

"Fast.  Cheap.  Good.  Pick two."

"We see a lot of arses on handlebars around here." - [J Ekdahl]

 

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

I do not get the point. Syntax? File level? Something else?

"SCSI is NOT magic. There are *fundamental technical
reasons* why it is necessary to sacrifice a young
goat to your SCSI chain now and then." -- John Woods

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

skeeve wrote:
I do not get the point. Syntax? File level? Something else?
No clobber.

"Experience is what enables you to recognise a mistake the second time you make it."

"Good judgement comes from experience.  Experience comes from bad judgement."

"Wisdom is always wont to arrive late, and to be a little approximate on first possession."

"When you hear hoofbeats, think horses, not unicorns."

"Fast.  Cheap.  Good.  Pick two."

"We see a lot of arses on handlebars around here." - [J Ekdahl]

 

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

joeymorin wrote:
skeeve wrote:
I do not get the point. Syntax? File level? Something else?
No clobber.
No need for clobber: only a port register changes.

"SCSI is NOT magic. There are *fundamental technical
reasons* why it is necessary to sacrifice a young
goat to your SCSI chain now and then." -- John Woods

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

skeeve wrote:
No need for clobber: only a port register changes.
skeeve wrote:
In-line assembly implicitly clobbers R0.

"Experience is what enables you to recognise a mistake the second time you make it."

"Good judgement comes from experience.  Experience comes from bad judgement."

"Wisdom is always wont to arrive late, and to be a little approximate on first possession."

"When you hear hoofbeats, think horses, not unicorns."

"Fast.  Cheap.  Good.  Pick two."

"We see a lot of arses on handlebars around here." - [J Ekdahl]

 

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

joeymorin wrote:
skeeve wrote:
No need for clobber: only a port register changes.
skeeve wrote:
In-line assembly implicitly clobbers R0.
I should have written "avr-gcc implicitly puts R0 in the clobber list."

From the inline assembly cookbook:

Quote:
Register r0 may be freely used by your assembler code and need not be restored at the end of your code.
To me, this is a requirements flaw.
If the compiler is using R0 for pretty much anything, the flaw will cost at least two cycles.

"SCSI is NOT magic. There are *fundamental technical
reasons* why it is necessary to sacrifice a young
goat to your SCSI chain now and then." -- John Woods

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

skeeve wrote:
I should have written "avr-gcc implicitly puts R0 in the clobber list."
...
To me, this is a requirements flaw.
If the compiler is using R0 for pretty much anything, the flaw will cost at least two cycles.
That was my interpretation.

The point I'm trying to make is that using basic asm instead of extended asm would appear to avoid this cost since the compiler doesn't parse basic asm statements:

Quote:
Unlike Extended asm, all Basic asm blocks are implicitly volatile. See Volatile. Similarly, Basic asm blocks are not treated as though they used a "memory" clobber (see Clobbers).
There is no direct mention that the normally implicitly clobbered r0 is dispensed with, but I suspect it is since a basic asm statement is essentially and entirely invisible to the compiler.

I may at some point devise a test of this assumption... but not today ;)

"Experience is what enables you to recognise a mistake the second time you make it."

"Good judgement comes from experience.  Experience comes from bad judgement."

"Wisdom is always wont to arrive late, and to be a little approximate on first possession."

"When you hear hoofbeats, think horses, not unicorns."

"Fast.  Cheap.  Good.  Pick two."

"We see a lot of arses on handlebars around here." - [J Ekdahl]

 

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

joeymorin wrote:
skeeve wrote:
I should have written "avr-gcc implicitly puts R0 in the clobber list."
...
To me, this is a requirements flaw.
If the compiler is using R0 for pretty much anything, the flaw will cost at least two cycles.
That was my interpretation.

The point I'm trying to make is that using basic asm instead of extended asm would appear to avoid this cost since the compiler doesn't parse basic asm statements:

Quote:
Unlike Extended asm, all Basic asm blocks are implicitly volatile. See Volatile. Similarly, Basic asm blocks are not treated as though they used a "memory" clobber (see Clobbers).
There is no direct mention that the normally implicitly clobbered r0 is dispensed with, but I suspect it is since a basic asm statement is essentially and entirely invisible to the compiler.
The quote about basic asm is about the generic gcc.
The AVR inline assembly cookbook is specific to avr-gcc.
I'd expect the latter to be correct.
'Tis my understanding that the gcc guys are not terribly interested in 8-bit micros in general or AVRs in particular.
Their coding rules explicitly state that one need not consider the possibility that int will be less than 32 bits.
Presumably that is why the -lm-less floating point does not work.
Quote:
I may at some point devise a test of this assumption... but not today ;)
Could be tricky.

"SCSI is NOT magic. There are *fundamental technical
reasons* why it is necessary to sacrifice a young
goat to your SCSI chain now and then." -- John Woods

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

skeeve wrote:
The quote about basic asm is about the generic gcc.
The AVR inline assembly cookbook is specific to avr-gcc.
I'd expect the latter to be correct.
Fair point, but the AVR inline assembly cookbook makes zero mention of basic asm statements. Nevertheless, they appear to be supported in AVR GCC.
skeeve wrote:
Quote:
I may at some point devise a test of this assumption... but not today ;)
Could be tricky.
Indeed. I can't conjure up a clear method at the moment.

I expect it might require enlisting the help of a couple of (probably volatile) extended asm statements which do clobber r0 (implicitly or explicitly) with which to sandwich a basic asm statement and some 'target' C code, in an attempt to fence-in the compiler and see what it does. I also expect looking at -O0 may be revealing.

The perceived need to determine the truth of this assumption rests in the notion that the OPs debugging code would alter the code he is seeking to debug, perhaps in subtle ways relating to the compiler's use of r0. Certainly the importance of this notion hasn't been made clear, and no instance of such an effect has been offered up for examination... so madly dancing on a pin again?

"Experience is what enables you to recognise a mistake the second time you make it."

"Good judgement comes from experience.  Experience comes from bad judgement."

"Wisdom is always wont to arrive late, and to be a little approximate on first possession."

"When you hear hoofbeats, think horses, not unicorns."

"Fast.  Cheap.  Good.  Pick two."

"We see a lot of arses on handlebars around here." - [J Ekdahl]