ATtiny3217 x WS2812B梦幻联动

TinyAVR 1-series是Microchip于2018年推出的AVR单片机系列,定位是新一代的8位单片机,ATtiny3217是其中最高端的一款。相比于ATmega328P那个时代的AVR,ATtiny3217不只加强了组件的功能,更是加入了EVSYS(Event System)和CCL(Configurable Custom Logic)这两大支撑CIP(Core Independent Peripherals)的组件,使得硬件中的消息传递十分灵活。对于我来讲,有吸引力的是它带来的可玩性。html

惋惜,ATtiny3217只提供VQFN-24封装,并且国内渠道不太好买到,另外尚未下载器。第三方开发板目前尚未,官方的则价格很贵,下不了手。ios

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的SCKMOSI信号和一个定时器的波形输出的逻辑运算得到了能驱动WS2812B的信号。这让我对ATtiny3217的执念更加深了。sass

下面先来介绍一下今天的出场嘉宾。安全

ATtiny3217 Curiosity Nano

半年前,趁着能够用公款的时机,我拔草了种草已久的开发板。app

在某宝买的,一块那么小的开发板居然要105元。还有一款ATtiny3217 Xplained Pro,要300+,还不包括扩展板,超出了预算限制。店家只有现货1块,队友买第2块的时候商家告知要去定货,因而就退款了。wordpress

板上有两颗单片机:一个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(压栈)、SBICBI(I/O寄存器的位操做)各减小一个周期。其中PUSH是最值得关注的,由于它大幅缩短了从事件触发到用户中断代码开始执行的间隔。(一个不太典型的中断disassembly见AVR单片机教程——定时器中断,它不典型在push太少,通常至少十几个。)

时钟终于不用经过熔丝位设置了,CLKCTRL能够运行时切换时钟源。中断也终于有两个优先级了,但有不少限制。

外设方面,首先是从xmega开始,寄存器就以struct来组织,好比之前设置PB6为输出是DDRB |= 1 << 6,如今是PORTB.DIR |= 1 << 6PORTB.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的信号是单线的,一方面这简化了灯带的设计,对级联也比较友好,但另外一方面这种信号不是任何一种常见的总线,也不能由常见总线信号经过简单变换获得,这带来了一些困难。

每一位都是先高电平后低电平,01的差异在于高低电平的时间不一样,0的高电平时间比较短。容许的时间范围都是比较宽的。一般每一位都是等长的,那么一位的时间范围为1.16 μs到1.38 μs。

每一个灯有4个引脚:VCCGNDDINDODO上的信号是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。设置PER7,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.SINGLETCA0.SPLIT是什么关系呢?)

在SPI发送时要求WO2SCK同步,但此时并不知道计数器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种状态对应的输出。根据前面的时序图,在011101111三种状况下输出为1LUT值为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保护的,在ENABLE1时不能更改,所以要先配置其余寄存器,再enable LUT,最后enable CCL。

并不是每一个信号都能做为LUT的任意输入,如SCK只能接IN0MOSI只能接IN1,而普通的GPIO则不能直接接进LUT。若是须要的话,能够把GPIO接到event channel上,设置其用户为LUT,再在LUT中选择对应的EVOUT。若是SCK要接IN1MOSIIN0,只能用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上,输出;SCKMOSIWO2分别在PC0PC2PB2上,为了用逻辑分析仪观察波形,也配置为输出。

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滞后了。

继续向后观察,第三、四、5字节都貌似正常,第6字节又出错了。仔细观察,第3字节像是降低沿对齐的PWM信号,而第4字节是高电平中心对齐的(center-aligned)。以4字节为周期,后面重复。事实上,每两字节之间SCK低电平延长了2个CPU周期,至关于WO2信号的90°相位差;这样周期为4字节就很好理解了。

因此,以让我开心为目的的改进失败了。

讨论

TCA与SPI的同步

若是你仔细看代码的话,应该是没法理解TCA0.SINGLE.CNT = 3;中的magic number的。的确,这个数是我一点点改直到SCKWO2上升沿对齐这样试出来的。若是把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信号在字节间被延长,尤为是后者,给我浇了一盆冷水。我尚未验证这种方案,但大几率是不行的,好在我还有别的方案。

你有什么方案吗?欢迎在评论区留言。

相关文章
相关标签/搜索