【ESP32 IDF】用RMT控制 WS2812 彩色燈帶

来源:https://www.cnblogs.com/tcjiaan/p/18147955
-Advertisement-
Play Games

在上一篇中,老周用 .NET Nano Framework 給大伙伴們演示了 WS2812 燈帶的控制,包括用 SPI 和 紅外RMT 的方式。利用 RMT 是一個很機靈的方案,不過,可能很多大伙伴對 ESP32 的 RMT 不是很熟悉。除了樂鑫自己的文檔,沒幾個人寫過相關的水文,可見這裡頭空白的水 ...


在上一篇中,老周用 .NET Nano Framework 給大伙伴們演示了 WS2812 燈帶的控制,包括用 SPI紅外RMT 的方式。利用 RMT 是一個很機靈的方案,不過,可能很多大伙伴對 ESP32 的 RMT 不是很熟悉。除了樂鑫自己的文檔,沒幾個人寫過相關的水文,可見這裡頭空白的水市場很充足,老周一時手癢,就決定再水一篇博文。

不管你有沒有做過物聯網項目,只要你有關註,你就會發現,當今時尚流行忽悠不擦嘴巴。許多教程就拿個 MicroPython 或者 Arduino,貼幾行代碼,然後叫你燒錄進去看效果。可是,效果看完了,你知道了啥?你學到了啥?你知道這裡頭做了啥?全 TM 不知。做教程的人只管忽悠,然後就沒下文了。這就是它們老喜歡用 Python 的原因。基於腳本語言的特性,很多庫都是高度封裝的,拿來直接敲幾行代碼就完事了。寫教程的是這樣,做培訓的也是這樣。

用 Arduino 好不好?好,開櫃即用;用 MicroPython 好不好?好,開桶即用。這就是現在為什麼 Py 流行的原因,做培訓的演示起來多起勁,幾行代碼(估計他們為這幾行代碼都練了無數次,都背下來了)天天敲,而且這麼簡單的代碼,現場演示也不怕出錯,然後告訴你:看看,做 AI,做 Iot 多簡單!但是,老周是很 BS 這些人的,只告訴你吃魚很香,卻不告訴你怎麼捕的魚。Python 不是不能用,而是你不能指望憑它來學編程。腳本語言本來就是做輔助用的。

如果你一開始用的是 C 語言,就算你沒在做項目,你反而可以堅持玩幾年,甚至十幾年。哪怕業餘玩玩,也能一層一層地挖掘出很多有趣的東西。

還有一種更離譜的觀點:Py 適合科研人員,可以快速驗證結果。C語言留給開發的苦逼去乾。老周認為:做科研的人在底層和基礎知識方面更應該比開發的人強,不然你研究個鴕鳥蛋!連基本的原理和細節都搞不清楚,那就是紙上談兵,洗錢罷了。就像現在某些建築,某些服裝,為什麼會出現許多反人類設計;很多產品也是反人類設計?正是因為做設計的人對生產、對技術、對基礎原理不瞭解,閉上眼睛無腦瞎編亂塗。有些設計人員對自己、對產品、對他人也是不負責的,自己設計的東西做出來,也不去試用一下,看看你設想的東西多麼不靠譜。

所以,老周寫的東西,一直以來都是立足於實際使用的,而不是立足炒作和無腦吹。吹得天花亂墜,如果用起來很難用的東西,老周是不會推薦的。

好了,不小心扯了一堆沒用的了。有大伙伴可能說老周這麼批別人不會得罪人嗎?得不得罪有啥關係呢,老周跟他們又不是一伙的,沒有利益關係,他們敢拿導彈轟老周嗎?

OK,不扯蛋了。說回 RMT,ESP32 中,一個周期 RMT 消息共 32 位,分兩段,每段16位。然後老周給你畫個圖。

別以為 32 個位這麼多能描述一整條消息,不是的,它只描述了一個脈衝周期罷了。你看,這個脈衝是不是被分成了兩段?為什麼要分成兩段?因為這樣就能說清楚了:高電平占了多長時間,低電平占了多長時間。也就是說,這一幀的數據包含了兩個電平的參數。

