概述
PX4 中的 uavcan 模块梳理起来是有点杂的,就像 commander 中的内容一个较为杂乱。如果要梳理划分的话,可以按照主题消息的类型来划分,PX4 中的 uavcan 模块需要接收处理的消息主题非常多,因此将其主要分为三类:
1)需要收发的非传感器主题消息
2)只需接收的传感器主题消息
3)服务器消息(用于固件升级)
框图如下:
在 PX4 中的 uavcan 进程中,在 uavcan_main 中创建主节点,并且在创建主节点时,会将所有的主题消息挂载在主节点上,主节点也就是飞控。
服务器消息有一定的条件,即需要根据 UAVCAN_ENABLE 参数设置,大于 1 时,才会开启。固件升级功能需要动态节点 ID 分配,因此在服务器文件 uavcan_servers 中,包含的就是这两个功能,固件升级与动态节点 ID 分配,不过也可以理解为就是一个固件升级功能,因为动态节点 ID 分配是为固件升级服务的。
传感器消息是非常多的,传感器模块都在 uavcan/sensors 目录下,正是由于种类多,因此 PX4 单独为传感器创建了一个管理器 sensor_bridge ,在管理器中统一创建各个传感器的实例,并且创建了一个默认4个通道的列表,管理所有传感器消息的 uORB 主题发布。
在 px4 中的目录对应如下:
主流程
要梳理主流程的脉络的话,对照 DroneCAN 的标准流程就可以。按照 DroneCAN 的流程,创建节点一般需要几个步骤:
1)创建节点,并且在创建节点时需要指定 CAN 底层驱动实例以及系统时钟实例
2)创建消息订阅器与发布器
3)创建进程运行节点
主流程的步骤相比上面的三步要复杂一些,一方面在主循环中进行时间同步的操作,另一方面也有更多的主题消息,不过按照上述的步骤,可以大致梳理出 PX4 的 uavcan 创建节点的主流程步骤:
1)创建节点,指定 CAN 底层实例与系统时钟实例
2)为节点创建各类主题消息
3)创建进程运行节点
4)在主循环中定时进行时间同步
根据上述的主流程步骤,uavcan_main 文件中的主要内容与功能:
1.UavcanNode::start 函数中创建节点,并指定 CAN 底层实例与系统时钟实例;
2.UavcanNode::UavcanNode 构造函数中,会创建需要收发的主题消息各控制器并挂在节点上;
3.UavcanNode::init 函数中:
1)设置节点名称与 ID 基础信息
2)初始化需要收发的各消息控制器(在构造函数中已经创建,此时只初始化)
3)创建传感器主题消息链表(由于传感器较多,所以使用一个链表来节省资源)
4)获取 UAVCAN_ENABLE 参数设置,大于1时,启动服务器,用于固件升级
4.UavcanNode::Run() 函数中进行线程创建,时间同步,参数获取等
在构造函数中,就可以看出需要收发的主题消息控制器有哪些,这些消息,在PX4中是跟着构造函数一起创建的:
在初始化函数 init 中,会设置节点名称与 ID :
/*******************设置节点名称与ID 基础信息*******************/
_node.setName("org.pixhawk.pixhawk"); //设置节点名称
_node.setNodeID(node_id); //设置节点ID
fill_node_info();
之后创建主题消息控制器:
/********************初始化各个其他模块的主题消息控制器***********************/
ret = _beep_controller.init(); //蜂鸣器控制器if (ret < 0) {return ret;
}// Actuators
ret = _esc_controller.init(); //电调控制器if (ret < 0) {return ret;
}
...
而传感器消息太多,因此传感器消息由一个管理器类 IUavcanSensorBridge,初始化函数中只需要调用此管理器类的一个成员函数 make_all 即可完成全部的创建:
// Sensor bridges
//创建所有传感器的消息链表(由于传感器太多,因此创建一个链表节省资源)
IUavcanSensorBridge::make_all(_node, _sensor_bridges);
传感器类主题消息的内容在下面的内容详细叙述,在此先跳过。
服务器的创建需要条件,需要根据 UAVCAN_ENABLE 参数:
/*获取 UAVCAN_ENABLE 参数设置,一共有0,1,2,3四种情况,大于1时就需要开启服务节点来支持升级*/
if (uavcan_enable > 1) {_servers = new UavcanServers(_node, _node_info_retriever);if (_servers) {int rv = _servers->init();if (rv < 0) {PX4_ERR("UavcanServers init: %d", ret);}}
}
UAVCAN_ENABLE 参数的定义如下:
0 - UAVCAN 禁用
1 - UAVCAN 对传感器支持,但没有动态节点ID分配与固件升级
2 - UAVCAN 对传感器支持,同时也支持动态节点ID分配与固件升级
3 - UAVCAN 对传感器支持,同时支持动态节点ID分配与固件升级,并且将电机控制信号也用UAVCAN输出
时间同步与固件升级
时间同步的流程可以参照《DroneCAN的实现库Libuavcan及基础功能示例》一文中的示例程序。主要需要注意的地方是主机的冗余机制: 如果当前模块是主机的话,会同时再创建一个从机运行,当检测到网络中有更高优先级的主机时,当前节点会启用从机 (suppress(false)),之后与新主机进行同步:
/** 检查网络中是否又更高优先级的主机* 如果有的话,激活本地的从机节点,当前模块由主机变从机,与新主机同步*/
if (_time_sync_slave.isActive()) { // "Active" 表示从设备网络中至少有一个远程主机if (_node.getNodeID() < _time_sync_slave.getMasterNodeID()) {/** 当前节点是优先级最高的主机节点 调用 suppress 函数,压制从机模式*/_time_sync_slave.suppress(true); // SUPPRESS} else {/** 网络中存在更高优先级的主机,因此取消压制从机,允许从机调整本地时钟*/_time_sync_slave.suppress(false); // UNSUPPRESS}} else {/** 当网络中没有主机时,必须压制从机,本主机是唯一时钟源*/_time_sync_slave.suppress(true);
}/** 发布时间同步主题消息*/
_time_sync_master.publish();
固件升级的流程也同样可以参照《DroneCAN的实现库Libuavcan及基础功能示例》一文中的示例程序。
需要收发的主题消息流程
此部分也是两种类型,不过大体上的类成员函数框架都是复制的。伪代码如下:
类名::类名(uavcan::INode &node) :要发布的主题消息(node),要订阅的主题消息(node),_timer(node) //软件定时器
{要发布的消息.setPriority(uavcan::TransferPriority::Default); //设置优先级
}int 类名::init()
{//启动要订阅的主题消息的回调函数int res = 要订阅的消息.start(StatusCbBinder(this, &类名::回调函数名));if (res < 0) {return res;} // 设置软件定时器的周期与回调函数,用于定时广播主题消息if (!_timer.isRunning()) {_timer.setCallback(TimerCbBinder(this, &UavcanSafetyState::periodic_update));_timer.startPeriodic(uavcan::MonotonicDuration::fromMSec(1000 / MAX_RATE_HZ));}return 0;
}//周期更新状态,然后发布消息
void 类名::广播回调函数(const uavcan::TimerEvent &)
{更新消息(void)要发布的主题消息.broadcast(cmd);
}//处理接受到的主题消息
void 类名::订阅回调函数()
{
}
如果是仅仅广播消息,不接收消息的控制器的话,例如安全开关,就不需要订阅,其代码也基本上是上述的模板套上去即可:
1.构造函数中创建主题消息并设置优先级:
2.初始化函数中设置软件定时器的周期与回调函数:
3.回调函数中更新数据并调用 broadcast 库函数来发送消息:
如果是订阅与发布都需要的话,例如电调,则在初始化过程中会再多增加一个启动主题消息的订阅,并指定回调函数:
传感器类主题消息流程
传感器类的主题消息控制器,在 PX4 中,又创建了一个管理器来管理这么多的传感器消息控制器,即 sensor_bridge。在 sensor_bridge 中,创建了一个列表,在 make_all 函数中,将创建的传感器对象都添加到列表中:
不过更重要的是,在 sensor_bridge 中,创建了一个默认为4的通道:
_channels(new uavcan_bridge::Channel[max_channels]),
由于传感器种类太多,PX4 不仅要接收传感器发送的主题数据,同时在接收之后,还要根据数据更新相应的 uORB 主题消息,因此 PX4 使用这 4 个通道来发布相应的 uORB 主题消息,避免同时发布的数据太多,节省资源,在 publish 函数中实现:
当通道中有 ID 与要发布的消息的传感器 ID 相同时,优先选择此通道,如果没有,则需按照空闲通道,当有空闲通道时,则发布 uORB 主题消息,如果没有空闲通道,则退出函数,暂不发布数据。
其余的单传感器的消息控制器的框架与上面一节的框架是一样的,只是没有广播消息,只订阅消息,例如电池,也是在构造函数中创建消息类型:
UavcanBatteryBridge::UavcanBatteryBridge(uavcan::INode &node) :UavcanSensorBridgeBase("uavcan_battery", ORB_ID(battery_status)),ModuleParams(nullptr),_sub_battery(node),_sub_battery_aux(node),_warning(battery_status_s::BATTERY_WARNING_NONE),_last_timestamp(0)
{
}
在初始化函数中,开启消息订阅,并指定回调函数:
int UavcanBatteryBridge::init()
{/*启动主题消息订阅器,并指定其回调函数*/int res = _sub_battery.start(BatteryInfoCbBinder(this, &UavcanBatteryBridge::battery_sub_cb));if (res < 0) {PX4_ERR("failed to start uavcan sub: %d", res);return res;}...
在回调函数中,更新数据,并且调用 sensor_bridge 的发布函数来发布 uORB 主题:
if (battery_aux_support[instance] == false) {publish(msg.getSrcNodeID().get(), &battery_status[instance]);
}
px4自身id与消息订阅使能
px4自身的id与各类外设的消息是否订阅的使能,在uavcan_param.c中,如果要修改飞控的id,以及是否订阅相关外设的消息,调节参数即可,或者在QGC中修改: