Skip to content

Commit

Permalink
[FEAT] Add first draft on CAN bus driver
Browse files Browse the repository at this point in the history
WARNING: this is untested
  • Loading branch information
EvanLF6768 committed Jun 16, 2024
1 parent 0d3c28f commit 4f9e8e1
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 34 deletions.
1 change: 1 addition & 0 deletions .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ CheckOptions:
readability-identifier-naming.VariableCase: lower_case
readability-identifier-naming.ConstantCase: CamelCase
readability-identifier-naming.ConstantPrefix: k
readability-identifier-naming.ConstantParameterCase: lower_case
readability-identifier-naming.ConstantIgnoredRegexp: .*V$
readability-identifier-naming.FunctionCase: CamelCase
readability-identifier-naming.FunctionIgnoredRegexp: ^(begin|end)$
Expand Down
1 change: 1 addition & 0 deletions common/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ set(COMMON_HEADERS
${PROJECT_SOURCE_DIR}/Inc/obc/utils/handle.hpp
${PROJECT_SOURCE_DIR}/Inc/obc/utils/meta.hpp
${PROJECT_SOURCE_DIR}/Inc/obc/bus.hpp
${PROJECT_SOURCE_DIR}/Inc/obc/bus/can.hpp
${PROJECT_SOURCE_DIR}/Inc/obc/port.hpp
)

Expand Down
37 changes: 24 additions & 13 deletions common/Inc/obc/bus.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
#include <optional>
#include <span>

#include <units/time.h>

#include "obc/ipc/callback.hpp"
#include "obc/utils/error.hpp"
#include "obc/utils/handle.hpp"
Expand Down Expand Up @@ -108,7 +110,7 @@ concept Buffer = requires(T& buf, V v, std::size_t i) {
*
* Can be used to facilitate trivial deserialization of C-style structs sent on
* a bus.
*
*
* @warning For portability, the struct should be tightly packed (containing
* explicit padding fields if required) with fixed sized integers.
*
Expand All @@ -127,7 +129,7 @@ auto StructAsBuffer(const T& s) -> std::span<const std::byte, sizeof(T)> {
*
* Can be used to facilitate trivial deserialization of C-style structs sent on
* a bus.
*
*
* @warning For portability, the struct should be tightly packed (containing
* explicit padding fields if required) with fixed sized integers.
*
Expand All @@ -149,9 +151,14 @@ auto StructAsBuffer(T& s) -> std::span<std::byte, sizeof(T)> {
* @tparam M The type of message to send.
*/
template<typename T, typename M = BasicMessage>
concept SendBus = requires(T& bus, const M& msg) {
{ bus.Send(msg) } -> std::same_as<void>;
} && Message<M>;
concept SendBus =
requires(T& bus, const M& msg, const units::milliseconds<float> timeout) {
typename T::SendError;

{
bus.Send(msg, timeout)
} -> std::same_as<std::expected<std::monostate, typename T::SendError>>;
} && utils::MaybeError<typename T::SendError> && Message<M>;

/**
* @brief Represents a bus that can listen to all received messages.
Expand Down Expand Up @@ -238,13 +245,14 @@ template<
concept RequestBus =
requires(
T& bus, const Req& req,
ipc::Callback<void, const Res&>::DummyProvider cb, Buf& buf
ipc::Callback<void, const Res&>::DummyProvider cb, Buf& buf,
const units::milliseconds<float> timeout
) {
typename T::RequestHandle;
typename T::RequestError;

{
bus.Request(req, cb.Func, buf)
bus.Request(req, cb.Func, buf, timeout)
} -> std::same_as<
std::expected<typename T::RequestHandle, typename T::RequestError>>;
} &&
Expand Down Expand Up @@ -278,9 +286,10 @@ concept MessageFilter = requires(T& filter, const M& msg) {
template<
typename T, typename Req = BasicMessage, typename Res = BasicMessage,
typename Flt = ipc::Callback<bool, const Res&>>
concept RequestBusMixinRequirements = requires(T& bus, const Req& req) {
{ bus.IssueRequest(req) } -> utils::ExpectedReturn<Flt>;
} && Message<Req> && Message<Res> && MessageFilter<Flt>;
concept RequestBusMixinRequirements =
requires(T& bus, const Req& req, const units::milliseconds<float> timeout) {
{ bus.IssueRequest(req, timeout) } -> utils::ExpectedReturn<Flt>;
} && Message<Req> && Message<Res> && MessageFilter<Flt>;

/**
* @brief A helper class for implementing request functionality.
Expand Down Expand Up @@ -329,11 +338,13 @@ class RequestBusMixin {
* @return An opaque handle that must be retained until the request is
* fulfilled or the response is no longer desired.
*/
auto Request(const Req& req, RequestCallback&& cb, Buf buf)
-> std::expected<RequestHandle, RequestError> {
auto Request(
const Req& req, RequestCallback&& cb, Buf buf,
const units::milliseconds<float> timeout
) -> std::expected<RequestHandle, RequestError> {
return RequestHandle(
m_request_handlers,
{.filter = UNWRAP(Derived().IssueRequest(req, buf)),
{.filter = UNWRAP(Derived().IssueRequest(req, buf, timeout)),
.callback = std::move(cb)}
);
}
Expand Down
171 changes: 171 additions & 0 deletions common/Inc/obc/bus/can.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/* USER CODE BEGIN Header */
/*
* 401 Ballon OBC
* Copyright (C) 2024 Bluesat and contributors.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* USER CODE END Header */

#define FDCAN1
#include <algorithm>
#include <array>
#include <mutex>
#include <optional>
#include <span>
#include <tuple>

#include <stm32h7xx_hal_fdcan.h>
#include <units/time.h>

#include "bus.hpp"
#include "ipc/mutex.hpp"
#include "scheduling/delay.hpp"
#include "scheduling/task.hpp"

namespace obc::bus {
/**
* To prevent this driver from locking up the entire system in an ISR, the
* implementation elects to use a polling approach.
*
* todo(evan): The more efficient approach would be to register an interupt
* which sets the ready flag on this task. This avoids excessive polling.
*/
class CanFd : public scheduling::StackTask<>, bus::ListenBusMixin<> {
public:
static constexpr size_t kMaxPayloadSize = 64;

enum class SendError { kTimeout, kInvalidSize };

inline CanFd(FDCAN_HandleTypeDef* handle) : m_handle {handle} {}

inline auto Send(
const BasicMessage& msg, const units::milliseconds<float> timeout
) -> std::expected<std::monostate, SendError> {
std::lock_guard lock {m_send_lock};

// Wait for a free slot in the message queue
scheduling::Timeout timeout_block(timeout);
if (!timeout_block.Poll([&]() {
return HAL_FDCAN_GetTxFifoFreeLevel(m_handle);
}))
return std::unexpected {SendError::kTimeout};

// Pad the payload so that it fits within a valid DLC size if required
auto [dlc, padding] {
UNWRAP_TAGGED(EncodeDlc(msg.data.size()), SendError::kInvalidSize)
};
// Don't waste time zeroing memory if this buffer is not used
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
std::array<std::byte, kMaxPayloadSize> padded_payload;
uint8_t* payload_ptr {reinterpret_cast<uint8_t*>(
padding ? ({
std::ranges::copy(msg.data, padded_payload.begin());
std::ranges::fill_n(
padded_payload.begin() + msg.data.size(), padding,
std::byte {0}
);
padded_payload.data();
})
: ({ msg.data.data(); })
)};

FDCAN_TxHeaderTypeDef header {
.Identifier {msg.address},
.IdType {FDCAN_EXTENDED_ID},
.TxFrameType {FDCAN_DATA_FRAME},
.DataLength {},
.ErrorStateIndicator {FDCAN_ESI_ACTIVE},
.BitRateSwitch {FDCAN_BRS_ON},
.FDFormat {FDCAN_FD_CAN},
.TxEventFifoControl {FDCAN_STORE_TX_EVENTS},
.MessageMarker {0},
};

// The call can only fail if the queue is full or the can bus is not
// started, both of these are our fault.
utils::CheckOrPanic(
HAL_FDCAN_AddMessageToTxFifoQ(m_handle, &header, payload_ptr), HalOk
);
return {};
}

protected:
inline auto Run() -> void override {
for (const auto& fifo : {FDCAN_RX_FIFO0, FDCAN_RX_FIFO1}) {
// Does this FIFO have any incoming messages?
if (!HAL_FDCAN_GetRxFifoFillLevel(m_handle, fifo)) continue;

FDCAN_RxHeaderTypeDef header {};
std::array<std::byte, kMaxPayloadSize> payload {};
// The call can only fail if the queue is full or the can bus is not
// started, both of these are our fault.
utils::CheckOrPanic(
HAL_FDCAN_GetRxMessage(
m_handle, fifo, &header,
reinterpret_cast<uint8_t*>(payload.data())
),
HalOk
);
FeedListeners(
{.address = header.Identifier,
.data = std::span<std::byte>(
payload.data(),
// Only well formed CAN frames should exist in the FIFO
utils::UnwrapOrPanic(DecodeDlc(header.DataLength))
)}
);
}
}

private:
static constexpr std::array<std::pair<uint32_t, size_t>, 15> kDlcSizeMap {
std::pair { FDCAN_DLC_BYTES_0, 0},
std::pair { FDCAN_DLC_BYTES_1, 1},
std::pair { FDCAN_DLC_BYTES_2, 2},
std::pair { FDCAN_DLC_BYTES_3, 3},
std::pair { FDCAN_DLC_BYTES_4, 4},
std::pair { FDCAN_DLC_BYTES_5, 5},
std::pair { FDCAN_DLC_BYTES_6, 6},
std::pair { FDCAN_DLC_BYTES_7, 7},
std::pair { FDCAN_DLC_BYTES_8, 8},
std::pair {FDCAN_DLC_BYTES_12, 12},
std::pair {FDCAN_DLC_BYTES_16, 16},
std::pair {FDCAN_DLC_BYTES_24, 24},
std::pair {FDCAN_DLC_BYTES_32, 32},
std::pair {FDCAN_DLC_BYTES_48, 48},
std::pair {FDCAN_DLC_BYTES_64, 64},
};

[[nodiscard]] inline static constexpr auto DecodeDlc(uint32_t dlc)
-> std::optional<size_t> {
for (auto& [map_dlc, map_size] : kDlcSizeMap)
if (dlc == map_dlc) return map_size;
return std::nullopt;
}

[[nodiscard]] inline static constexpr auto EncodeDlc(size_t size)
-> std::optional<std::pair<uint32_t, size_t>> {
for (auto& [map_dlc, map_size] : kDlcSizeMap)
if (size <= map_size)
return {
{map_dlc, map_size - size}
};
return std::nullopt;
}

FDCAN_HandleTypeDef* m_handle;
ipc::Mutex m_send_lock {};
};
} // namespace obc::bus
4 changes: 2 additions & 2 deletions common/Inc/obc/scheduling/delay.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ class Timeout : public detail::Timeout {
*/
template<Pollable F>
auto Poll(F& f) -> std::optional<std::remove_reference_t<decltype(*f())>> {
do {
while (!(*this)) {
if (auto x = f()) return *x;
Yield();
} while (!(*this));
}
return std::nullopt;
}

Expand Down
29 changes: 16 additions & 13 deletions common/Inc/obc/scheduling/task.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ namespace obc::scheduling {
*/
class Task {
public:
Task(const Task& other) = delete;
Task(Task&& other) = delete;

auto operator=(const Task& other) -> Task& = delete;
auto operator=(Task&& other) -> Task& = delete;

/**
* @brief Stops the underlying task immediately.
*/
virtual inline ~Task() { vTaskDelete(NULL); }

protected:
// This is interfacing with C-Style FreeRTOS code which uses out
// parameters to initialise values
// NOLINTBEGIN(cppcoreguidelines-pro-type-member-init,hicpp-member-init)
Expand All @@ -55,18 +67,6 @@ class Task {

// NOLINTEND(cppcoreguidelines-pro-type-member-init,hicpp-member-init)

Task(const Task& other) = delete;
Task(Task&& other) = delete;

auto operator=(const Task& other) -> Task& = delete;
auto operator=(Task&& other) -> Task& = delete;

/**
* @brief Stops the underlying task immediately.
*/
virtual inline ~Task() { vTaskDelete(NULL); }

protected:
/**
* @brief The function to be called periodically to execute the task.
*/
Expand Down Expand Up @@ -128,8 +128,11 @@ constexpr std::uint32_t kDefaultStackDepth = 4096;
* @brief Mixin class to statically create a fixed-size stack for a task.
*/
template<std::uint32_t StackDepth = kDefaultStackDepth>
class StackTask {
class StackTask : public Task {
protected:
StackTask() : Task(m_task_stack) = default;

private:
std::array<StackType_t, StackDepth> m_task_stack {};
};
} // namespace obc::scheduling
Loading

0 comments on commit 4f9e8e1

Please sign in to comment.