1-16 位是第一個電平,前15位表示該電平持續的時間,最後一位(圖中的 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,就表明這個周期沒有高電平。於是,你能想到,如果一個周期內全是高電平呢,是不是這樣?

0000 1111 0101 0011
0000 1000 0110 0111

至於電平的時間長度是單位,這個要看定時器的頻率的。還記得嗎?上一篇水文中,老周說預設用的是 APB 時鐘,80 MHz,假設我們分頻後讓定時器的頻率變成 1 MHz,即 1 000 000 Hz,然後 1s / 1000000 = 0.000001 秒,即 1 微秒(us)/ Tick。那麼,這個15位的整數就和微秒數一致。

現在,你明白了 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;

咦,這個類型咋這麼怪啊?不怪,這種貨叫做內聯,說人話就是:裡面的結構體和 val 的值共用記憶體。

前面的 struct 有四個欄位:

duration0:第一個電平的時長,後面的冒號和15表示它占 15 位;

level0:表示第一個電平值,占一位;

duration1:第二個電平的時長,占 15 位;

level1:第二個電平的值,占一位。

那麼,我問你,這四個欄位加起來多位,是不是 32 ?val 的類型是 uint32 ,無符號32位整數。前面的結構體和 val 是不是大小相同?都是4個位元組?是吧,於是,它們用同一塊記憶體,也就是說,這個 rmt_symbol_word_t 你可以用四個欄位去設置它,也可以直接用一個整數去設置。C 語言是直接操作記憶體的,可以強制轉換,在後面調用相關函數時,可以取地址直接賦值給 void* (指針)。

請你記住這個類型,你可以字面翻譯為”符號字“,或者叫 RMT 描述符號。記好了,一個符號字只描述一個周期的脈衝哦。要是向 WS2812 發數據,RGB共 24 位,一個燈珠你就要發 24 個 符號字,點亮兩個燈就發 48 個符號字。我要點100個燈呢,那就 24*100 唄。你不妨理解為:一個符號字就是代表一個二進位位。有幾個二進位位就得發送幾個符號。

這裡要說明一點:.NET Nano Framework 用的 IDF 是 4xx 的,而目前新的版本是 5xx 的,新舊版本之間在 RMT 操作上有很大區別,函數也不同。不過,原理差不多,說直白一點就是:把記憶體中的 rmt_symbol_word_t 隊列發送出去。

由於版本更新,.NET Nano Framework 後面肯定要適配新版 IDF 的,所以,老周決定用新的版本的方式演示。在新版本 API 中,不需要分頻設定了。其實直接設置頻率更好,尤其是對初學者,總覺得分頻很難懂。不過老周可以把分頻總結為:把匯流排/或CPU/或其他振蕩源的頻率除以某個數,得到更低的頻率。即原來的頻率太高了,要降一降。比較,原頻 120 MHz,分頻繫數為 4,那就調整為 120/4 = 30 MHz。樹莓派(Raspberry Pi Pico)Pico 的官方SDK中,PWM的頻率也是用到了分頻。不過小草莓先分頻,再計數。先把頻率降一下,然後周期性地數 256(0-255),如果計數滿 255 重新回到 0,再計數。所以,RPI Pico 的 PWM 頻率其實算起來挺麻煩,要考慮分頻,還要考慮計數次數。

ESP 32 新的 IDF 直接讓你配置頻率了,這樣更方便更直觀。

下麵老周說說 RMT API 怎麼用。不要聽別人造謠,說 IDF 很難用,其實不難用的。畢竟是官方的,功能很全,官方團隊直接維護。老周安裝 IDF 就沒失敗過,這裡再次強調用兩點,保證你能成功安裝:1、裝好 Python 後,pip 改國內源;2、在 VS Code 的 Esp 插件中下載 IDF時,選樂鑫的伺服器,不要選 github。

然後,其他選項你隨意。其實它無非就用到兩個目錄,一個放 IDF 的源碼,一個放編譯的 tools。然後會設置環境變數 IDF_PATH 等。

下麵請記住一個萬能規律,不管你用的什麼開發板,什麼晶元,什麼平臺,所有外部設備的通信都是這樣的流程:

1、配置參數;

2、init(初始化);

3、載入驅動(一般在 init 時就完成,這一步許多平臺可省略);

4、讀/寫數據;

5、清理資源。

一、配置階段

RMT API 定義專門的結構體,用於配置參數。

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;

這是配置發送的,如果接收數據,要用 rmt_rx_channel_config_t,用起來一樣,搞懂一個,另一個就懂了。註意,接收和發送的函數是分佈在兩個頭文件中的,發送是 rmt_tx.h,接收是 rmt_rx.h。因為驅動 WS2812 是輸出,屬於發送模式,咱們只用 rmt_tx_channel_config_t 結構體。

不要看它那麼多成員,其實,在實際使用時,咱們不需要全都用,不用的保持預設(不賦值就是了)。

gpio_num:用來發信號的引腳,GPIO 號。這個可用枚舉值(在 gpio_num.h 頭文件中),如 GPIO_NUM_0 表示 GPIO0,GPIO_NUM_33 表示 GPIO33,也可以直接用整數,如 33、25、8 等。

clk_src:振動的時鐘源,可以用 RMT_CLK_SRC_DEFAULT 表示預設值,即用 APB 時鐘,80兆那個。一般不用選其他,畢竟不是每個板子都通用,預設是比較通用。

resolution_hz:這個就是直接設置頻率了,不用思考分頻的事了。

mem_block_symbols:分配記憶體量,常用 64。註意它的大小不是位元組,而是 符號字(rmt_symbol_word_t),就是最開始咱們介紹那個,32位兩個階段那個,描述兩個電平時長的。比如,設置64就是分配的記憶體可以放 64 個符號字,位元組是 64 * 4,32位嘛,是吧,前面反覆說了。

trans_queue_depth:隊列深度,一般不要太大,4 或 8 均可。數據在傳輸時,不是馬上就發出去的,而是放進一個隊列中,然後驅動層會調度這個隊列,慢慢發(其實很快發完)。設置為4表示隊列中可以放(掛起)4條等待傳輸的符號字。

intr_priority:中斷的優先值,非特殊情況保持預設。

另外,此結構體內嵌了一個 flags 結構體。

invert_out:是否電平反向,1表示開啟。就是反轉電平,比如,本來高的變低,低的變高。這個一般不用;

with_dma:是否走 DAM 通道,不占用CPU運算資源;

io_loop_back:就跟在電腦上 ping 127.0.0.1 一樣,“我發給我自己”,即自發自收(在同一引腳上)。這個一般沒啥用。

io_od_mode:是否設置為開漏模式。

二、初始化階段

配置完相關參數後,調用 rmt_new_tx_channel 函數,用已配置的參數創建通信通道。

esp_err_t rmt_new_tx_channel(const rmt_tx_channel_config_t *config, rmt_channel_handle_t *ret_chan);

config 引用配置結構體實例,ret_chan 接收創建的通道句柄,後面在發送數據時要用。所以,在調用此函數前,先聲明一個 rmt_channel_handle_t 類型的變數,最後是全局的。

新版 API 雖然精簡了許多,但也有缺點:在配置好參數創建通道後,就不能再修改參數了,除非重新初始化。而舊版 API 是可以修改的。

三、啟用通道

調用 rmt_enable 函數啟用通道。

esp_err_t rmt_enable(rmt_channel_handle_t channel);

channel 就是剛剛創建的通道。這一步很關鍵,也很容易遺忘。不啟用通道的話,是無法接收和發送數據的。如果忘了,你測試來測試去,死活不能工作,你甚至會懷疑自己寫錯了協議。如果要禁用通道,可以調用 rmt_disable 函數。

esp_err_t rmt_disable(rmt_channel_handle_t channel);

這兩個函數都聲明在 rmt_common.h 頭文件中。

四、創建編碼器

創建編碼器可以在啟用通道之前完成,第【三】、【四】階段順序不重要。IDF 內置兩個編碼器:

1、bytes encoder:就是把你給它的位元組數組轉換為符號字(前面說過的 rmt_symbol_word);

2、copy encoder:這玩意兒很玄,如果你看官方文檔介紹可能會懷疑人生,不知道說啥。老周用一句話概括:這貨就是不處理不轉換,你直接把符號字傳給它,然後它複製到驅動層的記憶體中,放入隊列準備發送。數據只是被覆制,不會修改。這是防止讓驅動空間的代碼跨空間引用用戶代碼,那樣有記憶體泄漏的風險,複製數據就不存在跨空間長距離引用,發完就清理。用戶代碼可能長期保持數據的生命周期。

當然,你可以寫自己的編碼器(組合使用內置的編碼器)。若要自定義,請認識一下 rmt_encoder_t 結構體。

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);

    /* 重置編碼器參數時用 */
    esp_err_t (*reset)(rmt_encoder_t *encoder);

    /* 清理編碼器時用 */
    esp_err_t (*del)(rmt_encoder_t *encoder);
};

