Skip to content

jonah-saltzman/bare-metal-echo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bare-metal STM32 Project 0: Echo

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

echo in action

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.

Reading characters and lines

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;
}

About

Bare-metal, interrupt-driven implementation of echo on a STM32F439ZI MCU

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published