20200925组会

需求说明

任务目标

设计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控制系统参数,并完成相应控制程序的运行。

模块设计

PWMoutPort

根据指定的参数,输出一定占空比的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()

根据outputDutyRatiofrequency,使用analogWrite()输出相应占空比的PWM波。

void Update()

不操作

Write()

读取给定的占空比,写入outputDutyRatio

PWMinPort

读取输入信号,计算信号的占空比。继承自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()

返回当前测得的占空比

PIDController

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为假,否则为真

FireCounter

记录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插件快速发布