第30章_瑞萨MCU零基础入门系列教程之IRDA红外遥控实验

2023-10-31 17:10

本文主要是介绍第30章_瑞萨MCU零基础入门系列教程之IRDA红外遥控实验,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id=728461040949

配套资料获取:https://renesas-docs.100ask.net

瑞萨MCU零基础入门系列教程汇总: https://blog.csdn.net/qq_35181236/article/details/132779862


第30章 IRDA红外遥控实验

本章目标

  • 学会使用红外遥控IRDA的通信协议;
  • 了解环形缓冲区的概念以及掌握基于环形缓冲区的程序设计;
  • 学会使用瑞萨RA6M5的GPT输入捕获功能;

30.1 IRDA红外遥控协议简介

NEC协议是众多红外遥控协议的其中一种,除NEC外,还有RC5、RC6等协议。市面上买到的非学习型万能电视遥控器大多集成一种或多种编码,一般都支持NEC协议。

NEC编码的一帧(通常按一下遥控器按钮所发送的数据)由引导码、地址码及数据码组成,如下图所示,把地址码及数据码取反的作用是验证数据的正确性。

当一直按住一个按钮的时候,会隔110ms左右发一次连续码,连续码后面不带任何数据。NEC协议的信号定义如下图所示:

引导码由一个9ms的低脉冲加上一个4.5ms的高脉冲组成,它用来通知接收方“我要开始传输数据了”。

数据1和0,开始都是0.56ms的低脉冲,对于数据1,后面的高脉冲比较长;对于数据0,后面的高脉冲比较短。

NEC协议里有很多时间,这些时间有一个有趣的现象,把所有时间里面最小的0.56ms看作基本脉冲宽度,假设用t表示,那么其它时长都是t的倍数。

NEC协议中,信号的最小时间单位是0.56ms,这个时间对人来说是很短的,但对于嵌入式系统它是很长的,足够做很多事情了。我们并不知道用户什么时候按下遥控器,使用轮询的方式特别耗资源,因此使用中断来处理。

30.2 模块配置

30.2.1 硬件连接

本次实验使用的是板载IRDA模块,其原理图如下图所示:

使用的引脚是P404,属于定时器GPT3的输入输出控制引脚。

30.2.2 GPT模块配置

在RASC中设置引脚和添加Stack模块,本小节就仅展示配置结果。

  1. 配置Pins

  1. 配置GPT Stack

30.3 设备对象封装

在实际开发过程中,会涉及很多不同的设备。本实验抛砖引玉,对于串口、定时器,都基于环形缓冲区来实现它们的操作。

30.3.1 串口设备

  1. 对象封装和管理

前面的章节里,使用串口时比较简单直接。本实验里,针对串口设备的特点,抽象出一个结构体(在dev_uart.h里):

typedef struct UartDev{char *name;unsigned char channel;int (*Init)(struct UartDev *ptdev);int (*Write)(struct UartDev *ptdev, unsigned char * const buf, unsigned int length);int (*Read)(struct UartDev *ptdev, unsigned char *buf, unsigned int length);struct UartDev *next;
}UartDev;

底层的程序里,对于每一个串口设备,都要实现一个UartDev结构体。

假设底层drv_uart.c里实现了一个UartDev结构体,要把它插入一个链表管理起来,在dev_uart.c里实现了如下链表插入函数:

static struct UartDev *gHeadUartDev;
void UartDeviceInsert(struct UartDev *ptdev)
{if(NULL == gHeadUartDev)gHeadUartDev = ptdev;Else{ptdev->next = gHeadUartDev;gHeadUartDev = ptdev;}
}
  1. 实现串口设备

在drv_uart.c中,构造一个UartDev结构体,代码如下:

static struct UartDev gLogDevice = {.name = "Log",.channel = 7,.Init = UARTDrvInit,.Read = UARTDrvRead,.Write = UARTDrvWrite,.next = NULL
};

然后提供一个UartDevicesCreate函数,调用“UartDeviceInsert(&gLogDevice)”把这个设备插入链表:

void UartDevicesCreate(void)
{UartDeviceInsert(&gLogDevice);gLogDevice.Init(&gLogDevice);
}
  1. 注册串口设备

底层的drv_uart.c提供了UartDevicesCreate函数来注册它实现的UartDev,谁来调用UartDevicesCreate?

假设还有另一个底层的drv_uart2.c也提供了Uart2DevicesCreate函数来注册它实现的UartDev,谁来调用Uart2DevicesCreate?

我们需要在设备层实现一个函数:UartDevicesRegister,用来调用底层的接口函数,管理底层实现的所有UartDev。代码如下:

void UartDevicesRegister(void)
{UartDevicesCreate();UartDeviceList();
}

