Go最新RT-Thread设备和驱动总结_rtt 设备驱动模型(1),2024Golang通用流行框架大全

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取


#### [关闭串口设备]( )


当应用程序完成串口操作后,可以关闭串口设备,通过如下函数完成:



rt_err_t rt_device_close(rt_device_t dev);


**参数\*\*\*\*描述**




|  |  |
| --- | --- |
| dev | 设备句柄 |
| **返回** | —— |
| RT\_EOK | 关闭设备成功 |
| -RT\_ERROR | 设备已经完全关闭,不能重复关闭设备 |
| 其他错误码 | 关闭设备失败 |


关闭设备接口和打开设备接口需配对使用,打开一次设备对应要关闭一次设备,这样设备才会被完全关闭,否则设备仍处于未关闭状态。


### [串口设备使用示例]( )


#### [中断接收及轮询发送]( )


示例代码的主要步骤如下所示:


1. 首先查找串口设备获取设备句柄。
2. 初始化回调函数发送使用的信号量,然后以读写及中断接收方式打开串口设备。
3. 设置串口设备的接收回调函数,之后发送字符串,并创建读取数据线程。


* 读取数据线程会尝试读取一个字符数据,如果没有数据则会挂起并等待信号量,当串口设备接收到一个数据时会触发中断并调用接收回调函数,此函数会发送信号量唤醒线程,此时线程会马上读取接收到的数据。
* 此示例代码不局限于特定的 BSP,根据 BSP 注册的串口设备,修改示例代码宏定义 SAMPLE\_UART\_NAME 对应的串口设备名称即可运行。


运行序列图如下图所示:


![串口中断接收及轮询发送序列图](https://img-blog.csdnimg.cn/img_convert/abbe10100184a5178a2d9283dc848b0a.png)



/*
* 程序清单:这是一个 串口 设备使用例程
* 例程导出了 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接收及轮询发送序列图](https://img-blog.csdnimg.cn/img_convert/8dd12c5a7989ed28796db5ee27b7373b.png)



/*
* 程序清单:这是一个串口设备 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,如果遇到结束标志,则表示一帧数据结束。示例代码的主要步骤如下所示:


1. 首先查找串口设备获取设备句柄。
2. 初始化回调函数发送使用的信号量,然后以读写及中断接收方式打开串口设备。
3. 设置串口设备的接收回调函数,之后发送字符串,并创建解析数据线程。


* 解析数据线程会尝试读取一个字符数据,如果没有数据则会挂起并等待信号量,当串口设备接收到一个数据时会触发中断并调用接收回调函数,此函数会发送信号量唤醒线程,此时线程会马上读取接收到的数据。在解析数据时,判断结束符,如果结束,则打印数据。
* 此示例代码不局限于特定的 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 串口传输的数据格式如下图所示:


![串口传输数据格式](https://img-blog.csdnimg.cn/img_convert/d8c4700f6ba80cedc7f63a565479fa56.png)


* 起始位:表示数据传输的开始,电平逻辑为 “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 可以和接收发送模式参数使用或 “|” 运算符一起使用。


##### [硬件工作模式选择]( )


由于用户层使用串口时,只关心应用层操作模式,不再关心硬件工作模式,使得应用层开发变得更加便捷,也增加了应用程序的可移植性。倘若用户开发时比较关心硬件具体的工作模式,那么应该对其工作模式如何选择?


串口外设的遵循如下规则:


1. 模式优先级为:DMA 模式 > 中断模式 > 轮询模式。即当有 DMA 配置时,默认使用 DMA 模式,以此类推。且非必要条件,不选择使用轮询模式。
2. 串口默认配置接收和发送缓冲区
3. 默认使用阻塞发送、非阻塞接收模式


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 中配置效果如下:


![menuconfig](https://img-blog.csdnimg.cn/img_convert/efb7f001028440d2b0eb732f766bedbf.jpeg)


上图所示,对于 UART1 的配置为开启 DMA RX 和 DMA TX,且发送和接收缓存区大小设置为 1024 字节。


由此用户在应用层对串口的接收和发送的操作模式进行配置时,无论配置阻塞或者非阻塞,均使用的是 DMA 模式。


###### [配置发送接收为中断模式]( )


在 menuconfig 中配置效果如下:


![menuconfig](https://img-blog.csdnimg.cn/img_convert/74d62a908928d4b470bc571ab4169569.jpeg)


上图所示,对于 UART1 的配置为关闭 DMA RX 和 DMA TX,且发送和接收缓存区大小设置为 1024 字节。


由此用户在应用层对串口的接收和发送的操作模式进行配置时,无论配置阻塞或者非阻塞,均使用的是中断模式。


###### [配置发送 DMA 模式、接收中断模式]( )


在 menuconfig 中配置效果如下:


![menuconfig](https://img-blog.csdnimg.cn/img_convert/2037b188bf960ffeb9e8b8b87df44cc2.jpeg)


上图所示,对于 UART1 的配置为关闭 DMA RX 和开启 DMA TX,且发送和接收缓存区大小设置为 1024 字节。


由此用户在应用层对串口的接收和发送的操作模式进行配置时,无论配置阻塞或者非阻塞,均使用的是 DMA 发送模式和中断接收模式。


在 menuconfig 中配置效果如下:


![menuconfig](https://img-blog.csdnimg.cn/img_convert/70701f6cc27d651e02a1142ba8aeb84d.jpeg)


上图所示,对于 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 接收及轮询发送序列图](https://img-blog.csdnimg.cn/img_convert/3378db536a3d3a8c978a18de9b7e54eb.png)



/*
* 程序清单:这是一个串口设备 开启 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 种中断触发模式:


![5 种中断触发模式](https://img-blog.csdnimg.cn/img_convert/879990c5b28a537e410a3f465d082f26.png)


* 输入输出模式可控制。
	+ 输出模式一般包括:推挽、开漏、上拉、下拉。引脚为输出模式时,可以通过配置引脚输出的电平状态为高电平或低电平来控制连接的外围设备。
	+ 输入模式一般包括:浮空、上拉、下拉、模拟。引脚为输入模式时,可以读取引脚的电平状态,即高电平或低电平。


### [访问 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 | 使能成功 |
| 错误码 | 使能失败 |


使用示例如下所示:



#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_pin_irq_enable(KEY0_PIN_NUM, PIN_IRQ_ENABLE);
}


#### [脱离引脚中断回调函数]( )


可以使用如下函数脱离引脚中断回调函数:



rt_err_t rt_pin_detach_irq(rt_int32_t pin);


**参数\*\*\*\*描述**




|  |  |
| --- | --- |
| pin | 引脚编号 |
| **返回** | —— |
| RT\_EOK | 脱离成功 |
| 错误码 | 脱离失败 |


引脚脱离了中断回调函数以后,中断并没有关闭,还可以调用绑定中断回调函数再次绑定其他回调函数。



#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_pin_irq_enable(KEY0_PIN_NUM, PIN_IRQ_ENABLE);
/* 脱离中断回调函数 */
rt_pin_detach_irq(KEY0_PIN_NUM);
}


### [PIN 设备使用示例]( )


PIN 设备的具体使用方式可以参考如下示例代码,示例代码的主要步骤如下:


1. 设置蜂鸣器对应引脚为输出模式,并给一个默认的低电平状态。
2. 设置按键 0 和 按键1 对应引脚为输入模式,然后绑定中断回调函数并使能中断。
3. 按下按键 0 蜂鸣器开始响,按下按键 1 蜂鸣器停止响。



/*
* 程序清单:这是一个 PIN 设备使用例程
* 例程导出了 pin_beep_sample 命令到控制终端
* 命令调用格式:pin_beep_sample
* 程序功能:通过按键控制蜂鸣器对应引脚的电平状态控制蜂鸣器
*/

#include <rtthread.h>
#include <rtdevice.h>

/* 引脚编号,通过查看设备驱动文件drv_gpio.c确定 */
#ifndef BEEP_PIN_NUM
#define BEEP_PIN_NUM 35 /* PB0 */
#endif
#ifndef KEY0_PIN_NUM
#define KEY0_PIN_NUM 55 /* PD8 */
#endif
#ifndef KEY1_PIN_NUM
#define KEY1_PIN_NUM 56 /* PD9 */
#endif

void beep_on(void *args)
{
rt_kprintf(“turn on beep!\n”);

rt\_pin\_write(BEEP_PIN_NUM, PIN_HIGH);

}

void beep_off(void *args)
{
rt_kprintf(“turn off beep!\n”);

rt\_pin\_write(BEEP_PIN_NUM, PIN_LOW);

}

static void pin_beep_sample(void)
{
/* 蜂鸣器引脚为输出模式 */
rt_pin_mode(BEEP_PIN_NUM, PIN_MODE_OUTPUT);
/* 默认低电平 */
rt_pin_write(BEEP_PIN_NUM, PIN_LOW);

/\* 按键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\_pin\_irq\_enable(KEY0_PIN_NUM, PIN_IRQ_ENABLE);

/\* 按键1引脚为输入模式 \*/
rt\_pin\_mode(KEY1_PIN_NUM, PIN_MODE_INPUT_PULLUP);
/\* 绑定中断,下降沿模式,回调函数名为beep\_off \*/
rt\_pin\_attach\_irq(KEY1_PIN_NUM, PIN_IRQ_MODE_FALLING, beep_off, RT_NULL);
/\* 使能中断 \*/
rt\_pin\_irq\_enable(KEY1_PIN_NUM, PIN_IRQ_ENABLE);

}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(pin_beep_sample, pin beep sample);


我有疑问: [RT-Thread 官方论坛]( )




---


## ADC 设备


已剪辑自: https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/adc/adc


### [ADC 简介]( )


ADC(Analog-to-Digital Converter) 指模数转换器。是指将连续变化的模拟信号转换为离散的数字信号的器件。真实世界的模拟信号,例如温度、压力、声音或者图像等,需要转换成更容易储存、处理和发射的数字形式。模数转换器可以实现这个功能,在各种不同的产品中都可以找到它的身影。与之相对应的 DAC(Digital-to-Analog Converter),它是 ADC 模数转换的逆向过程。ADC 最早用于对无线信号向数字信号转换。如电视信号,长短播电台发射接收等。


#### [转换过程]( )


如下图所示模数转换一般要经过采样、保持和量化、编码这几个步骤。在实际电路中,有些过程是合并进行的,如采样和保持,量化和编码在转换过程中是同时实现的。


![ADC 转换过程](https://img-blog.csdnimg.cn/img_convert/389d396b6bdea0fbf01506606c5b6f53.png)


采样是将时间上连续变化的模拟信号转换为时间上离散的模拟信号。采样取得的模拟信号转换为数字信号都需要一定时间,为了给后续的量化编码过程提供一个稳定的值,在采样电路后要求将所采样的模拟信号保持一段时间。


将数值连续的模拟量转换为数字量的过程称为量化。数字信号在数值上是离散的。采样保持电路的输出电压还需要按照某种近似方式归化到与之相应的离散电平上,任何数字量只能是某个最小数量单位的整数倍。量化后的数值最后还需要编码过程,也就是 A/D 转换器输出的数字量。


#### [分辨率]( )


分辨率以二进制(或十进制)数的位数来表示,一般有8位、10位、12位、16位等,它说明模数转换器对输入信号的分辨能力,位数越多,表示分辨率越高,恢复模拟信号时会更精确。


#### [精度]( )


精度表示 ADC 器件在所有的数值点上对应的模拟值和真实值之间的最大误差值,也就是输出数值偏离线性最大的距离。


Note


注:精度与分辨率是两个不一样的概念,请注意区分。


#### [转换速率]( )


转换速率是指 A/D 转换器完成一次从模拟到数字的 AD 转换所需时间的倒数。例如,某 A/D 转换器的转换速率为 1MHz,则表示完成一次 AD 转换时间为 1 微秒。


### [访问 ADC 设备]( )


应用程序通过 RT-Thread 提供的 ADC 设备管理接口来访问 ADC 硬件,相关接口如下所示:


**函数\*\*\*\*描述**




|  |  |
| --- | --- |
| rt\_device\_find() | 根据 ADC 设备名称查找设备获取设备句柄 |
| rt\_adc\_enable() | 使能 ADC 设备 |
| rt\_adc\_read() | 读取 ADC 设备数据 |
| rt\_adc\_disable() | 关闭 ADC 设备 |


#### [查找 ADC 设备]( )


应用程序根据 ADC 设备名称获取设备句柄,进而可以操作 ADC 设备,查找设备函数如下所示:



rt_device_t rt_device_find(const char* name);


**参数\*\*\*\*描述**




|  |  |
| --- | --- |
| name | ADC 设备名称 |
| **返回** | —— |
| 设备句柄 | 查找到对应设备将返回相应的设备句柄 |
| RT\_NULL | 没有找到设备 |


一般情况下,注册到系统的 ADC 设备名称为 adc0,adc1等,使用示例如下所示:



#define ADC_DEV_NAME “adc1” /* ADC 设备名称 /
rt_adc_device_t adc_dev; /
ADC 设备句柄 /
/
查找设备 */
adc_dev = (rt_adc_device_t)rt_device_find(ADC_DEV_NAME);


#### [使能 ADC 通道]( )


在读取 ADC 设备数据前需要先使能设备,通过如下函数使能设备:



rt_err_t rt_adc_enable(rt_adc_device_t dev, rt_uint32_t channel);


**参数\*\*\*\*描述**




|  |  |
| --- | --- |
| dev | ADC 设备句柄 |
| channel | ADC 通道 |
| **返回** | —— |
| RT\_EOK | 成功 |
| -RT\_ENOSYS | 失败,设备操作方法为空 |
| 其他错误码 | 失败 |


使用示例如下所示:



#define ADC_DEV_NAME “adc1” /* ADC 设备名称 /
#define ADC_DEV_CHANNEL 5 /
ADC 通道 /
rt_adc_device_t adc_dev; /
ADC 设备句柄 /
/
查找设备 /
adc_dev = (rt_adc_device_t)rt_device_find(ADC_DEV_NAME);
/
使能设备 */
rt_adc_enable(adc_dev, ADC_DEV_CHANNEL);


#### [读取 ADC 通道采样值]( )


读取 ADC 通道采样值可通过如下函数完成:



rt_uint32_t rt_adc_read(rt_adc_device_t dev, rt_uint32_t channel);


**参数\*\*\*\*描述**




|  |  |
| --- | --- |
| dev | ADC 设备句柄 |
| channel | ADC 通道 |
| **返回** | —— |
| 读取的数值 |  |


使用 ADC 采样电压值的使用示例如下所示:



#define ADC_DEV_NAME “adc1” /* ADC 设备名称 */
#define ADC_DEV_CHANNEL 5 /* ADC 通道 */
#define REFER_VOLTAGE 330 /* 参考电压 3.3V,数据精度乘以100保留2位小数*/
#define CONVERT_BITS (1 << 12) /* 转换位数为12位 */

rt_adc_device_t adc_dev; /* ADC 设备句柄 */
rt_uint32_t value;
/* 查找设备 */
adc_dev = (rt_adc_device_t)rt_device_find(ADC_DEV_NAME);
/* 使能设备 */
rt_adc_enable(adc_dev, ADC_DEV_CHANNEL);
/* 读取采样值 */
value = rt_adc_read(adc_dev, ADC_DEV_CHANNEL);
/* 转换为对应电压值 */
vol = value * REFER_VOLTAGE / CONVERT_BITS;
rt_kprintf(“the voltage is :%d.%02d \n”, vol / 100, vol % 100);


实际电压值的计算公式为:采样值 \* 参考电压 / (1 << 分辨率位数),上面示例代码乘以 100 将数据放大,最后通过 vol / 100 获得电压的整数位值,通过 vol % 100 获得电压的小数位值。


#### [关闭 ADC 通道]( )


关闭 ADC 通道可通过如下函数完成:



rt_err_t rt_adc_disable(rt_adc_device_t dev, rt_uint32_t channel);


**参数\*\*\*\*描述**




|  |  |
| --- | --- |
| dev | ADC 设备句柄 |
| channel | ADC 通道 |
| **返回** | —— |
| RT\_EOK | 成功 |
| -RT\_ENOSYS | 失败,设备操作方法为空 |
| 其他错误码 | 失败 |


使用示例如下所示:



#define ADC_DEV_NAME “adc1” /* ADC 设备名称 */
#define ADC_DEV_CHANNEL 5 /* ADC 通道 */
rt_adc_device_t adc_dev; /* ADC 设备句柄 */
rt_uint32_t value;
/* 查找设备 */
adc_dev = (rt_adc_device_t)rt_device_find(ADC_DEV_NAME);
/* 使能设备 */
rt_adc_enable(adc_dev, ADC_DEV_CHANNEL);
/* 读取采样值 */
value = rt_adc_read(adc_dev, ADC_DEV_CHANNEL);
/* 转换为对应电压值 */
vol = value * REFER_VOLTAGE / CONVERT_BITS;
rt_kprintf(“the voltage is :%d.%02d \n”, vol / 100, vol % 100);
/* 关闭通道 */
rt_adc_disable(adc_dev, ADC_DEV_CHANNEL);


#### [FinSH 命令]( )


在使用设备前,需要先查找设备是否存在,可以使用命令 `adc probe` 后面跟注册的 ADC 设备的名称。如下所示:



msh >adc probe adc1
probe adc1 success


使能设备的某个通道可以使用命令 `adc enable` 后面跟通道号。



msh >adc enable 5
adc1 channel 5 enables success


读取 ADC 设备某个通道的数据可以使用命令 `adc read` 后面跟通道号。



msh >adc read 5
adc1 channel 5 read value is 0x00000FFF
msh >


关闭设备的某个通道可以使用命令 `adc disable` 后面跟通道号。



msh >adc disable 5
adc1 channel 5 disable success
msh >


### [ADC 设备使用示例]( )


ADC 设备的具体使用方式可以参考如下示例代码,示例代码的主要步骤如下:


1. 首先根据 ADC 设备名称 “adc1” 查找设备获取设备句柄。
2. 使能设备后读取 adc1 设备对应的通道 5 的采样值,然后根据分辨率为 12 位,参考电压为 3.3V 计算实际的电压值。
3. 最后关闭 ADC 设备对应通道。


运行结果:打印实际读取到的转换的原始数据和经过计算后的实际电压值。



/*
* 程序清单: ADC 设备使用例程
* 例程导出了 adc_sample 命令到控制终端
* 命令调用格式:adc_sample
* 程序功能:通过 ADC 设备采样电压值并转换为数值。
* 示例代码参考电压为3.3V,转换位数为12位。
*/

#include <rtthread.h>
#include <rtdevice.h>

#define ADC_DEV_NAME “adc1” /* ADC 设备名称 */
#define ADC_DEV_CHANNEL 5 /* ADC 通道 */
#define REFER_VOLTAGE 330 /* 参考电压 3.3V,数据精度乘以100保留2位小数*/
#define CONVERT_BITS (1 << 12) /* 转换位数为12位 */

static int adc_vol_sample(int argc, char *argv[])
{
rt_adc_device_t adc_dev;
rt_uint32_t value, vol;
rt_err_t ret = RT_EOK;

/\* 查找设备 \*/
adc_dev = (rt\_adc\_device\_t)rt\_device\_find(ADC_DEV_NAME);
if (adc_dev == RT_NULL)
{
    rt\_kprintf("adc sample run failed! can't find %s device!\n", ADC_DEV_NAME);
    return RT_ERROR;
}

/\* 使能设备 \*/
ret = rt\_adc\_enable(adc_dev, ADC_DEV_CHANNEL);

/\* 读取采样值 \*/
value = rt\_adc\_read(adc_dev, ADC_DEV_CHANNEL);
rt\_kprintf("the value is :%d \n", value);

/\* 转换为对应电压值 \*/
vol = value \* REFER_VOLTAGE / CONVERT_BITS;
rt\_kprintf("the voltage is :%d.%02d \n", vol / 100, vol % 100);

/\* 关闭通道 \*/
ret = rt\_adc\_disable(adc_dev, ADC_DEV_CHANNEL);

return ret;

}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(adc_vol_sample, adc voltage convert sample);


### [常见问题]( )


**A:** 使用的源代码还不支持 ADC 设备驱动框架。建议更新源代码。


我有疑问: [RT-Thread 官方论坛]( )




---


## CAN 设备


已剪辑自: https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/can/can


### [CAN 简介]( )


CAN 是控制器局域网络 (Controller Area Network, CAN) 的简称,是由以研发和生产汽车电子产品著称的德国 BOSCH 公司开发的,并最终成为国际标准(ISO 11898),是国际上应用最广泛的现场总线之一。


CAN 控制器根据两根线上的电位差来判断总线电平。总线电平分为显性电平和隐性电平,二者必居其一。发送方通过使总线电平发生变化,将消息发送给接收方。 CAN 的连接示意图如下图所示:


![CAN 连接图](https://img-blog.csdnimg.cn/img_convert/61cb60a3ec9f7132e972d950718a4b10.png)


CAN 总线有如下特点:


* CAN 总线是可同时连接多个单元的总线。可连接的单元总数理论上是没有限制的。但实际上可连接的单元数受总线上的时间延迟及电气负载的限制。降低通信速度,可连接的单元数增加;提高通信速度,则可连接的单元数减少。
* 多主控制。在总线空闲时,所有的单元都可开始发送消息(多主控制)。多个单元同时开始发送时,发送高优先级 ID 消息的单元可获得发送权。
* 消息的发送。在 CAN 协议中,所有的消息都以固定的格式发送。总线空闲时,所有与总线相连的单元都可以开始发送新消息。两个以上的单元同时开始发送消息时,根据标识符 ID 决定优先级。ID 表示访问总线的消息的优先级。两个以上的单元同时开始发送消息时,对各消息 ID 的每个位进行逐个仲裁比较。仲裁获胜(被判定为优先级最高)的单元可继续发送消息,仲裁失利的单元则立刻停止发送而进行接收工作。
* 根据整个网络的规模,可设定适合的通信速度。在同一网络中,所有单元必须设定成统一的通信速度。即使有一个单元的通信速度与其它的不一样,此单元也会输出错误信号,妨碍整个网络的通信。不同网络间则可以有不同的通信速度。

 CAN 协议包括 5 种类型的帧:
* 数据帧
* 遥控帧
* 错误帧
* 过载帧
* 帧间隔


数据帧和遥控帧有标准格式和扩展格式两种格式。标准格式有 11 个位的 ID,扩展格式有 29 个位的 ID。


各种帧的用途如下表所示:


帧 帧用途




|  |  |
| --- | --- |
| 数据帧 | 用于发送单元向接收单元传送数据的帧 |
| 遥控帧 | 用于接收单元向具有相同 ID 的发送单元请求数据的帧 |
| 错误帧 | 用于当检测出错误时向其它单元通知错误的帧 |
| 过载帧 | 用于接收单元通知其尚未做好接收准备的帧 |
| 帧间隔 | 用于将数据帧及遥控帧与前面的帧分离开来的帧 |


### [访问 CAN 设备]( )


应用程序通过 RT-Thread 提供的 I/O 设备管理接口来访问 CAN 硬件控制器,相关接口如下所示:


函数 描述




|  |  |
| --- | --- |
| rt\_device\_find | 查找设备 |
| rt\_device\_open | 打开设备 |
| rt\_device\_read | 读取数据 |
| rt\_device\_write | 写入数据 |
| rt\_device\_control | 控制设备 |
| rt\_device\_set\_rx\_indicate | 设置接收回调函数 |
| rt\_device\_close | 关闭设备 |


#### [查找 CAN 设备]( )


应用程序根据 CAN 设备名称查找设备获取设备句柄,进而可以操作 CAN 设备,查找设备函数如下所示,



rt_device_t rt_device_find(const char* name);


参数 描述




|  |  |
| --- | --- |
| name | 设备名称 |
| **返回** | —— |
| 设备句柄 | 查找到对应设备将返回相应的设备句柄 |
| RT\_NULL | 没有找到相应的设备对象 |


一般情况下,注册到系统的 CAN 设备名称为 can1,can2 等,使用示例如下所示:



#define CAN_DEV_NAME “can1” /* CAN 设备名称 */

static rt_device_t can_dev; /* CAN 设备句柄 /
/
查找 CAN 设备 */
can_dev = rt_device_find(CAN_DEV_NAME);


#### [打开 CAN 设备]( )


通过设备句柄,应用程序可以打开和关闭设备,打开设备时,会检测设备是否已经初始化,没有初始化则会默认调用初始化接口初始化设备。通过如下函数打开设备:



rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags);


参数 描述




|  |  |
| --- | --- |
| dev | 设备句柄 |
| oflags | 打开设备模式标志 |
| **返回** | —— |
| RT\_EOK | 设备打开成功 |
| -RT\_EBUSY | 如果设备注册时指定的参数中包括 RT\_DEVICE\_FLAG\_STANDALONE 参数,此设备将不允许重复打开 |
| 其他错误码 | 设备打开失败 |


目前 RT-Thread CAN 设备驱动框架支持中断接收和中断发送模式。oflags 参数支持下列取值 (可以采用或的方式支持多种取值):



#define RT_DEVICE_FLAG_INT_RX 0x100 /* 中断接收模式 /
#define RT_DEVICE_FLAG_INT_TX 0x400 /
中断发送模式 */


以中断接收及发送模式打开 CAN 设备的示例如下所示:



#define CAN_DEV_NAME “can1” /* CAN 设备名称 */

static rt_device_t can_dev; /* CAN 设备句柄 */
/* 查找 CAN 设备 */
can_dev = rt_device_find(CAN_DEV_NAME);
/* 以中断接收及发送模式打开 CAN 设备 */
rt_device_open(can_dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);


#### [控制 CAN 设备]( )


通过命令控制字,应用程序可以对 CAN 设备进行配置,通过如下函数完成:



rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg);


参数 描述




|  |  |
| --- | --- |
| dev | 设备句柄 |
| cmd | 控制命令 |
| arg | 控制参数 |
| **返回** | —— |
| RT\_EOK | 函数执行成功 |
| 其他错误码 | 执行失败 |


arg(控制参数)根据命令不同而不同,cmd(控制命令)可取以下值:



#define RT_DEVICE_CTRL_RESUME 0x01 /* 恢复设备 */
#define RT_DEVICE_CTRL_SUSPEND 0x02 /* 挂起设备 */
#define RT_DEVICE_CTRL_CONFIG 0x03 /* 配置设备 */

#define RT_CAN_CMD_SET_FILTER 0x13 /* 设置硬件过滤表 */
#define RT_CAN_CMD_SET_BAUD 0x14 /* 设置波特率 */
#define RT_CAN_CMD_SET_MODE 0x15 /* 设置 CAN 工作模式 */
#define RT_CAN_CMD_SET_PRIV 0x16 /* 设置发送优先级 */
#define RT_CAN_CMD_GET_STATUS 0x17 /* 获取 CAN 设备状态 */
#define RT_CAN_CMD_SET_STATUS_IND 0x18 /* 设置状态回调函数 */
#define RT_CAN_CMD_SET_BUS_HOOK 0x19 /* 设置 CAN 总线钩子函数 */


##### [设置波特率]( )


设置波特率的示例代码如下所示:



#define CAN_DEV_NAME “can1” /* CAN 设备名称 */

static rt_device_t can_dev; /* CAN 设备句柄 */

/* 查找 CAN 设备 */
can_dev = rt_device_find(CAN_DEV_NAME);
/* 以中断接收及发送方式打开 CAN 设备 */
res = rt_device_open(can_dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);
/* 设置 CAN 通信的波特率为 500kbit/s*/
res = rt_device_control(can_dev, RT_CAN_CMD_SET_BAUD, (void *)CAN500kBaud);


##### [设置工作模式]( )


设置工作模式的示例代码如下所示:



#define CAN_DEV_NAME “can1” /* CAN 设备名称 */

static rt_device_t can_dev; /* CAN 设备句柄 */

/* 查找 CAN 设备 */
can_dev = rt_device_find(CAN_DEV_NAME);
/* 以中断接收及发送方式打开 CAN 设备 */
res = rt_device_open(can_dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);
/* 设置 CAN 的工作模式为正常工作模式 */
res = rt_device_control(can_dev, RT_CAN_CMD_SET_MODE, (void *)RT_CAN_MODE_NORMAL);


##### [获取 CAN 设备状态]( )


获取 CAN 设备状态的示例代码如下所示:



#define CAN_DEV_NAME “can1” /* CAN 设备名称 */

static rt_device_t can_dev; /* CAN 设备句柄 */
static struct rt_can_status status; /* 获取到的 CAN 总线状态 */

/* 查找 CAN 设备 */
can_dev = rt_device_find(CAN_DEV_NAME);
/* 以中断接收及发送方式打开 CAN 设备 */
res = rt_device_open(can_dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);
/* 获取 CAN 总线设备的状态 */
res = rt_device_control(can_dev, RT_CAN_CMD_GET_STATUS, &status);


##### [设置硬件过滤表]( )


过滤表控制块各成员描述如下所示:



struct rt_can_filter_item
{
rt_uint32_t id : 29; /* 报文 ID */
rt_uint32_t ide : 1; /* 扩展帧标识位 */
rt_uint32_t rtr : 1; /* 远程帧标识位 */
rt_uint32_t mode : 1; /* 过滤表模式 */
rt_uint32_t mask; /* ID 掩码,0 表示对应的位不关心,1 表示对应的位必须匹配 */
rt_int32_t hdr; /* -1 表示不指定过滤表号,对应的过滤表控制块也不会被初始化,正数为过滤表号,对应的过滤表控制块会被初始化 */
#ifdef RT_CAN_USING_HDR
/* 过滤表回调函数 */
rt_err_t (*ind)(rt_device_t dev, void *args , rt_int32_t hdr, rt_size_t size);
/* 回调函数参数 */
void *args;
#endif /*RT_CAN_USING_HDR*/
};


如果需要过滤的报文 ID 为 0x01 的标准数据帧,使用默认过滤表,则过滤表各个成员设置如下:



struct rt_can_filter_item filter;
/* 报文 ID */
filter.id = 0x01;
/* 标准格式 */
filter.ide = 0x00;
/* 数据帧 */
filter.rtr = 0x00;
/* 过滤表模式 */
filter.mode = 0x01;
/* 匹配 ID */
filter.mask = 0x01;
/* 使用默认过滤表 */
filter.hdr = -1;


为了方便表示过滤表的各个成员变量的值, RT-Thread 系统提供了匹配过滤表的宏



#define RT_CAN_FILTER_ITEM_INIT(id,ide,rtr,mode,mask,ind,args)
{(id), (ide), (rtr), (mode), (mask), -1, (ind), (args)}


过滤表宏中各个位分别和过滤表结构体成员变量一一对应,只是使用的过滤表是默认的过滤表。


则上述过滤信息使用过滤表的宏可以表示为



RT_CAN_FILTER_ITEM_INIT(0x01, 0, 0, 1, 0x01, RT_NULL, RT_NULL);


当需要使用过滤表时还需要指定过滤表配置控制块的成员变量,过滤表的配置控制块成员变量的组成如下所示:



struct rt_can_filter_config
{
rt_uint32_t count; /* 过滤表数量 /
rt_uint32_t actived; /
过滤表激活选项,1 表示初始化过滤表控制块,0 表示去初始化过滤表控制块 */
struct rt_can_filter_item items; / 过滤表指针,可指向一个过滤表数组 */
};


设置硬件过滤表示例代码如下所示:



#define CAN_DEV_NAME “can1” /* CAN 设备名称 */

static rt_device_t can_dev; /* CAN 设备句柄 */

can_dev = rt_device_find(CAN_DEV_NAME);

/* 以中断接收及发送模式打开 CAN 设备 */
rt_device_open(can_dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);

struct rt_can_filter_item items[1] =
{
RT_CAN_FILTER_ITEM_INIT(0x01, 0, 0, 1, 0x01, RT_NULL, RT_NULL),
/* 过滤 ID 为 0x01,match ID:0x100~0x1ff,hdr 为 - 1,设置默认过滤表 */
};
struct rt_can_filter_config cfg = {1, 1, items}; /* 一共有 1 个过滤表 */
/* 设置硬件过滤表 */
res = rt_device_control(can_dev, RT_CAN_CMD_SET_FILTER, &cfg);


#### [发送数据]( )


使用 CAN 设备发送数据,可以通过如下函数完成:



rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos, const void* buffer, rt_size_t size);


参数 描述




|  |  |
| --- | --- |
| dev | 设备句柄 |
| pos | 写入数据偏移量,此参数 CAN 设备未使用 |
| buffer | CAN 消息指针 |
| size | CAN 消息大小 |
| **返回** | —— |
| 不为 0 | 实际发送的 CAN 消息大小 |
| 0 | 发送失败 |


CAN 消息原型如下所示:



struct rt_can_msg
{
rt_uint32_t id : 29; /* CAN ID, 标志格式 11 位,扩展格式 29 位 */
rt_uint32_t ide : 1; /* 扩展帧标识位 */
rt_uint32_t rtr : 1; /* 远程帧标识位 */
rt_uint32_t rsv : 1; /* 保留位 */
rt_uint32_t len : 8; /* 数据段长度 */
rt_uint32_t priv : 8; /* 报文发送优先级 */
rt_uint32_t hdr : 8; /* 硬件过滤表号 */
rt_uint32_t reserved : 8;
rt_uint8_t data[8]; /* 数据段 */
};


使用 CAN 设备发送数据示例程序如下所示:



#define CAN_DEV_NAME “can1” /* CAN 设备名称 */

static rt_device_t can_dev; /* CAN 设备句柄 */
struct rt_can_msg msg = {0}; /* CAN 消息 */

can_dev = rt_device_find(CAN_DEV_NAME);

/* 以中断接收及发送模式打开 CAN 设备 */
rt_device_open(can_dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);

msg.id = 0x78; /* ID 为 0x78 */
msg.ide = RT_CAN_STDID; /* 标准格式 */
msg.rtr = RT_CAN_DTR; /* 数据帧 */
msg.len = 8; /* 数据长度为 8 */
/* 待发送的 8 字节数据 */
msg.data[0] = 0x00;
msg.data[1] = 0x11;
msg.data[2] = 0x22;
msg.data[3] = 0x33;
msg.data[4] = 0x44;
msg.data[5] = 0x55;
msg.data[6] = 0x66;
msg.data[7] = 0x77;
/* 发送一帧 CAN 数据 */
size = rt_device_write(can_dev, 0, &msg, sizeof(msg));


