在为树莓派安装好实时补丁后,它已经能够作为一个高性能控制器来代替传统的单片机或其他嵌入式系统,相比单片机除了计算能力更高外如树莓派4B为例其内部还具有GPU资源可以运行深度学习或OpenCV等单片机中无法运行的库,大大提高了机器人的运算能力和扩展能力,但相比单片机来说其实时性还是稍有欠缺,另外最大的问题的通讯接口的缺乏,如树莓派仅有串口、IIC或SPI等通讯接口,无法实现面向伺服驱动器的通讯控制,这也是如MIT使用UpBoard时为其外扩一块载板来实现CAN通讯的原因,对于这一部分可以使用在MOCO-ML开源四足机器人底盘项目中的主控制器,其以STM32F4作为CPU除了可以直接运行步态控制外也可以作为一个树莓派的载板仅实现扩展CAN和传感器的功能,载板使用SPI2与树莓派进行通讯,具体原理图如下:

1. 远程调试环境搭建(VScode 远程编译)

对于树莓派来说其比较典型的开发方式是直接在上面安装相应的IDE或编译器采用显示屏或远程桌面的方式进行本地编译和运行,但由于其自带的IDE都不是太好用,另外IDE的运行对大量占用树莓派的资源因此这里推荐采用VScode的方式来进行远程编译测试,相比交叉编译的方法这样不需要安装大量的外部软件和复杂的配置,借助VScode强大的编辑功能可以快速实现远程代码调试,同时其易用的IDE风格也能快速调试,能基本实现类似Keil下开发单片机的效果,那首先需要完成VScode在Windows下的安装:

(1)首先完成VScode的安装,具体方法可以参考网络资源:

(2)安装remote development的插件,在插件搜索处输入并在线安装:

(3)连接树莓派配置SSH远程服务器

可以采用WIFI或网线的方式连接树莓派,在获取到其IP后,在VScode新出现的SSH服务控件中点击+号并输入对应IP,注意配置SSH时需要包括对应树莓派的用户名@,否则用户名是本机电脑:

在正确配置后会在SSH TARGETS中显示可以连接的IP地址,需要删除则在Config中删除或修改相关参数进行刷新:

点击连接,并输入树莓派对应登录密码:

正确连接后将打开一个新的VScode界面并且在左下角显示对于IP地址:

(4)配置项目文件夹

在完成连接后如何编译和编辑相关项目,首先需要在树莓派中已经有建立好的Cmake或makefile项目,在VScode最上方点击Open Folder则可以显示树莓派中的文件系统,通过选择项目的更目录地址VScode会自动将其以完整项目的形式在左边浏览界面打开:

当项目配置完成后相比传统使用远程桌面或者文件传输打开的方式,VScode会自动连接相关头文件和索引信息,也就是可以查看到具体的头文件调用关系,这一点非常的有利于代码开发:

对于项目编译来说你可以进一步配置相关gcc编译器,而我还是采用传统在控制台make和run的方式进行开发,相比单片机系统来说其最大的优势是可以在控制台打印,大大加快逻辑程序的调试,但对于相关单步debug的具体方法我目前还没有配置成功:

综上,基于VScode实现远程编译编辑树莓派代码的步骤十分简单,虽然不如交叉编译有完整的IDE环境,但是也比SSH或WinSCP的开发方式好很多,而且基本能满足小项目的开发。

2. 使用SPI实现树莓派与单片机的进行通讯(V1版本)

之前提到为实现树莓派与驱动器的CAN通讯,基于STM32构建了一个载板,实现扩展IMU、遥控器、上位机、数传、GPS的功能,由于数据量大要保证高速传输这里采用SPI接口来实现数据互传,具体的逻辑是树莓派作为主机,单片机作为从机,考虑到数据校验主机按协议发送一帧数据,由于SPI时同步读写的机制单片机在接收中断中马上回传一个字节的数据,而具体发送的字符也是按协议逐个递增,从而实现两边数据的相互传输,通过状态机判断协议帧头再校验完成后进行解码,目前这个机制比较容易编程实现实测在720K频率下能满足1ms一次的数据传输,单片机部分SPI2接口的初始化范例如下:

void SPI2_Init(void)
{	
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef  SPI_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
NVIC_InitTypeDef   NVIC_InitStructure;

GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15; 
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN;//
GPIO_Init(GPIOB, &GPIO_InitStructure);//
	
GPIO_PinAFConfig(GPIOB,GPIO_PinSource13,GPIO_AF_SPI2);
GPIO_PinAFConfig(GPIOB,GPIO_PinSource14,GPIO_AF_SPI2); 
GPIO_PinAFConfig(GPIOB,GPIO_PinSource15,GPIO_AF_SPI2);
 
RCC_APB1PeriphResetCmd(RCC_APB1Periph_SPI2,ENABLE);
RCC_APB1PeriphResetCmd(RCC_APB1Periph_SPI2,DISABLE);
         	
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Slave;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; 
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;    
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;   
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8;  
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;		//设置SPI的数据大小:SPI发送接收8位帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;    // CPOL = 0  PI
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;  // CPHA = 0
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;	
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32;		//定义波特率预分频的值:波特率预分频值为256
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;	//指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
SPI_InitStructure.SPI_CRCPolynomial = 7;	//CRC值计算的多项式
SPI_Init(SPI2, &SPI_InitStructure); 
SPI_Cmd(SPI2, ENABLE);

NVIC_InitStructure.NVIC_IRQChannel 	= SPI2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 	= 0x01;
NVIC_InitStructure.NVIC_IRQChannelSubPriority 		= 0x00;
NVIC_InitStructure.NVIC_IRQChannelCmd 				= ENABLE;
NVIC_Init(&NVIC_InitStructure);
SPI_I2S_ITConfig(SPI2,SPI_I2S_IT_RXNE, ENABLE);	
SPI_Cmd(SPI2, ENABLE); //使能SPI外设
}		
}

在接收中断中首先判断上一组数据是否发送完,在spi_flag_pi[1]置1后可以刷新数据,第二处理接收到的字节,以状态机的方式检查帧头,并完成校验;最后发送一个字节的数据并在成功后将全局计数器加1,当计数器超过发送数组长度表示上一帧数据已经发送完可以刷新对应发送数组spi_tx_buf:(具体而言单片机回传姿态角和角速度,关节角度和力矩;树莓派下发期望角度和力矩前馈,以及使能、复位、关节PD参数等信息)

void SPI2_IRQHandler(void)
{ 
	static char state=0,rx_cnt;
	static int spi_tx_cnt_send=0;
	char sum_r=0;
	int i,j;
	unsigned char  data_temp[8];
	char err,_cnt;
	char id;
	static u8 _data_len2 = 0,_data_cnt2 = 0;
	uint16_t data;
	static int send_flag=0;

	if(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) != RESET)	
	{
		if(spi_flag_pi[1]==1){//data can change
			spi_flag_pi[1]=0;
			spi_dt[0] = Get_Cycle_T(16); 

			if(send_flag==1){
			send_flag=0;
			slave_send(1);
			}//发送数据重新赋值
			else{
			send_flag=1;
			slave_send(2);
			}		
		}						
		data = SPI_I2S_ReceiveData(SPI2);//中断读取SPI数据
	
		if(state==0&&data==0xFA)
		{
			state=1;
			spi_rx_buf[0]=data;
		}
		else if(state==1&&data==0xFF)
		{
			state=2;
			spi_rx_buf[1]=data;
		}
		else if(state==2&&data>0&&data<0XF1)
		{
			state=3;
			spi_rx_buf[2]=data;
		}
		else if(state==3&&data<SPI_BUF_SIZE)
		{
			state = 4;
			spi_rx_buf[3]=data;
			_data_len2 = data;
			_data_cnt2 = 0;
		}
		else if(state==4&&_data_len2>0)
		{
			_data_len2--;
			spi_rx_buf[4+_data_cnt2++]=data;
			if(_data_len2==0)
				state= 5;
		}
		else if(state==5)
		{
			state = 0;
			spi_rx_buf[4+_data_cnt2]=data;
			spi_rx_cnt=4;
			slave_rx(spi_rx_buf,_data_cnt2+5);
			spi_rx_cnt_all++;
		}
		else
			state = 0;

		//同步发送
		SPI2_ReadWriteByte_s(spi_tx_buf[spi_tx_cnt_send++]); 
		if(spi_tx_cnt_send>spi_tx_cnt&&spi_flag_pi[1]==0)//发送完毕可以重新赋值
		{ spi_flag_pi[1]=1;
		  spi_tx_cnt_send=0;
		}
	}
}

校验函数中,首先计算校验码,并依据数据帧ID按协议界面树莓派下发命令:

void slave_rx(u8 *data_buf,u8 num)
{ static u8 cnt[4];
	u8 id;
	vs16 rc_value_temp;
	u8 sum = 0;
	u8 i;
	int anal_cnt=4;
	for( i=0;i<(num-1);i++)
		sum += *(data_buf+i);
	if(!(sum==*(data_buf+num-1)))		return;		//判断sum
	if(!(*(data_buf)==0xFA && *(data_buf+1)==0xFF))		return;		//判断帧头
  if(*(data_buf+2)==1)//
  { 
		spi_dt[1] = Get_Cycle_T(17); 
	  spi_master_loss_pi=0;
		spi_master_connect_pi=1;
		IWDG_Feed();
		for(id=0;id<4;id++){
		robot.Leg[id].tar_sita[0]=floatFromData_spi(spi_rx_buf,&anal_cnt);	
		robot.Leg[id].tar_sita[1]=floatFromData_spi(spi_rx_buf,&anal_cnt);
		leg_motor[id].set_t[0]=floatFromData_spi(spi_rx_buf,&anal_cnt);
		leg_motor[id].set_t[1]=floatFromData_spi(spi_rx_buf,&anal_cnt);
		}
	}
	else if (*(data_buf+2)==2)//
  { 
		spi_dt[2] = Get_Cycle_T(18); 
		spi_master_loss_pi=0;
		spi_master_connect_pi=1;
		IWDG_Feed();
		for(id=0;id<4;id++){
		leg_motor[id].q_reset[0]=floatFromData_spi(spi_rx_buf,&anal_cnt);
		leg_motor[id].q_reset[1]=floatFromData_spi(spi_rx_buf,&anal_cnt);
		}
				
		leg_motor[0].t_to_i[0]=leg_motor[0].t_to_i[1]=floatFromData_spi(spi_rx_buf,&anal_cnt);//电机零偏复位角度
		robot.Leg[0].q_pid.kp=floatFromData_spi(spi_rx_buf,&anal_cnt);
		robot.Leg[0].q_pid.kd=floatFromData_spi(spi_rx_buf,&anal_cnt);				
		leg_motor[0].max_i[0]=leg_motor[0].max_i[1]=charFromData_spi(spi_rx_buf,&anal_cnt);
		leg_motor[0].motor_en=charFromData_spi(spi_rx_buf,&anal_cnt);
		leg_motor[0].reset_q=charFromData_spi(spi_rx_buf,&anal_cnt);

		for(id=1;id<4;id++){
		leg_motor[id].t_to_i[0]=leg_motor[0].t_to_i[0];
		leg_motor[id].t_to_i[1]=leg_motor[0].t_to_i[1];
		robot.Leg[id].q_pid.kp=robot.Leg[0].q_pid.kp;
		robot.Leg[id].q_pid.kd=robot.Leg[0].q_pid.kd;			
		leg_motor[id].max_i[0]=leg_motor[0].max_i[0];
		leg_motor[id].max_i[1]=leg_motor[0].max_i[1];
		leg_motor[id].motor_en=leg_motor[0].motor_en;
		leg_motor[id].reset_q=leg_motor[0].reset_q;
		}
	}
	else if (*(data_buf+2)==99)//
  { 
		spi_dt[2] = Get_Cycle_T(18); 

		test_spi_rx[0]=floatFromData_spi(spi_rx_buf,&anal_cnt);
		test_spi_rx[1]=floatFromData_spi(spi_rx_buf,&anal_cnt);
	}	
}

对于树莓派部分这里参考linux下的SPI接口例程:

