No tracking, no bookmarks. How to assemble your own cellphone

What does your phone know about you? How secure is your information, and who can access it? Do you know that you can build and flash your own, 100% secure cellphone in just a few days? Today, I will explain how to do this.

Some time ago, I had assembled a handy based on a GSM communication module. The modern electronics were placed in a vintage case with an external receiver and rotary dialer. Alas, the scheme turned out to be poorly designed, and the phone was unhandy.

So, I decided to make another attempt based on a new concept. My goal was to create a compact device serving as a simple push-button phone. Ideally, its battery life should be at least one weak. No unwanted services, no suspicious apps, no annoying notifications – only calls, text messages, and a phone book.

According to the best world experts, the repairability of this device is 10 out of the 10

According to the best world experts, the repairability of this device is 10 out of the 10

The project was presented at Chaos Constructions 2019 and, to my (nice) surprise, attracted the interest of the audience. Many people were eager to learn about the internal structure of a mobile phone; therefore, today I will explain how to assemble such a gadget.

WARNING

Modern cellular systems enable carriers to track the customers in real time. Using the triangulation of several towers, it is possible to locate a person with an accuracy of some 20 m. Of course, there are some ways to negate the tracking, but such techniques go beyond the scope of this article.

The functional scheme and components

First, it is necessary to specify the technical requirements to the future device. I need to make and receive calls, read and write text messages (using various fonts, including the Cyrillic script), and manage contacts in the phone book. These are the very basic functions any cellphone must support. Of course, this list can be continued; for instance, I would like to have some built-in games (Snake, Tetris, etc.), but such functions can be easily added at the final stage.

The key element of my device is a SIM800C mobile communication module. It includes fully featured radio and audio units and supports the basic GSM functions. In other words, this is an almost ready-to-use GSM-UART bridge; all it needs is an external control terminal.

So, I need a screen, a keyboard, and some microcontroller to run the main program. I decided to use an ST7735 display module with the resolution of 128 x 160 pixels as a screen because for this one, I have a complete library to draw symbols and graphical primitives. In fact, the display selection is not critical for the project, and you can use any model with suitable dimensions.

A 16-button keypad is implemented via shift registers (two 8-bit 74HC165 microchips). The output of such registers is not a fully featured SPI bus because they don’t go into a high impedance state state even after turning them off. Therefore, the keypad cannot use the hardware SPI bus connected to the display. Instead, I used a software implementation.

The device will be controlled by an STM32 microcontroller. The high performance is not required in this case; so, low-cost models would fit as well. I chose an F103C8T6 MCU; its power is more than sufficient for my purposes. In addition, BluePill debug boards (an excellent alternative to Arduino by the way) are also based on this microcontroller; this enabled me to assemble a prototype and test the performance of its components from the very beginning.

INFO

Some F103C8T6 boards have 128 KB of memory instead of 64 KB. However, this is an undocumented feature, and I don’t recommend relying on an ‘extra’ memory bank while designing your project.

Later, I decided to add a bonus to the project – a W25Q32 external 32-Mbit serial flash memory chip. This allowed me not to overwrite the microcontroller’s flash memory and store all contacts separately. In addition, it became possible to upload pictures, symbols, and other raster graphic elements to the phone.

The mobile phone scheme is standard and does not require special comments. The SIM800C turns on when a logical high is applied to the REST pin (the Q1 transistor connected to the PA0 pin of the microcontroller is used). Light emitting diodes VD2 and VD3 indicate the state of the radio module. VD2 blinks during a successful connection, while VD3 is alight all the time when SIM800C is active.

Schematic diagram of the device

Schematic diagram of the device

The components are assembled on two one-sided printed boards, mostly through surface mounting. The first board accommodates the radio module, microcontroller, external memory microchip, and slots for the antenna and speaker. The entire second board is occupied by the keypad. The assembled device is put into a plexiglass case and fastened by M3 screws.

The phone is powered by a 1500 mAh LiPol battery. This is roughly half the capacity of modern flagship smartphones; however, it is sufficient to power the handmade phone for a week in the standby mode (power consumption around 6 mA) or for 24 hours in the active mode (power consumption around 40 mA).

