Anyone here using C to compose 'object-based' Microcontroller programmes?

Thread Starter

Hugh Riddle

Joined Jun 12, 2020
78
It's related to but much simpler than 'object-oriented' programme composition. I attempt it (for a mid-range PIC application) by having each .c source file in my project hold only the variable(s) and functions needed for a single purpose (e.g. PowerManager.c, BatteryMonitor.c, Timebase.c) and strive to maximise 'encapsulation' by using C scoping qualifiers to 'hide' everything but the functions which must be accessible from other C source files.

I find this approach's clarity of purpose somehow results in clear layout, truly contextual descriptions, near-grammatical naming, highly readable code and fewer comments within function definitions. It has given me a calmer and more pleasurable way to work in (highly preprocessed) assembler and now C although I'm still struggling to understand fully some of the C scoping defaults and qualifiers I seek to exploit.

I'm hoping some others are working along these lines, as there could be some interesting discussion. I'm pretty new to C and weak on computer science and fearing you'll all say either 'we've been doing that for years' or 'definitely not the way'.
 

JohnInTX

Joined Jun 26, 2012
4,787
It's related to but much simpler than 'object-oriented' programme composition. I attempt it (for a mid-range PIC application) by having each .c source file in my project hold only the variable(s) and functions needed for a single purpose (e.g. PowerManager.c, BatteryMonitor.c, Timebase.c) and strive to maximise 'encapsulation' by using C scoping qualifiers to 'hide' everything but the functions which must be accessible from other C source files.
I think your approach is excellent and at the risk of disappointing you, I have been doing exactly that for years. It sounds to me that any lacking in skills are more in the C language / syntax area. You sound very strong in good programming, organization and problem solving skills - that's the hard part. Thumbing through any decent C reference will clear up any language issues.

It takes more work to do it like you are but the payoffs are as you stated - a calmer and more pleasurable way to work. I would add 'safer' to the list. That approach leads to more reliable and maintainable solutions to the task at hand. In my systems, it is not unusual to have 75-100+ files. Each peripheral, attached IO etc. gets one. Each attached chip DAC, ADC, memory, etc. gets one. That low level functionality is used by system functionality which in turn is used by the main program logic. Setting a FAULT condition is never done in the main code flow. It is done by calling a SystemFault() routine which in turn calls whatever it needs to indicate a fault which in turn calls low level routines. That way, different setups can be accommodated without changing the main program flow. A system fault may turn on an LED, send a message to the display processor, send a message to a connected host or do nothing depending on the hardware configuration. If all of that were done in the main program flow it would be a nightmare to maintain and risk breaking the system when something minor changes. How the hardware is configured is defined by a big config.h file that includes 'snap-in' modules that all have entry points for all the system functions supported. How those functions are actually provided actually unknown to the main program logic.

I think you are exactly on the right track.
Well done.
 

nsaspook

Joined Aug 27, 2009
12,997
It seems to me you're just using old school structured programming that's been the recommended HLL approach since ALGOL 60.
https://www.theregister.com/2020/05/15/algol_60_at_60/

You use the 'object-oriented' data/code organization when it's useful and don't use it when it's not in a controller sized project. If you have dynamically changing (from I/O config bits or manual inputs) data (motor mode and characteristics structures) with similar processing routines (motor testing routines) for a specific type internal programming model (an assembly with X type of motor) it makes sense to unify (motor structure) that into a 'object' structure abstraction for easy manipulation using something like pointers. On projects with lots of mainly static state machine type operations with complex but static computations on I/O object-oriented' is less useful IMO.

The OOP revolution has somehow managed to group-think 'object-oriented' means structured programming but the opposite is true IMO. 'Object-oriented' is just one aspect of a much larger set of programming styles under the umbrella of structured programming.
 

MrChips

Joined Oct 2, 2009
30,618
OOP is appropriate if you are developing HUGE apps for GUI on a Windows, iOS, Android, Linux, etc. system where you have huge memory space along with disk swapping and memory management, IMO.

On an embedded MCU system where speed and memory is limited, C alone works fine.
What you need to focus on is project management, organising your libraries, .c and .h files. You need to think in terms of SW maintenance and reusability across different projects. Strategic use of structures goes a long way in defining your elements in terms of objects. You can get by without using true OOP concepts.
 

nsaspook

Joined Aug 27, 2009
12,997
I would say you can get by without using a 'true' OOP language (something that C++ is not). Using low or zero runtime cost OOP concepts is important (even to hardware guys that also program) in many embedded applications like the actual Linux kernel that's written in C because it helps to push errors from run-time to compile-time. Abstraction is already done in the kernel in C. Encapsulation is already done in the kernel in C. Polymorphism is already done in the kernel in C. Etc... in C. Rewritting it in a OOP language would mainly change semantic coding styles not hardware interface structure with a good optimizing compiler and OO can be as written badly as the worse spaghetti language constructs when you look at the end result runtime. This is really true when you need to debug the ASM code generated. Because C is a simple language the basic ASM constructs generated by a C compiler closely resemble the source language constructs. When you are talking to actual hardware this is important vs the virtual machine world of most applications programs.

 

Thread Starter

Hugh Riddle

Joined Jun 12, 2020
78
I would say you can get by without using a 'true' OOP language (something that C++ is not). Using low or zero runtime cost OOP concepts is important (even to hardware guys that also program) in many embedded applications like the actual Linux kernel that's written in C because it helps to push errors from run-time to compile-time. Abstraction is already done in the kernel in C. Encapsulation is already done in the kernel in C. Polymorphism is already done in the kernel in C. Etc... in C. Rewritting it in a OOP language would mainly change semantic coding styles not hardware interface structure with a good optimizing compiler and OO can be as written badly as the worse spaghetti language constructs when you look at the end result runtime. This is really true when you need to debug the ASM code generated. Because C is a simple language the basic ASM constructs generated by a C compiler closely resemble the source language constructs. When you are talking to actual hardware this is important vs the virtual machine world of most applications programs.

I'm using an 'object-based' method far simpler than any OOP language. C seems a very useful procedure-oriented language. I work around that to accommodate and largely hide simple data+functions objects, one to each source file, and find various advantages accrue. That method also seems to work well in separating off the dirty-hands stuff of I/O and peripherals. I'm not seeking inheritance or polymorphism but you've got me wondering what polymorphism and encapsulation being 'done in the C kernel' means. I really enjoyed that ALGOL60 at 60 article you linked.
 
Last edited:

nsaspook

Joined Aug 27, 2009
12,997
Like I said, before the current 'object' fad what you are doing would be called structured programming. 'Objects' is only one tool in the structured programming bag. Too many objects make ravioli code instead of spaghetti code.
https://www.docsity.com/en/news/pro...asta-spaghetti-lasagna-ravioli-macaroni-code/
Programming Pasta - Spaghetti, Lasagna, Ravioli and Macaroni Code

58:00
When programmers describe code as 'procedural', it’s generally not meant as a compliment. There is a belief that we have collectively moved pass such thinking and onto better paradigms. But a paradigm is no more than a pattern language, a family of solutions fit for a context.
'done in the C kernel'
Polymorphism: File handle IOCTL interface. Read() handles almost any I/O interface from network-sockets, Unix-sockets, pipes, tty, files, etc ...
Encapsulation: Networking. struct sock — network layer representation of sockets
 
Last edited:

Thread Starter

Hugh Riddle

Joined Jun 12, 2020
78
Like I said, before the current 'object' fad what you are doing would be called structured programming. 'Objects' is only one tool in the structured programming bag. Too many objects make ravioli code instead of spaghetti code.
https://www.docsity.com/en/news/pro...asta-spaghetti-lasagna-ravioli-macaroni-code/
Programming Pasta - Spaghetti, Lasagna, Ravioli and Macaroni Code

58:00

'done in the C kernel'
Polymorphism: File handle IOCTL interface. Read() handles almost any I/O interface from network-sockets, Unix-sockets, pipes, tty, files, etc ...
Encapsulation: Networking. struct sock — network layer representation of socketswith you
Thanks for responding. you have helped stimulate me to look more widely; I'm a novice at C and much else and was unaware of Read() but will investigate it should I need I/O polymorphism.

The Algol at 60 article seems explicitly to describe the roots of language development in terms of improving 'procedures'.

It's time I revealed more about the approach that has largely transported my programme composing from pain to pleasure and hope to do that today in the main thread.
 

Ian Rogers

Joined Dec 12, 2012
1,136
Most C programmers kinda use oop anyway..

Take a simple program...

Main() calls several library calls... LCD.. EEPROM... SERIAL... etc... All these are separate files with all data and functions contained.. This is the pre class way.. If the variables are not external then they are invisible ( scoped) to that file..

Most C++ compiler compile to C first, so It must be possible to completely code oop in C..
 

nsaspook

Joined Aug 27, 2009
12,997
One key to going from pain to pleasure in programming is understanding data structures (in C, values without oop features). How you structure the data to be processed determines the code needed to process it efficiently and helps to organize the programs modular structure if you construct a modular data model from the beginning.
https://en.wikipedia.org/wiki/Data_structure
 

Thread Starter

Hugh Riddle

Joined Jun 12, 2020
78
A BIT OF BACKGROUND
A good many years ago, I stumbled upon a book about the HOOD method commissioned by the European Space Agency and forcibly applied in their space and aerospace projects for Ada, Fortran and C. For the very first time, I beheld the textual and graphic clarity I had been craving.

HOOD stands for Hierarchical Object-Oriented Design and employs a parent-child structure which among other things seems to help resist spaghettification. It attained stability before it seems being plundered by the mainstream UML, the clarity of which IMHO quickly succumbs to increasing complexity (view a complicated UML statechart).

ESA provide some information at:
http://www.esa.int/TEC/Software_engineering_and_standardisation/TECKLAUXBQE_0.html
and bits of the book, including an explanatory HOOD object diagram, at: https://www.sciencedirect.com/science/article/abs/pii/S1572596006800343

I very simply applied what I had absorbed from that book to my embedded assembler work, using lots of preprocessing (including some basic C-like run-time operations and tightly controlled inter-.asm 'messaging'; assembly took as long as a C compile); the exercise was enjoyable and successful and delivered the PIC firmware for a medical device firm's product.

I've attached an imaginary template for a HOOD-style source file which I hope won't overly need modifying for C. Its top bits are filled in from a current project for solar-powered flashing placard and I've made [notes] against its other headings. I will later re-post it with some of its (underdeveloped and doubtless error-stricken code) - I really am a C novice - and hope to learn more.
 

Attachments

nsaspook

Joined Aug 27, 2009
12,997
That sounds really good but I would be careful about being dogmatic with that approach with smaller embedded projects. As I posted with the Pasta code link, spaghettification is only one form of code pasta.
Program structure should suit the language. Make it something a regular C procedural programmer can easily understand. Too many layers, abstractions and 'objects' can obscure the programs intent.

I'm modifying some code from the web for a simple serial port command parser. One header file and one source file should be all you need to understand how it works.
C:
/*
* File:   scmd.h
* Author: root
*
* Created on October 9, 2020, 6:42 PM
*/

/*
* adapted from original code link: http://pcarduino.blogspot.com/2014/01/simple-serial-port-command-line.html
* refactored and adapted for embedded xc32 apps
*/

#ifndef SCMD_H
#define    SCMD_H

#ifdef    __cplusplus
extern "C" {
#endif

    /*
     * serial port command parser
     */
#define CLI_CMD_BUFFER_SIZE 128

    typedef struct _t_cmd {
        const char *cmd;
        void (*fh)(void*);
    } t_cmd;

    typedef struct _t_cli_ctx {
        t_cmd *cmds;
        char cmd[CLI_CMD_BUFFER_SIZE];
        uint8_t cpos;
    } t_cli_ctx;

#define CLI_IO_INPUT(__data) \
    linux_getc(__data)

#define CLI_IO_OUTPUT(__data, __len) \
    linux_putc(__data, __len)

#define KEY_CODE_BACKSPACE    0x7f
#define KEY_CODE_DELETE        0x7e
#define KEY_CODE_ENTER        '\r'
#define KEY_CODE_ESCAPE        0x1b

#define POSINC(__x) (((__x) < (CLI_CMD_BUFFER_SIZE - 1)) ? (__x + 1) : (__x))
#define POSDEC(__x) ((__x) ? ((__x) - 1) : 0)

    typedef enum _t_cmd_status {
        E_CMD_OK = 0,
        E_CMD_NOT_FOUND,
        E_CMD_TOO_SHORT,
        E_CMD_EMPTY
    } t_cmd_status;

    /*
     * command execute functions
     * prototypes
     */
    void fh_hw(void *a_data);
    void fh_hi(void *a_data);
    void fh_ho(void *a_data);

    /*
     * command parser functions
     */
    void scmd_init(void);
    void cli_read(t_cli_ctx *);

#ifdef    __cplusplus
}
#endif

#endif    /* SCMD_H */
C:
#include "vcan.h"
#include "scmd.h"

static t_cmd g_cmds[] = {

    { "hw", fh_hw},
    { "hi", fh_hi},
    { "ho", fh_ho},

    // null command terminator
    { 0x00, 0x00}
};

const char cmdm[] = "\r\n Command processor V0.1\r\n";
t_cli_ctx cli_ctx; // command buffer
extern const char *build_date, *build_time;
uint8_t res = E_CMD_OK;

static void cli_init(t_cli_ctx *a_ctx, t_cmd *a_cmds)
{
    memset(a_ctx, 0x00, sizeof(t_cli_ctx));
    a_ctx->cmds = a_cmds;
    /*
     * serial port boot messages
     */
    while (UART3_WriteFreeBufferCountGet() < 10);
    sprintf(a_ctx->cmd, "\r\n VCAN %s %s\r\n", build_date, build_time);
    UART3_Write((uint8_t *) a_ctx->cmd, strlen(a_ctx->cmd));
    while (UART3_WriteFreeBufferCountGet() < 10);
    UART3_Write((uint8_t *) cmdm, strlen(cmdm));
}

void scmd_init(void)
{
    cli_init(&cli_ctx, g_cmds);
}

uint8_t linux_getc(uint8_t *a_data)
{
    if (UART3_ReadCountGet()) {
        UART3_Read(a_data, 1);
        return 1;
    } else {
        return 0;
    }
}

uint8_t linux_putc(uint8_t *data, uint8_t a_len)
{
    UART3_Write(data, a_len);
    return 1;
}

static uint8_t _cli_interpret_cmd(t_cli_ctx *a_ctx)
{
    uint8_t i = 0;
    uint8_t ret = E_CMD_OK;

    if (!strlen(a_ctx->cmd)) {
        return E_CMD_EMPTY;
    }

    if (strlen(a_ctx->cmd) < 2) {
        return E_CMD_TOO_SHORT;
    }

    while (a_ctx->cmds[i].fh) {
        if (!strncmp(a_ctx->cmds[i].cmd, a_ctx->cmd, strlen(a_ctx->cmds[i].cmd))) {

            // call the handler
            a_ctx->cmds[i].fh((void *) a_ctx);
            break;
        }
        i++;
    }

    if (!a_ctx->cmds[i].fh) {
        ret = E_CMD_NOT_FOUND;
    }

    return ret;
}

void cli_read(t_cli_ctx *a_ctx)
{
    uint8_t i = 0x00;

    // if no character available - then exit
    if (!CLI_IO_INPUT(&i)) return;

    /// char by char matching
    switch (i) {
    case KEY_CODE_BACKSPACE: // backspace
    case KEY_CODE_DELETE: // del
        break;
    case KEY_CODE_ESCAPE: // special characters
        break;
    case KEY_CODE_ENTER: // new line
        a_ctx->cmd[POSINC(a_ctx->cpos)] = '\0';
        CLI_IO_OUTPUT((unsigned char *) "\r\n", 2);
        res = _cli_interpret_cmd(a_ctx);
        a_ctx->cpos = 0;
        memset(a_ctx->cmd, 0x00, CLI_CMD_BUFFER_SIZE);
        break;
    default:
        /* echo */
        if (a_ctx->cpos < (CLI_CMD_BUFFER_SIZE - 1)) {
            a_ctx->cmd[a_ctx->cpos++] = i;
            CLI_IO_OUTPUT(&i, 1);
        }
        break;
    }
}
Inside main for the command function implementations.

C:
void fh_hw(void *a_data)
{
    t_cli_ctx *cmd=a_data;
    POS3CNT = 1000;
    UART3_Write((uint8_t*) cmd->cmd, strlen(cmd->cmd));
}

void fh_hi(void *a_data)
{
    POS3CNT = 3000;
    UART3_Write((uint8_t*) " hi      ", 8);
}

void fh_ho(void *a_data)
{
    POS3CNT = 0;
    UART3_Write((uint8_t*) " ho      ", 8);
}
This is mainly just placeholder code to check for proper operation from a remote terminal.
 
Last edited:

Thread Starter

Hugh Riddle

Joined Jun 12, 2020
78
That sounds really good but I would be careful about being dogmatic with that approach with smaller embedded projects. As I posted with the Pasta code link, spaghettification is only one form of code pasta.
Program structure should suit the language. Make it something a regular C procedural programmer can easily understand. Too many layers, abstractions and 'objects' can obscure the programs intent.

I'm modifying some code from the web for a simple serial port command parser. One header file and one source file should be all you need to understand how it works.
C:
/*
* File:   scmd.h
* Author: root
*
* Created on October 9, 2020, 6:42 PM
*/

/*
* adapted from original code link: http://pcarduino.blogspot.com/2014/01/simple-serial-port-command-line.html
* refactored and adapted for embedded xc32 apps
*/

#ifndef SCMD_H
#define    SCMD_H

#ifdef    __cplusplus
extern "C" {
#endif

    /*
     * serial port command parser
     */
#define CLI_CMD_BUFFER_SIZE 128

    typedef struct _t_cmd {
        const char *cmd;
        void (*fh)(void*);
    } t_cmd;

    typedef struct _t_cli_ctx {
        t_cmd *cmds;
        char cmd[CLI_CMD_BUFFER_SIZE];
        uint8_t cpos;
    } t_cli_ctx;

#define CLI_IO_INPUT(__data) \
    linux_getc(__data)

#define CLI_IO_OUTPUT(__data, __len) \
    linux_putc(__data, __len)

#define KEY_CODE_BACKSPACE    0x7f
#define KEY_CODE_DELETE        0x7e
#define KEY_CODE_ENTER        '\r'
#define KEY_CODE_ESCAPE        0x1b

#define POSINC(__x) (((__x) < (CLI_CMD_BUFFER_SIZE - 1)) ? (__x + 1) : (__x))
#define POSDEC(__x) ((__x) ? ((__x) - 1) : 0)

    typedef enum _t_cmd_status {
        E_CMD_OK = 0,
        E_CMD_NOT_FOUND,
        E_CMD_TOO_SHORT,
        E_CMD_EMPTY
    } t_cmd_status;

    /*
     * command execute functions
     * prototypes
     */
    void fh_hw(void *a_data);
    void fh_hi(void *a_data);
    void fh_ho(void *a_data);

    /*
     * command parser functions
     */
    void scmd_init(void);
    void cli_read(t_cli_ctx *);

#ifdef    __cplusplus
}
#endif

#endif    /* SCMD_H */
C:
#include "vcan.h"
#include "scmd.h"

static t_cmd g_cmds[] = {

    { "hw", fh_hw},
    { "hi", fh_hi},
    { "ho", fh_ho},

    // null command terminator
    { 0x00, 0x00}
};

const char cmdm[] = "\r\n Command processor V0.1\r\n";
t_cli_ctx cli_ctx; // command buffer
extern const char *build_date, *build_time;
uint8_t res = E_CMD_OK;

static void cli_init(t_cli_ctx *a_ctx, t_cmd *a_cmds)
{
    memset(a_ctx, 0x00, sizeof(t_cli_ctx));
    a_ctx->cmds = a_cmds;
    /*
     * serial port boot messages
     */
    while (UART3_WriteFreeBufferCountGet() < 10);
    sprintf(a_ctx->cmd, "\r\n VCAN %s %s\r\n", build_date, build_time);
    UART3_Write((uint8_t *) a_ctx->cmd, strlen(a_ctx->cmd));
    while (UART3_WriteFreeBufferCountGet() < 10);
    UART3_Write((uint8_t *) cmdm, strlen(cmdm));
}

void scmd_init(void)
{
    cli_init(&cli_ctx, g_cmds);
}

uint8_t linux_getc(uint8_t *a_data)
{
    if (UART3_ReadCountGet()) {
        UART3_Read(a_data, 1);
        return 1;
    } else {
        return 0;
    }
}

uint8_t linux_putc(uint8_t *data, uint8_t a_len)
{
    UART3_Write(data, a_len);
    return 1;
}

static uint8_t _cli_interpret_cmd(t_cli_ctx *a_ctx)
{
    uint8_t i = 0;
    uint8_t ret = E_CMD_OK;

    if (!strlen(a_ctx->cmd)) {
        return E_CMD_EMPTY;
    }

    if (strlen(a_ctx->cmd) < 2) {
        return E_CMD_TOO_SHORT;
    }

    while (a_ctx->cmds[i].fh) {
        if (!strncmp(a_ctx->cmds[i].cmd, a_ctx->cmd, strlen(a_ctx->cmds[i].cmd))) {

            // call the handler
            a_ctx->cmds[i].fh((void *) a_ctx);
            break;
        }
        i++;
    }

    if (!a_ctx->cmds[i].fh) {
        ret = E_CMD_NOT_FOUND;
    }

    return ret;
}

void cli_read(t_cli_ctx *a_ctx)
{
    uint8_t i = 0x00;

    // if no character available - then exit
    if (!CLI_IO_INPUT(&i)) return;

    /// char by char matching
    switch (i) {
    case KEY_CODE_BACKSPACE: // backspace
    case KEY_CODE_DELETE: // del
        break;
    case KEY_CODE_ESCAPE: // special characters
        break;
    case KEY_CODE_ENTER: // new line
        a_ctx->cmd[POSINC(a_ctx->cpos)] = '\0';
        CLI_IO_OUTPUT((unsigned char *) "\r\n", 2);
        res = _cli_interpret_cmd(a_ctx);
        a_ctx->cpos = 0;
        memset(a_ctx->cmd, 0x00, CLI_CMD_BUFFER_SIZE);
        break;
    default:
        /* echo */
        if (a_ctx->cpos < (CLI_CMD_BUFFER_SIZE - 1)) {
            a_ctx->cmd[a_ctx->cpos++] = i;
            CLI_IO_OUTPUT(&i, 1);
        }
        break;
    }
}
Inside main for the command function implementations.

C:
void fh_hw(void *a_data)
{
    t_cli_ctx *cmd=a_data;
    POS3CNT = 1000;
    UART3_Write((uint8_t*) cmd->cmd, strlen(cmd->cmd));
}

void fh_hi(void *a_data)
{
    POS3CNT = 3000;
    UART3_Write((uint8_t*) " hi      ", 8);
}

void fh_ho(void *a_data)
{
    POS3CNT = 0;
    UART3_Write((uint8_t*) " ho      ", 8);
}
This is mainly just placeholder code to check for proper operation from a remote terminal.
Thanks nsaspook for your interest. Your code looks beautifully clearly presented and its abbreviations as clear as they go. Unfortunately, I'd need a lot of explanation (and a great deal more learning) to understand and really appreciate how it works.

In the elementary domain I inhabit, the 'method' I espouse requires the source file to include natural English explanations. I also try to make identifiers cogent and relatively readable albeit lengthy and dwell on that in the reply and code I hope to post today.
 

nsaspook

Joined Aug 27, 2009
12,997
If you write C then it's a good idea to look at some of the best embedded C code in the Linux kernel and some style rules for that code. This is not to derail your own personal style (for my own stuff I don't follow all the rules), it's to inform you what you will typically see to looking for C code examples to solve X problem and what others will expect from your source code.

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/?h=v5.9
https://www.kernel.org/doc/html/v4.10/process/coding-style.html
C is a Spartan language, and so should your naming be. Unlike Modula-2 and Pascal programmers, C programmers do not use cute names like ThisVariableIsATemporaryCounter. A C programmer would call that variable tmp, which is much easier to write, and not the least more difficult to understand.
...
Comments are good, but there is also a danger of over-commenting. NEVER try to explain HOW your code works in a comment: it’s much better to write the code so that the working is obvious, and it’s a waste of time to explain badly written code.
http://www.kroah.com/linux/talks/ols_2002_kernel_codingstyle_talk/html/mgp00001.html
 
Last edited:

Thread Starter

Hugh Riddle

Joined Jun 12, 2020
78
Please bear with my developing code's shortcomings (learning C since August; first C project) - it's mainly about the method. Below I use the term OBject to denote a C source file holding a single 'object-based' entity comprising one or more C variables and functions which together efficiently serve a particular purpose. Barely rates on the OOP scale.

The REQUIRED OPERATIONS and PROVIDED OPERATIONS sections respectively hold a C prototype for every function that calls or is called by another OBject, each with a unique identifier which incorporates its calling and target OBject's names. PowerManager's PROVIDED OPERATIONS DEFINITIONS section currently includes a (heavily indented) prototype immediately above every definition. Sub-operations accessible only within PowerManager have simpler identifiers not guaranteed to be unique.

