No último artigo, falei sobre a interrupção do DebugMon e os registros associados a ela.Neste artigo, escreveremos a implementação do depurador para UART.Peça de baixo nível
Aqui e aqui, há uma descrição da estrutura de solicitações e respostas do servidor GDB. Embora pareça simples, não o implementaremos no microcontrolador pelos seguintes motivos:- Redundância de big data. Endereços, valores de registradores, variáveis são codificadas como uma sequência hexadecimal, o que aumenta o volume da mensagem em 2 vezes
- A análise e a coleta de mensagens exigirão recursos adicionais
- O rastreamento do final do pacote é necessário por tempo limite (o cronômetro estará ocupado) ou por uma máquina automática complexa, que aumentará o tempo gasto na interrupção do UART
Para obter o módulo de depuração mais fácil e rápido, usaremos um protocolo binário com sequências de controle:- 0xAA 0xFF - Início do quadro
- 0xAA 0x00 - Fim do quadro
- 0xAA 0xA5 - Interrupção
- 0xAA 0xAA - Substituído por 0xAA
Para processar essas seqüências durante a recepção, é necessário um autômato com 4 estados:- Aguardando o caractere ESC
- Aguardando o segundo caractere do início da sequência de quadros
- Recepção de dados
- A última vez que um caractere Esc foi aceito
Mas para enviar estados, você já precisa do 7:- Enviando o primeiro byte Início do quadro
- Enviando um segundo byte Início do quadro
- Enviando dados
- Enviando Fim do quadro
- Enviando substituição de caracteres Esc
- Enviando o primeiro byte de interrupção
- Enviando segundo byte de interrupção
Vamos escrever a definição da estrutura dentro da qual todas as variáveis do módulo estarão localizadas:typedef struct
{
unsigned tx:1;
unsigned StopProgramm:1;
union {
enum rx_state_e
{
rxWaitS = 0,
rxWaitC = 1,
rxReceive = 2,
rxEsc = 3,
} rx_state;
enum tx_state_e
{
txSendS = 0,
txSendC = 1,
txSendN = 2,
txEsc = 3,
txEnd = 4,
txSendS2 = 5,
txBrk = 6,
} tx_state;
};
uint8_t pos;
uint8_t buf[128];
uint8_t txCnt;
} dbg_t;
#define dbgG ((dbg_t*)DBG_ADDR)
Os estados das máquinas de recebimento e transmissão são combinados em uma variável, pois o trabalho será realizado no modo half duplex. Agora você pode escrever autômatos com um manipulador de interrupções.Manipulador UARTvoid USART6_IRQHandler(void)
{
if (((USART6->ISR & USART_ISR_RXNE) != 0U)
&& ((USART6->CR1 & USART_CR1_RXNEIE) != 0U))
{
rxCb(USART6->RDR);
return;
}
if (((USART6->ISR & USART_ISR_TXE) != 0U)
&& ((USART6->CR1 & USART_CR1_TXEIE) != 0U))
{
txCb();
return;
}
}
void rxCb(uint8_t byte)
{
dbg_t* dbg = dbgG;
if (dbg->tx)
return;
switch(dbg->rx_state)
{
default:
case rxWaitS:
if (byte==0xAA)
dbg->rx_state = rxWaitC;
break;
case rxWaitC:
if (byte == 0xFF)
dbg->rx_state = rxReceive;
else
dbg->rx_state = rxWaitS;
dbg->pos = 0;
break;
case rxReceive:
if (byte == 0xAA)
dbg->rx_state = rxEsc;
else
dbg->buf[dbg->pos++] = byte;
break;
case rxEsc:
if (byte == 0xAA)
{
dbg->buf[dbg->pos++] = byte;
dbg->rx_state = rxReceive;
}
else if (byte == 0x00)
{
parseAnswer();
}
else
dbg->rx_state = rxWaitS;
}
}
void txCb()
{
dbg_t* dbg = dbgG;
switch (dbg->tx_state)
{
case txSendS:
USART6->TDR = 0xAA;
dbg->tx_state = txSendC;
break;
case txSendC:
USART6->TDR = 0xFF;
dbg->tx_state = txSendN;
break;
case txSendN:
if (dbg->txCnt>=dbg->pos)
{
USART6->TDR = 0xAA;
dbg->tx_state = txEnd;
break;
}
if (dbg->buf[dbg->txCnt]==0xAA)
{
USART6->TDR = 0xAA;
dbg->tx_state = txEsc;
break;
}
USART6->TDR = dbg->buf[dbg->txCnt++];
break;
case txEsc:
USART6->TDR = 0xAA;
dbg->txCnt++;
dbg->tx_state = txSendN;
break;
case txEnd:
USART6->TDR = 0x00;
dbg->rx_state = rxWaitS;
dbg->tx = 0;
CLEAR_BIT(USART6->CR1, USART_CR1_TXEIE);
break;
case txSendS2:
USART6->TDR = 0xAA;
dbg->tx_state = txBrk;
break;
case txBrk:
USART6->TDR = 0xA5;
dbg->rx_state = rxWaitS;
dbg->tx = 0;
CLEAR_BIT(USART6->CR1, USART_CR1_TXEIE);
break;
}
}
Tudo é bem simples aqui. Dependendo do evento que ocorreu, o manipulador de interrupção chama uma máquina receptora ou uma máquina de transmissão. Para verificar se tudo funciona, escrevemos um manipulador de pacotes que responde com um byte:void parseAnswer()
{
dbg_t* dbg = dbgG;
dbg->pos = 1;
dbg->buf[0] = 0x33;
dbg->txCnt = 0;
dbg->tx = 1;
dbg->tx_state = txSendS;
SET_BIT(USART6->CR1, USART_CR1_TXEIE);
}
Compilar, costurar, correr. O resultado é visível na tela, funcionou.Em seguida, você precisa implementar os análogos de comando do protocolo do servidor GDB:- leitura de memória
- registro de memória
- parada do programa
- execução continuada
- leitura do registro do kernel
- entrada de registro do kernel
- definindo um ponto de interrupção
- excluir ponto de interrupção
O comando será codificado com o primeiro byte de dados. Os códigos das equipes têm números na ordem de sua implementação:- 2 - ler memória
- 3 - registro de memória
- 4 - parar
- 5 - continuação
- 6 - ler caso
- 7 - instalar ponto de interrupção
- 8 - ponto de interrupção de limpeza
- 9 - etapa (falha ao implementar)
- 10 - entrada de registro (não implementada)
Os parâmetros serão transmitidos nos seguintes bytes de dados.A resposta não conterá o número do comando, como já sabemos qual equipe enviou.Para impedir que o módulo cause exceções do BusFault durante operações de leitura / gravação, você deve mascará-lo quando usado no M3 ou superior, ou gravar um manipulador do HardFault para o M0.Memcpy seguroint memcpySafe(uint8_t* to,uint8_t* from, int len)
{
static const uint32_t BFARVALID_MASK = (0x80 << SCB_CFSR_BUSFAULTSR_Pos);
int cnt = 0;
SCB->CFSR |= BFARVALID_MASK;
uint32_t mask = __get_FAULTMASK();
__disable_fault_irq();
SCB->CCR |= SCB_CCR_BFHFNMIGN_Msk;
while ((cnt<len))
{
*(to++) = *(from++);
cnt++;
}
SCB->CCR &= ~SCB_CCR_BFHFNMIGN_Msk;
__set_FAULTMASK(mask);
return cnt;
}
A configuração do ponto de interrupção é implementada procurando o primeiro registro inativo FP_COMP.Código de instalação de pontos de interrupção
dbg->pos = 0;
addr = ((*(uint32_t*)(&dbg->buf[1])))|1;
for (tmp = 0;tmp<8;tmp++)
if (FP->FP_COMP[tmp] == addr)
break;
if (tmp!=8)
break;
for (tmp=0;tmp<NUMOFBKPTS;tmp++)
if (FP->FP_COMP[tmp]==0)
{
FP->FP_COMP[tmp] = addr;
break;
}
break;
A limpeza é feita procurando o ponto de interrupção definido. Parar a execução define o ponto de interrupção no PC atual. Ao sair da interrupção do UART, o kernel entra imediatamente no DebugMon_Handler.O próprio manipulador DebugMon é muito simples:- 1. O sinalizador para parar a execução está definido.
- 2. Todos os pontos de interrupção definidos são limpos.
- 3. Aguardando a conclusão do envio de uma resposta ao comando em uart (se não tiver tempo para ir)
- 4. O envio da sequência de interrupção começa
- 5. No loop, os manipuladores das máquinas de transmissão e recepção são chamados até que o sinalizador de parada seja abaixado.
Código do manipulador DebugMonvoid DebugMon_Handler(void)
{
dbgG->StopProgramm = 1;
for (int i=0;i<NUMOFBKPTS;i++)
FP->FP_COMP[i] = 0;
while (USART6->CR1 & USART_CR1_TXEIE)
if ((USART6->ISR & USART_ISR_TXE) != 0U)
txCb();
dbgG->tx_state = txSendS2;
dbgG->tx = 1;
SET_BIT(USART6->CR1, USART_CR1_TXEIE);
while (dbgG->StopProgramm)
{
if (((USART6->ISR & USART_ISR_RXNE) != 0U)
&& ((USART6->CR1 & USART_CR1_RXNEIE) != 0U))
rxCb(USART6->RDR);
if (((USART6->ISR & USART_ISR_TXE) != 0U)
&& ((USART6->CR1 & USART_CR1_TXEIE) != 0U))
txCb();
}
}
Leia os registros do kernel do C-Syshny quando a tarefa for problemática, então reescrevi parte do código no ASM. O resultado é que nem DebugMon_Handler, nem o manipulador de interrupções UART, nem as máquinas usam a pilha. Isso simplificou a definição de valores de registro do kernel.Servidor Gdb
A parte do microcontrolador do depurador funciona, agora vamos escrever o link entre o IDE e nosso módulo.Do zero, escrever um servidor de depuração não faz sentido; portanto, tomemos um já pronto como base. Como tenho mais experiência no desenvolvimento de programas em .net, tomei esse projeto como base e o reescrevi para outros requisitos. Seria mais correto adicionar suporte para a nova interface no OpenOCD, mas levaria mais tempo.Na inicialização, o programa pergunta com qual porta COM trabalhar, depois começa a escutar na porta TCP 3333 e aguarda a conexão do cliente GDB.Todos os comandos do protocolo GDB são convertidos em um protocolo binário.Como resultado, uma implementação de depuração UART viável foi lançada.Conclusão
Descobriu-se que a depuração do próprio controlador não é algo super complicado.Teoricamente, colocando este módulo em uma seção de memória separada, ele também pode ser usado para piscar o controlador.Os arquivos de origem foram postados no GitHub para o estudo geral daparte do microcontrolador doservidor GDB