数码之家

 找回密码
 立即注册

QQ登录

只需一步,快速开始

微信登录

微信扫一扫,快速登录

搜索
查看: 164|回复: 7

[AVR] IOT化改造:三个零件,给大金家用中央空调加装遥控功能

[复制链接]
发表于 昨天 23:17 | 显示全部楼层 |阅读模式
本帖最后由 maidoo 于 2025-9-16 23:17 编辑

背景
2011年的夏天,用三个零件,给大金家用中央空调面板增加了红外遥控功能。参见老版论坛
《三个零件,给大金家用中央空调加装遥控功能》
就是这样的面板,通过模拟按键的方式可以红外遥控到面板上的电源和风力两个按钮。10多年来一直工作稳定。


近些年来随着智能家居的普及和应用,配合天猫精灵万能红外遥控器,可以实现远程遥控。车到楼下可以提前打开空调,停好车进屋的时候房间就已经凉快了。


新问题
但有个小问题,就是这个电源按钮是一个乒乓开关,远程无法确认实际的开关状态,就怕多按了一下,就状态混乱了。
所以最好能把开和关用不同的指令区分开来。这个需求我称之为IoT化改造。
因为10年前的改造方案是通过外挂电路板的方式模拟按键,这个外挂的AvR单片机只会根据红外指令去按对应的按钮,它并不知道当前这个电源按钮按下去后,这个空调是变开了还是变关了。


解决办法
解决的办法也很简单:从面板上的电源LED上增加飞线到单片机的io脚上,再配合软件的修改,单片机就可以知道当前空调是开还是关,那就可以根据当前的状态去决定开空调和关空调不同的动作。


具体方法及使用
软件在完全保留以前版本功能基础上,增加了根据学习到电源按钮的指令码,派生出开和关指令码的功能。
简单说,IOT方案是根据空调面板上电源开关学习到的红外指令,派生出固定开和固定关的指令内码
        如学习到的红外按键的十六进制32位指令码(MSB)是 xxXXxxxx,根据第二字节XX的值派生
        XX+1的指令为按开,即 xxJJxxxx,其中JJ=XX+1,其余xx保持不变
        XX+2的指令为按关,即 xxKKxxxx,其中KK=XX+2,其余xx保持不变

这个版本的软件同时还修改了蜂鸣器的反馈,开空调是滴滴2个短鸣,关空调是滴滴——(1短加1长)
有了明确的开空调和关空调的不同的指令内码,在ha智能家居平台上就很容易配置了。也可以通过技术手段生成这两个内码的红外信号,让万能红外遥控器学习一下,也就可以用现成的智能音箱控制了。

软件
软件开源贴二楼,用avr-gcc编译后1020字节,把1024字节的flash空间占得满满当当的。


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册 微信登录

x
 楼主| 发表于 昨天 23:17 | 显示全部楼层
本帖最后由 maidoo 于 2025-9-16 23:28 编辑

2楼放软件



// Ir_DAKIN_IOT.c 在 AVR ATtiny13A单片机上 之编译说明
// Compiler: avr-gcc (AVR_8_bit_GNU_Toolchain_3.7.0_1796) 7.3.0
// Target : MCU=attiny13a; Crystal=1.2000Mhz (使用片内RC振荡器,出厂默认值)
// Fuse: LowByte=0x2A, HighByte=0xFF, ExtByte=0xFF, 芯片默认值基础上保留EEPROM
// 发布日期: 2025-9-3, 抗战胜利暨反法西斯战争胜利80周年大阅兵。
// copyrights, 2010~2025, maidoo@163.com  麦兜-CDMA650 欢乐在 数码之家


/* ========================================================================
        大金中央空调面板控制器,加装支持IOT场景(固开、固关)的红外遥控
   ========================================================================
    AVR ATtiny13单片机    _____   _____
            Reset  PB5 -|1    \_/    8|- VCC
       POW_AN I/O  PB3 -|2   AT      7|- PB2   POW_SEN IN
         BEEP OUT  PB4 -|3   Tiny13  6|- PB1   INT0红外输入
                   GND -|4___________5|- PB0   FAN_AN I/O


    这是个附加到大金中央空调控制面板里面的外挂电路,通过飞线连接到被控按钮的焊点上,
    通过飞线输出低电平脉冲来模拟按键,实现红外遥控特定按钮(电源开关、风力大小)的功能。
    AVR检测到飞线上的按钮按下并保持5秒钟,则启动对应飞线通道的学习过程,BEEP长鸣。
    在学习过程中,按任意键将退出学习,超过8秒没有收到有效的红外指令将结束学习过程。


    智能家居IOT场景中使用遥控开关,仅有乒乓开关的情况下,远程操控时,无法得知开关的实际状态,就无法盲操
    因此需要有固定开、固定关的指令。便于配合智能音箱遥控开、遥控关,这样可以放心地重复发送。可以盲操。
    解决办法是增加一条飞线POW_SEN到原面板电源LED指示灯上,让AVR能感知实际的开关状态。


    IOT方案是根据空调面板上电源开关飞线通道学习到的红外指令,派生出固定开和固定关的指令内码
        如学习到的红外按键的十六进制32位指令码(MSB)是 xxXXxxxx,根据第二字节XX的值派生
        XX+1的指令为按开,即 xxJJxxxx,其中JJ=XX+1,其余xx保持不变
        XX+2的指令为按关,即 xxKKxxxx,其中KK=XX+2,其余xx保持不变


    仅支持最常用的NEC格式的红外编码,每个按键指令有4字节的数据。
    NEC指令格式请参考:https://www.sbprojects.net/knowledge/ir/nec.php
    虽然NEC红外原定义是LSB格式(低位在前),但是开源IrRmote库直接使用32位无符号数表示
    一个内码(MSB高位在前)的程序代码很简洁方便,考虑到且IOT业务中常用IrRemote库解码红外指令,
    为了方便解码后的数据交流,本程序修改为MSB方式解码红外指令的每个字节。
   ======================================================================== */


#include <avr/io.h>
#include <avr/eeprom.h>
#include <util/delay_basic.h>
#include <avr/interrupt.h>
#include <string.h>


/*  --------------------------------------------------
    硬件引脚定义如有调整,请在这里修改
    红外接收器使用了INT0中断,所以只能固定在PB1,没得选。
    --------------------------------------------------  */
// 感知空调控制面板上“电源LED”状态的输入脚,带上拉,低有效
#define IO_POW_SEN              _BV(PB2)
// 飞线到空调面板上的电源按钮的IO,低有效
#define IO_POW_AN               _BV(PB3)
// 飞线到空调面板上的风力按钮的IO,低有效
#define IO_FAN_AN               _BV(PB0)
// 接个蜂鸣器,收到有效红外指令,吱一声,高有效。可用PB4
#define IO_BEEP                 _BV(PB4)


// 输出的模拟按键的个数
#define TOTAL_AN_COUNT          2
// 所有模拟按键的MASK码
#define KEYS_MASK               (IO_POW_AN | IO_FAN_AN)
// 各模拟按键对应的IO管脚,IO_POW_AN电源按钮要放到第一个
const unsigned char OutPinMask [TOTAL_AN_COUNT] = {IO_POW_AN, IO_FAN_AN};
// 模拟按钮低电平有效时的判断表达式
#define KEY_PRESSED             ( ~(PINB) & KEYS_MASK )
// 感知空调面板上的电源指示灯,判断实际状态
#define IS_POW_ON               ( ~(PINB) & IO_POW_SEN )


// 延时参数,输出脉冲宽度。1.2MHz时钟下,最大值65535理论对应218毫秒。推论:30061对应100毫秒,24050对应80毫秒
#define OUT_PULSE_WIDTH         30061
#define OUT_BEEP_WIDTH          20000
// 延时参数,影响进入学习状态前按住按钮所需的时间
// 从255递减到该值的时间  195: 约3秒钟  155: 约5秒钟
#define WAIT_LEARNING           155
// 延时参数,进入学习状态后的学习超时时间,从该值递减到零的时间,大约每秒递减20      160: 约8秒钟
#define DURLING_LEARNING        160


// 红外指令解码的状态机
typedef enum{
             IR_idle,
             IR_waitstart,
             IR_getaddrlow,
             IR_getaddrhigh,
             IR_getdata,
             IR_getdatainv
            }_IRstate;
volatile _IRstate IRstate = IR_idle;


//定义位操作
#define SET_BIT(reg,bitmask)    ( reg |=  (bitmask) )
#define CLR_BIT(reg,bitmask)    ( reg &= ~(bitmask) )
#define INV_BIT(reg,bitmask)    ( reg ^=  (bitmask) )
#define GET_BIT(reg,bitmask)    ( reg &   (bitmask) )
#define GET_PIN(bitmask)        ( PINB  &   (bitmask) )
#define OUT_HIGH(bitmask)       ( PORTB |=  (bitmask) )
#define OUT_LOW(bitmask)        ( PORTB &= ~(bitmask) )
#define OUT_INV(bitmask)        ( PORTB ^=  (bitmask) )


