자 첫번째 STM32F 프로그램을 하나 만들어 보자.
제목이 "Hello World!"이다. 전통적으로 어떤 프로그램 언어를 처음 배울때 해당 언어와 개발툴에 적응하기 위해 최초 작성해 보는 프로그램인데 단순히 화면에 "Hello World"라는 문자를 찍어보는게 끝이다.
우리도 전통을 따르기로 하자.
다만 MCU프로그램이므로 MCU답게 버튼을 누르면 LED가 켜지는 기능도 추가하겠다. 임베디드판 Hello World인 셈이다.
|
1. 준비물
기본적으로 NECLEO보드와 CubeIDE가 있어야 한다.
이에 더해 화면에 Hello World문자를 찍어야 하므로 시리얼 통신을 할 수 있는 윈도우용 터미널 프로그램이 하나 필요하다.
TeraTerm, Putty등 무슨 프로그램을 쓰든 상관없다.
본 블로그에서는 다른 사이트에서 많이 언급되는 TeraTerm으로 진행하겠다. Open Source이므로 무료다.
Release Tera Term 5.2 · TeraTermProject/teraterm
Tera Term (Ver 5.2), TTSSH (Ver 3.2) 主な変更点 単語の区切り文字に Unicode を使用できるようにした。 5.0 から発生したいくつかの問題を修正した。 すべての変更については、変更履歴を確認してく
github.com
제일 밑에 Assets란에 teraterm-x.x.exe 파일을 다운받아 설치하면 된다.
2. 목표
이 장에서는 NUCLEO보드에 달린 User Button(B1)을 누를때마다 User LED의 LD1(Green) LED에 불이 들어오고 꺼지도록 프로그램을 작성해 본다.
또한 버튼을 누를때 마다 UART를 이용해 윈도우 터미널쪽으로 "Hello World!" 문자열을 전송한다.
3. CubeIDE 로 작업하기
CubeIDE를 제일 처음 실행하면 Workspace를 저장할 디렉토리를 지정하라고 나오는데 STM32 프로젝트를 모아서 저장해놓을 디렉토리를 적당히 하나 만들고 그 디렉토리를 지정하자.
그러면 이후부터는 CubeIDE에서 프로젝트를 새로 만들때마다 이 디렉토리에 프로젝트가 생성된다.
최초 CubeIDE가 실행되면 탭에 Information Center가 뜬다. 각종 STM32관련 링크가 표시되고 신규 프로젝트를 생성할 수 있는 버튼도 표시된다. 링크를 따라 들어가서 확인할게 있으면 확인하고 창을 닫자.
그러면 다음과 같이 왼쪽에 있던 Project Explorer가 확장되어 보인다.
여기에서 Create a New STM32 project를 선택하거나 메인메뉴의 "File-New-STM32 Project"를 선택해 새 프로젝트를 만들면 된다.
프로젝트를 생성하면 자기가 알아서 필요한 파일을 다운로드 받는 작업을 진행한다.
다운로드가 완료되면 다음과 같이 Target Selection창이 뜬다.
자신이 사용할 MCU나 개발보드를 선택하는 화면이다. 여기서 선택된 MCU/Board대로 소스가 만들어진다.
개발보드를 사용하지 않고 별도의 STM MCU를 사용한 프로젝트를 진행한다면 화면상단 탭에서 MCU/MPU Selector를, NUCLEO개발보드를 사용한다면 Board Selector를 선택하면 되는데 우리는 보드를 사용하므로 Board Selector를 선택한다.
Board Selector화면에 보면 ST사에서 판매하는 각 Board리스트가 이미지와 함께 나열되어 있다. 자신이 가지고 있는 보드를 선택하면 되는데 종류가 많아 찾기 어렵다면 화면 왼쪽의 PRODUCT INFO의 Type란에서 보드 카테고리를 선택하면 리스트의 내용을 필터링 할 수 있다.
보드를 선택했다면 화면 오른쪽 하단의 "Next >" 버튼이 활성화되므로 누르자.
그러면 다음과 같은 창이 표시되는데 여기서 프로젝트 이름을 HelloWorld라고 입력하고 나머지는 그대로 둔다.
참고로 Option란을 보면
- Target Language : 컴파일러를 C를 사용할지 C++을 사용할지 결정한다.
- Targeted Binary Type : Executable을 선택하면 MCU에 업로드되어 실행될 수 있는 프로그램을 작성하는 프로젝트를 만들겠다는 의미이고, Static Library를 선택하면 다른 프로젝트에 사용될 자신만의 전용 라이브러리 프로젝트를 만들겠다는 의미이다. 차후 자주 사용하는 함수나 기능들은 Static Library로 만들어 놓으면 해당 프로젝트에서 이 Library를 링크시켜 쓰면 된다.
- Targeted Project Type : 자동 생성하는 Framework코드를 STM32Cube포맷대로 만들지 그냥 빈 소스로 놔둘지 결정.
모두 입력했으면 아래의 Finish버튼을 눌러 프로젝트를 생성하자.
생성중 "Initialize all peripherals with their default Mode ?" 라는 질문창과 "Open Associated Perspective?"창이 뜨는데 둘다 Yes를 누르면 된다.
프로젝트 생성이 완료되면 위와 같은 화면이 뜬다. 이 화면이 CubeMX이고 여기서 각종 설정을 GUI환경으로 진행할 수 있다.
CubeMX는 잠시 놔두고 화면 왼쪽의 Project Explorer에서 왼쪽 그림과 같이 /HelloWorld/Core/Src 을 확장해 보면 CubeIDE가 만들어준 각 소스파일들이 존재한다.
프로그램의 Entry Point인 main()함수가 있는 main.c 파일도 보인다. 열어보자.
각종 주석과 함께 자동으로 생성된 소스들이 보인다. 의미는 차차 파악하기로 하고 먼저 main()함수를 찾자.
main()함수내에서는 최초로 HAL_Init()함수를 호출하고 그 밑으로 여러가지 초기화 함수들이 등장한다.
다시 밑으로 내려가면 while(1) 문장이 보이는데 이 while문이 MCU전원이 OFF될 때 까지 계속해서 돌아가는 무한 루프이므로 주로 이 while문 안에 프로그램을 하게 될것이다.
그런데 군데군데
/* USER CODE BEGIN ... */
/* USER CODE END ... */
과 같이 USER CODE BEGIN/END로 시작하는 주석들이 보인다. main()뿐 아니라 main()바깥에도 여러개 있다.
CubeIDE가 만든 소스에 자신의 코드를 집어 넣을때는 반드시 이 주석의 BEGIN부분과 END부분 사이에 코드를 넣어야 한다.
CubeMX에서 어떤 설정을 변경하고 저장을 하면 CubeMX는 자동으로 소스를 변경내용에 맞게 업데이트를 진행한다.
이때 기존 소스는 무시하고 신규 내용으로 덮어써 버리는데 이때 /*USER CODE BEGIN...*/과 /*USER CODE END...*/ 사이에 있는 소스는 그대로 보존을 해준다.
그러므로 자신이 만든 소스를 날려먹고 싶지 않으면 반드시 BEGIN과 END사이에 소스를 작성해야 한다. 물론 CubeMX를 사용하지 않는다면 아무곳에나 적어도 상관없다.
이 USER CODE 주석블럭은 USER CODE BEGIN Include , USER CODE BEGIN PTD, USER CODE BEGIN 0, USER CODE BEGIN 1, ... 등과 같이 여러군데 등장하는데 어느 위치에 코딩하든 큰 상관은 없다. 자신이 편한 위치에 코딩하면 된다.
예전에 STM32관련 자료를 찾다 어느 블로그를 들어갔는데 거기 댓글 중 질문으로 소스를 USER CODE BEGIN 1에 넣어야 되나요 2에 넣어야 되나요 라는 질문이 있는걸 봤다.
아무 상관없다. 그냥 BEGIN/END 사이에만 넣으면 된다.
/* USER CODE BEGIN Includes */ 는 자신이 추가할 #include문을 집어넣으라고 정의해놓은 곳인데 #include말고 다른 문장을 써도 C문법만 맞다면 아무런 상관이 없다는 말이다.
단, 소스상에서 include, 전역변수 정의, define등 정리해서 넣는게 소스 가독성을 높이는데 유리하므로 가급적 알맞은 위치에 코딩하는 습관을 들이자.
다시 CubeMX화면으로 돌아가자.
만일 창을 닫았다면 Project Explorer에서 HelloWorld.ioc 를 찾아 더블클릭하면 다시 열린다.
CubeMX화면의 상단에 보면 Pinout & Configuration, Clock Configuration, Project Manager, Tools 탭이 있는데 나머지는 필요할때 설명하기로 하고 먼저 첫번째 Pinout & Configuration항목을 보자.
화면 왼쪽에서 System Core를 클릭해 확장해보면 DMA, GPIO, IWDG등등이 보인다. 여기서 GPIO를 선택한다.
GPIO(General Purpose Input Output)는 MCU가 외부 장치와 신호를 주고 받기 위해 사용하는 Pin이다. 각 핀은 목적이 정해진 핀도 있지만 대부분은 그 이름대로 사용자가 임의의 목적으로 사용할 수 있는 범용목적의 핀이며 그 목적을 자신에게 맞게 정하는 곳이 위 그림의 GPIO설정 부분이다.
우리는 현재 사용자가 User Button을 누르면 LED1에 불이 들어오게 할 것이므로 둘 다 On/Off기능만 가지는 단순 Bit I/O로서 GPIO를 설정하면 된다. (단, 이 예제에서는 버튼의 경우 단순 Bit I/O는 아니고 인터럽트 방식을 사용한다.)
NUCLEO-F429보드는 User Button이 PC13번 핀에, LED1이 PB0에 할당되어 있고 앞서 프로젝트 생성시 F429보드를 선택했으므로 CubeMX에서 PC13, PB0가 디폴트로 할당되어 있는걸 볼 수 있다.
그 밑으로 PB7, PB14에 LED2, LED3이 각각 할당된것도 볼 수 있다.
리스트에서 해당 핀을 선택하면 아래에 해당 핀 설정값이 표시되고 MCU이미지에서 그 핀이 깜박거린다.
먼저 GPIO에서 PB0와 PC13을 아래 그림과 같이 설정하자.
PB0 설정을 간략하게 설명하면
- GPIO output level : 일반적인 상태에서 이 핀의 출력을 Low 상태로 두겠다는 뜻이다. PB0는 LED1에 연결되어 있으므로 이렇게 설정하면 LED는 꺼져있는 상태가 된다. 반대로 High로 두면 평상시에는 LED가 켜져 있는 상태가 된다.
- GPIO mode : Output Push Pull과 Ouptut Open Drain방식 두가지가 있다. Open Drain은 별도 장에서 설명하겠지만 간략히 설명하자면 STM32의 GPIO 핀에서 출력되는 전압은 3.3V이다. 이 전압을 그대로 사용해서 외부 장치나 소자를 제어한다면 출력 그대로를 사용하면 되지만 만약 외부 장치의 입력전압이 3.3V가 아니고 5V인 경우 이 핀의 전압을 그대로 사용할 수는 없다. 이런 경우는 외부 장치에 전달되는 전압을 MCU가 아닌 별도 전압을 끌어와서 인가하고 MCU의 출력핀은 이 전압을 Low/High상태로 바꾸는 역할만 하게 할 수 있는데 이런 방식을 Open Drain이라고 한다. 이 예제에서는 그냥 Ouput Push Pull로 놔둔다.
- GPIO Pull-up/Pull-down : 일반적으로 GPIO 핀의 상태는 Low/High 두가지 상태중 하나를 가져야 한다. 하지만 실제로는 전압이 정해지지 않은 상태 즉, floating상태에 있는 상황이 발생한다. 예를들어 User Button에 연결된 PC13의 경우 누르지 않은 경우는 0V, 눌러진 경우는 3.3V가 출력이 되어야 하지만 누르지 않은 경우 전압이 정해지지 않은 상태 즉 floating상태에 있게된다. 이를 방지하기 위해서는 별도의 회로를 구성해줘야 하는데 이때 사용하는 회로를 Pull Up/Pull Down회로라고 한다. Pull Up은 floating상태를 끌어올려 원래의 전압을 만들어주고 Pull Down은 floating상태를 끌어내려 0V로 만들어 주는 회로이다.
대부분의 MCU들은 각 GPIO에 Pull Up/Pull Down 회로를 내장하고 있다. STM32도 마찬가지여서 코드상에서 설정을 해주면 해당 회로가 동작하게 된다. PB0의 경우 평소 Low상태를 유지하다 코드에서 High신호를 줄때 바뀌어야 하고 평소때는 floating상태가 되어 있으므로 Pull-down으로 설정해 평소에 0V가 되도록 한다. - Maximum output speed : 해당 GPIO의 입출력 속도를 결정한다. 지금 예제와 같이 속도에 별 상관없는 입출력일 경우 Low상태로 놔두면 되지만 해당 핀을 PWM등의 고속출력이 필요할 경우 이 값을 적당히 변경해야 한다. 당연한 이야기지만 속도가 빠를 수록 소비전력은 증가한다.
- User Label : 해당 핀의 이름을 지정하는 칸이다. Label을 바꾸면 오른쪽 MCU그림의 핀에 표시되는 이름도 같이 바뀐다. 그리고 이 이름은 소스에서도 사용할 수 있는데 잠시후 소스 구현시 설명하도록 하겠다.
CubeMX가 Java 베이스라 그런지 Graphic 처리가 조금 느리다. User Label에서 값을 변경하면 MCU이미지쪽도 실시간으로 변경되는데 느려서 편집이 용이하지 않다. 이름이 길다면 직접 편집하기 보다는 메모장등에서 이름을 입력해놓고 복사하기-붙여넣기로 넣는게 속편하다.
PC13 설정도 같이 보자.
- GPIO mode : PB0와 다르게 이 핀의 mode는 External Interrupt../ External Event... 로만 선택되도록 되어 있다. 먼저 리스트에서 PC13을 선택해보자. MCU이미지에서 해당 핀이 깜박거릴 것이다.
이미지에서 이 핀을 클릭해보자. 팝업메뉴가 뜨고 현재 GPIO_EXTI13이 선택되어 있을 것이다(PB0의 경우 GPIO_Output으로 설정되어 있다. 즉 단순 Bit I/O 출력신호로 사용하겠다는 것이다). 이것은 이 핀의 용도를 외부 인터럽트 입력신호로 이용하겠다는 의미이다. (핀번호에 따라 GPIO_EXTIn 과 같이 인터럽트 번호가 달라진다.) 물론 이 메뉴에서 GPIO_Input을 선택해서 Interrupt방식이 아니라 단순 Input Pin으로 설정해서 이 예제를 진행해도 되나 자동으로 생성되는 소스에 대한 개략적인 설명을 위해 인터럽트 방식을 그대로 사용한다. (인터럽트 개념은 별도의 장에서 설명한다.)
현재 디폴트로 선택된 "External Interrupt Mode with Rising edge tirgger detection"을 그대로 둔다. 이 모드는 이 버튼을 눌렀을때 즉 Low상태에 있다가 High상태가 되는 시점에서 인터럽트를 발생시키겠다는 의미이다. - GPIO Pull-up/Pull-down : PB0 내용과 동일하다. 입력되는 버튼상태가 평상시는 Low, 누르면 High 상태가 되어야 하나 Low상태가 floating상태이므로 Pull-down으로 설정한다.
- User Label : 동일.
위에서 우리는 PC13을 외부 인터럽트 신호로 사용하기로 했다. 하지만 이 상태로는 인터럽트 신호가 수신되지 않는다.
아래 그림과 같이 왼쪽 System Core의 NVIC를 선택하거나 GPIO의 NVIC탭을 클릭해서 아래와 같이 "EXTI line[15:10] interrupts" 항목의 Enabled에 체크를 해줘야 한다. (왼쪽에서 NVIC를 선택하면 사용되는 모든 인터럽트가 표시되고 GPIO에서 NVIC를 선택하면 GPIO에서 선언된 인터럽트만 표시된다.)
NVIC는 Nested Vectored Interrupt Controller의 약자로 ARM의 Cortex M4프로세서에서 정의 된 중첩 인터럽트 처리 컨트롤러 이다. 자세한 설명은 인터럽트 관련 장에서 하기로 하고 우선은 발생한 인터럽트를 수신하기 위해서는 이 화면에서 인터럽트로 등록해야 한다는 것만 알면 된다.
Pinout & Configuration에서 GPIO 핀설정은 완료되었다.
그 다음 Clock Configuration은 이 예제에서는 사용할 일이 없으므로 Timer관련 장에서 설명하기로 하고 Project Manager항목으로 이동하자.
왼쪽에서 Code Generator를 선택하고 Generated files 섹션에서 "Generate peripheral initialization as a pair of '.c/.h'files per peripheral" 항목에 체크표시를 하자.
Peripheral이란건 MCU내부가 아닌 MCU외부 장치를 통칭하는 말이다. 즉 주변장치라고 보면 된다. 따라서 우리가 위에서 설정한 GPIO뿐 아니라 시리얼통신에 사용될 UART, USB OTG등 모든 항목들이 이 Peripheral에 해당한다.
위에서 체크한 항목은 이들 Peripheral에 대한 소스를 각 Peripheral별로 쪼개서 별도 파일로 만들라는 의미이다.
이 상태에서 저장 버튼을 누르거나 Ctrl+S 를 누르면 코드를 생성하겠냐는 메시지박스가 출력되고 Yes를 선택하면 설정한 내용대로 CubeMX가 소스를 만들어 준다.
하지만 그전에 먼저 Project Explorer에서 'HelloWorld/Core/Src' 안에 있는 파일 리스트를 보자.
main.c, stm32f4xx_hal_msp.c, stm32f4xx_it.c, syscalls.c, sysmem.c, system_stm32f4xx.c 총 6개의 C파일이 있다. 그리고 그중 stm32f4xx_it.c 파일을 열어서 제일 아래로 내려보면 아래 코드가 마지막으로 나온다.
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
이걸 확인하고 다시 CubeMX화면으로 돌아와 변경내용을 저장한 뒤 다시 Project Explorer에서 위의 폴더를 확인해보자. eth.c, gpio.c, usart.c, usb_otg.c 파일이 추가되었을 것이다.
이건 CubeMX의 Project Manager에서 "Generate peripheral initialization as a pair of '.c/.h'files per peripheral" 항목에 체크를 했기 때문이다.
이렇게 체크를 한 뒤 소스를 생성하면 각 Peripheral별로 별도 파일로 소스를 분리해준다.
main.c 파일을 열어 main()함수로 가보자.
main함수 중간쯤에 MX_GPIO_Init()함수를 호출하는 부분이 있다. Ctrl키를 누른 상태에서 이 함수를 클릭하면 해당 함수의 구현부로 이동하는데 자동으로 gpio.c파일이 열리면서 해당 함수로 포커스가 이동한다.
그리고 소스파일중 아까 저장전에 열어봤던 stm32f4xx_it.c 파일도 열어서 제일 아래를 확인해보자. 원래는 SysTick_Handler()함수가 제일 마지막이었으나 그 밑에 EXTI15_10_IRQHandler() 함수가 추가된 것이 보일것이다.
이 함수는 아까 GPIO를 설정할때 NVIC항목에서 " EXTI line[15:10] interrupts"에 체크를 함으로써 자동생성된 것이다.
다시 CubeMX의 Project Manager에서 "Generate peripheral initialization as a pair of '.c/.h'files per peripheral" 항목을 체크해제한 뒤 저장을 해보자.
아까와는 달리 파일에 eth.c, gpio.c등의 파일이 보이지 않을 것이다. 그리고 main함수에서 Ctrl을 누르고 MX_GPIO_Init()함수를 클릭해보면 아까와는 달리 MX_GPIO_Init()함수가 main.c 파일에 포함된것을 알 수 있다.
이번 예제에서는 사실 파일을 쪼개건 main.c에 통합하건 별 상관이 없다. 컴파일된 실행파일의 퍼포먼스에 영향을 주는것도 아니다. 단지 소스를 분리해서 관리하므로써 코딩 효율을 높이기 위함이므로 편한대로 설정하자.
단, 나중에 프로젝트가 커지고 코딩량이 많아지면 main.c에 자신이 코딩한 내용이 많아질 것이다. 파일 하나에 많은 양이 있으면 코드 가독성도 떨어질뿐더러 유지보수가 용이하지 않다. 이런 경우는 파일을 분리해서 관리하는게 여러모로 유리하다.
4. 코딩
자 드디어 소스 파일을 열고 코딩할 때이다.
그전에 한가지만 더 짚고 넘어가자.
아까 우리가 User Button에 연결된 PC13에 대해 EXTI line[15:10] Interrupt를 설정했다.
인터럽트라는건 MCU가 우리가 실행한 소스를 열심히 실행하고 있는데 외부에서 MCU에게 지금 하는거 잠깐 중지하고 이거부터 해줘라고 방해를 하는 행위를 말한다.
전원이 모자라거나 코드를 더이상 실행할 수 없는 상황에서 발생하는 MCU자체의 인터럽트도 있고 GPIO핀으로 우리가 임의의 신호를 발생시켜 인터럽트를 거는 경우도 있다. MCU는 우선순위에 따라 이 인터럽트의 처리를 먼저 진행하게 된다.
자세한 설명은 별도의 장에서 하기로 하고 여기서는 인터럽트가 발생했을때 우리는 소스상에서 이 신호에 대한 처리를 하는 루틴을 직접 작성할 수 있다.
자동으로 생성된 소스중 stm32f4xx_it.c 파일을 열어보자.(파일명 접미사로 _it가 쓰인건 이 파일이 Interrupt와 관련된 파일이라는 의미다)
위에서 부터 NMI_Handler(), HardFault_Handler(), MemManage_Handler()등의 함수가 있고, 아까 설명한 EXTI15_10_IRQHandler()함수가 제일 마지막에 나온다.
참고로 NMI_Handler()는 NMI(Non Maskable Interrupt)에 대한 처리 루틴으로 하드웨어 이상등 심각한 상황 발생시 호출되는 인터럽트이다. 모든 인터럽트중 우선순위가 제일 높으므로 호출즉시 실행된다.
소스에 NMI_Hander()함수가 있고 현재 이 인터럽트가 발생하면 while(1)로 무한루프에 빠지도록 되어 있다.
이 의미는 더이상 코드를 진행할 수 없는 심각한 오류로 인해 MCU가 아무런 동작을 하지 않도록 하겠다는 의미다.
필요한 경우 USER CODE BEGIN/USER CODE END 주석 사이에 자신의 코드를 넣을수 있다.
그리고 나머지 인터럽트 핸들러들도 동일하게 구성되어 있다.
우리 예제에서는 사용하지 않으므로 일단 놔두고 제일 아래 EXTI15_10_IRQHandler()함수로 이동하자.
이 핸들러에서는 다시 HAL_GPIO_EXTI_IRQHandler()함수를 호출한다. Ctrl키를 누른상태에서 이 함수를 클릭해보면 해당 함수 구현부분으로 이동하는데 거기에 보면 다시 HAL_GPIO_EXTI_Callback()함수를 호출하는 부분이 있다.
다시 이 함수를 Ctrl키를 누른상태에서 클릭해보면 바로 아래쪽에 구현된 함수로 이동한다.
HAL_GPIO_EXTI_Callback()함수 제일 앞에는 __weak 라는 지시자가 있는데 이 지시자가 붙은 함수는 사용자가 이 함수를 재정의해서 사용할 수 있고 재정의 했을 경우 컴파일러에게 기존 소스는 무시하고 사용자가 작성한 함수로 대체하라는 의미이다. 즉 이 함수 전체를 복사해서 다른곳에 다시 정의하고 거기에 자신이 원하는 코드를 코딩해놓으면 해당 인터럽트 발생시 __weak가 붙은 자동생성 코드가 아니라 자신의 코드를 실행시킬 수 있다는 의미이다.(재정의할때 __weak는 빼야한다.)
따라서 EXTI line[15:10] Interrupt가 발생하면
EXTI15_10_IRQHander() -> HAL_GPIO_EXTI_IRQHandler() -> HAL_GPIO_EXTI_Callback()순으로 실행된다. (단, EXTI15_10_IRQHandler()호출은 인터럽트 벡터테이블에 저장된 주소에 따라 MCU가 호출해준다. 자세한 내용은 이후에 작성할 Entry Point장에서 설명한다.)
우리는 HAL_GPIO_EXTI_Callback함수를 main.c에 재정의하도록 하겠다.
main.c를 열어 아래와 같이 윗부분에 #include 두개를 추가한다. (반드시 USER CODE BEGIN/END사이여야 한다.)
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "eth.h"
#include "usart.h"
#include "usb_otg.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <string.h>
#include <stdio.h>
/* USER CODE END Includes */
그리고 main()함수를 찾아 그 위의 /* USER CODE BEGIN 0 */ 에 다음 소스를 넣는다.(다른 USER CODE BEGIN/END에 넣어도 상관없지만 main()함수 바로 위에 있어서 여기에 작성했다.)
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
char buf[256] = {0,};
if(GPIO_Pin == GPIO_PIN_13) {
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
sprintf(buf, "Hello World!\r\n");
HAL_UART_Transmit(&huart3, (const uint8_t*)buf, strlen(buf), 10);
}
}
/* USER CODE END 0 */
User Button을 누르면 EXTI[15:10] 인터럽트가 발생하고 이 인터럽트에 대한 실행 루틴은 설명대로 바로 위에 작성한 HAL_GPIO_EXTI_Callback()함수가 된다.
먼저 파라미터로 전달된 GPIO_Pin이 13번 핀인지 확인하는 if문이 나온다. 이 예제에서는 사실상 의미가 없긴 하지만 다른 인터럽트가 정의되어 있다면 이를 구분하기 위해 이렇게 확인해야 한다는걸 보여주기 위해 넣었다.(EXTI[15:10] 인터럽트는 10~15번 핀별로 이 하나의 인터럽트를 공유한다. 예를들어 PC13, PF10번 핀을 각각 GPIO_EXTI13, GPIO_EXTI10으로 설정했다면 동일한 EXTI[15:10] 인터럽트가 발생한다. 이 경우 위와 같이 핀번호로 구분해서 처리를 다르게 할 수 있다.)
if문 안쪽으로 HAL_GPIO_TogglePin()함수가 나온다. 이 함수는 해당 핀의 출력을 Low/High로 번갈아 가며 토글시킨다. 즉 이 함수를 호출할때마다 현재 핀 상태를 반대값으로 설정한다. 현재 PB0에 연결된 LED1번을 토글시켜야 하므로 B와 0을 지정한 GPIOB, GPIO_PIN_0를 파라미터로 지정하면 된다.(핀이 PF10이라면 각각 GPIOF, GPIO_PIN_10)
그리고 CubeMX에서 GPIO 핀설정 할때 User Label란이 있었다. 이 칸의 이름을 바꾸면 CubeMX의 MCU이미지상의 핀이름도 같이 바뀐다. 이와 더불어 코드 생성시 main.h 파일에도 #define문으로 이 이름을 생성해 준다.
여기 보면 우리가 사용할 User Button은 USER_Btn_Pin, USER_Btn_GPIO_Port로, LED1은 LD1_Pin, LD1_GPIO_Port로 정의되어 있는걸 볼 수 있다.
#define USER_Btn_Pin GPIO_PIN_13
#define USER_Btn_GPIO_Port GPIOC
#define USER_Btn_EXTI_IRQn EXTI15_10_IRQn
#define MCO_Pin GPIO_PIN_0
#define MCO_GPIO_Port GPIOH
#define RMII_MDC_Pin GPIO_PIN_1
#define RMII_MDC_GPIO_Port GPIOC
#define RMII_REF_CLK_Pin GPIO_PIN_1
#define RMII_REF_CLK_GPIO_Port GPIOA
#define RMII_MDIO_Pin GPIO_PIN_2
#define RMII_MDIO_GPIO_Port GPIOA
#define RMII_CRS_DV_Pin GPIO_PIN_7
#define RMII_CRS_DV_GPIO_Port GPIOA
#define RMII_RXD0_Pin GPIO_PIN_4
#define RMII_RXD0_GPIO_Port GPIOC
#define RMII_RXD1_Pin GPIO_PIN_5
#define RMII_RXD1_GPIO_Port GPIOC
#define LD1_Pin GPIO_PIN_0
#define LD1_GPIO_Port GPIOB
#define RMII_TXD1_Pin GPIO_PIN_13
#define RMII_TXD1_GPIO_Port GPIOB
#define LD3_Pin GPIO_PIN_14
#define LD3_GPIO_Port GPIOB
#define STLK_RX_Pin GPIO_PIN_8
따라서 위의 코드를 아래와 같이 바꿔도 상관없다. 편한 방식대로 하자.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
char buf[256] = {0,};
if(GPIO_Pin == USER_Btn_Pin) {
HAL_GPIO_TogglePin(LD1_GPIO_Port, LD1_Pin);
sprintf(buf, "Hello World!\r\n");
HAL_UART_Transmit(&huart3, (const uint8_t*)buf, strlen(buf), 10);
}
}
/* USER CODE END 0 */
그 밑으로 UART통신으로 PC쪽에 Hello World!라는 문자를 전송하기 위한 코드가 나온다.
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout)
첫번째 파라미터로 UART핸들 포인터를 전달해야 하는데 생성된 소스에 이미 정의가 되어 있다.
위치는 소스 생성시 Project Manager에서 Peripheral별로 소스를 분리하는지에 대한 옵션 선택에 따라 위치가 다른데 분리하지 않았다면 main.c에, 분리했다면 usart.c 파일에
UART_HandleTypeDef huart3;
과 같이 정의되어 있다.
이는 CubeMX에서 이미 디폴트로 USART3을 사용한다고 선언해 놓았기 때문이다. 다시 CubeMX를 열어 아래와 같이 확인하자.
그림과 같이 USART3의 Mode가 Asynchronous로 되어 있는걸 볼 수 있다. 나머지 USART들은 Disable되어 있으므로 소스에는 정의되지 않는다.
밑으로 USART3의 세부 설정이 나오는데 Baud Rate가 115200, Data bit수가 8bit, 패리티비트는 None Parity, Stop bit 1개로 설정되어 있다.
따라서 CubeMX가 만들어준 이 핸들로 HAL_UART_Transmit()함수를 호출하면 데이터가 UART3을 통해 전송된다.
이 함수의 두번째 파라미터는 실제 보낼 문자열이고 세번째는 보낼 문자열의 길이이다.
길이를 일일이 카운트 하지 않기 위해 strlen() C표준함수를 사용했고 문자열은 char형 배열에 담았다.
두번째 파라미터를 (const uint8_t*)로 강제형변환시켰는데 C의 string관련 함수들은 파라미터가 char*형식이고 HAL_UART_Transmit()함수의 파라미터는 uint8_t*이라서 강제 형변환시켰다. 같은 1바이트 자료형이므로 크게 문제될건 없다. 이 함수의 마지막은 Timeout시간이다.
코딩이 완료되었다.
main()함수 안에는 별다른 코딩이 필요없으므로 컴파일 버튼을 눌러보자.
망치가 컴파일, 벌레가 코드 주입 후 Debug모드로 실행, 둥근바탕에 오른쪽 세모가 코드주입후 실행 버튼이다.
Dubug실행이나 실행버튼을 누를 경우 컴파일이 되어 있지 않다면 자동으로 컴파일을 진행한다.
컴파일시 아래 Console창에 다음과 같이 0 errors가 나와야 한다.
16:14:54 Build Finished. 0 errors, 0 warnings. (took 658ms)
Debug버튼이나 실행버튼을 누를때는 PC에 NUCLEO보드가 USB로 연결되어 있어야 한다.
그리고 CubeIDE가 실행파일을 MCU에 주입하고 나면 테스트를 진행할 수 있다. 단 Debug로 실행한 경우 자동으로 main()함수의 HAL_Init()호출 부분에 Break point가 지정되어 멈춰 있을 것이다. 이때 F8키를 눌러 계속 진행하면 된다. 그냥 Run버튼을 누른 경우에는 F8키를 누를 필요는 없다.
보드의 버튼을 누르기전 제일 처음 설치한 TeraTerm을 실행하자.
그리고 "TeraTerm: 새 연결" 창에서 시리얼을 선택하고 STMicroeletronics STLINK를 선택한 뒤 확인버튼을 누른다.(이미지에서는 COM4로 되어 있지만 PC에 따라 다를 수 있다.)
그리고 TeraTerm 메뉴에서 설정-시리얼포트 를 선택해 속도를 115200로 설정한다. (CubeMX의 USART3 설정시 이 속도로 설정했다.) 나머지는 디폴트값으로 두고 "New setting"을 클릭한다.
이제 NUCLEO보드의 User Button을 눌러보자.
한번 누를때 마다 LED1번이 켜졌다 꺼졌다 하고 누를때마다 TeraTerm화면으로 Hello World!가 나오면 성공이다.
5. 마무리
Hello World 를 작성해봤다.
STM32를 처음접했다면 아직까지 뭐가뭔지 모를 수도 있다.
사실 이 장의 목적은 CubeIDE와 CubeMX의 프레임웍에 대한 맛보기를 보여주기 위함이다.
이것들이 어떤식으로 자동 소스를 생성하고 이들 소스가 어떻게 구성되어 있는지를 개략적으로 보여줌으로써 앞으로 코딩을 좀 더 원활하게 하기 위해 이 장을 작성했다.
각각의 세부 내용은 앞으로 나올 게시물에서 하나씩 살펴볼 예정이다.
스스로도 CubeIDE와 CubeMX를 하나씩 건드려가며 소스 구성이 어떻게 바뀌는지 따라가 보기 바란다.
'임베디드 > STM32' 카테고리의 다른 글
6. [STM32] GPIO 기초 (0) | 2024.07.25 |
---|---|
5. [STM32] NUCLEO F429/439 보드 Pinmap (0) | 2024.07.22 |
4. [STM32] Entry Point (0) | 2024.07.12 |
3. [STM32] 디버깅 (0) | 2024.07.12 |
1. [STM32] STM32 MCU (0) | 2024.07.08 |