Grüße, Habr Gemeinschaft. Vor kurzem hat unser Unternehmen das IRIS-Mess- und Steuergerät auf den Markt gebracht. Als Hauptprogrammierer dieses Projekts möchte ich Sie über die Entwicklung der Geräte-Firmware informieren (Laut Projektmanager macht die Firmware nicht mehr als 30% der gesamten Arbeit von der Idee bis zur Massenproduktion aus). Der Artikel ist in erster Linie für unerfahrene Entwickler nützlich, um die Arbeitskosten eines "echten" Projekts und für Benutzer zu verstehen, die "unter die Haube schauen" möchten.
Zweck des Geräts
IRIS ist ein multifunktionales Messgerät. Er weiß, wie man Strom (Amperemeter), Spannung (Voltmeter), Leistung (Wattmeter) und eine Reihe anderer Größen misst. KIP IRIS merkt sich ihre Maximalwerte und schreibt Oszillogramme. Eine detaillierte Beschreibung des Geräts finden Sie auf der Website des Unternehmens.
Ein bisschen Statistik
Zeitliche Koordinierung
Erstes Commit für SVN: 16. Mai 2019.
Veröffentlichung: 19. Juni 2020.
* Dies ist Kalenderzeit, keine Vollzeitentwicklung während der gesamten Laufzeit. Es gab Ablenkungen für andere Projekte, Erwartungen an technische Spezifikationen, Hardware-Iterationen usw.
Commits
Nummer in SVN: 928
Woher kommt das?
1) Ich bin ein Befürworter von Microcommitting während der Entwicklung.
2) Duplikate in Zweigen für Hardware und Emulator.
3) Dokumentation
. Die Zahl mit einer Nutzlast in Form eines neuen Codes (Trunk-Zweig) beträgt also nicht mehr als 300.
Anzahl der Codezeilen
Statistiken wurden vom Dienstprogramm cloc mit Standardparametern erfasst, mit Ausnahme der Quellen HAL STM32 und ESP-IDF ESP32.
STM32-Firmware: 38.334 Codezeilen. Davon:
60870-5-101: 18751
ModbusRTU: 3859
Oszilloskop: 1944
Archivierer: 955
ESP32 Firmware: 1537 Codezeilen.
Hardwarekomponenten (Peripheriegeräte beteiligt)
Die Hauptfunktionen des Geräts sind in der STM32-Firmware implementiert. Die ESP32-Firmware ist für die Bluetooth-Kommunikation verantwortlich. Die Kommunikation zwischen den Chips erfolgt über UART (siehe Abbildung im Header).
NVIC ist ein Interrupt-Controller.
IWDG - Watchdog-Timer zum Neustart des Chips bei Firmware-Auflegen.
Timer - Timer-Interrupts halten den Projekt-Herzschlag aufrecht.
EEPROM - Speicher zum Speichern von Produktionsinformationen, Einstellungen, Maximalwerten und ADC-Kalibrierungskoeffizienten.
I2C ist eine Schnittstelle für den Zugriff auf den EEPROM-Chip.
NOR - Speicher zum Speichern von Wellenformen.
QSPI ist eine Schnittstelle für den Zugriff auf den NOR-Speicherchip.
RTC - Echtzeituhr liefert Zeitverlauf nach dem Ausschalten des Gerätes.
ADC - ADC.
RS485 ist eine serielle Schnittstelle für die Verbindung über die Protokolle ModbusRTU und 60870-101.
DIN, DOUT - diskreter Ein- und Ausgang.
Taste - eine Taste auf der Vorderseite des Geräts zum Umschalten der Anzeige zwischen den Messungen.
Softwarearchitektur
Hauptsoftwaremodule
Messdatenstrom
Operationssystem
Unter Berücksichtigung der Einschränkungen der Größe des Flash-Speichers (Betriebssystem führt Overhead ein) und der relativen Einfachheit des Geräts wurde beschlossen, die Verwendung des Betriebssystems aufzugeben und mit Interrupts auszukommen. Dieser Ansatz wurde bereits mehr als einmal in Artikeln über Habré hervorgehoben, daher werde ich nur Flussdiagramme von Aufgaben innerhalb von Interrupts mit ihren Prioritäten geben.
Beispielcode. Verzögerte Interrupt-Generierung in STM32.
// 6
HAL_NVIC_SetPriority(CEC_IRQn, 6, 0);
HAL_NVIC_EnableIRQ(CEC_IRQn);
//
HAL_NVIC_SetPendingIRQ(CEC_IRQn);
//
void CEC_IRQHandler(void) {
// user code
}
PWM 7-Segmentanzeige
Das Gerät verfügt über zwei Zeilen mit jeweils 4 Zeichen, insgesamt 8 Anzeigen. Die 7 Segmentanzeigen haben jeweils 8 parallele Datenleitungen (A, B, C, D, E, F, G, DP) und 2 Farbauswahlzeilen (grün und rot).
Wellenformspeicher
Der Speicher ist nach dem Prinzip eines Ringpuffers mit 64 KB Slots pro Wellenform (feste Größe) organisiert.
Sicherstellung der Datenkonsistenz im Falle eines unerwarteten Herunterfahrens
Im EEPROM werden Daten in zwei Kopien mit einer zusätzlichen Prüfsumme am Ende geschrieben. Wenn das Gerät zum Zeitpunkt der Datenaufzeichnung ausgeschaltet ist, bleibt mindestens eine Kopie der Daten erhalten. Die Prüfsumme wird auch zu jeder Schicht der Oszilloskopdaten hinzugefügt (Messwerte an den ADC-Eingängen), sodass eine ungültige Prüfsumme der Schicht ein Zeichen für das Ende des Oszillogramms ist.
Automatische Generierung der Softwareversion
1) Erstellen Sie die Datei
version.fmt : #define SVN_REV ($ WCREV $)
2) Fügen Sie vor dem Erstellen des Projekts den Befehl (für System Workbanch) hinzu:
SubWCRev $ {ProjDirPath} $ {ProjDirPath} /version.fmt $ {ProjDirPath} /version.h
Nach Ausführung dieses Befehls wird eine Datei version.h mit der letzten Festschreibungsnummer erstellt.
Es gibt ein ähnliches Dienstprogramm für GIT: GitWCRev. /version.fmt ./main/version.h
#define GIT_REV ($ WCLOGCOUNT $)
Damit können Sie Commit und Softwareversion eindeutig abgleichen .
Emulator
weil Die Entwicklung der Firmware begann vor dem Erscheinen der ersten Hardwareinstanz, dann begann ein Teil des Codes als Konsolenanwendung auf einem PC zu schreiben.
Vorteile:
- Die Entwicklung und das Debuggen für einen PC ist einfacher als direkt auf der Hardware.
- die Fähigkeit, Eingangssignale zu erzeugen.
- die Möglichkeit, den Client auf einem PC ohne Hardware zu debuggen. Der com0com-Treiber ist auf dem PC installiert, wodurch zwei COM-Ports erstellt werden. Einer von ihnen startet den Emulator und der andere verbindet den Client.
- trägt zu schöner Architektur bei, weil Sie müssen die Schnittstelle von hardwareabhängigen Modulen auswählen und zwei Implementierungen schreiben
Beispielcode. Zwei Implementierungen zum Lesen von Daten aus eeprom.
uint32_t eeprom_read(uint32_t offset, uint8_t * buf, uint32_t len);
ifdef STM32H7
uint32_t eeprom_read(uint32_t offset, uint8_t * buf, uint32_t len)
{
if (diag_isError(ERR_I2C))
return 0;
if (eeprom_wait_ready()) {
HAL_StatusTypeDef status = HAL_I2C_Mem_Read(&I2C_MEM_HANDLE, I2C_MEM_DEV_ADDR, offset, I2C_MEMADD_SIZE_16BIT, buf, len, I2C_MEM_TIMEOUT_MS);
if (status == HAL_OK)
return len;
}
diag_setError(ERR_I2C, true);
return 0;
}
#endif
#ifdef _WIN32
static FILE *fpEeprom = NULL;
#define EMUL_EEPROM_FILE "eeprom.bin"
void checkAndCreateEpromFile() {
if (fpEeprom == NULL) {
fopen_s(&fpEeprom, EMUL_EEPROM_FILE, "rb+");
if (fpEeprom == NULL)
fopen_s(&fpEeprom, EMUL_EEPROM_FILE, "wb+");
fseek(fpEeprom, EEPROM_SIZE, SEEK_SET);
fputc('\0', fpEeprom);
fflush(fpEeprom);
}
}
uint32_t eeprom_read(uint32_t offset, uint8_t * buf, uint32_t len)
{
checkAndCreateEpromFile();
fseek(fpEeprom, offset, SEEK_SET);
return (uint32_t)fread(buf, len, 1, fpEeprom);
}
#endif
Beschleunigung der Datenübertragung (Archivierung)
Um das Herunterladen von Wellenformen zu beschleunigen, wurden diese vor dem Senden archiviert. Die Uzzib- Bibliothek wurde als Archivierer verwendet . Das Entpacken dieses Formats in C # erfolgt in wenigen Codezeilen.
Beispielcode. Datenarchivierung.
#define ARCHIVER_HASH_BITS (12)
uint8_t __RAM_288K archiver_hash_table[sizeof(uzlib_hash_entry_t) * (1 << ARCHIVER_HASH_BITS)];
bool archive(const uint8_t* src, uint32_t src_len, uint8_t* dst, uint32_t dst_len, uint32_t *archive_len)
{
struct uzlib_comp comp = { 0 };
comp.dict_size = 32768;
comp.hash_bits = ARCHIVER_HASH_BITS;
comp.hash_table = (uzlib_hash_entry_t*)&archiver_hash_table[0];
memset((void*)comp.hash_table, 0, sizeof(archiver_hash_table));
comp.out.outbuf = &dst[10]; // skip header 10 bytes
comp.out.outsize = dst_len - 10 - 8; // skip header 10 bytes and tail(crc+len) 8 bytes
comp.out.is_overflow = false;
zlib_start_block(&comp.out);
uzlib_compress(&comp, src, src_len);
zlib_finish_block(&comp.out);
if (comp.out.is_overflow)
comp.out.outlen = 0;
dst[0] = 0x1f;
dst[1] = 0x8b;
dst[2] = 0x08;
dst[3] = 0x00; // FLG
// mtime
dst[4] =
dst[5] =
dst[6] =
dst[7] = 0;
dst[8] = 0x04; // XFL
dst[9] = 0x03; // OS
unsigned crc = ~uzlib_crc32(src, src_len, ~0);
memcpy(&dst[10 + comp.out.outlen], &crc, sizeof(crc));
memcpy(&dst[14 + comp.out.outlen], &src_len, sizeof(src_len));
*archive_len = 18 + comp.out.outlen;
if (comp.out.is_overflow)
return false;
return true;
}
Beispielcode. Daten entpacken.
// byte[] res; //
using (var msOut = new MemoryStream())
using (var ms = new MemoryStream(res))
using (var gzip = new GZipStream(ms, CompressionMode.Decompress))
{
int chunk = 4096;
var buffer = new byte[chunk];
int read;
do
{
read = gzip.Read(buffer, 0, chunk);
msOut.Write(buffer, 0, read);
} while (read == chunk);
//msOut.ToArray();//
}
Über dauerhafte Änderungen in der TK
Meme aus dem Internet:
- Aber Sie haben die Leistungsbeschreibung gebilligt!
- Technische Aufgabe? Wir dachten, TK sei ein "Standpunkt", und wir haben mehrere davon.
Beispielcode. Tastaturhandhabung.
enum {
IVA_KEY_MASK_NONE,
IVA_KEY_MASK_ENTER = 0x1,
IVA_KEY_MASK_ANY = IVA_KEY_MASK_ENTER,
}IVA_KEY;
uint8_t keyboard_isKeyDown(uint8_t keyMask) {
return ((keyMask & keyStatesMask) == keyMask);
}
Wenn Sie sich einen solchen Code angesehen haben, denken Sie vielleicht, warum er alles aufgestapelt hat, wenn das Gerät nur eine Taste enthält? In der ersten Version des TK gab es 5 Tasten und mit deren Hilfe war geplant, die Bearbeitung der Einstellungen direkt auf dem Gerät durchzuführen:
enum {
IVA_KEY_MASK_NONE = 0,
IVA_KEY_MASK_ENTER = 0x01,
IVA_KEY_MASK_LEFT = 0x02,
IVA_KEY_MASK_RIGHT = 0x04,
IVA_KEY_MASK_UP = 0x08,
IVA_KEY_MASK_DOWN = 0x10,
IVA_KEY_MASK_ANY = IVA_KEY_MASK_ENTER | IVA_KEY_MASK_LEFT | IVA_KEY_MASK_RIGHT | IVA_KEY_MASK_UP | IVA_KEY_MASK_DOWN,
}IVA_KEY;
Wenn Sie also eine Kuriosität im Code finden, müssen Sie sich nicht sofort mit schlechten Worten an den vorherigen Programmierer erinnern, vielleicht gab es zu dieser Zeit Gründe für eine solche Implementierung.
Einige Entwicklungsprobleme
Die Spülung ist vorbei
Der Mikrocontroller verfügt über 128 KB Flash-Speicher. Irgendwann hat der Debug-Build dieses Volume überschritten. Ich musste die Optimierung durch Volumen -Os aktivieren. Wenn ein Debugging auf Hardware erforderlich war, wurde eine spezielle Assembly mit einigen deaktivierten Softwaremodulen (modbas, 101st) erstellt.
QSPI-Datenfehler
Manchmal erschien beim Lesen von Daten über qspi ein "zusätzliches" Byte. Das Problem verschwand, nachdem die Priorität der qspi-Interrupts erhöht wurde.
Oszilloskop-Datenfehler
weil Daten werden von DMA gesendet, der Prozessor kann sie möglicherweise nicht "sehen" und alte Daten aus dem Cache lesen. Der Cache muss validiert werden.
Beispielcode. Cache-Validierung.
// QSPI/DMA
SCB_CleanDCache_by_Addr((uint32_t*)(((uint32_t)&data[0]) & 0xFFFFFFE0), dataSize + 32);
// ADC/DMA CPU
SCB_InvalidateDCache_by_Addr((uint32_t*)&s_pAlignedAdcBuffer[0], sizeof(s_pAlignedAdcBuffer));
ADC-Probleme (unterschiedliche Messwerte von Einschalten zu Einschalten)
Vom Einschalten bis zum Einschalten erschien im Gerät ein anderer Offset der Strommesswerte (in der Größenordnung von 10-30 mA). Die Lösung wurde von Kollegen von Kompel in der Person von Vladislav Barsov und Alexander Kvashin unterstützt, für die sie sich sehr bedanken.
Beispielcode. ADC-Initialisierung.
//
HAL_ADCEx_Calibration_SetValue (&hadc1, ADC_SINGLE_ENDED, myCalibrationFactor[0]);
HAL_ADCEx_Calibration_SetValue (&hadc1, ADC_DIFFERENTIAL_ENDED, myCalibrationFactor[1]);
HAL_ADCEx_LinearCalibration_SetValue (&hadc1, &myLinearCalib_Buffer[0]);
Hintergrundbeleuchtung anzeigen
Bei den "leeren" 7-Segment-Anzeigen erschien anstelle einer vollständigen Abschaltung eine schwache Beleuchtung. Der Grund ist, dass in der realen Welt die Wellenform nicht perfekt ist. Wenn Sie den Code gpio_set_level (0) ausgeführt haben, bedeutet dies nicht, dass sich der Signalpegel sofort geändert hat. Die Fackel wurde durch Hinzufügen einer PWM zu den Datenleitungen beseitigt.
Uart-Fehler in HAL
Nachdem ein Over-Run-Fehler aufgetreten ist, funktioniert der UART nicht mehr. Das Problem wurde mit dem HAL-Patch behoben:
Beispielcode. Patch für HAL.
--- if (((isrflags & USART_ISR_ORE) != 0U)
--- && (((cr1its & USART_CR1_RXNEIE_RXFNEIE) != 0U) ||
--- ((cr3its & (USART_CR3_RXFTIE | USART_CR3_EIE)) != 0U)))
+++ if ((isrflags & USART_ISR_ORE) != 0U)
{
__HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_OREF);
Zugriff auf nicht ausgerichtete Daten
Der Fehler trat nur auf der Hardware in einer Assembly mit der Optimierungsstufe -Os auf. Anstelle von realen Daten las der Modbus-Client Nullen.
Beispielcode. Fehler beim Lesen nicht ausgerichteter Daten.
float f_value;
uint16_t registerValue;
// registerValue 0
//registerValue = ((uint16_t*)&f_value)[(offsetInMaximeterData -
// offsetof(mbreg_Maximeter, primaryValue)) / 2];
// memcpy
memcpy(& registerValue, ((uint16_t*)&f_value) + (offsetInMaximeterData -
offsetof(mbreg_Maximeter, primaryValue)) / 2, sizeof(uint16_t));
Finden der Ursachen von HardFault
Eines der von mir verwendeten Tools zur Lokalisierung von Ausnahmen sind Überwachungspunkte. Ich verteile Überwachungspunkte um den Code und nachdem die Ausnahme angezeigt wurde, verbinde ich mich mit dem Debugger und sehe, an welchem Punkt der Code übergeben wurde.
Beispielcode. SET_DEBUG_POINT (__ LINE__).
//debug.h
#define USE_DEBUG_POINTS
#ifdef USE_DEBUG_POINTS
// SET_DEBUG_POINT1(__LINE__)
void SET_DEBUG_POINT1(uint32_t val);
void SET_DEBUG_POINT2(uint32_t val);
#else
#define SET_DEBUG_POINT1(...)
#define SET_DEBUG_POINT2(...)
#endif
//debug.c
#ifdef USE_DEBUG_POINTS
volatile uint32_t dbg_point1 = 0;
volatile uint32_t dbg_point2 = 0;
void SET_DEBUG_POINT1(uint32_t val) {
dbg_point1 = val;
}
void SET_DEBUG_POINT2(uint32_t val) {
dbg_point2 = val;
}
#endif
// :
SET_DEBUG_POINT1(__line__);
Tipps für Anfänger
1) Sehen Sie sich die Codebeispiele an. Für esp32 sind Beispiele im SDK enthalten. Für stm32 im HAL-Speicher STM32CubeMX \ STM32Cube_FW_H7_V1.7.0 \ Projects \ NUCLEO-H743ZI \ Beispiele \
2) Google: Programmierhandbuch <Ihr Chip>, technisches Referenzhandbuch <Ihr Chip>, Anwendungshinweis <Ihr Chip>, Datenblatt <Ihr Chip>.
3) Wenn Sie technische Schwierigkeiten haben und die beiden wichtigsten Punkte nicht geholfen haben, sollten Sie die Kontaktaufnahme mit dem Support nicht vernachlässigen, sondern die Händler, die direkten Kontakt zu den Ingenieuren des Unternehmens des Herstellers haben.
4) Fehler befinden sich nicht nur in Ihrem Code, sondern auch in der HAL des Herstellers.
Vielen Dank für Ihre Aufmerksamkeit.