RT-Thread论坛
直播中

乐侨珂

9年用户 1064经验值
擅长:控制/MCU
私信 关注
[问答]

ulog输出异常-中断与线程打印冲突怎么解决?

逻辑:can收到数据后,can中断打印test,并把数据放到消息队列,main函数的消息队列打印消息内容
问题:只要can中断中有test打印的代码,整个串口就会卡死;
中断中无test打印的代码,main函数的打印正常;
main函数的打印代码注释掉的话,中断中的test打印正常;
.config相关的配置

  • #
  • # Utilities
  • #
  • # CONFIG_RT_USING_RYM is not set
  • CONFIG_RT_USING_ULOG=y
  • # CONFIG_ULOG_OUTPUT_LVL_A is not set
  • # CONFIG_ULOG_OUTPUT_LVL_E is not set
  • # CONFIG_ULOG_OUTPUT_LVL_W is not set
  • # CONFIG_ULOG_OUTPUT_LVL_I is not set
  • CONFIG_ULOG_OUTPUT_LVL_D=y
  • CONFIG_ULOG_OUTPUT_LVL=7
  • CONFIG_ULOG_USING_ISR_LOG=y
  • CONFIG_ULOG_ASSERT_ENABLE=y
  • CONFIG_ULOG_LINE_BUF_SIZE=128
  • CONFIG_ULOG_USING_ASYNC_OUTPUT=y
  • CONFIG_ULOG_ASYNC_OUTPUT_BUF_SIZE=2048
  • CONFIG_ULOG_ASYNC_OUTPUT_BY_THREAD=y
  • CONFIG_ULOG_ASYNC_OUTPUT_THREAD_STACK=2048
  • CONFIG_ULOG_ASYNC_OUTPUT_THREAD_PRIORITY=30

  • #
  • # log format
  • #
  • CONFIG_ULOG_OUTPUT_FLOAT=y
  • CONFIG_ULOG_USING_COLOR=y
  • CONFIG_ULOG_OUTPUT_TIME=y
  • # CONFIG_ULOG_TIME_USING_TIMESTAMP is not set
  • CONFIG_ULOG_OUTPUT_LEVEL=y
  • CONFIG_ULOG_OUTPUT_TAG=y
  • # CONFIG_ULOG_OUTPUT_THREAD_NAME is not set
  • CONFIG_ULOG_BACKEND_USING_CONSOLE=y
  • # CONFIG_ULOG_BACKEND_USING_FILE is not set
  • # CONFIG_ULOG_USING_FILTER is not set
  • # CONFIG_ULOG_USING_SYSLOG is not set
  • # CONFIG_RT_USING_UTEST is not set
  • # CONFIG_RT_USING_VAR_EXPORT is not set
  • # CONFIG_RT_USING_RESOURCE_ID is not set
  • # CONFIG_RT_USING_ADT is not set
  • # CONFIG_RT_USING_RT_LINK is not set
  • # CONFIG_RT_USING_VBUS is not set


can中断中打印的代码

  • void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
  •     struct can_rx_msg msg;

  •     if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &msg.RxHeader, msg.data) != HAL_OK) {
  •         LOG_E("get can rx errorn");
  •     }

  •     if(msg.RxHeader.StdId != 0x601){ // A板TSDO 0x601
  •         return;
  •     }
  • LOG_E("test");
  •     if(RT_EOK != rt_mq_send(&can1_obj.rx_msg_mq, &msg, sizeof(msg))){
  •         LOG_E("send my_canopen_sdo_mq failed");
  •     }
  • }


main线程打印消息队列的数据

  • int main(void)
  • {

  •     while (1)
  •     {
  •         struct can_rx_msg rx_msg;
  •         if(1 == can_receive_data(&can1_obj, &rx_msg, RT_WAITING_FOREVER))
  •         {
  •             LOG_D("stdid:%x dlc:%d data:%x %x %x %x %x %x %x %x",
  •             rx_msg.RxHeader.StdId, rx_msg.RxHeader.DLC,
  •             rx_msg.data[0], rx_msg.data[1], rx_msg.data[2], rx_msg.data[3],
  •             rx_msg.data[4], rx_msg.data[5], rx_msg.data[6], rx_msg.data[7]);
  •         }
  •     }
  • }




回帖(1)

贾永世

2025-10-11 16:10:52

根据问题描述,存在以下现象:
1. 在CAN中断服务函数中,如果调用ulog输出"test",同时将数据放入消息队列,然后在主线程中从消息队列取出数据并打印,会导致整个串口卡死。
2. 如果中断服务函数中没有ulog打印"test",则主线程的打印正常。
3. 如果主线程的打印代码注释掉,中断服务函数中的ulog打印"test"正常。

