TinyAVR 1-series是Microchip于2018年推出的AVR单片机系列,定位是新一代的8位单片机,ATtiny3217是其中最高端的一款。相比于ATmega328P那个时代的AVR,ATtiny3217不仅增强了组件的功能,更是加入了EVSYS(Event System)和CCL(Configurable Custom Logic)这两大支撑CIP(Core Independent Peripherals)的组件,使得硬件中的消息传递十分灵活。对于我来说,有吸引力的是它带来的可玩性。
可惜,ATtiny3217只提供VQFN-24封装,而且国内渠道不太好买到,另外还没有下载器。第三方开发板目前还没有,官方的则价格很贵,下不了手。
WS2812B是Worldsemi(华彩威)的一款内置控制电路的LED,RGB三种颜色均有8位256级亮度。WS2812B的数据信号为单线归零码,带整形输出,(理论上)可以支持无限级联。单片机PWM控制RGB灯占用大量定时器资源,以旧AVR型号为例,RGB三个通道至少需要2个定时器,而定时器总共不过3个。在各种外置控制方案中,WS2812B整合了控制逻辑,更加小巧。
WS2812B以5050、灯带和软屏等形式出售,很容易获得,自己用5050设计PCB也很方便。
有一天我读到一篇application note,其中有用ATtiny1617(3217同系列)的CCL实现WS2812B的总线。我起初感到十分新奇,在看懂了实现原理之后,我直接拍手叫好——它利用SPI的SCK
和MOSI
信号和一个定时器的波形输出的逻辑运算获得了能驱动WS2812B的信号。这让我对ATtiny3217的执念更加深了。
下面先来介绍一下今天的出场嘉宾。
ATtiny3217 Curiosity Nano
半年前,趁着可以用公款的时机,我拔草了种草已久的开发板。
在某宝买的,一块那么小的开发板竟然要105元。还有一款ATtiny3217 Xplained Pro,要300+,还不包括扩展板,超出了预算限制。店家只有现货1块,队友买第2块的时候商家告知要去订货,于是就退款了。
板上有两颗单片机:一个ATSAMD21E18,用作电源控制器、调试器、虚拟串口等;另一个当然是ATtiny3217啦。
没错,调试器,这对于AVR是不多见的,因为调试器只有Microchip卖,它又卖得很贵——我们通常只用USBasp下载器。新的AVR系列都用UPDI(Unified Program and Debug Interface)来调试,包括烧写,USBasp是不支持的(但好像能支持xmega的PDI),而Curiosity Nano不仅能给板上的单片机调试,还可以通过官方推荐的硬改来调试外部单片机。
开发板两边的排针孔之间有16 mil的错位,排针用力插进去就能连接牢固,无需焊接。
ATtiny3217虽然从名字上看属于tiny系列,实际上比作为mega的ATmega328P和ATmega324PA等老产品强不少,至少跟xmega是一个级别的。在它之上有megaAVR 0-series(以ATmega4809为代表)系列和DA/DB系列,都是新产品。
ATtiny3217拥有32 KB flash、256字节EEPROM和2 KB SRAM。新产品的EEPROM不是真正的EEPROM,而是在HEF(high-endurance flash)中模拟出来的,由NVMCTRL提供字节粒度的读写。(BTW:Microchip的PIC系列先开始这么做的;EEPROM成本较高,我在多款单片机中看到了用flash取代EEPROM的趋势。)
CPU方面,0-/1-series都用AVRxt指令集(见AVR® Instruction Set Manual),相比328的AVRe+改进了指令周期数,主要是写RAM更快,使CALL
(子过程调用)、ST
(写RAM)、PUSH
(压栈)、SBI
和CBI
(I/O寄存器的位操作)各减少一个周期。其中PUSH
是最值得关注的,因为它大幅缩短了从事件触发到用户中断代码开始执行的间隔。(一个不太典型的中断disassembly见AVR单片机教程——定时器中断,它不典型在push
太少,一般至少十几个。)
时钟终于不用通过熔丝位设置了,CLKCTRL可以运行时切换时钟源。中断也终于有两个优先级了,但有很多限制。
外设方面,首先是从xmega开始,寄存器就以struct
来组织,比如以前设置PB6
为输出是DDRB |= 1 << 6
,现在是PORTB.DIR |= 1 << 6
或PORTB.DIRSET = 1 << 6
。(xmega以前的AVR的寄存器定义是各单片机中做得最差的之一,就算我已经写过几十遍定时器1 ms中断,每次写之前还是得查datasheet才能知道WGM0[2:0]的哪个组合是CTC模式。但凡稍微正常一点的头文件都会给一个TC0_WGM_CTC
之类的宏吧。
The Amazing $1 Microcontroller:
The worst header files were from the megaAVR, the PSoC 4000S, the Kinetis KE04, the HT-66, the Sanyo LC-87. These header files have zero documentation, no predefined bit offsets, and no bit-addressable register definitions. Their header files are little more than register names attached to addresses.
其实他们明明可以把这些宏定义补上去的。)
每个外设都是新的,不仅是寄存器组织变了,功能也有很大改进:
-
GPIO:以
DIRSET
等寄存器和虚拟端口两种方式支持位操作;一些组件的输入输出信号对应两组引脚,可以整体切换。 -
定时器:16位TCA作PWM输出、2个16位TCB主要作输入、12位TCD生成两路同步PWM,还有一个16位RTC。
-
总线:USART中的fractional baud rate generator可以处理主频和波特率非整数倍的情况;SPI有了缓冲区;I²C支持1 MHz的Fm+,主机和从机可以在两组引脚上单独工作。
-
模拟:双10位ADC,其中一个会在需要时被电容触摸控制器占用,可通过随机延时消除任意频率的干扰;三个8位DAC,其中一个可以输出到外部;三个模拟比较器。
-
CIP:CCL用组合与时序逻辑实现事件的组合,EVSYS控制组件之间的连接。
针对CIP举个例子:按键按下时触发ADC转换,要求按键有消抖。常规的做法是每间隔一段时间读一次按键,用一定的算法消抖,判断按下时开始ADC转换;而借助CIP,这个功能可以这样实现:
按键的电平又GPIO读入,RTC产生一定频率的时钟,两者通过EVSYS接到CCL的LUT上(look-up table,可以实现任意3输入的组合逻辑,这里只用了按键一个输入),LUT输出接滤波器(filter,其输出在连续两次输入相同时才会更新),再通过EVSYS接到ADC触发转换。这些过程都是不需要CPU干预的,CPU此时应该处于一种睡眠状态,或在执行其他耗时的操作。ADC转换完成后产生中断,这才需要CPU执行相应代码。
WS2812B
WS2812B的信号是单线的,一方面这简化了灯带的设计,对级联也比较友好,但另一方面这种信号不是任何一种常见的总线,也不能由常见总线信号通过简单变换得到,这带来了一些困难。
每一位都是先高电平后低电平,0
和1
的差别在于高低电平的时间不同,0
的高电平时间比较短。允许的时间范围都是比较宽的。通常每一位都是等长的,那么一位的时间范围为1.16 μs到1.38 μs。
每个灯有4个引脚:VCC
、GND
、DIN
、DO
。DO
上的信号是DIN
信号除了前24个bit以外的部分,这24个bit以绿红蓝、MSB优先的顺序锁存进WS2812B。前一个灯的DO
接后一个的DIN
,如此级联。
没有信号时数据线保持低电平,当低电平时间超过280 μs时就会RESET,锁存的数据更新到亮度上。所有级联的灯在几乎同一时刻更新。
如果你以前接触过WS2812B,可能会觉得以上信息和你记忆中的有一些偏差。的确,上面这份datasheet来自官网,而网上流传的是之前的版本,外网上比较通用的版本如下:
有人对datasheet描述不明确感到不满,于是做了个实验测试高低电平时间的最低条件,并对WS2812B的内部原理作了猜测。实验结果如下:
方案
首先这不是我想出来的方案,链接在文首。
我们让定时器产生两倍于SCK
频率的方波WO2
,上升沿对齐;MOSI
设置为上升沿更新,从SCK
上升沿到下一个上升沿为一个bit。在这一bit中,高电平占前1/4为WS2812B的0
,1/2为1
。
单片机时钟频率为10 MHz(内部20 MHz,分频系数2),SCK
频率为10 MHz / 16 = 625 kHz,WO2
频率为1.25 MHz。这样算下来t0H = 400 ns,t0L = 1200 ns,t1H = t1L = 800 ns。尽管不符合上述任何一个版本的时序,但是都差得不大,实测可以工作(我也不知道我买的WS2812B应该参考哪个时序)。
时钟
ATtiny3217的时钟可以用程序更改,但还是有一个参数需要用熔丝位设置——内部RC时钟是20 MHz还是16 MHz。出厂默认是20 MHz,所以就不用改了。如果要改的话,在Microchip Studio(原Atmel Studio)的菜单栏Tools/Device Programming里。
CLKCTRL
寄存器组是被保护起来的,写入操作需要一个特殊的流程:先向CCP
(configuration change protection)寄存器里写IO寄存器对应的key,然后在4周期里写被保护的寄存器。
CCP = CCP_IOREG_gc;
CLKCTRL.MCLKCTRLB = CLKCTRL_PDIV_2X_gc | CLKCTRL_PEN_bm;
赋值号左边是寄存器,大部分都是分组的;右边的_gc
表示group configuration,_bm
表示bit mask,还有_bp
表示bit position。
下面是从iotn3217.h
(我们还是应该#include <avr/io.h>
)中截取的几段,展示了分组的寄存器定义以及相关的宏是如何用标准C语言实现的:
typedef volatile uint8_t register8_t;
//--------------------------------------------------------------------------
/* Clock controller */
typedef struct CLKCTRL_struct
{
register8_t MCLKCTRLA; /* MCLK Control A */
register8_t MCLKCTRLB; /* MCLK Control B */
register8_t MCLKLOCK; /* MCLK Lock */
register8_t MCLKSTATUS; /* MCLK Status */
register8_t reserved_1[12];
register8_t OSC20MCTRLA; /* OSC20M Control A */
register8_t OSC20MCALIBA; /* OSC20M Calibration A */
register8_t OSC20MCALIBB; /* OSC20M Calibration B */
register8_t reserved_2[5];
register8_t OSC32KCTRLA; /* OSC32K Control A */
register8_t reserved_3[3];
register8_t XOSC32KCTRLA; /* XOSC32K Control A */
register8_t reserved_4[3];
} CLKCTRL_t;
/* CLKCTRL.MCLKCTRLB bit masks and bit positions */
#define CLKCTRL_PEN_bm 0x01 /* Prescaler enable bit mask. */
#define CLKCTRL_PEN_bp 0 /* Prescaler enable bit position. */
#define CLKCTRL_PDIV_gm 0x1E /* Prescaler division group mask. */
#define CLKCTRL_PDIV_gp 1 /* Prescaler division group position. */
#define CLKCTRL_PDIV0_bm (1<<1) /* Prescaler division bit 0 mask. */
#define CLKCTRL_PDIV0_bp 1 /* Prescaler division bit 0 position. */
#define CLKCTRL_PDIV1_bm (1<<2) /* Prescaler division bit 1 mask. */
#define CLKCTRL_PDIV1_bp 2 /* Prescaler division bit 1 position. */
#define CLKCTRL_PDIV2_bm (1<<3) /* Prescaler division bit 2 mask. */
#define CLKCTRL_PDIV2_bp 3 /* Prescaler division bit 2 position. */
#define CLKCTRL_PDIV3_bm (1<<4) /* Prescaler division bit 3 mask. */
#define CLKCTRL_PDIV3_bp 4 /* Prescaler division bit 3 position. */
/* Prescaler division select */
typedef enum CLKCTRL_PDIV_enum
{
CLKCTRL_PDIV_2X_gc = (0x00<<1), /* 2X */
CLKCTRL_PDIV_4X_gc = (0x01<<1), /* 4X */
CLKCTRL_PDIV_8X_gc = (0x02<<1), /* 8X */
CLKCTRL_PDIV_16X_gc = (0x03<<1), /* 16X */
CLKCTRL_PDIV_32X_gc = (0x04<<1), /* 32X */
CLKCTRL_PDIV_64X_gc = (0x05<<1), /* 64X */
CLKCTRL_PDIV_6X_gc = (0x08<<1), /* 6X */
CLKCTRL_PDIV_10X_gc = (0x09<<1), /* 10X */
CLKCTRL_PDIV_12X_gc = (0x0A<<1), /* 12X */
CLKCTRL_PDIV_24X_gc = (0x0B<<1), /* 24X */
CLKCTRL_PDIV_48X_gc = (0x0C<<1), /* 48X */
} CLKCTRL_PDIV_t;
//--------------------------------------------------------------------------
#define CLKCTRL (*(CLKCTRL_t *) 0x0060) /* Clock controller */
SPI
上升沿串出,下降沿采样,这是SPI mode 1。SCK
频率为主频除以16。
SPI0.CTRLA = SPI_MASTER_bm | SPI_PRESC_DIV16_gc | SPI_ENABLE_bm;
SPI0.CTRLB = SPI_SSD_bm | SPI_MODE_1_gc;
SPI发送一字节:向寄存器写入来发送,轮询寄存器等待发送完成。
SPI0.DATA = byte;
while (!(SPI0.INTFLAGS & SPI_IF_bm))
;
TCA
产生方波通常用CTC(现FRQ)模式,但是极性不好控制(其实现在有CMPnOV
位了),改用PWM。设置PER
为7
,PWM周期为8个CPU周期;CMP2
为4,占空比为4 / 8 = 50%。
没有硬件设施可以实现定时器和SPI的同步,所以在初始化中先不开启定时器输出。
TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc;
TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP2EN_bm | TCA_SINGLE_WGMODE_SINGLESLOPE_gc;
TCA0.SINGLE.PER = 7;
TCA0.SINGLE.CMP2 = 4;
(TCA有两种模式:一个16位(single)和两个8位(split)。你觉得TCA0.SINGLE
和TCA0.SPLIT
是什么关系呢?)
在SPI发送时要求WO2
和SCK
同步,但此时并不知道计数器CNT
的值,所以把它清零,然后开启输出。SPI发送完后再关闭输出。
void ws2812b_write(uint8_t byte)
{
TCA0.SINGLE.CNT = 0;
TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc | TCA_SINGLE_ENABLE_bm;
SPI0.DATA = byte;
while (!(SPI0.INTFLAGS & SPI_IF_bm))
;
TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc;
TCA0.SINGLE.CTRLC = 0;
}
CCL
LUT
寄存器的8位分别存放IN[2:0]
的8种状态对应的输出。根据前面的时序图,在011
、101
和111
三种情况下输出为1
,LUT
值为0xA8
。
CCL.LUT1CTRLB = CCL_INSEL1_SPI0_gc | CCL_INSEL0_SPI0_gc;
CCL.LUT1CTRLC = CCL_INSEL2_TCA0_gc;
CCL.TRUTH1 = 0xA8;
CCL.LUT1CTRLA = CCL_OUTEN_bm | CCL_ENABLE_bm;
CCL.CTRLA = CCL_RUNSTDBY_bm | CCL_ENABLE_bm;
CCL的寄存器是被ENABLE
保护的,在ENABLE
为1
时不能更改,因此要先配置其他寄存器,再enable LUT,最后enable CCL。
并非每个信号都能作为LUT的任意输入,如SCK
只能接IN0
、MOSI
只能接IN1
,而普通的GPIO则不能直接接进LUT。如果需要的话,可以把GPIO接到event channel上,设置其用户为LUT,再在LUT中选择对应的EVOUT。如果SCK
要接IN1
而MOSI
接IN0
,只能用EVSYS这种方法,但这没有任何意义——总是可以通过修改LUT
达到相同的功能。
GPIO
(Datasheet中的一些“GPIO”指的是GPIOR(general-purpose I/O registers),我们讲的GPIO叫“PORT”,有些章节里也叫“GPIO”。)
为了和application note中一致,SPI0和LUT1的输出都移到非默认的引脚上,在那里默认引脚和其他功能冲突了。Alternative pins通过PORTMUX
配置:
PORTMUX.CTRLA = PORTMUX_LUT1_ALTERNATE_gc;
PORTMUX.CTRLB = PORTMUX_SPI0_ALTERNATE_gc;
按键在PB7
上,没有外部上拉电阻,启用内部上拉电阻(在);LED在PA3
上,LUT1-OUT
即WS2812B的信号在PC1
上,输出;SCK
、MOSI
、WO2
分别在PC0
、PC2
、PB2
上,为了用逻辑分析仪观察波形,也配置为输出。
PORTA.DIRSET = PIN3_bm;
PORTB.DIRSET = PIN2_bm;
PORTB.PIN7CTRL = PORT_PULLUPEN_bm;
PORTC.DIRSET = PIN2_bm | PIN1_bm | PIN0_bm;
为了便于测试,写个在按键按下时翻转LED并写8个WS2812B的逻辑(点击展开):
#include <stdbool.h>
#include <avr/io.h>
#define F_CPU 10000000
#include <util/delay.h>
void ws2812b_write(uint8_t byte)
{
TCA0.SINGLE.CNT = 0;
TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc | TCA_SINGLE_ENABLE_bm;
SPI0.DATA = byte;
while (!(SPI0.INTFLAGS & SPI_IF_bm))
;
TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc;
TCA0.SINGLE.CTRLC = 0;
}
int main()
{
CCP = CCP_IOREG_gc;
CLKCTRL.MCLKCTRLB = CLKCTRL_PDIV_2X_gc | CLKCTRL_PEN_bm;
SPI0.CTRLA = SPI_MASTER_bm | SPI_PRESC_DIV16_gc | SPI_ENABLE_bm;
SPI0.CTRLB = SPI_SSD_bm | SPI_MODE_1_gc;
TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc;
TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP2EN_bm | TCA_SINGLE_WGMODE_SINGLESLOPE_gc;
TCA0.SINGLE.PER = 7;
TCA0.SINGLE.CMP2 = 4;
CCL.LUT1CTRLB = CCL_INSEL1_SPI0_gc | CCL_INSEL0_SPI0_gc;
CCL.LUT1CTRLC = CCL_INSEL2_TCA0_gc;
CCL.TRUTH1 = 0xA8;
CCL.LUT1CTRLA = CCL_OUTEN_bm | CCL_ENABLE_bm;
CCL.CTRLA = CCL_RUNSTDBY_bm | CCL_ENABLE_bm;
PORTMUX.CTRLA = PORTMUX_LUT1_ALTERNATE_gc;
PORTMUX.CTRLB = PORTMUX_SPI0_ALTERNATE_gc;
PORTA.DIRSET = PIN3_bm;
PORTB.DIRSET = PIN2_bm;
PORTB.PIN7CTRL = PORT_PULLUPEN_bm;
PORTC.DIRSET = PIN2_bm | PIN1_bm | PIN0_bm;
bool prev = 0;
while (1)
{
bool curr = PORTB.IN & PIN7_bm;
if (prev && !curr)
{
for (uint8_t i = 0; i != 24; ++i)
ws2812b_write(0x0A);
PORTA.OUTTGL = PIN3_bm;
}
prev = curr;
_delay_ms(1);
}
}
测试结果
It works!
这是一个字节的波形。WO2
在左右各有一个额外的周期,但这并不影响LUT1-OUT
在闲时为低电平(idle state = low)。
改进
先别高兴得太早,看看这里最后两个字节:
两个字节之间有明显的间隔,这从代码里也能看出来。虽然间隔时间比实测最短的RESET时间9 μs还要短一半,但让我很不舒服。
ATtiny3217的SPI有一个缓冲字节,利用它或许可以实现多个字节连续发送:
void ws2812b_write(const uint8_t* byte, uint8_t length)
{
TCA0.SINGLE.CNT = 3;
TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc | TCA_SINGLE_ENABLE_bm;
SPI0.INTFLAGS |= SPI_TXCIF_bm;
for (const uint8_t* end = byte + length; byte != end; ++byte)
{
while (!(SPI0.INTFLAGS & SPI_DREIE_bm))
;
SPI0.DATA = *byte;
}
while (!(SPI0.INTFLAGS & SPI_TXCIF_bm))
;
TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc;
TCA0.SINGLE.CTRLC = 0;
}
记得在AVR单片机教程——DAC中,USART in SPI mode的缓冲区一方面让我要额外注意每次都要把UDR0
读掉以获得新鲜的数据,另一方面在我需要连续发送两个字节时相比SPI更节省CPU资源,让我得以实现音乐播放器。如果要在编程简单和功能强大之间选择的话,我还是会选择后者。那么这次ATtiny3217的SPI缓冲区能否让它胜任WS2812B的连续发送呢?让我们来看看波形:
前两行很符合预期——SCK
信号没有出现间断。加入第三行,密集的线条可能迷惑了你的双眼,但是第四行足够明显——第一字节的输出是正常的,但是第二字节就不对了。究其原因,是第二字节的第一个SCK
上升沿出现在它本来应该对应的WO2
上升沿和它后面的下降沿中间,换言之SCK
滞后了。
继续向后观察,第3、4、5字节都貌似正常,第6字节又出错了。仔细观察,第3字节像是下降沿对齐的PWM信号,而第4字节是高电平中心对齐的(center-aligned)。以4字节为周期,后面重复。事实上,每两字节之间SCK
低电平延长了2个CPU周期,相当于WO2
信号的90°相位差;这样周期为4字节就很好理解了。
所以,以让我开心为目的的改进失败了。
讨论
TCA与SPI的同步
如果你仔细看代码的话,应该是无法理解TCA0.SINGLE.CNT = 3;
中的magic number的。的确,这个数是我一点点改直到SCK
和WO2
上升沿对齐这样试出来的。如果把SPI0.INTFLAGS |= SPI_TXCIF_bm;
这一句移到前面去,这个数就得改成7
——这说明移的那句需要4个周期来执行。
同理,改进前的TCA0.SINGLE.CNT = 0;
也只是一个巧合,而不是像application note上说的那样:
Since there is no synchronization between the TCA output and the SPI clock, it is necessary to start and stop the TCA each time data is sent to the LEDs. It is also necessary to clear the TCA CNT register before TCA is started. This is done to make sure that the TCA starts counting from zero each time the LEDs are updated.
很显然,这样做是低效的、不安全的:低效在这个magic number需要花工夫去找,不安全在也许改变一下编译器的优化等级就能让你花的工夫作废。
另一种逻辑
老版本WS2812B的时序可以大致理解为1/3和2/3的高电平占比,而上述方案只能实现分母为4的占比。不过就1
而言,3/4比1/2更接近2/3,要做到3/4也只需要把IN[2:0] = 0b110
对应的输出改成1
就可以了。为什么application note不是这样做的呢?
在1/2的方案中,只要SCK
为低电平,输出就是低电平;SCK
的闲时电平是SPI mode能完全确定的,因而能保证输出的闲时电平为低。在3/4的方案中,三输入的组合逻辑可以理解为输入有至少两个高电平时输出为高(提问:哪款常见的逻辑IC能实现这样的功能?);那么如果数据的LSB为1
,输出就完全跟着WO2
走。而WO2
在SPI发送完后还有一段高电平,除非这一段能被消除,否则3/4方案就是不可行的。
那么如何消除呢?也可以像上面那样搞个magic number,开始发送后等待这么多个周期,然后关闭TCA输出。这个数只要在一个[n, n+3]的区间里即可,没那么严格。但是,一旦主频改变,重新找吧!
IO分配与占用
我开了SCK
等信号的输出,是为了看波形,如果不开,那个引脚还可以用吗?输出是不行的,一旦DIR
位为1
,它输出的就是SCK
信号;输入或许可以。
所以,尽管我只需要SCK
信号在内部使用,它却必须占用一个引脚,这好吗?ATtiny3217一共只有24个pin,尽管有alternative pins,但毕竟总数摆在这,挺容易冲突的。不知Microchip的工程师有没有思考过这个问题,还是说tiny系列的应用场景连24 pins都已经嫌多了?或许吧,虽然我舍不得。
那么如何安排引脚呢?Atmel START是一个在线的工具,帮助你配置引脚、时钟和各种组件,就像隔壁厂家的某立方体一样。
后记
最近在做一个涉及WS2812B灯带的项目。为了锻炼自己,我要把整个写级联WS2812B的操作做成无需CPU干预的,这当然离不开DMA。我在网上找到三种方案,但它们都有严重的内存overhead,以至于很难把整个灯带的数据在一次DMA请求中发送出去,至少不划算。
本文的方案则不存在这样的问题,因为WS2812B的一个字节就对应SPI的一个字节。但是TCA与SPI的同步和SCK
信号在字节间被延长,尤其是后者,给我浇了一盆冷水。我还没有验证这种方案,但大概率是不行的,好在我还有别的方案。
你有什么方案吗?欢迎在评论区留言。