rt-thread 驱动篇(六)serialX弊端及解决方法

描述

前言

控制台,做为一种人机交互接口,相较于其他接口(显示器、网络终端),可能是最简单的。它耗用资源少,容易配置,几乎是任何芯片会自带的外设。而且可以很容易和计算机建立连接。因而,串口控制台可能是程序员进行人机交互的首选。

启用控制台,可以帮我们在系统全速运行时窥探系统运行状况。可以监测其它外设或者组件初始化过程。

一月份,笔者在论坛发的 serialX 串口驱动反响很大,应该会让很多人眼前一亮(老王卖瓜)。当初,决定费力研究它的初衷很简单 —— 应用和驱动弱耦合、真阻塞非阻塞特性。这两个月来,笔者一直在实际项目中使用它,而且应用到了控制台上,同时发现了一些问题。

控制台串口问题汇总

问题一、任务调度器启动前 `rt_kprintf` 死循环到 tx 函数

问题原因是,笔者打开控制台串口设定的 flag 是阻塞写方式,无论是中断发送还是 DMA 发送都依赖中断。但是任务调度器启动前是关全局中断的,这样导致触发发送失败,发送缓冲区满了以后再有写动作就会永久死到 tx 函数里。

> 应对之策:任务调度器启动前只能使用 poll 发送模式。任务调度器启动后或临启动时切换到中断或 DMA 模式。

问题二、出现异常后,`rt_kprintf` 无输出

这个现象和上一条有点儿类似。不同的是,虽然全局中断是开着的,但是串口中断优先级不足,导致控制台设备停止工作。

在 arm9 架构上出现 Undef SWI PAbt DBbt 等等 trap 后,串口外设中断级别不足,导致这些 trap 中的 printf 输出失效。

> 应对之策:unset 控制台串口设备,或者将控制台串口切换到 poll 发送模式。

问题三、`rt_hw_interrupt_disable` 之后 `rt_kprintf` 无输出

这个和“问题一”是一样的,根本原因是,非 poll 模式必须有中断它才能工作,关中断以后设备停止工作。

> 应对之策:尽量减少关中断时间;避免在关中断之后写串口设备。

问题四、遇到 `_rt_scheduler_stack_check` 也会停止输出

因为 `_rt_scheduler_stack_check` 函数最后先关全局中断,然后进入 while 死循环。这个时候串口中断肯定也失效了。

> 应对之策:关全局中断前,先 flush 串口设备。让串口把 “stack overflow” 的提示信息输出完。

问题五、打断点后 `rt_kprintf` 输出不完整,部分数据没输出到控制台

因为 debug 断点停止的时候,前边 printf 缓存的数据可能还没来得及送到串口移位寄存器,cpu 的时钟被断点打断停止运行了,导致部分数据没输出。继续运行程序就可以出现剩余信息输出。这是非阻塞设备的特性。

 

以上这些问题是所有非 poll 非阻塞设备输出都会遇到的现象。在 RTOS 系统里,应用程序不可避免地要和中断打交道,了解中断对我们编程思想的影响很重要。


完整解决方案

rt_device 增加 flush 接口

flush 接口对带缓存设备是极其有用的,无论是阻塞还是非阻塞模式,我们总有需求要求*在某个代码节点设备的缓存已经是空的*,*或者要求实现通信同步*。

`struct rt_device` 增加 `flush` 接口

struct rt_device
{
...
   rt_err_t  (*flush)  (rt_device_t dev);
...
};

serialX.c 添加 `flush` 回调函数实现 `static rt_err_t rt_serial_flush(struct rt_device *dev)` ,用于等待串口驱动层发送缓存发完数据。另外底层外设也增加 flush 接口,用于等待串口发送寄存器中的*最后一个字节数据*被搬到了移位发送寄存器中。

console 添加 unset flush 控制台设备接口

void rt_console_unset_device()
{
   if (_console_device != RT_NULL)
   {
       /* close old console device */
       rt_device_close(_console_device);
       _console_device = RT_NULL;
   }
}
RT_WEAK void rt_hw_console_flush()
{
   /* empty console output */
}
void rt_console_flush()
{
#ifdef RT_USING_DEVICE
   if (_console_device == RT_NULL)
   {
       rt_hw_console_flush();
   }
   else
   {
       rt_device_flush(_console_device);
   }
#else
   rt_hw_console_flush();
#endif /* RT_USING_DEVICE */
}

有 set 也有 unset, 不是吗? unset 是为了调用 `rt_hw_console_output` 而不是 `rt_device_write` 输出打印信息。

`rt_console_flush` 既考虑启用设备框架也考虑未启用设备框架两种情况。`rt_device_flush(_console_device)` 会调用上文的 `rt_serial_flush` ;`rt_hw_console_flush` 和 `rt_hw_console_output` 类似用于不使用设备框架,自定义 `rt_kprintf` 底层接口时要实现的。视实际情况实现 `rt_hw_console_flush` 。例如 NUC970 UART 自带了 FIFO ,需要实现 `rt_hw_console_flush`

