外卖自提柜设备端主控

简介:外卖自提柜,类似蜂巢之类的快递柜。 基本功能包括与服务器通信,控制开柜,显示信息,声音提示,验证码输入等等。

开源协议: Public Domain

发布时间:2020-04-23 13:38:42
  • 6.7k
  • 12
  • 63
描述

原文在CSDN

项目实战-外卖自提柜 1.项目介绍、协议制定

项目实战-外卖自提柜 2. CubeMX + FreeRTOS入门

项目实战-外卖自提柜 3. FreeRTOS主要API的应用

项目实战-外卖自提柜 4. FreeRTOS 堆栈分配、调试技巧

项目实战-外卖自提柜 5. ESP8266 01S配置与掉线处理

项目实战-外卖自提柜 6. 硬件工作(原理图、PCB绘制)

项目介绍

外卖自提柜,类似蜂巢之类的快递柜。 工作流程:

  • 外卖员通过手机APP扫描柜体上面的固定二维码,在APP中输入客户的手机号
  • 完成后,服务器向对应手机号发送含有取货密码的短信
  • 同时自动分配一个空柜子,向设备端发送一个开柜指令,内容包括,柜号、开柜密码等
  • 设备端收到开柜指令后开柜
  • 客户收到短信后凭密码取外卖,取完后设备端上报服务器取货成功的信息。

基本功能包括与服务器通信,控制开柜,显示信息,声音提示,验证码输入等等。

服务器和APP是别人做的,我做设备端,柜体用下面这种。 在这里插入图片描述

方案选型

方案: MCU + WIFI模块 + GPRS模块 + 显示屏 + 键盘 选型: stm32f103rbt6 + esp8266 + sim800 + lcd彩屏 + 矩阵键盘

一开始觉得这个项目so easy 烂大街 ,乍一看确实,这选型也太烂大街了(笑),如果说这是一道电赛题,几天也能弄出来,最后花了两个月左右...

工作流程

设备端主要工作流程如下:

  1. 硬件开机后与服务器连接,连接成功后,硬件自动向服务器发送注册指令, 包含本机的Id,服务器收到后会将该机器注册进来,进行监管。
  2. 当有客户想要存放时,会扫描硬件二维码获取机器Id,然后在App上打开某个格子,服务器会向该机器发送存货指令 , 包含要打开的机器Id,格子Id,存放模式,取货验证码等等,同时服务器会向取货的客户发送6位验证码短信。
  3. 机器接收到存柜存货指令后,尝试打开相应格子,并保存验证码,若打开成功,则发回给服务器开柜成功指令表示成功。否则返回开柜失败指令表示失败。
  4. 客户来取物品时,在机器上输入相应的六位密码,响应密码的格子就会自动打开,然后向服务器发送取货指令,报告格子被打开。
  5. 持续工作,设备需要每30s发送一次心跳指令

协议制定

协议部分雏形是做服务器的同学定的,这部分直接导致系统从裸奔变成跑FreeRTOS。

帧头 + Length + CmdId + DevId + Content + FrameId + 校验和

成分 描述
帧头 0x0a 0x0a 0x0a 0x0a
Length 指令字节数总长度,包括其本身和校验和,两个字节的无符号short类型,顺序为 [低字节,高字节]
CmdId 指令的Id , 一个字节的无符号byte类型
DevId 目标设备的Id,两个字节的无符号short类型,顺序为 [低字节,高字节]
Content 该条指令包含的详细信息
FrameId 每一帧的唯一Id,两字节无符号short类型,顺序为[低字节,高字节]
校验和 一字节有符号byte类型

不同指令的Content不同:

  1. 注册帧000:设备向服务器发送的认证信息,在服务器上注册该设备 Content为空
  2. 回复帧001:回复数据正确 Content为空
  3. 心跳帧002:心跳保持 Content为空
  4. 存货开柜帧003:服务器向设备发送存货开柜指令 Content内容包含: -CellId:机器格子的编号,要开启的格子。两个字节的无符号short类型,顺序为 [低字节,高字节] -Mode:代表存储的模式(常温,保温,制冷),一个字节的无符号byte -PassWord:表示存储密码,六个字节的char字符串,顺序即为密码顺序 -SendAddress:表示存件者的id,11个字节的电话号码,char字符串,顺序即为号码顺序 -ReceiveAddress:表示取件者的id,含义同上
  5. 开柜成功帧004:设备开柜成功 Content内容与指令003相同
  6. 开柜失败帧005:设备开柜失败 Content包含: -SendAddress : 表示存件者的id,11个字节的电话号码,char字符串,顺序即为号码顺序
  7. 取货帧006:客户取货成功 Content包含: -CellId:机器格子的编号,要开启的格子。两个字节的无符号short类型,顺序为 [低字节,高字节]

第一个任务

初步入门FreeRTOS以后,着重解决通信部分,重新梳理一下与服务器通信部分的需求:

设备端和服务器通信,发送方每发送一条指令,接收方都要在收到后返回一个应答帧,发送方收到应答帧后,才判断此次通信正常,若规定时间内未收到应答帧,则重新发送。 另外需要注意的是,发送方在等待接收方返回应答帧时,不能阻塞系统运行,也就是说,即便当前有一帧数据在等待应答,也不影响下一帧数据的发送,且理论上应该保证同时在等待应答的帧的数量不受限制

根据上述需求,显而易见的,应当把每一帧的发送单独作为一个任务,这个任务对这一帧进行监听,并控制重发。只要系统还有足够的剩余栈,就可以不断地创建新的发送任务,这样就可以保证最大限度地使用硬件资源保证每一帧的通信“并行”。 刚好,FreeRTOS创建任务时是可以传入一个参数的,这个参数就可以传入我们要发送的数据。 第一个任务诞生了: 数据发送任务:

/**
  * @brief  数据发送任务
  * @note   需要向服务器发送一条指令时,就创建一个发送任务,特点是等待回复和重发时不会阻塞其他任务进行
  * @param  argument:要发送的数据
  * @retval None
  */
void SendData_Task(void const * argument)
{
    //待添加
    for(;;)
    {
        //待添加
    }
}

下面来构思函数体中要写些什么

首先,肯定是要发送数据了,发送数据之前,有一件事要考虑,由于传入的是argument是指针,这个任务在进行过程中,这个指针指向的内容很可能被其他任务更改,所先需要先申请空间来拷贝要发送的数据 再来回顾一下帧格式: 帧头 + Length + CmdId + DevId + Content + FrameId + 校验和

成分 描述
帧头 0x0a 0x0a 0x0a 0x0a
Length 指令字节数总长度,包括其本身和校验和,两个字节的无符号short类型,顺序为 [低字节,高字节]
CmdId 指令的Id , 一个字节的无符号byte类型
DevId 目标设备的Id,两个字节的无符号short类型,顺序为 [低字节,高字节]
Content 该条指令包含的详细信息
FrameId 每一帧的唯一Id,两字节无符号short类型,顺序为[低字节,高字节]
校验和 一字节有符号byte类型

我们通过上述的Length获取数据长度,然后用FreeRTOS提供的API: pvPortMalloc 申请内存,这个函数与C语言的malloc的区别是,前者从FreeRTOS的TOTAL_HEAP_SIZE中申请空间,而后者是从系统的堆(heap)中申请空间。 详细的分析看这篇博客: https://www.cnblogs.com/LinTeX9527/p/8007541.html

数据发送任务的前几行代码有着落了:

void SendData_Task(void const * argument)
{
    uint8_t *Data;              //创建指针
    uint16_t Data_Len = 0;      //数据长度
    Data_Len = ((uint16_t*)argument)[0];//获取数据长度
    Data = pvPortMalloc(Data_Len-1);    //申请内存,去掉校验和1字节
    memcpy(Data,(uint8_t*)argument,sizeof(uint8_t)*(Data_Len-1));   //复制数组,去掉校验和
    for(;;)
    {
        //待添加
    }
}

互斥量的使用

当然,如果这里严谨一点的话,你会发现,即便这里进行了数据拷贝,但拷贝也不是一瞬间完成的,所以拷贝的时候,这段数据仍然不是安全的,仍可能被更改,下面就用到FreeRTOS的另一个功能了: 互斥量

正如其名,一个资源在被一个任务访问时,不能再被另一个任务访问,就叫互斥。 通过下面两个函数实现互斥:

osMutexWait(mutex_CopyData_h, osWaitForever);   //等待互斥量被释放
osMutexRelease(mutex_CopyData_h);   //释放互斥量

这其中mutex_CopyData_h是互斥量的句柄(可以看作是名称),osWaitForever表示一直阻塞等待,直到互斥量被释放。

如何使用呢? 按照上述情形举例,我们要在拷贝数据时用互斥量进行保护,数据发送任务就改进为下面这种形式:

/**
  * @brief  数据发送任务
  * @note   需要向服务器发送一条指令时,就创建一个发送任务,特点是等待回复和重发时不会阻塞其他任务进行
  * @param  argument:要发送的数据
  * @retval None
  */
void SendData_Task(void const * argument)
{
    uint8_t *Data;              //申请内存指针
    uint16_t Data_Len = 0;      //数据长度
    Data_Len = ((uint16_t*)argument)[0];//获取数据长度
    Data = pvPortMalloc(Data_Len-1);    //申请内存,去掉校验和1字节
    osMutexWait(mutex_CopyData_h, osWaitForever);   //等待互斥量被释放
    /*被互斥量保护的区域*/
    memcpy(Data,(uint8_t*)argument,sizeof(uint8_t)*(Data_Len-1));   
    /*被互斥量保护的区域*/
    osMutexRelease(mutex_CopyData_h);   //释放互斥量
    for(;;)
    {
        //待添加发送函数
    }
}

osMutexWaitosMutexRelease之间,就是我们希望保护的位置。 当然这只完成了一半,同样的,我们需要在存在数据覆盖风险的位置设置互斥量的保护区。

例如下面:传入数据发送任务的参数是名为Data_Buf的数组

osThreadDef(DATA_SEND_TASK_H,SendData_Task, osPriorityHigh,0, 128); //心跳帧重发任务的宏
osThreadCreate(osThread(DATA_SEND_TASK_H),Data_Buf)

那么我需要在修改Data_Buf的位置设置互斥量保护区:

osMutexWait(mutex_CopyData_h, osWaitForever);   //等待互斥量被释放
Data_Buf[0] = 0;
osMutexRelease(mutex_CopyData_h);   //释放互斥量

被互斥量保护的区域,同时只能被一个任务访问,直到这个任务释放互斥量,下一个任务才能访问。 这样,我们就可以保证拷贝数据的时候,数据不会被误修改。

消息队列的使用

我们继续完善数据发送任务,回到需求分析,数据发送任务除了需要完成数据发送,还需要监听是否收到与此帧数据匹配的应答帧。

如果同时有好几个数据发送任务在等待应答帧,这时候收到了一条应答帧,对于某一个数据发送任务来说,如何判断这条应答帧是发给自己的呢?

上翻查阅数据帧格式的表格,可以看到,每一帧数据有唯一的FrameId,回复帧也有FrameId,它的FrameId与它要回复的数据帧的FrameId相同。

对于某一个数据发送任务来说,它只需要与收到的回复帧的FrameId进行匹配,若与自己的Frame相同,则判断这个回复帧是回复给自己的,如果是回复给自己的,这个数据发送任务就完成了自己的使命,可以把自己删除了。

所以当有多帧数据同时等待回复帧时,需要开设一个缓存区,存放收到的回复帧的FrameId,供数据发送任务查询。

这个缓存区,就交给 消息队列来完成

FreeRTOS对消息队列的处理,我用到了下面几个API:

//查询队列中元素的个数
osMessageWaiting(MsgBox_Frame_Id_Handle);
//获取并删除队列中的一个元素
osMessageGet(MsgBox_Frame_Id_Handle,osWaitForever);
//向队列存放一个元素
osMessagePut(MsgBox_Frame_Id_Handle,evt.value.v,osWaitForever);
  • MsgBox_Frame_Id_Handle是这个队列的句柄
  • osWaitForever表示这个函数执行的超时时间,超过了这个值就会自动退出,这里是永久等待
  • evt.value.v是要向队列里存入的元素

如何实现查询队列中是否有与自己匹配的FrameId呢?

我的思路是,先通过osMessageWaiting读出当前队列中元素的数量N ,进入循环,每个循环中,使用osMessageGet取出一个元素,由于队列是先进先出,所以这个元素是从队列头部取出的,判断是否匹配,如果匹配,皆大欢喜,这个数据发送任务就解脱了;如果不匹配,再将这个元素用osMessagePut重新加入到队列尾部,这样循环N次,就相当于把队列查询了一遍。

数据发送任务就基本完成了:

/**
  * @brief  数据发送任务
  * @note   需要向服务器发送一条指令时,就创建一个发送任务,特点是等待回复和重发时不会阻塞其他任务进行
  * @param  argument:要发送的数据
  * @retval None
  */
void SendData_Task(void const * argument)
{
    uint8_t *Data;              //申请内存指针
    uint16_t Data_Len = 0;      //数据长度
    Data_Len = ((uint16_t*)argument)[0];//获取数据长度
    uint16_t FrameId = 0;       //帧Id
    uint32_t MsgBox_Data_Num = 0;//队列中有效数据的数量
    osEvent evt;                //存放osMessageGet的返回值
    Data = pvPortMalloc(Data_Len-1);    //申请内存,去掉校验和1字节
    osMutexWait(mutex_CopyData_h, osWaitForever);   //等待互斥量被释放
    /*被互斥量保护的区域*/
    memcpy(Data,(uint8_t*)argument,sizeof(uint8_t)*(Data_Len-1));   
    /*被互斥量保护的区域*/
    osMutexRelease(mutex_CopyData_h);   //释放互斥量
    FrameId = (uint16_t)Data[Data_Len-2]<<8|(uint16_t)Data[Data_Len-3]; //装载这一帧数据的FrameId
    for(;;)
    {
        osMutexWait(mutex_id_Resend, osWaitForever);//获取互斥量,防止其他的数据发送任务打断
        MsgBox_Data_Num = osMessageWaiting(MsgBox_Frame_Id_Handle); //获取当前队列数量
        if(MsgBox_Data_Num != 0)        //如果队列非空
        {
            for(i=0;i<MsgBox_Data_Num;i++)
            {
                evt = osMessageGet(MsgBox_Frame_Id_Handle,100); //从队列中取出一个元素
                if(evt.value.v == FrameId)      //如果FrameId匹配
                {
                    /****删除任务****/
                    osMutexRelease(mutex_id_Resend);    //释放令牌
                    vPortFree(Data);                    //释放内存
                    osThreadTerminate (NULL);           //删除本任务
                }
                else        //如果不匹配
                {
                    osMessagePut(MsgBox_Frame_Id_Handle,evt.value.v,500) //存回队列尾
                }
            }
        }
        User_SendData(Data,Data_Len);   //发送数据
        osMutexRelease(mutex_id_Resend);//释放互斥量
        osDelay(5000);  //每5s检测一次
    }
}

除了上面的思路,我这里还使用了一个互斥量,用以保护整个发送过程,因为当有多个数据发送任务都再执行时,队列的取出和放回动作可能会被打断,出现某种极端情况。 例如任务A刚刚从队列中取出一个元素,发现跟自己的FrameId不匹配,但还没来得及放回去,CPU控制权就被任务B抢去了,任务B查询的时候,就少了这个任务A取走的元素,造成误判。 另外,发送数据是通过串口的,执行时间也比较长,如果发送时被打断,可能造成不可预估的后果,所以使用互斥量进行保护是十分有必要的。

在整个项目中,主要用到的就是上面几个API数据发送任务,也是仅有的稍显复杂的任务,另外还有一些调试用的API,下一节更新。

任务划分

根据功能划分了下面几个任务

  1. 人机交互任务: 包括按键扫描、LCD显示、蜂鸣器扫描,优先级较低
  2. 无线模块管理任务: 包括检测到服务器离线时,对WIFI模块/GPRS模块进行重新初始化,切换wifi网络或运营商网络模式等,优先级最高
  3. TCP透传发送任务 当要发送一帧数据时,该任务被创建,发送一帧数据,并对这帧数据进行监听,等待接收方回复,若未收到回复,则重新发送,通信完成则删除本任务。优先级较高
  4. TCP接收数据解析任务 由接收中断触发,解析数据,执行相应操作,优先级最高

这里简单列两个 人机交互任务:

/**
  * @brief  人机交互任务
  * @note   包括按键扫描、LCD显示、蜂鸣器鸣叫
  * @param  argument:任务参数(未用到)
  * @retval None
  */
