FK~

Moje odkazy

Ostatní odkazy

Close Windows
Nenajdete mě na Facebooku ani Twitteru
Rozpad EU
Jsem členem FSF
There Is No Cloud …just other people's computers.
Sane software manifesto / Manifest příčetného softwaru

Paralelní port jako generátor signálu

vydáno: 11. 6. 2017 19:05, aktualizováno: 5. 7. 2020 16:39

Dnes oprášíme zase jednu starou dobrou technologii – paralelení port – a ukážeme si, jak ji softwarově ovládat. Protože je to dost nízkoúrovňová záležitost, nebude to tentokrát v Javě ale v C++. Cíl bude poměrně skromný: generovat obdélníkový signál s frekvencí 10 000 Hz a zadanou střídou (což zde neznamená střed chleba). Ve výsledku budu signál generovat jiným programem, nicméně nejdřív si chci otestovat jednotlivé části systému samostatně, takže teď to bude jen LEDka a pár řádků kódu bez nějakých složitostí.

konektor DB-25 pro paralelní port (LPT)

Paralelní port (LPT)

Dříve býval paralelní port běžnou součástí každého počítače (pamatuji je od 386 po Pentia a Athlony), ovšem na dnešních základních deskách je najdeme jen sporadicky. Prodávají se ale dodatečné PCI a PCIe karty.

Port se používal hlavně pro připojení tiskáren (ostatně taky se mu říká LPT – Line Print Terminal) případně jiných periferií. Ale je to poměrně univerzální rozhraní – skoro jako GPIO, které známe třeba z Arduina nebo Raspberry Pi. Můžeme zde nastavovat logickou 0 a 1 resp. napětí 0 a 5 V na jednotlivých pinech a stejně tak je i číst. (5 V je teoretická maximální hodnota, ve skutečnosti bývá napětí nižší)

Paralelní port, zdroj: https://en.wikipedia.org/wiki/File:25_Pin_D-sub_pinout.svg licence: GNU FDL a CC BY SA autor: Andrew Buck

V základu máme k dispozici 8 + 4 výstupní a 5 vstupních pinů. U novějších portů máme obousměrné piny a můžeme mít více vstupů. Více na Wikipedii: Parallel port a v článku Interfacing the Standard Parallel Port:

The non bi-directional ports were manufactured with the 74LS374's output enable tied permanent low, thus the data port is always output only. When you read the Parallel Port's data register, the data comes from the 74LS374 which is also connected to the data pins. Now if you can overdrive the '374 you can effectively have a Bi-directional Port. (or a input only port, once you blow up the latches output!)

What is very concerning is that people have actually done this. I've seen one circuit, a scope connected to the Parallel Port distributed on the Internet. The author uses an ADC of some type, but finds the ADC requires transistors on each data line, to make it work! No wonder why. Others have had similar trouble, the 68HC11 cannot sink enough current (30 to 40mA!)

Pro běžné periferie se dnes prakticky nepoužívá, přesto má svoje nesporné výhody a dodnes najde využití. Na rozdíl od od Arduina nebo RPi máme k dispozici výkonný stroj (se spoustou paměti, diskem, rychlou sítí atd.) a na rozdíl od sériového nebo nedejbože USB portu máme spolehlivé nízkoúrovňové rozhraní s minimální (a stabilní) latencí.

Z paralelního portu můžeme udělat třeba zvukovou kartu, připojit přes něj gamepad od SNESu nebo jím ovládat nějaký stroj (což bude zajisté užitečnější).

Zjištění adresy portu

S paralelním portem budeme pracovat nízkoúrovňově, takže místo souboru /dev/parport0 nás zajímá přímo adresa příslušného portu v paměti. Tu v GNU/Linuxu zjistíme následujícím příkazem:

# cat /proc/ioports | grep parport
0378-037a : parport0
037b-037f : parport0
	e400-e402 : parport1
	e403-e407 : parport1

První paralelní port, který je na základní desce, mívá typicky adresu 0x378. Port na mojí přídavné kartě má adresu 0xe400.

Program na generování signálu

