This post is part of my Roadtest Review of Cypress PSoC 62S2 Wi-Fi & BT5.0 Pioneer Dev Kit - Review. My review is splitted into multiple reviews and tutorials. Main page of review contains brief description of every chapter. Some projects are implemented in multiple variants. Main page of review contains brief description about every chapter, project and variants and contains some reccomendations for reading.
Table of Contents
- Intruction, Table of Contents, Overview of Table of Contets
- Review of Development Board
- Review of Microcontroller
- Review of Wi-FI and BT Module
- Review of Onboard Peripherals
- Review of Onboard Programmer and Debugger
- Review of Development Software
- Review of Documentation
- Project 1 - Serial Interfaces
- Project 2 - Simple Cloud Application
- Project 3 - FRAM
- Project 4 - CapSense and Bluetooth
- Project 5 - Dual Core Application (this article)
- Project 6 - Profiler
- Summary and Score
Project 5 – Dual Core Appplication
In this project I will create simple application that will use both cores of MCU. Application will use IPC peripheral which will be used to synchronize status of system initialization and then will be used for transferring parsed commands from Cortex-M0 core to Cortex-M4 core. Application will provide basic user interface for RGB LED over UART. Processing UART and parsing message will be offloaded to CM0 and controlling RGB LED to CM4.
Create new project based on Dual-CPU Empty PSoC6 App template. This template creates two eclipse projects for you. One for code for Cortex-M0 core and second for Cortex-M4 core. The dual core applications on PSoC6 works in the way that only Cortex-M0 runs at startup. Code running at CM0 can enable and start CM4. Address space and memory layout is shared for both MCU. You must take it in account. Default compilation toolset in ModusToolbox is configured in way that CM4 project depends on CM0 project. So, When you click build in CM0 project it will build only CM0 code and no CM4 code. In opposition when you click build in CM4 it will compile code for CM0 first (because CM4 project depends on it) and then it will build final image as combination of already generated CM0 image with building code for CM4. When building code for CM4 you will see two compilations output. As I mentioned before memory (FLASH and SRAM) is shared and linker script contains some definition of memory layout for each project. Default configuration is that 8 KiB (from 2 MiB) Flash and 8 KiB (From 1 MIB) RAM is dedicated for CM0 operation and other memory is available for CM4 and system libraries. Because I will create more complex code for CM0 I will need repartition layout of Flash to dedicate more Flash space to CM0 and less CM4.
Changing Flash memory layout
Flash layout is defined in linker script in folder mtb_shared. If you are using ModusToolbox IDE you see mtb_shared as one of the project in project explorer. Open file
mtb_shared\TARGET_CY8CKIT-062S2-43012\latest-v2.X\COMPONENT_CM0P\TOOLCHAIN_GCC_ARM\cy8c6xxa_cm0plus.ld
Navigate to MEMORY section to the flash region. Change LENGTH from 0x2000 to 0x20000 (add one zero). This change space from 8 KiB to 128 KiB. Now we must move start of space reserved for CM4. Open linker script for CM4:
mtb_shared\TARGET_CY8CKIT-062S2-43012\latest-v2.X\COMPONENT_CM4\TOOLCHAIN_GCC_ARM\cy8c6xxa_cm4_dual.ld
Search for FLASH_CM0P_SIZE constant and change it to 0x20000 (add one zero). This constant is used for calculations within that file later.
At last, we must modify constant CY_CORTEX_M4_APPL_ADDR which is used to start CM4 in CM0 code. We must set it to start of vector table of CM4 code. Open file
mtb_shared\TARGET_CY8CKIT-062S2-43012\latest-v2.X\system_psoc6.h
Find definition of CY_CORTEX_M4_APPL_ADDR constant and change value 0x2000 to 0x20000 (add one zero).
That is all for repartitioning. Now linker allows you to produce bigger CM0 code.
Initialization of cores and system
Default code for CM0 very early starts CM4 using Cy_SysEnableCM4 function call (note that this code use constant what we have modified in previous step). Initialization in default CM4 is very similar to all previous application which I have done. It inits BSP and the beginning. It is good option to initializate BSP in CM4 rather than CM0 because CM4 can run HAL (CM0 do not) and cybsp_init alocates hardware block for preventing conflicts. But in CM0 we will initialize UART and it is good idea to initialize UART after BSP (especially clocking system) became initialized. We will use semaphore synchronization mechanism for that. We initialize semaphore in CM0 before starting CM4 and wait until CM4 unlock them. In CM0 we will do following:
cy_rslt_t status; status = Cy_IPC_Sema_Set(16, false); assert(status == CY_IPC_SEMA_SUCCESS); Cy_SysEnableCM4(CY_CORTEX_M4_APPL_ADDR); while (Cy_IPC_Sema_Status(16) != CY_IPC_SEMA_STATUS_UNLOCKED) { }
I selected semaphore number 16. Some low numbers are reserved for system. In CM´4 I must use the same number. In CM4 we will do following
cy_rslt_t result; // init BSP result = cybsp_init(); CY_ASSERT(result == CY_RSLT_SUCCESS); // clear semaphore to notify CM0 that BSP is initialized Cy_IPC_Sema_Clear(16, false);
Now we have assured that CM0 will be blocked in while loop until CM4 completes all initialization work. After that we can initialize UART on CM0 side.
Cortex-M0 side
UART_Init();
This function initializes UART using PDL. Details about that are in project 1 – PDL variant.
void UART_Init() { cy_rslt_t status; const cy_stc_scb_uart_config_t uartConfig = { .uartMode = CY_SCB_UART_STANDARD, .enableMutliProcessorMode = false, .smartCardRetryOnNack = false, .irdaInvertRx = false, .irdaEnableLowPowerReceiver = false, .oversample = 12UL, .enableMsbFirst = false, .dataWidth = 8UL, .parity = CY_SCB_UART_PARITY_NONE, .stopBits = CY_SCB_UART_STOP_BITS_1, .enableInputFilter = false, .breakWidth = 11UL, .dropOnFrameError = false, .dropOnParityError = false, .receiverAddress = 0UL, .receiverAddressMask = 0UL, .acceptAddrInFifo = false, .enableCts = false, .ctsPolarity = CY_SCB_UART_ACTIVE_LOW, .rtsRxFifoLevel = 0UL, .rtsPolarity = CY_SCB_UART_ACTIVE_LOW, .rxFifoTriggerLevel = 0UL, .rxFifoIntEnableMask = 0UL, .txFifoTriggerLevel = 0UL, .txFifoIntEnableMask = 0UL, }; status = Cy_SCB_UART_Init(SCB5, &uartConfig, &uartContext); CY_ASSERT(status == CY_RSLT_SUCCESS); Cy_GPIO_SetHSIOM(UART_PORT, UART_RX_NUM, P5_0_SCB5_UART_RX); Cy_GPIO_SetHSIOM(UART_PORT, UART_TX_NUM, P5_1_SCB5_UART_TX); Cy_GPIO_SetDrivemode(UART_PORT, UART_RX_NUM, CY_GPIO_DM_HIGHZ); Cy_GPIO_SetDrivemode(UART_PORT, UART_TX_NUM, CY_GPIO_DM_STRONG_IN_OFF); status = Cy_SysClk_PeriphAssignDivider(PCLK_SCB5_CLOCK, UART_CLK_DIV_TYPE, UART_CLK_DIV_NUMBER); CY_ASSERT(status == CY_RSLT_SUCCESS); status = Cy_SysClk_PeriphSetDivider(UART_CLK_DIV_TYPE, UART_CLK_DIV_NUMBER, 71UL); CY_ASSERT(status == CY_RSLT_SUCCESS); status = Cy_SysClk_PeriphEnableDivider(UART_CLK_DIV_TYPE, UART_CLK_DIV_NUMBER); CY_ASSERT(status == CY_RSLT_SUCCESS); Cy_SCB_UART_Enable(SCB5); }
In the main of CM0 I will prompt user for commands. Logic I outsourced to prompt function for simplifier code structure.
while (1) {
prompt();
}
I created simple helper function that reads one line from UART to the buffer until new line character is received or buffer overflow should happen. Function supports strings up to 128 bytes in length and is very easy. It is just some basic algorithmic and pointer arithmetic stuff.
#define MAX_COMMAND_LENGTH 128 int readLine(char* buffer) { char receiveBuffer[MAX_COMMAND_LENGTH]; char* receiveBufferPtr = receiveBuffer; char* receiveBufferEnd = receiveBuffer + MAX_COMMAND_LENGTH; uint32_t receivedBytes; while (1) { while ((receivedBytes = Cy_SCB_UART_GetNumInRxFifo(SCB5)) == 0) {} // check overflow if (receiveBufferPtr == receiveBufferEnd) { Cy_SCB_UART_PutString(SCB5, "Message was too long. Try again.\r\n"); return 1; } // receive 1 byte *receiveBufferPtr = Cy_SCB_UART_Get(SCB5); // transmit until succeeds while (Cy_SCB_UART_Put(SCB5, *receiveBufferPtr) == 0) { } // check end of line to return data if (*receiveBufferPtr == '\r') { // transmit until succeeds while (Cy_SCB_UART_Put(SCB5, '\n') == 0) { } memcpy(buffer, receiveBuffer, MAX_COMMAND_LENGTH); buffer[receiveBufferPtr - receiveBuffer] = '\0'; return 0; } receiveBufferPtr++; } }
In the prompt function I will create buffers for user input, read string from user using readLine and build a command for CM4 core from received inputs. Command is encoded as
(color << 8) | value
Color is kind of RGB channel – 1=red, 2=green, 4=blue
Value is new value for RGB channel.
char rgbChannel[MAX_COMMAND_LENGTH]; char rgbValue[MAX_COMMAND_LENGTH]; uint32_t command = 0; Cy_SCB_UART_PutString(SCB5, "Select RGB channel (red|gree|blue): "); if (readLine(rgbChannel) != 0) { return -1; } if (strcmp(rgbChannel, "red") == 0) { command |= 0x100; } else if (strcmp(rgbChannel, "green") == 0) { command |= 0x200; } else if (strcmp(rgbChannel, "blue") == 0) { command |= 0x400; } else { Cy_SCB_UART_PutString(SCB5, "Unknown RGB channel. Allowed values are red, gree and blue.\r\n"); return -1; } Cy_SCB_UART_PutString(SCB5, "Type new value (0-255): "); if (readLine(rgbValue) != 0) { return -1; } char* end; long value = strtol(rgbValue, &end, 10); if (*end != '\0') { Cy_SCB_UART_PutString(SCB5, "Invalid value. Valid range is 0 - 255.\r\n"); return -1; } if (value < 0 || value > 255) { Cy_SCB_UART_PutString(SCB5, "Value out of range. Valid range is 0 - 255.\r\n"); return -1; } command |= value;
The last part of that function is to transfer command to CM4 core. In PDL this could be done using Cy_IPC_Drv_SendMsgWord function. You pass pointer to structure of IPC block which you want to use, mask for triggering interrupts in secondary core and value that you want to transfer. Low numbered IPC blocks are reserved for PDL and HAL use. I chose 10th IPC block and 10th IPC interrupt block. If you need transfer more than 8 bytes, you can transfer pointer. It will work because memory is shared, so pointers from one core are valid in second core. Cy_IPC_Drv_SendMsgWord fails if the IPC block is locked by someone else. We will call it in while loop and we will call it until it succeeds. After that we can complete function by returning non-error code.
while (Cy_IPC_Drv_SendMsgWord(IPC_STRUCT10, (1 << 10), command) != CY_RSLT_SUCCESS) { } return 0;
Cortex-M4 side
At the beginning of CM4 we will initialize RGB led.
result = cy_rgb_led_init(CYBSP_LED_RGB_RED, CYBSP_LED_RGB_GREEN, CYBSP_LED_RGB_BLUE, CY_RGB_LED_ACTIVE_LOW); CY_ASSERT(result == CY_RSLT_SUCCESS);
For receiving messages from CM0 we will setup IPC interrupt. We must also call Cy_IPC_Drv_SetInterruptMask to enable receiving interrupts on notify event from 10th IPC block (third parameter). We do not want to receive any release event because release event is called by us in CM4 code (second parameter).
cy_stc_sysint_t intcfg; intcfg.intrPriority = 7; intcfg.intrSrc = cpuss_interrupts_ipc_10_IRQn; Cy_IPC_Drv_SetInterruptMask(IPC_INTR_STRUCT10, 0, 1 << 10); Cy_SysInt_Init(&intcfg, messageReceived); NVIC_EnableIRQ(intcfg.intrSrc); __enable_irq();
Now we can end up in infinite loop. Usually there were some interesting code which will run at the same time as CM0 code but for now I will use empty while loop.
while (1) { }
Now we can write code for processing message from IPC (interrupt handler).
void messageReceived() { }
At first, we clear pending interrupt.
Cy_IPC_Drv_ClearInterrupt(IPC_INTR_STRUCT10, 0, 1 << 10);
Next, we read message from IPC block.
cy_rslt_t status; uint32_t command; status = Cy_IPC_Drv_ReadMsgWord(IPC_STRUCT10, &command); CY_ASSERT(status == CY_RSLT_SUCCESS);
And now because we do not need IPC block locked anymore (we safely copied received message to variable), we can release block. After this call CM0 will be able to send next message.
Cy_IPC_Drv_ReleaseNotify(IPC_STRUCT10, 0);
And now we can execute command.
if (command & 0x100) { r = command & 0xFF; } else if (command & 0x200) { g = command & 0xFF; } else if (command & 0x400) { b = command & 0xFF; } uint32_t color = cy_rgb_led_create_color(r, g, b); cy_rgb_led_on(color, CY_RGB_LED_MAX_BRIGHTNESS);
Variables r, g and b are declared globally to remain state of other non-changed channels.
int r, g, b;
That is all. You can run application. On the UART you can type red, green, or blue and set the new value for specified RGB channel. CM0 handles UART and sends parsed commands to CM4.
Top Comments