在上一篇文章中,我讨论了DebugMon中断及其相关的寄存器。在本文中,我们将编写UART调试器的实现。低层部分
这里和这里描述了GDB服务器的请求和响应的结构。尽管看起来很简单,但由于以下原因,我们将不会在微控制器中实现它:- 大数据冗余。地址,寄存器的值,变量被编码为十六进制字符串,这使消息量增加了2倍
- 解析和收集消息将占用更多资源
- 超时(定时器忙)或复杂的自动计算机都需要跟踪数据包的结尾,这将增加在UART中断中花费的时间
为了获得最简单,最快的调试模块,我们将使用带有控制序列的二进制协议:- 0xAA 0xFF-帧开始
- 0xAA 0x00-帧结束
- 0xAA 0xA5-中断
- 0xAA 0xAA-替换为0xAA
要在接收期间处理这些序列,需要具有4种状态的自动机:- 等待ESC字符
- 等待帧开始序列的第二个字符
- 数据接收
- 上次接受Esc字符
但是要发送状态,您已经需要7:- 发送第一个字节开始帧
- 发送第二个字节帧的开始
- 传送资料
- 提交帧结束
- 发送Esc字符替换
- 发送第一个中断字节
- 发送第二个中断字节
让我们写出所有模块变量都将位于其中的结构定义: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)
由于工作将以半双工模式进行,因此接收和发送机的状态被组合为一个变量。现在,您可以使用中断处理程序自己编写自动机。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;
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;
}
}
这里的一切都很简单。根据发生的事件,中断处理程序将调用接收计算机或传输计算机。为了验证一切正常,我们编写了一个响应一个字节的数据包处理程序: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)
{
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;
}
通过搜索第一个非活动寄存器FP_COMP来实现断点设置。代码安装断点
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;
通过搜索设置的断点来完成清洁。停止执行会在当前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++)
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();
}
}
当任务有问题时,请从C-Syshny读取内核寄存器,因此我在ASM上重写了一些代码。结果是DebugMon_Handler,UART中断处理程序和计算机都没有使用堆栈。这简化了内核寄存器值的定义。Gdb服务器
调试器的微控制器部分正常工作,现在让我们编写IDE与我们模块之间的链接。从头开始,编写调试服务器没有任何意义,因此让我们以现成的服务器为基础。由于我在.net上开发程序的经验最丰富,因此我以该项目为基础并将其重写为其他要求。在OpenOCD中添加对新接口的支持会更正确,但是会花费更多时间。在启动时,程序会询问要使用哪个COM端口,然后开始侦听TCP端口3333,并等待GDB客户端连接。所有GDB协议命令都转换为二进制协议。结果,发布了可行的UART调试实现。结论
事实证明,调试控制器本身并不是一件非常复杂的事情。从理论上讲,通过将该模块放置在单独的内存部分中,它也可以用于刷新控制器。源文件发布在GitHub上,用于GDB服务器的微控制器部分的常规研究。