Русский
Русский
English
Статистика
Реклама

Dma

Попытка использовать современный C и паттерны проектирования для программирования микроконтроллеров

31.01.2021 18:12:32 | Автор: admin
Всем привет!

Проблема использования С++ в микроконтроллерах терзала меня довольно долгое время. Дело было в том, что я искренне не понимал, как этот объектно ориентированный язык может быть применим к встраиваем системам. Я имею ввиду, как выделять классы и на базе чего составлять объекты, то есть как именно применять этот язык правильно. Спустя некоторое время и прочтения n-ого количества литературы, я пришёл к кое каким результатам, о чем и хочу поведать в этой статье. Имеют ли какую либо ценность эти результаты или нет остается на суд читателя. Мне будет очень интересно почитать критику к моему подходу, чтобы наконец ответить себе на вопрос: Как же правильно использовать C++ при программировании микроконтроллеров?.

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

В этой статье, я, на примере использования USART в МК stm32 для связи с esp8266 постараюсь изложить свой подход и его основные преимущества. Начнем с того, что главное преимущество использование C++ для меня это возможность сделать аппаратную развязку, т.е. сделать использование модулей верхнего уровня независимым от аппаратной платформы. Это будет вытекать в то, что система станет легко модифицирована при каких либо изменениях. Для этого я выделил три уровня абстракции системы:

  1. HW_USART аппаратный уровень, зависит от платформы
  2. MW_USART средний уровень, служит для развязки первого и третьего уровней
  3. APP_ESP8266 уровень приложения, ничего не знает о МК

HW_USART


Самый примитивный уровень. Я использовал камень stm32f411, USART 2, также выполнил поддержку DMA. Интерфейс реализован в виде всего трех функций: инициализировать, отправить, получить.

Функция инициализации выглядит следующим образом:

