printf復習
當我們寫printf("%d\n", 1);的時候,printf函數並不能通過C語言語法得知第二個參數是int類型。printf是一個變參函數(variadic function):
int printf(const char *restrict format, ...);
參數的類型都是通過格式串format推導出的。如果參數類型與格式串中指定的不匹配,或提供的參數數量少於需要的,將導致未定義行為。
由於參數類型是動態的,printf和scanf比靜態類型的std::cout和std::cin慢,前提是后者的眾多overhead被手動消除。
C為可變參數提供了va_start、va_arg、va_copy、va_end、va_list等工具,定義在頭文件<stdarg.h>中。va_arg用於取出參數,va_copy用於拷貝參數供多次使用。引用cppreference上的例子:
#include <stdio.h>
#include <stdarg.h>
#include <math.h>
double sample_stddev(int count, ...)
{
/* Compute the mean with args1. */
double sum = 0;
va_list args1;
va_start(args1, count);
va_list args2;
va_copy(args2, args1); /* copy va_list object */
for (int i = 0; i < count; ++i) {
double num = va_arg(args1, double);
sum += num;
}
va_end(args1);
double mean = sum / count;
/* Compute standard deviation with args2 and mean. */
double sum_sq_diff = 0;
for (int i = 0; i < count; ++i) {
double num = va_arg(args2, double);
sum_sq_diff += (num-mean) * (num-mean);
}
va_end(args2);
return sqrt(sum_sq_diff / count);
}
int main(void)
{
printf("%f\n", sample_stddev(4, 25.0, 27.3, 26.9, 25.7));
}
<stdio.h>還定義了vprintf系列函數,與不帶v的相比,可變參數...都換成了va_list的實例:
int vprintf(const char *format, va_list vlist);
可以借此實現自己的printf。
可變參數在傳遞的過程中會被執行默認參數提升(default argument promotion),對於整數類型執行整數提升(提升為int或unsigned int),對於float類型提升成double。
格式串format中的普通字符直接拷貝到輸出流,由%引導的稱為轉換格式(conversion specification),在%和轉換說明符(conversion specifier)之間可以有若干修飾符,實現對齊、精度等功能,轉換說明符有c、s、d、f等,詳見cppreference。
UART實現
單片機開發板並沒有可以用於輸出的控制台,printf調用最后都會歸結為_write函數:
int _write(int file, char* ptr, int len);
_write函數需要把ptr指向的len字節的數據以想要的形式發送,在此就沿用上一篇中的UART異步IO,於是printf就可以打印在串口上了。
為了方便日后使用,我把USART相關的代碼抽離出來放在一個新的源文件里,IDE生成的代碼去掉MX_USART1_UART_Init和USART1_IRQHandler兩個函數,再加上這一對文件就可以使用了。
usart1.h:
#include <stdio.h>
void MX_USART1_UART_Init();
void usart1_transmit(char c);
char usart1_receive();
usart1.c:
#include "usart1.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include "cmsis_gcc.h"
#include "stm32f4xx_hal.h"
typedef char queue_element_t;
typedef struct
{
uint16_t mask;
uint16_t head;
uint16_t tail;
queue_element_t data[0];
} queue_t;
static inline queue_t* queue_create(uint16_t _size)
{
if (_size & (_size - 1))
_size = 256;
queue_t* q = malloc(sizeof(queue_t) + _size * sizeof(queue_element_t));
if (q)
{
q->mask = _size - 1;
q->head = q->tail = 0;
}
return q;
}
static inline bool queue_empty(const queue_t* _queue)
{
return _queue->head == _queue->tail;
}
static inline uint16_t queue_size(const queue_t* _queue)
{
return (_queue->tail - _queue->head) & _queue->mask;
}
static inline uint16_t queue_capacity(const queue_t* _queue)
{
return _queue->mask;
}
static inline queue_element_t queue_peek(const queue_t* _queue)
{
return _queue->data[_queue->head];
}
static inline void queue_push(queue_t* _queue, const queue_element_t _ele)
{
_queue->data[_queue->tail] = _ele;
_queue->tail = (_queue->tail + 1) & _queue->mask;
}
static inline void queue_pop(queue_t* _queue)
{
_queue->head = (_queue->head + 1) & _queue->mask;
}
extern UART_HandleTypeDef huart1;
extern void Error_Handler();
queue_t* tx_buffer;
queue_t* rx_buffer;
void USART1_IRQHandler()
{
uint32_t isrflags = USART1->SR;
uint32_t cr1its = USART1->CR1;
uint32_t errorflags = 0x00U;
errorflags = (isrflags & (uint32_t)(USART_SR_PE | USART_SR_FE | USART_SR_ORE | USART_SR_NE));
if (errorflags == RESET)
{
if (((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET))
{
queue_push(rx_buffer, USART1->DR);
return;
}
if (((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET))
{
USART1->DR = queue_peek(tx_buffer);
queue_pop(tx_buffer);
if (queue_empty(tx_buffer))
USART1->CR1 &= ~USART_CR1_TXEIE & UART_IT_MASK;
return;
}
}
HAL_UART_IRQHandler(&huart1);
}
void MX_USART1_UART_Init()
{
tx_buffer = queue_create(1024);
rx_buffer = queue_create(1024);
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
USART1->CR1 |= USART_CR1_RXNEIE & UART_IT_MASK;
}
void usart1_transmit(char c)
{
uint16_t capacity = queue_capacity(tx_buffer);
bool ok = false;
while (1)
{
__disable_irq();
ok = capacity - queue_size(tx_buffer) >= 1;
if (ok)
break;
__enable_irq();
__NOP();
}
queue_push(tx_buffer, c);
USART1->CR1 |= USART_CR1_TXEIE & UART_IT_MASK;
__enable_irq();
}
char usart1_receive()
{
bool ok = false;
while (1)
{
__disable_irq();
ok = !queue_empty(rx_buffer);
if (ok)
break;
__enable_irq();
__NOP();
}
char c = queue_peek(rx_buffer);
queue_pop(rx_buffer);
__enable_irq();
return c;
}
int _write(int file, char* ptr, int len)
{
for (int i = 0; i != len; ++i)
usart1_transmit(*ptr++);
return len;
}
main.c(部分):
#include "main.h"
#include "usart1.h"
UART_HandleTypeDef huart1;
uint8_t count = 0;
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
while (1)
{
printf("Hello world: %d\n", count);
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
++count;
HAL_Delay(500);
}
}
ITM實現
明明已經用調試器連接了開發板和電腦,還要加個USB轉串口工具就顯得很累贅;IDE和串口監視器兩個窗口的頻繁切換也讓Alt和Tab鍵損壞的幾率增加了幾成。有沒有辦法讓開發板通過調試器和IDE就能輸出呢?
可以用ARM的ITM(Instrumentation Trace Macroblock),通過TRACESWO發送。SWO與JTAG的JTDIO是同一個引腳,用標准ST-LINK的20-pin排線可以連接,但是10-pin的簡版ST-LINK沒有引出SWO,因此要使用ITM調試不能用簡版的4線接法。
ITM無需初始化,直接調用ITM_SendChar函數即可發送,該函數定義在\Drivers\CMSIS\Include\core_cmx.h中。ITM版的_write函數,不過是把usart1_transmit換成ITM_SendChar而已。
#include "main.h"
#include <stdio.h>
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
int _write(int file, char* ptr, int len)
{
for (int i = 0; i != len; ++i)
ITM_SendChar(*ptr++);
return len;
}
uint8_t count = 0;
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
while (1)
{
printf("Hello world: %d\n", count);
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
++count;
HAL_Delay(500);
}
}
為了在IDE中看到printf輸出的內容,需要做幾步配置。首先進入Debug模式,在調試選項的Debugger頁啟用SWV:

找到SWV ITM Data Console窗口:

窗口右上角Configure trace,勾選Port 0:

點擊Start Trace。這樣就可以看見printf的輸出了:

雜記
好久沒更博客了。這兩周一直在做搖搖棒,硬件軟件交替着改,總算是做出一個比較穩定的顯示效果了。計划本月再更兩篇。
有一次下載器與搖搖棒的連接有松動,數據傳輸錯誤,導致熔絲位被修改,時鍾源選擇了不存在的,程序無法啟動,也無法下載新的程序。還好我帶着這塊STM32開發板,在一個引腳上產生一個較高頻率的方波,連接到單片機的晶振引腳,改回熔絲位,算是把單片機救活了。本來STM32開發板帶着是要寫這篇printf的,博客沒寫,倒是有救場的用途。
與printf相對的scanf,我也嘗試過實現,但是有兩個問題,一是我沒有找到在STM32CubeIDE中如何通過ITM向單片機發送,二是_read函數的len參數總是1024,這是想讓我一次性讀1024個字節再返回嗎?