這個結構體的成員都是函數指針,你讓它們分別指向你定義的函數,就實現了自定義編碼了。這個東西你可能看得很繞,為什麼函數的輸入參數還要 rmt_encoder_t ?這是因為 C 結構體不能繼承,要想實現類開繼承的功能,就得定義一個更大的結構體,然後大結構體中引用 rmt_encoder_t,模擬調用基類成員。由於 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;

就是定義一個結構體,然後裡面有個 base,base 就是 rmt_encoder_t 類型,這就等於從抽象基類派生出 rmt_bytes_encoder和rmt_copy_encoder類型,其他成員則用於參數配置。訪問 encode、reset、del 函數指針時就通過 S.base.encode(....) 來調用。當然,你自己寫的話不一定要搞那麼複雜,就是按 rmt_encoder_t 結構的三個函數指針成員,引其引用你寫的函數就行了。

初始化 bytes encoder 使用 rmt_new_bytes_encoder 函數,初始化 copy encoder 使用 rmt_new_copy_encoder 函數。調用函數前,先聲明 rmt_encoder_handle_t 類型的變數,該變數會引用創建的編碼器,由函數的 ret_encoder 參數賦值。

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);

創建編碼器後用變數保存引用,不需要我們手動調用,傳輸數據時會自動調用。

