当前位置: 首页 > news >正文

详解 RT-Thread 串口一配置、设备查找与打印功能(rt_kprintf)的绑定机制

前言

在rt-thread上做开发时,会经常使用到rt_kprintf()打印函数作为调试手段,突然好奇这个打印函数是怎么绑定到串口1上去的?默认通过串口1输出可以修改吗,比如修改为串口2输出?串口1又是如何配置的,为什么一上来就可以用

一、核心逻辑:rt_kprintf 的输出路径

进入rt_kprintf 函数的内部:

RT_WEAK int rt_kprintf(const char *fmt, ...)
{va_list args;rt_size_t length;static char rt_log_buf[RT_CONSOLEBUF_SIZE];va_start(args, fmt);/* the return value of vsnprintf is the number of bytes that would be* written to buffer had if the size of the buffer been sufficiently* large excluding the terminating null byte. If the output string* would be larger than the rt_log_buf, we have to adjust the output* length. */length = rt_vsnprintf(rt_log_buf, sizeof(rt_log_buf) - 1, fmt, args);if (length > RT_CONSOLEBUF_SIZE - 1)length = RT_CONSOLEBUF_SIZE - 1;
#ifdef RT_USING_DEVICEif (_console_device == RT_NULL){rt_hw_console_output(rt_log_buf);}else{rt_device_write(_console_device, 0, rt_log_buf, length);}
#elsert_hw_console_output(rt_log_buf);
#endif /* RT_USING_DEVICE */va_end(args);return length;
}

主要关注这部分代码:

#ifdef RT_USING_DEVICEif (_console_device == RT_NULL){rt_hw_console_output(rt_log_buf);  // 路径二(备用)}else{rt_device_write(_console_device, 0, rt_log_buf, length);  // 路径一(主路径)}
#endif

可以看到rt_kprintf的输出路径有两个:

1、路径一:通过设备框架输出(RT_USING_DEVICE 已开启时)

RT_USING_DEVICE 这个是默认开启的,咱用RT-Thread肯定得开启这玩意,不开的话那还用个锤子
它是用于开启设备驱动框架功能。

  • _console_device 变量:

这是一个全局指针,指向当前系统的 “控制台设备”
也就是说这个变量决定了我们的打印输出使用的是哪一个串口,因此在系统启动初始化的时候,肯定会在某一个地方设置_console_device这个变量,这个我们后面再去找找。

  • rt_device_write 函数:

RT-Thread 设备框架的标准写接口,作用是向指定设备( _console_device 指向的设备)写入数据。

2、路径二:硬件直接输出(_console_device 未初始化时)

当 _console_device 为 NULL(如设备框架未初始化),输出路径会走这条。

  • rt_hw_console_output 函数:
    这是一个硬件层面的控制台输出函数,直接操作底层硬件(不经过设备框架)。
    在 STM32 中,该函数直接操作 USART1->DR 寄存器(串口一的数据寄存器),将字符串逐个字节发送出去。

不过理论上是这样的,我们点进这个函数的内部:

RT_WEAK void rt_hw_console_output(const char *str)
{/* empty console output */
}
RTM_EXPORT(rt_hw_console_output);

发现它是一个空的弱定义,所以,如果你想使用路径二,那你需要在这个rt_hw_console_output函数内部补充对串口寄存器的操作,这个时候你写的是哪个串口的寄存器那么打印就会使用哪个串口了。或者自己再写一个强定义函数,实现对串口硬件寄存器的操作

当然了,大多数情况都是使用的路径一

二、关键关联:如何与串口一绑定?

当使用路径一时,最终访问到串口一,这是在系统初始化时进行的绑定。
(路径二我们就不看了,路径二你想使用哪个串口你就在rt_hw_console_output这个函数内部使用哪个串口的寄存器就好了)

1、串口一的绑定

在这里插入图片描述
我们先找到这个函数rtthread_startup()
这个函数是操作系统内核的启动入口,负责将系统从裸机状态带入多任务运行状态。

  • rt_hw_board_init 函数:
RT_WEAK void rt_hw_board_init()
{ota_app_vtor_reconfig();extern void hw_board_init(char *clock_src, int32_t clock_src_freq, int32_t clock_target_freq);/* Heap initialization */
#if defined(RT_USING_HEAP)rt_system_heap_init((void *) HEAP_BEGIN, (void *) HEAP_END);
#endifhw_board_init(BSP_CLOCK_SOURCE, BSP_CLOCK_SOURCE_FREQ_MHZ, BSP_CLOCK_SYSTEM_FREQ_MHZ);/* Set the shell console output device */
#if defined(RT_USING_DEVICE) && defined(RT_USING_CONSOLE)rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif/* Board underlying hardware initialization */
#ifdef RT_USING_COMPONENTS_INITrt_components_board_init();
#endif
}

这里面有一句:

#if defined(RT_USING_DEVICE) && defined(RT_USING_CONSOLE)rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif

我们点进RT_CONSOLE_DEVICE_NAME这个就会发现:

#define RT_CONSOLE_DEVICE_NAME "uart1"

rt_console_set_device(RT_CONSOLE_DEVICE_NAME); 就是用于设置系统控制台输出设备的函数
宏定义为 RT_CONSOLE_DEVICE_NAME = “uart1”,则控制台会绑定到名为 “uart1” 的串口设备。
因此,若想要修改打印时用来输出的串口,需要改的就是RT_CONSOLE_DEVICE_NAME 这个宏。

三、串口一在哪配置?

我们初始化一个工程,一上来就能使用串口一去打印输出,并没有对它进行配置,说明系统默认对它进行了配置,我们找一下它在哪。

RT_WEAK void rt_hw_board_init()
{ota_app_vtor_reconfig();extern void hw_board_init(char *clock_src, int32_t clock_src_freq, int32_t clock_target_freq);/* Heap initialization */
#if defined(RT_USING_HEAP)rt_system_heap_init((void *) HEAP_BEGIN, (void *) HEAP_END);
#endifhw_board_init(BSP_CLOCK_SOURCE, BSP_CLOCK_SOURCE_FREQ_MHZ, BSP_CLOCK_SYSTEM_FREQ_MHZ);/* Set the shell console output device */
#if defined(RT_USING_DEVICE) && defined(RT_USING_CONSOLE)rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif/* Board underlying hardware initialization */
#ifdef RT_USING_COMPONENTS_INITrt_components_board_init();
#endif
}

我们进到这个函数内部:
在这里插入图片描述

void hw_board_init(char *clock_src, int32_t clock_src_freq, int32_t clock_target_freq)
{extern void rt_hw_systick_init(void);extern void clk_init(char *clk_source, int source_freq, int target_freq);#ifdef BSP_SCB_ENABLE_I_CACHE/* Enable I-Cache---------------------------------------------------------*/SCB_EnableICache();
#endif#ifdef BSP_SCB_ENABLE_D_CACHE/* Enable D-Cache---------------------------------------------------------*/SCB_EnableDCache();
#endif/* HAL_Init() function is called at the beginning of the program */HAL_Init();/* enable interrupt */__set_PRIMASK(0);/* System clock initialization */clk_init(clock_src, clock_src_freq, clock_target_freq);/* disbale interrupt */__set_PRIMASK(1);rt_hw_systick_init();/* Pin driver initialization is open by default */
#ifdef RT_USING_PINextern int rt_hw_pin_init(void);rt_hw_pin_init();
#endif/* USART driver initialization is open by default */
#ifdef RT_USING_SERIALextern int rt_hw_usart_init(void);rt_hw_usart_init();
#endif}

可以发现在这部分代码的最后对串口进行了初始化。

#ifdef RT_USING_SERIALextern int rt_hw_usart_init(void);rt_hw_usart_init();
#endif

我们进到rt_hw_usart_init() 这个函数内部。

int rt_hw_usart_init(void)
{rt_size_t obj_num = sizeof(uart_obj) / sizeof(struct stm32_uart);struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;rt_err_t result = 0;stm32_uart_get_dma_config();for (int i = 0; i < obj_num; i++){/* init UART object */uart_obj[i].config = &uart_config[i];uart_obj[i].serial.ops    = &stm32_uart_ops;uart_obj[i].serial.config = config;/* register UART device */result = rt_hw_serial_register(&uart_obj[i].serial, uart_obj[i].config->name,RT_DEVICE_FLAG_RDWR| RT_DEVICE_FLAG_INT_RX| RT_DEVICE_FLAG_INT_TX| uart_obj[i].uart_dma_flag, NULL);RT_ASSERT(result == RT_EOK);}return result;
}

rt_hw_usart_init() 这个函数是RT-Thread 中所有使能的串口设备的统一初始化入口

    struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;

这里有一个默认配置,我们看一下这个默认配置是啥:


/* Default config for serial_configure structure */
#define RT_SERIAL_CONFIG_DEFAULT           \
{                                          \BAUD_RATE_921600, /* 921600 bits/s */  \DATA_BITS_8,      /* 8 databits */     \STOP_BITS_1,      /* 1 stopbit */      \PARITY_NONE,      /* No parity  */     \BIT_ORDER_LSB,    /* LSB first sent */ \NRZ_NORMAL,       /* Normal mode */    \RT_SERIAL_RB_BUFSZ, /* Buffer size */  \RT_SERIAL_FLOWCONTROL_NONE, /* Off flowcontrol */ \0                                      \
}

因此系统启动后,当前所有开启的串口都是这个默认配置
在这里插入图片描述
后面在实际使用的时候,如果在不用串口有不同波特率的需求,需要重新更新配置覆盖掉这个默认配置。

    pKnifeMotoDev->Config.baud_rate = BAUD_RATE_115200;pKnifeMotoDev->Config.data_bits = DATA_BITS_8;pKnifeMotoDev->Config.stop_bits = STOP_BITS_1;pKnifeMotoDev->Config.bufsz = 2048;pKnifeMotoDev->Config.parity = PARITY_NONE;res = rt_device_control(pKnifeMotoDev->Dev, RT_DEVICE_CTRL_CONFIG, &pKnifeMotoDev->Config);

比如这样。你在具体的线程里使用串口时,进行串口设备调用的时候,还会进行一次配置,那么串口就会以你这次的配置为准。如果你在启动串口设备时不进行新的配置覆盖,那么它就会使用默认配置,我们串口一使用的就是默认配置。

四、rt_device_find() 究竟是如何找到设备的?

在串口设备使用中,我们会这么写;

    // 1. 查找串口设备uart_dev = rt_device_find(UART_DEVICE_NAME);if (uart_dev == RT_NULL){rt_kprintf("查找 %s 设备失败!\n", UART_DEVICE_NAME);return;}

然后UART_DEVICE_NAME是个宏定义,表示你使用的是串口几。

#define UART_DEVICE_NAME  "uart3"

那就会引起我们的好奇,这个rt_device_find()是如何找设备的。
先来看一下函数内部

rt_device_t rt_device_find(const char *name)
{return (rt_device_t)rt_object_find(name, RT_Object_Class_Device);
}
RTM_EXPORT(rt_device_find);

继续进去

rt_object_t rt_object_find(const char *name, rt_uint8_t type)
{struct rt_object *object = RT_NULL;struct rt_list_node *node = RT_NULL;struct rt_object_information *information = RT_NULL;information = rt_object_get_information((enum rt_object_class_type)type);/* parameter check */if ((name == RT_NULL) || (information == RT_NULL)) return RT_NULL;/* which is invoke in interrupt status */RT_DEBUG_NOT_IN_INTERRUPT;/* enter critical */rt_enter_critical();/* try to find object */rt_list_for_each(node, &(information->object_list)){object = rt_list_entry(node, struct rt_object, list);if (rt_strncmp(object->name, name, RT_NAME_MAX) == 0){/* leave critical */rt_exit_critical();return object;}}/* leave critical */rt_exit_critical();return RT_NULL;
}

1、rt_device_find 的核心原理:遍历链表匹配名称

核心代码在这里:

    /* try to find object */rt_list_for_each(node, &(information->object_list)){object = rt_list_entry(node, struct rt_object, list);if (rt_strncmp(object->name, name, RT_NAME_MAX) == 0){/* leave critical */rt_exit_critical();return object;}}

首先来看这一句:

rt_list_for_each(node, &(information->object_list))  // 遍历该类型对象的链表

先来说它的作用:便利访问链表中的每个节点
那这个链表是啥?这个链表其实就是设备类对象的链表,里面由所有已注册的设备(包括串口、I2C、SPI 等设备)组成的。
为什么这个链表是设备类链表?那当然是在前面实现的,是这句代码:

    information = rt_object_get_information((enum rt_object_class_type)type);

但是它的实现我不想细究了,感兴趣的朋友可以自己研究。
反正我们知道rt_device_find () 里面所查找的链表是设备类链表
其实想想也知道:“这不是废话嘛~一个找设备的函数,那肯定是要去设备链表里面找呀,难不成去线程链表里面找?就好比你要买苹果,你去衣帽区找,这不是扯淡嘛”
现在再来看这一句代码:

        object = rt_list_entry(node, struct rt_object, list);

它的作用:从链表节点(node)找到它所属的对象结构体(struct rt_object)让我们能访问这个对象的名称“name”
我不讨论里面实现的细节,对细节感兴趣的可以自己“偷偷”研究
我们来说说为什么要有这句话?
在前面,假设我们通过便遍历链表获取到了一个链表节点,这个链表节点它是一个对象结构体的成员,这个对象结构体中还有其它成员,比如我们要找的“name”,那现在我们想访问的是这个“name”,但我们只有链表节点的地址,没有这个“name”的地址,这该怎么办?
于是我们通过链表节点的地址进行反推,得到了这个对象结构体的地址,然后我们在通过对象结构体地址+“name”在这个结构体中的偏移量成功访问到了“name”。

  • 用一个通俗的例子来解释:

链表节点(node)就像 “书包上的拉链”,它是书包(对象结构体 struct rt_object)的一部分。
书包上还有 “姓名牌”(name 成员)等其他东西。
当你只抓住了拉链(知道 node 的地址),通过 rt_list_entry 就能找到整个书包(对象结构体的地址),然后就能看到书包上的姓名牌(访问 name 成员)。

再来看最后这一部分代码:

        if (rt_strncmp(object->name, name, RT_NAME_MAX) == 0){/* leave critical */rt_exit_critical();return object;}

我们得到了设备结构体的地址,就可以通过这样object->name访问到name了。
接下来接是将object->name和我们输入的name进行比对,直到匹配上,最后返回目标对象结构体指针object

2、对象结构体的“name”(花名册)从何而来?

我们通过rt_device_find() 传入一个字符串,然后去对应“花名册”去找到相应的设备,那这个“花名册”肯定不是凭空出现的,肯定是在哪里被初始化出来的。

核心在这部分代码里:

int rt_hw_usart_init(void)
{rt_size_t obj_num = sizeof(uart_obj) / sizeof(struct stm32_uart);struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;rt_err_t result = 0;stm32_uart_get_dma_config();for (int i = 0; i < obj_num; i++){/* init UART object */uart_obj[i].config = &uart_config[i];uart_obj[i].serial.ops    = &stm32_uart_ops;uart_obj[i].serial.config = config;/* register UART device */result = rt_hw_serial_register(&uart_obj[i].serial, uart_obj[i].config->name,RT_DEVICE_FLAG_RDWR| RT_DEVICE_FLAG_INT_RX| RT_DEVICE_FLAG_INT_TX| uart_obj[i].uart_dma_flag, NULL);RT_ASSERT(result == RT_EOK);}return result;
}

rt_hw_usart_init()这个函数是RT-Thread 系统的串口硬件初始化的入口函数,它是在系统启动阶段的硬件初始化环节被调用的,具体是在板级初始化过程中被调用的。
这行代码是核心:

        result = rt_hw_serial_register(&uart_obj[i].serial, uart_obj[i].config->name,RT_DEVICE_FLAG_RDWR| RT_DEVICE_FLAG_INT_RX| RT_DEVICE_FLAG_INT_TX| uart_obj[i].uart_dma_flag, NULL);

这里的 uart_obj[i].config->name 就是该串口设备的名称(比如 “uart1”、“uart2”),它会被传入注册函数,最终成为 “花名册” 中的记录。

“花名册”的源头:

        uart_obj[i].config = &uart_config[i];

继续进去uart_config:

static struct stm32_uart_config uart_config[] =
{
#ifdef BSP_USING_UART1UART1_CONFIG,
#endif
#ifdef BSP_USING_UART2UART2_CONFIG,
#endif
#ifdef BSP_USING_UART3UART3_CONFIG,
#endif
#ifdef BSP_USING_UART4UART4_CONFIG,
#endif
#ifdef BSP_USING_UART5UART5_CONFIG,
#endif
#ifdef BSP_USING_UART6UART6_CONFIG,
#endif
#ifdef BSP_USING_UART7UART7_CONFIG,
#endif
#ifdef BSP_USING_UART8UART8_CONFIG,
#endif
#ifdef BSP_USING_LPUART1LPUART1_CONFIG,
#endif
};

这个就是“花名册”的源头
在这里插入图片描述

http://www.dtcms.com/a/321840.html

相关文章:

  • 完整设计 之 运行时九宫格 (太乙九宫 播放器)
  • AI 记忆管理系统:工程实现设计方案
  • 【感知机】感知机(perceptron)学习算法知识点汇总
  • 代码随想录算法训练营第三十八天、三十九天|动态规划part11、12
  • 【LLM开发学习】
  • 小程序实现二维码图片Buffer下载
  • C#结合HALCON去除ROI选中效果的实现方法
  • django uwsgi启动报错failed to get the Python codec of the filesystem encoding
  • 如何永久删除三星手机中的照片?
  • Nestjs框架: 接口安全与响应脱敏实践 --- 从拦截器到自定义序列化装饰器
  • Charles中文版抓包工具功能解析,提升API调试与网络性能优化
  • Redis原理,命令,协议以及异步方式
  • 【数字投影】艺术视觉在展厅中的多维传达与设计创新
  • 【MySQL】初识索引
  • 51c视觉~合集16
  • 批量把在线网络JSON文件(URL)转换成Excel工具 JSON to Excel by WTSolutions
  • NOIP 2024 游记
  • 不同的子序列-二维动态规划
  • GeeLark 7月功能更新回顾
  • 【补题】Codeforces Round 776 (Div. 3) E. Rescheduling the Exam
  • 三方相机问题分析七:【datespace导致GPU异常】三方黑块和花图问题
  • 显示器同步技术终极之战:G-Sync VS. FreeSync
  • xml 格式化
  • 卷板矫平机:把“翘脾气”的金属板材变平整
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘huggingface_hub’问题
  • C# 装箱拆箱
  • 数据结构进阶 详谈红黑树
  • Redis(⑤-线程池隔离)
  • javaSE(基础):5.抽象类和接口
  • C+++——内存管理