您当前的位置: 首页 >  嵌入式

韦东山

暂无认证

  • 0浏览

    0关注

    506博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

【嵌入式Linux应用开发】温湿度监控系统——多线程与温湿度的获取显示

韦东山 发布时间:2022-07-12 15:26:14 ,浏览量:0

1. 概述

​ 在前几篇的文章中,我们已经学习了LVGL界面绘制以及paho mqtt的同步客户端和异步客户端的操作,那么本篇就会综合前面的知识,加上Linux系统的多线程以及线程间通信的知识,将LVGL、MQTT、多线程、消息队列这些知识使用起来,形成我们最终的产品。

温湿度监控系统应用开发所有文章
  1. 【嵌入式Linux应用开发】移植LVGL到Linux开发板
  2. 【嵌入式Linux应用开发】初步移植MQTT到Ubuntu和Linux开发板
  3. 【嵌入式Linux应用开发】SquareLine Studio与LVGL模拟器
  4. 【嵌入式Linux应用开发】温湿度监控系统——绘制温湿度折线图
  5. 【嵌入式Linux应用开发】温湿度监控系统——学习paho mqtt的基本操作
  6. 【嵌入式Linux应用开发】温湿度监控系统——多线程与温湿度的获取显示
  7. 【嵌入式Linux应用开发】设计温湿度采集MCU子系统
适用开发板

​ 适用于百问网的STM32MP157开发板和IMX6ULL开发板及其对应的屏幕,需要注意的是编译链要对应更改。

  • 100ASK_STM32MP157
  • 100ASK_IMX6ULL
2. Linux的多线程编程

​ 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退出程序。

2.2 退出线程

​ 我们使用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键自动补齐,看一下有哪些函数:

在这里插入图片描述

3.1 获得一个消息队列

在这里插入图片描述

​ 获取一个新的消息需要传入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);

在这里插入图片描述

可以看到在手册中msgsndmsgrcv是一起解释的,对于发送函数msgsnd,需要传入4个参数:

  • 消息队列的IDmsgid

  • 发送的消息数据指针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            
关注
打赏
1658827356
查看更多评论
0.0401s