Embedded Drivers & Real Time Operating Systems

This book covers material that will be utilized in CMPE 146 and CMPE 244

Getting Started with Preet's Classes

Advise from prior students:

  • I have 8 years experience in the industry and this class is very industry oriented. Pay attention to the code review comments. They're very helpful.
  • Definitely not a beginner's course, so in order to not be overwhelmed, make sure that you have an understanding of c/c++.
  • Try to understand the concepts and do not solve the assignment only for grades, but for knowledge.
  • Know your basics of C and electrical engineering
  • Don't miss class.
  • DO THE LABS. They provide so much essential knowledge and structured understanding of the RTOS concepts
  • Brush up on your c and datasheet reading knowledge
  • Don't procrastinate, This class can get you an interview!!!
  • Start labs early, get comfortable with Git
  • Definitely keep ahead of the homework and start early - the first 6-8 weeks are rough, but things start to connect and the productivity stays the same working through the project even with lesser homework pressure.

Useful Knowledge

Project: MP3 Player (CmpE146)

Project: MP3 Player (CmpE146)

MP3 Project

Project Summary

The goal of this project is to create a fully functional MP3 music player using SJOne/SJ2 Microcontroller board as the source for music and control. MP3 files will be present on an SD card. SJOne board reads these files and transfers data to a audio decoding peripheral for generating music. User would be able to use player controls (start/stop/pause) for playing MP3 songs and view the track information on a display.

Block Diagram



Split your project into manageable RTOS tasks. There should be dedicated tasks for:

  • Reading an MP3 file
  • Playing an MP3 file


Here is the psuedocode:

typedef char songname_t[16];

QueueHandle_t Q_songname;
QueueHandle_t Q_songdata;

