并行更新RGBW LED灯条;快速内存shuffle(使用内联汇编程序?)

时间:2017-12-08 23:32:50

标签: assembly arduino inline-assembly

我正在尝试使用Arduino Nano并行更新四个RGBW LED灯条。 这些条连接到数字引脚0-3,它等于I / O寄存器PORTD的0-3位。 (Image: LEDs wired to Arduino)

条带类型是SK6812 RGBW,但我不认为这是一个非常重要的信息。 (Datasheet)

重要的是,为了更新一个LED,您需要快速连续提供32位数据,如数据表中所述。 我已经设法通过准备一个32位的数组(名为LED [32])来保存每个条带的一个LED的信息。然后将这些值加载到I / O寄存器PORTD中以将引脚驱动为高电平和低电平。 LED [32]阵列如下所示:

(从LSB到MSB顺序: w(白色) b(蓝色) r(红色) g(绿色)

引脚4-7在开头保存,并将在每个帧(X)中加载以保持它们原样)

<table border="1" <tr>
  <td>bit7          </td>
  <td>bit6          </td>
  <td>bit5          </td>
  <td>bit4          </td>
  <td>bit3          </td>
  <td>bit2          </td>
  <td>bit1          </td>
  <td>bit0          </td>
  </tr>
  <tr>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>W3_0</td>
    <td>W2_0</td>
    <td>W1_0</td>
    <td>W0_0</td>
  </tr>
  <tr>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>W3_1</td>
    <td>W2_1</td>
    <td>W1_1</td>
    <td>W0_1</td>
  </tr>
  <tr>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>W3_2</td>
    <td>W2_2</td>
    <td>W1_2</td>
    <td>W0_2</td>
  </tr>
  <tr>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>W3_3</td>
    <td>W2_3</td>
    <td>W1_3</td>
    <td>W0_3</td>
  </tr>
  <tr>
    <td>...</td>
    <td>...</td>
    <td>...</td>
    <td>...</td>
    <td>...</td>
    <td>...</td>
    <td>...</td>
    <td>...</td>
  </tr>
  <tr>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>r3_4</td>
    <td>r2_4</td>
    <td>r1_4</td>
    <td>r0_4</td>
  </tr>
  <tr>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>r3_5</td>
    <td>r2_5</td>
    <td>r1_5</td>
    <td>r0_5</td>
  </tr>
  <tr>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>r3_6</td>
    <td>r2_6</td>
    <td>r1_6</td>
    <td>r0_6</td>
  </tr>
  <tr>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>X</td>
    <td>r3_7</td>
    <td>r2_7</td>
    <td>r1_7</td>
    <td>r0_7</td>
  </tr>
</table>

需要在LED写入过程之前计算此信息。 两次写入过程之间的时间必须小于80uS! 对于16 MHz Arduino,即1280个周期。

此时我的计算速度不够快。

LED的信息存储在名为LED "Number of LEDs" 4的阵列中 每个LED阵列的一个维度,第四个用于四种颜色,最后一个用于四个不同的条带。

我要写入所有LED的代码:

static inline __attribute__ ((always_inline)) void showPixel() {
  // Send the 32 Bits down every row. Remember that each pixel is 32 bits wide (8 bits each for R,G, B & W)
  uint8_t bit;
  uint8_t onPixel,offPixel; //output of PORTD when high or low is being written
  cli(); //no interrupts
  offPixel = PIXEL_PORT;  //safe output of Port D
  offPixel &= 0xf0; //create Bitmask for setting bit 4-7 of Port D to original value and leds off 0bxxxx0000
  onPixel = offPixel | 0x0f;  //led pins high plus IO pins as they were 0bxxxx1111
  
  for(uint8_t ledNr=0; ledNr < NUM_LEDS; ledNr++)  {
    
    shuffle(0,LEDs[ledNr][3][0],LEDs[ledNr][3][1],LEDs[ledNr][3][2],LEDs[ledNr][3][3],offPixel);//white
    shuffle(8,LEDs[ledNr][2][0],LEDs[ledNr][2][1],LEDs[ledNr][2][2],LEDs[ledNr][2][3],offPixel);//blue
    shuffle(16,LEDs[ledNr][0][0],LEDs[ledNr][0][1],LEDs[ledNr][0][2],LEDs[ledNr][0][3],offPixel);//red
    shuffle(24,LEDs[ledNr][1][0],LEDs[ledNr][1][1],LEDs[ledNr][1][2],LEDs[ledNr][1][3],offPixel);//green
    
  
    bit=32; 
    while (bit--) { //send out the 32 bytes
      sendBitX4_lower( LED[bit] ,onPixel,offPixel); 
    }
  }
  sei(); //activate interrupts
}

我的随机播放功能:

static inline  __attribute__ ((always_inline)) void shuffle(uint8_t bit, uint8_t v0, uint8_t v1,uint8_t v2, uint8_t v3,uint8_t IOpins){
  uint8_t  res,pos,mask=8;
  pos=bit+8;
    //LED[bit]=0; EDIT: this was a test
    //LED[bit++]=0; to see if decreasing the resolution
    //LED[bit++]=0; speeds it up enough to work
    //bit++;        at 5 bit resolution it was barely fast enough
  while(bit<pos){
    if(v3 & mask) res=8;
    else res=0;
    if(v2 & mask) res|=4;
    if(v1 & mask) res|=2;
    if(v0 & mask) res|=1;
    mask<<=1;
    res|=IOpins;      //Set bits 0-3 to the output that was present
    LED[bit]=res;
    bit++;
  }

在双重函数中需要做什么可能有点难。我试图绘制它,所以也许你可以更容易理解(attachment shuffle.pdf)。 基本上,每种颜色的计算分为4个部分。每个shuffle将写入LED [32]数组的8个字节。这个过程看起来有点像被倒置的矩阵。 LED [32]的每个字节具有4个不同字节的LED阵列的元件。从LED [0]的LSB开始,然后向上移动到LED [8]的MSB,依此类推。

我尝试了不同的例子。一些有位移位,有些指针通过数组,但这是最快的。

我的问题是:在这么多次循环中进行这种计算是否物理可行?如果是,怎么样? 可能是内联汇编程序,但我刚刚进入... 谢谢你的帮助。如果您有兴趣,我们可以对此进行优化,并让所有人都可以访问:)

更新: 我不认为可以绕过洗牌,因为每个LED输出32位信息的时间是至关重要的。在我的代码中,sendBitX4_lower()函数被调用32次来完成。 发送一位信息的时间是1.25μs±600ns,比如1.9μs,即最多30个周期。

如果您有兴趣,这就是代码:

static inline __attribute__ ((always_inline)) void sendBitX4_lower( uint8_t bits ,uint8_t onBits,uint8_t offBits ) {
    asm volatile (
      "out %[port], %[onBits] \n\t"           // 1st step - send T0H high 

      ".rept %[T0HCycles] \n\t"               // Execute NOPs to delay exactly the specified number of cycles
        "nop \n\t"
      ".endr \n\t"

      "out %[port], %[bits] \n\t"             // set the output bits to thier values for T0H-T1H
      ".rept %[dataCycles] \n\t"               // Execute NOPs to delay exactly the specified number of cycles
      "nop \n\t"
      ".endr \n\t"

      "out %[port],%[offBits]  \n\t"        // last step - T1L all bits low

      // Don't need an explicit delay here since the overhead that follows will always be long enough
      ::
      [port]    "I" (_SFR_IO_ADDR(PIXEL_PORT)),
      [bits]   "d" (bits),
      [onBits]   "d" (onBits),
      [offBits]  "d" (offBits),
      [T0HCycles]  "I" (NS_TO_CYCLES(T0H) - 2),           // 1-bit width less overhead  for the actual bit setting, note that this delay could be longer and everything would still work
      [dataCycles]   "I" (NS_TO_CYCLES((T1H-T0H)) - 2)// Minimum interbit delay. Note that we probably don't need this at all since the loop overhead will be enough, but here for correctness
    );
    // Note that the inter-bit gap can be as long as you want as long as it doesn't exceed the reset timeout (which is A long time)
}  

我想每帧的一些时间可以用来做一部分计算但是怀疑所有这些。那将是960个周期。它可能有效,因为现在不需要内存保存部分,但另一方面需要写入端口。 因此,对于一个帧的所有计算都需要在此序列中找到时间:Timing Overview这将涉及来自RAM的加载以及可能导致条件跳转的四个“if”(sbrc 1-2个循环)。 我已经看过Adafruit的图书馆(https://github.com/adafruit/Adafruit_NeoPixel)以获得灵感。

1 个答案:

答案 0 :(得分:1)

如何(盲目尝试,因为我没有arduino IDE,我也没有尝试使用任何C编译器进行编译,因此您可能需要修复语法):

static void showPixel() {
    const uint8_t colorOffsets[4] = { 1, 0, 2, 3 };     // move somewhere into constants?
    ... set up "offPixel" here
    cli(); //no interrupts
    for(uint8_t ledNr=0; ledNr < NUM_LEDS; ++ledNr)  {
        for (uint8_t colorIdx = 0; colorIdx < 4; ++colorIdx) {
            const uint8_t* v_ptr = LEDs[ledNr][colorOffsets[colorIdx]];
            uint8_t bitMask = 0x80;
            do {
                uint8_t toSend = offPixel;  // upper 4 bits preserved PORTD, lower 4 bits cleared
                // set lower 4 bits by the colour values
                if (v_ptr[0] & bitMask) toSend |= 1;
                if (v_ptr[1] & bitMask) toSend |= 2;
                if (v_ptr[2] & bitMask) toSend |= 4;
                if (v_ptr[3] & bitMask) toSend |= 8;

                //TODO send "toSend" to PORTD
                ???
                PIXEL_PORT = toSend;  // guessing it

                bitMask >>= 1;              // next bit of values
            } while (bitMask); // all 8 bits of color value
        }
    }
    sei(); //activate interrupts
}

我不打算那个,为什么?它通过所有LED发送数据,听起来像是一个足够大的单元,只需在代码存储器中使用一次。

它应该从每个条带的顶部位扫描绿色,红色,蓝色,白色,构建要发送到PORTD的字节(LED数据中的引脚0-3,来自offPixel的4-7个松树)。然后你应该发送它。内存中没有任何混乱,读取它们应该发出的模式中的正确位。

它也会发送完整的8位颜色,如果你想强制将某些位设置为零,你可以将while (bitMask)更改为某个特定的位测试,然后再发出offPixel个值总共发射8位的时间。

编辑:

我在godbolt set to AVR gcc中试了一下这个来源,我有这些观察......

首先使用内部源(上面的godbolt链接上的完整源代码。我不确定Arduino如何定义PORTD,所以我把它放在一些易失性内存绝对地址,应该足够接近):

for(uint8_t ledNr=0; ledNr < NUM_LEDS; ++ledNr)  {
    for (uint8_t colorIdx = 0; colorIdx < 4; ++colorIdx) {
        const uint8_t* v_ptr = LEDs[ledNr][colorOffsets[colorIdx]];
        const uint8_t v0 = v_ptr[0];
        const uint8_t v1 = v_ptr[1];
        const uint8_t v2 = v_ptr[2];
        const uint8_t v3 = v_ptr[3];
        uint8_t bitMask = 0x80;
        do {
            PIXEL_PORT = offPixel;      // set the LED bits to low
            asm volatile("": : :"memory");
            // uint8_t toSend = offPixel;  // upper 4 bits preserved PORTD, lower 4 bits cleared
            // // set lower 4 bits by the colour values
            // if (v0 & bitMask) toSend |= 1;
            uint8_t toSend = offPixel | ((v0 & bitMask) ? 1 : 0);
            if (v1 & bitMask) toSend |= 2;
            if (v2 & bitMask) toSend |= 4;
            if (v3 & bitMask) toSend |= 8;

            PIXEL_PORT = toSend;        // set the LED bits to high
            asm volatile("": : :"memory");

            bitMask >>= 1;              // next bit of values
        } while (bitMask); // all 8 bits of color value
    } //for (uint8_t colorIdx = 0; colorIdx < 4; ++colorIdx) {
} //for(uint8_t ledNr=0; ledNr < NUM_LEDS; ++ledNr)

生成的程序集看起来像是可以使用的基础,它将循环展开8次(对于bitMask)并将bitMask测试转换为特定的sbrc + ori对,这是有意义的对我来说。问题是,端口上的off + on位设置得太短(它会将位切换为ON,然后通过在下一条指令中将它们关闭来立即启动下一位,并且没有太多工作要做向下,除了添加nop延迟循环)。

主要问题是,为了在所有32个LED上获得固定时序,您需要在展开的循环之前准备初始状态,并在第7/8位测试结束时继续准备下一个状态。延迟,所以下一个LED将在固定时间内启动,就像下一个LED一样。

C输出直接看起来不可用,但对于你自己的展开循环来说可能是合理的模板(如果你在汇编时已经足够好了,我不想写满{{1}例程,因为我从来没有进行AVR汇编,而且我不知道如何读取/写入端口,而且编写这种大小的展开循环非常繁琐。

核心代码(由我评论)(在此部分中测试了第4位,即showPixels()):

bitMask == 0x10

我会用与剩余比特测试相同的方式手工编写初始部分,即(使人类阅读更容易让所有比特由相同的时尚代码处理):

    out 52-0x20,r20    // PORTD = offPixel
    ldi r23,lo8(1)
    sbrs r24,4         // 4th bit (the number goes from 7 to 0)
    ldi r23,lo8(0)     // toSend = 0/1 (? (v0 & bitMask))
    or r23,r20         // toSend |= offPixel
    sbrc r25,4
    ori r23,lo8(2)     // if (v1 & bitMask) toSend |= 2
    sbrc r21,4
    ori r23,lo8(4)     // if (v2 & bitMask) toSend |= 4
    sbrc r22,4
    ori r23,lo8(8)     // if (v3 & bitMask) toSend |= 8
    out 52-0x20,r23    // PORTD = toSend

(我尝试修改C以提示,但是gcc改为插入两个分支 out 52-0x20,r20 //1c // PORTD = offPixel mov r23,r20 //1c // toSend = offPixel sbrc r24,4 //1/2c // 4th bit (the number goes from 7 to 0) ori r23,lo8(1) //1c // if (v0 & bitMask) toSend |= 1 sbrc r25,4 //1/2c ori r23,lo8(2) //1c // if (v1 & bitMask) toSend |= 2 sbrc r21,4 //1/2c ori r23,lo8(4) //1c // if (v2 & bitMask) toSend |= 4 sbrc r22,4 //1/2c ori r23,lo8(8) //1c // if (v3 & bitMask) toSend |= 8 out 52-0x20,r23 //1c // PORTD = toSend 来直接加载rjmpoffPixel值的寄存器,烦人......)

对于跳过/设置条件,offPixel+1对将固定2个时钟,因此在将sbrc + ori写入端口后,将需要10个时钟周期才能写入ON状态。如果我正确地阅读您的时间概述,那看起来稍微偏离了可接受的范围。因此,您可以稍后将某个OFF移出,例如在第二个offPixel之前,这将使其在OFF和ON sbrc之间有7个时钟。 (实际上手持gb on godbolt作品:https://godbolt.org/g/V2vf2X

然后你有4 + 12用于下一个循环...新的开始部分(直到第二个out已经吃4c,并且你有12c通过内务或人工延迟填补。

在内务/延迟部分结束(位2,1,0 ...),您需要将新的LED值提取到sbrc寄存器中,为简单起见,我可能会使用两个寄存器范围,如{ {1}}用于第一次传递,v0/v1/v2/v3用于第二次传递,并执行16次循环(可能需要仔细的寄存器使用设计以适应可用的备用寄存器)。

考虑到这一点,条纹的r18+值的加载也可以向前移动指针,即r26+,大约2个周期(我不确定哪个时钟申请,在XMEGA上不访问SRAM你可以是1个周期,但我认为你正在用vX?)访问SRAM,即4x2 = 8个周期。这可以完全适合12c延迟和备用周期来调整ldd rX,Z+(从绿色[1]到红色[0]以及从红色[0]到蓝色[2],即第一种情况下的LEDs并且Z秒,然后sub v_ptr,8应该在蓝色[2]完成后指向白色[3]。如果add v_ptr,4排列良好,你可以{{1只是Z)的低部分。

所以总代码架构就像:

  • 将指针LEDs准备到sub/addZ reg)
  • CLI()
  • 阅读ledptr(整个代码的固定注册表)
  • {//循环32次此
  • 2c LEDs[0][1] =指向nextColorPtr表Z
  • 的指针
  • {//循环4次
    • 8c使用offPixel
    • 读取v0,v1,v2,v3
    • 1c toSend = offPixel
    • 2c if(v0&amp; 0x80)toSend | = 1
    • 1c PORTD = offPixel // OFF状态发送到此处(14c之后)
    • 6c 3x if(v1 / 2/3&amp; 0x80)toSend | = 2/4/8
    • 1c PORTD = toSend // ON状态发送到此处(7c之后)
    • ~4-8c Y(在{ -8, +4, +0, +0 }的表格中添加-8 / + 4/0/0到Z+(太懒了,不能亲自去看看究竟是什么时钟将是)(也取决于你是否Z += [Y+]对齐,所以它足以调整低Z部分,或者你需要对整个{Y进行适当的调整{1}})。
    • ~8-4c人工LEDs延迟将剩余时间填入12c总计
    • //第二位从这里开始
    • 1c toSend = offPixel
    • 2c if(v0&amp; 0x40)toSend | = 1
    • 1c PORTD = offPixel // OFF状态发送到此处(自ON状态后16c之后)
    • 6c 3x if(v1 / 2/3&amp; 0x40)toSend | = 2/4/8
    • 1c PORTD = toSend // ON状态发送到此处(7c之后)
    • 12c Z延迟
    • //第三位从这里开始
    • 1c toSend = offPixel
    • 2c if(v0&amp; 0x20)toSend | = 1
    • 1c PORTD = offPixel // OFF状态发送到此处(自ON状态后16c之后)
    • 6c 3x if(v1 / 2/3&amp; 0x20)toSend | = 2/4/8
    • 1c PORTD = toSend // ON状态发送到此处(7c之后)
    • 12c add延迟
    • ...
    • 测试Y是否超出Z并且如果这是来自主要32个循环的最后一个并且分支到最后一个位代码的3个变体,则一个变体循环到&#34; //循环4次&#34;继续(必须延迟加分支到4c循环,总共16c直到OFF状态),第二个变量循环回&#34; //循环32次&#34;继续下一个LED,必须在2c完成,总共16c直到OFF状态(即只是分支== 2c),第三个变量是继续退出程序(处理32个LED),由{{1启动每个变量延迟填写最多12c
    • //有效Y的最后一位变体示例
    • //最后一位从这里开始
    • 1c toSend = offPixel
    • 2c if(v0&amp; 0x01)toSend | = 1
    • 1c PORTD = offPixel // OFF状态发送到此处(自ON状态后16c之后)
    • 6c 3x if(v1 / 2/3&amp; 0x01)toSend | = 2/4/8
    • 1c PORTD = toSend // ON状态发送到此处(7c之后)
    • 2c nop nop
    • 2c branch to // loop 4次

...

  • //无效Y的最后一位变量的示例,但是有效的Z指针(或计数器&lt; 32)
  • //最后一位从这里开始
  • 1c toSend = offPixel
  • 2c if(v0&amp; 0x01)toSend | = 1
  • 1c PORTD = offPixel // OFF状态发送到此处(自ON状态后16c之后)
  • 6c 3x if(v1 / 2/3&amp; 0x01)toSend | = 2/4/8
  • 1c PORTD = toSend // ON状态发送到此处(7c之后)
  • 2c branch to //循环32次

我不会尝试为gcc编写内联ASM,因为我在指定被破坏的寄存器/等时完全丢失了..即使其成为有效的gcc内联。而且这让我花费的时间超出了我的预期。 (如果我自己这样做,我会把它写成独立的装配)

但是,如果我理解你的时间绘制正确,它看起来很可行。一般般,但可以4条。如果你需要超过4个条带,那么在这里仍有一些延迟可能会在最大值时将其撞到8个条带,但这需要在几个条件下更积极地展开/交错循环的开始/结束比特(对于8个条带,它可能需要在整个8位代码上交错管理,即根本没有复制/粘贴,每个延迟由下一个LED准备代码组成,并且需要2组工作注册)

如果我没有忽略任何东西,这应该产生固定的7个时钟OFF状态,16个ON状态时钟(两个位之间总共23个时钟),用于整个32个LED x 4种颜色(128个字节发送到端口)

目前建议的源代码已经超过100行代码,编写,调试和维护非常繁琐,但是因为你需要固定时序,看起来是最合理的方法。