Let’s create an OS for a calculator (Numworks) ! Part 1. Blinking LED

It’s been a while since my last post on this fabulous blog ! But I’m back with a new a project which is still work in progress as of right now. I hope you’ll like it. (^▽^)

The general idea

So last year my high school made me buy a Numworks calculator, which is by far more powerfull than my old TI Collège Plus. First thing I did was to lookup online if I could hack it, and surprise surprise yes you can 🎉. I then followed this well written guide that taught me how to unlock my calculator and install some CFW (Custom FirmWare) on it. Let’s understand how it works. The Numworks flash memory layout is split into 2 main categories :

  • The Internal memory which is small and just holds the bootloader in charge of choosing which OS to boot when the calculator is reset.
  • The External memory which is much larger and can hold 2 OS, one in Slot A and one in Slot B.

In my case I decided to create an OS so it can goes in one of the External memory slot. I made this choice because there are already some really good bootloader out there, personnally I use the one from Upsilon which is able to receive slot very easily.

What will I need

Now that I know what I want to do, let’s see what I need to achieve that. My target for my firmware is a STM32F730V8T6 which is ARMv7-M and Cortex-M7 based. So to compile it, I’ll use the gcc-arm-none-eabi toolchain and combine it with a Makefile for convenience. I will then flash my .bin file to the calc using the webdfu from TI Planet or by using the dfu-util command.

Let’s start coding !

Now the fun part begins 😀 The first thing to do is to create the linker file. If you are unfamiliar with baremetal developement you might not really know what is the purpose of a linker file, so let me explain. The linker file is a crucial part when you do some C/C++ programming because it handles how your code will be mapped in the tiny memory allocated to your firmware. Here is an example inspired from my linker file (simplified):

linker.ld
MEMORY
{
  FLASH ( rx )      : ORIGIN = 0x90400000, LENGTH = 4M /* This is for the B slot */
  RAM ( rxw )       : ORIGIN = 0x20000000, LENGTH = 256K
}
SECTIONS
{ 
  /* epsilon prefix */
  .signed_payload_prefix ORIGIN(FLASH) : {
    FILL(0xFF);
    BYTE(0xFF)
    . = ORIGIN(FLASH) + SIGNED_PAYLOAD_LENGTH;
  } >FLASH

  .kernel_header : {
    KEEP(*(.kernel_header))
  } >FLASH

  .slot_info : {
    *(.slot_info*)
  } >RAM

  /* The vector table (handle all the interrupts) located in vector_table.cpp */
  .isr_vector_table ORIGIN(RAM) + 512 : AT(ORIGIN(FLASH) + SIZEOF(.signed_payload_prefix) + SIZEOF(.kernel_header)) {
    _isr_vector_table_start_flash = LOADADDR(.isr_vector_table);
    _isr_vector_table_start_ram = .;
    KEEP(*(.isr_vector_table))
    _isr_vector_table_end_ram = .;
  } >RAM

  /* this is to prevent the bootloader from booting straight up in our os (we set all to 0) */
  .exam_mode_buffer ORIGIN(FLASH) + SIZEOF(.signed_payload_prefix) + SIZEOF(.kernel_header) + SIZEOF(.isr_vector_table) : {
    . = ALIGN(4K);
    _exam_mode_buffer_start = .;
    KEEP(*(.exam_mode_buffer))
    /* Note: We don't increment "." here, we set it. */
    . = . + FIRST_FLASH_SECTOR_SIZE;
    _exam_mode_buffer_end = .;
  } >FLASH
  
  [...] /* You can find my full code at https://github.com/ayabusa/Numworks-zeta-os */

The first part, the MEMORY block, is important because we specify there where our FLASH memory is located in our case for the B slot it’s 0x90400000 and it has 4M of total space. We do the same for the RAM.

Then there are all the sections that compose our binary file, for example the signed_payload_prefix, kernel_header, slot_info, and exam_mode_buffer are sections that are needed by the bootloader to consider the OS as “valid”. To understand all of that I looked at the linker from Upsilon and used the ImHEX program to verify if all was working as intended. Another interesting part is the isr_vector_table, this is a section that tells the microcontroller what to do when the program reset, in our case I tell it to launch our main function written in C. And that’s it our program is launched !

Blinking LED

When you are doing some electronics the equivalent of Hello World is a blinking led, so let’s try to do that ! The led we are going to turn on is the exam mode LED (I’m not sure it’s recommended to play with it but who cares ;D).

Let’s take a look at the code that lights our LED

main.c
#include "main.h"
#include "../device/stm32f730xx.h"
// our blue led is on B0
#define BLUE_LED_PIN  (0)
// store the current state of GIOB
uint8_t GPIOB_state = 0;

// this is our main function, I separated it from the c++ code because I don't like this language
void main_entry(){
    led_init();
    set_led_blue(true);
    while(1){
        /* infinite loop */
    }
}

void led_init(){
    // enable GPIOB
    RCC->AHB1ENR   |= RCC_AHB1ENR_GPIOBEN ;
    // setup blue led
    GPIOB->MODER  &= ~(0x3 << (BLUE_LED_PIN*2)); // we clear the 2 gpio_moder bits
    GPIOB->MODER  |=  (0x1 << (BLUE_LED_PIN*2)); // we set it to general out
    GPIOB->OTYPER &= ~(1 << BLUE_LED_PIN); // we set it to push-pull
}

void set_led_blue(bool state){
    if(state){
        GPIOB_state |= (1 << pin); // turn on
    }else{
        GPIOB_state &= ~(1 << pin); // turn off
    }  
    GPIOB->ODR = GPIOB_state; // apply our modifies state to the actual register
}

Wow we do a lot of things for just a LED so let’s break down the code 😊

  • The first thing we do is to import the stm32f730xx.h header, it’s not necessary but really convenient because it translates the name of register into their address.
  • Then in our main_entry, we start by telling the MCU what the led pin will do, in our case it’s an output pin in “push-pull mode”
  • And after that we can finally set our LED to HIGH by setting the right register !

And tadaaaa our led is now on 😮

But how did I figure it out ?

The first thing I want to point out is that I’m really new to bare-metal coding. So I followed this great tutorial by Vivonomicon, which introduced me to STM32 coding. I then adapted it and used the reference manual for my mcu from ST as my main source of documentation.

This was only the beginning!

Turning a led on is great but getting the keyboard working and LCD working is better. It’s why I’m now trying to develop a minimal and bare-metal HAL (Hardware Abstraction Layer) for my numworks. For now I’ve got the LED, the keyboard and the clock working and I’m now trying to make the LCD work! I’ll keep posting my updates here so if you want to get the latest update you can subscribe to my RSS feed and follow my work on github !

Final word & Special thanks

Big shoutout to the Omega/Upsilon team for all of their effort on this piece of hardware, and the community around it.
Thanks to Vivonomicon for his amazing tutorial.
And of course thanks to Numworks for making their product open source, even if it’s sadly not the case anymore :'(
And thank YOU for taking the time of reading my article ❤️
You can find all my code for this project on the Zeta github repo !

See ya 👋
~ Ayabusa

5 thoughts on “Let’s create an OS for a calculator (Numworks) ! Part 1. Blinking LED”

Leave a Comment

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