在前几篇的文章中,我们已经学习了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开发板及其对应的屏幕,需要注意的是编译链要对应更改。
- 100ASK_STM32MP157
- 100ASK_IMX6ULL
Linux的多线程编程如果要深入使用的话,会涉及到很多的知识,在一个庞大的嵌入式产品中,需要开发者对多线程进行精细化设计,来优化代码提高CPU的执行效率,但是在本次的温湿度监控系统中,我们只需要掌握多线程的创建和退出就好。
我们在Ubuntu的终端输入指令man pthread
然后按TAB
键自动补齐,可以看到很多关于线程的函数:
比如我们对创建线程的api感兴趣,想知道它的信息,就可以在终端输入指令man pthread_create
,然后就看到如下信息:
这里会告诉我们要使用这个函数需要包含什么头文件,函数的每个参数是什么意思,返回值有哪些信息等,我们就可以通过这个说明来学习这个函数的使用,然后再去网上参考别人的使用经验,总结成为自己的学习经验。
2.1 创建线程 前面已经通过man指令查看了pthread_create
的用法,我们现在直接写代码来学习。首先在前面创建的工作区间新建一个C源文件pthread_1.c
:
book@100ask:~/workspace$ cd /home/book/workspace
book@100ask:~/workspace$ touch pthread_1.c
然后编辑这个C源文件:
- 包含线程头文件
#include
- 包含C库文件
#include
- 创建两个线程的入口函数
线程入口函数的形式从创建线程的API参数就可以确定下来
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
可以看到是void *(*start_routine)(void*)
这样的,所以我们的入口函数这样写:
static void *thread1(void *paramater);
static void *thread2(void *paramater)
在入口函数中我们打印一些信息,如下:
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源文件中包含头文件:
#include
我们可以使两个线程不同时间间隔打印:
static void *thread1(void *paramater)
{
int count = 0;
while(1)
{
printf("Thread 1 running: %d\n", count++);
sleep(1);
}
}
static void *thread2(void *paramater)
{
int count = 0;
while(1)
{
printf("Thread 2 running: %d\n", count++);
sleep(2);
}
}
-
创建线程
入口函数写好后,我们就可以去创建线程了,我们在main函数中使用
pthread_create
创建线程:- 定义线程句柄
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; }
所以我们的main函数最终是这样:
int 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
gcc -o pthread_1 pthread_1.c -lpthread
这样就得到了可执行输出文件
pthread_1
,我们./pthread_1
执行后的效果:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0wY4dpps-1657596796741)(LinuxApp-6-finishproducts/image-20220704172748831.png)]
可以按
CTRL+C
退出程序。
我们使用man
指令学习下如何使用线程退出函数:
man pthread_exit
从描述那里可以看到这个函数可以终止调用该函数的线程,即如果我在thread1
里面调用了pthread_exit
,那么thread1
就会被终止,而其它线程继续运行:
static void *thread1(void *paramater)
{
int retval;
int count = 0;
while(1)
{
printf("Thread 1 running: %d\n", count++);
sleep(1);
if(count==5) pthread_exit(&retval);
}
}
static void *thread2(void *paramater)
{
int retval;
int count = 0;
while(1)
{
printf("Thread 2 running: %d\n", count++);
sleep(2);
if(count==3) pthread_exit(&retval);
}
}
// 在main函数的主循环中加个打印
int main(char argc, char** argv)
{
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, thread2, NULL);
if(ret != 0)
{
printf("Failed to create thread2.\n");
return -1;
}
while(1)
{
printf("Main>>>\r\n");
sleep(1);
}
}
我们这样修改后重新编译执行看下效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xdul86C8-1657596796743)(LinuxApp-6-finishproducts/image-20220704174520751.png)]
可以看到线程1执行5此后就没再打印了,线程2打印了3次后就没打印了。
3. Linux的消息队列 对于消息队列的学习我们还是使用man
来查询学习,先使用man msg
+TAB
键自动补齐,看一下有哪些函数:
获取一个新的消息需要传入key关键字还要设置一个新建的标志msgflg,如果msgflag
设置IPC_CREAT
,那么不管key
值有没有被其它的消息队列占用,都能成功的获取到消息队列,返回该消息队列的ID,如果该消息队列是已创建的则是打开一个已存在的消息队列;如果msgflag
设置为ICP_CREAT | IPC_EXCL
,那么如果key
已经被其他的队列占用的话,是无法获取到该关键字对应的新的消息队列的,返回错误码-1,例如:
int msg_id = msgget(1234, IPC_PRIVATE | IPC_CREAT);
int msg_id1 = msgget(2345, IPC_CREAT | IPC_EXCL);
3.2 发送消息
发送消息队列的API是msgsnd
:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
可以看到在手册中msgsnd
和msgrcv
是一起解释的,对于发送函数msgsnd
,需要传入4个参数:
-
消息队列的ID
msgid
; -
发送的消息数据指针
msgp
; -
发送的消息数据大小
msgsz
; -
发送标志
msgflg
;发送数据指针
mgsp
它指向的是一个形如:
struct msgbuf{
long mtype;
char mtext[1];
};
的结构体,其中mtype
必须是一个大于0的值来表示消息类型,这个类型值在后面接收消息的时候可以用到,比如可以让接收方不接收这个类型的消息(搭配msgflg=MSG_EXCEPT
使用),也可以让接收方接收到消息队列中第一个类型为msgtyp=mtype
的消息,这种情况下就没有队列的先进先出的特性了。
发送的数据大小是msgp
中除了mtype
的数据大小,而不是整个消息结构体的大小。
发送标志支持如下几种:
-
0
:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列 -
IPC_NOWAIT
:如果消息队列满了,新的消息将不会被写入队列 -
IPC_NOERROR
:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程
返回值:
0
:发送消息成功- -1:发送消息失败,错误码在erorr中
使用示例:
struct msgbuf{
long mtype;
char value;
};
struct msgbuf qbuf = {1, 12};
int qmsg = msgget(1234, IPC_CREAT | IPC_PRIVATE);
int ret0 = msgsnd(qmsg, &qbuf.mtype, sizeof(qbuf.value), 0); // 阻塞发送
int ret1 = msgsnd(qmsg, &qbuf.mtype, sizeof(qbuf.value), IPC_NOWAIT); // 非阻塞发送
int ret2 = msgsnd(qmsg, &qbuf.mtype, sizeof(qbuf.value), IPC_NOERROR); // 如果超出截断发送
3.3 接收消息
接收消息的API:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数解释:
参数名称参数释义msqid
消息队列的IDmsgp
存放消息的指针msgsz
指定接收消息的大小msgtyp
执行接收消息的类型:0-读取消息队列中第一个数据;>0:读取消息队列中类型为msgtyp
的第一个数据;如果msgflg=MSG_EXCEPT
,那么队列中和第一个类型不为msgtyp
的数据将会被读取;>8)&0xFF;
uint8_t humi_value = value&0xFF;
lv_slider_set_value(ui_tempSlider, temp_value, LV_ANIM_OFF);
lv_slider_set_value(ui_humiSlider, humi_value, LV_ANIM_OFF);
lv_chart_set_next_value(ui_chart, temp, temp_value);
lv_chart_set_next_value(ui_chart, humi, humi_value);
lv_chart_refresh(ui_chart);
}
我们对于服务器发送的温湿度格式是:数据=温度*256+湿度,也就是代码中的:
value = (temp_value8)&0xFF;
uint8_t humi_value = value&0xFF;
这个格式读者可以自定义,只要子系统发送以及监测系统解析的时候是同一套格式即可。
更新LVGL的chart和slider的值其实很简单,调用LVGL对应的API即可,如果不熟悉可以百度和查看LVGL的官方文档。
4.2 建立mqtt客户端以及订阅主题 我们需要将mqtt的源码移植到工程里面(前提是已经按照前面的文章将mqtt安装到了ubuntu和Linux开发板):
book@100ask:~$ cd /home/book/workspace/lvgl_demo
book@100ask:~/workspace/lvgl_demo$ mkdir mqtt
book@100ask:~/workspace/lvgl_demo$ cd mqtt
book@100ask:~/workspace/lvgl_demo/mqtt$ cp -r /home/book/workspace/mqtt/paho.mqtt.c/src ./
book@100ask:~/workspace/lvgl_demo/mqtt$ touch mqtt_iot.h mqtt_iot.c mqtt.mk
这里直接放源码, 登录服务器的链接地址、用户名这些省略,读者自己去阿里云物联网平台建立设备然后填写信息:
// mqtt_iot.h
#ifndef __MQTT_IOT_H__
#define __MQTT_IOT_H__
#include
#include
#include
typedef enum{
DisconThread,
PubThread
}Mqtt_Thread;
typedef struct
{
long mtype; /* message type, must be > 0 */
unsigned int value; /* message data */
}msgbuf;
int mqtt_disconnect(void);
int mqtt_iot(void);
#endif /* __MQTT_IOT_H__ */
// mqtt_iot.c
#include "mqtt_iot.h"
#include "src/MQTTClient.h" //需要在系统中提前安装好MQTT,可以参考
#include
#include
#include
#include
#define ADDRESS "tcp://${your mqtt server url}:1883" //根据 MQTT 实际主机地址调整
#define CLIENTID "${your client id}"
#define USERNAME "${your username}"
#define PASSWORD "${your password}"
#define QOS 0
#define TIMEOUT 10000L
#define SUB_TOPIC "${your subscribe topic}"
extern void set_temp_humi_data(unsigned short value);
MQTTClient client; //定义一个MQTT客户端client
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
//传递给MQTTClient_setCallbacks的回调函数 消息到达后,调用此回调函数
int msgarrvd(void *context, char *topicName, int topicLen, MQTTClient_message *message)
{
printf("Message arrived\n");
printf(" topic: %s\n", topicName);
printf(" message: %.*s\n", message->payloadlen, (char*)message->payload);
unsigned short value = 0;
unsigned short len = message->payloadlen;
char *buf = (char*)message->payload;
for(unsigned short i=0; i
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?