레이블이 ARM인 게시물을 표시합니다. 모든 게시물 표시
레이블이 ARM인 게시물을 표시합니다. 모든 게시물 표시

2024년 10월 3일 목요일

CIF 480 Embedded DVR 개발기

우여 곡절 끝에 X86으로 DVR을 완성하고 한숨 돌렸을 때였다.

개발 하고 나서 보면 반성할 점도 많고 후회되는 부분도 많은 제품이었다.

다행인건 , 그렇게 해서라도 잘 동작은 해줬다는 것이었다.


시간이 지나고 새로운 DVR 개발에 대한 요청이 들어왔다.

기간은 6개월,  당시 TOP3 안에 드는 보안업체에 주력 납품될 H.264 코덱 지원하는 DVR개발이었다.

성능은 CIF480 FPS, 당시 그 정도 성능을  내는 저가형 Embedded 제품은 없다고 봐야 하는 것과 마찬가지였다. 아직  hisilicon 칩이 라인업과 성능이 애매하던 시절이었다.


가능성을 조사를 하기 위한 한달의 여유를 경영진에 요청하였다.

일단 코덱의 경우 이전 제품을 만들면서 Softlogic 사와 좋은 관계를 유지하게 되어 , 신형 H.264 코덱에 대한 정보를 받을 수 있었다.

Softlogic사의 코덱인 Solo6110이 새로 출시 될 예정이었고, 압축, 재생 성능과 비디오 출력 기능이 있어 코덱 + 비디오, OSD  기능을 한번에 해결할 수 있었다.

다음은 SOC였다. (메인 CPU)

Solo6110 개발용 EVK 에 장착된 SOC는 프리스케일사의 Power PC 계열의 SOC였다.

이것으로 개발하면 초기 개발은 비교적 쉬워지겠지만 몇 가지 이유가 마음에 걸려 다른 SOC를 찾기로 했다.

일단, 가격이 비싸다. 당시 해당 SOC는 20$ ~ 30$ 가량 하던 SOC로 목표 금액인 200$ 에 맞추기 위해서는 SOC가 차지하는 비중이 너무 컷고, 칩이 크다보니 주변 구성 칩들도 가격이 적지 않았다.

그리고 Big-endian 이었다. 나는 PC 친화적인 방향으로 하여 개발 편의성과 PC 와 연동을 쉽게 하는 것을 목표로 삼고 있었기 때문에, Little-Endian 을 선호하였다.

SOC를 선정하기 위해 아래와 같은 조건의 SOC를 팀원들과 함께 찾아보았다.

1. 가격이 15$ 이하일것

2. Little endian 일것

3. PCI bus 필수 지원 (Solo6110 코덱이 PCI BUS)

4. SATA 지원 (하드디스크 지원 및 백업용 ODD 지원)


마음에 쏙 드는 칩은 없었고, 지금도 그렇지만 문서상 사양과 실제 사용 가능한 사양이 일치하지 않는 경우도 많았다.

여러 칩 벤더의 영업분들과 미팅을 하였고, Cortina 사의 DS3516으로 결정하였다.

칩 사양은 아래와 같았다.

CPU: ARM9TDMI

clock: 200Mhz

RAM TYPE: SDRAM

PCI BUS: 있음

USB : 2.0

SATA : 3G (2gen)

가격 : 8$

Power PC에 비하면 매우 부족한 CPU 성능이었지만, 다른 부가 기능들이 충분히 만족하였으므로, 부족한 CPU성능을 최적화로 극복해 보기로 했다.


부족한 성능을 극복하기 위하여 이번에도 Solo6110의 드라이버를 직접 개발하기로 했다.

DVR의 제일 많이 사용하는 데이터 전송인 압축된 데이터 전송, OSD 출력에 CPU 의존성을 최대한 낮추기 위하여 PCI BUS MASTER DMA를 최대한 활용해야만 했다.

덕분에 DVR의 엔진과도 같은 상당한 기능을 Solo6110 드라이버에서 담당하게 되었다.

덩치 큰 드라이버 개발도 쉽지 않았지만, 생각보다 CPU의 제약 사항도 많았다.

ARM9TDMI 의 성능 한계 상 나눗셈 연산이 매우 느렸고, unaligned memory access 를 지원하지 않아, 데이터를 읽을 때에는 반드시 변수 크기의 배수 주소에서 읽어야만 했다.

무슨 뜻이냐면, 32bit int 형 변수를 읽으려면 32bit는 4byte 이므로 4의 배수 포인터에 있는 변수만 읽을 수 있고, 4의 배수가 아닐 경우 읽은 값이 깨져서 들어오거나 일부 CPU는 unaligned exception error 가 발생한다.

