使用ATtiny13A制作以纽扣电池供电的顶棚灯遥控器【开发篇】

日本語|English|中国语
| 15 min read
Author: shuji-morimoto shuji-morimotoの画像
Information

为了覆盖更广泛的受众,这篇文章已从日语翻译而来。
您可以在这里找到原始版本。

上一篇的文章请看【準備編】

到目前为止,前期准备终于完成。从这里开始进入正题,进行电路设计和程序开发。

自制红外遥控器的目标

#

目标如下:

  • 尽可能集成遥控器(HK9493)的功能
  • 纽扣电池(3V)供电
  • 省电
  • 降低零件成本

遥控器操作按钮规格

#

根据【準備編】中对红外线发送数据的解析得到,按钮对应的发送数据(命令)如下:

命令数组 ch1 ch2 ch3 长按
暖色 0x9139522C(0xA8) 0x9539522C(0xAC) 0x9939522C(0xA0)
白色 0x9039522C(0xA9) 0x9439522C(0xAD) 0x9839522C(0xA1)
点亮 0x2D09522C(0x24) 0x3509522C(0x3C) 0x3D09522C(0x34)
熄灯 0x2F09522C(0x26) 0x3709522C(0x3E) 0x3F09522C(0x36)
明亮 0x2A09522C(0x23) 0x3209522C(0x3B) 0x3A09522C(0x33)
暗淡 0x2B09522C(0x22) 0x3309522C(0x3A) 0x3B09522C(0x32)
夜灯 0x2E09522C(0x27) 0x3609522C(0x3F) 0x3E09522C(0x37)
全灯 0x2C09522C(0x25) 0x3409522C(0x3D) 0x3C09522C(0x35)
晚安30分钟 0xA139522C(0x98) 0xAA39522C(0x93) 0xB339522C(0x8A)
频道确定 0xDA39522C(0xE3) 0xDB39522C(0xE2) 0xDC39522C(0xE5)

(0xNN)是奇偶校验数据,长按表示该命令支持长按,通过持续按下按钮可实现调光。

共有3个频道,通过按对应的频道确定按钮,可以只接受该频道的命令。

电路图

#

电路图

左侧有8个用于遥控控制的开关(SW),通过中央模块(编码器)连接到ATtiny13A。
虽然红外LED需要流过50mA到100mA的大电流,但由于ATtiny13A无法直接输出大电流,因此采用了晶体管驱动。
由于开关数量较多,且通过中央模块连接到ATtiny13A,所以布线较多。

开关的ON/OFF

#

电路图左侧排列有8个开关,这些正是遥控器中用于点亮、熄灯等功能的按钮。
对于初学者(我也曾是其中的一员)来说,可能会对这种电路设计感到疑惑,因此我展示了类似的三种电路。你能看出当SW开/关时OUT电压的区别吗?

pull_up

左侧
当SW打开时,电源电压3V施加于OUT,电流随之流动;当SW关闭时,OUT并非实际为0V,而是处于未连接的悬浮状态,状态不确定。这种情况称为“浮动状态”或“高阻抗”,是不允许出现的,必须确保输出固定为0V或保持在施加电压状态。

中间
当SW关闭时,电源电压3V施加于OUT,OUT上会流过依电源电压与电阻R决定的电流(V/R);当SW打开时,OUT变为0V。
如果没有电阻R,电源的正负极会直接短路,造成大电流和短路故障。
而加入电阻R后,由于电流不易流动,当电阻值较大(10K至数MΩ)时,几乎不流电,从而避免了短路。在这种情况下,电阻R的正极侧为3V,负极侧为0V,所以OUT保持在0V。
这种电阻称为上拉电阻,此电路称为上拉电路。

右侧
这是将中间电路中电阻R和SW的位置调换所得。
当SW关闭时,OUT连接到电源的负极,因此为0V。
当SW打开时,与先前相同,电阻R的正极侧为3V,负极侧为0V,因此OUT变为3V。
这种电阻称为下拉电阻,此电路称为下拉电路。

