Дебаг на CH32V00x та SDI Print

Зазвичай для відлагодження коду я використовую логування через UART TX. Проте іноді цей спосіб недоступний: апаратний UART може бути зайнятий іншою периферією, або ж під час розводки плати лінію TX просто не вивели, бо вона здавалася непотрібною. Але відлагоджувати прошивку якось треба.

Для мікроконтролерів серії CH32V існує чудова альтернатива UART - механізм SDI Print (Serial Debug Interface). Він дозволяє відправляти лог-повідомлення через єдиний дріт програмування та читати їх на ПК.

На апаратному рівні для передачі даних використовуються два 32-бітні регістри:

#define DEBUG_DATA0_ADDRESS  ((volatile uint32_t*)0xE00000F4)
#define DEBUG_DATA1_ADDRESS  ((volatile uint32_t*)0xE00000F8)

Спосіб взаємодії з цими регістрами кардинально відрізняється залежно від обраного фреймворку: офіційного від WCH (noneos-sdk) чи мінімалістичного ch32fun.

У цій інструкції я опишу, як налаштувати логування саме для офіційного фреймворку від WCH.

У коді програми, замість стандартної ініціалізації UART, потрібно увімкнути SDI Print викликом функції SDI_Printf_Enable():

#include <ch32v00x.h>
#include <debug.h>

void NMI_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void HardFault_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));

int main(void) {
    SystemCoreClockUpdate();
    Delay_Init();
    SDI_Printf_Enable();

    uint16_t counter = 0;
    while (1) {
        printf("Hello from printf! %d\n", counter);
        counter++;
        Delay_Ms(1000);
    }
    return 0;
}

void NMI_Handler(void) {}
void HardFault_Handler(void)
{
    while (1)
    {
    }
}

Але просто викликати функцію в коді недостатньо. Щоб стандартний printf почав перенаправляти дані у відлагоджувальні регістри замість UART, потрібно правильно сконфігурувати збірку за допомогою макросів препроцесора:

  • DEBUG=0 — відключає стандартне логування через UART.

  • SDI_PRINT=SDI_PR_OPEN — вмикає алгоритм “пакування” даних у регістри DATA0 та DATA1 (детальніше про те, як це працює “під капотом”, можна підглянути у файлі debug.c фреймворку, функція _write() ).

Оскільки я використовую PlatformIO, найзручніше задати ці прапорці безпосередньо у конфігураційному файлі platformio.ini:

...
build_flags = -DDEBUG=0 -DSDI_PRINT=SDI_PR_OPEN

Після цього компілюємо та заливаємо прошивку на мікроконтролер.

Звичанйо я одразу після цього спробував minichlink -T він мав би запустити термінал та показати вивід, але з прошивкою що використовує офіційний wch фремворк це не спрацювало, і термінал залишається порожнім. Отже відкладаємо minichlink вбік — цей інструмент заточений переважно під взаємодію з прошивками на ch32fun.

Натомість ми використаємо сучаснішу утиліту — wlink. Її можна легко встановити за допомогою пакетного менеджера cargo (якщо у вас встановлений Rust):

cargo install wlink

Поекспериментувавши з wlink, я знайшов потрібну команду для активації нашого відлагодження:

$ wlink sdi-print enable
19:15:14 [INFO] Connected to WCH-Link v2.21(v41) (WCH-LinkE-CH32V305)
19:15:14 [INFO] Attached chip: CH32V003 [CH32V003A4M6] (ChipID: 0x00320510)
19:15:14 [INFO] Enabling SDI print
19:15:14 [INFO] Now you can connect to the WCH-Link serial port

І ось тут відбувається найцікавіше! Програматор WCH-LinkE має вбудований USB-UART перехідник, яким я часто користуюся. Виконана команда дає вказівку програматору перехоплювати дані зі шлейфа SDI та апаратно перенаправляти їх у свій віртуальний COM-порт.

Тепер, якщо відкрити /dev/ttyACM0 (або відповідний порт у вашій ОС) у Serial Monitor від PlatformIO чи будь-якому іншому терміналі, ми нарешті побачимо наш вивід:

--- Terminal on /dev/ttyACM0 | 9600 8-N-1
--- Available filters and text transformations: debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at https://bit.ly/pio-monitor-filters
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H
Hello from printf! 0
Hello from printf! 1
Hello from printf! 2
Hello from printf! 3

Важливий нюанс: Перемикати WCH-LinkE в режим SDI Print (командою wlink sdi-print enable) потрібно кожного разу після прошивки мікроконтролера або перепідключення USB, оскільки програматор скидає цей стан.

P.S. Важливе зауваження щодо економії пам’яті

Не використовуйте printf без нагальної потреби — це надзвичайно “важка” функція, яка з’їдає доволі багато дорогоцінної Flash-пам’яті (а у CH32V003 її всього 16 КБ). Оскільки вона тягне за собою під капотом весь механізм розбору форматів (%d, %s, %x тощо), її виклик обійдеться вам у кілька кілобайт.

Ось наочний приклад. Якщо використати стандартний printf:

    char *msg = "Hello from printf!\n";
    uint8_t msg_len = strlen(msg);
    printf(msg);

Компілятор покаже таке використання пам’яті:

Flash: [===       ]  27.6% (used 4520 bytes from 16384 bytes)

А якщо для відлагодження вам потрібно виводити лише звичайні текстові повідомлення без форматування змінних, краще викликати низькорівневу функцію _write безпосередньо:

    char *msg = "Hello from printf!\n";
    uint8_t msg_len = strlen(msg);
    _write(0, msg, msg_len);

У цьому випадку споживання ресурсів значно впаде:

Flash: [=         ]   8.9% (used 1456 bytes from 16384 bytes)

Різниця у 3 кілобайти для мікроконтролера такого класу — це величезний обсяг.

Коментарі

Популярні дописи з цього блогу

Підключення поворотного енкодера до CH32V: теорія, осцилограми та код на С

Програмний I2C проти апаратного: запускаємо U8x8 на CH32V003

Огляд DC-DC Step-down Buck перетворювачів