制作一款用于座位管理的墨水屏

更新时间Invalid date

概况

现在电子墨水屏的概念越来越火,与之关联的电子标签,电子公交站牌等产品也如雨后春笋一样涌出。墨水屏的优点很多,比如无蓝光的护眼效果,接近 180 度的可视角度,阳光下可视,类纸张的质感,有极低的功耗,可以免布线使用,显示非常稳定可靠。

本 Demo 基于墨水屏,设计了一款低功耗座位管理器。管理器支持联网管理、拥有超长续航能力,可联网管理、展示清晰,用户无需布线。后期调整以后即可应用于各种场景的显示终端。

步骤

  • 第 1 步:硬件设计

    整体架构方案如图

    主控板

    主控板选择由涂鸦智能出品的 TYDE-ZTU-MCU-L431 三明治开发板。

    详细原理图和 PCB 请参考 主控板完整原理图主控板完整 PCB

    主控芯片

    TYDE-ZTU-MCU-L431 的主控芯片选用 STM32L431CCT6。这是一款基于 Arm Cortex-M4 32 位 RISC 核心的超低功耗微控制器,工作频率可达 80 MHz。Cortex-M4 核心具有浮点单元 (FPU) 单精度,支持所有 Arm® 单精度数据处理指令和数据类型。它还实现了一套完整的 DSP 指令和内存保护单元 (MPU),提高了应用的安全性。

    STM32L431CCT6 设备嵌入高速存储器(闪存高达 256 kbyte, 64 kbyte 的 SRAM)有四种保护机制:读出保护、写保护、专有代码读出保护和防火墙。

    STM32L431CCT6 还具备一个快速 12 位 ADC (5 Msps)、两个比较器、一个运算放大器、两个 DAC 通、一个内部电压参考缓冲器、一个低功耗 RTC、一个通用 32 位定时器、一个专用于电机控制的 16 位 PWM 定时器、四个通用 16 位定时器,和两个 16 位低功耗定时器。此外,还有多达 21 个电容感测通道可用,功能非常强大。

    MCU

    详情请查看 STM32L431 数据手册

    通讯模组

    通讯模组的功能是获取 涂鸦 IoT 开发平台 下发的数据信息,并将该信息传递给主控 MCU,MCU 也可以也通过通信模组,将数据上报到云端。

    涂鸦提供的支持多种通信协议、多种尺寸规格、多种工作温度、多种焊接方式的一系列超高性价比自研模组,广泛应用于各种产品类型和开发方式,开发者可根据自身的产品需求进行灵活选择。

    由于本次有低功耗的需求,建议开发者选择一些支持低功耗模式的模组,比如 Bluetooth Low Energy,Zigbee 等,并且根据所选模组,适当修改一下电路。

    供电方式

    开发板具有两种供电方式,分别是 USB 和电池供电,且两种供电方式只能二选一。

    方式一

    第一种方式是由 USB 供电。USB 提供的 5V 电压,一部分给 U1 CH340C 串口芯片供电,另外一部分通过 U3 BL1117 LDO,将 5V 电压转化为 3.3V 电压,给单片机和模组供电。

    电路

    优点

    这种方式适用于调试阶段,由于有 CH340C 串口芯片,用户插上 USB,借助串口调试助手,可以直接和 MCU 进行通讯。

    缺点
    U1 和 U3 为非低功耗器件,漏电流较大,因此正常工作时,整机功耗较高。

    方式二

    第二种方式是由一颗 CR2450 扣式纽扣电池通过 D2 二极管,直接给单片机和模组供电。

    优点

    • 这种方式电路较为简单,整机器件的静态功耗只有几微安,完全满足低功耗设备的要求。

    • CR2450 纽扣电池是锂锰环保纽扣式电池,其储存寿命长、产品性能稳定可靠、自放电率低、抗漏液性强。2450 代表着电池的标准尺寸, 24 表示直径是 24mm, 5 代表电池的高度为 5mm,0 代表电池形状为圆柱。新电池开路电压通常在 3.3V 左右,容量大概为 520mAh。

    电路

    bat

    这两路供电方式可以通过 P4 处的跳线帽进行切换,如下图,其中 P_3.3V 为 USB 方式供电,B_3V 为纽扣电池供电。

    exchange

    功能板

    功能板主要有两个核心器件。

    • 字库芯片:为开发者提供所需中文、英文或者数字的像素数组。
    • 墨水屏:将像素数组的数据写入墨水屏后,墨水屏即可显示开发者想要的画面。字库芯片和墨水屏使用的都是 SPI 接口。

    功能板的详情请参考 功能板完整原理图功能板完整 PCB

    字库芯片

    本方案采用 GT5SLAD3B-FA 标准矢量字库芯片,开发者也可以选用其他字库芯片。

    GT5SLAD3B-FA 是一款支持 16-192 点阵的矢量字库芯片,使用灰度算法使汉字边缘变得比纯点阵字更加柔和、平滑。采用高通超小型嵌入式矢量字库,具有字体平滑及不失真等特点,可以产生多种高质量的文字输出,可以支持加粗、倾斜、反白、阴影等文字特效,支持 GBK 中文,GB18030 中文,ASCII 码,拉丁文这几种编码。

    应用原理图如下图:

    具体引脚功能为:

    引脚 功能
    串行数据输出(SO) 该引脚用来把数据从芯片串行输出,数据在时钟的下降沿移出。
    串行数据输入(SI) 该引脚用来把数据从串行输入芯片,数据在时钟的上升沿移入。
    串行时钟输入(SCLK) 数据在时钟上升沿移入,在下降沿移出。
    片选输入(CS#) 所有串行数据传输开始于 CS#下降沿,CS#在传输期间必须保持为低电平,在两条指令之间保持为高电平。
    总线挂起输入(HOLD#) 该信号用于片选信号有效期间暂停数据传输,在总线挂起期间,串行数据输出信号处于高阻态,芯片不对串行数据输入信号和串行时钟信号进行响应。
    • 当 HOLD# 信号变为低并且串行时钟信号(SCLK)处于低电平时,进入总线挂起状态。
    • 当 HOLD# 信号变为高并时串行时钟信号(SCLK)处于低电平时,结束总线挂起状态。

    优点
    字库芯片的使用也比较方便。在此之前,开发者通常是使用文字取模软件进行取模,然后存储在程序数组里,再拿去显示,当只显示一些特定的字符或文字时,这是一个不错的方式,但是当需要显示的字符比较多时,这种方法就不太适合了,这时候字库芯片的优势就体现出来了。

    字库芯片厂家有提供对应的 .lib 静态库文件,这种静态库文件, 是由 c 源文件经过与开发者相似的环境进行编译得到的,使用方式和 c 文件一致,把 lib 文件和对应的 h 头文件添加到开发者的开发项目工程里即可,根据头文件定义的函数接口,进行调用。

    开发者只需要输入所需文字的对应字符编码(比如 GB18030 编码),文字宽度,文字高度,文字粗细这几个参数,就可以获取该文字的点阵数组,再将点阵数组数据传递显示函数中,就可以将文字显示出来了,非常简单。详情可查看 GT5SLAD3B-FA 数据手册 中的固件开发章节。

    墨水屏

    电子墨水屏又被称为电子纸显示技术。电子纸显示技术(简称 EPD),由美国麻省理工大学教授约瑟夫.雅各布森及其研发团队,经历 30 余年研发成功。

    原理

    电子墨水屏是由许多电子墨水组成,电子墨水可以看成一个个胶囊的样子。每一个胶囊里面有液体电荷,其中正电荷染白色,负电荷染黑色。当我们在一侧给予正负电压,带有电荷的液体就会被分别吸引和排斥。这样,每一个像素点就可以显示白色或者黑色了。

    因为电子墨水的刷新是不连续的,每一次刷新完成就可以保持现在的图形,即使拔掉电池也依旧保存。电子墨水具有双稳态磁滞效应,所以即便电池没电,小球也不会回复原状或者进入随机的混沌状态,而是保持最后画面的状态,此时耗电量为 0。

    参数:

    型号 QYEG0420BNS19A 备注
    外形尺寸 91.0 x77.0x 1.2mm /
    显示区域尺寸 84.8 x 63.6mm /
    分辨率 400 x 300 /
    支持显示颜色 黑色、白色 /
    模式 全屏刷新、局部刷新 两种刷新模式区别如下:
    • 全刷:电子纸刷新需要画面闪烁多次后,最终显示所需要的画面,其中闪烁的目的是清除显示残影,以达到最佳的显示效果。
    • 局刷:电子纸刷新无画面闪烁,局刷需要用户在使用的时候,刷新几次后,进行一次全刷操作,以清除显示残影。
    刷新功耗 12.6mW /
    温度范围 0~50℃ 避免阳光长时间直射显示屏表面
    湿度范围 35%~65%RH /

    特性:

    • 内置驱动器 IC,无需另配驱动器,仅需少量外围器件,即可通过 MCU 控制显示,节省资源

    • 超宽视角,将近 180°

    • 超低功耗(断电可以继续保持显示内容)

    • 纯反射模式

    • 防眩硬涂层前表面

    • 低电流深度睡眠模式

    • 采用 COG 封装,IC 厚度 300um

    • 使用寿命(无故障刷新次数):100 万次以上

    电路原理图:

    EPD_SCH

    主要引脚功能如下

    名称 描述
    GDR N 勾道场效应管的栅极驱动控制脚
    RESE 控制回路的电流检测输入脚
    VSH2 正源极驱动电压
    TSCL I2C 数字温度传感器的时钟信号接口
    TSDA I2C 数字温度传感器的数据信号接口
    BS1 总线接口选择引脚
    BUSY 繁忙状态输出引脚
    RES# 复位信号输入脚, 低电平有效
    D/C# 数据/命令控制引脚
    CS# 芯片片选引脚
    SCL SPI 接口的时钟引脚
    SDA SPI 接口的数据引脚
    VDDIO 逻辑接口的电源引脚, 应与 VCI 脚连接
    VCI 芯片电源引脚
    VSS 参考地
    VDD 核心逻辑电源引脚
    VPP 测试脚 , 保持开路
    VSH1 正源极驱动电压
    VGH 正门极驱动电压和 VSH1 的电源引脚
    VSL 负源极驱动电压
    VGL 负门极驱动电压,VCOM 和 VSL 的电源引脚
    VCOM VCOM 驱动电压

    翻阅数据手册如下,可知源极驱动电压 VSH 引脚和门极驱动电压 VGH 引脚电压典型值都远大于芯片电源引脚,因此对该引脚还需要进行一个升压的处理。

    如原理图所示,3.3V 输入源,电感 L1,MOS 管 Q1,电容 C1,二极管 D3,电阻 R2 构成了一个最基本的 boost 升压电路,MOS 管 Q1 的导通或截止状态,由 E_GDR 控制。

    • 当 MOS 管 Q1 导通时,输入电压经过电感 L1 后直接通过限流电阻 R2 返回到 GND,这导致通过电感 L1 的电流线性的增大,此时输出滤波电容 C1 向负载放电。

    • 当 MOS 管 Q1 截止时,由于电感 L1 的电流不能在瞬间发生突变,因此在电感 L1 上产生反向电动势 Vs 以维持通过电流不变。此时二极管 D3 导通,3.3V 和 Vs 两电压串联后,以超过 3.3V 大小的电压向负载供电,并对输出滤波电容 C1 充电,如此循环,由此实现对 E_PREVGH 引脚的升压操作。

    同样的,对于 E_PREVGL 引脚,

    • 当 MOS 管 Q1 截止时,电容 C2 充电,二极管 D1 导通,D2 截止,电流经过 D1 流向 GND,理想情况下电容 C2 两端的电压差为 3.3V+Vs。

    • 当 MOS 管 Q1 导通时,Q1 的漏极接近 0V,由于电容 C2 电压不能突变,可认为二极管 D2 的 K 极电势为-(3.3V+Vs),电容 C2 放电,二极管 D1 截止,D2 导通,电流经过 D2 流向 C2,由此实现对 E_PREVGL 引脚负电压“升压”操作。

    墨水屏的使用方法

    墨水屏的操作流程整体也是比较简单的,大致流程如下。

    注意:

    • 墨水屏刷新频率建议开发刷新时间间隔至少 180 秒(支持局刷功能的产品除外)。
    • 墨水屏若有在低功耗场景中使用需求,如果显示画面不经常刷新,建议开发者将墨水屏设置为睡眠模式或者将墨水屏驱动供电部分通过模拟开关断开,这样操作既可以降低功耗,同时也可以延长墨水屏的使用寿命。
    • 使用场所要求:墨水屏显示屏建议在室内使用,若在户外使用,需避免墨水屏长时间阳光直射,同时做好紫外线防护措施。开发者在设计产品的时候,要首先考虑使用环境是否满足墨水屏正常工作的温湿度要求。
      详情请查看 QYEG0420BNS19A 数据手册

  • 第 2 步:整机搭建

    单片机与模组部分

    98361067.jpg

    开发板与硬件模块部分

    字库芯片与墨水屏幕部分:

    zk+epd

    设备整体图:

    80f54dd.jpg
  • 第 3 步:创建产品并搭建开发环境

    在涂鸦 IoT 开发平台上创建产品

    硬件部分搭建完成后,我们就可以在 涂鸦智能 IoT 平台 创建产品了,下面是创建墨水屏 Demo 的流程步骤。

    1. 登录 涂鸦 IoT 开发平台

    2. 在平台首页,单击 创建产品

    3. 在左侧 标准类目 导航栏中,选择 电工 > 智能开关 > 开关
      9F71.tmp.jpg

    4. 选择 自定义方案

    5. 完善产品信息,填写产品名称、产品型号(选填)、通信协议、功耗类型。可以选择 Zigbee 或蓝牙通讯协议,本方案以 Zigbee 为例进行介绍。

    6. 单击 创建产品

    7. (可选)在功能定义选项中,可以先选择一项标准功能 开关,方便后续调试。

    8. 自定义功能 区域单击添加功能,例如本次方案需要添加五个 DP 点,按照如下类别依次新建 5 个自定义功能点。

    9. 设备面板 页面中选择想要的面板,开始调试时可选择开发调试面板,后面可根据自身需要自由配置面板。

    10. 在硬件开发页面中,开发者根据需求选择对应 Zigbee 模组或蓝牙模组, 需注意的是要选择低功耗固件,否则会对设备功耗有很大影响。

    11. 上述操作完成之后,单击 下载全部 下载所有开发需要用到的资料,如 MCU SDK、模组调试助手和和功能点调试文件等,接下来就可以开始嵌入式程序的开发。

    搭建开发环境

    本次方案采用涂鸦 MCU SDK+Zigbee 模组的设计来进行开发,下面是搭建开发环境的流程步骤。

    1. ST 开发环境安装
      官方下载并安装 keil,完成后开始搭建开发工程。

    2. MCU SDK 获取

      1. 涂鸦 IoT 开发平台上创建产品后就可以获取到 MCU SDK 软件包。将 MCU SDK 添加到开发工程中,单击 Build 并根据软件提示修改相关错误或者警告信息。

      2. 移植了 MCU-SDK 后,再搭配一块烧录并授权了通用对接低功耗固件的 Zigbee 或蓝牙模组,此时 MCU 就具备了连接涂鸦云和上报下发 DP 点的功能。

      3. 待程序编译通过之后,就可以下载到开发板中进行调试和测试。开发者可以先使用开发包中的串口调试助手分别模拟 MCU 和模拟模组来验证二者是否正常工作或通信。调试模组助手使用方法,见 调试模组助手

    STM32 支持 ST-Link ,J-Link 等工具下载,这里推荐 ST-Link,引脚连接方式如下:

    ST-Link STM32F103
    SWCLK SWCLK
    SWDIO SWDIO
    NRST NRST
    VDD VDD
    GND GND
  • 第 4 步:嵌入式开发

    完成上述步骤后正式开始墨水屏 Demo 的应用功能开发。

    方案设计

    功能需求

    功能描述 详细说明
    屏幕显示
  • 座位号
  • 座位状态
  • 提醒信息
  • 二维码
  • 复位按键 长按 3 秒,设备重新配网
    信息下发
  • 正常:预定/无预定
  • 异常:暂不开放
  • 预约信息记录 后台记录近 30 天座位占用信息
    低电量报警 设备电池电量低于 10%上报至管理端(云端)

    流程逻辑

    lc

    工程目录结构

    ├── User
    │   ├── main.c                                             /* 主程序入口文件 */
    │   └── MY_ST_config.h                                     /* 硬件配置文件 */
    ├── system                                                 /* 系统文件目录 */
    │   ├── delay.c                                            /* 延时函数 */
    │   ├── delay.h
    │   ├── EPAPER.c                                           /* 电子屏幕初始化 */
    │   ├── EPAPER.h
    │   ├── GT5SLAD3BFA_stm32l431_keil5.lib                    /* 字库芯片静态库文件 */
    │   ├── GT5SLAD3B-GTA6401.h                                /* 字库芯片头文件 */
    │   ├── IO.c                                               /* GPIO 口初始化 */
    │   ├── IO.h
    │   ├── key.c                                              /* 按键初始化 */
    │   ├── key.h
    │   ├── picture.h                                          /* 图片像素数据存储 */
    │   ├── RCC.c                                              /* 系统时钟配置 */
    │   ├── RCC.h
    │   ├── SPI.c                                              /* SPI 初始化 */
    │   ├── SPI.h
    │   ├── sys.c                                              /* 系统任务文件 */                 
    │   ├── sys.h
    │   ├── qrcode_create.c                                    /* 二维码组件 */
    │   ├── qrcode_create.h
    │   ├── tuya_qrcode_create.c                               
    │   ├── tuya_qrcode_create.h
    │   ├── USART.c                                            /* 串口初始化 */
    │   ├── USART.h
    │   ├── utf8ToUnicode.c                                    /* UTF8 转 UNICODE */
    │   └── utf8ToUnicode.h
    ├── CJSON
    │   ├── cJSON.c                                            /* JSON 配置文件 */
    │   └── cJSON.h
    ├── mcu_sdk
    │   ├── mcu_api.c                                          /* dp 功能数据文件 */
    │   ├── mcu_api.h
    │   ├── protocol.c                                         /* 协议分析和接收模块发送消息时的响应 */
    │   ├── protocol.h
    │   ├── system.c                                           /* 单片机与 zigbee 通信的框架分析 */
    │   ├── system.h
    │   └── zigbee.h                                           /* SDK 中使用的宏定义 */
    
    
    

    硬件外设

    首先是硬件部分的配置,本次硬件部分主要在于墨水屏幕的显示和字库芯片的读取,字库芯片与屏使用同一块 SPI 驱动,所以首先将 SPI 进行初始化配置,另外 MCU 与 Zigbee 模组通过串口进行通信,所以也要将串口进行初始化。

    //SPI
    void SPI_Init(void)
    {
    	SPI_SCK_OUT;
    	SPI_MOSI_OUT;
    	SPI_MISO_IN;
    	LIB_CS_OUT;
    	EPD_CS_OUT;
    	EPD_DC_OUT;
    	EPD_RST_OUT;
    	EPD_BUSY_IN;
    }
    
    //UART2  通信串口
    void Configure_USART_ZIGBEE(uint32_t bound) //PA2 MTX , PA3 MRX
    {
    	RCC->APB1RSTR1 &=~(1<<17);
    	RCC->AHB2ENR |= 1<<0;
    	GPIOA->MODER &=~(3<<4|3<<6);        
    	GPIOA->MODER |=2<<4|2<<6;  
    	GPIOA->AFR[0] &=~(0xf<<8|0xf<<12);
        GPIOA->AFR[0] |=7<<8|7<<12;
    	RCC->APB1ENR1 |=1<<17;
    	USART_ZIGBEE->BRR = 16000000 / bound; 
    	USART_ZIGBEE->CR1 |= 1<<0|1<<2|1<<3|1<<5;
    	NVIC_SetPriority(USART2_IRQn, 1);
    	NVIC_EnableIRQ(USART2_IRQn);	
    }
    
    //USART3   log 串口
    void Configure_USART_LOG(uint32_t bound)    //PB10 TXD   PB11 RXD   
    {
    	RCC->APB1RSTR1 &=~(1<<18);              //恢复串口 3
    	RCC->AHB2ENR |= 1<<1;                   //使能 GPIOB 时钟
    	GPIOB->MODER &=~(3<<20|3<<22);     
    	GPIOB->MODER |=2<<20|2<<22;       
    	GPIOB->AFR[1] &=~(0xf<<8|0xf<<12);
    	GPIOB->AFR[1] |=7<<8|7<<12;
    	RCC->APB1ENR1 |=1<<18;                     //使能串口 3 时钟
    	USART_LOG->BRR = 16000000 / bound; 
    	USART_LOG->CR1 |= 1<<0|1<<2|1<<3|1<<5;  
    	while((USART_LOG->ISR & 1<<6) != 1<<6)
    	{ 
    		break;/* add time out here for a robust application */
    	}	
    	NVIC_SetPriority(USART3_IRQn, 1);
    	NVIC_EnableIRQ(USART3_IRQn);	
    }
    

    墨水屏部分

    下面是电子屏的显示部分,屏幕初始化完成后可以直接在主程序中调用显示函数,另外如果 SPI 初始化有问题可以将模拟 SPI 改为硬件 SPI 再进行驱动。

    屏幕显示也可以显示图片,采用取模软件将图片里的像素点数据放进 picture.h 文件中就可以显示图片。

    注意

    • 注意输出图片大小不能超过屏幕的尺寸大小,屏幕显示完成后调用局刷与全刷函数可以实现屏幕的局部刷新与全局刷新。
    • 注意屏幕每次刷新完后需要进入休眠模式,否则会对设备的整机功耗造成较大影响。

    • 屏幕刷新函数(全刷/局刷)

      屏幕刷新函数 描述 优点 缺点
      全屏刷新 整个页面全部刷新一次,整个屏幕要闪几次 没有残影 需要多刷几下屏幕
      局部刷新 每一次刷新显示内容时,不会整个屏幕都刷新,仅刷新有画面和字的地方 优势是屏幕不会闪烁 会有残影,但残影问题多刷几次白屏就能清除掉或者执行一次全刷也可以清除

      注意:在实现墨水屏的全局刷新与局部刷新功能时, 从局刷转到全刷时休眠后一定要先进入初始化再刷新。

      // refresh_mode = Full      全屏刷新
      // refresh_mode = Partial   局部刷新
      void EPD_HW_Init(const unsigned char refresh_mode)
      {
      	EPD_W21_Init();						
      	Epaper_READBUSY();
      	Epaper_Write_Command(0x12); 
      	Epaper_READBUSY();
      	Epaper_Write_Command(0x01);   
      	Epaper_Write_Data(0x2B);
      	Epaper_Write_Data(0x01);
      	Epaper_Write_Data(0x00);
      	Epaper_Write_Command(0x11);    
      	Epaper_Write_Data(0x03);	
      	Epaper_Write_Command(0x44);
      	Epaper_Write_Data(0x00);		
      	Epaper_Write_Data(0x31);   
      	Epaper_Write_Command(0x45);        
      	Epaper_Write_Data(0x00);   	
      	Epaper_Write_Data(0x00);
      	Epaper_Write_Data(0x2B);	
      	Epaper_Write_Data(0x01); 
      	Epaper_Write_Command(0x3C); 
      	Epaper_Write_Data(0x01);	
      	Epaper_Write_Command(0x18);
      	Epaper_Write_Data(0x80);	  
      	Epaper_Write_Command(0x22); 
      		if(refresh_mode==Full)
      			Epaper_Write_Data(0xB1);  
      		if(refresh_mode==Partial)
      			Epaper_Write_Data(0xB9); 
      	Epaper_Write_Command(0x20); 
      	Epaper_READBUSY(); 	
      	Epaper_Write_Command(0x4E);  
      	Epaper_Write_Data(0x00);
      	Epaper_Write_Command(0x4F);  
      	Epaper_Write_Data(0x00);
      	Epaper_Write_Data(0x00);
      	Epaper_READBUSY();	
      }
      
    • 屏幕显示函数

      //mode==POS , 正显
      //mode==NEG , 负显
      //mode==OFF , 清除
      void EPD_Dis_Part(unsigned int xstart,unsigned int ystart,const unsigned char * datas,unsigned int PART_LINE,unsigned int PART_COLUMN,unsigned char mode)
      {
      	unsigned int i;  
      	int xend,ystart_H,ystart_L,yend,yend_H,yend_L;
      	xstart=xstart/8;                   //转换为字节
      	xend=xstart+PART_LINE/8-1; 
      	ystart_H=ystart/256;
      	ystart_L=ystart%256;
      	yend=ystart+PART_COLUMN-1;
      		yend_H=yend/256;
      		yend_L=yend%256;		
      	Epaper_Write_Command(0x44);       // set RAM x address start/end
      	Epaper_Write_Data(xstart);    	  // RAM x address start;
      	Epaper_Write_Data(xend);    	  // RAM x address end
      	Epaper_Write_Command(0x45);       // set RAM y address start/end
      	Epaper_Write_Data(ystart_L);      // RAM y address start Low
      	Epaper_Write_Data(ystart_H);      // RAM y address start High
      	Epaper_Write_Data(yend_L);    	  // RAM y address end Low
      	Epaper_Write_Data(yend_H);    	  // RAM y address end High
      	Epaper_Write_Command(0x4E);   	  // set RAM x address count
      	Epaper_Write_Data(xstart); 
      	Epaper_Write_Command(0x4F);   	  // set RAM y address count
      	Epaper_Write_Data(ystart_L);
      	Epaper_Write_Data(ystart_H);
      	Epaper_Write_Command(0x24);   //Write Black and White image to RAM
      		for(i=0;i<PART_COLUMN*PART_LINE/8;i++)
      			{                         
      				if (mode==POS)
      					{
      						Epaper_Write_Data(*datas);
      						datas++;
      					}
      				if (mode==NEG)
      					{
      						Epaper_Write_Data(~*datas);
      						datas++;
      					}	
      			if (mode==OFF)
      				{
      						Epaper_Write_Data(0xFF);
      					}		
      			} 	
      }
      
    • 屏幕刷白屏函数

      void EPD_WhiteScreen_White(void)
      
      {
      unsigned int k;
      	Epaper_Write_Command(0x24);          //write RAM for black(0)/white (1)   36
      		for(k=0;k<ALLSCREEN_GRAGHBYTES;k++)
      		{
      			Epaper_Write_Data(0xff);
      		}
      	
      	Epaper_Write_Command(0x26);          //write RAM for black(0)/white (1)
      		for(k=0;k<ALLSCREEN_GRAGHBYTES;k++)
      		{
      			Epaper_Write_Data(0xff);
      		}	
      		EPD_Update();
      }
      
    • 硬件复位函数

      当需要清掉局部信息并重新显示新的信息时就可以用硬件复位函数来实现;

      void EPD_W21_Init(void)
      {
      	EPD_W21_RST_0;     
      	driver_delay_xms(100); 
      	EPD_W21_RST_1; 			//hard reset  
      	driver_delay_xms(100); 
      }
      
    • 屏幕休眠函数

      void EPD_DeepSleep(void)
      {  	
      Epaper_Write_Command(0x10);  //enter deep sleep
      Epaper_Write_Data(0x01); 
      driver_delay_xms(100);
      }
      

      注意:屏幕刷新完后必须进入休眠模式。

    字库芯片

    在使用字库芯片时,用户只要知道字符的内码,就可以计算出该字符点阵在芯片中的地址,然后就可从该地址连续读出点阵信息用于显示。本次屏幕显示信息主要通过从字库芯片中读取数据从而显示在屏幕上,从字库中读到的数据一个字符就是一个数组数据,然后电子屏再将每一位数组数据显示在屏幕上。

    • 字库芯片初始化

      void zk_init(void)
      {
      Rom_csH;
      MOSIH;
      Rom_sckH;
      }
      
    • 字库芯片休眠

      void GT5S_DeepSleep(void)
      { 
      	Rom_csL; 	
      	Send_Byte(0xB9);
      	Rom_csH;
      	delay_us(40);
      }
      
    • 字库芯片唤醒

      以低电平作为起始位,高电平作为停止位;

      void GT5S_Wakeup(void)
      { 
      	Rom_csL; 	
      	Send_Byte(0xAB);
      	Rom_csH;
      	delay_us(40);
      }
      
    • 矢量文字读取函数

      调用方式:通过指定参数进行调用,获取点阵数据到 pBits[]数组中

      /*
      	*函数名	get_font()
      	*功能		矢量文字读取函数
      	*参数:pBits		数据存储
      	*		sty		  文字字体选择 @矢量公用部分
      	*		fontCode	字符编码中文:GB18030, ASCII/外文: unicode
      	*		width		文字宽度
      	*		height		文字高度
      	*		thick		文字粗细
      	*返回值:文字显示高度
      **/
      unsigned int get_font(unsigned char *pBits,unsigned char sty,unsigned long fontCode,unsigned char width,unsigned char height, unsigned char thick);
      

      如果有需要固定显示的文字部分,可在程序中将固定文字信息写在数组中从而直接显示固定信息,一个 ASCII 码对应 2 位 GBK 内码占一个字节,一个中文字符对应 4 位 GBK 内码占两个字节。

      unsigned char jtwb[128]="共享空间,预定优先";	        
      unsigned char state[128]="暂不开放";	
      unsigned char pBits[512]; 
      

      将固定信息通过文字读取函数 get_font()与屏幕显示函数 EPD_Dis_Part()进一步显示在墨水屏上。

      void SEAT_SET(void)
      {
      	EPD_HW_Init(Partial);
      	zk_init();
      	GT5S_Wakeup(); 
      	get_font(pBits,VEC_SONG_STY,(jtwb[0]<<8)+jtwb[1],24,24,24);  //共
      	EPD_Dis_Part(196,55,pBits,24,24,NEG); 
      	get_font(pBits,VEC_SONG_STY,(jtwb[2]<<8)+jtwb[3],24,24,24);  //享
      	EPD_Dis_Part(220,55,pBits,24,24,NEG);
      	get_font(pBits,VEC_SONG_STY,(jtwb[4]<<8)+jtwb[5],24,24,24);  //空
      	EPD_Dis_Part(244,55,pBits,24,24,NEG);
      	get_font(pBits,VEC_SONG_STY,(jtwb[6]<<8)+jtwb[7],24,24,24);  //间
      	EPD_Dis_Part(268,55,pBits,24,24,NEG);
      	EPD_Part_Update();
      	EPD_DeepSleep();
      	GT5S_DeepSleep();
      }
      

    设备配网

    网关配网

    由于墨水屏需要连接到 涂鸦 IoT 开发平台,依靠平台来实现自动化、App 端操控,以及设备之间的相互联动,所以需要有 Zigbee 网关来帮助设备连接上智能平台。Zigbee 网关的作用就是负责连接智能平台,间接地把 Zigbee 设备接入我们的智能平台,确保手机和 Zigbee 网关处于同一个 WiFi 网络,以保证手机与智能网关之间有效连接。

    1. 将网关与电源连接,并通过网线与家庭 2.4GHz 频段路由器相连。
    2. 确认配网指示灯(绿灯)常亮(若指示灯处于其他状态,长按复位键,至绿灯常亮)。
    3. 确保手机连接家庭 2.4GHz 频段路由器,此时手机、网关处于同一个局域网。
    4. 打开涂鸦智能 APP 首页,点击页面右上角添加按钮 +
    5. 选择网关中控/有线网关(zigbee),依照提示操作设备入网。
    6. 添加成功后,即可在列表中找到网关设备。

    Zigbee 模组配网

    Zigbee 网关配网成功后,即可在网关里添加子设备。依据涂鸦智能 App 首页配网提示操作子设备配网,配网成功后即可在涂鸦智能 App 上进行调试。

    除了在程序里写入配网指令实现配网,还可以通过涂鸦调试助手发送配网指令来操作设备入网。更多详情,请查看 Zigbee 串口协议

    注意:配网之前必须先在 涂鸦 IoT 开发平台 添加 DP,否则配网会一直失败。

    DP 数据链路处理

    定义一个结构体数组来存放 MCU 接收网关下发或上报的 DP value。

    TYPE_BUFFER_S FlashBuffer;
    typedef struct {
      uint8_t seat_set[255];     //座位信息
      uint8_t st_qrcode[255];    //座位二维码链接地址
      uint8_t st_qrcode1[255];   //第一条座位二维码链接地址
      uint8_t st_qrcode2[255];   //第二条座位二维码链接地址
      uint8_t st_add[255];       //增量预约信息起始时间
      uint8_t et_add[255];       //增量预约信息终止时间
      uint8_t n_add[255];        //增量预约者姓名
      uint8_t st_all[255];       //全量预约信息起始时间
      uint8_t et_all[255];       //全量预约信息终止时间
      uint8_t n_all[255];        //全量预约者姓名
      uint8_t low_power;         //低电量数值
    } TYPE_BUFFER_S;
    

    座位信息更新

    调试过程可使用调试面板来下发 DP,MCU 收到指令后回复并执行对应的操作。

    • 座位信息设置

      1. 下发 DP101 ,DP 数据格式为{“n”:“seat number”,“st”:“state”}
      DP ID 内容 备注
      101 “query” 设备发送,同步座位号信息
      格式 {“n”:“seat number”,“st”:“state”} n:座位编码字段
      seat number:编码内容
      st:座位状态字段
      state:座位真实状态 (enable disable repairing)

      下发 DP 后 MCU 接收并解析出座位编码、座位状态等字段信息,然后驱动屏幕将座位编号信息显示在墨水屏上;

      1. 定义一个结构体,用来表示墨水屏的三种工作状态:
      typedef enum
      {
      	enable,     //使用中
      	disable,    //未开放
      	reparing    //维修中
      }state;
      

      DP 解析及处理函数:

      /*****************************************************************************
      函数名称 : dp_download_seat_set_handle
      功能描述 : 针对 DPID_SEAT_SET 的处理函数
      输入参数 : value:数据源数据
      		: length:数据长度
      返回参数 : 成功返回:SUCCESS/失败返回:ERROR
      使用说明 : 可下发可上报类型,需要在处理完数据后上报处理结果至 app
      *****************************************************************************/
      static unsigned char dp_download_seat_set_handle(const unsigned char value[], unsigned short length)
      {
      	//示例:当前 DP 类型为 STRING
      	unsigned char ret;
      	unsigned char dp_download_seat_set_handle = NULL; 
      	cJSON *root = NULL, *item = NULL, *item1 = NULL;
      	char *number = NULL;
      	char *state = NULL; 
      	char *pstr = NULL;
      	pstr = (char*)malloc(length+1);
      	if(NULL == pstr){
      		printf("malloc err\r\n");
      		return ERROR;
      	}
      	my_memset(pstr, 0x00, length+1);
      	my_memcpy(pstr, value, length);
      	printf("value: %s\r\n", value);
      	root = cJSON_Parse(pstr);
      	free(pstr), pstr = NULL;
      	if(NULL == root){							
      		printf("root ERROR\r\n");
      		return ERROR;
      	}
      	item = cJSON_GetObjectItem(root, "n");          //获取座位编码字段
      	if(NULL == item){			 				
      		printf("item ERROR\r\n");
      	}
      	number = (char*)malloc(sizeof(item));
      	if(NULL == number){
      		printf("malloc number err\r\n");
      		return ERROR;
      	}
      	free(number), number = NULL;
      	number = item->valuestring;
      	printf("number: %s\r\n", number);
      	memcpy(FlashBuffer.seat_set,number,strlen(number));
      	item1 = cJSON_GetObjectItem(root, "st");           //获取座位实际状态
      	if(NULL == item1){						    	
      		printf("item1 ERROR\r\n");
      		return ERROR;
      	}
      	state = (char*)malloc(sizeof(item1));            
      	if(NULL == state){
      		printf("malloc state err\r\n");
      		return ERROR;
      	}
      	free(state), state = NULL;
      	state = item1->valuestring;	
      	printf("state: %s\r\n", state);      
      	if(0 == strcmp(state, "enable")){				    //使用中	
      		SEAT_SET();                                     //显示座位详细信息
      	}	
      	else if(0 == strcmp(state, "disable")){				 //未开放
      		SEAT_CLOSE();                                    //显示“暂不开放”
      	}	
      		else if(0 == strcmp(state, "repairing")){		 //维修中 
      		SEAT_CLOSE();                                    //显示“暂不开放”
      	}	
      	cJSON_Delete(root);
      	return 0;
      	//处理完 DP 数据后应有反馈
      	ret = mcu_dp_string_update(DPID_SEAT_SET,value, length);
      	if(ret == SUCCESS)
      	{
      		Enter_stop_mode();     //处理完 dp 后设备需进入休眠模式
      		return SUCCESS;
      	}
      	else
      		return ERROR;
      }
      

      以座位 S-1234 为例,当座位状态为 enable 时才可以执行预约的动作,如果是 disable 和 repairing 则显示该座位暂不开放(可根据自己需求修改)。

      座位状态 DP 格式 显示内容
      enable {“n”:“S-1234”,“st”:"enable "} 显示座位编号,若有预约信息则显示预约信息,无预约信息显示空白
      disable {“n”:“S-1234”,“st”:"disable "} 显示座位编号以及“暂不开放”
      repairing {“n”:“S-1234”,“st”:"repairing "} 显示座位编号以及“暂不开放”

      MCU 接收到 value 后将解析出的信息存放在 FlashBuffer.seat_set[] 中并通过修改显示函数EPD_Dis_Part() 中的横纵坐标来调整在屏幕中的位置。

      注意:显示函数完成后,屏幕和字库芯片都需要进入休眠。

      座位编号显示函数:

      void SEAT_SET(void)
      {
      	EPD_HW_Init(Partial);            //屏幕局刷初始化
      	zk_init();                       //字库芯片初始化
      	GT5S_Wakeup();                   //字库芯片唤醒
      	get_font(pBits,VEC_FT_ASCII_STY, FlashBuffer.seat_set[0],48,48,48);     //下发信息
      	EPD_Dis_Part(8,45,pBits,48,48,NEG);  
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.seat_set[1],48,48,48);    
      	EPD_Dis_Part(40,45,pBits,48,48,NEG);
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.seat_set[2],48,48,48);  
      	EPD_Dis_Part(64,45,pBits,48,48,NEG);		           
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.seat_set[3],48,48,48);
      	EPD_Dis_Part(88,45,pBits,48,48,NEG);	 
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.seat_set[4],48,48,48); 
      	EPD_Dis_Part(114,45,pBits,48,48,NEG);	            
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.seat_set[5],48,48,48);
      	EPD_Dis_Part(136,45,pBits,48,48,NEG);	            
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.seat_set[6],48,48,48);   
      	EPD_Dis_Part(160,45,pBits,48,48,NEG);	            	
      	get_font(pBits,VEC_SONG_STY,(jtwb[0]<<8)+jtwb[1],24,24,24);               //固定信息
      	EPD_Dis_Part(196,55,pBits,24,24,NEG); 
      	get_font(pBits,VEC_SONG_STY,(jtwb[2]<<8)+jtwb[3],24,24,24);  
      	EPD_Dis_Part(220,55,pBits,24,24,NEG);
      	get_font(pBits,VEC_SONG_STY,(jtwb[4]<<8)+jtwb[5],24,24,24);  
      	EPD_Dis_Part(244,55,pBits,24,24,NEG);
      	get_font(pBits,VEC_SONG_STY,(jtwb[6]<<8)+jtwb[7],24,24,24);  
      	EPD_Dis_Part(268,55,pBits,24,24,NEG);
      	get_font(pBits,VEC_SONG_STY,(jtwb[8]<<8)+jtwb[9],24,24,24);  
      	EPD_Dis_Part(292,55,pBits,24,24,NEG); 
      	get_font(pBits,VEC_SONG_STY,(jtwb[10]<<8)+jtwb[11],24,24,24);  
      	EPD_Dis_Part(310,55,pBits,24,24,NEG);
      	get_font(pBits,VEC_SONG_STY,(jtwb[12]<<8)+jtwb[13],24,24,24);  
      	EPD_Dis_Part(330,55,pBits,24,24,NEG);
      	get_font(pBits,VEC_SONG_STY,(jtwb[14]<<8)+jtwb[15],24,24,24);
      	EPD_Dis_Part(354,55,pBits,24,24,NEG);
      	get_font(pBits,VEC_SONG_STY,(jtwb[16]<<8)+jtwb[17],24,24,24);
      	EPD_Dis_Part(380,55,pBits,24,24,NEG);
      	EPD_Part_Update();
      	EPD_DeepSleep();                           //屏幕休眠
      	GT5S_DeepSleep();                          //字库芯片睡眠
      }
      
    • 座位二维码链接

      可通过调试面板下发 DP102 生成对应座位二维码信息,DP 存放字节长度最大为 62 字节,若 URL 超过了最大限长,可将二维码链接分为两段 DP 来下发;这里以长度为 80 的 URL 为例,单条 DP 可以不固定长度,但不能超过限长,开发者可以按照自身的长度自行分配分两段还是三段。

      DPID 内容 备注
      102 “query” 设备发送,同步座位链接信息
      格式 {“qr”:“url”,“d”:“th”,“a”:“all”} qr:二维码链接字段;url:座位链接长度不超过 255 设备通过 url 本地生成二维码
      d:二维码条目字段
      th:第几段二维码链接信息
      a:二维码字段总量
      all:共有几段二维码

      示例:座位 1111 URL 地址为 https://seat-reservation-mobile.tuyacn.com/weapp?a=1&seat_id=1435857125099298816

      故 DP 格式: {“qr”:“https://seat-reservation-mobile.tuyacn.com”,“d”:“1”,“a”:“2”}
      {“qr”:"/weapp?a=1&seat_id=1435857125099298816",“d”:“2”,“a”:“2”}

      下发 DP 102 后 MCU 接收并解析出 URL 并存放在 FlashBuffer.st_qrcode 中,进一步调用二维码生成函数将座位二维码显示在墨水屏上。

      DP 解析及处理函数:

      /*****************************************************************************
      函数名称 : dp_download_st_qrcode_handle
      功能描述 : 针对 DPID_ST_QRCODE 的处理函数
      输入参数 : value:数据源数据
      		: length:数据长度
      返回参数 : 成功返回:SUCCESS/失败返回:ERROR
      使用说明 : 可下发可上报类型,需要在处理完数据后上报处理结果至 app
      *****************************************************************************/
      static unsigned char dp_download_st_qrcode_handle(const unsigned char value[], unsigned short length)
      {
      	//示例:当前 DP 类型为 STRING
      	unsigned char ret;
      	unsigned char dp_download_st_qrcode_handle = NULL;  
      	cJSON *root = NULL, *item_1 = NULL, *item_2 = NULL, *item_3 = NULL;
      	char *qrcode = NULL;
      	char *d = NULL;
      	char *a = NULL;
      	char *pstr = NULL;
      	pstr = (char*)malloc(length+1);
      	if(NULL == pstr){
      		printf("malloc error\r\n");
      		return ERROR;
      	}
      	my_memset(pstr, 0x00, length+1);
      	my_memcpy(pstr, value, length);
      	printf("value: %s\r\n", value);
      	root = cJSON_Parse(pstr);
      	free(pstr), pstr = NULL;
      	if(NULL == root){							
      		printf("root ERROR!!!!!!!!!!!!!");
      		return ERROR;
      	}
      	//qr      URL 字段
      	item_1 = cJSON_GetObjectItem(root, "qr");          
      	if(NULL == item_1){			 				
      		printf("item_1 ERROR\r\n");
      	}
      	qrcode = (char*)malloc(sizeof(item_1));
      	if(NULL == qrcode){
      		printf("malloc qrcode err\r\n");
      		return ERROR;
      	}
      	free(qrcode), qrcode = NULL;
      	qrcode = item_1->valuestring;
      	printf("qrcode: %s\r\n", qrcode);
      	//d       url 次序
      	item_2 = cJSON_GetObjectItem(root, "d");        
      	if(NULL == item_2){						    	
      		return ERROR;
      	}
      	d = (char*)malloc(sizeof(item_2));
      	if(NULL == d){
      		printf("malloc d err\r\n");
      		return ERROR;
      	}
      	free(d), d = NULL;
      	d = item_2->valuestring;
      	printf("d: %s\r\n", d);
      	if(0 == strcmp(d, "1")){		                  //第一段 url				 
      		memcpy(FlashBuffer.st_qrcode1,qrcode,strlen(qrcode));
      		printf("FlashBuffer.st_qrcode1: %s\r\n", FlashBuffer.st_qrcode1);
      	}	
      	else if(0 == strcmp(d, "2")){				 	  //第二段 url		 
      		memcpy(FlashBuffer.st_qrcode2,qrcode,strlen(qrcode));
      		printf("FlashBuffer.st_qrcode2: %s\r\n", FlashBuffer.st_qrcode2);
      		strcat(FlashBuffer.st_qrcode1,FlashBuffer.st_qrcode2);
      		printf("FlashBuffer.st_qrcode1: %s\r\n", FlashBuffer.st_qrcode1);	  
      		memset(FlashBuffer.st_qrcode, 0x00, length+1);
      		memcpy(FlashBuffer.st_qrcode,FlashBuffer.st_qrcode1,strlen(FlashBuffer.st_qrcode1));
      		qrcod_test();		                          //二维码显示函数	   
      	}
      	//a
      	item_3 = cJSON_GetObjectItem(root, "a");
      	if(NULL == item_3) {
      		return ERROR;
      	}
      	a = item_3->valuestring;
      	cJSON_Delete(root);
      	root = NULL;
      	return 0;
      	//处理完 DP 数据后应有反馈
      	ret = mcu_dp_string_update(DPID_ST_QRCODE,value, length);
      	if(ret == SUCCESS)
      	{
      		Enter_stop_mode();                       //处理完后设备进入低功耗模式   
      		return SUCCESS;
      	}
      	else
      		return ERROR;
      }
      

      二维码显示函数

      TY_CREATE_IN_T Qrcod_CREATE_IN;
      TY_CREATE_OUT_T *Qrcod_CREATE_OUT;
      void qrcod_test(void)
      {
      	Qrcod_CREATE_IN.ecc_level=QRCODE_ECC_MEDIUM;
      	Qrcod_CREATE_IN.version=9;                            //版本为 9   53*53
      	Qrcod_CREATE_IN.magnifications=2;                     //放大倍数 2
      	Qrcod_CREATE_IN.mode=STORAGE_MODE_BIT;
      	Qrcod_CREATE_IN.information=FlashBuffer.st_qrcode;                        //url 存放位置
      	tuya_svc_image_generate_qrcode_create(&Qrcod_CREATE_IN,&Qrcod_CREATE_OUT);
      	EPD_HW_Init(Partial);
      	EPD_Dis_Part2(288,150,Qrcod_CREATE_OUT->dst_data,112,106);                //显示坐标位置
      	EPD_Part_Update();
      	EPD_DeepSleep();
      	tuya_svc_image_generate_qrcode_create_free(Qrcod_CREATE_OUT);
      }
      

      开发者可以根据自己需求修改 URL 信息,另外如果 URL 前半部分固定不变的话可以将其写在程序中,通过 DP 只下发后半部分 URL,可以简化很多步骤。

    预约信息更新

    DP ID 内容 备注
    103 {“st”:time,“et”:time,“n”:“name”,“t”:“type”,“d”:“th”} st:预约起始时间字段
    et:预约结束时间字段
    time:时间
    n:预约者名字字段
    name:名字内容
    t:类型字段
    type:add:增加
    del:删除
    d:第几条预约信息

    DP 103 是预约信息的更新,更新类型包括新增与删除,更新的前提是设备已经从服务端同步过才有更新。

    例如当前显示两条预约信息,当需要更新第一条预约信息时先下发更新 DP,并且第一条更新完成的同时需将第二条信息删除,下面为更新与删除的举例;

    类型 举例
    新增 {“st”:“10:00”,“et”:“12:00”,“n”:“无施”,“t”:“add”,“d”:“1”}
    删除 {“st”:“10:00”,“et”:“12:00”,“n”:“无施”,“t”:“del”,“d”:“2”}

    DP 解析及处理函数:

    /*****************************************************************************
    函数名称 : dp_download_st_add_handle
    功能描述 : 针对 DPID_ST_ADD 的处理函数
    输入参数 : value:数据源数据
            : length:数据长度
    返回参数 : 成功返回:SUCCESS/失败返回:ERROR
    使用说明 : 只下发类型,需要在处理完数据后上报处理结果至 app
    *****************************************************************************/
    static unsigned char dp_download_st_add_handle(const unsigned char value[], unsigned short length)
    {
        //示例:当前 DP 类型为 STRING
        unsigned char ret;
        unsigned char dp_download_st_add_handle = NULL;  
    	my_memset(FlashBuffer.st_add, 0x00, length+1);
    	memcpy(FlashBuffer.st_add, value,length*sizeof(unsigned char));
    	cJSON *root = NULL, *item = NULL, *item1 = NULL, *item2 = NULL, *item3 = NULL,*item4 = NULL;
    	char* st = NULL;            //预约起始时间
    	char* et = NULL;            //预约终止时间
    	char* n = NULL;             //预约者姓名
        char* t = NULL;             //预约类型 : add/del
    	char* d = NULL;             //预约信息次序
    	char *pstr = NULL;
    	pstr = (char*)malloc(length+1);
    	printf("pstr: %s\r\n", pstr);
        if(NULL == pstr){
    		printf("malloc err\r\n");
    		return ERROR;
    	}
        my_memset(pstr, 0x00, length+1);
        my_memcpy(pstr, value, length);
    	root = cJSON_Parse(pstr);
    	free(pstr), pstr = NULL;
        if(NULL == root){								
    		printf("ERROR!!!!!!!!!!!!!");
    		return ERROR;
        }
    	//st  预约起始时间
        item = cJSON_GetObjectItem(root, "st");
    	if(NULL == item){			 				
            printf("item ERROR\r\n");
        }
    	 st = item->valuestring;
    	 printf("st: %s\r\n", st);
         memcpy(FlashBuffer.st_add,st,strlen(st));
         //et  预约终止时间
        item1 = cJSON_GetObjectItem(root, "et");
    	if(NULL == item1){			 			
            printf("item1 ERROR\r\n");
        }
    	 et = item1->valuestring;
    	 printf("et: %s\r\n", et);
    	 memcpy(FlashBuffer.et_add,et,strlen(et));		
    	//n    预约者姓名
    	 item2 = cJSON_GetObjectItem(root, "n");
    	 if(NULL == item2){			 				
        }
    	 n = item2->valuestring;
    	 printf("n: %s\r\n", n);
         memcpy(FlashBuffer.n_add,n,strlen(n));	
    	//t   预约类型 : add/del
    	 item3 = cJSON_GetObjectItem(root, "t");
        if(NULL == item3){						    
    		return ERROR;
        }
    	 t = item3->valuestring;	
    	 printf("t: %s\r\n", t);	
    	//d   预约信息次序
    	 item4 = cJSON_GetObjectItem(root, "d");
        if(NULL == item4){						    	
    		return ERROR;
        }
    	 d = item4->valuestring;
    	 printf("d: %s\r\n", d);
    			
    	 if(0 == strcmp(t, "add"))         //新增
        {  
            if(0 == strcmp(d, "1"))        //新增第一条信息
                ST_ADD1();  
            else if(0 == strcmp(d, "2"))   //新增第二条信息
                ST_ADD2();  
            else  
                ST_ADD3();                //新增第三条信息
        }  
        else  
        {   
            if(0 == strcmp(t, "del"))       //删除
    	       {
    			   if(0 == strcmp(d, "1"))  
                      ST_DEL1();                   //删除第一条信息
                   else if(0 == strcmp(d, "2"))  
                      ST_DEL2();                  //删除第二条信息
                   else  
                      ST_DEL3();                  //删除第三条信息
              }  
        } 
        cJSON_Delete(root);
        return 0;	
        //处理完 DP 数据后应有反馈
        ret = mcu_dp_string_update(DPID_ST_ADD,value, length);
            if(ret == SUCCESS)
    	{
    		Enter_stop_mode();                  //处理完后设备进入低功耗模式
            return SUCCESS;
    	}
        else
            return ERROR;;
    }
    
    

    MCU 接收到 Value 后将解析出的起始时间存放在 FlashBuffer.st_add,将终止时间存放在 FlashBuffer.et_add,姓名字段存放在 gbk_n 中,并通过修改显示函数 EPD_Dis_Part() 中的横纵坐标来调整在屏幕中的位置。

    • 更新函数:

      void ST_ADD1(void)
      {
      	EPD_SetRAMValue_BaseMap(pBits);                     //保留背景信息
      	EPD_HW_Init(Partial);
      	zk_init();
      	GT5S_Wakeup();
      	memset(pBits,0,sizeof(pBits));
      	memcpy(pBits, pBits, sizeof(pBits));
      	unsigned long gbk_n[30];
      	utf8ToGBK(FlashBuffer.n_add, gbk_n, 30);             //GBK 转换
      	//time    
      	get_font(pBits,VEC_FT_ASCII_STY, FlashBuffer.st_add[0],24,24,24);     //st  
      	EPD_Dis_Part(0,150,pBits,24,24,NEG);  
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.st_add[1],24,24,24);    
      	EPD_Dis_Part(16,150,pBits,24,24,NEG);
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.st_add[2],24,24,24);    
      	EPD_Dis_Part(32,150,pBits,24,24,NEG);		           
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.st_add[3],24,24,24);   
      	EPD_Dis_Part(44,150,pBits,24,24,NEG);	 
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.st_add[4],24,24,24);     
      	EPD_Dis_Part(60,150,pBits,24,24,NEG);	            
      	get_font(pBits,VEC_FT_ASCII_STY,0X2D,24,24,24);  
      	EPD_Dis_Part(80,150,pBits,24,24,NEG);
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_add[0],24,24,24);      //et  
      	EPD_Dis_Part(96,150,pBits,24,24,NEG);	            
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_add[1],24,24,24);    
      	EPD_Dis_Part(112,150,pBits,24,24,NEG);	
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_add[2],24,24,24);   
      	EPD_Dis_Part(130,150,pBits,24,24,NEG);
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_add[3],24,24,24);    
      	EPD_Dis_Part(140,150,pBits,24,24,NEG);	
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_add[4],24,24,24);   
      	EPD_Dis_Part(153,150,pBits,24,24,NEG);	
      //name
      	get_font(pBits,VEC_SONG_STY,gbk_n[0],24,24,24);                   
      	EPD_Dis_Part(176,150,pBits,24,24,NEG);	
      	get_font(pBits,VEC_SONG_STY,gbk_n[1],24,24,24);    
      	EPD_Dis_Part(200,150,pBits,24,24,NEG);	
      	EPD_Part_Update();
      	EPD_DeepSleep();
      	GT5S_DeepSleep();
      }
      
    • 删除函数:

      void ST_DEL1(void)
      {
      	EPD_SetRAMValue_BaseMap(pBits);  
      	EPD_HW_Init(Partial); 	         
      	zk_init();
      	GT5S_Wakeup();
      	memcpy(pBits, pBits, sizeof(pBits));
      	unsigned long gbk_n[30];
      	utf8ToGBK(FlashBuffer.n_add, gbk_n, 30);
      	get_font(pBits,VEC_FT_ASCII_STY, FlashBuffer.st_add[0],24,24,24);     //st
      	EPD_Dis_Part(0,150,pBits,24,24,OFF);
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.st_add[1],24,24,24);    
      	EPD_Dis_Part(16,150,pBits,24,24,OFF);	
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.st_add[2],24,24,24);     
      	EPD_Dis_Part(32,150,pBits,24,24,OFF);	
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.st_add[3],24,24,24);    
      	EPD_Dis_Part(44,150,pBits,24,24,OFF);	
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.st_add[4],24,24,24); 
      	EPD_Dis_Part(60,150,pBits,24,24,OFF);	
      	get_font(pBits,VEC_FT_ASCII_STY,0x2D,24,24,24);                     //-
      	EPD_Dis_Part(80,150,pBits,24,24,OFF);
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_add[0],24,24,24);     //et
      	EPD_Dis_Part(96,150,pBits,24,24,OFF);	
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_add[1],24,24,24);     
      	EPD_Dis_Part(112,150,pBits,24,24,OFF);	
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_add[2],24,24,24); 
      	EPD_Dis_Part(130,150,pBits,24,24,OFF);	
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_add[3],24,24,24);     
      	EPD_Dis_Part(140,150,pBits,24,24,OFF);	
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_add[4],24,24,24);
      	EPD_Dis_Part(153,150,pBits,24,24,OFF);	
      	get_font(pBits,VEC_SONG_STY,gbk_n[0],24,24,24); 
      	EPD_Dis_Part(176,150,pBits,24,24,OFF); 
      	get_font(pBits,VEC_SONG_STY,gbk_n[1],24,24,24);  
      	EPD_Dis_Part(200,150,pBits,24,24,OFF);
      	EPD_Part_Update();
      	EPD_DeepSleep();
      	GT5S_DeepSleep();
      }
      

    预约信息同步

    DP ID 内容 备注
    104 “query” 设备发送,同步预约信息(全量)
    {“st”:time,“et”:time,“n”:“name”,“d”:“th”,“a”:“all”} st:预约起始时间字段 et:预约结束时间字段 time:时间 n:预约者姓名字段 name:姓名 d:单条预约信息 a:预约信息总量

    DP 104 是预约信息的同步,设备上电后同步所有数据,若查询到几条预约信息就对应显示几条预约信息,如查询到有三条信息则依次三条预约信息,下发格式。

    预约信息全量 举例
    第一条 {“st”:“10:00”,“et”:“12:30”,“n”:“一一”,”d”:”1”,”a”:”3”}
    第二条 {“st”:“14:10”,“et”:“16:30”,“n”:“一二”,”d”:”2”,”a”:”3”}
    第三条 {“st”:“18:30”,“et”:“20:00”,“n”:“一三”,”d”:”3”,”a”:”3”}
    • DP 解析及处理函数:

      /*****************************************************************************
      函数名称 : dp_download_st_all_handle
      功能描述 : 针对 DPID_ST_ALL 的处理函数
      输入参数 : value:数据源数据
      		: length:数据长度
      返回参数 : 成功返回:SUCCESS/失败返回:ERROR
      使用说明 : 可下发可上报类型,需要在处理完数据后上报处理结果至 app
      *****************************************************************************/
      static unsigned char dp_download_st_all_handle(const unsigned char value[], unsigned short length)
      {
      	//示例:当前 DP 类型为 STRING
      	unsigned char ret;
      	unsigned char dp_download_st_all_handle = NULL;
      	memset(FlashBuffer.st_all, 0x00, length+1);
      	memcpy(FlashBuffer.st_all, value,length*sizeof(unsigned char));
      	cJSON *root = NULL, *item_1 = NULL, *item_2 = NULL, *item_3 = NULL, *item_4 = NULL, *item_5 = NULL;
      	char *st = NULL;
      	char *et = NULL; 
      	char *n = NULL;
      	char *d = NULL;
      	char *a = NULL;
      	char *pstr = NULL;
      	pstr = (char*)malloc(length+1);
      	printf("pstr: %s\r\n", pstr);
      	if(NULL == pstr){
      		printf("malloc error\r\n");
      		return ERROR;
      	}
      	my_memset(pstr, 0x00, length+1);
      	my_memcpy(pstr, value, length);
      	printf("pstr: %s\r\n", pstr);
      	my_memset(FlashBuffer.st_all, 0x00, length+1);
      	my_memcpy(FlashBuffer.st_all, value, length);
      	root = cJSON_Parse(pstr);
      	free(pstr), pstr = NULL;
      	if(NULL == root){								
      		printf("root ERROR!!!!!!!!!!!!!");
      		return ERROR;
      	}
      	//st
      	item_1 = cJSON_GetObjectItem(root, "st");
      	if(NULL == item_1){			 				
      		printf("item_1 ERROR\r\n");
      	}
      	st = item_1->valuestring;
      	printf("st: %s\r\n", st);
      	memcpy(FlashBuffer.st_all,st,strlen(st));	
      	//et
      	item_2 = cJSON_GetObjectItem(root, "et");
      	if(NULL == item_2){			 				
      		printf("item_2 ERROR\r\n");
      	}
      		et = item_2->valuestring;
      		printf("et: %s\r\n", et);
      	memcpy(FlashBuffer.et_all,et,strlen(et));
      	//n
      	item_3 = cJSON_GetObjectItem(root, "n");
      	if(NULL == item_3){			 				
      	return ERROR;
      	}
      	n = item_3->valuestring;
      	printf("n: %s\r\n", n);
      	memcpy(FlashBuffer.n_all,n,strlen(n));
      	//d
      	item_4 = cJSON_GetObjectItem(root, "d");
      	if(NULL == item_4){						    	
      		return ERROR;
      	}
      	d = item_4->valuestring;
      	printf("d: %s\r\n", d);	
      	if(0 == strcmp(d, "1")){		      //第一条预约信息全量				 
      		ST_ALL1();                   
      	}	
      	else if(0 == strcmp(d, "2")){		  //第二条预约信息全量			 	 
      		ST_ALL2();
      	}	
      	else if(0 == strcmp(d, "3")){         //第三条预约信息全量	
      		ST_ALL3();			
      	}	
      	//a
      	item_5 = cJSON_GetObjectItem(root, "a");
      	if(NULL == item_5) {
      		return ERROR;
      	}
      	a = item_5->valuestring;
      	printf("a: %s\r\n", a);
      	cJSON_Delete(root), root = NULL;
      	return 0;
      	//处理完 DP 数据后应有反馈
      	ret = mcu_dp_string_update(DPID_ST_ALL,value, length);
      	if(ret == SUCCESS)
      	{
      		Enter_stop_mode();             //进入低功耗模式
      		return SUCCESS;
      	}
      	else
      		return ERROR;
      }
      

      MCU 接收到 Value 后将解析出的起始时间存放在 FlashBuffer.st_all,将终止时间存放在 FlashBuffer.et_all,姓名字段存放在 gbk_1 中,并通过修改显示函数 EPD_Dis_Part() 中的横纵坐标来调整在屏幕中的位置。

    • 显示函数:

      void ST_ALL1(void)
      {
      	EPD_HW_Init(Partial);
      	zk_init();
      	GT5S_Wakeup();
      	unsigned long gbk_1[30];
      	utf8ToGBK(FlashBuffer.n_all, gbk_1, 30);
      	//time
      	get_font(pBits,VEC_FT_ASCII_STY, FlashBuffer.st_all[0],24,24,24);  
      	EPD_Dis_Part(0,150,pBits,24,24,NEG);  
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.st_all[1],24,24,24);    
      	EPD_Dis_Part(16,150,pBits,24,24,NEG);
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.st_all[2],24,24,24);  
      	EPD_Dis_Part(32,150,pBits,24,24,NEG);		      
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.st_all[3],24,24,24);   
      	EPD_Dis_Part(44,150,pBits,24,24,NEG);	 
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.st_all[4],24,24,24);   
      	EPD_Dis_Part(60,150,pBits,24,24,NEG);	            
      	get_font(pBits,VEC_FT_ASCII_STY,0X2D,24,24,24);   
      	EPD_Dis_Part(80,150,pBits,24,24,NEG);	            
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_all[0],24,24,24);  
      	EPD_Dis_Part(96,150,pBits,24,24,NEG);	            
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_all[1],24,24,24);      
      	EPD_Dis_Part(112,150,pBits,24,24,NEG);	
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_all[2],24,24,24);    
      	EPD_Dis_Part(130,150,pBits,24,24,NEG);
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_all[3],24,24,24);    
      	EPD_Dis_Part(140,150,pBits,24,24,NEG);	
      	get_font(pBits,VEC_FT_ASCII_STY,FlashBuffer.et_all[4],24,24,24);   
      	EPD_Dis_Part(153,150,pBits,24,24,NEG);	
      	//name
      	get_font(pBits,VEC_SONG_STY,gbk_1[0],24,24,24);   
      	EPD_Dis_Part(176,150,pBits,24,24,NEG);	
      	get_font(pBits,VEC_SONG_STY,gbk_1[1],24,24,24); 
      	EPD_Dis_Part(200,150,pBits,24,24,NEG);	
      	EPD_Part_Update();
      	EPD_DeepSleep();
      	GT5S_DeepSleep();
      }
      

    电量报警

    当设备电池电量低于 10% 时将低电量状态上报至后台端,由于电池的电量和电压不是线性关系,若要通过电压来做电量指示的话需要多做几组充电放电的曲线测试,程序中也需要分三种情况处理:空载时、负载时、以及充电时将三种情况分别表示出来,用电压做电量指示才会更准确一些。

    本次设计在计量电池电量时采用设备功耗来计算电池电量,如设备休眠状态(80ms)电流 10uA,接收状态(600us)5mA,则可以算出平均电流,已知电池总电量,就可以计算出设备可工作时间。当电池电量低于 10%,上报至后台端。

    功耗调试

    MCU 与模组唤醒逻辑如下:

    TX/RX:通讯串口

    IO1:Zigbee 唤醒 MCU 引脚,拉低唤醒,电平持续时间 10ms 以上。

    IO2:MCU 唤醒 Zigbee 引脚,拉低唤醒,电平持续时间 10ms 以上。

    MCU 与模组唤醒的引脚为 PB4、PB5,对应 MCU 的 P B0、PC13。

    模组功耗调试

    Zigebee 模组烧录的固件为低功耗固件,可通过配置 Zigbee 网络策略参数来调节模组的功耗,以下为 Zigbee 模组的常用参数。

    Zigbee 网络策略参数 默认值
    心跳时间 17000
    配网时间 180
    poll 时间 5000
    上电持续 poll 30
    poll 失败次数 4
    应用触发 rejion 1
    rejion 间隔 180
    rejion 次数 1
    发送功率 11

    下面是对各个参数的详细描述,

    • 心跳时间:是用来维护设备和网关之间的数据链路是否正常的手段,强电设备的心跳时间默认为 150+random(30)秒,低功耗设备的心跳时间默认为 4 小时,设置范围为 10~5*3600 秒,且网关判定 12 小时内没有收到心跳则认为设备离线。

    • 配网时间:当 MCU 发送配网指令之后,模组会执行一段时间的配网操作,并发送当前网络状态为配网状态。在一段时间内由于某些原因(例如附近没有开启配网的网络或者距离较远)导致模组没有加入到合适网络,则配网超时。配网超时之后,模组将处于未配网状态,同时也会将此状态发送给 MCU。配网超时时间默认为 180 秒,设置范围为 30~600 秒。

    • poll 时间(唤醒周期):poll 时间是指已经加入到网络的低功耗模组会在周期内唤醒,唤醒之后低功耗模组会发送数据请求(Data request)至其父节点,用于告知父节点,其当前处于唤醒状态,父节点是否为其缓存数据。如果存在缓存数据,则父节点可以将数据发送给低功耗模组。

      :Poll 值主要是影响功耗。唤醒周期越短,功耗越大。poll 最小值为 200ms,小于最小值按照最小值处理。建议取值小于等于 8s,这里 poll 时间默认为 5,000ms,设置范围为 200~10,000ms。

    • 上电持续 poll:通常上电之后,设置一段时间的快速 poll,可以在这个时间窗内将网关的配置命令下发。上电之后的快速 Poll 的时间默认为 30 秒,设置范围为 30~600s。

    • poll 失败次数:低功耗模块发送 Data request 之后,父节点首先是需要回复 ack,该 ack 是对 Data request 的应答。如果有缓存数据则将数据发送给模块,如果没有数据发送,则仅需回复 ack。如果模块发送了 Data request,但由于环境、距离、父节点断电等因素导致模块没有收到 ack,则模块的 poll 失败次数会加 1,如果在累加的过程中重新收到父节点的 ack,则累加清零,当累加到的一定的值时(Poll 失败次数),认为模块丢失父节点,需要触发 rejion。默认值为 4 次,设置范围为 3~40 次。

    • rejoin(重连):即重新加入到网络,但无须网关开启配网模式,是一种专门用于低功耗设备在父节点丢失时,重新加入网络的一种机制。

    • rejoin 间隔: 周期触发 rejion 的间隔时间。低功耗模块触发 rejion 之后,发送 beacon request 的周期间隔。默认值为 180s,即当设备丢失父节点时,会间隔 180 秒尝试 rejion。

    • rejoin 次数:低功耗模块触发 rejion 之后,发送 beacon request 的次数,默认值为 1,可设置范围为 1~10 次。

    • 发送功率:发送功率越大,功耗越高,默认值为 11dB,可设置范围为 3~19dB。

    以上几个参数为模组较常用的几个参数,模组网络参数调节完之后测出模组休眠状态功耗约为 2.4uA,以上参数满足低功耗要求故开发者无须再次修改调节。

    MCU 功耗调试

    MCU 有多种低功耗模式:睡眠模式、低功耗运行模式、低功耗睡眠模式、停止模式、待机模式。

    待机模式电流最低,但是待机模式时的 MCU 处于不受控制的状态,所有的 IO 口都工作在高阻抗的状态下,只有专门的几个引脚能将 MCU 唤醒,而每次唤醒后相当于系统复位,RAM 中的数据全部丢失,在外部器件连接的情况下,器件的引脚可能会吸收大量的电流,反而达不到低功耗的要求。

    停止模式的功耗仅次于待机模式。在 STOP 模式下,PLL、HSL、HSE 都被停止,RAM 和寄存器的值保留。

    本次方案采用 STOP2 模式为功耗最低的睡眠模式,且 STOP2 模式只能从运行模式进入,开发者可根据自身应用场景选择相应的模式,下面是 MCU 在几种模式下的功耗大小,

    低功耗模式 电流消耗 唤醒时间
    STOP0 100 µA 0.7 µs
    STOP1 4.6 µA 4 µs
    STOP2 2.7µA 5 µs

    下面为 MCU 进入低功耗模式的流程步骤

    • 进入低功耗前的配置:

      1. 把所有开启的外设先失能,再把引脚设为模拟模式,最后关闭外设时钟;

      2. 进入 STOP2 模式,调用低功耗函数;可配置为中断唤醒或事件唤醒,通过 __WFI() 或 __WFE() 选择唤醒模式;

      void Enter_stop_mode(void)
      {
      	RCC->APB1ENR1|=1<<28;     //PWR 电源接口使能
      	RCC->APB1RSTR1|=1<<17;    //复位所以 IO 口
      	RCC->APB1RSTR1|=1<<18; 
      	RCC->AHB2ENR|=0<<1;       //禁止 GPIO B
      	RCC->AHB2ENR|=0<<2;       //禁止 GPIO C
      	RCC->APB1ENR1|= 0<<14; 
      	SPI_MISO_SLEEP;
      	EPD_BUSY_SLEEP;
      	//PB0 作为外部中断
      	Exit_IO4();	
      	//进入 STOP2 模式
      	__WFI();	             //Wait For Interrupt
      }
      
    • 出低功耗后的配置:

      1. 先恢复时钟配置

      2. 恢复外设状态,如 GPIO、串口、SPI 等

      3. 在需要进入 STOP 模式的地方直接调用函数

      	//退出 STOP2 后设置
      	void Exit_stop_mode(void)
      	{	
      		SystemClock_Config();
      		IO_Init();
      		RCC->AHB2ENR|=1<<1;      
      		RCC->AHB2ENR|=1<<2; 
      		printf("exit stop\r\n");
      	}
      
      	```
      
      
    • 中断唤醒

      void Exit_IO4(void)
      {
      	PA4_IN;               //PA4 唤醒引脚
      	EXTI->IMR1|=1<<4;     //中断请求未屏蔽
      	EXTI->RTSR1|=1<<4;    //line4 上升沿 
      	NVIC_SetPriority(EXTI4_IRQn, 1);
      	NVIC_EnableIRQ(EXTI4_IRQn);	
      	EXTI->PR1 = 1<<4;    //清除中断标志位
      	
      }
      

      注意:在调试低功耗时芯片有时会进入休眠自锁状态出现无法下载的情况,可按住开发板的复位键不放开再点下载程序,过 1~2 秒后放开按键即可烧写成功。

小结

设备完成后,你就可以把它放在座位上用于座位预约情况的管理了,墨水屏省电且不需要布线的优势将会显露无疑。

zidj