본문 바로가기
프로그래밍

녹에서 베어메탈 작업 관리를 위한 일반 인터페이스

by it-view 2022. 1. 13.
반응형

이 기사에서는 실시간 환경에서 작업 관리를 일반화하기 위한 솔루션에 대해 살펴봅니다. 이 코드는 러스트 프로그래밍 언어와 밀접하게 결합되어 있지만, 개념은 어디에서나 구현될 수 있다.

이러한 요구는 베어메탈 커널 프로젝트에서 비롯되었다. 제가 개발하는 하드웨어는 싱글코어 ARMv7 Cortex-M 칩입니다. 10대. GitHub에서 모든 소스 코드를 찾을 수 있습니다.

베어메탈 프로그램은 여러 작업이 필요합니다. 문제는, 얼마나 동시에 했으면 좋겠냐는 거죠 내 커널은 RTOS(Real Time Operating System)로 분류될 수 있으며, 이는 내가 작업을 관리하기 위해 스레드 스케줄러를 사용하지 않는다는 것을 의미한다. 저는 프로세스 모음을 가지고 있으며 각 프로세스는 매우 빠르게 실행되어 병렬 처리처럼 보입니다.

다음은 작업 관리에 대한 일반적인 접근 방식의 예입니다. 난 이 관문들을 부른다. 이 기사에서는 게이트가 어떻게 진화했는지와 게이트를 구현하는 한 가지 방법에 대해 살펴보겠습니다.

 
// System entrypoint
loop {
     blink_task();
}
fn blink_task() {
      gate_open!()
         .once(|| { pin_mode(13, OUTPUT); })
         .when_ms(500, || { pin_out(13, HIGH); })
         .when_ms(500, || { pin_out(13, LOW); })
         .build();
}

다음은 게이트의 동작에 대한 또 다른 예입니다.

// When data is available over serial, read the bytes into a buffer.
loop {
  gate_open!().when(
    || { return serial_available() > 0 }, 
    || { serial_buffer.append(serial_read()); }
    ).build();
}

여기서 중요한 점은 게이트가 루프 { } 세그먼트에 존재하지만 커널에 컴퓨팅 파워를 다시 제공해야 하는 시기를 자동으로 처리한다는 것이다. 당신이 직접 국가 관리를 시행할 필요는 없습니다, 게이트는 모든 것을 추상화합니다.

구세계

 

일반적으로 임베디드 태스크에 대한 몇 가지 패턴을 발견했습니다.

  • 태스크에는 종종 초기화 및 작업이라는 두 가지 이상의 구분 상태가 있습니다.
  • 태스크는 복잡한 중첩 조건을 가질 수 있습니다.
  • 작업은 종종 무한 루프에 배치되며 각 반복이 중단된 부분을 선택할 수 있어야 합니다.

우리가 가장 좋아하는 예를 들어 불을 깜빡이는 것을 보자. 실시간에 가까운 성능을 요구하는 시스템에서는 지연()을 사용하고 리소스를 독점할 수 없습니다. 다음은 깜박임 상태 사이에서 처리 능력을 어떻게 산출할 수 있는지 보여주는 몇 가지 녹 코드입니다.

static mut INITIALIZED: bool = false;
static mut NEXT_ITERATION: u64 = 0;
static mut HIGH: bool = false;
// System entrypoint
loop {
      blink_task();
}
fn blink_task() {
      if unsafe { INITIALIZED } == false {
                pin_mode(13, Output);
                unsafe { INITIALIZED = true };
      } else {
                if system::millis() > unsafe { NEXT_ITERATION } {
                              pin_out(13, unsafe { !HIGH });
                    unsafe { 
                                      HIGH = !HIGH;
                                      NEXT_ITERATION = system::millis() + 500; 
                    };
      }
}
}

이것은 확실히 효과가 있지만, 일반적이지 않습니다. 시스템의 각 작업은 프로세스의 흐름을 제어하는 데 도움이 되는 여러 정적 변수를 필요로 합니다. 그것은 매우 빠르게 통제불능 상태가 된다.

 

문의 해부도

만약 우리가 중요한 작업 부분을 나타내는 인터페이스에 동의한다면, 그것은 전투의 절반입니다. 나머지 절반은 성가신 정적 변수들을 모두 추출하고 있습니다…

게이트 소개!

게이트는 조건에 의해 게이트된 일련의 연결된 작업을 설명합니다. 게이트의 인터페이스는 다음과 같습니다.

pub trait Gated {
      fn new() -> Self;
      fn when(&mut self, cond: CondFn, then: ExecFn) -> &mut Self;
      fn when_ms(&mut self, then: ExecFn) -> &mut Self;
      fn once(&mut self, then: ExecFn) -> &mut Self;
      fn build(&mut self) -> Gate;
      fn process(&mut self);
}
pub struct Gate {
      pub conditions: Vector::<CondFn>,
            pub functions: Vector::<ExecFn>,
                  pub durations: Vector::<u64>,
                        pub target_times: Vector::<u64>,
                              pub current_index: usize,
                                    pub tail: usize,
                                          pub built: bool,
}
 

설계 패턴에 익숙하다면 작성기 패턴으로 인식할 수 있습니다. 이 기능을 통해 여러 논리 블록을 연결할 수 있습니다. 다음과 같은 설계 원칙을 사용하여 이 인터페이스를 구현한다면 연결된 작업 단위를 표현하기 위한 훨씬 더 깔끔한 메커니즘을 갖게 될 것입니다.

게이트 설계 원칙

  • 조건이 충족되지 않으면 게이트는 양보해야 합니다.
  • 조건이 충족되면 게이트는 수반되는 작업 기능을 한 번만 실행합니다.
  • 게이트의 후속 호출은 이미 처리된 코드 블록을 다시 실행하지 않습니다.
  • 게이츠는 프로미스처럼 함께 묶일 수 있다.
  • 시퀀스의 모든 게이트가 호출되면 전체 흐름이 재설정됩니다.

실행

저는 주어진 문 주위에 코드를 주입할 수 있는 독특한 기회를 주는 러스트 매크로를 사용하여 게이트를 구현하기로 결정했습니다. 이 언어 기능을 사용하면 루프 { } 내에서 안정적이고 생성된 임의의 이산 게이트에 대해 실행할 수 있는 것을 작성해야 합니다.

 

게이트를 나타내는 고유한 해시를 유도할 수 있다면 게이트 제공 메커니즘이 따를 수 있는 다음과 같은 유사 코드가 합리적인 템플릿이 될 수 있습니다.

static mut SYSTEM_GATES: BTreeMap<u32, Gate> = BTreeMap::new();
loop {
     // Given a unique identifier that describes a block of code...
     let id: u32 = code_hash();
     let gate: Gate = unsafe { SYSTEM_GATES.get(id) };
     match gate {
              None => {
                           // Create gate and add it to the SYSTEM_GATES static
              },
                       Some(gate) => {
                                    // Process gate
                       }
     }
}

여기에 한 가지 재앙적인 주의사항이 있다: 녹은 반사를 지원하지 않는다. 그 부분은 잠시 접어두고 매크로가 어떻게 생겼는지 알아보겠습니다.

이 매크로에서는 게이트의 존재 여부를 조회하고 빛나는 새 게이트 또는 저장된 인스턴스를 반환합니다. 이 매크로의 멋진 점은 게이트 앞에 코드를 주입한다는 것입니다. 이것은 제가 잠시 후에 다룰 중요한 기능입니다.

