Графическая библиотека для МК на С++. Драйвер KS0108.

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

Итак, реалированные фичи:
  1. Дисплей можно подключать на любые свободные ноги МК;
  2. Независимомть от целевой платформы;
  3. Возможность отладки на ПК;
  4. Высокоуровневый код для рисования не зависит от дисплея;
  5. Минимальные требования к RAM, ROM и F_CPU;
  6. Картинки могут храниться как по строкам, так и по столбцам;
  7. Поддержка нескольких шрифтов;
  8. Поддержка юникода.
  9. Рисование линий заданной толщины;
  10. Шрифты и картинки могут быть любого доступного размера.

Пункты 1 и 2 получаются на халяву при использовании механизма работы с портами ввода-вывода из этой-же библиотеки. Для этого класс-драйвер дисплея определим как шаблон:
template<class Cs1, class Cs2, class Reset, class Rw, class Di, class E,  class DataBus>
class KS0108 :public KS0108Base
{...};

Здесь Cs1, Cs2, Reset, Rw, Di, E — отдельные пины, DataBus — целый восьми-битный порт или список из восьми произвольных пинов (PinList). Способ подключения дисплея можно определить так:
// typedef Pc0Inv Cs1; 
typedef Pc1Inv Cs2;
typedef Pc2 Rst;
typedef Pc3 Rw;
typedef Pc4 Di;
typedef Pc5 E;
typedef PinList<Pa0, Pa1, Pa2, Pa3, Pa4, Pa5, Pa6, Pa7> LcdDataBus;
typedef KS0108<Cs1, Cs2, Rst, Rw, Di, E, LcdDataBus> Lcd;

Входы Cs1 и Cs2 имеют низкий активный уровень на моём дисплее поэтому используются инвртированные пины Pc0Inv и Pc1Inv, для которых Cs1::Set(); означает опустить в 0, а Cs1::Clear(); — поднять в 1.

Интерфейс драйвера дисплея прост и минималистичен:
static void Init();
static void Fill(Color color);
static void PutPixel(Coord x, Coord y, Color color);
template<class BitmapT>
static void DrawBitmap(const BitmapT &bitmap, Coord x, Coord y, Color foreground, Color background);
static void Flush();
static void SetOutputMode(OutputMode mode);

Init, Fill и PutPixel — думаю, комментариев не требуют.
Flush — по идее слив видео-буфера на дисплей, но в данном драйвере нет буфера, всё рисуется сразу на дисплее. Ведь не всегда есть лишний килобайт RAM для видео-буфера. Так что эта функция — задел на будущее и сейчас ничего не делает.
SetOutputMode — режим вывода пикселей изображения. OutputMode может принимать следующие значения: WriteMode, XorMode, AndMode, AndNotMode, InvertMode. Коментариев тут тоже не требуется.
Самое интересное это функция DrawBitmap. Она имеет шаблонный параметр BitmapT, который представляет класс реализующий картинку. Этот класс должен реализовывать 3 функции:
uint8_t operator()(unsigned x, unsigned y)const;
unsigned Width()const;
unsigned Height()const;

Width() и Height() — это естественно ширина и высота картинки, а operator() возвращает значение пикселя в заданной позиции. Таким образом осуществляется независимость от способа представления картинки.
Рассматрисать реализацию всех функций я не буду — они достаточно просты, рассмотрю только DrawBitmap, она самая интересная:
template<class BitmapT>
void DrawBitmap(const BitmapT &bitmap, Coord x, Coord y, Color foreground, Color /*background*/)
{
// проверяем на выход за границы дисплея
   if(y >= Height() || x >= Width())
      return;
   if(x < -(int)bitmap.Width() || y < -(int)bitmap.Height())
      return;
// включаем инверсию цвета если цвет переднего плана 0
   OutputMode oldMode = _mode;
   if(!foreground)
      _mode = (OutputMode)(_mode ^ OpInvert);
// координата начала текущей страницы
   uint_fast8_t pageStart = (uint_fast8_t)max<Coord>(0, y);
// координата конца текущей страницы
   uint_fast8_t pageEnd = ((pageStart + 8) & 0xf8);
// максимальная Y координата
   uint_fast8_t maxY = (uint_fast8_t)min<Coord>(y + bitmap.Height(), Height());
// максимальная и минимальная X координаты
   uint_fast8_t maxX = (uint_fast8_t)min<Coord>(bitmap.Width() + x, Width());
   uint_fast8_t minX = (uint_fast8_t)max<Coord>(0, x);
// конец страницы не выходит за пределы картинки и экрана
   if(pageEnd > maxY)
         pageEnd = maxY;
// пока не дошли до maxY 
   while(pageEnd <= maxY && pageStart < maxY)
   {
// устанавливаем страницу и адрес в странице сразу в двух контроллерах
      SetPage(pageStart >> 3 , minX);
// сколько пикселей в текущей странице 1..8
      uint_fast8_t pagePixels = pageEnd - pageStart;
// битовая маска для страницы - в 1 выставлены биты, которые не меняем
      uint_fast8_t mask = PageMask(pageStart, pageEnd);
// на сколько бит сдвинуть данные прочитанные из битмапа 0..7
      uint_fast8_t dataShift = 7 - ((pageEnd - 1) & 7);
// цикл по X
      for(uint_fast8_t j = minX; j < maxX; j++)
      {
         uint8_t data = 0;
         Coord dx = j - x;
         Coord dy = pageStart - y;
// читаем нужное кол-во бит картинки и упаковываем их в байт
         for(uint_fast8_t i = 0; i < pagePixels; i++)
         {
            data >>= 1;
            if(bitmap(dx , i + dy))
               data |= 0x80;
         }
// сдвигаем
         data >>= dataShift;
// пишем на дисплей
         WritePage(data, mask, j);
      }
// переходим на следующую страницу
      pageStart = pageEnd;
      pageEnd += 8;
// не выходим за пределы картинки и экрана
      if(pageEnd > maxY)
         pageEnd = maxY;
   }
   _mode = oldMode;
}

