站内搜索
发作品签到
简易PID
专业版

简易PID

简介

使用梁山派复刻立创-电赛TI:简易PID入门项目;因为梁山派比较熟悉,就用梁山派复刻了这个项目,其中屏幕使用了1.8寸128X160分辨率的屏幕。

简介:使用梁山派复刻立创-电赛TI:简易PID入门项目;因为梁山派比较熟悉,就用梁山派复刻了这个项目,其中屏幕使用了1.8寸128X160分辨率的屏幕。
电赛TI训练营-简易PID项目
复刻成本:50

开源协议

LGPL 3.0

(未经作者授权,禁止转载)
创建时间:2025-04-29 20:59:56更新时间:2025-06-12 10:33:05

描述

项目简介

本项目使用梁山派复刻立创-电赛TI:简易PID入门项目。该项目在硬件上,除开发板外,也是仅使用一个编码器电机 + 电机驱动 + 屏幕 + 按键 + 电源。和官方差不多,大部分器件换成了贴片。

项目功能

  • 屏幕显示二级菜单,通过按键选择;
  • 屏幕显示PID曲线变化;
  • 按键长短按控制和调参;
  • 实现电机PID的实时定速调整;
  • 实现电机PID的实时定距调整;

硬件设计

项目一方面考虑到电机功率比较大,担心USB坏掉,另一方面DC-DC我也一直想尝试,所以使用了DC12V输入,同时也可以使用梁山派USB供电,实测可以驱动电机,为了安全考虑还是使用12V供电稳妥。

1. DC-DC

考虑到可调电压的DC-DC芯片设计起来比较麻烦,第一次使用这里就选择了DC-DC5V的芯片XL1509-5.0,该芯片可以输出2A的电流,带动电机很稳。
电路图如下:

image.png

2. LDO

LDO这里选用了SSP7903P33PR,可以输出1A电流,给开发板供电也比较稳定。

image.png

3. LED灯

这里使用了WS2813C-V3,使用定时器PWM-DMA驱动。

image.png

4. 电机驱动

这里使用了RZ7899-MS芯片,RZ7899-MS 是一款 DC 双向马达驱动芯片,最大输出电流可达到6A,工作电压在3.0v~30V,这里使用5V驱动。当然也可以直接使用DC输入的12V。我选用的电机堵转电流在1.3A,额定电流200mA,使用这个芯片完全没问题。
image.png

5. LCD屏幕

这里使用的屏幕是1.8寸,分辨率是128X160。驱动芯片是ST7735。

image.png

6. 按键

这里按键接了上拉电阻。

image.png

2. 软件设计

电机驱动使用了PWM,这里使用的彩灯WS2813C-V3也需要PWM,软件先从PWM开始。

1. PWM-WS2813C-V3驱动

1.1 源码

    gpio_mode_set(PORT_BIN, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_BIN);
    gpio_output_options_set(PORT_BIN, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_BIN);
    gpio_af_set(PORT_BIN, GPIO_BIN_AF, GPIO_BIN);

    // 初始化DMA通道
    dma_single_data_parameter_struct dma_init_struct;
    rcu_periph_clock_enable(RCU_DMA_PWM);  // 启用DMA时钟
    dma_deinit(BSP_DMA, BSP_DMA_CHN);
    dma_init_struct.periph_addr = (uint32_t)TIMER_ADDR;
    dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
    dma_init_struct.memory0_addr = (uint32_t)WS2812_RGB_Buff;
    dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
    dma_init_struct.periph_memory_width = DMA_PERIPH_WIDTH_16BIT;
    dma_init_struct.circular_mode = DMA_CIRCULAR_MODE_ENABLE;
    dma_init_struct.direction = DMA_MEMORY_TO_PERIPH;
    dma_init_struct.number = sizeof(WS2812_RGB_Buff)  ;
    dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH;
    dma_single_data_mode_init(BSP_DMA, BSP_DMA_CHN, &dma_init_struct);
    /* 使能通道外设 */
    dma_channel_subperipheral_select(BSP_DMA, BSP_DMA_CHN, BSP_DMA_SET);
    dma_channel_enable(BSP_DMA, BSP_DMA_CHN);
    dma_flag_clear(BSP_DMA, BSP_DMA_CHN, DMA_FLAG_FTF);
    // 开启定时器时钟
    timer_parameter_struct timer_initpara;
    rcu_periph_clock_enable(BSP_PWM_TIMER_RCU);
    rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4);
    timer_deinit(BSP_PWM_TIMER);

    // 配置定时器参数
    timer_initpara.prescaler = 0; // 根据系统时钟和目标频率计算
    timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
    timer_initpara.counterdirection = TIMER_COUNTER_UP;
    timer_initpara.period = 299; // 根据目标频率计算
    timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
    timer_initpara.repetitioncounter = 0;
    timer_init(BSP_PWM_TIMER, &timer_initpara);
    timer_oc_parameter_struct timer_ocinitpara;
    timer_ocinitpara.outputstate = TIMER_CCX_DISABLE;
    timer_ocinitpara.outputnstate = TIMER_CCXN_ENABLE;
    timer_ocinitpara.ocpolarity = TIMER_OC_POLARITY_HIGH;
    timer_ocinitpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH;
    timer_ocinitpara.ocidlestate = TIMER_OC_IDLE_STATE_LOW;
    timer_ocinitpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
    timer_channel_output_config(BSP_PWM_TIMER, BSP_PWM_CHN, &timer_ocinitpara);//输出定时器
    timer_channel_output_pulse_value_config(BSP_PWM_TIMER, BSP_PWM_CHN, 0);
    timer_channel_output_mode_config(BSP_PWM_TIMER, BSP_PWM_CHN, TIMER_OC_MODE_PWM0);//输出模式
    timer_channel_output_shadow_config(BSP_PWM_TIMER, BSP_PWM_CHN, TIMER_OC_SHADOW_DISABLE);//影子寄存器不使能
    timer_primary_output_config(BSP_PWM_TIMER, ENABLE);
    timer_dma_enable(BSP_PWM_TIMER, TIMER_DMA_UPD);
    timer_auto_reload_shadow_enable(BSP_PWM_TIMER);
    timer_enable(BSP_PWM_TIMER);
    dma_interrupt_enable(BSP_DMA, BSP_DMA_CHN, DMA_CHXCTL_FTFIE); //DMA中断使能
    nvic_irq_enable(BSP_DMA_Channe_IRQn, 2, 1);

1.2 定时器

硬件配置了DIN和BIN输入驱动,这个彩灯输入信号选择一个输入即可,这里选用了DIN输入,对应引脚是PB0,PB0在数据手册中对应两个定时器。都可以驱动。

image.png
PBO的复用功能查看数据手册可以得知是AF1或者AF2,怎么选择取决于定时器选择。

image.png

1.3 DMA

DMA选择查看用户手册,得知DMA0对应定时器2的驱动,DMA1对应定时器0驱动,这里参考大佬的具体分析

image.png
这里的定时器2通道选择TIMER2_UP的通道。查看通道对应的PERIEN位域是101,所以这里选择DMA_SUBPERI5

image.png

image.png
同理使用定时器0时,参考手册中的DMA1,TIMER0_UP,对应的PERIEN位域是110,所以这里选择DMA_SUBPERI6

1.4 DMA寄存器配置

这里配置的DMA寄存器是通道 X 捕获/比较值寄存器,在用户手册中可以查看通道1对应的地址偏移是0X38,通道2对应的地址偏移是0X3C。

image.png

image.png
对应定时器的地址查看手册得知是

image.png
image.png
根据地址相加就可以得到DMA应该配置的外设地址是0x4000043c或者0x40010038

1.5 彩灯配置

彩灯驱动的DMA数据,根据WS2813C-V3数据手册的数据传输时间是:

image.png
这里的PWM配置了是80K,则1码对应占空比数据是200,0码对应的占空比的数据是0码。具体代码是定义一个二维数组,前面填充颜色的24bit的PWM值,后面填充24bit的复位值。

2. 电机驱动

电机驱动不需要使用DMA,这里简单配置下定时器即可。这里定时器不分频,定时1ms,自动重载值是239999。

void motor_init(void)
{
    rcu_periph_clock_enable(RCU_BI);
    gpio_mode_set(PORT_BI, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_BI);
    gpio_output_options_set(PORT_BI, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_BI);
    gpio_af_set(PORT_BI, GPIO_BI_AF, GPIO_BI);

    rcu_periph_clock_enable(RCU_FI);
    gpio_mode_set(PORT_FI, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_FI);
    gpio_output_options_set(PORT_FI, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_FI);
    gpio_af_set(PORT_FI, GPIO_FI_AF, GPIO_FI);

    // 开启定时器时钟
    timer_parameter_struct timer_initpara;

    rcu_periph_clock_enable(MOTOR_RCU_TIMER);
    rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4);

    timer_deinit(MOTOR_PWM_TIMER);

    // 配置定时器参数
    timer_initpara.prescaler = 0; // 根据系统时钟和目标频率计算
    timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
    timer_initpara.counterdirection = TIMER_COUNTER_UP;
    timer_initpara.period = 29999; // 根据目标频率计算
    timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
    timer_initpara.repetitioncounter = 0;
    timer_init(MOTOR_PWM_TIMER, &timer_initpara);

    timer_oc_parameter_struct timer_ocinitpara;

    timer_ocinitpara.outputstate = TIMER_CCX_ENABLE;
    timer_ocinitpara.outputnstate = TIMER_CCXN_DISABLE;
    timer_ocinitpara.ocpolarity = TIMER_OC_POLARITY_HIGH;
    timer_ocinitpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH;
    timer_ocinitpara.ocidlestate = TIMER_OC_IDLE_STATE_HIGH;
    timer_ocinitpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;

    timer_channel_output_config(MOTOR_PWM_TIMER, MOTOR_PWM_CHBI, &timer_ocinitpara);//输出定时器
    timer_channel_output_config(MOTOR_PWM_TIMER, MOTOR_PWM_CHFI, &timer_ocinitpara);//输出定时器

    timer_channel_output_pulse_value_config(MOTOR_PWM_TIMER, MOTOR_PWM_CHBI, 10000);//占空比
    timer_channel_output_mode_config(MOTOR_PWM_TIMER, MOTOR_PWM_CHBI, TIMER_OC_MODE_PWM0);//输出模式
    timer_channel_output_shadow_config(MOTOR_PWM_TIMER, MOTOR_PWM_CHBI, TIMER_OC_SHADOW_DISABLE);//影子寄存器不使能



    timer_channel_output_pulse_value_config(MOTOR_PWM_TIMER, MOTOR_PWM_CHFI, 0);//占空比
    timer_channel_output_mode_config(MOTOR_PWM_TIMER, MOTOR_PWM_CHFI, TIMER_OC_MODE_PWM0);//输出模式
    timer_channel_output_shadow_config(MOTOR_PWM_TIMER, MOTOR_PWM_CHFI, TIMER_OC_SHADOW_DISABLE);//影子寄存器不使能

    timer_auto_reload_shadow_enable(MOTOR_PWM_TIMER);
    timer_enable(MOTOR_PWM_TIMER);
}

2. 按键

2.1 按键初始化

由于有硬件上拉,这里无需上拉,配置很简单。

    rcu_periph_clock_enable(RCU_GPIOA);
    gpio_mode_set(GPIOA, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6);

按键驱动同样使用开源按键库 flexible_button
首先初始化使用按键的配置,包括按键回调函数、单击、长按的时间等

void user_button_init(void)
{
    int i;
    key_init();
    /* 初始化按键数据结构 */
    memset(&user_button[0], 0x0, sizeof(user_button));

    user_button[BUTTON_UP].usr_button_read = button_up_read;//按键读值回调函数
    user_button[BUTTON_UP].cb = (flex_button_response_callback)btn_up_cb;//按键事件回调函数

    user_button[BUTTON_LEFT].usr_button_read = button_left_read;//按键读值回调函数
    user_button[BUTTON_LEFT].cb = (flex_button_response_callback)btn_left_cb;//按键事件回调函数

    user_button[BUTTON_RIGHT].usr_button_read = button_right_read;//按键读值回调函数
    user_button[BUTTON_RIGHT].cb = (flex_button_response_callback)btn_right_cb;//按键事件回调函数

    user_button[BUTTON_DOWN].usr_button_read = button_down_read;//按键读值回调函数
    user_button[BUTTON_DOWN].cb = (flex_button_response_callback)btn_down_cb;//按键事件回调函数


    user_button[USER_BUTTON_MAX].usr_button_read = button_en_read;//按键读值回调函数
    user_button[USER_BUTTON_MAX].cb = (flex_button_response_callback)btn_en_cb;//按键事件回调函数
    /* 初始化 按键引脚 */

    for (i = 0; i < USER_BUTTON_MAX + 1; i ++)
    {
        user_button[i].id = i;
        user_button[i].pressed_logic_level = 0; //按键按下时的逻辑电平
        user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1000);
        user_button[i].long_press_start_tick = FLEX_MS_TO_SCAN_CNT(5000);
        user_button[i].long_hold_start_tick = FLEX_MS_TO_SCAN_CNT(10000);

        flex_button_register(&user_button[i]);
    }
}

这里的长按时间根据flexible_button.h文件中的扫码频率来计算
#define FLEX_BTN_SCAN_FREQ_HZ 100 // How often flex_button_scan () is called
#define FLEX_MS_TO_SCAN_CNT(ms) (ms / (1000 / FLEX_BTN_SCAN_FREQ_HZ))

这里我配置成100了,大概10ms运行一次扫描函数。
扫描函数如下:

KEY_STATUS key_scan(void)
{
    KEY_STATUS states;
    //
    states.up = gpio_input_bit_get(GPIOA, GPIO_PIN_6) ? 1 : 0;//K1
    states.down = gpio_input_bit_get(GPIOA, GPIO_PIN_5) ? 1 : 0;//K2
    states.en = gpio_input_bit_get(GPIOA, GPIO_PIN_3) ? 1 : 0;//K3
    states.left = gpio_input_bit_get(GPIOA, GPIO_PIN_4) ? 1 : 0;//K4
    states.right = gpio_input_bit_get(GPIOA, GPIO_PIN_2) ? 1 : 0;//K5

    return states;
}

因为有硬件上拉,初始化里面的按下逻辑电平是0。根据扫描的数据,来执行对应的回调函数。回调函数根据实际实现是功能来实现。

4. 编码器获取

这里和官方一样,使用定时器+中断,20ms运行一次获取中断数据,下面配置了PB3和PB5的中断。

void encoder_init(void)
{
    rcu_periph_clock_enable(RCU_GPIOB);
    rcu_periph_clock_enable(RCU_SYSCFG);
    gpio_mode_set(GPIOB, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN_3 | GPIO_PIN_5);

    nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2);
    nvic_irq_enable(EXTI3_IRQn, 3U, 3U); // 抢占优先级3,子优先级3

    /* 连接中断线到GPIO */
    syscfg_exti_line_config(EXTI_SOURCE_GPIOB, EXTI_SOURCE_PIN3);
    /* 初始化中断线 */
    exti_init(EXTI_3, EXTI_INTERRUPT, EXTI_TRIG_RISING);
    /* 使能中断 */
    exti_interrupt_enable(EXTI_3);
    /* 清除中断标志位 */
    exti_interrupt_flag_clear(EXTI_3);
    nvic_irq_enable(EXTI5_9_IRQn, 3U, 3U); // 抢占优先级3,子优先级3
    /* 连接中断线到GPIO */
    syscfg_exti_line_config(EXTI_SOURCE_GPIOB, EXTI_SOURCE_PIN5);
    /* 初始化中断线 */
    exti_init(EXTI_5, EXTI_INTERRUPT, EXTI_TRIG_RISING);

    /* 使能中断 */
    exti_interrupt_enable(EXTI_5);
    /* 清除中断标志位 */
    exti_interrupt_flag_clear(EXTI_5);
    timer_parameter_struct timer_initpara;
    rcu_periph_clock_enable(RCU_TIMER1);
    rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4);
    timer_deinit(TIMER1);
    // 配置定时器参数
    timer_initpara.prescaler = 23999; // 根据系统时钟和目标频率计算
    timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
    timer_initpara.counterdirection = TIMER_COUNTER_UP;
    timer_initpara.period = 199; // 根据目标频率计算
    timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
    timer_initpara.repetitioncounter = 0;
    timer_init(TIMER1, &timer_initpara);
    timer_enable(TIMER1);
    nvic_irq_enable(TIMER1_IRQn, 3, 2); // 设置中断优先级为 3,2断
    /* 使能中断 */
    timer_interrupt_enable(TIMER1, TIMER_INT_UP); // 使能更新事件中断
}
void EXTI3_IRQHandler()
{
    if (exti_interrupt_flag_get(EXTI_3) == SET)
    {
        if (gpio_input_bit_get(GPIOB, GPIO_PIN_5) == SET) 
        {
            motor_encoder.temp_count--;
        }
        else
        {
            motor_encoder.temp_count++;
        }
        exti_interrupt_flag_clear(EXTI_3); 
    }
}
void EXTI5_9_IRQHandler()
{
    if (exti_interrupt_flag_get(EXTI_5) == SET)
    {
        if (gpio_input_bit_get(GPIOB, GPIO_PIN_3) == SET) 
        {
            motor_encoder.temp_count++;
        }
        else
        {
            motor_encoder.temp_count--;
        }
        exti_interrupt_flag_clear(EXTI_5); 
    }
}
void TIMER1_IRQHandler(void)
{

    if (timer_interrupt_flag_get(TIMER1, TIMER_INT_FLAG_UP) == SET)
    {
        timer_interrupt_flag_clear(TIMER1, TIMER_INT_FLAG_UP);  
        encoder_update();
    }
}

中断部分代码参考立创开发板手册-外部中断按键点灯即可。关于 16 个 IO 中断的中断类型为 0-4 引脚配置为 EXTIx_IRQn(x=可取 1-4),5-9 引脚配置EXTI5_9_IRQn,10-15 引脚配置为 EXTI10_15_IRQn 。

5. LCD驱动

这里驱动使用SPI硬件驱动,电路设计参考数据手册,选用了硬件SPI引脚。
引脚复用参考下图配置。
image.png

/*开启时钟*/
	rcu_periph_clock_enable(RCU_LCD_SCL);
	rcu_periph_clock_enable(RCU_LCD_SDA);
	rcu_periph_clock_enable(RCU_LCD_CS);
	rcu_periph_clock_enable(RCU_LCD_DC);
	rcu_periph_clock_enable(RCU_LCD_RES);
	rcu_periph_clock_enable(RCU_LCD_BLK);
	rcu_periph_clock_enable(RCU_SPI_HARDWARE); // 开启SPI时钟

	/*配置SPI的SCK GPIO*/
	gpio_af_set(PORT_LCD_SCL, LINE_AF_SPI, GPIO_LCD_SCL);
	gpio_mode_set(PORT_LCD_SCL, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_LCD_SCL);
	gpio_output_options_set(PORT_LCD_SCL, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_SCL);
	gpio_bit_set(PORT_LCD_SCL,GPIO_LCD_SCL);

	/*配置SPI的MOSI GPIO*/
	gpio_af_set(PORT_LCD_SDA, LINE_AF_SPI, GPIO_LCD_SDA);
	gpio_mode_set(PORT_LCD_SDA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_LCD_SDA);
	gpio_output_options_set(PORT_LCD_SDA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_SDA);
	gpio_bit_set(PORT_LCD_SDA, GPIO_LCD_SDA);	 
	
	/* 配置DC */
	gpio_mode_set(PORT_LCD_DC,GPIO_MODE_OUTPUT,GPIO_PUPD_PULLUP,GPIO_LCD_DC);
	gpio_output_options_set(PORT_LCD_DC,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,GPIO_LCD_DC);
	gpio_bit_write(PORT_LCD_DC, GPIO_LCD_DC, SET);

	/* 配置CS */
	gpio_mode_set(PORT_LCD_CS,GPIO_MODE_OUTPUT,GPIO_PUPD_NONE,GPIO_LCD_CS);
	gpio_output_options_set(PORT_LCD_CS,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,GPIO_LCD_CS);
	gpio_bit_write(PORT_LCD_CS, GPIO_LCD_CS, SET);

	/* 配置RES */
	gpio_mode_set(PORT_LCD_RES,GPIO_MODE_OUTPUT,GPIO_PUPD_PULLUP,GPIO_LCD_RES);
	gpio_output_options_set(PORT_LCD_RES,GPIO_OTYPE_PP,GPIO_OSPEED_50MHZ,GPIO_LCD_RES);
	gpio_bit_write(PORT_LCD_RES, GPIO_LCD_RES, SET);

	/* 配置BLK */
	gpio_mode_set(PORT_LCD_BLK, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_BLK);
	gpio_output_options_set(PORT_LCD_BLK, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_BLK);
	gpio_bit_write(PORT_LCD_BLK, GPIO_LCD_BLK, SET);

	/*配置SPI参数*/
	spi_parameter_struct spi_init_struct;
	spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX; // 配置为传输模式全通工
	spi_init_struct.device_mode = SPI_MASTER;			   // 配置为主机
	spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT;	   // 8位数据
	spi_init_struct.clock_polarity_phase = SPI_CK_PL_HIGH_PH_2EDGE;
	spi_init_struct.nss = SPI_NSS_SOFT;	  // 软件CS
	spi_init_struct.prescale = SPI_PSC_4; // 4分频
	spi_init_struct.endian = SPI_ENDIAN_MSB;
	spi_init(PORT_SPI, &spi_init_struct);

	/*使能SPI*/
	spi_enable(PORT_SPI);

设置参数界面,绘制选框函数如下:

