MCU Coding
Published 23 February 2022
A collection of useful MCU programming techniques and sensor-related details that I have encountered in embedded projects.
This page was originally intended as a collection that could grow over time. The examples below cover integer and memory considerations, I²C EEPROM page writes, a bit-banged UART for debugging, and handling two's-complement sensor values.
Optimization and integer types
When programming a microcontroller, RAM and flash usage may be
important. Fixed-width integer types from <stdint.h>
are useful when the exact width of a value matters.
| Type | Typical size | Typical use | Minimum | Maximum |
|---|---|---|---|---|
uint8_t |
1 byte | Characters, small sensor values | 0 | 255 |
uint16_t |
2 bytes | ADC and sensor data | 0 | 65,535 |
uint32_t |
4 bytes | Bit fields, counters, flags | 0 | 4,294,967,295 |
int8_t |
1 byte | Signed sensor data | -128 | 127 |
int16_t |
2 bytes | Signed arithmetic | -32,768 | 32,767 |
int32_t |
4 bytes | Signed arithmetic | -2,147,483,648 | 2,147,483,647 |
float |
Usually 4 bytes | Floating-point arithmetic | Implementation dependent | |
double |
Usually 8 bytes* | Floating-point arithmetic | Implementation dependent | |
unsigned char |
1 byte | 0 | At least 255 | |
unsigned int |
Usually 2 or 4 bytes | Natural integer type for the CPU | 0 | Implementation dependent |
char |
1 byte | Signedness is implementation dependent | ||
int |
Usually 2 or 4 bytes | Natural signed integer type | Implementation dependent | |
long |
At least 4 bytes | Implementation dependent | ||
* The exact size of C types is compiler and architecture dependent.
On some embedded platforms, double has the same
representation as float.
On small controllers it can also be useful to avoid floating-point operations when they are not needed. On some architectures, especially those without hardware floating-point support, floating-point code can increase both execution time and code size.
Fixed-width types such as uint8_t and
uint16_t are particularly convenient for registers,
protocols and sensor data. However, they are not necessarily the
most efficient type for every function argument.
For example, compare:
uint8_t usart_send(uint8_t c)
{
...
}
with:
unsigned int usart_send(unsigned int c)
{
...
}
On some 32-bit microcontrollers the native unsigned int
version can generate equally compact or even smaller code, despite
using only the lowest eight bits of the argument. Narrow integer
arguments may require additional masking or extension instructions.
I²C EEPROM page writes
I²C EEPROMs can normally be accessed byte-by-byte or using page writes. After a write operation, the EEPROM needs time to program its memory cells. A typical write-cycle time is several milliseconds.
Writing one byte at a time therefore becomes unnecessarily slow. Page-write mode allows several bytes to be transferred to the EEPROM's internal page buffer before one programming cycle takes place.
The page size depends on the EEPROM and is commonly 64 or 128 bytes. An important detail is that a page write must not cross a page boundary. The software therefore needs to split a block into suitable pieces.
The example below was written for a 32-bit NXP microcontroller and a CAT24C512 EEPROM with a 128-byte page size.
#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; // 7 bits for page size 128
unsigned int pN = (addr + count) >> 7;
unsigned int p, n;
unsigned int nFirst, nLast;
nFirst = I2C_PAGESIZE - (addr % I2C_PAGESIZE);
if (count < nFirst)
nFirst = count;
nLast = (addr + count) % I2C_PAGESIZE;
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);
addr += n;
pData += n;
}
return r;
}
The function
i2ceeprom_writeBlockArray(array_ptr, bytecount)
sends the array over I²C using the EEPROM's device address.
The first two bytes contain the target memory address.
Bit-banged UART
Sometimes a microcontroller needs to be debugged when there is no display, unused hardware UART, or convenient debugger connection. Bit-banging a UART transmitter on an ordinary GPIO pin can be a very useful diagnostic tool.
Debug messages and hexadecimal values can then be sent to a computer through a USB-to-TTL serial cable. In this example I used 2400 baud, 8 data bits, no parity and one stop bit (8N1).
The GPIO was connected to an FTDI TTL serial cable such as the TTL-232R-3V3-WE.
// Hardware/MCU-specific definitions.
#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)
// 2400 baud: approximately 416.7 us per bit.
#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;
}
// Start bit
BB_OUT_WR_LOW();
WAIT_US(Tx_WAIT_BPS_START);
// Eight data bits, least-significant bit first
for (i = 8; i != 0; --i) {
if (data_byte & 1)
BB_OUT_WR_HIGH();
else
BB_OUT_WR_LOW();
data_byte >>= 1;
WAIT_US(Tx_WAIT_BPS_DATA);
}
// Stop bit
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));
}
The GCC diagnostic pragmas in the original code were used to suppress conversion warnings around operations where truncation was deliberate.
Bit-banged UART is particularly useful for diagnostics because the receiver requires no cooperation from the target. The target only needs one available output pin.
Two's complement
Sensors frequently return signed measurements using two's-complement representation. If the sensor value is not already stored in a correctly sized signed C type, the sign needs to be handled explicitly.
The following old VBScript example converts an unsigned 8-bit value:
function twoc8(x)
if x and 128 Then
twoc8 = -((x xor 255) + 1)
else
twoc8 = x
End if
end function
Here bit 7 is the sign bit. If it is set, the value represents a negative number.
Sign extension
If a signed value occupies fewer bits than the integer used to hold it, its sign bit needs to be extended before the value can be interpreted correctly.
| Original 8-bit value | 10000000 |
|---|---|
| Sign-extended to 16 bits | 1111111110000000 |
| Invert bits | 0000000001111111 |
| Add one | 0000000010000000 |
| Result | -128 |
The following MCU function was useful when different sensors returned signed values with different widths. One sensor used an 11-bit two's-complement representation and another used 8 bits.
// Convert a two's-complement sensor value 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))) {
// Sign extension.
d = d | ~((1 << (bits - 1)) - 1);
}
return (int16_t)d;
}
#pragma GCC diagnostic pop
For example, with bits = 8, bit 7 is treated as the
sign bit. With bits = 11, bit 10 becomes the sign bit.