Salutations, communauté Habr. Récemment, notre société a lancé le dispositif de mesure et de contrôle IRIS sur le marché. En tant que programmeur principal de ce projet, je veux vous parler du développement du firmware de l'appareil (selon le chef de projet, le firmware ne représente pas plus de 30% du travail total de l'idée à la production de masse). L'article sera avant tout utile pour les développeurs novices en termes de compréhension des coûts de main-d'œuvre d'un «vrai» projet et des utilisateurs qui veulent «regarder sous le capot».
Objectif de l'appareil
IRIS est un appareil de mesure multifonctionnel. Il sait mesurer le courant (ampèremètre), la tension (voltmètre), la puissance (wattmètre) et un certain nombre d'autres grandeurs. KIP IRIS mémorise leurs valeurs maximales, écrit des oscillogrammes. Une description détaillée de l'appareil est disponible sur le site Web de l'entreprise.
Un peu de statistiques
Horaire
Premier engagement à SVN: 16 mai 2019
Sortie: 19 juin 2020.
* Il s'agit de l'heure du calendrier, pas du développement à plein temps tout au long du trimestre. Il y avait des distractions pour d'autres projets, des attentes de spécifications techniques, des itérations matérielles, etc.
S'engage
Nombre en SVN: 928
D'où cela vient-il?
1) Je suis partisan du micro-engagement pendant le développement
2) Dupliques dans les branches pour le matériel et l'émulateur
3) La documentation
Ainsi, le numéro avec une charge utile sous la forme d'un nouveau code (trunk branch) n'est pas supérieur à 300.
Nombre de lignes de code
Les statistiques ont été collectées par l'utilitaire cloc avec des paramètres par défaut, à l'exclusion des sources HAL STM32 et ESP-IDF ESP32.
Micrologiciel STM32: 38 334 lignes de code. Dont:
60870-5-101: 18751
ModbusRTU: 3859
Oscilloscope: 1944 Archiveur
: 955
Firmware ESP32: 1537 lignes de code.
Composants matériels (périphériques concernés)
Les principales fonctions de l'appareil sont implémentées dans le firmware STM32. Le micrologiciel ESP32 est responsable de la communication Bluetooth. La communication entre les puces se fait via UART (voir la figure dans l'en-tête).
NVIC est un contrôleur d'interruption.
IWDG - minuterie de surveillance pour redémarrer la puce en cas de blocage du micrologiciel.
Minuteries - Les interruptions du minuteur maintiennent le rythme du projet.
EEPROM - mémoire pour stocker les informations de production, les paramètres, les lectures maximales, les coefficients d'étalonnage ADC.
I2C est une interface pour accéder à la puce EEPROM.
NOR - mémoire pour stocker les formes d'onde.
QSPI est une interface pour accéder à la puce mémoire NOR.
RTC - l'horloge en temps réel fournit le cours du temps après la mise hors tension de l'appareil.
ADC - ADC.
RS485 est une interface série pour la connexion via les protocoles ModbusRTU et 60870-101.
DIN, DOUT - entrée et sortie TOR.
Bouton - un bouton sur le panneau avant de l'appareil pour commuter l'indication entre les mesures.
Architecture logicielle
Principaux modules logiciels
Flux de données de mesure
système opérateur
Compte tenu des limites de la quantité de mémoire flash (le système d'exploitation introduit des frais généraux) et de la relative simplicité de l'appareil, il a été décidé d'abandonner l'utilisation du système d'exploitation et de se débrouiller avec les interruptions. Cette approche a déjà été mise en avant dans des articles sur Habré plus d'une fois, je ne donnerai donc que des organigrammes des tâches à l'intérieur des interruptions avec leurs priorités.
Exemple de code. Génération d'interruption retardée dans 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
}
Affichage à 7 segments PWM
L'appareil comporte deux lignes de 4 caractères chacune, soit un total de 8 indicateurs. Les 7 segments d'affichage ont 8 lignes de données parallèles (A, B, C, D, E, F, G, DP) et 2 lignes de sélection de couleur (verte et rouge) pour chacun.
Stockage de forme d'onde
Le stockage est organisé sur le principe d'un buffer circulaire avec 64 Ko slots par waveform (taille fixe).
Assurer l'intégrité des données en cas d'arrêt inattendu
Dans l'EEPROM, les données sont écrites en deux exemplaires avec une somme de contrôle ajoutée à la fin. Si, au moment de l'enregistrement des données, l'appareil est éteint, au moins une copie des données restera intacte. La somme de contrôle est également ajoutée à chaque tranche des données de l'oscilloscope (valeurs mesurées aux entrées ADC), de sorte qu'une somme de contrôle invalide de la tranche sera un signe de la fin de l'oscillogramme.
Génération automatique de la version du logiciel
1) Créer un fichier version.fmt:
#define SVN_REV ($ WCREV $)
2) Avant de créer le projet, ajoutez la commande (pour System Workbanch):
SubWCRev $ {ProjDirPath} $ {ProjDirPath} /version.fmt $ {ProjDirPath} /version.h
Après avoir exécuté cette commande, un fichier version.h sera créé avec le dernier numéro de validation.
Il existe un utilitaire similaire pour GIT: GitWCRev. /version.fmt ./main/version.h
#define GIT_REV ($ WCLOGCOUNT $)
Cela vous permet de faire correspondre sans ambiguïté la validation et la version du logiciel.
Émulateur
Parce que le développement du firmware a commencé avant l'apparition de la première instance de matériel, puis une partie du code a commencé à être écrite comme une application console sur un PC.
Avantages:
- le développement et le débogage pour un PC sont plus faciles que directement sur le matériel.
- la possibilité de générer des signaux d'entrée.
- la possibilité de déboguer le client sur un PC sans matériel. Le pilote com0com est installé sur le PC, ce qui crée une paire de ports com. L'un d'eux démarre l'émulateur et l'autre connecte le client.
- contribue à la belle architecture, car vous devez sélectionner l'interface des modules dépendants du matériel et écrire deux implémentations
Exemple de code. Deux implémentations de lecture de données depuis 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
Accélération du transfert de données (archivage)
Pour augmenter la vitesse de téléchargement des formes d'onde, elles ont été archivées avant d'être envoyées. La bibliothèque uzlib a été utilisée comme archiveur . La décompression de ce format en C # se fait en quelques lignes de code.
Exemple de code. Archivage des données.
#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;
}
Exemple de code. Déballage des données.
// 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();//
}
À propos des modifications permanentes des savoirs traditionnels
Meme d'Internet:
- Mais vous avez approuvé les termes de référence!
- Tâche technique? Nous pensions que les savoirs traditionnels étaient un «point de vue» et nous en avons plusieurs.
Exemple de code. Manipulation du clavier.
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);
}
Après avoir regardé un tel morceau de code, vous pourriez vous demander pourquoi il a tout empilé, s'il n'y a qu'un seul bouton dans l'appareil? Dans la première version du TK, il y avait 5 boutons et, à l'aide d'eux, il était prévu de mettre en œuvre l'édition des paramètres directement sur l'appareil:
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;
Donc, si vous trouvez une bizarrerie dans le code, vous n'avez pas besoin de vous souvenir immédiatement du programmeur précédent avec de mauvais mots, peut-être à ce moment-là il y avait des raisons pour une telle implémentation.
Quelques problèmes de développement
La couleur est terminée
Le microcontrôleur dispose de 128 Ko de mémoire flash. À un moment donné, la version de débogage a dépassé ce volume. J'ai dû activer l'optimisation par volume -Os. Si le débogage sur le matériel était nécessaire, alors un assemblage spécial a été fait avec la désactivation de certains modules logiciels (modbas, 101st).
Erreur de données QSPI
Parfois, lors de la lecture de données via qspi, un octet "supplémentaire" est apparu. Le problème a disparu après avoir augmenté la priorité des interruptions qspi.
Erreur de données de l'oscilloscope
Parce que les données sont envoyées par DMA, le processeur peut ne pas les «voir» et lire les anciennes données du cache. Vous devez effectuer la validation du cache.
Exemple de code. Validation du cache.
// 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));
Problèmes ADC (différentes lectures d'une mise en marche à l'autre)
De la mise en marche à la mise en marche, un décalage différent des lectures de courant (environ 10-30 mA) est apparu dans l'appareil. La solution a été aidée par des collègues de Kompel en la personne de Vladislav Barsov et Alexander Kvashin pour lesquels un grand merci à eux.
Exemple de code. Initialisation ADC.
//
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]);
Rétroéclairage de l'écran
Sur les afficheurs 7 segments «vides», au lieu d'un arrêt complet, un faible éclairage est apparu. La raison en est que dans le monde réel, la forme d'onde n'est pas idéale, et si vous exécutez le code gpio_set_level (0), cela ne signifie pas que le niveau du signal a immédiatement changé. La torche a été éliminée en ajoutant un PWM aux lignes de données.
Erreur Uart dans HAL
Après qu'une erreur Over-Run s'est produite, l'UART a cessé de fonctionner. Le problème a été résolu avec le patch HAL:
Exemple de code. Patch pour 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);
Accéder aux données non alignées
L'erreur s'est manifestée uniquement sur le matériel dans un assembly avec le niveau d'optimisation -Os. Au lieu de données réelles, le client modbus lit des zéros.
Exemple de code. Erreur lors de la lecture des données non alignées.
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));
Trouver les causes de HardFault
L'un des outils de localisation d'exception que j'utilise est watchpoints. Je disperse des points de surveillance autour du code, et après l'apparition de l'exception, je me connecte au débogueur et vois à quel point le code est passé.
Exemple de code. 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__);
Conseils pour les débutants
1) Jetez un œil aux exemples de code. Pour esp32, des exemples sont inclus avec le SDK. Pour stm32 dans le stockage HAL STM32CubeMX \ STM32Cube_FW_H7_V1.7.0 \ Projects \ NUCLEO-H743ZI \ Exemples \
2) Google: manuel de programmation <votre puce>, manuel de référence technique <votre puce>, note d'application <votre puce>, fiche technique <votre puce>.
3) Si vous rencontrez des difficultés techniques et que les 2 points principaux n'ont pas aidé, vous ne devez pas négliger de contacter le support, mais plutôt les distributeurs qui ont un contact direct avec les ingénieurs de l'entreprise du fabricant.
4) Les bogues ne sont pas seulement dans votre code, mais aussi dans le HAL du fabricant.
Merci de votre attention.