UORB

轉載地址:http://blog.arm.so/armteg/pixhawk/183-0503.html


Pixhawk 飛控系統是基於ARM的四軸以上飛行器的飛行控制器, 它的前身是PX4-IMU,Pixhawk 把之前的IMU進行了完整的重構,最新版本是2.4.3。而對應的Pixhawk 1.x版本與2.x版本的區別在於,I/O板與FMU是否整合在一起。

uORB是Pixhawk系統中非常重要且關鍵的一個模塊,它肩負了整個系統的數據傳輸任務,所有的傳感器數據、GPS、PPM信號等都要從芯片獲取後通過uORB進行傳輸到各個模塊進行計算處理。

 

uORB 的架構簡述

uORB全稱爲micro object request broker (uORB),即 微對象請求代理器,實際上uORB是一套跨進程的IPC通訊模塊。在Pixhawk中, 所有的功能被獨立以進程模塊爲單位進行實現並工作。而進程間的數據交互就由爲重要,必須要能夠符合實時、有序的特點。

 

Pixhawk 使用NuttX實時ARM系統, 而uORB對於NuttX而言,它僅僅是一個普通的文件設備對象,這個設備支持Open、Close、Read、Write、Ioctl以及Poll機制。 通過這些接口的實現,uORB提供了一套“點對多”的跨進程廣播通訊機制, “點”指的是通訊消息的“源”,“多”指的是一個源可以有多個用戶來接收、處理。而“源”與“用戶”的關係在於,源不需要去考慮用戶是否可以收到某條被廣播的消息或什麼時候收到這條消息。它只需要單純的把要廣播的數據推送到uORB的消息“總線”上。對於用戶而言,源推送了多少次的消息也不重要,重要的是取回最新的這條消息。

 

uORB實際上是多個進程打開同一個設備文件,進程間通過此文件節點進行數據交互和共享。

 

uORB 的系統實現

uORB的實現位於固件源碼的src/modules/uORB/uORB.cpp文件,它通過重載CDev基類來組織一個uORB的設備實例。並且完成Read/Write等功能的重載。uORB 的入口點是uorb_main函數,在這裏它檢查uORB的啓動參數來完成對應的功能,uORB支持start/test/status這3條啓動參數,在Pixhawk的rcS啓動腳本中,使用start參數來進行初始化,其他2個參數分別用來進行uORB功能的自檢和列出uORB的當前狀態。

在rcS中使用start參數啓動uORB後,uORB會創建並初始化它的設備實例, 其中的實現大部分都在CDev基類完成。這個過程類似於Linux設備驅動中的Probe函數,或者Windows 內核的DriverEntry,通過init調用完成設備的創建,節點註冊以及派遣例程的設置等。

 

uORB 源碼分析之Open

    uORB 的Open接口實現了“源”或“用戶” 打開uORB句柄的功能,打開uORB的句柄就意味着一個源的創建或把一個用戶關聯到某個源。我在這以源的創建爲開端,逐步講解Open的過程:

orb_advert_t orb_advertise(const struct orb_metadata *meta, const void *data) {     int result, fd;     orb_advert_t advertiser;     /* open the node as an advertiser */     fd = node_open(PUBSUB, meta, data, true);     if (fd == ERROR)         return ERROR;     /* get the advertiser handle and close the node */     result = ioctl(fd, ORBIOCGADVERTISER, (unsigned long)&advertiser);     close(fd);     if (result == ERROR)         return ERROR;     /* the advertiser must perform an initial publish to initialise the object */     result= orb_publish(meta, advertiser, data);     if (result == ERROR)         return ERROR;     return advertiser; }

orb_advertise 其實就是一個int,  meta是一個已定義好的源描述信息,裏面就2個成員,分別爲name以及size。保存了通訊的名稱以及每次發送數據的長度。 創建源的過程爲3個步驟, 打開uORB的設備節點, 獲取設備實例, 推送第一條消息。

 

	/*
	 * Generate the path to the node and try to open it.
	 */
	ret = node_mkpath(path, f, meta);

	if (ret != OK) {
		errno = -ret;
		return ERROR;
	}

	/* open the path as either the advertiser or the subscriber */
	fd = open(path, (advertiser) ? O_WRONLY : O_RDONLY);

從代碼中可以看出, 每個源都在/PUBSUB/目錄下有一個設備節點。首先通過node_mkpath來拼接好設備節點路徑,然後根據要打開的是源節點還是用戶節點來選擇標識

 

 

int
ORBDevNode::open(struct file *filp)
{
	int ret;

	/* is this a publisher? */
	if (filp->f_oflags == O_WRONLY) {

		/* become the publisher if we can */
		lock();

		if (_publisher == 0) {
			_publisher = getpid();
			ret = OK;

		} else {
			ret = -EBUSY;
		}

		unlock();

		/* now complete the open */
		if (ret == OK) {
			ret = CDev::open(filp);

			/* open failed - not the publisher anymore */
			if (ret != OK)
				_publisher = 0;
		}

		return ret;
	}

	/* is this a new subscriber? */
	if (filp->f_oflags == O_RDONLY) {

		/* allocate subscriber data */
		SubscriberData *sd = new SubscriberData;

		if (nullptr == sd)
			return -ENOMEM;

		memset(sd, 0, sizeof(*sd));

		/* default to no pending update */
		sd->generation = _generation;

		filp->f_priv = (void *)sd;

		ret = CDev::open(filp);

		if (ret != OK)
			free(sd);

		return ret;
	}

	/* can only be pub or sub, not both */
	return -EINVAL;
}

uORB中規定了源節點只允許寫打開,用戶節點只允許只讀打開。 我認爲上面的Open代碼裏lock到unlock那段根本就不需要~ 那裏僅僅是判斷不允許重複創建同一個話題而已。而去重完全可以依賴其他的一些機制來解決, CDev::Open就不在繼續往裏說了。~如果oflags是RDONLY,那就表示要打開的是一個用戶設備節點,sd是爲這個用戶準備的一個上下文結構。裏面包含了一些同步計數器等信息,比如sd->generation,這裏保存了當前用戶讀取到的消息的索引號,而_generation來源於源設備的每次寫操作,每次源寫入數據時,_generation會累加。每次用戶讀取數據時會把_generation同步到自己的sd->generation,通過這種處理,如果當前用戶的sd->generation不等於全局的_generation就意味着源剛剛寫入過數據,有最新的通訊消息可以供讀取。

 

uORB設備的讀和寫

 

ssize_t
ORBDevNode::read(struct file *filp, char *buffer, size_t buflen)
{
	SubscriberData *sd = (SubscriberData *)filp_to_sd(filp);

	/* if the object has not been written yet, return zero */
	if (_data == nullptr)
		return 0;

	/* if the caller's buffer is the wrong size, that's an error */
	if (buflen != _meta->o_size)
		return -EIO;

	/*
	 * Perform an atomic copy & state update
	 */
	irqstate_t flags = irqsave();

	/* if the caller doesn't want the data, don't give it to them */
	if (nullptr != buffer)
		memcpy(buffer, _data, _meta->o_size);

	/* track the last generation that the file has seen */
	sd->generation = _generation;

	/*
	 * Clear the flag that indicates that an update has been reported, as
	 * we have just collected it.
	 */
	sd->update_reported = false;

	irqrestore(flags);

	return _meta->o_size;
}

 

讀分爲3步, 首先判斷參數是否合理,然後屏蔽中斷拷貝數據,最後更新同步信息。值得注意的是,如果沒有源寫數據,那麼read會在第一個判斷就退出,原因是_data緩衝區在首次write時纔會成功申請。generation的同步這裏也不在繼續說了

 

