## [STM32 I2C] - Study of I2C slave task
**Github:** https://github.com/CarterWu-M/STM32_I2C_MASTER
**Board:** NUCLEO-F072RB
**IDE:** STM32CubeIDE

---
### Introduction
---
This article explains how to use two I2C interfaces connected to each other, with one acting as the master and the other as the slave. The focus is primarily on the master side. In the master task, there is a command queue to receive commands generated by the polling task every second or commands generated by pressing the button.
The main task of the master is to process the commands in the command queue, send commands to the slave, and receive and confirm the responses from the slave. The main APIs used in this example are:
* HAL_I2C_Master_Seq_Transmit_DMA()
* HAL_I2C_Master_Seq_Receive_DMA()
* HAL_I2C_Slave_Seq_Transmit_DMA()
* HAL_I2C_Slave_Seq_Receive_DMA()
The slave side uses dual I2C address settings, so the master can simulate communication with two slave sides.
If you are interested in the slave task, you can refer to another article titled 'Study of I2C Slave Task'.
---
### STM32CubeMX settings
---

**PB6,PB7:** Configure I2C1 for the slave
**PB10,PB11:** Configure I2C2 for the master
**PC13:** Configure GPIO EXTI for a button
**PA5:** Configure GPIO output for a LED

Configure the I2C1 speed to 100 kHz and the address length to 7 bits
Enable dual address acknowledgment

Enable the I2C1 global interrupt

Enable I2C1 RX and TX with DMA

Configure the I2C2 speed to 100 kHz and the address length to 7 bits

Enable the I2C2 global interrupt

Enable I2C2 RX and TX with DMA
---
### Code Structure
---






 
---
### API/Queue
---

The key feature of a queue is that it follows the FIFO (First In, First Out) principle. This means the first element added to the queue will be the first one to be removed.
This example code sets the queue to store 5 commands, with each command having a maximum length of 20 bytes.
```c=
//Queue.h
#ifndef SRC_QUEUE_H_
#define SRC_QUEUE_H_
#include "main.h"
#include "stdbool.h"
#define QUEUE_SIZE (5)
#define MAX_DATA_LEN (20)
typedef struct {
uint8_t arrData[QUEUE_SIZE][MAX_DATA_LEN];
uint8_t arrDataLen[QUEUE_SIZE];
uint8_t front;
int8_t rear;
uint8_t size;
} Queue;
int Queue_init(Queue *pQ);
bool Queue_isEmpty(Queue *pQ);
bool Queue_isFull(Queue *pQ);
int Queue_enqueue(Queue *pQ, uint8_t arrData[MAX_DATA_LEN], uint8_t dataLen);
int Queue_dequeue(Queue *pQ, uint8_t arrData[MAX_DATA_LEN], uint8_t *pDataLen);
#endif /* SRC_QUEUE_H_ */
```
```c=
//Queue.c
#include "API/Queue.h"
#include "string.h"
int Queue_init(Queue *pQ) {
if (!pQ) {
return -1;
}
pQ->front = 0;
pQ->rear = -1;
pQ->size = 0;
return 0;
}
bool Queue_isEmpty(Queue *pQ) {
if (!pQ) {
return true;
}
return 0 == pQ->size;
}
bool Queue_isFull(Queue *pQ) {
if (!pQ) {
return true;
}
return QUEUE_SIZE == pQ->size;
}
int Queue_enqueue(Queue *pQ, uint8_t arrData[MAX_DATA_LEN], uint8_t dataLen) {
if (Queue_isFull(pQ) || !arrData || !dataLen || MAX_DATA_LEN < dataLen) {
return -1;
}
pQ->rear = (pQ->rear + 1) % QUEUE_SIZE;
memcpy(pQ->arrData[pQ->rear], arrData, dataLen);
pQ->arrDataLen[pQ->rear] = dataLen;
pQ->size++;
return 0;
}
int Queue_dequeue(Queue *pQ, uint8_t arrData[MAX_DATA_LEN], uint8_t *pDataLen) {
if (Queue_isEmpty(pQ) || !arrData || !pDataLen) {
return -1;
}
uint8_t dataLen = pQ->arrDataLen[pQ->front];
memcpy(arrData, pQ->arrData[pQ->front], dataLen);
*pDataLen = dataLen;
pQ->front = (pQ->front + 1) % QUEUE_SIZE;
pQ->size--;
return 0;
}
```
---
### API/CmdPackage
---

