Skip to content

[Geek之路] 自己动手做智能家居(二)智能门窗感应器的开发

2020年8月更新: 这两年对嵌入式方向的学习太少了,回过头看两年前做的东西,虽然模块连到一起通过一些简单的代码能跑起来,但没有产品化的思维,不具有实用价值。智能家居有Home Assistant这么强大的轮子,作为一个不专业人士从头自己做太不划算了,后面还会继续学习这方面的知识,慢慢摸索。但应该要有一些产品思维,把东西做成能用的,而不仅是简单能跑的。

上一篇架构设计与技术选型中, 已经完成了大致的软硬件框架设计, 这节开始记录嵌入式开发的过程.

模块引脚与电路接线

首先嵌入式设备这块有4个硬件:

  • Wifi模块: 承担数据处理和收发的重任, 整个门窗感应器设备的核心(ESP8266-01S只要9块8, 超强Wifi模块带回家)
  • 干簧管模块: 经过模转数封装的干簧管传感器, Digital Output直接输入TTL电平. 磁铁距离约1.5cm以内时输出低电平, 磁铁距离超过阈值输出高电平(只要2块钱一个, 配上一个3毛钱的钕铁硼磁铁就开袋即食)
  • 蜂鸣器模块: 当干簧管模块输出高电平时, 即门被打开时, 中间那个引脚应该输入PWM波让蜂鸣器鸣叫(买的是3引脚无源蜂鸣器, 只要8毛钱一个)

电路仿真软件Proteus里面没有Wifi模块, 也找不到3脚蜂鸣器, 这里拿纸笔画了个很不专业的线路图, 包括了每个模块的引脚接线方法. 其中ESP8266-01S有两个GPIO引脚, 作用如下:

  • GPIO0用作系统的输入, 感受来自干簧管传感器的电平
  • GPIO2用作系统的输出, 控制输出PWM波来让蜂鸣器沉默或发出不同的声音(更正一下: GPIO2应该连接三脚蜂鸣器的S端, 中间的引脚连接5V电源, 这里笔误了) circuit

固件烧录

ESP8266-01S这款神奇的元件, 8Mb的Flash, 最高160MHz的32位CPU, 功耗最低20μA, IEEE 802.11b/g/n都支持 ,甚至还能当路由器, 竟然只要10块钱不到. 文档在这里都能查到, 非常齐全. 相关的工具和SDK在这里都能下载到最新版本. 之前只烧录过Arduino的AVR单片机和51单片机, 这块神奇的板子还没试过自己写固件烧进去, 于是先依葫芦画瓢烧一个HelloWorld试试.

环境准备

第一步是通过UART连接串口到电脑上, 原料: USB转TTL模块一只(最好是FT232RL的芯片, 据说更稳定), 杜邦线若干. 接线如下:

  • ESP8266 RXD -> USB转串口模块 TXD
  • ESP8266 TXD -> USB转串口模块 RXD
  • ESP8266 GND USB转串口模块 GND -> 共地
  • ESP8266 VCC -> 3.3V电源
  • USB转串口模块 USB -> 计算机USB

这个时候用串口软件, 设置为74880的波特率, 模块上电就能看到串口输出了. 一般使用的115200波特率会导致串口看到的启动信息是乱码(貌似和ESP板子里的26M晶振有关), 115200波特率看到的启动信息大概是这样的, 这时可以给串口发送AT指令连接Wifi或是建立无线热点等等 serial

如果把GPIO0接地拉低, 重新上电或复位就能看到上图最后一行那样的乱码, 这样是进入了下载模式, 后面烧录固件的时候, 都是需要把GPIO0拉低再上电复位的.

工程编译和程序烧录

从官网下载的一体化SDK就包括了默认的很多示例项目, 工程目录的结构在下图中用红字标注了. SDK有NonOS和RTOS两种, 一般选择NonOS即可, RTOS则类似于μcOSII这样的根据时间片和任务优先级调度多任务的实时操作系统固件, 这个简单的应用还用不到. ec

先把HelloWorld工程执行Clean Project, Build Project, 编译成功后开烧. 期间也经历过一些波折, 用了技术交流群里的老版本烧录工具, 报了一些莫名奇妙的错误. 最后还是在官网找的资料和工具靠谱, 烧录成功了. 配置如下图所示, 亦可合并成一个大的hex直接从0x00烧进去.
sl

程序编写

基础环境和编译烧录都打通了, 开始撸起袖子写代码了. SDK提供的示例非常丰富, 我选择在mqtt示例的基础上开发. 相关代码放到Github中了, 地址是这里, 在官网工具页面上下载安装好一体化SDK并把mqtt示例的项目替换成Github此目录下的Eclipse项目即可.

整体的设计逻辑是这样的:

  1. 重写MQTT连接成功以及订阅事件的回调函数
  • 连接上MQTT Broker时, 发送设备上线消息
  • 订阅设备开关命令, 并根据开关调用蜂鸣器的开关函数
  1. 实现蜂鸣器开关函数
  • 蜂鸣器关, GPIO2的PWM波占空比设置为0
  • 蜂鸣器开, GPIO2的PWM波占空比设置为100
  1. 设置一个定时器, 每隔500ms检测一次干簧管传感器的电平
  • 低电平即门正常关闭状态: 重置一些状态变量
  • 高电平即门打开状态: 调用蜂鸣器开函数, 如果鸣叫超过1分钟不做处理
  1. 定时器每运行60次即30秒, 向MQTT Broker发布一条设备在线消息

下面贴一下user_main.c中的关键代码, 基本上完全的面向过程编程

c
#include "ets_sys.h"
#include "driver/uart.h"
#include "osapi.h"
#include "mqtt.h"
#include "wifi.h"
#include "pwm.h"
#include "config.h"
#include "debug.h"
#include "gpio.h"
#include "user_interface.h"
#include "mem.h"

#define INTERVAL 500
#define INIT_DELAY 10000
#define HEART_BEAT 60 //30s
#define BEEP_DURATION 120 //60s

#define PWM_PERIOD 500 //500ns
#define PWM_DUTY 100
#define PWM_IDLE 0

//传感器检测标识, 每次触发定时器自增长
LOCAL uint64_t id = 0;
LOCAL bool beeping = false;
LOCAL bool beepTimes = 0;
LOCAL bool remoteControl = false;

MQTT_Client mqttClient;
os_timer_t sensor_timer;
os_timer_t beep_timer;

//生成PWM波的GPIO2
uint32 io_info[][3] = {{PERIPHS_IO_MUX_GPIO2_U,FUNC_GPIO2,2}};
//PWM波占空比
uint32 pwm_duty_init[1] = {PWM_IDLE};

/***
 * 控制无源蜂鸣器打开和关闭
 * 在GPIO2上生成PWM, GPIO模拟PWM
 */
void beep(bool isBeep) {
  beeping = isBeep;
  if(isBeep) {
    if(beepTimes < BEEP_DURATION) {
      os_printf("PWM -> Beep \r\n");
      pwm_set_duty(PWM_DUTY, 0);
      pwm_start();
    } else {
      os_printf("PWM -> Stop Beep \r\n");
      pwm_set_duty(PWM_IDLE, 0);
      pwm_start();
    }
  } else {
    os_printf("PWM 0 -> No Beep \r\n");
    pwm_set_duty(PWM_IDLE, 0);
    pwm_start();
  }
}

/**
 * 获取数据以及处理逻辑
 * 1. 获取GPIO0电平
 * 2. 高电平触发蜂鸣器
 * 3. mqtt发送数据到服务器
 * 4. 每60次检测(30s), 触发一次心跳健康检测
 * */
void handleSensor(void) {
  id++;
  os_timer_disarm(&sensor_timer);

  //设备心跳检测
  if(id % HEART_BEAT == 0) {
    MQTT_Publish(&mqttClient, "/device/heartbeat", "DOOR_SENSOR", 11, 1, 0);
  }

  //获取GPIO0输入并判断
   uint8 sensor_res = GPIO_INPUT_GET(GPIO_ID_PIN(0));

  //门被打开(干簧管无磁)的状态
  if(sensor_res != 0) {
    beepTimes++;
    if(beeping == false && remoteControl == false) {
      os_printf("Door Opened!...\r\n");
      beep(true);
      MQTT_Publish(&mqttClient, "/device/data", "DOOR_SENSOR::door_open", 11, 1, 0);
    }
  } else {
    //若门开通过远程控制不要叫, 关门后1分钟, 远程控制失效
    //若门关状态远程控制报警, 1分钟后也失效回归平静
    if(remoteControl) {
      beepTimes++;
      if(beepTimes >= BEEP_DURATION) {
        remoteControl = false;
      }
    } else {
      beepTimes = 0;
    }

    if(beeping == true && remoteControl == false) {
      os_printf("Door Closed!...\r\n");
      beep(false);
      MQTT_Publish(&mqttClient, "/device/data", "DOOR_SENSOR::door_close", 11, 1, 0);
    }
  }
  os_timer_setfn(&sensor_timer, (os_timer_func_t *)handleSensor, NULL);

  os_timer_arm(&sensor_timer, INTERVAL, 1);
}


/**
 * 干簧管传感器数据获取定时器, 500ms触发一次
 * */
void initSensor() {

  PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0);
  os_printf("Sensor READY\r\n");

  //初次启动等待wifi和mqtt初始化完毕再检测传感器
  os_timer_disarm(&sensor_timer);
  os_timer_setfn(&sensor_timer, (os_timer_func_t *)handleSensor, NULL);
  os_timer_arm(&sensor_timer, INIT_DELAY, 1);
}


void wifiConnectCb(uint8_t status)
{
  if(status == STATION_GOT_IP){
    MQTT_Connect(&mqttClient);
  } else {
    MQTT_Disconnect(&mqttClient);
  }
}
void mqttConnectedCb(uint32_t *args)
{
  MQTT_Client* client = (MQTT_Client*)args;
  os_printf("MQTT: Connected\r\n");

  //发布设备上线注册消息
  MQTT_Publish(client, "/device/online", "DOOR_SENSOR", 11, 2, 0);
  //订阅开关命令
  MQTT_Subscribe(client, "/device/on", 1);
  MQTT_Subscribe(client, "/device/off", 1);
}

void mqttDisconnectedCb(uint32_t *args)
{
  MQTT_Client* client = (MQTT_Client*)args;
  os_printf("MQTT: Disconnected\r\n");
}

void mqttPublishedCb(uint32_t *args)
{
  MQTT_Client* client = (MQTT_Client*)args;
  os_printf("MQTT: Published\r\n");
}

void mqttDataCb(uint32_t *args, const char* topic, uint32_t topic_len, const char *data, uint32_t data_len)
{
  char *topicBuf = (char*)os_zalloc(topic_len+1),
      *dataBuf = (char*)os_zalloc(data_len+1);

  MQTT_Client* client = (MQTT_Client*)args;

  os_memcpy(topicBuf, topic, topic_len);
  topicBuf[topic_len] = 0;

  os_memcpy(dataBuf, data, data_len);
  dataBuf[data_len] = 0;

  os_printf("Receive topic: %s, data: %s \r\n", topicBuf, dataBuf);

  //订阅数据的回调, 用于远程控制
  if(os_strncmp(topicBuf, "/device/on", 10) == 0) {
    os_printf("Remote control -> on. \r\n");
    remoteControl = true;
    beep(true);
  }
  if(os_strncmp(topicBuf, "/device/off", 11) == 0) {
    os_printf("Remote control -> off. \r\n");
    remoteControl = true;
    beep(false);
  }

  os_free(topicBuf);
  os_free(dataBuf);
}


/******************************************************************************
 * FunctionName : user_rf_cal_sector_set
 * Description  : SDK just reversed 4 sectors, used for rf init data and paramters.
 *                We add this function to force users to set rf cal sector, since
 *                we don't know which sector is free in user's application.
 *                sector map for last several sectors : ABCCC
 *                A : rf cal
 *                B : rf init data
 *                C : sdk parameters
 * Parameters   : none
 * Returns      : rf cal sector
 *******************************************************************************/
uint32 ICACHE_FLASH_ATTR
user_rf_cal_sector_set(void)
{
    enum flash_size_map size_map = system_get_flash_size_map();
    uint32 rf_cal_sec = 0;
    
    switch (size_map) {
        case FLASH_SIZE_4M_MAP_256_256:
            rf_cal_sec = 128 - 5;
            break;
            
        case FLASH_SIZE_8M_MAP_512_512:
            rf_cal_sec = 256 - 5;
            break;
            
        case FLASH_SIZE_16M_MAP_512_512:
        case FLASH_SIZE_16M_MAP_1024_1024:
            rf_cal_sec = 512 - 5;
            break;
            
        case FLASH_SIZE_32M_MAP_512_512:
        case FLASH_SIZE_32M_MAP_1024_1024:
            rf_cal_sec = 1024 - 5;
            break;
            
        default:
            rf_cal_sec = 0;
            break;
    }
    
    return rf_cal_sec;
}


void user_init(void)
{
  uart_init(BIT_RATE_115200, BIT_RATE_115200);
  os_delay_us(1000000);

  CFG_Load();

  MQTT_InitConnection(&mqttClient, sysCfg.mqtt_host, sysCfg.mqtt_port, sysCfg.security);
  MQTT_InitClient(&mqttClient, sysCfg.device_id, sysCfg.mqtt_user, sysCfg.mqtt_pass, sysCfg.mqtt_keepalive, 1);

  MQTT_InitLWT(&mqttClient, "/lwt", "offline", 0, 0);
  MQTT_OnConnected(&mqttClient, mqttConnectedCb);
  MQTT_OnDisconnected(&mqttClient, mqttDisconnectedCb);
  MQTT_OnPublished(&mqttClient, mqttPublishedCb);
  MQTT_OnData(&mqttClient, mqttDataCb);

  WIFI_Connect(sysCfg.sta_ssid, sysCfg.sta_pwd, wifiConnectCb);

  os_printf("\r\nSystem started ...\r\n");

  //初始化PWM
  pwm_init(PWM_PERIOD,  pwm_duty_init , 1 ,io_info);
  set_pwm_debug_en(0);
  pwm_start();
  os_printf("PWM START\r\n");

  //初始化MQTT client, 发布设备注册消息, 订阅设备开关命令消息及回调
  //GPIO0 监听干簧管电位, 高电位触发GPIO2
  //GPIO2 生成PWM激活蜂鸣器
  //MQTT client 发布警告消息通知MQTT broker
  initSensor();
}

这个代码是调试过的版本了, 按照设计的接线和这些代码编译的固件, 实现了预期功能. 磁铁一碰上就会滴滴滴~的叫, 远程一条MQTT消息就可以控制蜂鸣器, 挺有意思的.

好久没写C语言了, 写起来感觉距离机器很近. 一次又一次接线、烧录、调试结束了, 还因为VCC-GND接反报废了一只干簧管, 终于完成嵌入式的部分了, 下节准备记录下后端开发.