http://events.linuxfoundation.org/sites/events/files/slides/praesentation.pdf

Posted by 쵸코케키

mmap 구현 방식 2가지

ldd 많이 참조함, 옅은 지식으로 쓴 내용이라 틀릴 수 있음

mmap의 역할?

kernel 공간 혹은 하드웨어 장치에 있는 메모리를 복사하는 과정 없이 user 공간에게 제공
rw가 엄청나게 발생하는 경우에 꽤나 의미있는 성능향상을 보여줄 수 있다
c 어플리케이션에서 mmap 함수를 호출하면 device driver의 mmap 함수가 호출된다
그 함수를 구현하는게 목표


어플리케이션 영역

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
          int fd, off_t offset);
int munmap(void *addr, size_t length);

자세한 설명은 man 페이지 참조
flag는 계속 늘어나는데 MAP_HUGETLB 같이 huge page를 쓸 때 필요한 옵션도 있다
그냥 일반적인 flag로는 MAP_SHARED(다른 프로세스랑 같이 공유) 가 있겠다
prot는 page의 권한에 관한 내용이다
페이지가 읽힐 예정인지 쓰거나 실행될련지 등등


mmap 구현

요약 : 아래의 함수들을 잘 조합해서 사용하면 된다

io_remap_pfn_range()
remap_pfn_range()

아니면 fault 를 처리하는 방식으로 구현해도 ok

vm_insert_page()
vm_insert_pfn()

fault 처리에 주로 사용된다

원형

include/linux/fs.h
struct file_operation {
    ...
    int (*mmap) (struct file *, struct vm_area_struct *);
}

remap_pfn_range 사용

remap_pfn_range - remap kernel memory to userspace
system RAM을 맵핑하고 싶을 때 사용
참고 : io_remap_pfn_range은 장치의 I/O memory 즉 phys_addr_t로 표현되는 놈을 매핑하고 싶을 때 사용
nommu가 아닌이상 동일

내가 파악한 바로는(틀릴 수 있지만 ㅠㅜ)

  1. 유저에서 mmap을 호출하면 (사용할 수 있는 커널 메모리 쫌만 주세요. 대신 제가 알아볼 수 있는 주소로요)
  2. 위의 요청 정보가 vma 형태로 driver로 들어오고(메모리 정보)
  3. 인제 개발자는 가지고 있던(미리 할당해두었던) 메로리를 vma랑 붙여주는데
  4. 그 때 사용하는 도구가 remap_pfn_range 인가보다

이 함수는 userspace의 addr ~ addr+size 범위의 virtual address를 위한 page table을 만든다
이 함수는 pfn<<PAGE_SHIFT ~ (pfn<<PAGE_SHIFT) + size 까지 해당하는 물리 메모리 영역에 영향을 준다

include/linux/pfn.h
#define PFN_PHYS(x)     ((phys_addr_t)(x) << PAGE_SHIFT)
#define PHYS_PFN(x)     ((unsigned long)((x) >> PAGE_SHIFT))

int remap_pfn_range(struct vm_area_struct *vma,  
                    unsigned long addr, 
                    unsigned long pfn,
                    unsigned long size, 
                    pgprot_t prot)

vma : 매핑될 virtual memory area - 변태가 아닌 이상 mmap 호출될 때 vma 인자가 같이 딸려오므로 그대로 쓰면 된다. 하지만 변태라면?!
addr : target user address to start at
pfn : page frame number of kernel memory
보통 physical address >> PAGE_SHIFT 한 값이다
이 physical address에 addr(target user address, virtual address)가 매핑되게 된다

size : 바이트 단위의 매핑될 영역 크기. 보통 vma->vm_end - vma->vm_start
prot : page protection flag, 보통 vma->vm_page_prot 값을 사용한다

이 함수를 호출할 때 흔히 다음과 같이 한다(아래 참조)

예시 : drivers/uio/uio.c

static const struct file_operations uio_fops = {
    ...
    .mmap = uio_mmap,
}

static int uio_map(struct file *filep, struct vm_area_struct *vma)
{
    ...
    return uio_mmap_physical(vma);
}

static int uio_mmap_physical(struct vm_area_struct *vma)
{
    ...
    return remap_pfn_range(vma,
                           vma->vm_start,
                           mem->addr >> PAGE_SHIFT,    //미리 할당해두었던 메모리 
                           vma->vm_end - vma->vm_start,
                           vma->vm_page_prot);
}

조금 형태가 다르긴 한데 이런 것도 있다
drivers/vfio/pci/vfio_pci.c

static int vfio_pci_mmap(void *device_data, struct vm_area_struct *vma)
{
    ...


    pgoff = vma->vm_pgoff &
            ((1U << (VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT)) - 1);    //???

    u64 phys_len = pci_resource_len(pdev, index);    
    req_start = pgoff << PAGE_SHIFT;
    req_len = vma->vm_end - vma->vm_start;

    if(phys_len < PAGE_SIZE || req_start + req_len > phys_len)    //크기 검사
        return -EINVAL;

    vma->vm_pgoff = (pci_resource_start(pdev, index) >> PAGE_SHIFT) + pgoff;

    return remap_pfn_range(vma, vma->start, vma->vm_pgoff,
                            req_len, vma->vm_page_prot);
    //어차피 remap_pfn_range를 호출하면 vma->vm_pgoff = pfn 이 된다
}

아니면 이런 애들도 있다
sound/core/pcm_native.c

int snd_pcm_lib_default_mmap(...)
{
    area->vm_flags |= VM_DONTEXPAND | VM_DONTDUMP;
#ifdef CONFIG_GENERIC_ALLOCATOR
    if (substream->dma_buffer.dev.type == SNDRV_DMA_TYPE_DEV_IRAM) {
            area->vm_page_prot = pgprot_writecombine(area->vm_page_prot);
            return remap_pfn_range(area, area->vm_start,
                            substream->dma_buffer.addr >> PAGE_SHIFT,
                            area->vm_end - area->vm_start, area->vm_page_prot);
    }    
#endif /* CONFIG_GENERIC_ALLOCATOR */
#ifndef CONFIG_X86 /* for avoiding warnings arch/x86/mm/pat.c */
    if (!substream->ops->page &&
        substream->dma_buffer.dev.type == SNDRV_DMA_TYPE_DEV)
            return dma_mmap_coherent(substream->dma_buffer.dev.dev,
                                     area,
                                     substream->runtime->dma_area,
                                     substream->runtime->dma_addr,
                                     area->vm_end - area->vm_start);
#endif /* CONFIG_X86 */
    /* mmap with fault handler */
    area->vm_ops = &snd_pcm_vm_ops_data_fault;
    return 0;
}

기타 다른 예시
drivers/infiniband/hw/nes/nes_verbs.c

static int nes_mmap(struct ...)
{
    ...
    remap_pfn_range( ...
    virt_to_phys >> PAGE_SHIFT ...)
    ...
}

.pgprot

잘 몰라서 생략
대략 cache 쓸껀지 말껀지 관한 내용인데
pgprot_noncached, pgprot_writecombine, pgprot_writethrough
등이 있다


기타

drivers/media/platform/omap/omap_vout.c
OMAP 24xx camera controller 코드

처음 매몰이 할당 코드

virt_addr = omap_vout_alloc_buffer(vout->buffer_size, &phy_addr);
vout->buf_virt_addr[i] = virt_addr;
vout->buf_phy_addr[i] = phy_addr;

unsigned long omap_vout_alloc_buffer(u32 buf_size, u32 *phys_addr)
{
    u32 order, size;
    unsigned long virt_addr, addr;

    size = PAGE_ALIGN(buf_size);    //참고로 가로px * 세로px * bpp(bits per pixel)
    order = get_order(size);
    virt_addr = __get_free_pages(GFP_KERNEL, order);
    addr = virt_addr;

    *phys_addr = (u32) virt_to_phys((void *) virt_addr);
    return virt_addr;
}

mmap 코드

vm_operations_struct omap_vout_vm_ops
    .open = vout->mmap_count++; 역할 수행(mmap )
    .close =  vout->mmap_count--; 역할 수행

mmap 호출한다고 해서 mmap open까지 같이 호출되지는 않는다
놀랍게도 r/w 둘 다 open을 호출하지 않는다
open은 process가 fork할 때 불려진다
그리고 vma에 대한 새로운 reference를 생성한다

static int omap_vout_mmap(struct file *file, struct vm_area_struct *vma)
{
    unsigned long start = vma->vm_start;
    unsigned long size = vma->vm_end - vm->vm_start;
    ...
    void* pos = (void *)vout->buf_virt_addr[i];            //kernel logical address(virtual address)
    vma->vm_pgoff = virt_to_phys((void *) pos) >> PAGE_SHIFT;    //physical
    vma->vm_ops = &omap_vout_vm_ops;
    ...

    while(size > 0) {
        unsigned long pfn;
        pfn = virt_to_phys((void *) pos) >> PAGE_SHIFT;        //물리 주소 획득
        if(remap_pfn_range(vma, start, pfn, PAGE_SIZE, PAGE_SHARED))
            return -EAGAIN;
        start += PAGE_SIZE;
        pos += PAGE_SIZE;
        size -= PAGE_SIZE;
    }

    vout->mmap_count++;        
}

remap_pfn_range에 size를 한 번에 넣는게 아니라 PAGE_SIZE로 나눠 하는 이유?
pixel size가 크다고 해도 어차피 kmalloc 한계인 MAX_ORDER(2^ 10 ~11, 1024) * PAGE_SIZE(4096B) = 4MB or 8MB 이내 범위로 메모리가 할당되어 있다
왜 이짓을 하냐면 메모리가 cma같은게 아닌이상 크게 할당이 안되고 PAGE_SIZE 크기로 할당해서 일듯?
vma 유저 메몰희랑 pfn 물리 매몰의랑

추가 참고 : http://www.cs.fsu.edu/~baker/devices/notes/ch15.html#(34)

그 외 fault 직접 관리

vm_opeartions_struct 의 fault 함수를 직접 구현해야 함
결론적으로 vmf->page = page 를 해주어 메모리를 제공하면 끝
제공해줄 page는 어디서 얻냐고?
여러가지 구현 방법이 있는데 get_zeroed_page 같은 함수로 미리 얻어놓거나
필요할 때마다 획득하거나 등등

예시 : sound/usb/usx2y/usX2Yhwdep.c, sound/core/memalloc.c
mmap이 호출되면 __get_free_pages 로 shared memory 호출하고
fault가 호출되면 아래의 함수가 호출된다

static int snd_us428ctls_vm_fault(struct vm_area_struct *area, 
                                  struct vm_fault *vmf)
{
    ...
    offset = vmf->pgoff << PAGE_SHIFT;        //physical to kernel logical
    vaddr = (char*)((struct usX2Ydev *)area->vm_private_data)->us428ctls_sharedmem + offset;    
    //미리 얻어놨던 메모리 조금씩 제공
    page = virt_to_page(vaddr);
    vmf->page = page;
    get_page(page);                // 나 이 페이지 쓴다!
}

혹은 다른 예시 - dma 같은게 필요 없는 경우
drivers/uio/uio.c

static int uio_vma_fault(struct vm_area_struct *vma, struct vm_fault *vmf)
{
    page = vmalloc_to_page(addr);
    get_page(page);
    vmf->page = page;
}

drivers/staging/android/ion/ion.c

static int ion_vm_fault(staruct vm_area_struct *vma, struct vm_fault *vmf)
{
    ...
    vm_insert_pfn(vma, (unsigned long)vmf->virtual_address, pfn);
    ...
}

그 외 dma common mmap

include/linux/dma-mapping.h

struct dma_map_ops {
    ...
    int (*mmap)(struct device *, struct vm_area_struct *,
                    void *, dma_addr_t, size_t, struct dma_attrs *attrs);
}

drivers/base/dma-mapping.c 참고
int dma_common_mmap(struct device *dev, struct vm_area_struct *vma,
                    void *cpu_addr, dma_addr_t dma_addr, size_t size)
{
        int ret = -ENXIO;
#if defined(CONFIG_MMU) && !defined(CONFIG_ARCH_NO_COHERENT_DMA_MMAP)
        unsigned long user_count = (vma->vm_end - vma->vm_start) >> PAGE_SHIFT;
        unsigned long count = PAGE_ALIGN(size) >> PAGE_SHIFT;
        unsigned long pfn = page_to_pfn(virt_to_page(cpu_addr));
        unsigned long off = vma->vm_pgoff;

        vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
                            include/linux/mm.h 

        if (dma_mmap_from_coherent(dev, vma, cpu_addr, size, &ret))
                return ret;

        if (off < count && user_count <= (count - off)) {
                ret = remap_pfn_range(vma, vma->vm_start,
                                      pfn + off,
                                      user_count << PAGE_SHIFT,
                                      vma->vm_page_prot);
        }   
#endif  /* CONFIG_MMU && !CONFIG_ARCH_NO_COHERENT_DMA_MMAP */

        return ret;
}

http://anster.egloos.com/m/2141358ㅁ

'devel > 개념' 카테고리의 다른 글

gpio pull down switch 연결  (0) 2016.11.16
도요타 캠리 급발진 버그 분석 보고서  (0) 2016.11.10
stm32f4 절전모드  (0) 2016.08.10
__GFP_COMP - compound page  (0) 2016.07.26
ld linker script 예시  (0) 2016.07.25
Posted by 쵸코케키
carry = carry & 0xff + ((carry & 0xff00)>>8);

-> 이렇게 하면 오류난다

carry = (carry & 0xff) + ((carry & 0xff00)>>8);

-> 이렇게 먼저 괄호로 묶어줘야 오류 안 발생

Posted by 쵸코케키

예제


원본 출처

http://www.mikrocontroller.net/attachment/143228/stm32_flash.ld




/*

*****************************************************************************

**

**  File        : stm32_flash.ld

**

**  Abstract    : Linker script for STM32F207 Device with

**                1MByte FLASH, 128KByte SRAM

**

**                Set heap size, stack size and stack location according

**                to application requirements.

**

**                Set memory bank area and size if external memory is used.

**

**  Target      : STMicroelectronics STM32

**

**  Environment : Eclipse with CDT plugin and GCC compiler from CodeSourcery

**

** Author           Date        Comment

**~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

**    T.Kamenicky      24/06/11    Upravil kod stazen z netu

** T.Kamenicky 13/10/11 Predelal na STM32F2

** T.Kamenicky 29/11/11    Pridal podporu externi SRAM

**

*****************************************************************************

*/


/* Entry Point */

/* This define entry point for linker, this address is used for CPU too as absolute first function */

ENTRY(Reset_Handler)


/* Highest address of the user mode stack, _estack is end of stack */

_estack = 0x20020000;    /* end of 128K RAM */


/* Generate a link error if heap and stack don't fit into RAM */

/* Heap is zero because we use own malloc implementation. The amount of heap is calculated 

 * from free space after compilation, see last section for RAM area */

_Min_Heap_Size = 0x0;      /* required amount of heap  */


/* This stack is used for main function. If FreeRTOS is used this stack is used by OS itself. */

/* The size of stack for tasks is defined in freertos config file and when creating the task. */

_Min_Stack_Size = 0x600; /* required amount of stack */


/* Specify the memory areas */

MEMORY

{

  FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 1022K  /* This is main program area */

  FACTORY_VAR(rx) : ORIGIN = 0x080FF800, LENGTH = 2K  /* This area is used for factory values, such as MAC address, ID...etc */

  RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 128K        /* This is RAM area */

  FSMC_SRAM (rx)  : ORIGIN = 0x64000000, LENGTH = 2048K       /* This is useful for external memory, like thru FSMC peripheral */

}


/* Define output sections */

SECTIONS

{

   /* The startup code goes first into FLASH */

  .isr_vector :

  {

    . = ALIGN(4);

    KEEP(*(.isr_vector)) /* Startup code */

    . = ALIGN(4);

  } >FLASH


  /* The program code and other data goes into FLASH */

  .text :

    . = ALIGN(4);

  {

    *(.text)           /* .text sections (code) */

    *(.text*)          /* .text* sections (code) */

    *(.rodata)         /* .rodata sections (constants, strings, etc.) */

    *(.rodata*)        /* .rodata* sections (constants, strings, etc.) */

    *(.glue_7)         /* glue arm to thumb code */

    *(.glue_7t)        /* glue thumb to arm code */


    KEEP (*(.init))

    KEEP (*(.fini))


     . = ALIGN(4);

    _etext = .;        /* define a global symbols at end of code */

  } >FLASH


  /* .ARM.extab names a section that contains exception unwinding information.  See EHABI for details. */

  /* .ARM.exidx names a section that contains index entries for section unwinding.  See EHABI for details. */

   .ARM.extab   : { *(.ARM.extab* .gnu.linkonce.armextab.*) } >FLASH

    .ARM : {

    __exidx_start = .;

      *(.ARM.exidx*)

      __exidx_end = .;

    } >FLASH


  /* Is used for compiler information, which is located only in elf file */

  .ARM.attributes 0 : { *(.ARM.attributes) }


  /* Those sections is used for initializing dynamic objects used in C++ */

 .preinit_array     :

  {

    PROVIDE_HIDDEN (__preinit_array_start = .);

    KEEP (*(.preinit_array*))

    PROVIDE_HIDDEN (__preinit_array_end = .);

  } >FLASH

  .init_array :

  {

    PROVIDE_HIDDEN (__init_array_start = .);

    KEEP (*(SORT(.init_array.*)))

    KEEP (*(.init_array*))

    PROVIDE_HIDDEN (__init_array_end = .);

  } >FLASH

  .fini_array :

  {

    PROVIDE_HIDDEN (__fini_array_start = .);

    KEEP (*(.fini_array*))

    KEEP (*(SORT(.fini_array.*)))

    PROVIDE_HIDDEN (__fini_array_end = .);

  } >FLASH


  /* used by the startup as memory location to initialize data */

  /* from this point the content of flash is copied to ram */

  _sidata = .;


  /* Initialized data sections goes into RAM, load LMA copy after code */

  .data : AT ( _sidata )

  {

    . = ALIGN(4);

    _sdata = .;        /* create a global symbol at data start */

    *(.data)           /* .data sections */

    *(.data*)          /* .data* sections */


    . = ALIGN(4);

    _edata = .;       /* define a global symbol at data end */

  } >RAM


  _SRAM_data = _sidata + _edata - _sdata;


  /* Second part of inicialized section goes into SRAM conected thru FSMC */

  /* FSMC_SRAM section, code must be located here explicitly            */

  /* unsigned long int __attribute__ ((section (".sram_data"))) my_initialized_number = 0xAABBCCDD; */

  .fsmc_sram_data : AT ( _SRAM_data )

  {

  . = ALIGN(4);

    _iSRAM_data = .;        

    *(.sram_data)      

    *(.sram_data*)

    

    . = ALIGN(4);

    _iSRAM_data_end = .;   

  } >FSMC_SRAM

  

  .fsmc_sram_bss : AT ( _iSRAM_data_end )

  {

    _iSRAM_null_data = .;       

    *(.sram_bss)

    *(.sram_bss*)

    

    . = ALIGN(4);

    _iSRAM_null_data_end = .;

  } >FSMC_SRAM

  

  .fsmc_sram_perma : AT ( _iSRAM_null_data_end )

  {

    _iSRAM_perma_data = .;       

    *(.sram_perma)

    *(.sram_perma*)

    

    . = ALIGN(4);

    _iSRAM_perma_data_end = .;

  } >FSMC_SRAM

  

   /* Uninitialized data section */

  . = ALIGN(4);

  

  .bss : AT ( _edata )

  {

    /* This is used by the startup in order to initialize the .bss secion */

    _sbss = .;         /* define a global symbol at bss start */

    __bss_start__ = _sbss;

    *(.bss)

    *(.bss*)

    *(COMMON)


    . = ALIGN(4);

    _ebss = .;         /* define a global symbol at bss end */

    __bss_end__ = _ebss;

  } >RAM

  

  /* Those numbers are used for SRAM init */

  PROVIDE ( _linker_sram_start = _iSRAM_data );

  PROVIDE ( _linker_sram_data_end = _iSRAM_data_end );

  PROVIDE ( _linker_sram_end = _iSRAM_null_data_end );

  PROVIDE ( _linker_sram_inits = _SRAM_data );

  

  /* Those numbers are used for malloc and sbrk functions */

  PROVIDE ( _linker_memory_start = _sdata );

  PROVIDE ( _linker_heap_start = _ebss );

  PROVIDE ( _linker_heap_end = (_estack - _Min_Stack_Size) );

  PROVIDE ( _linker_memory_end = _estack );

  PROVIDE ( _end = _ebss );


   /* User_heap_stack section, used to check that there is enough RAM left */

  ._user_heap_stack : AT ( _linker_heap_end )

  {

    . = ALIGN(4);

    . = . + _Min_Heap_Size;

    . = . + _Min_Stack_Size;

    . = ALIGN(4);

  } >RAM


 /* FLASH memory area for factory default values                          */

  /* Example of usage: extern int foo(void) __attribute__ ((section (".fvtext"))); */

  /* unsigned long int my_factory_number_changed_in_production = 0xAABBCCDD __attribute__ ((section (".fvrodata"))); */ 

  .factory_values :

  {

    . = ALIGN(4);

    *(.fvtext)        /* .fvtext sections (code) */

    *(.fvtext*)       /* .fvtext* sections (code)  */

    *(.fvrodata)      /* read-only data (constants) */

    *(.fvrodata*)

    . = ALIGN(4);

  } >FACTORY_VAR


  /* Remove information from the standard libraries */

  /DISCARD/ :

  {

    libc.a ( * )

    libm.a ( * )

    libgcc.a ( * )

  }

}





'devel > man & example' 카테고리의 다른 글

man page 비슷한거  (0) 2016.11.30
gcc inline assembly  (0) 2016.08.11
arm stm32f4xx software interrupt handler 만들기  (0) 2016.08.09
arm assembly byte swap 예제 분석  (0) 2016.04.13
dev file  (0) 2015.08.04
Posted by 쵸코케키

arm gcc 기준

아주 좋은 참고 링크

http://www.joinc.co.kr/w/Site/Embedded/Documents/LinuxKernelStudyForSystemenginer/app3.basic.html

https://wiki.kldp.org/wiki.php/DocbookSgml/GCC_Inline_Assembly-KLDP


작성해주신 고마운 분들에게 감사를



cmsis 라이브러리 예제 파일들

cmsis_gcc.h

io.h

그 외 linux kernel /arch/arm/include/asm/*

arch/x86/include/asm/*

들이 있다


__attribute__((always_inline)) static inline uint32_t test_return(void)
{
    uint32_t    ret;

    __asm volatile (
        "MOV %0, #41 \n"
        : "=r" (ret)                    //return value
    );

    return ret;
}

uint32_t test_return(void)
{
    uint32_t ret;

    ret = 41;

    return ret;
}
__attribute__((always_inline)) static inline uint32_t test_arg_return(uint32_t arg)
{
    uint32_t    ret;

    __asm volatile (
        "MOV %0, %1 \n"
        : "=r" (ret)                    //write only, return value
        : "r"    (arg)
    );

    return ret;
}

uint32_t test_arg_return(uint32_t arg)
{
    uint32_t ret;

    ret = arg;

    return ret;
}
__attribute__((always_inline)) static inline uint32_t test_arg_return(uint32_t arg, uint8_t *addr)
{
    uint32_t    ret;

#if 0
    //이런식으로 r3 레지스터를 직접 지정할 수 있다, 그리고 인자도 여러개 쓸 수 있다    
    __asm volatile (
            "LDRB    r3, %2 \n"
            "MOV %0, r3 \n"
            : "=r" (ret)                    //return value?"
            : "r" (value), "Q" (*addr)
    );

#else
    //포인터에서 값을 읽어 변수에 대입할 수도 있다
    uint32_t    tmp;

    __asm volatile (
            "LDREXB    %1, %3 \n"
            "MOV %0, %1 \n"
            : "=r" (ret), "+r" (tmp)                    //return value?"
            : "r" (value), "Q" (*addr)
    );
#endif


    return ret;
}


uint32_t test_arg_return(uint32_t arg, uint8_t *addr)
{
    uint32_t ret;

#if 0
    ret = *addr;
#else
    uint32_t tmp;

    tmp = *addr;
    ret = tmp;
#endif

    return ret;
}
__attribute__((always_inline)) static inline uint32_t core_test(uint8_t value, volatile uint8_t *addr)
{
    uint32_t    ret;
#if 1
    __asm volatile (
            "LDRB r3, %2 \n"
            "MOV %0, r3 "
            : "=r" (ret)                    //return value?"
            : "r" ((uint32_t) value), "Q" (*addr)
    );
#else
    //uint32_t    tmp;

    __asm volatile (
            "STREXB %0, %2, %1"
            : "=r" (ret), "=Q" (*addr)
            : "r" ((uint32_t) value));
#endif

    return ret;
}


Posted by 쵸코케키

2016. 8. 10. 13:43 devel/개념

stm32f4 절전모드

STM32F4xx 시리즈의 sleep, stop mode에 대해



STM32F4xx block diagram


 stm32f41x_block_diagram

시리즈 마다 다르겠지만 대충 이런 식으로 생겼다

일단은 넘어가자




#Sleep Mode

Sleep Mode는 아래 함수로 쉽게 진입이 가능한데

HAL_PWR_EnterSLEEPMode(PWR_LOWPOWERREGULATOR_ON, PWR_SLEEPENTRY_WFI);

Wakeup 조건이 모든 인터럽트기 때문에 sleep으로 들어가기 전에 SysTick 관련 interrupt도 꺼야 한다

안 그러면 잠자리에 눕자마자 일어나는 불상사가......

SysTick 때문에 바로 깨어날 경우 아래와 같이 Tick을 잠시 중단 시키면 된다

 

HAL_SuspendTick();

HAL_PWR_EnterSLEEPMode(PWR_LOWPOWERREGULATOR_ON, PWR_SLEEPENTRY_WFI);

HAL_ResumeTick();

 

1번 인자로 PWR_MAINREGULATOR_ON를 이용해도 좋다 그런데 MR이 뭔지 모르겠다

2번째 인자로 PWR_SLEEPENTRY_WFE를 사용해도 된다

여기서 Event가 뭐인고 하니 EXTIexception 등등

자세한 내용은 D022708, 48p Wakeup from WFE 를 참고 바란다

 

 

# 1.2v domain - DM00127514, 112p

core, memory, digital peripherals

 

# STOP MODE - DM00127514, 113p, 120p

1.2v domain, PLLs, HSI, HSE RC oscillator disabled

Internal SRAM, register contents are preserved

SRAM 내용 유지(SRAM에 관한 상세 내용은 아래에), 레지스터 내용 유지

그리고 각종 oscillator domain 관련은 block diagram을 확인 하여 영향도를 체크하자

 

# HSI, HSE - DM00127514, 135p

상세 내역은 pdf 참조

일단은 AHB에 연결된 아해들이 정상 동작 않겠구나 생각해두자

 

그건 그렇고 stop mode에도 2가지 모드가 있다

이하 STOP MODE 해석 안 하는 게 더 나을 꺼 같아서 생략

 

Normal mode

the 1.2 V domain is preserved in nominal leakage mode.

It is the default mode when the main regulator (MR) or the low-power regulator (LPR) is enabled.

- HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_SLEEPENTRY_WFI);

 

Under drive mode

the 1.2 V domain is preserved in reduced leakage mode.

This mode is only available when the main regulator or the lowpower regulator is in low voltage mode

- HAL_PWREx_EnterUnderDriveSTOPMode(PWR_LOWPOWERREGULATOR_UNDERDRIVE_ON, PWR_SLEEPENTRY_WFI);

 

Under drive mode가 전기 더 덜 먹는다는 이야기

대신 wakeup time이 더 소모 된다.

          

 

#SRAM?

2.3.1 Embedded SRAM, 68p, 71p 참조

MCU 내부에 있는 램. 용량 매우 작다

linker script 작성에 따라서 다르긴 하지만 external RAM 없다는 가정하에

일반적으로 흔히 우리가 생각하는 heap stack이 살아 숨쉬는 RAM이라고 보면 된다

 

STOP mode에서 Under drive Mode 로 진입하면 더욱 저전력 가능

대신 wakeup time이 더 소모

wakeup time의 딜레이를 사용자가 줘야 하는 게 아니라 그냥 좀 더 걸린다 정도로 이해하면 될 듯

그래도 머 ns 단위로 전부 깨어나지 않을까

각각 딜레이 보면 < Xns 이런 식으로 되어있고 정확한 값은 없음

아래 Table 20. Stop operating modes 참고



'devel > 개념' 카테고리의 다른 글

도요타 캠리 급발진 버그 분석 보고서  (0) 2016.11.10
mmap 구현 - 작성중  (0) 2016.08.11
__GFP_COMP - compound page  (0) 2016.07.26
ld linker script 예시  (0) 2016.07.25
컴퓨터 프로그래밍 전반적인 지식 기초 소양  (0) 2016.07.14
Posted by 쵸코케키

소프트 인터럽트 핸들러

software interrupt

SVCall을 사용해서 구현


#0. 구현 부터 시작

- a. linker script 확인

section 정의에 interrupt vector 가 잘 정의 되어있는지 체크

/* Sections */

SECTIONS

