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 done using low level registry accesses (this article)
- Project done using PDL library
- Project done using HAL library
- Project done using MBED
- Project 2 - Simple Cloud Application
- Project 3 - FRAM
- Project 4 - CapSense and Bluetooth
- Project 5 - Dual Core Application
- Project 6 - Profiler
- Summary and Score
Project 1 – Serial Interfaces
Project 1 is simple project in which I connect I2C temperature sensor HTU21DHTU21D, reads temperature using them and prints it over UART. All articles and variants of this project are available as tutorials. I have used this to make opinions about Cypress platform and I reused these skills later when writing chapters with “Review” in name. This chapter is designed as tutorial, not a review. I have implemented this project in 4 ways:
- In C and ModusToolbox IDE without using any library. Just accessing registers. I have done this to look how are some peripherals of MCU designed. If their design makes sense and If it is easy to use them. It shows how to setup clocks of the MCU, setup I2C, UART and Timer (TCPWM) and communicate over it. All just using raw registry access. For definition of registers, I am including PDL but otherwise solution shown in tutorial does not use any library.
- In C and ModusToolbox IDE using PDL library provided by Cypress. This example features my library from github to control sensor and is based on PDL library provided by Cypress. Otherwise, it does the same as previous two variants, but it is hidden in library calls. This mainly shows how easy is to use library provided by Cypress.
- In C and ModusToolbox IDE using HAL library provided by Cypress. This variant also does the same but uses HAL library provided by Cypress and shows how easy is to use this library.
- In C and MBED Studio. Solution is based on MBED OS. This variant shows portable solution using MBED environment.
This article is first variant of implementation. It implements example using direct registry access and is implemented in C. It is developed using ModusToolbox eclipse-based IDE. The article is designed as tutorial. My thoughts (review) about this project and related things are presented in chapters which name starts with word “Review”. Information in this article are quite useless in real use. Currently almost nobody develops firmware using registry access. I used it only to make opinion and take some thoughts about design of MCU and its main concepts for the review purposes. I recommend reading 2nd (PDL library) or 3rd (HAL library) variant of this article before because this it is very hard to understand how to write programs for PSoC 6 using this method. I ordered It is as first because it is lowest level (without any library efficiently used).
Create project in ModusToolbox IDE
At first you need to open ModusToolbox IDE and create new project. To do that select File > New > ModusToolbox Application. Do not choose new Project.
The dialog window will appear. At the first page of wizard choose the correct development board. If you remember, in the chapter “Review of development board” I mentioned that the most import name of board is “CY8CKIT-062S2-43012” and you see it again. The click Next.
At the second page select the type of Application. You can select same example app if you begin with the board but in this tutorial lets select Empty PSoC6 App. Do not forget to type the name of project next to his type. Then press create.
Then in Quick Panel will appear 4 additional buttons enabling you to run or debug app. Both in two variants for J-Link (you do not need this) and KitProg3 (you need this). KitProg3 is onboard programmer. J-Link you use if you debug external (even custom designed) board using external standalone programmer. In this example I will use only integrated KitProg3.
For making code completion working I recommend to building project. This cause eclipse to know details about building environment and learn what files can analyze for code completion.
Later we will need prints floats using printf function (and send it over UART). By default, standard functions do not support printing floats because float related code occupies a lot of flash and ram memory. Tiny hack how to make support for floats is include retarget-io library. This library is designed to retarget printf calls to UART. We will not do that in this example because this example controls all peripheral using registry access (not a printf) but including this library into project will bring us support for formatting float using snprintf function. To include library to project, select Library Manager in Quick Panel.
In Library Manager switch to Libraries tab and select retarget-io library. Then press Update button. In bottom text area you can see progress of cloning library from GitHub. After library will be downloaded and included into project Close the window.
Now we have prepared everything to write code.
Setup clock of MCU
The first what we must do is setup whole clock system of the MCU to get it running on expected speeds and setup clocks for other peripherals and secondary Cortex M0 core. I will configure clock using PLL which get 8MHz internal oscillator as input and will output clock at speed of 100MHz. Main Core of MCU can run at higher speed (up to 150 MHz) but for simplicity I will configure it to 100 Mhz. Note that this is also default configuration of lot a of projects created by ModusToolbox. If you are using PDL or HAL the first call in main usually is cybsp_init function which will do similar work what I do in this section. It does a much more, but I will not use that in this example.
You may remember the era of 8bit MCUs where you do not need to configure any clocks (with some exceptions like divider for SPI bus and so on). In nowadays MCUs it is much more complicated because they are expected to run much faster and their clock system provides lot of functionality that 8bits never offered. PSoC6 contains also tons of features in clock subsystem and it offers a lot of features in clock subsystem then many other ARM competitors. More details about clock subsystem you can find in chapter “Review of microcontroller”. In this part I will only briefly describe what it can do and how to configure it. Clock system for PSoC6 is in TRM described by this image.
I will use only part of it. I will use only IMO, PLL1 and Clk_HF[0]. Later I will also configure Peripheral Clock Dividers when configuring peripherals.
The initialization of clock system I will do in function SYSTEM_Init. So, create this function and call it from main.
void SYSTEM_Init() { }
int main(void) { SYSTEM_Init(); for (;;) { } }
Because we will increase the speed of core, we must handle little side effect which occur when running on high speeds. The Core executes instructions from some memory. It should be Flash, RAM or some external one. In my program it will be flash only, but RAM will be also accessed. The side effect is that while core can run at high speed the memory should not be able to feed core with instructions so fast. Because that we must add some latency to accessing memory. For SRAM it is problem only when running in ultra-low powered mode. For flash it is problem in both modes. In TRM you can find how much wait states you need to setup. Answer for 100 MHz is shown in following table.
So, I must configure number of wait states of flash controller to 3. At the lowest level it could be done using following command which directly writes the correct value to register. In TRM you can find details about this register.
FLASHC->FLASH_CTL = 0x00000003;
At this time, it is good to say what do the “FLASHC” and “FLASH_CTL” names come from and how does it work. FLASHC is defined in PDL library in file related to the MCU (not a MCU family or library itself. It is defined for every single part). It is pointer to structure. The pointer has specific value, and that value is base address of area in peripherals memory. In this memory there are mapped special function registers of peripherals. The pointed structure contains definition of all registers and reserved blocks within group of registers related to flash controller. FLASH_CTL is register of flash controller. PDL follows this pattern in all functions. There is every single peripheral defined.
As I mentioned before I will use internal 8MHz oscillator as the source for PLL so I must enable it first. This can be done using this command.
SRSS->CLK_IMO_CONFIG = 0x80000000;
Now the internal oscillator is running, and I will configure PLL itself. From the TRM you can decode following formula to calculate output frequency.
output freq = ((input / reference_divider) * feedback_divider) / output_divider
input is 8MHz and I need to configure reference_divider, feedback_divider and output_divider. There are some restrictions about that values and you can find them in TRM.
I selected following values:
- reference_divider as 2
- feedback_divider as 50
- output_divider as 2
This results into formula ((8M / 2) * 50) / 2 which is expected 100 MHz. The following code configure PLL with these values.
SRSS->CLK_PLL_CONFIG[0] = (2 << 16) | // output divider is 2 (2 << 8) | // reference divider is 2 (50 << 0); // feedback divider is 50
When the PLL is configured We can enable it. We cannot do it in the same write instruction as the configuration is done. We will enable it in next instruction. Add command that set flag in this register. This efficiently enables the PLL.
SRSS->CLK_PLL_CONFIG[0] |= 0x80000000;
The PLL does not starts immediately but it may take some time. Wait until it gets locked at the correct frequency. We will wait until it became locked with the following loop.
while ((SRSS->CLK_PLL_STATUS[0] & 0x00000001) == 0) {}
Now PLL works and at output is 100 MHz clock. As last we must configure that the clock to the core is clock from PLL because we can route different clocks to different parts of system. Following command configures PLL as the source for Cortex M4 core, peripherals and also for Cortex M0 core. Clocks for peripherals and M0 core can be also divided if required but I wont do that in this project.
SRSS->CLK_ROOT_SELECT[0] = 0x80000000 | // enable first root clock (clocks to ARM cores and peripherals) 0x00000001; // use PATH1 (PLL0) as source
Now the core subsystem is configured. You have seen that while we are use very complex MCU It is not a rocket science and you can setup complex clock system of quite complex MCU just using 5 registers in this minimalistic example.
Configuration of I2C
PSoC6 has no dedicated I2C controller. AS I mentioned in “Review of microcontroller” chapter, PSoC6 is set of generic modules with software defined interconnectivity. There are 13 generic Serial Communications Blocks (SCB). All of them supports I2C so you can use 13 I2Cs at the same time. I will use SCB2 because it has exposed SDA and SCL wire on the expansion header. The initialization of bus I will do in function named HTU21D_Init.
void HTU21D_Init() { }
The first thing to do is selected mode of SCB block. Except I2C it supports UART and SPI. I2C is default but in the same register we must selected that we are working with bytes (8bit) and not words (16bit).
SCB2->CTRL = 0x00000800;
Now we must do I2C specific configuration of the block. Following command will set I2C mode (master/slave), sets the flag that SCB will NACK the transaction in case the internal buffer became full and set oversampling to 10. Oversampling is used to choose how much clock cycles will be used for sampling/emitting the low and high part of bit transfer. In I2C there may be requirements to duty cycle different from 50% So you can select a ration using this setting. I am setting ration of 1:1 (50% duty cycle). One half of bit transfer will be 10 clock cycles (in register it is written with -1 offset, because value of 0 does not makes sense). Totally transmission of one bit takes 20 clock cycles of internal clock.
SCB2->I2C_CTRL = 0x80000000 | // master mode 0x00000200 | // nack if buffer full 0x00000090 | // low phase oversampling = 9 0x00000009; // high phase oversampling = 9
Now we will configure details about how data are passed from FIFO to bus. We will configure that we are emitting MSB first, it is 8 bits, and the output is open drain.
SCB2->TX_CTRL = 0x00010000 | // open drain output 0x00000100 | // MSB first 0x00000007; // 8bits
At this point the peripheral is configured. It offers much more functionality but for my simple testing program it is enough. The next thing before enabling the peripheral is configuration of GPIO ports. By default, the ports are generic ports and are not connected to any peripheral. Choice of peripheral is done using high speed input output matrix (HSIOM). I will configure pins 0 and 1 of port 9 to alternative function 7 (which is configured as 0x13 in register). You can find this in datasheet. Following picture is screenshot of alternate function mapping from datasheet with highlighted selected configuration.
Configuration of HSIOM is done using following command.
HSIOM_PRT9->PORT_SEL0 = 0x00000013 | // set alternative action of P9_0 to ACT_7 0x00001300; // set alternative action of P9_1 to ACT_7
Now We will configure GPIO ports to be driven as open drain with external pullup (pullup means that line is driven low by I2C node, otherwise it is driven high by pullup). Configuration is done using following command.
GPIO_PRT9->CFG = 0x00000004 | // set drive low mode on P9_0 0x00000008 | // enable input buffer on P9_0 0x00000040 | // set P9_1 to drive low mode 0x00000080; // enable input buffer on P9_1
The last thing is input clock. PSoC6 is designed to have not clocks connected to peripheral but they are connected over user configurable divider. There are 29 dividers of 4 types. You can configure them independently and connect freely any peripheral to any divider. You can connect multiple peripherals to one divider if you want clock peripheral by the same frequency. In this example I will use the simplest 8bit divider which divides input frequency (which is 100 MHz as configured before) by 8bit integer number. Later for UART I will use more advanced fractional divider.
The question is how to divide clock? At beginning I need to know what frequency I need to get on output of divider when I want to get 100kHz I2C bus. If you remember I set oversampling to 20 in total (for both low and high phase of clock signal when transmitting bit) so to reach 100kHz at output I need to have 100kHz * 20 = 2MHz input clock for SBC. This means that I need to configure divider for dividing 100 MHz to 2 MHz. The correct division rate is 50 and it could be simply done using integer divider because 100MHz / 50 is 2 MHz. Calculation did not result into any float number which is good for now.
The following command will select the clock divider (first 8bit divider) for SCB2 (clock number is 2 according to datasheet)
PERI->CLOCK_CTL[2] = 0x00000000;
The following command configure first 8bit divider to divide by 50. Value is with -1 offset because dividing by 0 does not make sense.
PERI->DIV_8_CTL[0] = 49 << 8;
And at last, we need to enable the configured divider.
PERI->DIV_CMD = 0x80000000 | // enable command 0x03FF0000; // do not use any other divider as phase alignment reference;
Now when we have clocked peripheral at correct frequency, we can enable it. This is done by adding enable bit to the register which we have seen at beginning.
SCB2->CTRL = 0x80000000 | // enable communication block now 0x00000800; // 8bit transfers
Now the SCB runs in I2C mode, samples the bus and can drive it. You have seen that It is not soo hard to configure peripheral manually without any library. Found required information is easy and straightforward with TRM in hands. Now we will use configured bus to access the sensor.
Reading temperature
Now let’s write function which will read temperature using sensor. The function will do following I2C operations:
- Start
- Address (0x40 << 1) + Write (0)
- Write command 0xE3
- Repeated start
- Address (0x40 << 1) + Read (1)
- Read 3 bytes (2 bytes are encoded temperature and third is CRC)
- Stop
I will name this function HTU21D_ReadTemperature, it will return 0 on success and 1 on error. Temperature will be passed to provided pointer to float.
int HTU21D_ReadTemperature(float* temperature) { }
Peripheral is backed by buffer. All transmitted data (including device address) are passed to transmit FIFO. Other parts of bus are implemented using commands. So, at beginning we need to start condition using command. This could be done using following command.
SCB2->I2C_M_CMD = 0x00000001;
The peripheral will do that at background, and we do not worry about it so much. Bus may be busy due to communication between other devices on the bus or it may be just held low for some reason. We do not worry about it. Peripheral will generate start condition as soon as possible. Immediately after executing START command we will add shifted I2C address and direction bit to the buffer. In case of HTU21DHTU21D it is ((0x40 << 1) | 0 for write command. The following command will do that.
SCB2->TX_FIFO_WR = 0x80;
After transmitting address and direction we need to wait until the transmission completes. Peripheral set one of the following flags.
- Data was ACKed by slave
- Data was NACked by slave
- Arbitration was lost when transferring
- Another error has occurred on bus.
I will create function HTU21D_WaitForTransmissionCompleteAndCheckStatus, which waits until the some of that flags occurs and then reports 0 in case of ACK, otherwise it will return 1. Function waits in while loop for any of these flags become non-zero and then clears the flag back and returns correct value.
int HTU21D_WaitForTransmissionCompleteAndCheckStatus() { uint32_t interruptFlags; while (((interruptFlags = SCB2->INTR_M) & 0x00000107) == 0) { } if (interruptFlags & 0x00000001) { // arbitration lost SCB2->INTR_M_SET = 0x00000001; // clear flag return 1; } else if (interruptFlags & 0x00000002) { // nack received SCB2->INTR_M_SET = 0x00000002; // clear flag return 1; } else if (interruptFlags & 0x00000004) { // ack received SCB2->INTR_M_SET = 0x00000004; // clear flag return 0; } else if (interruptFlags & 0x00000100) { // bus error SCB2->INTR_M_SET = 0x00000100; // clear flag return 1; } else { return 1; } }
Switch back to ReadTemperature function. There we will call function checking for status and aborting action in case of error.
if (HTU21D_WaitForTransmissionCompleteAndCheckStatus() != 0) { return 1; }
When the address and write is transmitted we can send command for sensor to execute measurement of temperature. It is command 0xE3 so we will send it and wait for result in the same way as we have done earlier.
SCB2->TX_FIFO_WR = 0xE3; // HTU21D command - read tempearture (blocking) if (HTU21D_WaitForTransmissionCompleteAndCheckStatus() != 0) { return 1; }
After that we will generate repeated start to switch the direction of transfer and send address and read flag.
SCB2->I2C_M_CMD = 0x00000001; // generate repeated start SCB2->TX_FIFO_WR = 0x81; // transmit address + read if (HTU21D_WaitForTransmissionCompleteAndCheckStatus() != 0) { return 1; }
The sensor will hold bus low until the measurement complete, so we need to add any delay before reading measured temperature. This will be managed by sensor itself by suing feature of I2C design.
Now we need to read byte from the reception buffer. We must wait until the byte become available there and then read it using register.
while (((interruptFlags = SCB2->RX_FIFO_STATUS) & 0x000000FF) == 0) { } uint8_t byte1 = (uint8_t)SCB2->RX_FIFO_RD;
If you remember we did not mention any time before to the peripheral if it should ACK or NACK reception of bytes. This pattern is common for other ARM Cortex-M MCUs like STM32 but In PSoC’s SCB it is implemented in the way that peripheral will not generate last clock of transmission until you execute ACK or NACK command in similar way how we generated START. This design allows you to determine the state ACK or do not ACK based on the received byte and you do not need to specify it before transmission. This means that now must generate ACK or NACK. Because we need read remaining two bytes, we will generate ACK. After generation of ACK we will wait until the command become executed. After the successful transmission of ACK, peripheral will clear flag that we have set.
SCB2->I2C_M_CMD = 0x00000004; // ACK the transmission while (SCB2->I2C_M_CMD & 0x00000004) {}
Now we will receive the next 2 bytes. In the last one we will generate NACK instead of ACK.
// reception of 2nd byte while (((interruptFlags = SCB2->RX_FIFO_STATUS) & 0x000000FF) == 0) { // wait while rx fifo is empty } uint8_t byte2 = (uint8_t)SCB2->RX_FIFO_RD; SCB2->I2C_M_CMD = 0x00000004; // ACK the transmission while (SCB2->I2C_M_CMD & 0x00000004) {} // reception of 3rd byte while (((interruptFlags = SCB2->RX_FIFO_STATUS) & 0x000000FF) == 0) { // wait while rx fifo is empty } uint8_t byte3 = (uint8_t)SCB2->RX_FIFO_RD; SCB2->I2C_M_CMD = 0x00000008; // NACK the transmission (last transmission) while (SCB2->I2C_M_CMD & 0x00000008) {} And at the end we will generate STOP on the bus by command. CB2->I2C_M_CMD = 0x00000010;
At this moment we have read temperature in RAW format. I will setup buffer with received bytes and pass it to functions checking CRC and function that will parse temperature from RAW data to float. You can find these functions as part of my library available at github. You can copy them from https://github.com/misaz/HTU21DLibrary/blob/master/src/HTU21D.c and remove underscore from start of their name. Functions uses constants (starting with HTU21D_E_ in name) that are defined in header file https://github.com/misaz/HTU21DLibrary/blob/master/src/HTU21D.h.
If all checking and parsing succeeded, I will return 0.
// complete buffer uint8_t receivedData[3]; receivedData[0] = byte1; receivedData[1] = byte2; receivedData[2] = byte3; if (HTU21D_VerifyChecksum(receivedData) != HTU21D_E_OK) { return 1; } HTU21D_ParseTemperature(temperature, receivedData); return 0;
Configuration of UART
Similarly, like I2C was configured I will configure another generic SCB to act as UART. I will write two low level functions for UART.
void UART_Init() { } void UART_PrintString(char* str) { }
At the beginning of init I will configure SCB5 (which can be accessed using expansion port and is connected to KitProg so you can use integrated USB-to-serial). I configure it to UART mode with 8bit data in FIFO buffer and oversampling of 10.
SCB5->CTRL = 0x02000000 | // UART mode 0x00000800 | // 8bit transfers 0x00000009; // oversampling is 10
UART mode of SCB supports multiple modes like standard, LIN or IrDA. I will use standard mode.
SCB5->UART_CTRL = 0x00000000;
I will configure FIFO to emits 8bit data and UART is LSB first.
SCB5->TX_CTRL = 0x0000007;
That is all what I need to configure the SCB to make UART working.
Now I must configure HSIOM to map UART to pin 1 of port 5.
HSIOM_PRT5->PORT_SEL0 = 0x00001200;
And configure it as strongly driven port with input buffer enabled.
GPIO_PRT5->CFG = 0x00000060 | // set strong mode on P5_1 0x00000080; // enable input buffer on P5_1
Last remaining thing to configure is input clock divider for block. I will use 16.5bit fractional divider which can divide input clock (100MHz) by A+(B/32) where A is 16bit integer and B is 5bit integer. I am oversampling bus 10 times so to reach baudrate of 115200 bauds I need to get frequency 1152000 Hz. I will calculate the ideal division rate of 100 MHz as following equation.
100 000 000 / 1152000 = 86,8055555556
From that number I can determine that A value is 86 and now I must gues ideal value for B value. Because there are only 32 possibilities, I can make simple excel table to calculate absolute error (difference from expected value) for each possible B value.
B | divider | real divider | real frequency (Hz) | Error |
0 | 86 + 0 / 32 | 86 | 1162790,698 | 10790,7 |
1 | 86 + 1 / 32 | 86,03125 | 1162368,325 | 10368,3 |
2 | 86 + 2 / 32 | 86,0625 | 1161946,26 | 9946,3 |
3 | 86 + 3 / 32 | 86,09375 | 1161524,501 | 9524,5 |
4 | 86 + 4 / 32 | 86,125 | 1161103,048 | 9103,0 |
5 | 86 + 5 / 32 | 86,15625 | 1160681,901 | 8681,9 |
6 | 86 + 6 / 32 | 86,1875 | 1160261,059 | 8261,1 |
7 | 86 + 7 / 32 | 86,21875 | 1159840,522 | 7840,5 |
8 | 86 + 8 / 32 | 86,25 | 1159420,29 | 7420,3 |
9 | 86 + 9 / 32 | 86,28125 | 1159000,362 | 7000,4 |
10 | 86 + 10 / 32 | 86,3125 | 1158580,739 | 6580,7 |
11 | 86 + 11 / 32 | 86,34375 | 1158161,419 | 6161,4 |
12 | 86 + 12 / 32 | 86,375 | 1157742,402 | 5742,4 |
13 | 86 + 13 / 32 | 86,40625 | 1157323,689 | 5323,7 |
14 | 86 + 14 / 32 | 86,4375 | 1156905,278 | 4905,3 |
15 | 86 + 15 / 32 | 86,46875 | 1156487,17 | 4487,2 |
16 | 86 + 16 / 32 | 86,5 | 1156069,364 | 4069,4 |
17 | 86 + 17 / 32 | 86,53125 | 1155651,86 | 3651,9 |
18 | 86 + 18 / 32 | 86,5625 | 1155234,657 | 3234,7 |
19 | 86 + 19 / 32 | 86,59375 | 1154817,755 | 2817,8 |
20 | 86 + 20 / 32 | 86,625 | 1154401,154 | 2401,2 |
21 | 86 + 21 / 32 | 86,65625 | 1153984,854 | 1984,9 |
22 | 86 + 22 / 32 | 86,6875 | 1153568,854 | 1568,9 |
23 | 86 + 23 / 32 | 86,71875 | 1153153,153 | 1153,2 |
24 | 86 + 24 / 32 | 86,75 | 1152737,752 | 737,8 |
25 | 86 + 25 / 32 | 86,78125 | 1152322,65 | 322,7 |
26 | 86 + 26 / 32 | 86,8125 | 1151907,847 | -92,2 |
27 | 86 + 27 / 32 | 86,84375 | 1151493,343 | -506,7 |
28 | 86 + 28 / 32 | 86,875 | 1151079,137 | -920,9 |
29 | 86 + 29 / 32 | 86,90625 | 1150665,228 | -1334,8 |
30 | 86 + 30 / 32 | 86,9375 | 1150251,618 | -1748,4 |
31 | 86 + 31 / 32 | 86,96875 | 1149838,304 | -2161,7 |
AS you can see the lowest error is -92,2 (in absolute value it is 92,2) for value 26 so
I will choose 26 as the B value of my divider. The selection of the divider, his configuration and his enablement are done using following three commands.
PERI->CLOCK_CTL[5] = 0x00000200; // use first fractional 16.5bit divider // set divider to to divide 86+26/32 = 86.8125 PERI->DIV_16_5_CTL[0] = (85 << 8) | // set divider integer part to 86 (26 << 3); // set divider fraction part to 26 PERI->DIV_CMD = 0x80000000 | // enable command 0x03FF0000 | // do not use any other divider as phase alignment reference; 0x00000200; // type of divider is 16.5
After that I will enable SCB.
SCB5->CTRL = 0x80000000 | // enable block 0x02000000 | // UART mode 0x00000800 | // 8bit transfers 0x00000009; // oversampling is 10
The print function is simple. It just writes entire string to TX FIFO. There could be a problem if the buffer (which is 128 bytes) became exhausted but in this example I will not write any long messages over uart and I also do some delay between transmitting messages.
void UART_PrintString(char* str) { while (*str) { SCB5->TX_FIFO_WR = *str++; } }
Configuration of Timer
The third part of this project is configuration of Timer. Timer will be used for delays. PSoC6 which is present on development board has 24 timers. They are also implemented as generic blocks. They are grouped into two peripherals (TCPWM0 and TCPWM1). TCPWM0 contains 8 timers and they are 32bit wide. TCPWM1 has remaining 24 timers and they are 16bit wide. All timers can be clocked by different divider. Dividers are the same dividers as we have seen when configuring I2C and UART. I will do not divide input clock (100Mhz) so I set 8bit divider to divide by 1. After configuring input clock divider by known way I will set counter value to 0, maximum value of counter I set to 100 milions and enables the timer. While timer is enabled it still does not count. To start counting Timer must be started. This should be done using trigger from some other peripheral or by command. In this project I will do that using command and I will start timer only in delay loop. After that I will stop timer again. So, the initialization does not start counting. Whole init function looks as follows.
void TIMER_Init() { PERI->CLOCK_CTL[15] = 0x00000001; // use second (1) 8bit divider for TCPWM_0_clock_0 (clock number 15) PERI->DIV_8_CTL[1] = 0; // set divider to 1 PERI->DIV_CMD = 0x80000000 | // enable command 0x03FF0000 | // do not use any other divider as phase alignment reference; 0x00000001; // it is second 8bit divider TCPWM0->CNT[0].COUNTER = 0; TCPWM0->CNT[0].PERIOD = 100000000; // peripheral clock is 100 MHz so this means that counter overflow every 1 second. TCPWM0->CTRL_SET = 0x01; // enable first timer }
Main
Now I can write final application in main. It will initialize clocks, I2C, UART and Timer and then reads temperature which it prints over UART. Delay is done using configured timer.
int main(void) { SYSTEM_Init(); TIMER_Init(); // wait until debugger is attached (prevents actions on I2C bus that may turn sensor into unresponsible state) TCPWM0->CNT[0].COUNTER = 0; // reset counter TCPWM0->CMD_START = 0x01; // start first timer while (TCPWM0->CNT[0].COUNTER < 25000000) {} // wait 250ms TCPWM0->CMD_STOP = 0x01; // stop first timer HTU21D_Init(); UART_Init(); UART_PrintString("Initialization completed.\r\n"); for (;;) { float temp; if (HTU21D_ReadTemperature(&temp) == 0) { char buffer[128]; snprintf(buffer, 128, "Temperature is %.2f°C\r\n", temp); UART_PrintString(buffer); } else { UART_PrintString("Error while reading temperature.\r\n"); } // some delay TCPWM0->CNT[0].COUNTER = 0; // reset counter TCPWM0->CMD_START = 0x01; // start first timer while (TCPWM0->CNT[0].COUNTER < 50000000) {} // wait 500ms TCPWM0->CMD_STOP = 0x01; // stop first timer } }
That is all. Now you can run (or debug) app using buttons in Quick panel window as highlighted on the following image (image comes from different project but the buttons are at the same place).
In serial monitor window you can see measured temperature.
That is all for this project. It is much harder to develop using low level access but while PSoC is very complex MCU it is still easier than I expected. For most users and real live usage, I recommend using PDL library, HAL library or MBED OS. In these cases, the project became implemented in tens of lines and you don’t need to worry about TRM. I used this project to review how is MCU designed. All my thoughts about board, MCU, development environment you can find in chapters which have “Review” in name.