I have been programming STM32 microcontrollers lately and came across the u8g2 display library. It’s a great library and it is full of functionality – but it lacks a bit of documentation if all you want is use it in an embedded C environment. Read below how to use U8g2 with a HX1230 display and hardware SPI.
The u8g2 library documentation is focused on Arduino. Arduino is fun and it has a huge amount of libraries on its own, but I don’t need the Arduino overhead and I’m fine with libopencm3 for the tiny bit of hardware abstraction that I need. And usually I’m reading the microcontroller reference manual anyway so why bother with the extras?
Luckily, if you want to use u8g2 on a plain C platform, that is rather easy, too. U8g2 is really very versatile. It supports lots of display controllers and lots of hardware. To make sure that it Runs Anywhere™, u8g2 can go bit-banging a few I/O-ports in order to communicate through SPI or I²C. You only need to register a couple of functions and you have ported it to the platform of your choice. In fact, you don’t need to code anything, because the bit-banging procedures are already there.
However, STM32 has built-in hardware for SPI and I wanted to use that. This, obviously, meant that I had to write a few funtions to support the hardware side. So here is what I did.
The basis for a ported structure is the u8g2_Setup_DISP_NNxNN_Q function, telling u8g2 that you would like to use display DISP with a NNxNN resolution. The “Q” in this mockup is used to have different buffer sizes for your display. In our case, we are going to use an hx1230 SPI driven display, hence our setup function is
u8g2_Setup_hx1230_96x68_f(&u8g2, U8G2_R0, u8x8_byte_stm32f4_hw_spi, \
u8x8_gpio_and_delay_stm32f4); // init u8g2 structure
As you can see, two more functions are showing up: u8x8_byte_stm32f4_hw_spi and u8x8_gpio_and_delay_stm32f4. These are callbacks that we need to define. A template callback for the gpio_and_delay function can be found here. And as we are going to use real hardware SPI, we will only use a few of the msg possibilities. We will have to implement the _DELAY_ ones; also, the GPIO_RESET may come in handy. We won’t use any others.
So we need a few “sleep” instructions. Arduino has these built-in, but on libopencm3, you’re on your own. Luckily, STM32, being ARM, has the systick timer. Systick will count clock cycles and if you know your clock speed, you’ll be able to measure time.
If your don’t set any clocks at the beginning of your program, the main clock will probably run at 8MHz (stm32f0 and stm32f1) or 16MHz (stm32f4). If you do setup a clock, things may be different: maximum clock speeds can be anything from 48MHz (stm32f030) to 168MHz (stm32f407). In the examples below, I’m using the stm32f4 default of 16MHz.
void systick_delay_us(unsigned delay) { // delay based on running on 16MHz clock // 16 systick == 1us systick_clear(); systick_set_clocksource(STK_CSR_CLKSOURCE_AHB); systick_set_reload(delay<<4); systick_counter_enable(); while (!systick_get_countflag()); systick_counter_disable();systick_clear(); } void systick_delay_ms(unsigned delay) { // delay based on running on 16MHz clock / 8 // 2000 systick == 1ms systick_clear(); systick_set_clocksource(STK_CSR_CLKSOURCE_AHB_DIV8); systick_set_reload(delay*2000); systick_counter_enable(); while (!systick_get_countflag()); systick_counter_disable();systick_clear(); }
This makes the delay cases in our uint8_t u8x8_gpio_and_delay_stm32f4(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) quite simple:
uint8_t u8x8_gpio_and_delay_stm32f4(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) { switch(msg) { case U8X8_MSG_GPIO_AND_DELAY_INIT: // called once during init phase, break; // can be used to setup pins case U8X8_MSG_DELAY_NANO: // delay arg_int * 1 nano second asm("NOP"); break; case U8X8_MSG_DELAY_100NANO: // delay arg_int * 100 nano seconds asm("NOP"); break; case U8X8_MSG_DELAY_10MICRO: // delay arg_int * 10 micro seconds systick_delay_us(10*arg_int); break; case U8X8_MSG_DELAY_MILLI: // delay arg_int * 1 milli second systick_delay_ms(arg_int); break; case U8X8_MSG_GPIO_RESET: // Reset pin: Output level in arg_int if (arg_int) { gpio_set(GPIOB, GPIO0); // RESET } else { gpio_clear(GPIOB,GPIO0); }; break; default: u8x8_SetGPIOResult(u8x8, 1); // default return value break; } return 1; }
As you can see, we cheat on the _NANO functions, but the rest is straightforward.
Next thing to fix is the hardware enabled SPI callback function. I called it uint8_t u8x8_byte_stm32f4_hw_spi and its parameters are dictated by the u8g2 library: (u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr). One thing that is quite annoying though, is that the hx1230 is a rather odd display, in the sense that it communicates through 9 bit sequences. Yep, you read that right: a 1-bit read/write level, then 8 bits to send the message.
Our STM32 can do 8-bit or 16-bit transfers – but not 9. But it can keep sending bits after 8 – the 9th bit just being the first bit of the next byte, and continue doing that. Our sending routine simply needs to keep track of the number of bits that are left in its internal buffer and send these first:
case U8X8_MSG_BYTE_SEND: // Send one or more bytes, located at arg_ptr
//arg_int contains the number of bytes.
for (int i=0; i<arg_int; i++) {
uint8_t currentbyte,senddata; currentbyte=*(uint8_t *)(arg_ptr+i); senddata=(retaindata<<(8-bitsleft))|(datacommandbit << (7-bitsleft)); retaindata=currentbyte; if (bitsleft < 7) senddata|=(currentbyte>>(bitsleft+1)); spi_send(SPI2,senddata); bitsleft++; if (bitsleft==8) { spi_send(SPI2,retaindata); bitsleft=0; } } break;
A small reset function:
static void spi2_setup(void) { rcc_periph_clock_enable(RCC_SPI2); rcc_periph_clock_enable(RCC_GPIOB); gpio_mode_setup(GPIOB, GPIO_MODE_AF, \ GPIO_PUPD_NONE, GPIO13|GPIO15 ); // SCK, MOSI - leaving out NSS gpio_set_output_options(GPIOB, GPIO_OTYPE_PP,\ GPIO_OSPEED_25MHZ, GPIO13|GPIO15); gpio_set_af(GPIOB, GPIO_AF5, GPIO13|GPIO15); // software driven NSS on GPIO12:gpio_mode_setup(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO12 );
gpio_set_output_options(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_25MHZ, GPIO12);
gpio_set(GPIOB,GPIO12);
spi_reset(SPI2);
// make SPI not care for Slave Select output. We'll do this ourselves with GPIO12
spi_enable_software_slave_management(SPI2);
spi_set_nss_high(SPI2);
spi_disable_ss_output(SPI2);
spi_init_master(SPI2,SPI_CR1_BAUDRATE_FPCLK_DIV_8, \
SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE, SPI_CR1_CPHA_CLK_TRANSITION_1, \
SPI_CR1_DFF_8BIT, SPI_CR1_MSBFIRST);
spi_enable(SPI2);
}
And now for the whole uint8_t u8x8_byte_stm32f4_hw_spi:
uint8_t u8x8_byte_stm32f4_hw_spi(u8x8_t *u8x8, uint8_t msg,\ uint8_t arg_int, void *arg_ptr) { static unsigned bitsleft; static uint8_t retaindata,datacommandbit; switch(msg) { case U8X8_MSG_BYTE_INIT: // Send once during display init spi2_setup(); gpio_mode_setup(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO0); gpio_set_output_options(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_25MHZ, GPIO0); gpio_set(GPIOB,GPIO0); break; case U8X8_MSG_BYTE_SET_DC: // Set the level of the data/command pin. // arg_int contains the expected output level. datacommandbit=(!!arg_int); break; case U8X8_MSG_BYTE_START_TRANSFER: // Set the chip select line (NSS) if (u8x8->display_info->chip_enable_level) { gpio_set(GPIOB, GPIO12); // NSS: select SPI } else { gpio_clear(GPIOB,GPIO12); }; bitsleft=0; break; case U8X8_MSG_BYTE_SEND: // See above for (int i=0; i>(bitsleft+1)); spi_send(SPI2,senddata); bitsleft++; if (bitsleft==8) { spi_send(SPI2,retaindata); bitsleft=0; } } break; case U8X8_MSG_BYTE_END_TRANSFER: // Unselect the device. if (bitsleft) { spi_send(SPI2,retaindata<<(8-bitsleft)); bitsleft=0; } while ((SPI_SR(SPI2) & SPI_SR_BSY)); // wait till all data is sent if (u8x8->display_info->chip_disable_level) { gpio_set(GPIOB, GPIO12); // NSS: unselect SPI } else { gpio_clear(GPIOB,GPIO12); }; break; default: return 0; } return 1; }
That’s all for now. Happy hacking!