V zásadě jde jen o cyklus, který provede patřičný počet opakování a uvnitř nastaví hodnotu pinu, chvíli počká, nastaví opačnou hodnotu a zase chvíli počká. Program nemá žádné volby a je parametrizovaný přímo v kódu:

int addr = 0xe400;    // parallel port address
int baseFreq = 10000; // base frequency in Hz
int outputPower = 10; // duty cycle; 100 = 100 %
int duration = 1;     // in seconds; total sleep time

Program spustíme pod rootem pomocí příkazu make run (pokud jsme kód změnili, před spuštěním se překompiluje, takže se s tím dá pracovat podobně, jako se skriptem). Zdrojové kódy jsou v mercurialu: lpt-signal-generator a níže v textu.

Inicializace a nastavování hodnot

Než začneme pracovat s paralelním portem, musíme zavolat funkci ioperm():

if (ioperm(addr,1,1)) {
	fwprintf(stderr, L"Access denied to port %#x\n", addr), exit(1);
}

Pokud nejsme root, tak program hned skončí. Trochu potíž je v tom, že když zadáme špatnou adresu, tak to projde – jednou se mi stalo, že PCI karta byla špatně zastrčená, takže ji systém neviděl – a program běžel, jako by nic, ale LEDka neblikala. Tohle by chtělo ještě vylepšit (našel jsem jen, jak to udělat přes ppdev ovladač, open()ioctl(), ale to není přímý přístup k portu).

A teď už se můžeme pustit do nastavování hodnot na pinech paralelního portu:

auto cycleCount = duration * baseFreq;

for (auto i = cycleCount; i > 0;  i--) {
	outb(0b00000001, addr);
	usleep(timeOn);
	outb(0b00000000, addr);
	usleep(timeOff);
}

V C++14 (a v GNU GCC někdy od roku 2007) můžeme zapisovat binární čísla, takže je to hezky přehledné. Ta jednička na konci znamená, že na prvním datovém pinu (resp. pinu č. 2 na konektoru DB-25) nastavíme napětí 5 V, zatímco na ostatních datových pinech bude 0 V.

Funkce outb() zapisuje celý bajt, takže nám přepíše i sedm zbývajících bitů. Pokud bychom chtěli zachovat jejich předchozí hodnoty, museli bychom si je někde zapamatovat. Práci s jednotlivými piny umožňuje knihovna Parpin.

Střída

Naším cílem je generovat obdélníkový signál se zadanou frekvencí (10 000 Hz) a zadaným poměrem časů, ve kterých je signál nahoře a dole – to se jmenuje Střída:

Střída – duty cycle – zdroj: https://cs.wikipedia.org/wiki/St%C5%99%C3%ADda_(elektronika)#/media/File:PWM_duty_cycle_with_label.gif licence: volné dílo

Časování

Abychom věděli, jak dlouho čekat ve stavu 1 a jak dlouho ve stavu 0, musíme jsme si spočítat dva časy:

// in microseconds:
auto oneSecond = 1000 * 1000;
auto timeOn =  oneSecond *        outputPower  / 100 / baseFreq;
auto timeOff = oneSecond * (100 - outputPower) / 100 / baseFreq;

V zápětí ale zjistíme, že to moc nefunguje, resp. program běží víc než dvakrát déle, než by měl:

# time ./lpt-signal-generator 
Parallel port:        e400
Base frequency:     10 000 Hz
Output power:           10 % duty cycle
Duration:                1 s
Cycle count:        10 000 ×
Time on:                10 μs 1× in each cycle
Time off:               90 μs 1× in each cycle
single outb():          64 μs 2× in each cycle
single outb():      64 335 ns 2× in each cycle

real    0m2.292s
user    0m0.044s
sys     0m0.176s

Ačkoli frekvence 10 000 Hz nevypadá na první pohled až tak vysoká (takt procesorů je v řádech GHz), dali jsme systému pořádně zabrat. Pouštět to jako běžný proces nikam nevede. Náš program musíme pustit s prioritou reálného času (a nemusí to být nutně na systému s RT jádrem). To už je o hodně lepší:

# time chrt 1 ./lpt-signal-generator 
Parallel port:        e400
Base frequency:     10 000 Hz
Output power:           10 % duty cycle
Duration:                1 s
Cycle count:        10 000 ×
Time on:                10 μs 1× in each cycle
Time off:               90 μs 1× in each cycle
single outb():          13 μs 2× in each cycle
single outb():      13 016 ns 2× in each cycle

real    0m1.265s
user    0m0.044s
sys     0m0.144s

Ale pořád ne dost dobré. Původní implementace je totiž dost naivní a nepočítá s časem stráveným ve funkci outb(). Takže jsem tam přidal výpočet, kolik μs se v těchto funkcích strávilo – k tomu jsem použil chrono::high_resolution_clock z C++, kde se pracuje s přesností na nanosekundy. Dosud jsem se pohyboval někde o řád až dva výše – např. pokud SQL dotaz trval pod 1 ms, tak se to zaokrouhlilo na 0, byl to krásný výsledek a nebylo, co dál řešit. Tady najednou ale i jednotky μs jsou moc dlouho.

Nečekám, že by to šlo ještě nějak moc optimalizovat – přeci jen interakce s operačním systémem a hardwarem nějaký čas zabere – takže nezbývá než patřičně zkrátit časy čekání. Zatím nevíme, zda se na daném pinu objeví těch 5 V spíše na začátku těch 13 μs nebo spíše na konci (pravděpodobnější), ale to je celkem jedno, protože by to způsobilo jen posun a proporce časů mezi 0 a 1 by zůstaly stejné. Horší by bylo, pokud by se např. směrem nahoru na 5 V nastavovala hodnota rychleji než směrem dolů na 0 V. Ale to softwarově nezjistíme.

Analýza signálu

Nastal čas připojit logický analyzátor. Stejně jako při zkoumání SNESu jsem použil otevřený hardware DSLogic:

logický analyzátor DSLogic

První měření – frekvence 1 000 Hz a střída 10 % – dopadlo relativně dobře, DSLogic ukazuje 947 Hz a 12,52 %:

měření 1000 Hz a 10% – DSLogic / LPT port

Druhý pokus: mělo být 5 000 Hz a 10 % – naměřeno 4 230 Hz a 18,44 %:

měření 5000 Hz a 10% – DSLogic / LPT port

Třetí pokus: mělo být 10 000 Hz a 10 % – naměřeno 6 580 Hz a 14,42 %:

měření 10 000 Hz a 10% – DSLogic / LPT port

První cykly bývají horší a kolísavé, pak se to zlepšuje, ale požadovaných hodnot náš generátor nikdy nedosáhne. Stabilizuje se na 7 850 Hz a 18,65 %. Na tuto stabilitu se ale nedá moc spolehnout a dost možná bude záviset na teplotě a měsíčním svitu. Možná by pomohlo zapálení obřadních svíček kolem počítače.

Vylepšený program

Teď by bylo na místě implementovat PID regulátor (PID = proporcionální, integrační a derivační; totéž, co máte ve své hexakoptéře/kvadkoptéře), jenže k tomu potřebujeme zpětnou vazbu a je to netriviální úloha nad rámec tohoto programu, takže se pokusím jen zkrátit časy čekání o čas strávený ve funkci outb().

Magická konstanta

Pro začátek zkusíme vidlácké programování a odečteme magické konstanty. Potřebujeme odečíst 2×13 μs, ovšem kdybychom odečetli 13 od 10, tak se dostaneme do mínusu (ne, usleep(-3) opravdu necestuje zpět v čase), takže musíme být trochu kreativní:

timeOn  -= 10;
timeOff -= 16;

Program nám teď vypisuje:

Parallel port:        e400
Base frequency:     10 000 Hz
Output power:           10 % duty cycle
Duration:                1 s
Cycle count:        10 000 ×
Time on:                 0 μs 1× in each cycle
Time off:               74 μs 1× in each cycle
single outb():           0 μs 2× in each cycle
single outb():         -14 ns 2× in each cycle

Poslední dva řádky teď jsou nepravdivé – je třeba je číst tak, že těch 10 000 cyklů doběhlo s odchylkou -280 μs, takže celkem přesně. Čas strávený ve funkci outb() je samozřejmě pořád stejný.

Na tomto HW je tento čas dlouhodobě stabilně těch 13 μs, takže už to dává docela hezké výsledky – frekvenci jsme už trefili (první cykly byly opět špatné, tohle jsou pozdější):

měření 10 000 Hz a 10% – korekce -10 a -16 μs – DSLogic / LPT port

Nicméně střída je pořád mimo: 12,22 % místo požadovaných 10 %. Z toho plyne, že střída pod cca 13 % je na tomto HW a s touto frekvencí nedosažitelná, protože i když voláme obě outb() funkce hned po sobě (timeOn = 0), na 10 μs mezi přepnutími úrovní se nedostaneme. (další věc je, že nějaký čas zabere i dekrementování proměnné a vyhodnocování podmínky v cyklu – tyto časy program neprávem přičítá režii funkce outb(), ale to asi moc nebude)

Zkusíme tedy něco lehčího – střídu 50 % – kde už můžeme odečíst 13 μs na obou stranách:

int outputPower = 50; // duty cycle; 50 = 50 %
timeOn  -= 13;
timeOff -= 13;

A tohle už je výsledek, se kterým můžu být spokojený – 10 340 Hz a 49,95 % stabilně (první cykly jsem zase přeskočil):

měření 10 000 Hz a 50% – korekce -13 a -13 μs – pěkný výsledek – DSLogic / LPT port

10 000 Hz / 20 % → 9 920 Hz / 20,14 %:

měření 10 000 Hz a 20% – korekce -13 a -13 μs – pěkný výsledek – DSLogic / LPT port

10 000 Hz / 80 % → 10 260 Hz / 80,92 %:

měření 10 000 Hz a 80% – korekce -13 a -13 μs – pěkný výsledek – DSLogic / LPT port

Opět pěkné výsledky. Při opakovaných měřeních kolísají hodnoty oběma směry, takže bych z těch rozdílných odchylek u 20 a 80 % zatím nevyvozoval žádné závěry.

Automatická kalibrace

Výše uvedené řešení samozřejmě nebude fungovat na jiných počítačích, kde bude magická konstanta jiná. Proto do programu přidáme automatickou kalibraci – na začátku pustíme nějaké cykly naprázdno (zapisujeme samé nuly), abychom zjistili, jakou má daný systém režii, a podle toho určili potřebnou korekci časů čekání.

Program taky vyhodnotí, zda je zadaná frekvence a střída na daném hardwaru dosažitelná:

# make run
chrt 1 ./lpt-signal-generator
Parallel port:        e400
Base frequency:     10 000 Hz
Output power:           20 % duty cycle
Duration:                1 s
Cycle count:        10 000 ×
Time on:                20 μs 1× in each cycle
Time off:               80 μs 1× in each cycle

Single outb():          13 μs 2× in each calibration cycle
Single outb():      13 743 ns 2× in each calibration cycle
Minimum power:          13 % feasible duty cycle
Maximum power:          87 % feasible duty cycle
Calibration:            OK both frequency and duty cycle should be correct
Sleep on:                7 μs 1× in each cycle
Sleep off:              67 μs 1× in each cycle

Deviation:          15 680 μs in total
Deviation:           1 568 ns in each cycle

Zvažoval jsem i průběžnou kalibraci během hlavního cyklu, ale tam by asi zdržovalo i samotné měření.

Makefile:

CXX=g++
CXXFLAGS += -std=c++11 -Wall

APP=lpt-signal-generator
PROGNAME=$(APP)
SRC=lpt.cpp

ALL: $(PROGNAME)

$(PROGNAME): $(SRC)
	$(CXX) $(CXXFLAGS) -o $(PROGNAME) $(SRC)

clean:
	rm -f $(PROGNAME)

run: $(PROGNAME)
	chrt 1 ./$(PROGNAME)

Celý program lpt-signal-generator:

/**
 * LPT signal generator
 * Copyright © 2017 František Kučera (frantovo.cz)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdlib.h>
#include <iostream>
#include <stdio.h>
#include <math.h>
#include <sys/io.h>
#include <unistd.h>
#include <chrono> // requires -std=c++11

/**
 * can not mix printf and wprintf
 * see https://stackoverflow.com/questions/8681623/printf-and-wprintf-in-single-c-code
 * > This is to be expected; your code is invoking undefined behavior.
 * > Per the C standard, each FILE stream has associated with it an "orientation" (either "byte" or "wide)
 * > which is set by the first operation performed on it, and which can be inspected with the fwide function.
 * > Calling any function whose orientation conflicts with the orientation of the stream results in undefined behavior.
 */
#include <wchar.h>
#include <locale.h>


using namespace std;

// TODO: data types revision
// TODO: time units s, μs, ns – naming convention / unification

/**
 * generates square wave signal on a parallel port pin with given frequency and duty cycle
 * mode info: https://blog.frantovo.cz/c/358/Paraleln%C3%AD%20port%20jako%20gener%C3%A1tor%20sign%C3%A1lu
 */
int main() {
	//cout << "LPT!" << endl; // same as using printf → breaks all folllowing wprintf() calls, see note above

	/*
	 * if setlocale() is missing, unicode characters are replaced with ? or „→“ with „->“ because C/POSIX locale is used, 
	 * see man setlocale:
	 * > On startup of the main program, the portable "C" locale is selected as default.
	 * > If locale is an empty string, "", each part of the locale that should be modified is set according to the environment variables.
	 */
	setlocale(LC_ALL,"");


	// configuration ----
	int addr = 0xe400; // parallel port address; first number of given port in: cat /proc/ioports | grep parport
	int baseFreq = 10000; // base frequency in Hz, should be between 5 000 between 10 000 Hz; lower frequency leads to dashed/dotted lines instead of greyscale
	int outputPower = 20; // duty cycle; 100 = 100 %
	int duration = 1; // in seconds; total sleep time, see note above
	// ------------------


	int valueWidth =  10; // just for padding of printed values
	int labelWidth = -15; // just for padding of printed labels

	// ' = thousand separator
	// * = padding
	wprintf(L"%*ls %*x\n", labelWidth, L"Parallel port:", valueWidth, addr); // or %#*x – adds 0x prefix
	wprintf(L"%*ls %'*d Hz\n", labelWidth, L"Base frequency:", valueWidth, baseFreq);
	wprintf(L"%*ls %*d %% duty cycle\n",  labelWidth, L"Output power:", valueWidth, outputPower);
	wprintf(L"%*ls %'*d s\n", labelWidth, L"Duration:", valueWidth, duration);

	// in microseconds:
	auto oneSecond = 1000 * 1000;
	auto timeOn =  oneSecond *        outputPower  / 100 / baseFreq;
	auto timeOff = oneSecond * (100 - outputPower) / 100 / baseFreq;

	auto cycleCount = duration * baseFreq;
	wprintf(L"%*ls %'*d ×\n", labelWidth, L"Cycle count:", valueWidth, cycleCount);
	wprintf(L"%*ls %'*d μs 1× in each cycle\n", labelWidth, L"Time on:",  valueWidth, timeOn);
	wprintf(L"%*ls %'*d μs 1× in each cycle\n", labelWidth, L"Time off:", valueWidth, timeOff);

	//wprintf(L"%*ls %*ls\n", labelWidth, L"unicode test:", valueWidth, L"čeština → …");

	wprintf(L"\n");

	// TODO: test whether this address is an parallel port
	if (ioperm(addr,1,1)) { fwprintf(stderr, L"Access denied to port %#x\n", addr), exit(1); }


	// calibration
	auto startTimestamp = chrono::high_resolution_clock::now();
	auto calibrationCycles = 10000;
	auto calibrationSleepTime = 10;

	for (auto i = calibrationCycles; i > 0; i--) {
		outb(0b00000000, addr);
		usleep(calibrationSleepTime);
		outb(0b00000000, addr);
		usleep(calibrationSleepTime);
	}

	auto finishTimestamp = chrono::high_resolution_clock::now();
	auto measuredDuration = chrono::duration_cast<chrono::nanoseconds>(finishTimestamp - startTimestamp).count();

	auto singleOutbCostNano = (measuredDuration - calibrationCycles*2*calibrationSleepTime*1000)/calibrationCycles/2;
	auto singleOutbCostMicro = singleOutbCostNano/1000;

	wprintf(L"%*ls %'*d μs 2× in each calibration cycle\n", labelWidth, L"Single outb():", valueWidth, singleOutbCostMicro);
	wprintf(L"%*ls %'*d ns 2× in each calibration cycle\n", labelWidth, L"Single outb():", valueWidth, singleOutbCostNano);

	auto minPower = 100*singleOutbCostNano/(1000*1000*1000/baseFreq);
	auto maxPower = 100-minPower;
	wprintf(L"%*ls %*d %% feasible duty cycle\n",  labelWidth, L"Minimum power:", valueWidth, minPower);
	wprintf(L"%*ls %*d %% feasible duty cycle\n",  labelWidth, L"Maximum power:", valueWidth, maxPower);

	if (singleOutbCostMicro < timeOn && singleOutbCostMicro < timeOff) {
		wprintf(L"%*ls %*ls both frequency and duty cycle should be correct\n",  labelWidth, L"Calibration:", valueWidth, L"OK");
		timeOn  -= singleOutbCostMicro;
		timeOff -= singleOutbCostMicro;
	} else if (2*singleOutbCostMicro < (timeOn + timeOff)) {
		wprintf(L"%*ls %*ls frequency should be OK, but duty cycle is not feasible\n",  labelWidth, L"Calibration:", valueWidth, L"WARNING");
		timeOn  -= singleOutbCostMicro;
		timeOff -= singleOutbCostMicro;

		if (timeOn < 0) {
			timeOff -= timeOn;
			timeOn = 0;
		} else {
			timeOn -= timeOff;
			timeOff = 0;
		}
	} else {
		wprintf(L"%*ls %*ls both frequency and duty cycle are not feasible\n",  labelWidth, L"Calibration:", valueWidth, L"ERROR");
		timeOn  = 0;
		timeOff = 0;
	}

	wprintf(L"%*ls %'*d μs 1× in each cycle\n", labelWidth, L"Sleep on:",  valueWidth, timeOn);
	wprintf(L"%*ls %'*d μs 1× in each cycle\n", labelWidth, L"Sleep off:", valueWidth, timeOff);

	wprintf(L"\n");


	// actual signal generation
	startTimestamp = chrono::high_resolution_clock::now();

	for (auto i = cycleCount; i > 0;  i--) {
		outb(0b00000001, addr); // first data out pin = data out 0 = pin 2 on DB-25 connector
		usleep(timeOn);
		outb(0b00000000, addr);
		usleep(timeOff);
	}

	finishTimestamp = chrono::high_resolution_clock::now();
	measuredDuration = chrono::duration_cast<chrono::nanoseconds>(finishTimestamp - startTimestamp).count();

	wprintf(L"%*ls %'*d μs in total\n", labelWidth, L"Deviation:", valueWidth, (measuredDuration-duration*oneSecond*1000)/1000);
	wprintf(L"%*ls %'*d ns in each cycle\n", labelWidth, L"Deviation:", valueWidth, (measuredDuration-duration*oneSecond*1000)/cycleCount);

}

Statistika počtu řádků kódu pomocí příkazu CLOC:

$ cloc-sql.sh .
 ╭────────┬─────────┬───────────┬───────────┬──────┬────────┬──────────────────╮
 │ jazyk  │ souborů │ prázdných │ komentářů │ kódu │ celkem │ celkem_graf      │
 ├────────┼─────────┼───────────┼───────────┼──────┼────────┼──────────────────┤
 │ C++    │       1 │        34 │        47 │   85 │    166 │ ████████████████ │
 │ make   │       1 │         6 │         0 │   12 │     18 │ ██░░░░░░░░░░░░░░ │
 │ celkem │       2 │        40 │        47 │   97 │    184 │                  │
 ╰────────┴─────────┴───────────┴───────────┴──────┴────────┴──────────────────╯
Record count: 3

Měření osciloskopem

Pomocí osciloskopu jsem ověřil, že generovaný signál je skutečně obdélníkový. Frekvence je 9 930 Hz a střída měla být v tomto případě 30 %, což tak nějak odpovídá. Ovšem napětí je jen cca 2,8 V, nikoli 5 V.

měření osciloskopem – DSCope – obdélníkový signál, 10 000 Hz, 30 %, 2,75 V

Ani na paralelním portu, který je přímo na desce, jsem nenaměřil 5 V – bylo tam nějakých 2,95 V. Specifikaci to ale neodporuje, protože paralelní port by měl mít TTL high od +2,4 do +5 V a TTL low od 0 do +0,8 V. Pro kontrolu: na USB portu jsem 5 V naměřil.