/*
功能:绘制选择框
参数:  x=起始X轴地址
        w=绘制的选择框矩形宽度
        y=起始Y轴地址
        h=绘制的选择框矩形高度
        line_length=选择框的线长度
        interval=选择框 与 被选择矩形 之间的间隔像素
        color=选择框的颜色
*/
void disp_select_box(int x, int w, int y, int h, int line_length, int interval, int color)
{
    //计算 选择框 与 被选择矩形 的距离间隔
    x = x - interval;
    w = w + (interval + interval);
    y = y - interval;
    h = h + (interval + interval);
    //左上角
    LCD_DrawLine(x, y, x + line_length, y, color);
    LCD_DrawLine(x, y, x, y + line_length, color);
    //右上角
    LCD_DrawLine(x + w, y, x + w - line_length, y, color);
    LCD_DrawLine(x + w, y, x + w, y + line_length, color);
    //左下角
    LCD_DrawLine(x, y + h, x + line_length, y + h, color);
    LCD_DrawLine(x, y + h, x, y + h - line_length, color);
    //右下角
    LCD_DrawLine(x + w, y + h, x + w - line_length, y + h, color);
    LCD_DrawLine(x + w, y + h, x + w, y + h - line_length, color);
}

带圆角的矩形函数如下:

void Drawarc(int x,int y,int a,int b,unsigned int r,unsigned int c)
{
    int x_tp, y_tp;
    int d; // Decision variable

    // Start with the top and bottom rows of the circle
    for (x_tp = 0; x_tp <= r; x_tp++) {
        // Calculate the corresponding y values
        y_tp = (int)sqrt(r * r - x_tp * x_tp);

        // Draw the horizontal lines from the circle edge to the center
        for (int i = x_tp; i >= -x_tp; i--) {
            LCD_DrawPoint(x + i, y - y_tp, c);
            LCD_DrawPoint(x + i, y + y_tp, c);
        }
    }

    // Now fill the rest of the circle
    d = 3 - 2 * r;
    while (x_tp > y_tp) {
        if (d < 0) {
            d += 4 * x_tp + 6;
        } else {
            d += 4 * (x_tp - y_tp) + 10;
            y_tp++;
        }
        x_tp--;

        // Draw the horizontal lines from the circle edge to the center
        for (int i = -x_tp; i <= x_tp; i++) {
            LCD_DrawPoint(x + i, y - y_tp, c);
            LCD_DrawPoint(x + i, y + y_tp, c);
        }

        // Draw the vertical lines from the circle edge to the center
        for (int i = -y_tp; i <= y_tp; i++) {
            LCD_DrawPoint(x + x_tp, y + i, c);
            LCD_DrawPoint(x - x_tp, y + i, c);
        }
    }
}

void LCD_ArcRect(uint16_t xsta,uint16_t ysta,uint16_t xend,uint16_t yend,uint16_t r,uint16_t color)
{

	
	//画四条边
	LCD_DrawLine(xsta+r,ysta,xend-r,ysta,color);
	LCD_DrawLine(xsta,ysta+r,xsta,yend-r,color);
	LCD_DrawLine(xsta+r,yend,xend-r,yend,color);
	LCD_DrawLine(xend,ysta+r,xend,yend-r,color);

	//再画四个圆角
	Drawarc(xsta+r,ysta+r,90,180,r,color);//左上
	Drawarc(xend-r,ysta+r,0,90,r,color);//右上
	Drawarc(xsta+r,yend-r,180,270,r,color);//左下
	Drawarc(xend-r,yend-r,270,360,r,color);//右下

	//画五个实心矩形
	LCD_Fill(xsta+r,ysta,xend-r,ysta+r,color);//上
	LCD_Fill(xend-r,ysta+r,xend,yend-r,color);//右
	LCD_Fill(xsta+r,yend-r,xend-r,yend,color);//下
	LCD_Fill(xsta,ysta+r,xsta+r,yend-r,color);//左
	LCD_Fill(xsta+r,ysta+r,xend-r,yend-r,color);//中

}

定速、定距界面,屏幕小,这里把一行数据拆分成了两行:

void ui_speed_page(void)
{
    TXT_OBJECT p = {0}, i = {0}, d = {0};

    //关闭背光
    LCD_BLK_Clr();

    //绘制全局背景
    LCD_Fill(0, 0, LCD_W, LCD_H, BLACK);

    int str_center_x = 0;
    LCD_ShowChinese(64, 3, (unsigned char *)"定速", WHITE, BLACK, 16, 1);
    //显示静态的 P I D 标题
    LCD_ShowChar(screen_center_x - str_center_x - 55, 20, 'P', WHITE, BLUE, 16, 1);
    LCD_ShowChar(screen_center_x - str_center_x,     20, 'I', WHITE, BLUE, 16, 1);
    LCD_ShowChar(screen_center_x - str_center_x + 55, 20, 'D', WHITE, BLUE, 16, 1);

    //显示 P 参数的圆角矩形背景
    p.start_x = screen_center_x - str_center_x - 60 - 15; //5
    p.start_y = 40;
    p.end_x = screen_center_x - str_center_x - 60 + 25; //80 - 55 + 25 = 45
    p.end_y = 40 + 24;
    LCD_ArcRect(p.start_x, p.start_y, p.end_x, p.end_y, 4, BLUE);

    //显示 I 参数的圆角矩形背景
    i.start_x = screen_center_x - str_center_x  - 20;  //80 -15 = 60
    i.start_y = 40;
    i.end_x = screen_center_x - str_center_x  + 20;// 100
    i.end_y = 40 + 24;
    LCD_ArcRect(i.start_x, i.start_y, i.end_x, i.end_y, 4, BLUE);

    //显示 D 参数的圆角矩形背景
    d.start_x = screen_center_x - str_center_x + 55 - 20;//115
    d.start_y = 40;
    d.end_x = screen_center_x - str_center_x + 55 + 20;
    d.end_y = 40 + 24;
    LCD_ArcRect(d.start_x, d.start_y, d.end_x, d.end_y, 4, BLUE);

    //显示静态的 Speed:  Target:  标题
    LCD_ShowString(10, 70, "Speed: ", WHITE, BLUE, 16, 1);
    LCD_ShowString(10, 100, "Target: ", WHITE, BLUE, 16, 1);

    LCD_BLK_Set();//打开背光
}

6. PID介绍

PID算法,即比例P-积分I-微分D控制器,它通过控制系统的偏差(目标值与实际值之间的差)来调节控制变量,使得系统达到或维持在一个预定的状态。
比例(P):比例部分直接与当前的误差成正比。它的作用是提供一个立即响应,并试图将系统的输出尽可能快地移向目标值。然而,仅依靠比例控制往往不能消除稳态误差。
积分(I):积分部分与误差随时间的累积成正比。它主要用于消除系统中的稳态误差,即当系统接近目标值但仍存在小的稳定误差时,积分作用会继续调整直到误差完全消失。
微分(D):微分部分与误差的变化率成正比。它预测系统行为的未来趋势,并据此做出调整,有助于减少超调和振荡,提高系统的稳定性。

PID的基本公式:
PID_OUT = (Kp x P)+ (Ki x I)+(Kd x D)
公式拆为静态参数和动态参数:
静态参数(控制器设定值,不随时间变化)
Kp:比例增益
Ki:积分增益
Kd:微分增益
这些是根据系统特性通过整定方法(如Ziegler-Nichols法、试凑法等)确定的固定系数,在控制系统运行过程中通常保持不变。
动态参数(随时间变化的变量)
P = e(当前误差)
I = I + e(误差累加)
D = e - last_e(当前误差减去上一次误差)

代码中首先定义一个PID结构体,然后根据公式去计算。

typedef struct
{
  float kp, ki, kd; 						      // 三个静态系数
  float change_p, change_i, change_d;	          // 三个动态参数
  float error, last_error; 						  // 误差、之前误差
  float max_change_i; 							  // 积分限幅
  float output, max_output; 				      // 输出、输出限幅
  int target;                                     // 目标
}PID;

/****************************************************
功能:PID计算
参数:pid = pid的参数输入
     target = 目标值
     current = 当前值
返回:PID计算后的结果
****************************************************/
float pid_calc(PID *pid, float target, float current)
{
    //用上一次的误差值更新 之前误差last_error
    pid->last_error = pid->error;
    //获取新的误差 = 目标值 - 当前值
    pid->error = target - current;

    //计算比例P = 目标值与实际值之间的误差e
    float pout = pid->error;
    //计算积分I = 误差e的累加
    pid->change_i += pid->error;
    //计算微分D = 当前误差e - 之前的误差last_e
    float dout = pid->error - pid->last_error;

    //积分I 限制不能超过正负最大值
    if(pid->change_i > pid->max_change_i)
    {
      pid->change_i = pid->max_change_i;
    }
    else if(pid->change_i < -pid->max_change_i)
    {
      pid->change_i = -pid->max_change_i;
    }

    //计算输出PID_OUT = (Kp x P)+ (Ki x I)+(Kd x D)
    pid->output = (pid->kp * pout) + (pid->ki * pid->change_i) + (pid->kd * dout);

    //输出 限制不能超过正负最大值
    if(pid->output > pid->max_output) pid->output = pid->max_output;
    else if(pid->output < -pid->max_output) pid->output = -pid->max_output;

    //返回PID计算的结果
    return pid->output;
}

