对万能遥控有兴趣的同学可以参考:esp32实现万能红外遥控器 基于开源红外码库IREXT,推荐看完本文再去看万能遥控,实现效果:
esp32实现语音万能遥控
esp32系列芯片集成了红外发送与接收控制器,可用于多种类型的红外通信。esp32一共有8个通道,每个通道都可以独立的进行发射和接收,一个通道不能同时进行发射和接收。
发射红外时,从内部特定地址的RAM中读取红外编码数据,并对这些数据进行38khz的载波调制;接收时,将接收的电平和电平时间经过转换后存放于该RAM。
每个通道独立配置时钟,时钟源有两个,APB = 80MHZ,REF_TICK 。官方建议使用APB时钟源。
时钟可由8位的DIVIDER
进行预分频,将分频后的信号作为发射/接收计数器的时钟源
。接下来的例程中我将分频系数设置为100,计数器的时钟频率为tick = 0.8MHZ,也就是1.25us。APB时钟还作为载波的时钟源,用于发射红外时需要用到的载波信号,如38Khz的典型红外载波。
item是esp32红外中用于描述一个脉冲信号的概念,一个item包含:电平以及电平时间信息
存放红外数据的RAM分为8个block,默认一个通道对应一个block,一个block的大小是64×32bit,一个32bit的RAM就是一个item,所以一个block最多存放64个item,也就是64个脉冲信号。
item在内存中的情况如下图,level记录电平的高低,period记录电平的时间(period内存放的是计数器计数的数值,还需要经过换算才能得到真实的时间)。所以用一个item可以表示一个周期的信号。如果将多个item连起来,就是一帧的数据。
计算公式:真实时间=period*divider/80M
;
在rmt_struct.h
中定义的结构体rmt_item32_t
,就是指向以上说的item。
typedef struct {
union {
struct {
uint32_t duration0 :15;
uint32_t level0 :1;
uint32_t duration1 :15;
uint32_t level1 :1;
};
uint32_t val;
};
} rmt_item32_t;
1,发射器从内部的RAM中读取数据。每次读取32位数据,高位地址先发射,低位地址后发射。
2,当接收器使能时,输入gpio检查到电平变化时,开始计数,当又一次检查到电平变化时,将上次电平的高低和持续时间写入RAM中,以此持续检测,直到发射器接收的电平变化时间大于设置的退出检测时间
,接收器才会停止接收。
3,通过设置RMT_IDLE_THRES_CH寄存器设置退出检测时间
,该寄存器的计数频率与接收计数器频率相同,tick = 100/80M=1.25us;
4,接收器还支持简单的滤波,滤波器可以帮助我们滤除一些持续时间过短的信号,比如一个几us的噪声,通过设置RMT_RX_FILTER_THRES_CH寄存器来设置滤除的噪声信号的时间宽度。注意RMT_RX_FILTER_THRES_CH寄存器的时钟源是APB时钟。所以时间宽度:fliter = RMT_RX_FILTER_THRES_CH*0.0125us;
在1.3节的时钟中,信号输出口sig_out连接到下图三极管的基极,从而控制三极管的导通,间接控制红外发光管的亮灭,当输出38khz的载波时,LED就会以38khz的频率闪烁。
一般的接收管是一个集成的元器件,具备光信号到电信号的转换,信号放大,解码等功能。当二极管接收到红外线时导通,光信号转为电信号,再经过放大电路,最后解码,解码的作用是滤除非38khz的信号,当接收的信号是38khz时,OUT脚输出低电平。
初始化外设,填充item的数据。然后调用以下函数将item中的内容写入RAM,发射。这个过程中填充item数据是灵活多样的,根据你要发射的数据不同,有不同的填充方法。本文就先介绍使用格力红外编码去填充,参考:
该函数将变量item中的数据写入特定的RAM中。
esp_err_t rmt_write_items(rmt_channel_t channel, const rmt_item32_t* rmt_item, int item_num, bool wait_tx_done);
该函数使能发射控制器,将对应通道的RAM中的数据发射出去,该操作会使任务进入阻塞。
esp_err_t rmt_wait_tx_done(rmt_channel_t channel, TickType_t wait_time);
/*
* @brief RMT transmitter initialization
*/
static void nec_tx_init()
{
rmt_config_t rmt_tx;
rmt_tx.channel = tx_channel;
rmt_tx.gpio_num = RMT_TX_GPIO_NUM;
rmt_tx.mem_block_num = 2; //由于格力红外有70个item,使用2个块,64×2=128个item
rmt_tx.clk_div = RMT_CLK_DIV;
rmt_tx.tx_config.loop_en = false; //关闭循环发射,只发射一次
rmt_tx.tx_config.carrier_duty_percent = 50; //载波占空比为50
rmt_tx.tx_config.carrier_freq_hz = 38000; //载波频率38khz 红外
rmt_tx.tx_config.carrier_level = 1; //载波高电平
rmt_tx.tx_config.carrier_en = RMT_TX_CARRIER_EN; //使能载波
rmt_tx.tx_config.idle_level = 0; //空闲状态低电平
rmt_tx.tx_config.idle_output_en = true; //输出使能
rmt_tx.rmt_mode = RMT_MODE_TX; //发射模式
ESP_LOGI(TAG, "[ 1.1 ] config rmt");
rmt_config(&rmt_tx);
ESP_LOGI(TAG, "[ 1.2 ] install rmt driver");
//安装红外发射通道 无需ringbuff
rmt_driver_install(rmt_tx.channel, 0, 0);
}
rmt_item32_t *item; //发射item
size_t size = 0;
int item_num = 0;
item_num = NEC_DATA_ITEM_NUM; //此处为70
size = (sizeof(rmt_item32_t) * item_num); //计算70个item所需的字节空间
item = (rmt_item32_t *)malloc(size); //记得free
memset((void *)item, 0, size);
nec_build_items(item, item_num, ir_msg->data0, ir_msg->data1); //根据要发射的数据,填充item
rmt_write_items(tx_channel, item, item_num, true); //将item写入通道对应的RAM并进入阻塞
rmt_wait_tx_done(tx_channel, portMAX_DELAY); //等待发送完成 进入阻塞
free(item);
nec_build_items();就是填充item的代码,在本例子中,实现的是填充格力红外编码,具体代码在文末提供。
如果你理解了发射的部分,那么接收就更加容易了。你可能猜想如果发射是构建item,那么接收就应该是获得item。恭喜你,老伙计,你答对了!这里我假设需要接收格力红外。
接收器会自动把某个通道接收到的数据写入对应的RAM中,并封装成item,再放入指定的ringbuff中,我们的程序只要拿到对应通道的ringbuff,就能从ringbuff中读取出脉冲序列。
关于啥是ringbuff,ringbuff就是一个缓存区buff。废话。详细可以看看 ringbuff简单实现
关于接收的几个重要函数:
获取指定通道的ringbuff
rmt_get_ringbuf_handle(rx_channel, &rb); //获取红外接收器对应通道的ringbuff
从ringbuff读取items 会进入阻塞 直到ringbuff中有新的数据(也就是接收到信号)
rmt_item32_t* item = (rmt_item32_t*) xRingbufferReceive(rb, &rx_size, portMAX_DELAY);
使用完ringbuff和item后要释放内存
vRingbufferReturnItem(rb, (void*) item);
开启或关闭接收通道,停止后ringbuff中不再有新的数据
rmt_rx_stop(rx_channel);
rmt_rx_start(rx_channel);
/*
* @brief RMT receiver initialization
*/
static void nec_rx_init()
{
rmt_config_t rmt_rx;
rmt_rx.channel = rx_channel;
rmt_rx.gpio_num = RMT_RX_GPIO_NUM;
rmt_rx.clk_div = RMT_CLK_DIV; //分频系数 100
rmt_rx.mem_block_num = 2;
rmt_rx.rmt_mode = RMT_MODE_RX;
rmt_rx.rx_config.filter_en = true; //开启滤波器
rmt_rx.rx_config.filter_ticks_thresh = 100; //滤波信号宽度100*80M=12.5us
rmt_rx.rx_config.idle_threshold = rmt_item32_tIMEOUT_US / 10 * (RMT_TICK_10_US); //退出接收时间:21000us后接收不到信号变化,退出接收
rmt_config(&rmt_rx);
ESP_LOGI(TAG,"rmt rx config");
//安装红外接收通道,rinbuff大小1000字节
rmt_driver_install(rmt_rx.channel, 1000, 0);
ESP_LOGI(TAG,"rx driver initialization ok");
}
接收代码就更加的简单了,只要调用这三个函数就够了。item的解析与对上文item的构建一样是最重要的,这里以格力红外为例,不同情况下有不同的解析代码。
rmt_rx_start(rx_channel);
rmt_item32_t* item = (rmt_item32_t*) xRingbufferReceive(rb, &rx_size, portMAX_DELAY);
rmt_rx_stop(rx_channel);
红外编码的构建:
该步骤的目标,是构建item集合,因为item对应的就是RAM中的32位的数据。根据具体的红外协议的要求,将电平,逻辑0,1的电平时间长度等写入item,并根据协议的长度设置item的数量。
例如,格力红外协议组成:
起始码(S)+35位数据码+连接码(C)+32位数据码
1、各种编码的电平宽度:
数据码由“0”“1”起始码、连接码组成:
0的电平宽度为:600us低电平+600us高电平,
1的电平宽度为:600us低电平+1600us高电平
起始码S电平宽度为:9000us低电平+4500us高电平
连接码C电平宽度为:600us低电平+20000us高电平
例如item构建函数:构建70个item,item的内容由使用的协议决定。
/*
* @brief Build NEC 32bit waveform.
*/
static void nec_build_items( rmt_item32_t* item, int item_num, uint64_t ir_data0,uint32_t ir_data1)
{
int j = 0;
nec_fill_item_header(item++); //构建起始信号
//35位数据码
for(j = 0; j < 35; j++) {
if(ir_data0 & 0x1) {
//ESP_LOGI(TAG, "item =1");
nec_fill_item_bit_one(item);
} else {
//ESP_LOGI(TAG, "item =0");
nec_fill_item_bit_zero(item);
}
item++;
ir_data0 >>= 1;
}
//连接信号
nec_fill_item_connect(item);
item++;
//32位数据码
for(j = 0; j < 32; j++) {
if(ir_data1 & 0x1) {
//ESP_LOGI(TAG, "item =1");
nec_fill_item_bit_one(item);
} else {
//ESP_LOGI(TAG, "item =0");
nec_fill_item_bit_zero(item);
}
item++;
ir_data1 >>= 1;
}
nec_fill_item_end(item);
}
/*
* @brief 填充item的电平和电平时间 需要将时间转换成计数器的计数值 /10*RMT_TICK_10_US
*/
static inline void nec_fill_item_level(rmt_item32_t* item, int high_us, int low_us)
{
item->level0 = 1;
item->duration0 = (high_us) / 10 * RMT_TICK_10_US;
item->level1 = 0;
item->duration1 = (low_us) / 10 * RMT_TICK_10_US;
}
/*
* @brief Generate NEC header value: active 9ms + negative 4.5ms
*/
static void nec_fill_item_header(rmt_item32_t* item)
{
nec_fill_item_level(item, NEC_HEADER_HIGH_US, NEC_HEADER_LOW_US);
}
/*
* @brief
*/
static void nec_fill_item_connect(rmt_item32_t* item)
{
nec_fill_item_level(item, NEC_CONNECT_HIGH_US, NEC_CONNECT_LOW_US);
}
/*
* @brief Generate NEC data bit 1: positive 0.56ms + negative 1.69ms
*/
static void nec_fill_item_bit_one(rmt_item32_t* item)
{
nec_fill_item_level(item, NEC_BIT_ONE_HIGH_US, NEC_BIT_ONE_LOW_US);
}
/*
* @brief Generate NEC data bit 0: positive 0.56ms + negative 0.56ms
*/
static void nec_fill_item_bit_zero(rmt_item32_t* item)
{
nec_fill_item_level(item, NEC_BIT_ZERO_HIGH_US, NEC_BIT_ZERO_LOW_US);
}
/*
* @brief Generate NEC end signal: positive 0.56ms
*/
static void nec_fill_item_end(rmt_item32_t* item)
{
nec_fill_item_level(item, NEC_BIT_END, 0x7fff);
}
红外编码的解析是构建的逆过程。首先了解红外接收器的工作原理:
在初始化时我们就开启了中断,红外模块的中断寄存器就只有三个,如下。在初始化过程中开启了发送和接收完成中断,在接收完成中断服务函数中,将RAM中接收到的数据写入ringbuff,所以我们需要在只需要从ringbuff中就能读取RAM中接收的数据,此部分代码如下:
接收中断:
RMT.conf_ch[channel].conf1.rx_en = 0;
int item_len = rmt_get_mem_len(channel);
//change memory owner to protect data.
RMT.conf_ch[channel].conf1.mem_owner = RMT_MEM_OWNER_TX;
if(p_rmt->rx_buf) {
//将RAM中数据写入ringbuff
BaseType_t res = xRingbufferSendFromISR(p_rmt->rx_buf, (void*) RMTMEM.chan[channel].data32, item_len * 4, &HPTaskAwoken);
if(res == pdFALSE) {
ESP_EARLY_LOGE(RMT_TAG, "RMT RX BUFFER FULL");
} else {
}
} else {
ESP_EARLY_LOGE(RMT_TAG, "RMT RX BUFFER ERROR\n");
}
RMT.conf_ch[channel].conf1.mem_wr_rst = 1;
RMT.conf_ch[channel].conf1.mem_owner = RMT_MEM_OWNER_RX;
RMT.conf_ch[channel].conf1.rx_en = 1;
break;
在接收完成后解析items:
/*
* 解析从item中的信息
*/
static int nec_parse_items(rmt_item32_t* item, int item_num, uint64_t* data0, uint32_t* data1)
{
int i;
uint64_t temp0 = 0;
uint32_t temp1 = 0;
//接收的数据长度不小于一次传输的长度
if(item_num < NEC_DATA_ITEM_NUM)
{
return -1;
}
//检查起始位
if(!nec_header_if(item))
{
return -2;
}
item++;
//检查数据位
for(i = 0; i < 35; i++)
{
ESP_LOGI(IR_TAG, "item data0 %u, %u, %u, %u", NEC_ITEM_DURATION(item->level0), NEC_ITEM_DURATION(item->duration0), NEC_ITEM_DURATION(item->level1), NEC_ITEM_DURATION(item->duration1));
if(nec_bit_one_if(item))
{
temp0 |= (1<<i);
}else if(nec_bit_zero_if(item))
{
temp0 &= (0<<i);
}else
{
ESP_LOGI(IR_TAG, "item i= %d", i);
return -3;
}
item++;
}
//检查连接段信号
if(nec_bit_connect_if(item))
{
return -4;
}
item ++;
//检查第二段信号
for(i = 0; i < 35; i++)
{
ESP_LOGI(IR_TAG, "item data1 %u, %u, %u, %u", NEC_ITEM_DURATION(item->level0), NEC_ITEM_DURATION(item->duration0), NEC_ITEM_DURATION(item->level1), NEC_ITEM_DURATION(item->duration1));
if(nec_bit_one_if(item))
{
temp1 |= (1<<i);
}else if(nec_bit_zero_if(item))
{
temp1 &= (0<<i);
}else
{
ESP_LOGI(IR_TAG, "item i= %d", i);
return -5;
}
item++;
}
*data0 = temp0;
*data1 = temp1;
return 0;
}
esp32 的rmt外设不仅可以用来处理红外,他可以广泛用于电平信号的接收与产生,他的分辨率可以达到微秒级别,使用rmt可以与其他的模块进行通信等。总之rmt的功能还是非常的强大的。
如果打算实现万能遥控的功能,可用参考我的博客:esp32实现万能红外遥控器,这篇博客是在本文的基础上增加了一个开源红外码库的使用
格力红外编码
YB0F2协议:http://bbs.eeworld.com.cn/thread-462015-1-1.html
esp32技术参考手册