Initial import of firmware and host projects

This commit is contained in:
2026-04-10 16:53:41 +08:00
commit 42a36164be
124 changed files with 13943 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
build*/
external*/
.vs/
**/.vs/
x64/
**/x64/
*.user
*.suo
*.pdb
*.idb
*.ilk
*.recipe
*.tlog
*.lastbuildstate
*.vcxproj.FileListAbsolute.txt
qt_work.log

72
CMakeLists.txt Normal file
View File

@@ -0,0 +1,72 @@
cmake_minimum_required(VERSION 3.20.0)
set(IP5306_LOCAL_MODULE "${CMAKE_CURRENT_SOURCE_DIR}/modules/ip5306")
set(IP5306_EXTERNAL_MODULE "E:/extra/modules/ip5306")
set(extra_zephyr_modules "$CACHE{ZEPHYR_EXTRA_MODULES}")
if(EXISTS "${IP5306_LOCAL_MODULE}/zephyr/module.yml")
list(APPEND extra_zephyr_modules "${IP5306_LOCAL_MODULE}")
elseif(EXISTS "${IP5306_EXTERNAL_MODULE}/zephyr/module.yml")
list(APPEND extra_zephyr_modules "${IP5306_EXTERNAL_MODULE}")
endif()
list(REMOVE_DUPLICATES extra_zephyr_modules)
set(ZEPHYR_EXTRA_MODULES "${extra_zephyr_modules}" CACHE STRING "Extra Zephyr modules" FORCE)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(new_kbd)
zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/inc)
zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/events)
zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/ui)
zephyr_compile_definitions(
LV_LVGL_H_INCLUDE_SIMPLE=1
LV_FONT_MONTSERRAT_14=0
LV_FONT_UNSCII_8=0
LV_FONT_DEFAULT=\&ui_font_keyboard_small_18
"LV_FONT_CUSTOM_DECLARE=LV_FONT_DECLARE(ui_font_keyboard_small_18) LV_FONT_DECLARE(ui_font_keyboard_time_48)"
)
target_compile_definitions(app PRIVATE
APP_HID_KEYMAP_DEF_PATH=\"hid_keymap_def.h\"
)
target_sources(app PRIVATE
src/main.c
src/events/battery_status_event.c
src/events/config_event.c
src/events/display_theme_event.c
src/events/hid_boot_event.c
src/events/hid_host_ack_event.c
src/events/hid_host_command_error_event.c
src/events/hid_host_command_event.c
src/events/hid_protocol_event.c
src/events/hid_report_event.c
src/events/hid_tx_done_event.c
src/events/hid_tx_event.c
src/events/hid_vendor_mask_event.c
src/events/keyboard_led_event.c
src/events/mode_event.c
src/events/qdec_step_event.c
src/events/time_sync_event.c
src/modules/battery_module.c
src/modules/ble_adv_ctrl_module.c
src/modules/ble_battery_module.c
src/modules/ble_bond_module.c
src/modules/ble_slot_ctrl_module.c
src/modules/display_module.c
src/modules/hid_host_command_module.c
src/modules/hid_tx_manager_module.c
src/modules/keyboard_module.c
src/modules/led_state_module.c
src/modules/mode_switch_module.c
src/modules/qdec_module.c
src/modules/time_manager_module.c
src/modules/usb_hid_module.c
src/modules/ble_hid_module.c
src/ui/display_ui.c
src/ui/fonts/ui_font_keyboard_small_18.c
src/ui/fonts/ui_font_keyboard_time_48.c
)

17
Kconfig Normal file
View File

@@ -0,0 +1,17 @@
menu "new_kbd"
config NEW_KBD_BLE_BOND_ENABLE
bool "Enable app-specific BLE bond support"
default y
depends on BT_BONDABLE
depends on BT_SETTINGS
depends on CAF_SETTINGS_LOADER
depends on CAF_BLE_COMMON_EVENTS
select CAF_BLE_BOND_SUPPORTED
help
Inform CAF modules that the application provides its own BLE bond
implementation and advertising must wait for it to become ready.
endmenu
source "Kconfig.zephyr"

6
KeyBorad/KeyBorad.slnx Normal file
View File

@@ -0,0 +1,6 @@
<Solution>
<Configurations>
<Platform Name="x64" />
</Configurations>
<Project Path="KeyBorad/KeyBorad.vcxproj" Id="175bed2a-ed45-4ddc-a67b-7c4042190634" />
</Solution>

View File

@@ -0,0 +1,121 @@
#pragma once
#include <QtCore/QByteArray>
#include <QtCore/QtGlobal>
struct Packet_len
{
static const quint8 Com_Len_UnKnow = 0;
static const quint8 Com_Len_HelloReq = 1;
static const quint8 Com_Len_HelloRsp = 9;
static const quint8 Com_Len_Bitmap = 29;
static const quint8 Com_Len_FunctionKeyEvent = 3;
static const quint8 Com_Len_LedState = 1;
static const quint8 Com_Len_TimeSync = 16;
static const quint8 Com_Len_ThemeRgb = 3;
static const quint8 Com_Len_Ack = 1;
static const quint8 Com_Len_Error = 2;
};
enum Packet_Type : quint8
{
Com_Type_UnKnow = 0x00,
Com_Type_HelloReq = 0x01,
Com_Type_HelloRsp = 0x02,
Com_Type_Bitmap = 0x10,
Com_Type_FunctionKeyEvent = 0x20,
Com_Type_LedState = 0x21,
Com_Type_TimeSync = 0x30,
Com_Type_ThemeRgb = 0x31,
Com_Type_Ack = 0x7E,
Com_Type_Error = 0x7F
};
enum Key_Action : quint8
{
Key_Action_Release = 0x00,
Key_Action_Press = 0x01
};
enum Error_Code : quint8
{
Error_Code_None = 0x00,
Error_Code_UnknownType = 0x01,
Error_Code_InvalidLength = 0x02,
Error_Code_InvalidParam = 0x03,
Error_Code_NotReady = 0x04
};
struct Packet
{
quint8 Com_Packet_Head1 = 0xAA;
quint8 Com_Packet_Head2 = 0x55;
quint8 len = Packet_len::Com_Len_UnKnow;
Packet_Type type = Com_Type_UnKnow;
QByteArray data;
};
struct Packet_HelloReq
{
quint8 ProtocolVersion = 0x01;
};
struct Packet_HelloRsp
{
quint8 ProtocolVersion = 0x01;
quint16 VendorId = 0;
quint16 ProductId = 0;
quint8 FirmwareMajor = 0;
quint8 FirmwareMinor = 0;
quint16 CapabilityFlags = 0;
};
struct Packet_Bitmap
{
QByteArray UsageBitmap = QByteArray(Packet_len::Com_Len_Bitmap, 0);
};
struct Packet_FunctionKeyEvent
{
quint16 Usage = 0;
quint8 Action = Key_Action_Release;
};
struct Packet_LedState
{
quint8 LedMask = 0;
};
struct Packet_TimeSync
{
quint8 Version = 1;
quint8 Flags = 0;
quint16 TimezoneMin = 0;
quint64 UtcMs = 0;
quint32 AccuracyMs = 0;
};
struct Packet_ThemeRgb
{
quint8 Red = 0;
quint8 Green = 0;
quint8 Blue = 0;
};
struct Packet_Ack
{
quint8 AckedType = 0;
};
struct Packet_Error
{
quint8 ErrorType = 0;
quint8 ErrorCode = Error_Code_None;
};

View File

@@ -0,0 +1,198 @@
#include "COM/Com_CdcDecode.h"
#include "COM/Com_CdcEncode.h"
namespace
{
constexpr quint8 COM_CDC_CONST_PACKET_HEAD1 = 0xAA;
constexpr quint8 COM_CDC_CONST_PACKET_HEAD2 = 0x55;
constexpr int COM_CDC_CONST_FRAME_OVERHEAD = 5;
constexpr int COM_CDC_CONST_MAX_PAYLOAD_LENGTH = 64;
bool Com_Cdc_Func_IsKnownLengthValid(Packet_Type Type, quint8 DataLength)
{
switch (Type)
{
case Com_Type_UnKnow:
return DataLength == Packet_len::Com_Len_UnKnow;
case Com_Type_HelloReq:
return DataLength == Packet_len::Com_Len_HelloReq;
case Com_Type_HelloRsp:
return DataLength == Packet_len::Com_Len_HelloRsp;
case Com_Type_Bitmap:
return DataLength == Packet_len::Com_Len_Bitmap;
case Com_Type_FunctionKeyEvent:
return DataLength == Packet_len::Com_Len_FunctionKeyEvent;
case Com_Type_LedState:
return DataLength == Packet_len::Com_Len_LedState;
case Com_Type_TimeSync:
return DataLength == Packet_len::Com_Len_TimeSync;
case Com_Type_ThemeRgb:
return DataLength == Packet_len::Com_Len_ThemeRgb;
case Com_Type_Ack:
return DataLength == Packet_len::Com_Len_Ack;
case Com_Type_Error:
return DataLength == Packet_len::Com_Len_Error;
default:
return true;
}
}
bool Com_Cdc_Func_ParseFrameAtStart(const QByteArray& ByteArray, Packet* p_Packet)
{
if ((p_Packet == nullptr) || (ByteArray.size() < COM_CDC_CONST_FRAME_OVERHEAD))
{
return false;
}
if (static_cast<quint8>(ByteArray.at(0)) != COM_CDC_CONST_PACKET_HEAD1)
{
return false;
}
if (static_cast<quint8>(ByteArray.at(1)) != COM_CDC_CONST_PACKET_HEAD2)
{
return false;
}
const quint8 DataLength = static_cast<quint8>(ByteArray.at(2));
if (DataLength > COM_CDC_CONST_MAX_PAYLOAD_LENGTH)
{
return false;
}
const Packet_Type Type =
static_cast<Packet_Type>(static_cast<quint8>(ByteArray.at(3)));
if (!Com_Cdc_Func_IsKnownLengthValid(Type, DataLength))
{
return false;
}
const int FrameLength = COM_CDC_CONST_FRAME_OVERHEAD + static_cast<int>(DataLength);
if (ByteArray.size() != FrameLength)
{
return false;
}
const QByteArray Body = ByteArray.left(FrameLength - 1);
const quint8 Checksum = Com_Cdc_Func_CalcChecksum(Body);
const quint8 ChecksumRx = static_cast<quint8>(ByteArray.at(FrameLength - 1));
if (Checksum != ChecksumRx)
{
return false;
}
p_Packet->Com_Packet_Head1 = COM_CDC_CONST_PACKET_HEAD1;
p_Packet->Com_Packet_Head2 = COM_CDC_CONST_PACKET_HEAD2;
p_Packet->len = DataLength;
p_Packet->type = Type;
p_Packet->data = ByteArray.mid(4, DataLength);
return true;
}
int Com_Cdc_Func_FindHead(const QByteArray& ByteArray)
{
for (int Index = 0; Index + 1 < ByteArray.size(); ++Index)
{
if ((static_cast<quint8>(ByteArray.at(Index)) == COM_CDC_CONST_PACKET_HEAD1) &&
(static_cast<quint8>(ByteArray.at(Index + 1)) == COM_CDC_CONST_PACKET_HEAD2))
{
return Index;
}
}
return -1;
}
void Com_Cdc_Func_KeepTailForNextScan(QByteArray* p_Buffer)
{
if (!p_Buffer->isEmpty() &&
(static_cast<quint8>(p_Buffer->at(p_Buffer->size() - 1)) == COM_CDC_CONST_PACKET_HEAD1))
{
*p_Buffer = QByteArray(1, static_cast<char>(COM_CDC_CONST_PACKET_HEAD1));
return;
}
p_Buffer->clear();
}
} // namespace
bool Com_Cdc_Func_ParseFrame(const QByteArray& ByteArray, Packet* p_Packet)
{
return Com_Cdc_Func_ParseFrameAtStart(ByteArray, p_Packet);
}
bool Com_Cdc_Func_TryTakeFrame(QByteArray* p_Buffer, Packet* p_Packet)
{
if ((p_Buffer == nullptr) || (p_Packet == nullptr))
{
return false;
}
while (true)
{
if (p_Buffer->size() < 2)
{
return false;
}
const int HeadIndex = Com_Cdc_Func_FindHead(*p_Buffer);
if (HeadIndex < 0)
{
Com_Cdc_Func_KeepTailForNextScan(p_Buffer);
return false;
}
if (HeadIndex > 0)
{
p_Buffer->remove(0, HeadIndex);
}
if (p_Buffer->size() < 4)
{
return false;
}
const quint8 DataLength = static_cast<quint8>(p_Buffer->at(2));
if (DataLength > COM_CDC_CONST_MAX_PAYLOAD_LENGTH)
{
p_Buffer->remove(0, 1);
continue;
}
const Packet_Type Type =
static_cast<Packet_Type>(static_cast<quint8>(p_Buffer->at(3)));
if (!Com_Cdc_Func_IsKnownLengthValid(Type, DataLength))
{
p_Buffer->remove(0, 1);
continue;
}
const int FrameLength = COM_CDC_CONST_FRAME_OVERHEAD + static_cast<int>(DataLength);
if (p_Buffer->size() < FrameLength)
{
return false;
}
const QByteArray FrameBytes = p_Buffer->left(FrameLength);
if (!Com_Cdc_Func_ParseFrameAtStart(FrameBytes, p_Packet))
{
p_Buffer->remove(0, 1);
continue;
}
p_Buffer->remove(0, FrameLength);
return true;
}
}

View File

@@ -0,0 +1,8 @@
#pragma once
#include <QtCore/QByteArray>
#include "COM/Com_Cdc.h"
bool Com_Cdc_Func_ParseFrame(const QByteArray& ByteArray, Packet* p_Packet);
bool Com_Cdc_Func_TryTakeFrame(QByteArray* p_Buffer, Packet* p_Packet);

View File

@@ -0,0 +1,159 @@
#include "COM/Com_CdcEncode.h"
namespace
{
Packet Com_Cdc_Func_MakePacket(Packet_Type Type, const QByteArray& Data)
{
Packet PacketData;
PacketData.type = Type;
PacketData.len = static_cast<quint8>(Data.size());
PacketData.data = Data;
return PacketData;
}
void Com_Cdc_Func_WriteLe16(QByteArray* p_Data, int Offset, quint16 Value)
{
(*p_Data)[Offset] = static_cast<char>(Value & 0x00FFU);
(*p_Data)[Offset + 1] = static_cast<char>((Value >> 8) & 0x00FFU);
}
void Com_Cdc_Func_WriteLe32(QByteArray* p_Data, int Offset, quint32 Value)
{
for (int Index = 0; Index < 4; ++Index)
{
(*p_Data)[Offset + Index] = static_cast<char>((Value >> (Index * 8)) & 0xFFU);
}
}
void Com_Cdc_Func_WriteLe64(QByteArray* p_Data, int Offset, quint64 Value)
{
for (int Index = 0; Index < 8; ++Index)
{
(*p_Data)[Offset + Index] = static_cast<char>((Value >> (Index * 8)) & 0xFFULL);
}
}
QByteArray Com_Cdc_Func_FitPayload(const QByteArray& Data, int ExpectedLength)
{
QByteArray Payload = Data.left(ExpectedLength);
if (Payload.size() < ExpectedLength)
{
Payload.append(ExpectedLength - Payload.size(), 0);
}
return Payload;
}
} // namespace
quint8 Com_Cdc_Func_CalcChecksum(const QByteArray& ByteArray)
{
quint8 Checksum = 0;
for (int Index = 0; Index < ByteArray.size(); ++Index)
{
const quint8 ByteValue = static_cast<quint8>(ByteArray.at(Index));
Checksum = static_cast<quint8>(Checksum ^ ByteValue);
}
return Checksum;
}
QByteArray Com_Cdc_Func_BuildFrame(const Packet& PacketData)
{
if (PacketData.data.size() > 0xFF)
{
return QByteArray();
}
QByteArray ByteArray;
const quint8 DataLength = static_cast<quint8>(PacketData.data.size());
ByteArray.append(static_cast<char>(PacketData.Com_Packet_Head1));
ByteArray.append(static_cast<char>(PacketData.Com_Packet_Head2));
ByteArray.append(static_cast<char>(DataLength));
ByteArray.append(static_cast<char>(PacketData.type));
ByteArray.append(PacketData.data);
const quint8 Checksum = Com_Cdc_Func_CalcChecksum(ByteArray);
ByteArray.append(static_cast<char>(Checksum));
return ByteArray;
}
QByteArray Com_Cdc_Func_BuildHelloReq(const Packet_HelloReq& PacketData)
{
QByteArray Payload(Packet_len::Com_Len_HelloReq, 0);
Payload[0] = static_cast<char>(PacketData.ProtocolVersion);
return Com_Cdc_Func_BuildFrame(Com_Cdc_Func_MakePacket(Com_Type_HelloReq, Payload));
}
QByteArray Com_Cdc_Func_BuildHelloRsp(const Packet_HelloRsp& PacketData)
{
QByteArray Payload(Packet_len::Com_Len_HelloRsp, 0);
Payload[0] = static_cast<char>(PacketData.ProtocolVersion);
Com_Cdc_Func_WriteLe16(&Payload, 1, PacketData.VendorId);
Com_Cdc_Func_WriteLe16(&Payload, 3, PacketData.ProductId);
Payload[5] = static_cast<char>(PacketData.FirmwareMajor);
Payload[6] = static_cast<char>(PacketData.FirmwareMinor);
Com_Cdc_Func_WriteLe16(&Payload, 7, PacketData.CapabilityFlags);
return Com_Cdc_Func_BuildFrame(Com_Cdc_Func_MakePacket(Com_Type_HelloRsp, Payload));
}
QByteArray Com_Cdc_Func_BuildBitmap(const Packet_Bitmap& PacketData)
{
return Com_Cdc_Func_BuildFrame(
Com_Cdc_Func_MakePacket(
Com_Type_Bitmap,
Com_Cdc_Func_FitPayload(PacketData.UsageBitmap, Packet_len::Com_Len_Bitmap)));
}
QByteArray Com_Cdc_Func_BuildFunctionKeyEvent(const Packet_FunctionKeyEvent& PacketData)
{
QByteArray Payload(Packet_len::Com_Len_FunctionKeyEvent, 0);
Com_Cdc_Func_WriteLe16(&Payload, 0, PacketData.Usage);
Payload[2] = static_cast<char>(PacketData.Action);
return Com_Cdc_Func_BuildFrame(
Com_Cdc_Func_MakePacket(Com_Type_FunctionKeyEvent, Payload));
}
QByteArray Com_Cdc_Func_BuildLedState(const Packet_LedState& PacketData)
{
QByteArray Payload(Packet_len::Com_Len_LedState, 0);
Payload[0] = static_cast<char>(PacketData.LedMask);
return Com_Cdc_Func_BuildFrame(Com_Cdc_Func_MakePacket(Com_Type_LedState, Payload));
}
QByteArray Com_Cdc_Func_BuildTimeSync(const Packet_TimeSync& PacketData)
{
QByteArray Payload(Packet_len::Com_Len_TimeSync, 0);
Payload[0] = static_cast<char>(PacketData.Version);
Payload[1] = static_cast<char>(PacketData.Flags);
Com_Cdc_Func_WriteLe16(&Payload, 2, PacketData.TimezoneMin);
Com_Cdc_Func_WriteLe64(&Payload, 4, PacketData.UtcMs);
Com_Cdc_Func_WriteLe32(&Payload, 12, PacketData.AccuracyMs);
return Com_Cdc_Func_BuildFrame(Com_Cdc_Func_MakePacket(Com_Type_TimeSync, Payload));
}
QByteArray Com_Cdc_Func_BuildThemeRgb(const Packet_ThemeRgb& PacketData)
{
QByteArray Payload(Packet_len::Com_Len_ThemeRgb, 0);
Payload[0] = static_cast<char>(PacketData.Red);
Payload[1] = static_cast<char>(PacketData.Green);
Payload[2] = static_cast<char>(PacketData.Blue);
return Com_Cdc_Func_BuildFrame(Com_Cdc_Func_MakePacket(Com_Type_ThemeRgb, Payload));
}
QByteArray Com_Cdc_Func_BuildAck(const Packet_Ack& PacketData)
{
QByteArray Payload(Packet_len::Com_Len_Ack, 0);
Payload[0] = static_cast<char>(PacketData.AckedType);
return Com_Cdc_Func_BuildFrame(Com_Cdc_Func_MakePacket(Com_Type_Ack, Payload));
}
QByteArray Com_Cdc_Func_BuildError(const Packet_Error& PacketData)
{
QByteArray Payload(Packet_len::Com_Len_Error, 0);
Payload[0] = static_cast<char>(PacketData.ErrorType);
Payload[1] = static_cast<char>(PacketData.ErrorCode);
return Com_Cdc_Func_BuildFrame(Com_Cdc_Func_MakePacket(Com_Type_Error, Payload));
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <QtCore/QByteArray>
#include "COM/Com_Cdc.h"
quint8 Com_Cdc_Func_CalcChecksum(const QByteArray& ByteArray);
QByteArray Com_Cdc_Func_BuildFrame(const Packet& PacketData);
QByteArray Com_Cdc_Func_BuildHelloReq(const Packet_HelloReq& PacketData);
QByteArray Com_Cdc_Func_BuildHelloRsp(const Packet_HelloRsp& PacketData);
QByteArray Com_Cdc_Func_BuildBitmap(const Packet_Bitmap& PacketData);
QByteArray Com_Cdc_Func_BuildFunctionKeyEvent(const Packet_FunctionKeyEvent& PacketData);
QByteArray Com_Cdc_Func_BuildLedState(const Packet_LedState& PacketData);
QByteArray Com_Cdc_Func_BuildTimeSync(const Packet_TimeSync& PacketData);
QByteArray Com_Cdc_Func_BuildThemeRgb(const Packet_ThemeRgb& PacketData);
QByteArray Com_Cdc_Func_BuildAck(const Packet_Ack& PacketData);
QByteArray Com_Cdc_Func_BuildError(const Packet_Error& PacketData);

View File

@@ -0,0 +1,227 @@
#include "DRI/Dri_Cdc.h"
#include "COM/Com_Cdc.h"
#include "COM/Com_CdcDecode.h"
#include "COM/Com_CdcEncode.h"
#include <QtCore/QElapsedTimer>
#include <QtCore/QtGlobal>
#include <QtSerialPort/QSerialPort>
#include <QtSerialPort/QSerialPortInfo>
namespace
{
bool Dri_Cdc_Func_TryReadFrame(
QSerialPort* p_Serial,
QByteArray* p_ReadBuffer,
int TimeoutMs,
Packet* p_Packet)
{
if ((p_Serial == nullptr) || (p_ReadBuffer == nullptr) || (p_Packet == nullptr))
{
return false;
}
if (Com_Cdc_Func_TryTakeFrame(p_ReadBuffer, p_Packet))
{
return true;
}
QElapsedTimer Timer;
Timer.start();
while (Timer.elapsed() < TimeoutMs)
{
const int RemainingMs = TimeoutMs - static_cast<int>(Timer.elapsed());
const int WaitMs = qMin(20, RemainingMs);
if (WaitMs <= 0)
{
break;
}
if (!p_Serial->waitForReadyRead(WaitMs))
{
continue;
}
const QByteArray ReadBytes = p_Serial->readAll();
if (!ReadBytes.isEmpty())
{
p_ReadBuffer->append(ReadBytes);
}
if (Com_Cdc_Func_TryTakeFrame(p_ReadBuffer, p_Packet))
{
return true;
}
}
return false;
}
} // namespace
QVector<Dri_Cdc_Struct_PortInfo> Dri_Cdc_Enum()
{
QVector<Dri_Cdc_Struct_PortInfo> PortList;
const QList<QSerialPortInfo> InfoList = QSerialPortInfo::availablePorts();
for (int Index = 0; Index < InfoList.size(); ++Index)
{
const QSerialPortInfo& Info = InfoList.at(Index);
Dri_Cdc_Struct_PortInfo PortInfo;
PortInfo.PortName = Info.portName();
PortInfo.Description = Info.description();
PortInfo.Manufacturer = Info.manufacturer();
PortList.append(PortInfo);
}
return PortList;
}
void Dri_Cdc_Deinit(Dri_Cdc_Struct_Port* p_Port)
{
if (p_Port->p_Port != nullptr)
{
if (p_Port->p_Port->isOpen())
{
p_Port->p_Port->close();
}
delete p_Port->p_Port;
}
*p_Port = Dri_Cdc_Struct_Port();
}
bool Dri_Cdc_Init(
Dri_Cdc_Struct_Port* p_Port,
const Dri_Cdc_Struct_InitConfig& Config)
{
Dri_Cdc_Deinit(p_Port);
const QVector<Dri_Cdc_Struct_PortInfo> PortList = Dri_Cdc_Enum();
if (PortList.isEmpty())
{
return false;
}
Packet_HelloReq HelloReq;
const QByteArray HelloReqFrame = Com_Cdc_Func_BuildHelloReq(HelloReq);
for (int Index = 0; Index < PortList.size(); ++Index)
{
const Dri_Cdc_Struct_PortInfo& PortInfo = PortList.at(Index);
QSerialPort* p_Serial = new QSerialPort();
p_Serial->setPortName(PortInfo.PortName);
p_Serial->setBaudRate(Config.BaudRate);
p_Serial->setDataBits(QSerialPort::Data8);
p_Serial->setParity(QSerialPort::NoParity);
p_Serial->setStopBits(QSerialPort::OneStop);
p_Serial->setFlowControl(QSerialPort::NoFlowControl);
if (!p_Serial->open(QIODevice::ReadWrite))
{
delete p_Serial;
continue;
}
p_Serial->readAll();
if (p_Serial->write(HelloReqFrame) != HelloReqFrame.size())
{
p_Serial->close();
delete p_Serial;
continue;
}
if (!p_Serial->waitForBytesWritten(Config.HandshakeTimeoutMs))
{
p_Serial->close();
delete p_Serial;
continue;
}
QByteArray HandshakeBuffer;
QElapsedTimer Timer;
Timer.start();
while (Timer.elapsed() < Config.HandshakeTimeoutMs)
{
Packet PacketData;
const int RemainingMs =
Config.HandshakeTimeoutMs - static_cast<int>(Timer.elapsed());
if (!Dri_Cdc_Func_TryReadFrame(
p_Serial,
&HandshakeBuffer,
qMin(20, RemainingMs),
&PacketData))
{
continue;
}
if (PacketData.type == Com_Type_HelloRsp)
{
p_Port->p_Port = p_Serial;
p_Port->IsOpened = true;
p_Port->PortName = PortInfo.PortName;
p_Port->ReadBuffer = HandshakeBuffer;
return true;
}
}
p_Serial->close();
delete p_Serial;
}
return false;
}
bool Dri_Cdc_Read(
Dri_Cdc_Struct_Port* p_Port,
QByteArray& ByteArray)
{
ByteArray.clear();
if (!p_Port->IsOpened || (p_Port->p_Port == nullptr))
{
return false;
}
Packet PacketData;
if (Com_Cdc_Func_TryTakeFrame(&p_Port->ReadBuffer, &PacketData) ||
Dri_Cdc_Func_TryReadFrame(p_Port->p_Port, &p_Port->ReadBuffer, 50, &PacketData))
{
ByteArray = Com_Cdc_Func_BuildFrame(PacketData);
return !ByteArray.isEmpty();
}
return false;
}
bool Dri_Cdc_Write(
Dri_Cdc_Struct_Port* p_Port,
const QByteArray& ByteArray)
{
if (!p_Port->IsOpened || (p_Port->p_Port == nullptr))
{
return false;
}
if (ByteArray.isEmpty())
{
return false;
}
if (p_Port->p_Port->write(ByteArray) != ByteArray.size())
{
return false;
}
if (!p_Port->p_Port->waitForBytesWritten(200))
{
return false;
}
return true;
}

View File

@@ -0,0 +1,40 @@
#pragma once
#include <QtCore/QByteArray>
#include <QtCore/QString>
#include <QtCore/QVector>
class QSerialPort;
struct Dri_Cdc_Struct_PortInfo
{
QString PortName;
QString Description;
QString Manufacturer;
};
struct Dri_Cdc_Struct_InitConfig
{
qint32 BaudRate = 115200;
int HandshakeTimeoutMs = 200;
};
struct Dri_Cdc_Struct_Port
{
QSerialPort* p_Port = nullptr;
bool IsOpened = false;
QString PortName;
QByteArray ReadBuffer;
};
QVector<Dri_Cdc_Struct_PortInfo> Dri_Cdc_Enum();
void Dri_Cdc_Deinit(Dri_Cdc_Struct_Port* p_Port);
bool Dri_Cdc_Init(
Dri_Cdc_Struct_Port* p_Port,
const Dri_Cdc_Struct_InitConfig& Config);
bool Dri_Cdc_Read(
Dri_Cdc_Struct_Port* p_Port,
QByteArray& ByteArray);
bool Dri_Cdc_Write(
Dri_Cdc_Struct_Port* p_Port,
const QByteArray& ByteArray);

View File

@@ -0,0 +1,4 @@
<RCC>
<qresource prefix="KeyBorad">
</qresource>
</RCC>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<UI version="4.0" >
<class>KeyBoradClass</class>
<widget class="QWidget" name="KeyBoradClass" >
<property name="objectName" >
<string notr="true">KeyBoradClass</string>
</property>
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>400</height>
</rect>
</property>
<property name="windowTitle" >
<string>KeyBorad</string>
</property>$centralwidget$
</widget>
<layoutDefault spacing="6" margin="11" />
<pixmapfunction></pixmapfunction>
<resources>
<include location="KeyBorad.qrc"/>
</resources>
<connections/>
</UI>

View File

@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="18.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{175BED2A-ED45-4DDC-A67B-7C4042190634}</ProjectGuid>
<Keyword>QtVS_v304</Keyword>
<WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">10.0</WindowsTargetPlatformVersion>
<WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">10.0</WindowsTargetPlatformVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v145</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v145</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
<QtInstall>D:\App\Qt\5.13.1\msvc2015_64</QtInstall>
<QtModules>core;serialport</QtModules>
<QtBuildConfig>debug</QtBuildConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
<QtInstall>D:\App\Qt\5.13.1\msvc2015_64</QtInstall>
<QtModules>core;serialport</QtModules>
<QtBuildConfig>release</QtBuildConfig>
</PropertyGroup>
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ClCompile>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ClCompile>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>false</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="COM\Com_Cdc.h" />
<ClCompile Include="COM\Com_CdcEncode.cpp" />
<ClInclude Include="COM\Com_CdcEncode.h" />
<ClCompile Include="COM\Com_CdcDecode.cpp" />
<ClInclude Include="COM\Com_CdcDecode.h" />
<ClCompile Include="DRI\Dri_Cdc.cpp" />
<ClCompile Include="main.cpp" />
<ClInclude Include="DRI\Dri_Cdc.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
<Import Project="$(QtMsBuild)\qt.targets" />
</ImportGroup>
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
<Filter Include="Form Files">
<UniqueIdentifier>{99349809-55BA-4b9d-BF79-8FDBB0286EB3}</UniqueIdentifier>
<Extensions>ui</Extensions>
</Filter>
<Filter Include="Translation Files">
<UniqueIdentifier>{639EADAA-A684-42e4-A9AD-28FC9BCB8F7C}</UniqueIdentifier>
<Extensions>ts</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="COM\Com_Cdc.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClCompile Include="COM\Com_CdcEncode.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClInclude Include="COM\Com_CdcEncode.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClCompile Include="COM\Com_CdcDecode.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClInclude Include="COM\Com_CdcDecode.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClCompile Include="DRI\Dri_Cdc.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClInclude Include="DRI\Dri_Cdc.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,19 @@
#include <QtCore/QCoreApplication>
#include <QtCore/QDebug>
#include "DRI/Dri_Cdc.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Dri_Cdc_Struct_Port Port;
Dri_Cdc_Struct_InitConfig Config;
Config.BaudRate = 115200;
const bool IsInitOk = Dri_Cdc_Init(&Port, Config);
qDebug() << "Dri_Cdc_Init =" << IsInitOk;
Dri_Cdc_Deinit(&Port);
return 0;
}