Natural language is considered unable to meet coding's need but see how extensively readable English permeates PowerManager, from the slightly terse initial and core function descriptions through near-conversationally phrased variable and questioning function identifiers to military-sounding commands and almost eliminates normal commentary (which I've used for my TBDs and to point out features of the method). I believe this practice can encourage coders to think more carefully about each OBject's core purpose and workings in its context and greatly speed subsequent familiarisation. To accommodate this, PowerManager's (Notepad++) text is 'short and fat' (mustn't wrap until after column 115).

I haven't yet added any error-trapping or tried to 'hide' code but, if I become less confused by C scoping defaults and qualifiers, will amend and tidy all sections. I'm particularly keen to understand how best to use visibility, rationales behind C prototypes, routing of messages and delights of spaghetti and would welcome all and every comment, suggestion and advice.
 

Attachments

BobaMosfet

Joined Jul 1, 2009
2,110
It's related to but much simpler than 'object-oriented' programme composition. I attempt it (for a mid-range PIC application) by having each .c source file in my project hold only the variable(s) and functions needed for a single purpose (e.g. PowerManager.c, BatteryMonitor.c, Timebase.c) and strive to maximise 'encapsulation' by using C scoping qualifiers to 'hide' everything but the functions which must be accessible from other C source files.

I find this approach's clarity of purpose somehow results in clear layout, truly contextual descriptions, near-grammatical naming, highly readable code and fewer comments within function definitions. It has given me a calmer and more pleasurable way to work in (highly preprocessed) assembler and now C although I'm still struggling to understand fully some of the C scoping defaults and qualifiers I seek to exploit.

I'm hoping some others are working along these lines, as there could be some interesting discussion. I'm pretty new to C and weak on computer science and fearing you'll all say either 'we've been doing that for years' or 'definitely not the way'.
@Hugh Riddle
What you are doing is how it was originally meant to be done- this is called 'modular' design. What you are actually doing, mimics the compiler itself- each of your 'c' source files is actually a library in source form.

The way it work is this.

You have main.c, and then you have a c file, and a h file for every other 'module'/library you wish to include. Scoping is easy.

for example:

main.c // core file
i2c.c & i2c.h // i2c comm library
debug.c & debug.h // debug utilities
ticktock.c & ticktock.h // time & 'clock' management
...

and so on.

Scoping is one of the things that a lot of C/C++ programmers don't understand well. You have global declaration, and static or local declarations.

main.c would include i2c.h. The i2c library would be scoped thus:

i2c.c: (globally accessible functions):

Code:
/*****
 *
 *    PROTOTYPES
 *
 *****/

void    i2c_init(unsigned long SCLf,unsigned char i2cPre);                                            // Initialize the i2c Interface
bool    sendbuf_i2c(unsigned char cmd,unsigned char *i2cBufP,unsigned char i2cDataLen,bool sFlag);    // Send to I2C Device
bool     readbuf_i2c(unsigned char cmd,unsigned char *i2cBufP,unsigned char i2cDataLen);                // Read from I2C Device
Any other prototype that is only for use in the i2c.c file would be declared as 'static', and thus opaque to the linker, and not referenced in the i2c.h header file.

The i2c.h header file has the above prototypes made available to main.c or any other file that includes the i2c library using the 'extern' reference, like so:

Code:
/*****
 *
 *    EXTERNAL PROTOTYPES
 *
 *****/

extern void        i2c_init(unsigned long SCLf,unsigned char i2cPre);                                            // i2c Comms Initialization
extern bool        sendbuf_i2c(unsigned char cmd,unsigned char *i2cBufP,unsigned char i2cDataLen,bool sFlag);    // Send to I2C Device
extern bool        readbuf_i2c(unsigned char cmd,unsigned char *i2cBufP,unsigned char i2cDataLen);                // Read from I2C Device
The actual function declarations are managed accordingly. Variables are scoped exactly the same way. Defines also exist in that defines that main does not need to know about are NOT in the library's header file.

It's modular, it makes sense, and it allows you to use any 'library' you create in any project, growing your capabilities without having to change library code at all.

Warning: Mini Rant:
One of the main problems with compiler makers today is that they have removed the ability to define 'include' pathing in projects. This is a critical mis-understanding by chip makers who make these compilers about a fundamental requirement of the C/C++ standard. Instead, they actually duplicate your 'library' files across your directory tree for every project. Stupid idiots have no idea the carnage they've created.

By having include pathing, it allows you to create a 'library' just like a compiler uses, whether or not in object form, and thus use that exact same code piece in every project. Which means that if you find a bug, you fix one file, and it fixes it across all projects- that's they old way. In the new way, without include pathing, you have to manually hunt that file down and fix it in every location across your directory tree.
 

Thread Starter

Hugh Riddle

Joined Jun 12, 2020
78
If you write C then it's a good idea to look at some of the best embedded C code in the Linux kernel and some style rules for that code. This is not to derail your own personal style (for my own stuff I don't follow all the rules), it's to inform you what you will typically see to looking for C code examples to solve X problem and what others will expect from your source code.

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/?h=v5.9
https://www.kernel.org/doc/html/v4.10/process/coding-style.html


http://www.kroah.com/linux/talks/ols_2002_kernel_codingstyle_talk/html/mgp00001.html
I beg to disagree on C being a spartan language justifying using very short identifiers (perhaps excepting variables local to function bodies). Longer names can help speed understanding the purpose served by a variable or function. I suspect short names may be a legacy from earlier (restricted name length) days and that some coders see cryptic names as helping make themselves indispensable.

The code you showed us certainly included medium-length abbreviations that were highly understandable - perhaps some day I'll acquire that skill. I'm a bit of a pedant but frequently review my lengthy identifiers. An exercise: In the code I posted, what might be a better C function identifier than that below?
void InterfaceWithHost_ StepIncreaseEnergySourceVoltage()
It decreases the forward (solar panel to rechargeable battery) voltage transformation ratio of a DC-DC (buck) converter by an amount corresponding to 1 output level of an on-chip DAC that (analogue) controls the converter's duty cycle.
 

nsaspook

Joined Aug 27, 2009
12,997
I'm only telling you what the common C (the next language after B so even computer language names are short ;) ) conventions are. The indispensable crack is a unnecessary put-down for people with a huge amount of experience. Sure, it's an idiom legacy but long term C embedded programmers will likely not change because the mental patterns (many have hardware engineering math-based backgrounds with a tradition of short identifiers and functions in mathematical equations) they use are simplistic, short mnemonics for hardware machine operations that are expressed in short legacy C notation as an abstract machine language, not a human grammar like COBOL.

qf.png
quadratic formula

For our own personal uses we all have personal tastes but if you share software or work in groups you will find that Camel-Case, long names are the exception the closer you get to actual hardware coding. Most just don't care about better function identifiers, they care about better function code (thinking about the problem rather than the representation), not overloading names with meaning.

Typing: void InterfaceWithHost_ StepIncreaseEnergySourceVoltage() obviously don't make if actually:

decreases the forward (solar panel to rechargeable battery) voltage transformation ratio of a DC-DC (buck) converter by an amount corresponding to 1 output level of an on-chip DAC that (analogue) controls the converter's duty cycle.

any better or worse than
Typing: void adj_bat_volts()

One study.
http://www2.unibas.it/gscanniello/Giuseppe_Scanniello@unibas/Home_files/TOSEM.pdf
We carried out a family of controlled experiments to investigate whether the use of abbreviated identifier names, with respect to full-word identifier names, affects fault fixing in C and Java source code. This family consists of an original (or baseline) controlled experiment and three replications. We involved 100 participants with different backgrounds and experiences in total. Overall results suggested that there is no difference in terms of effort, effectiveness, and efficiency to fix faults, when source code contains either only abbreviated or only full-word identifier names. We also conducted a qualitative study to understand the values, beliefs, and assumptions that inform and shape fault fixing when identifier names are either abbreviated or full-word. We involved in this qualitative study six professional developers with one to three years of work experience. A number of insights emerged from this qualitative study and can be considered a useful complement to the quantitative results from our family of experiments. One of the most interesting insight is that developers, when working on source code with abbreviated identifier names, adopt a more methodical approach to identify and fix faults by extending their focus point and only in a few cases they expand abbreviated identifiers.
In this article, we present a family of controlled experiments to assess whether abbreviated or full word identifier names have any effect on the removal of faults in unfamiliar C and Java source code. Our family consisted of four experiments involving a total of 100 participants. Results indicated that the difference in using abbreviated and full-word identifier names is not statistically significant with respect to the effort, the effectiveness, and the efficiency to identify and fix faults. We also conducted a qualitative study on a real application containing real faults. We involved in this study six professional developers with one to three years of work experience. Results allowed us to find additional insights and complement those from our family of experiments. We can summarize these insights as follows: fault fixing in source code with abbreviated identifiers needs a more methodical approach and abbreviated identifiers are not perceived as major obstacle to fix faults in source code.
 
Last edited:

Thread Starter

Hugh Riddle

Joined Jun 12, 2020
78
I'm only telling you what the common C (the next language after B so even computer language names are short ;) ) conventions are. The indispensable crack is a unnecessary put-down for people with a huge amount of experience. Sure, it's an idiom legacy but long term C embedded programmers will likely not change because the mental patterns (many have hardware engineering math-based backgrounds with a tradition of short identifiers and functions in mathematical equations) they use are simplistic, short mnemonics for hardware machine operations that are expressed in short legacy C notation as an abstract machine language, not a human grammar like COBOL.

View attachment 219622
quadratic formula

For our own personal uses we all have personal tastes but if you share software or work in groups you will find that Camel-Case, long names are the exception the closer you get to actual hardware coding. Most just don't care about better function identifiers, they care about better function code (thinking about the problem rather than the representation), not overloading names with meaning.

Typing: void InterfaceWithHost_ StepIncreaseEnergySourceVoltage() obviously don't make if actually:

decreases the forward (solar panel to rechargeable battery) voltage transformation ratio of a DC-DC (buck) converter by an amount corresponding to 1 output level of an on-chip DAC that (analogue) controls the converter's duty cycle.

any better or worse than
Typing: void adj_bat_volts()

One study.
http://www2.unibas.it/gscanniello/Giuseppe_Scanniello@unibas/Home_files/TOSEM.pdf
Thanks. I'm impressed by hearing of research into the effects of name lengths but who are the 'we' you refer to? I can understand software experts, scientists and the mathematically inclined using highly abbreviated names which are nevertheless probably in some ways familiar amongst them and can myself fall into that habit but now believe it can impede the sharing of specialised knowledge with outsiders and learners such as myself.

