STM32项目开发:基于CAN总线的多节点通信与数据采集系统

文章目录

    • [1. 项目概述与设计背景](#1. 项目概述与设计背景)
      • [1.1 系统功能定义](#1.1 系统功能定义)
      • [1.2 硬件准备清单](#1.2 硬件准备清单)
    • [2. 硬件连接与系统架构](#2. 硬件连接与系统架构)
      • [2.1 CAN总线终端电阻原理](#2.1 CAN总线终端电阻原理)
      • [2.2 系统硬件拓扑结构](#2.2 系统硬件拓扑结构)
      • [2.3 引脚接线详细说明](#2.3 引脚接线详细说明)
    • [3. CubeMX配置详解](#3. CubeMX配置详解)
      • [3.1 工程创建与基础配置](#3.1 工程创建与基础配置)
      • [3.2 CAN参数计算与配置](#3.2 CAN参数计算与配置)
      • [3.3 串口配置(仅Master需要)](#3.3 串口配置(仅Master需要))
    • [4. 软件流程设计](#4. 软件流程设计)
      • [4.1 整体数据流向分析](#4.1 整体数据流向分析)
      • [4.2 CAN过滤器工作原理](#4.2 CAN过滤器工作原理)
    • [5. 代码实现:公共基础代码](#5. 代码实现:公共基础代码)
      • [5.1 main.c文件头部配置](#5.1 main.c文件头部配置)
      • [5.2 CAN过滤器配置函数](#5.2 CAN过滤器配置函数)
      • [5.3 串口重定向函数(仅Master需要)](#5.3 串口重定向函数(仅Master需要))
    • [6. 代码实现:发送节点](#6. 代码实现:发送节点)
      • [6.1 Node A主程序代码](#6.1 Node A主程序代码)
      • [6.2 Node B主程序代码](#6.2 Node B主程序代码)
    • [7. 代码实现:主控接收端](#7. 代码实现:主控接收端)
      • [7.1 Master主程序初始化](#7.1 Master主程序初始化)
      • [7.2 CAN接收中断回调函数](#7.2 CAN接收中断回调函数)
      • [7.3 完整的Master main函数结构](#7.3 完整的Master main函数结构)
    • [8. 编译与烧录流程](#8. 编译与烧录流程)
      • [8.1 工程编译](#8.1 工程编译)
      • [8.2 程序烧录](#8.2 程序烧录)
    • [9. 调试与验证步骤](#9. 调试与验证步骤)
      • [9.1 验证前的准备工作](#9.1 验证前的准备工作)
      • [9.2 验证步骤与预期结果](#9.2 验证步骤与预期结果)
      • [9.3 验证失败的处理方法](#9.3 验证失败的处理方法)
    • [10. 常见问题排查与解决方案](#10. 常见问题排查与解决方案)
      • [10.1 完全收不到数据](#10.1 完全收不到数据)
      • [10.2 数据接收时断时续](#10.2 数据接收时断时续)
      • [10.3 程序卡死在初始化阶段](#10.3 程序卡死在初始化阶段)
    • [11. 系统功能扩展思路](#11. 系统功能扩展思路)
      • [11.1 添加数据校验机制](#11.1 添加数据校验机制)
      • [11.2 实现命令下发功能](#11.2 实现命令下发功能)
      • [11.3 增加更多采集节点](#11.3 增加更多采集节点)
    • [12. 技术总结](#12. 技术总结)

1. 项目概述与设计背景

在工业自动化、汽车电子以及复杂的嵌入式系统中,设备之间的通信至关重要。传统的UART串口通信在多节点组网时存在抗干扰能力差、布线复杂、主从机制死板等缺陷。CAN(Controller Area Network)总线凭借其差分信号传输带来的高抗干扰性、自动仲裁机制以及双线挂载多设备的特性,成为了工业现场的首选通信方案。

本项目将手把手带领大家实现一个基于CAN总线的多节点数据采集系统。我们将构建一个由"1个主接收端(Master)"和"2个数据发送节点(Node A & Node B)"组成的微型网络。这个网络虽然简单,但涵盖了CAN总线应用的核心知识点,非常适合零基础学习者入门学习。

1.1 系统功能定义

整个系统的功能分配非常明确,每个节点都有自己独特的职责。主节点负责监听总线并汇总显示数据,从节点负责采集并上报数据。这种主从架构是工业现场最常见的组网方式之一。

节点A (ID: 0x11) 的主要功能是模拟采集温度数据。温度传感器每500毫秒采集一次当前环境温度,并通过CAN总线向整个网络广播。温度数据以单字节形式发送,数值范围设定在20到40摄氏度之间,模拟真实的室内温度变化情况。每一次数据发送都会触发板载LED灯的翻转,这样开发者可以通过观察LED的闪烁频率直观判断节点是否正常工作。

节点B (ID: 0x22) 的主要功能是模拟采集湿度数据。与温度采集不同,湿度数据每秒(1000毫秒)更新一次。湿度数值的模拟范围设定在50%到80%之间,这是一个比较舒适的室内湿度区间。湿度数据同样通过CAN总线进行广播,但由于采集周期较长,LED的闪烁频率也会相应较慢,这种差异可以帮助我们区分不同的节点。

主控端 (Master) 是整个系统的数据汇聚中心。它不主动发送任何数据,专注于监听CAN总线。当总线上有数据经过时,Master通过硬件中断的方式立即获取数据帧,然后解析其中的标准ID来识别数据来源(是温度数据还是湿度数据)。最终通过UART串口将解析后的数据打印到电脑屏幕上。在调试阶段,这种可视化输出非常重要,它能帮助开发者直观看到整个通信系统的运行状态。

1.2 硬件准备清单

为了完成这个项目,我们需要准备以下硬件设备。这些设备在淘宝或电子元器件商店都很容易购买到,整套成本大约在50元左右。

微控制器方面,我们需要3块STM32F103C8T6核心板。这是ST公司推出的一款经典型单片机,性价比极高,内部集成了完整的CAN控制器硬件,非常适合学习CAN总线通信。每块核心板的价格大约在10到15元之间,选择兼容版(蓝色PCB)即可,不需要追求原厂。

CAN收发器是CAN通信中不可或缺的器件。STM32内部集成的是CAN控制器(属于协议层),它输出的是TTL电平信号,无法直接驱动CAN总线。我们需要使用TJA1050或SN65HVD230将TTL信号转换为差分信号。TJA1050需要5V供电,而SN65HVD230可以直接使用3.3V供电,对初学者更友好。建议购买现成的CAN收发器模块,带有DB9接口或插针接口的那种。

调试工具方面,一块ST-Link V2下载器是必需的。它不仅用于下载程序,还可以作为调试器使用单步执行代码。如果已经有其他调试器如J-Link或CMSIS-DAP也可以使用。

辅助材料包括面包板用于临时搭建电路、杜邦线用于连接各个模块、以及2个120欧姆的终端电阻。终端电阻的作用是消除信号反射,在CAN总线中非常重要,必须安装在总线两端的节点上。

2. 硬件连接与系统架构

2.1 CAN总线终端电阻原理

CAN总线最核心的硬件要求是终端电阻。在解释终端电阻之前,我们需要理解什么是信号反射。当高频信号沿传输线传播时,如果遇到阻抗不连续的点(如线缆末端),部分信号会反射回来,与原信号叠加造成干扰。CAN总线作为高速通信网络,其信号边沿非常陡峭,反射问题尤为突出。

在CAN总线的物理层标准中,要求在总线两端(也就是距离最远的两个节点)各并联一个120欧姆的电阻。这个阻值是经过精密计算的,与总线的特性阻抗相匹配。当信号到达总线末端时,电阻将信号完全吸收,从而彻底消除反射。在实际工程中,如果忘记安装终端电阻,通信距离稍长就会导致数据错误,这也是初学者最常遇到的问题之一。

2.2 系统硬件拓扑结构

整个系统的硬件连接可以划分为三个部分:CAN总线网络、Master主控系统、采集节点系统。下面通过Mermaid流程图来展示它们之间的物理连接关系。
采集节点 B
采集节点 A
CAN总线网络
主控端系统
UART TX
PA11 RX
PA12 TX
PA11/PA12
PA11/PA12
STM32 Master

STM32F103C8T6
USB转TTL

CH340G
CAN收发器

TJA1050
CAN_H (黄/绿线)
120Ω

终端电阻
CAN_L (蓝/白线)
STM32 Node A

ID: 0x11
CAN收发器
STM32 Node B

ID: 0x22
CAN收发器
120Ω

终端电阻

2.3 引脚接线详细说明

所有STM32F103系列芯片的CAN引脚是固定的,不需要像其他外设那样可以任意映射到任意引脚上。对于F103C8T6,CAN1默认映射到PA11和PA12引脚。具体接线如下表所示,这些引脚在所有三个节点上都是一致的。

STM32引脚 功能说明 连接到CAN收发器
PA11 CAN_RX (接收) RXD引脚
PA12 CAN_TX (发送) TXD引脚
5V或3.3V 供电正极 VCC引脚
GND 供电负极 GND引脚

需要特别注意收发器的供电电压。TJA1050必须使用5V供电,而SN65HVD230可以使用3.3V供电。在购买收发器模块时,建议选择支持宽电压输入的模块,这样可以使用STM32开发板的5V输出供电,避免额外的电源问题。

CAN收发器的CAN_H和CAN_L引脚需要连接到总线电缆上。这里有一个容易出错的地方:CAN_H必须连接到所有节点的CAN_H,CAN_L必须连接到所有节点的CAN_L,不能交叉连接。如果接反了,虽然不会烧毁设备,但通信绝对无法建立。

3. CubeMX配置详解

3.1 工程创建与基础配置

为了让代码能够实际落地,我们必须从配置开始。STM32CubeMX是ST官方提供的图形化配置工具,它可以自动生成HAL库的初始化代码,大大降低了开发门槛。即使是零基础的小白,只要按照步骤操作,也能完成复杂的外设配置。

首先打开CubeMX,点击"New Project"创建新工程。在左侧的MCU选择器中,搜索"STM32F103C8T6",然后选择对应的型号。选中之,后点击"Start Project"进入配置界面。

进入配置界面后,我们需要依次配置以下几个关键部分。**RCC(时钟配置)**部分:在右侧的引脚视图中找到RCC相关引脚,然后在左侧的配置栏中找到"RCC"选项,展开后找到"High Speed Clock (HSE)",选择"Crystal/Ceramic Resonator"。这表示我们将使用外部晶振作为系统时钟源,相比内部RC振荡器,外部晶振频率更精确,对CAN通信的波特率稳定性至关重要。

**SYS(系统调试接口)**部分:找到"SYS"选项,将"Debug"设置为"Serial Wire"。这允许我们使用SWD接口进行调试和程序下载,同时释放出PA13和PA14引脚供其他功能使用。很多初学者忘记配置这个选项,导致后续无法进行调试。

**Clock Configuration(时钟树配置)**部分:这是最关键的一步。点击顶部的"Clock Configuration"标签页进入时钟树配置界面。在"System Clock Mux"中选择PLL作为时钟源,然后在PLL Source Mux中选择HSE。重要的是将HCLK设置为72MHz,这是STM32F103的标准最高主频。设置完成后,CubeMX会自动计算并配置所有分频器和倍频器。

3.2 CAN参数计算与配置

CAN总线的波特率配置是整个系统中最重要的参数之一。所有连接到同一总线上的设备必须配置完全相同的波特率,否则通信无法建立。波特率的计算涉及到APB1时钟频率和多个时序参数。

STM32F103的CAN控制器挂载在APB1总线上。根据上一步的配置,APB1的时钟频率为36MHz(注意:APB1的频率是HCLK的一半,即72MHz/2=36MHz)。CAN控制器的工作时序基于"时间量子"(Time Quanta)的概念。一个时间量子是CAN控制器工作的基本时间单位,由APB1时钟经过预分频后得到。

波特率的计算公式如下:
波特率 = APB1时钟频率 ÷ (预分频系数 × (1 + BS1 + BS2))

其中BS1是时间段1的量子数,BS2是时间段2的量子数。SJW是同步跳转宽度,用于补偿不同节点之间的时钟误差。

为了得到常用的500kbps波特率,我们需要让APB1时钟36MHz除以某个整数等于500kHz。即:
36MHz ÷ 500kHz = 72

这意味着我们需要让(预分频系数 × (1 + BS1 + BS2)) = 72。一个常用的配置组合是:预分频系数=9,BS1=5,BS2=2,验证一下:9 × (1 + 5 + 2) = 9 × 8 = 72,完全正确。

在CubeMX中的具体配置步骤如下:首先在左侧找到"Connectivity"分类,展开后点击"CAN"。在弹出的配置界面中,勾选"Master Mode"(注意这只是表示启用CAN外设,并非主从关系)。然后在参数设置区域按照以下数值进行配置:

参数名称 设置值 说明
Prescaler 9 预分频系数,将36MHz分频得到4MHz
Time Quanta in Bit Segment 1 5 Times BS1段长度
Time Quanta in Bit Segment 2 2 Times BS2段长度
ReSynchronization Jump Width 1 Time 同步跳转宽度
Time Triggered Communication Mode Disable 禁用时间触发模式
Automatic Bus-Off Management Enable 开启自动总线关闭管理
Automatic Wake-Up Mode Enable 开启自动唤醒
Automatic Retransmission Enable 开启自动重传
Receive Fifo Locked Mode Disable 禁用FIFO锁定
Transmit Fifo Priority Disable 禁用优先级发送
Operating Mode Normal 正常工作模式(重要!)

特别提醒:Operating Mode必须选择Normal,如果选择Loopback模式,CAN控制器只会自发自收,无法与外部设备通信。这是初学者最容易忽略的设置。

完成CAN配置后,还需要配置NVIC(嵌套向量中断控制器)。在CAN配置界面中找到"NVIC Settings"标签,勾选"USB Low priority or CAN RX0 interrupt",将其设置为Enabled。这是接收中断的入口,必须开启才能接收到数据。

3.3 串口配置(仅Master需要)

作为主控端,Master需要通过串口将接收到的数据打印到电脑上。在CubeMX中配置串口的方法如下:

在"Connectivity"分类下找到"USART1",点击进入配置界面。将Mode设置为"Asynchronous"(异步模式)。在"Parameter Settings"中,保持默认的8N1配置(8位数据位、无校验、1位停止位),波特率设置为115200。这个波特率是初学者最常用的设置,与绝大多数串口调试助手兼容。

配置完成后,别忘了在"NVIC Settings"中开启USART1的中断。虽然这里不使用中断方式进行串口通信,但开启中断可以让HAL库更好地处理数据。

4. 软件流程设计

4.1 整体数据流向分析

在编写代码之前,我们先梳理一下数据的流转逻辑。这有助于从宏观上理解整个系统的工作方式,即使遇到问题也能快速定位。

系统上电后,三个节点首先进行各自的初始化。Master节点需要配置CAN过滤器、启动CAN外设、开启接收中断、初始化串口。Node A和Node B需要配置CAN过滤器、启动CAN外设、配置发送参数。初始化完成后,所有节点进入正常工作状态。

数据采集和发送由两个从节点主动发起。Node A每500毫秒读取一次模拟的温度数据,填充到发送缓冲区,然后调用HAL库的发送函数将数据帧推送到CAN总线上。数据帧包含发送者的标准ID(0x11)、数据长度(8字节)以及实际的数据内容。

数据帧在CAN总线上传播,被所有连接到总线的节点接收。Master节点的CAN控制器收到数据帧后,首先检查过滤器的掩码设置。如果ID匹配,数据会被放入接收FIFO,并触发RX0中断。在中断服务程序中,Master读取数据帧,解析其中的ID和数据内容,最后通过串口输出到电脑屏幕上。
串口终端 CAN总线 节点B (0x22) 节点A (0x11) 主控端 (0x01) 串口终端 CAN总线 节点B (0x22) 节点A (0x11) 主控端 (0x01) 系统初始化 采集温度 loop [NodeA数据采集周期 (500ms)] 采集湿度 loop [NodeB数据采集周期 (1000ms)] 配置CAN过滤器 启动CAN外设 开启RX0中断 打印启动信息 模拟温度值+1 发送CAN帧 (ID=0x11) 触发RX0中断 解析数据来源 打印温度数据 模拟湿度值+1 发送CAN帧 (ID=0x22) 触发RX0中断 解析数据来源 打印湿度数据

4.2 CAN过滤器工作原理

CAN过滤器是CAN控制器中一个非常重要但又经常被初学者忽视的功能。它的作用类似于邮件分拣员:CAN总线上每秒可能流通着成千上万个数据帧,我们的节点只需要接收特定ID的数据,这时就需要过滤器来筛选。

STM32F103的CAN控制器提供14个过滤器组(Filter Bank),每个过滤器组可以配置为掩码模式或列表模式。在掩码模式下,我们需要设置一个ID和一个掩码。掩码中为1的位表示该位必须匹配,为0的位表示该位可以是任意值。例如,如果我们设置ID=0x11,掩码=0x7FF(所有位都关心),那么只有ID完全等于0x11的数据帧才会被接收。

如果我们设置ID=0x00,掩码=0x000(所有位都不关心),那么无论ID是什么,数据帧都会被接收。这是最宽松的配置,适合学习阶段使用。实际工程项目中,我们通常会根据需要接收的ID范围来配置掩码,以过滤掉不相关的总线流量。

5. 代码实现:公共基础代码

5.1 main.c文件头部配置

为了方便零基础学习者理解,我们不封装复杂的库文件,所有的代码都直接写在main.c中。这种方式虽然不够优雅,但非常适合学习理解每个步骤的作用。

在使用CubeMX生成工程后,我们需要手动添加一些代码来实现我们的功能。首先打开Core/Src/main.c文件,找到用户代码区域。

文件名:Core/Src/main.c

c 复制代码
/* USER CODE BEGIN Includes */
#include <stdio.h>      // 标准输入输出库,提供printf函数
#include <string.h>     // 字符串处理库
/* USER CODE END Includes */

在文件开头引入必要的头文件。stdio.h提供了printf函数的支持,这在调试阶段打印信息非常有用。string.h提供了一些字符串处理函数,虽然本例中使用不多,但属于常规引入。

5.2 CAN过滤器配置函数

接下来在用户代码区域0中添加CAN过滤器配置函数。这个函数是整个CAN通信的基础,如果不配置过滤器,CAN控制器会默认拒绝接收所有数据帧,导致通信完全无法工作。

文件名:Core/Src/main.c

c 复制代码
/* USER CODE BEGIN 0 */

// CAN发送数据结构体定义
CAN_TxHeaderTypeDef TxHeader;    // 发送帧头结构体
uint8_t TxData[8];                // 发送数据缓冲区,8字节
uint32_t TxMailbox;               // 发送邮箱编号

// CAN接收数据结构体定义  
CAN_RxHeaderTypeDef RxHeader;    // 接收帧头结构体
uint8_t RxData[8];                // 接收数据缓冲区,8字节

// CAN过滤器配置函数
// 说明:STM32的CAN控制器需要配置过滤器才能接收数据
// 如果不配置,默认会拒绝所有接收请求
void CAN_Filter_Config(void)
{
    CAN_FilterTypeDef sFilterConfig;

    // 配置过滤器参数
    sFilterConfig.FilterBank = 0;                        // 使用过滤器组0(共14组,0-13)
    sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;   // 掩码模式:对比ID和掩码决定是否接收
    sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;  // 32位宽度模式
    sFilterConfig.FilterIdHigh = 0x0000;                // 验证码高16位:0表示完全匹配
    sFilterConfig.FilterIdLow = 0x0000;                 // 验证码低16位
    sFilterConfig.FilterMaskIdHigh = 0x0000;            // 掩码高16位:0表示不关心该位
    sFilterConfig.FilterMaskIdLow = 0x0000;             // 掩码低16位:0表示不关心该位
    // 综合以上设置:本过滤器接收所有ID的数据帧
    
    sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;   // 接收到的报文放入FIFO0队列
    sFilterConfig.FilterActivation = ENABLE;             // 使能过滤器
    sFilterConfig.SlaveStartFilterBank = 14;             // 双CAN模式下从CAN起始组号(单CAN忽略)

    // 调用HAL库函数配置过滤器
    if (HAL_CAN_ConfigFilter(&hcan, &sFilterConfig) != HAL_OK)
    {
        // 配置失败进入错误处理函数
        Error_Handler();
    }
}

/* USER CODE END 0 */

这段代码的核心在于过滤器参数的理解。FilterId和FilterMaskId都设置为0,配合掩码模式(IDMASK),意味着不进行任何过滤,接收所有ID的数据帧。这是学习阶段最简单粗暴的配置方式,可以确保我们能够收到所有节点发送的数据。实际项目中,应该根据需要只接收特定ID范围的数据,以提高效率。

5.3 串口重定向函数(仅Master需要)

Master节点需要将接收到的数据通过串口打印到电脑上。HAL库默认不提供printf的支持,我们需要手动实现串口重定向。这段代码利用了ARM编译器的特性,将标准库的printf输出重定向到USART1。

文件名:Core/Src/main.c (仅Master)

c 复制代码
/* USER CODE BEGIN 0 */

// 重定向printf函数到UART1
// 这段代码使用了GCC编译器的特性
#ifdef __GNUC__
    // GCC编译器下的实现
    int _write(int file, char *ptr, int len)
    {
        // 调用HAL库的UART发送函数
        HAL_UART_Transmit(&huart1, (uint8_t*)ptr, len, HAL_MAX_DELAY);
        return len;
    }
#else
    // 非GCC编译器(如Keil MDK)下的实现
    int fputc(int ch, FILE *f)
    {
        HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
        return ch;
    }
#endif

/* USER CODE END 0 */

这段代码的作用是拦截标准库的写入操作,将原本要输出到控制台的数据改为通过UART1发送。使用时直接调用printf函数即可,就像在电脑上使用printf一样。需要特别注意的是,GCC编译器和Keil编译器对重定向的实现方式不同,这里通过条件编译同时兼容两种编译器。

6. 代码实现:发送节点

6.1 Node A主程序代码

发送节点的核心逻辑是:初始化CAN外设 → 循环采集数据 → 发送数据帧 → 延时 → 重复。下面是Node A的完整代码实现。

文件名:Core/Src/main.c (Node A)

c 复制代码
  /* USER CODE BEGIN 2 */
  
  // 第一步:配置CAN过滤器
  CAN_Filter_Config();
  
  // 第二步:启动CAN外设
  // 必须调用此函数才能开始CAN通信
  if (HAL_CAN_Start(&hcan) != HAL_OK)
  {
      // 启动失败进入错误处理
      Error_Handler();
  }

  // 第三步:配置发送帧头参数
  TxHeader.StdId = 0x11;            // 标准ID:0x11 (节点A的标识)
  TxHeader.ExtId = 0x00;           // 扩展ID:未使用,设为0
  TxHeader.IDE = CAN_ID_STD;       // 帧类型:标准帧 (11位ID)
  TxHeader.RTR = CAN_RTR_DATA;     // 帧格式:数据帧 (不是远程帧)
  TxHeader.DLC = 8;                 // 数据长度:8字节 (最大支持8字节)
  TxHeader.TransmitGlobalTime = DISABLE; // 不发送时间戳
  
  // 初始化发送数据
  memset(TxData, 0, 8);             // 清零数据缓冲区
  
  /* USER CODE END 2 */

  /* USER CODE BEGIN WHILE */
  
  // 模拟温度传感器初始值
  uint8_t temperature = 20;         // 初始温度20度
  uint8_t counter = 0;              // 发送计数器
  
  while (1)
  {
    // 模拟温度数据变化
    // 温度在20-40度之间循环变化
    temperature++;
    if (temperature > 40)
    {
        temperature = 20;
    }
    counter++;
    
    // 填充发送数据
    // 协议设计:第0字节=温度值,第1-3字节=版本号(0x01 0x02 0x03)
    TxData[0] = temperature;        // 实际温度值
    TxData[1] = 0x01;               // 协议版本号
    TxData[2] = 0x02;               // 节点类型标识
    TxData[3] = counter;            // 发送计数器
    TxData[4] = 0xAA;               // 填充字节,用于调试观察
    TxData[5] = 0xBB;               // 填充字节
    TxData[6] = 0xCC;               // 填充字节
    TxData[7] = 0xDD;               // 填充字节

    // 发送数据帧
    // 参数说明:
    //   &hcan: CAN句柄
    //   &TxHeader: 发送帧头信息
    //   TxData: 要发送的数据
    //   &TxMailbox: 存储发送邮箱编号的变量(用于查询发送状态)
    if (HAL_CAN_AddTxMessage(&hcan, &TxHeader, TxData, &TxMailbox) != HAL_OK)
    {
        // 发送失败处理
        // 实际项目中可以添加LED闪烁来指示错误
        // 这里选择静默忽略,继续下一次发送
    }

    // 翻转板载LED指示灯(PC13)
    // 通过观察LED闪烁可以判断程序是否正常运行
    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);

    // 延时500ms
    // Node A每500ms发送一次数据
    HAL_Delay(500);
    
    /* USER CODE END WHILE */
  }

这段代码有几个关键点需要理解。首先是TxHeader的配置,每个节点必须有唯一的StdId,这样接收方才能区分数据来源。DLC设置8表示本次发送8字节数据,这是CAN2.0A标准帧允许的最大数据长度。其次是HAL_CAN_AddTxMessage函数,这是HAL库提供的发送函数,内部会自动选择空闲的发送邮箱。如果所有邮箱都被占用,函数会返回错误码。

板载LED的翻转是一个很好的调试手段。如果LED不亮,说明程序可能卡死在初始化阶段;如果LED常量不闪烁,说明主循环没有正常执行;只有LED按预期频率闪烁,才能证明程序正常运行。

6.2 Node B主程序代码

Node B的代码与Node A几乎完全相同,唯一的区别是StdId和数据内容。如果同时只有一个STM32开发板,可以先烧录Node A的代码测试,测试完毕后修改以下几行再烧录到另一块板子上。

文件名:Core/Src/main.c (Node B)

c 复制代码
  /* USER CODE BEGIN 2 */
  
  // CAN过滤器配置(与Node A完全相同)
  CAN_Filter_Config();
  
  // 启动CAN外设
  if (HAL_CAN_Start(&hcan) != HAL_OK)
  {
      Error_Handler();
  }

  // 配置发送帧头 - 这里改为Node B的ID
  TxHeader.StdId = 0x22;            // 标准ID:0x22 (Node B的标识)
  TxHeader.ExtId = 0x00;            // 扩展ID
  TxHeader.IDE = CAN_ID_STD;       // 标准帧
  TxHeader.RTR = CAN_RTR_DATA;     // 数据帧
  TxHeader.DLC = 8;                 // 8字节数据
  TxHeader.TransmitGlobalTime = DISABLE;
  
  memset(TxData, 0, 8);
  
  /* USER CODE END 2 */

  /* USER CODE BEGIN WHILE */
  
  // 模拟湿度传感器初始值
  uint8_t humidity = 50;            // 初始湿度50%
  uint8_t counter = 0;
  
  while (1)
  {
    // 模拟湿度数据变化
    // 湿度在50-80%之间循环变化
    humidity++;
    if (humidity > 80)
    {
        humidity = 50;
    }
    counter++;
    
    // 填充发送数据
    // 协议设计:第0字节=湿度值,第1字节=节点标识
    TxData[0] = humidity;           // 实际湿度值
    TxData[1] = 0x02;               // 节点类型标识(与Node A不同)
    TxData[2] = 0x00;               // 保留
    TxData[3] = counter;            // 发送计数器
    TxData[4] = 0x11;               // 填充字节
    TxData[5] = 0x22;               // 填充字节
    TxData[6] = 0x33;               // 填充字节
    TxData[7] = 0x44;               // 填充字节

    // 发送数据帧
    if (HAL_CAN_AddTxMessage(&hcan, &TxHeader, TxData, &TxMailbox) != HAL_OK)
    {
        // 发送失败处理
    }

    // 翻转LED指示灯
    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);

    // 延时1000ms
    // Node B每1000ms发送一次(比Node A慢)
    HAL_Delay(1000);
    
    /* USER CODE END WHILE */
  }

对比Node A和Node B的代码,主要差异在于StdId(0x11 vs 0x22)、数据含义(温度 vs 湿度)以及发送周期(500ms vs 1000ms)。正是因为StdId的不同,Master才能准确区分数据的来源。

7. 代码实现:主控接收端

7.1 Master主程序初始化

Master节点的初始化过程比发送节点多一些步骤,因为它不仅需要启动CAN接收,还需要开启串口输出。特别是CAN接收中断的激活,是保证实时接收数据的关键。

文件名:Core/Src/main.c (Master)

c 复制代码
  /* USER CODE BEGIN 2 */
  
  // 第一步:配置CAN过滤器
  // Master也需要配置过滤器,否则无法接收数据
  CAN_Filter_Config();
  
  // 第二步:启动CAN外设
  if (HAL_CAN_Start(&hcan) != HAL_OK)
  {
      Error_Handler();
  }

  // 第三步:激活CAN接收中断
  // 当FIFO0中有待处理的报文时,产生中断通知CPU
  // 这是实现实时接收的关键机制
  if (HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
  {
      Error_Handler();
  }
  
  // 第四步:初始化串口输出
  // 打印系统启动信息到电脑
  printf("========================================\r\n");
  printf("  CAN Master Station Started\r\n");
  printf("  System Clock: 72MHz\r\n");
  printf("  CAN Baudrate: 500kbps\r\n");
  printf("========================================\r\n");
  printf("Waiting for CAN data...\r\n");
  printf("\r\n");
  
  /* USER CODE END 2 */

  /* USER CODE BEGIN WHILE */
  
  while (1)
  {
    // Master节点的主循环不需要做任何事情
    // 所有数据接收都在中断回调函数中完成
    // 这里可以添加其他辅助功能,如LED状态指示
    
    // LED常量亮表示系统正常运行
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
    
    // 主循环延时
    HAL_Delay(1000);
    
    /* USER CODE END WHILE */
  }

Master节点的设计采用了中断驱动模式。主循环几乎是空闲的,所有数据接收工作都在中断回调函数中完成。这种设计有多个优点:首先,CPU不需要轮询检查接收状态,效率更高;其次,中断响应速度快,不会丢失任何数据帧;第三,主循环可以腾出来处理其他任务,如显示刷新、按键处理等。

7.2 CAN接收中断回调函数

中断回调函数是整个Master节点的核心。当CAN控制器收到有效数据帧并通过过滤器筛选后,会自动调用这个函数。在这个函数中,我们需要读取数据帧的内容并进行解析处理。

文件名:Core/Src/main.c (Master 回调函数)

c 复制代码
/* USER CODE BEGIN 4 */

// CAN接收FIFO0消息挂起中断回调函数
// 参数:hcan - CAN句柄
// 返回值:无
// 说明:当FIFO0中有新消息时由硬件自动调用
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    // 从FIFO0读取接收到的消息
    // 参数说明:
    //   hcan: CAN句柄
    //   CAN_RX_FIFO0: 从FIFO0读取
    //   &RxHeader: 存储接收帧头信息的结构体
    //   RxData: 存储接收数据的缓冲区
    if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData) == HAL_OK)
    {
        // 成功读取到数据帧
        
        // 根据标准ID判断数据来源
        switch (RxHeader.StdId)
        {
            case 0x11:
                // 来自Node A (温度节点)
                // 解析数据:第0字节是温度值
                printf("[Node A] Temperature: %d C | ", RxData[0]);
                printf("NodeID: 0x%02X | ", RxHeader.StdId);
                printf("DLC: %d | ", RxHeader.DLC);
                printf("Raw: %02X %02X %02X %02X\r\n", 
                       RxData[0], RxData[1], RxData[2], RxData[3]);
                break;
                
            case 0x22:
                // 来自Node B (湿度节点)
                // 解析数据:第0字节是湿度值
                printf("[Node B] Humidity: %d %% | ", RxData[0]);
                printf("NodeID: 0x%02X | ", RxHeader.StdId);
                printf("DLC: %d | ", RxHeader.DLC);
                printf("Raw: %02X %02X %02X %02X\r\n", 
                       RxData[0], RxData[1], RxData[2], RxData[3]);
                break;
                
            default:
                // 来自未知节点
                printf("[Unknown Node] ID: 0x%02X | DLC: %d | Data:", 
                       RxHeader.StdId, RxHeader.DLC);
                for (int i = 0; i < RxHeader.DLC; i++)
                {
                    printf(" %02X", RxData[i]);
                }
                printf("\r\n");
                break;
        }
        
        // 收到数据后翻转LED作为指示
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
    }
    else
    {
        // 读取失败(通常不会发生)
        printf("[Error] CAN receive failed!\r\n");
    }
}

/* USER CODE END 4 */

这个回调函数的设计包含了几个重要的知识点。首先是通过StdId来区分不同的数据发送节点,这是多节点通信中的基本做法。其次是RxHeader中包含的丰富信息,除了StdId还有DLC(数据长度)、IDE(帧类型)、RTR(远程帧标志)等,可以用于更复杂的数据协议设计。第三是Raw数据的打印,通过十六进制格式可以观察到完整的原始数据,便于调试分析。

7.3 完整的Master main函数结构

为了让零基础学习者能够完整理解,这里给出Master节点main函数的完整结构,包括所有用户代码区域的位置。

文件名:Core/Src/main.c (Master 完整框架)

c 复制代码
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "can.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */
CAN_TxHeaderTypeDef TxHeader;
CAN_RxHeaderTypeDef RxHeader;
uint8_t TxData[8];
uint8_t RxData[8];
uint32_t TxMailbox;
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);

/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_CAN_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  
  // === Master节点初始化代码开始 ===
  
  // 1. 配置CAN过滤器
  CAN_Filter_Config();
  
  // 2. 启动CAN外设
  if (HAL_CAN_Start(&hcan) != HAL_OK)
  {
      Error_Handler();
  }
  
  // 3. 激活接收中断
  if (HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
  {
      Error_Handler();
  }
  
  // 4. 串口打印启动信息
  printf("========================================\r\n");
  printf("  CAN Master Station Started\r\n");
  printf("========================================\r\n");
  
  // === Master节点初始化代码结束 ===
  
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    // 主循环可以空闲,或处理其他任务
    HAL_Delay(1000);
    /* USER CODE END WHILE */
  }
  /* USER CODE BEGIN 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  // 由CubeMX自动生成,勿手动修改
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 0 */

// CAN过滤器配置函数
void CAN_Filter_Config(void)
{
    CAN_FilterTypeDef sFilterConfig;
    
    sFilterConfig.FilterBank = 0;
    sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
    sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
    sFilterConfig.FilterIdHigh = 0x0000;
    sFilterConfig.FilterIdLow = 0x0000;
    sFilterConfig.FilterMaskIdHigh = 0x0000;
    sFilterConfig.FilterMaskIdLow = 0x0000;
    sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
    sFilterConfig.FilterActivation = ENABLE;
    sFilterConfig.SlaveStartFilterBank = 14;
    
    if (HAL_CAN_ConfigFilter(&hcan, &sFilterConfig) != HAL_OK)
    {
        Error_Handler();
    }
}

// printf重定向函数
#ifdef __GNUC__
int _write(int file, char *ptr, int len)
{
    HAL_UART_Transmit(&huart1, (uint8_t*)ptr, len, HAL_MAX_DELAY);
    return len;
}
#else
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}
#endif

/* USER CODE END 0 */

/* USER CODE BEGIN 4 */

// CAN接收中断回调函数
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData) == HAL_OK)
    {
        switch (RxHeader.StdId)
        {
            case 0x11:
                printf("[Node A] Temp: %d C\r\n", RxData[0]);
                break;
            case 0x22:
                printf("[Node B] Humi: %d %%\r\n", RxData[0]);
                break;
            default:
                printf("[Unknown] ID: 0x%X\r\n", RxHeader.StdId);
                break;
        }
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
    }
}

/* USER CODE END 4 */

8. 编译与烧录流程

8.1 工程编译

完成代码编写后,需要进行编译生成可执行文件。在Keil MDK中,点击菜单栏的"Project" → "Build Target"(或直接按F7快捷键)开始编译。首次编译会耗时较长,因为需要编译整个HAL库。后续编译会快很多,因为只编译修改过的文件。

编译过程中会在底部的Build Output窗口显示编译信息。如果代码没有问题,最后会显示"0 Error(s), 0 Warning(s)"。如果有错误,需要根据错误提示进行修改。最常见的错误包括头文件未包含、变量未定义、函数调用参数不匹配等。

8.2 程序烧录

编译成功后,需要将生成的hex文件烧录到STM32芯片中。连接好ST-Link下载器,确保开发板已上电。在Keil中点击"Download"按钮(或按F8快捷键)开始烧录。

烧录过程中,ST-Link的指示灯会闪烁。烧录成功后,可以在Keil的调试界面查看程序是否按预期运行。如果使用CubeProgrammer工具,可以更直观地看到烧录进度和结果。

9. 调试与验证步骤

9.1 验证前的准备工作

在开始验证之前,需要确保所有硬件连接正确。首先检查CAN收发器的供电是否正常,TJA1050需要5V供电,LED应该亮起;SN65HVD230使用3.3V供电,LED状态取决于模块设计。其次检查CAN_H和CAN_L的连接,确保所有节点的CAN_H连在一起,CAN_L连在一起。

然后检查终端电阻的安装。在总线两端的节点(通常是Master和Node B)的CAN_H与CAN_L之间应该各并联一个120欧姆电阻。如果使用面包板,可以直接插上电阻;如果使用专用CAN模块,模块上通常自带终端电阻拨码开关,拨到ON位置即可。

最后准备串口调试助手。Master节点的USART1通过PA9(TX)输出数据。连接USB转TTL模块:USB转TTL的RX接STM32的PA9,USB转TTL的GND接STM32的GND。打开串口调试助手(如SecureCRT、Putty、MobaXterm等),设置波特率115200,数据位8,停止位1,无校验,然后打开串口。

9.2 验证步骤与预期结果

完成以上准备工作后,给所有板子上电。复位Master板子,应该立即在串口调试助手中看到以下输出:

复制代码
========================================
  CAN Master Station Started
========================================
Waiting for CAN data...

如果没有看到这行输出,说明串口连接有问题,或者程序没有正常启动。检查串口调试助手的设置是否正确,检查USB转TTL模块的驱动是否安装成功。

正常情况下,等待几秒钟后,应该能看到Node A和Node B发送的数据交替出现:

复制代码
[Node A] Temp: 20 C
[Node A] Temp: 21 C
[Node B] Humi: 50 %
[Node A] Temp: 22 C
[Node A] Temp: 23 C
[Node B] Humi: 51 %
[Node A] Temp: 24 C
...

观察这个输出,可以发现:Node A的数据每500ms更新一次,数值递增;Node B的数据每1000ms更新一次,数值递增;两者的更新频率不同,这与代码中的延时时间一致。

同时观察三块开发板的LED灯。Master板的LED应该随着每次数据接收而闪烁;Node A的LED每500ms闪烁一次;Node B的LED每1000ms闪烁一次。这种视觉反馈可以直观地确认每个节点都在正常工作。

9.3 验证失败的处理方法

如果无法看到预期的数据输出,需要按照以下顺序进行排查。首先检查硬件层面:CAN收发器是否正常供电(5V或3.3V根据模块型号而定);CAN_H和CAN_L是否接反;120欧姆终端电阻是否安装;所有板子的GND是否共连。其次检查软件层面:CubeMX中CAN的工作模式是否设置为Normal而不是Loopback;NVIC中是否开启了CAN RX0中断;CAN过滤器是否正确配置。

如果使用示波器或逻辑分析仪,可以直接观察PA12(CAN_TX)引脚上是否有数据波形。正常情况下,应该能看到差分信号。如果TX引脚有波形但总线上没有,可能是收发器损坏或连接问题。

10. 常见问题排查与解决方案

10.1 完全收不到数据

这是最常见的问题。现象是串口调试助手中完全没有输出,Master似乎没有接收到任何数据。造成这个问题的原因有多个方面,需要系统性地排查。

第一个原因是CAN收发器未正确供电。很多初学者会忽略这一点,导致收发器根本不工作。解决方法是用万用表测量收发器模块的VCC和GND之间的电压,确保供电正常。TJA1050模块需要5V供电,如果只接了3.3V,它不会工作但也不会损坏。

第二个原因是CAN_H和CAN_L接反。CAN总线是差分信号,H和L不能接反。解决方法是对照原理图或模块丝印,确认所有节点的CAN_H接在一起,CAN_L接在一起。

第三个原因是最常见的:CAN过滤器配置被遗漏或配置错误。在CubeMX生成的初始代码中,并没有过滤器配置的代码,必须手动添加。如果使用了本教程提供的CAN_Filter_Config函数仍然无法接收,请检查该函数是否在main函数中被调用。

第四个原因是中断未开启。在CubeMX中需要同时在CAN配置界面和NVIC设置中开启中断,缺一不可。

10.2 数据接收时断时续

这个问题表现为串口有输出,但数据会突然丢失或显示乱码。造成这个问题的常见原因包括终端电阻缺失或阻值不对、多个节点未共地、以及总线负载过重。

终端电阻是CAN总线中最容易被忽视的元件。根据CAN标准,总线两端必须安装120欧姆终端电阻。如果缺少终端电阻,信号在总线末端无法被吸收,会反射回来造成干扰,导致远距离通信失败。即使通信距离很短(如实验室环境),最好也加上终端电阻以确保信号质量。

共地问题也很重要。虽然CAN是差分信号,但所有节点的GND必须连接在一起。如果某个节点浮地(没有共地),信号参考点不一致,可能导致接收错误。

10.3 程序卡死在初始化阶段

如果板子上电后LED不亮也不闪烁,串口没有输出,可能是因为程序卡死在了初始化阶段。最常见的原因是HAL库初始化失败,通常是因为CubeMX中的时钟配置与实际硬件不符。

检查开发板上焊接的晶振是8MHz还是其他频率。在CubeMX中,HSE应该选择"Crystal/Ceramic Resonator",而不是"Bypass Clock Source"。如果不确定,可以先用示波器测量晶振引脚是否有波形。

另一个可能的原因是程序下载到了错误的地址。确保ST-Link的SWDIO和SWCLK连接正确,且在CubeProgrammer中选择的是正确的芯片型号。

11. 系统功能扩展思路

11.1 添加数据校验机制

当前的数据协议非常简单,只传输了原始数值。在实际工程中,需要添加数据校验以确保数据的可靠性。可以在数据帧中加入CRC校验码或简单的累加和校验。发送端在填充数据时计算校验码并放在特定字节,接收端收到后重新计算校验码进行比对,如果不匹配则丢弃该帧。

11.2 实现命令下发功能

当前系统是单向通信(从节点到Master)。可以扩展为双向通信,让Master能够向从节点下发命令。例如,让Master发送ID为0x01的命令帧,从节点收到后执行相应动作(如校准传感器、改变采样频率等)。这需要在从节点中也实现接收功能。

11.3 增加更多采集节点

当前系统有2个从节点,理论上CAN总线最多可以支持2^11=2048个不同的节点(标准帧11位ID)。可以按照相同的步骤增加更多节点,只需要给每个节点分配唯一的ID,并在Master的switch-case中添加对应的处理分支即可。

12. 技术总结

通过本项目,我们成功实现了一个完整的CAN总线多节点通信与数据采集系统。从硬件连接到软件配置,从代码编写到调试验证,涵盖了CAN总线应用开发的各个方面。

与传统的UART串口相比,CAN总线具有明显的优势:差分信号传输提供了极高的抗干扰能力,适合工业环境;多主站架构使得任意节点都可以主动发送数据,无需轮询;内置的错误检测和自动重传机制保证了通信的可靠性;最远可达1公里的通信距离使其特别适合大型设备的组网。

虽然本项目的硬件简单、协议简单,但涉及的知识点却是CAN通信的核心基础。掌握这些内容后,可以进一步学习CANopen、DeviceNet等高层协议,或者将CAN总线应用于汽车电子、工业机器人、楼宇自动化等更复杂的系统中。

希望这篇教程能够帮助零基础的学习者快速入门STM32的CAN总线开发。

相关推荐
12.=0.2 小时前
【stm32_2.1】【快速入门】自举模式、Flash闪存、LED点灯——对二极管PN结解析
stm32·单片机·嵌入式硬件
辰哥单片机设计2 小时前
STM32智能风扇(机智云)
stm32·单片机·嵌入式硬件
【 STM32开发 】2 小时前
【STM32 + CubeMX】低功耗 -- SLEEP 睡眠模式
stm32·单片机·低功耗·sleep·睡眠模式
芯芯点灯3 小时前
LIS2DW12驱动,功耗,数据可视化
驱动开发·单片机
Nice__J3 小时前
Mcu架构以及原理——2.Cortex-M流水线与指令集
单片机·嵌入式硬件·架构
小白橘颂4 小时前
【C语言】基础概念梳理(一)
c语言·开发语言·stm32·单片机·mcu·物联网·51单片机
aini_lovee4 小时前
SIM7600模块STM32控制程序
stm32·单片机·嵌入式硬件
是翔仔呐4 小时前
第13章 超声波测距传感器驱动:HC-SR04底层原理与C语言实现
c语言·开发语言·单片机·嵌入式硬件·gitee
小飞菜涅5 小时前
fast-lio2复现
嵌入式硬件·学习·ubuntu