五、發送數據

發送數據調用 rmt_transmit 函數,參數包括:剛創建的通道、編碼器,以及要發送的符號字數組(多個符號字一同推入隊列,不必一個一個推)。調用此函數只是把消息放進傳輸隊列,至於是否立即發送,那看隊列裡面擁不擁擠了,由驅動層自行處理,我們不用管。

如果你不使用中斷,但希望等到數據發出去了再執行後面的程式代碼,那可以調用 rmt_tx_wait_all_done 函數,它會等待指定的時間,直到數據發送出去才返回。等待時間可以用最大值—— portMAX_DELAY。

六、清理

如果你的程式不是一直發數據,或只是特定時候發送。那傳輸完數據後應當清理相應的對象。

rmt_del_encoder:清除剛創建的編碼器。

rmt_disable:禁用通道。

rmt_del_channel:清除通道。

如果程式一直發數據,可以不清理。

 

官方有一個示例是用 RMT 驅動燈帶的,但那個用了混合編碼器,弄得有點複雜,老周這裡直接用 copy encoder 複製符號字。符號字咱們自己生成。

先做好初始化工作。

1、聲明相關參數。

// 聲明區
#define GPIO_NUM 6             // 引腳號
#define TICK_FREQ 10 * 1000000 // 頻率
#define LED_NUM 24             // 燈珠數目

這裡我把頻率設置為 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;
/* 要傳輸的顏色數據 */
static rmt_symbol_word_t rgbSymbols[24 * LED_NUM] = {0};

註意符號字數組,大小是燈珠數 * 24。為什麼24呢?因為 RGB 數據加起來24位,一個符號字只能描述一個位。

zeroSymbol 表示發送 0 時的電平,表示發送 1 時的電平,resetSymbol 是複位電平,每發完一次數據都要一個複位電平,告訴 WS2812 我這兒發送完了。這幾個電平信息的初始化代碼:

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;

    // 複位信號全為低電平
    resetSymbol.duration0 = 25 * (TICK_FREQ / 1000000);
    resetSymbol.level0 = 0;
    resetSymbol.duration1 = 25 * (TICK_FREQ / 1000000);
    resetSymbol.level1 = 0;
}

