
简易PID
简介
使用梁山派复刻立创-电赛TI:简易PID入门项目;因为梁山派比较熟悉,就用梁山派复刻了这个项目,其中屏幕使用了1.8寸128X160分辨率的屏幕。
简介:使用梁山派复刻立创-电赛TI:简易PID入门项目;因为梁山派比较熟悉,就用梁山派复刻了这个项目,其中屏幕使用了1.8寸128X160分辨率的屏幕。开源协议
:LGPL 3.0
(未经作者授权,禁止转载)描述
项目简介
本项目使用梁山派复刻立创-电赛TI:简易PID入门项目。该项目在硬件上,除开发板外,也是仅使用一个编码器电机 + 电机驱动 + 屏幕 + 按键 + 电源。和官方差不多,大部分器件换成了贴片。
项目功能
- 屏幕显示二级菜单,通过按键选择;
- 屏幕显示PID曲线变化;
- 按键长短按控制和调参;
- 实现电机PID的实时定速调整;
- 实现电机PID的实时定距调整;
硬件设计
项目一方面考虑到电机功率比较大,担心USB坏掉,另一方面DC-DC我也一直想尝试,所以使用了DC12V输入,同时也可以使用梁山派USB供电,实测可以驱动电机,为了安全考虑还是使用12V供电稳妥。
1. DC-DC
考虑到可调电压的DC-DC芯片设计起来比较麻烦,第一次使用这里就选择了DC-DC5V的芯片XL1509-5.0,该芯片可以输出2A的电流,带动电机很稳。
电路图如下:

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

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

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

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

6. 按键
这里按键接了上拉电阻。

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在数据手册中对应两个定时器。都可以驱动。

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

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

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


同理使用定时器0时,参考手册中的DMA1,TIMER0_UP,对应的PERIEN位域是110,所以这里选择DMA_SUBPERI6。
1.4 DMA寄存器配置
这里配置的DMA寄存器是通道 X 捕获/比较值寄存器,在用户手册中可以查看通道1对应的地址偏移是0X38,通道2对应的地址偏移是0X3C。


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


根据地址相加就可以得到DMA应该配置的外设地址是0x4000043c或者0x40010038。
1.5 彩灯配置
彩灯驱动的DMA数据,根据WS2813C-V3数据手册的数据传输时间是:

这里的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引脚。
引脚复用参考下图配置。

/*开启时钟*/
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. 项目逻辑
流程图如下:

项目主要逻辑就是根据按键变化执行相应的函数,本项目中的四个按键,从左往右依次是上、下、确认、左、右。
按键的回调函数如下(部分):
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;
}
代码中根据所处页面不用触发管理函数。
按键功能描述
- 上:参数页调节参数;定距\定速界面切换选框;电机开启后切换PID参数界面和PID波形界面;
- 下:参数页调节参数;定距\定速界面切换选框;电机开启后切换PID参数界面和PID波形界面;
- 确认:确认进入其他界面;
- 左:首页切换定距,定速;退出参数设置、定速、定距界面;
- 右:首页切换定距,定速;
具体操作请参考演示视频。
8.版本说明
V1.0 基本实现PID项目功能,PID波形界面为切换界面显示。
设计图
未生成预览图,请在编辑器重新保存一次BOM
暂无BOM
克隆工程知识产权声明&复刻说明
本项目为开源硬件项目,其相关的知识产权归创作者所有。创作者在本平台上传该硬件项目仅供平台用户用于学习交流及研究,不包括任何商业性使用,请勿用于商业售卖或其他盈利性的用途;如您认为本项目涉嫌侵犯了您的相关权益,请点击上方“侵权投诉”按钮,我们将按照嘉立创《侵权投诉与申诉规则》进行处理。
请在进行项目复刻时自行验证电路的可行性,并自行辨别该项目是否对您适用。您对复刻项目的任何后果负责,无论何种情况,本平台将不对您在复刻项目时,遇到的任何因开源项目电路设计问题所导致的直接、间接等损害负责。










