OrangePi Kunpeng Pro体验——安装Hass与驱动SPI小屏幕

OrangePi Kunpeng Pro 是一款面向开发者和爱好者的高性能开发板。在本次测评中,主要将以前的一些代码在该开发板上实现,包括docker部署hass,引脚驱动SPI小屏幕。中间遇到了一些小小问题,但都成功了,一起来试试吧~ 

一、开箱

1. 开箱全貌

快递第三天收到了主办方寄来的OrangePi Kunpeng Pro套装(主板,8G,电源,散热组件,32GB存储卡),SD卡中已经安装了最新的openEuler系统,即插即用👍。

 

2. 主板观赏

以下说几个比较关注的,具体的主板说明可参考官方链接。

正面:CPU、内存、无线网卡和一些接口指示灯。

接口:PD电源输入、HDMI、多个USB3.0接口和一个千M网口。

 

3. 背面接口

清晰明了的三种不同存储接入接口:SD卡、SSD和EMMC,并可通过两个拨码开关选择启动方式。特别注意的是,虽然M2接口支持nvme和sata两种SSD硬盘,但是默认是nvme硬盘,如果接入的是sata硬盘,需要进行额外的操作(如图就是sata硬盘,额外操作在后续会介绍)。

 

二、基础入门

0. 说明

1) 账号密码均为:openEuler

 

1. 烧录系统到sata固态硬盘(nvme可跳过前三个步骤)

SD卡速度慢,手头有一个sata固态硬盘,正好用上。但是sata的固态硬盘,需要额外的修改才能够被开发板识别。

1) 系统烧录:使用balenaEtcher,将系统烧录到硬盘中(使用移动硬盘盒),随后插入到开发板M.2接口中(由于无螺丝,使用了胶带简单固定),但此时还无法读取到这个硬盘。

2) 暂不更改拨码开关,从SD卡进入系统,在此系统下更新SATA 驱动需要的的dt.img 文件。

首先进入/opt/opi_test/sata 文件夹:

cd /opt/opi_test/sata

然后运行下update.sh 脚本来更新SATA 对应的dt.img

sudo ./update.sh

然后重启,使用lsblk查看硬盘,可正常识别:

3) 将SD卡的dt.img配置,更新到sata硬盘中(需要根据情况修改sata硬盘的节点名称,如图为sda)

sudo dd if=/opt/opi_test/dt_img/dt_drm_sata.img of=/dev/sda count=4096 seek=114688 bs=512

4) 切换拨码开关,以SSD方式启动,顺利开机。使用df -h可查看当前系统空间。

 

2. 增加swap内存

开发板内存有8G,大部分应用已经完全足够,不够时还可以通过设置swap扩展系统内存。

1) 创建一个swap文件

sudo fallocate -l 16G /swapfile

2) 依次配置

sudo chmod 600 /swapfile # 权限为root用户可以读写 
sudo mkswap /swapfile 
sudo swapon /swapfile 
free -h # 查看内存结果

可以看到swap空间为15G(小数部分被直接忽略了),使用free -m可以看到更详细的数据。

3) 设置重启自动生效

将对应的配置添加到/etc/fstab 文件中。

echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

 

3. 配置无线网络

前面都用的是有线网口,接下来通过ssh配置其连接无线wifi。

1) 使用 nmcli 命令扫描附近的 Wi-Fi 网络:

nmcli dev wifi list

2) 使用 nmcli 命令连接到你的 Wi-Fi 网络。假设Wi-Fi SSID 是 SSIDWiFi,密码是 MyPassword,请进行修改:

sudo nmcli dev wifi connect SSIDWiFi password MyPassword

3) 验证连接状态:

nmcli dev status 
# 或者 ifconfig

 

4. 安装docker

方法1:直接使用 YUM 安装 Docker。简单,但可能安装的是系统软件仓库中提供的较老版本的 Docker。

sudo yum update -y
sudo yum install -y docker
sudo systemctl enable docker
sudo systemctl start docker

方法2:通过 YUM 源安装 Docker。首先添加了 Docker 的 YUM 源,然后使用 yum install 命令安装 Docker 软件包。确保了安装的是最新版本的 Docker,并且可以通过 YUM 包管理器进行更新。

1) 更新

sudo yum update -y

2) 添加 Docker YUM 源

需要添加如下源,如果后面update报错,可以删除该文件。

简单说明:Docker 官方提供了适用于 CentOS/RHEL 的 YUM 源,而 openEuler 在很大程度上与 CentOS/RHEL 兼容,因此使用这些源进行 Docker 的安装。

sudo nano /etc/yum.repos.d/docker-ce.repo

添加以下内容:

[docker-ce-stable]
name=Docker CE Stable - $basearch
baseurl=https://download.docker.com/linux/centos/7/$basearch/stable
enabled=1
gpgcheck=1
gpgkey=https://download.docker.com/linux/centos/gpg

3) 安装 Docker

sudo yum install -y docker-ce docker-ce-cli containerd.io

4) 启动 Docker 服务

sudo systemctl start docker                 # 启动服务
sudo systemctl enable docker                # 开机自启
sudo systemctl status docker                # 查看状态

5) 测试

sudo docker run hello-world

6) 其他一些指令

以下为使用docker常用的一些指令:

docker --version                     # 查看 Docker 版本
docker run hello-world               # 运行一个 Hello World 容器
docker ps -a                         # 列出所有容器
docker images                        # 列出所有镜像
docker stop CONTAINER_ID             # 停止一个运行中的容器
docker start CONTAINER_ID            # 启动一个停止的容器
docker rm CONTAINER_ID               # 移除一个容器
docker rmi IMAGE_ID                  # 移除一个镜像
docker logs CONTAINER_ID             # 查看容器日志
docker exec -it CONTAINER_ID /bin/bash # 进入一个运行中的容器
docker stats CONTAINER_ID            # 查看容器的资源使用情况
docker build -t my-image:latest .    # 构建一个 Docker 镜像
docker pull ubuntu:latest            # 拉取一个 Docker 镜像
docker push my-image:latest          # 推送一个 Docker 镜像到仓库
docker info                          # 显示 Docker 系统信息
docker network ls                    # 查看 Docker 网络配置
docker network create NETWORK_NAME   # 创建一个 Docker 网络
docker network connect NETWORK_NAME CONTAINER_ID # 连接容器到指定网络
docker network disconnect NETWORK_NAME CONTAINER_ID # 断开容器与网络的连接

 

三、功耗测量

虽然针对这个高性能开发板,低功耗是不太可能了,但是测量功耗可以明确对电源的需求。目前开发板搭配了最高3A的电源(仅考虑12V)。

1.说明

开发板使用了256G Sata固态硬盘作为系统盘,插入了网线和电源,不接显示器使用ssh登录。使用系统自带的风扇调节方案无修改。依次测量开机、CPU25%、50%和75%运行下、和关机的功耗。

 

2. 测量程序

写一个cpu_stress.py程序,占用一个核进行满负荷运行(25%CPU占用)。多开可占用更多的CPU资源。

#!/bin/bash

echo "Starting CPU stress test..."

while true; do
    # 执行一些无限循环的计算任务,例如计算圆周率
    echo "scale=5000; 4*a(1)" | bc -l >/dev/null
