Понадобилось мне для проекта по приколу сделать i2c slave (ведомого устройства), но не просто эмуляцию одного устройства (например eeprom), а сразу эдакого эмулятора с, если можно это так назвать, API, к которому можно уже привязывать эмуляции конкретных реализаций устройств на произвольные адреса.

На тему реализации ведомых устройств на STM32 с использованием LL я как-то не особо много инфы нарыл, в итоге накостылил, как сам понял :D Тест отвёрткой проходит, хотя первые реализации иногда прям залипали намертво.

В статье я не буду глубоко описывать регистры, саму работу шины, прочее, этого добра навалом. Просто окунёмся в дип дарк фэнтези реализацию эмулятора простейшего тача cst816s.

Для тестирования нам понадобится примерно подобный стенд:

Блок-схема собранного на столе макета
Блок-схема собранного на столе макета

Примечание: Описания мастер-платы здесь не будет, если очень надо, то могу в отдельной статье описать драйвер LCD с примером использования в LVGL.

I2C Slave

Начнём с базы. Для начала реализовываем фундамент — это i2c_slave файлы, которые содержат в себе самый основной код по обработке прерываний и зарегистрированные реализации ведомых устройств. Этот фундамент умеет работать и отвечать мастеру самостоятельно, даже без единой привязки эмуляторов, щедро вываливая всё происходящее в лог (Segger RTT).

Если представить это в виде блок-схемы, то получится следующее:

wtf
wtf

i2c_slave.h

В хедере довольно минимальный набор необходимого. Дефайны и структуры:

  • I2C_SLAVE_SLAVES_MAX — сколько максимум поддерживается ссылок на эмуляции устройств

  • I2C_SLAVE_HW_DEBUG_LOG — куча дебаг инфы в RTT

  • slave_instance_t — "ссылка" на реализацию эмулятора конкретного устройства

  • i2c_slave_t — конфиг самого i2c_slave (например, можно сделать несколько, под каждый хардварный i2c)

Вызываемые из основной программы функции:

  • i2cSlave_Init — базовая инициализация

  • i2cSlave_Enable \ i2cSlave_Disable — оперативное отключение i2c_slave (при включении специально не активируется, т.к. бывают ситуации, что нужно сначала быть на шине мастером, а потом переключиться в режим ведомого)

  • i2cSlave_Register — регистрация (добавление ссылки) нового эмулятора

Колбеки, которые уже вызываются из прерывания периферии (в моей реализации i2c на LL в режима мастера прерывания вообще не используются):

  • i2cSlave_IrqAddrSet — новый запрос от мастера с адресом ведомого устройства

  • i2cSlave_IrqRecive — принят байт

  • i2cSlave_IrqTransmit — запрошен байт

  • i2cSlave_IrqNack — получен NACK

  • i2cSlave_IrqError — ошибка на шине

Последние функции вызываются из простого обработчика прерываний, пример которого будет ниже.

Скрытый текст
#ifndef _I2C_SLAVE_H_
#define _I2C_SLAVE_H_

/* ----------------------------------------------------------------------------------------------------------------- //
v0.10 - 2025.07.20:
- Первоначальная версия

v0.11 - 2025.07.21:
- Мелкие правки, вроде работает

// ----------------------------------------------------------------------------------------------------------------- */

#include "main.h"

#ifndef I2C_SLAVE_SLAVES_MAX
#define I2C_SLAVE_SLAVES_MAX          20
#endif

#define I2C_SLAVE_LOG_PREFIX          "[i2c_slv] "

#ifndef I2C_SLAVE_HW_DEBUG_LOG
#define I2C_SLAVE_HW_DEBUG_LOG        0
#endif

typedef struct __attribute__((__packed__))
{
   uint8_t addr;
   void *conf;
   void (*freg)(void *, uint8_t);
   void (*fwrite)(void *, uint8_t);
   void (*fread)(void *, uint8_t *);
   void (*fdone)(void *);
} slave_instance_t;

typedef struct __attribute__((__packed__))
{
   uint8_t inited;
   uint8_t addr;
   uint8_t reg;
   uint8_t slave_num;
   uint8_t slave_id;
   void *i2c;
   slave_instance_t slave[I2C_SLAVE_SLAVES_MAX];
} i2c_slave_t;

uint32_t i2cSlave_Init(i2c_slave_t *conf, void *hi2c);
void i2cSlave_Enable(i2c_slave_t *conf);
void i2cSlave_Disable(i2c_slave_t *conf);
uint32_t i2cSlave_Register(i2c_slave_t *conf, uint8_t slave_addr, void *slave_conf, void *freg, void *fwrite, void *fread, void *fdone);

// Call in Interrupt
uint32_t i2cSlave_IrqAddrSet(i2c_slave_t *conf, uint8_t addr);
void i2cSlave_IrqRecive(i2c_slave_t *conf);
void i2cSlave_IrqTransmit(i2c_slave_t *conf);
void i2cSlave_IrqComplete(i2c_slave_t *conf);
void i2cSlave_IrqNack(i2c_slave_t *conf);
void i2cSlave_IrqError(i2c_slave_t *conf);

#endif // _I2C_SLAVE_H_

i2c_slave.c

В реализации же особо ничего специфичного, но есть-таки пара моментов:

  • i2c_slave отвечает на все адреса, разрешённые маской (а это значит, что реализация поиска устройств у мастера может сбоить и "видеть" кучу устройств на шине), и выглядит это так:

    Мастер сканит шину и видит иллюзию изобилия ведомых устройств, а реальный только 0x15
    Мастер сканит шину и видит иллюзию изобилия ведомых устройств, а реальный только 0x15
  • если нет реализации эмулятора, а мастер запрашивает байт, то i2c_slave отправляет ему 0xFF всегда

Скрытый текст
#include "i2c_slave.h"
#include <string.h>

uint32_t i2cSlave_Init(i2c_slave_t *conf, void *hi2c)
{
   conf->i2c = hi2c;
   conf->inited = 1;
   conf->slave_num = 0;
   conf->slave_id = 0xff;
   conf->reg = 0;

   for (uint8_t i = 0; i < I2C_SLAVE_SLAVES_MAX; i++)
   {
      conf->slave[i].addr = 0xff;
   }

   // TODO: кривое определение, но для большинства задач пойдёт
   SysView_LogSuccess("\r\n" I2C_SLAVE_LOG_PREFIX "Inited at I2C%u\r\n", (hi2c == I2C1) ? 1 : 2);
   return 0x00;
}

void i2cSlave_Enable(i2c_slave_t *conf)
{
   if (conf->inited)
   {
      LL_I2C_SetOwnAddress1(conf->i2c, (0x01 << 1), LL_I2C_OWNADDRESS1_7BIT);
      LL_I2C_SetOwnAddress2(conf->i2c, (0x00 << 1), LL_I2C_OWNADDRESS2_MASK07); // 7 bit only compare
      LL_I2C_EnableOwnAddress1(conf->i2c);
      LL_I2C_EnableOwnAddress2(conf->i2c);

      LL_I2C_EnableIT_RX(conf->i2c);
      LL_I2C_EnableIT_TX(conf->i2c);
      LL_I2C_EnableIT_ADDR(conf->i2c);
      LL_I2C_EnableIT_NACK(conf->i2c);
      LL_I2C_EnableIT_ERR(conf->i2c);
      LL_I2C_EnableIT_STOP(conf->i2c);

      SysView_LogInfo(I2C_SLAVE_LOG_PREFIX "Enabled\r\n");
   }
}

void i2cSlave_Disable(i2c_slave_t *conf)
{
   if (conf->inited)
   {
      LL_I2C_DisableOwnAddress1(conf->i2c);
      LL_I2C_DisableOwnAddress2(conf->i2c);

      LL_I2C_DisableIT_RX(conf->i2c);
      LL_I2C_DisableIT_TX(conf->i2c);
      LL_I2C_DisableIT_ADDR(conf->i2c);
      LL_I2C_DisableIT_NACK(conf->i2c);
      LL_I2C_DisableIT_ERR(conf->i2c);
      LL_I2C_DisableIT_STOP(conf->i2c);

      conf->slave_id = 0xff;
      conf->reg = 0;
      SysView_LogInfo(I2C_SLAVE_LOG_PREFIX "Disabled\r\n");
   }
}

uint32_t i2cSlave_Register(i2c_slave_t *conf, uint8_t slave_addr, void *slave_conf, void *freg, void *fwrite, void *fread, void *fdone)
{
   if (conf->inited)
   {
      if (conf->slave_num < (I2C_SLAVE_SLAVES_MAX - 1))
      {
         for (uint8_t i = 0; i < conf->slave_num; i++)
         {
            if (conf->slave[i].addr == slave_addr)
            {
               SysView_LogWarning(I2C_SLAVE_LOG_PREFIX "0x%x already registered at id = %u! Skip\r\n", slave_addr, i);
               return 0x01;
            }
         }

         SysView_LogInfo(I2C_SLAVE_LOG_PREFIX "Registered: addr = 0x%x, id = %u\r\n", slave_addr, conf->slave_num);
         conf->slave[conf->slave_num].addr = slave_addr;
         conf->slave[conf->slave_num].conf = slave_conf;
         conf->slave[conf->slave_num].freg = freg;
         conf->slave[conf->slave_num].fwrite = fwrite;
         conf->slave[conf->slave_num].fread = fread;
         conf->slave[conf->slave_num].fdone = fdone;
         conf->slave_num++;
         return 0x00;
      }
   }

   return 0x01;
}

uint32_t i2cSlave_IrqAddrSet(i2c_slave_t *conf, uint8_t addr)
{
   if (conf->inited)
   {
#if (I2C_SLAVE_HW_DEBUG_LOG)
      SysView_LogInfo(I2C_SLAVE_LOG_PREFIX "Addr 0x%x: ", addr);
#endif
      conf->addr = addr;
      conf->reg = 0;
      conf->slave_id = 0xff;
      for (uint8_t i = 0; i < conf->slave_num; i++)
      {
         if (conf->slave[i].addr == conf->addr)
         {
            conf->slave_id = i;
#if (I2C_SLAVE_HW_DEBUG_LOG)
            SysView_LogInfo("id = 0x%x\r\n", conf->slave_id);
#endif
            return 0x00;
         }
      }

      if (conf->slave_id == 0xff)
      {
#if (I2C_SLAVE_HW_DEBUG_LOG)
         SysView_LogWarning("not found\r\n");
#endif
      }
   }

   return 0x01;
}

void i2cSlave_IrqRecive(i2c_slave_t *conf)
{
   if (conf->inited)
   {
      uint8_t data = LL_I2C_ReceiveData8(conf->i2c);
      if (conf->slave_id != 0xff)
      {
         if (conf->reg == 0)
         {
#if (I2C_SLAVE_HW_DEBUG_LOG)
            SysView_LogInfo(I2C_SLAVE_LOG_PREFIX "Reg = 0x%x\r\n", data);
#endif
            conf->reg = 1;
            conf->slave[conf->slave_id].freg(conf->slave[conf->slave_id].conf, data);
         }
         else
         {
#if (I2C_SLAVE_HW_DEBUG_LOG)
            SysView_LogInfo(I2C_SLAVE_LOG_PREFIX "Write 0x%x\r\n", data);
#endif
            conf->slave[conf->slave_id].fwrite(conf->slave[conf->slave_id].conf, data);
         }
      }
      else
      {
         SysView_LogWarning(I2C_SLAVE_LOG_PREFIX "Write skip 0x%x\r\n", data);
      }
   }
}