void main(void) {
  Q_songname = xQueueCreate(1, sizeof(songname));
  Q_songdata = xQueueCreate(1, 512);

// Reader tasks receives song-name over Q_songname to start reading it
void mp3_reader_task(void *p) {
  songname name;
  char bytes_512[512];
  while(1) {
    xQueueReceive(Q_songname, &name[0], portMAX_DELAY);
    printf("Received song to play: %s\n", name);
    while (!file.end()) {
      xQueueSend(Q_songdata, &bytes_512[0], portMAX_DELAY);

// Player task receives song data over Q_songdata to send it to the MP3 decoder
void mp3_player_task(void *p) {
  char bytes_512[512];
  while (1) {
    xQueueReceive(Q_songdata,  &bytes_512[0], portMAX_DELAY);
    for (int i = 0; i < sizeof(bytes_512); i++) {
      while (!mp3_decoder_needs_data()) {
// CLI needs access to the QueueHandle_t where you can queue the song name
// One way to do this is to declare 'QueueHandle_t' in main() that is NOT static
// and then extern it here
extern QueueHandle_t Q_songname;

app_cli_status_e cli__mp3_play(app_cli__argument_t argument,
                               sl_string_t user_input_minus_command_name,
                               app_cli__print_string_function cli_output) {
  // user_input_minus_command_name is actually a 'char *' pointer type
  // We tell the Queue to copy 32 bytes of songname from this location
  xQueueSend(Q_songname, user_input_minus_command_name, portMAX_DELAY);
  printf("Sent %s over to the Q_songname\n", user_input_minus_command_name);

Project Requirements

Non-Functional Requirements:

  • Should be dynamic.
    • As in, you should be able to add more songs and play them
  • Should be accurate.
    • Audio should not sound distorted,
    • Audio should not sound slower or faster when running on your system.
  • Should be user friendly.
    • User should be able to figure out how to use the device without a user manual.
    • Product must be packaged in some enclosure. No wires can be vision for the project.

Functional Requirements

  1. System must use the SJOne/SJ2 on board SD card to read MP3 audio files.
  2. System must communicate to an external MP3 decoder
  3. System must allow users to control the MP3 player (You may use the onboard buttons for this)
    1. User must be able to play and pause of song
    2. user must be able to select a song
  4. System must use an external display to show a menu providing the following information:
    1. Current playing song
    2. Information about current playing song
    3. Menu showing how to select a different song
    4. (Not all of the above need to be shown on the same display)
  5. System software must separated into tasks. EX: Display task, MP3 Read Task, Controller Task etc...
  6. System software must be thread safe always.
  7. System software must use OS structures and primitives where applicable.
  8. System software may only utilize 50% or less CPU
    • You must have an LCD screen for "diagnostics" where you print the CPU utilization and update it at least every 1 second

Prohibited Actions:

  1. System MAY NOT use an external SD card reader embedded into MP3 device. YOU MAY use an external SD card reader if your SD card reader is broken
  2. You must interface to external LCD screen (not the on-board LCD screen)
    • On-board screen is too small
    • The goal is to interface to external components (practice with HW design)
  3. Use of any external libraries (specifically Arduino) that drive the hardware you intent to use. You must make the drivers from scratch for every part you make.

Permitted Action:

  1. You may use the internal buttons for controlling the MP3 player.
  2. You may use the 7-segment display and LEDs above buttons for debugging but not as the main menu.
Project: MP3 Player (CmpE146)

Song list code module

Collect MP3 song list from the SD card

Reference Articles

Get a list of MP3 files (naive way)

The objective of this code is to get a list of *.mp3 files at the root directory of your SD card.

#include "ff.h"

void print_list_of_mp3_songs(void) {
  FRESULT result;
  FILINFO file_info;
  const char *root_path = "/";

  DIR dir;
  result = f_opendir(&dir, root_path);

  if (result == FR_OK) {
    while (1) {
      result = f_readdir(&dir, &file_info);

      const bool item_name_is_empty = (file_info.fname[0] == 0);
      if ((result != FR_OK) || item_name_is_empty) {
        break; /* Break on error or end of dir */

      const bool is_directory = (file_info.fattrib & AM_DIR);
      if (is_directory) {
        /* Skip nested directories, only focus on MP3 songs at the root */
      } else { /* It is a file. */
        printf("Filename: %s\n", file_info.fname);
Get list of MP3 songs

Here is a better way to design code such that a dedicated code module will obtain song-list for you:

// @file: song_list.h

#pragma once

#include <stddef.h> // size_t

typedef char song_memory_t[128];

/* Do not declare variables in a header file */
#if 0
static song_memory_t list_of_songs[32];
static size_t number_of_songs;

void song_list__populate(void);
size_t song_list__get_item_count(void);
const char *song_list__get_name_for_item(size_t item_number);
#include <string.h>

#include "song_list.h"

#include "ff.h"

static song_memory_t list_of_songs[32];
static size_t number_of_songs;

static void song_list__handle_filename(const char *filename) {
  // This will not work for cases like "file.mp3.zip"
  if (NULL != strstr(filename, ".mp3")) {
    // printf("Filename: %s\n", filename);

    // Dangerous function: If filename is > 128 chars, then it will copy extra bytes leading to memory corruption
    // strcpy(list_of_songs[number_of_songs], filename);

    // Better: But strncpy() does not guarantee to copy null char if max length encountered
    // So we can manually subtract 1 to reserve as NULL char
    strncpy(list_of_songs[number_of_songs], filename, sizeof(song_memory_t) - 1);

    // Best: Compensates for the null, so if 128 char filename, then it copies 127 chars, AND the NULL char
    // snprintf(list_of_songs[number_of_songs], sizeof(song_memory_t), "%.149s", filename);

    // or
    // number_of_songs++;

void song_list__populate(void) {
  FRESULT res;
  static FILINFO file_info;
  const char *root_path = "/";

  DIR dir;
  res = f_opendir(&dir, root_path);

  if (res == FR_OK) {
    for (;;) {
      res = f_readdir(&dir, &file_info); /* Read a directory item */
      if (res != FR_OK || file_info.fname[0] == 0) {
        break; /* Break on error or end of dir */

      if (file_info.fattrib & AM_DIR) {
        /* Skip nested directories, only focus on MP3 songs at the root */
      } else { /* It is a file. */

size_t song_list__get_item_count(void) { return number_of_songs; }

const char *song_list__get_name_for_item(size_t item_number) {
  const char *return_pointer = "";

  if (item_number >= number_of_songs) {
    return_pointer = "";
  } else {
    return_pointer = list_of_songs[item_number];

  return return_pointer;
int main(void) {
  for (size_t song_number = 0; song_number < song_list__get_item_count(); song_number++) {
    printf("Song %2d: %s\n", (1 + song_number), song_list__get_name_for_item(song_number));

Project: Video Game (CmpE244)

Project: Video Game (CmpE244)

LED Matrix Driver



An LED matrix is different from most panel displays. The LEDs are standard tri-color RGB LEDs. Each color on each LED is driven by one bit of a 3-bit shift register. These shift registers are then connected to one another with one shift registers output to the next registers input in a process known as a Daisy Chaining.



Figure1. 4 x 4 LED Matrix Latching Data (Provided by Sparkfun)

In the above figure, an example 4 by 4 matrix is used to show the steps of how data is clocked into the board. 

  1. Starting from the first Row, select what color you want for each LED in the Row
  2. Now Enter the 3 bits of data that creates that color into DataIn and Clock it into the Shift Register. Repeat this process for every LED in the Row
  3. Pull both Output Enable And Latch High, which disables your display and moves the data within the shift registers to the output Latch respectively.
  4. Using Address Lines A and B, select the next row you want to alter
  5. Set the Latch and Output Enable Low, which enables your display and lets you write Data to the next Row.

With this method of driving your matrix keep in mind that:

  • If you want to alter one LED within a row, you need to enter the data for every LED before it in the row. If you want to clock the color data for say the 27th LED in a particular row you also need to include the data of the 26 other LEDs behind it as well. It is like climbing a ladder, you cannot just start climbing at the tenth rung, you have to climb the first nine first.
  • Due to the nature of the shift register their is no native PWM support although you can achieve more colors through Binary code Modulation and Image Planes


LED matrix Parameters & Pins

Most of you will end up buying from SparkFun and Adafruit and most likely pick matrices with the following dimensions with the following scan rates

  • 32 by 32 with a scan rate of 1:16
  • 32 by 64 with a scan rate of 1:16
  • 64 by 64 with a scan rate of 1:32

It's fairly obvious that the bigger the matrix the more resolution you have to work with for your game. However you will also need to potentially provide more current for your board. A 5V 4A power supply is usually preferred in the absolute worst case where all LEDs are lit up at once, but for many of you a 5V 3A power supply will be sufficient. The pins for the board will typically be as follows:


Figure 2. To the Left, a 64 by 64 Matrix Input Pin, To the Right a 32 by 32 or 32 by 64 version 

Matrix LED pins



High R data


High G data


High B data


Low R data


Low G data


Low B data


Row select A


Row select B


Row select C


Row select D

E (Potentially Another GND )

Row select E (Or GND)


Clock signal.


Active Low Output Enables LED panel Cascade


Latch denotes the end of data.




Taking a 64 by 64 matrix as an example you will note that there are only 5 address pins A,B,C,D, and E which would only let you access 25 = 32 rows so how would you write data to the other 32?

The scan rates listed above are technical specs for a Matrix board that simply describes how many LEDs are lit up on it at a time. For example a 64 by 64 matrix has 4096 LEDs total. With a scan rate of 1:32 that means that at any given time 4096/32 = 128 LEDs are lit up at a time. Notice that number is exactly two rows of your example 64 by 64 matrix. The matrix displays the data you supplied to the rows via a scanning method starting from the first row of the top half of the board and the first row of the second half of the board. This method divides the 64 by 64 matrix into two 32 row chunks that can be easily addressed with 5 address pins, but how do you decide WHICH of the chunks you talk to?

Notice that there are 6 data pins R1,G1,B1,R2,G2 and B2. As you expect, the RGB1 pins will provide the data to the row you are selecting in the top half of the board and the RGB2 pins will provide the data to the row you are selecting in the bottom half of the board. This scanning is visualized below. 


Figure 3. Scan Rate visualized (Provided by Sparkfun)

So if only two rows are being displayed at a time, how can it appear like all the LEDs are lit up? LED matrices use the same persistence of vision explored in your PWM lab to display full images by “scanning” your image at a high enough frequency that your eye can’t keep up with. Most of your matrices should have included a refresh frequency as part of the technical specs. This frequency is the target your Sjtwo board must provide to the LED board to display your image. For this 64 by 64 example, 400Hz is the refresh frequency so your board would need to provide a clock frequency at or above this number. If the frequency is low enough you will be able to see the pattern above. The clock speed you provide to your board will be the speed your game runs at. Most modern day games run at 30 frames per second (fps) or 60 fps which translated to frequency is 30 Hz and 60Hz respectively. Your matrices will most likely be operating at much higher hertz so take this into account when designing animation and game logic that relies on time


We can finally summarize how our LED driver must be written

  1. Starting from the first Row of you matrix, Select what color you want to clock in to the LED
  2. Determine if that LED is in the upper or lower portion of your LED and clock that data in using either the RGB1 pins or RGB2 pins
  3. Set the bits and Clock them in and repeat this process for all the LEDs in the row
  4. Once a full row has been entered, disable the Output and set Latch High
  5. Using A,B,C,D,E pins, specify the next row to write to 
  6. Set Latch back down to low and re-enable the Output
  7. Repeat this process until your entire Matrix has been written to


#pragma once
#include <stdio.h>
typedef enum {//Color combinations of a RGB LED, experiment with the values
} color_code;
//Should set up all your matrix Pins
void led_driver__matrixInit(void);
//Should set the color of an LED anywhere on board
void led_driver__setPixel(uint8_t row, uint8_t col, color_code color);
//Should draw out a row on your board
void led_driver__drawRow(uint8_t row);
//Should draw the whole LED matrix
void led_driver__drawBoard(void);

SJ2 Board and Software

SJ2 Board and Software

SJ2 Development Environment

There are two major components of the development environment:

  • Compile a program for the ARM processor (such as the SJ2 board)
  • Compile a program for your host machine (your laptop)

Get started with the development environment by either:

  1. Download and install Git and then clone the SJ2-C repository
  2. Go to the SJ2-C repository and download the zip file 


Compile the SJ2 project

Most of the documentation related to the ARM compiler is captured in a few README files that you can read here. We will not repeat the details here so please read the linked article. You can watch the following video to get started:



After setting up the SJ2-C development environment, try the following:

  • Compile the FreeRTOS sample project (lpc40xx_freertos)
  • Load it onto the processor
  • Modify the program (maybe printf statement), and load/run it again
  • Use a serial terminal program
  • Type "help" at the terminal window.  Also try "help <command name>"
  • Use all of the possible commands, and briefly skim through the code at handlers_general.c to get an idea of how the code works for each command.



 Computer cannot recognize the SJ2 development board. 

  • This error normally happens because of missing Silicon Lab driver. 
  • Solution:  check the install folder inside the development packet (...sjtwo-c-master\installs\drivers). Please Install the driver, then start the computer and try to connect the device again.

"No such file or directory " after running Scons command. 

  • Please check, if the directory to the development folder has a name, which contain white space. 
  • SolutionDon’t use directory with spaces.

Cannot recognize the command Scons. 

  • This error normally happens when Scons is not installed, there are corruption during the installation Scons packet. Sometimes, you need to upgrade the pip to latest version in order to install Scons
  • Solution: Please check the pip version and upgrade the pip to latest version, then reinstall the Scons if necessary. After installation,  restart the computer and try the Scons command again. 

"Sh 1 : python : not found" after running Scons command 

  • It might appear when you have a multiple python versions, or you already had a python with different management packet (For example,  python is installed in your machine through Anaconda, etc ). As the result, the python path might not setup correctly. 
  • Solution:  Please check out these two article for your best option:
  • To make it simple, You can also uninstall the python environment, and  download the latest python version here then reinstall it again ( check in Add Python x.y to PATH at the beginning of the installing option )

Python3 is present, and "python" is not available (such as new Mac OS)

Add the following lines to you ~/.zshrc

alias python=python3
alias pip=pip3
export PATH=$PATH:"$(python3 -m site --user-base)/bin"

VMs are not recommended

  • While it is possible to pass the serial (COM) port to the VM, it can be really tricky.
  • Unless you have prior experience with serial port passthrough, using VMs for this class is not recommended.
  • If you are on Windows and want to use Linux, use WSL1 instead of WSL2 or VMs


Compile x86 project

x86 stands for instruction set for your laptop, which means that the project can be compiled and run on your machine without having to compile, load, and run it on your hex project. Being able to compile a project for your x86 host machine also provides the platform for being able to run unit-tests.

Youtube: x86 FreeRTOS Simulator


SJ2 Board Startup
  • The real boot location is actually at entry_point.c
  • Initial values of RAM are copied from Flash memory's *data section
    • See startup__initialize_ram() at startup.c
  • ARM core's floating point unit, and interrupts are initialized
  • Clock and a timer API is initialized
  • Peripherals and sensors are initialized
  • Finally, call to main() is made

Unit-Test Framework



The development environment contains built-in code formatting tool. Each time you compile, it will first reformat the source code according to preset Google coding format.

SJ2 Board and Software

SJ2 Board

SJ2 board has lots of in-built sensors and a 128*64 OLED. It has 96kb of RAM and 120MHZ CPU.