done

 

3. 结果

1) 开机:接入typeC供电后,开发板自动开机。首先开发板通过PD协议让电源输入电压升到12V,风扇启动,电流最大到1.1A,经过40s后稳定到660mA。

2) cpu运行测量:依次测量CPU占用25%、50%和75%时的功耗,由于其中1核被设置为了AI核,无法被该程序调用,因此最高占用只有75%。在25%、50%和75%占用时,电流分别为750mA、800mA和970mA

3) 关机:最后,通过指令poweroff,使开发板关机,测量功耗。此时电压仍保持在12V,电流为280mA,风扇不转。

 

四、Docker部署Hass

在安装docker后,依次安装homeassistant、数据库、mqtt服务器、esphome、nodered,并让他们互相链接,是指一个完善的、且可方便更新的智能家居管理系统,而且不影响其他服务的安装。

1. 配置docker

在上述安装好docker后,通过如下指令添加当前用户到Docker组:

sudo usermod -aG docker ${USER}
newgrp docker    # 重新登录,以应用用户组更改

 

 

2. 安装docker-compose

由于 openEuler 使用 ARM 架构,需要下载适用于 ARM 的 Docker Compose 二进制文件:

1) 下载 Docker Compose

sudo curl -L "https://github.com/docker/compose/releases/download/$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d'"' -f4)/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

2) 设置执行权限

sudo chmod +x /usr/local/bin/docker-compose

3) 验证安装

docker-compose --version

 

3. 准备hass文件夹

首先需要创建一个hass-all文件夹,用于放上述几个容器的文件,并配置文件夹权限。随后需要配置mqtt需要的文件,和nodered需要的文件夹。

1) 创建hass-all文件夹

cd
mkdir /home/openEuler/hass-all
sudo chmod -R 777 /home/openEuler/hass-all/    # 给文件夹权限

2)配置mosquitto.conf文件

cd /home/openEuler/hass-all/mosquitto/config
touch mosquitto.conf
chmod 777 mosquitto.conf
nano mosquitto.conf

3)配置nodered权限

cd /home/openEuler/hass-all
mkdir nodered
chmod 777 nodered/

 

 

4. 写入docker-compose.yml文件

在hass-all中写入docker-compose.yml,方便一键启动:

cd /home/openEuler/hass-all
nano docker-compose.yml

 

随后,写入如下内容,注意mariadb数据库中的password,可根据自己情况进行修改:

version: '3'
services:
  homeassistant:
    container_name: homeassistant
    image: homeassistant/home-assistant:stable
    volumes:
      - /home/openEuler/hass-all/homeassistant:/config
    environment:
      - TZ=Asia/Shanghai
    network_mode: host
    restart: unless-stopped
    depends_on:
      - mosquitto
      - mariadb

  mosquitto:
    container_name: mosquitto
    image: eclipse-mosquitto:latest
    volumes:
      - /home/openEuler/hass-all/mosquitto/config:/mosquitto/config
      - /home/openEuler/hass-all/mosquitto/data:/mosquitto/data
      - /home/openEuler/hass-all/mosquitto/log:/mosquitto/log
    restart: always
    ports:
      - "1883:1883"
      - "9001:9001"

  mariadb:
    container_name: mariadb
    image: mariadb:latest
    volumes:
      - /home/openEuler/hass-all/mariadb:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_DATABASE=homeassistant
      - MYSQL_USER=root
      - MYSQL_PASSWORD=password
    restart: always
    ports:
      - "3306:3306"

  nodered:
    container_name: nodered
    image: nodered/node-red:latest
    volumes:
      - /home/openEuler/hass-all/nodered:/data
    user: "node-red"
    restart: always
    ports:
      - "1880:1880"

  esphome:
    container_name: esphome
    image: esphome/esphome
    volumes:
      - /home/openEuler/hass-all/esphome:/config
    network_mode: host
    restart: always
    ports:
      - "6052:6052"
      - "6123:6123"

 

 

5. 启动docker-compose

随后,通过指令启动上述docker:

docker-compose up       # 关闭窗口后就会停止上述docker,方便调试
# docker-compose up -d    # 后台运行

 

 

6. 进入homeassistant

通过ifconfig查看开发板IP地址,随后浏览器输入IP:8123端口,进行homeassistant配置,完成后如图,可以看到当地的天气啦!

 

7. 后续配置

1) 数据库配置:由于homeassistant的内置数据库效率低,在后面多设备情况下,可能会影响稳定性,因此使用mariadb作为其数据库。

修改/home/openEuler/hass-all/homeassistant/configuration.yaml文件,加入如下内容,其中的数据库password需要与上面对应:

recorder:
  db_url: mysql://root:password@127.0.0.1/homeassistant?charset=utf8

2) 界面添加:将上述的nodered和esphome添加到homeassistant界面中。

修改/home/openEuler/hass-all/homeassistant/configuration.yaml文件,加入如下内容,其中的IP需要对应修改为自己的:

panel_iframe:
 nodered:
   title: 'Node-Red'
   icon: 'mdi:shuffle-variant'
   #填写node-red的地址
   url: 'http://192.168.10.181:1880/'

 esphome:
   title: 'ESPHome'
   icon: 'mdi:car-esp'
   #填写node-red的地址
   url: 'http://192.168.10.181:6052/'

完成上述后,重启docker-compose,可以看到如下内容。

 

3)安装HACS

HACS可以帮助homeassistant扩展更多的界面和应用,如小米、天气卡片等。

参考官方:https://hacs.xyz/docs/setup/download/,使用container的教程

打开HA的bash,输入如下指令即可

wget -O - https://get.hacs.xyz | bash -

随后打开homeassistant中的高级模式。最后添加集成HACS,并进行相应配置,即可显示HACS内容。

4) 配置Node-Red

在Node-Red中添加节点node-red-contrib-home-assistant-websocket,并安装。

5)配置MQTT

在homeassistant中添加集成MQTT,并配置如下

 

五、使用SPI小屏幕

开发板和小电脑最大的区别是,开发板上有引出多功能引脚,可以方便连接外部设备和传感器。在这里,测试使用该开发板驱动SPI小屏幕。

1. 查看手册

  SPI小屏幕包括引脚:GND VCC SCL SDA RES DC CS BLK引脚,1.14寸st7789 TFT屏幕,定义如下。

  开发板的引脚如下,需要使用到SPI引脚和几个通用引脚。

 

2. 引脚控制测试

根据手册,进行引脚控制。

0)报错解决:测试过程中,发现gpio_operate -h报错,究其原因是sudo yum update导致库更新不兼容,解决办法是将对应库降级,即可解决。

# 降级
sudo yum downgrade glibc glibc-common
# 重新加载驱动
lsmod | grep gpio
dmesg | grep gpio
# 重启
sudo reboot

 

1)基础引脚测试

使用引脚2-20,读取其方向为输入,随后读取其value,发现为1。通过杜邦线连接2-20和GND,在此读取value,发现变成了1。

gpio_operate -h                    # 帮助help
gpio_operate get_direction 2 20    # 查看引脚方向,0表示输入,1表示输出
gpio_operate get_value 2 20        # 获取引脚值

2) SPI回环测试

