Author Topic: How is this programmed?  (Read 434 times)

0 Members and 1 Guest are viewing this topic.

Offline Ground_Loop

  • Regular Contributor
  • *
  • Posts: 116
  • Country: us
How is this programmed?
« on: April 10, 2020, 11:47:20 pm »
I have a Arduino sketch that references functions in a manner that looks like members of a structure.  I'd like to do the same for a PIC project. For instance, in the Arduino sketch there are numerous functions defined for the LCD display control such as Set_Cursor(x,y), blink(), clear(), etc. defined in the usual manner in header and source files.  However, when used in the main sketch they are referenced similar to structure members as LCD.Set_Cursor(x,y) or LCD.clear(). I don't see in any of the files how the "LCD." portion of the reference is setup and would really like to do it in other programs on other platforms.  Apologies if this is a confusing description.
« Last Edit: April 10, 2020, 11:55:49 pm by Ground_Loop »
There's no point getting old if you don't have stories.
 

Offline WattsThat

  • Regular Contributor
  • *
  • Posts: 215
  • Country: us
Re: How is this programmed?
« Reply #1 on: April 10, 2020, 11:56:20 pm »
No specifics on devices/compilers/environments so it’s hard answer the question but...

LCD is instantiated somewhere in the Arduino code, you just haven’t looked in the right place.

The Arduino environment use the GCC compilers which are C++, in the PIC world, the 8 bit compilers don’t do C++. So if it’s an 8bit PIC, you’re barking up the wrong tree, can’t do it.
 

Online greenpossum

  • Frequent Contributor
  • **
  • Posts: 260
  • Country: au
Re: How is this programmed?
« Reply #2 on: April 10, 2020, 11:57:47 pm »
The Arduino compiler actually is a C++ compiler and what you have described are member functions, and are referred to similar to storage members but of course there is only one instance of the function. The object which the function is invoked on is implementation-wise handled like a hidden argument.

To do the same on the PIC you'd need a C++ compiler, otherwise you'd have to emulate it in C, say by passing a pointer to the object (struct) as the first argument by convention. That's what's done in languages like Python.
 

Offline Ground_Loop

  • Regular Contributor
  • *
  • Posts: 116
  • Country: us
Re: How is this programmed?
« Reply #3 on: April 11, 2020, 12:33:02 am »
Thanks.  I had a suspicion it was a C++ thing.  My question is answered.
BTW, my other project is on a PIC24 using XC16.  I just thought the technique was cool and wanted to replicate it.  Not really a pressing need though.
There's no point getting old if you don't have stories.
 

Offline Mechatrommer

  • Super Contributor
  • ***
  • Posts: 9711
  • Country: my
  • reassessing directives...
Re: How is this programmed?
« Reply #4 on: April 11, 2020, 12:51:29 am »
I just thought the technique was cool and wanted to replicate it.
its called OOP (Object Oriented Programming) its been there for ages. as long as i can remember, C++ is the one who started it...
It's extremely difficult to start life.. one features of nature.. physical laws are mathematical theory of great beauty... You may wonder Why? our knowledge shows that nature is so constructed. We simply have to accept it. One could describe the situation by saying that... (Paul Dirac)
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 5330
  • Country: fr
Re: How is this programmed?
« Reply #5 on: April 11, 2020, 12:59:36 am »
I just thought the technique was cool and wanted to replicate it.
its called OOP (Object Oriented Programming) its been there for ages. as long as i can remember, C++ is the one who started it...

Nah. OOP concepts date back to the early 60's. And I think the "first" real language featuring them was Simula:
https://en.wikipedia.org/wiki/Simula
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 1714
  • Country: fi
    • My home page and email address
Re: How is this programmed?
« Reply #6 on: April 11, 2020, 07:46:51 pm »
At the risk of being accused of trolling, here is a wall of text, in an attempt to describe the two main patterns used in C (as opposed to C++) relevant to this question.  This message contains the first part, function pointers.



Function pointers

In C, there are two types of pointers: object pointers that point to data, and function pointers that point to functions and behave like functions.  For example:
Code: [Select]
#include <stdlib.h>
#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }

int main(void)
{
    int (*op)(int, int);  /* Function pointer named 'op', points to a function that returns an int, and takes two ints as arguments */

    op = add;
    printf("op = add; op(5, 3) = %d, add(5, 3) = %d.\n", op(5, 3), add(5, 3));

    op = sub;
    printf("op = sub; op(6, 2) = %d, sub(6, 2) = %d.\n", op(6, 2), sub(6, 2));

    op = mul;
    printf("op = mul; op(7, 4) = %d, mul(7, 4) = %d.\n", op(7, 4), sub(7, 4));

    return EXIT_SUCCESS;
}
Many C compilers allow assignment between void object pointers and function pointers, because this is required by the POSIX standard (via e.g. dlsym()) function.  However, C itself does not require that, so for example, in the above code op = NULL; would not be strictly conforming C code.  Of course, even when allowed, calling op(0, 0); (with any parameters) would still dereference a NULL function pointer, and crash the program.

I have written an example command-line Reverse Polish Calculator example here, showing both how a function pointer in a structure is used to keep track of the operators, and how to dynamically load plugins at run time adding new operators.  It should compile and run on all POSIXy operating systems (Linux, Mac OS, *BSDs, many Unix systems).

While function pointers are commonly said to be the way to implement object-oriented programming in C (and the GTK+ widget toolkit is possibly one of the best examples on how it is done), in practice function pointers are much more useful for event-driven programming.

In microcontroller projects, user interfaces are often implemented with event-driven programming.  On a small PIC with an LCD, and a potentially large/complex/deep menu system, we can use function pointers (and object pointers for the menu structure) in a struct to describe the action in each menu entry, and keep it in ROM/FLASH, minimizing the amount of RAM needed.

For example, let's say we have a 2×20 alphanumeric display (or 128×32 OLED) or a similar limited display. Let's say we have four menu traversal buttons, Back, Enter, Prev, Next; and a rotary encoder for changing the displayed value.  We have both integer and decimal numeric values (the encoder button switching between the integer and decimal part, for example), some different strings to choose from, and so on.  Let's assume we have a preprocessor macro FLASHMEM we can use as a type attribute.  Let each menu view be described by a struct:
Code: [Select]
struct menu_view {
    /* Menu navigation */
    const struct menu_view FLASHMEM  *back;
    const struct menu_view FLASHMEM  *enter;
    const struct menu_view FLASHMEM  *prev;
    const struct menu_view FLASHMEM  *next;

    /* Name or title of this menu entry */
    const char FLASHMEM  *title;

    /* RAM data for this menu entry, if any -- can be any type */
    void  *data;

    /* Function to draw the rest of the display (except title); last parameter is dwell time in milliseconds */
    void (*update)(const struct menu_view FLASHMEM *, void *, unsigned int);

    /* Events; last parameter is the time since last event in microseconds */
    void (*increment)(const struct menu_view FLASHMEM *, void *, unsigned int);
    void (*decrement)(const struct menu_view FLASHMEM *, void *, unsigned int);
    void (*press)(const struct menu_view FLASHMEM *, void *, unsigned int);
    void (*release)(const struct menu_view FLASHMEM *, void *, unsigned int);
}
The idea is that you first declare the functions you need to implement each menu view, and then a set of struct menu_view in ROM/flash, and then populate those structs to implement the menu views.  While the structures could be made smaller -- for example, one could use an array instead, and bytes (for up to 256 menu views) or shorts (for up to 32768 menu views) instead of object pointers for the menu structures --, only the menu values (say, index to an array of strings for string entries) need to reside in RAM.

The same common code will handle menu navigation, and a single set of event functions can handle all menu entries of the same type.

In practice, designing such menu systems is best started from the desired menu views themselves.  To a programmer, it might sound bass-ackwards, but really, knowing what to implement is half the battle.  This approach also ensures that you know all required members and functionality from the start of programming, so you can consolidate common code to simple functions, and provide all needed functionality with the least amount of resources used: a big win when talking about 8-bit microcontrollers!
 

Offline Ground_Loop

  • Regular Contributor
  • *
  • Posts: 116
  • Country: us
Re: How is this programmed?
« Reply #7 on: April 11, 2020, 08:04:09 pm »
Found it
This is at the top of the function declarations in the cpp file.  Not sure how "::" makes this work though.

LiquidCrystal_I2C::LiquidCrystal_I2C(uint8_t lcd_addr, uint8_t lcd_cols, uint8_t lcd_rows, uint8_t charsize)