// 保存EEPROM的起始地址,根据datasheet描述的缺陷,不要从0地址开始使用
#define EE_MARGIN               0x10
#define START_T0                ( TCCR0B = 0x04 )
#define STOP_T0                 ( TCCR0B = 0x00 )
#define ENABLE_INT0             ( GIMSK  = 0x40 )
#define DISABLE_INT0            ( GIMSK  = 0x00 )
#define BEEP_ON                 ( OUT_HIGH(IO_BEEP) )
#define BEEP_OFF                ( OUT_LOW (IO_BEEP) )


volatile unsigned char IR_Ready;
volatile unsigned char TimerCount;


// NEC格式,每个红外指令有4个字节
typedef struct {
                unsigned char addrl;        // 地址低位
                unsigned char addrh;        // 地址高位
                unsigned char data;         // 指令码
                unsigned char datainv;      // 指令反码
                } _IRCMD;
volatile _IRCMD LearnedCMD[TOTAL_AN_COUNT];
volatile _IRCMD curCMD;


//----------------------------------------------------------------------------
// INTERUPT HANDLER (TIMER/COUNTER0 OVERFLOW)       void timer0_ovf_isr(void)
//----------------------------------------------------------------------------
ISR(TIM0_OVF_vect) {
    // 在内部默认的1.2MHz RC振荡器配置下,再256分频后,T0计满0xFF后的溢出周期约为53毫秒
    // 2011-1-15, 室温5摄氏度,实测周期为57.8毫秒
    if (! (-- TimerCount)) {
        IRstate = IR_idle;
        STOP_T0;
    }
}




// Attiny13缺省RC时钟1.2MHz,256分频后周期为206微秒,
// T0每206us计数加1,以下定义各时间段的计数值
//#define msec_15     0x48
//#define msec_12p5   0x3C
//#define msec_9      0x2B
//#define msec_2p5    0x0C
//#define msec_1p68   0x08
//#define msec_0p9    0x04


// Attiny13缺省RC时钟1.2MHz,256分频后周期为206微秒,
// T0每206us计数加1,以下定义各时间段的计数值
// 20130627, ATtiny13A比ATtiny13V的内置RC振荡器频率略低些,放宽这里的参数
// 以支持13A。正常情况下,TCNT0将采集到:
//             引导码=13.5mS; 逻辑1=2.25mS; 逻辑0=1.12mS; 重复码=11.25mS
// 以下定义各时间段的TCNT0的计数值
#define msec_15     0x48
#define msec_12p5   0x38
#define msec_9      0x2B
#define msec_2p5    0x0F
#define msec_1p68   0x08
#define msec_0p9    0x04


// 20250903,八十周年九三阅兵圆满完成。改为按照MSB格式解码,以迎合IOT中常用的开源IrRemote库的格式
// 原bits=0x01; 改为bits=0x80;    原bits=bits<<1; 改为bits=bits>>1;
//----------------------------------------------------------------------------
// INTERUPT EXTERMNAL INTERUPT 0
//----------------------------------------------------------------------------
ISR(INT0_vect) {
    static unsigned char bits;
    unsigned char time;
    switch(IRstate) {
        case IR_idle:
            TCNT0=0; START_T0; IRstate=IR_waitstart;
            TimerCount = 2;     // 最后一个脉冲后2个溢出周期(53ms)结束
            break;
        case IR_waitstart:
            // a "start of data" is 13.5Msec,a "1" is 2.25Msec,a "0" is 1.12 msec
            time=TCNT0; TCNT0=0;
            // greater than 12.5Msec & less than 15 msec = start code
            if ((time>msec_12p5)&&(time<msec_15)) {
                curCMD.addrl= curCMD.addrh= curCMD.data= curCMD.datainv= 0;
                bits=0x80; IRstate=IR_getaddrlow;
            }   else { IRstate=IR_idle;}  // too short, bad data just go to idle
            break;
        case IR_getaddrlow:
            time=TCNT0; TCNT0=0;
            // if  > 2.5msec or shorter than .9Msec bad data, go to idle
            if ((time>msec_2p5)||(time<msec_0p9)) {IRstate=IR_idle; break; }
            // MSB格式:接收到的第一个比特作为字节最高位
            if (time>msec_1p68)  curCMD.addrl|= bits;   // greater than 1.68Msec is a 1
            bits=bits>>1; if (!bits) {IRstate=IR_getaddrhigh; bits=0x80; }
            break;
        case IR_getaddrhigh:
            time=TCNT0; TCNT0=0;
            if ((time>msec_2p5)||(time<msec_0p9)) {IRstate=IR_idle; break; }
            if (time>msec_1p68)  curCMD.addrh|= bits;
            bits=bits>>1; if (!bits) {IRstate=IR_getdata; bits=0x80; }
            break;
        case IR_getdata:
            time=TCNT0; TCNT0=0;
            if ((time>msec_2p5)||(time<msec_0p9)) {IRstate=IR_idle; break; }
            if (time>msec_1p68)  curCMD.data|= bits;
            bits=bits>>1; if (!bits) {IRstate=IR_getdatainv; bits=0x80; }
            break;
        case IR_getdatainv:
            time=TCNT0; TCNT0=0;
            if ((time>msec_2p5)||(time<msec_0p9)) {IRstate=IR_idle; break; }
            if (time>msec_1p68)  curCMD.datainv|= bits;
            bits=bits>>1; if (!bits) {IR_Ready=1; IRstate=IR_idle; } // 完整的一帧数据结束
            break;
        default:
            IRstate=IR_idle;
            break;
    }
}


static void port_init(void) {
    // 蜂鸣器或者发光二极管指示灯,输出模式
    DDRB  = IO_BEEP;
    // PB1做INT0输入,电源感应教做输入,带上拉电阻;
    PORTB = _BV(PB1) | IO_POW_SEN | KEYS_MASK;
}


//TIMER0 initialize - prescale:256
// WGM: Normal
static void timer0_init(void) {
    //TCCR0B = 0x00; //stop
    STOP_T0;
    OCR0A = 0xEA;
    OCR0B = 0xEA;
    // TCNT0 = 0x16;    //set count
    TCCR0A = 0x00;
    //TCCR0B = 0x04; //start timer
}


static void init_devices(void) {
    cli();         //disable all interrupts
    port_init();
    timer0_init();
    MCUCR = 0x02;  // INT0下降沿触发
    TIMSK0 = 0x02; //timer interrupt sources
    // GIMSK = 0x40;  //interrupt sources
    ENABLE_INT0;
    sei();         //re-enable interrupts
}


void load_learned_instruction(void) {
    // void eeprom_read_block (void *__dst, const void *__src, size_t __n);
    eeprom_read_block((void *)&LearnedCMD, (void *)EE_MARGIN, sizeof(LearnedCMD));
}


void pulse_out(unsigned char Pin) {
    OUT_LOW(Pin);  SET_BIT(DDRB, Pin);  // IO口拉低,然后切换为输出状态
    _delay_loop_2(OUT_PULSE_WIDTH);
    OUT_HIGH(Pin); CLR_BIT(DDRB, Pin);  // 完事后,先拉高,在切回输入状态
}


void beep_n1(void) {                // 1短鸣叫
    BEEP_ON; _delay_loop_2(OUT_BEEP_WIDTH); BEEP_OFF; _delay_loop_2(OUT_BEEP_WIDTH);
}
void beep_m1(unsigned char x) {     // x长鸣叫
    BEEP_ON;
    for (unsigned char i=x; i; i--)
        _delay_loop_2(OUT_BEEP_WIDTH);
    BEEP_OFF;
}




