目录
一. 选型
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 型指令。
这些指令格式规整有序,体现着数学之美,优点如下:
- 这种指令格式可提高性能功耗比,指令只有六种格式,并且所有的指令都是 32 位长,这简化了指令解码;
- RISC-V 指令提供三个寄存器操作数,而不是像 x86-32 一样,让源操作数和目的操作数共享一个字段。减少了软件的程序操作。
- 在 RISC-V 中对于所有指令,将源寄存器(rs1和rs2)和目标寄存器(rd)固定在同样的位 置,以简化指令译码,意味着在解码指令之前,就可以先开始访问寄存器。
- 立即数被打包,朝着最左边可用位的方向,并且已分配好以减少硬件复杂度。所有立即数的符号位总是在指令的第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 = <®_pio1_8>;
vcc-pfo-supply = <®_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
程序设计思路
- 首先是打开摄像头设备;
- 查询设备的属性或功能;
- 设置设备的参数,譬如像素格式、 帧大小、 帧率;
- 申请帧缓冲、 内存映射;
- 帧缓冲入队;
- 开启视频采集;
- 帧缓冲出队、对采集的数据进行处理;
- 处理完后,再次将帧缓冲入队,往复;
- 结束采集。
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);