
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.

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.

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

Fluxo de dados de medição

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.

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.

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.

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.