突然发现忘记写这个了

使用Keil开发时, 如果想通过printf将数据打印到串口, 需要使用以下代码:

int fputc(int ch, FILE *f)
{
    /* 发送一个字节数据到 USART1,超时时间设置为 0xFFFF */ 
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
    return ch;
}

即重写fputc

如果转到基于CMake与GCC的工程时会发现以上代码不起作用, 调用printf时数据不会通过USART1发出来。

原因是Keil使用的ArmCC编译器与GCC编译器使用的C标准库不同。

编译器ARMCCGCC
C标准库MicroLib(ARM专门为嵌入式设备适配)NewLib(通用底层)
printf内容流向printf() -> vfprintf() -> fputc()printf() -> vfprintf() -> _write() (syscall) -> __io_putchar()

可以看到NewLib的数据流向中没有fputc出现, 所以重写fputc对重定向printf没有作用。

由于在MicroLib下数据只能走fputc, 所以这里不展开讲。重点讲一下NewLib下的重写方案

  1. 和MicroLib下相同, 只是将重写的函数从fputc()改为__io_putchar()
int __io_putchar(int ch)
{
    /* 发送一个字节数据到 USART1,超时时间设置为 0xFFFF */ 
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
    return ch;
}

好处是修改点相同, 可以直接将MicroLib下的代码搬过来, 便于快速移植。

但多字节传输时会频繁从syscall进入HAL, 需要多次执行包括获取HAL互斥锁、检查USART状态机、等待单字节传输完成等等一系列流程, 耗时更多、性能较差, 并且如果使用多线程或多任务还可能导致传输错位

例如线程A发送”Hello”, 中途被中断打断执行线程B, 线程B发送”world”, 最终发送效果可能是”Helworldlo”或者其他被打乱的数据

而且单字节传输无法使用DMA

因为单字节启动DMA的配置开销比直接阻塞发还大

所以在NewLib中, 更推荐的做法是直接重写_write()

int _write(int file, char *ptr, int len)
{
    // 直接一次性将整段缓冲区通过串口发送
    HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, 0xFFFF);
    return len; 
}

此时可以在入口处加入互斥锁等IPC保证传输的原子性, 更进一步, 可使用DMA+中断回调释放信号量来实现异步传输。

笔记关联