6
\$\begingroup\$

My current embedded project uses a 16x2 LCD controlled by the HD44780 standard controller. My PIC18 speaks to the LCD via the Adafruit LCD serial backpack (schematic link). I chose the SPI interface.

The HD44780 is controlled through various instruction writes to 8 data pins (DB0-7), a read/write pin (R/W), a register select pin (RS), and an enable pin (E). Link to the instruction set.

The instructions are composed of bits that indicate certain settings... configuration parameters for lack of a better term.

Everything is working as expected, but I am uncertain about my design. In order to best organize my code for readability and flexibility I have tried to follow the following approach:

  • Assign each HD44780 configuration parameter via #define directive in the header file
  • Build an array of char type for each instruction and load it with the appropriate configuration parameters
  • Execute instructions by passing the instruction array via pointer and using shift operations to construct the SPI output in the order expected by serial backpack

This is all well and good, however, I have then nagging feeling that storing the instructions as arrays is not the best approach. I would appreciate any advice on how you would approach this issue with a bent towards clarity and efficiency. My header and .c files are below, main.c is not included but calls LCD_send via the instruction aliases.


HEADER FILE

/* 
 * File:   LCD_SPI_16x2.h
 * Author: rbs
 * Comments:
 * Revision history: 
 */

/*Hardware configuration:
 *
 *  Adafruit LCD serial backpack bit configuration:
 *  [DB4 | DB5 | DB6 | DB7 | E | RS | RW | LITE]
 *
 *  HD44780 instruction set bit configuration (4-bit mode):
 *  [RS | RW | DB7 | DB6 | DB5 | DB4]
 * 
 *  HD44780 instruction set bit configuration (8-bit mode):
 *  [RS | RW | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0]
 * 
 *  PIC18 MSSP1 SPI peripheral:
 *  LSB first, clock = FOSC/4, SCK idle = low, TX on SCK low -> high, SDI not used
 */

// This is a guard condition so that contents of this file are not included
// more than once.  
#ifndef LCD_SPI_16X2_V2_H
#define LCD_SPI_16X2_V2_H

#include <xc.h> // include processor files - each processor file is guarded. 

//function prototypes
//void LCD_init(void);
void LCD_send(char *cmd);
void LCD_spi_out(char data);
//void LCD_send_string(const char * str);
//void LCD_set_cursr(int row, int col);

#define _E 0x08         //alias for HD44780 ENABLE bit

//HD44780 LCD parameters
#define _BL  1          //1-> backlight on       0-> backlight off

#define _ID  1          // 1-> increment         0-> decrement
#define _S   0          // 1-> display shift     0-> no shift
#define _D   0          // 1-> display on        0-> display off
#define _C   0          // 1-> cursor on         0-> cursor off
#define _B   0          // 1-> cursor blinks     0-> cursor does not blink
#define _SC  0          // 1-> shift display     0-> shift cursor
#define _RL  0          // 1-> shift to the left 0-> shift to the right
#define _N   1          // 1-> 2 lines           0-> 1 line
#define _F   0          // 1-> 5x10 dot font     0-> 5x8 dot font
#define _DL8 1          // 8-bit mode        
#define _DL4 0          // 4-bit mode

//HD44780 LCD instruction aliases
#define _Clear         LCD_send(Clear_display)
#define _Home          LCD_send(Home)
#define _Entry_mode    LCD_send(Entry_mode)
#define _Display_onOff LCD_send(Display_onOff))
#define _Cursor_shift  LCD_send(Cursor_shift)
#define _Function8     LCD_send(Function_set_8bit)
#define _Function4     LCD_send(Function_set_4bit)

//HD44780 LCD instruction set, see format note above
char Clear_display[10]      = {0,0,0,0,0,0,0,0,0,1};
char Home[10]               = {0,0,0,0,0,0,0,0,1,0};
char Entry_mode[10]         = {0,0,0,0,0,0,0,1,_ID,_S};
char Display_onOff[10]      = {0,0,0,0,0,0,1,_D,_C,_B};
char Cursor_shift[10]       = {0,0,0,0,0,1,_SC,_RL,0,0};
char Function_set_8bit[10]  = {0,0,0,0,1,_DL8,_N,_F,0,0};
char Function_set_4bit[10]  = {0,0,0,0,1,_DL4,_N,_F,0,0};
char Cursor_pos[10]         = {0,0,1,0,0,0,0,0,0,0};

#endif  /* LCD_SPI_16X2_V2_H */

.C FILE

/*
 * File:   LCD_SPI_16x2.c
 * Author: rbs
 *
 * Created on April 4, 2017, 10:00 PM
 */


#include <xc.h>
#include "system_initialize.h"
#include "LCD_SPI_16x2_v2.h"


void LCD_init(){
    //not completed yet
}

void LCD_send(char *cmd){
 /* Adafruit LCD serial backpack bit configuration:
  *  UPPER:
  *  [DB4 | DB5 | DB6 | DB7 | E | RS | RW | LITE]
  *  LOWER:
  *  [DB0 | DB1 | DB2 | DB3 | E | RS | RW | LITE]
  *  HD44780 incoming instruction configuration:
  *  [RS | RW | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0]
  */
    char Upper = 0x00;
    char Lower = 0x00;

    Upper |= (*(cmd))<<2;      //set RS bit
    Upper |= (*(cmd+1))<<1;    //set RW bit
    Upper |= (*(cmd+5))<<7;    //set data bits for upper 4bits of command 
    Upper |= (*(cmd+4))<<6;
    Upper |= (*(cmd+3))<<5;
    Upper |= (*(cmd+2))<<4;
    Upper |= _BL;              //set back light bit

    Lower |= (*(cmd))<<2;      //set RS bit
    Lower |= (*(cmd+1))<<1;    //set RW bit
    Lower |= (*(cmd+9))<<7;    //set data bits for lower 4bits of command
    Lower |= (*(cmd+8))<<6;
    Lower |= (*(cmd+7))<<5;
    Lower |= (*(cmd+6))<<4;
    Lower |= _BL;              //set back light bit

    LCD_spi_out(Upper);
    LCD_spi_out(Lower);
}

void LCD_spi_out(char data){
    _CS = 0;                   //chip select = 0
    SSP1BUF = (data|_E);       //send out data with E pin high
    __delay_ms(2);
    _CS = 1;                   //clock data out of LCD backpack shift register
    __delay_ms(2);             //delay for HD44780 to process

     _CS = 0;                  //chip select = 0
    SSP1BUF = data;            //send out data with E pin low to set HD44780
    __delay_ms(2);
    _CS = 1;                   //clock data out of LCD backpack shift register
    __delay_ms(2);
}
\$\endgroup\$
3
\$\begingroup\$

I see a number of things that may help you improve your code.

Don't put definitions in header files

There are declarations, such void LCD_spi_out(char data); and there are definitions which would be the actual body of the function. The header file should only have declarations. The code has proper separation for functions, but not for data. To fix that move lines like these

char Clear_display[10]      = {0,0,0,0,0,0,0,0,0,1};
char Home[10]               = {0,0,0,0,0,0,0,0,1,0};

into the .c file and then declare them like this in the .h file:

char Clear_display[10];
char Home[10];

Be careful with signed vs. unsigned

Your functions take char * and char as arguments, but it's actually implementation dependent whether a char is signed or unsigned. I'd recommend being specific and using uint8_t types (from <stdint.h>) instead.

Use more efficient storage

Using 10 bytes for the storage of 10 bits is not very efficient. Instead, I'd recommend storing things as bits. In particular, I'd suggest that each of the 8-bit commands should be stored as a uint8_t. For example:

uint8_t Home = 0x01u;

The RS and RW bits are intrinsic to the operation and don't need to be stored at all. That is, every command will have the RS bit equal to 0 (specifying "command" rather than "data") and RW will always be 0 (specifying "write" rather than "read").

Minimize work

Note that the low four bits of Upper and Lower are identical, so it would make sense to calculate them only once. Further, it would probably make sense to define them as bitmasks so they could easily be added in via bitwise-or operations.

Use const where practical

As @TobySpeight correctly pointed out in a comment, things that don't change should be declared const. In this code, that would be both the data arrays and the argument to the LCD_send function since that function doesn't and shouldn't alter the passed array.

Consider assembly language

In embedded systems, both memory and CPU cycles are often both in short supply, so it's much more common for embedded system programmers to use assembly language for some parts of the code. The drawback is that the code is inherently less portable and probably harder to maintain. Bit manipulation is one thing that is often done more directly in assembly languages than in C, but we can still make improvements just in C. One way to do it might look like this:

void LCD_send_cmd(uint8_t cmd) {
    uint8_t hi = _BL;
    uint8_t lo = _BL;
    lo |= (cmd & 0x01) ? 0x80 : 0;
    lo |= (cmd & 0x02) ? 0x40 : 0;
    lo |= (cmd & 0x04) ? 0x20 : 0;
    lo |= (cmd & 0x08) ? 0x10 : 0;
    hi |= (cmd & 0x10) ? 0x80 : 0;
    hi |= (cmd & 0x20) ? 0x40 : 0;
    hi |= (cmd & 0x40) ? 0x20 : 0;
    hi |= (cmd & 0x80) ? 0x10 : 0;
    LCD_spi_out(hi);
    LCD_spi_out(lo);
}

Alternatively, it might be worth factoring out the bit manipulations so that you can send both commands and data:

void LCD_send(uint8_t cmd, uint8_t flags) {
    uint8_t hi = flags;
    uint8_t lo = flags;
    lo |= (cmd & 0x01) ? 0x80 : 0;
    lo |= (cmd & 0x02) ? 0x40 : 0;
    lo |= (cmd & 0x04) ? 0x20 : 0;
    lo |= (cmd & 0x08) ? 0x10 : 0;
    hi |= (cmd & 0x10) ? 0x80 : 0;
    hi |= (cmd & 0x20) ? 0x40 : 0;
    hi |= (cmd & 0x40) ? 0x20 : 0;
    hi |= (cmd & 0x80) ? 0x10 : 0;
    LCD_spi_out(hi);
    LCD_spi_out(lo);
}

void LCD_send_cmd(uint8_t cmd) {
    LCD_send(cmd, _BL);
}

void LCD_send_data(uint8_t cmd) {
    LCD_send(cmd, RS | _BL);
}
\$\endgroup\$
  • \$\begingroup\$ Actually, your edit didn't add extern where it's needed: extern char const Clear_display[10]; and extern char const Home[10]; \$\endgroup\$ – Toby Speight Apr 11 '17 at 14:38
  • \$\begingroup\$ Yes, but that's because I wouldn't do it that way. Instead, I'd make them static within the .c file and create functions for those actions. That way, the external interface could stay the same even if some other display were used. \$\endgroup\$ – Edward Apr 11 '17 at 15:07
  • \$\begingroup\$ In that case, they wouldn't need to be mentioned in the header file, then? \$\endgroup\$ – Toby Speight Apr 11 '17 at 15:29
  • \$\begingroup\$ The functions would need to be declared in the header, but the data would not. \$\endgroup\$ – Edward Apr 11 '17 at 15:56
  • \$\begingroup\$ Yes, that's what I meant. \$\endgroup\$ – Toby Speight Apr 11 '17 at 16:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.