离子色谱仪实验[嵌入式Linux项目]-Qt开发日志

环境搭建

先说一下搭建以下环境的原因,开发流程如图。

  1. Win10下开发Qt程序,需要Qt5与Qt Creator
  2. 虚拟机下安装Ubuntu16(高版本如18不行),安装Qt5交叉编译链
  3. 将编译后的ARM环境下的可执行程序通过FTP或SFTP上传至嵌入式Linux生产环境,运行可执行程序。

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第1张图片

ARM生产环境

实验设备为 FriendlyARM 的 CortexA9 Smart4418 实验箱。配套资源网址 为:http://wiki.friendlyarm.com/wiki/index.php/Smart4418/zh

OS:FriendlyCore,是一个没有 X-windows 环境,基于 Ubuntucore 构建的系统,使用 Qt-Embedded 作为图形界面的轻量级系统,兼容 Ubuntu 系统软件源,非常适合于企业用户用作产品的基础 OS。

PC开发环境

开发在虚拟机下的Ubuntu16系统,安装了Qt5、Qt Creator以及Qt5交叉编译链工具。

Qt5安装

sudo apt-get install qt5

Qt Creator安装

Windows上安装Qt Creator,编写运行成功后,传至Linux上进行交叉编译,将目标程序放ARM上直接与运行。

Qt5交叉编译链

FriendlyARM交叉编译链arm-linux-gcc下载地址:https://github.com/friendlyarm/prebuilts

如何交叉编译(以编译QtE-Demo项目为例):

mkdir build && cd build
/usr/local/Trolltech/Qt-5.10.0-nexell32-sdk/bin/qmake ../QtE-Demo.pro
make

在ARM机上运行,如提示:EGL library doesn't support Emulator extensions

输入如下命令解决:

export QT_QPA_EGLFS_INTEGRATION=none

ARM网络设置

ARM连接电脑热点

查看Wifi设备

nmcli dev

开启Wifi设备

nmcli r wifi on

扫描附近Wifi热点

nmcli dev wifi

连接到指定Wifi热点

nmcli dev wifi connect "SSID" password "PASSWORD" ifname wlan0

查看IP地址

ifconfig

第一次连接后,以后每次开机,ARM都会自动连接之前的Wifi。

通过Win10网络设置也可以看到已连接设备的IP。

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第2张图片

SSH远程访问ARM

拿到设备IP后,使用SSH协议远程访问ARM,使用SFTP协议上传下载程序。

工具:MobaXterm

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第3张图片

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第4张图片

ARM I/O读写

GPIO引脚控制LED

linux下对/sys/class/gpio中的gpio的控制 (转)

  1. 配置GPIO
  2. 设置GPIO的方向
  3. 设置GPIO的输出电平

配置GPIO43与GPIO44,其分别控制两个LED灯的亮灭。

cd /sys/class/gpio
echo 43 > export
echo 44 > export
cd gpio43
echo high > direction
echo low > direction
echo 1 > value
echo 0 > value
cd gpio44
...

测试Demo:

//初始化引脚
int fd;
char buf[5];
char gpioExportPath[30] = "/sys/class/gpio/export";
//配置GPIO
fd = open(gpioExportPath, O_WRONLY);
if (fd < 0) {
    printf("gpioExportPath:%s\topen failed", gpioExportPath);
    exit(1);
}
sprintf(buf, "%2d",this->ledNo);
write(fd, buf, 2);
close(fd);

//设置GPIO方向
char gpioDirPath[40];
sprintf(gpioDirPath, "/sys/class/gpio/gpio%d/direction", this->ledNo);
fd = open(gpioDirPath, O_WRONLY);
if (fd < 0) {
    printf("gpioDirPath:%s\topen failed", gpioDirPath);
    exit(1);
}
sprintf(buf, "high");
write(fd, buf, 3);
close(fd);
//控制亮灭
void updateLedStatus(int ledNo)
{
    int fd;
    char buf[2];
    char path[30];
    sprintf(path, "/sys/class/gpio/gpio%d/value" , ledNo);

    fd = open(path, O_WRONLY);
    if (fd < 0) {
        printf("updateLedStatus failed");
        exit(1);
    }

    if (ledStatus)
    {
        buf[0] = '1';
    }
    else
    {
        buf[0] = '0';
    }

    write(fd, buf, 1);

    close(fd);
}

AD读取

测试Demo

#include 
#include 
#include 
#include 

void delay()
{
	int i,j;
	for (i = 0; i < 200; i++)
		for (j = 0; j < 100; j++);
}

