En el último artículo, hablé sobre la interrupción de DebugMon y los registros asociados con ella.En este artículo escribiremos la implementación del depurador para UART.Parte de bajo nivel
Aquí y aquí hay una descripción de la estructura de solicitudes y respuestas del servidor GDB. Aunque parezca simple, no lo implementaremos en el microcontrolador por las siguientes razones:- Gran redundancia de datos. Las direcciones, los valores de los registros y las variables se codifican como una cadena hexadecimal, lo que aumenta el volumen del mensaje 2 veces
- Analizar y recopilar mensajes requerirá recursos adicionales
- Es necesario realizar un seguimiento del final del paquete ya sea por tiempo de espera (el temporizador estará ocupado) o por una máquina automática compleja, lo que aumentará el tiempo pasado en la interrupción UART
Para obtener el módulo de depuración más fácil y rápido, utilizaremos un protocolo binario con secuencias de control:- 0xAA 0xFF - Inicio de trama
- 0xAA 0x00 - Fin del marco
- 0xAA 0xA5 - Interrupción
- 0xAA 0xAA - Reemplazado por 0xAA
Para procesar estas secuencias durante la recepción, se requiere un autómata con 4 estados:- Esperando al personaje ESC
- Esperando el segundo carácter de la secuencia de inicio de trama
- Recepción de datos
- La última vez que se aceptó un personaje Esc
Pero para enviar estados, ya necesita 7:- Envío del primer byte Inicio de trama
- Enviar un segundo byte Inicio de trama
- Enviando datos
- Enviar fin de marco
- Envío de reemplazo de personaje Esc
- Enviar el primer byte de interrupción
- Envío del segundo byte de interrupción
Escribamos la definición de la estructura dentro de la cual se ubicarán todas las variables del módulo: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)
Los estados de las máquinas receptoras y transmisoras se combinan en una variable, ya que el trabajo se realizará en modo semidúplex. Ahora puede escribir autómatas con un controlador de interrupciones.Controlador 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;
}
}
Aquí todo es bastante simple. Dependiendo del evento que ocurrió, el controlador de interrupción llama a una máquina receptora o una máquina de transmisión. Para verificar que todo funciona, escribimos un controlador de paquetes que responde con un 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, coser, correr. El resultado es visible en la pantalla, funcionó.A continuación, debe implementar comandos análogos del protocolo del servidor GDB:- lectura de memoria
- registro de memoria
- parada del programa
- ejecución continua
- registro del núcleo leído
- entrada de registro de kernel
- establecer un punto de interrupción
- eliminar punto de interrupción
El comando se codificará con el primer byte de datos. Los códigos de los equipos tienen números en el orden de su implementación:- 2 - leer memoria
- 3 - registro de memoria
- 4 - parada
- 5 - continuación
- 6 - leer caso
- 7 - instalar punto de interrupción
- 8 - despejar el punto de interrupción
- 9 - paso (no se pudo implementar)
- 10 - registro de entrada (no implementado)
Los parámetros se transmitirán en los siguientes bytes de datos.La respuesta no contendrá el número de comando, como Ya sabemos qué equipo envió.Para evitar que el módulo genere excepciones de BusFault durante las operaciones de lectura / escritura, debe enmascararlo cuando se usa en M3 o superior, o escribir un controlador HardFault para 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;
}
La configuración del punto de interrupción se implementa buscando el primer registro inactivo FP_COMP.Código de instalación de puntos de interrupción
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;
La limpieza se realiza buscando el punto de interrupción establecido. La detención de la ejecución establece un punto de interrupción en la PC actual. Al salir de la interrupción UART, el núcleo ingresa inmediatamente a DebugMon_Handler.El controlador DebugMon en sí es muy simple:- 1. Se establece la bandera para detener la ejecución.
- 2. Todos los puntos de interrupción establecidos se borran.
- 3. Esperando la finalización del envío de una respuesta al comando en uart (si no tuvo tiempo de irse)
- 4. Comienza el envío de la secuencia de interrupción
- 5. En el bucle, se llama a los manejadores de las máquinas de transmisión y recepción hasta que se baja la bandera de detención.
Código de controlador 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();
}
}
Leer los registros del kernel de C-Syshny cuando la tarea es problemática, por lo que reescribí parte del código en ASM. El resultado es que ni DebugMon_Handler, ni el controlador de interrupciones UART, ni las máquinas usan la pila. Esto simplificó la definición de los valores de registro del núcleo.Servidor gdb
La parte del microcontrolador del depurador funciona, ahora escribamos el enlace entre el IDE y nuestro módulo.Desde cero, escribir un servidor de depuración no tiene sentido, así que tomemos uno listo como base. Como tengo la mayor experiencia en el desarrollo de programas en .net, tomé este proyecto como base y lo reescribí para otros requisitos. Sería más correcto agregar soporte para la nueva interfaz en OpenOCD, pero tomaría más tiempo.Al inicio, el programa pregunta con qué puerto COM trabajar, luego comienza a escuchar en el puerto TCP 3333 y espera a que se conecte el cliente GDB.Todos los comandos del protocolo GDB se traducen en un protocolo binario.Como resultado, se lanzó una implementación de depuración UART viable.Conclusión
Resultó que depurar el controlador en sí no es algo súper complicado.Teóricamente, al colocar este módulo en una sección de memoria separada, también se puede usar para actualizar el controlador.Los archivos fuente se publicaron en GitHub para el estudio general de laparte del microcontrolador delservidor GDB