Traffic light - Project

Course: IS1300 - Embedded Systems, link

The project was built with a Nucleo L476RG development board and a traffic light shield made by Matthias Becker, KTH. The program is implemented as different tasks by FreeRTOS.

Picture of the Nucleo development Board
Nucleo L476RG development board
Picture of the traffic light shield made by Matthias Becker, KTH
The traffic light shield made by Matthias Becker, KTH
The main functionality
The junction is able to detect cars through toggle switches and sense if a pedestrian want to cross by pressing a pedestrian button. Different software timers were used to keep track of delay times. The remaining times are displayed as bars on the OLED display.

All the LEDs were connected to three shift registers and the bits in the registers were updated with SPI. The brightness of the LEDs can be adjusted with PWM depending on the ADC (analog to digital converter) value from a potentiometer.
Picture of the layout of the junction
Layout of the junction
Tasks
Five tasks were used to handle the program logic, each with a priority depending on how time critical it was. MainLogicController had the highest priority since it was the core logic, while the potentiometer and OLED tasks had a lower priority and simply used vTaskDelay between updates.

To communicate between tasks, queues were mainly used. By sending items through queues different tasks could be synchronized together and minimizing the usage of global variables. In some scenarios it is unnecessary for tasks to be in the ready state if nothing new has happened. For example when no buttons have been pressed or no timer have been expired. Therefore they were mainly implemented to be in the blocked state until something has been received in the queue.

The pedestrian blink task worked a bit differently. Instead of blocking forever it waited on its own queue with a timeout. This way it woke up either when told to start or stop blinking a pedestrian light, or periodically to toggle an already active LED, without needing a separate polling loop.
Video of the junction switching direction.

Code example: event-driven state machine

All decision logic went through one single queue, MainQueueHandle. Both hardware interrupts (car sensors, pedestrian buttons) and expiring FreeRTOS timers sent a small struct to that queue. MainLogicController waited on xQueueReceive until something arrived, so the task spent almost all its time in the blocked state instead of polling:

void TimerCallback(TimerHandle_t xTimer) {
    QueueItemStruct queueItemToSend;
    queueItemToSend.typeID = 2;             // 2 == event came from a timer
    queueItemToSend.data = (uint32_t)xTimer;
    xQueueSend(MainQueueHandle, &queueItemToSend, 0);
}

void MainLogicController(void *argument) {
    QueueItemStruct queueItemRecieved;
    TrafficLightEvents currentEvents;
    Init_Junction(&currentEvents);

    TrafficLightState currentState = VERTICAL_GREEN_STATE;

    for (;;) {
        xQueueReceive(MainQueueHandle, &queueItemRecieved, portMAX_DELAY);

        if (queueItemRecieved.typeID == 1)      
          Interrupt_Event(&currentEvents, queueItemRecieved.data, queueItemRecieved.data2);
        else if (queueItemRecieved.typeID == 2) 
          Timer_Event(&currentEvents, (TimerHandle_t)queueItemRecieved.data);

        currentState = StateMachine(currentState, &currentEvents);
    }
}

StateMachine took the current state and the accumulated event flags, and decided the next state. It also triggered the side effects, like updating the LEDs and starting or stopping timers. One of the eight states looked like this:

case(VERTICAL_TO_RED_STATE): {
    // Pedestrians can still request a walk signal while the light is changing
    if (eventStruct->pedestrianPressed_AcrossH && !eventStruct->pedestrianDelayExpired_H && !xTimerIsTimerActive(PedestrianDelayTimerHandle_H)) {
        BlinkItemStruct BlinkItemToSend = { .pedId = 1, .cmd = 1 }; // Start blinking
        xQueueSend(BlinkQueueHandle, &BlinkItemToSend, 0);
        xTimerStart(PedestrianDelayTimerHandle_H, 0);
    }

    if (eventStruct->orangeDelayExpired) {
        eventStruct->orangeDelayExpired = false;
        Update_Registers_Safe(ALL_RED);
        xTimerStart(AllRedDelayTimerHandle, 0);
        nextState = ALL_RED_TO_HORIZONTAL_STATE;
    }
    break;
}

Thread-safe SPI updates

All 24 LED bits across the junction were stored in three shift registers, updated over SPI. Both MainLogicController and the pedestrian blink task could write to the registers, so a mutex was used to protect the read and write of the shift register value:

void Update_Registers_Safe(uint32_t bitsToSet) {
    if (xSemaphoreTake(xShiftRegisterMutex, portMAX_DELAY) == pdTRUE) {
        Update_Registers(bitsToSet);
        xSemaphoreGive(xShiftRegisterMutex);
    }
}

void Update_Registers(uint32_t bitsToSet) {
    uint8_t bytesToSend[3] = {
        bitsToSet & 0xFF,
        (bitsToSet >> 8) & 0xFF,
        (bitsToSet >> 16) & 0xFF
    };

    if (HAL_SPI_Transmit(&hspi3, bytesToSend, 3, 50) == HAL_OK) {
        HAL_GPIO_WritePin(_595_STCP_GPIO_Port, _595_STCP_Pin, GPIO_PIN_SET);
        HAL_GPIO_WritePin(_595_STCP_GPIO_Port, _595_STCP_Pin, GPIO_PIN_RESET);
        shiftRegisterValue = bitsToSet;
    }
}

Note: To comply with KTH's guidelines against plagiarism, only selected code excerpts are shown for this school project, not the full submitted solution.