View File

@@ -0,0 +1,305 @@
# KeyBorad Protobuf 对齐步骤
这份步骤是按当前工程目录写的,目标是:
1. 保留当前 CDC 传输帧: `AA 55 + len + type + data + checksum`
2.`data` 改成 protobuf 二进制载荷
3. 上位机使用 protobuf C++
4. 下位机使用 nanopb
## 1. 先确定边界
当前工程里三层职责不要打乱:
- `KeyBorad\KeyBorad\DRI\Dri_Cdc.*`
作用: 串口枚举、握手、收发字节流
- `KeyBorad\KeyBorad\COM\Com_CdcEncode.*`
作用: 把一帧 CDC 包组出来
- `KeyBorad\KeyBorad\COM\Com_CdcDecode.*`
作用: 从 CDC 字节流缓存里拆出一帧
引入 protobuf 以后,建议职责变成:
- `DRI`
只管串口
- `COM`
只管外层帧
- `proto`
只管 `data` 的结构定义
也就是说:
- `type` 继续保留在外层帧里
- `data` 改为 protobuf 序列化结果
- `len` 改为 protobuf 载荷实际长度
## 2. 当前已经准备好的文件
本次已经加了两份协议源文件:
- `KeyBorad\proto\keyboard.proto`
- `KeyBorad\proto\keyboard.options`
其中:
- `keyboard.proto`
定义了 `HelloReqPayload / HelloRspPayload / BitmapPayload / FunctionKeyEventPayload / TimeSyncPayload` 等消息
- `keyboard.options`
`nanopb``bytes usage_bitmap` 指定了 `max_size:29`
## 3. 下载工具
### 上位机 protobuf
按 protobuf 官方文档,`protoc``--cpp_out` 生成 C++ 代码。
你需要准备:
- `protoc.exe`
- protobuf C++ runtime 头文件和库
官方参考:
- protobuf C++ generated code guide
- protobuf releases / protoc binary
### 下位机 nanopb
你需要准备:
- `nanopb` runtime
`pb.h`, `pb_common.c/h`, `pb_encode.c/h`, `pb_decode.c/h`
- `nanopb_generator.py`
官方参考:
- nanopb overview
- nanopb generator reference
## 4. 生成代码
### 4.1 生成上位机 C++ 代码
在工程根目录 `C:\Users\lst\Desktop\动态链接库版本\20260320_new_keyboard` 下执行:
```powershell
New-Item -ItemType Directory -Force -Path KeyBorad\generated\cpp | Out-Null
protoc --proto_path=KeyBorad\proto --cpp_out=KeyBorad\generated\cpp KeyBorad\proto\keyboard.proto
```
预期生成:
- `KeyBorad\generated\cpp\keyboard.pb.h`
- `KeyBorad\generated\cpp\keyboard.pb.cc`
### 4.2 生成下位机 nanopb 代码
如果你是 nanopb 源码目录:
```powershell
New-Item -ItemType Directory -Force -Path KeyBorad\generated\nanopb | Out-Null
python third_party\nanopb\generator\nanopb_generator.py --output-dir=KeyBorad\generated\nanopb --strip-path KeyBorad\proto\keyboard.proto
```
如果你用的是 nanopb 二进制包,自带 generator也可以直接换成对应可执行文件。
预期生成:
- `KeyBorad\generated\nanopb\keyboard.pb.h`
- `KeyBorad\generated\nanopb\keyboard.pb.c`
## 5. 先做一处关键改造
### 必改: 不再把 payload 长度当成固定值
一旦 `data` 改成 protobuf`HelloRsp / TimeSync / Ack``data` 长度就不一定再等于今天的固定字节数。
所以迁移时要先把 `COM` 层规则改成:
- `len` 表示 protobuf 载荷实际字节数
- `Com_CdcDecode` 不再用 `type -> 固定 len` 做强校验
- 改成:
- 校验帧头
- 校验 `len`
- 校验 checksum
- 可选校验某个 `type` 的最大长度,不再校验固定长度
建议做法:
- 先保留 `Packet_Type`
- 删除或弱化 `Packet_len` 在解码期的“硬匹配”
- `Packet_len` 只保留给旧协议兼容或做上限参考
## 6. 上位机接入方式
### 6.1 在 VCXPROJ 里加入生成文件
把下面文件加入 `KeyBorad\KeyBorad\KeyBorad.vcxproj`:
- `..\generated\cpp\keyboard.pb.cc`
- `..\generated\cpp\keyboard.pb.h`
并加 include 目录:
- `KeyBorad\generated\cpp`
- protobuf runtime include 目录
再链接 protobuf C++ runtime 库。
### 6.2 改造原则
不要再在 `Com_Cdc.h` 里维护两份字段和 `data`
建议改成两层:
- 原始帧:
`Packet`
- protobuf payload:
`keyboard::cdc::HelloReqPayload` 等生成类
### 6.3 发送流程
`HelloReq` 为例:
1. 创建 `keyboard::cdc::HelloReqPayload`
2. 填字段
3. `SerializeToString()` / `SerializeToArray()`
4. 把序列化结果塞进 `Packet.data`
5. `Packet.type = Com_Type_HelloReq`
6.`Com_Cdc_Func_BuildFrame()` 打外层包
伪代码:
```cpp
keyboard::cdc::HelloReqPayload payload;
payload.set_protocol_version(1);
std::string bytes;
payload.SerializeToString(&bytes);
Packet frame;
frame.type = Com_Type_HelloReq;
frame.data = QByteArray(bytes.data(), static_cast<int>(bytes.size()));
QByteArray tx = Com_Cdc_Func_BuildFrame(frame);
```
### 6.4 接收流程
1. `Dri_Cdc_Read()` 读到完整 CDC 帧
2. `Com_Cdc_Func_ParseFrame()` 拿到 `Packet`
3. 根据 `Packet.type` 选择 protobuf 消息类型
4.`Packet.data``ParseFromArray()`
伪代码:
```cpp
Packet frame;
if (!Com_Cdc_Func_ParseFrame(frameBytes, &frame))
{
return false;
}
if (frame.type == Com_Type_HelloRsp)
{
keyboard::cdc::HelloRspPayload payload;
if (!payload.ParseFromArray(frame.data.constData(), frame.data.size()))
{
return false;
}
}
```
## 7. 下位机接入方式
下位机不要碰外层 CDC 帧格式,直接沿用:
- 帧头
- 长度
- type
- checksum
只把 `type` 对应的 `data` 改成 nanopb 编解码。
建议流程:
1. 串口 ISR / DMA 收满一帧
2. 校验 `AA 55 / len / checksum`
3. 根据 `type``keyboard.pb.h` 里的消息描述符
4. `pb_decode()` 解 protobuf 载荷
5. 业务处理
6. 回复时 `pb_encode()` 出 payload再挂回外层 CDC 帧
## 8. 推荐迁移顺序
建议按下面顺序,不要一次全换:
1. 只提交 `.proto``.options`
2. 上位机先接 `HelloReq / HelloRsp`
3. 下位机把 `HelloReq / HelloRsp` 改成 nanopb
4. 确认握手通了,再迁移 `Ack / Error`
5. 然后迁移 `TimeSync / ThemeRgb`
6. 最后迁移 `Bitmap / FunctionKeyEvent`
原因:
- `HelloReq / HelloRsp` 字段简单,最适合先打通生成链路
- `Bitmap``bytes[29]`,更适合在 `nanopb` 选项验证完后再切
## 9. 你当前 COM 文件还能起什么作用
即使引入 protobuf当前 `COM` 层仍然非常有价值:
- `Com_CdcEncode`
继续负责外层帧打包
- `Com_CdcDecode`
继续负责流式拆包
- `Packet_Type`
继续作为外层快速分发 ID
真正被 protobuf 替代的是:
- 旧的 `data` 手写字段布局
- 手工小端拼 payload 的代码
一句话:
- `COM` 继续保留
-`COM` 不再定义业务 payload 字段
- payload 交给 `.proto`
## 10. 一套最小可复现命令
假设你已经装好 `protoc``nanopb`,最小步骤就是:
```powershell
Set-Location C:\Users\lst\Desktop\动态链接库版本\20260320_new_keyboard
New-Item -ItemType Directory -Force -Path KeyBorad\generated\cpp | Out-Null
protoc --proto_path=KeyBorad\proto --cpp_out=KeyBorad\generated\cpp KeyBorad\proto\keyboard.proto
New-Item -ItemType Directory -Force -Path KeyBorad\generated\nanopb | Out-Null
python third_party\nanopb\generator\nanopb_generator.py --output-dir=KeyBorad\generated\nanopb --strip-path KeyBorad\proto\keyboard.proto
```
然后做两边接入:
1. 上位机 `vcxproj` 加入 `keyboard.pb.cc`
2. 下位机工程加入 `keyboard.pb.c`
3.`HelloReq / HelloRsp``data` 改为 protobuf payload
4.`Com_CdcDecode` 的固定长度校验去掉
## 11. 最后一个关键建议
如果你只是想“共享结构定义”,那就:
- 保留外层 `type`
- 每个 `type` 对应一个 protobuf message
不要一开始就把外层 `type` 也塞进 protobuf `oneof` 里。
原因很简单:
- 外层 `type` 对调试串口抓包很直观
- 下位机分发也更省
- 迁移成本最小

View File

@@ -0,0 +1 @@
BitmapPayload.usage_bitmap max_size:29

View File

@@ -0,0 +1,83 @@
syntax = "proto3";
package keyboard.cdc;
// The AA55 CDC frame still lives outside protobuf:
// [head1][head2][len][type][data][checksum]
// `data` is the serialized protobuf payload for the matching `type`.
// Keep the outer `type` field for fast dispatch and debugging.
enum CdcPacketType {
CDC_PACKET_TYPE_UNKNOWN = 0;
CDC_PACKET_TYPE_HELLO_REQ = 1;
CDC_PACKET_TYPE_HELLO_RSP = 2;
CDC_PACKET_TYPE_BITMAP = 16;
CDC_PACKET_TYPE_FUNCTION_KEY_EVENT = 32;
CDC_PACKET_TYPE_LED_STATE = 33;
CDC_PACKET_TYPE_TIME_SYNC = 48;
CDC_PACKET_TYPE_THEME_RGB = 49;
CDC_PACKET_TYPE_ACK = 126;
CDC_PACKET_TYPE_ERROR = 127;
}
enum KeyAction {
KEY_ACTION_RELEASE = 0;
KEY_ACTION_PRESS = 1;
}
enum ErrorCode {
ERROR_CODE_NONE = 0;
ERROR_CODE_UNKNOWN_TYPE = 1;
ERROR_CODE_INVALID_LENGTH = 2;
ERROR_CODE_INVALID_PARAM = 3;
ERROR_CODE_NOT_READY = 4;
}
message HelloReqPayload {
uint32 protocol_version = 1;
}
message HelloRspPayload {
uint32 protocol_version = 1;
uint32 vendor_id = 2;
uint32 product_id = 3;
uint32 firmware_major = 4;
uint32 firmware_minor = 5;
uint32 capability_flags = 6;
}
message BitmapPayload {
bytes usage_bitmap = 1;
}
message FunctionKeyEventPayload {
uint32 usage = 1;
KeyAction action = 2;
}
message LedStatePayload {
uint32 led_mask = 1;
}
message TimeSyncPayload {
uint32 version = 1;
uint32 flags = 2;
sint32 timezone_min = 3;
fixed64 utc_ms = 4;
fixed32 accuracy_ms = 5;
}
message ThemeRgbPayload {
uint32 red = 1;
uint32 green = 2;
uint32 blue = 3;
}
message AckPayload {
uint32 acked_type = 1;
}
message ErrorPayload {
uint32 error_type = 1;
ErrorCode error_code = 2;
}

47
app.overlay Normal file
View File

@@ -0,0 +1,47 @@
#include <zephyr/dt-bindings/gpio/gpio.h>
/ {
chosen {
zephyr,boot-mode = &boot_mode0;
};
hid_dev_0: hid_dev_0 {
compatible = "zephyr,hid-device";
label = "HID_BOOT";
protocol-code = "keyboard";
in-report-size = <8>;
in-polling-period-us = <1000>;
out-report-size = <8>;
out-polling-period-us = <1000>;
};
hid_dev_1: hid_dev_1 {
compatible = "zephyr,hid-device";
label = "HID_NKRO";
protocol-code = "none";
in-report-size = <31>;
in-polling-period-us = <1000>;
out-report-size = <31>;
out-polling-period-us = <1000>;
};
raw_hid: hid_dev_2 {
compatible = "zephyr,hid-device";
label = "HID_RAW";
protocol-code = "none";
in-report-size = <64>;
in-polling-period-us = <1000>;
out-report-size = <64>;
out-polling-period-us = <1000>;
};
};
&gpregret1 {
status = "okay";
boot_mode0: boot_mode@0 {
compatible = "zephyr,retention";
status = "okay";
reg = <0x0 0x1>;
};
};

View File

@@ -0,0 +1,2 @@
config BOARD_ATGUIGU_KEYBOARD_DONGLE
select SOC_NRF52833_QDAA

View File

@@ -0,0 +1,2 @@
&pinctrl {
};

View File

@@ -0,0 +1,42 @@
/dts-v1/;
#include <nordic/nrf52833_qdaa.dtsi>
#include "atguigu_keyboard_dongle-pinctrl.dtsi"
/ {
model = "Keyboard Dongle";
compatible = "atguigu,atguigu-keyboard-dongle";
chosen {
zephyr,sram = &sram0;
zephyr,flash = &flash0;
zephyr,code-partition = &slot0_partition;
};
};
&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot_partition: partition@0 {
label = "mcuboot";
reg = <0x00000000 DT_SIZE_K(48)>;
};
slot0_partition: partition@c000 {
label = "image-0";
reg = <0x0000c000 DT_SIZE_K(220)>;
};
slot1_partition: partition@43000 {
label = "image-1";
reg = <0x00043000 DT_SIZE_K(220)>;
};
storage_partition: partition@7a000 {
label = "storage";
reg = <0x0007a000 DT_SIZE_K(24)>;
};
};
};

View File

@@ -0,0 +1,10 @@
identifier: atguigu_keyboard_dongle/nrf52833
name: Keyboard Dongle
vendor: atguigu
type: mcu
arch: arm
ram: 128
flash: 512
toolchain:
- zephyr
supported: []

View File

@@ -0,0 +1,2 @@
CONFIG_ARM_MPU=y
CONFIG_HW_STACK_PROTECTION=y

View File

@@ -0,0 +1,9 @@
set(OPENOCD_NRF5_SUBFAMILY "nrf52")
board_runner_args(jlink "--device=nRF52833_xxAA" "--speed=4000")
board_runner_args(pyocd "--target=nrf52833" "--frequency=4000000")
include(${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake)
include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake)
include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)
include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
include(${ZEPHYR_BASE}/boards/common/openocd-nrf5.board.cmake)

View File

@@ -0,0 +1,5 @@
board:
name: atguigu_keyboard_dongle
vendor: atguigu
socs:
- name: nrf52833

View File

@@ -0,0 +1,2 @@
# Suppress "unique_unit_address_if_enabled" to handle some overlaps
list(APPEND EXTRA_DTC_FLAGS "-Wno-unique_unit_address_if_enabled")

View File

@@ -0,0 +1,2 @@
config BOARD_ATGUIGU_MINI_KEYBOARD
select SOC_NRF52840_QFAA

View File

@@ -0,0 +1,73 @@
&pinctrl {
qdec_default: qdec_default {
group1 {
psels = <NRF_PSEL(QDEC_A, 0, 10)>,
<NRF_PSEL(QDEC_B, 1, 6)>;
bias-pull-up;
};
};
qdec_sleep: qdec_sleep {
group1 {
psels = <NRF_PSEL(QDEC_A, 0, 10)>,
<NRF_PSEL(QDEC_B, 1, 6)>;
low-power-enable;
};
};
led_spi_default: led_spi_default {
group1 {
psels = <NRF_PSEL(SPIM_MOSI, 0, 20)>;
};
};
led_spi_sleep: led_spi_sleep {
group1 {
psels = <NRF_PSEL(SPIM_MOSI, 0, 20)>;
low-power-enable;
};
};
lcd_spi_default: lcd_spi_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 1, 13)>,
<NRF_PSEL(SPIM_MOSI, 0, 28)>;
};
};
lcd_spi_sleep: lcd_spi_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 1, 13)>,
<NRF_PSEL(SPIM_MOSI, 0, 28)>;
low-power-enable;
};
};
i2c1_default: i2c1_default {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 1, 0)>,
<NRF_PSEL(TWIM_SCL, 0, 24)>;
bias-pull-up;
};
};
i2c1_sleep: i2c1_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 1, 0)>,
<NRF_PSEL(TWIM_SCL, 0, 24)>;
low-power-enable;
};
};
pwm0_default: pwm0_default {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 11)>;
};
};
pwm0_sleep: pwm0_sleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 11)>;
low-power-enable;
};
};
};

View File

@@ -0,0 +1,240 @@
/dts-v1/;
#include <nordic/nrf52840_qiaa.dtsi>
#include "atguigu_mini_keyboard-pinctrl.dtsi"
#include <zephyr/dt-bindings/pinctrl/nrf-pinctrl.h>
#include <zephyr/dt-bindings/led/led.h>
/ {
model = "Mini Keyboard, 17 keys";
compatible = "atguigu,atguigu-mini-keyboard";
chosen {
zephyr,sram = &sram0;
zephyr,flash = &flash0;
zephyr,code-partition = &slot0_partition;
zephyr,display = &st7789v3;
};
aliases {
backlight = &backlight;
};
mode_sense: mode-sense {
compatible = "voltage-divider";
io-channels = <&adc 5>;
output-ohms = <1>;
full-ohms = <1>;
};
battery_sense: battery-sense {
compatible = "voltage-divider";
io-channels = <&adc 7>;
output-ohms = <100000>;
full-ohms = <200000>;
power-gpios = <&gpio0 9 GPIO_ACTIVE_HIGH>;
power-on-sample-delay-us = <100>;
};
pwm_leds: pwm_leds {
compatible = "pwm-leds";
status = "okay";
backlight: pwm_led_0 {
pwms = <&pwm0 0 PWM_MSEC(10) PWM_POLARITY_INVERTED>;
};
};
led_0: led_0 {
compatible = "gpio-leds";
status = "okay";
chan0 {
gpios = <&gpio0 17 GPIO_ACTIVE_LOW>;
};
};
led_1: led_1 {
compatible = "gpio-leds";
status = "okay";
chan0 {
gpios = <&gpio1 2 GPIO_ACTIVE_LOW>;
};
};
mipi_dbi: mipi_dbi {
compatible = "zephyr,mipi-dbi-spi";
status = "okay";
spi-dev = <&spi3>;
dc-gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>;
reset-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
write-only;
#address-cells = <1>;
#size-cells = <0>;
st7789v3: st7789v@0 {
compatible = "sitronix,st7789v";
status = "okay";
reg = <0>;
mipi-max-frequency = <32000000>;
width = <320>;
height = <172>;
x-offset = <0>;
y-offset = <34>;
vcom = <0x2b>;
gctrl = <0x35>;
vrhs = <0x11>;
vdvs = <0x20>;
mdac = <0xA0>;
lcm = <0x2c>;
colmod = <0x55>;
gamma = <0x01>;
porch-param = [ 0c 0c 00 33 33 ];
cmd2en-param = [ 5a 69 02 01 ];
pwctrl1-param = [ a4 a1 ];
pvgam-param = [ d0 00 02 07 0a 28 32 44 42 06 0e 12 14 17 ];
nvgam-param = [ d0 00 02 07 0a 28 31 54 47 0e 1c 17 1b 1e ];
ram-param = [ 00 f0 ];
rgb-param = [ c0 02 14 ];
mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";
};
};
};
&spi2 {
status = "okay";
pinctrl-0 = <&led_spi_default>;
pinctrl-1 = <&led_spi_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 21 GPIO_ACTIVE_LOW>;
led_strip: ws2812@0 {
compatible = "worldsemi,ws2812-spi";
supply-gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>;
reg = <0>;
spi-max-frequency = <8000000>;
chain-length = <17>;
color-mapping = <LED_COLOR_ID_GREEN LED_COLOR_ID_RED LED_COLOR_ID_BLUE>;
spi-one-frame = <0xFC>;
spi-zero-frame = <0xC0>;
};
};
&i2c1 {
status = "okay";
pinctrl-0 = <&i2c1_default>;
pinctrl-1 = <&i2c1_sleep>;
pinctrl-names = "default", "sleep";
clock-frequency = <400000>;
ip5306: pmic@75 {
status = "okay";
compatible = "injoinic,ip5306";
reg = <0x75>;
keepalive-gpios = <&gpio0 22 GPIO_ACTIVE_LOW>;
keepalive-interval-ms = <10000>;
keepalive-offload;
};
};
/* 编码器 */
&qdec {
status = "okay";
/* 引用上面定义的标签 */
pinctrl-0 = <&qdec_default>;
pinctrl-1 = <&qdec_sleep>;
/* 指定 pinctrl-0 为默认pinctrl-1 为睡眠 */
pinctrl-names = "default", "sleep";
/* 别忘了 QDEC 必须的两个属性 */
steps = <40>;
led-pre = <0>;
};
&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot_partition: partition@0 {
label = "mcuboot";
reg = <0x00000000 DT_SIZE_K(48)>;
};
slot0_partition: partition@c000 {
label = "image-0";
reg = <0x0000c000 DT_SIZE_K(472)>;
};
slot1_partition: partition@82000 {
label = "image-1";
reg = <0x00082000 DT_SIZE_K(472)>;
};
storage_partition: partition@f8000 {
label = "storage";
reg = <0x000f8000 DT_SIZE_K(32)>;
};
};
};
&spi3 {
status = "okay";
pinctrl-0 = <&lcd_spi_default>;
pinctrl-1 = <&lcd_spi_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 2 GPIO_ACTIVE_LOW>;
};
&adc {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
channel@5 {
reg = <5>;
zephyr,gain = "ADC_GAIN_1_6";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 20)>;
zephyr,input-positive = <NRF_SAADC_AIN5>;
zephyr,resolution = <12>;
};
channel@7 {
reg = <7>;
zephyr,gain = "ADC_GAIN_1_6";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 20)>;
zephyr,input-positive = <NRF_SAADC_AIN7>;
zephyr,resolution = <12>;
};
};
&pwm0 {
status = "okay";
pinctrl-0 = <&pwm0_default>;
pinctrl-1 = <&pwm0_sleep>;
pinctrl-names = "default", "sleep";
};
&gpio0 {
status = "okay";
};
&gpio1 {
status = "okay";
};
&gpiote {
status = "okay";
};
&usbd {
status = "okay";
};
&uicr {
nfct-pins-as-gpios;
gpio-as-nreset;
};

View File

@@ -0,0 +1,10 @@
identifier: atguigu_mini_keyboard/nrf52840
name: Mini Keyboard, 17 keys
vendor: atguigu
type: mcu
arch: arm
ram: 256
flash: 1024
toolchain:
- zephyr
supported: []

View File

@@ -0,0 +1,2 @@
CONFIG_ARM_MPU=y
CONFIG_HW_STACK_PROTECTION=y

View File

@@ -0,0 +1,9 @@
set(OPENOCD_NRF5_SUBFAMILY "nrf52")
board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000")
board_runner_args(pyocd "--target=nrf52840" "--frequency=4000000")
include(${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake)
include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake)
include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)
include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
include(${ZEPHYR_BASE}/boards/common/openocd-nrf5.board.cmake)

View File

@@ -0,0 +1,5 @@
board:
name: atguigu_mini_keyboard
vendor: atguigu
socs:
- name: nrf52840

View File

@@ -0,0 +1,2 @@
# Suppress "unique_unit_address_if_enabled" to handle some overlaps
list(APPEND EXTRA_DTC_FLAGS "-Wno-unique_unit_address_if_enabled")

View File

@@ -0,0 +1,411 @@
# 自定义 BLE Bond 以支持多身份连接的设计说明
## 1. 背景
CAF 自带的 `ble_bond` 适合简单外设场景。它默认假设应用只使用 `BT_ID_DEFAULT` 一个本地身份,职责主要是:
- 等待 `settings_loader` 完成初始化
- 根据按键触发删除默认身份上的 bond
- 通过 `CONFIG_CAF_BLE_BOND_SUPPORTED` 告知 CAF 应用实现了 BLE bond
如果应用希望像多设备键盘一样支持多个 Bluetooth identity 对应多个 slot则必须提供自定义 `ble_bond`。因为此时已经不只是“是否有 bond”而是“哪一个 peer 属于哪一个 slot、当前应使用哪一个 identity、切换 slot 时如何与 `ble_adv`/`ble_state` 协同”。
---
## 2. CAF 自带 BLE Bond 与自定义 BLE Bond 的差异
### 2.1 CAF 自带 BLE Bond
特点:
- 只管理默认 identity
- 不维护 slot 到 identity 的映射
- 不发布 `ble_peer_operation_event`
- 不处理多主机切换
- 不处理 peer 所属 slot 自动归位
适用场景:
- 单主机 BLE 外设
- 仅需擦除 bond
- 不需要多身份、多 slot、多 peer 路由
### 2.2 自定义 BLE Bond
特点:
- 维护应用 slot 与 Bluetooth stack identity 的映射
- 保存当前选中的 slot
- 在 slot 切换时发布 `ble_peer_operation_event`
-`ble_adv` 协同切换广告 identity
-`ble_state` 协同确认连接落在正确 identity 上
- 处理同一 peer 属于哪个 slot
- 可扩展实现 peer 迁移、自动回 slot、自动重配等策略
适用场景:
- 多设备键盘
- 每个 slot 对应独立 bond
- 需要在多个 identity 间切换广告和连接
- 需要对 host 行为做应用级策略控制
---
## 3. 两版 BLE Bond 架构图
### 3.1 CAF 自带 BLE Bond 架构
```mermaid
flowchart LR
A[settings_loader] --> B[CAF ble_bond]
C[click_event] --> B
B -->|bt_unpair BT_ID_DEFAULT| D[Bluetooth Stack]
B -->|MODULE_STATE_READY| E[CAF Modules]
D --> F[单一默认 Identity]
```
说明:
- `CAF ble_bond` 不参与 slot 选择
- 也不参与广告 identity 路由
- 仅在用户触发擦除时对默认 identity 执行 `bt_unpair()`
### 3.2 自定义多身份 BLE Bond 架构
```mermaid
flowchart TD
A[settings_loader] --> B[custom ble_bond]
C[config_event / slot control] --> B
D[ble_peer_event CONNECTED/SECURED/DISCONNECTED] --> B
B --> E[slot <-> bt_stack_id LUT]
B --> F[current selected slot]
B --> G[ble_peer_operation_event]
G --> H[CAF ble_adv]
G --> I[slot name / GAP name provider]
H --> J[Advertising on selected Identity]
J --> K[Windows / Host]
K --> D
B -->|validate connection identity| L[bt_conn_disconnect]
B -->|find peer owner slot| M[auto switch / reconnect]
```
说明:
- 自定义版本已经成为 BLE peer 管理的核心策略层
- 它既要维护持久化状态,也要处理运行时连接路由
- `ble_adv` 只负责按事件切换 identity 广播,不负责理解 slot 语义
---
## 4. 相比 CAF 自带 BLE Bond额外必须完成的工作
### 4.1 身份资源规划
必须先定义:
- 一共有多少个应用 slot
- `CONFIG_BT_ID_MAX` 至少要覆盖这些 slot
- 是否保留 `BT_ID_DEFAULT`
- 每个 slot 对应哪个 stack identity
当前实现中:
- `slot_count = 3`
- `APP_PEER_COUNT = CONFIG_BT_ID_MAX - 1`
- `BT_ID_DEFAULT` 不直接用于可切换 slot
- slot 0/1/2 分别映射到 identity 1/2/3
### 4.2 持久化保存应用层状态
CAF 自带实现不保存 slot 语义。自定义版本必须自行保存:
- 当前选中的 slot
- slot 到 stack identity 的 LUT
否则重启后:
- `ble_adv` 可能在错误 identity 上广播
- 连接会落到旧 identity
- 导致加密后被应用判定为错误 slot
### 4.3 向 CAF 明确声明“应用实现了 BLE bond”
如果应用没有通过 Kconfig `select CAF_BLE_BOND_SUPPORTED`:
- `ble_adv` 不会等待自定义 `ble_bond` ready
- 开机可能在错误 identity 上提前广播
- 最终出现连接到 old id、加密后又断开的异常
因此自定义 BLE bond 不能只写 C 文件,还必须补齐 Kconfig 接线。
### 4.4 设计并发布 peer operation 事件
CAF 自带 `ble_bond` 不发布 `ble_peer_operation_event`,但多身份实现必须发布,例如:
- `PEER_OPERATION_SELECTED`
- `PEER_OPERATION_ERASE_ADV`
- `PEER_OPERATION_ERASED`
这些事件会驱动:
- `ble_adv` 切换广告 identity
- 名称提供器更新 GAP/广告名
- LED/状态模块同步 UI
### 4.5 处理 slot 切换时的现有连接
当用户切换 slot 时,应用必须决定:
- 现有 LE 连接是否断开
- 断开后是否立刻以新 identity 重启广告
如果不主动断开:
- 连接仍停留在旧 identity
- 逻辑上已经切到新 slot但链路仍属于旧 slot
- 后续加密、重连、广告状态都会错乱
### 4.6 在连接建立后校验“连接是否属于当前 slot”
多身份实现必须在 `PEER_STATE_SECURED` 甚至更早的 `CONNECTED` 时校验:
- 当前连接的 `info.id`
- 当前选中 slot 对应的 `bt_stack_id`
如果不一致:
- 这是旧 identity、旧连接或错误路由
- 必须主动断开,避免错误 bond 被继续使用
### 4.7 处理 peer 与 slot 的归属关系
CAF 自带实现没有“peer 属于哪个 slot”这个概念。多身份实现必须处理:
- 同一个 peer 之前绑定在哪个 slot
- 当前连接是不是连错 slot
- 是否自动切回该 peer 所属 slot
- 是否允许 peer 从一个 slot 迁移到另一个 slot
当前 C1 实现已经支持:
- 当 peer 连到错误 slot 时
- 自动识别该 peer 真实所属 slot
- 自动切回正确 slot
- 主动断开一次,等待 host 重连
### 4.8 处理 bond 擦除和 identity reset
多身份实现通常不能只 `bt_unpair(BT_ID_DEFAULT, NULL)`,还要区分:
- 删除某个 slot 的 peer
- 删除全部 slot
- 是否要 `bt_id_reset()`
- 擦除后是否需要重启该 identity 的广告会话
这部分已经不再是单纯 bond 删除而是“identity 生命周期管理”。
---
## 5. 自定义 BLE Bond 实现中的重点注意事项
### 5.1 先解决 Kconfig 接线,再调连接逻辑
这是最容易遗漏的点。
如果没有正确让应用 `select CAF_BLE_BOND_SUPPORTED`:
- `ble_adv` 启动顺序会不对
- 自定义 `ble_bond` 即使逻辑正确,也会在启动阶段表现异常
这属于架构接线问题,不是连接状态机问题。
### 5.2 广播名和 GAP 名必须统一策略
多 slot 场景下,名称策略必须明确:
- 是每个 slot 不同名字
- 还是所有 slot 同名
如果广告名和 GAP Device Name 不一致:
- Windows 可能扫描到一个名字
- 配对后读到另一个名字
- 容易造成驱动实例重建、名字重命名、缓存混乱
当前项目已经验证:
- 统一 GAP 名与广告名后
- Windows 行为更稳定
### 5.3 不要假设 Windows 删除设备就等于双方 bond 都删了
这在多 slot 里尤其关键。
Windows 端删设备后:
- 主机端密钥可能删了
- 设备端旧 slot 的本地 bond 仍然存在
这会导致:
- 新 slot 上重新配对被 SMP 拒绝
- 日志出现 `Refusing new pairing. The old bond must be unpaired first.`
因此自定义 BLE bond 必须明确“本地 bond”和“主机侧配对记录”不是同一件事。
### 5.4 host 删除密钥无法直接读取,只能通过行为推断
设备无法直接读取 Windows 是否删除了配对密钥。
只能根据以下现象推断:
- 已绑定 slot 上能否顺利加密
- 是否重新发起 pairing
- 是否出现安全失败或 SMP 冲突
因此设计 C2 这类“自动清旧 bond 并重配”的逻辑时,必须以“推断”而不是“查询”来实现。
### 5.5 连接事件与安全事件分工要清楚
推荐分层:
- `CONNECTED` 阶段处理 peer 归属和 slot 自动路由
- `SECURED` 阶段确认连接确实落在当前选中 identity
- `DISCONNECTED` 阶段清理自动切换或迁移的临时状态
如果所有逻辑都堆到 `SECURED`:
- 处理时机偏晚
- 经常已经进入 SMP/安全过程
- 更容易触发 host 侧异常
### 5.6 自动切 slot 时要接受“一次断开再重连”
这是应用策略设计里的现实约束。
当 peer 连错 slot 时:
- 应用切换到正确 slot
- 需要让旧连接断开
- 然后等待 host 按正确 identity 重新连入
不要追求“同一条连接无感切换 identity”这在 BLE identity 语义上不现实,也不稳定。
### 5.7 `bt_foreach_bond()` 和地址匹配要谨慎
如果实现 peer 所属 slot 判断,通常会用:
- `bt_conn_get_dst(conn)`
- `bt_foreach_bond(local_id, ...)`
- `bt_addr_le_cmp()`
需要注意:
- public address 与 random/RPA 地址行为不同
- 已绑定设备可能因为隐私地址导致匹配行为更复杂
- 实际测试必须覆盖 Windows、Android、iOS 等 host
当前项目日志里 host 使用 public address因此匹配较直接。
### 5.8 identity reset 的语义要和 UI/配置动作对齐
在自定义多 slot 实现中,`erase_peer()` 往往同时包含:
- 删除 bond
- reset identity
- 触发广告重启
因此必须保证 UI 动作和内部语义一致。例如:
- “清当前 slot 配对” 是否意味着彻底 reset 当前 identity
- “清全部配对” 是否会影响所有 slot 广播身份
如果定义不清,后续调试会非常混乱。
### 5.9 必须大量依赖日志建立可观测性
自定义 BLE bond 不像 CAF 默认版那样简单。建议至少保留以下日志:
- 当前 slot 与 stack identity
- settings 恢复结果
- slot 切换请求
- `PEER_OPERATION_SELECTED`
- 连接建立的 `info.id`
- `SECURED` 时的 identity 匹配结果
- peer 所属 slot 自动识别结果
- 自动切 slot 开始与结束
没有这些日志,多身份调试成本会非常高。
---
## 6. 推荐的实现分层
为了避免自定义 BLE bond 过度膨胀,建议分层如下:
- `ble_bond`
负责 slot/identity 映射、持久化状态、peer 归属、配对策略
- `ble_adv`
负责在指定 identity 上广播
- `slot_name / GAP name provider`
负责名称策略
- `slot_ctrl`
负责把按键或配置命令转换成 slot 选择请求
- `ble_state`
负责连接、安全、断开事件广播
这个分层里最重要的原则是:
- `ble_adv` 不理解 slot 语义
- `ble_state` 不理解 slot 策略
- slot 语义统一由自定义 `ble_bond` 决策
---
## 7. 当前项目自定义 BLE Bond 已实现的内容
当前 `new_kbd` 自定义 `ble_bond` 已经具备:
- 3 个应用 slot
- slot 到 identity 的持久化映射
- 当前 slot 的持久化保存
- `settings_loader` 后初始化 identity
- `PEER_OPERATION_SELECTED` 广播给 `ble_adv`
- slot 切换时主动断开现有 LE 连接
- `SECURED` 时校验当前连接是否落在期望 identity
- C1: peer 连错 slot 时自动识别所属 slot 并切回
尚未自动化的部分:
- C2: 主机端删密钥后,本地自动删除旧 bond 并允许重配
- peer 迁移到新 slot 的完整自动化策略
- 更复杂 host 场景下的异常恢复
---
## 8. 结论
CAF 自带 `ble_bond` 是“单 identity 的 bond 擦除模块”。
一旦要支持多 slot、多 identity、多 host 路由,自定义 `ble_bond` 就不再只是替代品,而是整套 BLE peer 管理策略的核心。
相对 CAF 自带实现,额外工作主要集中在:
- identity 规划
- 状态持久化
-`ble_adv`/`ble_state` 的事件协同
- slot 选择与连接路由
- peer 归属识别
- 多 host / host 缓存 / bond 生命周期处理
实现时最需要注意的是:
- Kconfig 接线必须正确
- 广播 identity 与当前 slot 必须严格一致
- 广告名/GAP 名策略必须统一
- host 删除配对记录不等于本地 bond 已删除
- 自动切 slot 和重连应被视为正常流程,而不是异常

