LED Dimming을 하기 위해서는 먼저 PWM을 생성해야 한다. 

 

PWM이란 무엇인가?

Pulse Width Modulation으로 쉽게 설명하면 펄스를 만들어 제어한다고 생각하면 될 것 같다.

PWM을 사용하려면 타이머를 설정해주어야한다. 

 

먼저 보드에서 PWM을 출력할 수 있는 단자를 확인하자

TIM3의 채널 1번이 CN4의 D4포트를 이용하여 사용할 수 있다는 것을 알 수 있다. 

Timers항목의 TIM3의 Channel1을 PWM Generation CH1로 선택한다. 

 

PWM의 주파수와 주기 등을 설정해야하는데 다음과 같은 항목들을 조정하여 가능하다.

 

먼저 사용한 클럭을 알아야하는데 클럭은 32MHz로 선택을 하였었다. (Clock Configuration 에서 설정 및 확인 가능)

 

그 다음 Prescaler , Counter Period, Pulse 등을 설정해야한다. 

Prescaler란?

단순히 생각하면 우리가 사용하는 타이머 클럭을 적정한 수로 나누어 사용하기 쉬운 값으로 변환하는 값이라고 생각하면 된다. 

예를 우리의 타이머 클럭은 32MHz인데 이는 너무 큰 주파수 값이므로

Prescaler를 32로 하면 1MHz , 320으로 하면 1000KHz, 3200으로 하면 100KHz등의 주파수로 변환할 수 있게된다. 

 

Counter Period?

Prescaler를 통해서 낮춘 타이머 클럭 펄스마다 타이머의 Counter가 1씩 증가된다.

설정한 Counter Period에 도달하면 Counter Period는 0이 되고 하나의 PWM 펄스를 출력한다.  

이렇게 Counter Period를 증가시키고 0을 만들고 반복하며 PWM 펄스를 만들 수 있다. 

 

클럭이 32Mhz일 때, 1Khz의 PWM을 만들고 싶다면?

1. Prescaler = 32, Counter Period = 1000 인 경우

- 타이머 클럭을 Prescaler로 나누면  32MHz / 32 = 1 MHz,  1/1000000초 마다 Count가 1씩 증가

- Counter Period가 1000이므로 1000까지 도달하는데 걸리는 시간은 1/1000000 * 1000 = 1/1000초

- Counter Period를 0으로 만들고 다시 Count 증가를 반복하며 PWM 펄스 출력

- Counter Period가 1000이되는데 걸리는 시간인 1/1000초 

- 1/1000초, 즉 1Khz마다 PWM 펄스를 출력 - > 1Khz의 PWM 생성

 

2. Prescaler = 3200, Counter Period = 10 인 경우

- 타이머 클럭을 Prescaler로 나누면  32MHz / 3200 = 10khz,  1/10000초 마다 Count가 1씩 증가

- Counter Period가 10이므로 10까지 도달하는데 걸리는 시간은 1/10000 * 10 = 1/1000초

- Counter Period를 0으로 만들고 다시 Count 증가를 반복하며 PWM 펄스 출력

- Counter Period가 1000이되는데 걸리는 시간인 1/1000초 

- 1/1000초, 즉 1Khz마다 PWM 펄스를 출력 - > 1Khz의 PWM 생성

 

등등  Prescaler와 Counter Period를 조정하여 PWM의 주기를 설정할 수 있다. 

 

1번 상황을 이용하여 PWM 세팅을 하였다.

Prescaler와 Count Period 모두 0부터 시작하므로 원하는 값에서 1을 빼주었다.

 

이제 PWM의 Duty를 설정해야한다. 

PWM의 Duty 는 PWM 펄스가 발생하였을 때, 얼마만큼 High의 상태로 유지할 것인가? 라고 생각하면 편할 것 같다. 

Pulse 항목에서 조절이 가능한데 Pulse의 범위는 Counter Period를 넘지 않도록 한다. 

쉽게 생각해서 타이머 클럭이 Counter Period를 Count할 때, 몇개의 클럭 펄스까지 PWM을 High로 할 것인가? 라고 보면 된다. 

즉 Counter Period를 1000, Pulse를 500으로 하면, Duty가 50%가 된다.  

Prescaler 32, Counter Period 1000, Pulse 500 으로 실제 Duty비가 50%인 1kHz가 만들어지는지 확인하기 위해서

Code Generation하여 코드를 생성해보자.

 

타이머를 초기화하고 HAL_TIM_PWM_Start()함수를 쓰면 PWM이 생성된다. 

HAL_TIM_PWM_Start 함수

 

main의 초기화 부분

실제 오실로스코프를 연결하여 확인하여 보자

(TIM3_CH1을 출력하는 CN4의 D3와 GND를 찍어서 확인하자)

1khz의 PWM이 잘 생성된 것을 확인할 수 있다. 

이 PWM을 LED에 연결하면... 

(저항은 220옴이 없어서 600옴 저항을 병렬로 연결하여 사용하였다)

1khz, 즉 1ms의 주기에 Duty가 50%이므로 0.5ms 간격으로 켜졌다 꺼졌다 하지만 사람 눈에는 계속 켜져있는 것으로 보인다.

 

이제 PWM을 만들었고, LED에 불도 켰으니...

PWM의 Duty를 요리조리 만지면 LED Dimming을 할 수 있을 것 같다. 

언제? 다음글에....

 

반응형

LED를 버튼으로도 제어를 할 수 있게 되었다. 

https://pilimage.tistory.com/17

 

[C/STM32] 3. LED ON/Off - 버튼 채터링 추가

버튼을 누르고 있으면 LED가 On되고 버튼을 떼면 LED가 Off되는 것은 이제 쉽다. https://pilimage.tistory.com/16 [C/STM32] 2. LED ON/OFF - 버튼 제어 추가 1초 간격으로 LED를 On/off를 하는 것은 이제 쉽다...

pilimage.tistory.com

 

버튼을 손으로 누르는 것 말고 PC로 제어를 할 수 있을까? 

당연히 가능하다.

 

Uart 통신을 이용하여 on이 들어오면 LED가 켜지고, off가 들어오면 LED가 꺼지도록 해보겠다. 

 

USB 컨버터가 없어도 STM32F746은 문제 없다. 

우리에겐 전원 겸 디버깅 용인 ST-LINK가 있다. 

ST-LINK에 VCP포트가 있다.

VCP란 Virtual COM Port로 USB를 통해 PC와 시리얼 통신을 가능하게 해준다.

그러므로 ST-LINK의 VCP_TX/RX를 Uart로 이용하면 된다. 

회로도에서 VCP_TX는 PA9 , VCP_RX는 PB7 인 것을 확인하고 해당 번호의 핀을 클릭하여 각각 USART1_TX, USART1_RX로 선택한다. (선택시 해당 핀 노란색으로 변화)

이제 핀 맵 왼쪽의 Connectivity 항목에서 USART1을 선택하고 Mode를 Asynchronous(비동기)로 선택한다. 

파라미터는 기본적으로 Baudrate는 115200 , word length는 8 bits , Paritiy 는 None , Stop Bits는 1로 설정한다.

그리고 Code Generation을 누르면 환경 설정은 끝이난다. 

 

그리고 이제 다음 상황을 생각하여 소스를 작성하였다.

a. on이 입력되면 LED가 켜지고 off가 들어오면 LED가 꺼지도록 함

b.  on / off 만 정확히 입력되어야 LED 제어, 그외의 입력은 무시 

 -> ex) onnnnn, offff, onoff 등은 동작 무시

c.  테스트에 minicom을 사용하여 키보드로 엔터를 입력할 시 개행문자로 \r (CR)로 입력받음

 -> 터미널마다, 운영체제마다 엔터를 눌렀을 때, 개행문자가 \r :  (CR), \n : (LF) , \r\n : (CRLF)로 다르게 입력

d.  minicom을 이용한 터미널에서 입력한 글씨를 출력하고 가독성을 위해 \n을 추가하여 Uart Transmit을 사용

 

1. HAL_UART_Receive 함수를 사용하여 rcv_data라는 변수에 1바이트씩 데이터를 받아 data_arr라는 버퍼에 저장시켰다. 

2. 받은 데이터가 '\r'이면 문자열의 끝을 알려주는 \0을 추가하였다.

3. strcmp를 사용하여 정확히 on , off만 입력되었을 때 기능이 동작하도록 하였다. 

4. 입력받은 데이터에 \n을 붙여 다시 pc로 전송하여 터미널에서 입력한 데이터를 볼 수 있도록 하였다.

5. 문자열 비교가 끝나면 다음 입력을 위해 idx=0으로 하여 버퍼를 재사용할 수 있도록 한다.

 

위의 소스코드대도 빌드하면 on이 입력되면 LED가 켜지고, off가 입력되면 LED가 꺼진다. 

 

원하는 대로 동작을 시켰지만 특정상황에서만 (\r로 개행될 때) 동작하는 것이 좀 찜찜할 수 도 있다. 

그 예로, echo on > /dev/ttyACM0 처럼 에코를 이용하여 on 또는 off를 보내면 \n이 마지막에 보내져서 기능이 동작하지 않는다..

 

그래서 \r이나 \n을 만나면 판단을 하도록 하였다. 

on인지 off인지 비교하는 부분에서 strcmp대신 strncmp를 이용하여 정해진 길이만큼만 비교하여 on인지 off인지 구분하였다. 

strncmp를 사용하여 문자열의 처음부분만 비교를 하다보니 onoff나 ontime 등도 on으로 인식한다는 문제가 있다.

strstr로 문자열을 검색하는 것 보다는 처음부분 비교하는게 좀 더 나을것 같아서 strncmp를 이용하였다. 

 

on/off를 입력하여 LED도 제어를 해보았다. 

이번 과정에서 가장 큰 문제점은 무엇일까?

 

메인의 while문에 HAL_UART_Receive를 사용하였기에 데이터가 들어올 때까지 timeout(위에서는 100ms)의 시간동안 대기한다. 

즉, 계속해서 Uart를 체크하는 Polling방식을 사용하였기 때문에 데이터가 들어올때까지 대기하는 시간동안은 버튼을 눌러도 동작하지 않는다.  HAL_UART_Receive의 timeout 시간을 길게하면 확실히 볼 수 있다.

 

이러한 현상을 막는 방법은 없을까?

당연히 있다. 

 

바로 인터럽트를 이용하는 방법이다. 

인터럽트를 사용한 방법은 다음 글로...

 

반응형

Linux에서 Golang 으로 GPIO를 제어해보았다. 

 

GPIO란? GPIO(General Purpose Input Output)는 일반적인 용도의 입출력 포트를 의미한다. 

 

NewGPIO로 구조체를 만들고, Pin(string)으로 제어할 GPIO 핀번호를 정한다. 

Out , In 으로 dir을 정하고 , High, Low로 값을 정하고, PinRead 로 해당 GPIO 값이 0인지 1인지 읽을 수 있다. 

PinUnexport로 사용을 종료할 수도 있다. 

 

핀을 초기화하고, 방향과 Low,High를 정해 사용할 수 있다. 

예를 들면

g:=NewGPIO()

g.Pin("1")

g.Out().High()

g.In().Low() 등등 

 

전체코드는 다음과 같다.

// gpio.go
package gpio

import (
	"io/ioutil"
	"log"
	"os"
)

const (
	gpioBasePath     = "/sys/class/gpio"
	gpioExportPath   = "/sys/class/gpio/export"
	gpioUnexportPath = "/sys/class/gpio/unexport"
)

type GPIO struct {
	pin string
}

func NewGPIO() GPIO {
	return GPIO{}
}

func (g GPIO) Pin(pin string) GPIO {
	g.pin = pin
	if _, err := os.Stat(gpioBasePath + "/gpio" + g.pin); os.IsNotExist(err) {
		err := ioutil.WriteFile(gpioExportPath, []byte(g.pin), 0666)
		if err != nil {
			log.Println(err)
		}
	}
	return g
}

func (g GPIO) Out() GPIO {
	err := ioutil.WriteFile(gpioBasePath+"/gpio"+g.pin+"/direction", []byte("out"), 0666)
	if err != nil {
		log.Println(err)
	}
	return g
}

func (g GPIO) In() GPIO {
	err := ioutil.WriteFile(gpioBasePath+"/gpio"+g.pin+"/direction", []byte("in"), 0666)
	if err != nil {
		log.Println(err)
	}
	return g
}

func (g GPIO) High() bool {
	err := ioutil.WriteFile(gpioBasePath+"/gpio"+g.pin+"/value", []byte("1"), 0666)
	if err != nil {
		log.Println(err)
		return false
	}
	return true
}

func (g GPIO) Low() bool {
	err := ioutil.WriteFile(gpioBasePath+"/gpio"+g.pin+"/value", []byte("0"), 0666)
	if err != nil {
		log.Println(err)
		return false
	}
	return true
}

func (g GPIO) PinRead(pin string) byte {
	value, err := ioutil.ReadFile(gpioBasePath + "/gpio" + pin + "/value")
	if err != nil {
		log.Println(err)
	}

	return value[0] - 48
}

func (g GPIO) PinUnexport(pin string) bool {
	err := ioutil.WriteFile(gpioUnexportPath, []byte(pin), 0666)
	if err != nil {
		log.Println(err)
		return false
	}
	return true
}

끝 !

반응형

+ Recent posts