设计FRC电源控制程序,能够读取和输出PWM信号,运行PID控制程序并对Fire信号进行计数。
STM32(BluePill)平台,其中以下端口支持PWM:PA0-PA3,PA6,PA7,PA8-PA12,PB0,PB1,PB6-PB8,PB12合计17路PWM输出。基于Arduino框架开发。
##环境配置
应用PlatformIO进行开发,platformio.ini配置如下
[env:bluepill_f103c8]
upload_flags = -c set CPUTAPID 0x2ba01477 ;寨板
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload-upload_protocol = stlink
若使用usb进行程序烧录和串口调试
首先在https://github.com/rogerclarkmelbourne/STM32duino-bootloader下载/binaries/generic_boot20_pc13.bin。
在macOS或Linux下载stlink工具,将开发板通过ST-Link编程器连接到计算机,运行下列命令
$ st-flash write generic_boot20_pc13.bin 0x8000000
执行完成后运行
$ ls /dev/tty.*
/dev/tty.usbmodemXXXXXX
之后即可使用usb进行程序烧录和串口调试。
platformio.ini配置如下:
[env:bluepill_f103c8]
platform = ststm32
board = bluepill_f103c8
framework = arduino
board_build.core = maple
upload_protocol = dfu
应用Arduino和ArduSensorPlatform框架提供的API,可通过调用相关API读取PWM信号和进行PWM输出,同时能够获取设置的PID控制系统参数,并完成相应控制程序的运行。
根据指定的参数,输出一定占空比的PWM信号。继承自SensorBase
成员变量
uint32_t pin
PWM输出引脚
uint32_t frequency
输出的PWM信号的频率,单位为Hz
float outputDutyRatio
设定的输出占空比
成员方法
PWMioPort(uint32_t pin, uint32_t frequency)
构造函数,设定IO引脚和PWM频率。对象创建时即通过
analogWriteFrequency函数设置PWM信号频率并在设定的引脚进行信号输出
~PWMioPort()
析构函数
void Output()
根据
outputDutyRatio和frequency,使用analogWrite()输出相应占空比的PWM波。
void Update()
不操作
Write()
读取给定的占空比,写入
outputDutyRatio。
读取输入信号,计算信号的占空比。继承自SensorBase
成员变量
uint32_t pin
PWM输入引脚
uint32_t frequency
输入的PWM信号的频率,单位为Hz
float sample
采样值,即占空比(0~1)。
成员方法
PWMinPort(uint32_t pin, uint32_t frequency)
构造函数,设定IO引脚、PWM频率。同时设置中断函数负责检测输入PWM信号占空比。
由于pulseIn函数运行时循环调用digitalRead()效率过低,因此占空比检测通过中断实现。一种方法是上升沿和下降沿触发函数分别调用micros()函数记录当前时间,作差得高电平持续时间从而计算占空比。micros()函数返回当前程序运行的时间,单位为微秒us。示例代码如下:
#include <Arduino.h>
#define PIN PA0
int pwm_value = 0; // us
int prev_time = 0;
void setup() {
attachInterrupt(PIN, rising, RISING);
}
void rising() {
attachInterrupt(PIN, falling, FALLING);
/* record rising edge time */
prev_time = micros();
}
void falling() {
attachInterrupt(PIN, rising, RISING);
/* record falling edge time to calculate the high level duration */
pwm_value = micros() - prev_time;
}
void loop() {
}
本例需要对上述方法进行修改,因为使用attachInterrupt函数注册中断,中断处理函数如果是类的成员函数必须为static,static函数无法访问非static成员变量。因此,将中断mode设置为CHANGE,设置下列变量和数组:
static int curr_num;
static int pin_list[4];
static int prev_state[4];
static int high_edge_time[4];
static int high_level_duration[4];
中断处理函数的具体实现如下:
void PWMinPort::ISR()
{
for (int i = 0; i < 4; i++)
{
if (pin_list[i] < 0)
{
break;
}
if (prev_state[i] == digitalRead(pin_list[i]))
{
continue;
}
if (prev_state[i] == HIGH)
{
high_level_duration[i] = micros() - high_edge_time[i];
prev_state[i] = LOW;
}
else {
high_edge_time[i] = micros();
prev_state[i] = HIGH;
}
break;
}
}
curr_num初始值为0,每次执行PWMinPort构造函数时记录pin_list[curr_num]为当前引脚号并将curr_num加1。prev_state记录上一时刻的状态,进入中断函数时通过轮询方式应用digitalRead()判断每个针脚当前状态与对应的prev_state是否一致,如果不一致判断当前是上升沿还是下降沿。如果是上升沿则通过micros()记录当前时刻存入high_edge_time,下降沿则调用micros()与high_edge_time做差得到高电平时间存入high_level_duration。
需要优化,digitalRead()执行需要花费3-4us,影响读取精度。PWM频率设置在10kHz下,遍历数组时每次下标+1会增加约4%的误差。设置3个端口,下标分别为0、1、2,分别读取占空比17%、48%、76%的10kHz PWM信号,得到的结果分别为17%、43%和65%。若改为1kHz则测量结果与设定基本一致。推测是存在中断抢占。
新方案:
static void ISR0();
static void ISR1();
static void ISR2();
static void ISR3();
每个输入引脚单独设置中断函数。构造函数中应用switch根据每次的curr_num对相应引脚绑定对应的中断服务函数。ISRX()中舍弃了判断引脚号的循环。
switch(curr_num)
{
case 0:
attachInterrupt(PIN0, ISR0, CHANGE);
break;
case 1:
attachInterrupt(PIN1, ISR1, CHANGE);
break;
...
}
暂定输入引脚为PA6、PA1、PB0、PB8。测量结果精确度有提升,但部分情况下仍然存在较大误差。输入频率超过10kHz时误差也将明显增大。1kHz时测量比较准确。
~PWMinPort()
析构函数
ISR()
见上文
void Update()
将当前占空比存入
sample。
用轮询方式找到本引脚对应的数组下标,从而读取高电平时间进而计算占空比。
int PWMinPort::Update()
{
// update the duty cycle from input pwm signal
int i;
for (i = 0; i < 4; i++) {
if (pin_list[i] == this->pin)
{
break;
}
}
if (i == 4)
{
return 0;
}
int period = 1000000 / frequency;
sample = (float) high_level_duration[i] / period;
// Serial.println(sample);
return 0;
}
Read()
返回当前测得的占空比
PID控制模块,能够获得Host给定的参数运行控制程序并进行输出。继承自SensorBase。本Sensor共七个通道,分别是Kp, Ki, Kd, enable, ref, feedback, output,通道号为0-6。
成员变量
float[3] pid
pid参数数组
bool enable
使能标志量,
enable == false强制将输出置0,同时pid积分项置0
float ref
控制系统给定值
float feedback
控制系统反馈量
float output
控制系统输出量
float integral
误差项积分
float lastError
上一采样周期的误差,用于求误差项微分
成员方法
PIDController(float *pid, float ref)
构造函数,根据提供的pid数组及给定量设置相应参数
~PIDController()
析构函数
Update()
当前
enable若为false则将积分项和输出置0,否则执行PID算法。
PID公式:
blockformula_editor u_{output}(t) = K_{p} \cdot e(t) + K_{i} \int^{t}_{0}e(t) + K_{d} \frac{\mathrm{d} e(t)}{\mathrm{d}t}
离散形式PID:
blockformula_editor u_{output}[k] = K_p \cdot error[k] + K_i \cdot ∑_j error[j] + K_d \cdot (error[k] - error[k - 1])
位置式PID代码:
err = ref - feedback; // current error
integral += err;
output = kp * err + ki * integral + kd * (err - lastErr)
lastErr = err;
Read()
根据指定的通道号,返回相应的数据,如output等。enable为真时返回1.0,否则返回0.0
Write()
读取相应的控制参数,如pid、ref等。当操作enable时若输入值为0.0则enable为假,否则为真
记录fire次数。每次上升沿相应的成员变量+1。采用中断实现,原理与PWMinPort类似。
成员变量
pin
输入引脚
fire_count
计数变量
成员方法
FireCounter
构造函数,设置引脚并初始化
fire_count为0.
~FireCounter
析构函数
Update
不操作
Read
返回当前计数
可以正常使用。
烧录Cdp相关代码时常量区.rodata和代码段.text越界,需要优化代码
section `.text' is not within region `FLASH'
`.rodata' will not fit in region `FLASH'
本文章使用limfx的vsocde插件快速发布