分享一个CW32 IO拓展项目:使用CW32L010做GPIO/ADC 扩展

描述

1. 设计目标

CW32L010是一款极具性价比的ARM Cortex-M0+内核微控制器。其核心优势在于以极致成本,集成了丰富的外设资源:包括一个12位精度的ADC、最多16个可配置的GPIO口以及四个通用定时器

正是基于这些特性,该芯片非常适合用于两类核心应用场景:

功能简单的独立设备,如传感器节点、小家电主控等。

作为主控系统的扩展单元,尤其是IO扩展和模拟信号采集。

当应用于IO/ADC扩展时,CW32L010不再是一颗简单的“胶合逻辑”芯片,而是一个可编程的智能外设。这意味着开发者需要为其独立设计和烧录固件,使其能够按照预设的协议与主控制器进行协同工作。

本项目(固件设计)当前阶段的目标,正是实现这样一个智能扩展模块的雏形。目前已完成的核心功能是:

基于串口(UART)的通信协议,确保与主控稳定、高效的数据交换。

完整的GPIO控制能力,支持引脚模式动态配置(输入/输出、上拉/下拉等)和状态读写。

ADC采样功能,可通过命令对指定通道进行模拟量采集并返回数据。

下一阶段的开发路线图已明确规划,将重点增加以下功能:

I²C从机接口扩展:将CW32L010本身模拟为一个I²C从设备,为仅具备I²C接口的主控提供GPIO和ADC扩展能力,增加应用灵活性。

PWM输出功能:充分利用其定时器资源,实现多通道、可调频率与占空比的PWM信号生成,用于控制LED亮度、电机速度或生成特定波形。

2. 使用电路图

隔离IO/ADC扩展
 

CW32

高端MOS控制,高端电流采样。
 

CW32

PCB

CW32

3. 通信协议介绍

本系统通过 UART 串口(波特率 115200)控制微控制器的 GPIO 引脚,支持模式配置、状态读写、ADC 采集及中断上报功能。

3.1 支持的IO及功能

系统上电后,所有支持的 IO 默认初始化为 输入模式

端口 (Port) 引脚 (Pin) 支持的功能 备注
Port A (0x00) PA02 输入 / 输出 / 中断 / ADC (CH2)  
  PA03 输入 / 输出 / 中断 / ADC (CH3)  
  PA04 输入 / 输出 / 中断 / ADC (CH4)  
  PA05 输入 / 输出 / 中断 / ADC (CH5)  
  PA06 输入 / 输出 / 中断 / ADC (CH6)  
Port B (0x01) PB00 输入 / 输出 / 中断 / ADC (CH7)  
  PB01 输入 / 输出 / 中断 / ADC (CH8)  
  PB02 输入 / 输出 / 中断 / ADC (CH9)  
  PB03 输入 / 输出 / 中断 / ADC (CH10)  
  PB04 输入 / 输出 / 中断 / ADC (CH11)  
  PB05 输入 / 输出 / 中断 / ADC (CH12)  
  PB06 输入 / 输出 / 中断 / ADC (CH13)  

注意: PA00 和 PA01 为串口通信引脚,禁止用于其他功能。

3.2串口通信协议

通信定义

波特率: 115200

数据格式: 4 字节固定长度 HEX 帧

帧结构: [命令码] [端口] [引脚] [参数]

端口定义

0x00: Port A

0x01: Port B

引脚定义

0x00 ~ 0x0F: 对应 Pin 0 ~ Pin 15

启动消息

设备上电或重启完成后,会自动发送以下 HEX 序列:

HEX: 52 45 41 44 (ASCII: "READ")

3. 3 命令详解

设置 IO 模式 (Set Mode)

发送: 01 00 02 03

接收: 01 00 02 01

发送: 01 01 00 01

接收: 01 01 00 01

参数 [Mode]:

0x00: 输入模式 (Input) - 默认

0x01: 输出模式 (Output Push-Pull)

0x02: 模拟模式 (Analog) - 用于 ADC

0x03: 中断模式 (Interrupt) - 双边沿触发

配置指定 IO 的工作模式。

命令码: 0x01

格式: 01 [Port] [Pin] [Mode]

成功响应: 01 [Port] [Pin] 01

示例: 将 PB00 设置为输出模式

示例: 将 PA02 设置为中断模式

读取 IO 电平 (Read Pin)

[State]: 00 (低电平) / 01 (高电平)

读取指定 IO 当前的电平状态。

命令码: 0x02

格式: 02 [Port] [Pin] 00 (末尾字节无效,补0即可)

响应: 02 [Port] [Pin] [State]

示例: 读取 PB00 电平

发送: 02 01 00 00

接收: 02 01 00 01 (当前为高电平)

3.4 设置 IO 电平 (Write Pin)

控制指定 IO 输出高低电平(需先配置为输出模式)。

0x00: 输出低电平

0x01: 输出高电平

命令码: 0x03

格式: 03 [Port] [Pin] [Value]

参数 [Value]:

响应: 03 [Port] [Pin] 01

示例: 设置 PB00 输出高电平

发送: 03 01 00 01

接收: 03 01 00 01

读取 ADC 值 (Read ADC)

发送: 04 00 04 00

接收: 04 0A 23 00

计算: 0x0A23 = 2595

命令码: 0x04

格式: 04 [Port] [Pin] 00

响应: 04 [High Byte] [Low Byte] 00

ADCValue = (High Byte << 8) | Low Byte

读取指定 IO 的 ADC 转换值(需先配置为模拟模式)。

示例: 读取 PA04 的 ADC 值

3.5 中断自动上报 (Interrupt Notify)

接收: 05 01 00 01 (PB00 变为高电平)

接收: 05 01 00 00 (PB00 变为低电平)

命令码: 0x05

格式: 05 [Port] [Pin] [State]

[State]: 中断发生后的当前电平 (00 或 01)

当 IO 配置为中断模式 (0x03) 后,电平发生变化(上升沿或下降沿)时,设备会自动发送此帧。

示例: PB00 电平跳变

3.6 重启设备 (System Reset)

复位微控制器。

发送: 06 00 00 00

接收: 06 01 00 00

格式: 06 00 00 00

响应: 06 01 00 00 (收到响应后设备将立即重启)

命令码: 0x06

示例: 重启设备

4. 实现代码

串口初始化,中断处理

 

volatile uint8_t rx_buffer[UART_RX_BUFFER_SIZE];
volatile uint16_t rx_index = 0;
volatile uint8_t cmd_ready = 0;
/**
 * @brief Initialize the debug UART1
 * @note PA00 RXD AF1  PA01 TXD AF1
 * 
 */
void Debug_Uart_Init(void)
{
    SYSCTRL_AHBPeriphClk_Enable(SYSCTRL_AHB_PERIPH_GPIOA | SYSCTRL_AHB_PERIPH_GPIOB, ENABLE);
    SYSCTRL_APBPeriphClk_Enable1(SYSCTRL_APB1_PERIPH_UART1, ENABLE);
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    GPIO_InitStructure.Pins = GPIO_PIN_0;
    GPIO_InitStructure.Mode = GPIO_MODE_INPUT_PULLUP;
    GPIO_Init(CW_GPIOA, &GPIO_InitStructure);
    GPIO_InitStructure.Pins = GPIO_PIN_1;
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_Init(CW_GPIOA, &GPIO_InitStructure);
    PA01_AFx_UART1TXD();
    PA00_AFx_UART1RXD();
    UART_InitTypeDef UART_InitStructure = {0};
    UART_InitStructure.UART_BaudRate = 115200;
    UART_InitStructure.UART_Source = UART_Source_PCLK;
    UART_InitStructure.UART_UclkFreq = 48000000; // 48MHz
    UART_InitStructure.UART_StopBits = UART_StopBits_1;
    UART_InitStructure.UART_Parity = UART_Parity_No ;
    UART_InitStructure.UART_HardwareFlowControl = UART_HardwareFlowControl_None;
    UART_InitStructure.UART_Mode = UART_Mode_Rx | UART_Mode_Tx;
    UART_Init(CW_UART1, &UART_InitStructure);
    // Enable UART RX Interrupt
    UART_ITConfig(CW_UART1, UART_IT_RC, ENABLE);
    NVIC_EnableIRQ(UART1_IRQn);
    // Enable ADC Clock
    SYSCTRL_APBPeriphClk_Enable1(SYSCTRL_APB1_PERIPH_ADC, ENABLE);
}
int fputc(int ch, FILE *f)
{
    UART_SendData_8bit(CW_UART1, (uint8_t)ch);
    while (UART_GetFlagStatus(CW_UART1, UART_FLAG_TXE) == RESET);
    return ch;
}
void UART1_RxCallback(uint8_t data)
{
    if (rx_index < UART_RX_BUFFER_SIZE)
    {
        rx_buffer[rx_index++] = data;
        if (rx_index >= UART_RX_BUFFER_SIZE)
        {
            cmd_ready = 1;
        }
    }
}
/**
 * @brief This funcation handles UART1
 */
