Самодельный передатчик (часть 2)

EagleB3

Тем, что в EEPROM будет записан “мусор”. И, возможно, обнаружится это только когда модель уже в воздухе. Ну, допустим, звезды так сложились, что у тебя на модели №3 перекосило микширование РВ с элеронами, и только в режиме посадки. А перед взлетом ты это не проверял. И взлетел, и летал, и потом пошел на посадку…

Но:

  1. Порче при таких раскладах подвержена обычно самая первая ячейка EEPROM. Что можно сделать, чтобы от этой порчи не пострадать написал, например, Nick_Shl вот здесь
  2. Можно (и полезно, и нужно) контролировать содержимое EEPROM подсчитывая код CRC. Как этого можно сделать написал Nick_Shl там же. Focus и MSV собирались сделать это в своих прошивках, но, по-моему, так и не сделали. И (по-моему же) в выложенных здесь прошивках это никто не сделал.
  3. За-ради сохранности EEPROM полезно быстро заканчивать переходные процессы выключения микроконтроллера. А для этого нужно ставить электролитические конденсаторы в фильтрах с емкостью не более 10 мкф. Об этом здесь уже писалось, но кто именно писал - не помню, сорри.
  4. Аппаратно (как здесь писал и уже не раз Aleksey Gorelikov, и один из разов писал он прямо на этой странице чуть выше) шунтировать выключатель питания аппы ключиком-мосфетом. Перед началом записи в память микроконтроллер включает шунт; если в этот момент ты выключишь питание - ничего не произойдет. А когда запись будет завершена, микроконтроллер отключает шунт, и если к этому времени выключатель питания разомкут - аппа выключится. Как вариант можно сделать включение аппы двумя кнопками (“Вкл”/“Выкл”) или нефиксирующимся тумблером на три положения - для включения двинул тумблер в сторону “Вкл”, подалось питание на контроллер, контроллер зашунтировал тумблер мосфетом, пискнул - можно тумблер отпускать. Работаем и периодически опрашиваем тумблер (как и любой другой выключатель). Если тумблер двинули в состояние “Выкл”, проверяем это состояние в течение нескольких секунд (как бы “антидребезг”, только более долгий) и если состояние устойчиво, то заканчиваем запись данных моделей в EEPROM, считаем CRC, записываем его в EEPROM и и отпускаем шунт. Аппа выключилась. Пищать с какого-то момента проверки устойчивости состояния тумблера “Выкл” (типа, сигнал получен; отпусти тумблер, если это по ошибке) и до момента отключения тревожно-художественно-особым писком - по желанию.

Таблетка от компа - от лукавого. Она может бекапить устройства с микропотреблением; микроконтроллер к таковым можно отнести весьма условно, и только в особых режимах. Впрочем, “Хозяин - барин”.

----------------------------------------------

А вот (может, кому будет интересно…) как встают кнопки SWT-1 в качестве триммеров на джойстики аппы Sanwa VG400. Конструкция всех 4-х площадок потенциометров абсолютно идентичная.

Клеились на эпоксидку “ЭДП” (на первом фото хорошо видны следы ее растекания по площадке, а мазалась площадка клеем аскетично, без излишеств; надеюсь, это признак сильного поверхностного натяжения - значит, и держаться должно крепко по идее); площадки в месте приклеивания кнопок предварительно обезжиривались антисиликоном и ацетоном, потом зачищались шкуркой (зерно примерно 600) до шероховатости. Бока кнопок обезжиривались, потом притирались к плоской поверхности на шкурке (зерно примерно 1000) - чтобы ничего наружу не торчало (верхняя металлическая пластинка, следы пресс-формы на корпусе кнопки) и было шероховатым.

Паять собираюсь легкоплавким припоем (ок.100 градусов; эпоксидка “ЭДП”, насколько я помню, выдерживает до 300 градусов) - иначе припаял бы провода до приклеивания

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

msv

У меня при выключении портился EEPROM на первых порах, пока не включил boden. Причем портились случайные группы ячеек, вовсе не первые… И даже не в моменты записи… С тех пор как включил boden ни одного сбоя не замечал. Поэтому так и не дошли руки написать контроль целостности eeprom. Хотя конечно это надо бы на всякий сделать, работы минут на 10…
Лично у меня сложилось впечатление, что проблема нулевой ячейки- это качующий из форума в форум миф… Ну может и была когда проблемная партия мег с этим глюком, но ни одного официального документа на эту тему не попадалось… В моей версии нулевая ячейка eeprom- первый символ в названии первой модели. У кого нибудь она портилась?

EagleB3
msv:

