Para a questão da TI

“Agora eu vou te mostrar um retrato ... Hmm ... eu aviso que isso é um retrato ... Enfim, por favor, trate-o como um retrato ...

Neste post, falaremos sobre o desenvolvimento e a depuração de programas para o CC1350 MK no ambiente de desenvolvimento do CCS recomendado pelo fabricante. Os méritos (e são) e as desvantagens (e como sem elas) dos produtos acima serão afetados. Não haverá capturas de tela no texto para mostrar (circulado) o local do ícone de compilação no ambiente de programação integrado ou selecionar um arquivo em um diretório. Reconhecendo a possibilidade fundamental de artigos em um estilo semelhante, tentarei focar em questões conceituais, na esperança de que meu leitor seja capaz de descobrir os detalhes.

O objetivo dessa obra, além de compartilhar a experiência adquirida, é uma tentativa de despertar inveja saudável entre os fabricantes domésticos de MK, que são concorrentes diretos da TI ("no país onde prosperamos") - a tarefa é francamente ingrata, mas eles dizem que uma pedra desgasta uma gota.

Enfatizarei imediatamente que será apenas sobre o Windows (além disso, apenas) versão 7, embora o site da TI tenha uma opção para Mac e Linux, eu ainda não os testei, estou pronto para acreditar que tudo não é tão legal por lá, mas por que pensar sobre o mal (ou vice-versa, tudo é ótimo lá, mas então por que inveja).

Então, o que o site da TI nos ensina - para começar a trabalhar com os módulos de avaliação, você precisa executar três etapas necessárias:

  1. Compre módulos de avaliação - concluídos.

    Nota sobre as margens (PNP): Você também precisará fazer isso, porque no ambiente de programação em questão eu pessoalmente (infelizmente) não consegui encontrar a capacidade de emular hardware para depuração, pelo menos para onde eu estava procurando.
  2. Instale o ambiente de desenvolvimento - faça o download, execute o instalador, tudo deu certo. Conectamos o módulo de avaliação ao USB - a lenha aumenta por si só e tudo deu certo novamente - feito. Ao tentar programar o dispositivo, recebemos uma mensagem sobre a necessidade de atualizar o firmware, concordamos e novamente tudo acabou. Em geral, não há nada sobre o que escrever se sempre e em toda parte ...
  3. Vá e estude o curso TI SimpleLink Academy 3.10.01 para o SimpleLink CC13x0 SDK 3.10 - uma sugestão estranha, parece que me ensina - apenas para estragar, mas que assim seja, eu abro o link correspondente e estou atordoado - quanto está empacotado lá.

Aqui vemos materiais de treinamento sobre como trabalhar com drivers de hardware SYS / BIOS e com o sistema operacional TI-RTOS e sobre como usar a pilha de rede NDK, incluindo USB, sobre protocolos sem fio e muitos outros aspectos do trabalho com representantes de várias famílias MK produzidas pela empresa. E toda essa riqueza é acompanhada de exemplos prontos para uso, e se levarmos em conta a presença de manuais do usuário e descrições de módulos, talvez não haja mais o que desejar. Mas também existem utilitários que facilitam o trabalho de preparação e configuração do código do programa, piscando e depurando de várias maneiras, e essa riqueza também é bastante documentada.

Pnp: se alguém estiver inclinado a considerar esse material como publicidade em relação à empresa, seus produtos e sistema de programação, provavelmente estará certo e estou realmente muito impressionado com o volume de software detectado. Sua qualidade será discutida mais detalhadamente e, espero, minhas suspeitas de viés serão dissipadas, não fiquei completamente cego pelo sentimento e continuo vendo perfeitamente as falhas do objeto de descrição, então esse não é o amor da juventude, mas um sentimento sério de um especialista adulto. Estou com medo de imaginar a quantidade de custos de material necessários para criar e manter esse volume de software e documentação para isso, mas obviamente isso não foi feito em um mês e a empresa provavelmente entende o que está fazendo.

Tudo bem, até adiarmos o estudo dos materiais para mais tarde, compreenderemos tudo "ao longo do caminho com a pepita" e abriremos o CCS com ousadia. Ele implementa o conceito de áreas de trabalho, recebidas do pai - Eclipse. Pessoalmente, o conceito de projeto está mais próximo de mim, mas ninguém nos incomoda para manter exatamente um projeto no espaço, então vamos seguir em frente.

Mas então as coisas ficam um pouco piores - abrimos o espaço de trabalho (RP) para nossa placa de depuração e vemos muitos projetos (como regra, em duas versões) no RTOS e no "ferro a descoberto"). Como eu disse anteriormente, isso não é crime, mas o fato de muitos projetos conterem os mesmos arquivos com módulos de software idênticos não é nada bom. O código é duplicado várias vezes e as alterações de suporte tornam-se uma tarefa não trivial. Sim, com essa solução, é muito mais fácil transferir o projeto simplesmente copiando o diretório, mas, para isso, existe a exportação do projeto, e ele é muito bem implementado. Os links para arquivos na árvore do projeto são adequadamente suportados, portanto, a decisão de incluir os arquivos nos exemplos fornecidos não pode ser considerada satisfatória.

Continuamos nossa pesquisa - começaremos a trabalhar com um projeto concluído, mas sem piscar um LED, embora haja dois deles na placa de depuração, mas trabalhando com uma porta serial, um exemplo de uartecho pronto. Criamos um novo PR, incluímos o projeto de seu interesse e ... nada resulta disso, fica claro na mensagem que é necessário incluir um projeto relacionado no PR. Não está muito claro por que isso é feito, mas não é difícil atender aos requisitos do ambiente, após o qual o projeto começa a ser montado.

Pnp: na máquina doméstica, usei o comando Importar projeto e todas as inclusões necessárias aconteceram por conta própria. Onde são indicados exatamente projetos relacionados, não sei, deixemos a análise desse aspecto para o futuro.

Nós compilamos, exibimos flash e iniciamos a depuração. Encontramos um fenômeno interessante - a execução passo a passo não é exibida adequadamente quando se considera a biblioteca de trabalho com uma porta serial - custos de otimização. Desativamos a otimização nas configurações do compilador (quais configurações não existem, existem realmente pessoas que as conhecem e, além disso, as utilizam), montamos o projeto novamente - e nada muda. Acontece que apenas esses arquivos são incluídos na árvore do projeto, pelo menos na forma de links. Adicionamos links às fontes da biblioteca e, após a reconstrução, tudo é depurado corretamente (desde que tenhamos a opção de gerar as informações de depuração ativadas).

Pnp: mas encontrei opções para habilitar a verificação de conformidade com o MISRA-C.

Pnp: outra maneira é usar o comando "Limpar ..." na montagem subsequente, por algum motivo, o comando "Criar tudo" não afeta o projeto associado.

Então descobrimos que nem tudo é sempre depurado normalmente, às vezes nos encontramos em áreas de código de máquina para as quais o compilador não encontra a fonte. Como o ambiente de programação nos fornece todos os arquivos necessários para o trabalho - o resultado do pré-processador, o código do montador e a placa do vinculador (você só precisa se lembrar de ativar as opções correspondentes), passamos para o último. Encontramos duas áreas do código do programa - começando em 0x0000. e a partir de 0x1000. (Arquiteturas de 32 bits são boas para todos, mas a escrita de endereços não é o ponto forte). Voltamos à documentação do microcircuito e descobrimos que há uma área de ROM mapeada especificamente para 0x1000. E ela contém a parte interna das bibliotecas. Argumenta-se que o uso de rotinas melhora o desempenho e reduz o consumo em comparação com o espaço de endereço 0x000. Enquanto dominamos o MK, não estamos tão interessados ​​nos últimos parâmetros, mas a conveniência da depuração é crucial. Você pode desativar o uso da ROM (mas para nossos propósitos) configurando a opção NO_ROM para o compilador, o que fazemos e remontamos o projeto.

Pnp: alternar para uma sub-rotina na ROM parece muito engraçado - não há longa transição no sistema de comando, portanto a transição é realizada primeiro com um retorno ao ponto intermediário na área de endereço baixo (0x0000), e já existe o comando de inicialização do PC, cujos parâmetros não são reconhecidos pelo desmontador. Algo em que não consigo acreditar, como se com esses custos indiretos você pudesse ganhar em velocidade, embora por rotinas longas - por que não.

A propósito, uma pergunta interessante é como é geralmente garantido que o conteúdo da ROM corresponda aos códigos-fonte gentilmente fornecidos pela empresa. Eu posso sugerir imediatamente um mecanismo para incorporar funções adicionais (é claro, depuração e serviço) na ROM, que para o usuário - o programador MK será completamente invisível. E, pessoalmente, não tenho dúvidas de que os desenvolvedores do chip também conhecem muitos outros mecanismos que implementam essa funcionalidade, mas encerraremos o ataque de paranóia.

Por outro lado, só posso dar boas-vindas à aparência de um análogo do BIOS, porque, a longo prazo, isso tornará o sonho dos desenvolvedores uma portabilidade real de código entre diferentes famílias MK, com um núcleo em realidade. Também observamos a peculiaridade da implementação da interação com módulos de software "incorporados". Se nas primeiras tentativas de criar um mecanismo semelhante implementado nos modelos TivaC, havia um supervisor de chamada que foi acessado com o número do grupo e o número do ponto de entrada no subprograma, o que causou uma sobrecarga significativa, então aqui a resolução das comunicações está no nível do vinculador devido a nomes duplos de funções e São inseridos saltos longos diretos para sub-rotinas na ROM. Isso é muito mais rápido na execução, mas requer uma recompilação do projeto ao alterar o modelo de uso.

Agora que estamos totalmente preparados para a depuração conveniente, retornamos ao nosso projeto e começamos a depurar silenciosamente o programa com acesso aos códigos-fonte dos módulos (bem, foi o que eu pensei ...), o que nos permitirá formar uma opinião sobre a qualidade desses textos. O projeto em estudo implementa um espelho do canal de comunicação serial e é extremamente conveniente para fins de treinamento. Obviamente, escolhemos a opção usando o RTOS, não vejo o menor motivo para não usá-lo em nossa configuração (muita memória e memória de programa).

Imediatamente, observamos que os códigos-fonte são apresentados em C, geralmente não é muito conveniente, muitas construções de linguagem parecem pesadas em comparação com seus análogos nas vantagens, mas os criadores estavam mais preocupados com a compatibilidade de código do que com o açúcar sintático. Embora seja possível criar uma versão C ++ das bibliotecas, a compilação condicional é conhecida há muito tempo e é usada em todos os lugares, mas isso implica custos adicionais de material. Certamente, a gerência da empresa sabe o que está fazendo, e meus comentários são uma espécie de "análise astuta", mas parece-me que eu também tenho o direito de opinar.

Eu também conheço a abordagem oposta, quando a biblioteca é projetada usando as ferramentas C ++ mais recentes, e quando perguntado o que fazer para os desenvolvedores que usam compiladores que não atendem às especificações mais recentes, a resposta perfeita é atualizar para novas versões ou não nesta biblioteca (eu recomendo fortemente a segunda opção nesses casos). Minha opinião pessoal é que, se realmente queremos que nosso produto seja usado (e a TI o deseja claramente, e não faz da biblioteca o princípio de "me deixar aqui, aqui está um novo tambor para você"), sua abordagem é certamente verdadeira.

O código fonte do programa parece clássico - inicializando o ambiente de hardware e software, criando tarefas e iniciando um sheduler no módulo principal, o texto da tarefa em um módulo de compilação separado. No exemplo em consideração, a tarefa é exatamente uma - mainThread, o objetivo não é totalmente claro a partir do nome e também, o que me confunde um pouco - o nome do arquivo que contém o texto de origem não coincide com o nome da função (uartecho.c - embora o nome esteja falando aqui), bem, sim A pesquisa no ambiente de programação é implementada de maneira padrão (menu de contexto ou F3 no nome da entidade) e não há problema com isso.

O processo de configuração dos parâmetros da tarefa antes do início é bastante esperado:

  1. criar uma estrutura de parâmetros (local, é claro),
  2. dê a ele valores padrão,
  3. definir parâmetros diferentes do padrão e
  4. use a estrutura ao criar a tarefa.

Apesar do tipo de naturalidade dessas operações, não é óbvio para todos os autores de bibliotecas, e vi várias implementações nas quais, por exemplo, não havia o estágio 2, que levou a um comportamento de programa engraçado (para um observador externo, não para um programador). Nesse caso, está tudo bem, a única questão que surgiu é por que os valores padrão não são constantes, provavelmente esse é um legado do passado danado.

PNP: no conhecido FREE-RTOS, uma abordagem ligeiramente diferente é adotada com os parâmetros da tarefa indicados diretamente no corpo da chamada da API da função de criação de tarefas. Os prós e contras dessas abordagens são os seguintes:

  1. + permite que você não especifique explicitamente parâmetros que correspondam aos valores padrão, + não requer que se lembre da ordem dos parâmetros, -mais detalhados, -mais custos de memória, -Você precisa conhecer os parâmetros padrão, -cria um objeto intermediário nomeado
  2. - requer a especificação de todos os parâmetros,-exige lembrar a ordem dos parâmetros, + é mais compacto, + requer menos memória, + não requer objetos intermediários nomeados.

    Existe um terceiro método, defendido pelo autor deste post (no estilo do TURBO), que tem seu próprio conjunto
  3. + permite que você não especifique explicitamente parâmetros que correspondam ao padrão, + não requer que se lembre da ordem dos parâmetros, -múltiplos verbais, custos de memória maiores, -Você precisa conhecer os parâmetros padrão, + trabalha no estilo lambda, + dificulta a implementação de erros padrão +, parece um pouco estranho por causa dos muitos colchetes certos.

Bem, há outra quarta opção, sem quaisquer desvantagens, mas exigindo C ++ não inferior a 14 - lambemos os lábios e passamos.

Iniciamos a depuração, executamos o programa e abrimos uma das duas portas seriais fornecidas pela placa de depuração na janela do terminal fornecida pelo ambiente de programação. Qual das duas portas (uma está depurando, provavelmente a segunda é o usuário, você pode ver seus números no sistema) é difícil dizer com antecedência, às vezes a mais nova, às vezes a mais antiga, pelo menos não muda quando você reconecta a placa, para que possa escrevê-la no quadro. Bem, mais um inconveniente: os terminais abertos não são salvos com o projeto e não são restaurados quando você abre uma sessão de depuração, apesar de não fecharem quando você sai. Verificamos a operação do programa e descobrimos imediatamente mais uma desvantagem - o terminal não pode ser configurado, por exemplo, ele basicamente funciona no estilo Unix com um fechamento / r, perdi o contato com esse minimalismo, embora ninguém nos incomode com o uso de um programa de terminal externo.

Pnp: Observamos mais um recurso da depuração, bem, isso é verdade para qualquer ambiente de desenvolvimento - quando alternamos tarefas com um sheduler, perdemos o foco do foco, os pontos de interrupção nos ajudarão a resolver esse problema.

Para começar, considere o processo de criação de uma instância de uma porta serial - tudo parece padrão aqui, uma estrutura é usada, cujos campos são atribuídos aos parâmetros necessários do objeto. Observe que, nos profissionais, temos a oportunidade, completamente ausente em C, de ocultar completamente toda a inicialização "por baixo do capô", mas eu já expressei possíveis argumentos a favor da segunda solução. Existe uma função para inicializar a estrutura de ajuste, e isso é bom (por mais paradoxal que pareça, essa função não parece obrigatória para os autores de algumas bibliotecas). Nesse ponto da história, a lua de mel termina e a vida comum (conjugal) começa.

Um estudo cuidadoso das fontes mostra que nem tudo é tão bom. Qual é o problema - a função de inicialização copia os valores padrão do objeto que está na região constante em nossa estrutura de controle, e isso é maravilhoso, mas por algum motivo:

  1. o objeto é global, embora seja usado pela única função para inicializar os parâmetros (uma vez uma prática semelhante custa à Toyota uma quantia decente) - bem, é fácil adicionar a diretiva estática;
  2. o objeto de controle é nomeado; em C, não há uma solução bonita para esse problema, ou melhor, existe uma solução com uma cópia anônima e eu a forneci em um longo período de tempo, mas muitos colchetes corretos não permitem chamar essa opção de realmente bonita; além disso, existe uma solução de beleza incrível, mas o que sonhar com um sonho;
  3. todos os campos do objeto são claramente redundantes em profundidade de bits, mesmo os campos de bits (enumerações de dois valores possíveis) são armazenados em palavras de 32 bits;
  4. constantes de modo enumeradas são definidas na forma de define, o que torna impossível verificar no estágio de compilação e necessário em tempo de execução;
  5. repetindo uma seção de um loop infinito em diferentes locais de possíveis falhas, seria muito mais correto fazer um manipulador (nesse caso vazio);
  6. Bem, todas as operações para configurar e iniciar uma tarefa podem (e devem) estar ocultas em uma função ou até em uma macro.

Mas a inicialização do buffer de recebimento é bem feita - usamos memória pré-reservada, nenhuma manipulação do heap, a cadeia de chamadas é um pouco complicada, mas tudo é legível.