电路图中的所有按钮均为上拉电路。

引脚数量问题

#

在ATtiny13A中,作为输入的引脚共有PB0到PB5共6个,但PB5兼作复位并在程序写入时使用,所以不予使用。
因此(除去1个RESET、4个GND和8个VCC)仅可使用5个引脚,其中1个用于红外发送时LED点亮的输出,故实际上只有4个输入引脚。

需要用4个引脚判断8个开关中哪个被按下[1],为此我考虑了以下几种方法。

A: 使用AD转换
将其中1根引脚设置为模拟输入模式,通过各开关接入不同阻值后测量电压值,并依据该电压判断按下了哪个开关。

B: 利用编码器
将8个输入线路接入编码器,通过3根或4根输出线路以二进制(3或4位数字)的形式进行判断。

C: 利用IO扩展器
将8个输入线路接入IO扩展器,ATtiny13A通过I2C(2根线)与IO扩展器通信,从而判断按下了哪个开关。

尝试方法A后发现,由于AD转换为8位,可将最大3V的输入电压转换为0到255的值,但由于开关有8个,需要利用不同阻值产生8个电压区间。设定各个阈值区间既繁琐又精度不足,容易误判,因此放弃此法。

方法B可利用SN74HC148N编码器电路实现,并且所需接入ATtiny13A的引脚数也正合适。

方法C由于需要编写I2C通信程序,既脱离主题又会占用较大程序空间,因此判断不切实际。

所以,最终采用了方法B。

Information

IO扩展器正如其名,是用于扩展(增加)输入输出引脚数量的电路。

Information

I2C (Inter Integrated Circuit) 是一种只需2根线(数据线和时钟线)即可与多个模块通信的协议。通常用于与液晶显示器、简单传感器等设备通信。发音为“爱斯克威尔德西”或“爱二西”。

查看SN74HC148N的数据手册(规格书)[2],输入与输出的对应关系如下表所示。

function table

从浅蓝色框中,通过观察INPUTS 0到7的高/低电平和OUTPUTS A0到A2的高/低电平,可以判断出哪个开关被按下[3]

但是,当仅INPUTS的0为低电平(最后一行)和所有INPUTS均为高(第二行)时,OUTPUTS A0到A2均为相同的高电平状态。这样,无论是对应INPUTS的0的开关被按下,还是没有任何开关按下,OUTPUTS都相同,无法判断。

因此,如红色框所示,作为INPUTS额外加入了EI(但始终为低电平),使得输入线路总数达到9根,同时在OUTPUTS中添加GS,使输出线路增加到4根。
这样,输出便以4位二进制表示出9种不同的模式。
当OUTPUTS的EO为低电平时,表示没有任何开关被按下,从而也可以据此判断,不过无论如何,输出仍然需要4根线路。

另外,输入引脚在无输入时必须保持为高电平(即低有效),因此需要预先上拉(前述: 开关的ON/OFF)。

可能会有人质疑:“那干脆用一个一开始就带有8个引脚可用的单片机不就好了?”但我想做点用ATtiny13A制作的东西,所以请理解为“这也是可以实现的”😅。

旁路电容

#

电容(C)即使不装也可能能工作,但具有以下作用:

  • 缓和电压变化(降噪)
  • 在电流突增时提供电力
Information

在电源(V)与地(GND)之间并联电容,可以旁通过滤噪声,因此称为旁路电容(简称为パスコン)。

程序在稳态下几乎不耗电(低于0.1uA)(后述),但一旦按下遥控器按钮,红外LED便会点亮。红外虽然肉眼不可见,但会像普通LED一样发光。
由于用于遥控器,需要发出比普通LED更强的光以保证信号能传达到几米之外的红外接收器,因此需要流过约50到100mA的电流[4]。这就会导致电流骤增、电压下降。

