How is this programmed? - Page 1
Forum section: electronics / general

author: Ground_Loop
author_rank: Frequent Contributor
timestamp: April 10, 2020, 11:47:20 pm
content: 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.

author: WattsThat
author_rank: Frequent Contributor
timestamp: April 10, 2020, 11:56:20 pm
content: 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.

author: greenpossum
author_rank: Frequent Contributor
timestamp: April 10, 2020, 11:57:47 pm
content: 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.

author: Ground_Loop
author_rank: Frequent Contributor
timestamp: April 11, 2020, 12:33:02 am
content: 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.

author: Mechatrommer
author_rank: Super Contributor
timestamp: April 11, 2020, 12:51:29 am
content: Quote from: Ground_Loop on April 11, 2020, 12:33:02 amits called OOP (Object Oriented Programming) its been there for ages. as long as i can remember, C++ is the one who started it...

author: SiliconWizard
author_rank: Super Contributor
timestamp: April 11, 2020, 12:59:36 am
content: Quote from: Mechatrommer on April 11, 2020, 12:51:29 amNah. 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

author: Nominal Animal
author_rank: Super Contributor
timestamp: April 11, 2020, 07:46:51 pm
content: 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 pointersIn C, there are two types of pointers:object pointersthat point to data, andfunction pointersthat point to functions and behave like functions. For example:Code:#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 betweenvoidobject pointers and function pointers, because this is required by thePOSIXstandard (via e.g.dlsym()) function. However, C itself does not require that, so for example, in the above codeop = NULL;would not be strictly conforming C code. Of course, even when allowed, callingop(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 examplehere, 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 implementobject-oriented programmingin C (and theGTK+widget toolkit is possibly one of the best examples on how it is done), in practice function pointers are much more useful forevent-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 macroFLASHMEMwe can use as a type attribute. Let each menu view be described by a struct:Code: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 ofstruct menu_viewin 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, knowingwhat to implementis 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!

author: Ground_Loop
author_rank: Frequent Contributor
timestamp: April 11, 2020, 08:04:09 pm
content: Found itThis 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_I2Clcd(0x3f, 20, 4);

author: tggzzz
author_rank: Super Contributor
timestamp: April 11, 2020, 08:05:54 pm
content: Quote from: Mechatrommer on April 11, 2020, 12:51:29 amSimula 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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: April 11, 2020, 08:33:25 pm
content: Second part of the Wall-Of-Text started a couple of messages earlier.Library function naming, andself/thisinitial memberStandard C does not have a mechanism to split interfaces from different libraries used in a program differentnamespaces. Symbols can be declaredstatic, 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 afreestanding 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 theGNU Scientific Librarybegin with the prefixgsl_.In C++, class member functions are provided with a pointer to the object they were called using,this. In Python, this reference isself. In general, some variant ofthis/selfis 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, ratherarcane(see e.g.LAPACKsix-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 examplehere.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 amatrixexists within a reference-countedstruct 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 samestruct owner. As long as one remembers to callmatrix_free(&m)on eachmatrix mthey no longer need, the data instruct 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 amatrix_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 asmatrix m;, we cannot pass the matrix itself by value, because then any changes tomwould be only visible within the function, and not to the caller! In C, if we want to modifymin a function with the change visible in the caller, we must pass a pointer to it. (Note thatmatrix *const mmeans thatmis aconstpointer to amatrix; i.e. that the pointermwill not be modified, but thematrixit 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 Clanguage bindings. If the C++ interface looks something likeCode: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 eitherCode:lcd *mydisplay = lcd_init(parameters...);lcd_print(mydisplay, "Hello world!");orCode:lcd mydisplay;lcd_init(&mydisplay, parameters...);lcd_print(&mydisplay, "Hello world!");depending on whether theLCD/lcdtype 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 lowercaselcdfor 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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: April 11, 2020, 08:49:35 pm
content: Ground_Loop, it looks like you are using Frank de BrabandersLiquidCrystal_I2CArduino 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 RickmansLiquidCrystal_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.

author: Ground_Loop
author_rank: Frequent Contributor
timestamp: April 12, 2020, 04:04:45 pm
content: Quote from: Nominal Animal on April 11, 2020, 08:49:35 pmFor 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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: April 12, 2020, 06:14:59 pm
content: Quote from: Ground_Loop on April 12, 2020, 04:04:45 pmRight. 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 theclassdefinition. (The: public Printpart means that it is a derivative of thePrintclass; if this class implements awritefunction, the Print class will provideprint()andprintln()functions.)In the C++ code,LiquidCrystal_I2C::LiquidCrystal_I2Cdefines theconstructorfunction, 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 specialthisreference to the object itself.If we look at the constructor function,Code: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) usingLiquidCrystal_I2C mydisplay(addr,cols,rows,charsize);or, because the header file defines the default value forcharsize, usingLiquidCrystal_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: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 usinglcd_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 examplehere(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?Quote from: Ground_Loop on April 12, 2020, 04:04:45 pmFully agreed!

author: Ground_Loop
author_rank: Frequent Contributor
timestamp: April 12, 2020, 07:08:17 pm
content: Quote from: Nominal Animal on April 12, 2020, 06:14:59 pmI assure you, no offense taken. Thanks for the lessons. I learned something new today.