Pnp: na janela de depuração, diante de nossos olhos, a pilha de chamadas, tudo é feito como deveria e profundamente - respeito e respeito. A única coisa surpreendente é uma tentativa de ocultar essa janela leva ao fim da sessão de depuração.

Bem, e mais uma decisão inesperada - definir o número possível de objetos na enumeração, para portas seriais e para esta placa de depuração igual a 1, no estilo

typedef enum CC1310_LAUNCHXL_UARTName { CC1310_LAUNCHXL_UART0 = 0, CC1310_LAUNCHXL_UARTCOUNT } CC1310_LAUNCHXL_UARTName; 

Essas soluções são padrão para transferências reais, mas para a descrição de objetos de hardware - e eu não sabia que isso é possível, embora funcione para mim. Terminamos a inicialização do ferro, vamos seguir em frente.

Em uma tarefa em execução, observamos um loop infinito clássico no qual os dados de uma porta serial são lidos pela função
 UART_read(uart, &input, 1); 
e imediatamente enviado de volta por função
 UART_write(uart, &input, 1); 
. Vamos para o primeiro e ver uma tentativa de ler caracteres do buffer de recebimento
 return (handle->fxnTablePtr->readPollingFxn(handle, buffer, size)) 
(como eu odeio essas coisas, mas em C é simplesmente impossível de outra maneira), nos aprofundamos e nos encontramos em UARTCC26XX_read e, a partir disso, entramos na implementação do buffer de anel - uma função
 RingBuf_get(&object->ringBuffer, &readIn) 
. Aqui, a vida comum entra em uma fase aguda.

Eu não queria dizer que não gostei desse módulo em particular (o arquivo ringbuf.c), ele foi escrito de maneira horrível e pessoalmente eu teria expulsado o lugar de uma empresa tão respeitada de autores desta parte com vergonha (você ainda pode me tomar no lugar deles, mas eu tenho medo que o nível salarial de nossos colegas indianos não combina comigo), mas eu provavelmente não sei o quê. Assista suas mãos:

1) o re-roll de ponteiros de leitura / gravação é implementado pelo restante da divisão

 object->tail = (object->tail + 1) % object->length; 

e não há otimizações do compilador ao executar essa operação, como sobrepor uma máscara de bit, já que o comprimento do buffer não é constante. Sim, este MK tem uma operação de divisão de hardware e é bem rápido (escrevi sobre isso), mas ainda assim nunca leva 2 ciclos de clock, como na implementação correta com relançamento honesto (e escrevi sobre isso também),

Pnp: vi recentemente uma descrição da nova arquitetura M7 na implementação e não me lembro de ninguém; portanto, por algum motivo, a divisão de 32 por 32 começou a ser executada em 2-12 ciclos em vez de 2-7. Ou isso é um erro de tradução ou ... nem sei o que pensar.

2) além disso, esse fragmento de código é repetido em mais de um local - macros e linhas para wimps, regra ctrl + C e ctrl + V, o princípio DRY atravessa a floresta,

3) foi implementado um contador completamente redundante de locais de reserva preenchidos, o que implica a seguinte desvantagem,

4) seções críticas em leitura e escrita. Bem, ainda posso acreditar que os autores deste módulo não leem minhas postagens no Habré (embora esse comportamento seja inaceitável para profissionais da área de firmware), mas eles devem estar familiarizados com o Livro do Mustang, pois esse problema é examinado em detalhes,

5) como uma cereja no bolo, foi introduzido um indicador do tamanho máximo do buffer, além disso, com um nome muito distorcido e uma descrição completamente ausente (o último se aplica geralmente a todo o módulo). Não excluo que essa opção pode ser útil para depuração, mas por que arrastá-la para a versão - temos algum ciclo de processador com a RAM?

6) ao mesmo tempo, o processamento de buffer overflow está completamente ausente (há um sinal de retorno -1 sobre essa situação) - mesmo no Arduino, deixaremos de lado a qualidade desse processamento, mas sua ausência é ainda pior. Ou os autores foram inspirados pelo fato conhecido de que quaisquer suposições são verdadeiras em relação a um conjunto relativamente vazio, incluindo o fato de que ele não está vazio?

Em geral, meus comentários são totalmente consistentes com a primeira linha do desmotivador sobre o tópico de revisão de código "10 linhas de código - 10 comentários".