View File

@@ -0,0 +1,281 @@
# BLE 时间同步服务 PC 上位机接入文档
## 1. 概述
当前项目实现了一套独立的 BLE 时间同步服务,供 PC 上位机在蓝牙连接后向键盘写入当前 UTC 时间、时区和时间精度。
这套服务的设计目标是:
- 不走 HID 报告通道,避免与键盘输入链路耦合。
- BLE 侧只负责协议适配,实际时间状态统一交给 `time_manager` 管理。
- 主机只负责“下发时间”,设备不通过此服务回读时间,也不通过 notify 返回确认。
结论上,这是一条:
- 自定义 GATT Service
- 单个可写 Characteristic
- 仅支持写入
- 需要加密连接
的单向时间同步通道。
## 2. GATT 定义
### 2.1 Service UUID
`0b7f5000-38d2-4f62-8f6f-36c4fd73a110`
### 2.2 Characteristic UUID
`0b7f5001-38d2-4f62-8f6f-36c4fd73a110`
### 2.3 Characteristic 属性
- `Write`
- `Write Without Response`
### 2.4 Characteristic 权限
- `Write Encrypted`
也就是说,上位机在写入前必须先完成配对/加密。未加密链路下,写入会被 GATT 层拒绝。
## 3. 设备端处理逻辑
### 3.1 依赖条件
设备端只有在以下两个条件同时满足时,才接受时间写入:
- BLE 栈已经 ready
- `time_manager` 已经 ready
如果模块尚未 ready写入会返回 ATT 错误。
### 3.2 写入成功后的行为
设备端收到合法 payload 后会:
1. 校验版本、长度和 flags。
2. 解析 `utc_ms``timezone_min``accuracy_ms`
3. 自动把同步来源标记为 `BLE`
4. 投递 `time_sync_event``time_manager`
5. `time_manager` 更新运行时钟状态,并延迟异步写入 settings。
注意:
- 当前 BLE 时间同步服务没有 `Read``Notify` 能力。
- 主机拿到 GATT 写成功,只能说明设备接受了这次写入。
- 设备不会通过这个服务主动回传“当前时间”。
## 4. 数据包格式
### 4.1 总长度
固定 `16` 字节。
### 4.2 字段布局
| 偏移 | 长度 | 字段名 | 类型 | 字节序 |
| --- | --- | --- | --- | --- |
| 0 | 1 | `version` | `uint8` | - |
| 1 | 1 | `flags` | `uint8` | - |
| 2 | 2 | `timezone_min` | `int16` | little-endian |
| 4 | 8 | `utc_ms` | `uint64` | little-endian |
| 12 | 4 | `accuracy_ms` | `uint32` | little-endian |
### 4.3 当前协议版本
- `version = 1`
### 4.4 flags 定义
当前只定义 1 个 bit
- `BIT(0)` = `TIMEZONE_VALID`
因此当前版本必须满足:
- `flags & 0x01 != 0`
推荐主机固定写:
- `flags = 0x01`
## 5. 字段语义
### 5.1 `timezone_min`
单位:分钟。
含义:本地时区相对 UTC 的偏移。
示例:
- 中国标准时间 UTC+8 -> `480`
- 印度 UTC+5:30 -> `330`
- UTC-5 -> `-300`
设备端当前接受范围:
- `-1440 ~ +1440`
### 5.2 `utc_ms`
单位:毫秒。
含义UTC 时间戳,不带本地时区偏移。
要求:
- 必须大于 `0`
### 5.3 `accuracy_ms`
单位:毫秒。
含义:本次时间来源的估计精度。
建议:
- 若上位机没有可靠精度信息,可直接写 `0`
- 若来自系统时间并已做网络对时,也可以给一个保守值,例如 `50``100`
## 6. 主机写入建议
### 6.1 推荐使用 Write Request
虽然设备同时支持:
- Write
- Write Without Response
但 PC 上位机侧建议优先使用 **Write Request带响应写**,原因是:
- 更容易拿到 ATT 层成功/失败结果
- 更方便在开发阶段定位协议错误
- 更适合作为配置/同步类命令
### 6.2 推荐时序
1. 扫描并连接设备。
2. 完成配对/加密。
3. 发现时间同步 Service 和 Characteristic。
4. 组包为固定 16 字节 payload。
5. 执行一次带响应写入。
6. 若写成功,可视为设备已接受此次同步请求。
## 7. Python 示例
下面示例基于 `bleak`,演示如何向设备写入当前系统时间。
```python
import asyncio
import struct
import time
from bleak import BleakClient
TIME_SYNC_CHAR_UUID = "0b7f5001-38d2-4f62-8f6f-36c4fd73a110"
def build_time_sync_payload(timezone_min: int, accuracy_ms: int = 0) -> bytes:
version = 1
flags = 0x01 # TIMEZONE_VALID
utc_ms = int(time.time() * 1000)
return struct.pack("<BBhQI", version, flags, timezone_min, utc_ms, accuracy_ms)
async def main(address: str):
payload = build_time_sync_payload(timezone_min=480, accuracy_ms=50)
async with BleakClient(address) as client:
await client.write_gatt_char(
TIME_SYNC_CHAR_UUID,
payload,
response=True,
)
print("time sync write done")
if __name__ == "__main__":
asyncio.run(main("XX:XX:XX:XX:XX:XX"))
```
说明:
- `struct.pack("<BBhQI", ...)` 对应协议定义的 little-endian 格式。
- `timezone_min=480` 表示 UTC+8。
- `response=True` 表示使用带响应写入。
## 8. 常见错误与排查
### 8.1 写入被拒绝
常见原因:
- 还没有完成配对/加密
- 写入长度不是 16 字节
- `version != 1`
- `flags` 未设置 `TIMEZONE_VALID`
- 使用了 prepare write / long write
### 8.2 写入成功但设备时间没更新
优先检查:
- 设备日志是否出现 `Accepted BLE time sync ...`
- 设备日志是否出现 `Time synchronized src=1 ...`
- 上位机是否错误地把本地时间直接当成 UTC 写入
### 8.3 时区显示错误
最常见原因:
- 主机把“本地毫秒时间戳”写进了 `utc_ms`
- 同时又传了 `timezone_min`
正确做法是:
- `utc_ms` 始终写 UTC 毫秒
- 本地时区单独放在 `timezone_min`
## 9. ATT 错误码语义
设备端当前会返回的典型 ATT 错误如下:
- `BT_ATT_ERR_UNLIKELY`
- 模块尚未 ready例如 BLE 栈或 `time_manager` 还未初始化完成
- `BT_ATT_ERR_INVALID_OFFSET`
- 非零 offset 写入
- `BT_ATT_ERR_ATTRIBUTE_NOT_LONG`
- 使用 prepare write / 长写流程
- `BT_ATT_ERR_VALUE_NOT_ALLOWED`
- payload 长度、版本、flags 或字段内容不合法
## 10. 当前服务边界
当前版本仅支持:
- 主机 -> 设备 单向校时
当前不支持:
- BLE 读回当前设备时间
- 校时结果通知
- 历史同步记录查询
- DST 单独字段
- 通过 payload 指定同步来源
同步来源在设备端固定记为:
- `TIME_SYNC_SOURCE_BLE`
## 11. 对接建议
对 PC 上位机实现建议如下:
- 首选带响应写入,不要默认用 write without response
- 时间戳统一使用 UTC 毫秒
- 时区单独使用分钟偏移
- 每次建立加密连接后可主动同步一次时间
- 若 PC 有系统授时状态,可把估计精度填入 `accuracy_ms`

View File

@@ -0,0 +1,314 @@
# Bluetooth SIG 规格页中的 BLE Profile 汇总
更新时间2026-04-01Asia/Hong_Kong
## 1. 说明与筛选口径
本文基于 Bluetooth SIG 官方规格索引页 `https://www.bluetooth.com/specifications/specs/` 当前处于 `Adopted` 状态的条目进行筛选,只保留运行在 Bluetooth Low Energy 体系上的 Profile包括
- 基于 GATT 的传统 BLE Profile
- 基于 LE Audio 的音频类 Profile
- 基于 Bluetooth Mesh / NLC 的 Profile
本文中的“详细说明”不是官方规范原文摘录,也不是对规范条款的逐句转述,而是我基于 BLE/GATT、LE Audio、Mesh/NLC 的通用工程记忆做的高层总结。用于快速建立全局认知和做架构预判是合适的;如果要做互操作或拿来做 BQB/QDID 级别实现,仍应回到对应规范逐条核对。
本次共纳入 **59** 个 Profile。
## 2. 总表
| Profile | 最新版本 | 类别 | 官方条目 |
| --- | --- | --- | --- |
| Alert Notification Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/alert-notification-profile-1-0/ |
| Ambient Light Sensor NLC Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/ambient-light-sensor-nlc-profile-1-0-1/ |
| Asset Tracking Profile | 1.0 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/asset-tracking-profile-1-0/ |
| Authorization Control Profile | 1.0 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/authorization-control-profile-1-0/ |
| Automation IO Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/automation-io-profile-1-0/ |
| Basic Audio Profile | 1.0.2 | LE Audio | https://www.bluetooth.com/specifications/specs/basic-audio-profile-1-0-2/ |
| Basic Lightness Controller NLC Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/basic-lightness-controller-nlc-profile-1-0-1/ |
| Basic Scene Selector NLC Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/basic-scene-selector-nlc-profile-1-0-1/ |
| Binary Sensor Profile | 1.0 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/binary-sensor-profile-1-0/ |
| Blood Pressure Profile | 1.1.1 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/blood-pressure-profile-1-1-1/ |
| Calendar Tasks and Notes Profile | 1.0.1 | 核心/GATT | https://www.bluetooth.com/specifications/specs/calendar-tasks-and-notes-profile-1-0-1/ |
| Call Control Profile | 1.0 | LE Audio | https://www.bluetooth.com/specifications/specs/call-control-profile-1-0/ |
| Common Audio Profile | 1.0.1 | LE Audio | https://www.bluetooth.com/specifications/specs/common-audio-profile-1-0-1/ |
| Continuous Glucose Monitoring Profile | 1.0.2 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/continuous-glucose-monitoring-profile-1-0-2/ |
| Cookware Profile | 1.0 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/cookware-profile-1-0/ |
| Coordinated Set Identification Profile | 1.1 | LE Audio | https://www.bluetooth.com/specifications/specs/coordinated-set-identification-profile-1-1/ |
| Cycling Power Profile | 1.1.1 | 运动/定位 | https://www.bluetooth.com/specifications/specs/cycling-power-profile-1-1-1/ |
| Cycling Speed and Cadence Profile | 1.0.1 | 运动/定位 | https://www.bluetooth.com/specifications/specs/cycling-speed-and-cadence-profile/ |
| Device Time Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/device-time-profile-1-0/ |
| Dimming Control NLC Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/dimming-control-nlc-profile-1-0-1/ |
| Electronic Shelf Label Profile | 1.0.1 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/electronic-shelf-label-profile-1-0-1/ |
| Emergency Profile | 1.0 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/emergency-profile-1-0/ |
| Energy Monitor NLC Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/energy-monitor-nlc-profile1-0-1/ |
| Environmental Sensing Profile | 1.0.1 | 核心/GATT | https://www.bluetooth.com/specifications/specs/environmental-sensing-profile-1-0-1/ |
| Find Me Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/find-me-profile-1-0/ |
| Fitness Machine Profile | 1.0.1 | 运动/定位 | https://www.bluetooth.com/specifications/specs/fitness-machine-profile-1-0-1/ |
| Gaming Audio Profile | 1.0.1 | LE Audio | https://www.bluetooth.com/specifications/specs/gaming-audio-profile-4/ |
| Generic Health Sensor Profile | 1.0 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/generic-health-sensor-profile/ |
| Global Navigation Satellite System Profile | 1.0 | 运动/定位 | https://www.bluetooth.com/specifications/specs/global-navigation-satellite-system-profile-1-0/ |
| Glucose Profile | 1.0.1 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/glucose-profile-1-0-1/ |
| Health Thermometer Profile | 1.0 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/health-thermometer-profile-1-0/ |
| Hearing Access Profile | 1.0.1 | LE Audio | https://www.bluetooth.com/specifications/specs/hearing-access-profile-1-0-1/ |
| Heart Rate Profile | 1.0 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/heart-rate-profile-1-0/ |
| HID Over GATT Profile | 1.1 | 核心/GATT | https://www.bluetooth.com/specifications/specs/hid-over-gatt-profile/ |
| HVAC Integration NLC Profile | 1.0 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/hvac-integration-nlc-profile/ |
| Industrial Measurement Device Profile | 1.0 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/industrial-measurement-device-profile-1-0/ |
| Insulin Delivery Profile | 1.0.2 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/insulin-delivery-profile-1-0-2/ |
| Internet Protocol Support Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/internet-protocol-support-profile-1-0/ |
| Location and Navigation Profile | 1.0.1 | 运动/定位 | https://www.bluetooth.com/specifications/specs/location-and-navigation-profile-1-0-1/ |
| Media Control Profile | 1.0 | LE Audio | https://www.bluetooth.com/specifications/specs/media-control-profile/ |
| Mesh Configuration Database Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/mesh-configuration-database-profile-1-0-1/ |
| Mesh Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/mesh-profile-1-0-1/ |
| Microphone Control Profile | 1.0 | LE Audio | https://www.bluetooth.com/specifications/specs/microphone-control-profile-1-0/ |
| Object Transfer Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/object-transfer-profile-1-0/ |
| Occupancy Sensor NLC Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/occupancy-sensor-nlc-profile-1-0-1/ |
| Phone Alert Status Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/phone-alert-status-profile-1-0/ |
| Physical Activity Monitor Profile | 1.0 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/physical-activity-monitor-profile-1-0/ |
| Proximity Profile | 1.0.1 | 核心/GATT | https://www.bluetooth.com/specifications/specs/proximity-profile-1-0-1/ |
| Public Broadcast Profile | 1.0.2 | LE Audio | https://www.bluetooth.com/specifications/specs/public-broadcast-profile-1-0-2/ |
| Pulse Oximeter Profile | 1.0.1 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/pulse-oximeter-profile-1-0-1/ |
| Ranging Profile | 1.0 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/ranging-profile-1-0/ |
| Reconnection Configuration Profile | 1.0.1 | LE Audio | https://www.bluetooth.com/specifications/specs/reconnection-configuration-profile-1-0-1/ |
| Running Speed and Cadence Profile | 1.0.1 | 运动/定位 | https://www.bluetooth.com/specifications/specs/running-speed-and-cadence-profile-1-0-1/ |
| Scan Parameters Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/scan-parameters-profile-1-0/ |
| Telephony and Media Audio Profile | 1.0.1 | LE Audio | https://www.bluetooth.com/specifications/specs/telephony-and-media-audio-profile-1-0-1/ |
| Time Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/time-profile-1-0/ |
| Voice Assistant Profile | 1.0 | LE Audio | https://www.bluetooth.com/specifications/specs/voice-assistant-profile-1-0/ |
| Volume Control Profile | 1.0 | LE Audio | https://www.bluetooth.com/specifications/specs/volume-control-profile-1-0/ |
| Weight Scale Profile | 1.0.1 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/weight-scale-profile-1-0-1/ |
## 3. 逐项详细说明
### 3.1 核心 / GATT 类
#### Alert Notification Profile
这是一个把“手机或主设备上的提醒事件”标准化转给外设的 Profile常见目标是手表、腕带、桌面提醒器之类的低功耗接收端。它关注的是类别化通知、已读状态、未读计数、立即提醒这类轻量信息而不是完整消息正文同步。工程上它适合做“提醒镜像”不适合做完整 IM 客户端。
#### Automation IO Profile
这个 Profile 面向简单工业控制和楼宇场景,核心思想是把离散输入、离散输出、模拟量等基础 I/O 点位做成统一抽象。它适合开关量、继电器、按钮、状态量等低复杂度控制接口,不强调复杂控制逻辑本身。工程上常被当作 BLE 版“轻量远程 I/O 面板”。
#### Calendar Tasks and Notes Profile
它的定位是让外设以标准方式访问日历、待办和笔记这类个人信息对象。和“通知”类 Profile 相比,它更偏对象化数据同步,强调项目集合、元数据、内容条目和访问流程。适用场景通常是可穿戴设备、车载设备或桌面配件做轻量 PIM 同步。
#### Device Time Profile
这个 Profile 的重点是设备侧时间基准管理适合让一个时间主设备给多个从设备做校时。它通常关心设备当前时间、时区、DST 或更新时间来源等信息。工程上它比通用的 Time Profile 更偏“设备维护”视角。
#### Environmental Sensing Profile
这是 BLE 里很常见的环境类 Profile覆盖温度、湿度、气压、露点、风速、空气质量等大量环境传感字段。它的价值在于给环境监测节点定义统一组织方式减少私有 GATT 设计。做传感器平台时,它通常是优先考虑的标准基线。
#### Find Me Profile
这是最早一批 BLE Profile 之一,场景非常直接:主设备触发外设发声、闪灯或震动,从而“找回”钥匙扣、标签或其他小设备。它的交互非常简单,通常没有复杂数据通道。工程上它偏“单向触发”,不是精确定位方案。
#### HID Over GATT Profile
HOGP 是 BLE 键盘、鼠标、遥控器、触控设备最核心的标准 Profile 之一。它把传统 HID 语义映射到 GATT让低功耗输入设备能和主机系统直接互通并支持报告模式、协议模式、Boot 兼容路径等。对键盘项目来说,它通常就是“标准输入外设”的第一选择。
#### Internet Protocol Support Profile
这个 Profile 的意义在于让 BLE 不只是传属性值,还能承载 IPv6/6LoWPAN 之类的上层网络协议。它面向的是更偏物联网基础设施的场景,而不是简单外设控制。工程上只有在你明确需要把设备纳入 IP 网络模型时才值得采用。
#### Object Transfer Profile
OTP 面向“对象”而不是“单个特征值”,适合在 BLE 上搬运图片、固件片段、记录文件、联系人卡片等具备元数据和可寻址性的内容。它通常依赖对象目录、对象元信息、读写偏移、创建删除等对象管理能力。和自定义长特征值相比,它更规范,但实现复杂度也更高。
#### Phone Alert Status Profile
PASP 主要用于把手机侧“来电铃声、振动、静音、提醒状态”这类手机告警状态同步给外设。它和 Alert Notification Profile 关系很近,但更强调电话和提醒设备状态而不是通知明细本身。典型设备是手环、腕表和桌面提醒器。
#### Proximity Profile
Proximity Profile 更关注链路“靠近/远离”带来的行为,比如距离太远时报警、靠近后自动静音或恢复。它通常依赖发射功率与链路损耗这类简单近远估计,不等同于现代高精度测距。工程上可以把它理解为 Find Me 的“距离关联版”。
#### Scan Parameters Profile
这个 Profile 主要用来让扫描方和被扫描方就扫描参数做一定程度的协同,目标是兼顾发现时延和功耗。它是很早期 BLE 生态为手机与外设配合而定义的补充机制。现代系统里它未必总是显性出现,但在兼容旧实现时仍值得知道。
#### Time Profile
Time Profile 是 BLE 时间同步体系的通用基线,关注当前时间、精度、参考时间来源以及时间更新。它适合需要基本时钟同步但又不想自定义时间服务的设备。和 Device Time Profile 相比,它更像“通用时间语义”的上层封装。
### 3.2 健康 / 医疗类
#### Blood Pressure Profile
这个 Profile 面向电子血压计,核心数据通常是收缩压、舒张压、平均动脉压、测量时间戳和脉搏等。它的价值在于让手机健康应用、家庭医疗网关和血压计之间具备一致的数据模型。工程上应特别注意单位、时序和一条记录的完整性。
#### Continuous Glucose Monitoring Profile
CGM Profile 面向连续血糖监测设备,强调连续采样、趋势、会话状态和告警管理。它比传统 Glucose Profile 更适合长时间持续数据流和设备状态追踪。医疗属性很强,实现时通常还要同时考虑记录缓存、断线补传和告警可靠性。
#### Generic Health Sensor Profile
从命名和生态位置看,它是为了给大量新型健康传感器提供一个更泛化的标准外壳,避免每类新传感器都重新造一套 Profile。它适合指标多样但又不值得为单一指标单独立 Profile 的健康设备。工程上可把它看作医疗/健康类的“可扩展通用框架”。
#### Glucose Profile
Glucose Profile 更偏间歇式测量设备,例如指尖采血血糖仪。它通常围绕测量记录、上下文信息、时间戳和历史数据访问来组织。和 CGM 相比,它不强调连续流,而更强调离散测量记录管理。
#### Health Thermometer Profile
这个 Profile 用于体温计或温度类健康设备,重点是体温测量值、测量部位、时间戳等。它在家庭健康设备中很常见,互操作也比较成熟。工程实现通常不复杂,但要处理好单位、一次性测量与持续测量模式的区分。
#### Heart Rate Profile
这是最常见的 BLE 健康/运动 Profile 之一,面向心率带、手环、运动设备。它核心传的是实时心率值,并可包含能量消耗或 RR-Interval 等附加信息。因为支持广、实现轻,很多平台把它当成 BLE 互操作示范案例。
#### Insulin Delivery Profile
这个 Profile 面向胰岛素泵或给药相关设备,重点是输注状态、历史记录、治疗参数和控制安全性。它比一般传感器 Profile 更强调状态机和风险控制语义。工程上要把“数据展示”和“可导致治疗动作的控制”严格分层。
#### Physical Activity Monitor Profile
它覆盖日常活动跟踪场景,如步数、活动时长、活动等级、消耗估算等。相比 Heart Rate 这类单一生理信号,它更偏行为统计与日常健康监测。适合手环、健康贴片、轻量可穿戴。
#### Pulse Oximeter Profile
这个 Profile 主要用于脉搏血氧设备,核心指标是 SpO2、脉率以及相关上下文。它在家用健康、睡眠监测和康复设备中很常见。工程上要区分瞬时显示值和可归档记录值避免 UI 和记录逻辑混淆。
#### Weight Scale Profile
Weight Scale Profile 面向体重秤和身体成分相关设备,基础数据是体重,也可带 BMI、身高、时间戳等信息。它的价值在于手机健康应用能直接理解标准数据而不需要厂商私有解析。对消费级产品来说这个 Profile 的生态成熟度很高。
### 3.3 运动 / 定位类
#### Cycling Power Profile
面向功率计、自行车台、训练设备,核心是功率值以及踏频、扭矩、左右平衡、曲线段等扩展信息。它属于运动传感里实现要求较高的一类,因为用户往往关心高刷新率和训练准确性。做骑行训练生态时,这个 Profile 很关键。
#### Cycling Speed and Cadence Profile
这个 Profile 用于车轮速度和踏频传感器,数据模型比功率计更轻。它非常适合电池供电的低功耗传感器做长续航设计。常见场景是码表、运动表、骑行 App 与车轮/踏频传感器互联。
#### Fitness Machine Profile
FTMP 面向跑步机、动感单车、划船机、椭圆机等健身器材,既包含器材上报,也允许控制类交互。它的意义是把“训练设备”从单纯传感器提升到可配置、可联动的训练终端。工程上需要特别留意控制权限和状态一致性。
#### Global Navigation Satellite System Profile
这个 Profile 面向 GNSS 接收设备,把位置、速度、卫星状态或导航相关信息以标准方式暴露给对端。它适合外置卫星定位模块、运动设备或定位记录器。工程上常见用途是把 GNSS 功能从主机中解耦到独立 BLE 外设。
#### Location and Navigation Profile
这个 Profile 是通用位置/导航数据封装,通常涵盖位置、速度、航向、海拔和导航点信息。它比单纯 GNSS 更高层,能容纳更多导航语义。适用于户外设备、船舶设备、骑行/跑步导航附件。
#### Running Speed and Cadence Profile
RSC Profile 面向跑步脚环、鞋夹或可穿戴传感器,关注跑速、步频、步长、跑/走状态等。它和骑行速度/踏频 Profile 类似,属于低功耗、低带宽但很实用的运动类标准。对跑步生态兼容性来说,它是基础件。
### 3.4 LE Audio 类
#### Basic Audio Profile
BAP 是 LE Audio 体系里的基础音频传输 Profile围绕单播/广播音频流、流配置和基本音频能力组织。可以把它理解为 LE Audio 的“音频承载基石”。很多更上层的音频 Profile 都会依赖它提供的流控制和能力表达。
#### Call Control Profile
CCP 用来表达通话相关控制语义,比如接听、挂断、保持、来电状态变化等。它的目的不是定义音频编解码,而是把“通话控制面”标准化。对耳机、免提设备、车载设备来说,它是电话业务的关键拼图。
#### Common Audio Profile
CAP 更像 LE Audio 各类设备协同行为的公共约束层,定义一组通用角色和组合方式,避免不同音频 Profile 各自形成孤岛。它通常与 CSIP、BAP 等配合使用。工程上可把它看成 LE Audio 设备行为一致性的“总装配规则”。
#### Coordinated Set Identification Profile
CSIP 用来把多个独立设备组织成一个协调集合,典型例子就是左右耳耳机、双扬声器或多设备音频系统。它解决的问题是“多个物理设备如何在逻辑上被识别为一组”。没有它,多设备配对、切换和同步体验会明显变差。
#### Gaming Audio Profile
从命名和 LE Audio 位置看,它面向游戏场景的低时延、双向音频和设备控制协同。它通常比通用媒体播放更强调时延预算、语音聊天和交互响应。工程上适合游戏耳机、掌机配件、无线麦克风方案。
#### Hearing Access Profile
HAP 面向助听器、听辅设备及其控制终端,重点是接入、控制、状态读取以及个性化听力功能配合。它是 LE Audio 在医疗辅助听力方向的重要接口层。实现时要特别关注可靠性、低时延和多设备协同。
#### Media Control Profile
MCP 把媒体播放控制抽象成标准接口,例如播放、暂停、下一曲、上一曲、快进、元数据查看等。它解决的是控制面统一,不直接替代音频流承载本身。对耳机、遥控器、车载控制面板都很有价值。
#### Microphone Control Profile
这个 Profile 聚焦麦克风相关控制,最典型的是静音/取消静音以及麦克风状态同步。它常用于耳机、会议设备、采集配件和语音终端。虽然看起来简单,但在多端协同和 UI 同步上很有必要。
#### Public Broadcast Profile
PBP 是 Auracast/Public Broadcast 体系中的关键 Profile解决的是公开广播音频如何被发现、识别和接入。它偏广播音频的“可发现性与可消费性”约束而不只是裸流传输。适合商场、机场、影院、公共导览等大范围音频分发场景。
#### Reconnection Configuration Profile
从命名和音频生态位置看,这个 Profile 用来描述设备断开后如何重连、优先重连谁、何时自动恢复等重连策略。它的目的通常是改善多主机、多耳塞、多场景切换时的用户体验。工程上它属于“体验型 Profile”对 TWS 这类设备尤其重要。
#### Telephony and Media Audio Profile
TMAP 是 LE Audio 设备在电话与媒体两大主场景中的角色组合规范。它更多定义“设备是什么、在这个场景里应该怎么协同”,而不是单一控制命令。可以把它理解为 LE Audio 终端类型与用例映射。
#### Voice Assistant Profile
从生态定位看VAP 面向语音助手接入场景,让耳机或外设能标准化启动助手会话、处理语音交互路径和相关状态。它通常和麦克风控制、音频流控制、设备角色协同使用。适用场景是耳机呼出助手、可穿戴语音入口、车载语音终端。
#### Volume Control Profile
VCP 负责音量控制与状态同步,包括绝对音量、音量步进、静音状态等。它的价值是让多类 LE Audio 终端对音量行为有一致表达。工程上如果你不希望每个设备都做私有音量协议,它是很自然的选择。
### 3.5 商业 / 工业 / 定位类
#### Asset Tracking Profile
这个 Profile 面向资产标签与定位基础设施,典型场景是仓储、零售、医院、物流等环境中的物品追踪。它通常会和广播、周期广播、方向查找或其他定位能力协作,而不是只靠一个静态 GATT 表。工程上它强调“标签、定位器、后台系统”三者的协同。
#### Authorization Control Profile
从命名看,它用于标准化“授权对象”和“访问控制状态”的 BLE 交互,适合门锁、闸机、设备启停授权、数字钥匙等场景。它的重点更像权限校验和授权流程,而不是通用传感。实现时安全边界、凭证生命周期和离线行为都应优先设计。
#### Binary Sensor Profile
这是一个很实用但语义非常克制的 Profile用来表达开/关、在/不在、触发/未触发、占用/未占用这类二值状态。它适合门磁、按钮、限位、干接点、简单安防触发器等设备。它的优势是简单、低功耗、互操作成本低。
#### Cookware Profile
从命名和家电生态位置看,它面向智能锅具、烹饪容器或配套温控设备。核心信息大概率围绕锅具温度、烹饪阶段、加热状态、定时和食材完成度提示。它适合做厨房配件的标准化接口,但实际产品里常仍会叠加厂商私有逻辑。
#### Electronic Shelf Label Profile
ESL Profile 是零售领域非常明确的商业场景标准,用于价签设备和控制基础设施之间的低功耗、批量、可靠更新。它强调群组调度、同步刷新、超低功耗待机和门店级大规模部署能力。对商超场景来说,这是 BLE 向“基础设施协议”扩展的代表作。
#### Emergency Profile
从名称判断,它服务于紧急告警、求助、事件上报和联动响应这类场景,例如老人监护、工业告警、紧急按钮设备。它通常不只是传一个布尔值,更强调事件等级、状态和响应流程。工程实现时要把“告警一定要送达”的可靠性目标单独考虑。
#### Industrial Measurement Device Profile
这个 Profile 面向工业测量设备,例如压力表、流量计、过程测量节点、便携仪表等。它的目标是让工业现场数据采集具备统一结构,而不是每家仪表都自定义私有 GATT。对工业场景来说它比消费电子 Profile 更强调量测语义和系统集成。
#### Ranging Profile
Ranging Profile 是 BLE 进入高精度距离测量后的关键 Profile核心目的就是把测距流程、角色和结果表达标准化。它通常适用于数字钥匙、物体接近判断、室内精细定位和安全接入场景。和早期 Proximity Profile 相比,它更接近“精确距离感知”而不是粗略远近估计。
### 3.6 Mesh / NLC 类
#### Mesh Configuration Database Profile
这个 Profile 更偏管理面,关注 Mesh 网络的配置数据库如何表达、交换和持久化。它不是普通终端用户直接感知的功能,而是配置工具、网关、后台系统之间保持网络一致性的基础。工程上它有助于把 Mesh 网络从“临时配网”提升为“可运维系统”。
#### Mesh Profile
Mesh Profile 是 Bluetooth Mesh 的核心定义了节点、消息中继、发布订阅、低功耗节点、Friend、代理等网络行为。虽然它不是传统意义上的单设备 GATT Profile但它的承载底座仍是 Bluetooth LE 广播和相关承载机制,所以应纳入 BLE 体系讨论。对照明、楼宇和大规模传感网络来说,它是总根。
#### Ambient Light Sensor NLC Profile
这个 NLC Profile 面向网络化照明控制里的环境光传感器节点。它把环境照度读数纳入 NLC 语义,使照明系统能做日光补偿、节能调光和场景联动。适合办公室、商业照明和智能楼宇。
#### Basic Lightness Controller NLC Profile
它对应最基础的亮度控制器角色,强调“以标准化方式控制灯光亮度”。和底层 Mesh Model 相比NLC Profile 给的是更高层、面向成品设备的约束组合。工程上它降低了不同厂商控制面板和灯具的对接成本。
#### Basic Scene Selector NLC Profile
这个 Profile 用来标准化场景选择器,例如面板上的“会议模式、演示模式、离开模式”按钮。它适合把复杂灯光/楼宇状态打包成少量场景入口。对用户交互而言,它比直接调模型参数友好得多。
#### Dimming Control NLC Profile
这个 Profile 更强调连续调光过程和控制行为,比单纯的“设定亮度值”更贴近真实调光器交互。它适合旋钮面板、滑条、墙控设备等。工程上通常和灯光控制器、亮度控制逻辑配合使用。
#### Energy Monitor NLC Profile
它把能耗监测设备纳入 NLC 语义,可用于照明回路或楼宇设备的电能数据采集。场景包括能耗可视化、节能优化和维护分析。它的价值在于让照明系统不只“能控”,还能“可度量”。
#### HVAC Integration NLC Profile
这个 Profile 体现了 NLC 从照明向楼宇环境系统扩展的趋势,用于暖通空调相关设备和照明/楼宇控制系统互联。它适合温控器、风机盘管接口、环境联动面板等设备。工程上它更强调跨子系统协同,而不只是灯控本身。
#### Occupancy Sensor NLC Profile
这个 Profile 面向人体存在/占用检测节点,是楼宇自动化里非常核心的传感输入。它常用于自动开灯、延时关灯、空调节能、会议室占用判断。和 Binary Sensor 相比,它位于 NLC 生态内部,强调与照明/楼宇场景联动。
## 4. 未纳入的网页条目
Bluetooth SIG 规格页里还存在不少名称中带有 `Profile` 的条目,但它们不属于 BLE 体系,或者主要是 BR/EDR / Classic Bluetooth 方向,因此本次没有纳入,例如:
- 3D Synchronization Profile
- A/V Remote Control Profile
- Advanced Audio Distribution Profile
- Basic Imaging Profile
- Basic Printing Profile
- BR/EDR Connection Handover Profile
- Device Identification Profile
- Dial-Up Networking Profile
- File Transfer Profile
- Generic A/V Distribution Profile
- Generic Object Exchange Profile
- Generic PIM Profile
- Hands-Free Profile
- Hardcopy Cable Replacement Profile
- Headset Profile
- Health Device Profile
- Human Interface Device Profile
- Message Access Profile
- Multi Profile Specification
- Object Push Profile
- Personal Area Networking Profile
- Phone Book Access Profile
- Serial Port Profile
- SIM Access Profile
- Synchronization Profile
- Video Distribution Profile
说明:其中个别条目可能在现代产品里和 LE 设备共存,但其规范本体并不是“基于 BLE 的 Profile”所以这里按“协议归属”而不是“产品是否可能双模”来排除。
## 5. 对 `new_kbd` 项目的直接参考价值
如果你的项目是键盘/输入设备,最直接相关的是:
- `HID Over GATT Profile`BLE 键盘主标准
- `Battery Service``Device Information Service``Bonding/Security` 等虽然不在本文 Profile 汇总内,但会和 HOGP 一起构成实际产品骨架
- 如果后续要做查找键盘、靠近告警,可参考 `Find Me Profile` / `Proximity Profile`
- 如果要做文件、配置对象同步,可看 `Object Transfer Profile` 的对象化思路,但一般键盘产品没必要直接上完整 OTP
如果要继续,我可以在下一步再补一份“**和 BLE 键盘最相关的 Profile / Service / Characteristic 清单**”,直接映射到你当前 `new_kbd` 工程的实现关注点。

View File

@@ -0,0 +1,791 @@
# 基于 Zephyr 的 LVGL 移植说明
## 1. 目标
本文档记录 `new_kbd` 项目在 `atguigu_mini_keyboard/nrf52840` 板卡上调通 LCD 与 LVGL 的过程,重点覆盖以下内容:
- 板级设备树需要满足的显示与背光条件
- 如何先用最基础的 Zephyr `display` 子系统验证硬件链路
- 如何切回 Zephyr 自带的 LVGL 适配层
- 当前项目已经落地的关键配置
- 常见问题与排查顺序
本文档对应的工程位置:
- 应用目录: `E:\projects\new_kbd`
- 板卡目录: `E:\extra\boards\atguigu\atguigu_mini_keyboard`
---
## 2. 硬件链路确认
当前板卡上的显示相关资源如下:
- 面板驱动: `sitronix,st7789v`
- 总线: `mipi-dbi-spi`
- 显示节点: `st7789v3`
- 背光: `pwm_leds/backlight`
- 背光 PWM 输出: `pwm0`
板级 DTS 中的关键节点在:
- [atguigu_mini_keyboard.dts](E:\extra\boards\atguigu\atguigu_mini_keyboard\atguigu_mini_keyboard.dts)
- [atguigu_mini_keyboard-pinctrl.dtsi](E:\extra\boards\atguigu\atguigu_mini_keyboard\atguigu_mini_keyboard-pinctrl.dtsi)
当前已经确认:
- 屏幕本身可以通过 Zephyr `display_write()` 正常显示颜色切换画面
- 背光不是普通 GPIO 开关,而是 `pwm-leds` 设备
- `p0.13` 与当前 LCD 使能无关,已经从显示路径里移除
这意味着:
- SPI/MIPI DBI 链路是通的
- ST7789V 初始化参数至少在当前板上可用
- 背光控制链路是通的
- 后续如果 LVGL 出现黑屏,优先怀疑的是 LVGL 初始化、线程模型或对象刷新,而不是底层显示驱动
---
## 3. 设备树要求
应用侧 overlay 在:
- [app.overlay](E:\projects\new_kbd\app.overlay)
当前与显示相关的关键要求有三条:
### 3.1 选择显示设备
必须在 `chosen` 中指定:
```dts
/ {
chosen {
zephyr,display = &st7789v3;
};
};
```
否则 Zephyr 的 `display` 子系统和 LVGL 模块都找不到目标显示设备。
### 3.2 打开显示控制器和面板节点
应用 overlay 中需要显式把下面这些节点设为 `okay`:
```dts
&spi2 {
status = "okay";
};
&mipi_dbi {
status = "okay";
};
&st7789v3 {
status = "okay";
};
```
### 3.3 打开 PWM 背光
当前背光不是由显示驱动自动管理,而是走 LED/PWM 子系统,所以还需要:
```dts
/ {
aliases {
backlight = &backlight;
};
};
&pwm_leds {
status = "okay";
};
&pwm0 {
status = "okay";
};
```
这样应用代码里可以通过 `LED_DT_SPEC_GET(DT_NODELABEL(backlight))` 拿到背光设备,再调用 `led_set_brightness_dt()` 设亮度。
---
## 4. 先验证裸显示,再切回 LVGL
这次移植采用了两阶段策略:
### 4.1 第一阶段: 先验证 Zephyr `display` 子系统
在这个阶段,应用不启用 LVGL而是直接调用:
- `display_get_capabilities()`
- `display_blanking_off()`
- `display_write()`
通过 RGB565 彩条整屏写入,确认:
- 面板已经真正收到像素数据
- 刷新链路不是空转
- 背光与显示内容是解耦的
这个阶段已经验证成功,屏幕可以正常显示颜色切换画面。
### 4.2 第二阶段: 切回 Zephyr 自带 LVGL 适配层
在确认底层链路没问题后,再恢复 LVGL。这样如果再次黑屏就可以把排查范围收缩到:
- LVGL 是否真的初始化成功
- UI 是否在正确线程创建
- 对象是否被正确刷新
- `display_blanking_off()` 与背光使能时机是否正确
这个两阶段方法是这次调试中最关键的分界线。
---
## 5. 当前 LVGL 方案
当前项目使用的是 Zephyr 自带的 LVGL 移植,不再手写 `lv_timer_handler()` 主循环。
相关应用代码在:
- [display_module.c](E:\projects\new_kbd\src\modules\display_module.c)
当前方案要点如下:
### 5.1 使用 Zephyr 自带的 LVGL 自动初始化
依赖 `CONFIG_LV_Z_AUTO_INIT=y`,由 Zephyr 在应用启动前完成:
- `lv_init()`
- display 注册
- 渲染缓冲区创建
- LVGL 核心初始化
因此应用层不需要再手动调用 `lvgl_init()`
### 5.2 使用 Zephyr 的 LVGL workqueue
依赖 `CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE=y`
这样 LVGL 核心 `lv_timer_handler()` 由 Zephyr 模块自己调度,应用层不再手动驱动 LVGL 时钟。
应用里如果还需要定期更新 UI当前做法是:
- 自己维护一个 `k_work_delayable`
- 通过 `lvgl_get_workqueue()` 把这个 work 投递到 LVGL 专用 workqueue
- 在 work 回调里 `lvgl_lock()` 后访问 LVGL API
这样做的原因是:
- 避免 UI 更新逻辑和 LVGL 核心不在同一执行上下文
- 避免对象创建、样式更新与内部刷新竞争
- 对当前板子来说,这是比“任意线程加锁直接操作 LVGL”更保守、更容易稳定的方案
### 5.3 先开背光,再创建 UI
当前时序是:
1. `display_blanking_off()`
2. `display_backlight_init()`
3. 将显示模块标记为 ready
4. 立即在 LVGL workqueue 上调度一次 UI 更新
背光初始化通过 `pwm-leds` 完成,不再依赖额外 GPIO。
### 5.4 当前 UI 验证策略
为了避免“画面有了但刚好看不见”的误判,当前 UI 使用了高对比度方案:
- 背景色在两种颜色之间切换
- 标题为 `Zephyr LVGL running`
- 计数文本为 `tick N`
如果这三项都能显示,基本可以判定 LVGL 渲染链路已经正常工作。
---
## 6. 当前关键配置
应用配置文件在:
- [prj.conf](E:\projects\new_kbd\prj.conf)
当前与 LVGL/显示相关的关键配置如下:
```conf
CONFIG_DISPLAY=y
CONFIG_MIPI_DBI=y
CONFIG_ST7789V=y
CONFIG_LVGL=y
CONFIG_LV_CONF_MINIMAL=y
CONFIG_LV_BUILD_EXAMPLES=n
CONFIG_LV_BUILD_DEMOS=n
CONFIG_LV_USE_LABEL=y
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_Z_AUTO_INIT=y
CONFIG_LV_Z_BITS_PER_PIXEL=16
CONFIG_LV_Z_LVGL_MUTEX=y
CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE=y
CONFIG_LV_Z_MEM_POOL_SIZE=16384
```
各项含义:
- `CONFIG_LV_Z_BITS_PER_PIXEL=16`
与 ST7789V 当前 `RGB565` 像素格式对应,避免渲染缓冲格式不匹配
- `CONFIG_LV_Z_LVGL_MUTEX=y`
提供 `lvgl_lock()` / `lvgl_unlock()`,确保应用层访问 LVGL API 时有统一互斥保护
- `CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE=y`
让 Zephyr 自动维护 LVGL 核心执行循环,不需要应用手动跑 `lv_timer_handler()`
- `CONFIG_LV_Z_MEM_POOL_SIZE=16384`
作为当前最小可工作的内存池配置
---
## 7. 构建方式
当前构建已不再默认强制使用 `-Og`,而是遵从工程自身配置。
当前工程已经启用:
```conf
CONFIG_SIZE_OPTIMIZATIONS=y
```
因此默认会走 size optimization适合当前 OTA 分区大小约束。
### 7.1 当前构建 skill 的行为
`ncs-fresh-build` skill 已经调整为:
- 不再默认注入 `CONFIG_DEBUG_OPTIMIZATIONS=y`
- 默认构建目录为项目根下的 `build`
- 如果项目下已有 `build` 且带构建元数据,则执行 `resume build`
- 如果不存在,则在项目根下创建 `build`
也就是说,当前默认构建输出目录是:
- `E:\projects\new_kbd\build`
### 7.2 当前已验证的构建结果
在当前 LVGL 配置下,系统构建通过,成功产出:
- `merged.hex`
- `dfu_application.zip`
最近一次 LVGL 镜像的资源占用约为:
- FLASH: `437780 / 482816`
- RAM: `96052 / 256 KB`
说明在当前 OTA 分区布局下LVGL 版本仍然可以装下。
---
## 8. 当前显示模块代码结构
当前显示模块入口在:
- [display_module.c](E:\projects\new_kbd\src\modules\display_module.c)
整体逻辑如下:
1. 等待 `main` 模块进入 `MODULE_STATE_READY`
2. 检查显示设备是否 ready
3. 读取显示能力并打印日志
4. 调用 `display_blanking_off()`
5. 初始化背光 PWM 亮度
6. 将模块标记为 initialized
7. 把第一个 UI 更新 work 投递到 LVGL workqueue
8. 后续每秒更新一次背景与计数文本
这种结构的好处是:
- 显示模块与主应用启动顺序解耦
- LVGL 核心循环交给 Zephyr 模块维护
- 应用只负责自己的 UI 逻辑
- 以后如果要继续扩展页面,也可以沿用同样的工作队列模型
---
## 9. 常见问题与排查顺序
### 9.1 背光亮,但完全黑屏
先不要怀疑 LVGL先切回裸 `display_write()` 测试版本。
如果裸显示也黑屏,优先检查:
- `zephyr,display` 是否指向正确节点
- `spi2` / `mipi_dbi` / `st7789v3` 是否都为 `okay`
- ST7789V 初始化参数是否匹配当前屏
- 面板 offset / colmod / madctl 是否正确
### 9.2 裸 `display_write()` 正常,但 LVGL 黑屏
优先检查:
- `CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE` 是否启用
- 应用是否仍在手动调用 `lv_timer_handler()`
- UI 更新是否发生在错误线程
- 是否遗漏 `lvgl_lock()`
- 文本和背景颜色是否刚好一致
### 9.3 背光不亮
优先检查:
- `backlight` alias 是否存在
- `&pwm_leds` 是否启用
- `&pwm0` 是否启用
- `pwm0_default` 的 pinctrl 是否正确
- 应用是否真的调用了 `led_set_brightness_dt()`
### 9.4 构建能过,但镜像塞不进 OTA 分区
优先检查:
- 是否误用了 `-Og`
- `CONFIG_SIZE_OPTIMIZATIONS` 是否生效
- 是否启用了不必要的 LVGL demos/examples
- 字体、部件和日志级别是否过大
---
## 10. 后续建议
如果后续继续完善显示功能,建议按下面顺序推进:
1. 先固定当前最小可工作 UI不要同时引入太多控件
2. 增加 RTT 日志,确认 LVGL 初始化和周期更新是否稳定
3. 再逐步接入输入设备或更复杂页面
4. 最后再考虑动画、图片资源和更大的字体
对当前项目来说,最重要的经验是:
- 先把底层 `display` 写屏跑通
- 再把问题收敛到 LVGL
- 最后再处理更高层的 UI 逻辑
这样可以显著降低显示移植的排查成本。
---
## 11. ST7789V 设备树参数详细说明
当前板上显示节点位于:
- [atguigu_mini_keyboard.dts](E:\extra\boards\atguigu\atguigu_mini_keyboard\atguigu_mini_keyboard.dts)
典型配置如下:
```dts
st7789v3: st7789v@0 {
compatible = "sitronix,st7789v";
status = "disabled";
reg = <0>;
mipi-max-frequency = <32000000>;
width = <320>;
height = <172>;
x-offset = <0>;
y-offset = <34>;
vcom = <0x2b>;
gctrl = <0x35>;
vrhs = <0x11>;
vdvs = <0x20>;
mdac = <0x60>;
lcm = <0x2c>;
colmod = <0x55>;
gamma = <0x01>;
porch-param = [ 0c 0c 00 33 33 ];
cmd2en-param = [ 5a 69 02 01 ];
pwctrl1-param = [ a4 a1 ];
pvgam-param = [ d0 00 02 07 0a 28 32 44 42 06 0e 12 14 17 ];
nvgam-param = [ d0 00 02 07 0a 28 31 54 47 0e 1c 17 1b 1e ];
ram-param = [ 00 f0 ];
rgb-param = [ c0 02 14 ];
mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";
};
```
下面按类别说明这些参数的意义。
### 11.1 设备身份与总线绑定
#### `compatible = "sitronix,st7789v"`
作用:
- 告诉 Zephyr 使用 ST7789V 显示驱动
- 绑定文件来自 [sitronix,st7789v.yaml](C:\ncs\v3.2.3\zephyr\dts\bindings\display\sitronix,st7789v.yaml)
- 驱动实现位于 [display_st7789v.c](C:\ncs\v3.2.3\zephyr\drivers\display\display_st7789v.c)
如果这个 `compatible` 不对,后续所有参数即使写对也不会被正确解释。
#### `reg = <0>`
作用:
- 这是该显示器在父 `mipi_dbi` 总线上的片选号/地址索引
- 对 SPI 模式来说,它最终会映射到父 SPI 控制器 `cs-gpios` 数组中的第几个片选
当前值是 `0`,表示使用父 SPI 控制器的第 0 个 CS。
#### `mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE"`
作用:
- 指定 ST7789V 通过 MIPI DBI Type-C 的 4 线 SPI 模式工作
- 命令/数据区分不是靠 9-bit SPI而是靠单独的 `dc-gpios`
这对当前板子很重要,因为板上已经专门引出了 DC GPIO。
#### `mipi-max-frequency = <32000000>`
作用:
- 指定面板接口希望使用的最高 SPI 时钟
- 这个值会进入 `struct spi_config.frequency`
- 最终实际频率还会受底层 SPI 控制器实例上限限制
当前项目中:
- 面板这里写的是 `32 MHz`
- 底层已经把 LCD 切到 `spi3`
- `spi3` 是 nRF52840 上支持 `32 MHz` 的高速 SPIM 实例
因此这个值现在是有效的,不再像早期挂在 `spi2` 时那样被 SoC 侧 `8 MHz` 上限卡住。
---
### 11.2 分辨率与有效显示窗口
#### `width = <320>`
作用:
- Zephyr `display_get_capabilities()` 返回的水平分辨率
- LVGL 也会按这个宽度创建显示对象和渲染缓冲
在当前横屏模式下,宽度已经从竖屏时的 `172` 改为 `320`
#### `height = <172>`
作用:
- Zephyr `display_get_capabilities()` 返回的垂直分辨率
- 决定应用层看到的逻辑屏高
在当前横屏模式下,高度已经从竖屏时的 `320` 改为 `172`
#### `x-offset = <0>`
作用:
- 指定 LCD 实际可视窗口在 ST7789V GRAM 中的列偏移
- 驱动在写入时,会把应用层传入的 `x` 坐标再加上这个偏移
对应驱动代码:
- `st7789v_set_lcd_margins()`
- `st7789v_set_mem_area()`
#### `y-offset = <34>`
作用:
- 指定 LCD 实际可视窗口在 ST7789V GRAM 中的行偏移
- 驱动会把应用层传入的 `y` 坐标再加上这个偏移
为什么横屏时从 `x-offset=34` 变成 `y-offset=34`:
- 这块屏的物理可视区域不是完整的 240x320而是裁剪出来的 `172x320`
- 当通过 `mdac` 做 XY 轴交换后,原来沿 X 方向的裁剪,需要同步转移到 Y 方向
这是横屏改造里最容易遗漏的一点。如果只改 `width/height``mdac`,不改 offset画面通常会出现:
- 偏移错位
- 局部黑边
- 显示越界
---
### 11.3 方向控制
#### `mdac = <0x60>`
作用:
- 对应 ST7789V 的 `MADCTL` 寄存器,即 Memory Data Access Control
- 用于控制:
- X/Y 轴交换
- 左右镜像
- 上下镜像
- RGB/BGR 顺序
对应驱动里的寄存器命令:
- `ST7789V_CMD_MADCTL`
当前值 `0x60` 的核心意义是:
- 打开 `MV`,进行 XY 轴交换
- 再配合镜像位,把竖屏坐标系旋转为横屏
对当前项目来说,`mdac` 是“横竖屏切换的核心参数”。
如果上板后发现:
- 画面已经横过来,但方向反了
- 或左右/上下镜像不对
通常只需要继续调整 `mdac`,而不一定需要再动 `width/height`
---
### 11.4 面板电气与模拟参数
下面这几项大多属于“面板电气初始化参数”,通常来自原厂例程、模组 demo、已有验证过的初始化序列或者靠经验调通。
它们的共同特点是:
- 不建议随便改
- 改错后往往不是“完全不亮”,而是
- 偏色
- 闪烁
- 对比度差
- 稳定性差
- 局部异常
#### `vcom = <0x2b>`
作用:
- VCOM 电压相关设置
- 影响液晶驱动偏置和整体显示稳定性
常见现象:
- 值不合适时,可能出现闪烁、灰阶异常、残影或对比度不自然
#### `gctrl = <0x35>`
作用:
- Gate Control控制面板栅极驱动相关参数
它更接近面板扫描电路的底层设置,一般按已知可工作配置保留。
#### `vrhs = <0x11>`
作用:
- VRH setting电压参考相关参数之一
#### `vdvs = <0x20>`
作用:
- VDV setting和驱动电压微调相关
注意:
- 驱动里只有同时存在 `vrhs``vdvs` 时,才会开启对应的 `VDVVRHEN` 流程
#### `lcm = <0x2c>`
作用:
- LCM control面板控制相关寄存器值
一般与模组硬件特性绑定,不建议脱离原配置单独尝试。
#### `gamma = <0x01>`
作用:
- Gamma curve 选择
- 会影响亮度过渡、灰阶、色彩观感
---
### 11.5 像素格式与颜色相关
#### `colmod = <0x55>`
作用:
- 对应 `COLMOD`,即 Interface Pixel Format
- 决定总线传输的像素位宽
当前值 `0x55` 表示 `RGB565 / 16-bit` 模式,这与当前软件配置完全一致:
- `CONFIG_ST7789V_RGB565=y`
- `CONFIG_LV_Z_BITS_PER_PIXEL=16`
如果这里和软件配置不匹配,常见现象包括:
- 颜色错乱
- 红蓝交换
- 图像看起来像“马赛克”或数据错位
#### `ram-param = [ 00 f0 ]`
作用:
- 对应 `RAMCTRL`
- 控制显示 RAM 访问的一些底层行为
这类参数通常和像素格式、总线模式、厂商推荐初始化序列成组使用。
#### `rgb-param = [ c0 02 14 ]`
作用:
- 对应 `RGBCTRL`
- 设置 RGB 接口/时序相关参数
虽然当前走的是 SPI MIPI DBI不是传统 RGB 并口,但 ST7789V 仍要求这组寄存器初始化。
---
### 11.6 时序与前后肩参数
#### `porch-param = [ 0c 0c 00 33 33 ]`
作用:
- 对应 `PORCTRL`
- 设置 porch也就是显示时序中的前后肩、空白区参数
这些参数会影响:
- 帧时序稳定性
- 扫描边界
- 某些情况下的闪烁或边缘异常
这组值一般与具体模组匹配,建议保留已有验证值。
---
### 11.7 扩展命令与电源控制参数
#### `cmd2en-param = [ 5a 69 02 01 ]`
作用:
- 对应 `CMD2EN`
- 开启 ST7789V 扩展命令页
如果这一步配置错误,后面某些扩展寄存器写入可能根本不会生效。
#### `pwctrl1-param = [ a4 a1 ]`
作用:
- 对应 `PWCTRL1`
- 电源控制相关参数
这会影响面板驱动供电行为和显示稳定性。
---
### 11.8 Gamma 曲线表
#### `pvgam-param = [ d0 00 02 07 0a 28 32 44 42 06 0e 12 14 17 ]`
作用:
- 对应 `PVGAMCTRL`
- Positive Voltage Gamma Control 参数表
#### `nvgam-param = [ d0 00 02 07 0a 28 31 54 47 0e 1c 17 1b 1e ]`
作用:
- 对应 `NVGAMCTRL`
- Negative Voltage Gamma Control 参数表
这两组参数共同决定:
- 亮暗过渡
- 色调倾向
- 灰阶表现
调这些参数通常属于“画质微调”,不是基础 bring-up 阶段的首选手段。除非当前已经能稳定显示,只是观感明显不对,否则不建议优先改这里。
---
### 11.9 这些参数里最关键、最常改的是哪些
在实际移植中,最常需要改的是下面这几项:
- `width`
- `height`
- `x-offset`
- `y-offset`
- `mdac`
- `mipi-max-frequency`
- `colmod`
其中:
- `width/height/x-offset/y-offset/mdac`
主要决定方向、有效显示区域和坐标映射
- `mipi-max-frequency`
主要决定带宽上限
- `colmod`
主要决定像素格式是否与软件配置匹配
而像下面这些:
- `vcom`
- `gctrl`
- `vrhs`
- `vdvs`
- `lcm`
- `gamma`
- `porch-param`
- `cmd2en-param`
- `pwctrl1-param`
- `pvgam-param`
- `nvgam-param`
- `ram-param`
- `rgb-param`
更像是“面板模组初始化模板的一部分”,通常是在已有可工作基础上尽量保持不动。
---
### 11.10 当前项目的实用建议
对当前这块屏来说,后续如果还要继续调整显示方向或显示区域,建议按下面顺序操作:
1. 先改 `mdac`
2. 再配套检查 `width/height`
3. 最后再修 `x-offset/y-offset`
不要一上来同时改所有寄存器,否则很难判断到底是哪一项导致了:
- 图像颠倒
- 画面偏移
- 部分区域黑边
- 写入越界
对当前项目,真正应该高频修改的设备树参数,其实主要就是这 5 项:
- `width`
- `height`
- `x-offset`
- `y-offset`
- `mdac`

View File