#[macro_export]
macro_rules! gate_open {
      ( $( $x:expr ),* ) => {
        {
                      // Get the hash of the gate in question
                      let id = code_hash();
                      let current_node = unsafe { GATES.get(id) };
                      let result: &mut Gate;

                      // Determine if the gate already exists, or if we need to make it
                      match current_node {
                                        None => {
                                                              // Let's create a new gate.
                                                              let new_gate = crate::mem::alloc::<Gate>();
                                                              unsafe { *new_gate = Gate::new(); }
                                                              // This new gate is what we'll return
                                                              result = unsafe { &mut (*new_gate) };
                                                              // Insert the gate in the global gate register
                                                              unsafe { GATES.insert(id, new_gate as u32) };
                                        },
                                                          Some(gate) => {
                                                                                result = unsafe { (gate as *mut Gate).as_mut().unwrap() };
                                                          }
                      }
                      result
        }
      };
}
 

빌더 구현 내부의 내용은 자세히 설명하지 않겠습니다. 그러나 높은 수준의 구현은 다음과 같습니다.

게이트에 구성요소를 추가할 때마다 조건이 조건 벡터에 저장되고, 작업 함수가 함수 벡터에 저장되는 식입니다. 시연할 코드는 다음과 같습니다.

pub fn when(&mut self, cond: CondFn, then: ExecFn) -> &mut Self {
      if self.built {
                return self;
      }
      self.conditions.add(cond);
      self.functions.add(then);
      self.tail += 1;
      return self;
}
pub fn build(&mut self) -> Gate {
      if self.built {
                self.process();
      } else {
                self.compiled = true;
      }
      return *self;
}

비밀 소스는 빌드 방법에 있습니다. 게이트가 이미 빌드된 경우 build()가 프록시 처리()됩니다. 이러한 방식으로 이후에 게이트를 구축하려고 하면 게이트가 대신 실행됩니다.

code_codice_codice_codice

 

그 모든 코드는 멋져 보이지만, 그것은 단순히 존재하지 않는 언어적 특징에 기초한다. 루프 { }에서 안정적인 함수의 고유한 해시를 어떻게 도출할 수 있습니까? 이 조각이 없으면 모든 것이 무너집니다.

언어의 제한이 아름다운 코드에 방해가 되지 않도록 하세요!

/// This function returns a u32 containing the
/// program counter of the line of code which
/// invokes this function.
pub fn code_hash() -> u32 {
      let result = 0;
      unsafe {
                asm!("mov r0, lr");
      }
      return result;
}

이게 내 저항력이야

녹 매크로가 함수 호출 주위에 코드를 주입할 수 있게 해주기 때문에 프로그램 카운터는 Gate() 생성자를 호출하기 전에 고유하게 된다. 그러나 이 모든 것을 반복 실행하므로 반복해도 일관성이 있습니다.

 

다시 말해, 게이트 생성자를 호출하는 코드의 리터럴 라인은 게이트마다 다를 것이다. 하지만 애플리케이션 루프의 반복 사이에는 절대 변하지 않습니다. 이 절대성을 이용하면, 우리는 이 전체 솔루션을 하나로 모으는 데 필요한 탐나는 고유한 해시를 생각해 낼 수 있습니다.

마무리

제 목표는 효율적인 베어메탈 코드를 작성하는 것을 매우 간단하게 만드는 것입니다. 다시 생각할 필요 없이 자동으로 처리 능력을 산출할 수 있도록 말이죠. 이 게이트 개념은 내 커널 프로젝트에서 이미 가치를 보았습니다. 그러나 다음과 같은 제한이 있습니다.

  • 두 개의 익명 함수를 나란히 두는 구문이 신경 쓰인다.
  • 게이트는 불필요하게 느껴지는 몇 가지 추가 사이클을 도입합니다.
  • WS2812b LED용 드라이버를 작성하는 것과 같이 매우 복잡한 타이밍의 경우 게이트가 충분히 강력하지 않다는 것을 알게 되었습니다.

그렇긴 하지만, 저는 이 개념이 근본적으로 가치를 제공하는 세상을 볼 수 있습니다. 나는 그것의 crate 버전을 곧 출시하기를 희망합니다.

 

전체 gate_open!() 프로토타입을 보려면 코드를 살펴보십시오. 이 기사를 내고 나면 조금 달라질 것 같지만, 그 감정은 같을 것이다.

댓글