Доброго здравия всем!
Сегодня я хочу вам рассказать, как постепенно студенты учатся разрабатывать ПО для микроконтроллера на примере драйвера UART на STM32F411. Код и архитектуру с небольшими моими изменениями и доработками я попытаюсь привести здесь.
Сразу отмечу, что все сделано статикой, как я учил :) (статические классы, статическая подписка, статический странно-рекурсивный шаблон, статический контейнер для команд и так далее), но можно реализовать тоже самое с помощью обычных объектов и обычного подхода. В обычном подходе архитектура была бы такая же, но кода немного больше, как по объему так и по количеству строк кода.
Данная статья не претендует на истину, а лишь показывает подход к реализации некоторых задач, в данном случае реализацию Uart драйвера на С++.
Идея
Итак была поставлена следующая задача:
- Драйвер должен уметь работать в асинхронном режиме
- Драйвер должен уметь отсылать заданной количество байт
- Драйвер должен уметь принимать заданное количество байт
- Драйвер должен вызывать событие по завершению передачи и по завершению чтения заданного количества данных
- Драйвер должен вызывать событие по приеме каждого байта
Общая архитектура
Картинка ниже поясняет назначение драйвера, чтобы иметь представление что такое вообще драйвер UART в данном контексте.
В общем случае — это слой не зависящий от аппаратной реализации UART, предоставляющий очень абстрактный интерфейс для приема, передачи данных.
Архитектура драйвера
Поскольку драйвер должен предоставлять не так много функций, его архитектуру очень упрощенно можно представить в таком виде:
У драйвера есть два метода:
WriteData(const uint8_t *pData, uint8_t size)
— для посылки заданного числа байтовReadData(uint8_t size)
— для приема заданного числа данных
А также события:
OnTransmit()
— вызывается UART модулем при передаче одного символаOnTransmitComplete()
— вызывается UART модулем при окончании передачиOnReceive()
— вызывается UART модулем при приеме одного символа
Драйвер будет иметь списки статических подписчиков. Всего 2 списка:
UartDriverTransmitCompleteObservers
— список содержит подписчиков на событиеOnTransmitComplete()
и просто вызывает у всех своих подписчиков методOnTransmitComplete()
template<typename ...Observers>
struct UartDriverTransmitCompleteObservers
{
__forceinline static void OnWriteComplete()
{
(Observers::OnTransmitComplete(), ...) ;
}
};
UartDriverReceiveObservers
— список содержит подписчиков на событиеOnReceiveComplete()
и просто вызывает у всех своих подписчиков методOnReceiveComplete()
template<typename ...Observers>
struct UartDriverReceiveCompleteObservers
{
__forceinline static void OnReadComplete(tBuffer& buffer, std::size_t bytesReceived)
{
(Observers::OnReceiveComplete(buffer, bytesReceived), ...) ;
}
};
Реализация
Метод WriteData()
Давайте посмотрим, как реализован метод посылки данных. Для начала немного для себя определим спецификацию метода:
Метод должен:
- Запретить прием (мы хотим передавать, и прием нам не нужен)
- Запретить прерывание по приему (также оно нам не нужно при передаче)
- Скопировать данные в буфер передачи
- Установить максимальное значение передаваемых байт в значение, которое передал пользователь
- Записать первый байт в UART
- Установить счетчик количества переданных байт в 1, так как один байт уже передали
- Инициировать передачу
Теперь можно это перевести в код:
static void WriteData(const std::uint8_t *pData, std::uint8_t bytesTosend)
{
assert(bytesTosend < txRxBuffer.size()) ;
// Проверим, что мы не находимся в режиме или записи.
// т.е. что предыдущие данные либо приняты либо уже отосланы
if ((status != Status::Write) && (status != Status::Read)) {
const CriticalSection cs;
Uart::DisableReceive();
Uart::DisableRxInterrupt();
bufferIndex = 0U;
bufferSize = bytesTosend;
std::memcpy(txRxBuffer.data(), pData, static_cast<std::size_t>(bytesTosend));
Uart::WriteByte(txRxBuffer[bufferIndex]);
bufferIndex++;
//устанавливаем режим передачи, что происходит передача
status = Status::Write;
Uart::StartTransmit();
}
}
Событие OnTransmit()
Теперь когда передача инициирована, каждый раз, когда байт будет отправлен (из регистра данных в защелку) UART модуль вызовет событие OnTransmit()
драйвера UartDriver
и нужно будет отослать следующий символ. Собственно это все чем занимается OnTransmit()
— отсылает следующий байт.
__forceinline static void OnTransmit()
{
// проверка все ли данные переданы (до защелки)
if(bufferIndex < bufferSize)
{
Uart::WriteByte(txRxBuffer[bufferIndex]) ;
bufferIndex ++ ;
} else
{
//Если все данные переданы, инициируем прерывание по опустошению защелки
//Чтобы убедиться, что последний байт точно вышел в линию
Uart::EnableTcInterrupt() ;
}
};
Событие OnTransmitComplete()
Как вы знаете у UART обычно есть два события одно по опустошению регистра данных, а второе по опустошению защелки (т.е. реально когда байт вышел в линию), поэтому логично, чтобы убедиться что последний байт вышел в линию, используют прерывание по опустошению защелки.
Метод OnTransmitComplete() должен сделать несколько вещей:
- Сбросить счетчик счетчик количества переданных байт
- Сбросить максимальное количество переданных данных
- Запретить передачу и прерывания по передаче
- Установить статус, что передача завершена
- Оповестить подписчиков на событие
OnTransmitComplete()
, которые будут в спискеUartDriverTransmitCompleteObservers
static void OnTransmitComplete() {
bufferIndex = 0U;
bufferSize = 0U;
Uart::DisableTcInterrupt();
Uart::DisableTxInterrupt() ;
Uart::DisableTransmit();
status = Status::WriteComplete;
// оповещаем подписчиков о том, что передача завершена
UartDriverTransmitCompleteObservers::OnWriteComplete() ;
}
Метод ReadData()
Тоже самое для метода чтение данных. Метод должен:
- Запретить передачу
- Запретить прерывания по передаче
- Установить максимальное значение принимаемых байт в значение, которое передал пользователь
- Обнулить счетчик количества принятых байт
- Инициировать прием
Смотрим код:
static auto ReadData(std::uint8_t size) {
assert(size < txRxBuffer.size()) ;
// Проверим, что мы не находимся в режиме или записи.
// т.е. что предыдущие данные либо приняты либо уже отосланы
if ((status != Status::Write) && (status != Status::Read))
{
const CriticalSection cs;
Uart::DisableTcInterrupt();
Uart::DisableTxInterrupt();
Uart::DisableTransmit();
bufferIndex = 0U;
bufferSize = size;
//устанавливаем режим приема, что происходит прием
status = Status::Read;
Uart::EnableReceive();
Uart::EnableRxInterrupt();
}
Событие OnReceive()
Это событие вызывается модулем UART каждый раз, как был принят байт. Необходимо считать количество принятых байт и как только оно станет количеству запрашиваемых пользователем, нужно закончить прием и оповестить подписчиков, что прием закончен.
static void OnReceive()
{
txRxBuffer[bufferIndex] = Uart::ReadByte() ;
bufferIndex ++ ;
if (bufferIndex == bufferSize)
{
status = Status::ReadComplete ;
UartDriverReceiveObservers::OnReadComplete(txRxBuffer, bufferIndex) ;
}
}
Весь код драйвера
Код драйвера полностью можно посмотреть под спойлером
#ifndef REGISTERS_UARTDRIVER_HPP
#define REGISTERS_UARTDRIVER_HPP
#include "susudefs.hpp" //for __forceinline
#include "hardwareuarttx.hpp" // for HardwareUartTx
#include "hardwareuarttc.hpp" //for HardwareUartTc
#include "hardwareuartrx.hpp" // for HardwareUartRx
#include <cstring> // for memcpy
#include "criticalsectionconfig.hpp" // for CriticalSection
#include "uartdriverconfig.hpp" // for tBuffer
template<typename UartModule, typename UartDriverTransmitCompleteObservers, typename UartDriverReceiveObservers>
struct UartDriver
{
using Uart = UartModule ;
enum class Status: std::uint8_t
{
None = 0,
Write = 1,
WriteComplete = 2,
Read = 3,
ReadComplete = 4
} ;
static void WriteData(const std::uint8_t *pData, std::uint8_t bytesTosend)
{
assert(bytesTosend < txRxBuffer.size()) ;
if ((status != Status::Write) && (status != Status::Read))
{
const CriticalSection cs;
Uart::DisableReceive();
Uart::DisableRxInterrupt();
bufferIndex = 0U;
bufferSize = bytesTosend;
std::memcpy(txRxBuffer.data(), pData, static_cast<std::size_t>(bytesTosend));
Uart::WriteByte(txRxBuffer[bufferIndex]);
bufferIndex++;
status = Status::Write;
Uart::StartTransmit();
//если работает без прерываний, то посылаем прямо тут
if constexpr (!std::is_base_of<UartTxInterruptable, typename Uart::Base>::value)
{
for(; bufferIndex < bytesTosend; ++bufferIndex)
{
while (!Uart::IsDataRegisterEmpty())
{
}
Uart::WriteByte(txRxBuffer[bufferIndex]);
}
while (!Uart::IsTransmitComplete())
{
}
//Proceed() ;
status = Status::WriteComplete ;
UartDriverTransmitCompleteObservers::OnWriteComplete() ;
} else
{
}
}
}
__forceinline static void OnTransmit()
{
if(bufferIndex < bufferSize)
{
Uart::WriteByte(txRxBuffer[bufferIndex]) ;
bufferIndex ++ ;
}
else
{
Uart::EnableTcInterrupt() ;
}
};
static void OnTransmitComplete()
{
bufferIndex = 0U;
bufferSize = 0U;
Uart::DisableTcInterrupt();
Uart::DisableTxInterrupt() ;
Uart::DisableTransmit();
status = Status::WriteComplete;
UartDriverTransmitCompleteObservers::OnWriteComplete() ;
}
static auto ReadData(std::uint8_t size)
{
assert(size < txRxBuffer.size()) ;
if ((status != Status::Write) && (status != Status::Read))
{
const CriticalSection cs;
Uart::DisableTcInterrupt();
Uart::DisableTxInterrupt();
Uart::DisableTransmit();
bufferIndex = 0U;
bufferSize = size;
status = Status::Read;
Uart::EnableReceive();
Uart::EnableRxInterrupt();
}
}
static void OnReceive()
{
txRxBuffer[bufferIndex] = Uart::ReadByte() ;
bufferIndex ++ ;
if (bufferIndex == bufferSize)
{
status = Status::ReadComplete ;
UartDriverReceiveObservers::OnReadComplete(txRxBuffer, static_cast<std::size_t>(bufferIndex)) ;
}
}
static Status GetStatus()
{
return status ;
}
static void ResetAll()
{
Uart::DisableTcInterrupt();
Uart::DisableTxInterrupt();
Uart::DisableTransmit();
Uart::DisableReceive();
Uart::DisableRxInterrupt() ;
bufferIndex = 0U;
bufferSize = 0U;
status = Status::None;
}
private:
inline static tBuffer txRxBuffer = {} ;
inline static std::uint8_t bufferSize = 0U ;
inline static std::uint8_t bufferIndex = 0U ;
inline static Status status = Status::None ;
};
#endif //REGISTERS_UARTDRIVER_HPP
Как этим пользоваться?
Например мы хотим реализовать очень простенький протокол SomeProtocol
, который всегда принимает 10 байт и отсылает 10 байт. Нулевой байт — это команда, последний это контрольная сумма. А данных 8 байт. т.е. окончание приема будем определять по количеству принятых байт, если 10, то посылка закончилась. (По хорошему так делать не надо, окончание посылки лучше делать по таймеру, но чтобы не плодить кода, я упростил до такого вот супер пупер протокола)
Все что нам нужно будет сделать это реализовать два метода OnTransmitComplete()
и OnReceiveComplete()
.
template <typename UartDriver>
struct SomeProtocol
{
__forceinline static void OnTransmitComplete()
{
//снова ожидаем приема 10 байт;
Proceed() ;
}
__forceinline static void OnReceiveComplete(tBuffer& buffer, std::size_t length)
{
// Примем завершен, разбираем посылку
assert(length <= buffer.size()) ;
//Надо проверить контрольну сумму, если не совпала скидываем протокол
if (CheckData(buffer))
{
//Команда лежит по 0 индексу буфера. Обрабатываем команду
// вообще хорошо бы тут выйти из прерывания. Т.е. оповестить задачу, что мол
// мол все пришло, обработуй команду... но упростим все и обработаем команду в
// в прерывании.
cmds::ProceedCmd(buffer[0], buffer); // команда заполнять буфер ответом.
//Отсылаем ответ
UartDriver::WriteData(buffer.data(), length) ;
} else
{
UartDriver::ResetAll() ;
}
}
__forceinline static void Proceed()
{
//Запрашиваем чтение по 10 байту.
UartDriver::ReadData(10) ;
}
//контейнер для команд
using cmds = CmdContainer<
Singleton<CmdWriteSomeData>::GetInstance(),
Singleton<CmdReadSomeData>::GetInstance()
> ;
};
// Просто еще подписчик на завершение передачи, например хотим моргнуть светодиодом
struct TestObserver
{
__forceinline static void OnTransmitComplete()
{
Led1::Toggle() ;
}
};
Теперь нужно произвести настройку драйвера подписать протокол на UartDriver
struct MyUartDriver: UartDriver<
//Это аппаратный модуль UART
HardwareUart,
// Подписываем SomeProtocol и TestObserver на событие OnTransmitComplete()
UartDriverTransmitCompleteObservers<SomeProtocol<MyUartDriver>, TestObserver>,
// Подписываем только SomeProtocol на событие OnReceiveComplete()
UartDriverReceiveCompleteObservers<SomeProtocol<MyUartDriver>>
> { };
using MyProtocol = SomeProtocol<MyUartDriver> ;
Заметьте, можно сделать сколь угодно много подписчиков, на завершение приема или передачи. Например, на завершение передачи я подписал два класса TestObserver
и SomeProtocol
, а на завершение приема только один — SomeProtocol
. Также можно настроить драйвер на любой UART модуль.
и теперь можно запускать протокол на работу:
int main()
{
//Запуск стека протокола
MyProtocol::Proceed() ;
while(true) { }
return 1 ;
}
UART модуль
Если вы еще читаете, наверное у вас возник резонный вопрос, что такое HardwareUart
UART модуль и откуда он взялся. Его упрощенная модель выглядит так:
По большому счету — это обертка над аппаратным UART микроконтроллера, в которую через список подключаются 3 дополнительных класса для обработки прерываний:
HardwareUartTx
— класс для обработки прерывания по опустошению регистра данных, содержащий список подписчиков, подписанных на это прерываниеHardwareUartTc
— класс для обработки прерывания по опустошению защелки, содержащий список подписчиков, подписанных на это прерываниеHardwareUartRx
— класс для обработки прерывания по приходу байта, содержащий список подписчиков, подписанных на это прерывание
Обработчики прерывания вызываются из метода HandleInterrupt()
класса HardwareUartBase
, который должен подставляеться в таблицу векторов прерываний
template<typename... Modules>
struct InterruptsList
{
__forceinline static void OnInterrupt()
{
//вызываем обработчики прерывания у подписчиков
(Modules::HandleInterrupt(), ...) ;
}
} ;
template<typename UartModule, typename InterruptsList>
struct HardwareUartBase
{
static void HandleInterrupt()
{
//обычно в списке HardwareUartTx, HardwareUartTc, HardwareUartRx и
// здесь вызываются их обработчики
InterruptsList::OnInterrupt() ;
}
...
} ;
template<typename UartModule, typename UartTransmitObservers>
struct HardwareUartTx
{
using Uart = typename UartModule::Uart ;
static void HandleInterrupt()
{
//Проверяем случилось ли прерывание по опустошению регистра данных
if(Uart::SR::TXE::DataRegisterEmpty::IsSet() &&
Uart::CR1::TXEIE::InterruptWhenTXE::IsSet())
{
UartTransmitObservers::OnTxDataRegEmpty();
}
}
};
template<typename UartModule, typename UartReceiveObservers>
struct HardwareUartRx
{
using Uart = typename UartModule::Uart ;
static void HandleInterrupt()
{
//Проверяем случилось ли прерывание по приему байта
if(Uart::CR1::RXNEIE::InterruptWhenRXNE::IsSet() &&
Uart::SR::RXNE::DataRecieved::IsSet() )
{
UartReceiveObservers::OnRxData();
}
}
};
template<typename UartModule, typename UartTransmitCompleteObservers>
struct HardwareUartTc
{
using Uart = typename UartModule::Uart ;
static void HandleInterrupt()
{
//Проверяем случилось ли прерывание по опустошению защелки
if(Uart::SR::TC::TransmitionComplete::IsSet() &&
Uart::CR1::TCIE::InterruptWhenTC::IsSet())
{
UartTransmitCompleteObservers::OnComplete();
Uart::SR::TC::TransmitionNotComplete::Set() ;
}
}
};
#ifndef REGISTERS_UART_HPP
#define REGISTERS_UART_HPP
#include "susudefs.hpp" //for __forceinline
#include <array> // for std::array
#include <cassert> // for assert
#include <cstring> // for memcpy
#include "criticalsectionguard.hpp" //for criticalsectionguard
template<typename UartModule, typename InterruptsList>
struct HardwareUartBase
{
using Uart = UartModule ;
using Base = Interface ;
__forceinline static void EnableTransmit()
{
UartModule::CR1::TE::Enable::Set();
};
static void DisableTransmit()
{
UartModule::CR1::TE::Disable::Set();
};
static void EnableReceive()
{
UartModule::CR1::RE::Enable::Set();
};
static void DisableReceive()
{
UartModule::CR1::RE::Disable::Set();
};
static void EnableTxInterrupt()
{
UartModule::CR1::TXEIE::InterruptWhenTXE::Set();
};
static void EnableRxInterrupt()
{
UartModule::CR1::RXNEIE::InterruptWhenRXNE::Set();
};
static void DisableRxInterrupt()
{
UartModule::CR1::RXNEIE::InterruptInhibited::Set();
};
static void DisableTxInterrupt()
{
UartModule::CR1::TXEIE::InterruptInhibited::Set();
};
static void EnableTcInterrupt()
{
UartModule::CR1::TCIE::InterruptWhenTC::Set();
};
static void DisableTcInterrupt()
{
UartModule::CR1::TCIE::InterruptInhibited::Set();
};
static void HandleInterrupt()
{
InterruptsList::OnInterrupt() ;
}
__forceinline static void ClearStatus()
{
UartModule::SR::Write(0);
}
static void WriteByte(std::uint8_t chByte)
{
UartModule::DR::Write(static_cast<std::uint32_t>(chByte)) ;
}
static std::uint8_t ReadByte()
{
return static_cast<std::uint8_t>(UartModule::DR::Get()) ;
}
static void StartTransmit()
{
EnableTransmit() ;
if constexpr (std::is_base_of<UartTxInterruptable, Interface>::value)
{
EnableTxInterrupt() ;
}
}
static bool IsDataRegisterEmpty()
{
return UartModule::SR::TXE::DataRegisterEmpty::IsSet() ;
}
static bool IsTransmitComplete()
{
return UartModule::SR::TC::TransmitionComplete::IsSet() ;
}
};
#endif //REGISTERS_UART_HPP
Настройка UART и подписчиков будет выглядеть так:
struct HardwareUart : HardwareUartBase<
USART2,
InterruptsList<
//Хотим использовать прерывание по опустошению регистра данных
HardwareUartTx<HardwareUart,
//Подписываем драйвер на прерывание по опустошению регистра данных Uart
UartTransmitObservers<MyUartDriver>>,
//Хотим использовать прерывание по опустошению защелки
HardwareUartTc<HardwareUart,
//Подписываем драйвер на прерывание по опустошению защелки
UartTransmitCompleteObservers<MyUartDriver>>,
//Хотим использовать прерывание по приему байта данных
HardwareUartRx<HardwareUart,
//Подписываем драйвер на прерывание по приему байта данных
UartReceiveObservers<MyUartDriver>>
>
>
{
};
Теперь легко можно подписывать на разные прерывания разных клиентов. Количество клиентов практически не ограничено, в данном случае мы подписали на все три прерывания UartDriver, но могли бы еще что-нить подписать. Также можно подключить к любому UART, в примере подключено к USART2.
Затем настроенный Uart модуль уже можно передавать в драйвер, как было показано чуть выше. Драйвер же также в свою очередь должен подписаться на события от Uart модуля.
struct MyUartDriver: UartDriver<
//Это аппаратный модуль UART
HardwareUart,
// Подписываем SomeProtocol и TestObserver на событие OnTransmitComplete()
UartDriverTransmitCompleteObservers<SomeProtocol<MyUartDriver>, TestObserver>,
// Подписываем только SomeProtocol на событие OnReceiveComplete()
UartDriverReceiveCompleteObservers<SomeProtocol<MyUartDriver>>
> { };
using MyProtocol = SomeProtocol<MyUartDriver> ;
Заключение
В общем и целом перед поставленной задачей товарищи студенты справились.
При включенном принудительном inline, весь код проекта занимает 1500 байт без оптимизации. Сделаны две команды, записи и чтения 12 параметров во Flash микроконтроллера. В проекте, можно настроить драйвер, чтобы он работал в синхронном режиме, можно в асинхронном. Можно подключать любое количество подписчиков, к любому UART. Собственно все задачи были выполнены и работа тянет на отлично :)
Да, затрачено на кодирование было 2 целых дня (думаю часов 20 в сумме). Думаю, из-за того, что архитектура мною уже была разработана на практических занятиях, а реализация — это дело уже не таке сложное.
Код был проверен мною в PVS-Studio. Изначально были найдены 4 предупреждения.
Все не уже не помню, отчет не сохранил: но точно были V2516 и V519, ошибки не критичные, но точно так делать не надо было :) Все исправлено, кроме V2516, он указывает на код, который используется для отладки, там поставил FIXME:.
Можно посмотреть код рабочего примера на IAR8.40.2 здесь, никаких доп библиотек не нужно, но нужна плата Nucleo-F411RE, сам проект лежит в папке FlashUartDinamicStand.
Основной код драйвера и Uart модуля лежит на гитхабе .
Автор: Сергей