1) Mutexes

뮤텍스는 공유 자원에 대한 접근을 제어하기 위한 동기화 도구, 뮤텍스는 한 번에 하나의 태스크만 자원에 접근할 수 있도록 보장하는 역할을 한다.

뮤텍스는 공유 리소스에 대한 액세스를 제어하는 데 사용되는 특수한 유형의 바이너리 세마포어이다

바이너리 세마포어와 다른 점 : 뮤텍스는 우선순위 역전 현상을 예방해줄 메커니즘을 포함

xSemaphoreHandle = xSemaphoreCreateMutex();            // 뮤텍스 생성

if (xSemaphoreTake(xMutex, portMAX_DELAY)) { // 뮤텍스 획득
    // 자원 사용
    xSemaphoreGive(xMutex);                  //뮤텍스 반환
}
  • 뮤텍스는 우선순위 역전 문제 방지를 위해 우선순위 상속 메커니즘을 제공

  • xSemaphoreCreateMutex(); 에서 NULL 반환 시 힙 영역 부족하다는 뜻

  • 뮤텍스를 태스크 A에서 사용중일 경우, 뮤텍스에 접근한 태스크 B는 Blocked state로 전환

  • 뮤텍스가 반환되면 B의 Blocked state 종료

2) Binary Semaphores

0 과 1로만 이뤄진 semaphore다. Critical section에 대해 p연산을 수행하면 semaphore가 0이 되어서 다른 task의 접근을 막는다.

접근을 요청한 task는 blocked 상태가 되며 들어갈 때 까지 기다린다.

  • take 함수에서 기다리는 시간을 설정
    • xSemaphoreGive();
    • xSemaphoreTake();

3) Priority Inheritance (우선순위 상속)

일시적으로 나의 우선순위(높은)를 주어서 Inversion 문제를 해결

해결 방법

  • 리소스를 가진 LP의 우선순위를 HP 수준으로 일시적으로 끌어올려 줌
  • 따라서 MP가 HP보다 먼저 실행되지 않게 한다.

4) Deadlock (or Deadly Embrace)

둘 이상의 태스크가 서로 가진 자원을 기다리며 무한 대기 상태에 빠지는 것

  • Task A는 Mutex X를 점유하고 Mutex Y를 기다림
  • Task B는 Mutex Y를 점유하고 Mutex X를 기다림
  • → 서로 양보하지 않아서 stuck

해결 방법

  • 리소스를 항상 같은 순서로 획득하도록 설계
  • 타임아웃 설정
  • 재귀 뮤텍스(Recursive Mutex) 사용 (아래 설명)

5) Recursive Mutexes (재귀적 뮤텍스)

xSemaphoreHandle mutex = xSemaphoreCreateRecursiveMutex();

xSemaphoreTakeRecursive(mutex);  
xSemaphoreTakeRecursive(mutex); // 같은 태스크이기 때문에 가능 (재귀)

xSemaphoreGiveRecursive(mutex);  
xSemaphoreGiveRecursive(mutex); // 두번 반환해야 완전 해제
  • 하나의 태스크가 같은 뮤텍스를 여러 번 획득 할 수 있도록 허용
  • 일반 Mutex는 한 번만 획득 가능 → 중첩 호출 시 문제 발생
  • 재귀 뮤텍스는 획득 횟수를 기억하고, 같은 수만큼 반환해야 진짜 반환됨

6) Mutexes and Task Scheduling

  • Mutex는 공유 자원 보호뿐 아니라 태스크 스케줄링에 간접적 영향도 줌
  • 예: 높은 우선순위 태스크가 뮤텍스를 기다리면 블로킹 상태로 빠지고, 그 사이 낮은 우선순위 태스크가 실행될 수 있음

→ 뮤텍스는 단순한 동기화 수단이 아니라, RTOS 스케줄링의 흐름까지 영향을 줌

7) Gatekeeper Tasks

  • 특정 리소스에 대한 단독 소유권을 갖는 태스크
  • 다른 태스크는 해당 리소스를 직접 접근하지 않고, Gatekeeper를 통해 간접 접근
  • Example 21 : gatekeeper 태스크를 사용하도록 vPrintString() 재구현(tick hook 함수를 이용해 200틱마다 메세지 전송)
    • Gatekeeper는 큐(xPrintQueue) 를 통해 문자열 포인터를 수신
    • 출력 요청 태스크는 printf 직접 호출 대신, 메시지를 큐에 전송
    • Gatekeeper는 메시지를 수신하면 printf로 출력, 이후 다시 대기
    • 대부분 시간은 xQueueReceive() 에서 Blocked 상태로 대기
    • Tick Hook
    • vApplicationTickHook() 사용 → 200틱마다 문자열 전송
    • 사용 전 FreeRTOSConfig.h에서 configUSE_TICK_HOOK = 1 설정 필요

Tick Hook 주의사항

    • Tick ISR 컨텍스트에서 실행됨
    • 코드 짧게 작성, 스택 사용 최소화
    • FromISR 계열 함수만 사용 가능
    • pxHigherPriorityTaskWoken → 사용하지 않으면 NULL로 설정 가능
   { 
    char *pcMessageToPrint; 
  for( ;; ) 
  { 
    xQueueReceive( xPrintQueue, &pcMessageToPrint, portMAX_DELAY ); 

    printf( "%s", pcMessageToPrint ); 
    fflush( stdout ); 
  } 
} 
static void prvPrintTask( void *pvParameters ) 
{ 
  int iIndexToString; 
  const TickType_t xMaxBlockTimeTicks = 0x20; 

  iIndexToString = ( int ) pvParameters; 
  for( ;; ) 
  { 

    xQueueSendToBack( xPrintQueue, &( pcStringsToPrint[ iIndexToString ] ), 0 ); 

    vTaskDelay( ( rand() % xMaxBlockTimeTicks ) ); 
  } 
} 
void vApplicationTickHook( void ) 
{ 
  static int iCount = 0; 

  iCount++; 
  if( iCount >= 200 ) 
  { 
    xQueueSendToFrontFromISR( xPrintQueue, &( pcStringsToPrint[ 2 ] ), NULL ); 
    iCount = 0; 
  } 
} 
static char *pcStringsToPrint[] = 
{ 
  "Task 1 ****************************************************\r\n", 
  "Task 2 ----------------------------------------------------\r\n", 
  "Message printed from the tick hook interrupt ##############\r\n" 
}; 

QueueHandle_t xPrintQueue; 

int main( void ) 
{ 

  xPrintQueue = xQueueCreate( 5, sizeof( char * ) ); 

  if( xPrintQueue != NULL ) 
  { 
    xTaskCreate( prvPrintTask, "Print1", 1000, ( void * ) 0, 1, NULL ); 
    xTaskCreate( prvPrintTask, "Print2", 1000, ( void * ) 1, 2, NULL ); 

    xTaskCreate( prvStdioGatekeeperTask, "Gatekeeper", 1000, NULL, 0, NULL ); 

    vTaskStartScheduler(); 
  } 

  for( ;; ); 
}
  • 멀티태스킹 시스템에서 여러 Task 나 ISR이 공유 자원에 동시에 접근
    → Data Corruption이 발생 가능
    1. Accessing Peripherals (주변장치 접근)
      • 여러 Task가 동시에 UART, SPI, I2C에 접근
      • 출력 충돌, 데이터 손상, 통신 오류
    2. Read-Modify-Write Operations (읽기-수정-쓰기 연산)
      • count++, total += value 같은 연산은 다단계 연산
      • context switching 발생 시 값이 꼬임
    3. Non-atomic Access to Variables (비원자적 변수 접근)
      • 32비트 변수나 구조체 등 멀티바이트 자료를 여러 Task가 접근
      • 중간 값이 읽히거나 쓰이면 예상과 다른 결과
    4. Function Reentrancy (재진입성 문제)
      • printf, rand 같은 함수는 thread-safe 하지 않음
      • 여러 Task가 동시에 호출하면 내부 버퍼 충돌
  • freeRTOS document에서 multitasking system의 오류 발생 포인트를 정리해보았다.
  • FreeRTOS 시스템에서는 Task 뿐만 아니라 ISR도 공유자원에 접근 가능
    → 위험성 증가 (ISR이 Task를 가로채 실행될 수 있음)

    ⇒ Mutual Exclusion 기법으로 관리되어야 함
  • mutual exclusion : 한 시점에 단 하나의 Task 또는 ISR만 자원에 접근 가능하게 제어
    • 자원 보호 및 데이터 무결성 유지
  • FreeRTOS 시스템에서 다양한 mutual exclusion 도구를 제공

→ 가장 간단하고 안전한 방법은 공유 자원이 없도록 설계하는 것
→ 하나의 자원은 하나의 Task만 접근!

다음은 위 4 방법 중 Critical Section을 만드는 두 가지 방법이다.

Critical section

인터럽트 응답 시간에 영향을 끼치므로, Critical Section은 짧아야 한다.

  • taskENTER_CRITICAL / taskEXIT_CRITICAL 함수
    • taskENTER_CRITICAL
    • critical section 시작
    • taskEXIT_CRITICAL
    • critical section 종료

두 함수 모두 입력 인자와 반환 값이 없다.

함수명이 FromISR로 끝나지 않으며, ISR로 호출되는 것만은 아니다.

⇒ ISR로부터 접근이 가능하기 때문에, 완전한 보호가 되지 않는다.

  • taskENTER_CRITICAL_FROM_ISR / taskEXIT_CRITICAL_FROM_ISR 함수

앞선 함수에서 interrupt safe 버전이다.

  • taskENTER_CRITICAL_FROM_ISR
  • taskEXIT_CRITICAL_FROM_ISR→ ENTER 함수의 반환 값과 일치해야만 한다.
  • 입력 인자로서 ENTER의 반환 값을 받는다.

Suspending (or Locking) the Scheduler

Scheduler에 lock을 걸어 선점을 막는다.

  • vTaskSuspendAll / vTaskResumeAll 함수
    • vTaskSuspendAll
      • return 값
      • 스케줄러가 suspend 된 동안 요청된 context switch는 보류 상태로 남는데, 스케줄러가 재개될 때 실행된다. 보류된 context switch가 스케줄러 재개 전에 수행되면 pdTRUE를 반환하고, 그렇지 않으면 pdFALSE를 반환한다.
    • 스케줄러를 suspend 한다.
    • vTaskResumeAll
    • 스케줄러 Suspend 해제시킨다

라즈베리파이 4 (BCM2711)에서 메모리 맵 I/O로 GPIO를 직접 제어해보는 예시 일부를 요약해서 필기해보았다.

메모리 맵 I/O (Memory-Mapped I/O)

개념:
CPU의 주소 공간에 메모리뿐 아니라 I/O 장치 레지스터(GPIO, UART, SPI 등)를 같은 주소 공간에 배치하는 방식.
→ CPU는 메모리에 접근하는 것과 동일한 방식으로 장치를 제어할 수 있음.
전체 메모리 공간을 메모리와 I/O 장치로 나눠서 사용

버스 요약

Address bus: CPU는 값을 가져오기 위해 접근할 메모리/장치 주소를 어드레스 버스를 통해 설정

Data bus: 데이터버스를 통해 값을 읽어옴.

Control bus: 읽기/쓰기 동작 제어 (입출력 방향 설정)

어드레스 버스를 통해 GPIO와 같은 H/W에 접근, 메모리 특정 주소를 사용해 GPIO에 값을 보내거나, 들어오는 값을 읽는다.

mmap()으로 라즈베리파이 led 제어해보기

bcm2711의 메모리 주소 맵핑

  1. BCM2711의 GPIO주소

GPIO base 주소: 0x7E200000 (I/O base + 0x200000)

  1. GPIO 주요 레지스터


GPFSELn (GPIO Function Select) : 각 GPIO 핀의 입출력 모드 설정 (핀당 3비트)

GPSETn : GPIO 출력 High (1로 설정)

GPCLRn : GPIO 출력 Low (0으로 클리어)

GPLEVn : 현재 입력 레벨 확인

  1. GPIO 방향 설정 (GPFSEL)

핀 하나당 3비트씩 할당하여 I/O 방향을 설정.

입력 : 000, 출력 : 001
특정 핀 번호를 GPFSEL 레지스터에서 찾는 방법:

총 0~5번으로 6개의 GPFSEL레지스터. gpio핀은 58개,
핀번호 / 10 → 사용할 GPFSEL 레지스터 선택

핀번호 % 10 → 그 레지스터 내에서의 비트 위치

mmap()

Memory-mapped I/O: I/O 장치를 메모리 주소 공간에 포함시켜 접근
Raspberry Pi 4 (BCM2711): GPIO base는 0xFE200000
직접 제어: /dev/mem → mmap()으로 매핑 → GPIO 레지스터 접근 → 토글
이제 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);

(사물인터넷을 위한 리눅스 프로그래밍 with 라즈베리파이)

mmap() : fd가 가리키는 객체를 offset의 값을 기준으로 length 만큼 메모리에 매핑하도록 커널에 요청하면 매핑된 시작 주소가 반환된다.

munmap() : mmap()함수와 연결된 메모리 해제.

주요 코드 흐름

/dev/mem → mmap()으로 매핑 → GPIO 레지스터 접근 → 토글

  1. /dev/mem 열기커널이 관리하는 물리 메모리 공간에 접근하기 위해 /dev/mem 사용
  2. mem_fd = open("/dev/mem", O_RDWR | O_SYNC);
  3. mmap()으로 물리 주소를 가상 주소에 매핑GPIO 레지스터 주소 영역을 프로세스의 가상주소에 매핑
  4. gpio_map = mmap(NULL, GPIO_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, mem_fd, GPIO_BASE);
  5. GPIO 포인터 설정이제 gpio[]배열처럼 접근하면 실제 레지스터에 읽기, 쓰기 가능하다.
  6. gpio= (volatile unsigned *)gpio_map;
  1. GPIO를 출력으로 설정 하는예
GPIO\_OUT(gno); 

매크로로 해당 핀을 출력 모드(001)로 설정.

  1. LED 토글

    GPIO_SET(gno) //high
    sleep(1);
    GPIO_CLR(gno) //Low
    sleep(1);

    1초 간격으로 GPIO핀을 on/off하는 예.

  2. 리소스 해제!

    munmap(gpio_map, GPIO_SIZE);
    close(mem_fd);

- 문자 디바이스 파일 character device file

사용자 공간 애플리케이션이 /dev/ 아래 디바이스 파일을 통해

커널에 구현된 문자 디바이스 드라이버와 소통할 수 있게 하는 인터페이스.

  • 문자 디바이스는 데이터를 byte 스트림단위로 읽고 쓴다.
  • 키보드,프린터,마우스,콘솔,직렬 포트(UART)등.
  • file_operations 구조체의 read,write함수 등을 구현해 데이터 송수신 처리.

- 블록 디바이스 파일 Block device file

/dev/ 아래 존재, 사용자 공간 애플리케이션이 커널 모드의 블록 디바이스 드라이버와 소통하도록 하는 인터페이스.

  • 데이터를 블록 단위(512B~수KB)로 읽고 씀.→랜덤 액세스 가능.
  • HDD,SSD, eMMC,SD,USB저장장치,CD/DVD 등
  • block_device_operations 구조체와 I/O스케줄러를 이용해 읽기/쓰기 요청을 큐에 넣고 처리한다.

문자 디바이스 파일

디바이스의 주 번호와 부 번호


**디바이스 번호의 자료형**
dev_t : 주 번호 12비트/ 부 번호 20 비트 . 32비트의 크기

#define MAJOR(dev) ((unsigned int) ((dev) >> 20))
메이저 번호:  어떤 드라이버가 이 디바이스를 처리하는지 식별
#define MINOR(dev) ((unsigned int) ((dev) & 0xfffff))
마이너 번호: 동일 드라이버 내 어떤 장치인지(tty,tty0b

typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;

문자 디바이스 파일 번호 동적 할당과 등록

dev_t sk_dev;
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
alloc_chrdev_region(&my_dev, 0, 1, "my_dev");

*dev : 할당된 디바이스 번호를 저장할 포인터.(my_dev에 저장)
baseminor:할당 시작할 부 번호(minor) (0부터 시작) 
count: 필요한 부 번호 개수 (디바이스 수)                
name: /proc/devices에 등록될 이름 (정보용)       

문자 디바이스 번호 등록 (번호 공간 예약)

dev_t devno = MKDEV(GPIO_MAJOR,GPIO_MINOR);
미리 정한 메이저/마이너 번호를 dev_t로 생성
register_chrdev_region(devno,count,GPIO_DEVICE);
이미 정해진 번호를 커널에 예약 등록,이미 사용중->에러.

#include <linux/fs.h>
int register_chrdev_region(dev_t from, unsigned count, const char *name);

from : 등록할 첫번째 디바이스 번호(메이저,마이너가 포함된 dev_t)
count : 등록할 연속된 디바이스 번호 개수
name : 디바이스 이름 (/proc/devices에 표시)
리턴 : 성공 시 0 실패시 음수 에러 (-EBUSY)

void unregister_chrdev_region(dev_t from, unsigned count);
register_chrdev_region으로 예약한 번호 공간 해제하여 다른 드라이버가 쓸 수 있도록 함.
from : 해제할 첫번째 디바이스 번호(예약한 dev_t)
count :해제할 연속 디바이스 번호 개수

번호 충돌 가능성 있음
번호를 명확히 알고 있어야 함
레거시 코드나 특별한 번호 필요할 때 사용

설명
/proc/devices 커널에 등록된 모든 캐릭터 및 블록 디바이스의 리스트. register_chrdev_region()에 의해 등록됨.
./dev/my_dev 유저 공간에서 사용하는 장치 파일 (device node). 커널이 자동으로 만들지 않음. 직접 mknod하거나 udev 설정 필요.

file_operations 구조체에 연산 등록하여 open,read,write 등의 파일 연산 함수 사용

include./linux/fs.h

file_operations 구조체의 역할

  • 리눅스 커널에서 디바이스 드라이버가 파일 시스템 인터페이스를 구현할 수 있도록 해주는 콜백 함수 집합.
    이 구조체에 함수를 등록함으로써
    open(),read(),write(),ioctl(),mmap()등의 시스템 콜이 호출될 때, 해당 드라이버의 동작이 실행되도록 연결.

cdev 구조체 (linux/cdev.h)

struct cdev { struct kobject kobj;//sysfs와 연동하기 위한 커널 오브젝트 
struct module \*owner;//(THIS\_MODULE)이 모듈이 언로드 되지 않도록 참조를 잡아줌 
const struct file\_operations \*ops;//open,read,write 등 파일 연산 함수들 집합 
struct list\_head list;//내부 연결 리스트. cdev가 여러개 일때 리스트로 관리 
dev\_t dev;//major, minor 조합의 디바이스 번호 
unsigned int count;// cdev가 몇개의 마이너 번호 다루는지(대부분 1개) } __randomize_layout; 

cdev_init (fs/char_dev.c)

구조체를 초기화하고 file operations에 open,write 등의 연산을 연결한다.

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
    memset(cdev, 0, sizeof *cdev);
    INIT_LIST_HEAD(&cdev->list);
    kobject_init(&cdev->kobj, &ktype_cdev_default);
    cdev->ops = fops;
}
 /* cdev_init() - initialize a cdev structure
 * @cdev: the structure to initialize
 * @fops: the file_operations for this device
 */

cdev_add

커널 공간의 문자 디바이스 구조체(struct cdev)를 시스템에 연결하여, 유저가 /dev/~ 경로로 접근할 수 있도록 등록한다.

 /*cdev_add() - add a char device to the system
 * @p: the cdev structure for the device
 * @dev: the first device number for which this device is responsible
 * @count: the number of consecutive minor numbers corresponding to this
 *         device
 *
 * cdev_add() adds the device represented by @p to the system, making it
 * live immediately.  A negative error code is returned on failure.
 */
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
    int error;

    p->dev = dev;
    p->count = count;

    if (WARN_ON(dev == WHITEOUT_DEV)) {
        error = -EBUSY;
        goto err;
    }

    error = kobj_map(cdev_map, dev, count, NULL,
             exact_match, exact_lock, p);
             //cdev_map에 전역 맵에 해당 번호 등록. 
    if (error)
        goto err;

    kobject_get(p->kobj.parent);

    return 0;

err:
    kfree_const(p->kobj.name);
    p->kobj.name = NULL;
    return error;
}

mknod

/dev/에 노드 파일을 만들어서 ,등록된 문자 디바이스에 파일로 유저가 접근할 수 있게한다. 디바이스 번호(major,minor)를 가진 파일을 만든다.

실행 흐름 함수/명령어
1. 디바이스 번호 할당 커널에서 Major,Minor번호 할당 alloc_chrdev_region(&devmfirstminor,count,”name”)
2. cdev 구조체 초기화 및 등록 (cdev 구조체에 파일 연산자 연결) cdev_init(&cdev, &fops) cdev_add(&cdev,dev,count)
3. 유저 공간에서 디바이스 파일 생성 mknod /dev/name c (major) (minor)
4. 유저 프로그램에서 /dev/ 파일을 open() 호출하면 커널 드라이버의 open 함수 호출. open(”/dev/name”,flags)

유저 공간의 애플리케이션 코드에서 시스템 호출을 통해
커널을 통해 어떻게 디바이스 파일에 접근하여 장치를 사용하는지 함수 호출을 따라가보았다.

사물인터넷을 위한 리눅스 프로그래밍 with 라즈베리파이(서영진)

1. 애플리케이션 호출

    fd = open("/dev/sk_dev", O_RDWR);

2. 시스템 호출

AT_FDCWD : 현재 작업 디렉토리에서 상대경로로 파일을 연다.

커널이 제공하는 매크로 (→ syscall 테이블에 매핑되어 호출.)

do_sys_open→do_sys_openat2→do_filp_open


do_filp_open→path_openat→do_open→vfsopen


nd : 경로 파싱 과정의 상태 저장소

#define EMBEDDED_LEVELS 2 struct nameidata { struct path path; struct qstr last; struct path root; struct inode *inode; /* path.dentry.d_inode */ unsigned int flags, state; unsigned seq, next_seq, m_seq, r_seq; int last_type; unsigned depth; int total_link_count; struct saved { struct path link; struct delayed_call done; const char *name; unsigned seq; } *stack, internal[EMBEDDED_LEVELS]; struct filename *name; struct nameidata *saved; unsigned root_seq; int dfd; vfsuid_t dir_vfsuid; umode_t dir_mode; } __randomize_layout;

3. ** 가상 파일 시스템 do_open->vfs_open**

open.c/vfs_open

4. inode로 파일 연결 do_dentry_open

  • do_dentry_open
    static int do_dentry_open(struct file *f,
                  int (*open)(struct inode *, struct file *))
    {
        static const struct file_operations empty_fops = {};
        struct inode *inode = f->f_path.dentry->d_inode;
        int error;

        path_get(&f->f_path);
        f->f_inode = inode;
        f->f_mapping = inode->i_mapping;
        f->f_wb_err = filemap_sample_wb_err(f->f_mapping);
        f->f_sb_err = file_sample_sb_err(f);

        if (unlikely(f->f_flags & O_PATH)) {
            f->f_mode = FMODE_PATH | FMODE_OPENED;
            f->f_op = &empty_fops;
            return 0;
        }

        if ((f->f_mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ) {
            i_readcount_inc(inode);
        } else if (f->f_mode & FMODE_WRITE && !special_file(inode->i_mode)) {
            error = file_get_write_access(f);
            if (unlikely(error))
                goto cleanup_file;
            f->f_mode |= FMODE_WRITER;
        }

        /* POSIX.1-2008/SUSv4 Section XSI 2.9.7 */
        if (S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode))
            f->f_mode |= FMODE_ATOMIC_POS;

        f->f_op = fops_get(inode->i_fop);
        if (WARN_ON(!f->f_op)) {
            error = -ENODEV;
            goto cleanup_all;
        }

        error = security_file_open(f);
        if (error)
            goto cleanup_all;

        error = break_lease(file_inode(f), f->f_flags);
        if (error)
            goto cleanup_all;

        /* normally all 3 are set; ->open() can clear them if needed */
        f->f_mode |= FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;
        if (!open)
            open = f->f_op->open;
        if (open) {
            error = open(inode, f);
            if (error)
                goto cleanup_all;
        }
        f->f_mode |= FMODE_OPENED;
        if ((f->f_mode & FMODE_READ) &&
             likely(f->f_op->read || f->f_op->read_iter))
            f->f_mode |= FMODE_CAN_READ;
        if ((f->f_mode & FMODE_WRITE) &&
             likely(f->f_op->write || f->f_op->write_iter))
            f->f_mode |= FMODE_CAN_WRITE;
        if ((f->f_mode & FMODE_LSEEK) && !f->f_op->llseek)
            f->f_mode &= ~FMODE_LSEEK;
        if (f->f_mapping->a_ops && f->f_mapping->a_ops->direct_IO)
            f->f_mode |= FMODE_CAN_ODIRECT;

        f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
        f->f_iocb_flags = iocb_flags(f);

        file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);

        if ((f->f_flags & O_DIRECT) && !(f->f_mode & FMODE_CAN_ODIRECT))
            return -EINVAL;

        /*
         * XXX: Huge page cache doesn't support writing yet. Drop all page
         * cache for this file before processing writes.
         */
        if (f->f_mode & FMODE_WRITE) {
            /*
             * Depends on full fence from get_write_access() to synchronize
             * against collapse_file() regarding i_writecount and nr_thps
             * updates. Ensures subsequent insertion of THPs into the page
             * cache will fail.
             */
            if (filemap_nr_thps(inode->i_mapping)) {
                struct address_space *mapping = inode->i_mapping;

                filemap_invalidate_lock(inode->i_mapping);
                /*
                 * unmap_mapping_range just need to be called once
                 * here, because the private pages is not need to be
                 * unmapped mapping (e.g. data segment of dynamic
                 * shared libraries here).
                 */
                unmap_mapping_range(mapping, 0, 0, 0);
                truncate_inode_pages(mapping, 0);
                filemap_invalidate_unlock(inode->i_mapping);
            }
        }

        return 0;

    cleanup_all:
        if (WARN_ON_ONCE(error > 0))
            error = -EINVAL;
        fops_put(f->f_op);
        put_file_access(f);
    cleanup_file:
        path_put(&f->f_path);
        f->f_path.mnt = NULL;
        f->f_path.dentry = NULL;
        f->f_inode = NULL;
        return error;
    }

해당 inode를 파일에 연결한다.(f→f_path 참조 수 증가, 페이지 캐시용 mapping설정 등)

드라이버가 등록한 file operation을 읽어옴.

/dev/my_dev/에서 open이 null이면 op→에 등록된 .open 함수, (드라이버 등록한 함수,my_open,user_open...) 실행

리눅스 디바이스 드라이버 기본 뼈대 구조 (모듈로 구현할때)

1. init / exit 함수

드라이버 로딩 시 초기화 (init)

드라이버 제거 시 자원 해제 (exit)

2. register_chrdev(), platform_driver_register() 등으로 디바이스를 커널에 등록

3. file_operations 구조체

유저 공간 → 커널 공간 인터페이스

open, read, write, unlocked_ioctl, release 등 시스템 콜과 연결

4. 모듈 매크로

module_init(init_func) → 드라이버 init 함수 등록

module_exit(exit_func) → 드라이버 exit 함수 등록

5. 모듈 정보

MODULE_LICENSE("GPL") → 커널에 라이선스 정보 전달
(GPL이 아니면 커널 내부 심볼 접근 제한)

선택적으로 MODULE_AUTHOR, MODULE_DESCRIPTION, MODULE_VERSION 등을 추가

문자디바이스,블록디바이스, 플랫폼디바이스, 등 디바이스 드라이버 공부 시작때 정리해놓았던 것 처음부터 다시 블로그에 정리하려한다.

module_init부터 initcall섹션 까지 따라가보기

커널이 모듈을 로드하면 .initcall*.init 섹션을 순회하면서 함수 포인터를 하나씩 실행

그중 .initcall6.init에 들어간 initModule 함수가 호출!

이렇게, 작성한 초기화 함수가 모듈 로딩 시 자동 실행된다.

1.module_init(initModule)




2. __initcall(initModule)

include/linux/module.h (_init, _exit)

include/linux/init.h

  1. __define_initcall(initModule,init)
    )
  • fn 함수 포인터를
  • sectionc(__sec)라는 특정 섹션에 넣고 static으로 선언하며, __used 속성으로 컴파일러가 최적화하지 않도록 한다.

fn 함수 주소가 .initcallxxx같은 섹션에 기록되어서

커널이나 모듈 로드시 이 함수들을 찾아서 실행한다.

readelf -S hello_module.ko (정해진 섹션확인)

)


T initModule → init 함수가 실제 텍스트 섹션에 있음
syscall 등록 후 사용자 공간에서 호출하여 사용한다!

빌트인 vs 모듈 형태 시스템 콜

<1>테이블에 번호를 추가해 빌트인 사용하는 것과
<2>.ko 모듈로 올렸다 내렸다 사용하는 방법 정리 후
모듈로 디바이스 드라이버 구현 시 initcall로 어떻게 시스템 콜이 되는지! 따라가보겠따.

<1> syscall번호를 테이블에 넣어 커널에 빌트인하여 사용

  1. 커널 공간 코드 작성.

     // kernel/hello.c
     #include <linux/kernel.h>
     #include <linux/syscalls.h>
    
     SYSCALL_DEFINE0(hello)
     {
         printk(KERN_INFO "Hello, system call!\n");
         return 0;
     }
    1. Makefile에 추가
  2. linux/arch/arm64/tools/syscall_64.tbl에 번호 추가

  3. 재빌드

    cd linux
    KERNEL=kernel8
    make bcm2711_defconfig
    

time make ARCH=arm64 CROSS_COMPILE="ccache aarch64-linux-gnu-" -j6 Image.gz modules dtbs

sudo make -j6 modules_install

sudo cp /boot/firmware/$KERNEL.img /boot/firmware/$KERNEL-backup.img
sudo cp arch/arm64/boot/Image.gz /boot/firmware/$KERNEL.img
sudo cp arch/arm64/boot/dts/broadcom/.dtb /boot/firmware/
sudo cp arch/arm64/boot/dts/overlays/
.dtb* /boot/firmware/overlays/
sudo cp arch/arm64/boot/dts/overlays/README /boot/firmware/overlays/


5. application (유저 코드) 작성
```c
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>

// 커널에서 지정한 syscall 번호 정의
#define __NR_hello 463

int main() {
    long ret;
    ret = syscall(__NR_hello);
    printf("syscall returned: %ld\n", ret);
    return 0;
}

<2> 모듈로 사용할때에 initcall 메커니즘.

  • 대부분 모듈 내에서 디바이스 드라이버 코드로 구현이 가능하다.

비교하여 정리하면,

  • 빌트인 → 안정적, 항상 사용 가능, 커널 수정 필요
    syscall_table[__NR_mycall] = &my_syscall_func;
  • 모듈 → 유연, 테스트 용이, 런타임 등록/해제 가능

재컴파일이 필요없는 커널의 오브젝트가되어 모듈로 올렸다 내렸다 사용하는 방법에 대해 정리에 앞서, 다음 글로 나눠 initcall로 모듈 로드 후 syscall 번호로 어떻게 호출되는지. 알아보겠다.

[Linux Foundation](- https://youtu.be/INWghYZH3hI?si=8K44yBoVqtcxhiFB)

위 U-Boot 부트로더 설명 영상을 보며 정리해보고,
코드를 타고타고 따라가보았다.
코드는 원래 노션 토글로 간추렸던 것들을 여기에 일단 다 옮겨보았는데, 좀 긴 편이니 아랫 부분에 두었습니다.
이글 전에 첫번재 작성한 U-Boot글이 더 쉽게 정리해놓았긴합니다만

U-Boot 사, Linux Foundation의 영상을 바탕으로도 정리해보았습니다. 부족한 내용정리라, 많은 조언 부탁드립니다~~

1. Boot ROM

  • 전원 인가를 하게 되면 SoC칩 내부에서 실행이 되는 코드(ROM). (EL3 익셉션 레벨3)
  • 칩 내부의 H/W의 기본 유닛을 초기화 설정.
  • Resides on reset vector(전원인가→리셋 벡터→boot ROM)
  • BootROM: on-chip boot loader 라고도 함.

  • Power-On Self-Test(POST) 자가 진단 수행 후, 비휘발성 메모리(NVRAM이나 CMOS)에 저장된 부팅 설정을 읽는다.
  • 부팅 디바이스 탐색 및 선택 후 1차 Bootloader를 로드한다
  • (Broadcom BCM2711)

2. 1st Bootloader

  • DRAM 컨트롤러 초기화 전. (가상 메모리X, DRAMX).
  • First bootloader는 램에서 실행되는게 아니라 칩 내부에 있는 Static RAM에서 실행되도록 보통 설계된다.
  • PMU,GPIO,UART,PMIC(전원 칩),DDR 컨트롤러와 같은 여러 IP를 초기화하는 역할(전원이나 클럭같은 기본적인 설정)
  • 2차 부트로더 로드(EL3→EL2)

  1. bootcode.bin
  2. U-Boot SPL(secondary program loader) (부트로더로 U-Boot를 쓸때)
    • u-boot.bin은 바로 boot ROM이 로드 하기에 커서, SPL으로 대신 로드(SRAM).
    • First user-controlled code(Boot ROM이후)
    • Responsible for additional HW initialization(DRAM,UART,i2C, PMIC, MMC등 H/W 초기화)
    • Loads U-Boot or Kernel directly
    • https://docs.u-boot.org/en/latest/usage/spl_boot.html

3. 2nd Boot loader(User boot loader, U-boot)

  • RAM에서 실행 (EL2)
  • 전체 하드웨어 초기화 board_init_f(),crto.s,board_init_r()
  • GPIO, UART, I2C, 타이머 등 주변 장치 초기화, 메모리 재배치,초기화
  • SD카드로 부터 mmc를 이용해 디바이스 트리(device tree)와 커널 이미지를 메모리에 로드.
  • Rootfs 경로를 bootargs로 커널에 전달.
  • 하드웨어 구성 정보를 담고 있는 구조체의 포인터 전달.
  • 커널에 커맨드 라인 포인터를 전달.
  • 제어권을 커널에게 넘겨주고, 메모리를 반환후 종료.

4. Linux Kernel

  • 커널 이미지 실행되면서 디바이스 트리 참조해서 초기화 수행(EL1)
  • startup code (head.S)
  • 주요 동작
    • MMU/캐시 설정
    • 디바이스 트리 파싱
    • 메모리/디바이스 초기화
    • mnt/root의 ext4 파일시스템을 “/”루트파일시스템로 mount 시도
    • →부팅 중 메시지로 출력

5. root FS- User space Process(init)

  • kernel_init()으로 PID1인 init 프로세스 실행
  • 루트 파일시스템에서 /sbin/init, systemd, init.d 스크립트 등 실행
  • system call, 쉘 등 사용자 공간 프로세스 시작

리셋부터, u-boot 부트로더 동작, init/main.c start_kernel()까지 소스코드

start.S ENTRY(_start)부터 start_kernel()까지 (cmd처리 중심)

  1. ENTRY(_start)
    파일 : arch/arm/cpu/armv8/start.S

  2. bl lowlevel_init


board_init_f()까지 코드를 안전하게 실행하기 위한 최소한의 초기화.
가능한 아무것도 하지 않는다.

  arch/arm/mach-bcm283x/lowlevel\_init.S  
![](https://velog.velcdn.com/images/yez_hong/post/42aff80b-2a3c-4984-86f1-192aefa5cdca/image.png))![](https://velog.velcdn.com/images/yez%5C_hong/post/42aff80b-2a3c-4984-86f1-192aefa5cdca/image.png)

3.start.s로 돌아와 bl _main으로 crto.s의 main으로 점프한다.

4.crt0.s:arch/arm/lib/crt0_64.s

-   crt0\_64.s 설명 주석코드

아래 보다시피

메모리 영역 재배치, bss영역 초기화 후 board_init_f로 분기한다.

crt0.s⇒다음 세가지 포함.

  • board_init_f
  • 하드웨어 초기사용을 위한 함수. 초기 메모리가 부족한 시점에서 실행되어 시스템 메모리(DRAM) 셋업 등 필수 환경을 준비.
  • relocate_code
  • U-Boot 바이너리를 실행 가능한 위치(DRAM 등)로 복사하고, 내부 주소를 재조정해 코드가 정상 실행되게 함.
  • board_init_r
  • 부팅 후반부 초기화 함수. 파일 시스템, 네트워크, 콘솔 등 다양한 장치를 초기화하며, 사용자 명령 입력 대기 상태로 전환.

1.board_init_f()
u-boot/common/board_f.c

board_init_r()로 넘어가기 전까지 시스템 메모리 초기화(SDRAM) 및 UART 등 기본 I/O준비.

다시 crto_64.s의 main으로 돌아간 후

메모리 재배치, bss영역 초기화 등 다음, board_init_r 호출

2.board_init_r()
u-boot/common/board_r.c

board_init f, r()로 2차 부트로더의 하드웨어, 주변장치 초기화 단계를 수행 후,

main_loop()→main.c로 진입한다.

3.common/main.c로 진입

사용자 버튼 처리(예 : GPIO 버튼 누를때 동작)
bootdelay처리,

부팅할 명령어 실행

  1. cli.c→ cli_loop()→cli_simple_loop호출

    common/cli_simple.c

    )

입력받기 위해 cli_readline 호출

common/cli_readline.c

cli_readline_into_buffer

cread_line_simple

  • cread_line_simple 전체코드
            static int cread_line_simple(const char *const prompt, char *p)
            {
             char *p_buf = p;
             int n = 0; /* buffer index */
             int plen = 0; /* prompt length */
             int col; /* output column cnt */
             int c;

             /* print prompt */
             if (prompt) {
             plen = strlen(prompt);
             puts(prompt);
             }
             col = plen;

             for (;;) {
             if (bootretry_tstc_timeout())
             return -2; /* timed out */
             schedule(); /* Trigger watchdog, if needed */

             c = getchar();

             /*
              * Special character handling
              */
             switch (c) {
             case '\r': /* Enter */
             case '\n':
             *p = '\0';
             puts("\r\n");
             return p - p_buf;

             case '\0': /* nul */
             continue;

             case 0x03: /* ^C - break */
             p_buf[0] = '\0'; /* discard input */
             return -1;

             case 0x15: /* ^U - erase line */
             while (col > plen) {
             puts(erase_seq);
             --col;
             }
             p = p_buf;
             n = 0;
             continue;

             case 0x17: /* ^W - erase word */
             p = delete_char(p_buf, p, &col, &n, plen);
             while ((n > 0) && (*p != ' '))
             p = delete_char(p_buf, p, &col, &n, plen);
             continue;

             case 0x08: /* ^H  - backspace */
             case 0x7F: /* DEL - backspace */
             p = delete_char(p_buf, p, &col, &n, plen);
             continue;

             default:
             /* Must be a normal character then */
             if (n >= CONFIG_SYS_CBSIZE - 2) { /* Buffer full */
             putc('\a');
             break;
             }
             if (c == '\t') { /* expand TABs */
             if (IS_ENABLED(CONFIG_AUTO_COMPLETE)) {
             /*
              * if auto-completion triggered just
              * continue
              */
             *p = '\0';
             if (cmd_auto_complete(prompt,
                   console_buffer,
                   &n, &col)) {
             p = p_buf + n; /* reset */
             continue;
             }
             }
             puts(tab_seq + (col & 07));
             col += 8 - (col & 07);
             } else {
             char __maybe_unused buf[2];

             /*
              * Echo input using puts() to force an LCD
              * flush if we are using an LCD
              */
             ++col;
             buf[0] = c;
             buf[1] = '\0';
             puts(buf);
             }
             *p++ = c;
             ++n;
             break;
             }
             }
            }
  • 사용자가 U-Boot 프롬프트에 입력한 문자열은 p_buf에 저장됨.

  • 엔터 치면 cread_line_simple은 입력한 문자열 길이를 리턴하고,

  • 그 다음 이 문자열은 명령어 파싱기로 넘어감.

  • simple_loop로 돌아와 문자열 길이를 받으면 last_command에 문자열을 복사해 rum_command를 실행

  • run_command_list (cli.c)

            int run_command_list(const char *cmd, int len, int flag)
            {
             int need_buff = 1;
             char *buff = (char *)cmd; /* cast away const */
             int rcode = 0;

             if (len == -1) {
             len = strlen(cmd);
            #ifdef CONFIG_HUSH_PARSER
             /* hush will never change our string */
             need_buff = 0;
            #else
             /* the built-in parser will change our string if it sees \n */
             need_buff = strchr(cmd, '\n') != NULL;
            #endif
             }
             if (need_buff) {
             buff = malloc(len + 1);
             if (!buff)
             return 1;
             memcpy(buff, cmd, len);
             buff[len] = '\0';
             }
            #ifdef CONFIG_HUSH_PARSER
             if (use_hush_old()) {
             rcode = parse_string_outer(buff, FLAG_PARSE_SEMICOLON);
             } else {
             rcode = parse_string_outer_modern(buff, FLAG_PARSE_SEMICOLON);
             }
            #else
             /*
              * This function will overwrite any \n it sees with a \0, which
              * is why it can't work with a const char *. Here we are making
              * using of internal knowledge of this function, to avoid always
              * doing a malloc() which is actually required only in a case that
              * is pretty rare.
              */
            #ifdef CONFIG_CMDLINE
             rcode = cli_simple_run_command_list(buff, flag);
            #else
             rcode = board_run_command(buff);
            #endif
            #endif
             if (need_buff)
             free(buff);

             return rcode;
            }
  • cmd_process (command.c)
            enum command_ret_t cmd_process(int flag, int argc, char *const argv[],
                    int *repeatable, ulong *ticks)
            {
             enum command_ret_t rc = CMD_RET_SUCCESS;
             struct cmd_tbl *cmdtp;

            #if defined(CONFIG_SYS_XTRACE)
             char *xtrace;

             xtrace = env_get("xtrace");
             if (xtrace) {
             puts("+");
             for (int i = 0; i < argc; i++) {
             puts(" ");
             puts(argv[i]);
             }
             puts("\n");
             }
            #endif

             /* Look up command in command table */
             cmdtp = find_cmd(argv[0]);
             if (cmdtp == NULL) {
             printf("Unknown command '%s' - try 'help'\n", argv[0]);
             return 1;
             }

             /* found - check max args */
             if (argc > cmdtp->maxargs)
             rc = CMD_RET_USAGE;

            #if defined(CONFIG_CMD_BOOTD)
             /* avoid "bootd" recursion */
             else if (cmdtp->cmd == do_bootd) {
             if (flag & CMD_FLAG_BOOTD) {
             puts("'bootd' recursion detected\n");
             rc = CMD_RET_FAILURE;
             } else {
             flag |= CMD_FLAG_BOOTD;
             }
             }
            #endif

             /* If OK so far, then do the command */
             if (!rc) {
             int newrep;

             if (ticks)
             *ticks = get_timer(0);
             rc = cmd_call(cmdtp, flag, argc, argv, &newrep);
             if (ticks)
             *ticks = get_timer(*ticks);
             *repeatable &= newrep;
             }
             if (rc == CMD_RET_USAGE)
             rc = cmd_usage(cmdtp);
             return rc;
            }

/* Look up command in command table */
cmdtp = find_cmd(argv[0]);
find_cmd()(command.c)

find_cmd(”boot”)는 cmdtp→cmd_rep(아래 설명)== do_booti_wrapper인 구조체를 찾아 반환한다.

cmd_call(command.c)

cmd_tbl을 통해 cmd_rep()으로 호출 (command.h)

  • cmd_rep는 struct cmd_tbl 내 명령어 실행 함수 포인터
    • 명령어 등록 매크로(U_BOOT_CMD 등)가 이 함수 포인터를 연결

command.h

  1. cmd/booti.c

이 매크로는 내부적으로 __u_boot_cmd라는 특수 섹션에 struct cmd_tbl을 등록, 이 구조체의 .cmd_rep 필드는 do_booti(), do_booti_wrapper()포인터가 된다.

  • do_booti함수 실행 (cmd/booti.c)
            int do_booti(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[])
            {
             struct bootm_info bmi;
             int states;
             int ret;

             /* Consume 'booti' */
             argc--; argv++;

             bootm_init(&bmi);
             if (argc)
             bmi.addr_img = argv[0];
             if (argc > 1)
             bmi.conf_ramdisk = argv[1];
             if (argc > 2)
             bmi.conf_fdt = argv[2];
             bmi.boot_progress = true;
             bmi.cmd_name = "booti";
             /* do not set up argc and argv[] since nothing uses them */

             if (booti_start(&bmi))
             return 1;

             /*
              * We are doing the BOOTM_STATE_LOADOS state ourselves, so must
              * disable interrupts ourselves
              */
             bootm_disable_interrupts();

             images.os.os = IH_OS_LINUX;
             if (IS_ENABLED(CONFIG_RISCV))
             if (IS_ENABLED(CONFIG_64BIT))
             images.os.arch = IH_ARCH_RISCV64;
             else
             images.os.arch = IH_ARCH_RISCV;
             else if (IS_ENABLED(CONFIG_ARM64))
             images.os.arch = IH_ARCH_ARM64;

             states = BOOTM_STATE_MEASURE | BOOTM_STATE_OS_PREP |
             BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO;
             if (IS_ENABLED(CONFIG_SYS_BOOT_RAMDISK_HIGH))
             states |= BOOTM_STATE_RAMDISK;

             ret = bootm_run_states(&bmi, states);

             return ret;
            }
  1. 커널 entry로 점프
  2. - boot_jump_linux (bootm.c) 커널 entry주소로 점프 (보통 Image or vmlinux 시작 주소)
            int do_bootm_linux(int flag, struct bootm_info *bmi)
            {
             struct bootm_headers *images = bmi->images;

             /* No need for those on ARM */
             if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
             return -1;

             if (flag & BOOTM_STATE_OS_PREP) {
             boot_prep_linux(images);
             return 0;
             }

             if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {
             boot_jump_linux(images, flag);
             return 0;
             }

             boot_prep_linux(images);
             boot_jump_linux(images, flag);
             return 0;
            }

            #if defined(CONFIG_BOOTM_VXWORKS)
            void boot_prep_vxworks(struct bootm_headers *images)
            {
            #if defined(CONFIG_OF_LIBFDT)
             int off;

             if (images->ft_addr) {
             off = fdt_path_offset(images->ft_addr, "/memory");
             if (off > 0) {
             if (arch_fixup_fdt(images->ft_addr))
             puts("## WARNING: fixup memory failed!\n");
             }
             }
            #endif
             cleanup_before_linux();
            }

            void boot_jump_vxworks(struct bootm_headers *images)
            {
            #if defined(CONFIG_ARM64) && defined(CONFIG_ARMV8_PSCI)
             armv8_setup_psci();
             smp_kick_all_cpus();
            #endif

             /* ARM VxWorks requires device tree physical address to be passed */
             ((void (*)(void *))images->ep)(images->ft_addr);
            }
            #endif
            /* Subcommand: GO */
            static void boot_jump_linux(struct bootm_headers *images, int flag)
            {
            #ifdef CONFIG_ARM64
             void (*kernel_entry)(void *fdt_addr, void *res0, void *res1,
             void *res2);
             int fake = (flag & BOOTM_STATE_OS_FAKE_GO);

             kernel_entry = (void (*)(void *fdt_addr, void *res0, void *res1,
             void *res2))images->ep;

             debug("## Transferring control to Linux (at address %lx)...\n",
             (ulong) kernel_entry);
             bootstage_mark(BOOTSTAGE_ID_RUN_OS);

             announce_and_cleanup(fake);

             if (!fake) {
             do_nonsec_virt_switch();

             update_os_arch_secondary_cores(images->os.arch);
            #else
             if ((IH_ARCH_DEFAULT == IH_ARCH_ARM64) &&
                 (images->os.arch == IH_ARCH_ARM))
             armv8_switch_to_el2(0, (u64)gd->bd->bi_arch_number,
                 (u64)images->ft_addr, 0,
                 (u64)images->ep,
                 ES_TO_AARCH32);
             else
             armv8_switch_to_el2((u64)images->ft_addr, 0, 0, 0,
                 images->ep,
                 ES_TO_AARCH64);
            #endif
             }
            #else
            #endif
            }
  • arch/arm/cpu/armv8/transition.S
            .pushsection .text.armv8_switch_to_el2, "ax"
            ENTRY(armv8_switch_to_el2)
             bl armv8_switch_to_el2_prep
             nop
             switch_el x6, 1f, 0f, 0f
            0:
             cmp x5, #ES_TO_AARCH64
             b.eq 2f
             /*
              * When loading 32-bit kernel, it will jump
              * to secure firmware again, and never return.
              */
             bl armv8_el2_to_aarch32
            2:
             /*
              * x4 is kernel entry point or switch_to_el1
              * if CONFIG_ARMV8_SWITCH_TO_EL1 is defined.
                     * When running in EL2 now, jump to the
              * address saved in x4.
              */
             br x4
            1: armv8_switch_to_el2_m x4, x5, x6
            ENDPROC(armv8_switch_to_el2)
            .popsection

starting kernel 메세지 출력 (common/bootm.c) 메세지는 u-boot에서 출력된다.

(하드웨어관련 메세지도 추가하기)

커널 이미지 로딩 후 , head.S 실행. ~/linux/arch/arm64/kernerl/head.S에서 start_kernel()로 진입

init/main.c :start_kernel() 진입해 초기화

        [U-Boot]
        cli_loop()
         └─> cli_simple_loop()
             └─> cli_readline()             // 콘솔에서 사용자 명령어 입력 대기 및 읽기
             └─> run_command("booti 80000 - 90000")  // 입력받은 명령어 실행 요청
                 └─> run_command_list()     // 명령어가 여러 개일 경우 분리해서 처리
                     └─> cmd_process()      // 명령어 문자열에서 명령어 구조체 검색 및 실행
                         └─> find_cmd("booti")   // 명령어 테이블에서 'booti' 명령어 찾음
                             └─> do_booti()       // 'booti' 명령어의 실제 실행 함수 호출 (cmd/booti.c)
            → booti_start(&bmi)
            → bootm_disable_interrupts()
            → bootm_run_states()
               └─> BOOTM_STATE_OS_PREP
               └─> BOOTM_STATE_OS_GO
                   └─> arch/arm/lib/bootm.c: do_bootm_linux()
                       └─> kernel 이미지 주소로 분기 → 커널 실행!


        [Linux Kernel]
        arch/arm64/kernel/head.S
         └─> stext
             └─> start_kernel()
                 └─> rest_init()
                     └─> kernel_init()

⇒ 전원을 켰을 때 main loop 안의 autoboot에 의해 시간 내에

키보드 입력이 들어오면 u-boot 프롬프트로 진입하고, run bootcmd(디폴트 부팅 커맨드)를 입력하면 부팅하여 start kernel 한다.

이제 명령어 해석기 (CLI)에서 계속 커맨드 받는 동작!!

start_kernel()이후 첫 프로세스 실행까지

  • start_kernel()이후 소스코드 동작 (보완하기)
    // Kernel startup entry point
    - 부트로더가 ARM64의 레지스터 x0에 디바이스 트리의 주소를 넣어주고,
     커널이 하드웨어 정보를 읽는다.
     (MMU랑 데이터 캐시 등이 비활성화된 상태여야 함)

    x0 = __pa(KERNEL_START)
    bl start_kernel

init/main.c 의 start_kernel()

  • 여기서 시스템 전역 데이터 구조 초기화, 스케줄러, 인터럽트, 타이머, 콘솔 등 핵심 서브시스템이 순차적으로 초기화된다.
  • start_kernel()의 마지막 rest_init()이 호출되고, 하는 역할

  • 첫번째 커널 스레드 생성 kernel_init()→ do_initcalls()를 통해 각종 서브시스템, 드라이버, 모듈을 초기화하는 스레드이다.

![](https://velog.velcdn.com/images/yez_hong/post/2fd2bf70-d4a2-4f30-93ae-ed3cdad6a9f2/image.png)
        static noinline void __ref __noreturn rest_init(void)
        {
         struct task_struct *tsk;
         int pid;

         rcu_scheduler_starting();
         /*
          * We need to spawn init first so that it obtains pid 1, however
          * the init task will end up wanting to create kthreads, which, if
          * we schedule it before we create kthreadd, will OOPS.
          */
          // 유저모드 스레드 생성1
         pid = user_mode_thread(kernel_init, NULL, CLONE_FS);
         // 커널모드 스레드 생성2
         pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES);
         system_state = SYSTEM_SCHEDULING;
         /* Call into cpu_idle with preempt disabled */
         cpu_startup_entry(CPUHP_ONLINE);
        }
-----------------------------------------------------------------
        void cpu_startup_entry(enum cpuhp_state state)
        {
         current->flags |= PF_IDLE;
         arch_cpu_idle_prepare();
         cpuhp_online_idle(state);
         while (1)
         do_idle();
        }
  • kernel_init()내부 주요 흐름
  • - 루트 파일 시스템 마운트 시도 - initrd나 initramfs처리 - do_basic_setup()→do_initcalls()호출
            static int __ref kernel_init(void *unused)
            {
             kernel_init_freeable();
             ->  do_basic_setup(); -> do_initcalls(); 호출
             => 유저 모드를 사용하기 전, 
             커널의 주요 기능을 사용하기 위한 초기화 완료
  • do_initcalls()는
  • - 커널에 등록된 각종 초기화 함수들을 실행 (드라이버, 서브시스템 등) - __initcall_start ~ __initcall_end 섹션의 함수들을 순차적으로 호출 - 두번째 스레드 : do_idle()로 진입해 스케줄러가 일을 줄 때까지 대기

사용자 공간(userspace) 진입

  • kernel_init() 함수가 마지막으로 init 프로세스(PID 1)를 실행.
  • /sbin/init, 또는 systemd, upstart, busybox init 등이 사용된다.
  • 이후 사용자 공간에서 서비스, 로그인, 데스크탑 환경 등 다양한 프로세스가 실행됨.
static int run_init_process(const char *init_filename)
{
 const char *const *p;

 argv_init[0] = init_filename;
 pr_info("Run %s as init process\n", init_filename);
 pr_debug("  with arguments:\n");
 for (p = argv_init; *p; p++)
 pr_debug("    %s\n", *p);
 pr_debug("  with environment:\n");
 for (p = envp_init; *p; p++)
 pr_debug("    %s\n", *p);
 return kernel_execve(init_filename, argv_init, envp_init);
}

라즈베리파이에서 UART3 시리얼 통신을 이용해 PC에서 Tera Term으로 문자를 출력됨을 확인해보기(minicom이용).(dtoverlay 작성을 해보는 간단한 연습)

예전에 했던 실습인데, 장치를 활성화 할때, menuconfig 설정으로 활성화를 하는 간단한 방법도 있지만,
이렇게 기존 dts 방식을 찾아가 수정하거나, 새로운 오버레이를 작성하는 연습을 했던 이유를 프로젝트 끝나서 느꼈다.
라즈베리파이 처럼 잘 알려지지 않은 타사의 보드에서 원하는 통신 모듈이나, 장치를 이용하고자 할때 호환이 되거나, 테스트된 정보가 없는 경우가 많다. 이럴때에 아래와 같은 dts 디바이스 트리 노드가 있는지 찾아본 후 , 수정하거나 추가함으로써 포팅을 시도하여 쓸 수 있다.

1. uart3 4,5번 핀 → TX,RX

  • 핀맵

2. /boot/fimware/config.txt 추가

3. 연결 확인

4. minicom 실행해 연결 확인

    sudo minicom -b 115200 -D /dev/ttyAMA3

minicom에서 문자 입력하여 tera term 터미널에 출력 확인

5. 프로그램 코드 구현

    #include <stdio.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <string.h>

    #define BUFFER_SIZE 1024

    int main(int argc, char *argv[]) {
        if (argc < 2) {
            printf("Usage: %s <string to send>\n", argv[0]);
            return 1;
        }

        char *message = argv[1];

        // UART3 포트 열기
        int fd = open("/dev/ttyAMA3", O_RDWR | O_NOCTTY | O_SYNC);
        // 읽기/쓰기 모드로 열며, 제어 터미널로 사용되지 않도록 설정하고, 쓰기 연산이 완료될 때까지 대기하는 동기식 모드로 열겠
        if (fd < 0) {
            perror("Failed to open /dev/ttyAMA3");
            return -1;
        }

        //  (UART3 포트로 보내기)
        ssize_t bytes_written = write(fd, message, strlen(message));
        if (bytes_written < 0) {
            perror("Failed to write to UART3");
            close(fd);
            return -1;
        }

        printf("Written %zd bytes: %s\n", bytes_written, message);

        // (UART3 포트에서 읽기)
        char buffer[BUFFER_SIZE];  // 버퍼 크기
        ssize_t bytes_read;
        int i = 0;


        while ((bytes_read = read(fd, &buffer[i], 1)) > 0) {

            if (buffer[i] == '\n' || buffer[i] == '\r') {
                break;
            }
            if(i<BUFFER_SIZE -1){
            i++;}
        }

        if (bytes_read < 0) {
            perror("Failed to read from UART3");
            close(fd);
            return -1;
        }

        buffer[i] = '\0';  
        printf("Read %d bytes: %s\n",i+1, buffer);

        close(fd);
        return 0;
    }
    gcc -o uart3_test uart3_test.c

실행 파일 생성

6. .dtbs 작성

기존 .dtsi 참고

bcm2711.dtsi

)

  1. 첫번째 시도 (/dev/ttyAMA3 장치파일 안뜸)
        /dts-v1/;
        /plugin/;

        / {
            compatible = "brcm,bcm2711";

            // UART3 포트 활성화 및 하드웨어 설정
            uart3: uart@7e201600 {
                compatible = "arm,pl011";  // UART3에 대한 호환성 설정
                reg = <0x7e201600 0x200>;  // UART3의 레지스터 주소와 크기
                status = "okay";  // UART3 포트 활성화
            };
        };

기존 .dtbs파일의 status 만 활성화하여 사용해보기

  1. 두번째 시도 (시리얼 통신 동작함)

*인터럽트, 클럭을 지우고 함(기존 파일 참고해 써볼 수는 있을 것 같다)

        /dts-v1/;
        /plugin/;

        / {
            compatible = "bcm2711,pi17";  // Raspberry Pi 4에 맞는 설정

            // fragment 0: UART3 포트 활성화
            fragment@0 {
                target = <&uart3>;
                __overlay__ {
                    status = "okay";  // UART3 포트 활성화
                };
            };

            // fragment 1: UART3 하드웨어 설정
            fragment@1 {
                target = <&uart3>;
                __overlay__ {
                    compatible = "arm,pl011";  // UART3에 대한 호환성 설정
                    reg = <0x7e201600 0x200>;  // UART3의 레지스터 주소와 크기
                //    interrupts = <GIC_SPI 121 IRQ_TYPE_LEVEL_HIGH>;  // 인터럽트 설정
                //    clocks = <&clocks BCM2835_CLOCK_UART>, <&clocks BCM2835_CLOCK_VPU>;  // 클록 설정
                 //   clock-names = "uartclk", "apb_pclk";  // 클록 이름 설정
                };
            };
        };

7. .dtbo 빌드

    dtc -@ -I dts -O dtb -o uart3-overlay.dtbo uart3-overlay.dts

인터럽트,클럭 관련 지워보고 시도 

warning이 나지만 시도해봄.

8. .dtbo파일 /boot/overlays에 복사

    sudo cp uart3-overlay.dtbo /boot/overlays
    sudo nano /boot/firmware/config.txt

config.txt에 추가

9. reboot 후 실행

   ls -l /dev/ttyA*로 장치 파일 뜨는지 확인.

문자열 helloimpi17 쓰기 pc→teraterm 송신한 출력 결과

teraterm에서 byebye 읽어 출력.

)

확인 완료!

Linux-Foundation

[RPI U-Boot](- https://elinux.org/RPi_U-Boot)

[Environment Variables](- https://docs.u-boot.org/en/latest/usage/environment.html)

- 부트로더의 역할

booting: 전원 인가 후 하드웨어 초기화(CPU,메모리, 클럭 설정 등)

loading: 운영체제인 커널을 로딩함.

역할 완료 후 제어권을 OS에 넘기면서 더 이상 메모리에 상주하지 않음.

U-Boot 빌드 및 부팅 흐름 요약

  • U-Boot(Universal Bootloader)

    • 오픈소스 2차 부트로더. 다양한 아키텍처(PowerPC,ARM,MIPS등)를 지원함

    • boot ROM→SPL1차 부트로더→2차부트로더 로드. (boot ROM에서 로드된다)

    • 하드웨어를 초기화하거나, 테스트를 한다.

    • 또한 애플리케이션을 다운로드,실행을 할 수 있다.

    • 리눅스 이미지 로드.

    • 같은 쉘에서 다른 보드의 명령어를 실행할 수 있도록하고 새로운 커맨드를 추가할 수 있는 확장성과 confiurable한 특징이 있다.

    • readme 설명

        This directory contains the source code for U-Boot, a boot loader for
        Embedded boards based on PowerPC, ARM, MIPS and several other
        processors, which can be installed in a boot ROM and used to
        initialize and test the hardware or to download and run application
        code.
      
        The development of U-Boot is closely related to Linux: some parts of
        the source code originate in the Linux source tree, we have some
        header files in common, and special provision has been made to
        support booting of Linux images.
      
        Some attention has been paid to make this software easily
        configurable and extendable. For instance, all monitor commands are
        implemented with the same call interface, so that it's very easy to
        add new commands. Also, instead of permanently adding rarely used
        code (for instance hardware test utilities) to the monitor, you can
        load and run it dynamically.

U-Boot 빌드 과정 (Make→ Link 까지)

1. Make

  • 어떤 C/ASM 소스가 빌드 대상이 될지 결정된다.
  • Makefile + Kconfig 시스템으로 보드/아키텍처 설정 반영
    • make → .config 생성
    • .config → include/autoconf.mk, include/autoconf.h 생성
    • 여기서 어떤 소스가 컴파일되고, 어떤 드라이버와 아키텍처 설정이 필요한지 결정

2. 링크 스크립트 생성
- u-boot.lds.S 파일을 전처리(cpp_하여 u-boot.lds 생성

예)

 cpp -E -P -include autoconf.h -include config.h -D__ASSEMBLY__ [u-boot.lds](http://u-boot.lds).S > u-boot.lds → GNU ld에 전달
    cpp -E -P \
        -include autoconf.h -include config.h \
        -D__ASSEMBLY__ \
        u-boot.lds.S > u-boot.lds
이 스크립트는 u-boot 이미지의 메모리 배치를 결정!

u-boot.lds 주요 내용

  • ENTRY(_start) → 실행 시작 심볼 지정

  • .text → 코드 영역 (start.o가 맨 앞에 배치)

  • .rodata, .data, .bss → 데이터 섹션

  • .u_boot_list →U-boot의 드라이버 / 명령어 등록용 리스트 섹션.

    → 이 단계 에서 실행 이미지의 “지도”가 만들어진다고 보면 된다!

3. 최종 링크
- ld -T u-boot.lds -o u-boot 명령으로 링크 수행
- start.o 포함 (텍스트 섹션 앞, 주소 0에 위치)→reset vector 역할.
- ENTRY(_start)로 _start 심볼을 실행 진입점으로 지정


리셋 이후 실행 흐름

Reset Vector→ _start (arch/xxx/cpu/start.S)

  • SoC 리셋 후 PC는 리셋 벡터 주소 가리킴(0x00000000 또는 0xFFFF0000)
  • U-Boot의 start.s 안 ENTRY(_start)가 이 위치에 배치!
  • 주요 역할
    • CPU 레지스터 초기화, 스택 포인터 설정, 캐시,MMU 설정 등 진행
  • 이후 reset : 레이블로 분기

reset 함수(start.S)

  • 하드웨어 초기화를 위한 아주 초반 단계
    • ROM/부트로더에서 넘긴 부트 파라미터 저장 (save_boot_params)
    • PIE(Position Independent Execution) 지원을 위한 코드/데이터 재배치 작업 수행 (.rela.dyn 처리)
  • 준비 끝나면 crt0.S 내의 _main 함수 호출! 진입!

_main (arch/xxx/lib/crt0.S)

  • 진짜 C 런타임으로 넘어가기 전의 준비 코드
  • 주요 동작
    • 초기 스택 재설정 (SRAM 및 캐시 영역)
    • board_init_f(0) 함수 호출

board_init_f (arch/xxx/lib/board_f.c)

  • 초기 하드웨어 초기화

  • 여기선 아직 DRAM을 쓰지 않아, 필수 장치만 켜준다.

    UART(디버깅 출력), 클럭,PLL , DRAM 컨트롤러 초기화 수행

    DRAM 주소 및 리로케이션 대상 주소 설정

    초기화 완료 후 _main 함수로 복귀

_main 복귀 이후 (crt0.s)

  • 이제 DRAM 사용 가능!
  • DRAM에 새로운 스택 포인터(SP)와 GD(Global Data) 구조체 설정
  • relocate_code 호출:
    • U-Boot 이미지를 ROM/Flash에서 DRAM으로 복사
    • PC를 DRAM 내 새 U-Boot 위치로 점프
  • relocate_vectors, c_runtime_cpu_setup호출 → 예외 벡터 설정, BSS 영역 초기화 진행

board_init_r(gd, gd->relocaddr) 호출(arch/xxx/lib/board_r.c)

  • 본격적인 런타임 단계.U-Boot 실행 시작
  • 모든 드라이버와 하위 시스템 초기화 (콘솔출력,eMMC/SD/NAND,네트워크, 환경변수로드)
  • U-Boot 메인 루프 진입. → autoboot(커널 로드,실행), CLI (명령어 해석기)진입!

라운드 로빈 스케줄링과 우선순위 스케줄링

라운드 로빈 스케줄링 예시

태스크 간 우선 순위 없이, 일정 시간 간격마다 돌아가며 실행하는 스케줄링
여기서는 Time tick으로 일정 시간 간격마다 실행 태스크를 바꾼다.

freeRTOS 레퍼런스를 참고하여, 같은 우선 순위 두 개 태스크로 실행해보는 예시를 연습해본다.
uart로 태스크마다의 메시지를 출력하며 일정시간씩 번갈아 실행하고있음을 확인합니다.

 

 

우선 순위 스케줄링

우선 순위 높은 태스크가 readylist(준비된태스크)앞에 놓인다.
그래서 두 개 태스크를 생성하고, 우선 순위가 다르다면 높은 순위의 태스크만 계속 돌아가고, 끝나고나서야
낮은 순위의 태스크가 실행됩니다.

스케줄링 시 태스크의 상태에는
Running(실행 중),Ready(준비되어 실행 대기),Block(wait,sleep하며 이벤트를 기다리는 상태),Suspend(실행 중단된 상태)가 있다.

 

 

Not Running State 종류

1. Block State

실행 시간 한 주기가 끝났거나, Delay함수를 호출하게 된 경우 이벤트를 기다리는 상태로 sleep하게 된다.
wait한다고도 한다.
스케줄러가 이 태스크를 준비시커나, 인터럽트나 다른 태스크에 의해 호출받는 등의 이벤트를 기다린다.

Delay를 주어(태스크1에 100ms,태스크2에 1000ms),태스크를 잠시 block 상태에 놓이게 한 후, 실행해보았습니다.

 

출력 결과

 

 

2. Suspend State

스케줄러의 동작을 멈추는 것이다.

⇒컨텍스트 스위칭을 일시적으로 막는다.

태스크 레벨에서의 스케줄링은 멈추지만, 인터럽트 서비스 루틴(ISR)은 실행 가능한 상태

 

 

3. Ready State

이벤트 발생 후 cpu 점유 대기 상태이다.
코드를 들어가면, 같은 순위의 태스크들끼리 더블링크드 리스트로 연결되어, readylist에 저장되어있다.(이전 글 마지막참고)

 

 

Create, Delete, argument 이용해보기

Task1 : argument를 이용해 0~9까지의 숫자를 랜덤 출력(20개씩출력해 확인)
전역으로 int* 타입 argument를 선언

int하나를 넣을 메모리 할당 후 arg파라미터로 전달한다.

Task1에서 Task2생성한다.

Task2 : Task1에서 생성되어 0을 출력하고 자기삭제를 반복

출력 결과

 

 

[8freeRTOS]161204_Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf
4.54MB

freeRTOS 공부를 시작하며 태스크에 대한 내용부터 필기합니다.

CMSIS (Cortex(Common) Microcontroller Software Interface Standard)

CubeIDE에서 freeRTOS를 사용할때,xtaskCreate,vtaskxx,pxreadylist~ 등 freeRTOS함수를 감싸는 ostaskCreate등의 CMSIS함수들이 있다.

FreeRTOS API에 기반한 CMSIS-RTOS를 사용하여 스레드(태스크)를 생성한다

Application: CMSIS-OS API만 사용

Wrapper: CMSIS-OS API 호출을 FreeRTOS 네이티브 호출로 변환

FreeRTOS Kernel: 실제 스레드 관리·동기화 실행

CMSIS 사용하는 이유 : 여러가지 펌웨어 버전들을 표준화하여 OS에서 API 호출방법을 편리하게 할 수 있도록 한다.
단점 : 함수 안에 freeRTOS 함수를 또 감싸며 호출하고 있어,코드 속도나 효율성면에서는 떨어진다.

Task 생성

아래에 osThreadCreate에는 여러 선택지 중 Static과 dynamic이 있는데,

크게 나누면 RAM에서 TCB의 공간과 스택 공간이 있는데, Static은 사용자가 직접 스택 메모리 배열을 선언해서 사용하고,

Dynamic은 내부의 malloc함수를 이용해 힙 메모리를 할당해 사용한다. FreeRTOS/Source/portable/MemMang/heap4.c에서 확인 가능.

osThreadCreate(CMSIS -RTOS API)
생성할 스레드(태스크)를 정의하고, xTaskCreate함수를 호출하여 생성한다.

Create의 파라미터를 →Handle이 전달한다.

cmsis_os.c의 Handle은 곧 tasks.c의 tskTaskContrlBlock 구조체포인터이다.

os_thread_def 구조체

xTaskCreate(freeRTOS)

xTaskCreate(
    vTaskFunction,        // Task 함수 포인터(함수명)
    "UART Task",          // Task 이름(디버깅 용도)
    128,                  // Task에 할당할 Stack의 깊이 (타입사이즈*바이트로 워드 단위)
    NULL,                 // Task함수로 전달할 사용자 정의 매개 변수 (void*포인터) 다양한 목적에 따라 구조체/포인터 전달.
    2,                    // Task 우선순위 (0~configMAX값-1)
    &uartTaskHandle       // 우선순위 변경, 삭제 등 API 호출 가능한 핸들, 필요 없으면 NULL

반환 값 : pdPASS (생성 성공), pdFAIL(Task생성실패)

→ 이 파라미터들로 Task를 생성한다.
);

BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,  
                        const char * const pcName,  
                        uint16_t usStackDepth,  
                        void *pvParameters,  
                        UBaseType_t uxPriority,  
                        TaskHandle_t *pxCreatedTask );
  1. TCB(Task Control Block) 메모리 힙에 동적 할당
  2. Task용 스택 메모리를 동적 할당
  3. 할당이 되면 Task를 init 및 TCB를 넘겨 ready 상태로 등록 후 pdPASS 성공상태 반환한다.
  4. pvPortMalloc함수 확인→FreeRTOS/Source/portable/MemMang/heap4.c

5. TaskControlBlock 구조체

  • 태스크 스택 포인터(SP)(어셈블리 접근성을 위해 가장 위에 위치)스택 포인터가 가리키는 ****컨텍스트에는 8개의 레지스터 세트로 구성된다.
  • xPSR,리턴 주소, LR(R14),R12, R3,R2,R1,R0 →이걸 MSP 또는 PSP에 푸시한다
  • 태스크가 마지막으로 실행되던 레지스터 값들(컨텍스트)은 태스크 스택(MSP,PSP)에 Push,pop으로 저장됨. 이걸 가리키는 스택 포인터를 TCB에 저장.
  • 태스크의 현재 상태 저장(정수값)(Ready,Blocked,Suspended)
  • 우선순위
  • 태스크 이름

TCB 구조체 코드

typedef struct tskTaskControlBlock /\* The old naming convention is used to prevent breaking kernel aware debuggers. _/  
{  ~~~~
//주요 내용만 발췌
ListItem_t            xStateListItem;    /*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */
ListItem_t            xEventListItem;        /*< Used to reference a task from an event list. */
UBaseType_t            uxPriority;            /*< The priority of the task.  0 is the lowest priority. */
StackType_t            *pxStack;            /*< Points to the start of the stack. */
char                pcTaskName[ configMAX_TASK_NAME_LEN ];/*< Descriptive name given to the task when created.  Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */

~~~~
} tskTCB;

이후 create된 태스크는 xStateListItem을 통해 순위에 따라 double linked list구조인 ready list에 저장된다.

 * Place the task represented by pxTCB into the appropriate ready list for
 * the task.  It is inserted at the end of the list.
 */
#define prvAddTaskToReadyList( pxTCB )                                                                \
    traceMOVED_TASK_TO_READY_STATE( pxTCB );                                                        \
    taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );                                                \
    vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \
    tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )
/*-----------------------------------------------------------*/

Support for Hierarchical Scheduling in FreeRTOS*

 

이전 4번 게시글 코드를 이용해, LED 만 부저로 바꿔 연결하여 구현합니다.

Key가 눌리면, 도->레->미->...->도로 변경과 동시에 'do','re'와 같이 어떤 음으로 바뀌는지 uart 시리얼 포트로 출력합니다.

음계 말고 이하 동일입니다.

음계 설정 하기

HCLK : 168000000Hz

1.Timer 9의 PSC,ARR,CCR1 설정
ARR값을 계속 변경하며 사용하니,
PSC로 식을 만들기 간단하도록 500000으로 스케일링 할 것입니다.

-예를 들어 계이름 '도'(523Hz)를 만든다면.

1) 168000000/(PSC+1)=500000, PSC=>336-1
2) 500000/(ARR+1)=523, ARR=>956-1

=>식으로 여러 음 번갈아 넣으려면
ARR=500000/((해당음의 Hz)-1);
으로 코드 작성합니다.

  1. CCR은 항상 50%로 합니다. ARR/2

계이름을 참고해 int buzzerHZ[8]={130,146,164,174,195,220,246,261};
도레미파솔라시도 리스트를 만들어 사용합니다.

Key가 눌리면, 도->레->미->...->도로 변경과 동시에 'do','re'와 같이 어떤 음으로 바뀌는지 uart 시리얼 포트로 출력합니다.

void USART3_Init(void)
{
     RCC->AHB1ENR |= 1<<3;// GPIOD 클럭 활성화
     RCC->APB1ENR |= 1<<18;//USART3

     //PD8,9핀 Alternative Function으로 설정
     GPIOD->MODER &= ~((0x3 << (2 * 8)) | (0x3 << (2 * 9)));
     GPIOD->MODER |= ((0x2 << (2 * 8)) | (0x2 << (2 * 9)));

     //MUX AFR uart tx,rx핀으로 설정
     GPIOD->AFR[1]&= ~(0xf<<4 | 0xf<<0);//MUX PD8,9(AFRH 8~15)
     GPIOD->AFR[1] |= (0x7<<4 | 0x7<<0);//uart3(AF7)

    //uart 레지스터 설정
     USART3->CR1 = (1<<13 | 1<<3 | 1<<2);//UE,TE,RE
     USART3->BRR = (22 << 4) | 13; //115200bps,42MHZ =356

}

void UART3_Send(char c){
    while(!(USART3->SR>>7 & 0x1));
    USART3->DR=c;
}

void UART3_Print(const char* str){
    while(*str){
        UART3_Send(*str++);
    }
}

void EXTI_KEY_Init(void){
    RCC->AHB1ENR |= (0x1 << 2); // GPIOC
    GPIOC->MODER &= ~(0x3 << (2 * 13)); // PC13: 입력 모드

    RCC->APB2ENR |= (1<<14);//SYSCFG

    SYSCFG->EXTICR[3]&= ~(0xF <<4);
    SYSCFG->EXTICR[3]|=(0x2<<4);//SYSCFG_EXTICR4(Port C)

    EXTI->IMR |=(1<<13); //line13인터럽트 마스크
    EXTI->RTSR |=(1<<13);//Rising edge

    EXTI->PR = (1<<13);//pending (인터럽트 플래그 리셋.)
    NVIC->ISER[1]|=0x1<<8;


}
int cnt=0;
int id_cnt=0;
//int ledtime[4]={20000, 10000, 5000, 2500};//1 0.5 0.25 0.125

int buzzerHZ[8]={130,146,164,174,195,220,246,261};
char msg[100];
char* HZname[8]={"do","re","mi","pa","sol","ra","si","do"};
//523:도,130:도

void EXTI15_10_IRQHandler(void)
{
    if(EXTI->PR & (0x1<<13)){
          EXTI->PR = (0x1<<13);//pending bit 클리어
          ++cnt;
          if(cnt%5==0){

         //      TIM9->ARR=ledtime[(++id_cnt)%4]-1;
              TIM9->ARR=50000/(buzzerHZ[(++id_cnt)%8])-1;
               TIM9->CCR1=(TIM9->ARR+1)/2;
               sprintf(msg,"%s\r\n",HZname[id_cnt%8]);
               UART3_Print(msg);



           }}

}



void TIM9_Init(void){
    RCC->AHB1ENR |= 1<<4;//GPIOE PE5

    GPIOE->MODER &= ~(0x3<<(5*2));
    GPIOE->MODER |= (0x2<<(5*2));//AF

    GPIOE->AFR[0] &= ~(0xf<<20);//MUX PE5
    GPIOE->AFR[0] |= (0x3<<20);//AF3

    RCC->APB2ENR |= 1<<16;//TIM9 clock
    TIM9->CR1= 0x0<<0;

    TIM9->PSC=336-1;//50000
    TIM9->ARR=956-1;//523
    TIM9->CCR1 = (TIM9->ARR)/2; // duty : 50%

    TIM9->CCMR1 |= 0x6<<4;//pwm 1 mode
    TIM9->CCER|=0x1;//output channel1

    // 타이머 시작
    TIM9->CR1 |= (1 << 0);
}

int main(void)
{

  HAL_Init();

  EXTI_KEY_Init();
  USART3_Init();
  TIM9_Init();
  SystemClock_Config();


  while (1);

}

이번엔 TIM9와 KEY 인터럽트를 이용해 GPIO 응용을 연습한 코드입니다.

PWM 출력으로 LED(커넥터에 점프선으로 직접 배선) 1초 주기로 on/off

  • 키 누른 회수가 5의 배수일 때마다 절반씩 주기 줄이며 돌아가며 변경.
    1초->0.5초->0.25->0.125초->1초..

*그리고 5의 배수나, 일정 카운트 이상일때 key 입력을 처리하는 것은 채터링 방지 효과가 있습니다.

해야할 것

아래 내용 모두 이전 포스팅에 이용 방법, 구조 설명 등 있습니다

  1. PE5와 GND -> LED를 꽂기.(긴 쪽 다리 +, 짧은 다리는 GND)

  2. key 익셉션 핸들러로 인터럽트 구현
  3. TIM9도 핸들러 구현
  4. (토글 주기)ARR값 변경할 값을 저장한 list만들기
  5. main안에 while문 동작 구현
void EXTI_KEY_Init(void){
    RCC->AHB1ENR |= (0x1 << 2); // GPIOC
    GPIOC->MODER &= ~(0x3 << (2 * 13)); // PC13: 입력 모드

    RCC->APB2ENR |= (1<<14);//SYSCFG

    SYSCFG->EXTICR[3]&= ~(0xF <<4);
    SYSCFG->EXTICR[3]|=(0x2<<4);//SYSCFG_EXTICR4(Port C)

    EXTI->IMR |=(1<<13); //line13인터럽트 마스크
    EXTI->RTSR |=(1<<13);//Rising edge

    EXTI->PR = (1<<13);//pending (인터럽트 플래그 리셋.)
    NVIC->ISER[1]|=0x1<<8;


}
int cnt=0;
int id_cnt=0;
int ledtime[4]={20000, 10000, 5000, 2500};//1 0.5 0.25 0.125


void EXTI15_10_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI15_10_IRQn 0 */
    if(EXTI->PR & (0x1<<13)){
          EXTI->PR = (0x1<<13);//pending bit 클리어
          ++cnt;
          if(cnt%5==0){
               TIM9->ARR=ledtime[(++id_cnt)%4]-1;
               TIM9->CCR1=(TIM9->ARR+1)/2;
           }}
  /* USER CODE END EXTI15_10_IRQn 0 */
 // HAL_GPIO_EXTI_IRQHandler(USER_Btn_Pin);
  /* USER CODE BEGIN EXTI15_10_IRQn 1 */

  /* USER CODE END EXTI15_10_IRQn 1 */
}


char msg[100];
volatile int id = 0;
volatile int sended = 0;



void TIM9_Init(void){
    RCC->AHB1ENR |= 1<<4;//GPIOE PE5

    GPIOE->MODER &= ~(0x3<<(5*2));
    GPIOE->MODER |= (0x2<<(5*2));//AF

    GPIOE->AFR[0] &= ~(0xf<<20);//MUX PE5
    GPIOE->AFR[0] |= (0x3<<20);//AF3

    RCC->APB2ENR |= 1<<16;//TIM9 clock
    TIM9->CR1= 0x0<<0;
      //168MHz
    TIM9->PSC=8400-1;    // 20KHz
    TIM9->ARR=10000-1;    //  1KHz
    TIM9->CCR1 = (TIM9->ARR)/2; // duty : 50%

    TIM9->CCMR1 |= 0x6<<4;//pwm 1 mode
    TIM9->CCER|=0x1;//output channel1

    // 타이머 시작
    TIM9->CR1 |= (1 << 0);
}


/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */
  EXTI_KEY_Init();

   TIM9_Init();
    //TIM6_Init();
  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */


  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
 /* MX_GPIO_Init();
  MX_ETH_Init();
  MX_USART3_UART_Init();
  MX_USB_OTG_FS_PCD_Init();
  MX_TIM9_Init();*/
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */

  while (1)
  {



    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

이전 펌웨어 기본 동작 구현 2 에서

Timer 6을 이용해 LED 토글했었던 것을 이어서,

@+'1'~'4'를 입력하면 LED 주기를 (2초 1초 0.5초 0.25초..등) 변경해보는 것을 구현해봅니다.

시리얼 포트로 해당 문장도 같이 출력합니다.

해야할 것

  1. 주변 장치 타이머,gpio Init
    TIM6 Init
    LED Init
    UART Init
    주변 peripheral->stmf4xxxx_reference manual.pdf
  2. 인터럽트로 입력 받을 UART 핸들러 구현
    ->stmf4xxxx_reference manual.pdf
    ->programming manual.pdf
    이전 글 참고 및 필요한 문서들은 공지사항에 첨부해두었습니다.
  3. LED 주기마다 토글시킬 TIM6 핸들러 구현
  4. main에 while문안에 인터럽트 이후 동작 구현

인터럽트로 구현 - UART 시리얼 포트로 @+1~4 입력 시 LED 주기 변경

void LED_Init(void){
    RCC->AHB1ENR |= (0x1<<1);//GPIOB

    GPIOB->MODER &= ~(0x3<<7*2);//LED output mode
    GPIOB->MODER |= (0x01 <<7*2);

    GPIOB->BSRR=(0x01<<(7+16));
}

void TIM6_Init(void){
    //TIM6 clock 활성화
     RCC->APB1ENR |= 1<<4;

     //APB1 : 84MHz

     //(7:ARR preload enable,2:update request sources, 1: update enable, 0 : counter reset)
     TIM6->CR1=(0x1<<7 | 0x1<<2 | 0x0<<1 | 0x0<<0);
     //84000000->
     TIM6->PSC=8400-1;//10000 10kHz
    // TIM6->ARR=10-1;//
     TIM6->ARR=5000-1;//10000/5000=2Hz 0.5s

     TIM6->CNT=0;//0부터 시작

     TIM6->DIER=0x1<<0; //Interrupt enable
     //NVIC에 TIM6_DAC_IRQ 번호 등록하기
      NVIC->ISER[1]|=0x1<<22; //54번 IRQ. ISER[1] 32~63
      TIM6->CR1 |= 0x1<<0;


}

void Change_LEDTime(int t){
    switch(t){
        case 1:
            TIM6->ARR=5000-1;//0.5s
            break;
        case 2:
            TIM6->ARR=10000-1;//1s
            break;
        case 3:
            TIM6->ARR=20000-1;//2s
            break;
        case 4:
            TIM6->ARR=30000-1;//3s
            break;
        default:
            TIM6->ARR=5000-1;
    }

}

void TIM6_DAC_IRQHandler(void)
{
  /* USER CODE BEGIN TIM6_DAC_IRQn 0 */
 if(TIM6->SR & (0x1)){
     TIM6->SR = ~(0x1);//rc_w0
     GPIOB->ODR ^=(0x1<<7);

 }}
void UART3_Init(void){
    RCC->AHB1ENR |= 1<<3;//GPIOD
    RCC->APB1ENR |= 1<<18;//USART3

    //PD8,9 AF모드
    GPIOD->MODER &= ~((0x3<< (2*8))| (0x3<<(2*9)));
    GPIOD->MODER |=((0x2<<(2*8)) | (0x2<<(2*9)));

    GPIOD->AFR[1]&= ~(0xf<<4 | 0xf<<0);//MUX PD8,9(AFRH 8~15)
    GPIOD->AFR[1] |= (0x7<<4 | 0x7<<0);//uart3

    USART3->CR1 =(1<<13 |1<<5| 1<<3 | 1<<2);//UE,TE,RE
    USART3->BRR=(22 << 4) | 13;//115200bps,42MHZ =356
      NVIC->ISER[1]|=0x1<<7; //39번 IRQ. ISER[1] 32~63

}

void UART3_Send(char c){
    while(!((USART3->SR>>7)&(0x1)));
    USART3->DR=c;
}
char UART3_Receive(void)
{
    while (!(USART3->SR & USART_SR_RXNE));
    return (char)(USART3->DR);
}

void UART3_Print(const char* str){
    while(*str){
        UART3_Send(*str++);
    }
}

char msg[100];
volatile int id = 0;
volatile int sended = 0;

void USART3_IRQHandler(void)
{

    if (USART3->SR & USART_SR_RXNE) {
        char c = (char)USART3->DR;

        if (c == '\r' || c == '\n') {
            msg[id] = '\0';
            sended = 1;
            id = 0;
            } 
        else if (id < 99) {
            msg[id++] = c;
        }
    }

}



int main(void)
{

  HAL_Init();
  UART3_Init();
   LED_Init();
   TIM6_Init();

  SystemClock_Config();

  while (1)
  {
  if(sended==1){
      sended=0;
      UART3_Print(msg);

    for(int i=0;msg[i]!='\0';++i){
             if(msg[i]=='@'){
                  int t=(int)(msg[i+1]-'0');
                  Change_LEDTime(t);
                  break;

               }}
    UART3_Print(msg);

  }

  }

}

TIM6 타이머 -polling, 익셉션 핸들러(인터럽트) 이용 | STMF429ZI :: skylarvv 님의 블로그

이번 글에서는 systick,TIM6,TIM9 타이머 중 TIM6을 이용해 LED를 일정 주기를 주어 ON/OFF하는 코드를 구현해봅니다.

위 링크에 TIM6 기본 구조와 활용에 대해 필기해두었습니다.

TIM6의 카운트 인터럽트 발생으로, LED를 토글시키는 동작입니다.
이번 글에서는 추가로 아래 구현이 필요합니다.

  • GPIO, prepherial 관련 레지스터 설정과
  • 해당 입력장치, timer 등이 어떤 핸들러에 해당하는지 찾으려면 (p.377)

STM32F4xx Reference Manual.zip
16.47MB

  • TIM6 핸들러 찾아 NVIC 테이블에 우선순위 등록, IRQ 핀 활성화하기.

pm0214-stm32-cortexm4-mcus-and-mpus-programming-manual-stmicroelectronics.pdf
2.61MB

 

ARR,Prescale 설정은 TIM6,systick 관련 이전 글들에 간단 설명 적어두었습니다.

void LED_Init(void){
    RCC->AHB1ENR |= (0x1<<1);//GPIOB

    GPIOB->MODER &= ~(0x3<<7*2);//LED output mode
    GPIOB->MODER |= (0x01 <<7*2);

    GPIOB->BSRR=(0x01<<(7+16));
}

void TIM6_Init(void){
    //TIM6 clock 활성화
     RCC->APB1ENR |= 1<<4;

     //APB1 : 84MHz
     int msec=500,pclk=84000000;

     //(7:ARR preload enable,2:update request sources, 1: update enable, 0 : counter reset)
     TIM6->CR1=(0x1<<7 | 0x1<<2 | 0x0<<1 | 0x0<<0);
     //84000000->
     TIM6->PSC=8400-1;//10000 10kHz
    // TIM6->ARR=10-1;//
     TIM6->ARR=5000-1;//10000/5000=2Hz 0.5s

     TIM6->CNT=0;//0부터 시작

     TIM6->DIER=0x1<<0; //Interrupt enable

}

void TIM6_DAC_IRQHandler(void)
{
  /* USER CODE BEGIN TIM6_DAC_IRQn 0 */
 if(TIM6->SR & (0x1)){
     TIM6->SR = ~(0x1);//rc_w0
     GPIOB->ODR ^=(0x1<<7);

 }
  /* USER CODE END TIM6_DAC_IRQn 0 */
  //HAL_TIM_IRQHandler(&htim6);
  /* USER CODE BEGIN TIM6_DAC_IRQn 1 */

  /* USER CODE END TIM6_DAC_IRQn 1 */
}
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */
  LED_Init();
  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */
  TIM6_Init();

  //NVIC에 TIM6_DAC_IRQ 번호 등록하기
  NVIC->ISER[1]|=0x1<<22; //54번 IRQ. ISER[1] 32~63

  TIM6->CR1 |= 0x1<<0;
  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  /*MX_GPIO_Init();
  MX_ETH_Init();
  MX_USART3_UART_Init();
  MX_USB_OTG_FS_PCD_Init();
  MX_TIM6_Init();*/
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

button Key 인터럽트로 LED 토글하기(레지스터제어) | STM32cubeIDE(32F429ZI) :: skylarvv 님의 블로그

Key 인터럽트 활용 관해서 전에 필기해두었습니다.

Nucleo-STM32F429ZI 보드를 사용했으며

이번엔 Key가 눌리면

%d th Key pressed로 몇번째 출력인지, UART로 출력하는 코드 구현 연습입니다.(Tera Term으로 메세지 확인)

사용한 UART, GPIO, Key 등 구조와 레지스터 사용에 대해서는 이전에 작성한 글들이 있습니다.

void USART3_Init(void)
{
     RCC->AHB1ENR |= 1<<3;// GPIOD 클럭 활성화
     RCC->APB1ENR |= 1<<18;//USART3

     //PD8,9핀 Alternative Function으로 설정
     GPIOD->MODER &= ~((0x3 << (2 * 8)) | (0x3 << (2 * 9)));
     GPIOD->MODER |= ((0x2 << (2 * 8)) | (0x2 << (2 * 9)));

     //MUX AFR uart tx,rx핀으로 설정
     GPIOD->AFR[1]&= ~(0xf<<4 | 0xf<<0);//MUX PD8,9(AFRH 8~15)
     GPIOD->AFR[1] |= (0x7<<4 | 0x7<<0);//uart3(AF7)

    //uart 레지스터 설정
     USART3->CR1 = (1<<13 | 1<<3 | 1<<2);//UE,TE,RE
     USART3->BRR = (22 << 4) | 13; //115200bps,42MHZ =356

}

void UART3_Send(char c){
    while(!(USART3->SR>>7 & 0x1));
    USART3->DR=c;
}

void UART3_Print(const char* str){
    while(*str){
        UART3_Send(*str++);
    }
}

void EXTI_KEY_Init(void){
    RCC->AHB1ENR |= (0x1 << 2); // GPIOC
    GPIOC->MODER &= ~(0x3 << (2 * 13)); // PC13: 입력 모드

    RCC->APB2ENR |= (1<<14);//SYSCFG

    SYSCFG->EXTICR[3]&= ~(0xF <<4);
    SYSCFG->EXTICR[3]|=(0x2<<4);//SYSCFG_EXTICR4(Port C)

    EXTI->IMR |=(1<<13); //line13인터럽트 마스크
    EXTI->RTSR |=(1<<13);//Rising edge

    EXTI->PR = (1<<13);//pending (인터럽트 플래그 리셋.)

}

char msg[100];
int cnt=0;

void EXTI15_10_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI15_10_IRQn 0 */

  if(EXTI->PR & (0x1<<13)){
      EXTI->PR = (1<<13);//pending bit 클리어
      ++cnt;
      sprintf(msg,"%d th Key pressed ",cnt);
      UART3_Print(msg);

  }
  /* USER CODE END EXTI15_10_IRQn 0 */
  //HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_13);
  /* USER CODE BEGIN EXTI15_10_IRQn 1 */

  /* USER CODE END EXTI15_10_IRQn 1 */
}
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */
  USART3_Init();

  NVIC->ISER[1]|=0x1<<8;//EXTI15_10->40번
  EXTI_KEY_Init();

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
 /* MX_GPIO_Init();
  MX_ETH_Init();
  MX_USART3_UART_Init();
  MX_USB_OTG_FS_PCD_Init();*/
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

저번 TIM6에 이어
GP (General purpsoe)타이머 TIM9의 구조와 활용에 대해 알아본다.

1. 타이머 구조 및 레지스터 설정 값 이해
2. polling으로 500ms마다 Uart로 메시지 출력 및 익셉션 활용으로 Uart메시지 출력 구현
3. PWM활용하여 MCU pin으로 1KHz,2KHz 등 의 주파수 및 duty로 파형 출력 구현

타이머 구조 이해

TIM6과 같은 16bit의 업카운팅 타이머이다.

  • 16 bit Auto-Reload Upcounter
  • : 지정한 값(ARR)에 도달하면 카운터가 0으로 되돌아가며 반복 업카운팅을 수행
  • 16 bit Prescaler
  • : 내부 클럭 주파수를 나누어, 카운터 입력 클럭을 조절

TIM9의 기능은 GPIO핀으로 PWM(Pulse-Width Modulation)를 출력가능하다는 것인데,
이번 글에서는 PE5핀을 통해 PWM 파형 출력하여 로직 애널라이저로 확인해 볼 것이다.
두 개의 채널로 두 파형의 입출력, 비교가 가능한데, 하나의 파형만 출력해볼 것이라 Channel1만 사용한다.

 

CH1 하나만 쓸것이고, PA2와 PE5가 가능한데 Nucleo-STMF429ZI보드는 처음에 PA2는 납땜이 필요해서, PE5핀을 사용할 것이다.

schematic 문서에 가면 PE5의 정확한 핀 위치를 알 수 있다.

 

항상 로직 애널라이저,주변장치등과 연결 시 GND와 출력 핀 두 곳을 같이 연결 후 PC와 USB전원 연결을 하도록 한다.

Register 기반 코드 구현

  1. TIM9 레지스터와 연결된 버스 확인 (클럭 활성화)

APB2와 연결된 클럭을 ON해줄 것이다.

  1. RCC APB2 periphral clock enable register
연결된 버스에 해당되는 RCC 레지스터 비트를 1로 설정해준다.  
RCC->APB2ENR = 0x1<<16; (여러 비트 활성화 시 or연산)
  1. TIM9/12 control register
  • Bit 3: OPM (One-pulse mode)
    • 0 : 업데이트 이벤트 발생 후에도 카운터 계속 동작
    • 1 : 다음 업데이트 이벤트에서 카운터 정지 (CEN 비트 자동 클리어)
  • Bit 2: URS (Update request source)
    • 0 : 카운터 오버플로우/언더플로우 또는 UG 비트 설정 시 인터럽트 발생
    • 1 : 오직 카운터 오버플로우/언더플로우에서만 인터럽트 발생
  • UEV(Update Event) 발생 시 인터럽트 요청 소스를 선택
  • Bit 1: UDIS (Update disable)
    • 0 : UEV 활성화 (오버플로우/UG 비트 설정 시 섀도 레지스터 로드 및 인터럽트 발생)
    • 1 : UEV 비활성화 (이벤트 발생 무시, 섀도 레지스터 유지; 단 UG 설정 시 카운터·프리스케일러 재초기화)
  • UEV 이벤트 생성을 활성/비활성화
  • Bit 0: CEN (Counter enable)
    • 0 : 카운터 비활성화
    • 1 : 카운터 활성화

원펄스 모드에서는 업데이트 이벤트 발생 시 CEN이 자동으로 클리어되어 카운터가 멈춤

 

4.TIM9/12 Interrupt enable register

  • Bit 0: UIE (Update interrupt enable)
    • 0 : 업데이트 인터럽트 비활성화
    • 1 : 업데이트 인터럽트 활성화
  1. TIM9/12 status register
    )
  • Bit 0: UIF (Update Interrupt Flag)
    • 업데이트 이벤트(오버플로우, UG 비트 설정, 슬레이브 리셋 등으로 레지스터 갱신) 발생하면 하드웨어에 의해 set되고 소프트웨어에 의해 clear됨
    • 0 : 업데이트 발생하지 않음
    • 1 : 업데이트 인터럽트 대기 중
    • 레지스터가 업데이트 될 때 하드웨어에 의해 set :
      • 오버플로우이고 TIMx_CR1 레지스터의 UDIS = ‘0’일 때
      • TIMx_EGR 레지스터의 UG 비트를 사용하여 소프트웨어에 의해 CNT가 재초기화될 때, TIMx_CR1 레지스터의 URS, UDIS 비트가 모두 0일 때

 

5. TIM9/12 event generation register

  • Bit 0 UG: Update generation
    • 소프트웨어가 ‘1’로 설정하면 카운터와 프리스케일러 카운터를 재초기화(0으로 클리어)하고, 레지스터 업데이트 이벤트(UEV)를 생성하며, 하드웨어가 자동으로 ‘0’으로 클리어합니다.
      • 0 : 동작 없음
      • 1 : 카운터 및 프리스케일러를 0으로 리셋, 자동리로드 레지스터(ARR 등)를 업데이트 이벤트로 갱신

 

5.TIM9/12 capture/compare mode register 1

 

PWM 모드 1로 설정할 것이다.

  1. TIM9/12 capture/compare enable register

 

카운트

  1. TIM9/12 prescaler

TIM6글에 프리스케일에 대한 간단한 이용 설명이 있습니다.

  1. TIM9/12 auto-reload register

Polling으로 500mS 마다 Uart로 ‘.’ 하나씩 출력 및 인터럽트로 ‘#’ 출력

void uart3_init(void){
    RCC->AHB1ENR |= 1<<3;//GPIOD
    RCC->APB1ENR |= 1<<18;//USART3

    //PD8,9 AF모드
    GPIOD->MODER &= ~((0x3<< (2*8))| (0x3<<(2*9)));
    GPIOD->MODER |=((0x2<<(2*8)) | (0x2<<(2*9)));

    GPIOD->AFR[1]&= ~(0xf<<4 | 0xf<<0);//MUX PD8,9(AFRH 8~15)
    GPIOD->AFR[1] |= (0x7<<4 | 0x7<<0);//uart3

    USART3->CR1 =(0<<15 |1<<13 | 0<<12 |0<<10 | 1<<3 | 1<<2);
        USART3->BRR=(22 << 4) | 13;//115200bps,42MHZ =356

}

void uart3_send(char c){
    while(!(USART3->SR>>7 &0x1));
    USART3->DR=c;
}
void uart3_printf(const char* str){
    while(*str){
        uart3_send(*str++);
    }
}

int main(void)
{

 HAL_Init();

 uart3_init();
 SystemClock_Config();

//TIM9 클럭 활성화(APB2에 연결)
  RCC->APB2ENR |= 1<<16;//TIM9 clock

//기본 타이머 설정 APB2->클럭 168000000Hz
  TIM9->PSC=16800-1; //168000000/16800 = 10000
  TIM9->ARR=10000-1;// 10000/(ARR+1)=1HZ 1sec으로 맞추려면 ARR=10000-1

  //(7:ARR preload enable,2:update request sources, 1: update enable, 0 : counter reset)
  TIM9->CR1=(0x1<<7 | 0x1<<2 | 0x0<<1 | 0x0<<0);
//필요에 따라 선택 가능 여기서 CR1은 설정해주지 않아도됨.

//count start
TIM9->CR1 |= 0x1<<0;

  while (1)
  {
  if(TIM9->SR & 0x1){//(UIF)Update interrupt flag
          TIM9->SR = ~(0x1<<0);
          uart3_printf(".");

      }

  }

}

CR1레지스터에서 preload를 설정해보았는데 preload하면 새 ARR값이 이전 카운트가 끝나는 즉시 갱신되면 error가 생길 가능성이 있어서, 미리 load해 놓는 그림이다.그러고, 다음 카운트를 시작할때 새로 담긴 값으로 시작한다.

 

타이머9로 들어오는 클럭은 168MHz이다.
Prescale : 일단 16비트에 맞는 값을 구하기 전에 간단한 값 10000으로 스케일링 해준다.
168000000/x=10000 , x=16800 = Prescale값.(0부터 세기때문에 실제는 +1더센다. 그래서 16800-1으로 1을 빼준값으로 넣어준다)

ARR : 1초를 맞추려면 1Hz, 1ms면 1KHz, 0.5ms=> 2KHz.
스케일 된 값 10000/(ARR+1)=1(Hz)
ARR 은 10000-1 설정해준다.->1초가 카운트 로드값이 된다.

타이머 익셉션핸들러(인터럽트)활용 UART 메시지 출력

TIM6과 마찬가지로 startup 벡터 테이블에 등록된 핸들러가 있다.

핸들러 함수는 stm32f4xx_it.c였던 것 같은데, 이 파일에서 함수가 생성된다. 이걸 잘라내서 main.c에 붙여 사용해도 된다. 함수가 한 파일에서 한번만 생성되어있으면 된다.

그리고 핸들러를 사용하면, 우선순위 등록과, CPU와 연결된 IRQ(interrupt request)핀을 활성화해줘야한다.
NVIC_SetPriority(TIM1_BRK_TIM9_IRQn, 1); // 우선순위 설정
NVIC_EnableIRQ(TIM1_BRK_TIM9_IRQn); // NVIC에 TIM9 IRQ 활성화
내장 함수 이용해도 되고, NVIC 레지스터 이용해도 된다.

// main.c

void TIM1_BRK_TIM9_IRQHandler(void)
{
    if (TIM9->SR & 0x01)  // Update interrupt flag 확인
    {
        TIM9->SR &= ~(0x01);  // 플래그 클리어
        uart3_write_char('9');
    }
}

// 인터럽트 TIM9 init
void TIM9_Timer_Init(void)
{
    // 1. TIM9 클럭 활성화 (APB2에 연결)
    RCC->APB2ENR |= (1 << 16);

    // 2. 타이머 설정 
    TIM9->PSC = 16799;         // 168MHz / 16800 = 10kHz (의미 : 1초당 10k번의 tick)
    TIM9->ARR = 9999;         // 10kHz(=1s)에서 (10k번의 tick이 발생하면 초기화)-> 1s
    TIM9->CNT = 0;            // 카운터 초기화

    // 3. 인터럽트 설정
    TIM9->DIER |= (1 << 0);    // Update interrupt enable
    NVIC_SetPriority(TIM1_BRK_TIM9_IRQn, 1); // 우선순위 설정
    NVIC_EnableIRQ(TIM1_BRK_TIM9_IRQn);      // NVIC에 TIM9 IRQ 활성화

    // 4. 타이머 시작
    TIM9->CR1 |= (1 << 0);
}

int main(void)
{
  uart3_init();
  SystemClock_Config();
  TIM9_Timer_Init();

  while (1);

  /* USER CODE END 3 */
}

PWM을 활용하여 MCU pin으로 1KHz, 2KHz 등 정해진 주파수 및 duty 파형 출력

2KHz, duty : 50%

CCR1레지스터로 duty비를 설정할 수 있는데,
High(ON) 상태가 전체 주기에서 차지하는 비율이다.
duty=한 주기에서 값이 1인 구간 길이/한 주기 전체 길이

주기 값이 10일때 CCR이 3이면,
1,2,3,4..10 중 3이상부터 1을 출력한다.
그럼 duty비는 (10-3)/10 = 약 70%이다.

CUBEIDE에서 설정할땐 duty비 가 ccr1레지스터 설정값으로 들어간다.
TIM9->CCR1 = (TIM9->ARR)/2; // duty : 50%
TIM9->CCR1 = (TIM9->ARR)/5; // duty : 20%

duty값이 크면 출력은 밝고(LED), 빠르다(모터회전의경우).

 

int main(void)
{

  HAL_Init();

  RCC->AHB1ENR |= 1<<4;//GPIOE PE5

  GPIOE->MODER &= ~(0x3<<(5*2));
  GPIOE->MODER |= (0x2<<(5*2));//AF

  GPIOE->AFR[0] &= ~(0xf<<20);//MUX PE5
  GPIOE->AFR[0] |= (0x3<<20);//AF3

  SystemClock_Config();

  RCC->APB2ENR |= 1<<16;//TIM9 clock

  // bit 0 : counter reset
  TIM9->CR1= 0x0<<0;
  //168MHz
  TIM9->PSC=8400-1;    // 20KHz
  TIM9->ARR=10-1;    //  2KHz


  TIM9->CCMR1 |= 0x6<<4;//pwm 1 mode
  //TIM9->CCMR1 |= 0x1<<3; //preload

  TIM9->CCER |= 0x1; //enable

  // 2KHz
  TIM9->CCR1 = (TIM9->ARR)/2; // duty : 50%
  //TIM9->CCR1 = (TIM9->ARR)/5; // duty : 20%

  TIM9->CR1 |= 0x1<<0;

  while (1);
}

1KHz, duty : 20%

로직 애널라이저를 확인할때 채널 번호 확인과,baud rate 체크 필요

Timer 6의 구조와 활용

  1. polling 하며 일정 주기마다 uart 메시지 출력,
  2. 익셉션핸들러(인터럽트) 이용으로 uart 메시지 출력을 해본다.

다음글 - GP(General Purpose)Timer인 Timer 9의 구조와 활용

 

STM32F4XX reference manual에 TIM6에 대해 나와있다.

  • 16비트 업카운터로, 0부터 설정값까지 카운트한다.
  • 동작 흐름 요약
    1. RCC로부터 APB1버스를 통해 TIM6에 클럭이 공급
    2. PSC(분주기) 작동
      • TIM6_PSC의 값만큼 클럭을 나눔
    3. CNT(카운터) 증가
      • PSC에서 분주된 클럭(CK_CNT)에 따라 CNT 값이 0부터 증가
    4. ARR 도달 시 업데이트 이벤트(U event) 발생
      • CNT == ARR 일때 발생
      • 인터럽트, TRGO 트리거 출력, CNT 초기화 가능

STM32F4xx 매뉴얼에 보면 Tim6엔 다양한 설정에 따른 클럭 다이어그램이 있다.
간단한 Counting Mode를 보면

internal clock을 2로 나눈 상황

  1. CNT_EN : Counter 활성화
  2. CK_CNT : CNT_EN 활성화 다음 클럭부터 Clock counter 동작
    internal clock을 2로 나눈 주기로 실행하기 때문에, CK_INT(내부 클럭)가 2번 발생 시 CK_CNT가 1번 동작
  3. Counter overflow : Counter register 값이 설정한 주기(ARR) 값을 넘으면 오버플로우 발생
  4. Update event(UE) : 오버플로우 발생함에 따라 Update Event 발생
  5. Update interrupt flag(UIF) : Update event가 발생함에 따라 Update interrupt flag 활성화(flag가 다시 0으로 되도록 clear 동작이 필요)

기본 동작 순서를 잘 읽어보면, 클럭 다이어그램을 이해할 수 있다.

다음은 prescaler를 사용할때의 다이어그램이다. 보통 시스템 클럭값으로부터 타이머 카운트값이나, Baud rate로 사용할때 원하는 값으로 줄여주기 위해, 스케일링해주는 prescaler이다.

타이머의 입력 클럭이 너무 빠르면, 카운터가 너무 빨리 증가해서 원하는 시간 제어(예: 1초 간격 인터럽트)가 어렵다.
그래서 Prescaler(전치분주기) 를 사용해 타이머 입력 클럭을 나누어 느리게 만들어 준다.

예로는 168000000(168MHz)의 클럭값이 있을때, 오버플로우를 방지하기도하고, 간단한 수로 만들고 사용하고자할때 500000으로 스케일링 해놓고 사용한다.

  • APB1 Timer Clock: 168 MHz
  • Prescaler (PSC): 335
  • ARR (Auto-Reload Register): 999

동작 계산

  1. Prescaler 설정 (PSC = 335)
    → 타이머 입력 클럭 = 168 MHz / (PSC + 1) = 168,000,000 / 336 = 500,000 Hz
  2. ARR 설정 (ARR = 999)
    → 카운터가 0 ~ 999까지 세고 오버플로우 발생 (1000번 count)
  3. 오버플로우 주기
    → 오버플로우 시간 = (ARR + 1) / 타이머 클럭
    → 1000 / 500,000 = 0.002초 (2ms)

=>2ms마다 타이머 인터럽트가 발생.

 

prescaler 값이 1 → 2 가 되는 상황

  1. CNT_EN : count를 활성화하여 다음 클럭부터 clock count 동작
  2. prescaler = 1 이므로 Prescaler buffer는 0, Prescaler counter는 0으로 유지
  3. TIMx_PSC 레지스터를 통해 새로운 prescaler 값을 설정
  4. 다음 update event에서 prescaler buffer= 1 이 되며, Prescaler counter는 2단위(0, 1)로 동작
  5. Prescaler counter에 맞춰 PSC counter가 0일 때 clock count 동작

4.2.2. 관련 레지스터 분석 및 설정 값 이해

  • register boundary address

 

  • RCC_APB1ENR
  •  

 

  • TIMx_CR1
    • Basic Timer의 동작을 설정하는 레지스터

 

  • TIMx_CR2
    Trigger 트리거 관련 레지스터

 

  • TIMx_DIER
    -인터럽트 활성화 레지스터
  • UIE (0번 비트)
    • 0 : 업데이트 인터럽트 비활성화
    • 1 : 업데이트 인터럽트 활성화
  • UDE (8번 비트)
    • 0 : 업데이트 DMA 요청 비활성화
    • 1 : 업데이트 DMA 요청 활성화
  • TIMx_SR
    -status 레지스터(인터럽트 flag 확인)bit 0 : UIF : 업데이트 인터럽트 플래그 (업데이트 이벤트 발생 시 하드웨어에 의해 flag 1로 설정됨 → clear (0으로) 해주어야 한다.

 

  • TIMx_CNT (counter)
    -현재 카운터 값 저장
  •  

 

  • TIMx_PSC (prescaler)

Bits 15:0 PSC : 타이머의 주기 결정

타이머의 입력 클럭을 나누는 역할 (타이머 클럭 분주)

실제 분주 값 = PSC + 1 (0을 포함해서 카운트하기때문)

 

  • STM32에서 TIM6의 기본 클럭은 84MHz (1초당 84M번의 tick)
  • // 예시 TIM6->PSC = 8400-1; // 84,000,000Hz / (8400-1)+1 = 10kHz로 분주

 

  • TIMx_ARR (auto-reload register)
    • reload value 설정 레지스터
  • 0xFFFF 초과하면 오버플로우
  • 카운터는 0부터 ARR까지 카운팅, 다시 0으로 리셋
// 예시

TIM6->ARR = 5000-1;            // 4999 = 500ms

 

 

  • 타이머 설정 방법(500ms 주기마다 출력)
  • The counter clock frequency CK_CNT is equal to fCK_PSC / (PSC[15:0] + 1).(p.709)
 Prescaler = 8399
 ARR       = 4999

    ⇒ 84,000,000 / (8399 + 1) = 10,000 Hz
    ⇒ 타이머가 0~4999까지 센 후 오버플로우 → 5000 / 10000 = 0.5초 (500ms)

코드 구현-1 Polling으로 500mS 마다 Uart로 ‘.’ 하나씩 출력

void uart3_init(void){
//1.clock 활성화
    RCC->AHB1ENR |= 1<<3;//GPIOD
    RCC->APB1ENR |= 1<<18;//USART3

//2.PD8,9핀 Alternative Function으로 설정
    GPIOD->MODER &= ~((0x3<< (2*8))| (0x3<<(2*9)));
    GPIOD->MODER |=((0x2<<(2*8)) | (0x2<<(2*9)));

//3.MUX AFR uart tx,rx핀으로 설정.
    GPIOD->AFR[1]&= ~(0xf<<4 | 0xf<<0);//MUX PD8,9(AFRH 8~15)
    GPIOD->AFR[1] |= (0x7<<4 | 0x7<<0);//uart3(AF7)

//4.uart 레지스터 설정
    //oversampling by 16(bit 15),UE,wordlength8bit+stop1bit(bit12),parityX(bit10),TE,RE
    USART3->CR1 =(0<<15 |1<<13 | 0<<12 |0<<10 | 1<<3 | 1<<2);
    USART3->BRR=(22 << 4) | 13;//115200bps,42MHZ =356

}

void uart3_send(char c){
    while(!(USART3->SR>>7 &0x1));
    USART3->DR=c;
}

void uart3_printf(const char* str){
    while(*str){
        uart3_send(*str++);
    }
}

int main(void)
{

 HAL_Init();
 uart3_init();
 SystemClock_Config();

//TIM6 clock 활성화
 RCC->APB1ENR |= 1<<4;

//APB1 : 84MHz
int msec=500,pclk=84000000; 

//(7:ARR preload enable,2:update request sources, 1: update enable, 0 : counter reset)
TIM6->CR1=(0x1<<7 | 0x1<<2 | 0x0<<1 | 0x0<<0);

//clk=84000000, PSC(8400-1)+1, CK_CNT=10000, 목표값 : 500ms, 주기 2
TIM6->PSC=8400-1;//prescaler 84000000->10kHz=10000

TIM6->ARR=5000-1;//5000=2Hz (int)(pclk/8400)/2)-1 reload
//TIM6->ARR=10*msec-1; // msec으로 표현
TIM6->CNT=0;//0부터count시작

//count start
TIM6->CR1 |= 0x1<<0;

char msg[100];

  while (1)
  {

      if(TIM6->SR & 0x1){//(UIF)Update interrupt flag
          uart3_printf(".");
          TIM6->SR =0x0;
      }
      /*
      sprintf(msg,"%d ",TIM6->CNT);//count출력
      uart3_printf(msg);
*/
  }

}

 

 

코드 구현-2 Exception(Interrupt) 활용하여 500mS 마다 Uart로 ‘#’ 하나씩 출력

startup 벡터 테이블에 등록되어 있는 TIM6_DAC_IRQHandler 이름을 사용해야 함.

void uart3_init(void){
    RCC->AHB1ENR |= 1<<3;//GPIOD
    RCC->APB1ENR |= 1<<18;//USART3

    //PD8,9 AF모드
    GPIOD->MODER &= ~((0x3<< (2*8))| (0x3<<(2*9)));
    GPIOD->MODER |=((0x2<<(2*8)) | (0x2<<(2*9)));

    GPIOD->AFR[1]&= ~(0xf<<4 | 0xf<<0);//MUX PD8,9(AFRH 8~15)
    GPIOD->AFR[1] |= (0x7<<4 | 0x7<<0);//uart3

    USART3->CR1 =(0<<15 |1<<13 | 0<<12 |0<<10 | 1<<3 | 1<<2);
    USART3->BRR=(22 << 4) | 13;//115200bps,42MHZ =356

}

void uart3_send(char c){
    while(!(USART3->SR>>7 &0x1));
    USART3->DR=c;
}

void uart3_printf(const char* str){
    while(*str){
        uart3_send(*str++);
    }
}

char msg[100];

void TIM6_DAC_IRQHandler(void)
{

    if (TIM6->SR & (1 << 0)) {
        uart3_printf("#");
        TIM6->SR = ~(1 << 0); // UIF 비트 리셋
    }

}

int main(void)
{
 HAL_Init();
 uart3_init();
 SystemClock_Config();

  RCC->APB1ENR |= 1<<4; //TIM6 clock

  int msec=300,pclk=84000000; //APB1 : 84MHz
  //(7:ARR preload enable,2:update request sources, 1: update enable, 0 : counter reset)
  TIM6->CR1=(0x1<<7 | 0x1<<2 | 0x0<<1 | 0x0<<0);

  //clk=84000000, PSC(8400-1)+1, CK_CNT=10000, 목표값 : 500ms, 주기 2
  TIM6->PSC=8400-1;//prescaler 84000000->10kHz=10000

  TIM6->ARR=5000-1;//5000=2Hz

  TIM6->CNT=0;//start value

  TIM6->DIER=0x1<<0;//Interrupt enable

  //NVIC에 TIM6_DAC_IRQ 번호 등록하기
  NVIC->ISER[1]|=0x1<<22; //54번 IRQ. ISER[1] 32~63

  //count start
  TIM6->CR1 |= 0x1<<0;

  while (1)
  {

      if(TIM6->SR & 0x1){//(UIF)Update interrupt flag
          uart3_printf(".");
           TIM6->SR &= ~(1 << 0);
      }
  }
  /* USER CODE END 3 */
}

NUCLEO-STM32F429ZI 보드를 사용했습니다.
(STM32F429 discovery 보드는 핀 번호가 달라서 핀 번호 설정만 바꿔서 하면 된다.)



IWDG 이해


iwdg은 카운트 다운을 하며, 일정신호를 주기적으로 보내는 정상 작동이 아닌 경우, 카운트 값을 갱신하지 않고 카운팅을 계속하여 0이 되면 시스템을 리셋한다.

iwdg은 내부의 LSI클럭을 이용하는 독립된 구조이다. RCC와 연결된 CSR이라는 레지스터로 LSI클럭을 onoff하는 비트가 있는데, 이걸로 IWDG의 실행에 영향을 주지 않는다.
iwdg는 한 번 CCCC로 enable되면, 끌 수 없다.
ST의 다른 칩들에서는 desable할 수도 있는데 이 보드는 끌 수없도록 설계되었다고 볼 수 있다.

WWDG와 다른점은 메인 클럭이 고장나도 독립적으로 작동을 유지한다는 것이다.

iwdg 레지스터제어로 구현해보기


다이어그램에 있는 iwdg의 작동을 제어하는 레지스터를 레퍼런스에서 찾아본다.

1. IWDG->KR(key register)


IWDR->KR에 5555값을 주면 PR(prescale),RLR(reload)레지스터에 접근할 수 있다.
왜 이런 방식으로 접근하는 이유는 실수로 watchdog 제어에 관련된 레지스터에 접근하는 것을 방지하는 것이다.
CCCC : iwdg 카운트 다운 시작.
AAAA : iwdg의 reload(카운트값)을 갱신. (정상작동 시 리셋되지않도록 계속 갱신해준다)





2. IWDG->PR(Prescaler register)


LSI클럭을 사용하는 iwdg의 클럭 값을 prescale로 divide해줄 수 있다. 표에 맞게 설정하면 된다.



3. IWDG->RLR(Reload register)


카운트 값을 12비트, 즉 4095내로 넣어준다.
그럼 0까지 down count한다.



4. RCC->CSR(clock control, status register)

 


iwdg reset에 의해 flag되는 29번 비트를 24번 remove비트로 지워줄 수 있다. 이 두 개를 사용해서 코드를 구현해본다.


근데 LSI osc와 관련된 비트가 있는데 이걸 해봤더니, 소용이 없다. 왜냐면 앞에서 레퍼런스에 나와있듯, iwdg이 한 번 실행되면 LSI클럭을 끌 수 없다. 그리고 이 레지스터에서의 0번 비트의 reset value도 0인데, 1로 on해주지 않고도 iwdg카운트를 시작해도 된다.
처음에 레지스터에 이 비트때문에 클럭 on해줘야하는건지 헷갈렸다. iwdg은 RCC의 경로와 다른 내부의 LSI클럭을 가지는 독립된 구조이다.

RCC→ CSR에 LSI클럭을 onoff하는 비트가 있지만, iwdg의 경우, 클럭 on off에 영향 받지 않고 동작한다. Iwdg은 시스템의 안전을 위한 장치이기 때문에, 소프트웨어 설정으로 멈출 수 없고, 한 번 시작되면 오직 reset에 의해서만 멈춘다.

코드 구현

iwdg에 의한 리셋을 확인해보는 코드이다.
uart3으로 Tera Term으로 메세지 출력해서 확인해보았다.

//처음엔 first try 출력, 두번째부터 reset by IWDG2 출력
void uart3_init(void){
    RCC->AHB1ENR |= 1<<3;//GPIOD
    RCC->APB1ENR |= 1<<18;//USART3

    //PD8,9 AF모드
    GPIOD->MODER &= ~((0x3<< (2*8))| (0x3<<(2*9)));
    GPIOD->MODER |=((0x2<<(2*8)) | (0x2<<(2*9)));

    GPIOD->AFR[1]&= ~(0xf<<4 | 0xf<<0);//MUX PD8,9(AFRH 8~15)
    GPIOD->AFR[1] |= (0x7<<4 | 0x7<<0);//uart3

    USART3->CR1 =(0<<15 |1<<13 | 0<<12 |0<<10 | 1<<3 | 1<<2);
    USART3->BRR=(22 << 4) | 13;//115200bps,42MHZ =356

}

void uart3_send(char c){
    while(!(USART3->SR>>7 &0x1));
    USART3->DR=c;
}

void uart3_printf(const char* str){
    while(*str){
        uart3_send(*str++);
    }
}

char msg[100];

void iwdg_init(void){
    IWDG->KR=0x5555;//enable access to PR,RLR
    IWDG->PR |=0x3; //divided by 32
    IWDG->RLR= 4096-1; //count load
    IWDG->KR=0xAAAA;//update
    IWDG->KR=0xCCCC;//start watchdog
    //Timeout = (4095 + 1) * 32 / 32000 = 4.096 sec
}

int main(void)
{
 HAL_Init();
  SystemClock_Config();
  uart3_init();

  if((RCC->CSR)&(0x1<<29)){
                  sprintf(msg,"%s","reset by IWDG2 ");
                  uart3_printf(msg);
                  RCC->CSR |= (0x1<<24);//리셋 Flag clear
                   while(1);
              }else{
                  sprintf(msg,"%s","first try");//IWDG 실행 전 리셋이 아직 되지않았을 때를 출력해서 확인.
                  uart3_printf(msg);
              }


  iwdg_init();
  while (1);//기다리다 타임 아웃 후 리셋되면 main다시 실행



}

보드에 있는 두 버튼 중 리셋 버튼을 누르면서 테스트 해본다. 보드 전원 연결(usb)을 해제했다가 다시 로드하고 해도된다.

+ Recent posts