INFO

The majority of the electronic components are available as ready-to-use evaluation boards or modules. Therefore, if you don’t want to spend time and effort on the pin layout and soldering, you may assemble the device on solderless breadboards.

Configuring UART

There are plenty of ways to program a microcontroller, including various languages (С/С++, Rust, etc.) and applied libraries abstracting the development from the hardware level (HAL by ST Microelectronics, Arduino Core, etc.). I used the canonical C programming language and libopencm3 open-source low-level hardware library.

WWW

The full set of source files for this project is available in the repository on GitHub.

First, it is necessary to initialize UART1 because it is responsible for the communication with the radio module. I used the standard parameters: 115200 baud and 8N1.

static void usart1_setup(void){
    /* Enable clocks for GPIO port A (for GPIO_USART1_TX) and USART1 */
    rcc_periph_clock_enable(RCC_GPIOA);
    rcc_periph_clock_enable(RCC_USART1);
    /* Enable the USART1 interrupt */
    nvic_enable_irq(NVIC_USART1_IRQ);
    /* PA9 TX,PA10 RX */
    gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO_USART1_TX);
    gpio_set_mode(GPIOA, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, GPIO_USART1_RX);
    /* Setup UART parameters */
    usart_set_baudrate(USART1, 115200);
    usart_set_databits(USART1, 8);
    usart_set_stopbits(USART1, USART_STOPBITS_1);
    usart_set_mode(USART1, USART_MODE_TX_RX);
    usart_set_parity(USART1, USART_PARITY_NONE);
    usart_set_flow_control(USART1, USART_FLOWCONTROL_NONE);
    usart_enable_rx_interrupt(USART1);
    usart_enable(USART1);
}

Then I have to set up the transmission of commands to the module. This can be done using, for instance, a third-party implementation: printf(). For that purpose, I will need the rprintf library. Its code is well-optimized and occupies just a few kilobytes in the memory. To make the library compatible with libopencm3, I have to make a few changes.

#38  #define UART USART1
...
#95  vfprintf_((&usart_send_blocking), format, arg);
...
#142 ch = usart_recv_blocking(UART);

Now I can send commands to the module as printf_("AT_command"), while the module’s response is received using interrupts and saved in the buffer. The buffer content is analyzed; if this is an expected response, then the handler function (the one used to display SMS and USSD messages) is called. It is also possible to display the message directly on the screen, which is very convenient for debugging purposes.

The screen

Similar to other peripherals, the display must be initialized prior to the usage. Of course, a suitable code can be found on the Internet, but I decided to write my own implementation. It does not take much time but gives a better understanding of the ST7735 capacity. In this project, I used the vendor’s documentation and premade pseudocode examples.

static void spi1_setup(void){
    /* Enable SPI1 Periph and gpio clocks */
    rcc_periph_clock_enable(RCC_SPI1);
    rcc_periph_clock_enable(RCC_GPIOA);
    /* Configure GPIOs:
      * SCK = PA5
      * DC = PA6
      * MOSI = PA7
      * CS = PA1
      * RST = PA4
      * LED = PB0
      */
    gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO5 | GPIO7);
    gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, GPIO4 | GPIO6 | GPIO1);
    /* Reset SPI, SPI_CR1 register cleared, SPI is disabled */
    spi_reset(SPI1);
    /* Set up SPI in Master mode with:
      * Clock baud rate: 1/64 of peripheral clock frequency
      * Clock polarity: Idle High
      * Clock phase: Data valid on 2nd clock pulse
      * Data frame format: 8-bit
      * Frame format: MSB First
      */
    spi_init_master(SPI1, SPI_CR1_BAUDRATE_FPCLK_DIV_2, SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE, SPI_CR1_CPHA_CLK_TRANSITION_1, SPI_CR1_DFF_8BIT, SPI_CR1_MSBFIRST);
    /* Set NSS management to software */
    spi_enable_software_slave_management(SPI1);
    spi_set_nss_high(SPI1);
    /* Enable SPI1 periph. */
    spi_enable(SPI1);
    gpio_set(GPIOA, GPIO1);
}

