网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
设备驱动框架层是对同类硬件设备驱动的抽象,将不同厂家的同类硬件设备驱动中相同的部分抽取出来,将不同部分留出接口,由驱动程序实现。
设备驱动层是一组驱使硬件设备工作的程序,实现访问硬件设备的功能。它负责创建和注册 I/O 设备,对于操作逻辑简单的设备,可以不经过设备驱动框架层,直接将设备注册到 I/O 设备管理器中,使用序列图如下图所示,主要有以下 2 点:
- 设备驱动根据设备模型定义,创建出具备硬件访问能力的设备实例,将该设备通过
rt_device_register()
接口注册到 I/O 设备管理器中。 - 应用程序通过
rt_device_find()
接口查找到设备,然后使用 I/O 设备管理接口来访问硬件。
对于另一些设备,如看门狗等,则会将创建的设备实例先注册到对应的设备驱动框架中,再由设备驱动框架向 I/O 设备管理器进行注册,主要有以下几点:
- 看门狗设备驱动程序根据看门狗设备模型定义,创建出具备硬件访问能力的看门狗设备实例,并将该看门狗设备通过
rt_hw_watchdog_register()
接口注册到看门狗设备驱动框架中。 - 看门狗设备驱动框架通过
rt_device_register()
接口将看门狗设备注册到 I/O 设备管理器中。 - 应用程序通过 I/O 设备管理接口来访问看门狗设备硬件。
看门狗设备使用序列图:
I/O 设备模型
RT-Thread 的设备模型是建立在内核对象模型基础之上的,设备被认为是一类对象,被纳入对象管理器的范畴。每个设备对象都是由基对象派生而来,每个具体设备都可以继承其父类对象的属性,并派生出其私有属性,下图是设备对象的继承和派生关系示意图。
设备对象具体定义如下所示:
struct rt\_device
{
struct rt\_object parent; /\* 内核对象基类 \*/
enum rt\_device\_class\_type type; /\* 设备类型 \*/
rt\_uint16\_t flag; /\* 设备参数 \*/
rt\_uint16\_t open_flag; /\* 设备打开标志 \*/
rt\_uint8\_t ref_count; /\* 设备被引用次数 \*/
rt\_uint8\_t device_id; /\* 设备 ID,0 - 255 \*/
/\* 数据收发回调函数 \*/
rt\_err\_t (\*rx_indicate)(rt\_device\_t dev, rt\_size\_t size);
rt\_err\_t (\*tx_complete)(rt\_device\_t dev, void \*buffer);
const struct rt\_device\_ops \*ops; /\* 设备操作方法 \*/
/\* 设备的私有数据 \*/
void \*user_data;
};
typedef struct rt\_device \*rt\_device\_t;
I/O 设备类型
RT-Thread 支持多种 I/O 设备类型,主要设备类型如下所示:
RT_Device_Class_Char /\* 字符设备 \*/
RT_Device_Class_Block /\* 块设备 \*/
RT_Device_Class_NetIf /\* 网络接口设备 \*/
RT_Device_Class_MTD /\* 内存设备 \*/
RT_Device_Class_RTC /\* RTC 设备 \*/
RT_Device_Class_Sound /\* 声音设备 \*/
RT_Device_Class_Graphic /\* 图形设备 \*/
RT_Device_Class_I2CBUS /\* I2C 总线设备 \*/
RT_Device_Class_USBDevice /\* USB device 设备 \*/
RT_Device_Class_USBHost /\* USB host 设备 \*/
RT_Device_Class_SPIBUS /\* SPI 总线设备 \*/
RT_Device_Class_SPIDevice /\* SPI 设备 \*/
RT_Device_Class_SDIO /\* SDIO 设备 \*/
RT_Device_Class_Miscellaneous /\* 杂类设备 \*/
其中字符设备、块设备是常用的设备类型,它们的分类依据是设备数据与系统之间的传输处理方式。字符模式设备允许非结构的数据传输,即通常数据传输采用串行的形式,每次一个字节。字符设备通常是一些简单设备,如串口、按键。
块设备每次传输一个数据块,例如每次传输 512 个字节数据。这个数据块是硬件强制性的,数据块可能使用某类数据接口或某些强制性的传输协议,否则就可能发生错误。因此,有时块设备驱动程序对读或写操作必须执行附加的工作,如下图所示:
当系统服务于一个具有大量数据的写操作时,设备驱动程序必须首先将数据划分为多个包,每个包采用设备指定的数据尺寸。而在实际过程中,最后一部分数据尺寸有可能小于正常的设备块尺寸。如上图中每个块使用单独的写请求写入到设备中,头 3 个直接进行写操作。但最后一个数据块尺寸小于设备块尺寸,设备驱动程序必须使用不同于前 3 个块的方式处理最后的数据块。通常情况下,设备驱动程序需要首先执行相对应的设备块的读操作,然后把写入数据覆盖到读出数据上,然后再把这个 “合成” 的数据块作为一整个块写回到设备中。例如上图中的块 4,驱动程序需要先把块 4 所对应的设备块读出来,然后将需要写入的数据覆盖至从设备块读出的数据上,使其合并成一个新的块,最后再写回到块设备中。
创建和注册 I/O 设备
驱动层负责创建设备实例,并注册到 I/O 设备管理器中,可以通过静态申明的方式创建设备实例,也可以用下面的接口进行动态创建:
rt_device_t rt_device_create(int type, int attach_size);
参数****描述
type | 设备类型,可取前面小节列出的设备类型值 |
attach_size | 用户数据大小 |
返回 | —— |
设备句柄 | 创建成功 |
RT_NULL | 创建失败,动态内存分配失败 |
调用该接口时,系统会从动态堆内存中分配一个设备控制块,大小为 struct rt_device 和 attach_size 的和,设备的类型由参数 type 设定。设备被创建后,需要实现它访问硬件的操作方法。
struct rt\_device\_ops
{
/\* common device interface \*/
rt\_err\_t (\*init) (rt\_device\_t dev);
rt\_err\_t (\*open) (rt\_device\_t dev, rt\_uint16\_t oflag);
rt\_err\_t (\*close) (rt\_device\_t dev);
rt\_size\_t (\*read) (rt\_device\_t dev, rt\_off\_t pos, void \*buffer, rt\_size\_t size);
rt\_size\_t (\*write) (rt\_device\_t dev, rt\_off\_t pos, const void \*buffer, rt\_size\_t size);
rt\_err\_t (\*control)(rt\_device\_t dev, int cmd, void \*args);
};
各个操作方法的描述如下表所示:
方法名称****方法描述
init | 初始化设备。设备初始化完成后,设备控制块的 flag 会被置成已激活状态 (RT_DEVICE_FLAG_ACTIVATED)。如果设备控制块中的 flag 标志已经设置成激活状态,那么再运行初始化接口时会立刻返回,而不会重新进行初始化。 |
open | 打开设备。有些设备并不是系统一启动就已经打开开始运行,或者设备需要进行数据收发,但如果上层应用还未准备好,设备也不应默认已经使能并开始接收数据。所以建议在写底层驱动程序时,在调用 open 接口时才使能设备。 |
close | 关闭设备。在打开设备时,设备控制块会维护一个打开计数,在打开设备时进行 + 1 操作,在关闭设备时进行 - 1 操作,当计数器变为 0 时,才会进行真正的关闭操作。 |
read | 从设备读取数据。参数 pos 是读取数据的偏移量,但是有些设备并不一定需要指定偏移量,例如串口设备,设备驱动应忽略这个参数。而对于块设备来说,pos 以及 size 都是以块设备的数据块大小为单位的。例如块设备的数据块大小是 512,而参数中 pos = 10, size = 2,那么驱动应该返回设备中第 10 个块 (从第 0 个块做为起始),共计 2 个块的数据。这个接口返回的类型是 rt_size_t,即读到的字节数或块数目。正常情况下应该会返回参数中 size 的数值,如果返回零请设置对应的 errno 值。 |
write | 向设备写入数据。参数 pos 是写入数据的偏移量。与读操作类似,对于块设备来说,pos 以及 size 都是以块设备的数据块大小为单位的。这个接口返回的类型是 rt_size_t,即真实写入数据的字节数或块数目。正常情况下应该会返回参数中 size 的数值,如果返回零请设置对应的 errno 值。 |
control | 根据 cmd 命令控制设备。命令往往是由底层各类设备驱动自定义实现。例如参数 RT_DEVICE_CTRL_BLK_GETGEOME,意思是获取块设备的大小信息。 |
当一个动态创建的设备不再需要使用时可以通过如下函数来销毁:
void rt_device_destroy(rt_device_t device);
参数****描述
device | 设备句柄 |
返回 | 无 |
设备被创建后,需要注册到 I/O 设备管理器中,应用程序才能够访问,注册设备的函数如下所示:
rt_err_t rt_device_register(rt_device_t dev, const char* name, rt_uint8_t flags);
参数****描述
dev | 设备句柄 |
name | 设备名称,设备名称的最大长度由 rtconfig.h 中定义的宏 RT_NAME_MAX 指定,多余部分会被自动截掉 |
flags | 设备模式标志 |
返回 | —— |
RT_EOK | 注册成功 |
-RT_ERROR | 注册失败,dev 为空或者 name 已经存在 |
Note
注:应当避免重复注册已经注册的设备,以及注册相同名字的设备。
flags 参数支持下列参数 (可以采用或的方式支持多种参数):
#define RT\_DEVICE\_FLAG\_RDONLY 0x001 /\* 只读 \*/
#define RT\_DEVICE\_FLAG\_WRONLY 0x002 /\* 只写 \*/
#define RT\_DEVICE\_FLAG\_RDWR 0x003 /\* 读写 \*/
#define RT\_DEVICE\_FLAG\_REMOVABLE 0x004 /\* 可移除 \*/
#define RT\_DEVICE\_FLAG\_STANDALONE 0x008 /\* 独立 \*/
#define RT\_DEVICE\_FLAG\_SUSPENDED 0x020 /\* 挂起 \*/
#define RT\_DEVICE\_FLAG\_STREAM 0x040 /\* 流模式 \*/
#define RT\_DEVICE\_FLAG\_INT\_RX 0x100 /\* 中断接收 \*/
#define RT\_DEVICE\_FLAG\_DMA\_RX 0x200 /\* DMA 接收 \*/
#define RT\_DEVICE\_FLAG\_INT\_TX 0x400 /\* 中断发送 \*/
#define RT\_DEVICE\_FLAG\_DMA\_TX 0x800 /\* DMA 发送 \*/
设备流模式 RT_DEVICE_FLAG_STREAM 参数用于向串口终端输出字符串:当输出的字符是 “\n”
时,自动在前面补一个 “\r”
做分行。
注册成功的设备可以在 FinSH 命令行使用 list_device
命令查看系统中所有的设备信息,包括设备名称、设备类型和设备被打开次数:
msh />list_device
device type ref count
-------- -------------------- ----------
e0 Network Interface 0
sd0 Block Device 1
rtc RTC 0
uart1 Character Device 0
uart0 Character Device 2
msh />
当设备注销后的,设备将从设备管理器中移除,也就不能再通过设备查找搜索到该设备。注销设备不会释放设备控制块占用的内存。注销设备的函数如下所示:
rt_err_t rt_device_unregister(rt_device_t dev);
参数****描述
dev | 设备句柄 |
返回 | —— |
RT_EOK | 成功 |
下面代码为看门狗设备的注册示例,调用 rt_hw_watchdog_register()
接口后,设备通过 rt_device_register()
接口被注册到 I/O 设备管理器中。
const static struct rt\_device\_ops wdt_ops =
{
rt_watchdog_init,
rt_watchdog_open,
rt_watchdog_close,
RT_NULL,
RT_NULL,
rt_watchdog_control,
};
rt\_err\_t rt\_hw\_watchdog\_register(struct rt\_watchdog\_device \*wtd,
const char \*name,
rt\_uint32\_t flag,
void \*data)
{
struct rt\_device \*device;
RT\_ASSERT(wtd != RT_NULL);
device = &(wtd->parent);
device->type = RT_Device_Class_Miscellaneous;
device->rx_indicate = RT_NULL;
device->tx_complete = RT_NULL;
device->ops = &wdt_ops;
device->user_data = data;
/\* register a character device \*/
return rt\_device\_register(device, name, flag);
}
访问 I/O 设备
应用程序通过 I/O 设备管理接口来访问硬件设备,当设备驱动实现后,应用程序就可以访问该硬件。I/O 设备管理接口与 I/O 设备的操作方法的映射关系下图所示:
查找设备
应用程序根据设备名称获取设备句柄,进而可以操作设备。查找设备函数如下所示:
rt_device_t rt_device_find(const char* name);
参数****描述
name | 设备名称 |
返回 | —— |
设备句柄 | 查找到对应设备将返回相应的设备句柄 |
RT_NULL | 没有找到相应的设备对象 |
初始化设备
获得设备句柄后,应用程序可使用如下函数对设备进行初始化操作:
rt_err_t rt_device_init(rt_device_t dev);
参数****描述
dev | 设备句柄 |
返回 | —— |
RT_EOK | 设备初始化成功 |
错误码 | 设备初始化失败 |
Note
注:当一个设备已经初始化成功后,调用这个接口将不再重复做初始化 0。
打开和关闭设备
通过设备句柄,应用程序可以打开和关闭设备,打开设备时,会检测设备是否已经初始化,没有初始化则会默认调用初始化接口初始化设备。通过如下函数打开设备:
rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags);
参数****描述
dev | 设备句柄 |
oflags | 设备打开模式标志 |
返回 | —— |
RT_EOK | 设备打开成功 |
-RT_EBUSY | 如果设备注册时指定的参数中包括 RT_DEVICE_FLAG_STANDALONE 参数,此设备将不允许重复打开 |
其他错误码 | 设备打开失败 |
oflags 支持以下的参数:
#define RT\_DEVICE\_OFLAG\_CLOSE 0x000 /\* 设备已经关闭(内部使用)\*/
#define RT\_DEVICE\_OFLAG\_RDONLY 0x001 /\* 以只读方式打开设备 \*/
#define RT\_DEVICE\_OFLAG\_WRONLY 0x002 /\* 以只写方式打开设备 \*/
#define RT\_DEVICE\_OFLAG\_RDWR 0x003 /\* 以读写方式打开设备 \*/
#define RT\_DEVICE\_OFLAG\_OPEN 0x008 /\* 设备已经打开(内部使用)\*/
#define RT\_DEVICE\_FLAG\_STREAM 0x040 /\* 设备以流模式打开 \*/
#define RT\_DEVICE\_FLAG\_INT\_RX 0x100 /\* 设备以中断接收模式打开 \*/
#define RT\_DEVICE\_FLAG\_DMA\_RX 0x200 /\* 设备以 DMA 接收模式打开 \*/
#define RT\_DEVICE\_FLAG\_INT\_TX 0x400 /\* 设备以中断发送模式打开 \*/
#define RT\_DEVICE\_FLAG\_DMA\_TX 0x800 /\* 设备以 DMA 发送模式打开 \*/
Note
注:如果上层应用程序需要设置设备的接收回调函数,则必须以 RT_DEVICE_FLAG_INT_RX 或者 RT_DEVICE_FLAG_DMA_RX 的方式打开设备,否则不会回调函数。
应用程序打开设备完成读写等操作后,如果不需要再对设备进行操作则可以关闭设备,通过如下函数完成:
rt_err_t rt_device_close(rt_device_t dev);
参数****描述
dev | 设备句柄 |
返回 | —— |
RT_EOK | 关闭设备成功 |
-RT_ERROR | 设备已经完全关闭,不能重复关闭设备 |
其他错误码 | 关闭设备失败 |
Note
注:关闭设备接口和打开设备接口需配对使用,打开一次设备对应要关闭一次设备,这样设备才会被完全关闭,否则设备仍处于未关闭状态。
控制设备
通过命令控制字,应用程序也可以对设备进行控制,通过如下函数完成:
rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg);
参数****描述
dev | 设备句柄 |
cmd | 命令控制字,这个参数通常与设备驱动程序相关 |
arg | 控制的参数 |
返回 | —— |
RT_EOK | 函数执行成功 |
-RT_ENOSYS | 执行失败,dev 为空 |
其他错误码 | 执行失败 |
参数 cmd 的通用设备命令可取如下宏定义:
#define RT\_DEVICE\_CTRL\_RESUME 0x01 /\* 恢复设备 \*/
#define RT\_DEVICE\_CTRL\_SUSPEND 0x02 /\* 挂起设备 \*/
#define RT\_DEVICE\_CTRL\_CONFIG 0x03 /\* 配置设备 \*/
#define RT\_DEVICE\_CTRL\_SET\_INT 0x10 /\* 设置中断 \*/
#define RT\_DEVICE\_CTRL\_CLR\_INT 0x11 /\* 清中断 \*/
#define RT\_DEVICE\_CTRL\_GET\_INT 0x12 /\* 获取中断状态 \*/
读写设备
应用程序从设备中读取数据可以通过如下函数完成:
rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos,void* buffer, rt_size_t size);
参数****描述
dev | 设备句柄 |
pos | 读取数据偏移量 |
buffer | 内存缓冲区指针,读取的数据将会被保存在缓冲区中 |
size | 读取数据的大小 |
返回 | —— |
读到数据的实际大小 | 如果是字符设备,返回大小以字节为单位,如果是块设备,返回的大小以块为单位 |
0 | 需要读取当前线程的 errno 来判断错误状态 |
调用这个函数,会从 dev 设备中读取数据,并存放在 buffer 缓冲区中,这个缓冲区的最大长度是 size,pos 根据不同的设备类别有不同的意义。
向设备中写入数据,可以通过如下函数完成:
rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos,const void* buffer, rt_size_t size);
参数****描述
dev | 设备句柄 |
pos | 写入数据偏移量 |
buffer | 内存缓冲区指针,放置要写入的数据 |
size | 写入数据的大小 |
返回 | —— |
写入数据的实际大小 | 如果是字符设备,返回大小以字节为单位;如果是块设备,返回的大小以块为单位 |
0 | 需要读取当前线程的 errno 来判断错误状态 |
调用这个函数,会把缓冲区 buffer 中的数据写入到设备 dev 中,写入数据的最大长度是 size,pos 根据不同的设备类别存在不同的意义。
数据收发回调
当硬件设备收到数据时,可以通过如下函数回调另一个函数来设置数据接收指示,通知上层应用线程有数据到达:
rt_err_t rt_device_set_rx_indicate(rt_device_t dev, rt_err_t (*rx_ind)(rt_device_t dev,rt_size_t size));
参数****描述
dev | 设备句柄 |
rx_ind | 回调函数指针 |
返回 | —— |
RT_EOK | 设置成功 |
该函数的回调函数由调用者提供。当硬件设备接收到数据时,会回调这个函数并把收到的数据长度放在 size 参数中传递给上层应用。上层应用线程应在收到指示后,立刻从设备中读取数据。
在应用程序调用 rt_device_write()
写入数据时,如果底层硬件能够支持自动发送,那么上层应用可以设置一个回调函数。这个回调函数会在底层硬件数据发送完成后 (例如 DMA 传送完成或 FIFO 已经写入完毕产生完成中断时) 调用。可以通过如下函数设置设备发送完成指示,函数参数及返回值见:
rt_err_t rt_device_set_tx_complete(rt_device_t dev, rt_err_t (*tx_done)(rt_device_t dev,void *buffer));
参数****描述
dev | 设备句柄 |
tx_done | 回调函数指针 |
返回 | —— |
RT_EOK | 设置成功 |
调用这个函数时,回调函数由调用者提供,当硬件设备发送完数据时,由驱动程序回调这个函数并把发送完成的数据块地址 buffer 作为参数传递给上层应用。上层应用(线程)在收到指示时会根据发送 buffer 的情况,释放 buffer 内存块或将其作为下一个写数据的缓存。
设备访问示例
下面代码为用程序访问设备的示例,首先通过 rt_device_find()
口查找到看门狗设备,获得设备句柄,然后通过 rt_device_init()
口初始化设备,通过 rt_device_control()
口设置看门狗设备溢出时间。
#include <rtthread.h>
#include <rtdevice.h>
#define IWDG\_DEVICE\_NAME "iwg"
static rt\_device\_t wdg_dev;
static void idle\_hook(void)
{
/\* 在空闲线程的回调函数里喂狗 \*/
rt\_device\_control(wdg_dev, RT_DEVICE_CTRL_WDT_KEEPALIVE, NULL);
rt\_kprintf("feed the dog!\n ");
}
int main(void)
{
rt\_err\_t res = RT_EOK;
rt\_uint32\_t timeout = 1000; /\* 溢出时间 \*/
/\* 根据设备名称查找看门狗设备,获取设备句柄 \*/
wdg_dev = rt\_device\_find(IWDG_DEVICE_NAME);
if (!wdg_dev)
{
rt\_kprintf("find %s failed!\n", IWDG_DEVICE_NAME);
return RT_ERROR;
}
/\* 初始化设备 \*/
res = rt\_device\_init(wdg_dev);
if (res != RT_EOK)
{
rt\_kprintf("initialize %s failed!\n", IWDG_DEVICE_NAME);
return res;
}
/\* 设置看门狗溢出时间 \*/
res = rt\_device\_control(wdg_dev, RT_DEVICE_CTRL_WDT_SET_TIMEOUT, &timeout);
if (res != RT_EOK)
{
rt\_kprintf("set %s timeout failed!\n", IWDG_DEVICE_NAME);
return res;
}
/\* 设置空闲线程回调函数 \*/
rt\_thread\_idle\_sethook(idle_hook);
return res;
}
补充说明
I/O 设备模型框架补充图
I/O 设备模型框架补充图是对 I/O 设备模型框架图的解释和补充说明,如下图所示。
图中各类里的c文件是各类对应的管理接口所在,比如设备基类rt_device的管理接口在device.c中。
图中设备驱动框架层有很多 RT-Thread 写好的类,图中只列出2类,其他类用 “xxx” 来表示,这些省略的类及其管理接口可以在 RT-Thread 源码 components/drivers 目录下找寻,比如该目录下可以找到serial/i2c/spi/sensor/can 等等相关目录。
图中设备驱动层的 “xxx” ,是 RT-Thread 支持的各 BSP 平台,在源码的 src/bsp 目录下找寻,比如stm32/gd32/at32/avr32/k210 等等。各个平台各自实现各个设备类型的硬件驱动能力,比如 STM32分别实现了 stm32_uart 类/stm32_adc 类等及其对应的管理接口;同样的,其他平台也分别各自实现了诸多对应类别及管理接口。
图中设备驱动层的各类里的c文件路径和名字只是示意,具体名字和路径由各BSP开发维护者自己定的。且随着 RT-Thread 版本的变化,各 BSP 的驱动路径和名字可能会发生变化。比如图中画的 STM32 串口设备类的管理接口所在路径是 bsp/stm32/drv_usart.c ,但实际路径是在 RT-Thread 源码下的 bsp/stm32/libraries/HAL_Drivers 下 。比如有的名字叫 drv_uart.c 等等。
该图横向看是分层思想,纵向看是各类派生继承关系。从下到上不断抽象、屏蔽下层差异,体现了面向对象的抽象的思想。子类受到父类的接口约束,子类各自实现父类提供的统一接口,又体现了面向接口编程的思想。比如从驱动层到驱动框架层,由不同厂商的相同硬件模块创建了很多子类对象,然后对接到同一个父类接口上,多对一,体现面向对象的抽象的威力。以串口设备为例,不管下层是 STM32、GD32 还是别的平台的,只要都是串口设备,都对接到 RT-Thread 的串口设备类——如图所绘,多个硬件对象对接同一个父类对象接口。同理,从设备驱动框架层到IO设备管理接口层,又是多对一,又是再一次的屏蔽差异,再一次的抽象。——面向对象的思想贯穿其中。
tips: 新增 BSP 设备驱动到 I/O 设备模型框架上时,开发者只需开发驱动层即可,设备驱动框架层和 I/O 设备管理层 RT-Thread 已写好了,无需改动,除非发现BUG或增加新的类别。
我有疑问: RT-Thread 官方论坛
UART 设备
已剪辑自: https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/uart/uart_v1/uart
UART 简介
UART(Universal Asynchronous Receiver/Transmitter)通用异步收发传输器,UART 作为异步串口通信协议的一种,工作原理是将传输数据的每个字符一位接一位地传输。是在应用程序开发过程中使用频率最高的数据总线。
UART 串口的特点是将数据一位一位地顺序传送,只要 2 根传输线就可以实现双向通信,一根线发送数据的同时用另一根线接收数据。UART 串口通信有几个重要的参数,分别是波特率、起始位、数据位、停止位和奇偶检验位,对于两个使用 UART 串口通信的端口,这些参数必须匹配,否则通信将无法正常完成。UART 串口传输的数据格式如下图所示:
- 起始位:表示数据传输的开始,电平逻辑为 “0” 。
- 数据位:可能值有 5、6、7、8、9,表示传输这几个 bit 位数据。一般取值为 8,因为一个 ASCII 字符值为 8 位。
- 奇偶校验位:用于接收方对接收到的数据进行校验,校验 “1” 的位数为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性,使用时不需要此位也可以。
- 停止位: 表示一帧数据的结束。电平逻辑为 “1”。
- 波特率:串口通信时的速率,它用单位时间内传输的二进制代码的有效位(bit)数来表示,其单位为每秒比特数 bit/s(bps)。常见的波特率值有 4800、9600、14400、38400、115200等,数值越大数据传输的越快,波特率为 115200 表示每秒钟传输 115200 位数据。
访问串口设备
应用程序通过 RT-Thread提供的 I/O 设备管理接口来访问串口硬件,相关接口如下所示:
函数****描述
rt_device_find() | 查找设备 |
rt_device_open() | 打开设备 |
rt_device_read() | 读取数据 |
rt_device_write() | 写入数据 |
rt_device_control() | 控制设备 |
rt_device_set_rx_indicate() | 设置接收回调函数 |
rt_device_set_tx_complete() | 设置发送完成回调函数 |
rt_device_close() | 关闭设备 |
查找串口设备
应用程序根据串口设备名称获取设备句柄,进而可以操作串口设备,查找设备函数如下所示,
rt_device_t rt_device_find(const char* name);
参数****描述
name | 设备名称 |
返回 | —— |
设备句柄 | 查找到对应设备将返回相应的设备句柄 |
RT_NULL | 没有找到相应的设备对象 |
一般情况下,注册到系统的串口设备名称为 uart0,uart1等,使用示例如下所示:
#define SAMPLE_UART_NAME "uart2" /* 串口设备名称 */
static rt_device_t serial; /* 串口设备句柄 */
/* 查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);
打开串口设备
通过设备句柄,应用程序可以打开和关闭设备,打开设备时,会检测设备是否已经初始化,没有初始化则会默认调用初始化接口初始化设备。通过如下函数打开设备:
rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags);
参数****描述
dev | 设备句柄 |
oflags | 设备模式标志 |
返回 | —— |
RT_EOK | 设备打开成功 |
-RT_EBUSY | 如果设备注册时指定的参数中包括 RT_DEVICE_FLAG_STANDALONE 参数,此设备将不允许重复打开 |
其他错误码 | 设备打开失败 |
oflags 参数支持下列取值 (可以采用或的方式支持多种取值):
#define RT\_DEVICE\_FLAG\_STREAM 0x040 /\* 流模式 \*/
/\* 接收模式参数 \*/
#define RT\_DEVICE\_FLAG\_INT\_RX 0x100 /\* 中断接收模式 \*/
#define RT\_DEVICE\_FLAG\_DMA\_RX 0x200 /\* DMA 接收模式 \*/
/\* 发送模式参数 \*/
#define RT\_DEVICE\_FLAG\_INT\_TX 0x400 /\* 中断发送模式 \*/
#define RT\_DEVICE\_FLAG\_DMA\_TX 0x800 /\* DMA 发送模式 \*/
串口数据接收和发送数据的模式分为 3 种:中断模式、轮询模式、DMA 模式。在使用的时候,这 3 种模式只能选其一,若串口的打开参数 oflags 没有指定使用中断模式或者 DMA 模式,则默认使用轮询模式。
DMA(Direct Memory Access)即直接存储器访问。 DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过 DMA 控制器为 RAM 与 I/O 设备开辟一条直接传送数据的通路,这就节省了 CPU 的资源来做其他操作。使用 DMA 传输可以连续获取或发送一段信息而不占用中断或延时,在通信频繁或有大段信息要传输时非常有用。
Note
注:* RT_DEVICE_FLAG_STREAM:流模式用于向串口终端输出字符串:当输出的字符是 "\n"
(对应 16 进制值为 0x0A)时,自动在前面输出一个 "\r"
(对应 16 进制值为 0x0D) 做分行。
流模式 RT_DEVICE_FLAG_STREAM 可以和接收发送模式参数使用或 “|” 运算符一起使用。
以中断接收及轮询发送模式使用串口设备的示例如下所示:
#define SAMPLE_UART_NAME "uart2" /* 串口设备名称 */
static rt_device_t serial; /* 串口设备句柄 */
/* 查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);
/* 以中断接收及轮询发送模式打开串口设备 */
rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
若串口要使用 DMA 接收模式,oflags 取值 RT_DEVICE_FLAG_DMA_RX。以DMA 接收及轮询发送模式使用串口设备的示例如下所示:
#define SAMPLE_UART_NAME "uart2" /* 串口设备名称 */
static rt_device_t serial; /* 串口设备句柄 */
/* 查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);
/* 以 DMA 接收及轮询发送模式打开串口设备 */
rt_device_open(serial, RT_DEVICE_FLAG_DMA_RX);
控制串口设备
通过控制接口,应用程序可以对串口设备进行配置,如波特率、数据位、校验位、接收缓冲区大小、停止位等参数的修改。控制函数如下所示:
rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg);
参数****描述
dev | 设备句柄 |
cmd | 命令控制字,可取值:RT_DEVICE_CTRL_CONFIG |
arg | 控制的参数,可取类型: struct serial_configure |
返回 | —— |
RT_EOK | 函数执行成功 |
-RT_ENOSYS | 执行失败,dev 为空 |
其他错误码 | 执行失败 |
控制参数结构体 struct serial_configure 原型如下:
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 reserved :4; /\* 保留位 \*/
};
RT-Thread 提供的配置参数可取值为如下宏定义:
/\* 波特率可取值 \*/
#define BAUD\_RATE\_2400 2400
#define BAUD\_RATE\_4800 4800
#define BAUD\_RATE\_9600 9600
#define BAUD\_RATE\_19200 19200
#define BAUD\_RATE\_38400 38400
#define BAUD\_RATE\_57600 57600
#define BAUD\_RATE\_115200 115200
#define BAUD\_RATE\_230400 230400
#define BAUD\_RATE\_460800 460800
#define BAUD\_RATE\_921600 921600
#define BAUD\_RATE\_2000000 2000000
#define BAUD\_RATE\_3000000 3000000
/\* 数据位可取值 \*/
#define DATA\_BITS\_5 5
#define DATA\_BITS\_6 6
#define DATA\_BITS\_7 7
#define DATA\_BITS\_8 8
#define DATA\_BITS\_9 9
/\* 停止位可取值 \*/
#define STOP\_BITS\_1 0
#define STOP\_BITS\_2 1
#define STOP\_BITS\_3 2
#define STOP\_BITS\_4 3
/\* 极性位可取值 \*/
#define PARITY\_NONE 0
#define PARITY\_ODD 1
#define PARITY\_EVEN 2
/\* 高低位顺序可取值 \*/
#define BIT\_ORDER\_LSB 0
#define BIT\_ORDER\_MSB 1
/\* 模式可取值 \*/
#define NRZ\_NORMAL 0 /\* normal mode \*/
#define NRZ\_INVERTED 1 /\* inverted mode \*/
/\* 接收数据缓冲区默认大小 \*/
#define RT\_SERIAL\_RB\_BUFSZ 64
接收缓冲区:当串口使用中断接收模式打开时,串口驱动框架会根据 RT_SERIAL_RB_BUFSZ 大小开辟一块缓冲区用于保存接收到的数据,底层驱动接收到一个数据,都会在中断服务程序里面将数据放入缓冲区。
RT-Thread 提供的默认串口配置如下,即 RT-Thread 系统中默认每个串口设备都使用如下配置:
#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 \*/ \
0 \
}
Note
注:默认串口配置接收数据缓冲区大小为 RT_SERIAL_RB_BUFSZ,即 64 字节。若一次性数据接收字节数很多,没有及时读取数据,那么缓冲区的数据将会被新接收到的数据覆盖,造成数据丢失,建议调大缓冲区,即通过 control 接口修改。在修改缓冲区大小时请注意,缓冲区大小无法动态改变,只有在 open 设备之前可以配置。open 设备之后,缓冲区大小不可再进行更改。但除缓冲区之外的其他参数,在 open 设备前 / 后,均可进行更改。
若实际使用串口的配置参数与默认配置参数不符,则用户可以通过应用代码进行修改。修改串口配置参数,如波特率、数据位、校验位、缓冲区接收 buffsize、停止位等的示例程序如下:
#define SAMPLE\_UART\_NAME "uart2" /\* 串口设备名称 \*/
static rt\_device\_t serial; /\* 串口设备句柄 \*/
struct serial\_configure config = RT_SERIAL_CONFIG_DEFAULT; /\* 初始化配置参数 \*/
/\* step1:查找串口设备 \*/
serial = rt\_device\_find(SAMPLE_UART_NAME);
/\* step2:修改串口配置参数 \*/
config.baud_rate = BAUD_RATE_9600; //修改波特率为 9600
config.data_bits = DATA_BITS_8; //数据位 8
config.stop_bits = STOP_BITS_1; //停止位 1
config.bufsz = 128; //修改缓冲区 buff size 为 128
config.parity = PARITY_NONE; //无奇偶校验位
/\* step3:控制串口设备。通过控制接口传入命令控制字,与控制参数 \*/
rt\_device\_control(serial, RT_DEVICE_CTRL_CONFIG, &config);
/\* step4:打开串口设备。以中断接收及轮询发送模式打开串口设备 \*/
rt\_device\_open(serial, RT_DEVICE_FLAG_INT_RX);
发送数据
向串口中写入数据,可以通过如下函数完成:
rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos, const void* buffer, rt_size_t size);
参数****描述
dev | 设备句柄 |
pos | 写入数据偏移量,此参数串口设备未使用 |
buffer | 内存缓冲区指针,放置要写入的数据 |
size | 写入数据的大小 |
返回 | —— |
写入数据的实际大小 | 如果是字符设备,返回大小以字节为单位; |
0 | 需要读取当前线程的 errno 来判断错误状态 |
调用这个函数,会把缓冲区 buffer 中的数据写入到设备 dev 中,写入数据的大小是 size。
向串口写入数据示例程序如下所示:
#define SAMPLE_UART_NAME "uart2" /* 串口设备名称 */
static rt_device_t serial; /* 串口设备句柄 */
char str[] = "hello RT-Thread!\r\n";
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT; /* 配置参数 */
/* 查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);
/* 以中断接收及轮询发送模式打开串口设备 */
rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
/* 发送字符串 */
rt_device_write(serial, 0, str, (sizeof(str) - 1));
设置发送完成回调函数
在应用程序调用 rt_device_write()
写入数据时,如果底层硬件能够支持自动发送,那么上层应用可以设置一个回调函数。这个回调函数会在底层硬件数据发送完成后 (例如 DMA 传送完成或 FIFO 已经写入完毕产生完成中断时) 调用。可以通过如下函数设置设备发送完成指示 :
rt_err_t rt_device_set_tx_complete(rt_device_t dev, rt_err_t (*tx_done)(rt_device_t dev,void *buffer));
参数****描述
dev | 设备句柄 |
tx_done | 回调函数指针 |
返回 | —— |
RT_EOK | 设置成功 |
调用这个函数时,回调函数由调用者提供,当硬件设备发送完数据时,由设备驱动程序回调这个函数并把发送完成的数据块地址 buffer 作为参数传递给上层应用。上层应用(线程)在收到指示时会根据发送 buffer 的情况,释放 buffer 内存块或将其作为下一个写数据的缓存。
设置接收回调函数
可以通过如下函数来设置数据接收指示,当串口收到数据时,通知上层应用线程有数据到达 :
rt_err_t rt_device_set_rx_indicate(rt_device_t dev, rt_err_t (*rx_ind)(rt_device_t dev,rt_size_t size));
参数****描述
dev | 设备句柄 |
rx_ind | 回调函数指针 |
dev | 设备句柄(回调函数参数) |
size | 缓冲区数据大小(回调函数参数) |
返回 | —— |
RT_EOK | 设置成功 |
该函数的回调函数由调用者提供。若串口以中断接收模式打开,当串口接收到一个数据产生中断时,就会调用回调函数,并且会把此时缓冲区的数据大小放在 size 参数里,把串口设备句柄放在 dev 参数里供调用者获取。
若串口以 DMA 接收模式打开,当 DMA 完成一批数据的接收后会调用此回调函数。
一般情况下接收回调函数可以发送一个信号量或者事件通知串口数据处理线程有数据到达。使用示例如下所示:
#define SAMPLE\_UART\_NAME "uart2" /\* 串口设备名称 \*/
static rt\_device\_t serial; /\* 串口设备句柄 \*/
static struct rt\_semaphore rx_sem; /\* 用于接收消息的信号量 \*/
/\* 接收数据回调函数 \*/
static rt\_err\_t uart\_input(rt\_device\_t dev, rt\_size\_t size)
{
/\* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 \*/
rt\_sem\_release(&rx_sem);
return RT_EOK;
}
static int uart\_sample(int argc, char \*argv[])
{
serial = rt\_device\_find(SAMPLE_UART_NAME);
/\* 以中断接收及轮询发送模式打开串口设备 \*/
rt\_device\_open(serial, RT_DEVICE_FLAG_INT_RX);
/\* 初始化信号量 \*/
rt\_sem\_init(&rx_sem, "rx\_sem", 0, RT_IPC_FLAG_FIFO);
/\* 设置接收回调函数 \*/
rt\_device\_set\_rx\_indicate(serial, uart_input);
}
接收数据
可调用如下函数读取串口接收到的数据:
rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size);
参数****描述
dev | 设备句柄 |
pos | 读取数据偏移量,此参数串口设备未使用 |
buffer | 缓冲区指针,读取的数据将会被保存在缓冲区中 |
size | 读取数据的大小 |
返回 | —— |
读到数据的实际大小 | 如果是字符设备,返回大小以字节为单位 |
0 | 需要读取当前线程的 errno 来判断错误状态 |
读取数据偏移量 pos 针对字符设备无效,此参数主要用于块设备中。
串口使用中断接收模式并配合接收回调函数的使用示例如下所示:
static rt\_device\_t serial; /\* 串口设备句柄 \*/
static struct rt\_semaphore rx_sem; /\* 用于接收消息的信号量 \*/
/\* 接收数据的线程 \*/
static void serial\_thread\_entry(void \*parameter)
{
char ch;
while (1)
{
/\* 从串口读取一个字节的数据,没有读取到则等待接收信号量 \*/
while (rt\_device\_read(serial, -1, &ch, 1) != 1)
{
/\* 阻塞等待接收信号量,等到信号量后再次读取数据 \*/
rt\_sem\_take(&rx_sem, RT_WAITING_FOREVER);
}
/\* 读取到的数据通过串口错位输出 \*/
ch = ch + 1;
rt\_device\_write(serial, 0, &ch, 1);
}
}
关闭串口设备
当应用程序完成串口操作后,可以关闭串口设备,通过如下函数完成:
rt_err_t rt_device_close(rt_device_t dev);
参数****描述
dev | 设备句柄 |
返回 | —— |
RT_EOK | 关闭设备成功 |
-RT_ERROR | 设备已经完全关闭,不能重复关闭设备 |
其他错误码 | 关闭设备失败 |
关闭设备接口和打开设备接口需配对使用,打开一次设备对应要关闭一次设备,这样设备才会被完全关闭,否则设备仍处于未关闭状态。
串口设备使用示例
中断接收及轮询发送
示例代码的主要步骤如下所示:
- 首先查找串口设备获取设备句柄。
- 初始化回调函数发送使用的信号量,然后以读写及中断接收方式打开串口设备。
- 设置串口设备的接收回调函数,之后发送字符串,并创建读取数据线程。
- 读取数据线程会尝试读取一个字符数据,如果没有数据则会挂起并等待信号量,当串口设备接收到一个数据时会触发中断并调用接收回调函数,此函数会发送信号量唤醒线程,此时线程会马上读取接收到的数据。
- 此示例代码不局限于特定的 BSP,根据 BSP 注册的串口设备,修改示例代码宏定义 SAMPLE_UART_NAME 对应的串口设备名称即可运行。
运行序列图如下图所示:
/\*
\* 程序清单:这是一个 串口 设备使用例程
\* 例程导出了 uart\_sample 命令到控制终端
\* 命令调用格式:uart\_sample uart2
\* 命令解释:命令第二个参数是要使用的串口设备名称,为空则使用默认的串口设备
\* 程序功能:通过串口输出字符串"hello RT-Thread!",然后错位输出输入的字符
\*/
#include <rtthread.h>
#define SAMPLE\_UART\_NAME "uart2"
/\* 用于接收消息的信号量 \*/
static struct rt\_semaphore rx_sem;
static rt\_device\_t serial;
/\* 接收数据回调函数 \*/
static rt\_err\_t uart\_input(rt\_device\_t dev, rt\_size\_t size)
{
/\* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 \*/
rt\_sem\_release(&rx_sem);
return RT_EOK;
}
static void serial\_thread\_entry(void \*parameter)
{
char ch;
while (1)
{
/\* 从串口读取一个字节的数据,没有读取到则等待接收信号量 \*/
while (rt\_device\_read(serial, -1, &ch, 1) != 1)
{
/\* 阻塞等待接收信号量,等到信号量后再次读取数据 \*/
rt\_sem\_take(&rx_sem, RT_WAITING_FOREVER);
}
/\* 读取到的数据通过串口错位输出 \*/
ch = ch + 1;
rt\_device\_write(serial, 0, &ch, 1);
}
}
static int uart\_sample(int argc, char \*argv[])
{
rt\_err\_t ret = RT_EOK;
char uart_name[RT_NAME_MAX];
char str[] = "hello RT-Thread!\r\n";
if (argc == 2)
{
rt\_strncpy(uart_name, argv[1], RT_NAME_MAX);
}
else
{
rt\_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX);
}
/\* 查找系统中的串口设备 \*/
serial = rt\_device\_find(uart_name);
if (!serial)
{
rt\_kprintf("find %s failed!\n", uart_name);
return RT_ERROR;
}
/\* 初始化信号量 \*/
rt\_sem\_init(&rx_sem, "rx\_sem", 0, RT_IPC_FLAG_FIFO);
/\* 以中断接收及轮询发送模式打开串口设备 \*/
rt\_device\_open(serial, RT_DEVICE_FLAG_INT_RX);
/\* 设置接收回调函数 \*/
rt\_device\_set\_rx\_indicate(serial, uart_input);
/\* 发送字符串 \*/
rt\_device\_write(serial, 0, str, (sizeof(str) - 1));
/\* 创建 serial 线程 \*/
rt\_thread\_t thread = rt\_thread\_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);
/\* 创建成功则启动线程 \*/
if (thread != RT_NULL)
{
rt\_thread\_startup(thread);
}
else
{
ret = RT_ERROR;
}
return ret;
}
/\* 导出到 msh 命令列表中 \*/
MSH\_CMD\_EXPORT(uart_sample, uart device sample);
DMA 接收及轮询发送
当串口接收到一批数据后会调用接收回调函数,接收回调函数会把此时缓冲区的数据大小通过消息队列发送给等待的数据处理线程。线程获取到消息后被激活,并读取数据。一般情况下 DMA 接收模式会结合 DMA 接收完成中断和串口空闲中断完成数据接收。
- 此示例代码不局限于特定的 BSP,根据 BSP 注册的串口设备,修改示例代码宏定义 SAMPLE_UART_NAME 对应的串口设备名称即可运行。
运行序列图如下图所示:
/\*
\* 程序清单:这是一个串口设备 DMA 接收使用例程
\* 例程导出了 uart\_dma\_sample 命令到控制终端
\* 命令调用格式:uart\_dma\_sample uart3
\* 命令解释:命令第二个参数是要使用的串口设备名称,为空则使用默认的串口设备
\* 程序功能:通过串口输出字符串"hello RT-Thread!",并通过串口输出接收到的数据,然后打印接收到的数据。
\*/
#include <rtthread.h>
#define SAMPLE\_UART\_NAME "uart3" /\* 串口设备名称 \*/
/\* 串口接收消息结构\*/
struct rx\_msg
{
rt\_device\_t dev;
rt\_size\_t size;
};
/\* 串口设备句柄 \*/
static rt\_device\_t serial;
/\* 消息队列控制块 \*/
static struct rt\_messagequeue rx_mq;
/\* 接收数据回调函数 \*/
static rt\_err\_t uart\_input(rt\_device\_t dev, rt\_size\_t size)
{
struct rx\_msg msg;
rt\_err\_t result;
msg.dev = dev;
msg.size = size;
result = rt\_mq\_send(&rx_mq, &msg, sizeof(msg));
if ( result == -RT_EFULL)
{
/\* 消息队列满 \*/
rt\_kprintf("message queue full!\n");
}
return result;
}
static void serial\_thread\_entry(void \*parameter)
{
struct rx\_msg msg;
rt\_err\_t result;
rt\_uint32\_t rx_length;
static char rx_buffer[RT_SERIAL_RB_BUFSZ + 1];
while (1)
{
rt\_memset(&msg, 0, sizeof(msg));
/\* 从消息队列中读取消息\*/
result = rt\_mq\_recv(&rx_mq, &msg, sizeof(msg), RT_WAITING_FOREVER);
if (result == RT_EOK)
{
/\* 从串口读取数据\*/
rx_length = rt\_device\_read(msg.dev, 0, rx_buffer, msg.size);
rx_buffer[rx_length] = '\0';
/\* 通过串口设备 serial 输出读取到的消息 \*/
rt\_device\_write(serial, 0, rx_buffer, rx_length);
/\* 打印数据 \*/
rt\_kprintf("%s\n",rx_buffer);
}
}
}
static int uart\_dma\_sample(int argc, char \*argv[])
{
rt\_err\_t ret = RT_EOK;
char uart_name[RT_NAME_MAX];
static char msg_pool[256];
char str[] = "hello RT-Thread!\r\n";
if (argc == 2)
{
rt\_strncpy(uart_name, argv[1], RT_NAME_MAX);
}
else
{
rt\_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX);
}
/\* 查找串口设备 \*/
serial = rt\_device\_find(uart_name);
if (!serial)
{
rt\_kprintf("find %s failed!\n", uart_name);
return RT_ERROR;
}
/\* 初始化消息队列 \*/
rt\_mq\_init(&rx_mq, "rx\_mq",
msg_pool, /\* 存放消息的缓冲区 \*/
sizeof(struct rx\_msg), /\* 一条消息的最大长度 \*/
sizeof(msg_pool), /\* 存放消息的缓冲区大小 \*/
RT_IPC_FLAG_FIFO); /\* 如果有多个线程等待,按照先来先得到的方法分配消息 \*/
/\* 以 DMA 接收及轮询发送方式打开串口设备 \*/
rt\_device\_open(serial, RT_DEVICE_FLAG_DMA_RX);
/\* 设置接收回调函数 \*/
rt\_device\_set\_rx\_indicate(serial, uart_input);
/\* 发送字符串 \*/
rt\_device\_write(serial, 0, str, (sizeof(str) - 1));
/\* 创建 serial 线程 \*/
rt\_thread\_t thread = rt\_thread\_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);
/\* 创建成功则启动线程 \*/
if (thread != RT_NULL)
{
rt\_thread\_startup(thread);
}
else
{
ret = RT_ERROR;
}
return ret;
}
/\* 导出到 msh 命令列表中 \*/
MSH\_CMD\_EXPORT(uart_dma_sample, uart device dma sample);
串口接收不定长数据
串口接收不定长数据需要用户在应用层进行处理,一般会有特定的协议,比如一帧数据可能会有起始标记位、数据长度位、数据、终止标记位等,发送数据帧时按照约定的协议进行发送,接收数据时再按照协议进行解析。
以下是一个简单的串口接收不定长数据示例代码,仅做了数据的结束标志位 DATA_CMD_END,如果遇到结束标志,则表示一帧数据结束。示例代码的主要步骤如下所示:
- 首先查找串口设备获取设备句柄。
- 初始化回调函数发送使用的信号量,然后以读写及中断接收方式打开串口设备。
- 设置串口设备的接收回调函数,之后发送字符串,并创建解析数据线程。
- 解析数据线程会尝试读取一个字符数据,如果没有数据则会挂起并等待信号量,当串口设备接收到一个数据时会触发中断并调用接收回调函数,此函数会发送信号量唤醒线程,此时线程会马上读取接收到的数据。在解析数据时,判断结束符,如果结束,则打印数据。
- 此示例代码不局限于特定的 BSP,根据 BSP 注册的串口设备,修改示例代码宏定义 SAMPLE_UART_NAME 对应的串口设备名称即可运行。
- 当一帧数据长度超过最大长度时,这将是一帧不合格的数据,因为后面接收到的字符将覆盖最后一个字符。
/\*
\* 程序清单:这是一个串口设备接收不定长数据的示例代码
\* 例程导出了 uart\_dma\_sample 命令到控制终端
\* 命令调用格式:uart\_dma\_sample uart2
\* 命令解释:命令第二个参数是要使用的串口设备名称,为空则使用默认的串口设备
\* 程序功能:通过串口 uart2 输出字符串"hello RT-Thread!",并通过串口 uart2 输入一串字符(不定长),再通过数据解析后,使用控制台显示有效数据。
\*/
#include <rtthread.h>
#define SAMPLE\_UART\_NAME "uart2"
#define DATA\_CMD\_END '\r' /\* 结束位设置为 \r,即回车符 \*/
#define ONE\_DATA\_MAXLEN 20 /\* 不定长数据的最大长度 \*/
/\* 用于接收消息的信号量 \*/
static struct rt\_semaphore rx_sem;
static rt\_device\_t serial;
/\* 接收数据回调函数 \*/
static rt\_err\_t uart\_rx\_ind(rt\_device\_t dev, rt\_size\_t size)
{
/\* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 \*/
if (size > 0)
{
rt\_sem\_release(&rx_sem);
}
return RT_EOK;
}
static char uart\_sample\_get\_char(void)
{
char ch;
while (rt\_device\_read(serial, 0, &ch, 1) == 0)
{
rt\_sem\_control(&rx_sem, RT_IPC_CMD_RESET, RT_NULL);
rt\_sem\_take(&rx_sem, RT_WAITING_FOREVER);
}
return ch;
}
/\* 数据解析线程 \*/
static void data\_parsing(void)
{
char ch;
char data[ONE_DATA_MAXLEN];
static char i = 0;
while (1)
{
ch = uart\_sample\_get\_char();
rt\_device\_write(serial, 0, &ch, 1);
if(ch == DATA_CMD_END)
{
data[i++] = '\0';
rt\_kprintf("data=%s\r\n",data);
i = 0;
continue;
}
i = (i >= ONE_DATA_MAXLEN-1) ? ONE_DATA_MAXLEN-1 : i;
data[i++] = ch;
}
}
static int uart\_data\_sample(int argc, char \*argv[])
{
rt\_err\_t ret = RT_EOK;
char uart_name[RT_NAME_MAX];
char str[] = "hello RT-Thread!\r\n";
if (argc == 2)
{
rt\_strncpy(uart_name, argv[1], RT_NAME_MAX);
}
else
{
rt\_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX);
}
/\* 查找系统中的串口设备 \*/
serial = rt\_device\_find(uart_name);
if (!serial)
{
rt\_kprintf("find %s failed!\n", uart_name);
return RT_ERROR;
}
/\* 初始化信号量 \*/
rt\_sem\_init(&rx_sem, "rx\_sem", 0, RT_IPC_FLAG_FIFO);
/\* 以中断接收及轮询发送模式打开串口设备 \*/
rt\_device\_open(serial, RT_DEVICE_FLAG_INT_RX);
/\* 设置接收回调函数 \*/
rt\_device\_set\_rx\_indicate(serial, uart_rx_ind);
/\* 发送字符串 \*/
rt\_device\_write(serial, 0, str, (sizeof(str) - 1));
/\* 创建 serial 线程 \*/
rt\_thread\_t thread = rt\_thread\_create("serial", (void (\*)(void \*parameter))data_parsing, RT_NULL, 1024, 25, 10);
/\* 创建成功则启动线程 \*/
if (thread != RT_NULL)
{
rt\_thread\_startup(thread);
}
else
{
ret = RT_ERROR;
}
return ret;
}
/\* 导出到 msh 命令列表中 \*/
MSH\_CMD\_EXPORT(uart_data_sample, uart device sample);
我有疑问: RT-Thread 官方论坛
UART 设备 v2 版本
已剪辑自: https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/uart/uart_v2/uart
Note
注:目前只有 github 的 master 分支上的 stm32l475-pandora 的 BSP 进行了串口 V2 版本的适配。
Note
注:如果用户已经清楚了解旧版本的串口框架,那么可直接跳过该文档的前部分关于串口介绍的内容,从访问串口设备章节开始查阅即可。
UART 简介
UART(Universal Asynchronous Receiver/Transmitter)通用异步收发传输器,UART 作为异步串口通信协议的一种,工作原理是将传输数据的每个字符一位接一位地传输。是在应用程序开发过程中使用频率最高的数据总线。
UART 串口的特点是将数据一位一位地顺序传送,只要 2 根传输线就可以实现双向通信,一根线发送数据的同时用另一根线接收数据。UART 串口通信有几个重要的参数,分别是波特率、起始位、数据位、停止位和奇偶检验位,对于两个使用 UART 串口通信的端口,这些参数必须匹配,否则通信将无法正常完成。UART 串口传输的数据格式如下图所示:
- 起始位:表示数据传输的开始,电平逻辑为 “0” 。
- 数据位:可能值有 5、6、7、8、9,表示传输这几个 bit 位数据。一般取值为 8,因为一个 ASCII 字符值为 8 位。
- 奇偶校验位:用于接收方对接收到的数据进行校验,校验 “1” 的位数为偶数 (偶校验) 或奇数(奇校验),以此来校验数据传送的正确性,使用时不需要此位也可以。
- 停止位: 表示一帧数据的结束。电平逻辑为 “1”。
- 波特率:串口通信时的速率,它用单位时间内传输的二进制代码的有效位 (bit) 数来表示,其单位为每秒比特数 bit/s(bps)。常见的波特率值有 4800、9600、14400、38400、115200 等,数值越大数据传输的越快,波特率为 115200 表示每秒钟传输 115200 位数据。
访问串口设备
应用程序通过 RT-Thread 提供的 I/O 设备管理接口来访问串口硬件,相关接口如下所示:
函数****描述
rt_device_find() | 查找设备 |
rt_device_open() | 打开设备 |
rt_device_read() | 读取数据 |
rt_device_write() | 写入数据 |
rt_device_control() | 控制设备 |
rt_device_set_rx_indicate() | 设置接收回调函数 |
rt_device_set_tx_complete() | 设置发送完成回调函数 |
rt_device_close() | 关闭设备 |
查找串口设备
应用程序根据串口设备名称获取设备句柄,进而可以操作串口设备,查找设备函数如下所示,
rt_device_t rt_device_find(const char* name);
参数****描述
name | 设备名称 |
返回 | —— |
设备句柄 | 查找到对应设备将返回相应的设备句柄 |
RT_NULL | 没有找到相应的设备对象 |
一般情况下,注册到系统的串口设备名称为 uart0,uart1 等,使用示例如下所示:
#define SAMPLE_UART_NAME "uart2" /* 串口设备名称 */
static rt_device_t serial; /* 串口设备句柄 */
/* 查找串口设备 */
serial = rt_device_find(SAMPLE_UART_NAME);
打开串口设备
通过设备句柄,应用程序可以打开和关闭设备,打开设备时,会检测设备是否已经初始化,没有初始化则会默认调用初始化接口初始化设备。通过如下函数打开设备:
rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags);
参数****描述
dev | 设备句柄 |
oflags | 设备模式标志 |
返回 | —— |
RT_EOK | 设备打开成功 |
-RT_EBUSY | 如果设备注册时指定的参数中包括 RT_DEVICE_FLAG_STANDALONE 参数,此设备将不允许重复打开 |
其他错误码 | 设备打开失败 |
oflags 参数支持下列取值 (可以采用或的方式支持多种取值):
/\* 接收模式参数 \*/
#define RT\_DEVICE\_FLAG\_RX\_BLOCKING 0x1000 /\* 接收阻塞模式 \*/
#define RT\_DEVICE\_FLAG\_RX\_NON\_BLOCKING 0x2000 /\* 接收非阻塞模式 \*/
/\* 发送模式参数 \*/
#define RT\_DEVICE\_FLAG\_TX\_BLOCKING 0x4000 /\* 发送阻塞模式 \*/
#define RT\_DEVICE\_FLAG\_TX\_NON\_BLOCKING 0x8000 /\* 发送非阻塞模式 \*/
#define RT\_DEVICE\_FLAG\_STREAM 0x040 /\* 流模式 \*/
用户使用串口时,不再根据硬件工作模式(轮询、中断、DMA)选择,而是根据具体的操作方式去配置,一般情况下,我们会选择使用 发送阻塞模式 以及 接收非阻塞模式 来进行开发。如下例子:
rt_device_open(dev, RT_DEVICE_FLAG_RX_NON_BLOCKING | RT_DEVICE_FLAG_TX_BLOCKING); // 串口设备使用模式为 (发送阻塞 接收非阻塞) 模式
Note
注:为了避免 阻塞 / 非阻塞模式 和 轮询 / 中断 / DMA 模式 在文中描述上可能存在的误解,故本文以 应用层操作模式 指代 阻塞 / 非阻塞模式,以 硬件工作模式 指代 轮询 / 中断 / DMA 模式。
而对于流模式 RT_DEVICE_FLAG_STREAM
,主要是当串口外设作为控制台时才会使用,该模式用来解决用户回车换行的问题,在正常的串口外设通信场景中,该模式一般不会使用。
Note
注:RT_DEVICE_FLAG_STREAM
流模式用于向串口终端输出字符串:当输出的字符是 "\n"
(对应 16 进制值为 0x0A)时,自动在前面输出一个 "\r"
(对应 16 进制值为 0x0D) 做分行。
流模式 RT_DEVICE_FLAG_STREAM 可以和接收发送模式参数使用或 “|” 运算符一起使用。
硬件工作模式选择
由于用户层使用串口时,只关心应用层操作模式,不再关心硬件工作模式,使得应用层开发变得更加便捷,也增加了应用程序的可移植性。倘若用户开发时比较关心硬件具体的工作模式,那么应该对其工作模式如何选择?
串口外设的遵循如下规则:
- 模式优先级为:DMA 模式 > 中断模式 > 轮询模式。即当有 DMA 配置时,默认使用 DMA 模式,以此类推。且非必要条件,不选择使用轮询模式。
- 串口默认配置接收和发送缓冲区
- 默认使用阻塞发送、非阻塞接收模式
Note
注:由于串口控制台的工作场景的独特性,其硬件工作模式为中断接收和轮询发送,用户使用时不建议参照串口控制台的模式进行配置,建议参照串口设备使用示例进行使用。
为了更加直观的表示应用层操作模式与硬件工作模式的对应关系,下面以图表和示例的方式进行说明。
发送端的模式对应关系如下表所示:
编号 配置发送缓冲区(有 / 无)说明 硬件工作模式(TX) 应用层操作模式(TX)
(1) | 不使用缓存区,且设置缓存区长度为0 | 轮询 | 阻塞 |
(2) | 不支持该模式 | 轮询 | 非阻塞 |
(3) | 使用缓存区 | 中断 | 阻塞 |
(4) | 使用缓存区 | 中断 | 非阻塞 |
(5) | 不使用缓存区,但需要设置缓冲区长度大于0 | DMA | 阻塞 |
(6) | 使用缓存区 | DMA | 非阻塞 |
对于编号 (1) 模式,如果必须使用轮询模式时,一定要将缓冲区大小配置为 0,因为如果缓冲区大小不为 0,在应用层使用发送阻塞模式时,将会使用中断模式(如果开 DMA,则使用 DMA 模式)。
对于编号 (2) 模式,当用户设置为 DMA 阻塞模式时,虽然设置了缓冲区不为 0,但是该缓冲区并不会进行初始化,而是直接进行 DMA 数据搬运。从而省去了内存搬运造成的性能下降的问题。需要注意的是,当使用 DMA 阻塞模式时,虽然不用缓冲区,但是也要将缓冲区长度设置为大于 0 的值,因为当缓冲区长度为 0 时,将会错误地使用轮询模式。
接收端的模式对应关系如下表所示:
编号 配置接收缓冲区(有 / 无)说明 硬件工作模式(RX) 应用层操作模式(RX)
(1) | 不使用缓存区,且设置缓存区长度为0 | 轮询 | 阻塞 |
(2) | 不支持该模式 | 轮询 | 非阻塞 |
(3) | 使用缓存区 | 中断 | 阻塞 |
(4) | 使用缓存区 | 中断 | 非阻塞 |
(5) | 使用缓存区 | DMA | 阻塞 |
(6) | 使用缓存区 | DMA | 非阻塞 |
对于编号 (1) 模式,如果必须使用轮询模式时,一定要将缓冲区大小配置为 0,因为如果缓冲区大小不为 0,在应用层使用接收阻塞模式时,将会使用中断模式(如果开 DMA,则使用 DMA 模式)。
下面举例说明如何配置硬件工作模式:
配置发送接收为 DMA 模式
在 menuconfig 中配置效果如下:
上图所示,对于 UART1 的配置为开启 DMA RX 和 DMA TX,且发送和接收缓存区大小设置为 1024 字节。
由此用户在应用层对串口的接收和发送的操作模式进行配置时,无论配置阻塞或者非阻塞,均使用的是 DMA 模式。
配置发送接收为中断模式
在 menuconfig 中配置效果如下:
上图所示,对于 UART1 的配置为关闭 DMA RX 和 DMA TX,且发送和接收缓存区大小设置为 1024 字节。
由此用户在应用层对串口的接收和发送的操作模式进行配置时,无论配置阻塞或者非阻塞,均使用的是中断模式。
配置发送 DMA 模式、接收中断模式
在 menuconfig 中配置效果如下:
上图所示,对于 UART1 的配置为关闭 DMA RX 和开启 DMA TX,且发送和接收缓存区大小设置为 1024 字节。
由此用户在应用层对串口的接收和发送的操作模式进行配置时,无论配置阻塞或者非阻塞,均使用的是 DMA 发送模式和中断接收模式。
在 menuconfig 中配置效果如下:
上图所示,对于 UART1 的配置为关闭 DMA RX 和 DMA TX,且发送和接收缓存区大小设置为 1024 字节。并且设置 UART1 TX buffer size
为 0。
由此用户在应用层对串口的接收和发送的操作模式进行配置时,发送只能使用阻塞模式,接收可以使用阻塞和非阻塞模式。串口控制台默认使用这样的配置模式,且操作模式为阻塞发送和非阻塞接收。
串口数据接收和发送数据的模式分为 3 种:中断模式、轮询模式、DMA 模式。在使用的时候,这 3 种模式只能选其一,若串口的打开参数 oflags 没有指定使用中断模式或者 DMA 模式,则默认使用轮询模式。
控制串口设备
通过控制接口,应用程序可以对串口设备进行配置,如波特率、数据位、校验位、接收缓冲区大小、停止位等参数的修改。控制函数如下所示:
rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg);
参数****描述
dev | 设备句柄 |
cmd | 命令控制字,可取值:RT_DEVICE_CTRL_CONFIG |
arg | 控制的参数,可取类型: struct serial_configure |
返回 | —— |
RT_EOK | 函数执行成功 |
-RT_ENOSYS | 执行失败,dev 为空 |
其他错误码 | 执行失败 |
控制参数结构体 struct serial_configure 原型如下:
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 rx_bufsz :16; /\* 接收数据缓冲区大小 \*/
rt\_uint32\_t tx_bufsz :16; /\* 发送数据缓冲区大小 \*/
rt\_uint32\_t reserved :4; /\* 保留位 \*/
};
RT-Thread 提供的配置参数可取值为如下宏定义:
/\* 波特率可取值 \*/
#define BAUD\_RATE\_2400 2400
#define BAUD\_RATE\_4800 4800
#define BAUD\_RATE\_9600 9600
#define BAUD\_RATE\_19200 19200
#define BAUD\_RATE\_38400 38400
#define BAUD\_RATE\_57600 57600
#define BAUD\_RATE\_115200 115200
#define BAUD\_RATE\_230400 230400
#define BAUD\_RATE\_460800 460800
#define BAUD\_RATE\_921600 921600
#define BAUD\_RATE\_2000000 2000000
#define BAUD\_RATE\_3000000 3000000
/\* 数据位可取值 \*/
#define DATA\_BITS\_5 5
#define DATA\_BITS\_6 6
#define DATA\_BITS\_7 7
#define DATA\_BITS\_8 8
#define DATA\_BITS\_9 9
/\* 停止位可取值 \*/
#define STOP\_BITS\_1 0
#define STOP\_BITS\_2 1
#define STOP\_BITS\_3 2
#define STOP\_BITS\_4 3
/\* 极性位可取值 \*/
#define PARITY\_NONE 0
#define PARITY\_ODD 1
#define PARITY\_EVEN 2
/\* 高低位顺序可取值 \*/
#define BIT\_ORDER\_LSB 0
#define BIT\_ORDER\_MSB 1
/\* 模式可取值 \*/
#define NRZ\_NORMAL 0 /\* normal mode \*/
#define NRZ\_INVERTED 1 /\* inverted mode \*/
#define RT\_SERIAL\_RX\_MINBUFSZ 64 /\* 限制接收缓冲区最小长度 \*/
#define RT\_SERIAL\_TX\_MINBUFSZ 64 /\* 限制发送缓冲区最小长度 \*/
RT-Thread 提供的默认串口配置如下,即 RT-Thread 系统中默认每个串口设备都使用如下配置:
/\* Default config for serial\_configure structure \*/
#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\_RX\_MINBUFSZ, /\* rxBuf size \*/ \
RT\_SERIAL\_TX\_MINBUFSZ, /\* txBuf size \*/ \
0 \
}
Note
注:虽然默认串口配置设置了 rx_bufsz 和 tx_bufsz 的大小,但是其缓冲区具体长度会在底层驱动初始化时再次配置,这里无需关心其值。
若实际使用串口的配置参数与默认配置参数不符,则用户可以通过应用代码进行修改。修改串口配置参数,如波特率、数据位、校验位、缓冲区接收 buffsize、停止位等的示例程序如下:
#define SAMPLE\_UART\_NAME "uart2" /\* 串口设备名称 \*/
static rt\_device\_t serial; /\* 串口设备句柄 \*/
struct serial\_configure config = RT_SERIAL_CONFIG_DEFAULT; /\* 初始化配置参数 \*/
/\* step1:查找串口设备 \*/
serial = rt\_device\_find(SAMPLE_UART_NAME);
/\* step2:修改串口配置参数 \*/
config.baud_rate = BAUD_RATE_9600; // 修改波特率为 9600
config.data_bits = DATA_BITS_8; // 数据位 8
config.stop_bits = STOP_BITS_1; // 停止位 1
config.rx_bufsz = 128; // 修改缓冲区 rx buff size 为 128
config.parity = PARITY_NONE; // 无奇偶校验位
/\* step3:控制串口设备。通过控制接口传入命令控制字,与控制参数 \*/
rt\_device\_control(serial, RT_DEVICE_CTRL_CONFIG, &config);
/\* step4:打开串口设备。以非阻塞接收和阻塞发送模式打开串口设备 \*/
rt\_device\_open(serial, RT_DEVICE_FLAG_RX_NON_BLOCKING | RT_DEVICE_FLAG_TX_BLOCKING);
发送数据
向串口中写入数据,可以通过如下函数完成:
rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos, const void* buffer, rt_size_t size);
参数****描述
dev | 设备句柄 |
pos | 写入数据偏移量,此参数串口设备未使用 |
buffer | 内存缓冲区指针,放置要写入的数据 |
size | 写入数据的大小 |
返回 | —— |
写入数据的实际大小 | 如果是字符设备,返回大小以字节为单位; |
0 | 需要读取当前线程的 errno 来判断错误状态 |
调用这个函数,会把缓冲区 buffer 中的数据写入到设备 dev 中,写入数据的大小是 size。
向串口写入数据示例程序如下所示:
#define SAMPLE\_UART\_NAME "uart2" /\* 串口设备名称 \*/
static rt\_device\_t serial; /\* 串口设备句柄 \*/
char str[] = "hello RT-Thread!\r\n";
struct serial\_configure config = RT_SERIAL_CONFIG_DEFAULT; /\* 配置参数 \*/
/\* 查找串口设备 \*/
serial = rt\_device\_find(SAMPLE_UART_NAME);
/\* 以非阻塞接收和阻塞发送模式打开串口设备 \*/
rt\_device\_open(serial, RT_DEVICE_FLAG_RX_NON_BLOCKING | RT_DEVICE_FLAG_TX_BLOCKING);
/\* 发送字符串 \*/
rt\_device\_write(serial, 0, str, (sizeof(str) - 1));
设置发送完成回调函数
在应用程序调用 rt_device_write()
写入数据时,如果底层硬件能够支持自动发送,那么上层应用可以设置一个回调函数。这个回调函数会在底层硬件数据发送完成后 (例如 DMA 传送完成或 FIFO 已经写入完毕产生完成中断时) 调用。可以通过如下函数设置设备发送完成指示 :
rt_err_t rt_device_set_tx_complete(rt_device_t dev, rt_err_t (*tx_done)(rt_device_t dev,void *buffer));
参数****描述
dev | 设备句柄 |
tx_done | 回调函数指针 |
返回 | —— |
RT_EOK | 设置成功 |
调用这个函数时,回调函数由调用者提供,当硬件设备发送完数据时,由设备驱动程序回调这个函数并把发送完成的数据块地址 buffer 作为参数传递给上层应用。上层应用(线程)在收到指示时会根据发送 buffer 的情况,释放 buffer 内存块或将其作为下一个写数据的缓存。
设置接收回调函数
可以通过如下函数来设置数据接收指示,当串口收到数据时,通知上层应用线程有数据到达 :
rt_err_t rt_device_set_rx_indicate(rt_device_t dev, rt_err_t (*rx_ind)(rt_device_t dev,rt_size_t size));
参数****描述
dev | 设备句柄 |
rx_ind | 回调函数指针 |
dev | 设备句柄(回调函数参数) |
size | 缓冲区数据大小(回调函数参数) |
返回 | —— |
RT_EOK | 设置成功 |
该函数的回调函数由调用者提供。若串口以中断接收模式打开,当串口接收到一个数据产生中断时,就会调用回调函数,并且会把此时缓冲区的数据大小放在 size 参数里,把串口设备句柄放在 dev 参数里供调用者获取。
若串口以 DMA 接收模式打开,当 DMA 完成一批数据的接收后会调用此回调函数。
一般情况下接收回调函数可以发送一个信号量或者事件通知串口数据处理线程有数据到达。使用示例如下所示:
#define SAMPLE\_UART\_NAME "uart2" /\* 串口设备名称 \*/
static rt\_device\_t serial; /\* 串口设备句柄 \*/
static struct rt\_semaphore rx_sem; /\* 用于接收消息的信号量 \*/
/\* 接收数据回调函数 \*/
static rt\_err\_t uart\_input(rt\_device\_t dev, rt\_size\_t size)
{
/\* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 \*/
rt\_sem\_release(&rx_sem);
return RT_EOK;
}
static int uart\_sample(int argc, char \*argv[])
{
serial = rt\_device\_find(SAMPLE_UART_NAME);
/\* 以非阻塞接收和阻塞发送模式打开串口设备 \*/
rt\_device\_open(serial, RT_DEVICE_FLAG_RX_NON_BLOCKING | RT_DEVICE_FLAG_TX_BLOCKING);
/\* 初始化信号量 \*/
rt\_sem\_init(&rx_sem, "rx\_sem", 0, RT_IPC_FLAG_FIFO);
/\* 设置接收回调函数 \*/
rt\_device\_set\_rx\_indicate(serial, uart_input);
}
接收数据
可调用如下函数读取串口接收到的数据:
rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size);
参数****描述
dev | 设备句柄 |
pos | 读取数据偏移量,此参数串口设备未使用 |
buffer | 缓冲区指针,读取的数据将会被保存在缓冲区中 |
size | 读取数据的大小 |
返回 | —— |
读到数据的实际大小 | 如果是字符设备,返回大小以字节为单位 |
0 | 需要读取当前线程的 errno 来判断错误状态 |
读取数据偏移量 pos 针对字符设备无效,此参数主要用于块设备中。
串口使用中断接收模式并配合接收回调函数的使用示例如下所示:
static rt\_device\_t serial; /\* 串口设备句柄 \*/
static struct rt\_semaphore rx_sem; /\* 用于接收消息的信号量 \*/
/\* 接收数据的线程 \*/
static void serial\_thread\_entry(void \*parameter)
{
char ch;
while (1)
{
/\* 从串口读取一个字节的数据,没有读取到则等待接收信号量 \*/
while (rt\_device\_read(serial, -1, &ch, 1) != 1)
{
/\* 阻塞等待接收信号量,等到信号量后再次读取数据 \*/
rt\_sem\_take(&rx_sem, RT_WAITING_FOREVER);
}
/\* 读取到的数据通过串口错位输出 \*/
ch = ch + 1;
rt\_device\_write(serial, 0, &ch, 1);
}
}
关闭串口设备
当应用程序完成串口操作后,可以关闭串口设备,通过如下函数完成:
rt_err_t rt_device_close(rt_device_t dev);
参数****描述
dev | 设备句柄 |
返回 | —— |
RT_EOK | 关闭设备成功 |
-RT_ERROR | 设备已经完全关闭,不能重复关闭设备 |
其他错误码 | 关闭设备失败 |
关闭设备接口和打开设备接口需配对使用,打开一次设备对应要关闭一次设备,这样设备才会被完全关闭,否则设备仍处于未关闭状态。
新旧版本串口使用区别
- 使用
rt_devide_open()
的入参oflags
区别:
// 旧版本 oflags 的参数取值
RT_DEVICE_FLAG_INT_RX
RT_DEVICE_FLAG_INT_TX
RT_DEVICE_FLAG_DMA_RX
RT_DEVICE_FLAG_DMA_TX
// 新版本 oflags 的参数取值
RT_DEVICE_FLAG_RX_NON_BLOCKING
RT_DEVICE_FLAG_RX_BLOCKING
RT_DEVICE_FLAG_TX_NON_BLOCKING
RT_DEVICE_FLAG_TX_BLOCKING
为了兼容旧版本的框架,使用新版本串口框架时旧版本的应用代码可直接使用,只需注意一点,旧版本的 oflags 参数不再起作用,默认使用新版本的操作模式: 接收非阻塞发送阻塞模式。
- 缓冲区宏定义区别
旧版本接收缓冲区统一为 RT_SERIAL_RB_BUFSZ
,旧版本没有发送缓冲区的设置。
新版本缓冲区进行了分离接收和发送,并且也可以对各个串口进行单独设置,例如:
// 设置 串口 2 的发送缓冲区为 256 字节,接收缓冲区为 1024 字节,见 rtconfig.h
#define BSP_UART2_RX_BUFSIZE 256
#define BSP_UART2_TX_BUFSIZE 1024
当从新版本往旧版本进行迁移时,如果使用了RT_SERIAL_RB_BUFSZ
,那么需要将本参数更改为对应的串口的具体的宏定义
- 串口配置
serial_configure
成员变量bufsz
的区别:
旧版本的 bufsz
指代串口接收缓冲区的大小,新版本由于需要分别设置发送和接收缓冲区,因此成员变量调整为 rx_bufsz
和 tx_bufsz
。
// 旧版本
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 reserved :6;
};
// 新版本
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 rx_bufsz :16;
rt\_uint32\_t tx_bufsz :16;
rt\_uint32\_t reserved :6;
};
串口设备使用示例
非阻塞接收和阻塞发送模式
当串口接收到一批数据后会调用接收回调函数,接收回调函数会把此时缓冲区的数据大小通过消息队列发送给等待的数据处理线程。线程获取到消息后被激活,并读取数据。
此例程以开启了 DMA 发送和接收模式为例,一般情况下 DMA 接收模式会结合 DMA 接收半完成中断、完成中断和串口空闲中断完成数据接收。
- 此示例代码不局限于特定的 BSP,根据 BSP 注册的串口设备,修改示例代码宏定义 SAMPLE_UART_NAME 对应的串口设备名称即可运行。
运行序列图如下图所示:
/\*
\* 程序清单:这是一个串口设备 开启 DMA 模式后使用例程
\* 例程导出了 uart\_dma\_sample 命令到控制终端
\* 命令调用格式:uart\_dma\_sample uart1
\* 命令解释:命令第二个参数是要使用的串口设备名称,为空则使用默认的串口设备
\* 程序功能:通过串口输出字符串 "hello RT-Thread!",并通过串口输出接收到的数据,然后打印接收到的数据。
\*/
#include <rtthread.h>
#include <rtdevice.h>
#define SAMPLE\_UART\_NAME "uart1" /\* 串口设备名称 \*/
/\* 串口接收消息结构 \*/
struct rx\_msg
{
rt\_device\_t dev;
rt\_size\_t size;
};
/\* 串口设备句柄 \*/
static rt\_device\_t serial;
/\* 消息队列控制块 \*/
static struct rt\_messagequeue rx_mq;
/\* 接收数据回调函数 \*/
static rt\_err\_t uart\_input(rt\_device\_t dev, rt\_size\_t size)
{
struct rx\_msg msg;
rt\_err\_t result;
msg.dev = dev;
msg.size = size;
result = rt\_mq\_send(&rx_mq, &msg, sizeof(msg));
if (result == -RT_EFULL)
{
/\* 消息队列满 \*/
rt\_kprintf("message queue full!\n");
}
return result;
}
static void serial\_thread\_entry(void \*parameter)
{
struct rx\_msg msg;
rt\_err\_t result;
rt\_uint32\_t rx_length;
static char rx_buffer[BSP_UART1_RX_BUFSIZE + 1];
while (1)
{
rt\_memset(&msg, 0, sizeof(msg));
/\* 从消息队列中读取消息 \*/
result = rt\_mq\_recv(&rx_mq, &msg, sizeof(msg), RT_WAITING_FOREVER);
if (result == RT_EOK)
{
/\* 从串口读取数据 \*/
rx_length = rt\_device\_read(msg.dev, 0, rx_buffer, msg.size);
rx_buffer[rx_length] = '\0';
/\* 通过串口设备 serial 输出读取到的消息 \*/
rt\_device\_write(serial, 0, rx_buffer, rx_length);
/\* 打印数据 \*/
rt\_kprintf("%s\n",rx_buffer);
}
}
}
static int uart\_dma\_sample(int argc, char \*argv[])
{
rt\_err\_t ret = RT_EOK;
char uart_name[RT_NAME_MAX];
static char msg_pool[256];
char str[] = "hello RT-Thread!\r\n";
if (argc == 2)
{
rt\_strncpy(uart_name, argv[1], RT_NAME_MAX);
}
else
{
rt\_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX);
}
/\* 查找串口设备 \*/
serial = rt\_device\_find(uart_name);
if (!serial)
{
rt\_kprintf("find %s failed!\n", uart_name);
return RT_ERROR;
}
/\* 初始化消息队列 \*/
rt\_mq\_init(&rx_mq, "rx\_mq",
msg_pool, /\* 存放消息的缓冲区 \*/
sizeof(struct rx\_msg), /\* 一条消息的最大长度 \*/
sizeof(msg_pool), /\* 存放消息的缓冲区大小 \*/
RT_IPC_FLAG_FIFO); /\* 如果有多个线程等待,按照先来先得到的方法分配消息 \*/
/\* 以 DMA 接收及轮询发送方式打开串口设备 \*/
rt\_device\_open(serial, RT_DEVICE_FLAG_RX_NON_BLOCKING | RT_DEVICE_FLAG_TX_BLOCKING);
/\* 设置接收回调函数 \*/
rt\_device\_set\_rx\_indicate(serial, uart_input);
/\* 发送字符串 \*/
rt\_device\_write(serial, 0, str, (sizeof(str) - 1));
/\* 创建 serial 线程 \*/
rt\_thread\_t thread = rt\_thread\_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);
/\* 创建成功则启动线程 \*/
if (thread != RT_NULL)
{
rt\_thread\_startup(thread);
}
else
{
ret = RT_ERROR;
}
return ret;
}
/\* 导出到 msh 命令列表中 \*/
MSH\_CMD\_EXPORT(uart_dma_sample, uart device dma sample);
我有疑问: RT-Thread 官方论坛
PIN 设备
已剪辑自: https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/pin/pin
引脚简介
芯片上的引脚一般分为 4 类:电源、时钟、控制与 I/O,I/O 口在使用模式上又分为 General Purpose Input Output(通用输入 / 输出),简称 GPIO,与功能复用 I/O(如 SPI/I2C/UART 等)。
大多数 MCU 的引脚都不止一个功能。不同引脚内部结构不一样,拥有的功能也不一样。可以通过不同的配置,切换引脚的实际功能。通用 I/O 口主要特性如下:
- 可编程控制中断:中断触发模式可配置,一般有下图所示 5 种中断触发模式:
- 输入输出模式可控制。
- 输出模式一般包括:推挽、开漏、上拉、下拉。引脚为输出模式时,可以通过配置引脚输出的电平状态为高电平或低电平来控制连接的外围设备。
- 输入模式一般包括:浮空、上拉、下拉、模拟。引脚为输入模式时,可以读取引脚的电平状态,即高电平或低电平。
访问 PIN 设备
应用程序通过 RT-Thread 提供的 PIN 设备管理接口来访问 GPIO,相关接口如下所示:
函数****描述
rt_pin_get() | 获取引脚编号 |
rt_pin_mode() | 设置引脚模式 |
rt_pin_write() | 设置引脚电平 |
rt_pin_read() | 读取引脚电平 |
rt_pin_attach_irq() | 绑定引脚中断回调函数 |
rt_pin_irq_enable() | 使能引脚中断 |
rt_pin_detach_irq() | 脱离引脚中断回调函数 |
获取引脚编号
RT-Thread 提供的引脚编号需要和芯片的引脚号区分开来,它们并不是同一个概念,引脚编号由 PIN 设备驱动程序定义,和具体的芯片相关。有3种方式可以获取引脚编号: API 接口获取、使用宏定义或者查看PIN 驱动文件。
使用 API
使用 rt_pin_get() 获取引脚编号,如下获取 PF9 的引脚编号:
pin_number = rt_pin_get("PF.9");
使用宏定义
如果使用 rt-thread/bsp/stm32
目录下的 BSP 则可以使用下面的宏获取引脚编号:
GET_PIN(port, pin)
获取引脚号为 PF9 的 LED0 对应的引脚编号的示例代码如下所示:
#define LED0_PIN GET_PIN(F, 9)
查看驱动文件
如果使用其他 BSP 则需要查看 PIN 驱动代码 drv_gpio.c 文件确认引脚编号。此文件里有一个数组存放了每个 PIN 脚对应的编号信息,如下所示:
static const rt\_uint16\_t pins[] =
{
__STM32_PIN_DEFAULT,
__STM32_PIN_DEFAULT,
\_\_STM32\_PIN(2, A, 15),
\_\_STM32\_PIN(3, B, 5),
\_\_STM32\_PIN(4, B, 8),
__STM32_PIN_DEFAULT,
__STM32_PIN_DEFAULT,
__STM32_PIN_DEFAULT,
\_\_STM32\_PIN(8, A, 14),
\_\_STM32\_PIN(9, B, 6),
... ...
}
以__STM32_PIN(2, A, 15)
为例,2 为 RT-Thread 使用的引脚编号,A 为端口号,15 为引脚号,所以 PA15 对应的引脚编号为 2。
设置引脚模式
引脚在使用前需要先设置好输入或者输出模式,通过如下函数完成:
void rt_pin_mode(rt_base_t pin, rt_base_t mode);
参数****描述
pin | 引脚编号 |
mode | 引脚工作模式 |
目前 RT-Thread 支持的引脚工作模式可取如所示的 5 种宏定义值之一,每种模式对应的芯片实际支持的模式需参考 PIN 设备驱动程序的具体实现:
#define PIN_MODE_OUTPUT 0x00 /* 输出 */
#define PIN_MODE_INPUT 0x01 /* 输入 */
#define PIN_MODE_INPUT_PULLUP 0x02 /* 上拉输入 */
#define PIN_MODE_INPUT_PULLDOWN 0x03 /* 下拉输入 */
#define PIN_MODE_OUTPUT_OD 0x04 /* 开漏输出 */
使用示例如下所示:
#define BEEP_PIN_NUM 35 /* PB0 */
/* 蜂鸣器引脚为输出模式 */
rt_pin_mode(BEEP_PIN_NUM, PIN_MODE_OUTPUT);
设置引脚电平
设置引脚输出电平的函数如下所示:
void rt_pin_write(rt_base_t pin, rt_base_t value);
参数****描述
pin | 引脚编号 |
value | 电平逻辑值,可取 2 种宏定义值之一:PIN_LOW 低电平,PIN_HIGH 高电平 |
使用示例如下所示:
#define BEEP_PIN_NUM 35 /* PB0 */
/* 蜂鸣器引脚为输出模式 */
rt_pin_mode(BEEP_PIN_NUM, PIN_MODE_OUTPUT);
/* 设置低电平 */
rt_pin_write(BEEP_PIN_NUM, PIN_LOW);
读取引脚电平
读取引脚电平的函数如下所示:
int rt_pin_read(rt_base_t pin);
参数****描述
pin | 引脚编号 |
返回 | —— |
PIN_LOW | 低电平 |
PIN_HIGH | 高电平 |
使用示例如下所示:
#define BEEP_PIN_NUM 35 /* PB0 */
int status;
/* 蜂鸣器引脚为输出模式 */
rt_pin_mode(BEEP_PIN_NUM, PIN_MODE_OUTPUT);
/* 设置低电平 */
rt_pin_write(BEEP_PIN_NUM, PIN_LOW);
status = rt_pin_read(BEEP_PIN_NUM);
绑定引脚中断回调函数
若要使用到引脚的中断功能,可以使用如下函数将某个引脚配置为某种中断触发模式并绑定一个中断回调函数到对应引脚,当引脚中断发生时,就会执行回调函数:
rt_err_t rt_pin_attach_irq(rt_int32_t pin, rt_uint32_t mode,
void (*hdr)(void *args), void *args);
参数****描述
pin | 引脚编号 |
mode | 中断触发模式 |
hdr | 中断回调函数,用户需要自行定义这个函数 |
args | 中断回调函数的参数,不需要时设置为 RT_NULL |
返回 | —— |
RT_EOK | 绑定成功 |
错误码 | 绑定失败 |
中断触发模式 mode 可取如下 5 种宏定义值之一:
#define PIN_IRQ_MODE_RISING 0x00 /* 上升沿触发 */
#define PIN_IRQ_MODE_FALLING 0x01 /* 下降沿触发 */
#define PIN_IRQ_MODE_RISING_FALLING 0x02 /* 边沿触发(上升沿和下降沿都触发)*/
#define PIN_IRQ_MODE_HIGH_LEVEL 0x03 /* 高电平触发 */
#define PIN_IRQ_MODE_LOW_LEVEL 0x04 /* 低电平触发 */
使用示例如下所示:
#define KEY0\_PIN\_NUM 55 /\* PD8 \*/
/\* 中断回调函数 \*/
void beep\_on(void \*args)
{
rt\_kprintf("turn on beep!\n");
rt\_pin\_write(BEEP_PIN_NUM, PIN_HIGH);
}
static void pin\_beep\_sample(void)
{
/\* 按键0引脚为输入模式 \*/
rt\_pin\_mode(KEY0_PIN_NUM, PIN_MODE_INPUT_PULLUP);
/\* 绑定中断,下降沿模式,回调函数名为beep\_on \*/
rt\_pin\_attach\_irq(KEY0_PIN_NUM, PIN_IRQ_MODE_FALLING, beep_on, RT_NULL);
}
使能引脚中断
绑定好引脚中断回调函数后使用下面的函数使能引脚中断:
rt_err_t rt_pin_irq_enable(rt_base_t pin, rt_uint32_t enabled);
参数****描述
pin | 引脚编号 |
enabled | 状态,可取 2 种值之一:PIN_IRQ_ENABLE(开启),PIN_IRQ_DISABLE(关闭) |
返回 | —— |
RT_EOK | 使能成功 |
错误码 | 使能失败 |
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
#define BEEP_PIN_NUM 35 /* PB0 */
/* 蜂鸣器引脚为输出模式 */
rt_pin_mode(BEEP_PIN_NUM, PIN_MODE_OUTPUT);
设置引脚电平
设置引脚输出电平的函数如下所示:
void rt_pin_write(rt_base_t pin, rt_base_t value);
参数****描述
pin | 引脚编号 |
value | 电平逻辑值,可取 2 种宏定义值之一:PIN_LOW 低电平,PIN_HIGH 高电平 |
使用示例如下所示:
#define BEEP_PIN_NUM 35 /* PB0 */
/* 蜂鸣器引脚为输出模式 */
rt_pin_mode(BEEP_PIN_NUM, PIN_MODE_OUTPUT);
/* 设置低电平 */
rt_pin_write(BEEP_PIN_NUM, PIN_LOW);
读取引脚电平
读取引脚电平的函数如下所示:
int rt_pin_read(rt_base_t pin);
参数****描述
pin | 引脚编号 |
返回 | —— |
PIN_LOW | 低电平 |
PIN_HIGH | 高电平 |
使用示例如下所示:
#define BEEP_PIN_NUM 35 /* PB0 */
int status;
/* 蜂鸣器引脚为输出模式 */
rt_pin_mode(BEEP_PIN_NUM, PIN_MODE_OUTPUT);
/* 设置低电平 */
rt_pin_write(BEEP_PIN_NUM, PIN_LOW);
status = rt_pin_read(BEEP_PIN_NUM);
绑定引脚中断回调函数
若要使用到引脚的中断功能,可以使用如下函数将某个引脚配置为某种中断触发模式并绑定一个中断回调函数到对应引脚,当引脚中断发生时,就会执行回调函数:
rt_err_t rt_pin_attach_irq(rt_int32_t pin, rt_uint32_t mode,
void (*hdr)(void *args), void *args);
参数****描述
pin | 引脚编号 |
mode | 中断触发模式 |
hdr | 中断回调函数,用户需要自行定义这个函数 |
args | 中断回调函数的参数,不需要时设置为 RT_NULL |
返回 | —— |
RT_EOK | 绑定成功 |
错误码 | 绑定失败 |
中断触发模式 mode 可取如下 5 种宏定义值之一:
#define PIN_IRQ_MODE_RISING 0x00 /* 上升沿触发 */
#define PIN_IRQ_MODE_FALLING 0x01 /* 下降沿触发 */
#define PIN_IRQ_MODE_RISING_FALLING 0x02 /* 边沿触发(上升沿和下降沿都触发)*/
#define PIN_IRQ_MODE_HIGH_LEVEL 0x03 /* 高电平触发 */
#define PIN_IRQ_MODE_LOW_LEVEL 0x04 /* 低电平触发 */
使用示例如下所示:
#define KEY0\_PIN\_NUM 55 /\* PD8 \*/
/\* 中断回调函数 \*/
void beep\_on(void \*args)
{
rt\_kprintf("turn on beep!\n");
rt\_pin\_write(BEEP_PIN_NUM, PIN_HIGH);
}
static void pin\_beep\_sample(void)
{
/\* 按键0引脚为输入模式 \*/
rt\_pin\_mode(KEY0_PIN_NUM, PIN_MODE_INPUT_PULLUP);
/\* 绑定中断,下降沿模式,回调函数名为beep\_on \*/
rt\_pin\_attach\_irq(KEY0_PIN_NUM, PIN_IRQ_MODE_FALLING, beep_on, RT_NULL);
}
使能引脚中断
绑定好引脚中断回调函数后使用下面的函数使能引脚中断:
rt_err_t rt_pin_irq_enable(rt_base_t pin, rt_uint32_t enabled);
参数****描述
pin | 引脚编号 |
enabled | 状态,可取 2 种值之一:PIN_IRQ_ENABLE(开启),PIN_IRQ_DISABLE(关闭) |
返回 | —— |
RT_EOK | 使能成功 |
错误码 | 使能失败 |
[外链图片转存中…(img-rHcgpVgR-1715720115563)]
[外链图片转存中…(img-webymsBf-1715720115563)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!