Функция WritePage осуществляет вывод одного элемента сраницы и выглядит так:
void WritePage(uint8_t data, uint_fast8_t mask, uint_fast8_t cx)
{
// выбор нужного контроллера
   if(cx  > 63)
   {
      Cs2::Set();
      Cs1::Clear();
      cx -= 64;
   }
   else
   {
      Cs2::Clear();
      Cs1::Set();
   }

   Di::Set();
// если выводим не полную страницу или делаем операции с уже выведенным изображением,
// то придётся читать из дисплея
   if(mask != 0 || (_mode & (OpXor | OpAnd)) )
   {
// первый раз читается статусный регистр
      DataBus::template SetConfiguration<0xff, DataBus::In>();
      Rw::Set();
      PulseE();
// по ходу инвертируем нудные биты в данных если режим инверсии.
      if(_mode & OpInvert)
         data ^= ~mask;
// читаем по текущему адресу
      E::Set();
      Util::delay_us<PulseDelay, SysClock::CpuFreq>();
      uint8_t dest = DataBus::PinRead();
// проводим битовые манипуляции в зависимомти от режима
      if(_mode & OpCopy)
         data = (dest & mask) | data;
      else
      {
         if(_mode & OpXor)
            data ^= dest;
      
         if(_mode & OpAnd)
            data = (data | mask) & dest;
      }
// возвращаем адрес искаженный чтением
      E::Clear();
      DataBus::template SetConfiguration<0xff, DataBus::Out>();

      Rw::Clear();
      Di::Clear();
      Write(SetAddressCmd + cx);
      Di::Set();
   }
   else if(_mode & OpInvert)
      data ^= 0xff;
// пишем данные в текущую позицию
   Write(data);
}

Кроме DrawBitmap WritePage (SetPage — тоже) нигде не используется. Так зачем нужно было выносить этот код в отдельную функцию? Просто DrawBitmap — шаблонная функция и при использовании разных типов битмапов компилятор любезно сгенерирует для каждого этого типа свою версию DrawBitmap. Если включить код WritePage (а он не очень маленький) непосредственно в DrawBitmap, то при этом он будет понапрасну продублирован в каждой инстанции DrawBitmap, а ведь он не зависит от шаблонного параметра BitmapT. Это явление называется «раздутие кода» (code bloat). Вынос фрагментов кода не зависящих от шаблонных параметров в отдельные функции — достаточно эффективное средство от раздутия кода, но тем не менее каждая инстанция DrawBitmap занимает порядка 400 байт (для AVR). Зачем-же делать эту функцию шаблонной и «раздувать код», если можно, например, просто передавать указатель на функцию чтения пикселя из картинки, или сделать абстрактный базовый класс Bitmap с виртуальными функциями (С++ всё-таки) и всё такое? Кто-то еще может вспомнить историю про то как чуваку не хватило одного байта, чтоб вместить прошивку. А вот зачем. При выводе картинок большое значение имеет скорость, вернее даже баланс универсальность — скорость — размер машинного кода. Вызавать функцию чтения пикселя по указателю — это дорого, с учетом того, что в шаблонной DrawBitmap вызов BitmapT.operator() всегда встраивается, в общей сложности DrawBitmap становится на 30-40% медленнее. Это иной раз имеет значение. К тому-же шаблонная DrawBitmap не отменяет возможности использования какого-нибудь абстрактного класса Bitmap с виртуальными функциями. То-есть имеется возможность выбирать между скоростью и размером машинного кода. В текущей реализации вывод картинки 128х64 из флеш памяти на весь экран занимает примерно 22 мс на AtMega16 бегающей на 16 МГц. При теоретическом минимуме 10 мс без учета чтения картинки, и организации циклов — дисплей быстрее не может. Вывод черного прямоугольника на весь экран занимает 15 мс, что очень не плохо.

Картинки.

Класс картинки, пригодный для вывода с помощью DrawBitmap выглядит примерно так:
template<class PtrT = uint8_t*>
class RowOrderedBitmap 
{
public:
   RowOrderedBitmap(PtrT data, unsigned width, unsigned height)
   :_data(data), _width(width), _height(height)
   {
      _bytesPerLine = (_width + 7) >> 3;
   }
   uint8_t operator()(unsigned x, unsigned y)const
   {
      return (*(_data + y * _bytesPerLine + (x >> 3)) & (1 << (x & 7))) != 0;
   }
   unsigned Width()const{return _width;}
   unsigned Height()const{return _height;}
protected:
   PtrT _data;
   unsigned _width, _height, _bytesPerLine;
};

Это полный код для картинки хранящейся по строкам — как видно всё очень просто. Опять шаблон! Зачем тип указателя на данные делать шаблонным параметром? Так ведь адресные пространства бывают разные, нужно уметь читать картинки, например, из флеш памяти на тех-же AVR-ках. В этом нам помогут умные указатели на флеш память.
Так объявляется картинка размещенная во флеш памяти:
const uint8_t bitmapData[] FLASH={ 
...
};
RowOrderedBitmap<FLASH_PTR(uint8_t)> bitmap(bitmapData, 64, 64);
…
// теперь нарисуем картинку с левым верхмим углом в точке 0, 0.
Lcd::DrawBitmap(bitmap, 0, 0);

Также с помощью функции DrawBitmap легко реализовать рисование заполненных прямоугольников:
class Fill
{
public:
   Fill(unsigned width, unsigned height, uint8_t c)
         :_width(width), _height(height), _c(C)
   {}
   uint8_t operator()(unsigned x, unsigned y)const
   {
      return _c;
   }
   unsigned Width()const{return _width;}
   unsigned Height()const{return _height;}
protected:
   unsigned _width, _height;
   uint8_t _c;
};
…
// нарисуем черный квадрат 20х20 по середине экрана.
Lcd::DrawBitmap(Fill(20, 20, 1), 54, 22);

Это в разы быстрее, чем рисовать закрашенный прямоугольник по-пиксельно.

Рисовалка.

Теперь к реализации остальных функций рисования. Большая их часть входят в класс Painter, который принимает тип дисплея в качестве шаблонного параметра:
template<class Display>
class Painter
{
public:
// расстояние между символами в пикселях
   void SetCharInterval(PenWidthType i);
// толщина линий
   void SetPenWidth(PenWidthType w);
// основной цвет
   void SetColor(Color c);
// цвет фона
   void SetBackColor(Color c);
   void DrawLine(Coord x1, Coord y1, Coord x2, Coord y2);
   void DrawCircle(Coord x, Coord y, Coord r);
   void DrawRect(Coord left, Coord top, Coord width, Coord height, bool filled=false);
   void FillRect(Coord left, Coord top, Coord width, Coord height);
   void Clear();
   void PutPixel(Coord x, Coord y, Color color);
   Coord Width(){return Display::Width();}
   Coord Height(){return Display::Height();}
   void SetOutputMode(OutputMode mode);

