Open datdadev opened 4 months ago
Implement a custom communication protocol to set and get values between the HMI and the MCU.
Why? The current protocol cannot be used?
There are a number of example plugins that hooks into the core in order to provide custom functionality, take a look at them.
In main.c
:
#include "main.h"
#include "driver.h"
#include "grbl/grbllib.h"
#include "Modbus.h"
static void SystemClock_Config (void);
static void MX_GPIO_Init (void);
/* EDITED */
UART_HandleTypeDef huart2;
/* USER CODE BEGIN PV */
uint8_t RxData[256];
uint8_t TxData[256];
extern uint16_t Holding_Registers_Database[50];
extern Coils_Database[25];
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART2_UART_Init(void);
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart,uint16_t Size){
if (RxData[0]==SLAVE_ID){
switch (RxData[1]){
case 0x01:
readCoils ();
break;
case 0x03:
readHoldingRegs();
break;
case 0x04:
readHoldingRegs();
break;
case 0x05:
writeSingleCoil();
break;
case 0x06:
writeSingleReg();
break;
case 15:
writeMultiCoils();
break;
case 16:
writeHoldingRegs();
break;
default:
break;
}
}
HAL_UARTEx_ReceiveToIdle_IT(&huart2, RxData, 256);
}
int main (void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
HAL_UARTEx_ReceiveToIdle_IT(&huart2, RxData, 256);
if(!(CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk)) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
grbl_enter();
}
In Src/my_plugin.c
:
#include "driver.h"
static on_report_options_ptr on_report_options;
static on_execute_realtime_ptr on_execute_realtime;
// Define the register address and values
#define REGISTER_ADDRESS 0x3
#define START_COMMAND 0x05
// Define the base address and offset for the register
#define PERIPHERAL_BASE_ADDRESS 0x40000000 // FIX_ME
#define REGISTER_OFFSET 0x00000010 // FIX_ME
// Function to read register value
static uint8_t read_register(uint8_t reg_address) {
volatile uint8_t* register_address;
// Calculate the register address
register_address = (volatile uint8_t*)(PERIPHERAL_BASE_ADDRESS + REGISTER_OFFSET + reg_address);
// Read and return the value from the register
return *register_address;
}
// Function to send a sequence of G-code commands
static void send_gcode_sequence(void) {
hal.stream.write("G1 X10 Y10 F1000\n"); // Example G-code command
hal.stream.write("G2 X20 Y20 I10 J10\n"); // Another example G-code command
}
// Check the register value and send G-code commands if needed
static void check_register_and_execute_gcode(sys_state_t state) {
// Check the register value
uint8_t reg_value = read_register(REGISTER_ADDRESS);
if (reg_value == START_COMMAND) {
send_gcode_sequence();
}
// Continue with any other real-time execution tasks
on_execute_realtime(state);
}
// Add info about our plugin to the $I report.
static void on_report_my_options(bool newopt) {
if (on_report_options) {
on_report_options(newopt);
}
if (!newopt) {
hal.stream.write("[PLUGIN:Register Monitor v1.00]" ASCII_EOL);
}
}
void my_plugin_init(void) {
// Add info about our plugin to the $I report.
on_report_options = grbl.on_report_options;
grbl.on_report_options = on_report_my_options;
// Add check register and execute G-code function to grblHAL foreground process
on_execute_realtime = grbl.on_execute_realtime;
grbl.on_execute_realtime = check_register_and_execute_gcode;
}
I haven’t tested it yet, but it built successfully. I’m not sure if it’s correct. I am trying to implement a while loop to check the register for a signal to start drawing based on HMI commands that change the STM32 register value. This my_plugin_init
will also check the register stage, run endlessly alongside the GRBL loop. Am I right?
Better to move your code in main.c to my_plugin.c.
This my_plugin_init will also check the register stage, run endlessly alongside the GRBL loop. Am I right?
Not really, check_register_and_execute_gcode()
will.
FYI send_gcode_sequence()
will output data to a connected sender, not deliver it to input processing. To do that redirect hal.stream.read
to your own function that will read from a buffer containing the commands. The macros plugin does that.
I wonder if I can block the default UART2 connection of GRBL defined in my_machine_map.h
to prevent overlapping data. Can I create a new UART2 initialization in my_plugin.c
? Am I using normal initialization, or do I need a special function to initialize the GPIOs?
I have a question. I tried this and it didn’t work. What is the purpose of using SERIAL1_PORT
?
#define SERIAL_PORT 1 // 2 // GPIOA: TX = 2, RX = 3
#define SERIAL1_PORT 2 // GPIOA: TX = 2, RX = 3
When debugging the variable RxData
, using the Hercules app to send data through UART2, the value remained unchanged in the live expression. This indicates that the HAL_UARTEx_RxEventCallback
function is not working or UART2 is not initialized properly. Am I right?
This is my latest modified my_plugin.c
, and I can't figure out why the callback function is not being called when there is a signal on UART2:
#include "driver.h"
#include "Modbus.h"
#include <string.h>
#include <stdio.h>
static on_report_options_ptr on_report_options;
static on_execute_realtime_ptr on_execute_realtime;
// UART variables
UART_HandleTypeDef huart2;
uint8_t RxData[256];
uint8_t TxData[256];
extern uint16_t Holding_Registers_Database[50];
extern uint8_t Coils_Database[25];
// Buffer for storing G-code commands
#define GCODE_BUFFER_SIZE 256
static char gcode_buffer[GCODE_BUFFER_SIZE];
static uint16_t gcode_buffer_index = 0;
static uint16_t gcode_buffer_length = 0;
// G-code file strings
const char* gcode_file_1 =
"G21 ; Set units to millimeters\n"
"G90 ; Absolute positioning\n"
"G1 F1500 ; Set feed rate\n"
"G1 X10 Y10 ; Move to position\n"
"G1 X20 Y10 ; Draw line\n"
"G1 X20 Y20 ; Draw line\n"
"G1 X10 Y20 ; Draw line\n"
"G1 X10 Y10 ; Draw line\n"
"M2 ; End of program\n";
const char* gcode_file_2 =
"G21 ; Set units to millimeters\n"
"G90 ; Absolute positioning\n"
"G1 F1500 ; Set feed rate\n"
"G1 X10 Y10 ; Move to position\n"
"G2 X20 Y20 I10 J0 ; Draw clockwise arc\n"
"G2 X10 Y10 I-10 J0 ; Complete the circle\n"
"M2 ; End of program\n";
const char* gcode_file_3 =
"G21 ; Set units to millimeters\n"
"G90 ; Absolute positioning\n"
"G1 F1500 ; Set feed rate\n"
"G1 X10 Y10 ; Move to position\n"
"G1 X20 Y10 ; Draw line\n"
"G1 X10 Y20 ; Draw line\n"
"G1 X20 Y20 ; Draw line\n"
"G1 X10 Y30 ; Draw line\n"
"G1 X20 Y30 ; Draw line\n"
"M2 ; End of program\n";
// Function prototypes
static void send_gcode_sequence(int selected_index);
static int16_t get_macro_char(void);
void SystemClock_Config(void);
void MX_GPIO_Init(void);
void MX_USART2_UART_Init(void);
// UART callback function
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART2) {
if (RxData[0] == SLAVE_ID) {
switch (RxData[1]) {
case 0x01:
readCoils();
break;
case 0x03:
readHoldingRegs();
break;
case 0x04:
readHoldingRegs();
break;
case 0x05:
writeSingleCoil();
break;
case 0x06:
writeSingleReg();
break;
case 15:
writeMultiCoils();
break;
case 16:
writeHoldingRegs();
break;
default:
break;
}
}
HAL_UART_Receive_IT(&huart2, RxData, 256);
}
}
// Function to send a sequence of G-code commands to the buffer
static void send_gcode_sequence(int selected_index) {
const char* selected_gcode;
// Select the appropriate G-code file
switch (selected_index) {
case 0:
selected_gcode = gcode_file_1;
break;
case 1:
selected_gcode = gcode_file_2;
break;
case 2:
selected_gcode = gcode_file_3;
break;
default:
return; // Invalid index, do nothing
}
// Copy the selected G-code into the buffer
strncpy(gcode_buffer, selected_gcode, GCODE_BUFFER_SIZE - 1);
gcode_buffer[GCODE_BUFFER_SIZE - 1] = '\0'; // Ensure null-termination
gcode_buffer_length = strlen(gcode_buffer);
gcode_buffer_index = 0;
// Redirect hal.stream.read to our buffer reader function
hal.stream.read = get_macro_char;
hal.stream.file = NULL; // Input stream is not file based
}
// Function to read a character from the G-code buffer
static int16_t get_macro_char(void) {
if (gcode_buffer_index < gcode_buffer_length) {
return gcode_buffer[gcode_buffer_index++];
} else {
return 0; // No more data
}
}
// Check the Holding_Registers_Database and send G-code commands if needed
static void check_register_and_execute_gcode(sys_state_t state) {
// Get data from Holding_Registers_Database
uint16_t number_to_write = Holding_Registers_Database[10];
uint16_t text_size = Holding_Registers_Database[12];
// Check if the values are within the required range
if (number_to_write < 0 || number_to_write > 9 || text_size < 1 || text_size > 10) {
return; // Values out of range, do nothing
}
// Select which G-code file to use based on some criteria
int selected_index = 0; // Example: you might have a function to determine this
send_gcode_sequence(selected_index);
// Continue with any other real-time execution tasks
on_execute_realtime(state);
}
// Add info about our plugin to the $I report
static void on_report_my_options(bool newopt) {
if (on_report_options) {
on_report_options(newopt);
}
if (!newopt) {
hal.stream.write("[PLUGIN:Register Monitor v1.00]" ASCII_EOL);
}
}
void my_plugin_init(void) {
// Add info about our plugin to the $I report
on_report_options = grbl.on_report_options;
grbl.on_report_options = on_report_my_options;
// Initialize UART2
MX_USART2_UART_Init();
// Start UART receive interrupt
HAL_UART_Receive_IT(&huart2, RxData, 256);
// Add check register and execute G-code function to grblHAL foreground process
on_execute_realtime = grbl.on_execute_realtime;
grbl.on_execute_realtime = check_register_and_execute_gcode;
}
void MX_USART2_UART_Init(void)
{
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart2) != HAL_OK)
{
// Initialization Error
Error_Handler();
}
// Configure GPIOs for USART2
MX_GPIO_Init();
}
void MX_GPIO_Init(void)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
// Configure GPIO pins for USART2 TX and RX
GPIO_InitStruct.Pin = GPIO_PIN_2 | GPIO_PIN_3;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART2;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
//void Error_Handler(void)
//{
// // User can add their own implementation to report the HAL error return state
// while (1)
// {
// }
//}
To clarify my current task, I aim to use the HMI as the host via UART2, but not for sending GRBL-based commands or parameter configurations. Instead, the HMI will modify specific registers inside the MCU based on button presses on the touch screen. My plan is to reassign the SERIAL_PORT
in the map file to make space for UART2
, allowing the HMI to adjust register values directly.
My first step is to implement my_plugin.c
to read registers. Since I've disabled the UART SERIAL_PORT
in the map file, I need to reinitialize it to handle interrupts within the my_plugin_init()
function and create an interrupt callback to process data from the HMI. However, it seems this isn't working as expected, possibly due to the way GRBLHAL is structured.
If successful, I'll modify the Holding_Registers_Database
variable. For this project, Holding_Registers_Database[10]
represents the file number to select, and Holding_Registers_Database[12]
is the scale factor for the G-code file. I will handle this function later.
I would greatly appreciate your guidance on this. Also, if you have time, please check out my forked GRBLHAL project here. Thank you!
get_macro_char()
should return -1 ( or SERIAL_NO_DATA
) when no data is available.
I am not familiar with the UART HAL, to me it seems the callback is not entered before the specified amount of data is received. Are your modbus messages 256 bytes long?
FYI you may use the grblHAL serial API instead of the ST UART HAL - see the client modbus code.
For my test project, I initially created the project in STM32CubeIDE with the selected STM32F407VET6 MCU. I copied these two Modbus.c and Modbus.h files for Modbus RTU communication, then initialized UART2 for interrupts. I used these files in main.c
without making any modifications or additions. It worked as expected; the interrupt callback executed when sending just one byte of data through Hercules.
For the grblHAL serial API, could you write me an example for doing this kind of task? The code you linked seems quite overwhelming for using another Modbus library for the spindle.
To clarify my current task, I aim to use the HMI as the host via UART2, but not for sending GRBL-based commands or parameter configurations. Instead, the HMI will modify specific registers inside the MCU based on button presses on the touch screen.
It worked as expected; the interrupt callback executed when sending just one byte of data through Hercules.
Odd, the callback is only called when all bytes has been received if I read the code correctly.
For the grblHAL serial API, could you write me an example for doing this kind of task?
You only need the parts that claims the stream. Poll for input calling stream.get_tx_buffer_count()
until a complete message has been received, then read it into the local rx buffer with stream.read()
before submitting it for processing. Use stream.write()
to send a message.
Note that if using the STM HAL you should typically not submit received messages for processing in the callback since this is called from an interrupt context. To avoid that post it for processing via task_add_immediate()
instead, this ensures it will be handled by the foreground process.
I’ve implemented a basic data transmission and reception system with a 256-byte buffer, as you suggested. The idea is that data received via UART should be stored sequentially starting from the beginning of RxData
. However, I'm experiencing issues where data is inconsistently received. When data is received, some bytes are occasionally lost, and the order of the bytes can become scrambled. Could you help me review this?
#include "driver.h"
#include <string.h>
#include <stdio.h>
static on_report_options_ptr on_report_options;
static on_execute_realtime_ptr on_execute_realtime;
#define RX_DATA_SIZE 256
static uint8_t RxData[RX_DATA_SIZE];
static uint16_t rx_index = 0;
// Function prototypes
static void poll_for_received_data(void);
// Function to poll for received data, store it in RxData, and write it back
static void poll_for_received_data(void) {
int16_t c;
// Read data from the stream
while ((c = hal.stream.read()) != -1) {
if (rx_index < RX_DATA_SIZE) {
RxData[rx_index++] = (uint8_t)c; // Store received character in RxData
} else {
rx_index = 0;
}
hal.stream.write((uint8_t)c); // Echo the received character
hal.stream.write(ASCII_EOL); // Write ASCII end-of-line character
}
}
// Check the Holding_Registers_Database and send G-code commands if needed
static void check_register_and_execute_gcode(sys_state_t state) {
// Poll for and process received data
poll_for_received_data();
// Continue with any other real-time execution tasks
on_execute_realtime(state);
}
// Add info about our plugin to the $I report
static void on_report_my_options(bool newopt) {
if (on_report_options) {
on_report_options(newopt);
}
if (!newopt) {
// Add ASCII_EOL after the plugin information string
const char plugin_info[] = "[PLUGIN:Register Monitor v1.00]" ASCII_EOL;
hal.stream.write((uint8_t*)plugin_info);
}
}
void my_plugin_init(void) {
// Add info about our plugin to the $I report
on_report_options = grbl.on_report_options;
grbl.on_report_options = on_report_my_options;
// Add check register and execute G-code function to grblHAL foreground process
on_execute_realtime = grbl.on_execute_realtime;
grbl.on_execute_realtime = check_register_and_execute_gcode;
}
You should wait until a complete message is received before reading:
static void poll_for_received_data(void) {
int16_t c;
// Read data from the stream
if(hal.stream.get_rx_buffer_count() == RX_DATA_SIZE) {
while ((c = hal.stream.read()) != -1) {
if (rx_index < RX_DATA_SIZE) {
RxData[rx_index++] = (uint8_t)c; // Store received character in RxData
} else {
rx_index = 0;
}
hal.stream.write((uint8_t)c); // Echo the received character
hal.stream.write(ASCII_EOL); // Write ASCII end-of-line character
}
}
}
If variable length messages then read enough to parse the length then wait for the rest.
Note that the default serial stream is also read by the main protocol loop, it will be random who gets the data. You need to read from another stream by claiming one like how it is done in the modbus_rtu code. Or if you know which stream instance to use you can claim it explicitly by calling stream_open_instance()
, this returns a pointer to the functions used to access it. An example can be found in this Trinamic UART driver code (do not disable rx though). Replace TRINAMIC_STREAM
with the stream instance - likely 1
since the default stream is UART based.
Looking at the modbus code I see that there is no CRC check of the received data, only range checks. If reception goes out of sync you should flush the input buffer before reading the next message - possibly after a short delay.
After trying for a while without success, I tested the method you suggested, and while it works, it still doesn't meet my needs for a custom receive interrupt. Please note that the project involves a single communication stream with the HMI.
To clarify, the simple program in main.c uses HAL_UARTEx_ReceiveToIdle_IT(&huart2, RxData, 256);
in idle line mode for data reception. This mode allows me to gather sufficient data but still requires further handling, which is more convenient for communication with the HMI. After receiving data in the interrupt callback, the functions for reading and writing coils and holding registers are defined in Modbus.c and Modbus.h, using extern UART_HandleTypeDef huart2;
for communication, primarily to send data to the HMI. These files are quite straightforward for communication purposes, so please take a look at them.
I want to implement this feature and am unsure if it's acceptable or possible to write it in my_plugin.c
. I really need your assistance with this.
it still doesn't meet my needs for a custom receive interrupt.
What are your needs that is not met?
I want to implement this feature and am unsure if it's acceptable or possible to write it in my_plugin.c
All your code can be placed in my_plugin.c (and by adding other files as needed), you can then easily upgrade to newer releases without applying modifications to the ones from the repo.
You mentioned that it was possible to place all the code from the three files I provided above, similar to what I attempted a week ago as referenced here. However, despite numerous attempts, I have not achieved the desired results. You also advised against using the ST HAL library for UART, but suggested that all my code could be placed in my_plugin.c
, which seems quite unusual to me.
I think the code I'm developing is receiving characters correctly, but not as hex values. I suspect that hal.stream.read()
is only reading ASCII characters.
hal.stream.read() returns int16, -1
if nothing was read else an 8-bit character (byte).
By default most control characters and top bit set characters are stripped on reception, some are sent to the real-time command handler. If they are needed a function that keeps them has to be registered with the stream, e.g. like this:
my_stream->set_enqueue_rt_handler(stream_buffer_all);
Oh, it is working well. However, when transmitting back those same received hex bytes using hal.stream.write()
, do I need to add a function to send the hex data instead of ASCII characters?
do I need to add a function to send the hex data instead of ASCII characters?
If you need each hex data (byte) to be encoded by sending multiple ASCII characters per byte then yes.
FYI the stream is 8-bit and an 8-bit char
is typically represented in the same way as a byte (int8_t
or uint8_t
) and thus interchangeable in code:
typedef signed char __int8_t ;
typedef unsigned char __uint8_t ;
I am not converting those hex values to ASCII before sending; I want to send the raw hex values back to the host (in this case, the HMI is the master and the microcontroller is the slave).
This is the code that processes RxData
and prepares TxData
for sending. However, I am encountering a problem where TxData
contains the data shown in Image 1. When I request to send out the data, only the beginning of TxData
is transmitted, while the 0x00
bytes in between and the last bytes are ignored shown in Image 2:
void sendData (uint8_t *data, int size)
{
// we will calculate the CRC in this function itself
uint16_t crc = crc16(data, size);
data[size] = crc&0xFF; // CRC LOW
data[size+1] = (crc>>8)&0xFF; // CRC HIGH
// HAL_UART_Transmit(&huart2, data, size+2, 1000);
uint8_t *buff = (uint8_t *)malloc(size + 2);
if (buff == NULL) {
// Handle memory allocation failure
return;
}
// Copy data to buffer
memcpy(buff, data, size+2);
// Send the buffer
hal.stream.write(buff);
// Free the allocated buffer
free(buff);
}
hal.stream.write()
is for sending a null terminated string, use hal.stream.write_n() if the data contains null characters (0s).
Thank you, this works pretty well. I have one more question: How can I put a sequence of commands into the read buffer and then send them to the machine for processing? For example, I have a string of G-code like this:
const char* gcode_file_1 =
"G21 ; Set units to millimeters\n"
"G90 ; Absolute positioning\n"
"G1 F1500 ; Set feed rate\n"
"G1 X10 Y10 ; Move to position\n"
"G1 X20 Y10 ; Draw line\n"
"G1 X20 Y20 ; Draw line\n"
"G1 X10 Y20 ; Draw line\n"
"G1 X10 Y10 ; Draw line\n"
"M2 ; End of program\n";
My idea is to poll for data via idle line detection and then check whether to start or stop the G-code execution based on the status. However, I am unsure why the status_checker()
function is not being called. The intended approach is to first save the hal.stream.read
pointer, which reads from the serial port, then switch to send_gcode
for sending data. After the sending is complete, the hal.stream.read
pointer should be restored to the backup.
You have a while(1) { }
and while (send_gcode() != SERIAL_NO_DATA) {}
loops in poll_for_received_data()
which means it never returns - it should or the core will stall. FYI poll_for_received_data()
will be called repeatedly by the core at a high frequency.
A serious issue with your code is that you use the default stream for reading the modbus commands, it is better to claim another stream for that - or you will have to stall the main loop from reading from the input while waiting for data. It is possible but not advisable - if you stall the main loop while gcode is executing motion just stops, you will have to delay stalling it until the gcode has finished running to avoid that.
hal.stream.set_enqueue_rt_handler(stream_buffer_all);
should be called in the init function.
I recently switched to a new UART2 stream, as I changed the default to UART1 in my_machine_map.h
. This is because I will primarily communicate with the HMI using the UART2 stream with the non-used main stream of UART1. I also added an extra serial port for the HMI stream.
#define BOARD_NAME "MY MACHINE"
//#define HAS_BOARD_INIT
#define SERIAL_PORT 1 // GPIOA: TX = 2, RX = 3
#define SERIAL1_PORT 2 // GPIOA: TX = 2, RX = 3
// ...
And this is my source code for my_plugin.c
. Am I doing it right by initializing the stream like this and restoring the stream after the G-code execution is finished?
I’m not sure if the state machine I’ve planned out is correct.
I would get rid of the while
loops in poll_for_received_data()
- since these blocks the foreground process, poll_for_received_data()
is aready part of the outer loop in the core. Read data from the modbus stream until a complete message is received and process it - return a modbus error to the client if gcode is already running, confirm if not
I’m not sure if the state machine I’ve planned out is correct.
If it works then you are good.
flush_input_buffer(void)
There is a stream function available for this, call modbus_stream.reset_read_buffer()
.
I am currently working on integrating an HMI (Human-Machine Interface) to set and get values through the MCU's custom register map. Specifically, I am looking to achieve the following:
Given that the G-code files are limited in number, I have opted to store them directly on the MCU. My goal is for the MCU to manage all the G-code parsing and execution internally once a file is selected via the HMI.
Summary of Requirements:
Questions:
main
function to handle this custom communication with the HMI?Any guidance or solutions on how to implement this would be greatly appreciated. Thank you!