Skocz do zawartości

Pomocna odpowiedź

Napisano (edytowany)

Cel artykułu

Czytając dyskusje i tutoriale na forum zauważyłem, że o ile jest wiele dobrych opisów sprzętu (chociażby tutoriale dotyczące procesorów), to brakuje opisów, jak z tych wszystkich informacji złożyć oprogramowanie (docelowo całe urządzenie)  dla złożonego urządzenia:  na przykład kosiarki(robota), automatyki domowej, przyrządu pomiarowego (na przykład woltomierza, amperomierza, generatora sygnałowego, oscyloskopu), odbiornika SDR i tak dalej. Wspólną częścią wszystkich tych (i innych) projektów jest współpraca składowych sprzętowych i komunikacja z otoczeniem - szczególnie na poziomie oprogramowania. Stąd wziął się pomysł artykułu.

Pisząc ten artykuł, chciałbym w maksymalnym stopniu wykorzystać materiały z forum, bo po co pisać o czymś, co jest dobrze opisane gdzie indziej i powszechnie dostępne. Na początek powinienem pisać o budowie i działaniu procesora. Lecz ten temat znajduje się na forum pod adresem https://forbot.pl/blog/mikrokontroler-wszystko-co-powinniscie-wiedziec-o-jego-dzialaniu-id1314 (autor GAndaLF). Zatem zacznę od oprogramowania. Jako środowisko programowania i płytkę będę używał Arduino/ATMega32, gdyż ten układ oprogramowanie/sprzęt jest chyba najbardziej popularny. Moja płytka to Arduino DueMilanove (AtMega32, zasilanie 5V, zegar 16MHz). W środowisku Arduino można programować też inne rodziny procesorów, np STM32, ESP32,czy też RP2040 czy RP2350. We wszystkich przykładach używam wyłącznie funkcji wbudowanych. Zatem wszystko, co napiszę, powinno dać się uruchomić na wszystkich rodzinach procesorów obsługiwanych przez Arduino. 

Pierwszym programem, którym chciałbym się zająć, jest BareMinimum. Kod programu wygląda tak (File\Examples\01.Basics\BareMinimum):

void setup() {
  // put your setup code here, to run once:
}

void loop() {
  // put your main code here, to run repeatedly:
}

Co robi ten program? NIC? Nieprawda. Na mojej płytce marnuje cykle zegarowe procesora z prędkością 16 milionów cykli na sekundę. Poza tym kompiluje się bez błędów i poprawnie wgrywa na płytkę – jest to wymaganie, które MUSI spełnić KAŻDY program. Natomiast faktycznie, ten program nie robi nic użytecznego.

Kolejnym programem jest Blink (File\Examples\01.Basics\Blink).

void setup() {
  pinMode(LED_BUILTIN, OUTPUT); // initialize digital pin LED_BUILTIN as an output.
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  // LED pin setting to the HIGH voltage level
  delay(1000);                      // wait for a second
  digitalWrite(LED_BUILTIN, LOW);   // LED pin setting to the LOW voltage level
  delay(1000);                      // wait for a second
}

Co robi ten program? W porównaniu z poprzednim coś użytecznęgo (?) – włącza i wyłącza diodę. Również i on marnuje cykle zegarowe z prędkością prawie 16 milionów cykli na sekundę. Do wykonania instrukcji digitalWrite() plus narzut kompilatora potrzeba kilkadziesiąt cykli zegarowych, zaś instrukcja delay() służy do opóźnienia wykonania następnej instrukcji, które osiąga się za pomocą marnowania instrukcji procesora. Poniżej przedstawiam program Blink działający bez użycia instrukcji delay(), w którym opóźnienie zrealizowałem za pomocą pustej pętli.

void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  //  LED pin set to the HIGH voltage level
  //***** delay(1000);                      // wait for a second
  for (volatile long i = 0; i < 400000; i++) {
    // Puste wnętrze
  }
  digitalWrite(LED_BUILTIN, LOW);   // LED pin set to the LOW voltage level
  //***** delay(1000);                      // wait for a second
  for (volatile long i = 0; i < 400000; i++) {
    // Puste wnętrze
  }
}

Jak widać zamieniłem funkcję biblioteczną delay() na pustą pętlę for() {}. Sposób, w jaki użyłem tę pętlę, wymaga moim zdaniem wyjaśnień