int main(int argc, char *argv[])//目前仅能保证上下通讯频率1ms
{
    static int cnt = 0;
    float sys_dt = 0;
    int flag = 0;
    int fd = spi_init();
    Cycle_Time_Init();
    printf("spi fd:%d\n", fd);
    printf("spi mode: %d\n", mode);
    printf("bits per word: %d\n", bits);
    printf("max speed: %d Hz (%d KHz)\n", speed, speed / 1000);

    while (1)
    {
        sys_dt = Get_Cycle_T(0);
        spi_loss_cnt += sys_dt;
        if (spi_loss_cnt > 3)
        {
            spi_loss_cnt = 0;
            spi_connect = 0;
            printf("SPI LOSS!!!\n");
        }
        if (cnt++ > 10)//10ms
        {
            cnt = 0;
            transfer(fd, 2);
        }
        else
            transfer(fd, 1); //1ms
    }
    close(fd);
    return 0;
}

发送函数中首先按协议给tx_buf赋值,并记录发送长度,由于SPI同步传输机制,在发送完后rx_buf中会读取到与发送长度相同的回传数据,此时已状态机的方式重新轮训数组判断帧头和校验,以同样的方式完成校验和解码:

void transfer(int fd, int sel)//发送
{
    static uint8_t state, rx_cnt;
    static uint8_t _data_len2 = 0, _data_cnt2 = 0;
    int ret;
    uint8_t data = 0;

    can_board_send(sel);

    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)spi_tx_buf,
        .rx_buf = (unsigned long)rx,
        .len = spi_tx_cnt,
        .delay_usecs = delay,
        .bits_per_word = bits,
        .cs_change = cs,
    };

    ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr); 

    if (ret < 1)
        pabort("can't send spi message");
    else
    {
        for (int i = 0; i < spi_tx_cnt; i++)
        {
            data = rx[i];
            if (state == 0 && data == 0xFF)
            {
                state = 1;
                spi_rx_buf[0] = data;
            }
            else if (state == 1 && data == 0xFB)
            {
                state = 2;
                spi_rx_buf[1] = data;
            }
            else if (state == 2 && data > 0 && data < 0XF1)
            {
                state = 3;
                spi_rx_buf[2] = data;
            }
            else if (state == 3 && data < SPI_BUF_SIZE)
            {
                state = 4;
                spi_rx_buf[3] = data;
                _data_len2 = data;
                _data_cnt2 = 0;
            }
            else if (state == 4 && _data_len2 > 0)
            {
                _data_len2--;
                spi_rx_buf[4 + _data_cnt2++] = data;
                if (_data_len2 == 0)
                    state = 5;
            }
            else if (state == 5)
            {
                state = 0;
                spi_rx_buf[4 + _data_cnt2] = data;
                spi_rx_cnt = 4;
                slave_rx(spi_rx_buf, _data_cnt2 + 5);
            }
            else
                state = 0;
            //printf("%02x ",rx[i]);
        }
        //printf("\n");
    }
}

SPI接口初始化部分:

int spi_init()
{
    int ret = 0;
    int fd;
    fd = open(device, O_RDWR);
    if (fd < 0)
        pabort("can't open device");
    ret = ioctl(fd, SPI_IOC_WR_MODE, &mode); //дģʽ
    if (ret == -1)
        pabort("can't set spi mode");
    ret = ioctl(fd, SPI_IOC_RD_MODE, &mode); //��ģʽ
    if (ret == -1)
        pabort("can't get spi mode");
    ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits); //д ÿ�ֶ���λ
    if (ret == -1)
        pabort("can't set bits per word");
    ret = ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits); //�� ÿ�ֶ���λ
    if (ret == -1)
        pabort("can't get bits per word");
    ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed); //д �������
    if (ret == -1)
        pabort("can't set max speed hz");
    ret = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed); //�� �������
    if (ret == -1)
        pabort("can't get max speed hz");

    return fd;
}

下面给出了一个将单片机计算姿态角回传并打印显示的效果:

综上,使用SPI实现单片机和树莓派的通讯已经是目前一个比较通用的方案,目前我实现的方式较为粗糙,由于发送接收的不一致会出现误码等BUG,后续还需要进一步完善,另外不知道是单片机还是配置的问题SPI通讯在1.5Mhz以上时会十分容易出错,这也是后续需要进一步优化的部分!https://pic2.zhimg.com/v2-14cf10e4c2d7541807bd8a8fe2ac0496.jpg?source=382ee89a