基于RT-Thread的STM32开发第一讲——USART
文章目录
- 前言
- 一、新建一个RT-Thread 项目
- 二、代码开发
- 1.board.h
- 2.usart_dma.c
- 3.usart_dma.h
- 4.main.h
- 三、uart v2对比
- 总结
前言
之前本人一直都是使用keil搭配STM32cubemx使用LL库来开发STM32,但随着使用,发现裸机编程对于集成大的工程越来越费劲,经常几天不看,自己都忘了为啥这么写。于是就想使用操作系统来开发。
于是我就去了解嵌入式系统中常用的实时操作系统(RTOS),发现使用较多是以下三种——RT-Thread、FreeRTOS、uC/OS 。我并没有过多去比较它们的优缺点,就冲着RT-Thread是唯一国产的且开源,而且是最晚面世的(本人就喜欢用新东西),就选择它了。
这里说一下对于没接触过操作系统如何开始学习。
我比较了比较多的RT-Thread学习书籍,推荐新手使用这本《RT-Thread实时操作系统——内核、驱动和应用开发技术》刘苗秀、沈鸿飞、廖建尚编著。搭配官网给的手册学习,你会发现书上内容都是手册里有的,直接看手册学也行,但本人喜欢用纸质书。
还有一本书《RT-Thread内核实现与运用开发实战指南——基于stm32》,这个不建议新手使用,他是想叫你复现一遍RT-Thread操作系统搭建,整本书都是讲内核的,很难短时间形成生产力,过于理论化了。
学习的时候看完一章一定要实现一下章节末尾的开发验证,这是形成生产力最宝贵的积累,学习内核的时候可能觉得繁琐了,不涉及具体的功能模块实现,但是请一定要坚持学完,后面各驱动模块的编写都会用到内核的知识。要多结合官网手册,官网手册很多解释更加全面。
按我的感知,之前没学过keil和STM32cubemx,甚至没接触过stm32的同学,也可以直接学习RT-Thread,因为操作系统对硬件进行了隐藏,所以不需要太多硬件知识,但是还是建议结合讲解cortex-mx处理器的书一起使用学习,了解一些硬件知识。当然,我觉得最好的转化是先有很多裸机开发经验,并且对芯片各外设的硬件逻辑有很好的了解再学习使用操作系统比较好。
说了那么多“题外话”,无非是不想大家走弯路,但这也是个人经验,大家借鉴即可。下面开始进入主题。
本章除了功能实现还将会说明一些本人学到的经验。
一、新建一个RT-Thread 项目
项目使用的是rt-thread标准版本,开发板使用的是正点原子探索者,mcu是stm32f407zgt6。开发环境使用的是rt-thread studio。
使用到了外设,就当大家对内核已经有所学习了,这里就不放新建工程的步骤了,只对于一些步骤进行说明。
RT-thread驱动版本看一下SDK管理器芯片下载说明,这里明确说明了stm32f4的0.2.3版本需要驱动4.1.0版本,那就按照要求来。
控制台串口选择一个方便的用来串口通信,注意最后一句初始时使用的是HSI,需要修改。
使用STM32cubemx生成一个对应的芯片型号的工程,只需要配置时钟数,其他的不需要配置,然后生成keil工程(注意用hal生成),将keil工程mian.c文件的函数system_clock_confign替换rt-thread工程里的drv_clk.c里的system_clock_confign函数,注意只替换内容,函数入口参数保留,如图。
然后打开drivers/stm32f4xx_hal_conf.h,将/* ########################## HSE/HSI Values adaptation ##################### */内容的各时钟值换成实际大小,默认的是HSE:8M,LSE:32.768K。
还有大家以前可能注意时钟频率有一个8分频的选择,在STM32cubemx时钟树上有这个选择配置,在代码中具体是由systick->CTRL寄存器的第三位CLKSOURCE配置的,这里说明一下。在system_clock_confign函数中不包括这部分内容配置,具体设置函数之前我找到过,比较难找,结论与STM32cubemx时钟树配置无关,系统都默认使用HCLK时钟频率作为systick延时频率。
接着新建文件usart.c和usart.h。用来存放用户工程,如何添加头文件路径就不介绍了。编译不出错工程就建立完成了。
二、代码开发
1.board.h
#define BSP_USING_UART1
#define BSP_UART1_TX_PIN "PA9"
#define BSP_UART1_RX_PIN "PA10"#define BSP_UART3_RX_USING_DMA //如果用DMA接收,则添加
#define BSP_UART3_TX_USING_DMA //如果用DMA发送,则添加
#define BSP_USING_UART3
#define BSP_UART3_TX_PIN "PB10"
#define BSP_UART3_RX_PIN "PB11"
需要把用到的串口引脚号添加进去,其中PA9和PA10是新建工程用来终端交互用的,系统自动添加了,我们按格式一样添加。
如果使用DMA,还需要打开RT-Thread setting,配置串口,打开DMA,如图。
如果以后有些功能代码都配置正确但无法使用,就有可能是这里配置没开。
官方文档有uart v2,对应的就是这里的choice serial version
选择,这里现用uart v1也就是原版,说明一下,uart v2目前官方适配及其有限,好像只有一个型号的mcu,其他型号都需自行适配,需要修改的地方也有一点,弄不好还会出现各种问题,新手不建议操作,至于uart v1的不足后面我会解释一下。
在drv_usart.c文件里有很多预编译命令,如
#ifdef BSP_USING_UART1UART1_INDEX,
#endif
就是定义了BSP_USING_UART1就能使用UART1_INDEX,这也是为什么需要在board.h宏定义BSP_USING_UART3,其他宏定义原理一样。
2.usart_dma.c
这是自定义的文件,里面就做两件事——初始化和回调函数。
先放完整代码,再讲解。使用的是DMA接收和发送
/** Copyright (c) 2006-2021, RT-Thread Development Team** SPDX-License-Identifier: Apache-2.0** Change Logs:* Date Author Notes* 2025-04-29 H1567 the first version*/
#include "usart_dma.h"#define UART_name "uart3"rt_device_t u3DMA_handle;
rt_mq_t mq_u3DMA;
static rt_err_t u3DMA_rx_entry(rt_device_t dev, rt_size_t size);
static void u3DMA_thread_entry(void *parameter);
int u3DMA_init(void)
{char str[] = "uart3_DMA init success\n";struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;u3DMA_handle = rt_device_find(UART_name);if(u3DMA_handle == RT_NULL){rt_kprintf("failed to uart3_DMA handle find\n");return -1;}if(rt_device_control(u3DMA_handle,RT_DEVICE_CTRL_CONFIG, &config) != RT_EOK){rt_kprintf("failed to uart3_DMA control config\n");return -1;}if(rt_device_open(u3DMA_handle,RT_DEVICE_FLAG_DMA_RX|RT_DEVICE_FLAG_DMA_TX) != RT_EOK){rt_kprintf("failed to uart3_DMA device open\n");return -1;}if(rt_device_set_rx_indicate(u3DMA_handle, u3DMA_rx_entry) != RT_EOK){rt_kprintf("failed to uart3_DMA rx_indicate\n");return -1;}rt_device_write(u3DMA_handle, 0, str, sizeof(str));rt_thread_mdelay(1);return 0;
}
int message_queue_init(void)
{static char mq_u3DMA_data[50];mq_u3DMA = rt_mq_create("mq_u3DMA", 4, sizeof(mq_u3DMA_data), RT_IPC_FLAG_PRIO);if(mq_u3DMA == RT_NULL){rt_kprintf("failed to message queue u3DMA create\n");return -1;}return 0;
}
int u3DMA_thread_init(void)
{rt_thread_t u3DMA_thread;u3DMA_thread = rt_thread_create("u3DMA_thread", u3DMA_thread_entry, RT_NULL, 1024, 12, 20);if(u3DMA_thread == RT_NULL){rt_kprintf("failed to u3DMA_thread create\n");return -1;}if(rt_thread_startup(u3DMA_thread) != RT_EOK){rt_kprintf("failed to u3DMA_thread startup\n");return -1;}return 0;
}
static rt_err_t u3DMA_rx_entry(rt_device_t dev, rt_size_t size)
{if(rt_mq_send(mq_u3DMA, &size, sizeof(size)) !=RT_EOK){rt_kprintf("failed to u3DMA message queue send\n");return -1;}return 0;
}
static void u3DMA_thread_entry(void *parameter)
{rt_size_t u3DMA_rx_size;char u3DMA_rx_data[65] = {0};while(1){rt_mq_recv(mq_u3DMA, &u3DMA_rx_size, sizeof(u3DMA_rx_size), RT_WAITING_FOREVER);if(rt_device_read(u3DMA_handle, 0, u3DMA_rx_data, u3DMA_rx_size) != 0){u3DMA_rx_data[u3DMA_rx_size] = '\n';rt_device_write(u3DMA_handle, 0, u3DMA_rx_data, u3DMA_rx_size+1);}}
}
2.1 初始化串口
流程不讲了,看手册就行,说一下这一句
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;
这里静态声明了一个serial_configure结构体,用来配置串口uart3,看结构体声明
struct serial_configure
{rt_uint32_t baud_rate;rt_uint32_t data_bits :4;rt_uint32_t stop_bits :2;rt_uint32_t parity :2;rt_uint32_t bit_order :1;rt_uint32_t invert :1;rt_uint32_t bufsz :16;rt_uint32_t flowcontrol :1;rt_uint32_t reserved :5;
};
包括了常用的波特率、数据位…等等。然后使用系统默认预定义RT_SERIAL_CONFIG_DEFAULT初始化
#define RT_SERIAL_CONFIG_DEFAULT \
{ \BAUD_RATE_115200, /* 115200 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 \
}
默认初始化后可以修改结构体config里的参数适合开发需要。然后把创建好的结构体config给rt_device_control函数实现初始化
if(rt_device_control(u3DMA_handle,RT_DEVICE_CTRL_CONFIG, &config) != RT_EOK){rt_kprintf("failed to uart3_DMA control config\n");return -1;}
这里面所有的rt_kprintf都是异常输出。最后为什么延时1ms呢,这里后面解释。
初始化消息列表和线程很平常,没啥好说的。
2.2 接收回调函数
这里也很简单,若串口以中断接收模式打开,当串口接收到一个数据产生中断时,就会调用回调函数,并且会把此时缓冲区的数据大小放在 size 参数里,把串口设备句柄放在 dev 参数里供调用者获取。若串口以 DMA 接收模式打开,当 DMA 完成一批数据的接收后会调用此回调函数。机制就类似中断
然后在回调函数将接收到的缓冲区的数据大小发送到消息列表中
注意在drv_usart.c文件中下面两句函数
HAL_UART_RxCpltCallback
HAL_UART_RxHalfCpltCallback
一个是DMA全满中断,一个是DMA半满中断,我们初始化时接收缓冲区是64个字节,那么接收累计满32个字节,调用一次接收回调函数(半满中断),满64个字节,调用一次接收回调函数(全满中断),大部分时候是不需要的,所以那就注释掉,这样只留下了空闲中断。注释如下
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{//struct stm32_uart *uart;RT_ASSERT(huart != NULL);//uart = (struct stm32_uart *)huart;//dma_isr(&uart->serial);
}
另外一个一样。
2.3 线程入口函数
注意啊,如果一个线程入口函数运行结束退出后,这个线程就被系统回收了,无法再使用这个线程,想要使用就只能重新初始化,所以线程里都放一个死循环,保证线程始终运行。因为rtthread 的线程挂起机制,只要在死循环里有挂起动作,这个死循环就不会使系统卡死,挂起后,系统自动去运行其他线程。mian线程使系统初始化的,所以一定不要忘了main函数里的死循环放延时函数。
再这里我始终等待消息列表的消息,等待过程中系统干其他活,一旦等到了立马回来,是不是很方便,将接收缓存区的数据接收到一个数组中,再加一个换行,再利用rt_device_write发送出去。这样就完成了。
后面把函数声明在.h文件里。然后再main.c里运用就行了。如下
3.usart_dma.h
/** Copyright (c) 2006-2021, RT-Thread Development Team** SPDX-License-Identifier: Apache-2.0** Change Logs:* Date Author Notes* 2025-04-29 H1567 the first version*/
#ifndef APP_USART_DMA_H_
#define APP_USART_DMA_H_#include <board.h>
#include <rtthread.h>int u3DMA_init(void);
int message_queue_init(void);
int u3DMA_thread_init(void);#endif /* APP_USART_DMA_H_ */
4.main.h
/** Copyright (c) 2006-2025, RT-Thread Development Team** SPDX-License-Identifier: Apache-2.0** Change Logs:* Date Author Notes* 2025-04-28 RT-Thread first version*/#include <rtthread.h>#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include <rtdbg.h>
#include "usart_dma.h"int main(void)
{u3DMA_init();message_queue_init();u3DMA_thread_init();while (1){rt_thread_mdelay(1000);}return RT_EOK;
}
这样工程就完成了,工程里运用uart1作为控制台串口,用uart3作为用户串口,实现DMA的接收与发送。
三、uart v2对比
想要深入了解uart的使用和uart v1与uart v2的区别建议去看看下面的文章
串口框架V1和V2版本对比
作者串口V2版本的开发者,内容是非常权威的。作者提到了v1版本的三个问题
1.发送模式不够完善:
串口驱动中,发送中断模式未发挥出中断应有的特性,造成使用中断时,仍然需要浪费大量CPU时间,影响系统整体性能。2.DMA发送使用不当容易丢包:
当写数据为轮询或者中断时,调用rt_device_write正确返回后即代表数据发送完成。而使用DMA时,调用rt_device_write返回后仅仅代表数据准备完毕,并不能代表数据发送完成,如果用户在正确返回后再次调用rt_device_write,将有可能出现上次数据未发送完成,数据又被修改的发送数据错误的问题 。
3.不同模式时,影响应用层执行逻辑:
结合第二点再引申下来这个问题:当使用中断和轮询时,发送模式为阻塞模式,使用DMA时,发送模式为非阻塞模式,且未对数据块进行保护。另外不仅仅是发送端的考虑,接收端也有可能会出现阻塞和非阻塞的模式选择问题。因此框架层应该更多关心阻塞非阻塞的操作模式,使得应用层执行流程能够统一。———————————————— 版权声明:本文为RT-Thread论坛用户「123」的原创文章,遵循CC 4.0
BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://club.rt-thread.org/ask/article/8e1d18464219fae7.html
第一点意思是不管是中断发送还是轮询发送都是以轮询发送进行,意思就是这两种模式发送过程中一直占用系统资源,发送完成前无法进行其他动作,这一点会稍微破坏系统的实时性,但我觉得只要一次发送的数据不大,这一点时间占用无伤大雅。如果真的要发送大量数据,就用DMA方式。
第二点意思就是,用DMA发送时,系统显示发送完成,也就是进入发送回调函数那一刻,系统只是把发送数据的首地址传输到发送端了,数据还没来得及发送。也就是说,如果再rt_device_write运行后立马更新发送数据的内容会导致数据紊乱,这也就是为什么我前面u3DMA_init函数末尾要加一句1ms延时的原因,如果不加,那么rt_device_write运行结束后函数立马退出,那么声明好的局部字符数组会失效,从而发送的数据也就变成乱码。如果把字符数组声明为全局变量,那就不用延时了。
总之来说就是在DMA发送时,要保证发送的数据在rt_device_write运行结束时开始维持到数据真正传输完毕,所需要保留的时间根据波特率去算,也可以自己编写保留机制,这个交给大家发挥。在我这个工程中,以1ms间隔循环发送60个字节也不会数据紊乱,所以还是很好避免的。
第三点就不是具体问题,是存在隐患,这个就暂时不论了。
在我这个工程中,想要改成轮询发送只要在rt_device_open里更改发送配置,然后在board.h注释DMA发送宏定义就可以了,其他代码不用更改,操作系统比起裸机来是不是很方便。
工程软件我会上传,里面还有usart.c文件是中断接收的,没什么值得注意的,有兴趣可以看看。
通过网盘分享的文件:IO_USART.zip 链接:
https://pan.baidu.com/s/1M9SSB8_-ebhUMKMrJI-fwQ?pwd=84r5 提取码: 84r5
总结
本文是关于RT-Thread学习的第一篇文章,有不足之处还望大家指出,因时间紧迫,有所错字请大家人脑规避。