This is where lcd. is assigned in the main program:
LiquidCrystal_I2C lcd(0x3f, 20, 4);
« Last Edit: April 11, 2020, 08:10:30 pm by Ground_Loop »
There's no point getting old if you don't have stories.
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 11768
  • Country: gb
    • Having fun doing more, with less
Re: How is this programmed?
« Reply #8 on: April 11, 2020, 08:05:54 pm »
I just thought the technique was cool and wanted to replicate it.
its called OOP (Object Oriented Programming) its been there for ages. as long as i can remember, C++ is the one who started it...

Simula was the first.
Smalltalk was the most influential.
Then came Objective C, a fusion of C and Smalltalk, used in the Mac.
C++ was C with classes hat ignored all the lessons of the past, repeated many mistakes and added  its own.
Java started with Smalltalk and added in all the lessons of the past, and became the most important OOP language.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 1714
  • Country: fi
    • My home page and email address
Re: How is this programmed?
« Reply #9 on: April 11, 2020, 08:33:25 pm »
Second part of the Wall-Of-Text started a couple of messages earlier.



Library function naming, and self/this initial member

Standard C does not have a mechanism to split interfaces from different libraries used in a program different namespaces.  Symbols can be declared static, in which case they are only visible in that file scope (as opposed to global -- accessible from other file scopes, separately compiled binaries linked to the same program -- or local), but that's about it.  Oh, and some names are strictly speaking reserved for the standard C library; but when programming microcontrollers, we typically use a freestanding environment (without the standard C library, and some "custom" libraries like Hardware Abstraction Layers of some sort instead), and not a standard "hosted environment" C programmers are used to.

To avoid problems with different libraries using the same names, many of them prepend their name to all public (or global) variables or functions they export.  For example, all functions provided by the GNU Scientific Library begin with the prefix gsl_.

In C++, class member functions are provided with a pointer to the object they were called using, this .  In Python, this reference is self.  In general, some variant of this/self is provided by all object-oriented programming languages.  Because C does not have it, we need to pass it explicitly.  Typically, we pass it as the first parameter, because that is easy to remember for us humans, and it allows preprocessor magick if one so desires.

Because I work a lot with numerical data, and occasionally matrices, and the current scientific library interfaces are, uh, rather arcane (see e.g. LAPACK six-letter function naming), and I am most interested in providing tools for other people to implement their ideas with, I developed a very simple interface for matrix operations.  I've shown a compilable example here.

Essentially, there is a single matrix object type, matrix, that describes both first-class rectangular matrices, and "views" to existing matrices.  The actual data exposed by a matrix exists within a reference-counted struct owner.  This means that you can expose the same data in any number of different matrices, transposed or reflected or even just picking an arbitrary regular submatrix, and the change in one is immediately visible in all matrices using the same struct owner.  As long as one remembers to call matrix_free(&m) on each matrix m they no longer need, the data in struct owners is managed and freed only when no longer needed.

(A secondary deletion scheme is also possible, allocation pools.  This means that instead of tracking every single matrix, you create a new allocation pool that you destroy when you are done, destroying all matrices and owner structs in one go.  Unfortunately, I discovered that this model is somehow too easy to break by new programmers, so I have dropped it for now.  I just wanted to point it out in case anyone wonders about it.)

The important point to notice is that each function is named with a matrix_ prefix.  (Naming this library something less generic, and using that name instead of such a generic name, would be even better, as it would reduce the possibility of name collisions.  Who knows, someone might have written another matrix library, and someone might wish to use them both at the same time.)

The first parameter is always a pointer to the matrix to be operated on.  While these particular matrices are typically declared as matrix m;, we cannot pass the matrix itself by value, because then any changes to m would be only visible within the function, and not to the caller!  In C, if we want to modify m in a function with the change visible in the caller, we must pass a pointer to it.  (Note that matrix *const m means that m is a const pointer to a matrix; i.e. that the pointer m will not be modified, but the matrix it points to may be modified.  This notation may seem odd at first, but I use it because it tells us human programmers how the function intends to access the pointer.  Current compilers can usually infer that kind of "const"-ness automatically, but at least in theory, it can also help the compiler to generate better code.)

What does this mean in practice, then?

There are a number of libraries with both C++ and C language bindings.  If the C++ interface looks something like
Code: [Select]
    LCD mydisplay(parameters...);
    mydisplay.print("Hello world!");
then, according to my description of function naming and passing the "object" as the first parameter in C, the C interface is probably either
Code: [Select]
    lcd *mydisplay = lcd_init(parameters...);
    lcd_print(mydisplay, "Hello world!");
or
Code: [Select]
    lcd mydisplay;
    lcd_init(&mydisplay, parameters...);
    lcd_print(&mydisplay, "Hello world!");
depending on whether the LCD/lcd type is intended to be dynamically allocated or not; it is basically an arbitrary choice for the library developer to make.

In a lot of C code, fully uppercase names are "reserved" for preprocessor macros.  It is just a programmer convention, but a common one; and that is the only reason I used lowercase lcd for the type name.  In other words, it is a common stylistic detail that might help in understanding existing code.

Obviously, not every library has both C++ and C bindings.  In fact, only libraries originally written in C tend to have both C++ and C bindings, because C++ is a higher-level language and requires a runtime component anyway.

Unfortunately, this also means that you cannot just take a C++ Arduino library, and expect it to magically work in C.  The two are completely different languages.  For someone who can program in both languages, it may be anything from trivial to impossible to port a library between the two languages.  Fortunately, Arduino libraries, especially display management libraries, tend to fall on the "easy/straightforward" sector, as only a small subset of C++ features are used in Arduino.
« Last Edit: April 11, 2020, 08:38:19 pm by Nominal Animal »
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 1714
  • Country: fi
    • My home page and email address
Re: How is this programmed?
« Reply #10 on: April 11, 2020, 08:49:35 pm »
Ground_Loop, it looks like you are using Frank de Brabanders LiquidCrystal_I2C Arduino library.  It consists of two files: a C++ header file, LiquidCrystal_I2C.h, and the C++ implementation, LiquidCrystal_I2C.cpp.  There are a couple of other alternatives, including John Rickmans LiquidCrystal_I2C, but they are of similar complexity.

None that I've seen have C interfaces, but they are all very straightforward, and use a strict subset of C++, making a C port quite straightforward.



 

Offline Ground_Loop

  • Regular Contributor
  • *
  • Posts: 116
  • Country: us
Re: How is this programmed?
« Reply #11 on: April 12, 2020, 04:04:45 pm »
Ground_Loop, it looks like you are using Frank de Brabanders LiquidCrystal_I2C Arduino library.  It consists of two files: a C++ header file, LiquidCrystal_I2C.h, and the C++ implementation, LiquidCrystal_I2C.cpp.  There are a couple of other alternatives, including John Rickmans LiquidCrystal_I2C, but they are of similar complexity.

None that I've seen have C interfaces, but they are all very straightforward, and use a strict subset of C++, making a C port quite straightforward.
For the Arduino, yes.  I have since written my own LCD display interface for the PIC24 that utilizes SPI and a 16-bit serial-parallel shift register that works really well and has no relation to the Arduino code.  My OP was a simple inquiry about the described technique used in the Arduino.  With a couple clues given here I was able to go back and figure out how it was done as indicated above.

Thanks for all the responses.  You folks have taught me a lot over the last couple years.  I'm older, late-mid career, and although a lifelong electronics hobbyist, I don't write software for a living.  Nonetheless, you're never too old to learn.
« Last Edit: April 12, 2020, 04:15:34 pm by Ground_Loop »
There's no point getting old if you don't have stories.
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 1714
  • Country: fi
    • My home page and email address
Re: How is this programmed?
« Reply #12 on: April 12, 2020, 06:14:59 pm »
My OP was a simple inquiry about the described technique used in the Arduino.  With a couple clues given here I was able to go back and figure out how it was done as indicated above.
Right.  Note that I write my walls of text answers in the hopes that not only you might get something out of them, but in case it helps someone else reading this thread afterwards.  So, I try to be thorough, and not make assumptions on how much you might know.  My intention is not to offend, just be thorough, but it seems many feel this approach is "trolling"; that's why the warning label in the first message.

If you look at the C++ header code, you'll see the class definition.  (The : public Print part means that it is a derivative of the Print class; if this class implements a write function, the Print class will provide print() and println() functions.)