Лично у меня сложилось впечатление, что проблема нулевой ячейки- это качующий из форума в форум миф… Ну может и была когда проблемная партия мег с этим глюком, но ни одного официального документа на эту тему не попадалось…

Да, вполне может быть, что это предание…

msv:

С тех пор как включил boden ни одного сбоя не замечал.

BOD-рулез. А уж встроенный-то грех не использовать.

dollop
msv:

В моей версии нулевая ячейка eeprom- первый символ в названии первой модели. У кого нибудь она портилась?

Да, у меня портилась. Но это случилось только при очень частом вкл/выкл передатчика (контакт по питанию фиговый был) Но там и не такое могло испортиться 😉

EagleB3

Ааааа…

Тогда с очевидностию вытекает следующий вопрос: портилась при этом ТОЛЬКО первая ячейка? Или и другие тоже?

dollop

Ну, как говориться, вопрос конечно интересный. Но ответа не знаю. То что первая буква в названии первой модели изменилась - это точно. Забито было “Manon”, а стало “первуюбуквунепомнюanon”. Помню, удивился. Ничего другого я не заметил, и на всякий случай все же обнулил еепром.

abalex
msv:

Лично у меня сложилось впечатление, что проблема нулевой ячейки- это качующий из форума в форум миф… Ну может и была когда проблемная партия мег с этим глюком, но ни одного официального документа на эту тему не попадалось…

Давным-давно у Атмела был такой AT90S2313, и для него:

AT90S2313 Rev. B/C Errata Sheet ( описание известных проблем ):

3. Reset During EEPROM Write
If reset is activated during EEPROM write, the result is not what should be expected. The EEPROM write cycle completes as normal, but the address registers reset to 0. The result is that both the address written and address 0 in the EEPROM can be corrupted.

Problem Fix/Workaround
Avoid using address 0 for storage, unless you can guarantee that you will not get a reset during EEPROM write.

По-русски: если EEPROM пишется и пришел сброс, то регистр адреса EEPROM будет сброшен в 0, и часть процесса записи ( до сброса ) будет в нужную ячейку, а часть ( после сброса ) - в 0-ю, в итоге обеим может поплохеть. И предлагается не хранить в 0-й ячейке ничего важного.

Но у этого процессора при сбросе обнулялся регистр адреса EEPROM, а потом для всех более новых ( включая меги ) это обнуление убрали. Хотя, например, при включении питания в этом регистре м.б. и 0 будет ( фирма не гарантирует, но все-таки… ), а если включение нечеткое ( плохой контакт и т.п. ) - процессор может взглючить и инициировать запись в EEPROM, и как раз в 0-ю ячейку.
Поэтому я на всякий случай после каждого чтения EEPROM ( и после записи, разумеется, дождавшись ее конца ) обнуляю регистр адреса. Понятно, это всего лишь уменьшает вероятность, но все-таки лишняя подпорка, которая м.б. когда-нибудь и удержит падающий забор ( или самолет ).

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

msv

ОК, спасибо! Теперь понятно откуда ноги растут.
Все же надеюсь, что BOD справится со своей задачей и не даст МК творить что попало…

Networx
Aleksey_Gorelikov:

Вадим, тебе имеет резон срисовать схему тренер\ученик с твоей аппы. Там достаточно интересно все реализовано. Передатчик включается при подсоединении шнура, Выключатель питания продублирован ключем и аппа отключается не тогда, когда ты тумблером щелкнул, а когда проц закончит запись в епром и в “спячку” ляжет.

К томуж и детали у тебя все есть. 😃 Ничего покупать не придется.

2 Aleksey_Gorelikov, подскажите где посмотреть по какой схеме и как подключаетса выключатель питания, на дополнительном ключе!
ПС: ресую печатку под себя и хочу сразу добавить туда схему выключения!
Спасибо!

msv:

У меня при выключении портился EEPROM на первых порах, пока не включил boden. Причем портились случайные группы ячеек, вовсе не первые… И даже не в моменты записи… С тех пор как включил boden ни одного сбоя не замечал. Поэтому так и не дошли руки написать контроль целостности eeprom. Хотя конечно это надо бы на всякий сделать, работы минут на 10…

Ех, неумею я писать коды!
Поетому буду ждать и надеятса на Вас!
Спасибо!

abalex
msv:

Все же надеюсь, что BOD справится со своей задачей и не даст МК творить что попало…