A propósito, o penúltimo das deficiências observadas nos faz pensar em coisas mais globais - mas como implementamos a classe base para poder realizar sua profunda modificação? Proteger todos os campos é uma idéia duvidosa (embora provavelmente seja a única correta), inserir uma chamada de funções amigáveis ​​nos herdeiros é muito parecido com muletas. Se, neste caso em particular, houver uma resposta simples para a questão de introduzir um indicador de plenitude do buffer - uma classe gerada com escrita e leitura sobrepostas e um contador adicional, implemente a leitura sem avançar o buffer (como neste caso) ou substituindo o último caractere colocado (eu vi esse implementação do buffer de anel) você não pode prescindir do acesso aos dados internos da classe pai.

Ao mesmo tempo, não há queixas sobre a implementação de realmente ler da interface serial - a entrada está bloqueando, na ausência de um número suficiente de caracteres no buffer de recebimento, um semáforo é engatilhado e o controle é transferido para o sheduler - tudo é implementado com precisão e corretamente. Pessoalmente, não gosto muito de controlar o equipamento em um procedimento de uso geral, mas isso reduz o aninhamento de procedimentos e o índice de complexidade ciclomática, independentemente do significado.

Vamos agora prestar atenção à transmissão dos dados recebidos para o canal serial, pois ao criar o objeto, ele foi fornecido com apenas um buffer de anel - o receptor. De fato, o buffer interno do hardware é usado para transmitir caracteres e, quando é preenchido, a espera pela prontidão é inserida (pelo menos no modo de operação de bloqueio). Eu não posso me ajudar, para não criticar o estilo das funções correspondentes: 1) por algum motivo, o objeto tem um ponteiro generalizado, que dentro da função constantemente se transforma em um ponteiro para caracteres
 *(unsigned char *)object->writeBuf); 
2) a lógica do trabalho é completamente opaca e um pouco confusa. Mas tudo isso não é tão importante, porque permanece oculto ao usuário e "não afeta a velocidade máxima".

No processo de pesquisa, encontramos mais um recurso - não vemos o código fonte de algumas funções internas no modo de depuração - isso ocorre devido a uma alteração de nomes para diferentes opções de compilação (ROM / NO_ROM). Substitua o arquivo de origem necessário (C: \ Jenkins \ jobs \ FWGroup-DriverLib \ espaço de trabalho \ modules \ output \ cc13xx_cha_2_0_ext_ driver_ib \ bin \ ccs /./../../../ driverlib / uart.c--) Eu falhei (mas realmente não tentei), embora tenha encontrado a fonte (é claro, no arquivo uart.c, obrigado, capitão), felizmente, esse fragmento é simples e é fácil identificar o código do assembler com o código-fonte em C (especialmente se você conhece os recursos da equipe do ITxxx). Não sei como resolver esse problema para bibliotecas com funções complexas, pensaremos quando surgir a necessidade.

E, finalmente, uma pequena observação - estou pronto para acreditar que o hardware da implementação do canal serial para os modelos MK CC13x0 é o mesmo do CC26x0, e a duplicação do conteúdo de um arquivo chamado UARTCC26XX.c --- não pode ser chamada de solução certa, mas a criação de um arquivo de definição intermediária com inclusão Eu gostaria do arquivo de origem, substituindo as funções e o comentário correspondente, pois isso tornaria o programa mais compreensível, e isso sempre deve ser bem-vindo, vout.

Portanto, o caso de teste funciona, aprendemos muito sobre a estrutura interna das bibliotecas padrão, notamos seus pontos fortes e não aspectos tão bons. Na conclusão da revisão, tentaremos encontrar a resposta para a pergunta que o programador geralmente se preocupa no dilema “SO ou não SO” - tempo de mudança de contexto. Duas maneiras são possíveis aqui: 1) a consideração do código fonte é bastante teórica, requer um nível de imersão no assunto que não estou pronto para demonstrar e 2) um experimento prático. Obviamente, o segundo método, diferentemente do primeiro, não fornece resultados absolutamente corretos, mas “a verdade é sempre concreta” e os dados obtidos podem ser considerados adequados se as medições forem organizadas corretamente.

