Skip to content

SD Card implementation, a technical walktrough

Terje Io edited this page Jul 8, 2021 · 4 revisions

The SD Card implementation makes good use of the fact that the HAL is function pointer based. This allows code to be "hooked into" grblHAL without the core codebase needing any changes or beeing aware that changes are beeing made to its runtime environment.

In order to stream GCode from a SDCard a number of features needs to be implemented:

  1. Somehow the SD Card needs to me mounted
  2. It must be possible to list the files
  3. It must be possible to select a file and start a job reading from the selected file
  4. It must be possible to cancel, pause and restart the job
  5. Proper error handling needs to be implemented
  6. If manual tool hange is to be supported it must be possible to suspend a running job

All this is achieved by just temporarily changing function pointers...

1 - 3 is implemented by adding a set of new system commands ($-commands). This is done by setting the ´hal.driver_sys_command_execute` function pointer to point to our system command parser. When the base parser comes across a system command it does not understand it will pass it too our function, we can then decide if it is a command we want to act upon. The pointer is set when the driver initializes the SD Card subsystem.

Our system command parser function:

static status_code_t sdcard_parse (uint_fast16_t state, char *line, char *lcline)
{
    status_code_t retval = Status_Unhandled;

    if(line[1] == 'F') switch(line[2]) {

        case '\0':
            retval = sdcard_ls(line); // (re)use line buffer for reporting filenames
            break;

        case 'M':
            retval = sdcard_mount() ? Status_OK : Status_SDMountError;
            break;

        case '=':
            if (state != STATE_IDLE)
                retval = Status_SystemGClock;
            else {
                if(file_open(&line[3])) {
                    gc_state.last_error = Status_OK;                            // Start with no errors
                    hal.report.status_message(Status_OK);                       // and confirm command to originator
                    memcpy(&active_stream, &hal.stream, sizeof(io_stream_t));   // Save current stream pointers
                    hal.stream.type = StreamSetting_SDCard;                     // then redirect to read from SD card instead
                    hal.stream.read = sdcard_read;                              // ...
                    hal.stream.enqueue_realtime_command = drop_input_stream;    // Drop input from current stream except realtime commands
#if M6_ENABLE
                    hal.stream.suspend_read = sdcard_suspend;                   // ...
#else
                    hal.stream.suspend_read = NULL;                             // ...
#endif
                    hal.driver_rt_report = sdcard_report;                       // Add percent complete to real time report
                    hal.report.status_message = trap_status_report;             // Redirect status message and feedback message
                    hal.report.feedback_message = trap_feedback_message;        // reports here
                    retval = Status_OK;
                } else
                    retval = Status_SDReadError;
            }
            break;

        default:
            retval = Status_InvalidStatement;
            break;
    }

    return retval;
}

Our fuction will be called with two versions of the system command, one uppercased with spaces removed (line) and one with no changes applied (lcline). Since filenames may contain spaces and the filing system selected may be case sensitive we choose to use the lcline version when handling filenames.

New system commands and reading data from the SD Card

$FM - mount the SD Card
$F - list files and directories recursively
$F=<filename - open a file and start reading from it

The function pointer magic happens when a $F=<filename command is issued. If the file is valid the current function pointers for stream handling is saved away - we need them later to restore normal operation when the job is complete. The input stream is then "redirected" to our function sdcard_read by changing the pointer hal.stream.read to point to our function. The main protocol loop calls the function pointed to by hal.stream.read when reading data from the input stream, it has no idea about where the data is coming from (and it does not need to know either).

Every character read from a input stream passes through the function pointed to by hal.stream.enqueue_realtime_command, this strips out real-time commands and submits them to be handled by the core state-machine. When reading data from the SD Card we still want the real-time input from the original stream to be handled normally (allowing reset, feed-hold etc.) but any other input should be discarded. This is achieved by redirecting hal.stream.enqueue_realtime_command to our function drop_input_stream, this calls the original we saved away and returns true to the original caller - indicating all characters should be thrown away (after real-time commands has been picked off):

// Read a character from the SD Card file
static int16_t sdcard_read (void)
{
    int16_t c = -1;

    if(file.eol == 1)
        file.line++;

    if(file.handle) {

        if(sys.state == STATE_IDLE || (sys.state & (STATE_CYCLE|STATE_HOLD)))
            c = file_read();

        if(c == -1) { // EOF or error reading or grbl problem
            file_close();
            if(file.eol == 0) // Return newline if line was incorrectly terminated
                c = '\n';
        }

    } else if(sys.state == STATE_IDLE)
        sdcard_end_job();

    return c;
}

// Drop input from current stream except realtime commands
ISR_CODE bool drop_input_stream (char c)
{
    active_stream.enqueue_realtime_command(c);

    return true;
}

Error handling

All status codes from handling input in the main protocol loop is passed over the hal.report.status_message function pointer before beeing sent to the output stream. By redirecting this to our own function trap_status_report we can then check if any error occured when the last line read from the SD Card was processed:

void trap_status_report (status_code_t status_code)
{
    if(status_code != Status_OK) {
        char buf[50];
        sprintf(buf, "error:%d in SD file at line %d\r\n", (uint8_t)status_code, file.line);
        hal.stream.write(buf);
        sdcard_end_job();
    }
}

If an error occurs we simply report the error and terminate the job.

Real time reporting

To indicate to the user how the job is progressing we want to add a message to the standard real time report. This is done by setting the hal.driver_rt_report to point to a function that formats such a message. This function will be called every time a real-time report is generated by the core:

static void sdcard_report (stream_write_ptr stream_write)
{
    stream_write("|SD:");
    stream_write(ftoa((float)file.pos / (float)file.size * 100.0f, 1));
}

Finish streaming from the SD Card on a M30

Normally streaming from a file is complete when the file ends, however the NIST specification states that a M30 command should terminate the job and rewind the file. To comply with NIST we can redirect feedback messages to a function and check for the program end message and terminate the job when this is encountered:

void trap_feedback_message (message_code_t message_code)
{
    report_feedback_message(message_code);

    if(message_code == Message_ProgramEnd)
        sdcard_end_job();
}

We call the original report function so that the GCode sender is also made aware of any feedback.

Manual tool change

grblHAL has support for manual tool change (driver dependent), in order for this to work when streaming a job from the SD Card we must pause the streaming and allow normal input to take place until tool change is complete. Manual tool change support activated in the core by pointing hal.stream.suspend_read to a handler function, if NULL then M6 commands will result in an error beeing generated. Our handler looks like this:

static bool sdcard_suspend (bool suspend)
{
    if(suspend) {
        hal.stream.reset_read_buffer();
        hal.stream.read = active_stream.read;               // Restore normal stream input for tool change (jog etc)
        hal.stream.enqueue_realtime_command = active_stream.enqueue_realtime_command;
        hal.report.status_message = report_status_message;  // as well as normal status messages reporting
    } else {
        hal.stream.read = sdcard_read;                      // Resume reading from SD card
        hal.stream.enqueue_realtime_command = drop_input_stream;
        hal.report.status_message = trap_status_report;     // and redirect status messages back to us
    }

    return true;
}

Basically this restores the redirected function pointers until the tool change is complete, and then sets them back to continue processing input from the SD Card file.

Completing the job

When the job is complete the file is closed and all redirected function pointers are set back to their original values:

static void sdcard_end_job (void)
{
    file_close();
    memcpy(&hal.stream, &active_stream, sizeof(io_stream_t));   // Restore stream pointers
    hal.stream.reset_read_buffer();                             // and flush input buffer
    hal.driver_rt_report = NULL;
    hal.report.status_message = report_status_message;
    hal.report.feedback_message = report_feedback_message;
}

Reset during streaming

Reset during streaming is handled via code in the driver that is called on a reset. A reset message is issued if the SD Card was streaming while a job was active, the file is closed and the function pointers changed are restored to their original values:

void sdcard_reset (void)
{
    if(hal.stream.type == StreamSetting_SDCard) {
        char buf[70];
        sprintf(buf, "[MSG:Reset during streaming of SD file at line: %d]\r\n", file.line);
        hal.stream.write(buf);
        sdcard_end_job();
    }
}

Conclusion

Function pointers provides a clean and flexible way of extending functionality, this without a single change to the core. No maze of #define and corresponding #if or #ifdef #else #endif statements possibly resulting in both overhead and unreadable/hard to maintain spaghetti code.


Full source for the SD Card plugin here.