智能水培机

news2024/12/23 14:38:19

目录

一. 选型

1.1 Lichee RV Dock

1.1.1 芯片:D1-H

1.1.2 镜像选择:Tina Linux

二. QT上位机

2.1 选择ID

2.2 主界面刷新数据

2.2.1 设置定时器

2.2.3 定义查询数据库表qtnew的函数checkNew_data

2.2.2 定义槽函数 Refresh_data

2.3 主界面按键控制

2.3.1 修改字段的值

2.3 显示图像

2.4 数据库操作

2.4.1 连接数据库

2.4.2 清空表

2.4.3 日期查询

2.4.4 显示所有

2.4.5 向字段插入图像

三. 驱动

3.1 GPIO

3.1.1 总体框架

3.1.2 SoC 级配置

3.1.3 board.dts 板级配置

3.1.4 pinctrl 接口

3.1.5 gpio接口

3.2 TWI

3.2.1 device tree 默认配置

3.2.2 board.dts 板级配置

3.2.3 驱动框架介绍

3.2.4 i2c-core 框架核心层接口

3.2.5 i2c 用户态调用接口

3.3 MIPI屏幕

3.4 RTL8723DS

四. 应用层编写

4.1 GPIO应用

4.1.1 GPIO初始化

4.1.2 GPIO导出

 4.1.3 GPIO取消导出

 4.1.4 GPIO设置方向

4.1.5 GPIO设置电平

 4.1.6 GPIO获取电平

4.2 控制PWM

4.2.1 导出PWM

4.2.2 设置PWM占空比

 4.2.3 设置PWM周期

4.2.4 PWM启用 

4.2.5 PWM禁用 

4.3 UART

4.4 RS485

4.5 连接wifi

4.6 V4L2

4.6.1 打开设备

4.6.2 查看设备的属性和能力

4.6.3 设置采集格式

4.6.4 申请缓冲区空间

4.6.5 内存映射

4.6.6 开启视频采集

4.6.7 读取帧并保存为.jpg格式的图片

4.6.8 读取数据并处理完之后要再次入队

4.6.9 停止采集,释放映射


一. 选型

1.1 Lichee RV Dock

1.1.1 芯片:D1-H

芯片的框图

该芯片内核采用的是平头哥设计的C906 CPU,该芯片是基于64bit RISC-V指令集设计的CPU,主频1GHz,板载512MB DDR3内存。

1 C906基于64bit RISC指令集架构。指令集(ISA)对上限定了软件的基本功能,对下制定了硬件实现的功能目标,它是一个计算机系统支持的所有机器指令的集合,常被看作软硬件之间的分界面,计算机系统工作的基本过程是:程序员编写的软件经编译器翻译成可执行程序,也就是一个机器指令的序列,然后由底层硬件一条条读取这些指令来实现。

指令集对上层软件来说,就是加减乘除等基本操作的一个集合,这些基本操作的堆砌形成了更复杂的软件功能,如队列堆栈,函数调用等。对下层硬件来说,指令集提供了一个计算机系统实现的目标蓝图,底层硬件可以看作对指令集的实现

RISC:如ARM架构,中心思想是指令系统的简化,除访问指令外其他指令的操作均在单周期内完成,并且是从寄存器到寄存器进行指令操作,使用大寄存器组,定长指令,译码方便,更容易实现流水线。

RISC-V架构:随着万物互联的趋势,RISC-V作为一款开源,低功耗,轻量型指令集,它将会广泛应用于IoT领域。

而RISC-V的指令集架构:

1 指令格式:

指令格式的选择需要考虑指令长度、地址码结构以及操作码结构。

共有6种指令格式

  • 用于寄存器-寄存器操作的 R 类型指令
  • 用于短立即数和访存 load 操作的 I 型指令
  • 用于访存 store 操作的 S 型指令
  • 用于条件跳转操作的 B 类型指令
  • 用于长立即数的 U 型指令
  • 用于无条件跳转的 J 型指令

这些指令格式规整有序,体现着数学之美,优点如下:

  1. 这种指令格式可提高性能功耗比,指令只有六种格式,并且所有的指令都是 32 位长,这简化了指令解码;
  2. RISC-V 指令提供三个寄存器操作数,而不是像 x86-32 一样,让源操作数和目的操作数共享一个字段。减少了软件的程序操作。
  3. 在 RISC-V 中对于所有指令,将源寄存器(rs1和rs2)和目标寄存器(rd)固定在同样的位 置,以简化指令译码,意味着在解码指令之前,就可以先开始访问寄存器。
  4. 立即数被打包,朝着最左边可用位的方向,并且已分配好以减少硬件复杂度。所有立即数的符号位总是在指令的第31位,以加速符号扩展电路。

2 RV32 寄存器

RISC-v有32个寄存器,RV32I 有 31 寄存器加上一个值恒为 0 的 x0 寄存器,我们只需使用零寄存器作为操作数完成功能相同的操作。访问寄存器中的数据要比访问存储器中的快得多,ARM-32有16个寄存器,X86-32有8个,RISC-V(包括大多数现代ISA)都有32个整型寄存器。寄存器越多,编译器和汇编程序员得工作会更轻松。一般每条 RISC-V 指令最多用一个时钟周期执行(忽略缓存未命中),ARM-32 和 x86-32 都有需要多个时钟周期执行的指令。

3 RV32 的Load和Store

RV32I 省略了 ARM-32 和 x86-32 的复杂寻址模式。RISC-V 没有特殊的堆栈指令,是将31个寄存器中某个作为堆栈指针。与 MIPS-32 不同,RISC-V 不支持延迟加载(delayed load),对于更长的流水线,延迟加载带来的收益逐渐消失,因此 RISC-V 无延迟槽。

RISC-V 去掉了 MIPS-32,Oracle SPARC 等指令集中被广为诟病的延迟分支特性等;对于条件分支,它还没有像 ARM-32 和 x86-32 那样使用条件码

4、访问指令

RISC-v架构的指令数目很简洁,基本指令只有40多条,采用小端格式,采用松散存储器模型。RISC-V架构有6条带条件跳转指令,减少了指令的条数,同时硬件设计上更简单。RISC-V对于没有分支预测器的低端CPU,采用静态分支预测机制,对于配有硬件分支预测器的高端CPU,还可采用高级的动态分支预测机制来保证性能。

5、定制指令集

RISC-V支持第三方的扩展,用户可以扩展自己的指令子集。如乘除法扩展,浮点运算扩展,原子指令,向量指令等。除此之外,可将CNN集成到硬件中,未来可扩展。几乎可以构建任何一个领域的微处理器,如云计算、存储、并行计算、虚拟化、MCU、应用处理器和DSP处理器等。

总结,RISC-V的特性如下:

1)32位字节可寻址的地址空间。

2)所有指令均为32位长。

3)31个寄存器组,32位宽。

4)所有的指令操作都是从寄存器到寄存器,访问快。(没有寄存器到内存)

5)所有算术、逻辑和移位指令都有立即数版本的指令。

6)仅提供一种数据寻址模式(寄存器+立即数)和PC相对分支

7)无乘法或除法指令

1.1.2 镜像选择:Tina Linux

D1-H官方给了三个镜像--Tina Linux,Debian,Ubuntu。但是因为Tina Linux是开源的并且资料齐全,所以项目中选择了Tina Linux这一SDK进行开发。Tina并不是传统意义上的操作系统,它是全志为其RISC-V系列处理器提供的一种轻量级,开源的嵌入式系统软件平台,和开发环境,软件框架,可能包括系统启动程序,驱动程序等。而Debian是一个完整的操作系统,它是一个开源的Linux发行版。

二. QT上位机

2.1 选择ID

下面的代码主要是对lineEdit的输入进行判断,判断是否输入为空,是否输入只包含空格,输入是否含有其他字符,如果输入只包含数字,则输入正确。输入正确则先去除输入中的空格,再声明一个指向'MainWindow'类的指针'w',且创建一个'MainWindow'类的实例,并调用构造函数。new关键字在堆上分配内存并返回新对象的指针,这个指针被赋值给'w'。

