Debugging ARM Cortex-M Microcontrollers oleh UART Bagian 2

Pada artikel terakhir , saya berbicara tentang interupsi DebugMon dan register yang terkait dengannya.

Pada artikel ini kita akan menulis implementasi debugger untuk UART.

Bagian tingkat rendah


Di sini dan di sini ada deskripsi struktur permintaan dan tanggapan dari server GDB. Meskipun tampaknya sederhana, kami tidak akan mengimplementasikannya di mikrokontroler karena alasan berikut:

  • Redundansi data besar. Alamat, nilai register, variabel dikodekan sebagai hex-string, yang meningkatkan volume pesan sebanyak 2 kali
  • Memilah dan mengumpulkan pesan akan membutuhkan sumber daya tambahan
  • Pelacakan akhir paket diperlukan baik oleh waktu habis (timer akan sibuk), atau oleh mesin otomatis yang kompleks, yang akan meningkatkan waktu yang dihabiskan dalam interupsi UART

Untuk mendapatkan modul debugging termudah dan tercepat, kami akan menggunakan protokol biner dengan urutan kontrol:

  • 0xAA 0xFF - Mulai dari bingkai
  • 0xAA 0x00 - Akhir bingkai
  • 0xAA 0xA5 - Mengganggu
  • 0xAA 0xAA - Digantikan oleh 0xAA

Untuk memproses urutan ini selama penerimaan, diperlukan otomat dengan 4 status:

  • Menunggu karakter ESC
  • Menunggu karakter kedua dari Mulai urutan frame
  • Penerimaan data
  • Terakhir kali karakter Esc diterima

Tetapi untuk mengirim status, Anda harus sudah 7:

  • Mengirim byte awal dari frame
  • Mengirim byte awal dari frame
  • Mengirim data
  • Mengirimkan Akhir bingkai
  • Mengirim penggantian karakter Esc
  • Mengirim byte Interrupt pertama
  • Mengirim Byte Interupsi Kedua

Mari kita menulis definisi struktur di mana semua variabel modul akan ditempatkan:

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) //   ,         

Keadaan mesin penerima dan transmisi digabungkan menjadi satu variabel karena pekerjaan akan dilakukan dalam mode setengah dupleks. Sekarang Anda dapat menulis automata sendiri dengan penangan interrupt.

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


Semuanya sangat sederhana di sini. Bergantung pada peristiwa yang terjadi, penangan interupsi memanggil mesin penerima atau mesin transmisi. Untuk memverifikasi bahwa semuanya berfungsi, kami menulis penangan paket yang merespons dengan satu 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);
}

Kompilasi, jahit, jalankan. Hasilnya terlihat di layar, itu berhasil.

Pertukaran tes


Selanjutnya, Anda perlu menerapkan analog perintah dari protokol server GDB:

  • membaca memori
  • catatan memori
  • program berhenti
  • eksekusi lanjutan
  • register kernel baca
  • entri register kernel
  • pengaturan breakpoint
  • hapus breakpoint

Perintah akan dikodekan dengan byte data pertama. Kode tim memiliki angka sesuai urutan pelaksanaannya:

  • 2 - baca memori
  • 3 - catatan memori
  • 4 - berhenti
  • 5 - lanjutan
  • 6 - baca case
  • 7 - pasang breakpoint
  • 8 - membersihkan breakpoint
  • 9 - langkah (gagal diterapkan)
  • 10 - daftar entri (tidak diterapkan)

Parameter akan dikirim dalam byte data berikut.

Jawabannya tidak akan berisi nomor perintah, seperti kita sudah tahu tim mana yang dikirim.

Untuk mencegah modul menyebabkan pengecualian BusFault selama operasi baca / tulis, Anda harus menutupinya saat digunakan pada M3 atau lebih tinggi, atau menulis penangan HardFault untuk M0.

Memcpy aman
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;
}


Pengaturan breakpoint diimplementasikan dengan mencari FP_COMP register tidak aktif pertama.

Memasang Kode Breakpoints
	
  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;


Pembersihan dilakukan dengan mencari breakpoint yang ditetapkan. Menghentikan eksekusi akan menetapkan breakpoint pada PC saat ini. Ketika keluar dari interupsi UART, kernel segera memasuki DebugMon_Handler.

Handler DebugMon sendiri sangat sederhana:

  • 1. Bendera untuk menghentikan eksekusi diatur.
  • 2. Semua breakpoint yang ditetapkan dihapus.
  • 3. Menunggu penyelesaian pengiriman respons terhadap perintah di uart (jika dia tidak berhasil pergi)
  • 4. Pengiriman urutan Interrupt dimulai
  • 5. Dalam loop, handler dari mesin pengirim dan penerima dipanggil sampai bendera berhenti diturunkan.

Kode Penangan 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(); 
  }
}


Baca register kernel dari C-Syshny ketika tugas bermasalah, jadi saya menulis ulang beberapa kode pada ASM. Hasilnya adalah DebugMon_Handler, atau interrupt handler UART, atau mesin tidak menggunakan stack. Ini menyederhanakan definisi nilai register kernel.

Server Gdb


Bagian mikrokontroler dari debugger berfungsi, sekarang mari kita menulis tautan antara IDE dan modul kami.

Dari awal, menulis server debug tidak masuk akal, jadi mari kita ambil yang sudah selesai sebagai dasar. Karena saya memiliki pengalaman paling banyak dalam mengembangkan program di .net, saya mengambil proyek ini sebagai dasar dan menulis ulang untuk persyaratan lain. Akan lebih benar untuk menambahkan dukungan untuk antarmuka baru di OpenOCD, tetapi itu akan membutuhkan lebih banyak waktu.

Saat startup, program ini menanyakan port COM mana untuk bekerja, kemudian mulai mendengarkan pada port TCP 3333 dan menunggu klien GDB untuk terhubung.

Semua perintah protokol GDB diterjemahkan ke dalam protokol biner.

Akibatnya, implementasi debugging UART yang bisa diterapkan dirilis.

Hasil akhir


Kesimpulan


Ternyata debugging controller itu sendiri bukanlah sesuatu yang super rumit.
Secara teoritis, dengan menempatkan modul ini di bagian memori yang terpisah, modul ini juga dapat digunakan untuk mem-flash controller.

File sumber diposting di GitHub untuk studi umum

bagian mikrokontroler dari
server GDB

All Articles