在上篇文章树莓派GPIO和PWM控制教程中,笔者详细介绍了如何使用树莓进行普通IO控制模拟,以及PWM 波形发送等操作,同时还提到了汽车电子常见的CAN 通讯也能够使用树莓派完成,本文针对此进行详细说明。
使用硬件为树莓派3b+, MCP2515 spiCAN模块 ,总成本控制在300 元以内。使用树莓派4b也可以,不过近期的4b价格离谱,而且仅仅can和io的测试,3b+性能完全够用。
操作系统使用ubuntu server ,如使用Raspberry Pi OS 也可以,本教程也可适用。需要注意的是,不能魔法上网的同学需要首先进行操作系统换源,以及python3 的换源。而其中ubuntu server arm 的系统不能够按照常用的清华源教程替换,需要使用后缀为-ports的源,笔者使用的源如下:
/etc/apt/sources.list
# 默认注释了源码仓库,如有需要可自行取消注释
deb https://mirrors.ustc.edu.cn/ubuntu-ports/ focal main restricted universe multiverse
# deb-src https://mirrors.ustc.edu.cn/ubuntu-ports/ focal main main restricted universe multiverse
deb https://mirrors.ustc.edu.cn/ubuntu-ports/ focal-updates main restricted universe multiverse
# deb-src https://mirrors.ustc.edu.cn/ubuntu-ports/ focal-updates main restricted universe multiverse
deb https://mirrors.ustc.edu.cn/ubuntu-ports/ focal-backports main restricted universe multiverse
# deb-src https://mirrors.ustc.edu.cn/ubuntu-ports/ focal-backports main restricted universe multiverse
deb https://mirrors.ustc.edu.cn/ubuntu-ports/ focal-security main restricted universe multiverse
# deb-src https://mirrors.ustc.edu.cn/ubuntu-ports/ focal-security main restricted universe multiverse
连接MCP2515和树莓派spi接口,并在操作系统中开启spi,整个的运行原理就是让MCP2515的CAN 通讯作为网络通讯接口,挂接到socketCAN 上,使用系统驱动spi,无需手工编写spi驱动以及can wrapper部分。针对socketCAN,网上有很多优秀的开源工具可以使用,笔者这里使用的是cantools ,可以使用dbc进行报文格式解析。
RPi Pin RPi Label CAN Module
02---------5V------------VCC
06---------GND-----------GND
19---------GPIO10--------MOSI (SI)
21---------GPIO9---------MISO (SO)
22---------GPIO25--------INT
23---------GPIO11--------SCK
24---------GPIO8---------CS
安装socket can工具以及cantools工具
sudo apt install can-utils
pip3 install cantools
使能树莓派SPI并加载MCP2515内核驱动
针对Ubuntu server 操作系统,在/boot/firmware/usercfg.txt
文件后添加如下内容,若操作系统为Raspberry Pi OS,则在/boot/config.txt
文件后添加如下内容:
dtparam=spi=on
dtoverlay=mcp2515-can0,oscillator=16000000,interrupt=25
dtoverlay=spi1-1cs
重启 sudo reboot -h now
输入sudo ifconfig -a
指令可以看到已经挂载了网络通讯卡CAN0,如没有ifconfig,则使用sudo apt install net-tools
进行安装
ubuntu@ubuntu:~$ sudo ifconfig -a
can0: flags=193<UP,RUNNING,NOARP> mtu 16
unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 10 (UNSPEC)
RX packets 9343435 bytes 74747480 (74.7 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 8 bytes 64 (64.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
eth0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
ether b8:27:eb:c2:61:84 txqueuelen 1000 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
输入 sudo ip -s -d link show can0
查看can0 通讯是否进入ready状态。
ubuntu@ubuntu:~$ sudo ip -s -d link show can0
3: can0: <NOARP,ECHO> mtu 16 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 10
link/can promiscuity 0 minmtu 0 maxmtu 0
can state STOPPED restart-ms 0
bitrate 1000000 sample-point 0.750
tq 125 prop-seg 2 phase-seg1 3 phase-seg2 2 sjw 1
mcp251x: tseg1 3..16 tseg2 2..8 sjw 1..4 brp 1..64 brp-inc 1
clock 8000000
re-started bus-errors arbit-lost error-warn error-pass bus-off
0 0 0 0 0 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
RX: bytes packets errors dropped overrun mcast
74955120 9369390 0 0 0 0
TX: bytes packets errors dropped carrier collsns
64 8 0 0 0 0
如上步骤如果已经完成,则可进行相关CAN 通讯开发,使用python和c都可以,因为系统支持命令行进行报文发送读取及设定,所以python可以简单的调用系统命令。c的话不建议调用系统命令,而是使用socket接口进行编程。
需注意,由于硬件限制,此方案can通讯波特率最高仅支持500Kbps
关闭can0
sudo ip link set can0 down
设置波特率 500K ,需注意bitrate 需要除2才是常规的通讯波特率
sudo ip link set can0 type can bitrate 1000000
开启can0
sudo ip link set can0 up
查看状态
sudo ip -s -d link show can0
接收报文命令
candump any,0:0,#FFFFFFFF
联合 cantools 使用dbc文件进行报文解码
candump can0 | cantools decode temp.dbc
发送报文命令
cansend can0 123#1122334455667788
设置回环 波特率 250K ,用于测试can通路,在没有其它硬件连接测试的情况下,可以设定成回环,自发自收
sudo ip link set can0 type can bitrate 500000 loopback on
/**
*-----------------------------------------------------------------------------
* @file can_control.c
* @brief
* @author Tomato
* @version 0.1
* @date 2021-07-22
* @note [change history]
*
* @copyright NAAAAA
*-----------------------------------------------------------------------------
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#define command "ip link set can0 type can bitrate 1000000"//将CAN0波特率设置为500K
#define up "ifconfig can0 up"//打开CAN0
#define down "ifconfig can0 down"//关闭CAN0
int can_init()
{
//关闭CAN设备,设置波特率后,重新打开CAN设备
system(down);
system(command);
system(up);
return 0;
}
int can_send(can_frame frame)
{
int s, nbytes;
struct sockaddr_can addr;
struct ifreq ifr;
//创建套接字
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
strcpy(ifr.ifr_name, "can0" );
//指定 can0 设备
ioctl(s, SIOCGIFINDEX, &ifr);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
//将套接字与 can0 绑定
bind(s, (struct sockaddr *)&addr, sizeof(addr));
//发送 frame[0]
nbytes = write(s, &frame, sizeof(frame));
if(nbytes != sizeof(frame))
{
printf("Send Error frame[0]\n!");
}
close(s);
return 0;
}
int can_receive(struct can_frame * r_frame,unsigned int filter_id)
{
int s, nbytes = 0;
struct sockaddr_can addr;
struct ifreq ifr;
struct can_frame frame;
struct can_filter rfilter;
// Initial fram
memset(&frame,0,sizeof(can_frame));
//创建套接字
s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
strcpy(ifr.ifr_name, "can0" );
//指定 can0 设备
ioctl(s, SIOCGIFINDEX, &ifr);
addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
//将套接字与 can0 绑定
bind(s, (struct sockaddr *)&addr, sizeof(addr));
//设置过滤规则,取消当前注释为禁用过滤规则,即不接收所有报文,
// 不设置此项(即如当前代码被注释)为接收所有ID的报文。
if (filter_id != 0)
{
rfilter.can_id = 0x123;
// CAN_EFF_MASK | CAN_SFF_MASK
rfilter.can_mask = CAN_SFF_MASK;
setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
}
while (nbytes == 0)
{
//接收总线上的报文保存在frame中
nbytes = read(s, &frame, sizeof(frame));
}
*r_frame = frame;
#ifdef MSG_DEBUG
printf("the nbytes:%d\n", nbytes);
printf("length:%d", sizeof(frame));
printf("ID=0x%X DLC=%d\n", frame.can_id, frame.can_dlc);
printf("data0=0x%02x\n",frame.data[0]);
printf("data1=0x%02x\n",frame.data[1]);
printf("data2=0x%02x\n",frame.data[2]);
printf("data3=0x%02x\n",frame.data[3]);
printf("data4=0x%02x\n",frame.data[4]);
printf("data5=0x%02x\n",frame.data[5]);
printf("data6=0x%02x\n",frame.data[6]);
printf("data7=0x%02x\n",frame.data[7]);
#endif
return 0;
}
int led_ctl_on(void)
{
struct can_frame frame;
memset(&frame, 0, sizeof(can_frame));
frame.can_id = 0x101;
frame.can_dlc = 8;
frame.data[0] = 1;
can_send(frame);
return 0;
}
int led_ctl_off(void)
{
struct can_frame frame;
memset(&frame, 0, sizeof(can_frame));
frame.can_id = 0x101;
frame.can_dlc = 8;
frame.data[0] = 2;
can_send(frame);
return 0;
}
float can_get_vol(void)
{
float vol_vle = 0;
struct can_frame frame;
memset(&frame, 0, sizeof(can_frame));
// wait until can frame 100 received
can_receive(&frame,0);
printf("###############################\n");
printf("length:%d", sizeof(frame));
printf("ID=0x%X DLC=%d\n", frame.can_id, frame.can_dlc);
printf("data0=0x%02x\n",frame.data[0]);
printf("data1=0x%02x\n",frame.data[1]);
printf("data2=0x%02x\n",frame.data[2]);
printf("data3=0x%02x\n",frame.data[3]);
printf("data4=0x%02x\n",frame.data[4]);
printf("data5=0x%02x\n",frame.data[5]);
printf("data6=0x%02x\n",frame.data[6]);
printf("data7=0x%02x\n",frame.data[7]);
vol_vle = (float)frame.data[0]/50;
return vol_vle;
}
int main(int argc, char* argv[])
{
char control_str[15];
float vol_val = 0;
if (argc < 2) {
printf("can_control service_type\n"
" example: ./can_control led_off/led_on/get_vol\n"
);
return 0;
}
strcpy(control_str,argv[1]);
// debug
printf("Argc : %d\n",argc);
printf("Argv : %s\n , %s\n",argv[0], argv[1]);
// can_init();
if (strcmp(control_str,"led_off")==0)
{
led_ctl_off();
}
else if (strcmp(control_str,"led_on")==0)
{
led_ctl_on();
}
else if (strcmp(control_str,"get_vol")==0)
{
vol_val = can_get_vol();
printf("Voltage is : %5.2f V\n", vol_val);
}
else
{
/* Do nothing */
}
return 0;
}