引言

STM32(基于ARM Cortex-M内核的微控制器)和树莓派(基于ARM Cortex-A内核的单板计算机)是嵌入式系统中两种非常流行的硬件平台。STM32擅长实时控制、低功耗和精确的硬件接口,而树莓派则擅长运行复杂的操作系统、处理大量数据和网络通信。将两者结合,可以构建功能强大的混合系统,例如:STM32负责实时传感器数据采集和电机控制,树莓派负责数据处理、用户界面和云端通信。

本文将详细介绍STM32与树莓派之间的多种通信方法,涵盖从硬件连接到软件编程的完整流程。我们将重点讨论以下几种主流通信方式:

  1. UART(串口通信):最简单、最常用的异步通信方式。
  2. I2C(内部集成电路总线):两线制同步通信,适合连接多个从设备。
  3. SPI(串行外设接口):高速同步通信,适合与高速外设(如显示屏、SD卡)通信。
  4. USB(通用串行总线):利用树莓派的USB主机功能与STM32的USB设备功能通信。
  5. 以太网(通过STM32的以太网MAC或外接模块):实现网络通信。
  6. CAN(控制器局域网络):常用于汽车和工业环境的可靠通信。

我们将以STM32F4系列(如STM32F407VGT6)和树莓派4B为例进行说明,但原理同样适用于其他型号。


1. UART(串口通信)

UART是最简单、最直接的通信方式,只需要连接TX、RX和GND三根线即可实现双向通信。

1.1 硬件连接

注意:STM32和树莓派的逻辑电平都是3.3V,因此可以直接连接,无需电平转换。

  • STM32端:选择一个UART外设,例如USART2。其引脚通常为:
    • PA2 (USART2_TX)
    • PA3 (USART2_RX)
  • 树莓派端:使用其板载的UART引脚(GPIO14/15)或通过USB转串口模块连接。

连接方式:

  • STM32 PA2 (TX) -> 树莓派 GPIO15 (RX) 或 USB转串口模块的 RX
  • STM32 PA3 (RX) -> 树莓派 GPIO14 (TX) 或 USB转串口模块的 TX
  • STM32 GND -> 树莓派 GND 或 USB转串口模块的 GND

重要提示:树莓派的板载UART默认可能被用于蓝牙或系统控制,需要在/boot/config.txt中配置以启用标准UART:

# 在树莓派终端中编辑配置文件
sudo nano /boot/config.txt
# 在文件末尾添加以下行
dtoverlay=pi3-disable-bt
enable_uart=1
# 保存并重启
sudo reboot

重启后,树莓派的UART设备通常为/dev/ttyAMA0

1.2 软件编程

1.2.1 STM32端编程(使用HAL库)

使用STM32CubeMX生成初始化代码,配置USART2为115200波特率,8位数据,无校验,1停止位。

接收中断方式(推荐)

// main.c
#include "main.h"
#include <stdio.h>
#include <string.h>

UART_HandleTypeDef huart2;
uint8_t rx_buffer[100];
uint8_t rx_index = 0;
uint8_t rx_data;

// 重定向printf到USART2
int __io_putchar(int ch) {
    HAL_UART_Transmit(&huart2, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
    return ch;
}

// 接收完成回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART2) {
        if (rx_data != '\n' && rx_index < 99) {
            rx_buffer[rx_index++] = rx_data;
        } else {
            rx_buffer[rx_index] = '\0'; // 字符串结束符
            printf("Received: %s\n", rx_buffer); // 打印接收到的数据
            rx_index = 0;
        }
        HAL_UART_Receive_IT(&huart2, &rx_data, 1); // 重新启动中断接收
    }
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART2_UART_Init();

    // 启动中断接收
    HAL_UART_Receive_IT(&huart2, &rx_data, 1);

    while (1) {
        // 主循环可以执行其他任务
        HAL_Delay(1000);
        printf("STM32 is running...\n"); // 每秒发送一条消息
    }
}

1.2.2 树莓派端编程(Python)