void i2cSlave_IrqTransmit(i2c_slave_t *conf)
{
   if (conf->inited)
   {
      if (conf->slave_id != 0xff)
      {
         static uint8_t data;
         data = 0x00;
         conf->slave[conf->slave_id].fread(conf->slave[conf->slave_id].conf, &data);
#if (I2C_SLAVE_HW_DEBUG_LOG)
         SysView_LogInfo(I2C_SLAVE_LOG_PREFIX "Read 0x%x\r\n", data);
#endif
         LL_I2C_TransmitData8(conf->i2c, data);
         return ;
      }

#if (I2C_SLAVE_HW_DEBUG_LOG)
      SysView_LogWarning(I2C_SLAVE_LOG_PREFIX "Read skip\r\n");
#endif
      LL_I2C_TransmitData8(conf->i2c, 0xff);
   }
}

void i2cSlave_IrqComplete(i2c_slave_t *conf)
{
   if (conf->inited)
   {
#if (I2C_SLAVE_HW_DEBUG_LOG)
      SysView_LogSuccess(I2C_SLAVE_LOG_PREFIX "Complete\r\n");
#endif
      conf->addr = 0xff;
      conf->reg = 0;
      if (conf->slave_id != 0xff)
      {
         conf->slave[conf->slave_id].fdone(conf->slave[conf->slave_id].conf);
      }

      conf->slave_id = 0xff;
   }
}

void i2cSlave_IrqNack(i2c_slave_t *conf)
{
   if (conf->inited)
   {
#if (I2C_SLAVE_HW_DEBUG_LOG)
      SysView_LogWarning(I2C_SLAVE_LOG_PREFIX "NACK\r\n");
#endif
      conf->addr = 0xff;
      conf->reg = 0;
      if (conf->slave_id != 0xff)
      {
         conf->slave[conf->slave_id].fdone(conf->slave[conf->slave_id].conf);
      }

      conf->slave_id = 0xff;
   }
}

void i2cSlave_IrqError(i2c_slave_t *conf)
{
   if (conf->inited)
   {
      LL_I2C_Disable(conf->i2c);
      LL_I2C_Enable(conf->i2c);
#if (I2C_SLAVE_HW_DEBUG_LOG)
      SysView_LogError(I2C_SLAVE_LOG_PREFIX "Error\r\n");
#endif
      conf->addr = 0xff;
      conf->reg = 0;
      if (conf->slave_id != 0xff)
      {
         conf->slave[conf->slave_id].fdone(conf->slave[conf->slave_id].conf);
      }

      conf->slave_id = 0xff;
   }
}

cst816s

Тут особо и описывать нечего, просто переписанная под себя версия, взятая с примера WaveShare.

cst816s.h

Скрытый текст
#ifndef _CST816S_H_
#define _CST816S_H_

/* ----------------------------------------------------------------------------------------------------------------- //
v0.10 - 2025.03.23:
- Первоначальная версия

v0.11 - 2025.05.20:
- Правки возвращаемых статусов
- Переход на target_common.h

// ----------------------------------------------------------------------------------------------------------------- */

#include "main.h"

#ifndef CST816S_TIMEOUT_MS
#define CST816S_TIMEOUT_MS                         500
#endif

#ifndef CST816S_DEFAULT_ADDRESS
#define CST816S_DEFAULT_ADDRESS                    0x15
#endif

#ifndef CST816S_RESET_PIN_ENABLE
#define CST816S_RESET_PIN_ENABLE                   0
#endif

#ifndef CST816S_DEBUG_SHOW_RAW_EVENTS
#define CST816S_DEBUG_SHOW_RAW_EVENTS              0
#endif

#define CST816S_LOG_PREFIX                         "[cst816s] "

typedef enum
{
   CST816S_GESTURE_NONE = 0x00,
   CST816S_GESTURE_SWIPE_UP = 0x01,
   CST816S_GESTURE_SWIPE_DOWN = 0x02,
   CST816S_GESTURE_SWIPE_LEFT = 0x03,
   CST816S_GESTURE_SWIPE_RIGHT = 0x04,
   CST816S_GESTURE_SINGLE_CLICK = 0x05,
   CST816S_GESTURE_DOUBLE_CLICK = 0x0B,
   CST816S_GESTURE_LONG_PRESS = 0x0C
} cst816s_gesture_t;

typedef enum
{
   CST816S_EVENT_DOWN = 0x00,
   CST816S_EVENT_UP,
   CST816S_EVENT_CONTACT,
} cst816s_event_type_t;

typedef struct __attribute__((__packed__))
{
   uint16_t x;
   uint16_t y;
} cst816s_event_t;

typedef struct __attribute__((__packed__))
{
   uint8_t address;
   uint8_t inited;
   uint8_t gesture; // cst816s_gesture_t
   uint8_t event_type; // cst816s_event_type_t
   cst816s_event_t event;
#if (CST816S_RESET_PIN_ENABLE)
   void *reset_port;
   uint16_t reset_pin;
#endif
   void *i2c;
} cst816s_touch_t;

uint8_t cst816s_Init(cst816s_touch_t *conf, void *hi2c, uint8_t address);
uint8_t cst816s_SetAutoSleep(cst816s_touch_t *conf, uint8_t enable, uint8_t sec);
uint8_t cst816s_ReadVersion(cst816s_touch_t *conf);
uint8_t cst816s_ReadEvent(cst816s_touch_t *conf);

#endif // _CST816S_H_

cst816s.c

Скрытый текст
#include "cst816s.h"
#include "target_delay.h"
#include "target_gpio.h"
#include "target_i2c.h"

__inline uint32_t cst816s_RegWrite(cst816s_touch_t *conf, uint8_t reg, uint8_t data)
{
   return target_I2C_WriteByte(conf->i2c, conf->address, reg, TARGET_I2C_REG_LEN_BYTE, data, CST816S_TIMEOUT_MS);
}

__inline uint32_t cst816s_RegRead(cst816s_touch_t *conf, uint8_t reg, uint8_t *data)
{
   return target_I2C_ReadByte(conf->i2c, conf->address, reg, TARGET_I2C_REG_LEN_BYTE, data, CST816S_TIMEOUT_MS);
}

uint8_t cst816s_Init(cst816s_touch_t *conf, void *hi2c, uint8_t address)
{
   conf->i2c = hi2c;
   conf->address = (address << 1);
   conf->inited = 0;

#if (CST816S_RESET_PIN_ENABLE)
   target_ResetPin(conf->reset_port, conf->reset_pin);
   target_Delay_ms(1);
   target_SetPin(conf->reset_port, conf->reset_pin);
   target_Delay_ms(10);
#endif

   if (target_I2C_IsDeviceReady(conf->i2c, conf->address, CST816S_TIMEOUT_MS) == 0x00)
   {
      conf->inited = 1;
      SysView_LogSuccess("\r\n"CST816S_LOG_PREFIX "Inited (addr 0x%x)\r\n", address);
      cst816s_ReadVersion(conf);
      cst816s_RegWrite(conf, 0xEC, 1); // Enable double-tap
      cst816s_SetAutoSleep(conf, 0, 255); // Disable auto sleep
      return STATUS_OK;
   }

   SysView_LogWarning("\r\n"CST816S_LOG_PREFIX "Init failed (addr 0x%x)\r\n", address);
   return STATUS_BUSY;
}

uint8_t cst816s_SetAutoSleep(cst816s_touch_t *conf, uint8_t enable, uint8_t sec)
{
   if (conf->inited)
   {
      uint32_t status = STATUS_OK;
      if (enable)
      {
         SysView_LogInfo(CST816S_LOG_PREFIX "Auto sleep enabled, timeout %u sec\r\n", sec);
         status  = cst816s_RegWrite(conf, 0xFE, 0x00);
         status |= cst816s_RegWrite(conf, 0xF9, sec);
      }
      else
      {
         SysView_LogInfo(CST816S_LOG_PREFIX "Auto sleep disabled\r\n");
         status = cst816s_RegWrite(conf, 0xFE, 0xFE);
      }

      return status;
   }

   return STATUS_ERROR;
}

uint8_t cst816s_ReadVersion(cst816s_touch_t *conf)
{
   if (conf->inited)
   {
      uint8_t data[3] = { 0 };
      uint32_t status = STATUS_OK;

      status  = cst816s_RegRead(conf, 0xA7, &data[0]); // ChipID
      status |= cst816s_RegRead(conf, 0xA8, &data[1]); // ProjID
      status |= cst816s_RegRead(conf, 0xA9, &data[2]); // Firmware

      SysView_LogInfo(CST816S_LOG_PREFIX "Chip ID: 0x%x\r\n", data[0]);
      SysView_LogInfo(CST816S_LOG_PREFIX "Project ID: 0x%x\r\n", data[1]);
      SysView_LogInfo(CST816S_LOG_PREFIX "Firmware: 0x%x\r\n", data[2]);

      return status;
   }

   return STATUS_ERROR;
}

uint8_t cst816s_ReadEvent(cst816s_touch_t *conf)
{
   if (conf->inited)
   {
      uint8_t data[6];
      uint32_t status = target_I2C_ReadMem(conf->i2c, conf->address, 0x01, TARGET_I2C_REG_LEN_BYTE, &data[0], 6, CST816S_TIMEOUT_MS);

      conf->gesture = data[0];
      //points = data[1];
      conf->event_type = data[2] >> 6;
      conf->event.x = ((data[2] & 0xF) << 8) + data[3];
      conf->event.y = ((data[4] & 0xF) << 8) + data[5];

#if (CST816S_DEBUG_SHOW_RAW_EVENTS)
      SysView_LogInfo(CST816S_LOG_PREFIX "Event: X=%03u Y=%03u\r\n", conf->event.x, conf->event.y);
      if (conf->gesture)
      {
         SysView_LogInfo(CST816S_LOG_PREFIX "Gesture: 0x%x\r\n", conf->gesture);
      }
#endif
      return status;
   }

   return STATUS_ERROR;
}

cst816s_emu

А тут уже интереснее, скопировал драйвер выше, переименовал и стал дописывать. Чего поменялось\появилось:

  • Вначале мастер опрашивает тач и читает с него Chip ID, Proj ID и Firmware Version, так вот, первые два я оставил как в чипе, а Firmware поменял на 0x69 (чтобы отличать эмулятор от хардварного чипа)

  • Добавляем функции I2C Slave API, которые потом будем привязывать

  • cst816s_emu_NewEvent — вызываем и таким образом "генерим" событие нажатия (формируется прерывание на int пине, и при запросе от мастера выдаём ему x,y, которые переданы в эту функцию)

cst816s_emu.h

Скрытый текст
#ifndef _CST816S_EMU_H_
#define _CST816S_EMU_H_

/* ----------------------------------------------------------------------------------------------------------------- //
v0.10 - 2025.07.20:
- Первоначальная версия на основе cst816s.h из LLSDK

// ----------------------------------------------------------------------------------------------------------------- */

