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