In the C++ code, LiquidCrystal_I2C::LiquidCrystal_I2C defines the constructor function, that initializes the object fields when it is instantiated.
Member functions are defined in a similar way, with the class name prefixed to the member function name.

All public and private variables declared in the class definition are directly accessible in the constructor and member functions.  There is also a special this reference to the object itself.

If we look at the constructor function,
Code: [Select]
class LiquidCrystal_I2C {
public:
    LiquidCrystal_I2C(uint8_t lcd_addr, uint8_t lcd_cols, uint8_t lcd_rows, uint8_t charsize = LCD_5x8DOTS);
private:
    uint8_t _addr;
    uint8_t _displayfunction;
    uint8_t _displaycontrol;
    uint8_t _displaymode;
    uint8_t _cols;
    uint8_t _rows;
    uint8_t _charsize;
    uint8_t _backlightval;
};

LiquidCrystal_I2C::LiquidCrystal_I2C(uint8_t lcd_addr, uint8_t lcd_cols, uint8_t lcd_rows, uint8_t charsize)
{
    _addr = lcd_addr;
    _cols = lcd_cols;
    _rows = lcd_rows;
    _charsize = charsize;
    _backlightval = LCD_BACKLIGHT;
}
In C++, the class is instantiated (an object of that class created) using
    LiquidCrystal_I2C mydisplay(addr, cols, rows, charsize);
or, because the header file defines the default value for charsize, using
    LiquidCrystal_I2C mydisplay(addr, cols, rows);
which works as if a fourth argument with that default value, LCD_5x8DOTS, was passed.

Ported to C, the same code would look like e.g.
Code: [Select]
typedef struct lcd_i2c {
    uint8_t _addr;
    uint8_t _displayfunction;
    uint8_t _displaycontrol;
    uint8_t _displaymode;
    uint8_t _cols;
    uint8_t _rows;
    uint8_t _charsize;
    uint8_t _backlightval;   
} lcd_i2c;
#define LCD_I2C_INIT(addr, cols, rows, charsize) { addr, 0, 0, 0, cols, rows, charsize, LCD_BACKLIGHT }

void lcd_i2c_init(lcd_i2c *display, uint8_t addr, uint8_t cols, uint8_t rows, uint8_t charsize)
{
    display->_addr = addr;
    display->_displayfunction = 0;
    display->_displaycontrol = 0;
    display->_displaymode = 0;
    display->_cols = cols;
    display->_rows = rows;
    display->_charsize = charsize;
    display->_backlightval = LCD_BACKLIGHT;
}
The preprocessor macro is not very common, but it is useful when it is defined.  It allows one to create the object in C using
    lcd_i2c  mydisplay = LCD_I2C_INIT(addr, cols, rows, charsize);
but the more common way is to use the initializer function,
    lcd_i2c  mydisplay;
    lcd_i2c_init(&mydisplay, addr, cols, rows, charsize);

You might wish to look at some of the C++ tutorials, for example here (do a web search to find one that suits you best).  Generally speaking, class/object features (including inheritance), and some rare template cases, are the main differences to C in Arduino C++ code.  (The Arduino environment uses a strict subset of C++ features, to keep the runtime to a minimum.  Exceptions aren't used, for example.)

To repeat, do not take the tone of my writing as condescending, because it is not.  I just have a style of "answering" that tries to help not just the immediate asker, but all those who find the thread later on, through a web search or something.  Why answer a single question, when you can answer a slew of similar questions at the same go?

I'm older, late-mid career, and although a lifelong electronics hobbyist, I don't write software for a living.  Nonetheless, you're never too old to learn.
Fully agreed!
 

Offline Ground_Loop

  • Regular Contributor
  • *
  • Posts: 116
  • Country: us
Re: How is this programmed?
« Reply #13 on: April 12, 2020, 07:08:17 pm »
To repeat, do not take the tone of my writing as condescending, because it is not.  I just have a style of "answering" that tries to help not just the immediate asker, but all those who find the thread later on, through a web search or something.  Why answer a single question, when you can answer a slew of similar questions at the same go?

I assure you, no offense taken.  Thanks for the lessons. I learned something new today. :)
There's no point getting old if you don't have stories.
 
The following users thanked this post: Nominal Animal


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf