通过UART调试ARM Cortex-M微控制器第2部分

在上一篇文章中,我讨论了DebugMon中断及其相关的寄存器。

在本文中,我们将编写UART调试器的实现。

低层部分


这里这里描述了GDB服务器的请求和响应的结构。尽管看起来很简单,但由于以下原因,我们将不会在微控制器中实现它:

  • 大数据冗余。地址,寄存器的值,变量被编码为十六进制字符串,这使消息量增加了2倍
  • 解析和收集消息将占用更多资源
  • 超时(定时器忙)或复杂的自动计算机都需要跟踪数据包的结尾,这将增加在UART中断中花费的时间

为了获得最简单,最快的调试模块,我们将使用带有控制序列的二进制协议:

  • 0xAA 0xFF-帧开始
  • 0xAA 0x00-帧结束
  • 0xAA 0xA5-中断
  • 0xAA 0xAA-替换为0xAA

要在接收期间处理这些序列,需要具有4种状态的自动机:

  • 等待ESC字符
  • 等待帧开始序列的第二个字符
  • 数据接收
  • 上次接受Esc字符

但是要发送状态,您已经需要7:

  • 发送第一个字节开始帧
  • 发送第二个字节帧的开始
  • 传送资料
  • 提交帧结束
  • 发送Esc字符替换
  • 发送第一个中断字节
  • 发送第二个中断字节

让我们写出所有模块变量都将位于其中的结构定义:

typedef struct 
{    
  // disable receive data
  unsigned tx:1;
  // program stopped
  unsigned StopProgramm:1;
  union {
    enum rx_state_e 
    {
      rxWaitS = 0, // wait Esc symbol
      rxWaitC = 1, // wait Start of frame
      rxReceive = 2, // receiving
      rxEsc = 3, // Esc received
    } rx_state;
    enum tx_state_e 
    {
      txSendS = 0, // send first byte of Start of frame
      txSendC = 1, // send second byte
      txSendN = 2, // send byte of data
      txEsc = 3,   // send escaped byte of data
      txEnd = 4,   // send End of frame
      txSendS2 = 5,// send first byte of Interrupt
      txBrk = 6,   // send second byte
    } tx_state;
  };
  uint8_t pos; // receive/send position
  uint8_t buf[128]; // offset = 3
  uint8_t txCnt; // size of send data
} dbg_t;
#define dbgG ((dbg_t*)DBG_ADDR) //   ,         

由于工作将以半双工模式进行,因此接收和发送机的状态被组合为一个变量。现在,您可以使用中断处理程序自己编写自动机。

UART处理程序
void 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; // debug vars pointer
  
  if (dbg->tx) // use half duplex mode
    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;
  }
}


这里的一切都很简单。根据发生的事件,中断处理程序将调用接收计算机或传输计算机。为了验证一切正常,我们编写了一个响应一个字节的数据包处理程序:

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

编译,缝制,运行。结果在屏幕上可见,可以正常工作。

测试交流


接下来,您需要从GDB服务器协议实现命令类似物:

  • 记忆读取
  • 记忆记录
  • 程序停止
  • 继续执行
  • 内核寄存器读取
  • 内核寄存器输入
  • 设置断点
  • 删除断点

该命令将使用数据的第一个字节进行编码。团队代码按执行顺序编号:

  • 2-读取内存
  • 3-记忆记录
  • 4-停止
  • 5-继续
  • 6-阅读案例
  • 7-安装断点
  • 8-清除断点
  • 9-步骤(无法实施)
  • 10-寄存器输入(未实现)

参数将与以下数据字节一起发送。

答案将不包含命令编号,因为 我们已经知道哪个团队发送了。

为了防止模块在读/写操作期间引发BusFault异常,在M3或更高版本上使用时必须屏蔽它,或者为M0编写HardFault处理程序。

安全记忆
int memcpySafe(uint8_t* to,uint8_t* from, int len)
{
    /* Cortex-M3, Cortex-M4, Cortex-M4F, Cortex-M7 are supported */
    static const uint32_t BFARVALID_MASK = (0x80 << SCB_CFSR_BUSFAULTSR_Pos);
    int cnt = 0;

    /* Clear BFARVALID flag by writing 1 to it */
    SCB->CFSR |= BFARVALID_MASK;

    /* Ignore BusFault by enabling BFHFNMIGN and disabling interrupts */
    uint32_t mask = __get_FAULTMASK();
    __disable_fault_irq();
    SCB->CCR |= SCB_CCR_BFHFNMIGN_Msk;

    while ((cnt<len))
    {
      *(to++) = *(from++);
      cnt++;
    }

    /* Reenable BusFault by clearing  BFHFNMIGN */
    SCB->CCR &= ~SCB_CCR_BFHFNMIGN_Msk;
    __set_FAULTMASK(mask);

    return cnt;
}


通过搜索第一个非活动寄存器FP_COMP来实现断点设置。

代码安装断点
	
  dbg->pos = 0; //  -    0
    addr = ((*(uint32_t*)(&dbg->buf[1])))|1; //    FP_COMP
    for (tmp = 0;tmp<8;tmp++) //      breakpoint 
      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;


通过搜索设置的断点来完成清洁。停止执行会在当前PC上设置断点。退出UART中断时,内核立即进入DebugMon_Handler。

DebugMon处理程序本身非常简单:

  • 1.设置停止执行的标志。
  • 2.清除所有设置的断点。
  • 3.等待发送对uart中命令的响应的完成(如果没有时间执行)
  • 4.开始发送中断序列
  • 5.在循环中,将调用发送和接收计算机的处理程序,直到降低停止标志为止。

DebugMon处理程序代码
void DebugMon_Handler(void)
{
  dbgG->StopProgramm = 1; //   
  
  for (int i=0;i<NUMOFBKPTS;i++) //  breakpoint
    FP->FP_COMP[i] = 0;
  
  while (USART6->CR1 & USART_CR1_TXEIE) //    
    if ((USART6->ISR & USART_ISR_TXE) != 0U)
      txCb();

  
  dbgG->tx_state = txSendS2; //   Interrupt 
  dbgG->tx = 1;
  SET_BIT(USART6->CR1, USART_CR1_TXEIE);

  while (dbgG->StopProgramm) //       
  {
  	//   UART  
    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(); 
  }
}


当任务有问题时,请从C-Syshny读取内核寄存器,因此我在ASM上重写了一些代码。结果是DebugMon_Handler,UART中断处理程序和计算机都没有使用堆栈。这简化了内核寄存器值的定义。

Gdb服务器


调试器的微控制器部分正常工作,现在让我们编写IDE与我们模块之间的链接。

从头开始,编写调试服务器没有任何意义,因此让我们以现成的服务器为基础。由于我在.net上开发程序的经验最丰富,因此我以项目为基础并将其重写为其他要求。在OpenOCD中添加对新接口的支持会更正确,但是会花费更多时间。

在启动时,程序会询问要使用哪个COM端口,然后开始侦听TCP端口3333,并等待GDB客户端连接。

所有GDB协议命令都转换为二进制协议。

结果,发布了可行的UART调试实现。

最后结果


结论


事实证明,调试控制器本身并不是一件非常复杂的事情。
从理论上讲,通过将该模块放置在单独的内存部分中,它也可以用于刷新控制器。

源文件发布在GitHub上,用于GDB服务器

微控制器部分的常规研究。

All Articles