{

/* The startup code into ROM memory */

.isr_vector :

{

. = ALIGN(4);

KEEP(*(.isr_vector)) /* Startup code */

. = ALIGN(4);

} >ROM

...

	/* Sections */
	SECTIONS
	{
		/* The startup code into ROM memory */
		.isr_vector :
		{
		. = ALIGN(4);
		KEEP(*(.isr_vector)) /* Startup code */
		. = ALIGN(4);
		} >ROM

- b. interrupt vector 잘 선언되어 있는지 확인

Drivers/CMSIS/ST/STM32xxxx/Source/Templates/gcc/startup_stm32xxxxxx.s

  .section  .isr_vector,"a",%progbits

.type  g_pfnVectors, %object

.size  g_pfnVectors, .-g_pfnVectors 


g_pfnVectors:

.word  _estack

.word  Reset_Handler

.word  NMI_Handler

... 생략 ...

.word  0

.word  SVC_Handler

Vector Table :D022708, 39p 표 참조

@@

offset이 역순으로 나와있는 것에 주의

SVC_Handler가 정확한 위치에 잘 선언되어 있다

- c. SVC_Handler 구현

stm32f4xx_it.h 에 void SVC_Handler(void); 선언

stm32f4xx_it.c 에 void SVC_Handler(void) 정의

이 파일을 보면 각종 interrupt/Exception handler 들이 정의되어 있다

인제 SVC 명령이 호출되면 이 핸들러가 실행이 된다~!!

궁금하면 핸들러에 led 점멸이나 uart 메세지 보내는 것을 사용해서 시험이 가능하다

Exception 발생하면 core 하드웨어가 자동으로 stack에 일부 register들을 백업한다

그 후 exception handler가 실행된다

그와 동시에 EXEC_RETURN 값이 LR에 기록된다

EXEC_RETURN는 Exception이전에 어떤 mode 상태에서 돌고 있다가 handler로 들어왔는지 알 수 있는데

적당히 모드 보고 MSP를 참조하든 PSP를 참조하든 알아서 해라라는 소리

이하의 내용은 'd. svc 호출' 항목을 참조한 뒤 보면 좋다

stack에는 r0-r3, r12, LR, PC, xPSR 이 들어가 있다

r0에는 우리가 필요한 void* data 가 들어가 있다

svc와 함께 전달되는 imm 8bit 숫자는 r1에서 읽어도 되고 다른 방법으로 추출 해도 된다

일단 필요한게 r0 인자값이랑 SVC와 같이 넘어온 imm값

이 두가지를 알기 위해 r0가 잠들어 있는 stack pointer가 필요하다

그런데 sp를 알기 위해선 arm core가 thread mode에서 넘어왔는지 process mode에서 넘어왔는지 구분을 해야 한다

(간단하게 설명하면 os 유/무 mode 구분) 

위에서 이걸 EXEC_RETURN으로 알 수 있다는 것을 안다

참고 : http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0203ik/BABFGEFG.html

참고 : ITE 구문 http://trace32.com/wiki/index.php/If-Then


아래의 코드를 보면 이해가 될꺼다 

msp - 0xFFFF FFF9, 9(1001)

psp - 0xFFFF FFFD, D(1101)

그래서 TST로 LR을 4(0100) 와 비교한다

void SVC_Handler(void)

{

__asm volatile(

"TST lr, #4 \n" //if(LR & 0x4) { Z=1 } else { Z=0 } update C, N flag

"ITE EQ \n" //if then else(ITE) if(Z==1 /*EQ*/) { MRSEQ } else { MRSNE }

"MRSEQ r0, MSP \n" //conditional execution (Z==1) - for thread mode

"MRSNE r0, PSP \n" //conditional execution (Z==0) - for process mode

"B SVC_Handler_main"

);

}


	void SVC_Handler(void)
	{
		__asm volatile(
				"TST lr, #4 \n"			//if(LR & 0x4) { Z=1 } else { Z=0 } update C, N flag
				"ITE EQ \n"			//if then else(ITE) if(Z==1 /*EQ*/) { MRSEQ } else { MRSNE }
				"MRSEQ r0, MSP \n"		//conditional execution (Z==1) - for thread mode
				"MRSNE r0, PSP \n"		//conditional execution (Z==0) - for process mode
				"B SVC_Handler_main"
		);
	}

process mode 자체를 안 쓴다면 아예 그냥 MRS r0, MSP랑 B svc_main_handler 구문만 남겨도 ok

SVC_Handler_main 구현은 뒷쪽에 이어서

- d. SVC 호출 : DDI0403EB, 455p 

SVC<c><q> #<imm8>

cq는 일단 생략

imm8 - immediate 8bit 즉 8bit 숫자

gcc inline assembly 기준

__asm volatile (

"SVC #2"

);

이런식으로 호출 가능, handler 쪽에서는 숫자2를 같이 건네 받음

인자로 imm 값을 건네고 싶을 때 다음과 같이 구현가능

__attribute__((always_inline)) static inline void invoke_exception(uint8_t value)

{

__asm volatile (

"SVC %0"

:

: "i" (value) //imm

);

}

만약 인자 1개를 추가로 같이 보내고 싶다고 가정하면 형태가 많이 변경된다

exception이 발생하면 하드웨어적으로 아래의 register들을 stack에 백업한다

@@ D022708, 42p

그리고 함수를 호출하면 인자 4개 까지는 r0 - r3 레지스터에 들어간다

예를 들면 void test(int a, unsigned char b, char* c, void* d)

r0 = a, r1 = b, r2 = c, r3 = d

이렇게 들어간채로 함수가 불리는데 이게 바로 calling convention

Parameter Passing, AAPCS(Arm Architecture Procedure Call Standard) 18p 

그래서 SVC 호출과 함께 맘대로 인자를 보내고 싶다면 함수내에서 SVC 호출을 하면 된다

뭔소린고 하니

#define svc(code) asm volatile ("svc %[immediate]"::[immediate] "i" (code))

#define SVC_TYPE1 0x01

#define SVC_TYPE2 0x02

__attribute__ ((noinline)) int invoke_exception(void *data, uint8_t immnum)

{

switch(immnum)

{

case SVC_TYPE1:

svc(SVC_TYPE1);

break;

case SVC_TYPE2:

svc(SVC_TYPE2);

break;

default:

return -1;

}


return 0;

}

아름다운 구현 방법이 있다면 좋겠는데 이 이상의 방법을 찾지 못하겠다

참고

https://falstaff.agner.ch/2013/02/18/cortex-m3-supervisor-call-svc-using-gcc/

같은 고민을 하는 분들

http://stackoverflow.com/questions/11377453/using-gcc-inline-assembly-with-instructions-that-take-immediate-values

inline mode는 함수를 풀어버리기 때문에 함수를 통해 인자를 받아야 하는 우리는 __attribute__((noinline))을 꼭 선언해줘야 한다

invoke_exception 함수를 호출하면 r0에 void* data 가 들어가게 되고

이 후 svc 호출로 r0는 stack에 들어가게 된다

svc와 함께 전달되는 imm 8bit 숫자는 r1에서 읽어도 되고 다른 방법으로 추출 해도 된다

호출은 일단 했으니 이 후의 내용은 '이전 c. SVC_handler' 항목을 참조

- e. SVC_Handler_main 상세 구현

SVC_Handler를 통해 r0에 MSP(stack pointer)를 얻어왔다

그리고 B SVC_Handler_main을 통해 다시 함수로 이동해 왔다

void SVC_Handler_main(void *svc_args)

{

unsigned int svc_number;


if(svc_args == NULL)

return ;


svc_number = ((char *) ((unsigned int*)svc_args)[6])[-2]; //imm

void* arg = (void*) ((unsigned int*)svc_args)[0];

}


현재 상태

r0 == void *svc_args == MSP

우리가 필요한 건 MSP에 저장되어 있는 r0(void *data)

그러므로 

void *arg = (void *) ((unsigned int*) svc_args)[0];

unsigned int*로 type casting 하는 이유는 register 크기가 32bit 니까

만약에 r1을 얻고 싶다면 ((unsigned int*) svc_args)[1]; 하면 된다

svc_number 즉 svc 랑 같이 보낸 imm 추출은 좀 더 많이 복잡하다

svc 명령 중 하위 8bit가 imm 이다

일단 svc 명령이 어떻게 내려졌는지 그걸 가져오고 그 다음 하위 8bit 를 구해서 imm을 획득하자

svc 명령이 불려지면 핸들러가 실행되기 전에 레지스터들이 백업 되는데 PC값에는 return address 

다시 말해 svc 명령 다음 실행 위치가 들어가 있다

그럼 PC - 2 하면 svc 명령 위치가 나오겠네?

PC = ((unsigned int *) svc_args)[6]

SVC 명령 위치 = ((unsigned int*) svc_args)[6] - 2

SVC 명령 = *(uint16_t *) (((unsigned int*) svc_args)[6] - 2)

svc 명령이 16bit 니까 (uint16_t *)로 type casting

이 값을 16진수로 뽑으면 0xDF## 이 된다

imm = SVC 명령 & 0xff

실제 연산하기 위해서는 16bit 포인터로 변환

unsigned int* pc_address = ((unsigned int*) svc_args)[6];

uint16_t* svc_address = pc_address - 2;

uint16_t svc = *(uint16_t *) svc_address; 

uint8_t svc_number = svc & 0xff;

이렇게 해도 되는데 이걸 짧게 줄이면

svc_number = ((char *) ((unsigned int*) svc_args)[6])[-2];

svc_number = ((char*) pc_address)[-2]

대충 메모리 구조 보면

[   ...    pc   ]

[  svc  \  imm  ]

\       \       \-->  svc 시작 주소값 == pc 시작 주소값 - sizeof(uint8_t)x2 == &((char*) pc_address)[-2]

\       \-->  svc 시작 주소값 + sizeof(uint8_t) == svc 주소값 + 1 

\-->  pc 주소값 == svc 끝 주소값 == svc 시작 주소값 + sizeof(uint8_t)x2



가능하면 쉽게 쓰려고 노력했는데 가독성이 영 꽝인거 같다




기타 관련 개념들


# ARMv7에는 A시리즈도 있고 R 시리즈도 있고 M 시리즈도 있는데 이놈들이 거의 같긴 한데 다른 부분들이 있다

그래서 예제같은거를 대충 가져와서 쓴다고해서 돌아가는게 아니라 정확하게 맞는 시리즈에 적용해야 된다


좋은 예제 리스트

linux kernel source의 

arch/arm/include/asm/io.h 등 기타 여러가지

arch/x86/include/asm/io.h



ARMv7-M Architecture Reference Manual - dmIO403E_B, 569p

Supervisor call(SVCall)

An exception caused explicitly by the SVC instruction. Application software uses the SVC instruction

to make a call to an underlying operating system. This is called a Supervisor call. The SVC instruction

enables the application to issue a Supervisor call that requires privileged access to the system and

executes in program order with respect to the application. ARMv7-M also supports an

interrupt-driven Supervisor-calling mechanism



579p

Interrupt Control and State Register - ICSR, 655p


# The Vector Table - 581p

The vector table contains the initialization value for the stack pointer, and the entry point addresses of each

exception handler

word offset in table - Exception using that Exception Number


그래서 linker descriptor 파일 보면

section에 isr_vector 정의했고 startup_stm32f479xx.s 파일 보면

.section  .isr_vector,"a",%progbits

  .type  g_pfnVectors, %object

  .size  g_pfnVectors, .-g_pfnVectors 

  

  g_pfnVectors:

  .word  _estack

  .word  Reset_Handler

  .word  BusFault_Handler

   ... 생략

  .word  SVC_Handler

  .word  DebugMon_Handler

  .word  0

  .word  PendSV_Handler

  .word  SysTick_Handler

  

  할당은 이렇게 해놓았고

  stm32f4xx_it.c 여기에 void BusFault_Handler(void); 함수 정의 

  stm32f4xx_it.h 여기에도 선언 추가

  


  http://egloos.zum.com/recipes/v/5037342

  http://www.thewireframecommunity.com/writing-a-basic-multitasking-os-for-arm-cortex-m3-processor

  http://stackoverflow.com/questions/24162109/arm-assembly-code-and-svc-numbering

  https://falstaff.agner.ch/2013/02/18/cortex-m3-supervisor-call-svc-using-gcc/

  

  ARMv7m pdf, 455p SVC 상세 이용법

  svc<c><q> #<imm>

  <c><q> - 

  imm - immediate constant

  

#Exception entry behavior 587p




이해하기 앞서 선행 정보 간단 요약

엄청 기니까 대충 넘겼다가 나중에 봐도 ok


Processor Mode : D022708 16p

thread mode

기본 사용 모드

handler mode

exception 처리할 때 사용된다. 처리 끝나면 다시 thread mode로 복귀


STACK

full descending stack

아래 방향으로 증가한다고 생각하면 편할듯

새 아이템을 스택에 넣으면 sp는 되려 감소한다

그리고 새 메모리 위치에 아이템을 기록한다

stack에는 2가지 종류가 있는데 main stack, process stack이 있다

thread mode 에서는 core가 어떤 stack을 쓸지 CONTROL register에 의해 결정할 수 있다

handler mode 에서는 항상 main stack을 사용한다(msp)


SP(R13) - Stack Pointer : D022708 18p

MSP(main stack pointer)

PSP(process stack pointer) - os 있을 때 context switching 용도로 사용

위의 내용들 참고


LR(R14) - Link Register 

subroutine이나 함수 호출, exception 에서 돌아갈 정보를 저장한다


PC(R15) - Program Counter

현재 프로그램 주소를 담고 있음


APSR - Application Program Status Register : 20p

명령어 실행하는 동안 발생한 상태 정보에 대해 나와있음. 표 참조

@@


Exception Priority : 37p, DDI0403EB 584p

SVC의 우선 순위는 SHPRx(System Handler Priority Register) 에서 설정 가능

exception 순위가 낮으면 높은 handler에게 preemption 선점 당할 수 있음

그 와중에 낮은 순위의 exception이 들어오면 pending 된 상태로 대기

 

Exception 종류 및 IRQ와의 관계 - D022708 36-39p

Vector Table


SHPRx - System Handler Priority Register : D022708 323p

exception 들의 priority 를 설정 가능

아마도 작을 수록 높은 우선 순위를 갖는 것 같다

(명시 되어있는 것은 못 찾겠으나 IRQ의 경우도 그렇고 37p의 Table. 16을 보면 -3 reset이 highest priority다)

 

Exception Entry - Exception이 발동하면? : 41p

Stack Frame Layout : 42p

@@ 


Exception return - EXEC_RETURN : D022708, 43p 

EXEC_RETURN은 exception 진입할 때 LR에 들어가는 값이다

@@



'devel > man & example' 카테고리의 다른 글

gcc __attribute__ section 지정으로 직접 변수 기록될 영역 고정하기  (0) 2016.08.11
gcc inline assembly  (0) 2016.08.11
arm assembly byte swap 예제 분석  (0) 2016.04.13
dev file  (0) 2015.08.04
ioctl  (0) 2012.07.26
Posted by 쵸코케키


https://lwn.net/Articles/619514/


A compound page is simply a grouping of two or more physically contiguous pages into a unit that can, in many ways, be treated as a single, larger page. They are most commonly used to create huge pages, used within hugetlbfs or thetransparent huge pages subsystem



'devel > 개념' 카테고리의 다른 글

mmap 구현 - 작성중  (0) 2016.08.11
stm32f4 절전모드  (0) 2016.08.10
ld linker script 예시  (0) 2016.07.25
컴퓨터 프로그래밍 전반적인 지식 기초 소양  (0) 2016.07.14
64bit linux kernel memory layout  (1) 2016.05.31
Posted by 쵸코케키

출처

http://www.hertaville.com/a-sample-linker-script.html




/******************************************************************************
    * This linker file was developed by Hussam Al-Hertani. Please use freely as
    * long as you leave this header in place. The author is not responsible for any
    * damage or liability that this file might cause.
******************************************************************************/

   /* Entry Point */
   ENTRY(Reset_Handler)

   /* Specify the memory areas */
   MEMORY
   {
     FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 0x10000 /*64K*/
     RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 0x02000 /*8K*/
   }

   /* define stack size and heap size here */
   stack_size = 1024;
   heap_size = 256;

   /* define beginning and ending of stack */
   _stack_start = ORIGIN(RAM)+LENGTH(RAM);
   _stack_end = _stack_start - stack_size;

   /* Define output sections */
   SECTIONS
   {
     /* The startup code goes first into FLASH */
     .isr_vector :
     {
       . = ALIGN(4);
       KEEP(*(.isr_vector)) /* Startup code */
       . = ALIGN(4);
     } >FLASH

     /* The program code and other data goes into FLASH */
     .text :
     {
       . = ALIGN(4);
       *(.text)           /* .text sections (code) */
       *(.text*)          /* .text* sections (code) */
       *(.rodata)         /* .rodata sections (constants, strings, etc.) */
       *(.rodata*)        /* .rodata* sections (constants, strings, etc.) */
       *(.glue_7)         /* glue arm to thumb code */
       *(.glue_7t)        /* glue thumb to arm code */
       . = ALIGN(4);
       _etext = .;        /* define a global symbols at end of code */
     } >FLASH

      .ARM.extab   : { *(.ARM.extab* .gnu.linkonce.armextab.*) } >FLASH
       .ARM : {
       __exidx_start = .;
         *(.ARM.exidx*)
         __exidx_end = .;
       } >FLASH

     /* used by the startup to initialize data */
     _sidata = .;

     /* Initialized data sections goes into RAM, load LMA copy after code */
     .data : AT ( _sidata )
     {
       . = ALIGN(4);
       _sdata = .;        /* create a global symbol at data start */
       *(.data)           /* .data sections */
       *(.data*)          /* .data* sections */

       . = ALIGN(4);
       _edata = .;        /* define a global symbol at data end */
     } >RAM

     /* Uninitialized data section */
     . = ALIGN(4);
     .bss :
     {
       /*  Used by the startup in order to initialize the .bss secion */
       _sbss = .;         /* define a global symbol at bss start */
       __bss_start__ = _sbss;
       *(.bss)
       *(.bss*)
       *(COMMON)

       . = ALIGN(4);
       _ebss = .;         /* define a global symbol at bss end */
       __bss_end__ = _ebss;
     } >RAM

       . = ALIGN(4);
       .heap :
       {
           _heap_start = .;
           . = . + heap_size;
                   _heap_end = .;
       } > RAM

       /* Remove information from the standard libraries */
       /DISCARD/ :
       {
           libc.a ( * )
           libm.a ( * )
           libgcc.a ( * )
       }

       .ARM.attributes 0 : { *(.ARM.attributes) }
   }


'devel > 개념' 카테고리의 다른 글

stm32f4 절전모드  (0) 2016.08.10
__GFP_COMP - compound page  (0) 2016.07.26
컴퓨터 프로그래밍 전반적인 지식 기초 소양  (0) 2016.07.14
64bit linux kernel memory layout  (1) 2016.05.31
헤더파일 전역변수 중복 오류  (0) 2016.04.21
Posted by 쵸코케키

http://www.edwardbosworth.com/My5155Textbook/MyText5155_AFrontMatter.htm


강추

'devel > 개념' 카테고리의 다른 글

__GFP_COMP - compound page  (0) 2016.07.26
ld linker script 예시  (0) 2016.07.25
64bit linux kernel memory layout  (1) 2016.05.31
헤더파일 전역변수 중복 오류  (0) 2016.04.21
EAS(Energy-Aware Scheduler) 스케쥴러 정리  (0) 2016.03.24
Posted by 쵸코케키
이전버튼 1 2 3 4 5 6 7 8 ··· 15 이전버튼

블로그 이미지
chocokeki
쵸코케키

공지사항

Yesterday
Today
Total

달력

 « |  » 2024.5
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

최근에 올라온 글

최근에 달린 댓글

글 보관함