MCU (Microcontroller) coding

This article is about some useful basics about MCU:s + sensors that you sometime stumble upon. I will add things later. Below is simply the most recent implementations.

 

Optimization

When programming a micro controller it's important to keep the flash and SRAM usage low. One way to do this is to use pre defined variable types, and keep track of how many bits they use:

  Bytes   Min Max
uint8_t 1 ASCII strings/characters, sensor/measurement data etc 0 255
uint16_t 2 ADC data, sensor data 0 65535
uint32_t 4 Bit fields/flags 0  4,294,967,295
int8_t 1 Signed sensor data  -128  127
int16_t 2 integer arithmetics  -32,768  32,767
int32_t 4 integer arithmetics  -2,147,483,648  2,147,483,647
float 4      
double 8      
unsigned char  1    0  255
unsigned int  2 or 4  Might be 4 bytes on 32-bit mcu  uint16_t or uint32_t uint16_t or uint32_t 
unsigned long  4    0  4,294,967,295
char  1    -128  127
int  2 or 4  Might be 4 bytes on 32-bit mcu int16_t or int32_t int16_t or int32_t 
long  4    -2,147,483,648  2,147,483,647

Mostly, you try to avoid linking the floating point library since floating point operations use some flash space. So we'll stick with integers.

When using shift operations, modulus (%) and integer math on sensor data you will probably stick with the named types like e.g uint8_t, uint16_t etc. And it's then tempting to use these types in function parameters as well.

But beware: on later 32-bit controllers, sending uint8_t as an argument will use more flash because there will be some mask operations added. On a 32-bit controller the declaration below:

uint8_t usart_send(uint8_t c) {}

will use more space than

unsigned int usart_send(unsigned int c) {}


even though unsigned int in this case is 32-bit and you will most certainly use the lowest 8 bits in "c" only.

 

 

i2c EEPROM

I2c EEPROMs can be written to and read from either by byte or page.

After each write the content needs to be written to the memory cells, and that usually takes about 5ms. To save time, the page write function can be used. To use this you'll need to know the page size of the EEPROM. This is a RAM buffer which you write to before its content, in the "write operation", is written to the memory cells. Page size is usually 64 or 128 bytes. You can fill a page partially, but at a page boundary it will be written (5ms delay).

There is a good explanation on this here.

So to write a block of data to an EEPROM might not be as straight forward as you think. One of my functions is as below. It is written for a 32-bit NXP micro controller, and the CAT24C512 EEPROM with a page size of 128 bytes.

#define I2C_PAGESIZE 128

I2C_STATUS_T i2ceeprom_writeBlock(unsigned int addr,uint8_t * pData, unsigned int count)
{
    uint8_t arr[I2C_PAGESIZE+2];
    I2C_STATUS_T r;
    unsigned int p0=addr>>7;                // NB: 7 bits for I2C_PAGESIZE=128
    unsigned int pN=(addr+count)>>7;        // Last page
    unsigned int p,n;
    unsigned int nFirst,nLast;


    nFirst=I2C_PAGESIZE-(addr % I2C_PAGESIZE);        // bytes left in first page
    if (count<nFirst) nFirst=count;                    // Only write this many

    nLast=(addr+count) % I2C_PAGESIZE;                // bytes to write in last page

    for (p=p0;p<=pN;p++) {
        if        (p==p0) n=nFirst;
        else if (p==pN) n=nLast;
        else             n=I2C_PAGESIZE;

        arr[0]=(uint8_t)(addr>>8);
        arr[1]=(uint8_t)(addr & 0xff);

        memcpy(&arr[2],pData,n);

        r= i2ceeprom_writeBlockArray(arr, n+2);
        if (r!=I2C_STATUS_DONE) break;

        Chip_Clock_System_BusyWait_ms(6);    // Write cycle time.

        addr+=n;
        pData+=n;
    }
    return r;
}

 The function i2ceeprom_writeBlockArray(array_ptr, bytecount) simply sends the data in the array to the i2c bus (using the EEPROM i2c address) with the target memory address as the first two bytes in the array.

 

Bit bang UART

Sometimes you want to debug a controller but there's no output/display or a way to run it from a debugger. Bit-banging UART protocol to an io pin might save the day. You can write text outputs at certain positions in the code, and send hex formatted integers. In this example I use 2400bps, 8N1 protocol. The io pin is connected to a USART-USB converter cable from FTDI (e.g TTL-232R-3V3-WE).