#include "main.h"
#include "cst816s.h"
#include "i2c_slave.h"

#ifndef CST816S_EMU_TIMEOUT_MS
#define CST816S_EMU_TIMEOUT_MS                  100
#endif

#ifndef CST816S_EMU_DEFAULT_ADDRESS
#define CST816S_EMU_DEFAULT_ADDRESS             0x15
#endif

#ifndef CST816S_EMU_DEBUG_SHOW_RAW_EVENTS
#define CST816S_EMU_DEBUG_SHOW_RAW_EVENTS       0
#endif

#define CST816S_EMU_CHIP_ID                     0xb6 // 0xA7
#define CST816S_EMU_PROJECT_ID                  0x02 // 0xA8
#define CST816S_EMU_FIRMWARE_VER                0x69 // 0xA9

#define CST816S_EMU_LOG_PREFIX                  "[cst816s_emu] "

typedef struct __attribute__((__packed__))
{
   uint8_t address;
   uint8_t inited;
   uint8_t reg_addr;
   uint8_t event_new;
   uint32_t event_time;
   cst816s_event_t event;
   void *int_port;
   uint16_t int_pin;
   void *i2c;
} cst816s_emu_touch_t;

uint32_t cst816s_emu_Init(cst816s_emu_touch_t *conf, void *hi2c, uint8_t address);
uint32_t cst816s_emu_NewEvent(cst816s_emu_touch_t *conf, uint16_t x, uint16_t y);
void cst816s_emu_Routine(cst816s_emu_touch_t *conf);

// I2C Slave API
void cst816s_emu_Reg(cst816s_emu_touch_t *conf, uint8_t data);
void cst816s_emu_Write(cst816s_emu_touch_t *conf, uint8_t data);
void cst816s_emu_Read(cst816s_emu_touch_t *conf, uint8_t *data);
void cst816s_emu_Done(cst816s_emu_touch_t *conf);

#endif // _CST816S_EMU_H_

cst816s_emu.c

Из интересного здесь, наверное, только реализация cst816s_emu_Read, в которой мы, считай, и должны следить за текущим адресом (и смещением, откуда мастер ожидает прочитать или записать данные), а также обрабатывать возможные нештатные ситуации (когда мастер пытается запросить данные или записать вне адресов симулированных регистров).

Скрытый текст
#include "cst816s_emu.h"
#include "target_gpio.h"
#include "target_delay.h"

uint32_t cst816s_emu_Init(cst816s_emu_touch_t *conf, void *hi2c, uint8_t address)
{
   conf->i2c = hi2c;
   conf->address = (address << 1);
   conf->inited = 1;
   conf->event_new = 0;
   target_SetPin(conf->int_port, conf->int_pin);
   SysView_LogSuccess("\r\n"CST816S_EMU_LOG_PREFIX "Inited (addr 0x%x)\r\n", address);
   return STATUS_OK;
}

uint32_t cst816s_emu_NewEvent(cst816s_emu_touch_t *conf, uint16_t x, uint16_t y)
{
   if (conf->inited)
   {
      if (conf->event_new == 0)
      {
         SysView_LogInfo(CST816S_EMU_LOG_PREFIX "Event: X=%03u Y=%03u\r\n", x, y);
         conf->event.x = x;
         conf->event.y = y;
         conf->event_time = SysTick_GetCurrentTick();
         conf->event_new = 1;
         return STATUS_OK;
      }

      return STATUS_BUSY;
   }

   return STATUS_ERROR;
}

void cst816s_emu_Routine(cst816s_emu_touch_t *conf)
{
   if (conf->inited)
   {
      if (conf->event_new)
      {
         target_ResetPin(conf->int_port, conf->int_pin);
         if (SysTick_GetCurrentTick() - conf->event_time) // 1ms
         {
            target_SetPin(conf->int_port, conf->int_pin);
            conf->event_new = 0;
         }
      }
   }
}

void cst816s_emu_Reg(cst816s_emu_touch_t *conf, uint8_t data)
{
   if (conf->inited)
   {
      conf->reg_addr = data;
   }
}

void cst816s_emu_Write(cst816s_emu_touch_t *conf, uint8_t data)
{
   if (conf->inited)
   {
      // TODO: skip all
   }
}

void cst816s_emu_Read(cst816s_emu_touch_t *conf, uint8_t *data)
{
   if (conf->inited)
   {
      switch (conf->reg_addr)
      {
         case 0xa7:
            *data = CST816S_EMU_CHIP_ID;
         break;

         case 0xa8:
            *data = CST816S_EMU_PROJECT_ID;
         break;

         case 0xa9:
            *data = CST816S_EMU_FIRMWARE_VER;
         break;

         case 0x01:
            // gesture
            *data = CST816S_GESTURE_NONE;
         break;

         case 0x02:
            // points
            *data = 0x01;
         break;

         case 0x03:
            // event type [7:6] + msb 4bit x
            *data = 0 | ((conf->event.x >> 8) & 0xF);
         break;

         case 0x04:
            // lsb x
            *data = (conf->event.x & 0xFF);
         break;

         case 0x05:
            // msb 4bit y
            *data = ((conf->event.y >> 8) & 0xF);
         break;

         case 0x06:
            // lsb y
            *data = (conf->event.y & 0xFF);
         break;

         default:
            *data = 0xFF;
         break;
      }

      conf->reg_addr++;
   }
}

void cst816s_emu_Done(cst816s_emu_touch_t *conf)
{
   if (conf->inited)
   {
      conf->reg_addr = 0x00;
   }
}

stm32g4xx_init

Пример инициализации I2C периферии на LL, всё остальное не столь важно:

Скрытый текст
void SysInit_I2C(void)
{
   // I2C1: SCL (PA15), SDA (PB7) - Slave
   LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_15, LL_GPIO_MODE_ALTERNATE);
   LL_GPIO_SetAFPin_8_15(GPIOA, LL_GPIO_PIN_15, LL_GPIO_AF_4);
   LL_GPIO_SetPinOutputType(GPIOA, LL_GPIO_PIN_15, LL_GPIO_OUTPUT_OPENDRAIN);
   LL_GPIO_SetPinSpeed(GPIOA, LL_GPIO_PIN_15, LL_GPIO_SPEED_FREQ_HIGH);

   LL_GPIO_SetPinMode(GPIOB, LL_GPIO_PIN_7, LL_GPIO_MODE_ALTERNATE);
   LL_GPIO_SetAFPin_0_7(GPIOB, LL_GPIO_PIN_7, LL_GPIO_AF_4);
   LL_GPIO_SetPinOutputType(GPIOB, LL_GPIO_PIN_7, LL_GPIO_OUTPUT_OPENDRAIN);
   LL_GPIO_SetPinSpeed(GPIOB, LL_GPIO_PIN_7, LL_GPIO_SPEED_FREQ_HIGH);

   LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_I2C1);

   LL_I2C_InitTypeDef I2C_InitStruct;
   I2C_InitStruct.PeripheralMode = LL_I2C_MODE_I2C;
   I2C_InitStruct.Timing = 0x00E057FD; // 400kHz
   //I2C_InitStruct.Timing = 0x20B0D9FF; // 100kHz
   I2C_InitStruct.AnalogFilter = LL_I2C_ANALOGFILTER_ENABLE;
   I2C_InitStruct.DigitalFilter = 2;
   I2C_InitStruct.OwnAddress1 = 0;
   I2C_InitStruct.TypeAcknowledge = LL_I2C_ACK;
   I2C_InitStruct.OwnAddrSize = LL_I2C_OWNADDRESS1_7BIT;
   LL_I2C_Init(I2C1, &I2C_InitStruct);

   LL_I2C_EnableAutoEndMode(I2C1);
   LL_I2C_SetOwnAddress2(I2C1, 0, LL_I2C_OWNADDRESS2_NOMASK);
   LL_I2C_DisableOwnAddress2(I2C1); // TODO: ...
   LL_I2C_DisableGeneralCall(I2C1);
   LL_I2C_EnableClockStretching(I2C1);

   NVIC_SetPriority(I2C1_EV_IRQn, 0);
   NVIC_EnableIRQ(I2C1_EV_IRQn);
   NVIC_SetPriority(I2C1_ER_IRQn, 0);
   NVIC_EnableIRQ(I2C1_ER_IRQn);

   // I2C2: SCL (PA9), SDA (PA8) - Master
   LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_8, LL_GPIO_MODE_ALTERNATE);
   LL_GPIO_SetAFPin_8_15(GPIOA, LL_GPIO_PIN_8, LL_GPIO_AF_4);
   LL_GPIO_SetPinOutputType(GPIOA, LL_GPIO_PIN_8, LL_GPIO_OUTPUT_OPENDRAIN);
   LL_GPIO_SetPinSpeed(GPIOA, LL_GPIO_PIN_8, LL_GPIO_SPEED_FREQ_HIGH);

   LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_9, LL_GPIO_MODE_ALTERNATE);
   LL_GPIO_SetAFPin_8_15(GPIOA, LL_GPIO_PIN_9, LL_GPIO_AF_4);
   LL_GPIO_SetPinOutputType(GPIOA, LL_GPIO_PIN_9, LL_GPIO_OUTPUT_OPENDRAIN);
   LL_GPIO_SetPinSpeed(GPIOA, LL_GPIO_PIN_9, LL_GPIO_SPEED_FREQ_HIGH);

   LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_I2C2);

   I2C_InitStruct.OwnAddress1 = 0;
   LL_I2C_Init(I2C2, &I2C_InitStruct);

   LL_I2C_EnableAutoEndMode(I2C2);
   LL_I2C_SetOwnAddress2(I2C2, 0, LL_I2C_OWNADDRESS2_NOMASK);
   LL_I2C_DisableOwnAddress2(I2C2);
   LL_I2C_DisableGeneralCall(I2C2);
   LL_I2C_EnableClockStretching(I2C2);
}

Из важных моментов здесь:

  • Оба I2C изначально инициализируются как мастеры

  • LL_I2C_SetOwnAddress2(I2C1, 0, LL_I2C_OWNADDRESS2_NOMASK) — отключаем маску для I2C1, чтобы i2c_slave мог отвечать на любой адрес, но в реальном проекте стоит пересмотреть эту строку

  • I2C1 после базовой инициализации переключается дальше в режим slave, и там же включаются прерывания от него (это прекрасно работает, когда, например, нужно сначала инициализировать как мастер FPD микросхему, активируя проброс I2C поверх FPD, а потом уже по этому же I2C отвечать как ведомое устройство)

  • I2C2 работает вообще без прерываний (самописный драйвер)

stm32g4xx_it

Пример базовой реализации вызова колбеков для I2C1:

#include "i2c_slave.h"
extern i2c_slave_t i2cs;

void I2C1_EV_IRQHandler(void)
{
   if (LL_I2C_IsActiveFlag_ADDR(i2cs.i2c))
   {
      LL_I2C_ClearFlag_ADDR(i2cs.i2c);
      i2cSlave_IrqAddrSet(&i2cs, (LL_I2C_GetAddressMatchCode(i2cs.i2c) >> 1));
   }
   else if (LL_I2C_IsActiveFlag_NACK(i2cs.i2c))
   {
      LL_I2C_ClearFlag_NACK(i2cs.i2c);
      i2cSlave_IrqNack(&i2cs);
   }
   else if (LL_I2C_IsActiveFlag_TXIS(i2cs.i2c))
   {
      i2cSlave_IrqTransmit(&i2cs);
   }
   else if (LL_I2C_IsActiveFlag_RXNE(i2cs.i2c))
   {
      i2cSlave_IrqRecive(&i2cs);
   }
   else if (LL_I2C_IsActiveFlag_STOP(i2cs.i2c))
   {
      LL_I2C_ClearFlag_STOP(i2cs.i2c);
      if (!LL_I2C_IsActiveFlag_TXE(i2cs.i2c))
      {
         LL_I2C_ClearFlag_TXE(i2cs.i2c);
      }

      i2cSlave_IrqComplete(&i2cs);
   }
}

void I2C1_ER_IRQHandler(void)
{
   i2cSlave_IrqError(&i2cs);
}

Главное, не забываем флаги сбрасывать. :-)

main

Ну и, собственно, вот так вызываем это всё добро:

Скрытый текст
#include "main.h"
#include "stm32g4xx_init.h"

#include "cst816s.h"
cst816s_touch_t touch;
volatile uint8_t touch_int = 0;

#include "i2c_slave.h"
i2c_slave_t i2cs;

#include "cst816s_emu.h"
cst816s_emu_touch_t touch_emu;

int main(void)
{
   SysInit_Clock();
   SysTick_Config(SystemCoreClock / 1000U); // 1ms // No OS
   SysInit_GPIO();
   SysInit_I2C();

   SysView_Init();
   SysView_PrintInfo();

   cst816s_Init(&touch, I2C2, CST816S_DEFAULT_ADDRESS);

   i2cSlave_Init(&i2cs, I2C1);
   i2cSlave_Enable(&i2cs);

   touch_emu.int_port = GPIOB;
   touch_emu.int_pin = LL_GPIO_PIN_5;
   cst816s_emu_Init(&touch_emu, I2C1, CST816S_EMU_DEFAULT_ADDRESS);
   i2cSlave_Register(&i2cs, CST816S_EMU_DEFAULT_ADDRESS, &touch_emu,
                     &cst816s_emu_Reg,
                     &cst816s_emu_Write,
                     &cst816s_emu_Read,
                     &cst816s_emu_Done);

   for (;;)
   {
      cst816s_emu_Routine(&touch_emu);
      if (touch_int)
      {
         touch_int = 0;
         cst816s_ReadEvent(&touch);
         cst816s_emu_NewEvent(&touch_emu, touch.event.x, touch.event.y);
      }
   }
}

Проверка

Бахаем теперь на столе стенд по блок-схеме:

Ещё и запитываем от J-Link всё.
Ещё и запитываем от J-Link всё.

Попервой работало как-то так (выше привёл уже исправленные исходники):

Когда мастер-плата пробует работать с эмулятором
Когда мастер-плата пробует работать с эмулятором

Но спустя небольшое время получаем:

Вот мы и получили троллейбус из буханки... работает!
Вот мы и получили троллейбус из буханки... работает!

Вот так это выглядит в консоли:

RTT
RTT

sysview_init (бонус)

Чисто допом, вдруг кому пригодится — реализация небольшой надстройки над Segger RTT, чтобы удобно инитить и выводить цветные буковки в RTT, как на записи экрана выше:

sysview_init.h

Скрытый текст
#ifndef _SYSTEMVIEW_INIT_H_
#define _SYSTEMVIEW_INIT_H_

#include "stm32g4xx.h"
#include <stdio.h>
#include "main.h"

#define SYSVIEW_APP_NAME                     "LLSDK App"
#define SYSVIEW_DEVICE_NAME                  "STM32G431CBU6"
#define SEGGER_SYSVIEW_EXCLUDE_PRINTF        1
#define SEGGER_RTT_MODE_DEFAULT              SEGGER_RTT_MODE_NO_BLOCK_SKIP
#define SYSVIEW_NUM_TASKS                    16 // No OS

#ifndef USE_CYCCNT_TIMESTAMP
#define USE_CYCCNT_TIMESTAMP                 1
#endif

#ifndef ENABLE_DWT_CYCCNT
#define ENABLE_DWT_CYCCNT                    (USE_CYCCNT_TIMESTAMP & SEGGER_SYSVIEW_POST_MORTEM_MODE)
#endif

#define DWT_CTRL                             (*(volatile unsigned long*) (0xE0001000uL))
#define NOCYCCNT_BIT                         (1uL << 25)
#define CYCCNTENA_BIT                        (1uL << 0)

#if(LIB_COMMON_SYSVIEW)

#include "SEGGER_SYSVIEW.h"
#include "SEGGER_SYSVIEW_Conf.h"
#include "SEGGER_RTT.h"
#include "SEGGER_RTT_Conf.h"

#define SysView_ExecuteTask(t)               SEGGER_SYSVIEW_OnTaskStartExec((U32)t); \
                                             t(); \
                                             SEGGER_SYSVIEW_OnTaskStopReady((U32)t, 0);

#define SysView_LogInfo                      SEGGER_SYSVIEW_PrintfTarget
#define SysView_LogWarning                   SEGGER_SYSVIEW_WarnfTarget
#define SysView_LogError                     SEGGER_SYSVIEW_ErrorfTarget
#define SysView_LogSuccess                   SEGGER_SYSVIEW_PrintfTarget
#define SysView_EnterISR                     SEGGER_SYSVIEW_RecordEnterISR
#define SysView_ExitISR                      SEGGER_SYSVIEW_RecordExitISR