void ID_Select::on_pushButton_clicked()
{
    if (ui->lineEdit->text().isEmpty()) {
         // 如果输入为空
         QMessageBox::information(nullptr, "Notice", "没有输入");
     } else if (ui->lineEdit->text().trimmed().isEmpty()) {
         // 如果输入只包含空格
         QMessageBox::information(nullptr, "Notice", "输入只包含空白字符");
     } else
     {
            QRegularExpression regex("^\\d+$");  // 正则表达式匹配整数
            if (regex.match(ui->lineEdit->text()).hasMatch()) {
                // 如果输入只包含数字
                QMessageBox::information(nullptr, "Notice", "输入只包含数字: " + ui->lineEdit->text());
                //trimmed()去除空格
                ID_N = ui->lineEdit->text().trimmed().toInt();
                MainWindow *w = new MainWindow(ID_N);
                w->show();
            } else {
                // 如果输入包含非数字字符
                QMessageBox::information(nullptr, "Notice", "输入包含非数字字符: " + ui->lineEdit->text());
            }

     }
}

2.2 主界面刷新数据

2.2.1 设置定时器

timer是一个QTimer对象的指针。setInterval(1000) 设置定时器的触发间隔为1000毫秒,即1秒,即定时器每隔一秒会发出一次timeout信号

timer->setInterval(1000); // 每1秒触发一次

 connect将timeout信号连接到槽函数Refresh_data。

 connect(timer, &QTimer::timeout, this, &MainWindow::Refresh_data)

QTimer *timer = new QTimer(this)
timer->setInterval(1000); // 每1秒触发一次
connect(timer, &QTimer::timeout, this, &MainWindow::Refresh_data);
timer->start(); // 启动定时器

2.2.3 定义查询数据库表qtnew的函数checkNew_data

准备查询语句," FROM qtnew " 指定数据表为qtnew,WHERE ID = ?,设置查询条件,'ID'字段的值等于一个占位符,这个占位符号用query.addBindValue(ID_S)替换为一个具体的值:ID_S。

QSqlQuery query;
query.prepare("SELECT * FROM qtnew WHERE ID = ?");
query.addBindValue(ID_S);
//查询MySQL的qtdata中最新的数据
void MainWindow::checkNew_data(QVariant *penlin_data,QVariant *fan_data,QVariant *shuijiare_data,QVariant *light_control,\
                               QVariant* level_control,QVariant *temp_control,\
                               QVariant *temp, QVariant *humi, QVariant *light, QVariant *CO2, QVariant *level,QVariant *position)
{
    // 准备查找语句
    QSqlQuery query;

    query.prepare("SELECT * FROM qtnew WHERE ID = ?");

//    query.addBindValue(getPrivateVar(ID_select));
//    qDebug()<<getPrivateVar(ID_select);
//    query.addBindValue(ID_select->getPrivateVar());
    query.addBindValue(ID_S);

    qDebug()<<"ID_S before setting text:"<<ID_S;
   qDebug()<<"ID_S:"<<ID_S;

    if (!query.exec())
    {
        qDebug() << "Error executing query:" << query.lastError().text();
        return;
    }
    if (query.next())
    {
        *penlin_data    = query.value(1);
        *fan_data       = query.value(2);
        *shuijiare_data = query.value(3);
        *level_control  = query.value(4);
        *temp_control   = query.value(5);
        if (light_flag == 0)
        *light_control  = query.value(6);     
        *CO2            = query.value(7);
        *position       = query.value(8);
        *light          = query.value(9);
        *level          = query.value(10);
        *temp           = query.value(11);
        *humi           = query.value(12);
         //qDebug() << "Latest order ID:" << temp.toString();
    }
    else
    {
         qDebug() << "No records found.";
    }
}

2.2.2 定义槽函数 Refresh_data

调用checkNew_data函数,读取数据库的数据更新。并且将数据库的值在QT的相应控价显示,并且如果设备端有控制变化,QT上位机也可以显示控制的状态。

void MainWindow::Refresh_data(void)
{
    checkNew_data(&penlin_data,&fan_data,&shuijiare_data,&light_control,\
                   &level_control,&temp_control,\
                   &temp, &humi, &light, &CO2, &level,&position);

    //设置控制滑杆的位置和值
    ui->horizontalSlider->setValue(light_control.toInt());
    ui->setlightNumla->setNum(light_control.toInt());

    ui->wendu->setText(temp.toString());
    ui->shidu->setText(humi.toString());
    ui->guangzhao->setText(light.toString());
    ui->co2->setText(CO2.toString());
    ui->shuiwei->setText(level.toString());
    ui->weizhi->setText(position.toString());

    if(penlin_data.toInt() == 1)
    {
        //喷淋打开
        action_penlin->setIcon(QIcon(":/new/prefix1/picture/pic/penglin_on.png"));
    }
    else
    {
        //喷淋关闭
        action_penlin->setIcon(QIcon(":/new/prefix1/picture/pic/penglin_off.png"));
    }

    if(fan_data.toInt() == 1)
    {
        //通风打开
        action_fan->setIcon(QIcon(":/new/prefix1/picture/pic/fan_on.png"));
    }
    else
    {
        //通风关闭
        action_fan->setIcon(QIcon(":/new/prefix1/picture/pic/fan_off.png"));
    }

    if(shuijiare_data.toInt() == 1)
    {

        //热水打开
        action_shuijiare->setIcon(QIcon(":/new/prefix1/picture/pic/water_item_on.png"));
    }
    else
    {
        //热水关闭
        action_shuijiare->setIcon(QIcon(":/new/prefix1/picture/pic/water_item_off.png"));
    }

    switch(level_control.toInt())
    {
        case -1:
            ui->pushButton->setEnabled(true);
            ui->pushButton_7->setEnabled(true);
            ui->pushButton_4->setEnabled(false);

        break;
        case 0:
            ui->pushButton->setEnabled(true);
            ui->pushButton_7->setEnabled(false);
            ui->pushButton_4->setEnabled(true);

        break;
        case 1:
            ui->pushButton->setEnabled(false);
            ui->pushButton_7->setEnabled(true);
            ui->pushButton_4->setEnabled(true);

        break;
        default:;break;
    }

    switch(temp_control.toInt())
    {
        case -1:
            ui->pushButton_2->setEnabled(true);
            ui->pushButton_8->setEnabled(true);
            ui->pushButton_9->setEnabled(false);
        break;
        case 0:
            ui->pushButton_2->setEnabled(true);
            ui->pushButton_8->setEnabled(false);
            ui->pushButton_9->setEnabled(true);
        break;
        case 1:
            ui->pushButton_2->setEnabled(false);
            ui->pushButton_8->setEnabled(true);
            ui->pushButton_9->setEnabled(true);
        break;
        default:;break;
    }
}

2.3 主界面按键控制

对于各个按钮定义槽函数,下面用喷淋按钮举例;根据是否按下,给表中相应字段发送改变的状态,改变字段的值。

//喷淋
void MainWindow::on_penlin_triggered()
{

    if(penlin_data.toInt() == 1)
    {
        penlin_data = 0;
        //action_penlin->setIcon(QIcon(":/new/prefix1/picture/pic/penglin_off.png"));
    }
    else
    {
        penlin_data = 1;
        //action_penlin->setIcon(QIcon(":/new/prefix1/picture/pic/penglin_on.png"));
    }
    qDebug()<<penlin_data;
    ChangeNew_Field("喷淋",penlin_data.toInt());
}

2.3.1 修改字段的值

//将某一字段(query_Field_Name)的值改为Field_Data
void MainWindow::ChangeNew_Field(QString query_Field_Name,int field_Data)
{
    // 准备更新语句
    QSqlQuery query;
    query.prepare("UPDATE qtnew SET "+query_Field_Name+" = ? WHERE ID = ?");

    // 绑定参数
//    query.addBindValue(field_Data);
//    query.addBindValue(ID_select.getPrivateVar());
    query.addBindValue(field_Data);
    //query.addBindValue(ID_select->getPrivateVar());
    query.addBindValue(ID_S);
    qDebug()<<ID_S;
    //执行更新
    if (!query.exec())
    {
         qDebug() << "Error executing query:" << query.lastError().text();
    }
    else
    {
        qDebug() << "Update successful!";
    }

}