bool usart2_init(uint32_t baud_rate){  bool res = false;    /*-------------GPIOA Enable, PA2-TX/PA3-RX ------------*/  BIT_BAND_PER(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN) = true;    /*----------GPIOA set-------------*/  GPIOA->MODER |= (GPIO_MODER_MODER2_1 | GPIO_MODER_MODER3_1);  GPIOA->OSPEEDR |= (GPIO_OSPEEDER_OSPEEDR2 | GPIO_OSPEEDER_OSPEEDR3);  constexpr uint32_t USART_AF_TX = (7 << 8);  constexpr uint32_t USART_AF_RX = (7 << 12);  GPIOA->AFR[0] |= (USART_AF_TX | USART_AF_RX);            /*!---------------USART2 Enable------------>!*/  BIT_BAND_PER(RCC->APB1ENR, RCC_APB1ENR_USART2EN) = true;    /*-------------USART CONFIG------------*/  USART2->CR3 |= (USART_CR3_DMAT | USART_CR3_DMAR);  USART2->CR1 |= (USART_CR1_TE | USART_CR1_RE | USART_CR1_UE);  USART2->BRR = (24000000UL + (baud_rate >> 1))/baud_rate;      //Current clocking for APB1    /*-------------DMA for USART Enable------------*/     BIT_BAND_PER(RCC->AHB1ENR, RCC_AHB1ENR_DMA1EN) = true;    /*-----------------Transmit DMA--------------------*/  DMA1_Stream6->PAR = reinterpret_cast<uint32_t>(&(USART2->DR));  DMA1_Stream6->M0AR = reinterpret_cast<uint32_t>(&(usart2_buf.tx));  DMA1_Stream6->CR = (DMA_SxCR_CHSEL_2| DMA_SxCR_MBURST_0 | DMA_SxCR_PL | DMA_SxCR_MINC | DMA_SxCR_DIR_0);       /*-----------------Receive DMA--------------------*/  DMA1_Stream5->PAR = reinterpret_cast<uint32_t>(&(USART2->DR));  DMA1_Stream5->M0AR = reinterpret_cast<uint32_t>(&(usart2_buf.rx));  DMA1_Stream5->CR = (DMA_SxCR_CHSEL_2 | DMA_SxCR_MBURST_0 | DMA_SxCR_PL | DMA_SxCR_MINC);    DMA1_Stream5->NDTR = MAX_UINT16_T;  BIT_BAND_PER(DMA1_Stream5->CR, DMA_SxCR_EN) = true;  return res;}

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

Тогда функция отправки выглядит следующим образом:

bool usart2_write(const uint8_t* buf, uint16_t len){   bool res = false;   static bool first_attempt = true;      /*!<-----Copy data to DMA USART TX buffer----->!*/   memcpy(usart2_buf.tx, buf, len);      if(!first_attempt)   {     /*!<-----Checking copmletion of previous transfer------->!*/     while(!(DMA1->HISR & DMA_HISR_TCIF6)) continue;     BIT_BAND_PER(DMA1->HIFCR, DMA_HIFCR_CTCIF6) = true;   }      first_attempt = false;      /*!<------Sending data to DMA------->!*/   BIT_BAND_PER(DMA1_Stream6->CR, DMA_SxCR_EN) = false;   DMA1_Stream6->NDTR = len;   BIT_BAND_PER(DMA1_Stream6->CR, DMA_SxCR_EN) = true;      return res;}

В функции есть костыль, в виде переменной first_attempt, которая помогает определить самая ли первая это отправка по DMA или нет. Зачем это нужно? Дело в том, что проверку о том, успешна ли предыдущая отправка в DMA или нет я сделал ДО отправки, а не ПОСЛЕ. Сделал я так, чтобы после отправки данных не тупо ждать её завершения, а выполнять полезный код в это время.

Тогда функция приема выглядит следующим образом:

uint16_t usart2_read(uint8_t* buf){   uint16_t len = 0;   constexpr uint16_t BYTES_MAX = MAX_UINT16_T; //MAX Bytes in DMA buffer      /*!<---------Waiting until line become IDLE----------->!*/   if(!(USART2->SR & USART_SR_IDLE)) return len;   /*!<--------Clean the IDLE status bit------->!*/   USART2->DR;      /*!<------Refresh the receive DMA buffer------->!*/   BIT_BAND_PER(DMA1_Stream5->CR, DMA_SxCR_EN) = false;   len = BYTES_MAX - (DMA1_Stream5->NDTR);   memcpy(buf, usart2_buf.rx, len);   DMA1_Stream5->NDTR = BYTES_MAX;   BIT_BAND_PER(DMA1->HIFCR, DMA_HIFCR_CTCIF5) = true;   BIT_BAND_PER(DMA1_Stream5->CR, DMA_SxCR_EN) = true;      return len;}

Особенностью этой функции является то, что мне заранее не известно сколько байт я должен получить. Для индикации полученных данных я проверяю флаг IDLE, затем, если состояние IDLE зафиксировано, чищу флаг и читаю данные из буфера. Если же состояние IDLE не зафиксировано, то функция просто возвращает нуль, то есть отсутствие данных.

На этом предлагаю закончить с низким уровнем и перейти непосредственно к C++ и паттернам.

MW_USART


Здесь я реализовал базовый абстрактный класс USART и применил паттерн прототип для создания наследников (конкретных классов USART1 и USART2). Я не буду описывать реализацию паттерна прототип, так как его можно найти по первой ссылке в гугле, а сразу приведу исходный код, и пояснения приведу ниже.

#pragma once#include <stdint.h>#include <vector>#include <map>/*!<========Enumeration of USART=======>!*/enum class USART_NUMBER : uint8_t{  _1,  _2};class USART; //declaration of basic USART classusing usart_registry = std::map<USART_NUMBER, USART*>; /*!<=========Registry of prototypes=========>!*/extern usart_registry _instance; //Global variable - IAR Crutch#pragma inline=forced static usart_registry& get_registry(void) { return _instance; }/*!<=======Should be rewritten as========>!*//*static usart_registry& get_registry(void) {   usart_registry _instance;  return _instance; }*//*!<=========Basic USART classes==========>!*/class USART{private:protected:     static void add_prototype(USART_NUMBER num, USART* prot)  {    usart_registry& r = get_registry();    r[num] = prot;  }    static void remove_prototype(USART_NUMBER num)  {    usart_registry& r = get_registry();    r.erase(r.find(num));  }public:  static USART* create_USART(USART_NUMBER num)  {    usart_registry& r = get_registry();    if(r.find(num) != r.end())    {      return r[num]->clone();    }    return nullptr;  }  virtual USART* clone(void) const = 0;  virtual ~USART(){}    virtual bool init(uint32_t baudrate) const = 0;  virtual bool send(const uint8_t* buf, uint16_t len) const = 0;  virtual uint16_t receive(uint8_t* buf) const = 0;};/*!<=======Specific class USART 1==========>!*/class USART_1 : public USART{private:  static USART_1 _prototype;    USART_1()   {      add_prototype( USART_NUMBER::_1, this);  }public:  virtual USART* clone(void) const override final  {   return new USART_1; }  virtual bool init(uint32_t baudrate) const override final; virtual bool send(const uint8_t* buf, uint16_t len) const override final; virtual uint16_t receive(uint8_t* buf) const override final;};/*!<=======Specific class USART 2==========>!*/class USART_2 : public USART{private:  static USART_2 _prototype;    USART_2()   {      add_prototype( USART_NUMBER::_2, this);  }public:  virtual USART* clone(void) const override final  {   return new USART_2; }  virtual bool init(uint32_t baudrate) const override final; virtual bool send(const uint8_t* buf, uint16_t len) const override final; virtual uint16_t receive(uint8_t* buf) const override final;};

Сначала файла идёт перечисление enum class USART_NUMBER со всеми доступными USART, для моего камня их всего два. Затем идёт опережающее объявление базового класса class USART. Далее идёт объявление контейнер а всех прототипов std::map<USART_NUMBER, USART*> и его реестра, который реализован в виде синглтона Мэйерса.

Тут я напоролся на особенность IAR ARM, а именно то, что он инициализирует статические переменные два раза, в начале программы и непосредственно при входе в main. Поэтому я несколько переписал синглтон, заменив статическую переменную _instance на глобальную. То, как это выглядит в идеале, описано в комментарии.

Далее объявлен базовый класс USART, где определены методы добавления прототипа, удаления прототипа, а также создания объекта(так как конструктор классов наследников объявлен как приватный, для ограничения доступа).

Также объявлен чисто виртуальный метод clone, и чисто виртуальные методы инициализации, отправки и получения.

После всего лишь, мы наследуем конкретные классы, где определяем чисто виртуальные методы, описанные выше.

Код определения методов привожу ниже:

#include "MW_USART.h"#include "HW_USART.h"usart_registry _instance; //Crutch for IAR/*!<========Initialization of global static USART value==========>!*/USART_1 USART_1::_prototype = USART_1();USART_2 USART_2::_prototype = USART_2();/*!<======================UART1 functions========================>!*/bool USART_1::init(uint32_t baudrate) const{ bool res = false; //res = usart_init(USART1, baudrate);  //Platform depending function return res;}bool USART_1::send(const uint8_t* buf, uint16_t len) const{  bool res = false;    return res;}uint16_t USART_1::receive(uint8_t* buf) const{  uint16_t len = 0;    return len;} /*!<======================UART2 functions========================>!*/bool USART_2::init(uint32_t baudrate) const{ bool res = false; res = usart2_init(baudrate);   //Platform depending function return res;}bool USART_2::send(const uint8_t* buf, const uint16_t len) const{  bool res = false;  res = usart2_write(buf, len); //Platform depending function  return res;}uint16_t USART_2::receive(uint8_t* buf) const{  uint16_t len = 0;  len = usart2_read(buf);       //Platform depending function  return len;}

Здесь реализованы методы НЕ пустышки только для USART2, так как его я и использую для общения с esp8266. Соответственно, наполнение может быть любое, также оно может быть реализовано с помощью указателей на функции, которые принимают свое значение исходя из текущего чипа.

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

APP_ESP8266


Определяю базовый класс для ESP8266 по паттерну одиночка. В нем определяю указатель на базовый класс USART*.

class ESP8266{private:  ESP8266(){}  ESP8266(const ESP8266& root) = delete;  ESP8266& operator=(const ESP8266&) = delete;    /*!<---------USART settings for ESP8266------->!*/  static constexpr auto USART_BAUDRATE = ESP8266_USART_BAUDRATE;  static constexpr USART_NUMBER ESP8266_USART_NUMBER = USART_NUMBER::_2;  USART* usart;    static constexpr uint8_t LAST_COMMAND_SIZE = 32;  char last_command[LAST_COMMAND_SIZE] = {0};  bool send(uint8_t const *buf, const uint16_t len = 0);    static constexpr uint8_t ANSWER_BUF_SIZE = 32;  uint8_t answer_buf[ANSWER_BUF_SIZE] = {0};    bool receive(uint8_t* buf);  bool waiting_answer(bool (ESP8266::*scan_line)(uint8_t *));    bool scan_ok(uint8_t * buf);  bool if_str_start_with(const char* str, uint8_t *buf);public:    bool init(void);    static ESP8266& Instance()  {    static ESP8266 esp8266;    return esp8266;  }};

Здесь же есть constexpr переменная, в которой и хранится номер используемого USART. Теперь для изменения номера USART нам достаточно только лишь поменять её значение! Связывание же происходит в функции инициализации:

bool ESP8266::init(void){  bool res = false;    usart = USART::create_USART(ESP8266_USART_NUMBER);  usart->init(USART_BAUDRATE);    const uint8_t* init_commands[] =   {    "AT",    "ATE0",    "AT+CWMODE=2",    "AT+CIPMUX=0",    "AT+CWSAP=\"Tortoise_assistant\",\"00000000\",5,0",    "AT+CIPMUX=1",    "AT+CIPSERVER=1,8888"  };    for(const auto &command: init_commands)  {    this->send(command);    while(this->waiting_answer(&ESP8266::scan_ok)) continue;  }      return res;}

Строка usart = USART::create_USART(ESP8266_USART_NUMBER); связывает наш уровень приложения с конкретным USART модулем.

Вместо выводов, просто выражу надежду, что материал окажется кому-нибудь полезен. Спасибо за прочтение!
Подробнее..

Разработка простейшего логического анализатора на базе комплекса Redd

16.06.2020 14:11:48 | Автор: admin
В прошлой статье цикла мы потренировались сохранять данные из потокового интерфейса в память средствами DMA. Пришла пора сделать какую-то полезную поделку, используя полученные навыки. Очень полезная при удалённой отладке вещь анализатор. Вообще, при работе с комплексом скорее нужны специализированные шинные анализаторы, но начинать лучше с чего-то попроще. Поэтому сейчас мы сделаем простейший логический анализатор на 32 канала. Понятно, что он будет совсем-совсем примитивным, но зато мы сделаем его своими руками. У кого ещё нет комплекса Redd, могут повторить опыт, используя любую макетную плату с ПЛИС фирмы Altera (Intel) и микросхемой ОЗУ. Итак, приступаем.



Предыдущие статьи цикла
  1. Разработка простейшей прошивки для ПЛИС, установленной в Redd, и отладка на примере теста памяти.
  2. Разработка простейшей прошивки для ПЛИС, установленной в Redd. Часть 2. Программный код.
  3. Разработка собственного ядра для встраивания в процессорную систему на базе ПЛИС.
  4. Разработка программ для центрального процессора Redd на примере доступа к ПЛИС.
  5. Первые опыты использования потокового протокола на примере связи ЦП и процессора в ПЛИС комплекса Redd.
  6. Веселая Квартусель, или как процессор докатился до такой жизни.
  7. Методы оптимизации кода для Redd. Часть 1: влияние кэша.
  8. Методы оптимизации кода для Redd. Часть 2: некэшируемая память и параллельная работа шин.
  9. Экстенсивная оптимизация кода: замена генератора тактовой частоты для повышения быстродействия системы.
  10. Доступ к шинам комплекса Redd, реализованным на контроллерах FTDI
  11. Работа с нестандартными шинами комплекса Redd
  12. Практика в работе с нестандартными шинами комплекса Redd
  13. Проброс USB-портов из Windows 10 для удалённой работы
  14. Использование процессорной системы Nios II без процессорного ядра Nios II
  15. Практическая работа с ПЛИС в комплекте Redd. Осваиваем DMA для шины Avalon-ST и коммутацию между шинами Avalon-MM



Определяем функциональность анализатора


На самом деле, самый-самый простейший анализатор мы сделали ещё в прошлый раз. Напомню, как выглядит шина AVALON_ST, скопировав рисунок из старой статьи:



То есть пробросили внешние линии на шину data, взвели сигнал valid, и началось запоминание по принципу отсюда и до обеда. Ну, то есть, пока память не закончится. Так работал мой осциллограф смешанных сигналов RIGOL, так работал логический анализатор HANTEK. Если для осциллографа смешанных сигналов по-другому нельзя, ведь аналоговый сигнал всё время изменяется, а он сохраняется вместе с цифрой, то для логического анализатора такой подход более, чем странен. Зачем сохранять данные без сжатия? В далёком 2007-м году добыл я китайский анализатор LA5034. Он был настолько китайским, что даже программа к нему сначала не имела английского интерфейса! Так вот, даже он уже не расходовал память на сохранение одних и тех же данных. Имея всего несколько килобайт ОЗУ (встроенного в ПЛИС), он позволял делать намного больше, чем дурацкий HANTEK с многомегабайтными микросхемами памяти.

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

Методика сжатия потока


Я выбрал самую простейшую методику сжатия. Линия задержки и компаратор прямого и задержанного на один такт сигнала.



Первый регистр на схеме выполняет чрезвычайно важную функцию. Нельзя работать с недискретизированными данными! Много лет назад я на этом обжёгся. У меня в проекте автомат переходил из одного состояния в другое. Всё бы ничего, но на графе переходов не было такой стрелки. В чём же дело? А я анализировал как раз сырые, а не дискретизированные данные. В результате, они могли изменить своё состояние в любой момент. Как известно, внутри ПЛИС у линий GCK скорость распространения более-менее единая, у остальных же линий совершенно произвольная. А состояние автомата задавалось в двоичном виде. То есть, для его хранения использовалось несколько битов, хранящихся в нескольких триггерах. В отличие от процессора, новое содержимое, которое защёлкнется в каждый бит, вычисляется независимо. И время прохождения сигнала в процессе этих вычислений от входа до триггера тоже для каждого бита своё.

И вот. Надо нам, скажем, перейти из состояния 0000 в зависимости от условий, или в 0001 или в 0110. И вот условие перехода изменилось очень близко к тактовому импульсу. Давайте я обозначу красными те биты, до которых данные успеют добежать, поэтому они примут новые значения для перехода в 0001, а синими те, до которых не успеют, и они примут значение для перехода в 0110. Итак: 0000.

В итоге, получаем состояние 0011. А на графе такого перехода не было! Кодирование методом OneHot не решит проблему, просто она станет очевидной (а так пока я отловил врага, пока понял, кто виноват 4 дня убил, ведь проявлялась беда очень редко, да и сначала я грешил на неверную реализацию логики).

Чтобы избежать этого, дискретизируем всё и вся! Что бы там ни защёлкнулось на входе, на выходе оно будет иметь стабильное состояние на протяжении такта. Поэтому на вход компаратора попадут уже стабильные данные!

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

У такой системы сжатия только один недостаток, но он делает её в таком виде совершенно неприемлемой. Мы не знаем, как долго держалось каждое стабильное состояние. Чтобы устранить данную проблему, добавим таймер. Если сейчас данные 32 бита, то имеет смысл добавить ещё 32-битный таймер, так как суммарная шина должна удваиваться, а после 32 идёт разрядность 64. Просто будем защёлкивать натикавшие показания. Зная значение таймера для прошлой и текущей записи, мы всегда поймём, как долго держалось прошлое значение. Правда, таймер имеет свойство переполняться. На частоте 100 МГц он переполнится через 42.9 секунды. Но ничто не мешает нам при нулевом значении таймера также произвести защёлкивание данных. Накладные расходы памяти будут не так велики, а программа догадается, что произошло переполнение и надо начать отмерять значения с начала. В итоге, получаем такую блок-схему:



Производительность анализатора


64-битная шина данных при 16-битной микросхеме SDRAM это не совсем хорошо. Допустим, мы тактируем ОЗУшку частотой 100 Мгц. Тогда, чисто теоретически, мы не можем использовать частоту дискретизации выше 25 МГц, ведь фактически каждое 64-битное слово будет уходить в ОЗУ в виде четырёх 16-битных слов. А практически, с поправкой на подачу команд микросхеме ОЗУ и циклы регенерации, предельная рабочая частота будет и того меньше. Может, даже 20 МГц.

Что на это можно сказать? Да, при разработке комплекса Redd не стояло задачи сделать супер производительный анализатор. Давайте взглянем на фирменные анализаторы, имеющиеся у меня под рукой. Вот простенький 16-битный. В нём стоит целых две ОЗУшины. Все ножки ПЛИС обслуживают каналы и ОЗУ. Ну, ещё на стык с USB уходят. А в Redd они ещё и для других целей используются.



Вот тот самый многострадальный абсолютно бесполезный HANTEK. Сжатия нет, но тоже две ОЗУшины. Причём, насколько я помню DDR. В своё время, неплохо его изучил: несколько лет назад хотел сделать прошивку со сжатием, даже выпросил у производителя UCF-файл, но так и не освоил работу с ОЗУ у Xilinx. Но с тех пор я мог подзабыть детали схемы.



А вот так изнутри выглядит туловище анализатора LeCroy через отверстие под установку головы:



Там целых четыре модуля памяти с кучей микросхем каждый. Мне не хочется его сейчас вскрывать, но когда я отчищал его от пыли, сильно проникся внешним видом той ПЛИС, которая стоит внутри. Столько модулей памяти в параллель обслуживать много ножек и ресурсов ПЛИСине требуется. И цена такого анализатора (разумеется, нового, а не с eBay) десятки тысяч долларов. Насколько я помню, даже больше полусотни тысяч.

В целом, если кому-то позарез нужна производительность, он может или приобрести макетную плату с 32-битной ОЗУ, или разработать свою, установив туда две 32-битные ОЗУшины. Или даже модули DIMM. Но это уже будет BGA ПЛИС, у неё будет уже другая цена, и всё (включая класс печатной платы) другое. А теория будет та же, что и сейчас, просто надо будет выкинуть преобразователь разрядности шины. Так что продолжаем рассуждения.

Вообще, на самом деле, и у нас всё не так плохо. Если данные идут небольшими пачками, то необходимо и достаточно установить блок FIFO. Пришла пачка она попала в очередь. Дальше на входе тишина, а данные из очереди постепенно уходят в ОЗУ. Таким образом, мгновенная производительность анализатора будет 100 МГц Но в целом всё будет хорошо при условии, что не переполняется FIFO. Именно поэтому я сделал целую статью, которая помогает оставить как можно больше памяти для нужд этого самого блока FIFO. Самое главное блок должен быть установлен там, где шина данных ещё 64-битная. Итого, получаем блок-схему анализатора:



Разработка головы анализатора


Ну что ж, приступаем к разработке головы. Я как-то привык к терминологии мощных шинных анализаторов, у которых имеется универсальное туловище, а уже к нему подключаются проблемно ориентированные головы. Поэтому и у нас будет туловище и голова. Для реализации выбранной схемы не нужно даже делать никаких автоматов.
Интерфейс модуля будет таким:
module AnalyzerHead (    input                   clk,    input                   reset,    input  logic            source_ready,    output logic            source_valid,    output logic[63:0]      source_data,    input logic [31:0]      channels);

Вот так мы реализуем процесс, который защёлкивает данные в регистрах и увеличивает счётчик:
    logic [31:0] counter = 0;    logic [31:0] channels_D1 = 0;    logic [31:0] channels_D2 = 0;    always @ (posedge clk, posedge reset)    if (reset == 1)    begin        counter <= 0;        channels_D1 <= 0;        channels_D2 <= 0;    end else    begin        channels_D1 <= channels;        channels_D2 <= channels_D1;        counter <= counter + 1;    end

Первое условие записи:
    logic valid1;  // Вариант срабатывания 1 - разные данные    assign valid1 = (channels_D1==channels_D2)?0:1;

Второе условие записи:
    logic valid2;    // Вариант срабатывания 2 - переполнение счётчика    assign valid2 = (counter == 0)?1:0;

Результирующее условие записи:
    // Сводим оба варианта воедино    // Если FIFO не готово - увы, данные пропадут    // В полноценном анализаторе надо зажигать аварию при этом    // тут - ну пропадут и пропадут...    assign source_valid = (valid1 | valid2) & source_ready;

Ну, и из опытов ясно, что байты на шине надо немного перекрутить:
 // Данные вот так вот вывернуты    assign source_data [63:56] = counter [7:0];    assign source_data [55:48] = counter [15:7];    assign source_data [47:40] = counter [23:16];    assign source_data [39:32] = counter [31:24];     assign source_data [31:24] = channels_D1 [7:0];    assign source_data [23:16] = channels_D1 [15:7];    assign source_data [15:8] = channels_D1 [23:16];    assign source_data [7:0] = channels_D1 [31:24];

Собственно, всё. Давайте для полноты картины я вставлю полный текст модуля в слитном варианте.
Полный текст модуля
module AnalyzerHead (    input                   clk,    input                   reset,    input  logic            source_ready,    output logic            source_valid,    output logic[63:0]      source_data,    input logic [31:0]      channels);    logic [31:0] counter = 0;    logic [31:0] channels_D1 = 0;    logic [31:0] channels_D2 = 0;    logic valid1;    logic valid2;    always @ (posedge clk, posedge reset)    if (reset == 1)    begin        counter <= 0;        channels_D1 <= 0;        channels_D2 <= 0;    end else    begin        channels_D1 <= channels;        channels_D2 <= channels_D1;        counter <= counter + 1;    end // Вариант срабатывания 1 - разные данные    assign valid1 = (channels_D1==channels_D2)?0:1; // Вариант срабатывания 2 - переполнение счётчика assign valid2 = (counter == 0)?1:0; // Сводим оба варианта воедино // Если FIFO не готово - увы, данные пропадут // В полноценном анализаторе надо зажигать аварию при этом // тут - ну пропадут и пропадут...    assign source_valid = (valid1 | valid2) & source_ready;  // Данные вот так вот вывернуты    assign source_data [63:56] = counter [7:0];    assign source_data [55:48] = counter [15:7];    assign source_data [47:40] = counter [23:16];    assign source_data [39:32] = counter [31:24];     assign source_data [31:24] = channels_D1 [7:0];    assign source_data [23:16] = channels_D1 [15:7];    assign source_data [15:8] = channels_D1 [23:16];    assign source_data [7:0] = channels_D1 [31:24]; endmodule


Упаковка головы в компонент для процессорной системы


Как-то зловеще звучит заголовок Но как бы там ни было, а упаковать всё в компонент нам надо. Мы тренировались делать подобное в этой статье.

У меня получилась шина AVALON_ST, штатные линии тактирования и сброса, и Но сначала рисунок с типовыми вещами:



Из нетиповых: для будущей задумки линии conduit пришлось дать осознанное имя типу сигнала. Оно нам ещё пригодится.



В остальном вроде, всё понятно.

Проектируем процессорную систему


Как мы уже рассматривали в этой статье, мы не станем добавлять в систему процессорное ядро Nios II, а воспользуемся блоком Altera JTAG-to-Avalon-MM.

Работать с контроллером SDRAM мы учились в этой статье, а в этой разбирались, как при помощи блока PLL разогнать систему до 100 Мгц. Экспериментировали с FIFO и изменением ширины шины AVALON_ST при помощи блока AVALON_ST_ADAPTER мы в этой статье. Наконец, с DMA мы экспериментировали буквально в прошлой статье.

Пришла пора собрать все эти знания в едином проекте! Вот такая у меня получилась навёрнутая структурная схема.



Страшно? Ничуть. Давайте пройдёмся по ней сверху вниз. Сначала идёт блок тактирования и сброса. Как всегда, для комплекса Redd, чтобы не мучиться, физическую ножку Reset я не использую (я её всегда виртуальной делаю). Так удобнее для данной конкретной аппаратуры, хоть и не совсем правильно. Тактирование же идёт на блок PLL. Как его настраивать, мы уже подробно рассматривали раньше. Если я вставлю сюда массу скриншотов, то сильно перегружу статью. С выхода c0 мы берём тактовый сигнал для всей нашей системы, а выход c1 экспортируем и подключаем к тактовому входу микросхемы SDRAM.

Master0 это тот самый компонент Altera JTAG-to-Avalon-MM, через который мы будем достукиваться до шины AVALON_MM. Он в настройках не нуждается. Доступ к шине нам нужен, чтобы управлять блоком DMA и чтобы считывать содержимое SDRAM с накопленными результатами.

Дальше идёт наш компонент Голова. А уже из неё растекается поток через цепочку шин AVALON_ST. Сначала он затекает в блок FIFO. Это первый блок, настройки которого стоит показать особо:



8 символов на слово, каждый символ 8 бит. Итого 8*8=64 бита. Ёмкость 4 килослова. Все остальные вещи протокола AVALON_ST отключены. Двойное тактирование сделано для того, чтобы в будущем голова могла работать на частоте, отличной от частоты работы туловища. Это нам пригодится, когда мы будем делать шинный анализатор USB.

Дальше данные перетекают в преобразователь разрядности. Вот его настройки:



Собственно, 8 символов на слово на входе, 4 символа на слово на выходе. 8 бит на символ. Тоже всё просто. Наконец, поток входит в блок DMA. Ему я только типы шин и максимальную длину передачи поправил, да выставил режим доступа только в режиме полного слова, чтобы поднять Fmax. По уму, такой огромный объём памяти дескрипторов не нужен (хотя, нутром чую, что через них мы можем реализовать кольцевой буфер для анализатора). Размер входного FIFO тоже можно уменьшить до минимума, ведь у нас есть FIFO до этого блока. Но, честно говоря, работа над статьёй и так уже затянулась, так что оставим эту оптимизацию для читателей в качестве самостоятельной работы.



Всё. Потоковая часть завершена. Дальше данные попадают в контроллер SDRAM. Напомню его настройки





Из функциональной части всё. Но кто следит за рассказом не по диагонали, а внимательно, наверное, заметил ещё один странный блок DataGen_0. Что это такое? Мы раньше такого не применяли!

Дело в том, что мне же как-то надо проверить работу головы. А это надо все 32 линии назначить на какие-то ножки ПЛИС, подключить к ним какой-то источник А все должны будут поверить мне на слово, что я это сделал. И потом думать, как это повторить у себя. Зачем? Давайте добавим тестовый генератор данных и подключим его не проводами, а через трассировочные ресурсы ПЛИС. Я сделал самый простой счётчик, который увеличивает своё значение в случайные моменты времени. В качестве генератора случайных чисел я взял 32-разрядную M-последовательность, а увеличиваю счётчик, когда в младших восьми битах появляется константа 0x12. Вот такой получился SystemVerilog код, реализующий эту функциональность (обратите внимание, что я по-прежнему не использую сигнал reset, хотя, здесь бы он пригодился):
module DataGen(   input clk,   output logic [31:0] data = 0);// Генератор случайных чиселlogic [31:0]shift_reg = 0;logic next_bit;assign next_bit = shift_reg[31] ^ shift_reg[30]   ^ shift_reg[29] ^ shift_reg[27] ^ shift_reg[25]   ^ shift_reg[ 0];always @(posedge clk)  if(shift_reg == 0)    shift_reg <= 32'h12345678;  else    shift_reg <= { next_bit, shift_reg[31:1] };// Целевой счётчикalways @(posedge clk)begin    if (shift_reg [7:0] == 8'h12)        data <= data + 1;endendmodule

В настройках компонента самая важная деталь это имя параметра Signal Type у conduit шины. Он должен быть таким же, какой я заполнил у соответствующего параметра головы. В остальном всё просто, здесь же нет никаких специальных шин, только conduit



Соединяем соответствующие линии (это единственное соединение на структурной схеме выше, которое я не стал подсвечивать каким-либо цветом), получаем то, что нужно.

Финал работ


Делаем линию reset виртуальной. Всем остальным ножкам я предпочёл сделать назначение не в GUI, а скопировал фрагмент файла *.qsf из проекта, сделанного в самой первой статье.
Вот этот фрагмент:
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to clk_clkset_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[12]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[11]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[10]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[9]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[8]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[7]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[6]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[5]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[4]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[3]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[2]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[1]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[0]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_ba[1]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_ba[0]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_cas_nset_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_clk_clkset_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_cs_nset_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[15]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[14]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[13]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[12]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[11]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[10]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[9]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[8]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[7]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[6]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[5]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[4]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[3]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[2]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[1]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[0]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dqm[1]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dqm[0]set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_ras_nset_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_we_nset_location_assignment PIN_64 -to sdram_addr[12]set_location_assignment PIN_60 -to sdram_addr[11]set_location_assignment PIN_44 -to sdram_addr[10]set_location_assignment PIN_59 -to sdram_addr[9]set_location_assignment PIN_58 -to sdram_addr[8]set_location_assignment PIN_55 -to sdram_addr[7]set_location_assignment PIN_54 -to sdram_addr[6]set_location_assignment PIN_53 -to sdram_addr[5]set_location_assignment PIN_52 -to sdram_addr[4]set_location_assignment PIN_51 -to sdram_addr[3]set_location_assignment PIN_50 -to sdram_addr[2]set_location_assignment PIN_49 -to sdram_addr[1]set_location_assignment PIN_46 -to sdram_addr[0]set_location_assignment PIN_73 -to sdram_dq[15]set_location_assignment PIN_72 -to sdram_dq[14]set_location_assignment PIN_71 -to sdram_dq[13]set_location_assignment PIN_70 -to sdram_dq[12]set_location_assignment PIN_69 -to sdram_dq[11]set_location_assignment PIN_68 -to sdram_dq[10]set_location_assignment PIN_67 -to sdram_dq[9]set_location_assignment PIN_66 -to sdram_dq[8]set_location_assignment PIN_30 -to sdram_dq[7]set_location_assignment PIN_28 -to sdram_dq[6]set_location_assignment PIN_11 -to sdram_dq[5]set_location_assignment PIN_10 -to sdram_dq[4]set_location_assignment PIN_7 -to sdram_dq[3]set_location_assignment PIN_3 -to sdram_dq[2]set_location_assignment PIN_2 -to sdram_dq[1]set_location_assignment PIN_1 -to sdram_dq[0]set_location_assignment PIN_65 -to sdram_dqm[1]set_location_assignment PIN_31 -to sdram_dqm[0]set_location_assignment PIN_34 -to sdram_ras_nset_location_assignment PIN_32 -to sdram_we_nset_location_assignment PIN_42 -to sdram_cs_nset_location_assignment PIN_33 -to sdram_cas_nset_location_assignment PIN_38 -to sdram_ba[0]set_location_assignment PIN_39 -to sdram_ba[1]set_location_assignment PIN_25 -to clk_clkset_location_assignment PIN_43 -to sdram_clk_clkset_instance_assignment -name VIRTUAL_PIN ON -to sdram_cke


Можно приступать к экспериментам Но все уже устали, так что практикой мы займёмся в следующий раз.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru