Найти тему
etrivia

CH32V003xxxx Разбираемся с GPIO. Часть 2.1

Ссылка на предыдущую часть "Микроконтроллеры почти задаром?"

На всякий случай уточню, что всё описанное ниже всего лишь моё понимание данной темы, возможно не совсем верное и ни в коем случае не является учебником. Написано для того чтобы лучше вникнуть самому и оставить заметки для себя же. Но если это окажется полезным ещё кому то, я буду только рад :)

Ещё один момент, дзен совершенно не приспособлен для публикации исходников. Не поддерживается тег <code> и ширина колонки всего 69 символов. Поэтому такое кривое форматирование. Но лучше так чем совсем никак.

В прошлой статье поудивлялись цене китайских микроконтроллеров и прошили первую программу. Теперь будем мигать светодиодами. Сохраним проект который у нас уже есть из первой статьи, просто добавим в него работу с GPIO.

Производитель стартового комплекта вместе со схемой платы предлагает исходники примеров работы со всей периферией микроконтроллера. На всякий случай я сохранил этот архив у себя CH32V003EVT.ZIP. Распаковываем. Идём по пути \EVT\EXAM\ и смотрим неплохой надо сказать подарок от производителя.

Примеры.
Примеры.

Мы будем мигать светодиодом, поэтому идём дальше в папку \EVT\EXAM\GPIO\GPIO_Toggle\User\ и открываем любым текстовым редактором файл main.c из него нам нужна функция инициализации GPIO

/*********************************************************************
* @fn GPIO_Toggle_INIT
* @brief Initializes GPIOA.0
* @return none
*/
void GPIO_Toggle_INIT(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOD, &GPIO_InitStructure);
}
Копируем её в наш исходник, можно вставить перед функцией инициализации UART. Также надо доработать функцию main(). Нужно вызвать функцию которую мы добавили, чтобы объяснить ножке контроллера, что она порт вывода. Перед вызовом функции настройки UART пишем вызов этой функции. Вот так

GPIO_Toggle_INIT();
USARTx_CFG();

В главный цикл нужно добавить вызов функции управляющей ножкой к которой подключен светодиод, подающей на эту ножку либо ноль, либо единицу. Копируем эту строчку опять же из примера

GPIO_WriteBit(GPIOD, GPIO_Pin_0, (i == 0) ? (i = Bit_SET) : (i = Bit_RESET));

Добавить её надо в начале цикла while(1) сразу за открывающей скобкой. И ещё надо объявить переменную i. Состояние этой переменной 1/0 будет записываться в порт к которому подключен светодиод и соответственно светодиод будет либо светится либо нет

u8 i = 0;.

Вписать эту строчку надо в начале функции main() сразу за открывающей скобкой.

Компилируем, прошиваем, подключаемся терминалом. Стучим по клавиатуре и смотрим как на каждый отправленный микроконтроллеру символ светодиод переключается, загорается и гаснет.

По традиции добавим что-нибудь от себя. На плате два светодиода, сделаем чтобы мигали по очереди. За строкой управляющей светодиодом добавляем ещё одну такую же, но вместо пина 0 указываем пин 4 и переменную i заменяем там на j. Вот так.

GPIO_WriteBit(GPIOD, GPIO_Pin_0, (i == 0) ? (i = Bit_SET) : (i = Bit_RESET));
GPIO_WriteBit(GPIOD, GPIO_Pin_4, (j == 0) ? (j = Bit_SET) : (j = Bit_RESET));

Объявляем эту переменную j рядом с i. Только инициализируем её не нулём, а единичкой.

u8 i = 0;
u8 j = 1;

И разумеется этот пин 4 надо добавить в функцию инициализации порта. Ищем в функции void GPIO_Toggle_INIT(void) строчку
GPIO_InitStructure.GPIO_Pin= GPIO_Pin_0; и дописываем в неё инициализацию пина 4, вот так GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_4;. Теперь при отправке символа в UART микроконтроллера светодиоды будут перемигиваться.

В итоге содержимое файла main.c в нашем проекте должно выглядеть так:

/********************************** (C) COPYRIGHT *******************************
* File Name : main.c
* Author : WCH
* Version : V1.0.0
* Date : 2022/08/08
* Description : Main program body.
*********************************************************************************
* Copyright (c) 2021 Nanjing Qinheng Microelectronics Co., Ltd.
* Attention: This software (modified or not) and binary are used for
* microcontroller manufactured by Nanjing Qinheng Microelectronics.
*******************************************************************************/
#include "debug.h"
/* Global define */ /* Global Variable */ vu8 val;

/*********************************************************************
* @fn GPIO_Toggle_INIT
* @brief Initializes GPIOA.0 * @return none
*/

void GPIO_Toggle_INIT(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOD, &GPIO_InitStructure); }
/*
*@Note
*Multiprocessor communication mode routine:
*Master:USART1_Tx(PD5)\USART1_Rx(PD6).
*This routine demonstrates that USART1 receives the data sent by CH341 and inverts
*it and sends it (baud rate 115200).
*
*Hardware connection:PD5 -- Rx
* PD6 -- Tx
*
*/

/*********************************************************************
* @fn USARTx_CFG
* @brief Initializes the USART2 & USART3 peripheral.
* @return none
*/
void USARTx_CFG(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
USART_InitTypeDef USART_InitStructure = {0};
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_USART1, ENABLE);
/* USART1 TX-->D.5 RX-->D.6 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOD, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOD, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl =
USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
}

/*********************************************************************
* @fn main
* @brief Main program.
* @return none
*/
int main(void)
{
u8 i = 0;
u8 j = 1;

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(115200);
printf("SystemClk:%d\r\n",SystemCoreClock);
printf( "ChipID:%08x\r\n", DBGMCU_GetCHIPID() );
USARTx_CFG();
GPIO_Toggle_INIT();
while(1)
{
GPIO_WriteBit(GPIOD, GPIO_Pin_0, (i == 0) ? (i = Bit_SET) : (i = Bit_RESET));
GPIO_WriteBit(GPIOD, GPIO_Pin_4, (j == 0) ? (j = Bit_SET) : (j = Bit_RESET));

while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET)
{
/* waiting for receiving finish */
}
val = (USART_ReceiveData(USART1));
USART_SendData(USART1, val);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET)
{
/* waiting for sending finish */
}
}
}

Теперь немного о печальном. Давайте посмотрим на содержимое нашего файла main.c. Что мы там видим? А видим мы там лютую дичь, трешь и угар, Содом и Гоморру. У любого программиста который занимался микроконтроллерами, особенно мелким, глядя на на это безобразие из глаз потечёт кровь. Я верю в то, что программисты производителя хотели как лучше, но получилось то, что получилось.

Чтобы более наглядно увидеть смысл дальнейших действий, скомпилируйте этот ужас и куда то отдельно выпишите строку со сводкой о использованных ресурсах контроллера.

text data bss dec hex filename
3644 56 268 3968 f80 CH32V003F4P6.elf

Это пригодится потом для сравнения.

Начнём с самого простого и того, что только что добавили, с портов ввода вывода (GPIO). А именно с функции которая их инициализирует. Первая строчка

GPIO_InitTypeDef GPIO_InitStructure = {0};

Зачем? Зачем для простейшей инициализации портов нужно вводить структуру? Просто удаляем эту строчку. Дальше идёт

  1. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);

Смысл этой функции в том, что в этих микроконтроллерах, тактирование всей периферии отключаемое и включить его, а фактически включить периферийный модуль задача программиста. Всего за включение тактирования периферии отвечают три регистра.

R32_RCC_AHBPCENR 0x40021014 AHB peripheral clock enable register
R32_RCC_APB2PCENR 0x40021018 APB2 peripheral clock enable register
R32_RCC_APB1PCENR 0x4002101C APB1 peripheral clock enable register

В данный момент нас интересует регистр R32_RCC_APB2PCENR именно он отвечает за включение тактирования порта D. Смотрим в даташите пункт 3.4.7 APB2 Peripheral Clock Enable Register (RCC_APB2PCENR).

RCC_APB2PCENR
RCC_APB2PCENR

В правой колонке видим, что после сброса бит IOPDEN выключен. Значит надо его включить. Включаем.

RCC->APB2PCENR|=RCC_IOPDEN; // Включаем тактирование порта D.

Спрашивается, ради этого нужно было городить целую функцию о существовании которой программист должен ещё знать? Вместо того чтобы посмотреть в даташит и установить бит в нужном регистре. Дальше идёт заполнение безумной структуры

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

Поскольку объявление самой структуры мы уже удалили то эти строчки следуют за ней в мусор. Осталась последняя строчка.

GPIO_Init(GPIOD, &GPIO_InitStructure);

Вот собственно и инициализация порта. Тут будет немного сложнее. Для работы с GPIO есть аж 18 регистров.

GPIO Register Description
GPIO Register Description

Нас в данный момент интересует регистр GPIOD_CFGLR, именно он отвечает за конфигурацию порта D. Смотрим в документации пункт
7.3.1.1 Port Configuration Register Low (GPIOx_CFGLR) (x=A/C/D).

GPIOx_CFGLR
GPIOx_CFGLR

Разбираемся. Порт 8 битный. За настройку каждой ножки отвечают 4 бита. Из них два бита MODEy отвечают за то является ножка входом или выходом и если выходом то с какой частотой этой ножкой можно дрыгать. Два бита CNFу устанавливают как именно этот вход или выход будет работать, "y" в конце имён этих регистров это переменная обозначающая номер бита порта. Начнём с MODE

  • 00: Ножка является входом.
  • 01: Выход. Максимальная скорость переключения 10 мегагерц.
  • 10: Выход. Максимальная скорость переключения 2 мегагерц.
  • 11: Выход. Максимальная скорость переключения 50 мегагерц.

Немного подробнее о частоте переключения. Сначала я подумал, что это подключаются какие то делители, ограничивающие скорость работы. Но реальность оказалась гораздо интереснее. На самом деле это регулируется скорость нарастания фронтов и убывания спадов импульсов. Ну или угол наклона фронтов и спадов. Кому как удобнее. Соответственно если скорость нарастания маленькая, то быстро ножкой дёргать не получится. Зато при маленькой скорости изменения уровня микроконтроллер создаёт меньше помех.

Теперь про CNF.

Если ножка настроена как вход,

  • 00: Аналоговый вход
  • 01: Плавающий вход. Обычный цифровой вход без подтягивающих резисторов. Важно, что оставлять так настроенную ножку никуда не подключенной нельзя.
  • 10: Цифровой вход подтянутый внутренними резисторами вверх или вниз. Направление подтяжки тоже можно выбрать, но это отдельная тема, когда будем разбираться как подключать кнопки.

Если ножка настроена как выход.

  • 00: Обычный push-pull выход. На ножке либо напряжение питания, либо она притянута к земле.
  • 01 : Выход с открытым коллектором. Работает только нижний транзистор и ножка подтягивается только к земле. Иногда это бывает очень удобно. Например популярные интерфейсы i2c и 1wire реализуется с помощью именного таких выходов.
  • 10: Тоже самое что и 00 но управлять состоянием ножки напрямую мы уже не сможем, этим будет заниматься какой либо из периферийных модулей. Например UART.
  • 11: Тоже, что и 10 но с открытым коллектором.

Теперь смотрим в правую колонку описания регистра и видим, что после сброса все CNFy равны 01, а не 00. При инициализации проще всего их обнулить, а потом записать нужные значения.

Важно!!! Не стоит упрощать себе жизнь и пытаться обнулить весь регистр целиком. Это превратит микроконтроллер в тыкву. Я проверял :) Пришлось сдувать микросхему и запаивать новую. Происходит это потому, что к пину 1 порта D подключается программатор, а при обнулении всего регистра этот пин из цифрового превращается в аналоговый вход. В результате программатор больше не видит этот микроконтроллер.

Разобрались что к чему. Можно писать инициализацию портов.

// Предварительно очищаем место для настройки.
GPIOD->CFGLR &= ~((0xF)|(0xF << 16)); // Настраиваем порты.
GPIOD->CFGLR |= (0x3 | (0x3 << 16));

Всё? Ну принципе можно оставить и так, будет работать, но проблема в том, что получившееся совершенно не читабельно, и уже через неделю глядя в собственный исходник, мы будем мучительно вспоминать о чём вообще тут идёт речь. И кстати совсем не факт, что вспомним. Проще буде переписать заново.

Первым делом пишем определения (дефайны), чтобы избавиться от "магических" чисел

// Шаблон для очистки.
#define PORT_CLEAR 0xF //
// Выбираем выход или вход.
#define INP_GPIO 0x0 // Input mode.
#define OUT_F10MHZ 0x1 // Output mode, maximum speed 10MHz.
#define OUT_F2MHZ 0x2 // Output mode, maximum speed 2MHz.
#define OUT_F50MHZ 0x3 // Output mode, maximum speed 50MHz.
// Если предыдущим выбором ножка сконфигурирована как выход.
// Выбираем тип выхода
#define OUT_PUSH_PULL 0x0 // Universal push-pull output mode.
#define OUT_OPEN_DRAIN 0x4 // Universal open-drain output mode.
#define OUT_MULTIPLEXED_PUSH_PULL 0x8 // Multiplexed function push-pull
// output mode.
#define OUT_MULTIPLEXED_OPEN_DRAIN 0xC // Multiplexing function open-drain
// output mode
// Если предыдущим выбором ножка сконфигурирована как вход.
// Выбираем тип входа
#define INP_ANALOG 0x0 // Analog input mode.
#define INP_FLOATING 0x4 // Floating input mode.
#define INP_UP_DOWN 0x8 // With pull-up and pull-down mode.
#define INP_RESERVED 0xC // Reserved.
// Сдвиги.
#define INIT_SHIFT_PIN_0 0x0 // Сдвиг для пина 0. //|
#define INIT_SHIFT_PIN_1 0x4 // Сдвиг для пина 1. //|
#define INIT_SHIFT_PIN_2 0x8 // Сдвиг для пина 2. //|
#define INIT_SHIFT_PIN_3 0xC // Сдвиг для пина 3. //|
#define INIT_SHIFT_PIN_4 0x10 // Сдвиг для пина 4. //|
#define INIT_SHIFT_PIN_5 0x14 // Сдвиг для пина 5. //|
#define INIT_SHIFT_PIN_6 0x18 // Сдвиг для пина 6. //|
#define INIT_SHIFT_PIN_7 0x1C // Сдвиг для пина 7. //|

И переписываем функцию с имеющимися определениями.

void GPIO_Toggle_INIT(void)
{
// Включаем тактирование периферии.
RCC->APB2PCENR|=RCC_IOPDEN;
// Предварительно очищаем место для настройки.
GPIOD->CFGLR &= ~(
(PORT_CLEAR << INIT_SHIFT_PIN_0)| // Led 1
(PORT_CLEAR << INIT_SHIFT_PIN_4) // Led 2
);
// Настраиваем порты.
GPIOD->CFGLR |= (
((OUT_F50MHZ | OUT_PUSH_PULL) << INIT_SHIFT_PIN_0)| // Led 1
((OUT_F50MHZ | OUT_PUSH_PULL) << INIT_SHIFT_PIN_4) // Led 2
);
}


Теперь всё читабельно и красиво. Можно легко добавить ножки которые надо инициализировать.

Поехали дальше, смотрим функцию конфигурирования UART
void USARTx_CFG(void) и видим набор знакомых строчек с инициализацией GPIO.