回环测试是指将SPI的SDI和SDO连接,发出去的数据被自己接收,查看收发数据是否一致判断SPI工作是否正常。(回环测试也可用于UART中)

ls /dev/spidev0.0            # 查看SPI设备
# 控制SPI进行测试,依次测试不连接和连接O/I的情况
sudo spidev_test -v -D /dev/spidev0.0

3) python控制引脚

写一个read_gpio.py程序,将上述bash指令由python调用,读取2-20引脚并显示其引脚状态。

import subprocess
import time

def get_gpio_value(gpio_group, gpio_pin):
    result = subprocess.run(f'gpio_operate get_value {gpio_group} {gpio_pin}', shell=True, capture_output=True, text=True)
    return result.stdout.strip()

def main():
    gpio_group = 2
    gpio_pin = 20

    while True:
        value = get_gpio_value(gpio_group, gpio_pin)
        print(f'GPIO {gpio_group}-{gpio_pin} Value: {value}')
        time.sleep(1)  # 每隔一秒读取一次

if __name__ == '__main__':
    main()

4) cpp控制引脚

上述都是在gpio_operate基础上进行引脚操作,不是特别方便。利用最底层,通过直接操作 sys/class/gpio来控制GPIO。以下是一个示例,周期读取GPIO2_20引脚状态并显示。

注意:GPIO2_20 是指第 2 组的第 20 个引脚,编号规则通常是 (组号 * 32) + 引脚号,例如 2 * 32 + 20 = 84。因此这里的引脚设置为84。

创建名为 gpio_read.cpp 的文件:

#include <iostream>
#include <fstream>
#include <string>
#include <unistd.h>

using namespace std;

class GPIO {
public:
    GPIO(int pin) : pinNumber(pin) {
        exportGPIO();
        setDirection("in");
    }

    ~GPIO() {
        unexportGPIO();
    }

    int getValue() {
        ifstream gpioValueFile("/sys/class/gpio/gpio" + to_string(pinNumber) + "/value");
        int value = -1;
        if (gpioValueFile.is_open()) {
            gpioValueFile >> value;
            gpioValueFile.close();
        } else {
            cerr << "Unable to get value for GPIO" << endl;
        }
        return value;
    }

private:
    int pinNumber;

    void exportGPIO() {
        ofstream gpioExportFile("/sys/class/gpio/export");
        if (gpioExportFile.is_open()) {
            gpioExportFile << pinNumber;
            gpioExportFile.close();
        } else {
            cerr << "Unable to export GPIO" << endl;
        }
        usleep(100000); // 等待 GPIO 文件系统创建
    }

    void unexportGPIO() {
        ofstream gpioUnexportFile("/sys/class/gpio/unexport");
        if (gpioUnexportFile.is_open()) {
            gpioUnexportFile << pinNumber;
            gpioUnexportFile.close();
        } else {
            cerr << "Unable to unexport GPIO" << endl;
        }
    }

    void setDirection(const string& direction) {
        ofstream gpioDirectionFile("/sys/class/gpio/gpio" + to_string(pinNumber) + "/direction");
        if (gpioDirectionFile.is_open()) {
            gpioDirectionFile << direction;
            gpioDirectionFile.close();
        } else {
            cerr << "Unable to set direction for GPIO" << endl;
        }
    }
};

int main() {
    int gpioPin = 84; // GPIO2_20 的编号

    GPIO gpio(gpioPin);

    while (true) {
        int value = gpio.getValue();
        cout << "GPIO " << gpioPin << " Value: " << value << endl;
        sleep(1); // 每秒读取一次
    }

    return 0;
}

编译和运行程序

g++ -o gpio_read gpio_read.cpp
sudo ./gpio_read

5) cpp控制SPI

使用 /dev/spidevX.Y 进行 SPI 通信,开发板为0.0的SPI,使用标准的 C++ 库和 ioctl 系统调用来控制 SPI 设备。

创建名为 spi_control.cpp 的文件:

#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
#include <cstring>

using namespace std;

class SPI {
public:
    SPI(const string& device, uint8_t mode, uint32_t speed) {
        fd = open(device.c_str(), O_RDWR);
        if (fd < 0) {
            perror("Failed to open SPI device");
            exit(1);
        }

        // 设置 SPI 模式
        if (ioctl(fd, SPI_IOC_WR_MODE, &mode) == -1) {
            perror("Failed to set SPI mode");
            exit(1);
        }

        // 设置 SPI 速度
        if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) == -1) {
            perror("Failed to set SPI speed");
            exit(1);
        }

        this->speed = speed;
        this->mode = mode;
    }

    ~SPI() {
        close(fd);
    }

    void transfer(uint8_t* tx_buffer, uint8_t* rx_buffer, size_t length) {
        struct spi_ioc_transfer spi;
        memset(&spi, 0, sizeof(spi));
        spi.tx_buf = reinterpret_cast<unsigned long>(tx_buffer);
        spi.rx_buf = reinterpret_cast<unsigned long>(rx_buffer);
        spi.len = length;
        spi.speed_hz = speed;
        spi.bits_per_word = bits_per_word;

        if (ioctl(fd, SPI_IOC_MESSAGE(1), &spi) == -1) {
            perror("Failed to transfer SPI message");
            exit(1);
        }
    }

private:
    int fd;
    uint32_t speed;
    uint8_t mode;
    uint8_t bits_per_word = 8;
};

int main() {
    string device = "/dev/spidev0.0"; // 根据需要修改设备路径
    uint8_t mode = SPI_MODE_0;
    uint32_t speed = 500000; // 500kHz

    SPI spi(device, mode, speed);

    uint8_t tx_buffer[] = {0x01, 0x02, 0x03, 0x04, 0x05};
    uint8_t rx_buffer[sizeof(tx_buffer)];

    while (true) {
        spi.transfer(tx_buffer, rx_buffer, sizeof(tx_buffer));
        cout << "Sent data: ";
        for (size_t i = 0; i < sizeof(tx_buffer); ++i) {
            cout << "0x" << hex << static_cast<int>(tx_buffer[i]) << " ";
        }
        cout << endl;

        cout << "Received data: ";
        for (size_t i = 0; i < sizeof(rx_buffer); ++i) {
            cout << "0x" << hex << static_cast<int>(rx_buffer[i]) << " ";
        }
        cout << endl;

        sleep(1); // 每秒进行一次通信
    }

    return 0;
}

编译和运行:

g++ -o spi_control spi_control.cpp
sudo ./spi_control

 

3. 程序封装

将上述的GPIO和SPI程序优化,并放置在工程文件夹下,方便后续调用。

1) 新建一个工程文件夹,名为TFT_SHOW,并包括文件夹include和src,后面创建的文件夹结构如下。

project_root/
├── include/
│   ├── GPIO.h
│   └── SPI.h
│   └── TFT.h
├── src/
│   ├── GPIO.cpp
│   ├── SPI.cpp
│   ├── TFT.cpp
│   └── main.cpp
├── CMakeLists.txt

2) 将上述GPIO优化,包括GPIO.h放在include中,GPIO.cpp放在src中。

#ifndef GPIO_H
#define GPIO_H

#include <string>

class GPIO {
public:
    enum Direction {
        IN,
        OUT
    };

    GPIO(int pin, Direction direction);
    ~GPIO();
    void setDirection(Direction direction);
    void setValue(int value);
    int getValue();

private:
    int pinNumber;
    void exportGPIO();
    void unexportGPIO();
    std::string directionToString(Direction direction);
};

#endif // GPIO_H
#include "GPIO.h"
#include <fstream>
#include <iostream>
#include <unistd.h>

using namespace std;

GPIO::GPIO(int pin, Direction direction) : pinNumber(pin) {
    exportGPIO();
    setDirection(direction);
}

GPIO::~GPIO() {
    unexportGPIO();
}

void GPIO::setDirection(Direction direction) {
    ofstream gpioDirectionFile("/sys/class/gpio/gpio" + to_string(pinNumber) + "/direction");
    if (gpioDirectionFile.is_open()) {
        gpioDirectionFile << directionToString(direction);
        gpioDirectionFile.close();
    } else {
        cerr << "Unable to set direction for GPIO" << endl;
    }
}

void GPIO::setValue(int value) {
    ofstream gpioValueFile("/sys/class/gpio/gpio" + to_string(pinNumber) + "/value");
    if (gpioValueFile.is_open()) {
        gpioValueFile << value;
        gpioValueFile.close();
    } else {
        cerr << "Unable to set value for GPIO" << endl;
    }
}

int GPIO::getValue() {
    ifstream gpioValueFile("/sys/class/gpio/gpio" + to_string(pinNumber) + "/value");
    int value = -1;
    if (gpioValueFile.is_open()) {
        gpioValueFile >> value;
        gpioValueFile.close();
    } else {
        cerr << "Unable to get value for GPIO" << endl;
    }
    return value;
}

void GPIO::exportGPIO() {
    ofstream gpioExportFile("/sys/class/gpio/export");
    if (gpioExportFile.is_open()) {
        gpioExportFile << pinNumber;
        gpioExportFile.close();
    } else {
        cerr << "Unable to export GPIO" << endl;
    }
    usleep(100000); // 等待 GPIO 文件系统创建
}

void GPIO::unexportGPIO() {
    ofstream gpioUnexportFile("/sys/class/gpio/unexport");
    if (gpioUnexportFile.is_open()) {
        gpioUnexportFile << pinNumber;
        gpioUnexportFile.close();
    } else {
        cerr << "Unable to unexport GPIO" << endl;
    }
}

string GPIO::directionToString(Direction direction) {
    return (direction == IN) ? "in" : "out";
}

3) 将上述SPI优化,包括SPI.h放在include中, SPI.cpp放在scr中。

#ifndef SPI_H
#define SPI_H

#include <string>
#include <cstdint>
#include <cstddef>

class SPI {
public:
    SPI(const std::string& device, uint8_t mode, uint32_t speed);
    ~SPI();
    void transfer(uint8_t* tx_buffer, uint8_t* rx_buffer, size_t length);

private:
    int fd;
    uint32_t speed;
    uint8_t mode;
    uint8_t bits_per_word = 8;
};

#endif // SPI_H
#include "SPI.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
#include <cstring>
#include <iostream>

using namespace std;

SPI::SPI(const std::string& device, uint8_t mode, uint32_t speed) {
    fd = open(device.c_str(), O_RDWR);
    if (fd < 0) {
        perror("Failed to open SPI device");
        exit(1);
    }

    // 设置 SPI 模式
    if (ioctl(fd, SPI_IOC_WR_MODE, &mode) == -1) {
        perror("Failed to set SPI mode");
        exit(1);
    }

    // 设置 SPI 速度
    if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) == -1) {
        perror("Failed to set SPI speed");
        exit(1);
    }

    this->speed = speed;
    this->mode = mode;
}

SPI::~SPI() {
    close(fd);
}

void SPI::transfer(uint8_t* tx_buffer, uint8_t* rx_buffer, size_t length) {
    struct spi_ioc_transfer spi;
    memset(&spi, 0, sizeof(spi));
    spi.tx_buf = reinterpret_cast<unsigned long>(tx_buffer);
    spi.rx_buf = reinterpret_cast<unsigned long>(rx_buffer);
    spi.len = length;
    spi.speed_hz = speed;
    spi.bits_per_word = bits_per_word;

    if (ioctl(fd, SPI_IOC_MESSAGE(1), &spi) == -1) {
        perror("Failed to transfer SPI message");
        exit(1);
    }
}

4)CMakeLists.txt

cmake_minimum_required(VERSION 3.10)

# 设置项目名称和版本
project(GPIOSPIControl VERSION 1.0)

# 指定 C++ 标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# 包含头文件目录
include_directories(${PROJECT_SOURCE_DIR}/include)

# 查找所有源文件
file(GLOB SOURCES ${PROJECT_SOURCE_DIR}/src/*.cpp)

# 添加可执行文件
add_executable(main ${SOURCES})

# 链接必要的库
target_link_libraries(main pthread)

5)简单测试

在src中写一个main.cpp,内容如下,进行测试:

#include "GPIO.h"
#include "SPI.h"
#include <iostream>
#include <unistd.h>

using namespace std;
#define SPI_MODE_0 0

int main() {
    // GPIO 示例
    GPIO gpio(84, GPIO::IN); // 例如 GPIO2_20 对应的编号为 84

    while (true) {
        int value = gpio.getValue();
        cout << "GPIO Value: " << value << endl;
        sleep(1); // 每秒读取一次
    }

    // SPI 示例
    /*
    string device = "/dev/spidev0.0"; // 根据需要修改设备路径
    uint8_t mode = SPI_MODE_0;
    uint32_t speed = 500000; // 500kHz

    SPI spi(device, mode, speed);

    uint8_t tx_buffer[] = {0x01, 0x02, 0x03, 0x04, 0x05};
    uint8_t rx_buffer[sizeof(tx_buffer)];

    while (true) {
        spi.transfer(tx_buffer, rx_buffer, sizeof(tx_buffer));
        cout << "Sent data: ";
        for (size_t i = 0; i < sizeof(tx_buffer); ++i) {
            cout << "0x" << hex << static_cast<int>(tx_buffer[i]) << " ";
        }
        cout << endl;

        cout << "Received data: ";
        for (size_t i = 0; i < sizeof(rx_buffer); ++i) {
            cout << "0x" << hex << static_cast<int>(rx_buffer[i]) << " ";
        }
        cout << endl;

        sleep(1); // 每秒进行一次通信
    }
    */

    return 0;
}

编译和运行:

# 进入文件夹
cd /home/openEuler/TFT_SHOW/
# 创建build文件并编译
mkdir build
cd build
cmake ..
# 编译
make
# 运行
sudo ./main

 

4. 引脚连接

TFT显示屏引脚 <----------> OrangePi Kunpeng Pro引脚

GND <----------> GND

VCC <----------> 3.3V

SCL <----------> GPIO2_25(SPI0_SDO)

SDA <----------> GPIO2_27(SPI0_SCLK)

RES <----------> GPIO2_20(84)

DC <----------> GPIO4_00(128)

CS <----------> GPIO2_26(SPI0_CS)

BLK <----------> GPIO0_03(3)

 

5. TFT驱动程序撰写

include中添加一个tft.h,包括如下内容,根据开源的eSPI_TFT进行修改适配

