Студенты пишут Uart драйвер для STM32F411 +13






Доброго здравия всем!


Сегодня я хочу вам рассказать, как постепенно студенты учатся разрабатывать ПО для микроконтроллера на примере драйвера 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()

Так выглядит UartDriverTransmitCompleteObservers
template<typename ...Observers>
struct UartDriverTransmitCompleteObservers
{
  __forceinline static void OnWriteComplete() 
  {
    (Observers::OnTransmitComplete(), ...) ;
  }
};

  • UartDriverReceiveObservers — список содержит подписчиков на событие OnReceiveComplete()и просто вызывает у всех своих подписчиков метод OnReceiveComplete()

Так выглядит UartDriverReceiveObservers
template<typename ...Observers>
struct UartDriverReceiveCompleteObservers
{
  __forceinline static void OnReadComplete(tBuffer& buffer, std::size_t bytesReceived)
  {
    (Observers::OnReceiveComplete(buffer, bytesReceived), ...) ;
  }
};

Подписаться на драйвер может хоть кто, у кого есть метод OnReceiveComplete() или
OnTransmitComplete(). На картинке в качестве примера показаны два подписчика SomeProtocol и OtherObserver.


Реализация


Метод WriteData()


Давайте посмотрим, как реализован метод посылки данных. Для начала немного для себя определим спецификацию метода:


Метод должен:
  • Не должны обрабатывать прием (мы хотим передавать, у нас один буфер (для экономии), мы не можем одновременно и принимать и передавать)
  • Скопировать данные в буфер передачи
  • Установить максимальное значение передаваемых байт в значение, которое передал пользователь
  • Записать первый байт в UART
  • Установить счетчик количества переданных байт в 1, так как один байт уже передали
  • Инициировать передачу

Теперь можно это перевести в код:


static void WriteData(const std::uint8_t *pData, std::uint8_t bytesTosend)
  {
    assert(bytesTosend < txRxBuffer.size()) ;
    const CriticalSection cs;
   // Проверим, что мы не находимся в режиме или записи. 
   // т.е. что предыдущие данные либо приняты либо уже отосланы
    if ((status != Status::Write) && (status != Status::Read))    
    {

      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()) ;
    const CriticalSection cs;
   //  Проверим, что мы не находимся в режиме чтения или записи. 
   // т.е. что предыдущие данные либо приняты либо уже отосланы
    if ((status != Status::Write) && (status != Status::Read))
    {
      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 ;
      const auto length = bufferIndex ;
      bufferIndex  = 0U;   

      UartDriverReceiveObservers::OnReadComplete(txRxBuffer, bufferIndex) ;
    }
  }

Весь код драйвера


Код драйвера полностью можно посмотреть под спойлером


uartdriver.hpp
#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()) ;
    const CriticalSection cs;
    if ((status != Status::Write) && (status != Status::Read))
    {

      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())
        {

        }

        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; 

    status = Status::WriteComplete;
    Uart::DisableTcInterrupt();
    Uart::DisableTxInterrupt() ;
    Uart::DisableTransmit();

    UartDriverTransmitCompleteObservers::OnWriteComplete() ; 

  }

  static auto ReadData(std::uint8_t size)
  {
    assert(size < txRxBuffer.size()) ;
    const CriticalSection cs;
    if ((status != Status::Write) && (status != Status::Read))
    {
      Uart::DisableTcInterrupt();
      Uart::DisableTxInterrupt();
      Uart::DisableTransmit();

      bufferIndex = 0U;      
      bufferSize = size;
      status = Status::Read;

      Uart::EnableRxInterrupt();
      Uart::EnableReceive();

    }

  }

  static void OnReceive()
  {
    txRxBuffer[bufferIndex] = Uart::ReadByte() ;
    bufferIndex ++ ;
    if (bufferIndex == bufferSize)
    {   
      status = Status::ReadComplete ;
      const auto length = bufferIndex ;
      bufferIndex = 0 ;
      UartDriverReceiveObservers::OnReadComplete(txRxBuffer, static_cast<std::size_t>(length)) ;      
    }
  }

  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;    
  }

  friend UartDriver& operator<<(UartDriver &rOs, const char* pString)
  {
    WriteData(reinterpret_cast<const std::uint8_t*>(pString), strlen(pString)) ;
    return rOs;
  }

  friend UartDriver& operator<<(UartDriver &rOs, float value)
  {
    WriteData(reinterpret_cast<const std::uint8_t*>(&value), sizeof(float)) ;
    return rOs;
  }

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() ;
  }
 ...
} ;

Классы HardwareUartTx , HardwareUartRx, HardwareUartTx
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() ;
    }
  }
};

Полный класс HardwareUartBase
#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, весь код проекта занимает 1600 байт без оптимизации. Сделаны две команды: запись и чтение 12 параметров во Flash микроконтроллера. В проекте, можно настроить драйвер, чтобы он работал в синхронном режиме, можно в асинхронном. Можно подключать любое количество подписчиков, к любому UART. Собственно все задачи были выполнены и работа тянет на отлично :)


Да, затрачено на кодирование было 2 целых дня (думаю часов 20 в сумме). Полагаю, из-за того, что архитектура мною уже была разработана на практических занятиях, а реализация — это дело уже не таке сложное.


Код был проверен мною в PVS-Studio. Изначально были найдены 4 предупреждения.
Все предупреждения уже не помню, отчет не сохранил: но точно были V2516 и V519, ошибки не критичные, но точно так делать не надо было :) Все исправлено, кроме V2516, он указывает на код, который используется для отладки, там поставил FIXME:.


Можно посмотреть код рабочего примера на IAR8.40.2 здесь, никаких доп библиотек не нужно, но нужна плата Nucleo-F411RE, сам проект лежит в папке FlashUart\DinamicStand.


Основной код драйвера и Uart модуля лежит на гитхабе .


PS: Спасибо fougasse, apro, gleb_l и besitzeruf за дельные замечания

Вы можете помочь и перевести немного средств на развитие сайта

Теги:



Комментарии (48):

  1. fougasse
    /#21284688

    ReadData и критические секции — разве оно так будет работать (я про выделение на стеке)?

    • lamerok
      /#21284734

      А почему нет, внутри фигурных скобок, области видимости, за ними объект уничтожается с вызовом деструктора.

      • apro
        /#21285334

        Потому что объект синхронизации должен с кем-то разделяться иначе его использование бессмысленно. А здесь видно что каждый вызов использует свой уникальный объект, то есть бессмыслица какая-то. Если только конечно внутри конструктора CriticalSection нет обращение к какой-то глобальной сущности.

        • lamerok
          /#21285542

          Соглашусь, справедливости ради, скорее всего критическая здесь была добавлена чтобы обеспечить атомарности того, что в скобках (не совсем правда понятно зачем). Логично было бы вызвать его до проверки, тогда хоть какой-то смысл будет, поскольку операция ставит статус Read. Поправлю.

      • fougasse
        /#21285680

        С кем синхронизация происходит в вашем случае?

        • lamerok
          /#21285814

          По идее тут нужна защита для установки статуса и его проверки, чтобы, если вдруг (чисто теоретический случай) в одном потоке вызывается ReadData()и делается проверка статуса, драйвер находится не в режиме Read или Write, здесь другой поток высоко-приоритетный тоже вызывает ReadData() и тоже делает проверку и тоже определяет, что находится не в режиме Read или Write, начинает менять ресурсы (BufferSize, BufferIndex и так далее)… потом возвращаемся в другой поток, и тот также меняет их, но уже под себя. Поэтому нужно, чтобы установка статуса и его проверка были атомарными.


          Это конечно так себе случай, но все сделано верно, тем более, что она особо есть не просит.

          • fougasse
            /#21286260

            Я еще раз спрошу, с кем вы синхронизируетесь локальной переменной в функции?
            Вы, в принципе, понимаете, что так в многопоточной системе работать не будет?

            • lamerok
              /#21286270

              С другим потоком, если ему вдруг вздумается вызывать ReadData(), он недолжен снова инициировать чтение своим запросом на ReadData(), если запрос ReadData() уже инициируется другим потоком.
              Да понимаю.

              • fougasse
                /#21286292

                Если понимаете, зачем тогда писать?
                Очень странный, как мне кажется, подход к обучению.

                • lamerok
                  /#21287246

                  В предыдущем комментарии, "Да понимаю", следует воспринимать, как "не понял вопроса", почему не будет работать?
                  Я действительно не понял вопроса...

            • EighthMayer
              /#21286894

              Рискну предположить что, во-первых, автор неправильно понял вопрос, а во вторых что, упрощенно говоря, класс критической секции в конструкторе (при создании объекта) выключает прерывания, а в деструкторе (при выходе объекта из области видимости) включает. Если так, то что именно будет не работать в многопоточной системе?

              • lamerok
                /#21286920 / +1

                Так и есть… практически
                в конструкторе


                s = __get_interrupt_state();
                __disable_interrupt();

                В деструкторе


                __set_interrupt_state(s);

  2. x893
    /#21284740 / +1

    Имя на github рассмешило.

  3. gleb_l
    /#21284846 / +3

    Странно, что метод отправки запрещает приём. В отличие от передачи, данные на приём могут нам свалиться в любой момент, и рассчитывать на то, что внешний мир замрёт, как только нам захочется что-то передать, нельзя

    • lamerok
      /#21284904

      В драйвер буфер приёма и передачи один, кроме того, регистр данных тоже один у микроконтроллера. Поэтому нет смысла принимать что — то во время передачи.
      И прерывание на весь модуль UART одно… внутри идёт проверка флагов, если оба флага(на приём и передачу) стоят, то вызовутся оба обработчика одновременно.
      Чтобы не зависеть от этого, сделан вот такой драйвер, преплогающий, что одновременно передачи и приёма быть не может. Зато он не зависит от аппаратуры вообще.

      • iliasam
        /#21284962 / +1

        Регистр данных USART_DR действительно один, но про него в даташите сказано:

        The Data register performs a double function (read and write) since it is composed of two registers, one for transmission (TDR) and one for reception (RDR)
        The TDR register provides the parallel interface between the internal bus and the output shift
        register (see Figure 1).
        The RDR register provides the parallel interface between the input shift register and the
        internal bus.

        И про сам модуль UASRT в даташите сказано:
        The universal synchronous asynchronous receiver transmitter (USART) offers a flexible means of full-duplex data exchange

        так что передача и прием одновременно возможны.

        • lamerok
          /#21285066

          Я не отрицаю факт одновременно передачи и записи… Просто в таком случае обработка будет несколько сложнее, как минимум нужны два буфера приём в и передачи.

        • besitzeruf
          /#21285296 / +1

          Регистров USART_DR физически 2. Доступ к одному из них осуществляется исключительно чтением по адресу USAR_DR, доступ к другому исключительно записью. В вашей же цитате из даташита:

          The Data register performs a double function (read and write) since it is composed of two registers, one for transmission (TDR) and one for reception (RDR)

          • lamerok
            /#21285558

            Да, точно. Таким образом можно все порешать введя отдельные буферы на прием и передачу.

      • gleb_l
        /#21284978 / +2

        Поэтому нет смысла принимать что — то во время передачи

        Поймите же, что события внешнего мира абсолютно асинхронны по отношению к тому, что происходит внутри вашего МК — и если в силу каких-то аппаратных или программных ограничений вашей системы она неспособна на это реагировать — значит, она спроектирована неправильно.

        Вы можете сказать «сейчас я буду передавать 10 байт» — и передать 10 байт синхронно в текущем потоке выполнения, но вы не можете сказать «сейчас я буду передавать 10 байт, все заткнитесь!» или «сейчас я буду ПРИНИМАТЬ 10 байт» — так как нет никаких оснований надеяться, что ваш респондент действительно прекратит передачу, даже если магическим образом узнает о вашем желании передать пакет, либо будет готов вам предоставить данные (к тому же нужной вам длины), как только вам захочется их принять — так как по факту передающая сторона с вами не синхронизирована и не в курсе ваших намерений. Представьте, что на той стороне — человек за консолью с клавиатурой и дисплеем — и он совершенно не обязан отвечать сообщениями фиксированной длины как только вы ему выплюнули на дисплей свой чанк данных.

        Поэтому полноценный драйвер а) должен быть асинхронным на прием, б) дуплексным, в) не ожидать на приемной стороне данные фиксированной длины. Иначе вы либо рискуете потерять принятый пакет или его часть, либо подвесите свой тред в ожидании приема, либо не сможете нормально отреагировать на пакет данных нестандартной длины или формата (например, вследствие ошибки канала)

        • lamerok
          /#21285096

          ОК.


          драйвер а) должен быть асинхронным на прием, б) дуплексным, в) не ожидать на приемной стороне данные фиксированной длины.

          На приём он ассинхронный, и только в момент передачи приём отключён, так как буфер приёма передачи один.
          Скажем так это ограничение, но оно преодалимо, путем введения второго буфера.
          С дуплексом тоже самое, да здесь сделано не фулдуплекс… но такой задачи не стояло перед студентами.
          Драйвер не ожидает фиксированной длины… Драйвер принимает все, что ему приходит. Он оповещает клиента о том, что пришло столько, сколько он запросил. А клиент может запрашивать и по 1 байту столько раз, сколько ему нужно. Пусть клиент и разбирается с окончанием приёма.
          Про это я вообще написал, мои комментарии:


          . (По хорошему так делать не надо, окончание посылки лучше делать по таймеру, но чтобы не плодить кода, я упростил до такого вот супер пупер протокола)
          // вообще хорошо бы тут выйти из прерывания. Т.е. оповестить задачу, что мол
          // все пришло, обработуй команду… но упростим все и обработаем команду
          // в прерывании

  4. IronHead
    /#21285052

    f411, uart на ++, без dma, запрещающий прием во время передачи
    а вы точно сможете научить студентов чему то толковому?

    • lamerok
      /#21285114

      411 потому что есть на кафедре, но это не важно, он хорошо подходит подо все, можно хоть что в принципе, ++ потому что проще, надёжнее и компактнее. DMA нет потому что расскажите мне как, например, следить за временем между байтами (например в link layerе полевого протокола HART), передавать Ок ещё можно, но принимать все равно прерывание нужно для пересброса таймера после приёма каждого байта.
      Запрещающий приём, уже пояснил, так как буфер приёма передачи один. Не все микроконтроллеры имеют 16 КБ ОЗУ, иногда приходится урезаться. И иметь два буфера, для того же HART, а это по 255 байт на каждый буфер, вообще не хорошо.
      И да точно.

      • mmMike
        /#21285568

        DMA нет потому что расскажите мне как, например, следить за временем между байтами (например в link layerе полевого протокола HART),

        1. Почему в статье так и не написано, что это драйвер не универсальный, а для HART?
        2. Какая необходимость следить за временем в рамках одного пакета HART? Что конкретно мешает их через DMA передавать?

        на завершение передачи я подписал два класса TestObserverи SomeProtocol
        А смысл? Если это одноядерный микроконтроллер (не типа ESP32), и без RTOS, то все равно однопоточно.
        Если "универсально" (что не бывает), что где обкладка WriteData? Или надеемся на авось и что вызова из прерывания/другого ядру, например, этой функции не будет?

        • lamerok
          /#21285596

          Про HART просто пример, что есть протоколы, где нужно следить за приемом каждого байта. Мое мнение, через прерывание — самый универсальный способ, DMA прикрутить не проблема, такой задачи не было, но за одним байтом я не знаю как следить в таком случае. HART — протокол, который на кафедре преподают, поэтому его и взял в качестве примера.


          HART отлично ложиться, ввиду того, что модемы через UART работают, и там да нужно следить за каждым байтом.


          Что то в коде и намека нет на какую то работу в процессе приема отдельного байта.

          Есть… намек. Это все должно делаться протоколом верхнего уровня, который использует драйвер. Можно SomeProtocol переделать как то так:


          template <typename UartDriver, typename Timer>
          struct SomeProtocol 
          {
          __forceinline static void OnTimeOut()
          {
            //Сработал таймер, вызвалось OnTimeout() - считаем, что вся посылка принята 
            //обрабатываем посылку из внутреннего буфера, можем послать сигнал задаче для
            // обработки, если обрабатывать долго.
          } 
          __forceinline static void OnReceiveComplete(tBuffer& buffer, std::size_t length)  
            {
               // Примем по запросу 1 байта завершен, так как запрашивали 1 байт, то его и обрабатываем
                assert(length <= buffer.size()) ;
                //например, просто перекладываем байт во внутренний буфер протокола, накапливаем посылку
                internatlBuffer[index++] = buffer[0] ;
                //перезапускаем таймер
                Timer::Reset() ;       
            }  
            __forceinline static void Proceed()  
            {
              //Запрашиваем чтение по 1 байту.
              UartDriver::ReadData(1) ;
            }
          ...
          }

          • mmMike
            /#21285630

            Ладно… учебная задача. Решена нормально. Но только лично у меня вызвал внутренний протест слово "драйвер" и какая то заявка на универсальность.
            А в результате вполне конкретная реализация с с явными ограничительными условиями (которые видны по коду).
            ИМХО, универсальный "драйверов" не бывает. используется адаптера HART — одна реализация. Просто UART для вывода логов и приема команд — совсем другая реализация. Дохлый 8-битный ARV — одно. STM32 — другое… ESP32 + FreeRTOS — совсем другое.


            Для студентов вполне нормальный код.


            IMHO. покажите мне хоть одного программера, которые смотрит на эти картинки с квадратиками. По коду ГОРАЗДО понятнее как и что работает. Размер картинок (визуально) больше кода.

            • lamerok
              /#21285662

              Ну он как бы универсальный… вы можете прикрутить его к любому протоколу.
              Другое дело, что не задействует модули разные. Но его его без изменения можно прикрутить что к AVR, что к STM8 — хотя опять же такой задачи не ставилось. Должно было работать на STM32 только.
              А вот если тянуть DMA, то уже не так "универсально". В целом конечно согласен, универсального ничего не бывает, но как можно больше универсально очень даже..


              покажите мне хоть одного программера

              На работе у меня так принято, вначале моделировать архитектуру, а потом реализовывать. Можно конечно формальные методы использовать, но таких инженеров трудно найти. Поэтому используем полуформальные с помощью UML. Можно посмотреть здесь:
              ГОСТ Р МЭК 61508-7-2012
              Раздел — C.3.12 UML

              • mmMike
                /#21285772

                Поэтому используем полуформальные с помощью UML

                Было модно. Было… Пробовали активно… не прижилось...


                Может прижиться только, если заказчики и разработчики говорят на одном языке (UML). Или есть жесткие формальные требования на документацию (военного вида, которую все равно никто не читает).
                Видел я на убогие программки тома такой документации по ГОСТ с такими красивыми картинками и с перлами, например C/C++ примеры вида


                chat *ptr = "sample string";
                int len = sizeof(ptr);

                где len по смыслу предполагается, что длинна строки


                Заказчики разные и в большинстве своем эти картинки воспринимают "ой… а что это такое". Подробный текст с описанием функционала на русском воспринимается всеми.


                Подозреваю, что UML прижился только в специфичных организациях с постоянными заказчиками.


                Описание на UML архитектуры (а тем более упаси боже генерация С++ кода по UML) — это не панацея и неоднозначностей в взаимопонимании не устраняет.


                IMHO… UML это пиар компания Rational Rose. Довольно ограниченный и плохочеловековоспнимаемый язык.
                И да… я даже курсы когда то (2003..2005) по этой хре… ни заканчивал.
                Я не говорю что все плохо.
                Seq диаграммы — замечательно.
                ER диаграммы — сложно обойтись при проектировании БД.


                А UML диаграммы классов — бр… нахлебался. не хотел бы.

                • lamerok
                  /#21285842

                  Вообще UML это не документация — это моделирование как ПО будет работать. Просто получается замечательно, если в конце автоматом еще получается и иметь документацию в виде UML.


                  Но суть не в UML, не важно на чем делается архитектура, мое мнение, что архитектура перед реализацией должна моделироваться даже просто на листочке в виде прямоугольников и кружков.


                  Но так как ГОСТ разрешает это моделирование с помощью UML, то зачем придумывать что-то еще?


                  И согласен с вами, не нужно сразу все диаграммы брать и пытаться использовать, достаточно использовать те диаграммы, которые дают команде понять как будет работать софт. Если в каком-то случае важна диаграмма последовательности — ок используем её, если достаточно диаграммы классов — ок. UML — это не панацея, это способ описать то, как будет работать программа, до момента, когда вы начинаете писать код.


                  Писать код, как правильно заметили, могут и студенты..., а вот правильно структурировать программу, думаю, уже нужен опыт.

                  • mmMike
                    /#21285884

                    Вопрос терминологии… всегда нужно определять что под чем подразумевают.
                    Когда я писал о "документировании" — подразумеваю описательный процесс. На бумажке ли, словами ли, языком UML ли. Не важно. Это описание чего либо. Не


                    Когда слышу "моделирование" — предполагаю, что это динамическая модель, позволяющая проверить работоспособность чего то. архитектуры в том числе. Демо программа или эмулятор электрической схемы, блочной логики и пр. Главное что в динамике.
                    Прогон моделей программ по UML описанию… у Rational Rose были такие планы. Да не сложилось как толком. Автогенерация кода по UML моделям была (убогая).

                • lamerok
                  /#21285876

                  chat *ptr = "sample string";
                  int len = sizeof(ptr);

                  У вас тут еще ошибка нельзя преобразовывать const char* в char'', а у вас тут неявное преобразование идет из const char* идет…
                  Так нельзя:


                  chat *ptr = "sample string";

                  • mmMike
                    /#21285936

                    Вы точно прочитали мой комментарий в котором я этот кусочек привел? :) это НЕ мой кусок кода!


                    ЭТО пример (над которым я ржал) из огромного набора фалов документаций на программку (по ГОСТ… сертифицированную ФСБ).
                    Там еще и IDL файл протокола всунутый прямо в PDF файл в таком режиме, что еще и не вытащишь сразу и еще парочка перлов.
                    За то все по ГОСТ… формуляр отдельным документом и прочее. Аж ностальгия (как синьки из ЗИП к военной технике).


                    У вас тут еще ошибка нельзя преобразовывать char в const char, а у вас тут неявное преобразование идет из const char* идет…

                    Это всего лишь warning, да и то если не задавлен ключами компиляции. Коряво, но не смертельно.


                    Соль шутки в sizeof от указателя..


                    Я надеюсь что это автор документации постебался. Хотя глядя на некоторые другие перлы реализации протокола обмена в этой программе…
                    Возможно и не постебался. А просто это уровень разработчиков в гос. конторах приближенных к..

                    • lamerok
                      /#21286006

                      Это всего лишь warning, да и то если не задавлен ключами компиляции. Коряво, но не смертельно.

                      Это запрещено стандартом, поэтому на каком-то из компиляторов может быть смертельно, либо код просто не соберется.


                      Про sizeof — понятно, видимо в конторе не использовали статический анализатор кода...

                      • mmMike
                        /#21286162

                        Это запрещено стандартом, поэтому на каком-то из компиляторов может быть смертельно, либо код просто не соберется.

                        Попробовал (как раз сейчас код с/с++ отлаживаю. gcc кроссплатформенный).
                        В "С" модуле даже warning не выдал на такой код.
                        В "C++" warning: ISO C++ forbids converting a string constant to 'char' [-Wwrite-strings]*


                        Просто интересно стало как такая строка могла пролезть. Наверное на С писали.
                        Да еще DLL у них была собрана на VisualStidio 2003 года (это в 2015 году!). Там возможно и на C++ ошибку не выдавало.

                        • lamerok
                          /#21286906

                          Да, скорее всего на Си sizeof. Наверное хотели сделать так:


                          chat ptr[] = "sample string";
                          size_t len = sizeof(ptr);

                          ТОгда было правильно, в любом случае, любой статический анализатор нашел бы эту ошибку. Тут проблема не в архитектуре, документах или коде, а в процессе разработки, можно было бы написать 100500 листов документов, при этом получить нерабочий код. От ошибок в коде никто не защищен, все делают ошибки, даже такие тривиальные. Но от таких простых ошибок очень легко избавиться даже студентам, например, статический анализатор кода вам об этом скажет, юнит тест может об этом рассказать. А вот, ошибки в логике или архитектуре — их отловить трудно, только уже на конечных системных тестах или у пользователя.


                          А вот если будет процесс с Peer review, с заливанием кода, только после проверки статическим анализатором и прохождением всех юнит тестов, то наверное такого бы не было. И архитектура в этом процесс первична. Ну по-крайней мере в моем понимании процесса разработки ПО.

    • EighthMayer
      /#21285530 / +2

      Действительно, как может научить чему-то студентов человек, пишущий код иначе чем вы! Да он же явно вредитель!

    • fougasse
      /#21285684 / -1

      Естественно, что нет.
      Человек критические секции расставляет "чтобы было", о какой правильности речь?

  5. Costic
    /#21287272

    Мне ваши статьи интересны, но ваш стиль программирования на мой взгляд — ужасен.
    Элементарный UART, который доступен «из коробки» и работает везде, вы смогли усложнить. При работе с UART периодически возникают ошибки на линии, где обработчики?
    Вы используете С++, но с буфером применяете «memcpy». Вот для буфера класс необходим, например, кольцевого. Я мог бы понять, если вы драйвер для какой-то ОС пишите или делаете реализацию iostream, чтобы воспользоваться унаследованными методами ввода-вывода. Тяжело ваш код читать. Классы С++ призваны скрывать детали реализации и должны предоставлять программные интерфейсы. А у вас все «кишки» класса наружу (простите, imho).

    • lamerok
      /#21287654

      Согласен в общем, справедливости ради:
      Это код студентов, была задача разобраться, как вообще работает прием, как оповещать о наступлении события, как развязать аппаратуру. Считаю они справились, решение показалось интересным и рабочим.


      Идея была такая, сделать разработку "любого" протокола простым, используя только несколько событий. Если считать, что UART модуль описан и все уже сделано, то задача программиста только сконцентрироваться на написании самого протокола.


      Обработок ошибок действительно нет, согласен они нужны. Как минимум ошибка четности нужна практически в любом протоколе. Это вопрос техники, его можно точно также приделать и сделать событие OnParityError().


      но с буфером применяете «memcpy»

      Я не знаю более быстрого способа. Он же входит в стандартную библиотеку, почему не воспользоваться? Хотя в данном случае можно было вообще не копировать, как видно, для приема и передачи используется тот же самый буфер, он же передается и в WriteData()


      Про буфер согласен, хорошо бы сделать класс отдельный.


      А у вас все «кишки» класса наружу (простите, imho).

      Не могли бы вы показать на примере, чтобы я точно определил, про что речь.


      Но, признаю есть такое, но редко, иногда хочется спрятать часть методов, но лень делать френдов. Однако в целом, торчит только то, что нужно, например, в UartDriverспрятать можно разве что:
      OnTransmitComplete() и OnReceive(). Но тогда нужно добавлять frenda, потому что UART прерывание должно вызвать эти методы. Как это кроме frenda обойти, я не знаю, если делать это через традиционный шаблон подписчик, то нужно было бы наследовать интерфейс опять таки с публичными виртуальными методами и они тоже торчали бы наружу. Но остальные методы то должны торчать наружу. Там всего 3 метода и все должны быть публичные.


      Вообще да, для упрощения используются структуры, иногда некоторые методы автоматом улетают в паблик, признаю — нехорошо, как тут написано https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rc-org
      используйте класс для инварианта… С этой точки зрения UartDriver должен быть class.


      По поводу, что усложнили — для конечного пользователя код довольно простой. Реализовать OnReceiveComplete() OnTransmitComplete() и дополнительно когда будет OnParityError() вроде бы не сложно, все остальное делает драйвер.

      • looogle
        /#21301420

        Вместо std::memcpy можно использовать более типобезопасный std::copy. По скорости он не уступает, а бывает даже быстрее. Да и сырой указатель не очень красиво, C-way.

        • lamerok
          /#21301424

          Спасибо, согласен, принято!

    • lamerok
      /#21287906

      увидел ваш комментарий из параллельной статьи,
      Так и тут код посылки будет простой:


      const char* message = "Hello, world!";
      MyUartDriver::WriteData(message, strlen(message)) ;

      Дальше, как вы и сказали можно переопределить cout и перенаправить поток в UART, через драйвер и тогда можно отсылать обычным способом std::cout << "Hello world" << std::endl;
      Просто можно сделать кое-что как подписаться на события и обработать их. Причем подписаться уже кодом из бизнес логики, не лазя в прерывания и ничего не зная о том, что там за регистры у микроконтроллера.


      Т.е. простота она для пользовательского кода, вам не нужно думать, что и как настроить, а можно сконцентрироваться на решение основной задачи, что должен делать прибор. Что там на задворках — без разницы, если это эффективно и без ошибок — один раз написал и забыл.

      • Costic
        /#21288024

        Поясню, что я имел ввиду про «кишки». Вы используете template<> очень часто в своём коде (из того что на Хабре встречалось). На мой взгляд даже там, где не следует. Но это уже личные предпочтения. В общем, использование template<> как раз выворачивает «кишки» и не даёт скрыть реализацию. Вместо шаблонов я бы использовал наследование и виртуальные функции. Если нужен свой обработчик — наследуем и меняем OnError() и т.д.
        Замечание про «memcpy()» в том, что это C-style, а у вас везде C++ и доступ к буферу некрасиво. class CRingBuffer это скроет. imho.

        • lamerok
          /#21288212

          Ок, понял. Я в начале статьи написал, что можно использовать другой подход, не статический, он как раз, то, что вы описываете.


          Виртуальны функции, я ничего против них не имею, за исключением накладных расходов. Поэтому стараюсь от них избавиться, считайте у меня такая фобия :)


          По поводу template не согласен, поясню почему…
          Если вы хотите подписать кого-то, вы как это будете делать. Два способа, либо через конструктор, либо метод делать. Давайте посмотрим через конструктор:


          В итоге ваш код как-то так будет выглядеть:


          UartDriver MyDriver(MyHardwareUart, Observer1, Observer2);
          
          class MyDriver 
          {
             public:
              template<typename... Args>
               MyDriver(HarwareUart& myUart, IObservers1& observer1, IObservers2& observer2  ): Uart(myUart),  Observer1(observer1), Observer2(observer2)
              {....} ;
             private:
              HarwareUart& Uart;
              IObserver1& Observer1 ;
              IObserver2& Observer2 ;
          } ;

          Все тоже самое… только через конструктор. Причем у вас тут еще дополнительного кода куча, который вообще зачем нужен? В приватной секции 3 ссылки держим, надо их еще инициализировать, конструктор описываем.


          Тоже самое с темплейтом будет выглядеть так


          using MyDriver = UartDriver<MyHardwareUart, Observer1, Observer2>
          
          template<typename UartModule, typename  Observer1, typename Observer2>
          struct UartDriver
          {
            using Uart = UartModule ;
          }

          Или вы имеете ввиду, что Uart — торчит наружу? то да, его надо в private убрать или вообще удалить, он вообще-то не нужен итак есть UartModule.


          Другое дело, что я частенько этим пренебрегаю и они у меня попадают в public, так как структуру использую, тут согласен, надо наверное на class переходить :)


          Про буфер — согласен.

          • Costic
            /#21288604

            Да, именно виртуальные функции и наследование. Ваш выбор template<> понятен, но на мой взгляд применение template обосновано только для контейнеров и алгоритмов. Хочу ещё раз напомнить о классе stream. Вы верно уловили мою мысль получить: uart << «Hello world!»; и т.д.
            Когда-то предполагалось, что класс С++ будет задавать интерфейс в .h (.hpp) файле, а его реализация будет скрыта в .cpp файле. Один программист сможет пользоваться интерфейсом из .hpp, пока второй будет дописывать и оптимизировать реализацию в .cpp. На примере кольцевого буфера — можно задать интерфейс, сделать простеший буфер uint8_t m_buffer[256]; и начать параллельную работу. Перегрузить operator [], operator ++ и.д. На выходе будет красота!

  6. evgeniy1294
    /#21290254

    1) Большинство ip-ядер, реализующих usart, так или иначе умеет генерировать прерывание IDLE, которое позволяет определять конец транзакции. Там, где данное прерывание не предусмотрено (или ядро кривое и не работает как надо, тот же cc1352), можно использовать таймер.
    Приём и передача стандартно выполняются DMA, под которые нужно два буфера для приёма и передачи. Вендоры сейчас начали пихать нормальные матрицы коммутации для DMA, часто с пробросом эвентов от периферии типа таймеров, так что разработка становится проще и удобнее. Аналогично все работает и с другими интерфейсами типа SPI.
    Передача байтами «вручную» конечно подходит для начальных этапов обучения, но не более.

    2) Опять таки IAR. Моё руководство требует с меня лицензионной чистоты софта и отсутствие лишних расходов. Чтобы нормально пользоваться IAR нужно знать его особенности (т.е. нужно прочесть много документации), нужно знать особенности рантайма, нужно знать и уметь писать скрипты линкера, из которых следуют нюансы устройства startup-ов.
    Зачем грузить этим студентов, если IAR им никто не купит? У них будет gcc, cmake и openocd. Я предпочту закупить оборудование, нанять ещё одного разработчика или дополнительную премию сотрудникам выплатить.
    IAR Embedded Workbench как IDE очень плох, разработка на нём как минимум не доставляет удовольствия. Вы же не заставляете бедных студентов им пользоваться?

    • lamerok
      /#21290342

      1) Про DMA понятно, следующая задача будет сделать тоже самое с использование DMA. Единственный момент, как я уже говорил, нужно следить за временем между приемами каждого байта, так как на этом основан механизм захвата токена в некоторых полевых протоколах.


      2) IAR уже не так страшен, есть подозрение, что последние версии компилятора используют либо что-то на основе GCC либо Glang, уж слишком похоже выглядят ворнинги и ошибки, найденные во время компиляции.


      Настройки линкера действительно нужно знать. Согласен, IAR IDE не айс для нормальных разработок, поэтому я использую Clion, но в связке с компилятором IAR и OpenOcd, правда там не все работает как надо.


      Cmake, OpenOcd — это хорошо, но для студентов это слишком, так как курс рассчитан обучение разработки, а не на обучение средствам для этой разработки и времени объяснять как работает сmake, как настроить OpenOcd с китайским отладчиком — совершенно нет. Надо, чтобы поставил и забыл. Можно было бы использовать Keil, но поскольку я использую IAR на работе, то и им даю IAR. Демо версия на 30 кБайт IAR — самое то, в 30 кБайт что угодно можно вместить вместе с операционной системой, графическим индикатором и каким-нибудь простеньким протоколом, типа Modbus.


      Вообще, мое мнение, для обучения не важно какая IDE, они все примерно одно и то же делают, одни немного продвинутее и удобнее, другие нет.В обучении важно не зацикливаться на том, какие средства для кодирования используются (хоть в нотепаде пиши ) и сборки (можно и make в нотепаде написать самому). Важно сконцентрироваться на том, как софт разрабатывается в принципе, какие подходы есть, как микроконтроллер работает и так далее — средства, это всего лишь инструменты, которые позволяют ускорить и упростить эту разработку — работать со средствами можно и самостоятельно обучиться.


      Почему используем IAR — потому что он имеет сертификат безопасности IEC 61508. А он необходим для разрабатываемого софта для устройств, работающих в надежных применениях, типа нефтеперерабатывающего завода, перегонной станции. Иначе тендер мимо кассы — и сертификат — это огромное преимущество. Потратить 5000-10000 долларов на лицензии, зато потом выиграть тендер в 1 000 000 — наверное это того стоит.