이번장에서는 GPIO의 단순 bit I/O기능을 이용한 실습을 해보자.
LED를 켜고 끄는 예제인데 단순히 껐다, 켰다하는건 이전장에서 많이 해봤으므로 여기서는 여러개의 LED를 배열해 좌우로 왔다갔다 하도록 만들것이다.
이때 출력 방식은 앞의 장에서는 설명하지 않았던 Open Drain방식을 사용한다.
|
1. 준비물
총 16개의 LED를 사용할 것이나 구성하기 귀찮다면 갯수를 줄여도 상관없다. 일단 16개를 사용한다는 가정하에 설명하겠다.
- LED 16개
- 330Ω 저항 16개
- 푸시버튼 2개
- Bread Board
- 5V 외부 전원
5V외부 전원은 일반 아답터나 요즘 알리에서 많이 팔고 있는 전압조절이 가능한 전원 공급 장치등을 사용하면 된다.
2. 목표
동영상과 같이 LED가 좌우로 왔다갔다 하면서 점멸하고 푸시버튼 두개로 움직이는 속도를 조절할 수 있게 할 것이다.
속도는 총 5단계로 제어할 것이고 푸시버튼을 눌렀을때 현재 속도 레벨만큼 LED 가 켜지고 1초동안 유지하게 할 것이다.
3. Open Drain방식의 출력
앞선 GPIO 글들에서 Open Drain에 대해 설명하지 않았으므로 이번 장에서는 Open Drain방식으로 IO를 설정해서 사용하도록 하겠다.
Open Drain은 외부 회로가 3.3V가 아니어서 STM32의 3.3V출력을 그대로 사용할 수 없는 경우, STM32의 출력 한계를 초과하는 회로에 연결해야 하는 경우 또는 I²2통신같은 Alternate Function 등에 사용된다.
자세한 내용은 다른 블로그나 이 블로그의 Open-Collector/Open-Drain글을 참조하자.
이 장의 진행을 위해 간단히만 설명하자면 Open Drain의 경우 Push-Pull방식과 달리 GPIO출력 신호는 실제 GPIO핀에서 3.3V출력이 나가지 않는다.
단지 신호에 따라 GPIO핀이 GND와 연결되게 하거나 끊거나 할 수만 있다. 따라서 Open Drain방식으로 설정하고 핀의 전압을 측정해봐도 전압은 나오지 않는다. (다시말해 Pull-down만 되고 Pull-up은 되지 않는다.)
대신 외부에 별도 전원을 추가해서 외부 장치는 이 전원을 사용하게 하고 Low/High신호만 MCU쪽에서 제어를 하는 방식이다.
내부 소자에 따라 Open Collector(BJT), Open Drain(MOSFET)으로 나뉘지만 동작방식은 동일하다. STM32에서는 Open Drain방식을 사용하므로 이 방식대로 오른쪽 그림으로 설명을 하자면, G라고 적힌 Gate(Signal)가 이 우리가 HAL_GPIO_WritePin()등으로 제어하는 신호이고 위쪽 D라고 적힌 Drain이 실제 GPIO 핀에 물려 있다. 그리고 아래 S라고 적힌 Source쪽은 GND로 연결되어 있다. (Drain쪽에 아무것도 연결되어 있지 않아 Open Drain이라 부른다.)
여기서 Gate쪽의 신호 여부에 따라 Drain과 Source가 연결되고 안되고 결정이 된다.
(즉, Gate쪽 신호에 따라 GND쪽으로 Pull-down되거나 플로팅 상태로 있게된다.)
외부 회로는 아래 그림과 같이 외부 전원에 Pull-up저항을 연결해야 Pull-up을 할 수 있고 Drain-Source가 연결되었을때 과도한 전류가 흐르는걸 방지할 수 있다.
STM32의 경우 내부에 P-MOS, N-MOS가 있고 Push/Pull방식은 이 둘을 모두 사용하고, Open-Drain방식인 경우 N-MOS만 사용된다.
N-MOS는 Gate쪽이 High인 경우 Drain->Source간이 연결되고, Low인 경우 연결이 끊어진다.
따라서 Signal이 High인 경우 Vext는 바로 GND로 당겨져 출력은 Low상태가 되며, Signal이 Low인 경우 Drain->Source연결이 끊어지면서 Vext의 전압이 그대로 Pull-up된다.
하지만 여기서 주의할 것이 있다. 위에서 설명한대로 Open-Drain구조는 Output Pin에 아무런 연결이 되어 있지 않은 Open상태라 Signal이 Low인 경우Floating상태이므로 당연히 정상적인 전압은 나오지 않는다.
하지만 CubeMX에서 해당 핀을 설정할 때 GPIO Pull-up/Pull-down 항목에서 "no pull-up and no pull-down"과 "Pull-up"을 선택할 수 있는데 "Pull-up"을 선택할 경우 3.3V출력이 된다.
아래 STM32의 GPIO 구성을 보면
출력이 나갈때 N-MOS와 오른쪽의 Pull-up/Pull-down 회로가 연결되어 있으므로 CubeMX에서 Pull-up을 선택할 경우 Vdd가 인가되어 3.3V출력이 나간다.
따라서 우리는 현재 외부 전원을 사용할 것이기 때문에 내부 Pull-up저항을 사용하지 않을 것이므로 "no pull-up and no pull-down"을 선택해야 한다.
사실 이 프로젝트에서는 굳이 Open Drain을 사용할 필요는 없다.
우리가 사용하는 STM32F429/439의 경우 Datasheet를 보면 출력의 총 전류값이 120mA이내 여야 한다.
그래서 한꺼번에 LED를 16개 연결하는건 문제가 있으므로 이걸 Open Drain으로 구현해서 Low/High 신호는 MCU가 하지만 실제 LED의 전원은 외부전원을 사용하겠다는 의도였다.
그런데 가만히 생각해보니 이 예제에서는 LED 16개가 동시에 켜질 일이 없었다. --;
그냥 Push/Pull방식으로 해도 되지만 그냥 진행하기로 하자.
4. 회로
LED는 외부전원에 각각 병렬로 연결되어 있고 330Ω저항을 통해 GPIO핀과 연결된다.
그리고 외부전원의 GND를 Nucleo보드의 GND와 연결했다.
푸시버튼은 내부 풀다운 저항을 사용했으므로 별도 외부 저항을 달지는 않았다.
5. CubeMX 설정
모든 Open Drain Output은 GPIO_Pull-up/Pull-down 항목을 "No pull-up and no pull-down"으로 설정했으며 푸시버튼 Input은 Pull-down으로 설정했다.
그리고 아래 설정이미지에서와 같이User Label에 출력 16개에 대해 OD01~OD16라는 이름으로 지정했으며 푸시버튼은 SPEED_DOWN, SPEED_UP으로 지정했다.
사용되는 핀 갯수가 많으므로 이후 소스에서 편하게 코딩하기 위해서는 User Label을 설정해주는게 좋다.
6. 코딩
먼저 소스에서 bool형의 변수를 사용할 것이므로 stdbool.h를 include한다.
/* USER CODE BEGIN Includes */
#include <stdbool.h>
/* USER CODE END Includes */
그 다음으로 제어해야 할 GPIO갯수가 많으므로 좀 쉽게 세팅하기 위해 GPIO_TypeDef와 GPIO Pin번호를 매칭시켜 하나로 관리하기 위한 구조체를 하나 정의한다.
/* USER CODE BEGIN PTD */
typedef struct _tagLEDPins
{
GPIO_TypeDef *pGPIO;
uint16_t Pin;
} FLEDPins;
/* USER CODE END PTD */
그 밑으로 소스에서 리터럴값을 그대로 적지 않기 위해 define몇개를 정의하자.
/* USER CODE BEGIN PD */
#define LED_COUNT 16
#define MIN_LED_INDEX 0x0001
#define MAX_LED_INDEX 0x0001 << (LED_COUNT - 1)
#if LED_COUNT > 16
#error "LED_COUNT value must be less than 16"
#endif
/* USER CODE END PD */
총 LED의 갯수를 정의하고 LED가 왔다갔다 할때 처음과 마지막을 체크해서 방향을 토글시키기 위해 처음과 마지막 비트 위치를 정의한다.
그 밑에 실수로 LED_COUNT를 16개보다 많이 설정하는걸 방지하기 위해 16을 초과할 경우 컴파일 오류가 발생하도록 했다. 이는 main함수 안에 정의할 uOnFlag 변수가 2바이트 정수(uint16_t)로 선언되어 있기 때문에 16을 초과할 수 없기 때문이다.
/* USER CODE BEGIN PV */
uint8_t gucSpeedIndex = 0;
FLEDPins ledPins[LED_COUNT] = {
{OD01_GPIO_Port, OD01_Pin}
, {OD02_GPIO_Port, OD02_Pin}
, {OD03_GPIO_Port, OD03_Pin}
, {OD04_GPIO_Port, OD04_Pin}
, {OD05_GPIO_Port, OD05_Pin}
, {OD06_GPIO_Port, OD06_Pin}
, {OD07_GPIO_Port, OD07_Pin}
, {OD08_GPIO_Port, OD08_Pin}
, {OD09_GPIO_Port, OD09_Pin}
, {OD10_GPIO_Port, OD10_Pin}
, {OD11_GPIO_Port, OD11_Pin}
, {OD12_GPIO_Port, OD12_Pin}
, {OD13_GPIO_Port, OD13_Pin}
, {OD14_GPIO_Port, OD14_Pin}
, {OD15_GPIO_Port, OD15_Pin}
, {OD16_GPIO_Port, OD16_Pin}
};
/* USER CODE END PV */
그리고 전역 변수 두개를 설정하는데 하나는 현재의 속도값 Index를 저장할 변수 하나와 구조체로 선언한 FLEDPins를 LED갯수만큼 배열로 만들어 초기화까지 진행하도록 했다.
구조체의 pGPIO에는 해당 포트의 GPIO_TypeDef형 포인터 변수를, Pin에는 핀번호를 지정하는데 CubeMX에서 해당 핀의 User Label을 위 소스와 같이 지정했으므로 main.h에 GPIO_TypeDef형 포인터와 핀번호가 동일하게 define되어 있을 것이다.
다음으로 푸시버튼 두개에 대한 처리를 하기 위해 별도의 함수를 하나 만들었다.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
void checkSpeedInput()
{
// [1]번 비트에 UP버튼, [0]번 비트에 DOWN버튼 상태 저장
uint8_t uiRead = (uint8_t)HAL_GPIO_ReadPin(SPEED_UP_GPIO_Port, SPEED_UP_Pin) << 1
| (uint8_t)HAL_GPIO_ReadPin(SPEED_DOWN_GPIO_Port, SPEED_DOWN_Pin);
int i;
uint16_t uiOn = 0;
if(uiRead != 0)
{
HAL_Delay(100); // 채터링 방지
if((uiRead & 0x01) != 0)
{
if(gucSpeedIndex < 4)
gucSpeedIndex++;
}
else if((uiRead & 0x02) != 0)
{
if(gucSpeedIndex > 0)
gucSpeedIndex--;
}
uiOn = (0x1F >> (4 - gucSpeedIndex)) & 0xFFFF;
for(i = 0; i < LED_COUNT; i++)
HAL_GPIO_WritePin(ledPins[i].pGPIO, ledPins[i].Pin, !(uiOn & (0x01 << i)));
HAL_Delay(1000);
}
}
/* USER CODE END 0 */
푸시버튼 입력이 있는지를 체크해서 있으면 현재 속도 Index값인 gucSpeedIndex값을 증감시킨다. 단, 속도는 0~4까지 5단계까지만 설정할 수 있고 이 변수 값에 따라 첫번째부터 gucSpeedIndex값 까지의 비트를 설정해 LED를 켠다.
그리고 1초동안 표시한 뒤 함수를 종료한다.
마지막으로 main함수 안에 다음과 같이 코딩한다.
int main(void)
{
/* USER CODE BEGIN 1 */
bool bFwd = true; // 현재 방향을 결정할 변수
uint16_t uOnFlag = 0x0001; // 16개 LED에 대한 On/Off여부. 각 비트로 대응.
/* 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 */
/* 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 */
// 속도 조절 버튼 감지
checkSpeedInput();
// 현재 방향에 따라 Shift처리 및 좌우 끝 도달여부 체크후 방향 전환
if(bFwd)
{
if((uOnFlag = uOnFlag << 1) == MAX_LED_INDEX)
bFwd = false;
}
else
{
if((uOnFlag = uOnFlag >> 1) == MIN_LED_INDEX)
bFwd = true;
}
// 16개 LED전체 출력 처리
for(int i = 0; i < LED_COUNT; i++)
{
// Pull-up구성이므로 uOnFlag의 해당 비트값과는 반대
HAL_GPIO_WritePin(ledPins[i].pGPIO, ledPins[i].Pin, !(uOnFlag & (0x0001 << i)));
}
// 현재 속도값으로 Delay
HAL_Delay(10 * (gucSpeedIndex + 1));
}
/* USER CODE END 3 */
}
먼저 제일 위에 지역변수 두개를 선언했다.
bFwd는 현재의 방향이다. LED On을 왼쪽으로 진행할지 오른쪽으로 진행할지 여부를 결정한다.
그리고 uOnFlag는 uint16_t형으로 선언되어 16개 LED에 대해 bit단위로 각각의 On/Off여부를 저장하고 있다. 이 변수는 초기값이 0x0001이므로 최하위비트만 1로 설정되고 나머지는 0으로 설정된다.
그리고 이 변수가 16비트 형이므로 LED_COUNT define값은 16을 초과할 수 없다.
while문 안에서는 먼저 푸시버튼 입력체크를 하기 위한 checkSpeedInput()함수를 호출한다.
그리고 bFwd방향에 따라 좌 또는 우로 1bit쉬프트 처리를 해서 On되는 LED의 위치를 이동시킨다.
이때 On되는 LED가 처음 또는 마지막인지 체크해서 방향을 바꿀 수 있도록 했다.
그리고 for문으로 LED_COUNT만큼 uOnFlag의 각 비트 값에 따라 각 LED를 On/Off시킨다.
마지막으로 HAL_Delay()를 호출해 속도를 결정한다.
gucSpeedIndex전역변수 값은 0~4까지의 값을 가지므로 1을 더해 최소 10ms 최대 50ms까지 속도 조절을 하도록 했다.
7. 마무리
앞서 이야기한바와 같이 이 프로젝트에서는 굳이 Open Drain방식을 쓸 필요가 없었다.
하지만 Open Drain이 무엇인지는 알고가야겠기에 조잡하지만 예제를 하나 만들어 보았다.
이 프로젝트에서 Open Drain을 사용한 목적을 굳이 적용을 해보자면 소스의 HAL_GPIO_WritePin()호출 부분을
// 기존
HAL_GPIO_WritePin(ledPins[i].pGPIO, ledPins[i].Pin, !(uOnFlag & (0x0001 << i)));
// 변경
HAL_GPIO_WritePin(ledPins[i].pGPIO, ledPins[i].Pin, !!(uOnFlag & (0x0001 << i)));
와 같이 !를 한번더 써서 값을 한번더 반전시키면 된다.
그러면 15개의 LED는 동시에 켜지고 한번에 하나의 LED만 꺼지면서 왔다갔다 하므로 Open Drain의 용도를 충족할 수 있을거 같다.
사실 S/W관점으로 볼 때는 Push/Pull이나 Open Drain이나 설정이외에는 동일하며 단지 외부 회로 구성이 바뀔 뿐이다.
'임베디드 > STM32' 카테고리의 다른 글
12. [STM32] Interrupt 개요 (0) | 2024.09.26 |
---|---|
11. [STM32] SWV/ITM 을 사용한 디버깅 (0) | 2024.09.23 |
9. [STM32] Bit-banding (0) | 2024.09.10 |
8. [STM32] GPIO - Peripheral Register 2 (0) | 2024.08.13 |
7. [STM32] GPIO - Peripheral Register 1 (0) | 2024.07.26 |