0 碼這裡設置的是 高電平持續 0.4 us,低電平持續 0.8 us;1 碼相反。這裡0.3-0.4,0.7-0.8都可以,老周這裡設置大一點的值,不容易抽風。如果設置0.3 和 0.7,在 ESP32 Pico 上有時候會抽風(有的燈珠不亮或顏色不對)。

這個時間算的是 tick 周期計數,我們設的頻率是每周期 0.1 us,除以1000000 就是一微秒內會 tick 多少次,這裡就是 1 us tick 10 次,那麼,0.4 us 就是 tick 0.4 * 10 = 4 次。就是這麼算出來的。複位信號全是低電平,按數據手冊是最少 50us,這裡把50分兩段,即電平1=25us,電平2=25us,電平值全為0。

那麼,RGB 怎麼轉為符號字呢?WS 2812c 中是 GRB 排列的,其他的晶元可以查資料,或者多次試驗來驗證順序。顏色值總共就 24 位,更簡潔的方法是用一個 32 位整數來表示一個顏色。發送時從高位開始處理,每處理一位,就向左移一位。直接看代碼。

void set_rgb(int index, uint32_t grb)
{
    if (index < 0 || index > LED_NUM - 1)
    {
        return; // 索引無效
    }
    // 迴圈的開始和結束索引
    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 變數的值,賦值一輪後,讓顏色值左移一位,就能實現從高位到低位發送了。

下麵代碼初始化發送通道和編碼器。

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,
        // 記憶體大小,指的是符號個數,不是位元組個數
        .mem_block_symbols = 64,
        // 傳輸隊列深度,不要設得太大
        .trans_queue_depth = 4
        // 禁用迴環(自己發給自己)
        //.flags.io_loop_back=0
    };
    // 調用函數初始化
    ESP_ERROR_CHECK(rmt_new_tx_channel(&cfg, &txChannel));
}

void init_encoder()
{
    // 目前配置不需要參數
    rmt_copy_encoder_config_t cfg = {};
    // 創建拷貝編碼器
    ESP_ERROR_CHECK(rmt_new_copy_encoder(&cfg, &rfEncoder));
}

調用 API 時,可以嵌套在 ESP_ERROR_CHECK 巨集中,它會自動檢查調用是否成功,不成功就輸出錯誤。

下麵代碼發送數據。

void send_data()
{
    // 配置
    rmt_transmit_config_t cfg = {
        // 不要迴圈發送
        .loop_count = 0};
    // 發送
    ESP_ERROR_CHECK(rmt_transmit(txChannel, rfEncoder, rgbSymbols, sizeof(rgbSymbols), &cfg));
    // 等待發送完畢
    // ESP_ERROR_CHECK(rmt_tx_wait_all_done(txChannel, portMAX_DELAY));
    // 發送複位信號
    ESP_ERROR_CHECK(rmt_transmit(txChannel, rfEncoder, &resetSymbol, 1, &cfg));
    // 等待完成
    ESP_ERROR_CHECK(rmt_tx_wait_all_done(txChannel, portMAX_DELAY));
}

 

在 app_main 函數中,先顯示紅色,一秒後顯示藍色,再過一秒顯示綠色。

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 系統移植函數,表示當前任務延時。註意這個延時函數的參數不是秒或毫秒,而是“跑多少圈” Tick。portTICK_PERIOD_MS 表示一毫秒 Tick 的步數。為什麼是相除,不是相乘?這個,老周舉一個不太恰當的例子:假如你跑一圈有 2000 步,現在我要你跑 8000 步,問你要跑幾圈 ?答案就是 8000 / 2000 = 4 圈。就是這樣。

這些 RTOS 函數在包含頭文件時得小心,你得先包含 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"

// 聲明區
#define GPIO_NUM 6             // 引腳號
#define TICK_FREQ 10 * 1000000 // 頻率
#define LED_NUM 24             // 燈珠數目
// #define DELAY_MS 20            // 延時

// 將RGB轉為GRB整數
#define COLOR_U32(r, g, b) ( \
    (uint32_t)g << 16 |      \
    (uint32_t)r << 8 |       \
    (uint32_t)b)

// 變數區
/* 發送通道 */
static rmt_channel_handle_t txChannel;
/* 編碼器 */
static rmt_encoder_handle_t rfEncoder;
/* 消息符號 */
static rmt_symbol_word_t zeroSymbol, oneSymbol, resetSymbol;
/* 要傳輸的顏色數據 */
static rmt_symbol_word_t rgbSymbols[24 * LED_NUM] = {0};

/************* 自定義函數 ******************/
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,
        // 記憶體大小,指的是符號個數,不是位元組個數
        .mem_block_symbols = 64,
        // 傳輸隊列深度,不要設得太大
        .trans_queue_depth = 4
        // 禁用迴環(自己發給自己)
        //.flags.io_loop_back=0
    };
    // 調用函數初始化
    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;

    // 複位信號全為低電平
    resetSymbol.duration0 = 25 * (TICK_FREQ / 1000000);
    resetSymbol.level0 = 0;
    resetSymbol.duration1 = 25 * (TICK_FREQ / 1000000);
    resetSymbol.level1 = 0;
}

/* 初始化編碼器 */
void init_encoder()
{
    // 目前配置不需要參數
    rmt_copy_encoder_config_t cfg = {};
    // 創建拷貝編碼器
    ESP_ERROR_CHECK(rmt_new_copy_encoder(&cfg, &rfEncoder));
}

/* 設置顏色 */
void set_rgb(int index, uint32_t grb)
{
    if (index < 0 || index > LED_NUM - 1)
    {
        return; // 索引無效
    }
    // 迴圈的開始和結束索引
    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;
    }
}

/* 發送數據 */
void send_data()
{
    // 配置
    rmt_transmit_config_t cfg = {
        // 不要迴圈發送
        .loop_count = 0};
    // 發送
    ESP_ERROR_CHECK(rmt_transmit(txChannel, rfEncoder, rgbSymbols, sizeof(rgbSymbols), &cfg));
    // 等待發送完畢
    // ESP_ERROR_CHECK(rmt_tx_wait_all_done(txChannel, portMAX_DELAY));
    // 發送複位信號
    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;
    /* 進入迴圈 */
    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的終值減去初值,然後各自除以燈珠數,得到一個平均遞變的值。在迴圈時,除第一個和最後一個燈珠外,其他燈珠的顏色都用初值 + 插值 * 索引,就是每個燈珠都依次遞增(減),最終趨近終值。

// 初始值
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;
// 迴圈設置燈珠顏色
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);
}
// 發送
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	   

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 題目:對順序表中的元素進行增加和刪除以及訪問 使用數組實現線性表的特性,需要知道三個條件:數組元素的容量、數組有效的最後一個元素的下標 /************************************************************************* * file ...
  • 順序表 題目一: 題目分析: 該題目需要先對順序表進行遍歷至元素x正確插入位置,再對順序表完成插入操作。因此涉及到for迴圈與if語句的使用 代碼實現 /******************************************************************** * * ...
  • 1:在離線的環境中導入鏡像 在無法訪問外網的情況下,通過將docker鏡像導出為一個包,然後導入到另外的一臺電腦上面,從而實現了不用訪問外網就能拉取鏡像了 #將鏡像輸出到這個tar包 [root@cleint ~]# docker save -o centos.tar centos #通過第三方的 ...
  • 2.1.1 命令提示符 在CentOS 7操作系統中,Linux命令提示符就像是你與電腦交流的一個小標誌,告訴你系統已經準備好接受你的指令了。 它通常會顯示在你打開的終端視窗或控制台的最前面。 讓我們來看一個示例: [root@node01 ~]$ 在這個示例中: root:是當前登錄的用戶名。 @ ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是在i.MXRT1xxx系列上用NAND型啟動設備時可用兩級設計縮短啟動時間。 去年痞子衡寫過一篇騷操作文章 《藉助i.MXRT10xx系列INIT_VTOR功能可以縮短程式熱重啟時間》,這對於 NAND 型啟動設備上程式熱重啟時間的縮短 ...
  • 聲明: 以下內容為個人筆記,內容不完全正確,請謹慎參考。 正則表達式 含義: 正則表達式使用的每個單獨字元串來描述、匹配一些列符合語法規則的字元串。在很多文本編輯器里,正則表達式通常被用來檢索、替換那些符合某個模式的文本。在Linux中,grep,sed,awk等文本處理工具都支持正則表達式進行模式 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...