【问题标题】:Parallel updating RGBW LED strip; fast memory shuffle (with inline assembler?)并行更新RGBW LED灯条;快速内存洗牌(使用内联汇编器?)
【发布时间】:2017-12-10 23:14:07
【问题描述】:

我正在尝试使用 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 的信息存储在名为 LEDs"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++;
  }

理解 suffle 函数需要做什么可能有点困难。我试着画了它,所以也许你可以更容易理解(attachment shuffle.pdf)。 本质上,每种颜色的计算分为 4 个部分。每次 shuffle 将写入 LED[32] 阵列的 8 个字节。这个过程看起来有点像一个正在反转的矩阵。 LED[32] 的每个字节都有 LED 阵列的 4 个不同字节的元素。从 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 加载以及可能导致条件跳转(sbrc 1-2 个周期)的四个“if”。 我查看了 Adafruit (https://github.com/adafruit/Adafruit_NeoPixel) 的图书馆以获取灵感。

【问题讨论】:

  • 为什么要先发送 Wx_0(根据那个 HTML 表),而数据表显示 “发送数据的顺序(R7 - R6 - ... W0)”?我是不是误会了什么? .... 和 “从 LSB 到 MSB 顺序:w(白色)b(蓝色)r(红色)g(绿色)” - 真的很奇特的 WBRG,或者它只是拼写错误,你有更多常见的WBGR还是WRGB?
  • sendBitX4_lower 是什么?为什么要在内存中对其进行混洗,然后再输出混洗后的数据,而无需将混洗后的值写入内存即可简单地立即输出。 IMO 写的东西要花很多钱。无论如何,1280 个周期听起来像是可用的年龄(请注意,在 ZX Spectrum 上,1 个显示帧有 70000 个周期(是的,那只有 70k),每条指令确实花费了 4 到 23 个周期,甚至没有 1 个),你有优化吗编译那个 C 时打开? .. 编辑:哦,这是LED vs LEDs .. :/
  • 最后,在shuffle:为什么你将前两位设置为零LED[bit]=0;(通过LED[bit++]=0; 覆盖第一位两次),然后通过bit++; 完全跳过第三位没有设置它,然后你从颜色值中设置剩余的 5 位......很奇怪。为什么不把每种颜色都填满 8 位? - 你的 Arduino 有什么 CPU?
  • @Ped7g 对不起,这是我的错。这不完全是我想要发布的代码。你现在看到的是通过降低分辨率来“加速”。这是一个实验,看看条带在什么时候起作用。如果只有 5 位被“洗牌”,它就可以工作(足够快),所以我得到 2^5=32 的分辨率。所以原始代码删除了这 4 行。
  • 阵列 LED 无故从 32 发出下降。所以白色实际上是在最后发送的。我还注意到数据表和我的芯片的区别。用我的芯片,红色和绿色的顺序似乎互换了。

标签: assembly arduino inline-assembly


【解决方案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 的字节(引脚 0-3 来自 LED 数据,引脚 4-7 来自offPixel)。然后你应该发送它。内存中没有洗牌,读取应该发出的模式中的正确位。

它还发送完整的 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 位太短(它将位打开,然后通过在下一条指令中将它们关闭来立即开始下一位,除了添加nop 延迟循环)。

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

直接的 C 输出看起来不可用,但可能是您自己展开循环的合理模板(如果您在汇编方面足够好,我不想写完整的 showPixels() 例程,因为我从来没有做了AVR组装,另外我不知道应该如何读取/写入端口,而且编写这种大小的展开循环非常乏味。

核心代码(由我评论)(在这部分测试了第 4 位,即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

我会以与其余位测试相同的方式手写初始部分,即(使人类更容易阅读所有位由相同的时尚代码处理):

    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

(我试图修改 C 来建议,但 gcc 改为插入两个分支 rjmp 以直接使用 offPixeloffPixel+1 值加载寄存器,烦人......)

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

然后你有 4+12 用于下一个循环......新的开始部分(直到第二个 sbrc 已经在吃 4c,你有 12c 需要通过内务或人为延迟来填充。

在管理/延迟部分的最后(位 2、1、0 ...),您需要将新的 LED 值获取到 v0/v1/v2/v3 寄存器中,为简单起见,我可能会使用两个寄存器范围,例如 r18+第一遍,r26+ 第二遍,循环 16 次(可能需要仔细的寄存器使用设计以适应可用的备用寄存器)。

想一想,vX 条纹值的负载也可以向前移动指针,即ldd rX,Z+,这大约是 2 个周期(我不确定要应用哪些时钟,在 XMEGA 上不访问 SRAM 你可以是 1 个周期,但我认为您正在使用 LEDs?) 访问 SRAM,即 4x2 = 8 个周期。这可以完全适应 12c 延迟和空闲周期来调整 Z(从绿色 [1] 到红色 [0] 和从红色 [0] 到蓝色 [2],即 sub v_ptr,8 在第一种情况下和 add v_ptr,4第二,然后Z应该在蓝色[2]完成后已经指向白色[3]。另外,如果你有LEDs对齐良好,你可以sub/add只是Z的低部分)。

所以总的代码架构是这样的:

  • 准备指针 ledptrLEDs[0][1] (Z reg)
  • cli()
  • 阅读offPixel(整个代码的固定注册)
  • { // 循环 32 次​​li>
  • 2c Y = 指向 nextColorPtr 表 { -8, +4, +0, +0 } 的指针
  • { // 循环 4 次
    • 8c 使用Z+ 读取 v0、v1、v2、v3
    • 1c toSend = offPixel
    • 2c if (v0 & 0x80) toSend |= 1
    • 1c PORTD = offPixel // OFF 状态发送到这里(14c 之后)
    • 6c 3x if (v1/2/3 & 0x80) toSend |= 2/4/8
    • 1c PORTD = toSend // ON 状态在此处发送(7c 之后)
    • ~4-8c Z += [Y+] (从Y 的表中添加 -8/+4/0/0 到 Z)(我懒得自己尝试看看会是什么确切的时钟)(还取决于您是否将LEDs对齐,因此调整较低的Z部分就足够了,或者您需要将16b add调整为整个Z
    • ~8-4c 人工nop 延迟将剩余时间填充到总共 12c
    • //第二位从这里开始
    • 1c toSend = offPixel
    • 2c if (v0 & 0x40) toSend |= 1
    • 1c PORTD = offPixel // OFF 状态发送到这里(在 16c 之后因为 ON 状态)
    • 6c 3x if (v1/2/3 & 0x40) toSend |= 2/4/8
    • 1c PORTD = toSend // ON 状态在此处发送(7c 之后)
    • 12c nop 延迟
    • //第三位从这里开始
    • 1c toSend = offPixel
    • 2c if (v0 & 0x20) toSend |= 1
    • 1c PORTD = offPixel // OFF 状态发送到这里(在 16c 之后因为 ON 状态)
    • 6c 3x if (v1/2/3 & 0x20) toSend |= 2/4/8
    • 1c PORTD = toSend // ON 状态在此处发送(7c 之后)
    • 12c nop 延迟
    • ...
    • 测试 Y 点是否超出nextColorPtr 并且如果这是主 32 循环中的最后一个并且分支到最后一位代码的 3 个变体,一个变体是循环到“// 循环 4 次”继续(必须延迟加上分支到循环在 4c 中总共有 16c 直到关闭状态),第二个变体循环回到“//循环 32 次”以继续下一个 LED,必须在 2c 中完成才能总共有 16c 直到关闭状态(即只是分支 == 2c) ,第三个变体是继续退出程序(处理了 32 个 LED),以 nop 延迟开始每个变体,总共填充到 12c
    • // 有效 Y 的最后一位变体示例
    • //最后一位从这里开始
    • 1c toSend = offPixel
    • 2c if (v0 & 0x01) toSend |= 1
    • 1c PORTD = offPixel // OFF 状态发送到这里(在 16c 之后因为 ON 状态)
    • 6c 3x if (v1/2/3 & 0x01) toSend |= 2/4/8
    • 1c PORTD = toSend // ON 状态在此处发送(7c 之后)
    • 2c nop nop
    • 2c 分支到 // 循环 4 次

...

  • // 无效 Y 但有效 Z 指针(或计数器
  • //最后一位从这里开始
  • 1c toSend = offPixel
  • 2c if (v0 & 0x01) toSend |= 1
  • 1c PORTD = offPixel // OFF 状态发送到这里(在 16c 之后因为 ON 状态)
  • 6c 3x if (v1/2/3 & 0x01) toSend |= 2/4/8
  • 1c PORTD = toSend // ON 状态在此处发送(7c 之后)
  • 2c 分支到 // 循环 32 次​​li>

我不会尝试为 gcc 编写内联 ASM,因为我完全迷失在指定被破坏的寄存器/等方面。即使其成为有效的 gcc 内联。另外,这比我预期的花费了我更多的时间。 (如果我自己这样做,我宁愿在独立程序集中编写)

但如果我正确理解了您的时序图,它看起来是可行的。马马虎虎,但可用于 4 条。如果您需要超过 4 个条带,那么这里和那里仍然会有一些延迟,以将其最多增加到 8 个条带,但这需要在几个位上更积极地展开/交错循环的开始/结束(对于 8剥离它可能需要在整个 8 位代码上交错处理,即根本不需要复制/粘贴,每个延迟都由一小段 next-LED 准备代码形成,并且需要 2 组工作 regs)。

如果我没有忽略任何事情,这应该为整个 32 个 LED x 4 种颜色(128 个字节发送到端口)产生固定的 7 个关闭状态时钟和 16 个开启状态时钟(两个位之间总共 23 个时钟)。

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

【讨论】:

  • 我已经通过微小的调整快速实现了您的代码。这些位首先需要设置为低,然后在特定时间后设置为它们的值而不是零。如果这是可视化的,我已经在我的问题中添加了时序概述。无需进一步细化,所有 LED 都会至少显示一些响应!!这表明立即计算它而不是存储它可能是要走的路!我会试着调整一下时间,看看它有多好。谢谢
  • @MilesDelwig 时间看起来真的很紧。您基本上可以在位掩码do {} while循环开始时将offPixel写入端口,但是您需要验证内部获取数据+ if()或以恒定的时间和足够快的速度执行,以将时序nops替换为而是内部循环代码。但是,如果您只有 7+4+12 个周期可用,那么可能无法同时使用 4 个条带(假设:port=off 1c, 4x[fetch 2c, if+or 2c] port=on 1c, shift 1c, loop 2c = 21c of 23c ...并且您需要将端口提升到中间的某个位置,而不是 while 循环的结尾。
  • 在我看来 2 个条带几乎是可能的(手动调整组件),你不能更新 2+2 交错吗?甚至单条和循环 4 次。每个条带仍以微秒为单位,因此延迟更新可能对人类来说是不可见的,除非图像数据中发生了一些完美的水平滚动。但如果总时间在 5ms 以下,则可能不明显,或者只是非常轻微的“斜体”效果。其他选项是准备具有所需位模式的整个位图,因此您可以直接输出它(每个字节包含要输出的 2 个低 4 位)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-07-23
  • 2019-05-11
  • 2012-10-20
  • 1970-01-01
  • 1970-01-01
  • 2011-06-25
  • 1970-01-01
相关资源
最近更新 更多