Segment Basics - AlbertGBarber/PixelSpork GitHub Wiki
For the full, raw code specs segment sets, segments, and sections, see Segment Set Class Notes, Segment Class Notes, and Section Struct Notes.
Jump To:
- What and Why Are Segment Sets
- The Building Blocks of Segment Sets
- Segment Sections
- Segments
- Segment Sets
- Using Segment Sets
- How are Segment Sets 2D?
- Advanced Topics
"Segments Sets" are basically the reason for this library's existence. Many years ago, when I first started working with addressable LEDs, I quickly ran into some frustrating limitations because, while addressable LEDs are undoubtedly smart, they are also purposefully dumb. Each LED knows nothing of its location within all the LEDs -- it only knows is how to light-up and pass-on lighting info to the next LED in line. Overall this is not a bad thing, it keeps the LEDs cheap and fast, while making it easy to connect and write to them. However, you are limited to using the LEDs as a single line in whatever orientation you wired them in.
Before writing this library, I looked into ways of virtually organizing my LEDs. The most obvious solution was to use an array or 2D matrix to arrange the LEDs. This is fine, but defining the array is tedious because you have to list all the LEDs, and it takes up quite a bit of memory, with the problem only getting worse the more LEDs you have. Likewise, for 2D effects, I found matrices unintuitive. Sure, they work great if your LEDs are already arranged in an 8x8 or 16x16 matrix, but what if they're setup in rings, or as part of a complex shape like a dodecahedron. In these cases you have to "squeeze" the shape into a square matrix, usually by defining a bunch of virtual "dummy" LEDs to fill in gaps. For me, this wasn't a very satisfying solution.
"Segments Sets" are my solution to virtually arranging LEDs. Instead of trying to "squeeze" your shape into a matrix, you define your shape by grouping sets of LEDs together, translating it directly from the physical to the virtual. For example, lets say you have a set of nested led rings, which you want to organize into a radial 2D disk, as shown below:
To create the disk shape, you would first group the LEDs by what ring they are in, separating each ring into an individual segment. You would then group the segments together using a segment set, creating the disk. To me, this is intuitive because you are describing the shape as you see it. I'll explain each of the steps to create segment sets later, but if you just want to jump into code, I have a full example for the disk (with code) here.
When the disk is used in effects, its 2D aspects are worked out on the fly -- the code determines how the rings "squeeze" into a 2D matrix. Not only does this mean that you don't have to do any of the "squeezing", but you can also manipulate segments sets at run time -- change their segments, their directions, color mode and more. Using segments sets also makes it easier to define more complex shapes, such as splitting the disk into two halves.
While segments sets are aimed at describing 2D shapes, they also work perfectly well for describing 1D shapes (ie a line of pixels). With the rings above, if I wanted to chain all the rings together into one line, I'd just use a single segment containing all the LEDs.
However, all of this does come some tradeoffs -- while, in most cases, segments should take up less memory than a traditional matrix, they require more CPU performance because more work is being done at run time to get the LED locations. In general, the more complex (how many segments and sections there are, not how long or large!) your segments are, the larger the performance hit will be. In practice, when testing on an ESP8266 with a simple segment set, you can easily get above 1000 LEDs on a single pin before you start to see slow-downs (with a good chunk of the slow-down probably coming just from writing to the LEDs, but I need to do some proper testing.....). Overall, I believe this is acceptable, especially because micro-controllers are getting better every year.
That being said, I do recommend running this library on a 32-bit or better micro-controller, such as an ESP or Teensy series MC. Your classic 8-bit Arduino can still run everything, but you'll run into performance and/or memory restrictions sooner. Unfortunately it's hard for me to give any hard caps for number of LEDs, effects, etc because everyone's requirements are unique.
That's enough theory for now, lets move on to the segment sets themselves.
Segment Set Structure:
A Segment Set is composed of a group of Segments, which are in turn composed of a group of Sections. A section is a group of LEDs.
To illustrate this visually:
graph TD;
SegmentSet-->Segment1;
SegmentSet-->Segment2;
Segment1-->Sec1(Section 1-1st group of LEDs);
Segment1-->Sec2(Section 2-2nt group of LEDs);
Segment2-->Sec3(Section 3-3rd group of LEDs);
Segment2-->Sec4(Section 4-4th group of LEDs);
In the diagram above, the segment set has two segments, who each have two sections. Note that this is just an example; segments sets and segments can have between 1 and 256 sub-components.
Next I'll go over each level in detail. If you're feeling a little lost, that's okay, hopefully the next page sections will begin to tie everything together.
For the full, raw code specs for each component, see Segment Set Class Notes, Segment Class Notes, and Section Struct Notes.
To jump into a fully worked example setup (using the LED rings pictured above), see the Ring Segments Example.
For full technical specs see: Section Struct Notes.
Segment sections are the basic building blocks of segment sets. A section is a group of LEDs addresses, with one or more sections being strung together to form a segment.
Sections come in two flavors: continuous and mixed. Theses sections can be used interchangeably (you can represent the same group of LEDs using either section type). Each type is intended tackle a specific configuration situation:
- Continuous sections work best when you have an unbroken length of LEDs.
- Mixed sections work best when you are grouping LEDs together from different parts of your LED strip.
Most of the time, "correct" type of section for your setup should be fairly obvious, with the least "complex" solution usually being the best. Likewise choosing the "correct" arrangement will lead to better performance and lower memory usage. That being said, either type can be used to describe any given group of LEDs, and sometimes you have edge cases where both types are about the same, so you're free to do whatever you're more comfortable with. See "Section Type Trade Offs" below for more info.
Below I will go over each section type in more detail.
Continuous sections are used to represent unbroken strings of LEDs. For example, if I wanted to store the entire LED strip, I would use a continuous section.
Continuous sections are represented by a starting pixel and a length. The start pixel is where the section starts, and the length is how long the section is.
For a code example, lets refer back to the LED rings image above. Lets say we wanted to make the outer (24 LED) ring into a segment. Assume the ring's LEDs are all wired in order going from 0 to 23, so we can represent it as a single continuous section:
const PROGMEM segmentSecCont ringSec0 = {0, 24}; //outer ring, 24 pixels
The line above creates a continuous section (type segmentSecCont
) named ringSec0
. The section starts at the first LED in the strip, and has a length of 24 LEDs. Note that sections are stored in program memory, as indicated by the const PROGMEM
. This saves run time memory (which is typically much smaller than programming memory), but does prevent you from modifying sections during run time.
For "fun", lets also setup the next ring (16 LEDs) as a section. For this example, the 16 LED ring is connected at end of the outer ring, so that they form a single line of pixels. In other words, the 24th LED (counting from 0) is the first LED in the 16 pixel ring.
const PROGMEM segmentSecCont ringSec1 = {24, 16}; //inner ring 1, 16 pixels
Here we've created a section for the 16 LED ring, named ringSec1
. It starts at LED 24, and has a length of 16 LEDs. Pay close attention to the starting number. At first glance you might think it will overlap with the last LED in the outer ring. However, this is not the case because we start counting LEDs from 0 instead of 1; the first LED in the strip is LED 0. So, although the outer ring is 24 LEDs long, it actually ends at LED 23 (try counting the LEDs starting with 0, and you'll see what I mean). This "off-by-1" is a common programming gotcha that you have to keep in mind when creating sections.
Note that continuous sections have a max length of 32,767 LEDS. You can have reverse sections by setting the length negative, which is covered in advanced segment usage.
Mixed sections are used to group LEDs that are in different parts of the strip. A mixed section is represented by an array that holds the LED locations, and a length that indicates the number of LEDs.
For example, referring back to the LED rings, lets say I wanted to form a segment for the outer ring, but I wanted to skip the LEDs on the 0th, 6th, 12th, and 18th LEDs. I could use a mixed section like:
const PROGMEM uint16_t ringSec0_arr[] = {1, 2, 3, 4, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 19, 20, 21, 22, 23}; //The array of LED locations
const PROGMEM segmentSecMix ringMixSec0 = { ringSec0_arr, SIZE(ringSec0_arr) }; //SIZE() gets the length of the array (19)
The first line creates an array of LED strip addresses named ringSec0_arr
. The array includes all the LEDs in the outer ring, except of those that we are skipping (0, 6, 12, 18). The second line creates the section (type segmentSecMix
), named ringMixSec0
, taking the ringSec0_arr
and its length as arguments.
Note that we are using the SIZE() macro to automatically set the array length.
Like with continuous sections, mixed sections are stored in programming memory, so they cannot be modified once the program is running.
Segments can either contain mixed or continuous sections, but not both at once, so it's important to pick the section type that works the best for your setup. However, note that the segments with in a segment set can have different section types, just not different types within the same segment, so you aren't restricted to one type for your whole set!
Now, take another look at the mixed sections ring example above. Note that we've only skipped 4 LEDs, so we've basically just listed out the whole ring to form the section.
Wouldn't it be better to split it in to four continuous sections? Like so:
//Sections for the outer ring, skipping LEDs 0, 6, 12, 18
const PROGMEM segmentSecCont ringSec1 = {1, 5}; //outer ring section 1 (LEDs 1 - 5)
const PROGMEM segmentSecCont ringSec2 = {7, 5}; //outer ring section 2 (LEDs 7 - 11)
const PROGMEM segmentSecCont ringSec3 = {13, 5}; //outer ring section 3 (LEDs 13 - 17)
const PROGMEM segmentSecCont ringSec4 = {19, 5}; //outer ring section 4 (LEDs 19 - 23)
This representation is equivalent to the ringMixSec0
we created before, but is it better? Yes and no. In general, having more sections makes it harder for the code to find an LEDs location (which needs to happen whenever we want to color that LED), but continuous sections usually require less programming memory than mixed sections, since they don't need to store every single LED. So, in this case, both representations are equally good/bad, and it's more-or-less personal preference.
Sometimes, the choice is obvious. Consider an 8x8 LED matrix. The LEDs are wired together into rows, so that each row is a continuous length of LEDs. IE row 1 is LEDs 0 to 7. To represent the matrix rows, we would use a series of continuous sections, with one section for each row. Using a set of mixed sections would just be a waste of memory, given we only need one continuous section for each row. Meanwhile, the matrix columns each consist of 8 single, disconnected LEDs, so it makes more sense to use mixed sections to represent them. Using continuous sections would require a single length section for each LED, which would both run slower, and take up more memory than using mixed sections (and be much more tedious to define!). To illustrate this further, I've written a separate page with example layouts for an 8x8 matrix here, but you should finish this page before checking it out.
In general, the choice is easier for larger numbers of LEDs, because the swings in memory/performance will be larger. For low numbers of LEDs, such as in the rings example, the change in memory/performance is usually negligible and so the representation is up to personal preference. You can always try both representations! I usually lean towards continuous sections because they're easier to read at a glace.
For full technical specs see: Segment Class Notes.
A segment is a group of sections that is treated as a continuous line of LEDs, going from one section to the next. A segment can either have continuous or mixed sections, but not both at once. Segments have a direction (direct
), which sets their orientation. A value of true
means the segment is oriented starting with the first LED in the first section and running to the last LED in the last section. A value of false
reverses the direction, so the segment starts at the last LED in the last section and runs to the first LED in the first section. Effects will draw LEDs according to the segment's direction. You can change a segment's direction freely during run time so you can reverse the direction of effects.
Segments are created using the Segment
class. Segments are assembled from an array of sections (up to 256), the array's length (number of sections), and a direction. For example, let's create a segment for the outer ring of the LED disk pictured above (ie ringSec0
from the sections examples):
const PROGMEM segmentSecCont ringSec0_arr[] = { {0, 24} }; //The outer ring section array (one section starting at the 0th LED, with length 24)
SegmentPS ringSeg0 = { ringSec0_arr, SIZE(ringSec0_arr), true); //The outer ring segment, containing the outer ring section array
The first line above creates an array of sections, ringSec0_arr
, for our segment. Within the array, we have a single section, starting at the 0th LED, and running 24 LEDs in length (ending at LED 23). Note that unlike the previous section examples, we've not created a specific variable for the individual section ( {0, 24} ), instead it's packaged into the array. This is a shortcut that saves us having to define a variable for each section in the array: ie creating the array like: ringSec0_arr[] = {ringSec0};
where ringSec0
is {0, 24}
section.
The second line above creates the segment, ringSeg0
, using the section array, the length of the array (1) (automatically calculated using the SIZE()
macro), and the direction of the segment.
For completeness, lets create a segment with multiple sections by revisiting the example above where we created ring sections skipping LEDs 0, 6, 12, 18.
const PROGMEM segmentSecCont ringSec0_arr[] = { {1, 5}, {7, 5}, {13, 5}, {19, 5} }; //The outer ring, split into sections, skipping LEDs 0, 6, 12, and 18
SegmentPS ringSeg0 = { ringSec0_arr, SIZE(ringSec0_arr), true); //The outer ring segment, containing the outer ring section array
Like, before, the first line creates the array of continuous sections, ringSec0_arr
, while the second line creates the actual segment, ringSeg0
. Each section is length 5, with their starting LEDs being set to skip over LEDs 0, 6, 12, and 18. Note that I've used the section shortcut by inserted the sections directly into the array, rather creating variables for each of them.
Finally, let's create a segment using mixed sections:
First make a mixed section:
const PROGMEM uint16_t myMixSec_arr[] = {1, 2, 3, 4, 5}; //The array of LED locations
const PROGMEM segmentSecMix myMixSec = { myMixSec_arr, SIZE(myMixSec_arr) }; //SIZE() gets the length of the array (19)
Then place the section in a segment:
const PROGMEM segmentSecMix myMixSec_sec_arr[] = { myMixSec };
SegmentPS segment3 = { myMixSec_sec_arr, SIZE(myMixSec_sec_arr), true };
Note that like with continuous sections, you must place your mixed sections into an array before turning them into a segment. This allows you to group multiple mixed sections together. In the example above, we created the mixed section as its own variable, myMixSec
before placing it in the myMixSec_sec_arr
array. This allows you to re-use myMixSec
in multiple segments, but is a bit cumbersome.
Like with continuous sections, you can place the mixed section definition directly in the array:
const PROGMEM uint16_t myMixSec_arr[] = {1, 2, 3, 4, 5}; //The array of LED locations
const PROGMEM segmentSecMix myMixSec_sec_arr[] = { { myMixSec_arr, SIZE(myMixSec_arr) } };
SegmentPS segment3 = { myMixSec_sec_arr, SIZE(myMixSec_sec_arr), true };
This is a bit harder to read than with continuous sections, but is the same overall.
For full technical specs see: Segment Class Notes.
Segment sets are the final step in organizing LEDs; grouping multiple segments together to form a 2D shape. Each segment is a line of LEDs, so by stacking them together in a set, we form a full shape. Using the LED rings as an example again, if we set each ring to be a single segment, we can group them together to form a full, 2D disk. This disk then becomes the canvas on which our effects paint.
To create a segment set, we need two things: an array of segments, and an array for storing the segment's LED colors (ignore this for now).
For example, to create the disk using the rings from the previous example we would:
//Create an array for storing the colors of all the LEDs in the rings (61 LEDs total)
//This is also required by FastLED
CRGB leds[61];
//Create a segment for each ring using one continuous section for each
//This is just using the code you've already seen for segments and sections
const PROGMEM segmentSecCont ringSec0[] = {{0, 24}}; //outer ring 1, 24 pixels
SegmentPS ringSegment0 = { ringSec0, SIZE(ringSec0), true };
const PROGMEM segmentSecCont ringSec1[] = {{24, 16}}; //outer ring 2, 16 pixels
SegmentPS ringSegment1 = { ringSec1, SIZE(ringSec1), true };
const PROGMEM segmentSecCont ringSec2[] = {{40, 12}}; //outer ring 3, 12 pixels
SegmentPS ringSegment2 = { ringSec2, SIZE(ringSec2), true };
const PROGMEM segmentSecCont ringSec3[] = {{52, 8}}; //outer ring 4, 8 pixels
SegmentPS ringSegment3 = { ringSec3, SIZE(ringSec3),true };
const PROGMEM segmentSecCont ringSec4[] = {{60, 1}}; //outer ring 5, 1 pixel (this is the disk's center pixel)
SegmentPS ringSegment4 = { ringSec4, SIZE(ringSec4), true };
//With all the segments created, we can group them into a segment set to form the disk
//First we create an array with all the ring segments, ordered from greatest to least LEDs (note that it's an array of pointers to the segments)
SegmentPS *rings_arr[] = { &ringSegment0 , &ringSegment1, &ringSegment2, &ringSegment3, &ringSegment4 };
//Now we create our disk segment set using an array for color storage, and the segment array from above
SegmentSetPS diskSet( leds, SIZE(leds), rings_arr, SIZE(rings_arr) );
That may look like a lot of code, but most of it is just creating a segment for each ring. The code we care about is in the last two lines, SegmentPS *rings_arr[] = { &ringSegment0 , &ringSegment1, &ringSegment2, &ringSegment3, &ringSegment4 };
, and SegmentSetPS diskSet( leds, SIZE(leds), rings_arr, SIZE(rings_arr) );
. In the first line we create an array of segments (it is actually an array of pointers to the segments, hence the "&"s, but you can ignore this). This is just like when we created an array of sections for each segment. The next line creates the segment set (type SegmentSetPS
), named diskSet
, using a CRGB
color array, leds
, for color storage, the ring segment array, and the lengths of both arrays (calculated automatically using SIZE()
).
But what is the leds
color storage array? Because addressable LEDs are dumb, being only able to receive data from the previous neighbor and then pass it on to their next, whenever we want to change the color of any LED, we need to re-write the colors of all the LEDs. For example, lets say we have 10 LEDs, and want to change the color of the 8th. We need to pass the color from the 1st, to the 2nd, etc, all the way to the 8th. This takes the same amount of time as if we were changing all 8 LED's colors, so instead of trying to write to each LED individually, it is easier to write to all the LEDs at once as a "frame". However, we need somewhere to store and assemble the frame before we write it out. FastLED uses a CRGB array, usually named leds
for this storage.
In the example code above, CRGB leds[61]
, is the frame storage. You usually only have one of these arrays for your whole program, with it being sized to match the number of LEDs in your strip. Effects write to the frame using the segment set, so the LED array needs to be included within the set.
The main way you use segment set is in effects. Segment sets are the canvas on which an effect paints, so every effect requires a segment set. In other words, effects know how to paint, while segment sets tell them where to paint. You can't make a picture with just paint or a canvas; you need both. A segment set is always supplied as an argument in an effect's constructor. You can read more about that here.
Segment sets also act as an interface for their individual segments, and include functions for accessing LED addresses and other variables. These functions are listed on the segment set notes page. Most of these functions are intended to be used by effects, so you probably won't need to touch them ever, however there are a few exceptions:
-
Generally, effects will draw themselves according to each segment's direction, so to reverse the direction of an effect, you need to reverse the direction of the segments. Segment sets include various direction setting functions to change all the segments' directions en-mass. You can find the full list here, but you'll probably use the
flipSegDirects()
most often, which simply reverses all the segments directions at once. -
Segment sets feature a diverse array of color mode settings, which focus on adding rainbows or custom gradients to your effects. This is a whole topic by itself, and is explained on the color modes page.
To access a segment set setting or function you would do (using the diskSet
segment set for example):
diskSet.flipSegDirects(); //Call flipSegDirect() to reverse the directions of all the segments in the diskSet segment set
diskSet.runOffset = true; //Set the diskSet's color mode runOffset value to true
There are also various helper functions used for drawing on segment sets. To keep the segment set code more clear, these have been partitioned off into their own namespace: segDrawutils
. They are not part of segment sets directly, but all require a segment set to be used. You probably won't need to use these very often, as effects usually handle most of the heavy lifting. You can find the full list of these functions here.
Finally, you can change segment sets during run time by swapping in/out different segments. Most effects should be okay with this, but it's not something I think most people will do very often, so it's never been a priority when writing effects. In other words, there may be bugs. If you do change a segment set's sections, be sure to call setNumLines()
and setNumLeds()
segment set functions, to re-calculate various segment variables. Note that while swapping around segments is okay, you should avoid changing segments themselves beyond setting their directions!
So far I've said that segment sets allow you to create 2D shapes with LEDs, but how does that work in practice? If you take a look at the list of effects in the right-hand wiki sidebar you may notice that each effect is tagged with either Seg Line, Seg, or nothing at all. These tags indicate how each effect interacts with segment sets, with first two indicating that the effect will be 2D. Effects with no tags indicate that the effect is 1D, treating the segment set as one continuous line.
Effects tagged with Seg means that the effect will treat each entire segment as a single pixel or independent line (depending on the effect). For example, the Segment Waves effect draws a repeating color pattern on the segment set, with each segment being a different color. Ie, if you had a pattern of red, green, blue, one whole segment would be red, the next blue, etc. Likewise, the Rain (Seg) creates a set of pixelated rain drops akin to the classic Matrix movie "code" effect, where each segment is treated as an independent line with its own rain.
Effects tagged with Seg Line work like those tagged with Seg, but the effect treats each segment line as a single pixel or independent line. Most effects use this tag.
Note that some effects have multiple tags. This means the effect can be configured to use segment lines, segments, or can possibly be locked to 1D.
Note that if an effect has either a Seg Line, Seg it will always try to draw in 2D (except for a few effects with more configuration options). If you want to keep an effect 1D, you must pass in a 1D segment set, ie a set with a single segment.
For live examples of how different segment sets change how effects look see the LED rings examples here.
Segment lines are lines that connect single pixel from every segment while trying to keep the line as straight as possible. They are "perpendicular" to the segment's direction. They are always determined dynamically, so if you modify the segment set, the segment lines will change too. So far, this probably makes little sense, but looking at the picture below should help.
In the image, we have an typical 8x8 matrix of addressable LEDs. In code, the matrix will be represented by a segment set consisting of 8 segments -- one for each of the matrix's rows. The segment lines are the colored lines in the image; they are the columns of the matrix, but why? For each pixel in the first (0th) segment, the code tries to draw a straight line from that pixel, through one pixel in every segment, to a pixel in the final segment. Because the 8x8 matrix is square, the segment lines are the straight columns of the matrix; each pixel in each segment corresponds to exactly one line.
But what if our segment set isn't square? What if each segment is a different length? The code still tries to draw the straightest line, but some pixels will be members of multiple lines. Take a look at the gif below. It shows the LED rings, formed into a 2D disk (each ring is one segment), as discussed in previous examples. The Segment Set Check utility is being run on the disk, which highlights the first and last segment lines in red and blue, while pulsing each other segment line in green one at a time.
As you can see, because the each ring is a different length, the segment lines overlap across multiple pixels. This is especially noticeable for the inner most "ring", which is just a single pixel. This pixel is a part of every segment line because it is the only pixel available in the segment (if the segment had two pixels, each would be in half the segment lines). In the gif, the pixel is always colored blue, because the blue segment line is always drawn last, overwriting the green/red from the other lines.
Finally, it's important to note that the number of segment lines in a segment set is always the same as the length of the longest segment. This is because, to be "straight", each segment line can only pass through one pixel per segment, so each pixel is included in the minimum number of segment lines possible. For the longest segment, this is one line per pixel.
Hopefully that all makes some sense, but maybe it would be helpful to see segment lines in action:
The two gifs below show the same effect running on the LED rings using two different segment sets (I've put a bit of paper over the rings to help diffuse the LEDs). The first is just a single, 1D line, so the effect is just 1D, running from the first to last pixel. The second gif shows the effect running on the disk segment set using segment lines. As if by magic, the effect is adapted to the disk shape, with the color bands shrinking proportionally to the center of the disk. I have done no extra configuration work here (although you could change the effect's spacing, speed, etc), I simply changed what segment set the effect is running on and the code handles the rest!
The effect on a 1D segment set | The effect on the 2D disk segment set |
At this point you should be proficient enough to start using segment sets, but there are a few extra features or topics that are too specific or uncommon to discuss here. They are discussed on the advanced segment usage page. While this reading isn't strictly needed, it may be helpful if you have a tricky LED configuration or other issue.
It will probably take some time understand segment sets. The easiest way is to try them out! For more you can check out the rings examples here, or the "2D Segment Sets for 2D Effects" code example in the library.