ssize_t
ORBDevNode::write(struct file *filp, const char *buffer, size_t buflen)
{
	/*
	 * Writes are legal from interrupt context as long as the
	 * object has already been initialised from thread context.
	 *
	 * Writes outside interrupt context will allocate the object
	 * if it has not yet been allocated.
	 *
	 * Note that filp will usually be NULL.
	 */
	if (nullptr == _data) {
		if (!up_interrupt_context()) {

			lock();

			/* re-check size */
			if (nullptr == _data)
				_data = new uint8_t[_meta->o_size];

			unlock();
		}

		/* failed or could not allocate */
		if (nullptr == _data)
			return -ENOMEM;
	}

	/* If write size does not match, that is an error */
	if (_meta->o_size != buflen)
		return -EIO;

	/* Perform an atomic copy. */
	irqstate_t flags = irqsave();
	memcpy(_data, buffer, _meta->o_size);
	irqrestore(flags);

	/* update the timestamp and generation count */
	_last_update = hrt_absolute_time();
	_generation++;

	/* notify any poll waiters */
	poll_notify(POLLIN);

	return _meta->o_size;
}

上面就是write的實現了,那個lock/unlock真心很雞肋,我是感覺多餘了,首次write會申請內存用於數據通訊, 然後關閉中斷拷貝數據防止在複製的過程用有用戶來read,最後是更新最後的更新時間以及同步計數器並且發送一個POLLIN的消息通知來喚醒那些還在等待uORB數據可讀的用戶

 

 

uORB設備的POLL狀態

當用戶沒有指定數據讀取的頻率時,每次源的write都會觸發一個POLLIN來喚醒用戶去讀取剛更新的數據。是否喚醒除了檢查generation的值以外,另外一個要求就是讀取頻率的限制,每個用戶可以單獨指定自己打算讀更新的頻率。

 

bool
ORBDevNode::appears_updated(SubscriberData *sd)
{
	/* assume it doesn't look updated */
	bool ret = false;

	/* avoid racing between interrupt and non-interrupt context calls */
	irqstate_t state = irqsave();

	/* check if this topic has been published yet, if not bail out */
	if (_data == nullptr) {
		ret = false;
		goto out;
	}

	/*
	 * If the subscriber's generation count matches the update generation
	 * count, there has been no update from their perspective; if they
	 * don't match then we might have a visible update.
	 */
	while (sd->generation != _generation) {

		/*
		 * Handle non-rate-limited subscribers.
		 */
		if (sd->update_interval == 0) {
			ret = true;
			break;
		}

		/*
		 * If we have previously told the subscriber that there is data,
		 * and they have not yet collected it, continue to tell them
		 * that there has been an update.  This mimics the non-rate-limited
		 * behaviour where checking / polling continues to report an update
		 * until the topic is read.
		 */
		if (sd->update_reported) {
			ret = true;
			break;
		}

		/*
		 * If the interval timer is still running, the topic should not
		 * appear updated, even though at this point we know that it has.
		 * We have previously been through here, so the subscriber
		 * must have collected the update we reported, otherwise
		 * update_reported would still be true.
		 */
		if (!hrt_called(&sd->update_call))
			break;

		/*
		 * Make sure that we don't consider the topic to be updated again
		 * until the interval has passed once more by restarting the interval
		 * timer and thereby re-scheduling a poll notification at that time.
		 */
		hrt_call_after(&sd->update_call,
			       sd->update_interval,
			       &ORBDevNode::update_deferred_trampoline,
			       (void *)this);

		/*
		 * Remember that we have told the subscriber that there is data.
		 */
		sd->update_reported = true;
		ret = true;

		break;
	}

out:
	irqrestore(state);

	/* consider it updated */
	return ret;
}

uORB 根據用戶指定的週期來設置hrt(實時定時器),每過一個時間間隔,hrt會被髮生調用,通過hrt_called來檢查這個調用。如果未發生,即便此時源的數據已經更新那麼也不會返回POLLIN來喚醒用戶去讀。 簡單來說,它通過控制POLLIN的週期來單方面控制用戶的讀取間隔。 如果是linux平臺,當用戶指定了時間間隔後, 我會爲它單獨初始化一個內核定時器,每次poll調用時檢查完可用更新後,再次檢查定時器即可。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章