🐱作者:一只大喵咪1201
🐱专栏:《智能家居项目》
🔥格言:你只管努力,剩下的交给时间!
如上图所示是裸机版本的智能家居项目总体框架结构,这篇文章开始,本喵要带着大家逐渐将智能家居项目从逻辑版本改为FreeRTOS版本,并且要增加温湿度显示和网络时间获取两个功能。
目录
- 😸裸机程序的不足
- 😸合并代码
- 😸改造
- 😹思路
- 😹将环形缓冲区改为队列
- 😹队列调试
- 😸DHT11温湿度传感器
- 😹时序及驱动层代码
- 😹其他层代码
- 😸总结
😸裸机程序的不足
int main()
{
while(1)
{
if(按键按下)
{
点亮LED灯();
}
if(网络输入)
{
OLED显示();
LED控制();
风扇控制();
...
}
}
}
上面就是本喵前面用裸机方式实现智能家居本质逻辑的伪代码,在while(1)
不停检测是否有输入事件产生,如果有则判断是按键输入还是网络输入,不同类型的输入事件对应不同的处理方式。
按键输入时,点亮LED灯的逻辑很简单,所以执行的非常快,当网络输入时,OLED显示等处理则比较复杂,执行速度相对较慢。
此时程序是顺序执行的,如果在处理网络输入时,又产生了按键输入,此时CPU并不会立刻去处理按键,而是需要等处理完网络输入后再去处理按键,这样一来就会感觉整个系统比较卡顿,延时比较高。
- 缺点非常明显:不同任务模块之间相互影响。
int main()
{
while(1)
{
检测输入事件();
}
}
void USART3_IRQHandler()
{
构造输入事件();
}
如上面伪代码,在裸机版本智能家居中,输入事件是在中断函数中构造的,尤其是在处理ESP8266
的串口3中断函数中,构造事件比较复杂。
当在中断函数中构造网络输入事件时,后台程序(main函数)是处于中断状态不执行的,如果此时按键输入发生了,后台程序也无法处理按键,只有构造完网络数据从中断函数中退出才能处理按键。
- 缺点非常明显:中断函数执行时间太长,就会导致:更低优先级的中断无法得到及时处理,后台程序无法及时执行。
用软件定时器解决:
创建一个硬件定时器(TIMx),依据该定时器设计软件定时器,软件定时器:
- 软件定时器周期性触发。
- 每个软件定时器都有一个回调函数。
如上图所示实现了两个软件定时器:
- 软件定时器1:每4个tick,执行一次
f1()
- 软件定时器2:每2个tick,执行一次
f2()
如果f1()
、f2()
执行时间很短,不超过1个tick时,效果很好。一旦f1()
或f2()
执行的时间比较长,就会出问题。
比如f2()
需要执行3个tick,当它执行到第二个tick的时候,下一次f2()
该执行了,但是当前的f2()
还没有结束,所以下一次的f2()
就得不到执行。
- 缺点:周期性执行的函数执行时间不能太长。
总的来说,裸机程序的主要缺点就是实时性不够高。
😸合并代码
在文章CubeMX对FreeRTOS的适配中本喵介绍了如何使用CubeMX创建FreeRTOS工程,此时打开前面裸机版本智能家居项目的CubeMX工程:
如上图,然后选择FREERTOS并且选择CMSIS_V2接口,然后生成代码:
如上图红色框,此时原本的裸机项目工程中就有了FreeRTOS了,文件多了一个Middlewares/FreeRTOS
,代码中多了内核以及FreeRTOS初始化部分代码。
在FreeRTOS执行之前,调用SmartHomeTask
函数,此时仍然没有使用到FreeRTOS,并且效果和裸机版本一样。
此时就实现了将裸机版本代码合并到FreeRTOS中,只是还没有使用任何和RTOS有关的东西。
接下来创建一个任务用来执行SmartHomeTask
函数:
如上图,使用xTaskCreate
创建一个新任务来执行智能家居函数SmartHome
,此时就将原本的裸机程序简单地变成了一个FreeRTOS程序了,执行效果和之前的裸机程序一样。
😸改造
😹思路
完全没有必要重新使用FreeRTOS实现一遍智能家居项目,只需要在裸机版本的基础上进行改造即可。
如上图所示,左边黑色框是智能家居系统的输入,中间红色是业务子系统,业务子系统任务和输入方通过输入队列来通信,右边黑色框是输出。
输入:
- 按键输入:使用中断来处理,把按键输入事件写入"输入队列"。
- 按键输入执行快,耗时短,不会影响其他任务的执行,所以直接操作即可。
- 网络输入:使用网络任务来处理,把网络输入写入到“输入队列”。
- 微信小程序发来的控制命令,解析等操作耗时长,裸机方式会影响其他任务的实时性。
- 温湿度输入:使用温湿度任务来处理,把温湿度结果写入"输入队列"。
- 温湿度的获取比较耗时,裸机方式会影响其他任务。
- 时间输入:使用时钟任务来处理,把获取的时间结果写入"输入队列"。
- 从互联网获取时间比较耗时,裸机方式会影响其他任务。
业务子系统:
- 从"输入队列"获取按键输入、微信控制信息,从而直接控制设备。
- 从"输入队列"获取温湿度、时间,发送给OLED任务显示内容。
为了保证整个业务子系统能够实时处理输入队列中的数据,毫无疑问,业务子系统也是一个任务。
输出:
- LDE灯和风扇:直接控制外设来控制设备。
- 只需要改变相关引脚的电平状态,执行时间很短,不会对其他任务造成影响。
- OLED任务:使用OLED任务来处理。
- OLED上要显示的内容很多,有IP地址、温湿度、时间
- 需要一个统筹管理的OLED任务:它负责操作OLED
- 其他任务只能向OLED任务发出显示请求
以上就整个项目的改造思路。
😹将环形缓冲区改为队列
裸机版本的智能家居中,输入事件会被放入一个环形缓冲区中,业务子系统从这个环形缓冲区中读取事件并作出判断。
现在将原本的环形缓冲区改成队列,用来存放输入事件:
如上图,将input_buffer.c
中原本的环形缓冲区改成一个队列g_xQueueInput
,并且增加一个InitInputQueue
函数用来初始化输入队列,在其内部调用xQueueCreate
创建队列,创建成功返回0,失败返回-1并打印错误信息。
如上图所示,在写入输入事件的函数PutInputEvent
中,原本是将输入事件写入到环形缓冲区中,此时改成使用xQueueSendFromISR
写入到输入队列中。
按键输入:
如上图代码,当按键中断发生以后,会触发定时器消抖,定时器超时后会调用回调函数使用PutInputEvent
将输入事件放入到输入队列中。
- 输入事件是在中断中放入输入队列的。
网络输入:
如上图代码所示,当网络数据到来时,会产生串口3中断,在中断函数中调用回调函数,回调函数中再调用PutInputEvent
将输入事件放入到输入队列中。
- 输入事件也是在中断中放入队列的。
可以看到,站在输入队列的角度来看,输入事件都来自中断,所以在PutInputEvent
函数中使用xQueueSendFromISR
将输入事件放入到输入队列中。
如上图代码,SmartHomeTask
是一个普通任务,在该任务中调用GetInputEvent
来读取输入队列中的输入事件,所以在该函数中使用的就是xQueueReceive
来读取。
😹队列调试
此时虽然改造完毕了,编译也没有报错,但是烧录到板子里并不能正常运行,本喵带着大家来调试一下:
如上图,在FreeRTOSConfig.h
中改造一下宏函数configASSERT
,在里面加一个printf
函数打印代码信息,再将程序烧录开发板中。
如上图,使用sscom
工具当作服务端和开发板建立UDP连接,然后发送控制指令。
如上图,此时在串口助手中将会看到configASSERT
中打印的代码信息,然后程序死机,也没有产生预想的动作。
如上图,定位到configASSERT
打印的代码信息处,发现这里调用了configASSERT
,条件是ucCurrentPriority >= ucMaxSysCallPriority
。
ucCurrentPriority
是当前中断的优先级。ucMaxSysCallPriority
,是允许使用系统调用中断的最高优先级。
既然程序死机了,说明执行了configASSERT
陷入了for(;;)
循环中,进而说明条件不满足,也就是当前中断的优先级在编号上小于允许使用系统调用中断的优先级编号。
- 当前中断的优先级高于允许使用系统调用中断的优先级,这是不被允许的,所以死机。
如上图所示,在调用xQueueGenericSendFromISR
向队列中写数据时,会调用portASSERT_IF_INTERRUPT_PRIORITY_INVALID
,这是一个宏函数,会调用vPortValidateInterruptPriority
,在该函数内会调用configASSERT
来判断当前中断优先级和允许使用系统调用优先级的关系。
如上图,当开发板收到网络数据后会产生串口3中断,然后在中断函数中调用回调函数来将输入事件写入到队列中,所以此时当前中断就是串口3中断。
- 串口3中断的优先级高于允许使用系统调用的中断优先级,所以不被允许,程序死机。
如上图,将串口3中的优先级改成14,仅比Tick中断15高一级,但是比允许使用系统调用的中断优先级低,此时就允许在串口3中断函数中使用xQueueSnendFormISR
向队列中写数据了。
此时程序就可以正常运行了,configASSERT
中的打印信息也不再打印,程序也不会死机,而且会产生预想的动作。
- 凡是会在中断中使用系统调用的,都必须把它的优先级设置成低于允许使用系统调用的中断优先级。
😸DHT11温湿度传感器
如上图是DHT11的接线图,只需要三根线,VDD,GND,以及DATA,传感器和MCU共用一根数据线来传送数据。
如上图,本喵的开发板用PF6
与DHT11的DATA
相连。
😹时序及驱动层代码
初始化:
如上图所示通讯过程示意图,用户MCU发送一次开始信号后,DHT11从低功耗模式转换到高速模式,等待主机开始信号结束后DHT11发送响应信号。
然后DHT11发送出40bit的数据,用户可选择读取部分数据.从模式下DHT11接收到开始信号触发一次温湿度采集如果没有接收到主机发送开始信号,DHT11不会主动进行温湿度采集,采集数据后转换到低速模式。
这个过程中:
- 主机发送开始信号后将DATA线拉高,释放总线控制权。
- DHT11拉低总线发出响应信号后,主动拉高总线准备输出数据。
- DHT11开始发送40bit(5 bytes)数据。
- 从机释放总线,等待下一次开始。
对于MCU而言:
- 主机GPIO输出低电平,然后拉高,转为输入模式。
- 主机读取GPIO电平,读到低电平之后再等待读取到高电平,确认收到DHT11的响应。
- 主机确认得到响应后,通过读取到GPIO高低电平的时长,来判断收到的是逻辑1还是逻辑0,来组成真正的数据。
- 主机连续读取到40个bit之后,转为输出模式,准备为下一次读取数据发出开始信号。
可以看到,主机的GPIO存在输入模式和输出模式的转换,所以该IO口需要设置成开漏输出模式,当将IO电平拉高后就成为了输入模式。
如上图所示,总线空闲状态为高电平,主机把总线拉低等待DHT11响应,主机把总线拉低必须大于18毫秒,保证DHT11能检测到起始信号。
DHT11接收到主机的开始信号后,等待主机开始信号结束,然后发送80us低电平响应信号。
主机发送开始信号结束后,将总线拉高保持20-40us,释放总线控制权,然后读取DHT11的响应信号。
总线为低电平,说明 DHT11发送响应信号,DHT11发送响应信号后,再把总线拉高80us,准备发送数据。
- 这里的延时是微妙级别的,而TICK中断一次的时间是1ms,所以需要配置一个定时器进行微妙级别的延时。
如上图,使用CubeMX配置定时器3作为微秒级别的定时器,分频系数是72,此时定时器的频率为72M/72=1MHZ
,CNT
计数值增加一次,用时1us。
如上图,在driver_dht11.c
中定义DHT11_TIM_Init
函数来初始微秒定时器,并且定义微秒延时函数DHT11_usDelay
,传入的us
值是多少,就延时多长时间。
如上图,定义HDT11_Init
函数来初始化DHT11引脚,设置为开漏输出模式,同时初始化微秒定时器。
如上图,在driver_dht11.h
中定义宏DHT11_H
用来拉高总线,DHT11_L
用来拉低总线,DHT11_IN
用来读取总线上的电平。
如上图,定义DHT11_Start
来发出起始信号,主机将总线拉低20ms,大于18ms
。
如上图所示,主机按照该流程图进行操作,操作完毕后就可以开始读取DHT11发送来的数据了。
如上图代码所示,主机发出起始信号后,将总线拉高保持40us释放总线控制权,然后读取总线电平,读取到低电平说明从机开始应答,然后进入第一个while
不断读取总线电平,当电平变为高以后说明从机结束应答。
再进入第二个while
,当电平变成低以后说明这是从机开始发送一个数据,接下来就是判断该bit的数据是0还是1。
读数据:
如上图,当DHT11开始控制总线后,每一bit数据都以 50us低电平时隙开始,高电平的长短定了数据位是0还是1。
- 50us低电平过后,高电平保持的时间在
26us~28us
之间表示该bit是低电平。
- 50us过后,高电平保持时间是
70us
表示该bit是高电平。
如上图,在读数据时,从机会先发维持50us的低电平,然后再维持一定时间的高电平,所以需要读取总线上电平变化:
如上图代码,val
是要等待的目标总线电平状态,timeout
是超时时间,单位是us
。
- 当
val
是1时,说明在调用该函数时,总线电平状态是0,当总线电平状态变为1以后返回。 - 当
val
是0时,说明在调用该函数时,总线电平状态是1,当总线电平状态变为0以后返回。
- 接收数据时以字节为单位,先接收到的bit是高位,后接收的是低位。
如上图所示,开始接收一个字节时,总线处于低电平,所以要先等其变为高,然后再延时40us,如果总线电平仍然是高,已经超过0所规定的28us,说明该bit是1。
由于先收到的bit是高位,所以每次接收一个bit时需要将data
左移一位,当前bit是1则与data
或等,是0的话则不用,只需要左移即可。
bit数据组合到data
中后,再等待总线电平为低,开始下一个bit的接收。
数据格式:
一次完整的数据传输为40bit,先接收到的是高位bit。
- 数据格式:
8bit
湿度整数数据+8bit
湿度小数数据+8bit
温度整数数据+8bit
温度小数数据+8bit
校验和。
数据传送正确时校验和数据等于8bit湿度整数数据+8bit湿度小数数据+8bit温度整数数据+8bit温度小数数据所得结果的末8位。
DHT11中小数部分是保留不用的,所以这两个字节的数据始终为0。
如上图代码所示,使用DHT11_Read
来读取温湿度数据,使用一个5个字节的数组来接收温湿度数据。
当主机发送完开始信号接收数据时,将接收到的5个字节数据放入到数组中,然后进行校验,如果校验通过则给输出型参数湿度和温度赋值。
😹其他层代码
设备层:
如上图所示,在dht11_device.h
中定义温湿度数据结构体DHT11_VAL
,以及描述DHT11设备结构体DHT11Dev
,包含设备号,温湿度数据,设备初始化方法,以及获取数据的方法。
如上图所示,在dht11_device.c
中,创建全局的DHT11结构体变量并初始化。
内核抽象层:
如上图所示是内核抽象层的DHT11设备初始化和获取数据的方法。
芯片抽象层:
如上图所示是芯片抽象层的DHT11设备初始化和获取温湿度数据的方法,其本质是在调用驱动层的DHT11设备初始化和获取数据的方法。
如上图所示,在dht11_test.c
中定义dht11_test
函数来测试DHT11模块,将接收到的温湿度数据通过串口打印出来。
如上图,在main.c
中创建一个任务来执行DHT11的测试函数。
如上图所示,可以看到本喵所在位置的温度T=18℃
,湿度H=49%
。和天气预报上面的大致相符。
😸总结
本篇文章主要介绍了如何将之前的裸机版本智能家居改造成FreeRTOS版本,要注意使用系统调用的中断优先级,不能太高。
除此之外还介绍了DHT11温湿度传感器的使用方法,以及使用代码实现。