7. 项目逻辑

流程图如下:

470流程图.png
项目主要逻辑就是根据按键变化执行相应的函数,本项目中的四个按键,从左往右依次是上、下、确认、左、右。

image.png按键的回调函数如下(部分):

void btn_en_cb(flex_button_t *btn)
{
    switch (btn->event)
    {
    case FLEX_BTN_PRESS_CLICK://单击事件
        //如果是首页
        if (get_show_state() == DEFAULT_PAGE)
        {
            //触发进入事件
            event_manager(&system_status, ENTER_EVENT);
            //根据首页选项显示对应功能页
            ui_select_page_show(get_default_page_flag());
            //如果下一个是定速页
            if (get_show_state() == PID_PAGE)
            {
                //显示定速页的参数
                ui_speed_page_value_set(get_speed_pid()->kp, get_speed_pid()->ki, get_speed_pid()->kd, get_encoder_count(), get_speed_pid()->target, 0);
            }
            //如果下一个是定距页
            if (get_show_state() == DISTANCE_PAGE)
            {
                //显示定距页的参数
                int current_angle = get_temp_encoder() * DEGREES_PER_PULSE;
                ui_distance_page_value_set(get_distance_pid()->kp, get_distance_pid()->ki, get_distance_pid()->kd, current_angle, get_distance_pid()->target, 0);
            }
        }
        //如果是定速页或者定距页
        else if (get_show_state() == PID_PAGE || get_show_state() == DISTANCE_PAGE)
        {
            //触发进入事件
            event_manager(&system_status, ENTER_EVENT);
            //显示选择框
            ui_speed_page_select_box(system_status.set_page_flag);
        }
        //如果是设置页
        else if (get_show_state() == SET_PAGE)
        {
            //触发进入事件
            event_manager(&system_status, ENTER_EVENT);
            //显示选中框
            ui_parameter_select_box_bold(system_status.set_page_flag);
        }
        break;
    case FLEX_BTN_PRESS_LONG_HOLD://长按保持事件
        //触发长按减事件
        event_manager(&system_status, LONG_PRESS_SUBTRACT_START_EVENT);
        break;
    case FLEX_BTN_PRESS_LONG_HOLD_UP://长按保持后抬起事件
        //触发长按结束事件
        event_manager(&system_status, LONG_PRESS_END_EVENT);
        break;
    default:
        break;
    
}

代码中根据所处页面不用触发管理函数。

按键功能描述
  1. 上:参数页调节参数;定距\定速界面切换选框;电机开启后切换PID参数界面和PID波形界面;
  2. 下:参数页调节参数;定距\定速界面切换选框;电机开启后切换PID参数界面和PID波形界面;
  3. 确认:确认进入其他界面;
  4. 左:首页切换定距,定速;退出参数设置、定速、定距界面;
  5. 右:首页切换定距,定速;
    具体操作请参考演示视频。

8.版本说明

V1.0 基本实现PID项目功能,PID波形界面为切换界面显示。

设计图

未生成预览图,请在编辑器重新保存一次

BOM

暂无BOM

3D模型

序号文件名称下载次数
暂无数据

附件

序号文件名称下载次数
1
演示视频.mp4
6
2
GD32F470pid.zip
13
克隆工程
添加到专辑
0
0
分享
侵权投诉
知识产权声明&复刻说明

本项目为开源硬件项目,其相关的知识产权归创作者所有。创作者在本平台上传该硬件项目仅供平台用户用于学习交流及研究,不包括任何商业性使用,请勿用于商业售卖或其他盈利性的用途;如您认为本项目涉嫌侵犯了您的相关权益,请点击上方“侵权投诉”按钮,我们将按照嘉立创《侵权投诉与申诉规则》进行处理。

请在进行项目复刻时自行验证电路的可行性,并自行辨别该项目是否对您适用。您对复刻项目的任何后果负责,无论何种情况,本平台将不对您在复刻项目时,遇到的任何因开源项目电路设计问题所导致的直接、间接等损害负责。

底部导航