Rewriting AVR/asm code for the attiny1616 - PORT_struct issues

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

So i'm trying to rewrite this piece of code for the WS211 LEDs to run on the ATTINY1616. Original code is written for the ATMEGA32xx running at 8MHz (i'll work out the timing issues later) where it worked great.

 

Original

// Modified sections of code by;
// Copyright (c) 2013 Danny Havenith
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

/**
 * Library for bit-banging data to WS2811 led controllers.
 * This file contains a definition of the send() function for 8 Mhz controllers.
 */

#ifndef WS2811_8_H_
#define WS2811_8_H_

#include <avr/io.h>
#include <util/delay_basic.h>

#define WS2811_PORT PORTD //set output port

/**
 * This function sends the RGB-data in an array of rgb structs through
 * the given io-pin.
 * The port is determined by the macro WS2811_PORT, but the actual pin to
 * be used is an argument to this function. This allows a single instance of this function
 * to control up to 8 separate channels.
 */
void WS_send(uint8_t *values, uint8_t number, uint8_t bit)
{
    
    const uint8_t mask =_BV(bit);
    uint8_t low_val = WS2811_PORT & (~mask);
    uint8_t high_val = WS2811_PORT | mask;
    uint16_t size = (uint16_t)(number * 3); // size in bytes (WS * 3 = total LEDs)

    // reset the controllers by pulling the data line low
    uint8_t bitcount = 7;
    WS2811_PORT = low_val;
    _delay_loop_1(107); // at 3 clocks per iteration, this is 320 ticks or 40us at 8Mhz

    // The labels in this piece of assembly code aren't very explanatory. The real documentation
    // of this code can be found in the spreadsheet ws2811@8Mhz.ods
    // A hint if you still want to follow the code below: The code for a regular bit (i.e. bits 7-1)
    // starts at label s00 with the current bit value already in the carry flag and it jumps halfway
    // to label cont06. The two-digit suffix of labels shows the "phase" of the signal at the time
    // of the execution, 00 being the first clock tick of the bit and 09 being the last.
    asm volatile(
    "start:  LDI %[bits], 7                          \n" // start code, load bit count
    "        LD __tmp_reg__, %a[dataptr]+            \n" // fetch first byte
    "cont06: NOP                                     \n"
    "cont07: NOP                                     \n"
    "        OUT %[portout], %[downreg]              \n" // Force line down, even if it already was down
    "cont09: LSL __tmp_reg__                         \n" // Load next bit into carry flag.
    "s00:    OUT %[portout], %[upreg]                \n" // Start of bit, bit value is in carry flag
    "        BRCS skip03                             \n" // only lower the line if the bit...
    "        OUT %[portout], %[downreg]              \n" // ...in the carry flag was zero.
    "skip03: SUBI %[bits], 1                         \n" // Decrease bit count...
    "        BRNE cont06                             \n" // ...and loop if not zero
    "        LSL __tmp_reg__                         \n" // Load the last bit into the carry flag
    "        BRCC Lx008                              \n" // Jump if last bit is zero
    "        LDI %[bits], 7                          \n" // Reset bit counter to 7
    "        OUT %[portout], %[downreg]              \n" // Force line down, even if it already was down
    "        NOP                                     \n"
    "        OUT %[portout], %[upreg]                \n" // Start of last bit of byte, which is 1
    "        SBIW %[bytes], 1                        \n" // Decrease byte count
    "        LD __tmp_reg__, %a[dataptr]+            \n" // Load next byte
    "        BRNE cont07                             \n" // Loop if byte count is not zero
    "        RJMP brk18                              \n" // Byte count is zero, jump to the end
    "Lx008:  OUT %[portout], %[downreg]              \n" // Last bit is zero
    "        LDI %[bits], 7                          \n" // Reset bit counter to 7
    "        OUT %[portout], %[upreg]                \n" // Start of last bit of byte, which is 0
    "        NOP                                     \n"
    "        OUT %[portout], %[downreg]              \n" // We know we're transmitting a 0
    "        SBIW %[bytes], 1                        \n" // Decrease byte count
    "        LD __tmp_reg__, %a[dataptr]+            \n"
    "        BRNE cont09                             \n" // Loop if byte count is not zero
    "brk18:  OUT %[portout], %[downreg]              \n"
    "                                                \n" // used to be a NOP here, but returning from the function takes long enough
    "                                                \n" // We're done.
    : /* no output */
    : /* inputs */
    [dataptr] "e" (values), 	// pointer to grb values
    [upreg]   "r" (high_val),	// register that contains the "up" value for the output port (constant)
    [downreg] "r" (low_val),	// register that contains the "down" value for the output port (constant)
    [bytes]   "w" (size),		// number of bytes to send
    [bits]    "d" (bitcount),       // number of bits/2
    [portout] "I" (_SFR_IO_ADDR(WS2811_PORT)) // The port to use
    );
}

#endif /* WS2811_8_H_ */

 

Where i'm stuck is, the low and high register (pin high and low) on the attiny1616 now gives PORT_struct conversion errors.

uint8_t low_val = WS2811_PORT & (~mask);

could be replaced by

PORTC.OUTCLR |= mask;

But this does not give what I assume needs to be a register value to pass in the assembly section (I have no experiance with assembly, yet).

This topic has a solution.
Last Edited: Tue. Dec 10, 2019 - 12:58 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0


dbrown2k wrote:

PORTC.OUTCLR |= mask;

That's a read-modify-write (RMW) - Don't you need:

PORTC.OUTCLR = mask;

ie, you just set the bits you want cleared ?

 

EDIT:

 

http://ww1.microchip.com/downloads/en/DeviceDoc/ATtiny3216_ATtiny1616-data-sheet-40001997B.pdf

 

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...
Last Edited: Tue. Dec 10, 2019 - 12:18 AM
This reply has been marked as the solution. 
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

So, when you say ATMEGA32xx I presume you mean a classic mega328P for example, and not a mega3209...

 

Well, the laziest way is to use the VPORTx registers, which are compatible with classic AVR. For example:

#define WS2811_PORT VPORTC.OUT

 

...and the errors should go away without modifying the legacy code.

 

Edit: otherwise, you will have to mess with the assembly code, because registers like PORTC.x are not in rage of the OUT assembly instruction.

In fact, I'm also porting code to the new chips, and in my case bitbang assembly code is also involved. This was the easiest solution I found.

 

Edit #2: For completeness, this is the "replacement table":

PINx    -> VPORTx.IN
PORTx   -> VPORTx.OUT
DDRx    -> VPORTx.DIR

 

Last Edited: Tue. Dec 10, 2019 - 12:54 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Yes the ATMEGA328P / PB etc.

 

Thank you that solved the errors, i'll make a note and have a dig into this further.

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

OK so using the VPORT is not giving me fast enough response time (600ns shortest and i need to get down to 250ns), what terms should i be using to search for this flavour of assembler on the newer ATTINYs?

 

I do not need to make the code flexible, I can re-write it so that the registers are hard coded.

 

Is there a way I can specify the direct port, so;

"        OUT %[portout], %[downreg]              \n"

would have the register value from iotn1616.h;

#define PORTA                (*(PORT_t *) 0x0400) /* I/O Ports */

I can see this is not feasible as the register is 400 and the limit on OUT is 31. Is there an alternative formulation that would be in range?

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

By factory default the clock is 3.33 MHz on the ATtiny1616.  Have you done anything to increase it?  If not, that may explain your timing discrepency.

 

Letting the smoke out since 1978

 

 

 

 

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

no, and not something i was aware of, I saw the 16/20MHz in the fuses. And that the supply voltage linear changed the frequency. How dows the full speed need setting on this chip?

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

Letting the smoke out since 1978

 

 

 

 

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


To save some possible headaches, since the register that controls the CPU clock speed is protected, I'll just leave a link to a simple program I wrote for the Tutorials section.

It shows how to setup the CPU clock divider.

https://www.avrfreaks.net/forum/...

 

Note: since your original code was written for 8MHz, and there is no x5 divider to get that from 20MHz, you will also need to change the OSCCFG fuse for operation at 16MHz, then use the x2 divider.

This will save you the trouble of fine tuning the code for a different clock speed.

 

Last Edited: Thu. Dec 12, 2019 - 01:54 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Thank you for the pointers, with the clock set to 16MHz and the following code to set the divider to 2

CCP = CCP_IOREG_gc; //set the write enable code
CLKCTRL.MCLKCTRLB = 1; //edit the divider

The timings are as expected.

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

dbrown2k wrote:

CCP = CCP_IOREG_gc; //set the write enable code
CLKCTRL.MCLKCTRLB = 1; //edit the divider

Don't do this. You can't guarantee that the C compiler will always generate code to meet the CCP timing requirement. This is why xmega.h contains _PROTECTED_WRITE(). You can even see this being used in the code that El Tangas linked to:

void clock_init(void) {
    _PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, CLKCTRL_PDIV_10X_gc | CLKCTRL_PEN_bm);
}

That is the way to make a write to MCLKCTRLB and know that whatever happens this code will always compile to meet the 4 cycle timing requirement.

 

For your info what xmega.h actually contains is:

#define _PROTECTED_WRITE(reg, value)				\
  __asm__ __volatile__("out %[ccp], %[ccp_ioreg]" "\n\t"	\
		       "sts %[ioreg], %[val]"			\
		       :					\
		       : [ccp] "I" (_SFR_IO_ADDR(CCP)),		\
			 [ccp_ioreg] "d" ((uint8_t)CCP_IOREG_gc),	\
			 [ioreg] "n" (_SFR_MEM_ADDR(reg)),	\
			 [val] "r" ((uint8_t)value))

The use of consecutive OUT then STS in this asm sequence guarantee the timing restriction.

Last Edited: Thu. Dec 12, 2019 - 10:27 AM