2.3 显示图像

1 创建QPixmap对象photo用于存储图像

2 从查询结果中提取图像数据,QByteArray imageData = query.value(0).toByteArray();

3 使用photo.loadFromData()从二进制数据中加载图像,指定图像格式为JPG

4 如果加载成功,将图像设置到 QLabel 控件 camera_show 中,并设置 setScaledContents(true) 使图像自动调整大小以适应 QLabel

void MainWindow::show_Image()
{
    // 准备查找语句
    QSqlQuery query;

    query.prepare("SELECT * FROM camera WHERE ID = ?");

    query.addBindValue(ID_S);

    if (!query.exec()) {
         qDebug() << "Query execution error:" << query.lastError().text();
         return;
     }

    if (query.next())
    {
        QPixmap photo;
        // 从数据库中读取图片为二进制数据,图片格式为 JPG,然后显示到 QLabel 里
        QByteArray imageData = query.value(0).toByteArray();
        if (!photo.loadFromData(imageData, "JPG")) {
            qDebug() << "Failed to load image from data";
            return;
        }

        ui->camera_show->setPixmap(photo);
        ui->camera_show->setScaledContents(true);
    }
    else
    {
        qDebug() << "No data found for the specified ID";
    }

}

2.4 数据库操作

2.4.1 连接数据库

void MainWindow::connectMySQL(void)
{
    qDebug()<<QSqlDatabase::drivers();
    QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
    db.setHostName("xx.xx.xx.xx");      //连接数据库主机名,这里需要注意(若填的为”127.0.0.1“,出现不能连接,则改为localhost)
    db.setPort(3306);                 //连接数据库端口号,与设置一致
    db.setUserName("qt");          //数据库用户名,与设置一致
    db.setPassword("zhongkeruihe");    //数据库密码,与设置一致
    db.setDatabaseName("qtsql");      //连接数据库名,与设置一致
    bool ok = db.open();
    if (ok){
       // QMessageBox::information(this, "infor", "success");
         qDebug()<<"success";
    }
    else {
        QMessageBox::information(this, "警告!", "DataBase Open Failed,Check whether there is a database or whether the parameters are correct!!!");
        qDebug()<<"error open database because"<<db.lastError().text();
        //ui->label->setText(db.lastError().text());
    }
}

2.4.2 清空表

清空表并且弹出警示框

bool database::clearDBTable()
{
    QSqlQuery query;
    QString strClearDB = "delete from qtdata";

    //创建一个消息框,询问用户是否确认操作
    QMessageBox::StandardButton reply;
    reply = QMessageBox::question(this,"清除数据",
       "确定要清除表中数据?",QMessageBox::Yes|QMessageBox::No);

    //根据用户的选择执行相应的操作
    if (reply == QMessageBox::Yes)
    {
        query.prepare(strClearDB);
    }
    else if(reply == QMessageBox::No)
    {

    }

    if(!query.exec())
    {
        qDebug()<<query.lastError();
    }
    return true;
}

2.4.3 日期查询

查找在两个时间段内插入的表中的数据

void database::SelectData()
{
    tableModel = new QSqlQueryModel;//定义一个数据库模型,指定父对象

    QString startTime = ui->dateTimeEdit->text();
    QString startTime2 = ui->dateTimeEdit_2->text();

    //查询操作
    QString strSelectData = "select *from qtdata where CurrentTime between '"+startTime+"' and '"+startTime2+"';";
    tableModel->setQuery(strSelectData);
    ui->tableView->setModel(tableModel);
    ui->tableView->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter);
    ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
}

2.4.4 显示所有

void database::SelectAllPushTableData(){
    tableModel = new QSqlQueryModel;//定义一个数据库模型,指定父对象

    QString strSelectData = "select * from qtdata";
    tableModel->setQuery(strSelectData);

//    ui->tableView->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter);
//    ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
    ui->tableView->setModel(tableModel);
    ui->tableView->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter);
    ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
}

2.4.5 向字段插入图像

QSqlQuery query;
QImage image("path/to/image.png"); // 读取图像
QByteArray byteArray;
QBuffer buffer(&byteArray);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "PNG"); // 将图像保存到byteArray中

query.prepare("UPDATE tableName SET imageField = :image WHERE id = 1");
query.bindValue(":image", byteArray);
if (!query.exec()) {
    qDebug() << "Update failed: " << query.lastError().text();
}

三. 驱动

3.1 GPIO

3.1.1 总体框架

Sunxi Pinctrl 整个驱动模块可以分为4个部分:Pinctrl interfaces,Pinctrl common frame,sunxi Pinctrl driver 以及 board configuration

Pinctrl interfaces:pinctrl 提供给上层用户调用的接口

Pinctrl framework:Linux提供的pinctrl驱动框架。主要处理Pinctrl Core,Pinctrl mux 和 Pinctrl conf 三个功能。

Pinctrl sunxi driver:sunxi 平台需要实现的驱动

board configuration:设置 pin 配置信息,一般采用设备树(dts)的方式进行配置

3.1.2 SoC 级配置

pio: pinctrl@2000000 {
	compatible = "allwinner,sun20iw1-pinctrl"; /* 兼容属性,用于驱动和设备绑定 */
	reg = <0x0 0x02000000 0x0 0x500>; /* 寄存器基地址0x02000000和范围0x500 */
	interrupts-extended = <&plic0 85 IRQ_TYPE_LEVEL_HIGH>,
	<&plic0 87 IRQ_TYPE_LEVEL_HIGH>,
	<&plic0 89 IRQ_TYPE_LEVEL_HIGH>,
	<&plic0 91 IRQ_TYPE_LEVEL_HIGH>,
	<&plic0 93 IRQ_TYPE_LEVEL_HIGH>,
	<&plic0 95 IRQ_TYPE_LEVEL_HIGH>; /* 该设备每个bank支持的中断配置和gic中断号,每个中断
	号对应一个支持中断的bank */
	device_type = "pio"; /* 设备类型属性 */
	clocks = <&ccu CLK_APB0>, <&dcxo24M>, <&rtc_ccu CLK_OSC32K>;
	clock-names = "apb", "hosc", "losc";
	gpio-controller; /* 表示是一个gpio控制器 */
	#gpio-cells = <3>; /* gpio属性需要配置的参数个数 */
	interrupt-controller; /* 表示是一个中断控制器 */
	#interrupt-cells = <3>; /* pin中断属性需要配置的参数个数,不支持中断可以删除 */
	#size-cells = <0>;
	vcc-pf-supply = <&reg_pio1_8>;
	vcc-pfo-supply = <&reg_pio3_3>;
 }

3.1.3 board.dts 板级配置

相同属性和结点,board.dts 的配置信息会覆盖 sun20iw1p1.dtsi 中的配置信息。

board.dts 中新增加的属性和结点,会追加到最终生成的 dtb 文件中。

&pio {
	sdc0_pins_a: sdc0@0 {
		allwinner,pins = "PF0", "PF1", "PF2",
				 "PF3", "PF4", "PF5";
		allwinner,function = "sdc0";
		allwinner,muxsel = <2>;
		allwinner,drive = <3>;
		allwinner,pull = <1>;
		pins = "PF0", "PF1", "PF2",
		       "PF3", "PF4", "PF5";
		function = "sdc0";
		drive-strength = <30>;
		bias-pull-up;
		power-source = <3300>;
	};
    ...........
}

3.1.4 pinctrl 接口

pinctrl_get,pinctrl_put,devm_pinctrl_get,devm_pinctrl_put,pinctrl_lookup_state,pinctrl_select_state,devm_pinctrl_get_select,devm_pinctrl_get_select_default,pinctrl_gpio_set_config

3.1.5 gpio接口

gpio_request,gpio_free,gpio_direction_input,gpio_direction_output,__gpio_get_value,__gpio_set_value,of_get_named_gpio,of_get_named_gpio_flags

3.2 TWI

全志公司的TWI总线兼容I2C总线协议,是一种简单,双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息,TWI控制器支持的标准通信速率为100Kbps,最高通信速率可以达到400Kbps。全志的TWI控制器支持以下功能:

支持主机模式和从机模式

主机模式下支持dma传输

主机模式下在多个主机的模式下支持总线仲裁

主机模式下支持时钟同步,位和字节等待

从机模式下支持地址检测中断

支持7bit 从机地址和10bit 从机地址

支持常规的 i2c 协议模式和自定义传输模式

3.2.1 device tree 默认配置

twi0: twi@0x05002000{
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "allwinner,sun20i-twi"; //具体的设备,用于驱动和设备的绑定
	device_type = "twi0"; //设备节点名称,用于sys_config.fex匹配
	reg = <0x0 0x02502000 0x0 0x400>; //TWI0总线寄存器配置
	interrupts-extended= <&plic0 25 IRQ_TYPE_LEVEL_HIGH>; //TWI0总线中断号、中断类型
	clocks = <&ccu CLK_BUS_I2C0>;//twi控器使用的时钟
	resets = <&ccu RST_BUS_I2C0>;//twi控器使用的reset时钟
	clock-names = "bus";
	clock-frequency = <400000>; //TWI0控制器的时钟频率
	dmas = <&dma 43>, <&dma 43>;//TWI0控制器的dma通道号
	dma-names = "tx", "rx";
	status = "disabled";//TWI0控制器是否使能
};

3.2.2 board.dts 板级配置

其中,TWI 速率由 “clock-frequency” 属性配置,最大支持 400K。

&twi0 {
	clock-frequency = <400000>;
	pinctrl-0 = <&twi0_pins_a>;
	pinctrl-1 = <&twi0_pins_b>;
	pinctrl-names = "default", "sleep";
	status = "disabled";
	eeprom@50 {
	compatible = "atmel,24c16";
	reg = <0x50>;
	status = "disabled";
};
};
twi0_pins_a: twi0@0 {
	pins = "PB10", "PB11"; /*sck sda*/
	function = "twi0";
	drive-strength = <10>;
};
twi0_pins_b: twi0@1 {
	pins = "PB10", "PB11";
	function = "gpio_in";
};

3.2.3 驱动框架介绍

Linux 中 I2C 体系结构上图所示,图中用分割线分成了三个层次:1. 用户空间,包括所有使用 I2C 设备的应用程序;2. 内核,也就是驱动部分;3. 硬件,指实际物理设备,包括了 I2C 控制 器和 I2C 外设。

其中,Linux 内核中的 I2C 驱动程序从逻辑上又可以分为 6 个部分:

1. I2C framework 提供一种 “访问 I2C slave devices” 的方法。由于这些 slave devices 由 I2C controller 控制,因而主要由 I2C controller 驱动实现这一目标

2. 经过I2C framework 的抽象,用户可以不用关心I2C总线的技术细节,只需要调用系统的接口,就可以与外部设备进行通信。正常情况下,外部设备是位于内核态的其他driver。I2C framework 也通过字符设备向用户空间提供类似的接口,用户空间程序可以通过接口访问从设备信息

3. 在 I2C framework 内部,有 I2C core、I2C busses、I2C algos 和 I2C muxes 四个模 块。

4. I2C core 使用 I2C adapter 和 I2C algorithm 两个子模块抽象 I2C controller 的功能, 使用 I2C client 和 I2C driver 抽象 I2C slave device 的功能(对应设备模型中的 device 和 device driver)。另外,基于 I2C 协议,通过 smbus 模块实现 SMBus(System Management Bus,系统管理总线)的功能。

5. I2C busses 是各个 I2C controller drivers 的集合,位于 drivers/i2c/busses/目录下, i2c-sunxi.c、i2c-sunxi.h。

6. I2C algos 包含了一些通用的 I2C algorithm,所谓的 algorithm,是指 I2C 协议的通信方 法,用于实现 I2C 的 read/write 指令,一般情况下,都是由硬件实现,不需要特别关注该目 录。

3.2.4 i2c-core 框架核心层接口

i2c_transfer;i2c_master_recv;i2c_master_send;i2c_smbus_read_byte;i2c_smbus_write_byte;i2c_smbus_read_byte_data;i2c_smbus_write_byte_data;i2c_smbus_read_word_data;i2c_smbus_write_word_data;i2c_smbus_read_block_data;i2c_smbus_write_block_data

3.2.5 i2c 用户态调用接口

i2cdev_open;i2cdev_read;i2cdev_write;i2cdev_ioctl

3.3 MIPI屏幕

MIPI-DSI的管脚是差分的,分为两种管脚,一种是时钟管脚,另外一种是数据管脚。

MIPI屏幕的dts,设备树配置:

&lcd0 {
	lcd_used            = <1>;

	lcd_driver_name     = "tft08006";
	lcd_backlight       = <100>;
	lcd_if              = <4>;
	//0:Video mode:实时刷屏,有ht,hbp等时序参数的定义
	lcd_dsi_if          = <0>;

	lcd_x               = <800>;
	lcd_y               = <480>;
	lcd_width           = <52>;
	lcd_height          = <52>;

	lcd_dclk_freq       = <26>;


	/*
	由下面两条公式得知,我们不需要设置lcd_hfp和lcd_vfp参数,因为驱动会自动根据其它几个已知
	参数中算出lcd_hfp和lcd_vfp。
	lcd_ht = lcd_x + lcd_hspw + lcd_hbp + lcd_hfp
	lcd_vt = lcd_y + lcd_vspw + lcd_vbp + lcd_vf
	*/
	//890 540 感觉还行
	lcd_hbp             = <46>;
	lcd_ht              = <900>;//885
	lcd_hspw            = <4>;
	lcd_vbp             = <21>;
	lcd_vt              = <550>;//535
	lcd_vspw            = <4>;
	//1:1 data lane
	lcd_dsi_lane        = <1>;
	//0:Package Pixel Stream,24bit RGB
	lcd_dsi_format		= <0>;
	//0:frame trigged automatically
	//刷屏时间为lcd_ht x lcd_vt
	lcd_dsi_te			= <0>;

	//0:一个port
	lcd_dsi_port_num	= <0>;
	//0:normal mode
	lcd_tcon_mode 		= <0>;
	//0:中断自动根据时序,由场消隐信号内部触发
	lcd_cpu_mode		= <0>;
	//0:Single Link(1 clock pair+3/4 data pair)
	// lcd_lvds_if         = <0>;
	// //0:8bit per color(4 data pair)
	// lcd_lvds_colordepth = <0>;
	// lcd_lvds_mode       = <0>;
	//0:RGB888 --- RGB888 direct
	lcd_frm             = <0>;
	//这些参数只有在 lcd_if=0 时才有效
	// lcd_hv_clk_phase    = <0>;
	//0:vsync active low,hsync active low
	//lcd_hv_sync_polarity= <0>;
	lcd_io_phase        = <0x0000>;
	//0:Lcd的Gamma校正功能关闭
	lcd_gamma_en        = <0>;
	lcd_bright_curve_en = <0>;
	//0:Lcd的色彩映射功能关闭
	lcd_cmap_en         = <0>;
	//0:disable
	lcd_fsync_en        = <0>;
	//LCD 的 fsync 功能,其中的有效电平时间长度,单位:像素时钟的个数。
	lcd_fsync_act_time  = <1000>;
	//无效电平时间长度
	lcd_fsync_dis_time  = <1000>;
	//0:有效电平为低
	lcd_fsync_pol       = <0>;

	deu_mode            = <0>;
	lcdgamma4iep        = <22>;
	smart_color         = <90>;

/*	lcd_gpio_0 =  <&pio PG 13 GPIO_ACTIVE_HIGH>;*/
	pinctrl-0 = <&dsi2lane_pins_a>;
	pinctrl-1 = <&dsi2lane_pins_b>;
};

3.4 RTL8723DS

Tina SDK 2.0 只提供了XR829的驱动,但是我的底板是使用的RTL8723DS,因此我们得自己移植驱动。

四. 应用层编写

4.1 GPIO应用

4.1.1 GPIO初始化

bool GPIO::initialize()
{
    if (m_initialized) {
        return true; // Already initialized
    }

    // Export GPIO pin
    if (!exportPin()) {
        return false;
    }

    m_initialized = true;
    return true;
}

4.1.2 GPIO导出

bool GPIO::exportPin()
{
    QFile file("/sys/class/gpio/export");
    if (file.open(QIODevice::WriteOnly))
    {
        QTextStream stream(&file);
        stream << m_pin;
        file.flush();
        file.close();
        return true;
    }
    qWarning() << "Failed to export GPIO pin:" << m_pin;
    return false;
}

 4.1.3 GPIO取消导出

bool GPIO::unexportPin()
{
    QFile file("/sys/class/gpio/unexport");
    if (file.open(QIODevice::WriteOnly))
    {
        QTextStream stream(&file);
        stream << m_pin;
        file.close();
        return true;
    }
    qWarning() << "Failed to unexport GPIO pin:" << m_pin;
    return false;
}

 4.1.4 GPIO设置方向

bool GPIO::setDirection(const QString &direction)
{
    QFile file(m_path + "direction");
    if (file.open(QIODevice::WriteOnly))
    {
        QTextStream stream(&file);
        stream << direction;
        file.close();
        return true;
    }
    qWarning() << "Failed to set direction for GPIO pin:" << m_pin;
    return false;
}

4.1.5 GPIO设置电平

bool GPIO::setValue(int value)
{
    QFile file(m_path + "value");
    if (file.open(QIODevice::WriteOnly))
    {
        QTextStream stream(&file);
        stream << value;
        file.close();
        return true;
    }
    qWarning() << "Failed to set value for GPIO pin:" << m_pin;
    return false;
}

 4.1.6 GPIO获取电平

int GPIO::getValue()
{
    QFile file(m_path + "value");
    if (file.open(QIODevice::ReadOnly))
    {
        QTextStream stream(&file);
        int value;
        stream >> value;
        file.close();
        return value;
    }
    qWarning() << "Failed to get value for GPIO pin:" << m_pin;
    return -1;
}

4.2 控制PWM

4.2.1 导出PWM

bool PWM::exportPWM()
{
    QFile file(pwmChipPath);
    if (!file.open(QIODevice::WriteOnly)) {
        qWarning() << "Failed to open file for writing:" << pwmChipPath;
        return false;
    }

    QTextStream stream(&file);
    stream << 2;
    file.close();

    // Verify if the PWM channel is exported by checking the existence of the directory
    QFile pwmDir(pwmPath);
    if (!pwmDir.exists()) {
        qWarning() << "Failed to export PWM channel. Directory does not exist:" << pwmPath;
        return false;
    }

    return true;
}

4.2.2 设置PWM占空比

bool PWM::setPeriod(int period) {
    QFile file(pwmPath + "period");
    if (file.open(QIODevice::WriteOnly)) {
        QTextStream stream(&file);
        stream << period;
        file.close();
        return true;
    }
    qWarning() << "Failed to set PWM period";
    return false;
}

 4.2.3 设置PWM周期

bool PWM::setDutyCycle(int dutyCycle) {
    QFile file(pwmPath + "duty_cycle");
    if (file.open(QIODevice::WriteOnly)) {
        QTextStream stream(&file);
        stream << dutyCycle;
        file.close();
        return true;
    }
    qWarning() << "Failed to set PWM duty cycle";
    return false;
}

4.2.4 PWM启用 

bool PWM::enable() {
    QFile file(pwmPath + "enable");
    if (file.open(QIODevice::WriteOnly)) {
        QTextStream stream(&file);
        stream << 1;  // 1 表示启用
        file.close();
        return true;
    }
    qWarning() << "Failed to enable PWM";
    return false;
}

4.2.5 PWM禁用 

bool PWM::disable() {
    QFile file(pwmPath + "enable");
    if (file.open(QIODevice::WriteOnly)) {
        QTextStream stream(&file);
        stream << 0;  // 0 表示禁用
        file.close();
        return true;
    }
    qWarning() << "Failed to disable PWM";
    return false;
}

4.3 UART

我使用的CO2模块的数据流格式为:

初始化串口并且定义handleReadyRead()槽函数,接收CO2串口模块的数据

void Serial::serial_Open()
{
             CO2_Data.resize(6);

            // 删除旧的串口对象(如果存在)
            if (serial) {
                delete serial;
            }

            serial = new QSerialPort();
            serial->setPortName("/dev/ttyS1"); // 你的串口名称
            serial->setBaudRate(QSerialPort::Baud9600);
            serial->setDataBits(QSerialPort::Data8);
            serial->setParity(QSerialPort::NoParity);
            serial->setStopBits(QSerialPort::OneStop);
            serial->setFlowControl(QSerialPort::NoFlowControl);

            // 打开串口,只允许读
            if (serial->open(QIODevice::ReadOnly)) {
                qDebug() << "UART_1 opened successfully.";
            } else {
                qDebug() << "Failed to open UART_1";
            }

            // 连接信号和槽
        connect(serial, &QSerialPort::readyRead, this, &Serial::handleReadyRead);
}

void Serial::handleReadyRead()
{
    QByteArray data = serial->readAll();
    qDebug() << "Received data:" << data;

    // 处理接收到的数据
    for (char byte : data) {
        if (index < CO2_Data.size()) {
            CO2_Data[index++] = byte; // 使用 QByteArray 的赋值方法
        }

        if (index >= CO2_Data.size()) {
            // 校验数据的正确性
            if (CO2_Data[0] == 0x2C &&
                CO2_Data[5] == (uint8_t)(CO2_Data[0] + CO2_Data[1] + CO2_Data[2] + CO2_Data[3] + CO2_Data[4]))
            {
                // 更新UI,确保在主线程中更新 UI
                QMetaObject::invokeMethod(ui->co2, "setText", Qt::QueuedConnection,
                                          Q_ARG(QString, QString::number(CO2_Data[1] * 256 + CO2_Data[2])));

                // 打印调试信息
                qDebug() << CO2_Data[0] << " " << CO2_Data[1] << " " << CO2_Data[2] << " "
                         << CO2_Data[3] << " " << CO2_Data[4] << " " << CO2_Data[5];
            }

            // 重置索引
            index = 0;
        }
    }
}

4.4 RS485

我使用的温湿度模块的传输格式为:

(1)命令报文格式

  主机发送读温湿度数据命令:

地址

功能码

数据起始地址高位

数据起始地址低位

数据个数高位

数据个数低位

CRC

16位校验

xx

03

00

02

00

02

xxxx低位在前

 从机传感器返回温湿度数据值:

地址

功能码

字节长度

温度返回数据

湿度返回数据

CRC

16位校验

xx

03

04

xxxx高位在前

xxxx高位在前

xxxx低位在前

主机发送读地址命令:

地址

功能码

数据起始地址高位

数据起始地址低位

数据个数高位

数据个数低位

CRC

16位校验

00

03

00

00

00

01

xxxx低位在前

 从机传感器返回地址值:

地址

功能码

字节长度

地址高位

地址低位

CRC

16位校验

00

03

02

00

xx

xxxx低位在前

主机发送地址设置命令:

地址

功能码

写入位置高位

写入位置低位

操作数高位

操作数低位

字节长度

写入内容高位

写入内容低位

CRC

16位校验

00

10

00

00

00

01

02

00

xx

xxxx低位在前

 从机传感器返回响应值:

地址

功能码

写入位置高位

写入位置低位

操作数高位

操作数低位

CRC

16位校验

00

10

00

00

00

01

xxxx低位在前

(2)帧格式(10位)

起始位

  D0

  D1

  D2

  D3

  D4

  D5

  D6

  D7

停止位