@@ -0,0 +1,926 @@
# NCS v3.2.3 集成的 GATT Service 汇总
更新时间2026-04-01Asia/Hong_Kong
## 1. 统计口径
本文面向 `C:\ncs\v3.2.3` 代码树中**应用可直接集成**的 GATT Service。
筛选规则:
-`zephyr/include/zephyr/bluetooth/services/``nrf/include/bluetooth/services/` 中公开头文件为主
-`zephyr/subsys/bluetooth/services/``nrf/subsys/bluetooth/services/` 中实际注册 GATT 服务的实现为准
- 优先整理**服务端**能力,也就是“本机作为 Peripheral/Server 暴露 GATT Service”时的用法
- `*_client``hogp``gattp` 这类主要用于 Central 侧发现/访问远端服务的客户端辅助库,不作为本文主表主体
按这个口径,当前 SDK 中可直接拿来做本地 GATT Service 的模块主要分为两类:
- Zephyr 自带标准服务
- NCS / Nordic 扩展服务
## 2. 先看结论
### 2.1 Zephyr 自带标准服务
| Service | Kconfig | 头文件 | 典型用途 |
| --- | --- | --- | --- |
| Alert Notification Service | `CONFIG_BT_ANS` | `<zephyr/bluetooth/services/ans.h>` | 对手表/手环输出通知类别与未读数 |
| Battery Service | `CONFIG_BT_BAS` | `<zephyr/bluetooth/services/bas.h>` | 暴露电量、电池状态 |
| Current Time Service | `CONFIG_BT_CTS` | `<zephyr/bluetooth/services/cts.h>` | 给客户端提供当前时间/校时 |
| Device Information Service | `CONFIG_BT_DIS` | `<zephyr/bluetooth/services/dis.h>` | 暴露厂商、型号、版本、PnP ID 等静态信息 |
| Heart Rate Service | `CONFIG_BT_HRS` | `<zephyr/bluetooth/services/hrs.h>` | 心率设备 |
| Immediate Alert Service | `CONFIG_BT_IAS` | `<zephyr/bluetooth/services/ias.h>` | 查找设备、远程触发蜂鸣/闪灯 |
| Nordic UART ServiceZephyr 版) | `CONFIG_BT_ZEPHYR_NUS` | `<zephyr/bluetooth/services/nus.h>` | 简单双向串口透传 |
| Object Transfer Service | `CONFIG_BT_OTS` | `<zephyr/bluetooth/services/ots.h>` | 面向对象的数据传输 |
| Tx Power Service | `CONFIG_BT_TPS` | 无独立公共 API 头 | 暴露当前发射功率 |
### 2.2 NCS / Nordic 扩展服务
| Service | Kconfig | 头文件 | 典型用途 |
| --- | --- | --- | --- |
| Bond Management Service | `CONFIG_BT_BMS` | `<bluetooth/services/bms.h>` | 让对端触发删 bond |
| Continuous Glucose Monitoring Service | `CONFIG_BT_CGMS` | `<bluetooth/services/cgms.h>` | 连续血糖监测 |
| Direction and Distance Finding Service | `CONFIG_BT_DDFS` | `<bluetooth/services/ddfs.h>` | 方位/距离测量结果输出 |
| Fast Pair Provider Service | `CONFIG_BT_FAST_PAIR` + `CONFIG_BT_FAST_PAIR_GATT_SERVICE` | `<bluetooth/services/fast_pair/fast_pair.h>` | Google Fast Pair |
| Human Interface Device Service | `CONFIG_BT_HIDS` | `<bluetooth/services/hids.h>` | BLE 键盘、鼠标、输入设备 |
| Latency Service | `CONFIG_BT_LATENCY` | `<bluetooth/services/latency.h>` | 延迟测试/回环 |
| LED Button Service | `CONFIG_BT_LBS` | `<bluetooth/services/lbs.h>` | 示例级 LED/Button 交互 |
| Memfault Diagnostic Service | `CONFIG_BT_MDS` | `<bluetooth/services/mds.h>` | Memfault 诊断数据导出 |
| Nordic Status Message Service | `CONFIG_BT_NSMS` | `<bluetooth/services/nsms.h>` | 暴露一段可读状态文本 |
| Nordic UART ServiceNordic 旧版) | `CONFIG_BT_NUS` | `<bluetooth/services/nus.h>` | NCS 样例常用串口透传 |
| Ranging Service | `CONFIG_BT_RAS` + `CONFIG_BT_RAS_RRSP` | `<bluetooth/services/ras.h>` | Channel Sounding / Ranging 服务端 |
| Running Speed and Cadence Service | `CONFIG_BT_RSCS` | `<bluetooth/services/rscs.h>` | 跑步速度步频 |
| Throughput Service | `CONFIG_BT_THROUGHPUT` | `<bluetooth/services/throughput.h>` | 吞吐测试 |
| Wi-Fi Provisioning Service | `CONFIG_BT_WIFI_PROV` | `<bluetooth/services/wifi_provisioning.h>` | 通过 BLE 给设备配 Wi-Fi |
## 3. 使用方式总规律
大多数服务都遵循类似流程:
1.`prj.conf` 里打开对应 `CONFIG_BT_*`
2. 包含头文件
3. 若服务需要运行时初始化,则在 `bt_enable()` 前后调用对应 `init/register`
4. 连接建立后,通过通知/读写回调/API 更新服务数据
5. 在广告里决定是否放服务 UUID但这不是注册服务的必要条件
有三类例外要先记住:
- **纯 Kconfig 静态服务**:例如 `DIS``TPS`,启用配置后就自动挂到 GATT DB上层几乎没有运行时 API
- **宏定义式实例服务**:例如 `NSMS``Zephyr NUS` 多实例、`HIDS`,需要先用宏定义实例,再初始化或更新
- **复杂协议服务**:例如 `OTS``RAS``Fast Pair`,除了开 Kconfig还要按它自己的状态机/回调/缓冲机制接入
## 4. 逐项说明
## 4.1 Zephyr 标准服务
### 4.1.1 Alert Notification Service, `ANS`
源码位置:
- `zephyr/subsys/bluetooth/services/ans.c`
- `zephyr/include/zephyr/bluetooth/services/ans.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_ANS=y
```
怎么用:
- 先在 Kconfig 里决定支持哪些提醒类别,例如 `CONFIG_BT_ANS_NALRT_CAT_EMAIL=y`
- 运行时如果还想动态设置支持位图,可以调用 `bt_ans_set_new_alert_support_category()``bt_ans_set_unread_support_category()`
- 有新提醒时调用 `bt_ans_notify_new_alert(conn, category, num_new, text)`
- 未读数变化时调用 `bt_ans_set_unread_count(conn, category, unread)`
适合场景:
- 手环、手表、桌面提醒器
- 你只想同步“有提醒/未读数/提醒类别”,而不是完整消息正文
注意点:
- 头文件明确写了 `bt_ans_notify_new_alert()` / `bt_ans_set_unread_count()` 会拿互斥锁,**不要从 BT RX 线程或 System Workqueue 里直接调用**
- 这个服务偏“提醒镜像”,不适合做完整消息中心
### 4.1.2 Battery Service, `BAS`
源码位置:
- `zephyr/subsys/bluetooth/services/bas/`
- `zephyr/include/zephyr/bluetooth/services/bas.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_BAS=y
```
如果要扩展电池状态:
```conf
CONFIG_BT_BAS_BLS=y
CONFIG_BT_BAS_BCS=y
```
怎么用:
- 最基本只需要周期性调用 `bt_bas_set_battery_level(level)`,把 0..100 的电量同步给客户端
- 读取当前缓存值可用 `bt_bas_get_battery_level()`
- 如果启用了扩展状态特征,可继续设置:
- `bt_bas_bls_set_battery_present()`
- `bt_bas_bls_set_battery_charge_state()`
- `bt_bas_bls_set_battery_charge_level()`
- `bt_bas_bls_set_service_required()`
- `bt_bas_bls_set_battery_fault()`
适合场景:
- 键盘、鼠标、耳机、手表、遥控器等所有电池供电外设
对你当前项目的价值:
- 这是 `new_kbd` 最应该启用的标准服务之一
- 你已经有 `ble_battery_module.c`,它天然适合映射到 BAS
### 4.1.3 Current Time Service, `CTS`
源码位置:
- `zephyr/subsys/bluetooth/services/cts.c`
- `zephyr/include/zephyr/bluetooth/services/cts.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_CTS=y
```
可选工具 API
```conf
CONFIG_BT_CTS_HELPER_API=y
```
怎么用:
- 定义 `struct bt_cts_cb`
- 至少实现 `fill_current_cts_time()`,因为服务在被读或发通知时要靠它拿当前时间
- 如果允许对端写时间,再实现 `cts_time_write()`
- 如果关心客户端是否订阅时间更新,实现 `notification_changed()`
- 初始化时调用 `bt_cts_init(&cb)`
- 时间变化后调用 `bt_cts_send_notification(reason)`
最小使用思路:
1. 系统自己维护 RTC / Unix 时间
2. CTS 被读时把当前时间填进 `struct bt_cts_time_format`
3. 如果时间是由手机写入的,就在 `cts_time_write()` 里反写到系统时钟
### 4.1.4 Device Information Service, `DIS`
源码位置:
- `zephyr/subsys/bluetooth/services/dis.c`
- `zephyr/subsys/bluetooth/services/Kconfig.dis`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DIS=y
CONFIG_BT_DIS_MANUF_NAME=y
CONFIG_BT_DIS_MANUF_NAME_STR="Your Company"
CONFIG_BT_DIS_MODEL_NUMBER=y
CONFIG_BT_DIS_MODEL_NUMBER_STR="KBD-01"
CONFIG_BT_DIS_PNP=y
```
怎么用:
- `DIS` 基本是**纯配置型服务**
- 你在 `prj.conf` 里打开需要的特征值并填字符串/ID服务会静态注册
- 常用字段包括:
- 厂商名
- 型号
- 序列号
- FW/HW/SW 版本
- PnP ID
- 医疗设备 UDI
适合场景:
- 几乎所有 BLE 外设都建议启用
对键盘项目的建议:
- 至少填 `Manufacturer Name``Model Number``FW Revision`
- 如果你将来打算做 HID 认证/系统识别,`PnP ID` 也建议补齐
### 4.1.5 Heart Rate Service, `HRS`
源码位置:
- `zephyr/subsys/bluetooth/services/hrs.c`
- `zephyr/include/zephyr/bluetooth/services/hrs.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_HRS=y
```
怎么用:
- 若要处理控制点请求,定义 `struct bt_hrs_cb` 并注册 `bt_hrs_cb_register(&cb)`
- 上报新心率时调用 `bt_hrs_notify(heartrate)`
- 如果支持 Energy Expended 重置,则在 `ctrl_point_write()` 里处理 `BT_HRS_CONTROL_POINT_RESET_ENERGY_EXPANDED_REQ`
适合场景:
- 心率带、健康设备、运动设备
### 4.1.6 Immediate Alert Service, `IAS`
源码位置:
- `zephyr/subsys/bluetooth/services/ias/ias.c`
- `zephyr/include/zephyr/bluetooth/services/ias.h`
- 参考样例:`zephyr/samples/bluetooth/peripheral/src/main.c`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_IAS=y
```
怎么用:
-`BT_IAS_CB_DEFINE(name)` 定义回调对象
- 在回调里实现:
- `no_alert()`
- `mild_alert()`
- `high_alert()`
- 远端写 Alert Level 时,服务会自动回调你
- 如果本机已经在报警,且你想主动停掉当前告警,可调用 `bt_ias_local_alert_stop()`
典型逻辑:
- `mild_alert()` 里短鸣、低频闪烁
- `high_alert()` 里持续蜂鸣或高亮闪烁
- `no_alert()` 里停蜂鸣、灭灯
### 4.1.7 Nordic UART Service, `Zephyr 版 NUS`
源码位置:
- `zephyr/subsys/bluetooth/services/nus/`
- `zephyr/include/zephyr/bluetooth/services/nus.h`
- 参考样例:`zephyr/samples/bluetooth/peripheral_nus`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_ZEPHYR_NUS=y
```
可选:
```conf
CONFIG_BT_ZEPHYR_NUS_DEFAULT_INSTANCE=y
CONFIG_BT_ZEPHYR_NUS_AUTO_START_BLUETOOTH=y
```
怎么用:
- 最简单:使用默认实例
- 定义 `struct bt_nus_cb`
- 注册 `bt_nus_cb_register(&cb, ctx)`
- 对端写 RX 特征值时,你的 `received()` 回调会收到数据
- 要发数据给对端时,调用 `bt_nus_send(conn, data, len)`
高级用法:
- 这个版本支持多实例
- 你可以先用 `BT_NUS_INST_DEFINE(name)` 定义多个 NUS 端点
- 然后对每个实例调用 `bt_nus_inst_cb_register()` / `bt_nus_inst_send()`
建议:
- 新代码如果想做“可扩展串口通道”Zephyr 版 NUS 比 Nordic 旧版更现代
### 4.1.8 Object Transfer Service, `OTS`
源码位置:
- `zephyr/subsys/bluetooth/services/ots/`
- `zephyr/include/zephyr/bluetooth/services/ots.h`
- 参考样例:`zephyr/samples/bluetooth/peripheral_ots`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_OTS=y
CONFIG_BT_L2CAP_DYNAMIC_CHANNEL=y
CONFIG_BT_GATT_DYNAMIC_DB=y
CONFIG_BT_SMP=y
```
按需求补充:
```conf
CONFIG_BT_OTS_DIR_LIST_OBJ=y
CONFIG_BT_OTS_OACP_WRITE_SUPPORT=y
CONFIG_BT_OTS_OACP_PATCH_SUPPORT=y
CONFIG_BT_OTS_OACP_CREATE_SUPPORT=y
CONFIG_BT_OTS_OACP_DELETE_SUPPORT=y
```
怎么用:
1. 先拿一个实例:`bt_ots_free_instance_get()`
2.`struct bt_ots_init_param`,配置支持的 OACP/OLCP Feature 与回调
3. 调用 `bt_ots_init(ots, &ots_init)`
4. 准备对象元数据与对象数据,再用 `bt_ots_obj_add(ots, &param)` 加入对象池
5. 客户端随后可通过 OTS 读/写/选择对象
什么时候值得用:
- 需要在 BLE 上传输“对象”,例如文件、图片、日志、记录块、配置块
- 需要比单个 characteristic 更规范的元数据和对象管理
什么时候不值得用:
- 只是传几字节配置或简单串口透传时,`NUS` 或自定义服务更轻
### 4.1.9 Tx Power Service, `TPS`
源码位置:
- `zephyr/subsys/bluetooth/services/tps.c`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_TPS=y
```
怎么用:
- 这是典型的**无应用层 API**服务
- 打开 `CONFIG_BT_TPS` 后,服务会静态注册
- 客户端读取 `TX Power Level` 特征值时,底层会通过 `bt_conn_le_get_tx_power_level()` 取当前发射功率并返回
适合场景:
- 定位、调试、需要标准化暴露 TX Power 的设备
## 4.2 NCS / Nordic 扩展服务
### 4.2.1 Bond Management Service, `BMS`
源码位置:
- `nrf/subsys/bluetooth/services/bms.c`
- `nrf/include/bluetooth/services/bms.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_BMS=y
```
怎么用:
- 定义 `struct bt_bms_init_params`
- 在里面配置支持哪些删 bond 操作,以及是否需要授权码
- 如果某些删除操作要授权,实现 `struct bt_bms_cb.authorize`
- 调用 `bt_bms_init(&init_params)`
适合场景:
- 设备端希望让手机或维护工具发起“删除本机 bond”
- 做售后恢复、重新配对、共享设备切换用户
对你当前项目的价值:
- 你的键盘已经在做 bond 管理,这个服务值得评估
- 但很多键盘不会直接对外开放 BMS而是自己做按键清配对逻辑
### 4.2.2 Continuous Glucose Monitoring Service, `CGMS`
源码位置:
- `nrf/subsys/bluetooth/services/cgms/`
- `nrf/include/bluetooth/services/cgms.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_CGMS=y
```
怎么用:
-`struct bt_cgms_init_param`
- 传感器类型
- 采样位置
- session 运行时长
- 初始通信间隔
- 回调
- 调用 `bt_cgms_init(&init)`
- 每有一条新血糖测量值,就调用 `bt_cgms_measurement_add(measurement)`
适合场景:
- 连续血糖监测设备
### 4.2.3 Direction and Distance Finding Service, `DDFS`
源码位置:
- `nrf/subsys/bluetooth/services/ddfs.c`
- `nrf/include/bluetooth/services/ddfs.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DDFS=y
```
怎么用:
-`struct bt_ddfs_init_params`
- 初始 features
- `struct bt_ddfs_cb` 回调
- 调用 `bt_ddfs_init(&init)`
- 应用拿到测距/方位结果后,用以下 API 通知客户端:
- `bt_ddfs_distance_measurement_notify()`
- `bt_ddfs_azimuth_measurement_notify()`
- `bt_ddfs_elevation_measurement_notify()`
回调的意义:
- `dm_ranging_mode_set()`:对端改测距模式时通知应用
- `dm_config_read()`:对端读取配置时由应用填充配置
- `*_notification_config_changed()`:通知开关变化
适合场景:
- 方向查找、距离估计、定位试验平台
### 4.2.4 Fast Pair Provider Service, `FPS`
源码位置:
- `nrf/subsys/bluetooth/services/fast_pair/fp_gatt_service.c`
- `nrf/include/bluetooth/services/fast_pair/fast_pair.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_FAST_PAIR=y
CONFIG_BT_FAST_PAIR_GATT_SERVICE=y
```
怎么用:
- 这不是通用 GATT 服务,而是 Google Fast Pair 生态专用服务
- 正常接入方式不是自己操作 characteristic而是走高层 API
- 典型流程:
1. `bt_enable()`
2. `settings_load()`
3. 注册必要回调
4. 调用 `bt_fast_pair_enable()`
- 运行时常用 API
- `bt_fast_pair_set_pairing_mode()`
- `bt_fast_pair_battery_set()`
- `bt_fast_pair_info_cb_register()`
适合场景:
- 要做经过 Fast Pair 生态认证的耳机、输入设备、Tag 类设备
不适合场景:
- 普通 BLE 外设
- 只想做自定义配对体验
### 4.2.5 Human Interface Device Service, `HIDS`
源码位置:
- `nrf/subsys/bluetooth/services/hids.c`
- `nrf/include/bluetooth/services/hids.h`
- 参考样例:
- `nrf/samples/bluetooth/peripheral_hids_keyboard`
- `nrf/samples/bluetooth/peripheral_hids_mouse`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_HIDS=y
CONFIG_BT_HIDS_MAX_CLIENT_COUNT=2
CONFIG_BT_HIDS_DEFAULT_PERM_RW_ENCRYPT=y
```
怎么用:
1.`BT_HIDS_DEF(hids_obj, ...)` 定义一个 HID 服务实例
2. 准备 `struct bt_hids_init_param`
- HID 信息
- 输入/输出/特征报告组
- Report Map
- 协议模式回调
- Control Point 回调
- Boot Keyboard/Mouse 回调
- `is_kb` / `is_mouse`
3. 调用 `bt_hids_init(&hids_obj, &init_param)`
4. 连接回调里调用:
- `bt_hids_connected(&hids_obj, conn)`
- `bt_hids_disconnected(&hids_obj, conn)`
5. 发报告时调用:
- 通用输入报告:`bt_hids_inp_rep_send()`
- Boot Keyboard`bt_hids_boot_kb_inp_rep_send()`
- Boot Mouse`bt_hids_boot_mouse_inp_rep_send()`
对键盘项目的意义:
- 如果你走标准 HID over GATT 键盘路线,这是最关键的服务实现
- 比起自定义 NUSHIDS 才是操作系统原生识别键盘的正路
### 4.2.6 Latency Service
源码位置:
- `nrf/subsys/bluetooth/services/latency.c`
- `nrf/include/bluetooth/services/latency.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_LATENCY=y
```
怎么用:
- 定义一个 `struct bt_latency latency`
- 可选定义 `struct bt_latency_cb`
- 调用 `bt_latency_init(&latency, &cb)`
- 对端对 Latency Characteristic 发写请求时,会回调 `latency_request()`
适合场景:
- 链路时延测量
- 实验室验证,不是典型产品服务
### 4.2.7 LED Button Service, `LBS`
源码位置:
- `nrf/subsys/bluetooth/services/lbs.c`
- `nrf/include/bluetooth/services/lbs.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_LBS=y
```
怎么用:
- 定义 `struct bt_lbs_cb`
- `led_cb`:远端写 LED 特征值时回调
- `button_cb`:远端读 Button 特征值时回调
- 初始化:`bt_lbs_init(&callbacks)`
- 按键状态变化时调用 `bt_lbs_send_button_state(button_state)`
适合场景:
- 教学、验证链路、最小双向 GATT 交互 demo
不建议:
- 直接当成量产产品协议
### 4.2.8 Memfault Diagnostic Service, `MDS`
源码位置:
- `nrf/subsys/bluetooth/services/mds.c`
- `nrf/include/bluetooth/services/mds.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_MDS=y
```
怎么用:
- 这个服务主要为 Memfault 诊断导出服务
- 启用后服务静态注册
- 如果想控制谁能访问诊断数据,在 `bt_enable()` 前调用 `bt_mds_cb_register(&cb)`
- 关键回调是 `access_enable(conn)`,用来决定连接方是否有权限访问 MDS 数据
适合场景:
- 你已经集成 Memfault并希望通过 BLE 导出诊断信息
### 4.2.9 Nordic Status Message Service, `NSMS`
源码位置:
- `nrf/subsys/bluetooth/services/nsms.c`
- `nrf/include/bluetooth/services/nsms.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_NSMS=y
```
怎么用:
- 这个服务不是 `init()` 风格,而是**宏定义实例**
-`BT_NSMS_DEF(nsms_obj, "Status", BT_NSMS_SECURITY_LEVEL_ENCRYPT, "idle", 64)` 定义一个服务实例
- 之后运行时调用 `bt_nsms_set_status(&nsms_obj, "connected")` 更新状态文本
特点:
- 适合快速暴露一段人类可读状态字符串
- 支持安全级别配置
- 支持多实例
适合场景:
- 开发调试状态
- 设备工作模式简报
### 4.2.10 Nordic UART Service, `Nordic 旧版 NUS`
源码位置:
- `nrf/subsys/bluetooth/services/nus.c`
- `nrf/include/bluetooth/services/nus.h`
- 参考样例:`nrf/samples/bluetooth/peripheral_uart`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_NUS=y
```
可选:
```conf
CONFIG_BT_NUS_AUTHEN=y
```
怎么用:
- 定义 `struct bt_nus_cb`
- 调用 `bt_nus_init(&callbacks)`
-`received()` 回调里处理远端写进来的数据
- 调用 `bt_nus_send(conn, data, len)` 给远端发通知
- `bt_nus_get_mtu(conn)` 可拿到当前可用有效负载大小
和 Zephyr 版 NUS 的区别:
- Nordic 旧版更接近历史 NCS 示例代码
- API 更简单,通常单实例
- 如果项目已经基于 `peripheral_uart` 一类示例写的,通常继续沿用这个版本更省事
注意:
- **不要同时把 Zephyr NUS 和 Nordic 旧版 NUS 当成同一个业务通道一起开**,它们 UUID 相同,容易造成概念和实现混乱
### 4.2.11 Ranging Service, `RAS`
源码位置:
- `nrf/subsys/bluetooth/services/ras/rrsp/ras_rrsp.c`
- `nrf/include/bluetooth/services/ras.h`
启用方法:
如果本机要作为**服务端/响应端**暴露 Ranging Service
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_RAS=y
CONFIG_BT_RAS_RRSP=y
```
怎么用:
- 服务端 RRSP 角色启用后会注册标准 Ranging Service
- 每个连接通常要先调用 `bt_ras_rrsp_alloc(conn)` 关联上下文
- 通过 `bt_ras_rd_buffer_cb_register()` 监听 ranging data buffer 生命周期
- 当有新的测距数据可提供时,使用数据缓冲 API
- `bt_ras_rd_buffer_ready_check()`
- `bt_ras_rd_buffer_claim()`
- `bt_ras_rd_buffer_release()`
- `bt_ras_rd_buffer_bytes_pull()`
- 连接释放时调用 `bt_ras_rrsp_free(conn)`
补充说明:
- 同一个头文件里还提供 `RREQ` API那是**客户端/请求端**使用的,不是本地服务端注册逻辑
适合场景:
- BLE Channel Sounding / Ranging 实验与产品验证
### 4.2.12 Running Speed and Cadence Service, `RSCS`
源码位置:
- `nrf/subsys/bluetooth/services/rscs.c`
- `nrf/include/bluetooth/services/rscs.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_RSCS=y
```
怎么用:
-`struct bt_rscs_init_params`
- `features`
- 支持的位置列表
- 当前位置
- 事件处理函数
- 控制点回调
- 调用 `bt_rscs_init(&init)`
- 生成新测量值时调用 `bt_rscs_measurement_send(conn, &measurement)`
控制点回调负责:
- `set_cumulative()`
- `calibration()`
- `update_loc()`
适合场景:
- 跑步传感器、脚环、步频速度设备
### 4.2.13 Throughput Service
源码位置:
- `nrf/subsys/bluetooth/services/throughput.c`
- `nrf/include/bluetooth/services/throughput.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_THROUGHPUT=y
```
怎么用:
- 定义 `struct bt_throughput throughput`
- 定义 `struct bt_throughput_cb`
- 调用 `bt_throughput_init(&throughput, &cb)`
如果本机作为服务端:
- 服务会被注册为一个测试用 Throughput Service
- 对端读/写该特征值时,你会在回调里拿到吞吐统计信息
如果本机作为客户端测试远端吞吐:
-`bt_gatt_dm` 发现远端 Throughput Service
- 调用 `bt_throughput_handles_assign(dm, &throughput)`
- 然后用 `bt_throughput_read()` / `bt_throughput_write()` 发测试流量
适合场景:
- 链路性能压测
- MTU / DLE / PHY 参数实验
### 4.2.14 Wi-Fi Provisioning Service
源码位置:
- `nrf/subsys/bluetooth/services/wifi_prov/wifi_prov_ble.c`
- `nrf/include/bluetooth/services/wifi_provisioning.h`
- 配套核心库:`nrf/include/net/wifi_prov_core/wifi_prov_core.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_WIFI_PROV=y
```
怎么用:
- 这个 BLE GATT 服务本身是 Wi-Fi Provisioning Core 的传输承载层
- 你真正要初始化的是 `wifi_prov_core`
- 典型流程:
1. 调用 `wifi_prov_init()`
2. 广告里带上 Provisioning Service UUID
3. 手机/配置工具向 Control Point 写 provisioning 请求
4. 核心库通过 `wifi_prov_send_rsp()` / `wifi_prov_send_result()` 把结果经 BLE 返回
适合场景:
- 设备本身有 Wi-Fi但没有屏幕/键盘,需要靠手机配网
## 5. 哪些是客户端辅助库,不是本地服务端
下面这些模块也在 `services` 目录里,但它们主要是**Central 侧访问远端服务**用的,不是“本机注册一个 GATT Service”
- `ams_client`
- `ancs_client`
- `bas_client`
- `cts_client`
- `dfu_smp`
- `gattp`
- `hogp`
- `hrs_client`
- `ias` 的 client API 部分
- `latency_client`
- `nus_client`
- `ots` 的 client API 部分
- `ras``RREQ` API 部分
如果你需要,我可以下一步单独再整理一份“**这些 client 库分别怎么发现远端服务、怎么订阅、怎么读写**”。
## 6. 对 `new_kbd` 项目的直接建议
如果目标是 BLE 键盘,优先级基本如下:
### 第一优先级
- `HIDS`:标准键盘主服务
- `BAS`:电量上报
- `DIS`:厂商/型号/版本
### 第二优先级
- `BMS`:如果你想让主机侧触发清配对
- `IAS` / `Proximity` 类思路:如果要做“找键盘”
- `TPS`:若你想暴露标准发射功率
### 通常不建议直接用于键盘主链路
- `NUS`:适合调试、配置通道,不适合作为系统键盘输入主协议
- `OTS`:太重
- `LBS`:示例用途
- `Throughput` / `Latency`:测试用途
## 7. 参考代码路径
你后面如果要继续落到工程实现,最值得先看的参考代码是:
- `C:\ncs\v3.2.3\nrf\samples\bluetooth\peripheral_hids_keyboard`
- `C:\ncs\v3.2.3\nrf\samples\bluetooth\peripheral_uart`
- `C:\ncs\v3.2.3\zephyr\samples\bluetooth\peripheral_nus`
- `C:\ncs\v3.2.3\zephyr\samples\bluetooth\peripheral_ots`
如果你要,我下一步可以继续给你补一份“**把这些 Service 映射到 `new_kbd` 当前代码结构的落地建议**”,直接对应你现在的 `ble_bond_module.c``ble_battery_module.c` 和后续 HIDS 模块拆分方式。

View File

@@ -0,0 +1,118 @@
# new_kbd 项目 Nordic NCS 官方知识索引
## 说明
- 生成时间2026-03-30
- 文档来源:仅收录 `docs.nordicsemi.com` 上可直接访问的 Nordic 官方文档
- 版本基线:按项目当前环境优先核对 `ncs-3.2.3`
- 项目芯片基线:`atguigu_mini_keyboard/nrf52840`
- 芯片依据:项目板级文件 [atguigu_mini_keyboard.yaml](E:/extra/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.yaml) 与 [atguigu_mini_keyboard.dts](E:/extra/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.dts) 明确基于 `nrf52840`
- 收录范围:仅收录 `E:\projects\new_kbd` 实际使用到的 NCS 官方知识,以及 Nordic 站点中同时存在的 Zephyr 镜像文档
- 不收录原则:如果没有核到 Nordic 官方页面,则不写入本索引
- 本版校验方式:使用浏览器渲染后再判定,凡标题或正文渲染为 `Error 404 - Page Not Found` / `ERROR CODE: 404` 的页面,均视为无效并移除
## NCS 托管的 Zephyr 文档镜像
### 构建与配置
- [应用开发总览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/develop/application/index.html)
- [CMake 构建系统NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/cmake/index.html)
- [Kconfig 配置系统NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/kconfig/index.html)
- [Sysbuild 系统构建NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/sysbuild/index.html)
### 设备树与设备模型
- [设备树入门NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/intro.html)
- [设备树 HOWTONCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/howtos.html)
- [设备树 C/C++ API 用法NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/api-usage.html)
- [zephyr,user 自定义节点NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/zephyr-user-node.html)
### 内核与并发
- [工作队列与延迟工作NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/kernel/services/threads/workqueue.html)
- [原子操作NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/kernel/services/other/atomic.html)
- [自旋锁 APINCS 镜像)](https://docs.nordicsemi.com/bundle/zephyr-apis-3.2.3/page/spinlock_8h.html)
- [时钟、超时与 uptimeNCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/kernel/services/timing/clocks.html)
### 日志与存储
- [日志子系统NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/services/logging/index.html)
- [Settings 设置子系统NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/services/storage/settings/index.html)
- [NVS 非易失存储NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/services/storage/nvs/nvs.html)
- [Flash Map 闪存映射NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/services/storage/flash_map/flash_map.html)
### 蓝牙 Low Energy
- [Bluetooth GAP 概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/bluetooth/api/gap.html)
- [Bluetooth 连接管理NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/bluetooth/api/connection_mgmt.html)
- [Bluetooth GATT 概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/bluetooth/api/gatt.html)
- [Bluetooth 标准服务总览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/bluetooth/api/services.html)
### USB 与 HID
- [USB 设备栈Device Stack NextNCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/usb/device_next/usb_device.html)
- [USB HID 设备 APINCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/usb/device_next/api/usbd_hid_device.html)
- [HID 通用定义与报告描述符NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/usb/api/hid.html)
### 外设驱动
- [ADC 外设概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/hardware/peripherals/adc.html)
- [GPIO 外设概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/hardware/peripherals/gpio.html)
- [I2C 外设概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/hardware/peripherals/i2c.html)
- [传感器子系统概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/hardware/peripherals/sensor/index.html)
- [LED 外设概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/hardware/peripherals/led.html)
### 显示与 GUI
- [显示子系统概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/hardware/peripherals/display/index.html)
- [PWM LEDs 设备树绑定NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/api/bindings/led/pwm-leds.html)
- [GPIO LEDs 设备树绑定NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/api/bindings/led/gpio-leds.html)
- [LVGL 官方示例入口NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/samples/modules/lvgl/lvgl.html)
### 通用工具宏与字节序
- [sys/byteorder 字节序 APINCS 镜像)](https://docs.nordicsemi.com/bundle/zephyr-apis-3.2.3/page/sys_2byteorder_8h.html)
- [spinlock 文件参考NCS API 镜像)](https://docs.nordicsemi.com/bundle/zephyr-apis-3.2.3/page/spinlock_8h.html)
## Partition Manager 与存储布局
- [Partition Manager](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/scripts/partition_manager/partition_manager.html)
## CAF 总览与基础设施
- [Common Application Framework (CAF) 总览](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/caf/caf_overview.html)
- [Application Event Manager](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/others/app_event_manager.html)
- [Application Event Manager Profiler Tracer](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/others/app_event_manager_profiler_tracer.html)
## CAF 模块
- [CAF Buttons 模块](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/caf/buttons.html)
- [CAF LEDs 模块](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/caf/leds.html)
- [CAF BLE Advertising 模块](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/caf/ble_adv.html)
- [CAF BLE State 模块](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/caf/ble_state.html)
- [CAF Power Manager 模块](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/caf/power_manager.html)
- [Settings Loader](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/applications/nrf_desktop/doc/settings_loader.html)
## CAF API / 辅助定义
- [led_effect API](https://docs.nordicsemi.com/bundle/nrf-apis-3.2.3/page/group_led_effect_CAF.html)
## 芯片与 Nordic 外设相关
- [nRF52840 驱动索引](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/nrf52840_drivers.html)
- [nRF52840 DK 用户指南](https://docs.nordicsemi.com/bundle/ug_nrf52840_dk)
- [Nordic nRF USBD 设备树绑定NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/api/bindings/usb/nordic_nrf-usbd.html)
- [nrf_qdec HAL / 寄存器 API](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/group_nrf_qdec.html)
- [nrf_saadc HAL / 寄存器 API](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/group_nrf_saadc.html)
- [nrf_usbd HAL / 寄存器 API](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/group_nrf_usbd.html)
- [nrf_gpio HAL / 寄存器 API](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/group_nrf_gpio.html)
- [nrf_pwm HAL / 寄存器 API](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/group_nrf_pwm.html)
- [nrf_spi HAL / 寄存器 API](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/group_nrf_spi.html)
- [nrf_twim HAL / 寄存器 API](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/group_nrf_twim.html)
## 备注
- `pm_static.yml` 相关内容,本次仅核到 Nordic 官方的 Partition Manager 页面;未单独核到专门以 `pm_static.yml` 命名的独立官方页面,因此这里索引到 Partition Manager 官方文档
- Zephyr 侧已经收录的知识,只要在 Nordic 站点存在并通过浏览器渲染校验,本索引也一并收录
- 芯片相关知识本次按项目实际硬件基线 `nrf52840` 收录,没有扩展到未使用的其他 Nordic SoC
- 当前 Nordic 站点里,很多 `zephyr-apis-3.2.3``nrf-apis-3.2.3` 与部分 CAF 事件页虽然返回 `200`,但浏览器渲染后是 `Error 404 - Page Not Found`,因此已从本索引移除

View File

@@ -0,0 +1,96 @@
# new_kbd 项目 Zephyr 官方知识索引
## 说明
- 生成时间2026-03-30
- 扫描范围:`src/``inc/``prj.conf``sysbuild.conf``app.overlay``pm_static.yml`
- 收录原则:仅收录 `E:\projects\new_kbd` 当前实际用到、且已确认页面真实存在的 Zephyr 官方文档
- 链接范围:`https://docs.zephyrproject.org/latest/``https://docs.zephyrproject.org/apidoc/latest/``https://docs.zephyrproject.org/latest/doxygen/html/`
- 不收录范围Nordic NCS/CAF/App Event Manager/Partition Manager 文档,以及项目自定义驱动或项目私有协议说明
- Nordic NCS 官方补充索引见:[new_kbd 项目 Nordic NCS 官方知识索引](./nordic_ncs_官方知识索引.md)
## 构建与配置
- [应用开发总览](https://docs.zephyrproject.org/latest/develop/application/index.html)
- [CMake 构建系统](https://docs.zephyrproject.org/latest/build/cmake/index.html)
- [Kconfig 配置系统](https://docs.zephyrproject.org/latest/build/kconfig/index.html)
- [Sysbuild 系统构建](https://docs.zephyrproject.org/latest/build/sysbuild/index.html)
## 设备树与设备模型
- [设备树入门](https://docs.zephyrproject.org/latest/build/dts/intro.html)
- [设备树 HOWTO](https://docs.zephyrproject.org/latest/build/dts/howtos.html)
- [设备树 C/C++ API 用法](https://docs.zephyrproject.org/latest/build/dts/api-usage.html)
- [zephyr,user 自定义节点](https://docs.zephyrproject.org/latest/build/dts/zephyr-user-node.html)
- [设备模型 API](https://docs.zephyrproject.org/apidoc/latest/group__device__model.html)
- [设备树通用标识符 API](https://docs.zephyrproject.org/apidoc/latest/group__devicetree-generic-id.html)
## 内核与并发
- [工作队列与延迟工作](https://docs.zephyrproject.org/latest/kernel/services/threads/workqueue.html)
- [原子操作](https://docs.zephyrproject.org/latest/kernel/services/other/atomic.html)
- [自旋锁 API](https://docs.zephyrproject.org/apidoc/latest/spinlock_8h.html)
- [时钟、超时与 uptime](https://docs.zephyrproject.org/latest/kernel/services/timing/clocks.html)
## 日志与存储
- [日志子系统](https://docs.zephyrproject.org/latest/services/logging/index.html)
- [Settings 设置子系统](https://docs.zephyrproject.org/latest/services/storage/settings/index.html)
- [NVS 非易失存储](https://docs.zephyrproject.org/latest/services/storage/nvs/nvs.html)
- [Flash Map 闪存映射](https://docs.zephyrproject.org/latest/services/storage/flash_map/flash_map.html)
## 蓝牙 Low Energy
- [Bluetooth GAP 概览](https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/gap.html)
- [Bluetooth GAP API](https://docs.zephyrproject.org/apidoc/latest/group__bt__gap.html)
- [Bluetooth 连接管理](https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/connection_mgmt.html)
- [Bluetooth 连接管理 API](https://docs.zephyrproject.org/apidoc/latest/group__bt__conn.html)
- [Bluetooth GATT 概览](https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/gatt.html)
- [Bluetooth GATT Server API](https://docs.zephyrproject.org/apidoc/latest/group__bt__gatt__server.html)
- [Bluetooth UUID API](https://docs.zephyrproject.org/apidoc/latest/group__bt__uuid.html)
- [Bluetooth Battery Service (BAS) API](https://docs.zephyrproject.org/apidoc/latest/group__bt__bas.html)
- [Bluetooth 标准服务总览](https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/services.html)
- [BLE HID 外设官方示例](https://docs.zephyrproject.org/latest/samples/bluetooth/peripheral_hids/README.html)
## USB 与 HID
- [USB 设备栈Device Stack Next](https://docs.zephyrproject.org/latest/connectivity/usb/device_next/usb_device.html)
- [USB Device API](https://docs.zephyrproject.org/apidoc/latest/group__usbd__api.html)
- [USB HID 设备 API](https://docs.zephyrproject.org/latest/connectivity/usb/device_next/api/usbd_hid_device.html)
- [USB HID Device API 参考](https://docs.zephyrproject.org/apidoc/latest/group__usbd__hid__device.html)
- [HID 通用定义与报告描述符](https://docs.zephyrproject.org/latest/connectivity/usb/api/hid.html)
- [zephyr,hid-device 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/usb/zephyr%2Chid-device.html)
## 外设驱动
- [ADC 外设概览](https://docs.zephyrproject.org/latest/hardware/peripherals/adc.html)
- [ADC API](https://docs.zephyrproject.org/apidoc/latest/group__adc__interface.html)
- [GPIO 外设概览](https://docs.zephyrproject.org/latest/hardware/peripherals/gpio.html)
- [GPIO API](https://docs.zephyrproject.org/apidoc/latest/group__gpio__interface.html)
- [I2C 外设概览](https://docs.zephyrproject.org/latest/hardware/peripherals/i2c.html)
- [传感器子系统概览](https://docs.zephyrproject.org/latest/hardware/peripherals/sensor/index.html)
- [传感器 API](https://docs.zephyrproject.org/apidoc/latest/group__sensor__interface.html)
- [LED 外设概览](https://docs.zephyrproject.org/latest/hardware/peripherals/led.html)
- [LED API](https://docs.zephyrproject.org/apidoc/latest/group__led__interface.html)
- [Nordic nRF QDEC 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/sensor/nordic%2Cnrf-qdec.html)
## 显示与 GUI
- [显示子系统概览](https://docs.zephyrproject.org/latest/hardware/peripherals/display/index.html)
- [MIPI-DBI SPI 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/mipi-dbi/zephyr%2Cmipi-dbi-spi.html)
- [ST7789V 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/display/sitronix%2Cst7789v.html)
- [PWM LEDs 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/led/pwm-leds.html)
- [GPIO LEDs 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/led/gpio-leds.html)
- [LVGL 官方示例入口](https://docs.zephyrproject.org/latest/samples/modules/lvgl/lvgl.html)
## 通用工具宏与字节序
- [sys/util 通用工具宏 API](https://docs.zephyrproject.org/apidoc/latest/group__sys-util.html)
- [sys/byteorder 字节序 API](https://docs.zephyrproject.org/latest/doxygen/html/sys_2byteorder_8h.html)
## 未纳入本索引的项目项
- `zephyr/drivers/power/ip5306.h``CONFIG_IP5306`:当前项目使用的是自定义驱动/自定义绑定Zephyr 官方 latest 站点未核到对应官方页面,因此未纳入
- `pm_static.yml` 对应的 Partition Manager 规划:属于 Nordic NCS 范畴,不属于 Zephyr 官方 docs 站点范围Nordic 官方索引见 [nordic_ncs_官方知识索引.md](./nordic_ncs_官方知识索引.md)
- `CAF``App Event Manager``settings_loader``ble_state``ble_common_event``power_manager``buttons_def.h` 等:属于 Nordic NCS/CAF 文档范围,不属于本 Zephyr 官方索引Nordic 官方索引见 [nordic_ncs_官方知识索引.md](./nordic_ncs_官方知识索引.md)
- 项目私有协议与实现,如时间同步私有 GATT 服务、主机 HID 命令协议、显示主题持久化逻辑等:只索引其依赖的 Zephyr 通用能力,不索引项目私有设计本身

34
inc/buttons_def.h Normal file
View File

@@ -0,0 +1,34 @@
/*
* CAF buttons 矩阵引脚定义
*
* 设计说明:
* - 本文件被 CAF buttons 模块通过 CONFIG_CAF_BUTTONS_DEF_PATH 直接包含;
* - 行列引脚顺序必须与板级 DTS 中 my_keyboard 的 row-gpios/col-gpios 保持一致;
* - key_id 的行列编号完全基于这里的数组下标,不依赖 input-keymap 节点。
*/
#include <caf/gpio_pins.h>
/*
* 该符号用于保证配置文件只被链接一次:
* 若被重复包含到多个编译单元,会在链接阶段报重复定义,避免静默错配。
*/
const struct {} buttons_def_include_once;
/* 列引脚:对应 atguigu_mini_keyboard.dts 中 my_keyboard/col-gpios 顺序。 */
static const struct gpio_pin col[] = {
{ .port = 0, .pin = 5 },
{ .port = 0, .pin = 6 },
{ .port = 0, .pin = 26 },
{ .port = 0, .pin = 30 },
};
/* 行引脚:对应 atguigu_mini_keyboard.dts 中 my_keyboard/row-gpios 顺序。 */
static const struct gpio_pin row[] = {
{ .port = 0, .pin = 15 },
{ .port = 0, .pin = 7 },
{ .port = 0, .pin = 12 },
{ .port = 0, .pin = 4 },
{ .port = 1, .pin = 9 },
{ .port = 0, .pin = 8 },
};

View File

@@ -0,0 +1,16 @@
#ifndef HID_HOST_COMMAND_PROTOCOL_H__
#define HID_HOST_COMMAND_PROTOCOL_H__
#include <stdint.h>
#define HID_HOST_CMD_DATA_SIZE 8U
#define HID_HOST_CMD_OUTPUT_PAYLOAD_SIZE (1U + HID_HOST_CMD_DATA_SIZE)
#define HID_HOST_CMD_ACK_PAYLOAD_SIZE 1U
#define HID_HOST_CMD_ID_THEME_COLOR 0x01U
#define HID_HOST_CMD_ID_TIME_SYNC 0x02U
#define HID_HOST_CMD_THEME_PARAM_SIZE 3U
#define HID_HOST_CMD_TIME_SYNC_PARAM_SIZE 8U
#endif /* HID_HOST_COMMAND_PROTOCOL_H__ */

9
inc/hid_host_transport.h Normal file
View File

@@ -0,0 +1,9 @@
#ifndef HID_HOST_TRANSPORT_H__
#define HID_HOST_TRANSPORT_H__
enum hid_host_transport {
HID_HOST_TRANSPORT_USB = 0,
HID_HOST_TRANSPORT_BLE,
};
#endif /* HID_HOST_TRANSPORT_H__ */

43
inc/hid_keymap_def.h Normal file
View File

@@ -0,0 +1,43 @@
/*
* HID keymap for current new_kbd numeric keypad layout.
* 说明:
* - 本文件仿照 nrf_desktop 的 hid_keymap_def.h 组织方式;
* - 仅由 keyboard_module.c 包含一次(通过 APP_HID_KEYMAP_DEF_PATH
* - 条目必须按 key_id 升序排列(按 KEY_ID(col, row) 计算后的数值顺序)。
*/
#include <caf/key_id.h>
/*
* 防止该定义文件被多处 include 导致重复符号。
* 约定仅由 keyboard_module.c 包含一次。
*/
const struct {} hid_keymap_def_include_once;
static const struct hid_keymap hid_keymap[] = {
/* col 0 */
{ KEY_ID(0, 1), 0x0053, REPORT_ID_KEYBOARD }, /* Num Lock */
{ KEY_ID(0, 2), 0x005F, REPORT_ID_KEYBOARD }, /* Keypad 7 */
{ KEY_ID(0, 3), 0x005C, REPORT_ID_KEYBOARD }, /* Keypad 4 */
{ KEY_ID(0, 4), 0x0059, REPORT_ID_KEYBOARD }, /* Keypad 1 */
{ KEY_ID(0, 5), 0x0062, REPORT_ID_KEYBOARD }, /* Keypad 0 */
/* col 1 */
{ KEY_ID(1, 1), 0x0054, REPORT_ID_KEYBOARD }, /* Keypad / */
{ KEY_ID(1, 2), 0x0060, REPORT_ID_KEYBOARD }, /* Keypad 8 */
{ KEY_ID(1, 3), 0x005D, REPORT_ID_KEYBOARD }, /* Keypad 5 */
{ KEY_ID(1, 4), 0x005A, REPORT_ID_KEYBOARD }, /* Keypad 2 */
{ KEY_ID(1, 5), 0x0063, REPORT_ID_KEYBOARD }, /* Keypad . */
/* col 2 */
{ KEY_ID(2, 1), 0x0055, REPORT_ID_KEYBOARD }, /* Keypad * */
{ KEY_ID(2, 2), 0x0061, REPORT_ID_KEYBOARD }, /* Keypad 9 */
{ KEY_ID(2, 3), 0x005E, REPORT_ID_KEYBOARD }, /* Keypad 6 */
{ KEY_ID(2, 4), 0x005B, REPORT_ID_KEYBOARD }, /* Keypad 3 */
/* col 3 */
{ KEY_ID(3, 0), 0x00E2, REPORT_ID_CONSUMER }, /* Mute */
{ KEY_ID(3, 1), 0x0056, REPORT_ID_KEYBOARD }, /* Keypad - */
{ KEY_ID(3, 3), 0x0057, REPORT_ID_KEYBOARD }, /* Keypad + */
{ KEY_ID(3, 5), 0x0058, REPORT_ID_KEYBOARD }, /* Keypad Enter */
};

161
inc/hid_report_descriptor.h Normal file
View File

@@ -0,0 +1,161 @@
#ifndef HID_REPORT_DESCRIPTOR_H_
#define HID_REPORT_DESCRIPTOR_H_
#include <zephyr/usb/class/usbd_hid.h>
#include "hid_host_command_protocol.h"
/* 与 HID Report Map 对齐的 Report ID。 */
enum {
REPORT_ID_KEYBOARD = 1,
REPORT_ID_CONSUMER = 3,
REPORT_ID_VENDOR = 4,
REPORT_ID_VENDOR_CMD = 5,
};
#define HID_KBD_USAGE_MAX 0x00E7U
#define HID_KBD_MOD_COUNT 8U
#define HID_KBD_BITMAP_BITS (HID_KBD_USAGE_MAX + 1U)
#define HID_KBD_BITMAP_SIZE ((HID_KBD_BITMAP_BITS + 7U) / 8U)
#define HID_KBD_PAYLOAD_SIZE (1U + HID_KBD_BITMAP_SIZE)
#define HID_BOOT_KBD_PAYLOAD_SIZE 8U
#define HID_CONSUMER_PAYLOAD_SIZE 2U
#define HID_VENDOR_PAYLOAD_SIZE HID_KBD_PAYLOAD_SIZE
#define HID_VENDOR_ACK_PAYLOAD_SIZE HID_HOST_CMD_ACK_PAYLOAD_SIZE
#define HID_KBD_LED_PAYLOAD_SIZE 1U
#define HID_FULL_REPORT_SIZE(payload) (1U + (payload))
/*
* HID_USAGE_PAGE() 只支持 1 字节 Usage Page。
* Vendor Defined Page(0xFF00) 需要 2 字节编码,因此在本地补一个 16 位版本,
* 避免在描述符里混用裸字节,后续维护时可以一眼看出字段语义。
*/
#define HID_USAGE_PAGE16(page_lsb, page_msb) \
HID_ITEM(HID_ITEM_TAG_USAGE_PAGE, HID_ITEM_TYPE_GLOBAL, 2), page_lsb, page_msb
/*
* 键盘(NKRO) + Consumer + Vendor 的复合 Report 描述符:
* - USB Report 接口和 BLE HIDS Report Map 统一使用这份定义,
* 避免两边手写常量后长期演进出现不一致。
* - Report ID 0x04 继续复用 NKRO payload承载私有状态/遮罩语义。
* - Report ID 0x05 预留给“主机命令 + 设备 ACK”通道
* - Output payload 固定 9 字节:[cmd(1) | data(8)]
* - Input payload 固定 1 字节:[cmd]
*/
#define HID_DESC_KEYBOARD_NKRO_CONSUMER() \
{ \
/* Generic Desktop 页:声明这是一个 Keyboard Application 集合。 */ \
HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), \
HID_USAGE(HID_USAGE_GEN_DESKTOP_KEYBOARD), \
HID_COLLECTION(HID_COLLECTION_APPLICATION), \
HID_REPORT_ID(REPORT_ID_KEYBOARD), \
\
/* Keyboard/Keypad 页:先定义 8bit Modifier再定义 232bit NKRO 位图。 */ \
HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP_KEYPAD), \
HID_USAGE_MIN8(0xE0), \
HID_USAGE_MAX8(0xE7), \
HID_LOGICAL_MIN8(0), \
HID_LOGICAL_MAX8(1), \
HID_REPORT_SIZE(1), \
HID_REPORT_COUNT(8), \
HID_INPUT(0x02), \
HID_USAGE_MIN8(0x00), \
HID_USAGE_MAX8(0xE7), \
HID_LOGICAL_MIN8(0), \
HID_LOGICAL_MAX8(1), \
HID_REPORT_SIZE(1), \
HID_REPORT_COUNT(HID_KBD_BITMAP_BITS), \
HID_INPUT(0x02), \
\
/* Report 协议下键盘 LED 输出NumLock/CapsLock/ScrollLock/Compose/Kana。 */ \
HID_USAGE_PAGE(0x08U), \
HID_USAGE_MIN8(0x01), \
HID_USAGE_MAX8(0x05), \
HID_LOGICAL_MIN8(0), \
HID_LOGICAL_MAX8(1), \
HID_REPORT_SIZE(1), \
HID_REPORT_COUNT(5), \
HID_OUTPUT(0x02), \
/* 补齐到 1 字节3bit padding标记为常量。 */ \
HID_REPORT_SIZE(3), \
HID_REPORT_COUNT(1), \
HID_OUTPUT(0x01), \
HID_END_COLLECTION, \
\
/* Consumer 页:使用 16bit Usage 承载多媒体按键(音量/播放/亮度等)。 */ \
HID_USAGE_PAGE(0x0CU), \
HID_USAGE(0x01U), \
HID_COLLECTION(HID_COLLECTION_APPLICATION), \
HID_REPORT_ID(REPORT_ID_CONSUMER), \
HID_LOGICAL_MIN8(0), \
HID_LOGICAL_MAX16(0xEA, 0x00), \
HID_USAGE_MIN16(0x00, 0x00), \
HID_USAGE_MAX16(0xEA, 0x00), \
HID_REPORT_SIZE(16), \
HID_REPORT_COUNT(1), \
HID_INPUT(0x00), \
HID_END_COLLECTION, \
\
/* Vendor 页:双向传输完整键盘状态/屏蔽遮罩payload 结构与 NKRO 一致。 */ \
HID_USAGE_PAGE16(0x00, 0xFF), \
HID_USAGE(0x02U), \
HID_COLLECTION(HID_COLLECTION_APPLICATION), \
HID_REPORT_ID(REPORT_ID_VENDOR), \
HID_LOGICAL_MIN8(0), \
HID_LOGICAL_MAX16(0xFF, 0x00), \
HID_REPORT_SIZE(8), \
HID_REPORT_COUNT(HID_VENDOR_PAYLOAD_SIZE), \
HID_USAGE(0x02U), \
HID_INPUT(0x02), \
HID_REPORT_COUNT(HID_VENDOR_PAYLOAD_SIZE), \
HID_USAGE(0x02U), \
HID_OUTPUT(0x02), \
HID_END_COLLECTION, \
\
/* Vendor 页(0xFF01):主机命令写入 + 设备 ACK 返回。 */ \
HID_USAGE_PAGE16(0x01, 0xFF), \
HID_USAGE(0x05U), \
HID_COLLECTION(HID_COLLECTION_APPLICATION), \
HID_REPORT_ID(REPORT_ID_VENDOR_CMD), \
HID_LOGICAL_MIN8(0), \
HID_LOGICAL_MAX16(0xFF, 0x00), \
HID_REPORT_SIZE(8), \
HID_REPORT_COUNT(HID_VENDOR_ACK_PAYLOAD_SIZE), \
HID_USAGE(0x05U), \
HID_INPUT(0x02), \
HID_REPORT_COUNT(HID_HOST_CMD_OUTPUT_PAYLOAD_SIZE), \
HID_USAGE(0x05U), \
HID_OUTPUT(0x02), \
HID_END_COLLECTION, \
}
/*
* RAW HID 的固定 64 字节输入/输出描述符。
* 设计意图:
* - 采用 Vendor Defined(0xFF00) 页,避免与标准键盘/多媒体语义冲突;
* - IN/OUT 都固定 64 字节,便于固件与上位机用定长帧做双向透传;
* - 不使用 Report ID接口只承载一个 RAW Report减少主机端解析分支。
*/
#define HID_DESC_RAW_64() \
{ \
/* Vendor Defined 页(0xFF00):供厂商私有协议传输,不绑定标准 HID 语义。 */ \
HID_USAGE_PAGE16(0x00, 0xFF), \
HID_USAGE(0x01), \
HID_COLLECTION(HID_COLLECTION_APPLICATION), \
HID_LOGICAL_MIN8(0), \
HID_LOGICAL_MAX16(0xFF, 0x00), \
HID_REPORT_SIZE(8), \
\
/* 输入页:定义 64 字节 Input 报文Data|Var|Abs(0x02) 与原描述符一致。 */ \
HID_REPORT_COUNT(0x40), \
HID_USAGE(0x01), \
HID_INPUT(0x02), \
\
/* 输出页:定义 64 字节 Output 报文,与输入长度对齐,简化双向协议。 */ \
HID_REPORT_COUNT(0x40), \
HID_USAGE(0x01), \
HID_OUTPUT(0x02), \
HID_END_COLLECTION, \
}
#endif

29
inc/led_state.h Normal file
View File

@@ -0,0 +1,29 @@
#ifndef NEW_KBD_LED_STATE_H__
#define NEW_KBD_LED_STATE_H__
#include <zephyr/types.h>
/* 模块内系统状态:只用于本项目的 LED 映射,不对外暴露协议语义。 */
enum led_ble_state {
LED_BLE_STATE_OFF = 0,
LED_BLE_STATE_WAIT_RECONNECT,
LED_BLE_STATE_PAIRING,
LED_BLE_STATE_CONNECTED,
LED_BLE_STATE_COUNT,
};
enum led_num_lock_state {
LED_NUM_LOCK_STATE_OFF = 0,
LED_NUM_LOCK_STATE_ON,
LED_NUM_LOCK_STATE_COUNT,
};
enum led_id_new_kbd {
LED_ID_NUM_LOCK = 0,
LED_ID_BLE_STATE,
LED_ID_COUNT,
};
#define LED_UNAVAILABLE 0xFF
#endif /* NEW_KBD_LED_STATE_H__ */

39
inc/led_state_def.h Normal file
View File

@@ -0,0 +1,39 @@
#include "led_state.h"
#include <caf/led_effect.h>
/*
* 该文件仅被 led_state_module.c 包含一次,用于定义:
* 1) 逻辑 LED 到 CAF LED 实例编号映射;
* 2) 每个逻辑状态对应的 LED 效果。
*/
const struct {} led_state_def_include_once;
/*
* CAF LED 实例编号来源于 DTS 中 status=okay 的 gpio-leds 顺序:
* - led_0 -> 0Num Lock
* - led_1 -> 1BLE 状态)
*/
static const uint8_t led_map[LED_ID_COUNT] = {
[LED_ID_NUM_LOCK] = 0,
[LED_ID_BLE_STATE] = 1,
};
/* Num Lock 指示:灭=关闭,常亮=开启。 */
static const struct led_effect led_num_lock_state_effect[LED_NUM_LOCK_STATE_COUNT] = {
[LED_NUM_LOCK_STATE_OFF] = LED_EFFECT_LED_OFF(),
[LED_NUM_LOCK_STATE_ON] = LED_EFFECT_LED_ON(LED_COLOR(255, 255, 255)),
};
/*
* BLE 指示灯策略:
* - OFF: 熄灭USB 连接或 BLE 非活动模式)
* - WAIT_RECONNECT: 慢闪1s toggle
* - PAIRING: 快闪0.5s toggle
* - CONNECTED: 常亮
*/
static const struct led_effect led_ble_state_effect[LED_BLE_STATE_COUNT] = {
[LED_BLE_STATE_OFF] = LED_EFFECT_LED_OFF(),
[LED_BLE_STATE_WAIT_RECONNECT] = LED_EFFECT_LED_BLINK(1000, LED_COLOR(255, 255, 255)),
[LED_BLE_STATE_PAIRING] = LED_EFFECT_LED_BLINK(500, LED_COLOR(255, 255, 255)),
[LED_BLE_STATE_CONNECTED] = LED_EFFECT_LED_ON(LED_COLOR(255, 255, 255)),
};

17
inc/settings_loader_def.h Normal file
View File

@@ -0,0 +1,17 @@
/*
* Defines modules that must reach READY before CAF settings_loader
* calls settings_load().
*/
/* Enforce single inclusion in the final link unit. */
const struct {} settings_loader_def_include_once;
#include <caf/events/module_state_event.h>
static inline void get_req_modules(struct module_flags *mf)
{
module_flags_set_bit(mf, MODULE_IDX(main));
#ifdef CONFIG_CAF_BLE_STATE
module_flags_set_bit(mf, MODULE_IDX(ble_state));
#endif
}

69
inc/time_manager.h Normal file
View File

@@ -0,0 +1,69 @@
#ifndef TIME_MANAGER_H__
#define TIME_MANAGER_H__
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* 时间同步来源保持传输无关:
* - BLE/USB/手动设置都复用同一套枚举;
* - 后续如果新增其他同步链路,只需要补枚举值,不需要改事件语义。
*/
enum time_sync_source {
TIME_SYNC_SOURCE_NONE = 0,
TIME_SYNC_SOURCE_BLE,
TIME_SYNC_SOURCE_USB,
TIME_SYNC_SOURCE_MANUAL,
TIME_SYNC_SOURCE_HID,
};
/*
* 时间同步更新载荷:
* - utc_ms 统一使用 UTC 毫秒时间戳,避免内部状态受本地时区影响;
* - timezone_min 记录“显示层”所需的时区偏移,当前不拆 DST
* - accuracy_ms 允许上位机表达这次校时的可信度,未知时传 0 即可。
*/
struct time_sync_update {
uint64_t utc_ms;
int16_t timezone_min;
uint32_t accuracy_ms;
enum time_sync_source source;
};
/*
* 时间快照用于提供给显示、日志或后续 USB/调试接口:
* - synchronized=true 表示当前开机周期内已经收到有效校时;
* - has_persisted_time=true 仅表示 flash 里存过一次历史校时,不代表当前时间仍然可信;
* - ready=false 表示 time_manager 还没等到 settings_loader 完成初始化。
*/
struct time_manager_snapshot {
uint64_t utc_ms;
int16_t timezone_min;
uint32_t accuracy_ms;
enum time_sync_source source;
bool ready;
bool synchronized;
bool has_persisted_time;
};
/* 返回当前模块是否已经完成初始化,供同步入口快速拒绝“过早写入”。 */
bool time_manager_is_ready(void);
/*
* 获取当前时间快照:
* - 返回 0snapshot 已填充;
* - 返回 -EINVAL参数为空
* - 返回 -EAGAIN模块未 ready
* - 返回 -ENODATA当前开机周期尚未完成有效校时。
*/
int time_manager_get_snapshot(struct time_manager_snapshot *snapshot);
#ifdef __cplusplus
}
#endif
#endif /* TIME_MANAGER_H__ */

33
inc/time_sync_protocol.h Normal file
View File

@@ -0,0 +1,33 @@
#ifndef TIME_SYNC_PROTOCOL_H__
#define TIME_SYNC_PROTOCOL_H__
#include <stdint.h>
#include <zephyr/sys/util.h>
/*
* 统一定义时间同步协议帧格式,方便 BLE/USB 两条链路共享:
*
* byte 0 : version
* byte 1 : flags
* byte 2-3 : timezone_min (little-endian, int16)
* byte 4-11: utc_ms (little-endian, uint64)
* byte 12-15: accuracy_ms (little-endian, uint32)
*/
#define TIME_SYNC_PROTOCOL_VERSION 1U
#define TIME_SYNC_PROTOCOL_PAYLOAD_SIZE 16U
#define TIME_SYNC_PROTOCOL_OFFSET_VERSION 0U
#define TIME_SYNC_PROTOCOL_OFFSET_FLAGS 1U
#define TIME_SYNC_PROTOCOL_OFFSET_TIMEZONE 2U
#define TIME_SYNC_PROTOCOL_OFFSET_UTC_MS 4U
#define TIME_SYNC_PROTOCOL_OFFSET_ACCURACY_MS 12U
/*
* 预留 flags 字段:
* - 当前版本只要求时区字段有效;
* - 后续如果要加 DST、闰秒或来源质量扩展可以继续复用这个字节。
*/
#define TIME_SYNC_PROTOCOL_FLAG_TIMEZONE_VALID BIT(0)
#endif /* TIME_SYNC_PROTOCOL_H__ */

View File

@@ -0,0 +1,5 @@
zephyr_library()
zephyr_library_sources_ifdef(CONFIG_IP5306 drivers/power/ip5306.c)
zephyr_library_sources_ifdef(CONFIG_IP5306 drivers/power/ip5306_keepalive_sw.c)
zephyr_library_sources_ifdef(CONFIG_IP5306_KEEPALIVE_HW_NRF drivers/power/ip5306_keepalive_nrf.c)
zephyr_include_directories(include)

5
modules/ip5306/Kconfig Normal file
View File

@@ -0,0 +1,5 @@
menu "IP5306 Module"
rsource "drivers/power/Kconfig"
endmenu

View File

@@ -0,0 +1,17 @@
config IP5306
bool "Injoinic IP5306 PMIC driver"
depends on I2C && GPIO
default n
help
Enable IP5306 PMIC driver over I2C.
config IP5306_KEEPALIVE_HW_NRF
bool "Enable nRF keepalive HW offload backend"
depends on IP5306 && SOC_FAMILY_NORDIC_NRF
select NRFX_GPIOTE
select NRFX_GPPI
select NRFX_RTC2
default y
help
Enable low-power keepalive offload backend using RTC2 + GPIOTE + GPPI.
Runtime selection is controlled by devicetree property keepalive-offload.

View File

@@ -0,0 +1,152 @@
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <zephyr/device.h>
#include <zephyr/drivers/power/ip5306.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/util.h>
#include "ip5306_priv.h"
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
#include <soc_nrf_common.h>
#define IP5306_CFG_KEEPALIVE_PSEL_INIT(inst) \
.keepalive_psel = NRF_DT_GPIOS_TO_PSEL_OR(DT_DRV_INST(inst), keepalive_gpios, 0),
#else
#define IP5306_CFG_KEEPALIVE_PSEL_INIT(inst)
#endif
LOG_MODULE_REGISTER(ip5306, LOG_LEVEL_INF);
#define DT_DRV_COMPAT injoinic_ip5306
#define IP5306_REG_READ0 0x70
#define IP5306_REG_READ1 0x71
#define IP5306_STATUS_BIT BIT(3)
static int ip5306_read_reg(const struct device *dev, uint8_t reg, uint8_t *val)
{
const struct ip5306_config *cfg = dev->config;
return i2c_reg_read_byte_dt(&cfg->i2c, reg, val);
}
static int ip5306_get_status_bit(const struct device *dev, uint8_t reg, bool *flag)
{
uint8_t value = 0U;
int ret;
if (flag == NULL) {
return -EINVAL;
}
ret = ip5306_read_reg(dev, reg, &value);
if (ret != 0) {
return ret;
}
*flag = ((value & IP5306_STATUS_BIT) != 0U);
return 0;
}
static int ip5306_api_is_charging(const struct device *dev, bool *charging)
{
return ip5306_get_status_bit(dev, IP5306_REG_READ0, charging);
}
static int ip5306_api_is_charge_full(const struct device *dev, bool *full)
{
return ip5306_get_status_bit(dev, IP5306_REG_READ1, full);
}
static void ip5306_keepalive_start(const struct device *dev)
{
struct ip5306_data *data = dev->data;
const struct ip5306_config *cfg = dev->config;
if (!cfg->has_keepalive_gpio || (data->keepalive_interval_ms == 0U)) {
data->backend = IP5306_KEEPALIVE_BACKEND_NONE;
return;
}
/*
* 选择策略:
* 1) DTS 显式请求硬件 offload 时,先尝试硬件后端;
* 2) 若硬件依赖不可用或资源被占用,则告警后回退软件后端。
*/
if (cfg->keepalive_offload) {
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
int ret = ip5306_keepalive_hw_nrf_start(dev);
if (ret == 0) {
LOG_INF("Keepalive backend=HW(nRF), pulse=%ums interval=%ums",
data->keepalive_pulse_ms, data->keepalive_interval_ms);
return;
}
LOG_WRN("HW keepalive unavailable (%d), fallback to SW backend", ret);
#else
LOG_WRN("HW keepalive requested but HW backend is not built, fallback to SW backend");
#endif
}
ip5306_keepalive_sw_start(dev);
LOG_INF("Keepalive backend=SW, pulse=%ums interval=%ums",
data->keepalive_pulse_ms, data->keepalive_interval_ms);
}
static int ip5306_init(const struct device *dev)
{
const struct ip5306_config *cfg = dev->config;
struct ip5306_data *data = dev->data;
if (!i2c_is_ready_dt(&cfg->i2c)) {
return -ENODEV;
}
if (cfg->has_keepalive_gpio) {
if (!gpio_is_ready_dt(&cfg->keepalive_gpio)) {
return -ENODEV;
}
if (gpio_pin_configure_dt(&cfg->keepalive_gpio, GPIO_OUTPUT_INACTIVE) != 0) {
return -EIO;
}
}
data->keepalive_high = false;
data->dev = dev;
data->backend = IP5306_KEEPALIVE_BACKEND_NONE;
data->keepalive_pulse_ms = (cfg->keepalive_pulse_ms != 0U) ?
cfg->keepalive_pulse_ms : IP5306_KEEPALIVE_DEFAULT_PULSE_MS;
data->keepalive_interval_ms = (cfg->keepalive_interval_ms != 0U) ?
cfg->keepalive_interval_ms : IP5306_KEEPALIVE_DEFAULT_INTERVAL_MS;
ip5306_keepalive_start(dev);
return 0;
}
static const struct ip5306_driver_api ip5306_api = {
.is_charging = ip5306_api_is_charging,
.is_charge_full = ip5306_api_is_charge_full,
};
#define IP5306_DEFINE(inst) \
static struct ip5306_data ip5306_data_##inst; \
static const struct ip5306_config ip5306_cfg_##inst = { \
.i2c = I2C_DT_SPEC_INST_GET(inst), \
.keepalive_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, keepalive_gpios, {0}), \
IP5306_CFG_KEEPALIVE_PSEL_INIT(inst) \
.keepalive_pulse_ms = DT_INST_PROP(inst, keepalive_pulse_ms), \
.keepalive_interval_ms = DT_INST_PROP(inst, keepalive_interval_ms), \
.has_keepalive_gpio = DT_INST_NODE_HAS_PROP(inst, keepalive_gpios), \
.keepalive_offload = DT_INST_PROP_OR(inst, keepalive_offload, false), \
}; \
DEVICE_DT_INST_DEFINE(inst, ip5306_init, NULL, &ip5306_data_##inst, \
&ip5306_cfg_##inst, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEVICE, \
&ip5306_api);
DT_INST_FOREACH_STATUS_OKAY(IP5306_DEFINE)

View File

@@ -0,0 +1,198 @@
#include <errno.h>
#include <hal/nrf_rtc.h>
#include <nrfx_gpiote.h>
#include <nrfx_rtc.h>
#include <helpers/nrfx_gppi.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/util.h>
#include "ip5306_priv.h"
LOG_MODULE_DECLARE(ip5306, LOG_LEVEL_INF);
#define IP5306_KEEPALIVE_RTC_FREQUENCY_HZ 32768U
#define IP5306_KEEPALIVE_RTC_CC_HIGH 0U
#define IP5306_KEEPALIVE_RTC_CC_LOW 1U
#define IP5306_KEEPALIVE_RTC_CC_PERIOD 2U
#define IP5306_KEEPALIVE_MIN_PERIOD_TICKS 3U
/* nRF52 单核场景默认使用 GPIOTE0 + RTC2。 */
static nrfx_gpiote_t ip5306_keepalive_gpiote = NRFX_GPIOTE_INSTANCE(NRF_GPIOTE_INST_GET(0));
static const nrfx_rtc_t ip5306_keepalive_rtc = NRFX_RTC_INSTANCE(2);
static bool ip5306_keepalive_hw_claimed;
static void ip5306_keepalive_rtc_handler(nrfx_rtc_int_type_t int_type)
{
/*
* 这里故意不处理任何中断逻辑:
* 本模块的保活脉冲由 RTC event -> GPPI/PPI -> GPIOTE task 的硬件链路完成,
* 不依赖线程或 ISR 参与,从而减少 CPU 唤醒次数、降低功耗。
*/
ARG_UNUSED(int_type);
}
static uint32_t ip5306_keepalive_ms_to_rtc_ticks(uint32_t time_ms)
{
/* 四舍五入到最近 tick降低长时间运行时累计漂移。 */
uint64_t ticks = ((uint64_t)time_ms * IP5306_KEEPALIVE_RTC_FREQUENCY_HZ + 500U) / 1000U;
return (uint32_t)MAX(ticks, 1U);
}
int ip5306_keepalive_hw_nrf_start(const struct device *dev)
{
struct ip5306_data *data = dev->data;
const struct ip5306_config *cfg = dev->config;
const bool active_high = ((cfg->keepalive_gpio.dt_flags & GPIO_ACTIVE_LOW) == 0U);
const uint32_t period_ticks = ip5306_keepalive_ms_to_rtc_ticks(data->keepalive_interval_ms);
uint32_t high_ticks = ip5306_keepalive_ms_to_rtc_ticks(data->keepalive_pulse_ms);
int ret;
if (ip5306_keepalive_hw_claimed) {
return -EBUSY;
}
if (period_ticks < IP5306_KEEPALIVE_MIN_PERIOD_TICKS) {
return -EINVAL;
}
if (high_ticks >= (period_ticks - 1U)) {
high_ticks = period_ticks - 1U;
}
if (!nrfx_gpiote_init_check(&ip5306_keepalive_gpiote)) {
ret = nrfx_gpiote_init(&ip5306_keepalive_gpiote, NRFX_GPIOTE_DEFAULT_CONFIG_IRQ_PRIORITY);
if ((ret != 0) && (ret != -EALREADY)) {
return ret;
}
}
ret = nrfx_gpiote_channel_alloc(&ip5306_keepalive_gpiote, &data->keepalive_gpiote_channel);
if (ret != 0) {
/* GPIOTE 通道不足通常意味着被其他驱动占用。 */
LOG_ERR("Failed to allocate GPIOTE channel");
return ret;
}
const nrfx_gpiote_output_config_t output_config = {
.drive = NRF_GPIO_PIN_S0S1,
.input_connect = NRF_GPIO_PIN_INPUT_DISCONNECT,
.pull = NRF_GPIO_PIN_NOPULL,
};
const nrfx_gpiote_task_config_t task_config = {
.task_ch = data->keepalive_gpiote_channel,
.polarity = NRF_GPIOTE_POLARITY_TOGGLE,
.init_val = active_high ? NRF_GPIOTE_INITIAL_VALUE_LOW : NRF_GPIOTE_INITIAL_VALUE_HIGH,
};
ret = nrfx_gpiote_output_configure(&ip5306_keepalive_gpiote, cfg->keepalive_psel,
&output_config, &task_config);
if (ret != 0) {
goto err_free_channel;
}
nrfx_gpiote_out_task_enable(&ip5306_keepalive_gpiote, cfg->keepalive_psel);
nrfx_gpiote_out_task_force(&ip5306_keepalive_gpiote, cfg->keepalive_psel, active_high ? 0U : 1U);
/*
* RTC2 初始化前先检查是否已被外部占用:
* 若占用则直接返回 -EBUSY让上层走软件保活回退。
*/
if (nrfx_rtc_init_check(&ip5306_keepalive_rtc)) {
ret = -EBUSY;
goto err_disable_task;
}
const nrfx_rtc_config_t rtc_cfg = NRFX_RTC_DEFAULT_CONFIG;
ret = nrfx_rtc_init(&ip5306_keepalive_rtc, &rtc_cfg, ip5306_keepalive_rtc_handler);
if ((ret != 0) && (ret != -EALREADY)) {
goto err_disable_task;
}
nrfx_rtc_disable(&ip5306_keepalive_rtc);
nrfx_rtc_counter_clear(&ip5306_keepalive_rtc);
/*
* RTC 角色:时间基准,按 compare 点产生事件。
* - CC0 到点 -> 产生 COMPARE0 event脉冲上升沿触发点
* - CC1 到点 -> 产生 COMPARE1 event脉冲下降沿触发点
* - CC2 到点 -> 触发周期边界,配合 SHORT 实现自动循环
*/
ret = nrfx_rtc_cc_set(&ip5306_keepalive_rtc, IP5306_KEEPALIVE_RTC_CC_HIGH, period_ticks - high_ticks - 10U, false);
if (ret != 0) {
goto err_disable_task;
}
ret = nrfx_rtc_cc_set(&ip5306_keepalive_rtc, IP5306_KEEPALIVE_RTC_CC_LOW, period_ticks - 10U, false);
if (ret != 0) {
goto err_disable_task;
}
ret = nrfx_rtc_cc_set(&ip5306_keepalive_rtc, IP5306_KEEPALIVE_RTC_CC_PERIOD, period_ticks, false);
if (ret != 0) {
goto err_disable_task;
}
const uint32_t set_task = active_high ?
nrfx_gpiote_set_task_address_get(&ip5306_keepalive_gpiote, cfg->keepalive_psel) :
nrfx_gpiote_clr_task_address_get(&ip5306_keepalive_gpiote, cfg->keepalive_psel);
const uint32_t clr_task = active_high ?
nrfx_gpiote_clr_task_address_get(&ip5306_keepalive_gpiote, cfg->keepalive_psel) :
nrfx_gpiote_set_task_address_get(&ip5306_keepalive_gpiote, cfg->keepalive_psel);
const uint32_t high_event = nrfx_rtc_event_address_get(&ip5306_keepalive_rtc, NRF_RTC_EVENT_COMPARE_0);
const uint32_t low_event = nrfx_rtc_event_address_get(&ip5306_keepalive_rtc, NRF_RTC_EVENT_COMPARE_1);
const uint32_t period_event = nrfx_rtc_event_address_get(&ip5306_keepalive_rtc, NRF_RTC_EVENT_COMPARE_2);
const uint32_t clear_task = nrfx_rtc_task_address_get(&ip5306_keepalive_rtc, NRF_RTC_TASK_CLEAR);
/*
* GPPI/PPI 角色:事件路由器。
* 将 RTC compare event 直接连接到 GPIOTE task不经过线程/ISR。
*/
ret = nrfx_gppi_conn_alloc(high_event, set_task, &data->keepalive_set_handle);
if (ret != 0) {
goto err_disable_task;
}
ret = nrfx_gppi_conn_alloc(low_event, clr_task, &data->keepalive_clr_handle);
if (ret != 0) {
nrfx_gppi_conn_free(high_event, set_task, data->keepalive_set_handle);
goto err_disable_task;
}
/*
* 某些 SoC/SDK 组合下 RTC SHORT 宏不可用,因此这里用第三条 GPPI 链路实现“周期自动清零”。
* 这样仍保持纯硬件闭环COMPARE2 event -> RTC CLEAR task不需要 CPU/ISR 参与。
*/
ret = nrfx_gppi_conn_alloc(period_event, clear_task, &data->keepalive_period_handle);
if (ret != 0) {
nrfx_gppi_conn_free(low_event, clr_task, data->keepalive_clr_handle);
nrfx_gppi_conn_free(high_event, set_task, data->keepalive_set_handle);
goto err_disable_task;
}
/*
* GPIOTE 角色GPIO 执行器。
* 收到 task 后立即对目标 pin 执行 SET/CLR输出保活脉冲波形。
*/
nrfx_gppi_conn_enable(data->keepalive_set_handle);
nrfx_gppi_conn_enable(data->keepalive_clr_handle);
nrfx_gppi_conn_enable(data->keepalive_period_handle);
nrfx_rtc_enable(&ip5306_keepalive_rtc);
ip5306_keepalive_hw_claimed = true;
data->backend = IP5306_KEEPALIVE_BACKEND_HW_NRF;
data->dev = dev;
return 0;
err_disable_task:
nrfx_gpiote_out_task_disable(&ip5306_keepalive_gpiote, cfg->keepalive_psel);
err_free_channel:
(void)nrfx_gpiote_channel_free(&ip5306_keepalive_gpiote, data->keepalive_gpiote_channel);
return ret;
}

View File

@@ -0,0 +1,54 @@
#include <zephyr/logging/log.h>
#include <zephyr/sys/util.h>
#include "ip5306_priv.h"
LOG_MODULE_DECLARE(ip5306, LOG_LEVEL_INF);
void ip5306_keepalive_sw_work_handler(struct k_work *work)
{
struct k_work_delayable *dwork = k_work_delayable_from_work(work);
struct ip5306_data *data = CONTAINER_OF(dwork, struct ip5306_data, keepalive_work);
const struct ip5306_config *cfg = data->dev->config;
int ret;
if ((data->backend != IP5306_KEEPALIVE_BACKEND_SW) || (data->keepalive_interval_ms == 0U))
{
return;
}
if (!data->keepalive_high)
{
ret = gpio_pin_set_dt(&cfg->keepalive_gpio, GPIO_OUTPUT_ACTIVE);
if (ret != 0)
{
LOG_ERR("SW keepalive set high failed: %d", ret);
(void)k_work_schedule(&data->keepalive_work, K_MSEC(data->keepalive_interval_ms));
return;
}
data->keepalive_high = true;
(void)k_work_schedule(&data->keepalive_work, K_MSEC(data->keepalive_pulse_ms));
}
else
{
ret = gpio_pin_set_dt(&cfg->keepalive_gpio, GPIO_OUTPUT_INACTIVE);
if (ret != 0)
{
LOG_ERR("SW keepalive set low failed: %d", ret);
}
data->keepalive_high = false;
(void)k_work_schedule(&data->keepalive_work, K_MSEC(data->keepalive_interval_ms));
}
}
int ip5306_keepalive_sw_start(const struct device *dev)
{
struct ip5306_data *data = dev->data;
data->backend = IP5306_KEEPALIVE_BACKEND_SW;
data->dev = dev;
k_work_init_delayable(&data->keepalive_work, ip5306_keepalive_sw_work_handler);
return (int)k_work_schedule(&data->keepalive_work, K_MSEC(data->keepalive_interval_ms));
}

View File

@@ -0,0 +1,58 @@
#ifndef IP5306_KEEPALIVE_PRIV_H_
#define IP5306_KEEPALIVE_PRIV_H_
#include <stdbool.h>
#include <stdint.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/util.h>
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
#include <helpers/nrfx_gppi.h>
#endif
#define IP5306_KEEPALIVE_DEFAULT_PULSE_MS 100U
#define IP5306_KEEPALIVE_DEFAULT_INTERVAL_MS 20000U
enum ip5306_keepalive_backend {
IP5306_KEEPALIVE_BACKEND_NONE,
IP5306_KEEPALIVE_BACKEND_SW,
IP5306_KEEPALIVE_BACKEND_HW_NRF,
};
struct ip5306_config {
struct i2c_dt_spec i2c;
struct gpio_dt_spec keepalive_gpio;
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
uint32_t keepalive_psel;
#endif
uint32_t keepalive_pulse_ms;
uint32_t keepalive_interval_ms;
bool has_keepalive_gpio;
bool keepalive_offload;
};
struct ip5306_data {
struct k_work_delayable keepalive_work;
bool keepalive_high;
uint32_t keepalive_pulse_ms;
uint32_t keepalive_interval_ms;
enum ip5306_keepalive_backend backend;
const struct device *dev;
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
uint8_t keepalive_gpiote_channel;
nrfx_gppi_handle_t keepalive_set_handle;
nrfx_gppi_handle_t keepalive_clr_handle;
nrfx_gppi_handle_t keepalive_period_handle;
#endif
};
int ip5306_keepalive_sw_start(const struct device *dev);
int ip5306_keepalive_hw_nrf_start(const struct device *dev);
#endif /* IP5306_KEEPALIVE_PRIV_H_ */

View File

@@ -0,0 +1,33 @@
title: Injoinic IP5306 PMIC
description: |
Injoinic IP5306 power management IC accessed over I2C.
This binding exposes two status bits and an optional KEY pin keepalive
pulse output.
compatible: "injoinic,ip5306"
include: i2c-device.yaml
properties:
keepalive-gpios:
type: phandle-array
description: |
GPIO connected to KEY pin. The driver sends periodic pulses on this pin
to keep boost output alive.
keepalive-pulse-ms:
type: int
default: 100
description: Pulse width in milliseconds for each keepalive pulse.
keepalive-interval-ms:
type: int
default: 20000
description: Interval in milliseconds between keepalive pulses.
keepalive-offload:
type: boolean
description: |
Prefer hardware keepalive offload backend when available on this SoC.
If not set, driver always uses software keepalive backend.

View File

@@ -0,0 +1 @@
injoinic Injoinic

View File

@@ -0,0 +1,48 @@
#ifndef ZEPHYR_INCLUDE_DRIVERS_POWER_IP5306_H_
#define ZEPHYR_INCLUDE_DRIVERS_POWER_IP5306_H_
#include <stdbool.h>
#include <errno.h>
#include <zephyr/device.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef int (*ip5306_get_flag_t)(const struct device *dev, bool *flag);
struct ip5306_driver_api {
ip5306_get_flag_t is_charging;
ip5306_get_flag_t is_charge_full;
};
static inline int ip5306_is_charging(const struct device *dev, bool *charging)
{
const struct ip5306_driver_api *api =
(const struct ip5306_driver_api *)dev->api;
if (api == NULL || api->is_charging == NULL) {
return -ENOSYS;
}
return api->is_charging(dev, charging);
}
static inline int ip5306_is_charge_full(const struct device *dev, bool *full)
{
const struct ip5306_driver_api *api =
(const struct ip5306_driver_api *)dev->api;
if (api == NULL || api->is_charge_full == NULL) {
return -ENOSYS;
}
return api->is_charge_full(dev, full);
}
#ifdef __cplusplus
}
#endif
#endif /* ZEPHYR_INCLUDE_DRIVERS_POWER_IP5306_H_ */

View File

@@ -0,0 +1,24 @@
function(ip5306_pre_image_cmake)
if(NOT CMAKE_HOST_WIN32)
return()
endif()
cmake_parse_arguments(HOOK "" "IMAGE" "IMAGES" ${ARGN})
if(NOT DEFINED HOOK_IMAGE OR NOT TARGET ${HOOK_IMAGE})
return()
endif()
# Device Guard blocks arm-zephyr-eabi-ranlib.exe on this host. Build indexed
# archives in a single ar invocation so child images never need ranlib.
set(archive_create "<CMAKE_AR> qcs <TARGET> <LINK_FLAGS> <OBJECTS>")
set(archive_append "<CMAKE_AR> q <TARGET> <LINK_FLAGS> <OBJECTS>")
foreach(lang C CXX ASM)
set_property(TARGET ${HOOK_IMAGE} APPEND PROPERTY CMAKE_ARGS
"-DCMAKE_${lang}_ARCHIVE_CREATE:STRING=${archive_create}"
"-DCMAKE_${lang}_ARCHIVE_APPEND:STRING=${archive_append}"
"-DCMAKE_${lang}_ARCHIVE_FINISH:STRING="
)
endforeach()
endfunction()

View File

@@ -0,0 +1,6 @@
build:
cmake: .
kconfig: Kconfig
sysbuild-cmake: sysbuild
settings:
dts_root: .

33
pm_static.yml Normal file
View File

@@ -0,0 +1,33 @@
mcuboot:
address: 0x0
end_address: 0x10000
region: flash_primary
size: 0x10000
mcuboot_pad:
address: 0x10000
end_address: 0x10200
region: flash_primary
size: 0x200
app:
address: 0x10200
end_address: 0xf8000
region: flash_primary
size: 0xe7e00
mcuboot_primary:
address: 0x10000
end_address: 0xf8000
orig_span: &id001
- mcuboot_pad
- app
region: flash_primary
size: 0xe8000
span: *id001
settings_storage:
address: 0xf8000
end_address: 0x100000
region: flash_primary
size: 0x8000

119
prj.conf Normal file
View File

@@ -0,0 +1,119 @@
CONFIG_CAF=y
CONFIG_HEAP_MEM_POOL_SIZE=2048
CONFIG_LOG=y
CONFIG_ASSERT=y
CONFIG_ASSERT_VERBOSE=y
CONFIG_RESET_ON_FATAL_ERROR=n
CONFIG_FAULT_DUMP=2
CONFIG_SIZE_OPTIMIZATIONS=y
CONFIG_ZCBOR=n
CONFIG_BOOTLOADER_MCUBOOT=n
CONFIG_MCUMGR=n
CONFIG_MCUMGR_TRANSPORT_BT=n
CONFIG_MCUMGR_GRP_IMG=n
CONFIG_MCUMGR_GRP_OS=n
CONFIG_IMG_MANAGER=n
CONFIG_STREAM_FLASH=n
CONFIG_RETAINED_MEM=y
CONFIG_RETENTION=y
CONFIG_RETENTION_BOOT_MODE=y
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_SMP=y
CONFIG_BT_PERIPHERAL_PREF_TIMEOUT=400
CONFIG_BT_DEVICE_NAME="new_kbd"
CONFIG_BT_DEVICE_APPEARANCE=961
CONFIG_BT_MAX_CONN=1
CONFIG_BT_MAX_PAIRED=4
CONFIG_BT_ID_MAX=4
CONFIG_SETTINGS=y
CONFIG_SETTINGS_NVS=y
CONFIG_NVS=y
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_BT_SETTINGS=y
CONFIG_CAF_BLE_STATE=y
CONFIG_CAF_BLE_ADV=y
CONFIG_CAF_BLE_ADV_FAST_ADV=y
CONFIG_CAF_BLE_ADV_FAST_INT_MIN=0x0030
CONFIG_CAF_BLE_ADV_FAST_INT_MAX=0x0060
CONFIG_CAF_BLE_ADV_FAST_ADV_TIMEOUT=180
CONFIG_CAF_MODULE_SUSPEND_EVENTS=y
CONFIG_CAF_SETTINGS_LOADER=y
CONFIG_BT_ADV_PROV_FLAGS=y
CONFIG_BT_ADV_PROV_GAP_APPEARANCE=y
CONFIG_BT_ADV_PROV_DEVICE_NAME=y
CONFIG_BT_ADV_PROV_DEVICE_NAME_PAIRING_MODE_ONLY=n
CONFIG_BT_ADV_PROV_SWIFT_PAIR=y
CONFIG_BT_HIDS=y
CONFIG_BT_CONN_CTX=y
CONFIG_BT_GATT_POOL=y
CONFIG_BT_GATT_CHRC_POOL_SIZE=16
CONFIG_BT_GATT_UUID16_POOL_SIZE=24
CONFIG_BT_HIDS_ATTR_MAX=40
CONFIG_BT_HIDS_INPUT_REP_MAX=4
CONFIG_BT_HIDS_OUTPUT_REP_MAX=3
CONFIG_BT_HIDS_FEATURE_REP_MAX=0
CONFIG_BT_BAS=y
CONFIG_BT_DIS=y
CONFIG_BT_DIS_PNP=y
CONFIG_BT_DIS_PNP_VID_SRC=2
CONFIG_BT_DIS_PNP_VID=0x1209
CONFIG_BT_DIS_PNP_PID=0x0001
CONFIG_USB_DEVICE_STACK_NEXT=y
CONFIG_USBD_HID_SUPPORT=y
CONFIG_UDC_BUF_POOL_SIZE=8192
CONFIG_UDC_BUF_COUNT=32
CONFIG_USBD_MAX_UDC_MSG=20
CONFIG_USBD_MSG_SLAB_COUNT=16
CONFIG_LED=y
CONFIG_LED_GPIO=y
CONFIG_CAF_LEDS=y
CONFIG_CAF_LEDS_GPIO=y
CONFIG_CAF_LEDS_PM_EVENTS=y
CONFIG_CAF_POWER_MANAGER=y
CONFIG_CAF_POWER_MANAGER_TIMEOUT=30
CONFIG_CAF_POWER_MANAGER_ERROR_TIMEOUT=10
CONFIG_REBOOT=y
CONFIG_CAF_KEEP_ALIVE_EVENTS=y
CONFIG_CAF_BUTTONS=y
CONFIG_CAF_BUTTONS_DEF_PATH="buttons_def.h"
CONFIG_CAF_BUTTONS_SCAN_INTERVAL=5
CONFIG_CAF_BUTTONS_DEBOUNCE_INTERVAL=10
CONFIG_ADC=y
CONFIG_I2C=y
CONFIG_IP5306=y
CONFIG_SENSOR=y
CONFIG_DISPLAY=y
CONFIG_MIPI_DBI=y
CONFIG_ST7789V=y
CONFIG_LVGL=y
CONFIG_LV_CONF_MINIMAL=y
CONFIG_LV_COLOR_16_SWAP=y
CONFIG_LV_BUILD_EXAMPLES=n
CONFIG_LV_BUILD_DEMOS=n
CONFIG_LV_USE_LABEL=y
CONFIG_LV_USE_FLEX=y
CONFIG_LV_USE_FONT_COMPRESSED=y
CONFIG_LV_TXT_ENC_UTF8=y
CONFIG_LV_Z_AUTO_INIT=y
CONFIG_LV_Z_VDB_SIZE=25
CONFIG_LV_Z_BITS_PER_PIXEL=16
CONFIG_LV_Z_LVGL_MUTEX=y
CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE=y
CONFIG_LV_Z_LVGL_WORKQUEUE_STACK_SIZE=8192
CONFIG_LV_Z_FLUSH_THREAD=y
CONFIG_LV_Z_DOUBLE_VDB=y
CONFIG_LV_Z_MEM_POOL_SIZE=16384
CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=4096

View File

@@ -0,0 +1,33 @@
#include "battery_status_event.h"
static void log_battery_status_event(const struct app_event_header *aeh)
{
const struct battery_status_event *event = cast_battery_status_event(aeh);
APP_EVENT_MANAGER_LOG(aeh,
"flags=0x%02x charging=%u full=%u soc=%u",
event->flags,
BATTERY_STATUS_IS_CHARGING(event->flags),
BATTERY_STATUS_IS_FULL(event->flags),
event->soc);
}
static void profile_battery_status_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct battery_status_event *event = cast_battery_status_event(aeh);
nrf_profiler_log_encode_uint8(buf, event->flags);
nrf_profiler_log_encode_uint8(buf, event->soc);
}
APP_EVENT_INFO_DEFINE(battery_status_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8),
ENCODE("flags", "soc"),
profile_battery_status_event);
APP_EVENT_TYPE_DEFINE(battery_status_event,
log_battery_status_event,
&battery_status_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,61 @@
#ifndef BATTERY_STATUS_EVENT_H
#define BATTERY_STATUS_EVENT_H
#include <stdbool.h>
#include <stdint.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
struct battery_status_event
{
struct app_event_header header;
uint8_t flags;
uint8_t soc;
};
#define BATTERY_STATUS_FLAG_CHARGING (1U << 0)
#define BATTERY_STATUS_FLAG_FULL (1U << 1)
#define BATTERY_STATUS_IS_CHARGING(flags) (((flags) & BATTERY_STATUS_FLAG_CHARGING) != 0U)
#define BATTERY_STATUS_IS_FULL(flags) (((flags) & BATTERY_STATUS_FLAG_FULL) != 0U)
APP_EVENT_TYPE_DECLARE(battery_status_event);
static inline void battery_status_event_submit(bool charging, bool full, uint8_t soc)
{
struct battery_status_event *event = new_battery_status_event();
event->flags = 0U;
if (charging) {
event->flags |= BATTERY_STATUS_FLAG_CHARGING;
}
if (full) {
event->flags |= BATTERY_STATUS_FLAG_FULL;
}
event->soc = soc;
APP_EVENT_SUBMIT(event);
}
static inline uint8_t battery_status_event_get_flags(const struct battery_status_event *event)
{
return event->flags;
}
static inline bool battery_status_event_is_charging(const struct battery_status_event *event)
{
return BATTERY_STATUS_IS_CHARGING(event->flags);
}
static inline bool battery_status_event_is_full(const struct battery_status_event *event)
{
return BATTERY_STATUS_IS_FULL(event->flags);
}
static inline uint8_t battery_status_event_get_soc(const struct battery_status_event *event)
{
return event->soc;
}
#endif

18
src/events/config_event.c Normal file
View File

@@ -0,0 +1,18 @@
#include "config_event.h"
static void log_config_event(const struct app_event_header *aeh)
{
const struct config_event *event = cast_config_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "status:%u %s rcpt:%02x id:%02x",
event->status,
event->is_request ? "req" : "rsp",
event->recipient,
event->event_id);
}
APP_EVENT_TYPE_DEFINE(config_event,
log_config_event,
NULL,
APP_EVENT_FLAGS_CREATE(
APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

54
src/events/config_event.h Normal file
View File

@@ -0,0 +1,54 @@
/*
* Lightweight config event used for local module configuration.
*/
#ifndef NEW_KBD_CONFIG_EVENT_H__
#define NEW_KBD_CONFIG_EVENT_H__
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Keep the same local recipient as nRF Desktop config channel. */
#define CFG_CHAN_RECIPIENT_LOCAL 0x00
/* Event ID field layout (compatible with nRF Desktop config_event encoding). */
#define MOD_FIELD_POS 4
#define MOD_FIELD_SIZE 4
#define MOD_FIELD_MASK BIT_MASK(MOD_FIELD_SIZE)
#define MOD_FIELD_GET(id) (((id) >> MOD_FIELD_POS) & MOD_FIELD_MASK)
#define OPT_FIELD_POS 0
#define OPT_FIELD_SIZE 4
#define OPT_FIELD_MASK BIT_MASK(OPT_FIELD_SIZE)
#define OPT_FIELD_GET(id) (((id) >> OPT_FIELD_POS) & OPT_FIELD_MASK)
#define OPT_ID_GET(opt) ((opt) - 1U)
enum config_status {
CONFIG_STATUS_SET = 0,
CONFIG_STATUS_FETCH,
CONFIG_STATUS_SUCCESS,
CONFIG_STATUS_REJECT,
};
struct config_event {
struct app_event_header header;
uint16_t transport_id;
bool is_request;
uint8_t event_id;
uint8_t recipient;
uint8_t status;
struct event_dyndata dyndata;
};
APP_EVENT_TYPE_DYNDATA_DECLARE(config_event);
#ifdef __cplusplus
}
#endif
#endif /* NEW_KBD_CONFIG_EVENT_H__ */

View File

@@ -0,0 +1,31 @@
#include "display_theme_event.h"
static void log_display_theme_event(const struct app_event_header *aeh)
{
const struct display_theme_event *event = cast_display_theme_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "rgb=(%u,%u,%u)",
event->red, event->green, event->blue);
}
static void profile_display_theme_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct display_theme_event *event = cast_display_theme_event(aeh);
nrf_profiler_log_encode_uint8(buf, event->red);
nrf_profiler_log_encode_uint8(buf, event->green);
nrf_profiler_log_encode_uint8(buf, event->blue);
}
APP_EVENT_INFO_DEFINE(display_theme_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8),
ENCODE("red", "green", "blue"),
profile_display_theme_event);
APP_EVENT_TYPE_DEFINE(display_theme_event,
log_display_theme_event,
&display_theme_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,30 @@
#ifndef DISPLAY_THEME_EVENT_H__
#define DISPLAY_THEME_EVENT_H__
#include <stdint.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
struct display_theme_event {
struct app_event_header header;
uint8_t red;
uint8_t green;
uint8_t blue;
};
APP_EVENT_TYPE_DECLARE(display_theme_event);
static inline void display_theme_event_submit(uint8_t red,
uint8_t green,
uint8_t blue)
{
struct display_theme_event *event = new_display_theme_event();
event->red = red;
event->green = green;
event->blue = blue;
APP_EVENT_SUBMIT(event);
}
#endif /* DISPLAY_THEME_EVENT_H__ */

View File

@@ -0,0 +1,26 @@
#include "hid_boot_event.h"
static void log_hid_boot_event(const struct app_event_header *aeh)
{
const struct hid_boot_event *event = cast_hid_boot_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "payload_len=%u", event->dyndata.size);
}
static void profile_hid_boot_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_boot_event *event = cast_hid_boot_event(aeh);
nrf_profiler_log_encode_uint16(buf, event->dyndata.size);
}
APP_EVENT_INFO_DEFINE(hid_boot_event,
ENCODE(NRF_PROFILER_ARG_U16),
ENCODE("len"),
profile_hid_boot_event);
APP_EVENT_TYPE_DEFINE(hid_boot_event,
log_hid_boot_event,
&hid_boot_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,45 @@
#ifndef HID_BOOT_EVENT_H__
#define HID_BOOT_EVENT_H__
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
/*
* HID Boot 输入报告事件:
* - dyndata 仅包含 boot keyboard payload
* - 不包含 report_id消费方固定按 boot keyboard 语义处理。
*/
struct hid_boot_event {
struct app_event_header header;
struct event_dyndata dyndata;
};
APP_EVENT_TYPE_DYNDATA_DECLARE(hid_boot_event);
static inline void hid_boot_event_submit(const uint8_t *data, size_t size)
{
struct hid_boot_event *event = new_hid_boot_event(size);
if ((size > 0U) && (data != NULL)) {
memcpy(event->dyndata.data, data, size);
}
APP_EVENT_SUBMIT(event);
}
static inline const uint8_t *hid_boot_event_get_data(const struct hid_boot_event *event)
{
return event->dyndata.data;
}
static inline size_t hid_boot_event_get_size(const struct hid_boot_event *event)
{
return event->dyndata.size;
}
#endif /* HID_BOOT_EVENT_H__ */

View File

@@ -0,0 +1,29 @@
#include "hid_host_ack_event.h"
static void log_hid_host_ack_event(const struct app_event_header *aeh)
{
const struct hid_host_ack_event *event = cast_hid_host_ack_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "transport=%u cmd=0x%02x",
event->transport, event->cmd);
}
static void profile_hid_host_ack_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_host_ack_event *event = cast_hid_host_ack_event(aeh);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->transport);
nrf_profiler_log_encode_uint8(buf, event->cmd);
}
APP_EVENT_INFO_DEFINE(hid_host_ack_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8),
ENCODE("transport", "cmd"),
profile_hid_host_ack_event);
APP_EVENT_TYPE_DEFINE(hid_host_ack_event,
log_hid_host_ack_event,
&hid_host_ack_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,29 @@
#ifndef HID_HOST_ACK_EVENT_H__
#define HID_HOST_ACK_EVENT_H__
#include <stdint.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
#include "hid_host_transport.h"
struct hid_host_ack_event {
struct app_event_header header;
enum hid_host_transport transport;
uint8_t cmd;
};
APP_EVENT_TYPE_DECLARE(hid_host_ack_event);
static inline void hid_host_ack_event_submit(enum hid_host_transport transport,
uint8_t cmd)
{
struct hid_host_ack_event *event = new_hid_host_ack_event();
event->transport = transport;
event->cmd = cmd;
APP_EVENT_SUBMIT(event);
}
#endif /* HID_HOST_ACK_EVENT_H__ */

View File

@@ -0,0 +1,36 @@
#include "hid_host_command_error_event.h"
static void log_hid_host_command_error_event(const struct app_event_header *aeh)
{
const struct hid_host_command_error_event *event =
cast_hid_host_command_error_event(aeh);
APP_EVENT_MANAGER_LOG(aeh,
"transport=%u cmd=0x%02x reason=%u",
event->transport,
event->cmd,
event->reason);
}
static void profile_hid_host_command_error_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_host_command_error_event *event =
cast_hid_host_command_error_event(aeh);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->transport);
nrf_profiler_log_encode_uint8(buf, event->cmd);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->reason);
}
APP_EVENT_INFO_DEFINE(hid_host_command_error_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8),
ENCODE("transport", "cmd", "reason"),
profile_hid_host_command_error_event);
APP_EVENT_TYPE_DEFINE(hid_host_command_error_event,
log_hid_host_command_error_event,
&hid_host_command_error_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,41 @@
#ifndef HID_HOST_COMMAND_ERROR_EVENT_H__
#define HID_HOST_COMMAND_ERROR_EVENT_H__
#include <stdint.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
#include "hid_host_transport.h"
enum hid_host_command_error_reason {
HID_HOST_COMMAND_ERROR_UNKNOWN_CMD = 0,
HID_HOST_COMMAND_ERROR_INVALID_LENGTH,
HID_HOST_COMMAND_ERROR_INVALID_PARAM,
HID_HOST_COMMAND_ERROR_NOT_READY,
};
struct hid_host_command_error_event {
struct app_event_header header;
enum hid_host_transport transport;
enum hid_host_command_error_reason reason;
uint8_t cmd;
};
APP_EVENT_TYPE_DECLARE(hid_host_command_error_event);
static inline void hid_host_command_error_event_submit(
enum hid_host_transport transport,
uint8_t cmd,
enum hid_host_command_error_reason reason)
{
struct hid_host_command_error_event *event =
new_hid_host_command_error_event();
event->transport = transport;
event->cmd = cmd;
event->reason = reason;
APP_EVENT_SUBMIT(event);
}
#endif /* HID_HOST_COMMAND_ERROR_EVENT_H__ */

View File

@@ -0,0 +1,47 @@
#include "hid_host_command_event.h"
static const char *transport_name(enum hid_host_transport transport)
{
switch (transport) {
case HID_HOST_TRANSPORT_USB:
return "usb";
case HID_HOST_TRANSPORT_BLE:
return "ble";
default:
return "unknown";
}
}
static void log_hid_host_command_event(const struct app_event_header *aeh)
{
const struct hid_host_command_event *event =
cast_hid_host_command_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "transport=%s cmd=0x%02x data_len=%u",
transport_name(event->transport),
event->cmd,
event->data_len);
}
static void profile_hid_host_command_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_host_command_event *event =
cast_hid_host_command_event(aeh);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->transport);
nrf_profiler_log_encode_uint8(buf, event->cmd);
nrf_profiler_log_encode_uint8(buf, event->data_len);
}
APP_EVENT_INFO_DEFINE(hid_host_command_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8),
ENCODE("transport", "cmd", "data_len"),
profile_hid_host_command_event);
APP_EVENT_TYPE_DEFINE(hid_host_command_event,
log_hid_host_command_event,
&hid_host_command_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,44 @@
#ifndef HID_HOST_COMMAND_EVENT_H__
#define HID_HOST_COMMAND_EVENT_H__
#include <stdint.h>
#include <string.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
#include <zephyr/sys/util.h>
#include "hid_host_command_protocol.h"
#include "hid_host_transport.h"
struct hid_host_command_event {
struct app_event_header header;
enum hid_host_transport transport;
uint8_t cmd;
uint8_t data_len;
uint8_t data[HID_HOST_CMD_DATA_SIZE];
};
APP_EVENT_TYPE_DECLARE(hid_host_command_event);
static inline void hid_host_command_event_submit(enum hid_host_transport transport,
uint8_t cmd,
const uint8_t *data,
size_t data_len)
{
struct hid_host_command_event *event = new_hid_host_command_event();
size_t copy_len = MIN(data_len, (size_t)HID_HOST_CMD_DATA_SIZE);
event->transport = transport;
event->cmd = cmd;
event->data_len = (uint8_t)copy_len;
memset(event->data, 0, sizeof(event->data));
if ((copy_len > 0U) && (data != NULL)) {
memcpy(event->data, data, copy_len);
}
APP_EVENT_SUBMIT(event);
}
#endif /* HID_HOST_COMMAND_EVENT_H__ */

View File

@@ -0,0 +1,34 @@
#include "hid_protocol_event.h"
static const char *const hid_protocol_name[] = {
[HID_PROTO_BOOT] = "BOOT",
[HID_PROTO_REPORT] = "REPORT",
};
static void log_hid_protocol_event(const struct app_event_header *aeh)
{
const struct hid_protocol_event *event = cast_hid_protocol_event(aeh);
__ASSERT_NO_MSG(event->protocol < ARRAY_SIZE(hid_protocol_name));
APP_EVENT_MANAGER_LOG(aeh, "protocol=%s",
hid_protocol_name[event->protocol]);
}
static void profile_hid_protocol_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_protocol_event *event = cast_hid_protocol_event(aeh);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->protocol);
}
APP_EVENT_INFO_DEFINE(hid_protocol_event,
ENCODE(NRF_PROFILER_ARG_U8),
ENCODE("protocol"),
profile_hid_protocol_event);
APP_EVENT_TYPE_DEFINE(hid_protocol_event,
log_hid_protocol_event,
&hid_protocol_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,49 @@
#ifndef HID_PROTOCOL_EVENT_H__
#define HID_PROTOCOL_EVENT_H__
#include <stdbool.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
enum hid_protocol_type {
HID_PROTO_BOOT = 0,
HID_PROTO_REPORT,
};
/*
* HID 传输层在收到主机 set_protocol 请求后上报该事件。
*/
struct hid_protocol_event {
struct app_event_header header;
enum hid_protocol_type protocol;
};
APP_EVENT_TYPE_DECLARE(hid_protocol_event);
static inline void hid_protocol_event_submit(enum hid_protocol_type protocol)
{
struct hid_protocol_event *event = new_hid_protocol_event();
event->protocol = protocol;
APP_EVENT_SUBMIT(event);
}
static inline enum hid_protocol_type hid_protocol_event_get_protocol(
const struct hid_protocol_event *event)
{
return event->protocol;
}
static inline bool hid_protocol_event_is_boot_protocol(const struct hid_protocol_event *event)
{
return event->protocol == HID_PROTO_BOOT;
}
static inline bool hid_protocol_event_is_report_protocol(const struct hid_protocol_event *event)
{
return event->protocol == HID_PROTO_REPORT;
}
#endif /* HID_PROTOCOL_EVENT_H__ */

View File

@@ -0,0 +1,42 @@
#include "hid_report_event.h"
static void log_hid_report_event(const struct app_event_header *aeh)
{
const struct hid_report_event *event = cast_hid_report_event(aeh);
uint8_t report_id = 0x00;
uint16_t payload_len = 0U;
if (event->dyndata.size >= 1U) {
report_id = event->dyndata.data[0];
payload_len = event->dyndata.size - 1U;
}
APP_EVENT_MANAGER_LOG(aeh, "report_id=0x%02x payload_len=%u",
report_id,
payload_len);
}
static void profile_hid_report_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_report_event *event = cast_hid_report_event(aeh);
uint8_t report_id = 0x00;
if (event->dyndata.size >= 1U) {
report_id = event->dyndata.data[0];
}
nrf_profiler_log_encode_uint8(buf, report_id);
nrf_profiler_log_encode_uint16(buf, event->dyndata.size);
}
APP_EVENT_INFO_DEFINE(hid_report_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U16),
ENCODE("report_id", "len"),
profile_hid_report_event);
APP_EVENT_TYPE_DEFINE(hid_report_event,
log_hid_report_event,
&hid_report_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,45 @@
#ifndef HID_REPORT_EVENT_H__
#define HID_REPORT_EVENT_H__
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
/*
* HID Report 输入报告事件:
* - dyndata 编码为 [report_id | payload]
* - 仅承载 Report protocol 报文。
*/
struct hid_report_event {
struct app_event_header header;
struct event_dyndata dyndata;
};
APP_EVENT_TYPE_DYNDATA_DECLARE(hid_report_event);
static inline void hid_report_event_submit(const uint8_t *data, size_t size)
{
struct hid_report_event *event = new_hid_report_event(size);
if ((size > 0U) && (data != NULL)) {
memcpy(event->dyndata.data, data, size);
}
APP_EVENT_SUBMIT(event);
}
static inline const uint8_t *hid_report_event_get_data(const struct hid_report_event *event)
{
return event->dyndata.data;
}
static inline size_t hid_report_event_get_size(const struct hid_report_event *event)
{
return event->dyndata.size;
}
#endif /* HID_REPORT_EVENT_H__ */

View File

@@ -0,0 +1,29 @@
#include "hid_tx_done_event.h"
static void log_hid_tx_done_event(const struct app_event_header *aeh)
{
const struct hid_tx_done_event *event = cast_hid_tx_done_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "kind=%u success=%u",
event->kind, event->success);
}
static void profile_hid_tx_done_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_tx_done_event *event = cast_hid_tx_done_event(aeh);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->kind);
nrf_profiler_log_encode_uint8(buf, event->success ? 1U : 0U);
}
APP_EVENT_INFO_DEFINE(hid_tx_done_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8),
ENCODE("kind", "success"),
profile_hid_tx_done_event);
APP_EVENT_TYPE_DEFINE(hid_tx_done_event,
log_hid_tx_done_event,
&hid_tx_done_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,28 @@
#ifndef HID_TX_DONE_EVENT_H__
#define HID_TX_DONE_EVENT_H__
#include <stdbool.h>
#include <stdint.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
#include "hid_tx_event.h"
struct hid_tx_done_event {
struct app_event_header header;
enum hid_tx_kind kind;
bool success;
};
APP_EVENT_TYPE_DECLARE(hid_tx_done_event);
static inline void hid_tx_done_event_submit(enum hid_tx_kind kind, bool success)
{
struct hid_tx_done_event *event = new_hid_tx_done_event();
event->kind = kind;
event->success = success;
APP_EVENT_SUBMIT(event);
}
#endif /* HID_TX_DONE_EVENT_H__ */

49
src/events/hid_tx_event.c Normal file
View File

@@ -0,0 +1,49 @@
#include "hid_tx_event.h"
static void log_hid_tx_event(const struct app_event_header *aeh)
{
const struct hid_tx_event *event = cast_hid_tx_event(aeh);
uint8_t report_id = 0x00;
uint16_t payload_len = event->dyndata.size;
if ((event->kind == HID_TX_KIND_REPORT) && (event->dyndata.size >= 1U)) {
report_id = event->dyndata.data[0];
payload_len = event->dyndata.size - 1U;
}
APP_EVENT_MANAGER_LOG(aeh,
"kind=%u route=%u report_id=0x%02x payload_len=%u",
event->kind,
event->route,
report_id,
payload_len);
}
static void profile_hid_tx_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_tx_event *event = cast_hid_tx_event(aeh);
uint8_t report_id = 0x00;
if ((event->kind == HID_TX_KIND_REPORT) && (event->dyndata.size >= 1U)) {
report_id = event->dyndata.data[0];
}
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->kind);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->route);
nrf_profiler_log_encode_uint8(buf, report_id);
nrf_profiler_log_encode_uint16(buf, event->dyndata.size);
}
APP_EVENT_INFO_DEFINE(hid_tx_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U16),
ENCODE("kind", "route", "report_id", "len"),
profile_hid_tx_event);
APP_EVENT_TYPE_DEFINE(hid_tx_event,
log_hid_tx_event,
&hid_tx_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

69
src/events/hid_tx_event.h Normal file
View File

@@ -0,0 +1,69 @@
#ifndef HID_TX_EVENT_H__
#define HID_TX_EVENT_H__
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
enum hid_tx_kind {
HID_TX_KIND_BOOT = 0,
HID_TX_KIND_REPORT,
};
enum hid_tx_route {
HID_TX_ROUTE_AUTO = 0,
HID_TX_ROUTE_USB,
HID_TX_ROUTE_BLE,
};
struct hid_tx_event {
struct app_event_header header;
enum hid_tx_kind kind;
enum hid_tx_route route;
struct event_dyndata dyndata;
};
APP_EVENT_TYPE_DYNDATA_DECLARE(hid_tx_event);
static inline void hid_tx_event_submit_routed(enum hid_tx_kind kind,
enum hid_tx_route route,
const uint8_t *data,
size_t size)
{
struct hid_tx_event *event = new_hid_tx_event(size);
event->kind = kind;
event->route = route;
if ((size > 0U) && (data != NULL)) {
memcpy(event->dyndata.data, data, size);
}
APP_EVENT_SUBMIT(event);
}
static inline void hid_tx_event_submit(enum hid_tx_kind kind,
const uint8_t *data,
size_t size)
{
hid_tx_event_submit_routed(kind, HID_TX_ROUTE_AUTO, data, size);
}
static inline const uint8_t *hid_tx_event_get_data(const struct hid_tx_event *event)
{
return event->dyndata.data;
}
static inline size_t hid_tx_event_get_size(const struct hid_tx_event *event)
{
return event->dyndata.size;
}
static inline enum hid_tx_route hid_tx_event_get_route(const struct hid_tx_event *event)
{
return event->route;
}
#endif /* HID_TX_EVENT_H__ */

View File

@@ -0,0 +1,26 @@
#include "hid_vendor_mask_event.h"
static void log_hid_vendor_mask_event(const struct app_event_header *aeh)
{
const struct hid_vendor_mask_event *event = cast_hid_vendor_mask_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "len=%u", event->dyndata.size);
}
static void profile_hid_vendor_mask_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_vendor_mask_event *event = cast_hid_vendor_mask_event(aeh);
nrf_profiler_log_encode_uint16(buf, event->dyndata.size);
}
APP_EVENT_INFO_DEFINE(hid_vendor_mask_event,
ENCODE(NRF_PROFILER_ARG_U16),
ENCODE("len"),
profile_hid_vendor_mask_event);
APP_EVENT_TYPE_DEFINE(hid_vendor_mask_event,
log_hid_vendor_mask_event,
&hid_vendor_mask_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,39 @@
#ifndef HID_VENDOR_MASK_EVENT_H__
#define HID_VENDOR_MASK_EVENT_H__
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
struct hid_vendor_mask_event {
struct app_event_header header;
struct event_dyndata dyndata;
};
APP_EVENT_TYPE_DYNDATA_DECLARE(hid_vendor_mask_event);
static inline void hid_vendor_mask_event_submit(const uint8_t *data, size_t size)
{
struct hid_vendor_mask_event *event = new_hid_vendor_mask_event(size);
if ((size > 0U) && (data != NULL)) {
memcpy(event->dyndata.data, data, size);
}
APP_EVENT_SUBMIT(event);
}
static inline const uint8_t *hid_vendor_mask_event_get_data(const struct hid_vendor_mask_event *event)
{
return event->dyndata.data;
}
static inline size_t hid_vendor_mask_event_get_size(const struct hid_vendor_mask_event *event)
{
return event->dyndata.size;
}
#endif /* HID_VENDOR_MASK_EVENT_H__ */

View File

@@ -0,0 +1,28 @@
#include "keyboard_led_event.h"
static void log_keyboard_led_event(const struct app_event_header *aeh)
{
const struct keyboard_led_event *event =
cast_keyboard_led_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "mask=0x%02x", event->led_mask);
}
static void profile_keyboard_led_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct keyboard_led_event *event =
cast_keyboard_led_event(aeh);
nrf_profiler_log_encode_uint8(buf, event->led_mask);
}
APP_EVENT_INFO_DEFINE(keyboard_led_event,
ENCODE(NRF_PROFILER_ARG_U8),
ENCODE("led_mask"),
profile_keyboard_led_event);
APP_EVENT_TYPE_DEFINE(keyboard_led_event,
log_keyboard_led_event,
&keyboard_led_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,73 @@
#ifndef KEYBOARD_LED_EVENT_H__
#define KEYBOARD_LED_EVENT_H__
#include <stdbool.h>
#include <stdint.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
/*
* 键盘 LED 事件:
* - 由 USB/BLE HID 接收主机输出报告后上报;
* - 仅保留主机下发的原始 LED mask消费方按需解码。
*/
struct keyboard_led_event {
struct app_event_header header;
uint8_t led_mask;
};
#define KEYBOARD_LED_MASK_NUM_LOCK (1U << 0)
#define KEYBOARD_LED_MASK_CAPS_LOCK (1U << 1)
#define KEYBOARD_LED_MASK_SCROLL_LOCK (1U << 2)
#define KEYBOARD_LED_MASK_COMPOSE (1U << 3)
#define KEYBOARD_LED_MASK_KANA (1U << 4)
#define KEYBOARD_LED_NUM_LOCK(mask) (((mask) & KEYBOARD_LED_MASK_NUM_LOCK) != 0U)
#define KEYBOARD_LED_CAPS_LOCK(mask) (((mask) & KEYBOARD_LED_MASK_CAPS_LOCK) != 0U)
#define KEYBOARD_LED_SCROLL_LOCK(mask) (((mask) & KEYBOARD_LED_MASK_SCROLL_LOCK) != 0U)
#define KEYBOARD_LED_COMPOSE(mask) (((mask) & KEYBOARD_LED_MASK_COMPOSE) != 0U)
#define KEYBOARD_LED_KANA(mask) (((mask) & KEYBOARD_LED_MASK_KANA) != 0U)
APP_EVENT_TYPE_DECLARE(keyboard_led_event);
static inline void keyboard_led_event_submit(uint8_t led_mask)
{
struct keyboard_led_event *event = new_keyboard_led_event();
event->led_mask = led_mask;
APP_EVENT_SUBMIT(event);
}
static inline uint8_t keyboard_led_event_get_mask(const struct keyboard_led_event *event)
{
return event->led_mask;
}
static inline bool keyboard_led_event_is_num_lock_on(const struct keyboard_led_event *event)
{
return KEYBOARD_LED_NUM_LOCK(event->led_mask);
}
static inline bool keyboard_led_event_is_caps_lock_on(const struct keyboard_led_event *event)
{
return KEYBOARD_LED_CAPS_LOCK(event->led_mask);
}
static inline bool keyboard_led_event_is_scroll_lock_on(const struct keyboard_led_event *event)
{
return KEYBOARD_LED_SCROLL_LOCK(event->led_mask);
}
static inline bool keyboard_led_event_is_compose_on(const struct keyboard_led_event *event)
{
return KEYBOARD_LED_COMPOSE(event->led_mask);
}
static inline bool keyboard_led_event_is_kana_on(const struct keyboard_led_event *event)
{
return KEYBOARD_LED_KANA(event->led_mask);
}
#endif /* KEYBOARD_LED_EVENT_H__ */

34
src/events/mode_event.c Normal file
View File

@@ -0,0 +1,34 @@
#include "mode_event.h"
static const char *const mode_name[] = {
[MODE_TYPE_USB] = "USB",
[MODE_TYPE_BLE] = "BLE",
[MODE_TYPE_2G4] = "2.4G",
};
static void log_mode_event(const struct app_event_header *aeh)
{
const struct mode_event *event = cast_mode_event(aeh);
__ASSERT_NO_MSG(event->mode_type < MODE_TYPE_COUNT);
APP_EVENT_MANAGER_LOG(aeh, "mode=%s(%u)", mode_name[event->mode_type], event->mode_type);
}
static void profile_mode_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct mode_event *event = cast_mode_event(aeh);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->mode_type);
}
APP_EVENT_INFO_DEFINE(mode_event,
ENCODE(NRF_PROFILER_ARG_U8),
ENCODE("mode"),
profile_mode_event);
APP_EVENT_TYPE_DEFINE(mode_event,
log_mode_event,
&mode_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

53
src/events/mode_event.h Normal file
View File

@@ -0,0 +1,53 @@
#ifndef MODE_EVENT_H
#define MODE_EVENT_H
#include <stdbool.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
typedef enum
{
MODE_TYPE_USB,
MODE_TYPE_BLE,
MODE_TYPE_2G4,
MODE_TYPE_COUNT,
} mode_type_t;
struct mode_event
{
struct app_event_header header;
mode_type_t mode_type;
};
APP_EVENT_TYPE_DECLARE(mode_event);
static inline void mode_event_submit(mode_type_t mode)
{
struct mode_event *event = new_mode_event();
event->mode_type = mode;
APP_EVENT_SUBMIT(event);
}
static inline mode_type_t mode_event_get_mode(const struct mode_event *event)
{
return event->mode_type;
}
static inline bool mode_event_is_usb(const struct mode_event *event)
{
return event->mode_type == MODE_TYPE_USB;
}
static inline bool mode_event_is_ble(const struct mode_event *event)
{
return event->mode_type == MODE_TYPE_BLE;
}
static inline bool mode_event_is_2g4(const struct mode_event *event)
{
return event->mode_type == MODE_TYPE_2G4;
}
#endif

View File

@@ -0,0 +1,26 @@
#include "qdec_step_event.h"
static void log_qdec_step_event(const struct app_event_header *aeh)
{
const struct qdec_step_event *event = cast_qdec_step_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "step=%d", event->step);
}
static void profile_qdec_step_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct qdec_step_event *event = cast_qdec_step_event(aeh);
nrf_profiler_log_encode_int8(buf, event->step);
}
APP_EVENT_INFO_DEFINE(qdec_step_event,
ENCODE(NRF_PROFILER_ARG_S8),
ENCODE("step"),
profile_qdec_step_event);
APP_EVENT_TYPE_DEFINE(qdec_step_event,
log_qdec_step_event,
&qdec_step_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,36 @@
#ifndef QDEC_STEP_EVENT_H__
#define QDEC_STEP_EVENT_H__
#include <stdint.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
struct qdec_step_event {
struct app_event_header header;
int8_t step;
};
APP_EVENT_TYPE_DECLARE(qdec_step_event);
static inline void qdec_step_event_submit(int8_t step)
{
struct qdec_step_event *event;
__ASSERT((step == 1) || (step == -1), "qdec step event must be +/-1");
if (step == 0) {
return;
}
event = new_qdec_step_event();
event->step = step;
APP_EVENT_SUBMIT(event);
}
static inline int8_t qdec_step_event_get_step(const struct qdec_step_event *event)
{
return event->step;
}
#endif /* QDEC_STEP_EVENT_H__ */

View File

@@ -0,0 +1,39 @@
#include "time_sync_event.h"
/* 统一输出来源字符串,便于日志快速确认是哪条链路在校时。 */
static const char *time_sync_source_name(enum time_sync_source source)
{
switch (source) {
case TIME_SYNC_SOURCE_NONE:
return "none";
case TIME_SYNC_SOURCE_BLE:
return "ble";
case TIME_SYNC_SOURCE_USB:
return "usb";
case TIME_SYNC_SOURCE_MANUAL:
return "manual";
case TIME_SYNC_SOURCE_HID:
return "hid";
default:
return "unknown";
}
}
/* 事件日志聚焦关键信息:来源、时区和 UTC 毫秒时间戳。 */
static void log_time_sync_event(const struct app_event_header *aeh)
{
const struct time_sync_event *event = cast_time_sync_event(aeh);
const struct time_sync_update *update = time_sync_event_get_update(event);
APP_EVENT_MANAGER_LOG(aeh,
"src=%s tz=%d utc_ms=%llu acc=%u",
time_sync_source_name(update->source),
update->timezone_min,
(unsigned long long)update->utc_ms,
update->accuracy_ms);
}
APP_EVENT_TYPE_DEFINE(time_sync_event,
log_time_sync_event,
NULL,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,35 @@
#ifndef TIME_SYNC_EVENT_H__
#define TIME_SYNC_EVENT_H__
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
#include "time_manager.h"
/*
* time_sync_event 是“同步入口 -> time_manager”的统一事件
* - BLE/USB/本地设置都提交同一类事件;
* - time_manager 是唯一消费者,也是唯一能修改运行时钟状态的模块。
*/
struct time_sync_event {
struct app_event_header header;
struct time_sync_update update;
};
APP_EVENT_TYPE_DECLARE(time_sync_event);
static inline void time_sync_event_submit(const struct time_sync_update *update)
{
struct time_sync_event *event = new_time_sync_event();
event->update = *update;
APP_EVENT_SUBMIT(event);
}
static inline const struct time_sync_update *time_sync_event_get_update(
const struct time_sync_event *event)
{
return &event->update;
}
#endif /* TIME_SYNC_EVENT_H__ */

18
src/main.c Normal file
View File

@@ -0,0 +1,18 @@
#include <app_event_manager.h>
#define MODULE main
#include <caf/events/module_state_event.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE);
int main(void)
{
if (app_event_manager_init()) {
LOG_ERR("Application Event Manager not initialized");
} else {
module_set_state(MODULE_STATE_READY);
}
return 0;
}

Some files were not shown because too many files have changed in this diff Show More