第二十六章 INFRARED_RECEPTION實驗
1)實驗平台:正點原子DNESP32S3開發板
2)章節摘自【正點原子】ESP32-S3使用指南—IDF版 V1.6
3)購買鏈接:https://detail.tmall.com/item.htm?&id=768499342659
4)全套實驗源碼+手冊+視頻下載地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32S3.html
5)正點原子官方B站:https://space.bilibili.com/394620890
6)正點原子DNESP32S3開發板技術交流羣:132780729
本章,我們將介紹ESP32-S3對紅外遙控器的信號解碼。ESP32-S3板子上標配的紅外接收頭和一個小巧的紅外遙控器。我們將利用管腳輸入功能,解碼開發板標配的紅外遙控器的編碼信號,並將編碼後的鍵值在LCD屏中顯示出來。
本章分為如下幾個小節:
26.1紅外遙控簡介
26.2 硬件設計
26.3 程序設計
26.4 下載驗證
26.1 紅外遙控簡介
26.1.1 紅外遙控技術介紹
紅外遙控是一種無線、非接觸控制技術,具有抗干擾能力強,信息傳輸可靠,功耗低,成本低,易實現等顯著優點,被諸多電子設備特別是家用電器廣泛採用,並越來越多的應用到計算機系統中。 由於紅外線遙控不具有像無線電遙控那樣穿過障礙物去控制被控對象的能力,所以,在設計紅外線遙控器時,不必要像無線電遙控器那樣,每套(發射器和接收器)要有不同的遙控頻率或編碼(否則,就會隔牆控制或干擾鄰居的家用電器),所以同類產品的紅外線遙控器,可以有相同的遙控頻率或編碼,而不會出現遙控信號“串門”的情況。這對於大批量生產以及在家用電器上普及紅外線遙控提供了極大的方便。由於紅外線為不可見光,因此對環境影響很小,再由紅外光波動波長遠小於無線電波的波長,所以紅外線遙控不會影響其他家用電器,也不會影響臨近的無線電設備。
26.1.2 紅外器件特性
紅外遙控的情景中,必定會有一個紅外發射端和紅外接收端。在本實驗中,正點原子的紅外遙控器作為紅外發射端,紅外接收端就是板載的紅外接收器,實物圖可以查看26.2.3小節原理圖部分。要使兩者通信成功,收/發紅外波長與載波頻率需一致,在這裏波長就是940nm,載波頻率就是38kHz。
紅外發射管也是屬於二極管類,紅外發射電路通常使用三極管控制紅外發射器的導通或者截至,在導通的時候,紅外發射管會發射出紅外光,反之,就不會發射出紅外光。雖然我們用肉眼看不到紅外光,但是我們藉助手機攝像頭就能看到紅外光。但是紅外接收管的特性是當接收到紅外載波信號時,OUT引腳輸出低電平;假如沒有接收到紅外載波信號時,OUT引腳輸出高電平。
紅外載波信號其實就是由一個個紅外載波週期組成。在頻率為38KHz下,紅外載波週期約等於26.3us(1s / 38KHz ≈ 26.3us)。在一個紅外載波發射週期裏,發射紅外光時間8.77us和不發射紅外光17.53us,發射紅外光的佔空比一般為1/3。相對的,整個週期內不發射紅外光,就是載波不發射週期。在紅外遙控器內已經把載波和不載波信號處理好,我們需要做的就是識別遙控器按鍵發射出的信號,信號也是遵循某種協議的。
26.1.3 紅外編解碼協議介紹
紅外遙控的編碼方式目前廣泛使用的是:PWM(脈衝寬度調製)的NEC協議和PhilipsPPM(脈衝位置調製)的RC-5協議的。開發板配套的遙控器使用的是NEC協議,其特徵如下:
1,8 位地址和 8 位指令長度;
2,地址和命令 2 次傳輸(確保可靠性);
3,PWM 脈衝位置調製,以發射紅外載波的佔空比代表“0”和“1”;
4,載波頻率為 38Khz;
5,位時間為 1.125ms 或 2.25ms;
在NEC協議中,如何為協議中的數據‘0’或者‘1’?這裏分開紅外接收器和紅外發射器。
紅外發射器:發送協議數據‘0’ = 發射載波信號560us + 不發射載波信號560us
發送協議數據‘1’ = 發射載波信號560us + 不發射載波信號1680us
紅外發射器的位定義如下圖所示。
圖26.1.3.1 紅外發射器位定義圖
紅外接收器:接收到協議數據‘0’ = 560us低電平 + 560us高電平
接收到協議數據‘1’ = 560us低電平 + 1680us高電平
紅外接收器的位定義如下圖所示。
圖26.1.3.2 紅外接收器位定義圖
NEC遙控指令的數據格式為:同步碼頭、地址碼、地址反碼、控制碼、控制反碼。同步碼由一個9ms的低電平和一個4.5ms的高電平組成,地址碼、地址反碼、控制碼、控制反碼均是8位數據格式。按照低位在前,高位在後的順序發送。採用反碼是為了增加傳輸的可靠性(可用於校驗)。
我們遙控器的按鍵“ALIENTEK”按下時,從紅外接收頭端收到的波形如下圖所示。
圖26.1.3.3 按鍵“ALIENTEK”所對應的紅外波形
從上圖中可以看到,其地址碼為0,控制碼為21(正確解碼後00010101)。可以看到在100ms之後,我們還收到了幾個脈衝,這是NEC碼規定的連發碼(由9ms低電平+2.25ms高電平+0.56ms低電平+97.94ms高電平組成),如果在一幀數據發送完畢之後,按鍵仍然沒有放開,則發射重複碼,即連發碼可以通過統計連發碼的次數來標記按鍵按下的長短/次數。
26.1.4 ESP32-S3紅外遙控(RMT)介紹
RMT是一個紅外發送和接收控制器,可通過軟件加解密多種紅外協議。RMT模塊可以實現將模塊內置RAM中的脈衝編碼轉換為信號輸出,或將模塊的輸入信號轉換為脈衝編碼存入RAM中。此外,RMT模塊可以選擇是否對輸出信號進行載波調製,也可以選擇是否對輸入信號進行濾波和去噪處理。
RMT共有八個通道,編碼為0~7,各通道可獨立用於發送或接收信號:
(1)0~3通道專門用於發送信號;
(2)4~7通道專門用於接收信號。
每個發送通道和接收通道分別有一組功能相同的寄存器。另外,發送通道3和接收通道7對應的RAM支持DMA訪問,因此還有DMA相關的控制和狀態寄存器。
26.2 硬件設計
26.2.1 例程功能
在LCD上顯示一些實驗信息之後,即進入等待紅外觸發,如果接收到正確的紅外信號,則解碼,並在LCD上顯示鍵值和所代表的意義。LED閃爍用於提示程序正在運行。
26.2.2 硬件資源
- LED燈
LED-IO1
- USART0
U0TXD-IO43
U0RXD-IO44
- XL9555
IIC_SDA-IO41
IIC_SCL-IO42
- SPILCD
CS-IO21
SCK-IO12
SDA-IO11
DC-IO40(在P5端口,使用跳線帽將IO_SET和LCD_DC相連)
PWR- IO1_3(XL9555)
RST- IO1_2(XL9555)
- 紅外接收頭
REMOTE_IN-IO2
- 正點原子紅外遙控器
26.2.3 原理圖
紅外接收頭相關原理圖,如下圖所示。
圖26.2.3.1 紅外接收頭原理圖
需要注意:REMOTE_IN和SD卡片選共用了IO2,所以它們不可以同時使用。
開發板配套的紅外遙控器外觀如圖26.2.2所示:
圖26.2.3.2 紅外遙控器
開發板上接收紅外遙控器信號的紅外管外觀如圖26.2.3所示。使用時需要遙控器有紅外管的一端對準開發板上的紅外管才能正確收到信號。
圖26.2.3.3 開發板上的紅外接收管位置
26.3 程序設計
26.3.1 程序流程圖
程序流程圖能幫助我們更好的理解一個工程的功能和實現的過程,對學習和設計工程有很好的主導作用。下面看看本實驗的程序流程圖:
圖26.3.1.1 INFRARED_RECEPTION實驗程序流程圖
26.3.2 RMT函數解析
ESP-IDF提供了一套API來配置RMT。要使用此功能,需要導入必要的頭文件:
#include "driver/rmt_rx.h"
接下來,作者將介紹一些常用的ESP32-S3中的RMT函數,這些函數的描述及其作用如下:
1,安裝RMT接收通道
該函數用於安裝RMT接收通道,其函數原型如下所示:
esp_err_t rmt_new_rx_channel(constrmt_rx_channel_config_t *config,
rmt_channel_handle_t *ret_chan);
該函數的形參描述,如下表所示:
表26.3.2.1 函數rmt_new_rx_channel()形參描述
該函數的返回值描述,如下表所示:
表26.3.2.2 函數rmt_new_rx_channel()返回值描述
2,配置RMT接收通道的回調函數
該函數用於配置RMT接收通道的回調函數,其函數原型如下所示:
esp_err_t rmt_rx_register_event_callbacks(rmt_channel_handle_trx_channel, constrmt_rx_event_callbacks_t *cbs, void *user_data);
該函數的形參描述,如下表所示:
表26.3.2.3 函數rmt_rx_register_event_callbacks()形參描述
該函數的返回值描述,如下表所示:
表26.3.2.4 函數rmt_rx_register_event_callbacks()返回值描述
3,創建一個基於NEC協議的RMT編碼器
該函數用於創建一個基於NEC協議的RMT編碼器,其函數原型如下所示:
esp_err_t rmt_new_ir_nec_encoder(constir_nec_encoder_config_t *config,
rmt_encoder_handle_t *ret_encoder);
該函數的形參描述,如下表所示:
表26.3.2.5 函數rmt_new_ir_nec_encoder()形參描述
該函數的返回值描述,如下表所示:
表26.3.2.6 函數rmt_new_ir_nec_encoder()返回值描述
4,使能RMT接收通道
該函數用於使能RMT接收通道,其函數原型如下所示:
esp_err_t rmt_enable(rmt_channel_handle_t channel);
該函數的形參描述,如下表所示:
表26.3.2.7 函數rmt_enable()形參描述
該函數的返回值描述,如下表所示:
表26.3.2.8 函數rmt_enable()返回值描述
5,啓動RMT接收通道的接收任務
該函數用於啓動RMT接收通道的接收任務,其函數原型如下所示:
esp_err_t rmt_receive(rmt_channel_handle_t rx_channel,
void *buffer,
size_t buffer_size,
const rmt_receive_config_t *config);
該函數的形參描述,如下表所示:
表26.3.2.9 函數rmt_enable()形參描述
該函數的返回值描述,如下表所示:
表26.3.2.10 函數rmt_enable()返回值描述
26.3.3 RMT驅動解析
在IDF版的16_infrared_reception例程中,作者在16_infrared_reception\components\BSP路徑下新增了一個REMOTE文件夾,分別用於存放remote.c、remote.h和ir_nec_encoder.c以及ir_nec_encoder.h這四個文件。其中,remote.h文件負責聲明RMT相關的函數和變量,ir_nec_encoder.h存放用於IR NEC幀編碼為RMT符號的RMT編碼器的相關結構體成員,而remote.c和ir_nec_encoder.c文件則實現了RMT的驅動代碼。下面,我們將詳細解析這四個文件的實現內容。
1,remote.h文件
/* 引腳定義 */
#define REMOTE_IN_GPIO_PIN GPIO_NUM_2
#define REMOTE_RESOLUTION_HZ 1000000
#define REMOTE_NEC_DECODE_MARGIN 200
/* NEC協議時序時間 */
#define NEC_LEADING_CODE_DURATION_0 9000
#define NEC_LEADING_CODE_DURATION_1 4500
#define NEC_PAYLOAD_ZERO_DURATION_0 560
#define NEC_PAYLOAD_ZERO_DURATION_1 560
#define NEC_PAYLOAD_ONE_DURATION_0 560
#define NEC_PAYLOAD_ONE_DURATION_1 1690
#define NEC_REPEAT_CODE_DURATION_0 9000
#define NEC_REPEAT_CODE_DURATION_1 2250
/* 保存NEC解碼的地址和命令字節 */
static uint16_t s_nec_code_address;
static uint16_t s_nec_code_command;
2,remote.c文件
const static char *TAG = "REMOTE_RECEIVE TEST";
QueueHandle_t receive_queue;
rmt_channel_handle_t rx_channel;
rmt_symbol_word_t raw_symbols[64]; /*對於標準NEC框架應該足夠*/
rmt_receive_config_t receive_config;
rmt_rx_done_event_data_t rx_data;
/**
*@brief 初始化RMT
*@param無
*@retval 無
*/
void remote_init(void)
{
ESP_LOGI(TAG, "Create RMT RX channel");
rmt_rx_channel_config_t rx_channel_cfg;
/*選擇APB時鐘源作為默認選項*/
rx_channel_cfg.clk_src =RMT_CLK_SRC_DEFAULT;
/*通道時鐘分辨率*/
rx_channel_cfg.resolution_hz =REMOTE_RESOLUTION_HZ;
/*內存塊大小*/
rx_channel_cfg.mem_block_symbols = 64;
/*配置RMT接收引腳*/
rx_channel_cfg.gpio_num = REMOTE_IN_GPIO_PIN;
rx_channel = NULL;
/*創建一個RMT接收通道*/
ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_channel_cfg, &rx_channel));
ESP_LOGI(TAG,"register RXdone callback");
/*創建能包含一個RMTRX完成事件數據類型的隊列*/
receive_queue=xQueueCreate(1, sizeof(rmt_rx_done_event_data_t));
/*使用斷言*/
assert(receive_queue);
/*事件回調,當一個RMT通道接收事務完成時調用*/
rmt_rx_event_callbacks_t cbs = {
/*RMT數據接收完成回調函數*/
.on_recv_done =RMT_Rx_Done_Callback,
};
/*配置RMT接收通道回調*/
ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_channel,
&cbs,
receive_queue));
/*以下時間要求基於NEC協議*/
rmt_receive_config_t receive_config = {
/*NEC信號的最短持續時間為560us,1250ns<560us,有效信號不會被視為噪聲*/
.signal_range_min_ns= 1250,
/*NEC信號的最長持續時間為9000us,12000000ns>9000us,接收不會提前停止*/
.signal_range_max_ns= 12000000,
};
/*配置循環發送RMT配置*/
rmt_transmit_config_t transmit_config = {
/*不循環*/
.loop_count = 0,
};
ESP_LOGI(TAG,"installIRNECencoder");
ir_nec_encoder_config_t nec_encoder_cfg = {
/*編碼器分辨率*/
.resolution =REMOTE_RESOLUTION_HZ,
};
/*創建一個NEC協議的RMT編碼器*/
rmt_encoder_handle_tnec_encoder = NULL;
/*創建一個RMT編碼器*/
ESP_ERROR_CHECK(rmt_new_ir_nec_encoder(&nec_encoder_cfg, &nec_encoder));
ESP_LOGI(TAG, "開啓RMT發送和接收通道");
/*使能RMT接收通道*/
ESP_ERROR_CHECK(rmt_enable(rx_channel));
/*準備接收,初始化RMT接收通道任務*/
ESP_ERROR_CHECK(rmt_receive(rx_channel,
raw_symbols,
sizeof(raw_symbols),
&receive_config));
while(1)
{
/*以下時間要求基於NEC協議*/
if(xQueueReceive(receive_queue, &rx_data, pdMS_TO_TICKS(1000))==pdPASS)
{
/*解析接收符號並打印結果*/
example_parse_nec_frame(rx_data.received_symbols, rx_data.num_symbols);
/*重新開始接收*/
ESP_ERROR_CHECK(rmt_receive(rx_channel,
raw_symbols,
sizeof(raw_symbols),
&receive_config));
}
}
}
RMT的各項配置以及介紹在26.3.1小節中進行了講解,我們來看一下while循環裏的函數實現過程。首先,我們使用隊列的方式對RMT數據進行處理,從中接收項目的隊列句柄(我們定義為receive_queue),由於該項目是通過複製接收的,必須提供足夠大小的緩衝區,故而我們定義了指向緩衝區的指針rx_data,接收到的項將被複制到這個緩衝區之中,之後通過if語句判斷該項的值與pdPASS的值是否相等,如果隊列成功創建則添加到就緒隊列中。其次,根據NEC編碼解析紅外協議並打印指令結果,最後,再次開啓接收任務函數。
接下來,介紹一下紅外按鍵掃描函數remote_scan,代碼如下:
/**
* @brief 根據NEC編碼解析紅外協議並打印指令結果
* @param 無
* @retval 無
*/
voidexample_parse_nec_frame(rmt_symbol_word_t *rmt_nec_symbols, size_t symbol_num)
{
uint8_t rmt_data = 0;
uint8_t tbuf[40];
char *str = 0;
switch (symbol_num) /* 解碼RMT接收數據 */
{
case 34: /* 正常NEC數據幀 */
{
if (nec_parse_frame(rmt_nec_symbols) )
{
rmt_data = (s_nec_code_command>> 8);
switch (rmt_data)
{
case 0xBA:
{
str = "POWER";
break;
}
case 0xB9:
{
str = "UP";
break;
}
case 0xB8:
{
str = "ALIENTEK";
break;
}
case 0xBB:
{
str = "BACK";
break;
}
case 0xBF:
{
str = "PLAY/PAUSE";
break;
}
case 0xBC:
{
str = "FORWARD";
break;
}
case 0xF8:
{
str = "vol-";
break;
}
case 0xEA:
{
str = "DOWN";
break;
}
case 0xF6:
{
str = "VOL+";
break;
}
case 0xE9:
{
str = "1";
break;
}
case 0xE6:
{
str = "2";
break;
}
case 0xF2:
{
str = "3";
break;
}
case 0xF3:
{
str = "4";
break;
}
case 0xE7:
{
str = "5";
break;
}
case 0xA1:
{
str = "6";
break;
}
case 0xF7:
{
str = "7";
break;
}
case 0xE3:
{
str = "8";
break;
}
case 0xA5:
{
str = "9";
break;
}
case 0xBD:
{
str = "0";
break;
}
case 0xB5:
{
str = "DLETE";
break;
}
}
lcd_fill(86, 110, 176, 150, WHITE);
sprintf((char *)tbuf, "%d", rmt_data);
printf("KEYVAL= %d, Command=%04X\n",
rmt_data, s_nec_code_command);
lcd_show_string(86, 110, 200, 16, 16, (char *)tbuf, BLUE);
sprintf((char *)tbuf, "%s", str);
lcd_show_string(86, 130, 200, 16, 16, (char *)tbuf, BLUE);
}
break;
}
case 2: /* 重複NEC數據幀 */
{
if (nec_parse_frame_repeat(rmt_nec_symbols))
{
printf("KEYVAL= %d, Command = %04X, repeat\n",
rmt_data, s_nec_code_command);
}
break;
}
default: /* 未知NEC數據幀 */
{
printf("Unknown NECframe\r\n\r\n");
break;
}
}
}
該函數調用nec_parse_frame()函數將RMT結果解碼出NEC地址和命令,我們這裏只需要處理解碼出的命令即可,因為地址是不會變的。處理解碼出來的命令我們通過轉置運算,轉換成我們便於理解的十進制數值也就是我們代碼中的的“KEYVAL”,為了便於對比與學習,我們仍然保留解碼出來的十六進制數據。當我們使用正點原子配發的紅外遙控器,並按下遙控器上面的按鍵時,經過解碼後會在開發板上顯示出對應的鍵值與按鍵所表示的圖標。同樣的,我們也對重複的NEC數據幀以及未知的NEC數據幀進行識別與處理,重複的NEC數據幀會通過串口打印鍵值以及十六進制的數據,並添加上“repeat”的標識以作區分。而對於未知數據幀會通過串口打印“Unknown NEC frame”的字樣。
/**
* @brief 將RMT接收結果解碼出NEC地址和命令
* @param 無
* @retval 無
*/
bool nec_parse_frame(rmt_symbol_word_t *rmt_nec_symbols)
{
rmt_symbol_word_t *cur = rmt_nec_symbols;
uint16_t address = 0;
uint16_t command = 0;
boolvalid_leading_code = nec_check_in_range(cur->duration0,
NEC_LEADING_CODE_DURATION_0) &&
nec_check_in_range(cur->duration1,
NEC_LEADING_CODE_DURATION_1);
if (!valid_leading_code)
{
return false;
}
cur++;
for (int i = 0; i < 16; i++)
{
if (nec_parse_logic1(cur))
{
address|= 1 << i;
}
else if (nec_parse_logic0(cur))
{
address&= ~(1 << i);
}
else
{
return false;
}
cur++;
}
for (int i = 0; i < 16; i++)
{
if (nec_parse_logic1(cur))
{
command|= 1 << i;
}
else if (nec_parse_logic0(cur))
{
command&= ~(1 << i);
}
else
{
return false;
}
cur++;
}
/* 保存數據地址和命令,用於判斷重複按鍵 */
s_nec_code_address = address;
s_nec_code_command = command;
return true;
}
該函數將接收到的電平數組解碼成紅外編碼,也就是NEC地址以及命令。首先,通過布爾型函數對比數據時序長度是否為邏輯1或者邏輯0,從而獲取地址、地址反碼以及命令、命令反碼,並檢查獲取的數據是否正確,最後分別保存數據地址和命令到s_nec_code_address以及s_nec_code_command,用於判斷重複按鍵。
26.3.4 CMakeLists.txt文件
打開本實驗BSP下的CMakeLists.txt文件,其內容如下所示:
set(src_dirs
IIC
LCD
LED
REMOTE
SPI
XL9555)
set(include_dirs
IIC
LCD
LED
REMOTE
SPI
XL9555)
set(requires
driver)
idf_component_register(SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
上述的紅色REMOTE驅動需要由開發者自行添加,以確保RMT驅動能夠順利集成到構建系統中。這一步驟是必不可少的,它確保了RMT驅動的正確性和可用性,為後續的開發工作提供了堅實的基礎。
3,ir_nec_encoder.h文件
/**
*@brief IR NEC scan code representation
*/
typedef struct {
uint16_t address;
uint16_t command;
}ir_nec_scan_code_t;
/**
*@brief Type of IR NEC encoder configuration
*/
typedef struct {
uint32_t resolution; /*!< Encoder resolution, in Hz */
}ir_nec_encoder_config_t;
4,ir_nec_encoder.c文件
該文件是ESP32的一個紅外編解碼文件,區別於API函數,該文件是作為RMT編解碼過程中的一個輔助文件,由於代碼過長,不便在此處張貼,請讀者到相關的例程中結合資料進行學習。
26.3.5 實驗應用代碼
打開main/main.c文件,該文件定義了工程入口函數,名為app_main。該函數代碼如下。
i2c_obj_t i2c0_master;
/**
* @brief 程序入口
* @param 無
* @retval 無
*/
void app_main(void)
{
esp_err_t ret;
ret = nvs_flash_init(); /* 初始化NVS */
if (ret ==ESP_ERR_NVS_NO_FREE_PAGES ||
ret ==ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
led_init(); /* 初始化LED */
i2c0_master = iic_init(I2C_NUM_0); /* 初始化IIC0 */
spi2_init(); /* 初始化SPI2 */
xl9555_init(i2c0_master); /* 初始化XL9555 */
lcd_init(); /* 初始化LCD */
lcd_show_string(30, 50, 200, 16, 16, "ESP32", RED);
lcd_show_string(30, 70, 200, 16, 16, "REMOTETEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "KEYVAL:", RED);
lcd_show_string(30, 130, 200, 16, 16, "SYMBOL:", RED);
remote_init(); /* 初始化REMOTE */
}
main函數代碼比較簡單,主要是通過remote_scan函數獲得紅外遙控輸入的數據(控制碼),然後顯示在LCD上面。
26.4 下載驗證
下載代碼後,可以看到LCD顯示如下圖所示。
圖26.4.1 紅外接收實驗測試圖
此時,我們通過遙控器按下不同的按鍵,則可以看到LCD上顯示了不同按鍵的鍵值和對應遙控器上的符號,如下圖所示。
圖26.4.2 紅外接收實驗測試圖