   template<class Bitmap>
   void DrawBitmap(const Bitmap &pic, Coord x, Coord y);

   template<class CharPtr, class Font>
   void DrawText(CharPtr str, Coord x, Coord y, const Font &font);
};

Большинство функций не требуют особых разъяснений. DrawBitmap, PutPixel, SetOutputMode, Clear, Width, Height являются простыми обёртками над функциями дисплея, просто для удобства.
Прямые рисуются по вполне стандартному алгоритму Брезенхэма, окружности — по алгоритму Мичнера. Основная сложность в них была реализовать рисование с заданной толщиной линий. Это не очень тривиальная задача, как может показаться. И реализовал я это достоточно быстрыми, но не вполне корректными способами. У прямых торцы всегда парраллельны оси Х или У, хотя должны быть перпендикулярны самой прямой, а еще лучше, чтоб они были круглыми. В окружностях появляется смешанная связанность пикселей и из-за этого некоторая визуальная неравномерность толшины линии. К томе-же сейчас линии и окружности рисуются по-пиксельно и по этому работают относительно медленно. Будет время попробую сделать что-то типа построчной растеризации — будет и быстрее и аккуратнее, быстрый DrawBitmap и здесь пригодится.
Остался только вывод текста:
template<class CharPtr, class Font>
void DrawText(CharPtr str, Coord x, Coord y, const Font &font)
{
   while(*str)
   {
      const typename Font::CharType pic = font.GetChar(*str);
      DrawBitmap(pic, x, y);
      x += pic.Width();
      if(_charInterval)
      {
         Display::DrawBitmap(Fill(_charInterval, pic.Height()), x, y, _backColor, _color);
         x += _charInterval;
      }
      ++str;
   }
}

Опять шаблон. CharPtr — тип указателя на символ нужен для поддержки юникода и строк во флеш памяти. Font — тип шрифта. У класса шрифта должна быть только одна функция: GetChar — она принимает символ и возвращает его изображение. Изображением символа должно быть нечто пригодное для рисования с помощью функции DrawBitmap дисплея. Конструкция в блоке if(_charInterval) служит для заполнения интервалов между символами.
Теперь перейдём к шрифтам. Применительно к МК распростронены два способа представления растровых шрифтов. В первом все символы имеют одинаковую ширину, для того чтоб найти смещение символа в таблице, достаточно умножить размер символа в байтах на его индекс в таблице. Этот способ хорошо подходит для маленьких размеров шрифта. Во втором случае символы имеют разную ширину и имеется дополнительная таблица, в которой хранятся ширина каждого символа и его смещение в массиве с изображениями. Для описания шрифтов используются две вспомогательные структуры:
struct CharInfo
{
   uint8_t width; 
   size_t offset;
};
struct FontInfo
{
   wchar_t   startChar,
         endChar;
   const uint8_t *   rawDataPtr;
   const CharInfo* charInfos;
};
Это описание символа и шрифта соответственно. CharInfo хранится ширина и смещение символа. FontInfo описывает не весь шрифт, а один диапазон символов, содержит начало и конец этого диапазона, указатель на графические данные и указатель на таблицу описаний символов. Если последний указатель ноль, то значит шрифт имеет фиксированную ширину.
Класс реализующий шрифт:
template<class BitmapBytePtrT=uint8_t*, class CharInfoPtrT=const CharInfo*>
class RastrFont
{
public:
   typedef ColumnOrderedBitmap<BitmapBytePtrT> CharType;
   RastrFont(const FontInfo *charSets, 
            uint_fast8_t charSetsCount,
            unsigned charWidth, 
            unsigned charHeight, 
            wchar_t unknownChar);

   const ColumnOrderedBitmap<BitmapBytePtrT> GetChar(char c)const
      {return GetChar(static_cast<wchar_t>((uint8_t)c));}
   const ColumnOrderedBitmap<BitmapBytePtrT> GetChar(wchar_t c)const;
protected:
   const FontInfo *_charSets;
   uint_fast8_t _charSetsCount;
   wchar_t _unknownChar;
   unsigned _width, _height;
   wchar_t _charSizeBytes;
};

Класс сделан шаблонным опять-же для поддержки данных размещенных во флеш памяти на AVR. Первый шаблонный параметр — тип указателя на графичечкие данные шрифта, второй — тип указателя на описатели символов. Конструктор класа принимает указатель на массив описаний диапазонов символов, его размер, ширину, высоту символов и unknownChar — символ выводимый когда для требуемого символа нет графических данных. Последний должен быть символом присутстующем в нулевой таблице. Обычно это что-то типа знака вопроса. Код функции GetChar выглядит следующим образом:
const CharType GetChar(wchar_t c)const
{
// ищем подходящую таблицу символов
   uint_fast8_t i = 0;
   for(; i < _charSetsCount &&
      ((unsigned)_charSets[i].startChar < (unsigned)c && 
      (unsigned)_charSets[i].endChar < (unsigned)c);
      i++);
// если не нашли - используем нулевую таблицу и символ-заглушку
   if(i == _charSetsCount)
   {
      i = 0;
      c = _unknownChar;
   }
   size_t offset, width;
   c -= _charSets[i].startChar;
// если есть таблица описаний символов - используем ее
// для поиска смешения графических данных
   if(_charSets[i].charInfos)
   {
      CharInfo ci = *CharInfoPtrT(&_charSets[i].charInfos[(unsigned)c]);
      width = ci.width;
      offset = ci.offset;
   }
// нет - шрифт с фиксированной шириной
   else 
   {
      width = _width;
      offset = _charSizeBytes * (unsigned)c;
   }
// возвращаем нужную картинку
   return CharType(_charSets[i].rawDataPtr + offset, width, _height);
}

Полное описание шрифта размером 5х8 точек с ASCII символами и русскими символами в кодировке CP-1251:

const uint8_t font5x8Ascii[] FLASH= 
{/* Графические данные */};

const unsigned char font5x8Rus[] FLASH= 
{/* Графические данные */};
// диапазон символов ASCII от пробела до тильды без таблицы описаний символов
FontInfo smallFontAscii = 
{
   ' ', '~', font5x8Ascii, 0
};
// диапазон русских символов от "А" до "я" без таблицы описаний символов
// Блин, про буквы "Ёё" забыл %)
FontInfo smallFontRus = 
{
   192, 255, font5x8Rus, 0
};
FontInfo smallFontData[] = {smallFontAscii, smallFontRus};
RastrFont<FLASH_PTR(uint8_t), FLASH_PTR(CharInfo) > smallFont(smallFontData, 2, 5, 8, '?');

Теперь используя этот шрифт можно что-то написать на дисплее:
Painter<Lcd> p;
p.Clear();
p.DrawText("Easyelectronics", 20, 30, smallFont);
p.DrawText("Привет, мир!", 0, 45, smallFont);


Тестирование и отладка.
Теперь самое интересное. Тестировать и отлаживать программы работающие с графикой непосредственно на МК не очень продуктивно — много времени тратится на бесконечные перезаливки прошивки. Толи дело с десктопными приложениями, ткнул кнопку — оно скомпилилось и смотри на результат, захотел — поотлаживал пошагово. Хочется и программы работающие с графикой на МК также тестировать, ну хотя-бы частично. А ведь нет ничего невозможного, у нас для этого есть почти всё, что нужно. Вся представленная библиотека никак не завязана ни на какую целевую платформу, даже драйвер дисплея не привязан к конкретной реализации портов ввода-вывода. Есть реализация тестовых портов ввода-вывода, которая используется для модульного тестирования библиотеки работы с портами. А значит можно протестировать графическую библиотеку вместе с драйвером дисплея, подцепив его к тестовым портам. Нужно только сделать штуку, которая эти тестовые порты будет слушать и изображать из себя дисплей. Итак тестовое приложение будет на «голом» Win32 API без каких либо сторонних библиотек. Собираться будет из 2008 VisualStudio.
Класс эмулирующий дисплей:
class Glcd :public KS0108Base
{
   static const int width = 128;
   static const int height = 64;
public:

   Glcd(void)
   {
// мы хотим получать уведомления гогда состояние портов меняется
      LcdDataPort::callback = this;
      LcdControlPort::callback = this;
   }
// Отобразить содержимое буфера на контексте устройства
   void Draw(HDC dc);
// эта функция принимает уведомления от портов,
// в ней происходит всё интересное - кому надо посмортят в исходниках
   template<class Port>
   inline void PortChanged();
// объявления тестовых портов и нужных пинов
   typedef TestPort<uint8_t, 222, Glcd> LcdDataPort;
   typedef TestPort<uint8_t, 333, Glcd> LcdControlPort;

   typedef InvertedPin<LcdControlPort, 0> Cs1;
   typedef InvertedPin<LcdControlPort, 1> Cs2;
   typedef TPin<LcdControlPort, 2> Rst;
   typedef TPin<LcdControlPort, 3> Rw;
   typedef TPin<LcdControlPort, 4> Di;
   typedef TPin<LcdControlPort, 5> E;
private:
   void SetPixel(unsigned x, unsigned y, unsigned color)
   {
      _buffer[y*width + x] = color ? 0 : 0x00ffffff;
   }
   unsigned GetPixel(unsigned x, unsigned y)
   {
      return _buffer[y*width + x] == 0;
   }
// состояние дисплея
   struct LcdCtrl
    {
        uint8_t page;
        uint8_t addr;
    } c1, c2;
// внутренний буфер изображения
   unsigned _buffer[width*height];
};

В остальном ничего особенного: объявлен глобальный объект типа Glcd и в оконной процедуре по событию ON_PAINT вызывается функция Draw.
Осталось только подключить драйвер к тестовому «дисплею»:
typedef KS0108<Glcd::Cs1, Glcd::Cs2, Glcd::Rst, Glcd::Rw, Glcd::Di, Glcd::E, Glcd::LcdDataPort> Lcd;

Структура демонстрационного примера
  • Mcucpp — библиотека;
  • GlcdTest — демонстрационный пример;
    • common — общие файлы примера;
    • GlcdTestAvr — исходники специфичные для AVR;

    • GlcdTestWin32 — исходники специфичные для Win32;
    • Release — скомпиленный Win32 пример;
    • GlcdTestAvr.aps — проект для AvrStudio 4;
    • GlcdTestWin32.sln — проект для MS Visual Studio 2008;
Пример для AVR расчитан выполнятся на AtMega16, работающей на частоте 16 МГц. Способ подключения дисплея описан в файле «GlcdTest\GlcdTestAvr\demo.h».
Скрин демонстрационной программы:
  • +10
  • 15 мая 2012, 15:07
  • neiver
  • 1
Файлы в топике: McucppGraphics.zip

Комментарии (96)

RSS свернуть / развернуть
Здорово!
0
… графическую попсистему..?
0
поправил.
0
Круто вообще!
А сколько времени ушло на написание?
К этому еще цвет и звук добавить…
0
Времени ушло недели 3 короткими урывками. Цвет там уже почти есть, только драйвер какого-нибудь цветного дисплея нужен и минимальная поддержка для цветных картинок.
0
все уже до тебя украли www.visualglcd.com/
0
Дык это комерческий продукт, 100 баксов стоит и работает только с небесплатным компилятором от Микроэлектроники.
0
и кого это останавливало?
да и почему с небесплатными? вроде он генерит исходник всех файлов
ну разве что работа с битами там сделана через макросы
-1
вот все тебе докопаться…
некоторым людям запрещщено работать с нелицензией, ты о них забыл?
или только о сбе думаешь Калыч?
человек хорошее дело делает, а тебе лишь бы ляпнуть.
+2
если запрещено, то покупают лицензию
100 баксов не такие уж большие деньги для рисовалки графических меню под кучу индикаторов

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

а что касается компиляторов и иде ихних, то свг юзает этот компилятор на паскале для авр и пиков, а он не я и его слово что-то да стоит
в их компиляторах есть почти все либы для работы с почти всеми частовстречающимися обвесами
0
свг юзает этот компилятор на паскале для авр и пиков, а он не я и его слово что-то да стоит
Да, меня несколько удивляет его отношение к поделкам mE. Я не нашел в них ничего из того, за что он их хвалил, кроме относительно приятной (по сравнению с AVR Studio 4 и подобными) IDE.
А библиотеки ихние достаточно мутные, тяжелые и убогие.
Программатор их не пробовал — он работает только с ихним же железом. Но насколько видно — ничего особенного, похож на AVRProg атмеловский. Впрочем, насчет оболочек к дудке согласен — одна другой кривее.
0
А разве это аналог того, что в посте?
Да и не люблю я инструменты микроэлектроники. Ограниченные они и глючные. Особенно компиляторы.
0
Господа! Помогите начинающему?) Нужно совладать с графическим 128х64 дисплеем на KS0108 контроллере. Времени особо разбираться нет( Открыл проект для AVR Studio 4. Столкнулся с проблемами при компиляции. make: *** [GlcdTestAvr.elf] Error 1 и всё тут.
Нужно подружить 128мегу с таким дисплеем.
0
А другие проекты нормально компилируются? Какая версия AVR Studio? Я использовал версии 4.17 совместно с WinAVR-20100110 и 4.19 с AVR Toolchain 3.3.
0
Разобрался с выводом. Нашёл рабочий пример CVAVR/Proteus. radiokot.ru/forum/download/file.php?id=5808&sid=9408f8ed30a88df6015e6968ce4c0906 Теперь надо подружиться с SPI FLASH Memory от Атмела.
0
Чем больше я вижу код с ООП, тем больше меня от него воротит).
Причем самое простое (классы / методы) еще понимаю и иногда использую. Но всякие шаблоны и прочее — уже больше запутывает чем помогает.
0
  • avatar
  • 286
  • 02 июня 2012, 13:24
В этом коде нет ООП. Почему почти все «критики» ставят знак равенства между С++ и ООП?
Шаблоны не нравятся? Не используйте их. Шаблоны — это очень мощный инструмент, позволяющий из высокоуровнего и переносимого исходного кода получать быстрый и компактный машинный код. При правильном пименении, конечно.
Шаблоны запутывают? Прямое обращение к регистрам аппаратуры с разных структурных уровней (если они вообще есть) программы, обилие «магических» чисел и т.д. запутывают больше ИМХО.
+3
А на кой менять шило на мыло (магические числа на шаблоны)?
Можно же писать культурный, понятный, быстрый код.
+1
Вот для этого шаблоны и нужны ;)
0
Шаблоны сложно писать, но удобно использовать и код получается более читаемый. В добавок он значительно более переносим. А прямое ползание по регистрам «понятный и быстрый код» только для того, кто его пишет.

P.S. Пожалуй, в современном промышленном программировании ключевое слово — читаемость. Код использующий шаблоны (но не сами шаблоны) более читаем и это уже достаточный повод, что бы его использовать. Скорость работы и компактность бинарника это лишь еще пара вишенок к этому пирожному…
+1
Шаблоны сложно писать, но удобно использовать и код получается более читаемый.
Это смотря как и кем они написаны. Я имею достаточно удовольстсия работать с этой хернёй. Пока итоги плачевны.
А прямое ползание по регистрам «понятный и быстрый код» только для того, кто его пишет.
Я не имел ввиду ползание по регистрам в понятном коде. Я имею ввиду что пожно отказаться и от магии чисел и от шаблонов и получить действительно читаемый код.
0
Эээ… Можно пример такого кода?
0
#include <stdio.h>
int main(int argc, char *argv[])
{
    printf("Hello World!\n");
    return 0;
}

И ведь сделали же.
Примеры же кривого кода уже приведены ниже. В добавок к ним афх добавляем и кустарные классы неопознанных программистов.
Большая ошибка говорить что шаблоны легки, понятны и круты. Это провоцируют бросание на них школоты и прочих хомячков. Полученное дерьмо разгребать и врагу не посоветуешь.
0
Я могу и hello world аргументировано за непонятность поругать. Но меня интересовал пример специфичный для эмбеддед.

Я нигде не писал, что шаблоны легки, понятны и круты. Но грамотный код написанный с использованием не менее грамотно спроектированных и реализованных шаблонов действительно легко читается и сопровождается.
0
Вопрос в том сколько этого грамотрого кода?
Hello world — да, тоже пример непонятный. Но объясняется он куда проще шаблонов. Мы живем в реальном мире, по этому анализировать надо реально-существующую ситуацию, а не то что «в теории это крутой механизм». Механизм то крутой, но им пользоваться не умеют. И грамотного его применения практически не найди. Зато все лезут его применять.
Я бы сказал хорошо, что появился дот-нет и си-шарп. Говнокодеры перебрались на него, и мне как С/С++ программисту стало гораздо проще. Не в обиду си-шарпу, язык хорош для изучения и использования, всё сам ни как не переберусь на него.
0
Механизм то крутой, но им пользоваться не умеют. И грамотного его применения практически не найди.
Поэтому статьи с грамотным применением шаблонов — это как раз таки хорошо.

Опять же, если шаблоны использовать как генерики — ничего непонятного в них нет (что может быть непонятного в обычном классе списка, в котором вместо void* в качестве типа хранимых данных указан *Т?). Крыша начинает ползти тогда, когда начинаешь на них метапрограммировать. Но увы, других механизмов МП там нет, а это весьма нужный инструмент.
0
Грамотного кода действительно не так много, но язык тут ни при чем, по большому счету. Он может стимулировать или наоборот мешать каким-то вещам, но и только.
Примеров грамотного применения шаблонов полно, начиная от классического STL (потроха которого срывали крышу даже в далеком 92-м) до весьма популярного boost-а. То, что пишет, публикует и описывает (в частности в этом топике) камрад neiver — тоже примеры грамотного использования шаблонов.
0
И Александреску еще. Он еще и объясняет, как оно работает)
" — Я покажу вам особую, шаблонную магию!"
0
Это смотря как и кем они написаны. Я имею достаточно удовольстсия работать с этой хернёй. Пока итоги плачевны.
С++-вые шаблоны мозговыносящи, как и весь язык. Так что да, из них очень легко сделать кашу (а уж практически все приемы метапрограммирования на них выносят моск на раз, см. boost и loki). Но при правильном применении — дюже полезная вещь. Опять же, если грамотно спроектированным кодом на шаблонах только пользоваться — получается очень простой и красивый код… Ценой понятности самих шаблонов.
В этом плане лучше все же раздельно генерики и средства метапрограммирования, но из распространенных языков такое есть разве что в питоне.
0
Подозреваю, что возможности метапрограммирования в плюсах получились случайно, как побочный эффект от введения шабонов в комбинации с системой типов, особенностями кодогенерации и прочими прибамбасами, которые вводились совершенно из других соображений. В пользу этого предположения говорит и тот факт, что возможности метапрограммирования за все эти годы никто особо в плюсах и не развивал (я имею в виду сам язык), а имеющиеся популярные либы, которые их используют, сносят крышу в одно касание своими потрохами.
0
Это да. И ты еще макросы забыл, они тоже существенный вклад вносят в метапрограммирование в плюсах.
а имеющиеся популярные либы, которые их используют, сносят крышу в одно касание своими потрохами.
О да! :)
0
Это да. И ты еще макросы забыл, они тоже существенный вклад вносят в метапрограммирование в плюсах.
Препроцессор в С/С++ это вещь, которая их убивает, IMHO. Горбатый синтаксис да еще и не имеющий с базовым языком ничего общего. Не даром все С-подобные языки от него отказались и это явно пошло им на пользу.
0
Тем не менее, в метапрограммировании на С++ он играет существенную роль.
0
За неимением ничего другого. Блин, был бы в С++ интерпретатор времени выполнения, который бы мог делать проверки текущего енваронмента и генерить код для компиляции — цены бы ему не было. А уж насколько проще и понятнее были бы либы шаблонов и говорить не приходится…
0
был бы в С++ интерпретатор времени выполнения
Может, все же, времени компиляции?
0
Точно, спасибо за поправку.
0
Как верно заметил камрад neiver , это не ООП. Для этого подхода существует название generic programming или по русски «обобщенное программирование».

P.S. «Воротит», обычно, от того, чего не понимаешь. Это повод разобраться со своими знаниями, а не крутить носом.
0
Тут еще немного есть от метапрограммирования.
0
Да, верно. Спасибо за уточнение.
0
Приветствую всех. У меня такой вопрос.
class Fill
{
public:
   Fill(unsigned width, unsigned height, uint8_t c)
         :_width(width), _height(height), _c(C)
   {}
   uint8_t operator()(unsigned x, unsigned y)const
   {
      return _c;
   }
   unsigned Width()const{return _width;}
   unsigned Height()const{return _height;}
protected:
   unsigned _width, _height;
   uint8_t _c;
};
…
// нарисуем черный квадрат 20х20 по середине экрана.
Lcd::DrawBitmap(Fill(20, 20, 1), 54, 22);
Fill — вроде как тип, а функция DrawBitmap принимает в качестве аргумента —
template<class BitmapT>
void DrawBitmap(const BitmapT &bitmap, Coord x, Coord y, Color foreground, Color /*background*/)
{
BitmapT &bitmap — т.е. вроде как ссылка на объект… и где Color foreground, Color…
И вот здесь
Fill(unsigned width, unsigned height, uint8_t c)
         :_width(width), _height(height), _c(C)
переменная с или С?
Или я не понял…
0
Fill — это класс реализующий такой же интерфейс как и битмап, и который можно нарисовать с помощью Lcd::DrawBitmap:
Lcd::DrawBitmap(Fill(20, 20, 1), 54, 22);

Здесь создаётся объект класса Fill с размерами 20х20 и цветом 1 и рисуется с помощью Lcd::DrawBitmap в определюнной позиции.
Lcd::DrawBitmap в качестве аргумента принимает ссылку на объект типа задаваемого шаблонным параметром. Тип аргумента выводится автоматически из фактически переданного в функцию параметра. Фактически в Lcd::DrawBitmap можно передать объект любого типа, у которого есть функции-члены:
Coord Height();
Coord Width();
Color operator()(Coord x, Coord y);


У foreground и background есть значения по умолчанию, их можно опускать.
переменная с или С?
Таки c — маленькая, видимо очепятка.
0
Lcd::DrawBitmap(Fill(20, 20, 1), 54, 22);
Здесь создаётся объект класса Fill с размерами 20х20 и цветом 1
т.е. пишем имя типа заместо объекта, и автоматически получаем нужный безымянный объект?
0
Да вызываем конструктор класса и получаем безымянный объект.
0
Fill(unsigned width, unsigned height, 
     Color c = Display::DefaultColor)
     :_width(width), _height(height), _c(C)
{}

Не могли бы Вы пояснить эту конструкцию, на пальцах так сказать. Не абстрактно про конструкторы, а конкретно эту. «Раскрутить» спираль. Не пойму механизм инициализации. Пошукал по учебникам, никак не достигну просветления.
0
Конструктор класса Fill принимает три параметра width, height и c. Последний имеет значение по умолчанию, равное статическому полю Display::DefaultColor. Типовой параметр Display в этом примере является KS0108, в нем DefaultColor определен как 1. То есть если вызвать конструктор Fill с двумя параметрами вместо трёх, то в третий автоматом подставится цвет по умолчанию для дисплея, то есть 1.
Далее (после двоеточия) значаниями переданными в конструктор инициализируются три поля класса. Инициализация полей по типу _width(width) в данном случае была-бы эквивалентна присваиванию в теле конструктора _width = width;
0
Инициализация полей по типу _width(width) в данном случае была-бы эквивалентна присваиванию в теле конструктора _width = width;
Ага. Вот это было непонятно, не встречал ранее такого способа.
А вообще мегареспект. Очень занятное чтиво, даже для общего развития.
0
Вот такой еще вопрос.
class Fill
{
  public:
    Fill(unsigned width, unsigned height, Color c =       
         Display::DefaultColor)	:_width(width), _height(height), _c(C)
    {}
    Color operator()(unsigned x, unsigned y)const
    {
	return _c;
    }
    unsigned Width()const{return _width;}
    unsigned Height()const{return _height;}
  protected:
    unsigned _width, _height;
    Color _c;
};
0
Случайно нажал отправить.
Т.е. если мы запишем это
Lcd::DrawBitmap(Fill(20, 20, 1), 54, 22);
то
Здесь создаётся объект класса Fill с размерами 20х20 и цветом 1
и для каждой такой записи сгенерятся свои
Color operator()(unsigned x, unsigned y)const
  {
    return _c;
  }
  unsigned Width()const{return _width;}
  unsigned Height()const{return _height;}
protected:
  unsigned _width, _height;
  Color _c;
? Или тут какой хитрый умысел?
0
Я чевой-то попутал с шаблонами. Класс у нас один, экземпляры динамически создаются. Методы пользуем класса, а переменные в нужный момент инициализируются соответствующими значениями экземпляра. Вроде так?
0
Ага. Примерно так.
0
Для демо заливка так наверное будет выглядеть?
MyPainter p;
p.Clear();
p.DrawBitmap(MyPainter::Fill(20, 20, 1), 54, 22);
0
Можно чуть проще:
MyPainter p;
p.Clear();
p.FillRect(54, 22, 20, 20);
0
Тогда следующий вопрос
template<class Display>
void Painter<Display>::FillRect(Coord left, Coord top, 
                             Coord width, Coord height)
{
  Fill fill(width, height, _color);
  DrawBitmap(fill, left, top);
}

Что за функция
Fill fill(width, height, _color);
, где ее прототип?
0
Рекомендую почитать Стефан Р. Дэвис «С++ для чайников». Подобные вопросы отпадут. Если кратко — объявляем объект fill типа Fill и вызываем конструктор с параметрами (width, height, _color).
0
Тьфу ты нуты… Вы уж прям сразу и «для чайников» :) Просто я по другому вижу мир :)
0
Чайником быть не зазорно :)
+1
Ты явно плохо знаешь С++, а эта книжка неплохо его описывает и хороша для старта.
0
Скобки сбили с толку. Не пользовался раньше такой инициализацией объектов, не привык еще. Хотя конечно, если посмотреть внимательно, видно что синтаксис не функции. Глупые вопросы еще будут :)
0
Быстрее прочитать книжку, которую я порекомендовал (она хорошая, я ее сам с удовольствием прочитал), чем ждать, пока на них все ответят :)
0
Ну это же очевидно,
Fill fill(width, height, _color);

это объявление объекта с инициализацией и т.д… Просто я проскакал, книжка тут не причем. Ну ей Богу, было бы стыдно задавать вопросы, не прочитав ни одной книжки.
0
Ну ей Богу, было бы стыдно задавать вопросы, не прочитав ни одной книжки.
Это тоже не стыдно, во всяком случае вначале. Стыдно не разбираясь в предмете учить других как они должны делать, но это другой случай :)
0
Согласен по всем пунктам. Ну и полноте флудить господа. Замусорим хорошую тему.
0
Я чайник. И раскрываю тему. Идите женщин поздравляйте :)
0
Вот такой вопрос. Не совсем в тему, но близко.
Есть базовый статический класс.
class Base
{  
  static uint8_t Get (void)
  {
    return (X + Y);
  }
}


И есть производные статические классы
class Child_1 : Base
{  
  static uint8_t  X; 
  static uint8_t  Y;
}

class Child_2 : Base
{  
  static uint8_t  X; 
  static uint8_t  Y;
}


Как сделать так, чтоб при вызове
Child_1::Get ();
Child_2::Get ();

использовались переменные данного производного экземпляра.
Чую, в базовом классе надо какие-то указатели чтоли на данные… и как то их инициализировать на лету. Полиморфизм, виртуализм… Читал, смотрел. Око видит, да зуб неймет… Или я много хочу?
0
static uint8_t Get (void)

У статической функции нет понятия экземпляра, ей не передается неявным образом указатель на this

А зачем вам именно статическая функция, почему бы просто не сделать 2 экземпляра одного класса без статической функции?
0
У статической функции

Вернее у метода а не у функции…
0
Хотелось их потом в шаблоны, как параметры передавать.
Ну пусть будет как вы говорите. Как реализовать?
0
Я имею ввиду решение в лоб, тобиш

class A {
  uint8_t  X; 
  uint8_t  Y;

public:
uint8_t Get (void)
  {
    return (X + Y);
  }
};

A a1, a2;

а1.Get();
а2.Get();


Как-то так, только дописать конструктор для инициализации X и Y.
0
Ну эт понятно. Получится, что в каждом экземпляре будет своя uint8_t Get (void). А функций дофига, и производных дофига. А нафига нам дофига :)
0
Ну эт понятно. Получится, что в каждом экземпляре будет своя uint8_t Get (void).

Нет, реализация uint8_t Get (void) будет существовать в памяти в одном экземпляре не зависимо от количества экземпляров класса.
0
Единственное, ели Вы напишете реализацию функции в хедере, компилятор ее заинлайнит.
0
реализацию функции

Млин, что то я второй раз подряд называю метод функцией.
Имеется ввиду реализация метода внутри декларации класса.
0
Получится, что в каждом экземпляре будет своя uint8_t Get (void). А функций дофига, и производных дофига. А нафига нам дофига :)
Прошу прощения, это я все про свои статические классы…
Я хотел объекты передавать как параметр в шаблоны, т.е. класс должен быть статический.
У меня шаблон статического класса с интерфейсом. Я генерирую классы, интерфейс плодится. Я подумал, может как-то вынести интерфейс в базовый класс… Ну и собственно изначальная мысль… Значит все таки нельзя?
0
передавать как параметр в шаблоны,
И при чём тут статические классы?
0
Что бы передать его как параметр-тип
0
Не статический класс что типом не является?
Его тоже передать можно.
0
Но без обьекта-то не попользуете
0
У меня шаблон статического класса с интерфейсом. Я генерирую классы, интерфейс плодится. Я подумал, может как-то вынести интерфейс в базовый класс…

А теперь понял Вашу задумку.

Понимаете на выходе (в вашем примере) мы получаем четыре статических переменных Child_1::X, Child_1::Y, Child_2::X, Child_2::Y. Посему нельзя написать одну реализацию Get(), которая бы сама «догадалась» с какой парой значений ей работать. Либо делать две реализации (для каждого класса), либо написать одну реализацию и передавать ей значения в явном виде.
0
Ну да, ну да… (грущу). А как же полиморфизм… :)
0
static uint8_t Get (void)

У статической функции нет понятия экземпляра, ей не передается неявным образом указатель на this
А пущай базовый класс будет обычным, зачем там статика… и методы соответственно. Может тут как нибудь вкрутить ядреную бабушку?
0
Дело не в базовом классе.

В конечном итоге, вы хотите вызывать статический метод Get(void). Причем, чтобы его реализация существовала в единичном экземпляре для всех классов, но в одном случае он бы складывал Child_1::X и Child_1::Y, а в другом Child_2::X и Child_2::Y, при том, что метод на входе не получает никаких параметров. Но ведь он «не знает», что вы от него хотите в данный момент. В обычные методы неявно передается указатель на экземпляр класса и там все просто, а в статический – не предаются. Это не «баг» языка, это логика работы статических методов. Их можно вызывать не создавая экземпляр класса.
Я думаю, вам стоит пересмотреть архитектуру проекта: если вам нужен полиморфизм – отказывайтесь от статических методов. Ели статические методы – передавайте параметры в явном виде.
0
Так я ж и говорою, пусть базовый класс будет не статичным, и методы в нем тоже не статичные. Обычный класс.
class Base
{  
  uint8_t Get (void)
  {
    return (X + Y);
  }
}

class Child_1 : Base
{  
  static uint8_t  X; 
  static uint8_t  Y;
}

class Child_2 : Base
{  
  static uint8_t  X; 
  static uint8_t  Y;
}
0
Во-первых, при таком наследовании вам понадобятся вириальные методы, т. к. X и Y в базовом классе не определены. Базовый класс можно сделать абстрактным, с чисто виртуальными методами для доступа к полям потомков, типа GetX() GetY().

Но это лирика, проблема не в этом. Насколько я понял, Вы хотите, в конечном итоге, вызывать именно статический метод. Без создания экземпляра объекта. Вызвать Get() у вас, соответственно, не получится.
0
А можт как-то так?
class Base
{  
  uint8_t  *X;
  uint8_t  *Y;

  Base(uint8_t  &x, uint8_t  &y) { X = x; Y = y; }

  uint8_t Get (void) { return (*X + *Y); }  
}

class Child_1 : Base
{  
  static uint8_t  X; 
  static uint8_t  Y;

  Child_1() : Base(X, Y) { }
}
0
Извиняюсь, но это не код а какой-то ужас. Помимо того что он не скомпилится (т. к. содержит множество ошибок), у вас какие-то странные действия типа присвоить указателю на uint8_t значение переменной переданное по ссылке (X = x; — слева указатель, справа значение). Даже если эти ошибки исправить – Get() не получится вызвать не создавая экземпляра.

ИМХО, Вам стоит пересмотреть именно архитектуру вашего кода.
0
Пардон муа, поспешил. Это псевдокод, так сказать идея, но каюсь, с ошибками.
Вот собственно, не мудрствуя лукаво:
#include <stdint.h>

class Base
{  
  public:
    uint8_t  X;
    uint8_t  Y;

    Base(uint8_t  x, uint8_t  y) { X = x; Y = y; }

    uint8_t Get (void) { return (X + Y); }  
};

template <uint8_t val_X, uint8_t val_Y>
class Child
{ 
  public:
    static Base A;
};

template <uint8_t val_X, uint8_t val_Y> 
          Base Child <val_X, val_Y>::A (val_X, val_Y);

typedef Child <2, 100> P1;
typedef Child <5, 200> P2;
typedef Child <10, 50> P3;

uint8_t i;
uint8_t k;
uint8_t n;

main()
{  
  i = P1::A.Get();
  k = P2::A.Get();
  n = P3::A.Get();
}

Компилируется, работает. Что скажете?
0
Так можно (через агрегацию), но ведь Вы не хотели создавать экземпляры класса, в с таком виде они создаются внутри обертки (P1::A, P2::A, P3::A).
По сути мы вернулись к варианту

Base A1,A2;
A1.Get();
A2.Get();


Только классы создаются как статические поля внутри класса обертки. Работать будет, но теперь это не наследование и поля обертки (Child) придётся передавать в агрегированный класс в явном виде и т. д. Но, если Вас такой интерфейс устраивает – то почему бы и нет…
0
Ну да. Обертка, чтоб в шаблон параметром можно было запихать. Ну и удобно, объявил один раз и пользуй…
но ведь Вы не хотели создавать экземпляры класса
дык не знаю как сделать, потому и написал :), но Вы вроде говорите, что нельзя…
0
На сколько шустро справлялась мега с демопримером?
0
Скорость работы здесь зависит в основном от дисплея. В демо примере, насколько я помню, получилось где-то 20-30 кадров в секунду.
0
Однако… Очень хороший показатель. На АРМ у меня некоторые кадры рисуются чуть более 100 мс, а времянка на пределе дисплея. Надо это проверить, найти узкое место.
0
Сама перерисовка дисплея занимает около 25 мс. Это если картинку рисовать. Если графические примитивы по пикселям, то в разы дольше. А вообще на этом дисплее пиксели на экране не успевают обновляться быстрее чем 70-10 раз в секунду.
0
Согласен!) Вообще, я привел длительность исполнения ф-ции Update(). За это время дисплей физически перерисовывается по нескольку раз =)
0
На мк, имеющих 1-2 свободных кб оперативки можно очень ускорить графику. Очень жаль тратить сотни МГц современных армов на популярный и распространённый, но такой медленный дисплей.
0
Совершенно согласен. Фреймбуфер под этот дисплей нужен всего 1 кб. Отправку буфера в дисплей можно поручить таймеру с ДМА и их прерываниям. Таймер дергает строб и генерирует ДМА запросы, ДМА кидает данные в порт.
0
Да, если так можно на большинстве мк, то очень выгодное решение получится.
Я уже что-то начал обдумывать с внешними регистрами и SPI/USART, и, возможно, ДМА — кажется последовательного интерфейса должно хватить на заливку фреймбуфера.
По драйверу этого дисплея, задержек и портов для 1986ВЕ9х у меня есть совсем небольшие небольшие доработки и рабочий тестовый пример для платки от LDM. Возможно Вас заинтересует добавить в репозиторий.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.