使用Python的pyserial库与STM32通信。

# install: pip install pyserial
import serial
import time

# 配置串口参数
ser = serial.Serial(
    port='/dev/ttyAMA0',  # 对于板载UART
    baudrate=115200,
    bytesize=serial.EIGHTBITS,
    parity=serial.PARITY_NONE,
    stopbits=serial.STOPBITS_ONE,
    timeout=1  # 读取超时时间(秒)
)

if ser.is_open:
    print("串口已打开")
else:
    print("串口打开失败")
    exit()

try:
    while True:
        # 发送数据到STM32
        message = "Hello from Raspberry Pi!\n"
        ser.write(message.encode('utf-8'))
        print(f"Sent: {message.strip()}")

        # 读取STM32的响应
        if ser.in_waiting > 0:
            response = ser.readline().decode('utf-8').strip()
            print(f"Received: {response}")

        time.sleep(1)  # 每秒发送一次

except KeyboardInterrupt:
    print("\n程序被用户中断")
finally:
    ser.close()
    print("串口已关闭")

运行结果

  • STM32会每秒发送”STM32 is running…“,并打印接收到的树莓派消息。
  • 树莓派会每秒发送”Hello from Raspberry Pi!“,并打印接收到的STM32消息。

2. I2C通信

I2C使用两根线(SDA和SCL)实现多设备通信,STM32可以作为主设备或从设备,树莓派通常作为主设备。

2.1 硬件连接

  • STM32端:选择I2C外设,例如I2C1。其引脚通常为:
    • PB6 (I2C1_SCL)
    • PB7 (I2C1_SDA)
  • 树莓派端:使用其板载I2C引脚(GPIO2/3)。
  • 连接方式
    • STM32 PB6 (SCL) -> 树莓派 GPIO3 (SCL)
    • STM32 PB7 (SDA) -> 树莓派 GPIO2 (SDA)
    • STM32 GND -> 树莓派 GND
  • 上拉电阻:I2C总线需要上拉电阻(通常4.7kΩ)连接到3.3V。STM32和树莓派的I2C引脚通常有内部上拉,但为了稳定性,建议外接上拉电阻。

树莓派配置

  • 启用I2C:sudo raspi-config -> Interface Options -> I2C -> Yes
  • 安装I2C工具:sudo apt install i2c-tools
  • 检测设备:sudo i2cdetect -y 1(树莓派4B的I2C总线为1)

2.2 软件编程

2.2.1 STM32端编程(作为从设备)

STM32配置为I2C从设备,地址设为0x30(7位地址)。

// main.c
#include "main.h"
#include <stdio.h>

I2C_HandleTypeDef hi2c1;
uint8_t rx_buffer[100];
uint8_t tx_buffer[] = "STM32 I2C Response";

// I2C从设备地址
#define I2C_SLAVE_ADDR 0x30

// I2C从设备接收回调
void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c) {
    if (hi2c->Instance == I2C1) {
        printf("I2C Received: %s\n", rx_buffer);
        // 可以在这里处理接收到的数据
    }
}

// I2C从设备传输完成回调
void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c) {
    if (hi2c->Instance == I2C1) {
        printf("I2C Transmission Complete\n");
    }
}

// I2C地址匹配回调
void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode) {
    if (hi2c->Instance == I2C1) {
        if (TransferDirection == I2C_DIRECTION_TRANSMIT) {
            // 主设备要写数据到从设备
            HAL_I2C_Slave_Receive_IT(hi2c, rx_buffer, sizeof(rx_buffer));
        } else {
            // 主设备要从从设备读数据
            HAL_I2C_Slave_Transmit_IT(hi2c, tx_buffer, sizeof(tx_buffer));
        }
    }
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_I2C1_Init();

    // 启动I2C从设备监听
    HAL_I2C_EnableListen_IT(&hi2c1);

    while (1) {
        // 主循环
        HAL_Delay(1000);
    }
}

2.2.2 树莓派端编程(作为主设备)

使用Python的smbus2库与STM32通信。

# install: pip install smbus2
import smbus2
import time

# I2C总线号(树莓派4B为1)
BUS = 1
# STM32从设备地址(7位)
DEVICE_ADDR = 0x30

try:
    # 创建I2C总线对象
    bus = smbus2.SMBus(BUS)
    print(f"I2C总线已打开,从设备地址: 0x{DEVICE_ADDR:02X}")

    while True:
        # 向STM32发送数据
        message = "Hello from Raspberry Pi via I2C"
        # 将字符串转换为字节列表
        data = [ord(c) for c in message]
        bus.write_i2c_block_data(DEVICE_ADDR, 0, data)
        print(f"Sent: {message}")

        # 从STM32读取数据
        # 读取16个字节
        received = bus.read_i2c_block_data(DEVICE_ADDR, 0, 16)
        # 将字节列表转换为字符串
        response = ''.join(chr(b) for b in received)
        print(f"Received: {response}")

        time.sleep(1)

except Exception as e:
    print(f"Error: {e}")
finally:
    bus.close()
    print("I2C总线已关闭")

运行结果

  • STM32作为从设备,监听I2C总线。当树莓派发送数据时,STM32会打印接收到的数据。
  • 树莓派作为主设备,每秒向STM32发送数据并读取STM32的响应。

3. SPI通信

SPI是一种高速、全双工的同步通信协议,使用4根线(MOSI, MISO, SCLK, CS)。

3.1 硬件连接

  • STM32端:选择SPI外设,例如SPI1。其引脚通常为:
    • PA5 (SPI1_SCK)
    • PA6 (SPI1_MISO)
    • PA7 (SPI1_MOSI)
    • PA4 (SPI1_CS) 或其他GPIO作为片选
  • 树莓派端:使用其板载SPI引脚(GPIO11/10/9/8)。
  • 连接方式
    • STM32 PA5 (SCK) -> 树莓派 GPIO11 (SCLK)
    • STM32 PA6 (MISO) -> 树莓派 GPIO9 (MISO)
    • STM32 PA7 (MOSI) -> 树莓派 GPIO10 (MOSI)
    • STM32 PA4 (CS) -> 树莓派 GPIO8 (CE0) 或其他GPIO
    • STM32 GND -> 树莓派 GND

树莓派配置

  • 启用SPI:sudo raspi-config -> Interface Options -> SPI -> Yes
  • 安装SPI工具:sudo apt install python3-spidev
  • 检查SPI设备:ls /dev/spidev*

3.2 软件编程

3.2.1 STM32端编程(作为从设备)

STM32配置为SPI从设备,使用中断方式接收和发送数据。

// main.c
#include "main.h"
#include <stdio.h>

SPI_HandleTypeDef hspi1;
uint8_t rx_buffer[100];
uint8_t tx_buffer[] = "STM32 SPI Response";

// SPI从设备接收回调
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
    if (hspi->Instance == SPI1) {
        printf("SPI Received: %s\n", rx_buffer);
        // 可以在这里处理接收到的数据
    }
}

// SPI从设备传输完成回调
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    if (hspi->Instance == SPI1) {
        printf("SPI Transmission Complete\n");
    }
}

// SPI从设备接收和传输完成回调(全双工)
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) {
    if (hspi->Instance == SPI1) {
        printf("SPI Full-Duplex Complete, Received: %s\n", rx_buffer);
    }
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_SPI1_Init();

    // 启动SPI从设备接收(全双工模式)
    HAL_SPI_TransmitReceive_IT(&hspi1, tx_buffer, rx_buffer, sizeof(tx_buffer));

    while (1) {
        // 主循环
        HAL_Delay(1000);
    }
}

3.2.2 树莓派端编程(作为主设备)

使用Python的spidev库与STM32通信。

# install: pip install spidev
import spidev
import time

# 配置SPI参数
spi = spidev.SpiDev()
spi.open(0, 0)  # 总线0,设备0(对应CE0)
spi.max_speed_hz = 1000000  # 1MHz
spi.mode = 0  # SPI模式0(CPOL=0, CPHA=0)

try:
    while True:
        # 发送数据到STM32
        message = "Hello from Raspberry Pi via SPI"
        # 将字符串转换为字节列表
        data = [ord(c) for c in message]
        # 发送并接收数据(全双工)
        received = spi.xfer2(data)  # xfer2会同时发送和接收
        # 将接收到的字节列表转换为字符串
        response = ''.join(chr(b) for b in received)
        print(f"Sent: {message}")
        print(f"Received: {response}")

        time.sleep(1)

except Exception as e:
    print(f"Error: {e}")
finally:
    spi.close()
    print("SPI已关闭")

运行结果

  • STM32作为从设备,通过SPI接收数据并发送响应。
  • 树莓派作为主设备,每秒发送数据并读取STM32的响应。

4. USB通信

STM32可以配置为USB设备(如CDC虚拟串口、HID设备等),树莓派作为USB主机与之通信。

4.1 硬件连接

  • STM32端:使用STM32的USB引脚(通常为PA11/PA12,对应USB_D-/USB_D+)。
  • 树莓派端:使用树莓派的USB-A端口。
  • 连接方式
    • STM32 PA11 (USB_D-) -> USB线的 D-
    • STM32 PA12 (USB_D+) -> USB线的 D+
    • STM32 GND -> USB线的 GND
    • STM32 5V(如果需要供电)-> USB线的 5V(注意:STM32的5V引脚通常来自USB,如果使用外部电源,请确保共地)

注意:STM32的USB引脚需要连接到USB连接器的D+和D-。对于开发板,通常有USB接口。如果使用裸芯片,需要外接USB连接器。

4.2 软件编程

4.2.1 STM32端编程(USB CDC虚拟串口)

使用STM32CubeMX生成USB CDC代码。

// main.c
#include "main.h"
#include "usbd_cdc_if.h"  // USB CDC接口

// USB CDC接收缓冲区
uint8_t usb_rx_buffer[100];
uint32_t usb_rx_length = 0;

// USB CDC接收回调函数(在usbd_cdc_if.c中定义)
// 这里我们假设在usbd_cdc_if.c中已经定义了回调函数,我们只需要在main.c中实现
// 实际上,回调函数通常在usbd_cdc_if.c中定义,但为了示例,我们在这里模拟

// 在usbd_cdc_if.c中,CDC_Receive_FS函数会被调用
// 我们可以在main.c中定义一个全局变量来存储接收到的数据
extern uint8_t UserRxBufferFS[APP_RX_DATA_SIZE];
extern uint32_t UserRxBufferFSLength;

// 在main.c中,我们可以定期检查USB接收状态
int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USB_DEVICE_Init();

    // 启动USB设备
    // USB CDC设备会自动运行

    while (1) {
        // 检查是否有新的USB数据
        if (UserRxBufferFSLength > 0) {
            // 处理接收到的数据
            printf("USB CDC Received: %s\n", UserRxBufferFS);
            // 清空接收缓冲区
            UserRxBufferFSLength = 0;
        }

        // 发送数据到树莓派
        uint8_t message[] = "STM32 USB CDC Response\n";
        CDC_Transmit_FS(message, sizeof(message));

        HAL_Delay(1000);
    }
}

注意:在STM32CubeMX中,需要配置USB为CDC模式,并生成代码。在usbd_cdc_if.c中,CDC_Receive_FS函数会将接收到的数据复制到UserRxBufferFS,并设置UserRxBufferFSLength。我们需要在main.c中定期检查这个长度。

4.2.2 树莓派端编程

树莓派会自动识别STM32的USB CDC设备为一个虚拟串口(如/dev/ttyACM0)。

import serial
import time

# 配置串口参数(与UART类似,但设备不同)
ser = serial.Serial(
    port='/dev/ttyACM0',  # USB CDC设备通常为ttyACM0或ttyUSB0
    baudrate=115200,
    bytesize=serial.EIGHTBITS,
    parity=serial.PARITY_NONE,
    stopbits=serial.STOPBITS_ONE,
    timeout=1
)