BTW, I've anyway been searching for a better identifier for my example as, unlike DC-DC converter voltage ratios, battery voltages are not stepped or adjusted. This relates to my finding V-I characteristic (ratiometric) transformation rarely being highlighted as crucial to maximum power point tracking (MPPT). So engineering's in there too with its leanings towards abbreviations, initialisms and jargon. I'm partial to camels and spaghetti and the best forum reply, seen I think on Stack Overflow, on what makes a function identifier too long:
"If you have to scroll the page sideways" - Lol
 

nsaspook

Joined Aug 27, 2009
12,997
By 'We' I mean anyone writing code for their own personal uses with no requirements for group styles or coding rules, where others can take it or leave it. Specialized programming with specialized knowledge is a (gender neural) priesthood in many ways. The outsiders and learners don't get to decide unless they pass the rituals, learn the Latin, get on the inside and earn the right to change the rules from the inside. IMO there is no spoon and no short-cuts.


Today I'm merging some IMU code from a Parallax LSM9DS1 9-axis IMU Module Propeller C lib into a new PIC32MK MCJ dev board using Mplab-X and XC32.
https://www.microchip.com/DevelopmentTools/ProductDetails/PartNO/DT100113
https://github.com/parallaxinc/Simple-Libraries/tree/Learn-Folder-Updates/Learn/Simple Libraries/Sensor/liblsm9ds1

Simple stuff that requires a lot of background knowledge of what's important to keep fairly intact and what can be tossed into the bit-bucket as incompatible with the new hardware.

C:
/**
* @file LSM9DS1_readAccel.c
*
* @author Matthew Matz
*
* @version 0.5
*
* @copyright
* Copyright (C) Parallax, Inc. 2016. All Rights MIT Licensed.
*
* @brief This Propeller C library was created for the Parallax 9-axis IMU Sensor, based
* on the STMicroelectronics LSM9DS1 inertial motion sensor chip.
*/

#include "lsm9ds1.h"

int __pinAG;

float __aRes;
int __aBiasRaw[3];
char __autoCalc;


void imu_readAccel(int *ax, int *ay, int *az)
{
  unsigned char temp[6]; // We'll read six bytes from the accelerometer into temp
  short tempX, tempY, tempZ;

  imu_SPIreadBytes(__pinAG, OUT_X_L_XL, temp, 6); // Read 6 bytes, beginning at OUT_X_L_XL
  tempX = (temp[1] << 8) | temp[0]; // Store x-axis values into ax
  tempY = (temp[3] << 8) | temp[2]; // Store y-axis values into ay
  tempZ = (temp[5] << 8) | temp[4]; // Store z-axis values into az

  *ax = (int) tempX;
  *ay = (int) tempY;
  *az = (int) tempZ;

  if (__autoCalc & 0b10)
  {
    *ax -= __aBiasRaw[X_AXIS];
    *ay -= __aBiasRaw[Y_AXIS];
    *az -= __aBiasRaw[Z_AXIS];
  }
}

void imu_readAccelCalculated(float *ax, float *ay, float *az)
{
  // Return the accel raw reading times our pre-calculated g's / (ADC tick):
  int tempX, tempY, tempZ;

  imu_readAccel(&tempX, &tempY, &tempZ);

  *ax = ((float) tempX) / __aRes;
  *ay = ((float) tempY) / __aRes;
  *az = ((float) tempZ) / __aRes;
}


/*
* Based on the Arduino Library for the LSM9SD1 by Jim Lindblom of Sparkfun Electronics
*/

/**
* TERMS OF USE: MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
C:
/**
* @file LSM9DS1_SPIread.c
*
* @author Matthew Matz
*
* @version 0.5
*
* [USER=200858]@Copyright[/USER]
* Copyright (C) Parallax, Inc. 2016. All Rights MIT Licensed.
*
* @brief This Propeller C library was created for the Parallax 9-axis IMU Sensor, based
* on the STMicroelectronics LSM9DS1 inertial motion sensor chip.
*/


#include "simpletools.h"
#include "lsm9ds1.h"


int __pinM, __pinSDIO, __pinSCL;

void imu_SPIreadBytes(unsigned char csPin, unsigned char subAddress, unsigned char *dest, unsigned char count)
{
  // To indicate a read, set bit 0 (msb) of first byte to 1
  unsigned char rAddress = 0x80 | (subAddress & 0x3F);

  // Mag SPI port is different. If we're reading multiple bytes,
  // set bit 1 to 1. The remaining six bytes are the address to be read
  if ((csPin == __pinM) && count > 1) rAddress |= 0x40;
 
  low(csPin);
  shift_out(__pinSDIO, __pinSCL, MSBFIRST, 8, rAddress);
  for (int i=0; i<count; i++)
  {
    dest[I] = shift_in(__pinSDIO, __pinSCL, MSBPRE, 8);
  }
  high(csPin);
}


/*
* Based on the Arduino Library for the LSM9SD1 by Jim Lindblom of Sparkfun Electronics
*/

/**
* TERMS OF USE: MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
Looking at the verbosity of code identifier variables and functions labels for the first time tells me little about actual hardware functionality and the hardware interfaces I need to change to make it compatible with the new hardware. For something like imu_SPIreadBytes the name is useful for what the function does but for the actual code in the function it's much less descriptive as I need to rip-out all of the internal code in that function and start over with something that reads selected spi ports on a PIC32.
 
Top