평소에는 컴파일러가 변수 크기에 맞춰 주소 정렬을 해주어 상관 없지만, 통신 등으로 binary 데이터를 분석 할 때에는 정렬이 되어있지 않는 경우를 대비하여 정렬된 주소에 한 바이트 씩 복사한 후 값을 읽어야 한다.

우여곡절 끝에 기본 기능을 완성하고 백업을 위한 ODD(광학 드라이브, CD라이터나 DVD라이터)를 연결할 차례가 되었다.

여기서 한번 뒤통수를 맞게 된다. SATA포트에 장착한 DVD Writer 가 동작하지 않는 것이었다.

확인 결과, DS3516은 분명 SATA를 지원하지만, 프로토콜을 하드디스크를 위한 ATA 만 지원하고, ODD 드라이브를 위한 ATAPI를 지원하지 않는 것이었다. 

이미 샘플보드를 만든 상황에서 급하게 비상 회의를 열고, PCI 버스를 확장하여 SATA컨트롤러인 sil3512를 추가하기로 한 것이다.

긴급으로 칩을 수배하여 수정된 보드에 다시 DVD Writer를 작동 시켰다.

이번에는 데이터가 깨지기 시작하는 것이었다.

확인 결과 DS3516는 PCI 의 burst 전송이 512 byte 이상 되면 데이터가 깨지는 현상이 발생하는 문제가 있었던 것이었다.

두 번째 난관은 비교적 평온하게 해결했다. 저가형 CPU다 보니 워낙 난관이 많았다 보니, 하드웨어 수정이 아닌 펌웨어나 소프트웨어 수정 작업은 그리 크게 느껴지지 않은 것이었다.

결국 DVD Write 하는 펌웨어, 소프트웨어도 직접 개발하여 PCI burst 를 512byte까지만 사용하도록 제약을 하였고, 직접 개발하게 되었으니 여기 서도 CPU를 최대한 쓰지 않게 하기 위하여 일반적인 DVD 백업처럼 iso 이미지 파일을 만든 후, 전송하여 굽는 방식이 아닌, 실시간으로 생성하여 iso 이미지를 저장할 공간 확보를 할 필요가 없도록 만들어, 백업 시간을 대폭 줄였다.

느린 CPU로 이것이 가능 한 이유는 DVD-writer들이 buffer under-run 방지 기술이 적용되어서 타이망의 제약을 받지 않아서 이기도 했다.

전화위복이라 하였던가, DVD-write 기능 직접 개발로 인해, 힘은 들었지만, 개발된 제품은 DVD backup이 타 제품보다 빠른 제품으로 인정받게 되었다.


중간에 예상치 못한 문제로 개발 기간도 2개월 가량 추가 되었고, sil3512 관련 부품이 추가되어 원가가 조금 오르긴 했지만, 목표했던 200$보다 저렴한 제품을 만들 수 있게 되었다.

다행히도 제품은 성공적으로 개발되어 보안회사에 무사히 납품할 수 있게 되었다.


2024년 9월 23일 월요일

테스크 스위칭과 TSS

오랜만에 글을 쓴다.

일이 많아 글을 쓸 여유가 없다, 나이가 먹고 더 기억력이 나빠지기 전에 해왔던 생각들을 글로 남겨 놓는 것도 좋을 것 같아 쓰기 시작한다.


OS에서 여러 개의 TASK들을 돌아가며 돌리기 위해서는 운용할 TASK 리스트를 준비 해 두고, CPU의 타이머 인터럽트가 발생 할 때 실행 우선 순번이 높은 TASK쪽으로 복귀하는 식으로 multitasking 을 운용한다.

코어가 1개인 CPU (hyper threading 은 제외한다)에서 실행되는 multitasking은 결국 timer interrupt 에 의한 CPU 노가다 인 샘이다.

하지만 이것은 그만큼 CPU들의 성능이 비약적으로 빨라졌기 때문에 가능해 진 것이다.

일반적으로 timer interrupt는 x86 계열 CPU에서는 초 당 1000번 (1ms 간격) , 그 외 CPU에서는 대부분 초 당 100번 (10ms  간격) 으로 발생한다.

CPU가 느리다면 하나의 task 당 1ms, 혹은 10ms 에 실행할 수 있는 코드의 양은 매우 적기 때문에 매우 느리고 답답할 것이다.

하지만 근래 CPU들의 경우 클럭이 최소 500Mhz 부터 5GHz까지 높은 속도로 실행된다.

이 뜻은 클럭 당 1개의 명령을 수행한다고 가정할 때 10ms 인터럽트 간격 동안 5,000,000 ~ 50,000,000 개의 명령을 실행할 수 있다는 뜻이다.

즉, 초 당 위 명령 정도만 실행하는 task는 동시에 100개 가량 운용해도 속도 저하가 없다는 뜻이다.

이 외에도 OS 차원에서 대기 중인 task (sleep 이나 이벤트 대기)의 경우 아직 wake 상태가 아니면 실행 안 하는 등의 최적화를 통하여 multitasking의 효율은 더욱 높아지게 되었다.


그래도 여전히 multitasking에서 부담스러운 부분이 있는데 바로 context switching 이다.

즉 task 교환이다.

A라는 task에서 연산을 하던 중, 타이머 인터럽트가 발생하여 B라는 task로 이동할 때 해상 task가 마지막에 운용되던 상태를 그대로 복원해야 하는 작업이 context switching 시 수행된다.

task가 사용하던 stack과 register의 마지막 상태 들을 저장 및 복원해야 한다.

이를 위하여 x86에서는 TSS를 지원하고 ARM계열에서는 레지스터를 동시에 여러 개를 stack에 push 하는 명령을 지원하였다.

32bit 초기 시절 32bit OS개발 업무를 하던 당시, x86의 TSS라는 지원이 뭔가 고급스럽게 느껴지게 되었고, 이 후 tss를 지원하지 않는 OS나 CPU들이 답답하게 느껴 졌었다.


TSS는 task state segment 라 하며 x86에서 테스크의 마지막 상태를 저장하고, 복원하기 위한 메모리 영역과 descriptor 였다.  따라서 테스크 이동을 할 경우 TSS의 NT 기능을 활용하면 FPU등 부분만 주의만 하면 비교적 고급스럽게 context switching을 할 수 있게 되었다.


하지만 시간이 흐르고 64bit 시절이 오며 x86역시 TSS를 호환상 유지하기는 하지만 context switching에서 큰 비중을 차지하지 않게 되었다.

이 시절이 되니, 나에게도 역시 TSS가 context switching에 더 이상은 유용해 보이지 않았다.

이유는 CPU의 많은 확장과 다양성 때문이었다.

예전에는 context switching을 하기 위하여 저장하고 복원해야 하는 것들이 general, stack, FPU 레지스터 정도였다.

하지만 지금은 CPU 종류 별로 다양한 부가 기능을 지원하다 보니, SIMD 계열 레지스터도 다양하게 늘어났고, NPU 연산까지 추가되고 있기 때문에 OS에서는 context switching 시 CPU가 지원하는 기능을 파악하고, 그에 맞춰 저장 및 복원해야 하는 항목들이 달라지는 상황이 온 것이다.

이럴 때 편하기는 하지만, 유연성이 떨어지는 TSS의 경우 효율이 좋기 어렵고 CPU 개발에 부담만 가중 시키는 것이라 생각된다.


세상이 변하면서 TSS에 대한 나의 생각도 변하게 되었다는 것이 생각나 글을 적어보았다.

앞으로 글을 쓸 거리가 생각나고 시간이 날 때 마다 글을 쓸 예정이다.

그럼 다음에...

2017년 5월 5일 금요일

ARM Cortex 와 최적화 - 나눗셈

요즘은 내가 주로 사용하는 SOC (system on chip)의 코어가 cortex 계열이다.
Cortex 계열을 사용하다보면 느끼는 점은, 예전 ARM 920 시절보다
참 많이 빨라지고 좋아졌다 라는 느낌이다.
적어도 정렬문제 (Byte alignment)에서 해방되었다는 것만 해도 큰 축복 같았다.
(이 부분에 대해서는 다음에 한번 글을 써보기로 하겠다.)
현업에서 제품을 개발하는 분들은 공감하실수 있는 부분이지만,
사용자 분들의 요구치는 항상 하드웨어 발전 속도를 앞지르는 것 같다.
특히 내가 있는 영상 음성 보안 분야의 경우 SOC 선택의 폭이 좁아지면서
비슷한 하드웨어로 가격과 성능을 경쟁하는 상태가 되었다.

즉, 같은 하드웨어로 누가 더 많은 성능을 뽑아내어 보다 많은 소비자의 요구 기능을 뽑아내는지 경쟁이 시작된 것이다.
(솔직히.. 이것때문에 매우 힘들다. 일부 업체의 경우 안정성까지 희생해 가면서 성능을 뽑아내는데, 이런 제품을 본 소비자 분들은 저 회사 제품과 같은 성능에 안정성까지 요구하시게 된다. 물론 동일한 가격으로.. ㅜ_ㅜ)

