Понадобилось мне для проекта по приколу сделать i2c slave (ведомого устройства), но не просто эмуляцию одного устройства (например eeprom), а сразу эдакого эмулятора с, если можно это так назвать, API, к которому можно уже привязывать эмуляции конкретных реализаций устройств на произвольные адреса.
На тему реализации ведомых устройств на STM32 с использованием LL я как-то не особо много инфы нарыл, в итоге накостылил, как сам понял :D Тест отвёрткой проходит, хотя первые реализации иногда прям залипали намертво.
В статье я не буду глубоко описывать регистры, саму работу шины, прочее, этого добра навалом. Просто окунёмся в дип дарк фэнтези реализацию эмулятора простейшего тача cst816s.
Для тестирования нам понадобится примерно подобный стенд:

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

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 если нет реализации эмулятора, а мастер запрашивает байт, то 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);
}
}
}
Проверка
Бахаем теперь на столе стенд по блок-схеме:

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

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

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

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