Make a Broken Screen Effect with WCH Chitu RISC-V

Author: 三氯甲烷

This tutorial will use both the LCD and IMU modules on the Chitu development board to implement the reading and display of IMU data, as well as the simulation of the broken screen effect.

In brief, the display at rest looks like:

img

When shaken, it looks like:

img

When bumped, it looks like:

img

The development process will be explained in 3 parts as follows

  • The API used by the IMU module

  • Modifications to the API used by the LCD module (extending its functionality).

  • The overall workflow of the code and other miscellaneous items.

An introduction of IMU module and its driver API

On the Chitu development board there is an Inertia Measurement Unit (IMU), model MPU6050, a small black square located between the LCD display and the TF card, and is connected to the processor via the IIC bus.

WCH and MRS IDE provide the API of IIC bus, so it is feasible to directly use the interface provided in ch32v30x_i2c.h for development. For the sake of simplicity and convenience of development and application, Chitu also provides a packaged API for the MPU6050 sensor, and the relevant functions are stored in IIC.h, IIC.c, MPU6050.h and MPU6050.c. To use them, you only need to #include "MPU6050.h" and then make modifications according to your requirements.
The following is a brief introduction to the functions and usage of related functions.

u8 MPU_Init(void);

The first step is to initiate the IMU, in which the IIC bus is initialised and a series of initialisation instructions are sent to MPU6050, and the operating status is checked at the end.

u8 MPU_Write_Len(u8 addr,u8 reg,u8 len,u8 *buf); 
u8 MPU_Read_Len(u8 addr,u8 reg,u8 len,u8 *buf);
u8 MPU_Write_Byte(u8 reg,u8 data); 
u8 MPU_Read_Byte(u8 reg);

These four functions are for reading and writing to the MPU6050 bus and are all called by other functions in the API. They are not normally used by external code, but if you want to add more functionality, then these functions will come in handy.

u8 MPU_Set_Gyro_Fsr(u8 fsr);
u8 MPU_Set_Accel_Fsr(u8 fsr);

These two functions are used to set the maximum range (Full-Scale Range, FSR), with Gyroscope set to ±250°/s, ±500°/s, ±1000°/s or ±2000°/s for a Gyroscope and Accelerometer set to ±2g, ±4g, ±8g or ±16g.

u8 MPU_Set_Rate(u16 rate); 
u8 MPU_Set_LPF(u16 lpf);

These two functions are used to set the MPU6050's data sampling rate, as well as the Low-Pass Filter (LPF) parameters of the data. The sampling rate of the sensor supports 4Hz to 1kHz, which is normally set to 1kHz without considering power consumption, while the low-pass filter is used for filtering of the output data to reduce data noise to a certain extent, and is normally set to 1/2 of the sampling rate.

short MPU_Get_Temperature(void);
u8 MPU_Get_Gyroscope(u16 *gx,u16 *gy,u16 *gz);
u8 MPU_Get_Accelerometer(u16 *ax,u16 *ay,u16 *az);

These last three functions are used to obtain the sensor temperature, the tri-axial angular velocity, and the tri-axial acceleration. The output data is raw, unprocessed data in the range -32768 to +32767 and needs to be converted according to the previously set FSR. The conversion process can be carried out separately in external code or integrated into the API and modified as required. The default X-axis square is the right side of the board, the Y-axis positive direction is the upper side of the board and the Z-axis positive direction is the back side of the board, which is a left-handed system rather than the usual right-handed system.

The MPU6050 is a powerful six-axis inertial sensor and has many uses to be exploited, so if you are interested you can refer to the MPU6050 user manual to further expand the function of the API.

2 Functional extension of the LCD module driver API

The use of the LCD module has been briefly introduced in the previous article Running Snake on WCH Chitu RISC-V MCU, but the LCD driver API used in it is rather basic and can hardly meet our needs for screen shattering simulation, so this part of the tutorial will make some modifications on the basis of the original API to achieve a better display.

2.1 Transparent text box (without background)

The lcd_show_char function of the original API (and the lcd_show_string and lcd_show_num that calls to this function) will display a background colour in the range of the text refresh, completely overwriting the original content. When displaying a prompt message, such a text background can look very obtrusive. (The background range of the text is highlighted in magenta in the image below)

img
However we want the display to look like the image below, where the text appears above the rest of the content but does not overshadow the content below. (The layer relationships are emphasised by the green text in the image below, with the central green text covering the spider web cracks, but without the missing square area)

img

The original function uses the lcd_address_set function to specify the position of the character on the LCD, then takes the glyph defined in font.h and notates it as temp, and sends the colour of each pixel point to the LCD in turn, according to the glyph. As the program does not know what was originally displayed in that area, it can only fill the area outside the font with the background colour BACK_COLOR.

lcd_address_set(x, y, x + size / 2 - 1, y + size - 1); //(x,y,x+8-1,y+16-1)
/* fast show char */
for (pos = 0; pos < size * (size / 2) / 8; pos++)
{
    temp = asc2_1608[(u16)data * size * (size / 2) / 8 + pos];
    for (t = 0; t < 8; t++)
    {
        if (temp & 0x80)
            colortemp = FORE_COLOR;
        else
            colortemp = BACK_COLOR;
        lcd_write_half_word(colortemp);
        temp <<= 1;
    }
}

As can be seen from the lcd_address_set function, the function first specifies the display refresh range by means of the 0x2A and 0x2B instructions, and then starts the transfer of the display content by means of the 0x2C instruction, i.e. writing to the LCD's frame buffer.

void lcd_address_set(u16 x1, u16 y1, u16 x2, u16 y2)
{
    lcd_write_cmd(0x2a);
    lcd_write_data(x1 >> 8);
    lcd_write_data(x1);
    lcd_write_data(x2 >> 8);
    lcd_write_data(x2);

    lcd_write_cmd(0x2b);
    lcd_write_data(y1 >> 8);
    lcd_write_data(y1);
    lcd_write_data(y2 >> 8);
    lcd_write_data(y2);

    lcd_write_cmd(0x2C);
}

In addition to the write frame buffer instruction (0x2C), the LCD controller ST7789 also provides a read frame buffer instruction (0x2E). Accordingly, we can add a new function that sends a read instruction after a specified refresh range, and then reads the display content in that range into the cache according to the timing sequence (3 bytes per pixel for reading, RGB666 format, and a 16*8 glyph taking up 384 bytes).

void LCD_AddressSetRead(u16 x1, u16 y1, u16 x2, u16 y2)
{
    LCD_WriteCmd(0x2a);
    LCD_WriteData(x1 >> 8);
    LCD_WriteData(x1);
    LCD_WriteData(x2 >> 8);
    LCD_WriteData(x2);

    LCD_WriteCmd(0x2b);
    LCD_WriteData(y1 >> 8);
    LCD_WriteData(y1);
    LCD_WriteData(y2 >> 8);
    LCD_WriteData(y2);

    LCD_WriteCmd(0x2E);
}

Since the chip does not support malloc operations at this time, the required cache area is hard-coded into the program code and will be automatically released after the character display process is executed:

u8 font_buff[384] = {0};
if(!bkground)
{
    LCD_AddressSetRead(x, y, x + size / 2 - 1, y + size - 1);
    LCD_ReadData();
    for (pos = 0; pos < size * (size / 2) * 3; pos++)
    {
        font_buff[pos] = LCD_ReadData();
    }
}

Then we just need to choose to cover the original content with the background colour or to retain it with a transparent background:

LCD_AddressSetWrite(x, y, x + size / 2 - 1, y + size - 1);//(x,y,x+8-1,y+16-1)
/* fast show char */
for (pos = 0; pos < size * (size / 2) / 8; pos++)
{
    temp_fontData = asc2_1608[(u16)data * size * (size / 2) / 8 + pos];
    for (t = 0; t < 8; t++)
    {
        if (temp_fontData & 0x80) colortemp = FORE_COLOR;
        else
        {
            if(bkground) colortemp = BACK_COLOR;
            else colortemp = LCD_ConvertColor(font_buff[(pos*8+t)*3]<<16 | font_buff[(pos*8+t)*3+1]<<8 | font_buff[(pos*8+t)*3+2]);
        }
        LCD_WriteHalfWord(colortemp);
        temp_fontData <<= 1;
    }
}

2.2 Layer overlapping (not cover) when image is displayed

As with the display of text, when displaying a broken screen effect, we do not want the image to overwrite the original display, so similar to the idea of a transparent text box, we can achieve a layer overlay by reading the original display (a black background image is required for the overlay).

However unlike text, pictures have a much higher resolution than individual characters, and a full 240x240 bitmap would take up 172.8kB of RAM, while the CH32V307 chip only has a total of 64kB of SRAM space (although this can be expanded to 128kB by configuring the Flash memory as RAM), so reading the entire screen content and putting it into the cache at once is not possible.

The solution here is quite simple: since we can't process it all at once, we'll process it in batches, reading one line at a time and completing it in 240 passes, so that only 720 bytes of cache are needed.

u8 buff[720] = {0};
for ( row = 0; row < height; row++ )
{
    // read original area
    LCD_AddressSetRead(x, y+row, x+width-1, y+row);
    LCD_ReadData();
    for( col = 0; col < width * 3; col++ )
    {
        buff[col] = LCD_ReadData();
    }
    // calculate overlay
    // ……

After reading, the cache content buff and the image p to be displayed are superimposed pixel by pixel, taking care to convert the format between RGB565 (image) and RGB666 (cache) and to determine if there is any data overflow after the superimposition. Finally, write the processed line to the LCD frame buffer.

// calculate overlay
for( col = 0; col < width; col++ )
{
    u8 pixel_high, pixel_low = 0;
    if (!flip) {    // no flip
        switch(rotate)
        {
        case 0x00:  //   0deg
            pixel_high = p[2*(width*row+col)];
            pixel_low = p[2*(width*row+col)+1];
            break;
        case 0x01:  //  90deg
            pixel_high = p[2*(width*col+(239-row))];
            pixel_low = p[2*(width*col+(239-row))+1];
            break;
        case 0x02:  // 180deg
            pixel_high = p[2*(width*(239-row)+(239-col))];
            pixel_low = p[2*(width*(239-row)+(239-col))+1];
            break;
        case 0x03:  // 270deg
            pixel_high = p[2*(width*(239-col)+row)];
            pixel_low = p[2*(width*(239-col)+row)+1];
            break;
        default:
            pixel_high = 0;
            pixel_low = 0;
        }
    }
    else {          // vertical flip (source file)
        switch(rotate)
        {
        case 0x00:  //   0deg
            pixel_high = p[2*(width*(239-row)+col)];
            pixel_low = p[2*(width*(239-row)+col)+1];
            break;
        case 0x01:  //  90deg
            pixel_high = p[2*(width*(239-col)+(239-row))];
            pixel_low = p[2*(width*(239-col)+(239-row))+1];
            break;
        case 0x02:  // 180deg
            pixel_high = p[2*(width*row+(239-col))];
            pixel_low = p[2*(width*row+(239-col))+1];
            break;
        case 0x03:  // 270deg
            pixel_high = p[2*(width*col+row)];
            pixel_low = p[2*(width*col+row)+1];
            break;
        default:
            pixel_high = 0;
            pixel_low = 0;
        }
    }
    //R
    tempR = buff[3*col] + (pixel_high&0xF8);
    if ( tempR > 0xFF ) buff[3*col] = 0xFF;
    else buff[3*col] = tempR;
    //G
    tempG = buff[3*col+1] + (((pixel_high&0x07)<<5)|((pixel_low&0xE0)>>3));
    if ( tempG > 0xFF ) buff[3*col+1] = 0xFF;
    else buff[3*col+1] = tempG;
    //B
    tempB = buff[3*col+2] + ((pixel_low&0x1F)<<3);
    if ( tempB > 0xFF ) buff[3*col+2] = 0xFF;
    else buff[3*col+2] = tempB;
}
// write new area
LCD_AddressSetWrite( x, y+row, x+width-1, y+row );
for( col = 0; col < width; col++ )
{
    LCD_WriteHalfWord( LCD_ConvertColor( buff[3*col]<<16 | buff[3*col+1]<<8 | buff[3*col+2] ) );
}

The image used in this step can be imitated by the font.h file. After the image is converted into a c array by tools such as Image2Lcd, it is stored in the image.h file and called in the main file using #include "image.h".

2.3 LCD backlight brightness adjustment

Chitu uses the PB14 pin of the chip to drive the backlight of the LCD module. This pin can be connected to the channel 2 inverting output (CH2N) of TIM1 to obtain the PWM signal. A half-baked PWM initialisation is provided in the original API, but there are a number of errors and omissions, so we will have to wait for a subsequent update. Here we can fix the PWM function ourselves.

First find the following code in the lcd_fsmc_init function:

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOE,ENABLE);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_14|GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOD, &GPIO_InitStructure);

Delete the RCC_APB2Periph_GPIOG from RCC_APB2PeriphClockCmd, as there is no GPIOG on the chip; then delete the PB14 initialization code here, as the FSMC does not need to use this pin.
Then modify the TIM1_PWMOut_Init function as instructed in the CH32V307 Tutorial [Episode 3] [Clock].

Finally, replace the last GPIO_SetBits(GPIOB,GPIO_Pin_14) function in the lcd_init function with the TIM1_PWMOut_Init function.
After the initialisation, we can adjust the brightness of the LCD with a function as shown below (here the count value arr=100, if other count values are used, the maximum value of the brightness can be set according to the count value).

void LCD_SetBrightness(u8 brightness)
{
    if (brightness > 100) brightness = 100;
    TIM_SetCompare2( TIM1, brightness );
}

3 Overall process and other miscellaneous items

Firstly, the initialisation of the peripherals and devices is completed. Here, ±1000°/s and ±16g have been chosen as the range of the IMU.

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
Delay_Init();
USART_Printf_Init(115200);

// report clock rate
u32 SystemCoreClock_MHz = SystemCoreClock / 1000000;
u32 SystemCoreClock_kHz = (SystemCoreClock % 1000000) / 1000;
printf("System Clock Speed = %d.%03dMHz\r\n", SystemCoreClock_MHz,SystemCoreClock_kHz);

LCD_Init();
LCD_SetBrightness(75);
crack_repair_flag = 1;

IMU_Init();
IMU_SetGyroFsr(0x02);
IMU_SetAccelFsr(0x03);
IMU_SetRate(1000);
IMU_SetLPF(500);

TIM6 and TIM7 are used to set two timer interrupts, 1kHz and 30Hz respectively. 1kHz clock signal tick is used for sensor data acquisition and judgement, and is also responsible for timing (timing unit is ms), while 30Hz signal refresh_flag is used for LCD refresh. As the LCD refresh is relatively time consuming, if it is constantly refreshed it will crowd out the IMU sampling time, resulting in a large amount of missed data, so it is recommended to limit the LCD refresh rate or place the IMU sampling in the interrupt callback function. The timer can also be used as described in CH32V307 Tutorial [Episode 3] [Clock].

Above is the initialisation part in the program. The next part is the main loop.

After receiving the tick signal, the program needs to complete the acquisition of IMU data, the determination of warning messages, and the determination of the broken screen threshold.

First the six axes of IMU data are collected (the acquisition function here outputs the converted true value in float format) and the total acceleration and total angular velocity are calculated from the three axis components. As the processor does not support the sqrt function provided in math.h, the floating point number is directly converted to an integer by multiplying it by 100 and calculating the square root of the integer (calculating the square root of an integer is much faster than a floating point number).

tick = 1; time = 0; refresh_flag = 1;
Button_INT_Init();  // wakeup button
Tick_TIM_Init( 1000-1, SystemCoreClock_MHz-1 ); Tick_TIM_INT_Init();        // 1kHz tick & time
Refresh_TIM_Init( 33333-1, SystemCoreClock_MHz-1 ); Refresh_TIM_INT_Init(); // 30Hz refresh_flag
printf("Display: Timer & IMU data\r\n");
printf("\r\n");

The integer square root function used is as follows (Digit-by-digit algorithm)

//square root function for int32
uint32_t sqrt32(uint32_t n)
{
    uint32_t c = 0x8000;
    uint32_t g = 0x8000;
    for(;;)
    {
        if(g*g > n)
            g ^= c;
        c >>= 1;
        if(c == 0)
            return g;
        g |= c;
    }
    return 0;
}

The warning is then judged to be triggered or not based on the total acceleration. The warning_flag here is a flag bit to display the warning message when the display is refreshed, while the two timestamps time_stamp_trigger and time_stamp_record are to allow the warning data to be remained on screen for a period of time after the warning has been triggered so that it can be read.

// IMU warning
if (acc_total_100x > acc_threshold_warning_100x)
{
    time_stamp_trigger = time; warning_flag = 1;
    if ( (acc_total_100x > acc_record_100x) || (time - time_stamp_record > 1000) )
    {
        time_stamp_record = time;
        acc_record_100x = acc_total_100x;
    }
}
else if (time - time_stamp_trigger >= 1000)
{
    warning_flag = 0;
    acc_record_100x = 0;
}

Then whether to trigger the broken screen effect is determined according to the total acceleration. The code here adds a function to determine the direction of acceleration and uses the direction of acceleration as the crack_flag for the broken screen signal (although the rotation function for the broken screen effect is not implemented).

// IMU glass crack
if ( ( acc_total_100x > acc_threshold_crack_100x ) )
{
    // direction record (for crack rotation)
    if ( abs(acc[0]) > abs(acc[1]) )
    {
        if ( acc[0] > 0 ) {    // east
            crack_flag = 0x10;
            if ( acc[1] > 0 ) crack_flag |= 0x02;  // east-north
            else crack_flag |= 0x08;                // east-south
        }
        else {                  // west
            crack_flag = 0x40;
            if ( acc[1] > 0 ) crack_flag |= 0x02;  // west-north
            else crack_flag |= 0x08;                // west-south
        }
    }
    else
    {
        if ( acc[1] > 0 ) {    // north
            crack_flag = 0x20;
            if ( acc[0] > 0 ) crack_flag |= 0x01;  // north-east
            else crack_flag |= 0x04;                // north-west
        }
        else {                  // south
            crack_flag = 0x80;
            if ( acc[0] > 0 ) crack_flag |= 0x01;  // south-east
            else crack_flag |= 0x04;                // south-west
        }
    }
}

After data acquisition and the determination of warning and broken screen thresholds are completed, the tick signal is cleared for the next trigger.

After receiving the display refresh signal refresh_flag, the code can display the corresponding content according to the warning_flag, crack_flag and other flag bits, including the real-time component and total amount of acceleration and angular velocity, warning flags, timing information, etc. This part is considered a UI design and it is recommended that you are free to play with it as much as possible.

When displaying the data, it should be noted that the compiler does not support direct formatting of decimals (i.e. formatting parameters such as %+6.1f or %5e are not supported), so the decimals need to be converted to an integer part + two integers in the decimal part for display. For example:

int32_t acc10x = (int)(acc[i] * 10);
if (acc10x > 9999) acc10x = 9999;
if (acc10x < -9999) acc10x = -9999;
LCD_ShowString(16, 16*(i+1), 16, TRUE, "%c:%+4d.%01d", 'X'+i, acc10x/10, abs(acc10x%10));
LCD_ShowString(84, 16*(i+1), 16, TRUE, "m/s2");

At the end of the refresh process, if the flag bit crack_flag for the broken screen effect is received, then we can use the modified LCD driver API to display the broken screen effect and the prompt message, then set the flag bit crack_halt_flag for the pause display to 1 to pause the screen refresh before pressing the repair key to avoid the new data being brushed on top of the broken screen effect (the repair key is processed using interrupts, and each flag bit is restored in the interrupt callback function. Please refer to "CH32V307 Tutorial [Episode 2] [GPIO]" for the method).

// IMU glass cracking display
// display after warning message
if (crack_flag)
{
    switch(crack_flag)
    {
    case 0x12:  // east-north
        LCD_OverlayImage( 0, 0, LCD_W, LCD_H, 0, TRUE, img_crack );
        break;
    case 0x18:  // east-south
        LCD_OverlayImage( 0, 0, LCD_W, LCD_H, 0, FALSE, img_crack );
        break;
    case 0x42:  // west-north
        LCD_OverlayImage( 0, 0, LCD_W, LCD_H, 2, FALSE, img_crack );
        break;
    case 0x48:  // west-south
        LCD_OverlayImage( 0, 0, LCD_W, LCD_H, 2, TRUE, img_crack );
        break;
    case 0x21:  // north-east
        LCD_OverlayImage( 0, 0, LCD_W, LCD_H, 1, FALSE, img_crack );
        break;
    case 0x24:  // north-west
        LCD_OverlayImage( 0, 0, LCD_W, LCD_H, 1, TRUE, img_crack );
        break;
    case 0x81:  // south-east
        LCD_OverlayImage( 0, 0, LCD_W, LCD_H, 3, TRUE, img_crack );
        break;
    case 0x84:  // south-west
        LCD_OverlayImage( 0, 0, LCD_W, LCD_H, 3, FALSE, img_crack );
        break;
    default:
        LCD_OverlayImage( 0, 0, LCD_W, LCD_H, 0, FALSE, img_crack );
    }
    LCD_SetColor(BLACK, CYAN);
    LCD_ShowString(16, 156, 16, FALSE, "Press            to repair");
    LCD_ShowString(64, 152, 24, FALSE, "Wake_Up");
    crack_halt_flag = 1;
}
// clear flag
refresh_flag = 0;

Then a screen-breaking simulator that can be used to scare kids has done : )

Leave a Reply

Your email address will not be published. Required fields are marked *