GPIO_InitTypeDef GPIO_InitStructure = {0};
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_USART1, ENABLE);
/* USART1 TX-->D.5 RX-->D.6 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOD, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOD, &GPIO_InitStructure);

Удаляем весь этот мусор, и переписываем нашу функцию инициализации с учётом тактирования UART и включения пятого и шестого GPIO

void GPIO_Toggle_INIT(void)
{
// Включаем тактирование периферии.
RCC->APB2PCENR|=RCC_IOPDEN| // Порт D.
RCC_USART1EN; // USART1.
// Предварительно очищаем место для настройки порта D.
GPIOD->CFGLR &= ~(
(PORT_CLEAR << INIT_SHIFT_PIN_0)| // LED1
(PORT_CLEAR << INIT_SHIFT_PIN_4)| // LED2
(PORT_CLEAR << INIT_SHIFT_PIN_5)| // USART1 TX
(PORT_CLEAR << INIT_SHIFT_PIN_6) // USART1 RX
);
// Настраиваем порт D.
GPIOD->CFGLR |= (
((OUT_F50MHZ | OUT_PUSH_PULL )<<INIT_SHIFT_PIN_0)| // LED1
((OUT_F50MHZ | OUT_PUSH_PULL )<<INIT_SHIFT_PIN_4)| // LED2
((OUT_F50MHZ|OUT_MULTIPLEXED_PUSH_PULL)<<INIT_SHIFT_PIN_5)|//USART1 TX
((INP_GPIO |INP_FLOATING )<<INIT_SHIFT_PIN_6)//USART1 RX
);
}

На последок смотрим строчку из примера, с помощью которой нам предлагают мигать светодиодом
GPIO_WriteBit(GPIOD, GPIO_Pin_0, (i == 0) ? (i = Bit_SET) : (i = Bit_RESET)); В функцию GPIO_WriteBit() в качестве параметров передаётся порт, номер GPIO который нужно установить и переменная в которой хранится текущее состояние этого GPIO. Кроме того зачем то используется тернарная операция вместо того чтобы просто инвертировать бит. Микроконтроллер сам по себе знает в каком состоянии у него ножки. Хранится эта информация в регистре GPIOD_OUTDR. Смотрим в даташите раздел
7.3.1.3 Port Output Register (GPIOx_OUTDR) (x=A/C/D)

GPIOx_OUTDR
GPIOx_OUTDR

Надо отметить, что у этого регистра двоякое назначение, если ножка сконфигурирована как выход, то в нем хранится состояние этой ножки. А если эта ножка вход с подтяжкой (with pull-up and pull-down mode) то тут задаётся направление подтяжки (0: Drop-down input. 1: Pull-up input). Смотрим правую колонку - по умолчанию ноль, значит ножка подтянута к земле. Но об этом в следующей статье.

Переписываем управление светодиодами с учётом вышеизложенного.

GPIO_WriteBit(GPIOD, GPIO_Pin_0, (GPIOD->OUTDR & GPIO_Pin_0) ^ 1);
GPIO_WriteBit(GPIOD, GPIO_Pin_4, (GPIOD->OUTDR & GPIO_Pin_0) ^ 1);

Не забываем удалить переменные i и j. Не нужны они больше.

Жмём "Rebuild" и сравниваем результат сборки с тем что ранее записали.

text data bss dec hex filename
3644 56 268
3968 f80 CH32V003F4P6.elf 3548 56 268 3872 f20 CH32V003F4P6.elf

В колонке dec видим, что организовав правильную работу всего одного периферийного модуля не только получили внятный текст исходника но и сэкономили 96 байт памяти программ. Вроде немного, но иногда это может оказаться важным.

На этом пока всё. Целиком весь проект можно скачать тут https://www.romram.ru/dzen/ch32v003_2_1.rar

К сожалению не удалось за раз рассмотреть работу всех регистров связанных с GPIO, слишком много текста получается. Продолжим в следующей статье.

Всё что тут написано можно обсудить в комментариях и телеграм канале etrivia.

Посетите наш магазин https://etrivia.ru

#CH32V003 #MounRiver #GPIO