在电池供电的情况下,最初使用新电池时无碍,但随着电量消耗,电压会逐渐降低。当电压降至接近单片机最低工作电压(ATtiny13A为1.8V)时,单片机可能会运行不稳定。但加装旁路电容可提供缓冲,使得电池能更长久(更高效)地使用。

程序

#

程序总行数约为280行。

  • 头文件include、define定义、数据数组定义约90行
  • setup()约90行
  • loop()约70行

控制逻辑(setup(), loop(), 以及其他函数)超过100行,但每段处理均有注释说明,并用大括号分块,寄存器设置中的位运算也逐位换行,因此尽管代码行数较多,但其内容十分简单。

#include <avr/io.h>
#include <util/delay.h>
#include <util/delay_basic.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <wiring_private.h>
#include <avr/pgmspace.h>
#include <avr/eeprom.h>

#define IR_COMMAND_WARMER   0 // 暖色
#define IR_COMMAND_WHITER   1 // 白色
#define IR_COMMAND_ON       2 // 点亮
#define IR_COMMAND_OFF      3 // 熄灯
#define IR_COMMAND_BRIGHTER 4 // 明亮
#define IR_COMMAND_DARKER   5 // 暗淡
#define IR_COMMAND_NIGHT    6 // 夜灯
#define IR_COMMAND_FULL     7 // 全灯
#define IR_COMMAND_NONE    15 // 无

#define HEADERMARK       1035 // 8T(3450微秒)
#define HEADERSPACE       510 // 4T(1700微秒)
#define DATAMARK          135 // 1T(450微秒)
#define ONESPACE          390 // 3T(1300微秒)
#define ZEROSPACE         135 // 1T(450微秒)

#define COMMAND_SIZE  5

// 照明控制
const byte command_1ch_0[] PROGMEM = {0x91, 0x39, 0x52, 0x2C, 1};
const byte command_1ch_1[] PROGMEM = {0x90, 0x39, 0x52, 0x2C, 1};
const byte command_1ch_2[] PROGMEM = {0x2D, 0x09, 0x52, 0x2C, 0};
const byte command_1ch_3[] PROGMEM = {0x2F, 0x09, 0x52, 0x2C, 0};
const byte command_1ch_4[] PROGMEM = {0x2A, 0x09, 0x52, 0x2C, 1};
const byte command_1ch_5[] PROGMEM = {0x2B, 0x09, 0x52, 0x2C, 1};
const byte command_1ch_6[] PROGMEM = {0x2E, 0x09, 0x52, 0x2C, 0};
const byte command_1ch_7[] PROGMEM = {0x2C, 0x09, 0x52, 0x2C, 0};
const byte command_2ch_0[] PROGMEM = {0x95, 0x39, 0x52, 0x2C, 1};
const byte command_2ch_1[] PROGMEM = {0x94, 0x39, 0x52, 0x2C, 1};
const byte command_2ch_2[] PROGMEM = {0x35, 0x09, 0x52, 0x2C, 0};
const byte command_2ch_3[] PROGMEM = {0x37, 0x09, 0x52, 0x2C, 0};
const byte command_2ch_4[] PROGMEM = {0x32, 0x09, 0x52, 0x2C, 1};
const byte command_2ch_5[] PROGMEM = {0x33, 0x09, 0x52, 0x2C, 1};
const byte command_2ch_6[] PROGMEM = {0x36, 0x09, 0x52, 0x2C, 0};
const byte command_2ch_7[] PROGMEM = {0x34, 0x09, 0x52, 0x2C, 0};
const byte command_3ch_0[] PROGMEM = {0x99, 0x39, 0x52, 0x2C, 1};
const byte command_3ch_1[] PROGMEM = {0x98, 0x39, 0x52, 0x2C, 1};
const byte command_3ch_2[] PROGMEM = {0x3D, 0x09, 0x52, 0x2C, 0};
const byte command_3ch_3[] PROGMEM = {0x3F, 0x09, 0x52, 0x2C, 0};
const byte command_3ch_4[] PROGMEM = {0x3A, 0x09, 0x52, 0x2C, 1};
const byte command_3ch_5[] PROGMEM = {0x3B, 0x09, 0x52, 0x2C, 1};
const byte command_3ch_6[] PROGMEM = {0x3E, 0x09, 0x52, 0x2C, 0};
const byte command_3ch_7[] PROGMEM = {0x3C, 0x09, 0x52, 0x2C, 0};

// 频道确定
const byte command_channel_1[] PROGMEM = {0xDA, 0x39, 0x52, 0x2C, 0};
const byte command_channel_2[] PROGMEM = {0xDB, 0x39, 0x52, 0x2C, 0};
const byte command_channel_3[] PROGMEM = {0xDC, 0x39, 0x52, 0x2C, 0};

// 晚安30分钟
const byte command_sleep30_1[] PROGMEM = {0xA1, 0x39, 0x52, 0x2C, 0};
const byte command_sleep30_2[] PROGMEM = {0xAA, 0x39, 0x52, 0x2C, 0};
const byte command_sleep30_3[] PROGMEM = {0xB3, 0x39, 0x52, 0x2C, 0};

// 红外线发送数据列表
const byte * const commands[] PROGMEM = {
    command_1ch_0,
    command_1ch_1,
    command_1ch_2,
    command_1ch_3,
    command_1ch_4,
    command_1ch_5,
    command_1ch_6,
    command_1ch_7,
    command_2ch_0,
    command_2ch_1,
    command_2ch_2,
    command_2ch_3,
    command_2ch_4,
    command_2ch_5,
    command_2ch_6,
    command_2ch_7,
    command_3ch_0,
    command_3ch_1,
    command_3ch_2,
    command_3ch_3,
    command_3ch_4,
    command_3ch_5,
    command_3ch_6,
    command_3ch_7,
    command_channel_1,
    command_channel_2,
    command_channel_3,
    command_sleep30_1,
    command_sleep30_2,
    command_sleep30_3,
};

// 用于频道切换的偏移量
byte command_offset = 0;

void setup() {
    // 端口设置
    {
        sbi(DDRB, DDB1);

        sbi(PORTB, PORTB0);
        sbi(PORTB, PORTB2);
        sbi(PORTB, PORTB3);
        sbi(PORTB, PORTB4);
    }

    // 定时器设置 
    {
        TCCR0A =
            (0 << COM0A1)
          | (0 << COM0A0)
          | (0 << COM0B1)
          | (0 << COM0B0)
          | (1 << WGM01)
          | (1 << WGM00)
          ; 

        TCCR0B =
            (0 << FOC0A)
          | (0 << FOC0B)
          | (1 << WGM02)
          | (0 << CS02)
          | (0 << CS01)
          | (1 << CS00)
          ;

        OCR0A = 31;
        OCR0B = 11;
        TIMSK0 = 0;
    }

    // 省电设置
    {
        sbi(PRR, PRADC);
    }

    // 频道设置的读写及频道设置的发送
    {
        uint8_t eeprom_address = 0x00;
        uint8_t read_data = eeprom_read_byte(&eeprom_address);

        if (read_data == 0 || read_data == 8 || read_data == 16) {
            command_offset = read_data;
        } else {
            command_offset = 0;
            eeprom_update_byte(&eeprom_address, command_offset);
        }

        _delay_ms(20);

        byte command_no = get_command_no();

        if (command_no == IR_COMMAND_ON) {
            read_data = 0;
            command_no = 0;
        } else if (command_no == IR_COMMAND_FULL) {
            read_data = 8;
            command_no = 1;
        } else if (command_no == IR_COMMAND_NIGHT) {
            read_data = 16;
            command_no = 2;
        } else {
            read_data = command_offset;
        }

        if (read_data != command_offset) {
            command_offset = read_data;
            eeprom_update_byte(&eeprom_address, command_offset);

            byte buffer[COMMAND_SIZE] = {0};
            command_offset = 24;
            get_command_data(command_no, buffer);
            ir_send(buffer);
            command_offset = read_data;
        }
    }

    // 中断设置与睡眠设置 
    cli();
    {
        sbi(GIMSK, PCIE);

        sbi(PCMSK, PCINT0);
        sbi(PCMSK, PCINT2);
        sbi(PCMSK, PCINT3);
        sbi(PCMSK, PCINT4);

        set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    }
    sei();
}