int main()
{
	int fd, len, i;
	char buf[20];
	for (i = 0; i < 5; i++)
	{
		fd = open("/sys/devices/platform/c0000000.soc/c0053000.adc/iio:device0/in_voltage7_raw", 0);
		if (fd < 0) exit(1);
		len = read(fd, buf, sizeof buf - 1);
		if (len > 0)
		{
			buf[len] = '\0';
			printf("%s\n", buf);
		}
		delay();
	}

	close(fd);
	return 0;
}

蜂鸣器

http://wiki.friendlyarm.com/wiki/index.php/Matrix_-_Buzzer/zh

https://www.eefocus.com/toradex/blog/17-05/420816_04520.html

cd /sys/class/pwm/pwmchip0
echo 0 > export
cd pwm0
echo 400000000 > period  #周期,单位ns
echo 100000000 > duty_cycle #设置占空比25%
echo 1 > enable  #开启
echo 0 > enable  #关闭

需求建模

用例图

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第5张图片

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第6张图片

活动图

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第7张图片

程序编码

服务端打算用Qt编写,开发流程是:1. 在Win10上使用IDE开发 2. 在虚拟机Linux上交叉编译 3. 在嵌入式Linux上运行程序查看效果。

界面设计

根据详细设计的功能,规划界面内容。

界面上展示的包括:LED控制、色谱图模块、LCD数值显示模块、网络连接展示模块。

后端隐含的功能模块包括:网络连接模块、网络传输模块、网络数据解析模块、GPIO读写模块、AD读取模块、蜂鸣器控制模块。

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第8张图片

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第9张图片

LED控制模块

先来实现LED控制功能。

设计一个LED控制类。

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第10张图片

ledcontrol.h

#ifndef LEDCONTROL_H
#define LEDCONTROL_H

/**
 * @brief LED控制类
 */
class LEDControl
{
public:
    LEDControl(int no);
    void initGPIO(); //初始化GPIO引脚
    void setLedStatus(bool status); //设置LED状态

private:
    void updateLedStatus(); //更新GPIO引脚状态,控制LED亮灭

public:
    int ledNo; //LED编号
    bool ledStatus; //LED状态

};

#endif // LEDCONTROL_H

ledcontrol.cpp

#include "ledcontrol.h"
#include 
#include 
#include 
#include 
#include 
#include 
#include 

/**
 * @brief LED控制类
 */
LEDControl::LEDControl(int no)
{
    this->ledNo = no;
    initGPIO(); //初始化LED对应的GPIO引脚
    this->setLedStatus(true); //默认高电平,灭
}

/**
 * @brief 初始化LED对应的GPIO引脚
 */
void LEDControl::initGPIO()
{
    int fd;
    char buf[5];
    char gpioExportPath[30] = "/sys/class/gpio/export";
    //配置GPIO
    fd = open(gpioExportPath, O_WRONLY);
    if (fd < 0) {
        printf("gpioExportPath:%s\topen failed", gpioExportPath);
        exit(1);
    }
    sprintf(buf, "%2d",this->ledNo);
    write(fd, buf, 2);
    close(fd);

    //设置GPIO方向
    char gpioDirPath[40];
    sprintf(gpioDirPath, "/sys/class/gpio/gpio%d/direction", this->ledNo);
    fd = open(gpioDirPath, O_WRONLY);
    if (fd < 0) {
        printf("gpioDirPath:%s\topen failed", gpioDirPath);
        exit(1);
    }
    sprintf(buf, "high");
    write(fd, buf, 3);
    close(fd);

}

/**
 * @brief 设置LED状态 false亮, true灭
 */
void LEDControl::setLedStatus(bool status)
{
    this->ledStatus = status;
    updateLedStatus();
    qDebug() << "led" << this->ledNo << "\t status:" << this->ledStatus;
}

/**
 * @brief 更新LED状态
 */
void LEDControl::updateLedStatus()
{
    int fd;
    char buf[2];
    char path[30];
    sprintf(path, "/sys/class/gpio/gpio%d/value" , this->ledNo);

    fd = open(path, O_WRONLY);
    if (fd < 0) {
        printf("updateLedStatus failed");
        exit(1);
    }

    if (this->ledStatus)
    {
        buf[0] = '1';
    }
    else
    {
        buf[0] = '0';
    }

    write(fd, buf, 1);

    close(fd);
}

AD读取模块

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第11张图片

adreader.h

#ifndef ADREADER_H
#define ADREADER_H