Полусамодельная зарядка для аккумуляторов на ATmega32, коммерческая прошивка ( автор - Vadim Kushnir, aka Vad64 ), сделано прилично, так что, видимо, и все фьюзы записаны как надо. Недавно менял в ней индикатор, а у него габариты чуть больше, при завинчивании продавило изоляцию и входное питание замкнуло на землю ( с дребезгом, т.е. несколько раз ). В итоге - зарядка попросила перекалибровку, т.е., видимо, слетела EEPROM. После перекалибровки работает.
Хотя эксперимент не совсем чистый - я еще когда ее собирал, видимо, коротнул один из выводов порта куда-то, ему слегка поплохело, и м.б. не только ему, а еще чему-нибудь внутри процессора ( хотя вроде все работало ).
Да и, теоретически, при бросках питания может перекосоглючить что-то в самом аппаратном блоке, который запись EEPROM отрабатывает, и куда сигнал сброса по идее не подключен, т.к. запись в EEPROM продолжается и во время сброса.
Так что не знаю…

М.б. процессор от остального питания диодом отделять и емкость на X000 мкф вешать, чтобы не было резких бросков при любых включениях-выключениях ( в т.ч. при плохом контакте аккумуляторов и т.п. ), BOD срабатывал бы в комфортных условиях, и после его срабатывания - заряда емкости хватало бы еще и на окончание м.б. начатой записи в EEPROM. Ну и после сброса - в программе паузу ( 10 мс ? ) делать и потом еще проверять окончание записи в EEPROM до какой-либо работы с ее регистрами.

EagleB3

…Я своим киндерам сделал “бюджетатор” для контроля времени работы за компьютером. Дивайс определяет пользователя по таблетке Touch Memory (DS1990); пока таблетка в считывателе и если баланс положительный - разрешает включить блок питания компьютера и списывает время в балансе. Питается оно от дежурки компьютера, часики (DS1307) на бекапе CR2032.
Так вот, чтобы мое жулье не пыталось пороть остаток бюджета варварскими методами (выдергивая вилку компьютера из розетки) я внешний БОД (4,7В) повесил за входным фильтром, и отсюда же питается ЖКИ и прочая индикация. После этой точки идет шоттка, за ней - кондюк на 3000 мкф. И питается с этого кондюка один только контроллер. Внутренний БОД (4,2В) висит на питании контроллера. Сигнал с внешнего БОДа запускает внешнее прерывание. По этому прерыванию экстренно сохраняется EEPROM и запускается процедура выключения.

Пока не слетало…

dmitryu

Зачем EEPROM при наличии 1307 и CR2032 ? 😃

EagleB3

А где же на каждого бюджетируемого члена семьи хранить размер ежедневного бюджета, остаток бюджета “на сегодня”, штраф (за “аварийное” отключение компьютера), да и само понятие “сегодня”?

Можно было бы в таблетке, но у нашего домофона таблетки склеротичные (DS1990).

v61

Чего-то тема в сторону домофонов поехала…

У кого-нибудь есть опыт питания кодера от 3.3 Вольт?
С модулем радиоканала на 2.4 ГГц вроде логично питать все усройство от 1S

dmitryu

DS1307 имеет 56 байт ОЗУ, питаемого от батарейки, не говоря уже о встроенном календаре.

EagleB3

… А Mega8535 имеет 512 байт EEPROM. Что же его, ножовкой отпиливать, раз DS1307 в схеме есть? 😁

К тому же мои террористы уже дважды на дивайс покушались: первый раз с перемычками, второй раз - с кусачками. Насмотрелись боевиков, понимаешь. 😅

А если ты про количество циклов перезаписи, так 100.000 раз мне более чем достаточно. Страниц 5…10 назад на эту тему уже один холивар вели; КМК, второй не нужен.

v61:

У кого-нибудь есть опыт питания кодера от 3.3 Вольт?
С модулем радиоканала на 2.4 ГГц вроде логично питать все устройство от 1S

Если делать по уму, то надо:

  • либо перепихивать его на низковольтный контроллер, работающий на 12МГЦ;
  • либо ставить преобразователь StepUp, раскачивать 1S до 5V и питать им штатную Atmega128;
  • либо брать 2S/3S и ставить преобразователь StepDown;
  • либо брать ATMega 128L и надеяться, что при питании 3,3В она вытянет 12МГц (но вот это уже не по уму, КМК!).

Себестоимость решения с преобразователями - от 50 рублей (1 канал на MC34063) до… ну, скажем, 400 рублей (два канала на LM2674).

Лично я предпочитаю вариант №3. Доводы:

  • В любом поле аккумуляторы 2S и 3S найдутся. 1S - не факт;
  • Можно питаться хоть от борта грузовика (24В);
  • “Тренер-Ученик”. Интерфейс у апп где-то что-то в районе 8…10 вольт. Ты уверен, что “Ученик” поймет 3,3В?
  • Имея 9 вольт (с аккумулятора) и 5 вольт (со стабилизатора), ты можешь добавлять в аппу практически что угодно. Имея 4,3В…3,3В - КМК, существенно меньше.
DmitryS

Отстал немного 😃. К посту 2160. Не самый лучший вариант, не по инженерному. Эпоксидка, не самый лучший клей для “китайской” пластмассы, слишком хрупкий. С “заду” кнопки лучше было бы приклеить упор, или надфилем проточить углубление под корпус кнопки а потом вклеить на циакрин.

EagleB3

Я думал и про первое, и про второе.
И решил пока не делать.

Кнопка триммера - не самое ответственное место; даже если она и отвалится, то большой беды случиться не должно. А сделать с проточками или упором можно и в процессе ремонта.

С другой стороны:

  1. Кнопка довольно плоская, собственно толкатель у кнопки - узкий. Заглубить кнопку - и он будет контактировать с приводом уже не всей толщиной, а половиной.
  2. Фото было сделано после того, как эпоксидка нанесена в один слой. После его затвердевания (где-то через час-полтора после нанесения) было капнуто дополнительно сзаду и с боков, так что своеобразный упор сделан. Отлит из эпоксидки. Надеюсь, что этого хватит. Разница в плечах рычага незначительная, разница в усилиях на пальце и на кнопках небольшие. Причем палец трет накатку на триммере “на сдвиг”.
DD

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

Nick_Shl
EagleB3:

Можно (и полезно, и нужно) контролировать содержимое EEPROM подсчитывая код CRC. Как этого можно сделать написал Nick_Shl там же.

Могу написать ещё раз 😃. Сначала скачиваем файл с функциями расчёта CRC. Затем добавляем следующий кусок кода туда, где происходит модификация EEPROM:

// *****************************************************************************
// ***   Мусорная переменная - нулевая ячейка подвержена самопорче   ***********
// *****************************************************************************
static eeprom unsigned char EEPROM_TRASH @(0x0000);

// *****************************************************************************
// ***   Переменная для хранения CRC, размещена в конце EEPROM   ***************
// *****************************************************************************
static eeprom unsigned short EEPROM_CRC @(0x1000 - sizeof(unsigned short));

// *****************************************************************************
// ***   Определения для удобства   ********************************************
// *****************************************************************************
// Адрес начала области EEPROM по которой будет считатся CRC
// Равен нулевому адресу EEPROM(0) + размер мусорной переменной
#define EEPROM_CRC_START ((unsigned char eeprom *)(0x0000 + sizeof(EEPROM_TRASH)))
// Размер области EEPROM по которой будет считатся CRC
// Равен размеру EEPROM - размер мусорной переменной - размер переменной с CRC
#define EEPROM_CRC_SIZE (0x1000 - sizeof(EEPROM_TRASH) - sizeof(EEPROM_CRC))

// *****************************************************************************
// ***   Подсчет CRC EEPROM и его записть   ************************************
// *****************************************************************************
static inline void Recalc_EEPROM_CRC(void)
{
    // Считаем новый EEPROM CRC и записываем его в EEPROM
    EEPROM_CRC = Crc16_eeprom(EEPROM_CRC_START, EEPROM_CRC_SIZE);
}

// *****************************************************************************
// ***   Подсчет CRC EEPROM   **************************************************
// *****************************************************************************
inline unsigned short Get_EEPROM_CRC(void)
{
    // Считаем EEPROM CRC
    return(Crc16_eeprom(EEPROM_CRC_START, EEPROM_CRC_SIZE));
}

// *****************************************************************************
// ***   Проверка корректности CRC EEPROM   ************************************
// *****************************************************************************
inline unsigned char Is_EEPROM_CRC_Correct(void)
{
    // Считаем EEPROM CRC и сравниваем с записанным
    // Если совпадает - возвращаем ИСТИНУ
    if(EEPROM_CRC == Crc16_eeprom(EEPROM_CRC_START, EEPROM_CRC_SIZE)) return(TRUE);
    // Иначе - ЛОЖ
    else return(FALSE);
}

После каждой записи в EEPROM нужно вызывать функцию Recalc_EEPROM_CRC(). Поэтому часто записи лучше не делать 😃. Это относится к триммерам.
При запуске делаем что-нибудь вроде этого:

    if(Is_EEPROM_CRC_Correct() == FALSE)
    {
        // Выводим запрос подтверждения сброса
        MsgBoxF("EEPROM CRC ERROR\nReset to factory\ndefaults ?", &Font_6x8, "ERROR", NULL);

        // Ждем отпускания кнопок, если были нажаты
        while(HB_ENTER || HB_BACK);
        // Ждем нажатия кнопок
        while(!HB_ENTER && !HB_BACK);
        // Eсли нажали ВВОД - сбрасываем
        if(HB_ENTER) TX_Reset();
    }
EagleB3:

либо брать ATMega 128L и надеяться, что при питании 3,3В она вытянет 12МГц (но вот это уже не по уму, КМК!).

По уму было бы запустить не на 12, а на 8 МГц. Вот только во всех версиях что я видел это сложновато 😃. На этот счёт я тоже уже высказывался. Сначала делаем определения:

// *****************************************************************************
// ***   Общие определения   ***************************************************
// *****************************************************************************
#define AVR_Clock_Freq 16000000              // Частота работы МК
#define Main_Timer_Clock  (AVR_Clock_Freq/8) // Частота работы таймера

А затем в математике используем так:

    // Длительность импульса:
    //          значение канала(+-0,5 мс) + центральное положение(1,5 мс)
    output[CH] = output[CH]               + (int)(Main_Timer_Clock/666.666);
    // приведение к int нужно для оптимизации: вычисленное на этапе компиляции число использовать как int
    // забавно получилось: 1,5 мс это как раз 1/666,6(6) секунды...

    // Если получившийся импульс меньше 0,9 мс (1/1111 сек) - ограничиваем
    if(output[CH] < Main_Timer_Clock/1111) output[CH] = Main_Timer_Clock/1111;

    // Если получившийся импульс больше 2,1 мс (1/476 сек) - ограничиваем
    if(output[CH] > Main_Timer_Clock/476) output[CH] = Main_Timer_Clock/476;

В случае изменения частоты работы достаточно изменить одно определение. В случае изменения делителя таймера - другое 😃. Боятся за падение производительности не стоит - компилятор все константные выражения вычисляет в момент компиляции.

v61:

С модулем радиоканала на 2.4 ГГц вроде логично питать все усройство от 1S

А модуль какой? Если для “апгрейда” 35/72 МГц, то он рассчитан на большое напряжение и у него внутри стоит линейный стабилизатор. Если вы ему дадите 3.3 В - думаю он вообще не запустится…

Nick_Shl

Продолжаем. В личку поступил вопрос:

VRV

Если есть возможность, поделитесь плз куском по созданию иерархического меню(или ссылками).

Отвечаю.
Заголовок “движка”(не весь, только то, что относится к главной функции меню):

/*******************************************************************************
*  UI_Engine.h
*
*  Радиоуправление: Движок пользовательского интерфейса, заголовок
*
*       Copyright (c) 2009 Nick Shl
*           All rights reserved.
*
*
*  Изменения:
*
*  Mar 25, 2009  Nick_Shl  Первоначальная версия
*
*/// ***************************************************************************

// *****************************************************************************
// ***   Описание пункта меню   ************************************************
// *****************************************************************************
typedef flash struct typeMenuButton
{
    flash char *   str;            // Название пункта меню
    void (*fun) (char AddParam);   // Функция вызываемая при нажатии ВВОД
    char * (*GetStr) (char * Buf, char AddParam); // Функция вывода дополнительной информации
    char AddParam;                 // Дополнительный параметр, передаваемый в функции
} MenuButton;

// *****************************************************************************
// ***   Описание меню   *******************************************************
// *****************************************************************************
typedef struct typeMenuPad
{
    flash char *    CaptionStr; // Название меню, выводимое в заголовке
    FontProfile *  CaptionFont; // Указатель на профайл шрифта заголовка
    FontProfile *     MenuFont; // Указатель на профайл шрифта пунктов меню
    flash MenuButton * Buttons; // Указатель на массив описания пунктов
    char            ButtonsNum; // Количество пунктов меню
    char            CurrentPos; // Выбранный пункт
    void      (*Before) (void); // Функция вызываемая перед отображением меню
    void      (*After)  (void); // Функция вызываемая перед отображением меню
} MenuPad;

// *****************************************************************************
// ***   Главный цикл меню   ***************************************************
// *****************************************************************************
void MenuCycle(MenuPad * Menu);

А это сама главная функция движка меню:

/*******************************************************************************
*  UI_Engine.c
*
*  Радиоуправление: Движок пользовательского интерфейса
*
*       Copyright (c) 2009 Nick Shl
*           All rights reserved.
*
*
*  Изменения:
*
*  Mar 25, 2009  Nick_Shl  Первоначальная версия
*
*/// ***************************************************************************
#include "UI_Engine.h"

