FreeRTOS_IO and Updating STM32CubeMXIDE

I’m going to put this here and hope that it is close enough in group.

I’m working on a successful (it’s working) project using STM32CubeMXIDE. What I want to implement are queue driven Interrupt enabled handlers for a Wroom32 (UART), NRF24L01 (SPI), and I2C (OLED displays).

I have working, interrupt driven drivers for SPI, generally didn’t need one for I2C, and have some routines for UARTs. I could write and patch together queues, IRQ driven I/O drivers, and so on. However, I read some descriptions of FreeRTOS_IO which seems to indicate that it will do what I need.

OK, download latest LTS zip from the .org site, then unzip, and construct something that at least uses some features.

Won’t compile. Cannot find FreeRTOS_IO.h as needed in the code snippets. It’s not in the download either. Documentation goes to FreeRTOS_plus_io… Those files for the _IO don’t seem to exist, so I think I’m likely doing something wrong.

At this point, I should point out the problem with CubeMX, and that’s an older version of FreeRTOS, and the inability to upgrade the existing version, and perhaps add on (or bolt on) PLUS code.

So I try to make a version of my project without FreeRTOS (easy), then bolt FreeRTOS on. Apparently REALLY needs App_FreeRTOS.c to define several functions. Picking the processor was a bit of a treat which was only solved by looking at an existing application that worked.

So if I’m going to go that way, I need to be able to update/upgrade the existing CubeMX version of FreeRTOS to make it work a bit better. That would be one thing. That seems to be plan B, but that’s not optimal, except to get it done and then not care about CubeMXIDE’s version of FreeRTOS.

I’m certainly looking for advice, here.
Thanks

Is this related to this post - Replacement for FreeRTOS-IO??

It is related since it addresses a similar topic.

Since there are two questions wrapped up here, I’ll go with the FreeRTOS_IO first.

Somewhere in the FreeRTOS_IO documentation there was mention of using queues gracefully with such things as SPI, USART, and I2C drivers. Naturally, once you start talking to the hardware directly, things become very device specific.

Taking I2C for instance, I was looking for a solution that offered the following:

  1. Normal (and standard) programmed I/O master/slave, blocking, read/write/restart read.
  2. Same but using interrupts
  3. using queues to supply the data, interrupts may be used to unload and stuff queues
  4. using DMA for large data block transfer.

The first test case would be a small OLED display.

  1. The normal blocking I/O is written and works
  2. Interrupt operation is working
  3. queues are useful, but not in all situations.
  4. DMA wants to bypass queues and deal with external blocks of data (screen refresh).

It seemed that FreeRTOS_IO would offer this.

I’ve looked at the data link in the other post that I had never seen. FreeRTOS_IO might help with #1, blocking, but apparently this version supports absolutely none of the other features I need.

I think I shall have to continue with my existing efforts, which need to be debugged, but cover feature cases 1-3, leaving out DMA. I suspect that my particular needs are a bit different, and I don’t know if you offer any solutions. I’m using an STMicro STM32L562VET processor on a custom designed board. The processor is an L5 core with 512K of FLASH, 256K of SRAM, and I have added an APS6404 32MBit memory (maps to 2Meg * 16 bits). I have a separate memory manager I use to reserve space in that memory, although FreeRTOS accesses it as a user heap. That same memory also contains virtual display memory as needed, which requires DMA from that memory to the actual physical display, so the memory management and transfer speed tend to important.

The second question was to see if there is a graceful way of manually updating the FreeRTOS version in CubeMXIDE. STMicro prefers Azure, which has a queue mechanism that is unsatisfactory for my needs. Hence, they update their packages slowly.

Thanks, hope this clarifies things

The aim of common IO is to provide a common interface which you implement for your hardware. This makes your application portable across hardware and more readable. You are right that it probably does not provide everything you are looking for.

Let us know any problem you face and we will be happy to help.

I do not think there is. You can clone FreeRTOS in a folder parallel to the CubeMX generated code to ensure that it is not over written every time you generate code using CubeMX.

As far as I can tell, when you removed the zero footprint mode, you took out at least one feature that I wanted.

At the level (I/O drivers for a particular chip type), that I need, I am not seeing how the existing code would be useful. It would be if I wanted this code to run on multiple platforms, but that is not the case.

I would be happy to use debugged FreeRTOS code if there were any that did what I wanted. I haven’t found that magic repository yet.

The need to update FreeRTOS somewhat vanished when the rather specific features I need became unavailable. It may pop back in with the next version of FreeRTOS, though.

I seem to be back at writing my own code.
Thanks for the help.

Given SPI, Serial, and I2C, I started working on I2C in the framework of the STM ecosystem.
With any interface, receive and transmit are options.
For I2C, I have blocking mode, DMA, and IRQ mode working. None of these need queues. Oddly enough, the implementation of I2C with queues seems to avoid working, having to do with the native I2C driver not working the same when individual calls to it are done (not working) to writing a block (working).
A solution to that for transmit would be to build a buffer in the driver, and write blocks of data (likely the size of a line buffer for the display). Since this has lots of memory needed, and since the calling program to the driver provides a buffer, I’m not seeing the advantage. Writing a queue at the application level, reading it into a buffer, then sending that buffer seems less efficient, so the queue mode for transmit is not useful. For receive, though, it should be. This will be more obvious when working with ESP series and WROOM-32 subsystems.

Then you are right - it probably is not suitable for your application.

This would only be useful if you wanted to serialize transmits from multiple tasks. Multiple tasks would write into the queue (which is thread safe) and then one task would read the from the queue and transmit.

In the STMicro world as provided by CubeMXIDE, there are a number of (rather awkwardly) written drivers for each subsystem. They have a crude form of semaphore built into them (which may not work). There’s generally a driver for blocking I/O, Interrupt driven I/O, and DMA I/O.
My drivers (as originally written), encapsulate this with FreeRTOS semaphores, Control line manipulation (CS, address, as needed), and are written in C++.
The rewrite is a bit more generalized, and allows setting up each interface for each of the three modes. There was an idea of putting queues in (and for receiving data, there will be a queue), but for transmission, for I2C Masters, their driver seems not to work in individual byte mode transmitting a block, but works well in transmitting a block in block mode.

The multiple tasks writing to a queue was one of the ideas, but using semaphores makes it thread safe anyway. It may not work with their drivers for I2C and SPI, but should work with USART.

Thanks

I’ll look, in summary, at three interfaces, SPI, I2C, USART.
Each has three native modes. Blocking, Interrupt, DMA.
There’s a fourth mode, Queues. That opens up a can of worms.

For the three interfaces:

  1. I2C: working in blocking, Interrupt, DMA. Transfer rate limited by bus speed. Queue, has implementation issues.
  2. SPI, working in blocking, Interrupt, DMA. Queues do not make sense with how SPI is used.
  3. USART, lots of problems here. Use is generally byte and not packets. Blocking mode works, but IRQ and DMA may not be useful because of the baud rates. Queues, in this case, do make sense, but mostly for console devices.

Not all modes make sense for all interfaces. Adding queues can vastly complicate the drivers.

Not all (ST MICRO) drivers support any integration with FreeRTOS. Thus, queues, when they make sense, do not always work (I do have a solution for USARTS using DMA access). Queues to not make sense in all applications, bus speed is one item.

Universal driver? Works for the three driver instance, but not in all modes. Queues can be integrated in some cases, but not all, simply because they don’t make sense.

A universal driver cannot make sense with only one model considered, there needs to be a different model for each interface.

Even then, using queues is somewhat difficult to integrate into a bare metal implementation because of timing issues, if not other problems.

Not all drivers are workable with FreeRTOS without some modification, a lot of things having to do with timing and interrupts come to mind.

There can be a specific example if needed.

Thanks for summarizing! I agree with your assessment and the assessment of @aggarg. A universal drive interface will only make sense if your underlying devices (to be driven) have similar configurations and behaviors. Given your needs across your interfaces are so different, it may make more sense to have a driver interface function for each interface type. This will allow you to maintain portability across MCUs at the cost of a few more functions.

The advantage I have here is that, with a bit of connivance, the initialization for each driver family is the same, or nearly so.

for I2C:

		uint32_t create (
				char which = 0,
				int IN_QUEUE_SIZE = 0,
				int OUT_QUEUE_SIZE = 0,
				I2C_TRANSFER_MODE IN_MODE = I2C_BLOCKING,
				I2C_TRANSFER_MODE OUT_MODE = I2C_BLOCKING,
				int Timeout = 100);

for SPI

		uint32_t create (
				char which = 0,									// interface
				char* label = nullptr,							// ID label
				int IN_QUEUE_SIZE = 0,							// if used
				int OUT_QUEUE_SIZE = 0, 						// if used
				SPI_TRANSFER_MODE IN_MODE = SPI_BLOCKING, 		// input mode
				SPI_TRANSFER_MODE OUT_MODE = SPI_BLOCKING,  	// output mode
				int Timeout = 100,								// timeout
				uint16_t CS_PIN = 0, 									// CS
				GPIO_TypeDef* CS_PORT = nullptr,
				uint16_t A0_PIN = 0,										// A0 (DISPLAYS)
				GPIO_TypeDef* A0_PORT = nullptr,
				uint16_t RESET_PIN = 0,									// RESET (DISPLAYS)
				GPIO_TypeDef* RESET_PORT = nullptr,
				uint16_t RW_PIN = 0,										// RARE DISPLAY
				GPIO_TypeDef* RW_PORT = nullptr,
				uint16_t READ_PIN = 0,									// RARE DISPLAY
				GPIO_TypeDef* READ_PORT = nullptr,
				uint16_t WRITE_PIN = 0,									// RARE DISPLAY
				GPIO_TypeDef* WRITE_PORT = nullptr

and for USARTS

		uint32_t create (
				char which = 0,									// interface
				char* label = nullptr,							// ID label
				int IN_QUEUE_SIZE = 0,							// if used
				int OUT_QUEUE_SIZE = 0, 						// if used
				USART_TRANSFER_MODE IN_MODE = USART_BLOCKING, 		// input mode
				USART_TRANSFER_MODE OUT_MODE = USART_BLOCKING,  	// output mode
				int Timeout = 100								// timeout
		);

Which are, for the most part, the same. The label is used to identify semaphores and queues, and can be added to the I2C should I wish.
Other differences have to do with the manner in which each driver is used, the architecture of the hardware, and the usefulness of various modes. Queues don’t do well on I2C and SPI, but do very well on USARTS. Interrupt modes (to a user provided buffer) make sense in I2C and SPI, but not in USARTS. Queues may be added to I2C (generally receive only) if I implement packets over I2C, but that’s only processor to processor.

In this I2C example, you can see the locations needed to support a different processor family:

uint32_t HAL_I2C::Send(uint16_t DevAddress, uint8_t* buffer, uint32_t Size, enum I2C_TRANSFER_MODE Override)
{

	enum I2C_TRANSFER_MODE		NEW_mode = OUT_mode;

	// ********************************** check input parameters***********************************
	// need to check overrides
	switch (Override)
	{
		case I2C_IRQ:			// TRANSMIT IS IRQ DRIVEN
		case I2C_DMA:			// TRANSMIT IS DMA FROM BUFFER, RECEIVE IS IRQ QUEUE (MAY CHANGE)
		case I2C_BLOCKING:		// TRANSMIT AND RECEIVE ARE BYTE BLOCKING
		{
			NEW_mode = Override;
			break;
		}
		default:
		{
			NEW_mode = OUT_mode;
			// leave mode as is, cannot override
		}
	}

	// must shift left before calling master transmit
	DevAddress <<= 1;					// address shifted

	switch (NEW_mode)
	{
		case I2C_BLOCKING:		// TRANSMIT AND RECEIVE ARE BYTE BLOCKING
		{
			time_start = htim2.Instance->CNT;
			// always semaphore protected
			xSemaphoreTake(semaphore, portMAX_DELAY);
			// direct call to STMICRO driver, blocking mode
			if (DevAddress == 0)
			{
				result = HAL_I2C_Slave_Transmit(hi2c, (uint8_t*) buffer, Size, timeout);
			}
			else
			{
				result = HAL_I2C_Master_Transmit(hi2c,DevAddress, (uint8_t*) buffer,Size, timeout);
			}
			// error reporting
			ERROR_CODE = hi2c->ErrorCode;
			ERROR_REASON = (enum COMM_ERROR_TYPE) ERROR_CODE;
			operations++;
			// record errors if needed
			switch (result)
			{
				case HAL_ERROR:    					//= 0x01U,
				{
					fail++;
					break;
				}
				case HAL_BUSY:     					//= 0x02U,
				{
					fail++;
					break;
				}
				case HAL_TIMEOUT:  					//= 0x03U
				{
					fail++;
					break;
				}
				case HAL_OK:
				{
					// was OK
					succeed++;
					break;
				}
			}

			// return semaphore for next operation
			xSemaphoreGive(semaphore);
			time_stop = htim2.Instance->CNT;
			delta_time = time_stop - time_start;
			break;
		}
		case I2C_QUEUE:			// TRANSMIT QUEUE SENDS
		{
			uint16_t			buf;

			// always semaphore protected
			xSemaphoreTake(semaphore, portMAX_DELAY);
			for (uint32_t i = 0; i < Size; i++)
			{
				// build address = MSB, data = LSB
				buf = (*buffer & 0xFF) + ((DevAddress & 0xFF) << 8);
				// send data
				xQueueSend(send_queue, &buf, portMAX_DELAY);
				buffer++;
			}
			operations++;
			// return semaphore for next operation
			xSemaphoreGive(semaphore);
			break;
		}
		case I2C_IRQ:			//
		{
			time_start = htim2.Instance->CNT;
			xSemaphoreTake(I2C_done[interface], portMAX_DELAY);

			// always semaphore protected
			xSemaphoreTake(semaphore, portMAX_DELAY);

			// direct call to STMICRO driver, blocking mode
			if (DevAddress == 0)
			{
				result = HAL_I2C_Slave_Transmit_IT(hi2c, (uint8_t*) buffer, Size);
			}
			else
			{
				result = HAL_I2C_Master_Transmit_IT(hi2c,DevAddress, (uint8_t*) buffer,Size);
			}
			// error reporting
			ERROR_CODE = hi2c->ErrorCode;
			ERROR_REASON = (enum COMM_ERROR_TYPE) ERROR_CODE;

			xSemaphoreTake(I2C_done[interface], portMAX_DELAY);

			operations++;
			hal_state = HAL_I2C_GetState(hi2c);
			switch (hal_state)
			{
			////				case HAL_I2C_STATE_TIMEOUT:
			////				{
			////					break;
			////				}
				case HAL_I2C_STATE_BUSY_TX:
				case HAL_I2C_STATE_BUSY_RX:
				case HAL_I2C_STATE_BUSY:
				{
					fail++;
					break;
				}
				case HAL_I2C_STATE_READY:
				{
					// was OK
					succeed++;
					break;
				}
				default:
				{
					// do nothing
				}
			}
			// return semaphore for next operation
			xSemaphoreGive(semaphore);
			xSemaphoreGive(I2C_done[interface]);
			time_stop = htim2.Instance->CNT;
			delta_time = time_stop - time_start;
			break;
		}
		case I2C_DMA:			// TRANSMIT IS DMA FROM BUFFER, RECEIVE IS IRQ QUEUE (MAY CHANGE)
		{
			time_start = htim2.Instance->CNT;
			xSemaphoreTake(I2C_done[interface], portMAX_DELAY);		// if can take, then no active DMA

			// always semaphore protected
			xSemaphoreTake(semaphore, portMAX_DELAY);

			// direct call to STMICRO driver, blocking mode
			if (DevAddress == 0)
			{
				result = HAL_I2C_Slave_Transmit_DMA(hi2c, (uint8_t*) buffer, Size);
			}
			else
			{
				result = HAL_I2C_Master_Transmit_DMA(hi2c,DevAddress, (uint8_t*) buffer,Size);
			}
			// error reporting
			ERROR_CODE = hi2c->ErrorCode;
			ERROR_REASON = (enum COMM_ERROR_TYPE) ERROR_CODE;

			xSemaphoreTake(I2C_done[interface], portMAX_DELAY);

			operations++;
			hal_state = HAL_I2C_GetState(hi2c);
			switch (hal_state)
			{
			////				case HAL_I2C_STATE_TIMEOUT:
			////				{
			////					break;
			////				}
				case HAL_I2C_STATE_BUSY_TX:
				case HAL_I2C_STATE_BUSY_RX:
				case HAL_I2C_STATE_BUSY:
				{
					fail++;
					break;
				}
				case HAL_I2C_STATE_READY:
				{
					// was OK
					succeed++;
					break;
				}
				default:
				{
					// do nothing
				}
			}
			// return semaphore for next operation
			xSemaphoreGive(semaphore);
			xSemaphoreGive(I2C_done[interface]);
			time_stop = htim2.Instance->CNT;
			delta_time = time_stop - time_start;
		break;
		}
		default:
		{
			// do nothing
		}
	} 							// end out mode switch
	return HAL_OK;
}

The structure is, to me, the more important feature.