Als erfahrener Entwickler im Bereich eingebetteter Systeme möchte ich Ihnen einen detaillierten Leitfaden zum RK3588-Chipsatz von Rockchip vorstellen. Dieser leistungsstarke SoC (System-on-Chip) hat sich in verschiedenen Anwendungsbereichen wie Edge Computing, KI-Beschleunigung und High-End-Embedded-Systemen etabliert. Dieser Leitfaden folgt den EEAT-Prinzipien (Expertise, Erfahrung, Autorität und Vertrauenswürdigkeit) und bietet fundierte Informationen für Entwickler aller Erfahrungsstufen.
Inhaltsverzeichnis
- Technische Spezifikationen des RK3588
- Entwicklungsumgebung einrichten
- Bootloader und Firmware
- Betriebssystem-Konfiguration
- Hardwareschnittstellen programmieren
- GPU- und NPU-Programmierung
- Multimedia-Verarbeitung
- Energiemanagement
- Debugging und Leistungsoptimierung
- Anwendungsfälle und Projekte
1. Technische Spezifikationen des RK3588
Der RK3588 ist ein hochintegrierter Octa-Core-Prozessor mit ARM-Architektur, entwickelt von Rockchip Electronics. Er kombiniert leistungsstarke CPU-Kerne mit spezialisierten Beschleunigern für KI, Grafik und Multimedia.
CPU-Architektur
- 4x ARM Cortex-A76 Kerne (bis zu 2,4 GHz)
- 4x ARM Cortex-A55 Kerne (bis zu 1,8 GHz)
- big.LITTLE-Architektur mit dynamischem Lastausgleich
GPU und NPU
- ARM Mali-G610 MP4 GPU (OpenGL ES 3.2, Vulkan 1.2)
- 6 TOPS NPU (Neural Processing Unit) für KI-Beschleunigung
- Unterstützung für TensorFlow, PyTorch, ONNX und andere KI-Frameworks
RAM und Speicher
- LPDDR4/LPDDR4X/LPDDR5-Unterstützung (bis zu 32 GB)
- eMMC 5.1, UFS 3.0, SD/MMC
- PCIe 3.0 für schnelle Speichererweiterungen
Multimedia-Fähigkeiten
- 8K-Videodekodierung (H.265/HEVC, H.264, VP9, AV1)
- 8K-Videoenkodierung (H.265/HEVC, H.264)
- HDR10, HDR10+, HLG-Unterstützung
- HDMI 2.1, DisplayPort 1.4, eDP, MIPI-DSI
Konnektivität
- 2,5-Gigabit-Ethernet
- USB 3.1, USB 2.0, USB-C
- MIPI-CSI für Kameraschnittstellen
- PCIe für Erweiterungskarten
2. Entwicklungsumgebung einrichten
Für die Entwicklung mit dem RK3588 benötigen Sie eine spezialisierte Umgebung. Hier führe ich Sie durch die notwendigen Schritte.
Benötigte Hardware
- RK3588-Entwicklungsboard (z.B. Rock 5 Model B, Radxa CM5, Orange Pi 5)
- USB-zu-TTL-Serielladapter für Konsolenzugriff
- Stromversorgung (12V/3A empfohlen)
- microSD-Karte (A2-Klasse, mindestens 32 GB)
- Netzwerkkabel für Ethernet-Verbindung
- HDMI-Kabel für Display-Ausgabe
Software-Voraussetzungen
- Linux-Entwicklungsumgebung (Ubuntu 20.04 LTS oder höher empfohlen)
- Cross-Compiler-Toolchain für ARM64
- Git für Quellcode-Management
- Docker (optional, für containerisierte Entwicklung)
Installation der Entwicklungswerkzeuge
# Grundlegende Pakete installieren
sudo apt update
sudo apt install -y git make gcc g++ build-essential libncurses-dev unzip python3 python3-pip
# ARM64-Toolchain installieren
sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
# Rockchip-spezifische Tools installieren
git clone https://github.com/rockchip-linux/rkbin.git
git clone https://github.com/rockchip-linux/rkdeveloptool.git
cd rkdeveloptool
autoreconf -i
./configure
make
sudo make install
Repository-Struktur für RK3588-Entwicklung
rk3588-dev/
├── kernel/ # Linux-Kernel-Quellcode
├── u-boot/ # U-Boot-Bootloader
├── buildroot/ # Rootfs-Generierung
├── rkbin/ # Rockchip-Binärdateien
├── device/ # Gerätespezifische Konfigurationen
├── toolchain/ # Cross-Compiler und Werkzeuge
└── output/ # Kompilierte Binärdateien
3. Bootloader und Firmware
Der Bootprozess des RK3588 umfasst mehrere Stufen, beginnend mit der ROM-Firmware bis hin zum Betriebssystem.
Bootsequenz
- BootROM (maskierter ROM im SoC)
- Miniloader/SPL (First-Stage-Bootloader)
- U-Boot (Second-Stage-Bootloader)
- Linux-Kernel
- Rootfs (Root-Dateisystem)
U-Boot für RK3588 kompilieren
# U-Boot-Repository klonen
git clone https://github.com/u-boot/u-boot.git
cd u-boot
# Auf einen stabilen Branch wechseln
git checkout v2023.01
# RK3588-spezifische Patches anwenden
wget https://gitlab.com/rockchip-linux/u-boot/-/archive/next-dev/u-boot-next-dev.tar.gz
tar xf u-boot-next-dev.tar.gz
cp -r u-boot-next-dev/arch/arm/mach-rockchip/rk3588 arch/arm/mach-rockchip/
# Konfiguration für Ihr spezifisches Board auswählen
make CROSS_COMPILE=aarch64-linux-gnu- rock-5b-rk3588_defconfig
# U-Boot kompilieren
make CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)
Tabelle: Rockchip-Bootmodi und Ihre Verwendung
Bootmodus | Beschreibung | Aktivierung | Anwendungsfall |
---|---|---|---|
Maskierter ROM | Integrierter ROM im SoC | Automatisch beim Einschalten | Initialer Bootprozess |
Recovery | Recovery-Modus | USB-OTG + Netzwerktaste | Firmware-Wiederherstellung |
Loader | Firmware-Update-Modus | USB-OTG im Maskierten ROM | Flashing neuer Firmware |
Normal | Standardmäßiger Boot | Ohne spezielle Tasten | Regulärer Systembetrieb |
Fastboot | Android-Fastboot | USB-OTG + Bootmodus-Taste | Android-System-Updates |
Firmware-Update mit rkdeveloptool
# Gerät in Maskierter-ROM-Modus versetzen
# (Drücken Sie die Recovery-Taste während des Einschaltens)
# Verbindung mit dem Gerät überprüfen
sudo rkdeveloptool ld
# Loader hochladen
sudo rkdeveloptool db rkbin/bin/rk35/rk3588_ddr_lp4_2112MHz_lp5_2400MHz_v1.08.bin
# Partition-Table hochladen
sudo rkdeveloptool gpt parameter.txt
# Bootloader-Image flashen
sudo rkdeveloptool wl 0x4000 u-boot/u-boot.img
# System-Image flashen (für schnelle Tests)
sudo rkdeveloptool wl 0x40000 system.img
# Gerät neu starten
sudo rkdeveloptool rd
4. Betriebssystem-Konfiguration
Der RK3588 unterstützt verschiedene Betriebssysteme, darunter Linux-Distributionen wie Debian, Ubuntu und spezialisierte Systeme wie Android.
Linux-Kernel für RK3588 kompilieren
# Kernel-Repository klonen
git clone https://github.com/torvalds/linux.git
cd linux
# Rockchip-Patches anwenden
git remote add rockchip https://github.com/rockchip-linux/kernel.git
git fetch rockchip
git checkout -b rk3588-dev rockchip/develop-5.10
# Kernel konfigurieren
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig
# Wichtige Konfigurationsoptionen:
# - CONFIG_ARCH_ROCKCHIP=y
# - CONFIG_CPU_RK3588=y
# - CONFIG_DRM_ROCKCHIP=y
# - CONFIG_VIDEO_ROCKCHIP_ISP=y
# - CONFIG_ROCKCHIP_MPP_SERVICE=y
# Kernel kompilieren
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) Image dtbs modules
# Module installieren
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- INSTALL_MOD_PATH=../rootfs modules_install
Root-Dateisystem erstellen
Sie können ein Root-Dateisystem mit Buildroot oder durch manuelle Anpassung einer bestehenden Distribution erstellen.
Mit Buildroot:
# Buildroot klonen
git clone https://git.buildroot.net/buildroot
cd buildroot
# Konfigurieren
make rockchip_rk3588_defconfig
make menuconfig
# Kompilieren
make -j$(nproc)
Mit Debian/Ubuntu:
# Erstellen eines leeren Image
dd if=/dev/zero of=rootfs.img bs=1M count=4096
# Partitionieren und Formatieren
sudo parted rootfs.img --script -- mklabel gpt
sudo parted rootfs.img --script -- mkpart primary ext4 1MiB 100%
sudo losetup -fP rootfs.img
LOOPDEV=$(sudo losetup -a | grep rootfs.img | awk -F: '{print $1}')
sudo mkfs.ext4 ${LOOPDEV}p1
# Root-Dateisystem mounten
mkdir -p rootfs
sudo mount ${LOOPDEV}p1 rootfs
# Debian-Basissystem installieren
sudo debootstrap --arch=arm64 bullseye rootfs http://deb.debian.org/debian
# Konfiguration anpassen
sudo chroot rootfs
echo "rk3588-dev" > /etc/hostname
cat > /etc/fstab << EOF
/dev/mmcblk0p1 / ext4 defaults 0 1
EOF
passwd root
exit
# Unmounten
sudo umount rootfs
sudo losetup -d $LOOPDEV
Tabelle: Unterstützte Betriebssysteme für RK3588
Betriebssystem | Unterstützungsgrad | Besonderheiten | Empfohlene Anwendungsfälle |
---|---|---|---|
Debian/Ubuntu | Vollständig | Breite Paketunterstützung, Desktop-Umgebungen | Allgemeine Entwicklung, Desktop-Anwendungen |
Android | Gut | Hardware-beschleunigte Multimedia | Touchscreen-Geräte, Media-Center |
Buildroot | Sehr gut | Minimales System, hochgradig anpassbar | Eingebettete Systeme, IoT-Geräte |
Yocto | Gut | Anpassbare Distribution-Builder | Industrielle Anwendungen |
ChromeOS | Eingeschränkt | Experimentelle Unterstützung | Web-zentrierte Anwendungen |
5. Hardwareschnittstellen programmieren
Der RK3588 bietet zahlreiche Hardwareschnittstellen, die für verschiedene Anwendungen genutzt werden können.
GPIO-Programmierung
Die allgemeinen Ein-/Ausgabe-Pins (GPIO) können über sysfs oder die neuere GPIO-Character-Device-API programmiert werden.
// GPIO über Character Device API steuern
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/gpio.h>
#define GPIO_CHIP_PATH "/dev/gpiochip0"
#define GPIO_PIN 17 // Beispiel-GPIO-Pin
int main() {
struct gpiohandle_request req;
struct gpiohandle_data data;
int fd, ret;
// GPIO-Chip öffnen
fd = open(GPIO_CHIP_PATH, O_RDONLY);
if (fd < 0) {
perror("GPIO-Chip öffnen fehlgeschlagen");
return EXIT_FAILURE;
}
// GPIO als Ausgang konfigurieren
memset(&req, 0, sizeof(req));
req.lineoffsets[0] = GPIO_PIN;
req.flags = GPIOHANDLE_REQUEST_OUTPUT;
strcpy(req.consumer_label, "gpio_test");
req.lines = 1;
ret = ioctl(fd, GPIO_GET_LINEHANDLE_IOCTL, &req);
if (ret < 0) {
perror("GPIO-Handle-Request fehlgeschlagen");
close(fd);
return EXIT_FAILURE;
}
// GPIO-Zustand umschalten (10 Mal)
for (int i = 0; i < 10; i++) {
data.values[0] = i % 2; // Wechsel zwischen 0 und 1
ret = ioctl(req.fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
if (ret < 0) {
perror("GPIO-Wert setzen fehlgeschlagen");
break;
}
usleep(500000); // 500ms warten
}
// Aufräumen
close(req.fd);
close(fd);
return EXIT_SUCCESS;
}
I2C-Kommunikation
I2C wird häufig für die Kommunikation mit Sensoren, Displays und anderen Peripheriegeräten verwendet.
// I2C-Kommunikation mit einem Sensor
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
#include <i2c/smbus.h>
#define I2C_BUS "/dev/i2c-3" // Abhängig vom Board-Layout
#define SENSOR_ADDR 0x68 // Beispieladresse eines Sensors
int main() {
int fd, ret;
unsigned char reg = 0x0F; // Beispiel-Register (ID-Register)
unsigned char value;
// I2C-Bus öffnen
fd = open(I2C_BUS, O_RDWR);
if (fd < 0) {
perror("I2C-Bus öffnen fehlgeschlagen");
return EXIT_FAILURE;
}
// I2C-Gerät auswählen
if (ioctl(fd, I2C_SLAVE, SENSOR_ADDR) < 0) {
perror("I2C-Gerät auswählen fehlgeschlagen");
close(fd);
return EXIT_FAILURE;
}
// Register lesen
ret = i2c_smbus_read_byte_data(fd, reg);
if (ret < 0) {
perror("I2C-Lesen fehlgeschlagen");
close(fd);
return EXIT_FAILURE;
}
value = (unsigned char)ret;
printf("Sensorwert: 0x%02X\n", value);
// Aufräumen
close(fd);
return EXIT_SUCCESS;
}
SPI-Kommunikation
Serial Peripheral Interface (SPI) wird für schnelle Kommunikation mit Displays, Flash-Speicher und anderen Geräten verwendet.
// SPI-Kommunikation mit einem Gerät
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
#define SPI_DEVICE "/dev/spidev0.0"
#define SPI_SPEED 1000000 // 1 MHz
int main() {
int fd, ret;
uint8_t tx[] = {0x01, 0x02, 0x03}; // Zu sendende Daten
uint8_t rx[sizeof(tx)] = {0}; // Empfangspuffer
struct spi_ioc_transfer tr = {
.tx_buf = (unsigned long)tx,
.rx_buf = (unsigned long)rx,
.len = sizeof(tx),
.speed_hz = SPI_SPEED,
.bits_per_word = 8,
};
// SPI-Gerät öffnen
fd = open(SPI_DEVICE, O_RDWR);
if (fd < 0) {
perror("SPI-Gerät öffnen fehlgeschlagen");
return EXIT_FAILURE;
}
// SPI-Modus einstellen (Mode 0)
uint8_t mode = SPI_MODE_0;
if (ioctl(fd, SPI_IOC_WR_MODE, &mode) < 0) {
perror("SPI-Modus einstellen fehlgeschlagen");
close(fd);
return EXIT_FAILURE;
}
// Bits pro Wort einstellen
uint8_t bits = 8;
if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0) {
perror("Bits pro Wort einstellen fehlgeschlagen");
close(fd);
return EXIT_FAILURE;
}
// Max. Geschwindigkeit einstellen
if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &SPI_SPEED) < 0) {
perror("SPI-Geschwindigkeit einstellen fehlgeschlagen");
close(fd);
return EXIT_FAILURE;
}
// Daten übertragen
ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
if (ret < 0) {
perror("SPI-Übertragung fehlgeschlagen");
close(fd);
return EXIT_FAILURE;
}
// Empfangene Daten anzeigen
printf("Empfangene Daten: ");
for (size_t i = 0; i < sizeof(rx); i++) {
printf("0x%02X ", rx[i]);
}
printf("\n");
// Aufräumen
close(fd);
return EXIT_SUCCESS;
}
6. GPU- und NPU-Programmierung
Der RK3588 verfügt über leistungsstarke Grafikprozessoren (GPU) und neuronale Verarbeitungseinheiten (NPU), die für rechenintensive Anwendungen genutzt werden können.
GPU-Programmierung mit OpenGL ES
Die Mali-G610 MP4 GPU unterstützt OpenGL ES 3.2 und Vulkan 1.2.
// Einfaches OpenGL ES 3.0 Beispiel
#include <GLES3/gl3.h>
#include <EGL/egl.h>
#include <stdio.h>
#include <stdlib.h>
// EGL-Variablen
EGLDisplay display;
EGLConfig config;
EGLContext context;
EGLSurface surface;
// OpenGL-Variablen
GLuint program;
GLuint vertexShader;
GLuint fragmentShader;
// Shader-Quellcode
const char *vertexShaderSource =
"#version 300 es\n"
"layout(location = 0) in vec3 position;\n"
"void main() {\n"
" gl_Position = vec4(position, 1.0);\n"
"}\n";
const char *fragmentShaderSource =
"#version 300 es\n"
"precision mediump float;\n"
"out vec4 fragColor;\n"
"void main() {\n"
" fragColor = vec4(1.0, 0.0, 0.0, 1.0);\n"
"}\n";
// EGL initialisieren
int initEGL() {
// Display öffnen
display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
printf("EGL-Display nicht gefunden\n");
return -1;
}
// EGL initialisieren
EGLint major, minor;
if (!eglInitialize(display, &major, &minor)) {
printf("EGL-Initialisierung fehlgeschlagen\n");
return -1;
}
printf("EGL-Version: %d.%d\n", major, minor);
// Konfiguration wählen
EGLint configAttribs[] = {
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_DEPTH_SIZE, 24,
EGL_NONE
};
EGLint numConfigs;
if (!eglChooseConfig(display, configAttribs, &config, 1, &numConfigs)) {
printf("EGL-Konfiguration wählen fehlgeschlagen\n");
return -1;
}
// Kontext erstellen
EGLint contextAttribs[] = {
EGL_CONTEXT_CLIENT_VERSION, 3,
EGL_NONE
};
context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
if (context == EGL_NO_CONTEXT) {
printf("EGL-Kontext erstellen fehlgeschlagen\n");
return -1;
}
// Pixel-Buffer-Surface erstellen
EGLint pbufferAttribs[] = {
EGL_WIDTH, 256,
EGL_HEIGHT, 256,
EGL_NONE
};
surface = eglCreatePbufferSurface(display, config, pbufferAttribs);
if (surface == EGL_NO_SURFACE) {
printf("EGL-Surface erstellen fehlgeschlagen\n");
return -1;
}
// Kontext aktivieren
if (!eglMakeCurrent(display, surface, surface, context)) {
printf("EGL-Kontext aktivieren fehlgeschlagen\n");
return -1;
}
return 0;
}
// Shader kompilieren
GLuint compileShader(GLenum type, const char *source) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, NULL);
glCompileShader(shader);
GLint success;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
GLchar infoLog[512];
glGetShaderInfoLog(shader, sizeof(infoLog), NULL, infoLog);
printf("Shader-Kompilierung fehlgeschlagen: %s\n", infoLog);
return 0;
}
return shader;
}
// OpenGL initialisieren
int initGL() {
// Shader kompilieren
vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource);
fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
// Shader-Programm erstellen
program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
GLint success;
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
GLchar infoLog[512];
glGetProgramInfoLog(program, sizeof(infoLog), NULL, infoLog);
printf("Shader-Programm-Verknüpfung fehlgeschlagen: %s\n", infoLog);
return -1;
}
// Shader nach Verknüpfung löschen
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return 0;
}
// Hauptfunktion
int main() {
// EGL und OpenGL initialisieren
if (initEGL() < 0 || initGL() < 0) {
return EXIT_FAILURE;
}
// Viewport einstellen
glViewport(0, 0, 256, 256);
// Bildschirm löschen
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// Shader-Programm aktivieren
glUseProgram(program);
// Dreieck-Vertices definieren
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
// Vertex-Buffer-Objekt erstellen
GLuint VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// Vertex-Attribut einstellen
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), 0);
glEnableVertexAttribArray(0);
// Dreieck zeichnen
glDrawArrays(GL_TRIANGLES, 0, 3);
// Buffer tauschen
eglSwapBuffers(display, surface);
// Kurz warten, um das Ergebnis zu sehen
printf("Dreieck gezeichnet\n");
sleep(1);
// Aufräumen
glDeleteBuffers(1, &VBO);
glDeleteProgram(program);
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglDestroySurface(display, surface);
eglDestroyContext(display, context);
eglTerminate(display);
return EXIT_SUCCESS;
}
NPU-Programmierung mit RKNN
Die NPU des RK3588 kann mit dem RKNN-SDK (Rockchip Neural Network SDK) programmiert werden.
// Beispiel für RKNN-Bildklassifizierung
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "rknn_api.h"
#define MODEL_PATH "mobilenet_v1.rknn"
#define INPUT_SIZE 224
// Bild laden (vereinfachte Version)
unsigned char* load_image(const char* filename, int* width, int* height, int* channels) {
// In einer realen Anwendung würde hier eine Bibliothek wie stb_image verwendet
// Für dieses Beispiel generieren wir ein Testbild
*width = INPUT_SIZE;
*height = INPUT_SIZE;
*channels = 3;
unsigned char* data = malloc(*width * *height * *channels);
if (!data) {
return NULL;
}
// Testbilddaten generieren (grauer Verlauf)
for (int y = 0; y < *height; y++) {
for (int x = 0; x < *width; x++) {
int idx = (y * *width + x) * *channels;
data[idx] = x % 256; // R
data[idx + 1] = y % 256; // G
data[idx + 2] = 128; // B
}
}
return data;
}
// Hauptfunktion
int main(int argc, char** argv) {
rknn_context ctx;
int ret;
// RKNN-Modell laden
FILE* fp = fopen(MODEL_PATH, "rb");
if (!fp) {
printf("Modell öffnen fehlgeschlagen: %s\n", MODEL_PATH);
return -1;
}
// Modellgröße bestimmen
fseek(fp, 0, SEEK_END);
long model_size = ftell(fp);
fseek(fp, 0, SEEK_SET);
// Modell in Speicher laden
void* model = malloc(model_size);
if (fread(model, 1, model_size, fp) != model_size) {
printf("Modell lesen fehlgeschlagen\n");
free(model);
fclose(fp);
return -1;
}
fclose(fp);
// RKNN initialisieren
ret = rknn_init(&ctx, model, model_size, 0, NULL);
free(model);
if (ret < 0) {
printf("RKNN-Initialisierung fehlgeschlagen: %d\n", ret);
return -1;
}
// RKNN-Eingabe-Attribute abrufen
rknn_input_output_num io_num;
ret = rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
if (ret < 0) {
printf("RKNN-Abfrage fehlgeschlagen: %d\n", ret);
rknn_destroy(ctx);
return -1;
}
printf("Modell hat %d Eingaben und %d Ausgaben\n", io_num.n_input, io_num.n_output);
// Eingabe-Tensoren konfigurieren
rknn_tensor_attr input_attrs[io_num.n_input];
memset(input_attrs, 0, sizeof(input_attrs));
for (int i = 0; i < io_num.n_input; i++) {
input_attrs[i].index = i;
ret = rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &input_attrs[i], sizeof(rknn_tensor_attr));
if (ret < 0) {
printf("RKNN-Eingabe-Attribut-Abfrage fehlgeschlagen: %d\n", ret);
rknn_destroy(ctx);
return -1;
}
}
// Ausgabe-Tensoren konfigurieren
rknn_tensor_attr output_attrs[io_num.n_output];
memset(output_attrs, 0, sizeof(output_attrs));
for (int i = 0; i < io_num.n_output; i++) {
output_attrs[i].index = i;
ret = rknn_query(ctx, RKNN_QUERY_OUTPUT_ATTR, &output_attrs[i], sizeof(rknn_tensor_attr));
if (ret < 0) {
printf("RKNN-Ausgabe-Attribut-Abfrage fehlgeschlagen: %d\n", ret);
rknn_destroy(ctx);
return -1;
}
}
// Eingabe-Tensor-Dimensionen anzeigen
printf("Eingabe-Tensoren:\n");
for (int i = 0; i < io_num.n_input; i++) {
printf(" Eingabe %d: Größe=%d, Typ=%d, Dimensionen=%d %d %d %d\n",
i, input_attrs[i].size, input_attrs[i].type,
input_attrs[i].dim[0], input_attrs[i].dim[1],
input_attrs[i].dim[2], input_attrs[i].dim[3]);
}
// Ausgabe-Tensor-Dimensionen anzeigen
printf("Ausgabe-Tensoren:\n");
for (int i = 0; i < io_num.n_output; i++) {
printf(" Ausgabe %d: Größe=%d, Typ=%d, Dimensionen=%d %d %d %d\n",
i, output_attrs[i].size, output_attrs[i].type,
output_attrs[i].dim[0], output_attrs[i].dim[1],
output_attrs[i].dim[2], output_attrs[i].dim[3]);
}
// Bild laden
int img_width, img_height, img_channels;
unsigned char* img_data = load_image("test.jpg", &img_width, &img_height, &img_channels);
if (!img_data) {
printf("Bild laden fehlgeschlagen\n");
rknn_destroy(ctx);
return -1;
}
// Eingaben vorbereiten
rknn_input inputs[1];
memset(inputs, 0, sizeof(inputs));
inputs[0].index = 0;
inputs[0].type = RKNN_TENSOR_UINT8;
inputs[0].size = img_width * img_height * img_channels;
inputs[0].fmt = RKNN_TENSOR_NHWC;
inputs[0].buf = img_data;
// Eingaben setzen
ret = rknn_inputs_set(ctx, io_num.n_input, inputs);
if (ret < 0) {
printf("RKNN-Eingaben setzen fehlgeschlagen: %d\n", ret);
free(img_data);
rknn_destroy(ctx);
return -1;
}
// Inferenz durchführen
printf("Inferenz läuft...\n");
ret = rknn_run(ctx, NULL);
if (ret < 0) {
printf("RKNN-Ausführung fehlgeschlagen: %d\n", ret);
free(img_data);
rknn_destroy(ctx);
return -1;
}
// Ausgaben abrufen
rknn_output outputs[io_num.n_output];
memset(outputs, 0, sizeof(outputs));
for (int i = 0; i < io_num.n_output; i++) {
outputs[i].want_float = 1;
}
ret = rknn_outputs_get(ctx, io_num.n_output, outputs, NULL);
if (ret < 0) {
printf("RKNN-Ausgaben abrufen fehlgeschlagen: %d\n", ret);
free(img_data);
rknn_destroy(ctx);
return -1;
}
// Top 5 Ergebnisse (für Klassifikationsmodell) finden
for (int i = 0; i < io_num.n_output; i++) {
float* output_buffer = (float*)outputs[i].buf;
uint32_t output_size = outputs[i].size / sizeof(float);
printf("Top 5 Klassifikationsergebnisse:\n");
for (int j = 0; j < 5 && j < output_size; j++) {
float max_value = -1000.0f;
int max_index = -1;
// Maximum finden
for (uint32_t k = 0; k < output_size; k++) {
if (output_buffer[k] > max_value) {
max_value = output_buffer[k];
max_index = k;
}
}
// Ergebnis anzeigen und Maximum entfernen für nächste Iteration
printf(" %d: Index=%d, Wert=%f\n", j, max_index, max_value);
output_buffer[max_index] = -1000.0f;
}
}
// Ausgaben freigeben
rknn_outputs_release(ctx, io_num.n_output, outputs);
// Aufräumen
free(img_data);
rknn_destroy(ctx);
return 0;
}
TensorFlow Lite auf RK3588 NPU
Der RK3588 unterstützt auch TensorFlow Lite mit NPU-Delegaten für beschleunigte KI-Verarbeitung. Hier ein Beispiel für die Integration:
// TensorFlow Lite mit RKNN-Delegaten
#include <iostream>
#include <cstdlib>
#include <tensorflow/lite/interpreter.h>
#include <tensorflow/lite/kernels/register.h>
#include <tensorflow/lite/model.h>
#include <tensorflow/lite/delegates/rknn/rknn_delegate.h>
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Verwendung: " << argv[0] << " <tflite_modell_pfad>" << std::endl;
return EXIT_FAILURE;
}
const char* model_path = argv[1];
// Modell laden
std::unique_ptr<tflite::FlatBufferModel> model =
tflite::FlatBufferModel::BuildFromFile(model_path);
if (!model) {
std::cerr << "Fehler beim Laden des TFLite-Modells" << std::endl;
return EXIT_FAILURE;
}
// Resolver erstellen
tflite::ops::builtin::BuiltinOpResolver resolver;
// Interpreter erstellen
std::unique_ptr<tflite::Interpreter> interpreter;
tflite::InterpreterBuilder(*model, resolver)(&interpreter);
if (!interpreter) {
std::cerr << "Fehler beim Erstellen des Interpreters" << std::endl;
return EXIT_FAILURE;
}
// RKNN-Delegaten erstellen
TfLiteRKNNDelegateOptions options = TfLiteRKNNDelegateOptionsDefault();
options.execution_priority = RKNN_PRIORITY_HIGH;
options.cache_path = "/data/rknn/cache";
TfLiteDelegate* rknn_delegate = TfLiteRKNNDelegateCreate(&options);
if (interpreter->ModifyGraphWithDelegate(rknn_delegate) != kTfLiteOk) {
std::cerr << "Fehler beim Anwenden des RKNN-Delegaten" << std::endl;
TfLiteRKNNDelegateDelete(rknn_delegate);
return EXIT_FAILURE;
}
// Tensor-Größen zuweisen
if (interpreter->AllocateTensors() != kTfLiteOk) {
std::cerr << "Fehler beim Zuweisen der Tensoren" << std::endl;
TfLiteRKNNDelegateDelete(rknn_delegate);
return EXIT_FAILURE;
}
// Eingabetensor abrufen
TfLiteTensor* input_tensor = interpreter->input_tensor(0);
// Beispiel-Eingabedaten erstellen (alle auf 0.5 setzen)
int input_size = 1;
for (int i = 0; i < input_tensor->dims->size; i++) {
input_size *= input_tensor->dims->data[i];
}
if (input_tensor->type == kTfLiteFloat32) {
float* input_data = interpreter->typed_input_tensor<float>(0);
for (int i = 0; i < input_size; i++) {
input_data[i] = 0.5f;
}
} else if (input_tensor->type == kTfLiteUInt8) {
uint8_t* input_data = interpreter->typed_input_tensor<uint8_t>(0);
for (int i = 0; i < input_size; i++) {
input_data[i] = 128;
}
} else {
std::cerr << "Nicht unterstützter Eingabetyp" << std::endl;
TfLiteRKNNDelegateDelete(rknn_delegate);
return EXIT_FAILURE;
}
// Inferenz durchführen
if (interpreter->Invoke() != kTfLiteOk) {
std::cerr << "Fehler bei der Inferenz" << std::endl;
TfLiteRKNNDelegateDelete(rknn_delegate);
return EXIT_FAILURE;
}
// Ausgabetensor abrufen
TfLiteTensor* output_tensor = interpreter->output_tensor(0);
// Ergebnisse ausgeben
if (output_tensor->type == kTfLiteFloat32) {
float* output_data = interpreter->typed_output_tensor<float>(0);
int output_size = 1;
for (int i = 0; i < output_tensor->dims->size; i++) {
output_size *= output_tensor->dims->data[i];
}
std::cout << "Ausgabe (erste 10 Werte):" << std::endl;
for (int i = 0; i < 10 && i < output_size; i++) {
std::cout << " [" << i << "]: " << output_data[i] << std::endl;
}
}
// Delegaten freigeben
TfLiteRKNNDelegateDelete(rknn_delegate);
return EXIT_SUCCESS;
}
7. Multimedia-Verarbeitung
Der RK3588 ist besonders leistungsfähig in der Multimedia-Verarbeitung mit Hardware-Codecs für 8K-Video, mehrere Kameraanschlüsse und hochwertige Audioverarbeitung.
Video-Dekodierung mit MPP (Media Process Platform)
Rockchip’s MPP-Framework ermöglicht die effiziente Nutzung der Hardware-Video-Codecs:
// Video-Dekodierungsbeispiel mit MPP
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <rockchip/rk_mpi.h>
#define MAX_FRAME_SIZE (8 * 1024 * 1024) // 8MB für einen Frame
int main(int argc, char** argv) {
if (argc < 3) {
printf("Verwendung: %s <eingang.h264> <ausgang.yuv>\n", argv[0]);
return -1;
}
// MPP-Kontext
MppCtx ctx;
MppApi *mpi;
// Input/Output Dateien
FILE *input_file = fopen(argv[1], "rb");
FILE *output_file = fopen(argv[2], "wb");
if (!input_file || !output_file) {
printf("Fehler beim Öffnen der Dateien\n");
return -1;
}
// MPP initialisieren
MPP_RET ret = mpp_create(&ctx, &mpi);
if (ret != MPP_OK) {
printf("MPP erstellen fehlgeschlagen: %d\n", ret);
return -1;
}
// Dekoder initialisieren
ret = mpi->control(ctx, MPP_SET_OUTPUT_TIMEOUT, (MppParam)&timeout);
if (ret != MPP_OK) {
printf("Timeout setzen fehlgeschlagen: %d\n", ret);
goto CLEANUP;
}
// Decoder konfigurieren
MppCodingType codec_type = MPP_VIDEO_CodingAVC; // H.264
ret = mpi->control(ctx, MPP_DEC_SET_CODING_TYPE, &codec_type);
if (ret != MPP_OK) {
printf("Codec-Typ setzen fehlgeschlagen: %d\n", ret);
goto CLEANUP;
}
// Decoder initialisieren
ret = mpi->init(ctx, MPP_CTX_DEC, codec_type);
if (ret != MPP_OK) {
printf("MPP initialisieren fehlgeschlagen: %d\n", ret);
goto CLEANUP;
}
// Eingabe-Puffer für Rohdaten
void *input_buffer = malloc(MAX_FRAME_SIZE);
if (!input_buffer) {
printf("Speicherallokation fehlgeschlagen\n");
goto CLEANUP;
}
// Paket für Dekodierung
MppPacket packet;
// Frame für dekodierte Ausgabe
MppFrame frame = NULL;
// Video-Stream dekodieren
size_t read_size;
size_t packet_size = 0;
while ((read_size = fread(input_buffer + packet_size, 1, 4096, input_file)) > 0) {
packet_size += read_size;
// Paket erstellen
ret = mpp_packet_init(&packet, input_buffer, packet_size);
if (ret != MPP_OK) {
printf("Paket initialisieren fehlgeschlagen: %d\n", ret);
break;
}
// Paket an Decoder senden
ret = mpi->decode_put_packet(ctx, packet);
if (ret != MPP_OK) {
printf("Paket senden fehlgeschlagen: %d\n", ret);
mpp_packet_deinit(&packet);
break;
}
mpp_packet_deinit(&packet);
// Dekodierte Frames abrufen
do {
ret = mpi->decode_get_frame(ctx, &frame);
if (ret != MPP_OK) {
printf("Frame abrufen fehlgeschlagen: %d\n", ret);
break;
}
if (frame) {
// Prüfen, ob Frame gültig ist
if (mpp_frame_get_errinfo(frame)) {
printf("Frame hat Fehler\n");
mpp_frame_deinit(&frame);
continue;
}
// Frame-Informationen abrufen
MppBuffer buffer = mpp_frame_get_buffer(frame);
if (buffer) {
void *ptr = mpp_buffer_get_ptr(buffer);
size_t size = mpp_buffer_get_size(buffer);
int width = mpp_frame_get_width(frame);
int height = mpp_frame_get_height(frame);
MppFrameFormat fmt = mpp_frame_get_fmt(frame);
printf("Dekodierter Frame: %dx%d, Format=%d\n", width, height, fmt);
// YUV-Daten in Ausgabedatei schreiben
fwrite(ptr, 1, size, output_file);
}
// Frame freigeben
mpp_frame_deinit(&frame);
frame = NULL;
}
} while (frame);
// Puffer zurücksetzen für nächstes Paket
packet_size = 0;
}
// Aufräumen
free(input_buffer);
CLEANUP:
if (ctx) {
mpi->reset(ctx);
mpp_destroy(ctx);
ctx = NULL;
}
if (input_file) fclose(input_file);
if (output_file) fclose(output_file);
return 0;
}
Kamera-Zugriff mit V4L2
Video4Linux2 (V4L2) ist die Standard-API für Kamera-Zugriff unter Linux:
// V4L2-Kamera-Beispiel
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#define CAMERA_DEVICE "/dev/video0"
#define BUFFER_COUNT 4
#define FRAME_WIDTH 1920
#define FRAME_HEIGHT 1080
#define PIXEL_FORMAT V4L2_PIX_FMT_YUYV
struct buffer {
void *start;
size_t length;
};
int main() {
int fd;
struct buffer *buffers;
// Kamera-Gerät öffnen
fd = open(CAMERA_DEVICE, O_RDWR);
if (fd < 0) {
perror("Kamera öffnen fehlgeschlagen");
return EXIT_FAILURE;
}
// Kamera-Fähigkeiten abfragen
struct v4l2_capability cap;
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
perror("Kamera-Fähigkeiten abfragen fehlgeschlagen");
close(fd);
return EXIT_FAILURE;
}
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
fprintf(stderr, "Gerät unterstützt keine Videoaufnahme\n");
close(fd);
return EXIT_FAILURE;
}
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
fprintf(stderr, "Gerät unterstützt kein Streaming\n");
close(fd);
return EXIT_FAILURE;
}
// Videoformat einstellen
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = FRAME_WIDTH;
fmt.fmt.pix.height = FRAME_HEIGHT;
fmt.fmt.pix.pixelformat = PIXEL_FORMAT;
fmt.fmt.pix.field = V4L2_FIELD_ANY;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
perror("Videoformat einstellen fehlgeschlagen");
close(fd);
return EXIT_FAILURE;
}
// Buffer anfordern
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));
req.count = BUFFER_COUNT;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
perror("Buffer anfordern fehlgeschlagen");
close(fd);
return EXIT_FAILURE;
}
if (req.count < 2) {
fprintf(stderr, "Nicht genug Pufferspeicher\n");
close(fd);
return EXIT_FAILURE;
}
// Buffer allozieren
buffers = calloc(req.count, sizeof(*buffers));
if (!buffers) {
perror("Speicherallokation fehlgeschlagen");
close(fd);
return EXIT_FAILURE;
}
// Buffer Memory-Map durchführen
for (unsigned int i = 0; i < req.count; i++) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
perror("Buffer-Info abfragen fehlgeschlagen");
close(fd);
free(buffers);
return EXIT_FAILURE;
}
buffers[i].length = buf.length;
buffers[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, buf.m.offset);
if (buffers[i].start == MAP_FAILED) {
perror("Memory-Map fehlgeschlagen");
close(fd);
free(buffers);
return EXIT_FAILURE;
}
}
// Buffer zur Warteschlange hinzufügen
for (unsigned int i = 0; i < req.count; i++) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
perror("Buffer zur Warteschlange hinzufügen fehlgeschlagen");
close(fd);
free(buffers);
return EXIT_FAILURE;
}
}
// Streaming starten
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {
perror("Streaming starten fehlgeschlagen");
close(fd);
free(buffers);
return EXIT_FAILURE;
}
// 10 Frames erfassen
for (int i = 0; i < 10; i++) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
// Buffer aus der Warteschlange holen
if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
perror("Buffer aus der Warteschlange holen fehlgeschlagen");
break;
}
printf("Frame %d erfasst: %ld Bytes\n", i, buffers[buf.index].length);
// Hier könnten Sie die Bilddaten verarbeiten
// z.B. in eine Datei schreiben:
char filename[20];
sprintf(filename, "frame%d.raw", i);
FILE *fp = fopen(filename, "wb");
if (fp) {
fwrite(buffers[buf.index].start, 1, buffers[buf.index].length, fp);
fclose(fp);
}
// Buffer wieder in die Warteschlange stellen
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
perror("Buffer zurück in die Warteschlange stellen fehlgeschlagen");
break;
}
}
// Streaming stoppen
if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0) {
perror("Streaming stoppen fehlgeschlagen");
}
// Aufräumen
for (unsigned int i = 0; i < req.count; i++) {
munmap(buffers[i].start, buffers[i].length);
}
free(buffers);
close(fd);
return EXIT_SUCCESS;
}