// *****************************************************************************
// ***   Главный цикл меню   ***************************************************
// *****************************************************************************
void MenuCycle(MenuPad * Menu)
{
    int i;
    unsigned char Color;
    unsigned char MenuHeaderH = 0;
    unsigned char StartPos = 0;
    unsigned char MenuCount = 0;
    unsigned char Kbd = 0;
    unsigned char ScrollHeight = 0;
    unsigned char ScrollBarHeight = 0;
    unsigned char ScrollBarWidth = 0;
    unsigned char ScrollBarStart = 0;
    unsigned char strLen;
    unsigned char bufLen;
    FontProfile * CaptionFont = Menu->CaptionFont;
    FontProfile * MenuFont = Menu->MenuFont;

#ifdef DEBUG
    printf("MenuCycle();\r");
    delay_ms(1);
#endif

    // Проверки
    if(Menu == NULL) return;          // Если не передали основную структуру - выходим
    if(Menu->Buttons == NULL) return; // Если не передали структуру кнопок - выходим
    if(Menu->ButtonsNum == 0) return; // Если количество пунктов меню равно 0 - выходим
    if(CaptionFont == NULL) CaptionFont = &Font_8x12; // Если не передали шрифт заголовка - устанавливаем по умолчанию
    if(MenuFont == NULL) MenuFont = &Font_6x8;        // Если не передали шрифт пунктов - устанавливаем по умолчанию

    MenuHeaderH = Font_8x8.H + CaptionFont->H;

    // Подсчет количества пунктов меню, помещающихся на экране
    MenuCount = (HeightS - MenuHeaderH - 3) / MenuFont->H;
    // Если на экране помещается больше чем есть в меню - ограничиваем
    if(MenuCount > Menu->ButtonsNum) MenuCount = Menu->ButtonsNum;

    // Высота полосы прокрутки: высота экрана - высота заголовка меню
    ScrollHeight = HeightS - (MenuHeaderH + 2 + 2);

    // Высота индикатора полосы прокрутки:
    // (количество отображаемых пунктов меню * количество доступных точек) / количество пунктов меню
    ScrollBarHeight = ((unsigned int)MenuCount * ScrollHeight) / Menu->ButtonsNum;
    // Если размер индикатора больше половины полосы прокрутки - приравниваем к половине
    if(ScrollBarHeight > ScrollHeight/2)
    {
         ScrollBarHeight = ScrollHeight/2;
    }

    // Ширина индикатора полосы прокрутки:
    ScrollBarWidth = 4;//MenuFont->W - 2;

    do{
        // Очищаем экран
        gfx_ClearBuf();

        // Отрисовываем заголовок меню
        gfx_SetXY(0, Font_8x8.H);
        gfx_PutFlashStr(Menu->CaptionStr, 0xFF, CaptionFont);
        gfx_DashLine(0, MenuHeaderH, WidthS, MenuHeaderH);

        // Если вызывающему нужно что либо выполнить какие-либо действия - вызываем
        if(Menu->Before != NULL) Menu->Before();

        do
        {
            // Очищаем место под заголовок с номером модели, иконкой и режимом
            gfx_FillRect(0, 0, WidthS - 1, Font_8x8.H, 0x00);

            gfx_SetXY(0, 0);
            // Номер модели
            sprintf(tmpBuf, "%d", Settings.ModelNum + 1);
            gfx_PutStr(tmpBuf, 0xFF, &Font_8x8);
            // Сдвигаем позицию рисования на 4 точки
            gfx_ChangeXY(3, 0);
            // Значек верта
            if(CurModel.type == TYPE_HELI)
            {
                gfx_Char(125, 0xFF, &Font_8x8);
                gfx_Char(126, 0xFF, &Font_8x8);
            }
            // Значек самолета
            if(CurModel.type == TYPE_PLANE)
            {
                gfx_Char(127, 0xFF, &Font_8x8);
                gfx_Char(128, 0xFF, &Font_8x8);
            }
            // Сдвигаем позицию рисования на 4 точки
            gfx_ChangeXY(3, 0);
            // Название режима
            gfx_PutStr(CurModel.Mode[FLY_MODE].name, 0xFF, &Font_6x8);

            // Стартовая позиция индикатора полосы прокрутки:
            // высота заголовка меню + (текущяя позицию в меню * количество доступных точек для индикатора) / количество пунктов меню
            ScrollBarStart = MenuHeaderH + 2 + 1 + (Menu->CurrentPos * (unsigned int)(ScrollHeight - ScrollBarHeight)) / (Menu->ButtonsNum - 1);
            // Если выбран последний пункт меню устанавливаем индикатор в самый низ,
            // т.к. при подсчете может потерятся точность и появится дырка
            if(Menu->CurrentPos == Menu->ButtonsNum - 1)
            {
                ScrollBarStart = ScrollHeight - ScrollBarHeight + MenuHeaderH + 2 + 1;
            }

            // Очищаем место под полосу прокрутки
            gfx_FillRect(0, MenuHeaderH + 2, ScrollBarWidth - 1, HeightS - 1, 0x00);
            // Отрисовываем полосу прокрутки
            gfx_Rectangle(0, MenuHeaderH + 2, ScrollBarWidth - 1, HeightS - 1, 0xFF);
            // Отрисовываем индикатор полосы прокрутки
            gfx_FillRect(0, ScrollBarStart, ScrollBarWidth - 1, ScrollBarStart + ScrollBarHeight, 0xFF);

            // Отрисовываем пункты меню
            for(i = StartPos; i < StartPos + MenuCount; i++)
            {
                // Количество символов в строке меню
                strLen = (WidthS - (ScrollBarWidth + 2)) / MenuFont->W;

                // Заполняем временный буфер пробелами
                memset(tmpStr, ' ', sizeof(tmpStr));

                // Если установлена функция возврата доп. информации
                if(Menu->Buttons[i].GetStr != NULL)
                {
                    // Получаем доп. информацию в буфер
                    Menu->Buttons[i].GetStr(tmpBuf, Menu->Buttons[i].AddParam);
                    // Считаем её размер
                    bufLen = strlen(tmpBuf);
                    // Если её размер больше количества символов в строке меню - ограничиваем
                    if(bufLen > strLen) bufLen = strLen;
                    // Копируем доп. информацию в строку меню, с выравниванием по правому краю
                    strcpy(tmpStr + (strLen - bufLen), tmpBuf);
                }

                // Копируем название пункта меню из флеша в временный буфер
                strcpyf(tmpStr, Menu->Buttons[i].str);
                // Если длинна названия меньше длинны строки помещающейся на экране - ставим пробел склеивая строчки
                if(strlen(tmpStr) < strLen) tmpStr[strlen(tmpStr)] = ' ';
                // Обрезаем строку на границе экрана(защита на случай превышения названием количества символов в строке меню)
                tmpStr[strLen] = '\0';

                // Устанавливаем место рисования
                gfx_SetXY(ScrollBarWidth + 2, MenuFont->H * (i - StartPos) + MenuHeaderH + 2);
                // Задаем цвет - нужно для выбранного пункта
                if(Menu->CurrentPos == i) Color = 0x00;
                else                      Color = 0xFF;
                // Рисуем строчку
                gfx_PutStr(tmpStr, Color, MenuFont);
            }
            // Обновляем экран
            gfx_Refresh();

            // Ждем отпускания кнопок
            WaitEmptyButtons(0);
            // Ждем нажатия кнопок
            Kbd = WaitButtonPress(0);

            // Изменяем позицию курсора, если нажато ВВЕРХ или ВНИЗ
            if((Kbd & B_UP)   && (Menu->CurrentPos > 0))                    Menu->CurrentPos--;
            if((Kbd & B_DOWN) && (Menu->CurrentPos < Menu->ButtonsNum - 1)) Menu->CurrentPos++;

            // Если курсор вышел за границы экрана - сдигаем меню на один пункт
            if(Menu->CurrentPos < StartPos)                 StartPos--;
            if(Menu->CurrentPos > StartPos + MenuCount - 1) StartPos++;
        }
        while(!(Kbd & (B_BACK | B_ENTER)));

        // Если вызывающему нужно что либо выполнить какие-либо действия - вызываем
        if(Menu->After != NULL) Menu->After();

        // Если нажали ВВОД и у выбранного пункта установлен обработчик
        if((Kbd & B_ENTER) && (Menu->Buttons[Menu->CurrentPos].fun != NULL))
        {
            // Ждем отпускания кнопок
            WaitEmptyButtons(0);
            // И вызываем обработчик
            Menu->Buttons[Menu->CurrentPos].fun(Menu->Buttons[Menu->CurrentPos].AddParam);
        }
    }
    while(!(Kbd & B_BACK));

    // Ждем отпускания кнопок
    WaitEmptyButtons(0);
}

