- PVSM.RU - https://www.pvsm.ru -

Доброго здравия всем!
Сегодня я хочу вам рассказать, как постепенно студенты учатся разрабатывать ПО для микроконтроллера на примере драйвера 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), ...) ;
}
};
Давайте посмотрим, как реализован метод посылки данных. Для начала немного для себя определим спецификацию метода:
Метод должен:
- Запретить прием (мы хотим передавать, и прием нам не нужен)
- Запретить прерывание по приему (также оно нам не нужно при передаче)
- Скопировать данные в буфер передачи
- Установить максимальное значение передаваемых байт в значение, которое передал пользователь
- Записать первый байт в 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();
}
}
Теперь когда передача инициирована, каждый раз, когда байт будет отправлен (из регистра данных в защелку) UART модуль вызовет событие OnTransmit() драйвера UartDriver и нужно будет отослать следующий символ. Собственно это все чем занимается OnTransmit() — отсылает следующий байт.
__forceinline static void OnTransmit()
{
// проверка все ли данные переданы (до защелки)
if(bufferIndex < bufferSize)
{
Uart::WriteByte(txRxBuffer[bufferIndex]) ;
bufferIndex ++ ;
} else
{
//Если все данные переданы, инициируем прерывание по опустошению защелки
//Чтобы убедиться, что последний байт точно вышел в линию
Uart::EnableTcInterrupt() ;
}
};
Как вы знаете у UART обычно есть два события одно по опустошению регистра данных, а второе по опустошению защелки (т.е. реально когда байт вышел в линию), поэтому логично, чтобы убедиться что последний байт вышел в линию, используют прерывание по опустошению защелки.
Метод OnTransmitComplete() должен сделать несколько вещей:
- Сбросить счетчик счетчик количества переданных байт
- Сбросить максимальное количество переданных данных
- Запретить передачу и прерывания по передаче
- Установить статус, что передача завершена
- Оповестить подписчиков на событие
OnTransmitComplete(), которые будут в спискеUartDriverTransmitCompleteObservers
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();
}
Это событие вызывается модулем 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 ;
}
Если вы еще читаете, наверное у вас возник резонный вопрос, что такое 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 здесь [1], никаких доп библиотек не нужно, но нужна плата Nucleo-F411RE, сам проект лежит в папке FlashUartDinamicStand.
Основной код драйвера и Uart модуля лежит на гитхабе [2].
Автор: Сергей
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/stm32/346855
Ссылки в тексте:
[1] примера на IAR8.40.2 здесь: https://yadi.sk/d/zmQ-19BUjx01Zg
[2] гитхабе : https://github.com/lamer0k/CortexLib/tree/master/AbstractHardware/Uart
[3] Источник: https://habr.com/ru/post/488574/?utm_source=habrahabr&utm_medium=rss&utm_campaign=488574
Нажмите здесь для печати.