volatile long i: Zmienna i jest licznikiem pętli. Kompilator języka C optymalizuje kod tak, aby kod wykonywał się maksymalnie szybko i zużywał jak najmniej zasobów. Słowo kluczowe volatile to wskazówka dla kompilatora, aby nie optymalizował zmiennej „i” oraz jej wykorzystania. Bez volatile kompilator usunąłby pętlę.

400000: Ilość obiegów pętli, dobrana tak, aby dla procesora o taktowaniu 16 MHz (moja płytka) odliczanie trwało około sekundy. Jedno przejście pętli zajmuje procesorowi kilkadziesiąt (ok. 40) cykli zegara.

W tym zastosowaniu pętla for(){} jest znacznie gorsza od funkcji bibliotecznej delay(). Jest niedokładna (nie wiadomo, ile cykli zegarowych trwa jeden obieg pętli, procesor zużywa ponadto trochę cykli na obsługę przerwań systemowych), liczbę obiegów trzeba dopasowywać dla procesora i zegara. Powyższy przykład zaprezentowałem po to, aby pokazać, że funkcja delay() to po prostu marnowanie mocy obliczeniowej procesora. Można wykorzystać tę moc w bardziej sensowny sposób, na przykład dodając możliwość zmiany częstotliwości migania diody w trakcie działania programu za pomocą polecenia wydanego z zewnętrznej konsoli.

Zanim to zrobię, pokażę, w jaki sposób działa funkcja millis(). Zwraca ona czas, jaki upłynął od momentu włączenia procesora w milisekundach. Podobnie jak funkcja delay(), jest ona dostępna bezpośrednio w kompilatorze (funkcja wbudowana - to znaczy dostępna bez dołączania zewnętrznych bibliotek za pomocą #include). Chcę wyświetlić na konsoli czas wykonania tej pętli. Aby to osiągnąć, wykorzystam kolejne funkcje wbudowane, obsługujące transmisję szeregową: serial.begin()i serial.print(). Środowisko Arduino (ja używam wersję 2.3.10) posiada wbudowany wbudowany monitor portu szeregowego, który podłącza się automatycznie do płytki. Pomysł na następne przykłady zaczerpnąłem z kursu Arduino ( https://forbot.pl/blog/kurs-arduino-ii-wielozadaniowosc-opoznienia-z-millis-id18418 ), który pokazuje sposób na realizowanie wielozadaniowości.

Zatem pierwszą rzeczą, którą chcę zademonstrować, jest pokazanie niestabilności czasu wykonania pętli. Chcę zacząć od nieznacznie zmodyfikowanego poprzedniego programu – usunąłem niepotrzebne linie programu

void setup() {
  pinMode(LED_BUILTIN, OUTPUT); // pin LED_BUILTIN zdefiniowany jako wyjście
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  //  pin LED_BUILTIN ustawiony na poziom napięcia HIGH
  for (volatile long i = 0; i < 400000; i++) { /* Puste wnętrze */ }

  digitalWrite(LED_BUILTIN, LOW);   //  pin LED_BUILTIN ustawiony na poziom napięcia LOW
  for (volatile long i = 0; i < 400000; i++)  { /* Puste wnętrze */}
}

Kolejną modyfikacją jest dodanie obsługi wysyłania komunikatu na konsolę

void setup() {
  pinMode(LED_BUILTIN, OUTPUT); // pin LED_BUILTIN zdefiniowany jako wyjście
  Serial.begin(9600);           // wstępnie zdefiniowana prędkość transmisji monitora Arduino
  Serial.println("Start programu"); // test transmisji danych
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  //  pin LED_BUILTIN ustawiony na poziom napięcia HIGH
  for (volatile long i = 0; i < 400000; i++)  { /* Puste wnętrze */ }
  digitalWrite(LED_BUILTIN, LOW);   //  pin LED_BUILTIN ustawiony na poziom napięcia LOW
  for (volatile long i = 0; i < 400000; i++)  { /* Puste wnętrze */}
}

W funkcji setup() włączyłem obsługę portu portu szeregowego i wysłałem na konsolę tekst „Start programu” jako test działania komunikacji. Komunikat ten pojawi się zawsze na początku działania programu, na przykład zaraz po jego wgraniu lub po naciśnięciu przycisku „RESET” na płytce. Ten komunikat pojawi się też po resecie programowym (skok pod adres pamięci 0) lub po zadziałaniu watchdoga (Watchdog Timer – WDT). Watchdog (pies zegarowy) zostanie użyty w jednej z kolejnych części tutoriala.

Kolejną modyfikacją jest wyświetlenie czasu wykonania pętli.

void setup() {
  pinMode(LED_BUILTIN, OUTPUT); // pin LED_BUILTIN zdefiniowany jako wyjście
  Serial.begin(9600);           // wstępnie zdefiniowana prędkość transmisji monitora Arduino
  Serial.println("Start programu"); // komunikat na konsolę
}

void loop() {
  long start; // czas na poczatku petli
  int czas;  // roznica czasu: czas na koncu petli - czas na początku
  digitalWrite(LED_BUILTIN, HIGH);  //  pin LED_BUILTIN ustawiony na poziom napięcia HIGH
  start = millis(); //właczenie stopera
  for (volatile long i = 0; i < 400000; i++)  { /* Puste wnętrze */ }
  czas = millis() - start; // odczyt stopera
  Serial.println(czas);   // na konsole 

  digitalWrite(LED_BUILTIN, LOW);   //  pin LED_BUILTIN ustawiony na poziom napięcia LOW
  start = millis(); //właczenie stopera
  for (volatile long i = 0; i < 400000; i++)  { /* Puste wnętrze */ }
  czas = millis() - start; // odczyt stopera
  Serial.println(czas);   // na konsole 
}

Powyższy program ma kilka wad. Pierwsza to czas działania pętli. Moja płytka przełączenie diody realizuje w czasie między 879 a 881 ms – zbyt szybko i niestabilnie. O ile średni czas można poprawić zwiększając wartość końcową licznika pętli z 400000 do większej wartości (jakiej? ustalić doświadczalnie? Dla każdej płytki indywidualnie?), Druga to niestabilność odmierzania czasu. Z rozrzutem czasów przełączenia nic w ten sposób nie zrobimy. Radykalnym rozwiązaniem problemu może być zamiana pustej pętli z eksperymentalnie znalezionej ilości obiegów na pętlę odmierzającą czas, wykorzystującą funkcję millis(). Poniżej zamieszczam program

void setup() {
  pinMode(LED_BUILTIN, OUTPUT); // pin LED_BUILTIN zdefiniowany jako wyjście
  Serial.begin(9600);           // wstępnie zdefiniowana prędkość transmisji monitora Arduino
  Serial.println("Start programu"); // komunikat na konsolę
}

void loop() {
  long start; // czas na poczatku petli
  int czas;  // roznica czasu: czas na koncu petli - czas na początku
  digitalWrite(LED_BUILTIN, HIGH);  //  pin LED_BUILTIN ustawiony na poziom napięcia HIGH
  start = millis();
  for(;;) {
     czas = millis() - start;
     if(czas >=1000) break;
     }
  Serial.println(czas);

  digitalWrite(LED_BUILTIN, LOW);   //  pin LED_BUILTIN ustawiony na poziom napięcia LOW
  start = millis();
  do  { czas = millis() - start; }
     while(czas < 1000);  
  Serial.println(czas);
}

Powyższy program przełącza diodę dokładnie co 1 sekundę i jest stabilny oraz niewrażliwy na zmianę płytek, w tym częstotliwości ich zegarów. (Odpowiedź na pytanie dlaczego wymaga wglądu w dokumentację i kod inicjujący procesora – jak jest realizowana funkcja millis(). ). Drugą wadą jest powielenie kodu – włączenie diody jest realizowane przez inny fragment kodu niż kod włączający diodę. I najważniejsza wada – powyższy program w dalszym ciągu marnuje cykle zegarowe, tyle że w inny sposób. Poniżej pokazuję skróconą wersję programu, w którym przełączanie diody jest realizowane poza pętlą główną, przez funkcję zewnętrzną. Przy okazji chciałem pokazać, że sposób organizacji pętli nie ma znaczenia. Ważne jest tylko, aby odmierzyła zadaną liczbę milisekund.

void zmien() {
if(digitalRead(LED_BUILTIN) == HIGH) digitalWrite(LED_BUILTIN, LOW); else digitalWrite(LED_BUILTIN, HIGH);
}

void setup() {
pinMode(LED_BUILTIN, OUTPUT); // pin LED_BUILTIN zdefiniowany jako wyjście
Serial.begin(9600); // wstępnie zdefiniowana prędkość transmisji monitora Arduino
Serial.println("Start programu"); // komunikat na konsolę
}

void loop() {
long start; // czas na poczatku petli
int czas; // roznica czasu: czas na koncu petli - czas na początku

zmien();
start = millis();
for(;;)
{
czas = millis() - start;
if(czas >=1000) break;
}
Serial.println(czas);
}

Przy okazji chciałbym tu poruszyć problem przepełnienia funkcji millis(). Konstrukcja

czas = millis() - start

liczy poprawnie różnicę czasu niezależnie od tego, czy w międzyczasie nastąpiło przepełnienie w funkcji millis(), czy też nie. Jest to związane z faktem, że obliczenia całkowitoliczbowe w komputerze wykonywane są dla liczb longint jako modulo(MAXLONG + 1).

W kolejnym kroku zmodyfikuję pętlę główną, która jest wykonywana w nieskończoność. Zawiera ona kolejną pętlę wykonywaną w nieskończoność ( for(;;) ) przerywaną po spełnieniu warunku. Po wyeliminowaniu tej pętli program przyjmuje następującą postać:

// stałe
#define LED_PERIOD 1000 // okres przełączania

// zmienne globalne

long PrevLed;

void zmien() {
if(digitalRead(LED_BUILTIN) == HIGH) digitalWrite(LED_BUILTIN, LOW); else digitalWrite(LED_BUILTIN, HIGH);
PrevLed = millis();
}

void setup() {
pinMode(LED_BUILTIN, OUTPUT); // pin LED_BUILTIN zdefiniowany jako wyjście
Serial.begin(9600); // wstępnie zdefiniowana prędkość transmisji monitora Arduino
Serial.println("Start programu"); // komunikat na konsolę
PrevLed = millis(); // inicjalizacja 
}

void loop() {
if (millis() - PrevLed >= int(LED_PERIOD) ) zmien();
}

Powyższy program też przełącza diodę co 1 sekundę i niszczy cykle zegarowe w takim samym tempie jak poprzednie. Różnica jest zasadnicza – w tym programie mogę dodać bez żadnych problemów wiele funkcji analogicznych jak zmien(), wykorzystując niszczone dotychczas cykle do wykonania konkretnej pracy i to bez zakłócania migania diody,czyli funkcji zmien(). Można go jeszcze ulepszyć umieszczając proces przełączania diody w funkcji zewnętrznej wywoływanej z pętli głównej. Funkcja zewnętrzna z punktu widzenia pętli głównej staje się zadaniem do wykonania.

// stałe
#define LED_PERIOD 1000 // okres przełączania

void zmien() {
static long PrevLed;  
if (millis() - PrevLed >= int(LED_PERIOD) ) {
     if(digitalRead(LED_BUILTIN) == HIGH) digitalWrite(LED_BUILTIN, LOW); else digitalWrite(LED_BUILTIN, HIGH);
     PrevLed = millis();
  }
}

void setup() {
pinMode(LED_BUILTIN, OUTPUT); // pin LED_BUILTIN zdefiniowany jako wyjście
Serial.begin(9600); // wstępnie zdefiniowana prędkość transmisji monitora Arduino
Serial.println("Start programu"); // komunikat na konsolę
}

void loop() {
  zmien();
}

W kolejnym programie chciałbym policzyć ilość obiegów funkcji loop() zrealizowanych w czasie 1 sekundy, pozostawiając miganie diody bez zmian. Realizuje to funkcja task_nuda(). Przy okazji zmieniłem nazwę funkcji zmien() na task_dioda().

// stałe
#define LED_PERIOD 1000 // okres przełączania diody

void task_dioda() {
static long PrevLed = 0;  
if (millis() - PrevLed >= int(LED_PERIOD) ) {
     if(digitalRead(LED_BUILTIN) == HIGH) digitalWrite(LED_BUILTIN, LOW); else digitalWrite(LED_BUILTIN, HIGH);
     PrevLed = millis();
  }
}

void task_nuda() { // liczba wywołań funkcji w czasie 1 sekundy
  static long PrevPrnt = 0;
  static long Licznik = 0;
  if(millis() - PrevPrnt >= 1000) {
    Serial.println(Licznik);
    Licznik = 0;
    PrevPrnt = millis();
  }
  Licznik += 1;

}

void setup() {
pinMode(LED_BUILTIN, OUTPUT); // pin LED_BUILTIN zdefiniowany jako wyjście
Serial.begin(9600); // wstępnie zdefiniowana prędkość transmisji monitora Arduino
Serial.println("Start programu"); // komunikat na konsolę
}

void loop() {
  task_dioda();
  task_nuda();
}

Po tej modyfikacji program dalej głównie marnuje cykle zegarowe. Ponadto przełącza diodę i dodatkowo liczy, ile razy została wywołana funkcja task_nuda, czyli ile razy została wykonana pętla loop(). Na mojej płytce otrzymuję wyniki w przedziale 186 – 188 tysięcy obiegów pętli loop() na sekundę. Pokazuje to wprost gigantyczne marnotrawstwo mocy obliczeniowej procesora. Przy okazji demonstruje też, jak pisać programy, aby wiele zadań wykonywało się praktycznie równolegle i ile mocy obliczeniowej mamy do dyspozycji.

W tej chwili program blink jest już systemem operacyjnym czasu rzeczywistego pracującym w trybie kooperacji zadań – w ten sposób pracował system operacyjny WINDOWS 3.1. Pętla loop() stała się schedulerem pracującym w trybie round-robin (dookoła Wojtek, w kółko Macieju). Program zajmuje u mnie 2374 bajtów (7%) pamięci programu i 214 bajtów (10%) pamięci operacyjnej. Zadania są wywoływane po upływie zadanego czasu - synchronicznie. Nie ma ograniczeń na długość czasu wykonywania zadania, chociaż w trakcie wykonywania każdego zadania wszystkie pozostałe nie są wykonywane.

W kolejnym odcinku mam zamiar pokazać wywoływanie asynchroniczne zadań (przycisk, lub kawałek przewodu połączony do pinu w trybie wyjścia i przyłączany lub nie do masy). Ponadto zastosowanie watchdoga do resetowania systemu – systemy kooperujące są wrażliwe na zawieszenie - jeżeli jakiekolwiek zadanie nie odda sterowania. Oprócz tego spróbuję dodać prosty interpreter komend pozwalający na programową (z konsoli Arduino) zmianę częstotliwości migania diody.

 

Powyższe napisałem korzystając z informacji uzyskanych z czatów ze sztuczną inteligencją (ChatGPT, Google Gemini).

Poprawka w kodzie: funkcje task_dioda() i task_nuda() powinny wyglądać tak:

void task_dioda() {
static unsigned long PrevLed = 0L;  
if (millis() - PrevLed >= int(LED_PERIOD) ) {
     if(digitalRead(LED_BUILTIN) == HIGH) digitalWrite(LED_BUILTIN, LOW); else digitalWrite(LED_BUILTIN, HIGH);
     PrevLed = millis();
  }
}

void task_nuda() { // liczba wywołań funkcji w czasie 1 sekundy
  static unsigned long PrevPrnt = 0L;
  static unsigned long Licznik = 0L;
  if(millis() - PrevPrnt >= 1000) {
    Serial.println(Licznik);
    Licznik = 0;
    PrevPrnt = millis();
  }
  Licznik += 1;
}

Sprawdziłem jeszcze raz. Okazało się, że to czy użyje się zmiennej ze znakiem, czy też nie, nie ma znaczenia. Dla zmiennej ze znakiem pierwsze przepełnienie nastąpi po ok. 25,5 dnia, a kolejne po ok. 51 dni. 

Edytowano przez Kogut007
Uwagi do błędów
20 godzin temu, Kogut007 napisał:

Powyższe napisałem korzystając z informacji uzyskanych z czatów ze sztuczną inteligencją (ChatGPT, Google Gemini).

A, to widać.

20 godzin temu, Kogut007 napisał:
static long PrevLed = 0;  
if (millis() - PrevLed >= int(LED_PERIOD) ) {

A Katowice są w północnej Polsce.

Weź to poraw bo aż w oczy kłuje. A następnym razem jak korzystasz z AI to czytaj to to Ci wypisała.

(edytowany)

Do ethank. A jak to miałem napisać? Cytowany wyjątek jest został wzięty z funkcji task_dioda(), wywoływanej z funkcji  loop() ponad  150 tysięcy razy na sekundę (o czym program informuje). Zmienna PrevLed jest zadeklarowana jako static, zatem w kolejnym wywołaniu zachowuje wartość uzyskaną w wywołaniu poprzednim. Dobrą praktyką jest nadawanie zmiennym jakiejś sensownej wartości początkowej. Poza tym pytanie, które powinieneś sobie zadać na początku - jakim cudem to działa? Co do AI - Ten fragment faktycznie pochodzi z dyskusji z AI - jej wynikiem jest rozwiązanie legendarnego problemu zawisu systemu używającego millis() po 51 dniach na skutek przepełnienia wewnętrznego licznika. Warunek użyty w funkcjach jest niewrażliwy na to przepełnienie.

Edytowano przez Kogut007
Uzupełnienie odpowiedzi
33 minuty temu, Kogut007 napisał:

Zmienna PrevLed jest zadeklarowana jako static,

To prawidłowo, ale...

1 godzinę temu, ethanak napisał:
static long PrevLed = 0;  

to już nie.

 

Dlaczego? Podstawienie do PrevLed zera wykonuje się raz na starcie działania programu. Zresztą kompilator zeruje obszary zmiennych, ale robi to raz podczas ładowania programu do pamięci. Co będzie, jesli zajdzie restart programowy - na przykład zadziała watchdog? Jaką wartość otrzyma po restarcie zmienna PrevLed - losową? I mamy wówczas losowe zawieszenie programu na losową liczbę milisekund.  

(edytowany)

Do orb777 Dokumentacja też może zawierać błędy. Poniżej przedstawiam wyjaśnienie problemu otrzymane od SI:

Cytat

Jak wyjaśnić „magię” modulo?

Warto użyć analogii licznika kilometrów w samochodzie lub tradycyjnego zegara wskazówkowego.

Przykład z zegarem (Modulo 12)

Jest godzina 10:00 (previousMillis).

Sprawdzamy czas później i jest godzina 2:00 (currentMillis) – licznik przeszedł przez punkt zero (przepełnił się).

Ile czasu upłynęło? Intuicyjnie wiemy, że 4 godziny.

Jak to liczy matematyka modulo?


W systemie modulo 12:


Wynik jest idealny, mimo że czas "się cofnął".

Cytat

Dowód w C++ (Modulo)

W Arduino Uno zmienna typu unsigned long jest 32-bitowa. Jej maksymalna wartość to 4 294 967 295 (0xFFFFFFFF).

Jeśli nasze zadanie ma się wykonać po 1000 ms, a licznik zaraz się przepełni:

previousMillis = 4 294 967 290 (5 milisekund przed końcem zakresu)

Mijają 1000 milisekund, licznik się przepełnia i "zeruje".

currentMillis = 995

Gdy w kodzie wykonujemy standardowe sprawdzenie warunku:

cpp

if (currentMillis - previousMillis >= interval)

Procesor wykonuje operację:

Ponieważ operujemy na typie unsigned (bez znaku), wynik ujemny nie istnieje. Następuje automatyczne przekręcenie licznika (niejawne rzutowanie modulo

😞

Wynik odejmowania to dokładnie 1000. Warunek 1000 >= 1000 staje się prawdziwy. System działa nieprzerwanie i bezbłędnie.

  Poza tym możesz sprawdzić to doświadczalnie. Zmień typ zmiennej PrevLed na static short i podstaw pod nią wynik rzutowania millis() na short. Licznik przepełni się po ok. 65 sekundach.

Edytowano przez Kogut007
Uzupełnienie
17 minut temu, Kogut007 napisał:

Dokumentacja też może zawierać błędy.

Ta akurat nie zawiera. Katowice nie leżą nad morzem.

 

18 minut temu, Kogut007 napisał:

Poniżej przedstawiam wyjaśnienie problemu otrzymane od SI

Tyle że to nie ten problem.

@orb777 ciekawe kiedy koleś wpadnie na to co spieprzył 🙂 W każdym razie moim zdaniem jak się próbuje pisać podręcznik to warto najpierw jakiś przeczytać... ale w końcu kto by tam czytał dokumentacje, długie i nudne 🙂

 

  • Lubię! 1
(edytowany)

Czy to takie trudne napisać, co zostało spieprzone? Czy Koledzy pamiętają dowcip o pierwszej kobiecej wyprawie w Kosmos? Poza tym: dlaczego działa, jeśli zostało spieprzone?

Edytowano przez Kogut007
Uzupełnienie odpowiedzi
1 minutę temu, Kogut007 napisał:

Czy to takie trudne napisać, co zostało spieprzone?

A czy to takie trudne sprawdzić co czaty giepaty wymyślą przed publikacją artykułu? 

Na razie swój artykuł lepiej wyrzuć do kosza bo po prostu piszesz nieprawdę (i to nie w jednym miejscu). 

1 godzinę temu, Kogut007 napisał:

Padło mi na wzrok?

Nie, to akurat mi padło, ale się staram!

Dziękuję. Mnie czasami też pada. Przy okazji: wiesz jaka jest różnica między młodym i starym informatykiem? otóż młody uważa, że 1 kB to 1000 bajtów. Stary uważa, że 1 km to 1024 m.

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...