FlatSat & I2C - Willzyax/FlatSat-SAMV71-I2C GitHub Wiki
This wiki page describes how to set up I2C communication between the FlatSat and an I2C enabled device. In the example described hereunder the device is a floating gate dosimeter, which is a type of radiation sensor developed by Sealicon. However, to date, these sensors only can communicate via SPI and the connection is actually achieved via an Arduino board that converts the I2C signals to SPI and vice versa. This fact is further ignored and the discussion continues as if the sensor is connected directly to the FlatSat. The I2C commands used in the code are part of the Advanced Software Framework (ASF) version 3, developed by Microchip Technology, previously Atmel. Note that Microchip uses its own Two-Wired Interface protocol (TWI), which is basically I2C. The ASF library files, with driver initialisations, clock speed definitions, etc, can be found in the lib/start/src folder. For more overall details the reader can consult the following links:
Inter-Integrated Communication, or I2C, is a serial communication protocol used to exchange information between electronic devices. On the communication network multiple master and slave devices can be present. The communication is synchronous, meaning that the data is transferred in sync with a clock signal generated by a master device. Each slave on the network has its own address used for identification. The protocol uses two lines: a data line (SDA) and a clock line (SCL). For more details with regards to the protocol, for example on the bitwise construction of messages, the reader is referred to more specialised literature, online guides, etc.
For the physical connection, the SDA and SCL lines of the sensor have to be connected to the SDA and SCL pins of the On-Board-Computer (OBC). Do not forget that the ground of the sensor and the FlatSat network have to be connected as well to ensure equal reference voltage.
The I2C drivers are already integrated in the FlatSat software and do not have to be included manually. When the user wants to develop the I2C on for example an Atmel development board, the easiest way is to include the libraries via the online Atmel Start tool. More details on setting up a development board are given in a separate section. The reader can find the bus driver setup functions in the driver_init file. In the examples folder, some basic functions are given. These are further explained hereunder.
The setup of the I2C bus can be done in the definition of the FreeRTOS task function. Reading and writing via I2C can then be defined in the infinite task loop depending on the user requirements.
struct io_descriptor *I2C_1_io;
i2c_m_sync_get_io_descriptor(&I2C_1, &I2C_1_io);
i2c_m_sync_enable(&I2C_1);
i2c_m_sync_set_slaveaddr(&I2C_1, 0x12, I2C_M_SEVEN);
io_write(I2C_1_io, (uint8_t *)"Hello World!", 12);
First, an IO descriptor is defined that is used to store the I2C attributes. This descriptor is then linked to the I2C structure already declared in the ASF library files. The I2C drivers and pins are defined in the driver_init and atmel_start_pins files in the ASF library folder. The reader might notice that there is also an I2C0 driver available, but these pins are not connected to the header on the FlatSat. Also note that the I2C drivers are automatically initialised in the driver_init file, where the necessary pins and I2C protocol clock speed are set. This clock speed might be important if, for example, the sensor's speed is limited. Settings of the I2C/TWI interface can be found in the hpl_twihs_config
file, which can be found in the obc\lib\start\src\config
folder. It can be used to enable SMBus (a derivative of I2C), set the baudrate, set clock stretching settings, etc. The I2C clock signal frequency itself is can be found in the peripheral_clk_config
file in the same folder. It is important to note that these config files are generated and do not necessarily allow for changing settings. For example, changing the the I2C clock macro in this file will change subsequent macro's where this value is used, but it will NOT change the actual clock frequency itself. For this you would have to go to the hpl_pmc_config
file in the same folder, where clock sources and prescalers can be set. These macro's are then used to setup registers in the hpl_pmc
files in the obc\lib\start\src\hpl\pmc
folder. Changing master clock frequencies also changes sleep and other functionalities, so changing these settings is not advised. Additionally, when using a development board, this process can be avoided by directly setting the correct clocks in Atmel Start, see the SAMV71 wiki.
Next, the communication is enabled and the slave address is set. This address should be defined in the sensor datasheet. Depending on the slave device, a 7 bit or 10 bit address can be used. Finally, an example is given on how a message can be send to the slave device. The variable to send should be a pointer and the total length in bytes of the message has to be included as well. To retrieve information over I2C from the sensor, the ``ìo_read()``` function can be used, which uses the same arguments, but instead of a pointer of the data to send a pointer to where the information has to be stored has to be provided.
The read and write functions described above call the i2c_m_sync_cmd_read
and i2c_m_sync_cmd_write
functions by giving the I2C descriptor as an parameter. These functions are designed to access specific registers in the slave device by first sending the slave address, then register address and finally sending or receiving information. For more flexibility with regards to what information to send and when to stop, the i2c_m_sync_transfer
function can be used.
You can check if the I2C on the FlatSat is working by using an oscilloscope connected to the pins. Note that your message will not be send, just the address of the slave whilst the master is trying to connect with it. After checking the SCL and the data lines SCA, you can reconstruct messages by noting that every time the clock goes up a bit is read. The first 7 bits should be the address and should be followed by 1, which signals no acknowledgement (ACK bit). When the master is asking info it keeps on sending acknowledged, as a last bit, to the slave indicating that new information is still being requested. This means that as soon as the master does not want to receive information anymore it sends 1, meaning not acknowledged, although it has actually received the information correctly. Instead of a oscilloscope a small logical logical analyser can be used, connected via USB to a PC running PulseView. This open-source program can directly decode your I2C messages for easy debugging.
An issue with I2C communication is that when communication is interrupted at the wrong moment, the master is left waiting for a response and the RTOS task of reading or writing data hangs. Several methods can be employed to try and mitigate this issue, a couple of which are discussed here.
The OBC software already includes a hardwired and a software based watchdog, which is implemented in the SpaceInventor library (si/src/platform/e70b/main.c) and initialised in the ASF driver_init file. You can manually trigger this watchdog for your specific task by including WDT_0_init();
in your code and regularly calling wdt_feed(&WDT_0);
in the task loop to reset the timer. This watchdog timer chooses a flash to reboot from depending on the boot_img
counter in the btldr.c. Basically, the OBC sets a counter for each flash partition. When it reboots it decrements the counter of the flash it has rebooted to. The software always picks the flash partition with the highest counter, thus effectively switching between the different flash partitions. However resetting the OBC seems like an overkill for this issue. A separate watchdog to only reset the I2C bus is an option but was not implemented.
The ASF library also includes an API for an I2C asynchronous driver. Using this driver, communication still happens synchronously but the task does not keep on waiting for the communication to be completed and the rest of the task continues to run whilst the I2C bus waits for a response. This does not, however, solve the complete issue, since the slave device might be waiting for a new start condition after interruption, which is not coming since the master is keeps on waiting for a response. Furthermore, this driver is not standard included in the OBC software and would have to be added separately.
TBD
The OBC has several physical types of memory available. To easily access this memory, an abstraction layer is added on top of this memory, called virtual memory. This means that the user seems to have access to one large memory, and the VMEM layer then takes care of the actual physical memory allocation. This is shown in the picture below, retrieved from the Space Inventor datasheets.
Data can be stored in the OBC VMEM by means of the following two functions:
vmem_download(12,100,VMEM_ADDRESS,ARRAY_SIZE,array_to_store);
vmem_upload(12,100,VMEM_ADDRESS,array_to_send,ARRAY_SIZE);
The first parameter is the node number, in this case the OBC, followed by a timeout variable, the memory address to use and finally the array size to send or the variable in which to store downloaded data. Available memory addresses can be determined via Satctl. Note that the array variables are, because of C syntax, actually pointers to the first element in the array. To download from VMEM, the satctl command download
[timeout] can be used as well in the terminal. This command can for example store bytes as characters in a text file.In this example parameters are retrieved from the sensor in a loop. The main intention is to give the user an idea on how to implement I2C and the details of the sensor functionalities are not further discussed here, but can retrieved from the sensor' datasheet. Do not forget to adapt the hooks.c and the meson.build files accordingly to implement this task. The VMEM functionality is demonstrated as well. A flag can be used to allow the task to upload the data gathered to the virtual memory. After every write the memory address is incremented as to not overwrite previous values. After a user defined number of loops, all the data that was stored in VMEM is downloaded again just to check that the data was stored correctly. Note that no specific reconstruction of the bytes of the memory is performed. The print_arr_8
function is just a simple for loop that prints out byte values of an array.
/*
define new parameter for backing storage of the parameter value. Underscore refers to backing storage that can be
accessed by any functionality, the same name without the _ can then be used to refer to the variable in this specific
c file (see the parameter creation wiki page). Using this definition makes the parameter accessible over CSP
*/
int32_t _FGDOS_data[ARRAY_SIZE] = {0,0,0,0,0,0};
#define FGDOS_ARRAY_ID 2000
PARAM_DEFINE_STATIC_RAM(FGDOS_ARRAY_ID, FGDOS_data, PARAM_TYPE_INT16, ARRAY_SIZE, ELEMENT_SIZE, PM_CONF, NULL,"",&_FGDOS_data,);
/*
set up the FreeRTOS task and allocate to it a specific memory stack
*/
void init_FGDOS() {
static StaticTask_t FGDOS_tcb;
static StackType_t FGDOS_stack[500];
xTaskCreateStatic(FGDOS_task, "HELLO", 500, NULL, 1,FGDOS_stack, &FGDOS_tcb);
}
/*
main task to perform
*/
void FGDOS_task(void * pvParams) {
TickType_t xLastWakeTime = xTaskGetTickCount();
TickType_t task_interval = 4; // the task interval in seconds
/* local variables for sending and receiving I2C */
uint8_t I2C_receive_arr[BYTES_I2C], I2C_send_arr[10], CSP_message[CSP_MESSAGE_LENGTH];
uint8_t fgdos_sensor, fgdos_rech_count, fgdos_temp;
uint32_t fgdos_f_sens, fgdos_f_ref;
float fgdos_window_factor;
float *p_fgdos_window_factor; // pointer used to reconstruct float from received bytes
/* set up I2C connection */
struct io_descriptor *I2C_1_io;
i2c_m_sync_get_io_descriptor(&I2C_1, &I2C_1_io);
i2c_m_sync_enable(&I2C_1);
i2c_m_sync_set_slaveaddr(&I2C_1, SLAVE_ADDRESS, I2C_M_SEVEN);
/* define parameters for vmem operation */
uint32_t *vmem_up; // pointer to array for data to upload
uint8_t vmem_down_arr[60]; // variable for intermediate downloaded vmem storage
uint32_t vmem_down_reconstructed[ARRAY_SIZE];
uint32_t vmem_addr = VMEM_ADDRESS; // vmem destination address
bool vmem_flag = false;
uint8_t vmem_counter = 0;
/* watchdog setup */
WDT_0_init();
/*
* main task loop
*/
for(;;) {
printf("I2C and VMEM TEST RUN \t slave address: %x\n",I2C_1.slave_addr);
/*
* Reading data from the FGDOS:
* data_received = sensor, temperature, F_sensor, F_reference, recharge_count, window_factor
* data_received types = byte, byte, 3*byte, 3*byte, byte , float (4*byte) -> 13 bytes
*/
if(io_read(I2C_1_io, I2C_receive_arr,BYTES_I2C)==BYTES_I2C){
printf("DATA RECEIVED : ");
print_arr_8(I2C_receive_arr,13,1); // function to print byte arrays
printf("SENSOR TEMP F_Sens F_Ref RECHARGES WINDOW_F \n");
fgdos_sensor = I2C_receive_arr[0];
fgdos_temp = I2C_receive_arr[1];
fgdos_rech_count = I2C_receive_arr[8] & 0x0F; // only 4 last bits determine recharge count
p_fgdos_window_factor = &I2C_receive_arr[9];
fgdos_window_factor = *p_fgdos_window_factor;
// frequencies consist of 18 bits (hence 0x3FFFF)
fgdos_f_sens = (((unsigned long)(I2C_receive_arr[2] << 8 | I2C_receive_arr[3] ) <<8 | I2C_receive_arr[4] ) & 0x3FFFF ) * fgdos_window_factor;
fgdos_f_ref = (((unsigned long)(I2C_receive_arr[5] << 8 | I2C_receive_arr[6] ) <<8 | I2C_receive_arr[7] ) & 0x3FFFF ) * fgdos_window_factor;
printf("%d %d %ld %ld %d %f \n",fgdos_sensor,fgdos_temp,fgdos_f_sens,fgdos_f_ref,fgdos_rech_count,fgdos_window_factor);
// store into parameter array
// NOTE: you can change _FGDOS_array directly, but when a remote array is defined
// you have to push it afterwards when not using param_set
param_set(&FGDOS_data,0,&fgdos_sensor);
param_set(&FGDOS_data,1,&fgdos_temp);
param_set(&FGDOS_data,2,&fgdos_f_sens);
param_set(&FGDOS_data,3,&fgdos_f_ref);
param_set(&FGDOS_data,4,&fgdos_rech_count);
param_set(&FGDOS_data,5,&fgdos_window_factor);
vmem_flag = true;
} else{
printf("wrong numbers of I2C bytes transferred...\n");
}
wdt_feed(&WDT_0);
vTaskDelayUntil(&xLastWakeTime, task_interval*configTICK_RATE_HZ);
/*
* test to see if setting registers works by setting sensor threshold frequency
* I2C_send_arr = registry , data
*/
I2C_send_arr[0] = THRESHOLD;
I2C_send_arr[1] = round(50000/fgdos_window_factor/1023);
io_write(I2C_1_io,I2C_send_arr,2);
printf("--------------------- THRESHOLD AUGMENTED ---------------------\n");
wdt_feed(&WDT_0); // reset watchdog
vTaskDelayUntil(&xLastWakeTime, task_interval*configTICK_RATE_HZ); // or use pdMS_TO_TICKS()
printf("\n \n");
// VMEM Test functions
if (vmem_flag){
// upload the array to VMEM for storage
vmem_up = I2C_receive_arr;
vmem_upload(12,100,vmem_addr,vmem_up,BYTES_I2C); // 13 bytes, automatically prints as well
vmem_addr += BYTES_I2C;
if (vmem_addr >= VMVEM_ADDRESS_MAX){
printf("max vmem address reached, overwritting previous data...");
vmem_addr = VMEM_ADDRESS;
}
vmem_counter++;
vmem_flag = false;
}
/*
* test to see if VMEM is actually working buy downloading an array of bytes and printing out
* the satctl command download <node> <address> <length> <file> [timeout] can be used as well in the terminal
*/
if (vmem_counter==2){
// after x loops, download everything from VMEM just to check if everything was stored correctly
vTaskDelayUntil(&xLastWakeTime, task_interval * configTICK_RATE_HZ);
vmem_download(12,100,VMEM_ADDRESS,BYTES_I2C*vmem_counter,vmem_down_arr);
printf("\t DOWNLOADED the following array: ");
print_arr_8(vmem_down_arr,ARRAY_SIZE*ELEMENT_SIZE*(vmem_counter+1),1);
vmem_counter = 0;
vmem_addr = VMEM_ADDRESS+100; // get away from used memory so it is not overwritten
}
}
}
TBD