가격이 고정되어 하드웨어를 손 볼 수 없다면 소프트웨어를 손봐서라도 고객의 요구를 만족시켜드려야 프로(?)라 할 수 있지 않겠는가... 라는 자조 섞인 말로 암시를 걸면서 개발자의 본분(?)인 삽질을 열심히 하고 있다.

성능을 조금이라도 더 뽑아낼 수 있는 부분이 있을까를 확인하기 위하여 시간을 핑계로 대강만 봐왔던 Cortex를 면밀하게 검토해 봤다.

헉.. 모든 Cortex 가 곱셈과 나눗셈이 CPU에 내장된 것이 아니었어?!
적어도 Cortex - M 시리즈는 몰라도 Cortex - A시리즈는 되있을 거라 생각했는데 그건 아니었다.

[Cortex 코어별 나눗셈 지원 여부]

CortexThumb 모드 지원일반모드지원
A57지원지원
A53지원지원
A15지원지원
A9지원안함지원안함
A8지원안함지원안함
A7지원지원
A5지원안함지원안함
R7지원지원
R5지원일부 지원
R4지원지원안함
M4지원지원안함
M3지원지원안함
M1지원안함지원안함
M0지원안함지원안함
M0+지원안함지원안함
             <출처: ARM Community : https://community.arm.com>

헉.. Cortex A9 와 Cortex A8이 나눗셈 명령 (SDIV/UDIV)가 기본지원이 아니라고!?
불행히도 내가 쓰는 SOC의 상당수 코어가 Cortex A9와 Cortex A8이었다.

Cortex가 기본적으로 나눗셈 instruction 을 지원하여 왠만해서는 25클럭을 넘지 않으리라 생각하고 만든 부분들을 전면적으로 재 검토 하여야 했다.
C++에서 나눗셈의 제수가 상수인 부분이야 쉬프트 계열 명령으로 자동 대체 되겠지만,
아닌 부분들은 뺄셈으로 구현하는 수 밖에 없게 된다.

그렇다고 피제수가 제수보다 작아질때까지 무조건 루프돌며 빼는 끝을 기약하기 힘든 형태로 뺀다면 32비트 기준으로 최대 40억회 가량의 루프를 돌아야 한다.
클럭으로 따지면 400억 클럭정도까지 소요될 수도 있다는 뜻이다.

그래서 일반적으로 제수와 피제수의 크기차이를 확인하고, 이에따라 방법을 결정하는데 비트 위치에 맞추어 빼는 형식으로 루프 회수를 줄인다.
예를 들어 14112 를 11로 나누는 경우, 무조건 빼가며 루프를 돌 경우 1282번의 루프를 돌아야 한다.
하지만 비트 위치에 맞추어 빼는 형식으로 하면 아래와 같다.
계산은 2진수 기준으로 한다.

14112 는 2진수로 11011100100000‬  (2진수 14자리)
11은 2진수로 1011  (2진수 4자리)

(루프 1회 - 13번 비트부터 제수와 동일한 4자리 비교)
11011100100000 -> 101100100000   (피제수)
10110000000000   (제수)
     10000000000   (몫)   피제수가 더 크므로 1

(루프 2회 - 12번 비트)
0101100100000   (피제수)
1011000000000   (제수)
     10000000000   (몫)   제수가 더 크므로 0

(루프 3회 - 11번 비트)
0101100100000   -> 100000 (피제수)
 101100000000   (제수)
     10100000000   (몫)   피제수가 더 크므로 1

(루프 4회 - 10번 비트)
00000100000 (피제수)
10110000000   (제수)
10100000000   (몫)   제수가 더 크므로 0

(루프 5회 - 9번 비트)
0000100000 (피제수)
1011000000   (제수)
10100000000   (몫)   제수가 더 크므로 0

(루프 6회 - 8번 비트)
000100000 (피제수)
101100000   (제수)
10100000000   (몫)   제수가 더 크므로 0

(루프 7회 - 7번 비트)
00100000 (피제수)
10110000   (제수)
10100000000   (몫)   제수가 더 크므로 0

(루프 8회 - 6번 비트)
0100000 (피제수)
1011000   (제수)
10100000000   (몫)   제수가 더 크므로 0

(루프 9회 - 5번 비트)
100000 (피제수)
101100   (제수)
10100000000   (몫)   제수가 더 크므로 0

(루프 10회 - 4번 비트)
100000  ->  1010 (피제수)
 10110   (제수)
10100000010   (몫)   피제수가 더 크므로 1

(루프 11회 - 3번 비트)
1010 (피제수)
1011   (제수)
10100000010   (몫)   제수가 더 크므로 0

위와 같이 11회의 루프에 연산을 마쳤다.
물론 사전에 bit 개수를 알아내는 작업이 필요하거나, 무조건 32 - 제수 비트수 만큼의 루프를 돌아야 하지만, 최대 32회의 루프면 충분하다.
그래도 CPU에서 직접 지원하는 명령보다 매우 느리다는 점은 여전하다.

Cortex도 A15급이상이 아닌 이상 되도록이면 상수가 아닌 변수간 나눗셈 연산은 피하거나 쉬프트로 대치하는것이 좋다는 것을 알게 되었다.

왠만하면 코드 재 사용을 하기 위하여 어셈블리는 되도록 손을 대지 않으려 하지만, 상황은 나에게 계속 어셈블리를 할 수 밖에 없는 상황이 되어가는것 같다.
1999년 경 C / C++을 잘 몰라 순수 어셈블리로 32bit OS를 개발했던 적이 있었다.
GPU 도움을 받지 못해 UI표현 속도가 너무 느려, 어디까지나 MMU 제어와 GUI, 멀티미디어에 대한 스터디 차원의 일이었지만, 이 작업이 인정받아 서울의 한 OS개발 프로젝트를 진행하는 개발사에 취직한 적이 있었다.
이 때 월요일에 출근해서 토요일까지 감금당하다 시피 일을 하다보니, 젊은 나이에 쇼크가 컷던지 OS 개발 분야를 포기하고, 지금과 같이 영상 , 음성 보안이나 신호처리로 전향하게 된 것 같다.
이 분야는 여러가지 SOC를 사용해야 하다보니 효율성을 위하여 재 사용 가능한 코드 개발을 지향하게 되었고, 때문에 어셈블리 사용을 최대한 자재하게 되었다.
세상은 그런 게으름(?)을 용납하지 않으려는지 아키텍쳐간 최적화를 하라고 요구한다.
사용자의 요구를 훨 씬 뛰어넘는 CPU가 나오기 전까진 앞으로도 이 삽질을 계속 해야만 할것 같다...

PS:  블로그에서 표그리기 왜이리 힘들까요 ㅠ_ㅠ

ARM 어플리케이션 디버깅 준비

[컴파일]

디버깅 모드로 컴파일 하면 나중에 에러 발생 시 나온 이미지 파일로 에러가 난 위치를 찾을 수 있다는 장점이 있다.

gcc 기준으로 컴파일 옵션에서 -O2 대신 -g -O0 옵션으로 대체한다.
-g는 함수, 해당 코드가 소스상 어느라인인지를 알려주는 라인정보등을 추가한다는 뜻이고,
-O0는 최적화 정도를 말하는데 0이므로 최적화를 하지 않는다는 뜻이다.
최적화를 실행 할 경우, 지역변수 생성을 동적으로 하고, 일부 코드의 경우 자동으로 최적화된 코드로 대체하기 때문에 에러가 발생한 위치를 소스와 매칭시킬수 없다.

[실행 전]
ulimit -c 명령을 이용하여 에러가 발생했을때 core 파일이 생성 될 수 있도록 한다.
core 파일은 말 그대로 해당 프로세스의 CPU상태 (레지스터 등), 메모리 상태를 덤프하는 것을 뜻한다.
하드디스크나 NFS등으로 실행하여 저장 용량이 넉넉 할 경우 ulimit -c unlimit 로 하여도 되지만
RAM DISK 등이나 NAND 플래시와 같이 여유 저장공간이 적은곳에서 실행파일을 실행 할 경우
용량에 맞춰서 수치를 적어준다. ex) ulimit - c 1024

[실행 후]
에러가 발생하였을 경우 실행파일 있는 자리에 .core 라는 파일이 생긴다.
당연한 말이지만 저장 불가능한 cd 나 dvd 와 같은 곳애서 실행한다면 생기지 않을 것이다.

.core 파일이 생성되었으면 그 자리나 source 파일 있는 곳에다가 복사해 놓고 gdb로 불러 들인다.
gdb역시 해당 CPU(타겟 CPU)에 맞는것을 써야한다.

실행방법은 
gdb 실행파일명 .core 
이다.

어디서 멈춰섰는지를 찾고 싶으면 화면에서 where 를 치면 된다.