//====================================================
// void __attribute__((noreturn)) main(void)
int main(void)
{
    unsigned char i, Key_State, cur_key_index = 0;
    init_devices();
    load_learned_instruction();     // 先读取学习到的指令数据
    while(1) {
        // ------------ 本机(原空调面板)按键按下 --------------------------------
        Key_State = KEY_PRESSED;
        if (Key_State) {
            DISABLE_INT0;           // 关INT0中断,暂停处理红外信号。红外中断函数会修改TimerCount,影响定时器
            TimerCount = 0xFF; START_T0;            // 启动定时器查看按钮按下的时长
            for (i=0; i<TOTAL_AN_COUNT; i++)        // 先搞清楚是按了哪个键
                if (Key_State == OutPinMask) {cur_key_index=i; break; }


            while ((KEY_PRESSED) && (WAIT_LEARNING < TimerCount));     // 循环等按键放开
            if (WAIT_LEARNING >= TimerCount) {      // 如果是长按超过5秒,开始学习过程
                TimerCount = 160;   // 设置8秒种超时,如果还没有收到任何信号,就放弃本次学习
                ENABLE_INT0;        // 开启INT0中断,等待学习红外信号,
                BEEP_ON;            // 打开蜂鸣器,开始鸣叫
                while (TimerCount) {// TimerCount由定时器递减,8秒后递减到0
                    //由于收到红外信号后会重新给TimerCount赋值2,所以无需再判断 IR_Ready 作为退出条件
                }
                if (IR_Ready) {     // 把学习到的指令数据保存到eeprom
                    IR_Ready = 0;
                    // void eeprom_write_block (const void *__src, void *__dst, size_t __n);
                    eeprom_write_block((void *)&curCMD, (void *)(EE_MARGIN + cur_key_index * sizeof(_IRCMD)), sizeof(_IRCMD));
                    load_learned_instruction();     // 更新当前RAM里的数据
                }
                BEEP_OFF;           // 关闭蜂鸣器
            }
            // 如果是短按按钮,原空调面板CPU处理,AVR不管。这里清理环境:尽快关闭定时器
            TimerCount = 1;         // 不要设置为0,防止 --TimerCount 后得 0xFF
            ENABLE_INT0;            // 开启INT0中断,允许处理红外信号
        }


        // ------------ 通常模式下,判断红外解码是否OK, ------------
        if (IR_Ready) {
            IR_Ready = 0; DISABLE_INT0;     // 暂停红外,杜绝数据被快速覆盖的可能性
            Key_State = IS_POW_ON;          // 采样保持当前空调的电源LED开关状态
            // 找到对应预学习的指令序号,来执行对应的动作
            for (i=0; i<TOTAL_AN_COUNT; i++) {
                // 找到对应的IO端口,输出低电平脉冲
/*              if ((curCMD.data  == LearnedCMD.data)  &&
                    (curCMD.addrh == LearnedCMD.addrh) &&
                    (curCMD.addrl == LearnedCMD.addrl)) {   */
                // 使用memcmp()内存比较函数,比上面的3个条件组合判断,节省至少14BYTE的FLASH空间
                if (memcmp((void*)&curCMD, (void*)&LearnedCMD, 4) == 0) {
                    BEEP_ON;
                    pulse_out(OutPinMask);       // 输出低电平脉冲,相当于按了一下面板上的按键,
                    BEEP_OFF;
                    if (0 == i) {                   // 第一个AN是电源开关,需要IOT化处理,补第二声鸣叫
                        _delay_loop_2(OUT_BEEP_WIDTH);
                        if (Key_State)  beep_m1(6); // 原来是开的,按后新状态是电源关闭,鸣叫一短一长,补一长
                           else         beep_n1();  // 按按钮后,新状态是电源开:鸣叫两短,补一短
                    }
                    break;
                }
            }
            // IOT方案,根据电源按钮派生的内码来执行固开、固关的动作
            // if ((curCMD.data  == LearnedCMD[0].data)) {
            if ((curCMD.data  == LearnedCMD[0].data) && (curCMD.addrl == LearnedCMD[0].addrl)) {
                if (curCMD.addrh == LearnedCMD[0].addrh +1) {   // 固开
                    if (! Key_State) pulse_out(OutPinMask[0]);
                    beep_n1(); beep_n1();
                }
                if (curCMD.addrh == LearnedCMD[0].addrh +2) {   // 固关
                    if (Key_State)   pulse_out(OutPinMask[0]);
                    beep_n1(); beep_m1(6);
                }
            }
            ENABLE_INT0;        // 恢复INT0,启动红外接收
        }
    }
    return 0;
}
/* end of file */









回复 支持 反对

使用道具 举报

发表于 昨天 23:40 | 显示全部楼层
给大金家用中央空调加装遥控功能
回复 支持 反对

使用道具 举报

发表于 9 小时前 | 显示全部楼层
不错不错,我用不上
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层
不错不错  技术牛
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层
不错不错  技术牛
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层
共来者借鉴
回复 支持 反对

使用道具 举报

发表于 6 分钟前 | 显示全部楼层

单片机程序很直观!
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册 微信登录

本版积分规则

APP|手机版|小黑屋|关于我们|联系我们|法律条款|技术知识分享平台

闽公网安备35020502000485号

闽ICP备2021002735号-2

GMT+8, 2025-9-17 09:32 , Processed in 0.093600 second(s), 7 queries , Gzip On, Redis On.

Powered by Discuz!

© 2006-2025 MyDigit.Net

快速回复 返回顶部 返回列表