/**
 * @brief AD读取类
 * 读取电压模拟量
 */
class ADReader
{
public:
    ADReader();
    int readVol(); //电压模拟量读取

private:
    char buf[20]; //AD读取缓冲区
    int readLen; //当次读取数据长度
    int adFileDesc; //AD设备文件描述符
};

#endif // ADREADER_H

adreader.cpp

#include "adreader.h"
#include 
#include 
#include 
#include 

/**
 * @brief AD读取类
 * 读取电压模拟量
 */
ADReader::ADReader()
{

}

/**
 * @brief 读取电压模拟量
 * @return
 */
int ADReader::readVol()
{
    //Win环境下测试用
    //if (true)
    //{
    //   return 800;
    //}

    //打开AD设备文件
    this->adFileDesc = open("/sys/devices/platform/c0000000.soc/c0053000.adc/iio:device0/in_voltage7_raw", 0);
    if (this->adFileDesc < 0)
    {
        printf("adFile open failed");
        exit(1);
    }
    //读取电压模拟量到buf
    this->readLen = read(this->adFileDesc, this->buf, sizeof this->buf - 1);
    if (this->readLen > 0)
    {
        this->buf[this->readLen] = '\0';
    }
    close(this->adFileDesc);
    return atoi(buf); //转化为int返回
}

蜂鸣器控制模块

对于服务端,如果读取的AD值超出安全范围,则控制蜂鸣器发出警报,同时告诉客户端警报开启。

buzzer.h

#ifndef BUZZER_H
#define BUZZER_H

/**
 * @brief 蜂鸣器
 */
class Buzzer
{
public:
    Buzzer();
    void setEnable(int enable_);

private:
    void initPWM();
    void updateEnable();
    int enable; //1开启,0关闭

};

#endif // BUZZER_H

buzzer.cpp

#include "buzzer.h"
#include 
#include 
#include 
#include 
#include 
#include 
#include 

/**
 * @brief 蜂鸣器
 */
Buzzer::Buzzer()
{
    initPWM();
}

/**
 * @brief 初始化PWM
 */
void Buzzer::initPWM()
{
    int fd;
    char buf[15];
    char exportPath[35] = "/sys/class/pwm/pwmchip0/export";
    //配置PWM
    fd = open(exportPath, O_WRONLY);
    if (fd < 0) {
        printf("pwmExportPath:%s\topen failed", exportPath);
        exit(1);
    }
    buf[0] = '0';
    write(fd, buf, 1);
    close(fd);

    //设置PWM周期
    char periodPath[45] = "/sys/class/pwm/pwmchip0/pwm0/period";
    fd = open(periodPath, O_WRONLY);
    if (fd < 0) {
        printf("pwmPeriodPath:%s\topen failed", periodPath);
        exit(1);
    }
    sprintf(buf, "400000000");
    write(fd, buf, 9);
    close(fd);

    //设置PWM占空比
    char dutyCyclePath[55] = "/sys/class/pwm/pwmchip0/pwm0/duty_cycle";
    fd = open(dutyCyclePath, O_WRONLY);
    if (fd < 0) {
        printf("pwmDutyCyclePath:%s\topen failed", dutyCyclePath);
        exit(1);
    }
    sprintf(buf, "100000000");
    write(fd, buf, 9);
    close(fd);
}

/**
 * @brief 更新蜂鸣器状态
 */
void Buzzer::updateEnable()
{
    int fd;
    char buf[2];
    char path[45] = "/sys/class/pwm/pwmchip0/pwm0/enable";
    fd = open(path, O_WRONLY);
    if (fd < 0) {
        printf("updatePWMEnable failed");
        exit(1);
    }

    buf[0] = '0' + this->enable;

    write(fd, buf, 1);

    close(fd);
}

/**
 * @brief 设置蜂鸣器开关状态
 * @param enable
 */
void Buzzer::setEnable(int enable_)
{
    if(this->enable == enable_) return; //防止重复写

    this->enable = enable_;
    updateEnable();
    qDebug() << "PWM: " << this->enable;
}

网络模块

基于TCPSocket进行网络通讯。

设计流程:

  1. 实现TCP服务端
  2. 使用TCP网络调试助手,测试服务端发送接收消息功能无误
  3. 制定通讯协议
  4. 实现TCP客户端

服务端网络模块

使用QT封装的TCP网络通讯类:QTcpServer与QTcpSocket。

