地磁8面校准完成
This commit is contained in:
133
apps/earphone/xtell_Sensor/sensor/AK8963.c
Normal file
133
apps/earphone/xtell_Sensor/sensor/AK8963.c
Normal file
@ -0,0 +1,133 @@
|
||||
|
||||
#include "AK8963.h"
|
||||
#include "math.h"
|
||||
#include "os/os_api.h"
|
||||
#include "../xtell.h"
|
||||
#include "printf.h"
|
||||
|
||||
// 用于存放从Fuse ROM读取的磁力计灵敏度校准值
|
||||
static float mag_asa_x = 1.0f;
|
||||
static float mag_asa_y = 1.0f;
|
||||
static float mag_asa_z = 1.0f;
|
||||
|
||||
// 磁力计在16-bit分辨率下的转换因子 (单位: uT/LSB)
|
||||
#define MAG_RAW_TO_UT_FACTOR (4912.0f / 32760.0f)
|
||||
|
||||
/**
|
||||
* @brief 初始化MPU9250的磁力计AK8963
|
||||
* @return 0: 成功, 1: MPU9250连接失败, 2: AK8963连接失败
|
||||
*/
|
||||
u8 MPU9250_Mag_Init(void) {
|
||||
|
||||
u8 temp_data[3];
|
||||
|
||||
// --- 检查 MPU9250 连接并复位 ---
|
||||
_gravity_sensor_get_ndata(MPU9250_ADDR_R, MPU9250_WHO_AM_I, temp_data, 1);
|
||||
if (temp_data[0] != 0x71 && temp_data[0] != 0x73) {
|
||||
printf("MPU9250 comm failed, read ID: 0x%X\n", temp_data[0]);
|
||||
return 1;
|
||||
}
|
||||
printf("MPU9250 get id:0x%X\n", temp_data[0]);
|
||||
|
||||
gravity_sensor_command(MPU9250_ADDR_W, MPU9250_PWR_MGMT_1, 0x80); // 软复位
|
||||
os_time_dly(10); // 等待复位完成
|
||||
|
||||
gravity_sensor_command(MPU9250_ADDR_W, MPU9250_PWR_MGMT_1, 0x01); // 退出睡眠,选择时钟源
|
||||
os_time_dly(2);
|
||||
|
||||
// --- 强制复位 I2C Master 模块并开启旁路 ---
|
||||
|
||||
gravity_sensor_command(MPU9250_ADDR_W, MPU9250_USER_CTRL, 0x20);
|
||||
os_time_dly(1);
|
||||
gravity_sensor_command(MPU9250_ADDR_W, MPU9250_USER_CTRL, 0x00);
|
||||
os_time_dly(1);
|
||||
|
||||
gravity_sensor_command(MPU9250_ADDR_W, MPU9250_INT_PIN_CFG, 0x02);
|
||||
os_time_dly(2);
|
||||
|
||||
// --- 再次验证 AK8963 连接 ---
|
||||
_gravity_sensor_get_ndata(AK8963_ADDR_R, AK8963_WIA, temp_data, 1);
|
||||
if (temp_data[0] != 0x48) {
|
||||
printf("AK8963 comm failed after final attempt, read ID: 0x%X\n", temp_data[0]);
|
||||
return 2;
|
||||
}
|
||||
printf("AK8963 get id: 0x%X\n", temp_data[0]);
|
||||
|
||||
// ------------------ 配置 AK8963 ------------------
|
||||
// Power-down模式
|
||||
gravity_sensor_command(AK8963_ADDR_W, AK8963_CNTL1, 0x00);
|
||||
os_time_dly(1);
|
||||
|
||||
// Fuse ROM access模式
|
||||
gravity_sensor_command(AK8963_ADDR_W, AK8963_CNTL1, 0x0F);
|
||||
os_time_dly(1);
|
||||
_gravity_sensor_get_ndata(AK8963_ADDR_R, AK8963_ASAX, temp_data, 3);
|
||||
|
||||
// 计算校准系数
|
||||
mag_asa_x = (float)(temp_data[0] - 128) / 256.0f + 1.0f;
|
||||
mag_asa_y = (float)(temp_data[1] - 128) / 256.0f + 1.0f;
|
||||
mag_asa_z = (float)(temp_data[2] - 128) / 256.0f + 1.0f;
|
||||
|
||||
// 再次进入Power-down模式
|
||||
gravity_sensor_command(AK8963_ADDR_W, AK8963_CNTL1, 0x00);
|
||||
os_time_dly(1);
|
||||
|
||||
// 设置工作模式:16-bit分辨率,100Hz连续测量模式 (0x16)
|
||||
gravity_sensor_command(AK8963_ADDR_W, AK8963_CNTL1, 0x16);
|
||||
os_time_dly(1);
|
||||
|
||||
printf("AK8963 configured successfully.\n");
|
||||
return 0; // 初始化成功
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief 读取磁力计的三轴原始数据
|
||||
* @param mx, my, mz - 用于存放X, Y, Z轴数据的指针 (int16_t类型)
|
||||
* @return 0: 成功, 1: 数据未就绪, 2: 数据溢出
|
||||
*/
|
||||
u8 MPU9250_Read_Mag_Raw(int16_t *mx, int16_t *my, int16_t *mz) {
|
||||
u8 read_buf[7];
|
||||
|
||||
// 检查数据是否准备好 (使用8位读地址)
|
||||
_gravity_sensor_get_ndata(AK8963_ADDR_R, AK8963_ST1, read_buf, 1);
|
||||
if (!(read_buf[0] & 0x01)) {
|
||||
return 1; // 数据未就绪
|
||||
}
|
||||
|
||||
// 连续读取7个字节 (使用8位读地址)
|
||||
_gravity_sensor_get_ndata(AK8963_ADDR_R, AK8963_HXL, read_buf, 7);
|
||||
|
||||
// 检查数据是否溢出
|
||||
if (read_buf[6] & 0x08) {
|
||||
return 2; // 数据溢出
|
||||
}
|
||||
|
||||
// 组合数据
|
||||
*mx = (int16_t)((read_buf[1] << 8) | read_buf[0]);
|
||||
*my = (int16_t)((read_buf[3] << 8) | read_buf[2]);
|
||||
*mz = (int16_t)((read_buf[5] << 8) | read_buf[4]);
|
||||
|
||||
return 0; // 读取成功
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 读取磁力计的三轴数据,并转换为uT(微特斯拉) (此函数内部逻辑不变)
|
||||
* @param mx, my, mz - 用于存放X, Y, Z轴数据的指针 (float类型)
|
||||
* @return 0: 成功, 1: 数据未就绪, 2: 数据溢出
|
||||
*/
|
||||
u8 MPU9250_Read_Mag_uT(float *mx, float *my, float *mz) {
|
||||
int16_t raw_mx, raw_my, raw_mz;
|
||||
|
||||
u8 status = MPU9250_Read_Mag_Raw(&raw_mx, &raw_my, &raw_mz);
|
||||
if (status != 0) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// 应用灵敏度校准,并转换为uT单位
|
||||
*mx = (float)raw_mx * mag_asa_x * MAG_RAW_TO_UT_FACTOR;
|
||||
*my = (float)raw_my * mag_asa_y * MAG_RAW_TO_UT_FACTOR;
|
||||
*mz = (float)raw_mz * mag_asa_z * MAG_RAW_TO_UT_FACTOR;
|
||||
|
||||
return 0;
|
||||
}
|
||||
46
apps/earphone/xtell_Sensor/sensor/AK8963.h
Normal file
46
apps/earphone/xtell_Sensor/sensor/AK8963.h
Normal file
@ -0,0 +1,46 @@
|
||||
// mpu9250_mag.h
|
||||
|
||||
#ifndef __MPU9250_MAG_H
|
||||
#define __MPU9250_MAG_H
|
||||
|
||||
#include "stdint.h" // 假设你有标准整数类型,u8 对应 uint8_t
|
||||
#include "gSensor/gSensor_manage.h"
|
||||
|
||||
//==================================================================================
|
||||
// MPU9250 和 AK8963 的 I2C 地址 (已转换为8位格式)
|
||||
//==================================================================================
|
||||
// MPU9250的7位地址是 0x68(接地)
|
||||
#define MPU9250_ADDR_7BIT 0x69
|
||||
#define MPU9250_ADDR_W (MPU9250_ADDR_7BIT << 1 | 0) // 8位写地址: 0xD0
|
||||
#define MPU9250_ADDR_R (MPU9250_ADDR_7BIT << 1 | 1) // 8位读地址: 0xD1
|
||||
|
||||
// AK8963磁力计的7位地址是 0x0C
|
||||
#define AK8963_ADDR_7BIT 0x0C
|
||||
#define AK8963_ADDR_W (AK8963_ADDR_7BIT << 1 | 0) // 8位写地址: 0x18
|
||||
#define AK8963_ADDR_R (AK8963_ADDR_7BIT << 1 | 1) // 8位读地址: 0x19
|
||||
|
||||
|
||||
//==================================================================================
|
||||
// MPU9250 相关寄存器 (用于开启旁路模式)
|
||||
//==================================================================================
|
||||
#define MPU9250_WHO_AM_I 0x75
|
||||
#define MPU9250_INT_PIN_CFG 0x37
|
||||
#define MPU9250_USER_CTRL 0x6A
|
||||
#define MPU9250_PWR_MGMT_1 0x6B
|
||||
//==================================================================================
|
||||
// AK8963 磁力计相关寄存器
|
||||
//==================================================================================
|
||||
#define AK8963_WIA 0x00
|
||||
#define AK8963_ST1 0x02
|
||||
#define AK8963_HXL 0x03
|
||||
#define AK8963_ST2 0x09
|
||||
#define AK8963_CNTL1 0x0A
|
||||
#define AK8963_ASAX 0x10
|
||||
|
||||
|
||||
u8 MPU9250_Mag_Init(void);
|
||||
u8 MPU9250_Read_Mag_Raw(int16_t *mx, int16_t *my, int16_t *mz);
|
||||
u8 MPU9250_Read_Mag_uT(float *mx, float *my, float *mz);
|
||||
|
||||
|
||||
#endif // __MPU9250_MAG_H
|
||||
@ -17,9 +17,7 @@
|
||||
#define BMP_IIC_READ_ADDRESS (BMP_IIC_WRITE_ADDRESS | 0x01)
|
||||
#endif
|
||||
|
||||
// BMP280 I2C 地址 (SDO/ADO 引脚接地)
|
||||
#define BMP280_I2C_ADDR_LOW (0x76*2)
|
||||
//7位地址:76, 8位地址:EC (接地)
|
||||
|
||||
|
||||
// BMP280 寄存器地址
|
||||
#define BMP280_REG_CALIB_START 0x88
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
/*
|
||||
三轴磁力计 - MMC5603NJ
|
||||
*/
|
||||
|
||||
#include "MMC56.h"
|
||||
#include "math.h"
|
||||
@ -9,45 +6,18 @@
|
||||
#include "gSensor/gSensor_manage.h"
|
||||
#include "printf.h"
|
||||
|
||||
|
||||
#define ENABLE_XLOG 1
|
||||
#ifdef xlog
|
||||
#undef xlog
|
||||
#endif
|
||||
#if ENABLE_XLOG
|
||||
#define xlog(format, ...) printf("[XT:%s] " format, __func__, ##__VA_ARGS__)
|
||||
#else
|
||||
#define xlog(format, ...) ((void)0)
|
||||
#endif
|
||||
|
||||
/*==================================================================================*/
|
||||
/* MMC5603NJ 内部定义 */
|
||||
/*==================================================================================*/
|
||||
|
||||
// 用于跟踪当前是否处于连续测量模式
|
||||
static u8 g_continuous_mode_enabled = 0;
|
||||
static uint8_t g_continuous_mode_enabled = 0;
|
||||
mmc5603nj_cal_data_t cal_data; //校准数据
|
||||
|
||||
/*==================================================================================*/
|
||||
/* 封装的底层I2C读写函数 */
|
||||
/*==================================================================================*/
|
||||
|
||||
/**
|
||||
* @brief 写入单个字节到MMC5603NJ寄存器
|
||||
*/
|
||||
static void mmc5603nj_write_reg(uint8_t reg, uint8_t data) {
|
||||
gravity_sensor_command(MMC_IIC_WRITE_ADDRESS, reg, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 从MMC5603NJ读取多个字节
|
||||
*/
|
||||
static uint32_t mmc5603nj_read_regs(uint8_t reg, uint8_t *buf, uint8_t len) {
|
||||
return _gravity_sensor_get_ndata(MMC_IIC_READ_ADDRESS, reg, buf, len);
|
||||
}
|
||||
|
||||
/*==================================================================================*/
|
||||
/* 外部接口函数实现 */
|
||||
/*==================================================================================*/
|
||||
// 外部接口函数实现
|
||||
|
||||
uint8_t mmc5603nj_get_pid(void) {
|
||||
uint8_t pid = 0;
|
||||
@ -56,69 +26,145 @@ uint8_t mmc5603nj_get_pid(void) {
|
||||
}
|
||||
|
||||
int mmc5603nj_init(void) {
|
||||
// 检查产品ID是否正确
|
||||
if (mmc5603nj_get_pid() != 0x10) {
|
||||
xlog("init error: check id error %d\n", mmc5603nj_get_pid());
|
||||
return -1; // 设备ID不匹配
|
||||
// ID
|
||||
if (mmc5603nj_get_pid() != 0x80) {
|
||||
printf("MMC5603NJ init failed: wrong Product ID (read: 0x%X)\n", mmc5603nj_get_pid());
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 对传感器执行软件复位 (将 INCTRL0 寄存器的 Do_reset 位置1)
|
||||
mmc5603nj_write_reg(MMC_INCTRL0, 0x08);
|
||||
// 软件复位
|
||||
mmc5603nj_write_reg(MMC_INCTRL1, 0x80); // SW_RESET bit
|
||||
os_time_dly(20); // 等待复位完成
|
||||
|
||||
// 设置20位分辨率 (BW[1:0] = 11)
|
||||
// 同时确保所有轴都使能 (X/Y/Z_inhibit = 0)
|
||||
mmc5603nj_write_reg(MMC_INCTRL1, 0x03);
|
||||
os_time_dly(1);
|
||||
|
||||
// 设置内部控制寄存器2
|
||||
// CMM_EN = 1 (使能连续模式功能)
|
||||
// HPOWER = 1 (高功耗模式,更稳定)
|
||||
mmc5603nj_write_reg(MMC_INCTRL2, 0x90); // 0b10010000
|
||||
|
||||
// 设置自动SET/RESET功能
|
||||
// AUTO_SR_EN = 1
|
||||
mmc5603nj_write_reg(MMC_INCTRL0, 0x20); // 0b00100000
|
||||
|
||||
g_continuous_mode_enabled = 0;
|
||||
return 0; // 初始化成功
|
||||
printf("MMC5603NJ initialized successfully.\n");
|
||||
|
||||
mmc5603nj_enable_continuous_mode(0x04);
|
||||
|
||||
|
||||
printf("\n--- Magnetometer Calibration Start ---\n");
|
||||
printf("Slowly rotate the device in all directions (like drawing a 3D '8')...\n");
|
||||
printf("Calibration will last for 20 seconds.\n\n");
|
||||
printf("will start after 5 seconds\n\n");
|
||||
os_time_dly(500);
|
||||
|
||||
// 定义校准时长和采样间隔
|
||||
const uint32_t calibration_duration_ms = 20000; // 20秒
|
||||
const uint32_t sample_interval_ms = 100; // 每100ms采样一次
|
||||
|
||||
// 初始化最大最小值
|
||||
// 使用一个临时变量来读取数据,避免干扰read函数的正常逻辑
|
||||
mmc5603nj_mag_data_t temp_mag_data;
|
||||
// 首次读取以获取初始值
|
||||
mmc5603nj_read_mag_data(&temp_mag_data); // 首次读取不应用校准
|
||||
|
||||
float max_x = temp_mag_data.x;
|
||||
float min_x = temp_mag_data.x;
|
||||
float max_y = temp_mag_data.y;
|
||||
float min_y = temp_mag_data.y;
|
||||
float max_z = temp_mag_data.z;
|
||||
float min_z = temp_mag_data.z;
|
||||
|
||||
uint32_t start_time = os_time_get(); // 假设os_time_get()返回毫秒级时间戳
|
||||
int samples = 0;
|
||||
int over = calibration_duration_ms/sample_interval_ms;
|
||||
|
||||
while (samples <= over) {
|
||||
// 读取原始磁力计数据
|
||||
mmc5603nj_read_mag_data(&temp_mag_data);
|
||||
|
||||
// 更新最大最小值
|
||||
if (temp_mag_data.x > max_x) max_x = temp_mag_data.x;
|
||||
if (temp_mag_data.x < min_x) min_x = temp_mag_data.x;
|
||||
|
||||
if (temp_mag_data.y > max_y) max_y = temp_mag_data.y;
|
||||
if (temp_mag_data.y < min_y) min_y = temp_mag_data.y;
|
||||
|
||||
if (temp_mag_data.z > max_z) max_z = temp_mag_data.z;
|
||||
if (temp_mag_data.z < min_z) min_z = temp_mag_data.z;
|
||||
|
||||
samples++;
|
||||
os_time_dly(sample_interval_ms / 10); // os_time_dly的参数通常是ticks (1 tick = 10ms)
|
||||
}
|
||||
|
||||
// 检查数据范围是否合理,防止传感器未动或故障
|
||||
if ((max_x - min_x < 0.1f) || (max_y - min_y < 0.1f) || (max_z - min_z < 0.1f)) {
|
||||
printf("\n--- Calibration Failed ---\n");
|
||||
printf("Device might not have been rotated enough.\n");
|
||||
printf("X range: %.2f, Y range: %.2f, Z range: %.2f\n", max_x - min_x, max_y - min_y, max_z - min_z);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 计算硬磁偏移 (椭球中心)
|
||||
cal_data.offset_x = (max_x + min_x) / 2.0f;
|
||||
cal_data.offset_y = (max_y + min_y) / 2.0f;
|
||||
cal_data.offset_z = (max_z + min_z) / 2.0f;
|
||||
|
||||
printf("\n--- Calibration Complete ---\n");
|
||||
printf("Collected %d samples.\n", samples);
|
||||
printf("Offsets (Gauss):\n");
|
||||
printf(" X: %.4f\n", cal_data.offset_x);
|
||||
printf(" Y: %.4f\n", cal_data.offset_y);
|
||||
printf(" Z: %.4f\n", cal_data.offset_z);
|
||||
printf("Please save these values and apply them in your code.\n\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mmc5603nj_set_data_rate(uint8_t rate) {
|
||||
mmc5603nj_write_reg(MMC_ODR, rate);
|
||||
}
|
||||
|
||||
void mmc5603nj_enable_continuous_mode(void) {
|
||||
uint8_t reg_val;
|
||||
|
||||
// 启用连续模式需要设置 INCTRL0 和 INCTRL2 寄存器
|
||||
// 1. 设置 INCTRL0 的 Cmm_en 位 (bit 7)
|
||||
mmc5603nj_read_regs(MMC_INCTRL0, ®_val, 1);
|
||||
reg_val |= 0x80;
|
||||
mmc5603nj_write_reg(MMC_INCTRL0, reg_val);
|
||||
|
||||
// 2. 设置 INCTRL2 的 Cmm_freq_en 位 (bit 4)
|
||||
mmc5603nj_read_regs(MMC_INCTRL2, ®_val, 1);
|
||||
reg_val |= 0x10;
|
||||
mmc5603nj_write_reg(MMC_INCTRL2, reg_val);
|
||||
void mmc5603nj_enable_continuous_mode(uint8_t rate) {
|
||||
// 在连续模式下,ODR寄存器必须被设置
|
||||
mmc5603nj_write_reg(MMC_ODR, rate); //要设置频率
|
||||
// mmc5603nj_set_data_rate(0x04);
|
||||
|
||||
// 启用连续模式 (INCTRL2的CMM_EN位已在init中设置)
|
||||
// 只需要设置 INCTRL0 的 CMM_FREQ_EN 位
|
||||
mmc5603nj_write_reg(MMC_INCTRL0, 0xA0); // 0b10100000 (CMM_FREQ_EN=1, AUTO_SR_EN=1)
|
||||
g_continuous_mode_enabled = 1;
|
||||
}
|
||||
|
||||
void mmc5603nj_disable_continuous_mode(void) {
|
||||
uint8_t reg_val;
|
||||
|
||||
// 禁用连续模式只需要清除 INCTRL2 的 Cmm_freq_en 位
|
||||
mmc5603nj_read_regs(MMC_INCTRL2, ®_val, 1);
|
||||
reg_val &= ~0x10; // 清除 bit 4
|
||||
mmc5603nj_write_reg(MMC_INCTRL2, reg_val);
|
||||
|
||||
// 禁用连续模式
|
||||
mmc5603nj_write_reg(MMC_INCTRL0, 0x20); // 恢复到仅使能 AUTO_SR_EN 的状态
|
||||
g_continuous_mode_enabled = 0;
|
||||
}
|
||||
|
||||
float mmc5603nj_get_temperature(void) {
|
||||
uint8_t status = 0;
|
||||
uint8_t temp_raw = 0;
|
||||
uint8_t timeout = 20;
|
||||
|
||||
// 1. 触发一次温度测量 (写入 0x02 到 INCTRL0 寄存器)
|
||||
mmc5603nj_write_reg(MMC_INCTRL0, 0x02);
|
||||
// 触发一次温度测量
|
||||
mmc5603nj_write_reg(MMC_INCTRL0, 0x02); // TAKE_MEAS_T
|
||||
|
||||
// 2. 等待测量完成 (轮询 STATUS1 寄存器的 Meas_T_done 位)
|
||||
// 等待测量完成
|
||||
do {
|
||||
os_time_dly(10); // 等待一下,避免过于频繁的I2C读取
|
||||
os_time_dly(10);
|
||||
mmc5603nj_read_regs(MMC_STATUS1, &status, 1);
|
||||
} while ((status & 0x80) == 0);
|
||||
timeout--;
|
||||
} while ((status & 0x80) == 0 && timeout > 0);
|
||||
|
||||
if (timeout == 0) {
|
||||
printf("Error: Temperature measurement timeout!\n");
|
||||
return -273.15f; // 返回一个绝对零度的错误值
|
||||
}
|
||||
|
||||
// 3. 读取温度原始值
|
||||
mmc5603nj_read_regs(MMC_TOUT, &temp_raw, 1);
|
||||
|
||||
// 4. 根据公式计算实际温度: Temp(°C) = -75 + 0.8 * TOUT
|
||||
return ((float)temp_raw * 0.8f) - 75.0f;
|
||||
}
|
||||
|
||||
@ -126,32 +172,50 @@ void mmc5603nj_read_mag_data(mmc5603nj_mag_data_t *mag_data) {
|
||||
uint8_t buffer[9];
|
||||
|
||||
if (g_continuous_mode_enabled) {
|
||||
// 连续模式下,直接读取数据即可
|
||||
mmc5603nj_read_regs(MMC_XOUT0, buffer, 9);
|
||||
// 连续模式下,只需检查数据是否就绪
|
||||
uint8_t status = 0;
|
||||
mmc5603nj_read_regs(MMC_STATUS1, &status, 1);
|
||||
if ((status & 0x40) == 0) { // Meas_M_done bit
|
||||
// 数据未就绪,可以选择返回或等待,这里我们直接返回旧数据
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 单次测量模式
|
||||
uint8_t status = 0;
|
||||
// 1. 触发一次磁场测量 (写入 0x01 到 INCTRL0 寄存器)
|
||||
mmc5603nj_write_reg(MMC_INCTRL0, 0x01);
|
||||
uint8_t timeout = 20;
|
||||
|
||||
// 2. 等待测量完成 (轮询 STATUS1 寄存器的 Meas_M_done 位)
|
||||
// 触发一次带自动SET/RESET的磁场测量
|
||||
mmc5603nj_write_reg(MMC_INCTRL0, 0x21); // 0b00100001 (TAKE_MEAS_M=1, AUTO_SR_EN=1)
|
||||
|
||||
// 等待测量完成
|
||||
do {
|
||||
os_time_dly(10); // 等待一下
|
||||
os_time_dly(10);
|
||||
mmc5603nj_read_regs(MMC_STATUS1, &status, 1);
|
||||
} while ((status & 0x40) == 0);
|
||||
|
||||
// 3. 读取9个字节的原始数据
|
||||
mmc5603nj_read_regs(MMC_XOUT0, buffer, 9);
|
||||
timeout--;
|
||||
} while ((status & 0x40) == 0 && timeout > 0);
|
||||
|
||||
if (timeout == 0) {
|
||||
printf("Error: Magnetic measurement timeout!\n");
|
||||
mag_data->x = mag_data->y = mag_data->z = 0.0f;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 读取9个字节的原始数据
|
||||
mmc5603nj_read_regs(MMC_XOUT0, buffer, 9);
|
||||
|
||||
// 解析数据 (20位分辨率)
|
||||
// 零点偏置: 2^19 = 524288, 灵敏度: 2^14 = 16384 LSB/Gauss
|
||||
int32_t raw_x = (buffer[0] << 12) | (buffer[1] << 4) | (buffer[6] >> 4);
|
||||
int32_t raw_y = (buffer[2] << 12) | (buffer[3] << 4) | (buffer[7] >> 4);
|
||||
int32_t raw_z = (buffer[4] << 12) | (buffer[5] << 4) | (buffer[8] >> 4);
|
||||
int32_t raw_x = ((uint32_t)buffer[0] << 12) | ((uint32_t)buffer[1] << 4) | ((uint32_t)buffer[6] & 0x0F);
|
||||
int32_t raw_y = ((uint32_t)buffer[2] << 12) | ((uint32_t)buffer[3] << 4) | ((uint32_t)buffer[6] >> 4);
|
||||
int32_t raw_z = ((uint32_t)buffer[4] << 12) | ((uint32_t)buffer[5] << 4) | ((uint32_t)buffer[8] & 0x0F);
|
||||
|
||||
// 应用偏置和灵敏度进行转换
|
||||
mag_data->x = ((float)raw_x - 524288.0f) / 16384.0f;
|
||||
mag_data->y = ((float)raw_y - 524288.0f) / 16384.0f;
|
||||
mag_data->z = ((float)raw_z - 524288.0f) / 16384.0f;
|
||||
|
||||
//减去偏移
|
||||
mag_data->x -= cal_data.offset_x;
|
||||
mag_data->y -= cal_data.offset_y;
|
||||
mag_data->z -= cal_data.offset_z;
|
||||
}
|
||||
@ -43,6 +43,12 @@ typedef struct {
|
||||
float z;
|
||||
} mmc5603nj_mag_data_t;
|
||||
|
||||
// 定义一个结构体来存放磁力计的硬磁偏移校准数据
|
||||
typedef struct {
|
||||
float offset_x;
|
||||
float offset_y;
|
||||
float offset_z;
|
||||
} mmc5603nj_cal_data_t;
|
||||
|
||||
/**
|
||||
* @brief 初始化MMC5603NJ传感器
|
||||
@ -60,7 +66,7 @@ void mmc5603nj_set_data_rate(uint8_t rate);
|
||||
/**
|
||||
* @brief 启用连续测量模式
|
||||
*/
|
||||
void mmc5603nj_enable_continuous_mode(void);
|
||||
void mmc5603nj_enable_continuous_mode(uint8_t rate);
|
||||
|
||||
/**
|
||||
* @brief 禁用连续测量模式
|
||||
|
||||
@ -1182,15 +1182,31 @@ unsigned char get_calibration_state(void){
|
||||
// Kp: 比例增益,决定了加速度计数据校正陀螺仪的权重。值越大,对加速度计的响应越快,但对运动加速度更敏感。
|
||||
// Ki: 积分增益,决定了用于校正陀螺仪静态漂移的权重。
|
||||
// Q_dt: 采样时间间隔(单位:秒),这里是10ms (0.01s),对应100Hz的采样率。
|
||||
#define HAVE_MAG 1
|
||||
#if HAVE_MAG == 0
|
||||
// -- 无地磁 --
|
||||
const float Kp = 2.0f;
|
||||
const float Ki = 0.005f;
|
||||
const float Q_dt = 0.01f;
|
||||
#else
|
||||
// -- 有地磁 --
|
||||
const float Kp = 0.3f;
|
||||
const float Ki = 0.001f;
|
||||
const float Q_dt = 0.01f;
|
||||
#endif
|
||||
|
||||
// --- 状态变量 ---
|
||||
// 四元数 (Quaternion),表示当前的姿态。初始化为 (1, 0, 0, 0),代表初始姿态为水平。
|
||||
static float q0 = 1.0f, q1 = 0.0f, q2 = 0.0f, q3 = 0.0f;
|
||||
// 陀螺仪积分误差项,用于补偿静态漂移
|
||||
static float exInt = 0.0f, eyInt = 0.0f, ezInt = 0.0f;
|
||||
|
||||
// 磁力计校准相关的变量
|
||||
float Error_Mag_f[3] = {0.0f, 0.0f, 0.0f};
|
||||
double Sum_Avg_Mag_f[3] = {0.0, 0.0, 0.0}; // 使用double避免累加过程中的精度损失
|
||||
// 临时存储校准后数据的数组
|
||||
signed short Temp_AccGyro[6] = {0};
|
||||
float Temp_Mag[3] = {0.0f, 0.0f, 0.0f};
|
||||
// =================================================================================================
|
||||
|
||||
|
||||
@ -1207,15 +1223,237 @@ static float exInt = 0.0f, eyInt = 0.0f, ezInt = 0.0f;
|
||||
* @param calibration_en 传入:外部校准使能标志。如果为0,则强制认为已经校准完成。
|
||||
* @param acc_gyro_input 传入和传出:包含6轴原始数据的数组指针,顺序为 [ACC_X, ACC_Y, ACC_Z, GYR_X, GYR_Y, GYR_Z]。该函数会对其进行原地修改,填充为校准后的数据。
|
||||
* @param Angle_output 传出:滤波后的结果,顺序为 [Pitch, Roll, Yaw],单位为度。
|
||||
* @param mag_data_input 传入:指向包含三轴磁力计数据的结构体指针。数据单位应为高斯(Gauss)。已经8面校准过的数据
|
||||
* @param yaw_rst 传入:Yaw轴重置标志。如果为1,则将整个姿态滤波器状态重置。
|
||||
*
|
||||
* @param quaternion_output 传出, 四元数,用于后续重力分量的去除计算
|
||||
* @return
|
||||
* - 0: 正在进行静态校准。
|
||||
* - 1: 姿态角计算成功。
|
||||
* - 2: 校准未完成,无法进行计算。
|
||||
*/
|
||||
unsigned char Q_SL_SC7U22_Angle_Output(unsigned char calibration_en, signed short *acc_gyro_input, float *Angle_output, unsigned char yaw_rst, float *quaternion_output)
|
||||
unsigned char Q_SL_SC7U22_Angle_Output(unsigned char calibration_en, signed short *acc_gyro_input, float *Angle_output, const mmc5603nj_mag_data_t* _mag_data_input, unsigned char yaw_rst, float *quaternion_output)
|
||||
{
|
||||
#if 1 //有地磁置1
|
||||
unsigned char sl_i = 0;
|
||||
// 如果外部强制禁用校准,则将标志位置1
|
||||
if (calibration_en == 0) {
|
||||
SL_SC7U22_Error_Flag = 1;
|
||||
}
|
||||
|
||||
// ====================== 坐标对齐 ======================
|
||||
mmc5603nj_mag_data_t mag_data_input;
|
||||
|
||||
mag_data_input.x = - _mag_data_input->x;
|
||||
mag_data_input.y = - _mag_data_input->y;
|
||||
mag_data_input.z = _mag_data_input->z;
|
||||
// ================================================================
|
||||
|
||||
|
||||
// =================================================================================
|
||||
// 静态校准
|
||||
// ---------------------------------------------------------------------------------
|
||||
if (SL_SC7U22_Error_Flag == 0) {
|
||||
unsigned short acc_gyro_delta[2];
|
||||
acc_gyro_delta[0] = 0;
|
||||
acc_gyro_delta[1] = 0;
|
||||
for (sl_i = 0; sl_i < 3; sl_i++) {
|
||||
acc_gyro_delta[0] += SL_GetAbsShort(acc_gyro_input[sl_i] - Temp_Accgyro[sl_i]);
|
||||
acc_gyro_delta[1] += SL_GetAbsShort(acc_gyro_input[3 + sl_i] - Temp_Accgyro[3 + sl_i]);
|
||||
}
|
||||
for (sl_i = 0; sl_i < 6; sl_i++) {
|
||||
Temp_Accgyro[sl_i] = acc_gyro_input[sl_i];
|
||||
}
|
||||
#if (ACC_RANGE == 2)
|
||||
if ((acc_gyro_delta[0] / 8 < 160) && (acc_gyro_delta[1] < 40) && (SL_GetAbsShort(acc_gyro_input[0]) < 3000) && (SL_GetAbsShort(acc_gyro_input[1]) < 3000) && (SL_GetAbsShort(acc_gyro_input[2] - 16384) < 3000)) {
|
||||
#elif (ACC_RANGE == 4)
|
||||
if ((acc_gyro_delta[0] / 8 < 160) && (acc_gyro_delta[1] < 40) && (SL_GetAbsShort(acc_gyro_input[0]) < 3000) && (SL_GetAbsShort(acc_gyro_input[1]) < 3000) && (SL_GetAbsShort(acc_gyro_input[2] - 8192) < 3000)) {
|
||||
#elif (ACC_RANGE == 8)
|
||||
if ((acc_gyro_delta[0] / 8 < 160) && (acc_gyro_delta[1] < 40) && (SL_GetAbsShort(acc_gyro_input[0]) < 3000) && (SL_GetAbsShort(acc_gyro_input[1]) < 3000) && (SL_GetAbsShort(acc_gyro_input[2] - 4096) < 3000)) {
|
||||
#elif (ACC_RANGE == 16)
|
||||
if ((acc_gyro_delta[0] / 8 < 160) && (acc_gyro_delta[1] < 40) && (SL_GetAbsShort(acc_gyro_input[0]) < 3000) && (SL_GetAbsShort(acc_gyro_input[1]) < 3000) && (SL_GetAbsShort(acc_gyro_input[2] - 2048) < 3000)) {
|
||||
#endif
|
||||
if (SL_SC7U22_Error_cnt < 200) SL_SC7U22_Error_cnt++;
|
||||
} else {
|
||||
SL_SC7U22_Error_cnt = 0;
|
||||
}
|
||||
if (SL_SC7U22_Error_cnt > 190) {
|
||||
//累加6轴数据
|
||||
for (sl_i = 0; sl_i < 6; sl_i++) Sum_Avg_Accgyro[sl_i] += acc_gyro_input[sl_i];
|
||||
|
||||
Sum_Avg_Mag_f[0] += mag_data_input.x;
|
||||
Sum_Avg_Mag_f[1] += mag_data_input.y;
|
||||
Sum_Avg_Mag_f[2] += mag_data_input.z;
|
||||
|
||||
SL_SC7U22_Error_cnt2++;
|
||||
if (SL_SC7U22_Error_cnt2 > 49) {
|
||||
SL_SC7U22_Error_Flag = 1;
|
||||
SL_SC7U22_Error_cnt2 = 0;
|
||||
SL_SC7U22_Error_cnt = 0;
|
||||
//6轴偏置
|
||||
for (sl_i = 0; sl_i < 6; sl_i++) Sum_Avg_Accgyro[sl_i] = Sum_Avg_Accgyro[sl_i] / 50;
|
||||
Error_Accgyro[0] = 0 - Sum_Avg_Accgyro[0];
|
||||
Error_Accgyro[1] = 0 - Sum_Avg_Accgyro[1];
|
||||
#if ACC_RANGE==2
|
||||
Error_Accgyro[2] = 16384 - Sum_Avg_Accgyro[2];
|
||||
#elif ACC_RANGE==4
|
||||
Error_Accgyro[2] = 8192 - Sum_Avg_Accgyro[2];
|
||||
#elif ACC_RANGE==8
|
||||
Error_Accgyro[2] = 4096 - Sum_Avg_Accgyro[2];
|
||||
#elif ACC_RANGE==16
|
||||
Error_Accgyro[2] = 2048 - Sum_Avg_Accgyro[2];
|
||||
#endif
|
||||
Error_Accgyro[3] = 0 - Sum_Avg_Accgyro[3];
|
||||
Error_Accgyro[4] = 0 - Sum_Avg_Accgyro[4];
|
||||
Error_Accgyro[5] = 0 - Sum_Avg_Accgyro[5];
|
||||
// //磁力计偏置 -- 不在这弄,在磁力计初始化的时候开始
|
||||
// Sum_Avg_Mag_f[0] /= 50.0;
|
||||
// Sum_Avg_Mag_f[1] /= 50.0;
|
||||
// Sum_Avg_Mag_f[2] /= 50.0;
|
||||
// Error_Mag_f[0] = 0.0f - (float)Sum_Avg_Mag_f[0];
|
||||
// Error_Mag_f[1] = 0.0f - (float)Sum_Avg_Mag_f[1];
|
||||
// Error_Mag_f[2] = 0.0f - (float)Sum_Avg_Mag_f[2];
|
||||
// xlog("AVG_Recode AX:%d,AY:%d,AZ:%d,GX:%d,GY:%d,GZ:%d\r\n", Sum_Avg_Accgyro[0], Sum_Avg_Accgyro[1], Sum_Avg_Accgyro[2], Sum_Avg_Accgyro[3], Sum_Avg_Accgyro[4], Sum_Avg_Accgyro[5]);
|
||||
// xlog("Error_Recode AX:%d,AY:%d,AZ:%d,GX:%d,GY:%d,GZ:%d\r\n", Error_Accgyro[0], Error_Accgyro[1], Error_Accgyro[2], Error_Accgyro[3], Error_Accgyro[4], Error_Accgyro[5]);
|
||||
}
|
||||
} else {
|
||||
SL_SC7U22_Error_cnt2 = 0;
|
||||
for (sl_i = 0; sl_i < 6; sl_i++) Sum_Avg_Accgyro[sl_i] = 0;
|
||||
// Sum_Avg_Mag_f[0] = 0.0;
|
||||
// Sum_Avg_Mag_f[1] = 0.0;
|
||||
// Sum_Avg_Mag_f[2] = 0.0;
|
||||
}
|
||||
return 0; // 返回0,表示正在校准
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// 姿态解算 (Mahony AHRS)
|
||||
// ---------------------------------------------------------------------------------
|
||||
if (SL_SC7U22_Error_Flag == 1) { // 确认已经校准完成
|
||||
|
||||
// --- Yaw轴/姿态重置 ---
|
||||
// 注意:重置yaw会重置整个姿态滤波器,使设备回到初始水平姿态
|
||||
if (yaw_rst == 1) {
|
||||
q0 = 1.0f; q1 = 0.0f; q2 = 0.0f; q3 = 0.0f;
|
||||
exInt = 0.0f; eyInt = 0.0f; ezInt = 0.0f;
|
||||
}
|
||||
|
||||
// --- 数据预处理 ---
|
||||
// 应用零点偏移补偿
|
||||
for (sl_i = 0; sl_i < 6; sl_i++) {
|
||||
Temp_Accgyro[sl_i] = acc_gyro_input[sl_i] + Error_Accgyro[sl_i];
|
||||
}
|
||||
|
||||
// Temp_Mag[0] = mag_data_input.x + Error_Mag_f[0];
|
||||
// Temp_Mag[1] = mag_data_input.y + Error_Mag_f[1];
|
||||
// Temp_Mag[2] = mag_data_input.z + Error_Mag_f[2];
|
||||
Temp_Mag[0] = mag_data_input.x;
|
||||
Temp_Mag[1] = mag_data_input.y;
|
||||
Temp_Mag[2] = mag_data_input.z;
|
||||
|
||||
// 将校准后的数据写回输入数组
|
||||
#if 1
|
||||
for (sl_i = 0; sl_i < 6; sl_i++) {
|
||||
acc_gyro_input[sl_i] = Temp_Accgyro[sl_i];
|
||||
}
|
||||
#endif
|
||||
|
||||
// 获取校准后的数据
|
||||
float ax = (float)Temp_Accgyro[0];
|
||||
float ay = (float)Temp_Accgyro[1];
|
||||
float az = (float)Temp_Accgyro[2];
|
||||
// 将陀螺仪数据从 LSB 转换为弧度/秒 (rad/s)
|
||||
// 转换系数 0.061 ≈ 2000dps / 32768 LSB; PI/180 ≈ 0.01745
|
||||
float gx = (float)Temp_Accgyro[3] * 0.061f * 0.0174533f; // Roll rate
|
||||
float gy = (float)Temp_Accgyro[4] * 0.061f * 0.0174533f; // Pitch rate
|
||||
float gz = (float)Temp_Accgyro[5] * 0.061f * 0.0174533f; // Yaw rate
|
||||
float mx = Temp_Mag[0];
|
||||
float my = Temp_Mag[1];
|
||||
float mz = Temp_Mag[2];
|
||||
|
||||
// --- Mahony算法核心 ---
|
||||
float norm;
|
||||
float q0q0 = q0 * q0;
|
||||
float q0q1 = q0 * q1;
|
||||
float q0q2 = q0 * q2;
|
||||
float q0q3 = q0 * q3;
|
||||
float q1q1 = q1 * q1;
|
||||
float q1q2 = q1 * q2;
|
||||
float q1q3 = q1 * q3;
|
||||
float q2q2 = q2 * q2;
|
||||
float q2q3 = q2 * q3;
|
||||
float q3q3 = q3 * q3;
|
||||
float hx, hy, bx, bz;
|
||||
float vx, vy, vz, wx, wy, wz;
|
||||
float ex, ey, ez;
|
||||
|
||||
// 归一化加速度计测量值,得到单位重力向量
|
||||
norm = sqrtf(ax * ax + ay * ay + az * az);
|
||||
if (norm > 0.0f) { ax /= norm; ay /= norm; az /= norm; }
|
||||
else { return 1; }
|
||||
|
||||
norm = sqrtf(mx * mx + my * my + mz * mz);
|
||||
if (norm > 0.0f) { mx /= norm; my /= norm; mz /= norm; }
|
||||
|
||||
// 根据当前姿态(四元数)估计重力向量的方向
|
||||
vx = 2.0f * (q1q3 - q0q2);
|
||||
vy = 2.0f * (q0q1 + q2q3);
|
||||
vz = q0q0 - q1q1 - q2q2 + q3q3;
|
||||
|
||||
// 计算磁场误差 (倾斜补偿)
|
||||
hx = 2.0f * mx * (0.5f - q2q2 - q3q3) + 2.0f * my * (q1q2 - q0q3) + 2.0f * mz * (q1q3 + q0q2);
|
||||
hy = 2.0f * mx * (q1q2 + q0q3) + 2.0f * my * (0.5f - q1q1 - q3q3) + 2.0f * mz * (q2q3 - q0q1);
|
||||
bx = sqrtf(hx * hx + hy * hy);
|
||||
bz = 2.0f * mx * (q1q3 - q0q2) + 2.0f * my * (q2q3 + q0q1) + 2.0f * mz * (0.5f - q1q1 - q2q2);
|
||||
wx = 2.0f * bx * (0.5f - q2q2 - q3q3) + 2.0f * bz * (q1q3 - q0q2);
|
||||
wy = 2.0f * bx * (q1q2 - q0q3) + 2.0f * bz * (q0q1 + q2q3);
|
||||
wz = 2.0f * bx * (q1q3 + q0q2) + 2.0f * bz * (0.5f - q1q1 - q2q2);
|
||||
|
||||
// 合并重力和磁场误差
|
||||
ex = (ay * vz - az * vy) + (my * wz - mz * wy);
|
||||
ey = (az * vx - ax * vz) + (mz * wx - mx * wz);
|
||||
ez = (ax * vy - ay * vx) + (mx * wy - my * wx);
|
||||
|
||||
// PI控制器
|
||||
if (Ki > 0.0f) {
|
||||
exInt += ex * Ki * Q_dt;
|
||||
eyInt += ey * Ki * Q_dt;
|
||||
ezInt += ez * Ki * Q_dt;
|
||||
}
|
||||
gx += Kp * ex + exInt;
|
||||
gy += Kp * ey + eyInt;
|
||||
gz += Kp * ez + ezInt;
|
||||
|
||||
// 使用校正后的角速度更新四元数 (一阶毕卡法)
|
||||
q0 += (-q1 * gx - q2 * gy - q3 * gz) * 0.5f * Q_dt;
|
||||
q1 += ( q0 * gx + q2 * gz - q3 * gy) * 0.5f * Q_dt;
|
||||
q2 += ( q0 * gy - q1 * gz + q3 * gx) * 0.5f * Q_dt;
|
||||
q3 += ( q0 * gz + q1 * gy - q2 * gx) * 0.5f * Q_dt;
|
||||
|
||||
// 归一化四元数,保持其单位长度
|
||||
norm = sqrtf(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3);
|
||||
q0 /= norm; q1 /= norm; q2 /= norm; q3 /= norm;
|
||||
|
||||
// --- 将四元数转换为欧拉角 (Pitch, Roll, Yaw) ---
|
||||
// Pitch (绕Y轴旋转)
|
||||
Angle_output[0] = asinf(-2.0f * (q1 * q3 - q0 * q2)) * 57.29578f;
|
||||
// Roll (绕X轴旋转)
|
||||
Angle_output[1] = atan2f(2.0f * (q0 * q1 + q2 * q3), q0q0 - q1q1 - q2q2 + q3q3) * 57.29578f;
|
||||
// Yaw (绕Z轴旋转)
|
||||
Angle_output[2] = atan2f(2.0f * (q1 * q2 + q0 * q3), q0q0 + q1q1 - q2q2 - q3q3) * 57.29578f;
|
||||
|
||||
//将四元数传出去
|
||||
if (quaternion_output != NULL) {
|
||||
quaternion_output[0] = q0; // w
|
||||
quaternion_output[1] = q1; // x
|
||||
quaternion_output[2] = q2; // y
|
||||
quaternion_output[3] = q3; // z
|
||||
}
|
||||
return 1; // 返回1,表示计算成功
|
||||
}
|
||||
|
||||
return 2; // 校准未完成,返回错误状态
|
||||
|
||||
#else
|
||||
unsigned char sl_i = 0;
|
||||
|
||||
// 如果外部强制禁用校准,则将标志位置1
|
||||
@ -1248,6 +1486,7 @@ unsigned char Q_SL_SC7U22_Angle_Output(unsigned char calibration_en, signed shor
|
||||
#endif
|
||||
if (SL_SC7U22_Error_cnt < 200) SL_SC7U22_Error_cnt++;
|
||||
} else {
|
||||
// printf("error: The calibration process has undergone a shift.\n");
|
||||
SL_SC7U22_Error_cnt = 0;
|
||||
}
|
||||
if (SL_SC7U22_Error_cnt > 190) {
|
||||
@ -1388,6 +1627,7 @@ unsigned char Q_SL_SC7U22_Angle_Output(unsigned char calibration_en, signed shor
|
||||
}
|
||||
|
||||
return 2; // 校准未完成,返回错误状态
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@ -9,6 +9,7 @@ Copyright (c) 2022 Silan MEMS. All Rights Reserved.
|
||||
|
||||
#include "gSensor/gSensor_manage.h"
|
||||
#include "printf.h"
|
||||
#include "MMC56.h"
|
||||
|
||||
//是否使能串口打印调试
|
||||
#define SL_Sensor_Algo_Release_Enable 0x00
|
||||
@ -132,7 +133,7 @@ unsigned char SL_SC7U22_Angle_Output(unsigned char calibration_en,signed short *
|
||||
|
||||
unsigned char Original_SL_SC7U22_Angle_Output(unsigned char calibration_en, signed short *acc_gyro_input, float *Angle_output, unsigned char yaw_rst);
|
||||
unsigned char SIX_SL_SC7U22_Angle_Output(unsigned char auto_calib_start, signed short *acc_gyro_input, float *Angle_output, unsigned char yaw_rst);
|
||||
unsigned char Q_SL_SC7U22_Angle_Output(unsigned char calibration_en, signed short *acc_gyro_input, float *Angle_output, unsigned char yaw_rst, float *quaternion_output);
|
||||
unsigned char Q_SL_SC7U22_Angle_Output(unsigned char calibration_en, signed short *acc_gyro_input, float *Angle_output, const mmc5603nj_mag_data_t *mag_data_input, unsigned char yaw_rst, float *quaternion_output);
|
||||
unsigned char get_calibration_state(void);
|
||||
/**寄存器宏定义*******************************/
|
||||
#define SC7U22_WHO_AM_I 0x01
|
||||
|
||||
Reference in New Issue
Block a user