ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [컴퓨터 구조] AHCI
    공학/컴퓨터구조 2023. 8. 10. 00:34

    글의 참고

    - https://www.intel.com/content/www/us/en/io/serial-ata/serial-ata-ahci-spec-rev1_3.html

    - http://www.usedsite.co.kr/pds/file/SerialATA_Revision_3_0_RC11.pdf

    - https://en.wikipedia.org/wiki/Advanced_Host_Controller_Interface

    - https://wiki.osdev.org/AHCI

    - https://github.com/Stichting-MINIX-Research-Foundation/minix/blob/master/minix/drivers/storage/ahci/ahci.c

    - https://github.com/rajesh5310/SBUnix/blob/master/sys/ahci.c

    - https://github.com/mit-pdos/biscuit/blob/master/biscuit/src/ahci/ahci.go

    - https://www.synopsys.com/dw/dwtb/sata_ahci/sata_ahci.html


    글의 전제

    - 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다. 나도 모르기 때문에 나중에 알아봐야 할 내용이라는 뜻이다.

    - 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다. 그러므로, 밑 줄 처친 글이 이해가 안간다면 링크를 따라서 관련 내용을 공부하자.

    - `글의 참조`에서 빨간색 볼드체로 체크된 링크는 이 글을 작성하면 가장 많이 참조한 링크다.


    글의 내용

    - AHCI

    : AHCI 컨트롤러는 이론적으로 32개의 포트를 가질 수 있다. 그리고 각 포트에는 SATA 드라이브/디바이스가 장착될 수 있다. 

     

     

    : 제일 먼저 볼 부분은 AHCI의 정의인 듯 하다.

    출처 - 출처 - serial-ata-ahci-spec-rev1_3.pdf

     

    : 소프트웨어와 Serial ATA (SATA) devices들이 통신할 수 있게 만드는 하드웨어 메커니즘이라고 정의한다. 뭔지는 알겠다. 그런데, 나는 SATA와 AHCI의 차이를 모르겠더라. 그래서 wikipedia를 참고해서 아래와 같은 내용을 알게됬다.

     

    출처 - https://en.wikipedia.org/wiki/Advanced_Host_Controller_Interface

    : 위에서 얘기하는 건 SATA 컨틀롤러가 3가지 동작 모드를 갖는다고 한다.

    1) IDE Mode 
    2) Standard AHCI Mode
    3) RAID

    : AHCI는 SATA 컨트롤러의 동작 모드중 하나라고 생각하면 된다. 

     

     

    : 참고할 만한 내용도 거의없고 구현 자체도 어렵다는 SATA AHCI에 대해좀 알아보자.

     

    : 먼저 알아야 하는 건 CPU와 AHCI 컨트롤러는 메모리 맵 방식으로 통신한다는 것이다. 즉, 별도의 `in, out` 명렁어는 필요가 없다. 메모리의 특정 번지에 AHCI 컨트롤러와 통신하기 위한 메모리를 할당해놓은 것이다. 

     

    : `10 Transport Layer`에서 FIS 관련 내용이 나온다. 근데 Transport Layer는 그냥 FIS나 마찬가지인 것 같다.

    출처 - SerialATA_Revision_3_0_RC11.pdf

     

     

    : `SerialATA_Revision_3_0_RC11.pdf` 문서에 `10.3 FIS Types`을 보자.

     

    출처 - SerialATA_Revision_3_0_RC11.pdf

     

     

    출처 - SerialATA_Revision_3_0_RC11.pdf

     

    : D2H 몇 가지 필드는 나중에 SATA Device 타입을 구분하기 위해서 알아둘 필요가 있다.

     

    출처 - SerialATA_Revision_3_0_RC11.pdf

    : LBA `Low(7:0), Mid(15:8), High(23:16)`는 알고 있자.

     

     

     

     

    : FIS(Frame Information Structure)의 약자로, host computer와 SATA storage device들이 정보 교환을 위해 사용하는 패 혹 프레임이다. 요놈은 `데이터`라기 보다는 `정보`에 가깝다(약간 헤더의 의미에 가깝다). 

     

    출처 - SerialATA_Revision_3_0_RC11.pdf

     

     

    - 근데 AHCI 컨트롤러는 PCI 버스에 붙어 있다. PCI 버스의 위치중 어디에 붙어있을까? PCI의 BAR[5]의 AHCI Base Address Register가 붙는다. 

     

    - HBA(Host Bus Adapter) region은 host computer와 connected storage devices들이 서로 통신하는 영역이다. 

     

    - 문서는 HBA는 아래와 같이 정의한다.

     

     

    출처 - serial-ata-ahci-spec-rev1_3.pdf

     

     

    - HBA가 silicon이다? 즉, 하드웨어다. 그리고 AHCI specification을 구현한 하드웨어다. 즉, 이 말은 HBA가 AHCI Controller라는 말이다.

     

     

    출처 - serial-ata-ahci-spec-rev1_3.pdf

     

     

    : IA(Intel Architecture) 구조에서의 HBA(AHCI Controller)가 어떻게 배치되는지 보여준다. 눈여겨 볼점은 2가지다

    1) HBA는 결국 PCI 버스에 붙어서 동작하는 놈이다.

    2) SATA devices들은 무조건 HBA에 붙어있다.  

     

    - ABAR(BAR5)

    : AHCI 컨트롤러는 PCI 헤더 타입 중 디바이스 타입으로 기본적으로 BAR 헤더가 6개가 존재한다. 그중에서 AHCI 컨트롤러는 앞에 5개의 BAR(0~4)는 여분으로 남겨놓고 BAR5만 사용한다. AHCI에서는 이 BAR5를 ABAR(AHCI Base Address) 라고 부른다.

    출처 - serial-ata-ahci-spec-rev1_3.pdf

    : ABAR의 정보는 다음과 같다.

     

    : ABAR로 지정된 영역은 충분양의 공간을 할당하고 있어야 한다. 저 영역안에는 3가지 중요한 정보들이 들어가기 때문이다.

    1" 전역 영역
    2" 각 포트 영역
    3" 벤더 영역

    : 벤더 영역을 할당해야 할 경우, 포트 영역에서 마지막 포트가 할당된 영역 다음으로 위치하게 한다. 하위 12비트는 플래그 비트들로 구성되어 있는 것을 볼 수 있다. 위에 [31:13] 이 `Base Address` 라고 되어있는데, 이 주소가 가리키는 영역이 `HBA Memory Registers`가 된다. 바로 아래서 설명한다.

     

    - HBA Memory Registers

    출처 - serial-ata-ahci-spec-rev1_3.pdf

    : HBA 영역은 `CPU`와 `SATA 디바이스`들이 통신하는 영역이다. 위의 레지스터들을 통해 MMIO 방식 PC와 SATA 디바이스들이 통신한다. MMIO 영역답게 HBA 영역에도 2가지 중요한 특징이 있다.

    1) 캐쉬를 허용하지 않는 영역(non-cacheable)
    2) lock access가 허용되지 않는 영역

     

    : HBA 영역은 2개로 나눌 수 있다. 

    1) Global
    2) Port

    : 0x100 아래의 존재하는 영역(0x00 ~ 0xFF)은 Global 영역이라고 부른다. 그 이후에는 각 32개의 포트가 각각 0x80 영역을 차지하고 있다(0x100 ~ 0x10FF). 쉽게 설명하면 각 포트는 개별 SATA Device 라고 생각하면 된다. 예를 들어, 3번 포트를 컨트롤한다는 것은 AHCI Controller의 3번 포트에 연결되어 있는 3번 SATA Device를 컨트롤한다는 것과 동일한 의미이다. 그리고 General 영역(0x00 ~ 0x100) 영역을 컨트롤한다는 것은 모든 SATA Device들에게 공통되는 부분을 컨트롤한다는 의미이다.

     

     

     

    - 그리고 

     

    출처 - serial-ata-ahci-spec-rev1_3.pdf

     

     

    : Port Registers는 굉장히 중요하다. 각 SATA Device들을 컨트롤하기 위해서는 SATA Device가 할당되어 있는 포트 레지스터를 컨트롤 할 주 알아야 한다. 아래의 P`x` 에서 x는 포트 번호(0~31)를 의미한다.

     

     

    출처 - serial-ata-ahci-spec-rev1_3.pdf

     

     

    : Port Registers는 총 128-byte 로 구성되어 있으며, 각 레지스터들에 대해 알아보자.

     

     

    출처 - serial-ata-ahci-spec-rev1_3.pdf

     

     

    : PxCLB는 말 그대로 Command List Base Address를 의미한다. 근데 주소가 32 + 22 개념으로 총 2^54 범위를 표현할 수 있다. 그리고 Command List Base Address에서 1KB 정렬이 되어 있어야 한다고 하는데, 그건 Command List 를 이루는 Command Headers들과 이 헤더들의 개수 때문이다. 아래에서 설명하겠지만, Command Header 하나당 32바이트이고, 이 개수가 최대 32개 까지 존재할 수 있다. 그래서 Command List의 사이즈가 최대 32 * 32로 1KB(1024B)가 되는 것이다.

     

     

    출처 - serial-ata-ahci-spec-rev1_3.pdf

     

     

    - HBA의 메모리 구조

    : HBA는 `메모리 맵` 방식을 사용하고 있으며, Host Computer과 SATA Devices들과 통신하기 위해서는 아래의 구조에 대해 알고 있어야 한다.

    출처 - serial-ata-ahci-spec-rev1_3.pdf

     

     

    : HBA의 메모리 영역은 크게 `Global 영역``Port 영역`으로 나뉜다. 위 그림에서 0x00 ~ 0xFF까지가 Global 영역이다. 그리고 0x100 ~ 0x10FF 까지가 Port 영역이다. Global 영역은 `General Host Control` 영역만 알면되기 때문에 Port 영역 보다는 간단하다, 그래서 나는 먼저 Port 영역부터 알아볼 거다. 

     

    : 위 그림에서 각 포트 영역은 2가지 자료구조로 구성되어 있다. 

    1) Received FIS
    2) Command List(Command Table)

     

     

     

    출처 - serial-ata-ahci-spec-rev1_3.pdf

     

     

    : Port Memory는 2가지 자료구조로 구성되어 있다. 

    : 먼저 `Received FIS`부터 살펴보자.

     

     

    : 우리가 SATA Devices들과 통신하기 위해서는 어떻게 해야할까? 

    출처 - serial-ata-ahci-spec-rev1_3.pdf

    : AHCI 1.3 specification에서는 HBA에 가지는system memory를 통해서 가능하다고 한다. 즉, 메모리 맵으로 통신이 가능하다는 말이다. 각 Port는 `Command List`와 `Received FIS Structure`들을 갖는것에 주목하자. 그리고 host computer는 각 포트를 통해 각 SATA device들과 통신할 수 있다는 것을 기억하자. 

     

     

     

     

    - 초기화(Initialization)

    : 위험한 발언일 수 있지만, 임베디드 분야에서 다비이스를 다룬다는 것은 1가지 데이터와 2가지 알고리즘만 스펙에 맞게 제공하면 기본적인 동작을 하는데 문제는 없는 것 같다. 

    0) 해당 디바이스에 대한 자료구조 만들기 - 데이터
    1) 초기화 - 알고리즘
    2) 읽기 및 쓰기 - 알고리즘

    : 먼저 디바이스의 `초기화`와 `읽기 및 쓰기` 과정이 어떻게 진행되는지 알아야 한다. 그래야 어떻게 자료구조를 구성해야 할지가 정해진다. 

     

    : AHCI는 PCI 디바이스이다. 그럼 일단, PCI부터 알아야 한다.

     " https://yohda.tistory.com/entry/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-PCI

     

    : HBA는 PCI Device이다. 그러므로, HBA도 자신만의 PCI Header를 갖는다.

    출처 - serial-ata-ahci-spec-rev1_3.pdf

     

    : HBA의 PCI Header가 PCI Type 1 Header와 비슷해 보인다.

     

    출처 - PCI 3.0 Revision Specification

     

     

    - 초기화

    : 포트 사용 가능 여부

    " 초기화는 AHCI 컨트롤러의 어떤 포트가 사용 가능한지 부터 검사한다. 처음에 말했다시피 AHCI 컨트롤러는 총 32개의 포트가 있다. 각 포트마다 SATA 드라이브가 연결될 수 있다.


    출처 - serial-ata-ahci-spec-rev1_3.pdf

    " PI(Port Implemented)는 `Generic Host Control`에 있는 레지스터다. 이 포트의 각 비트는 몇 번째 포트가 현재 Enable or Disable 되어있는지 알 수 있다.

     

    : 드라이브 장착 여부

    " AHCI 컨트롤러가 사용 가능하다고 하는 포트를 찾았으면, 해당 포트에 장착된 SATA 드라이브가 있는지 검사해야 한다. 만약, 드라이브가 장착되어 있지 않다면 당연히 장착을 해야 한다. QEMU 같은 경우는, 이미지를 만들어서 `-hda` 혹 `-drive` 같은 옵션을 사용하면 된다.

     

    " 드라이브 장착 여부는 PxSSTS 레지스터의 `Device Detection` 레지스터를 통해서 확인이 가능하다. `Device Detection`값은 0x03 이여야 하고, `Interface Power Management`는 0x01 이여야 한다. `Current Interface Speed`는 0x00 만 아니면 된다. 

    출처 - serial-ata-ahci-spec-rev1_3.pdf

     

    : 시그니처 확인

    " ATA에서는, 특정 명령어 및 리셋 이후에 시그니처값이 어딘가에 써진다. 이 시그니처에는 현재 장착된 드라이브가 어떤 ATA 를 지원하는지 알려준다.

    출처 - SerialATA_Revision_3_0_RC11.pdf

     

    : `COMRESET`, `power-on reset`, `software reset` 후에, SEMB가 현재 장착된 SEP를 인식하면, SEMB가 유니크한 `SEMB-specific signature`를 Command Block Registers를 써놓는다고 한다(SEMB와 SEP는 SATA 스펙에 나와있는데, 현재 시그니처를 확인하는 부분과는 크게 상관이없다). Command Block Registers는 위의 `Error`, `Count(7:0)`, `Count(15:8)` ... `Status` 레지스터들을 말하한다. 이 레지스터들을 `Task File Registers` 라고도 한다.

     


    출처 - serial-ata-ahci-spec-rev1_3.pdf

    : PxSIG 레지스터에 있는 값은 각 포트에서 인식한 디스크 드라이브 시그니처를 의미한다. 이 값은 위에서 말한대로 리셋시에 자동으로 초기화되기 때문에 별도로 `D2H Register FIS`를 명령어를 사용할 필요는 없다. 리셋이 되고 특정 시점이후에 이 레지스터에 위의 값들이 자동으로 써진다. 그러므로, 우리는 리셋시점에 이 레지스터를 읽어서 현재 장착된 디스크 드라이브가 지원하는 프로토콜(인터페이스)를 확인하면 된다. SATA에서는 시그니처가 4가지 정도 존재한다.

    1" 일반 SATA : 0x00000101
      ; 참고 - Serial ATA Revision 3.0 - DHR2: Send_good_status
    2" SATAPI(패킷 커맨드를 지원) : 0xEB140101
      ; 참고 - Serial ATA Revision 3.0 - DHR2: Send_good_status
    3" SEMB(SATA Enclosure Management Bridge) : C33C0101
      ; 참고 - Serial ATA Revision 3.0 - 13.13.4.1 Discovery
    4" Port Multiplier : 0x96690101
       ; 참고 - Serial ATA Revision 3.0 - 13.15.2.2 Software Reset

     

     

    : AHCI Controller를 초기화하기 위해서는 메모리 맵 방식으로 통신하면 된다. 그럼 메모리의 어느 영역을 읽고 써야 AHCI Controller와 통신할 수 있을까? 이 주소는 PCI Header에 써있다.

     

    출처 - serial-ata-ahci-spec-rev1_3.pdf

    : PCI Header에서 `ABAR` 이라고 써있는 지점에 우리가 원하는 AHCI Controller의 Base address가 작성되어 있다. AHCI Controller는 PCI 인터페이스를 사용하기 때문에, 우리는 또 PCI 프로토콜을 해석할 수 있는 뭔가를 작성해야 한다는 소리다.

     

     

     

     

    : 코드로 HBA의 자료구조를 뽑아내기 위해서 아래와 같은 도식도를 만들어 봤다. 

    : 노란색을 PORT 0의 영역, 붉은색이 PORT 1의 영역, ... , 초록색이 PORT 31의 영역이다. Port Memory는 Command List, Reserved FIS로 나뉜다고 말했다. 그리고 Command List안에 총 32개의 Command Headers가 들어가는데, 각 Command Header 하나당 하나의 Command Table을 가라킨다. Command Table의 사이즈는 동적이다. 

     


    - 데이터 읽기

     

    // https://github.com/rajesh5310/SBUnix/blob/master/sys/ahci.c#L49
    int read(HBA_PORT *port, DWORD startl, DWORD starth, DWORD count, QWORD buf)  
    {
        //   buf = KERNBASE + buf;
            port->is = 0xffff;              // Clear pending interrupt bits
           // int spin = 0;           // Spin lock timeout counter
            int slot = find_cmdslot(port);
            if (slot == -1)
                    return 0;
            uint64_t addr = 0;
    //        print("\n clb %x clbu %x", port->clb, port->clbu);
            addr = (((addr | port->clbu) << 32) | port->clb);
            HBA_CMD_HEADER *cmdheader = (HBA_CMD_HEADER*)(KERNBASE + addr);
     
            //HBA_CMD_HEADER *cmdheader = (HBA_CMD_HEADER*)(port->clb);
            cmdheader += slot;
           cmdheader->cfl = sizeof(FIS_REG_H2D)/sizeof(DWORD);     // Command FIS size
            cmdheader->w = 0;               // Read from device
            cmdheader->c = 1;               // Read from device
            cmdheader->p = 1;               // Read from device
            // 8K bytes (16 sectors) per PRDT
            cmdheader->prdtl = (WORD)((count-1)>>4) + 1;    // PRDT entries count
     
            addr=0;
            addr=(((addr | cmdheader->ctbau)<<32)|cmdheader->ctba);
            HBA_CMD_TBL *cmdtbl = (HBA_CMD_TBL*)(KERNBASE + addr);
            
            //memset(cmdtbl, 0, sizeof(HBA_CMD_TBL) + (cmdheader->prdtl-1)*sizeof(HBA_PRDT_ENTRY));
            int i = 0; 
      //      print("[PRDTL][%d]", cmdheader->prdtl);
            // 8K bytes (16 sectors) per PRDT
            for (i=0; i<cmdheader->prdtl-1; i++)
            {
                   cmdtbl->prdt_entry[i].dba = (DWORD)(buf & 0xFFFFFFFF);
                    cmdtbl->prdt_entry[i].dbau = (DWORD)((buf << 32) & 0xFFFFFFFF);
                    cmdtbl->prdt_entry[i].dbc = 8*1024-1;     // 8K bytes
                    cmdtbl->prdt_entry[i].i = 0;
                    buf += 4*1024;  // 4K words
                    count -= 16;    // 16 sectors
           }
            /**If the final Data FIS transfer in a command is for an odd number of 16-bit words, the transmitter�s
    Transport layer is responsible for padding the final Dword of a FIS with zeros. If the HBA receives one
    more word than is indicated in the PRD table due to this padding requirement, the HBA shall not signal
    this as an overflow condition. In addition, if the HBA inserts padding as required in a FIS it is transmitting,
    an overflow error shall not be indicated. The PRD Byte Count field shall be updated based on the
    number of words specified in the PRD table, ignoring any additional padding.**/
            
            // Last entry
    
            cmdtbl->prdt_entry[i].dba = (DWORD)(buf & 0xFFFFFFFF);
            cmdtbl->prdt_entry[i].dbau = (DWORD)((buf << 32) & 0xFFFFFFFF);
            cmdtbl->prdt_entry[i].dbc = count<<9;   // 512 bytes per sector
            cmdtbl->prdt_entry[i].i = 0;
            
    
            // Setup command
            FIS_REG_H2D *cmdfis = (FIS_REG_H2D*)(&cmdtbl->cfis);
     
            cmdfis->fis_type = FIS_TYPE_REG_H2D;
            cmdfis->c = 1;  // Command
            cmdfis->command = ATA_CMD_READ_DMA_EX;
     
            cmdfis->lba0 = (BYTE)startl;
            cmdfis->lba1 = (BYTE)(startl>>8);
            cmdfis->lba2 = (BYTE)(startl>>16);
            cmdfis->device = 1<<6;  // LBA mode
     
            cmdfis->lba3 = (BYTE)(startl>>24);
            cmdfis->lba4 = (BYTE)starth;
            cmdfis->lba5 = (BYTE)(starth>>8);
     
            cmdfis->countl = count & 0xff;
            cmdfis->counth = count>>8;
     
        //    print("[slot]{%d}", slot);
            port->ci = 1;    // Issue command
           // Wait for completion
            while (1)
            {
                    // In some longer duration reads, it may be helpful to spin on the DPS bit 
                    // in the PxIS port field as well (1 << 5)
                    if ((port->ci & (1<<slot)) == 0) 
                            break;
                    if (port->is & HBA_PxIS_TFES)   // Task file error
                    {
                            //print("Read disk error\n");
                            return 0;
                    }
            }
         //   print("\n after while 1"); 
         //   print("\nafter issue : %d" , port->tfd);
            // Check again
            if (port->is & HBA_PxIS_TFES)
            {
                    //print("Read disk error\n");
                    return 0;
            }
            
        //    print("\n[Port ci ][%d]", port->ci);
            int k = 0;
            while(port->ci != 0)
            {
                print("[%d]", k++);
            }
            return 1;
    }
    
    int find_cmdslot(HBA_PORT *port)
    {
        // An empty command slot has its respective bit cleared to �0� in both the PxCI and PxSACT registers.
            // If not set in SACT and CI, the slot is free // Checked
            DWORD slots = (port->sact | port->ci);
            int num_of_slots= (abar->cap & 0x0f00)>>8 ; // Bit 8-12
            int i;
            for (i=0; i<num_of_slots; i++)
            {
                    
                    if ((slots&1) == 0)
                    {
                           // print("\n[command slot is : %d]", i);
                            return i;
                            
                    }       
                    slots >>= 1;
            }
                    print("Cannot find free command list entry\n");
            return -1;
    }

    : 위 코드를 제일 먼저 하는 것이 포트 레지스터의 SACT비트와 CI비트를 확인하는 것이다. 그 이유는 Read 동작의 제일 처음은 일단 Command를 만들어야 하기 때문이다. 아래 내용을 보면 `empty command slot은 PxSACT와 PxCI 비트가 0인 slot이다` 라고 나와있다. 그래서 위의 코드에서 find_cmdsloft이 

     

     

    : 그리고 `Generic Host Control` 메모리 영역에 Host Capabilities 레지스터가 있는데, 여기에 [12:08] 각 포트가 갖는 command slots의 개수가 들어있다. 이 레지스터가 Generic Host Control에 있는 걸로 봐서는 각 포트별 동일한 command slot 개수를 갖는 것으로 보인다. 

     

     

    - Endian-ness

    : 데이터를 아래와 같이 저장하면 8바이트가 서로 바껴서 출력된다. 즉, 리틀 엔디안으로 저장된다. 근데 8바이트 데이터를 저장해서 인지 8바이트 리틀 엔디안으로 처리됬다.

     


    - 인터럽트

    : AHCI는 PCI 디바이스이기 때문에, PCI에서 인터럽트를 설정해야 한다. 

     

     

     

    '공학 > 컴퓨터구조' 카테고리의 다른 글

    [컴퓨터 구조] MMIO  (0) 2023.08.11
    Port-Mapped IO  (1) 2023.08.11
    [컴퓨터 구조] ISA 버스  (0) 2023.08.09
    [컴퓨터 구조] PCI  (1) 2023.08.09
    [컴퓨터 구조] PCI 인터럽트  (0) 2023.08.08
Designed by Tistory.