Použitý hardware

K naměřeným hodnotám by se slušelo uvést konfiguraci. Protože jsem potřeboval druhý paralelní port, bylo nutné přidat PCI kartu. Jako první jsem zkoušel kartu s čipem MosChip MCS 9835 CV:

PCI karta s paralelním (LPT) portem – čip MosChip MCS 9835 CV

ale s tou jsem neuspěl – systém ji sice viděl, ovšem LEDka neblikala. Údajně je dobré se vyhnout čipům 9805/9815 a použít raději 9845/9865/9901. Zkusím to ještě rozchodit, ale prozatím jsem použil jinou kartu s čipem MosChip MCS 9865 1V, která fungovala hned:

PCI karta s paralelním (LPT) portem – čip MosChip MCS 9865 1V

Celé to běží na starším Celeronu a desce Asus P5PE-VM, ale dokud to bude fungovat, není důvod HW měnit.

# cat /proc/cpuinfo 
processor       : 0
vendor_id       : GenuineIntel
cpu family      : 15
model           : 4
model name      : Intel(R) Celeron(R) CPU 2.80GHz
stepping        : 9
microcode       : 0x3
cpu MHz         : 2793.026
cache size      : 256 KB
fdiv_bug        : no
hlt_bug         : no
f00f_bug        : no
coma_bug        : no
fpu             : yes
fpu_exception   : yes
cpuid level     : 5
wp              : yes
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe lm constant_tsc up pebs bts pni dtes64 monitor ds_cpl tm2 cid cx16 xtpr lahf_lm
bogomips        : 5586.05
clflush size    : 64
cache_alignment : 128
address sizes   : 36 bits physical, 48 bits virtual
power management:

Verze jádra:

# uname -ro
3.4-9-rtai-686-pae GNU/Linux

Čeština (unicode) v C/C++

Tohle přímo nesouvisí s řešenou úlohou, ale mně prostě vadí, když některé základní věci nefungují, tak mi to nedalo a aspoň jsem se naučil, jaké funkce používat pro vypisování i jiných než ASCII znaků.

V čem je problém? Dnes se používá převážně vícebajtové kódování (většinou UTF-8), takže neplatí, že 1 znak = 1 bajt, a kvůli tomu nefunguje spousta starých funkcí, které vznikly v době, kdy tento předpoklad platil. Vícebajtové znaky se nám pomocí nich sice nějak vypsat podaří, ale nebudou fungovat takové věci jako počítání šířky řetězce, což je potřeba, když chceme např. doplnit patřičný počet mezer zleva nebo zprava pro zarovnání.

Místo klasické funkce printf():

int vw =  10; // valueWidth: just for padding of printed values
int lw = -15; // labelWidth: just for padding of printed labels

printf("%*s %'*d μs 1× in each cycle\n", lw, "Time off:", vw, timeOff);

tedy potřebujeme:

#include <wchar.h>
#include <locale.h>

wprintf(L"%*ls %'*d μs 1× in each cycle\n", lw, L"Time off:", vw, timeOff);

Kolize mezi printf()wprintf()

První záludnost, na kterou jsem narazil, je to, že v kódu nemůžeme míchat funkce printf()wprintf() – musíme si jednu vybrat a té se držet – jinak nám totiž ta druhá skupina funkcí nebude fungovat (záleží, kterou v kódu zavoláme jako první – pěkný vedlejší efekt). Je to popsané v dokumentaci:

It is important to never mix the use of wide and not wide operations on a stream. There are no diagnostics issued. The application behavior will simply be strange or the application will simply crash. The fwide function can help avoid this.

Nevypíše se žádná chyba – ani při kompilaci ani v době běhu – druhý typ funkcí se prostě rozbije a nebude vypisovat nic. K rozbití wprintf() funkcí vede i nevinně vypadající řádek:

cout << "LPT!" << endl;

Chybějící setlocale()

Když už vše přepíšeme na wprintf(), a přestaneme tak míchat dohromady bajtové a znakové funkce, a změníme "…" na L"…"%s na %ls, tak zjistíme, že to stejně nefunguje a místo znaků s háčky a čárkami to píše otazníky a místo např. to píše ->. Program totiž používá C/POSIX lokalizaci a tyto znaky nepodporuje. Napravíme to zavoláním funkce:

setlocale(LC_ALL,"");

Z manuálové stránky:

On startup of the main program, the portable "C" locale is selected as default.

If locale is an empty string, "", each part of the locale that should be modified is set according to the environment variables.

Použité IDE

Většinou používám Netbeans a to i pro jiné jazyky než Javu. Ale tentokrát jsem si IDE sestavil z jednodušších nástrojů a kód psal a spouštěl (přes SSH) rovnou na vzdáleném stroji, kde mám paralelní port.

Jako editor jsem použil Emacs, obrazovku rozdělil vertikálně na dvě poloviny pomocí Screen a kompiloval přes Make v GCC (všechno GNU nástroje).

Rychlokurz GNU Screen:

  • Ctrl+a, | – rozdělí obrazovku vertikálně
  • Ctrl+a, Tab – přepínání mezi rozdělenými polovinami obrazovky
  • Ctrl+a, c – spustí shell v dané části obrazovky (po rozdělení v ní nic neběží)

IDE složené z GNU Emacs a GNU Screen

Závěr

Zatím máme sice jen blikající LEDku, ale zato je to blikání řízené CPU a poměrně přesně časované. Co se týče kódu, bylo to hlavně programátorské cvičení a výlet někam jinam – do světa μs a jiného jazyka, než v kterém běžně pracuji, což občas neškodí.

P.S. nejsem C ani C++ programátor, takže mě rozhodně neurazí, když budete mít nějaké připomínky k mému kódu a návrhy na vylepšení.

Odkazy a zdroje:

Témata: [C++] [C] [hardware] [3D tisk] [CNC] [elektřina]

Komentáře čtenářů


VS, 31. 12. 2017 16:19 [odpovědět]

Cyklické spouštění nějaké úlohy s danou periodou je úplně základní use case v realtime aplikacích. Přesně na tyhle úlohy se místo usleep() používá clock_nanosleep(), který řeší právě probuzení v daném absolutním čase. Viz příklad https://rt.wiki.kernel.org/index.php/Squarewave-example -- přesně stejné použítí paralelního portu

Odpadají pak všechny potíže, kterými se zabývá většina obsahu článku. Nicméně přesnost časů pod 100 us je na PC/Linux platformě už docela problém, což nakonec ukazují i výsledky v článku. Viz také nástroj cyclictest. Výrazně lepších výsledků by se dalo dosáhnout implementací v kernel space jako modul, ale pořád to bude použitelné více méně jen na hraní.

Předpokládám, že to neběželo na kernelu s RT PREEMPT patchem. Pak by bylo zajímavé si třeba na tom osciloskopu udělat analýzu min/max periody. Průměr bude v pořádku, ale téměř jistě tam budou místy případy, kdy to bude úplně mimo. Zvlášť, pokud paralelně s tím programem poběží nějaká další zátěž. Záleží pak na aplikaci, jestli to lze tolerovat nebo ne.

Přidat komentář

reagujete na jiný komentář (zrušit)
jméno nebo přezdívka
název příspěvku
webová stránka, blog
e-mailová adresa
nápověda: možnosti formátování
ochrana proti spamu a špatným trollům

Náhled komentáře