rt thread中的can通信 学习记录

张开发
2026/4/17 8:35:05 15 分钟阅读

分享文章

rt thread中的can通信 学习记录
can通信特点两根线CANHCANL多设备挂在同一根线上靠id识别数据本历程采用正点原子阿波罗stm32f429can回环模式回环模式不需要经过具体的引脚can的基础知识点1优先级两个以上的单元同时开始发送消息时根据标识符 ID 决定优先级。ID 表示访问总线的消息的优先级。理解CAN 的数据帧里有一个 ID。在 CAN 里ID 越小优先级越高。例如ID 为0x01的消息优先级高于 ID 为0x10的消息。ID 不仅用来表示优先级在实际应用中通常用来表示“这条消息包含什么内容”比如是发动机转速还是车门状态。2仲裁比较两个以上的单元同时开始发送消息时对各消息 ID 的每个位进行逐个仲裁比较。仲裁获胜...继续发送仲裁失利...立刻停止并接收。理解这叫做线与机制。在物理电平上逻辑0是显性电平逻辑1是隐性电平。显性电平可以覆盖隐性电平。一边发送一边回读。发送隐性电平(1)却读回显性电平(0)的节点就知道自己输了失去仲裁立刻转为接收模式。35种帧数据帧用于发送节点向接收节点传输实际的数据。在 RT-Thread 中你配置rt_can_msg结构体填入id、设置rtr RT_CAN_DTR表示数据帧并填入你的业务数据比如温度传感器的值然后发送。遥控帧遥控帧没有数据段。它的结构和数据帧很像但它的 RTR远程传输请求位是隐性电平1。当某个节点收到与自己 ID 匹配的遥控帧时它应该立刻回复一个包含实际数据的数据帧。错误帧用于在接收或发送消息时检测出错误并通知总线上的所有其他节点。CAN 总线有极其严格的自我诊断机制CRC 校验、位填充检查等。任何一个节点一旦发现总线上的信号不符合规则就会立刻发送一串连续的“破坏性”电平6个显性位。这会主动破坏当前正在传输的帧迫使所有节点丢弃这个坏消息。STM32 内部的 CAN 控制器硬件会自动检测错误并发送错误帧。你需要做的只是在 RT-Thread 中检查错误中断或错误计数器看看总线是不是物理上出问题了比如线缆短路、干扰太大。过载帧格式和错误帧非常相似但它是在帧间隔期间发送的用来强行延长两帧之间的等待时间。帧间隔它是由连续的 3 个隐性位逻辑 1组成的。无论是谁刚发完一帧都必须等待这 3 个位的时间过去总线才算真正进入“空闲”状态大家才能开始新一轮的抢夺仲裁。如果此时硬件还在处理上一帧的收尾工作就会用到前面提到的“过载帧”来延长这个间隔。stm32中纯硬件行为STM32 会严格遵守这个规则确保总线节奏有条不紊。你不需要、也无法用代码去干预帧间隔。4接收中断与发送中断接收中断目的是收总线信号进入stm32经过硬件滤波后存入FIFO中这时候fifo非空产生rx中断信号进入isr中断服务函数在中断里读取fifo的数据可以通过释放信号量来唤醒接收线程发送中断目的是发软件把第一帧信息放入空闲的硬件邮箱触发硬件发送发送成功后邮箱变成空闲产生tx中断在中断函数里从软件的发送队列中提取下一帧信息放入空邮箱再次请求发送。5rt_can_filter_item过滤器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; /* 过滤表模式0 表示标识符屏蔽位模式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*/ };1.id你想接收的 ID这是你期望收到的 CAN 帧 ID。标准帧范围0x000~0x7FF扩展帧范围0x00000000~0x1FFFFFFF2.ide标准帧还是扩展帧0(RT_CAN_STDID)只接收标准帧1(RT_CAN_EXTID)只接收扩展帧3.rtr数据帧还是远程帧0(RT_CAN_DTR)只接收数据帧最常用电机发回来的数据都是这个1(RT_CAN_RTR)只接收远程帧很少用4.mode两种过滤模式模式 0掩码模式 (RT_CAN_FILTER_MODE_MASK)意思设定 “哪些位必须严格等于id哪些位可以随便变”。就像相亲id是 “要求”mask是 “哪些要求必须满足”。模式 1列表模式 (RT_CAN_FILTER_MODE_LIST)意思只接收完全等于id的那一帧。就像点名只点id这一个号。5.mask掩码配合模式 0 使用这是一个 32 位的数。某一位为1收到的数据的这一位 必须 和id的这一位 一样。某一位为0收到的数据的这一位 无所谓是 0 是 1 都可以。6.hdr过滤器硬件编号STM32F429 的 CAN1 有 14 个硬件过滤器0~13。-1让 RT-Thread 自动帮你分配一个空闲的过滤器。0~13你手动指定用第几个过滤器。建议初学者直接填-1。7.#define RT_CAN_FILTER_ITEM_INIT(id,ide,rtr,mode,mask,ind,args) \ {(id), (ide), (rtr), (mode), (mask), -1, (ind), (args)}这是一个 C 语言宏它会自动生成一个结构体的初始化列表。宏参数对应结构体成员含义初学者建议填什么idid你想接收的 CAN ID比如0x123ideide标准帧 / 扩展帧RT_CAN_STDIDrtrrtr数据帧 / 远程帧RT_CAN_DTRmodemode列表模式 / 掩码模式RT_CAN_FILTER_MODE_LISTmaskmask掩码0x000(列表模式下无效)indind回调函数RT_NULL(不用管)argsargs回调参数RT_NULL(不用管)实战操作快速填好结构体// 定义并初始化 struct rt_can_filter_item filter RT_CAN_FILTER_ITEM_INIT( 0x123, // id RT_CAN_STDID, // ide (标准帧) RT_CAN_DTR, // rtr (数据帧) RT_CAN_FILTER_MODE_LIST, // mode (列表模式) 0x000, // mask (列表模式下无效填0) RT_NULL, // ind (无回调) RT_NULL // args (无参数) ); // 直接调用 API 设置即可 rt_device_control(can_dev, RT_CAN_CMD_SET_FILTER, filter);过滤器例子1总线上只有 ID0x123 的数据能进来其他全屏蔽。struct rt_can_filter_item filter; // 1. 清空结构体 rt_memset(filter, 0, sizeof(filter)); // 2. 配置参数 filter.id 0x123; // 只接收 ID 为 0x123 的数据 filter.ide RT_CAN_STDID; // 标准帧 filter.rtr RT_CAN_DTR; // 数据帧 filter.mode RT_CAN_FILTER_MODE_LIST; // 列表模式精确匹配 filter.hdr -1; // 自动分配过滤器编号 // 3. 调用 API 设置过滤器 rt_device_control(can_dev, RT_CAN_CMD_SET_FILTER, filter);过滤器例子2需求我要控制 8 个电机它们的 ID 是 0x100, 0x101 ... 0x107。分析ID 的二进制是0001 0000 0xxx最后 3 位是电机编号。struct rt_can_filter_item filter; rt_memset(filter, 0, sizeof(filter)); filter.id 0x100; // 基准 ID filter.ide RT_CAN_STDID; filter.rtr RT_CAN_DTR; filter.mode RT_CAN_FILTER_MODE_MASK; // 掩码模式 // 【关键】掩码设置 // 二进制1111 1111 1000 (0x7F8) // 意思前 8 位必须是 0x100后 3 位随便 (0~7) filter.mask 0x7F8; filter.hdr -1; rt_device_control(can_dev, RT_CAN_CMD_SET_FILTER, filter);6rt_can_msg信息结构struct rt_can_msg { rt_uint32_t id : 29; /* 【必学】CAN ID */ 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]; /* 【必学】真正的数据内容 */ };1.idCAN 帧 ID如果是标准帧 (ide0)0x000~0x7FF(11 位)如果是扩展帧 (ide1)0x00000000~0x1FFFFFFF(29 位)2.ide标准帧还是扩展帧含义ID 是 11 位的还是 29 位的。可选值RT-Thread 已经定义好了宏直接用RT_CAN_STDID(0)标准帧 (11 位 ID) —— 最常用电机、工业控制一般用这个。RT_CAN_EXTID(1)扩展帧 (29 位 ID)。3.rtr数据帧还是远程帧含义这帧是用来发数据的还是用来 “请求” 别人发数据的。RT_CAN_DTR(0)数据帧 —— 99.9% 用这个你发数据、电机回传数据都是这个。RT_CAN_RTR(1)远程帧 (Remote Transmission Request) —— 几乎不用。4.len数据长度含义data[8]数组里有几个字节是有效的。范围0~8。注意CAN 一帧最多只能发 8 个字节实战填写发送时你发几个字节就填几比如发 8 个就填8。接收时读这个变量知道收到了几个字节。5.data[8]真正的数据负载含义你要传的具体内容。大小8 个字节。实战填写发送时把你要发的数填进去。接收时从这里面读数据。6.priv发送优先级含义当多个发送邮箱都有数据时优先级高的先发。范围0~255。实战填写直接填 0 或者不管它初始化时清 0 即可。一般简单应用用不到硬件优先级调度。7.hdr硬件过滤器表号含义只读这帧数据是通过第几个过滤器进来的。实战用法比如你设了过滤器 0 收电机 1过滤器 1 收电机 2。收到数据后看一眼hdr如果是 0就是电机 1 发的如果是 1就是电机 2 发的。初学者可以忽略直接看id更直观。8.rsv和reserved含义保留位给未来扩展用的。实战不要去写它也不用读它。7MSH_CMD_EXPORT()注册命令MSH_CMD_EXPORT(can_test, start can loopback test);把can_test这个函数注册成命令。如果用户在终端敲help请显示这行描述start can loopback test。用户现在可以直接命令行运行这个函数8rt_device_set_rx_indicatert_err_t rt_device_set_rx_indicate( rt_device_t dev, rt_err_t (*rx_ind)(rt_device_t dev, rt_size_t size) );rt_device_t dev给哪个设备设置回调函数参数二部分含义(*rx_ind)这是一个指针名字叫rx_ind(rt_device_t dev, rt_size_t size)它指向的函数必须接收这两个参数rt_err_t它指向的函数返回值必须是rt_err_t自己写的回调函数必须是这个样子// 返回值必须是 rt_err_t // 参数必须是 (rt_device_t, rt_size_t) rt_err_t 你的函数名(rt_device_t dev, rt_size_t size) { // 做你想做的事 return RT_EOK; // 必须返回一个值 }8rt_device_write();rt_device_write(can_dev, 0, txmsg, sizeof(txmsg));概念代码中的位置含义数值1. CAN 有效数据长度txmsg.lenCAN 总线真正发出去几个字节你自己设的0~82. 驱动缓冲区大小sizeof(txmsg)你传给 RT-Thread 驱动的这包数据有多大整个结构体的大小通常是 16 或 20 字节sizeof(txmsg)告诉 RT-Thread 设备驱动“我给你传了一个结构体它的大小是这么多字节你要完整地把它读进去解析。”9rt_device_read(can_dev, 0, rxmsg, sizeof(rxmsg))can_dev(设备句柄)作用告诉系统你要从哪个具体的设备里读数据。在这里它代表我们之前打开的can1外设。0(读取位置/偏移量 pos)作用对于像 SD 卡或 EEPROM 这样的存储设备这个参数决定了从第几个扇区或字节开始读。为什么是 0CAN 总线是一种数据流/数据包设备新来的数据总是按顺序排在硬件的 FIFO先进先出队列里。我们只能“从头往外拿”没有“偏移”的概念所以这里固定填0或-1都可以。rxmsg(数据存放地址)作用这是你准备好的“空箱子”的地址。你告诉底层驱动“请把读到的 CAN 数据原封不动地塞到这块内存里”。sizeof(rxmsg)(期望读取的大小)作用告诉底层你想读多大的数据。rxmsg是一个struct rt_can_msg类型的结构体包含了帧 ID、数据长度、8个字节的具体数据等信息。这里就是要求底层完整地读取一帧报文的结构体大小。函数返回实际上读取多少个字节具体例子基于 RT-Thread 设备驱动框架的CAN 回环自己发自己收通信测试。它的核心架构采用了经典的“中断通知 线程处理”模式底层硬件CAN 硬件接收到数据后触发底层的接收中断。中间层回调中断触发我们的回调函数回调函数什么复杂业务都不做只负责“释放一个信号量”相当于拉响警报然后立刻退出保证中断极速响应。上层线程专门有一个接收线程在后台“死等”这个信号量。平时它处于挂起休眠状态不占用 CPU一旦信号量被释放它立刻苏醒把 CAN 硬件里的数据读出来并打印读完继续休眠。如何初始化can通信1系统配置好环境后系统里挂载了很多外设can1usart等通过rt_device_find(can1)2拿到句柄后打开设备rt_device_open()3配置参数波特率工作模式硬件过滤。rt_device_control()4注册回调函数触发接收中断对信息进行接收。rt_device_set_rx_indicate()5填好信息结构体后发送信息rt_device_write()#include rtthread.h // RT-Thread 内核头文件包含基础数据类型、线程、信号量等API #include rtdevice.h // RT-Thread 设备驱动框架头文件CAN设备API属于这里 #include board.h // 板级支持包头文件包含底层硬件相关定义 #define CAN_DEV_NAME can1 // 定义要操作的 CAN 设备名称 static rt_device_t can_dev RT_NULL; // 定义一个设备句柄指针用于后续操作该 CAN 设备 static struct rt_semaphore rx_sem; // 定义一个信号量结构体用于在中断和接收线程之间同步数据到达的事件 /* 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; } /* CAN 数据接收处理线程的入口函数 * 该线程在后台独立运行负责将底层接收到的数据读取出来并打印。 */ static void can_rx_thread_entry(void *parameter) { struct rt_can_msg rxmsg {0}; // 定义一个 CAN 消息结构体用于存放读取到的数据并初始化为 0 while (1) // 线程主循环 { /* 挂起当前线程无限期阻塞等待信号量RT_WAITING_FOREVER。 只有当中断回调函数释放了 rx_sem 信号量时该线程才会被唤醒并往下执行。 */ if (rt_sem_take(rx_sem, RT_WAITING_FOREVER) RT_EOK) { /* 使用 while 循环不断读取硬件 FIFO 中的数据防止中断触发太快导致数据堆积遗漏。 rt_device_read 返回实际读取的字节数当返回值等于一帧报文大小(sizeof(rxmsg))时说明成功读到完整一帧。 */ while (rt_device_read(can_dev, 0, rxmsg, sizeof(rxmsg)) sizeof(rxmsg)) { /* 打印接收到的报文 ID */ rt_kprintf(\n[CAN RX] ID: 0x%x, Data: , rxmsg.id); /* 循环打印报文中的具体数据段%02X 表示以至少2位宽度的十六进制大写打印不足补0方便对齐查看 */ for (int i 0; i rxmsg.len; i) rt_kprintf(%02X , rxmsg.data[i]); rt_kprintf(\n); } } } } /* CAN 综合测试函数负责设备初始化、线程创建以及发送测试数据 */ int can_test(void) { struct rt_can_msg txmsg {0}; // 定义用于发送的 CAN 消息结构体 rt_err_t res; // 用于保存函数返回的状态码 rt_thread_t thread; // 定义线程句柄 static uint8_t is_initialized 0; // 定义静态标志位用于防止用户在终端多次输入命令导致的重复初始化 /* 1. 根据设备名称在系统中查找对应的 CAN 设备 */ can_dev rt_device_find(CAN_DEV_NAME); if (!can_dev) { rt_kprintf([ERR] Find %s failed!\n, CAN_DEV_NAME); return -RT_ERROR; // 找不到设备则退出测试 } rt_kprintf([OK] Found %s device.\n, CAN_DEV_NAME); /* 2. 防呆保护如果之前已经打开并初始化过先尝试关闭它 (主要为解决重复打开导致的报错问题) */ if (is_initialized 1) { rt_kprintf([WARN] Device already init, trying to close and re-open...\n); rt_device_close(can_dev); // 注意这里为了简单不做完全的反初始化如删除线程、删除信号量等仅通过 close 解决 open 报错 } /* 如果是首次运行该命令或者说尚未完成初始化过程则执行核心初始化动作 */ if (is_initialized 0) { /* 3. 初始化接收信号量名字设为 rx_sem初始值为0采用先进先出(FIFO)的唤醒模式 */ rt_sem_init(rx_sem, rx_sem, 0, RT_IPC_FLAG_FIFO); /* 4. 打开 CAN 设备 (关键步骤) 以 中断接收(RT_DEVICE_FLAG_INT_RX) 和 中断发送(RT_DEVICE_FLAG_INT_TX) 的模式打开 */ rt_kprintf([INFO] Opening CAN device...\n); res rt_device_open(can_dev, RT_DEVICE_FLAG_INT_RX | RT_DEVICE_FLAG_INT_TX); if (res ! RT_EOK) { rt_kprintf([ERR] Open failed! Error code: %d\n, res); return -RT_ERROR; // 打开失败退出 } rt_kprintf([OK] CAN device opened.\n); /* 5. 配置 CAN 底层工作参数 */ rt_kprintf([INFO] Setting baudrate to 500kbps...\n); /* 通过 rt_device_control 接口下发命令将波特率设为 500k */ rt_device_control(can_dev, RT_CAN_CMD_SET_BAUD, (void *)CAN500kBaud); rt_kprintf([INFO] Setting Loopback mode...\n); /* 设为 回环模式(Loopback)即自己发自己收不经过外部物理总线便于单板验证代码逻辑 */ rt_device_control(can_dev, RT_CAN_CMD_SET_MODE, (void *)RT_CAN_MODE_LOOPBACK); /* 将我们上面编写好的 can_rx_call 注册为底层 CAN 外设的接收中断通知回调函数 */ rt_device_set_rx_indicate(can_dev, can_rx_call); /* 6. 创建并启动接收线程 线程名can_rx入口函数 can_rx_thread_entry无参数传入栈大小1024字节优先级15时间片10个tick */ thread rt_thread_create(can_rx, can_rx_thread_entry, RT_NULL, 1024, 15, 10); if (thread) rt_thread_startup(thread); // 创建成功后立刻启动该线程让其进入就绪态等待信号量 /* 标记初始化流程已完成后续再次执行 can_test 命令时将跳过上述 3~6 步 */ is_initialized 1; rt_kprintf([OK] CAN Init Complete!\n); } /* 7. 构造并发送测试数据 */ txmsg.id 0x123; // 设置本次发送帧的 ID 为 0x123 txmsg.ide RT_CAN_STDID; // 设置为标准帧 (Standard ID, 11位) txmsg.rtr RT_CAN_DTR; // 设置为数据帧 (Data Frame) txmsg.len 8; // 规定本次数据长度为 8 个字节 /* 利用 for 循环填充具体的 8 字节测试数据内容0x0A, 0x0B, 0x0C... */ for (int i 0; i 8; i) txmsg.data[i] i 0x0A; rt_kprintf(\n[CAN TX] Sending...\n); /* 调用标准的设备写入接口把准备好的 txmsg 结构体扔给底层 CAN 硬件发送出去 */ rt_device_write(can_dev, 0, txmsg, sizeof(txmsg)); return RT_EOK; } /* 将 can_test 函数导出为 MSH 控制台命令可在串口终端里敲 can_test 触发执行该函数 */ MSH_CMD_EXPORT(can_test, start can loopback test); /* 应用层的入口主函数由于我们的业务逻辑挂载在了终端命令上所以此处直接返回即可 */ int main(void) { return RT_EOK; }1.can_rx_call(接收中断回调函数)触发时机当 CAN 硬件 FIFO 接收到一帧完整的数据时底层 HAL 库的中断会自动调用这个函数。作用执行rt_sem_release(rx_sem);释放一次信号量。它的作用就像是传达室大爷收到快递CAN 数据后按一下呼叫铃释放信号量通知楼上的接件员接收线程来拿绝不在传达室里拆快递。2.can_rx_thread_entry(接收处理线程)触发时机系统启动后就会创建并在后台独立运行。作用真正的“接件员”。rt_sem_take这是一个阻塞函数如果信号量没被释放这行代码就会卡住挂起线程出让 CPU 权限给其他任务。rt_device_read一旦拿到信号量苏醒就进入while循环把 CAN 接收 FIFO 里积压的数据全部读出来防止中断太快导致数据堆积并打印出 ID 和具体的字节数据。3.can_test(初始化与发送测试函数)触发时机在串口控制台输入can_test命令并回车时触发。作用这是整个流程的“总导演”做了三件大事安全初始化通过is_initialized标志位判断如果是第一次运行就去查找外设、初始化信号量、打开 CAN 设备、设置 500K 波特率和回环模式Loopback最后把接收线程创建并跑起来。防爆破机制如果你在控制台连续多次输入该命令它会检测到已经初始化过不再重复建线程而是利用rt_device_close做简单的规避然后直接发信息rt_device_write。发送数据打包一帧 ID 为0x123的标准数据帧内容为0A 0B 0C 0D 0E 0F 10 11调用rt_device_write把数据塞给 CAN 硬件发出去。

更多文章