Реклама ⓘ
Главная » Микроконтроллеры
Призовой фонд
на апрель 2024 г.
1. 100 руб.
От пользователей

Похожие статьи:


Реклама ⓘ

PSoC. Глава 3. DAC и UART RX линия

Си для микроконтроллеров

Для успешного создания собственных устройств на микроконтроллерах (да и вообще), нужно обладать достаточно широким кругом базовых знаний и умений. Например, нужно как минимум умение программировать, паять, разбираться в электрических схемах. Желательно знание английского. С паянием у меня самого не очень. Английскому на сайте cxem.net я тоже учить вас не стану. Остается только программуха. Большая часть МК программируется на Си. Почему именно Си? Ввиду своего минимализма. С++ и C# выросли из Cи потому что, людям требовалось создавать большие проекты. Где без определённого абстрагирования от деталей ( таких как память, байты, биты ) ну просто не обойтись. В нашем же деле быстродействие и минимализм подходят лучше всего. Я постараюсь задеть только те аспекты языка, которые необходимы для программирования микроконтроллеров.

Во-первых это бесконечные циклы. Суть цикла том, что код внутри выполнять определённое количество раз, в зависимости от условия. Например:

for(int i = 0; i<100500; i++)
{
    //какой-то код
}

Здесь "какой-то код" выполниться 100500 раз. Тот же цикл можно переписать так:

int i = 0;
while(i<100500)
{
    //какой-то код
    i++;                  //тоже самое что и i = i + 1;
}

Часто для программирования м-к нужно что бы цикл выполнялся бесконечно, то есть с того момента как выполниться вход в цикл, до выключения питания. Для этого в заголовке цикла стоит написать условие, которое никогда не станет ложно. Например:

while( 2>1 )
{
    //какой-то код
}

Здесь "какой-то код" будет выполняться, пока микроконтроллер будет подключён к питанию. Или пока его не разбить молотком. Условные операторы ">" "<" "==" "!=". Возвращают значение 0, если условие ложно и 1, если - истинно. А значение внутри круглых скобок уже сравнивается с нулём. И принимается решение о том, выполнять цикл далее или прервать. Из этого следует, что наш цикл можно упростить ещё и до такого:

while( 1 )
{
    //какой-то код
}

Типы данных. Что бы там не говорили преподаватели. Типы данных отличаются только размером занимаемой памяти( и некоторой интерпретацией компилятора ). Говоря о микроконтроллерах, размеры типов для Си тяжело назвать точно. Потому что, это зависит от целевой платформы. Но всегда можно сделать sizeof(тип); и получить размер типа в байтах.
Каждый байт состоит из 8 бит. В Си мы можем варьировать 10, 16-ричными значениями значениями. Если мы хотим присвоить переменной значение 12 (десятичных) то нам стоит написать так:

i = 12;

Если мы хотим записать 16-тиричное значение, то перед числом стоит написать "0x". Например:

i = 0xFC;

То, что написано после "0x" называется - хекс байтом. Один байт, записанный двумя 16-тиричными числами.
Побитовые операции. Иногда бывает нужно, проверить является ли n-й бит в переменной единицей. Для этого в Cи существуют побитовые операции. "&" "|" "~" "<<" ">>". Непосредственно саму проверку можно провести так:

 if((i >> n) & 1)

Где i – переменная у которой будем проверять биты. n – порядковый номер бита. Биты в переменных отсчитываются с лева на право.

Указатель. Суть в том, что программист получает возможность маневрировать не самими данными, а указателем на память с этими данными. То, что я только что написал, наверное, малопонятно, и я попытаюсь объяснить на примере. Допустим я написал некоторую функцию, у который один входной параметр типа int и возвращаемое значение тоже типа int. Прототип функции будет выглядить так:

int func(int input);

В функцию я хочу передать значение int in; a получать значение int out;. Тогда наш код будет выглядеть примерно так:

int out;
int in = 100500;
out = func(in);

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

То, что мы видим на скрине, это и есть память. Это может быть память микроконтроллера, компьютера. Чего угодно, у того у чего есть память. В первой колонке мы видим смешение байтов. Это как бы их адреса. Во второй - сами байты, в Hex виде. В третей колонке - их интерпретация в ACSII символах. Я неспроста выделил 4ре байта. А выделил я их, потому что один int занимает 4 байт. Допустим, где-нибудь в программном коде мы написали: int i = 0х64; тогда компилятору придётся выделить 4-ре байта в свободной памяти и записать в них значение 0х64. Если так случилось что свободная память была как раз по адресу 0х42. То компилятор может поместить нашу переменную именно в эти ячейки. Память устройства в таком случае будет выглядеть именно так на скриншоте. А суть указателей в том, что они являются не самими данными (00 00 00 64), а указателем в памяти на эти данные (в нашем случае 0x42). Что бы получить указатель на данные, нам надо перед переменной написать символ взятия адреса &. А что бы от указателя перейти к данным - надо разыменовать указатель *.

int i = 64;
int* pointer = &i;    //указатель на данные с i
*pointer = *pointer + 1;    //теперь i = 65

Ближе к делу

Напомню, что на прошлом занятии мы успешно реализовали протокол передачи данных между компом и микроконтроллером. Связь у нас получилась односторонняя. Однако модуль UART может работать и в полнодуплексном режиме. И не требует для этого почти никаких дополнительных реализаций.
Если в предыдущем примере (хотя по сути, это является одним большим примером) мы использовали значение напряжения на потенциометре, для посылки его по UART'ту, то теперь для данных пришедших с компьютера будем использовать один из светодиодов, как индикатор выходного напряжения. Данные, которые приходят с UART'а на МК мы будем посылать на ЦАП. Модель взаимодействия можно представить следующим образом:

DAC (ЦАП)

DAC - Digital-to-analog converter (цифро-аналоговый преобразователь, ЦАП). Прибор, задачей которого является преобразование цифрового сигнала в аналоговый. Противоположную задачу выполняет ADC, аналогово-цифровой преобразователь (АЦП). По основным характеристикам приборы похожи. И у тех и других есть частота дискретизации, рабочий диапазон, количество преобразований в секунду и тд. Это уже частично обсуждалось в Главе 1. Подробную информацию можно получить пройдя по этой ссылке.

Пример 4. Callback. (Часть 1)

Задачи: Принять данные, которые мы будем посылать с компа на микроконтроллер.

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

ОЧЕНЬ важно, что бы input RX был соединён с Port_1_6. Иначе работать не будет. Так как именно этот порт зарезервирован для работы с UART. Если возникнут проблемы с настройкой рабочих частот и битрейта, смотрите главу 2.

Для начала модифицируем функции вывода LCD, для того, что бы отслеживать, не снимаемое напряжение с ADC, а входящие данные на UART. Это очень пригодится при отладке. В самом начале функции main() стоит закомментировать строку вывода "Mesured Voltage" и написать что-нибудь на этот раз более подходящие, например:

	//LCD_PrCString("Measured Voltage");
	LCD_PrCString("Input Voltage");

Вывод измеренных значений внутри бесконечного цикла тоже стоит закомментировать:

	//LCD_Position(1,0);					//установка позиции для вывода
	//LCD_PrString(buf);

При успешном получении данных на блок RX выставляется в единицу флаг UART_RX_REG_FULL. Который, кстати, не так то уж и легко получить. Каждый блок RX и TX, модуля UART, имеют свои буферы, выходы, выходы, регистры сдвига, статуса и управления. Нас же интересует регистр статуса блока RX. Потому, что именно там лежит наш флаг UART_RX_REG_FULL. Регистр можно, в каком то плане сравнить с переменной. Он тоже имеет размер. Только скорость взаимодействия с регистрами на порядок выше чем с памятью. Плюс некоторые биты регистров могут взводиться аппаратно. Что позволяет чипу реагировать на процессы происходящие извне. Что бы достать данные из регистра статуса RX нам понадобиться функция UART_bReadRxStatus(); которая возвращает 8-ми битную переменную. Для этого, где-нибудь в глобальном поле видимости объявляем переменную:

unsigned char rxStatus;

и на каждой итерации бесконечного цикла присваиваем ей значение

rxStatus = UART_bReadRxStatus();

Теперь у нас есть 8ми битная переменная, типа char в которой храниться состояние регистра статуса на момент вызова функции UART_bReadRxStatus();.
В мануалах по модулю UART была найдена вот такая таблица.


из которой видно, что интересующий нас бит находиться на 3-й позиции в регистре статуса. А проверить находится он в состоянии 1 или 0 можно вот такой хитрой строчкой:

if( (rxStatus >> 3) & 1 )

Осталось добавить на схему DAC (Digital-to-analog converter). И выводить значение напряжения, при выполнении вышеописанного условия. Будем использовать DAC8. Он хорошо подходит в виду своей разрядности. Так как одни пакет данных приходящих по RX линии и DAC имеют одну и туже величину 8бит. Их будет очень удобно использовать совместно. Так как нам удастся избежать излишних преобразований. А точности выходного напряжения в 8бит для индикации яркости светодиода нам хватит с головой. Выход DAC'ка надо вывести на шину AnalogOutBus_1. А саму шину вывести на один из свободных портов контроллера, у меня она выведена на Port_0_5.

После того как выше описанные операции были проделаны. Переходим к исходному коду и добавляем следующие строчки.
Во-первых это запуск DAC и его начальное значение:

DAC8_Start(DAC8_HIGHPOWER);
DAC8_WriteBlind(0);

Во-вторых - вывод значения, при успешном его получении.

if( (rxStatus >> 3) & 1 )
{
	inputChar = UART_cReadChar();
	DAC8_WriteBlind(inputChar);
        
}

Нельзя не упомянуть то, что во многих функциях максимальное значение байта является 254, а не 255.
Финальная прошивка микроконтроллера выглядит так:

//----------------------------------------------------------------------------
// C main line
//----------------------------------------------------------------------------

#include         // part specific constants and macros
#include "PSoCAPI.h"    // PSoC API definitions for all User Modules
#include "stdlib.h"

int iResult;
double dResult;
const double scaleFactor = 0.0012315996074583;
const double scaleFactorDAC = 0.0184705882352941;

char* buf;

unsigned char rxStatus;
unsigned char inputChar;
void main(void)
{
	LCD_Start();
	LCD_Position(0,0);
	//LCD_PrCString("Measured Voltage");
	LCD_PrCString("Input Voltage");
	
	DAC8_Start(DAC8_HIGHPOWER);
	DAC8_WriteBlind(0);
	UART_Start(UART_PARITY_NONE); 
	PGA_Start(PGA_HIGHPOWER);			//запуск PGA
	ADCINC12_Start(ADCINC12_HIGHPOWER);	//запуск АЦП
	ADCINC12_GetSamples(0);				//установка АЦП на бесперерывную работу
	M8C_EnableGInt ; // Uncomment this line to enable Global Interrupts
	while(1) 		// главный цикл прошивки
	{
		if (ADCINC12_fIsDataAvailable() != 0)	//проверка на данных в ADC	
		{
			iResult = ADCINC12_iGetData() +2048;	
			ADCINC12_ClearFlag();			
			dResult = iResult * scaleFactor;
			//LCD_Position(1,0);					//установка позиции для вывода
			buf = ftoa(dResult,0);
			//LCD_PrString(buf);
			UART_PutString(buf);
			UART_PutChar('\n');
			//UART_PutCRLF();
		}
		rxStatus = UART_bReadRxStatus();
		if( (rxStatus >> 3) & 1 )
		{
			inputChar = UART_cReadChar();
			DAC8_WriteBlind(inputChar);
			LCD_Position(1,0);
			dResult = inputChar * scaleFactorDAC;
			buf = ftoa(dResult,0);
			LCD_PrString(buf);
		}
	}
}

Теперь собираем проект, загружаем прошивку. Подключаем микроконтроллер, так как показано на фото:

Включаем питание и тестируем его прогой COM Port Toolkit или HyperTerm. Основная область COM Port Tolkit разбита на две части, в правой находятся пришедшие данные на порт, в - левой отправленные. Нажимаем кнопочку редактирование отправляемых данных:

Во всплывающем диалоге вводим значение байта, который мы хотим послать. И нажимаем кнопку send. Тестируем выходное напряжение при разных значениях отправленных байтов.

Пример 4. Callback. *(Часть 2. Interface)

На самом деле принципиальной разницы, на чём писать интерфейс нету. Это может быть Qt если вам нравиться С++. Это может быть Win API, MFC, Java. Если вы не любите интерфейс вообще, можно создать простое консольное приложение.

На нашей форме нам понадобиться ещё пару элементов управления. Это TrackBar, ещё один TextBox и два lablel. Это всё нужно вытянуть на форму и слепить примерно в следующий внешний вид.

Напомню что в Visual Studio во время редактирования формы, справа от рабочего пространства по умолчанию есть панель свойства (properties). В которой есть много чего полезного. Например, можно настроить внешний вид элемент, установить возможность изменения значения элементов, шаг дискретизации нашего TrackBar'а. И ещё много чего интересного. Кроме свойств, у каждого элемента формы, есть ещё и события. Событие значит то, что при совершении какого-то определённого действия, должен выполниться некий код (например, клик по кнопки, или захват мыши, или нажатие клавиши). Вот и нам нужно событие перемещение курсора нашего TrackBar'а. Переключится со свойств на событие можно кнопкой:

Расположенной на панели инструментов.

Из событий нам понадобиться Scroll. Просто нажимаем на его название два раза мышкой и студия самостоятельно создаст за нас функцию которая будет вызываться в момент наступления этого события. Прототип функции которую создаст студия выглядит примерно следующим образом.

        private void trackBar1_Scroll(object sender, EventArgs e)
        {
            //напиши свой код сюда
        }

 В эту функцию нам надо добавить чтение значения из TrackBar'а посылку данных на Com порт, и вывод нового значения на экран. Всё вместе это будет выглядеть так:

            byte[] voltageValue = new byte[1];
            voltageValue[0] = (byte)(trackBar1.Value * 2.5);
            com.SendData(voltageValue);
            OutputTextBox.Text = voltageValue[0].ToString();

Стоит заметить, что у класса Com функции SendData пока не существует. Но реализуется она очень просто, так это всего лишь оболочка над функцией класса SerialPort.
 

        public void SendData(byte[] b)
        {
            port.Write(b, 0, 1);
        }

На всякий случай приведу полный листинг обеих файлов использованных в моей программе.

ФАЙЛ Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO.Ports;

namespace Client
{
    public partial class Form1 : Form
    {
        private Com com;
        public Form1()
        {
            InitializeComponent();
            com = new Com(InputTextBox);
            com.Strart();
            OutputTextBox.Text = "0.00";
        }
        private void trackBar1_Scroll(object sender, EventArgs e)
        {
            byte[] voltageValue = new byte[1];
            voltageValue[0] = (byte)(trackBar1.Value * 2.5);
            com.SendData(voltageValue);
            OutputTextBox.Text = voltageValue[0].ToString();
        }
    }
}

ФАЙЛ Com.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO.Ports;
using System.Windows.Forms;

namespace Client
{
    class Com
    {
        private string buffer;  //буфер 
        private string value;   //форматированные данные для отображения 
        private TextBox textBox;//элемент для вывода на экран
        private SerialPort port = new SerialPort("COM1", 19200, Parity.None, 8, StopBits.One);
        //ну и естественно сам порт
        public Com(TextBox textBox)
        {
            this.textBox = textBox;
        }
        private void received(object sender, SerialDataReceivedEventArgs e)
        {
            buffer += port.ReadExisting();
            if (buffer.Contains('\n'))
            {
                value = buffer.Split('\n')[0];
                buffer = buffer.Remove(0, value.Length + 1);
                if (textBox.InvokeRequired)
                    textBox.Invoke((MethodInvoker) delegate { textBox.Text = value; });
                else
                    textBox.Text = value;
            }
        }
        public void SendData(byte[] b)
        {
            port.Write(b, 0, 1);
        }
        public void Strart()
        {
            port.DataReceived += new SerialDataReceivedEventHandler(received);
            port.Open();
        }
        public void Stop()
        {
            port.Close();
        }
    }
}

Ну и наконец, после всех пересборок проекта результат на видео!

Прикрепленные файлы:

Теги:

Опубликована: Изменена: 16.03.2014 0 0
Я собрал 0 0
x

Оценить статью

  • Техническая грамотность
  • Актуальность материала
  • Изложение материала
  • Полезность устройства
  • Повторяемость устройства
  • Орфография
0

Средний балл статьи: 0 Проголосовало: 0 чел.

Комментарии (1) | Я собрал (0) | Подписаться

0
black #
Только integer (int) это 16 битное слово до 65535, а не 100500
Ответить
Добавить комментарий
Имя:
E-mail:
не публикуется
Текст:
Защита от спама:
В чем измеряется электрическое сопротивление?
Файлы:
 
Для выбора нескольких файлов использйте CTRL

Pickit 2 - USB-программатор PIC-микроконтроллеров
Pickit 2 - USB-программатор PIC-микроконтроллеров
Мини гравер 125 Ватт Конструктор для сборки: предусилитель на лампе 6N3
вверх