#### [设置接收回调函数]( )


可以通过如下函数来设置数据接收指示,当 CAN 收到数据时,通知上层应用线程有数据到达 :



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 | 设置成功 |


该函数的回调函数由调用者提供。CAN 设备在中断接收模式下,当 CAN 接收到一帧数据产生中断时,就会调用回调函数,并且会把此时缓冲区的数据大小放在 size 参数里,把 CAN 设备句柄放在 dev 参数里供调用者获取。


一般情况下接收回调函数可以发送一个信号量或者事件通知 CAN 数据处理线程有数据到达。使用示例如下所示:



#define CAN_DEV_NAME “can1” /* CAN 设备名称 */
static rt_device_t can_dev; /* CAN 设备句柄 */
struct rt_can_msg msg = {0}; /* CAN 消息 */

/* 接收数据回调函数 */
static rt_err_t can_rx_call(rt_device_t dev, rt_size_t size)
{
/* CAN 接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
rt_sem_release(&rx_sem);

return RT_EOK;

}

/* 设置接收回调函数 */
rt_device_set_rx_indicate(can_dev, can_rx_call);


#### [接收数据]( )


可调用如下函数读取 CAN 设备接收到的数据:



rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size);


参数 描述




|  |  |
| --- | --- |
| dev | 设备句柄 |
| pos | 读取数据偏移量,此参数 CAN 设备未使用 |
| buffer | CAN 消息指针,读取的数据将会被保存在缓冲区中 |
| size | CAN 消息大小 |
| **返回** | —— |
| 不为 0 | CAN 消息大小 |
| 0 | 失败 |


Note


注:接收数据时 CAN 消息的 hdr 参数必须要指定值,默认指定为 -1 就可以,表示从接收数据的 uselist 链表读取数据。也可以指定为硬件过滤表号的值,表示此次读取数据从哪一个硬件过滤表对应的消息链接读取数据,此时需要设置硬件过滤表的时候 hdr 有指定正确的过滤表号。如果设置硬件过滤表的时候 hdr 都为 -1,则读取数据的时候也要赋值为-1。


CAN 使用中断接收模式并配合接收回调函数的使用示例如下所示:



#define CAN_DEV_NAME “can1” /* CAN 设备名称 */

static rt_device_t can_dev; /* CAN 设备句柄 */
struct rt_can_msg rxmsg = {0}; /* CAN 接收消息缓冲区 */

/* hdr 值为 - 1,表示直接从 uselist 链表读取数据 */
rxmsg.hdr = -1;

/* 阻塞等待接收信号量 */
rt_sem_take(&rx_sem, RT_WAITING_FOREVER);
/* 从 CAN 读取一帧数据 */
rt_device_read(can_dev, 0, &rxmsg, sizeof(rxmsg));


#### [关闭 CAN 设备]( )


当应用程序完成 CAN 操作后,可以关闭 CAN 设备,通过如下函数完成:



rt_err_t rt_device_close(rt_device_t dev);


参数 描述




|  |  |
| --- | --- |
| dev | 设备句柄 |
| **返回** | —— |
| RT\_EOK | 关闭设备成功 |
| -RT\_ERROR | 设备已经完全关闭,不能重复关闭设备 |
| 其他错误码 | 关闭设备失败 |


关闭设备接口和打开设备接口需配对使用,打开一次设备对应要关闭一次设备,这样设备才会被完全关闭,否则设备仍处于未关闭状态。


### [CAN 设备使用示例]( )


示例代码的主要步骤如下所示:


1. 首先查找 CAN 设备获取设备句柄。
2. 初始化信号量,然后以中断接收及中断发送方式打开 CAN 设备。
3. 创建读取数据线程。
4. 发送一帧 CAN 数据。


* 读取数据线程首先会设置接收回调函数,然后设置硬件过滤表,之后会等待信号量。当 CAN 设备接收到一帧数据时会触发中断并调用接收回调函数,此函数会发送信号量唤醒线程,此时线程会马上读取接收到的数据。
* 此示例代码不局限于特定的 BSP,根据 BSP 注册的 CAN 设备,修改示例代码宏定义 CAN\_DEV\_NAME 对应的 CAN 设备名称即可运行。


运行序列图如下图所示:


![CAN 中断接收及发送序列图](https://img-blog.csdnimg.cn/img_convert/a19179b4c39363edc2466c79c0ba4a24.png)


程序运行起来后在命令行输入 `can_sample` 即可运行示例代码,后面数据为 CAN 设备接收到的数据:



\ | /

  • RT - Thread Operating System
    / | \ 4.0.1 build Jun 24 2019
    2006 - 2019 Copyright by rt-thread team
    msh >can_sample
    ID:486 0 11 22 33 0 23 4 86
    ID:111 0 11 22 33 0 23 1 11
    ID:555 0 11 22 33 0 23 5 55
    ID:211 0 11 22 33 0 23 2 11
    ID:344 0 11 22 33 0 23 3 44

可以使用 CAN 分析工具连接对应 CAN 设备收发数据,第一帧数据为 CAN 示例代码发送的 ID 为 0X78的数据, 效果如下图所示:


![CAN 分析工具数据收发过程](https://img-blog.csdnimg.cn/img_convert/71f09330b18d2e0109034aba09c2d71d.png)



/*
* 程序清单:这是一个 CAN 设备使用例程
* 例程导出了 can_sample 命令到控制终端
* 命令调用格式:can_sample can1
* 命令解释:命令第二个参数是要使用的 CAN 设备名称,为空则使用默认的 CAN 设备
* 程序功能:通过 CAN 设备发送一帧,并创建一个线程接收数据然后打印输出。
*/

#include <rtthread.h>
#include “rtdevice.h”

#define CAN_DEV_NAME “can1” /* CAN 设备名称 */

static struct rt_semaphore rx_sem; /* 用于接收消息的信号量 */
static rt_device_t can_dev; /* CAN 设备句柄 */

/* 接收数据回调函数 */
static rt_err_t can_rx_call(rt_device_t dev, rt_size_t size)
{
/* CAN 接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
rt_sem_release(&rx_sem);

return RT_EOK;

}

static void can_rx_thread(void *parameter)
{
int i;
rt_err_t res;
struct rt_can_msg rxmsg = {0};

/\* 设置接收回调函数 \*/
rt\_device\_set\_rx\_indicate(can_dev, can_rx_call);

#ifdef RT_CAN_USING_HDR
struct rt_can_filter_item items[5] =
{
RT_CAN_FILTER_ITEM_INIT(0x100, 0, 0, 0, 0x700, RT_NULL, RT_NULL), /* std,match ID:0x100~0x1ff,hdr 为 - 1,设置默认过滤表 */
RT_CAN_FILTER_ITEM_INIT(0x300, 0, 0, 0, 0x700, RT_NULL, RT_NULL), /* std,match ID:0x300~0x3ff,hdr 为 - 1 */

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

/* 从 CAN 读取一帧数据 */
rt_device_read(can_dev, 0, &rxmsg, sizeof(rxmsg));


#### [关闭 CAN 设备]( )


当应用程序完成 CAN 操作后,可以关闭 CAN 设备,通过如下函数完成:



rt_err_t rt_device_close(rt_device_t dev);


参数 描述




|  |  |
| --- | --- |
| dev | 设备句柄 |
| **返回** | —— |
| RT\_EOK | 关闭设备成功 |
| -RT\_ERROR | 设备已经完全关闭,不能重复关闭设备 |
| 其他错误码 | 关闭设备失败 |


关闭设备接口和打开设备接口需配对使用,打开一次设备对应要关闭一次设备,这样设备才会被完全关闭,否则设备仍处于未关闭状态。


### [CAN 设备使用示例]( )


示例代码的主要步骤如下所示:


1. 首先查找 CAN 设备获取设备句柄。
2. 初始化信号量,然后以中断接收及中断发送方式打开 CAN 设备。
3. 创建读取数据线程。
4. 发送一帧 CAN 数据。


* 读取数据线程首先会设置接收回调函数,然后设置硬件过滤表,之后会等待信号量。当 CAN 设备接收到一帧数据时会触发中断并调用接收回调函数,此函数会发送信号量唤醒线程,此时线程会马上读取接收到的数据。
* 此示例代码不局限于特定的 BSP,根据 BSP 注册的 CAN 设备,修改示例代码宏定义 CAN\_DEV\_NAME 对应的 CAN 设备名称即可运行。


运行序列图如下图所示:


![CAN 中断接收及发送序列图](https://img-blog.csdnimg.cn/img_convert/a19179b4c39363edc2466c79c0ba4a24.png)


程序运行起来后在命令行输入 `can_sample` 即可运行示例代码,后面数据为 CAN 设备接收到的数据:



\ | /

  • RT - Thread Operating System
    / | \ 4.0.1 build Jun 24 2019
    2006 - 2019 Copyright by rt-thread team
    msh >can_sample
    ID:486 0 11 22 33 0 23 4 86
    ID:111 0 11 22 33 0 23 1 11
    ID:555 0 11 22 33 0 23 5 55
    ID:211 0 11 22 33 0 23 2 11
    ID:344 0 11 22 33 0 23 3 44

可以使用 CAN 分析工具连接对应 CAN 设备收发数据,第一帧数据为 CAN 示例代码发送的 ID 为 0X78的数据, 效果如下图所示:


![CAN 分析工具数据收发过程](https://img-blog.csdnimg.cn/img_convert/71f09330b18d2e0109034aba09c2d71d.png)



/*
* 程序清单:这是一个 CAN 设备使用例程
* 例程导出了 can_sample 命令到控制终端
* 命令调用格式:can_sample can1
* 命令解释:命令第二个参数是要使用的 CAN 设备名称,为空则使用默认的 CAN 设备
* 程序功能:通过 CAN 设备发送一帧,并创建一个线程接收数据然后打印输出。
*/

#include <rtthread.h>
#include “rtdevice.h”

#define CAN_DEV_NAME “can1” /* CAN 设备名称 */

static struct rt_semaphore rx_sem; /* 用于接收消息的信号量 */
static rt_device_t can_dev; /* CAN 设备句柄 */

/* 接收数据回调函数 */
static rt_err_t can_rx_call(rt_device_t dev, rt_size_t size)
{
/* CAN 接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
rt_sem_release(&rx_sem);

return RT_EOK;

}

static void can_rx_thread(void *parameter)
{
int i;
rt_err_t res;
struct rt_can_msg rxmsg = {0};

/\* 设置接收回调函数 \*/
rt\_device\_set\_rx\_indicate(can_dev, can_rx_call);

#ifdef RT_CAN_USING_HDR
struct rt_can_filter_item items[5] =
{
RT_CAN_FILTER_ITEM_INIT(0x100, 0, 0, 0, 0x700, RT_NULL, RT_NULL), /* std,match ID:0x100~0x1ff,hdr 为 - 1,设置默认过滤表 */
RT_CAN_FILTER_ITEM_INIT(0x300, 0, 0, 0, 0x700, RT_NULL, RT_NULL), /* std,match ID:0x300~0x3ff,hdr 为 - 1 */

[外链图片转存中…(img-noJhjdBT-1715518247955)]
[外链图片转存中…(img-QYEmQuQ0-1715518247955)]
[外链图片转存中…(img-QiFMGR8h-1715518247955)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值