void UART1_IRQHandler(void)
{
    /* USER CODE BEGIN */
    if(UART_GetITStatus(CW_UART1, UART_IT_RC) != RESET)
    {
        uint8_t data = UART_ReceiveData_8bit(CW_UART1);
        UART_ClearITPendingBit(CW_UART1, UART_IT_RC);
        UART1_RxCallback(data);
    }
    /* USER CODE END */
}

 

2.循环读取命令

 

while (1)
{
    Process_UART_Command();
}
void Process_UART_Command(void)
{
    if (cmd_ready)
    {
        uint8_t cmd = rx_buffer[0];
        uint8_t port = rx_buffer[1];
        uint8_t pin = rx_buffer[2];
        uint8_t param = rx_buffer[3];
        switch (cmd)
        {
            case CMD_SET_MODE:
                Cmd_SetMode(port, pin, param);
                break;
            case CMD_READ_PIN:
                Cmd_Read(port, pin);
                break;
            case CMD_WRITE_PIN:
                Cmd_Write(port, pin, param);
                break;
            case CMD_READ_ADC:
                Cmd_Read_ADC(port, pin);
                break;
            case CMD_RESET:
                Cmd_Reset();
                break;
            default:
                // Unknown command
                break;
        }
        // Reset buffer
        rx_index = 0;
        cmd_ready = 0;
        memset((void*)rx_buffer, 0, UART_RX_BUFFER_SIZE);
    }
}

 

3.0x01 配置指定 IO 的工作模式函数

 

void Cmd_SetMode(uint8_t port_idx, uint8_t pin, uint8_t mode)
{
    // Validate Pin
    if (!Is_Pin_Valid(port_idx, pin)) return;
    GPIO_TypeDef* gpio_port = Get_GPIO_Port(port_idx);
    uint16_t gpio_pin = Get_GPIO_Pin(pin);
    if (gpio_port == NULL) return;
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    GPIO_InitStructure.Pins = gpio_pin;
    if (mode == 0x01) // Output
    {
        GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
    }
    else if (mode == 0x02) // Analog
    {
        GPIO_InitStructure.Mode = GPIO_MODE_ANALOG;
    }
    else if (mode == 0x03) // Interrupt (Double Edge)
    {
        GPIO_InitStructure.Mode = GPIO_MODE_INPUT; // Standard Input
        GPIO_InitStructure.IT = GPIO_IT_RISING | GPIO_IT_FALLING;
        // Enable NVIC for the port
        if (port_idx == PORT_A)
        {
            NVIC_EnableIRQ(GPIOA_IRQn);
        }
        else if (port_idx == PORT_B)
        {
            NVIC_EnableIRQ(GPIOB_IRQn);
        }
    }
    else // Input (Default to 0x00)
    {
        GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
    }
    GPIO_Init(gpio_port, &GPIO_InitStructure);
    // Return success: [CMD] [PORT] [PIN] [0x01]
    printf("%c%c%c%c", CMD_SET_MODE, port_idx, pin, 0x01);
}

 

4.0x02 读取指定 IO 当前的电平状态。

 

void Cmd_Read(uint8_t port_idx, uint8_t pin)
{
    // Validate Pin
    if (!Is_Pin_Valid(port_idx, pin)) return;
    GPIO_TypeDef* gpio_port = Get_GPIO_Port(port_idx);
    uint16_t gpio_pin = Get_GPIO_Pin(pin);
    if (gpio_port == NULL) return;
    uint8_t state = GPIO_ReadPin(gpio_port, gpio_pin);
    // Return format: [CMD] [PORT] [PIN] [STATE]
    printf("%c%c%c%c", CMD_READ_PIN, port_idx, pin, state);
}

 

5.0x03控制指定 IO 输出高低电平(需先配置为输出模式)。

 

void Cmd_Write(uint8_t port_idx, uint8_t pin, uint8_t value)
{
    // Validate Pin
    if (!Is_Pin_Valid(port_idx, pin)) return;
    GPIO_TypeDef* gpio_port = Get_GPIO_Port(port_idx);
    uint16_t gpio_pin = Get_GPIO_Pin(pin);
    if (gpio_port == NULL) return;
    GPIO_WritePin(gpio_port, gpio_pin, (value ? GPIO_Pin_SET : GPIO_Pin_RESET));
    // Return success: [CMD] [PORT] [PIN] [0x01]
    printf("%c%c%c%c", CMD_WRITE_PIN, port_idx, pin, 0x01);
}

 

6.0x04读取指定 IO 的 ADC 转换值(需先配置为模拟模式)。

 

uint32_t Get_ADC_Channel(uint8_t port_idx, uint8_t pin)
{
    // Mapping based on CW32L010 datasheet/header
    // PA00 - > ADC_IN0 ... PA07 - > ADC_IN7 (Note: PA07 is not available on all packages, check specific map)
    // Actually from cw32l010_adc.h comments:
    // PA00- >CH0, PA01- >CH1, PA02- >CH2, PA03- >CH3, PA04- >CH4, PA05- >CH5, PA06- >CH6
    // PB00- >CH7, PB01- >CH8, PB02- >CH9, PB03- >CH10, PB04- >CH11, PB05- >CH12, PB06- >CH13
    if (port_idx == PORT_A)
    {
        if (pin <= 6) return (uint32_t)pin; // CH0-CH6
    }
    else if (port_idx == PORT_B)
    {
        if (pin <= 6) return (uint32_t)(pin + 7); // CH7-CH13
    }
    return 0xFFFFFFFF; // Invalid
}
void Cmd_Read_ADC(uint8_t port_idx, uint8_t pin)
{
    // Validate Pin
    if (!Is_Pin_Valid(port_idx, pin)) return;
    uint32_t adc_ch = Get_ADC_Channel(port_idx, pin);
    if (adc_ch == 0xFFFFFFFF) return;
    ADC_InitTypeDef ADC_InitStructure = {0};
    ADC_InitStructure.ADC_ClkDiv = ADC_Clk_Div4;
    ADC_InitStructure.ADC_ConvertMode = ADC_ConvertMode_Once;
    ADC_InitStructure.ADC_SQREns = ADC_SqrEns0to0;
    ADC_InitStructure.ADC_IN0.ADC_InputChannel = adc_ch;
    ADC_InitStructure.ADC_IN0.ADC_SampTime = ADC_SampTime12Clk;
    ADC_Init(&ADC_InitStructure);
    ADC_Enable();
    ADC_SoftwareStartConvCmd(ENABLE);
    // Wait for conversion (simple polling with timeout)
    uint32_t timeout = 10000;
    while (timeout--)
    {
        if (ADC_GetITStatus(ADC_IT_EOC) != RESET)
        {
            ADC_ClearITPendingBit(ADC_IT_EOC);
            break;
        }
    }
    uint16_t result = ADC_GetConversionValue(0); // Get result from SQR0
    ADC_Disable();
    // Return: [CMD] [High Byte] [Low Byte] [0x00]
    printf("%c%c%c%c", CMD_READ_ADC, (uint8_t)(result > > 8), (uint8_t)(result & 0xFF), 0x00);
}

 

7.0x05当 IO 配置为中断模式 (0x03) 后,电平发生变化(上升沿或下降沿)时,设备会自动发送此帧。

 

/**
 * @brief This funcation handles GPIOA
 */
void GPIOA_IRQHandler(void)
{
    /* USER CODE BEGIN */
    GPIO_ISR_Handler(PORT_A);
    /* USER CODE END */
}
/**
 * @brief This funcation handles GPIOB
 */
void GPIOB_IRQHandler(void)
{
    /* USER CODE BEGIN */
    GPIO_ISR_Handler(PORT_B);
    /* USER CODE END */
}
void GPIO_ISR_Handler(uint8_t port_idx)
{
    GPIO_TypeDef* gpio_port = Get_GPIO_Port(port_idx);
    if (gpio_port == NULL) return;
    uint16_t isr = gpio_port- >ISR;
    for (uint8_t i = 0; i < 16; i++)
    {
        // Check validity first
        if (!Is_Pin_Valid(port_idx, i)) continue;
        uint16_t pin_mask = (1 < < i);
        if (isr & pin_mask)
        {
            // Clear interrupt flag
            gpio_port- >ICR = ~pin_mask;
            // Read current state
            uint8_t state = (gpio_port- >IDR & pin_mask) ? 1 : 0;
            // Send notification: [CMD_IT_NOTIFY] [PORT] [PIN] [STATE]
            printf("%c%c%c%c", CMD_IT_NOTIFY, port_idx, i, state);
        }
    }
}

 

8.0x06 复位微控制器。

 

void Cmd_Reset(void)
{
    // Send acknowledgement: [CMD_RESET] [0x01] [0x00] [0x00]
    printf("%c%c%c%c", CMD_RESET, 0x01, 0x00, 0x00);
    // Wait for UART transmission
    delay_ms(50);
    // Reset System
    NVIC_SystemReset();
}

 

5. 完整代码

完成代码请访问仓库:
https://gitee.com/aotengyang/open-source-code

固件使用 VScode EIDE 插件进行开发,如需使用keil进行编译,需要将依赖的文件添加。

审核编辑 黄宇

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分