CAN设备应用指南

CAN 是控制器局域网络 (Controller Area Network, CAN) 的简称,是由以研发和生产汽车电子产品著称的德国 BOSCH 公司开发的,并最终成为国际标准(ISO 11898),是国际上应用最广泛的现场总线之一。
CAN 控制器根据两根线上的电位差来判断总线电平。总线电平分为显性电平和隐性电平,二者必居其一。发送方通过使总线电平发生变化,将消息发送给接收方。CAN 的连接示意图如下图所示:

640?wx_fmt=png

CAN 总线有如下特点:

⚪CAN 总线是可同时连接多个单元的总线。可连接的单元总数理论上是没有限制的。但实际上可连接的单元数受总线上的时间延迟及电气负载的限制。降低通信速度,可连接的单元数增加;提高通信速度,则可连接的单元数减少。

⚪多主控制。在总线空闲时,所有的单元都可开始发送消息(多主控制)。多个单元同时开始发送时,发送高优先级 ID 消息的单元可获得发送权。

⚪消息的发送。在 CAN 协议中,所有的消息都以固定的格式发送。总线空闲时,所有与总线相连的单元都可以开始发送新消息。两个以上的单元同时开始发送消息时,根据标识符 ID 决定优先级。ID 表示访问总线的消息的优先级。两个以上的单元同时开始发送消息时,对各消息 ID 的每个位进行逐个仲裁比较。仲裁获胜(被判定为优先级最高)的单元可继续发送消息,仲裁失利的单元则立刻停止发送而进行接收工作。

⚪根据整个网络的规模,可设定适合的通信速度。在同一网络中,所有单元必须设定成统一的通信速度。即使有一个单元的通信速度与其它的不一样,此单元也会输出错误信号,妨碍整个网络的通信。不同网络间则可以有不同的通信速度。

CAN 协议包括 5 种类型的帧:

数据帧

遥控帧

错误帧

过载帧

帧间隔


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

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

640?wx_fmt=png

访问 CAN 设备

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

640?wx_fmt=png

查找 CAN 设备

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

1rt_device_t rt_device_find(const char* name);

640?wx_fmt=png

一般情况下,注册到系统的 CAN 设备名称为 can1,can2 等,使用示例如下所示:
1#define CAN_DEV_NAME       "can1"      /* CAN 设备名称 */
2
3static rt_device_t can_dev;            /* CAN 设备句柄 */
4/* 查找 CAN 设备 */
5can_dev = rt_device_find(CAN_DEV_NAME);

打开 CAN 设备

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

1rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags);

640?wx_fmt=png

目前 RT-Thread CAN 设备驱动框架支持中断接收和中断发送模式。oflags 参数支持下列取值 (可以采用或的方式支持多种取值):
1#define RT_DEVICE_FLAG_INT_RX       0x100     /* 中断接收模式 */
2#define RT_DEVICE_FLAG_INT_TX       0x400     /* 中断发送模式 */

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

1#define CAN_DEV_NAME       "can1"      /* CAN 设备名称 */
2
3static rt_device_t can_dev;            /* CAN 设备句柄 */
4/* 查找 CAN 设备 */
5can_dev = rt_device_find(CAN_DEV_NAME);
6/* 以中断接收及发送模式打开 CAN 设备 */
7rt_device_open(can_dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);

控制 CAN 设备


通过命令控制字,应用程序可以对 CAN 设备进行配置,通过如下函数完成:
1rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg);

640?wx_fmt=png

arg(控制参数)根据命令不同而不同,cmd(控制命令)可取以下值:
 1#define RT_DEVICE_CTRL_RESUME           0x01    /* 恢复设备 */
 2#define RT_DEVICE_CTRL_SUSPEND          0x02    /* 挂起设备 */
 3#define RT_DEVICE_CTRL_CONFIG           0x03    /* 配置设备 */
 4
 5#define RT_CAN_CMD_SET_FILTER           0x13    /* 设置硬件过滤表 */
 6#define RT_CAN_CMD_SET_BAUD             0x14    /* 设置波特率 */
 7#define RT_CAN_CMD_SET_MODE             0x15    /* 设置 CAN 工作模式 */
 8#define RT_CAN_CMD_SET_PRIV             0x16    /* 设置发送优先级 */
 9#define RT_CAN_CMD_GET_STATUS           0x17    /* 获取 CAN 设备状态 */
10#define RT_CAN_CMD_SET_STATUS_IND       0x18    /* 设置状态回调函数 */
11#define RT_CAN_CMD_SET_BUS_HOOK         0x19    /* 设置 CAN 总线钩子函数 */

设置波特率

设置波特率的示例代码如下所示:
 1#define CAN_DEV_NAME       "can1"      /* CAN 设备名称 */
 2
 3static rt_device_t can_dev;            /* CAN 设备句柄 */
 4
 5/* 查找 CAN 设备 */
 6can_dev = rt_device_find(CAN_DEV_NAME);
 7/* 以中断接收及发送方式打开 CAN 设备 */
 8res = rt_device_open(can_dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);
 9/* 设置 CAN 通信的波特率为 500kbit/s*/
10res = rt_device_control(can_dev, RT_CAN_CMD_SET_BAUD, (void *)CAN500kBaud);

设置工作模式

设置工作模式的示例代码如下所示:
 1#define CAN_DEV_NAME       "can1"      /* CAN 设备名称 */
 2
 3static rt_device_t can_dev;            /* CAN 设备句柄 */
 4
 5/* 查找 CAN 设备 */
 6can_dev = rt_device_find(CAN_DEV_NAME);
 7/* 以中断接收及发送方式打开 CAN 设备 */
 8res = rt_device_open(can_dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);
 9/* 设置 CAN 的工作模式为正常工作模式 */
10res = rt_device_control(can_dev, RT_CAN_CMD_SET_MODE, (void *)RT_CAN_MODE_NORMAL);

获取 CAN 设备状态
获取 CAN 设备状态的示例代码如下所示:
 1#define CAN_DEV_NAME       "can1"      /* CAN 设备名称 */
 2
 3static rt_device_t can_dev;            /* CAN 设备句柄 */
 4static struct rt_can_status status;    /* 获取到的 CAN 总线状态 */
 5
 6/* 查找 CAN 设备 */
 7can_dev = rt_device_find(CAN_DEV_NAME);
 8/* 以中断接收及发送方式打开 CAN 设备 */
 9res = rt_device_open(can_dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);
10/* 获取 CAN 总线设备的状态 */
11res = rt_device_control(can_dev, RT_CAN_CMD_GET_STATUS, &status);

设置硬件过滤表

过滤表控制块各成员描述如下所示:
 1struct rt_can_filter_item
 2{
 3    rt_uint32_t id  : 29;   /* 报文 ID */
 4    rt_uint32_t ide : 1;    /* 扩展帧标识位 */
 5    rt_uint32_t rtr : 1;    /* 远程帧标识位 */
 6    rt_uint32_t mode : 1;   /* 过滤表模式 */
 7    rt_uint32_t mask;       /* ID 掩码,0 表示对应的位不关心,1 表示对应的位必须匹配 */
 8    rt_int32_t hdr;         /* -1 表示不指定过滤表号,对应的过滤表控制块也不会被初始化,正数为过滤表号,对应的过滤表控制块会被初始化 */
 9#ifdef RT_CAN_USING_HDR
10    /* 过滤表回调函数 */
11    rt_err_t (*ind)(rt_device_t dev, void *args , rt_int32_t hdr, rt_size_t size);
12    /* 回调函数参数 */
13    void *args;
14#endif /*RT_CAN_USING_HDR*/
15};
如果需要过滤的报文 ID 为 0x01 的标准数据帧,使用默认过滤表,则过滤表各个成员设置如下:
 1struct rt_can_filter_item filter;
 2/* 报文 ID */
 3filter.id = 0x01;
 4/* 标准格式 */
 5filter.ide = 0x00;
 6/* 数据帧 */
 7filter.rtr = 0x00;
 8/* 过滤表模式 */
 9filter.mode = 0x01;
10/* 匹配 ID */
11filter.mask = 0x01;
12/* 使用默认过滤表 */
13filter.hdr = -1;

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

1#define RT_CAN_FILTER_ITEM_INIT(id,ide,rtr,mode,mask,ind,args) \
2     {(id), (ide), (rtr), (mode), (mask), -1, (ind), (args)}
过滤表宏中各个位分别和过滤表结构体成员变量一一对应,只是使用的过滤表是默认的过滤表。

则上述过滤信息使用过滤表的宏可以表示为,
1RT_CAN_FILTER_ITEM_INIT(0x01, 0, 0, 1, 0x01, RT_NULL, RT_NULL);

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

1struct rt_can_filter_config
2{
3    rt_uint32_t count;                  /* 过滤表数量 */
4    rt_uint32_t actived;                /* 过滤表激活选项,1 表示初始化过滤表控制块,0 表示去初始化过滤表控制块 */
5    struct rt_can_filter_item *items;   /* 过滤表指针,可指向一个过滤表数组 */
6};
设置硬件过滤表示例代码如下所示:
 1#define CAN_DEV_NAME       "can1"      /* CAN 设备名称 */
 2
 3static rt_device_t can_dev;            /* CAN 设备句柄 */
 4
 5can_dev = rt_device_find(CAN_DEV_NAME);
 6
 7/* 以中断接收及发送模式打开 CAN 设备 */
 8rt_device_open(can_dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);
 9
10struct rt_can_filter_item items[1] =
11{
12    RT_CAN_FILTER_ITEM_INIT(0x01, 0, 0, 1, 0x01, RT_NULL, RT_NULL),
13    /* 过滤 ID 为 0x01,match ID:0x100~0x1ff,hdr 为 - 1,设置默认过滤表 */
14};
15struct rt_can_filter_config cfg = {1, 1, items}; /* 一共有 1 个过滤表 */
16/* 设置硬件过滤表 */
17res = rt_device_control(can_dev, RT_CAN_CMD_SET_FILTER, &cfg);

发送数据


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

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

640?wx_fmt=png

CAN 消息原型如下所示:
 1struct rt_can_msg
 2{
 3    rt_uint32_t id  : 29;   /* CAN ID, 标志格式 11 位,扩展格式 29 位 */
 4    rt_uint32_t ide : 1;    /* 扩展帧标识位 */
 5    rt_uint32_t rtr : 1;    /* 远程帧标识位 */
 6    rt_uint32_t rsv : 1;    /* 保留位 */
 7    rt_uint32_t len : 8;    /* 数据段长度 */
 8    rt_uint32_t priv : 8;   /* 报文发送优先级 */
 9    rt_uint32_t hdr : 8;    /* 硬件过滤表号 */
10    rt_uint32_t reserved : 8;
11    rt_uint8_t data[8];     /* 数据段 */
12};
使用 CAN 设备发送数据示例程序如下所示:
 1#define CAN_DEV_NAME       "can1"      /* CAN 设备名称 */
 2
 3static rt_device_t can_dev;            /* CAN 设备句柄 */
 4struct rt_can_msg msg = {0};           /* CAN 消息 */
 5
 6can_dev = rt_device_find(CAN_DEV_NAME);
 7
 8/* 以中断接收及发送模式打开 CAN 设备 */
 9rt_device_open(can_dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);
10
11msg.id = 0x78;              /* ID 为 0x78 */
12msg.ide = RT_CAN_STDID;     /* 标准格式 */
13msg.rtr = RT_CAN_DTR;       /* 数据帧 */
14msg.len = 8;                /* 数据长度为 8 */
15/* 待发送的 8 字节数据 */
16msg.data[0] = 0x00;
17msg.data[1] = 0x11;
18msg.data[2] = 0x22;
19msg.data[3] = 0x33;
20msg.data[4] = 0x44;
21msg.data[5] = 0x55;
22msg.data[6] = 0x66;
23msg.data[7] = 0x77;
24/* 发送一帧 CAN 数据 */
25size = rt_device_write(can_dev, 0, &msg, sizeof(msg));

设置接收回调函数


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

640?wx_fmt=png

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