if ser.is_open:
    print("USB CDC串口已打开")
else:
    print("USB CDC串口打开失败")
    exit()

try:
    while True:
        # 发送数据到STM32
        message = "Hello from Raspberry Pi via USB CDC\n"
        ser.write(message.encode('utf-8'))
        print(f"Sent: {message.strip()}")

        # 读取STM32的响应
        if ser.in_waiting > 0:
            response = ser.readline().decode('utf-8').strip()
            print(f"Received: {response}")

        time.sleep(1)

except KeyboardInterrupt:
    print("\n程序被用户中断")
finally:
    ser.close()
    print("USB CDC串口已关闭")

运行结果

  • STM32作为USB CDC设备,通过USB线与树莓派通信。
  • 树莓派将STM32识别为虚拟串口,使用串口通信库进行通信。

5. 以太网通信

STM32可以通过内置的以太网MAC(如STM32F407)或外接以太网模块(如W5500)与树莓派进行网络通信。

5.1 硬件连接

5.1.1 STM32内置以太网MAC

  • STM32端:使用STM32F407的以太网MAC引脚(通常为RMII接口):
    • PA1 (ETH_REF_CLK)
    • PA2 (ETH_MDIO)
    • PA7 (ETH_CRS_DV)
    • PB11 (ETH_TX_EN)
    • PB12 (ETH_TXD0)
    • PB13 (ETH_TXD1)
    • PC1 (ETH_MDC)
    • PC4 (ETH_RXD0)
    • PC5 (ETH_RXD1)
  • 树莓派端:使用其以太网端口。
  • 连接方式:需要通过以太网变压器和RJ45接口连接到网络。通常使用一个以太网模块(如LAN8720)连接到STM32,然后通过网线连接到路由器,树莓派也连接到同一个路由器。

5.1.2 使用外接以太网模块(如W5500)

  • STM32端:使用SPI接口连接W5500模块。
  • 连接方式:STM32的SPI引脚连接到W5500的SPI引脚,W5500的以太网接口通过RJ45连接到网络。

5.2 软件编程

5.2.1 STM32端编程(使用LwIP协议栈)

使用STM32CubeMX配置以太网MAC和LwIP协议栈。

// main.c
#include "main.h"
#include "lwip.h"
#include "lwip/udp.h"
#include "lwip/tcp.h"
#include "lwip/netif.h"
#include "lwip/init.h"
#include "lwip/sockets.h"

// UDP服务器配置
#define UDP_PORT 5000
struct udp_pcb *udp_server;

// UDP接收回调
void udp_recv_callback(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) {
    if (p != NULL) {
        // 打印接收到的数据
        printf("UDP Received: %s\n", (char*)p->payload);
        
        // 发送响应
        char response[] = "STM32 UDP Response";
        struct pbuf *resp_p = pbuf_alloc(PBUF_TRANSPORT, strlen(response), PBUF_RAM);
        if (resp_p != NULL) {
            memcpy(resp_p->payload, response, strlen(response));
            udp_sendto(pcb, resp_p, addr, port);
            pbuf_free(resp_p);
        }
        
        pbuf_free(p);
    }
}

// TCP服务器配置
#define TCP_PORT 8080
struct tcp_pcb *tcp_server;

// TCP接收回调
err_t tcp_recv_callback(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) {
    if (p != NULL) {
        // 打印接收到的数据
        printf("TCP Received: %s\n", (char*)p->payload);
        
        // 发送响应
        char response[] = "STM32 TCP Response";
        tcp_write(tpcb, response, strlen(response), TCP_WRITE_FLAG_COPY);
        tcp_output(tpcb);
        
        pbuf_free(p);
    }
    return ERR_OK;
}