void loop() {
    sleep_mode();

    _delay_ms(20);

    byte command_no;
    byte buffer[COMMAND_SIZE] = {0};
    byte *longPress = &buffer[4];

    do {
        if (*longPress) {
            _delay_ms(100);
        }

        command_no = get_command_no();

        if (command_no == IR_COMMAND_NONE) {
            return;
        }

        get_command_data(command_no, buffer);
        ir_send(buffer);
    } while (*longPress);
}

// 获取命令号
byte get_command_no() {
    return
          ((bit_is_set(PINB, PINB4) ? 1 : 0) << 3)
        | ((bit_is_set(PINB, PINB2) ? 1 : 0) << 2)
        | ((bit_is_set(PINB, PINB0) ? 1 : 0) << 1)
        | ((bit_is_set(PINB, PINB3) ? 1 : 0) << 0)
        ;
}

// 获取指定命令号的红外线发送数据
void get_command_data(byte command_no, byte *out) {
    memcpy_P(out, (byte *)pgm_read_byte(
                &commands[command_offset + command_no]), COMMAND_SIZE);
}

// 红外线发送
void ir_send(byte *command) {
    // 生成数据部分
    uint32_t data =
          ((uint32_t)command[0] << 24)
        | ((uint32_t)command[1] << 16)
        | ((uint32_t)command[2] <<  8)
        | ((uint32_t)command[3] <<  0)
        ;

    // 奇偶校验计算
    byte parity = command[0] ^ command[1];

    // 发送引导部分
    sbi(TCCR0A, COM0B1);
    _delay_loop_2(HEADERMARK);
    cbi(TCCR0A, COM0B1);
    _delay_loop_2(HEADERSPACE);

    // 发送数据部分
    for (uint8_t i = 0; i < 32; i++) {
        sbi(TCCR0A, COM0B1);
        _delay_loop_2(DATAMARK);
        cbi(TCCR0A, COM0B1);
        _delay_loop_2((data>>i) & 1 ? ONESPACE : ZEROSPACE);
    }
    
    // 发送奇偶校验
    for (uint8_t i = 0; i < 8; i++) {
        sbi(TCCR0A, COM0B1);
        _delay_loop_2(DATAMARK);
        cbi(TCCR0A, COM0B1);
        _delay_loop_2((parity>>i) & 1 ? ONESPACE : ZEROSPACE);
    }

    // 发送尾部
    sbi(TCCR0A, COM0B1);
    _delay_loop_2(DATAMARK);
    cbi(TCCR0A, COM0B1);
    _delay_loop_2(ZEROSPACE);
}

编译(保险丝)设置

#

为了实现省电,修改了以下设置:

  • 禁用BOD (Brown Out Detection)
  • 将时钟频率改为1.2MHz

禁用BOD
电源电压为3V,但当电压低于单片机的工作电压(ATtiny13A为1.8V)时,可能会引起误操作。BOD是一种检测电压下降并向单片机施加复位使其停止工作的机制。
可能在单片机发生误操作前,就因电压不足(LED点亮所需的正向电压不足)而无法进行红外发送。而且禁用BOD后功耗大幅降低,对省电贡献显著。

