воскресенье, 22 февраля 2009 г.

Широтно імпульсна модуляція: фокус з Linux/RTAI

Переклад статті від Pramode C.E.

(http://linuxgazette.net/issue97/pramode.html)

У минулій статті, я розповів про основи програмування у реальному часі з Linux/RTAI. Ця стаття демонструє приємний фокус, що ви можете зробити на вашому домашньому “тазику” , якщо зробите просту електричну схему для вашого паралельного порту(LPT) (і звичайно запустите пропатчене RTAI ядро). Також я продемонструю прості приклади використання:

  • міжпроцесних повідомлень

  • поштових скриньок (Mail boxes)

Будьте обачні коли граєтесь з залізом вашого ПК – і не шукайте мене у разі якщо щось спалите!

Фокус

Ви хочете щоб світлодіод (LED) повільно займався, поступово стаючи все яскравішим. А досягнувши максимуму, почав поступово згасати, теж потроху. І така поведінка повторювалась циклічно. Контролюючи силу струму, що тече через нього, ми можемо контролювати його яскравість. Однак паралельний порт ПК може давати лише два рівня напруги: низький – 0 В та високий – близько 5 В. І цього неможливо змінити, ми можемо лише включати та виключати світлодіод миттєво, але це не те чого нам хочеться.

Широтно імпульсна модуляція

Рис. 1.ШІМ сигнал

Уявіть, що ви ідете на автомобілі особливим чином. Ви швидко їдете на протязі 3 секунд, потім стоїте на місці на протязі 7 секунд, потім знову таким чином. Якщо ви будете продовжувати так робити, ви через деякий час здолаєте відстань між двома точками. Поділивши відстань на цей час, можна отримати “середню” швидкість. Що станеться з нею, якщо ви збільшите кількість часу швидкої їзди? Звичайно вона зросте, а якщо зменшите – то зменшиться. Таким самим чином, замість прикладення постійної DC-напруги до світлодіода (LED), ми прикладаємо сигнал фіксованої частоти (скажімо 1 кГц, період 1 мс). Світлодіод буде яскраво горіти на протязі 1 мс періоду, тобто можна сказати сигнал буде високим 0,8 мс і низьким на протязі 0,2 мс. Варіюючи циклічність зміни рівня сигналу (залишаючи частоту фіксованою), ми зможемо доставляти різні кількості потужності до схеми, роблячи світло яскравішим чи тьмянішим. Тепер ми зможемо контролювати аналоговий пристрій у суто цифровий спосіб!

Залізо

Вам не знадобиться нічого окрім світлодіоду (бажано яскравого) та 1 кОм резистору. Резистор та світлодіод мають бути з'єднані послідновно між піном №2 (вихідним) та піном №25 (землею) паралельного порту. Вісім вихідних пінів (з 2 по 9) паралельного порту можуть бути керовані через порт вводу-виводу(IO) 0x378. Якщо записати 1 до 0x378 то у результаті високий рівень з'явиться лише на піні №2; Якщо записати 0xff то високий рівень з'явиться на усіх пінах, при запису іншого шаблону-маски можна керувати логічні рівні на довільних пінах.

Рис. 2.Принципова схема

Резистор обмежує силу струму, що тече скрізь світлодіод до декількох мілліампер. Проблема полягає у тому, що світлодіод буде горіти дуже кволо, якщо обмежити силу струму, що через нього тече. Рішенням є використання транзистору як перемикача.


Програмне забезпечення

Головна ідея проста – ми пишемо задачу реального часу, що буде вмикати світлодіод, засинати на деякий час, вимикати його, знову засинати на деякий час. Сума часу ввімкненого та вимкненого стану 1 мс. З початку, час ввімкненого стану буде 0 мс, а вимкненого 1 мс. На наступній ітерації, ввімкнений стан буде 1 * 1 мкс, а вимкнений (1 мс – ввімкнений час). Н а наступній ітерації, ввімкнений стан буде 2 * 1 мкс, і так далі. На 1000-ній ітерації ввімкнений стан буде 1000 * 1 мкс, тобто 1 мс і вимкнений буде (1 мс – ввімкнений час), тобто 0 мс. Як тільки ми дістанемо такого моменту, ми починаємо зменшувати час ввімкненого стану так, що він поступово зменшується до 0 мс, а час вимкненого стану зросте до 1 мс. Цей процес повторюється. Таким чином через 1 с (кожна ітерація займає 1 мс, і ми маємо 1000 ітерацій), світлодіод пройде від вимкненого стану до найяскравішого можливого рівня, а у наступну 1 с повільно повернеться до вимкненого стану.

Ось код, що реалізує цю ідею:

#include <linux/kernel.h>
#include <linux/module.h>
#include <rtai.h>
#include <rtai_sched.h>

#define STACK_SIZE 4096

#define MIN_ON_PERIOD 0
#define TOTAL_PERIOD 1000000 /* 1ms */
#define NSTEPS 1000
#define STEP_PERIOD 1000

#define LPT1 0x378


static RTIME on_time, off_time, total_period;
static RT_TASK my_task;

enum direction {DOWN, UP};

static void pwm_task(int n)
{
int step = 0;
static int dir = UP;

while(1) {
outb(0xff, LPT1);
rt_sleep(on_time);
outb(0x0, LPT1);
rt_sleep(off_time);
if(step == NSTEPS) {
dir = !dir;
step = 0;
}
step++;
if(dir == UP) on_time = nano2count(step*STEP_PERIOD);
else if(dir == DOWN) on_time = total_period - nano2count(step*STEP_PERIOD);
off_time = total_period - on_time;
}
}

int init_module(void)
{
RTIME now;

rt_set_oneshot_mode();
rt_task_init(&my_task, pwm_task, 0, STACK_SIZE, 0, 0, 0);
start_rt_timer(0);

on_time = nano2count(MIN_ON_PERIOD);
off_time = nano2count(TOTAL_PERIOD);
total_period = nano2count(TOTAL_PERIOD);

now = rt_get_time() + total_period;
rt_task_make_periodic(&my_task, now, total_period);

return 0;
}


void cleanup_module(void)
{
stop_rt_timer();
rt_busy_sleep(10000000);
rt_task_delete(&my_task);
}

Код ШІМ pwm_task() має бути легко зрозумілим.

Оскільки RTAI гарантує, що завдання реального часу завжди виконаються у завдані строки (deadlines), караючи лише звичні (non-realtime) завдання, ми можемо бачити, що процес генерації ШІМ сигналу гладенько продовжується навіть при високому навантаженні системи. Часовий крок у 1 мкс, що використаний у нашому коді для RTAI буде важко досягти, та однак ми не побачимо жодних візуальних ознак вказуючих на це (поки не використаємо осцилоскоп для перегляду форми сигналу). Але, це всього лиш кумедна програмка!

Посилання повідомлень

Завдання можуть посилати повідомлення одне одному. Повідомлення — це просто целочислене значення. Ось проста програма, що демонструє посилання повідомлень:


#include <linux/module.h>
#include <rtai.h>
#include <rtai_sched.h>

#define LPT1_BASE 0x378
#define STACK_SIZE 4096
#define TIMERTICKS 1000000000

static RT_TASK tasks[2];

static void task_sender(int t)
{
int msg = 0xab;
RT_TASK *r;
r = rt_send(&tasks[1], msg);
rt_printk("sender: r = %x\n", r);
}

static void task_receiver(int t)
{
int msg;
RT_TASK *r;

r = rt_receive(&tasks[0], &msg);
rt_printk("receiver: msg = %x\n", msg);
rt_printk("receiver: r = %x\n", r);
}


int init_module(void)
{
RTIME tick_period, now;

rt_set_periodic_mode();
rt_task_init(&tasks[0], task_sender, 0, STACK_SIZE, 0, 0, 0);
rt_task_init(&tasks[1], task_receiver, 0, STACK_SIZE, 0, 0, 0);

rt_printk("sender = %x\n", &tasks[0]);
rt_printk("recevier = %x\n", &tasks[1]);
tick_period = start_rt_timer(nano2count(TIMERTICKS));
now = rt_get_time();
rt_task_make_periodic(&tasks[1], now + tick_period, tick_period);
rt_task_make_periodic(&tasks[0], now + 2*tick_period, tick_period);
return 0;
}

void cleanup_module(void)
{
stop_rt_timer();
rt_busy_sleep(10000000);
rt_task_delete(&tasks[0]);
rt_task_delete(&tasks[1]);
}

Завдання отримувач запускається на наступному тіку(відмітці) таймеру(годинника). І негайно запускає функцію rt_receive. Перший аргумент це адреса об'єкту RT_TASK відповідаючого завданню відправнику. Оскільки відправник ще не активний, task_receive блокується. На наступному тіку годинника, task_sender стає активним і посилає повідомлення “message0xab до task_receiver. Завдання отримувач виходить з заблокованого стану та друкує отримане повідомлення і адресу об'єкту RT_TASK відповідаючого завданню відправнику.

Використання поштових скриньок

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

#include <linux/module.h>
#include <rtai.h>
#include <rtai_sched.h>

#define LPT1_BASE 0x378
#define STACK_SIZE 4096
#define TIMERTICKS 1000000000

static RT_TASK tasks[2];
static MBX my_mbx;

static void task_sender(int t)
{
int msg = 0x12cd, r;
r = rt_mbx_send(&my_mbx, &msg, sizeof(msg));
rt_printk("sender: r = %d\n", r);
}

static void task_receiver(int t)
{
int msg, r;
r = rt_mbx_receive(&my_mbx, &msg, sizeof(msg));
rt_printk("receiver: msg = %x\n", msg);
rt_printk("receiver: r = %d\n", r);
}


int init_module(void)
{
RTIME tick_period, now;

rt_set_periodic_mode();
rt_task_init(&tasks[0], task_sender, 0, STACK_SIZE, 0, 0, 0);
rt_task_init(&tasks[1], task_receiver, 0, STACK_SIZE, 0, 0, 0);

rt_mbx_init(&my_mbx, 4*sizeof(int));
tick_period = start_rt_timer(nano2count(TIMERTICKS));
now = rt_get_time();
rt_task_make_periodic(&tasks[1], now + tick_period, tick_period);
rt_task_make_periodic(&tasks[0], now + 2*tick_period, tick_period);
return 0;
}

void cleanup_module(void)
{
stop_rt_timer();
rt_busy_sleep(10000000);
rt_mbx_delete(&my_mbx);
rt_task_delete(&tasks[0]);
rt_task_delete(&tasks[1]);

}

Скринька представляє статичну змінну (static variable) типу MBX. Ми створюємо нову поштову скриньку викликаючи rt_mbx_init. Другий аргумент це розмір скриньки. Завдання відправник викликає rt_mbx_send і зберігає повідомлення 'msg' розміру 'sizeof(msg)' у поштову скриньку. Завдання отримувач отримує повідомлення викликаючи rt_mbx_receive. Отримувач блокується1 поки всі байти повідомлення не будуть отримані (чи поки не виникне якась помилка).

Висновки

Я почав вивчення Linux/RTAI через цікавість, та однак в мене виникла спокуса знати більше. Я сподіваюсь моя цікавість стане заразною і більше читачів почнуть лудити самостійно. Тільки не забудьте розповісти нам про ваші експерименти!


1Коли кажуть блокується, то мають на увазі, що завдання призупиняє своє виконання.

Комментариев нет: