CPU load indicator

Что это?

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

Собственно это очень простое устройство, плавно показывающее текущую нагрузку процессора меняя яркость светодиода(ну или по желанию можно использовать другую нагрузку). Людям, которые занимаются моддингом особо думаю понравится – нужно только приложить фантазию :) Работает через USB, причём программный, реализованный с помощью прооекта V-USB. В качестве мк выбран Atmega8

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

Принцип работы

Есть 2 стороны – девайса и компьютера. Компьютер периодически с помощью программы посылает через usb информацию о том, на сколько нужно зажечь ярко светодиод. А девайс слушает комманду и плавно приблежает яркость светодиода от текущего значения к новому.

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

Подробнее

Девайс

Схема достаточно простая и вполне собирается легко на макетной плате(у кого она есть) или как у меня – на картонке :) Конечно же, при желании можно сделать печатную плату – будет шикарно – я делать не стал, потому, что CPU load indicator оказался промежуточным этапом к большему проекту который я сейчас делаю. Конечно, гик скажет, что это слишком просто, чтоб уделять этому внимание, но кто-то только учиться, вроде меня =:)

Комментарии по схеме:

  • J1 – USB порт к компьютеру(D- это вторая нога, D+ это третья), J2 – SPI видимо к программатору :)
  • X1, C5, C6 – генератор. Кварц можно поставить, например, на 15МГц, главное, чтоб V-USB поддерживал эту частоту, а кондёры тут от 18 до 22пф можно выбирать. У меня 22пф стоят
  • R1 – подтягивающий резистор, лучше всё-таки 10кОм. Для работы мк ведь требуется reset пассивный, а т.к. он инверсный, нужно подать “1”. По идее, там есть внутри 100кОм, но как пишут, нога ловит каждый чих
  • D2, D3 – стабилитроны для того, чтоб не превысить напряжение на линии USB – так должно быть по стандарту, единица – не должна привышать 3.6в. У меня стеклянные – чёрной полоской к сигнальным линиям(D+ & D-)
  • R4, R5 – везде стоят по 68 Ом, у меня нормально работает 100 Ом. Если я правильно понимаю, они нужны чтоб не получилось ситуации, когда можно спалить порт мк
  • R2 – нужно оставить именно таким, это некая индикация USB-хосту что подключённо низкоскоростное устройство. Для эксперемента можно подцепить к usb только этот резюк и ядро будет писать что обнаруженно новое низкоскоростное устройство.
  • C3 – сглаживает помехи по питанию

ШИМ (PWM)

Наверное всё это мутно – интервалы, шаги, яркость.. Так вот, “яркость” – это численное значение от 0..255 – значение, которое подаётся на ШИМ(Широтно – Импульсная Модуляция) выход. Про ШИМ можно много где прочитать, мне понравилось на myRobot. Соответственно максимальной яркости(состоянию, в котором максимальное количество “1”) соответствует 255, минимальной – 0(максимальное количество “0”), среднему – 127(примерное равное количесво “1” и “0”). Думаю логика прослеживается.

Когда компьютер послал новое значение для ШИМ, которое нужно установить на выходе девайса, то девайс постепенно приблежает текущее значение к новому, с определённой задержкой. Благодаря этому яркость плавно меняется. Так, например, в темноте плавно повышается яркость после еле различимого свечения и место рядом с девайсом около компьютера краснеет – ну разумеется если стоит красный светодиод.

Как я уже указал, программа посылает уже конкретное значение ШИМ которое нужно установить. Но перед отправкой, нужно его ещё вычислить. Т.е. при загрузки процессора в 100% яркость индикатора должна быть максимальной, а не (если бы послать просто значение ШИМ 100) меньше половины от полной. Для этого нужно пересчитать его по простой формуле: (cpu_load * 255/100), где cpu_load – текущая нагрузка процессора.

Надеюсь пока ещё понятно, я ж предупредил что минимальная подготовка должна быть =).
А ведь вполне может быть такая ситуация, что светодиод начинает светиться только от значния 80 в ШИМ? Вполне может быть – значит при маленькой нагрузке мы вообще не увидим свечения. А что, если после ШИМ 200 индикатор так ярко светит, что кажется, вот-вот сгорит? Можно прийти в выводу, что требуется поставить рамки от и до и высчитывать соответственно величину ШИМ. Вычисляется по формуле(выжимка из кода):

cpu_load = ((valueMax - valueMin) / 100.0 ) * cpu_load + valueMin ;

Названия переменных говорят за себя, пояснения не требуются, как я думаю.

Управляющая программа(сторона компьютера) – драйвер

Основная программа сосредоточенна в software/cpu_indicator.c.
Внимание! Для сборки приложения нужно воспользоваться коммандой make clean all. Для запуска(из папки software) ./cpuload_indicator.
У вас обязательно должен быть установлен пакет libusb-dev и libgtop-dev, иначе ничего не соберётся. Разумеется в зависимости от дистрибутива исходные коды этих библиотек могут отличаться названием.

libusb нужен очевидно для работы с USB, а libgtop для получения информации о текущей нагрузке процессора.

В начале software/cpu_indicator.c подключаются хедеры и указываются дефолтные значения переменных.
О функциях

  1. bool parseOpts(int, char *) – служит для парсинга коммандной строки и проверки на корректность значений.
  2. bool connectUsb() – ищет устройство среди всех и открывает его, если нашла. А если не нашла то в зависимости от наличия аргумента коммандной строки -catch будет пытаться искать снова или просто попросит завершить работу программы с информацией о том, что устройство не найдённо. В противном случае, если указан -catch программа будет ждать пока подключат устройство. После открытия успешного устройства посылается комманда устанавливающая шаг времени, с которым значение ШИМ приблежаться к новому. Этот аргумент можно задать в коммандной строке ключом -step N, где N – значение задержки в мсек.
  3. int main (int, char *) – собвственнно входная точка программы. Вызывает parseOpts() и connectUsb(), высчитывает нагрузку на процессор за время в процентах, переводит в соответствующее значение ШИМ с учётом верхних и нижних смещений границ(о чём речь шла ранее) и посылает значение в USB. При ошибке, в зависимости от -catch, либо завершается работа программы, либо пытается подсоединиться снова, если аргумент указан.
    Особенно сложным оказалось для меня получение информации о нагрузки на процессор. Я воспользовался статьёй Calculating CPU Usage from /proc/stat, где нагрузка вычисляется bash скриптом и берётся информация из /proc/stat.
  4. void usage(char *) – выводит информацию об использовании, лучше показать:
    $ ./cpuload_indicator -help
    Control program for CPU Load indicator (c) ruX 2010
    Usage ./cpuload_indicator [option=[value]] ..
    Options:
       -min NN     - set minimal PWM value [0..255]
       -max NN     - set maxmimum PWM value [0..255]
       -step NN    - set step interval for LED (in msec)
       -update NN  - interval for update load  about current CPU(in seconds)
       -catch      - never quit(always trying to catch device)
       -help       - this help promt
    Copyright by ruX, 2010, http://ruX.pp.ru/

Думаю на столько краткого описания достаточно. За подробностями – в исходник. Есть ещё файл obdev_usb.c – там лежат довольно стандартные функции для открытия устройства(по его VID:PID, имени, имени разработчика) – они качуют из различных проектов, иногда немного правясь. Так и я взял за из проекта usb-relay.

Программа прошивки

Разумеется, всё что относится к прошивке девайса лежит в firmware, как ни странно :)

Код программы для мк можно разделить условно на 2 части: работа с USB, реализованная с помощью библиотеки V-USB и основной логики.

Важный этап – настройка V-USB, которая находится в файле firmware/usbdrv.h. То, что там определенно будет решать каким будет устройство. Файл шаблонный, от obdev. Но добавил 2 строчки, которые определяют константы комманд, которые посылает компьютер – для установки нового значения ШИМ и установки интервала шага приближения к новому значению:

#define REQUEST_SET_TARGET      1
#define REQUEST_SET_STEP        2

