[C] 纯文本查看 复制代码
#include <hk32f030m.h>
#include <hk32f030m_gpio.h>
#include <hk32f030m_rcc.h>
#include <adc/cm_hk030.hpp>
#include <ioxx_button_trigger.hpp>
#include <lipid_filter.hpp>
#include <pin/cm_hk030.hpp>
#include <scheduler_basic.hpp>
#include <scheduler_tick.hpp>
/* ================================== Systick 配置 ======================================= */
namespace scheduler_basic {
// #define _SCHEDULER_TICK_SYS_TICK_CYCLE_1_MS
#ifdef _SCHEDULER_TICK_SYS_TICK_CYCLE_N_MS
static_assert(_SCHEDULER_TICK_SYS_TICK_CYCLE_N_MS > 0);
constexpr uint32_t SYS_TICK_CYCLE_N_MS = _SCHEDULER_TICK_SYS_TICK_CYCLE_N_MS;
#elif defined(_SCHEDULER_TICK_SYS_TICK_CYCLE_1_MS)
constexpr uint32_t SYS_TICK_CYCLE_N_MS = 1; // 中断周期10 毫秒
#elif defined(_SCHEDULER_TICK_SYS_TICK_CYCLE_100_MS)
constexpr uint32_t SYS_TICK_CYCLE_N_MS = 100; // 中断周期100 毫秒
#else
constexpr uint32_t SYS_TICK_CYCLE_N_MS = 10; // 默认中断周期10 毫秒
#endif
// 每毫秒SysTick 计数值,调用setup_systick 更新
extern uint32_t global_variable_ticks_per_milli_second;
// 每微秒SysTick 计数值,调用setup_systick 更新
extern uint32_t global_variable_ticks_per_micro_second;
// 根据指定的中断中期配置SysTick,使能SysTick 中断,用CPU 时钟驱动计数
int setup_systick() {
global_variable_ticks_per_micro_second = SystemCoreClock / 1000'000;
global_variable_ticks_per_milli_second = global_variable_ticks_per_micro_second * 1000;
uint32_t ticks = global_variable_ticks_per_milli_second * SYS_TICK_CYCLE_N_MS;
return SysTick_Config(ticks);
}
extern volatile uint32_t global_variable_ms_counter;
extern volatile uint32_t global_variable_us_counter;
/**
* @brief 在SysTick 中断里调用,更新毫秒计数器及精度补偿变量
*
*/
void systick_interrupt_counter_inc() {
// 毫秒和微秒用两个计数变量
// systick 中断优先级最低,被打断可能造成奇怪的BUG,所以最好是先关掉中断
__disable_irq();
global_variable_ms_counter += SYS_TICK_CYCLE_N_MS;
global_variable_us_counter += SYS_TICK_CYCLE_N_MS * 1000;
__enable_irq();
}
/**
* @brief 获取毫秒时间戳,可在中断函数中使用
*
*/
uint32_t clock_ms() {
/**
* SYS_TICK_CYCLE_N_MS > 1 时,需要用SysTick 当前值VAL 做计算,补全不足SYS_TICK_CYCLE_N_MS 的零头部分。
* 由于SysTick 计数器持续运行,计算中可能出现的问题有:
*
* 1.
*
*/
if constexpr (SYS_TICK_CYCLE_N_MS > 1) {
// SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; // 暂停SysTick
__disable_irq();
uint32_t v0 = SysTick->VAL;
uint32_t ms = global_variable_ms_counter;
uint32_t v = SysTick->VAL;
// uint32_t v1 = SysTick->VAL;
// if(NVIC_GetPendingIRQ(SysTick_IRQn)) {
// ms +=SYS_TICK_CYCLE_N_MS;
// }
__enable_irq();
if (v > v0) {
ms += SYS_TICK_CYCLE_N_MS;
}
// uint32_t v1 = SysTick->VAL;
// SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; // 重开
uint32_t ticks_per_cycle = SysTick->LOAD + 1;
ms += (ticks_per_cycle - v) / global_variable_ticks_per_milli_second;
return ms;
}
else {
return global_variable_ms_counter;
}
}
/**
* @brief 获取微秒时间戳
*
*/
uint32_t clock_us() {
__disable_irq();
uint32_t us = global_variable_us_counter;
uint32_t v = SysTick->VAL;
if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) {
us += SYS_TICK_CYCLE_N_MS * 1000;
}
__enable_irq();
uint32_t ticks_per_cycle = SysTick->LOAD + 1;
us += (ticks_per_cycle - v) / global_variable_ticks_per_micro_second;
return us;
}
/**
* @brief 获取CPU 时钟周期数, 可在中断函数中使用
*
* 最大计时周期等于SysTick 中断周期
*
* @return uint32_t
*/
uint32_t clock_ticks() {
// 把Systick 计数变成从小到大增加的顺序,方便使用
return SysTick->LOAD - SysTick->VAL;
}
uint32_t ticks_to_ms(uint32_t ticks) {
return ticks / global_variable_ticks_per_milli_second;
}
uint32_t ticks_to_us(uint32_t ticks) {
return ticks / global_variable_ticks_per_micro_second;
}
// 以下都是给定时调度器用的时钟源
struct SysTickMsSource {
inline static auto get_time() {
return clock_ms();
}
using TimeType = uint32_t;
};
struct SysTickUsSource {
inline static auto get_time() {
return clock_us();
}
using TimeType = uint32_t;
};
struct SysTickTicksSource {
inline static auto get_time() {
return clock_ticks();
}
using TimeType = uint32_t;
};
} // namespace scheduler_basic
#include <cstdint>
namespace scheduler_basic {
uint32_t global_variable_ticks_per_milli_second;
uint32_t global_variable_ticks_per_micro_second;
volatile uint32_t global_variable_ms_counter;
volatile uint32_t global_variable_us_counter;
} // namespace scheduler_basic
// using TimeSource = scheduler_basic::PerfCounterMsSource;
// using TimeType = TimeSource::TimeType;
using TimeSource = scheduler_basic::SysTickMsSource;
using TimeType = TimeSource::TimeType;
// 初始化定时调度器对象,最大任务数10
namespace sb = scheduler_basic;
sb::DelayCallback3<TimeSource, 10> dcall;
/* ================================== 数码管引脚定义 ======================================= */
constexpr size_t DIG_COUNT = 5;
constexpr size_t SEG_COUNT = 8;
// 数码管对应8 + 5 一共13 个引脚,全都是推挽输出模式,
// 用union 把seg 引脚和dig 引脚的数组连到一个长度13 的数组all_pin 里
// 方便批量设置GPIO 模式
const union {
ioxx::Pin all_pin[SEG_COUNT + DIG_COUNT];
struct {
ioxx::Pin seg[SEG_COUNT];
ioxx::Pin dig[DIG_COUNT];
} pin;
} TUBE = {
.pin = {
// ======== 共阴数码管8 段引脚 ==========
// 用一个数组把八个段引脚装一起方便处理
// 8 段对应一个字节,按顺序,A 是LSB,DP 是MSB
// 所以SEG_A 在数组里是是[0],
.seg = {
ioxx::Pin(GPIOD, 1), // 数码管SEG A 引脚,连接到阳极
ioxx::Pin(GPIOC, 7), // SEG B
ioxx::Pin(GPIOC, 6), // SEG C
ioxx::Pin(GPIOC, 5), // SEG D
ioxx::Pin(GPIOC, 4), // SEG E
ioxx::Pin(GPIOC, 3), // SEG F
ioxx::Pin(GPIOB, 4), // SEG G
ioxx::Pin(GPIOD, 6), // 小数点 DP
},
// ======= 共阴数码管5 位COM 引脚 =============
// 连接到数码管阴极,所以低电平有效
// 同样装在一个数组里,[0] 对应左侧数码管第一位
// DIG4 同时作为按键输入引脚,要设置为开漏输出,且启用内部下拉,以读取按键输入的高电平信号
.dig = {
ioxx::Pin(GPIOA, 3), // DIG3
ioxx::Pin(GPIOD, 4), // DIG4
ioxx::Pin(GPIOA, 0), // DIG0
ioxx::Pin(GPIOA, 1), // DIG1
ioxx::Pin(GPIOA, 2), // DIG2
}}};
constexpr size_t DIG4_KEY_POS = 1; // DIG4 在dig 数组中的位置
/* =============================================== 数码管引脚操作 ============================================ */
/** 初始化所有数码管引脚为输出模式,并将位选引脚设为高电平,关闭数码管显示
*
* 引脚直接驱动数码管需要比较大的电流,驱动速度应该选比较高的等级
*/
void init_tube_pin() {
using namespace ioxx;
PinInit pi;
pi.mode(mode::out).drive(drive::push_pull);
// 设置所有引脚为推挽输出
for (const Pin& p : TUBE.all_pin) {
pi.init(p);
// 拉高所有引脚,默认关闭数码管
p.set();
}
// 单独设置DIG4 为开漏输出
pi.drive(drive::open_drain).pull(pull::down);
pi.init(TUBE.pin.dig[DIG4_KEY_POS]);
}
/** 用一个字节按顺序设置数组内引脚的输出电平,数组[0] 对应LSB
*/
void write_pins_by_byte(const ioxx::Pin* pins, size_t pins_count, uint8_t b) {
for (; pins_count > 0; --pins_count) {
if (0x01 & b) {
pins->set();
}
else {
pins->clr();
}
b = b >> 1;
++pins;
}
}
/**
* @brief 写入段选引脚,SEG A 对应[0]
*
* @param b
*/
void write_seg_by_byte(uint8_t b) {
write_pins_by_byte(TUBE.pin.seg, SEG_COUNT, b);
}
/**
* @brief 写入位选引脚,DIG0 对应[0]
*
* @param b
*/
void write_dig_by_byte(uint8_t b) {
write_pins_by_byte(TUBE.pin.dig, DIG_COUNT, b);
}
void nrst_switch_to_pa0() {
// 切换NRST 为PA0
RCC->APB1ENR |= RCC_APB1ENR_IOMUXEN;
GPIOMUX->NRST_PIN_KEY = 0x5AE1;
GPIOMUX->NRST_PA0_SEL = 1;
}
/* ================================== 按键处理 ======================================= */
// 实现按键消抖和点击、长按逻辑
static ioxx::ButtonTrigger<ioxx::polarity::high> button_trigger;
/* ================================== 数码管显示操作 ======================================= */
// 共阴数码管码表:
// 0 1 2 3 4 5 6 7 8 9
constexpr static uint8_t TUBE_CODE[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};
constexpr uint8_t TUBE_CODE_DP_MASK = 0x80; // 添加小数点的bit MASK
constexpr uint8_t TUBE_CODE_NONE = 0x00; // 无显示
constexpr uint8_t TUBE_CODE_MINUS = 0x40; // 显示中间横杠,或负号
constexpr uint8_t TUBE_CODE_U = 0x3E; // 显示字母U
constexpr uint8_t TUBE_CODE_J = 0x0E; // 显示字母J
constexpr uint8_t TUBE_CODE_P = 0x73;
constexpr uint8_t TUBE_CODE_A = 0x77;
constexpr uint8_t TUBE_CODE_C = 0x39;
constexpr uint8_t TUBE_CODE_L = 0x38;
// 显示缓冲区
// 0, 1 对应左侧两位的数码管;2, 3, 4 对应右侧的三位
static uint8_t display_buffer[DIG_COUNT];
// 数码管单个位的刷新周期,如果周期是20ms,则每个位都以50Hz 频率闪烁
// 20ms 要平分给5 位数码管,所以每个位点亮4ms 然后就熄灭16ms,切换到其他位,所以位的切换周期是4ms,频率250Hz。
// 刷新周期太大会让数码管看起来有明显的闪烁,太小切换太频繁,又会占用过多CPU 时间,50Hz 一般应该没问题
constexpr TimeType DISPLAY_REFRESH_PERIOD = 20;
// 位选的切换周期,5 个位平分整个刷新周期,所以就是刷新周期除以5
constexpr TimeType DIG_SWITCH_PERIOD = DISPLAY_REFRESH_PERIOD / DIG_COUNT;
// 按键扫描周期大于等于7ms
constexpr TimeType KEY_SCAN_PERIOD = 9;
/** 定时刷新数码管的延时任务,同时负责定时读取按键
*
* 所有SEG 引脚经过数码管中的二极管和各个DIG 引脚连接,
* 所以要从DIG4 读取按键输入,必须先将所有SEG 拉低,去除干扰
*
*/
TimeType callback_task_cycle_display() {
static size_t dig_index = 0;
static bool is_time_to_key_scan = true;
write_seg_by_byte(0x00); // 先将SEG 全部拉低
// 切换位选引脚,拉高上一个位选,之后再拉低当前位选
TUBE.pin.dig[dig_index].set();
++dig_index;
if (dig_index >= DIG_COUNT) {
dig_index = 0;
}
// 到时间扫描按键
if (is_time_to_key_scan) {
is_time_to_key_scan = false;
// 添加定时任务,到时间再通知扫描按键
dcall.add_task(
[]() -> TimeType {
is_time_to_key_scan = true;
return 0;
},
KEY_SCAN_PERIOD);
// 直接读取DIG4 的电平,因为此时所有DIG 引脚都是拉高的状态,
// 不必判断当前是哪个DIG 引脚。开漏输出引脚拉高时可以直接读取输入电平
button_trigger.feed(TUBE.pin.dig[DIG4_KEY_POS].get());
}
// 拉低当前位选
TUBE.pin.dig[dig_index].clr();
write_seg_by_byte(display_buffer[dig_index]); // 写入显示值
return DIG_SWITCH_PERIOD;
}
/* ================================== 显示数据操作 ======================================= */
enum class display_mode {
u_j = 0, // 左侧两位数码管显示U 表示电压,右侧三位显示J,表示电流
u_p = 1, // 两位显示电压U,三位显示功率P
j_u = 2, // 反过来,用三位显示电压,两位显示电流
// TODO:
cal = 3, // 校准模式
};
constexpr int DISPLAY_MODE_NUM_MIN = 0;
constexpr int DISPLAY_MODE_NUM_MAX = 3;
display_mode current_display_mode = display_mode::u_j;
/**
* @brief 存储4 个msg 对应的显示值的数组,内层数组顺序和display_buffer 相同
*/
constexpr static uint8_t MSG_DISPLAY_CODE[4][DIG_COUNT] = {
{0X00, TUBE_CODE_U, 0x00, TUBE_CODE_J, 0X00}, // 0: U J
{0X00, TUBE_CODE_U, 0X00, TUBE_CODE_P, 0X00}, // 1: U P
{0X00, TUBE_CODE_J, 0X00, TUBE_CODE_U, 0X00}, // 2: J U
{0X00, 0X00, TUBE_CODE_C, TUBE_CODE_A, TUBE_CODE_L}, // 3: CAL
};
// is_messaging 为true 时,表示数码管正用于显示静态的状态信息,一段时间内禁止更新显示缓冲区
// 测量数据会被丢弃,不显示
static bool is_messaging = false;
/**
* @brief 将选定的msg 送进显示缓冲区,进入messaging 状态,一段时间内不会更新显示数据
*
* 如果在messaging 状态再次发出新的msg,则更新显示,倒计时重置
*
* @param t
*/
void show_mode_message() {
// 数码管显示状态信息的时间,超过时间后,重新开始显示测量数据
constexpr TimeType MSG_DURATION = 1000;
static uint8_t task_index;
auto msg_code = MSG_DISPLAY_CODE[static_cast<int>(current_display_mode)];
for (int i = 0; i < DIG_COUNT; ++i) {
display_buffer[i] = msg_code[i];
}
if (!is_messaging) {
// 如果当前不是messaging 状态,则进入messaging
is_messaging = true;
/** 添加延时回调,定时退出messaging 状态
*/
task_index = dcall.add_task(
[]() -> TimeType {
is_messaging = false;
return 0;
},
MSG_DURATION);
}
else {
// 如果已经是messaging 状态,则重置任务的倒计时,
// 避免重复多次添加任务把调度挤满
dcall.reset_task(task_index, MSG_DURATION);
}
}
// 进入U_P 模式前的状态
static display_mode last_mode_before_u_p;
/**
* @brief 在U_J 和J_U 模式中切换,然后调用show_mode_message
*
* 如果当前模式是U_P 则切换为进入U_P 模式之前的状态
*
*/
void cycle_u_j_mode() {
if (current_display_mode == display_mode::u_p) {
current_display_mode = last_mode_before_u_p;
}
else if (current_display_mode == display_mode::j_u) {
current_display_mode = display_mode::u_j;
}
else {
current_display_mode = display_mode::j_u;
}
show_mode_message();
}
/**
* @brief 切换为U_P 模式
*
*/
void switch_to_u_p_mode() {
if (current_display_mode != display_mode::u_p) {
last_mode_before_u_p = current_display_mode;
current_display_mode = display_mode::u_p;
}
show_mode_message();
}
/**
* @brief 将mA ,mV ,mW数值转换成A ,V 或W 的显示数据,添加小数点
*
* 数值不能是负数,如果数值大于9V 或9A,则结果保留一位小数,否则保留两位小数。
* 功率值有可能超过99W,没有小数部分
* 计算得到三位数字结果后,再转换成数码管显示值,然后依次送进buf 指针指定显示缓冲区位置
*
* @param num
* @param buf
*/
void milli_num_to_3_digit(uint32_t num, uint8_t* buf) {
// 在messaging 状态不更新显示值
if (is_messaging) {
return;
}
uint8_t temp_buf[5] = {0};
uint_fast8_t c;
// 从十位数开始,将num 拆解成5 个显示值
for (int_fast8_t i = 4; i >= 0; --i) {
num /= 10;
c = num % 10;
temp_buf[i] = TUBE_CODE[c];
}
// 添加小数点
temp_buf[2] |= TUBE_CODE_DP_MASK;
// 找出不为0 的第一个数的位置, 带小数点的0 != 0
uint8_t* start_pos = temp_buf;
while (*start_pos == TUBE_CODE[0]) {
++start_pos;
}
for (uint_fast8_t i = 0; i < 3; ++i) {
buf[i] = start_pos[i];
}
}
/**
* @brief 将数值送入显示缓冲区
*
* @param num 单位是mV,最大不超过99V
*/
void show_milli_volt_amp_watt(uint32_t volt, uint32_t amp) {
// 如果是U_J 或U_P 模式,电压在左侧,显示两位数
// 所以milli_num_to_3_digit 从显示缓冲区[0] 开始写入3 个转换值
// 写在[2] 处的多余的1 个数在之后显示J 或P 时覆盖掉
uint32_t left = volt;
if (current_display_mode == display_mode::j_u) {
left = amp;
}
milli_num_to_3_digit(left, display_buffer);
uint32_t right = amp;
if (current_display_mode == display_mode::j_u) {
right = volt;
}
else if (current_display_mode == display_mode::u_p) {
right = volt * amp / 1000;
}
milli_num_to_3_digit(right, &display_buffer[2]);
}
/* ==================================== ADC 输入 ========================================= */
// 电压引脚,连接分压器
const ioxx::Pin V_SENSE_PIN{GPIOD, 2};
constexpr auto V_SENSE_CHANNEL = adxx::channel::ain4;
// 电流引脚,连接运放
const ioxx::Pin I_SENSE_PIN{GPIOD, 3};
constexpr auto I_SENSE_CHANNEL = adxx::channel::ain3;
static adxx::Adc adcc;
void init_adc_pin() {
using namespace ioxx;
PinInit()
.pull(pull::none)
.mode(mode::analog)
.init(I_SENSE_PIN)
.init(V_SENSE_PIN);
}
void init_adc() {
// 不连续模式
adcc.init(adxx::adc_mode::discontinuous);
// 使能ADC 通道
// 反向转换时,次序是: 0 电压检测 1 电流检测
adcc.set_channel({I_SENSE_CHANNEL, V_SENSE_CHANNEL});
// 设置ADC 系数固定为3.3V / 4095
// 用内置参考电压测出来的系数偏小,不如简单的用外部3.3V 电源
adcc.ad_factor_by_full_scale(static_cast<uint32_t>(3.3E6));
}
/* ==================================== 数据处理 ========================================= */
using FilterValueType = uint32_t;
using FilterType = lipid::MovingAverageFilter<FilterValueType, FilterValueType, 16>;
// 电压、电流、参考电压采样值滑动平均滤波器,窗口尺寸 == 16
static FilterType adc_filter[2];
FilterType& v_filter = adc_filter[0];
FilterType& i_filter = adc_filter[1];
// 经过滤波后的ADC 采样原始值
static FilterValueType adc_avg[2] = {0};
FilterValueType& v_avg = adc_avg[0];
FilterValueType& i_avg = adc_avg[1];
// 显示数据更新周期设为300ms,毕竟数字变得太快了眼睛看不过来
// 这期间持续采样并滑动平均滤波
TimeType callback_task_adc_update() {
// 每10 毫秒查询一次ADC 结果,3 个通道一共30 毫秒,足以在300ms 刷新周期内全部采样8 次
constexpr TimeType ADC_UPDATE_PERIOD = 10;
auto ch_index = adcc.completed();
if (ch_index >= 0) {
// 已转换完成,将ADC 值送进滤波器,获取滤波结果
adc_avg[ch_index] = adc_filter[ch_index].feed(adcc.fetch_real());
// 继续启动转换
adcc.start();
}
return ADC_UPDATE_PERIOD;
}
TimeType callback_task_display_data_update() {
constexpr TimeType DISPLAY_DATA_UPDATE_PERIOD = 300;
uint32_t milli_v = v_avg / 1000 * 10;
uint32_t milli_i = i_avg / 1000;
milli_v += 200; // 电压测量值似乎比实际值总是低0.2V(200mV),所以补偿一下
// 电压测量电路有10 倍衰减,导致输入信号比较小,比如从5V 到9V,ADC 输入变化只有0.4V,
// 所以电压测量精度比电流精度可能更低
show_milli_volt_amp_watt(milli_v, milli_i);
return DISPLAY_DATA_UPDATE_PERIOD;
}
/* ============================================ M I A N 👀👀🫵👊============================================ */
/** 初始化代码
*/
void hw_setup() {
// "在使用进入 stop 低功耗模式时,一定要记得将 PWR 时钟打开"
// "没有打开 PWR 时钟,则会功能异常,且不能再次烧录程序"
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
scheduler_basic::setup_systick();
// 启动中断
__enable_irq();
// 配置GPIO
ioxx::enable_gpio();
nrst_switch_to_pa0();
// 数码管引脚初始化
init_tube_pin();
// ADC 初始化
init_adc_pin();
init_adc();
}
void setup() {
hw_setup();
// 启动ADC
adcc.start();
// 添加不会终止的定时任务
// 调度器最大容量10 绰绰有余
dcall.add_task(callback_task_cycle_display, 0); // 数码管刷新任务
dcall.add_task(callback_task_adc_update, 0); // ADC 定时采样、滤波任务
dcall.add_task(callback_task_display_data_update, 0); // 显示数据刷新任务
// 上电后默认先显示消息,指示当前模式
show_mode_message();
// TODO: 存储用户选择的模式
}
// 方便调试器查看时间变量
volatile uint32_t main_ms;
volatile uint32_t ms_diff = main_ms - scheduler_basic::global_variable_ms_counter;
int main(void) {
setup();
// sb::TimeCycle<TimeSource> c;
while (1) {
main_ms = scheduler_basic::clock_ms();
dcall.tick();
// ====== 处理按键 =======
// 单击切换电压、电流显示位置
if (button_trigger.clicked()) {
cycle_u_j_mode();
}
// 长按显示功率
if (button_trigger.long_pressed()) {
switch_to_u_p_mode();
}
}
}
extern "C" {
// 如果中断函数直接放在cpp 文件,由于C++ 编译器会修改函数名,就和中断向量表的命名不匹配了
__attribute__((used)) void SysTick_Handler() {
// user_code_insert_to_systick_handler();
scheduler_basic::systick_interrupt_counter_inc();
}
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(char* file, uint32_t line) {
/* User can add his own implementation to report the file name and line number,
tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* Infinite loop */
while (1) {
}
}
#endif /* USE_FULL_ASSERT */