The command consists of three parts: header, data, and CRC32.
Header has an element called dataLen; it indicates the actual data length.
CRC32 calculation ranges from the header to the data.

* **Cmd_pack():** Fill the data into pData; it will assemble the header and CRC32 and return the result in pCmd
**addr:** Assign the I2C address.
**cmdId:** Assign the command ID.
**pData:** Assign the command data.
**dataLen:** Assign the command data length.
**pCmd:** Output the command that includes the header, data, and CRC32.
**cmdLen:** Output the command length.
* **Cmd_unpack():** Fill the command into pCmd; it will disassemble the header and CRC32 and return the data in pData
**pAddr:** Output the I2C address.
**pCmdId:** Output the command ID.
**pCmd:** Assign the command that includes the header, data, and CRC32.
**cmdLen:** Assign the command length.
**pData:** Output the command data.
**pDataLen:** Output the command data length.
```c=
//CmdPackage.h
#ifndef INC_API_CMDPACKAGE_H_
#define INC_API_CMDPACKAGE_H_
#include "main.h"
typedef struct {
uint8_t addr;
const uint8_t ver;
uint8_t cmdId;
uint8_t dataLen;
const uint16_t magicNum;
} CmdHdr;
// cmd = | header (6 bytes) | data (dataLen bytes) | CRC32 (4 bytes) |
#define CRC_LEN (4)
int Cmd_pack(uint8_t addr, uint8_t cmdId, uint8_t *pData, uint8_t dataLen, uint8_t *pCmd, uint8_t *pCmdLen);
int Cmd_unpack(uint8_t *pAddr, uint8_t *pCmdId, uint8_t *pCmd, uint8_t cmdLen, uint8_t *pData, uint8_t *pDataLen);
#endif /* INC_API_CMDPACKAGE_H_ */
```
```c=
//CmdPackage.c
#include <string.h>
#include "main.h"
#include "API/CRC.h"
#include "API/CmdPackage.h"
static CmdHdr cmdHeader = {
.ver = 0x01,
.magicNum = 0xaabb,
};
#define CRC_LEN (4)
#define MAX_CMD_LEN (255)
#define MAX_DATA_LEN (MAX_CMD_LEN - sizeof(cmdHeader) - CRC_LEN)
int Cmd_pack(uint8_t addr, uint8_t cmdId, uint8_t *pData, uint8_t dataLen, uint8_t *pCmd, uint8_t *pCmdLen)
{
if (!pCmd || !pCmdLen) {
return -1;
}
if (0 != dataLen && !pData) {
return -1;
}
if (MAX_DATA_LEN < dataLen) {
return -1;
}
cmdHeader.addr = addr;
cmdHeader.cmdId = cmdId;
cmdHeader.dataLen = dataLen;
memcpy(pCmd, &cmdHeader, sizeof(CmdHdr));
memcpy(pCmd + sizeof(CmdHdr), pData, dataLen);
uint32_t crc = CRC32_calculate(pCmd, sizeof(CmdHdr) + dataLen);
memcpy(pCmd + sizeof(CmdHdr) + dataLen, &crc, CRC_LEN);
*pCmdLen = sizeof(CmdHdr) + dataLen + CRC_LEN;
return 0;
}
int Cmd_unpack(uint8_t *pAddr, uint8_t *pCmdId, uint8_t *pCmd, uint8_t cmdLen, uint8_t *pData, uint8_t *pDataLen)
{
if (!pAddr || !pCmdId || !pCmd || sizeof(CmdHdr) + CRC_LEN > cmdLen || !pData || !pDataLen) {
return -1;
}
uint32_t crcCalc = CRC32_calculate(pCmd, cmdLen - CRC_LEN);
uint32_t crcParse;
memcpy(&crcParse, pCmd + cmdLen - CRC_LEN, CRC_LEN);
if (crcCalc != crcParse) {
return -2;//CRC check error
}
CmdHdr hdr;
memcpy(&hdr, pCmd, sizeof(CmdHdr));
if (hdr.magicNum != cmdHeader.magicNum) {
return -3;//check magic number error
}
*pAddr = hdr.addr;
*pCmdId = hdr.cmdId;
memcpy(pData, pCmd + sizeof(CmdHdr), hdr.dataLen);
*pDataLen = hdr.dataLen;
return 0;
}
```
---
### API/CmdProc
---
All command IDs are defined in CmdProc.h. Cmd_procS() creates a corresponding table based on the command ID and is defined in CMdProcSlave.c. Similarly, Cmd_procM() creates a corresonding table based on the command ID and is defined in CmdProcMaster.c
* **Cmd_procS():** This API is for use by the slave task.
**cmdId:** The command ID that is unpacked from the request command.
**arrReq[]:** The request data sent from the master. (unpacked from the request command)
**pReqLen:** The length of the request data.
**arrRes[]:** The reponse data that is generated by this API.
**pResLen:** The length of the response data.
* **Cmd_procM():** This API is for use by the master task.
**addr:** The slave address unpacked from the response command.
**arrRes[]:** The response data unpacked from the response command.
**resLen:** The response data length that is unpacked from the response command.
```c=
//CmdProc.h
#ifndef INC_API_CMDPROC_H_
#define INC_API_CMDPROC_H_
//command ID
#define CMD_R_MCU_TEMP (0x0A)
#define CMD_R_FAN_SPEED (0x01)
#define CMD_W_FAN_PWM (0x02)
//for the slave task to use
int Cmd_procS(uint8_t cmdId, uint8_t arrReq[], uint8_t *pReqLen, uint8_t arrRes[], uint8_t *pResLen);
//for the master task to use
int Cmd_procM(uint8_t addr, uint8_t cmdId, uint8_t arrRes[], uint8_t resLen);
#endif /* INC_API_CMDPROC_H_ */
```
```c=
//CmdProcMaster.c
#include "main.h"
#include "API/CmdProc.h"
static int getMcuTemp(uint8_t addr, uint8_t arrData[], uint8_t dataLen);
static int getFanSpeed(uint8_t addr, uint8_t arrData[], uint8_t dataLen);
static int checkRet(uint8_t addr, uint8_t arrData[], uint8_t dataLen);
//Master task's command table
static struct {
uint8_t cmdId;
int (*pProcFun)(uint8_t addr, uint8_t arrData[], uint8_t dataLen);
} arrT[] = {
{CMD_R_MCU_TEMP, getMcuTemp},
{CMD_R_FAN_SPEED, getFanSpeed},
{CMD_W_FAN_PWM, checkRet},
};
#define TBL_NUM (sizeof(arrT) / sizeof(arrT[0]))
int Cmd_procM(uint8_t addr, uint8_t cmdId, uint8_t arrRes[], uint8_t resLen)
{
if (!arrRes || !resLen) {
return -1;
}
uint8_t idx;
for (idx = 0; TBL_NUM > idx; idx++) {
if (arrT[idx].cmdId == cmdId) {
break;
}
}
if (TBL_NUM == idx) {
return -1; //the cmdId is not found
}
if (arrT[idx].pProcFun(addr, arrRes, resLen)) {
return -1;
}
return 0;
}
static int getMcuTemp(uint8_t addr, uint8_t arrData[], uint8_t dataLen)
{
if (!arrData || !dataLen) {
return -1;
}
//example
if (0x10 == addr) {
//do something
}
else if (0x11 == addr) {
//do something
}
return 0;
}
static int getFanSpeed(uint8_t addr, uint8_t arrData[], uint8_t dataLen)
{
if (!arrData || !dataLen) {
return -1;
}
//example
if (0x10 == addr) {
//do something
}
else if (0x11 == addr) {
//do something
}
return 0;
}
static int checkRet(uint8_t addr, uint8_t arrData[], uint8_t dataLen)
{
if (!arrData || !dataLen) {
return -1;
}
//return: 0 => success
if (0 != arrData[0]) {
return -1;
}
return 0;
}
```
```c=
//CmdProcSlave.c
#include "main.h"
#include "API/CmdProc.h"
static int getMcuTemp(uint8_t arrRes[], uint8_t *pResLen);
static int getFanSpeed(uint8_t arrRes[], uint8_t *pResLen);
static int setFanPWM(uint8_t arrReq[], uint8_t *pReqLen);
enum CmdType {
CMD_WRITE,
CMD_READ,
CMD_TYPE_NUM
};
//Slave task's command table
static struct {
uint8_t cmdId;
enum CmdType cmdType;
int (*pProcFun)(uint8_t *parrData, uint8_t *pDataLen);
} arrT[] = {
{CMD_R_MCU_TEMP, CMD_READ, getMcuTemp},
{CMD_R_FAN_SPEED, CMD_READ, getFanSpeed},
{CMD_W_FAN_PWM, CMD_WRITE, setFanPWM},
};
#define TBL_NUM (sizeof(arrT) / sizeof(arrT[0]))
int Cmd_procS(uint8_t cmdId, uint8_t arrReq[], uint8_t *pReqLen, uint8_t arrRes[], uint8_t *pResLen)
{
if (!arrReq || !arrRes || !pResLen) {
return -1;
}
uint8_t idx;
for (idx = 0; TBL_NUM > idx; idx++) {
if (arrT[idx].cmdId == cmdId) {
break;
}
}
if (TBL_NUM == idx) {
return -1; //the cmdId is not found
}
if (CMD_WRITE == arrT[idx].cmdType) {
arrT[idx].pProcFun(arrReq, pReqLen);
*pResLen = 0;
}
else {//CMD_READ
arrT[idx].pProcFun(arrRes, pResLen);
}
return 0;
}
static int getMcuTemp(uint8_t arrRes[], uint8_t *pResLen)
{
if (!arrRes || !pResLen) {
return -1;
}
//example
arrRes[0] = 30;
*pResLen = 1;
return 0;
}
static int getFanSpeed(uint8_t arrRes[], uint8_t *pResLen)
{
if (!arrRes || !pResLen) {
return -1;
}
//example
arrRes[0] = 145;
*pResLen = 1;
return 0;
}
static int setFanPWM(uint8_t arrReq[], uint8_t *pReqLen)
{
if (!arrReq || !pReqLen) {
return -1;
}
//example
uint8_t fanPwm = arrReq[0];
return 0;
}
```
---
### main.c
---
```c=
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_I2C1_Init();
MX_I2C2_Init();
MX_CRC_Init();
/* USER CODE BEGIN 2 */
I2CSlave_init();
I2CMaster_init();
/* USER CODE END 2 */
/* USER CODE BEGIN WHILE */
while (1)
{
I2CSlave_task();
I2CMaster_task();
/* USER CODE END WHILE */
}
}
```
---
### Service/I2C_slave
---
Specify two sets of I2C address during initialization
```c=
//I2C_slave.c
int I2CSlave_init(void)
{
hi2c1.Init.OwnAddress1 = 0x10 << 1;
hi2c1.Init.OwnAddress2 = 0x11 << 1;
if (HAL_OK != HAL_I2C_Init(&hi2c1)) {
return -1;
}
HAL_I2C_EnableListen_IT(&hi2c1);
return 0;
}
```
---
### Service/I2C_master
---
```c=
//I2C_master.h
#ifndef INC_SERVICES_I2C_MASTER_H_
#define INC_SERVICES_I2C_MASTER_H_
#include "main.h"
#include "API/Queue.h"
int I2CMaster_init(void);
int I2CMaster_task(void);
int I2CMaster_enqueueCmd(uint8_t arrData[MAX_DATA_LEN], uint8_t dataLen);
#endif /* INC_SERVICES_I2C_MASTER_H_ */
```
The I2CMaster_enqueueCmd() API is for other services to call; in this example, it is called when the button is pressed.
```c=
//I2C_master.c
int I2CMaster_enqueueCmd(uint8_t arrData[MAX_DATA_LEN], uint8_t dataLen)
{
if (!arrData || !dataLen) {
return -1;
}
if (Queue_enqueue(&cmdQ, arrData, dataLen)) {
return -1;
}
return 0;
}
```
Declare a global variable cmdQ in I2C_master.c, and I2CMaster_init() initializes this command queue.
```c=
//I2C_master.c
static Queue cmdQ;
int I2CMaster_init(void)
{
Queue_init(&cmdQ);
return 0;
}
```
The two main tasks of I2CMaster_task() are processing the command queue and enqueuing the polling command every second.
* **STATE_WAITING:** Wait unitil there is a command in the queue, then start transferring the command to the slave
* **STATE_XFERING:** Send a command to the slave and read the returned data
* **STATE_PROCESSING:** Process the returned data
```c=
//I2C_master.c
int I2CMaster_task(void)
{
enum {
STATE_WAITING,
STATE_XFERING,
STATE_PROCESSING,
STATE_NUM
};
static uint8_t txLen = 0;
static uint8_t rxLen = 0;
static uint8_t state = STATE_WAITING;
int ret = -1;
switch (state) {
case STATE_WAITING:
if (!Queue_isEmpty(&cmdQ)) {
if (Queue_dequeue(&cmdQ, arrMaTx, &txLen)) {
return -1;
}
memset(arrMaRx, 0, sizeof(arrMaRx));
state = STATE_XFERING;
}
else {
break;
}
case STATE_XFERING:
if (0 > (ret = xferf(arrMaTx, txLen, arrMaRx, &rxLen))) {
return -1;
}
if (1 == ret) {//still in transfer
break;
}
if (2 == ret) {//timeout
state = STATE_WAITING;
break;
}
if (0 == ret) {
state = STATE_PROCESSING;
}
case STATE_PROCESSING:
uint8_t addr = 0;
uint8_t cmdId = 0;
uint8_t arrRes[MAX_DATA_LEN] = {0};
uint8_t resLen = 0;
if (Cmd_unpack(&addr, &cmdId, arrMaRx, rxLen, arrRes, &resLen)) {
return -1;
}
if (Cmd_procM(addr, cmdId, arrRes, resLen)) {
return -1;
}
state = STATE_WAITING;
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);//Blinking LED for observation
break;
default:
//reserve
break;
}
//polling task
static uint32_t timeBase;
if (1000 <= HAL_GetTick() - timeBase) {
timeBase = HAL_GetTick();
uint8_t arrD[MAX_DATA_LEN];
uint8_t arrCmd[] = {CMD_R_MCU_TEMP};
uint8_t arrAddr[] = {0x10, 0x11};
#define CMD_NUM sizeof(arrCmd) / sizeof(arrCmd[0])
#define ADDR_NUM sizeof(arrAddr) / sizeof(arrAddr[0])
for (uint8_t i = 0; CMD_NUM > i; i++) {
for(uint8_t j = 0; ADDR_NUM > j; j++) {
if (Cmd_pack(arrAddr[j], arrCmd[i], NULL, 0, arrD, &txLen)) {
return -1;
}
if (Queue_enqueue(&cmdQ, arrD, txLen)) {
return -1;
}
}
}
}
return 0;
}
```
The xferf() function is responsible for sending the command to the slave and reading the returned data from the slave.
* **STATE_TX:** Trigger command transmission.
* **STATE_TX_DONE:** Confirm whether the transmission is complete.
* **STATE_RX:** Trigger data reception.
* **STATE_RX_DONE:** Confirm whether the reception is complete.
```c=
//I2C_master.c
#include <string.h>
#include "Services/I2C_master.h"
#include "API/CmdPackage.h"
#include "API/CmdProc.h"
#include "API/Queue.h"
#include "main.h"
extern I2C_HandleTypeDef hi2c2;
static Queue cmdQ;
static uint8_t arrMaRx[MAX_DATA_LEN];
static uint8_t arrMaTx[MAX_DATA_LEN];
static uint8_t rxCnt = 0;
static bool isRxDone = false;
static bool isTxDone = false;
static uint8_t i2cAddr = 0;
static int xferf(uint8_t arrMaTx[], uint8_t txLen, uint8_t arrMaRx[], uint8_t *pRxLen)
{
if (!arrMaTx || !txLen || !arrMaRx || !pRxLen) {
return -1;
}
enum {
STATE_TX,
STATE_TX_DONE,
STATE_RX,
STATE_RX_DONE,
STATE_NUM
};
static uint8_t state = STATE_TX;
static uint32_t timeBaseTX = 0;
static uint32_t timeBaseRX = 0;
switch (state) {
case STATE_TX:
i2cAddr = arrMaTx[0];
if (HAL_OK != HAL_I2C_Master_Seq_Transmit_DMA(&hi2c2, i2cAddr << 1, arrMaTx, txLen, I2C_FIRST_FRAME)) {
HAL_I2C_DeInit(&hi2c2);
HAL_I2C_Init(&hi2c2);
return -1;
}
timeBaseTX = HAL_GetTick();
state = STATE_TX_DONE;
case STATE_TX_DONE:
if (isTxDone) {
isTxDone = false;
timeBaseTX = HAL_GetTick();
state = STATE_RX;
}
else if (5 < HAL_GetTick() - timeBaseTX) {
state = STATE_TX;
return 2;//tx timeout
}
break;
case STATE_RX:
if (1 < HAL_GetTick() - timeBaseTX) {
rxCnt = 0;
//Read the command header first to obtain the data length,
//then use the data length to read the remaining data
if (HAL_OK != HAL_I2C_Master_Seq_Receive_DMA(&hi2c2, i2cAddr << 1, arrMaRx, sizeof(CmdHdr), I2C_NEXT_FRAME)) {
HAL_I2C_DeInit(&hi2c2);
HAL_I2C_Init(&hi2c2);
return -1;
}
timeBaseRX = HAL_GetTick();
state = STATE_RX_DONE;
}
break;
case STATE_RX_DONE:
if (isRxDone) {
isRxDone = false;
*pRxLen = rxCnt;
state = STATE_TX;
return 0;//done
}
else if (10 <= HAL_GetTick() - timeBaseRX) {
state = STATE_TX;
return 2;//rx timeout
}
break;
default:
//reserve
break;
}
return 1;//busy
}
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
if (I2C2 == hi2c->Instance) {
isTxDone = true;
}
}
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
static uint8_t dataLen = 0;
if (I2C2 == hi2c->Instance) {
if (0 == rxCnt) {
rxCnt += sizeof(CmdHdr);
CmdHdr *pHdr = (CmdHdr*)arrMaRx;
dataLen = pHdr->dataLen;
//Read the remaining data
if (HAL_OK != HAL_I2C_Master_Seq_Receive_DMA(hi2c, i2cAddr << 1, arrMaRx + sizeof(CmdHdr), dataLen + CRC_LEN, I2C_LAST_FRAME)) {
HAL_I2C_DeInit(&hi2c2);
HAL_I2C_Init(&hi2c2);
return;
}
}
else {
rxCnt += (dataLen + CRC_LEN);
isRxDone = true;
}
}
}
```
---
### Service/btnDetect.c
---
```c=
#include <string.h>
#include "main.h"
#include "API/CmdPackage.h"
#include "API/CmdProc.h"
#include "API/Queue.h"
#include "Services/I2C_master.h"
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_PIN_13 == GPIO_Pin) {
if (GPIO_PIN_RESET == HAL_GPIO_ReadPin(GPIOC, GPIO_Pin))
{
uint8_t arrD[MAX_DATA_LEN];
uint8_t txLen;
if (Cmd_pack(0x10, CMD_R_FAN_SPEED, NULL, 0, arrD, &txLen)) {
return;
}
if (I2CMaster_enqueueCmd(arrD, txLen)) {
return;
}
}
}
}
```
---
### Youtube Video
---
{%youtube zhts142eh6A %}
The polling task sends two commands every second. When one command finisheds, it triggers the LED to toggle. You will see the LED turn on and off, which means both commands have finished.
You can see that pressing the button also triggers the LED to toggle, but it only sends one command. So, you will see half the data on the scope compared to the polling task.
---
### Reference
---
* https://www.geeksforgeeks.org/introduction-and-array-implementation-of-queue/