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
信號在字節間被延長,尤其是后者,給我澆了一盆冷水。我還沒有驗證這種方案,但大概率是不行的,好在我還有別的方案。
你有什么方案嗎?歡迎在評論區留言。