更改时钟频率
基本上,AVR单片机的时钟频率越低,其能在更低电压下运行。
另外,虽然默认的时钟频率为9.6MHz,但由于红外发送仅需要38KHz调制以及约450微秒延时(等待处理)的精度,所以不需要高频。
定时器依赖于时钟频率,因此在生成38KHz调制信号及计算延时时需做相应计算,但为了简化处理,便采用了网络上常用的1.2MHz。

数据声明

#

在包含头文件与常量定义后,各个命令数据以数组形式定义。
用于控制照明的命令,每个命令为4字节(不含奇偶校验),共30个命令,因此数据总计为120字节。
如前所述,SRAM仅有64字节,完全不足,因此必须加以巧妙处理。

const byte command_1ch_0[] PROGMEM = {0x91, 0x39, 0x52, 0x2C, 1};

数组最后一个元素若为1则表示该命令支持长按,否则为0,以此判断是否为长按命令。

PROGMEM 是Arduino草图中的特殊关键字,用于将数据存储在Flash内存(程序存储区)而非SRAM中。由此可以定义超出SRAM容量的数据。虽然这增加了程序体积,但仍能勉强控制在Flash内存容量以内。

编译时日志

最大1024字节的Flash内存中,草图使用了972字节(94%)。
最大64字节的RAM中,全局变量占用1字节(1%),局部变量可用63字节。

setup() 处理

#

这里进行寄存器设置。由于需要参考数据手册了解各寄存器的配置,仅凭代码可能难以完全理解,所以这里只作简要说明。

使用sbi(寄存器, 位)将指定寄存器的指定位置为1;使用cbi(寄存器, 位)则将其置为0。

端口设置

  • 由于红外LED连接在PB1(pin6)上,因此将该端口设为输出模式。
  • 连接到编码器的8个按钮在关闭时输出为HIGH(低有效),故将PB0、PB2、PB3、PB4的输出均设为HIGH。

定时器设置
用于生成38KHz调制信号(38KHz交替ON/OFF的信号)的设置:

  • 将波形生成模式设为高速PWM
  • OCR0A = 31,由公式 ((1.2MHz / 38KHz) / 1(分频)) - 1 = 30.6 计算得出
  • OCR0B = 11,使占空比为1/3,由 (1.2MHz / 38KHz) / 3 = 10.5 得出

38KHz调制波形的生成由单片机定时器完成,因此在程序(loop函数中)只需指定PB1(pin6)的输出状态,控制非常简洁。

下面的图片为使用简易示波器实际测得的波形。虽然测得的矩形波为36.7KHz而非38KHz,但在这种误差范围内红外发送完全没有问题。

省电设置
这里设置了降低A/D转换器功耗的选项,但似乎仅在Active状态或使用set_sleep_mode(SLEEP_MODE_IDLE)时有效,因此可能并非必需。本程序指定的睡眠模式为set_sleep_mode(SLEEP_MODE_PWR_DOWN)。

红外线发送频道设置的读写
遥控器具有频道切换(3档)和确定按钮。原本计划只在初次设置后便不再使用,因此一开始打算不实现此功能。但因程序区域有余,遂实现了该功能。不过,由于ATtiny13A的所有端口均已占用,故无可用引脚。经过反复斟酌,决定在通电(安装纽扣电池)且调用setup函数时,根据当时按下的按钮类型来进行频道设置。

按钮 频道设置
未按下时 保存的频道
点亮按钮 1ch
夜灯按钮 2ch
全灯按钮 3ch

频道设置写入EEPROM中,因此即使更换纽扣电池,也能恢复先前设定的频道。

中断设置与睡眠设置

  • 将中断类型设置为当引脚状态发生变化时触发中断
  • 对编码器的输出(PB0, PB2, PB3, PB4)启用引脚变化中断
  • 将睡眠模式设定为SLEEP_MODE_PWR_DOWN

睡眠模式共有SLEEP_MODE_IDLE、SLEEP_MODE_ADC、SLEEP_MODE_PWR_DOWN三种,其工作方式如下图所示:

设定为SLEEP_MODE_PWR_DOWN时,所有时钟停止,发射电路亦停止,单片机进入深度睡眠状态,除监测中断外一切活动均停止。因而功耗降至0.1uA以下,几乎不再消耗电力。
唤醒方式仅有以下几种:

  • INT0外部中断或引脚变化中断
  • 看门狗定时器中断

loop() 处理

#

循环处理
这是程序的主循环。

  1. 通过sleep_mode()函数进入睡眠状态,直到按钮被按下程序才会唤醒
  2. 当按钮按下并通过引脚变化中断唤醒后,为防止抖动延时20毫秒
  3. 确定被按下的按钮,获取对应命令数据并执行红外发送(使红外LED闪烁)
  4. 若按下的按钮为支持长按的按钮,则每隔100毫秒重复相同处理
  5. 退出loop()函数后会立即重新调用loop(),回到第1步
Information

抖动是指按钮或开关在开启/关闭时因机械振动而产生短暂的多次开闭现象。如果利用引脚变化中断触发中断,则一次按下可能会引发多次中断。

获取命令号
将编码器的输出(PB0, PB2, PB3, PB4)通过位移运算转换为4位数字并返回,该数字即为命令号。

获取指定命令号的红外线发送数据
数据声明时,命令数据存储在Flash内存中,此处从Flash内存中获取。

红外线发送

  1. 从命令数据数组生成发送数据
      通过位移运算生成32位数据
  2. 奇偶校验计算
      奇偶校验通过对数据代码执行异或(xor) data[0] ^ data[1]计算
  3. 发送引导部分
      先发送ON状态8T,再转为OFF并等待4T
  4. 发送数据部分
      对命令数据右移,检查最低位以决定发送时的空闲等待时间
  5. 发送奇偶校验
      对奇偶校验数据,同样执行与引导部分相同的处理
  6. 发送尾部
      理论上OFF状态至少需8ms,但下述设置亦可正常工作

根据IRremote库测量结果,T=450微秒。为测量此时间,使用ATTiny13A中定义于util/delay_basic.h_delay_loop_2(loop_count)函数。
根据数据手册,_delay_loop_2(1)消耗4个CPU周期。当CPU频率(F_CPU)为1.2MHz时,则1,200,000[Hz] / 4[周期] = 300,000,即300,000次循环约等于1秒。
由于参数为16位计数器(最大可达65536),故当传入30000时,_delay_loop_2(30000)约等于延时100毫秒。

由此,若要延时1T=450微秒,则应使用_delay_loop_2(135)。求出8T、4T、3T时,loop_count的值如下:

#define headerMark  1035 // 8T(3450微秒):3450*30000/100000 = 345*3 = 1035
#define headerSpace  510 // 4T(1700微秒):1700*30000/100000 = 170*3 =  510
#define dataMark     135 // 1T(450微秒) : 450*30000/100000    45*3 =  135
#define oneSpace     390 // 3T(1300微秒):1300*30000/100000 = 130*3 =  390
#define zeroSpace    135 // 1T(450微秒) : 450*30000/100000 =  45*3 =  135

总结

#

经过反复试验,实现了目标所要求的充分利用单片机功能、省电且低成本的设计,并使得loop()函数编写得异常简洁。唯一遗憾的是“晚安30分钟”命令尚未能调用,计划在未来的机会中挑战此功能。
下一篇将是【基板・ケース作成編】


  1. 未考虑多个开关同时按下的情况 ↩︎

  2. SN74HC148N的数据手册。在秋月电子售价仅30日元,非常便宜 ↩︎

  3. X的值与判断无关 ↩︎

  4. 实际上,由于红外信号以38KHz脉冲形式反复ON/OFF,因此并非持续点亮,而是以高速闪烁。 ↩︎

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。