|
本帖最后由 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 */
|
|