下面的代码包含了这几个过程

下面的代码包含了这几个过程

串口初始化,创建新的QSerialPort对象并配置串口参数,如端口号,波特率,数据位等

发送请求,创建一个 QByteArray 对象,按照协议格式追加数据字节,包括从站地址、功能码、寄存器地址及数量。

处理接收数据函数,处理从串口读取到的数据

处理响应,对接收到的数据进行检查和提取数据,比如我读取的是温湿度模块,所以我读取温湿度的值。

计算CR16,通过查找表计算数据的CRC16的值

发送地址更改请求

//RS485
void Serial::serial_485_Open()
{
    // 删除旧的串口对象(如果存在)
    if (serial_485) {
        delete serial_485;
    }

    serial_485 = new QSerialPort(this);
    serial_485->setPortName("/dev/ttyS4"); // 你的串口名称
    serial_485->setBaudRate(QSerialPort::Baud9600);
    serial_485->setDataBits(QSerialPort::Data8);
    serial_485->setParity(QSerialPort::NoParity);
    serial_485->setStopBits(QSerialPort::OneStop);
    serial_485->setFlowControl(QSerialPort::NoFlowControl);

    // 打开串口
    if (serial_485->open(QIODevice::ReadWrite)) {
        qDebug() << "UART_4 opened successfully.";
        sendRequest();
    } else {
        qDebug() << "Failed to open UART_4";
    }

    connect(serial_485, &QSerialPort::readyRead, this, &Serial::handleReadyRead_485);
}
void Serial::sendRequest()
{
    QByteArray request;
    request.append(static_cast<char>(0x01));  // 从站地址
    request.append(static_cast<char>(0x03));  // 功能码
    request.append(static_cast<char>(0x00));  // 起始地址高字节
    request.append(static_cast<char>(0x02));  // 起始地址低字节
    request.append(static_cast<char>(0x00));  // 寄存器数量高字节
    request.append(static_cast<char>(0x02));  // 寄存器数量低字节

    // 计算 CRC
    uint crc = CRC16(request);
    request.append(static_cast<char>(crc & 0xFF));     // CRC 低位
    request.append(static_cast<char>((crc >> 8) & 0xFF));  // CRC 高位

    serial_485->write(request);
    if (serial_485->waitForBytesWritten(1000)) {
        qDebug() << "Request sent successfully.";
    } else {
        qDebug() << "Failed to send request.";
    }
}

void Serial::handleReadyRead_485()
{
    QByteArray response = serial_485->readAll();
    processResponse(response);
}
void Serial::processResponse(const QByteArray &response)
{
    if (response.size() != 9)// 长度应为9
    {
        qDebug() << "Invalid response length.";
        return;
    }

     // 提取数据
    unsigned char temperatureH = response[3];
    unsigned char temperatureL = response[4];
    unsigned char humidityH = response[5];
    unsigned char humidityL = response[6];
    unsigned char crcL = response[7];
    unsigned char crcH = response[8];

    // 计算 CRC
    QByteArray data = response.left(7);  // 取前7个字节进行CRC校验(不包括 CRC 字节)
    uint calculatedCRC = CRC16(data);

    // 校验 CRC
    if (calculatedCRC == ((crcH << 8) | crcL)) {
        qDebug() << "CRC valid!";
        qDebug() << "Temperature:" << ((temperatureH << 8) | temperatureL);
        qDebug() << "Humidity:" << ((humidityH << 8) | humidityL);
    } else {
        qDebug() << "CRC invalid!";
    }
}
uint Serial::CRC16(const QByteArray &data)
{
    unsigned char uchCRCHi = 0xFF;  // 高CRC字节初始化
           unsigned char uchCRCLo = 0xFF;  // 低CRC字节初始化
           uint uIndex;

           static const unsigned char auchCRCHi[] = {
               // CRC 查找表的高字节部分
               0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
               0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
               0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
               0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
               0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
               0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
               0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
               0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
               0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1,
               0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
               0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
               0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
               0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1,
               0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
               0x00, 0xC1, 0x81, 0x40
           };

           static const unsigned char auchCRCLo[] = {
               // CRC 查找表的低字节部分
               0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7,
               0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E,
               0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9,
               0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC,
               0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
               0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32,
               0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D,
               0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38,
               0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF,
               0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
               0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1,
               0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4,
               0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB,
               0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA,
               0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
               0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0,
               0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 0x5D, 0x9D,
               0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A,
               0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83,
               0x41, 0x81, 0x80, 0x40
           };

           for (int i = 0; i < data.size(); ++i)
           {
               uIndex = uchCRCHi ^ static_cast<unsigned char>(data[i]);  // 计算CRC
               uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex];
               uchCRCLo = auchCRCLo[uIndex];
           }

           return (uchCRCHi << 8) | uchCRCLo;
}

void Serial::sendAddressChangeRequest()
{
    QByteArray request;

    // 构造请求数据
    request.append(static_cast<char>(0x00));  // 地址(主机地址)
    request.append(static_cast<char>(0x10));  // 功能码(写寄存器)
    request.append(static_cast<char>(0x00));  // 写入位置高位
    request.append(static_cast<char>(0x00));  // 写入位置低位
    request.append(static_cast<char>(0x00));  // 操作数高位(这里假设为0)
    request.append(static_cast<char>(0x01));  // 操作数低位(当前地址为0x01)
    request.append(static_cast<char>(0x02));  // 字节长度(修改地址需要2字节)
    request.append(static_cast<char>(0x00));  // 写入内容高位(新地址高位)
    request.append(static_cast<char>(0x02));  // 写入内容低位(新地址低位)

    // 计算CRC
    uint crc = CRC16(request);
    request.append(static_cast<char>(crc & 0xFF));     // CRC 低位
    request.append(static_cast<char>((crc >> 8) & 0xFF));  // CRC 高位

    serial_485->write(request);
    if (serial_485->waitForBytesWritten(1000)) {  // 等待最多3000毫秒
        qDebug() << "Address change request sent successfully.";
    } else {
        qDebug() << "Failed to send address change request.";
    }
}

4.5 连接wifi

执行一个外部命令,通过QProcess进行管理。进程启动执行wifi_connect_ap_test ssid password

void Serial::wifi_Connect(int *flag,const QString  &command)
{
    // 创建 QProcess 对象
        QProcess *process = new QProcess(this); // 'this' 是 'Serial' 类的实例,继承自 QObject

        // 设置进程的可执行文件
       // process->setProgram(command);

        // 启动进程
        process->start(command);

        // 等待进程启动
        if (!process->waitForStarted()) {
            qWarning() << "Failed to start process";
            process->deleteLater(); // 使用 deleteLater 释放内存
            return;
        }

        // 等待进程完成
        if (!process->waitForFinished()) {
            qWarning() << "Process did not finish correctly";
            process->deleteLater(); // 使用 deleteLater 释放内存
            return;
        }

        // 获取输出结果
        QString output = process->readAllStandardOutput();
        QString error = process->readAllStandardError();

        // 打印输出和错误信息
        qDebug() << "Process finished with exit code:" << process->exitCode();
        qDebug() << "Exit status:" << process->exitStatus();
        qDebug() << "Output:" << output;
        qDebug() << "Error:" << error;

        if (output.isEmpty()) {
            *flag = 0; // 标记输出为空,失败
        } else {
            *flag = 1; // 标记成功
        }

        // 释放内存
        process->deleteLater(); // 使用 deleteLater 释放内存
}

4.6 V4L2

程序设计思路

  1. 首先是打开摄像头设备;
  2. 查询设备的属性或功能;
  3. 设置设备的参数,譬如像素格式、 帧大小、 帧率;
  4. 申请帧缓冲、 内存映射;
  5. 帧缓冲入队;
  6. 开启视频采集;
  7. 帧缓冲出队、对采集的数据进行处理;
  8. 处理完后,再次将帧缓冲入队,往复;
  9. 结束采集。

4.6.1 打开设备