The library implements functions that draw dots, lines, and circles, print symbols and entire strings, and refresh the screen. It also supports Cyrillic symbols encoded in CP 866. The key component of the code is the call of the st7735_sendchar(char* c) function that allows to consecutively display strings on the screen, including escape sequences. At this point, the following symbols are supported: line feed (\n), carriage return (\r), screen clearing (\a), and backspace (\b).

void st7735_sendchar(char ch){
    uint16_t px, py;

    gpio_clear(STPORT, STCS);
    if (ch == '\r') {
        pos -= pos % 26;
        return;
    }
    if (ch == '\n') {
        pos += 26;
        pos -= pos % 26;
        return;
    }
    if (ch == '\a') {
        pos = 0;
        st7735_clear(bg_color);
        return;
    }
    if (ch == '\b') {
        pos--;
        px = (pos % 26) * 6;
        py = (pos / 26) * 8;
        st7735_drawchar(px, py, 0x20, txt_color, bg_color);
        return;
    }
    if(pos > 416) {
        pos=0;
        st7735_clear(bg_color);
    }
    px = (pos % 26) * 6;
    py = (pos /26) * 8;

    st7735_drawchar(px, py, ch, txt_color, bg_color);
    pos++;
    while (SPI_SR(SPI) & SPI_SR_BSY);
    gpio_set(STPORT,STCS);
}

By default, green text is displayed on black background. The colors can be set by calling the function st7735_set_printf_color(unit16_t text, uint16_t back). Also, I have to implement an additional function displaying the symbol currently entered on the keypad by the user.

void st7735_virt_sendchar(char ch){
    uint16_t px, py;
    gpio_clear(STPORT, STCS);
    px = (pos % 26) * 6;
    py = (pos / 26) * 8;
    if (ch > 0x20)  {
        st7735_drawchar(px, py, ch, RED, bg_color);
    }
    while (SPI_SR(SPI) & SPI_SR_BSY);
    gpio_set(STPORT, STCS);
}

This function is similar to st7735_sendchar(), but it neither processes escape sequences nor changes the current position of the symbol. Therefore, the call of st7735_sendchar() after st7735_virt_sendchar()redraws on the screen the symbol displayed by st7735_virt_sendchar().

Keypad

All buttons are connected to the software SPI via the shift registers. The 4х4key library processes the user input. The keypad has two layouts, English and Russian; in both layouts, four symbols are allocated to each button.


Unlike button phones of the 2000s, in my project, symbols are selected not by the number of clicks, but by the duration of pressing the button. This is because old mobile phones were equipped with membrane keypads, while tact buttons are tight, and it is inconvenient to switch through the letters by repeatedly pressing them.

Let’s examine the input processing in more detail. The get_key() function is responsible for the keyboard inquiry. The read_key() procedure is used: it reads the current state of the shift registers and returns two bytes of information from the buttons. No key combinations have been set yet, but they can be easily added if necessary.

The phone switches between layouts after receiving the code 0x0002; in all other cases, the respective character code is returned. Depending on the selected language, the value of the ch_map variable is incremented.

...
while (!key_code) {
    key_code = read_key();
}
do {
    if(key_code == 0x0002) {
        if (ch_map < 2) {
            ch_map++;
        else {
            ch_map = 0;
        }
        show_char_map(ch_map);
        while(key_code==2) key_code=read_key();
    }
    while(!key_code) {
        key_code = read_key();
    }
} while (key_code == 0x0002);
...

Then the key_map() function is called; it takes the button code and layout number as inputs. The function searches for the required character in the char_map data array and returns the result. The subsequent processing depends on the received character.

...
if (key == '\n') {
    delay(500);
} else if (key == '\b') {
    delay(500);
} else if(key == ' ') {
#ifdef ECHO
    st7735_virt_sendchar(key);
#endif
    delay(800);
    timer_start();
    old_keycode = key_code;
    do {
        key_code = read_key();
        if (key_code){
            if (key_code == old_keycode) {
                count++;
                if (count > 15) {
                    count = 0;
                }
                wait_key_counter = 0;
            } else {
                count = 0;
                break;
            }
            key = keymap2(count, 2);
#ifdef ECHO
            systick_interrupt_disable();
            st7735_virt_sendchar(key);
            systick_interrupt_enable();
#endif
            delay(900);
    }
} while (wait_key_counter < 1000);
timer_stop();
...

The final keypad processing code is more complicated than I wanted, but I got nearly all the required symbols on 16 buttons, which is pretty convenient. However, in some situations, I want the buttons to react instantly (e.g. when I select a menu point or take an incoming call). For that purpose, a separate function is implemented: fast_get_key(); it operates with a reduced array of characters.

char fast_get_key() {
    uint16_t key_code;

    char key;
    while (!key_code) {
        key_code=read_key();
    }
    key = keymap(key_code, 0);
    while (read_key()) {
         __asm__("nop");
    }
#ifdef ECHO
    echo(key);
#endif
    return key;
}

Now I have a display and a keypad; so, to assemble a terminal, I need to implement the functions stprintf() and kscanf(). For that purpose, I use the above-mentioned rprintf library again; although this time, the number of changes that have to be made in it is slightly larger.

int stprintf(const char \*format, …) {
    va_list arg;
    va_start(arg, format);
    stprintf_((&st7735_sendchar), format, arg);
    va_end(arg);
    return 0;
}

The situation with the kscanf() function is more complicated because as many as two functions receive characters from the keypad. So, I have no choice but to integrate them together by setting up an input switch between get_key() и fast_get_key(). Concurrently, I add the control character \b.

void set_scanf_mode(unsigned char mode) {
    fast_mode=mode;
}

int kscanf(const char* format, …) {
    va_list args;
    va_start( args, format );

    int count = 0;
    char ch = 0;
    char buffer[kscanf_buff_size];
    kscanf_buffer = buffer;

    while (count <= kscanf_buff_size ) {
        if(fast_mode) {
            ch = fast_get_key();
        } else {
            ch = get_key();
        }
        if (ch == '\b') {
            if (kscanf_buffer > buffer) {
                kscanf_buffer--;
            }
            continue;
        } else {
            count++;
        }
        if (ch != '\n' && ch != '\r') {
            *kscanf_buffer++ = ch;
        else {
            break;
        }
    }

    *kscanf_buffer = '\0';
    kscanf_buffer = buffer;
    count =  ksscanf(kscanf_buffer, format, args);
    va_end(args);
    return count;
}

The input/output system has been implemented, and a nearly fully featured terminal is ready to use. For instance, to clear the screen and display the traditional greeting, all I have to do is enter the following string:

stprintf("\aHello World!");

GSM module

I will use the function that sends SMS messages as an example demonstrating the interaction with the SIM800 module. The text mode is more illustrative; so, I am going to use it. To be able to send Cyrillic messages, it is necessary to set up the encoding:

void sim800_init_cmd() {
    printf_("AT+CMGF=1\r\n");
    for (uint32_t i = 0; i < 0xFFFFFF; i++) __asm__("nop");
    printf_("AT+CSCS=\"UCS2\"\r\n");
    for (uint32_t i = 0; i < 0xFFFFFF; i++) __asm__("nop");
    printf_("AT+CSMP=17,167,0,8\r\n");
    for (uint32_t i = 0; i < 0xFFFFFF; i++) __asm__("nop");
}

void fast_sms_send(char *text, char *tel) {
    char *p_tel;
    char u_tel[64]="+7";
    char temp[512];

    if (tel[0] == '8') {
        p_tel = tel + 1;
    } else if (tel[0] == '+') {
        p_tel = tel + 2;
    } else {
        p_tel = tel;
    }

    strcat(u_tel, p_tel);
    strcpy(temp, text);
    cp866_to_utc2(temp);
    cp866_to_utc2(u_tel);
    stprintf("\aSend sms\r\nAT+CMGS=\"%s\"\r\n%s\x1A", u_tel, temp);
    printf_("AT+CMGS=\"%s\"\r\n", u_tel);

    for (uint32_t i = 0; i < 0xFFFFFF; i++) __asm__("nop");
    printf_("%s\x1A", temp);
}

Now I can include something meaningful in the code, for instance:

fast_sms_send("Hello world!", "89162402484");

Let’s switch to the Cyrillic layout and try to send a one-line message:

void write_sms() {
    char text[256];
    char tel[13];
    uint8_t ret_code;

    stprintf("\aSMS writer v0.01\r\n"
                    "Enter the sms text\r\n"
                    ">");
    kscanf("%s", text);
    ret_code = telbook_get_number(tel);
    if(!ret_code) {
        return;
    }
    fast_sms_send(text, tel);
}

A record from the phone book is used here as the contact’s number. Now it is time to describe this component in more detail.

Phone book

As said above, the contacts are stored in the external memory chip. Each record is 32 bytes in size: 16 bytes are allocated for the phone number and 16 bytes, for the contact’s name. Currently, these data are recorded in plain text without encryption. But of course, it is preferable to use the AES or any other block cipher.

The phone book must support the following basic functions: contact selection (telbook_get_number()), adding a contact (telbook_rec_add()), and removing a contact (telbook_rec_del()). In addition, it should be possible to search the phone book for a specific name using the function telbook_find_name(). The 25q32 library is designed for low-level interactions with the memory chip; it performs all hardware implementation aspects.

Graphics

What other functions can be implemented with a color display and a few megabytes of free memory? Of course, pictures! The mobile phone can display 16-bit BMP files with the resolution of 128 x 160 pixels. The pictures are stored in the external flash chip and are displayed by the function img_from_flash() that receives the address of the pixel array. The format structure is simple; all important details are available here.


The image is displayed on the screen in parts using the stack-based buffer. During each pass, 4096 bytes are read from the memory to the buffer and then sent to the screen. Yes, the F103C8T6 board is equipped with a DMA controller designed specifically for such tasks. But it is impossible to statically allocate the entire frame buffer in the memory; so, the DMA effect would be minimal in this case.

void img_from_flash_v3(uint32_t addr) {
    uint8_t bufer[4096];
    gpio_clear(STPORT, STCS);
    st7735_sendcmd(ST7735_MADCTL);
    st7735_senddata(1 << 7);
    while (SPI_SR(SPI) & SPI_SR_BSY);
    gpio_set(GPIOA, STCS);

    for(uint8_t i = 0; i < 10; i++) {
        w25_read(addr + (i * 4096), bufer, 4096);
        st7735_drawimg(0,0+16*i,128,16, bufer);
    }

    gpio_clear(STPORT, STCS);
    st7735_sendcmd(ST7735_MADCTL);
    st7735_senddata(MV | MX);
    while (SPI_SR(SPI) & SPI_SR_BSY);
    gpio_set(STPORT, STCS);
}

Prior to viewing images stored in the memory, it is necessary to write them there using UART2 and xmodem file transfer protocol. At the input side, I process the data using the xmodem_to_flash() function: the address of the file beginning in the flash memory is transferred to that function.

void xmodem_to_flash(uint32_t addr) {
    unsigned char buf[132];
    uint32_t byte = 0;
    uint8_t lastlen, ch;

    usart2_init();
    usart_send_blocking(USARTX, NAK);
    while(1){
        ch = usart_recv_blocking(USARTX);
        if (ch == SOH){
            for (uint8_t i = 0; i < 131; i++) {
                ch = usart_recv_blocking(USARTX);
                buf[i]=ch;
            }
            lastlen=129;
            while(buf[lastlen--] == EOF);
            lastlen -= 1;
            w25_write(addr + byte, buf + 2, lastlen);
            byte += lastlen;
            usart_send_blocking(USARTX,ACK);
            continue;
        }
        if (ch == EOT){
            usart_send_blocking(USARTX, ACK);
            break;
        }
    }
    usart2_deinit();
}

In other words, to write a file from my computer, I have to start the transfer using a terminal program (e.g. Minicom) and then call the function xmodem_to_flash().

Energy efficiency

Short battery life is a weak point of many modern smartphones, including flagship models. In my project, I use several techniques allowing to reduce the power consumption.

First, I restrict the appetite of the radio module. The command AT+CSCLK=1 and logical high at the DTR pin switch the SIM800C module to the sleep mode (sim800_sleep()). It still can take incoming calls and text messages, but to transmit commands from the microcontroller, I have to apply a logical low to the DRT again and wait for some 50 ms (sim800_wake()). In this mode, the power consumption is just a few mA.

The display light also consumes plenty of power, so it is logical to disable it in standby (the functions st7735_sleep() and st7735_wake()). And most importantly, switching the microcontroller to the hibernation mode allows saving additional 30 mA.

void standby (void) {
    SCB_SCR |= SCB_SCR_SLEEPDEEP;
    PWR_CR |= PWR_CR_PDDS;
    PWR_CR |= PWR_CR_CWUF;

__asm__("WFI");
}

The last string (Wait For Interrupt) switches the F103C8T6 board to the standby mode, and an interrupt is required to exit it. In this particular case, this is the application of a logical low to the microcontroller’s REST pin.

Interface

The device has a simple text interface. When the menu is called, the screen is cleared and help (i.e. list of available functions and respective keys) is displayed. Then the device waits for user input, and the cycle is repeated. All menu functions are contained in a separate file: menu.c.

void main_help_menu(void) {
    stprintf("\aHELP\r\n"
                        "~ - ATA\r\n"
                        "! - ATH\r\n"
                        "1 - data menu\r\n"
                        "2 - call menu \r\n"
                        "3 - img menu\r\n"
                        "4 - power menu\r\n"
                        "5 - sim800 menu\r\n"
                        "6 - help\r\n"
                        "7 - sim800 PWR\r\n"
                        "8 - sleep\r\n"
                        "9 - sleep logo\r\n"
                        "* - tel book\r\n"
                        "0 - sms menu");
}

void get_keybord_cmd(void) {
    char bufer[64];
    uint32_t addr,  l, n = 4096;
    char key;
    key=fast_get_key();

    switch (key){
        case '1': data_menu(); break;
        case '2': telbook_menu_v2(); break;
        case '3': img_menu(); break;
        case '4': power_menu(); break;
        case '5': sim800_menu(); break;
        case '6': main_help_menu(); break;
        case '7': sim800_power(); break;
        case '8': st7735_sleep();
                        w25_powerdown();
                        standby();
                        break;
        case '9': w25_powerdown();
                        standby();
                        break;
        case '0': sms_menu(); break;
        case '*': telbook_menu(); break;
        case '~': sim800_take_call(); break;
        case '!': sim800_ath(); break;
    }
    return;
}

Below are brief descriptions of some menu functions. The ATA command makes it possible to take an incoming call; the ATH command declines or ends a call. The “data” menu simplifies the interaction with the external memory and allows to view the dump of any region in real time, either in ASCII or HEX. In addition, it can be used to rewrite bytes to arbitrary addresses, including manual control over the contact fields (although it is handier to use the respective menu section for that purpose).

The “call” menu is used for speed dial, while the “tel book” menu allows editing and adding contacts. The “power” menu allows changing the power saving settings, while the “sleep” и “sleep logo” commands switch the device to sleep mode with power consumption of 6 and 9 mA, respectively.

The device supports some other useful features. For instance, the “img” menu serves as a gallery and provides access to the saved pictures, while the sim800 board directly communicates with the radio module using the standard AT commands. This feature was of great assistance to me during the debugging.

Project ideas and perspectives

I had really enjoyed working on this project: implementing new functions, identifying errors, and resolving various issues. Needless to say, I am not going to rest on the laurels. Below are a few ideas to be implemented soon: built-in games, data encryption, MMS, a notebook, and additional fields in the contacts.

This list can be continued. Modern microcontrollers have plenty of useful interfaces and allow connecting various peripherals: external RAM chips, SD cards, high-res screens, and even digital cameras. With some effort, you can assemble a fully featured smartphone! Good luck!


Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>