Desenvolvimento do dispositivo de medição IRIS

imagem

Saudações, comunidade Habr. Recentemente, nossa empresa lançou o dispositivo de medição e controle IRIS no mercado. Como principal programador deste projeto, quero falar sobre o desenvolvimento do firmware do dispositivo (de acordo com o gerente de projeto, o firmware não é mais do que 30% do trabalho total da ideia à produção em massa). O artigo será útil principalmente para desenvolvedores novatos em termos de compreensão dos custos de mão de obra de um projeto "real" e usuários que desejam "dar uma olhada nos bastidores".



Objetivo do dispositivo



IRIS é um dispositivo de medição multifuncional. Ele sabe como medir corrente (amperímetro), tensão (voltímetro), potência (wattímetro) e uma série de outras grandezas. KIP IRIS lembra seus valores máximos, escreve oscilogramas. Uma descrição detalhada do dispositivo pode ser encontrada no site da empresa.



Um pouco de estatística



Cronometragem



Primeiro comprometa-se com o SVN: 16 de maio de 2019.

Versão: 19 de junho de 2020.

* Este é o período do calendário, não o desenvolvimento em tempo integral ao longo do período. Havia distrações para outros projetos, expectativas de especificações técnicas, iterações de hardware, etc.



Compromissos



Número no SVN: 928 De

onde vem isso?

1) Eu sou um defensor da microcommitting durante o desenvolvimento

2) Duplicatas em branches para hardware e emulador

3) Documentação

Então, o número com uma carga útil na forma de um novo código (branch trunk) não é mais que 300.

imagem



Número de linhas de código



As estatísticas foram coletadas pelo utilitário cloc com parâmetros padrão, excluindo as fontes HAL STM32 e ESP-IDF ESP32.

imagem

Firmware STM32: 38.334 linhas de código. Sendo :

60870-5-101: 18751

ModbusRTU: 3859

Osciloscópio: 1944

Arquivador: 955

ESP32 firmware: 1537 linhas de código.



Componentes de hardware (periféricos envolvidos)



As principais funções do dispositivo são implementadas no firmware STM32. O firmware ESP32 é responsável pela comunicação Bluetooth. A comunicação entre os chips é realizada via UART (veja a figura no cabeçalho).

NVIC é um controlador de interrupção.

IWDG - temporizador watchdog para reiniciar o chip em caso de desligamento do firmware.

Temporizadores - as interrupções do temporizador mantêm a pulsação do projeto.

EEPROM - memória para armazenar informações de produção, configurações, leituras máximas, coeficientes de calibração ADC.

I2C é uma interface para acessar o chip EEPROM.

NOR - memória para armazenamento de formas de onda.

QSPI é uma interface para acessar o chip de memória NOR.

RTC - relógio em tempo real fornece curso de tempo após desligar o dispositivo.

ADC - ADC.

RS485 é uma interface serial para conexão via protocolos ModbusRTU e 60870-101.

DIN, DOUT - entrada e saída discretas.

Botão - um botão no painel frontal do dispositivo para alternar a indicação entre as medições.



Arquitetura de software



Módulos principais de software



imagem



Fluxo de dados de medição



imagem



sistema operacional



Levando em consideração as limitações da quantidade de memória flash (o SO introduz sobrecarga) e a relativa simplicidade do dispositivo, decidiu-se abandonar o uso do sistema operacional e conviver com as interrupções. Essa abordagem já foi destacada em artigos sobre Habré mais de uma vez, então darei apenas fluxogramas de tarefas dentro de interrupções com suas prioridades.

imagem



Código de amostra. Geração de interrupção atrasada em 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
}




Display PWM de 7 segmentos



O dispositivo possui duas linhas de 4 caracteres cada, num total de 8 indicadores. As telas de 7 segmentos têm 8 linhas de dados paralelas (A, B, C, D, E, F, G, DP) e 2 linhas de seleção de cores (verde e vermelha) para cada uma.

imagem



Armazenamento de forma de onda



O armazenamento é organizado no princípio de um buffer circular com slots de 64 KB por forma de onda (tamanho fixo).



Garantir a integridade dos dados em caso de desligamento inesperado



Na EEPROM, os dados são gravados em duas cópias com uma soma de verificação adicionada no final. Se no momento da gravação dos dados o aparelho for desligado, pelo menos uma cópia dos dados permanecerá intacta. A soma de verificação também é adicionada a cada fatia dos dados do osciloscópio (valores medidos nas entradas ADC), portanto, uma soma de verificação inválida da fatia será um sinal do fim do oscilograma.



Geração automática de versão de software



1) Crie o arquivo version.fmt:

#define SVN_REV ($ WCREV $)

2) Antes de construir o projeto, adicione o comando (para System Workbanch):

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

Depois de executar este comando, um arquivo version.h com o último número de confirmação será criado.



Existe um utilitário semelhante para GIT: GitWCRev. /version.fmt ./main/version.h

#define GIT_REV ($ WCLOGCOUNT $)

Isso permite que você corresponda inequivocamente o commit e a versão do software.



Emulador



Porque o desenvolvimento do firmware começou antes do aparecimento da primeira cópia do hardware, então parte do código começou a ser escrito como um aplicativo de console em um PC.

imagem

Vantagens:

- o desenvolvimento e a depuração para um PC são mais fáceis do que diretamente no hardware.

- a capacidade de gerar quaisquer sinais de entrada.

- a capacidade de depurar o cliente em um PC sem hardware. O driver com0com é instalado no PC, o que cria um par de portas COM. Um deles inicia o emulador e o outro conecta o cliente.

- contribui para uma bela arquitetura, porque você tem que selecionar a interface dos módulos dependentes de hardware e escrever duas implementações



Código de amostra. Duas implementações de leitura de dados da 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




Aceleração da transferência de dados (arquivamento)



Para aumentar a velocidade de download dos oscilogramas, eles foram arquivados antes do envio. A biblioteca uzlib foi usada como arquivador . A descompactação desse formato em C # é feita em algumas linhas de código.



Código de amostra. Arquivamento de dados.




#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 amostra. Descompactando dados.



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




Sobre mudanças permanentes no TK



Meme da Internet:

- Mas você aprovou os termos de referência!

- Tarefa técnica? Pensamos que o TK era um "ponto de vista" e temos vários deles.



Código de amostra. Manuseio do 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);
}


Depois de olhar para esse pedaço de código, você pode pensar por que ele empilhou tudo, se há apenas um botão no dispositivo? Na primeira versão do TK havia 5 botões e com a ajuda deles foi planejado implementar a edição das configurações diretamente no 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;


Portanto, se você encontrar algo estranho no código, não precisará se lembrar imediatamente do programador anterior com palavrões, talvez naquela época houvesse motivos para tal implementação.



Alguns problemas de desenvolvimento



O flush acabou



O microcontrolador possui 128 KB de memória flash. Em algum ponto, a compilação de depuração excedeu esse volume. Tive que habilitar a otimização por volume -Os. Se a depuração no hardware for necessária, uma montagem especial foi feita com alguns módulos de software desabilitados (modbas, 101st).



Erro de dados QSPI



Às vezes, ao ler dados via qspi, um byte "extra" aparecia. O problema desapareceu depois de aumentar a prioridade das interrupções qspi.



Erro de dados do osciloscópio



Porque os dados são enviados por DMA, o processador pode não "ver" e ler os dados antigos do cache. Você precisa realizar a validação do cache.



Código de amostra. Validação de 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));




Problemas de ADC (leituras diferentes de ligar para ligar)



Desde a ativação até a ativação, um deslocamento diferente das leituras de corrente (cerca de 10-30 mA) apareceu no dispositivo. A solução foi ajudada por colegas de Kompel na pessoa de Vladislav Barsov e Alexander Kvashin pelos quais muitos agradecimentos a eles.



Código de amostra. Inicialização 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]);




Indicação de luz de fundo



Nos indicadores "vazios" de 7 segmentos, em vez de um desligamento completo, uma iluminação fraca apareceu. A razão é que no mundo real a forma de onda não é perfeita e se você executou o código gpio_set_level (0), isso não significa que o nível do sinal mudou imediatamente. O flare foi eliminado adicionando um PWM às linhas de dados.



Erro UART em HAL



Após a ocorrência de um erro Over-Run, o UART parou de funcionar. O problema foi corrigido com o patch HAL:



Código de amostra. Patch para 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);




Acessando dados não alinhados



O erro se manifestou apenas no hardware em uma montagem com o nível de otimização -Os. Em vez de dados reais, o cliente modbus lê zeros.



Código de amostra. Erro ao ler dados desalinhados.



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




Encontrando as causas do HardFault



Uma das ferramentas de localização de exceção que uso são os pontos de controle. Espalho pontos de controle ao redor do código e, depois que a exceção aparece, me conecto com o depurador e vejo em que ponto o código passou.



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




Dicas para iniciantes



1) Dê uma olhada nos exemplos de código. Para esp32, exemplos estão incluídos no SDK. Para stm32 no armazenamento HAL STM32CubeMX \ STM32Cube_FW_H7_V1.7.0 \ Projects \ NUCLEO-H743ZI \ Examples \

2) Google: manual de programação <seu chip>, manual de referência técnica <seu chip>, nota de aplicação <seu chip>, folha de dados <seu chip>.

3) Se você tiver alguma dificuldade técnica e os 2 pontos principais não ajudarem, você não deve deixar de entrar em contato com o suporte, mas sim com os distribuidores que têm contato direto com os engenheiros da empresa do fabricante.

4) Os bugs não estão apenas no seu código, mas também no HAL do fabricante.



Obrigado pela atenção.



All Articles