// TCP连接回调
err_t tcp_accept_callback(void *arg, struct tcp_pcb *newpcb, err_t err) {
    // 设置接收回调
    tcp_recv(newpcb, tcp_recv_callback);
    return ERR_OK;
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_ETH_Init();
    MX_LWIP_Init();

    // 初始化UDP服务器
    udp_server = udp_new();
    if (udp_server != NULL) {
        udp_bind(udp_server, IP_ADDR_ANY, UDP_PORT);
        udp_recv(udp_server, udp_recv_callback, NULL);
        printf("UDP Server started on port %d\n", UDP_PORT);
    }

    // 初始化TCP服务器
    tcp_server = tcp_new();
    if (tcp_server != NULL) {
        tcp_bind(tcp_server, IP_ADDR_ANY, TCP_PORT);
        tcp_server = tcp_listen(tcp_server);
        tcp_accept(tcp_server, tcp_accept_callback);
        printf("TCP Server started on port %d\n", TCP_PORT);
    }

    while (1) {
        // 处理LwIP协议栈
        MX_LWIP_Process();
        HAL_Delay(10);
    }
}

5.2.2 树莓派端编程(UDP/TCP客户端)

import socket
import time

# UDP客户端
def udp_client():
    # 创建UDP套接字
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server_address = ('192.168.1.100', 5000)  # STM32的IP地址和UDP端口
    
    try:
        while True:
            message = "Hello from Raspberry Pi via UDP"
            sock.sendto(message.encode('utf-8'), server_address)
            print(f"Sent UDP: {message}")
            
            # 设置超时
            sock.settimeout(1.0)
            try:
                data, addr = sock.recvfrom(1024)
                print(f"Received UDP: {data.decode('utf-8')}")
            except socket.timeout:
                print("UDP timeout")
            
            time.sleep(1)
    finally:
        sock.close()

# TCP客户端
def tcp_client():
    # 创建TCP套接字
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = ('192.168.1.100', 8080)  # STM32的IP地址和TCP端口
    
    try:
        sock.connect(server_address)
        print("TCP connected")
        
        while True:
            message = "Hello from Raspberry Pi via TCP"
            sock.sendall(message.encode('utf-8'))
            print(f"Sent TCP: {message}")
            
            # 接收响应
            data = sock.recv(1024)
            print(f"Received TCP: {data.decode('utf-8')}")
            
            time.sleep(1)
    except Exception as e:
        print(f"TCP Error: {e}")
    finally:
        sock.close()

# 选择一种通信方式
if __name__ == "__main__":
    # 运行UDP客户端
    udp_client()
    # 或者运行TCP客户端
    # tcp_client()

运行结果

  • STM32作为UDP/TCP服务器,监听指定端口。
  • 树莓派作为客户端,向STM32发送数据并接收响应。

6. CAN通信

CAN总线常用于汽车和工业环境,具有高可靠性和实时性。STM32通常有内置CAN控制器,树莓派需要外接CAN模块(如MCP2515)。

6.1 硬件连接

  • STM32端:选择CAN外设,例如CAN1。其引脚通常为:
    • PA11 (CAN1_RX)
    • PA12 (CAN1_TX)
  • 树莓派端:使用外接CAN模块(如MCP2515),通过SPI连接到树莓派。
  • 连接方式
    • STM32 PA11 (CAN_RX) -> CAN总线的 CAN_HCAN_L(通过CAN收发器,如TJA1050)
    • STM32 PA12 (CAN_TX) -> CAN总线的 CAN_HCAN_L(通过CAN收发器)
    • 树莓派的CAN模块连接到同一个CAN总线。
    • CAN总线两端需要连接120Ω终端电阻。

6.2 软件编程

6.2.1 STM32端编程(CAN过滤器和中断)

// main.c
#include "main.h"
#include <stdio.h>

CAN_HandleTypeDef hcan1;
CAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[8];
CAN_TxHeaderTypeDef tx_header;
uint8_t tx_data[8];

// CAN接收中断回调
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
    if (hcan->Instance == CAN1) {
        if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data) == HAL_OK) {
            printf("CAN Received: ID=0x%03X, Data=", rx_header.StdId);
            for (int i = 0; i < rx_header.DLC; i++) {
                printf("%02X ", rx_data[i]);
            }
            printf("\n");
            
            // 发送响应(回显)
            tx_header.StdId = 0x123;  // 响应ID
            tx_header.DLC = rx_header.DLC;
            tx_header.IDE = CAN_ID_STD;
            tx_header.RTR = CAN_RTR_DATA;
            memcpy(tx_data, rx_data, rx_header.DLC);
            HAL_CAN_AddTxMessage(&hcan1, &tx_header, tx_data, NULL);
        }
    }
}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_CAN1_Init();

    // 配置CAN过滤器(接收所有ID)
    CAN_FilterTypeDef filter;
    filter.FilterIdHigh = 0x0000;
    filter.FilterIdLow = 0x0000;
    filter.FilterMaskIdHigh = 0x0000;
    filter.FilterMaskIdLow = 0x0000;
    filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
    filter.FilterBank = 0;
    filter.FilterMode = CAN_FILTERMODE_IDMASK;
    filter.FilterScale = CAN_FILTERSCALE_32BIT;
    filter.FilterActivation = CAN_FILTER_ENABLE;
    HAL_CAN_ConfigFilter(&hcan1, &filter);

    // 启动CAN
    HAL_CAN_Start(&hcan1);
    // 启用CAN接收中断
    HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);

    // 发送初始消息
    tx_header.StdId = 0x123;
    tx_header.DLC = 8;
    tx_header.IDE = CAN_ID_STD;
    tx_header.RTR = CAN_RTR_DATA;
    for (int i = 0; i < 8; i++) {
        tx_data[i] = i;
    }
    HAL_CAN_AddTxMessage(&hcan1, &tx_header, tx_data, NULL);

    while (1) {
        // 主循环
        HAL_Delay(1000);
    }
}

6.2.2 树莓派端编程(使用python-can库)

# install: pip install python-can
import can
import time

# 配置CAN接口(假设使用MCP2515模块)
# 需要先安装socketcan或使用python-can的特定后端
# 这里假设使用socketcan,需要先配置树莓派的CAN接口
# 例如:sudo ip link set can0 up type can bitrate 500000

try:
    # 创建CAN总线对象
    bus = can.interface.Bus(channel='can0', bustype='socketcan')
    print("CAN总线已打开")

    # 创建消息
    msg = can.Message(arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7], is_extended_id=False)

    while True:
        # 发送消息
        bus.send(msg)
        print(f"Sent CAN: ID=0x{msg.arbitration_id:03X}, Data={msg.data}")

        # 接收消息(非阻塞)
        try:
            received = bus.recv(timeout=1.0)
            if received:
                print(f"Received CAN: ID=0x{received.arbitration_id:03X}, Data={received.data}")
        except can.CanError as e:
            print(f"CAN Error: {e}")

        time.sleep(1)

except Exception as e:
    print(f"Error: {e}")
finally:
    bus.shutdown()
    print("CAN总线已关闭")

运行结果

  • STM32作为CAN节点,发送和接收CAN消息。
  • 树莓派通过外接CAN模块与STM32通信。

总结

本文详细介绍了STM32与树莓派之间的多种通信方法,包括UART、I2C、SPI、USB、以太网和CAN。每种方法都有其适用场景:

  • UART:简单、直接,适合低速、点对点通信。
  • I2C:适合多设备、中低速通信,硬件连接简单。
  • SPI:高速、全双工,适合与高速外设通信。
  • USB:利用树莓派的USB主机功能,适合需要虚拟串口或HID设备的场景。
  • 以太网:适合网络通信,可以实现远程控制和数据传输。
  • CAN:高可靠性,适合汽车和工业环境。

在实际项目中,可以根据具体需求选择合适的通信方式。例如,如果只需要简单的数据交换,UART是最简单的选择;如果需要连接多个传感器,I2C可能更合适;如果需要高速数据传输,SPI或以太网是更好的选择。

希望本文能帮助你成功实现STM32与树莓派之间的通信。如有任何问题,欢迎进一步讨论。