In these projects, I implement common or interesting functions using
an STM32F439ZI MCU on a Nucleo-144 board, from the ground up. I do not
use any Hardware Abstraction Layer or pre-written drivers. I do use
header files containing register addresses and macros that execute
inline assembly that cannot be generated by GCC, such as for disabling
interrupts. And, depending on the project I use elements of the Newlib
standard library, for example for its string formatting features. I also
use an auto-generated linker script and startup assembly file which
zero-fills the bss
segment and which I modified to call my
initialization procedure.
My hand-rolled board initialization procedure can be found in /Src/init.c
Echo receives user input over the MCU's USART peripheral and echoes the input back over the same peripheral. Echo works one line at a time, using a stdio-esque input buffer to store input until a line is ready to be used. Rather than polling for characters to be ready to be read from the USART data register, echo uses interrupt requests and handlers to process input and determine when a line has been entered.
The USART3 interrupt fires when a character is ready to be read from the data register. It also writes the read character back to the terminal so the user can see their input as they type.
extern volatile uint16_t rxbuffer_pos;
extern volatile uint8_t rxbuffer[0x1000];
extern volatile uint8_t line_ready;
void USART3_IRQHandler(void)
{
uint8_t ch = USART3->DR & 0xFF;
rxbuffer[rxbuffer_pos] = ch;
++rxbuffer_pos;
uart3_writechar(ch);
if (ch == '\n')
line_ready = 1;
}
My implementation of readline
waits for interrupts, checking if a line
is ready to be read when it wakes. force_print
circumvents stdio to write
characters directly to the serial line.
char* readline(const char* prompt)
{
if (prompt)
force_print((char*)prompt, strlen(prompt), 0);
while (!line_ready)
__WFI();
__disable_irq();
char* line = (char*)malloc(rxbuffer_pos);
memcpy(line, (char*)rxbuffer, rxbuffer_pos);
line[rxbuffer_pos - 1] = '\0';
rxbuffer_pos = 0;
line_ready = 0;
__enable_irq();
return line;
}
Interrupts are disabled during copying of the line to allocated memory, to prevent additional characters from being written to the input buffer and the buffer length from changing.
Then main
simply executes readline
forever:
int main(void)
{
printf("welcome...\n");
char* msg;
while (1)
{
msg = readline(0);
printf("%s\n", msg);
free(msg);
}
}
In order to use printf
, which ultimately uses the write
syscall, I wrote a
simple "driver" for the MCU to print over the USART peripheral:
// io.c
void uart3_writechar(int c)
{
while ((USART3->SR & (1UL << 7)) == 0)
{
}
USART3->DR = (c & 0xFF);
}
int __io_putchar(int ch)
{
uart3_writechar(ch);
return 1;
}
// syscalls.c
int _write(int file, char *ptr, int len)
{
(void)file;
int DataIdx;
for (DataIdx = 0; DataIdx < len; DataIdx++)
{
__io_putchar(*ptr++);
}
return len;
}