Chapter 11.4: Control RGB LEDs with an LED Controller Part 4
05/144/2016 | 06:18 AM
In the last section, we built a software driver for the TI TLC5940 LED driver and used it to illuminate some test LEDs. We also figured out how to debug the driver with the help of the Simplicity Studio tools. In this section, we will create some data structures to make it easier to work with named colors, and then improve that driver to make use of the DMADRV library. This library allows us to more easily develop code that invokes the DMA peripheral.
Setting Specific Colors
We can develop some helper functions to turn on individual colors per LED driver channel and a color mixer to create standard blended colors such as purple.
typedef struct color_code_bits
{
uint8_t red;
uint8_t green;
uint8_t blue;
} color_code_struct;
#define WHITE { 0xFF, 0xFF, 0xFF }
#define RED { 0xFF, 0x00, 0x00 }
#define GREEN { 0x00, 0xFF, 0x00 }
#define BLUE { 0x00, 0x00, 0xFF }
#define PURPLE { 0xFF, 0x00, 0xFF }
#define YELLOW { 0xFF, 0xFF, 0x00 }
#define ORANGE { 0xFF, 0x0F, 0x00 }
// Sets the color in memory but does not write it
void set_color_buffer(uint8_t led_number, color_code_struct color)
{
const uint8_t ch0 = led_number * 3;
const uint8_t ch1 = ch0 + 1;
const uint8_t ch2 = ch1 + 1;
// Shift the 8-bit RGB code over by 4 to get to 12 bits of GS
stream.channel[ch0].grayscale = (uint16_t) (color.red << 4);
stream.channel[ch1].grayscale = (uint16_t) (color.green << 4);
stream.channel[ch2].grayscale = (uint16_t) (color.blue << 4);
}
These functions can then be used to set whatever color codes we want per channel, like purple/yellow/purple:
const color_code_struct purple = PURPLE;
set_color_buffer(0, purple);
const color_code_struct yellow = YELLOW;
set_color_buffer(1, yellow);
set_color_buffer(2, purple);
// Now send the stream to the TLC5940
stream.mode = GRAYSCALE_MODE; // Now write the GS data
write_serial_stream();
This resulted in a nice purple color on the first and third LED, but the yellow LED had a bit of a greenish tint. Likewise, setting all colors to 0xFF resulted in a blue-ish white. It seems necessary to compensate for the relative brightness of each color LED in the RGB set to get the blended colors just right.
Introduction to Direct Memory Access (DMA)
We used DMA in the last chapter with the help of the SPIDRV library, but we didn’t have to program anything in the DMA engine ourselves. We could use SPIDRV again here, but instead we will learn another way to set up DMA ourselves so that we gain a deeper understanding of how it works.
The DMA peripheral is a highly-configurable tool that moves data from one place to another, and it does so without help from the MCU once the transfer is started. This allows the MCU to sleep or do other things. There are multiple channels available to perform multiple transfers between different sources and destinations. The DMA transfer can be triggered through many sources and generates an interrupt when it is done moving the data. It is a simple mechanism, but it is sometimes difficult to understand and can be harder to debug than sequential programming. The toughest part is understanding how to set it up. Fortunately, Silicon Labs has provided a DMA driver called DMADRV. To use this driver, all you need to do is include the dmadrv.h file at the top of your source code as well as copy the required files from the Simplicity Studio emdrv directory. See the example in Github here for the required files that I have copied into the project src directory.
The DMA peripheral can also be set up through use of the em_drv library. There are examples in Application Note AN0013 Direct Memory Access that configure things more manually. In short, the DMA peripheral looks for something called a descriptor table located in RAM that holds the configuration of the data transfer. This configures the DMA behavior, along with the configuration registers that reside inside the DMA peripheral. All of these things need to be configured, and then the DMA peripheral can do the transfer once its trigger event occurs. The descriptor table is used by the DMA peripheral at the trigger event to determine how to complete the transfer, including end addresses, address increment size, number of transfers, etc. When the transfer is complete, the DMA peripheral can be configured to issue an interrupt.
There are four modes of operation of the DMA peripheral. DMADRV only simplifies the use of the Basic DMA mode, but does not do too much to help with the other advanced modes. That is good for this example, since we only need Basic DMA mode for transferring data from our array in RAM to the USART peripheral.
To use the DMADRV, all we need to do is put a bit of code in the beginning of our main program to initialize the DMADRV and let it find an open DMA channel for us. This is great, because we don’t have to worry about using the same channel that SPIDRV or some other module is already using.
// These variables are global, at the top of the file
unsigned int dma_channel;
// Transfer Flag
volatile bool dma_in_progress;
// The following code is in the main function
Ecode_t result;
// Initialize DMA.
result = DMADRV_Init();
if (result != ECODE_EMDRV_DMADRV_OK)
{
DEBUG_BREAK
}
// Request a DMA channel.
result = DMADRV_AllocateChannel( &dma_channel, NULL );
if (result != ECODE_EMDRV_DMADRV_OK)
{
DEBUG_BREAK
}
Now, we have a DMA channel that we can pass into the DMADRV functions. To make the transfer, we can replace the for loop that we used inside of the write_serial_stream function with the DMADRV_MemoryPeripheral function. First, we define the callback function that is called when the DMA transfer is complete:
void dma_transfer_complete(unsigned int channel, bool primary, void *user)
{
// Clear flag to indicate that transfer is complete
dma_in_progress = false;
}
Next, we have to change a few things inside of the write_serial_stream function to handle the DMA transfer. If you recall from the previous section, we incremented through the serial_stream array backwards in order to send the most significant byte first on the USART, as the TLC5940 expects to see the data.
// Now write the stream, MSByte first
for (int i=length-1; i>=0; i--)
{
USART_Tx(USART1, stream_buffer[i]);
}
But we can’t do that with DMA, as it can only iterate through memory in the forward direction. So we have to fill the serial_stream array backwards instead, and then the direction in which the DMA peripheral fetches the data from memory and pushes it to the USART will be in the correct order.
// This must be global to be used for DMA
uint8_t stream_buffer[MAX_STREAM_BIT_LEN/8];
void write_serial_stream()
{
int length;
// Must pack the bits in backwards for DMA driver
if (stream.mode == DOT_CORRECTION_MODE)
{
length = DC_STREAM_BIT_LEN / 8;
for (int i=0; i < length; i++)
{
stream_buffer[length-i-1] = pack_dc_byte(i);
}
}
else
{
length = MAX_STREAM_BIT_LEN/8;
for (int i=0; i < length; i++)
{
stream_buffer[length-i-1] = pack_gs_byte(i);
}
}
// Set/clear the VPRG pin
if (stream.mode == DOT_CORRECTION_MODE)
{
GPIO_PinOutSet(CONTROL_PORT, VPRG_PIN);
}
else
{
GPIO_PinOutClear(CONTROL_PORT, VPRG_PIN);
}
dma_in_progress = true;
// Start the DMA transfer.
DMADRV_MemoryPeripheral( dma_channel,
dmadrvPeripheralSignal_USART1_TXBL,
(void*)&(USART1->TXDATA),
stream_buffer,
true,
length,
dmadrvDataSize1,
(void *) dma_transfer_complete,
NULL );
while (dma_in_progress)
; //EMU_EnterEM2(true);
for (volatile int i=0; i < 10000; i++)
;
// Latch the data
GPIO_PinOutSet(CONTROL_PORT, XLAT_PIN);
for (volatile int i=0; i < 100; i++)
;
GPIO_PinOutClear(CONTROL_PORT, XLAT_PIN);
}
You can see that we replaced for loop that sent data to the USART within write_serial_stream with the DMADRV_MemoryPeripheral function, which means that the data is flowing from memory to a peripheral. There is a companion function that goes the other way.
Before the DMA_MemoryPeripheral function, we set a flag for dma_in_progress, and then waited for it to clear before moving on. The flag clear takes place inside the dma_transfer_complete callback function when the DMA transfer completes. However, instead of waiting in a while loop for the DMA to finish, we could have let the MCU go work on something else or entered into a sleep state, as long as we take care ahead of time to configure the DMA interrupt to wake the MCU from sleep.
You should notice that the serial_stream array was moved outside of the write_serial_stream function. It is super important that you make all variables that are referenced by DMA globally persistent. In our example, it makes no difference because we wait for the DMA transfer to finish, but as soon as we remove that blocking logic and allow the system to leave this function, the local variable serial_stream will be reclaimed by the system for other purposes and the data will become mangled. By making serial_stream global, it is preserved even after the write_serial_stream function exists, when the DMA transfer might still be in progress.
This should have been an easy introduction to DMA transfers. We will revisit DMA in future chapters to set up more complicated modes and really try to melt your brain.
This completes the chapter on interfacing with an external LED driver over a non-standard serial stream interface. By now, you should be well on your way to becoming a competent solderer and getting to know your way around the initialization of a few EFM32 peripherals. You should also feel like something of an expert in the types and applications of LEDs.
Chapter 11.4: Control RGB LEDs with an LED Controller Part 4
In the last section, we built a software driver for the TI TLC5940 LED driver and used it to illuminate some test LEDs. We also figured out how to debug the driver with the help of the Simplicity Studio tools. In this section, we will create some data structures to make it easier to work with named colors, and then improve that driver to make use of the DMADRV library. This library allows us to more easily develop code that invokes the DMA peripheral.
Setting Specific Colors
We can develop some helper functions to turn on individual colors per LED driver channel and a color mixer to create standard blended colors such as purple.
These functions can then be used to set whatever color codes we want per channel, like purple/yellow/purple:
This resulted in a nice purple color on the first and third LED, but the yellow LED had a bit of a greenish tint. Likewise, setting all colors to 0xFF resulted in a blue-ish white. It seems necessary to compensate for the relative brightness of each color LED in the RGB set to get the blended colors just right.
Introduction to Direct Memory Access (DMA)
We used DMA in the last chapter with the help of the SPIDRV library, but we didn’t have to program anything in the DMA engine ourselves. We could use SPIDRV again here, but instead we will learn another way to set up DMA ourselves so that we gain a deeper understanding of how it works.
The DMA peripheral is a highly-configurable tool that moves data from one place to another, and it does so without help from the MCU once the transfer is started. This allows the MCU to sleep or do other things. There are multiple channels available to perform multiple transfers between different sources and destinations. The DMA transfer can be triggered through many sources and generates an interrupt when it is done moving the data. It is a simple mechanism, but it is sometimes difficult to understand and can be harder to debug than sequential programming. The toughest part is understanding how to set it up. Fortunately, Silicon Labs has provided a DMA driver called DMADRV. To use this driver, all you need to do is include the dmadrv.h file at the top of your source code as well as copy the required files from the Simplicity Studio emdrv directory. See the example in Github here for the required files that I have copied into the project src directory.
The DMA peripheral can also be set up through use of the em_drv library. There are examples in Application Note AN0013 Direct Memory Access that configure things more manually. In short, the DMA peripheral looks for something called a descriptor table located in RAM that holds the configuration of the data transfer. This configures the DMA behavior, along with the configuration registers that reside inside the DMA peripheral. All of these things need to be configured, and then the DMA peripheral can do the transfer once its trigger event occurs. The descriptor table is used by the DMA peripheral at the trigger event to determine how to complete the transfer, including end addresses, address increment size, number of transfers, etc. When the transfer is complete, the DMA peripheral can be configured to issue an interrupt.
There are four modes of operation of the DMA peripheral. DMADRV only simplifies the use of the Basic DMA mode, but does not do too much to help with the other advanced modes. That is good for this example, since we only need Basic DMA mode for transferring data from our array in RAM to the USART peripheral.
To use the DMADRV, all we need to do is put a bit of code in the beginning of our main program to initialize the DMADRV and let it find an open DMA channel for us. This is great, because we don’t have to worry about using the same channel that SPIDRV or some other module is already using.
Now, we have a DMA channel that we can pass into the DMADRV functions. To make the transfer, we can replace the for loop that we used inside of the write_serial_stream function with the DMADRV_MemoryPeripheral function. First, we define the callback function that is called when the DMA transfer is complete:
Next, we have to change a few things inside of the write_serial_stream function to handle the DMA transfer. If you recall from the previous section, we incremented through the serial_stream array backwards in order to send the most significant byte first on the USART, as the TLC5940 expects to see the data.
But we can’t do that with DMA, as it can only iterate through memory in the forward direction. So we have to fill the serial_stream array backwards instead, and then the direction in which the DMA peripheral fetches the data from memory and pushes it to the USART will be in the correct order.
You can see that we replaced for loop that sent data to the USART within write_serial_stream with the DMADRV_MemoryPeripheral function, which means that the data is flowing from memory to a peripheral. There is a companion function that goes the other way.
Before the DMA_MemoryPeripheral function, we set a flag for dma_in_progress, and then waited for it to clear before moving on. The flag clear takes place inside the dma_transfer_complete callback function when the DMA transfer completes. However, instead of waiting in a while loop for the DMA to finish, we could have let the MCU go work on something else or entered into a sleep state, as long as we take care ahead of time to configure the DMA interrupt to wake the MCU from sleep.
You should notice that the serial_stream array was moved outside of the write_serial_stream function. It is super important that you make all variables that are referenced by DMA globally persistent. In our example, it makes no difference because we wait for the DMA transfer to finish, but as soon as we remove that blocking logic and allow the system to leave this function, the local variable serial_stream will be reclaimed by the system for other purposes and the data will become mangled. By making serial_stream global, it is preserved even after the write_serial_stream function exists, when the DMA transfer might still be in progress.
This should have been an easy introduction to DMA transfers. We will revisit DMA in future chapters to set up more complicated modes and really try to melt your brain.
This completes the chapter on interfacing with an external LED driver over a non-standard serial stream interface. By now, you should be well on your way to becoming a competent solderer and getting to know your way around the initialization of a few EFM32 peripherals. You should also feel like something of an expert in the types and applications of LEDs.
PREVIOUS | NEXT