Writing a Blinky Program: Difference between revisions
No edit summary |
|||
(9 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
== Initial Downloads == |
== Initial Downloads == |
||
For most projects I suggest you hand-type code that is in a code block in these articles. However for files that are not super important to the lesson, I will provide downloads. So for example, in this lesson, you will be writing a <code>main</code> program which means that the Makefile, startup code |
For most projects I suggest you hand-type code that is in a code block in these articles. However for files that are not super important to the lesson, I will provide downloads. So for example, in this lesson, you will be writing a <code>main</code> program which means that the Makefile, startup code, and interrupts do not pertain to you just yet, and hand-typing them will not be super useful/a worthwhile use of your time. So the provided initial code is here: https://code.umd.edu/robotics-at-maryland/embedded-onboarding/lesson3 |
||
== Goal == |
== Goal == |
||
A common practice whenever one wants to program with something new is to write a very simple "proof-of-concept" program. If you are learning a new language perhaps you want to write a program that prints "Hello, World!" to <code>stdout</code>, or if you are learning a new library you want to do something to that effect utilizing some basic feature of that library (such as sending a network packet over Ethernet from one computer to another saying a message like "Hello, World!"). In the case of an embedded system, the standard beginner program of choice is to write a "Blinky" program, that is, a program that turns an LED on the board on and off at a certain rate. |
A common practice whenever one wants to program with something new is to write a very simple "proof-of-concept" program. If you are learning a new language perhaps you want to write a program that prints "Hello, World!" to <code>stdout</code>, or if you are learning a new library you want to do something to that effect utilizing some basic feature of that library (such as sending a network packet over Ethernet from one computer to another saying a message like "Hello, World!"). In the case of an embedded system, the standard beginner program of choice is to write a "Blinky" program, that is, a program that turns an LED on the board on and off at a certain rate. |
||
<blockquote> |
|||
'''Exercise''' |
|||
Why is printing "Hello, World!" not a good first program for an embedded device?</blockquote> |
|||
== Structure of an Embedded Program == |
== Structure of an Embedded Program == |
||
Embedded programs follow the same abstract structure as programming for any system: set up your resources, then enter the main execution. |
Embedded programs follow the same abstract structure as programming for any system: set up your resources, then enter the main execution. For now, setting up your resources essentially means writing certain values to registers to tune the settings of the system, things like the clock, turning on or off certain pins on the board, or editing the speed of various communication protocols. Main execution for embedded devices will almost always live in an infinite loop. The idea is, "When I get power, turn on and run my code until I no longer have power, then I'll stop," so it makes sense to have the CPU constantly executing code (otherwise it would just be wasting power). |
||
== Configs == |
== Configs == |
||
''Navigate to <code>config.c</code>. This is where we will store the configuration code. You will be writing the code for <code>config_led()</code>.'' |
|||
One benefit of programming embedded devices is the freedom you get to configure anything you could possibly imagine on the system. The first thing you'll encounter is the clock. The official maximum clock rate of the CPU is 64 MHz (meaning 64 million CPU cycles occur every second, more on this later). So with that constraint, you are free to set the clock as you wish. For this project I have set the clock to run at the default value of 16 MHz, since right now we are not doing anything super intensive. |
|||
Now we want to blink the LED. The LED will turn on when it gets power, and off when it doesn't have power. The LED is connected to the CPU via one of the pins (on this system, it happens to be GPIO PA5). Since almost every pin starts disabled by default, we need to tell the hardware we want PA5 enabled with the ability to output an electrical signal (which will then toggle to turn the LED on and off). According to the official library documentation, we need to turn on the clock for the pin, and use <code>HAL_GPIO_Init()</code> to configure it. This function takes a GPIO port (which for now you can think of as a method the hardware uses to organize all the GPIO pins) and a configuration stored in a C struct <code>GPIO_InitTypeDef</code>. As per the documentation, we want the following configuration:<syntaxhighlight lang="c"> |
|||
GPIO_InitTypeDef led_config = { |
|||
.Pin = GPIO_PIN_5, |
|||
.Mode = GPIO_MODE_OUTPUT_PP, |
|||
.Pull = GPIO_NOPULL, |
|||
.Speed = GPIO_SPEED_FREQ_LOW, |
|||
.Alternate = 0 |
|||
}; |
|||
</syntaxhighlight>Line by line we have: |
|||
* We want to edit pin '''5''' of '''P'''ort '''A''' (hence the pin name PA5) |
|||
* We want to '''output''' data with this pin, and we will use '''P'''ush-'''P'''ull to do so (don't worry about what that means for now, just know that it means electrical signals will be outputted on this pin) |
|||
* You can ignore the <code>.Pull</code> and <code>.Speed</code> lines as well |
|||
* <code>.Alternate</code> is a field that is used if we wanted to do something more complicated with this pin, such as digital communication with other devices. Since we are not doing anything complicated, we set this to <code>0</code>, meaning don't do any alternate function |
|||
Then turn on the clock and write the config:<syntaxhighlight lang="c"> |
|||
__HAL_RCC_GPIOA_CLK_ENABLE(); |
|||
HAL_GPIO_Init(GPIOA, &led_config); |
|||
</syntaxhighlight>That's it! Our LED is now enabled. |
|||
== Main == |
|||
''Navigate to <code>main.c</code> for this part. This file is blank because you will be writing everything here.'' |
|||
=== Headers and Main === |
|||
This section is for people who are not yet super comfortable with C. Whenever you do anything with a '''symbol''' (a variable or function) or a struct in C, it must be '''declared''' prior. For functions, the standard in C is to place '''function prototypes''' (that is, a line just containing the '''signature''' of a function) in a '''header file''' (raw text file, usually ending with a <code>.h</code> suffix). Such files are then "copied-and-pasted" into each C source file you want by writing the line <code>#include "name_of_header.h"</code>. Since we want to use definitions provided by our board manufacturer in their library, we must include the file that has all those definitions. So start with the line <code>#include "stm32g0xx_hal.h"</code>. Now we want to use <code>config_clock()</code> and <code>config_led()</code> that were written in <code>config.c</code>, so write <code>#include "config.h"</code> on the next line. Finally the C standard says that a C program starts execution in a function called <code>main</code>, so define it as follows:<syntaxhighlight lang="c"> |
|||
int main(void) { |
|||
} |
|||
</syntaxhighlight> |
|||
=== Executing our Configs === |
|||
Inside of <code>main</code>, we must first call a function to start up the HAL, so the first line of <code>main</code> should be <code>HAL_Init();</code>. Now we are ready to start configuring our board for our application. First configure the clock, and then the LED:<syntaxhighlight lang="c"> |
|||
int main(void) { |
|||
HAL_Init(); |
|||
config_clock(); |
|||
config_led(); |
|||
} |
|||
</syntaxhighlight> |
|||
=== The Main Loop === |
|||
Now that we're done with the configs, it's time to start writing some application code. How do we translate the logic of turning an LED on and off into code? Suppose we had a function called <code>toggle()</code> which flips the state of the LED. So if it is off and we call <code>toggle()</code>, the LED turns on, and vice versa. The CPU is really fast, so if we just wrote that line in an infinite loop, the LED would (at a rate of almost 16 million times a second!) turn on and off. I don't know about you but my eyes cannot discern that. If I were to do this with a light switch, it's as though I would want to ''wait a little bit'' in between turning the light on and off to make it more noticeable. That's exactly what we want to do in code. Flip the switch, wait a little bit, flip the switch, wait a little bit, flip the switch, and so on. |
|||
Say we want to wait a second between flipped switches. Fortunately the HAL has a function to wait a specified number of milliseconds, called <code>HAL_Delay()</code>. The <code>toggle()</code> function provided by HAL is actually called <code>HAL_GPIO_TogglePin()</code> and takes as arguments (you guessed it) the port and pin we want to flip. Thus our final <code>main.c</code> should be as follows:<syntaxhighlight lang="c"> |
|||
#include "stm32g0xx_hal.h" |
|||
#include "config.h" |
|||
int main(void) { |
|||
HAL_Init(); |
|||
config_clock(); |
|||
config_led(); |
|||
for (;;) { // this is one way to do an infinite loop in C |
|||
HAL_Delay(1000); |
|||
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); |
|||
} |
|||
} |
|||
</syntaxhighlight> |
|||
<blockquote> |
|||
'''Exercise''' |
|||
How do you think <code>HAL_Delay</code> works? How is the CPU able to figure out how long one second actually is?</blockquote> |
|||
== |
== Compiling and Flashing == |
||
Using the provided Makefile, run the command <code>make</code> to compile, and run the command <code>make flash</code> to flash the board. Hopefully your board should be blinking! Feel free to mess around with the constant in the <code>HAL_Delay()</code> call to make the LED blink faster or slower. |
Latest revision as of 17:20, 6 October 2024
Initial Downloads[edit | edit source]
For most projects I suggest you hand-type code that is in a code block in these articles. However for files that are not super important to the lesson, I will provide downloads. So for example, in this lesson, you will be writing a main
program which means that the Makefile, startup code, and interrupts do not pertain to you just yet, and hand-typing them will not be super useful/a worthwhile use of your time. So the provided initial code is here: https://code.umd.edu/robotics-at-maryland/embedded-onboarding/lesson3
Goal[edit | edit source]
A common practice whenever one wants to program with something new is to write a very simple "proof-of-concept" program. If you are learning a new language perhaps you want to write a program that prints "Hello, World!" to stdout
, or if you are learning a new library you want to do something to that effect utilizing some basic feature of that library (such as sending a network packet over Ethernet from one computer to another saying a message like "Hello, World!"). In the case of an embedded system, the standard beginner program of choice is to write a "Blinky" program, that is, a program that turns an LED on the board on and off at a certain rate.
Exercise
Why is printing "Hello, World!" not a good first program for an embedded device?
Structure of an Embedded Program[edit | edit source]
Embedded programs follow the same abstract structure as programming for any system: set up your resources, then enter the main execution. For now, setting up your resources essentially means writing certain values to registers to tune the settings of the system, things like the clock, turning on or off certain pins on the board, or editing the speed of various communication protocols. Main execution for embedded devices will almost always live in an infinite loop. The idea is, "When I get power, turn on and run my code until I no longer have power, then I'll stop," so it makes sense to have the CPU constantly executing code (otherwise it would just be wasting power).
Configs[edit | edit source]
Navigate to config.c
. This is where we will store the configuration code. You will be writing the code for config_led()
.
One benefit of programming embedded devices is the freedom you get to configure anything you could possibly imagine on the system. The first thing you'll encounter is the clock. The official maximum clock rate of the CPU is 64 MHz (meaning 64 million CPU cycles occur every second, more on this later). So with that constraint, you are free to set the clock as you wish. For this project I have set the clock to run at the default value of 16 MHz, since right now we are not doing anything super intensive.
Now we want to blink the LED. The LED will turn on when it gets power, and off when it doesn't have power. The LED is connected to the CPU via one of the pins (on this system, it happens to be GPIO PA5). Since almost every pin starts disabled by default, we need to tell the hardware we want PA5 enabled with the ability to output an electrical signal (which will then toggle to turn the LED on and off). According to the official library documentation, we need to turn on the clock for the pin, and use HAL_GPIO_Init()
to configure it. This function takes a GPIO port (which for now you can think of as a method the hardware uses to organize all the GPIO pins) and a configuration stored in a C struct GPIO_InitTypeDef
. As per the documentation, we want the following configuration:
GPIO_InitTypeDef led_config = {
.Pin = GPIO_PIN_5,
.Mode = GPIO_MODE_OUTPUT_PP,
.Pull = GPIO_NOPULL,
.Speed = GPIO_SPEED_FREQ_LOW,
.Alternate = 0
};
Line by line we have:
- We want to edit pin 5 of Port A (hence the pin name PA5)
- We want to output data with this pin, and we will use Push-Pull to do so (don't worry about what that means for now, just know that it means electrical signals will be outputted on this pin)
- You can ignore the
.Pull
and.Speed
lines as well .Alternate
is a field that is used if we wanted to do something more complicated with this pin, such as digital communication with other devices. Since we are not doing anything complicated, we set this to0
, meaning don't do any alternate function
Then turn on the clock and write the config:
__HAL_RCC_GPIOA_CLK_ENABLE();
HAL_GPIO_Init(GPIOA, &led_config);
That's it! Our LED is now enabled.
Main[edit | edit source]
Navigate to main.c
for this part. This file is blank because you will be writing everything here.
Headers and Main[edit | edit source]
This section is for people who are not yet super comfortable with C. Whenever you do anything with a symbol (a variable or function) or a struct in C, it must be declared prior. For functions, the standard in C is to place function prototypes (that is, a line just containing the signature of a function) in a header file (raw text file, usually ending with a .h
suffix). Such files are then "copied-and-pasted" into each C source file you want by writing the line #include "name_of_header.h"
. Since we want to use definitions provided by our board manufacturer in their library, we must include the file that has all those definitions. So start with the line #include "stm32g0xx_hal.h"
. Now we want to use config_clock()
and config_led()
that were written in config.c
, so write #include "config.h"
on the next line. Finally the C standard says that a C program starts execution in a function called main
, so define it as follows:
int main(void) {
}
Executing our Configs[edit | edit source]
Inside of main
, we must first call a function to start up the HAL, so the first line of main
should be HAL_Init();
. Now we are ready to start configuring our board for our application. First configure the clock, and then the LED:
int main(void) {
HAL_Init();
config_clock();
config_led();
}
The Main Loop[edit | edit source]
Now that we're done with the configs, it's time to start writing some application code. How do we translate the logic of turning an LED on and off into code? Suppose we had a function called toggle()
which flips the state of the LED. So if it is off and we call toggle()
, the LED turns on, and vice versa. The CPU is really fast, so if we just wrote that line in an infinite loop, the LED would (at a rate of almost 16 million times a second!) turn on and off. I don't know about you but my eyes cannot discern that. If I were to do this with a light switch, it's as though I would want to wait a little bit in between turning the light on and off to make it more noticeable. That's exactly what we want to do in code. Flip the switch, wait a little bit, flip the switch, wait a little bit, flip the switch, and so on.
Say we want to wait a second between flipped switches. Fortunately the HAL has a function to wait a specified number of milliseconds, called HAL_Delay()
. The toggle()
function provided by HAL is actually called HAL_GPIO_TogglePin()
and takes as arguments (you guessed it) the port and pin we want to flip. Thus our final main.c
should be as follows:
#include "stm32g0xx_hal.h"
#include "config.h"
int main(void) {
HAL_Init();
config_clock();
config_led();
for (;;) { // this is one way to do an infinite loop in C
HAL_Delay(1000);
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
}
Exercise
How do you think
HAL_Delay
works? How is the CPU able to figure out how long one second actually is?
Compiling and Flashing[edit | edit source]
Using the provided Makefile, run the command make
to compile, and run the command make flash
to flash the board. Hopefully your board should be blinking! Feel free to mess around with the constant in the HAL_Delay()
call to make the LED blink faster or slower.