原文在CSDN
[项目实战-外卖自提柜 1.项目介绍、协议制定](https://blog.csdn.net/weixin_44578655/article/details/105945891)
[项目实战-外卖自提柜 2. CubeMX + FreeRTOS入门](https://blog.csdn.net/weixin_44578655/article/details/105952248)
[项目实战-外卖自提柜 3. FreeRTOS主要API的应用](https://blog.csdn.net/weixin_44578655/article/details/105969808)
[项目实战-外卖自提柜 4. FreeRTOS 堆栈分配、调试技巧](https://blog.csdn.net/weixin_44578655/article/details/105992659)
[项目实战-外卖自提柜 5. ESP8266 01S配置与掉线处理](https://blog.csdn.net/weixin_44578655/article/details/106004124)
[项目实战-外卖自提柜 6. 硬件工作(原理图、PCB绘制)](https://blog.csdn.net/weixin_44578655/article/details/106009141)
## 项目介绍
外卖自提柜,类似蜂巢之类的快递柜。
工作流程:
* 外卖员通过手机APP扫描柜体上面的固定二维码,在APP中输入客户的手机号
* 完成后,服务器向对应手机号发送含有取货密码的短信
* 同时自动分配一个空柜子,向设备端发送一个开柜指令,内容包括,柜号、开柜密码等
* 设备端收到开柜指令后开柜
* 客户收到短信后凭密码取外卖,取完后设备端上报服务器取货成功的信息。
基本功能包括与服务器通信,控制开柜,显示信息,声音提示,验证码输入等等。
服务器和APP是别人做的,我做设备端,柜体用下面这种。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200506105739397.png)
## 方案选型
方案:
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创建任务时是可以传入一个参数的**,这个参数就可以传入我们要发送的数据。
第一个任务诞生了:
**数据发送任务:**
``` c
/**
* @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](https://www.cnblogs.com/LinTeX9527/p/8007541.html)
数据发送任务的前几行代码有着落了:
``` c
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的另一个功能了: **互斥量**
**正如其名,一个资源在被一个任务访问时,不能再被另一个任务访问,就叫互斥**。
通过下面两个函数实现互斥:
``` c
osMutexWait(mutex_CopyData_h, osWaitForever); //等待互斥量被释放
osMutexRelease(mutex_CopyData_h); //释放互斥量
```
这其中**mutex\_CopyData\_h**是互斥量的句柄(可以看作是名称),**osWaitForever**表示一直阻塞等待,直到互斥量被释放。
如何使用呢?
按照上述情形举例,我们要在拷贝数据时用互斥量进行保护,数据发送任务就改进为下面这种形式:
``` c
/**
* @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(;;)
{
//待添加发送函数
}
}
```
**osMutexWait**和**osMutexRelease**之间,就是我们希望保护的位置。
当然这只完成了一半,同样的,我们需要在**存在数据覆盖风险的位置**设置互斥量的保护区。
例如下面:传入**数据发送任务**的参数是名为**Data_Buf**的数组
``` c
osThreadDef(DATA_SEND_TASK_H,SendData_Task, osPriorityHigh,0, 128); //心跳帧重发任务的宏
osThreadCreate(osThread(DATA_SEND_TASK_H),Data_Buf)
```
那么我需要在修改Data_Buf的位置设置互斥量保护区:
``` c
osMutexWait(mutex_CopyData_h, osWaitForever); //等待互斥量被释放
Data_Buf[0] = 0;
osMutexRelease(mutex_CopyData_h); //释放互斥量
```
被互斥量保护的区域,同时只能被一个任务访问,直到这个任务释放互斥量,下一个任务才能访问。
这样,我们就可以保证拷贝数据的时候,数据不会被误修改。
## 消息队列的使用
我们继续完善数据发送任务,回到需求分析,**数据发送任务**除了需要完成数据发送,还需要监听是否收到**与此帧数据匹配**的应答帧。
如果同时有好几个**数据发送任务**在等待应答帧,这时候收到了一条应答帧,对于某一个**数据发送任务**来说,如何判断这条应答帧是发给自己的呢?
上翻查阅数据帧格式的表格,可以看到,每一帧数据有**唯一的FrameId**,回复帧也有FrameId,它的FrameId与它要回复的数据帧的FrameId相同。
对于某一个**数据发送任务**来说,它只需要与收到的回复帧的FrameId进行匹配,若与自己的Frame相同,则判断这个回复帧是回复给自己的,如果是回复给自己的,这个**数据发送任务**就完成了自己的使命,可以把自己删除了。
所以当有多帧数据同时等待回复帧时,需要开设一个缓存区,存放收到的回复帧的FrameId,供**数据发送任务**查询。
这个缓存区,就交给 **消息队列**来完成
FreeRTOS对消息队列的处理,我用到了下面几个API:
``` c
//查询队列中元素的个数
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次,就相当于把队列查询了一遍。
数据发送任务就基本完成了:
``` c
/**
* @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]
ID |
Name |
Designator |
Footprint |
Quantity |
1 |
K2-1101UT-B4SW-01_JX |
KEY1,KEY5,KEY4 |
SW_PUSH_2P_6MM_H5MM_JX |
3 |
2 |
32.768KHz |
X1 |
OSC-TH_BD2.0-P0.70-D0.3 |
1 |
3 |
AMS1117-3.3 |
U6,U3 |
SOT-223-4_L6.5-W3.5-P2.30-LS7.0-BR |
2 |
4 |
HX25003-2A |
CN10,CN7,CN15,CN13,CN12,CN11,CN9,CN6,CN8,CN5,CN2,CN4,CN3,CN14 |
CONN-TH_2P-P2.50_HX25003-2A |
14 |
5 |
10uF |
C13,C10,C23,C18 |
C0603 |
4 |
6 |
LED-0603_R |
LED3,LED4 |
LED0603_RED |
2 |
7 |
GP2301 |
Q3 |
SOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR |
1 |
8 |
CH340C |
U2 |
SOP-16_L10.0-W3.9-P1.27-LS6.0-BL |
1 |
9 |
1N4148 |
D1,D8,D2,D11,D7,D4,D13,D5,D9,D6,D10,D12,D3 |
SOD-123_L2.8-W1.8-LS3.7-RD |
13 |
10 |
12pf |
C21,C20,C17,C16 |
C0603 |
4 |
11 |
SS-12D10L5 |
SW1 |
SW-TH_SS-12D10L5 |
1 |
12 |
100nF |
C6,C8,C15,C22,C5,C12,C19,C9,C14,C2,C4,C11,C25,C3,C1,C24,C7 |
C0603 |
17 |
13 |
10118194-0001LF |
USB2,USB1 |
MICRO-USB-SMD_10118194-0001LF |
2 |
14 |
ASMD0805-075 |
F1 |
F0805 |
1 |
15 |
LED-0603_B |
LED2 |
LED0603_BLUE |
1 |
16 |
HDR-F-2.54_1x8 |
H1 |
HDR-F-2.54_1X8 |
1 |
17 |
500 |
R14,R15 |
R0603 |
2 |
18 |
1K |
R28,R13,R36,R26,R24,R38,R22,R40,R18,R17,R30,R4,R6,R9,R32,R10,R34,R20 |
R0603 |
18 |
19 |
esp8266 |
U4 |
ESP8266 |
1 |
20 |
HDR-M-2.54_1x2 |
J2,J1,J3 |
HDR-M-2.54_1X2 |
3 |
21 |
YS-MBZ12085C05R42_C409842 |
BUZZER1 |
BUZ-TH_BD12.0-P6.50-D0.6-FD |
1 |
22 |
10K |
R2,R31,R19,R25,R5,R33,R27,R7,R21,R35,R41,R16,R37,R29,R23,R3,R39 |
R0603 |
17 |
23 |
500Ω |
R1 |
R0603 |
1 |
24 |
0Ω |
R12,R8 |
R0603 |
2 |
25 |
8MHz |
X3 |
HC-49US_L11.5-W4.5-P4.88 |
1 |
26 |
sim800l |
U5 |
HDR1X6_SIM800L_BOARD |
1 |
27 |
LED-0603_G |
LED1 |
LED0603_GREEN |
1 |
28 |
STM32F103RBT6 |
U1 |
LQFP-64_10X10X05P |
1 |
29 |
HDR-F-2.54_1x10 |
H3 |
HDR-F-2.54_1X10 |
1 |
30 |
HDR-F-2.54_1x4 |
H4,H2 |
HDR-F-2.54_1X4 |
2 |
31 |
GP2302 |
Q11,Q10,Q13,Q16,Q8,Q7,Q12,Q5,Q1,Q4,Q9,Q14,Q15,Q2,Q6 |
SOT-23-3_L2.9-W1.3-P1.90-LS2.4-BR |
15 |
32 |
HDR-M-2.54_1x4 |
J4 |
HDR-M-2.54_1X4 |
1 |
展开
71
110
收藏到专辑