Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
内核学习营
《内核学习营》+水一方+再谈rt-thread的设备管理
发布于 2018-10-13 00:51:59 浏览:2515
订阅该版
* 本帖最后由 吉帅虎 于 2018-10-13 10:29 编辑 * 在学习营学习的过程中,对外设的应用试验虽然做完了,但是感觉还不是太清楚。虽然文档有很多,但是文档的专业性比较强,好像更偏重于架构和软件方面的介绍,对于一直跑裸机的童鞋们有点儿困难,而且当前的bsp库中的设备管理部分代码不是很清晰(也可能是我用的这款单片机的bsp里面不清楚,其他的没有看呢),因此文档和代码对应起来还是有些困难,今天又对应文档和代码细细的分析了一下。把心得说一下。 1、首先要说的是就是I/O设备的管理,也就是分层。各个文档里都是按这个图说的。 ![TIM图片20181012223604.png](/uploads/201810/12/223827pmhdiu51dl615c4k.png) 按照这个图来分。对于一个I/O设备的分层最终是五层:最底层就是硬件,也就是电路板,这个其实不应该包含在软件部分说,所以文档里说分三层。三层就不包括上图中的硬件和应用程序。 往上一层就是驱动层,也就是如何操作硬件,比如如何让一个管脚输出高电平,如何让串口输出数据,如何让ADC去读取外部输入的值。这一部分代码最终操作的是硬件,一般都会用到硬件相应的库函数,如STM32里面的HAL库,或者STD库等,代码操作的是寄存器。 再往上是I/O设备管理层,有的文档里称为设备容器。是用来管理设备的。用来在RTT系统中创建相应的设备,并操作相应的设备。这一部分的代码就已经开始于硬件无关了,其功能实现是通过调用下面一层的驱动函数实现的。上图中列出了在同一个对象容器里的不同类型的设备,其实也可以有多个相同类型的设备,比如串口1,串口2,这些对设备管理器来说都是不同的设备。 再往上是I/O设备接口,就是给应用程序调用的函数。比如想控制某个管脚的应该如何调用哪一个函数。 最上面是应用层,就是用户用来执行具体任务的,比如我用一个管脚去控制一个LED,要让LED如何动作,多久亮一次,什么时候亮,都是用户决定的。 从软件角度考虑,我觉得对I/O设备的管理分为四层比较好,除去硬件层,加上应用层。用户需要做的就是编写驱动代码和应用代码。其实驱动代码我相信RTT团队会很快都完善起来的。 2、接下来应该是代码的实现。文档里大部分说的都是I/O设备控制块,是设备管理器是实现机制和操作流程。详见官方文档,我就不再赘述了。 [https://club.rt-thread.org/ask/question/8078.html](https://club.rt-thread.org/ask/question/8078.html) 3、I/O设备的接口,接口部分对应的I/O设备管理的功能,在I/O设备管理器里定义了设备所能执行的全部功能,可以参考上面的官方文档。一般就是打开、关闭、读、写等。 4、以STM32F10x的BSP为例,针对PWM设备代码和上面的分析进行一些分析,同时会结合其他设备的代码进行比较说明 以下观点只是个人在学习过程中的想法,如果有不对的地方请大家多多指正,以免耽误后人学习。 对于比较复杂或者陌生的代码。我习惯从上层往底层分析。 在RTT学习营给出的设备例程代码中,PWM设备的驱动文件为drv_pwm.c,代码如下: ![drv_pwm.c](/uploads/201810/12/232420ghshdzhjpoeovfsf.attach),这里的驱动应该是指硬件驱动,即驱动层的代码。根据一般常识,使用一个外设肯定要在硬件上初始化,查找文件里的初始化部分,很容易找到相应的代码,如下 ``` int rt_hw_pwm_init(void) { int ret = RT_EOK; TIM_TypeDef *TIMx; //定时器x PWM句柄 GPIO_InitTypeDef GPIO_InitStruct; TIM_HandleTypeDef TIMx_Handler; //定时器3PWM句柄 TIM_OC_InitTypeDef TIMx_CHxHandler; //定时器3通道4句柄 TIMx = TIM5; /* add pwm initial. */ __HAL_RCC_TIM5_CLK_ENABLE(); //使能定时器3 __HAL_RCC_GPIOH_CLK_ENABLE(); //开启GPIOB时钟 /* TIM5 channel1 GPIO pin configuration */ GPIO_InitStruct.Pin = GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_HIGH; GPIO_InitStruct.Alternate = GPIO_AF2_TIM5; HAL_GPIO_Init(GPIOH, &GPIO_InitStruct); /* TIM5 channel2 GPIO pin configuration */ GPIO_InitStruct.Pin = GPIO_PIN_11; HAL_GPIO_Init(GPIOH, &GPIO_InitStruct); /* TIM5 channel3 GPIO pin configuration */ GPIO_InitStruct.Pin = GPIO_PIN_12; HAL_GPIO_Init(GPIOH, &GPIO_InitStruct); TIMx_Handler.Instance = TIMx; TIMx_Handler.Init.Prescaler = 12-1; //SystemCoreClock/180 = 1MHz TIMx_Handler.Init.CounterMode = TIM_COUNTERMODE_UP; //向上计数模式 TIMx_Handler.Init.Period = 256-1; //自动重装载值 TIMx_Handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&TIMx_Handler); //初始化PWM TIMx_CHxHandler.OCMode = TIM_OCMODE_PWM1; //模式选择PWM1 TIMx_CHxHandler.Pulse = 20; //设置比较值,此值用来确定占空比,默认比较值为自动重装载值的一半,即占空比为50% TIMx_CHxHandler.OCPolarity = TIM_OCPOLARITY_LOW; //输出比较极性为低 HAL_TIM_PWM_ConfigChannel(&TIMx_Handler, &TIMx_CHxHandler, TIM_CHANNEL_1); //配置TIMx通道1 HAL_TIM_PWM_ConfigChannel(&TIMx_Handler, &TIMx_CHxHandler, TIM_CHANNEL_2); //配置TIMx通道2 HAL_TIM_PWM_ConfigChannel(&TIMx_Handler, &TIMx_CHxHandler, TIM_CHANNEL_3); //配置TIMx通道3 ret = rt_device_pwm_register(&_stm32_pwm_device.parent, "pwm", &pwm_ops, TIMx); return ret; }```我们通过分析代码不难发现。这段代码的大部分都是硬件的初始化配置,用的是STM32官方的HAL库,如果使用其他的库的话替换掉相应的库即可,这一部分代码和我们用裸机写代码的时候是一样的,习惯用什么库就用什么库。与裸跑代码不同的是这一句ret = rt_device_pwm_register(&_stm32_pwm_device.parent, "pwm", &pwm_ops, TIMx);结合文档的说明我们知道,这一句是用来在I/O设备管理器里面注册PWM设备的,其中设备名为pwm。我们可以注册pwm2或者其他名称的pwm设备。stm32F10x系列最多有十几个定时器,如果每个定时器作为一个pwm设备。最多可以有十几个pwm设备,他们具备相同的硬件特征和操作方法。当然我觉得也可以把多个pwm设备当成一个pwm设备,然后通过通道区分。但是这样会增加代码难度,实际使用中用到的也很少, 因此就没有再考虑。通过注册这一个过程,代码就切换到了管理层。但是驱动层的代码并没有完成,因为到现在我们还不知道如何控制某一路PWM输出指定的波形。 对比I/O设备管理的文档,我们发现在注册设备的时候我们用到的是rt_device_pwm_register()函数。而不是文档里的rt_device_register()函数。rt_device_pwm_register()的代码如下: ``` rt_err_t rt_device_pwm_register(struct rt_device_pwm *device, const char *name, const struct rt_pwm_ops *ops, const void *user_data) { rt_err_t result = RT_EOK; memset(device, 0, sizeof(struct rt_device_pwm)); device->parent.type = RT_Device_Class_Miscellaneous; device->parent.init = RT_NULL; device->parent.open = RT_NULL; device->parent.close = RT_NULL; device->parent.read = _pwm_read; device->parent.write = _pwm_write; device->parent.control = _pwm_control; device->ops = ops; device->parent.user_data = (void *)user_data; result = rt_device_register(&device->parent, name, RT_DEVICE_FLAG_RDWR); return result; }```通过分析代码,我们知道rt_device_pwm_register()是对rt_device_register()函数的进一步封装,封装的原因是标准设备注册函数无法完成pwm设备 一些参数,因为不同的设备的操作方法和参数结构均不相同,因为对不同的设备操作参数可以进行再次的封装。rt_device_register()函数属于RTT外设管理器的核心部分代码,具有通用性。可以暂时不去了解其实现方法,先记住所有的设备都一样就行了。在深入学习的时候再去了解。先说对设备的再次封装,既然注册函数进行了再次封装,那么外设的结构肯定也再次进行了封装,其实我们从注册的时候的 参数就可以知道,传入的参数是struct rt_device_pwm 结构,而不是 struct rt_device结构。通过查找发现struct rt_device_pwm的结构如下。比标准的rt_device结构多了一个ops,ops的全称是operations,意思是操作,即pwm设备比通用设备多了一个操作接口。(这个是管理员回帖中说明的,我再重新编辑到帖子里的。在此对管理员[yqiu](https://www.rt-thread.org/qa/space-uid-10961.html)表示感谢) ```struct rt_device_pwm { struct rt_device parent; const struct rt_pwm_ops *ops; };```OPS的结构如下,即定义了一个control函数的指针。这个控制函数的入口参数包括pwm设备句柄,命令(实现何种操作),参数指针```struct rt_pwm_ops { rt_err_t (*control)(struct rt_device_pwm *device, int cmd, void *arg); };```再次看rt_device_pwm_register(&_stm32_pwm_device.parent, "pwm8", &pwm_ops, TIM8);调用的时候传入的参数包括_stm32_pwm_device.parent的指针,设备名称,ops指针,定时器序号TIM8(根据宏定义可以得知是一个指针,即TIM8寄存器的首地址,在此函数中的意义是一个用户参数)。这里又多了一个设备结构,即_stm32_pwm_device,查找这个定义,结果如下,显然是对pwm设备的再次封装```struct rt_stm32_pwm { struct rt_device_pwm parent; rt_uint32_t period[PWM_CHANNEL_MAX]; rt_uint32_t pulse[PWM_CHANNEL_MAX]; };```至此,在标准rt-device的基础上,先加上了操作接口构成了rt_device_pwm,再加上了PWM的具体参数,周期和占空比。构成了最终的stm32_pwm设备.这也是大部分pwm设备的常用参数和控制方法。 了解了这些,我们依然不了解如何控制相应的通道输出PWM波形。通过最终的pwm设备的结构,我们知道现在用到的pwm设备在标准的基础上增加了控制函数和占空比,周期两个参数。这些参数的初始化尤为重要,这个过程自然是 在设备的注册阶段。回到上面的 rt_device_pwm_register函数。这个函数中首先为pwm初始化了内存,定义了设备类型为RT_Device_Class_Miscellaneous(杂类)。分配了三个公共函数的指针,其中init,open,close功能没有赋值,即这三个功能无效。有效的功能为read,write和control,对应的函数是_pwm_read、_pwm_write,_pwm_control,另外传递了ops指针和用户数据(此应用中为TIM8的指针)。之后进行了注册。注意这里传递是PWM设备指针,而不是stm32-pwm设备,因此不需要对周期和占空比进行设置。 由于pwm设备有三个函数接口,因为需要我们编写read,write,pwm_pwm_control三个函数。 三个函数的代码如下 ``` static rt_err_t _pwm_control(rt_device_t dev, int cmd, void *args) { rt_err_t result = RT_EOK; struct rt_device_pwm *pwm = (struct rt_device_pwm *)dev; if (pwm->ops->control) { result = pwm->ops->control(pwm, cmd, args); } return result; } /* pos: channel void *buffer: rt_uint32_t pulse size : number of pulse, only set to sizeof(rt_uint32_t). */ static rt_size_t _pwm_read(rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size) { rt_err_t result = RT_EOK; struct rt_device_pwm *pwm = (struct rt_device_pwm *)dev; rt_uint32_t *pulse = (rt_uint32_t *)buffer; struct rt_pwm_configuration configuration = {0}; configuration.channel = pos; if (pwm->ops->control) { result = pwm->ops->control(pwm, PWM_CMD_GET, &configuration); if (result != RT_EOK) { return 0; } *pulse = configuration.pulse; } return size; } /* pos: channel void *buffer: rt_uint32_t pulse size : number of pulse, only set to sizeof(rt_uint32_t). */ static rt_size_t _pwm_write(rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size) { rt_err_t result = RT_EOK; struct rt_device_pwm *pwm = (struct rt_device_pwm *)dev; rt_uint32_t *pulse = (rt_uint32_t *)buffer; struct rt_pwm_configuration configuration = {0}; configuration.channel = pos; if (pwm->ops->control) { result = pwm->ops->control(pwm, PWM_CMD_GET, &configuration); if (result != RT_EOK) { return 0; } configuration.pulse = *pulse; result = pwm->ops->control(pwm, PWM_CMD_SET, &configuration); if (result != RT_EOK) { return 0; } } return size; } ```三个函数都包含result = pwm->ops->control(pwm, PWM_CMD_SET, &configuration);说明pwm设备的最终操作是通过ops里的control函数实现的(pwm的ops里只有这一个函数,其他设备应该可以包含其他函数,即操作方法)。接下来需要查找ops->control的函数原型,原型如下:``` static rt_err_t control(struct rt_device_pwm *device, int cmd, void *arg) { rt_err_t result = RT_EOK; // TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; rt_kprintf("drv_pwm.c control cmd: %d.
", cmd); if (cmd == PWM_CMD_ENABLE) { struct rt_pwm_configuration *cfg; // uint32_t TIMx_channel= 0; TIM_TypeDef *TIMx; //定时器x PWM句柄 // TIM_HandleTypeDef TIMx_Handler; TIMx = (TIM_TypeDef *)device->parent.user_data; // TIMx_Handler.Instance = TIMx; cfg = (struct rt_pwm_configuration *)arg; if ((cfg->channel > PWM_CHANNEL_MAX)||(cfg->channel < 1)) { result = -RT_EIO; return result; } // TIMx_channel = (cfg->channel-1)*4; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; switch(cfg->channel) { case 1: { TIM_OC1Init(TIMx, &TIM_OCInitStructure); }break; case 2: { TIM_OC2Init(TIMx, &TIM_OCInitStructure); }break; case 3: { TIM_OC3Init(TIMx, &TIM_OCInitStructure); }break; case 4: { TIM_OC4Init(TIMx, &TIM_OCInitStructure); }break; default: break; } rt_kprintf("PWM_CMD_ENABLE
"); } else if (cmd == PWM_CMD_SET) { rt_kprintf("PWM_CMD_SET
"); result = set(device, (struct rt_pwm_configuration *)arg); } else if (cmd == PWM_CMD_GET) { rt_kprintf("PWM_CMD_GET
"); result = get(device, (struct rt_pwm_configuration *)arg); } return result; } ```看到这里,我们又发现了与硬件寄存器相关的操作代码,很显然,这里就是最终对应外设的地方,即如何实现对应的管脚输出相应的PWM波形。(管脚序号在rt_hw_pwm_init里确定了,这里的代码TIM_OC4Init()等函数确定了上层参数中相关设置具体在那个通道上输出)。此函数中一共定义了三个指令PWM_CMD_ENABLE,PWM_CMD_SET,PWM_CMD_GET,实现了对pwm的控制(调整占空比,周期等)和读写功能。 接下来是操作接口,即API函数。例程中给代码如下 ``` #ifdef RT_USING_FINSH #include
FINSH_FUNCTION_EXPORT_ALIAS(rt_pwm_enable, pwm_enable, enable pwm by channel.); FINSH_FUNCTION_EXPORT_ALIAS(rt_pwm_set, pwm_set, set pwm.); #ifdef FINSH_USING_MSH static int pwm_enable(int argc, char **argv) { int result = 0; if (argc != 2) { rt_kprintf("Usage: pwm_enable 1
"); result = -RT_ERROR; goto _exit; } result = rt_pwm_enable(atoi(argv[1])); _exit: return result; } MSH_CMD_EXPORT(pwm_enable, pwm_enable 1); static int pwm_set(int argc, char **argv) { int result = 0; if (argc != 4) { rt_kprintf("Usage: pwm_set 1 100 50
"); result = -RT_ERROR; goto _exit; } result = rt_pwm_set(atoi(argv[1]), atoi(argv[2]), atoi(argv[3])); _exit: return result; } MSH_CMD_EXPORT(pwm_set, pwm_set 1 100 50); #endif /* FINSH_USING_MSH */ ```上面的API接口是在FINSH里可以操作的两个函数,pwm_enable使能相应的通道,pwm_set设置占空比和周期。这两个函数里对PWM的控制分别是rt_pwm_enable函数和rt_pwm_set。追根溯源,rt_pwm_enable的原型如下, ```rt_err_t rt_pwm_enable(int channel) { rt_err_t result = RT_EOK; struct rt_device *device = rt_device_find("pwm"); struct rt_pwm_configuration configuration = {0}; if (!device) { return -RT_EIO; } configuration.channel = channel; result = rt_device_control(device, PWM_CMD_ENABLE, &configuration); return result; } rt_err_t rt_device_control(rt_device_t dev, int cmd, void *arg) { RT_ASSERT(dev != RT_NULL); RT_ASSERT(rt_object_get_type(&dev->parent) == RT_Object_Class_Device); /* call device write interface */ if (device_control != RT_NULL) { return device_control(dev, cmd, arg); } return -RT_ENOSYS; } #define device_control (dev->control)``` 可以看出,rt_pwm_enable函数的最终实现也是设备控制块中的_pwm_control函数实现,即注册的时候指定的三个函数之一。同样的方法,我们也可以找出rt_pwm_set函数的原型也是_pwm_control函数。这两个函数的参数是为了适应FINSH而设置的,我们在自己的应用中可以修改为传递其他参数,比如rt_err_t rt_device_control(rt_device_t dev, int cmd, void *arg)传递是参数是以指针形式传递的,导出的指令为MSH_CMD_EXPORT(pwm_set, pwm_set 1 100 50);实际使用中是在调试助手里输入的字符串,格式为pwm_set 1 100 50。这样的代码在应用程序层调用的时候就比较麻烦,我们可以增加传递uint8类型的参数或者参数指针。 至此,我们对pwm设备的结构和用法应该有了一个比较透彻的了解。希望大家多提宝贵意见,方便大家的学习 最终对RTT设备管理的代码提一些建议 1、代码分层问题:既然官方文档里对I/O设备管理分成了三层,那么代码为什么不分成三层。代码与介绍的文档无法快速的一一对应,刚开始学习起来有些吃力。也许对类似的代码结构熟悉的人很快就了解了,但是对于一直裸跑的工程师或者刚入门的学生来说有点儿乱,不容易入门。就像我已经完成了多个设备的实验,但是依然无法清楚的把代码和文档对应起来,对代码理解还是很吃力。和学习营的其他同学沟通过,有些人也有类似的问题。 2、代码命名规范问题:总的来说,RTT的代码命名还是比较规范的,本次学习营提供的设备驱动文件均已drv_开头。但是文件里的内容却不大一样,比如pwm设备的注册函数原型位于rt_drv_pwm.c文件里面。但是ADC设备的注册函数则位于drv_adc.c中。另外pin设备和串口设备则没有以drv_开头。而spi设备不同层的代码的所处的文件名结构不一致,这也导致入门学习时的一些疑惑,有误导协议。也许我用的文件不是最新的,如过最新的文档已经修改过,希望回帖告知一下。 3、BSP里包含的设备代码问题。bsp里最基本的一般都包含led驱动和串口代码。当前的bsp的代码对这两种基本设备的代码分层也不是很清晰,文件命名不规范,甚至API接口使用上都比较混乱。以LED为例,led的应用代码如下: ``` void rt_hw_led_on(rt_uint32_t n) { switch (n) { case 0: GPIO_SetBits(led1_gpio, led1_pin); break; case 1: GPIO_SetBits(led2_gpio, led2_pin); break; default: break; } } void rt_hw_led_off(rt_uint32_t n) { switch (n) { case 0: GPIO_ResetBits(led1_gpio, led1_pin); break; case 1: GPIO_ResetBits(led2_gpio, led2_pin); break; default: break; } }```这里对led管脚的驱动使用的STD库的接口函数,而没有使用rt设备的接口rt_pin_write();如果在最基本的设备上能够做到分层清晰,学习起来也会更新快捷。 4、尽快善不同的芯片和不同库的BSP,这个确实有很大的工作量,我就这么一说。:lo
查看更多
2
个回答
默认排序
按发布时间排序
yqiu
2018-10-13
这家伙很懒,什么也没写!
[i=s] 本帖最后由 yqiu 于 2018-10-13 09:55 编辑 [/i] 点赞! 1. ops 是 operations 的缩写; 2. 关于 3 点建议很好,是目前 BSP 的痛点,这部分都会在即将进行的 BSP 重构中考虑: * 代码分层问题 * 代码命名规范问题 * BSP里包含的设备代码问题 你也来一起参与吧。
q80351951
2019-02-16
这家伙很懒,什么也没写!
[i=s] 本帖最后由 q80351951 于 2019-2-16 01:38 编辑 [/i] 这篇文章研究了半天,写的很好,但我的版本是 \ | / - RT - Thread Operating System / | \ 4.0.1 build Feb 15 2019 2006 - 2019 Copyright by rt-thread team ![](https://www.rt-thread.org/qa/data/attachment/forum/201902/15/143454xghxi5kakl5fagw5.png) 由于RT-Thread_env里面没有PWM2_CH2 ,PWM2_CH3 ,所以手动添加了 #define BSP_USING_PWM2_CH3 #define BSP_USING_PWM2_CH2 文件 rtconfig.h现在是这个样子 /* On-chip Peripheral Drivers */ #define BSP_USING_GPIO #define BSP_USING_UART #define BSP_USING_UART1 #define BSP_USING_TIM #define BSP_USING_TIM11 #define BSP_USING_PWM #define BSP_USING_PWM2 #define BSP_USING_PWM2_CH4 #define BSP_USING_PWM2_CH3 //手动添加的 #define BSP_USING_PWM2_CH2//手动添加的 #define BSP_USING_ADC #define BSP_USING_ADC1 现在只有CH4可以工作。 对这个驱动还没搞太明白,还有哪里需要改动? 问题是TIM2的CH2,CH3,没有输出,按文章分析在我源码里找不到 static rt_err_t control(struct rt_device_pwm *device, int cmd, void *arg) 这个函数。只找到了下面个这个 struct rt_device_pwm; struct rt_pwm_ops { rt_err_t (*control)(struct rt_device_pwm *device, int cmd, void *arg); }; 本人水平欠佳,对C结构体指针函数理解不够深,犯晕,没搞懂这个函数到底是怎么回事。
撰写答案
登录
注册新账号
关注者
0
被浏览
2.5k
关于作者
吉帅虎
这家伙很懒,什么也没写!
提问
12
回答
12
被采纳
0
关注TA
发私信
相关问题
1
【内核学习】rtthread内核移植记录-STM32F103ZET6-HAL库
2
《内核学习营》+水一方+自用STM32F103VC 板RT-Thread内核移植分享
3
《内核学习营》+水一方+项目中创建标准的 RT-Thread工程
4
内核学习营+坦然+探索者stm32f407板子RT-thread循环点亮led灯
5
<内核学习营>+坦然+探索者stm32f407板子RT-thread串口字符点灯
6
<内核学习营>+坦然+探索者stm32f407板子RT-thread的pwm点灯实验
7
<内核学习营>+坦然+探索者stm32f407板子RT-thread串口实验
8
<内核学习营>+坦然+野火stm32f103板子RT-thread读写SD卡实验
9
<内核学习营>+坦然+探索者stm32f407板子RT-thread的RTC闹钟实验
10
【内核学习营】+王秀峰+led_rgb
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
最新文章
1
在用clangd开发RTT吗,快来试试如何简单获得清晰干净的工作区
2
GD32F450 片内 flash驱动适配
3
STM32H7R7运行CherryUSB
4
RT-Smart首次线下培训,锁定2024 RT-Thread开发者大会!
5
使用RC522软件包驱动FM1722
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
UART
WIZnet_W5500
ota在线升级
freemodbus
PWM
flash
cubemx
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
flashDB
GD32
socket
中断
编译报错
Debug
SFUD
rt_mq_消息队列_msg_queue
msh
keil_MDK
ulog
C++_cpp
MicroPython
本月问答贡献
a1012112796
10
个答案
1
次被采纳
踩姑娘的小蘑菇
4
个答案
1
次被采纳
红枫
4
个答案
1
次被采纳
张世争
4
个答案
1
次被采纳
Ryan_CW
4
个答案
1
次被采纳
本月文章贡献
catcatbing
3
篇文章
6
次点赞
YZRD
2
篇文章
5
次点赞
qq1078249029
2
篇文章
2
次点赞
xnosky
2
篇文章
1
次点赞
Woshizhapuren
1
篇文章
5
次点赞
回到
顶部
发布
问题
分享
好友
手机
浏览
扫码手机浏览
投诉
建议
回到
底部