본문 바로가기
임베디드 개발/펌웨어

TL16C752C, Dual UART (16550 호환 칩)

by eteo 2026. 5. 31.
반응형

 

 

일단 이 칩은 16550 아키텍처를 기반으로 64-Byte FIFO를 탑재한 Dual UART 컨트롤러 칩이다.

 

 

16550이 무엇인가?

1980년대 시절 PC의 Serial Port 구현을 위한 외장 UART 컨트롤러로 널리 쓰이던 칩이 National Semiconductor사에서 출시한 8250, 16450, 16550 시리즈였다.

 

8250, 16450 계열은 데이터 레지스터를 제외한 송수신 버퍼가 따로 없어서 인터럽트 부담이 컸고, 16550에서 16-Byte FIFO가 최초 도입되면서 실사용성이 크게 좋아졌다. 이후 여러 제조사에서 "16550 compatible"을 표방하는 호환 칩이 대량으로 나오며 레지스터 구조가 사실상 표준처럼 굳었다.

 

현재에도 16550 IP가 널리 쓰이고 있고, 리눅스 커널 소스나 U-Boot 소스에도 16550 호환 칩을 다룰 수 있는 드라이버가 존재한다.

 

오늘 살펴볼 TL16C752C 칩도 16550 호환 레지스터를 가지고 있기 때문에 그런 기본 드라이버를 거의 수정 없이 활용할 수 있다. 다만, FIFO 사이즈가 64 Byte로 확대됐고, 흐름제어같은 추가 기능을 위한 확장 레지스터가 더해진 구조이다.

 

 

 

용도

이 칩은 MCU나 DSP에 흔히 붙은 시리얼 인터페이스 방식이 아니라, 8-bit 병렬 버스 인터페이스 방식으로 SoC의 외부 로컬 버스에 연결되어 Memory Mapped I/O 방식으로 제어된다.

 

 

 

기본스펙

  • 전원 : 1.8 V / 2.5 V / 3.3 V / 5 V 지원 (공급 전원에 따라 입력 클럭 한계와 설정 가능한 보드레이트 한가 달라짐)
  • Dual UART (2채널)
  • 각 채널당 64-byte TX/RX FIFO 탑재
  • 48 MHz 클럭 입력시 최대 3 Mbps의 Baudrate 지원
  • Hardware/Software Flow Control 지원
  • Sleep Mode 지원
  • FIFO Trigger Level 설정 기능
  • Prescaler 지원

 

 

 

 

핀아웃 & Block Diagram

 

  • A0~A2 : 주소선, 내부 레지스터 선택 용도
  • D0~D7 : 8-bit 데이터 버스
  • /IOR & /IOW : Read & Write strobe*
    • strobe : 비동기식 데이터 전송에서 데이터 선에 유효한 데이터가 실렸음을 알리기 위해 보내는 타이밍 제어 신호
  • /CSA & /CSB : A or B 채널 선택 신호
  • RXA & TXA, RXB & TXB : 듀얼 채널의 시리얼 Rx & Tx 핀
  • XTAL1 & XTAL2 : 기준 클럭
  • INTA & INTB : 채널별 인터럽트 출력

 

채널 A/B는 송수신 FIFO와 UART 레지스터 뱅크가 각각 독립적이다. 다만, 주소선과 병렬 데이터 버스는 공유하므로 지금 접근하는 대상이 채널 A인지 B인지는 CSA/CSB로 구분한다.

 

그리고 전원 및 리셋핀과 클럭 소스는 채널간 공유되지만, Baudrate 관련 설정(prescaler 및 divisior 값)은 채널별로 독립 설정이 가능하다. 또한 인터럽트 핀도 채널별로 독립적이다.

 

 

 

제어방식

로우레벨에서의 제어 방식을 생각해 봤을 때, 예를 들어 채널 A의 THR 레지스터에 데이터를 쓴다고 가정하면 다음의 과정으로 이루어질 것이다.

 

  • A0~A2 = 000 (THR 주소)
  • CSA = Low (채널 A 선택)
  • CSB = High
  • D0~D7 = 보낼 데이터
  • IOW 펄스

이게 한 번의 Write Cycle이다.

 

 

 

다만, 실제로는 SoC 설계자가 외부 버스가 메모리 맵으로 보이게끔 메모리 컨트롤러를 구성하고 나면, CPU가 특정 주소로 load/store를 수행하는 과정에서 HW가 알아서 A[2:0], CSA/CSB, IOW/IOR 같은 신호를 생성한다.

 

따라서 SW 개발자는 채널별로 주어진 특정 메모리 주소에 값을 읽고 쓰는 것만으로 UART 컨트롤러를 제어할 수 있다.

 

 

 

 

기본 레지스터 

기본적으로 16550 레지스터 맵을 따른다.

 

 

 

 


16550의 레지스터의 특이한 구조 및 주의 사항

위 레지스터 맵에서 특이한 점을 하나 발견할 수 있다. 주소가 000인 레지스터는 RHR, THR, DLL 총 3개이고 옆에는 R, W, RW와 같은 접근 속성이 표시되어있다. 왜 동일한 주소에 여러 레지스터 할당되어 있을까?

 

이 칩은 과거 PC ISA 버스 환경에서 주소 공간이 제한적이던 시절에 설계되었기 때문에, 주소핀 3개만으로 최대한 많은 기능을 구현해야 했다.

 

그래서 동일한 주소라도 읽기/쓰기 접근에 따라 접근하는 레지스터가 달라지는 구조가 나온다. 거기에 더해 특정 레지스터가 특정 값으로 설정된 상황에선 동일한 주소의 동일한 읽기/쓰기 접근이라고 해도 접근 레지스터가 달라지기 때문에 한 주소에 최대 4개 레지스터까지 배치되는 저런 구조가 가능한 것이다.

 

이러한 구조 때문에 제어 시 주의할 점이 있다. 예를 들어, 주소 010은 읽을 때는 IIR, 쓸 때는 FCR이다. (특정 조건이 만족되지 않은 상황을 가정한다.)

 

그런데 만약 com_port->FCR |= 0x03; 같은 Read-Modify-Write 연산을 수행하면, IIR (Interrupt Identification Regieter) 값을 읽어와서 FCR (FIFO Control Register)에 덮어쓰게 되므로 의도와는 전혀 다른 값이 쓰이고 하드웨어가 오동작하는 사태가 발생할 수 있다.

 

따라서, 접근 속성이 Write-Only인 레지스터는 반드시 완성 값 전체를 구성한 뒤 한 번에 Write하는 방식을 사용해야 한다.

 

 

 

 

U-Boot 드라이버 코드

U-Boot 드라이버 코드에서 16550 호환 칩을 어떤식으로 제어하고 있는지 대강 살펴봤다. 다음 코드는 드라이버 소스 원본은 아니고 의미 해석을 위해 간략화한 버전이다.

 

원본 : https://elixir.bootlin.com/u-boot/v2026.01/source/drivers/serial/ns16550.c

 

 

/* 초기화 함수 */
void ns16550_init(struct NS16550 *com_port, int baud_divisor) {
    // 인터럽트 disable
    com_port->ier = 0x00;
    
    // RTS, DTR 신호 enable
    com_port->mcr = 0x03;
    
    // FIFO enable 및 TX/RX FIFO Clear
    com_port->fcr = 0x07;
    
     // Divisor Latch Enable(DLAB) bit Set, DLL/DLH 접근 가능하도록 함
    com_port->lcr = 0x80;
    
    // Baudrate Divisor LSB 설정 
    com_port->dll = baud_divisor & 0xff;
    
    // Baudrate Divisor LSB 설정
    com_port->dlh = (baud_divisor >> 8) & 0xff;
    
    // DLL/DLH 접근 비활성화 및 8N1(8 data bits, No parity, 1 stop bit) 설정
    com_port->lcr = 0x03;
}

 

 

/* 문자 송신 함수 */
void ns16550_putc(struct NS16550 *com_port, char c) {
    // LSR(Line Status Register) 레지스터의 THR(Transmit Holding Register) Empty 비트가 Set 될 때까지 대기
    while (!(com_port->lsr & 0x20));
    
    // THR(Transmit Holding Register) 레지스터에 데이터 씀
    com_port->thr = c;
}

 

 

/* 문자 수신 함수 */
char ns16550_getc(struct NS16550 *com_port) {
    // LSR 레지스터의 DR(Data Ready) 비트를 polling으로 확인하며 대기
    while ((com_port->lsr & 0x01) == 0);

    // RHR(Receiver Holding Register) 레지스터에서 데이터 읽기
    return com_port->rhr;
}

 

 

 

 

그리고 소스를 보다가 인상적인 부분을 발견했다.

 

ns16550.h를 보면 레지스터를 구조체로 선언해 두고, 각 채널의 베이스 어드레스를 디바이스 트리에서 얻은 뒤, 베이스 어드레스를 레지스터 구조체 포인터로 캐스팅해 각 멤버로 주소 오프셋을 접근하는 방식이다. 여기까지는 흔히 볼 수 있는 패턴이다.

 

그런데 흥미로운 점은, 문자열 치환 매크로로 구조체 멤버의 alias를 만들어 같은 오프셋을 서로 다른이름으로 접근할 수 있도록 한 부분이다.

 

#define UART_REG(x) u32 x

struct ns16550 {
	UART_REG(rbr);		/* 0 */
	UART_REG(ier);		/* 1 */
	UART_REG(fcr);		/* 2 */
	UART_REG(lcr);		/* 3 */
	UART_REG(mcr);		/* 4 */
	UART_REG(lsr);		/* 5 */
	UART_REG(msr);		/* 6 */
	UART_REG(spr);		/* 7 */
};

#define thr rbr
#define iir fcr
#define dll rbr
#define dlm ier

 

 

예를 들어, 구조체에는 fcr 멤버만 선언되어 있지만, iir이 전처리 단계에서 fcr로 치환되므로 아래의 두 표현은 결국,

com_port->iir
com_port->fcr

 

다음과 같이 동일한 주소를 가르키게된다.

(uint8_t *)(UART_CH_BASE + 0x02)

 

이 칩은 하나의 주소가 정해진 조건에 따라 다른 레지스터로 동작하게끔 설계되어 있기 때문에, 이를 코드에서 자연스럽게 구현한 것이다.

 

모든 멤버 변수가 하나의 메모리 공간을 공유하는 공용체(Union)로도 이와 같은 구조를 구현할 수 있을테지만, 위 방식은 코드가 훨씬 간결해진다는 장점이 있다.

반응형