Sonlu otomata yumuşak grafikler ile. Sonlu durum makineleri, zaparok olmadan nasıl programlanır. Makinelerin pratik uygulaması

  • 07.03.2020

Durum makinesi, bir şeyin sonlu sayıda durumunu içeren soyut bir modeldir. Herhangi bir komutun yürütme akışını temsil etmek ve kontrol etmek için kullanılır. Durum makinesi, oyunlarda yapay zeka uygulamak, hantal ve karmaşık kodlar yazmadan düzgün bir çözüm elde etmek için idealdir. Bu yazıda, teoriyi ele alacağız ve ayrıca basit ve yığın tabanlı bir durum makinesinin nasıl kullanılacağını öğreneceğiz.

Halihazırda bir durum makinesi kullanarak yapay zeka yazmak üzerine bir dizi makale yayınladık. Bu seriyi henüz okumadıysanız, şimdi okuyabilirsiniz:

Sonlu durum makinesi nedir?

Bir sonlu durum makinesi (veya basitçe FSM - Sonlu durum makinesi), varsayımsal bir durum makinesine dayanan bir hesaplama modelidir. Aynı anda sadece bir durum aktif olabilir. Bu nedenle, herhangi bir eylemi gerçekleştirmek için makinenin durumunu değiştirmesi gerekir.

Durum makineleri genellikle bir şeyin yürütme akışını düzenlemek ve temsil etmek için kullanılır. Bu, özellikle oyunlarda AI uygularken kullanışlıdır. Örneğin, düşmanın "beynini" yazmak için: her durum bir tür eylemi temsil eder (saldırı, kaçma vb.).

Sonlu bir otomat, köşeleri durumlar ve kenarları aralarındaki geçişler olan bir grafik olarak temsil edilebilir. Her kenar, geçişin ne zaman gerçekleşmesi gerektiğini gösteren bir etikete sahiptir. Örneğin, yukarıdaki resimde, oyuncunun yakınlarda olması koşuluyla, otomatın durumu "gezinme" durumundan "saldırı" durumuna değiştireceğini görebilirsiniz.

Zamanlama durumları ve geçişleri

Bir sonlu durum makinesinin uygulanması, durumlarının ve aralarındaki geçişlerin tanımlanmasıyla başlar. Bir karınca yuvasına yaprak taşıyan bir karıncanın hareketlerini tanımlayan bir durum makinesi düşünün:

Başlangıç ​​noktası, karınca yaprağı bulana kadar aktif kalan "yaprak bul" durumudur. Bu olduğunda, durum "eve git" olarak değişecektir. Karıncamız karınca yuvasına gelene kadar aynı durum aktif kalacaktır. Bundan sonra durum tekrar "yaprak bul" olarak değişir.

"Yaprak bul" durumu etkinse, ancak fare imleci karıncanın yanındaysa, durum "kaçmak" olarak değişir. Karınca, fare imlecinden yeterince güvenli bir mesafeye gelir gelmez, durum tekrar "yaprak bul" olarak değişecektir.

Lütfen eve giderken veya evden çıkarken, karıncanın fare imlecinden korkmayacağını unutmayın. Neden? Niye? Ve karşılık gelen bir geçiş olmadığı için.

Basit bir durum makinesinin uygulanması

Bir durum makinesi, tek bir sınıf kullanılarak uygulanabilir. FSM diyelim. Fikir, her durumu bir yöntem veya işlev olarak uygulamaktır. Aktif durumu belirlemek için activeState özelliğini de kullanacağız.

Genel sınıf FSM ( private var activeState:Function; // otomatın aktif durumuna işaretçi genel fonksiyon FSM() ( ) genel fonksiyon setState(state:Function) :void ( activeState = state; ) public function update() :void ( if ( activeState != null) ( activeState(); ) ) )

Her durum bir fonksiyondur. Üstelik oyun çerçevesi her güncellendiğinde çağrılacak şekilde. Daha önce de belirtildiği gibi, activeState, aktif durum işlevine bir işaretçi saklayacaktır.

FSM sınıfının update() yöntemi her oyun karesinde çağrılmalıdır. Ve sırayla, şu anda aktif olan devletin işlevini arayacak.

setState() yöntemi, yeni etkin durumu ayarlayacaktır. Ayrıca, otomatın bazı durumlarını tanımlayan her fonksiyonun FSM sınıfına ait olması gerekmez - bu, sınıfımızı daha evrensel hale getirir.

Durum makinesi kullanma

Karınca AI'yı uygulayalım. Yukarıda, hallerini ve aralarındaki geçişleri zaten gösterdik. Onları tekrar gösterelim, ama bu sefer koda odaklanacağız.

Karıncamız, beyin alanı olan Ant sınıfı ile temsil edilmektedir. Bu sadece FSM sınıfının bir örneğidir.

Genel sınıf Ant ( genel var konum:Vector3D; genel var hız:Vector3D; genel var beyin:FSM; genel işlev Ant(posX:Number, posY:Number) ( konum = yeni Vector3D(posX, posY); vehız = yeni Vector3D( -1, -1); beyin = new FSM(); // Bir yaprak arayarak başlayın. beyin. setState(findLeaf); ) /** * "findLeaf" durumu. * Karıncanın yaprak aramasını sağlar. */ public function findLeaf() :void ( ) /** * "goHome" durumu * Karıncanın karınca yuvasına girmesine neden olur */ public function goHome() :void ( ) /** * "runAway" durumu * * / public function runAway() :void ( ) public function update():void ( // Durum makinesini güncelleyin. Bu işlev // aktif durum işlevini çağırır: findLeaf(), goHome () veya runAway(). brain.update(); // Karıncanın hareketine hız uygulayın. moveBasedOnVelocity(); ) (...) )

Ant sınıfı ayrıca hız ve konum özelliklerini de içerir. Bu değişkenler, Euler yöntemini kullanarak hareketi hesaplamak için kullanılacaktır. Oyun çerçevesi her güncellendiğinde update() işlevi çağrılır.

Aşağıda, yaprakları bulmaktan sorumlu durum olan findLeaf() ile başlayan yöntemlerin her birinin uygulaması verilmiştir.

Genel işlev findLeaf() :void ( // Karıncayı yaprağa taşır. hız = new Vector3D(Game.instance.leaf.x - position.x, Game.instance.leaf.y - position.y); if (mesafe (Oyun .instance.leaf, bu)<= 10) { // Муравей только что подобрал листок, время // возвращаться домой! brain.setState(goHome); } if (distance(Game.mouse, this) <= MOUSE_THREAT_RADIUS) { // Курсор мыши находится рядом. Бежим! // Меняем состояние автомата на runAway() brain.setState(runAway); } }

goHome() durumu, karıncanın eve gitmesini sağlamak için kullanılır.

Genel işlev goHome() :void ( // Karıncayı ev hızına taşır = new Vector3D(Game.instance.home.x - position.x, Game.instance.home.y - position.y); if (distance( Oyun. instance.home, bu)<= 10) { // Муравей уже дома. Пора искать новый лист. brain.setState(findLeaf); } }

Ve son olarak, runAway() durumu - fare imlecinden kaçarken kullanılır.

Genel işlev runAway() :void ( // Karıncayı imleç hızından uzaklaştırır = new Vector3D(position.x - Game.mouse.x, position.y - Game.mouse.y); // İmleç hala etrafta mı? ? if ( Distance(Game.mouse, this) > MOUSE_THREAT_RADIUS) ( // Hayır, zaten çok uzakta. Yaprakları bulmaya geri dönme zamanı. brain.setState(findLeaf); ) )

FSM İyileştirmesi: Yığın Tabanlı Otomat

Eve dönüş yolundaki karıncanın da fare imlecinden kaçması gerektiğini hayal edin. FSM eyaletleri şöyle görünecek:

Önemsiz bir değişiklik gibi görünüyor. Hayır, böyle bir değişiklik bizim için sorun yaratır. Mevcut durumun "kaçmak" olduğunu hayal edin. Fare imleci karıncadan uzaklaşırsa ne yapmalı: eve mi gitsin yoksa yaprak mı ara?

Bu sorunun çözümü yığın tabanlı bir durum makinesidir. Yukarıda uyguladığımız basit FSM'den farklı olarak, bu tür FSM, durumları yönetmek için bir yığın kullanır. Yığının en üstünde aktif durum bulunur ve yığından durumlar eklendiğinde/çıkarıldığında geçişler meydana gelir.

Ve işte yığına dayalı bir durum makinesinin çalışmasının görsel bir gösterimi:

Yığın tabanlı bir FSM'nin uygulanması

Böyle bir sonlu durum makinesi, basit bir makineyle aynı şekilde uygulanabilir. Fark, gerekli durumlara yönelik bir dizi işaretçinin kullanılması olacaktır. Artık activeState özelliğine ihtiyacımız olmayacak, çünkü yığının üst kısmı zaten aktif duruma işaret edecektir.

Genel sınıf StackFSM ( private var stack:Array; public function StackFSM() ( this.stack = new Array(); ) public function update() :void ( var currentStateFunction:Function = getCurrentState(); if (currentStateFunction != null) ( currentStateFunction(); ) ) genel işlev popState() :Function ( dönüş stack.pop(); ) genel işlev pushState(durum:Function) :void ( if (getCurrentState() != durum) ( stack.push(durum) ; ) ) genel işlev getCurrentState() :Function ( dönüş yığın.uzunluk > 0 ? yığın: boş; ) )

setState() yönteminin pushState() (yığının en üstüne yeni durum eklenmesi) ve popState() (yığının tepesinden durumu kaldırma) ile değiştirildiğini unutmayın.

Yığın tabanlı FSM'yi kullanma

Yığın tabanlı bir durum makinesi kullanırken, her durumun gerekmediğinde kendisini yığından çıkarmaktan sorumlu olduğunu unutmamak önemlidir. Örneğin, düşman zaten yok edilmişse, saldırı() durumu kendisini yığından kaldırmalıdır.

Public class Ant ( (...) public var brain:StackFSM; public function Ant(posX:Number, posY:Number) ( (...) brain = new StackFSM(); // Bir yaprak beyin arayarak başlayın. pushState( findLeaf); (...) ) /** * "findLeaf" durumu * Karıncanın yaprakları aramasını sağlar */ public function findLeaf() :void ( // Karıncayı yaprağa taşır. hız = yeni Vector3D(Game.instance.leaf.x - position.x, Game.instance.leaf.y - position.y); if (mesafe(Game.instance.leaf, bu)<= 10) { //Муравей только что подобрал листок, время // возвращаться домой! brain.popState(); // removes "findLeaf" from the stack. brain.pushState(goHome); // push "goHome" state, making it the active state. } if (distance(Game.mouse, this) <= MOUSE_THREAT_RADIUS) { // Курсор мыши рядом. Надо бежать! // Состояние "runAway" добавляется перед "findLeaf", что означает, // что состояние "findLeaf" вновь будет активным при завершении состояния "runAway". brain.pushState(runAway); } } /** * Состояние "goHome". * Заставляет муравья идти в муравейник. */ public function goHome() :void { // Перемещает муравья к дому velocity = new Vector3D(Game.instance.home.x - position.x, Game.instance.home.y - position.y); if (distance(Game.instance.home, this) <= 10) { // Муравей уже дома. Пора искать новый лист. brain.popState(); // removes "goHome" from the stack. brain.pushState(findLeaf); // push "findLeaf" state, making it the active state } if (distance(Game.mouse, this) <= MOUSE_THREAT_RADIUS) { // Курсор мыши рядом. Надо бежать! // Состояние "runAway" добавляется перед "goHome", что означает, // что состояние "goHome" вновь будет активным при завершении состояния "runAway". brain.pushState(runAway); } } /** * Состояние "runAway". * Заставляет муравья убегать от курсора мыши. */ public function runAway() :void { // Перемещает муравья подальше от курсора velocity = new Vector3D(position.x - Game.mouse.x, position.y - Game.mouse.y); // Курсор все еще рядом? if (distance(Game.mouse, this) >MOUSE_THREAT_RADIUS) ( // Hayır, zaten çok uzakta. Yaprak aramaya geri dönme zamanı. brain.popState(); ) ) (...) )

Çözüm

Durum makineleri, oyunlarda yapay zeka mantığını uygulamak için kesinlikle yararlıdır. Geliştiricinin tüm olası seçenekleri görmesini sağlayan bir grafik olarak kolayca temsil edilebilirler.

Durum işlevlerine sahip bir durum makinesi uygulamak, basit ama güçlü bir tekniktir. FSM kullanılarak daha da karmaşık durum karışıklıkları uygulanabilir.

Bu makalede, "durum makinesi" terimi, az sayıdaki durumdan birinde olabilen bir algoritmayı ifade eder. "Durum", giriş ve çıkış sinyallerinin yanı sıra giriş sinyalleri ve sonraki durumların belirli bir ilişkisini tanımlayan belirli bir durumdur. Akıllı okuyucu, bu makalede açıklanan durum makinelerinin Mealy makineleri olduğunu hemen not edecektir. Bir Mealy makinesi, çıktıların yalnızca durumun işlevleri olduğu Moore makinesinin aksine, çıktıların mevcut durumun ve girdinin işlevleri olduğu sonlu durumlu bir makinedir. Her iki durumda da sonraki durum, mevcut durumun ve giriş sinyalinin bir fonksiyonudur.

Basit bir sonlu durum makinesi örneğini düşünün. Bir metin dizesinde "//" karakter dizisini tanımanız gerektiğini hayal edin. Şekil 1, bunun bir durum makinesi kullanılarak nasıl yapıldığını gösterir. Eğik çizginin ilk oluşumu bir çıkış sinyali üretmez, ancak otomatın ikinci duruma girmesine neden olur. Otomat ikinci durumda bir eğik çizgi bulamazsa, arka arkaya 2 eğik çizgiye ihtiyaç duyduğu için ilkine döner. İkinci eğik çizgi bulunursa, otomat bir "hazır" sinyali verir.

Müşterinin neye ihtiyacı olduğunu öğrenin.

Bir Durum Geçiş Şeması çizin

Geçiş işlemlerini detaylandırmadan durum makinesinin "iskeletini" kodlayın.

Geçişlerin düzgün çalıştığından emin olun.

Geçişlerin ayrıntılarını belirtin.

Test yapmak.

Durum makinesi örneği

Sonlu durum makinesinin daha ilginç bir örneğini ele alalım - uçağın iniş takımının geri çekilmesini ve uzatılmasını kontrol eden bir program. Çoğu uçakta bu prosedür bir elektro-hidrolik kontrol mekanizması kullanılarak gerçekleştirilse de (sadece uçakta bilgisayar olmadığı için), insansız hava araçları gibi bazı durumlarda yazılım kontrolünü kullanmaya değer.

İlk önce, ekipmanla ilgilenelim. Uçağın iniş takımı, ön iniş takımı, ana sol iniş takımı ve sağ ana iniş takımından oluşur. Hidrolik bir mekanizma ile tahrik edilirler. Elektrikle çalışan bir hidrolik pompa, güç aktüatörüne basınç sağlar. Yazılımı kullanarak pompa açılır veya kapatılır. Bilgisayar yön valfinin konumunu ayarlar - "aşağı" veya "yukarı", basıncın kasayı yükseltmesine veya indirmesine izin vermek için. İniş takımı ayaklarının her birinin bir limit anahtarı vardır: bunlardan biri iniş takımı kaldırılırsa kapanır, diğeri - "aşağı" konumda sabitlenirse. Uçağın yerde olup olmadığını belirlemek için, uçağın ağırlığı burun dişlisi üzerindeyse, burun dişlisi üzerindeki limit anahtarı kapanır. Pilotun kontrolleri, bir üst/alt iniş takımı kolundan ve kapatılabilen, yeşil (aşağı konum) veya kırmızı (geçiş konumu) üç ışıktan (her bacak için bir tane) oluşur.

Şimdi sonlu durum makinesinin geliştirilmesine geçelim. İlk ve en zor adım, müşterinin gerçek beklentilerini anlamaktır. Durum makinesinin avantajlarından biri, programcıyı olası tüm durumları düşünmeye ve sonuç olarak müşteriden gerekli tüm bilgileri almaya zorlamasıdır. Neden bunu en zor aşama olarak görüyorum? Ve size kaç kez böyle bir görev tanımı verildi: Uçak yerdeyse iniş takımlarını geri çekmeyin.

Tabii ki, bu önemlidir, ancak müşteri bunun her şeyin bittiği yer olduğuna inanır. Peki ya diğer davalar? Uçak yerden kalktığı anda iniş takımlarını geri çekmek yeterli mi? Uçak pistteki bir tümsek üzerinden sekerse ne olur? Pilot, park sırasında vites kolunu "yukarı" konuma getirir ve sonuç olarak kalkışa başlarsa ne olur? Bu durumda şasi yükselmeli mi?

Durum makinesi açısından düşünmenin faydalarından biri, müşterinin hemen önünde bir projeksiyon panosuna hızlı bir şekilde bir durum geçiş diyagramı çizebilmeniz ve süreci onlarla birlikte gerçekleştirebilmenizdir. Durum geçişinin aşağıdaki tanımı kabul edilir: “geçişe neden olan olay” / “geçişin bir sonucu olarak çıkış sinyali”. Yalnızca müşterinin başlangıçta istediği şeyi geliştirseydik (“uçak yerdeyse iniş takımını geri çekmeyin”), o zaman Şekil 2'de gösterilen makineyi almış olurduk.

Bir durum geçiş diyagramı (veya başka bir algoritma) hazırlarken aşağıdakileri aklınızda bulundurun:

Bilgisayarlar mekanik donanımlara göre çok hızlıdır.

Yapılması gerekenleri anlatan makine mühendisi, bilgisayarlar ve algoritmalar hakkında sizin kadar bilgi sahibi olmayabilir. Ve bu da olumlu bir nokta, aksi takdirde size ihtiyaç duyulmaz!

Mekanik veya elektrikli bir parça bozulursa programınız nasıl davranır?

Müşterinin gerçekten ne istediğini temel alan bir durum makinesi Şekil 3'te gösterilmektedir. Burada, uçağın iniş takımının kesinlikle havada olana kadar geri çekilmesini önlemek istiyoruz. Bunun için iniş şalterini açtıktan sonra makine birkaç saniye bekler. Ayrıca, uçak park halindeyken birisi kolu hareket ettirirse sorunları önleyecek şekilde, seviyeden ziyade pilot kolunun yükselen kenarına yanıt vermek istiyoruz. İniş takımlarının geri çekilmesi veya uzatılması birkaç saniye sürer ve bu işlem sırasında pilotun fikrini değiştirip kolu ters yönde hareket ettireceği duruma hazırlıklı olmalıyız. Ayrıca, "Kalkış bekleniyor" durumundayken uçak tekrar inerse, zamanlayıcının yeniden başlayacağını ve iniş takımlarının yalnızca uçak 2 saniye boyunca havada kaldığında geri çekileceğini unutmayın.

Durum Makinesi Uygulaması

Liste 1, Şekil 3'te gösterilen durum makinesinin uygulamasıdır. Kodun bazı ayrıntılarını tartışalım.

/*listeleme 1*/

typedef numaralandırma(GEAR_DOWN = 0, WTG_FOR_TKOFF, RAISING_GEAR, GEAR_UP, LOWERING_GEAR) Durum_Türü;

/*Bu dizi, belirli durumlarda çağrılacak işlevlere işaretçiler içerir*/

geçersiz(*state_table)() = (GearDown, WtgForTakeoff, RaisingGear, GearUp, LoweringGear);

State_Type curr_state;

InitializeLdgGearSM();

/* Otomatın kalbi bu sonsuz döngüdür. karşılık gelen fonksiyon

Geçerli durum, yineleme başına bir kez çağrılır */

süre (1) {

durum_tablo();

AzaltmaTimer();

geçersiz InitializeLdgGearSM( geçersiz )

curr_state = GEAR_DOWN;

/*Donanımı durdur, ışıkları kapat, vb.*/

geçersiz Vites küçültmek( geçersiz )

/* Uçak varsa bekleme durumuna geç

Yerde değil ve şasiyi kaldırmak için bir komut alındı ​​* /

eğer((gear_lever == YUKARI) && (prev_gear_lever == AŞAĞI) && (squat_switch == YUKARI)) (

curr_state = WTG_FOR_TKOFF;

prev_gear_lever = gear_lever;

geçersiz RaisingGear( geçersiz )

eğer((nosegear_is_up == YAPILMIŞ) && (leftgear_is_up == YAPILMIŞ) && (rtgear_is_up == YAPILMIŞ)) (

curr_state = GEAR_UP;

/*Pilot fikrini değiştirdiyse, iniş takımı aşağı durumuna gidin*/

eğer(gear_lever == AŞAĞI) (

curr_state = LOWERING_GEAR;

geçersiz Vites arttırmak( geçersiz )

/*pilot kolu "aşağı" konuma getirdiyse,

"İniş takımlarını indirme" durumuna git * /

eğer(gear_lever == AŞAĞI) (

curr_state = LOWERING_GEAR;

geçersiz WtgForTakeoff( geçersiz )

/* Kasayı kaldırmadan önce bekleyin. */

eğer(zamanlayıcı<= 0.0) {

curr_state = RAISING_GEAR;

/*Tekrar dokunursak veya pilot fikrini değiştirirse, her şeye yeniden başlayın*/

eğer((squat_switch == AŞAĞI) || (gear_lever == AŞAĞI)) (

curr_state = GEAR_DOWN;

/* Kolu tekrar değiştirmesini istemek istemiyorum

Bu sadece bir sıçramaydı.*/

prev_gear_lever = AŞAĞI;

geçersizİndirme Dişlisi( geçersiz )

eğer(gear_lever == YUKARI) (

curr_state = RAISING_GEAR;

eğer((nosegear_is_down == YAPILMIŞ) && (leftgear_is_down == YAPILMIŞ) &&(rtgear_is_down == YAPILMIŞ)) (

curr_state = GEAR_DOWN;

İlk olarak, her durumun işlevselliğinin ayrı bir C işlevi tarafından uygulandığını fark edebilirsiniz. Elbette, her durum için ayrı bir durum içeren bir switch deyimi kullanarak bir otomat uygulamak mümkün olabilir, ancak bu çok uzun bir işleve yol açabilir (20-30 durumun her biri için 1 durum başına 10-20 satır kod) ). Testin son aşamalarında kodu değiştirirseniz de hatalara yol açabilir. Bir vakanın sonundaki break ifadesini hiç unutmamış olabilirsiniz ama benim böyle vakalarım oldu. Her durum için ayrı bir fonksiyonunuz varsa, bir devletin kodu asla diğerinin koduna girmeyecektir.

Bir switch ifadesi kullanmaktan kaçınmak için, durum işlevlerine yönelik bir dizi işaretçi kullanıyorum ve dizinin dizini olarak kullanılan değişkeni enum türünde ilan ediyorum.

Basit olması için, anahtarların durumunu okumaktan, pompaları açıp kapatmaktan vb. sorumlu G/Ç donanımı basit değişkenler olarak temsil edilir. Bu değişkenlerin, görünmez yollarla ekipmanla ilişkilendirilen "sihirli adresler" olduğu varsayılır.

Bir başka bariz şey de bu noktada kodun özel bir rol oynamamasıdır. Sadece bir eyaletten diğerine geçer. Bu önemli bir ara adımdır ve göz ardı edilmemelidir. Bu arada, giriş sinyallerinin mevcut durumunu ve değerlerini yazdıracak koşullu derleme yönergeleri (#ifdef DEBUG .. #endif) arasına print ifadeleri eklemek güzel olurdu.

Başarının anahtarı, durum geçişine neden olan kodda, yani. girdinin gerçekleştiğini belirler.

Kod tüm durumlardan doğru bir şekilde geçerse, bir sonraki adım kodun "doldurulmasını", yani çıkış sinyalini tam olarak neyin ürettiğini yazmaktır. Her geçişin bir giriş sinyali (onu tetikleyen olay) ve bir çıkış sinyali (örneğimizde olduğu gibi donanım G/Ç cihazı) olduğunu unutmayın. Bunu bir durum geçiş tablosu biçiminde yakalamak genellikle yararlıdır.

Durum geçiş tablosu, durum geçişi başına bir satıra sahiptir.

Bir durum makinesini kodlarken, onu güçlü tutmaya çalışın - müşterinin gereksinimleri ile kod arasında güçlü bir uyum. Örneğin, durum makine kodunun bir durum geçiş tablosu ve bir durum geçiş diyagramı gibi görünmesini sağlamak için donanım ayrıntılarını başka bir işlev katmanında gizlemek gerekebilir. Bu simetri, hataları önlemeye yardımcı olur ve durum makinelerinin gömülü programcı cephaneliğinin neden bu kadar önemli bir parçası olduğunu açıklar. Tabii ki, aynı etkiyi onay kutuları ve sonsuz sayıda iç içe if ifadesi ile elde edebilirsiniz, ancak kodu izlemek ve müşterinin istekleriyle karşılaştırmak çok zor olurdu.

Liste 2'deki kod parçacığı, RaisingGear() işlevini genişletir. RaisingGear() işlevi kodunun Raising Gear durumu için geçiş tablosunun 2 satırını yansıtma eğiliminde olduğunu unutmayın.

geçersiz RaisingGear( geçersiz )

/*Tüm anahtarlar açıldıktan sonra, "şasi yukarı" durumuna gidin*/

eğer((nosegear_is_up == YAPILMIŞ) && (leftgear_is_up == YAPILMIŞ) && (rtgear_is_up == YAPILMIŞ)) (

pompa_motoru=KAPALI;

gear_lights = SÖNDÜR;

curr_state = GEAR_UP;

/*Pilot fikrini değiştirirse, iniş takımını geri çekmeye başlayın*/

eğer(gear_lever == AŞAĞI) (

pump_direction = AŞAĞI;

curr_state = GEAR_LOWERING;

Gizli durumlardan kaçınmayı unutmayın. Gizli durum, tembellikten somut bir durum eklemek yerine koşullu bir alt durum eklemeye çalıştığınızda görünür. Örneğin, kodunuz aynı giriş sinyalini moda bağlı olarak farklı şekillerde işliyorsa (yani farklı durum geçişlerini başlatıyorsa), bu bir gizli durumdur. Bu durumda bu devlet ikiye bölünmemeli mi acaba? Gizli durumların kullanılması, durum makinesi kullanmanın tüm faydalarını ortadan kaldırır.

Uygulama olarak, kasanın geri çekme veya uzatma döngüsüne bir zaman aşımı ekleyerek az önce incelediğimiz durum makinesini genişletebilirsiniz. makine mühendisi hidrolik pompanın 60 saniyeden fazla çalışmasını istemiyor. Döngü sona ererse, pilot yeşil ve kırmızı ışıkların yanması ile uyarılmalı ve tekrar denemek için kolu tekrar hareket ettirebilmelidir. Ayrıca varsayımsal bir makine mühendisine, pompa çalışırken ters yönde hareket etmekten nasıl etkilendiğini sorabilirsiniz, çünkü bu 2 kez olur, pilot fikrini değiştirir. Tabii ki, tamirci bunun olumsuz olduğunu söyleyecektir. O zaman yön değiştirirken pompayı hızlı bir şekilde durdurmak için durum makinesini nasıl değiştirirsiniz?

Durum Makinesi Testi

Sonlu durum makineleri olarak kodlama algoritmalarının güzelliği, test planının neredeyse otomatik olarak kendini yazmasıdır. Tek yapmanız gereken her bir durum geçişinden geçmek. Bunu genellikle elimde bir işaretleyici ile, testi geçerken durum geçiş diyagramındaki okların üzerini çizerek yaparım. Bu, "gizli durumlardan" kaçınmanın iyi bir yoludur - testlerde somut durumlardan daha sık gözden kaçırılırlar.

Orta büyüklükteki bir durum makinesinde bile 100'e kadar farklı geçiş olabileceğinden, bu oldukça sabır ve çok kahve gerektirir. Bu arada, atlama sayısı bir sistemin karmaşıklığını ölçmenin harika bir yoludur. İkincisi, müşterinin gereksinimlerine göre belirlenir ve durum makinesi, test kapsamını açık hale getirir. Daha az organize bir yaklaşımla, gereken test miktarı aynı derecede etkileyici olabilir, ancak bunun hakkında hiçbir şey bilmiyorsunuz.

Mevcut durumu, giriş ve çıkış sinyallerinin değerlerini görüntüleyen kodda print deyimlerini kullanmak çok uygundur. Bu, "Yazılım Testinin Altın Kuralı"nın ne ifade ettiğini kolayca gözlemlemenizi sağlar: Programın yapmak istediği şeyi yaptığını ve ayrıca ekstra hiçbir şey yapmadığını test edin. Başka bir deyişle, yalnızca beklediğiniz çıktıları mı alıyorsunuz ve bunun dışında neler oluyor? Herhangi bir "zor" durum geçişi var mı, yani. döngünün sadece bir yinelemesi için rastgele geçen durumlar? Çıktılar, siz onları beklemediğinizde değişiyor mu? İdeal olarak, printfs çıktınızın bir durum geçiş tablosuna çok benzemesi gerekir.

Son olarak - ve bu, yalnızca durum makinesi tabanlı yazılımlar için değil, herhangi bir gömülü yazılım için geçerlidir - yazılımı gerçek donanım üzerinde ilk kez çalıştırırken çok dikkatli olun. Kutupları yanlış anlamak çok kolay - "Hata, '1'in şasiyi yükseltmek ve '0'ın ise düşürmek anlamına geldiğini düşündüm." Çoğu durumda, donanım asistanım, yazılımımın işleri doğru yönde hareket ettirdiğinden emin olana kadar değerli bileşenleri korumak için geçici bir "tavuk anahtarı" kullandı.

başlatmak

Tüm müşteri gereksinimleri karşılandığında, bu karmaşıklıkta bir durum makinesini birkaç gün içinde başlatabilirim. Neredeyse her zaman otomatlar ne istersem onu ​​yapar. Elbette en zoru müşterinin ne istediğini tam olarak anlamak ve müşterinin ne istediğini kendisinin de bildiğinden emin olmaktır. İkincisi çok daha uzun sürer!

Martin Gomez, Johns Hopkins Üniversitesi Uygulamalı Fizik Laboratuvarı'nda programcıdır. Araştırma uzay aracının uçuşu için yazılım geliştirmede görev aldı. 17 yıldır gömülü sistem geliştirme alanında çalışmaktadır. Martin, Cornell Üniversitesi'nden Havacılık ve Uzay Mühendisliği lisans derecesine ve Elektrik Mühendisliği alanında yüksek lisans derecesine sahiptir.

Otomata teorisi, ayrık bilgi dönüştürücü modellerini inceleyen ayrık matematiğin bir dalıdır. Bu tür dönüştürücüler hem gerçek cihazlar (bilgisayarlar, canlı organizmalar) hem de hayali cihazlardır (aksiyomatik teoriler, matematiksel makineler). Özünde, bir sonlu durum makinesi bir aygıt olarak tanımlanabilir. M giriş ve çıkış kanallarına sahip olan, saat anları olarak adlandırılan ayrık zaman anlarının her birinde, son durumlardan birindedir.

Herhangi bir zamanda giriş kanalında t =1, 2, ... cihaza M giriş sinyalleri gelir (bazı sonlu sinyal setlerinden). Bir sonraki an için durum değişikliği yasası, giriş sinyaline ve cihazın o andaki durumuna bağlı olarak ayarlanır. Çıkış sinyali duruma ve o andaki giriş sinyaline bağlıdır (Şekil 1).

Durum makinesi, gerçek ayrık bilgi işleme cihazlarının matematiksel bir modelidir.

durum makinesi sistem denir bir= (X , Q , Y , , ), nerede X , Q , Y keyfi boş olmayan sonlu kümelerdir ve ve - işlevleri:

    bir çok X ={a 1 , ..., a m ) denir giriş alfabesi , ve onun unsurları giriş sinyalleri , onların dizileri sloganlar ;

    bir çok Q ={q 1 , ..., q n ) denir birçok eyalet otomat ve elemanları - devletler ;

    bir çok Y ={b 1 , ..., b p ) denir çıkış alfabesi , onun unsurları çıkış sinyalleri , onların dizileri çıkış kelimeleri ;

    işlev : X Q Q aranan geçiş fonksiyonu ;

    işlev :X Q Y aranan çıkış fonksiyonu .

Böylece, (x , q )Q , (x , q )Y  için x X , q Q .

Aşağıdaki gibi çalışan durum makinesi ile hayali bir cihaz ilişkilendirilir. Setten bir durumda olabilir Q , setten sinyal al X ve bir setten sinyaller yayınlayın Y .

2. Sonlu bir otomat tanımlama yöntemleri

Soyut otomatları tanımlamanın birkaç eşdeğer yolu vardır, bunlardan üçü şunlardır: tablo , geometrik ve işlevsel .

2.1 Makinenin tablo ataması

Bir otomatın tanımından, her zaman aşağıdakileri içeren iki girişli bir tablo tarafından tanımlanabileceği sonucu çıkar. t çizgiler ve P sütunların kesiştiği yerde sütunlar q ve çizgiler a fonksiyon değerleri değerdir (a i , q j ), (a i , q j ).

q

a

q 1

q j

q n

a 1

(a 1 , q 1), (a 1 , q 1)

(a 1 , q j ), (a 1 , q j )

(a 1 , q n ), (a 1 , q n )

a i

(a i , q 1), (a i , q 1)

(a i , q j ), (a i , q j )

(a i , q n ), (a i , q n )

a m

(a m , q 1), (a m , q 1)

(a m , q j ), (a m , q j )

(a m , q n ), (a m , q n )

2.2. Moore diyagramı ile bir otomat tanımlama

Bir sonlu durum makinesini belirtmenin başka bir yolu da grafikseldir, yani bir grafik kullanmaktır. Otomat, etiketli bir yönlendirilmiş grafik olarak temsil edilir. G(Q , D ) çok köşeli Q ve birçok yay D ={(q j , (a i , q j ))| q j Q , a i X ), ark ( q j , (a i , q j )) bir çift ( a i , (a i , q j )). Böylece bu yöntemle, otomatın durumları, durumların sembollerinin girildiği dairelerle gösterilir. q j (j = 1, …, n ). Her daireden gerçekleştirilir t giriş alfabesinin karakterlerine karşılık gelen bire bir oklar (yönlendirilmiş kenarlar) X ={a 1 , ..., a m ). Mektuba karşılık gelen ok a i X ve çemberi terk etmek q j Q , çift ( a i , (a i , q j )) ve bu ok şuna karşılık gelen bir daireye götürür: (a i , q j ).

Ortaya çıkan çizim denir otomat grafiği veya, Moore diyagramı . Çok karmaşık olmayan otomatlar için bu yöntem, şundan daha açıklayıcıdır: tablo şeklinde.

Bugün makineli tüfekler hakkında konuşacağız, ancak hiçbir şekilde Rus ordusunun askerlerinin elinde tutulanlar değil. Otomatik programlama gibi ilginç bir mikrodenetleyici programlama stili hakkında konuşacağız. Daha doğrusu, bu bir programlama tarzı bile değil, bir mikrodenetleyici programcısının hayatını çok daha kolaylaştırabileceği bütün bir kavram. Programcıya sunulan birçok görevin çok daha kolay ve basit bir şekilde çözülmesi nedeniyle programcıyı baş ağrısından kurtarır. Bu arada, otomatik programlama genellikle denir SWITCH teknolojisi.

Bu yazıyı yazmanın teşvik edici olduğunu belirtmek isterim. SWITCH teknolojisi hakkında bir dizi makale Vladimir Tatarçevski. Makale dizisine "Mikrodenetleyiciler için uygulama yazılımı geliştirmede SWITCH teknolojisinin uygulanması" denir.

Bu arada, ABP mikrodenetleyicileri için programlama tekniklerini ayrıntılı olarak ele alacağım programlama üzerine bir dizi makale planladım, Kaçırma…. İyi hadi gidelim!

Program, programcı tarafından belirlenen komutları sırayla yürütür. Sıradan bir bilgisayar programı için, programın çalışmasının sonuçlarını monitörde görüntülerken çalışmasını tamamlayıp durdurması tamamen normaldir.

Bir mikrodenetleyici programı, yürütülmesini basitçe sonlandıramaz. Oynatıcıyı veya kayıt cihazını açtığınızı hayal edin. Güç düğmesine bastınız, istediğiniz şarkıyı seçtiniz ve müziğin keyfini çıkardınız. Bununla birlikte, müzik kulak zarınızı karıştırmayı bıraktığında, oynatıcı donar ve düğmelere basmaya hiçbir şekilde tepki vermez ve hatta daha da fazlası bir tef ile dans etmenize tepki verir.

Ve bu nedir? Her şey yolunda - oynatıcınızın derinliklerinde bulunan denetleyici, programını yürütmeyi yeni bitirdi. Burada ne kadar rahatsız edici olduğunu görebilirsiniz.

Buradan, mikrodenetleyici için programın durmaması gerektiği sonucuna varıyoruz. Özünde, sonsuz bir döngü olmalıdır - yalnızca bu durumda oynatıcımız doğru şekilde çalışır. Şimdi size mikrodenetleyiciler için program kodu tasarımlarının neler olduğunu göstereceğim, bunlar tasarım bile değil, bazı programlama stilleri.

Programlama stilleri.

“Programlama stilleri” kulağa biraz anlaşılmaz geliyor, ama neyse. Bununla ne demek istiyorum?Bir kişinin daha önce hiç programlama yapmadığını yani genel olarak tam bir aptal olduğunu düşünelim.

Bu kişi programlama üzerine birçok kitap okudu, dilin tüm temel yapılarını öğrendi.Artık bilgiye erişim sınırsız olduğu için bilgileri parça parça topladı. Bütün bunlar iyi, ama ilk programları nasıl görünecek? Bana öyle geliyor ki felsefe yapmayacak, basitten karmaşığa giden yolu izleyecek.

Dolayısıyla bu stiller, basit bir seviyeden daha karmaşık bir seviyeye giden adımlardır, ancak aynı zamanda daha etkilidir.

İlk başta, programın herhangi bir tasarım özelliğini düşünmedim. Az önce programın mantığını oluşturdum - bir akış şeması çizdim ve kodu yazdım. Sürekli olarak bir tırmıkla karşılaşanlardan. Ama bu, ilk defa buhar banyosu yapmadığım ve “basit döngü” stilini kullandığım zamandı, sonra kesintileri kullanmaya başladım, sonra otomatlar vardı ve yola çıktık ...

1. Basit döngü. Bu durumda program herhangi bir karmaşıklık olmadan döngü yapar ve bunun artıları ve eksileri vardır. Artı sadece yaklaşımın basitliğinde, kurnaz tasarımlar icat etmenize gerek yok, düşündüğünüz gibi yazıyorsunuz (yavaş yavaş kendi mezarınızı kazıyorsunuz).

Void main(void) ( initial_AL(); //çevrenin başlatılması while(1) ( Leds_BLINK(); //LED flaşör işlevi signal_on(); //sinyali açma işlevi signal_off(); // sinyali kapatma işlevi l=button(); //butonlara basmaktan sorumlu değişken switch(l) //Değişkenin değerine bağlı olarak, bir veya başka bir işlem gerçekleştirilir ( durum 1: ( Deistvie1(); / /Bir fonksiyon yerine, koşullu bir operatör Deistvie2(); //veya birkaç dal geçiş durumu Deistvie3(); Deistvie4(); Deistvie5(); ); durum 2: ( Deistvie6(); Deistvie7(); Deistvie8(); Deistvie9(); Deistvie10(); ); . . . . . . ) ) )

Programın çalışma noktası sırayla hareket eder. Bu durumda, tüm eylemler, koşullar ve döngüler sırayla yürütülür. Kod yavaşlamaya başlar, birçok ekstra koşul eklemeniz gerekir, bu nedenle algıyı karmaşıklaştırır.

Bütün bunlar, programı büyük ölçüde karıştırır ve koddan bir dizi koşul çıkarır. Sonuç olarak, bu kod eklenemez veya çıkarılamaz, yekpare bir parça gibi olur. Tabii ki, hacim büyük olmadığında kod değiştirilebilir, ancak daha da zorlaşır.

Bu yaklaşımla birkaç program yazdım, büyük değillerdi ve pek işe yaramadılar, ancak görünürlük arzulananı bıraktı. Yeni bir koşul eklemek için tüm kodu kürekle atmak zorunda kaldım çünkü her şey bağlıydı. Bu, birçok hataya ve baş ağrısına neden oldu. Derleyici mümkün olan en kısa sürede yemin etti, böyle bir programın hatalarını ayıklamak cehenneme döndü.

2. Döngü + kesintiler.

Kesintileri kullanarak sonsuz frenleme döngüsünü kısmen çözebilirsiniz. Kesintiler kısır döngüyü kırmaya yardımcı olur, önemli bir olayı kaçırmamanıza, ek işlevler eklemenize yardımcı olur (zamanlayıcılardan gelen kesintiler, harici kesintiler).

Diyelim ki düğmelerin işlenmesini veya bir kesintide önemli bir olayı izlemeyi kapatabilirsiniz. Sonuç olarak, program daha görsel hale gelir ancak daha az kafa karıştırıcı olmaz.

Ne yazık ki, kesinti sizi programın dönüştüğü karmaşadan kurtarmaz. Tek bir bütün olan şeyleri parçalara ayırmak mümkün olmayacaktır.

3. Otomatik programlama.

Böylece bu makalenin ana konusuna geliyoruz. Sonlu durum makinelerinde programlama, programı ilk iki örnekte bulunan eksikliklerden kurtarır. Program daha basit hale gelir, değiştirilmesi kolaydır.

Otomat tarzında yazılmış bir program, koşullara bağlı olarak şu veya bu duruma geçiş yapan bir anahtar gibidir. Durumların sayısı başlangıçta programcı tarafından bilinir.

Kabaca, bir ışık anahtarı gibi. Açık ve kapalı iki durum ve açık ve kapalı iki durum vardır. Şey, önce ilk şeyler.

Anahtar teknolojisinde çoklu görev uygulaması.

Mikrodenetleyici yükü kontrol edebilir, LED'leri yanıp sönebilir, tuş vuruşlarını izleyebilir ve çok daha fazlasını yapabilir. Ama tüm bunlar aynı anda nasıl yapılır? Bu sorunun birçok çözümü var. Bunların en basiti daha önce bahsettiğim kesmelerin kullanılmasıdır.

Program sırasında, bir kesinti meydana geldiğinde, denetleyicinin dikkati program kodunun yürütülmesinden uzaklaştırılır ve kısa bir süre için kesintinin sorumlu olduğu programın başka bir parçasını yürütür. Kesme çalışacaktır, ardından programın çalışma noktası, kesme tarafından denetleyicinin kesintiye uğratıldığı yerden devam edecektir (kelimenin kendisi denetleyicinin kesintiye uğradığını gösterir).

Çoklu görevi uygulamanın başka bir yolu da işletim sistemlerinin kullanılmasıdır. Evet, düşük güçlü bir denetleyicide kullanılabilecek küçük işletim sistemleri zaten görünmeye başladı. Ancak çoğu zaman bu yöntemin biraz gereksiz olduğu ortaya çıkıyor. Sonuçta, az kan dökülmesiyle geçinmek oldukça mümkünken, neden kontrolör kaynaklarını gereksiz işlerle boşa harcıyorsunuz?

Anahtar teknolojisi kullanılarak yazılan programlarda, mesajlaşma sistemi sayesinde böyle bir çoklu görev “illüzyonu” elde edilir. "İllüzyon" yazdım çünkü gerçekten öyle, çünkü program fiziksel olarak kodun farklı bölümlerini aynı anda çalıştıramıyor. Mesajlaşma sisteminden biraz daha bahsedeceğim.

Mesajlaşma sistemi.

Mesajlaşma sistemini kullanarak birden fazla işlemi yok edebilir ve çoklu görev yanılsaması yaratabilirsiniz.

Diyelim ki LED'in değiştirildiği bir programa ihtiyacımız var. Burada iki makinemiz var, onlara LEDON diyelim - LED'i açmaktan sorumlu makine ve LEDOFF makinesi - LED'i kapatmaktan sorumlu makine.

Otomatların her birinin iki durumu vardır, yani, otomat, bir bıçak anahtarının açık veya kapalı olması gibi, aktif veya aktif olmayan bir durumda olabilir.

Bir makine etkinleştirildiğinde LED yanar, diğer makine etkinleştirildiğinde LED söner. Küçük bir örnek düşünün:

Int main(void) ( INIT_PEREF(); //çevre birimlerinin (LED'ler) başlatılması InitGTimers(); //zamanlayıcıların başlatılması InitMessages(); //mesaj işleme mekanizmasının başlatılması InitLEDON(); //LEDON otomatının başlatılması InitLEDOFF(); // LEDOFF otomatının başlatılması SendMessage(MSG_LEDON_ACTIVATE); //LEDON otomatını etkinleştir sei(); //Kesmeleri etkinleştir //Ana döngü while(1) ( ProcessLEDON(); //LEDON yinelemesi otomat ProcessLEDOFF(); //LEDOFF otomatının ProcessMessages yinelemesi (); //mesaj işleme ); )

3-7. satırlarda çeşitli başlatmalar meydana gelir, bu nedenle şu anda bununla özellikle ilgilenmiyoruz. Ama sonra şunlar olur: ana döngüye başlamadan önce (((((()))) bir mesaj göndeririz, otomata bir mesaj göndeririz.

SendMessage(MSG_LEDON_ACTIVATE)

LED'i aydınlatmaktan sorumludur. Bu küçük adım olmadan, hurdy-gurdy çalışmaz. Ardından, işin büyük kısmını ana sonsuz while döngüsü yapar.

Küçük arasöz:

Mesajın üç durumu vardır. Yani, mesaj durumu inaktif, ayarlanmış fakat inaktif ve aktif olabilir.

Mesajın başlangıçta etkin olmadığı ortaya çıktı, mesajı gönderdiğimizde "kurulu ancak etkin değil" durumunu aldı. Ve bu bize aşağıdakileri verir. Program sıralı olarak yürütüldüğünde LEDON otomatı bir mesaj almaz. LEDON otomatının boşta bir yinelemesi, mesajın basitçe alınamadığı gerçekleşir. Mesaj "kurulu ancak etkin değil" durumuna sahip olduğundan, program yürütmeye devam eder.

Tüm otomatlar boşta kaldıktan sonra kuyruk ProcessMessages() işlevine ulaşır. Bu işlev, tüm otomat yinelemeleri tamamlandıktan sonra her zaman döngünün sonuna yerleştirilir. ProcessMessages() işlevi, mesajı "ayarlanmış ancak etkin değil" durumundan "etkin" duruma değiştirir.

Sonsuz döngü ikinci turu tamamladığında, resim zaten tamamen farklıdır. ProcessLEDON otomatının yinelemesi artık boşta olmayacak. Makine mesajı alabilecek, yanan duruma geçebilecek ve ayrıca mesajı sırayla gönderebilecektir. LEDOFF otomatına yönlendirilecek ve mesajın yaşam döngüsü tekrarlanacaktır.

"Aktif" duruma sahip olan mesajların ProcessMessages işlevi ile karşılaştıklarında yok edildiğini belirtmek isterim. Bu nedenle, bir mesaj sadece bir otomat tarafından alınabilir. Başka bir mesaj türü daha var - bunlar yayın mesajları, ama onları dikkate almayacağım, onlar da Tatarchevskiy'in makalelerinde iyi bir şekilde ele alındı.

zamanlayıcılar

Doğru mesajlaşma ile durum makinelerinin çalışma sırasını kontrol edebiliriz, ancak sadece mesajlar olmadan yapamayız.

Önceki örnek kod parçacığının istendiği gibi çalışmayacağını fark etmiş olabilirsiniz. Makineler mesaj alışverişi yapacak, LED'ler değişecek ama biz bunu görmeyeceğiz. Sadece loş bir LED göreceğiz.

Bunun nedeni, gecikmelerin yetkili bir şekilde işlenmesini düşünmememizdir. Sonuçta, LED'leri dönüşümlü olarak açıp kapatmak bizim için yeterli değil, LED'in her durumda, örneğin bir saniye oyalanması gerekir.

Algoritma aşağıdaki gibi olacaktır:

Büyütmek için tıklayabilirsiniz

Bu blok şemaya, zamanlayıcı işaretlendiğinde, elbette bir eylemin gerçekleştirildiğini - LED'i yakmayı veya kapatmayı eklemeyi unuttum.

1. Duruma mesaj alarak giriyoruz.

2. Zamanlayıcı/sayaç okumalarını kontrol ediyoruz, işaretli ise işlemi gerçekleştiriyoruz, yoksa kendimize bir mesaj gönderiyoruz.

3. Bir sonraki otomata bir mesaj gönderiyoruz.

4. Çıkış

Bir sonraki girişte, her şey tekrarlanır.

SWITCH teknolojisi programı. Üç adım.

Sonlu otomatlarda bir program yazalım ve bunun için sadece üç basit adım yapmamız gerekecek. Program basit olacak, ancak basit şeylerle başlamaya değer. Anahtarlama LED'li bir program bizim için uygundur. Bu çok iyi bir örnek, o yüzden yeni bir şey icat etmeyelim.

Programı C dilinde yazacağım, ancak bu, sonlu otomatlarda sadece C ile yazmanız gerektiği anlamına gelmiyor, başka herhangi bir programlama dili kullanmak oldukça mümkün.

Program modüler olacak ve bu nedenle birkaç dosyaya bölünecek. Modüllerimiz şunlar olacak:

  • Programın ana döngü modülü leds_blink.c, HAL.c, HAL.h dosyalarını içerir.
  • Zamanlayıcı modülü timers.c, timers.h dosyalarını içerir
  • Mesaj işleme modülü mesajlar.c, mesajlar.h dosyalarını içerir
  • Makine modülü 1 ledon.c, ledon.h dosyalarını içerir
  • Makine modülü 2 ledoff.c dosyalarını içerir, ledoff .h

Aşama 1.

Bir proje oluşturuyoruz ve statik modüllerimizin dosyalarını hemen ona bağlıyoruz: timers.c, timers.h, message.c, message.h.

Programın ana döngüsünün modülünün leds_blink.c dosyası.

#include "hal.h" #include "messages.h" //mesaj işleme modülü #include "timers.h" //timer modülü //Zamanlayıcı kesintileri //############# # ################################################# ############################# ISR(TIMER0_OVF_vect) // Kesinti vektör geçişi (T0 sayaç zamanlayıcı taşması) ( ProcessTimers(); / /Zamanlayıcı kesme işleyicisi) //###################################### ### ################################################ int ana (void) ( INIT_PEREF(); //çevrenin başlatılması (LED'ler) InitGTimers(); //zamanlayıcıların başlatılması InitMessages(); //mesaj işleme mekanizmasının başlatılması InitLEDON(); //LEDON otomatının başlatılması InitLEDOFF(); StartGTimer( TIMER_SEK); //Zamanlayıcıyı başlat SendMessage(MSG_LEDON_ACTIVATE); //FSM1 otomatını etkinleştir sei(); //Kesmeleri etkinleştir //Ana döngü while(1) ( ProcessLEDON(); //yineleme) LEDON otomatı ProcessLEDOFF(); ProcessMessages(); //mesaj işleme ); )

İlk satırlarda kalan modüller ana programa bağlanır. Burada timer modülü ile mesaj işleme modülünün bağlı olduğunu görüyoruz. Programda sonraki, taşma kesme vektörüdür.

int main (void) satırından ana programın başladığını söyleyebiliriz. Ve her şeyin ve herkesin başlatılmasıyla başlar. Burada çevreyi başlatıyoruz, yani ilk değerleri karşılaştırıcının giriş / çıkış portlarına ve kontrolörün diğer tüm içeriğine ayarlıyoruz. Bütün bunlar INIT_PEREF işlevi tarafından yapılır, ana gövdesi hal.c dosyasında olmasına rağmen burada çalıştırıyoruz.

Ardından, zamanlayıcıların başlatılmasını, mesaj işleme modülünü ve otomatların başlatılmasını görüyoruz. Burada, işlevlerin kendileri modüllerinin dosyalarına yazılmasına rağmen, bu işlevler de basitçe başlatılır. Ne kadar uygun olduğunu görün. Programın ana metninin okunması kolay kalır ve bacağınızı kıran gereksiz kodlarla karıştırılmaz.

Ana başlatmalar bitti, şimdi ana döngüyü başlatmamız gerekiyor. Bunu yapmak için başlangıç ​​mesajını gönderiyoruz ve ayrıca saatimizi başlatıyoruz - zamanlayıcıyı başlatıyoruz.

BaşlatGTimer(TIMER_SEK); //zamanlayıcıyı başlat SendMessage(MSG_LEDON_ACTIVATE); //FSM1 makinesini etkinleştir

Ve ana döngü, dediğim gibi, çok basit görünüyor. Tüm otomatların fonksiyonlarını yazıyoruz, sırayı takip etmeden sadece bir sütuna yazıyoruz. Bu işlevler otomat işleyicileridir ve otomat modüllerinde bulunur. Mesaj işleme modülünün işlevi bu otomat piramidini tamamlar. Tabii bunu daha önce mesajlaşma sistemiyle uğraşırken söylemiştim. Artık ana program döngüsünün modülünün iki dosyasının daha nasıl göründüğünü görebilirsiniz.

Hal.h, programın ana döngü modülünün başlık dosyasıdır.

#ifndef HAL_h #define HAL_h #include #Dahil etmek //Kesintileri içeren standart kütüphane #define LED1 0 #define LED2 1 #define LED3 2 #define LED4 3 #define Komparator ACSR //karşılaştırıcı #define ViklKomparator 1<

Gördüğünüz gibi, bu dosya doğası gereği tek bir yürütülebilir kod satırı içermez - bunların tümü makro ikameleri ve kitaplık bağlantılarıdır. Bu dosyaya sahip olmak hayatı çok güzel kılıyor, görünürlüğü artırıyor.

Ancak Hal.c dosyası zaten yürütülebilir bir dosyadır ve daha önce de belirttiğim gibi çeşitli çevresel başlatmalar içerir.

#include "hal.h" void INIT_PEREF(void) ( //G/Ç bağlantı noktalarını başlat //########################### ################################################# # ##### Komparator = ViklKomparator; //karşılaştırıcı başlatma - DDRD'yi kapatın = 1<

Eh, programın ana döngüsünün modülünü gösterdim, şimdi son adımı atmamız gerekiyor, otomatların modüllerini kendimiz yazmamız gerekiyor.

Aşama 3

Bizim durumumuzda sonlu otomatların modüllerini yazmak bize kalıyor, LEDON otomatı ve LEDOFF otomatı. Başlangıç ​​olarak LED'i yakan makine için programın metnini, ledon.c dosyasını vereceğim.

//dosya ledon.c #include "ledon.h" #include "timers.h" #include "messages.h" unsigned char ledon_state; //durum değişkeni void InitLEDON(void) ( ledon_state=0; //varsa burada diğer otomat değişkenlerini başlatabilirsiniz) void ProcessLEDON(void) ( switch(ledon_state) ( case 0: //inactive state if(GetMessage (MSG_LEDON_ACTIVATE) )) //bir mesaj varsa kabul edilir ( //ve timer kontrol edilir if(GetGTimer(TIMER_SEK)==one_sek) //zamanlayıcı 1s ayarlamışsa, çalıştır ( StopGTimer(TIMER_SEK); PORTD = 1<

Burada ilk satırlarda her zaman olduğu gibi kütüphaneler bağlanır ve değişkenler bildirilir. Ardından, daha önce tanıştığımız işlevlere geçtik. Bu, InitLEDON otomatının başlatma işlevi ve ProcessLEDON otomat işleyicisinin kendisinin işlevidir.

İşleyicinin gövdesinde, zamanlayıcı modülünden ve mesaj modülünden gelen işlevler zaten işlenmektedir. Ve otomatın mantığı, anahtar kasası tasarımına dayanmaktadır. Ve burada, birkaç durum anahtarı ekleyerek otomat işleyicisinin de karmaşık olabileceğini görebilirsiniz.

Otomatın başlık dosyası daha da basit olurdu:

//fsm1 dosyası #ifndef LEDON_h #define LEDON_h #include "hal.h" void InitLEDON(void); void ProcessLEDON(void); #endif

Burada hal.h link dosyasını bağlarız ve ayrıca fonksiyonların prototiplerini belirtiriz.

LED'i kapatmaktan sorumlu dosya yalnızca ayna görüntüsünde neredeyse aynı görünecek, bu yüzden burada göstermeyeceğim - isteksizlik 🙂

Tüm proje dosyalarını bu linkten indirebilirsiniz ====>>> BAĞLANTI.

İşte sadece üç adım ve programımız bitmiş bir görünüme kavuştu, bu da bugünkü görevimin bittiği ve tamamlamanın zamanı geldiği anlamına geliyor. Bana öyle geliyor ki bu yazıda verilen bilgiler sizin için çok faydalı olacak. Ancak ancak bu bilgiyi uygulamaya koyduğunuzda gerçek fayda sağlayacaktır.

Bu arada, özellikle ilginç olacak bir dizi ilginç proje planladım, bu yüzden emin olun. yeni makaleler için abone olun . Ayrıca ek materyaller göndermeyi planlıyorum, pek çok kişi sitenin ana sayfasından abone oluyor. Buradan da abone olabilirsiniz.

Şimdi gerçekten her şeye sahibim, bu yüzden size iyi şanslar, iyi bir ruh hali ve tekrar görüşmek dileğiyle.

Yok Vladimir Vasilyev

Makale, basit durum makinelerini ve bunların anahtar yapıları, çalışma zamanı tablolarını ve Boost Statechart kitaplığını kullanarak C++'daki uygulamalarını tartışıyor.

giriiş

Kabaca söylemek gerekirse, sonlu durum makinesi (Sonlu Durum Makinesi), kullanıcının gözünde, içine bir şeyler aktarabileceğiniz ve oradan bir şeyler alabileceğiniz bir kara kutudur. Bu, karmaşık bir algoritmayı gizlemenize izin veren çok uygun bir soyutlamadır, ayrıca durum makineleri çok verimlidir.

Sonlu otomatlar, durumlar ve geçişlerden oluşan diyagramlar olarak tasvir edilir. Basit bir örnekle açıklayayım:

Muhtemelen tahmin ettiğiniz gibi, bu bir ampul durum şemasıdır. İlk durum siyah bir daire ile gösterilir, geçişler oklarla, bazı oklar işaretlenir - bunlar, makinenin daha sonra başka bir duruma geçtiği olaylardır. Böylece, ilk durumdan hemen duruma geçiyoruz. ışık kapalı- lamba yanmıyor. Düğmeye basarsanız, makine durumunu değiştirecek ve işaretli oku izleyecektir. butona basınız, devlete açık- lamba açık. Bu durumdan butona bastıktan sonra ok ile tekrar duruma geçebilirsiniz. ışık kapalı.

Atlama tabloları da yaygın olarak kullanılmaktadır:

Makinelerin pratik uygulaması

Durum makineleri programlamada yaygın olarak kullanılmaktadır. Örneğin, cihazın çalışmasını bir otomat şeklinde hayal etmek çok uygundur. Bu, kodu denemeyi ve korumayı daha basit ve daha kolay hale getirecektir.

Ayrıca, sonlu otomatlar her türlü metin ayrıştırıcısını ve çözümleyicisini yazmak için kullanılır, bunların yardımıyla alt dizileri etkili bir şekilde arayabilirsiniz, düzenli ifadeler de sonlu bir otomata çevrilir.

Örneğin, bir metindeki sayıları ve kelimeleri saymak için bir otomat uygulayacağız. Başlamak için, bir sayının, boşluk karakterleriyle (boşluk, sekme, satır besleme) çevrelenmiş, 0 ila 9 arasında rastgele uzunlukta bir basamak dizisi olacağını kabul edelim. Bir kelime, harflerden oluşan ve ayrıca boşluk karakterleriyle çevrelenmiş rastgele uzunlukta bir dizi olarak kabul edilecektir.

Bir diyagram düşünün:

İlk durumdan duruma geçiyoruz Başlat. Mevcut karakteri kontrol ediyoruz ve eğer bir harf ise duruma gidiyoruz Kelime olarak işaretlenmiş ok boyunca mektup. Bu durum, bir kelimeyi düşündüğümüz anda, diğer sembollerin analizinin bu varsayımı doğrulayacağını veya çürüteceğini varsayar. Bu nedenle, bir sonraki karakteri düşünün, eğer bir harfse, durum değişmez (olarak işaretlenmiş dairesel oka dikkat edin). mektup). Karakter bir harf değilse, ancak bir boşluk karakterine karşılık geliyorsa, bu varsayımın doğru olduğu ve kelimeyi bulduğumuz anlamına gelir (ok boyunca hareket ederiz Uzay bir duruma Başlat). Sembol ne harf ne de boşluk ise, varsayımda bir hata yaptık ve düşündüğümüz dizi bir kelime değil (oku takip ediyoruz) Bilinmeyen bir duruma Atlamak).

Hünerli Atlamak bir boşluk karakteriyle karşılaşılıncaya kadar kalıyoruz. Boşluk bulunduktan sonra oku takip ediyoruz. Uzay bir duruma Başlat. Bu, arama düzenimizle eşleşmeyen satırı tamamen atlamak için gereklidir.

Devlete geçişten sonra Başlat, arama döngüsü baştan tekrarlanır. Sayı tanıma dalı, kelime tanıma dalına benzer.

Anahtar talimatlarını kullanarak otomasyon

Birincisi olası durumlar:

Bundan sonra, geçerli karakteri otomata kaydırarak dize üzerinde yineleniriz. Otomatın kendisi, önce mevcut durum bölümüne bir geçiş gerçekleştiren bir switch ifadesidir. Bölümün içinde, olaya (gelen karakter) bağlı olarak mevcut durumu değiştiren bir if-else yapısı vardır:

const size_t uzunluk = metin.uzunluk () ; for (size_t i = 0 ; i != uzunluk; ++ i) ( const char akım = metin[ i] ; geçiş (durum) ( case State_Start: if (std:: isdigit (current) ) ( durum = Durum_Numarası; ) else if (std:: isalpha (geçerli) ) ( state = State_Word; ) break ; case State_Number: if (std:: isspace (geçerli)) (

Burada şemaya bakıyoruz - mevcut durum sayı, Etkinlik Uzay(boşluk karakteriyle karşılaşıldı), yani bir sayı bulundu:

BulunanNumara() ; durum = Durum_Başlangıç; ) else if (! std:: isdigit (geçerli) ) ( state = State_Skip; ) break ; durum State_Word: if (std:: isspace (geçerli) ) ( FoundWord() ; durum = State_Start; ) else if (! std:: isalpha (geçerli) ) ( durum = State_Skip; ) break ; durum State_Skip: if (std::isspace (geçerli) ) ( state = State_Start; ) break ; ))

Sonuç:

Yüksek verim

Basit algoritmalar için uygulama kolaylığı

- bakımı zor

Çalışma Zamanı Yorumu

Bu yaklaşımın fikri basittir - bir geçiş tablosu oluşturmanız, doldurmanız ve ardından bir olay meydana geldiğinde tabloda aşağıdaki durumu bulmanız ve geçiş yapmanız gerekir:

enum Events( Event_Space, Event_Digit, Event_Letter, Event_Unknown ) ; void ProcessEvent(Events event) ; ... struct Geçiş (Stateler BaseState_; Events Event_; States TargetState_; ) ; void AddTransition(States fromState, Events event, State toState); ... typedef std::vektör< transition>Geçiş Tablosu; Geçiş Tablosu Geçişleri_; Durumlar MevcutDurum_;

Tablonun doldurulması:

AddTransition(State_Start, Event_Digit, State_Number) ; AddTransition(State_Start, Event_Letter, State_Word) ; AddTransition(State_Number, Event_Space, State_Start) ; AddTransition(State_Number, Event_Letter, State_Skip) ; AddTransition(State_Number, Event_Unknown, State_Skip) ; AddTransition(State_Word, Event_Space, State_Start) ; AddTransition(State_Word, Event_Digit, State_Skip) ; AddTransition(State_Word, Event_Unknown, State_Skip) ; AddTransition(State_Skip, Event_Space, State_Start) ;

Oldukça net çıktı. Netlik için ödeme, tesadüfen, genellikle önemli olmayan, makinenin daha yavaş çalışması olacaktır.

Otomatın, belirli olayların meydana gelmesi üzerine bazı kodları bildirebilmesi için, yapıya geçiş hakkında bilgi ekleyebilirsiniz ( geçiş) işlev işaretçisi ( Eylem) adı verilecek:

typedef void (DynamicMachine:: * Eylem) () ; struct Geçiş (States BaseState_; Events Event_; State TargetState_; Action Action_; ) ; ... void AddTransition(Devletten Devlet, Olaylar olayı, Devletten Devlete, Eylem eylemi); ... AddTransition (State_Number, Event_Space, State_Start ve DynamicMachine:: FoundNumber ) ;

Sonuç:

Esneklik ve görünürlük

Bakımı daha kolay

- Anahtar bloklarına kıyasla daha az performans

Yürütme süresi yorumu. Hız optimizasyonu

Görünürlük hız ile birleştirilebilir mi? Bir otomatın, anahtar bloklarına dayalı bir otomat kadar verimli olması pek olası değildir, ancak açığı kapatmak mümkündür. Bunu yapmak için, tablodan bir matris oluşturmak gerekir, öyle ki geçiş hakkında bilgi almak için arama yapmayın, durum ve olay sayısına göre bir seçim yapın:

Sonuç, bellek tüketimi pahasına elde edilir.

Sonuç:

Esneklik, görünürlük

etkili

- Bellek tüketimi (muhtemelen önemsiz)

Durum Çizelgesi'ni artırın

Bir durum makinesini kendiniz uygulamanın birkaç yolunu tartıştık. Bir değişiklik olarak, Boost'tan otomata oluşturmak için bir kitaplık düşünmeyi öneriyorum. Bu kitaplık çok güçlüdür, önerilen özellikler çok karmaşık sonlu durum makineleri oluşturmanıza olanak tanır. Oldukça kısaca bakacağız.

O halde önce olayları tanımlayalım:

namespace Events( struct Digit : boost::statechart::event< Digit>( ) ; struct Harf : boost::statechart::event< Letter>( ) ; struct Space : boost::statechart::event< Space>( ) ; structUnknown : boost::statechart::event< Unknown> { } ; }

Makinenin kendisi (şablonun ikinci parametresinin başlangıç ​​durumu olduğuna dikkat edin):

struct Machine : boost::statechart::state_machine< Machine, States:: Start > { } ;

Ve devletlerin kendileri. Durumların içinde, geçişleri tanımlayan bir tür tanımlamak gerekir ( reaksiyonlar) ve birkaç geçiş varsa, bunları boost::mpl::list türü listesinde listeleyin. İkinci şablon parametresi basit_durum– arabayı tanımlayan yazın. Geçişler, geçiş şablonu parametreleriyle tanımlanır, çift Etkinlik - Sonraki Durum:

ad alanı Durumları ( struct Start : boost::statechart::simple_state< Start, Machine> < boost:: statechart :: transition < Events:: Digit , States:: Number >< Events:: Letter , States:: Word >> reaksiyon; ) ; yapı Numarası: boost::statechart::simple_state< Number, Machine>( typedef boost::mpl::list< boost:: statechart :: transition < Events:: Space , States:: Start >, boost::statechart::geçiş< Events:: Letter , States:: Skip >, boost::statechart::geçiş< Events:: Unknown , States:: Skip >> reaksiyon; ) ; struct Word : boost::statechart::simple_state< Word, Machine>( typedef boost::mpl::list< boost:: statechart :: transition < Events:: Space , States:: Start >, boost::statechart::geçiş< Events:: Digit , States:: Skip >, boost::statechart::geçiş< Events:: Unknown , States:: Skip >> reaksiyon; ) ; struct Atla: boost::statechart::simple_state< Skip, Machine>( typedef boost::statechart::geçiş< Events:: Space , States:: Start >reaksiyonlar; ) ; )

Makine inşa edilmiştir, yalnızca başlatmak için kalır ve şunları kullanabilirsiniz:

makine makinesi; makine.initiate(); ...makine .process_event(Events::Space() ) ;

Sonuç:

Esneklik, genişletilebilirlik

- Yeterlik

Verim

Oluşturulan otomatların hızını kontrol etmek için bir test programı yazdım. Makineler aracılığıyla ~ 17 MB boyutundaki metni sürdüm. İşte koşunun sonuçları:

Metin yükleniyor Metin uzunluğu: 17605548 bayt 0,19 s BoostMachine Çalıştırılıyor Sözcükler: 998002, sayılar: 6816 0,73 s Çalıştırılıyor DynamicMachine Sözcükler: 998002, sayılar: 6816 0,56 s FastDynamicMachine Çalıştırılıyor Sözcükler: 998002, sayılar: 6816 0,29 s: sayılar SimpleMachine Çalıştırılıyor 0: 99 s

İncelenmemiş ne kalır

Sonlu otomatların birkaç uygulaması daha ortaya çıkmadı (http://www.rsdn.ru/article/alg/Static_Finite_State_Machine.xml ve http://www.rsdn.ru/article/alg/FiniteStateMachine.xml öneririm), jeneratörler Açıklamalardan, Boost sürüm 1.44.0'dan Meta Durum Makinesi kitaplığından ve durum makinelerinin resmi açıklamalarından otomatlar oluşturun. Meraklı okuyucu, yukarıdakilerin tümüne aşina olabilir.