А теперь пример использования. Главное меню:

// *****************************************************************************
// ***   Главное Меню   ********************************************************
// *****************************************************************************

// ***   Пункты меню   *********************************************************
flash MenuButton TestMenuButtons[] =
{{"Model",    ModelMenu,    NULL},
 {"Controls", ControlsMenu, NULL},
 {"Channels", ChannelsMenu, NULL},
 {"Timer",    TimerMenu,    NULL},
 {"Options",  OptionsMenu,  NULL},
 {"IO Test",  IO_Test_AP,   NULL},
 {"Demo",     Demo_AP,      NULL}};

// ***   Функция меню   ********************************************************
void MainMenu(void)
{
    // Формируем структуру меню
    MenuPad Pad = {"Main Menu", &Font_8x12, &Font_6x8, TestMenuButtons, NumberOf(TestMenuButtons), 0, NULL, NULL};
    // Вызываем обработчик меню
    MenuCycle(&Pad);
}

Это просто. А теперь пример поинтереснее: реализация на этом движке микшера. Используется дополнительный параметр из структуры:

// *****************************************************************************
// ***   Меню каналов(микшера)   ***********************************************
// *****************************************************************************

// ***  Текущий настраиваемый канал   ******************************************
static char MixerCurrentChannel;

// ***  Изменение настройки микшера   ******************************************
void MixerChangeSetting(char AddParam)
{
    // Вызываем окно изменения значения
    EditNumDlg(&CurModel.Mode[FLY_MODE].Chanels[MixerCurrentChannel][AddParam], PTR_CHAR, -100, 100, 4, "Mixer");
}

// ***   Получение строки настройки микшера   **********************************
char * MixerGetStr(char * Buf, char AddParam)
{
    // Формируем в буфере текущее значение
    sprintf(Buf, "%d", CurModel.Mode[FLY_MODE].Chanels[MixerCurrentChannel][AddParam]);
    // Возвращаем указатель на переданный буфер
    return(Buf);
}

// ***   Список пунктов настройки микшера для канала   *************************
flash MenuButton MixerMenuButtons[] =
{{"Ailerons",  MixerChangeSetting, MixerGetStr, CTRL_AIL},
 {"Elevator",  MixerChangeSetting, MixerGetStr, CTRL_ELE},
 {"Throttle",  MixerChangeSetting, MixerGetStr, CTRL_THR},
 {"Rudder",    MixerChangeSetting, MixerGetStr, CTRL_RUD},
 {"SW 1",      MixerChangeSetting, MixerGetStr, CTRL_SW1},
 {"SW 2",      MixerChangeSetting, MixerGetStr, CTRL_SW2},
 {"SW 3",      MixerChangeSetting, MixerGetStr, CTRL_SW3},
 {"Aux 1",     MixerChangeSetting, MixerGetStr, CTRL_AUX1},
 {"Virtual 1", MixerChangeSetting, MixerGetStr, CTRL_V1},
 {"Virtual 2", MixerChangeSetting, MixerGetStr, CTRL_V2},
 {"Trim",      MixerChangeSetting, MixerGetStr, CTRL_TRIM}};

// ***   Меню настройки микшера для канала   ***********************************
void ChannelsMixerMenu(char AddParam)
{
    // Формируем структуру меню
    MenuPad Pad = {"Mixer", &Font_8x12, &Font_6x8, MixerMenuButtons, NumberOf(MixerMenuButtons), 0, NULL, NULL};
    // Запоминаем номер текущего управляющего элемента
    MixerCurrentChannel = AddParam;
    // Вызываем обработчик меню
    MenuCycle(&Pad);
}

// ***   Список каналов для настройки микшера   ********************************
flash MenuButton ChannelsMenuButtons[] =
{{"CH 1", ChannelsMixerMenu, NULL, 0},
 {"CH 2", ChannelsMixerMenu, NULL, 1},
 {"CH 3", ChannelsMixerMenu, NULL, 2},
 {"CH 4", ChannelsMixerMenu, NULL, 3},
 {"CH 5", ChannelsMixerMenu, NULL, 4},
 {"CH 6", ChannelsMixerMenu, NULL, 5},
 {"CH 7", ChannelsMixerMenu, NULL, 6},
 {"CH 8", ChannelsMixerMenu, NULL, 7}};

// ***   Меню выбора каналов для настройки микшера   ***************************
void ChannelsMenu(char AddParam)
{
    // Формируем структуру меню
    MenuPad Pad = {"Channels", &Font_8x12, &Font_6x8, ChannelsMenuButtons, NumberOf(ChannelsMenuButtons), 0, NULL, NULL};
    // Если количество каналов модели меньше чем максимальное количество - устанавливаем их,
    // что бы не отображать отсутствующие у модели каналы
    if(Pad.ButtonsNum > CurModel.num_ch) Pad.ButtonsNum = CurModel.num_ch;
    // Вызываем обработчик меню
    MenuCycle(&Pad);
}

Где-то так…
Вопросы, обсуждения реализации и конструктивная критика приветствуется.
Хотя на данный момент я передатчиком не занимаюсь и не использую(потому что не летаю). Считаю, что он достиг своего предела - во-первых исковеркан внешне, во-вторых нет апгейдабельности - сменных модулей. Прикрутить конечно, можно но вот зачем? Мне бы хотелось сделать новый передатчик на основе Hitec Optic 6, использовать экран TIC, плату по возможности подогнать под существующую, что бы проводку не менять… Только вот донора нет…