В основном файле логики firmware/cpuload_indicator.c сначала устанавливается конфигурация перефирии: направление вывода и настройка ШИМ.

    // Configure port B pin 1 as output
    DDRB = (_BV(1));
   
    // Setup PWM
    TCCR1A=0xA1;
    TCCR1B=0x09;
    TCNT1H=0x00;

Дальше вызывается process() в котором и происходит основная работа – один раз инициализируется USB и начинается бесконечный цикл, в котором происходит опрос usb – usbPoll() и приближение значения ШИМ, выполняется маленькая задержка:

void process(void)
{
    unsigned int dsec = 0;

    for(startUsb(); ;dsec++){    
        usbPoll();

        if (dsec % (delay * 10) == 0 && OCR1A != target) {
            if (OCR1A > target) OCR1A--; else OCR1A++;
        }
       
        _delay_ms(0.1);
       
        if (dsec == 10000) dsec = 0;
    }
}

У меня в качестве выхода ШИМ используется T/C1 – PORTB, бит 1, нога 15 в PDIP корпусе, соответственно имеем дело с OCR1A. Ещё нюанс: компьютер посылает интервал шага приближения в мсек, а внутри цикла задержка 0.1мсек, поэтому приходиться делать (delay * 10), чтоб привести к одному порядку – к примеру у нас компьютер передал,что шаг – 50мсек(=5 * 10-2, значит нужно сделать 50 * 10 = 500 циклов перед тем, как будет приблежение значения ШИМ к новому.

Если кто то не понял код выше – в цикле сначала происходит опрос, а дальше проверяется, если наступило время, когда нужно сделать шаг(т.е. интервал задержки прошёл) и при этом текущее значение ШИМ это не то, что компьютер “попросил” выставить, то приблежаемся к требуемому значению. Дальше задержка на 10-4сек и попытка избежать переполнения если число в счётчике циклов dsec достаточно большое.

Ещё определенна uchar usbFunctionSetup(uchar data[8]), которая вызывается, когда компьютер послал данные – в нашем случае это либо новое значение ШИМ, либо установка интервала шага приблежения.

uchar usbFunctionSetup(uchar data[8])
{
    usbRequest_t *request = (usbRequest_t*) data;
    switch (request->bRequest) {
    case REQUEST_SET_TARGET:
        target = request->wValue.bytes[0];
        break;
   
    case REQUEST_SET_STEP:
        delay = (short int)(request->wValue.bytes);
        break;
    }
   
    return 0;
}

Структура usbRequest_t *request содержит информацию о том, что нам пришло, подробнее можно узнать в доках. request->bRequest определяет номер комманды, а request->wValue.bytes переданные от компьютера данные.
target – значение ШИМ, которое нужно установить, фактически используется только 1 байт, поэтому и выбираем только первый байт из того, что пришло. А delay – задержка в милисекундах, поэтому тут одним байтом не обойтись, берём 2 сразу, делая тайпкастинг к short int.

Прошивка прошивки :)

В firmware/ определён мэйкфайл, который можно настроить под себя – в моём случае я использовал мк atmega8, программатор usbasp и частота цварца – 12MГц. По желанию можно изменить:

. . . .
DEVICE = atmega8
PROGRAMMER = usbasp
FREQ = 12000000
. . . .

Чтоб полностью откомпилировать, прошить программу и фьюзы, достаточно выполнить make clean all flash fuse :) Из фьюзов нужно отметить, что установленно тактирование от кварца, свыше 12МГц, кому интересно найдут интерпретацию фьюзов. Кстати о них(для atmega8) : старший байт – 0xC9, младший – 0xEF

Фото

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

Финал

На этом всё, буду рад если кому то это помогло. Хотя это уже помогло мне самому :)
Такие простые штуки позволяют сразу много всего пощюпать, хоть и не глубоко. А это помогает разложить по полочкам тонны информации.

Это – всё таки посложнее просто миганием светодиодов. И даже посложнее чем мигание светодиодов через USB(кстати, огромное спасибо проекту usb-Relay, который помог мне разобраться во многом).

Стоимость сборки такого устройства порядка 100-120р(по крайней мере с ценами, которые в мегаэлектронике в СПБ)

У линуксодидов проблемм быть не должно. У *bsd и solaris как я понимаю тоже нет(разве что поправить пути к инклюдам в мейкфайле). С виндой всё хуже. То, что нужно libusb это очевидно, кто чистал описание проекта V-USB сразу это поняли. По идее, как я понимаю, ничего не изменится, кроме места получения текущей нагрузки процессора – тут придётся разбираться самостоятельно. Или, если получится, воспользоваться cygwin(не из за libusb, а из за libgtop!), если умеете, я если честно практически не предствляю что он умеет и как :) Ну и разумеется нужно иметь make & gcc & winAvr

Скачать прошивку, программу и схему(в формате KiCad) в одном файле: cpuload_indicator_v0.1.tar.gz

  • Василий

    А можно сделать 8 или больше каналов ШИМ?

    • ruX

      Здравствуй. Аппаратно мега8 имеет только 3 ШИМ.
      Но программно можно реализовать столько каналов, сколько ножек вывода(для меги это около 20) и даже больше, если попробовать поизвращаться с мультиплексированием.
      Програмный ШИМ проще всего реализовать в цикле, если не требуется высокая точность – очень просто и наглядно. А если требуется точно, то на прерываниях таймера.

  • Василий

    Особо точно не надо. Есть софт – цветомузыка на СИ шарпе. Есть таблица градаций яркости 64 логарифмические ступеньки 8 битного ШИМа. Железная часть реализована на 2-х Тиньках. На первой преобразователь USB – RS232, на второй RS232 – 8 канальный софтовый ШИМ (написан в BASCOM AVR,так как в микроконтроллерах больше ни на чём пока не умею). Первая прошивка без переделка взята с японского сайта http://www.recursion.jp/avrcdc/cdc-232.html#usage, вторая самописная. Хочется всё загнать в одну микруху. Набрёл на Ваш блог, понимаю, что это то, что нужно. Но опыта написания устройств на С нет. Не будете ли вы так любезны привести фрагмент кода, например для 8 канального ШИМа? В принципе с самим ШИМом очень даже знаком, но только на Бейсике…

  • ruX

    Василий, вы хотите всё таки реализовать всё через RS232(cdc)? Или с только помощью v-usb & libusb?
    Если на одном мк, то на тини, какой?
    Что касается ШИМ, очень хорошо описанно тут http://www.artem.ru/elektro/pwm1.html – как раз не требуется высокая точность. Там описан 3х канальный ШИМ, соответственно увеличивая размеры массивов level (текущий уровень яркости) и ledBits(маска, выделяющаяя ногу со светодиодом)
    В вашем цикле не забывайте опрашивать usbPoll() как у меня(если будете использовать v-usb), минимум с интервалом в 50мсек, чаще – лучше.

  • Василий

    Я хочу реализовать только с помощью v-usb & libusb. Желательно на МЕГЕ8. А через cdc я уже реализовал…

  • ruX

    Ну вот и хорошо. Меги8 должно хватить для этого за глаза и за уши.
    Предлагаю так:
    Если номер комманды(request->bRequest) меньше 8, то нужно требуется установить новое значение ШИМ для канала, который равен номеру комманды. А значение ШИМ которое нужно установить будет в request->wValue.bytes[0]
    Примерно так:

     usbRequest_t *request = (usbRequest_t*) data;
     if (request->bRequest < 8) {
         level[request->bRequest] = request->wValue.bytes[0];
         return 0;
     }

    Можно так же ввести комманду(пусть номер 8) для установки всех значений ШИМ за раз, вроде этого

     int i;
     if (request->bRequest == 8) {
         for(i = 0; i < 8; i++) level[i] = request->wValue.bytes[i];
         return 0;
     }
  • Василий

    Спасибо! А в основном цикле необходимо будет реализовать ШИМ + usbPoll() ?

    int main(void) {
    register unsigned char scancounter=0;
    register unsigned char i;
    register unsigned char glow=0;
    unsigned char ledbits[3]={0b00000001,0b00000010, 0b00000100,0b00001000,0b00010000, 0b00100000,0b00100000,0b01000000, 0b10000000};

    // set C5 direction – output
    LEDS_DDR=0b11111111;

    // turn off all leds
    LEDS_PORT=0b11111111;

    for(;;){
    // main pwm part
    for (i=0;i=level[i]){
    // off – turn on the pin
    LEDS_PORT|=ledbits[i];
    }
    else {
    // on – turn off he pin
    LEDS_PORT&=~ledbits[i];
    }

    }

    scancounter++;

    // Do main task
    process();

    }
    }

    void process(void)
    {
    unsigned int dsec = 0;

    for(startUsb(); ;dsec++){
    usbPoll();

    if (dsec % (delay * 10) == 0 && OCR1A != target) {
    if (OCR1A > target) OCR1A–; else OCR1A++;
    }

    _delay_ms(0.1);

    if (dsec == 10000) dsec = 0;
    }
    }

  • ruX

    Я немного переформатировал код для наглядности и кое что исправил:
    – блок, который генерирует импульсы PWM должен находиться в цикле опроса
    – строку //_delay_ms(0); можно разкоментировать и поставить то, что вам хочется – будет некоторая инертность. Но думаю для светомузыки это излишне.
    – в массиве было 9 значений, так не должно быть, у нас всего 8 бит на порт :) Тем более вы хотели сделать 8ми канальный шим. Если хотите больше соответственно нужно использовать другие порты, благо их навалом ещё в меге8
    – ввёл дефайн CHANNEL_COUNT – количество каналов
    – PWM_VALUE_MIN – минимальное значение которое можно установить в канал
    – PWM_VALUE_MAX – максимальное значение которое можно установить в канал

    Получилось нечто(не компилировал, просто прикинул)

    #define CHANNEL_COUNT 8
    #define PWM_VALUE_MIN 0
    #define PWM_VALUE_MAX 255

    unsigned char ledbits[CHANNEL_COUNT] = {1, 2, 4, 8, 16, 32, 64, 128};
    unsigned char level[CHANNEL_COUNT] = {0, 0, 0, 0, 0, 0, 0, 0};

    int main(void) {

        // Ноги - на выход
        LEDS_DDR = 0b11111111;
       
        // Предустановка
        LEDS_PORT = 0b11111111;

        // Do main task
        process();
    }


    void process(void) {
        unsigned int scancounter = PWM_VALUE_MIN;

        for(startUsb(); ;scancounter++){
            usbPoll();

            // main pwm part
            for (i = 0; i < CHANNEL_COUNT; i++){
                if (scancounter >= level[i]){
                    // off – turn on the pin
                    LEDS_PORT |= ledbits[i];
                } else {
                    // on – turn off he pin
                    LEDS_PORT &= ~ledbits[i];
                }
            }
           
            if (scancounter >= PWM_VALUE_MAX) scancounter >= PWM_VALUE_MIN;
            //_delay_ms(0);
        }
    }

    Соответственно если менять PWM_VALUE_MAX/PWM_VALUE_MIN нужно незабыть пересчитать в драйвере значение которое посылаешь.

  • Василий

    Огромное спасибо! К сожалению смогу попробовать в железе только на следующей неделе…

  • ruX

    Удачи! Прошивка если не готова то практически завершенна, останется только правильно обвязать мк и изменить драйвер(который как я понял уже написан у вас).
    Не забудьте, что сейчас выход – инверсный. Соответственно используйте соответствующее подключение на выходе ноги – светодиод у вас там или транзистор будет. Почему так сделанно – по ссылке у artem.ru объясненно.

  • Василий

    Драйвер придётся переделать на вывод libusb, но это уже не сильно большая проблема. Примеры на C# есть. Со стороны компьютера проблем я думаю не будет, так как есть опыт.
    Эщё раз спасибо, как получится, отпишусь.

  • Василий

    Совсем забыл спросить, для этого устройства драйвер нужен или оно определяется как HID?