1. 概述
 在前几篇的文章中,我们已经学习了LVGL界面绘制以及paho mqtt的同步客户端和异步客户端的操作,那么本篇就会综合前面的知识,加上Linux系统的多线程以及线程间通信的知识,将LVGL、MQTT、多线程、消息队列这些知识使用起来,形成我们最终的产品。
温湿度监控系统应用开发所有文章
- 【嵌入式Linux应用开发】移植LVGL到Linux开发板
 - 【嵌入式Linux应用开发】初步移植MQTT到Ubuntu和Linux开发板
 - 【嵌入式Linux应用开发】SquareLine Studio与LVGL模拟器
 - 【嵌入式Linux应用开发】温湿度监控系统——绘制温湿度折线图
 - 【嵌入式Linux应用开发】温湿度监控系统——学习paho mqtt的基本操作
 - 【嵌入式Linux应用开发】温湿度监控系统——多线程与温湿度的获取显示
 - 【嵌入式Linux应用开发】设计温湿度采集MCU子系统
 
适用开发板
 适用于百问网的STM32MP157开发板和IMX6ULL开发板及其对应的屏幕,需要注意的是编译链要对应更改。
2. Linux的多线程编程
 Linux的多线程编程如果要深入使用的话,会涉及到很多的知识,在一个庞大的嵌入式产品中,需要开发者对多线程进行精细化设计,来优化代码提高CPU的执行效率,但是在本次的温湿度监控系统中,我们只需要掌握多线程的创建和退出就好。
	我们在Ubuntu的终端输入指令man pthread然后按TAB键自动补齐,可以看到很多关于线程的函数:

比如我们对创建线程的api感兴趣,想知道它的信息,就可以在终端输入指令man pthread_create,然后就看到如下信息:

这里会告诉我们要使用这个函数需要包含什么头文件,函数的每个参数是什么意思,返回值有哪些信息等,我们就可以通过这个说明来学习这个函数的使用,然后再去网上参考别人的使用经验,总结成为自己的学习经验。
2.1 创建线程
	前面已经通过man指令查看了pthread_create的用法,我们现在直接写代码来学习。首先在前面创建的工作区间新建一个C源文件pthread_1.c:
1  | book@100ask:~/workspace$ cd /home/book/workspace  | 
然后编辑这个C源文件:
- 包含线程头文件
 
1  | 
- 包含C库文件
 
1  | 
- 创建两个线程的入口函数
 
线程入口函数的形式从创建线程的API参数就可以确定下来
1  | int pthread_create(pthread_t *thread, const pthread_attr_t *attr,  | 
可以看到是void *(*start_routine)(void*)这样的,所以我们的入口函数这样写:
1  | static void *thread1(void *paramater);  | 
在入口函数中我们打印一些信息,如下:
1  | printf("Thread 1 running: %d\n", count++);  | 
为了避免打印太快,我们可以加一个延时函数sleep/usleep,我们同样可以使用man指令来学习这两个函数:
 延时函数
- sleep:延时x秒
 

如果我们要使用sleep函数的话这里看不到我们需要哪些头文件,这时候我们就可以用man指令指定章节查看:
man 1/2/3 sleep,下图是man 3 sleep的结果:
- usleep:延时x微妙
 

所以我们需要在C源文件中包含头文件:
1  | 
我们可以使两个线程不同时间间隔打印:
1  | static void *thread1(void *paramater)  | 
创建线程
入口函数写好后,我们就可以去创建线程了,我们在main函数中使用
pthread_create创建线程:- 定义线程句柄
 
1
2pthread_t thread1_t;
pthread_t thread2_t;- 不设置优先级等属性也不传参创建线程
 
1
2
3
4
5
6
7
8
9
10
11
12
13int ret = pthread_create(&thread1_t, NULL, thread1, NULL);
if(ret != 0)
{
printf("Failed to create thread1.\n");
return -1;
}
ret = pthread_create(&thread2_t, NULL, thread1, NULL);
if(ret != 0)
{
printf("Failed to create thread2.\n");
return -1;
}所以我们的main函数最终是这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24int main(char argc, char* argv)
{
pthread_t thread1_t;
pthread_t thread2_t;
int ret = pthread_create(&thread1_t, NULL, thread1, NULL);
if(ret != 0)
{
printf("Failed to create thread1.\n");
return -1;
}
ret = pthread_create(&thread2_t, NULL, thread1, NULL);
if(ret != 0)
{
printf("Failed to create thread2.\n");
return -1;
}
while(1)
{
sleep(1);
}
}然后使用gcc编译它,需要注意的是编译的时候需要连接线程的库
pthread,所以编译的时候要加上-lpthread1
gcc -o pthread_1 pthread_1.c -lpthread
这样就得到了可执行输出文件
pthread_1,我们./pthread_1执行后的效果:
可以按
CTRL+C退出程序。
2.2 退出线程
	我们使用man指令学习下如何使用线程退出函数:
1  | man pthread_exit  | 

从描述那里可以看到这个函数可以终止调用该函数的线程,即如果我在thread1里面调用了pthread_exit,那么thread1就会被终止,而其它线程继续运行:
1  | static void *thread1(void *paramater)  | 
我们这样修改后重新编译执行看下效果:

可以看到线程1执行5此后就没再打印了,线程2打印了3次后就没打印了。
3. Linux的消息队列
	对于消息队列的学习我们还是使用man来查询学习,先使用man msg+TAB键自动补齐,看一下有哪些函数:

3.1 获得一个消息队列

	获取一个新的消息需要传入key关键字还要设置一个新建的标志msgflg,如果msgflag设置IPC_CREAT,那么不管key值有没有被其它的消息队列占用,都能成功的获取到消息队列,返回该消息队列的ID,如果该消息队列是已创建的则是打开一个已存在的消息队列;如果msgflag设置为ICP_CREAT | IPC_EXCL,那么如果key已经被其他的队列占用的话,是无法获取到该关键字对应的新的消息队列的,返回错误码-1,例如:
1  | int msg_id = msgget(1234, IPC_PRIVATE | IPC_CREAT);  | 
3.2 发送消息
	发送消息队列的API是msgsnd:
1  | int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);  | 

可以看到在手册中msgsnd和msgrcv是一起解释的,对于发送函数msgsnd,需要传入4个参数:
消息队列的ID
msgid;发送的消息数据指针
msgp;发送的消息数据大小
msgsz;发送标志
msgflg;发送数据指针
mgsp它指向的是一个形如:
1  | struct msgbuf{  | 
的结构体,其中mtype必须是一个大于0的值来表示消息类型,这个类型值在后面接收消息的时候可以用到,比如可以让接收方不接收这个类型的消息(搭配msgflg=MSG_EXCEPT使用),也可以让接收方接收到消息队列中第一个类型为msgtyp=mtype的消息,这种情况下就没有队列的先进先出的特性了。
	发送的数据大小是msgp中除了mtype的数据大小,而不是整个消息结构体的大小。
 发送标志支持如下几种:
0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列IPC_NOWAIT:如果消息队列满了,新的消息将不会被写入队列IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程
返回值:
0:发送消息成功- -1:发送消息失败,错误码在erorr中
 
使用示例:
1  | struct msgbuf{  | 
3.3 接收消息
 接收消息的API:
1  | ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);  | 
参数解释:
| 参数名称 | 参数释义 | 
|---|---|
msqid | 
消息队列的ID | 
msgp | 
存放消息的指针 | 
msgsz | 
指定接收消息的大小 | 
msgtyp | 
执行接收消息的类型: 0-读取消息队列中第一个数据; >0:读取消息队列中类型为 msgtyp的第一个数据;如果msgflg=MSG_EXCEPT,那么队列中和第一个类型不为msgtyp的数据将会被读取;<0:将会读取队列中第一个等于或者小于 msgtyp类型的数据 | 
msgflg | 
接收消息队列的标志位:IPC_CREAT:如果队列中没有数据或者没有指定类型的数据,则直接返回,不堵塞线程,返回错误码在error中;MSG_COPY:这个标志需要搭配IPC_CREAT使用,将会从指定位置读取消息,如果指定位置处没有消息将会理解返回-1,错误码在error中;MSG_EXCEPT:指定不接收某些msgtyp的消息数据;MSG_NOEORROR:如果队列中的数据个数大于指定接收msgsz,那么就会截断只读取msgsz个数据出来; | 
| 返回值 | 0-成功;-1失败,错误码在error中 | 
使用示例:
1  | struct msgbuf{  | 
3.4 销毁消息队列
 消息队列创建后是独立于线程的,所以如果退出线程而没有去撤销消息队列的话,那么我们创建的消息队列就会一直存在在后台中,除非我们重启系统,因而我们为了减小这样的影响,可以在退出线程结束程序前将我们创建的队列都销毁掉,使用的接口是:
1  | int msgctl(int msqid, int cmd, struct msqid_ds *buf);  | 
参数解释:
| 参数名称 | 参数解释 | 
|---|---|
| msgqid | 消息队列的ID | 
| cmd | 控制这个消息队列的操作指令:IPC_STAT:将指定ID的消息队列的内核信息copy到msqid_ds *buf中;IPC_SET:给指定ID的消息队列写入一些保存在msqid_ds *buf中的信息;IPC_RMID:理解删除指定ID的消息队列;IPC_INFO:将指定ID的消息队列在系统中的信息copy到msqid_ds *buf中;MSG_INFO:返回一些和IPC_INFO类似的信息到buf中;MSG_STAT:返回一些和IPC_STAT类似的信息到buf中; | 
| buf | 保存信息的结构体指针,如果 | 
| 返回值 | 在IPC_STAT、IPC_SET、IPC_RMID下成功的话返回0,在IPC_INFO或MSG_INFO下成功的话返回队列在内核或者系统的索引值;错误的话返回-1,错误码在 error中; | 
使用示例:
1  | int dmsg = msgget(1234, IPC_CREAT | IPC_PRIVATE);  | 
4. 显示温湿度
 我们在前面已经学习了MQTT的一些基本操作,以及LVGL中表格chart和滑动条slider的一些基本操作,刚才又学习了线程通信的消息队列,现在就可以综合起来实现我们的目标了。
 我们想要完成的是将Linux开发板变成一个MQTT客户端,和阿里云服务器建立连接,然后订阅一个主题获得温湿度数据,开发板在接收到了订阅的主题的消息后,处理数据,然后发送消息队列给ui,去设置chart和slider更新显示:

所以我们的开发板其实只需要完成MQTT的订阅任务然后去处理再发送消息给LVGL即可。
4.1 更新chart和slider
 我们可以将更新chart和slider数值和显示的函数放到main.c里面去实现:
1  | book@100ask:~$ cd /home/book/workspace/lvgl_demo  | 
然后找个地方加入如下的代码:
1  | void set_temp_humi_data(uint16_t value)  | 
我们对于服务器发送的温湿度格式是:数据=温度*256+湿度,也就是代码中的:
1  | value = (temp_value<<8) + humi_value  | 
而解析就是:
1  | uint8_t temp_value = (value>>8)&0xFF;  | 
这个格式读者可以自定义,只要子系统发送以及监测系统解析的时候是同一套格式即可。
更新LVGL的chart和slider的值其实很简单,调用LVGL对应的API即可,如果不熟悉可以百度和查看LVGL的官方文档。
4.2 建立mqtt客户端以及订阅主题
 我们需要将mqtt的源码移植到工程里面(前提是已经按照前面的文章将mqtt安装到了ubuntu和Linux开发板):
1  | book@100ask:~$ cd /home/book/workspace/lvgl_demo  | 
 这里直接放源码, 登录服务器的链接地址、用户名这些省略,读者自己去阿里云物联网平台建立设备然后填写信息:
1  | // mqtt_iot.h  | 
而mqtt.mk文件就和ui.mk是一样的写法:
1  | MQTT_DIR_NAME ?= mqtt  | 
4.3 main函数初始化LVGL和mqtt
 我们需要在main函数里面初始化LVGL的参数以及我们绘制的ui,还要初始化mqtt成功建立客户端和服务器的连接后,将主动断开服务器的任务放到一个线程里面去:
1  | 
  | 
4.4 编译运行
 因为我们添加了mqtt的代码以及对应的.mk,所以需要将mqtt.mk放到工程目录下的Makefile中:
1  | include $(LVGL_DIR)/mqtt/mqtt.mk  | 
然后执行make编译,并且将编译出来的可执行文件拷贝到挂载目录:
1  | book@100ask:~/workspace/lvgl_demo$ make -j4  | 
最后去开发板上将其拷贝出来执行:
1  | [root@100ask:~]# mount -t nfs -o nolock,vers=3 192.168.50.12:/home/book/nfs_rootfs /mnt  | 
这样界面运行起来了,且也能看到设置的mqtt打印信息:

4.5 阿里云模拟下发消息验证
 开发板已经将程序运行起来了,我们就去阿里云下发模拟消息看一下:



然后看下开发板的屏幕以及终端:

可以看到接收到了消息,屏幕就不拍照了,实际上屏幕的滑动条数值也变了,表格也出来了点。
 至此,一个温湿度监控系统就完成了,下一步是去搞一个探测温湿度的子系统,用单片机+RT-Thread+MQTT+DHT11来完成。
5. 总结
 可以看到我们最终没有用到消息队列,这是因为当前的系统功能还很简单,不需要这个操作,但是读者可以在此基础上进行扩展,使这个系统不仅能订阅子系统的消息,还能发送消息给子系统来完成某个控制,这时候可以看下使用消息队列是否会更科学。
- 本文作者: 摘星星的小朋友
 - 本文链接: http://slhking.github.io/2022/07/05/LinuxApp-6-finishproducts/
 - 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!