본문 바로가기
Power Electronics

디지탈 전력변환-제2장 CCS + C2000Ware 개발환경 및 F28P65x 프로젝트 구성

by linuxgo 2026. 6. 21.

2.1 서론 

전력 변환기를 설계하는 엔지니어가 흔히 저지르는 착각이 하나 있다. "회로만 제대로 그리면 코드는 인터넷에 있는 예제를 가져다 쓰면 된다"는 생각이다. 그러나 실제 현장에서 보드를 들고 와서 "ePWM 듀티가 이상하게 튄다", "ISR이 가끔 멈춘다", "Flash에 구워서 실행하면 RAM에서 디버깅할 때와 동작이 다르다" 같은 문제로 며칠을 허비하는 경우, 그 원인의 상당수는 회로나 알고리즘이 아니라 개발환경 — 특히 링커가 메모리를 어떻게 배치했는지, 어떤 코드가 Flash에 남아있고 어떤 코드가 RAM으로 복사됐는지 — 를 이해하지 못한 데서 온다.

1장에서 우리는 F28P65x가 어떤 하드웨어를 가졌는지 — 클록이 어떻게 분배되고, 메모리가 어떻게 나뉘고, 인터럽트가 몇 사이클 만에 처리되는지 — TRM 원문을 근거로 정리했다. 이 2장에서는 그 하드웨어 지식을 실제로 동작하는 펌웨어로 옮기는 도구 체인 전체를 다룬다. Code Composer Studio(CCS)라는 IDE, 그 IDE가 호출하는 컴파일러·어셈블러·링커, C2000Ware라는 거대한 소프트웨어 패키지의 폴더 구조, 그리고 이 모든 것을 그래픽으로 엮어주는 SysConfig까지 — 이 장을 끝까지 읽고 나면 "왜 ISR 코드를 ramfuncs 섹션에 넣어야 하는지", "왜 .cmd 파일에 PAGE 0과 PAGE 1이 따로 있는지" 같은 질문에 더 이상 "그냥 예제에 그렇게 쓰여 있어서"라고 답하지 않게 될 것이다.

2.2 C2000Ware 생태계 구조

2.2.1 C2000Ware란 무엇인가

C2000Ware는 TI가 C2000 패밀리 전체를 위해 배포하는 단일 소프트웨어 패키지다. 디바이스별 드라이버, 헤더 파일, 수백 개의 동작 예제, 보드 설계 자료(스키매틱·BOM·거버 파일)까지 하나로 묶여 있다. 과거에는 "controlSUITE"라는 이름으로 디바이스 패밀리마다 따로 설치하는 방식이었지만, C2000Ware로 전환되면서 하나의 설치본 안에서 모든 디바이스를 다룰 수 있는 통합 구조로 바뀌었다.

이 통합 구조는 단순한 마케팅 문구가 아니라 실제 배포 저장소(TexasInstruments/c2000ware-core-sdk)의 구조로 직접 확인할 수 있다. 이 저장소는 F28P65x를 F2837xD, F2838x, F28004x 등 다른 모든 C2000 디바이스와 동일한 driverlib / device_support / libraries / utilities 폴더 체계 아래 나란히 포함하고 있다. 이 전환이 중요한 이유는, F28379D로 개발하던 코드 자산을 F28P65x로 옮길 때 폴더 구조 자체는 동일하게 유지되어, "어디서 무엇을 찾아야 하는지"에 대한 학습 비용이 거의 들지 않기 때문이다.

2.2.2 최상위 폴더 구조

C2000Ware를 설치하면 다음과 같은 최상위 디렉터리가 생성된다.

폴더 역활
driverlib 디바이스별 드라이버 라이브러리 소스 및 driverlib 기반 예제
device_support 디바이스별 비트필드 헤더, 공통 초기화 코드, 예제, 사용설명서(PDF)
libraries 디바이스별·코어 공통 라이브러리(DSP, IQMath, Digital Control 등)
utilities Flash 프로그래머, Windows 드라이버 등 개발 보조 도구
boards LaunchPad·ControlCARD·Experimenter Kit의 하드웨어 설계 자료
examples 여러 디바이스·주변장치를 아우르는 통합 예제
uninstallers 제거 도구

이 중 실무에서 가장 자주 드나들게 되는 곳은 단연 driverlib와 device_support다. 그런데 막상 처음 폴더를 열어보면 "왜 두 곳에 비슷한 게 또 있지?"라는 의문이 든다. 이 의문을 풀어보자.

2.2.3 driverlib와 device_support — 두 갈래 길의 이유

C2000Ware는 주변장치 레지스터에 접근하는 방법을 세 가지로 제공한다. 직접 레지스터 접근, 비트필드(Bitfield), 그리고 드라이버 라이브러리(DriverLib)다. 세 가지가 실제로 같은 동작을 수행하는 코드를 비교하면 차이가 분명해진다.

/* 방법 1: 직접 레지스터 접근 — 주소를 그대로 씀, 가독성 최하 */
*CMPR1 = 0x1234;

/* 방법 2: 비트필드 — 구조체로 레지스터를 표현, 의미는 보이지만 비트 단위 지식 필요 */
EPwm1Regs.CMPA.bit.CMPA = EPwm1Regs.TBPRD * duty;

/* 방법 3: Driverlib — 함수 이름으로 의도가 명확, 최고 수준의 추상화 */
EPWM_setCounterCompareValue(EPWM1_BASE, EPWM_COUNTER_COMPARE_A, duty);

이 세 가지 중 DriverLib이 신세대 MCU의 권장 접근 방식이며, 구형 MCU에서는 DriverLib이 아예 지원되지 않아 비트필드만 사용 가능한 경우도 있다. 다만 신세대 MCU에서도 비트필드는 호환성과 마이그레이션 편의를 위해 계속 패키징되고 있다. 즉 비트필드는 "옛날 방식이라 못 쓰는 것"이 아니라 "구형 코드 자산을 신세대 디바이스로 옮길 때 변경을 최소화하기 위한 다리" 역할을 한다.

이 두 접근 방식이 폴더 구조에 그대로 반영되어 있다.

driverlib 소스 위치
  driverlib/<device>/driverlib/        ← .c, .h 소스 전체 (직접 빌드 가능한 CCS 프로젝트 포함)
  driverlib/<device>/driverlib/inc/    ← 레지스터 정의 하드웨어 헤더 (주변장치당 1개 파일)
  driverlib/<device>/examples/         ← 주변장치별 driverlib 예제

bitfield(레지스터 헤더) 소스 위치
  device_support/<device>/headers/     ← 비트필드 헤더
  device_support/<device>/common/      ← 공통 초기화 코드
  device_support/<device>/examples/    ← 비트필드 기반 예제
  device_support/<device>/docs/        ← 디바이스별 사용설명서 PDF (예: CCS 프로젝트 설정법)

본서는 1장부터 일관되게 Driverlib을 코딩 표준으로 채택하고 있다. 그 이유는 단순히 "최신이라서"가 아니라, 1장에서 확인했듯이 TRM이 권장하는 복잡한 절차(예: SYSPLLMULT 69사이클 지연, ADC 트림값 로드)를 Driverlib 함수가 이미 검증된 형태로 캡슐화하고 있기 때문이다. 레지스터를 직접 두드리는 코드는 그 모든 절차를 작성자가 일일이 재현해야 하며, 한 단계라도 빠뜨리면 1장에서 본 것처럼 "ISR이 한 번만 실행되고 멈추는" 등의 버그로 이어진다.

2.2.4 라이브러리와 유틸리티

libraries 폴더에는 DSP 연산, IQMath(고정소수점 수학), Digital Control(디지털 제어기 블록), FreeRTOS 데모 등이 들어있다. 이들은 driverlib과 달리 디바이스 종속적이지 않은 알고리즘 자산이며, 전력 변환 알고리즘(PID, FOC 등)을 구현할 때 본서 후반부에서 적극적으로 활용한다.

utilities 폴더에는 Flash 프로그래머와 같은 개발 보조 도구가 들어있다. 보드를 양산 단계로 옮길 때 CCS 없이 단독으로 Flash를 굽는 도구가 필요한데, 그 해답이 여기 있다.

2.3 Code Composer Studio — 프로젝트 생성과 빌드 흐름

2.3.1 CCS는 무엇으로 이루어져 있는가

Code Composer Studio는 Eclipse 프레임워크 위에 TI의 컴파일러 도구 체인(cl2000)을 얹은 통합 개발 환경이다. "통합"이라는 말의 실질적 의미는, 코드 편집기·컴파일러·어셈블러·링커·디버거·Flash 프로그래머가 모두 하나의 창 안에서 연동된다는 뜻이다. CCS 자체는 디바이스를 모르는 범용 셸이고, 실제 디바이스 지식 — F28P65x의 메모리맵, 클록, 레지스터 — 은 C2000Ware가 제공하는 헤더와 .cmd 파일을 통해 CCS 안으로 흘러들어온다.

2.3.2 새 프로젝트 생성 — 공식 절차

CCS 12.8 사용설명서가 명시하는 신규 프로젝트 생성 절차는 다음과 같다.

메뉴: Project → New CCS Project... (또는 File → New → CCS Project)

New CCS Project 마법사에서:
  1. Target 디바이스 입력 또는 선택
  2. Connection(디버그 프로브) 타입 확인 — 자동 인식되지 않으면 직접 선택
  3. Project name 입력
  4. Use default location 체크 해제 시 프로젝트 저장 위치를 워크스페이스 밖으로 지정 가능
  5. Compiler version — CCS가 해당 디바이스 패밀리용으로 발견한 최신 버전이 기본 선택됨
  6. Output type 선택:
       Executable    — 타깃에 로드/실행 가능한 .out 생성 (가장 일반적)
       Static Library — 정적 라이브러리 생성
       System        — 멀티코어 디바이스의 여러 코어 프로젝트를 묶어 관리
  7. Project template/example 선택 후 Finish

여기서 "System" 프로젝트 타입은 F28P65x처럼 CPU1/CPU2 듀얼코어 디바이스를 다룰 때 특히 중요하다. CPU1 프로젝트와 CPU2 프로젝트를 개별적으로 만들 수도 있지만, System 프로젝트로 묶으면 "CPU1 빌드 → CPU2 빌드 → 단일 디버그 세션으로 두 코어 동시 로드"라는 흐름을 CCS가 자동으로 관리해준다. 듀얼코어 전력 변환 시스템(CPU1: 제어 루프, CPU2: 통신)을 구성할 때는 처음부터 System 프로젝트로 시작하는 것이 이후 디버깅 편의성 면에서 유리하다.

CCS 공식 문서는 이 절차에 중요한 단서를 하나 덧붙인다.

"Creating a new CCS Project from scratch requires a bit of prior knowledge about the device. Hence, it is recommended to always start with an existing CCS example project first."

즉 백지 상태에서 새 프로젝트를 만드는 것보다, C2000Ware에 포함된 driverlib 예제를 Import하여 거기서부터 수정해나가는 방식이 공식적으로 권장된다. 이는 게으름의 문제가 아니라, 예제 프로젝트 안에 이미 그 디바이스에 맞는 .cmd 파일, 클록 초기화 코드, 컴파일러 옵션이 검증된 상태로 들어있기 때문이다. 처음부터 새로 만들면 이 모든 것을 직접 채워 넣어야 하고, 사소한 누락 — 예를 들어 FPU 컴파일러 옵션을 빼먹는 것 — 이 며칠짜리 디버깅으로 이어질 수 있다.

2.3.3 기존 예제 Import — 실무에서 더 자주 쓰는 경로

메뉴: Project → Import CCS Projects

  1. Select search-directory에서 Browse로 예제 폴더 위치 지정
     (또는 압축 아카이브가 있다면 Select archive file)
  2. Discovered projects 목록에서 원하는 프로젝트 선택
  3. Copy projects into workspace 체크 여부 결정
       체크함  → 프로젝트 폴더 전체가 워크스페이스로 복사됨
                (C2000Ware 예제들은 이 방식이 안전하도록 설계되어 있음)
       체크 안 함 → 원래 위치에 그대로 남아 참조됨,
                  단 원본을 수정하면 다른 프로젝트에도 영향
  4. Finish

Copy projects into workspace를 체크하는 것이 권장된다. 체크하지 않으면 원본 C2000Ware 설치 폴더의 예제 파일을 직접 수정하게 되는데, 이후 C2000Ware를 업데이트하거나 다른 프로젝트에서 같은 예제를 참조할 때 의도치 않은 충돌이 생길 수 있다.

2.3.4 빌드와 디버그 — 한 번의 클릭 뒤에 숨은 6단계

CCS의 Run → Debug 메뉴 하나가 실제로는 다음 여섯 단계를 순차적으로 수행한다.

1. 소스 파일 저장 여부 확인 (수정사항이 있으면 저장 프롬프트)
2. 프로젝트 증분 빌드 (Incremental Build)
3. 디버거 시작 — CCS Debug 퍼스펙티브로 전환
4. 타깃에 연결 (XDS110 등 디버그 프로브를 통해)
5. 프로그램을 타깃에 로드 (Flash 프로그래밍 포함)
6. main 함수까지 실행(Run to main)

이 흐름을 이해하면 디버깅 중 발생하는 흔한 혼란이 풀린다. 예를 들어 "방금 코드를 고쳤는데 디버거에 옛날 동작이 보인다"는 증상은 대개 2단계(빌드)가 실패했거나 스킵된 경우다. 또한 "Run to main까지는 가는데 그 이후 멈춘다"는 증상이라면, 이는 빌드·로드·연결 문제가 아니라 6단계 이후의 애플리케이션 코드 자체의 문제임을 빠르게 좁혀낼 수 있다.

빌드만 하고 디버거는 띄우지 않고 싶다면 별도로 Project → Build Project를 사용한다. 양산 전 코드 사이즈만 확인하고 싶을 때, 혹은 CI 환경에서 빌드 자동화를 구성할 때 이 명령이 유용하다.

2.4 C28x 링커스크립트(.cmd) 완전 분석

2.4.1 링커가 실제로 하는 일

컴파일러는 C 소스 파일 하나하나를 오브젝트 파일(.obj)로 바꾼다. 이 오브젝트 파일 안에는 "이 함수는 .text라는 섹션에 넣어줘", "이 전역 변수는 .ebss라는 섹션에 넣어줘"라는 식의 정보만 있을 뿐, 실제로 메모리의 어느 주소에 위치할지는 정해져 있지 않다. 이 빈 자리를 채우는 것이 링커의 역할이다.

TI 공식 문서는 링커의 역할을 세 가지로 요약한다.

As the linker combines object files, it performs the following tasks: Allocates sections into the target system's configured memory; Relocates symbols and sections to assign them to final addresses; Resolves undefined external references between input files.

이 세 가지 작업 — 배치(Allocate), 재배치(Relocate), 참조 해결(Resolve) — 을 사용자가 직접 제어하는 통로가 바로 링커스크립트(.cmd 파일)다. 그리고 이 제어는 단 두 개의 지시어, MEMORY와 SECTIONS로 이루어진다.

2.4.2 MEMORY 지시어 — "여기에 이런 메모리가 있다"

MEMORY 지시어는 타깃 시스템에 실제로 존재하는 메모리 범위에 이름을 붙이는 일을 한다. 가장 단순한 형태는 다음과 같다.

MEMORY
{
    FAST_MEM : origin = 0x0100, length = 0x0100
    SLOW_MEM : origin = 0x7000, length = 0x1000
}

origin은 시작 주소, length는 그 메모리 영역의 크기다. 여기까지는 단순하지만, C28x 링커스크립트에만 등장하는 독특한 개념이 하나 있다 — PAGE다.

2.4.3 PAGE 0과 PAGE 1 — 역사적 기원과 현재의 의미

C28x용 .cmd 파일을 처음 보면 다음과 같은 구조에 당황하게 된다.

MEMORY
{
PAGE 0 :
    RAMM0    : origin = 0x000000, length = 0x000400
    H0SARAM  : origin = 0x00A000, length = 0x002000

PAGE 1 :
    RAMM1    : origin = 0x000400, length = 0x000400
    L0SARAM  : origin = 0x008000, length = 0x001000
}

왜 메모리가 두 개의 "페이지"로 나뉘어 있을까? TI 공식 Linker Command File Primer는 이 질문에 명확한 역사적 답을 제공한다.

"The C28xx devices are descended from a long line of C2xxx devices which started in the 1980's. Those early devices had separate memory buses for code and data. These buses were connected to physically separate memory blocks. Thus, it was possible for a specific address on PAGE 0 to have different contents than that same address on PAGE 1."

즉 PAGE 0과 PAGE 1은 1980년대 C2xxx 세대가 프로그램 버스와 데이터 버스를 물리적으로 분리했던 하버드 아키텍처의 흔적이다. 당시에는 PAGE 0의 주소 0x8000과 PAGE 1의 주소 0x8000이 완전히 다른 물리 메모리를 가리켰다. F28P65x를 포함한 현대 C28x 디바이스는 사실상 모든 메모리 버스가 통합되어 있어 이 분리가 더 이상 필요하지 않지만, 관례적으로 PAGE 0은 프로그램(코드) 공간, PAGE 1은 데이터 공간으로 계속 사용된다.

공식 문서는 이와 관련해 실무적으로 중요한 경고를 덧붙인다.

"If your linker command file uses PAGE 0 and PAGE 1, it is best to continue using it that way."

즉 새로 작성하는 .cmd 파일에서도 이 관례를 깨지 말아야 한다. PAGE 0과 PAGE 1에 동일한 메모리 범위 이름을 동시에 정의하는 것은 문법적으로는 허용되지만(서로 다른 "페이지"이므로), 실수로 같은 물리 주소를 두 번 정의하면 의도하지 않은 메모리 겹침이 발생할 수 있으므로 주의가 필요하다.

2.4.4 SECTIONS 지시어 — "이 코드 조각을 저 메모리에 넣어라"

MEMORY가 "어떤 땅이 있는지"를 정의했다면, SECTIONS는 "어떤 건물을 어느 땅에 지을지"를 정의한다. 가장 기본적인 형태부터 단계적으로 살펴보자.

1단계 — 축약 없는 완전한 표기:

SECTIONS
{
    output_section_name
    {
        file1.obj(.text)
        file2.obj(.text)
        file3.obj(.text)
    } > FLASH
}

이 코드는 output_section_name이라는 출력 섹션을 만들고, 그 안에 file1.obj·file2.obj·file3.obj 각각의 .text 입력 섹션을 모아 담은 뒤, 그 전체를 FLASH라는 메모리 영역에 배치한다.

2단계 — 와일드카드 축약:

SECTIONS
{
    output_section_name
    {
        *(.text)   /* 아직 다른 출력 섹션에 속하지 않은 모든 .text 입력 섹션 */
    } > FLASH
}

3단계 — 가장 흔히 보는 최종 축약형:

SECTIONS
{
    .text > FLASH
}

이 마지막 형태에서 출력 섹션 이름과 입력 섹션 이름이 똑같이 .text로 보이는 것은 우연이 아니라 관례다. 실제 프로젝트의 .cmd 파일에서 마주치는 코드 대부분이 이 3단계 축약형이므로, 그 이전 두 단계가 생략된 형태라는 것을 이해하고 있으면 더 복잡한 .cmd 파일도 어렵지 않게 읽힌다.

2.4.5 컴파일러가 생성하는 표준 섹션들

C 코드를 컴파일하면 컴파일러가 자동으로 다음과 같은 이름의 섹션들을 만들어낸다. 각 섹션이 무엇을 담는지 아는 것은 .cmd 파일을 읽고 쓰는 데 필수적이다.

섹션 이름 초기화 여부 내용
.text 초기화됨 실행 가능한 코드
.bss / .ebss 비초기화 전역 변수 (0으로 시작)
.cinit 초기화됨 전역 변수를 초기값으로 채우기 위한 테이블
.const 초기화됨 초기화된 전역 상수
.stack 비초기화 시스템 스택
.sysmem / .esysmem 비초기화 malloc()이 사용하는 힙 메모리 풀
.switch 초기화됨 일부 switch문을 위한 점프 테이블
.cio 비초기화 stdio 함수용 버퍼

예를 들어 다음과 같은 평범한 C 코드는,

int x = 2;   /* 전역 변수, 초기값 있음 */
int y;       /* 전역 변수, 초기값 없음 */

컴파일 시 x의 초기값 2는 .cinit 섹션에, y가 위치할 공간은 .ebss 섹션에 배정된다. 이 구분이 왜 중요한가 하면, .cinit은 Flash(비휘발성 메모리)에 있어야 하고, .ebss는 RAM에 있어야 한다는 점이 명확해지기 때문이다. 전원이 꺼졌다 켜져도 x가 항상 2로 시작해야 한다면, 그 초기값 2는 Flash에 영구 저장되어 있다가 부팅 시 RAM의 .ebss 영역으로 복사되어야 한다.

2.4.6 ISR을 RAM에서 실행하는 이유 — LOAD와 RUN의 분리

1장에서 우리는 F28P65x의 Flash가 SARAM보다 접근 시간이 길다는 것을 TRM 공식($RWAIT=\lceil f_{SYSCLK}/f_{FCLK}-1\rceil$)으로 확인했다. 20kHz 제어 루프의 ISR처럼 매 주기 반드시 실행되어야 하는 코드는 이 Flash 지연을 피하기 위해 RAM에서 실행되어야 한다. 그런데 RAM은 휘발성이므로, 전원이 꺼지면 내용이 사라진다. 이 모순을 푸는 방법이 바로 .cmd 파일의 LOAD/RUN 분리 문법이다.

SECTIONS
{
    .TI.ramfuncs : LOAD = FLASHD,
                   RUN  = RAML0,
                   LOAD_START(_RamfuncsLoadStart),
                   LOAD_END(_RamfuncsLoadEnd),
                   RUN_START(_RamfuncsRunStart)
}

이 코드가 의미하는 바는 다음과 같다.

  • 프로그램이 **로드(Flash에 구워질 때)**될 때는 .TI.ramfuncs 섹션이 FLASHD라는 Flash 영역에 저장된다.
  • 프로그램이 실행될 때는 이 섹션이 RAML0라는 RAM 영역에 있는 것처럼 동작한다.
  • LOAD_START, LOAD_END, RUN_START는 이 두 주소(Flash상의 시작 주소, RAM상의 시작 주소)를 가리키는 심벌을 만들어준다.

여기서 결정적으로 중요한 사실 하나를 TI 공식 문서가 명시적으로 경고한다.

"Note this copy is not done automatically. Explicit steps, not discussed here, must be taken in the application code."

즉 .cmd 파일이 "Flash의 어디서 RAM의 어디로 복사해야 하는지"라는 지도는 만들어주지만, 실제로 그 복사를 수행하는 코드는 작성자가 직접 작성해야 한다. 이 복사는 보통 main() 함수 맨 앞에서 memcpy(RamfuncsRunStart, RamfuncsLoadStart, RamfuncsLoadEnd - RamfuncsLoadStart)와 유사한 형태로 수행되며, C2000Ware 예제에서는 MemCopy()라는 헬퍼 함수로 자주 제공된다. 1장의 ISR 코드에서 사용한 #pragma CODE_SECTION(epwm1_isr, "ramfuncs")가 바로 이 .TI.ramfuncs 섹션에 해당 함수를 배정하는 지시어였다는 것을, 이제 그 동작 원리까지 포함해 완전히 이해할 수 있다.

2.4.7 메모리 속성(RWX)과 GROUP — 보호와 순서 제어

.cmd 파일을 읽다 보면 메모리 이름 뒤에 괄호로 (RX)나 (RWX)가 붙은 것을 종종 본다.

MEMORY
{
    FLASH1 (RX)  : origin = 0x00204000, length = 0x1C000
    C0     (RWX) : origin = 0x20000000, length = 0x2000
}

각 글자는 R(읽기 가능), W(쓰기 가능), X(실행 코드 포함 가능), I(초기화 가능)를 의미하며, 아무 표기가 없으면 네 가지 속성이 모두 허용된 것으로 간주된다. 실무에서는 이 속성이 강제로 적용되기보다는, 그 메모리 영역에 어떤 종류의 섹션이 들어가야 하는지를 문서화하는 용도로 가장 많이 쓰인다.

여러 출력 섹션이 메모리상에서 반드시 연속된 순서로 배치되어야 할 때는 GROUP 지시어를 사용한다.

GROUP : > CTOMRAM
{
    PUTBUFFER
    PUTWRITEIDX
    GETREADIDX
}

GROUP을 쓰지 않고 세 출력 섹션을 따로따로 같은 메모리 영역에 배치하면, 링커는 그 순서를 보장하지 않으며 다른 섹션이 그 사이에 끼어들 수도 있다. CPU1과 CPU2가 IPC로 메시지를 주고받을 때처럼, 버퍼와 인덱스 변수가 정확한 상대 위치를 유지해야 하는 경우 GROUP이 필수적이다.

2.5 SysConfig — 그래픽 기반 주변장치 설정

2.5.1 SysConfig가 풀어주는 문제

1장의 ePWM·ADC 초기화 코드를 돌이켜보면, EPWM_setClockPrescaler, EPWM_setActionQualifierAction 같은 함수를 십수 개씩 순서대로 호출해야 했다. 이 순서와 인자값들은 TRM과 Driverlib API 문서를 정확히 대조하지 않으면 틀리기 쉽다. SysConfig는 이 진입장벽을 낮추기 위한 TI의 그래픽 설정 도구다.

TI 공식 문서는 SysConfig의 가치를 다음과 같이 요약한다.

"C2000 real-time MCUs can be initialized through C2000 SysConfig which generates reliable and pre-verified code for configuring your device. Device configuration errors are caught by the tool, and the developer is notified of the unsupported settings."

핵심은 "pre-verified code"라는 표현이다. SysConfig가 생성하는 초기화 코드는 결국 우리가 1장에서 직접 작성한 것과 같은 Driverlib 함수 호출의 나열이다. 차이는, SysConfig GUI에서 클록을 잘못 설정하거나 핀이 충돌하면 빌드 이전에, 즉 GUI 단계에서 즉시 오류로 알려준다는 점이다.

2.5.2 SysConfig의 작동 위치

SysConfig는 C2000 driverlib 소프트웨어 계층 위에 구축되어 있다. 즉 SysConfig는 새로운 코딩 방식이 아니라, driverlib 함수 호출을 그래픽으로 생성해주는 코드 생성기다. 프로젝트 안에 .syscfg 확장자를 가진 파일이 있으면, 이를 더블클릭하는 순간 CCS에 내장된 SysConfig 편집기가 열린다.

SysConfig 시작 절차 (공식 문서 기준)
─────────────────────────────────────────────
1. CCS에서 Project → Import CCS Project로 .syscfg가
   포함된 driverlib 예제 import
   (C2000Ware\<버전>\driverlib\<device>\examples\...)
2. Project Explorer에서 .syscfg 파일을 더블클릭
   → SysConfig GUI가 CCS 내부에 열림
3. 우측 상단 Device View 버튼으로 디바이스/패키지
   확인 (핀 배치를 패키지 단위로 시각화)
4. 주변장치 추가 → 옵션 설정 → 자동으로
   ti_msp_dl_config.c 또는 동등한 초기화 파일 생성
─────────────────────────────────────────────

2.5.3 SysConfig가 다루는 범위 — 단순 핀먹스를 넘어서

SysConfig는 단순한 핀 멀티플렉싱 도구로 오해되기 쉽지만, 실제로는 훨씬 복잡한 모듈까지 포괄한다. TI 문서는 CLB(Configurable Logic Block, 사용자 정의 로직)와 DCSM(Dual Code Security Module, 지적재산 보호)을 명시적으로 언급한다.

"More complicated peripherals such as the Configurable Logic Block (CLB)... or the Dual Code Security Module (DCSM)... are also included in the C2000 SysConfig ecosystem."

이는 CLB처럼 본질적으로 "사용자가 회로처럼 그리는" 주변장치에서 특히 가치가 크다. CLB의 로직 타일 연결을 코드로 일일이 기술하는 것보다 그래픽으로 블록을 잇는 편이 훨씬 직관적이기 때문이다.

2.5.4 웹 버전과 디바이스 마이그레이션

SysConfig는 로컬 설치 없이도 dev.ti.com을 통해 웹에서 바로 사용할 수 있으며, CCS Cloud에서도 동일하게 동작한다. 또한 "Universal Project"라는 기능을 통해, 한 C2000 디바이스용으로 설정한 프로젝트를 클릭 한 번으로 다른 C2000 디바이스로 마이그레이션할 수 있다. 이는 F28379D로 먼저 프로토타입을 만들고 이후 F28P65x로 옮기는 전형적인 개발 흐름에서 실질적인 시간 절약으로 이어진다.

2.6 HAL 계층 — driverlib이 레지스터를 감싸는 방식

2.6.1 헤더와 구현의 분리 — 단순 함수와 복잡한 함수의 차이

driverlib의 소스 구조를 들여다보면, 모든 주변장치 드라이버가 동일한 패턴을 따른다는 것을 알 수 있다.

driverlib/<device>/driverlib/
  epwm.h    epwm.c     ← 헤더 + 구현
  adc.h     adc.c
  sysctl.h  sysctl.c
  ...
  inc/      ← 레지스터 정의 하드웨어 헤더 (주변장치당 1개)

TI 공식 문서는 헤더 파일(.h)이 담는 내용을 네 가지로 구분한다.

#defines – usually meant to be used for parameters; typedefs – both struct and enum, usually meant to be used for parameters; static inline functions – all relatively short functions are inlined for performance; extern function prototypes.

이 설명이 실제 F28P65x driverlib 소스(C2000Ware v26.01.00.00, epwm.h/sysctl.h/adc.h)에서 정확히 어떻게 나타나는지 직접 대조해보면 흥미로운 패턴이 드러난다.

/* epwm.h — 단순 레지스터 쓰기 함수: static inline으로 정의됨 */
static inline void
EPWM_setCounterCompareValue(uint32_t base, EPWM_CounterCompareModule compModule,
                            uint16_t compCount)
{
    /* HWREGH(base + ...) = compCount; 식의 단순 레지스터 대입 */
}

static inline void
EPWM_setActionQualifierAction(uint32_t base, ...)
{
    /* 마찬가지로 레지스터 비트 조작 한두 줄 */
}

/* sysctl.h — PLL Lock 폴링, DCC 검증 등 다단계 절차가 필요한 함수:
 * extern으로 선언되고 sysctl.c에 실제 구현이 분리되어 있음           */
extern bool
SysCtl_setClock(uint32_t config);

/* adc.h — 트림값 로드 등 부수 작업이 있는 함수도 동일하게 extern */
extern void
ADC_setMode(uint32_t base, ADC_Resolution resolution,
            ADC_SignalMode signalMode);

즉 공식 문서의 "relatively short functions are inlined"라는 설명은 함수 단위가 아니라 함수의 복잡도에 따라 선택적으로 적용된다. EPWM_setCounterCompareValue()처럼 레지스터 한두 개를 대입하는 짧은 함수는 실제로 인라인으로 펼쳐져 직접 레지스터를 두드리는 것과 동일한 실행 속도를 낸다. 반면 1장에서 다룬 SysCtl_setClock()(PLL 설정 8단계와 Lock 폴링을 포함)이나 ADC_setMode()(분해능 변경 시 트림값을 함께 로드)처럼 여러 단계의 절차나 부수 효과가 있는 함수는 extern으로 선언되어 별도 오브젝트 코드로 컴파일된다.

이 구분은 실무적으로 중요한 의미를 가진다. ISR처럼 매 사이클이 중요한 코드에서 EPWM_setCounterCompareValue()를 호출하는 것은 추가 함수 호출 오버헤드가 없으므로 안심하고 사용할 수 있다. 반면 SysCtl_setClock()이나 ADC_setMode()는 ISR 안에서 반복 호출할 함수가 아니라 — 애초에 그렇게 설계되지도 않았다 — 초기화 시점에 한 번 호출하는 함수라는 것이 코드 구조 자체로 드러난다.

2.6.2 하드웨어 헤더와 주변장치 드라이버의 관계

driverlib/inc 폴더에 있는 하드웨어 헤더 파일은 실제 레지스터의 비트 단위 정의를 담고 있다. 이는 1장에서 우리가 TRM을 직접 읽으며 확인했던 SYSPLLMULT의 REFDIV, ODIV, IMULT 비트필드와 정확히 대응한다. 즉 driverlib의 함수(SysCtl_setClock() 등)는 내부적으로 이 하드웨어 헤더의 비트 정의를 참조하여 레지스터를 조작하며, 사용자는 그 비트 단위 디테일을 몰라도 함수 인자만으로 의도를 전달할 수 있다.

이 계층 구조를 그림으로 정리하면 다음과 같다.

사용자 애플리케이션 코드
        │  EPWM_setCounterCompareValue(EPWM1_BASE, ..., duty)
        ▼
driverlib 함수
        │  단순 레지스터 쓰기 → static inline (epwm.h에 직접 정의)
        │  PLL/트림 등 다단계 절차 → extern (epwm.c/sysctl.c/adc.c에 구현)
        ▼
하드웨어 헤더 (driverlib/inc/hw_epwm.h 등)
        │  EPWM_O_CMPA, 비트 위치 #define
        ▼
실제 F28P65x 레지스터 (TRM에 정의된 물리 주소)

2.6.3 비트필드와의 공존 — 점진적 마이그레이션

C2000Ware는 driverlib과 비트필드가 같은 프로젝트 안에 공존하는 것을 막지 않는다. 실제로 레거시 F2837xD 코드 자산을 가진 팀이 F28P65x로 전환할 때, 모든 코드를 한 번에 driverlib으로 재작성하는 대신 점진적으로 모듈 단위로 전환하는 경우가 흔하다. 이때 SPRAAA85E 응용 리포트가 두 접근 방식을 함께 사용하는 방법을 안내한다. 다만 본서는 처음부터 새로 설계하는 F28P65x 기준 코드이므로, 일관성과 유지보수성을 위해 Driverlib 단일 표준을 고수한다.

2.7 빌드·플래시·디버그 — XDS110과 JTAG 흐름

2.7.1 XDS110 — LaunchPad에 내장된 디버그 프로브

LAUNCHXL-F28P65X 공식 사용설명서(SPRUJ71)는 보드 구성을 다음과 같이 명시한다.

"The LaunchPad also features two independent BoosterPack XL expansion connectors (80-pins), dedicated 12-bit/16-bit differential ADC header, on-board Controller Area Network (CAN) transceiver supporting both standard CAN (DCAN) and CAN-FD (MCAN), two 5V encoder interface (eQEP) connectors, FSI connector, EtherCAT connector, power-domain isolation, and an on-board XDS110 debug probe."

즉 별도 구매 없이 XDS110 디버그 프로브가 보드 위에 내장되어 있다. 같은 문서는 보드의 LED 배치까지 안내하는데, LED7은 "XDS110 debug probe에 공급되는 3.3V 전원"을 별도로 표시한다 — 이는 XDS110 회로가 메인 MCU 전원 도메인과 분리되어 있어, MCU 쪽에 전원 문제가 있어도 디버그 프로브가 독립적으로 동작할 수 있도록 설계되었음을 보여준다. XDS110 자체의 공식 사양은 다음과 같다.

항목 사양
지원 프로토콜 JTAG(IEEE 1149.1), cJTAG(IEEE 1149.7), SWD/SWO
호스트 연결 USB 2.0 High-Speed (480Mbps)
필요 CCS 버전 v7.0 이상
부가 기능 14핀 보조 포트(UART 풀듀플렉스 + GPIO 4개), 30핀 확장 포트

XDS110은 구형 XDS100 시리즈를 대체하는 제품으로, 하나의 프로브로 JTAG·cJTAG·SWD를 모두 지원하는 최초의 TI 디버그 프로브다. F28P65x는 C28x 기반이므로 JTAG 경로를 사용하며, SWD는 Arm 코어 기반 디바이스(예: Sitara, MSP432)에 해당한다.

2.7.2 보드 위 XDS110 vs 외부 XDS110

LaunchPad나 ControlCARD에는 이미 XDS110이 내장되어 있으므로, 별도의 외부 XDS110을 추가로 연결해도 디버깅 성능상의 이점은 없다. 외부 XDS110이 유용해지는 시점은 자체 제작한 양산 보드를 디버깅할 때다. 자체 보드에는 보통 디버그 프로브가 내장되어 있지 않으므로, 보드 위의 JTAG 헤더에 외부 XDS110을 연결하여 CCS와 통신한다. 즉 LaunchPad로 알고리즘을 검증한 뒤 실제 전력단 보드로 옮겨갈 때, 외부 XDS110 하나를 구비해두면 두 환경을 매끄럽게 오갈 수 있다.

2.7.3 Target Configuration File(.ccxml)

CCS가 디버그 프로브와 타깃 디바이스를 인식하는 방법은 .ccxml이라는 타깃 구성 파일을 통해서다. 이 파일은 "어떤 디버그 프로브(XDS110 등)를 사용하며, 어떤 디바이스(F28P650DK 등)에 연결할 것인지"를 명시한다. 새 CCS 프로젝트를 만들 때 보통 자동으로 생성되지만, 보드를 교체하거나 멀티코어 디버그 설정을 변경할 때는 이 파일을 직접 편집하거나 새로 만들어 Set as Active Target Configuration으로 활성화해야 한다.

2.8 F28P65x 표준 프로젝트 골격

이제까지 다룬 내용 — C2000Ware 폴더 구조, .cmd 파일의 PAGE/MEMORY/SECTIONS, ramfuncs를 통한 RAM 실행, SysConfig, XDS110 디버그 — 을 하나로 엮어, 본서 전체에서 기준으로 삼을 F28P65x 표준 프로젝트 골격을 정리한다.

2.8.1 프로젝트 디렉터리 구조

my_power_converter_project/
├── my_power_converter_project.syscfg   ← SysConfig 설정 (클록, GPIO, ePWM, ADC 핀먹스)
├── F28P65x_FLASH_lnk.cmd               ← 링커스크립트 (Flash 실행 버전)
├── F28P65x_RAM_lnk.cmd                 ← 링커스크립트 (RAM 전용 디버그 버전)
├── main_cpu1.c                         ← CPU1 메인 + ISR
├── sys_init.c / sys_init.h             ← 시스템 클록 초기화
├── epwm_init.c / epwm_init.h           ← ePWM 초기화
├── adc_init.c   / adc_init.h           ← ADC 초기화
├── control/                            ← 제어 알고리즘 (PID, FOC 등, 이후 장에서 추가)
└── targetConfigs/
    └── F28P650DK.ccxml                 ← 타깃 구성 파일

2.8.2 Flash 버전과 RAM 버전 링커스크립트를 함께 두는 이유

실무에서는 .cmd 파일을 보통 두 벌 준비한다. 하나는 코드를 Flash에 영구 저장하고 ISR만 ramfuncs로 RAM에 복사하는 Flash 버전, 다른 하나는 모든 코드를 처음부터 RAM에 올려 빠른 반복 디버깅에 쓰는 RAM 버전이다. CCS 프로젝트의 Build Configuration(Debug/Release 등) 기능을 이용해 두 버전을 전환하면, 알고리즘을 수정하며 빠르게 반복 테스트할 때는 RAM 버전으로 즉시 로드하고, 최종 검증 단계에서는 Flash 버전으로 전원 차단 후 재부팅까지 확인하는 식으로 개발 속도와 신뢰성을 모두 확보할 수 있다.

2.8.3 F28P65x용 .cmd 골격 예시

1장에서 TRM으로 확인한 메모리 분류(LS0~LS9, GS0~GS4, D0-D1/D2-D5)를 반영한 골격은 다음과 같다.

/*==============================================================================
 * 파일   : F28P65x_FLASH_lnk.cmd
 * 목적   : F28P65x 전력 변환 표준 프로젝트 — Flash 실행 + ISR RAM 상주
 * 참조   : TI Linker Command File Primer, TMS320C28x Assembly Language
 *          Tools User's Guide (SPRU513Z)
 *
 * 주의: LS2~LS9, GS0~GS4의 정확한 주소와 Flash 뱅크 크기는 본서 1장에서
 *       확인했듯 TRM이 데이터시트로 위임한 영역이다. 아래 주소는 반드시
 *       C2000Ware에 포함된 F28P65x 공식 .cmd 예제로 최종 대조할 것.
 *============================================================================*/

MEMORY
{
PAGE 0 :   /* 프로그램(코드) 공간 — 관례상 PAGE 0 유지 */
    BEGIN      : origin = 0x080000, length = 0x000002
    RAMM0      : origin = 0x000000, length = 0x000400
    RAMD0      : origin = ____, length = ____   /* CPU1 전용 D0 영역 */
    FLASH_BANK0: origin = ____, length = ____   /* 부팅 시 CPU1 할당 뱅크 */

PAGE 1 :   /* 데이터 공간 — 관례상 PAGE 1 유지 */
    RAMM1      : origin = 0x000400, length = 0x000400
    RAMLS4     : origin = 0x008000, length = 0x000800  /* 제어 변수 전용 */
    RAMLS5     : origin = 0x008800, length = 0x000800  /* ADC 버퍼 전용 */
    RAMGS0     : origin = ____, length = ____           /* CPU1↔CPU2 공유 */
}

SECTIONS
{
    /* 일반 코드: Flash에 저장, 그대로 Flash에서 실행 */
    .text       : > FLASH_BANK0,  PAGE = 0

    /* ISR 코드: Flash에 저장(LOAD), 부팅 시 RAM으로 복사 후 실행(RUN) */
    .TI.ramfuncs : LOAD = FLASH_BANK0,
                   RUN  = RAMD0,
                   LOAD_START(_RamfuncsLoadStart),
                   LOAD_END(_RamfuncsLoadEnd),
                   RUN_START(_RamfuncsRunStart),
                   PAGE = 0

    /* 전역 변수 */
    .ebss       : > RAMM1,   PAGE = 1
    .cinit      : > FLASH_BANK0, PAGE = 0

    /* 제어 루프 전용 데이터 — LS4에 고정 배치 */
    ctrl_vars   : > RAMLS4,  PAGE = 1
    adc_buffer  : > RAMLS5,  PAGE = 1

    /* CPU1↔CPU2 공유 데이터 — GS0에 배치 */
    shared_data : > RAMGS0,  PAGE = 1

    .stack      : > RAMM1,   PAGE = 1
}

이 골격에서 의도적으로 ____로 비워둔 부분은, 1장 1.9절에서 명시했듯 LS2~LS9·GS0~GS4의 정확한 주소가 TRM이 아닌 데이터시트 또는 C2000Ware 공식 .cmd 예제 파일에서 와야 하기 때문이다. 임의의 추정값을 채워 넣는 대신, 실제 빌드 전 C2000Ware의 driverlib/f28p65x/examples/cpu1/.../ccs/에 포함된 검증된 .cmd 파일과 반드시 대조해야 한다는 점을 본서는 일관되게 강조한다.

2.8.4 main() 진입 전 복사 코드

ramfuncs 섹션이 .cmd 파일에서 LOAD/RUN으로 분리 정의되었다면, 실제 복사는 다음과 같이 main() 최상단에서 명시적으로 수행한다.

extern uint16_t RamfuncsLoadStart, RamfuncsLoadEnd, RamfuncsRunStart;

void main(void)
{
    /* Flash에 저장된 ISR 코드를 RAM으로 복사 — .cmd가 정의한 지도를
     * 실제로 수행하는 단계. 이 호출이 없으면 ramfuncs는 Flash에
     * 머무른 채로 실행되어, 1장에서 계산한 5ns(RAM) 대신 Flash
     * 접근시간(RWAIT 기반)이 그대로 적용된다.                      */
    memcpy(&RamfuncsRunStart, &RamfuncsLoadStart,
           (size_t)(&RamfuncsLoadEnd - &RamfuncsLoadStart));

    Device_init();
    /* 이하 1장 1.9.4절의 초기화 절차 계속 */
}

 

2.9 장 요약

본 장에서는 F28P65x 하드웨어 지식(1장)을 실제 펌웨어로 옮기는 전체 도구 체인을 다루었다.

  • C2000Ware는 driverlib(권장)과 bitfield(호환용) 두 갈래로 레지스터 접근을 제공하며, 폴더 구조가 이 구분을 그대로 반영한다.
  • CCS 프로젝트는 백지에서 시작하기보다 검증된 예제를 Import하는 것이 공식적으로 권장되며, Run→Debug 한 번이 저장·빌드·연결·로드·실행의 6단계를 수행한다.
  • .cmd 링커스크립트의 PAGE 0/1은 1980년대 하버드 아키텍처의 역사적 흔적이며, MEMORY가 땅을 정의하고 SECTIONS가 그 땅에 건물을 배치한다. LOAD/RUN 분리 문법으로 ISR을 Flash에 저장하면서 RAM에서 실행할 수 있지만, 그 복사는 자동이 아니라 애플리케이션 코드가 명시적으로 수행해야 한다.
  • SysConfig는 driverlib 위에서 동작하는 검증된 코드 생성기이며, CLB·DCSM 같은 복잡한 모듈까지 포괄한다.
  • XDS110은 LaunchPad에 내장되어 있으며, 자체 양산 보드로 옮길 때는 외부 XDS110과 .ccxml 타깃 구성이 필요하다.

다음 장에서는 이 골격 위에 1장에서 다룬 ePWM·ADC를 포함한 전력변환 DSP 주변장치 전체(DAC, eQEP, CMPSS, SPI/I2C)를 TRM 직접 검증 방식으로 다룬다.

참고문헌

  1. Texas Instruments, C2000™ Software Guide, Release v1.5, December 2023. 
  2. Texas Instruments, Code Composer Studio 12.8.0 User's Guide — Getting Started.
  3. Texas Instruments, TI Linker Command File Primer. 
  4. Texas Instruments, TMS320C28x Assembly Language Tools User's Guide, SPRU513Z, October 2023. 
  5. Texas Instruments, User's Guide C2000™ F28P65x Series LaunchPad™ Development Kit, SPRUJ71. 
  6. Texas Instruments, TMDSEMU110-U (XDS110) Debug Probe product page. 
  7. Texas Instruments, Application Report SPRAAA85E, DriverLib and Bitfield Implementation Guide. 
  8. Texas Instruments, c2000ware-core-sdk official repository (driverlib source, C2000Ware v26.01.00.00).