How to initialize an attiny85 by code

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

Hi folks,

 

I've written an I2C bootloader in C that's mostly working except, so far, for one thing: when there are I2C communication errors during the transmission of the user application from the I2C master, the bootloader makes a "safety flash erase" (this is by design) but, after trying again to transmit-flash the application, it writes garbage on the first page of the flash.

However, if I do a power-on reset after erasing, the application is flashed correctly.

 

Then I have two questions:

- How should I initialize by code all things related to self-programming after erasing memory, before retrying to write again? (registers, temporary page buffer, etc). If you have code examples, I will appreciate it.

- What things are recommended to initialize in general to imitate a reset on the Tiny 85? At the moment I'm only disabling the watchdog and the interrupts.

 

Thank you very much for your help!

 

This topic has a solution.
Last Edited: Tue. Oct 2, 2018 - 02:21 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

casanovg wrote:
How should I initialize by code all things related to self-programming after erasing memory, before retrying to write again?
The ultimate would be a watchdog reset which (almost) returns a chip to the power on state.

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

Thank you for answering so fast ...

.

clawson wrote:
The ultimate would be a watchdog reset which (almost) returns a chip to the power on state.

 

Yes, that's the workaround I've implemented, but it takes forever to make the WDT reset and, in the meantime, the tiny seizes the i2c bus (at least in my test setup). I would prefer a code solution if its possible.

Last Edited: Wed. Sep 26, 2018 - 01:20 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

casanovg wrote:
but it takes forever to make the WDT reset
15 ms is by you forever?

 

"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."

"Read a lot.  Write a lot."

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

 

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

joeymorin wrote:
15 ms is by you forever?

 

It is not a problem, when I made the posting I had not noticed that WDTO_1S meant 1 second delay. However, the issue of the Tiny "seizing" the i2c bus while resetting is still happening. Is there a code initialization that everybody uses to replace the wdt reset?

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

casanovg wrote:
However, the issue of the Tiny "seizing" the i2c bus while resetting is still happening.
After the 15 ms of waiting for the reset, the bus will be released, as all of the tiny's I/O pins go high-impedance immediately.  Reset will likely take in the neighbourhood of 65 ms, depending on clock source and SUT fuses, but as mentioned the bus is already released by then.

 

Since you're using the t85, you must be implementing I2C at least partly in software, since that device lacks a full I2C peripheral.  I would look to your implementation of I2C in your bootloader and compare that to the published standard.

 

I'm also not convinced that this is your problem in the first place, at least not your >>only<< problem.   If you're trying to solve the "after trying again to transmit-flash the application, it writes garbage on the first page of the flash" problem, I would look to your bootloader code.  There's nothing in the hardware which would inherently lead to that problem.  If your first flash page is 'garbage', i'd suggest that you're reloading the temporary buffer page after it has already been (at least partially) loaded with data during the previous (failed) bootload attempt.  The temporary buffer is erased only under certain conditions, including a page write, but not including a page erase, so if the buffer had some data in it before you encountered the I2C error which you then follow by a 'safety flash erase', that erasure is not sufficient for wiping the temporary buffer.  Simply overwriting the temporary buffer with new data is not sufficient.  It must be erased every time, just like a flash page.  You can deliberately clear the temporary buffer simply writing the CTPB bit in SPMCSR to 1.  Are you doing this?

 

I would work to determine exactly what is happening rather than using the blunt-force workaround of a device reset to circumvent what is surely a software bug in your own code.

"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."

"Read a lot.  Write a lot."

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

 

Last Edited: Sat. Sep 29, 2018 - 08:17 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Thanks for your thorough reply.

After the 15 ms of waiting for the reset, the bus will be released

Yes, it is released as you describe it. The point is that the application will have more than one attiny85 slave on the i2c bus. While the i2c master would eventually be busy updating the attiny firmware one at a time, I do not know if it could bring additional consequences that one of them takes the bus. Maybe there is no problem, but if I could avoid this behavior (avoiding the wdt reset) it would be better for me.

I would look to your implementation of I2C in your bootloader

I am using Don Blake's USI TWI slave driver, slightly modified to work without interrupts. The bootloader code is here, just in case you want to take a look: https://github.com/casanovg/timonel/blob/master/timonel-bootloader/tml-bootloader.c

CTPB bit in SPMCSR to 1.  Are you doing this?

Yes, I have tried this by executing it immediately after erasing the memory (after an app upload with i2c errors), but the result was the same, garbage on the first page. By the way, is it necessary to reset this bit again before filling the buffer? I could not find this specified in the datasheet. The only thing that is working well so far is the WDT reset, but it has the "bus hang" issue.

 

It is very likely that, as you say, the code has basic errors that I still can't see.

 

Last Edited: Sun. Sep 30, 2018 - 05:56 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I have looked at neither your code nor USI_TWI_Slave.c in any depth.  Have you tried calling UsiTwiSlaveInit() when I2C gets crowbarred, instead of resetting via the WDT?  If that doesn't work, then I'd start looking at the I2C library to see how it works, and why calling UsiTwiSlaveInit() leaves the crowbar in place.

"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."

"Read a lot.  Write a lot."

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

 

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

So far, I'm not directly calling UsiTwiSlaveInit () in the bootloader code when the error occurs, I just call it in the setup block.

 

But, among the multiple tests that I have done trying to solve the first-page problem, I have done this: when an i2c error occurs, the i2c master forces an application erasing and a jump to the beginning of the application. As the application no longer exists, the T85 ends again in the bootloader, therefore running the setup block.

 

