Closed andreamerello closed 4 years ago
Available PWM outputs:
TIM1 (ch1) - 16 bit counter
TIM2 (ch1) - 32 bit counter
TIM3 (ch1) - 16 bit counter
TIM12 (ch1, ch2) - 16 bit counter - But CH2 conflicts with SPI!
TIM5 (ch4) - 16 bit counter
TIM12 is the only timer for which more than one channel is available. No complementary PWM outputs are available on remaining channels.
[2]
CubeMX settings from the Discovery board demo project are almost OK
Global settings:
Prescaler: depending by incoming clock and desired PWM frequency (coarse)
TIMx_ARR (cubemx counter period): fine tune desired PWM frequency, keep high to maximize PWM resolution
Repetition count: set to zero. It serve for generating less update events when PWM is running. In our case we don't explicitly need the update event. The update event is used internally to trigger preload (see channel configuration).
"To make the timer channel to output repetitive pulses with the same t_on and t_off parameters and without the timer generating an “update event” on each PWM cycle, the timer repetition counter should be used." [1]
"As soon as an “update event” is generated within the timer peripheral, the content of the preload register instance is transferred to the active register instance immediately." [1]
Preload(cubemx auto-reload preload): Disabled - This affects TIMx_ARR, no channels CCR regs,
Channel settings:
PWM Mode: TODO
CH polarity, CH idle state: depending by application
Output compare preload: Enabled - this refers to the channel (i.e. TIMx_CCMR1 reg, OC1PE).
"The preload feature can be of big interest when outputting a PWM signal on a certain timer channel. [..] writing directly to the channel register in the middle of a PWM period may generate spurious waveforms. To overcome this problem, the preload feature should be enabled for the concerned timer channel register." [1]
Pulse: Disabled (if enabled PWM Disables after 1st cycle)
Fast mode: N.U. Relevant only for input trigger.
Trigger output and dead time settings: all disabled
TIMx_CCR1 reg regulates PWM duty i.e. it contains the duration of the ON period
TODO: HAL API
The Following code in an extract from CubeMX TIM1 configuration
htim1.Instance = TIM1;
htim1.Init.Prescaler = 0;
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
htim1.Init.Period = 60000;
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim1.Init.RepetitionCounter = 0;
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_Base_Init(&htim1) != HAL_OK)
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim1, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_PWM_Init(&htim1) != HAL_OK)
{
Error_Handler();
}
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 0;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
{
Error_Handler();
}
HAL_TIM_MspPostInit(&htim1);
}
[1] ST AN4776
[2] ST UM1907
Each driver is written in C (no OOP).
There are two class of I2C drivers:
Each I2C slave driver is intended to drive a single (or a family of) IC model, and it is agnostic wrt outside world. Each driver can be instantiated multiple times (i.e. we have more than one instance of the same sensor).
We'll also have board-specific SW module that handles the I2C subsystem at a higher level; it contains glue code that knows which I2C slave drivers do we have and where are they attached to. It initializes and connects things together. There is a single instance for the I2C board SW module.
Each driver will expose an initialization function, i.e.
int mcp23017_init(mcp23017_handle_t *h, int (*i2c_xfer)(struct i2c_xfer_list_t *xfers), int i2c_addr)
The i2c_xfer
parameter is a function pointer to a function that performs actual I2C xfer. It can perform a single transfer or a list of xfers (i.e. using repeated starts). The i2c_addr
is the I2C address of the I2C slave, excluding the R/W bit.
The h
parameter is filled by this function but it's allocated by the caller; it has to be considered as opaque by the caller (except for debugging purposes) and it has to be passed to all other driver functions, which are custom depending by the slave IC being driven, i.e.
mcp23017_pin_set_direction(mcp23017_handle_t *h, .....)
This data is typically a private driver struct; I2C slave drivers keep their state here. This allows for having multiple instances of I2C slave drivers.
The I2C board SW module can statically (globally) allocate this data on its heap. This avoid usage of dynamic memory while still allowing the flexibility to compose the slave I2C drivers as per the board design requirements without limitations. Since the I2C board SW module is a singleton by design, this does not cause any issue.
Example: An I2C bus with three slave devices
-------------
| |
| mcp23017 |
------- |------- | |
| I2C1 | -------------
MCU |-------| -------------
| | | |
------- |------- | xyz123 |
| | |
| -------------
| -------------
| | |
|------- | xyz123 |
| |
-------------
The board I2C SW module would look like this (we omit checks for return values for simplicity here)
#include <i2c_hal.h>
#include "mcp23017.h"
#include "zyx123.h"
mcp23017_handle_t gpio_exp_handle;
xyz123_handle_t inlet_p_sensor_handle, outlet_p_sensor_handle;
int i2c1_xfer(struct i2c_xfer_list_t *xfers, int i2c_addr)
{
/*
* Calls I2C host HAL functions
*
* i.e hal_i2c1_xfer(...);
*/
}
int i2c_subsystem_init(void)
{
/*
* Initialize host controller driver here with proper e.g. bus speed
*
* i.e. hal_i2c1_init(I2C_SPEED_400);
*/
mcp23017_init(&gpio_exp_handle, i2c1_xfer, 0x51);
mcp23017_pin_set_direction(&gpio_exp_handle, .....);
xyz123_init(&inlet_p_sensor_handle, i2c1_xfer, 0x54);
xyz123_init(&outlet_p_sensor_handle, i2c1_xfer, 0x55);
}
The I2C board software module will also wraps I2C slave driver APIs to high level APIs for application, i.e. a sync API for getting measures could look like this:
int sensors_read_pressure_sync(int &inlet_pres_mbar, int &outlet_pres_mbar)
{
int ret;
ret = mcp23017_set_pin_state(P_SENSORS_ENABLE_PIN, 1);
if (ret)
return ret;
msleep(10);
ret = xyz123_read_pressure(&inlet_p_sensor_handle, inlet_pres_mbar);
if (ret)
return ret;
ret = xyz123_read_pressure(&outlet_p_sensor_handle, outlet_pres_mbar);
if (ret)
return ret;
return mcp23017_set_pin_state(P_SENSORS_ENABLE_PIN, 0);
}
Now let's suppose that we have an I2C mux driver.. In the following example we have one I2C bus connected to a I2C gpio expander and a I2C mux. On each MUX slave channel we have a "xyz123" sensor attached.
-------------
| |
| mcp23017 | -------------
------- |------- | | | |
| I2C1 | ------------- | xyz123 |
MCU |-------| ------------- I2C1_1 /-----| |
| | | |---------/ -------------
------- |------- | tca9548 |
| |---------\ -------------
------------- I2C1_2 \---- | |
| xyz123 |
| |
-------------
the board I2C SW module would look as follow
#include <i2c_hal.h>
#include "mcp23017.h"
#include "xyz123.h"
#include "tca9548.h"
tca9548_handle_t i2c1_mux_handle
mcp23017_handle_t gpio_exp_handle;
xyz123_handle_t inlet_p_sensor_handle, outlet_p_sensor_handle;
int i2c1_xfer(struct i2c_xfer_list_t *xfers, int i2c_addr)
{
/*
* Calls I2C host HAL functions
*
* i.e hal_i2c1_xfer(...);
*/
}
int i2c1_1_xfer(struct i2c_xfer_list_t *xfers, int i2c_addr)
{
tca9548_select(&i2c1_mux_handle, 1);
i2c1_xfer(xfers, addr);
}
int i2c1_2_xfer(struct i2c_xfer_list_t *xfers, int i2c_addr)
{
tca9548_select(&i2c1_mux_handle, 2);
i2c1_xfer(xfers, addr);
}
int i2c_subsystem_init(void)
{
/*
* Initialize host controller driver here with proper e.g. bus speed
*
* i.e. hal_i2c1_init(I2C_SPEED_400);
*/
mcp23017_init(&gpio_exp_handle, i2c1_xfer, 0x81);
mcp23017_pin_set_direction(&gpio_exp_handle, .....);
tca9548_init(&i2c1_mux_handle, i2c1_xfer, 0x44);
xyz123_init(&inlet_p_sensor_handle, i2c1_1_xfer, 0x55);
xyz123_init(&outlet_p_sensor_handle, i2c1_2_xfer, 0x55);
}
NOTE: the I2C mux driver should be smart enough to avoid redundant switches. i.e. it keeps record of the current state and avoid to trigger a switch if it's already in the requested state.
NOTE: the glue code that wraps to the high-level API would be unchanged; I2C topology affects only a small code portion (initialization function and auxiliary I2C-accessors functions in the board-level I2C SW module).
The device firmware can be logically divided in three logical blocks:
All the firmware blocks run in a single context that can be an hardware timer callback or the system timer tick. The execution flow is sequential. The execution sequnce should be: 1 - read the sensor data 2 - run the control logic 3 - update the actuation devices (valves) 4 - update the output device (LCD)
The visualization of the device state on the output interface can be ran at a lower frequency if necessary.
The board's sensors and actuation devices are accessed using the board driver interface. It defines:
Let's suppose that the device integrates two pressure sensors, two flow sensors and one oxigen concentration sensor. The input structure would be:
typedef struct
{
uint16_t pressure1;
uint16_t pressure2;
uint16_t flow1;
uint16_t flow2;
uint16_t o2;
} board_sensor_data_t;
Using two actuated valves as control actuation devices the output data structure would be something like:
typedef struct
{
uint16_t valve1;
uint16_t valve2;
} board_actuation_data_t;
At device startup time the firmware must call the board driver's initialization function. It will setup the hardware resources and initialize the internal data structures.
int board_init(void);
The initialization function returns a code that indicates possible board fauilures. The higher level firmware is responsible to periodically call the sensor reading function and the actuator update function.
int board_read_sensors(board_sensor_data_t* in_data);
int board_apply_actuation(board_actuation_data_t* out_data);
The periodic tick function should be:
int periodic_tick(void)
{
board_sensor_data_t sensor_data;
board_actuation_data_t actuation_data;
int ret_code;
// Read the board sensors
ret_code = board_read_sensors(&sensor_data);
if(ret_code != 0)
{
TRIGGER_DEVICE_FAULT(ret_code);
return ret_code;
}
// Run the control logic
ret_code = control_run(&sensor_data, &actuation_data);
if(ret_code != 0)
{
TRIGGER_DEVICE_FAULT(ret_code);
return ret_code;
}
// Set the actuation devices
ret_code = board_apply_actuation(&actuation_data);
if(ret_code != 0)
{
TRIGGER_DEVICE_FAULT(ret_code);
return ret_code;
}
// Update the user interface output device
ret_code = ui_update(&sensor_data, &actuation_data);
if(ret_code != 0)
{
TRIGGER_DEVICE_FAULT(ret_code);
return ret_code;
}
return 0;
}
Control module and user interface module could/should have their own data structures instead of using the board drivers's ones. Names and types of the structure members will be better defined once that the specific board layout and the sensor will be fixed.
We suggest this design architecture because it offers some important plus.
thanks @andreamerello
cc: @marcoaccame
I just pushed in the code branch of the repository a first section of peripheral driver code. I put it in a folder called driver. Maybe you like it in another place, just move it wherever you want :).
Thanks @mircodisalvo for this addition 👍
I would thus mention @marcoaccame who's taking care of the branch code
.
hi all, sorry if i could not contribute to the issue before.
@mircodisalvo: that's ok where you pushed your code. we shall have time next week to arrange better the folder organization if needed.
@andreamerello: good to see some stm code which manages pwm. for i2c consider using non-blocking mechanisms, maybe w/ dma and user-defined callbacks executed by the dma handler at the end of transfer. we have some working code using stm32 hal which follows this mechanism. if you like we can go through it.
and surely we need to explore how to interface the drivers to the application framework. we can do next week if it is ok for you.
bye for now.
Hi,
@marcoaccame : I and @andreamerello discussed in the past days about possible implementation of the firmware, expecially on the driver side. We are proposing the synchronous/sequential model because in our scenario we think we probably don't have those characteristics that are improved by an asynchronous architecture. I'll try to explain my point of view.
We can indeed read the I2C sensors using an interrupt based procedure, with or without the DMA. The main goal of this would be that the CPU would be free of executing something else unless the sensor readings are completed. Anyway in our case we have to: 1 - read sensors 2 - run the controller 3 - apply the actuation 4 - update user interface (UI) 5 - sleep until next tick Even running the sensor reading operation in background we can't execute the second task because running the controller absolutely needs the sensor's output. We must wait for the step one to complete.
The only advantage I can see is that we could change the process execution to: 1 - run the controller 2 - apply the actuation 3 - read sensors 4 - update user interface (UI) 5 - sleep until next tick In this case we could run in a pseudo-parallel manner the sensor reading and the UI update tasks paying the drawback of handling a one-step old measures inside the controller. The measure delay could be unacceptable from the control point of view or, at least, are unacceptable unless they're proven to be good enough.
In addition unless there's enough idle time (operation 5 of the execution) there's no real need to try to parallelize the operations. Any code executed in IRQ context would introduce some kind of jitter or uncertainty about the execution order that must be validated. That would not be easy at all.
We have to test if we can speed up the operation on the I2C bus with the use of DMA batching different transactions into a single one. We can have some improvement parallelizing the reading of different I2C bus but we still don't know if we have more than one available. In both this cases the control function execution should wait for the sensor data to be available in any case.
I hope to have been clear, let me know what do you think about this issue. Mirco
Hi all, I think we should discuss in a proper meeting. what about this afternoon?
@marcoaccame Today I'm in lab in Erzelli and I have no microphone for videoconf here. Can we do that tomorrow moring? I'll work from home and I have proper setup there..
the meeting is required because i would like to realign us all with the state of the application skeleton currently tested on the evaluation board.
and in particular i wold like to:
i think that only after a description of the existing framework it will be clearer the complete picture so that we all can progress in the development of the parts.
@marcoaccame Today I'm in lab in Erzelli and I have no microphone for videoconf here. Can we do that tomorrow moring? I'll work from home and I have proper setup there..
ok @andreamerello, it is fine by me. in the meantime pls have a look at the application skeleton described in https://github.com/icub-tech-iit/ventilator/tree/code/tests/boards/stm32f7disco/demo003/instructions. In NOTE.1 you also find the comment from ugo which i talked about in previous post. I think that i may implement and test it as well.
I'm free for meeting up today, don't know about availability tomorrow though. At any rate, go ahead and don't get constrained by me.
To summarize my suggestion, I would do the following:
read sensors
hooked on real-time tick at 100 Hz.run the controller
hooked on the DMA interrupt (that is when stage 1 is done).apply the actuation
hooked on the same DMA interrupt as function call right after stage 2.Other operations like handling of UI can be carried out with lower priority/rate, for example at 25/30 Hz.
We've pushed a new version of the driver package. We have moved to the async API, as discussed.
Other than the pressure and flowmeter drivers, we have added support for I2C mux and I2C gpio-expander and the support for PWM generation.
We're certainly done with this, you guys having done a great job 👍 Further refinements and bug fixes can be conveniently made in dedicated issues.
In this issue we are going to publishing some misc notes we wrote in EDL regarding firmware design and implementation proposal. They include architectural aspects and notes about HW configuration.
They are still draft, but we would like to start sharing something; they are about the low level part (sensor reading/pwm generation).