Wiring Pi - PWM.c 분석
#include <wiringPi.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main (void)
{
int bright ;
printf ("Raspberry Pi wiringPi PWM test program\n") ;
if (wiringPiSetup () == -1)
exit (1) ;
pinMode (1, PWM_OUTPUT) ;
for (;;)
{
for (bright = 0 ; bright < 1024 ; ++bright)
{
pwmWrite (1, bright) ;
delay (1) ;
}
for (bright = 1023 ; bright >= 0 ; --bright)
{
pwmWrite (1, bright) ;
delay (1) ;
}
}
return 0 ;
}
해당 코드가 pwm의 예제 코드이며, 이 코드에서 다시 pinMode부터 분석해보려고 한다.
주요 분석 코드로 pinMode, pwmWrite 를 분석 할 예정이다.
1. pinMode(int, int)
사실, 이 코드에서 제일 헷갈리는 부분이다. 왜냐면..
if (모드가 Input일때)
if()~
else if ~
else if ~
else if ~
..
이런 식으로, 정말 헷갈리게 적혀있어서 어렵다!!
그래서, 동작하기에 필요한 부분을 다 빼고 조건문만 정리해보았더니 다음과 같다.
① 입력 관련
if (INPUT==mode || PM_OFF==mode) {
if (piRP1Model())
{
if (INPUT==mode){ ... }
else { //PM_OFF ... }
}
else { //GPIO 세팅 }
if (PM_OFF==mode && !usingGpioMem && pwm && gpioToPwmALT[pin]>0)
{
//PWM pin -> reset
if (channel>=0 && channel<=3 && piRP1Model()) { ... }
}
}
② 출력 관련 (지난번에 봐서, 대강 넘어감)
else if (mode == OUTPUT) {...}
③ Soft_PWM(CPU 사용 PWM), Soft_Tone(소리출력용 주파수 등 설정용), PWM_Tone(하드웨어 PWM)
else if (mode == SOFT_PWM_OUTPUT)
{
softPwmCreate (origPin, 0, 100) ;
}
else if (mode == SOFT_TONE_OUTPUT)
{
softToneCreate (origPin) ;
}
else if (mode == PWM_TONE_OUTPUT)
{
pinMode (origPin, PWM_OUTPUT) ; // Call myself to enable PWM mode
pwmSetMode (PWM_MODE_MS) ;
}
④ PWM
이때, RP1Model 함수에 따라, 라즈베리파이 5에 해당하는 함수를 확인한다.
라즈베리파이 5가 아닌 경우, 기존 방식대로 pwm을 설정해주게 된다!
else if (PWM_OUTPUT == mode || PWM_MS_OUTPUT == mode || PWM_BAL_OUTPUT == mode)
{
if (0 == alt) { // Not a hardware capable PWM pin
// 하드웨어 PWM이 지원되지 않는 핀이면 아무 동작도 안 함.
}
int channel = gpioToPwmPort[pin];
if (piRP1Model()) { // ✅ 라즈베리파이 RP1 칩셋 (라즈베리파이 5)인지 확인
if (channel >= 0 && channel <= 3) {
// PWM 채널 설정 (마크/스페이스 모드 사용)
pwm[RP1_PWM0_CHAN_START + RP1_PWM0_CHAN_OFFSET * channel + RP1_PWM0_CHAN_CTRL] =
(RP1_PWM_TRAIL_EDGE_MS | RP1_PWM_FIFO_POP_MASK);
// PWM 글로벌 활성화
unsigned int ctrl = pwm[RP1_PWM0_GLOBAL_CTRL];
pwm[RP1_PWM0_GLOBAL_CTRL] = ctrl | (1 << channel) | RP1_PWM_CTRL_SETUPDATE;
// GPIO 모드를 PWM 출력 모드로 변경
pads[1 + pin] = RP1_PAD_DEFAULT_FROM9; // 출력 활성화
pinModeAlt(origPin, alt); // PWM 모드로 설정
}
}
else { // ✅ 라즈베리파이 4 이하 모델의 PWM 설정 방식
// 해당 핀을 PWM 모드로 설정
*(gpio + fSel) = (*(gpio + fSel) & ~(7 << shift)) | (alt << shift);
delayMicroseconds(110); // 클럭 안정화를 위해 대기
if (PWM_OUTPUT == mode || PWM_BAL_OUTPUT == mode) {
pwmSetMode(PWM_MODE_BAL); // 균형 모드 (Balanced Mode, 기본값)
}
else {
pwmSetMode(PWM_MODE_MS); // 마크/스페이스 모드 (Mark-Space Mode)
}
}
if (PWM_OUTPUT == mode) { // ✅ 기본 PWM 설정
pwmSetRange(1024); // 기본 PWM 범위를 1024로 설정
pwmSetClock(32); // 19.2MHz / 32 = 600kHz (기본 PWM 주파수 설정)
}
}
⑤ GPIO_CLOCK
else if (mode == GPIO_CLOCK) { ... }
여기까지 해서, 우선 영역을 구분했다.
그렇다면, PWM 모드는 어떤 것들이 있는 것일까?
PWM_MODE_BAL (Balanced Mode, 기본값)
- PWM 파형을 균등하게 배분하여 부드러운 출력 가능
- 주파수 변화가 있을 때 신호 왜곡이 적음
PWM_MODE_MS (Mark-Space Mode)
- 마크(ON)와 스페이스(OFF) 비율을 정밀하게 제어할 수 있음
- 서보 모터 제어나 특정 하드웨어에서 요구하는 신호에 적합
그렇다면, PWM_TONE_OUTPUT과의 차이점은?
PWM_OUTPUT은 정확한 신호를 제공하는 하드웨어 PWM
PWM_TONE_OUTPUT은 소프트웨어 방식으로 신호를 생성하는 기능이다.
구분 | PWM_OUTPUT / PWM_MS_OUTPUT / PWM_BAL_OUTPUT | PWM_TONE_OUTPUT |
사용 방식 | 하드웨어 PWM을 사용 | GPIO를 톤(주파수) 출력 모드로 설정 |
주요 용도 | LED 밝기 조절, 모터 속도 제어 | 사운드 발생 (부저, 스피커 제어) |
신호 형태 | 듀티 사이클 기반 | 특정 주파수의 신호 출력 |
정확성 | 하드웨어 PWM이므로 정확도가 높음 | 소프트웨어 방식이라 정확도가 낮을 수 있음 |
지원 핀 | 라즈베리파이에서 일부 GPIO 핀만 하드웨어 PWM 지원 | 모든 GPIO 핀에서 사용 가능 |
설정 방식 | pwmWrite(pin, value); | softToneWrite(pin, frequency); |
최종 정리
PWM_OUTPUT | 기본 PWM (균형 모드) |
PWM_MS_OUTPUT | 마크/스페이스 PWM (서보 모터 등) |
PWM_BAL_OUTPUT | 균형 모드 PWM (라즈베리파이 기본 설정) |
PWM_TONE_OUTPUT | 주파수 생성 (소리 발생) |
라즈베리파이 5에선...
라즈베리파이 5에서는 새로운 칩셋으로 인해, 새로운 레지스터로 수행해야 한다.
PWM을 제어하려면 RP1_PWM0_GLOBAL_CTRL 및 RP1_PWM0_CHAN_CTRL 등의 새로운 레지스터를 사용하게 되며, 주요 레지스터는 다음과 같다.
레지스터 | 설명 |
RP1_PWM0_GLOBAL_CTRL | PWM 전체를 활성화하는 전역 제어 레지스터 |
RP1_PWM0_CHAN_CTRL | 각 PWM 채널을 개별적으로 제어하는 레지스터 |
RP1_PWM0_CHAN_START | PWM 신호 시작 지점 설정 |
RP1_PWM0_CHAN_OFFSET | 각 PWM 채널 간의 오프셋 |
RP1_PAD_DEFAULT_FROM9 | 특정 GPIO 핀을 PWM 모드로 설정 |
RP1_PWM_CTRL_SETUPDATE | PWM 설정을 즉시 반영하는 플래그 |
+-----+-----+---------+------+---+---Pi 5---+---+------+---------+-----+-----+
| BCM | wPi | Name | Mode | V | Physical | V | Mode | Name | wPi | BCM |
+-----+-----+---------+------+---+----++----+---+------+---------+-----+-----+
| | | 3.3v | | | 1 || 2 | | | 5v | | |
| 2 | 8 | SDA.1 | - | 0 | 3 || 4 | | | 5v | | |
| 3 | 9 | SCL.1 | - | 0 | 5 || 6 | | | 0v | | |
| 4 | 7 | GPIO. 7 | - | 0 | 7 || 8 | 0 | - | TxD | 15 | 14 |
| | | 0v | | | 9 || 10 | 0 | - | RxD | 16 | 15 |
| 17 | 0 | GPIO. 0 | - | 0 | 11 || 12 | 0 | - | GPIO. 1 | 1 | 18 |
| 27 | 2 | GPIO. 2 | - | 0 | 13 || 14 | | | 0v | | |
| 22 | 3 | GPIO. 3 | - | 0 | 15 || 16 | 0 | - | GPIO. 4 | 4 | 23 |
| | | 3.3v | | | 17 || 18 | 0 | - | GPIO. 5 | 5 | 24 |
| 10 | 12 | MOSI | - | 0 | 19 || 20 | | | 0v | | |
| 9 | 13 | MISO | - | 0 | 21 || 22 | 0 | - | GPIO. 6 | 6 | 25 |
| 11 | 14 | SCLK | - | 0 | 23 || 24 | 0 | - | CE0 | 10 | 8 |
| | | 0v | | | 25 || 26 | 0 | - | CE1 | 11 | 7 |
| 0 | 30 | SDA.0 | IN | 1 | 27 || 28 | 1 | IN | SCL.0 | 31 | 1 |
| 5 | 21 | GPIO.21 | - | 0 | 29 || 30 | | | 0v | | |
| 6 | 22 | GPIO.22 | - | 0 | 31 || 32 | 0 | - | GPIO.26 | 26 | 12 |
| 13 | 23 | GPIO.23 | - | 0 | 33 || 34 | | | 0v | | |
| 19 | 24 | GPIO.24 | - | 0 | 35 || 36 | 0 | - | GPIO.27 | 27 | 16 |
| 26 | 25 | GPIO.25 | - | 0 | 37 || 38 | 0 | - | GPIO.28 | 28 | 20 |
| | | 0v | | | 39 || 40 | 0 | - | GPIO.29 | 29 | 21 |
+-----+-----+---------+------+---+----++----+---+------+---------+-----+-----+
| BCM | wPi | Name | Mode | V | Physical | V | Mode | Name | wPi | BCM |
+-----+-----+---------+------+---+---Pi 5---+---+------+---------+-----+-----+
이때 내 라즈베리파이의 gpio readall 결과는 위와 같고, 일반적으로 지원하는 핀은 다음과 같다.
PWM0 채널 0 | GPIO 12 | Pin 32 |
PWM0 채널 1 | GPIO 13 | Pin 33 |
PWM1 채널 0 | GPIO 18 | Pin 12 |
PWM1 채널 1 | GPIO 19 | Pin 35 |
2. pwmWrite(int, int)
void pwmWrite (int pin, int value)
{
...
if ((pin & PI_GPIO_MASK) == 0) // On-Board Pin
{
//대강 핀 모드에 맞게 핀 값 세팅
if (piRP1Model()) { //라베파 5인 경우
if (channel>=0 && channel<=3) {
unsigned int addr = RP1_PWM0_CHAN_START+RP1_PWM0_CHAN_OFFSET*channel+RP1_PWM0_CHAN_DUTY;
pwm[addr] = value; // ✅ RP1 PWM 채널의 듀티 사이클 설정
readback = pwm[addr];
}
}
else {
*(pwm + channel) = value ; // ✅ 기존 BCM 방식으로 PWM 듀티 사이클 설정
readback = *(pwm + channel);
}
}
else //확장 GPIO 설치 한 경우
{
if ((node = wiringPiFindNode (pin)) != NULL)
node->pwmWrite (node, pin, value) ;
}
}
라즈베리파이 4 이전(BCM2711)까지에서는 pwm + channel 주소에 직접 값을 쓰는 방식으로 PWM 설정하며,
라즈베리파이 5 이후부턴 기존 BCM 방식과 다르게, PWM 값을 전용 레지스터(RP1_PWM0_CHAN_DUTY)에 직접 기록하여 제어하고, PWM 설정 후, 값을 다시 읽어 확인 (readback = pwm[addr])
그렇다면 readback은 무엇일까?
- *(pwm + channel) = value; → PWM 레지스터(PWM_DAT)에 값을 한 번 저장
- readback = *(pwm + channel); → 저장된 값이 반영되었는지 확인
- 이후에는 이 값이 하드웨어 PWM 회로에 의해 자동으로 사용
그렇다면 하드웨어 PWM 회로는 어떤 과정으로 동작하는거지? 싶어서 확인해보니 다음과 같다.
- PWM_CTL 레지스터에서 PWM 기능을 활성화
- PWM_RNG 레지스터에서 PWM 주기(범위)를 설정
- PWM_DAT 레지스터 값을 하드웨어 타이머가 지속적으로 참조
- PWM 신호는 타이머가 계속 동작하면서 자동으로 생성
- PWM_DAT 값이 변경되지 않는 한, PWM 신호는 변하지 않고 유지
하드웨어 동작 과정의 예시는 다음과 같다.
시간 (t) | PWM 카운터 값 (0 ~ 1023) | PWM_DAT 값 (512) | 출력 상태 (GPIO 18) |
t = 0 | 0 | 512 | HIGH (ON) |
t = 256 | 256 | 512 | HIGH (ON) |
t = 511 | 511 | 512 | HIGH (ON) |
t = 512 | 512 | 512 | LOW (OFF) |
t = 768 | 768 | 512 | LOW (OFF) |
t = 1023 | 1023 | 512 | LOW (OFF) |
t = 0 (다음 사이클 시작) | 0 | 512 | HIGH (ON) |
이런식으로, pwm 카운터를 정해두고 pwm 데이터와 비교하며 출력상태를 조절하게 된다.
그러면 어떻게 관리하는 것이고 어떻게 차이를 두는 것일까?
이건.. GPT에게 물어보았다.
┌──────────────────────────┐
│ PWM Counter │ (0 → 1 → 2 → ... → PWM_RNG)
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ Comparator (>) │ (현재 Counter 값과 PWM_DAT 값 비교)
└────────────┬─────────────┘
│
┌────────┴────────┐
│ Logic Gates │ (AND, OR 등으로 출력 결정)
└────────┬────────┘
│
▼
PWM Output (HIGH / LOW)
┌───────────────────────────┐
Counter ──▶│ 비교기: (Counter < DAT) │─── AND 게이트 ──▶ PWM 출력 (HIGH/LOW)
└───────────────────────────┘
대강 이런 식으로 진행되는 것!
사실 집중이 크게 되지 않아서.. 이상하게 정리 된 부분도 있을 것 같다...
그러니, 만약 보고 이상하다면 알려주신다면 매우 감사!! ㅎㅎㅎ