In short, yes, I've tried calling UsiTwiSlaveInit () after the error, but with the same result, garbage on the first page. Besides this, the i2c comm appear to have been recovered after the error, since "erase-jump to app" master's commands are fulfilled by the T85.

 

What I expected to find, is a solution to improve the setup block that, in my view, is missing some initializations to start well (without the WDT reset).

Last Edited: Sun. Sep 30, 2018 - 01:13 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Then it sounds like your I2C issue is actually a non-issue.  The only remaining problem is the flash page corruption.  Study <avr/boot.h>.

"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."

"Read a lot.  Write a lot."

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

 

Last Edited: Sun. Sep 30, 2018 - 04:54 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I do not see using the WDT timeout as a "workaround", but as the prefered way to generate a reset from within software.

 

Another way would be to route a wire from an I/O pin to the reset pin.

 

If you have troubles with a 15ms timeout before the WDT triggers, you can start by disabling all interrupts.

 

If you do not want to use the WDT at all, then first disable all interrupts, and if you want to use a peripheral, then thouroughly initialize that peripheral before you attempt to use it. Write full bytes to all the registers used by that peripheral.

If you are fiddling with an AVR on this level, you should also study the inner workings of the startup code.

In a C program the compiler adds some stuff before main( ) starts.

There is some software to initialize the stack pointer and the zero register and there are some loops to copy static data from flash to ram.

Paul van der Hoeven.
Bunch of old projects with AVR's:
http://www.hoevendesign.com

This reply has been marked as the solution. 
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I do not see using the WDT timeout as a "workaround", but as the prefered way to generate a reset from within software.

It's a workaround because a reset shouldn't be necessary to begin with.  The only reason the OP is resetting the device is to work around the problem of a corrupted first flash page written after an I2C recovery.  I posit that this is due to the temporary page buffer containing previously inserted data.  I proposed the use of the CTPB bit in SPMCSR, but the OP claims to have tried that without success.

 

Another look at the datasheet reveals some conflicting information regarding the CTPB bit.  First:

 

 

... and:

 

 

Those two passages suggest that the only thing necessary to achieve the erasure of the temporary page buffer is the writing of CTPB to 1.  However:

 

 

That passage suggests that one must write CTPB to 1 at the same time that SPMEN is writting to 1, and that one must then execute an SPM instruction after setting CTPB (perhaps within 4 clock cycles, as with other SPM operations).  Although the passage "... will have a special meaning, as described elsewhere." suggests that this may not be so, since no such passage exists for CTPB, it is at least worth a shot I think.  There is, one way or another, a datasheet errata here.  Either clarity on the use of CTPB with SPMEN and the SPM instruction is lacking, or the suggesting that they be used together is incorrect.

 

In any event, I'd propose this instead of your call to wdt_enable().  First define these:

#define BOOT_TEMP_BUFF_ERASE         (_BV(__SPM_ENABLE) | _BV(CTPB))
#define boot_temp_buff_erase()                   \
(__extension__({                                 \
    __asm__ __volatile__                         \
    (                                            \
        "sts %0, %1\n\t"                         \
        "spm\n\t"                                \
        :                                        \
        : "i" (_SFR_MEM_ADDR(__SPM_REG)),        \
          "r" ((uint8_t)(BOOT_TEMP_BUFF_ERASE))  \
    );                                           \
}))

And then call it:

boot_temp_buff_erase();

I've written the above macros to conform to the format and style used in <avr/boot.h>.

 

Note that you'll then need to handle a restart of your bootloader without relying on an actual device reset.  You could simply use break; to fall through your outer eternal for(;;) loop, and wrap the entire contents of main() in another loop, thereby rerunning your init code.

 

Something like:

#define BOOT_TEMP_BUFF_ERASE         (_BV(__SPM_ENABLE) | _BV(CTPB))
#define boot_temp_buff_erase()                   \
(__extension__({                                 \
    __asm__ __volatile__                         \
    (                                            \
        "sts %0, %1\n\t"                         \
        "spm\n\t"                                \
        :                                        \
        : "i" (_SFR_MEM_ADDR(__SPM_REG)),        \
          "r" ((uint8_t)(BOOT_TEMP_BUFF_ERASE))  \
    );                                           \
}))

// Function Main
int main() {
    /*  ___________________
       |                   |
       |    Restart Loop   |
       |___________________|
    */
    for (;;) {
        /*  ___________________
           |                   |
           |    Setup Block    |
           |___________________|
        */
        MCUSR = 0;                              /* Disable watchdog */
        WDTCR = ((1 << WDCE) | (1 << WDE));
        WDTCR = ((1 << WDP2) | (1 << WDP1) | (1 << WDP0));
        cli();                                  /* Disable Interrupts */
#if ENABLE_LED_UI
        LED_UI_DDR |= (1 << LED_UI_PIN);        /* Set led pin data direction register for output */
#endif
        CLKPR = (1 << CLKPCE);                  /* Set the CPU prescaler for 8 MHz */
        CLKPR = (0x00);
        UsiTwiSlaveInit(I2C_ADDR);              /* Initialize I2C */
        Usi_onReceiverPtr = ReceiveEvent;       /* I2C Receive Event */
        Usi_onRequestPtr = RequestEvent;        /* I2C Request Event */
        statusRegister = (1 << ST_APP_READY);   /* In principle, assume that there is a valid app in memory */
        word dlyCounter = TOGGLETIME;
        byte exitDly = CYCLESTOEXIT;            /* Delay to exit bootloader and run the application if not initialized */
        /*  ___________________
           |                   |
           |     Main Loop     |
           |___________________|
        */
        for (;;) {
            // Initialization check
            if ((statusRegister & ((1 << ST_INIT_1) + (1 << ST_INIT_2))) != \
            ((1 << ST_INIT_1) + (1 << ST_INIT_2))) {
                // ============================================
                // = Blink led until is initialized by master =
                // ============================================
                if (dlyCounter-- <= 0) {
#if ENABLE_LED_UI
                    LED_UI_PORT ^= (1 << LED_UI_PIN);   /* Blinks on each main loop pass at TOGGLETIME intervals */
#endif
                    dlyCounter = TOGGLETIME;
                    if (exitDly-- == 0) {
                        RunApplication();
                    }
                }
            }
            else {
                if (dlyCounter-- <= 0) {                /* Decrease dlyCounter on each main loop pass until it */
                    //                                  /* reaches 0 before running code to allow I2C replies  */
                    // =======================================
                    // = Exit bootloader and run application =
                    // =======================================
                    if ((statusRegister & ((1 << ST_EXIT_TML) + (1 << ST_APP_READY))) == \
                    ((1 << ST_EXIT_TML) + (1 << ST_APP_READY))) {
                        RunApplication();                       /* Launch application */
                    }
                    if ((statusRegister & ((1 << ST_EXIT_TML) + (1 << ST_APP_READY))) == \
                    (1 << ST_EXIT_TML)) {
                        statusRegister |= (1 << ST_DEL_FLASH);  /* Set Erase flash */
                    }
                    // ========================================
                    // = Delete application from flash memory =
                    // ========================================
                    if ((statusRegister & (1 << ST_DEL_FLASH)) == (1 << ST_DEL_FLASH)) {
#if ENABLE_LED_UI
                        LED_UI_PORT |= (1 << LED_UI_PIN);       /* Turn led on to indicate erasing ... */
#endif
                        word pageAddress = TIMONEL_START;       /* Erase flash ... */
                        while (pageAddress != RESET_PAGE) {
                            pageAddress -= PAGE_SIZE;
                            boot_page_erase(pageAddress);
                        }
                        //SPMCSR |= (1 << CTPB);                  /* Clear temporary buffer */
                        //SPMCSR &= ~(1 << CTPB);                 /* Is this necessary? */
                        boot_temp_buff_erase();
                        break;
                    }
                    // ========================================================================
                    // = Write received page to flash memory and prepare to receive a new one =
                    // ========================================================================
                    if ((pageIX == PAGE_SIZE) & (flashPageAddr < TIMONEL_START)) {
#if ENABLE_LED_UI
                        LED_UI_PORT ^= (1 << LED_UI_PIN);   /* Turn led on and off to indicate writing ... */
#endif
                        boot_page_erase(flashPageAddr);
                        boot_page_write(flashPageAddr);
                        word tpl = (((~((TIMONEL_START >> 1) - ((((appResetMSB << 8) | appResetLSB) + 1) & 0x0FFF)) + 1) & 0x0FFF) | 0xC000);
                        if (flashPageAddr == RESET_PAGE) {    /* Calculate and write trampoline */
                            for (int i = 0; i < PAGE_SIZE - 2; i += 2) {
                                boot_page_fill((TIMONEL_START - PAGE_SIZE) + i, 0xffff);
                            }
                            //boot_page_fill((TIMONEL_START - 2), (((~((TIMONEL_START >> 1) - ((((appResetMSB << 8) | appResetLSB) + 1) & 0x0FFF)) + 1) & 0x0FFF) | 0xC000));
                            boot_page_fill((TIMONEL_START - 2), tpl);
                            boot_page_write(TIMONEL_START - PAGE_SIZE);
                        }
                        if ((flashPageAddr) == (TIMONEL_START - PAGE_SIZE)) {
                            /*
                            Read the previous page to the bootloader start  - OK!
                            Write it to the temporary buffer                - OK!
                            Check if the last two bytes are 0xFF            - OK!
                            If yes, then the the application fits in memory, flash the trampoline again - OK!
                            If no, it means that the application is too big, erase it!                  - OK!
                            TODO: Implement ALLOW_USE_TPL_PG (allow use trampoline page).
                            TODO: Implement AUTO_TPL_CALC (auto-trampoline calculation & flash), if it is not
                                  selected, the i2c master has to calculate the trampoline jump and pass it,
                                  maybe with GETTMNLV.
                            TODO: Implement a GETTMNLV reply code that indicates the commands available.
                            */
                            const __flash unsigned char * flashAddr;
                            for (byte i = 0; i < PAGE_SIZE - 2; i += 2) {
                                flashAddr = (void *)((TIMONEL_START - PAGE_SIZE) + i);
                                word pgData = (*flashAddr & 0xFF);
                                pgData += ((*(++flashAddr) & 0xFF) << 8);
                                boot_page_fill((TIMONEL_START - PAGE_SIZE) + i, pgData);
                            }
                            flashAddr = (void *)(TIMONEL_START - 2);
                            word pgData = (*flashAddr & 0xFF);
                            pgData += ((*(++flashAddr) & 0xFF) << 8);
                            if (pgData == 0xFFFF) {
                                boot_page_fill(TIMONEL_START - 2, tpl);
                                boot_page_erase(TIMONEL_START - PAGE_SIZE);
                                boot_page_write(TIMONEL_START - PAGE_SIZE);
                            }
                            else {                                      /* Application too big to fit in memory! */
                                statusRegister |= (1 << ST_DEL_FLASH);  /* Set Erase flash */
                            }
                        }
                        flashPageAddr += PAGE_SIZE;
                        pageIX = 0;
                    }
                    //dlyCounter = I2CDLYTIME;
                }
            }
            /* ..................................................
               . I2C Interrupt Emulation ...................... .
               . Check the USI Status Register to verify        .
               . whether a USI start handler should be launched .
               ..................................................
            */
            if (USISR & (1 << USISIF)) {
                UsiStartHandler();      /* If so, run the USI start handler ... */
                USISR |= (1 << USISIF); /* Reset the USI start flag in USISR register to prepare for new ints */
            }
            /* .....................................................
               . I2C Interrupt Emulation ......................... .
               . Check the USI Status Register to verify           .
               . whether a USI counter overflow should be launched .
               .....................................................
            */
            if (USISR & (1 << USIOIF)) {
                UsiOverflowHandler();   /* If so, run the USI overflow handler ... */
                USISR |= (1 << USIOIF); /* Reset the USI overflow flag in USISR register to prepare for new ints */
            }
        }
    }
    return 0;
}

If this works, then you may want to submit it to the AVR Libc team, and submit a datasheet errata to Atmel.

 

If I have time I'll try a test myself on a t85.  Not sure when I'll have time though...

 

EDIT:  note that a break and an outer for(;;) loop may not be sufficient, as there are variables in .bss and .data which will not get reinitialised that way.  I haven't perused your code to see if this will be a problem.  I expect it will though.  Might be better to do a complete restart of your bootloader with an rjmp or an ijmp or the like, but this will require knowledge of the bootloader's start address.  There are a number of ways to do this.

 

Even that may not be sufficient, as that will still leave SFR registers uninitialised.  Again, I've not perused your code.  You're in a better position to do the analysis.

 

I expect that you've already experimented with non-wdt solutions to 'restarting' your bootloader before you abandoned attempts with CTPB.  I would return to whatever you had in place then, but use the macros I define above to attempt the temp buffer erase.

 

EDIT: extraneous comma removed from macro

"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."

"Read a lot.  Write a lot."

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

 

Last Edited: Tue. Oct 2, 2018 - 03:12 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Thank you!

If you are fiddling with an AVR on this level, you should also study the inner workings of the startup code.

I need to solve asap the problem exposed in my first post, once that is done, I will study boot.h more relaxed, I hope ...

 

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

Joey, thank you very much again for taking the time to help me with this.

 

I will try with your boot_temp_buff_erase() macro right now.

 Might be better to do a complete restart of your bootloader with an rjmp or an ijmp or the like

I expect that you've already experimented with non-wdt solutions to 'restarting' your bootloader before you abandoned attempts with CTPB

Under "normal" situations, that is, with a properly received and flashed application, I'm not using the WDT to exit the bootloader, I am jumping to a fixed memory location two bytes before the bootloader start (a trampoline), which contains a rjmp that points to the beginning of the actual application. The trampoline is set (flashed) together with the application. The restart vector at position 0, when the memory is loaded with an application, always points to the start address of the bootloader.

 

So, to restart the bootloader after a defective application reception, I simply jump to the trampoline. As the memory and the trampoline are deleted as soon as a reception problem occurs, the Tiny again ends up in the bootloader code, which (I assume) is the bootloader setup block. I'll place the call to your macro right there, in the setup block.

 

I'll let you know how it was ...

 

Last Edited: Sun. Sep 30, 2018 - 10:51 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

casanovg wrote:
Tiny again ends up in the bootloader code, which (I assume) is the bootloader setup block.
Maybe not.

 

If you're using the last word before your bootloader, note that it will contain 0xFFFF after an erase.  This is not a NOP instruction.  In fact, it decodes to:

sbrs r31,7

So if bit 7 of r31 is 0, it won't skip, and is effectively a NOP.  However, if bit 7 of r31 is 1, the result will be that it will skip the first instruction of your bootloader.

 

Maybe r31[7] is always 0.  Maybe it isn't always, but so far for you it has been, and you've just been lucky.  Maybe it isn't, and you've been unlucky, and that may explain some of your trouble.

 

To avoid this, make sure that r31:0 r31[7] is 0 before jumping to your trampoline.  If you're using ijmp to get to the trampoline, then you may run into trouble.  So, avoid ijmp.

 

 

casanovg wrote:
I'll place your macro right there, in the setup block.
I assume you mean a >>call<< to the macro.  You can put the definition of the macros anywhere before they are called.

"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."

"Read a lot.  Write a lot."

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

 

Last Edited: Mon. Oct 1, 2018 - 04:56 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Amazing, that makes a lot of sense. But then, why does it work with the WDT reset? In the end, the only difference with the jump to the "cleared" trampoline is not that the Tiny travels through many 0xFFs instead of just 2? (the entire memory, except the bootloader section). Or the WDT reset always sets r31 to 0, so the workaround actually works?

 

Anyway, I will try first with setting r31:7 to 0 before jumping to the trampoline and "SPMCSR |= (1 << CTPB)" in the setup block. If this doesn't work, then I'll try calling your macro in the setup block instead of the single instruction ...

Last Edited: Tue. Oct 2, 2018 - 05:43 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

casanovg wrote:
Amazing, that makes a lot of sense. But then, why does it work with the WDT reset? In the end, the only difference with the jump to the "cleared" trampoline is not that the Tiny travels through many 0xFFs instead of just 2? (the entire memory, except the bootloader section). Or the WDT reset always sets r31 to 0, so the workaround actually works?
A reset has no effect on SRAM or GP registers, only on I/O registers (SFR) and internal logic.

 

It would always work for a WDT reset (after an erase of all non-bootloader pages) because the reset vector is at 0x0000.  Having erased flash, that location no longer contains your rjmp to the bootloader.  Instead, all of application flash contains 0xFFFF, or sbrs r31,7.  Provided your bootloader is located at an even word address (which I would guess it is, since flash pages sizes are always an even number of words... the t85 has a 32-word page size), then you will always eventually land on the first instruction of your bootloader, regardless of the value of r31[7].

 

The risk I tried to explain above is if you rjmp straight to your trampoline at [bootloader_address - 1] while r31 >= 0x80, the first instruction of your bootloader will be skipped.  I have no earthly idea:

1) what that instruction is, since you haven't provided .lss of your bootloader

2) what the likelihood is of r31 being >= 0x80.

"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."

"Read a lot.  Write a lot."

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

 

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

I just wanted to confirm that your macro solves the first-page garbage issue. I've made several tests with "SPMCSR |= (1 << CTPB)" and "boot_temp_buff_erase()" in the setup block and, in the first case failed several times, while never failed in the second one.

 

Regarding "asm volatile("cbr r31, 0x80")", I haven't noticed any difference in my tests when it's included (I have included it just before the instruction to jump to the trampoline).

Nevertheless, I will keep it included anyway, just in case.

 

A small detail, this comma does not go there, right?

       "r" ((uint8_t)(BOOT_TEMP_BUFF_ERASE)), \
   );                                         \
}))

Thank you very much for your help!

 

Last Edited: Tue. Oct 2, 2018 - 02:24 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Glad to hear it.

 

The bad news is that there's still an errata in the datasheet (probably several datasheets).  Well, we knew that anyway I suppose.  Now we know which part of it is wrong, and which part of it is right.

 

Plus, the AVR Libc team may want to know, perhaps they'll amend boot.h to include a new macro for clearing the temporary page buffer.

 

Question:  Why jump to the trampoline to restart your bootloader?  Why not just jump to the first instruction of the bootloader directly, since that's what you're after anyway?  Then you wouldn't ever need to worry about whether r31 >= 0x80.

 

EDIT: If you continue to use the trampoline after a 'safety erase', you could just insert a nop as the first instruction in your bootloader.  That too would avoid any possible issue with r31 >= 0x80. /EDIT

A small detail, this comma does not go there, right?

Correct.  That was a copy/paste error on my part.  Apologies. I hadn't tested the macro for buildability.

 

 

"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."

"Read a lot.  Write a lot."

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

 

Last Edited: Tue. Oct 2, 2018 - 02:51 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Question:  Why jump to the trampoline to restart your bootloader?

Good point. In principle, because I assumed it was the same, only that it would take a few more clock cycles until landing on the bootloader. A second reason would be to save a couple of bytes, however, probably this wouldn't be the case since I would not have to use "cbr r31, 0x80".

 

I'll give it a try. I'm using this to jump on the trampoline (suggested by clawson):

typedef void (*fptr_t)(void);
fptr_t RunApplication = (fptr_t)((BOOTLOADER_START - 2) / 2);
// ...
RunApplication();

I will include this too ...

fptr_t RestartBootloader = (fptr_t)((BOOTLOADER_START) / 2);

 

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

casanovg wrote:
(suggested by clawson):
Where did I ever suggest a "- 2" in that ?

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

casanovg wrote:
I'll give it a try. I'm using this to jump on the trampoline (suggested by clawson):
That code will work of course, but it forces a pointer into SRAM.  A silly test:

#include <avr/io.h>

#define BOOTLOADER_START 0x1E00

typedef void (* fptr_t)(void);

fptr_t RunApplication = (fptr_t)((BOOTLOADER_START - 2) / 2);

int main(void) {

  RunApplication();
  while (1);

}
$ avr-gcc -Wall -O1 -g -Werror -mmcu=attiny85 foo.c -o foo.elf
$ avr-objdump -S foo.elf

foo.elf:     file format elf32-avr

Disassembly of section .text:

00000000 <__vectors>:
   0:	0e c0       	rjmp	.+28     	; 0x1e <__ctors_end>
   2:	20 c0       	rjmp	.+64     	; 0x44 <__bad_interrupt>
   4:	1f c0       	rjmp	.+62     	; 0x44 <__bad_interrupt>
   6:	1e c0       	rjmp	.+60     	; 0x44 <__bad_interrupt>
   8:	1d c0       	rjmp	.+58     	; 0x44 <__bad_interrupt>
   a:	1c c0       	rjmp	.+56     	; 0x44 <__bad_interrupt>
   c:	1b c0       	rjmp	.+54     	; 0x44 <__bad_interrupt>
   e:	1a c0       	rjmp	.+52     	; 0x44 <__bad_interrupt>
  10:	19 c0       	rjmp	.+50     	; 0x44 <__bad_interrupt>
  12:	18 c0       	rjmp	.+48     	; 0x44 <__bad_interrupt>
  14:	17 c0       	rjmp	.+46     	; 0x44 <__bad_interrupt>
  16:	16 c0       	rjmp	.+44     	; 0x44 <__bad_interrupt>
  18:	15 c0       	rjmp	.+42     	; 0x44 <__bad_interrupt>
  1a:	14 c0       	rjmp	.+40     	; 0x44 <__bad_interrupt>
  1c:	13 c0       	rjmp	.+38     	; 0x44 <__bad_interrupt>

0000001e <__ctors_end>:
  1e:	11 24       	eor	r1, r1
  20:	1f be       	out	0x3f, r1	; 63
  22:	cf e5       	ldi	r28, 0x5F	; 95
  24:	d2 e0       	ldi	r29, 0x02	; 2
  26:	de bf       	out	0x3e, r29	; 62
  28:	cd bf       	out	0x3d, r28	; 61

0000002a <__do_copy_data>:
  2a:	10 e0       	ldi	r17, 0x00	; 0
  2c:	a0 e6       	ldi	r26, 0x60	; 96
  2e:	b0 e0       	ldi	r27, 0x00	; 0
  30:	e6 e5       	ldi	r30, 0x56	; 86
  32:	f0 e0       	ldi	r31, 0x00	; 0
  34:	02 c0       	rjmp	.+4      	; 0x3a <__do_copy_data+0x10>
  36:	05 90       	lpm	r0, Z+
  38:	0d 92       	st	X+, r0
  3a:	a2 36       	cpi	r26, 0x62	; 98
  3c:	b1 07       	cpc	r27, r17
  3e:	d9 f7       	brne	.-10     	; 0x36 <__do_copy_data+0xc>
  40:	02 d0       	rcall	.+4      	; 0x46 <main>
  42:	07 c0       	rjmp	.+14     	; 0x52 <_exit>

00000044 <__bad_interrupt>:
  44:	dd cf       	rjmp	.-70     	; 0x0 <__vectors>

00000046 <main>:

fptr_t RunApplication = (fptr_t)((BOOTLOADER_START - 2) / 2);

int main(void) {

  RunApplication();
  46:	e0 91 60 00 	lds	r30, 0x0060	; 0x800060 <RunApplication>
  4a:	f0 91 61 00 	lds	r31, 0x0061	; 0x800061 <RunApplication+0x1>
  4e:	09 95       	icall
  50:	ff cf       	rjmp	.-2      	; 0x50 <main+0xa>

00000052 <_exit>:
  52:	f8 94       	cli

00000054 <__stop_program>:
  54:	ff cf       	rjmp	.-2      	; 0x54 <__stop_program>

Note how the pointer in SRAM must be initialised at runtime by the CRT, and how two two-word lds instructions are required to load that value into Z before the icall.

 

Making that pointer static allows the compiler to recognise that its value never changes:

static fptr_t RunApplication = (fptr_t)((BOOTLOADER_START - 2) / 2);

... yielding:


foo.elf:     file format elf32-avr

Disassembly of section .text:

00000000 <__vectors>:
   0:	0e c0       	rjmp	.+28     	; 0x1e <__ctors_end>
   2:	15 c0       	rjmp	.+42     	; 0x2e <__bad_interrupt>
   4:	14 c0       	rjmp	.+40     	; 0x2e <__bad_interrupt>
   6:	13 c0       	rjmp	.+38     	; 0x2e <__bad_interrupt>
   8:	12 c0       	rjmp	.+36     	; 0x2e <__bad_interrupt>
   a:	11 c0       	rjmp	.+34     	; 0x2e <__bad_interrupt>
   c:	10 c0       	rjmp	.+32     	; 0x2e <__bad_interrupt>
   e:	0f c0       	rjmp	.+30     	; 0x2e <__bad_interrupt>
  10:	0e c0       	rjmp	.+28     	; 0x2e <__bad_interrupt>
  12:	0d c0       	rjmp	.+26     	; 0x2e <__bad_interrupt>
  14:	0c c0       	rjmp	.+24     	; 0x2e <__bad_interrupt>
  16:	0b c0       	rjmp	.+22     	; 0x2e <__bad_interrupt>
  18:	0a c0       	rjmp	.+20     	; 0x2e <__bad_interrupt>
  1a:	09 c0       	rjmp	.+18     	; 0x2e <__bad_interrupt>
  1c:	08 c0       	rjmp	.+16     	; 0x2e <__bad_interrupt>

0000001e <__ctors_end>:
  1e:	11 24       	eor	r1, r1
  20:	1f be       	out	0x3f, r1	; 63
  22:	cf e5       	ldi	r28, 0x5F	; 95
  24:	d2 e0       	ldi	r29, 0x02	; 2
  26:	de bf       	out	0x3e, r29	; 62
  28:	cd bf       	out	0x3d, r28	; 61
  2a:	02 d0       	rcall	.+4      	; 0x30 <main>
  2c:	05 c0       	rjmp	.+10     	; 0x38 <_exit>

0000002e <__bad_interrupt>:
  2e:	e8 cf       	rjmp	.-48     	; 0x0 <__vectors>

00000030 <main>:

static fptr_t RunApplication = (fptr_t)((BOOTLOADER_START - 2) / 2);

int main(void) {

  RunApplication();
  30:	ef ef       	ldi	r30, 0xFF	; 255
  32:	fe e0       	ldi	r31, 0x0E	; 14
  34:	09 95       	icall
  36:	ff cf       	rjmp	.-2      	; 0x36 <main+0x6>

00000038 <_exit>:
  38:	f8 94       	cli

0000003a <__stop_program>:
  3a:	ff cf       	rjmp	.-2      	; 0x3a <__stop_program>

The two two-word lds instructions have been replaced with two one-word ldi instructions, and there is no longer a pointer in SRAM.  Note also how __do_copy_data is now eliminated, since there are no other initialised variables in static storage.  Additionally (although not shown in the .lss), the two bytes tacked onto the end of the image (constituting the pointer initialisation value copied by __do_copy_data) are eliminated.  Even if __do_copy_data remains due to other still-extant objects in SRAM, using static eliminates 3 words (6 bytes) from flash.

 

For completeness sake, you should also use const to indicate to the compiler that these are in fact unchanging values, but static appears to be sufficient, and the addition of const results in no different behaviour:

typedef void (* const fptr_t)(void);

static const fptr_t RunApplication = (const fptr_t)((BOOTLOADER_START - 2) / 2);

Using const without static is not enough, as that still forces the pointer into SRAM, and __do_copy_data to reappear.

 

I would have thought the compiler could go one better and replace the ldi/ldi/icall with a straight rcall (saving 2/4 more words/bytes), but I haven't been able to elicit that behaviour.

 

 

"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."

"Read a lot.  Write a lot."

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

 

Last Edited: Wed. Oct 3, 2018 - 02:40 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Where did I ever suggest a "- 2" in that ?

You didn't, I just meant (in a positive way) that you suggested a general solution to jump to a specific memory location. The -2 is from my side, to jump from the bootloader to the trampoline position.

 

Last Edited: Tue. Oct 2, 2018 - 03:02 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Making a finding like yours is far beyond my current knowledge of the platform and the toolchain. I mean, I can follow it (striving), but I could not have found this alone right now, so thank you very much for that!

 

I haven't tested it with the chip because I don't have it with me right now (I take for granted that works), but, just compiling the current configuration of the bootloader, you get an improvement of 18 bytes less in the .hex. Great!

 

1-With:
=======
fptr_t RunApplication = (fptr_t)((BOOTLOADER_START - 2) / 2);
fptr_t RestartBootloader = (fptr_t)((BOOTLOADER_START) / 2);

Size of .hex
------------
text    data     bss     dec     hex filename
   0    1466       0    1466     5ba tml-bootloader.hex

2-With:
=======
static const fptr_t RunApplication = (const fptr_t)((BOOTLOADER_START - 2) / 2);
static const fptr_t RestartBootloader = (const fptr_t)(BOOTLOADER_START / 2);

Size of .hex
------------      
text    data     bss     dec     hex filename
   0    1448       0    1448     5a8 tml-bootloader.hex

 

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

Glad to hear it.  18 bytes is better than nothing :-)

 

I'm curious about the size of the bootloader in general, however.  1.5KB is quite large.  That's three times the size of optiboot.  While I know it's a bit of apples/oranges, I expect you could likely shave at least 0.5KB off of that without too much effort.  I suppose you should get it working first, then worry about getting it small.

"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."

"Read a lot.  Write a lot."

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

 

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

Yes, it really is a bit bigger than I would like. Anyway, I consider that, in general, it's already working. The objective would be that the size of version 1 is <= 21 pages (~ 1.3k), leaving 107 pages (- 4 bytes) available for the application in a T85.

 

Surely there is lack of experience on my part to optimize the AVR code, but there have to consider that the T85 does not have a bootloader section, redirection of interrupts or dedicated I2C hardware, so everything has to be solved by software.

 

For v1.0 I will finish adding configurations in "tml-config.h" during these days to make some functions optional. One, in particular, that takes a lot of memory is the onboard trampoline calculation and handling, mainly to share its page with the application. Off-loading this function in the I2C master, you gain several bytes, but, it gives me the idea that the bootloader becomes (even) more fragile or vulnerable, I do not know what will be better, I'll leave it as an option to choose in each setup.

 

I'm also sure that there must be small gains here or there, like the improvement you proposed, which I'm not seeing today. Another possibility would be to completely change the I2C solution implemented today with USI_TWI, but that would be in the next versions. At this point, I need to move forward with the non-AVR parts of the project. Though it was fun and I learned a lot, writing this bootloader was an important deviation from the original plan.

 

The intention of making the bootloader code public has is that smarter/more experienced people can propose improvements, or implement them directly.

 

There is a small application that I took from the micronucleus project that allows updating the bootloader itself (it works well with this bootloader). This leaves me peace of mind knowing that if there is a better version tomorrow, it could eventually be updated remotely (I hope :)

 

 

Last Edited: Wed. Oct 3, 2018 - 02:58 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

OK, you've piqued my interest. Why would you need "trampolines" on a small AVR with less then 128K flash? Presumably you are using the word "trampolines" in some other sense? Do you mean a "dispatch table" to share functions in the bootloader to the app via a table of function pointers? I don't see why that would cost more than 16 bits per function and a very small piece of code to put the base address of the array into a common area that both can access ? I'd do it something like this:

typedef void (*fptr_t)(void);

void foo(void) {
    PORTB = 0x55;
}

void bar(void) {
    PORTB = 0xAA;
}

fptr_t functions[] = {
    foo,
    bar
};

int main(void) {
    // bootloady stuff then...

    TCNT1 = &functions;
    reset_to_app();
}

I chose to pass the 16 bit table pointer in TCNT1 as I assume it's not doing anything at the time and is a useful 16 bit register that's just lying around. The app code then does:

typedef void (*fptr_t)(void);

fptr_t * pFns;

int main(void) {
    pFns = TNCT1;
    pFns[0](); // calls foo()
    pFns[1](); // calls bar()
}

if you wanted to dress this up a bit more perhaps:

#define foo pFns[0]
#define bar pFns[1]

int main(void) {
    pFns = TNCT1;
    foo(); // calls foo()
    bar(); // calls bar()
}

The only thing you have to keep in step is the order of:

fptr_t functions[] = {
    foo,
    bar

and the [0], [1], etc. that calls the entries in the function pointer array. I have "foo" at [0] and "bar" at [1].

 

BTW the fns don't have to be "void (void)". It just makes the syntax cleaner if they happen to be. But you might have:

int bar(char a, char b) {
    return a + b;
}

then in the array:

fptr_t functions[] = {
    foo,
    (fptr_t)bar

So you are just saying that for building the array assume it's like "void(void)" the only thing that would matter is at the point of invocation in the app code that you might want to correct the input/output set up. Perhaps:

typedef int (*bar_t)(char, char);

bar_t my_bar = (bar_t)pFns[1];

TCNTA = my_bar(123, 207);

 

Of course this passing of function pointers all breaks down when the bootloader is so far from the app code (like in mega2560 or an xmega384) that a 16 bit function pointer won't reach - that is when the real use of "trampolines" would come into the picture - they convert short calls/jumps into long ones - hence the name "trampoline".

Last Edited: Wed. Oct 3, 2018 - 03:19 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Maybe I'm not using the word trampoline correctly. What I want to say is a "known memory address (fixed) to where the bootloader points when it wants to finish its execution and start the user's application".

 

I will illustrate how this bootloader works: there is a first state in which only the bootloader exist in the flash, the rest of the memory contains 0xFFFF. So, after turning the T85 on, it ends up in the upper part of the memory and the bootloader starts. There is no "trampoline" (as I call it) and the reset vector also has 0xFFFF (pic 1).

 

Then, if the I2C master initializes the bootloader, and it sends an application correctly, the bootloader modifies the first two bytes of the application before writing it to flash so that the reset vector points to the bootloader instead of the application. With this, it tries to make sure that the bootloader will always start after resetting the chip.

 

To start the application, because the i2c master so orders it, or a timeout, the bootloader jumps to the "trampoline" (rjmp that points to the application start). This is located just in the last two bytes of the memory page before the bootloader section. This page that contains the "trampoline", is written by the bootloader along with the application. To calculate the "trampoline" rjmp back to the app, the bootloader uses the application's original first two bytes.

 

Perhaps there is a better way to solve this in a T85, but it works and only takes 4 bytes of the application space, two for the modified reset vector and two for the "trampoline" to the application.

 

Attachment(s): 

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

Ah apologies, I missed the fact it was a tiny you are talking about. Their lack of BOOTRST means the power on jump thing is always going to be a bit of a kludge. So I see what you mean by "trampoline" now. You mean the modified RJMP at 0x0000. Sorry for the noise.

Last Edited: Wed. Oct 3, 2018 - 04:55 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

clawson wrote:

OK, you've piqued my interest. Why would you need "trampolines" on a small AVR with less then 128K flash? Presumably you are using the word "trampolines" in some other sense? Do you mean a "dispatch table" to share functions in the bootloader to the app via a table of function pointers? I don't see why that would cost more than 16 bits per function and a very small piece of code to put the base address of the array into a common area that both can access ? I'd do it something like this:

typedef void (*fptr_t)(void);

void foo(void) {
    PORTB = 0x55;
}

void bar(void) {
    PORTB = 0xAA;
}

fptr_t functions[] = {
    foo,
    bar
};

int main(void) {
    // bootloady stuff then...

    TCNT1 = &functions;
    reset_to_app();
}

I chose to pass the 16 bit table pointer in TCNT1 as I assume it's not doing anything at the time and is a useful 16 bit register that's just lying around. The app code then does:

typedef void (*fptr_t)(void);

fptr_t * pFns;

int main(void) {
    pFns = TNCT1;
    pFns[0](); // calls foo()
    pFns[1](); // calls bar()
}

if you wanted to dress this up a bit more perhaps:

#define foo pFns[0]
#define bar pFns[1]

int main(void) {
    pFns = TNCT1;
    foo(); // calls foo()
    bar(); // calls bar()
}

The only thing you have to keep in step is the order of:

fptr_t functions[] = {
    foo,
    bar

and the [0], [1], etc. that calls the entries in the function pointer array. I have "foo" at [0] and "bar" at [1].

 

BTW the fns don't have to be "void (void)". It just makes the syntax cleaner if they happen to be. But you might have:

int bar(char a, char b) {
    return a + b;
}

then in the array:

fptr_t functions[] = {
    foo,
    (fptr_t)bar

So you are just saying that for building the array assume it's like "void(void)" the only thing that would matter is at the point of invocation in the app code that you might want to correct the input/output set up. Perhaps:

typedef int (*bar_t)(char, char);

bar_t my_bar = (bar_t)pFns[1];

TCNTA = my_bar(123, 207);

 

Of course this passing of function pointers all breaks down when the bootloader is so far from the app code (like in mega2560 or an xmega384) that a 16 bit function pointer won't reach - that is when the real use of "trampolines" would come into the picture - they convert short calls/jumps into long ones - hence the name "trampoline".

 

I'm trying to creat the function table as you mentioned above. At the bootloader everything compile correctly.

But at the appication it errors out 

 

Severity	Code	Description	Project	File	Line
Error		invalid type argument of unary '*' (have 'uint16_t {aka unsigned int}')	

I know thw TCNT1 is 16bit but why the compiler is complaining about the pointer of the function ?

 

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

clawson wrote:
Why would you need "trampolines"

EVERYONE needs trampolines...

https://www.youtube.com/watch?v=...

Best-Tramp-Ever.

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:

clawson wrote:

Why would you need "trampolines"

 

EVERYONE needs trampolines...

https://www.youtube.com/watch?v=...

Best-Tramp-Ever.

 

He's freaking genius!

 

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

That was delightful laugh

"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."

"Read a lot.  Write a lot."

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

 

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

joeymorin wrote:
That was delightful

I'm not a regular watcher of AGT America's Got Talent or BGT Britain's Got Talent.  but due to the magic of YouTube, one can watch just those that strike your fancy.  I enjoy the "fun",  unexpected novelty acts for a bit of light online entertainment.  Most can never get too far in the competition, but still fun to watch. 

 

Unexpected?  Watch this...  [spoiler:  there are a number of regulars here of a certain age]

https://www.youtube.com/watch?v=...

 

Unexpected?  Talking dog...

https://www.youtube.com/watch?v=...

 

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

Like the lady says, that is something I've never seen before.

 

And I desperately want to believe that Simon really is that stupid ;-)

"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."

"Read a lot.  Write a lot."

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