使用Qt建立一个TCP服务端分为以下几步:

  • 设置监听端口
  • 绑定newConnection信号与处理该信号的槽函数

Sample:

//设置监听端口
tcpserver->listen(QHostAddress::Any, this->serverPort.toInt());
//新客户端连接
connect(tcpserver, &QTcpServer::newConnection, this, &MainWidget::newConnect);

在newConnect槽函数处理TCP客户端的连接请求。

处理流程:

  1. 获取客户端Socket
  2. 绑定readyRead信号与处理该信号的槽函数
  3. 绑定disconnected信号与处理该信号的槽函数
  4. 获取当前客户端的IP与端口,界面展示用
  5. 将当前客户端Socket添加到List统一管理
/**
 * @brief 新客户端连接
 */
void MainWidget::newConnect()
{
    clientSocket = tcpserver->nextPendingConnection();
    qDebug() << "新客户端连接:"
             << QHostAddress(clientSocket->peerAddress().toIPv4Address()).toString();
    //有可读消息
    connect(clientSocket,SIGNAL(readyRead()),
            this,SLOT(readMessage()));
    //断开连接
    connect(clientSocket,SIGNAL(disconnected()),
            this,SLOT(lostConnect()));

    //地址
    QString currentIP = QHostAddress(clientSocket->peerAddress().toIPv4Address()).toString();
    quint16 currentPort = clientSocket->peerPort();
    //添加到显示面板
    client_list->push_back(currentIP + ":" + QString::number(currentPort));
    ui->tb_clients->clear();
    for (int i = 0; i < client_list->length(); i++)
    {
        ui->tb_clients->append(client_list->at(i));
    }
    //添加到列表
    socket_list->push_back(clientSocket);
}

/**
 * @brief 接收消息
 */
void MainWidget::readMessage()
{
    //遍历客户端列表,所有客户端
    for (int i = 0; i < socket_list->length(); i++)
    {
        read_message = socket_list->at(i)->readAll();
        if(!(read_message.isEmpty()))
        {
            qDebug() << "读取消息 [ClientIP:" << QHostAddress(socket_list->at(i)->peerAddress().toIPv4Address()).toString() << "]";
            qDebug() << "[ClientMsg]:" <<  read_message;
            //回传同样的消息
            sendMessage(read_message);
            //解析消息
            analyzeMessage(read_message, socket_list->at(i));
            break;
        }
    }
}

/**
 * @brief 发送消息
 * @param infomation
 */
void MainWidget::sendMessage(QString infomation)
{
    QByteArray b = infomation.toLatin1();
    char* msg = b.data();
    for(int i = 0; i < socket_list->length(); i++){
        socket_list->at(i)->write(msg);
        qDebug() << "Send:"<< msg;
    }
}

测试TCP服务端

确定接收与发送功能无误。

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第12张图片

通讯协议制定

制定TCP服务端与客户端之间的通讯协议。

数据首部:#

  1. 客户端要读取AD通道0的采样值
    1. 发送 #R1 开启读取
    2. 发送 #R0 关闭读取
  2. 服务端返回采样值
    1. 发送 #B00800,表示800mV,有效数据长度为5。
  3. 客户端要控制LED的亮灭,或者服务端通知客户端当前更新后的LED状态
    1. 发送 #L11 两个LED都灭
    2. 发送 #L00 两个LED都亮
    3. 发送 #L01 LED1灭,LED2亮
    4. 发送 #L10 LED1亮,LED2灭
  4. 服务端返回报警信号:
    1. 发送 #W1 报警提示

客户端网络模块

客户端创建一个TCP连接的流程:

  1. 初始化操作,就是绑定一下需要处理的信号。
    1. 绑定readRead信号与处理该信号的槽函数
  2. 连接建立
    1. 调用connectToHost,指定服务端IP与端口,建立TCP连接
    2. 调用waitForConnected等待连接成功
  3. 连接断开
    1. 调用disconnectFromHost,断开与服务端的连接

Sample:

//init TCP Server
this->tcpClient = new QTcpSocket(this);   //实例化tcpClient
this->tcpClient->abort();                 //取消原有连接
connect(tcpClient, SIGNAL(readyRead()), this, SLOT(ReadData()));
/**
 * @brief 连接按钮点击事件
 */
void MainWidget::on_btn_conn_clicked()
{
    if(ui->btn_conn->text()=="连接")
    {
        tcpClient->connectToHost(ui->edit_ip->text(), ui->edit_port->text().toInt());
        if (tcpClient->waitForConnected(1000))  // 连接成功则进入if{}
        {
            ui->btn_conn->setText("断开");
            ui->btn_send->setEnabled(true);
            ui->btn_led1->setEnabled(true);
            ui->btn_led2->setEnabled(true);
        }
    }
    else
    {
        tcpClient->disconnectFromHost();
        if (tcpClient->state() == QAbstractSocket::UnconnectedState \
                || tcpClient->waitForDisconnected(1000))  //已断开连接则进入if{}
        {
            ui->btn_conn->setText("连接");
            ui->btn_send->setEnabled(false);
        }
    }
}

消息解析模块

消息解析流程:

  1. 将接收到的msg根据#分割(可能一次发多条消息?一次只会发送接收一条消息的话,不加消息头不分割也是可以的)。
  2. 判断消息类型,获取消息携带的数据,执行不同的功能。

离子色谱图绘制模块

QCustomPlot:https://www.qcustomplot.com/

使用的QCustomPlot绘图窗口部件。

优点是没有过多的依赖库,使用方便,将头文件与源文件添加至项目工程即可。

 //init Plot
SendTimeInterval = 500; //发送时间间隔
//开始绘制
setupRealtimeDataDemo(ui->plot);
ui->plot->replot();

初始化Plot

流程:

  1. 添加一条数据线,设置颜色与数据名。
  2. 配置横坐标,数据类型、数据格式、间隔等。
  3. 纵坐标默认配置即可。
  4. 定义一个QTimer定时器,绑定timeout信号与处理该信号的槽函数。
  5. 在槽函数中获取数据并刷新Plot。
/**
 * @brief QCustomPlot初始化
 * @param customPlot
 */
void MainWidget::setupRealtimeDataDemo(QCustomPlot *customPlot)
{
  customPlot->addGraph();
  customPlot->graph(0)->setPen(QPen(Qt::red));
  customPlot->graph(0)->setName("Vol");

  //横坐标
  customPlot->xAxis->setTickLabelType(QCPAxis::ltDateTime);
  customPlot->xAxis->setDateTimeFormat("hh:mm:ss");
  customPlot->xAxis->setAutoTickStep(false);
  customPlot->xAxis->setTickStep(2);
  customPlot->axisRect()->setupFullAxesBox();
  customPlot->legend->setVisible(true);//右上角小图标
  
  //通过QTimer定时获取数据并刷新Plot
  connect(&dataTimer, SIGNAL(timeout()), this, SLOT(realtimeDataSlot()));
  dataTimer.start(SendTimeInterval);
}
/**
 * @brief 绘制折线
 */
void MainWidget::realtimeDataSlot()
{
    //横轴:key 时间 单位s
    double key = QDateTime::currentDateTime().toMSecsSinceEpoch()/1000.0;
    
    //纵轴:value 电压模拟量
    int value = this->adReader->readVol();
    ui->lcd_vol->setDigitCount(4);
    ui->lcd_vol->setMode(QLCDNumber::Dec);
    ui->lcd_vol->display(QString::number(value));

    //发送给客户端
    if (adSendSwitch)
    {
        QString msg = QString("#B%1").arg(value,5,10,QLatin1Char('0'));
        sendMessage(msg);
    }

    //添加数据到曲线0
    ui->plot->graph(0)->addData(key, value);


    //删除8秒之前的数据
    ui->plot->graph(0)->removeDataBefore(key-8);

    //设定graph曲线y轴的范围
    ui->plot->yAxis->setRange(500, 7000);

    ui->plot->xAxis->setRange(key+0.25, 8, Qt::AlignRight);//设定x轴的范围
    ui->plot->replot();
}

远程报警模块

对于服务端,如果读取的AD值超出安全范围,则控制蜂鸣器发出警报,同时告诉客户端警报开启。

客户端实现报警就是播放一段音频。

在QT项目中添加一个资源文件,存放warning.wav报警音频文件。

在项目pro文件,添加multimedia多媒体配置

QT       += core gui network multimedia

通过QSound类播放媒体资源

QSound::play(":/media/warning.wav");

项目部署与运行

将编译后的服务端项目Server放在/usr/local目录下

通过SFTP上传后,设置文件权限:chmod 777 Server

运行:./Server

若运行Qt程序报错:

解决方法:export QT_QPA_EGLFS_INTEGRATION=none

服务端:

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第13张图片

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第14张图片

客户端:

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第15张图片

离子色谱仪实验[嵌入式Linux项目]-Qt开发日志_第16张图片

你可能感兴趣的:(Qt)