调用UartDevicesRegister后,在链表里就记录有底层实现的各个UartDev了。

  1. 查找串口设备

如何使用串口设备?需要在链表里根据名字找到UartDev,代码如下:

struct UartDev *UartDeviceFind(const char *name)
{struct UartDev *ptdev = gHeadUartDev;while(ptdev){if(strstr(ptdev->name, name)){return ptdev;}ptdev = ptdev->next;}return NULL;
}
  1. 打印整个串口设备列表

链表打印也很简单,从表头开始遍历、打印:

void UartDeviceList(void)
{struct UartDev *ptdev = gHeadUartDev;printf("\r\nUart Device List:\r\n");while(ptdev){printf("\t%s\r\n", ptdev->name);ptdev = ptdev->next;}printf("\r\n");
}

30.3.2 定时器设备

  1. 对象封装

根据定时器的特点抽象出一个结构体,里面有名称、通道等属性,还有初始化、启停、读取等操作方法。代码如下:

typedef struct TimerDev{char *name;unsigned char channel;unsigned char status;int (*Init)(struct TimerDev *ptdev);int (*Start)(struct TimerDev *ptdev);int (*Stop)(struct TimerDev *ptdev);int (*Read)(struct TimerDev *ptdev, unsigned char *buf, unsigned int length);int (*Timeout)(struct TimerDev *ptdev, unsigned int timeout);struct TimerDev *next;
}TimerDevice;
  1. 实现定时器设备

本章要使用GPT3的输入捕获功能,因而在drv_gpt.c中实现了这个定时器:

static struct TimerDev gGPTDevice = {.name = "GPT3",.channel = 3,.status = 0,.Init = GPTDrvInit,.Start = GPTDrvStart,.Stop = GPTDrvStop,.Read = GPTDrvRead,.Timeout = NULL,.next = NULL
};void GPTTimerDevicesCreate(void)
{TimerDeviceInsert(&gGPTDevice);
}
  1. 管理定时器设备

对于瑞萨的RA6M5处理器而言,定时器分为3大类:

  1. 滴答定时器
  2. GPT
  3. 看门狗定时器

根据config.h中的宏开关来决定是否注册这些设备:

void TimerDevicesRegister(void)
{
#if DRV_USE_SYSTICKSystickTimerDevicesCreate();
#endif#if DRV_USE_GPTGPTTimerDevicesCreate();
#endif#if DRV_USE_WDT
#endif#if DRV_USE_IWDT
#endifTimerDeviceList();
}

30.4 驱动程序

30.4.1 初始化定时器

本次实验会使用定时器:红外遥控器的输入引脚,触发定时器产生中断,在中断回调函数中记录时间。所以需要使能ECL模块来连接GPIO和GPT3,并且使能GPT3的触发功能。

另外,接收到的红外遥控信号是一系列的波形,读取到完整的波形后,才能解析出数据。所以需要为GPT3创建一个环形缓冲区,用来保存一系列的时间值。

初始化代码如下:

static struct RingBuffer *gGPT3Buffer = NULL;
static int GPTDrvInit(struct TimerDev *ptdev)
{if(NULL==ptdev) return EINVAL;switch(ptdev->channel){case 0:case 1:case 2:break;case 3:{/* 打开GPT设备完成初始化 */fsp_err_t err = g_timer3.p_api->open(g_timer3.p_ctrl, g_timer3.p_cfg);assert(FSP_SUCCESS == err);/* 使能GPT的ELC功能 */err = g_timer3.p_api->enable(g_timer3.p_ctrl);assert(FSP_SUCCESS == err);/* 打开ELC设备完成初始化 */err = g_elc.p_api->open(g_elc.p_ctrl, g_elc.p_cfg);assert(FSP_SUCCESS == err);/* 使能ELC的连接功能 */err = g_elc.p_api->enable(g_elc.p_ctrl);assert(FSP_SUCCESS == err);/* 给GPT3申请一个缓冲区存储采样捕获数据 */gGPT3Buffer = RingBufferNew(1024);break;}case 4:case 5:case 6:case 7:case 8:case 9:break;default:break;}return ESUCCESS;
}

第24行分配的缓冲区,只在这个驱动文件用到,用户并不关心GPT3的内部实现,所以gGPT3Buffer被设置为static类型,不要对外暴露它的存在。

30.4.2 定时器开启

调用GPT的start函数开启即可:

static int GPTDrvStart(struct TimerDev *ptdev)
{if(NULL==ptdev) return -EINVAL;switch(ptdev->channel){case 0:case 1:case 2:break;case 3:{/* 开启GPT的计数 */fsp_err_t err = g_timer3.p_api->start(g_timer3.p_ctrl);assert(FSP_SUCCESS == err);break;}case 4:case 5:case 6:case 7:case 8:case 9:break;default:break;}return ESUCCESS;
}

30.4.3 定时器关闭

关闭定时器停止计数,顺便讲计数器清零、清除环形缓冲区:

static int GPTDrvStop(struct TimerDev *ptdev)
{if(NULL==ptdev) return -EINVAL;switch(ptdev->channel){case 0:case 1:case 2:break;case 3:{/* 停止GPT的计数 */fsp_err_t err = g_timer3.p_api->stop(g_timer3.p_ctrl);assert(FSP_SUCCESS == err);err = g_timer3.p_api->reset(g_timer3.p_ctrl);assert(FSP_SUCCESS == err);gOverflowCount = 0;if(NULL != gGPT3Buffer)gGPT3Buffer->Clear(gGPT3Buffer);break;}case 4:case 5:case 6:case 7:case 8:case 9:break;default:break;}return ESUCCESS;

30.4.4 中断回调函数

在定时器的回调函数里,有一个全局变量:gOverflowCount,它每隔10us累加一次。当发生GPIO捕获事件时,可以把gOverflowCount当做时间存入环形缓冲区。代码如下:

void timer3_callback(timer_callback_args_t * p_args)
{switch(p_args->event){case TIMER_EVENT_CYCLE_END:{gOverflowCount++;break;}case TIMER_EVENT_CAPTURE_A:case TIMER_EVENT_CAPTURE_B:{uint32_t  lCaptureTime = gOverflowCount;if(NULL != gGPT3Buffer)gGPT3Buffer->Write(gGPT3Buffer, (uint8_t*)&lCaptureTime, sizeof(uint32_t));break;}default:break;}
}

30.4.5 读取定时器的采样数据

定时器的回调函数里,把信号的触发时间存入到唤醒缓冲区。要解析数据时,需要从环形缓冲区中读取数据。代码如下:

static int GPTDrvRead(struct TimerDev *ptdev, unsigned char *buf, unsigned int length)
{if(NULL == ptdev)   return -EINVAL;if(NULL == buf)     return -EINVAL;if(0 == length)     return -EINVAL;if(NULL == gGPT3Buffer)   return -EINVAL;unsigned int ret = gGPT3Buffer->Read(gGPT3Buffer, buf, length);if(ret != length)   return -ENOMEM;return (int)length;
}

30.5 红外模块驱动

30.5.1 遥控器键值

红外遥控器上的所有按键的十六进制键值如下:

const unsigned char KeyCode[20] = {0x45, 0x47, 0x44, 0x40, 0x43, 0x07, 0x15, 0x09, 0x16, 0x19, 0x0D, 0x0C, 0x18, 0x5E, 0x08, 0x1C, 0x5A, 0x42, 0x52, 0x4A};

为了方便理解,针对这些键值定义了一个字符串指针数组:

const unsigned char *KeyName[20] = {"Open", "Menu", "Test", "+", "Return", "Back", "Suspend", "Forward", "0", "-", "Cancle", "1", "2", "3", "4", "5", "6", "7", "8", "9"};

30.5.2 红外设备对象封装

对于红外模块而言,最重要的就是获取键值,也就是读取函数;另外也要对红外模块进行初始化,抽象出一个结构体:

typedef struct IRDADev{char *name;int (*Init)(struct IRDADev *ptdev);int (*Read) (struct IRDADev *ptdev, unsigned char *key_code, char **key_name);
}IRDADevice;

然后在dev_irda.c中构造IRDADev,代码如下:

static struct IRDADev gIRDADev = {.name  = "nec",.Init = IRDADevInit,.Read = IRDADevRead
};struct IRDADev *IRDADeviceGet(void)
{return &gIRDADev;
}

30.5.3 数据解码

根据NEC协议,数据的解码总体分为3步:

  1. 查找引导码字段;
  2. 查找连发码/非连发码字段;
  3. 数据解析;

引导码是两个采样捕获数据的时间差在9ms左右,连发码的时间差是2.25ms左右,非连发码的在4.5ms左右,误差取为500us。

当收到一个引导码后,后续会传来32个位数据(表示地址和数据),那么定时器就需要采样64次,根据时间差分辨该位是1还是0。

综上所述,IRDA的解码程序如下设计:

static int IRDADevRead(struct IRDADev *ptdev, unsigned char *key_code, char *key_name)
{if(NULL == ptdev)       return -EINVAL;if(NULL == pTimerDev)   return -EINVAL;uint16_t temp_buff[64] = {0};uint8_t pQ = 0;uint32_t tick[2] = {0};uint8_t step = 0;uint16_t duty = 0;uint32_t value = 0;uint8_t i = 0, cnt = 0;ptdev->key = NULL;while(1){if(pTimerDev->Read(pTimerDev, (uint8_t*)&tick[pQ], sizeof(uint32_t)) != sizeof(uint32_t)){continue;}pQ++;if(pQ==2){duty = (uint16_t)(tick[1] - tick[0])*10;tick[0] = tick[1];pQ = 1;}switch(step){case 0:{   if( (duty>=8500) && (duty<=9500) )  // 引导码{step++;}break;}case 1:{if( (duty>=4000) && (duty<=5000) )    // 非连发码{i = 0;cnt = 0;step++;}else if( (duty>=2000) && (duty<=2500) )   // 连发码{step += 3;}else{step = 0;}break;}case 2:{                temp_buff[i] = duty;i++;if(i==64)   step++;break;}case 3:{for(i=0;i<64;i+=2){if( (temp_buff[i]>=450) && (temp_buff[i]<=650) ) {if( (temp_buff[i+1]>=450) && (temp_buff[i+1]<=650) )    // 逻辑0{} else if( (temp_buff[i+1]>=1000) && (temp_buff[i+1]<=2000) ) // 逻辑1{value = value + (1<<cnt);}cnt++;}}step++;break;}case 4:{step = 0;pQ = 0;uint8_t sys_code = (value>>16)&0xFF;for(i=0; i<20; i++){if(KeyCode[i] == sys_code){*key_code = sys_code;*key_name = (char*)KeyName[i];return ESUCCESS;}}return EIO;;}default:break;}}
}

成功解码后,返回按键码、按键名字。

30.6 测试程序

测试程序放在Applications文件夹中,IRDA的测试源文件为app_irda.c,在里面实现了一个设备测试函数,将所有用到的设备进行注册,初始化IRDA设备,并且读取键值打印出来,代码如下所示:

void IRDAAppTest(void)
{UartDevicesRegister();TimerDevicesRegister();struct IRDADev *pIRDA = IRDADeviceGet();if(NULL == pIRDA){printf("Failed to get IRDA device!\r\n");return;}pIRDA->Init(pIRDA);while(1){if(ESUCCESS == pIRDA->Read(pIRDA)){printf("%s\r\n", pIRDA->key);}}
}

30.7 测试结果

在hal_entry()函数中调用测试函数IRDAAppTest,将编译出来的二进制可执行文件烧录到板子上运行,打开串口助手,按下遥控器按键可以看到类似下面的信息:


本章完

这篇关于第30章_瑞萨MCU零基础入门系列教程之IRDA红外遥控实验的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/316852

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

30常用 Maven 命令

Maven 是一个强大的项目管理和构建工具,它广泛用于 Java 项目的依赖管理、构建流程和插件集成。Maven 的命令行工具提供了大量的命令来帮助开发人员管理项目的生命周期、依赖和插件。以下是 常用 Maven 命令的使用场景及其详细解释。 1. mvn clean 使用场景:清理项目的生成目录,通常用于删除项目中自动生成的文件(如 target/ 目录)。共性规律:清理操作

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

数论入门整理(updating)

一、gcd lcm 基础中的基础,一般用来处理计算第一步什么的,分数化简之类。 LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a; } <pre name="code" class="cpp">LL lcm(LL a, LL b){LL c = gcd(a, b);return a / c * b;} 例题:

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

Java 创建图形用户界面(GUI)入门指南(Swing库 JFrame 类)概述

概述 基本概念 Java Swing 的架构 Java Swing 是一个为 Java 设计的 GUI 工具包,是 JAVA 基础类的一部分,基于 Java AWT 构建,提供了一系列轻量级、可定制的图形用户界面(GUI)组件。 与 AWT 相比,Swing 提供了许多比 AWT 更好的屏幕显示元素,更加灵活和可定制,具有更好的跨平台性能。 组件和容器 Java Swing 提供了许多

【IPV6从入门到起飞】5-1 IPV6+Home Assistant(搭建基本环境)

【IPV6从入门到起飞】5-1 IPV6+Home Assistant #搭建基本环境 1 背景2 docker下载 hass3 创建容器4 浏览器访问 hass5 手机APP远程访问hass6 更多玩法 1 背景 既然电脑可以IPV6入站,手机流量可以访问IPV6网络的服务,为什么不在电脑搭建Home Assistant(hass),来控制你的设备呢?@智能家居 @万物互联

poj 2104 and hdu 2665 划分树模板入门题

题意: 给一个数组n(1e5)个数,给一个范围(fr, to, k),求这个范围中第k大的数。 解析: 划分树入门。 bing神的模板。 坑爹的地方是把-l 看成了-1........ 一直re。 代码: poj 2104: #include <iostream>#include <cstdio>#include <cstdlib>#include <al