--[[ @module necir @summary necir NEC协议红外接收 @version 3.5 @date 2023.09.03 @author lulipro @usage --注意: --1、本库在Air101和Air103测试通过,Air105由于SPI传输帧与帧之间存在间隔因此暂不支持。 --2、本库实现了NEC红外数据接收,发送请使用LuatOS底层固件自带的ir.sendNEC()函数。 --3、由于本库基于标准四线SPI接口实现,所以虽然只用到了MISO引脚,但是其他3个SPI相关引脚在使用期间 -- 无法作为其他用途,除非执行necir.close()并等待necir.isClosed()为true后,SPI将完全释放,才可以用于其他用途。 --硬件模块:VS1838及其兼容的一体化接收头 --接线示意图: --1、支持单IO模式,即仅使用一个SPI_MISO引脚,此时necir.init的irq_pin参数必须是SPI_MISO所在引脚。 --2、支持多从机,如果SPI总线上需要挂接其他从设备,为了在不使用红外通信期间,避免VS1838干扰总线的MISO,则可以用 -- 一个NMOS(例如AO3400)控制VS1838的电源地,如下所示,GPIO充当VS1838的片选信号,当GPIO输出低 -- 则禁用VS1838,此时VS1838不工作,其OUT引脚为高阻,不会干扰MISO;当GPIO输出高,则启用VS1838, -- 此时其OUT引脚会输出红外通信信号到单片机。由于一般的SPI从机片选逻辑为低使能,因此这样可以用 -- 同一个片选GPIO来控制VS1838以及另一个SPI从机,因为片选逻辑是相反的。配合necir库的necir.close() -- 和necir.isClosed()可以最大化的复用SPI接口,避免SPI的独占而浪费。 -- ____________________ ____________________ -- | | 单IO | | -- | SPI_MISO |--------------| OUT | -- | Air10x | | VS1838 | -- | | | 一体化接收头 | -- | GPIO |---- ----| GND | -- |____________________| | | |____________________| -- | | -- | ____|________ -- | | D | -- --| G NMOS | -- |____S________| -- | -- GND --用法实例:演示用同一个SPI接口驱动VS1838和W25QXX --用法实例: local necir = require("necir") --定义用户回调函数 local function my_ir_cb(frameTab) log.info('get ir msg','addr=',frameTab[1],frameTab[2],'data=',frameTab[3]) end sys.taskInit(function() local CS = gpio.setup(pin.PA07,0) --VS1838(NMOS控制其GND)与W25QXX共用的片选引脚 necir.init(spi.SPI_0,pin.PB03,my_ir_cb) while 1 do --=============================== log.info('------necir start------') CS(1) --使能VS1838 necir.start() --开启necir数据接收过程 sys.wait(10000) log.info('necir request to close') necir.close() --请求关闭necir while not (necir.isClosed()) do sys.wait(200) end CS(0) --除能VS1838 log.info('necir closed') sys.wait(1000) --=============================== log.info('------setup to read w25qxx chip id------') spi.setup(spi.SPI_0,nil, 0,--CPHA 0,--CPOL 8,--数据宽度 100000,--频率 spi.MSB,--高低位顺序 spi.master,--主模式 spi.full--全双工 ) --读取W25QXX chi id,0XEF15,表示芯片型号为W25Q32,0XEF16,表示芯片型号为W25Q64 CS(0) --片选W25QXX spi.send(spi.SPI_0,string.char(0x90)..string.char(0x00)..string.char(0x00)..string.char(0x00)) local chip_id = spi.recv(spi.SPI_0,2) log.info('w25qxx id=',chip_id:toHex()) CS(1) --取消片选W25QXX sys.wait(1000) end end) ]] local necir = {} local sys = require "sys" local NECIR_IRQ_PIN --上升沿中断检测引脚 local NECIR_SPI_ID --使用的SPI接口的ID local NECIR_SPI_BAUDRATE = (14222*4) --SPI时钟频率,单位HZ local NECIR_SPI_RECV_BUFF_LEN = (32+192+192) --SPI接收数据的长度 local recvBuff --SPI接收数据缓冲区 local recvNECFrame={} --依次存储:地址码,地址码取反,数据码,数据码取反 local recvCallback --NEC报文接收成功后的用户回调函数 local isNeedTaskFlag --接收任务是否需要运行的标志 local isClosedFlag --necir是否已经完全关闭的标志 --[[ ==============实现原理================================================ NEC协议中无论是引导信号,逻辑0还是逻辑1,都由若干个562.5us的周期组成。 例如 引导信号: 16个562.5us的低电平+8个562.5us的高电平组成 逻 辑 1 :1个562.5us的低电平+3个562.5us的高电平组成 逻 辑 0 :1个562.5us的低电平+1个562.5us的高电平组成 采样定理告诉我们,一切数字信号,都可以通过高倍速采样还原。 我们使用SPI的MISO引脚对红外接收管的输出进行【连续采样】。 我们使用4个SPI字节去采样一个562.5us的红外信号周期,因此SPI的时钟频率设置为 (14222*4) Hz 则 引导信号: 16*4个0x00 + 8*4个0xff组成 逻 辑 1 :1*4个0x00 + 3*4个0xff组成,共16字节 逻 辑 0 :1*4个0x00 + 1*4个0xff组成 ,共8字节 NEC的引导信号由一段低电平+一段高电平组成,为了降低采样深度,避免空间占用,我们选择 从后面的高电平产生的上升沿开始进行SPI接收采样 而不是从第一个下降沿就开始。 确定采样深度。NEC协议中,地址码和数据码都是连续传输2次,连续的2个字节是相互取反的关系, 因此这2个字节的总的传输时间是固定,因为前一个字节的某个位是1,则必定后一个字节对应位是0, 则总传输时间就是 = (逻辑1传输时间+逻辑0传输时间)* 8, 则(地址码+ 地址码取反) 和 (数据码+数据码取反)的采样深度都是 (16+8)*8 = 192字节。 引导码高电平部分则是8*4字节。这样我们就确定了SPI的总传输字节数= 32 + 192 + 192=416字节。 理想情况下中断检测到引导码产生的上升沿就会立刻开始SPI数据采集,这样SPI传输 416 字节 能刚好采集引导码的4.5ms高电平加后面的4字节数据,但由于LuatOS中断响应存在一定延迟,导致真正开始 SPI采集会在引导码上升沿开始后的若干个毫秒延迟后才开始,经过测试一般延迟在1ms以内,但具体延迟取决于 LuatOS的行为,只要这个延迟能让SPI采集到完整的引导码后面的4字节数据就都是可以接受的,即延迟不能超过 4.5ms,一般这个情况是能满足的。 本方法需要的SPI频率为56888Hz,而Air101和Air103的SPI频率 = 40MHz/(2*(div_REG+1)), 则div_REG = 350即可实现。如果芯片的SPI无法配置频率也将不支持此方案。 考虑到LuatOS中断响应存在一定延迟,导致SPI接收的信号与实际输出的信号存在一定的 滞后导致字节错位。因此对接收到的SPI字节数据 依次向后 采用16字节长度的窗口切出子串, 对这个子串进行模式匹配,来确定当前片段对应是NEC的逻辑1还是逻辑0。 即: 在当前位置开始的后面的16个字节形成的子串中,如果存在连续的9个0xff, 则认为当前位置是逻辑1,否则认为是逻辑0。 为什什么是9个,考虑下面的情形: 逻辑1: 0x00 0x00 0x00 0x00 0xff 0xff 0xff 0xff ... 0xff [ 4 个 ] [ 12 个 ] 逻辑0: 0x00 0x00 0x00 0x00 0xff 0xff 0xff 0xff 0x00 0x00 0x00 0x00 0xff 0xff 0xff 0xff [ 4 个 ] [ 4 个 ] [ 下一个逻辑位(部分) ] 可以发现,如果是逻辑0,则连续的16个字节窗口中最多出现4个连续的0xff,因此可以作为区分。 lua中的string.find()可以方便的实现字符串模式匹配,因此代码实现很简单。 ]] --从收到的SPI数据中解析出NEC报文 local function parseRecvData() local fs,fe local si --数据重新初始化 recvNECFrame[1] = 0 recvNECFrame[2] = 0 recvNECFrame[3] = 0 recvNECFrame[4] = 0 --尝试找到第一个不是0xff出现的位置,用于跳过引导信号字节,这部分最多32字节 si=1 while si <= 32+5 do if 0xff ~= string.byte(string.sub(recvBuff,si,si)) then break end si = si + 1 end --32+5个0xff后依然没有出现地址数据,属于不正常的信号,直接退出 if 0xff == string.byte(string.sub(recvBuff,si,si)) then return end --遍历引导信号后续的信号,整个收到的SPI数据进行NEC数据解析还原 while si<=NECIR_SPI_RECV_BUFF_LEN do for k = 1, 4, 1 do --解析出NEC报文的4个字节 for i = 0, 7, 1 do --每个字节8位 --在当前位置开始的后面的16个字节中,如果存在连续的9个0xff的值, --则认为当前位置是逻辑1 fs,fe = string.find( string.sub(recvBuff,si,si+16-1), '\xff\xff\xff\xff\xff\xff\xff\xff\xff' ) if fs and fe then --找到这种模式 --这个bit为1(NEC协议大小端为LSB First) recvNECFrame[k] = (recvNECFrame[k] | (1<