目录

基于PY32F002A的6+1通道遥控小车II - 控制篇

这篇继续介绍6+1通道遥控小车的控制端, 关于遥控手柄的硬件和软件设计的说明

PCB实物

正面

在嘉立创下单了PCB, 收到的是这个样子的.

  • PCB中二极管的位置稍微偏上, 存在与螺丝短接的风险, 在新的PCB设计中已经将其下移.
  • 无线模块的天线没有覆漆, 在LCEDA中不知道怎么修改. PCB做出来是焊盘的效果(上锡了), 不影响使用.

背面

因为空间限制, PY32F002A和74HC595/165都放到了背面

分割后的各个模块

遥控面板成品

遥控面板的焊接过程运气不错, 从贴片到接插件都是一次成功, 没有返工.

正面

  • 空间限制, 只比一张名片稍微大点, 布局比较局促.
  • LCD因为是裸片没有托板, 和背光板一起是用热熔胶直接固定在PCB上的.

LCD试车, 显示没问题

背面

  • 正面基本上全是接插件, 如果PY32F002A放到这面, 将来万一烧坏更换非常麻烦, 所以贴片元件都放到了背面
  • 电源接口用的是XH2.54
  • LCD背光担心电流过大, 补焊串了一颗1KR的电阻

LCD控制界面

这是最终的LCD控制界面

  • 上面两道横杆代表旋钮的模拟量
  • 中间和下方的四道横杆代表摇杆的模拟量
  • 两边的6个数字代表了模拟量的数值, 都是8bit, 从0 - 255
  • 下方的8个方格代表了8个开关量, 高亮(黑)代表按键按下(低电压), 正常(白)代表按键松开(高电压)

软件设计

整体结构

因为只考虑发送, 所以控制端的流程较为简单, 做一个大循环肯定可行, 采集数据 -> 发送数据 -> 采集数据 -> 发送数据. 如果要提升大循环的效率, 因为LCD显示和无线发送共用SPI, 需要保留在大循环, ADC可以用定时器触发做成DMA, 节省出ADC的时间.

最终使用的执行流程是

  • 使用一个uint8_t pad_state[8]存储6+1通道的数据
  • ADC使用定时器触发, 通过DMA存储转换结果到6个双字节内存地址, ADC DMA转换完成后
    • 将结果转为8bit, 存入 pad_state,
    • 收集74HC165的按键状态, 合成一个byte 也存入pad_state
    • 计算CRC并存至 pad_state 最后一个字节
  • 外层大循环读取 pad_state
    • 更新LCD显示
    • 通过无线发送数据

主循环

int main(void)
{
// ... /* Infinite loop */
while(1)
{
// 更新LCD显示
DRV_Display_Update(pad_state);
// 发送
wireless_tx++;
if (XL2400_Tx(pad_state, XL2400_PLOAD_WIDTH) == 0x20)
{
wireless_tx_succ++;
}
// 每 255 次发送, 打印一次成功次数, 用于标识成功率
if (wireless_tx == 0xFF)
{
wireless_state[10] = wireless_tx_succ;
DEBUG_PRINTF("TX_SUCC: %02X\r\n", wireless_tx_succ);
wireless_tx = 0;
wireless_tx_succ = 0;
}
// 延迟可以调节
LL_mDelay(20);
}
}

DMA中断

void DMA1_Channel1_IRQHandler(void)
{
uint8_t crc = 0;
if (LL_DMA_IsActiveFlag_TC1(DMA1) == 1)
{
LL_DMA_ClearFlag_TC1(DMA1);
// 转换DMA读数为uint8_t并存入pad_state
for (uint8_t i = 0; i < 6; i++)
{
pad_state[i] = (uint8_t)(*(adc_dma_data + i) >> 4);
crc += pad_state[i];
}
// 从 74HC165 读取按键状态
pad_state[6] = HC165_Read();
// 存入CRC结果
pad_state[7] = crc + pad_state[6];
}
}

无线通讯

无线部分使用的是硬件SPI驱动的 XL2400, 代码可以参考

https://github.com/IOsetting/py32f0-template/tree/main/Examples/PY32F0xx/LL/SPI/XL2400_Wireless

传输的数据格式为固定长度8字节

#define XL2400_PLOAD_WIDTH       8   // Payload width

其中字节[0, 5]为6个ADC采集的数值结果, 字节[6]为74HC165采集的按键结果, 字节[7]为CRC校验.

收发的地址是固定的(将来需要改进)

const uint8_t TX_ADDRESS[5] = {0x11,0x33,0x33,0x33,0x11};
const uint8_t RX_ADDRESS[5] = {0x33,0x55,0x33,0x44,0x33};

输入采集

ADC采集

DMA初始化

void MSP_DMA_Config(void)
{
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);
LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_SYSCFG); // Remap ADC to LL_DMA_CHANNEL_1
LL_SYSCFG_SetDMARemap_CH1(LL_SYSCFG_DMA_MAP_ADC);
// Transfer from peripheral to memory
LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
// Set priority
LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PRIORITY_HIGH);
// Circular mode
LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MODE_CIRCULAR);
// Peripheral address no increment
LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PERIPH_NOINCREMENT);
// Memory address increment
LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MEMORY_INCREMENT);
// Peripheral data alignment : 16bit
LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PDATAALIGN_HALFWORD);
// Memory data alignment : 16bit
LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MDATAALIGN_HALFWORD);
// Data length
LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, 6);
// Sorce and target address
LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_1, (uint32_t)&ADC1->DR, (uint32_t)adc_dma_data, LL_DMA_GetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1));
// Enable DMA channel 1
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);
// Enable transfer-complete interrupt
LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1); NVIC_SetPriority(DMA1_Channel1_IRQn, 0);
NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}

ADC初始化

void MSP_ADC_Init(void)
{
__IO uint32_t backup_setting_adc_dma_transfer = 0; LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_ADC1); LL_ADC_Reset(ADC1);
// Calibrate start
if (LL_ADC_IsEnabled(ADC1) == 0)
{
/* Backup current settings */
backup_setting_adc_dma_transfer = LL_ADC_REG_GetDMATransfer(ADC1);
/* Turn off DMA when calibrating */
LL_ADC_REG_SetDMATransfer(ADC1, LL_ADC_REG_DMA_TRANSFER_NONE);
LL_ADC_StartCalibration(ADC1); while (LL_ADC_IsCalibrationOnGoing(ADC1) != 0); /* Delay 1ms(>= 4 ADC clocks) before re-enable ADC */
LL_mDelay(1);
/* Apply saved settings */
LL_ADC_REG_SetDMATransfer(ADC1, backup_setting_adc_dma_transfer);
}
// Calibrate end /* PA0 ~ PA5 as ADC input */
LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_0, LL_GPIO_MODE_ANALOG);
LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_1, LL_GPIO_MODE_ANALOG);
LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_2, LL_GPIO_MODE_ANALOG);
LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_3, LL_GPIO_MODE_ANALOG);
LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_4, LL_GPIO_MODE_ANALOG);
LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_5, LL_GPIO_MODE_ANALOG);
/* Set ADC channel and clock source when ADEN=0, set other configurations when ADSTART=0 */
LL_ADC_SetCommonPathInternalCh(__LL_ADC_COMMON_INSTANCE(ADC1), LL_ADC_PATH_INTERNAL_NONE); LL_ADC_SetClock(ADC1, LL_ADC_CLOCK_SYNC_PCLK_DIV2);
LL_ADC_SetResolution(ADC1, LL_ADC_RESOLUTION_12B);
LL_ADC_SetDataAlignment(ADC1, LL_ADC_DATA_ALIGN_RIGHT);
LL_ADC_SetLowPowerMode(ADC1, LL_ADC_LP_MODE_NONE);
LL_ADC_SetSamplingTimeCommonChannels(ADC1, LL_ADC_SAMPLINGTIME_41CYCLES_5); /* Set TIM1 as trigger source */
LL_ADC_REG_SetTriggerSource(ADC1, LL_ADC_REG_TRIG_EXT_TIM1_TRGO);
LL_ADC_REG_SetTriggerEdge(ADC1, LL_ADC_REG_TRIG_EXT_RISING);
/* Single conversion mode (CONT = 0, DISCEN = 0), performs a single sequence of conversions, converting all the channels once */
LL_ADC_REG_SetContinuousMode(ADC1, LL_ADC_REG_CONV_SINGLE); LL_ADC_REG_SetDMATransfer(ADC1, LL_ADC_REG_DMA_TRANSFER_UNLIMITED);
LL_ADC_REG_SetOverrun(ADC1, LL_ADC_REG_OVR_DATA_OVERWRITTEN);
/* Enable: each conversions in the sequence need to be triggerred separately */
LL_ADC_REG_SetSequencerDiscont(ADC1, LL_ADC_REG_SEQ_DISCONT_DISABLE);
/* Set channel 0/1/2/3/4/5 */
LL_ADC_REG_SetSequencerChannels(ADC1, LL_ADC_CHANNEL_0 | LL_ADC_CHANNEL_1 | LL_ADC_CHANNEL_2 | LL_ADC_CHANNEL_3 | LL_ADC_CHANNEL_4 | LL_ADC_CHANNEL_5); LL_ADC_Enable(ADC1); // Start ADC regular conversion
LL_ADC_REG_StartConversion(ADC1);
}

用于触发ADC的TIM1定时器初始化

void MSP_TIM1_Init(void)
{
LL_TIM_InitTypeDef TIM1CountInit = {0}; // RCC_APBENR2_TIM1EN == LL_APB1_GRP2_PERIPH_TIM1
LL_APB1_GRP2_EnableClock(RCC_APBENR2_TIM1EN); TIM1CountInit.ClockDivision = LL_TIM_CLOCKDIVISION_DIV1;
TIM1CountInit.CounterMode = LL_TIM_COUNTERMODE_UP;
// 系统时钟48MHz, 预分频8K, 预分频后定时器时钟为6KHz
TIM1CountInit.Prescaler = (SystemCoreClock / 6000) - 1;
// 每600次计数一个周期, 每秒10个周期, 可以减小数值提高频率
TIM1CountInit.Autoreload = 600 - 1;
TIM1CountInit.RepetitionCounter = 0;
LL_TIM_Init(TIM1, &TIM1CountInit);
/* Triggered by update */
LL_TIM_SetTriggerOutput(TIM1, LL_TIM_TRGO_UPDATE);
LL_TIM_EnableCounter(TIM1);
}

开关量采集

74HC165的状态读取

uint8_t HC165_Read(void)
{
uint8_t i, data = 0; HC165_LD_LOW; // Pull down LD to load parallel inputs
HC165_LD_HIGH; // Pull up to inhibit parallel loading for (i = 0; i < 8; i++)
{
data = data << 1;
HC165_SCK_LOW;
HC165_NOP; // NOP to ensure reading correct value
if (HC165_DATA_READ)
{
data |= 0x01;
}
HC165_SCK_HIGH;
}
return data;
}

74HC165的示例代码, 可以参考 https://github.com/IOsetting/py32f0-template/tree/main/Examples/PY32F0xx/LL/GPIO/74HC165_8bit_Parallel_In_Serial_Out

LCD显示

PY32F002A驱动ST7567的示例代码可以参考 Examples/PY32F0xx/LL/SPI/ST7567_128x64LCD, 但是这个示例, 包括GitHub上可以搜到的其它示例, 都是使用 128 x 8 的内存作为显示缓存, 通过读写这块缓存再将缓存内容写入 ST7567 实现的显示内容更新. 这种方式可以实现非常灵活的显示, 缺点就是需要占用1KB的内存. 对于STM32F103这类有16KB或20KB内存的控制器, 1KB内存不算什么, 但是 PY32F002A 只有4KB内存, 1KB就值得考虑一下了. 因为遥控部分的数显, 显示格式相对固定, page之间可以相互独立, 没有相互交叠的部分, 启动后只需要显示滑动条和读数, 因此完全可以采用直接输出的方式.

换成直接输出后就变成这样的显示函数了, 定制LCD显示是比较费时费事的一步.

移动光标到坐标

void ST7567_SetCursor(uint8_t page, uint8_t column)
{
ST7567_WriteCommand(ST7567_SET_PAGE_ADDRESS | (page & ST7567_SET_PAGE_ADDRESS_MASK));
ST7567_WriteCommand(ST7567_SET_COLUMN_ADDRESS_MSB | ((column + ST7567_X_OFFSET) >> 4));
ST7567_WriteCommand(ST7567_SET_COLUMN_ADDRESS_LSB | ((column + ST7567_X_OFFSET) & 0x0F));
}

指定宽度和偏移量, 填入固定内容

static void DRV_DrawRepeat(uint8_t symbol, uint8_t width, uint8_t offset, uint8_t colorInvert)
{
symbol = symbol << offset;
symbol = colorInvert? ~symbol : symbol;
ST7567_TransmitRepeat(symbol, width);
}

画出横条

static void DRV_DrawHorizBar(uint8_t page, uint8_t column, uint8_t size)
{
ST7567_SetCursor(page, column);
DRV_DrawRepeat(0x7E, 1, 0, 0);
DRV_DrawRepeat(0x42, size, 0, 0);
DRV_DrawRepeat(0x7E, 1, 0, 0);
}

在横条中画出高亮滑块

static void DRV_DrawHorizBarCursor(uint8_t page, uint8_t column, uint8_t value, uint8_t barWidth, uint8_t cursorWidth, uint8_t direction)
{
value = direction? value : 255 - value;
ST7567_SetCursor(page, column + 1);
DRV_DrawRepeat(0x42, barWidth, 0, 0);
ST7567_SetCursor(page, column + 1 + (value * (barWidth - cursorWidth) / 255));
DRV_DrawRepeat(0x7E, cursorWidth, 0, 0);
}

画出竖条和竖条光标的方法更复杂, 这里就不贴代码了.

在main函数的while循环中, 每次会更新LCD显示

void DRV_Display_Update(uint8_t *state)
{
// 更新按键显示
DRV_DrawKeyState(*(state + 6));
// 更新4个横条的显示
DRV_DrawHorizBarCursor(0, 10, *(state + 4), 50, 4, 0);
DRV_DrawHorizBarCursor(0, 65, *(state + 5), 50, 4, 1);
DRV_DrawHorizBarCursor(7, 0, *(state + 1), 60, 4, 0);
DRV_DrawHorizBarCursor(7, 65, *(state + 2), 60, 4, 1);
// 更新2个竖条显示, 因为竖条处于多个page, 每次更新显示都需要全部重绘
DRV_DrawVertiBar(0, 1, 52);
DRV_DrawVertiBarCursor(0, 1, 52, *(state + 0), 4, 0);
DRV_DrawVertiBar(121, 1, 52);
DRV_DrawVertiBarCursor(121, 1, 52, *(state + 3), 4, 1);
// 输出6个模拟通道的数值(0 ~ 255)
DRV_DrawNumber(1, 10, *(state + 4));
DRV_DrawNumber(1, 100, *(state + 5)); DRV_DrawNumber(4, 10, *(state + 0));
DRV_DrawNumber(4, 100, *(state + 3)); DRV_DrawNumber(5, 10, *(state + 1));
DRV_DrawNumber(5, 100, *(state + 2));
}

用直接写入的方式, 在不开JLink RTT 的情况下, 整机内存只需要不到400个字节, 资源节约效果明显.

普冉PY32系列(十一) 基于PY32F002A的6+1通道遥控小车II - 控制篇的更多相关文章

  1. 普冉PY32系列(三) PY32F002A资源实测 - 这个型号不简单

    目录 普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境 普冉PY32系列(三) P ...

  2. 普冉PY32系列(七) SOP8, SOP10和SOP16封装的PY32F003/PY32F002A管脚复用

    目录 普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境 普冉PY32系列(三) P ...

  3. 普冉PY32系列(四) PY32F002/003/030的时钟设置

    目录 普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境 普冉PY32系列(三) P ...

  4. 普冉PY32系列(六) 通过I2C接口驱动PCF8574扩展的1602LCD

    目录 普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境 普冉PY32系列(三) P ...

  5. 普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介

    目录 普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境 PY32F0系列上市其实相 ...

  6. 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境

    目录 普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境 以下介绍PY32F0系列在 ...

  7. 普冉PY32系列(五) 使用JLink RTT代替串口输出日志

    目录 普冉PY32系列(一) PY32F0系列32位Cortex M0+ MCU简介 普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境 普冉PY32系列(三) P ...

  8. JavaScript系列-----对象基于哈希存储(<Key,Value>之Value篇) (3)

    JavaScript系列-----Objectj基于哈希存储<Key,Value>之Value 1.问题提出 在JavaScript系列-----Object之基于Hash<Key, ...

  9. JavaScript系列-----对象基于哈希存储(<Key,Value>之Key篇) (1)

    1.Hash表的结构 首先,允许我们花一点时间来简单介绍hash表. 1.什么是hash表 hash表是一种二维结构,管理着一对对<Key,Value>这样的键值对,Hash表的结构如下图 ...

  10. SQL Server 2008空间数据应用系列十一:提取MapInfo地图数据中的空间数据解决方案

    原文:SQL Server 2008空间数据应用系列十一:提取MapInfo地图数据中的空间数据解决方案 友情提示,您阅读本篇博文的先决条件如下: 1.本文示例基于Microsoft SQL Serv ...

随机推荐

  1. Cortex M3 CORE

    Cortex CM3 内核架构 CM3内核主要包含几个部分:取指(Fetch)\指令译码(Decoder/DEC)\执行(EXEC)\ALU 内存取数通过load & store指令,就是通过 ...

  2. 宝塔部署 springboot 项目遇到的 一些bug处理方案

    1,上传的项目(jar包)的数据库用户名 .密码 , 和服务器的数据库用户名.密码不一致 2,数据库的表结构没有创建 3, 宝塔 phpmyadmin 进不去 原因: 服务器没有放行888端口, 宝塔 ...

  3. SQL联结

    1联结 那我们又该如何创建联结呢? So easy! 规定要联结的所有表以及它们如何关联就可以了. 在设置关联条件时,为避免不同表被引用的列名相同,我们需要使用完全限定列名(用一个点分隔表名和列名), ...

  4. 【Mysql系列】(二)日志系统:一条更新语句是如何执行的

    有的时候博客内容会有变动,首发博客是最新的,其他博客地址可能会未同步,认准https://blog.zysicyj.top 这篇文章是从Github ReadMe拷贝的,内容实践下载是没问题的,能够正 ...

  5. [转帖]【Python】计算程序运行时间的方法总结

    一.第一种方法 利用time包: import time def test(): start_time = time.time() # 记录程序开始运行时间 s = 0 for i in range( ...

  6. [转帖]Optimizing Block Device Parameter Settings of Linux

    https://support.huawei.com/enterprise/en/doc/EDOC1000181485/ddbc0e8b/optimizing-block-device-paramet ...

  7. [转帖]linux 查看CPU 内存的信息

    https://bbs.huaweicloud.com/blogs/302929   [摘要] ECS信息规格:2vCPUs | 4GiB | kc1.large.2镜像:openEuler 20.0 ...

  8. Vue中is属性的用法 可以动态切换组件

    is 是组件的一个属性,用来展示组件的名称 is和component联用哈 vue提供了component来展示对应的组件名称 compont是一个占位符,is这个属性,用来展示对应的组件名称 三个子 ...

  9. Go 跟踪函数调用链,理解代码更直观

    Go 跟踪函数调用链,理解代码更直观 目录 Go 跟踪函数调用链,理解代码更直观 一.引入 二.自动获取所跟踪函数的函数名 三.增加 Goroutine 标识 四.让输出的跟踪信息更具层次感 五.利用 ...

  10. 文心一言 VS 讯飞星火 VS chatgpt (188)-- 算法导论14.1 5题

    五.用go语言,给定 n 个元素的顺序统计树中的一个元素 x 和一个自然数 i ,如何在O(lgn)的时间内确定工在该树线性序中的第 i 个后继? 文心一言,代码正常运行: 在顺序统计树(也称为平衡二 ...