Outputting debugging information is an essential debugging tool in embedded development. One characteristic of embedded development is that often there is no operating system or file system, making conventional methods of printing logs to files generally unsuitable.
The most common method is to output UART logs through a serial port. For example, with the 51 microcontroller, as long as the serial port driver is implemented, outputting through the serial port is sufficient.
-
How can we print logs for a newly acquired chip that lacks a serial port driver? -
In some applications where timing is critical, what should we do if serial port log output takes too long? For example, during USB enumeration. -
What if certain bugs occur during normal operation but do not reproduce when the serial port log is enabled? -
How can we output logs when some packages do not have a serial port, or the serial port is already used for other purposes?
This article introduces how to print debugging information when the microcontroller lacks a serial port.
To be precise, we are not outputting logs here, but rather using a method to view logs without a serial port. During chip development, we can connect to a debugger for debugging and use breakpoints, but some operations cannot be interrupted, making breakpoint debugging impossible.In such cases, we can consider printing logs to SRAM. After the entire operation is completed, we can check the log buffer in SRAM through the debugger, achieving indirect log output.
The test platform used in this article is the STM32F407 Discovery, based on USB host experimental code. The principles apply to other embedded platforms as well.
First, define a structure for printing logs as follows:
typedef struct {
volatile u8 type;
u8* buffer; /* log buffer pointer*/
volatile u32 write_idx; /* log write position*/
volatile u32 read_idx; /* log read position*/
}log_dev;
Define a segment of SRAM space as the log buffer.
static u8 log_buffer[LOG_MAX_LEN];
The log buffer is a circular buffer, allowing for infinite log printing in a small buffer. However, the drawback is apparent; if logs are not output in time, they will be overwritten by new logs. The buffer size is allocated based on the SRAM size, and here we use 1kB.
Include the header file #include <stdio.h>, and implement the function fputc() in the code.
//redirect fputc
int fputc(int ch, FILE *f)
{
print_ch((u8)ch);
return ch;
}
Writing data to SRAM:
/* write log to buffer or I/O */
void print_ch(u8 ch)
{
log_dev_ptr->buffer[log_dev_ptr->write_idx++] = ch;
if(log_dev_ptr->write_idx >= LOG_MAX_LEN){
log_dev_ptr->write_idx = 0;
}
}
To facilitate control over log printing formats, we add a custom printing function in the header file:
#ifdef DEBUG_LOG_EN
#define DEBUG(...) printf("usb_printer:"__VA_ARGS__)
#else
#define DEBUG(...)
#endif
Add SWO operation function sets in the log structure:
typedef struct{
u8 (*init)(void* arg);
u8 (*print)(u8 ch);
u8 (*print_dma)(u8* buffer, u32 len);
}log_func;
typedef struct {
volatile u8 type;
u8* buffer;
volatile u32 write_idx;
volatile u32 read_idx;
//SWO
log_func* swo_log_func;
}log_dev;
SWO only requires the print operation function, implemented as follows:
u8 swo_print_ch(u8 ch)
{
ITM_SendChar(ch);
return 0;
}
Using SWO to output logs first outputs to the log buffer, and then outputs when the system is idle; of course, it can also output directly. Delaying log output can affect the real-time nature of logs, while direct output can affect time-sensitive code execution, so the choice depends on the scenario requiring log output. By calling the output_ch() function within a while loop, we can achieve log output when the system is idle.
/*output log buffer to I/O*/
void output_ch(void)
{
u8 ch;
volatile u32 tmp_write,tmp_read;
tmp_write = log_dev_ptr->write_idx;
tmp_read = log_dev_ptr->read_idx;
if(tmp_write != tmp_read){
ch = log_dev_ptr->buffer[tmp_read++];
//swo
if(log_dev_ptr->swo_log_func)
log_dev_ptr->swo_log_func->print(ch);
if(tmp_read >= LOG_MAX_LEN){
log_dev_ptr->read_idx = 0;
}else{
log_dev_ptr->read_idx = tmp_read;
}
}
}
1. Output via IDE
Using the SWO output function in the IDE requires the following configuration (Keil):
2. Output via STM32 ST-LINK Utility
Using the STM32 ST-LINK Utility does not require special settings; simply open the Printf via SWO viewer under the ST-LINK menu and press start:
All the above methods are suitable when serial port logs are temporarily unavailable or used just for a short time. For long-term use, outputting logs via the serial port is still necessary, as most of the time, it is not possible to connect to the debugger.
To add serial port log output, we only need to add the operation function set for the serial port:
typedef struct {
volatile u8 type;
u8* buffer;
volatile u32 write_idx;
volatile u32 read_idx;
volatile u32 dma_read_idx;
// uart
log_func* uart_log_func;
// SWO
log_func* swo_log_func;
}log_dev;
Implement the serial port driver function:
log_func uart_log_func = {
uart_log_init,
uart_print_ch,
0,
};
Adding serial port log output is similar to the SWO process, and will not be elaborated further. However, the issue to discuss is that the serial port’s low rate and the time required for data output can severely affect system operation. Although printing to SRAM first and then delaying output can mitigate the impact, if the system interrupts frequently or requires time-consuming calculations, logs may be lost. To solve this problem, we need to address the simultaneous operation of the CPU and outputting data to the serial port, and embedded engineers can immediately think of DMA as a good solution.
Using DMA to transfer log data to the serial port output does not affect CPU operation, thus solving the problem of time-consuming serial port log output affecting the system: Why use DMA for serial communication in STM32? The initialization functions for the serial port and DMA are as follows:
u8 uart_log_init(void* arg)
{
DMA_InitTypeDef DMA_InitStructure;
u32* bound = (u32*)arg;
// GPIO port settings
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); // Enable GPIOA clock
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE); // Enable USART2 clock
// Serial port 2 corresponding pin multiplexing
GPIO_PinAFConfig(GPIOA,GPIO_PinSource2,GPIO_AF_USART2);
// USART2 port configuration
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; // Multiplex function
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // Speed 50MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // Push-pull multiplex output
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // Pull-up
GPIO_Init(GPIOA,&GPIO_InitStructure);
// USART2 initialization settings
USART_InitStructure.USART_BaudRate = *bound; // Baud rate settings
USART_InitStructure.USART_WordLength = USART_WordLength_8b; // Data format with 8-bit length
USART_InitStructure.USART_StopBits = USART_StopBits_1; // One stop bit
USART_InitStructure.USART_Parity = USART_Parity_No; // No parity bit
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;// No hardware flow control
USART_InitStructure.USART_Mode = USART_Mode_Tx; // Transmit mode
USART_Init(USART2, &USART_InitStructure); // Initialize serial port 1
#ifdef LOG_UART_DMA_EN
USART_DMACmd(USART2,USART_DMAReq_Tx,ENABLE);
#endif
USART_Cmd(USART2, ENABLE); // Enable serial port 1
USART_ClearFlag(USART2, USART_FLAG_TC);
while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET);
#ifdef LOG_UART_DMA_EN
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
// Config DMA channel, uart2 TX usb DMA1 Stream6 Channel
DMA_DeInit(DMA1_Stream6);
DMA_InitStructure.DMA_Channel = DMA_Channel_4;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART2->DR);
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA1_Stream6, &DMA_InitStructure);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
#endif
return 0;
}
The function for outputting data via DMA to the serial port is as follows:
u8 uart_print_dma(u8* buffer, u32 len)
{
if((DMA1_Stream6->CR & DMA_SxCR_EN) != RESET){
// dma not ready
return 1;
}
if(DMA_GetFlagStatus(DMA1_Stream6,DMA_IT_TCIF6) != RESET){
DMA_ClearFlag(DMA1_Stream6,DMA_FLAG_TCIF6);
DMA_Cmd(DMA1_Stream6,DISABLE);
}
DMA_SetCurrDataCounter(DMA1_Stream6,len);
DMA_MemoryTargetConfig(DMA1_Stream6, (u32)buffer, DMA_Memory_0);
DMA_Cmd(DMA1_Stream6,ENABLE);
return 0;
}
Here, for convenience, we directly use the polling method for the DMA status register. If needed, it can be modified to use DMA interrupt mode. The datasheet can be referenced to find that serial port 2 uses DMA1 channel 4 stream 6:
Finally, we discuss how to output logs when there is no serial port in certain packages, or when the serial port has been used for other purposes. In such cases, we can find a free general IO to simulate UART protocol and output logs to the serial port tool on the host computer.
As long as we output high and low levels on the IO at determined times, we can simulate waveforms. This determined timing corresponds to the serial port’s baud rate.
To achieve precise timing, we use the TIM4 timer to generate a 1us delay. Note: The timer cannot be reused; in the test project, TIM2 and TIM3 have already been used. Reusing them would cause confusion.
The initialization function is as follows:
u8 simu_log_init(void* arg)
{
TIM_TimeBaseInitTypeDef TIM_InitStructure;
u32* bound = (u32*)arg;
// GPIO port settings
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); // Enable GPIOA clock
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // Speed 50MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // Push-pull multiplex output
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // Pull-up
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_2);
// Config TIM
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE); // Enable TIM4 clock
TIM_DeInit(TIM4);
TIM_InitStructure.TIM_Prescaler = 1; // 2 prescaler
TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_InitStructure.TIM_Period = 41; // 1us timer
TIM_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM4, &TIM_InitStructure);
TIM_ClearFlag(TIM4, TIM_FLAG_Update);
baud_delay = 1000000/(*bound); // Calculate delay per bit based on baud rate
return 0;
}
The delay function using the timer is as follows:
void simu_delay(u32 us)
{
volatile u32 tmp_us = us;
TIM_SetCounter(TIM4, 0);
TIM_Cmd(TIM4, ENABLE);
while(tmp_us--){
while(TIM_GetFlagStatus(TIM4, TIM_FLAG_Update) == RESET);
TIM_ClearFlag(TIM4, TIM_FLAG_Update);
}
TIM_Cmd(TIM4, DISABLE);
}
The final simulation output function must ensure interrupts are disabled before outputting; after outputting one byte, interrupts can be re-enabled; otherwise, garbled output may occur:
u8 simu_print_ch(u8 ch)
{
volatile u8 i=8;
__asm("cpsid i");
//start bit
GPIO_ResetBits(GPIOA, GPIO_Pin_2);
simu_delay(baud_delay);
while(i--){
if(ch & 0x01)
GPIO_SetBits(GPIOA, GPIO_Pin_2);
else
GPIO_ResetBits(GPIOA, GPIO_Pin_2);
ch >>= 1;
simu_delay(baud_delay);
}
// stop bit
GPIO_SetBits(GPIOA, GPIO_Pin_2);
simu_delay(baud_delay);
simu_delay(baud_delay);
__asm("cpsie i");
return 0;
}
This article introduced several methods used for printing debugging information during development. Methods are always fixed, but the key lies in flexible usage; by printing effective debugging information, we can help solve issues encountered in development and later maintenance, avoiding unnecessary detours.
Comprehensive Examples of Microcontroller Programming
Collection of 50 Books on Microcontroller Interfaces
Learn Microcontroller in Ten Days – Complete Version
Microcontroller Peripheral Circuit Design
100 Examples of Microcontroller Programming Techniques
👇 Scan to Join the Microcontroller WeChat Group and Download Materials for Free 👇