KT6368A

KT6368A 芯片是一款支持蓝牙双模的纯数据芯片,蓝牙5.1版本。芯片的亮点在超小尺寸,超级价格。以及简单明了的透传和串口AT控制功能。大大降低了嵌入蓝牙在其它产品的开发难度和成本。

同时支持 SPP 和 BLE 。但是只能任选其中一个协议使用。

备注:这款芯片最大的特点,就是便宜,使用简单,生产简单。无其他,便宜才是王道。

请注意,一旦蓝牙被连接之后,芯片自动进入透传模式。不再识别AT指令。所以AT指令只能用于,未连接状态下面使用 。

引脚说明

yjsm

引脚编号 名称 I/O 类型 功能 其他功能
1 VBAT P LDO 电源 -
1 VDDIO P IO 电源 3.3V -
2 PA9 I/O GPIO(上拉) 长按复位;
ADC8:ADC 通道 8;
3 VSS P GND(地) -
4 BT_RF - 射频天线 -
5 BTOSCI I BTOSCI -
6 BTOSCO O BTOSCO -
7 USB0DM I/O GPIO(下拉) IIC_SDA_A:IIC 数据线 (A);
ADC11:ADC 通道 11;
UART1_RXD:UART1 数据输入(D);
8 USB0DP I/O GPIO(下拉) IIC_SCL_A:IIC 时钟线 (A);
ADC10:ADC 通道 10;
UART1_TXD:UART1 数据输出(D);

串口命令

KT6368A 使用 AT 串口指令进行设置,具体通讯命令请查看用户手册;

上位机实现

假设我们要通过上位机发送一个 .bin 文件到 MCU

数据发送过程中需要解决的问题

  • 数据完整性:防止数据丢失或乱序。

  • 数据准确性:确保接收的数据与发送的数据一致,避免传输过程中数据损坏。

  • 传输控制:支持数据分片、确认和重传机制,避免传输中断导致的失败。

  • 文件校验:在文件传输完成后,使用校验(如 CRC 或 MD5)确保文件内容无误。

解决方案

由于 .bin 文件通常比较大,蓝牙传输(尤其是通过 UART 的模块)无法一次发送整个文件,因此需要分片传输。具体步骤如下:

1.文件分片

  • 将 .bin 文件按照固定大小(如 512 字节/1024 字节)分割为多个数据包。
  • 每个数据包包含以下信息:
    1. 包头:标识数据包起始,如固定值 0xFFAA。
    2. 包编号:用于标识每个数据包,防止乱序。
    3. 数据长度:本包中数据部分的字节数。
    4. 数据内容:文件的实际数据片段。
    5. 校验值:如 CRC16 或简单的和校验,用于确保本包内容的完整性。
    6. 包尾:标识数据包结束,如固定值 0xFFBB。

2.确认与重传机制

  • 每发送一个包,上位机等待 MCU 的确认(ACK)。
  • 如果接收到 ACK,则发送下一个数据包。
  • 如果超时未收到 ACK,或者收到 NACK,则重发当前数据包。

3.文件完整性校验

  • 文件发送完成后,发送整个文件的校验值(如 CRC32、MD5),让 MCU 对接收到的文件进行校验,确保完整性。
  • 如果校验失败,则重新发送。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# Python 上位机示例程序
import time
import struct
from bleak import BleakClient

# 配置蓝牙设备
DEVICE_ADDRESS = "XX:XX:XX:XX:XX:XX" # 替换为 KT6368 的蓝牙 MAC 地址
SERVICE_UUID = "your-service-uuid" # 替换为蓝牙服务 UUID
CHAR_UUID = "your-char-uuid" # 替换为特征值 UUID

# 分片大小(根据蓝牙模块和 UART 的实际情况调整)
CHUNK_SIZE = 512

# 计算 CRC16(用于数据校验)
def calc_crc16(data: bytes) -> int:
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
return crc & 0xFFFF

# 分片传输文件
async def send_file(file_path):
async with BleakClient(DEVICE_ADDRESS) as client:
print("Connected to Bluetooth device")

# 打开 .bin 文件
with open(file_path, "rb") as f:
file_data = f.read()

total_size = len(file_data)
print(f"File size: {total_size} bytes")

# 按分片大小分割文件
num_chunks = (total_size + CHUNK_SIZE - 1) // CHUNK_SIZE
for i in range(num_chunks):
# 获取当前分片数据
start = i * CHUNK_SIZE
end = min(start + CHUNK_SIZE, total_size)
chunk = file_data[start:end]

# 构造数据包
chunk_len = len(chunk)
crc = calc_crc16(chunk) # 计算分片的 CRC 校验
packet = struct.pack("<HHH", 0x55AA, i, chunk_len) + chunk + struct.pack("<H", crc) + struct.pack("<H", 0xFFBB)

# 发送数据包
await client.write_gatt_char(CHAR_UUID, packet)
print(f"Sent chunk {i + 1}/{num_chunks}")

# 等待 ACK 确认
ack = await client.read_gatt_char(CHAR_UUID)
if ack != b"ACK":
print(f"Error: No ACK for chunk {i + 1}. Retrying...")
i -= 1 # 重发当前分片

# 文件发送完成后,发送文件总 CRC32
file_crc = calc_crc16(file_data)
packet = struct.pack("<I", file_crc) + struct.pack("<H", 0xFFBB)
await client.write_gatt_char(CHAR_UUID, packet)
print("File transfer complete. Waiting for MCU confirmation...")

# 接收 MCU 响应
response = await client.read_gatt_char(CHAR_UUID)
if response == b"SUCCESS":
print("File transfer successful!")
else:
print("File transfer failed. MCU reported an error.")

MCU 实现

蓝牙模块的 GATT 服务接收这些包后,通过 UART 将每个数据包发送给 MCU。

MCU 的 UART 接口逐字节接收数据,并根据协议(如分片协议、CRC 校验等)处理。

MCU 对每个接收到的数据包进行校验,确认无误后返回 ACK(通过 UART 发送到蓝牙模块)。

蓝牙模块再通过 GATT 将 ACK 发送给上位机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include "stm32f1xx_hal.h"
#include <string.h>
#include <stdbool.h>

#define UART_BUFFER_SIZE 1024
#define FLASH_START_ADDRESS 0x08020000 // 固件存储地址(具体地址视情况而定)

uint8_t uart_rx_buffer[UART_BUFFER_SIZE]; // UART 接收缓冲区
uint8_t file_buffer[UART_BUFFER_SIZE]; // 文件缓冲区
uint32_t received_crc = 0; // 接收到的 CRC
uint32_t file_size = 0; // 接收到的文件大小
uint32_t current_address = FLASH_START_ADDRESS;

// CRC16 校验函数
uint16_t calc_crc16(uint8_t *data, uint32_t length) {
uint16_t crc = 0xFFFF;
for (uint32_t i = 0; i < length; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xA001;
else
crc >>= 1;
}
}
return crc;
}

// UART 接收中断回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 解析分片数据包
uint16_t packet_header = (uart_rx_buffer[1] << 8) | uart_rx_buffer[0];
uint16_t chunk_index = (uart_rx_buffer[3] << 8) | uart_rx_buffer[2];
uint16_t chunk_len = (uart_rx_buffer[5] << 8) | uart_rx_buffer[4];
uint16_t received_crc = (uart_rx_buffer[chunk_len + 6] << 8) | uart_rx_buffer[chunk_len + 7];
uint16_t packet_tail = (uart_rx_buffer[chunk_len + 8] << 8) | uart_rx_buffer[chunk_len + 9];

if (packet_header == 0x55AA && packet_tail == 0xFFBB) {
// 校验 CRC
uint16_t calculated_crc = calc_crc16(&uart_rx_buffer[6], chunk_len);
if (calculated_crc == received_crc) {
// 存储数据到文件缓冲区或直接写入 Flash
memcpy(file_buffer, &uart_rx_buffer[6], chunk_len);
HAL_UART_Transmit(&huart1, (uint8_t *)"ACK", 3, 100); // 发送 ACK
} else {
HAL_UART_Transmit(&huart1, (uint8_t *)"NACK", 4, 100); // 发送 NACK
}
} else {
// 包头或包尾错误,发送 NACK
HAL_UART_Transmit(&huart1, (uint8_t *)"NACK", 4, 100);
}

// 准备接收下一个数据包
HAL_UART_Receive_IT(&huart1, uart_rx_buffer, UART_BUFFER_SIZE);
}
}

示例电路

sldl

示例代码

https://github.com/TooUpper/SensorDrive