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
,所以编译的时候要加上-lpthread
1
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 许可协议。转载请注明出处!