The bb_uart_send_byte() is the core function (with the timing), the others are for sending strings and hex representations of uint8,uint16 and uint32.

 

// Below defines are (of course) hardware/MCU specific. 
// BB_OUT_PIN is the io pin, the other macros are for setting it high or low.
#define BB_OUT_PIN 9
#define BB_OUT_WR_HIGH() Chip_GPIO_SetPinState(NSS_GPIO, 0, BB_OUT_PIN, true)
#define BB_OUT_WR_LOW()  Chip_GPIO_SetPinState(NSS_GPIO, 0, BB_OUT_PIN, false)
#define WAIT_US(x)       Chip_Clock_System_BusyWait_us(x)


// 2400bps
#define Tx_WAIT_BPS_START 417
#define Tx_WAIT_BPS_DATA  417


void bb_uart_send_byte (unsigned int data_byte)
{
  uint8_t i;
  static bool first=true;

  if (first) {
      Chip_GPIO_SetPinDIROutput(NSS_GPIO,0, BB_OUT_PIN);
      BB_OUT_WR_HIGH();
      WAIT_US(800);
      first=false;
  }

  BB_OUT_WR_LOW(); //Start bit
  WAIT_US(Tx_WAIT_BPS_START);
  for (i=8; i!=0; --i) {
      if (data_byte & 1) BB_OUT_WR_HIGH();
      else                 BB_OUT_WR_LOW();
      data_byte = (unsigned int) (data_byte >> 1); // rotate right to get next bit
      WAIT_US(Tx_WAIT_BPS_DATA);
  }
  BB_OUT_WR_HIGH();
  WAIT_US(Tx_WAIT_BPS_DATA);
}

void bb_uart_send_str (char * s)
{  while (*s) bb_uart_send_byte((unsigned int)*(s++)); }

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wconversion"

void bb_uart_send_hex4(unsigned int c)
{
    if (c>9) c+=('A'-10);
    else c+='0';
    bb_uart_send_byte(c);
}
#pragma GCC diagnostic pop

void bb_uart_send_hex8(unsigned int v)
{
    bb_uart_send_hex4 ((v>>4) & 0x0f);
    bb_uart_send_hex4 ((v) & 0x0f);
}

void bb_uart_send_hex16(unsigned int v)
{
    bb_uart_send_hex4 ((v>>12) & 0x0f);
    bb_uart_send_hex4 ((v>>8) & 0x0f);
    bb_uart_send_hex4 ((v>>4) & 0x0f);
    bb_uart_send_hex4 ((v) & 0x0f);
}
void bb_uart_send_hex32(unsigned int v)
{
    bb_uart_send_hex16 ((uint16_t)(v>>16));
    bb_uart_send_hex16 ((uint16_t)(v & 0xffff));
}
#endif

The #pragma statements are to avoid warnings from the compiler about loosing bits on shift operations.

 

 

Two's complement

Sensors sometime report back data in twos complement form. To convert the data to a signed integer, this can be done for 8 bits like in the vbs script function below. In a MCU this is done similarly.

function twoc8(x)
 if x and 128 Then
    twoc8=-((x xor 255) +1)
 else
    twoc8=x
 End if
end function

x is here entered as an unsigned byte, where bit 7 denotes a negative number if set.

NB: You might have to extend the sign (bit 7) before doing the bit inversion.

E.g if using 16 bits: 0000000010000000b -> 1111111110000000b.
Then invert like 1111111110000000b -> 0000000001111111b
Then  add +1 like 0000000001111111b -> 0000000010000000b
Add a minus sign and you're done:   -0000000010000000b = -128

 

In a MCU the function below might also work. It takes a low and high byte from a sensor and returns the signed 16-bit representative. Note that you also pass number of bits, e.g 8 for 8-bit twos complement. In this case I used it because one sensor reported 11 bits two complemet data and another 8 bits twos complement.

// Convert 16-bit 2-complement to int16_t
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wconversion"
static int16_t twoc2int(uint8_t vH, uint8_t vL,uint8_t bits)
{
    int d;

    d=vH<<8 | vL;

    if (d & (1<<(bits-1)))
        d = d | ~((1 << (bits-1)) - 1);    // sign extension, ...11110000000 for 8-bit negative

    return (int16_t)d;
}
#pragma GCC diagnostic pop

 

Add comment

Security code
Refresh