#elif(LIB_COMMON_RTT)

#include "SEGGER_RTT.h"
#include "SEGGER_RTT_Conf.h"

#define SysView_ExecuteTask(t)               t();
#define SysView_LogInfo(FORMAT, ...)         SEGGER_RTT_printf(0, FORMAT, ##__VA_ARGS__)
#define SysView_LogWarning(FORMAT, ...)      SEGGER_RTT_printf(0, RTT_CTRL_TEXT_BRIGHT_YELLOW FORMAT RTT_CTRL_RESET, ##__VA_ARGS__)
#define SysView_LogError(FORMAT, ...)        SEGGER_RTT_printf(0, RTT_CTRL_TEXT_BRIGHT_RED FORMAT RTT_CTRL_RESET, ##__VA_ARGS__)
#define SysView_LogSuccess(FORMAT, ...)      SEGGER_RTT_printf(0, RTT_CTRL_TEXT_BRIGHT_GREEN FORMAT RTT_CTRL_RESET, ##__VA_ARGS__)
#define SysView_EnterISR()
#define SysView_ExitISR()

#else

#define SysView_ExecuteTask(t)               t();
#define SysView_LogInfo(...)
#define SysView_LogWarning(...)
#define SysView_LogError(...)
#define SysView_LogSuccess(...)
#define SysView_EnterISR()
#define SysView_ExitISR()

#endif

void SysView_Init(void);
void SysView_PrintInfo(void);
void SysView_AddTask(void* pTask, const char* sName, uint32_t Prio);
char SysView_WaitForInputChar(void);

#endif // _SYSTEMVIEW_INIT_H_

sysview_init.c

Скрытый текст
#include "sysview_init.h"
#include "build_info.h"

#if(LIB_COMMON_SYSVIEW)
static void _cb_SendTaskList(void);
static void _cb_SendSystemDesc(void);

static const SEGGER_SYSVIEW_OS_API SysView_API = { NULL, _cb_SendTaskList };
static SEGGER_SYSVIEW_TASKINFO SysView_TasksInfo[SYSVIEW_NUM_TASKS];
static uint32_t SysView_TasksNum = 0;

static void _cb_SendSystemDesc(void)
{
   SEGGER_SYSVIEW_SendSysDesc("N="SYSVIEW_APP_NAME",D="SYSVIEW_DEVICE_NAME);
   SEGGER_SYSVIEW_SendSysDesc(
   //",I#15=SysTick"
   );
}

static void _cb_SendTaskList(void)
{
   for (uint32_t n = 0; n < SysView_TasksNum; n++)
      SEGGER_SYSVIEW_SendTaskInfo(&SysView_TasksInfo[n]);
}

void SysView_AddTask(void* pTask, const char* sName, uint32_t Prio)
{
   uint32_t n;
   SEGGER_SYSVIEW_OnTaskCreate((U32)pTask);

   if (SysView_TasksNum > SYSVIEW_NUM_TASKS)
      return;

   n = SysView_TasksNum;
   SysView_TasksNum++;

   SysView_TasksInfo[n].TaskID      = (U32)pTask;
   SysView_TasksInfo[n].sName       = sName;
   SysView_TasksInfo[n].Prio        = Prio;
   SysView_TasksInfo[n].StackBase   = 0;
   SysView_TasksInfo[n].StackSize   = 0;
}
#else
void SysView_AddTask(void* pTask, const char* sName, uint32_t Prio)
{
}
#endif // LIB_COMMON_SYSVIEW

void SysView_Init(void)
{
#if(LIB_COMMON_SYSVIEW)
#if(SEGGER_SYSVIEW_CORE == SEGGER_SYSVIEW_CORE_CM3)
   if (((DWT_CTRL & NOCYCCNT_BIT) == 0) && ((DWT_CTRL & CYCCNTENA_BIT) == 0))
   {
      DWT_CTRL |= CYCCNTENA_BIT;
   }
#endif // SEGGER_SYSVIEW_CORE

   SEGGER_SYSVIEW_Init(SystemCoreClock, SystemCoreClock, &SysView_API, _cb_SendSystemDesc);
   SEGGER_SYSVIEW_SetRAMBase(SYSVIEW_BASE_RAM_ADDR);
#elif(LIB_COMMON_RTT)
   SEGGER_RTT_Init();
#endif // LIB_COMMON_SYSVIEW
}

#if(LIB_COMMON_SYSVIEW || LIB_COMMON_RTT)
char SysView_WaitForInputChar(void)
{
   while(SEGGER_RTT_HasKey() == 0) ;
   return SEGGER_RTT_GetKey();
}
#else
char SysView_WaitForInputChar(void)
{
   return 0x00;
}
#endif

void SysView_PrintInfo(void)
{
   SysView_LogInfo(RTT_CTRL_CLEAR);
   SysView_LogInfo("Compiled:    %s\r\n", COMPILE_DATE);
   SysView_LogInfo("Compiler:    %s\r\n", COMPILER_VERSION);
#ifdef xPortSysTickHandler // FreeRTOS
   SysView_LogInfo("FreeRTOS:    %s\r\n", tskKERNEL_VERSION_NUMBER);
#endif
   SysView_LogInfo("Device:      %s\r\n", SYSVIEW_DEVICE_NAME);
   SysView_LogInfo("App Name:    %s\r\n", SYSVIEW_APP_NAME);
   SysView_LogInfo("Core Clock:  %u Hz (%u MHz)\r\n\r\n", SystemCoreClock, SystemCoreClock / 1000000);
}

Подписывайтесь на канал (ссылки нет), ставьте классы, обсирайте в комментах. :3

Комментарии (0)