#ifndef TFT_H
#define TFT_H

#include <iostream>
#include <string>
#include "GPIO.h"
#include "SPI.h"

using namespace std;

// 定义相关宏和命令
#define TFT_INIT_DELAY 0x80
#define TFT_NOP 0x00
#define TFT_SWRST 0x01
#define TFT_SLPIN 0x10
#define TFT_SLPOUT 0x11
#define TFT_NORON 0x13
#define TFT_INVOFF 0x20
#define TFT_INVON 0x21
#define TFT_DISPOFF 0x28
#define TFT_DISPON 0x29
#define TFT_CASET 0x2A
#define TFT_PASET 0x2B
#define TFT_RAMWR 0x2C
#define TFT_RAMRD 0x2E
#define TFT_MADCTL 0x36
#define TFT_COLMOD 0x3A

// 其他宏定义
#define TFT_MAD_MY 0x80
#define TFT_MAD_MX 0x40
#define TFT_MAD_MV 0x20
#define TFT_MAD_ML 0x10
#define TFT_MAD_RGB 0x00
#define TFT_MAD_BGR 0x08
#define TFT_MAD_MH 0x04
#define TFT_MAD_SS 0x02
#define TFT_MAD_GS 0x01

#ifdef TFT_RGB_ORDER
#if (TFT_RGB_ORDER == 1)
#define TFT_MAD_COLOR_ORDER TFT_MAD_RGB
#else
#define TFT_MAD_COLOR_ORDER TFT_MAD_BGR
#endif
#else
#ifdef CGRAM_OFFSET
#define TFT_MAD_COLOR_ORDER TFT_MAD_BGR
#else
#define TFT_MAD_COLOR_ORDER TFT_MAD_RGB
#endif
#endif

#define TFT_IDXRD 0x00
#define ST_CMD_DELAY 0x80
#define ST7789_240x240_XSTART 0
#define ST7789_240x240_YSTART 0

// ST7789 特定命令
#define ST7789_NOP 0x00
#define ST7789_SWRESET 0x01
#define ST7789_RDDID 0x04
#define ST7789_RDDST 0x09
#define ST7789_RDDPM 0x0A
#define ST7789_RDD_MADCTL 0x0B
#define ST7789_RDD_COLMOD 0x0C
#define ST7789_RDDIM 0x0D
#define ST7789_RDDSM 0x0E
#define ST7789_RDDSR 0x0F
#define ST7789_SLPIN 0x10
#define ST7789_SLPOUT 0x11
#define ST7789_PTLON 0x12
#define ST7789_NORON 0x13
#define ST7789_INVOFF 0x20
#define ST7789_INVON 0x21
#define ST7789_GAMSET 0x26
#define ST7789_DISPOFF 0x28
#define ST7789_DISPON 0x29
#define ST7789_CASET 0x2A
#define ST7789_RASET 0x2B
#define ST7789_RAMWR 0x2C
#define ST7789_RGBSET 0x2D
#define ST7789_RAMRD 0x2E
#define ST7789_PTLAR 0x30
#define ST7789_VSCRDEF 0x33
#define ST7789_TEOFF 0x34
#define ST7789_TEON 0x35
#define ST7789_MADCTL 0x36
#define ST7789_IDMOFF 0x38
#define ST7789_IDMON 0x39
#define ST7789_RAMWRC 0x3C
#define ST7789_RAMRDC 0x3E
#define ST7789_COLMOD 0x3A

// 其他定义
#define TFT_BGR 0
#define TFT_RGB 1
#define ST7789_2_DRIVER
#define TFT_RGB_ORDER TFT_RGB
#define TFT_WIDTH 240
#define TFT_HEIGHT 135

// 定义引脚
#define TFT_DC 128
#define TFT_RST 84
#define TFT_BL 3

class TFT_ST7789
{
public:
    uint colstart = 40;
    uint rowstart = 53;

    TFT_ST7789(SPI &spi, GPIO &dc, GPIO &rst, GPIO &bl);
    int tft_init(uint8_t r = 0, uint8_t g = 0, uint8_t b = 0);
    void tft_deinit();
    void tft_transRotation();
    void tft_invertDisplay(bool i);
    uint16_t color565(uint8_t r, uint8_t g, uint8_t b);
    void fillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color);
    void fillScreen(uint32_t color);
    void pushImage(int32_t x, int32_t y, int32_t w, int32_t h, uint16_t *data);
    void tft_drawrgb(uint8_t *rgb, uint32_t len);
    void tft_drawrgb(uint8_t *r, uint8_t *g, uint8_t *b, uint32_t len);
#ifdef TFT_OPENCV
    void tft_drawrgb(Vec3b *rgb, uint32_t len);
    void tft_drawjpg(string path, uint16_t *dat);
    void tft_drawjpg(Mat &img, uint16_t *dat);
    void tft_drawjpg(string path);
    void tft_drawjpg(Mat &img);
#endif
    void tft_drawbgr(uint8_t *bgr, uint32_t len);
    void tft_drawbgr(uint8_t *r, uint8_t *g, uint8_t *b, uint32_t len);
#ifdef TFT_OPENCV
    void tft_drawbgr(Vec3b *bgr, uint32_t len);
#endif

private:
    void pin_init();
    void st7789_init();
    void tft_commandList(const uint8_t *addr);
    uint8_t spi_read_write(uint8_t send_data);
    void spi_writenb(const char *tbuf, uint32_t len);
    void tft_Write_8(uint8_t dat);
    void tft_Write_16(uint16_t C);
    void tft_Write_16(const uint16_t *C, uint32_t len);
    void tft_Write_16(uint16_t C, uint32_t len);
    void tft_Write_32(uint32_t C);
    void tft_Write_32C(uint16_t C, uint16_t D);
    void tft_Write_32D(uint32_t C);
    void tft_writecmd(uint8_t c);
    void tft_writedat(uint8_t d);
    void pushPixels(const void *data_in, uint32_t len);
    void setWindow(int32_t x0, int32_t y0, int32_t x1, int32_t y1);
    void pushBlock(uint16_t color, uint32_t len);

    SPI &spi;
    GPIO &dc;
    GPIO &rst;
    GPIO &bl;
};

#endif // TFT_H
在src中添加一个tft.cpp文件
#include "TFT.h"
#include <unistd.h>
#include <iostream>

using namespace std;

TFT_ST7789::TFT_ST7789(SPI &spi, GPIO &dc, GPIO &rst, GPIO &bl) : spi(spi), dc(dc), rst(rst), bl(bl)
{
    pin_init();
}

void TFT_ST7789::pin_init()
{
    rst.setValue(1);
    dc.setValue(1);
    bl.setValue(1);
}

int TFT_ST7789::tft_init(uint8_t r, uint8_t g, uint8_t b)
{
    pin_init();
    rst.setValue(1);
    usleep(5000);
    rst.setValue(0);
    usleep(20000);
    rst.setValue(1);
    usleep(150000);
    st7789_init();

    tft_transRotation();
    fillScreen(color565(r, g, b));

    return 0;
}

void TFT_ST7789::st7789_init()
{
    static const uint8_t st7789[] = {
        8,
        TFT_SLPOUT, TFT_INIT_DELAY, 255,
        TFT_COLMOD, 1 + TFT_INIT_DELAY, 0x55, 10,
        TFT_MADCTL, 1, 0x00,
        TFT_CASET, 4, 0x00, 0x00, 0x00, 0xF0,
        TFT_PASET, 4, 0x00, 0x00, 0x00, 0xF0,
        TFT_INVON, TFT_INIT_DELAY, 10,
        TFT_NORON, TFT_INIT_DELAY, 10,
        TFT_DISPON, TFT_INIT_DELAY, 255};
    tft_commandList(st7789);
}

void TFT_ST7789::tft_commandList(const uint8_t *addr)
{
    uint8_t numCommands = *(addr++);
    uint8_t numArgs;
    uint8_t ms;

    while (numCommands--)
    {
        tft_writecmd(*(addr++));
        numArgs = *(addr++);
        ms = numArgs & TFT_INIT_DELAY;
        numArgs &= ~TFT_INIT_DELAY;

        while (numArgs--)
        {
            tft_writedat(*(addr++));
        }

        if (ms)
        {
            ms = *(addr++);
            usleep((ms == 255 ? 500 : ms) * 1000);
        }
    }
}

void TFT_ST7789::tft_transRotation()
{
    tft_writecmd(TFT_MADCTL);
    tft_writedat(TFT_MAD_MX | TFT_MAD_MV | TFT_MAD_COLOR_ORDER);
}

void TFT_ST7789::tft_invertDisplay(bool i)
{
    tft_writecmd(i ? TFT_INVON : TFT_INVOFF);
    tft_writecmd(i ? TFT_INVON : TFT_INVOFF);
}

void TFT_ST7789::tft_deinit()
{
    // 反初始化过程
}

uint16_t TFT_ST7789::color565(uint8_t r, uint8_t g, uint8_t b)
{
    return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}

void TFT_ST7789::fillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color)
{
    setWindow(x, y, x + w - 1, y + h - 1);
    pushBlock(color, w * h);
}

void TFT_ST7789::fillScreen(uint32_t color)
{
    fillRect(0, 0, TFT_WIDTH, TFT_HEIGHT, color);
}

void TFT_ST7789::pushImage(int32_t x, int32_t y, int32_t w, int32_t h, uint16_t *data)
{
    setWindow(x, y, x + w - 1, y + h - 1);
    pushPixels(data, w * h);
}

void TFT_ST7789::pushPixels(const void *data_in, uint32_t len)
{
    uint16_t *data = (uint16_t *)data_in;
    tft_writecmd(TFT_RAMWR);
    for (uint32_t i = 0; i < len; i++)
    {
        tft_Write_16(data[i]);
    }
}

void TFT_ST7789::pushBlock(uint16_t color, uint32_t len)
{
    tft_writecmd(TFT_RAMWR);
    for (uint32_t i = 0; i < len; i++)
    {
        tft_Write_16(color);
    }
}

void TFT_ST7789::tft_drawrgb(uint8_t *rgb, uint32_t len)
{
    if (TFT_WIDTH * TFT_HEIGHT != len)
        return;
    uint16_t dat[len];
    for (uint32_t i = 0; i < len; i++)
    {
        dat[i] = color565(rgb[i * 3 + 0], rgb[i * 3 + 1], rgb[i * 3 + 2]);
    }
    pushImage(0, 0, TFT_WIDTH, TFT_HEIGHT, dat);
}

void TFT_ST7789::tft_drawrgb(uint8_t *r, uint8_t *g, uint8_t *b, uint32_t len)
{
    if (TFT_WIDTH * TFT_HEIGHT != len)
        return;
    uint16_t dat[len];
    for (uint32_t i = 0; i < len; i++)
    {
        dat[i] = color565(r[i], g[i], b[i]);
    }
    pushImage(0, 0, TFT_WIDTH, TFT_HEIGHT, dat);
}

#ifdef TFT_OPENCV
void TFT_ST7789::tft_drawrgb(Vec3b *rgb, uint32_t len)
{
    if (TFT_WIDTH * TFT_HEIGHT != len)
        return;
    uint16_t dat[len];
    for (uint32_t i = 0; i < len; i++)
    {
        dat[i] = color565(rgb[i][0], rgb[i][1], rgb[i][2]);
    }
    pushImage(0, 0, TFT_WIDTH, TFT_HEIGHT, dat);
}

void TFT_ST7789::tft_drawjpg(string path, uint16_t *dat)
{
    Mat img = imread(path);
    Mat imgResize;
    resize(img, imgResize, Size(TFT_WIDTH, TFT_HEIGHT));
    Scalar color;
    for (int i = 0; i < TFT_HEIGHT; i++)
    {
        for (int j = 0; j < TFT_WIDTH; j++)
        {
            color = imgResize.at<Vec3b>(i, j);
            dat[i * TFT_WIDTH + j] = color565(color[2], color[1], color[0]);
        }
    }
    pushImage(0, 0, TFT_WIDTH, TFT_HEIGHT, dat);
}

void TFT_ST7789::tft_drawjpg(Mat &img, uint16_t *dat)
{
    Mat imgResize;
    resize(img, imgResize, Size(TFT_WIDTH, TFT_HEIGHT));
    Scalar color;
    for (int i = 0; i < TFT_HEIGHT; i++)
    {
        for (int j = 0; j < TFT_WIDTH; j++)
        {
            color = imgResize.at<Vec3b>(i, j);
            dat[i * TFT_WIDTH + j] = color565(color[2], color[1], color[0]);
        }
    }
    pushImage(0, 0, TFT_WIDTH, TFT_HEIGHT, dat);
}

void TFT_ST7789::tft_drawjpg(string path)
{
    uint16_t dat[TFT_WIDTH * TFT_HEIGHT];
    tft_drawjpg(path, dat);
    pushImage(0, 0, TFT_WIDTH, TFT_HEIGHT, dat);
}

void TFT_ST7789::tft_drawjpg(Mat &img)
{
    uint16_t dat[TFT_WIDTH * TFT_HEIGHT];
    tft_drawjpg(img, dat);
    pushImage(0, 0, TFT_WIDTH, TFT_HEIGHT, dat);
}
#endif

void TFT_ST7789::tft_drawbgr(uint8_t *bgr, uint32_t len)
{
    if (TFT_WIDTH * TFT_HEIGHT != len)
        return;
    uint16_t dat[len];
    for (uint32_t i = 0; i < len; i++)
    {
        dat[i] = color565(bgr[i * 3 + 2], bgr[i * 3 + 1], bgr[i * 3 + 0]);
    }
    pushImage(0, 0, TFT_WIDTH, TFT_HEIGHT, dat);
}

void TFT_ST7789::tft_drawbgr(uint8_t *b, uint8_t *g, uint8_t *r, uint32_t len)
{
    if (TFT_WIDTH * TFT_HEIGHT != len)
        return;
    uint16_t dat[len];
    for (uint32_t i = 0; i < len; i++)
    {
        dat[i] = color565(r[i], g[i], b[i]);
    }
    pushImage(0, 0, TFT_WIDTH, TFT_HEIGHT, dat);
}

#ifdef TFT_OPENCV
void TFT_ST7789::tft_drawbgr(Vec3b *bgr, uint32_t len)
{
    if (TFT_WIDTH * TFT_HEIGHT != len)
        return;
    uint16_t dat[len];
    for (uint32_t i = 0; i < len; i++)
    {
        dat[i] = color565(bgr[i][2], bgr[i][1], bgr[i][0]);
    }
    pushImage(0, 0, TFT_WIDTH, TFT_HEIGHT, dat);
}
#endif

void TFT_ST7789::tft_writecmd(uint8_t c)
{
    dc.setValue(0); // Command mode
    spi.transfer(&c, nullptr, 1);
    dc.setValue(1); // Data mode
}

void TFT_ST7789::tft_writedat(uint8_t d)
{
    dc.setValue(1); // Data mode
    spi.transfer(&d, nullptr, 1);
}

void TFT_ST7789::setWindow(int32_t x0, int32_t y0, int32_t x1, int32_t y1)
{
    int32_t addr_row = 0xFFFF;
    int32_t addr_col = 0xFFFF;

    x0 += colstart;
    x1 += colstart;
    y0 += rowstart;
    y1 += rowstart;

    tft_writecmd(TFT_CASET);
    tft_Write_32C(x0, x1);
    tft_writecmd(TFT_PASET);
    tft_Write_32C(y0, y1);
    tft_writecmd(TFT_RAMWR);
}

void TFT_ST7789::tft_Write_8(uint8_t dat)
{
    spi.transfer(&dat, nullptr, 1);
}

void TFT_ST7789::tft_Write_16(uint16_t C)
{
    uint8_t data[2] = {static_cast<uint8_t>(C >> 8), static_cast<uint8_t>(C & 0xFF)};
    spi.transfer(data, nullptr, 2);
}

void TFT_ST7789::tft_Write_16(const uint16_t *C, uint32_t len)
{
    for (uint32_t i = 0; i < len; i++)
    {
        tft_Write_16(C[i]);
    }
}

void TFT_ST7789::tft_Write_16(uint16_t C, uint32_t len)
{
    for (uint32_t i = 0; i < len; i++)
    {
        tft_Write_16(C);
    }
}

void TFT_ST7789::tft_Write_32(uint32_t C)
{
    uint8_t data[4] = {
        static_cast<uint8_t>(C >> 24),
        static_cast<uint8_t>(C >> 16),
        static_cast<uint8_t>(C >> 8),
        static_cast<uint8_t>(C & 0xFF)};
    spi.transfer(data, nullptr, 4);
}

void TFT_ST7789::tft_Write_32C(uint16_t C, uint16_t D)
{
    uint8_t data[4] = {
        static_cast<uint8_t>(C >> 8),
        static_cast<uint8_t>(C & 0xFF),
        static_cast<uint8_t>(D >> 8),
        static_cast<uint8_t>(D & 0xFF)};
    spi.transfer(data, nullptr, 4);
}

void TFT_ST7789::tft_Write_32D(uint32_t C)
{
    uint8_t data[4] = {
        static_cast<uint8_t>(C >> 24),
        static_cast<uint8_t>(C >> 16),
        static_cast<uint8_t>(C >> 8),
        static_cast<uint8_t>(C & 0xFF)};
    spi.transfer(data, nullptr, 4);
}

 

6. 测试程序

在src中添加一个main.cpp文件,添加如下代码:

#include "TFT.h"
#include "GPIO.h"
#include "SPI.h"
#include <iostream>
#include <unistd.h>

using namespace std;

#define SPI_MODE_0 0

int main() {
    // 初始化 GPIO 和 SPI
    GPIO dc(128, GPIO::OUT);
    GPIO rst(84, GPIO::OUT);
    GPIO bl(3, GPIO::OUT);

    SPI spi("/dev/spidev0.0", SPI_MODE_0, 25000000);    // 设置最高25M

    // 初始化 TFT 显示屏
    TFT_ST7789 tft(spi, dc, rst, bl);
    if (tft.tft_init() != 0) {
        cerr << "Failed to initialize TFT" << endl;
        return -1;
    }

    cout << "TFT initialized successfully" << endl;
    // 基本测试:填充屏幕颜色
    cout << "Filling screen with red color" << endl;
    tft.fillScreen(tft.color565(255, 0, 0)); // 红色
    sleep(2);
    cout << "Filling screen with green color" << endl;
    tft.fillScreen(tft.color565(0, 255, 0)); // 绿色
    sleep(2);
    cout << "Filling screen with blue color" << endl;
    tft.fillScreen(tft.color565(0, 0, 255)); // 蓝色
    sleep(2);

    // 刷新屏幕
    tft.tft_deinit();
    cout << "TFT test completed" << endl;
    
    return 0;
}

 

7. 运行效果

运行程序后,屏幕依次刷新为黑色、红色、绿色、蓝色,最后停在蓝色。测试过程中发现刷新的速度特别慢,即使将SPI的速率改为最高25M,也难以满足正常刷新要求。应该是SPI驱动库没有选对,后续有机会再进行优化。

 

六、评测观点

1. 可以夸夸的地方

性能强悍:相比于之前使用的树莓派4B和4B,桌面流畅度和网页浏览提升很多。

接口丰富:开发板没有为了卡片式而过分缩减接口,有两个标准的HDMI接口、背面完整长度的M2接口和EMMC接口、typeC形式的USB3接口。

主动散热:性能强悍带来的是功耗爆炸,主动可调散热让性能释放更加自由。

 

2. 值得改进的地方

系统资源目前官方安装的系统为openEuler,还未有更多专用适配的系统例如Ubuntu、安卓等。

官方案例:手册很完善,但是官方示例较少,用户难以快速体验到开发板的优势。

引脚驱动:手册中提供了gpio_operate方式进行gpio操作,但是针对python、C、C++等语言的支持库不完善。

 

3. 最后说说

非常荣幸能够获得测评OrangePi Kunpeng Pro的机会,在测试过程中也尽可能将以往的一些小代码应用在这块优秀的开发板中,最后也都成功实现了,实属不易。相信随着加入OrangePi Kunpeng Pro的开发者增多,官方支持的加大,OrangePi Kunpeng Pro将会越来越好,毕竟,性能高的底子还是有的。

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

wifi贴码推广哪家靠谱?

如今越来越多的人想轻资产创业&#xff0c;WIFI贴码是共享行业最无成本的创业项目了&#xff0c;而在选择厂商的时候&#xff0c;大家就想要知道哪家公司靠谱&#xff0c;更好、更便宜、可靠。那么wifi贴码推广哪家靠谱&#xff1f;别急&#xff0c;下面小编将带你一起了解。 目…

LLM - 模型下载与 git-lfs 安装

目录 一.引言 二.安装 git lfs 1.使用 apt-get 安装 2.使用 Brew 安装 3.LFS 验证 三.总结 一.引言 在 HuggingFace 上下载模型时提供一个 git clone 的指令&#xff0c;执行后可以下载对应模型的模型文件: 但是本机还没有 git lfs 命令: git: lfs is not a git comman…

CPU对代码执行效率的优化,CPU的缓存、指令重排序

目录 一、CPU对代码执行效率的优化 1. 指令流水线&#xff08;Instruction Pipelining&#xff09; 2. 超标量架构&#xff08;Superscalar Architecture&#xff09; 3. 动态指令重排序&#xff08;Dynamic Instruction Reordering&#xff09; 4. 分支预测&#xff08;…

文献解读-群体基因组第二期|《中国人群中PAX2新生突变的检测及表型分析:一项单中心研究》

关键词&#xff1a;应用遗传流行病学&#xff1b;群体测序&#xff1b;群体基因组&#xff1b;基因组变异检测&#xff1b; 文献简介 标题&#xff08;英文&#xff09;&#xff1a;Detection of De Novo PAX2 Variants and Phenotypes in Chinese Population: A Single-Cente…

下一代 CI/CD:利用 Tekton 和 ArgoCD 实现云原生自动化

一、回顾目标 背景&#xff1a; ​ 部门业务上云&#xff0c;之前服务采用传统的部署方式&#xff0c;这种方式简单&#xff0c;但是不能为应用程序定义资源使用边界&#xff0c;很难合理地分配计算资源&#xff0c;而且程序之间容易产生影响。随着互联网时代的到来&#xff…

【Chrono Engine学习总结】6-创建自定义场景-6.1-3D场景获取

由于Chrono的官方教程在一些细节方面解释的并不清楚&#xff0c;自己做了一些尝试&#xff0c;做学习总结。 Chrono可以导入自定义的三维模型&#xff0c;所以想自己搭建一个3D仿真环境。过程中遇到了一些问题&#xff0c;记录与整理。 1、3D环境的创建方法 Chrono的Irrlich…

2024年6月1日(星期六)骑行禹都甸

2024年6月1日 (星期六&#xff09;骑行禹都甸&#xff08;韭葱花&#xff09;&#xff0c;早8:30到9:00&#xff0c;昆明氧气厂门口集合&#xff0c;9:30准时出发【因迟到者&#xff0c;骑行速度快者&#xff0c;可自行追赶偶遇。】 偶遇地点:昆明氧气厂门口集合 &#xff0c;…

视频监控业务平台LntonCVS运用国标协议对接视频汇聚管理综合平台应用方案

为了实现“以信息化推动应急管理能力现代化”的目标&#xff0c;应急管理部提出了加速现代信息技术与应急管理业务深度融合的计划。这一计划是国家加强和改进应急管理工作的关键举措&#xff0c;也是满足日益严峻的应急管理形势和人民群众不断增长的公共安全需求的紧迫需求。 为…

12个好用的视频自动加字幕工具

多个提供视频字幕生成及翻译服务的工具和平台&#xff0c;涵盖了从自动识别到翻译、配音等多种功能&#xff0c;适合不同需求的用户选择使用。 网易见外: 支持视频智能字幕功能&#xff0c;转换正确率高&#xff0c;转换后的字幕可以直接导入Pr和会声会影等主流剪辑工具&#…

【Axure教程】拖动换位选择器

拖动换位选择器通常用于从一个列表中选择项目并将其移动到另一个列表中。用户可以通过拖动选项来实现选择和移动。这种交互方式在许多Web应用程序中很常见&#xff0c;特别是在需要对项目分组的情况下。 所以今天作者就教大家怎么在Axure用中继器制作一个拖动换位选择器的原型…

【Linux】初识Linux和Linux环境配置

1.什么是Linux操作系统 说到电脑系统 我想有大多数人会脱口而出&#xff1a;windows、mac 是的&#xff0c;这也是如今市场上主流的两种操作系统。 但是对于IT相关的人士来说&#xff0c;还有一种系统也是必须有姓名 那就是Linux Linux&#xff0c;Linux Is Not UniX 的…

Java_认识String类

在 C 语言中已经涉及到字符串了&#xff0c;但是在 C 语言中要表示字符串只能使用字符数组或者字符指针&#xff0c; 可以使用标准库提 供的字符串系列函数完成大部分操作&#xff0c;但是这种将数据和操作数据方法分离开 的方式不符合面相对象的思想&#xff0c;而字符串应用又…

VI 使用替换命令快速注释多行

使用替换命令快速注释多行&#xff1a; 按下 Esc 键确保你在普通模式下。输入 :起始行号,结束行号s/^/#/ 并按 Enter 键。 :起始行号 和 结束行号 分别是你要注释的起始行和结束行的行号。 关于正则 s/^/#/各个部分解释&#xff1a; s/: 这是vi编辑器中的替换命令的开头。s 表…

【Linux】centos7编写C语言程序,补充:使用yum安装软件包组

确保已安装gcc编译器 C语言程序&#xff0c;一般使用gcc进行编译&#xff0c;需确保已安装gcc。 若没有&#xff0c;可以使用yum安装gcc&#xff08;版本4.8.5&#xff09;&#xff0c;也可以使用SCL源安装gcc&#xff08;例如&#xff1a;版本9.3&#xff09;。 安装gcc&am…

htb_BoardLight

信息收集 nmap -sSVC 10.10.11.11开放80端口&#xff0c;将boardlight.htb写入/etc/hosts 同步进行子域名和目录扫描 子域名扫不到 这个目录扫描很奇怪哈&#xff0c;明明访问80端口有页面&#xff0c;就是扫不出来 直接浏览器访问80端口&#xff0c;四处看看&#xff0c;发…

Blazor入门-连接MySQL的简单例子:列出数据+简单查询

参考&#xff1a; ASP.NET Core 6.0 Blazor Server APP并使用MySQL数据库_blazor mysql-CSDN博客 https://blog.csdn.net/mzl87/article/details/129199352 本地环境&#xff1a;win10, visual studio 2022 community, mysql 8.0.33 (MySQL Community Server), net core 6.0 目…

DiffIR论文阅读笔记

ICCV2023的一篇用diffusion模型做Image Restoration的论文&#xff0c;一作是清华的教授&#xff0c;还在NIPS2023上一作发表了Hierarchical Integration Diffusion Model for Realistic Image Deblurring&#xff0c;作者里甚至有Luc Van Gool大佬。模型分三个部分&#xff0c…

Linux静态库、共享动态库介绍、制作及使用

参考学习&#xff1a;Linux下的各种文件 、动态库基本原理和使用方法&#xff0c;-fPIC选项的来龙去脉 、Linux静态库和动态库分析 文章写作参考&#xff1a;Linux共享库、静态库、动态库详解 - sunsky303 - 博客园 (cnblogs.com) 一.Linux共享库、静态库、动态库详解 使用G…

三十五岁零基础转行成为AI大模型开发者怎么样呢?

以下从3个方面帮大家分析&#xff1a; 35岁转行会不会太晚&#xff1f;零基础学习AI大模型开发能不能学会&#xff1f;AI大模型开发行业前景如何&#xff0c;学完后能不能找到好工作&#xff1f; 一、35岁转行会不会太晚&#xff1f; 35岁正处于人生的黄金时期&#xff0c;拥…

将四种算法的预测结果绘制在一张图中

​ 声明&#xff1a;文章是从本人公众号中复制而来&#xff0c;因此&#xff0c;想最新最快了解各类智能优化算法及其改进的朋友&#xff0c;可关注我的公众号&#xff1a;强盛机器学习&#xff0c;不定期会有很多免费代码分享~ 之前的一期推文中&#xff0c;我们推出了…