一般情况下接收回调函数可以发送一个信号量或者事件通知 CAN 数据处理线程有数据到达。使用示例如下所示:
 1#define CAN_DEV_NAME       "can1"      /* CAN 设备名称 */
 2static rt_device_t can_dev;            /* CAN 设备句柄 */
 3struct rt_can_msg msg = {0};           /* CAN 消息 */
 4
 5/* 接收数据回调函数 */
 6static rt_err_t can_rx_call(rt_device_t dev, rt_size_t size)
 7{
 8    /* CAN 接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
 9    rt_sem_release(&rx_sem);
10
11    return RT_EOK;
12}
13
14/* 设置接收回调函数 */
15rt_device_set_rx_indicate(can_dev, can_rx_call);

接收数据


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

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

640?wx_fmt=png

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

CAN 使用中断接收模式并配合接收回调函数的使用示例如下所示:
 1#define CAN_DEV_NAME       "can1"      /* CAN 设备名称 */
 2
 3static rt_device_t can_dev;            /* CAN 设备句柄 */
 4struct rt_can_msg rxmsg = {0};         /* CAN 接收消息缓冲区 */
 5
 6/* hdr 值为 - 1,表示直接从 uselist 链表读取数据 */
 7rxmsg.hdr = -1;
 8
 9/* 阻塞等待接收信号量 */
10rt_sem_take(&rx_sem, RT_WAITING_FOREVER);
11/* 从 CAN 读取一帧数据 */
12rt_device_read(can_dev, 0, &rxmsg, sizeof(rxmsg));

关闭 CAN 设备


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

1rt_err_t rt_device_close(rt_device_t dev);

640?wx_fmt=png

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

CAN 设备使用示例

示例代码的主要步骤如下所示:
1、首先查找 CAN 设备获取设备句柄。
2、初始化信号量,然后以中断接收及中断发送方式打开 CAN 设备。
3、创建读取数据线程。
4、发送一帧 CAN 数据。

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

运行序列图如下图所示:

640?wx_fmt=jpeg

程序运行起来后在命令行输入 can_sample 即可运行示例代码,后面数据为 CAN 设备接收到的数据:
 1 \ | /
 2- RT -     Thread Operating System
 3 / | \     4.0.1 build Jun 24 2019
 4 2006 - 2019 Copyright by rt-thread team
 5msh >can_sample
 6ID:486   0 11 22 33  0 23  4 86
 7ID:111   0 11 22 33  0 23  1 11
 8ID:555   0 11 22 33  0 23  5 55
 9ID:211   0 11 22 33  0 23  2 11
10ID:344   0 11 22 33  0 23  3 44

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

640?wx_fmt=png

  1/*
  2 * 程序清单:这是一个 CAN 设备使用例程
  3 * 例程导出了 can_sample 命令到控制终端
  4 * 命令调用格式:can_sample can1
  5 * 命令解释:命令第二个参数是要使用的 CAN 设备名称,为空则使用默认的 CAN 设备
  6 * 程序功能:通过 CAN 设备发送一帧,并创建一个线程接收数据然后打印输出。
  7*/
  8
  9#include <rtthread.h>
 10#include "rtdevice.h"
 11
 12#define CAN_DEV_NAME       "can1"      /* CAN 设备名称 */
 13
 14static struct rt_semaphore rx_sem;     /* 用于接收消息的信号量 */
 15static rt_device_t can_dev;            /* CAN 设备句柄 */
 16
 17/* 接收数据回调函数 */
 18static rt_err_t can_rx_call(rt_device_t dev, rt_size_t size)
 19{
 20    /* CAN 接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
 21    rt_sem_release(&rx_sem);
 22
 23    return RT_EOK;
 24}
 25
 26static void can_rx_thread(void *parameter)
 27{
 28    int i;
 29    rt_err_t res;
 30    struct rt_can_msg rxmsg = {0};
 31
 32    /* 设置接收回调函数 */
 33    rt_device_set_rx_indicate(can_dev, can_rx_call);
 34
 35#ifdef RT_CAN_USING_HDR
 36    struct rt_can_filter_item items[5] =
 37    {
 38        RT_CAN_FILTER_ITEM_INIT(0x100, 0, 0, 1, 0x700, RT_NULL, RT_NULL), /* std,match ID:0x100~0x1ff,hdr 为 - 1,设置默认过滤表 */
 39        RT_CAN_FILTER_ITEM_INIT(0x300, 0, 0, 1, 0x700, RT_NULL, RT_NULL), /* std,match ID:0x300~0x3ff,hdr 为 - 1 */
 40        RT_CAN_FILTER_ITEM_INIT(0x211, 0, 0, 1, 0x7ff, RT_NULL, RT_NULL), /* std,match ID:0x211,hdr 为 - 1 */
 41        RT_CAN_FILTER_STD_INIT(0x486, RT_NULL, RT_NULL),                  /* std,match ID:0x486,hdr 为 - 1 */
 42        {0x555, 0, 0, 1, 0x7ff, 7,}                                       /* std,match ID:0x555,hdr 为 7,指定设置 7 号过滤表 */
 43    };
 44    struct rt_can_filter_config cfg = {5, 1, items}; /* 一共有 5 个过滤表 */
 45    /* 设置硬件过滤表 */
 46    res = rt_device_control(can_dev, RT_CAN_CMD_SET_FILTER, &cfg);
 47    RT_ASSERT(res == RT_EOK);
 48#endif
 49
 50    while (1)
 51    {
 52        /* hdr 值为 - 1,表示直接从 uselist 链表读取数据 */
 53        rxmsg.hdr = -1;
 54        /* 阻塞等待接收信号量 */
 55        rt_sem_take(&rx_sem, RT_WAITING_FOREVER);
 56        /* 从 CAN 读取一帧数据 */
 57        rt_device_read(can_dev, 0, &rxmsg, sizeof(rxmsg));
 58        /* 打印数据 ID 及内容 */
 59        rt_kprintf("ID:%x", rxmsg.id);
 60        for (i = 0; i < 8; i++)
 61        {
 62            rt_kprintf("%2x", rxmsg.data[i]);
 63        }
 64
 65        rt_kprintf("\n");
 66    }
 67}
 68
 69int can_sample(int argc, char *argv[])
 70{
 71    struct rt_can_msg msg = {0};
 72    rt_err_t res;
 73    rt_size_t  size;
 74    rt_thread_t thread;
 75    char can_name[RT_NAME_MAX];
 76
 77    if (argc == 2)
 78    {
 79        rt_strncpy(can_name, argv[1], RT_NAME_MAX);
 80    }
 81    else
 82    {
 83        rt_strncpy(can_name, CAN_DEV_NAME, RT_NAME_MAX);
 84    }
 85    /* 查找 CAN 设备 */
 86    can_dev = rt_device_find(can_name);
 87    if (!can_dev)
 88    {
 89        rt_kprintf("find %s failed!\n", can_name);
 90        return RT_ERROR;
 91    }
 92
 93    /* 初始化 CAN 接收信号量 */
 94    rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO);
 95
 96    /* 以中断接收及发送方式打开 CAN 设备 */
 97    res = rt_device_open(can_dev, RT_DEVICE_FLAG_INT_TX | RT_DEVICE_FLAG_INT_RX);
 98    RT_ASSERT(res == RT_EOK);
 99    /* 创建数据接收线程 */
100    thread = rt_thread_create("can_rx", can_rx_thread, RT_NULL, 1024, 25, 10);
101    if (thread != RT_NULL)
102    {
103        rt_thread_startup(thread);
104    }
105    else
106    {
107        rt_kprintf("create can_rx thread failed!\n");
108    }
109
110    msg.id = 0x78;              /* ID 为 0x78 */
111    msg.ide = RT_CAN_STDID;     /* 标准格式 */
112    msg.rtr = RT_CAN_DTR;       /* 数据帧 */
113    msg.len = 8;                /* 数据长度为 8 */
114    /* 待发送的 8 字节数据 */
115    msg.data[0] = 0x00;
116    msg.data[1] = 0x11;
117    msg.data[2] = 0x22;
118    msg.data[3] = 0x33;
119    msg.data[4] = 0x44;
120    msg.data[5] = 0x55;
121    msg.data[6] = 0x66;
122    msg.data[7] = 0x77;
123    /* 发送一帧 CAN 数据 */
124    size = rt_device_write(can_dev, 0, &msg, sizeof(msg));
125    if (size == 0)
126    {
127        rt_kprintf("can dev write data failed!\n");
128    }
129
130    return res;
131}
132/* 导出到 msh 命令列表中 */
133MSH_CMD_EXPORT(can_sample, can device sample);

END

RT-Thread线上活动

1、【RT-Thread能力认证考试12月——RCEA】经过第一次考试的验证,

能力认证官网链接:https://www.rt-thread.org/page/rac.html(在外部浏览器打开)

640?wx_fmt=jpeg

立即报名

#题外话# 喜欢RT-Thread不要忘了在GitHub上留下你的640?wx_fmt=pngSTAR640?wx_fmt=png哦,你的star对我们来说非常重要!链接地址:https://github.com/RT-Thread/rt-thread

RT-Thread线下活动

1、STM32全国研讨会,RT-Thread近期参展城市预告:上海、广州、顺德

你可以添加微信18917005679为好友,注明:公司+姓名,拉进 RT-Thread 官方微信交流群

640?wx_fmt=jpeg

RT-Thread

长按二维码,关注我们

640?wx_fmt=png
看这里,求赞!求转发!
640?wx_fmt=gif

640?wx_fmt=gif点击阅读原文进入GitHub

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值