Para começar, para estimar o tempo de comutação, precisamos aprender como avaliar o tempo geral de execução de vários fragmentos de programa. Nesta arquitetura, existe um módulo de depuração, parte do qual é um contador do sistema. As informações sobre este módulo são bastante acessíveis, mas o diabo, como sempre, está oculto nos detalhes. Primeiro, vamos tentar configurar o modo necessário com as alças diretamente através do acesso aos registradores. Nós encontramos rapidamente o bloco de registro CPU_DWT e nele encontramos o contador CYCCNT em si e o registro de controle CTRL com o bit CYCCNTENA. Naturalmente, ou, como dizem, é claro, nada aconteceu e o site da ARM tem uma resposta para a pergunta por que - é necessário ativar o módulo de depuração com o bit TRCENA no registro do DEMCR. Mas o último registro não é tão simples - no bloco DWT não existe, em outros blocos é preguiçoso pesquisar - eles são bastante longos, mas não encontrei nenhuma pesquisa por nome na janela do registro (mas seria bom tê-lo). Entramos na janela de memória, inserimos o endereço do registro (é conhecido por nós a partir da data) (a propósito, por algum motivo, o formato hexadecimal do endereço não é o padrão, você precisa adicionar o prefixo 0x com canetas) e, de repente, vemos uma célula de memória nomeada com o nome CPU_CSC_DEMCR. É engraçado, para dizer o mínimo, por que a empresa renomeou os registros em comparação com os nomes propostos pelo licenciante da arquitetura, provavelmente era necessário. E exatamente, no bloco de registros CPU_CSC, encontramos nosso registro, configuramos o bit desejado nele, retornamos ao contador, ativamos e tudo funcionou.

Pnp: ainda existe uma pesquisa por nome, é chamada (naturalmente) pela combinação Ctrl-F, existe apenas no menu de contexto, mas no habitual é cancelada, peço desculpas aos desenvolvedores.

Imediatamente, noto outra desvantagem da janela de memória - a impressão do conteúdo é interrompida indicando as células nomeadas, o que torna a saída rasgada e não segmentada em 16 (8,32, 64, substitui as necessárias) palavras. Além disso, o formato de saída muda quando a janela é redimensionada. Talvez tudo isso possa ser configurado conforme as necessidades do usuário, mas, com base em minha própria experiência (e de que outra forma devo proceder), declaro que a configuração do formato de saída da janela de exibição de memória não se aplica a soluções intuitivamente óbvias. Sou totalmente a favor de ativar um recurso tão conveniente como exibir áreas de memória nomeadas na janela de visualização; caso contrário, muitos usuários nunca saberiam sobre isso, mas também é preciso ter cuidado com aqueles que desejam desativá-lo conscientemente.

A propósito, eu não abandonaria completamente a possibilidade de criar macros (ou scripts) para trabalhar com o ambiente, porque tinha que fazer essa configuração de registro (para permitir a medição do tempo) todas as vezes após a redefinição do MK, porque considero a correção de código inserindo manipulações de registro para fins de depuração não muito correto. Porém, embora eu nunca tenha encontrado macros, o trabalho com registros pode ser bastante simplificado devido ao fato de que registros individuais (necessários) podem ser incluídos na janela de expressão, facilitando e agilizando significativamente o trabalho com eles.

Para enfatizar que o sentimento do engenheiro pela família MK não se acalmou (caso contrário, discuto aspectos diferentes do ambiente de desenvolvimento), observei que o contador funciona bem - não consegui encontrar ciclos extras em nenhum dos modos de depuração, mas antes disso acontecer pelo menos na série MK, desenvolvida pela LuminaryMicro.

Assim, esboçamos o plano do experimento para determinar o tempo de alternância de contexto - crie uma segunda tarefa que aumentará um determinado contador interno (em um loop infinito), inicie o MC por um certo tempo, encontre a relação entre o contador do sistema e o contador de tarefas. Em seguida, inicie o MK por um tempo semelhante (não necessariamente exatamente o mesmo) e insira 10 caracteres em um ritmo aproximadamente uma vez por segundo. Pode-se esperar que isso resulte em 10 alternando para a tarefa de eco e 10 voltando para a tarefa de contador. Sim, essas alternâncias de contexto serão executadas não de acordo com o cronômetro do sheduler, mas de acordo com o evento, mas isso não deve afetar o tempo total de execução da função investigada; portanto, começamos a implementar o plano, criar a contra-tarefa e iniciá-la.

Aqui encontramos um recurso do RTOS, pelo menos na configuração padrão - ele não é "real": se a tarefa prioritária está constantemente pronta para execução (e a contra-tarefa é essa) e não dá controle ao sheduler (não espera sinais, não adormece, não bloqueado por sinalizadores, etc.); em seguida, nenhuma tarefa de menor prioridade será executada a partir da palavra. Este não é o Linux, no qual vários métodos são usados ​​para garantir que todos recebam quantum, "para que ninguém se ofenda". Esse comportamento é esperado, muitos RTOS leves se comportam assim, mas o problema é mais profundo, pois o gerenciamento não recebe tarefas de igual prioridade com as constantemente preparadas. É por isso que, neste exemplo, coloquei a tarefa de eco, que está em espera, a prioridade é maior que a tarefa de contador constantemente pronta, caso contrário, a última capturará todos os recursos do processador a tempo.

Começamos o experimento, a primeira parte (apenas aguardando o tempo de execução) forneceu os dados sobre a proporção dos contadores 406181 / 58015 = 7 - é bastante esperado. A segunda parte (com 10 caracteres consecutivos por ~ 10 segundos) fornece os resultados 351234k-50167k * 7 = 63k / 20 = 3160 ciclos, o último dígito é o tempo associado ao procedimento de alternância de contexto nos ciclos MK. Pessoalmente, esse valor me parece um pouco maior que o esperado, continuamos pesquisando, parece que ainda existem ações que estragam as estatísticas.

PNP: um erro comum de um experimentador é não avaliar os resultados esperados anteriormente e acreditar no lixo recebido (oi para 737 desenvolvedores).

É óbvio (“sim, bastante óbvio”) que o resultado, além da troca de contexto real, também contém o tempo necessário para executar as operações de leitura de um caractere do buffer e enviá-lo para a porta serial. Menos óbvio é que ele também tem o tempo necessário para processar uma interrupção quando é recebida e colocar o caractere no buffer de recebimento). Como podemos separar um gato da carne - por isso temos um truque complicado - paramos o programa, inserimos 10 caracteres e o iniciamos. Podemos esperar (devemos olhar para a fonte) que uma interrupção na recepção ocorrerá apenas uma vez e imediatamente todos os caracteres serão enviados do buffer de recebimento para o anel, o que significa que veremos menos sobrecarga. Também é fácil determinar o tempo de entrega na porta serial - produziremos cada segundo caractere e resolveremos as 2 equações lineares resultantes com 2 desconhecidas. E é possível e ainda mais simples - não deduzir nada, o que eu fiz.

E aqui estão os resultados de manipulações tão complicadas: tornamos a entrada do pacote e as medidas ausentes diminuem - 2282, desligamos a saída e os custos caem para 1222 - isso é melhor, embora eu estivesse esperando 300 ciclos.

Mas com o tempo de leitura, nada disso pode ser apresentado; ele é dimensionado ao mesmo tempo que o tempo de troca de contexto desejado. A única coisa que posso oferecer é desligar o timer interno no início da inserção do caractere recebido e ligá-lo novamente antes de entrar no modo de espera pelo próximo. Em seguida, dois contadores funcionarão de forma síncrona (com exceção da alternância) e podem ser facilmente determinados. Mas essa abordagem requer uma profunda implementação de programas do sistema nos textos, e ainda o componente de manipulação de interrupções permanecerá. Portanto, proponho limitar-me aos dados já obtidos, o que nos permite afirmar firmemente que o tempo de troca de tarefas no TI-RTOS em questão não excede 1222 ciclos de relógio, que para uma determinada frequência de relógio é de 30 microssegundos.

PNP: ainda muito - contei ciclos de 100: 30 para salvar o contexto, 40 para determinar a tarefa finalizada e 30 para restaurar o contexto, mas temos uma ordem de magnitude maior. Embora a otimização tenha sido desativada agora, ligue –o2 e veja o resultado: não mudou muito - tornou-se 2894 em vez de 3160.

Há outra idéia: se o sistema operacional suportasse alternar tarefas ponto a ponto, você poderia executar duas tarefas com contadores, obter magicamente dados sobre o número de comutadores em um tempo e calcular a perda do contador do sistema, mas devido às peculiaridades do sheduler, sobre o qual eu Como já foi dito, essa abordagem não levará ao sucesso. Embora seja possível outra opção - executar pingue-pongue entre duas tarefas ponto a ponto (ou mesmo ponto a ponto) por meio de um semáforo, é fácil calcular o número de alternâncias de contexto aqui - você precisará experimentá-lo, mas será amanhã.

A pesquisa tradicional no final do post, desta vez, será dedicada não ao nível da apresentação (é claro para qualquer leitor imparcial que ele esteja além de qualquer elogio e supere qualquer expectativa), mas ao tópico do próximo post.

Source: https://habr.com/ru/post/pt451918/


All Articles