视频类设备对应得设备节点为/dev/videoX(X为0、1、2…)

	//定义一个设备描述符
    int fd;
    fd = open("/dev/videoX", O_RDWR);
    if(fd < 0){
        perror("video设备打开失败\n");
        return -1;
    }
    else{
        printf("video设备打开成功\n");
    }

4.6.2 查看设备的属性和能力

1. 首先查看设备是否为视频采集设备

    ioctl(fd, VIDIOC_QUERYCAP, &vcap);
    if (!(V4L2_CAP_VIDEO_CAPTURE & vcap.capabilities)) {
        perror("Error: No capture video device!\n");
        return -1;
    }

 2. 查看摄像头所支持的所有像素格式,先看一下v4l2_fmtdesc结构体

struct v4l2_fmtdesc {
	__u32 index; /* index 就是一个编号 */
	__u32 type; /* enum v4l2_buf_type */
	__u32 flags;
	__u8 description[32]; /* description 字段是一个简单地描述性字符串 */
	__u32 pixelformat; /* pixelformat 字段则是对应的像素格式编号 */
	__u32 reserved[4];
};

代码实现,使用VIDIOC_ENUM_FMT

    struct v4l2_fmtdesc fmtdesc;
    fmtdesc.index = 0;
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    
    printf("摄像头支持所有格式如下:\n");
    while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc) == 0){
        printf("v4l2_format%d:%s\n",fmtdesc.index,fmtdesc.description);
        fmtdesc.index++;
    }

3. 查看摄像头所支持的分辨率,先看一下v4l2_frmsizeenum结构体

struct v4l2_frmsizeenum {
	__u32 index; /* Frame size number */
	__u32 pixel_format; /* 像素格式 */
	__u32 type; /* type */
	union { /* Frame size */
		struct v4l2_frmsize_discrete discrete;
		struct v4l2_frmsize_stepwise stepwise;
	};
	__u32 reserved[2]; /* Reserved space for future use */
};

struct v4l2_frmsize_discrete {
	__u32 width; /* Frame width [pixel] */
	__u32 height; /* Frame height [pixel] */
};

比如我们要枚举出摄像头 MJPEG 像素格式所支持的所有分辨率:

struct v4l2_frmsizeenum frmsize;
frmsize.index = 0;
frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

printf("MJPEG格式支持所有分辨率如下:\n");
frmsize.pixel_format = V4L2_PIX_FMT_MJPEG;
while(ioctl(fd,VIDIOC_ENUM_FRAMESIZES,&frmsize) == 0){
    printf("frame_size<%d*%d>\n",frmsize.discrete.width,frmsize.discrete.height);
    frmsize.index++;
}

4. 查看摄像头所支持的视频采集帧率,先看一下v4l2_frmivalenum结构体

struct v4l2_frmivalenum {
	__u32 index; /* Frame format index */
	__u32 pixel_format;/* Pixel format */
	__u32 width; /* Frame width */
	__u32 height; /* Frame height */
	__u32 type; /* type */
	union { /* Frame interval */
	struct v4l2_fract discrete;
	struct v4l2_frmival_stepwise stepwise;
	};
	__u32 reserved[2]; /* Reserved space for future use */
};

struct v4l2_fract {
	__u32 numerator; //分子
	__u32 denominator; //分母
};

比如我们要枚举出摄像头 MJPEG 格式下640*480分辨率所支持的帧数:

    struct v4l2_frmivalenum frmival;
    frmival.index = 0;
    frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    frmival.pixel_format = V4L2_PIX_FMT_MJPEG;
    frmival.width = 640;
    frmival.height = 480;
    while(ioctl(fd,VIDIOC_ENUM_FRAMEINTERVALS,&frmival) == 0){
        printf("frame_interval under frame_size <%d*%d> support %dfps\n",frmival.width,frmival.height,frmival.discrete.denominator / frmival.discrete.numerator);
        frmival.index++;
    }

4.6.3 设置采集格式

首先要定义结构体v4l2_format来保存采集格式信息,使用VIDIOC_S_FMT指令设置格式,最后用VIDIOC_G_FMT指令查看相关参数是否生效

    struct v4l2_format vfmt;
    vfmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    vfmt.fmt.pix.width = 640;
    vfmt.fmt.pix.height = 480;
    vfmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
    if(ioctl(fd,VIDIOC_S_FMT,&vfmt) < 0){
        perror("设置格式失败\n");
        return -1;
    }
    // 检查设置参数是否生效
    if(ioctl(fd,VIDIOC_G_FMT,&vfmt) < 0){
        perror("获取设置格式失败\n");
        return -1;
    }
    else if(vfmt.fmt.pix.width == 640 && vfmt.fmt.pix.height == 480 && vfmt.fmt.pix.pixelformat == V4L2_PIX_FMT_MJPEG){
        printf("设置格式生效,实际分辨率大小<%d * %d>,图像格式:Motion-JPEG\n",vfmt.fmt.pix.width,vfmt.fmt.pix.height);
    }
    else{
        printf("设置格式未生效\n");
    }

4.6.4 申请缓冲区空间

申请帧缓冲顾名思义就是申请用于存储一帧图像数据的缓冲区, 使 VIDIOC_REQBUFS 指令可申请帧缓冲
其中struct v4l2_requestbuffers 结构体描述了申请帧缓冲的信息,代码实现如下:

    struct v4l2_requestbuffers reqbuf;
    reqbuf.type  = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    reqbuf.count = 3;   //3个帧缓冲
    reqbuf.memory = V4L2_MEMORY_MMAP;
    if(ioctl(fd,VIDIOC_REQBUFS,&reqbuf) < 0){
        perror("申请缓冲区失败\n");
        return -1;
    }

4.6.5 内存映射

    // 将帧缓冲映射到进程地址空间
    void *frm_base[3];  //映射后的用户空间的首地址
    unsigned int frm_size[3];

    struct v4l2_buffer buf;
    buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    // 将每一帧对应的缓冲区的起始地址保存在frm_base数组中,读取采集数据时,只需直接读取映射区即可
    for(buf.index=0;buf.index<3;buf.index++){
        ioctl(fd, VIDIOC_QUERYBUF, &buf);
        frm_base[buf.index] = mmap(NULL,buf.length,PROT_READ | PROT_WRITE,MAP_SHARED,fd,buf.m.offset);
        frm_size[buf.index] = buf.length;

        if(frm_base[buf.index] == MAP_FAILED){
            perror("mmap failed\n");
            return -1;
        }

        // 入队操作
        if(ioctl(fd,VIDIOC_QBUF,&buf) < 0){
            perror("入队失败\n");
            return -1;
        }
    }

4.6.6 开启视频采集

    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(fd, VIDIOC_STREAMON, &type) < 0){
        perror("开始采集失败\n");
        return -1;
    }

4.6.7 读取帧并保存为.jpg格式的图片

    struct v4l2_buffer  readbuffer;
    readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    readbuffer.memory = V4L2_MEMORY_MMAP;
    if(ioctl(fd, VIDIOC_DQBUF, &readbuffer) < 0){
        perror("读取帧失败\n");
    }

    // 保存这一帧,格式为jpg
    FILE *file = fopen("v4l2_cap.jpg","w+");
    fwrite(frm_base[readbuffer.index],buf.length,1,file);
    fclose(file);

4.6.8 读取数据并处理完之后要再次入队

    if(ioctl(fd,VIDIOC_QBUF,&readbuffer) < 0){
        perror("入队失败\n");
    }

4.6.9 停止采集,释放映射

    // 停止采集
    if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0){
        perror("停止采集失败\n");
        return -1;
    }

    // 释放映射
    for(int i=0;i<3;i++){
        munmap(frm_base[i],frm_size[i]);
    }
    
	close(fd);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2103525.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

讨论运维监控工具的普及程度

在讨论运维监控工具的普及程度时&#xff0c;加入PIGOSS BSM产品的分析是非常有意义的&#xff0c;因为PIGOSS BSM是一款在中国市场具有一定影响力的运维监控工具。 PIGOSS BSM运维监控工具是一款综合性的IT运维监控解决方案&#xff0c;它能够对多层次的IT资源进行监测&#x…

2024.09.04【读书笔记】|如何使用GATK ASEReadCounter工具进行ASE(等位基因特异性表达)分析

准备数据&#xff1a; 获取基因组序列&#xff08;FASTA格式&#xff09;和对应的基因组注释文件&#xff08;GTF或GFF格式&#xff09;。获取样本的BAM文件&#xff0c;确保这些文件已经过排序和索引。获取变异信息文件&#xff08;VCF格式&#xff09;&#xff0c;包含样本的…

运动耳机哪个牌子的好?精选5款值得入手的骨传导运动耳机分享!

在过去的两年里&#xff0c;骨传导耳机逐渐被大众的所熟知。可能毕竟长时间使用音量过大的传统入耳式耳机&#xff0c;多多少少会对我们的听力健康构成威胁。所以很多人就想找一款不伤耳朵的耳机。然后就了解到了骨传导耳机&#xff0c;所以就会延伸出这些问题——骨传导耳机好…

【效率工具】推荐五款电脑桌面软件,轻量好用!

电脑桌面软件可以帮助我们在电脑上创建分区&#xff0c;然后将文件、文件夹、应用程序等图标整理到对应的分区中&#xff0c;方便我们精准访问文件。不同的电脑桌面软件功能可能存在一些差异&#xff0c;本文分享几个常用的桌面整理工具&#xff0c;以及一些以及分享一些关于电…

EvoSuite使用总结

1.安装EvoSuite插件 以IDEA为例&#xff0c;在Plugins栏搜索EvoSuite后点击install&#xff0c;安装完成后重启IDEA 2.使用EvoSuite 选中文件右键选择Run EvoSuite 生成成功可以看到如下提示&#xff1a; 注意事项&#xff1a; 生成路径&#xff1a;src/test/java 使用juni…

【C++ 第十九章】异常

1.C语言传统的处理错误的方式 传统的错误处理机制&#xff1a; 终止程序&#xff0c;如 assert&#xff0c;缺陷&#xff1a;用户难以接受。如发生内存错误、除 0 错误时就会终止程序。 返回错误码&#xff0c;缺陷&#xff1a;需要程序员自己去查找对应的错误。如系统的很多库…

趣解网络安全专业术语(保密性、暴露)零基础入门到精通,收藏这一篇就够了

保密性confidentiality 使信息不泄露给未授权的个人、实体、进程&#xff0c;或不被其利用的特性。 想象一下&#xff0c;你有一个神秘的盒子&#xff0c;里面装满了你最珍贵的秘密。这个盒子有一个特殊的锁&#xff0c;只有你和你最亲密的朋友能打开它。这个锁特别聪明&#…

新一代交互模式:LUICUIVUI

随着技术的发展&#xff0c;特别是人工智能和机器学习的进步&#xff0c;交互方式也在不断演变。以下是一些新概念&#xff0c;它们描述了当下和未来可能的交互方式&#xff1a; Conversational UI (CUI)&#xff1a; 以对话为基础的用户界面&#xff0c;用户通过自然语言与系统…

Moveit2 Move Group C++ 接口

系列文章目录 留空 文章目录 系列文章目录前言一、完整代码二、编写步骤三、代码分析1. 引入必要的头文件2. 初始化和配置 ROS2 环境3. 设置 MoveIt 规划组和场景4. 可视化5. 获取基本信息6. 开始演示7. 规划姿态目标8. 可视化计划路径9. 移动到姿势目标10. 规划关节空间目标1…

OpenAI发布GPT-4o mini,3.5从此退出历史舞台?

随着OpenAI在2024年7月18日正式发布GPT-4o Mini&#xff0c;无疑在科技界引发了一场新的风暴。这一创新不仅标志着GPT-3.5模型正式退出历史舞台&#xff0c;更预示着人工智能在自然语言处理领域迈入了一个全新的时代。 之前速度最快的模型一直是GPT3.5&#xff0c;随着后来的GP…

数据结构——开篇

一、数据结构&#xff08;内存中&#xff09; 1、定义 用来保存一种或多种特定关系的数据的集合&#xff08;组织和存储数据&#xff09;。 程序 数据结构算法 2、特定关系 &#xff08;1&#xff09;逻辑结构 数据元素与元素之间的关系。 分类&#xff1a;①集合&…

电脑找不到x3daudio1_7.dll怎么解决?5种方法科学修复x3daudio1_7.dll

如果在使用电脑过程中遇到“找不到x3daudio1_7.dll”的错误&#xff0c;这通常意味着您的系统缺少一个关键组件&#xff0c;它是与 Microsoft DirectX 相关的一个文件&#xff0c;主要用于处理高级音频功能&#xff0c;尤其是在游戏和其他多媒体应用程序中。其实这个问题通常可…

传统CV算法——基于Opencv的多目标追踪算法

基于 OpenCV 的跟踪算法有多种&#xff0c;每种算法都有其特定的应用场景和优缺点。以下是一些常见的基于 OpenCV 的目标跟踪算法&#xff1a; 1. BOOSTING 跟踪器 描述&#xff1a;基于 AdaBoost 算法的跟踪器。它是一种早期的跟踪算法&#xff0c;使用的是基于弱分类器的强…

归并、计数排序(画图详解)

归并排序&#xff1a; 基本思想&#xff1a;先递归再回归&#xff0c;在回归的时候进行归并排序 归并排序&#xff1a; 适用于两个有序数组&#xff0c;合并成一个数组的时候&#xff0c;也就是先要递归&#xff0c;递归到最后就相当于一个元素&#xff0c;一个元素就是有序的。…

Unity数据持久化 之 二进制存储法

本文仅作笔记学习和分享&#xff0c;不用做任何商业用途 本文包括但不限于unity官方手册&#xff0c;unity唐老狮等教程知识&#xff0c;如有不足还请斧正​​ 前置知识&#xff1a;1 Byte 8 bit &#xff0c;所以0000 00001 就是一个字节&#xff0c; 该串数字转为十进制代表1…

通过cmd命令的方式转码MP4为webp动图。附带命令解释。

zihao 通过cmd命令的方式转码MP4为webp动图&#xff1a; 均衡大小和z效果的配置&#xff08;直接拷贝后需要改下路径&#xff09;&#xff1a; ffmpeg -i E:\steam\222.mp4 -vcodec libwebp -filter:v fpsfps24 -lossless 0 -compression_level 5 -q:v 35 -loop 1 -preset def…

深入浅出Promise,循序渐进掌握JavaScript异步编程

一. Promise基本用法 Promise 是 JavaScript 中处理异步操作的一种方式。它是一个对象&#xff0c;代表了一个异步操作的最终完成或失败的结果。 Promise 有三种状态&#xff1a; pending &#xff08;进行中&#xff09;、 fulfilled &#xff08;已成功&#xff09; 和 rej…

如何在SQL Server中恢复多个数据库?

一次性恢复多个 SQL数据库吗可以吗&#xff1f; "是的&#xff0c;可以一次性恢复多个 SQL 数据库。通常情况下&#xff0c;只要备份文件的名称与相应的数据库匹配&#xff0c;且没有附加的日期或时间信息&#xff0c;就可以通过有效的 T-SQL 脚本来完成恢复。如果你希望…

虚幻引擎VR游戏开发03| 键位映射

Enhanced input mapping 按键映射 在虚幻引擎&#xff08;Unreal Engine&#xff09;中&#xff0c;Enhanced Input Mapping 是一个用于管理和处理输入&#xff08;例如键盘、鼠标、手柄等&#xff09;的系统。它提供了一种更灵活、更强大的方式来定义和响应用户输入&#xff…

MMO移动同步(1)

多个客户端同时连入游戏 这篇会从以下五个部分讲解&#xff1a; 同步的基本概念 完善角色进入及离开处理 CharacterManager(C/S) EntityManager(C/S) 打包运行Win客户端 同步基本概念 同步&#xff1a;角色信息&#xff0c;位置&#xff0c;状态同步&#xff1b;客户端和…