> 如果使用了 DMA 模式,底层实现 flush 还是有点儿难度的。需要花点儿心思。

延迟 `rt_console_set_device` 调用

挪到任务调度器启动前,那么之前的控制台输出怎么实现?答案是使用 `rt_hw_console_output`。如上所说,第一次使用 poll 模式打开控制台串口,到这里临启动任务调度器的时候再次用 中断/DMA 模式打开控制台串口也可以。但是,多次用不同模式打开同一个设备会引入另外的问题,要不要先关闭上次的 open 呢?假如之前没有打开过呢?

   /* Set the shell console output device */
#ifdef RT_USING_CONSOLE
   rt_console_flush();
rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif
   /* start scheduler */
   rt_system_scheduler_start();

这时候,我们的 `rt_console_set_device` 可以用任何模式打开控制台串口设备

   if (rt_device_open(new_device, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_STREAM
                       | RT_DEVICE_FLAG_INT_RX
                       | RT_DEVICE_FLAG_INT_TX
                       ) == RT_EOK) {
       _console_device = new_device;
   }

或者,先用 poll 模式 set console device

       /* set new console device */
       if (rt_device_open(new_device, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_STREAM
                       ) == RT_EOK) {
           _console_device = new_device;
       }

当第二次 reset 的时候,需要先 unset (用到了上面提到的 `rt_console_unset_device`),因为 `rt_console_set_device` 不允许重复 set 同一个设备,也没法修改打开设备的参数。写另外一个 set api 也就变的必要了

       if (rt_device_open(new_device, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_STREAM
                       | RT_DEVICE_FLAG_INT_RX
                       | RT_DEVICE_FLAG_INT_TX
                       ) == RT_EOK) {
           _console_device = new_device;
       }

但是,我为什么不喜欢这种方式呢?

1.  board 初始化阶段需要初始化系统时钟、倍频 cpu 时钟、 `rt_hw_systick_init`、 `rt_system_heap_init`、 `rt_hw_pin_init`、还有 `rt_hw_usart_init` 设备,可能还有 `rt_console_set_device`。为了能第一时间使用上控制台串口,`rt_hw_usart_init` 必须尽早执行,然后是 `rt_console_set_device` 。
2.  但是 uart 设备可能用到动态申请内存,这样就必然要求 `rt_system_heap_init` 先于 `rt_hw_usart_init` 。
3.  初始化系统时钟、倍频 cpu 时钟、 `rt_hw_systick_init`、 `rt_system_heap_init`、 `rt_hw_usart_init` 。也只能这样了,前边几步的串口打印需求就忽略了吧,`rt_system_heap_init`->`rt_memheap_init` 里的 `RT_DEBUG_LOG` 调试信息就忽略了吧。

如果不着急用串口设备,先简单初始化串口外设,让 `rt_hw_console_output` 以最快的速度工作起来。如此一来,初始化流程可能就可以变成,初始化系统时钟、倍频 cpu 时钟、 **`rt_hw_console_init`**、 `rt_hw_systick_init`、... 。后面是初始化顺序都无关紧要了,而且所有的打印信息需求都可以满足。最后在任务调度器启动前选择某个串口设备做控制台串口,将会避免前文说到的*问题一*。

进入不可恢复状态的处理

- 以 `_rt_scheduler_stack_check` 为例,关中断前先 flush 控制台。

   rt_console_flush();
   level = rt_hw_interrupt_disable();
   while (level);

- 还比如 SWI 异常,`rt_hw_cpu_shutdown` 也会关中断,进入 while 死循环。先 unset 控制台,使用 `rt_hw_console_output` 进行 poll 输出之后的输出需求。

void rt_hw_trap_swi(struct rt_hw_register *regs)
{
   rt_console_unset_device();
   rt_hw_show_register(regs);   rt_kprintf("software interrupt\n");
   rt_hw_cpu_shutdown();
}

**注:为避免因中断优先级,引起串口设备中断得不到响应,在中断响应里切忌调用 `rt_console_flush` 函数**

结束

控制台串口在系统中扮演着极其重要的角色,对其处理不当,会引起各种依赖问题。有人就有疑虑了,做其它通信用时 serialX 会不会存在同样的隐患?笔者保证,您不在中断响应里调用 `rt_device_flush` 就不会出现以上所有列出来的问题。

笔者下一篇计划聊聊内核启动流程的问题,虽然之前发过一篇文章 [rt-thread 系统启动及 SysTick 初始化流程优化可行性分析]( https://club.rt-thread.org/ask/article/2881.html ),里面提了一种可能的系统启动流程,当时只是一种想法,并不系统。再写一篇,笔者希望把需要考虑的问题以及优缺点系统化地说明白,可能还会提及控制台串口设备,以及控制台对内核启动流程的影响。


相关文章:

rt-thread 驱动篇(一) serialX 框架理论

rt-thread 驱动篇(二) serialX 理论实现

rt-thread 驱动篇(三) serialX 压力测试

rt-thread 驱动篇(四)serialX 多架构适配

rt-thread 驱动篇(五)serialX 小试牛刀

  审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分