在上一篇中,老周用 .NET Nano Framework 給大伙伴們演示了 WS2812 燈帶的控制,包括用 SPI 和 紅外RMT 的方式。利用 RMT 是一個很機靈的方案,不過,可能很多大伙伴對 ESP32 的 RMT 不是很熟悉。除了樂鑫自己的文檔,沒幾個人寫過相關(guān)的水文,可見這里頭空白的水市場很充足,老周一時手癢,就決定再水一篇博文。
不管你有沒有做過物聯(lián)網(wǎng)項目,只要你有關(guān)注,你就會發(fā)現(xiàn),當今時尚流行忽悠不擦嘴巴。許多教程就拿個 MicroPython 或者 Arduino,貼幾行代碼,然后叫你燒錄進去看效果。可是,效果看完了,你知道了啥?你學到了啥?你知道這里頭做了啥?全 TM 不知。做教程的人只管忽悠,然后就沒下文了。這就是它們老喜歡用 Python 的原因?;谀_本語言的特性,很多庫都是高度封裝的,拿來直接敲幾行代碼就完事了。寫教程的是這樣,做培訓(xùn)的也是這樣。
用 Arduino 好不好?好,開柜即用;用 MicroPython 好不好?好,開桶即用。這就是現(xiàn)在為什么 Py 流行的原因,做培訓(xùn)的演示起來多起勁,幾行代碼(估計他們?yōu)檫@幾行代碼都練了無數(shù)次,都背下來了)天天敲,而且這么簡單的代碼,現(xiàn)場演示也不怕出錯,然后告訴你:看看,做 AI,做 Iot 多簡單!但是,老周是很 BS 這些人的,只告訴你吃魚很香,卻不告訴你怎么捕的魚。Python 不是不能用,而是你不能指望憑它來學編程。腳本語言本來就是做輔助用的。
如果你一開始用的是 C 語言,就算你沒在做項目,你反而可以堅持玩幾年,甚至十幾年。哪怕業(yè)余玩玩,也能一層一層地挖掘出很多有趣的東西。
還有一種更離譜的觀點:Py 適合科研人員,可以快速驗證結(jié)果。C語言留給開發(fā)的苦逼去干。老周認為:做科研的人在底層和基礎(chǔ)知識方面更應(yīng)該比開發(fā)的人強,不然你研究個鴕鳥蛋!連基本的原理和細節(jié)都搞不清楚,那就是紙上談兵,洗錢罷了。就像現(xiàn)在某些建筑,某些服裝,為什么會出現(xiàn)許多反人類設(shè)計;很多產(chǎn)品也是反人類設(shè)計?正是因為做設(shè)計的人對生產(chǎn)、對技術(shù)、對基礎(chǔ)原理不了解,閉上眼睛無腦瞎編亂涂。有些設(shè)計人員對自己、對產(chǎn)品、對他人也是不負責的,自己設(shè)計的東西做出來,也不去試用一下,看看你設(shè)想的東西多么不靠譜。
所以,老周寫的東西,一直以來都是立足于實際使用的,而不是立足炒作和無腦吹。吹得天花亂墜,如果用起來很難用的東西,老周是不會推薦的。
好了,不小心扯了一堆沒用的了。有大伙伴可能說老周這么批別人不會得罪人嗎?得不得罪有啥關(guān)系呢,老周跟他們又不是一伙的,沒有利益關(guān)系,他們敢拿導(dǎo)彈轟老周嗎?
OK,不扯蛋了。說回 RMT,ESP32 中,一個周期 RMT 消息共 32 位,分兩段,每段16位。然后老周給你畫個圖。
別以為 32 個位這么多能描述一整條消息,不是的,它只描述了一個脈沖周期罷了。你看,這個脈沖是不是被分成了兩段?為什么要分成兩段?因為這樣就能說清楚了:高電平占了多長時間,低電平占了多長時間。也就是說,這一幀的數(shù)據(jù)包含了兩個電平的參數(shù)。
1-16 位是第一個電平,前15位表示該電平持續(xù)的時間,最后一位(圖中的 L)表示電平,1表示高電平,0表示低電平;
17-32 位是第二個電平,前15位表示該電平的時長,最后一位表示電平,1是高,0是低。
舉個例子:
0000 1101 0011 0111 0011 1111 0101 1000
先看第一行,最后一位是1,說明是低電平,時長就是 0000 1101 0011 011,不含最后一位。
第二行呢,最后一位是0,說明是低電平,時長就是 0011 1111 0101 100,不含最后一位。
如果整個脈沖全是低電平呢,那就這樣:
0000 0000 1111 0110 0000 0000 0110 1010
最后一位都是0,就表明這個周期沒有高電平。于是,你能想到,如果一個周期內(nèi)全是高電平呢,是不是這樣?
0000 1111 0101 0011 0000 1000 0110 0111
至于電平的時間長度是單位,這個要看定時器的頻率的。還記得嗎?上一篇水文中,老周說默認用的是 APB 時鐘,80 MHz,假設(shè)我們分頻后讓定時器的頻率變成 1 MHz,即 1 000 000 Hz,然后 1s / 1000000 = 0.000001 秒,即 1 微秒(us)/ Tick。那么,這個15位的整數(shù)就和微秒數(shù)一致。
現(xiàn)在,你明白了 RMT 是怎么描述一個脈沖的了,于是,IDF 中有這么個類型:
typedef union { struct { uint16_t duration0 : 15; uint16_t level0 : 1; uint16_t duration1 : 15; uint16_t level1 : 1; }; uint32_t val; } rmt_symbol_word_t;
咦,這個類型咋這么怪?。坎还?,這種貨叫做內(nèi)聯(lián),說人話就是:里面的結(jié)構(gòu)體和 val 的值共用內(nèi)存。
前面的 struct 有四個字段:
duration0:第一個電平的時長,后面的冒號和15表示它占 15 位;
level0:表示第一個電平值,占一位;
duration1:第二個電平的時長,占 15 位;
level1:第二個電平的值,占一位。
那么,我問你,這四個字段加起來多位,是不是 32 ?val 的類型是 uint32 ,無符號32位整數(shù)。前面的結(jié)構(gòu)體和 val 是不是大小相同?都是4個字節(jié)?是吧,于是,它們用同一塊內(nèi)存,也就是說,這個?rmt_symbol_word_t 你可以用四個字段去設(shè)置它,也可以直接用一個整數(shù)去設(shè)置。C 語言是直接操作內(nèi)存的,可以強制轉(zhuǎn)換,在后面調(diào)用相關(guān)函數(shù)時,可以取地址直接賦值給 void* (指針)。
請你記住這個類型,你可以字面翻譯為”符號字“,或者叫 RMT 描述符號。記好了,一個符號字只描述一個周期的脈沖哦。要是向 WS2812 發(fā)數(shù)據(jù),RGB共 24 位,一個燈珠你就要發(fā) 24 個 符號字,點亮兩個燈就發(fā) 48 個符號字。我要點100個燈呢,那就 24*100 唄。你不妨理解為:一個符號字就是代表一個二進制位。有幾個二進制位就得發(fā)送幾個符號。
這里要說明一點:.NET Nano Framework 用的 IDF 是 4xx 的,而目前新的版本是 5xx 的,新舊版本之間在 RMT 操作上有很大區(qū)別,函數(shù)也不同。不過,原理差不多,說直白一點就是:把內(nèi)存中的 rmt_symbol_word_t 隊列發(fā)送出去。
由于版本更新,.NET Nano Framework 后面肯定要適配新版 IDF 的,所以,老周決定用新的版本的方式演示。在新版本 API 中,不需要分頻設(shè)定了。其實直接設(shè)置頻率更好,尤其是對初學者,總覺得分頻很難懂。不過老周可以把分頻總結(jié)為:把總線/或CPU/或其他振蕩源的頻率除以某個數(shù),得到更低的頻率。即原來的頻率太高了,要降一降。比較,原頻 120 MHz,分頻系數(shù)為 4,那就調(diào)整為 120/4 = 30 MHz。樹莓派(Raspberry Pi Pico)Pico 的官方SDK中,PWM的頻率也是用到了分頻。不過小草莓先分頻,再計數(shù)。先把頻率降一下,然后周期性地數(shù) 256(0-255),如果計數(shù)滿 255 重新回到 0,再計數(shù)。所以,RPI Pico 的 PWM 頻率其實算起來挺麻煩,要考慮分頻,還要考慮計數(shù)次數(shù)。
ESP 32 新的 IDF 直接讓你配置頻率了,這樣更方便更直觀。
下面老周說說 RMT API 怎么用。不要聽別人造謠,說 IDF 很難用,其實不難用的。畢竟是官方的,功能很全,官方團隊直接維護。老周安裝 IDF 就沒失敗過,這里再次強調(diào)用兩點,保證你能成功安裝:1、裝好 Python 后,pip 改國內(nèi)源;2、在 VS Code 的 Esp 插件中下載 IDF時,選樂鑫的服務(wù)器,不要選 github。
然后,其他選項你隨意。其實它無非就用到兩個目錄,一個放 IDF 的源碼,一個放編譯的 tools。然后會設(shè)置環(huán)境變量 IDF_PATH 等。
下面請記住一個萬能規(guī)律,不管你用的什么開發(fā)板,什么芯片,什么平臺,所有外部設(shè)備的通信都是這樣的流程:
1、配置參數(shù);
2、init(初始化);
3、加載驅(qū)動(一般在 init 時就完成,這一步許多平臺可省略);
4、讀/寫數(shù)據(jù);
5、清理資源。
一、配置階段
RMT API 定義專門的結(jié)構(gòu)體,用于配置參數(shù)。
typedef struct { gpio_num_t gpio_num; rmt_clock_source_t clk_src; uint32_t resolution_hz; size_t mem_block_symbols; size_t trans_queue_depth; int intr_priority; struct { uint32_t invert_out: 1; uint32_t with_dma: 1; uint32_t io_loop_back: 1; uint32_t io_od_mode: 1; } flags; } rmt_tx_channel_config_t;
這是配置發(fā)送的,如果接收數(shù)據(jù),要用?rmt_rx_channel_config_t,用起來一樣,搞懂一個,另一個就懂了。注意,接收和發(fā)送的函數(shù)是分布在兩個頭文件中的,發(fā)送是 rmt_tx.h,接收是 rmt_rx.h。因為驅(qū)動 WS2812 是輸出,屬于發(fā)送模式,咱們只用?rmt_tx_channel_config_t 結(jié)構(gòu)體。
不要看它那么多成員,其實,在實際使用時,咱們不需要全都用,不用的保持默認(不賦值就是了)。
gpio_num:用來發(fā)信號的引腳,GPIO 號。這個可用枚舉值(在 gpio_num.h 頭文件中),如?GPIO_NUM_0 表示 GPIO0,GPIO_NUM_33 表示 GPIO33,也可以直接用整數(shù),如 33、25、8 等。
clk_src:振動的時鐘源,可以用?RMT_CLK_SRC_DEFAULT 表示默認值,即用 APB 時鐘,80兆那個。一般不用選其他,畢竟不是每個板子都通用,默認是比較通用。
resolution_hz:這個就是直接設(shè)置頻率了,不用思考分頻的事了。
mem_block_symbols:分配內(nèi)存量,常用 64。注意它的大小不是字節(jié),而是 符號字(rmt_symbol_word_t),就是最開始咱們介紹那個,32位兩個階段那個,描述兩個電平時長的。比如,設(shè)置64就是分配的內(nèi)存可以放 64 個符號字,字節(jié)是 64 * 4,32位嘛,是吧,前面反復(fù)說了。
trans_queue_depth:隊列深度,一般不要太大,4 或 8 均可。數(shù)據(jù)在傳輸時,不是馬上就發(fā)出去的,而是放進一個隊列中,然后驅(qū)動層會調(diào)度這個隊列,慢慢發(fā)(其實很快發(fā)完)。設(shè)置為4表示隊列中可以放(掛起)4條等待傳輸?shù)姆栕帧?/p>
intr_priority:中斷的優(yōu)先值,非特殊情況保持默認。
另外,此結(jié)構(gòu)體內(nèi)嵌了一個 flags 結(jié)構(gòu)體。
invert_out:是否電平反向,1表示開啟。就是反轉(zhuǎn)電平,比如,本來高的變低,低的變高。這個一般不用;
with_dma:是否走 DAM 通道,不占用CPU運算資源;
io_loop_back:就跟在電腦上 ping 127.0.0.1 一樣,“我發(fā)給我自己”,即自發(fā)自收(在同一引腳上)。這個一般沒啥用。
io_od_mode:是否設(shè)置為開漏模式。
二、初始化階段
配置完相關(guān)參數(shù)后,調(diào)用?rmt_new_tx_channel 函數(shù),用已配置的參數(shù)創(chuàng)建通信通道。
esp_err_t rmt_new_tx_channel(const rmt_tx_channel_config_t *config, rmt_channel_handle_t *ret_chan);
config 引用配置結(jié)構(gòu)體實例,ret_chan 接收創(chuàng)建的通道句柄,后面在發(fā)送數(shù)據(jù)時要用。所以,在調(diào)用此函數(shù)前,先聲明一個?rmt_channel_handle_t 類型的變量,最后是全局的。
新版 API 雖然精簡了許多,但也有缺點:在配置好參數(shù)創(chuàng)建通道后,就不能再修改參數(shù)了,除非重新初始化。而舊版 API 是可以修改的。
三、啟用通道
調(diào)用?rmt_enable 函數(shù)啟用通道。
esp_err_t rmt_enable(rmt_channel_handle_t channel);
channel 就是剛剛創(chuàng)建的通道。這一步很關(guān)鍵,也很容易遺忘。不啟用通道的話,是無法接收和發(fā)送數(shù)據(jù)的。如果忘了,你測試來測試去,死活不能工作,你甚至會懷疑自己寫錯了協(xié)議。如果要禁用通道,可以調(diào)用?rmt_disable 函數(shù)。
esp_err_t rmt_disable(rmt_channel_handle_t channel);
這兩個函數(shù)都聲明在 rmt_common.h 頭文件中。
四、創(chuàng)建編碼器
創(chuàng)建編碼器可以在啟用通道之前完成,第【三】、【四】階段順序不重要。IDF 內(nèi)置兩個編碼器:
1、bytes encoder:就是把你給它的字節(jié)數(shù)組轉(zhuǎn)換為符號字(前面說過的 rmt_symbol_word);
2、copy encoder:這玩意兒很玄,如果你看官方文檔介紹可能會懷疑人生,不知道說啥。老周用一句話概括:這貨就是不處理不轉(zhuǎn)換,你直接把符號字傳給它,然后它復(fù)制到驅(qū)動層的內(nèi)存中,放入隊列準備發(fā)送。數(shù)據(jù)只是被復(fù)制,不會修改。這是防止讓驅(qū)動空間的代碼跨空間引用用戶代碼,那樣有內(nèi)存泄漏的風險,復(fù)制數(shù)據(jù)就不存在跨空間長距離引用,發(fā)完就清理。用戶代碼可能長期保持數(shù)據(jù)的生命周期。
當然,你可以寫自己的編碼器(組合使用內(nèi)置的編碼器)。若要自定義,請認識一下?rmt_encoder_t 結(jié)構(gòu)體。
struct rmt_encoder_t { /* 編碼時用 */ size_t (*encode)(rmt_encoder_t *encoder, rmt_channel_handle_t tx_channel, const void *primary_data, size_t data_size, rmt_encode_state_t *ret_state); /* 重置編碼器參數(shù)時用 */ esp_err_t (*reset)(rmt_encoder_t *encoder); /* 清理編碼器時用 */ esp_err_t (*del)(rmt_encoder_t *encoder); };
這個結(jié)構(gòu)體的成員都是函數(shù)指針,你讓它們分別指向你定義的函數(shù),就實現(xiàn)了自定義編碼了。這個東西你可能看得很繞,為什么函數(shù)的輸入?yún)?shù)還要 rmt_encoder_t ?這是因為 C 結(jié)構(gòu)體不能繼承,要想實現(xiàn)類開繼承的功能,就得定義一個更大的結(jié)構(gòu)體,然后大結(jié)構(gòu)體中引用 rmt_encoder_t,模擬調(diào)用基類成員。由于 IDF 支持 C++,為了好用,你不妨用 C++ 類去封裝。
看看官方的源碼是怎么封裝的。
typedef struct rmt_bytes_encoder_t { rmt_encoder_t base; // encoder base class size_t last_bit_index; // index of the encoding bit position in the encoding byte size_t last_byte_index; // index of the encoding byte in the primary stream rmt_symbol_word_t bit0; // bit zero representing rmt_symbol_word_t bit1; // bit one representing struct { uint32_t msb_first: 1; // encode MSB firstly } flags; } rmt_bytes_encoder_t; typedef struct rmt_copy_encoder_t { rmt_encoder_t base; // encoder base class size_t last_symbol_index; // index of symbol position in the primary stream } rmt_copy_encoder_t;
就是定義一個結(jié)構(gòu)體,然后里面有個 base,base 就是 rmt_encoder_t 類型,這就等于從抽象基類派生出 rmt_bytes_encoder和rmt_copy_encoder類型,其他成員則用于參數(shù)配置。訪問 encode、reset、del 函數(shù)指針時就通過 S.base.encode(....) 來調(diào)用。當然,你自己寫的話不一定要搞那么復(fù)雜,就是按 rmt_encoder_t 結(jié)構(gòu)的三個函數(shù)指針成員,引其引用你寫的函數(shù)就行了。
初始化 bytes encoder 使用?rmt_new_bytes_encoder 函數(shù),初始化 copy encoder 使用?rmt_new_copy_encoder 函數(shù)。調(diào)用函數(shù)前,先聲明?rmt_encoder_handle_t 類型的變量,該變量會引用創(chuàng)建的編碼器,由函數(shù)的 ret_encoder 參數(shù)賦值。
esp_err_t rmt_new_bytes_encoder(const rmt_bytes_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder); esp_err_t rmt_new_copy_encoder(const rmt_copy_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder);
創(chuàng)建編碼器后用變量保存引用,不需要我們手動調(diào)用,傳輸數(shù)據(jù)時會自動調(diào)用。
五、發(fā)送數(shù)據(jù)
發(fā)送數(shù)據(jù)調(diào)用?rmt_transmit 函數(shù),參數(shù)包括:剛創(chuàng)建的通道、編碼器,以及要發(fā)送的符號字數(shù)組(多個符號字一同推入隊列,不必一個一個推)。調(diào)用此函數(shù)只是把消息放進傳輸隊列,至于是否立即發(fā)送,那看隊列里面擁不擁擠了,由驅(qū)動層自行處理,我們不用管。
如果你不使用中斷,但希望等到數(shù)據(jù)發(fā)出去了再執(zhí)行后面的程序代碼,那可以調(diào)用?rmt_tx_wait_all_done 函數(shù),它會等待指定的時間,直到數(shù)據(jù)發(fā)送出去才返回。等待時間可以用最大值——?portMAX_DELAY。
六、清理
如果你的程序不是一直發(fā)數(shù)據(jù),或只是特定時候發(fā)送。那傳輸完數(shù)據(jù)后應(yīng)當清理相應(yīng)的對象。
rmt_del_encoder:清除剛創(chuàng)建的編碼器。
rmt_disable:禁用通道。
rmt_del_channel:清除通道。
如果程序一直發(fā)數(shù)據(jù),可以不清理。
?文章來源:http://www.zghlxwxcb.cn/news/detail-855124.html
官方有一個示例是用 RMT 驅(qū)動燈帶的,但那個用了混合編碼器,弄得有點復(fù)雜,老周這里直接用 copy encoder 復(fù)制符號字。符號字咱們自己生成。
先做好初始化工作。
1、聲明相關(guān)參數(shù)。
// 聲明區(qū) #define GPIO_NUM 6 // 引腳號 #define TICK_FREQ 10 * 1000000 // 頻率 #define LED_NUM 24 // 燈珠數(shù)目
這里我把頻率設(shè)置為 10 MHz,即一 tick 為 0.1 us。因為 WS2812 的電平時長有 0.2-0.8 us,所以要把 Tick 精確到 0.1 us,這樣好控制。
2、聲明全局變量。
static rmt_channel_handle_t txChannel; /* 編碼器 */ static rmt_encoder_handle_t rfEncoder; /* 消息符號 */ static rmt_symbol_word_t zeroSymbol, oneSymbol, resetSymbol; /* 要傳輸?shù)念伾珨?shù)據(jù) */ static rmt_symbol_word_t rgbSymbols[24 * LED_NUM] = {0};
注意符號字數(shù)組,大小是燈珠數(shù) * 24。為什么24呢?因為 RGB 數(shù)據(jù)加起來24位,一個符號字只能描述一個位。
zeroSymbol 表示發(fā)送 0 時的電平,表示發(fā)送 1 時的電平,resetSymbol 是復(fù)位電平,每發(fā)完一次數(shù)據(jù)都要一個復(fù)位電平,告訴 WS2812 我這兒發(fā)送完了。這幾個電平信息的初始化代碼:
void init_symbols() { // 0碼高電平 zeroSymbol.duration0 = 0.4 * (TICK_FREQ / 1000000); zeroSymbol.level0 = 1; // 0碼低電平 zeroSymbol.duration1 = 0.8 * (TICK_FREQ / 1000000); zeroSymbol.level1 = 0; // 1碼高電平 oneSymbol.duration0 = 0.8 * (TICK_FREQ / 1000000); oneSymbol.level0 = 1; // 1碼低電平 oneSymbol.duration1 = 0.4 * (TICK_FREQ / 1000000); oneSymbol.level1 = 0; // 復(fù)位信號全為低電平 resetSymbol.duration0 = 25 * (TICK_FREQ / 1000000); resetSymbol.level0 = 0; resetSymbol.duration1 = 25 * (TICK_FREQ / 1000000); resetSymbol.level1 = 0; }
0 碼這里設(shè)置的是 高電平持續(xù) 0.4 us,低電平持續(xù) 0.8 us;1 碼相反。這里0.3-0.4,0.7-0.8都可以,老周這里設(shè)置大一點的值,不容易抽風。如果設(shè)置0.3 和 0.7,在 ESP32 Pico 上有時候會抽風(有的燈珠不亮或顏色不對)。
這個時間算的是 tick 周期計數(shù),我們設(shè)的頻率是每周期 0.1 us,除以1000000 就是一微秒內(nèi)會 tick 多少次,這里就是 1 us tick 10 次,那么,0.4 us 就是 tick 0.4 * 10 = 4 次。就是這么算出來的。復(fù)位信號全是低電平,按數(shù)據(jù)手冊是最少 50us,這里把50分兩段,即電平1=25us,電平2=25us,電平值全為0。
那么,RGB 怎么轉(zhuǎn)為符號字呢?WS 2812c 中是 GRB 排列的,其他的芯片可以查資料,或者多次試驗來驗證順序。顏色值總共就 24 位,更簡潔的方法是用一個 32 位整數(shù)來表示一個顏色。發(fā)送時從高位開始處理,每處理一位,就向左移一位。直接看代碼。
void set_rgb(int index, uint32_t grb) { if (index < 0 || index > LED_NUM - 1) { return; // 索引無效 } // 循環(huán)的開始和結(jié)束索引 int startIdx = index * 24; int endIdx = startIdx + 24; for (int i = startIdx; i < endIdx; i++) { if (grb & 0x00800000) { // 1 rgbSymbols[i] = oneSymbol; } else { // 0 rgbSymbols[i] = zeroSymbol; } // 左移一位 grb <<= 1; } }
index 是某個燈珠的索引,每一次處理都跟 0x00800000 進行“與”運算,就是確定第 24 位(最高)位是否為1,若為1就用?oneSymbol 變量的值,若為0就用?zeroSymbol 變量的值,賦值一輪后,讓顏色值左移一位,就能實現(xiàn)從高位到低位發(fā)送了。
下面代碼初始化發(fā)送通道和編碼器。
void init_tx_channel() { rmt_tx_channel_config_t cfg = { // GPIO .gpio_num = GPIO_NUM, // 時鐘源:默認是APB .clk_src = RMT_CLK_SRC_DEFAULT, // 分辨率,即頻率 .resolution_hz = TICK_FREQ, // 內(nèi)存大小,指的是符號個數(shù),不是字節(jié)個數(shù) .mem_block_symbols = 64, // 傳輸隊列深度,不要設(shè)得太大 .trans_queue_depth = 4 // 禁用回環(huán)(自己發(fā)給自己) //.flags.io_loop_back=0 }; // 調(diào)用函數(shù)初始化 ESP_ERROR_CHECK(rmt_new_tx_channel(&cfg, &txChannel)); } void init_encoder() { // 目前配置不需要參數(shù) rmt_copy_encoder_config_t cfg = {}; // 創(chuàng)建拷貝編碼器 ESP_ERROR_CHECK(rmt_new_copy_encoder(&cfg, &rfEncoder)); }
調(diào)用 API 時,可以嵌套在?ESP_ERROR_CHECK 宏中,它會自動檢查調(diào)用是否成功,不成功就輸出錯誤。
下面代碼發(fā)送數(shù)據(jù)。
void send_data() { // 配置 rmt_transmit_config_t cfg = { // 不要循環(huán)發(fā)送 .loop_count = 0}; // 發(fā)送 ESP_ERROR_CHECK(rmt_transmit(txChannel, rfEncoder, rgbSymbols, sizeof(rgbSymbols), &cfg)); // 等待發(fā)送完畢 // ESP_ERROR_CHECK(rmt_tx_wait_all_done(txChannel, portMAX_DELAY)); // 發(fā)送復(fù)位信號 ESP_ERROR_CHECK(rmt_transmit(txChannel, rfEncoder, &resetSymbol, 1, &cfg)); // 等待完成 ESP_ERROR_CHECK(rmt_tx_wait_all_done(txChannel, portMAX_DELAY)); }
?
在 app_main 函數(shù)中,先顯示紅色,一秒后顯示藍色,再過一秒顯示綠色。
while (1) { // 紅色 for (i = 0; i < LED_NUM; i++) { set_rgb(i, COLOR_U32(0xff, 0x0, 0x0)); } send_data(); // 延時 vTaskDelay(1000 / portTICK_PERIOD_MS); // 藍色 for (i = 0; i < LED_NUM; i++) { set_rgb(i, COLOR_U32(0x0, 0x0, 0xff)); } send_data(); // 延時 vTaskDelay(1000 / portTICK_PERIOD_MS); // 綠色 for (i = 0; i < LED_NUM; i++) { set_rgb(i, COLOR_U32(0x0, 0xff, 0x0)); } send_data(); // 延時 vTaskDelay(1000 / portTICK_PERIOD_MS); }
vTaskDelay 是 RTOS 系統(tǒng)移植函數(shù),表示當前任務(wù)延時。注意這個延時函數(shù)的參數(shù)不是秒或毫秒,而是“跑多少圈” Tick。portTICK_PERIOD_MS 表示一毫秒 Tick 的步數(shù)。為什么是相除,不是相乘?這個,老周舉一個不太恰當?shù)睦樱杭偃缒闩芤蝗τ?2000 步,現(xiàn)在我要你跑 8000 步,問你要跑幾圈 ?答案就是 8000 / 2000 = 4 圈。就是這樣。
這些 RTOS 函數(shù)在包含頭文件時得小心,你得先包含?FreeRTOS.h,然后再包含其他頭文件,否則容易報錯。
下面是完整代碼:
#include <stdlib.h> #include <string.h> #include "driver/rmt_common.h" #include "driver/rmt_encoder.h" #include "driver/rmt_types.h" #include "driver/rmt_tx.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" // 聲明區(qū) #define GPIO_NUM 6 // 引腳號 #define TICK_FREQ 10 * 1000000 // 頻率 #define LED_NUM 24 // 燈珠數(shù)目 // #define DELAY_MS 20 // 延時 // 將RGB轉(zhuǎn)為GRB整數(shù) #define COLOR_U32(r, g, b) ( \ (uint32_t)g << 16 | \ (uint32_t)r << 8 | \ (uint32_t)b) // 變量區(qū) /* 發(fā)送通道 */ static rmt_channel_handle_t txChannel; /* 編碼器 */ static rmt_encoder_handle_t rfEncoder; /* 消息符號 */ static rmt_symbol_word_t zeroSymbol, oneSymbol, resetSymbol; /* 要傳輸?shù)念伾珨?shù)據(jù) */ static rmt_symbol_word_t rgbSymbols[24 * LED_NUM] = {0}; /************* 自定義函數(shù) ******************/ void init_tx_channel() { rmt_tx_channel_config_t cfg = { // GPIO .gpio_num = GPIO_NUM, // 時鐘源:默認是APB .clk_src = RMT_CLK_SRC_DEFAULT, // 分辨率,即頻率 .resolution_hz = TICK_FREQ, // 內(nèi)存大小,指的是符號個數(shù),不是字節(jié)個數(shù) .mem_block_symbols = 64, // 傳輸隊列深度,不要設(shè)得太大 .trans_queue_depth = 4 // 禁用回環(huán)(自己發(fā)給自己) //.flags.io_loop_back=0 }; // 調(diào)用函數(shù)初始化 ESP_ERROR_CHECK(rmt_new_tx_channel(&cfg, &txChannel)); } /* 初始化符號 */ void init_symbols() { // 0碼高電平 zeroSymbol.duration0 = 0.4 * (TICK_FREQ / 1000000); zeroSymbol.level0 = 1; // 0碼低電平 zeroSymbol.duration1 = 0.8 * (TICK_FREQ / 1000000); zeroSymbol.level1 = 0; // 1碼高電平 oneSymbol.duration0 = 0.8 * (TICK_FREQ / 1000000); oneSymbol.level0 = 1; // 1碼低電平 oneSymbol.duration1 = 0.4 * (TICK_FREQ / 1000000); oneSymbol.level1 = 0; // 復(fù)位信號全為低電平 resetSymbol.duration0 = 25 * (TICK_FREQ / 1000000); resetSymbol.level0 = 0; resetSymbol.duration1 = 25 * (TICK_FREQ / 1000000); resetSymbol.level1 = 0; } /* 初始化編碼器 */ void init_encoder() { // 目前配置不需要參數(shù) rmt_copy_encoder_config_t cfg = {}; // 創(chuàng)建拷貝編碼器 ESP_ERROR_CHECK(rmt_new_copy_encoder(&cfg, &rfEncoder)); } /* 設(shè)置顏色 */ void set_rgb(int index, uint32_t grb) { if (index < 0 || index > LED_NUM - 1) { return; // 索引無效 } // 循環(huán)的開始和結(jié)束索引 int startIdx = index * 24; int endIdx = startIdx + 24; for (int i = startIdx; i < endIdx; i++) { if (grb & 0x00800000) { // 1 rgbSymbols[i] = oneSymbol; } else { // 0 rgbSymbols[i] = zeroSymbol; } // 左移一位 grb <<= 1; } } /* 發(fā)送數(shù)據(jù) */ void send_data() { // 配置 rmt_transmit_config_t cfg = { // 不要循環(huán)發(fā)送 .loop_count = 0}; // 發(fā)送 ESP_ERROR_CHECK(rmt_transmit(txChannel, rfEncoder, rgbSymbols, sizeof(rgbSymbols), &cfg)); // 等待發(fā)送完畢 // ESP_ERROR_CHECK(rmt_tx_wait_all_done(txChannel, portMAX_DELAY)); // 發(fā)送復(fù)位信號 ESP_ERROR_CHECK(rmt_transmit(txChannel, rfEncoder, &resetSymbol, 1, &cfg)); // 等待完成 ESP_ERROR_CHECK(rmt_tx_wait_all_done(txChannel, portMAX_DELAY)); } void app_main(void) { // 1、初始化通道 init_tx_channel(); // 2、初始化符號 init_symbols(); // 3、初始化編碼器 init_encoder(); // 4、使能通道 ESP_ERROR_CHECK(rmt_enable(txChannel)); int i; /* 進入循環(huán) */ while (1) { // 紅色 for (i = 0; i < LED_NUM; i++) { set_rgb(i, COLOR_U32(0xff, 0x0, 0x0)); } send_data(); // 延時 vTaskDelay(1000 / portTICK_PERIOD_MS); // 藍色 for (i = 0; i < LED_NUM; i++) { set_rgb(i, COLOR_U32(0x0, 0x0, 0xff)); } send_data(); // 延時 vTaskDelay(1000 / portTICK_PERIOD_MS); // 綠色 for (i = 0; i < LED_NUM; i++) { set_rgb(i, COLOR_U32(0x0, 0xff, 0x0)); } send_data(); // 延時 vTaskDelay(1000 / portTICK_PERIOD_MS); } }
下面是效果:
?
補充一下漸變效果。原理是用RGB的終值減去初值,然后各自除以燈珠數(shù),得到一個平均遞變的值。在循環(huán)時,除第一個和最后一個燈珠外,其他燈珠的顏色都用初值 + 插值 * 索引,就是每個燈珠都依次遞增(減),最終趨近終值。
// 初始值 uint8_t r0 = 255, g0 = 0, b0 = 0; // 最終值 uint8_t r1 = 0, g1 = 0, b1 = 255; // 計算要插補的均值 uint8_t ri, gi, bi; ri = (r1 - r0) / LED_NUM; gi = (g1 - g0) / LED_NUM; bi = (b1 - b0) / LED_NUM; // 循環(huán)設(shè)置燈珠顏色 for (i = 0; i < LED_NUM; i++) { // 如果是第一個燈,直接用初值 if (i == 0) { uint32_t color = COLOR_U32(r0, g0, b0); set_rgb(i, color); continue; } // 如果是最后一個燈,直接用終值 if (i == LED_NUM - 1) { uint32_t c = COLOR_U32(r1, g1, b1); set_rgb(i, c); continue; } // 其他情況,用插值 uint32_t color = COLOR_U32( r0 + i * ri, g0 + i * gi, b0 + i * bi); set_rgb(i, color); } // 發(fā)送 send_data(); // 等待3秒 vTaskDelay(3000 / portTICK_PERIOD_MS); // 再來一次 r0 = 204; g0 = 0; b0 = 204; r1 = 0; g1 = 102; b1 = 0; // 計算插入均值 ri = (r1 - r0) / LED_NUM; gi = (g1 - g0) / LED_NUM; bi = (b1 - b0) / LED_NUM; for (i = 0; i < LED_NUM; i++) { if (i == 0) { uint32_t c = COLOR_U32(r0, g0, b0); set_rgb(i, c); continue; } if (i == LED_NUM - 1) { uint32_t c = COLOR_U32(r1, g1, b1); set_rgb(i, c); continue; } uint32_t c = COLOR_U32( r0 + i * ri, g0 + i * gi, b0 + i * bi); set_rgb(i, c); } // 發(fā)送 send_data(); // 等待3秒 vTaskDelay(3000 / portTICK_PERIOD_MS);
效果如下圖:
由于計算插值的時候沒有用浮點數(shù),漸變看起來不太絲滑。
?
好了,今天就水到這里了。文章來源地址http://www.zghlxwxcb.cn/news/detail-855124.html
到了這里,關(guān)于【ESP32 IDF】用RMT控制 WS2812 彩色燈帶的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!