Desarrollo del dispositivo de medición IRIS

imagen

Saludos, comunidad Habr. Recientemente, nuestra empresa lanzó al mercado el dispositivo de control y medición IRIS. Como programador principal de este proyecto, quiero contarles sobre el desarrollo del firmware del dispositivo (según el gerente del proyecto, el firmware no es más del 30% de la cantidad total de trabajo desde la idea hasta la producción en masa). El artículo será principalmente útil para desarrolladores novatos en términos de comprensión de los costos laborales de un proyecto "real" y usuarios que quieran "mirar bajo el capó".



Propósito del dispositivo



IRIS es un dispositivo de medición multifuncional. Sabe cómo medir corriente (amperímetro), voltaje (voltímetro), potencia (vatímetro) y una serie de otras cantidades. KIP IRIS recuerda sus valores máximos, escribe oscilogramas. Se puede encontrar una descripción detallada del dispositivo en el sitio web de la empresa.



Un poco de estadística



Sincronización



Compromiso por primera vez con SVN: 16 de mayo de 2019.

Lanzamiento: 19 de junio de 2020.

* Este es el tiempo del calendario, no el desarrollo a tiempo completo durante todo el período. Hubo distracciones para otros proyectos, expectativas de especificaciones técnicas, iteraciones de hardware, etc.



Compromete



Número en SVN: 928

¿De dónde viene esto?

1) Soy partidario del microcompromiso durante el desarrollo

2) Duplicados en ramas para hardware y emulador

3) Documentación

Entonces, el número con una carga útil en forma de un nuevo código (rama troncal) no es más de 300.

imagen



Número de líneas de código



Las estadísticas fueron recopiladas por la utilidad cloc con parámetros predeterminados, excluyendo las fuentes HAL STM32 y ESP-IDF ESP32.

imagen

Firmware STM32: 38,334 líneas de código. De los cuales:

60870-5-101: 18751

ModbusRTU: 3859

Osciloscopio: 1944 Archivador

: 955

Firmware ESP32: 1537 líneas de código.



Componentes de hardware (periféricos involucrados)



Las principales funciones del dispositivo se implementan en el firmware STM32. El firmware ESP32 es responsable de la comunicación Bluetooth. La comunicación entre los chips se realiza a través de la UART (ver figura en el encabezado).

NVIC es un controlador de interrupciones.

IWDG: temporizador de vigilancia para reiniciar el chip en caso de que el firmware se cuelgue.

Temporizadores: las interrupciones del temporizador mantienen el ritmo del proyecto.

EEPROM: memoria para almacenar información de producción, configuraciones, lecturas máximas, coeficientes de calibración ADC.

I2C es una interfaz para acceder al chip EEPROM.

NOR - memoria para almacenar formas de onda.

QSPI es una interfaz para acceder al chip de memoria NOR.

RTC: el reloj en tiempo real proporciona un curso de tiempo después de apagar el dispositivo.

ADC - ADC.

RS485 es una interfaz en serie para la conexión a través de los protocolos ModbusRTU y 60870-101.

DIN, DOUT: entrada y salida discretas.

Botón: un botón en el panel frontal del dispositivo para cambiar la indicación entre mediciones.



Arquitectura de software



Módulos de software principales



imagen



Flujo de datos de medición



imagen



sistema operativo



Teniendo en cuenta las limitaciones de la cantidad de memoria flash (el sistema operativo introduce gastos generales) y la relativa simplicidad del dispositivo, se decidió abandonar el uso del sistema operativo y arreglárselas con las interrupciones. Este enfoque ya se ha destacado en artículos sobre Habré más de una vez, por lo que solo daré diagramas de flujo de tareas dentro de las interrupciones con sus prioridades.

imagen



Código de muestra. Generación de interrupciones retardadas en 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
}




Pantalla PWM de 7 segmentos



El dispositivo tiene dos líneas de 4 caracteres cada una, un total de 8 indicadores. Las pantallas de 7 segmentos tienen 8 líneas de datos paralelas (A, B, C, D, E, F, G, DP) y 2 líneas de selección de color (verde y rojo) para cada una.

imagen



Almacenamiento de formas de onda



El almacenamiento se organiza según el principio de un búfer circular con ranuras de 64 KB por forma de onda (tamaño fijo).



Garantizar la integridad de los datos en caso de un cierre inesperado



En la EEPROM, los datos se escriben en dos copias con una suma de comprobación adicional al final. Si en el momento de la grabación de datos el dispositivo está apagado, al menos una copia de los datos permanecerá intacta. La suma de control también se agrega a cada segmento de los datos del osciloscopio (valores medidos en las entradas del ADC), por lo que una suma de control no válida del segmento será un signo del final del oscilograma.



Generación automática de versión de software



1) Cree el archivo version.fmt:

#define SVN_REV ($ WCREV $)

2) Antes de construir el proyecto, agregue el comando (para System Workbanch):

SubWCRev $ {ProjDirPath} $ {ProjDirPath} /version.fmt $ {ProjDirPath} /version.h

Después de ejecutar este comando, se creará un archivo version.h con el último número de confirmación.



Existe una utilidad similar para GIT: GitWCRev. /version.fmt ./main/version.h

#define GIT_REV ($ WCLOGCOUNT $)

Esto le permite hacer coincidir sin ambigüedades la confirmación y la versión del software.



Emulador



Porque el desarrollo del firmware comenzó antes de la aparición de la primera instancia de hardware, luego parte del código comenzó a escribirse como una aplicación de consola en una PC.

imagen

Ventajas:

- El desarrollo y la depuración para PC es más fácil que directamente en el hardware.

- la capacidad de generar cualquier señal de entrada.

- la capacidad de depurar el cliente en una PC sin hardware. El controlador com0com está instalado en la PC, lo que crea un par de puertos com. Uno de ellos inicia el emulador y el otro conecta al cliente.

- contribuye a la hermosa arquitectura, porque tienes que seleccionar la interfaz de los módulos dependientes del hardware y escribir dos implementaciones



Código de muestra. Dos implementaciones de lectura de datos de 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




Aceleración de la transferencia de datos (archivo)



Para aumentar la velocidad de descarga de formas de onda, se archivaron antes de enviarlas. La biblioteca uzlib se utilizó como archivador . El desempaquetado de este formato en C # se realiza en un par de líneas de código.



Código de muestra. Archivo de datos.




#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;
}




Código de muestra. Desembalaje de datos.



// 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();//    
                        }




Acerca de los cambios permanentes en los conocimientos tradicionales



Meme de Internet:

- ¡Pero usted aprobó los términos de referencia!

- ¿Tarea técnica? Pensamos que los conocimientos tradicionales eran un "punto de vista", y tenemos varios de ellos.



Código de muestra. Manejo del teclado.




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);
}


Habiendo examinado un código de este tipo, podría pensar por qué lo apiló todo, si solo hay un botón en el dispositivo. En la primera versión del TK había 5 botones y con la ayuda de ellos se planeó implementar la edición de la configuración directamente en el dispositivo:


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;


Entonces, si encuentra una rareza en el código, entonces no necesita recordar inmediatamente al programador anterior con malas palabras, tal vez en ese momento hubo razones para tal implementación.



Algunos problemas de desarrollo



El rubor se acabó



El microcontrolador tiene 128 KB de memoria flash. En algún momento, la compilación de depuración superó este volumen. Tuve que habilitar la optimización por volumen -Os. Si se requería depurar el hardware, entonces se realizó un ensamblaje especial con la desactivación de algunos módulos de software (modbas, 101st).



Error de datos QSPI



A veces, al leer datos a través de qspi, aparecía un byte "extra". El problema desapareció después de aumentar la prioridad de las interrupciones qspi.



Error de datos del osciloscopio



Porque DMA envía datos, es posible que el procesador no los "vea" y lea los datos antiguos de la caché. Debe realizar la validación de la caché.



Código de muestra. Validación de caché.




//           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));




Problemas de ADC (diferentes lecturas de encendido a encendido)



Desde el encendido hasta el encendido, apareció en el dispositivo un desplazamiento diferente de las lecturas actuales (aproximadamente 10-30 mA). La solución fue ayudada por colegas de Kompel en la persona de Vladislav Barsov y Alexander Kvashin por lo que muchas gracias a ellos.



Código de muestra. Inicialización de 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]);




Indicación de luz de fondo



En los indicadores de 7 segmentos "vacíos", en lugar de un apagado completo, apareció una iluminación débil. La razón es que en el mundo real la forma de onda no es ideal, y si ejecutó el código gpio_set_level (0), no significa que el nivel de la señal cambió inmediatamente. El destello se eliminó agregando un PWM a las líneas de datos.



Error Uart en HAL



Después de que ocurriera un error de Over-Run, la UART dejó de funcionar. El problema se solucionó con el parche HAL:



Código de muestra. Parche para HAL'a.



---    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);




Accediendo a datos no alineados



El error se manifestó solo en el hardware en un ensamblado con el nivel de optimización -Os. En lugar de datos reales, el cliente modbus lee ceros.



Código de muestra. Error al leer datos no alineados.



	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));




Encontrar las causas de HardFault



Una de las herramientas de localización de excepciones que utilizo son los puntos de observación. Disperso los puntos de observación alrededor del código y, después de que aparece la excepción, me conecto con el depurador y veo qué punto ha pasado el código.



Código de muestra. 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__);




Consejos para principiantes



1) Eche un vistazo a los ejemplos de código. Para esp32, se incluyen ejemplos con el SDK. Para stm32 en almacenamiento HAL STM32CubeMX \ STM32Cube_FW_H7_V1.7.0 \ Projects \ NUCLEO-H743ZI \ Examples \

2) Google: manual de programación <su chip>, manual de referencia técnica <su chip>, nota de aplicación <su chip>, hoja de datos <su chip>.

3) Si tiene alguna dificultad técnica y los 2 puntos principales no le ayudaron, no debe descuidar el contacto con el soporte, sino con los distribuidores que tienen contacto directo con los ingenieros de la empresa del fabricante.

4) Los errores no solo están en su código, sino también en el HAL del fabricante.



Gracias por su atención.



All Articles