当你满怀期待地将Keil MDK项目从AC5迁移到AC6,却发现原本正常的printf输出突然"哑火"了——这种场景对嵌入式开发者来说再熟悉不过。本文将带你深入剖析AC6编译器下printf重定向失效的根源,提供三种不同层级的解决方案,并最终推荐ST官方的最佳实践方案。
在AC5环境下运行良好的printf重定向代码,迁移到AC6后突然失效,通常表现为程序执行到printf时卡住或无任何输出。这种现象背后隐藏着两个关键的技术差异:
AC5时代常用的#pragma import(__use_no_semihosting)指令在AC6中已被弃用。这个指令原本的作用是告诉编译器不要使用半主机模式——一种通过调试器连接主机进行输入输出的机制。在AC6中,需要使用新的汇编语法:
c复制__asm(".global __use_no_semihosting\n\t");
注意:这个变更源于AC6改用基于LLVM的编译器架构,对底层符号处理方式进行了重构。
许多重定向代码会通过__GNUC__宏来判断编译器类型,但AC6虽然基于LLVM,却会意外定义这个宏。正确的判断条件应该是:
c复制#if defined(__GNUC__) && !defined(__clang__)
// GCC特有实现
#else
// 其他编译器实现
#endif
这个细微差别会导致代码错误地选择了不兼容的实现路径。下表对比了不同编译器的宏定义特征:
| 编译器类型 | __GNUC__定义 |
__clang__定义 |
__ARMCC_VERSION值范围 |
|---|---|---|---|
| AC5 | 未定义 | 未定义 | ≤ 6000000 |
| AC6 | 定义 | 定义 | ≥ 6010050 |
| GCC | 定义 | 未定义 | 未定义 |
针对上述问题,我们可以对原有AC5代码进行最小化修改:
将原来的pragma指令替换为汇编语法:
c复制// 原AC5代码
// #pragma import(__use_no_semihosting)
// AC6兼容代码
__asm(".global __use_no_semihosting\n\t");
更新条件编译逻辑,正确处理AC6的宏定义特征:
c复制#if defined(__GNUC__) && !defined(__clang__)
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
c复制#if !defined(__MICROLIB)
__asm(".global __use_no_semihosting\n\t");
void _sys_exit(int x) { x = x; }
void _ttywrch(int ch) { ch = ch; }
FILE __stdout;
#endif
#if defined(__GNUC__) && !defined(__clang__)
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
PUTCHAR_PROTOTYPE {
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
return ch;
}
基础方案虽然能解决问题,但在工程实践中我们还需要考虑更多因素:
在多任务环境下,printf可能被多个线程同时调用,需要添加互斥保护:
c复制PUTCHAR_PROTOTYPE {
static osMutexId_t printf_mutex = NULL;
if (printf_mutex == NULL) {
printf_mutex = osMutexNew(NULL);
}
osMutexAcquire(printf_mutex, osWaitForever);
HAL_StatusTypeDef status = HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
osMutexRelease(printf_mutex);
return (status == HAL_OK) ? ch : EOF;
}
扩展实现以支持运行时切换不同的UART端口:
c复制static UART_HandleTypeDef *active_uart = &huart1;
void set_printf_uart(UART_HandleTypeDef *huart) {
active_uart = huart;
}
PUTCHAR_PROTOTYPE {
HAL_UART_Transmit(active_uart, (uint8_t *)&ch, 1, 1000);
return ch;
}
增强鲁棒性,处理可能的传输错误:
c复制PUTCHAR_PROTOTYPE {
HAL_StatusTypeDef status = HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
if (status != HAL_OK) {
// 可在此添加错误处理逻辑,如重试或记录错误
return EOF;
}
return ch;
}
STMicroelectronics在其HAL库中提供了一套经过充分验证的解决方案,具有以下优势:
建议新建retarget.c文件,包含以下内容:
c复制#include "usart.h"
#include <stdio.h>
#if defined(__CC_ARM) /* AC5 */
#if !defined(__MICROLIB)
struct __FILE { int dummyVar; };
FILE __stdout;
#endif
#elif defined(__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050) /* AC6 */
#if !defined(__MICROLIB)
FILE __stdout;
#endif
#endif
#if defined(__ICCARM__) /* IAR */
size_t __write(int Handle, const unsigned char *Buf, size_t Bufsize) {
for(size_t i=0; i<Bufsize; i++) {
HAL_UART_Transmit(&huart1, (uint8_t *)&Buf[i], 1, 1000);
}
return Bufsize;
}
#elif defined(__CC_ARM) || (defined(__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050)) /* AC5/AC6 */
int fputc(int ch, FILE *f) {
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
return ch;
}
#else /* GCC */
int __io_putchar(int ch) {
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
return ch;
}
#endif
huart1为你的UART句柄当重定向仍然不工作时,可以尝试以下调试方法:
确保在AC6模式下正确配置了库选项:
当UART不可用时,可以考虑使用SWO(Serial Wire Output):
c复制#define ITM_Port8(n) (*((volatile unsigned char *)(0xE0000000+4*n)))
void ITM_SendChar(uint8_t ch) {
if ((CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk) &&
(ITM->TCR & ITM_TCR_ITMENA_Msk) &&
(ITM->TER & (1UL << 0))) {
while (ITM->PORT[0].u32 == 0);
ITM->PORT[0].u8 = ch;
}
}
// 在printf重定向中调用ITM_SendChar作为备用
PUTCHAR_PROTOTYPE {
if (huart1.Instance) { // 检查UART是否初始化
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
} else {
ITM_SendChar(ch);
}
return ch;
}
配置J-Scope来实时监控printf输出:
c复制#include "EventRecorder.h"
void init_debug(void) {
EventRecorderInitialize(EventRecordAll, 1);
EventRecorderStart();
}
c复制EventRecord2(1, ch, 0); // 记录字符到Event Recorder
在实际项目中,我通常会先使用ST官方方案作为基础,然后根据项目需求添加互斥锁、多端口支持等扩展功能。特别是在RTOS环境中,不加保护的printf重定向往往是难以追踪的随机崩溃的罪魁祸首。