void Interactive_Task(void const * argument)
{
    for(;;)
    {
        /**矩阵键盘扫描**/
        /**键值处理**/
        /**LCD显示**/
        /**蜂鸣器扫描**/
        osDelay(20);
    }
}

无线模块管理任务:

/**
  * @brief  无线模块管理任务
  * @note   检测服务器是否离线,若离线则重新初始化无线模块
  * @param  argument:任务参数(未用到)
  * @retval None
  */
void WirelessCTR_Task(void const * argument)
{
    osDelay(1000);      //等待ESP8266上电
    for(;;)
    {
        if(server_sta == SERVER_OFF_LINE)   //服务器离线
        {
//          osThreadSetPriority(NULL,osPriorityHigh);   //调高优先级,防止打断
            esp8266_init();     //初始化esp8266
//          osThreadSetPriority(NULL,osPriorityNormal); //调低优先级
        }
        osDelay(20);
    }
}

图片.png

ESP8266 01S

在这里插入图片描述 配置TCP透传,用到的AT指令如下:

AT指令 功能
AT 测试硬件是否正常
ATE0 关闭回显
AT+CWMODE=1 设置为客户端
AT+CIPSTATUS 判断状态:返回2表示已正常连接WIFI;返回3表示已正常连接服务器
AT+CWJAP="MyWIFI","123456" 连接WIFI
AT+CWAUTOCONN=1 设为自动连接WIFI模式
AT+CIPSTART="TCP","192.111.1.1",8888 连接服务器
AT+CIPMODE=1 设为透传模式
AT+CIPSEND 开始透传
+++ 关闭透传

初始化流程图: 在这里插入图片描述

返回值的处理方法

基本思路是,开辟一个数组,收到的返回值存入数组,发送完指令后,等待一段时间,读取数组,利用**strstr()**这个函数,判断数组中是否有期望的返回值,查找完成后清空数组 以AT指令为例:

usart3_tx_dma_enable((uint8_t*)"AT",2); //发送AT指令
osDelay(50);    //等待50ms
if(strstr(mes_buf,"OK")!=NULL)  //找到对应字符串
    res = 1;            //返回1,否则返回0
else
    res = 0;
clear_mes_buf();    //清空缓冲
return res;

退出透传出错解决办法

发送“+++”,不加\r\n, 但这会导致这之后的一个AT指令失效,所以,在发送完+++以后,还要再发送一个\r\n,后面的AT指令才能生效。

//关闭透传
void close_tran()
{
    usart3_tx_dma_enable((uint8_t*)"+++",3);    //发送+++
    osDelay(500);   //延时500ms
    usart3_tx_dma_enable((uint8_t*)"\r\n",2);   //实际测试时,发完+++以后,还需要一个指令(带\r\n)激活模块
    osDelay(100);   //延时100ms
}

如何判断服务器是否离线

一般情况下,在透传过程中服务器突然离线,会返回一个closed,但由于此前一直处于透传模式,单片机想要捕捉这个closed比较困难,所以需要用别的手段判断服务器是否异常离线。

这个项目的协议中,有心跳和回复帧的机制,可以根据发出的心跳是否得到回复来判断服务器是否在线。 如果检测到异常离线,再去重新初始化ESP8266,再进一步判断WIFI是否异常、服务器是否异常,定位问题。

原理图绘制

ESP8266和SIM800供电选择电路: 在这里插入图片描述


这里使用一个NMOS和一个PMOS实现模块切换,测试效果正常。

电磁锁驱动电路 在这里插入图片描述



我用的是NMOS,栅极电阻可以小一点,我实际用的是470R,这里甚至可以把栅极电阻短接。 R39是为了栅极下拉,防止IO口浮空时导致输出不稳定 D13是续流二极管,电磁锁是感性元件,防止关断瞬间击穿MOS

单片机最小系统部分 在这里插入图片描述


这里有一个防反接电路,主要是考虑到SWD接口容易插反,烧掉单片机,Q16是一个NMOS,用来防反接,插反以后MOS自动关断,R12是一个0欧电阻,不想用防反接功能,可以焊接R12进行短接。

USB转TTL部分 在这里插入图片描述


这个保险丝救了我好几次,一定不要省!!!用的是6V 700mA的自恢复保险丝。

PCB绘制

PCB的一点点经验,大佬勿喷,

实话说,这种板子,随便画也能用...

芯片的电源引脚做好退耦,退耦电容要靠近引脚 在这里插入图片描述在这里插入图片描述


晶振走线尽量短,晶振周围不要走电源线 在这里插入图片描述


USB信号线尽量不走过孔,平行走线 在这里插入图片描述


天线下方不要铺铜: 在这里插入图片描述


如果布局很紧凑,要针对性的多打一些过孔 在这里插入图片描述


1.8寸TFT屏、SIM800L、ESP8266: 图片.png


4x4薄膜矩阵键盘:

图片.png


焊接:

图片.png

图片.png

没有风枪...千万别学我 吹风机.png


洗版: 图片.png


接完如图: 在这里插入图片描述

测试视频链接: 外卖自提柜测试视频

设计图

自提柜原理图

在编辑器中打开
ID Name Designator Footprint Quantity
1 K2-1101UT-B4SW-01_JX KEY1,KEY4,KEY5 SW_PUSH_2P_6MM_H5MM_JX 3
2 32.768KHz X1 OSC-TH_BD2.0-P0.70-D0.3 1
3 AMS1117-3.3 U3,U6 SOT-223-4_L6.5-W3.5-P2.30-LS7.0-BR 2
4 HX25003-2A CN8,CN10,CN12,CN11,CN9,CN1,CN7,CN2,CN3,CN6,CN5,CN4 CONN-TH_2P-P2.50_HX25003-2A 12
5 NCE6005AS Q8,Q18,Q3,Q5,Q2 SOP-8_L4.9-W3.9-P1.27-LS6.0-BL 5
6 10uF C23,C18,C13,C10 C0603 4
7 LED-0603_R LED4,LED3 LED0603_RED 2
8 CH340C U2 SOP-16_L10.0-W3.9-P1.27-LS6.0-BL 1
9 1N4148 D4,D3,D2,D11,D10,D1,D9,D8,D7,D6,D5 SOD-123_L2.8-W1.8-LS3.7-RD 11
10 12pf C21,C20,C17,C16 C0603 4
11 SS-12D10L5 SW1 SW-TH_SS-12D10L5 1
12 100nF C8,C15,C25,C1,C2,C11,C12,C3,C4,C19,C22,C5,C6,C9,C7,C24 C0603 16
13 10118194-0001LF USB1 MICRO-USB-SMD_10118194-0001LF 1
14 ASMD0805-075 F1 F0805 1
15 HDR-F-2.54_1x8 H1 HDR-F-2.54_1X8 1
16 500 R15,R14 R0603 2
17 1K R18,R25,R40,R26,R44,R13,R48,R49,R52,R45,R38,R4,R10,R9 R0603 14
18 esp8266 U4 ESP8266 1
19 HDR-M-2.54_1x2 J1,J2 HDR-M-2.54_1X2 2
20 YS-MBZ12085C05R42_C409842 BUZZER1 BUZ-TH_BD12.0-P6.50-D0.6-FD 1
21 10K R6,R50,R41,R27,R43,R3,R19,R24,R5,R7,R46,R47,R51,R2 R0603 14
22 500Ω R1 R0603 1
23 R8,R12 R0603 2
24 8MHz X3 HC-49US_L11.5-W4.5-P4.88 1
25 LED-0603_G LED1 LED0603_GREEN 1
26 STM32F103RBT6 U1 LQFP-64_10X10X05P 1
27 220uF C14 CAP-SMD_BD8.0-L8.3-W8.3-FD 1
28 HDR-F-2.54_1x10 H3 HDR-F-2.54_1X10 1
29 HDR-F-2.54_1x4 H4,H2 HDR-F-2.54_1X4 2
30 GP2302 Q4,Q16 SOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR 2
31 HDR-M-2.54_1x4 J4 HDR-M-2.54_1X4 1

展开

工程成员

服务时间

周一至周五 9:00~18:00
  • 153 6159 2675

服务时间

周一至周五 9:00~18:00
  • 立创EDA微信号

    easyeda

  • QQ交流群

    664186054

  • 开源平台公众号

    oshwhub