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/bashecho "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: homeassistantimage: homeassistant/home-assistant:stablevolumes:- /home/openEuler/hass-all/homeassistant:/configenvironment:- TZ=Asia/Shanghainetwork_mode: hostrestart: unless-stoppeddepends_on:- mosquitto- mariadbmosquitto:container_name: mosquittoimage: eclipse-mosquitto:latestvolumes:- /home/openEuler/hass-all/mosquitto/config:/mosquitto/config- /home/openEuler/hass-all/mosquitto/data:/mosquitto/data- /home/openEuler/hass-all/mosquitto/log:/mosquitto/logrestart: alwaysports:- "1883:1883"- "9001:9001"mariadb:container_name: mariadbimage: mariadb:latestvolumes:- /home/openEuler/hass-all/mariadb:/var/lib/mysqlenvironment:- MYSQL_ROOT_PASSWORD=password- MYSQL_DATABASE=homeassistant- MYSQL_USER=root- MYSQL_PASSWORD=passwordrestart: alwaysports:- "3306:3306"nodered:container_name: noderedimage: nodered/node-red:latestvolumes:- /home/openEuler/hass-all/nodered:/datauser: "node-red"restart: alwaysports:- "1880:1880"esphome:container_name: esphomeimage: esphome/esphomevolumes:- /home/openEuler/hass-all/esphome:/confignetwork_mode: hostrestart: alwaysports:- "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 timedef 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 = 2gpio_pin = 20while 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; // 500kHzSPI 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 0int main() {// GPIO 示例GPIO gpio(84, GPIO::IN); // 例如 GPIO2_20 對應的編號為 84while (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; // 500kHzSPI 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 3class 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_OPENCVvoid 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);
#endifvoid 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_OPENCVvoid tft_drawbgr(Vec3b *bgr, uint32_t len);
#endifprivate: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);
}
#endifvoid 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);
}
#endifvoid TFT_ST7789::tft_writecmd(uint8_t c)
{dc.setValue(0); // Command modespi.transfer(&c, nullptr, 1);dc.setValue(1); // Data mode
}void TFT_ST7789::tft_writedat(uint8_t d)
{dc.setValue(1); // Data modespi.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 0int main() {// 初始化 GPIO 和 SPIGPIO 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將會越來越好,畢竟,性能高的底子還是有的。
?