另外,提供的.config配置片段显示:
- 启用了ulog(CONFIG_RT_USING_ULOG=y)
- 日志输出级别配置中,仅有一个被设置(从配置看,可能配置了DEBUG级别?但具体是哪个级别被设置并不明确,因为其他级别被注释掉了,但有一个CONFIG_ULOG_...,可能被设置成了某个级别)

问题分析:
1. 中断服务函数中调用ulog打印:ulog在中断中打印会使用到系统资源(如缓冲区、设备操作等),而中断上下文要求快速执行,不应进行耗时的操作,尤其是可能引起阻塞或调度的操作。
2. 串口卡死:可能是由于中断和主线程同时访问串口资源(同一个串口设备)导致竞争,而ulog的输出通常是非线程安全的(除非有保护机制),尤其是在中断上下文和线程上下文同时调用时。
3. 主线程和中断服务函数都在使用同一个串口输出,而串口输出设备在RT-Thread中默认可能没有做重入保护。

RT-Thread的ulog设计:
- ulog在输出时,会使用到日志设备(如串口),在日志输出过程中,会对日志设备加锁(通过互斥锁)以保证线程安全。但是在中断上下文中,由于互斥锁可能导致阻塞(中断中不能阻塞),因此ulog在中断中打印日志时,默认是不允许的,除非进行了特殊处理。

查看RT-Thread中ulog的源码(ulog.c)可以发现:
- 在`ulog_output_to_all_backend`函数中,会检查当前上下文:
  ```c
  if (rt_interrupt_get_nest() != 0)
  {
      /* if current context is interrupt, output log immediately */
      ...
  }
  else
  {
      /* lock output */
      if (rt_mutex_take(&ulog.lock, RT_WAITING_FOREVER) == RT_EOK)
      {
          ...
          rt_mutex_release(&ulog.lock);
      }
  }
  ```
  但是在中断中,不会获取互斥锁,而是直接输出。然而,直接输出到后端设备(如串口)时,串口设备驱动本身可能没有做中断保护。也就是说,如果在中断中输出日志,同时另一个线程(比如主线程)也在输出日志,那么就会发生冲突,导致串口输出混乱甚至卡死。

解决方案:
1. 避免在中断服务函数中调用ulog打印,因为中断服务函数应该尽量简短,快速执行完毕。可以将日志输出转移到线程中执行。
2. 如果非要在中断中打印,可以考虑使用异步日志输出机制。但RT-Thread的ulog默认不支持异步,需要额外处理。
3. 检查串口驱动是否支持在中断上下文中被调用(即重入保护)。通常串口驱动在发送数据时,会有一个发送缓冲区和一个锁(自旋锁或关中断)来保护。如果串口驱动没有做这样的保护,那么就需要修改串口驱动。

针对当前问题,建议:
- 不在CAN中断中直接打印日志,而是将日志输出放到主线程(或专门的日志线程)中。
- 例如,在中断中,仅将数据放入消息队列,然后由主线程从消息队列中取出数据并打印日志(包括“test”和消息内容)。

修改代码示例:

原中断服务函数可能类似这样:
```c
static void can_rx_isr(...)
{
    /* 接收数据 */
    ...

    /* 在中断中打印 */
    LOG_D("test");  // 这行导致问题

    /* 将数据放入消息队列 */
    rt_mq_send(&mq, ...);
}
```

修改为:
```c
static void can_rx_isr(...)
{
    /* 接收数据 */
    ...

    /* 不再在中断中打印,而是将需要打印的信息也放入消息队列(或者通过其他方式通知线程) */
    /* 假设消息队列传递的是数据,那么我们可以将"test"打印也移到主线程 */
    rt_mq_send(&mq, ...);
}
```

然后在主线程中:
```c
void main_thread_entry(void *param)
{
    while (1)
    {
        if (rt_mq_recv(&mq, ...) == RT_EOK)
        {
            LOG_D("test");  // 这里打印原本在中断中打印的"test"
            LOG_D("message content: ..."); // 打印消息内容
        }
    }
}
```

这样就将中断中的日志输出移到了主线程,避免了在中断上下文中输出日志,从而避免了冲突。

另外,根据.config配置,确认ulog的日志级别是否设置正确,确保需要的日志级别是打开的。

如果确实需要在中断中输出日志(比如调试),可以考虑以下方法:
1. 在ulog的后端(比如串口后端)中,使用一个缓冲区,在中断中只将日志信息放入缓冲区,然后由专门的线程负责输出。这需要修改ulog的后端实现。
2. 或者,在中断中使用简单的非阻塞方式输出,比如使用轮询方式发送串口数据(这样会占用中断时间,不推荐)。

但总体而言,中断中打印日志不是好习惯,建议在中断中只做关键操作(置标志、发送消息等),将非实时操作交给线程处理。

最后,检查RT-Thread的串口驱动是否支持中断重入保护。在串口驱动发送函数中,通常会使用互斥锁(mutex)或者自旋锁(spinlock)来保护发送过程。如果使用的是互斥锁,在中断上下文中不能使用(因为互斥锁在获取时可能会导致阻塞),所以中断中只能使用无锁的方式,这需要驱动设计为支持中断上下文的发送(例如,使用关中断或者无锁缓冲区)。但即使这样,如果中断和线程同时调用串口发送,仍可能冲突。因此,最好的办法还是避免在中断中使用串口输出。

根据您的描述,问题是由中断上下文与线程上下文对串口资源的竞争导致的。以下是根本原因分析和解决方案:




问题根源




  1. 中断与线程的串口竞争

    ulog在输出时会对串口设备加锁(使用互斥锁)。当中断服务程序(ISR)和线程同时调用ulog时:



    • 线程:通过rt_mutex_take()获取锁(允许阻塞等待)。

    • 中断:无法阻塞等待锁(中断中不能休眠),导致:

      • 若线程已持有锁,中断中的ulog会绕过锁直接输出,导致串口数据混乱。

      • 若中断持续触发,串口被中断抢占,线程无法完成输出,表现为"卡死"。





  2. 现象分析



    • 中断有打印 + 主线程有打印 → 卡死(竞争冲突)

    • 中断无打印 → 主线程正常(无竞争)

    • 主线程注释 → 中断打印正常(无竞争)






解决方案


方法1:禁止在中断中使用ulog(推荐)


中断中仅做关键操作(如发送数据到队列),日志移到线程中输出:


// CAN中断服务函数
void can_isr(...) {
    rt_mq_send(&mq, &data, sizeof(data)); // 仅发送数据到队列
    // 移除中断内的 ulog 打印
}

// 主线程循环
void main_thread() {
    while (1) {
        if (rt_mq_recv(&mq, &data, ...) == RT_EOK) {
            LOG_I("test");            // 线程内打印日志
            LOG_I("Data: %d", data);  // 线程内打印数据
        }
    }
}

方法2:使用自旋锁保护串口输出(需修改驱动)



适用于必须中断打印的场景,但会增加中断延迟。





  1. 修改串口驱动

    将默认的互斥锁(mutex)替换为自旋锁(spinlock)


    // 在串口驱动发送函数中:
    static rt_size_t uart_write(rt_device_t dev, const void *buf, rt_size_t size) {
       struct rt_uart_device *uart = (struct rt_uart_device *)dev;
       rt_base_t flags;

       // 关中断获取自旋锁(防止中断抢占)
       flags = rt_hw_interrupt_disable();

       // 执行串口发送操作
       hardware_send_data(buf, size);

       // 恢复中断状态
       rt_hw_interrupt_enable(flags);
       return size;
    }


  2. 权衡  

    • ✅ 允许中断中安全输出

    • ❌ 中断中串口输出耗时增加(可能影响实时性)



方法3:启用ulog异步模式(需RT-Thread支持)


ulog配置中开启异步输出,日志先存入缓冲区,由独立线程处理输出:


# .config 文件添加
CONFIG_ULOG_ASYNC_OUTPUT=y
CONFIG_ULOG_ASYNC_OUTPUT_BUF_SIZE=1024

确保中断中仅调用ulog的异步接口(如LOG_RAW()),但需验证兼容性。




配置建议


检查.config确保日志级别允许输出:


# 启用 INFO 级别日志(主线程需此级别)
CONFIG_ULOG_OUTPUT_LVL_I=y

# 确保控制台后端已开启
CONFIG_ULOG_BACKEND_USING_CONSOLE=y



关键原则



  1. 中断中避免耗时操作:日志打印、内存分配等操作严禁在中断中执行。

  2. 资源隔离:中断与线程共享资源(如串口)需通过锁或队列同步。

  3. 实时性优先:中断处理时间应短于数据到达间隔。


通过将日志输出移至主线程(方法1),可彻底解决竞争问题,且符合RTOS最佳实践。

举报

更多回帖

发帖
×
20
完善资料,
赚取积分