Der Betrieb mit Komponenten außerhalb des Mikrocontrollers oder des Ziels selbst ist die Norm in der Firmware-Entwicklung. Daher ist es wichtig zu wissen, wie man Bibliotheken für sie entwickelt. Diese Bibliotheken ermöglichen es uns, mit ihnen zu interagieren und Informationen oder Befehle auszutauschen. Es ist jedoch nicht ungewöhnlich, dass diese Interaktionen mit Komponenten in Legacy-Code oder Code von Studenten (oder weniger Studenten) direkt im Anwendungscode erfolgen oder dass diese Interaktionen, selbst wenn sie in separaten Dateien abgelegt werden, intrinsisch erfolgen an das Ziel gebunden.
Sehen wir uns ein schlechtes Beispiel der Bibliotheksentwicklung für einen Bosch BME280 Temperatur-, Feuchtigkeits- und Drucksensor in einer Anwendung für einen STMicroelectronics STM32F401RE an. Im Beispiel wollen wir die Komponente initialisieren und alle 1 Sekunde die Temperatur ablesen. (Im Beispielcode lassen wir alle von STM32CubeMX/IDE erzeugten „Rauschen“ weg, wie z. B. die Initialisierung verschiedener Uhren und Peripheriegeräte oder Kommentare wie USER CODE BEGIN oder USER CODE END.)
#include "i2c.h" #include <stdint.h> int main(void) { uint8_t idx = 0U; uint8_t tx_buffer[64] = {0}; uint8_t rx_buffer[64] = {0}; uint16_t dig_temp1 = 0U; int16_t dig_temp2 = 0; int16_t dig_temp3 = 0; MX_I2C1_Init(); tx_buffer[idx++] = 0b10100011; HAL_I2C_Mem_Write(&hi2c1, 0x77U << 1U, 0xF4U, 1U, tx_buffer, 1U, 200U); HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0x88U, 1U, rx_buffer, 6U, 200U); dig_temp1 = ((uint16_t)rx_buffer[0]) | (((uint16_t)rx_buffer[1]) << 8U); dig_temp2 = (int16_t)(((uint16_t)rx_buffer[2]) | (((uint16_t)rx_buffer[3]) << 8U)); dig_temp3 = (int16_t)(((uint16_t)rx_buffer[4]) | (((uint16_t)rx_buffer[5]) << 8U)); while (1) { float temperature = 0.0f; int32_t adc_temp = 0; int32_t t_fine = 0; float var1 = 0.0f; float var2 = 0.0f; HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0xFAU, 1U, rx_buffer, 3U, 200U); adc_temp = (int32_t)((((uint32_t)rx_buffer[0]) << 12U) | (((uint32_t)rx_buffer[1]) << 4U) | (((uint32_t)rx_buffer[2]) >> 4U)); var1 = (((float)adc_temp) / 16384.0f - ((float)dig_temp1) / 1024.0f) * ((float)dig_temp2); var2 = ((((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f) * (((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f)) * ((float)dig_temp3); t_fine = (int32_t)(var1 + var2); temperature = ((float)t_fine) / 5129.0f; // Temperature available for the application. } }
Anhand dieses Beispiels können wir eine Reihe von Fragen aufwerfen: Was passiert, wenn ich das Ziel ändern muss (sei es aufgrund von Lagerknappheit, dem Wunsch, Kosten zu senken oder einfach an einem anderen Produkt zu arbeiten, das dieselbe Komponente verwendet)? Was passiert, wenn ich mehr als eine Komponente desselben Typs im System habe? Was passiert, wenn ein anderes Produkt dieselbe Komponente verwendet? Wie kann ich meine Entwicklung testen, wenn ich noch nicht über die Hardware verfüge (eine in der Berufswelt sehr häufige Situation, in der sich Firmware- und Hardware-Entwicklungsphasen an bestimmten Stellen im Prozess oft überschneiden)?
Für die ersten drei Fragen besteht die Antwort darin, den Code zu bearbeiten, sei es, um ihn beim Zielwechsel vollständig zu ändern, den vorhandenen Code zu duplizieren, um mit einer zusätzlichen Komponente desselben Typs zu arbeiten, oder um denselben Code für die zu implementieren anderes Projekt/Produkt. Bei der letzten Frage gibt es keine Möglichkeit, den Code zu testen, ohne über die Hardware zu verfügen, um ihn auszuführen. Das bedeutet, dass wir erst nach Fertigstellung der Hardware mit dem Testen unseres Codes beginnen und mit der Behebung von Fehlern beginnen können, die mit der Firmware-Entwicklung selbst verbunden sind, wodurch sich die Produktentwicklungszeit verlängert. Dies wirft die Frage auf, die zu diesem Beitrag führt: Ist es möglich, Bibliotheken für Komponenten zu entwickeln, die unabhängig vom Ziel sind und eine Wiederverwendung ermöglichen? Die Antwort ist ja, und das werden wir in diesem Beitrag sehen.
Um Bibliotheken von einem Ziel zu isolieren, befolgen wir zwei Regeln: 1) Wir implementieren die Bibliothek in ihrer eigenen Kompilierungseinheit, d. h. in ihrer eigenen Datei, und 2) es gibt keine Verweise auf zielspezifische Header oder Funktionen . Wir werden dies demonstrieren, indem wir eine einfache Bibliothek für den BME280 implementieren. Zunächst erstellen wir in unserem Projekt einen Ordner namens bme280. Im Ordner „bme280“ erstellen wir die folgenden Dateien: bme280.c, bme280.h und bme280_interface.h. Zur Klarstellung: Nein, ich habe nicht vergessen, die Datei bme280_interface.c zu nennen. Diese Datei wird nicht Teil der Bibliothek sein.
Normalerweise platziere ich die Bibliotheksordner in Application/lib/.
Die Datei bme280.h deklariert alle in unserer Bibliothek verfügbaren Funktionen, die von unserer Anwendung aufgerufen werden sollen. Andererseits implementiert die Datei bme280.c die Definitionen dieser Funktionen sowie alle Hilfs- und privaten Funktionen, die die Bibliothek möglicherweise enthält. Was enthält also die Datei bme280_interface.h? Nun, unser Ziel, was auch immer es sein mag, muss auf die eine oder andere Weise mit der BME280-Komponente kommunizieren. In diesem Fall unterstützt der BME280 entweder SPI- oder I2C-Kommunikation. In beiden Fällen muss das Ziel in der Lage sein, Bytes für die Komponente zu lesen und zu schreiben. Die Datei bme280_interface.h deklariert diese Funktionen, sodass sie aus der Bibliothek aufgerufen werden können. Die Definition dieser Funktionen ist der einzige Teil, der an das spezifische Ziel gebunden ist, und sie ist das Einzige, was wir bearbeiten müssen, wenn wir die Bibliothek auf ein anderes Ziel migrieren.
Wir beginnen mit der Deklaration der verfügbaren Funktionen in der Bibliothek in der Datei bme280.h.
#include "i2c.h" #include <stdint.h> int main(void) { uint8_t idx = 0U; uint8_t tx_buffer[64] = {0}; uint8_t rx_buffer[64] = {0}; uint16_t dig_temp1 = 0U; int16_t dig_temp2 = 0; int16_t dig_temp3 = 0; MX_I2C1_Init(); tx_buffer[idx++] = 0b10100011; HAL_I2C_Mem_Write(&hi2c1, 0x77U << 1U, 0xF4U, 1U, tx_buffer, 1U, 200U); HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0x88U, 1U, rx_buffer, 6U, 200U); dig_temp1 = ((uint16_t)rx_buffer[0]) | (((uint16_t)rx_buffer[1]) << 8U); dig_temp2 = (int16_t)(((uint16_t)rx_buffer[2]) | (((uint16_t)rx_buffer[3]) << 8U)); dig_temp3 = (int16_t)(((uint16_t)rx_buffer[4]) | (((uint16_t)rx_buffer[5]) << 8U)); while (1) { float temperature = 0.0f; int32_t adc_temp = 0; int32_t t_fine = 0; float var1 = 0.0f; float var2 = 0.0f; HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0xFAU, 1U, rx_buffer, 3U, 200U); adc_temp = (int32_t)((((uint32_t)rx_buffer[0]) << 12U) | (((uint32_t)rx_buffer[1]) << 4U) | (((uint32_t)rx_buffer[2]) >> 4U)); var1 = (((float)adc_temp) / 16384.0f - ((float)dig_temp1) / 1024.0f) * ((float)dig_temp2); var2 = ((((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f) * (((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f)) * ((float)dig_temp3); t_fine = (int32_t)(var1 + var2); temperature = ((float)t_fine) / 5129.0f; // Temperature available for the application. } }
Die Bibliothek, die wir erstellen, wird sehr einfach sein und wir werden nur eine grundlegende Initialisierungsfunktion und eine weitere implementieren, um eine Temperaturmessung zu erhalten. Jetzt implementieren wir die Funktionen in der Datei bme280.c.
Um den Beitrag nicht zu ausführlich zu machen, überspringe ich die Kommentare, die die Funktionen dokumentieren würden. Dies ist die Datei, in der diese Kommentare abgelegt werden. Da heute so viele KI-Tools verfügbar sind, gibt es keine Entschuldigung dafür, Ihren Code nicht zu dokumentieren.
Das Grundgerüst der bme280.c-Datei würde wie folgt aussehen:
#ifndef BME280_H_ #define BME280_H_ void BME280_init(void); float BME280_get_temperature(void); #endif // BME280_H_
Konzentrieren wir uns auf die Initialisierung. Wie bereits erwähnt, unterstützt der BME280 sowohl I2C- als auch SPI-Kommunikation. In beiden Fällen müssen wir das entsprechende Peripheriegerät des Ziels (I2C oder SPI) initialisieren und dann in der Lage sein, Bytes darüber zu senden und zu empfangen. Angenommen, wir verwenden I2C-Kommunikation, wäre es im STM32F401RE:
#include "i2c.h" #include <stdint.h> int main(void) { uint8_t idx = 0U; uint8_t tx_buffer[64] = {0}; uint8_t rx_buffer[64] = {0}; uint16_t dig_temp1 = 0U; int16_t dig_temp2 = 0; int16_t dig_temp3 = 0; MX_I2C1_Init(); tx_buffer[idx++] = 0b10100011; HAL_I2C_Mem_Write(&hi2c1, 0x77U << 1U, 0xF4U, 1U, tx_buffer, 1U, 200U); HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0x88U, 1U, rx_buffer, 6U, 200U); dig_temp1 = ((uint16_t)rx_buffer[0]) | (((uint16_t)rx_buffer[1]) << 8U); dig_temp2 = (int16_t)(((uint16_t)rx_buffer[2]) | (((uint16_t)rx_buffer[3]) << 8U)); dig_temp3 = (int16_t)(((uint16_t)rx_buffer[4]) | (((uint16_t)rx_buffer[5]) << 8U)); while (1) { float temperature = 0.0f; int32_t adc_temp = 0; int32_t t_fine = 0; float var1 = 0.0f; float var2 = 0.0f; HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0xFAU, 1U, rx_buffer, 3U, 200U); adc_temp = (int32_t)((((uint32_t)rx_buffer[0]) << 12U) | (((uint32_t)rx_buffer[1]) << 4U) | (((uint32_t)rx_buffer[2]) >> 4U)); var1 = (((float)adc_temp) / 16384.0f - ((float)dig_temp1) / 1024.0f) * ((float)dig_temp2); var2 = ((((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f) * (((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f)) * ((float)dig_temp3); t_fine = (int32_t)(var1 + var2); temperature = ((float)t_fine) / 5129.0f; // Temperature available for the application. } }
Sobald das Peripheriegerät initialisiert ist, müssen wir die Komponente initialisieren. Hier müssen wir auf die Angaben des Herstellers im Datenblatt zurückgreifen. Hier ist eine kurze Zusammenfassung: Wir müssen den Temperaturmesskanal starten (der sich standardmäßig im Schlafmodus befindet) und einige im ROM der Komponente gespeicherte Kalibrierungskonstanten lesen, die wir später zur Berechnung der Temperatur benötigen.
Ziel dieses Beitrags ist es nicht, den Umgang mit dem BME280 zu erlernen, daher werde ich Einzelheiten zu seiner Verwendung überspringen, die Sie im Datenblatt finden.
Die Initialisierung würde so aussehen:
#ifndef BME280_H_ #define BME280_H_ void BME280_init(void); float BME280_get_temperature(void); #endif // BME280_H_
Details zum Kommentieren. Die von uns gelesenen Kalibrierungswerte werden in den Variablen dig_temp1, dig_temp2 und dig_temp3 gespeichert. Diese Variablen werden als global deklariert, sodass sie für die übrigen Funktionen in der Bibliothek verfügbar sind. Sie werden jedoch als statisch deklariert, sodass sie nur innerhalb der Bibliothek zugänglich sind. Niemand außerhalb der Bibliothek muss auf diese Werte zugreifen oder sie ändern.
Wir sehen auch, dass der Rückgabewert der I2C-Anweisungen überprüft wird und im Falle eines Fehlers die Funktionsausführung angehalten wird. Das ist in Ordnung, aber es kann verbessert werden. Wäre es nicht besser, den Aufrufer der Funktion BME280_init zu benachrichtigen, dass etwas schief gelaufen ist, wenn das der Fall wäre? Dazu definieren wir die folgende Aufzählung in der Datei bme280.h.
Ich verwende typedef für sie. Es gibt Diskussionen über die Verwendung von typedef, da sie die Lesbarkeit des Codes verbessern, jedoch auf Kosten der Ausblendung von Details. Es ist eine Frage der persönlichen Vorlieben und der Frage, ob alle Mitglieder des Entwicklungsteams auf dem gleichen Stand sind.
void BME280_init(void) { } float BME280_get_temperature(void) { }
Zwei Anmerkungen: Normalerweise füge ich das Suffix _t zu Typedefs hinzu, um anzuzeigen, dass es sich um Typedefs handelt, und ich füge das Typedef-Präfix zu den Werten oder Mitgliedern des Typedefs hinzu, in diesem Fall BME280_Status_. Letzteres dient dazu, Kollisionen zwischen Aufzählungen aus verschiedenen Bibliotheken zu vermeiden. Wenn jeder OK als Aufzählung verwenden würde, wären wir in Schwierigkeiten.
Jetzt können wir sowohl die Deklaration (bme280.h) als auch die Definition (bme280.c) der Funktion BME280_init ändern, um einen Status zurückzugeben. Die endgültige Version unserer Funktion wäre:
void BME280_init(void) { MX_I2C1_Init(); }
#include "i2c.h" #include <stdint.h> #define BME280_TX_BUFFER_SIZE 32U #define BME280_RX_BUFFER_SIZE 32U #define BME280_TIMEOUT 200U #define BME280_ADDRESS 0x77U #define BME280_REG_CTRL_MEAS 0xF4U #define BME280_REG_DIG_T 0x88U static uint16_t dig_temp1 = 0U; static int16_t dig_temp2 = 0; static int16_t dig_temp3 = 0; void BME280_init(void) { uint8_t idx = 0U; uint8_t tx_buffer[BME280_TX_BUFFER_SIZE] = {0}; uint8_t rx_buffer[BME280_RX_BUFFER_SIZE] = {0}; HAL_StatusTypeDef status = HAL_ERROR; MX_I2C1_Init(); tx_buffer[idx++] = 0b10100011; status = HAL_I2C_Mem_Write( &hi2c1, BME280_ADDRESS << 1U, BME280_REG_CTRL_MEAS, 1U, tx_buffer, (uint16_t)idx, BME280_TIMEOUT); if (status != HAL_OK) return; status = HAL_I2C_Mem_Read( &hi2c1, BME280_ADDRESS << 1U, BME280_REG_DIG_T, 1U, rx_buffer, 6U, BME280_TIMEOUT); if (status != HAL_OK) return; dig_temp1 = ((uint16_t)rx_buffer[0]); dig_temp1 = dig_temp1 | (((uint16_t)rx_buffer[1]) << 8U); dig_temp2 = ((int16_t)rx_buffer[2]); dig_temp2 = dig_temp2 | (((int16_t)rx_buffer[3]) << 8U); dig_temp3 = ((int16_t)rx_buffer[4]); dig_temp3 = dig_temp3 | (((int16_t)rx_buffer[5]) << 8U); return; }
Da wir die Status-Enumeration verwenden, müssen wir die Datei bme280.h in die Datei bme280.c einschließen. Wir haben die Bibliothek bereits initialisiert. Jetzt erstellen wir die Funktion zum Abrufen der Temperatur. Es würde so aussehen:
typedef enum { BME280_Status_Ok, BME280_Status_Status_Err, } BME280_Status_t;
Ihnen ist es aufgefallen, oder? Wir haben die Funktionssignatur so geändert, dass sie einen Status zurückgibt, der angibt, ob Kommunikationsprobleme mit der Komponente aufgetreten sind oder nicht, und das Ergebnis über den Zeiger zurückgegeben wird, der als Parameter an die Funktion übergeben wird. Wenn Sie dem Beispiel folgen, denken Sie daran, die Funktionsdeklaration in der Datei bme280.h so zu ändern, dass sie übereinstimmen.
BME280_Status_t BME280_init(void);
Großartig! An diesem Punkt können wir in der Anwendung Folgendes haben:
#include "i2c.h" #include <stdint.h> int main(void) { uint8_t idx = 0U; uint8_t tx_buffer[64] = {0}; uint8_t rx_buffer[64] = {0}; uint16_t dig_temp1 = 0U; int16_t dig_temp2 = 0; int16_t dig_temp3 = 0; MX_I2C1_Init(); tx_buffer[idx++] = 0b10100011; HAL_I2C_Mem_Write(&hi2c1, 0x77U << 1U, 0xF4U, 1U, tx_buffer, 1U, 200U); HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0x88U, 1U, rx_buffer, 6U, 200U); dig_temp1 = ((uint16_t)rx_buffer[0]) | (((uint16_t)rx_buffer[1]) << 8U); dig_temp2 = (int16_t)(((uint16_t)rx_buffer[2]) | (((uint16_t)rx_buffer[3]) << 8U)); dig_temp3 = (int16_t)(((uint16_t)rx_buffer[4]) | (((uint16_t)rx_buffer[5]) << 8U)); while (1) { float temperature = 0.0f; int32_t adc_temp = 0; int32_t t_fine = 0; float var1 = 0.0f; float var2 = 0.0f; HAL_I2C_Mem_Read(&hi2c1, 0x77U << 1U, 0xFAU, 1U, rx_buffer, 3U, 200U); adc_temp = (int32_t)((((uint32_t)rx_buffer[0]) << 12U) | (((uint32_t)rx_buffer[1]) << 4U) | (((uint32_t)rx_buffer[2]) >> 4U)); var1 = (((float)adc_temp) / 16384.0f - ((float)dig_temp1) / 1024.0f) * ((float)dig_temp2); var2 = ((((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f) * (((float)adc_temp) / 131072.0f - ((float)dig_temp1) / 8192.0f)) * ((float)dig_temp3); t_fine = (int32_t)(var1 + var2); temperature = ((float)t_fine) / 5129.0f; // Temperature available for the application. } }
Super sauber! Das ist lesbar. Ignorieren Sie die Verwendung der Error_Handler-Funktion von STM32CubeMX/IDE. Im Allgemeinen wird die Verwendung nicht empfohlen, aber zum Beispiel funktioniert es bei uns. Also, ist es geschafft?
Naja, nein! Wir haben unsere Interaktionen mit der Komponente in eigene Dateien gekapselt. Aber sein Code ruft immer noch Zielfunktionen (HAL-Funktionen) auf! Wenn wir das Ziel ändern, müssen wir die Bibliothek neu schreiben! Hinweis: Wir haben noch nichts in die Datei bme280_interface.h geschrieben. Lassen Sie uns das jetzt angehen.
Wenn wir uns die Datei bme280.c ansehen, sind unsere Interaktionen mit dem Ziel dreifach: Peripheriegeräte zu initialisieren, Bytes zu schreiben/senden und Bytes zu lesen/empfangen. Wir deklarieren also diese drei Interaktionen in der Datei bme280_interface.h.
#ifndef BME280_H_ #define BME280_H_ void BME280_init(void); float BME280_get_temperature(void); #endif // BME280_H_
Wenn Sie es bemerken, haben wir auch einen neuen Typ für den Schnittstellenstatus definiert. Anstatt die Zielfunktionen nun direkt aufzurufen, rufen wir diese Funktionen aus der Datei bme280.c auf.
void BME280_init(void) { } float BME280_get_temperature(void) { }
Et voilà! Die Zielabhängigkeiten sind aus der Bibliothek verschwunden. Wir haben jetzt eine Bibliothek, die für STM32, MSP430, PIC32 usw. funktioniert. In den drei Bibliotheksdateien sollte nichts Spezifisches für ein Ziel erscheinen. Was ist das Einzige, was noch übrig ist? Nun, die Schnittstellenfunktionen definieren. Dies ist der einzige Teil, der für jedes Ziel migriert/angepasst werden muss.
Normalerweise mache ich das im Ordner Application/bsp/components/.
Wir erstellen eine Datei namens bme280_implementation.c mit folgendem Inhalt:
void BME280_init(void) { MX_I2C1_Init(); }
Wenn wir die Bibliothek in einem anderen Projekt oder auf einem anderen Ziel verwenden möchten, müssen wir auf diese Weise nur die Datei bme280_implementation.c anpassen. Der Rest bleibt genau gleich.
Damit haben wir ein einfaches Beispiel einer Bibliothek gesehen. Diese Implementierung ist die einfachste, sicherste und gebräuchlichste. Abhängig von den Besonderheiten unseres Projekts gibt es jedoch unterschiedliche Varianten. In diesem Beispiel haben wir gesehen, wie eine Auswahl der Implementierung zum Linkzeitpunkt durchgeführt wird. Das heißt, wir haben die Datei bme280_implementation.c, die die Definitionen der Schnittstellenfunktionen während des Kompilierungs-/Verknüpfungsprozesses bereitstellt. Was würde passieren, wenn wir zwei Implementierungen haben wollten? Einer für die I2C-Kommunikation und einer für die SPI-Kommunikation. In diesem Fall müssten wir die Implementierungen zur Laufzeit mithilfe von Funktionszeigern angeben.
Ein weiterer Aspekt ist, dass wir in diesem Beispiel davon ausgehen, dass es nur einen BME280 im System gibt. Was würde passieren, wenn wir mehr als eine hätten? Sollten wir Code kopieren/einfügen und Präfixe zu Funktionen wie BME280_1 und BME280_2 hinzufügen? Nein. Das ist nicht ideal. Wir würden Handler verwenden, um es uns zu ermöglichen, mit derselben Bibliothek auf verschiedenen Instanzen einer Komponente zu arbeiten.
Diese Aspekte und wie wir unsere Bibliothek testen können, bevor unsere Hardware überhaupt verfügbar ist, sind ein Thema für einen anderen Beitrag, den wir in zukünftigen Artikeln behandeln werden. Im Moment gibt es keine Entschuldigung dafür, Bibliotheken nicht ordnungsgemäß zu implementieren. Meine erste Empfehlung (und paradoxerweise die, die ich für den Schluss aufgehoben habe) lautet jedoch: Stellen Sie zunächst sicher, dass der Hersteller nicht bereits eine offizielle Bibliothek für seine Komponente bereitstellt. Dies ist der schnellste Weg, eine Bibliothek zum Laufen zu bringen. Seien Sie versichert, dass die vom Hersteller bereitgestellte Bibliothek wahrscheinlich einer ähnlichen Implementierung folgen wird wie die, die wir heute gesehen haben, und unsere Aufgabe wird es sein, den Schnittstellenimplementierungsteil an unser Ziel oder Produkt anzupassen.
Wenn Sie sich für dieses Thema interessieren, finden Sie diesen und andere Beiträge zur Entwicklung eingebetteter Systeme auf meinem Blog! ?
Das obige ist der detaillierte Inhalt vonWiederverwendbare Komponentenbibliotheken: Vereinfachung der Migration zwischen Zielen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!