Text
                    Лусио ди Джасио
bWAWiWflMlKgaRiESSSK&QJM

Программирование на С микроконтроллеров PIC24
Programming 16-bit PIC Microcontrollers in C Learning to Fly the PIC24 Lucio Di Jasio Elsevier Inc. 30 Corporate Drive, Suite 400, Burlington MA 01803 USA
Лусио ди Джасио Программирование на С микроконтроллеров PIC24 Перевод с английского: Ю. А. Шпак Киев, "МК-Пресс” СПб, “КОРОНА-ВЕК” 2014
ББК 32.973-04 Д44 УДК 004.312 Ди Джасио Л. Д44 Программирование на С микроконтроллеров PIC24: Пер. с англ. — К.: “МК- Пресс”, СПб.: “КОРОНА-ВЕК”, 2014. — 336 с., ил. ISBN 978-5-7931-0529-3 (“КОРОНА-ВЕК”) ISBN 978-966-8806-57-5 (“МК-Пресс”) ISBN 978-0-7506-8292-3 (англ.) Лусио ди Джасио, эксперт из компании Microchip, предлагает свой уникальный взгляд на револю- ционную технологию PIC24, проводя читателя от основ 16-разрядной архитектуры до сложных про- граммных разработок средствами языка С, включая реализацию многозадачности с помощью пре- рываний PIC24, управление ЖК-дисплеями, формирование звуковых и видеосигналов, доступ к за- поминающим устройствам большой емкости и др. Вне всякого сомнения, эта книга будет полезна как опытным PIC-разработчикам, так и новичкам в мире встроенных систем. ББК 32.973-04 Подписано в печать 31.03.2014. Формат 70 х 100 1/16. Бумага офсетная. Печать офсетная. Усл. печ. л. 27,2. Уч.-изд. л. 17,9. Тираж 2000 экз. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства Elsevier Inc. Authorized translation from the English language edition published by Elsevier Inc., Copyright © 2007. All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, electronic, mechanical, photocopying, recording, or otherwise, without prior written permission of the publisher. Russian language edition published by MK-Press according to the Agreement with Elsevier Inc., Copyright © 2014. ISBN 978-7931-0529-3 (“KOPOHA-BEK”) ISBN 978-966-8806-57-5 (“МК-Пресс”) ISBN 978-0-7506-8292-3 (англ.) © “МК-Пресс”, 2014
Посвящается Cape
Содержание Об авторе...........................................................13 Предисловие.........................................................14 Введение............................................................15 На кого рассчитана эта книга......................................15 Структура книги...................................................16 Чем эта книга не является.........................................17 Контрольные списки................................................17 ЧАСТЬ I. ОСНОВЫ.......................................................18 Глава 1. Первый полет...............................................19 План полета.......................................................19 Предполетный контроль.............................................20 Полет.............................................................20 Компиляция и компоновка.........................................22 Сборка первого проекта..........................................23 Инициализация портов............................................24 Повторная проверка порта А......................................25 Проверка порта В................................................26 Разбор полета.....................................................28 Заметки для экспертов по ассемблеру...............................28 Заметки для экспертов по Р1С......................................29 Заметки для экспертов по С........................................29 Советы и хитрости.................................................29 Упражнения........................................................30 Ссылки............................................................30 Глава 2. Знакомство с циклами.......................................31 План полета.......................................................31 Предполетный контроль.............................................31 Полет.............................................................32 Конструкция while...............................................33 Имитация в режиме “Animate”.....................................35 Использование логического анализатора...........................38 Разбор полета.....................................................40 Заметки для экспертов по ассемблеру...............................40 Заметки для экспертов по Р1С......................................40 Заметки для экспертов по С........................................40 Советы и хитрости.................................................41 Упражнения........................................................41 Ссылки............................................................41 Глава 3. И еще о циклах.............................................42 План полета.......................................................42
7 Предполетный контроль..................................................43 Полет..................................................................43 Конструкция do.......................................................43 Объявление переменных................................................44 Конструкция for......................................................44 Примеры циклов.......................................................45 Массивы..............................................................46 Новая демонстрация...................................................46 Тестирование с помощью логического анализатора.......................48 Использование демонстрационной платы Explorer 16.....................49 Разбор полета..........................................................50 Заметки для экспертов по ассемблеру....................................50 Заметки для экспертов по Р1С...........................................50 Заметки для экспертов по С.............................................50 Советы и хитрости......................................................51 Упражнения.............................................................52 Ссылки.................................................................52 Глава 4. Числа...........................................................53 План полета............................................................53 Предполетный контроль..................................................54 Полет..................................................................54 Вопросы оптимизации..................................................55 Тестирование.........................................................56 Использование целочисленного типа long...............................56 Заметки по умножению чисел типа long.................................57 Тип данных long long.................................................57 Числа с плавающей запятой............................................58 Заметки для экспертов по С.............................................58 Измерение эффективности..............................................59 Разбор полета..........................................................61 Заметки для экспертов по ассемблеру....................................62 Заметки для экспертов по Р1С...........................................63 Советы и хитрости......................................................63 Математические библиотеки............................................63 Комплексные типы данных..............................................63 Упражнения.............................................................64 Ссылки.................................................................64 Глава 5. Прерывания......................................................65 План полета............................................................65 Предполетный контроль..................................................65 Полет..................................................................66 Вложение прерываний..................................................69 Системные прерывания.................................................69 Шаблон и пример для прерывания от модуля Timerl......................70 Реальный пример для модуля Timerl....................................71 Тестирование прерывания от модуля Timerl.............................73 Вспомогательный тактовый генератор...................................75 Календарь реального времени..........................................76 Управление несколькими прерываниями..................................77
8 Содержание Разбор полета..........................................................77 Заметки для экспертов по С.............................................71 Заметки для экспертов по ассемблеру....................................78 Заметки для экспертов по PIC...........................................78 Советы и хитрости......................................................78 Упражнения.............................................................80 Ссылки.................................................................80 Глава 6. Заглянем под капот..............................................81 План полета............................................................81 Предполетный контроль..................................................81 Полет..................................................................82 Распределение пространства памяти....................................83 Окно Program Space Visibility........................................84 Исследование распределения памяти....................................86 Файлы .тар...........................................................89 Указатели............................................................90 . Куча................................................................91 Модели памяти MPLAB СЗО..............................................92 Разбор полета..........................................................93 Заметки для экспертов по С.............................................93 Заметки для экспертов по ассемблеру....................................93 Заметки для экспертов по Р1С...........................................93 Советы и хитрости......................................................94 Упражнения.............................................................94 Ссылки.................................................................94 ЧАСТЬ II. СОЛЬНЫЙ ПОЛЕТ....................................................95 Глава 7. Обмен данными...................................................96 План полета............................................................96 Предполетный контроль..................................................97 Полет..................................................................97 Синхронные последовательные интерфейсы...............................97 Асинхронные последовательные интерфейсы..............................98 Параллельные интерфейсы..............................................99 Синхронный обмен данными с помощью модулей SPI......................100 Проверка команды “Read Status Register”.............................102 Запись в память EEPROM..............................................105 Чтение содержимого памяти...........................................105 Библиотека функций для работы с энергонезависимым хранилищем данных.106 Тестирование новой библиотеки NVM...................................109 Разбор полета.........................................................111 Заметки для экспертов по С............................................111 Заметки для экспертов по Р1С..........................................111 Советы и хитрости.................................................... 112 Упражнения............................................................113 Ссылки................................................................113 Глава 8. Асинхронный обмен данными......................................114 План полета...........................................................114 Предполетный контроль.................................................115
9 Полет.............................................................. 115 Конфигурирование модуля UART......................................116 Передача и прием данных...........................................118 Тестирование подпрограмм последовательного обмена данными.........119 Разработка простой консольной библиотеки..........................120 Тестирование терминала VT100......................................122 Использование последовательного порта в качестве средства отладки.124 Матрица...........................................................124 Разбор полета.......................................................126 Заметки для экспертов по С..........................................126 Заметки для экспертов по Р1С........................................127 Советы и хитрости...................................................127 Упражнения..........................................................128 Ссылки..............................................................128 Глава 9. Стеклянное счастье...........................................129 План полета.........................................................130 Предполетный контроль...............................................130 Полет...............................................................130 Совместимость с контроллером HD44780..............................131 ПортРМР...........................................................133 Конфигурирование порта РМР для управления модулем ЖК-дисплея......134 Небольшая библиотека функций для доступа к ЖК-дисплею.............135 Расширенное управление ЖК-дисплеем................................138 Разбор полета.......................................................139 Заметки для экспертов по С..........................................140 Советы и хитрости...................................................141 Упражнения..........................................................141 Ссылки..............................................................141 Глава 10. Этот аналоговый мир.........................................142 План полета.........................................................142 Предполетный контроль...............................................143 Полет...............................................................143 Первое преобразование.............................................145 Автоматический выбор длительности выборки.........................146 Демонстрационная программа........................................147 Игра..............................................................148 Измерение температуры.............................................150 Еще одна игра.....................................................153 Разбор полета.......................................................154 Заметки для экспертов по С......................................... 155 Советы и хитрости.................................................. 155 Упражнения......................................................... 155 Ссылки............................................................. 155 ЧАСТЬ III. ДАЛЬНИЙ РЕЙС.................................................156 Глава 11. Фиксация входных данных.....................................157 План полета.........................................................158 Полет...............................................................158 Протокол обмена данными через порт PS/2...........................158
10 Содержание Взаимодействие микроконтроллера PIC24 с портом PS/2...................159 Захват на входе.......................................................159 Тестирование метода захвата на входе с помощью сценариев стимулов.....163 Тестирование подпрограмм приема данных через порт PS/2................167 Имитация..............................................................168 Профиль имитатора.....................................................169 Второй метод: уведомление об изменении сигнала........................170 Сравнительная оценка..................................................174 Третий метод: опрос портов ввода-вывода...............................174 Тестирование метода опроса портов ввода-вывода........................178 Стоимость и эффективность решения.....................................180 Завершение интерфейса. Добавление FIFO-буфера.........................182 Завершение интерфейса. Декодирование кодов клавиш.....................185 Разбор полета...........................................................188 Советы и хитрости.......................................................188 Упражнения..............................................................189 Ссылки..................................................................189 Глава 12. Черный экран.....................................................190 План полета.............................................................191 Полет...................................................................191 Формирование полного видеосигнала.....................................193 Использование модулей Output Compare..................................196 Распределение памяти..................................................199 Последовательный вывод изображения....................................200 Разработка видеомодуля................................................202 Тестирование видеогенератора..........................................205 Оценка производительности.............................................207 Черный экран..........................................................208 Тестовый образец......................................................209 Построение изображений................................................211 Звездная ночь.........................................................212 Рисование линий.......................................................213 Алгоритм Брезенхема...................................................214 Рисование математических функций......................................217 Двухмерная визуализация трехмерных функций............................218 Фракталы..............................................................221 Текст.................................................................226 Тестирование модуля TextOnGPage.......................................229 Разработка текстовой видеостраницы....................................230 Тестирование производительности текстовой страницы....................238 Разбор полета...........................................................241 Советы и хитрости.......................................................241 Упражнения..............................................................242 Ссылки..................................................................242 Глава 13. Запоминающие устройства большой емкости..........................243 План полета.............................................................243 Полет...................................................................244 Физический интерфейс с картами SD/MMC.................................244 Взаимодействие с платой Explorer! 6...................................245
11 Новый проект.........................................................245 Выбор рабочего режима SPI............................................247 Передача команд в режиме SPI....*....................................247 Завершение инициализации карты SD/MMC................................249 Чтение данных из карты SD/MMC........................................251 Запись данных в карту SD/MMC........................................ 253 Применение интерфейсного модуля SD/MMC...............................255 Разбор полета...........................................................259 Советы и хитрости.......................................................259 Упражнения..............................................................260 Ссылки..................................................................260 Глава 14. Файловый ввод-вывод.............................................261 План полета.............................................................262 Полет...................................................................262 Секторы и кластеры...................................................262 Таблица размещения файлов FAT........................................263 Корневой каталог.................................................... 264 Поиск клада..........................................................266 Открытие файла.......................................................273 Чтение данных из файла...............................................280 Закрытие файла.......................................................282 Создание модуля файлового ввода-вывода...............................282 Тестирование функций f орепМ () и f readM ().........................284 Запись данных в файл.................................................286 Еще раз о закрытии файла.............................................290 Вспомогательные функции..............................................291 Тестирование завершенного модуля файлового ввода-вывода..............294 Размер кода..........................................................297 Разбор полета...........................................................297 Советы и хитрости.......................................................298 Упражнения..............................................................298 Ссылки..................................................................298 Глава 15. Проигрыватель...................................................299 План полета.............................................................299 Полет...................................................................300 Использование модулей Output Compare в режиме ШИМ....................301 Применение ШИМ для цифро-аналогового преобразования..................303 Формирование аналоговой волны........................................304 Воссоздание голосовых сообщений......................................306 Проигрыватель........................................................307 Файловый формат WAVE.................................................307 Функция play ()......................................................308 Низкоуровневые аудио-подпрограммы....................................313 Тестирование проигрывателя файлов WAVE...............................316 Оптимизация файлового ввода-вывода...................................318 Профилирование светодиодов...........................................318 Чтобы узнать больше, заглянем под капот..............................321 Разбор полета...........................................................324 Советы и хитрости.......................................................324
12 Содержание Упражнения........................................................324 Ссылки............................................................325 Контрольные списки..................................................326 Настройка нового проекта..........................................326 Добавление к проекту сценария компоновщика........................326 Создание и добавление к проекту нового файла......................326 Добавление файлов в проект (метод А)..............................326 Добавление файлов в проект (метод Б)............................. 327 Добавление текстовых файлов в проект..............................327 Настройка отладки в MPLAB S1M.....................................327 Характеристики семейства P1C24FJ..................................327 Сборка проекта....................................................327 Настройка логического анализатора.................................328 Характеристики микроконтроллера P1C24FJ128GA010...................328 Настройка отладчика ICD2..........................................328 Программирование в MPLAB ICD2.....................................329 Конфигурация для демонстрационной платы Explorer 16...............329 Аварийные ситуации................................................330 Перезапуск драйверов USB (неудачное подключение ICD2)..........330 Невозможно установить точку прерывания (при работе с ICD2).....330 Потерян курсор при пошаговой отладке в MPLAB SIM...............330 После выбора команды Halt MPLAB “зависает” (при работе с ICD2).331 Демонстрационная плата Explorer 16................................331 Содержимое прилагаемого к книге компакт-диска.......................332
Об авторе Лусио Ди Джасио (Lucio Di Jasio) получил степень магистра электроники в Университете Триста, Италия, в 1990 году, защитив диссертацию по теме “Имита- ция цифровых логических схем с помощью модели параллелизма Оккам”. По окон- чании университета он работал в качестве программиста и разработчика аппаратных средств над проектами в самых разнообразных областях, включая параллельную обработку цифровых изображений в промышленных автоматизированных системах средствами языка С, программирование в системе SCADA под Unix C/4GL и крип- тографию для систем безопасности в автомобильной индустрии. В 1995 году Лусио приступил к работе в компании Microchip Technology в каче- стве прикладного инженера, курирующего страны южной Европы. В 2000 году он переехал в город Чандлер (штат Аризона), занялся разработкой решений информа- ционной безопасности KEELOQ® и издал несколько руководств. В 2002 году Лусио занял пост в отделе маркетинга, отвечая за описание и выход на рынок семейств High Pin Count и High Density микроконтроллеров PIC. Начиная с 2005 года он возглавляет группу прикладных систем: большую команду инжене- ров, разрабатывающих и продвигающих на рынок решения Microchip в самых раз- нообразных прикладных сферах, включая преобразование энергии с применением элементов искусственного интеллекта, управление двигателями, осветительные сис- темы и др. В 2002 году Лусио получил лицензию частного пилота, и с тех пор налетал на различных одномоторных самолетах более 400 часов. Он является владельцем са- молета Cessna 172 (N75816), на котором при любой возможности старается улететь подальше от жаркого аризонского лета.
Предисловие Написать эту книгу оказалась гораздо сложнее, чем я предполагал (а предпола- гал я, поверьте, немало). Она никогда бы не увидела свет, если бы не 110% под- держки и понимания со стороны моей жены Сары. Сразу же хочу особо поблагода- рить моего друга Стива Боулинга (Steve Bowling), который является не только экс- пертом в вопросах 16-разрядной архитектуры Microchip, но еще и пилотом. Он вы- полнил научное редактирование этой книги и дал много полезных советов для де- монстрационных проектов и аппаратных экспериментов. Также большое спасибо Эрику Лосону (Eric Lawson) за то, что постоянно ободрял меня в процессе работы над книгой и потратил немало времени на исправление моих “километровых” пред- ложений и неправильно расставленных запятых. Отдельная благодарность — Тангу Нгуену (Thang Nguyen), подавшему идею этой книги; Джо Држевицки (Joe Drzewiecky) и Винсу Шерду (Vince Sheard) за то, что терпеливо выслушивали мои постоянные причитания и усердно трудились над совершенствованием среды MPLAB®; Калему Уилки (Calum Wilkie) и Гаю Маккар- ти (Guy McCarthy) за быструю реакцию на все мои запросы и предложения, что очень помогло мне в понимании внутренней организации компилятора и библиотек MPLAB СЗО. Также спасибо всем моим друзьям и коллегам из Microchip Technology и мно- гим инженерам, занятым в сфере проектирования встроенных систем, с которыми мне посчастливилось работать за последние годы. Вы оказали огромное влияние на мой профессиональный рост и очень обогатили мой опыт в решении задач встроен- ных систем управления.
Введение Я давно мечтал написать книгу об одном из величайших увлечений в моей жиз- ни — пилотировании. Мне хотелось сделать это так, чтобы другие инженеры также захотели принять вызов и воплотить мечту в реальность: научиться летать. Тем не менее, я понимал, что тех нескольких часов, которые я провел в воздухе за штурва- лом самолета, недостаточно для того, чтобы называть себя заслуживающим доверия экспертом по пилотированию. Когда же мне представилась возможность написать книгу о новых 16-разрядпых микроконтроллерах Microchip PIC24, я просто не смог победить искушение объединить программирование и полеты в одном проекте. По большому счету, обучение пилотированию подразумевает следование хорошо структурированному плану, позволяющему приобретать новые навыки и расширять свои горизонты. В процессе этого ты проходишь множество теоретических и прак- тических уроков и в конце концов получаешь лицензию частного пилота. Впрочем, эта лицензия, на самом деле, — только начало совершенно нового приключения. Опа лишь дает право учиться дальше. Здесь напрашивается прямая аналогия с про- цессом обучения программированию или исследованием возможностей новой мик- роконтроллерной архитектуры. По этой причине я па протяжении всей книги про- вожу параллели между двумя мирами. На кого рассчитана эта книга Сейчас — самый момент сказать, что читателя этой книги ожидает немало ин- тересного: множество увлекательных экспериментов с программными и аппарат- ными проектами, а также курс обучения “с пуля” по программированию па С повой серии превосходных 16-разрядпых RISC-процессоров. Я бы хотел это сказать, но... Не могу. Не могу, потому что все вышеупомянутое относится к этой книге только отчасти. Конечно, я надеюсь, что она будет для читателя увлекательна, а экспери- менты — по-настоящему полезны, однако для того, чтобы “переварить” материал, представленный в первых нескольких главах “галопом по Европе”, необходим оп- ределенный уровень подготовленности и готовность напрягать голову. Данное издание ориентировано па программистов, обладающих начальным или средним уровнем знаний, но не для “полных” новичков. Не ожидайте увидеть здесь правил двоичной арифметики, пояснения шестнадцатеричной записи и основ про- граммирования. Тем не менее, прежде, чем приняться за более сложные проекты, мы кратко затронем основы программирования па С в тех вопросах, которые отно- сятся к последнему поколению 16-разрядных микроконтроллеров PIC общего на- значения. Я предполагаю, что читатель попадает в одну из четырех категорий: • программист встроенных систем, имеющий опыт работы па ассемблере, по лишь поверхностно знакомый с языком С; • эксперт по микроконтроллерам PIC®, обладающий базовыми познаниями в про- граммировании па С; • студент или профессионал с определенными навыками программирования на С или C++ для персонального компьютера (ПК);
16 Введение • другие высшие формы жизни (программисты, я знаю, что вы не любите, когда вас называют просто “программистами”, поэтому и придумал такое вот опреде- ление). Читатель будет находить что-то интересное для себя в каждой главе в зависи- мости от своего практического опыта и уровня подготовленности. Я постарался, чтобы каждая из глав содержала как методики программирования на С, так и ин- формацию о новой аппаратной периферии. Тот, кто со всем этим уже знаком, может сразу же смело переходить к разделу для экспертов и дополнительным упражнени- ям, завершающим главу. Среди прочих, в книге рассмотрены следующие вопросы: • структура программы на С, предназначенной для встроенной системы: циклы, циклы и еще раз циклы; • базовые операции ввода-вывода и работы со временем; • базовая многозадачность па С применительно ко встроенным системам (ис- пользование прерываний PIC24); • новые особенности периферии PIC24 (здесь — типичный перечень): о модуль захвата на входе (Input Capture); о модуль сравнения на выходе (Output Compare); о модуль уведомления об изменениях (Change Notification); о ведущий параллельный порт (РМР); о последовательный асинхронный обмен данными; о последовательный синхронный обмен данными; о аналого-цифровое преобразование; • методы управления ЖК-дисплеями; • методы формирования видео-сигналов; • методы формирования аудио-сигналов; • методы доступа к накопителям большой емкости; • методы совместного с ПК использования файлов на больших накопителях. Структура книги Подобно курсам пилотирования, книга состоит из трех частей. Первая из них содержит шесть небольших глав с постепенным повышением уровня сложности, каждая из которых посвящена обзору основной аппаратной периферии микрокон- троллера PIC24FJ128GA010 и одному аспекту языка С с применением компилятора MPLAB СЗО. О Студенческая версия MPLAB СЗО находится на прилагаемом к книге компакт-диске в папке MPLAB. В каждой главе рассматривается как минимум один демонстрационный проект. При этом изначально достаточно исключительно эмулятора MPLAB SIM без нали- чия фактического оборудования (хотя, возможно, понадобится еще и демонстраци- онная плата Explorer 16). е Пакет установки среды MPLAB версии 7.40 находится на прилагаемом к книге компакт-диске в папке mplab\mp740_Fu11. Вторя часть книги содержит четыре главы. Здесь демонстрационная плата Explorer 16 (или ее аналог) начинает выполнять более важную роль, поскольку пеко-
Чем эта книга не является 17 торые из задействованных периферийных устройств потребуют надлежащего тести- рования на реальном оборудовании. Наконец, пять глав в завершающей части книги содержат уроки, использующие материал из предыдущих глав, для изучения новых периферийных устройств в ходе разработки проектов повышенной сложности. Здесь потребуется демонстрационная плата Explorer 16 и базовые познания в сфере макетирования (представьте себе: вам, возможно, придется взять в руки паяльник!). Если кто-то не хочет работать с базо- выми средствами аппаратного макетирования или не имеет к ним доступа, то через Web-сайт www.flyingthepic24.com можно получить специальную плату рас- ширения, содержащую все схемы и компоненты, необходимые для реализации де- монстрационных проектов. ®Все исходные коды проектов для каждой главы находятся на прилагаемом к книге компакт-диске в папке Проекты. Чем эта книга не является Эта книга не является заменой техническим описаниям, справочному руково- дству и руководству программиста PIC24, опубликованным компанией Microchip Technology, а также — руководству пользователя компилятора MPLAB СЗО и лю- бых связанных с ним библиотек и программных средств. Наиболее “свежие” версии перечисленных документов можно загрузить с Web-сайта www. microchip. com. е Некоторые из них также находятся на прилагаемом к книге компакт-диске в папках Документа- ция иМРЬАВХДокументация. Ознакомьтесь с этими документами и всегда держите их под рукой. Я часто ссылаюсь на них в книге и по мере необходимости даю небольшие выдержки из них в виде блок-схем и другой информации. Тем не менее, мое изложение не может за- менить сведения, представленные в официальных руководствах. Если читатель за- метит какое-либо несоответствие между моими словами и официальной документа- цией, ВСЕГДА доверяйте последней. Кроме того, можете связаться со мной через Web-сайт www.flyingthepic24.com, на котором также будут опубликованы любые коррективы и полезные подсказки. Отмечу также, что эту книгу нельзя назвать самоучителем по языку С, хотя в первых нескольких главах и дан обзор основных аспектов программирования на этом языке. Контрольные списки Пилоты (как профессионалы, так и любители) для выполнения даже простых процедур до и в ходе полета используют контрольные списки. И дело не в том, что эти процедуры тяжело запомнить, или что у пилотов память хуже, чем у других. Контрольные списки предохраняют от человеческих ошибок, вероятность которых особенно повышается в стрессовых ситуациях. Пилоты могут позволить себе, на- верное, меньше ошибок, чем представители любой другой профессии, и потому безопасность для них важнее гордости. Конечно, ошибки при программировании PIC24 не столь опасны, однако для ускорения наиболее популярных операций программирования и отладки я подгото- вил своеобразные контрольные списки (находятся в конце книги). Надеюсь, они по- могу!' вам на первых порах в ходе изучения новых средств PIC24.
ЧАСТЬ I Основы
ГЛАВА 1 Первый полет В этой главе: ► Компиляция и компоновка ► Сборка первого проекта ► Инициализация портов ► Повторная проверка порта А > Проверка порта В Первый полет для любого курсанта — это обычно размытая последователь- ность коротких, но очень сильных переживаний, а именно: • экстаз от первого взлета под управлением инструктора; • судорожные попытки, вцепившись в штурвал, в течение нескольких минут вы- ровнять самолет после вступительной речи инструктора о том, что “это может сделать любой, кто умеет водить машину”; • подкатывающая к горлу тошнота, когда инструктор начинает заход на посадку с помощью маневра под названием “боковое скольжение”, когда кажется, что самолет падает боком на взлетно-посадочную полосу. Для тех, кто сейчас делает первые шаги в области программирования для встро- енных систем, первая глава станет чем-то подобным. План полета У каждого полета должна быть какая-то цель, и лучшей отправной точкой для него является план полега. Для пас им станет первый проект, выполненный с при- менением 16-разрядпого микроконтроллера PIC24, а для некоторых из читателей — первый проект, созданный с помощью интегрированной среды разработки MPLAB® и языка MPLAB СЗО. Даже тот, кто ничего не знает о языке С, наверняка, что-то да слышал о знаменитом примере программы “Привет мир!”. Но даже если и не слы- шал, я сейчас о нем расскажу. Со времени появления несколько десятилетий назад первой книги по языку С, принадлежавшей перу Кернигана (Kernighan) и Ричи (Ritchie), каждая пристойная книга по этой теме обязательно включает в себя пример программы, выводящей на экран компьютера строку “Привет, мир!”. Дань этой традиции отдали авторы сотен, если не тысяч, книг, и я не собираюсь становиться исключением из правила. Тем не менее, в нашем случае все будет выглядеть несколько иначе. Давайте будем реали- стами: мы говорим о программировании микроконтроллеров, применяемых для управления встроенными системами.
20 Глава 1. Первый полет В то время, как персональный компьютер или рабочую станцию невозможно представить без монитора, в случае со встроенными системами это далеко не так. Как следствие, для нашего первого приложения мы воспользуемся более примитив- ным устройством вывода: линией цифрового порта. В последующих главах будет показано, как взаимодействовать с ЖК-дисплеем и терминалом, подключенным к последовательному порту. Впрочем, к тому времени мы уже научимся чему-то бо- лее практически применимому, чем вывод строки “Привет', мир!”. Предполетный контроль Каждому полету предшествует' предполетный осмотр: обычный обход самолета, в ходе которого, среди всего прочего, проверяется наличие горючего в баке и крыльев на фюзеляже. Итак, проверим, что у нас присутствуют все необходимые элементы для установки (с прилагаемого к книге компакт-диска или после загрузки последних вер- сий с Web-сайта компании Microchip по адресу www. microchip. com/mplab): • бесплатная интегрированная среда разработки MPLAB; • программный имитатор MPLAB SIM; • компилятор с языка С (бесплатная студенческая версия) MPLAB СЗО. Рассмотрим контрольный список для создания нового проекта в среде MPLAB. 1. Выберите команду меню Project ► Project Wizard, чтобы активизировать мастер создания проектов, который автоматизирует выполнение последующих шагов. 2. В первом окне мастера нажмите кнопку Next (Далее). 3. Выберите устройство PIC24FJ128GA010 и нажмите кнопку Next. 4. Выберите набор инструментов Microchip СЗО Toolsuite и нажмите кнопку Next. 5. В поле Project Name (Название проекта) введите “Hello Embedded World” (“При- вет встроенный мир”), а в поле Project Directory (Каталог проекта) — “Hello” и нажмите кнопку Next. 6. В.следующем окне мастера просто нажмите кнопку Next, поскольку нам не нужно копировать каких-либо исходных файлов из предыдущих проектов или каталогов. 7. Нажмите кнопку Finish (Готово) для завершения работы мастера. Поскольку, это первый проект, дополнительно выполните следующие действия. 8. Создайте новое окно редактора по команде меню File ► New. 9. Введите следующие три строки с комментариями: // // Hello Embedded World! // 10. Выберите команду меню File ► Save As и сохраните файл под именем Hell o . с. 11. Выберите команду меню Project ► Save Project, чтобы сохранить проект. Полет Настало время написать немного кода. Я буквально вижу, как читатель трепе- щет (особенно если никогда раньше не писал С-программ для встроенных уст- ройств). Наша первая строка кода будет выглядеть следующим образом: #include <p24fj128ga010.h>
Полет 21 Это еще, по сути, — не оператор С, а, скорее, — псевдокоманда для препроцес- сора, которая указывает компилятору перед тем, как двигаться дальше, прочитать содержимое файла со специфическими настройками устройства. Этот файл с рас- ширением . h содержит не более, чем длинный список имен (и размеров) внутрен- них регистров специального назначения для выбранной модели PIC24. Если вклю- чаемый файл корректен, то такие имена должны в точности соответствовать анало- гичному списку из технического описания устройства. При необходимости, файл . h можно в любой момент открыть в редакторе MPLAB — это обычный текстовый файл. Ниже представлен фрагмент из файла p24fjl28ga010.h, в котором опре- делен счетчик команд и несколько других регистров специального назначения: extern volatile unsigned int PCL ___attribute___((_sfr__)); extern volatile unsigned char PCH __attribute___((__sfr__)); extern volatile unsigned char TBLPAG ___attribute__((__sfr__)); extern volatile unsigned char PSVPAG ___attribute__((__sfr__)); extern volatile unsigned int RCOUNT ____attribute__((__sfr__)); extern volatile unsigned int SR ___attribute__((___sfr_)); Возвращаемся к нашему исходному файлу Hello. с. Добавим в него еще пару строк, знакомящих читателя с функцией main () : main () { } Теперь у нас есть полностью работоспособная, однако пустая, а значит — со- вершенно бесполезная программа на языке С. Между двумя фигурными скобками мы вскоре поместим несколько первых команд для нашей встроенной системы. Независимо от размещения функции main () внутри файла, микроконтроллер (читай — счетчик команд) после подачи питания или любого сброса всегда перехо- дит именно к ней. Хочу предупредить: перед входом в функцию main () микроконтроллер вы- полняет короткий фрагмент кода инициализации, автоматически добавленный ком- поновщиком. Этот фрагмент называют “кодом с 0”. Он отвечает за ряд необходи- мых базовых задач, включая инициализацию стека микроконтроллера. Напоминаю, что наша цель — активизировать одну или несколько линий ввода- вывода. Пусть это будут выводы RA0-7 порта А. При программировании на ас- семблере нам пришлось бы написать пару команд mov для передачи константы в выходной порт. На С это выглядит намного проще: мы можем просто воспользо- ваться так называемым оператором присваивания, как в следующем примере. #include <p24fj128ga010.h> main () { PORTA = Oxff; } Прежде всего отметим, что все операторы в языке С оканчиваются символом точки с запятой На первый взгляд это действие напоминает математическое сравнение, однако это не так. В операторе присваивания вначале вычисляется выражение справа от знака “=”, после чего полученный результат (в данном случае — обычная константа) записы- вается в контейнер, указанный слева от знака “=”. В нашем примере в качестве та-
22 Глава 1. Первый полет кого контейнера выступает 16-разрядный регистр специального назначения микро- контроллера (его имя предопределено в файле . h). ПРИМЕЧАНИЕ В языке С префиксу Ох соответствует обозначение шестнадцатеричной формы за- писи. Без него компилятор предполагает, что число представлено в десятичной сис- теме счисления. Аналогичным образом, двоичные константы обозначаются префик- сом ОЬ, в то время как восьмеричной форме записи по историческим причинам со- ответствует префикс в виде одиночного нуля 0 (но кто в наше время использует восьмеричные числа?). Компиляция и компоновка Теперь, когда мы завершили создание единственной функции main () нашей первой программы на С, необходимо каким-то образом преобразовать исходный код в двоичный исполняемый. С помощью интегрированной среды MPLAB это можно сделать одним щелчком мыши! Такая операция называется сборкой проекта. Последовательность событий при этом — довольно длинная и сложная, однако ее можно обобщить в виде двух основных этапов: • Компиляция. Вызывается компилятор С, который формирует файл с объект- ным кодом (. о). Этот файл еще нельзя назвать исполняемым в полном смысле этого слова. В то время как основная часть кода уже сформирована, все адреса функций и переменных все еще остаются неопределенными. По сути, этот файл называют перемещаемым кодовым объектом. При наличии нескольких фалов с исходным текстом программы, данный этап повторяется для каждого из них. • Компоновка. Компоновщик определяет корректное размещение в памяти каж- дой функции и переменной. При необходимости на данном этапе можно доба- вить любое количество предварительно скомпилированных объектных файлов и стандартных библиотечных функций. Среди несколько результирующих фай- лов, созданных компоновщиком, находится и фактически исполняемый двоич- ный файл с расширением . hex. Все это происходит очень быстро после выбора команды меню Project ► Build АП. Тех, кто предпочитает работать в режиме командной строки, могу обрадовать: компилятор и компоновщик можно вызвать и без использования интегрированной среды MPLAB, хотя в этом случае придется сверяться с руководством пользователя компилятора MPLAB С. Далее в книге мы будем работать исключительно в графи- ческой среде MPLAB, поскольку это значительно проще. Для того чтобы указать MPLAB, какие файлы необходимо компилировать, их имена (в данном примере — Hello.с) необходимо добавить в список исходных файлов проекта (папка Source Files). Для того чтобы компоновщик мог назначить корректные адреса каждой пере- менной и функции, MPLAB необходимо предоставить имя специфического для уст- ройства “сценария компоновки” (файл с расширением . gid). Точно так же как включаемый файл (. h) описывает имена (и размеры) регистров специального на- значения для компилятора, сценарий компоновки (.gid) информирует компонов- щик о предопределенном размещении этих регистров в памяти (согласно специфи- кациям устройства). Кроме того, он содержит важную информацию о пространстве
Полет 23 памяти: доступный объем флэш-памяти и ОЗУ, а также — соответствующие диапа- зоны адресов. Сценарий компоновки — это обычный текстовый файл, который можно от- крыть и исследовать в редакторе MPLAB. Ниже представлен фрагмент файла p24f jl28ga010 .gid, в котором опреде- лены адреса счегчика команд и нескольких других регистров специального назна- чения: PCL = 0x2E; PCL = 0x2E; PCH SS 0x30; PCH = 0x30; TBLPAG = 0x32; TBLPAG = 0x32; PSVPAG = 0x34; PSVPAG 0x34; RCOUNT 0x36; RCOUNT = 0x36; SR 5= 0x42; SR = 0x42; Сборка первого проекта Рассмотрим последние несколько шагов, необходимых для завершения нашего первого демонстрационного проекта. 1. Добавьте текущий файл с исходным кодом в список Source Files. Для этого можно воспользоваться одним из трех способов. Пока что мы рассмотрим толь- ко первый из них: а) Открыть окно проекта, если оно еще не открыто, выбрав пункт меню View ► Project. б) Разместить указатель над окном редактора и щелкнуть правой кнопкой мы- ши, чтобы вызвать контекстное меню. в) Выбрать в контекстном меню команду Add to project (Добавить в проект). 2. Добавьте в проект сценарий компоновки для PIC24. Для этого используют сле- дующую процедуру: а) Щелкнуть правой кнопкой мыши на элементе Linker Scripts в окне проекта. б) Выбрать в контекстном меню команду Add file (Добавить файл), найти и вы- брать файл p24fj128ga010.gid в подкаталогеMPLAB C30\support\ gid. Теперь окно проекта должно со- ответствовать рис. 1.1. 3. Выберите команду меню Project ► Build АП и понаблюдайте за работой компиля- тора СЗО и компоновщика. В. результате на вкладке Build (Сборка) окна Output Рис. 1.1. Окно Project в среде MPLAB для проекта Hello Embedded World
24 Глава 1. Первый полет (Результаты) в MPLAB появится несколько сообщение об успешном выполне- нии операций (по крайней мере, мы на это надеемся), и будег создан исполняе- мый файл (рис. 1.2). Build | Vetsion Control | Find in Files ] MPLAB SIM | Executing:MC:\ProgramFiles\Microchip\MPLAB C30\bin\pic30-gcc.exe" -mqlu-24FJl'28GAoTo Make: The target "C:\work\C30\1 Hello\Hello Embedded World.cof" is out of date. Executing. “C:\Program Files\Microchip\MPLAB C30\bin\pic30-gcc.exe" -Wl, "C:\work\C30\1 Hello\ Executing: "C:\Prograrn Files\Microchip\MPLAB ASM30 Suite\bin\pic30-bm2hex.exe" "Hello Embe Loaded C:\work\C30\1 Hello\Hello Embedded Wortd.cof. BUILD SUCCEEDED J Рис. 1.2. Вкладка Build окна Output в MPLAB после успешной сборки проекта 4. Выберите пункт меню Debugger ► Select Tool ► MPLAB SIM, чтобы активизиро- вать в качестве главного инструмента отладки в данном упражнении имитатор MPLAB SIM. 5. Прежде чем выполнить код, откройте окно Watch (Наблюдения) (команда меню View ► Watch), выберите в расположенном слева раскрывающемся списке эле- мент PORTA и нажмите- кнопку Add SFR (Добавить регистр специального назна- чения) (рис. 1.3). Рис. 1.3. Окно Watch в среде MPLAB Нажмите кнопку Reset (Сброс) имитатора (или выберите команду меню ря Debugger ► Reset ► Processor Reset) и понаблюдайте за содержимым реги- [Mil стра PORTA. По команде Reset он должен быть обнулен. Установите курсор внутри функции main() в строке, содержащей оператор присваивания, щелкните правой кнопкой мыши и выберите в контекстном ме- ню команду Run to Cursor (Выполнить до курсора). Это позволит пропустить весь инициализационный код компилятора С (сО) и сразу же перейти к началу программы. 8. Выполните один шаг отладки (т.е. только один текущий оператор), нажав кноп- ку Step Over или Step Into. Обратите внимание на изменение содер- жимого регистра PORTA в окне Watch. Ничего не изменилось? Сюр- ЕЙ! № приз! Инициализация портов Настал момент заглянуть в книги, в частности, — в главу 9 технического опи- сания микроконтроллера PIC24FJ128GA, где описываются порты ввода-вывода. Порт А — это довольно активно используемый 16-разрядный порт (рис. 1.4). Если взглянуть на распределение выводов, указанное в спецификации, то станет очевидно, что одни и те же линии мультиплексируются между многими периферий-
Полет 25 ними модулями. Кроме того, можно выяснить направление передачи данных для всех выводов после сброса. Для всех микроконтроллеров PIC® они по умолчанию конфигурируются как входы. Направление передачи для выводов порта А задает ре- гистр специального назначения TRISA. Таким образом, нам необходимо добавить в программу еще один оператор, конфигурирующий все выводы порта А как выхо- ды. Только после этого мы сможем увидеть изменение их состояния. #include <p24fj128ga010.h> main () { TRISA =0; // Все выводы порта A - выходы PORTA = Oxff; } Рис. 1.4. Устройство типичного порта ввода-вывода PIC24 Повторная проверка порта А Проделайте следующие действия. 1. Выполните повторную сборку проекта. 2. Установите курсор в строку с инициализацией регистра TRISA. 3. Выберите команду Run to Cursor, чтобы пропустить инициализацию компилято- ра, как было показано ранее. 4. Выполните еще два шага. Получилось! (рис. 1.5).
26 Глава 1. Первый полет Addeegs | Synibol Narne [ Value [ 02С2 PORTA OxOOFF __________ Рис. 1.5. Теперь в окне Watch видно, что содержимое порта А изменилось Если все было сделано верно, то содержимое порта А должно смениться значе- нием 0x0OFF и быть выделено в окне Watch красным цвегом. Привет.', мир! То, что мы выбрали именно порт А, продиктовано отчасти тем, что “А” — пер- вая буква алфавита, по в большей степени тем, что к выводам RA0-RA7 популярной демонстрационной платы Explorer 16 удобно подключить восемь светодиодов. Та- ким образом, выполнение рассмотренной выше программы на реальной демонстра- ционной плате приведет к включению светодиодов. Ура! Ура! Елочка гори! Проверка порта В В завершение урока мы исследуем еще один порт ввода-вывода: порт В. Для этого достаточно просто заменить в программе идентификаторы PORTA и TRISA на PORTB и TRISB соответственно. Перекомпонуйте проект, следуя инструкциям из предыдущего упражнения, и запустите программу на выполнение. Опять сюрприз! Код, который работал для порта А, для порта В не действует! Без паники! Это так задумано. Я специально выбрал такой пример, чтобы чита- тель пережил легкий шок от перехода на PIC24. Так закаляют настоящих бойцов. Опять обратимся к техническому описанию и внимательно исследуем схемы распределения выводов PIC24. Между восьмиразрядными микроконтроллерами PIC и повой архитектурой PIC24 есть два фундаментальных различия. • большинство выводов порта В мультиплексированы с аналоговыми входами аналого-цифрового преобразователя (АЦП), в то время как в восьмиразрядной архитектуре для этой цели был выделен, главным образом, порт А (другими словами, эти два порта поменялись ролями!); • в архитектуре PIC24 при мультиплексировании па некоторый вывод входпо- го/выходного сигнала разрешенного периферийного модуля, этот сигнал полно- стью определяет' режим работы вывода независимо от содержимого регистра TRISx. В восьмиразрядной архитектуре только пользователь мог определить направление передачи для вывода, даже если его использования требовал ка- кой-либо периферийный модуль. По умолчанию выводы, мультиплексированные с “аналоговыми” входами, от- соединены от своих “цифровых” входов. Именно поэтому в последнем примере мы не получили никакого результата. При подаче питания все выводы порта В в микро- контроллере PIC24FJ128GA010 по умолчанию выполняют функцию аналоговых входов. Как следствие, чтение порта В дает все нули. Тем не менее, обращаю вни- мание на то, что выходной фиксатор порта В установлен корректно, хотя этого и не видно по регистру PORTB. Для того чтобы убедиться в этом, проверьте содержимое регистра LATB. Для соединения входов порта В с цифровыми входами необходимо действовать в соответствии со входами модуля АЦП. В спецификации указано, что за цифро- аналоговое распределение каждого вывода отвечает регистр специального назначе- ния AD1PCFG (рис. 1.6). Таким образом, для реализации нашей задачи необходимо присвоить 1 каждому разряду регистра AD1PCGF.
Полет 27 Старший байт: R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 PCFG15 | PCFG14 | PCFG13 | PCFG12 | PCFG11 | PCFG10 | PCFG9 | PCFG8 разряд 15 разряд 8 Младший байт: R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 PCFG7 | PCFG6 PCFG5 PCFG4 | PCFG3 | PCFG2 | PCFG1 | PCFG0 разряд 7 разряд 0 Разряды 15-0 PCFG15:PCFG0 — разряды управления конфигурацией аналоговых входов 1 — вывод для соответствующего аналогового канала сконфигурирован в цифровом режиме; чтение порта разрешено 0 — вывод сконфигурирован в аналоговом режиме; чтение порта запрещено; АЦП опрашивает напряжение на выводе Рис. 1.6. AD1PCFG—регистр конфигурирования порта АЦП Теперь наша программа в полном виде выглядит следующим образом: ^include <p24fj128ga010.h> main () { TRISB =0; // Все выводы порта В - выходы AD1PCFG = Oxffff; // Все выводы порта В - цифровые PORTB = Oxff; } На этот раз компиляция и пошаговое выполнение программы даст желаемый результат (рис. 1.7). Рис. 1.7. Проект Hello Embedded World
28 Глава 1. Первый полет Разбор полета Каждый полет требует последующего разбора, когда ты сидишь в удобном кресле со стаканом холодной воды в руке и размышляешь вместе с инструктором над усвоенными уроками. Разработка программ на С для микроконтроллеров PIC24 — не так уж и сложна (по крайней мере, по сравнению с эквивалентами на ассемблере). Для управления линиями ввода-вывода, представляющими собой наиболее фундаментальное сред- ство взаимодействия с микроконтроллером, достаточно буквально двух-трех опера- торов. Следует помнить, что компилятор СЗО не умеет читать наши мысли. Как и в слу- чае с ассемблером, за корректную инициализацию передачи данных отвечаег только сам программист. И опять-таки, необходимо изучать спецификации и выяснять ма- лейшие различия между знакомыми нам восьми- и новыми 16-разрядными микрокон- троллерами PIC. Несмотря на все высокоуровневые достоинства языка С, создание программ для устройств со встроенным управлением по-прежнему требует хороших знаний мель- чайших нюансов используемого аппаратного обеспечения. Заметки для экспертов по ассемблеру Если кому-то трудно принять на веру корректность кода, созданного компиля- тором MPLAB СЗО, то оп может в любой момент открыть окно Disassembly View (Листинг дизассемблера) с помощью соответствующего пункта меню View. Это окно содержит код, сгенерированный компилятором, в то время как каждая строка ис- ходного текста на С отображена в виде комментария перед соответствующим ас- семблерным фрагментом (рис. 1.8). Рис. 1.8. Окно Disassembly Listing В этом режиме можно даже выполнять код пошагово и применять любые опе- рации отладки, хотя я настоятельно рекомендую этого не делать (или, по крайней мере, ограничиться лишь небольшими экспериментами в ходе изучения первых глав книги). Удовлетворив свое любопытство, научитесь доверять компилятору. В конце концов использование языка С приведет к настоящему всплеску продуктивности вашего труда и сделает исходный код гораздо более читабельным и удобным в со- провождении.
Заметки для экспертов по PIC 29 В качестве последнего упражнения откройте окно Memory Usage Gauge (Шкала использования памяти) (команда меню View ► Memory Usage Gau- ge) (рис. 1.9). He пугайтесь! Хотя паши три строки кода за- няли более 300 байт памяти программ, это ни в коем случае не говорит о неэффективности язы- ка С. Просто таков минимальный размер блока кода, формируемого (для нашего же удобства) компилятором СЗО. Все дело в упомянутом ранее коде инициализации (сО), о которым мы погово- Рис. 1.9. Окно Memory Usage Gauge в среде MPLAB рим подробнее в последующих главах во время изучения переменных, распределе- ния памяти и прерываний. Заметки для экспертов по PIC Тем из читателей, которые знакомы с архитектурами PIC16 и PIC18, будет ин- тересно узнать, что большинство управляющих регистров PIC24, включая регистры портов ввода-вывода, теперь — 16-разрядные. Кроме того, изучая спецификации PIC24 обратите внимание на тот факт, что названия большинства периферийных устройств очень похожи или даже совпадают с названиями привычных восьмираз- рядных периферийных устройств. Таким образом, освоиться с ними не составит труда! Заметки для экспертов по С Конечно же, мы могли бы использовать функцию printf из стандартной биб- лиотеки С. Фактически, такие библиотеки вполне применимы и при работе с ком- пилятором MPLAB СЗО, однако не забывайте, что мы создаем программы для встроенных систем, а не для рабочих станций с гигабайтами памяти. Привыкайте к тому, что вы имеете дело с низкоуровневой аппаратной периферией микрокон- троллеров PIC24. Простой вызов библиотечной функции, наподобие printf, мо- жет увеличить исполняемый код па несколько килобайт. Не следует исходить из предположения, что пользователя всегда будет доступен последовательный порт с терминалом или текстовый дисплей. Вместо этого тщательно взвешивайте целесо- образность применения каждой функции и библиотеки в свете ограниченных ресур- сов, с которым приходится оперировать в мире встроенных систем. Советы и хитрости Семейство микроконтроллеров PIC24FJ построено с применением КМОП-тех- нологий (3 В) с рабочим диапазоном 2,0..3,6 В. Вследствие этого, должно использо- ваться напряжение питания Vd(j = 3 В, что ограничивает выходное напряжение каж- дой линии ввода-вывода при формировании уровня лог. 1. Тем не менее, организо- вать взаимодействие с традиционными устройствами на 5 В не составит труда: • для получения на выходе 5 В используйте управляющие регистры ODCx (О ОСА для порта A, ODCB для порта В и т.д.) для перевода отдельных выходов в режим с открытым стоком и подключения внешних подтягивающих резисторов к ис- точнику напряжения 5 В;
30 Глава 1. Первый полет • в то же время цифровые входы изначально допускают напряжение до 5 В, и по- тому на них можно напрямую подать входные сигналы уровня 5 В. Тем не менее, соблюдайте осторожность с линиями ввода-вывода, мультиплек- сированными с аналоговыми входами, поскольку они не поддерживают напряжений выше Vdd. Упражнения При наличии платы Explorer 16: 1. С помощью контрольного списка “Настройка отладчика MPLAB ICD2” (см. приложение в конце книги) подготовьте проект к отладке. 2. Для тестирования примера с портом А подключите плату Explorer 16 и прокон- тролируйте вывод визуально с помощью восьми светодиодов. 3. Для тестирования примера с портом В подключите к выводу RB0 вольтметр или цифровой мультиметр и понаблюдайте за движением стрелки в ходе пошагово- го выполнения программного кода. Ссылки • Книга Б. Кернигана и Д. Ричи “Язык программирования С” была впервые изда- на в 1978 году! Второе издание 1988 года включает в себя описание более позд- него стандарта ANSI С, который ближе к стандарту, используемому компилято- ром MPLAB СЗО (ANSI90). • По Web-адресу http://en.wikibooks.org/wiki/C_Programming на- ходится книга по программированию на С. Это очень удобно, если только вы не имеете ничего против электронных книг. Советую заглянуть в главу “A taste of С” (“Вкус С”) — там вы найдете традиционный пример с выводом строки “При- вет, мир!”.
ГЛАВА 2 Знакомство с циклами В этой главе: > Конструкция while Имитация в режиме "Animate" > Использование логического анализатора Примечательно, что в авиации также существуют своеобразные циклы — замк- нутые, фиксированные траектории движения самолетов вокруг определенного рай- она. Каждый аэропорт использует такие траектории па определенной (всем извест- ной) высоте и размещении относительно всех взлетно-посадочных полос. Их назна- чение — организовать воздушное движение вокруг аэропорта. По сучи, это очень напоминает обычное круговое движение на автомобильных развязках. Все самолеты кружат в заданном направлении, соответствующем преобладающему направлению ветра. Они идут на одинаковой высоте, что упрощает им визуально отслеживать друг друга. Все пилоты переговариваются друг с другом и с диспетчерской вышкой (если таковая присугствует) па одной и той же радиочастоте. Обучаясь пилотирова- нию, вы обязательно должны провести какое-то время (особенно на первых уроках), летая по кругу под руководством инструктора. При этом отрабатываются следую- щие одна за другой последовательности из посадки и взлета после касания шасси земли. Таким образом оттачиваются вновь приобретенные навыки. Обучаясь про- граммированию встроенных систем, также необходимо освоить подобную петлю под названием “главный цикл”. План полета Для программ, управляющих встроенными системами, необходима некоторая структура для контроля за выполнением кода. В данном уроке мы рассмотрим осно- вы синтаксиса циклов в С и познакомимся с новым периферийным модулем: 16- разрядным таймером (Timer 1). Также мы впервые воспользуемся двумя средствами MPLAB® SIM: режимом “Animate” и логическим анализатором. Предполетный контроль Для второго урока нам понадобятся те же программные компоненты (с прилагае- мого к книге компакт-диска или после загрузки последних версий с Web-сайта ком- пании Microchip), что и в предыдущей главе: • интегрированная среда разработки MPLAB;
32 Глава 2. Знакомство с циклами • программный имитатор MPLAB SIM; • студенческая версия компилятора MPLAB СЗО. Рассмотрим контрольный список для создания нового проекта в среде MPLAB. 1. Выберите команду меню Project ► Project Wizard, чтобы активизировать мастер создания проектов, который автоматизирует выполнение последующих шагов. 2. В первом окне мастера нажмите кнопку Next (Далее). 3. Выберите устройство PIC24FJ128GA010 и нажмите кнопку Next. 4. Выберите набор инструментов Microchip СЗО Toolsuite и нажмите кнопку Next. 5. В поле Project Name введите “A loop in the pattern” (“Пример цикла”), а в поле Project Directory — “Циклы” и нажмите кнопку Next. 6. В следующем окне мастера просто нажмите кнопку Next, поскольку нам не нужно копировать каких-либо исходных файлов из предыдущих проектов или каталогов. 7. Нажмите кнопку Finish (Готово) для завершения работы мастера. Далее добавьте в проект сценарий компоновщика р24 f j 128ga010 . gid, ко- торый при типичной установке среды MPLAB находится в папке C:\Program Files\Microchip\MPLAB C30\support\gld. Затем создадим новый файл и добавим его в проект: 8. Откройте новое окно редактора. 9. Введите заголовок главной программы: // // A loop in the pattern // 10. Выберите команду меню Project ► Add New File to Project и сохраните файл под именем loop. с. В результате он будет автоматически добавлен в список ис- ходных файлов проекта. 11. Сохраните проект. Полет Один из главных вопросов, которые могут возникнуть после прохождения пре- дыдущего урока, звучит следующим образом: “Что произойдет после выполнения всего кода функции main () ?”. Ответ: “Ничего”. Или, по крайней мере, ничего не- ожиданного. Устройство будет инициализировано повторно, и программа будет' вы- полняться снова и снова. Фактически, компилятор на всякий случай сразу же после кода функции main () помещает' специальную команду программного сброса. Во встроенных сис- темах приложение должно непрерывно выполняться от момента подачи питания до нажатия кнопки “Выкл.”. Таким образом, описанный выше способ повторного вы- полнения программы после сброса на первый взгляд выглядит вполне приемлемо, однако в действительности такой вариант эффективен лишь в немногих случаях. Вскоре вы обнаружите, что подобный “цикл” несколько “кривоват”. Достигнув конца программы и выполнив сброс, микроконтроллер возвращается к самому на- чалу всего инициализациопного кода, включая кратко упомянутый в предыдущей главе сегмент сО. В результате, насколько бы ни был быстротечным этап инициали- зации, из-за пего цикл становится очень несбалансированным. Повторная инициа- лизация всех регистров специального назначения и глобальных переменных замед-
Полет 33 ляет работу приложения, хотя в большинстве случаев в ней нет необходимости. Бо- лее предпочтительный вариант — создать так называемый главный цикл. Но для начала рассмотрим основные циклические конструкции языка С. Конструкция while В языке С доступны по меньшей мере три способа кодирования циклов, и пер- вый из них — конструкция while: while (х) ( // Код цикла Любой код, помешенный между двумя фигурными скобками ({ }), будет повто- ряться до тех нор, пока логическое выражение в круглых скобках (х) возвращает значение t rue. Но что такое логическое выражение в терминах языка С? Прежде всего следует отметить, что в С логические выражения ничем не отли- чаются от арифметических. Так, булевым логическим значениям TRUE и FALSE со- ответствуют обычные целые числа согласно простому правилу: • FALSE — число 0; • TRUE — любое целое число, кроме 0. Таким образом, значению true соответствует не только 1, по также, скажем, 13 или -27 8. Для того построения логических выражений используют ряд логиче- ских операторов: • || — логическое “ИЛИ”; • & & — логическое “И”; • ! — логическое “НЕ”. Эти операторы воспринимают свои операнды, как логические (булевы) значе- ния согласно описанному выше правилу, возвращая также логическое значение. Рассмотрим несколько элементарных примеров. Если a = 17,ab = l (т.е. когда оба операнда равны true), получаем: • (а | | Ь) дает true; • (а && Ь) дает true; • ( ! а) дает false. Кроме того, существуют операторы, сравнивающие два числа (целых или веще- ственных) и возвращающие соответствующее логическое значение: • == — “равно” (обратите внимание на то, что, в отличие от рассмотренного в предыдущей главе оператора присваивания, этот оператор состоит из двух знаков равенства); • ! == — “не равно”; • > — “больше”; • >= — “больше или равно”; • < — “меньше”; • <= — “меньше или равно”. Рассмотрим несколько примеров для а = 10: • (а > 1) дает true;
34 Глава 2. Знакомство с циклами • (-а >= 0) дает false; • (а == 17) дает false; • (а ! = 3) дает true. Возвращаясь к конструкции while, мы сказали, что цикл выполняется до тех пор, пока выражение в круглых скобках дает логическое значение true (т.е. любое целое число, кроме 0). Когда это выражение дает логическое значение false, цикл прерывается и программа продолжает выполняться с первого оператора после за- крывающей фигурной скобки. Учтите, что выражение всегда оценивается до выполнения кода, заключенного в фигурные скобки, при каждом проходе цикла. Рассмотрим несколько интересных примеров циклов: While (0) { // Код цикла... } Постоянное условие false означает, что цикл никогда не выполняется, в чем мало смысла. По суги, это — один из главных кандидатов на звание чемпиона мира по бесполезности программного кода. Еще один пример: while (1) { // Код цикла... } Постоянное условие true означает, что цикл — бесконечный. В этом уже есть смысл, и, фактически, именно такую конструкцию мы будем использовать впредь для организации главных программных циклов. Для удобочитаемости кода мы вос- пользуемся более элегантным подходом, объявив пару констант: #define TRUE 1 #define FALSE 0 В дальнейшем они будут постоянно применяться в программах, например: While (TRUE) { I/ Код цикла... } Наступил момент добавить несколько новых строк в исходный файл loop, с и рассмотреть применение конструкции while на практике. #include <p24fj128ga010.h> main() { // Инициализация управляющих регистров TRISA = OxffOO; // Выводы 0..7 порта A - выходы // Главный цикл приложения while(1) { .PORTA = Oxff; // Активизируем выводы 0-7 PORTA =0; // Отключаем все выводы } }
Полет 35 Структура этой программы, по сути, совпадает со структурой любой программы управления встроенной системой, написанной на С. В ней всегда присутствуют две основные части: • инициализация, включая периферийные устройства и переменные (выполняется только один раз в начале программы); • главный цикл, который содержит всю функциональность, определяющую пове- дение приложения (выполняется бесконечно). Имитация в режиме “Animate” Откомпилируйте и скомпонуйте программу loop, с и подготовьтесь к про- граммной имитации, руководствуясь контрольным списком “Настройка отладки в MPLAB SIM”. Для того чтобы протестировать код рассматриваемого примера с помощью имитатора, я рекомендую воспользоваться режимом “Animate” (пункт меню Debugger ► Animate). В этом режиме эмулятор выполняет по одной программной строке исходного кода па С, останавливаясь после каждой из них на S секунд, чтобы успеть проанализировать текущие результаты. Если в окно Watch добавить регистр специального назначения PORTA, то можно увидеть, что он попеременно принимает значения Oxff и 0x00. Скорость выполнения программы в режиме “Animate” задают в диалоговом ок- не Simulator Settings (Параметры имитатора), которое открывается по команде меню Debugger ► Settings. Например, в поле Animation Step Time (Значение шага) на вклад- ке Animation/Real Time Updates (Обновления в режиме реального времени) можно указать значение 500 мс. Как вы понимаете, режим “Animate” — это полезное и на- глядное средство отладки, однако он дает совершенно неверное представление о фактическом времени выполнения программы. Например, если бы рассмотренная выше программа была выполнена на реальном устройстве (скажем, в демонстраци- онной плате Explorer 16, оснащенной микроконтроллером PIC24 с частотой 32 МГц), то светодиоды, подключенные к выходам порта А, мерцали бы настолько быстро, что глаза этого мерцания просто не различали бы. По сути, каждый светодиод пере- ключался бы несколько миллионов раз в секунду. Для приемлемого замедления мерцания светодиодов (например, до двух раз в секунду) предлагаю воспользоваться таймером — одним из ключевых периферий- ных устройств в составе микроконтроллера PIC24. В данном случае из пяти тайме- ров, реализованных в PIC24FJ128GA010, мы задействуем первый. Ему соответству- ет один из наиболее гибких и простых периферийных модулей: модуль Timer 1. Все, что от нас требуется, — заглянуть в спецификацию PIC24 и найти там соответст- вующую структурную схему (рис. 2.1) и описание управляющих регистров модуля Timer 1. В частности, нас интересуют идеальные значения для инициализации. Таким образом мы выясним, что для управления большинством функций моду- ля Timerl используют три регистра специального назначения: • TMR1 — 16-разрядный счетный регистр; • T1CON — управляет активацией и рабочим режимом таймера; • PR1 — предназначен для организации периодического сброса таймера (в дан- ном примере не используется). Для того чтобы счет начинался с нуля, следует очистить регистр TMR1: TMR1 = 0;
36 Глава 2. Знакомство с циклами TCKPS1:TCKPSO Рис. 2.1. Структурная схема модуля 16-разрядного таймера Timerl Затем можно инициализировать регистр T1CON (рис. 2.2) таким образом, чтобы таймер работал в простой конфигурации: • модуль Timerl активен: TON = 1; • в качестве источника импульсов синхронизации служит главный задающий ге- нератор (Fosc/2): TCS = 0; • предварительный делитель установлен па максимальное значение (1:256): TCKPS = 11; • стробирование и синхронизация па входе не требуются, поскольку мы исполь- зуем напрямую главный генератор синхроимпульсов: TGATE = 0, TSYNC = 0; • работа с ждущем режиме пас не интересует: TSIDL = 0 (значение по умолча- нию). Старший байт: R/W-0 U-0 R/W-0 U-0 и-о и-о и-о и-о TON — TSIDL — I — разряд 15 разряд 8 Младший байт: и-о R/W-0 R/W-0 R/W-0 и-о R/W-0 R/W-0 и-о | TGATE | TCKPS1 TCKPS0 МММ» ' TSYNC TCS I разряд 7 разряд 0 Рис. 2.2. Т1 CON—управляющий регистр модуля Timerl Если все разряды объединить в одном 16-разрядном значении для назначения регистру T1CON, то получится: T1CON = 0Ы000000000110000;
Полет 37 В более компактной шестнадцатеричной записи это выглядит таким образом: T1CON = 0x8030; После инициализации таймера начинается цикл, в котором мы ожидаем дости- жения регистром TMR1 требуемого значения, заданного константой DELAY: while( TMR1 < DELAY) { // Ожидаем } В случае использования тактового генератора на 32 МГц для получения за- держки около четверти секунды значение DELAY должно быть довольно большим. Его можно вычислить на основании следующей формулы: Tdelay = (2/Fosc) * 256 * DELAY, где Tdelay = 256 мс. Разрешив эту формулу относительно DELAY, мы получим зна- чение 16 000: ^define DELAY 16000 Разместив два подобных' цикла задержки перед каждым присваиванием для порта А внутри главного цикла, мы получаем конечную программу: #include <p24fj128ga010.h> #define DELAY 16000 main() ( // Инициализация управляющих регистров TRISA = OxffOO; // Выводы 0..7 порта A - выходы T1CON = 0x8030; // Включение TMR1, делитель 1:256 Tclk/2 // Главный цикл приложения while (1) ( // 1. Активизация выводов 0-7 и ожидание в течение j секунд PORTA = Oxff; TMR1 =0; // Обнуление счетчика while (TMR1 < DELAY) I // Просто ожидаем } // 2. Отключение всех выводов и ожидание в течение j секунд PORTA = 0x00; TMR1 =0; // Обнуление счетчика while (TMR1 < DELAY) ( // Просто ожидаем } } // Главный цикл } // main ПРИМЕЧАНИЕ При программировании на С легко запутаться в открывающих и закрывающих фи- гурных скобках. Даже скрупулезно соблюдая отступы, очень скоро уже нельзя вспомнить, какой открывающей скобке соответствует та или иная закрывающая. По этой причине рекомендую сопровождать каждую закрывающую скобку небольшим комментарием, поскольку это упрощает работу с исходным текстом программы.
38 Глава 2. Знакомство с циклами Теперь — самое время выполнить сборку проекта и проверить, как он работает. При наличии демонстрационной платы Explorer 16 можно сразу же попробовать за- пустить код на выполнение. Светодиоды должны мерцать достаточно медленно: примерно два раза в секунду. Тем не менее, при попытке выполнить тот же код в имитаторе MPLAB SIM окажется, что программа работает слишком медленно. Не знаю, насколько мощный компьютер у читателя, но на моей системе MPLAB SIM и близко не достиг скоро- сти выполнения, характерной для микроконтроллера PIC24 на 32 МГц. В режиме “Animate” дела обстоят еще хуже, поскольку, как было показано ра- нее, в нем перед выполнением очередной строки кода вносится дополнительная за- держка примерно в полсекунды. Таким образом, с целью отладки в имитаторе зна- чение константы DELAY следует выбрать гораздо меньшим (например, 16). Файл loop. с находится на прилагаемом к книге компакт-диске в папке проекты\02 - Циклы. Использование логического анализатора В завершение урока я предлагаю после сборки проекта немного поэксперимен- тировать с новым инструментом имитации: логическим анализатором MPLAB. Он дает графическое, в высшей степени наглядное представление регистрируемых зна- чений для любого числа выходов устройств, однако требует небольшой предвари- тельной настройки. В первую очередь необходимо удостовериться в том, что активна функция сле- жения. 1. Выберите команду меню Debugger ► Settings и в диалоговом окне Simulator Set- tings перейдите на вкладку Osc/Trace (Генератор/Слежение). 2. В группе Trace Options (Параметры слежения) установите флажок Trace All (От- слеживать все). 3. Теперь можете открыть окно логического анализатора (рис. 2.3), выбрав коман- ду меню View ► Simulator Logic Analyzer. Рис. 2.3. Окно Logic Analyzer 4. Нажмите кнопку Channels (Каналы), чтобы открыть диалоговое окно Configure channels (Конфигурирование каналов) (рис. 2.4).
Полет 39 Рис. 2.4. Диалоговое окно Configure Channels 5. В этом окне можно выбрать выходы устройств, для которых необходимо вы- полнить визуализацию сигналов. Для нашего примера выберите вывод RA0 и нажмите кнопку Add => (Добавить). 6. Нажмите кнопку ОК, чтобы закрыть диалоговое окно Configure Channels. ПРИМЕЧАНИЕ..................................................... Описанные выше действия перечислены в приложении в контрольном списке' “На- стройка логического анализатора”. Запустите программу на выполнение и, выдержав небольшую паузу, нажмите кнопку Halt (Стоп). В окне Logic Analyzer должно быть отображе- j"’"o [)0 но графическое представление последовательности прямолинейных им- пульсов (рис. 2.5). Рис. 2.5. Окно Logic Analyzer содержит графическое представление последовательности импульсов
40 Глава 2. Знакомство с циклами Разбор полета В этом небольшом уроке было показано, каким образом компилятор MPLAB СЗО реализует останов программы. Мы создали наш первый структурированный проект, содержащий внутри функции main() раздел инициализации и бесконеч- ный цикл. Для этой цели мы рассмотрели циклическую конструкцию while, а так- же кратко затронули вопрос построения логических выражений. В завершение уро- ка был рассмотрен пример программы с применением модуля таймера и проведен эксперимент с окном Logic Analyzer для создания графического представления сиг- нала на выходе RA0. Заметки для экспертов по ассемблеру Логические выражения в С могут сыграть злую шутку с программистами па ас- семблере, привыкшими иметь дело с поразрядными операторами с теми же назва- ниями (AND, OR, NOT...). В языке С также используются поразрядные операторы, однако во избежание путаницы я намеренно не упомянул о них в данном уроке. По- добные операторы вычисляют результат, сопоставляя пары разрядов из двух опера- торов па основании определенной таблицы истинности. В то же время логические операторы воспринимают каждый операнд как цельное булево значение независимо от количества разрядов, из которых оно состоит. Поясним это на следующем примере: 11110101 11110101 (TRUE) поразрядное OR 00001000 логическое OR 00001000 (TRUE) дает 11111101 дает 00000001 (TRUE) Заметки для экспертов по PIC Я уверен, что вы заметили исчезновение модуля TimerO. Но не велика потеря! Оставшиеся пять таймеров PIC24 настолько насыщены разными возможностями, что по функциональности таймера 0 тосковать не приходится. Имена всех регистров специального назначения, отвечающих за управление таймерами, совпадают с име- нами, использованными в микроконтроллерах PIC16 и PIC18, и во многом сходны по структуре. Но все же не забывайте сверяться со спецификациями. Разработчики умудрились втиснуть в таймеры несколько новых возможностей: • все таймеры теперь 16-разрядные; • каждый таймер оснащен 16-разрядным регистром периода; • для таймеров 2/3 и 4/5 доступен новый механизм одновременного использова- ния в паре, обеспечивающий 32-разрядпый режим счета; • к таймеру 1 была добавлена новая возможность стробирования внешнего такто- вого сигнала. Заметки для экспертов по С Тот, кто привык к программированию па С для персонального компьютера или рабочей станции, ожидает, что по окончании функции main () управление возвра- щается операционной системе. Хотя для PIC24 существует несколько операциоп-
Советы и хитрости 41 пых систем реального времени (RTOS — Real-Time Operating System), большинство приложений их не использует ввиду отсутствия такой необходимости. В частности, это справедливо для всех простых примеров, рассмотренных в этой книге. По умол- чанию компилятор СЗО подразумевает, что операционная система, в которую можно было бы вернуть управление, отсутствует, и потому принимает наиболее безопасное решение: формирует сигнал сброса. Советы и хитрости Некоторые встроенные приложения предназначены для выполнения своего главного цикла безостановочно на протяжении нескольких месяцев или даже лет, когда устройство не отключается и не принимает сигнал сброса. Однако управляю- щие регистры микроконтроллера — это обычные ячейки ОЗУ. В виде этого сущест- вует небольшая вероятность того, что их содержимое может быть изменено вслед- ствие колебаний питающего напряжения (не выявленных схемой сброса по обнару- жению провала напряжения); электромагнитного импульса, выданного каким-либо расположенным поблизости “шумным” оборудованием, или даже воздействия кос- мического излучения. Рано или поздно это происходит, и насколько быстро — за- висит от приложения. При разработке системы, которая должна работать достовер- но в течение длительных промежутков времени, необходимо серьезно обдумать не- обходимость обеспечения периодического “обновления” наиболее важных управ- ляющих регистров ключевых периферийных блоков. Сгруппируйте последовательность команд инициализации в одной или не- скольких функциях, и вызовите их сразу же после подачи питания, перед входом в главный цикл. Однако внутри этого цикла также необходимо реализовать вызов функций инициализации в тот момент, когда не решается никакая критически важ- ная задача. Таким образом будет обеспечено периодическое обновление содержи- мого управляющих регистров. Упражнения 1. Вместо попеременного включения и отключения выводов порта А выдайте на них значения счетчика. 2. Вместо попеременного включения и отключения реализуйте циклический сдвиг. Ссылки • По Web-адресу http://en.wikipedia.org/wiki/Control_flowllLo- ops вы найдете обширный обзор языков программирования и проблем, связан- ных с кодированием и укрощением циклов. • По Web-адресу http: //en.wikipedia.org/wiki/Spaghetti_code по- казано, как код может' выйти из-под контроля, если игнорировать циклы.
ГЛАВА 3 И еще о циклах В этой главе: > Конструкция do ► Объявление переменных Конструкция for ► Примеры циклов Массивы ► Новая демонстрация Тестирование с помощью логического анализатора ► Использование демонстрационной платы Explorer!6 В авиации существует фигура пилотажа, называемая “мертвой петлей”. Она “по зубам” только умелым пилотам, управляющим специально оснащенным для этой задачи самолетом. Не знаю, обрадует это вас или разочарует, но в ходе обучения пилотированию подобные трюки вас делать не заставят. Впрочем, вам будет доста- точно и других задач, наподобие оттачивания разнообразных поворотов, включая развороты над заданной точкой, “змейки”, виражи и стандартные заходы па курс. Во время подобных тренировок вы обнаруживаете, насколько бывает сложно, дви- гаясь в трехмерном пространстве, контролировать только одно из измерений. На- пример, кружа вокруг ориентира на земле, поначалу обычно очень нелегко поддер- живать постоянную высоту и скорость. Если при этом еще добавить ветер, то воз- никают еще и проблемы с сохранением требуемой дистанции до заданной точки, из- за чего круги получаются совсем не круглыми. Но, как говорится, “усердие и труд все перетрут”. В языке С также присутствует несколько видов циклических конструкций. Для того чтобы распознавать, какая из них оптимальна в каждой конкретной ситуации, потребуется определенная практика. План полета В предыдущем уроке мы узнали о существовании в любом встроенном прило- жении главного цикла. В этой главе мы продолжим изучение методик, доступных программисту па С для реализации циклов. По ходу будет кратко затронуто объяв- лепие целочисленных переменных, операторы инкремента и декремента, а также вопросы объявления и использования массивов. Как и в любом хорошем уроке пилотирования, теория будет сразу же закреп- ляться на практике. В завершение мы выполним упражнение, реализующее все рас- смотренные в этой главе концепции и средства.
Предполетный контроль 43 Предполетный контроль В этом уроке мы продолжаем работать с программным имитатором MPLAB® SIM, но для наглядности можно воспользоваться и демонстрационной платой Ex- plorer 16. При желании для подготовки нового проекта используйте контрольный список “Настройка нового проекта” из приложения в конце книги. При этом при- свойте проекту имя More Loops (“Еще о циклах”) и создайте в нем файл с исход- ным кодом More. с. Полет В конструкции while блок кода, заключенный между двумя фигурными скоб- ками, выполняется до тех пор, пока логическое выражение возвращает булево зна- чение true (не 0). Логическое выражение оценивается до выполнения тела цикла, а значит в том случае, если оно сразу же возвращает значение false, код внутри фигурных скобок вообще не будет выполнен. Конструкция do Если требуется, чтобы тело цикла выполнялось хотя бы раз, а последующие по- вторения уже зависели от результата логического выражения, можно воспользо- ваться циклической конструкцией do. Ее синтаксис: do { // Код цикла... } while (х); Пусть вас не смущает тот факт, что в завершение конструкции do используется все то же ключевое слово while, — принцип действия этих двух циклов совершен- но разный. В конструкции do вначале всегда выполняется код, заключенный в фигурные скобки (если таковой присутствует), и только потом оценивается логическое выра- жение. Конечно же, если все, что нам нужно, — это бесконечный цикл внутри функ- ции main (), то разницы нет, какой конструкцией воспользоваться: do или while: main () { // Код инициализации // Главный цикл приложения do { } while (1) } // main В качестве особого случая рассмотрим следующий пример: do { // Блок кода... } while (0); Блок кода внутри такой циклической конструкции в любом случае будет вы- полнен только один раз. Другими словами, использование слов do и while в дан- ном примере — пустая трата сил (и мы получаем еще одного кандидата па звание чемпиона мира по бесполезности программного кода).
44 Глава 3. И еще о циклах А теперь рассмотрим более полезный пример, в котором с помощью конструк- ции while организован цикл для выполнения некоторого блока кода четко задан- ное число раз. Прежде всего, нам*требуется переменная для реализации счета. Дру- гими словами, необходимо выделить одну или несколько ячеек ОЗУ для хранения значения счетчика. ПРИМЕЧАНИЕ___________________ ____________ В предыдущих двух уроках нам удавалось почти полностью игнорировать вопрос объявления переменных, поскольку мы опирались исключительно на регистры спе- циального назначения PIC24, которые по сути и являются предопределенными пе- ременными. Объявление переменных Для объявления целочисленной переменной можно использовать следующий синтаксис: int с; Поскольку мы используем ключевое слово int для объявления переменной с как 16-разрядпого (знакового) целого числа, компилятор MPLAB СЗО выделит для нее два байта памяти. Позже компоновщик определит, где именно будут размещены эти два байта в физическом ОЗУ выбранной модели PIC24. После определения пе- ременная с может хранить значения от минимального -32 768 до максимального +32 767. Если нам необходим более широкий целочисленный диапазон, мы можем воспользоваться знаковым типом данных long: long с; Для такой переменной компилятор MPLAB СЗО выделит 32 бита (четыре бай- та). Если же нам достаточно небольшого счетчика с диапазоном значений от -128 до +127, то мы можем воспользоваться целочисленным типом char: char с; В этом случае компилятор MPLAB СЗО выделит восемь бит (один байт). Все три типа данных можно модифицировать с помощью атрибута unsigned (беззнаковые целые числа): unsigned char с; // Диапазон 0..255 unsigned int i; // Диапазон 0..65,535 unsigned long 1; // Диапазон 0.-4,294,967,295 Кроме того, существуют типы данных, предназначенные для арифметики чисел с плавающей запятой: float f; // 32-разрядное вещественное число long double d; // 64-разрядное вещественное число Конструкция for Возвращаемся к нашему примеру со счетчиком. Все, что нам нужно, — это це- лочисленная переменная, используемая в качестве индекса, которая покрывает диа- пазон значений от 0 до 5. Таким образом, достаточно типа данных char: char i; // Переменная i объявлена как 8-разрядное целое со знаком
Полет 45 i = 0; // Инициализация индекса (счетчика) while (i<5) { // Вставьте здесь свой код... // Он будет выполнен при i = 0, 1, 2, 3, 4 i = i+1; // инкремент } Подобные циклы — неотъемлемый элемент повседневной жизни программиста. В языке С для их кодирования существует специальная конструкция for, которая для рассмотренного случая выглядит следующим образом: for (i=0; i<5; i=i+l) ( // Вставьте здесь свой код... // Он будет выполнен при i = 0, 1, 2, 3, 4 } Согласитесь, что конструкция for более компактна и удобочитаема. Кроме то- го, она более проста в отладке. Три выражения в круглых скобках, отделенные друг от друга точкой с запятой после ключевого слова for — в точности те же вы- ражения, что и в предыдущем примере: • инициализация индекса; • проверка условия выхода из цикла с помощью логического выражения; • приращение индекса (счетчика) — в данном случае в сторону увеличения. Возможно, кому-то конструкция for показалась упрощенной формой конст- рукции while, однако это не так. В данном случае вначале оценивается логическое выражение, и если оно сразу же возвращает значение false, то код внутри фигур- ных скобок вообще не выполняется. Сейчас — самое время поговорить об одной сокращенной форме записи, очень популярной при программирования на С. Речь идет о специальных операторах ин- кремента и декремента: • +4---инкремент (например, 14-4-, что эквивалентно i = i-i-1); • декремент (например, i—, что эквивалентно i = i-1). Более подробно мы об этом поговорим в последующих главах. Примеры циклов Рассмотрим еще несколько примеров использования конструкции for и опера- торов инкремепта/декремепта. Вначале счет от 0 до 4: for ( i=0; i<5; i+ + ) { // Вставьте здесь свой код... // Он будет выполнен при i = 0, 1, 2, 3, 4 } Затем обратный счет от 4 до 0: for ( i=4; i>=0; i—) { // Вставьте здесь свой код... // Он будет выполнен при i = 4, 3, 2, 1, 0
46 Глава 3. И еще о циклах Можем ли мы использовать конструкцию for для кодирования бесконечного главного цикла программы? Конечно, и вот пример: main () { // 0. Код инициализации // 1. Главный цикл приложения for (; 1;) { } } // main Если кому-то нравится, то он может использовать такую форму. Лично я пред- почитаю конструкцию while (сами знаете: от старых привычек тяжело отвыкать). Массивы Прежде, чем мы приступим к разработке нашего следующего проекта, рассмот- рим еще одну особенность языка С: массивы. Массив — это просто неразрывный блок памяти, содержащий заданное число идентичных однотипных элементов. По- сле того как массив определен, к каждому его элементу можно получить доступ по имени массива и индексу элемента. Массив объявляется как обычная переменная, но с указанием в квадратных скобках количества элементов: char с [ 10]; int i[10]; long 1 [10]; // с — массив из 10 целых восьмиразрядных чисел // i - массив из 10 целых 16-разрядных чисел // 1 - массив из 10 целых 32-разрядных чисел Те же квадратные скобки используют для ссылки на содержимое каждого эле- мента или присвоения ему значений: а = с[0]; с[1] = 123; i[2] = 12345; 1[3] = 123*i[4]; // Значение 1-го элемента массива с копируется в а // Второму элементу массива с присваивается 123 // Третьему элементу массива i присваивается 12345 // Вычисляется произведение 123 и значения пятого // элемента массива i ПРИМЕЧАНИЕ В языке С элементы массива размером N нумеруются, начиная с нуля, т.е. 0, 1, 2...(N-1). Рассмотрим пример, в котором мы объявляем массив из 10 целых чисел и ини- циализируем каждый его элемент константой 1: int а[10]; // Объявление массива из 10 целых чисел: а[0], а[1]...а[9] int i; // Счетчик цикла for (i=0; i<10; i++) { a[i] = 1; } Новая демонстрация Наилучший способ завершить этот урок — применить все рассмотренные до сих пор элементы языка С в очередном проекте. Суть данного проекта: заставить набор светодиодов, подключенных к выводам порта А (например, в демонстраци-
Полет 47 ошюй плате Explorer 16), мерцать в быстрой последовательности, чтобы при рит- мичном смещении платы влево и вправо они отображали короткое сообщение. Как насчет “Hello World!” или просто скромного “HELLO”? еФайл More Loops .с с представленным ниже кодом находится на прилагаемом к книге компакт- диске в папке Проекты\ 03 - Еще о циклах. #include <p24fj128ga010.h> // 1. Определение констант временной задержки #define SHORT_DELAY 100 #define LONG_DELAY 800 // 2. Объявление и инициализация массива с шаблоном сообщения char bitmap[30] = { Obllllllll, // Н ОЬООООЮОО, ObOOOOlOOO, Obll111111, ObOOOOOOOO, оьоооооооо, Obllllllll, // E OblOOOlOOl, OblOOOlOOl, OblOOOOOOl, ObOOOOOOOO, ObOOOOOOOO, Obllllllll, // L OblOOOOOOO, OblOOOOOOO, OblOOOOOOO, ObOOOOOOOO, ObOOOOOOOO, Obllllllll, // L OblOOOOOOO, OblOOOOOOO, OblOOOOOOO, ObOOOOOOOO, ObOOOOOOOO, ObOllllllO, // о OblOOOOOOl, OblOOOOOOl, ObOllllllO, ObOOOOOOOO, ObOOOOOOOO }; // 3. Главная программа main () { // 3.1 Объявление переменной int i; // i - индекс // 3.2 Инициализация TRISA = OxffOO; // Выводы порта А, соединенные co светодиодами, // работают как выходы T1CON = 0x8030; // TMR1 включен, делитель 1:256 Tclk/2 // 3.3 Главный цикл while (1)
48 Г лава 3. И еще о циклах // 3.3.1 Цикл отображения, рука движется вправо for(i=0; i<30; i++) { // Обновляем светодиоды PORTA = bitmap[i]; // Короткая пауза TMR1 = 0; while (TMR1 < SUORT_DELAY) { } } // for i // 3.3.2 Длинная пауза, рука движется обратно влево PORTA =0; // Отключаем светодиоды // Длинная пауза TMR1 = 0; while (TMR1 < LONG_DELAY) { } } // Главный цикл } // main В разделе 1 мы определяем две константы временной задержки для управления скоростью мерцания светодиодов в ходе выполнения и отладки программы. В разделе 2 мы объявляем и инициализируем массив из 30 целых восьмираз- рядных чисел, каждое из которых содержит шаблон включения светодиодов в опре- деленный момент времени. СОВЕТ________________ _________________________ Для того чтобы четко увидеть сообщение, выделите единицы на странице книги с помощью цветного маркера. Раздел 3 содержит главную программу: объявление переменной (3.1), после ко- торого следует инициализация микроконтроллера (3.2) и главный цикл (3.3). Главный цикл (while) в свою очередь разбит на две части: • 3.3.1 — фактическая последовательность включения светодиодов из 30 шагов, которые должны быть выполнены в ходе перемещения платы слева направо; для последовательного доступа к элементам массива иснользуегся конструкция for, а для реализации надлежащих пауз с помощью таймера 1 — конструкция while; • 3.3.2 — пауза для обратного движения, реализованная путем длительной за- держки с помощью таймера 1 (конструкция while). Тестирование с помощью логического анализатора Для того чтобы протестировать программу, мы вначале воспользуемся про- граммным имитатором MPLAB SIM и окном Logic Analyzer. 1. Выполните сборку проекта (см. контрольный список “Сборка проекта” в конце книги). 2. Откройте окно Logic Analyzer. 3. Нажмите кнопку Channels и добавьте в список все выводы порта А (от RA0 до RA7), соединенные с линейкой светодиодов. Для того чтобы не пропустить ничего важного, можете свериться с контроль- ными списками “Настройка отладки в MPLAB SIM” и “Настройка логического ана- лизатора” в конце книги.
Полет 49 4. Перейдите в окно редактора, установите курсор на первом операторе в разделе 3.3.2, щелкните правой кнопкой мыши и выберите в контекстном меню команду Run to Cursor. В результате будет выполнена целиком часть программы, реали- зующая вывод сообщения (3.3.1), и процесс остановится перед длинной задерж- кой. 5. Как только имитация остановится на строке с курсором, можете переключиться в окно Logic Analyzer и проверить форму выходного сигнала. Опа должна соот- ветствовать рис. 3.2. Для удобства анализа результатов я добавил к иллюстра- ции несколько точек, соответствующих включенным светодиодам на первых нескольких шагах последовательности. Если такие точки проставить во всех по- зициях, где на выводах был зафиксирован высокий уровень сигнала, то полу- чится сообщение “HELLO”. Рис. 3.1. Окно Logic Analyzer после первого прохода Использование демонстрационной платы Explored6 При наличии демонстрационной платы Explorer 16 все становится вдвойне ин- тересно. 1. С помощью контрольного списка “Настройка отладчика MPLAB ICD2” активи- зируйте внутрисхемный отладчик. 2. По контрольному списку “Конфигурация для демонстрационной платы Explor- er^” проверьте корректность настройки конфигурационных разрядов. 3. Опираясь на контрольный список “Программирование в MPLAB ICD2”, запро- граммируйте впутрисхемно микроконтроллер PIC24. Если все прошло успешно, то, приглушив освещение в комнате и тряся плату из стороны в сторону, можно будет прочитать светящееся сообщение. Впрочем, такой эксперимент далек от идеала. С помощью имитатора и окна Logic Analyzer мы мо- жем выбрать для визуализации конкретный фрагмент последовательности и “замо- розить” его на экране, в то время как синхронизировать движение демонстрацион- ной платы с мерцанием светодиодов не так-то просто. Возможно, придется откорректировать временные константы. Например я по- сле ряда экспериментов счел идеальными для короткой и длинной задержки значе- ния 100 и 800 соответственно, по в вашем случае они могут отличаться.
50 Г лава 3. И еще о циклах Разбор полета В этом уроке мы познакомились с объявлением переменных нескольких базо- вых типов, включая целочисленные и вещественные. Кроме того, для создания по- следовательности шаблонов включения светодиодов мы воспользовались массивом, а для ее циклической отработки — конструкцией for. Заметки для экспертов по ассемблеру Тот, кто подумал, что операторы инкремента и декремента транслируются ком- пилятором СЗО в ассемблерные команды inc и dec, почти прав. “Почти”, потому что операторы ++ и — в действительности намного “умнее”. Если они применяют- ся к целым числам, как в рассмотренных выше примерах, то действительно им со- ответствуют ассемблерные команды inc и dec. Но если их применить к указателю (переменная, содержащая адрес некоторой ячейки памяти), то происходит прираще- ние адреса на точное число байт, необходимое для представления величины, на ко- торую ссылается указатель. Так, для указателя на 16-разрядное целое приращение адреса составит 2, для указателя на 32-разрядное целое — 4 и т.д. Для того чтобы удовлетворить свое любопытство, переключитесь в окно дизассемблера и посмот- рите, каким образом MPLAB СЗО формирует ассемблерный код в зависимости от ситуации. Циклы в С порой вызывают вопросы. Где проверять условие: в начале или в конце? Использовать конструкцию for или нет? Иногда ответ на подобные во- просы диктует сам кодируемый алгоритм, однако зачастую мы имеем значительную свободу выбора. В таких случаях советую выбирать ту циклическую конструкцию, которая делает исходный текст программы более наглядным. Заметки для экспертов по PIC На компактность и эффективность кода может значительно повлиять архитек- тура целевого микроконтроллера и особенно то, чем оперирует арифметико- логическое устройство (АЛУ): байтами или словами. В то время как в восьмираз- рядных архитектурах PIC16 и PIC18 везде, где возможно, использовались одно- байтные целые числа, в 16-разрядной архитектуре PIC24 не менее эффективно мож- но работать и с двухбайтными значениями. Единственным фактором, сдерживаю- щим повсеместное использование компилятором MPLAB СЗО 16-разрядных целых чисел, является относительная ограниченность внутренних ресурсов микроконтрол- лера (если говорить конкретнее — объем ОЗУ). Заметки для экспертов по С Несмотря на то, что микроконтроллеры PIC24 оснащены сравнительно боль- шим ОЗУ, приложениям для встроенных систем всегда приходится мириться с ре- альностью ограничений по стоимости и размерам. Тот, кто привык программиро- вать на С для ПК или рабочих станций, скорее всего, никогда не использовал для счетчиков циклов типов данных меньше, чем int. Что ж, придется переучиваться. Чем больше байтов вы сэкономите при разработке приложения, тем выше вероят- ность, что вы сможете подобрать модель микроконтроллера PIC24 поменьше и по- дешевле. Хотя такая экономия иногда составляет всего лишь несколько центов на один микроконтроллер, при выпуске устройств тысячными или даже миллионными
Советы и хитрости 51 сериями финансовый эффект получается значительный. Другими словами, тот, кто выработал у себя привычку объявлять переменные минимально возможного разме- ра, становится лидером в сфере разработки встроенных систем управления. Советы и хитрости Думаю, читатель уже успел заметить, что я в третьем уроке подряд рекомендую начать имитацию, установив курсор в первой строке кода и выбрав команду Run То Cursor вместо того, чтобы просто выполнить пошаговое выполнение программы. Зачем все это нужно? Почему бы просто на перейти в режим “Animate” сразу же по- сле сборки проекта? Как я уже не однократно упоминал, все дело — в коде инициализации СО. Так- же добавлю, что у MPLAB есть навязчивая идея оградить пользователя от низко- уровневых подробностей. Фактически, MPLAB даже не показывает курсор (боль- шую зеленую стрелку) при пошаговой отладке, что иногда сильно сбивает с толку). Вы не сможете увидеть трассировку кода СО даже в окне Disassembly Listing. Однако код СО уже начинает обретать для нас интерес. Например, в последнем упражнении мы объявили массив bitmap [ ] и инициализировали его заданным на- бором значений. Массив, являясь структурой данных, во время выполнения про- граммы размещается в ОЗУ, поэтому компилятор должен указать в коде инициали- зации СО на необходимость скопировать содержимое массива из таблицы в флэш- памяти сразу же после запуска программы. Единственный способ увидеть содержимое кода СО — открыть окно Program Memory (Память программ) (команда меню View ► Program Memory), выбрать режим Symbolic (Символический) с помощью соответствующей кнопки у нижнего края ок- на, и тщательно исследовать ассемблерный код. При этом можно ориентироваться по особым меткам. Первая строка в окне Program Memory соответствует вектору сброса PIC24 и всегда содержит команду безусловного перехода к действительному началу программы. 0000 goto _reset Далее придется пролистать несколько страниц кода, которым, как вскоре будет показано, соответствует таблица векторов прерываний. В конце концов, вы найдете метку _reset. Здесь находятся три ключевых фрагмента кода: • инициализация указателя стека (wl5): _reset mov.w #0x81e,wl5 • вызов подпрограммы для инициализации переменной (в ОЗУ): rcall _data_init • вызов функции main (): call main • команда программного сброса по окончании выполнения программы: reset Надеюсь, пока что ваше любопытство удовлетворено. Если в будущем в ходе отладки вы не сможете найти курсор, то, вполне вероятно, он находится именно в рассмотренном фрагменте кода. Вероятно, что-то вызывало сброс (ошибка в про- грамме или какое-то внешнее событие) и вы оказались в глубинах кода инициализа-
52 Глава 3. И еще о циклах ции СО. Для выхода из такой ситуации обращайтесь к контрольным спискам в раз- деле “Аварийные ситуации” приложения в конце книги. Упражнения 1. Усовершенствуйте синхронизацию мерцания, реализовав ожидание нажатия кнопки в момент начала движения платы. 2. Добавьте переключатель для регистрации начала обратного движения платы. Ссылки • Web-страница http: //www.bugbookcomputermuseum.com/BugBook~ Titles . html — музей классических книг по микропроцессору Intel 8080. По- думать только: он появился всего лишь 30 лет назад, а кажется, что уже прошло несколько столетий!
ГЛАВА 4 Числа В этой главе: > Вопросы оптимизации > Тестирование > Использование целочисленного типа long > Заметки по умножению чисел типа long > Тип данных long long > Числа с плавающей запятой Наше ощущение пространственного положения основано на особом устройстве (лабиринт уха или вестибулярный аппарат), расположенном внутри головы. Оно да- ет нам обратную связь по силе тяжести и движению. Однако, в отличие от вестибу- лярного аппарата птиц, наш не рассчитан на полеты. Его можно легко ввести в за- блуждение даже небольшим центростремительным ускорением, что в отсутствие визуальных ориентиров (например, в условиях тумана, внутри облаков или в ходе ночного полета) может запросто привести к срыву в штопор. Поскольку мы не мо- жем опираться на чувства, нам приходится ориентироваться по приборам, чтобы выяснить скорость, направление и, что, пожалуй, особенно важно — где верх, а где низ. Другими словами, вся информация, которая поступает в разум птицы от ее ор- ганов чувств, мы получаем только в виде чисел. После первых нескольких полетов, курсант летной школы проводит немало времени за заучиванием “правильных” чисел для своего самолета: оптимальная ско- рость набора высоты или планирующего спуска, скорость отрыва от земли или за- хода на посадку и т.д. В большинстве случаев все эти параметры указаны в руково- дстве по пилотированию, в техническом описании самолета и, для удобства, — в соответствующих контрольных списках. Каждый летчик старается придерживать- ся их как можно точнее, чтобы управление машиной не приводило к аварийным си- туациям. Любой опытный пилот — особенно рейсовых самолетов, за штурвалом ко- торых он проводит тысячи часов в год, — может рассказать, насколько непринуж- денным становится полет, когда ты знаешь все числа наизусть. Подобным же образом и в мире встроенных систем нам необходимо хорошо знать числовые типы данных, их сравнительные характеристики и преимущества каждого из них. План полета В этом уроке будет выполнен обзор всех числовых типов данных, предлагаемых компилятором MPLAB® СЗО. Мы узнаем, сколько памяти он выделяет’ под числовые
54 Глава 4. Числа переменные и исследуем относительную эффективность подпрограмм, используе- мых для выполнения арифметических операций. При этом в качестве базового из- мерительного инструмента будет задействовано окно Stopwatch (Секундомер), дос- тупное при работе с имитатором MPLAB SIM. Такая практика поможет читателю выбирать для встроенных приложений “правильные” числа с пониманием баланса между производительностью и ресурсами памяти, ограничениями работы в режиме реального времени и сложностью. Предполетный контроль Весь этот урок опирается исключительно на программные средства в составе интегрированной среды MPLAB, компилятора MPLAB СЗО и имитатора MPLAB SIM. Для создания нового проекта под именем Numbers (“Числа”) и исходного фай- ла numbers. с, который будет содержать текст программы, воспользуйтесь кон- трольным списком “Настройка нового проекта” в конце книги. Полет Для ознакомления со всеми доступными типами данных рекомендую обратить- ся к руководству пользователя компилятора MPLAB СЗО. Можете сразу переходить к главе 5, в которой сразу же перечислены поддерживаемые целочисленные типы (табл. 4.1). Таблица 4.1. Целочисленные типы данных Тип данных Биты Минимальное значение Максимальное значение char,signed char 8 -128 127 unsigned char 8 0 255 short, signed short 16 -32 768 32 767 unsigned short 16 0 65 535 int,signed int 16 -32 768 32 767 unsigned int 16 0 65 535 long,signed long 32 -23' 231 - 1 unsigned long 32 0 232 -1 long long**, signed long long** 64 -263 263-1 unsigned long long** 64 0 2м- 1 "Расширение ANSI-89 Как видно из табл. 4.1, существует 10 различных целочисленных типов данных, определенных стандартом ANSI С: char, int, short, long и long long — ка- ждый в знаковом (по умолчанию) и беззнаковом варианте. Также в табл. 4.1 указано количество бит, выделяемых компилятором MPLAB СЗО для каждого из типов, и соответствующий диапазон допустимых значений. В знаковых значениях один разряд отводится под знак, в результате чего число- вой диапазон сужается вдвое. Также интересно отметить, что для компилятора СЗО типы int и short — это синонимы. Для каждого из них выделяется 16 бит памяти. Как восьми-, так и 16-разрядные значения эффективно обрабатываются АЛУ мик- роконтроллера PIC24, благодаря чему большинство арифметических операций мо- жет быть закодировано компилятором с помощью малого числа эффективных ко- манд. Типу данных long соответствуют 32-разрядные (четырехбайтные) значения,
Полет 55 в то время как типу long long (определен в расширении стандарта ANSI С 1989 года) требуются восемь байт. Операции над длинными целыми реализуются компи- лятором с помощью коротких подставляемых последовательностей команд, поэтому использование чисел типа long (и тем более — long long) несколько снижает производительность приложения. Переходим к первому примеру: unsigned int main () { i = 0x1234; j = 0x5678; k = i * j; i,j,k; // Присвоение i // Присвоение j // Перемножение исходного значения исходного значения и сохранение результата в к После сборки проекта (команда меню Project ► Build АП или комбинация клавиш <Ctrl+F 10>) можно открыть окно Disassembly Listing (команда меню View ► Disasse- mbly Listing) и исследовать сформированный компилятором код. Даже не зная в де- талях набора команд PIC24, здесь можно распознать два оператора присваивания. Они реализованы путем загрузки константы в регистр w0, а оттуда — в ячейки па- мяти, выделенные под переменные i и j. 204D20 884290 i = 1234; mov.w #0x4d2,0x0000 // // Загрузка константы в W0 mov.w 0x0000,0x0852 Перенос данных из W0 в i 2162Е0 j = 5678; mov.w #0xl62e,0x0000 // Загрузка l константы в W0 8842А0 mov.w 0x0000,0x0854 // Перенос данных из W0 в j 804291 k = i * j; mov.w 0x0852,0x0002 // Перенос данных из i в W1 8042А0 mov.w 0x0854,0x0000 H Перенос данных из j в W0 В98800 8842В0 mul.ss 0x0002,0x0000,0x0000 mov.w 0x0000,0x0856 // Перенос результата в к Умножение реализовано путем переноса значений из ячеек памяти, выделенных под две целочисленные переменные i и j, обратно в регистры w0 и wl с после- дующим их перемножением по команде mul. Результат, полученный в регистре w0, сохраняется в ячейках памяти, выделенных для переменной к. Как видите, все до- вольно просто. Вопросы оптимизации Можно заметить, что программа в целом несколько избыточна. Например, зна- чение переменной j при его повторной загрузке перед самым умножением все еще присутствует в регистре w0. Получается, компилятор не замечает, что в операции загрузки нет необходимости? Действительно, компилятор не распознает подобных моментов. Его роль — создать “безопасный” код, избегая (по крайней мере изначально) любых предполо- жений, с помощью стандартных последовательностей команд. Позже, после надле- жащей настройки параметров оптимизации, можно выполнить повторную компиля- цию для устранения избыточного кода. В процессе же разработки и отладки настоя- тельно рекомендуется отключить любую оптимизацию, поскольку она может моди- фицировать структуру анализируемого кода и привести к проблемам при пошаговой
56 Глава 4. Числа отладке и расстановке точек прерывания. В этой книге мы будем избегать какой-ли- бо оптимизации, но при этом проверка на соответствие требуемому уровню произ- водительности все же будет выполняться. В результате все примеры из этой и по- следующих глав можно компилировать с помощью бесплатной студенческой вер- сии компилятора СЗО (находится на прилагаемом к книге компакт-диске в папке MPLAB). Тестирование Для тестирования кода можно работать с имитатором непосредственно в окне Disassembly Listing, пошагово выполняя каждую ассемблерную команду. Второй ва- риант — работать с исходным текстом в окне редактора, пошагово выполняя каж- дый оператор языка С. В обоих случаях мы можем: 1. Установить курсор в первой строке, содержащей инициализацию первой пере- менной, и выбрать команду Run То Cursor, чтобы программа выполнила ини- циализацию и остановилась непосредственно перед первой командой, которую мы хотим исследовать. 2. Открыть окно Watch (команда меню View ► Watch), выбрать в расположенном слева раскрывающемся списке элемент WREG0 и нажать кнопку Add SFR. 3. Повторить предыдущий пункт для элемента WREG1. 4. Выбрать в расположенном справа раскрывающемся списке элемент i и нажать кнопку Add Symbol (Добавить символ). 5. Повторить предыдущий пункт для символов j и к. 6. Выполнить следующие несколько строк программы в пошаговом режиме “Step Over”, наблюдая за содержимым регистров и переменных в окне Watch. Как уже было отмечено ранее, когда значение переменной в окне Watch изменяется, оно для наглядности выделяется красным цветом. Если тест необходимо повторить, выполните сброс (команда меню Debugger ► Reset ► Processor Reset), опять разместите курсор в первой строке анализируемого кода и выберите команду Run То Cursor. Использование целочисленного типа long Изменив одну лишь первую строку, мы можем изменить всю программу, заста- вив ее выполнять операции на переменными целочисленного типа long. unsigned long main () { i = 0x1234; // Присвоение i исходного значения j = 0x5678; // Присвоение j исходного значения k = i * j; // Перемножение и сохранение результата в к Выполните повторную сборку проекта и переключитесь в окно Disassembly List- ing (если окно редактора развернуто на весь экран, заслоняя собой открытое ранее окно Disassembly Listing, то для быстрого переключения в него можно воспользо- ваться комбинацией клавиш <Ctrl+Tab>). Как видим, теперь ассемблерный код зна- чительной объемней, чем в предыдущей версии программы. Часть инициализации осталась такой же простой, а вот для реализации умножения команд потребовалось больше. k = i * j;
Полет 57 8042С1 mov.w 0x0858,0x0002 8042Е0 mov.w 0x085с,0x0000 В80А00 mul.uu 0x0002,0x0000,0x0008 8042С1 mov.w 0x0858,0x0002 8042F0 mov.w 0x085e,0x0000 В98800 mul.ss 0x0002,0x0000,0x0000 780105 mov.w 0x000a,0x0004 410100 add.w 0x0004,0x0000,0x0004 8042Е1 mov.w 0x085c,0x0002 8042D0 mov.w 0x085a,0x0000 В98800 mul.ss 0x0002,0x0000,0x0000 410100 add.w 0x0004,0x0000,0x0004 780282 mov.w 0x0004,0x000a 884304 mov.w 0x0008,0x0860 884315 mov.w 0x000a,0x0862 АЛУ PIC24 в каждый момент времени может обрабатывать только 16 бит, по- этому 32-разрядпое умножение фактически представляет собой последовательность 16-разрядных умножений и сложений. Последовательность, формируемая компиля- тором, во многом напоминает методику, которой нас учили в начальной школе. Раз- личие состоит только в том, что в данном случае обрабатываются машинные слова, а не цифры. Заметки по умножению чисел типа long На практике для выполнения 32-разрядного умножение с помощью 16- разрядных команд требуется четыре умножения и два сложения, однако компилятор фактически выдал только три команды умножения. В чем же дело? Перемножение двух целых чисел типа long (по 32 бита каждое) дает 64-раз- рядный результат. Однако в представленном выше примере мы определили, что ре- зультат должен храниться в еще одной переменной типа long, т.е. ограничили его максимум 32 битами. Тем самым мы создали все предпосылки для возникновения переполнения, ио в то же время позволили компилятору проигнорировать старшие значащие разряды результата. Как следствие, компилятор полностью устранил чет- вертый этап умножения, сразу же выполнив своеобразную оптимизацию. Тип данных long long Теперь изменим тип переменных на long long (64-разрядпый): unsigned long main () long i,j,k; i = 0x1234; j = 0x5678; k = i * j; // Присвоение i исходного значения // Присвоение j исходного значения // Перемножение и сохранение результата в к Если теперь перекомпилировать и исследовать новый код в окне Disassembly Listing, то окажется, что на этот раз компилятор избрал другой подход. Вместо более длинных подстановочных последовательностей теперь используются всего лишь несколько команд пересылки данных в предопределенные регистры с последующим вызовом какой-то подпрограммы. Эта подпрограмма расположена в листинге дизассемблера после кода функции main (). Ее можно четко идентифицировать по строке комментария, которая ука- зывает, что данный фрагмент является частью библиотечного модуля muldi3.c.
58 Глава 4. Числа Исходный текст этой подпрограммы фактически относится к документации компи- лятора СЗО и расположен в подкаталоге MPLAB C30\src\libm\src. В данном случае, обратившись к подпрограмме, компилятор явно пошел на компромисс. Вызов подпрограммы подразумевает несколько дополнительных ко- манд и требует использования стека. С другой стороны для каждого перемножения чисел типа long long будет задействовано всего лишь несколько команд, что экономит кодовое пространство. Числа с плавающей запятой Кроме целочисленных, компилятор СЗО поддерживает несколько типов данных для работы с дробными значениями: так называемыми числами с плавающей запя- той (или вещественными числами). В нашем распоряжении — зри таких типа, отли- чающихся уровнем разрешения: float, double и long double. Учтите, что по умолчанию компилятор MPLAB СЗО на основании формата ве- щественных чисел одинарной точности, определенного стандартом IEEE754, выде- ляет для значений типа float и double одинаковое число бит. Как настоящие ве- щественные числа двойной точности по стандарту IEEE754 обрабатываются только значения типа long double, (табл. 4.2). Таблица 4.2. Вещественные типы данных Тип Биты EMin EMax N Min N Max float 32 -126 127 2~126 2128 double 32 -126 127 2~126 2128 long double 64 -1 022 1 023 2~1022 2 Ю24 Е — экспонента N — нормализованное значение (приближенное) Заметки для экспертов по С Я твердо уверен, что описанные выше установки для обработки вещественных чисел были использованы разработчиками MPLAB СЗО намеренно с целью упроще- ния и повышения эффективности переноса сложных математических алгоритмов в приложения, предназначенные для управления встроенными системами. В боль- шинстве случаев документированные алгоритмы и библиотеки рассчитаны на про- изводительность и ресурсы персональных компьютеров и рабочих станций и пото- му по возможности опираются на арифметику вещественных чисел двойной точно- сти для достижения максимальной точности вычислений. Обычно во встроенных системах немного жертвуют точностью ради уровня производительности, необхо- димого для обеспечения отклика в реальном режиме времени. Если подобные жертвы не нужны, то в определенных местах программы можно изменить типы переменных с double на long double или же установить специ- альный глобальный параметр компилятора (выбрать команду меню Project ► Build Options ► Project, установить флажок Use Alternate Setting (Использовать альтерна- тивную настройку) и ввести в расположенном ниже поле ‘Mho-short-double”). Поскольку микроконтроллер PIC24 не содержит аппаратного модуля для обра- ботки чисел с плавающей запятой (FPU), все операции этого вида должны кодиро- ваться компилятором с применением соответствующих библиотек. Разумеется, раз- мер и сложность таких библиотек значительно превосходят эти же показатели для библиотек целочисленных вычислений. Таким образом, использование веществен-
Заметки для экспертов по С 59 ных типов данных неминуемо влечет за собой определенную потерю производи- тельности, однако, если задача требует точного учета дробных величин, то можно смело доверить это компилятору СЗО. Изменим рассмотренный ранее пример, задействовав вещественные перемен- ные: float i,j,k; main () { i = 12.34; j = 56.78; k = i * j; } // Присвоение i исходного значения // Присвоение j исходного значения // Перемножение и сохранение результата в к После перекомпиляции и исследования содержимого окна Disassembly Listing выясняется, что компилятор сразу же предпочел использовать вместо подставляе- мого кода подпрограмму. Если в программе вместо типа float указать double или long double, то результат будет аналогичным: изменятся только начальные присвоения, после чего будет вызвана та же подпрограмма, что и раньше. Компилятор С настолько упрощает использование любого типа данных, что возникает искушение всегда выбирать самые “большие” целочисленные и вещест- венные типы — просто “на всякий случай”, чтобы избежать риска переполнения или потери значащих разрядов. В то же время правильный выбор типа данных во встроенном приложении может играть ключевую роль для поддержания баланса между производительностью и потреблением ресурсов. Для принятия взвешенного решения необходимо знать об ожидаемом уровне эффективности, соответствующем тому или иному типу данных. Измерение эффективности Давайте применим полученные до этого момента знания о средствах имитации для измерения фактической относительной эффективности арифметических биб- лиотек (целочисленных и вещественных), задействованных компилятором СЗО. Для начала мы воспользуемся специальным окном Stopwatch, применяемым при про- граммной имитации встроенными средствами MPLAB SIM. За основу возьмем сле- дующую программу: ©Соответствующий файл Numbers, с находится на прилагаемом к книге компакт-диске в папке Проекты\04 - Числа. "// // Числа // int il, i2, i3; long 11/ 12, 13; long long 111, 112, 113; float fl,f2, f3; long double dl, d2, d3; main () { il = 1234; i2 = 5678; i3= il * i2; // Тест целых чисел (16-разрядных) // 1. Перемножение чисел типа int
60 Глава 4. Числа 11 = 1234; 12 = 5678; 13= 11 * 12; // Тест длинных целых чисел (32-разрядных) // 2. Перемножение чисел типа long 111 = 1234; // Тест целых чисел типа long long (64-разрядных) 112 = 5678; 113= 111 * 112; // 3. fl = 12.34; // Тест вещественных чисел одинарной точности f2 = 56.78; f3= fl * f2; // 4. Перемножение чисел типа float dl = 12.34; // Тест вещественных чисел двойной точности d2 = 56.78; d3= dl * d2; // 5. Перемножение чисел типа double } После компиляции и компоновки проекта можно установить курсор в окне ре- дактора в строке, содержащей первое целочисленное перемножение (// 1.) и вы- брать команду Run То Cursor, чтобы установить счетчик команд в начальную пози- цию для нашего теста. Откройте окно Stopwatch (команда меню Debugger ► Stop- watch) и разместите его на экране в соответствии со своими предпочтениями (лично мне нравится размещать его вдоль нижнего края экрана, чтобы оно не заслоняло со- бой окно редактора). Нажмите в окне Stopwatch кнопку Zero, чтобы обнулить таймер, и выполните один оператор программы в режиме “Step Over” (команда меню Debugger ► Step Ov- er или клавиша <F8>). После того как имитатор завершит обновление окна Stop- watch, зарегистрируйте где-нибудь время, которое ушло на выполнение целочис- ленной операции. Это время отображается имитатором в виде числа циклов вместе со значением в миллисекундах, полученным как количество циклов, умноженное на частоту синхронизации имитатора (параметр, задаваемый па вкладке Osc/Trace диа- логового окна Simulator Settings, которое открывается по команде меню Debugger ► Settings). Далее установите курсор в строке со следующим умножением (/ / 2 .) и опять выберите команду Run То Cursor или просто продолжайте пошаговое выполнение программы, пока не достигнете данной строки. Опять-таки, обнулите секундомер, выполните один шаг отладки и зарегистрируйте полученный показатель времени. Повторите описанный выше процесс для получения всех пяти замеров, соответст- вующих различным типам данных. Пример полученных результатов (в виде количества циклов) показан в табл. 4.3. Здесь же отображены относительные коэффициенты эффективности (получаются путем деления числа циклов в каждой строке па число циклов, зафиксированное для базового типа). Таблица 4.3. Относительная эффективность вьнислений для MPLAB СЗ01.30 (без оптимизации) Тест на перемножение Число циклов Эффективность относительно int long float Чисел типа int 4 1 — — Чисел типа long 15 3,75 1 — Чисел типа long long 99 24,75 6,6 — Чисел типа float 121 30 8 1 Чисел типа double 317 79 21 2,6
Разбор полета 61 Не тревожьтесь, если у вас получились другие значения, поскольку на измере- ния влияет несколько факторов. Будем надеяться, в следующих версиях компилято- ра СЗО будут задействованы более эффективные библиотеки и/или появятся средст- ва оптимизации, доступные па этапе тестирования. Не забывайте, что рассмотренный вид тестирования далек от тех строгих требо- ваний, которые предъявляются реальными средствами анализа производительности. По большому счету, нас в данном случае интересовало только общее представление о влиянии на эффективность вычислений различных типов. Для этой цели пред- ставленная выше табл. 4.3 вполне наглядно показывает некоторые интересные зако- номерности. Как и следовало ожидать, 16-разрядные операции — самые скоростные. Пере- множение чисел типа long (32-разрядпых) занимает почти в четыре раза больше времени, а та же операция применительно к значениям типа long long (64- разрядпые) — еще па порядок медленнее. Опять-таки, операции с вещественными числами одинарной точности предсказуемо менее эффективные, чем целочисленные операции. В то время как перемножение 32-разрядных целых всего лишь в четыре раза медленнее перемножения 16-разрядных целых, перемножение 32-разрядпых вещественных чисел протекает медленнее уже более, чем в 30 раз. Тем не менее, переход от 32- к 64-разрядпым вычислениям с вещественными числами увеличивает количество требуемых циклов только вдвое. Таким образом, задействованные ком- пилятором библиотеки для вещественных вычислений двойной точности более эф- фективны по сравнению с соответствующими 64-разрядпыми библиотеками для це- лочисленных вычислений. Так в каких же случаях следует использовать вещественные числа, и в каких — целочисленную арифметику? Кроме очевидных ситуаций, из пройденного до сих пор материала мы можем извлечь следующие правила: • по возможности пользоваться целыми числами (т.е. когда не требуются дроб- ные значения или алгоритм можно адаптировать под целочисленную арифмети- ку); • использовать “наименьший” целочисленный тип, не приводящий к переполне- нию или потере значащих разрядов; • если обработки вещественных чисел никак не избежать, то будьте готовы к сни- жению производительности откомпилированной программы; • переход от вещественных чисел одинарной точности (float или double) к вещественным числам двойной точности (long double) снижает эффектив- ность всего лишь в два раза. Также помните, что вещественные типы данных предлагают наибольшие диапа- зон допустимых значений, однако при этом всегда вносят приближение. По этой причине такие типы не рекомендуют для финансовых расчетов, для которых более предпочтительными является тип long или long long, когда все операции вы- полняются в центах вместо долларов с дробями. Разбор полета В этом уроке мы познакомились не только с существующими типами данных и выяснили отводимый для пих объем памяти, по и узнали об их влиянии па отком- пилированную программу (размер кода и скорость выполнения). Для подсчета ко- мандных циклов (а значит — и времени), требуемых для выполнения фрагментов
62 Глава 4. Числа кода, мы воспользовались окном Stopwatch имитатора MPLAB SIM. Вся эта инфор- мация пригодится читателю в будущем для поддержания баланса между точностью вычислений и производительностью встроенного приложения. Заметки для экспертов по ассемблеру Тех немногочисленных храбрецов, которые при программировании на ассемб- лере решались работать с вещественными числами, возможности, предоставляемые для этого компилятором С, безусловно, обрадуют. Теперь арифметика одинарной и двойной точности ничуть не сложнее кодирования целочисленных вычислений. Тем не менее, при работе с целыми числами ситуация иногда выходит из-под контроля, когда компилятор скрывает детали реализации, и некоторые операции становятся неясными и гораздо менее удобочитаемыми. Перечислим несколько примеров преобразований и байтовых операций, которые могут' вызывать некоторое беспокойство: • преобразование целочисленных типов в другие целочисленные типы меньшей или большей разрядности; • извлечение или установка старшего или младшего байта в 16-разрядном типе данных; • извлечение или установка одного разряда в целочисленной переменной. Язык С предлагает удобные механизмы решения перечисленных проблем путем неявного приведения типов: i int i; // 16-разрядное long 1; // 32-разрядное 1 = i; // Значение i записывается в два младших байта 1, // а два старших байта 1 обнуляются Явное приведение типов может потребоваться в тех случаях, когда в его отсут- ствие компилятор выдает сообщение об ошибке: int i ; long 1; i = (int) 1; // 16-разрядное // 32-разрядное // (int) - это приведение типа, в результате которого // два старших байта 1 отбрасываются Для работы с целочисленными типами, размер которых меньше одного байта, используют битовые поля. Битовые поля обрабатываются компилятором MPLAB СЗО очень эффективно, ввиду чего везде, где возможно, рекомендуется применять манипуляции с разрядами. Библиотечные файлы для PIC24 содержат множество примеров определения битовых полей для работы со всеми управляющими разря- дами периферийных устройств и регистров специального назначения процессора. Ниже представлен фрагмент включаемого файла, использованного в нашем проекте. В нем определен управляющий регистр таймера 1 под названием T1CON, а всем его управляющим разрядам соответствуют поля структуры: extern unsigned int T1CON; extern union { struct { unsigned :1; unsigned TCS:1; unsigned TSYNC:1; unsigned :1; unsigned TCKPSO:!;
Заметки для экспертов по PIC 63 unsigned TCKPS1:1; unsigned TGATE:1; unsigned :6; unsigned TSID1:1; unsigned :1; unsigned TON:1; }; struct { unsigned :4; unsigned TCKPS:2; }; } TICONbits; Заметки для экспертов no PIC Пользователи, знакомые с восьмиразрядными микроконтроллерами PIC и соот- ветствующими компиляторами, заметят значительное увеличение быстродействия как для целочисленной, так и для вещественной арифметики. Изначально 16-разряд- ное АЛУ архитектуры PIC24 имеет явное преимущество, обрабатывая вдвое больше разрядов за один цикл, однако, благодаря наличию до восьми рабочих регистров, производительность повышается еще больше. В результате процесс кодирования ключевых арифметических подпрограмм и числовых алгоритмов становится более эффективным. Советы и хитрости Математические библиотеки Компилятор MPLAB СЗО поддерживает несколько стандартных библиотек ANSI С, включая: • limits.h — содержит множество полезных макроопределений предельных значений, зависящих от реализации (например, CHAR_BIT — разрядность зна- чений типа char или INT_MAX — наибольшее целое число). • float. h — содержит аналогичные определения предельных значений, но уже для вещественных чисел (например, FLT_MAX_EXP — наибольшая экспонента для вещественной переменной одинарной точности). • math. h — содержит тригонометрические, логарифмические и экспоненциаль- ные функции, а также функции округления. Комплексные типы данных Компилятор MPLAB СЗО поддерживает комплексные типы данных, расши- ряющие целочисленные или вещественные типы. Рассмотрим пример объявления вещественного типа одинарной точности: __complex_ float z; Обратите внимание на использование двух символов подчеркивания перед и после ключевого слова complex. Объявленная подобным образом переменная z теперь содержит действитель- ную и мнимую часть, к которым можно обращаться независимо с помощью конст- рукций _real___ z и__imag___ z соответственно.
64 Глава 4. Числа Аналогичным образом, следующее объявление дает комплексную 16-разрядную целочисленную переменную: __complex_ int х; Комплексные константы легко создаются путем добавления суффикса i или j, как в следующих примерах: х = 2 + 3 j ; z = 2.Of + 3.0fj; К комплексным типам данных применимы все стандартные арифметические операторы (т, *, /), а также специальный оператор ~, выполняющий комплекс- ное сопряжение. Комплексные типы бывают очень удобны в некоторых приложениях, делая код более удобочитаемым и помогая избежать тривиальных ошибок. К сожалению, на момент написания этой книги поддержка комплексных переменных па этапе отлад- ки в интегрированной среде MPLAB была реализована только частично, предостав- ляя в окне Window доступ только к действительной части. Упражнения 1. Напишите программу, в которой таймер 2 используется в качестве секундомера для оценки эффективности в режиме реального времени. Если разрядности тай- мера недостаточно, то: • воспользуйтесь предварительным делителем (с потерей младших разрядов) или • реализуйте 32-разрядный режим счета путем объединения таймеров 2 и 3. 2. Выясните относительную эффективность операции деления для различных ти- пов данных. 3. Выясните эффективность тригонометрических функций относительно стан- дартных арифметических операций. 4. Выясните относительную эффективность операции умножения для комплекс- ных типов данных. Ссылки • Если вам интересно, каким образом компилятор С может аппроксимировать не- которые функции из библиотеки math, посетите Web-сграпицу по адресу http://еп.wikipedia.org/wiki/Taylor series.
ГЛАВА 5 Прерывания В этой главе: ► Вложение прерываний Системные прерывания к Шаблон и пример для прерывания от модуля Timerl V- Реальный пример для модуля Timerl Тестирование прерывания от модуля Timerl л Вспомогательный тактовый генератор Календарь реального времени > Управление несколькими прерываниями Каждого пилота учат постоянно наблюдать за линией горизонта для отслежива- ния других самолетов и визуальных ориентиров, уточняющих направление полета. При этом ему ежесекундно необходимо сверяться с приборами, контролируя ско- рость и высоту, и в добавок ко всему еще и заглядывать в карту. Время от времени пилоту приходится сосредотачиваться только на одном из перечисленных источни- ков информации, поэтому для пего крайне важно знать, каким из приборов следует уделять больше внимания, а каким — меньше па том или ином этапе полета в раз- личных условиях. Другими словами, летчики должны научиться работать в много- задачном режиме, правильно распределяя приоритеты и оптимизируя расход време- ни, чтобы всегда держать ситуацию под контролем. По причине минимизации размеров и стоимости встроенные системы, реали- зуемые огромными тиражами, обычно не могут позволить себе такую роскошь как многозадачная операционная система и вынуждены использовать вместо нее меха- низмы прерываний. План полета In this lesson we will see how the MPLAB® C30 compiler allows us to easily man- age the interrupt mechanisms offered by the PIC24 microcontroller architecture. After a brief review of some of the C language extensions and some practical considerations, we will present a short example of how to use the secondary (low-frequency) oscillator to maintain a real-time clock. Предполетный контроль На протяжении всего этого урока мы будем работать исключительно с про- граммными средствами, включая интегрированную среду MPLAB, компилятор
66 Глава 5. Прерывания MPLAB СЗО и имитатор MPLAB StM. Ориентируясь по контрольному списку “На- стройка нового проекта”, создайте проект под названием “Interrupts” (“Прерыва- ния”) и файл исходного кода interrupts . с. Полет Прерывание — это внутреннее или внешнее событие, требующее незамедли- тельного внимания -со стороны центрального процессора. Архитектура PIC24 пре- доставляет гибкую систему прерываний, поддерживающая до 118 различных источ- ников прерывания. С каждым такйм Источником можно сопоставить уникальный фрагмент кода, называемый подпрограммной обслуживания прерывания. Для обес- печения требуемого ответного действия эта подпрограмма напрямую связана со специальным указателем (“вектором”). Прерывания могуг быть полностью асинхронными по отношению к ходу вы- полнения главной программы. Это означает, что они могуг возникать в любой мо- мент времени и в непредсказуемом порядке. Для обеспечения оперативного отклика на событие и быстрого возврата в главную программу крайне важно минимизиро- вать время отклика (период между возникновением события и выполнением первой команды подпрограммы обслуживания прерывания). В архитектуре PIC24 это время не только очень мало, но и фиксировано для каждого источника прерывания: всего лишь три командных цикла для внутренних событий и четыре — для внешних. Бла- годаря этой важной особенности системы управления прерываниями, микрокон- троллеры PIC24 получают явное преимущество перед многими конкурентами. Компилятор MPLAB СЗО помогает справиться со сложной системой прерыва- ний, предоставляя ряд новых языковых расширений. Микроконтроллеры PIC24 хра- нят все векторы прерываний в одной большой таблице, называемой IVT (Interrupt Vector Table), и компилятор MPLAB СЗО может автоматически связывать эти векто- ры со “специальными”, определенными пользователем функциями С. При этом, однако, следует учитывать ряд ограничений: • эти функции не могут’ возвращать значений (т.е. они объявляются как void); • в них не передаются параметры (т.е. используется параметр void); • их нельзя вызывать напрямую из других функций; • в идеале они не должны вызывать какие-либо другие функции. Первые три ограничения — очевидны, учитывая природу механизма прерыва- ний. Поскольку прерывания вызываются внешними событиями, функции их обра- ботки не могут принимать параметры или возвращать значения ввиду отсутствия вызывающей программы. Последнее ограничение, скорее, рекомендательно и слу- жит для повышения эффективности работы системы. Представленный ниже пример иллюстрирует синтаксис, используемый для свя- зывания некоторой функции с вектором прерывания от модуля Timer 1. void __attribute__ ( (interrupt)) _TlInterrupt (void) ( // Здесь расположен код подпрограммы обслуживания прерывания... } // _InterruptVector Имя функции _Т1 Interrupt выбрано не произвольно, а является предопре- деленным идентификатором для прерываний от таймера 1 в таблице IVT микрокон- троллера PIC24 (см. техническое описание). Это закодировано в сценарии компо- новки: файле . gid, загружаемом для текущего проекта.
Полет 67 Механизм____attribute_____ ( ( ) ) используется компилятором СЗО во мно- гих ситуациях для определения специальных функций, наподобие расширений язы- ка С. Лично мне такая форма записи кажется слишком длинной и трудной для вос- приятия. Вместо нее я рекомендую использовать макросы, определенные в каждом включаемом файле . h для микроконтроллера PIC24. Это значительно повысит чи- табельность исходного кода. Так, в представленном ниже примере для получения функции, аналогичной рассмотренной выше, используется макрос _ISR: void _ISR _TlInterrupt (void) { // Здесь расположен код подпрограммы обслуживания прерывания... } // _InterruptVector В таблицах 5-1а и 5-1Ь технического описания семейства микроконтроллеров PIC24FJ128GA010 показано, какие события могут использоваться для вызова пре- рываний. Так, среди внешних источников для этих микроконтроллеров доступны: • пять — для внешних выводов с обнаружением по изменению уровня; • 22 — для внешних выводов, соединенных с модулем уведомления об изменени- ях (Change Notification); • пять — для модулей захвата на входе (Input Capture); • пять — для модулей сравнения на выходе (Output Compare); • два — для интерфейсов последовательного обмена данными (модули UART); • четыре — для синхронных последовательных интерфейсов (SPI и 12С™); • один — для ведущего параллельного порта (РМР). К внутренним источникам прерывания микроконтроллера PIC24FJ128GA010 относятся: • пять — для 16-разрядных таймеров; • один — для АЦП; • один — для модуля аналоговых компараторов; • один — для часов и календаря реального времени; • один — для средств контроля циклическим избыточным кодом (CRC). Многие из этих источников в свою очередь могут формировать по несколько запросов па прерывание. Например, периферийный последовательный интерфейс (UART) поддерживает три типа прерываний: • когда новые данные были приняты и помещены в буфер приема для обработки; • когда данные были переданы в пустой буфер передачи, готовый к отправке но- вого информационного пакета; • при обнаружении ошибочного состояния, когда для восстановления обмена данными требуется какое-либо действие. Каждому источнику прерывания также соответствует пять управляющих разря- дов в различных регистрах специального назначения (табл. 5.1). • разряд разрешения прерывания (обычно обозначен с помощью суффикса IE): о если он содержит 0, то соответствующее событие не вызывает прерывания; о если он содержит 1, то прерывание разрешено; • флаг прерывания (обычно обозначен с помощью суффикса IF) — устанавлива- ется в 1 каждый раз при возникновении соответствующего события независимо от состояния разряда разрешения;
68 Глава 5. Прерывания ПРИМЕЧАНИЕ Однажды установленный флаг прерывания должен быть сброшен вручную. Другими словами, его необходимо обнулить до выхода из подпрограммы обслуживания пре- рывания, иначе эта же подпрограмма будет сразу же вызвана еще раз. • уровень приоритетности (обычно обозначен с помощью суффикса IP). Преры- ванию можно назначить один из семи уровней приоритетности. Если одновре- менно возникает два различных запроса на прерывание, то первым будет об- служен тот из них, для которого определен более высокий приоритет'. Для ко- дирования приоритетности каждого источника прерываний отведены три раз- ряда. В каждый момент времени уровень приоритетности для задач процессора PIC24 хранится в регистре SR в разрядах IPL0..IPL2. Прерывания, у которых приоритет' ниже текущего значения IPL, игнорируются. При подаче питания всем источникам прерываний по умолчанию назначается уровень приоритетно- сти 4, а процессору — уровень 0. Таблица 5.1. Векторы прерываний семейства микроконтроллеров PIC24FJ128GA010 Источник прерывания Номер вектора Адрес в IVT Адрес в AIVT Размещение разрядов прерывания Флаг Разрешение Приоритет АЦП1 —преобразование завершено 13 00002Eh 00012Eh IFS0<13> IEC0<13> IPC3<6:4> Внешнее прерывание 0 0 000014h 000114h IFS0<0> IEC0<0> IPC0<2:0> Внешнее прерывание 1 20 00003Ch 00013Ch IFS1<4> IEC1<4> IPC5<2:0> Внешнее прерывание 2 29 00004Eh 00014Eh IFS1<13> IEC1<13> IPC7<6:4> Внешнее прерывание 3 53 00007Eh 00017Eh IFS3<5> IEC3<5> IPC13<6:4> Внешнее прерывание 4 54 000080h 000180h IFS3<6> IEC3<6> IPC13<10:8> Генератор CRC 67 00009Ah 00019Ah IFS4<3> IEC4<3> IPC16<14:12> Захват на входе 1 1 000016h 000116h IFS0<1> IEC0<1> IPC0<6:4> Захват на входе 2 5 00001 Eh 00011Eh IFS0<5> IEC0<5> IPC1<6:4> Захват на входе 3 37 00005Eh 00015Eh IFS2<5> IEC2<5> _ IPC9<6:4> Захват на входе 4 38 000060h 000160h IFS2<6> IEC2<6> IPC9<10:8> Захват на входе 5 39 000062h 000162h IFS2<7> IEC2<7> IPC9<14:12> Компаратор 18 000038h 000138h IFS1<2> IEC1<2> IPC4<10:8> Уведомление об изменениях 19 00003Ah 00013Ah IFS1 <3> IEC1<3> IPC4<14:12> Сравнение на выходе 1 2 000018h 000118h IFS0<2> IEC0<2> IPC0<10:8> Сравнение на выходе 2 6 000020h 000120h IFS0<6> IEC0<6> IPC1<10:8> Сравнение на выходе 3 25 000046h 000146h IFS1<9> IEC1<9> IPC6<6:4> Сравнение на выходе 4 26 000048h 000148h IFS1 <10> IEC1 <10> IPC6<10:8> Сравнение на выходе 5 41 000066h 000166h IFS2<9> IEC2<9> IPC10<6:4> Таймер 1 3 OOOOIAh 00011 Ah IFS0<3> IEC0<3> IPC0<14:12> Таймер 2 7 000022h 000122h IFS0<7> IEC0<7> IPC1<14:12> . Таймер 3 8 000024h 000124h IFS0<8> IEC0<8> IPC2<2:0> Таймер 4 27 00004Ah 00014Ah IFS1<11> IECK11> IPC6<14:12> Таймер 5 28 00004Ch 00014Ch IFS1 <12> IEC1 <12> IPC7<2:0> Часы/календарь реального времени 62 000090h 000190h IFS3<14> IEC3<14> IPC15<10:8> 12С 1 — событие ведомого блока 16 000034h 000134h IFS1 <0> IEC1<0> IFC4<2:0> 12С 1 — событие ведущего блока 17 000036h 000136h IFS1 <1 > IEC1<1> IPC4<6:4> 12С 2 — событие ведомого блока 49 000076h 000176h IFS3<1> IEC3<1> IPC12<6:4> 12С 2 — событие ведущего блока 50 000078h 000178h IFS3<2> IEC3<2> IPC12<10:8>
Полет 69 Таблица 5.1. Окончание Источник прерывания Номер вектора Адрес в IVT Адрес в AIVT Размещение разрядов прерывания Флаг Разрешение Приоритет РМР (ведущий параллельный порт) 45 00006Eh 00016Eh IFS2<13> IEC2<13> IPC11<6:4> SPI 1 - - ошибка • 9 000026h 000126h IFS0<9> IEC0<9> IPC2<6:4> SPI 1 — событие 10 000028h 000128h IFS0<10> IEC0<10> IPC2<10:8> SPI 2- - ошибка 32 000054h 000154h IFS2<0> IEC2<0> IPC8<2:0> SPI2 — событие 33 000056h 000156h IFS2<1> IEC2<1> IPC8<6:4> UART 1 — ошибка 65 000096h 000196h IFS4<1> IEC4<1> IPC16<6:4> UART 1 — передатчик 12 00002Ch 00012Ch IFS0<12> IEC0<12> IPC3<2:0> UART 1 - - приемник 11 00002Ah 00012Ah IFSQ<11> IEC0<11> IPC2<14:12> UART 2 - — ошибка 66 000098h 000198h IFS4<2> IEC4<2> IPC16<10:8> UART 2 — передатчик 31 000052h 000152h IFS1<15> IEC1<15> IPC7<10:8> UART 2 - - приемник 30 000050h 000150h IFS1<14> IEC1 <14> IPC7<10:8> При подаче питания все источники прерываний по умолчанию неактивны. На- ряду с назначенным уровнем приоритетности, также существуег относительный (установленный по умолчанию) приоритет среди различных источников прерыва- ний в соответствии с фиксированным порядком их следования в таблице IVT. Вложение прерываний Прерывания можно вкладывать одно в другое, прерывая менее приоритетные подпрограммы обслуживания прерываний более приоритетными. Этот механизм контролируется разрядом NSTDIS регистра INTCON1 микроконтроллера PIC24. Когда разряд NSTDIS содержит 1, при приеме запроса на прерывание процес- сору присваивается максимальный уровень приоритетности (IPL = 7) независимо от уровня приоритетности, определенного для данного события. Это гарантирует, что новые прерывания не будут обслужены до тех пор, пока обслуживается текущее. Другими словами, при установке NSTDIS = 1 уровень приоритетности каждого прерывания используется только для разрешения конфликтов при одновременном поступлении разных запросов, в результате чего все прерывания обслуживаются по- следовательно. Системные прерывания Первые восемь позиции в таблице IVT занимают дополнительные векторы, предназначенные для обнаружения особых ошибочных состояний, наподобие сбоя выбранного тактового генератора процессора, обращения по некорректному адресу, переполнения стека или деления на ноль (математическая ошибка) (табл. 5.2). Таблица 5.2. Векторы системных прерываний Номер вектора Адрес в IVT Источник системного прерывания 0 000004h Зарезервировано 1 000006h Ошибка тактового генератора 2 000008h Ошибочный адрес 3 OOOOOAh Ошибка стека 4 OOOOOCh Математическая ошибка 5 OOOOOEh Зарезервировано 6 000010h Зарезервировано 7 000012h Зарезервировано
70 Глава 5. Прерывания Поскольку такие ошибки обычно приводят к фатальным последствиям для ра- ботающего приложения, им назначены фиксированные уровни приоритетности вы- ше семи базовых уровней, доступных для всех остальных прерываний. Это также означает, что их невозможно случайно маскировать (или задержать с помощью раз- ряда NSTDIS), благодаря чему мы получаем дополнительный уровень надежности. Компилятор MPLAB СЗО сопоставляет все векторы системных прерываний с един- ственной определенной по умолчанию подпрограммой, формирующей сигнал сбро- са процессора. Это правило можно изменить с помощью той же методики, что и для всех подпрограмм обслуживания прерываний общего назначения. Шаблон и пример для прерывания от модуля Timerl Возможно, кому-то все это кажется ужасно запуганным, но скоро вы сами убе- дитесь, насколько быстро реализовать обработку прерываний, если придерживаться представленного ниже простого шаблона. Этот шаблон мы будем постоянно ис- пользовать в будущих практических примерах, демонстрирующих применение в ка- честве источника прерываний периферийного модуля Timerl. Мы начнем с написа- ния подпрограммы обслуживания прерывания: // 1. Подпрограмма обслуживания прерывания от модуля Timerl void _ISR _TlInterrupt(void) { // Введите здесь исходный код // . . . //Не забудьте обнулить перед выходом флаг прерывания _T1IF = 0; } //Tllnterrupt Как и прежде, мы воспользовались макросом _ISR, объявив функцию и пере- даваемые в нее параметры как void. Кроме того, никогда не следует забывать о та- ком важном аспекте, как обнуление перед выходом из функции флага прерывания (_T1IF). В общем случае код приложения должен быть очень лаконичным. Цель любой подпрограммы обслуживания прерывания — эффективно выполнить простую зада- чу, быстро отреагировав на событие. Существует общее правило: при большом пре- дполагаемом объеме исходного текста (особенно, если в нем предусматривается вы- зов функций) рекомендуется внимательно проанализировать цели и структуру при- ложения. Длительные вычисления должны размещаться в функции main () (в част- ности, — в главном цикле), а не внутри подпрограммы обслуживания прерывания, где первостепенную роль играет время. Завершим наш шаблон несколькими строками кода, добавленными в функцию main (): main () { // 2. Инициализация _Т11Р = 4; // Установка приоритетности таймера 1, // (4 - значение по умолчанию) TMR1 =0; // Обнуление таймера PR1 = period-1; // Инициализация регистра периода // 2.1 Конфигурирование источника тактирования модуля Timerl // и настройка синхронизации T1CON = 0x8000; // Проверка настроек регистра T1CON
Полет 71 // 2.2 Инициализация разрядов управления прерываниями от таймера 1 _T1IF =0; // Обнуление флага прерьщания перед _Т11Е =1; // разрешением источника прерывания Т1 // 2.3 Инициализация уровня приоритетности процессора _1Р = 0; // 0 - значение по умолчанию // 3. Главный цикл while(1) ( // Основной код... } // Главный цикл } // main В разделе 2 назначается уровень приоритетности для источника прерывания Timerl, хотя этого можно было явно и не делать, поскольку уровень приоритетно- сти 4 назначается всем источникам прерываний по умолчанию при подаче питания. Кроме того, мц обнулили таймер и инициализировали его регистр периода. В разделе 2Д было завершено конфигурирование модуля таймера путем его ак- тивизации с заданными настройками. В разделе 2.2 сразу же перед разрешением источника прерывания обнуляется флаг прерывания. Событие, вызывающее прерывание, для модуля таймера определено как мо- мент, в который значение таймера достигает значения, хранимого в регистре перио- да. В этот момент устанавливается флаг прерывания, а таймер обнуляется, чтобы начать новый цикл. Если при этом установлен еще и разряд разрешения прерыва- ния, а уровень приоритетности выше, чем текущий приоритет процессора (_1Р), то сразу же вызывается функция обслуживания прерывания. В разделе 2.3, тем не менее, уровень приоритетности процессора инициализи- руется еще раз, хотя этого можно было явно и не делать, поскольку уровень при- оритетности 0 назначается процессору по умолчанию при подаче питания. В разделе 3 расположен код главного цикла. Если все было сделано верно, то главный цикл будет выполняться бесконечно, периодически прерываясь кратковре- менными обращениями к подпрограмме обслуживания прерывания. Реальный пример для модуля Timerl Добавив всего лишь пару строк кода, мы можем превратить этот шаблон в бо- лее практичный пример, в котором модуль Timerl используется для организации часов реального времени, отсчитывающих десятые доли секунд, секунды и минуты. В качестве простого средства индикации мы задействуем восемь младших разрядов порта А для двоичного отображения отсчета секунд. Внесите в программу следующие дополнения: • перед пунктом 1 добавьте объявление нескольких целочисленных переменных, служащих в качестве счетчиков секунд и минут: int dSec = 0; int Sec = 0; int Min = 0; • в пункте 1,2 подпрограмма обслуживания прерывания должна инкрементиро- вать счетчик: dSec++;
72 Глава 5. Прерывания Кроме того, необходимо добавить еще несколько строк кода для обработки пе- реноса в секунды и минуты: • в пункте 2 инициализируйте регистр периода для модуля Timerl значением, ко- торое (при тактовой частоте 32 МГц) даст нам 10 секунд между прерываниями: PR1 = 25000-1; // 25,000 * 64 * 1 цикл (62.5 нс) = 0.1 с • определите младшие восемь разрядов порта А как выходы: TRISA = OxffOO; • для того чтобы упростить получение желаемого периода, в пункте 2.1 установи- те коэффициент 1:64 для предварительного делителя таймера 1: T1CON = 0x8020; • в пункте 3 добавьте код внутри главного цикла, чтобы постоянно обновлять со- держимое младшего байта регистра PORTA текущим значением счетчика мил- лисекунд: PORTA = Sec; Новый проект готов к сборке. • Соответствующий файл interrupts.с находится на прилагаемом к книге компакт-диске в папке Проекты\05 - Прерывания. #include <p24fj128ga010.h> int dSec = 0; int Sec = 0; int Min = 0; // 1. Подпрограмма обслуживания прерывания от таймера 1 void _ISR _TlInterrupt( void) { // 1.1 dSec++; // Инкрементирование счетчика десятых долей секунды if (dSec >9) //10 десятых в секунду { dSec = 0; Sec++; // Инкрементирование счетчика минут if (Sec >59) //60 секунд составляют минуту { Sec = 0; // 1.2. Инкрементирование счетчика минут Min++; ' if (Min > 59) // В часе - 59 минут Min = 0; } // Минуты } // Секунды // 1.3. Обнуление флага прерывания _T1IF = 0; } //Tllnterrupt main () { // 2. Инициализация таймера 1, T1ON, коэффициент предделения 1:1, // внутренни источник тактирования
Полет 73 -ТИР = 4; // Это в любом случае - значение по умолчанию TMR1 = 0; И Обнуляем таймер PR1 = 25000-1; // Инициализируем регистр периода TRISA = OxffOO; // Определяем младшие 8 разрядов порта А как выходы // 2.1. Конфигурирование модуля Timerl T1CON = 0x8020; // Разрешеш, коэффициент предделителя 1:64, // внутренне тактирование // 2.2. Инициализация прерываний от таймера 1, обнуление флага, // разрешение источника _T1IF = 0; -THE = 1; // 2.3. Инициализация уровня приоритетности процессора _1Р = 0; // Это в любом случае - значение по умолчанию // 3. Главный цикл while (1) { // Основной код PORTA = Sec; } // Главный икл } // main Тестирование прерывания от модуля Timerl Выполните следующую последовательность действий. 1. Откройте окно Watch. 2. Добавьте в него следующие переменные: • dSec — выбирается из раскрывающегося списка Symbol с последующим нажа- тием кнопки Add Symbol; • TMR1 — выбирается из раскрывающегося списка SFR с последующим нажати- ем кнопки Add SFR; • SR — выбирается из раскрывающегося списка SFR с последующим нажатием кнопки Add SFR. 3. Откройте окно Stopwatch имитатора (команда меню Debugger ► Stopwatch). 4. Установите точку прерывания на первой команде подпрограммы обслуживания прерывания после комментария //1.1. Для этого разместите курсор в этой строке, щелкните правой кнопкой мыши и выберите в контекстном меню ко- манду Set Breakpoint (или же просто дважды щелкните мышью). Теперь мы сможем отслеживать моменты возникновения прерываний. 5. Запустите программу на выполнение, выбрав команду меню Debugger ► Run или нажав клавишу <F9>. Имитация быстро остановится, а указатель текущей ко- манды (зеленая стрелка) окажется на точке прерывания внутри подпрограммы обслуживания прерывания. Итак, мы остановились внутри подпрограммы обслуживания прерывания. Это означает, что возникло соответствующее событие, т.е. таймер 1 достиг числа 24 999 (не забывайте, что таймер 1 начинает счет с 0, а значит было выполнено 25 000 от- счетов). Умножив это значение на коэффициент делителя получаем 25,000 х 64 = 1,6 миллиона циклов.
74 Глава 5. Прорывания Показания в окне Stopwatch указывают на то, что общее число выполненных циклов в действительности составляет чуть больше 1,6 миллиона. Дело в том, что в это значение также включено время, требуемое для инициализационной части программы. При быстродействии микроконтроллера PIC24 (16 миллионов команд в секунду или 62,5 нс на цикл) мы получаем десятую долю секунды! В окне Watch можно отследить текущее значение уровня приоритетности про- цессора (IP). Поскольку мы находимся внутри подпрограммы обслуживания пре- рывания, сконфигурированного на уровень 4, именно это число должно содержать- ся в разрядах 3, 4 и 5 регистра состояния SR. Для удобства среда MPLAB отобража- ет полностью декодированное содержимое регистра состояния у нижнего края глав- ного окна. На рис. 5.1 обведено значение IP в строке состояния (IP4 соответствуег четвер- тому уровню приоритетности), а также — содержимое регистра SR и фактическое значение времени в окне Stopwatch (в миллисекундах). Рис. 5.1. Состояние процессора после прерывания от таймера 1 Путем пошагового выполнения программы, начиная от текущей позиции (с по- мощью команды Step Over или Step In), мы можем отследить результат работы сле- дующих нескольких команд внутри подпрограммы обслуживания прерывания. По ее завершении уровень приоритетности вернется к исходному значению (обратите внимание на обозначение 1Р0 в строке состояния и обнуление разрядов 5, 6 и 7 в ре- гистре SR). 6. Опять выберите команду Run. Зеленая стрелка счетчика команд окажется впуг- ри подпрограммы обслуживания прерывания. На этот раз обратите внимание, что к предыдущему результату счета было добавлено ровно 1,6 миллиона цик- лов.
Полет 75 7. Добавьте в окно Watch переменные Sec и Min. 8. Выберите команду Run еще несколько раз, чтобы удостовериться в том, что по- сле 10 итераций инкрементируется счетчик секунд. Для тестирования инкре- мента минут лучше убрать текущую точку прерывания и установить новую не- сколькими строками ниже, иначе команду Run придется выбирать 600 раз! 9. Установите новую точку прерывания на операторе Min++ в разделе 1.2. 10. Опять выберите команду Run, и удостоверьтесь в том, что счетчик секунд уже обнулен. 11. Выберите команду Step Over, и счетчик минут будет' инкрементирован. Подпрограмма обслуживания прерывания выполняется всего 600 раз через точ- ные интервалы времени в одну десятую долю секунды. Тем временем код внутри главного цикла выполняется бесконечно. При этом в его распоряжении несметных 960 миллионов циклов. Честно говоря, паша демонстрационная программа исполь- зует этот огромный потенциал довольно бестолково: тратит его на постоянное об- новление содержимое регистра PORTA. В реальном же приложении параллельно ра- боте точных часов реального времени можно решать множество разнообразных за- дач. Вспомогательный тактовый генератор Для получения часов реального времени мы могли бы использовать еще одну особенность модуля Timerl микроконтроллера PIC24 (присуща также и предыду- щим поколениям восьмиразрядных микроконтроллеров PIC). Вместо основного вы- сокочастотного генератора для тактирования таймера 1 можно задействовать до- полнительный низкочастотный генератор. Поскольку он рассчитан на малую часто- ту (обычно его используют совместно с недорогим кристаллом па 32 768 Гц), то для работы ему требуется совсем немного энергии. Кроме того, поскольку он не зависит от схемы основного тактового генератора, то может продолжать работать при пере- ходе процессора в один из режимов пониженного энергопотребления. По сути, до- полнительный тактовый генератор — ключевой элемент для многих таких режимов. В некоторых случаях его используют для замены основного генератора, в то время как в других он остается активным только для тактирования таймера 1 или выбран- ной группы периферийных устройств. Для того чтобы адаптировать рассмотренный выше пример под использование дополнительного тактового генератора, в исходный текст программы требуется вне- сти совсем незначительные модификации: • изменить подпрограмму обслуживания прерывания таким образом, чтобы она отсчитывала только секунды и минуты (более низкая частота не требует допол- нительного этапа для десятых долей секунды): // 1. Подпрограмма обслуживания прерывания от таймера 1 void —ISR _TlInterrupt( void) { // 1.1. Обнуление флага прерывания _T1IF = 0; и 1.2. Sec++; // Инкрементируем счетчик секунд if (Sec > 59) // В минуте - 60 секунд { Sec = 0; Min++; // Инкрементируем счетчик минут
76 Глава 5. Прерывания if (Min > 59) // В часе 60 минут Min = 0; } /7 Минуты } //Tllnterrupt • в разделе 2 изменить содержимое регистра периода таким образом, чтобы одно прерывания возникало раз в 32 768 циклов: PR1 = 32768-1; // Инициализация регистра периода • в разделе 2.1 изменить слово конфигурации таймера 1 (предварительный дели- тель больше не требуется): T1CON = 0x8002; // Разрешен, делитель 1:1, исползуется // дополнительный тактовый генератор ©Соответствующий файл interrupts 32k. с находится на прилагаемом к книге компакт-диске в папке Проекты\05 - Прерывания. К сожалению, с помощью имитатора нам не удастся сразу же проверить эту но- вую конфигурацию, поскольку вход дополнительного тактового генератора автома- тически не имитируется. В последующих уроках будет показано, каким образом с помощью нового на- бора инструментов получить файл стимулов, который можно использовать для при- емлемой эмуляции кристалла на 32 кГц, подключенного к выводам TICK и SOSCI микроконтроллера PIC24. Календарь реального времени Разрабатывая предыдущие два примера, мы могли бы воспользоваться часами реального времени для реализации полнофункционального календаря, отсчиты- вающего дни, дни недели, месяцы и годы. Эти несколько строк кода выполнялись бы только раз в день, раз в месяц или раз в год, и потому никак бы не снизили об- щую производительность приложения. Хотя разработка кода для отсчета лет и представляет собой интерес с познава- тельной точки зрения, микроконтроллер PIC24FJ128GA010 уже оснащен встроен- ным модулем полнофункционального календаря реального времени (RTCC). Он не только тактируется от того же низкочастотного дополнительного генератора, но и реализует различные полезные функции, наподобие прерывания в заданный мо- мент времени. Другими словами, после инициализации модуля RTCC его можно на- строить таким образом, чтобы формировать запрос на прерывание в конкретный ме- сяц, день, час, минуту и секунду один раз в году (или даже один раз в четыре года, если в качестве даты выбрать 29-е февраля). Соответствующая подпрограмма обслуживания прерывания выглядит следую- щим образом: // 1. Подпрограмма обслуживания прерывания от модуля RTCC void _ISR _RTCCInterrupt( void) { // 1.1. Обнуление флага прерывания JRTCIF = 0; // 1.2. Код здесь выполняется только раз в год, // т.е. один раз за каждые 365 х 24 х 60 х 60 х 16 000 000 = // = 504 576 000 000 000 циклов процессора } // RTCCInterrupt
Разбор полета 77 Управление несколькими прерываниями Встроенным приложениям обычно необходимо обслуживать несколько источ- ников прерываний. Например, последовательный порт может периодически требо- вать к себе внимания в тот момент, когда активен модуль ШИМ, выполняющий об- новление сигнала на аналоговом выходе. Или же для формирования импульсных выходных последовательностей могут одновременно использоваться несколько таймеров, в то время как входы опрашиваются аналого-цифровым преобразовате- лем с буферизацией считанных значений. Количество вариантов, которые можно получить с помощью 118 источников прерываний, практически неограниченно, что дает практически неограниченное число возможных ошибок. Стоит не проявить не- много бдительности и здравого смысла, как сложные механизмы системы прерыва- ний сразу же начинают приводить к проблемам. Необходимо всегда придерживаться следующих правил: • старайтесь сделать подпрограммы обслуживания прерываний как можно более короткими и эффективными; при этом ни в коем случае не пытайтесь в них об- рабатывать поступающие данные, а ограничьте их содержимое лишь буфериза- цией, передачей и установкой флагов; • при одновременном возникновении двух событий для определения того из них, . которое должно быть обслужено первым, используйте уровни приоритетности; • всегда взвешивайте все “за” и “против” прежде, чем воспользоваться вложен- ными прерываниями, поскольку это усложняет программу и может приводить к ошибкам. Впрочем, если подпрограммы обслуживания прерываний — корот- кие и эффективные, то дополнительна задержка, вносимая вследствие ожидания завершения обработки текущего прерывания до обслуживания следующего, крайне незначительна. Если вы решите, что можно обойтись и без вложения прерываний, то удостоверьтесь, что управляющий разряд NSTDIS всегда уста- новлен в 1: -NSTDIS =1; // Запрет вложения прерываний (выбор по умолчанию) Разбор полета В этом уроке мы увидели, насколько простой задачей становится кодирование подпрограмм обслуживания прерываний, благодаря языковым расширениям, встро- енным в компилятор СЗО, и мощным механизмам управления прерываниями, пред- лагаемым архитектурой PIC24. Прерывания могут быть чрезвычайно эффективным инструментов в руках разработчика встроенных систем, поскольку позволяют реа- лизовать одновременно несколько задач без потерь драгоценного времени и ресур- сов процессора. Но вместе с тем они могут стать и источником множества проблем. В рамках одного урока невозможно рассмотреть все аспекты этого вопроса, поэтому за дополнительной информацией обращайтесь к справочному руководству по PIC24 и руководству пользователя MPLAB СЗО. Наконец, мы познакомились поближе с использованием модуля Timerl и дополнительного тактового генератора, а также кратко затронули особенности нового модуля календаря реального времени RTCC. Заметки для экспертов по С Таблица векторов прерываний (IVT) — это ключевая часть кодового сегмента СО микроконтроллера PIC24. Фактически, в первых 256 ячейках памяти программ
78 Глава 5. Прерывания должно присутствовать две копии этой таблицы: первая используется в ходе выпол- нения программы, а вторая (альтернативная таблица IVT) — в ходе отладки. Имен- но эти две таблицы составляли “львиную долю” кода СО во всех примерах, которые мы рассмотрении в первых пяти уроках. Таким образом, для получения “чистого” объема кода необходимо вычесть 256 слов (768 байт) из размера каждой выполни- мой программы. Заметки для экспертов по ассемблеру Для объявления функции как подпрограммы обслуживания прерывания можно воспользоваться макросом _ISRFAST, а для того чтобы в дальнейшем опа могла обращаться к развитым средствам архитектуры PIC24, следует задействовать набор из четырех теневых регистров. Теневые регистры позволяют процессору автомаги- чески сохранять содержимое первых четырех рабочих регистров (W0-W3, т.е. наи- более часто используемых) и основную часть содержимого регистра SR в специаль- ных зарезервированных ячейках памяти без необходимости использования стека. Тем самым они обеспечивают минимальное время отклика на запрос на прерыва- ние. Само собой, поскольку существует только один набор таких регистров, их ис- пользование ограничено приложениями, в которых в каждый момент времени об- служивается только одно прерывание. Это не означает, что мы должны использо- вать на все приложение единственное прерывание, — просто придется применять макрос _ISRFAST только в приложениях, в которых всем прерываниям назначен одинаковый уровень приоритетности. Или же, при наличии нескольких уровней, — зарезервировать _ISRFAST для подпрограмм обслуживания прерываний с наивыс- шим приоритетом. Заметки для экспертов по PIC Обратите внимание, что в архитектуре PIC24 отсутствует отдельный управ- ляющий разряд, запрещающий все прерывания, зато присутствует специальная ко- манда DISI, запрещающая прерывания на ограниченное число циклов. Если для некоторого фрагмента кода необходимо временно отключить все прерывания, то можно воспользоваться следующей командой встроенного ассемблера: __asm__ volatile("disi #0x3FFF"); // Временно запрещает все прерывания // Разместите здесь программный код // . . . DISICNT =0; // Разрешение всех прерываний Советы и хитрости Согласно техническому описанию микроконтроллеров PIC24 для активизации маломощного дополнительного тактового генератора необходимо установить в 1 разряд SOSCEN в регистре OSCCON. Но не торопитесь сразу же вносить коррективы в последний пример и пытаться выполнить его па реальной плате. Обратите внима- ние, что регистр OSCCON, содержащий важные управляющие разряды микрокон- троллера и влияющий на выбор основного активного генератора и его частоту, за- щищен механизмом блокировки. По этой причине необходимо предварительно вы- полнить специальную последовательность действий для снятия блокировки, иначе команда будет проигнорирована. Ниже представлен соответствующий пример, реа- лизованный с помощью встроенного ассемблера:
Советы и хитрости 79 // Последовательность для снятия блокировки с регистра OSCCON и // установки б 1 разряда SOSCEN asm volatile ("mov #OSCCON, Wl") ; asm volatile ("mov.b #0x46, W2"); asm volatile ("mov.b #0x57, W3"); asm volatile ("mov.b #0x02, WO"); // SOSCEN =1 asm volatile ("mov.b W2, [Wl]"); asm volatile ("mov.b W3, [Wl]"); asm volatile ("mov.b WO, [Wl]"); Аналогичный механизм блокировки применяется и для защиты главного реги- стра управления календарем реального времени RCFGCAL. Для разрешения записи в этот регистр должен быть установлен в 1 разряд RTCWREN, но для него требуется собственная последовательность снятия блокировки. Соответствующий пример по- казан ниже: > // Последовательность для снятия блокировки с регистра RCFGCAL и // установки в 1 разряда RTCWREN asm volatile("disi #5"); asm volatile("mov #0x55, w7"); asm volatile("mov w7,_NVMKEY"); asm volatile("mov #0xAA, w8"); asm volatile("mov w8,_NVMKEY"); asm volatile("bset _RCFGCAL, #13"); // RTCWREN =1; asm volatile("nop"); asm volatile("nop"); После этих двух операций, инициализирующих календарь реального времени, установка даты и времени не вызывает трудностей: _RTCEN =0; // Отключение модуля // Пример установки даты 01.12.2006, среда 12:01:30 _RTCPTR =3; // Начало последовательности загрузки // Год // Месяц-1/день-1 // День недели/часы // Минуты/секунды RTCVAL = 0x2006; RTCVAL = 0x1100; RTCVAL = 0x0312; RTCVAL = 0x0130; // Необязательная калибровка // CAL = 0x00; // Включение и блокировка _RTCEN =1; // Включение модуля -RTCWREN =0; // Блокировка настроек Настройка сигнализации не требует каких-либо специальных комбинаций сня- тия блокировки. Ниже представлен пример, который поможет вам запомнить мой № рождения: // Отключение сигнализации _ALRMEN = 0; // Установка сигнала на конкретный день в году (мой день рождения) _ALRMPTR =2; // Начало последовательности ALRMVAL = 0x1124; // Месяц-1/день-1 ALRMVAL = 0x0006; // День недели/час ALRMVAL = 0x0000; // Минуты/секунды
80 Глава 5. Прерывания // Установка счетчика повторений _ARPT =0; // Один раз —CHIME =1; // Неопределенно // Установка маски _AMASK = Obi001; сигнализации // Один раз в год _ALRMEN = 1; -RTCIF = 0; _RTCIE = 1; // Включение сигнализации // Обнуление флага прерывания // Разрешение прерывания Упражнения Напишите подпрограммы на основе прерываний для следующих приложений: • эмуляция обмена данными по последовательному порту; • радиоприемник с дистанционным управлением; • видеовывод в формате NTSC (реализация этой задачи рассматривается в после- дующих главах книги). Ссылки • http://www.aopa.org — Web-сайт Ассоциации владельцев самолетов и пилотов (Aircraft Owners and Pilots Association).
ГЛАВА 6 Заглянем под капот В этой главе: > Распределение пространства памяти ii~ Окно Program Space Visibility ► Исследование распределения памяти ► Файлы .тар ► Указатели ► Куча ► Модели памяти MPLAB" СЗО - " в f ВVЖ?В* Ш'К' г; ' ., - х' ••' Независимо от того, йа кого вы учитесь: водителя или пилота — рано или позд- но вам придется заглянуть под капот (или, выражаясь авиационными терминами, — обтекатель). От вас не требуется знания всех тонкостей работы механизмов автомо- биля или самолета, поскольку заниматься их ремонтом — задача профессиональных механиков, однако базовое понимание процессов помогает в управлении машиной. Кроме того, при возникновении небольших неполадок вы сможете выявить их и да- же, возможно, — устранить самостоятельно. Отчасти это применимо и к работе с компиляторами. Рано или поздно вам при- ходится «заглядывать под капот», чтобы получить оптимальную эффективность ис- полняемого кода. В «отсек двигателя» мы заглянули еще в самом первом уроке, од- нако теперь настало время немного поковыряться в деталях. План полета В этом уроке мы рассмотрим основы объявления строк, чтобы на их примере познакомиться с методиками распределения памяти, применяемыми в компиляторе MPLAB СЗО. Архитектура RISC микроконтроллеров PIC24 бросает интересные вы- зовы и предлагает новаторские решения. Для того чтобы исследовать, каким обра- зом компилятор MPLAB СЗО и компоновщик сообща выдают наиболее компактный и эффективный код, мы воспользуемся несколькими средствами, включая окна Disassembly Listing и Program Memory, а также файл . тар. Предполетный контроль В этом уроке мы будем работать исключительно с программными средствами, включая интегрированную среду MPLAB, компилятор MPLAB СЗО и имитатор MPLAB SIM.
82 Глава 6. Заглянем под капот С помощью контрольного списка “Настройка нового проекта” создайте проект под названием “Strings” (“Строки”) и новый файл исходного кода strings . с. Полет Строки в языке С — это обычные массивы символов ASCII. Все символы, вхо- дящие в состав строки, хранятся в памяти последовательно в виде восьмиразрядных элементов. После завершающего символа строки размещен дополнительный байт, содержащий флаг окончания: нулевое значение, которой в символьной записи пред- ставляют как 1 \0 ’. Впрочем, учтите, что все это — лишь соглашение, применимое при работе со стандартной библиотекой обработки С-строк string.h. Никто не мешает создать новую библиотеку и хранить строки в массивах, где первый элемент отводится под хранение длины строки (методика, знакомая программистам на языке Pascal). Кроме того, при разработке “международных” приложений, использующих большие набо- ры символов (например, китайский, японский или корейский), потребуется коди- ровка Unicode, в которой, в отличие от обычной таблицы ASCII, на каждый символ может приходиться по несколько байт. Базовую поддержку преобразования строк из формата ASCII в Unicode и наоборот по стандарту ANSI90 реализует библиотека MPLAB СЗО stdlib. h. А теперь рассмотрим объявление переменной, содержащей один символ: char с; Таким же образом в одном из предыдущих уроков мы объявляли восьмиразряд- ные целые числа, которым по умолчанию соответствует диапазон значений - 128..+127. Такую переменную можно инициализировать непосредственно при объявлении с помощью числового значения: char с = 0x41; Еще один вариант инициализации — с помощью ASCII-значения: char с = 'а’; Обратите внимание на использование одинарных кавычек для обозначения от- дельных символов ASCII. В представленных выше примерах результат — один и тот же, поскольку компилятор С распознает отдельные символы как числа. Теперь объявим и инициализируем строку как массив восьмиразрядных целых чисел (символов): char s[5] = {*Н’, 'Е', 'L', ’L', ’О'}; В этом примере мы инициализировали массив с помощью стандартной формы записи, применяемой для числовых массивов, однако для этого можно было бы вос- пользоваться и гораздо более удобной формой, специально предназначенной для инициализации строк: char s[5] = "HELLO"; Существует еще более упрощенная форма инициализации, избавляющая от не- обходимости подсчитывать число символов в строке (и, как следствие, уменьшаю- щая вероятность возникновения ошибок): char s[] = "HELLO";
Полет 83 Компилятор MPLAB СЗО автоматически определит количество символов, необ- ходимое для хранения такой строки, добавив к ней нулевой символ окончания. Дру- гими словами, представленная выше форма инициализации эквивалентна следую- щей инициализации: char s[6] = { 'Н', 'Е', 'L', 'L', 'О’, '\0’ }; Присвоение значений и выполнение арифметических операций для символов, как восьмиразрядных целых, ничем не отличается от правил, применяемых для це- лых чисел: char с; // Объявление с как восьмиразрядного целого со знаком с = 'а'; // Присвоение значения, соответствующего символу 'а' по // таблице ASCII C+ + ; // Инкрементирование с, после чего оно будет содержать // значение, соответствующее символу 'Ь' по таблице ASCII Подобные операции можно выполнить с любым элементом символьного масси- ва (строки), однако, в отличие от представленного выше примера, короткой формы записи для присвоения значений строкам не существует: char s[15] ; // Объявление s как строки из 15 символов s = "Hello!"; // Ошибка! Это не действует! Подключив к исходному коду файл st ring, h, мы получаем доступ к различ- ным полезным функциям, позволяющим: • копировать содержимое строк: strcpy(s, "Привет"); // s = "Привет" • объединять две строки: strcat(s, ", мир!"); // s = "Привет, мир!" • определять длину строки: i - strlen(s); // i = 12 и многое другое. Распределение пространства памяти Как и при инициализации числовых переменных, каждый раз при объявлении и инициализации строки в виде char s[] = "Flying with the PIC24"; происходит три вещи: • компоновщик MPLAB СЗО резервирует для хранения переменной последова- тельность ячеек памяти в ОЗУ (пространство данных) (для представленного выше примера — 22 байта); эта область входит в состав “ближнего” раздела данных ndata; • компоновщик MPLAB СЗО сохраняет значение инициализации в таблице дли- ной 22 байта (в памяти программ); эта область входит в состав кодового раздела init; • компилятор MPLAB СЗО создает небольшую подпрограмму, которая будет вы- зываться перед функцией main (часть упомянутого в предыдущих главах кода
84 Глава 6. Заглянем под капот СО) для копирования значений из памяти программ в память данных, тем самым инициализируя переменную. Другими словами, строка “Flying with the PIC24” занимает в два раза больше места, чем предполагалось, поскольку, кроме того, что под нее отведено место в ОЗУ, ее копия хранится еще и в флэш-памяти программ. К тому же следует учи- тывать код инициализации и время, затрачиваемое на фактическое копирование. Если строка не используется непосредственно в программе, а предназначена для пе- редачи через последовательный порт или для вывода на дисплей, то мы можем сэ- кономить драгоценные ресурсы микроконтроллера, объявив строку как константу. В результате для нее не резервируется область ОЗУ и отсутствует код инициализа- ции. const char s[] = "Flying with the PIC24"; Теперь компоновщик MPLAB СЗО будет выделять место под строку только в памяти программ в кодовом разделе const. Доступ к такой строке можно полу- чить через окно Program Space Visibility (Видимость области программ) — новшест- во архитектуры PIC24, о которой мы поговорим чуть позже. Теперь строка воспринимается компилятором как непосредственный указатель на память программ, вследствие чего отпадает необходимость в использовании об- ласти ОЗУ. В предыдущих примерах этого урока мы уже встречали еще один вариант неяв- ного объявления строки как константы: strcpy( sf "Привет"); Строка “Привет” в данном случае неявно объявляется как const char, и раз- мещается в разделе const памяти программ, для доступа к которой используют ок- но Program Space Visibility. ПРИМЕЧАНИЕ______________________________________________ ______________ Если одна,и та же строковая константа используется в программе несколько раз, то компилятор MPLAB СЗО с целью оптимизации использования памяти автоматически сохранит только одну ее копию в разделе const, даже если все средства оптимиза- ции компилятора были отключены. Окно Program Space Visibility Архитектура PIC24 несколько отличается от большинства других 16-разрядпых микроконтроллерных архитектур, с которыми, возможно, знаком читатель. В отли- чие от более распространенной модели фон Неймана, она рассчитана на максималь- ную производительность, достигаемую по гарвардской модели. Различие заключа- ется в использовании двух полностью независимых шин: одна — для доступа к па- мяти программ (Flash), а другая — для доступа к памяти данных (ОЗУ). В результа- те достигается удвоение пропускной способности, поскольку в тот момент, когда шипа данных занята в ходе выполнения команды, шина памяти программ свободна для извлечения следующего кода команды и активизации процесса его дешифрова- ния. В то же время, в традиционной фон-неймановской архитектуре эти две опера- ции чередуются, что, естественно, сказывается на общей производительности. Не- достаток такой архитектуры заключается в необходимости уделять особое внимание доступу к константам и данным, хранимым в памяти программ.
Полет 85 Архитектура PIC24 предоставляет два метода чтения данных из памяти про- грамм: • с помощью специальной команды табличного чтения third; • с помощью окна Program Space Visibility (PSV), которое можег занимать до 32 Кбайт памяти программ, доступной для шины памяти данных. Другими сло- вами, PSV — это своеобразный мост между шинами памяти программ и данных (рис. 6.1). Пространство памяти программ (Flash) Пространство памяти данных (ОЗУ) 16 разрядов 24 разряда 0x0000 0x0800 0x2800 0X7FFF OxFFFF Рис. 6.1. Окно Program Space Visibility микроконтроллера PIC24Fj128GA010 Обратите внимание на тот факт, что микроконтроллеры PIC24 используют 24- разрядную шину памяти программ при 16-разрядной шине данных. Ввиду этого не- соответствия разрядности двух шин использование “моста” PSV становится еще бо- лее интересным. В действительности PSV связывает с шиной памяти данных только младшие 16 разрядов шины памяти программ. Старшая часть (восемь разрядов) ка- ждого слова памяти программ для окна PSV недоступна. В то же время, при исполь- зовании команд табличного чтения память программ доступна полностью, но — це- ной необходимости проводить различие между работой с данными в ОЗУ (прямая адресация) и с данными в памяти программ (через специальные команды таблично- го доступа). Таким образом, программисту микроконтроллеров PIC24 предоставляется вы- бор: более удобный, однако менее эффективный с точки зрения использования па- мяти метод пересылки данных между двумя шинами с помощью окна PSV, или же более эффективное и менее “прозрачное” решение в виде команд табличного досту- па. Взвесив все “за” и “против”, разработчики компилятора MPLAB СЗО решили задействовать оба механизма, хотя и для решения с помощью каждого из них раз- личных задач: • окно PSV используют для работы с константными массивами (числовыми и строковыми), чтобы для констант и переменных можно было использовать один и тот же тип указателя на шину памяти данных; • для достижения максимальной компактности и эффективности механизм таб- личного доступа предназначен для выполнения инициализации переменных (ограничен сегментом СО).
86 Глава 6. Заглянем под капот Исследование распределения памяти Мы начнем исследование рассмотренных вопросов с помощью имитатора MPLAB SIM и следующего фрагмента кода: 7*............................................... ............. ** Строки */ #include <p24fj128ga010.h> #include <string.h> // 1. Объявление переменных const char a[] = "Learn to fly with the PIC24"; char b[100] = "Initialized"; // 2. Главная программа main () { strcpy(b, "MPLAB СЗО"); // Присвоение нового содержимого строке b } //main Выполните следующие действия. 1. Выполните сборку проекта, ориентируясь по соответствующему контрольному списку в конце книги. 2. Откройте в среде MPLAB окно Watch. 3. С помощью кнопки Add Symbol добавьте в список окна Watch переменные а и Ь (рис. 6.2). Рис. 6.2. Добавление массивов в окно Watch Маленький символ “+” слева от имен а и Ь означает, что эти переменные — массивы, и позволяет развертывать их для просмотра отдельных элементов прямо в окне Watch (рис. 6.3). По умолчанию MPLAB отображает каждый элемент массива в виде символа ASCII, однако эту настройку можно изменить в соответствии со соб- ственными предпочтениями: 1. Выделить элемент массива щелчком левой кнопкой мыши. 2. Щелкнуть правой кнопкой мыши, чтобы отобразить контекстное меню окна Watch. 3. Выбрать команду Properties (Свойства) (последняя в меню). В результате на эк- ране появится диалоговое окно со свойствами окна Watch (рис. 6.4).
Полот 87 Рис. 6.3. Развертывание массива в окне Watch В этом диалоговом окне можно из- менить формат, используемый для ото- бражения содержимого выбранного элемента массива. Обратите также вни- мание на недоступное для редактирова- ния поле Memory (Память). Его содер- жимое позволяет узнать, где размещена переменная: в пространстве данных или кода. Если команду Properties выбрать для константной строки а, то в качестве пространства памяти будет указано “Program”. Это означает, что строка ис- пользует минимальный требуемый объ- ем флэш-памяти программ микрокон- троллера PIC24, и доступ к ней можно получить через окно PSV, а значит отпа- дает необходимость в резервировании области ОЗУ. В то же время, для строки Ь в диа- логовом окне свойств указано, что она размещена в регистровом файле (“File Register”), т.е. в ОЗУ. Продолжая паше исследование, обратите внимание на то, что строка а выглядит уже инициализированной, поскольку окно Watch отображает ее готовой к использо- ванию сразу же после сборки проекта. В отличие от нее, строка Ь — пустая и не- инициализированная. Она примет свое значение только после того, как мы устано- вим указатель в первой строке кода внутри функции main и выберем команду Run То Cursor (рис. 6.5). Как видим, строка b размещена в пространстве ОЗУ, и для инициализации пе- ременной требуется предварительно выполнить кодовый сегмент СО. Рис. 6.4. Диалоговое окно со свойствами окна Watch ПРИМЕЧАНИЕ Строки в окне Watch выравниваются в столбце Value по правому краю, поэтому для просмотра длинных строк необходимо увеличить размеры окна Watch.
88 Глава 6. Заглянем под капот Рис. 6.5. Массив инициализирован И опять-таки, воспользуемся окном Disassembly Listing для просмотра кода, соз- данного компилятором:__________ ; Г . \ ГТ Л\ С Ci- Ci- Х-* • \ Л V \ \ V/ 1: /* ' 2: ** Строки 3: 4: */ 5: #include <p24fjl28ga010.h> 6: #include <string.h> 4 7: 8: // 1. Объявление переменных 9: 10: const char' a[] = "Learn to fly with the PIC24"; 11: char b[100] = "Initialized"; 12: 13: // 2. Главная программа 14: main () 15: { 0028А FA0000 Ink #0x0 16: strcpy( b, "MPLAB C30"); 0028С 282В21 mov.w #0x82b2,0x0002 0028Е 208000 mov.w #0x800,0x0000 00290 07FFF7 rcall 0x000280 17: 18: } // main 00292 FA8000 ulnk 00294 060000 return --- с:\pic30-build\build_20060131\src\standardc\sxl\strcpy.с 00280 780100 mov.w 0x0000,0x0004 00282 784931 mov.b [0x0002++] , [0x0004] 00284 E00432 cpO.b [0x0004++] 00286 3AFFFD bra nz, 0x000282 00288 060000 return Полный результат дизассемблирования функций main () и strcpyO распо- ложен в конце листинга. Обратите внимание на то, какой компактный код дает под- программа strcpy (): всего лишь пять команд. Кроме того, следует отметить, что была подключена только одна подпрограмма, хотя библиотека string. h содержит
Полет 89 десятки функций. Несмотря на то, что все они объявлены в файле string .h, муд- рый компоновщик добавил только ту функцию, которая действительно необходима. Впрочем, чего нельзя увидеть в окне Disassembly Listing, так это кода инициали- зации СО. Как уже было упомянуто в одной из предыдущих глав, для того чтобы его увидеть, необходимо воспользоваться окном Program Memory (рекомендую выбрать в нем вкладку Symbolic). Самые любопытные и терпеливые читатели обнаружат, что строка b инициализируется с помощью команд табличного чтения (third), извле- кающих данные из памяти программ (Flash) и сохраняющих их в выделенной облас- ти в пространстве данных (ОЗУ). Файлы .тар |А8 Souice Files All Souice FJes (x.c.x.h х.а$т;х.зз;х.юс x.s;’.bas/.$< Assembly Souice Files (‘asm;* *, as;*, inc;*, s) C Souice Files (*cf.h) Basic Souice Files (‘.bas;x inc) SCL Souice Fies (*.scl) Linkei Files (’,.hk;x.lki;x gid) Еще одно средство, позволяющее нам лучше разобраться с инициализацией строк (и любых других массивов) и размещением их в памяти, — это файлы . тар. Эти текстовые файлы, создаваемые компоновщиком MPLAB СЗО, можно легко про- смотреть с помощью редактора MPLAB. Они предназначены специально для реше- ния вопросов, связанных с распределением памяти. Файл . тар хранится в том же каталоге, что и все остальные проектные файлы. Выберите команду ме- ню File ► Open и найдите папку с проектом. По умол- чанию редактор MPLAB перечисляет все файлы . с. Тип .тар можно выбрать в раскрывающемся списке, расположенном у нижнего края диалогового окна по- иска файлов (рис. 6.6). Файлы . тар обычно довольно длинные и многословные, однако, научившись распознавать только некоторые наиболее важные его фрагменты, вы сможете нахо- дить множество полезных данных. Например, в самом начале файла .тар находится раздел “Program Memory Usage” (“Использование памяти программ”): Puc. 6.6. Выбор типа файлов . map Program Memory Usage section address length (PC units) length (bytes) (dec) . reset 0 0x4 0x6 (6) . ivt 0x4 Oxfc 0x17a (378) . aivt 0x104 Oxfc 0x17a (378) . text 0x200 0x96 Oxel (225) .const 0x2 96 0x26 0x39 (57) .dinit 0x2 be 0x4c 0x72 (114) . isr 0x308 0x2 0x3 (3) Total program memory used (bytes): 0x489 (1161) <1% Это список небольших разделов кода, собранных компоновщиком MPLAB СЗО в особом порядке (заданном сценарием . gid) и размещении. Названия большинст- ва разделов интуитивно понятны, в то время как другие обоснованы исторически: • . reset — размещение вектора сброса; • . ivt — таблица векторов прерываний, рассмотренная в предыдущей главе; • .aivt — альтернативная таблица векторов прерываний; • .text — раздел, в котором размещен весь код, созданный компилятором MPLAB СЗО из исходного текста программы (название . text перешло по на- следству от самой первой реализации компилятора С);
90 Глава 6. Заглянем под капот • . const — раздел, содержащий константы (целочисленные и строковые), дос- туп к которым реализован через окно PSV; • .dinit — раздел для размещения данных инициализации переменных (ис- пользуется кодом СО); • . isr — адрес размещения подпрограммы обслуживания прерывания (в данном случае — выбранной по умолчанию). Именно в разделе . const хранится константная строка а, а также — явно за- данная в программе строка “MPLAB СЗО”. Обе они предназначены для доступа че- рез окно PSV. В этом можно убедиться, исследовав содержимое окна Program Memo- ry по адресу 0x2 9 6. Обратите внимание па группировку символов “по два”, не за- бывая, что окно PSV позволяет' нам использовать только 16 разрядов каждого 24- разрядного слова памяти программ: 00290 — . 07FFF7 FA8000 060000 00654С . Le. 00298 — 007261 00206Е 006F74 006620 аг. .п . . . to. . f. 002 АО — 00796С 007720 007469 002068 1у. . и. . , it. .h . 002А8 — 006874 002065 004950 003243 th. .е . . . PI. . C2. 002В0 — 000034 00504D 00414С 002042 4. . . ИР. . , LA. .B . 002В8 — 003343 000030 000800 000064 СЗ . .0. . . . d. . Строка инициализации переменной b находится в разделе .dinit. Опа подго- товлена для доступа через команды табличного чтения, и потому использует все 24 разряда каждого слова памяти программ. Обратите внимание на группирование символов “по три”: ОО2СО ---- 000002 696Е49 616974 7А696С ....Ini. tia.liz. ОО2С8 ---- 006465 000000 000000 000000 ed.............. 002D0 ---- 000000 000000 000000 000000 ................ Следующая часть файла .шар, заслуживающая нашего внимания, — раздел “Data Memory Usage” (“Использование памяти данных”, т.е. ОЗУ): Data Memory Usage section address alignment gaps total length (dec) .ndata 0x800 0 0x64 (100) Total data memory used (bytes): 0x64 (100) 1% В нашем простом примере упомянут только раздел . ndata. В нем содержится единственная переменная Ь, для которой зарезервировано 100 байт, начиная с адре- са 0x800 — первой ячейки ОЗУ в микроконтроллерах PIC24. Указатели Указатели — это переменные, содержащие косвенные ссылки па другие пере- менные или фрагменты их содержимого. В программировании па С указатели и строки идут “рука об руку”, поскольку они относятся к одному и тому же эффек- тивному механизму обработки массивов любого типа. Впрочем, несмотря на все свои преимущества, они же представляют собой и наиболее опасный инструмент в руках разработчика, поскольку приводят к подавляющему большинству про- граммных ошибок. В некоторых языках программирования (например, Java) в по- пытке повысить надежность и контролируемость программ были задействованы крайние меры, выраженные в полном отказе от использования указателей.
Полет 91 Компилятор MPLAB СЗО использует преимущества 16-разрядной архитектуры PIC24 для упрощения обработки больших областей памяти данных (до 32 Кбайт ОЗУ). В частности, благодаря окну PSV, компилятор MPLAB СЗО не проводит раз- личий между указателями на объекты в памяти данных и константами, размещен- ными в пространстве памяти программ. Это позволяет одному и тому же набору стандартных функций в случае необходимости манипулировать переменными и/или блоками памяти общего назначения из обоих пространств. В следующем классическом примере использование указателей сравнивается с индексированием для последовательного доступа к элементам массива типа int: int *pi; // Определяем указатель на целое число int i; // Индекс/счетчик int а[10]; // Целочисленный массив // 1. Последовательный доступ путем индексирования массива for(i=0; i<10; i++) a[i] = i; // 2. Последовательный доступ с помощью указателя pi = а; for(i=0; KIO; i++) { *pi = i; pi++; } В пункте 1 при каждом проходе цикла for мы используем в качестве индекса элементов массива переменную-счетчик i. Для реализации такого присвоения ком- пилятору необходимо взять значение i, умножить его на размер элемента массива в байтах (2) и добавить полученное смещение к начальному адресу массива а. В пункте 2 мы инициализируем указатель, содержащий начальный адрес масси- ва а. При каждом проходе цикла для реализации присвоения просто используется указатель (*), который затем инкрементируется. Если сравнить эти два случая, то можно увидеть, что с помощью указателя мы выигрываем по крайней мере один шаг умножения на каждом проходе цикла. Если внутри цикла элемент массива используется несколько раз, то мы получаем значи- тельный прирост производительности. Синтаксис использования указателей в С может быть очень “сжатым”, позволяя создавать эффективный код, однако это увеличивает вероятность возникновения оши- бок. Как минимум, необходимо знать наиболее популярные сокращения. Так, рас- смотренный выше фрагмент кода можно было бы переписать следующим образом: // 2. Последовательный доступ с помощью указателя for(i=0, р=а; i<10; i++) *pi++ = i; Кроме того, отметим, что “пустому” указателю (т.е. указателю, который ни па что не указывает) соответствует специальное значение NULL, реализация которого зависит от конкретного приложения и определено в файле stddef. h. Куча Одно из преимуществ указателей заключается в возможности манипулировать объектами, определенными в памяти динамически (т.е. во время выполнения). “Ку- ча” — это область памяти данных, зарезервированная для подобного использования.
92 Глава 6. Заглянем под капот Для выделения и освобождения блоков памяти в куче предназначен специальный набор функций, реализованных в стандартной библиотеке С stdlib. h. Две наибо- лее базовые из них: • void *malloc (size_t size) ; — извлекает из кучи блок памяти требуе- мого размера и возвращает указатель на пего; • void free (void *ptr) ; — возвращает в кучу блок памяти, на который указывает указатель pt г. Компоновщик MPLAB СЗО размещает кучу в незадействованном пространстве памяти ОЗУ выше всех глобальных переменных проекта и зарезервированной об- ласти стека. Хотя объем незадействованной памяти компоновщику известен и ука- зан в файле . тар для каждого проекта, размер кучи необходимо задавать явно. Выберите команду меню Project ► Build Options ► Project, чтобы открыть диало- говое окно Build Options, перейдите на вкладку MPLAB Link30 и задайте размер кучи (Heap Size). Выделяйте для этого как можно больше памяти, поскольку это позволит функции malloc() использовать память максимально эффективно. В конце кон- цов, все, что не выделено под кучу, просто не используется. Модели памяти MPLAB СЗО Архитектура PIC24 предусматривает очень эффективное (компактное) кодиро- вание команд для всех операций, выполняемых над памятью данных в пределах первых 8 Кбайт адресного пространства. Эту область памяти называют “ближней”, и в случае с микроконтроллером PIC24FJ128GA010 ей соответствует группа регист- ров специального назначения (первые 2 Кбайт) и расположенные после них 6 Кбайт ОЗУ общего назначения. Вне ближней области, лежат только верхние 2 Кбайт ОЗУ. Доступ к памяти за границей 8 Кбайт требует применения методов косвенной адресации (указателей) и в случае неверного планирования может привести к сни- жению производительности приложения. Стек (содержит все локальные перемен- ные функций С) и куча (используется для динамического распределения памяти) изначально адресуются с помощью указателей и, соответственно, являются идеаль- ными кандидатами на размещение в верхней области ОЗУ. Именно это по умолча- нию и пытается сделать компоновщик. Кроме того, для максимизации эффективно- сти он пытается разместить все глобальные переменные, определенные в проекте, в ближней области памяти. Если переменную нельзя разместить в этой области, то она должна быть явно объявлена с помощью атрибута far, чтобы компилятор соз- дал соответствующий код доступа. Подобную модель памяти данных называют “малой” в противоположность “большой” модели, где каждая переменная считается “дальней”, если явно не определена как “ближняя” с помощью атрибута near. На практике, при работе с микроконтроллером PIC24FJ128GA010 почти всегда используется малая модель памяти, и только в редких случаях возникает необходи- мость определять некоторую переменную с помощью атрибута far. В уроке 12 мы рассмотрим подобный случай, когда очень большой массив пришлось объявить как far, поскольку в противном случае он просто не поместился бы в ближней области памяти. В результате не только компилятор создаст корректные команды адресации, но и компоновщик поместит их в верхнюю область ОЗУ, отдав приоритет’ осталь- ным глобальным переменным и предоставив доступ к ним в ближней области. Поскольку доступ к элементам массива (явно с помощью указателей или путем индексирования) в любом случае осуществляется путем косвенной адресации, по- терь производительности или увеличения кода не возникнет.
Разбор полета 93 Аналогичный подход применяется и к пространству памяти программ. Факти- чески, внутри каждого откомпилированного модуля, вызов функций реализован с помощью более компактной схемы адресации, основанной на максимальном диа- пазоне в 32 Кбайт. Модели памяти программ (малая и большая) определяют поведе- ние компилятора/компоновщика в отношении адресации функций в пределах вы- шеупомянутого 32 Кбайтного диапазона и за его пределами. Разбор полета В языке С строки определяются как обычные символьные массивы, однако стандарты С не поддерживают различных областей памяти (ОЗУ или Flash) или ка- ких-то особых механизмов для “наведения моста” между разными шинами гарвард- ской архитектуры. Программист, работающий с х<омпилятором MPLAB СЗО, должен обладать базовым пониманием преимуществ и недостатков различных механизмов и стратегий распределения памяти, чтобы максимально эффективно использовать драгоценные ресурсы микроконтроллера (особенно ОЗУ) для реализации задач встроенного управления. Заметки для экспертов по С В языке С атрибут const обычно используется совместно с большинством ти- пов данных только для того, чтобы помочь компилятору в поиске ошибок использо- вания общих параметров. Когда параметр передается в функцию как константа или подобным образом объявляется переменная, то компилятору не составляет груда распознать любую попытку изменить их. Эта семантика в очень естественной фор- ме расширяется в компиляторе MPLAB СЗО с помощью окна PSV, что позволяет создавать более эффективные реализации (в чем мы уже успели убедиться). Заметки для экспертов по ассемблеру Библиотека string. h содержит множество полезных функций манипулирова- ния блоками памяти с помощью указателей, которые применимы для реализации операций над любыми массивами (не обязательно строками). К числу таких функ- ций относят memcpy (), memcmp (), memset () и memmove (). В то же время, библиотека ctype.h содержит функции, позволяющие разли- чать отдельные символы в зависимости от их положения в таблице ASCII, распо- знавать верхний и нижний регистр букв и/или выполнять преобразования регистра. Заметки для экспертов по PIC Поскольку память программ микроконтроллеров PIC24 реализована с примене- нием технологии Flash, которая поддерживает программирование обычным напря- жением питания даже во время работы устройства и выполнения кода, существует возможность разработки так называемых “загрузчиков”: приложений, которые ав- томатически частично или полностью обновляют собственный код. Кроме того, в некоторых наиболее базовых ограничениях область флэш-памяти программ мож- но использовать в качестве энергонезависимого хранилища. Впрочем, для записи в такую память необходимо применять методы табличного доступа, и притом — крайне осторожно. Окно PSV доступно только для чтения и предоставляег доступ только к 16 из 24 разрядов каждой ячейки в памяти программ.
94 Глава 6. Заглянем лод капот Кроме того, обратите внимание на то, что запись в такую память можно выпол- нять только полными строками по 64 слова в каждой с предварительным стиранием блоками по восемь строк по 512 слов в каждой. В результате при общей обработке отдельных слов или небольших структур данных частое обновление становится не- выгодно с практической точки зрения. Советы и хитрости Если заставить нулевой символ конца строки работать на нас, то работа со строками в С превращается в веселую забаву. Рассмотрим, например, следующую функцию туеру (): void туеру(char *dest, char * sre) { while(*dest++ = *src++); } Этот фрагмент кода — очень опасный, поскольку в нем не ограничивается ко- личество копируемых символов. Кроме того, отсутствует проверка размера буфера, на который указывает указатель dest. Можете представить, что произойдет, если строка sre не будет заканчиваться нулевым символом! Эта функция запросто мо- жет выйти за пределы областей, выделенных под переменные, и разрушить все дан- ные в ОЗУ, включая драгоценное содержимое регистров специального назначения. Как минимум, необходимо по возможности проверять передаваемые в функции указатели на предмет их инициализации. Сравнивайте их со значением NULL (объ- явлено в файле stdlib. h и stddef. h) — это поможет избежать ошибок. Ограничьте число копируемых байтов. Логично предположить, что размер за- действованных в программе строк и массивов известен, если же это не так, восполь- зуйтесь оператором sizeof(). Более падежная реализация функции туеру () может выглядеть следующим образом: void myepy(char *dest, char *src, int max) { if ((dest ’= NULL) && (sre != NULL)) while ( (max— > 0) && (*src) ) *dest++ = *src++; } Упражнения Разработайте функции обработки строк для выполнения следующих операций: • последовательный поиск строки в массиве строк; • бинарный поиск; • простая библиотека управления хэш-таблицей. Ссылки • Обратитесь по Web-адресу http://en.wikipedia.org/wiki/Pointers #Support_in_various_programming_languages, чтобы узнать больше об указателях и о том, как с ними работать в различных языках программирова- ния.
ЧАСТЬ II Сольный полет Примите поздравления! Вы прошли первые уроки, обретя необходи- мую уверенность для своего первого полета без инструктора в сосед- нем кресле. Теперь вас ожидает сольный полег! Как следствие, в пред- ставленном далее наборе уроков от вас ожидается большего. Во второй части книги мы продолжим поэтапное изучение базовой пе- риферии, позволяющей микроконтроллерам PIC24 взаимодействовать с внешним миром. Поскольку примеры станут несколько сложнее, ре- комендуется иметь под рукой физическую демонстрационную плату для выполнения практических экспериментов. Я часто буду ссылаться па стандартную плату Microchip Explorer 16, однако с тем же успехом можно использовать и изделия других производителей с аналогичными характеристиками.
ГЛАВА 7 Обмен данными В этой главе: ' - ; *: 6 * Л / ' ’ ' ' ' ' " л .• ' > Синхронные последовательные интерфейсы > Асинхронные последовательные интерфейсы Параллельные интерфейсы > Синхронный обмен данными с помощью модулей SPI Проверка команды “Read Status Register” Запись в память EEPROM & Чтение содержимого памяти > Библиотека функций для работы с энергонезависимым хранилищем данных >• Тестирование новой библиотеки NVM Некоторые авиакомпании иногда используют дополнительный радиоканал, по которому можно услышать фактические переговоры между пилотами и диспетче- рами. Когда их слышишь впервые, то просто не верится, что эти фразы — осмыс- ленные. Переговоры кажутся бессвязным набором цифр и непонятных аббревиатур, в которых на первый взгляд нет никакого смысла. Тем не менее, познакомившись с авиационной терминологией, ты начинаешь понимать, о чем идет речь. Оба пило- та и диспетчеры следуют точному протоколу. При этом используются определен- ные радиочастоты, а для обмена информацией с пунктом управления каждый лег- чик должен освоить особый язык позывных и условных обозначений. В мире встроенных систем обмен данными также сводится к следованию опре- деленному протоколу и соблюдению характеристик физической среды передачи. При программировании таких систем важно уметь не только использовать различ- ные коммуникационные интерфейсы, но и правильно их выбирать в каждом кон- кретном случае. План полета В этом уроке мы рассмотрим пару периферийных коммуникационных уст- ройств, реализованных во всех устройствах общего назначения нового семейства микроконтроллеров PIC24. В частности, будут изучены асинхронные интерфейсы последовательного обмена данными UART1 и UART2, а также синхронные интер- фейсы SPI1 и SPI2. Мы рассмотрим их относительные преимущества и ограничения для использования во встроенных системах.
Предполетный контроль 97 Предполетный контроль В дополнение к обычным программным средствам (интегрированная среда MPLAB, компилятор MPLAB СЗО и имитатор MPLAB SIM), в этом уроке потребу- ется демонстрационная плата Explorer 16, а также внутрисхемный отладчик MPLAB IC2. С помощью контрольного списка “Настройка нового проекта” создайте новый проект под именем SPI и новый файл исходного кода spi2 . с. Полет Микроконтроллер PIC24FJ128GA010 содержит семь периферийных коммуни- кационных устройств, предназначенных для упрощения решения различных задач встроенных приложений. Шесть из них — “последовательные”, поскольку передают и принимают по одному биту информации за раз: • два универсальных асинхронных приемо-передатчика (UART); • два последовательных синхронных интерфейса SPI; • два последовательных синхронных интерфейса 12С. Основное отличие синхронного интерфейса, наподобие SPI и 12С, от асинхрон- ного (например, UART) заключается в способе передачи информации о синхрони- зации между передатчиком и приемником. Синхронной периферии требуется физи- ческая линия (провод) для выделенного тактового сигнала, обеспечивающего син- хронизацию работы двух устройств. Устройство, выдающее тактовый сигнал, обыч- но называют “ведущим”, а устройство, принимающее такой сигнал, — “ведомым”. Синхронные последовательные интерфейсы Например, интерфейс 12С, использует два провода (а значит — и два вывода микроконтроллера): один — для тактового сигнала (линия SCL), а второй (ревер- сивный) — для передачи данных (линия SDA) (рис. 7.1). PIC24 интерфейс 12С (Ведущий) Тактовый сигнал (SCL) Периферия 12С (Ведомый) Данные (SD А) Я " ,|||Г F Рис. 1.1. Структурная схема интерфейса 12С В отличие от 12С, в интерфейсе SPI используются две линии данных: одна — входная (SDI), и одна — выходная (SDO) (рис. 7.2). Таким образом, он требует од- ного дополнительного провода, однако позволяет реализовать одновременную пе- редачу данных в обоих направлениях (т.е. более скоростной). Для подключения нескольких устройств к одним и тем же последовательным линиям (конфигурация шины) интерфейсу 12С требуется 10-разрядный адрес, кото- рый должен быть передан по линии данных перед фактическими данными. Это за- медляет передачу, однако позволяет подключить к одной паре проводов SCL и SDA (теоретически) до 1 000 устройств. Кроме того, интерфейс 12С позволяет несколь- ким устройствам выступать в качестве ведущих и совместно использовать одну ши- ну по простому протоколу арбитража.
98 Глава 7. Обмен данными Рис. 7.2. Структурная схема интерфейса SP С другой стороны, интерфейс SPI для подключения каждого устройства требует дополнительной физической линии выбора ведомого (SS). На практике это означа- ет, что в случае использования интерфейса SPI с увеличением количества подклю- ченных устройств пропорционально возрастает потребность в выводах микрокон- троллера PIC24 (рис. 7.3). Рис. 7.3. Структурная схема шины SPI Использование шины SPI одновременно несколькими ведущими устройствами теоретически возможно, однако на практике встречается очень редко. Главное пре- имущество интерфейса SPI — это его простота и быстродействие, которое па поря- док выше даже самой скоростной шипы 12С (оно компенсирует даже некоторую из- быточность, характерную для протокола SPI). Асинхронные последовательные интерфейсы В асинхронных коммуникационных интерфейсах отсутствует линия тактирова- ния и обычно задействованы две линии передачи данных: ТХ (входная) и RX (вы- ходная). Кроме того, можно использовать две дополнительные линии доя обеспече- ния обмена с квитированием (рис. 7.4). Рис. 7.4. Структурная схема асинхронного последовательного интерфейса
Полет 99 Синхронизация между передатчиком и приемником достигается путем извлече- ния информации о тактовом сигнале из самого потока данных, в который внедряют- ся стартовый и столовый биты. В таком варианте надежный обмен данными обеспе- чивается точным форматированием с фиксированной скоростью передачи. Для повышения помехозащищенности, позволяющей увеличить физические расстояния передачи до нескольких километров, некоторые стандарты асинхронно- го обмена требуют использования специальных передатчиков. Каждый последовательный коммуникационный интерфейс имеет свои достоин- ства и недостатки, которые вместе с наиболее типичными областями применения сведены в табл. 7.1. Таблица 1.1. Сравнение синхронных и асинхронных последовательных интерфейсов Синхронные Асинхронный Периферия SPI l2C UART Максимальная ско- рость передачи 10 Мбит/с 1 Мбит/с 500 Кбит/с Максимальный размер буфера Ограничен количеством выводов 128 устройств Двухточечная (RS232) 256 устройств (RS485) Количество выводов 3 + п х CS 2 2 Достоинства Простота, низкая стои- мость, большое быстро- действие Малое число выводов, возможность подключе- ния нескольких ведущих устройств Большие расстояния пере- дачи, повышенная помехо- защищенность (требуются передатчики) Недостатки Одно ведущее устройство, малые расстояния переда- чи Наиболее медленнодей- ствующий, малые рас- стояния передачи Требует точной частоты тактового сигнала Типичные области применения Прямое подключение к специализированным микросхемам и другой пе- риферии на той же печат- ной плате Подключение по шине к периферии на той же печатной плате Взаимодействие с терми- налами, персональными компьютерами и другими системами сбора данных Примеры Последовательная память EEPROM (серия 25СХХХ), АЦП МСР320Х, Ethernet- контроллер ENC28J60, CAN-контроллер МСР251Х Последовательная па- мять EEPROM (серия 24СХХХ), датчики темпе- ратуры МСР98ХХ, АЦП МСР322Х RS232, RS422, RS485, шина LIN, lrDA-интерфейс МСР2550 Параллельные интерфейсы Перечень базовых коммуникационных интерфейсов микроконтроллеров PIC24 завершает ведущий параллельный порт РМР (Parallel Master Port), обеспечивающий одновременную передачу до восьми бит данных. При этом он предоставляет не- сколько адресных линий, что позволяет напрямую взаимодействовать с большинст- вом коммерческих модулей ЖК-дисплеев (буквенно-цифровые и графические мо- дули со встроенным контроллером), а также — с картами памяти Compact Flash (или CF-устройствами ввода-вывода), портами принтеров и многочисленными уст- ройствами с восьмибитным параллельным обменом данными, реализующими стан- дартные управляющие сигналы -CS, -RD и -WR.
100 Глава 7. Обмен данными В оставшейся части этого урока мы остановимся на изучении только синхрон- ного последовательного интерфейса SPI, а асинхронные последовательные интер- фейсы и порт РМР рассмотрим подробнее в последующих главах книги. Синхронный обмен данными с помощью модулей SPI Интерфейс SPI — это, наверное, самый простой из всех существующих интер- фейсов, хотя микроконтроллеры PIC24 при работе с ним предоставляют множество дополнительных и интересных возможностей (рис. 7.5). (1) В стандартных режимах данные передаются напрямую между SPIxSR и SPIxBUE Рис. 7.5. Структурная схема модуля SPI Интерфейс SPI по сути представляет собой восьмиразрядный сдвиговой ре- гистр, в который постоянно “вталкиваются” биты (начиная с младшего значащего) с линии SDI, “выталкивая” старшие разряды в линию SDO по сигналу синхрониза- ции на выводе SCK. Если устройство сконфигурировано как ведущее на шине, то тактовый сигнал формируется внутренне (для достижения максимальной гибкости его получают из периферийного синхросигнала после каскада из двух предделителей) и выдается на вывод SCK. Если устройство — ведомое, то оно получает тактовый сигнал с вывода SCK. Как и в случае со всеми другими периферийными устройствами, основные кон- фигурационные настройки определяются отдельным регистром специального на- значения SPIxCONl (рис. 7.6), а дополнительные — регистром SPIxCONl.
Полет 101 Старший байт: и-о и-о и-о R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 | — DISSCK | DISSDO | MODE16 | SMP СКЕ разряд 15 разряд 8 Младший байт: R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 SSEN СКР MSTEN SPRE2 SPRE1 SPRE0 PPRE1 | PPRE0 разряд 7 разряд 0 Рис. 7.6. Управляющий регистр SPixCONi Для демонстрации базовой функциональности периферии SPI мы воспользуем- ся демонстрационной платой Explorer 16, в которой модуль SPI2 микроконтроллера PIC24 подключен к EEPROM-схеме 25LC256 (ее еще часто называют последова- тельной EEPROM-памятыо, SEE или просто Е2) — недорогой, энергонезависимой, долговечной памяти на 256 Кбит (32 Кбайт). Для того чтобы подготовить модуль SPI2 к обмену данными с устройством последовательной памяти, его необходимо тщательно конфигурировать. Устройство SEE реагирует на небольшой набор восьмиразрядных (MODE 16 = 0) команд, которые согласно спецификации должны выдаваться по интерфейсу SPI со следующими установками: • неактивным для тактового сигнала считается низкий, а активным — высокий уровень (СКР = 0); • состояние последовательного выхода изменяется при переходе из активного со- стояния в неактивное (СКЕ = 1). Микроконтроллер PIC24 выступает в роли ведущего устройства на шине (MSTEN = 1) и выдает тактовый сигнал SCK, полученный из внутреннего синхро- сигнала после прохождения через предделители (в данном случае для получения суммарного коэффициента 1:512 мы используем выбранные по умолчанию коэффи- циенты 1:64 и 1:8). Выбранное конфигурационное значение можно определить как константу, ко- торая позже будет записана в регистр SPI2CON1: ♦define SPI_MASTER 0x0120 // Выбор 8-разрядного режима ведущего, // СКЕ=1, СКР=0 Для активизации периферии (как и в случае с большинством других перифе- рийных устройств микроконтроллеров PIC24) мы обратимся к регистру SPI2STAT. Главный разряд активизации управления — разряд 15, поэтому для надежности оп- ределим еще одну константу: tfdefine SPI_ENABLE 0x8000 //Активизация порта SPI, очистка статуса Вывод 12 порта D соединен с линией выбора кристалла памяти (CS), для кото- рого активным является низкий уровень сигнала, поэтому мы добавим в программу еще два определения (опять таки — для повышения ее надежности): #define CSEE _RD12 // Выбор линии для схемы SEE #define TCSEE _TRISD12 // TRIS-управление для вывода CSEE Теперь можно написать код инициализации периферии: // 1. Инициализация периферии SPI для PIC24
102 Глава?. Обмен данными TCSEE =0; // Вывод SSEE - выход CSEE =1; // Отменяем выбор кристалла SEE // (перевод в режим низкого энергопотребления) SPI2CON1 = SPI_MASTER; // Выбор режима SPI2STAT = SPI_ENABLE; // Активизируем периферийный модуль Далее создадим небольшую функцию, предназначенную для обмена данными с последовательным EEPROM-устройством: // Одновременная передача и прием одного байта данных int writeSPI2(int data) { SPI2BUF = data; // Запись в буфер для ТХ while(!SPI2STATbits.SPIRBF); // Ожидаем завершения передачи return SPI2BUF; // Считывание принятого значения }//writeSPI2 Функция writeSPI2 обеспечивает передачу данных в двух направлениях. Она сразу же записывает символ в буфер передачи и переходит в цикл ожидания уста- новки флага приема, что указывает па завершение передачи и возврата данных уст- ройством. Затем принятые данные возвращаются в качестве значения функции. Тем не менее, при взаимодействии с устройством памяти бывают ситуации, ко- гда модулю передается некоторая команда, но ответа сразу же не поступает. Кроме того, иногда данные считываются из памяти, однако микроконтроллеру PIC24 больше не требуется выдавать никаких команд. В первом случае (команда записи) значение, возвращаемое функцией, можно просто проигнорировать, а во втором (команда чтения) в ходе сдвига данных, поступающих от устройства, памяти можно передавать некоторое фиктивное значение. В спецификации микросхемы 25LC256 содержится подробное описание всех возможных последовательностей команд, применимых для обмена данными с уст- ройством памяти. Для их кодирования пригодится следующий набор констант: // Команды для SEE-модуля 25LC256 #define SEE WRSR 1 // Запись в регистр состояния #define see' 'write 2 // Команда записи #define see' 'read 3 // Команда чтения #define see' 'WDI 4 // Запрет записи #define see' 'stat 5 // Чтение’регистра состояния #define see" 'wen 6 // Разрешение записи Теперь мы можем написать небольшую тестовую программу для проверки кор- ректности обмена данными с устройством (например, опросить с помощью команды чтения регистра состояния “Read Status Register” модуль памяти и удостовериться в том, что периферия SPI сконфигурирована корректно). Проверка команды “Read Status Register” После передачи соответствующей команды (SEE_STAT) необходимо добавить вызов функции writeSPI2() с фиктивными данными для фиксации ответа от устройства памяти (рис. 7.7). Передача любой команды модулю SEE требует как минимум следующих опе- раций: • активизация памяти путем перевода вывода CS в состояние низкого уровня; • выдача из сдвигового регистра восьми бит команды; • дополнительные операции в зависимости от специфики команды;
Полот 103 • деактивация памяти (перевод вывода CS в состояние высокого уровня) для за- вершения команды (возврат устройства в режим пониженного энергопотребле- ния). SCK 21 22 23 24 25 26 27 28 29 30 31 ЛЛЛЛЛЛЛЛЛЛЛ. Команда 1 б бит адреса ••• SI so Высокий импеданс Вывод данных Рис. 1.1. Полная временная диаграмма для команды “Read Status Register” На практике для реализации операции “Read Status Register” требуется следую- щий код: // Проверка состояния последовательной памяти EEPROM CSEE =0; // Выбор кристалла SEE writeSPI2(SEE_STAT); // Передача команды READ STATUS; игнорируем // непосредственные данные i = writeSPI2(0); // Передаем фиктивное значение, считываем /7 данные CSEE =1; // Деактивация модуля памяти для завершения // команды Полный листинг проекта выглядит следующим образом. 7*............... ** SPI2demo */ #include <p24fj128ga010.h> // I/O definitions #define CSEE _RD12 #define TCSEE _TRISD12 // // Выбор линии для модуля SEE TRIS-управление для вывода CSEE // конфигурирование периферии ♦define SPI MASTER 0x0120 // // ♦define SPI_ENABLE 0x8000 // Выбор 8-разрядного режима ведущего, СКЕ=1, СКР=0 Активизируем порт SPI, очистка статуса // Команды для микросхемы ♦ define SEE_WRSR 1 ♦ define SEE_WRITE 2 ♦ define SEE_READ 3 ♦ define SEE_WDI 4 ♦ define SEE_STAT 5 ♦ define SEE_WEN 6 EEPROM-памяти 25LC256 // Запись в регистр состояния // Команда записи // Команда чтения // Запрет записи // Чтение регистра состояния // Разрешение записи // Передача и прием одного байта intwriteSPI2(int data) { SPI2BUF = data; // Запись в буфер для ТХ while(’SPI2STATbits.SPIRBF); // Ожидаем завершения передачи return SPI2BUF; // Чтение принятого значения } //writeSPI2
104 Глава 7. Обмен данными main () { int i; // 1. Инициализация периферийного модуля SPI TCSEE =0; // Вывод SSEE - выход CSEE =1; // Деактивация модуля EEPROM-памяти SPI2CON1 = SPI_MASTER; // Выбираем режим SPI2STAT = SPI—ENABLE; // Активизируем периферийный модуль // 2. Проверка состояния модуля SEE CSEE = 0; // Выбор модуля EEPROM-памяти writeSPI2(SEE_STAT); // Передача команды READ STATUS i = writeSPI2(0); // Передача фиктивного значения, чтение данных CSEE =1; // Завершение команды } / /main Следуя контрольному списку “Настройка отладчика MPLAB ICD2” активизи- руйте внутрисхемный отладчик и подготовьте конфигурацию проекта. Затем с по- мощью контрольного списка “Сборка проекта” откомпилируйте и скомпонуйте код демонстрационной программы. 1. После подключения ICD2 к демонстрационной плате Explorer 16 запрограмми- руйте микроконтроллер PIC24, выбрав команду меню Debugger ► Program. По умолчанию MPLAB для минимизации времени программирования избирает наименьший диапазон памяти, требуемый для передачи кода проекта в устрой- ство. Спустя несколько секунд микроконтроллер PIC24 должен быть запро- граммирован, проверен и готов к работе. 2. Добавьте в проект окно Watch. 3. Выберите в раскрывающемся списке Symbol элемент i и нажмите кнопку Add Symbol. 4. Разместите курсор в последней строке кода внутри главного цикла и дважды щелкните мышью, чтобы установить точку прерывания, после чего запустите программу на выполнение с помощью команды меню Debugger ► Run. Когда ход выполнения программы прервется, в переменной i, отображенной в окне Watch, должно находиться содержимое регистра состояния микросхемы 25LC256. К сожалению, окажется, что по умолчанию регистр состояния модуля па- мяти 25LC256 после включении питания содержит значение 0x00, поскольку раз- ряды ВР1..ВР0 обнулены (защита блока), фиксатор разрешения записи (WEL — Write Enable Latch) отключен, и флаг “Выполняется запись” (WIP — Write In Pro- gress) не может быть установлен (табл. 7.2). Таблица 7.2..Регистр состояния микросхемы 25LC256 7 6 5 4 3 2 1 0 W/R — — — W/R W/R R R WPEN X X X ВР1 ВРО WEL WIP W/R — запись/чтение; R — только чтение. Не особо наглядный результат для нашей небольшой тестовой программы, по- этому нам необходимо что-то предпринять. Для начала перед опросом регистра со- стояния установим разряд WEL. Для этого добавим следующий код перед разделом 2, который по ходу переименуем в 2.2:
Полет 105 // 2.1. Передача команды разрешения записи CSEE =0; // Выбор кристалла SEE writeSPI2(SEE—WEN); // Передача команды; игнорируем данные CSEE =1; // Завершение команды Перекомпонуйте проект, еще раз запрограммируйте устройство, установите точку прерывания в последней строке главного цикла программы и выберите ко- манду меню Run или Run to Cursor. Если все было сделано верно, то переменная i в окне Watch будет выделена красным цветом (должна содержать значение 2). Теперь, когда установлен разряд WEL, мы можем добавить команду записи и начать “модификацию” содержимого EEPROM-устройства. Например, можно реализовать запись по одному байту или запись длинной строки вплоть до 64 байт с помощью страничной команды “Page Write”. Об ограничениях, накладываемых на адресацию в последнем режиме, можно более подробно узнать из технического описания устройства. Запись в память EEPROM После передачи команды записи перед выдачей фактических данных требуется два байта адреса (ADDR_MSB, ADDR_LSB). Рассмотрим пример корректной после- довательности операторов для реализации записи: // Передача команды записи CSEE =0; // Выбор модуля SEE writeSPI2(SEE_WRITE); // Передаем команду, игнорируем фактические // данные writeSPI2(ADDR_MSB); // Передаем старший байт адреса памяти writeSPI2(ADDR_LSB); // Передаем младший байт адреса памяти writesPI2(data); // Передаем фактические данные для записи // Передача новых данных для страничной записи CSEE =1; // Начало фактического цикла записи в EEPROM Обратите внимание, что фактический цикл записи в память EEPROM начинает- ся только после того, как на линии CS опять установится высокий уровень сигнала. Кроме того, для завершения цикла перед выдачей новой команды потребуется вы- ждать некоторое время (Twc), определенное в спецификации запоминающего уст- ройства. Для того чтобы память гарантировано получила достаточно времени для завершения команды записи, применяют два метода. Простейший из них заключа- ется в добавлении после набора операторов, реализующих запись, фиксированной задержки, длительность которой должна превышать максимальную допустимую продолжительность цикла для запоминающего устройства (Twc max = 5 мс). Более совершенный метод заключается в проверке перед выдачей очередной команды чтения/записи содержимого регистра состояния в ожидании сброса флага “Запись активна” (Write In Progress, WIP), что попутно сопровождается установкой разряда разрешения записи Write Enable. Таким образом, мы ожидаем только точное минимальное время, необходимое запоминающему устройству в текущих рабочих условиях. Чтение содержимого памяти Чтение содержимого памяти реализуется еще проще. Рассмотрим фрагмент ко- да с необходимой для этого последовательностью операторов: // Передача команды записи CSEE =0; // Выбор модуля SEE
106 Глава 7. Обмен данными writeSPl2(SEE_READ); writeSPI2(ADDR_MSB); writeSPI2(ADDR—LSB); data = writeSPI2 (0); ’// Дальнейшее чтение CSEE = 1; //. Перпедаем команду, игнорируем фактические // данные // Передаем старший байт адреса памяти // Передаем младший байт адреса памяти // Передаем фиктивное значение, читаем данные данных с последовательным увеличением адреса // Завершаем чтение, переход в режим // пониженного энергопотребления В случае необходимости данная последовательность операторов может быть расширена для чтения всего содержимого памяти, а по достижении последнего ад- реса (0x7FFF) можно начать чтение сначала (с адреса 0x0000). Библиотека функций для работы с энергонезависимым хранилищем данных Теперь мы можем скомпоновать небольшую библиотеку функций, предназна- ченных для доступа к последовательной EEPROM-памяти 25LC256. В этой библио- теке все детали реализации, наподобие задействованных портов SPI, специфичных последовательностей команд и особенностей хронометража, будуг скрыты. Откры- тыми останутся только две базовые команды для чтения и записи целочисленных данных в обобщенное энергонезависимое запоминающее устройство. С помощью мастера Project Wizard и привычного контрольного списка создайте новый проект под именем NVM. Создав новый файл исходного кода nvm. с, скопи- руйте в него определения из ранее подготовленного проекта SPI2: /* ** Библиотека доступа NVM */ tfinclude <p24fj128ga010.h> #include "NVM.h” // Определения для ввода-вывода для микроконтроллера PIC24 и // демонстрационной платы Explorer16 #define CSEE _RD12 // Выбор линии для памяти EEPROM #define TCSEE _TRISD12 // TRIS-управление для вывода CSEE // Конфигурация периферии #define SPI_MASTER 0x0122 // Выбор 8-битного режима ведущего, // СКЕ = 1,СКР = 0 #define SPI_ENABLE 0x8000 // Активизация порта SPI, обнуление // статуса // Команды последовательной EEPROM-памяти 25LC256 #define SEE—WRSR 1 // Запись в регистр состояния #define SEE_WRITE 2 // Команда записи #define SEE_READ 3 // Команда чтения #define SEE—WDI 4 // Запрет записи #define SEE_STAT 5 // Чтение регистра состояния #define SEE_WEN 6 И Разрешение записи Из того же проекта можно почерпнуть код инициализации, функцию записи в SPI2 и команду чтения регистра состояния. Каждый из этих фрагментов оформим в виде отдельной функции:
Полет 107 void InitNVM(void) { // Инициализируем периферию SPI TCSEE = 0; // Вывод SSEE - выход CSEE = 1; // Выбор модуля SEE SPI2CON1 = SPI_MASTER; // Выбор режима SPI2STAT = SPI_ENABLE; // Активизация периферии }//InitNVM int writeSPI2(int data) { // Одновременно передаем и принимаем один байт данных SPI2BUF = data; // Запись в буфер для ТХ while(!SPI2STATbits.SPIRBF); // Ожидаем завершения передачи -return SPI2BUF; // Считываем принятое значение }//WriteSPI2 int ReadSR(void) // Проверка регистра состояния модуля Serial EEPROM int i; CSEE = 0; // Выбор модуля Serial EEPROM WriteSPI2(SEE_STAT); // Передаем команду чтения статуса i = WriteSPI2(0); // Передача/прием CSEE = 1; return i; // Прекращаем выполнение команды }//ReadSR Для создания функции, считывающей целочисленное значение из энергонезави- симой памяти, вначале необходимо удостовериться в том, что предыдущая команда (записи) была корректно прервана. Для этого следует извлечь содержимое регистра состояния. Для формирования целого значения используегся последовательное чте- ние двух байтов: int iReadNVM(int address) { // Чтение 16-разрядного значения, начиная с четного адреса int Isb, msb; // Ожидаем завершения любых операций while(ReadSR() & 0x3); // Проверяем два младших разряда WEN и WIP // Выполняем 16-разрядное чтение (т.е. считываем два байта) CSEE =0; // Выбор модуля Serial EEPROM WriteSPI2(SEE_READ); // Команда чтения WriteSPI2(address » 8); // Вначале адрес старшего байта WriteSPI2(address & Oxfe); // Адрес младшего байта (выровненный по // слову) msb = WriteSPI2(0); // Передаем фиктивное значение, // считываем старший байт Isb = WriteSPI2 (0); // Передаем фиктивное значение, // считываем младший байт CSEE = 1; return ( (msb << 8) + Isb ); }//iReadNVM Наконец, создадим функцию разрешения записи на основе небольшого фраг- мента кода, использованного в предыдущем проекте для доступа к фиксатору Write Enable. Добавим к нему реализацию страничной записи:
108 Глава 7. Обмен данными void WriteEnable(void) { // Передача команды разрешения записи Write Enable CSEE =0; // Выбор модуля Serial EEPROM WriteSPI2(SEE_WEN); // Команда разрешения записи CSEE =1; // Завершение команды }//WriteEnable void iWriteNVM(int address, int data) { // Запись 16-разрядного значения, начиная с четного адреса int Isb, msb; // Ожидаем завершения любых операций while(ReadSR() & 0x3); // Проверяем два младших разряда WEN и WIP // Устсналиваем разряд Write Enable WriteEnable (); // Реализуем 16-разрядную запись (страничная запись двух байт) CSEE =0; // Выбор модуля Serial EEPROM WriteSPI2(SEE_WRITE); // Команда записи WriteSPI2(address » 8); // Вначале адрес старшего байта WriteSPI2(address & Oxfe); // Адрес младшего байта (выровненный по // слову) WriteSPI2 (data»8) ; // Передаем старший байт WriteSPI2(data & Oxff); // Передаем младший байт CSEE = 1; }//iWriteNVM Можно было бы добавить еще какие-нибудь функции (например, для доступа к данным типа long и longlong), но для нашего примера это излишне. Обратите внимание, что операция “страничной записи” (подробности см. в спе- цификации модуля памяти 25LC256) требует, чтобы адрес был выровнен по границе степени двух (в данном примере это справедливо только для четного адреса). Для согласованности это же требование должно учитываться и в функции чтения. Сохраните код в файле nvm. с и добавьте его в проект с помощью одного из трех методов, указанных в контрольных списках. Например, можете щелкнуть пра- вой кнопкой мыши в окне редактора и выбрать в контекстном меню команду Add to Project или же щелкнуть правой кнопкой мыши в окне проекта на элементе Source Files, выбрать в контекстном меню команду Add Files и выбрать файл NVM. с. Файл nvm. с находится также на прилагаемом к книге компакт-диске в папке Проекты\07 - spi. Для того чтобы некоторые из функций в рассмотренном программном модуле были доступны для других приложений, создайте новый файл под именем NVM. h и добавьте в пего следующие объявления: 7*................. * * Библиотека функций доступа NVM * * Предназначена для последовательной EEPROM-памяти 25LC256, * * как запоминающего NVM-устройства для систем на базе * * микроконтроллера PIC24 и демонстрационной платы Explorer16 */ // Инициализация доступа к запоминающему устройству void InitNVM(void);
Полет 109 // Функции 16-битного чтения и записи // ПРИМЕЧАНИЕ: адрес должен быть четным в даиапазоне 0x0000..0x7ffe // (см. ограничения для страничной записи в спецификации устройства) int iReadNVM(int address); void iWriteNVM(int address, int data); В результате общедоступными будут только функции инициализации и чтения/ записи, а все остальные детали реализации — скрыты. Файл nvm . h находится также на прилагаемом к книге компакт-диске в папке Проекты\07 - spi. Добавьте файл NVM. h в проект, щелкнув правой кнопкой мыши в окне проекта на элементе Header Files. Тестирование новой библиотеки NVM Для того чтобы опробовать функциональность библиотеки, можно создать тес- товое приложение, содержащее несколько строк кода, постоянно считывающих со- держимое некоторой ячейки памяти (по адресу 0x1234), увеличивающих считанное значение и записывающих его обратно в память: 7*...................................... ** Тест библиотеки NVM */ tfinclude <p24fj128ga010.h> tfinclude "NVM.h" main() { int data; // Инициализация порта SPI2 и линии CS для доступа к модулю 25LC256 InitNVMO ; // Главный цикл while(1) { // Считываем содержимое ячейки памяти data = iReadNVM(0x1234); // Увеличиваем на единицу текущее значение Nop(); // <- Установите здесь точку прерывания data++; // Записываем новое значение обратно iWriteNVM(0x1234, data); // address++; }// Главный цикл }//main Сохраните файл под именем NVMtest. с и добавьте его в текущий проект. еФайл NVMtest. с находится также на прилагаемом к книге компакт-диске в папке Проекты\07 - SPI. После выбора команды Build АП компилятор MPLAB СЗО последовательно об- работает два файла с исходным кодом (. с), после чего компоновщик объединит по- лученный объектный код в исполняемый файл . hex.
110 Глава 7. Обмен данными Для тестирования данного кода мы воспользуемся отладчиком ICD2, поскольку средства MPLAB SIM не позволяют точно имитировать порты SPL Выберите соот- ветствующий пункт в подменю Debugger ► Select Tool. Кроме того, выберите коман- ду меню Project ► Build Options и установите на вкладке MPLAB LINK30 флажок Link for ICD2 (рис. 7.8) Рис. 7.8. Вкладка MPLAB UNK30 диалогового окна Build Options Эта настройка необходима при работе с отладчиком ICD2 для резервирования нескольких ячеек ОЗУ (в конце пространства памяти) для самого отладчика во из- бежание конфликтов с памятью, выделенной для нашего приложения. Если команда Build АН отработала успешно, то будет получен код, готовый для программирования в устройство. Для тестирования библиотеки NVM добавьте пе- ременную data в окно Watch и установите точку прерывания в строке, следующей сразу же после команды чтения. В результате после выбора команды Run, програм- ма остановится после первой операции считывания. Запомнив значение переменной data, выберите команду Run еще раз. Значение будет постоянно увеличиваться. Даже если мы подадим на устройство сигнал сбро- са или полностью отключим плату от источника питания, а затем подключим ее по- вторно, то увидим, что содержимое ячейки 0x1234 осталось неизменным. ВНИМАНИЕ! Если главный цикл выполнять непрерывно, то тестовая программа вскоре станет для микросхемы памяти испытанием на выносливость. По сути, цикл будет постоян- но перепрограммировать ячейку 0x1234 со скоростью, которая, в основном, зависит от текущего значения Twc устройства. В лучшем случае (максимальное время Twc - 5 мс) это означает 200 обновлений в секунду. Другими словами, теоретический пре- дел выносливости памяти EEPROM (миллион циклов) будет исчерпан за 5 000 се- кунд, т.е. немного меньше, чем за полтора часа непрерывной работы.
Разбор полета 111 Разбор полета В этом уроке мы кратко рассмотрели использование периферийного модуля SPI в простейшей конфигурации для получения доступа к микросхеме последователь- ной EEPROM-памяти 25LC256 — одного^из наиболее популярных модулей энерго- независимой памяти в области встроенных систем. Созданная нами небольшая биб- лиотека функций пригодится в будущих проектах для обеспечения взаимодействия с запоминающим устройством объемом 32 Кбайт. Заметки для экспертов по С Программисты на С, привыкшие к разработке кода для рабочих станций и пер- сональных компьютеров, наверняка захотят расширить библиотеку NVM, включив в нее наиболее гибкие и полные функции. Я настоятельно рекомендую воздержать- ся от подобных шагов, и особенно — от добавления параметров в библиотечные подпрограммы. В мире встроенных систем передача дополнительных параметров приводит к дополнительной загрузке стека, увеличению времени обмена данными с ним и к более массивному результирующему коду. Старайтесь не усложнять библиотеки, чтобы их было просто тестировать и со- провождать. Это не означает, что необходимо совсем забросить правила объектно- ориентированного программирования. Совсем наоборот. Рассмотренный выше про- ект’ — пример объектной инкапсуляции, поскольку все детали интерфейса SPI и внутренней работы микросхемы памяти EEPROM полностью скрыты от пользова- теля, которому предоставляется только простой механизм доступа к обобщенному запоминающему устройству. Заметки для экспертов по PIC При разработке представленного выше примера мы не учитывали каких-либо аспектов скорости доступа и просто сконфигурировали модуль SPI в расчете па максимально медленное функционирование. Периферийный модуль SPI микрокон- троллеров PIC24 работает вне диапазона частот системной синхронизации, которые в современных моделях может достигать 16 МГц. Совсем немногие периферийные устройства могуг работать па такой скорости при напряжении 3 В. Так, микросхема последовательной EEPROM-памяти 25LC256 поддерживает максимальную такто- вую частоту 5 МГц при напряжении питания 2,5..4,5 В. Таким образом, наиболее скоростную конфигурацию порта SPI, совместимую с запоминающим устройством, можно получить, когда коэффициент первичного предделителя составляет 4:1, а вторичного — 1:1 (16 МГц /4 = 4 МГц). В результате, последовательные вызовы команды чтения обеспечивают максимальную пропускную способность 4 Мбит/с или 512 Кбайт/с. При такой скорости центральный процессор между каждым прие- мом байта данных все еще способен выполнять 32 команды, что недостаточно для сложных вычислений, но вполне приемлемо для простых задач передачи данных. В дополнение к возможностям SPI, реализованным в большинстве микрокон- троллеров PIC в модулях SSP и MSSP (выбор полярности и активного фронта так- тового сигнала, а также работа в режиме ведущего или ведомого устройства), мо- дуль SPI-иптерфейса микроконтроллера PIC24 предоставляет ряд новых возможно- стей, включая: • режим 16-битной передачи;
112 Глава?. Обмен данными • выбор фазы опроса входных данных; • режим покадровой передачи; • управление синхроимпульсами передачи кадров (выбор полярности и активного фронта); • расширенный режим (восьмиуровневые FIFO-буферы приема/передачи). В частности, в ходе операции последовательного чтения и/или страничной за- писи для повышения производительности и увеличения (вдвое) числа циклов, дос- тупных центральному процессору в промежутках между обращениями к буферам SPI, можно использовать режим 16-битной передачи. Однако по-настоящему раз- грузить процессор позволяет расширенный режим (Enhanced Mode) с его восьми- уровневыми FIFO-буферами. До восьми слов (16 байт) можно записывать в буферы SPI или извлекать их оттуда небольшими фрагментами, оставляя в промежутках го- раздо больше времени центральному процессору на обработку данных. Советы и хитрости Если важные данные хранятся во внешней энергонезависимой памяти, то реко- мендуем принять некоторые дополнительные меры безопасности (как аппаратные, так и программные). С точки зрения аппаратуры удостоверьтесь, что: • что рядом с запоминающим устройством смонтирован надлежащий конденса- тор для развязки питающего напряжения; • линия выбора кристалла CS оснащена подтягивающим резистором на 10 кОм во избежание проблем при включении и сбросе микроконтроллера; • на линии SCK также может быть добавлен подтягивающий резистор на 10 кОм во избежание тактирования периферии в ходе периферийного сканирования и других тестовых процедур платы; • фронты сигналов включения и отключения питания микроконтроллера не со- держат шумов и достаточно крутые. Это гарантирует падежный сброса по по- даче питания. В случае необходимости добавьте в схему внешние средства кон- троля напряжения (например устройства серии МСР809). Затем для устранения даже наименее вероятных программных сбоев или слу- чайного срабатывания процедуры записи под воздействием пресловутых магнитных бурь, можно применить ряд программных мер. • Избегайте чтения и особенно — обновления NVM-содержимого сразу же после подачи питания. Выждите несколько миллисекунд, пока стабилизируется пи- тающее напряжение. • Реализуйте программный флаг разрешения записи, который должен устанавли- ваться приложением перед вызовом подпрограммы записи и, возможно, — по- сле проверки определенного условия, специфичного для приложения. • Реализуйте счетчик уровней стека. Каждая функция в стеке вызовов, реализо- ванных в библиотеке, должна увеличивать значение этого счетчика при входе и уменьшать при выходе. Подпрограмма записи не должна вызываться, если счетчик уровней стека не содержит предполагаемого значения. • Сохраняйте две копии каждого существенного фрагмента данных и реализуйте два отдельных вызова подпрограммы записи. Если каждая копия содержит хотя бы простую контрольную сумму, то при ее считывании будет проще выявить и восстановить поврежденные данные.
Упражнения 113 Упражнения 1. Разработайте буферизированные (кольцевые) версии функций чтения и записи. 2. Активизируйте новый режим 16-битной передачи по интерфейсу SPI для уско- рения базовых операций чтения и записи. 3. Некоторые функции в библиотеке содержат блокирующие циклы, которые мо- гут привести к снижению общей производительности приложения. Разработай- те версию библиотеки без блокировки с помощью прерываний от порта SPI. Ссылки • http://www.microchip.com/stellent/idcplg?IdcService= S S_GET_PAGE & nodeId=l4 0 6 & dDocName=en 010003 — воспользуйтесь этой ссылкой или самостоятельно найдите на Web-сайте компании Microchip бесплатное программное средство под названием “Total Endurance Software”, которое позволяет оценить долговечность NVM-устройства в существующих рабочих условиях. Оно дает общее представление о допустимом количестве циклов записи и предполагаемом сроке службы устройства до достижения оп- ределенной частоты возникновения сбоев.
ГЛАВА О Асинхронный обмен данными В этой главе: ► Конфигурирование модуля UART ► Передача и прием данных ► Тестирование подпрограмм последовательного обмена данными * Разработка простой консольной библиотеки ► Тестирование терминала VT100 > Использование последовательного порта в качестве средства отладки ► Матрица X-' •=> ИЙМ* 'Al л,!»', S& *' ; >A ' '' '' - " Тот, кто когда-либо пользовался радиопередатчиками, будь то переносная ра- ция или настоящая стационарная радиостанция, хорошо знает, что подобные пере- говоры коренным образом отличаются от бесед по мобильному телефону. Прежде всего, радиосистемы — полудуплексные. Это означает, что вы не можете говорить, если кто-то уже говорит. Вы вынуждены терпеливо слушать, ожидая своей очереди, а затем высказываете свою мысль максимально кратко, предоставляя другим воз- можность также поучаствовать в разговоре. При этом для предотвращения кон- фликтов и непониманий используется простая система голосовых подтверждений. Именно такая схема используется в авиации, где существует точный протокол: набор правил, предписывающих, кто, что и как должен говорить в каждый отдельно взятый момент времени. У каждого своя роль: диспетчеры, пилоты, работники кон- трольных пунктов и вышек... И все они координировано и эффективно используют один и тот же носитель информации. Эта иллюстрация хорошо описывает многие асинхронные протоколы последо- вательного обмена данными. Некоторые из них — дуплексные, в то время как дру- гие — полудуплексные. Некоторые — многопунктовые, другие — двухточечные, однако все они требуют координирования и строгого следования базовым правилам (стандартам), без которых обмен данным будет невозможен или малоэффективен. План полета В этом уроке мы рассмотрим модули асинхронных последовательных интер- фейсов UART1 и UART2 микроконтроллеров PIC24, а также разработаем базовую консольную библиотеку, которая нам пригодится в будущих проектах для органи- зации взаимодействия с устройствами и отладки.
Предполетный контроль 115 Предполетный контроль В дополнение к обычным программным средствам (оболочка MPLAB, компи- лятор MPLAB СЗО и имитатор MPLAB SIM) в этом уроке потребуется демонстра- ционная плата Explorer 16, внутрисхемный отладчик MPLAB ICD2 и ПК с последо- вательным портом RS232 (или соответствующим адаптером для порта USB). Кроме того, нам понадобится программа имитации терминала. Пользователям операционной системы Microsoft® Windows® будет достаточно приложения Hyper- Terminal, которое запускают по соответствующей команде системного подменю Пуск ► Все программы ► Стандартные к Связь. Полет Интерфейс UART — наверное, самый старый из используемых во встроенных системах. Некоторые из его свойств были продиктованы необходимость совмести- мости с первыми механическими телетайпами. Таким образом, по крайней мере не- которые из задействованных в нем технологий уходят корнями в первую половину прошлого века. С другой стороны, сегодня найти асинхронный последовательный порт в новом компьютере (и особенно — ноутбуке) — задача почти невыполнимая. Последова- тельный порт об'ьявили “пережитком прошлого” и за последние несколько лет мно- гие производители компьютеров под давлением заменили его интерфейсом USB. Тем не менее, невзирая на потерю популярности и явные преимущества технологии USB, асинхронный последовательный интерфейс, благодаря своей чрезвычайной простоте и дешевизне реализации, по-прежнему удерживает прочные позиции в ми- ре встроенных систем. По сегодняшний день применяют четыре основных класса технологий асин- хронной последовательной передачи: • двухточечное соединение RS232, которое часто называют просто “последова- тельным портом”, — используется терминалами, модемами и персональными компьютерами с приемопередатчиками на ±12 В; • многопунктовое последовательное соединение RS485 (EIA-485) — применяется в промышленных приложениях, использует девятиразрядное слово и особые полудуплексные приемопередатчики; • недорогая низковольтная шина LIN — предназначена для некритичных задач автомобилестроения; гребует наличия приемопередатчика UART с возможно- стью автоматического определения скорости передачи; • инфракрасные беспроводные каналы — требуют модуляцию сигнала 38-40 кГц и оптические приемопередатчики. Модули UART микроконтроллеров PIC24 могут поддерживать все четыре клас- са наряду с некоторыми дополнительными возможностями (рис. 8.1). Для демонстрации базовой функциональности периферии UART мы воспользу- емся платой Explorer 16, на которой модуль UART2 соединен с приемопередатчиком RS232 и стандартным гнездом типа D на девять выводов. Он может быть подклю- чен к любому последовательному порту ПК или в отсутствие этого “пережитка прошлого” — к порту USB с помощью специального преобразователя. В обоих слу- чаях с платой Explorer 16 можно обмениваться данными с помощью программы HyperTerminal.
116 Глава 8. Асинхронный обмен данными Рис. 8.1. Упрощенная структурная схема модуля UART На первом этапе определим следующие параметры передачи: • скорость обмена; • количество бит данных; • наличие бита контроля четности; • количество стоп-битов; • протокол обмена с подтверждением. Для нашего примера мы воспользуемся быстродействующей и удобной конфи- гурацией “115200, 8, N, 1, CTS/RTS”, что подразумевает: • 115 200 бод; • восемь бит данных; • отсутствие бита контроля четности; • один стоп-бит; • обмен с подтверждением с использованием линий CTS и RTS. Конфигурирование модуля UART С помощью контрольного списка “Настройка нового проекта” создайте проект под именем Serial и новый файл исходного кода serial. с. Для начала мы до- бавим несколько полезных определений для организации ввода-вывода с помощью линий подтверждения передачи: /* ★★ Асинхронный последовательный обмен данными ** Код демонстрации UART2 по интерфейсу RS232 */ #include <p24fj128ga010.h> // Определения для Explorerl6 # define CTS _RF12 // Clear To Send, вход, аппаратное квитирование # define RTS _RF13 // Request To Send, выход, аппаратное квитирование # define TRTSTRISFbits.TRISF13 // TRIS-управление выводом RTS Аппаратное квитирование особенно важно при обмене данными с помощью терминала HyperTerminal, поскольку Windows — многозадачная операционная сис- тема, и приложения иногда работают с большими задержками, что может привести к потере данных. Для опроса готовности терминала к приему нового символа мы за- действуем один вывод RF12 платы Explorer 16, сконфигурированный как вход (сиг-
Полет 117 нал “Clear То Send”, CTS). Для уведомления терминала о том, что приложение гото- во принять символ, мы используем вывод RF13, сконфигурированный как выход (сигнал “Request То Send”, RTS). * Для установки скорости обмена необходимо задействовать генератор BREG2: 16-разрядный счетчик, работающий от схемы тактирования периферии. В специфи- кации устройства сказано, что в нормальном режиме работы (BREGH = 0) коэффи- циент делителя составляет 1:16 в отличие от скоростного режима (BREGH = 1) с ко- эффициентом 1:4. С помощью простой формулы, указанной в спецификации, мы можем вычислить идеальную настройку для нашей конфигурации: BREG2 = (Fosc / 8 / baudrate) - 1 ; Для BREGH=1 В пашем случае мы получим следующее выражение: BREG2 = (Fosc / 8 / 115,200) - 1 = 33.7,' где Fosc = 32 МГц. Для того чтобы определить, как образом лучше всего округлить результат (нам требуется 16-разрядное целое), мы воспользуемся обратной формулой, вычислим фактическую скорость обмена и выясним погрешность, выраженную в процентах: Ошибка = ((Fosc/ 8 / (BREG2 +1)) - Скорость) / Скорость % Округлив до значения 34, мы получим фактическую скорость 114 285 бод с вполне приемлемой погрешностью 0,7%. Для значения 33 получаем 117 647 бод и погрешность 2,1%, которая превышает допустимые ±2% для стандартного порта RS232. Таким образом, определяем константу BRATE как #define BRATE 34 // 115200 бод (BREGH=1) Еще две константы помогут нам определить значения инициализации для глав- ных управляющих регистров модуля UART2: U2MODE и U2STA (рис. 8.2 и рис. 8.3). Старший байт: R/W-0 U-0 R/W-0 R/W-0 R/W-0 U-0 R/W-0 R/W-0 UARTEN у USIDL IREN RTSMD — UEN1 UENO разряд 15 разряд 8 Младший байт: R/W-0 НС R/W-0 R/W-0 HC R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 WAKE LPBACK ABAUD RXINV BREGH PDSEL1 | PDSELO STSEL разряд 7 разряд 0 Рис. 8.2. Управляющие регистры UxMODE Значение инициализации для регистра U2MODE включает разряд BREGH, коли- чество стоп-битов и настройку бита контроля четности. #define U_ENABLE 0x8008 // Активизация UART, BREGH=1, один стоп-бит, // без контроля четности Инициализация для регистра U2STA заключается в активизации передатчика и сбросе флагов ошибки: #define U ТХ 0x0400 // Активизация передачи, сброс всех флагов
118 Глава 8. Асинхронный обмен данными Старший байт: R/W-0 R/W-0 R/W-0 и-о R/W-0 НС R/W-0 R-0 R-1_ UTXISEL1 UTXINV | UTXISEL0 | — | UTXBRK | UTXEN UTXBF TRMT разряд 15 разряд 8 Младший байт: R/W-0 R/W-0 R/W-0 R-1 R-0 R-0 R/C-0 R-0 URXISEL1 | URXISEL0 | ADDEN RIDLE PERR FERR OERR | URXDA разряд 7 разряд 0 Рис. 8.3. Управляющие регистры UxSTA Задействовав определенные выше константы, мы создадим новую функцию для инициализации управляющего регистра модуля UART2, генератора скорости пере- дачи и выводов, используемых для обмена с квитированием: void initU2( void) ( U2BRG = BRATE; U2MODE = U_ENABLE; // Инициализируем генератор скорости передачи // Инициализируем модуль UART U2STA = U_TX; // Активизируем передатчик TRTS = 0; // Делаем вывод RTS выходом RTS = 1; // Устанавливаем вывод RTS в состояние по // initU2 // умолчанию ("не готов") Передача и прием данных Передача символа через последовательный порт происходит в зри этапа: 1. Проверка готовности терминала (на ПК запущена программа HyperTerminal) путем проверки линии CTS. До тех пор, пока на этой линии высокий (неактив- ный) уровень сигнала, мы ожидаем. 2. Проверка того, что UART не занят передачей предыдущих данных. Модули UART микроконтроллеров PIC24 оснащены четырехуровневым FIFO-буфером, поэтому все, что нам нужно сделать, — это дождаться освобождения хотя бы верхнего уровня (т.е. сброса флага UTXBF, указывающего на полное заполне- ние буфера передачи). 3. Передача нового символа в выходной буфер модуля UART. Все это можно реализовать в одной небольшой функции: int putU2(int с) { while (CTS); // Ожидаем "О" на линии CTS while (U2STAbits.UTXBF); // Ожидаем освобождения буфера передачи U2TXREG = с; return с; } // putU2 Для приема символа из последовательного порта используется процедура, по- добная рассмотренной выше. 1. Известить терминал о готовности к приему путем выдачи сигнала RTS (актив- ный низкий уровень). 2. Дождаться поступления каких-либо данных во входной буфер, на что укажет установка флага URXDA.
Полет 119 3. Извлечь символ из входного буфера. И опять таки, все это можно реализовать в одной простой функции: char getU2(void) { RTS =0; // Выдаем запрос на передачу while (!U2STAbits.URXDA); // Ожидаем поступления нового символа return U2RXREG; // Считываем символ из входного буфера RTS = 1; }// getU2 Тестирование подпрограмм последовательного обмена данными Для того чтобы протестировать подпрограммы управления последовательным портом, мы можем написать небольшую программу, которая инициализирует по- следовательный порт, передает приглашение и позволяет нам ввести в терминале с клавиатуры строку, отображая эхом каждый введенный символ на экране ПК: main () ( char с; // 1. Инициализация последовательного порта UART2 initU2 () ; // 2. Приглашение putU2('>'); // 3. Главный цикл while (1) { // 3.1 Ожидаем символ с = getU2(); // 3.2 Сразу же возвращаем символ putU2(с); } // Главный цикл }// main Выполните следующие действия. 1. Скомпонуйте проект, после чего с помощью стандартного контрольного списка активизируйте отладчик ICD2 и запрограммируйте плату Explorer 16. 2. Подключите кабель последовательной передачи к ПК (напрямую или через USB-преобразователь) и настройте программу HyperTerminal на работу с пара- метрами “115200, п, 8, 1, RTS/CTS” через доступный СОМ-порт. 3. Нажмите в окне HyperTerminal кнопку Вызов, чтобы активизировать имитацию терминала. 4. Выберите команду меню Debugger ► Run, чтобы запустить на выполнение де- монстрационную программу. ПРИМЕЧАНИЕ ................................................... .... При работе с UART рекомендую не использовать пошаговый режим, точки прерыва- ния и команду Run То Cursor! За более подробными пояснениями обращайтесь к разделу “Советы и хитрости” в конце этой главы.
120 Глава 8. Асинхронный обмен данными Обратите также внимание, что программа HyperTerminal может быть уже на- строена на эхо-возврат каждого переданного символа. В таком случае результат бу- дет в буквальном смысле слова удвоен. Для деактивации этой функции нажмите кнопку Отключить и выберите команду меню Файл ► Свойства. В диалоговом окне свойств перейдите на вкладку Параметры (рис. 8.4). Сейчас — самое время настро- ить пару параметров, которые пригодятся нам в оставшейся части урока. Выполните следующие действия. 1. Выберите режим эмуляции терминала VT100, чтобы стало доступным количе- ство команд (это даст нам больше контроля над позицией курсора на экране терминала). 2. Для завершения конфигурирования нажмите кнопку Параметры ASCII. В поя- вившемся на экране диалоговом окне (рис. 8.5) сбросьте флажок Отображать введенные символы на экране. 3. Также установите флажок Дополнять символы возврата каретки (CR) переводами строк (LF). Это гарантирует, что каждый раз при получении ASCII-символа окончания абзаца (’ \г’) в строку будет автоматически вставляться дополни- тельный символ перехода на новую строку (’ \п ’). Рис. 8.4. Диалоговое окно Свойства программы HyperTerminal ...........55 Отправка да»«1ых в формате ASCII....... Г III ' UH Г mw сямсс-пы t< t и Зододжкадотстр-ж: [(J не [(J мс ....7; •••;"• ". •;;.... .......- .. s.. . %. П ряем данных в Формат e ASCI ......... Г' Длполмть символы ооэпрэта каретки|CRj елреподъщ стглк ILF) Г” Цреобразовывать входящие данные в 7-разрядный код ASCII I/ Пйрениси1В. строки, превышающие ширину тормола Рис. 8.5. Диалоговое окно Параметры ASCII Разработка простой консольной библиотеки Для того чтобы преобразовать наш демонстрационный проект в полноценную терминальную консольную библиотеку, которая может пригодиться в будущих про- ектах, нам достаточно только двух дополнительных функций: для вывода целой (за- канчивающейся нулевым символом) строки и для ввода полной текстовой строки. Вывод строки, как вы уже догадались, — задача несложная: int putsU2 (char *s) { while (*s) // Цикл до тех пор, пока *s == '\0' (конец строки) putU2(*s++); // Передаем символ и указатель на следующий символ } // putsU2 Это простой цикл, постоянно вызывающий функцию putU2 для посимвольной передачи строки в последовательный порт.
Полет 121 Чтение текстовой строки из терминала (консоли) в буфер может быть реализо- ван не менее просто, однако мы несколько усложним задачу, реализовав проверку переполнения буфера (на случай, если пользователь введет слишком длинную стро- ку), а также — преобразование символа возврата каретки в конце строки в коррект- ный символ ’ \ 0 ’. char *getsnU2(char *s, ( char *p = s; do { *s = getU2 (); if (*s == '\r') break; s+ + ; len--; } while (len > 1 ) ; *s = ’\0’; return p; } //getsnU2 int len) // Копирует указатель на буфер // Ожидаем новый символ // Конец строки - конец цикла // Увеличиваем на единицу указатель на буфер // До тех пор, пока буфер не заполнится // Добавляем к строке нулевой символ // Возвращаем указатель на буфер На практике эту функцию в представленном виде использовать будет тяжелова- то ввиду отсутствия эхо-вывода введенного текста. Кроме того, у пользователя нет' права на ошибку. Достаточно малейшей опечатки, и всю строку придется вводить заново. Если вы похожи па меня, то постоянно допускаете массу опечаток, и наиболее востребованная клавиша па вашей клавиатуре — это <Backspace>. По этой причине улучшенная версия функции getsnU2 должна включать эхо-вывод символов и поддерживать хотя бы клавишу <Backspace> для исправления ошибок в тексте. Для реализации подобной задачи достаточно буквально двух строк кода. Эхо-вывод добавляется после приема каждого символа, а клавиша <Backspace> (ASCII-код 0x8) декодируется для перемещения указателя на буфер на один символ назад (до тех пор, пока мы не находимся в начале строки). Кроме того, потребуется вывести особую последовательность символов для визуального удаления предыду- щего символа с экрана терминала. char *getsnU2(char *s, int len) ( char *p = s; // Копируем указатель на буфер int cc = 0; // do { *s = getU2 (); // putU2(*s); // if ( (*s == BACKSPACE) { putU2(' ’); // putU2(BACKSPACE); len++; s —; // continue; } if (*s == '\n') // Счетчик символов Ожидаем новый символ Эхо-выдача символа && (S > Р) ) Затираем последний символ Сдвигаем назад указатель Перевод строки - игнорируем continue; if (*s == '\r' ) // Конец строки - завершаем цикл break; s++; // Увеличиваем указатель на буфер len--; } while (len > 1 ) ; // До тех пор, пока буфер не заполнен
122 Глава 8. Асинхронный обмен данными *s = '\0 '; // Добавляем к строке нулевой символ return р; // Возвращаем указатель на буфер } // getsnU2 Поместите все функции к отдельный файл с именем conU2 .с и создайте не- большой заголовочный файл conU2. h, чтобы определить функции (прототипы) и константы, доступные для внешнего мира: 7*................................... “................................. ** CONU2.h ** console I/O library for Explorerl6 board */ // Определения для ввода-вывода через Explorer16 #define CTS _RF12 // Clear To Send, вход, аппаратное квитирование tfdefine RTS _RF13 // Request To Send, выход, аппаратное квитирование #define BACKSPACE 0x8 // ASCII-код клавиши <Backspace> // Инициализация последовательного порта (UART2,115200032MHz,8,N,1,CTS/RTS) void initU2(void); // Передача символа в последовательный порт int putU2(int с); // Ожидаем поступления в последовательнй порт нового символа char getU2(void); // Передача в последовательный порт строки с завершающим нулевым символом int putsU2(char *s); // Принимаем строку в буфер char *getsnU2 (char *s, int n) ;• ©Файлы conU2.c и conU2.h находятся также на прилагаемом к книге компакт-диске в папке Проекты\08 - СОМ. Тестирование терминала VT100 Поскольку мы активизировали режим имитации терминала VT100 (см. выше настройки программы HyperTerminal), в нашем распоряжении теперь есть несколь- ко команд для управления экраном терминала и позицией курсора: • clrscr — очистка экрана терминала; • home — перемещение курсора в левый верхний угол экрана. Эти команды реализуются путем передачи так называемых “управляющих по- следовательностей” или, как их еще называют, “escape-последовательностей” (оп- ределены стандартами ЕСМА-48, ISO/IEC 6429 и ANSI Х3.64). Все они начинаются с символов “ESC” (ASCII-код 0x1b) и “[” (открывающая квадратная скобка): // Полезный макрос для имитации терминала VT100 #define clrscr () putsU2("\xlb[2J") #define home() putsU2(”\xlb[1,1H") Для того чтобы протестировать консольную библиотеку, мы теперь можем на- писать небольшую программу, которая будет: 1. Инициализировать последовательный порт. 2. Очищать экран терминала.
Полет 123 3. Передавать сообщение приветствия. 4. Передавать символ приглашения. 5. Считывать полную строку текста. 6. Выводить текст в новой строке. Сохраните следующий код в новом файле с именем CONU2test. с. ©Файлы conU2test.c находится также на прилагаемом к книге компакт-диске в папке ПроектыХ 08 - СОМ. /* ** Тест CONU2 ** Демонстрация асинхронного обмена данными по интерфейсу RS232 UART2 */ #include <p24fj128ga010.h> #include "conU2.h" ^define BUF_SIZE 128 main () { char s[BUF_SIZE]; // 1. Инициализируем последовательный порт консоли initU2 () ; // 2. Текстовое приглашение clrscr () ; home(); putsU2("Learn to fly with the PIC24!"); // 3. Главный цикл while (1) { putU2 ( ">"); // Приглашение // 3.1. Считываем полную текстовую строку getsnU2(s, BUF_SIZE); // 3.2. Передаем строку в последовательный порт putsU2(s); // 3.3. Передаем символ возврата каретки putU2('\г'); } // Главный цикл }// main Выполните следующие действия. 1. Создайте новый проект, ориентируясь по соответствующему контрольному списку, добавьте в него файлы conU2 . h, conU2 . с и conU2test. с и выпол- ните компоновку. 2. Подключите отладчик ICD2 с помощью соответствующего контрольного спи- ска и запрограммируйте плату Explorer 16. 3. Проверьте возможности только что созданной консольной библиотеки.
124 Глава 8. Асинхронный обмен данными Использование последовательного порта в качестве средства отладки В лице небольшой библиотеки, реализующей обмен данными с консолью через последовательный порт, мы обрели мощное средство отладки. Теперь мы можем с помощью функций вывода на экран терминала отслеживать содержимое критиче- ски важных переменных и другой диагностической информации. Выходную информацию можно легко отформатировать и представить в наибо- лее удобном для чтения виде. Для установки параметров, Помогающих оттестиро- вать код, или простой приостановки выполнения программы по требованию для чтения диагностических данных можно добавить специальные функции ввода. По- добные средства отладки — одни из самых старых и эффективно используются со времен изобретения первого компьютера. Матрица Для того чтобы завершить этот урок на развлекательной поте, мы разработаем демонстрационный проект под названием “Матрица”. Его цель — проверить быст- родействие последовательного порта и имитации терминала па ПК путем передачи на терминал массивного текста. Единственная проблема в данном случае заключа- ется в том, что у нас (пока что) пет большого запоминающего устройства, из кото- рого можно было бы извлечь какое-либо осмысленное содержимое и передать его на терминал. Самый лучший выход из сложившейся ситуации — сформировать текст с помощью генератора псевдослучайных чисел. Подходящую функцию rand (), возвращающую положительное целое число в диапазоне от 0 до MAX_RAND (константа, определенная в файле limits.h, которая в реализации компилятора MPLAB СЗО равна 32 767), предоставляет библиотека s tdlib. h. С помощью оператора, дающего остаток от целочисленного деления, значение, возвращаемое упомянутой функцией, можно уменьшить до любого целочисленного диапазона и получить только определенный набор печатаемых символов из таблицы ASCII. Например, следующий оператор, дает только символы, коды которых лежат в диапазоне от 33 до 127: putU2 (33 + (rand () % 94) ); Для формирования более привлекательного вывода (особенно в сопоставлении с кинофильмом “Матрица”) мы представим хаотическое содержимое не в строках, а в столбцах. С помощью генератор псевдослучайных чисел мы будем изменять со- держимое и “длину” каждого столбца, постоянно обновляя экран. Создайте файл Matrix . с и введите в него следующий код. еФайл Matrix.с находится также на прилагаемом к книге компакт-диске в папке Проекты\08 - сом. ”7*...................................................................... ** Матрица */ #include <p24fj128ga010.h> #include "CONU2.h" #include <stdlib.h> #define COL 40
Полет 125 #define ROW 23 ttdefine DELAY 3000 main () { int v[40]; // Вектор, содержащий длину каждой строки int i,j,k; // 1. Инициализация T1CON = 0x8030; // TMR1 включен, коэффициент предделителя 256, Tcy/2 initU2(); // Инициализируем консоль (115200, 8, N, 1, CTS/RTS) clrscrO; // Очищаем терминал (имитация VT100) getU2(); // Ожидаем один символ, чтобы создать случайную // последовательность srand(TMR1); // 2. Инициализируем длину каждого столбца for(j=0; j<COL; j++) v[j] = rand() % ROW; // 3. Главный цикл while(1) ( home(); // 3.1. Обновляем экран хаотически выбранными столбцами for(i=0; i<ROW; i++) { // Обновляем построчно for(j=0; j<COL; j++) ( // Выводим случайный символ до длины каждого столбца if (i < v[j]) putU2 (33 + (rand() % 94) ) ; else putU2(’ '); putu2(' '); } // for j per () ; } // for i // 3.2. Случайным образом изменяем длину каждого столбца for(j=0; j<COL; j++) ( switch (rand () % 3) { case 0: // Увеличиваем длину v[ j]++; if (v[ j] > ROW) v[j] = ROW; break; case 1: // Уменьшаем длину v[ j ] — ; if (v[j] < 1) v[j] = 1; break; default: // Без изменений break;
126 Глава 8. Асинхронный обмен данными } // switch } // for } // Главный цикл } // main Забудьте о целесообразности — просто наблюдайте за мельканием кодов и на- слаждайтесь. Все равно они мелькают слишком быстро, чтобы можно было что- либо прочитать. По сути, внутри цикла в пункте 3.1 необходимо добавить неболь- шую задержку, чтобы вывод был более приятен для глаз: // 3.1.1. Задержка для замедления обновления экрана TMR1 = 0; while (TMR1 < DELAY); Разбор полета В этом уроке мы разработали небольшую консольную библиотеку с функциями ввода-вывода, по ходу рассмотрев функциональность модуля UART для взаимодей- ствия с последовательным портом RS232. Плата Explorer 16 была подключена к тер- миналу (имитированному) VT100 (Windows-npoipaMMa HyperTerminal). Разработан- ная библиотека еще пригодится нам в последующих главах как средство отладки и пользовательский интерфейс для более сложных проектов. Заметки для экспертов по С Я уверен, что вы уже подумываете о том, чтобы воспользоваться для прямого вывода в модуль UART более развитыми функциями, определенными в библиотеке st di о. h (например, printf ). Это вполне возможно. Достаточно заменить только одну из ключевых библиотечных функций: write.. .......... 7*................... ** write.с ** Заменяет стандартную функцию write ftinclude <p24fj128ga010.h> tfinclude <stdio.h> #include "conu2.h" int write(int handle, void *buffer, unsigned int len) ( int i, *p; const char *pf; switch (handle) ( case 0: // stdin case 1: // stdout case 2: // stderr for (i = len; i; —i) putU2(*(char*)buffer); break; default: break; } // switch
Заметки для экспертов по PIC 127 return(len); } // write Сохраните этот код в файле write. с в проектной папке и добавьте его в спи- сок исходных файлов проекта. С этогр момент компоновщик будет подключать данную функцию, и любой вызов одной из функций библиотеки stdio.h, реали- зующих вывод в стандартные потоки (stdin, stdout, stderr), перенаправляется на UART2. Учтите, что вы по-прежнему ответственны за надлежащую инициализацию мо- дуля UART, а в проект должен быть включен файл conu2 . с. Заметки для экспертов по PIC Рано или поздно всем разработчикам встроенных систем придется смириться с использованием шины USB. Хотя пока что переходник от последовательного к USB-порту — вполне разумное решение, наступит момент, когда возникнет по- вальный спрос на разработки, ориентированные на прекрасную производительность и совместимость шины USB. В некоторые модели восьмиразрядных микроконтрол- леров PIC в качестве стандартного интерфейса уже встроен модуль USB Serial Inter- face Engine (SIE). Компания Microchip предлагает большой набор программ под USB, включая драйверы и готовые решения для самых разнообразных прикладных областей. Одно из таких решений под названием Communication Device Class (CDC) делает USB-подключение абсолютно “прозрачным” для ПК. Что важнее всего, оно не требует создания и/или установки каких-либо специальных Windows-драйверов. При разработке приложения на языке С программист даже не увидит разницы — разве что отсутствие необходимости определять специальные коммуникационные параметры. При работе с шиной USB не нужно устанавливать скорость обмена дан- ными, выполнять контроль по четности, выбирать помер порта... И все это — при гораздо более высоком быстродействии. Советы и хитрости Как было упомяпуго в начале этого урока, для обмена данными с программой HyperTerminal не рекомендуется использовать пошаговое выполнение подпро- грамм, использующих модуль UART. Это, скорее всего, приведет только к тому, что HyperTerminal начнет сбоить или просто без каких-либо явных причин “зависнет”, игнорируя любые входные данные. Для того чтобы понять суть этой проблемы, не- обходимо глубже понимать, каким образом работает внугрисхемный отладчик MPLAB ICD2. После выполнения каждой команды в пошаговом режиме или при достижении точки прерывания отладчик ICD2 не только приостанавливает центральный процес- сор, но и “замораживает” всю периферию, причем — намертво. В результате через цифровые вены модулей не протекает ни единого тактового импульса. Когда это случатся с приемопередатчиком UART в тот момент, когда он выполняет передачу, последовательный выход ТХ фиксируется в его текущем состоянии. Если при этом из сдвигового регистра выдается бит (и особенно если он содержит логическую единицу), то линия ТХ сразу же замирает в состоянии “паузы” (низкий уровень сиг- нала). На другом конце программа HyperTerminal распознает устойчивое состояние “паузы” и интерпретирует его как сбой па линии, предположив, что соединение по-
128 Глава 8. Асинхронный обмен данными теряно. В результате произойдет отключении линии, однако программа Hyper- Terminal, будучи довольно примитивной, не позаботится о том, чтобы известить пользователя о происходящем. Ни звукового сигнала, пи сообщения об ошибке — ничего! Впрочем, понимая сугь проблемы, решить ее не составит труда. После запуска программы с ICD2 необходимо просто не забыть перед нажатием в HyperTerminal кнопки Вызов нажать кнопку Отключить. После этого процесс обмена данными пойдет своим чередом. Упражнения Для минимизации влияния на ход выполнения программы (и отладку) создайте консольную библиотеку с буферизированным вводом-выводом (с использованием прерываний). Ссылки • http://en.wikipedia.org/wiki/ANSI_escape_code — ссылка па полную таблицу ANSI управляющих кодов, используемых при имитации VT100 в HyperTerminal.
ГЛАВА 9 Стеклянное счастье В этой главе: ► Совместимость с контроллером HD44780 Порт РМР ► Конфигурирование порта РМР для управления модулем ЖК-дисплея ► Небольшая библиотека функций для доступа к ЖК-дисплею Расширенное управление ЖК-дисплеем В былые дни кабина любого самолета — от наименьшего одномоторного “Cessna” до ультразвукового “Concord” — была оснащена большими круглыми приборами, напоминающими манометры. За свою вездесущность эти шесть основ- ных приборов, всегда размещаемых в одном и том же порядке, получили ласковое прозвище “шесть банок”. Но попытайтесь в следующий раз, когда будете куда-то лететь, заглянуть в кабину пилота. Конечно же, вы по-прежнему увидите множество разных ручек и переключателей, однако “шести банок” па передней напели среди них уже не будет. Им па замену пришел большой гладкий кусок стекла (или два та- ких куска). Именно так пилоты й называют эту революционную подмену: “склян- ки”, — хотя за ней скрывается больше кремния, чем они могут предполагать. Так, в кабинах самолетов за последние несколько лет произошел настоящий цифровой переворот. Для того чтобы втиснуть максимальный объем информации в этот очень про- стой, интуитивно понятный и приятный глазу интерфейс, по другую сторону стекла трудится множество мощных процессоров. Движущей силой этой инновации стала технология GPS (Global Positioning System — глобальная система навигации и опре- деления положения), и все производители самолетов сегодня в каждой очередной модели добавляют несколько новых стеклянных штучек. Вообще, бытует мнение, что недавний бум в продажах самолетов, ставший толчком к развитию всей отрасли авиастроения в целом, был вызван именно восторгом пилотов от новой “остеклен- ной” кабины. К сожалению, вам, как начинающему пилоту, на своих первых занятиях летать на подобных самолетах не придется. Для того чтобы современные технологии дос- тигли авиапарка летных школ, потребуется определенное время, однако рано или поздно это обязательно случится. Стеклянное счастье уже не за горами. В мире встроенных систем также повсеместно используется стекло в виде ЖК- дисплеев, так что рассмотрим основы ЖК-интерфейсов.
130 Глава 9. Стеклянное счастье План полета В этом уроке мы научимся взаимодействовать с небольшим и недорогим моду- лем ЖК-дисплея. Данный проект — подходящая возможность изучить и воспользо- ваться ведущим параллельным портом РМР (Parallel Master Port) — новым, гибким интерфейсом микроконтроллеров PIC24. Предполетный контроль Кроме обычных программных средств (интегрированная среда MPLAB, компи- лятор MPLAB СЗО и имитатор MPLAB SIM), в этом уроке нам потребуется только демонстрационная плата Explorer 16 и внутрисхемный отладчик MPLAB ICD2. Полет На плате Explored 6 можно разместить модуль точечно-матричного, буквенно- цифрового ЖК-индикатора одного из трех типов или один тип графического ЖК- дисплея. По умолчанию плата поставляется с простым буквенно-цифровым ЖК- модулем “2x16” с питанием 3 В (Tianma TM162JCAWG1), совместимым со стан- дартным контроллером HD44780. Он представляет собой полнофункциональную систему индикации, состоящую из стеклянной ЖК-панели, блока мультиплексиро- вания столбцов и строк, схемы питания и микроконтроллера. Сборка выполнена по технологии COG (Chip On Glass — кристалл на стекле). Благодаря высокой степени интеграции, схема управления точечно-матричным индикатором была чрезвычайно упрощена. Вместо сотен выводов, необходимых драйверам столбцов и строк для прямого контроля за каждым элементом матрицы, мы можем взаимодействовать с модулем через простую восьмиразрядную параллельную шину с помощью всего лишь одиннадцати выводов (рис. 9.1). 4-33V LCD1 □05 DS2 DU3 ОЭ5 □аа R4-4 ЮК ---(RE1/PMD1) J---(Й£27р"М0^ R+3S 1 .ЗК| RE4/PMD4, 1 rEs/pmd's""} RE6/PMD6.J rh/mhj R915/PMAQ --------|VE£ [-a. +3.3V [- RS Vo Va ~CNQ Puc. 9.1. Типичное распределение выводов буквенно-цифрового ЖК-модуля В случае с буквенно-цифровыми модулями мы можем напрямую помещать ко- ды ASCII-символов в RAM-буфер ЖК-модуля (память типа DDRAM). Результи- рующее изображение формируется встроенным генератором (таблицей) с помощью матрицы элементов размерностью 5x7, представляющей каждый символ. Такая таб- лица обычно содержит расширенный набор символов ASCII (рис. 9.2). Это означает, что в нее можно поместить небольшой набор нелатинских букв и часто используе- мых знаков. В то время как таблица генератора символов в большинстве случаев реализована в ПЗУ контроллера дисплея, различные модули позволяют расширить
Полет 131 набор символов (в некоторых моделях — до восьми) путем доступа к небольшому внутреннему буферу типа CGRAM. Char, code Рис. 9.2. Таблица генератора символов, используемая контроллерами ЖК-индикаторов, совместимыми с HD44780 Совместимость с контроллером HD44780 Как было упомянуто выше, ЖК-модуль “2x16”, использованный на плате Ex- plorer^, — один из огромного множества доступных па рынке вариантов ЖК-дисп- леев в конфигурациях от одной до четырех строк из 8, 16, 20, 32 или 40 символов каждая, совместимых с оригинальным чипсетом HD44780, который сегодня счита- ется промышленным стандартом. Совместимость с HD44780 подразумевает, что встроенный контроллер содер- жит только два раздельна адресуемых регистра: один — для ASCII-данных, а вто- рой — для команд. При этом для настройки дисплея и управления им можно ис- пользовать стандартный набор команд (табл. 9.1). Использованные внутри команд HD44780 биты описаны в табл. 9.2. Благодаря такой стандартизации, любой код, разработанный для управления ЖК-индикатором на плате Explorer 16, можно без всякой коррекции использовать с любым другим НО44780-совместимым ЖК-модулем.
132 Глава 9. Стеклянное счастье Таблица 9.1. Набор команд чипсета HD44780 Команда Код Описание Время выпол- нения RS RZW DB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0 Очистка дисплея 0 0 0 0 0 0 0 0 0 1 Очищает дисплей и перево- дит курсор в исходную пози- цию (адрес 0) 1,64 мс Курсор — в исходную позицию 0 0 0 0 0 0 0 0 1 ★ Возвращает в исходную пози- цию курсор и индикацию. Со- держимое памяти DDRAM ос- тается неизменным 1,64 мс Начать ус- тановку режима 0 0 0 0 0 0 . 0 1 IZD S Задает направление смеще- ния курсора (IZD) и сдвиг ин- дикации (S). Эти операции выполняются в ходе чтения^ записи данных 40 мкс Контроль включе- ния/отк- лючения индикации 0 0 0 0 0 0 1 D С в Задает включение/отклю- чение всего дисплея (D), BKHioHeHHeZoTKHioHCHne курсо- ра (С) и мерцание символа курсора (В) 40 мкс Сдвиг кур- сора/ин- дикации 0 0 0 0 0 1 SZC RZL ★ ★ Устанавливает смещение курсора или сдвиг индикации (SZC) или направление сдвига (RZL). Содержимое памяти DDRAM остается неизменным 40 мкс Установка функции 0 0 0 0 1 DL N F * * Устанавливает длину данных (DL), номер строки индикато- ра (N) и шрифт сим[’_.юв (F) 40 мкс Установка адреса CGRAM 0 0 0 1 Адрес памяти CGRAM Устанавливает адрес памяти CGRAM, после чего переда- ются и принимаются данные 40 мкс Установка адреса DDRAM 0 0 1 Адрес памяти DDRAM Устанавливает адрес памяти DDRAM, после чего переда- ются и принимаются данные 40 мкс Чтение флага за- нятости и счетчика адреса 0 1 BF Адрес памяти CGRAMZDDRAM Считывает флаг занятости (BF), указывающий на актив- ность внутренней операции, и содержимое счетчика адре- са памяти CGRAMZDDRAM (в зависимости от предыду- щей команды) 40 мкс Запись в CGRAMZ DDRAM 1 0 Данные для записи Записывает данные в память CGRAM или DDRAM 40 мкс Чтение из CGRAMZ DDRAM 1 1 Считанные данные Считывает данные из памяти CGRAM или DDRAM 40 мкс
Полет 133 Таблица 9.2. Биты из команд HD44780 Название бита Установка / состояние I/D 0 — уменьшение позиции курсора 0 — увеличение позиции курсора S 0 — отсутствие сдвига индикации 1 — сдвиг индикации D 0 — отключение дисплея 1 — включение дисплея С 0 — отключение курсора 1 — включение курсора В 0 — отключение мерцания курсора 1 — активизация мерцания курсора S/C 0 — смещение курсора 1 — сдвиг индикации R/L 0 — сдвиг влево 1 — сдвиг вправо DL 0 — четырехразрядный интерфейс 1 — восьмиразрядный интерфейс N 0 — цикл 1/8 или 1/11 (одна строка) 1 — цикл 1/16 (две строки) F 0 — матрица 5x7 1 — матрица 5x10 BF 0 — разрешение приема команды 1 — активна внутренняя операция Порт РМР Восьмиразрядная шина, совместно используемая всеми модулями индикации, чрезвычайно проста. Кроме восьми двунаправленных линий данных (с целью эко- номии выводов их количество можно уменьшить до четырех с помощью специаль- ного режима), она содержит: • линия разрешения стробирования (Е); __ • линия выбора режима записи/чтепия (R/W); • линия адреса (RS) для выбора регистра. Для реализации командной последовательности каждой шипы не состарило бы труда управлять вручную одиннадцатью выводами с помощью регистров порта Е и D, однако вместо это мы воспользуемся возможностью исследовать возможности новой периферии архитектуры PIC24. Речь идет о ведущем параллельном порте РМР (Parallel Master Port). Разработчики семейства микроконтроллеров PIC24 соз- дали этот новый адресуемый параллельный порт для автоматизации и ускорения доступа к большому спектру внешних устройств общего назначения, включая ана- лого-цифровые преобразователи, буферы оперативной памяти, совместимые с ши- ной ISA интерфейсы, модули ЖК-дисплеев и даже жесткие диски и карты Compact- Можно сказать, что РМР — это гибкая шина ввода-вывода, добавленная в архи- тектуру PIC24, которая не мешает ни 24-разрядной шине памяти программ, ни 16- разрядной шине памяти данных (и не замедляет их работу). Порт РМР предлагает: • восьми- или 16-разрядный двунаправленный тракт данных; • до 64 Кбайт адресного пространства (16 адресных линий); • шесть дополнительных линий стробирования/управления: о разрешения; о фиксации адреса; о чтения; о записи; о две линии выбора кристалла. Порт РМР также можно сконфигурировать для работы в режиме ведомого, что- бы подключиться в качестве адресуемого периферийного устройства к более круп- ной микропроцессорной или микроконтроллерной системе.
134 Глава 9. Стеклянное счастье Командные последовательности чтения из шины и записи в нее — полностью программируемые, поэтому можно не только сконфигурировать под целевую шину полярность и набор управляющих сигналов, но и точно настроить временные харак- теристики в соответствии с быстродействием периферийных устройств. Конфигурирование порта РМР для управления модулем ЖК-дисплея Как и в случае со всеми остальными периферийными модулями микроконтрол- лера PIC24, за конфигурирование порта РМР отвечает ряд управляющих регистров. Первый из них — PMCON, содержащий знакомый набор разрядов, типичный для всех регистров xxCON. Старший байт: R/W-0 U-0 R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 PMPEN — PSIDL | ADRMUX1 | ADRMUX0 | PTBEEN | PTWREN | PTRDEN разряд. 15 разряд 8 Младший байт: R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 R/W-0 CSF1 CSFO I ALP | CS2P CS1P ВЕР WRSP RDSP разряд 7 разряд 0 Рис. 9.3. Управляющий регистр pmcon Однако перечень управляющих регистров, которые нам придется инициализи- ровать, на этот раз — несколько длиннее и включает: PMMODE, PMADDR, PMSTAT, PMPEN и (не обязательно) PADCFG1. Они предоставляют множество разнообразных возможностей и требуют к себе самого пристального внимания. Впрочем, мы не бу- дем каждый из них рассматривать подробно, а остановимся только на ключевых функциях, требуемых конкретно для взаимодействия с ЖК-модулем: • активизация порта РМР; • выбор полностью демультиплексированного интерфейса (использование раз- дельных линий данных и адреса); • активизация разрешения сигнала стробирования (RD4); • разрешение сигнала чтения (RD5); • активизация высокого активного уровня стробирующих импульсов; • установка высокого активного уровня сигнала чтения и низкого активного уровня сигнала записи; • активизация режима ведущего устройства с сигналами чтения и записи па од- ном выводе (RD5); • использование восьмиразрядной шипы (выводы порта Е); • требуется только один разряд адреса, поэтому мы остановимся на минимальной конфигурации, состоящей из разрядов РМАО (RB15) и РМА1. Кроме того, необходимо учитывать, что типичный ЖК-модуль — это чрезвы- чайно медленнодействующее устройство. По этой причине мы выберем наиболее щадящий временной график, добавим максимально возможное число циклов ожи- дания на каждом этапе процесса чтения или записи: • 4 х Тсу — время ожидания установки данных перед чтепием/записыо; • 15 х Тсу — время ожидания между сигналами R/W и Е;
Полет 135 • 4 х Тсу — время ожидания установки данных после включения интерфейса. Небольшая библиотека функций для доступа к ЖК-дисплею Опираясь на контрольный список “Настройка нового проекта”, создайте проект и новый файл исходного кода. Для начала разработаем подпрограмму инициализа- ции ЖК-индикатор. Вполне естественно, она начнется с инициализации управляю- щих регистров порта РМР: void LCDinit( void) ( // Инициализация РМР PMCON = 0x83BF; // PMMODE = 0x3FF; // PMPEN = 0x0001; // Активизируем РМР, длинные периоды ожидания Режим ведущего 1 Активизируем РМАО Теперь мы в состоянии обмениваться данными с ЖК-модулем, а значит сможем выполнить стандартную процедуру инициализации ЖК-дисплея, рекомендованную производителем. Эта процедура должна протекать в строго определенных времен- ных рамках (подробности см. в табл. 9.1 с набором команд чипсета HD44780) и на- чинаться как минимум через 30 мс, необходимых ЖК-модулю для собственной внутренней инициализации (сброс по подаче питания). Для простоты и надежности мы жестко закодируем задержку в функции инициализации ЖК-модуля, а для соз- дания простых и точных временных циклов на всех последующих этапов восполь- зуемся таймером Тimer 1: // Инициализируем TMR1 T1CON = 0x8030; // Fosc/2, предделитель 1:256, 16 мкс/импульс // Выжидаем 30 мс TMR1 = 0; while(TMRl < 2000); // 2000 х 16 мкс = 32 мс Для удобства мы также определим пару констант, чтобы сделать код более чи- табельным: ttdefine LCDDATA 1 #define LCDCMD 0 #define PMDATA PMDIN1 // RS = 1 ; доступ к регистру данных // RS = 0 ; доступ к регистру команд // Буфер данных РМР Для передачи каждой команды в ЖК-модуль вначале выбирают регистр команд (установка адреса РМАО = RS = 0), после чего начинается процедура записи через порт РМР путем размещения байта требуемой команды в выходном буфере данных РМР: PMADDR = LCDCMD; // Выбираем регистр команд (ADDR = 0) PMDATA = ObOOlllOOO; // Установка функции: 8-разрядный интерфейс, // две линии, 5x7 Порт РМР выполнит следующую полную процедуру записи в шипу. 1. Адрес выставляет на шину адреса РМР (РМАО). 2. Содержимое регистра PMDATA выставляется па шину данных РМР (PMD0- PMD7). _ 3. По прошествии времени 4 х Тсу сигнал R/W стабилизируется на низком уровне (RD5). 4. По прошествии времени 15 х Тсу сигнал Е стабилизируется на высоком уровне (RD4).
136 Глава 9. Стеклянное счастье 5. По прошествии времени 4 х Тсу уровень сигнала Е понижается, и содержимое регистра PMDATA удаляется из шипы. Как видим, процедура получается довольно длинной, поскольку длится более 20 х Тсу = 1,25 мкс. Другими словами, порт РМР все еще будет выполнять ее в то время, как микроконтроллер PIC24 уже выполнит не менее 20 команд. Впрочем, на этот раз нам не стоит беспокоиться о времени, необходимом порту РМР, поскольку мы все равно собирались выждать не менее 40 мкс, чтобы дать ЖК-модулю воз- можность выполнить команду: TMR1 = 0; while (TMR1 < 3); // 3 х 16 мкс = 48 мкс Затем мы аналогичным образом пройдем остальные этапы процедуры инициа- лизации ЖК-модуля: PMDATA = ОЬООООНОО; TMR1 = 0; while(TMRl < 3); PMDATA = 0Ь00000001; TMR1 = 0; while(TMR1 < 100); PMDATA = ObOOOOOllO; TMR1 = 0; while(TMR1 < 100); // Включаем индикацию, скрываем курсор // отключаем мерцание // 3 х 16 мкс = 48 мкс // Очищаем дисплей // 100 х 16 мкс = 1,6 мс // Смещаем курсор без сдвига индикации // 100 х 16 мкс = 1,6 мс После инициализации ЖК-модуля все немного упрощается, поскольку отпадает необходимость в циклах задержки, благодаря возможности использовать команду ЖК-модуля “Чтение флага занятости”. С ее помощью мы узнаем, что встроенный контроллер ЖК-модуля завершил выполнение последней команды и готов принять и обработать новую. Для того чтобы прочитать регистр состояния ЖК-ипдикатора, содержащий флаг занятости, необходимо, чтобы порт РМР выполнил процедуру чтения шины. Этот процесс протекает в два этапа. Вначале считывается (и игнори- руется) содержимое буфера данных РМР, чтобы начать процесс чтения. По завер- шении процедуры чтения в буфере данных окажется фактическое значение, прочи- танное из шины, и оно опять считывается из буфера данных РМР. Но как опреде- лить, что порт РМР завершил процедуру чтения? Очень просто. Мы можем прове- рить флаг занятости в управляющем регистре PMSTAT. Итак, подведем итог... Для того чтобы проверить флаг занятости ЖК-модуля, необходимо вначале проверить флаг занятости порта РМР, выдать команду чтения, опять проверить флаг занятости порта РМР и, наконец, получить доступ к фактиче- скому содержимому регистра состояния ЖК-модуля. Передав в функцию чтения в качестве параметра адрес регистра, мы получим более обобщенную подпрограмму, способную прочитать, как регистр состояния ЖК-индикатора, так и регистр данных: char LCDread(int addr) { int dummy; while(PMMODEbits.BUSY) ; // Ожидаем, пока порт РМР завершит // выполнение предыдущей команды PMADDR = addr; / / Выбираем адрес команды dummy = PMDATA; / / Инициализируем цикл чтения, считываем // фиктивное значение while(PMMODEbits.BUSY) ; // Ожидаем, пока порт РМР завершит // процедуру return(PMDATA); // Считываем регистр состояния } // LCDread
Полет 137 Регистр состояния ЖК-модуля содержит флаг занятости и текущее значение указателя на оперативную память ЖК-иидикатора. Воспользуемся для извлечения этих элементов двумя простыми макросами: LCDbusy () и LCDaddr (). Еще один макрос задействуем для доступа к регистру данных: ♦define LCDbusy() LCDread(LCDCMD) & 0x80 ♦define LCDaddr() LCDread(LCDCMD) & 0x7F ♦define getLCDQ LCDread(LCDDATA) Для записи данных и команд в ЖК-модуль можно воспользоваться функцией LCDbusy(): void LCDwrite(int addr, char с) { while(LCDbusy()); while(PMMODEbits.BUSY); // Ожидаем освобождения порта РМР PMADDR = addr; PMDATA = c; } // LCDwrite В дополнение к библиотеке напишем еще несколько макросов: • put LCD () — передача ASCII-даппых в ЖК-модуль: ♦define putLCD(d) LCDwrite(LCDDATA, (d)) • LCDcmd () — передача команд в ЖК-модуль: ♦define LCDcmd(с) LCDwrite(LCDCMD, (c)) • LCDhome () — перемещение курсора к первому символу в строке: ♦define LCDhome() LCDwrite(LCDCMD, 2) • LCDclr () — очистка всего содержимого дисплея: ♦define LCDclr() LCDwrite(LCDCMD, 1) И наконец, для удобства создадим функцию putsLCD (), которая будет пере- давать в ЖК-модуль целые строки с завершающим нулевым символом: void putsLCD(char *s) ( wh i 1 e (* s) putLCD (*s + +) ; } //putsLCD Запустим все это в работу, добавив короткую функцию main: main( void) { // Инициализация LCDinit() ; // Выводим в первой строке заголовок putsLCD("Flying the PIC24"); // Главный цикл (пока что пустой)
138 Глава 9. Стеклянное счастье while (1) { } } // main Если компоновка проекта и программирование платы Explorer 16 с применением отладчика ICD2 прошли гладко, то в первой строке ЖК-дисплея должна появиться строка “Flying the PIC24”, с чем вас и поздравляю. Расширенное управление ЖК-дисплеем Тем, кому все вышесказанное показалось сущим пустяком, предлагаю несколь- ко более сложную и интересную задачу. Говоря о совместимости буквенно-цифровых ЖК-модулей с чипсетом HD44780, мы упомянули, что содержимое индикаторов формируется контроллером с помощью специальной таблицы: генератора символов, размещенного в ПЗУ. Так- же мы упомянули о возможности расширения набора символов с помощью допол- нительного RAM-буфера, известного как CGRAM. Запись в память CGRAM создает новые графические-элементы с помощью матрицы размерами 5x7. Как насчет добавления к набору символов ЖК-дисплея платы Explorer 16 не- большого самолета? Для этого нам понадобится функция, устанавливающая с по- мощью команды “Установка адреса CGRAM” указатель на RAM-буфер ЖК-модуля, на первый адрес памяти CGRAM (а еще лучше — макрос, использующий функцию LCDwrite ()): #define LCDsetG(a) LCDwrite(LCDCMD, (a & 0x3F) | 0x40) Для формирования двух новых символьных матриц 5x7 (одна — для носа, а другая — для хвоста самолета) мы воспользуемся функцией putLCD (). Пять младших разрядов каждого байта будут выделены для определения одной строки в матрице. После того, как определена последняя строка каждого символа, добавля- ется еще один (восьмой) байт данных для выравнивания по блоку соседнего симво- ла. // Формируем два новых символа LCDsetG(0); putLCD (ОЬОООЮ) ; putLCD (ОЬОООЮ) ; putLCD(ObOOllO); putLCD (0Ы1111) ; putLCD(ObOOllO); putLCD (ОЬОООЮ) ; putLCD (ОЬОООЮ) ; putLCD(O); // Выравнивание putLCD(ObOOOOO); putLCD(ObOOlOO); putLCD(ObOllOO); putLCD(Obi1100); putLCD(ObOOOOO); putLCD(ObOOOOO); putLCD(ObOOOOO); putLCD(0); // Выравнивание Теперь два новых символа доступны по кодам 0 и 1 в таблице генератора. Для того чтобы указатель па буфер опять указывал на память DDRAM, воспользуемся следующим макросом:
Разбор полота 139 ♦define LCDsetC(a) LCDwrite(LCDCMD, (a & 0x7F) I 0x80) ПРИМЕЧАНИЕ____________________________________ Хотя первая строка индикатора соответствует адресам с б по Oxf буфера DDRAM, второй строке всегда соответствуют адреса с 0x40 по Ox4f независимо от размеров дисплея (т.е. количества символов в каждой его строке). Для того чтобы наш самолет “летел” по точному графику и всегда оставался видимым, нам опять потребуется простой механизм задержки, основанный на тай- мере Timerl. ЖК-индикаторы — медленнодействующие, и в том случае, если дис- плей обновляется слишком быстро, изображение на нем исчезает, подобно призра- ку. #define TFLY 9000 // 9000 х 16 мкс = 144 мс tfdefine DELAY() TMRl=0; while(TMR1 < TFLY) Настал момент разработать простой алгоритм, чтобы заставить наш самолет “летать” в главном цикле: // Главный цикл while(1) { // Весь самолет появляется у правого края LCDsetC(0x40 + 14); putLCD(O); putLCD(l); DELAY(); // Летим справа налево for(i=13; i>=0; i—) { LCDsetC(0x40 + i) ; // Устанавливаем курсор в следующую // позицию putLCD (0); putLCD(l); // Новый самолет putLCD(' '); // Удаляем старый хвост DELAY(); } // Нос исчезает за левым краем, видим только хвост LCDsetC(0x40) ; putLCD(1); putLCD(' '); DELAY(); // Удаляем хвост LCDsetC(0x40) ; // Указатель на левый край второй строки putLCD(' '); // Рисуем только нос, появляющийся справа LCDsetC(0x40 + 15); // Указатель на правый край второй строки putLCD(0); DELAY(); } // Повторяем главный цикл Разбор полета В этом уроке мы научились использовать порт РМР для управления модулем ЖК-дисплея, хотя в действительности прошлись только “по верхам”. Кроме того,
140 Глава 9. Стеклянное счастье ЖК-модуль — относится к относительно медленнодействующей периферии, поэто- му на первый взгляд может показаться, что мы не получаем особых преимуществ от использования порта РМР вместо традиционного ввода-вывода. Тем не менее, на самом деле даже при доступе к таким простым и медленным устройствам порт РМР дает два важных преимущества: • временные характеристики, последовательность и мультиплексирование управ- ляющих сигналов всегда гарантированно соответствует конфигурационным па- раметрам, что устраняет риск возникновения опасных конфликтов шипы и/или сбойных рабочих ситуаций вследствие ошибок кодирования или непредвиден- ных условий функционирования; • центральный процессор полностью освобожден от обслуживания периферий- ной шины, что позволяет одновременно решать любое число более приоритет- ных задач. Заметки для экспертов по С Как и в предыдущем уроке, в котором речь шла об асинхронных последова- тельных интерфейсах, низкоуровневые подпрограммы ввода-вывода, определенные в библиотеке stdio. h (в частности, write) можно заменить с целью перенаправ- ления вывода па ЖК-дисплей. Мы можем расширить предыдущий пример, реализо- вав перенаправление на модуль UART2 для стандартных потоков (stdin, stdout и stderr) и добавив четвертый поток для ЖК-дисплея: /* * * write.с * * Заменяет стандартную библиотечную функцию write * / ^include <p24fj128ga010.h> ^include <stdio.h> ^include "conU2.h" #include "LCD.h" int write(int handle, void *buffer, unsigned int len) { l n t i, * p; const char *pf; switch (handle) t case 0: // stdin case 1: // stdout case 2: // stderr for (i = len; i; --i) putU2(*(char*)buffer); break; case LCD: // Дополнительный поток for (i = len; i; —i) putLCD(*(char*)buffer); break; default: break; } // switch
Советы и хитрости 141 return (len); } //write ®Этот вариант файла Write.с находится также на прилагаемом к книге компакт-диске в папке Проекты\09 - Стеклянное счастье. В качестве альтернативного варианта можно перенаправить поток stdout на ЖК-дисплей в качестве главного вывода приложения, а поток stderr — в после- довательный порт в целях отладки. Кроме того, вполне вероятно, что на данном этапе понадобится модифицировать функцию putLCDO, чтобы опа могла интер- претировать специальные символы, наподобие ’ \п ’ для перехода на новую строку, или даже реализовать частичное декодирование некоторых управляющих ANSI- последовательностей для возможности позиционировать курсор и очищать дисплей, как в случае с терминальной консолью. Советы и хитрости Поскольку ЖК-дисплей — медленнодействующее периферийное устройство, реализация задержек в ожидании завершения его команд с помощью блокирующих циклов (как в примерах, рассмотренных в данном уроке) в некоторых приложениях может привести к недопустимой трате циклов центрального процессора. Более оп- тимальная схема заключается в кэшировании команд ЖК-модуля в FIFO-буфере и периодическом их выполнении с помощью механизма прерываний. Таким обра- зом, реализуется выполнение медленнодействующих процессов в фоновом режиме по отношению к приложению. Пример такой реализации можно найти в файле LCD. с в папке Проекты\ЬСП прилагаемого к книге компакт-диска. Упражнения 1. Расширьте функцию putLCDO, чтобы опа корректно интерпретировала сле- дующие символы: • ’ \п ’ — переход па следующую строку; • ’ \г ’ — перемещение курсора в начало текущей строки; • ’ \ t ’ — переход в фиксированную позицию табуляции. 2. Расширьте функцию putLCDO, чтобы она корректно интерпретировала сле- дующие управляющие ANSI-коды: • ’ \xlb [ 2 J ’ — полная очистка экрана; • ’ \xlb [ 1, III' — перемещение курсора в начальную позицию; • ’ \xlb [ n, mH ’ — перемещение курсора в строку п, столбец т. Ссылки • http://www.microchip.com/stellent/idcplg?IdcService= SS_GET_PAGE&nodeId=1824&appnote=en011993 — ссылка на руково- дство Microchip по стеку TCP/IP для всех микроконтроллеров PIC; • http: / /www .microchip . com/stellent/idcplg?T.dcService= SS_GET_PAGE&nodeId=1824&appnote=en012108 — руководство с опи- санием протокола SNMP для приложений на базе стека Microchip TCP/IP.
ГЛАВА 1 О Этот аналоговый мир В этой главе: > Первое преобразование > Автоматический выбор длительности выборки > Демонстрационная программа > Игра > Измерение температуры > Еще одна игра Существуют некоторые вещи, которые, как бы ты ни старался, никогда не уда- ется в точности повторить дважды. Хороший пример этого — посадка. Даже наибо- лее опытные командиры экипажей время от времени допускают “ляпы” во время посадки. Я не сомневаюсь, что некоторым читателям уже довелось прочувствовать это, когда они подпрыгивали вместе с самолетом во время касания шасси посадоч- ной полосы. Но что же делает посадку таким сложным элементом пилотажа? Все дело в том, что никогда невозможно получить в точности одинаковые ус- ловия приземления. Постоянно изменяется скорость и направление ветра, варьиру- ются рабочие характеристики двигателя, и даже крылья немного меняют свою фор- му в результате колебаний температуры воздуха. Добавьте к этому непостоянство реакций (и расторопности) пилота, и вы получите бесконечное число непредсказуе- мых обстоятельств, ведущих к бесконечному числу потенциальных неприятностей. Мы живем в аналоговом мире. Все входные переменные (температура, скорость и направление ветра), возбудители наших сенсорных систем (свет, звуки, давление), и результирующие действия (например, движения мышц пилота для управления са- молетом) — аналоговые величины. Мы постоянно учимся интерпретировать (или, точнее сказать, — преобразовывать) все аналоговые значения, поступающие из ок- ружающего мира, и принимать наиболее оптимальные решения. Все, что необходи- мо для достижения совершенства (или состояния, близкого к нему), — практика. В области встроенных систем внешнюю аналоговую информацию вначале тре- буется преобразовать в цифровую форму, и одним из ключевых средств взаимодей- ствия микроконтроллера с “реальным” миром является аналого-цифровой преобра- зователь (АЦП). План полета Семейство микроконтроллеров PIC24 разрабатывалось с прицелом на встроен- ные системы и потому идеально подходит для взаимодействия с аналоговым миром. Все модели оснащены скоростным АЦП, способным выполнять 500 000 преобразо-
Предполетный контроль 143 ваний в секунду, с входным мультиплексором, позволяющим быстро и с высоким разрешением опрашивать несколько аналоговых входов. В данном уроке мы вос- пользуемся 10-разрядпым модулем АЦП микроконтроллера PIC24FJ128GA010 для реализации двух простых измерений через плату Explorer 16: • считывание уровня входного напряжения с потенциометра; • считывание уровня входного напряжения с датчика температуры. Предполетный контроль Кроме обычных программных средств (интегрированная среда MPLAB, компи- лятор MPLAB СЗО и имитатор MPLAB SIM), в этом уроке потребуется демонстра- ционная плата Explorer 16 и внутрисхемный отладчик MPLAB ICD2. Полет Как и в случае с любым другим периферийным модулем PIC24, первый шаг в использовании АЦП — это знакомство с его внутренним устройством и основны- ми управляющими регистрами. А значит придется еще раз перечитать специфика- цию микроконтроллера и заглянуть в руководство по Explorer 16, что изучить схемы подключений. Мы начнем со структурной схемы модуля АЦП (рис. 10.1). Внутренняя шина данных AVdo 16 Компаратор Vinl AN1 AN2 AN3 AN4 Va- Va+ ЦАП 10-разрядный l SAR AVSS ANS VINH управление опросом Управление преобра зованием Схема управления Управление входным мульти! шексором Управление конфигурацией вывода Рис. 10.1. Структурная схема модуля АЦП ANO VlNH Схема преобразования Форматирование данных ADC1BUFO: A0C1BUFF AD1CON1 AD1CON2 AD1CON3 AD1CHS AD1PCFG
144 Глава 10. Этот аналоговый мир Эта схема довольно изощренная и предлагает множество интересных возмож- ностей: • под аналоговые входы можно задействовать до 16 выводов; • для выбора различных входных аналоговых каналов и источников опорного на- пряжения служат два входных мультиплексора; • результат, полученный на выходе 10-разрядного преобразователя, можно от- форматировать под 16-разрядпую целочисленную или вещественную арифме- тику (со знаком или без); • схема управления предусматривает различные последовательности автоматиче- ского преобразования с синхронизацией с работой других модулей и входов, задействованных в данном процессе; • результат преобразования сохраняется в 16-разрядном буфере глубиной 16 слов, который можно сконфигурировать для последовательного считывания или простой FIFO-буферизации. Все эти возможности требуют надлежащего конфигурирования нескольких управляющих ре- гистров, в которых (особенно поначалу) можно запросто запутаться. Поэтому мы начнем с наи- более простого примера: опроса потенциометра R6 на плате Explorer 16 (рис. 10.2). Потенциометр на 10 кОм подключен напря- мую к шипе электропитания, чтобы его выход мог охватить весь диапазон напряжений от 3,3 В до опорного уровня “земли”. Он соединен с вы- водом RB5, соответствующий аналоговому входу AN5 входного мультиплексора АЦП. С помощью соответствующего контрольного списка создайте проект и новый файл исходного кода pot. с со ссылкой па соответствующий заголовочный файл. В пего мы добавим определение двух полезных констант. Первая из них (РОТ) зада- ет входной капал, назначенный потенциометру, а вторая (AINPUTS) представляег собой маску, определяющую распределение аналоговых и цифровых входов: /* * * Этот аналоговый мир ★ ★ Преобразование аналогового сигнала с потенциометра * / #include <p24fj128ga010.h> # define POT 5 // Понетциоментр на 10k подключен к выводу AN5 t fdefine AINPUTS Oxffef // Аналоговые входы для Explorerl6: РОТ и TSENS Фактическую инициализацию всех управляющих регистров АЦП лучше всего реализовать в виде небольшой функции initADCO, конфигурирующей следую- щие регистры: • AD1PCFG — маска выбора аналоговых входных каналов (0 в некотором разряде обозначает аналоговый, al — цифровой вход); • AD1CON1 — определяет' автоматическое начало преобразования по завершению этапа выборки; результат будет отформатирован как обычное беззнаковое целое с выравниванием по правому разряду; • AD1CSSL — будет обнулен, поскольку функция опроса не используется (только один вход); 4-3.3V ► R8 k1OK £12 NL Рис.. 10.2. Потенциометр R6 демонстрационной платы Explored6
Полет 145 • AD ICON2 — определи?! использование мультиплексора MUXA и соединяет входы опорного сигнала AI (П с аналоговыми входами AVdd и AVss; • AD1CON3 — выбирает источник тактирования и коэффициент деления для пре- образования. Конфигурирование завершается установкой разряда ADON, что активизирует всю периферию АЦП. void initADC(int amask) { AD1PCFG = amask; AD1CON1 = 0; // Выбираеми аналоговые входы // Ручное управление процессом преобразования' AD1CSSL = 0; // Опрос не требуется AD1CON2 = 0; // Используем MUXA, входы AVss и AVdd // используются как Vref+/- AD1CON3 = 0xlF02; // Tad = 2 х Тсу = 125 нс > 75 нс ADlCONlbits.ADON = 1; // Активизируем АЦП } //initADC Передавая в подпрограмму инициализации параметр amask, мы делаем ее дос- таточно гибкой для восприятия нескольких входных каналов в будущих приложе- ниях. Первое преобразование По сути аналого-цифровое преобразование — это двухэтапный процесс. Преж- де всего необходимо снять сигнал входного напряжения, после чего вход можно от- ключить и выполнить собственно преобразование считанного напряжения в число- вое значение. Эти два этапа контролируются двумя управляющими разрядами в ре- гистре AD1CON1: SAMP И DONE. Для обеспечения необходимой точности измерений важную роль играет хроно- метраж этих двух этапов. • На этапе выборки внешний сигнал подается на внутренний конденсатор, кото- рый необходимо зарядить до уровня входного напряжения. Для этого требуется выделить достаточно времени, которое обычно пропорционально импедансу источника входного сигнала (в нашем случае составляет менее 5 кОм), а также емкости конденсатора. В общем случае, чем больше время выборки, тем лучше результат, согласующийся с частотой входного сигнала (в нашем примере роли не играет). • Длительность этапа преобразования зависит от выбранного источника тактиро- вания АЦП. Тактовый сигнал обычно получают из синхросигнала центрального процессора после пропускания через делитель или с помощью независимого RC-осциллятора. Последний вариант, хотя и привлекает своей простотой, хо- рошо подходит в тех случаях, когда преобразование выполняется в “спящем” режиме (режиме пониженного энергопотребления) при отключенном тактовом генераторе центрального процессора. В остальном, предпочтительный способ формирования тактового сигнала — с помощью делителя, поскольку он обеспе- чивает синхронизацию с центральным процессором и, как следствие, — более надежное подавление внутреннего шума. Частота тактового сигнала должна быть максимально большой согласно спецификациям модуля АЦП (в нашем примере время Tad должно превышать 75 нс, учитывая минимальный коэффи- циент деления 2).
146 Глава 10. Этот аналоговый мир Рассмотрим базовую подпрограмму преобразования: int readADC(int ch) ( AD1CHS = ch; ADlCONlbits.SAMP = 1; TMR1 = 0; while (TMR1< 100); ADlCONlbits.DONE = 1; while (’ADlCONlbits.DONE); return ADC1BUF0; } // readADC // 1. Выбираем входной аналоговый канал // 2. Начинаем выборку // 3. Выжидаем время выборки // 6.25 мкс // 4. Начинаем преобразование // 5. Ожидаем завершения преобразования // 6. Считываем результат преобразования Автоматический выбор длительности выборки Как видим, согласно такому базовому методу мы сами отвечаем за обеспечение точных временных характеристик этапа выборки с помощью двух циклов ожида- ния. Однако микроконтроллеры PIC24 позволяют отчасти автоматизировать данный процесс. Длительность этапа выборки определяется без нашего участия при усло- вии, что импеданс входного источника достаточно мал для того, чтобы обеспечить максимальную продолжительность дискретизации 32 х Tad (в нашем примере 32 х 120 нс = 3,8 мкс). Этого можно достичь, записав в разряды SSRC регистра AD1CON1 значение 0Ь111, чтобы активизировать автоматическое начало преобразования по завершении периода дискретизации. Сам период выбирают с помощью разрядов SAM регистра AD1CON3. Рассмотрим откорректированную функцию инициализации АЦП, использую- щую автоматический выбор длительности дискретизации и момента начала преоб- разования: void initADC(int amask) AD1PCFG = amask; // Выбор аналоговых входов AD1CON1 = ОхООЕО; // После выборки автоматически начинается // преобразование AD1CSSL = 0; // Опрос не требуется AD1CON2 = 0; // Используем MUXA; и AVss и AVdd задействованы как Vref+/- AD1CON3 = 0xlF02; // Tsamp = 32 х Tad; Tad = 125 нс ADlCONlbits.ADON = 1; // Включаем АЦП //initADC Автоматическое начало преобразования по завершении этапа выборки дает два преимущества: • гарантированно корректное время выборки без необходимости использовать какие-либо циклы задержки и/или другие ресурсы; • для выполнения всей процедуры дискретизации и преобразования достаточно одной команды (начала этапа выборки). Когда АЦП сконфигурирован подобным образом, активизация преобразования и считывание результата становится совсем простой задачей: • AD1CHS выбирает входной канал для MUXA; • установка разряда SAMP в регистре AD1CON1 начинает этап выборки с автома- тическим выбором длительности, после которого сразу же следует преобразо- вание;
Полет 147 • как только будет выполнена вся процедура, и результат готов к использованию, в регистре AD1CON1 устанавливается разряд DONE; • чтение регистра ADC1BUF0 сразу же дает' требуемый результат преобразования. int feadADC(int ch) { AD1CHS = ch; и 1. Выбор аналогового входного канала ADlCONlbits.SAMP = 1; // 2. Начало выборки while (!ADlCONlbits.DONE); // 3. Ожидаем завершения преобразования return ADC1BUF0; // 4. Считываем результат преобразования } // readADC Демонстрационная программа Все что нам осталось сделать, — придумать какой-нибудь интересный способ проявить преобразованное значение с помощью демонстрационной платы Explorer 16. Наиболее очевидное решение — подключить светодиоды к порту А, од- нако вместо простого двоичного вывода восьми старших разрядов 10-разрядного результата мы немного усложним задачу, сделав визуальную демонстрацию более близкой к аналоговой природе входного сигнала. Мы будем каждый раз включать по одному светодиоду, индицируя изменение угла поворота механического регулятора. Главная подпрограмма для тестирования наших аналого-цифровых функций выглядит следующим образом: main () { int а; // Инициализация initADC(AINPUTS); // Инициализируем АЦП для аналоговых входов // платы Explorerl6 TRISA = Oxff00; // Выбираем выводы порта А в качестве выходов // для управления светодиодами // Главный цикл while (1) { а = readADC(РОТ); // Выбираем вход РОТ и выполняем преобразование // Уменьшаем 10-разрядный результат до 3-разрядного значения (0..7) // (делим на 128 путем сдвига вправо 7 раз) а »= 7; // Включаем только соответствующий светодиод // 0 -> карйний слева светодиод.... 7-> крайний справа светодиод PORTA = (0x80 » а); } // Главный цикл } // main После вызова подпрограммы инициализации (в которой с помощью маски оп- ределено, что разряд 5 — это аналоговый вход), мы инициализируем регистр TRISA таким образом, чтобы выводы, подключенные к линейке светодиодов, были цифро- выми выходами. Затем в главном цикле мы выполняем преобразование на выводе AN5 и форма- тируем результат таким образом, чтобы он соответствовал нашим требованиям ин-
148 Глава 10. Этот аналоговый мир дикации. Согласно заданной конфигурации будет получен 10-разрядный целочис- ленный результат преобразования, выровненный по правому краю (диапазон значе- ний 0..1024). Разделив это число па 128 (т.е. семикратно сдвинув его вправо), мы можем уменьшить диапазон до 0..7. Кроме того, для получения восьми необходи- мых светодиодных конфигураций конечный результат потребуется подвергнуть еще одной трансформации. Дело в том, что светодиод, соответствующий старшему зна- чащему разряду, находится на левом крае линейки. Для того чтобы согласовать движение потенциометра (по часовой стрелке) и смещение индекса светодиода (вправо), необходимо, начав с шаблона 0Ы0000000, сдвигать его вправо. Скомпонуйте проект, после чего с помощью знакомого контрольного списка “Настройка отладчика MPLAB ICD2” запрограммируйте плату Explorer 16. Если все было сделано правильно, то в результате вращения ручки потенциометра будет по- следовательно изменяться количество включенных светодиодов. Игра Должен признать, что рассмотренный выше пример не особо впечатляет даже новичков. Что особенного в том, что мы используем 16-разрядную машину, которая может выполнять до 16 миллионов операций в секунду, для аналого-цифрового преобразования примерно 200 000 раз в секунду (32 Tad выборки + 12 Tad преобра- зования, где Tad = 125 нс) и получаем всего лишь три разряда результата и включе- ние/отключепие одного светодиода? Предлагаю разработать что-нибудь более изо- щренное и интересное, например, игру “Стукни крота”. Правила игры просты... Второй по счету светодиод, контролируемый микро- контроллером PIC24, исполняет роль крота. Он отличается от управляемого пользо- вателем светодиода (молоток) своим приглушенным свечением. Наша задача — вращать ручку потенциометра, сминая “молоток”до тех пор, пока он не достигнет “крота” (т.е. “стукнет” его). Сразу же после этого в случайно выбранной позиции появляется новый “крот”, и весь процесс повторяется сначала. В данном случае нам пригодится генератор псевдослучайных \лсел: функция rand (), объявленная в файле stdlib.h, — поскольку все компьютерные игры требуют определенной степени непредсказуемости. С ее помощью мы будем вы- числять позицию каждого нового “крота”. Сохраните исходный файл из первого проекта под новым именем LEDgame. с и создайте полностью новый проект. Затем обновите функцию main (), добавив к ней несколько строк кода: main () { int a, г, с; // 1. Инициализация initADC(AINPUTS); // Инициализируем АЦП для аналоговых • // входов платы Explorerl6 TRISA = OxffOO; // Выбираем выводы порта А в качестве // выходов для управления светодиодами // 2. Используем первое считывание для активизации генератора // псевдослучайных чисел srand(readADC(РОТ)); г = 0x80; с = 0;
Полет 149 // 3. Главный цикл while(1) { а = readADC(РОТ); // Выбираем вход РОТ и выполняем // преобразование // 3.1. Уменьшаем 10-разрядныи результат до трехразрядного // значения (0..7) // (делим на 128 или семь раз сдвигаем вправо) а »= 7; // 3.2. Включаем только соответствующий светодиод // 0 -> крайний слева светодиод.... 7-> крайний справа светодиод а = (0x80 » а); // 3.3. Как только курсор достигает случайно выбранной точки, // генерируем новое случайное значение while (а == г) г = 0x80 >> (гапсЦ) & 0x7); // 3.4. Отображаем "молоток" (яркий светодиод) и "крота" // (приглушенный светодиод) if ( (с & Oxf) == 0) PORTA = а + г; // "Крот" светится только 1/16 времени // свечения "молотка" else PORTA = а; // "Молоток" светится всегда // 3.5. Счетчик цикла C+ + ; } // Главный цикл } // main • В пункте 1 выполняется обычная инициализация модуля АЦП и порта А, к ко- торому подключена линейка светодиодов. • В пункте 2 происходит первое считывание значения потенциометра, которое используется в качестве основы для генератора случайных чисел. В результате каждая позиция “крота” будет уникальной при условии, что ручка потенцио- метра не находится в крайнем левом или крайнем правом положении. Это даст основу 0 или 1 023 соответственно, из-за чего игра будет следовать одной и той же схеме, поскольку псевдослучайная последовательность при каждом переза- пуске программы использует одинаковый шаг. • В пункте 3 начинается главный цикл, где, как и в предыдущем примере, считы- вается 10-разрядное значение, которое затем сокращается до трех старших раз- рядов (пункт 3.1). • В пункте 3.2 происходит преобразование в позицию светодиода а, что нам уже знакомо, а вот пункт 3.3 открывает' нам нечто новое. Если позиция “молотка”, представленная значением а, совпадает с позицией “крота” (значение г), то сра- зу же вычисляется повое случайное число. Эта операция повторяется в цикле while, поскольку при каждом вычислении нового случайного значения г су- ществует' вероятность, что оно совпадет с предыдущим. Другими словами, но- вый “крот” может появиться прямо под “полотком”, что, согласитесь, совсем неинтересно.
150 Глава 10. Этот аналоговый мир • Пункты 3.4 и 3.5 посвящены включению и сравнению позиций двух светодио- дов. Для индикации обеих элементов на линейке можно было бы просто “сло- жить” два двоичных шаблона а и г, но в таком случае игроку было бы сложно определить, какой из светодиодов — “крот”, а какой — “молоток”. Для того чтобы приглушить светодиод “крота”, внутри главного цикла можно опреде- лить моменты свечения одновременно обоих светодиодов и моменты видимо- сти только “молотка”. Поскольку главный цикл выполняется сотни тысяч раз в секунду, наши глаза воспринимают свечение “крота” как более приглушенное пропорционально количеству пропущенных циклов. Например, если светодиод “крота” включается только один раз за 16 циклов, то будет казаться, что его яр- кость в 16 раз меньше яркости светодиода “молотка”. • Этот механизм реализуется с помощью счетчика с, который постоянно инкре- ментируется в пункте 3.5. В пункте 3.4 мы проверяем только четыре младших разряда счетчика (0...15) и включаем светодиод “крота” только в тот момент, когда все эти разряды содержат 0. В остальных 15 циклах включается только светодиод “молотка”. Скомпонуйте проект и загрузите его в плату Explorer 16. Согласитесь, что те- перь все — намного интересней! Измерение температуры А теперь переходим к более серьезным вещам... В плату Explorer 16 встроен датчик температуры Microchip ТС 1047А, выдающий строго линейное выходное на- пряжение. Это устройство — миниатюрное, поскольку выполнено в корпусе SOT-23 (три вывода для поверхностного монтажа). Потребление тока ограничено 35 мкА (типичное значение), в то время как напряжение питания может покрывать весь диапазон от 2,5 до 5,5 В. Выходное напряжение не зависит от напряжения питания и представляет собой строго линейную функцию температуры (обычно — в преде- лах 0,5°С) с наклоном ровно 10 мВ/°С. Смещение можно корректировать, чтобы по- лучить абсолютную индикацию температуры по формуле, показанной па рис. 10.3. Температура (°C) Рис. 10.3. Выходная характеристика датчика ТС1047
Полот 151 Теперь можно применить АЦП микроконтроллера PIC24 для преобразования выходного напряжения в цифровые данные. Датчик температуры напрямую под- ключен к аналоговому входному каналу AN4 согласно схематике платы Explorer 16. Рис. 10.4. Схема подключения датчика температуры ТС1047А к демонстрационной плате ExplorerlO Можно повторно использовать АЦП-функции, разработанные для предыдущего упражнения, поместив их в новый проект TSense и сохранив ранее созданный файл исходного кода под именем Tsense. с. Начнем модификацию кода с включения определения новой константы TSENS для входного канала АЦП, назначенного датчику температуры: /* * * Этот аналоговый мир * * Преобразование аналогового сигнала от датчика температуры ТС1047 * / tfinclude <p24fj128ga010.h> # define POT 5 // Потенциометр на 10k подключен ко входу AN5 # define TSENS 4 // Датчик температуры ТС1047 с напряжением на // выходе tfdefine AINPUTS Oxffcf // Аналоговые входы для выводов РОТ и TSENS // платы Explorerl6 // Инициализируем АЦП для одного преобразования, выбираем аналоговые // входы void initADC(int amask) { AD1PCFG = amask; // Выбираем аналоговые входы AD1CON1 = ОхООЕО; // Автоматическое преобразование по завершении // выборки AD1CSSL = 0; // Опрос не требуется AD1CON3 = 0X1F02; // Максимальная длительность выборки = 31 Tad, // Tad = 2 х Тсу = 125 нс > 75 нс AD1CON2 = 0; // Используем MUXA; AVss и AVdd - как Vref+/- ADlCONlbits.ADON = 1; // Включаем АЦП } //initADC int readADC(int ch) { AD1CHS = ch; // Выбираем аналоговые входные каналы ADlCONlbits.SAMP =1; // Начинаем выборку с автоматическим // преобразованием по завершении while (’ADlCONlbits.DONE); // Ожидаем завершения преобразования return ADC1BUF0; // Считываем результат преобразования } // readADC Как видим, с точки зрения конфигурирования АЦП и активизации процедуры преобразования ничего не изменилось, а вот отображение результата с помощью
152 Глава 10. Этот аналоговый мир светодиодов — задача посложнее. Датчики температуры дают определенный уро- вень шума и потому для получения более достоверных данных обычно используют небольшую фильтрацию. Точный результат можно вычислить, усреднив значение 16 выборок: а = 0; for (j=16; j>0; j-~) a += readADC(TSENS) ; // Накапливаем 16 последовательных считываний // температуры i = а >> 4; // Делим результат на 16 для получения среднего Но как отобразить температуру с помощью линейки светодиодов? Мы могли бы взять старшие разряды результата преобразования и представить их в двоичной или двоично-десятичной форме, однако это, опять-таки, неинтересно. Предлагаю реали- зовать относительную индикацию температуры. Для этого мы перед главным цик- лом считаем исходное значение температуры и используем его в качестве смещения для центральной позиции линейки. В главном цикле позиция будет обновляться, смещаясь вправо в случае повышения температуры, или влево в случае ее пониже- ния. Полный код, реализующий опрос температуры, выглядит следующим образом: main () { int a, i, j; // 1. Инициализация initADC(AINPUTS); TRISA = OxffOO; T1CON = 0x8030; // Инициализируем АЦП для аналоговых входов // платы Explorerl6 // Выбираем вывода порта А в качестве выходов // для управления светодиодами // Включаем таймер TMR1, коэффициент // предделителя = 1:256 Tclk/2 // 2. Считываем исходное значение температуры а = 0; for (j=16; j>0; j--) a += readADC(TSENS); // Считываем температуру i = a » 4; // В результате мы получили основу для центрального светодиода // 3. Главный цикл while(1) { // 3.1. Считываем новое (усредненное) значение температуры а = 0; for (j=16; j>0; j —) ( TMR1 = 0; while (TMR1 < 3900); // 3900 x 256 x Tcy ~= 1 c a += readADC(TSENS); // Считываем температуру } a >>= 4; // Усреднение 16 выборок // 3.2. // a = 3 + Сравниваем с исходным значением и смещаем индикацию на одну позицию на градус Цельсия (а - i) ; // 3.3. Поддерживаем результат в диапазоне 0..7; сохраняем // видимость индикации
Полет 153 if (а > 7) а = 7; if (а < 0) а = 0; // 3.4. Включаем соответствующий светодиод PORTA = (0x80 » а); } // Главный цикл } // main • В пункте 3.2 определяется разница между исходным значением i и новым ус- редненным значением а. Если она равна нулю, то включается центральный све- тодиод. • В пункте 3.3 результат проверяется на принадлежность к диапазону допусти- мых значений. Если разница — отрицательная, а ее ширина — более трех раз- рядов, то должен включиться крайний слева светодиод. Если разница — поло- жительная, а ее ширина — более четырех разрядов, то должен включиться край- ний справа светодиод. • В пункте 3.4 результат отображается, как в предыдущем примере. В завершение упражнения рекомендую добавить цикл задержки внутри цикла усреднения в пункте 3.1, чтобы немного замедлить индикацию путем снижения час- тоты обновлений до периода около одной секунды. Более высокая частота обновле- ния дает только раздражающее мерцание, когда температура близка к среднему зна- чению между двумя соседними позициями индикации. Скомпонуйте проект и загрузите его в плату Explorer 16. Затем найдите датчик температуры на плате (он находится у левого нижнего угла процессорного модуля PIC24 и внешне напоминает транзистор для поверхностного монтажа). Запустите программу и понаблюдайте за изменением индикации в результате небольших ко- лебаний температуры, вызванных прикосновением к датчику руки или обдуванием его холодным или теплым воздухом. Еще одна игра Для того чтобы немного поразвлекаться с датчиком температуры, можно объе- динить два последних упражнения в одной игре. Идея состоит в том, чтобы управ- лять “молотком” с помощью датчика. Нагрейте его до такой температуры, чтобы включился крайний справа светодиод, а затем начните обдувать его холодным воз- духом, чтобы индикация начала смещаться влево. main () { int a, i, j, k, г; // 1. Инициализация initADC(AINPUTS); // Инициализируем АЦП для аналоговых входов // платы Explorerl6 TRISA = OxffOO; // Выбираем выводы порта А в качестве выходов // для управления светодиодами T1CON = 0x8030; // Включаем таймер TMR1, коэффициент // предделителя = 1:256 Tclk/2 // 2. Используем первую выборку для активизации генератора // всевдослучайных чисел
154 Глава 10. Этот аналоговый мир srand(readADC(TSENS)); // Получаем первую случайную позицию г = 0x80 » (rand() & 0x7); к = 0; // 3. Вычисляем усредненное значение в качестве исходной точки а = 0; for (j=16; j>0; j —) a += readADC(TSENS); // Считываем температуру i = a >> 4; // 4. Главный цикл while (1) { // 4.1. Получаем усредненное значение через одну секунду а = 0; for (j=16; j>0; j-~) { TMR1 = 0;' while (TMR1 < 3900) // 16 x 3900 x 256 x Тсу ~= 1 c { // Включаем светодиоды "молотка" и "крота" if ((TMR1 & Oxf) == 0) PORTA = k + r; else PORTA = k ; } a += readADC( TSENS); // Считываем температуру } a »= 4; // Усреднение по 16 выборкам // 4.2. Сравниваем с исходным значением и смещаем индикацию на // одну позицию на один градус Цельсия а = 3 + (а - i) ; // Поддерживаем результат в диапазоне 0..7; поддерживаем видимость // индикации if (а > 7) а = 7; if (а < 0) а = 0; // Обновляем светодиод "молотка" к = (0x80 » а); // 5.3. Как только "молоток ударит по кроту", генерируем новую // позицию while (к == г) г = 0x80 >> (rand() & 0x7); } // Главный цикл } // main Разбор полета В этом уроке мы начали исследовать возможности модуля АЦП микроконтрол- леров PIC24. Была использована только одна из наиболее простых конфигураций и рассмотрены лишь некоторые из развитых возможностей этого модуля. При этом мы задействовали два типа аналоговых входов платы Explorer 16, совместив в про- цессе разработки игровых программ приятное с полезным.
Заметки для экспертов по С 155 Заметки для экспертов по С Хотя микроконтроллеры PIC24 используют команду быстрого деления, попусту расходовать циклы процессора нет смысла. В мире встроенных систем каждый та- кой цикл — на вес золота. Если делитель представляет собой степень двойки, то лучше использовать целочисленное деление в виде обычного сдвига вправо на со- ответствующее число разрядов. Такая операция по крайней мере на порядок эффек- тивнее обычного деления. Если делитель — не степень двойки, можно попытаться откорректировать приложение, чтобы исправить ситуацию. Так, в нашем последнем примере мы вполне могли бы выбрать 10, 15 или 20 опросов температуры, однако предпочли 16 выборок, поскольку это сводит деление к простому сдвигу суммы на четыре разряда вправо (в одном командном цикле микроконтроллера PIC24). Советы и хитрости Если требуемая длительность выборки превосходит максимально допустимую (32 х Tad), то можно попытаться увеличить значение Tad или, что еще лучше, — ак- тивизировать автоматический запуск дискретизации (по завершении преобразова- ния). В результате цепь опроса будет всегда задействована при отсутствии преобра- зования. Фактическое преобразование начинается по обнулению программой разря- да SAMP. Кроме того, самые разнообразные периоды выборки с наименьшими нагрузка- ми на центральный процессор можно получить путем периодического обнуления управляющего разряда SAMP с помощью таймера Timer3 (одна из возможностей для разрядов SSRC в регистре AD1CON1) и разрешения прерываний по завершению преобразования модулем АЦП. В таком случае отсутствуют любые циклы ожида- ния, а используются только периодические прерывания в моменты появления гото- вого к извлечению результата. Упражнения Накопите результаты преобразований с помощью FIFO-буфера АЦП. Настройте таймер Timer 3 на автоматическое преобразование таким образом, чтобы прерыва- ние возникало только в момент заполнения буфера значениями температуры, гото- выми для усреднения. Ссылки • http://www.microchip.com/stellent/idcplg?IdcService=SS_GE T_PAGE&nodeId=2102&param=en021419&pageId=79&pageId=79 — описание разнообразных датчиков температуры с различными интерфейсами, включая прямые цифровые выходы 12С™ и SPI.
ЧАСТЬ III Дальний рейс Примите поздравления! Пройдя еще несколько уроков, вы научились более сложным полетам, и мы переходим к третьей (и последней) части учебного курса, в котором вас ожидает дальний рейс. Оставив позади бесконечные круги вокруг аэропорта, утомительные взлеты-посадки и маневры над тренировочной зоной, мы отправляемся в настоящий полет! Эта часть посвящена разработке проектов, требующих использования сразу нескольких периферийных модулей. Поскольку примеры станут более сложными, под рукой рекомендуется иметь не только демонстра- ционную плату Explorer 16, но также средства макетирования для до- бавления в случае необходимости к этой плате новой функционально- сти. Простые схемы соединений и маркировка компонентов будет дана в ходе рассмотрения следующих глав. Дополнительные платы расши- рения и варианты макетов для наиболее сложных проектов также пред- ставлены на Web-сайтах FlyingthePIC24.com и Programming- thePIC24.com.
ГЛАВА 1 1 Фиксация входных данных В этой главе: > Протокол обмена данными через порт PS/2 Взаимодействие микроконтроллера PIC24 с портом PS/2 Захват на входе > Тестирование метода захвата на входе с помощью сценариев стимулов ; Тестирование подпрограмм приема данных через порт PS/2 > Имитации Профиль имитатора > Второй метод: уведомление об изменении сигнала Сравнительная оценка Третий метод: опрос портов ввода-вывода > Тестирование метода опроса портов ввода-вывода > Стоимость и эффективность решения & Завершение интерфейса. Добавление FIFO-буфера > Завершение интерфейса. Декодирование кодов клавиш Как было отмечено в предыдущей главе, современная электроника быстро за- полняет кабины нилотов почти всех самолетов, кроме самых маленьких. Жидкокри- сталлические дисплеи вытеснили традиционные навигационные приборы, а спутни- ковые GPS-нриемники прорисовывают положение самолета в режиме реального времени на цветных картах с изображениями рельефа местности и, при наличии до- полнительного оборудования, — ежеминутно обновляемых погодных схем. Пилоты могут' ввести весь план полета в навигационную систему, а затем следовать мар- шруту, руководствуясь анимационной картой, как в компьютерной игре. Впрочем, обращаться с этими новыми средствами не так-то просто. Как и в случае с любым компьютерным приложением, для управления каждым прибором используется сис- тема меню плюс набор разных кнопок и регуляторов. С их помощью пилот может вводить данные быстро и интуитивно. Тем не менее, сжатое пространство кабины по-прежнему накладывает' серьезные ограничения на тип и количество подобных устройств ввода, которые (по крайней мере, в первых поколениях) напоминают средства управления простым FM-радиоприемником. Если ваша машина оснащена навигационной GPS-системой, и вы, находясь в иностранном городе, пытались с помощью той маленькой ручки ввести адрес (скажем, “Bahnhofstrasse, 17, Munich”), одновременно следя за оживленным движе- нием, то вы прекрасно понимаете, с какими трудностями сталкиваются пилоты. Следующее поколение устройств ввода, встречающееся в некоторых современных самолетах, — это клавиатура. Она уже стала привычным элементом в кабинах реак-
158 Глава 11. Фиксация входных данных тивпых самолетов бизнес-класса и все чаще появляется в самолетах попроще. Ду- маю, вы тоже не отказались бы от клавиатуры в своей следующей машине? План полета С изобретением шины USB компьютеры, наконец, избавились от нескольких “устаревших” интерфейсов, которые использовались на протяжении десятилетий с самого момента появления “персоналок” IBM. Один из таких интерфейсов — порт PS/2 для подключения мыши и клавиатуры. В результате подобного перехода рынок наводнило огромное множество “старых” клавиатур, и даже новые клавиатуры под порт PS/2 продаются по сильно заниженным ценам. Это дает нам прекрасную воз- можность оснастить наши будущие Р1С24-проекты мощным средством ввода. Кро- ме того, мы получаем повод исследовать альтернативные методы построения ин- терфейсов и проанализировать их достоинства и недостатки. Итак, дальше мы зай- мемся разработкой программных конечных автоматов, освежим наши познания о прерываниях и изучим некоторые новые периферийные модули. Полет Физический порт PS/2 использует пя- тиконтактный разъем DIN или шестикон- тактный Mini-DIN. Первый из них был распространен в старых компьютерах IBM PC серий XT и АТ и уже давно вышел из обихода. А вот уменьшенная шестикон- тактная версия используется и по сей день. Хотя расположение выводов в этих двух разъемах различается (рис. 11.1), их элек- Пятиконтактный разъем DIN (АТ/ХТ): Штепсель 1 - Такт 2-Данные 3-NC 4 -"Земля* 5-Vcc (+5 В) Шестиконтактный разъем Mini-DIN (PS/2): трические характеристики идентичны. Хост должен обеспечить напряжение питания 5 В. Потребление тока варьирует- ся в зависимости от модели и года выпус- ка клавиатуры, однако обычно находится в диапазоне 50.. 100 мА (в оригинальной спецификации даже сказано, что макси- мальный предел составляет 275 мА). 1 - Данные 2-NC 3 - "Земля" 4-Vcc (+5 В) 5 - Такт 6-NC Рис. 11.1. Физический интерфейс порта PS/2 Линии данных и тактового сигнала — с открытым коллектором и подтягиваю- щими резисторами (1-10 кОм), что обеспечивает двунаправленную передачу. В нор- мальном режиме работы обеими линиями управляет' клавиатура, передающая дан- ные в ПК. Тем не менее, в случае необходимости компьютер может перехватить управление для конфигурирования клавиатуры или изменения состояния светодио- дов “Caps” и “Num Lock”. Протокол обмена данными через порт PS/2 В ждущем режиме в линиях данных и синхронизации с помощью подтягиваю- щих резисторов (находятся внутри клавиатуры) поддерживается сигнал высокого уровня. В таком состоянии клавиатура активна и готова к передаче данных по нажа- тию клавиши. Если хост удерживает' тактовую линию в состоянии низкого уровня более 100 мкс, то любые последующие передачи от клавиатуры приостанавливают-
Полет 159 ся. Если хост, удерживая линию данных в состоянии низкого уровня, освобождает линию синхронизации, то это интерпретируется как запрос на передачу команды (рис. 11.2). Такт Данные Бит 0 Бит 2 Бит 4 Бит 6 Контроль четности Старт Бит 1 Бит 3 Бит 5 Бит 7 Рис. 11.2. Форма сигналов при обмене данными между клавиатурой и хостом Этот протокол — интересная смесь синхронной и асинхронной передачи дан- ных, рассмотренных в предыдущих главах. Он синхронный, поскольку использует- ся тактовая линия, но в то же время подобен асинхронному протоколу ввиду огра- ничения восьмибитного пакета стартовым и стоповым битами с добавлением бита контроля четности. К сожалению, фактическая скорость передачи данных не стан- дартизована и может изменяться в зависимости от устройства, температуры и фазы лупы. Типичные значения колеблются в диапазоне 10.. 16 Кбит/с. Данные изменяют- ся в тот момент, когда тактовая линия находится в состоянии высокого уровня. Данные корректны, когда в тактовой линии — сигнал низкого уровня. Независимо от направления передачи за синхронизацию всегда отвечает клавиатура. ПРИМЕЧАНИЕ В случае с шичой USB роли меняются местами, поскольку каждое периферийное устройство является ведомым по отношению к хосту, формирующему сигнал син- хронизации. Это значительно все упрощает при работе с операционной системой, функционирующей не в реальном времени с бесприоритетной многозадачностью (например, Windows®). Последовательный и параллельный порты также были асин- хронными интерфейсами и, наверное, по этой же причине с появлением специфи- кации шины USB оба они ушли в небытие. Взаимодействие микроконтроллера PIC24 с портом PS/2 Благодаря некоторым уникальным особенностям протокола, взаимодействие с клавиатурой PS/2 становится довольно сложной задачей, поскольку нельзя ис- пользовать пи интерфейс SPI, ни модуль UART микроконтроллера PIC24. Интер- фейс SPI не поддерживает 11-разрядные слова (допускаются только разрядность 8 или 16), a UART требует периодической передачи особых символов-разделителей, необходимых для использования такой мощной возможности как автоматическое определение скорости обмена данными. Кроме того, следует отметить, что протокол PS/2 основан па сигналах уровня 5 В. Это требует осмотрительности в выборе вы- водов, напрямую подключаемых к микроконтроллеру PIC24. По сути, можно ис- пользовать только цифровые входы, поддерживающие уровень 5 В, что исключает порты ввода-вывода, мультиплексированные с АЦП. Захват на входе Первое, что приходит на ум, — реализовать последовательный обмен данными через порт PS/2 программно с помощью механизма захвата па входе (Input Capture).
160 Глава 11. Фиксация входных данных От 16-разрядных тамероа TMRy TMRx Установка флага ICxIF (в регистре IFSn) Примечание: Буква "х* а названии сигнала, регистра или разряда обозначает номер канала фиксации Рис. 11.3. Структурная схема модуля захвата на входе Микроконтроллер PIC24FJ128GA010 оснащен пятью модулями Input Capture, которые соединены соответственно с выводами IC1-IC5, мультиплексированными на выводы 8, 9, 10, 11 и 12 порта D. Каждый такой модуль управляется с помощью отдельного регистра ICxCON в комбинации с одним или двумя таймерами (Timer2 и Timer3). Захват на входе срабатывает по одному из следующих событий: • нарастающий фронт; • ниспадающий фронт; • нарастающий и ниспадающий фронт; • четвертый нарастающий фронт; • шестнадцатый нарастающий фронт. Текущее значение выбранного таймера фиксируется и сохраняется в FIFO-бу- фере для извлечения путем чтения соответствующего регистра ICxBUF. В добавок ко всему, после запрограммированного ко- личества событий активации захвата (каж- дый раз, каждое второе, третье или четвер- тое событие) может быть выдан запрос на прерывание. Для активизации периферийного мо- дуля Input Capture и получения потока дан- ных от клавиатуры PS/2 мы можем соеди- нить вход IC1 с тактовой линией и сконфи- гурировать модуль па прерывание по каж- дому ниспадающему фронту импульса синхронизации (рис. 11.4). Ниспадающий фронт Событие захвата на входе Рис. 11.4. Сигналы интерфейса PS/2 и возникновение события захвата на входе
Полет 161 Следуя привычной схеме, создайте новый проект и добавьте в него представ- ленный ниже код инициализации: ^define. PS2DATA _RG12 // вход, поддерживающий напряжение 5 В #deflne PS2CLOCK _RD8 // Вход ТС1 модуля Input Capture vc-Lci initKBD(void) { // Обнуляем флаг KBDReady = 0; _TRISD8 = 1; _TRISG12 = 1; IC1CON = 0x0002; -IC1IF = 0; _IC1IE = 1; } // void initKBD // Вывод IC1 = RD8 - вход (тактовый сигнал) // Вывод RG12 - вход (данные) // Используем TMR3, прерывание по каждому // захвату по ниспадающему фронту // Обнуляем флаг прерывания // Разрешаем прерывание по входу IC1 Нам также понадобится подпрограмма обслуживания прерывания для вектора IC1. Она должна выполнять роль конечного автомата, производя следующую по- следовательность действий. 1. Проверка наличия стартового бита (низкий уровень сигнала в линии данных). 2. Прием восьми бит данных в сдвиговой регистр и вычисление бита контроля четности. 3. Проверка корректности бита контроля четности. 4. Проверка наличия стоп-бита (высокий уровень сигнала в линии данных). Если какой-либо из этих этапов завершается неудачно, конечный автомат дол- жен сформировать сигнал сброса и вернуться в исходное состояние. В случае прие- ма корректного байта данных он сохраняется в буфере, и устанавливается соответ- ствующий флаг, что указывает главной программе или любой подпрограмме на то, что код клавиши готов к выборке. Для извлечения этого когда достаточно скопиро- вать его из буфера с последующим обнулением флага (рис. 11.5). Линия данных - Рис. 11.5. Диаграмма работы конечного автомата, принимающего данные из порта PS/2
162 Глава 11. Фиксация входных данных Конечный автомат требует только четыре состояния и счетчик, а все его пере- ходы сведены в табл. 11.1. Таблица 11.1. Таблица переходов конечного автомата, принимающего данные из порта PS/2 Состояние Условия Действие Старт Линия данных — низкий уровень Инициализация счетчика битов, инициализация бита контроля четности, переход в состояние “Бит" Бит Счетчик битов < 8 Приема бита в сдвиговой регистр (первым поступает младший разряд, сдвиг вправо), обновление бита кон- троля четности, увеличение счетчика битов Счетчик битов = 8 Переход в состояние “Четность" Четность Четность — чет Ошибка. Возврат в состояние “Старт" Четность — нечет Переход в состояние “Стоп" Стоп Линия данных — низкий уровень Ошибка. Возврат в состояние “Старт" Линия данных — высокий уровень Сохранение кода клавиши в буфере, установка флага, переход в состояние “Старт” С теоретической точки зрения этот конечный автомат должен иметь 11 состоя- ний, учитывая каждый вход в состояние “Бит” с уникальным значением счетчика битов, однако модель с четырьмя состояниями более эффективна с точки зрения реализации на языке С. Определим несколько констант и переменных, необходимых для функционирования конечного автомата: // Определения конечного автомата для клавиатуры PS/2 #define PS2START О #define PS2BIT 1 #define PS2PARITY 2 ttdefine PS2STOP 3 // Конечный автомат и буфер int PS2State; unsigned char KBDBuf; // Временный буфер int KCount, KParity,; // Сетчик битов и бит контроля четности // Флаг готовности кода клавиши и буфер volatile int KBDReady; volatile unsigned char KBDCode; ПРИМЕЧАНИЕ ____ Ключевое слово volatile применяют в объявлении переменных как модификатор, извещающий компилятор о том, что содержимое переменной может изменяться не- предсказуемым образом вследствие прерывания или действия другого аппаратного механизма. В данном случае мы используем его для предотвращения какой-либо оптимизации (извлечение циклов, абстрактные процедуры и т.д.) со стороны компи- лятора, когда задействованы переменные KBDReady и KBDCode. По большому сче- ту, в этом примере мы могли бы ключевое слово volatile и не указывать (в конце концов, вся оптимизация отключается в ходе отладки), однако в будущем попытка использовать данный код в более сложных проектах привела бы к серьезным про- блемам. Переменные KBDReady и KBDcode — единственные, задействованные как в подпрограмме обслуживания прерывания, так и в основном коде интерфейса.
Полет 163 Подпрограмму обслуживания прерывания для модуля захвата на входе IC1 можно реализовать с помощью простого оператора switch, охватывающего всю функциональность конечного автомата. voir _ISR —IClInterrupt(void) { / Подпрограмма обслуживания прерывания по захвату на входе switch(PS2State){ default: case PS2START: if (1PS2DAT) { KCount =8; // Инициализируем счетчик битов KParity =0; // Инициализируем бит контроля четности PS2State = PS2BIT; } break; case PS2BIT: KBDBuf >>= 1; // Сдвигаем бит данных if (PS2DAT) KBDBuf += 0x80; KParity Л= KBDBuf; // Обновляем бит контроля четности if (—KCount == 0) // Если прочитаны все биты, переходим PS2State = PS2PARITY; break; case PS2PARITY: if (PS2DAT) KParity Л= 0x80; if (KParity & 0x80) //Если бит контроля нечетный, продолжаем PS2State = PS2STOP; else PS2State = PS2START; break; case PS2STOP: if (PS2DAT) // Проверяем стоп-бит { KBDCode = KBDBuf; // Сохраняем код клавиши в буфере KBDReady = 1; / / Устанавливаем флаг готовности кода } PS2State = PS2START; break; } // switch // Сбрасываем флаг прерывания IC1IF = 0; } // Прерывание от IC1 Тестирование метода захвата на входе с помощью сценариев стимулов Для подсоединения разъема PS/2 Mini-DIN к демонстрационной плате Explo- rer 16 можно воспользоваться небольшой перфорированной областью моделирова- ния. Единственная альтернатива этому методу — разработка дополнительной платы (PICTail™) для подключения в разъем расширения. Впрочем, прежде, чем присту- пить к проектированию такой платы, необходимо удостовериться в правильном ис-
164 Глава 11. Фиксация входных данных пользовании выводов и корректности кода. В это нам опять поможет программный имитатор MPLAB® SIM. В предыдущих главах для проверки выходных значений мы использовали ими- татор совместно с окнами Watch, Stopwatch и Logic Analyzer, однако на этот раз нам понадобятся еще и сымитировать входные сигналы. Для этого MPLAB SIM предос- тавляет немало различных возможностей и ресурсов. По сути, их так много, что по- началу это множество выглядит даже пугающим. Прежде всего; имитатор поддерживает два типа входных стимулов: • асинхронные — обычно формируются вручную самим пользователем; • синхронные — формируются автоматически имитатором после заданного сце- нарием промежутка времени, выраженного в циклах процессора или секундах. Файлы сценариев . scl, содержащие описание синхронных стимулов (могут быть довольно сложными), готовят с помощью специального средства под называ- нием “SCL-генератор”. Его можно вызвать с помощью команды меню Debugger ► SCL Generator ► New Workbook. Для подготовки простейшего сценария стимулов, ко- торый в заданные моменты времени назначает некоторые значения определенным входам и целым регистрам, в окне SCL Workbook следует выбрать вкладку Pin/Regis- ter Actions. Выбрав в раскрывающемся списке Time Units единицы измерения времени (в нашем случае — микросекунды), щелкните мышью на заголовке столбца Click here to Add Signals (Щелкните здесь, чтобы добавить сигналы). Далее с помощью диалогового окна Add/Remove Pin/Registers добавьте в таблицу столбцы для каждого входа, участвующего в процессе имитации: RG12 (линия данных PS/2) и IC1 (вывод модуля Input Capture, который необходимо соединить с тактовой линией PS/2). По- сле этого можно приступать к формированию таблицы стимулов. Для имитации ти- пичной передачи данных от клавиатуры PS/2 нам потребуется 11 циклов тактового сигнала частотой 10 кГц (см. рис. 11.4). Таким образом, в таблице стимулов события должны следовать с периодом 50 мкс. Пример такой таблицы для имитации переда- чи кода клавиши 0x79 представлен на рис. 11.6 и в табл. 11.2 Рис. 11.6. Окно SCL-генератора
Полет 165 Таблица 11.2. Пример таблицы SCL-генератора для простой PS/2 -имитации Время (мкс) RG12 IC1 Пояснение 0 1 1 Состояние ожидания, обе линии “подтянуты" резисторами 100 1 1 150 0 0 Первый ниспадающий фронт, стартовый бит (0) 200 1 1 250 1 0 Бит 0, младший разряд кода клавиши (1) 300 0 1 350 0 0 Бит 1 (0) 400 0 1 450 0 0 Бит 2(0) 500 1 1 550 1 0 Бит 3(1) 600 1 1 650 1 0 Бит 4(1) 700 1 1 750 1 0 Бит 5(1) 800 1 1 850 1 0 Бит 6 ( 1) 900 0 1 950 0 0 Бит 7, старший разряд кода клавиши (0) 1000 0 1 1050 0 0 Бит контроля четности (0) 1100 1 1 1150 1 0 Стоп-бит (1) 1200 1 1 Состояние ожидания Как только таблица SGL-генератора заполнена, ее содержимое можно сохра- нить для использования в будущем. Для этого необходимо нажать кнопке Save Workbook. В результате будет сформирован ASCII-файл с расширением . sbs. Тео- ретически его можно редактировать вручную с помощью редактора интегрирован- ной среды MPLAB или любого текстового редактора, однако делать это настоятель- но не рекомендуется. Формат файла . sbs — очень строгий и его запросто можно нарушить, даже этого не заметив. Если у кого-то возник вопрос, почему простую таблицу называют “Рабочей книгой” (“Workbook”), предлагаю исследовать осталь- ные вкладки диалогового окна SCL Workbook. Как видим, в нашем примере мы ис- пользуем только один из множества доступных методов формирования стимулов, что является лишь малой толикой возможностей SCL-генератора. Файл рабочей книги может содержать разнотипные стимулы, определяемые па одной или нескольких вкладках диалогового окна SCL Workbook (ниже показан фрагмент файла .scl). # # SCL Builder Setup File: Do not edit!! И VERSION: 3.22.00.00 И FORMAT: vl.40.00 # # DEVICE: PIC24FJ128GA010 # # PINREGACTIONS us No Repeat
166 Глава 11. Фиксация входных данных RG12 IC1 О 1 1 100 1 1 150 О О 200 1 1 Фактически сценарий стимулов можно уже получить на основании таблицы, которую мы только что сформировали. Он будет содержать реальные команды и информацию, используемую MPLAB SIM для имитации входных сигналов. Рас- смотрим фрагмент файла стимулов: // // .../IC PS2 simulation.scl // Generated by SCL Generator ver. 3.22.00.00 // DATE TIME // configuration for "pic24fj128ga010” is end configuration; testbench for "pic24fj128ga010" is begin process is begin wait for 0 us; report ’’Stimulus actions after 0 us"; RG12 <= '1'; IC1 <= ’1’; wait; end process; process is begin wait for 100 us; report "Stimulus actions after 100 us"; RG12 <= ’1’; IC1 <= ’1'; wait; end process; Можно отметить определенное сходство синтаксиса SCL-файла и некоторых языков описания аппаратных средств (VHDL). И наверное, это не случайно. Такой структурированный формат, по сути, специально предназначен для обеспечения гибкости описания стимулов и быстрого протекания имитации.
Полет 167 Тестирование подпрограмм приема данных через порт PS/2 Прежде, чем мы сможем воспользоваться полученным файлом стимулов, необ- ходимо внести несколько последних штрихов в проект. Выделим подпрограммы приема данных через порт PS/2 в отдельный модуль (назовем его PS2IC. с). Не за- будьте включить этот файл в проект (щелчок правой'кнопкой мыши в окне редакто- ра и выбор команды Add to Project контекстного меню). ©Файл PS2IC.C находится также на прилагаемом к книге !компакт-диске в папке Проекты\11 - Фиксация входных даных\1С. Кроме того, подготовим заголовочный файл, содержащий объявления функции initKBD (), флага KBDReady и буфера KBDCode для приема кода клавиши: /* * * ** PS2lC.h ** PS/2 keyboard input library using input capture */ extern volatile int KBDReady; extern volatile unsigned char KBDCode; void initKBD( void); Обратите внимание, что объявлять для всеобщего использования какие-либо другие детали внутренней реализации приемника PS/2 не нужно. Это дает нам сво- боду испытывать в будущем новые методы без необходимости вносить изменения в интерфейс. Сохраните заголовочный файл под именем PS2IC.h и включите его в проект. ©Файл PS2ic.h находится также на прилагаемом к книге компакт-диске в папке Проекты\11 - Фиксация входных даных\1С. Кроме того, создадим файл PS2ICTest. с, содержащий главную подпрограм- му и использующий модуль PS2IC для тестирования функциональности: 7* ** Тестирование клавиатуры PS/2 ★ ★ */ #include <p24fj128ga010.h> #include ”PS2IC.h” main () X TRISA = OxffOO; initKBD(); while (1) // Вызываем подпрограмму инициализации t if (KBDReady) // Ожидаем установки флага PORTA = KBDCode; // Извлекаем код клавиши и выдаем его в порт А KBDReady = 0; // Сбрасываем флаг } // Главный цикл } //main
168 Глава 11. Фиксация входных данных ©Файл PS2lCTest.c находится также на прилагаемом к книге компакт-диске в папке Проекты\ 11 - Фиксация входных даных\1С. Здесь мы инициализируем младшие восемь линий порта А (на плате Explorer 16 соединены со светодиодами) как выходы и вызываем подпрограмму инициализации клавиатуры PS/2, которая в свою очередь инициализирует' выбранные входы, ко- нечный автомат и прерывания по захвату на входе. В главном цикле реализовано ожидание установки флага подпрограммой об- служивания прерывания, извлечение кода клавиши и его индикация с помощью све- тодиодов. В завершение флаг обнуляется, указывая на готовность к приему нового символа. Не забудьте добавить файл PS2lCTest.c в проект и выбрать команду меню Project ► Build All. Имитация Вместо того, чтобы сразу же активизировать имитацию, откройте подменю Debugger ► Stimulus Controller (Контроллер стимулов) (рис. 11.7). Выберите команду New Scenario (Новый сцена- рий), чтобы открыть диалоговое окно Stimulus Cont- roller (рис. 11.8). Это окно позволяет прикреплять к проекту сценарии синхронных стимулов, создан- ных с помощью SCL-генератора, и добавлять к ним “асинхронные стимулы”, активизируемые с помо- щью кнопок Fire, расположенных в левом столбце таблицы Stimulus Controller. Нажмите кнопку Attach (Прикрепить) и выбери- те сформированный ранее файл .scl. Теперь по- лученный “сценарий” можно сохранить для даль- нейшего использования, однако в нашем случае в этом нет смысла, поскольку мы имеем дело толь- Select Tool Clear Memory ► I Animate ;:C j Step Into F7 Step Over FS | ' Reset ► breakpoints.,. F2 StopWatch Stimulus Controller »I New Scenario SCL Generator ► Open Scenario Profile ► Save Scenario Refresh PM Close Scenario Settings.,, Puc. 11.7. Подменю Stimulus Controller ко с одним SCL-файлом и не будем больше создавать каких-либо асинхронных сти- мулов. Рис. 11.8. Диалоговое окно Stimulus Controller
Полет 169 ПРИМЕЧАНИЕ ________~ Диалоговое окно Stimulus Controller необходимо держать открытым (в фоне). Не нажимайте кнопку Exit (Выйти), поскольку это приведет к закрытию сценария и оста- вит нас без стимулов! И вот он, момент истины! Нажмите кнопку Reset (или выберите команду меню Debugger ► Reset) и понаблюдайте за активизацией первого стимула, соответствую- щего отметке 0 мкс. Напоминаем, что согласно таблице SCL-генератора он предпо- лагает' установку линий RG12 и IC1 в состояние высокого уровня. Об активизации стимула можно узнать по сообщению на вкладке MPLAB SIM окна Output (рис. 11.9). , __ Budd | Vcf$ion Contiol | Fbd in Files МТЬДВ SIM j МР1ДВ ICD 21 i SIM-N0001 Note: Stimulus actions after 0 us Puc. 11.9. Вклада MPLAB SIM окна Output содержит сообщение об активизации стимула Теперь можете на свой выбор пройтись по программе в пошаговом режиме или в режиме “Animate”, чтобы проверить корректность ее выполнения. Я рекомендую установить точку прерывания внутри главного цикла сразу же после оператора, ко- пирующего KBDCode в порт А. Затем откройте окно Watch, выберите в списке SFR элемент PORTA и запустите программу на выполнение. Через несколько секунд сра- ботает точка прерывания, и регистр PORTA будет содержать данные, переданные по линиям порта PS/2 в режиме имитации (т.е. значение 0x7 9). Профиль имитатора Тем, кого интересует скорость имитации работы микроконтроллера PIC24 на компьютере, в меню De- bugger для отладчика MPLAB SIM присутствует пункт Profile. Вначале выберите в этом подменю ко- манду Reset Profile (Сброс профиля) (рис. 11.10) для обнуления счетчиков и таймеров имитатора. Затем удалите все точки прерывания и на несколько се- кунд активизируйте имитацию по команде меню De- bugger ► Run. Наконец, приостановите имитацию с помощью команды меню Debugger ► Halt и выбе- рите команду меню Debugger ► Profile ► Display Profi- le. В результате па вкладке MPLAB SIM окна Output появится относительно длинный отчет о том, сколь- ко раз использовалась процессором каждая команда в ходе имитации (рис. 11.11). В завершение отчета указана оценка абсолют- Select Tool ► : Clear Memory > Run F9 Animate • V- "" Step Into F7 Step Over F8 Reset > Breakpoints... F2 Stopwatch Stimulus Contriver ► SCL Generator ► Profile ► | Reset Profile Refresh PM Display Profile Settings... Puc. 11.10. Подменю Profile ной скорости имитации. В примере, показанном на рис. 11.11, опа составила целых 2,7 миллиона операций в секунду. Это означает, что на моем ноутбуке программный имитатор работает со скоростью, примерно составляющей одну шестую часть фак- тической скорости процессора. Должен сказать, совсем неплохо!
170 Глава 11. Фиксация входных данных Рис. 11.11. Профиль имитатора Второй метод: уведомление об изменении сигнала Несмотря па то, что захват на входе — вполне эффективная мегодика, сущест- вуют и другие варианта организации эффективного взаимодействия с клавиатурой PS/2. В частности, для этого можно воспользоваться еще одним интересным пери- ферийным модулем, которым оснащены микроконтроллеры PIC24: Change Notifica- tion (CN, “уведомление об изменениях”). В его распоряжении — целых 22 вывода, что дает нам определенную свободу в выборе идеального распределения сигналов при взаимодействии с интерфейсом PS/2 с гарантированным отсутствием конфлик- тов с другими функциями, которые требуются в проекте или уже задействованы на плате Explorer 16. С модулем CN связаны всего лишь четыре управляющих регистра (табл. 11.3). Регистры CNEN1 и CNEN2 содержат разряды разрешения прерываний для каждого из входов CN. Обратите внимание на то, что для всего модуля CN используется только один вектор прерываний, поэтому задача определения входа, на котором из- менился уровень сигнала, возлагается па подпрограмму обслуживания прерывания. Таблица 11.3. Управляющие регистры модуля CN Регистр \дрсс Бит 15 Бит 14 Бит 13 Бит 12 Бит 11 Бит 10 Бит 9 Бит 8 Бит 7 Бит 6 Бит 5 Бит 4 БитЗ Бит 2 Бит 1 БИГО CNEN1 0060 CN15IE CN14IE CN13IE CN12IE CN11IE CN10IE CN9IE CN8IE CN7IE CN6IE CN5IE CN4IE CN3IE CN2IE CN1IE CN0IE CNEN2 0062 ' — • — . :: . •• . • CN21IE CN20IE CN19IE CN18IE CN17IE CN16IE CNPU1 0068 CN15PUE CN14PUE CN13PUE CN12PUE :nhpue 3N10PUE CN9PUE CN8PUE 3N7PUE 2N6PUE CN5PUE CN4PUE CN3PUE CN2PUE CN1PUE CN0PUE CNPU2 006Л < В —- - CN21PUE CN20PUE CN19PUE CN18PUE CN17PUE CN16PUE "—" означает нереализованный разряд, который считывается как “0". С каждым выводом модуля CN связан подтягивающий резистор, выполняющий роль источника тока, благодаря которому отпадает необходимость во внешних ре- зисторах при подключении кнопочного или клавишного устройства. Эти подтяги- вающие резисторы активизируют с помощью регистров CNPU1 and CNPU2, содер- жащие управляющие разряды для каждого из выводов модуля CN. Установка одно- го из управляющих разрядов активизирует подтягивающий резистор для соответст- вующего вывода. На практике все, что нам необходимо для поддержки интерфейса PS/2, — толь- ко один из входов CN, соединенный с тактовой линией PS2. В данном случае подтя- гивающий резистор микроконтроллера PIC24 не требуется, поскольку он уже встро- ен в клавиатуру. Нам необходимо выбрать один из 22 входов CN, который не используется АЦ11 (как помните, нам требуется вывод с поддержкой напряжения 5 В) и не задейство- ван под какую-либо другую периферию демонстрационной платы Explorer 16. Для этого придется немного покопаться в спецификации устройства и руководстве по Explorer 16. Как только вывод выбран (например, CN11, мультиплексированный
Полет 171 с выводом 9 порта G, линией SS модуля SPI2 и адресной линией 2 модуля РМР), можно создать простую подпрограмму инициализации: tfdefine PS2CLOCK JRG9 // Вход CN11 #define PS2DAT _RG12 // Любой вход, поддерживающий 5 В void initKBD(void) { // Клавиатура PS/2 CNEN1 = 0x0800; // Разрешаем уведомление об изменении сигнала // на входе CN11 _CNIF =0; // Обнуление флага прерывания _CNIE =1; // Разрешаем прерывание по уведомлению об // изменении сигнала } // initKBD Что касается подпрограммы обслу- живания прерывания, то мы можем ис- пользовать в точности тот же конечный автомат, что и в предыдущем примере, добавив только пару строк кода, гаранти- рующих отслеживание ниспадающего фронта тактового сигнала (рис. 11.12). По сути, при использовании модуля Input Capture мы можем реализовать пре- рывание только по одному из фронтов тактового импульса, в то время как мо- дуль Change Notification позволяет сде- лать это сразу для двух фронтов. Для распознавания фронта необходимо про- сто проверить состояние тактовой линии служивапия прерывания: Рис. 11.12. Сигналы интерфейса PS/2 в случае с модулем Change Notification же после входа в подпрограмму об- void _ISR _CNInterrupt(void) { // Подпрограмма обслуживания прерывания по уведомлению об изменении // сигнала // Удостоверяемся, что это был ниспадающий фронт if (PS2CLK == 0) ( // Конечный автомат приема сигнала из порта PS/2 switch(PS2State){ default: case PS2START: if (’PS2DAT) { KCount =8; // Инициализируем счетчик битов KParity =0; // Инициализируем бит контроля четности PS2State = PS2BIT; } break; case PS2BIT: KBDBuf >>=1; // Сдвигаем бит данных if (PS2DAT) KBDBuf += 0x80; KParity л= KBDBuf; // Обновляем бит контроля четности
172 Глава 11. Фиксация входных данных if (—KCount == 0) // Если все биты прочитаны, переходим PS2State = PS2PARITY; break; case PS2PARITY: if (PS2DAT) KParity Л= 0x80; if (KParity & 0x80) // Если нечет, то продолжаем PS2State = PS2STOP; else PS2State = PS2START; break; case PS2STOP: KBDBuf »= 1; if (PS2DAT) KBDBuf += 0x80; KParity Л= KBDBuf; if (—KCount == 0) PS2State = PS2PAR1 break; // Сдвигаем бит данных // // 1TY; Обновляем бит контроля четности Если все биты прочитаны, переходим } // switch } // if (проверка ниспадающего фронта) // Сбрасываем флаг прерывания _CNIF = 0; } // Прерывание от CN Добавим объявления констант и переменных, уже использованных в предыду- щем примере: #include <p24fj128ga010.h> #include "PS2CN.h" # define PS2DAT _RG12 // Вход данных PS2 # define PS2CLK _RG9 // Вход тактового сигнала PS2 / / Определение конечного автомата клавиатуры PS/2 # define PS2START 0 # define PS2BIT 1 # define PS2PARITY 2 # define PS2STOP 3 // Конечный автомат и буфер клавиатуры PS2 int PS2State; unsigned char KBDBuf; int KCount, KParity; // Буфер для хранения кода клавиши volatile int KBDReady; volatile unsigned char KBDCode; Скомпонуйте эти фрагменты кода в файл PS2CN. с. ©Файл PS2CN.C находится также на прилагаемом к книге компакт-дисчо с папке Проекты\11 - Фиксация входных даных\СЫ.
Полет 173 Заголовочный файл PS2CN.h будет почти идентичен файлу из предыдущего примера, поскольку 7*.......................................................................... * * PS2CN.h ★ * * * Ввод данных от клавиатуры PS/2 с помощью модуля Change Notification */ extern volatile int KBDReady; extern volatile unsigned char KBDCode; void initKBD( void); ©Файл PS2CN.h находится также на прилагаемом к книге компакт-диске в папке проекты\11 - Фиксация входных даных\СЬ1. Создайте новый проект под именем PS2CN и добавьте в него вышеупомянутые файлы .си . h. Наконец, создайте главный модуль для проверки новой методики. Опять-таки, он будет почти идентичным предыдущему проекту: 7*............................................................................ * * Тестирование клавиатуры PS/2 ★ ★ */ ^include <p24fj128ga010.h> #include "PS2CN.h" main () f t TRISA = OxffOO; initKBDO; // Вызов подпрограммы инициализации while (1) I if (KBDReady) // Ожидаем установки флага t PORTA = KBDCode; // Извлекаем код клавиши и выдаем его в порт А KBDReady =0; // Сбрасываем флаг } // Главный цикл } //main Сохраните проект, а затем откомпилируйте и скомпонуйте его модули с помо- щью команды меню Project ► Build АП. Для проверки методики уведомления об из- менении сигнала мы опять воспользуемся средствами формирования стимулов ими- татора MPLAB SIM, повторив большинство действий из нашего предыдущего про- екта. Активизируйте SCL-генератор (команда меню Debugger ► SCL Generator ► New Workbook) и создайте в диалоговом окне SCL Workbook два столбца: для линии дан- ных PS/2, соединенной с выводом RG12, и для тактовой линии PS/2, соединенной со входом CN11 модуля Change Notification. Добавьте в таблицу SCL-генератора ту же последовательность событий, что и в предыдущем примере, заменив столбец IC1 столбцом CN11. Сохраните рабочую кни- гу под именем PS2CN. sbs и нажмите кнопку Generate SCL From Workbook, чтобы создать файл сценария стимулов PS2CN. scl. Наконец, откройте диалоговое окно Stimulus Controller (команда меню Debugger ► Stimulus Controller ► New Scenario) и
174 Глава 11. Фиксация входных данных создайте в нем новый сценарий. Затем нажмите кнопку Attach и выберите файл PS2CN. scl для активизации имитации входных данных. При желании можеге со- хранить сценарий, по не закрывайте диалоговое окно Stimulus Controller (хотя его можно свернуть). Теперь мы готовы к выполнению кода и тестированию (в режиме имитации) ра- боты нового интерфейса PS/2. Откройте окно Watch и добавьте в его список регистр PORTA. Затем установите точку прерывания внутри главного цикла сразу же после оператора копирования кода клавиши в регистр PORTA. Наконец, выполните сброс (команда меню Debugger ► Reset) и удостоверьтесь в том, что произошло первое со- бытие (установка высокого уровня сигнала на обеих входных линиях PS/2 в момент времени 0 мкс). Запустите программу на выполнение (команда меню Debugger ► Run). Если все было сделано правильно, то процессор менее, чем через секунду, ос- тановится на точке прерывания, а регистр PORTA будет содержать код клавиши 0x7 9. С чем вас и поздравляю! Сравнительная оценка Как нетрудно убедиться, переход от метода захвата па входе к методу уведом- ления об изменении сигнала не составляет особых проблем. Модули Input Capture и Change Notification чрезвычайно эффективны и, хотя и предназначены для разных целей, применительно к нашей задаче действуют почти идентично. Тем не менее, в мире встроенных систем необходимо постоянно искать наименее дорогостоящий вариант из нескольких (даже на первый взгляд равнозначных) альтернатив. Давайте оценим реальные расходы на каждое из рассмотренных решений, подсчитав задей- ствованные ресурсы. В случае с захватом на входе мы, по сухи, используем один из пяти модулей IC, доступных в модели PIC24FJ128GA010. Эта периферия предназначена для совмест- ной работы с таймером (Timer2 или Timer3), хотя в пашем примере мы пе использо- вали никакой информации о времени, а только — механизм прерывания по распо- знанию фронта входного импульса. При работе с модулем Change Notification мы использовали только один из 22 входов, однако при этом потребовалось контролировать единственный вектор пре- рываний. Другими словами, если понадобится управлять еще каким-либо выводом этого модуля, то мы будем вынуждены организовать совместное использование вектора прерываний, добавляя задержки и усложняя программу. Следовательно, этот вариант менее предпочтительный. Третий метод: опрос портов ввода-вывода Существует еще один метод взаимодействия с клавиатурой PS/2, который имеет смысл рассмотреть. Он — наиболее базовый и подразумевает' использование тайме- ра, настроенного для периодического формирования запросов на прерывание. При этом в качестве входов можно использовать любые (поддерживающие напряжение 5 В) порты ввода-вывода микроконтроллера. Таким образом, данный метод наибо- лее гибок с точки зрения конфигурирования и макетирования. Кроме того, он — са- мый универсальный, поскольку любой микроконтроллер (даже самый миниатюр- ный и дешевый) содержит как минимум один таймер. Его суть очень проста: через равные промежутки времени, заданные с помощью значения регистра периода вы- бранного таймера, возникает прерывание (рис. 11.13).
Полет 175 Периодический опрос Тактовая линия Линия данных Корректны данные Рис. 11.13. Сигналы интерфейса PS/2 при регулярном опросе портов ввода-вывода На этот раз мы остановим свой выбор на таймере Timer4, поскольку до этого еще ни разу его не использовали. Таким образом, в качестве регистра периода вы- ступает PR4. Подпрограмма обслуживания прерывания Т4Interrupt опрашивает состояние тактовой линии PS/2 на предмет наличия ниспадающего фронта в течение предыдущего периода. Если такой фронт обнаружен, считывается состояние линии данных. Для того чтобы определить частоту опроса (т.е. выяснить оптимальное значение регистра PR4), необходимо рассмотреть кратчайший промежуток времени, допус- тимый между двумя фронтами в тактовой линии PS/2. Он задается максимальной скоростью передачи, установленной для интерфейса PS/2, которая согласно доку- ментации составляет около 16 Кбит/с. При такой скорости тактовый сигнал может быть представлен в виде последовательности прямоугольных импульсов с рабочим циклом примерно 50% и периодом около 62,5 мкс. Другими словами, каждый раз, когда в линии данных находится бит, в тактовой линии чуть дольше 30 мкс удержи- вается сигнал низкого уровня, и примерно столько же времени, за которое поступа- ет следующий бит, — сигнал высокого уровня. Записав в регистр PR4 значение, со- ответствующее периоду прерывания менее 30 мкс (например, 25 мкс), мы гаранти- руем, что тактовая линия будет опрошена между двумя последовательными фрон- тами как минимум один раз. Впрочем, скорость передачи от клавиатуры может составлять всего 10 Кбит/с, что дает нам максимальное расстояние между фронтами около 50 мкс. В таком слу- чае опрос линий синхронизации и данных между двумя соседними фронтами будет происходить два-три раза. Другими словами, необходимо создать конечный автомат для обнаружения фактической позиции ниспадающего фронта и корректного от- слеживания тактового сигнала PS/2 (рис. 11.14). Такт = 0, ниспадающий фронт Рис. 11.14. Диаграмма конечного автомата для опроса тактового сигнала
176 Глава 11. Фиксация входных данных Конечный автомат требует только двух состояний, а все переходы сведены в табл. 11.4. Таблица 11.4. Таблица переходов конечного автомата для опроса тактового сигнала Состояние Условия Действие Состояние 0 Такт = 0 Остаемся в состоянии 0 Такт = 1 Нарастающий фронт, переход в состояние 1 Состояние 1 Такт = 1 Остаемся в состоянии 1 Такт = 0 Обнаружен ниспадающий фронт, активизируем конечный авто- мат чтения данных, переходим в состояние 0 При обнаружении ниспадающего фронта мы можем использовать для чтения данных тот же конечный автомат, что и в предыдущих проектах. Важно отметить, что в этом случае значение в линии данных не обязательно опрашивается сразу же после фактической позиции ниспадающего фронта в тактовой линии. Между этими двумя точками могут быть значительные задержки. Во избежание чтения за преде- лами допустимого периода, линии синхронизации и данных необходимо обязатель- но опрашивать одновременно. Согласно спецификации интерфейса PS/2, если в так- товой линии — низкий уровень сигнала, то данные можно считать корректными. На практике это требование выливается в необходимость назначать в качестве входов для синхросигнала и данных выводы одного и того же порта. В нашем примере для тактовой линии мы опять выберем вывод RG12, а для линии данных — вывод RG15. Таким образом, копирование содержимого регистра PORTG во временную переменную после входа в подпрограмму обслуживания прерывания создаст микро- задержку и даст идеальную синхронизацию опроса двух линий. Ниже представлена простейшая реализация конечного автомата для опроса так- товой линии, показанного на рис. 11.14. #define PS2DAT _RG12 // Вход для данных PS/2 tfdefine PS2CLK _RG15 // Вход для тактового сигнала PS/2 ^define CLKMASK 0x8000 // Маска для обнаружения тактовой линии tfdefine DATMASK 0x1000 // Маска для обнаружения линии данных unsigned char KBDBuf; int KState; // Буфер volatile int KBDReady; volatile unsigned char KBDCode; void _ISR _T4Interrupt(void) { int PS2IN; // Опрашиваем входы одновременно PS2IN = PORTG; // Конечный автомат опроса тактового сигнала от клавиатуры if (KState) { // Предыдущий сигнал был высокого уровня, состояние 1 if (! (PS2IN & CLKMASK)) // PS2CLK = 0 { // Обнаружен ниспадающий фронт KState =0; // Переход в состояние 0 <<<... Здесь размещается конечный автомат чтения данных!>>>
Полет 177 } // Ниспадающий фронт else { // Такт все еще = 1, остаемся в состоянии 1 } // Такт все еще = 1 } // Состояние 1 else { // Состояние О if (PS2IN & CLKMASK) // PS2CLK = 1 { // Обнаружен нарастающий фронт KState =1; // Переходим в состояние 1 } // Нарастающий фронт else { // Такт все еще = 0, остаемся в состоянии О } // Такт все еще = О } // Состояние О // Сбрасываем флаг прерывания _T4IF = 0; } // Прерывание от Т4 Благодаря периодичности опросов, мы можем, приложив минимум усилий, до- бавить функциональность, которая сделает взаимодействие с интерфейсом PS/2 бо- лее падежным. Прежде всего, можно реализовать счетчик циклов ожидания для обоих состояний конечного автомата опроса тактового сигнала. Это позволит от- слеживать слишком длительные задержки, вызванные сбоем (например, отсоедине- нием клавиатуры во время передачи данных или потерей синхронизации подпро- граммой приема). Новая таблица переходов (табл. 11.5) включает счетчик задержки Ktimer. Таблица 11.5. Таблица переходов конечного автомата для опроса тактового сигнала (с отслеживанием задержки) Состояние Условия Действие Состояние 0 Такт = 0 Остаемся в состоянии 0, декрементируем Ktimer. Если Ktimer = 0, то ошибка — сброс конечного автомата чтения данных Такт = 1 Нарастающий фронт, переход в состояние 1 Состояние 1 Такт = 1 Остаемся в состоянии 1, декрементируем Ktimer. Если Ktimer = 0, то ошибка — сброс конечного автомата чтения данных Такт = 0 Обнаружен ниспадающий фронт, активизируем коночный авто- мат чтения данных, переходим в состояние 0, повторная инициа- лизация Ktimer Новая таблица переходов добавляет' в нашу подпрограмму обслуживания пре- рывания всего лишь несколько операторов. void _ISR _T4Interrupt(void) ( int PS2IN; // Опрашиваем входы одновременно PS2IN = PORTG; // Конечный автомат опроса тактового сигнала от клавиатуры
178 Глава 11. Фиксация входных данных if (KState) { // Предыдущий сигнал был высокого уровня, состояние 1 if (! (PS2IN & CLKMASK)) // PS2CLK = О { // Обнаружен ниспадающий фронт KState =0; // Переход в состояние 0 KTimer = КМАХ; // Сброс счетчика <<<... Здесь размещается конечный автомат чтения данных!>» } // Ниспадающий фронт else { // Такт все еще = 1, остаемся в состоянии 1 KTimer—; if (KTimer ===0) // Время задержки превысило предел! PS2State = PS2START; // Сброс конечного автомата чтения } // Такт все еще = 1 } // Состояние 1 else { // Состояние 0 if (PS2IN & CLKMASK) // PS2CLK = 1 { // Обнаружен нарастающий фронт KState =1; // Переходим в состояние 1 } // Нарастающий фронт else { // Такт все еще = 0, остаемся в состоянии 0 KTimer—; if (KTimer -=0) //• Время задержки превысило предел! PS2State = PS2START; // Сброс конечного автомата чтения } // Такт все еще = 0 } // Состояние 0 // Сбрасываем флаг прерывания _T4IF = 0; } // Прерывание от Т4 Тестирование метода опроса портов ввода-вывода А теперь воспользуемся конечным автоматом чтения данных из предыдущих проектов, откорректировав его для обработки значения, сохраненного в PS2IN при входе в подпрограмму обслуживания прерывания: switch(PS2State){ default: case PS2START: if ( ! (PS2IN & DATMASK)) { KCount =8; // Инициализируем счетчик битов KParity =0; // Инициализируем бит контроля четности PS2State = PS2BIT; } break; case PS2BIT: KBDBuf >>= 1; // Сдвигаем бит данных if (PS2IN & DATMASK) // PS2DAT KBDBuf += 0x80;
Полет 179 KParity А= KBDBuf; // Вычисляем бит контроля четности if (--KCount == 0) // Если все биты считаны, переходим PS?State = PS2PARia . break; са е PS2PARITY: f (PS2IN & DATMASK) KParity Л= 0x80; if (KParity & 0x80) // Если печет, то продолжаем PS2State = PS2STOP; else PS2State = PS2START; break; case PS2STOP: if (PS2IN & DATMASK) // Проверяем стоп-бит { KBDCode = KBDBuf; // Записываем в буфер KBDReady =1; // Устанавливаем флаг } PS2State = PS2START; break; } // switch Завершим этот третий модуль надлежащей подпрограммой инициализации. void initKBD(void) ( // Инициализация портов ввода-вывода _TRISG15 =1; // RG15 - вход, тактовый сигнал PS/2 _TRISG12 =1; // RG12 - вход, данные PS/2 // Сбрасываем флаг KBDReady = 0; PR4 = 25 * 16; //25 мкс, устанавливаем регистр периода T4CON = 0x8000; // Включаем Timer4, предделитель - 1:1 _T4IF =0; // Сбрасываем флаг прерывания _Т41Е =1; // Разрешаем прерывание } // init KBD Здесь все очевидно и дополнительных пояснений не требует. Присвойте моду- лю . соотв^гствутощий заголовочный файл: ** PS2T4.h * * ** Опрос входов от клаиватуры PS/2 с помощью Timer4 */ extern volatile int KBDReady; extern volatile unsigned char KBDCode; void initKBD(void); О Файлы PS2T4.C и PS2T4.h находятся также на прилагаемом к книге компакт-диске в папке Проекты\11 - Фиксация входных даных\Т4. Этот заголовочный файл практически идентичен рассмотренным ранее, что справедливо также и для главного модуля:
180 Глава 11. Фиксация входных данных /* ** Тестирование клавиатуры PS/2 */ ^include <р24fj128ga010.h> #include ”PS2T4.h” main () { TRISA = OxffOO; initKBD(); while (1) // Вызов подпрограммы инициализации t if (KBDReady) // Ожидаем установки флага i PORTA = KBDCode; // Извлекаем код клавиши и выдаем его в порт А KBDReady = 0; И Сбрасываем флаг } } // Главный цикл } //main Создайте проект PS2T4 и добавьте в пего все три файла. Скомпонуйте модули с помощью команды меню Project ► Build All, а затем и с помощью рассмотренной в предыдущих двух примерах процедуры создайте сценарий стимулов PS2T4.scl. Не забудьте, что в этот раз стимулу для тактовой линии соответствует вывод RG15. Создайте новый сценарий в диалоговом окне Stimulus Controller и прикрепите файл PS2T4.scl для активизации имитации входных данных (не забывайте, что диало- говое окно Stimulus Controller следует оставить открытым). Откройте окно Watch и добавьте в его список регистр PORTA. Наконец, установите точку прерывания внутри главного цикла сразу же после оператора копирования кода клавиши в ре- гистр PORTA и запустите программу на выполнение. Если все сделано пра- вильно, то в регистре PORTA окажется код клавиши 0x7 9. Стоимость и эффективность решения Сравнив это решение с предыдущими двумя, можно сказать, что опрос портов ввода-вывода дает нам наибольшую свободу в выборе входов и использует только один ресурс (таймер) и один вектор прерываний. Кроме того, периодичные преры- вания не мешают выполнению других задач. Отслеживание времени задержки — еще одно преимущество. Для реализации подобной возможности в предыдущих двух методиках нам, кроме модулей Input Capture и Change Notification с соответст- вующими прерываниями,-пришлось бы использовать отдельный таймер и еще одну подпрограмму обслуживания прерывания. Если же оценивать с точки зрения эф- фективности кода, то методы захвата на входе и уведомления об изменении сигнала более выгодные, поскольку прерывание возникает только в момент обнаружения фронта. В этом отношении оптимальный метод — захват на входе, так как он позволяет выбрать конкретный, интересующий нас тип фронта (т.е. ниспадающий фронт так- тового сигнала). Метод опроса портов явно отличается самой длинной подпрограм- мой обслуживания прерываний, однако количество строк не отражает фактического “веса”. По сути, из двух вложенных конечных автоматов при каждом вызове вы- полняется только несколько операторов, что дает минимальные расходы времени.
Полот 181 Для того чтобы выяснить фактические программные издержки, обусловленные подпрограммой обслуживания прерывания, мы можем выполнить один простой тест для каждой из трех реализаций интерфейса PS/2. В качестве примера восполь- зуемся последней из них. Для индикации момента, когда микроконтроллер входит в подпрограмму обслуживания прерывания, выделим один из выводов для подклю- чения светодиода: void _ISR _T4Interrupt(void) { _RA0 =1; // Установка флага (мы внутри подпрограммы) «< Здесь размещается код подпрограммы обслуживания прерываниям _RA0 =0; // Сброс флага (возврат в главную программу) } Результат можно увидегь па экране компьютера с помощью окна Logic Analyzer имитатора MPLAB SIM. Опираясь па соответствующий контрольный список, акти- визируйте буфер слежения и установите корректную скорость имитации. Выберите канал RA0 и перекомпонуйте проект. Для тестирования первых двух методов опять потребуется открыть диалоговое окно Stimulus Controller для имитации входов, ина- че прерывание вообще никогда не возникнет. Для тестирования подпрограммы опроса стимулы не нужны, поскольку таймер в любом случае будет формировать запрос на прерывание. Нас особо интересует, сколько времени уходит па постоянный опрос при отсутствии данных от клавиату- ры. Позвольте имитатору MPLAB SIM поработать несколько секунд, а затем оста- новите имитацию и переключитесь в окно Logic Analyzer. Для того чтобы увидеть результат полностью, немного отмасштабируйте изображение (рис. 11.15). Рис. 11.15. Измерение периода опроса портов с помощью окна Logic Analyzer
182 Глава 11. Фиксация входных данных Активизируйте кнопку Cursor и перетащите мышью красные курсоры, чтобы измерить количество циклов между нарастающими фронтами соседних ' * импульсов на выводе RA0, обозначающих момент входа в подпрограмму обслужи- вания прерывания. Поскольку мы выбрали период 25 мкс, между вызовами прошло 400 циклов (25 мкс * 16 циклов/мкс при 32 МГц). Измерение количества циклов между двумя фронтами одного импульса па выводе RA0 дает нам хорошее пред- ставление о длительности подпрограммы обслуживания прерывания. Лично у меня получилось 16 циклов. Соотношение этих двух значений указывает на затраты вы- числительных ресурсов, требуемых для взаимодействия с интерфейсом PS/2. В на- шем случае оно составляет всего лишь 2,5%. Завершение интерфейса. Добавление FIFO-буфера Независимо от выбранного решения, для завершения интерфейса с клавиатурой PS/2 следует позаботиться еще о ряде аспектов. Прежде всего, необходимо добавить механизм FIFO-буферизации между подпрограммами интерфейса PS/2 и “потреби- телем” или главным приложением. До сих пор мы использовали простой регистр для хранения только одного кода клавиши, принятого последним. При более тща- тельном изучении протокола работы клавиатуры PS/2 выясняется, что при нажатии и отпускании одной клавиши хосту передается от трех до пяти кодов. Если учесть комбинации с клавишами <Shift>, <Ctrl> и <Alt>, то ситуация еще более усугубля- ется. Совершенно очевидно, что однобайтного регистра никак не достаточно. Пред- лагаю реализовать хотя бы 16-байтный FIFO-буфер. Запись в него нового кода кла- виши можно легко реализовать внутри подпрограммы обслуживания прерывания приемника. Такой буфер проще всего объявить как символьный массив, а два указа- теля будут отслеживать “голову” и “хвост” по кольцевой схеме (рис. 11.16). Рис. 11.16. Кольцовой FIFO-буфер // Кольцевой буфер unsigned char КСВ[KB_SIZE]; // "Голова" и "хвост" (указатели записи и чтения) volatile int KBR, KBW; Содержимое буфера можно отслеживать, следуя нескольким простым прави- лам: • указатель записи KBW (“голова”) помечает первую пустую ячейку, которая при- мет следующий код клавиши; • указатель чтения KBR (“хвост”) помечает первую заполненную ячейку; • когда буфер пуст, KBR и KBW указывают на одну и ту же ячейку; • когда буфер заполнен, KBW указывает на ячейку перед KBR;
Полет 183 • после чтения или записи символа соответствующий указатель инкрементирует- ся; • при достижении конца массива каждый указатель циклически возвращается к первому элементу. Добавьте в подпрограмму инициализации следующий фрагмент кода: // Инициализация указателей кольцевого буфера KBR = 0; KBW = 0; Затем обновите подпрограмму обслуживания прерывания для состояния “Стоп” конечного автомата: case PS2STOP: if (PS2IN & DATMASK) // Проверяем стоп-бит ( KCB[KBW] = KBDBuf; // Запись в буфер if ( (KBW+1)%KB_SIZE != KBR) // Проверяем, заполнен ли буфер KBW++; // Если нет, то инкрементируем счетчик KBW %= KB_STZE; // Циклический возврат на начало буфера } PS2State = PS2START; break; Обратите внимание, что для получения остатка от деления на размер буфера используется оператор %. Это дает циклический переход указателя к началу кольце- вого буфера. Для извлечения кодов клавиш из FIFO-буфера следует учитывать ряд аспектов. В частности, если выбрать методы захвата на входе или уведомления об изменении сигнала, то вместо механизма с флагами и одним регистром потребуется написать новую функцию (назовем ее getKeyCode ()), которая будет возвращать значение FALSE при отсутствии кода в буфере и TRUE при наличии хотя бы одного кода. Сам код возвращается по ссылке: int getKeyCode(char *с) { if (KBR == KBW) // Буфер пуст return FALSE; // Буфер содержит как минимум один код клавиши *с = КСВ[KBR++]; // Извлекаем первый код клавиши KBR %= KB_SIZE; // Переносим указатель на начало буфера return TRUE; } // getKeyCode Обратите внимание, что подпрограмма изменяет только указатель чтения, по- этому данную операцию безопасно выполнять, когда разрешены прерывания. Если прерывание возникнет в ходе извлечения данных, то возможны два сценария: • буфер был пуст — новый код добавляется в буфер, однако функция getKeyCode “заметит” его только при следующем вызове; • буфер не был пуст — подпрограмма обслуживания прерывания добавит новый символ в хвост буфера, если там достаточно свободного места. В обоих случаях не стоит особо волноваться о каких-либо конфликтах или опасных последствиях.
184 Глава 11. Фиксация входных данных Если избрать методику опроса, то имеет смысл исследовать еще одну возмож- ность. Поскольку прерывание от таймера постоянно активно, его можно использо- вать для решения дополнительной задачи. Идея заключается в реализации простого механизма для предоставления кодов клавиш подпрограмме приема данных и по- стоянной проверки буферного регистра па его готовность получить данные из FIFO- буфера. Это позволит заключить все управление FIFO-буфером внутри подпро- граммы обслуживания прерываний, что сделает буферизацию “прозрачной” и про- стой. Полная подпрограмма обслуживания прерываний в случае опроса портов вво- да-вывода выглядит следующим образом: void _ISR _T4Interrupt(void) { int PS2IN; // Проверка доступности буфера if (JKBDReady && (KBR!=KBW)) { KBDCode = KCB[KBR++); KBR %= KB_SIZE; KBDReady =1; // Сиогнал доступности символа } // Опрашиваем одновременно два входа PS2IN = PORTG; // Конечный автомат считывания данных от клавиатуры if (KState) { // Предыдущий сигнал был высокого уровня, состояние 1 if (!(PS2IN & CLKMASK)) // PS2CLK = О { // Обнаружен ниспадающий фронт KState =0; // Переход в состояние 0 KTimer = КМАХ; // Инициализация счетчика switch (PS2State){ default: case PS2START; if (!(PS2IN & DATMASK)) { KCount =8; // Инициализируем счетчик битов KParity =0; // Инициализируем бит контроля четности PS2State = PS2BIT; } break; case PS2BIT: KBDBuf >>= 1; // Сдвигаем бит данных if (PS2IN & DATMASK) //PS2DAT KBDBuf += 0x80; KParity Л= KBDBuf; // Вычисляем бит контроля четности if (—KCount == 0) // Если считаны все биты, переходим PS2State = PS2PARITY; break; case PS2PARITY: if (PS2IN & DATMASK) KParity Л= 0x80; if (KParity & 0x80) // Если чет, то продолжаем
Полет 185 PS2State = PS2STOP; else PS2State = PS2START; break; case PS2STOP: if (PS2IN & DATMASK) // Проверяем стоп-бит { KCB[KBW] = KBDBuf; // Запись в буфер if ((KBW+1)%KB_SIZE != KBR) // Проверяем,полон ли буфер KBW++; // Если нет, инкрементируем указатель KBW %= KB_SIZE; // Перенос указателя на начало буфера } PS2State = PS2START; break; } // switch } // Ниспадающий фронт else { // Тактовый сигнал еще высокого уровня, остаемся в состоянии 1 KTimer—; if (KTimer == 0) PS2State = PS2START; } // Тактовый сигнал еще высокого уровня } // Состояние 1 else { // Состояние 0 if (PS2IN & CLKMASK) // PS2CLK = 1 { // Нарастающий фронт, переходим в состояние 1 KState = 1; } // Нарастающий фронт else { // Тактовый сигнал еще низкого уровня, остаемся в состоянии 1 KTimer—; if (KTimer == 0) PS2State = PS2START; } // Тактовый сигнал еще низкого уровня } // Состояние 0 // Сбрасываем флаг прерывания _T4IF = 0; } // Прерывание от Timer4 Завершение интерфейса. Декодирование кодов клавиш До сих пор речь шла исключительно о кодах клавиш, и, наверняка, кто-то из чи- тателей подумал, что они соответствуют кодам ASCII. Например, мы нажимаем на клавиатуре клавишу <А> и получаем ASCII-код 0x41. Но, к сожалению, это не так. По историческим причинам даже новейшие USB-клавиатуры все еще используют так называемые коды опроса, когда каждой клавише назначается определенное чи- словое значение, связанное с оригинальной программно-аппаратной реализацией первой клавиатуры IBM PC образца примерно 1980 года (использовала микрокон- троллер 8048): На самом деле преобразование кодов клавиш в конкретный набор символов па высоком уровне (выполняется драйверами клавиатуры Windows) — это неплохо^ поскольку оно обеспечивает обобщенный механизм поддержки множества раскладок клавиатуры для разных языков. Следует также отметить, что по тем же историческим причинам, существует по меньшей мере три различных, частично со-
186 Глава 11. Фиксация входных данных вместимых наборов кодов опроса. К счастью, по умолчанию все клавиатуры под- держивают набор №2, на котором мы и сосредоточим свое внимание. При каждом нажатой любой клавиши, включая <Shift> и <Ctrl>, хосту нереда- ется соответствующий код опроса, называемый “кодом нажатия”. Однако, как толь- ко та же клавиша будет отпущена, хосту передается новая последовательность ко- дов опроса под названием “код отпускания”. Код отпускания обычно представляет собой код нажатия с добавлением префикса OxFO. Некоторым клавишам назначен двухбайтный код нажатия (например, <Ctrl>, <Alt> и клавиши управления курсо- ром) и, соответственно, — трехбайтный код отпускания (табл. 11.6). Таблица 11.6. Пример кодов нажатия и отпускания из набора кодов опроса №2 Клавиша Код нажатия Код отпускания <А> 1С F0, 1С <5> 2Е F0, 2Е <F10> 09 F0, 09 <—>> Е0, 74 Е0, F0, 74 Правая <Ctrl> Е0, 14 Е0, F0, 14 Для того чтобы обработать эту информацию и преобразовать коды опроса в надлежащие коды ASCII, потребуется таблица для установки соответствий между базовыми кодами опроса и английской раскладкой клавиатуры. // Коды клавиатуры PS/2 (стандартный набор №2) const char keyCodes 0, F9, 0, F5, F3 0, F10, F8, F6, [128]={ , Fl, F2, F12, F4, TAB, ’0, //00 //08 о, о, L_SHFT, 0,L _CTRL, ’q’ , 11’ , 0, //10 о, 0, 'z’, ’s’, ’a’, ’w’, '2 ', 0, //18 0, 'с ' , ' x ', ’ d ' , 'e', ’4', -3’, 0, //20 о, ’ ’ , ’ v ’, ’ f ' , ’t’, 'r', ’5’, 0, //28 0, ’ n ’ , ’ b ', ’ h ’ , 'д’, ’у', ’6’, 0, //30 0, 0, ’m', ’j’, '□', ’7’, ’8', 0, //38 о, ' , ’ , ’ k ’, ’ i ’ , 'o’, 'O’, ’9’, 0, //40 о, ’ . , 'p', 0, //48 о, о, CAPS, vo, '[, = , o, o, R SHFT, ENTER, 0, 0x5c, 0, 0, //50 //58 0, 0, 0, ' 1 o, ’ . 0, 0, 0, 0 ' , 0, ' 4 ' , ', ’2’, '5' , BKSP, 0, ’7’, 0, 0, 0, , '6', ’8’, ESC, NUM, //60 //68 //70 Fll, ’ + ’, ’3’, ’ f ’*', ’9’, 0, 0 //78 }; Обратите внимание, что этот массив был объявлен как константа, чтобы быть размещенным в области памяти программ с целью экономии драгоценного ОЗУ. Также было бы неплохо использовать аналогичную таблицу для функции <Shift> каждой клавиши. const char keySCodes[128] = { О, F9, О, F5, F3, Fl, F2, F12, //00 0, F10, F8, F6, F4, TAB, 0, //08 0, 0, L_SHFT, 0, L_CTRL,'Q',’!0, //10 0, 0, ’Z', ’S’, ’A’, ’W, ’О’, 0, //18 0, ’С’, ’X’, ’D', 'Е', ’#’, 0, //20 0, ' ’, 'V, 'F', 'Т', 'R', 0, //28 0, 'N', ’В', ’ll’, ’С, ’У, ,А’, 0, //30 0, 0, ’М’, 'J', ’□’, ’&’, 0, //38
Полет 187 О, '<’, 'К', ’I’, ’0’, ’)', ’(’, 0, //40 О, ’>’, ’?’, ’L’, ' : ’Р’, 0, //48 О, 0,0, ’(, О, 0, //50 CAPS, R_SHFT, ENTER, '0, ’ Г, О, 0, //58 О, О, О, О, О, О, BKSP, 0, //60 О, ’1', 0, ’4’, ’7', О, О, 0, //68 О, ’2’, ’5', ’6’, ’8’, ESC, NUM, //70 Fll, ’+’, ’3’, ’9’, О, 0 //78 }; Для всех символов ASCII преобразование не представляет собой сложности, однако функциональным клавишам, а также клавишам <Shift> и <Ctrl> необходимо назначить специальные значения. Соответствующие коды в наборе ASCII сущест- вуют только для некоторых из них: // Символы специальных функций #define TAB 0x9 #define BKSP 0x8 #define ENTER Oxd #define ESC Oxlb Для всех остальных клавиш придется создать собственные соглашения, а для тех из них, которые использоваться не будут, можно просто назначить условный код 0. #define L_SHFT 0x12 #define R_SHFT 0x12 #define CAPS 0x58 #define L_CTRL 0x0 #define NUM 0x0 #define Fl 0x0 #define F2 0x0 #define F3 0x0 #define F4 0x0 #define F5 0x0 #define F6 0x0 ttdefine F7 0x0 #define F8 0x0 #define F9 0x0 #define F10 0x0 #define Fll 0x0 #define F12 0x0 Представленная ниже функция getC () выполняет базовые преобразования для большинства клавиш общего назначения, а также обрабатывает состояние <Shift> и <Caps Lock>: int CapsFlag = 0; char getC(void) { unsigned char c; while (1) { while(!KBDReady); // Ожидаем нажатия клавиши // Проверяем, не является ли код кодом нажатия while (KBDCode == 0xf0) { // Убираем код нажатия
188 Глава 11. Фиксация входных данных KBDReady = 0; // Ожидаем нового кода клавиши while (!KBDReady); // Проверяем, отпущена ли клавиша <Shift>, if (KBDCode == L_SHFT) CapsFlag = 0; // и избавляемся от нее KBDReady = 0; // Ожидаем следующую клавишу while (!KBDReady); } // Проверка специальных клавиш if (KBDCode == L_SHFT) { CapsFlag = 1; KBDReady = 0; } else if (KBDCode == CAPS) { CapsFlag = !CapsFlag; KBDReady = 0; } else // Преобразование в код ASCII { if (CapsFlag) c = keySCodes[KBDCode%128] ; else c = keyCodes[KBDCode%128]; break; } } // Убираем текущий символ KBDReady = 0; return (c); } // getC Разбор полета В этом уроке мы узнали, как организовать взаимодействие с компьютерной клавиатурой PS/2 с помощью трех разных методов. Это дало нам идеальную воз- можность изучить два новых периферийных модуля: Input Capture и Change Notifi- cation. Кроме того, мы осудили методы реализации FIFO-буфера и отточили наши навыки обработки прерываний. На протяжении всего урока в центре внимания по- стоянно находился вопрос баланса в использовании ресурсов и производительности каждого из решений. Советы и хитрости Каждая клавиатура PS/2 оснащена внутренним FIFO-буфером на 16 кодов кла- виш. Это позволяет накапливать информацию о нажатых пользователем клавиш даже в случае неготовности хоста к приему данных. Как уже было отмечено в нача- ле этой главы, хост может в любой момент заблокировать передачу, переведя такто- вую линию в состояние низкого уровня (по крайней мерс ..а 100 мкс) и удерживая
Упражнения 189 ее в таком состоянии требуемое время. Когда тактовая линия будет освобождена, клавиатура возобновит передачу данных, хранимых в FIFO-буфере. Для того чтобы воспользоваться своим правом заблокировать обмен данными с клавиатурой, тактовой линией необходимо управлять с помощью выхода с откры- тым стоком. К счастью, в случае с микроконтроллером PIC24 это не составляет тру- да, благодаря конфигурируемым портам ввода-вывода. Каждому порту соответству- ет собственный управляющий регистр ODCx, который позволяет индивидуально сконфигурировать схему управления каждого выхода для работы в режиме откры- того стока. ПРИМЕЧАНИЕ .......................................... Эта возможность чрезвычайно полезна при подо ре PI.C24 к любому устройству с напряжением :3 > : В нашем примере для превращения тактовой линии PS/2 в выход с открытым стоком требуется всего лишь несколько строк кода: _ODG13 =1; // Конфигурируем вывод 14 порта G на работу в режиме // открытого стока _LATG13 =1; // Сразу же разрешаем использовать для выхода // подтягивающий резистор _TRISG13 =0; // Активизируем схему управления выходом Обратите внимание, что как и у всех микроконтроллеров PIC, даже если вывод сконфигурирован как выход, его текущее состояние все равно может быть прочита- но, как для входа. Таким образом, при попеременной блокировке и приеме симво- лов от клавиатуры пег смысла постоянно переключаться между входом и выходом. Упражнения 1. Добавьте функцию для передачи клавиатуре команд, управляющих состоянием светодиодов и скоростью повторения символа. 2. Замените функцию read () библиотеки stdio.h версией, перенаправляющей ввод с клавиатуры с потока stdin. 3. Добавьте поддержку мыши PS/2. Ссылки • http://www.computer-engineering.org — превосходный Web-сайт, содержащий массу полезной документации по интерфейсу PS/2 для клавиатуры и мыши.
ГЛАВА *1 2 Черный экран В этой главе: > Формирование полного видеосигнала & Использование модулей Output Compare > Распределение памяти к Последовательный вывод изображения & Разработка видеомодуля > Тестирование видеогенератора > Оценка производительности > Черный экран > Тестовый образец & Построение изображений > Звездная ночь > Рисование линий > Алгоритм Брезенхема & Рисование математических функций > Двухмерная визуализация трехмерных функций > Фракталы > Текст > Тестирование модуля TextOnGPage > Разработка текстовой видеостраницы > Тестирование производительности текстовой страницы Мне всегда нравилось водить машину ночью. Обычно в это время дороги раз- гружены, а воздух — освежающий. Фары же встречного движения меня никогда особо не раздражали, если только я не сильно устал. Однако конда мой инструктор предложил совершить дальний ночной полет, я немного занервничал. Уже сама мысль о черной, непроницаемой пустоте за стеклом кабины была несколько... пу- гающей. Тем не менее, через неделю я был уже совсем другого мнения. Конечно, ночной полет несколько сложнее кружения вокруг аэропорта, и к нему необходимо чуть тщательней готовится, однако это того стоит! Когда ты летишь над незаселен- ной областью, небо над головой наполненного таким количеством звезд, которого исконный горожанин, наподобие меня, никогда в жизни не видел. Такое ощущение, что ты сидишь не в самолете, а в космическом корабле, летящем к другой солнеч- ной системе. Во время же полета над большим городом серые, однообразные скоп- ления зданий и автостоянок превращаются в восхитительный, безбрежный карнавал света. Как будто ты оказался на праздновании Рождества! Как оказалось, экран ни- когда не бывает полностью черным. Он всю ночь показывает увлекательное шоу.
План полота 191 План полета В данном уроке мы рассмотрим мегодики взаимодействия с телеэкраном или любым дисплеем, поддерживающим стандартный полный видеосигнал. Это — хо- роший повод воспользоваться новыми возможностями нескольких периферийных модулей микроконтроллера PIC24 и изучить новые методики программирования. Первой целью нашего проекта будет получение полностью черного экрана (хорошо синхронизированного видеокадра), однако вскоре мы заполним его различными графическими изображениями. Полет Сегодня в мире видео существует множено различных форматов и стандар- тов, однако, наверное, самым старым и популярным является так называемый “пол- ный” (composite) видеоформат. Именно он использовался в первых телевизорах и на сегодняшний день представляет’ собой минимальный общий знаменатель любой ви- деосистемы будь то современный плоскоэкранный телевизор с высоким разрешени- ем изображения, DVD-проигрыватель или пленочный видеомагнитофон. Все видео- устройства основаны па одном базовом принципе: изображение прорисовываегся построчно. Прорисовка начинается от левого верхнего угла экрана, двигается по го- ризонтали к правому краю, а затем быстро перескакивает обратно к левому краю в позиции следующей строки. Данный “зигзагообразный” процесс продолжается до достижения правого нижнего угла экрана, после чего повторяется сначала (рис. 12.1). При этом изображение обновляется доста- точно быстро для того, чтобы наши глаза не замечали мелькания, а движения на экране выглядят плавными и непрерывными. В ходе развития телевидения в разных частях мира были разработаны немного не- совместимые системы, однако базовый ме- ханизм остается тем же. Отличается только количество строк, составляющих изображе- ние, частота обновления и способ кодирова- ние. 12.1. Развертка видеоизображения ния информации о цвете (табл. 12.1). Таблица 12.1. Примеры различных видеостандартов США Европа, Азия Франция и другие Стандарт NTSC PAL SECAM Кадров в секунду 29.97* 25 25 Количество строк 525 625 625 * Раньше стандарту NTSC соответствовала частота 30 кадров в секунду, однако с введением но- вого цветового эталона она была изменена на 29,97, чтобы соответствовать особой частоте ге- нератора цветовой поднесущей. В табл. 12.1 перечислены три наиболее распространенных видеостандарта, при- нятых в США, Европе и Азии. Все они кодируют информацию о яркости (т.е. базо- вое черно-белое изображение) вместе с информацией о синхронизации в опреде- ленном аналогичным образом полном сигнале. Слово “полный” подразумевает, что в одном видеосигнале объединены три разных элемента: фактический сигнал ярко- сти и информация о горизонтальной и вертикальной синхронизации (рис. 12.2).
192 Глава 12. Черный экран импульс гашения Рис. 12.2. Полный NTSC-сигнал (для горизонтальной строки) Сигнал горизонтальной строки по сути состоит из: • импульса горизонтальной синхронизации, используемого дисплеем для иден- тификации начала каждой строки; • задней площадки строчного интервала гашения (back porch), создающей черную рамку вокруг изображения; • фактического сигнала яркости (чем выше уровень напряжения, тем ярче сверит- ся точка); • передней площадки строчного интервала гашения (front porch), создающей пра- вый край изображения. Цветовая информация передается отдельно, модулированная по высокочастот- ной поднесущей. По способу кодирования цветовой информации три основных стандарта сильно различаются, однако для наших целей будет проще полностью проигнорировать эту проблему и получить простое черно-белое изображение. Для обеспечения относительно высокого разрешения при небольшой полосе пропускания все стандартные видеосистемы используют методику под названием “чересстрочная развертка”. Она подразумевает, что для каждого кадра передается и прорисовывается па экране только половина строк. Поочередное отображение четных и нечетных строк, составляющих изображение, дает эффективное обновле- ние экрана па половинной частоте (25 Гц для PAL и 30 Гц для NTSC). Такой подход приемлем для традиционного телевещания, однако может приводить к раздражаю- щему мерцанию при отображении текста и особенно — горизонтальных линий, что характерно для компьютерных мониторов. По этой причине все современные ком- пьютерные мониторы используют не чересстрочную, а построчную развертку. Кро- ме того, большинство современных телевизоров (особенно использующих жидкок- ристаллические и плазменные технологии), выполняют сборку принятого чересст- рочного сигнала и формируют изображение уже построчно. В нашем проекте мы также обойдем чересстрочную развертку, пожертвовав половиной разрешения ради более устойчивого и удобочитаемого изображения на экране. Другими словами, мы будем передавать кадры из 262 строк (для NTSC) на удвоенной частоте 60 кадров/с. Тем читателям, которым проще работать с телевизорами или мониторами PAL или SECAM, будет несложно откорректировать проект' под разрешение 312 строк с час- тотой обновления 50 кадров/с.
Полет 193 Полный сигнал видеокадра показан па рис. 12.3. Передние уравнивающие импульсы Импульсы вертикальной синхронизации Задние уравнивающие импульсы Перван строка изображении 4---Начало кадра Рис. 12.3. Полный сигнал видеокадра Обратите внимание, что из общего числа строк, составляющих каждый кадр, три периода заполнены удлиненными синхроимпульсами для обеспечения верти- кальной синхронизации, идентифицирующей начало каждого нового кадра. Перед ними и после них следуют группы из грех дополнительных сгрок, которые называ- ют передними уравнивающими и задними уравнивающими. Формирование полного видеосигнала Поскольку мы ограничили проект формированием простого черно-белого изо- бражения (без оттенков серого и цветов) при отсутствии чересстрочной развертки, то можем значительно упростить требования к аппаратной и программной части. Так, аппаратный интерфейс можно сократить всего лить до трех резисторов, со- единенных с двумя цифровыми выводами. Один из этих выводов отвечает за выда- чу синхроимпульсов, а второй — за фактический сигнал яркости (рис. 12.4). Рис 12.4. Простой аппаратный интерфейс для выдачи видеосигнала NTSC Номинал резисторов следует выбирать таким образом, чтобы относительные амплитуды сигналов яркости и синхронизации были близки к спецификациям стан- дарта NTSC, суммарная амплитуда была примерно равна 1 В в размахе, а выходное сопротивление цепи составляло около 75 Ом. Стандартные номиналы, показанные на рис. 12.4, удовлетворяют этим требованиям и дают три базовых уровня, необхо- димых для получения черно-белого изображения (табл. 12.2 и рис. 12.5). Таблица 12.2. Формирование импульсов яркости и синхронизации Характеристика сигнала Синхросигнал Видеосигнал Синхроимпульс 0 0 Уровень черного цвета 1 0 Уровень белого цвета 1 1
194 Глава 12. Черный экран Рис. 12.5. Упрощенный полный NTSC-сигнал Поскольку мы не собираемся использовать чересстрочную развертку, можно также упростить этапы переднего и заднего уравнивания, а также — вертикальной синхронизации, выдавая в каждом периоде обычный одиночный импульс горизон- тальной синхронизации (рис. 12.6). Передние Импульсы уравнивающие Н----вертикальной импульсы синхронизации Задние уравнивающие импульсы ___Первая строка изображения 4---Начало кадра Рис. 12.6. Упрощенный видеокадр NTSC (без чересстрочной развертки) Теперь задача формирования полного видеосигнала упрощается до построения простого конечного автомата, которым можно управлять с фиксированной перио- дичностью с помощью прерываний от таймера. Структура конечного автомата весьма тривиальна, поскольку каждому состоянию соответствует один тип строки, входящей в состав кадра, который повторяется фиксированное количество раз до перехода в следующее состояние (рис. 12.7). Рис. 12.7. Диаграмма конечного автомата для формирования видеокадра Переходы между состояниями описаны в табл. 12.3.
Полет 195 Таблица 12.3. Таблица переходов конечного автомата для формирования видеокадра Состояние Повторение Переход в состояние Переднее уравнивание PREEQ N раз Вертикальная синхронизация Вертикальная синхронизация Три раза Заднее уравнивание Заднее уравнивание POSTEQ и раз Строка изображения Строка изображения VRES раз Переднее уравнивание В то время как количество строк вертикальной синхронизации фиксировано и определено видеостандартом NTSC, задача определения количества строк, из ко- торого фактически состоит изображение внутри каждого кадра (в допустимых пре- делах, конечно же), возлагается па нас. Хотя теоретически можно было бы исполь- зовать все доступные строки для отображения на экране максимально возможного обт>ема информации, нам придется учитывать некоторые практические ограничения (в частности, размер ОЗУ для хранения видеоизображения внутри микроконтролле- ра PIC24FJ128GA010). Такие ограничения обуславливают конкретное количество строк (VRES), задействованных в изображении, в то время как все остальные строки (до определенного стандартом NTSC числа) останутся пустыми. Если принять, что V_NTSC — это общее количество строк, составляющих ви- деокадр стандарта NTSC, a VRES — требуемое вертикальное разрешение, то значе- ния PREEQ_N и POSTEQJN можно определить следующим образом: #define V_NTSC 262 // Общее количество строк в кадре #define VSYNC_N 3 // Строки вертикальной синхронизаци // Подсчитываем количестве оставшихся пустых строк сверху и снизу #define VBLANK_N (V_NTSC - VRES - VSYNC_N) #define PREEQ_N VBLANK_N/2 // Переднее уравнивание // пустые строки снизу #define POSTEQ_N VBLANK_N - PREEQ_ _N // Заднее уравнивание + // пустые строки сверху Если для формирования временной развертки избрать таймер Timer3, то для по- лучения прерываний необходимо инициализировать регистр периода PR3. В таком случае конечный автомат реализуется в подпрограмме обслуживания прерываний, базовая структура которой выглядит следующим образом: // Таблица "Следующее состояние" int VS [4] = { SV_SYNC, SV_POSTEQ, SV_LINE, SV_PREEQ}; // Таблица "Следующий счетчик" int VC[4] = ( VSYNC_N, POSTEQ_N, VRES, PREEQ_N}; void _ISRFAST _T3lnterrupt(void) { j // Начинаем с синхроимпульса SYNC = 0; // Декреметируем счетчик по вертикали VCount—; // Конечный автомат switch (VState) { case SV_PREEQ: // Импульс горизонтальной синхронизации
196 Глава 12. Черный экран break; case SV_SYNC: // Импульс вертикальной синхронизации break; case SV-POSTEQ: , // Импульс горизонтальной синхронизации break; default: case SV_LINE: } //switch // Дополнение конечного автомата if (VCount == 0) { VCount = VC[VState]; VState = VS[VState]; } // Сбрасываем флаг прерывания _T3IF = 0; } // Прерывание от таймера ТЗ Оказавшись в подпрограмме обслуживания прерывания, мы можем сразу же ус- тановить низкий уровень сигнала па выходе Sync, чтобы начать формирование им- пульса горизонтальной синхронизации. Однако для завершения импульса и получе- ния остальной части сигнала горизонтальной строки нам необходим какой-нибудь механизм установки корректного периода (примерно 4,5 мкс). Рассмотрим три возможных варианта решения этой задачи: • создать короткий цикл задержки с помощью счетчика; • воспользоваться вторым таймером и соответствующим прерыванием; • воспользоваться модулями Output Compare (сравнение па выходе) и соответст- вующими прерываниями. Первый вариант, пожалуй, — самый простой с точки зрения кодирования, од- нако обладает явным недостатком: он тратит слишком много циклов процессора (4,5 мкс х 16 циклов в мкс = 72 цикла). С учетом повторов для каждой горизонталь- ной строки (63,5 мкс или примерно 1018 циклов) мы получаем целых 7% доступно- го процессорного времени. Второй способ явно более эффективный, и у пас уже достаточно опыта в использовании прерываний от таймера для создания простого конечного автомата. Третий вариант подразумевает применение повой периферии, которую мы в предыдущих главах еще не исследовали, а значит он заслуживает бо- лее пристального внимания. Использование модулей Output Compare Микроконтроллер PIC24FJ128GA010 оснащен пятью периферийными модуля- ми Output Compare, которые используют для решения самых разнообразных задач,
Полот 197 включая формирование одиночного импульса, формирование последовательности импульсов и широтно-импульсную модуляцию (ШИМ). Каждый модуль можно свя- зать с одним из двух 16-разрядных таймеров (Timer2 или Timer3) и имеет в своем распоряжении один выход, который можно сконфигурировать для переключения уровня сигнала и создания в случае необходимости нарастающего или ниспадающе- го фронта. Но что наиболее важно, с каждым модулем Output Compare сопоставлен независимый вектор прерывания. Структурная схема модуля показана на рис. 12.8. Установка флага Входы регистра TMR от генератора развертки (см. Примечание 3). Сигналы совпадения периода от генератора развертки (см. Примечание 3). Примечание 1: Символу Y соответствует номер канала модуля Output Compare от 1 до 8. 2: Вывод OCFA контролирует каналы OC1-OC4 channels. Вывод OCFB контролирует каналы OC5-OC8. 3: Каждый канал модуля Output Compare может использовать один из двух выбираемых пользователем генераторов развертки. Информацию о генераторах развертки, связанных с модулем, ищите в спецификации микроконтроллера. Рис. 12.8. Структурная схема модуля Output Compare В режиме одиночного импульса регистр OCxR можно использовать для опреде- ления момента (относительно значения выбранного таймера) возникновения преры- вания и, в случае необходимости, — установки требуемого уровня па выходе. Един- ственный конфигурационный регистр, требуемый для управления каждым модулем Output Compare — это OCxCON (рис. 12.9). Старший байт: U-0 U-0 R/W-0 U-0 и-о и-о и-о и-о — | OCSIDL — — — Г' - разряд 15 разряд 8 Младший байт: и-о и-о и-о R-0 НС R/W-0 R/W-0 R/W-0 R/W-0 OCFLT | OCTSEL | ОСМ2 ОСМ1 осмо разряд 7 разряд 0 Рис. 12.9. Управляющий регистр OCxCONмодуля Output Compare В нашем примере механизм сравнения па выходе очень удобен, поскольку нам необходимо предпринимать какие-то действия в конкретные два момента времени: конец импульса горизонтальной синхронизации при переднем или заднем уравни- вании или формировании строки вертикальной синхронизации, а также конец зад-
198 Глава 12. Черный экран ней площадки строчного интервала гашения в начальной точке фактического изо- бражения (рис. 12.10). Прерывание ст модуля ОСЗ Interrupt (строка вертикальной синхронизации) Рис. 12.10. Последовательность прерываний для строки синхронизации С помощью одного из модулей Output Compare (остановим свой выбор на ОСЗ) мы определим точный момент окончания импульса синхронизации. Соответствую- щий выход (RD2) нам использовать не нужно. Вместо этого в подпрограмме обслу- живания прерывания будет устанавливаться высокий уровень сигнала Sync. void -ISRFAST _OC3Interrupt(void) { SYNC =1; // Переводим выход на уровень черного цвета _OC3IF =0; // Сбрасываем флаг прерывания } // OC3Interrupt Управляющий регистр OC3CON будет инициализирован таким образом, чтобы модуль Output Compare работал в режиме одиночного импульса (ОСМ = 001) и ис- пользовал в качестве генератора развертки таймер Timer3 (OCTSEL = 1). Кроме того, мы инициализируем регистр OC3R выбранным значением времени в зависимости от типа строки (состояния конечного автомата): // Конечный автомат switch (VState) { case SV_PREEQ: // Импульс горизонтальной синхронизации OC3R = HSYNC_T; OC3CON = 0x0009; // Режим одиночного события break; case SV-SYNC: // Импульс вертикальной синхронизации OC3R = H_NTSC - HSYNC_T; OC3CON = 0x0009; -// Режим одиночного события break; Г case SV_POSTEQ: // Импульс горизонтальной синхронизации OC3R = HSYNCJT; OC3CON = 0x0009; // Режим одиночного события При формировании видеостроки мы воспользуемся еще одним модулем Output Compare (ОС4) для обозначения конечной точки задней площадки строчного интер- вала гашения, а фактический вывод строки изображения реализуем в соответст- вующей подпрограмме обслуживания прерывания (рис. 12.11).
Полет 199 Рис. 12.11. Последовательность прерываний для видеостроки case SV_LINE: // Активизируем ОСЗ для конечной точки импульса горизонтальной // синхронизации OC3R = HSYNC_T; OC3CON = 0x0009; //.Одиночное событие // Активизируем ОС4 для конечной точки задней площадки строчного // интервала гашения OC4R = HSYNC_T + BPORCHJT; OC4CON = 0x0009; // Одиночное событие break; Распределение памяти До сих пор мы работали с формированием сигналов синхронизации, состав- ляющих видеосигнал NTSC, с управлением только через один из двух выводов про- стого аппаратного интерфейса. Второй вывод будет задействован после получения одной из строк фактического изображения. Переключая видеовывод, мы можем че- редовать сегменты строки, прорисовываемые белым (1) или черным (0) цветом. По- скольку стандарт NTSC определяет максимальную полосу пропускания яркости сигнала в 4,2 МГц, а интервал между передней и задней площадками строчного ин- тервала гашения — 52 мкс, максимальное количество чередуемых сегментов (цик- лов) черного и белого цвета составляет' 218 (52 х 4,2). Другими словами, макси- мальное теоретическое разрешение.по горизонтали — 436 пикселей на строку (если предположить, что экран используется полностью от края до края). Максимальное разрешение по вертикали получают из общего количества строк, составляющего каждый стандартный кадр NTSC, минус минимальное число строк уравнивания и вертикальной синхронизации (т.е. 253). Если бы нам потребовалось сформировать изображение наибольшего возможного размера, то оно представляло бы собой массив 253 х 436 пикселей (110 308 пикселей). Если для представления каждого пикселя использовать один бит, то нам необходимо разместить в памяти 13,5 Кбайт данных, что намного превосходит доступные 8 Кбайт ОЗУ микрокон- троллера PIC24FJ128GA010. Хотя возможность сформировать изображение с высо- ким разрешением очень привлекательна, на практике всегда приходится помнить об ограничениях ОЗУ. В памяти должно быть достаточно свободного места для нор- мальной работы приложения, храпения значений стека и переменных. Хотя существует почти бесконечное количество возможных комбинаций раз- решения по горизонтали и вертикали, дающих приемлемый расход оперативной па- мяти, для выбора идеального варианта необходимо придерживаться следующего
200 Глава 12. Черный экран правила: разрешение по горизонтали должно быть кратным 16, чтобы упростить ма- тематические расчеты при определении позиции пикселя в карте распределения па- мяти (предполагается, что мы используем массив целых чисел). Кроме того, во из- бежание геометрического искажения изображения (например, прорисовки окружно- сти в виде овала) соотношение значений двух разрешений должно составлять при- мерно 4:3. Если выбрать горизонтальное разрешение 256 пикселя (IIRES), а вертикаль- ное — 192 строки (VRES), то мы получим расход памяти 6 144 байт (256 х 192 / 8), что оставляет целых 2 048 байт для хранения стека и переменных приложения. Компилятор СЗО позволяет без труда разместить в оперативной памяти цело- численный массив (группами по 16 пикселей в каждом слове) для храпения всей карты изображения. Однако следует убедиться в том, что все содержимое массива адресуется, что будет невозможно, если его объявить как переменную типа near (выбор по умолчанию при использовании малой модели памяти). Такие переменные должны находиться в пределах первых 8 Кбайт адресуемого пространства данных, однако это пространство также содержит регистры специального назначения и об- ласть PSV. Наилучший способ избежать ошибки распределения памяти, — явно объявить карту видеопамяти с атрибутом far. #define _FAR _attribute__((far)) int _FAR VMap[VRES * (HRES/16)]; Это гарантирует, что доступ к элементам массива осуществляется через указа- тели. Последовательный вывод изображения Поскольку каждая строка изображения представлена в массиве VMap строкой из 16 целых чисел, нам необходимо последовательно выводить каждый бит (пик- сель) между задней и передней площадками строчного интервала гашения полного видеосигнала через короткие промежутки времени (52 мкс). Другими словами, требуется обновлять выбранный видеовыход новым значени- ем пикселя хотя бы раз в 200 нс. Это дает примерно три машинных цикла между пикселями, что слишком мало для простого цикла сдвига, даже если мы планируем кодирование непосредственно на ассемблере. Что еще хуже, даже если удастся ужать цикл до такого мизера, для формирования изображения все равно потребует- ся непомерно высокий процент процессорного времени, оставляющий совсем мало машинных циклов для главного приложения (в лучшем случае 18%). К счастью, микроконтроллер PIC24 оснащен периферийным модулем, позволяющим организо- вать эффективный последовательный вывод данных изображения. Речь идет о син- хронном интерфейсе SPI. В одной из предыдущих глав мы использовали порт SPI2 для обмена данными с последовательной памятью. Тогда мы отметили, что модуль SPI, по сути, состоит из обычного сдвигового регистра, который можно тактировать с помощью внешне- го (в режиме ведомого) или внутреннего (в режиме ведущего) синхросигнала. В на- шем новом проекте можно воспользоваться ведущим модулем SPI1, выход SDO ко- торого напрямую соединен с видеовыводом аппаратного интерфейса. Вход данных SDI в таком случае не задействован, а выводы SCK и SS — отключены. Среди множества новых свойств модуля SPI микроконтроллера PIC24 два иде- ально подходят для нашего видеоприложения: возможность работать в 16-разряд- ном режиме и наличие восьмиуровневого FIFO-буфера.
Полет 201 16-разрядный режим позволяет практически удвоить скорость передачи данных между картой памяти изображения и модулем SPI, а восьмиуровневый FIFO-бу- фер — загружать в SPI-буфер за раз до 128 пикселей (8 слов х 16 бит) и быстро воз- вращаться из подпрограммы обслуживания прерывания (всего лишь через 25 мкс для второй завершающей загрузки). Таким образом, достигается максимальная эф- фективность видеогенератора, поскольку для каждой строки изображения требуется только две коротких вспышки активности. Создадим подпрограмму обслуживания прерывания для второго модуля Output Compare, которая будет активизироваться конечным автоматом сразу же после зад- ней площадки строчного интервала гашения для формирования фактической строки изображения: void -ISRFAST _ОС4Interrupt(void) { // Загружаем в FIFO-буфер модуля SPI восемь 16-разрядных слов, // что дает 128 пикселей SPI1BUF = *VPtr++; SPI1BUF = *VPtr++; SPI1BUF = *VPtr++; SPI1BUF = *VPtr++; SPI1BUF = *VPtr++; SPI1BUF = *VPtr++; SPI1BUF = *VPtr++; SPI1BUF = *VPtr++; if (--HCount > 0) { // Опять активизируем для следующей загрузки по SPI OC4R += (Р1Х_Т * 7 * 16); OC4CON = 0x0009; // Одиночное событие // Сбрасываем флаг прерывания _OC4IF = 0; } // Прерывание от ОС4 Обратите внимание, каким образом подпрограмма обслуживания прерывания реконфигурирует модуль ОС4 для второй вспышки (второй половины строки изо- бражения) после загрузки в SPI-буфер первых 128 пикселей данных. Теперь, когда все фрагменты головоломки определены, можно написать пол- ную подпрограмму инициализации для всех требуемых видеогенератору модулей: void initVideo(void) { // Устанавливаем уровни приоритета _Т31Р =4; // Это в любом случае - значение по умолчанию _ОС31Р = 4; -OC4IP = 4; TMR3 =0; // Обнуляем таймер PR3 = H_NTSC; // Инициализируем регистр периода для строки NTSC // 2.1. Конфигурируем модуль Timer3 T3CON = 0x8000; // Включен, предделитель 1:1, внутренний такт // 2.2. Инициализируем прерывания от Timer3/OC3/OC4, сбрасываем флаг _OC3IF = 0; OC3IE = 1;
202 Глава 12. Черный экран _OC4IF = 0; -OC4IE = 1; _T3IF = 0; _Т31Е = 1; // 2.3. Инициализируем уровень приоритета для процессора _1Р = 0; // Это в любом случае - значение по умолчанию // Инициализируем модуль SPI1 if (Р1Х_Т == 2) SPI1CON1 = 0x043В; // Ведущий, 16 бит, SCK/SS отключены, // предделитель 1:3 else SPI1CON1 = 0x0437; // Ведущий, 16 бит, SCK/SS отключены, // предделитель 1:2 SPI1CON2 = 0x0001; // Расширенный режим, 8 х FIFO SPI1STAT = 0x8000; // Активизируем порт SPI // Инициализируем порт F для сигнала Sync -TRISG0 = 0; // Вывод SYNC - выход // Инициализируем конечный автомат вертикальной синхронизаци VState = SV_PREEQ; VCount = PREEQ—N; } // initVideo Обратите внимание на использование параметра Р1Х_Т для выбора разных ко- эффициентов предделителя тактового сигнала модуля SPI с целью адаптации к раз- личным требованиям, предъявляемых к горизонтальному разрешению. Установка Р1Х_Т = 3 дает наименьшее искажение изображения, выделяя под каждый пиксель три тактовых цикла. Это дает в сумме 187,5 нс, что очень близко к значению 200 нс, ранее вычисленному для горизонтального разрешения 256 пикселей. Разработка видеомодуля Теперь мы можем завершить кодирование полного конечного автомата, добавив все необходимые определения и распределения выводов: /* * * NTSC-видео с помощью прерываний от модулей ТЗ и Output Compare * / #include <p24fj128ga010.h> #include ’’Graphic.h” // Определения портов ввода-вывода # define SYNC _LATG0 // Выход # define SDO RF8 // SPI1 SDO // Определения для конечного автомата формирования // NTSC-видео по вертикали ttdefine V_NTSC 262 // Общее число строк в кадре # define VSYNC_N 3 // Строки вертиклаьной синхронизации // Подсчитываем количество fldefine VBLANK_N (V_NTSC - ttdefine PREEQ_N VBLANK_N/2 оставшихся черных линий сверху и снизу VRES - VSYNC_N) // Переднее уравнивание + пустые строки // снизу
Полет 203 #define POSTEQ_N VBLANK_N - PREEQ_N // Заднее уравнивание + пустые // строки сверху // Определение конечного автомата вертикальной синхронизации #define SV_PREEQ 0 #define SV_SYNC 1 tfdefine SV_POSTEQ 2 #define SV_LINE 3 // Определения для конечного автомата формирования NTSC-видео // по горизонтали ♦define H_NTSC ♦define HSYNC_ 1018 T 90 _T 90 3 // Общее количество Тсу в строке (63,5 мкс) // // // // // Тсу в импульсе горизонтальной синхронизации Тсу в задней площадке строчного интервала #define #define BPORCH PIX-T гашения Тсу в каждом пикселе только 2 и 3) (корректные значения - #define FAR attribute _((far)) int _FAR VMap[VRES * (HRES/16)]; volatile int *VPtr; volatile int HCount, VCount, VState, HState; // Таблица "Следующее состояние" int VS[4] = { SV—SYNC, SV_POSTEQ, SV_LINE, SV_PREEQ}; // Таблица "Следующий счетчик№ int VC[4] = { VSYNC_N, POSTEQ_N, VRES, PREEQ_N}; void _ISRFAST _T3Interrupt(void) ( // Начинаем импульс Sync SYNC = 0; // Декрементируем счетчик по вертикали VCount—; // Конечный автомат для вертикали switch (VState) { case SV_PREEQ: // Импульс горизонтальной синхронизации OC3R = HSYNC_T; OC3CON = 0x0009; // Одиночное событие break; case SV_SYNC: // Импульс вертикальной синхронизации OC3R = H_NTSC - HSYNC_T; OC3CON = 0x0009; // Одиночное событие break; case SV_POSTEQ: // Импульс горизонтальной синхронизации OC3R = HSYNC_T; OC3CON - 0x0009; // Одиночное событие //Во время последнего заднего уравнивания готовимся к новому кадру if (VCount == 0)
204 Глава 12. Черный экран VPtr = VMap; } break; default: case SV_LINE: // Импульс горизонтальной синхронизации OC3R = HSYNC_T; OC3CON = 0x0009; // Одиночное событие // Активизируем модуль ОС4 для SPI-загрузки OC4R = HSYNC-T + BPORCH_T; OC4CON = 0x0009; // Одиночное событие HCount = HRES/128; // Загружаем 8x16 бит за раз break; } //switch // Расширение конечного автомата if (VCount == 0) { VCount = VC[VState]; VState = VSfVState] ; } // Сбрасываем флаг прерывания _T3IF = 0; } // Прерывание от ТЗ Для того чтобы сделать этот модуль по-настоящему библиотечным, необходимо добавить в него рассмотренные выше подпрограммы обслуживания прерываний для модулей ОСЗ и ОС4, а также — пару дополнительных функций: void clearScreen(void) { int i, j; int *v; v = (int *)&VMap[0]; // Очищаем экран for (i=0; i < (VRES*(HRES/16)); i++) *v++ = 0; } //clearScreen void haltvideo(void) { T3CONbits.TON = 0; // Отключаем конечный автомат } //haltvideo void synchV( void) { while (VCount ! = 1); } // synchV Функция clearScreen удобна для инициализации карты памяти изображения (массива VMap), а функция haltvideo — для приостановки формирования видео- сигнала в том случае, если какая-либо задача требует 100% процессорного времени.
Полет 205 Функцию synchV используют для синхронизации задачи с видеогенератором. Она возвращает некоторое значение только в том случае, когда видеогеиератор на- чал прорисовку последней строки экрана. Это может пригодиться графическим приложениям для минимизации мерцания и/или обеспечения более плавного про- листывания и движения изображения. Сохраните все эти функции в файле graphic . с и добавьте этот файл в новом проекте Video. Затем создайте новый файл и добавьте в него следующие определе- ния: /* ★ ★ NTSC-видео ★ ★ Графическая библиотека * / # define VRES 192 // Требуемое разрешение по вертикали ttdefine HRES 256 // Требуемое разрешение по горизонтали void initVideo(void); void haltvideo(void); void clearScreen(void); void synchV(void) ; extern int VMap[HRES/16*VRES ] ; Сохраните этот файл под именем graphic. h и добавьте его в тот же проект. ©Файлы Graphic, с и Graphic, h находятся также на прилагаемом к книге компакт-диске в папке Проекты\12 - BnfleoXGraphic. Обратите внимание, что для внешнего мира доступны только два параметра: разрешение по вертикали и по горизонтали. В разумных пределах (обусловленных временными ограничениями и многими другими аспектами, рассмотренными в пре- дыдущих подразделах) их можно изменить для адаптации к конкретным требовани- ям приложения. Конечный автомат и все остальные механизмы модуля видеогене- ратора примут соответствующие временные характеристики автоматически. Тестирование видеогенератора Для того чтобы протестировать только что созданный модуль видеогенератора, нам потребуется только имитатор MPLAB SIM и несколько строк кода главной про- граммы: "77......................................................................... // GraphicTest.с И // Тестирование базового графического модуля // #include <p24fj128ga010.h> tfinclude ”../graphic/graphic.h” main() { // Инициализация TRISA = 0xff80; clearScreen(); initVideo(); // Младший разряд порта A - выход для отладки // Инициализируем видеокарту // Активизируем конечный автомат
206 Глава 12. Черный экран // Главный цикл while(1) ( } // Главный цикл } // main ®Файл GraphicTest.с находится также на прилагаемом к книге компакт-диске в папке Проекты\12 - Видео. Сохраните и перекомпонуйте проект, откройте окно Logic Analyzer и добавьте в каналы анализатора вывод RGO (Sync) и выход SDO1 (видеосигнал). Теперь може- те запустить на несколько секунд имитацию, а затем нажать кнопку Halt и переклю- читься в окно Logic Analyzer, чтобы просмотреть результат. Объем трассировочной намята имитатора ограничен и допускает визуализацию лишь небольшого фрагмен- та целого видеокадра. Другими словами, скорее всего будет получен не самый ин- тересный результат в виде равномерной последовательности синхроимпульсов и ровного видеосигнала. К сожалению, имитатор не поддерживаег выход порта SPI, поэтому придется ждать до момента запуска приложения на реальной аппаратуре. Что касается линии Sync, то здесь нас интересует момент формирования сигна- ла вертикальной синхронизации с помощью последовательности трех продолжи- тельных импульсов горизонтальной синхронизации в начале каждого кадра. Уста- новив точку прерывания на первой строке поднрохраммы обслуживания прерыва- ния от модуля ОС4 (впервые вызывается в начале первой строки изображения), можно обеспечить остановку имитации ближе к началу кадра (рис. 12.12). Рис. 12.12. Изображение в окно Logic Analyzer: импульсы вертикальной синхронизации. Набравшись терпения, можно подсчитать количество строк (одна на импульс синхронизации) после трех вертикальных (длительных) импульсов вертикальной синхронизации и удостовериться в том, что их — 33 (т.е. (262-192-3) / 2). Кроме то- го, можно приблизить центральную часть диахраммы, чтобы убедиться в коррект- ности временных характеристик импульсов синхронизации в области переднего/ заднего уравнивания и строк вертикальной синхронизации. Курсоры окна Logic Analyzer позволяют проконтролировать количество циклов, составляющих период горизонтальной строки, и ширину импульса горизонтальной синхронизации. Не забывайте, что окно Logic Analyzer аппроксимирует считанные
Полет 207 значения по ближайшему экранному пикселю, поэтому точность результата зависит от степени приближения и разрешающей способности экрана. Само собой, если не- обходимо установить временной интервал с абсолютной точностью, то самый пра- вильный мегод — воспользоваться функцией секундомера программного имитатора MPLAB SIM совместно с соответствующим образом расставленными точками пре- рывания. Рис. 12.13. Приближенное изображение одной строки с областью переднего уравнивания Оценка производительности Поскольку модуль видеогенератор использует три различных источника преры- ваний, а конечный автомат реализует четыре состояния, было бы неплохо выяснить фактическую загрузку процессора. Это можно сделать с помощью окна Logic Analy- zer, отобразив храфически расход процессорного времени внухри разных нодиро- грамм обслуживания прерываний (рис. 12.14). 2055000.0 2055200.0 2055400.0 2055600.0 2055000.0 Рис. 12.14. Содержимое окна Logic Analyzer, позволяющее оценить производительность
208 Глава 12. Черный экран Для этого нам понадобится внести несколько простых модификаций в каждую из трех подпрограмм обслуживания прерываний. Вывод RA0 порта А мы использу- ем в качестве флага, который будет устанавливаться в момент входа в подпрограм- му и сбрасываться при выполнении главного цикла: void _ISRFAST _T3Interrupt(void) { _RA0=l; _RA0=0; } // Прерывание от ТЗ void -ISRFAST _OC3Interrupt(void) { _RA0=l; _RA0=0; } // Прерывание от ОСЗ void _ISRFAST _OC4Interrupt(void) { _RA0=l; _RA0=0; } // Прерывание от ОС После перекомпиляции и добавления в перечень каналов, фиксируемых логиче- ским анализатором, вывода RA0, можно приблизить изображение одного периода горизонтальной строки (выбрать строку изображения). Измерив с помощью курсоров окна Logic Analyzer приблизительную длитель- ность каждой подпрограммы обслуживания прерываний и просуммировав значения для худшего случая (т.е. когда для строки изображения возникали все четыре пре- рывания), мы получим 200 циклов из периода строки, равного 1 018 циклам. Таким образом, потребление процессорного времени составляет менее 20%, что очень и очень хорошо. Черный экран Конечно, какое-то время эксперименты с имитатором и логическим анализато- ром могут вызывать интерес, однако я уверен, что читатель уже рвется в бой и хочет' попробовать что-то настоящее. Видеоинтерфейс можно протестировать на обычном телевизоре (или любом другом устройстве, поддерживающем полный видеосигнал NTSC), соединенном с микроконтроллером PIC24 с помощью простого интерфейса с тремя резисторами. В случае использования платы Explorer 16 придется взять в ру- ки паяльник и разместить вышеупомянутые три резистора на небольшой области макетирования в правом верхнем углу демонстрационной платы, а затем подклю- чить их к стандартному видеоштекеру RCA. Если читатель чувствует, что это ему по силам, то он может даже разработать небольшую печатную плазу для подключе- ния к разъемам расширения Explorer 16. ПРИМЕЧАНИЕ Информацию о готовых платах расширения, которые пригодятся при разработке сложных проектов, рассмотренных далее в этой части, можно найти на Web-сайте www.flyingt:hepic24.com.
Полет 209 Какой бы вариант ни был избран, после подачи питания на плазу Explorer 16 эк- ран будет совершенно пустым (или точнее сказать, черным). Конечно, это — уже достижение, поскольку означает, что сигналы горизонтальной и вертикальной син- хронизации были декодированы телевизором корректно, благодаря чему и получи- лось равномерное заполнение черным цветом. Рис. 12.15. Черный экран Тестовый образец Теперь можно приступать к заполнению видеомассива чем-то более привлека- тельным (чем-то несложным, что позволило бы сразу удостовериться в корректном функционировании видеогенератора). Создадим новую тестовую программу: 7/................................................. // GraphicTest2.с // // Тестирование базового графического модуля // #include <p24fj128ga010.h> tfinclude "../graphic/graphic.h” main () { int x, y; // Заполняем карту видеопамяти образцом for(y=0; y<VRES; y++) for (x=0; x<HRES/16; x++) VMap[y*16 + x]= y; initVideoO; // Активизируем конечный автомат // Главный цикл while (1) { } // Главный цикл } // main Файл GraphicTest2.с находится также на прилагаемом к книге компакт-диске в папке Проекты\12 - Видео.
210 Глава 12. Черный экран На этот раз вместо вызова функции clearScreen для инициализации массива VMap используются два вложенных цикла. Внешний цикл (по у) подсчитывает вер- тикальные строки, а внутренний (по х) реализует смещение по горизонтали, запол- няя 16 слов (каждое по 16 бит) одним и тем же значением: номером строки. Други- ми словами, для первой строки каждое 16-разрядпое слово будет содержать значе- ние 0, для второй — значение 1 и т.д. до достижения последней строки (192), где каждому слову будет присвоено значение 191 (шестнадцатеричное OxBF). В результате компоновки проекта и проверки результата на видеовыходе мы получим па экране узор, показанный на рис. 12.16. Несмотря на простоту этого образца, мы можем многое увидегь. Прежде всего, обратите внимание, что каждое слово визуально представлено па экране в двоичном виде, где старший разряд находится слева. Эго объясняется порядком выдачи битов модулем SPI, когда первым передается именно старший значащий разряд. Во-вто- рых, мы видим, что последняя строка содержит ожидаемый образец OxOObf, из че- го можно сделать вывод, что на экран выведены все строки карты памяти. Различ- ные устройства вывода (телевизоры, проекторы, ЖК-папели и т.д.) фиксируют изо- бражение более-менее эффективно и в состоянии представить его более резким в за- висимости от фактического разрешения экрана и входной полосы пропускания. Итак, с помощью микроконтроллера PIC24 мы получили па экране вертикаль- ные полосы, что не такое уж и малое достижение. Фактически, для того чтобы пра- вильно выровнять каждый пиксель строка за строкой и получить строго вертикаль- ную линию, отклик па прерывания от таймера должен быть лишен малейшего иска- жения — выдающаяся характеристика архитектуры всех микроконтроллеров PIC. Впрочем, это не означает, что совершенно никаких недостатков не будет также и на больших экранах. Наверняка, проявятся небольшие паразитные изображения и, воз- можно, — незначительные визуальные дефекты. Будем реалистами. Простой ин- терфейс с тремя резисторами — это, все-таки, не панацея. По большому счету, рассматриваемый интерфейс полного видеосигнала не от- личается очень высоким качеством результата. Это и не удивительно. Как, навер- ное, известно читателю, для обеспечения более устойчивого и корректного изобра- жения в S-Video, VGA и большинстве других видеоинтерфейсов сигналы яркости и синхронизации разделены.
Полет 211 Построение изображений Итак, удостоверившись в корректности функционирования модуля графическо- го дисплея, мы можем сосредоточиться на формировании фактических изображе- ний, и первый естественный шаг — разработать функцию, которая позволяет засве- тить один пиксель в точке экрана с точными координатами (х, у). Для этого необ- ходимо вначале получить из координаты у помер строки. Если координаты х и у основаны на традиционном декартовом представлении с началом координат, распо- ложенном в левом верхнем углу экрана, то перед доступом к карте памяти потребу- ется инвертировать адрес, чтобы первой строке в карте соответствовала максималь- ная координата у VRES-1 или 189, а последней строке — координата у = 0. Кроме того, поскольку паша карта памяти организована строками по 16 слов, полученное количество строк необходимо умножить на 16, чтобы получить адрес первого слова в заданной строке: VMap [ (VRES-1-у) *16]. Пиксели сгруппированы в 16-разрядпые слова, поэтому для получения коорди- наты х вначале необходимо идентифицировать слово, содержащее требуемый пик- сель. Простое деление па 16 даст нам смещение слова в строке. Добавив это смеще- ние к адресу строки, вычисленному по представленной выше формуле, мы получим полный адрес слова внутри карты памяти: VMap[(VRES-1-у)* 16 + (х/16)] Для оптимизации расчетов адреса операции умножения и деления можно реа- лизовать с помощью сдвига: VMap [ (VRES-1-у) « 4 + (х»4)] Для идентификации внутри слова позиции разряда, соответствующей требуе- мому пикселю, можно использовать остаток от деления х па 16 или, что более эф- фективно, — маскировать младшие четыре разряда координаты х. Поскольку нам необходимо засветить пиксель, применим двоичную операцию “ИЛИ” к маске с единственным установленным разрядом в соответствующей пози- ции пикселя. Помня о том, что дисплей помещает старший разряд каждого слова слева (модуль SPI выдает первым старший значащий разряд), маску можно сформи- ровать с помощью следующего выражения: (0x8000 » (х & Oxf)) Сложив все элементы вместе, получаем главную функцию рисования: VMap [ ( (VRES-1-у) «4) + (х»4)] |= (0x8000 » (х & Oxf)) В качестве последнего штриха добавим “обрезку”, т.е. простую проверку того, что заданные координаты корректны и находятся в границах текущего экрана: void Plot(unsigned х, unsigned у) { if ((x<HRES) && (y<VRES)) VMap [ ( (VRES-l-y) «4) + (x»4)] |= (0x8000 » (x & Oxf)); } // plot Определив параметры x и у как беззнаковые целые, мы гарантируем, что отри- цательные значения будут также отброшены, поскольку они воспримутся как длин- ные числа вне границ разрешения экрана.
212 Глава 12. Черный экран Звездная ночь Для проверки только что созданной функции рисования создайте новый проект и включите в пего файлы graphic, с и graphic, h (напоминаем, что их можно найти па прилагаемом к книге компакт-диске в папке Проекты\12 ~ Видео\ Graphic. Кроме того, мы воспользуемся генератором псевдослучайных чисел, реа- лизованным в стандартной С-библиотеке s tdlib. h: 77...................................................................... // GraphicTest3. с // // Тестирование базового графического модуля // Хаотическое рисование точек // #include <p24fj128ga010.h> tfinclude "../graphic/graphic.h" #include <stdlib.h> void plot(unsigned x, unsigned y) { if ((x<HRES) && (y<VRES) ) VMap [ ( (VRES-l-y) «4) + (x»4) ] |= (0x8000 » (x & Oxf) ) ; } // plot main () { int i; // Инициализация clearScreen(); // Инициализируем видеокарту initVideo(); // Активизируем конечный автомат srand(13); // Инициализируем генератор псевдослучайных чисел for(i=0; К1000; i++) { plot(rand()%HRES, rand()%VRES); } // Главный цикл while(1) { } // Главный цикл } // main Файл GraphicTest3. с находится также на прилагаемом к книге компакт-диске в папке Проекты\12 - Видео. Теперь изображение па экране должно напоминать звездное небо (рис. 12.17). Впрочем, это небо не отличается реалистичностью, потому что в нем отсутствуют какие-либо звездные скопления. Другими словами, не достает Млечного Пуги! Но зато мы удостоверились, что генератор псевдослучайных чисел работает исправно. Теперь функцию plot можно добавить в модуль graphic, с. Не забудьте также добавить прототип в файле graphic. h, чтобы мы могли воспользоваться им в последующих упражнениях: void plot(unsigned, unsigned);
Полот 213 Рис. 12.17. На экране появилось звездное небо Рисование линий Следующий очевидный этап — рисование линий или, если говорить точнее, — сегментов линий. Горизонтальные и вертикальные сегменты не представляют про- блемы (для их рисования достаточно одного цикла for), чего нельзя сказать о на- клонных линиях. Будет правильно начать с базовой формулы для линии, проведен- ной между двумя точками, знакомой нам еще со школы: у = уО + (yl - уО)/(х 1 - хО) * ( х - хО), где (хО,уО) и (х 1 ,у1) — координаты двух точек. Эта формула дает нам для любого значения х соответствующую координату у, поэтому мы используем ее в цикле для каждого отдельного значения х между на- чальной и конечной точками линии:____________________ 77""".~........................ " ................................ // Line Testi.с // // Тестирование базовой функции рисования линий // ♦include <p24fj128ga010.h> ♦include "../graphic/graphic.h" main() { int x; float xO = 10, yO = 20, xl = 200, yl = 150, x2 = 20, y2 = 150; // Инициализация clearScreen(); // Инициализируем видеокарту initVideo(); // Активизируем конечный автомат // Рисуем наклонную линию (х0,у0) - (xl,yl) for(x=x0; x<xl; х++) plot(х, у0+(yl-yO)/(xl-хО)*(х-хО)); // Рисуем вторую (почти отвесную) линию (х0,у0) - (х2,у2)
214 Глава 12. Черный экран for(x=xO; х<х2; х++) plot (х, у0+(у2-у0)/(х2-х0)*(х-хО)) ; // Главный цикл while (1) { } // Главный цикл } // main в Файл Line Testi. с находится также на прилагаемом к книге компакт-диске в папке Проекты\ 12 - Видео. Приемлемый результат получится только для первого, более пологого сегмента, для которого расстояние по горизонтали (х1 - хО) больше расстояния по вертикали (у1 - уО). Вторая линия отображается в виде разрозненных точек, что нас никак не может устраивать (рис. 12.18). Кроме того, нам приходится использовать арифмеги- ку вещественных чисел, которая по сравнению с целочисленной требует значитель- но больше вычислительных ресурсов. Рис. 12.18. Рисование наклонных линий Алгоритм Брезенхема В далеком 1962 году во время работы в лаборатории IBM в Сан-Хосе Джек Бре- зенхем (Jack Bresenham) разработал алгоритм рисования линий, использующий ис- ключительно целочисленную арифметику. Сегодня его считают основанием любой компьютерной графической программы. Алгоритм Брезенхема построен на трех ас- пектах оптимизации: • сокращение направлений рисования до одного (слева направо); • уменьшение вариантов крутизны линии до одного, когда наибольшей является горизонтальная размерность; • умножение обеих сторон уравнения на размерность по горизонтали (de It ах) для получения только целых величин. В результате код рисования линий выходит компактным и чрезвычайно эффек- тивным. Рассмотрим его вариант, адаптированный для нашего видеомодуля: tfdefine abs(а) ( ( (а)> 0) ? (а) : -(a))
Полет 215 void line( int xO, int yO, int xl, int yl) { int steep, t ; int deltax, deltay, error; int x, y; int ystep; steep = (abs(yl - yO) > abs(xl - xO) ) ; if (steep) { // Переставляем x и у t = xO; xO = yO; yO = t; t = xl; xl = yl; yl = t; } if (xO > xl) { // Завершаем перестановку t = xO; xO = xl; xl = t; t = yO; yO = yl; yl = t; } deltax = xl - xO; deltay = abs(yl - yO) ; error = 0; У = yO; if (yO < yl) ystep = 1; else ystep = -1; for (x = xO; x < xl; x++) { if (steep) plot(y,x); else plot(x,y); error += deltay; if ( (error«l) >= deltax) { у += ystep; error -= deltax; } // if } // for } // line Эту функцию можно добавить в видеомодуль graphic . с и объявить в заголо- вочном файле graphic. h. Для проверки эффективности алгоритма Брезенхема создайте новый проект, включив в пего библиотеку stdlib.h для доступа к генератору псевдослучайных чисел. Представленный ниже пример программы вначале рисует по краю экрана рамку, а затем использует функцию рисования линий, в которую передаются хаоти- чески выбранные координаты (рис. 12.19). Кроме того, в главном цикле проверяется состояние кнопки S3 (крайняя слева у нижнего края демонстрационной платы Ex- plorer 16), нажатие которой приводит к очистке экрана и формированию нового на- бора линий. 77..................................... // Bresenham.c // // Пример использования алгоритма Брезенхема // ♦include <p24fj128ga010.h> ♦include <stdlib.h> ♦include "../graphic/graphic.h"
216 Глава 12. Черный экран main () { int i; // Инициализация initVideo(); // Активизируем конечные автоматы srand(12); // Главный цикл . while(1) { clearScreen(); line(О, О, О, VRES-1); line (О, VRES-1, HRES-1, VRES-1); line(HRES-l, VRES-1, HRES-1, 0) ; line (0, 0, HRES-1, 0) ; for( i = 0; i<100; i++) line(rand()%HRES, rand()%VRES, rand()%HRES, rand()%VRES); // Ожидаем нажатие кнопки while(1) { if (!_RD6) break; } // Ожидание } // Главный цикл } // main Файл Bresenham.c находится также на прилагаемом к книге компакт-диске в папке ПроектыХ 12 - Видео. Рис. 12.19. Результат тестирования алгоритма Брезенхема Скорость этого алгоритма рисования впечатляет! Благодаря ему, микрокон- троллеру PIC24 вполне “по зубам” даже задача прорисовки тысяч линий.
Полет 217 Рисование математических функций Итак, у нас есть завершенный графический модуль, и мы можем приступать к разработке интересных примеров его применения. Начнем с классической задачи рисования графика на основании данных, полученных от датчика. Для нашей де- монстрационной программы упростим эту задачу до вычисления значений некото- рой математической функции. Возьмем, к примеру, искаженную синусоиду: у(х) = х * sin(x). Предположим, нам необходимо нарисовать график этой функции для значений х в диапазоне от 0 до 8тг. С помощью небольших манипуляций эту функцию можно адаптировать под размеры нашего экрана, а также входной диапазон 0..200 и вы- ходной 4-75.-75: . _ ________ 7* ** Рисование графика функции * * */ ^include <p24fj128ga010.h> #include <math.h> #include "../graphic/graphic.h" ttdefine XO 10 #define Y0 (VRES/2) #define PI 3.141592654f main ( void) { int x, y; float xf, yf; // Инициализация clearScreen(); initVideo(); // Рисуем оси X и Y, пересекающиеся в точке (X0,Y0) line(X0, 10, XO, VRES-10); // Ось Y line(X0-5, Y0, HRES-10, Y0) ; // Ось X // Рисуем график функции for(x=0; х<200; х++) { xf = (8 * PI / 200) * (float) x; yf = 75.0 / ( 8 * PI) * xf * sin (xf); plot(x+XO, yf+YO); } // Главный цикл while(1); } // main Файл Graphic!, с находится также на прилагаемом к книге компакт-диске и папке проекты\12 - Видео.
218 Глава 12. Черный экран Учитывая, что точки графика постепенно отдаляются друг от друга (рис. 12.20), для соединения каждой новой точки с предыдущей можно воспользоваться алго- ритмом рисования линий. Рис. 12.20. График синусоидальной функции Двухмерная визуализация трехмерных функций Рисование графиков трехмерных функций в двух измерениях — задача поинте- ресней. Перед нами открывается перспектива искажения перспективы (прошу про- щения за каламбур), и возможность соединить точки для формирования сетки. Простейший метод втиснуть третью ось в двухмерное изображение — воспользоваться так называемым изометрическим проецирова- нием: методом, который требует минимум вы- числительных ресурсов и дает лишь незначи- тельное визуальное искажение. Представленные ниже формулы, примененные к координатам х, у и z некоторой точки в трехмерном простран- стве, дают координаты рх и ру проекции на двухмерную плоскость (наш видеоэкран) (рис. 12.21): рх = х+у/2; ру = z 4- у/2; Рис. 12.21. Изометрическое проецирование Для рисования трехмерного графика функции z = Дх,у) мы воспользуемся сет- кой точек, равномерно расположенных в плоскости XY, для создания которой при- меним два вложенных цикла. Для каждой точки будем вычислять функцию, чтобы получить координату z, и использовать изометрическое проецирование для вычис- ления пары координат (рхру). Затем с помощью сегмента мы соединим только что рассчитанную точку с предыдущей точкой в том же ряду (или в том же столбце). В то время как отслеживание координат предыдущей точки в том же ряду — задача довольно простая, храпение координат точек для всех ранее рассчитанных рядов может потребовать значительного объема памяти. Например, если мы исполь- зуем сетку размерами 20x20, то нам потребуется хранить координаты 400 точек. На
Полет 219 каждую из них отводится целое число, что в результате дает 800 слов или 1 600 байт драгоценного ОЗУ. В действительности же, как очевидно из рис. 12.22, нам доста- точно лишь координат точек на прорисованном “краю” сетки. Таким образом, тре- бование к расходу памяти сокращается всего лишь до 20 координатных пар. Рис. 12.22. Рисование сетки для двухмерной визуализации трехмерной функции Представленная ниже программа визуализирует график функции z(x,y) = l/sqrt( х2 +у2) * cos(sqrt( х2 + у2) для значений х и у в диапазоне -3тг..+3 п,........... ............... 7*...................................... """ ................. ** Рисование графика трехмерной функции tfinclude <p24fj128ga010.h> #include <math.h> #include "../graphic/graphic.h” #define X0 10 ^define Y0 10 #define PI 3.141592654f #define NODES 20 #define SIDE 10 typedef struct { int x; int y; } point; point edge[NODES], prev; main( void) { int i, j, x, y, z; float xf, yf, zf, sf; int px, py; // Инициализация clearScreen(); initVideo(); // Рисуем оси X, Y и Z, пересекающиеся в точке (Х0,Y0)
220 Глава 12. Черный экран linefXO, 10, XO, VRES-50); // Ось Z line(X0-5, YO, HRES-10, YO); // Ось X line(X0-2, YO-2, X0+120, Y0+120); // Ось Y // Инициализируем массив точек предыдущего края for(j = 0; j<NODES; j++) { edge[j].x = X0+ j*SIDE/2; edge[j].у = Y0+ j*SIDE/2; // Рисуем график функции for(i=0; i<NODES; i++) { // Преобразовываем диапазон координаты X в 0..200 со сдвигом 100 х = i * SIDE; xf = (6 * PI / 200) * (float)(x-100); prev.y = Y0; prev.x = X0 + x; for (j=0; j<NODES; j++) ( // Преобразовываем диапазон координаты Y в 0..200 co сдвигом 100 у = j * SIDE; yf = (6 * PI / 200) * (float)(y-100); // Вычисляем функцию sf = sqrt(xf * xf + yf * yf) ; zf = 1/(1 + sf) * cos(sf); // Масштабируем результат z = zf * 75; // Применяем изометрическую перспективу и смещение рх = Х0 + х + у/2; ру = Y0 + z + у/2; // Рисуем точку plot(рх, ру) ; // Рисуем соединительные линии для визуализации сетки line (рх, ру, prev.x, prev.y); // Соединяем предыдущую точку //с той же координатой X line(px, ру, edge[j].x, edge[j].у); // Обновляем предыдущие точки prev.x = рх; prev.y = ру; edge[j].х = рх; edge[j].у = ру; } // for j } // for i // Главный цикл while(1); // main
Полет 221 Файл Graph2d.с находится также на прилагаемом к книге компакт-диске в папке Проекты\12 - Видео. После компоновки проекта и подключения к дисплею обратите внимание на скорость прорисовки графика микроконтроллером PIC24, хотя для этого требуется значительный объем вычислений с вещественными числами (функция последова- тельно применяется к 400 точкам, а в видеопамяти отображается 800 сегментов ли- ний) (рис. 12.23). Рис. 12.23. График трехмерной функции Фракталы Термин “фрактал” изобрел в 1975 году математик Бенуа Мандельброт (Benoit Mandelbrot) (участвовал в исследовательских проектах лаборатории IBM) для обо- значения геометрической структуры с дробной размерностью, обладающей свойст- вом рекурсивности (каждая ее часть является уменьшенной копией целого). Множе- ство примеров фракталов существует в природе (облака, снег, горы, сети рек и даже кровеносные сосуды), хотя диапазон масштабирования их свойства самоподобия обычно ограничен. Поскольку фракталы хорошо подходят для создания впечатляющих компью- терных изображений, наверное, наиболее популярный пример математического фрактала — множество Мандельброта. Оно определено как подмножество ком- плексной плоскости с повторяющейся квадратичной функцией z2 4- с. Точки (с) ком- плексной плоскости, на которые не “отклоняется” итерация, рассматриваются как часть множества. Поскольку легко доказать, что как только модуль z становится больше 2, итерация обязательно отклоняется (поскольку данная точка не является частью множества), мы можем применить исключение. Проблема заключается в том, что до тех пор, пока модуль z остается меньше двух, мы никак не можем опре- делить момент остановки итерации и объявления точечной области множества. По этой причине компьютерные алгоритмы, реализующие множество Мандельброта, обычно используют аппроксимацию, устанавливая произвольное максимальное чи- сло повторений, за пределами которого точку просто относят к множеству. Рассмотрим пример кодирования внутренней итерации на С: // Инициализация х = хО;
222 Глава 12. Черный экран У = уО; к = 0; // Основная итерация do ( х2 = х*х; у2 = у*у; у = 2*х*у + уО; х = х2 - у2 + хО; к+ +; } while ( (х2 + у2 < 4) && (к < MAXIT)); // Проверка на принадлежность точки множеству Мандельброта if (k == MAXIT) plot(хО, уО); Здесь хО и у0—координаты в комплексном пространстве точки с. Эту итерацию можно повторять для каждой точки квадратичного подмножества комплексной плоскости, чтобы получить изображение целого множества Ман- дельброта. Из литературы известно, что целое множество заключено в диске радиу- сом 2 вокруг начала координат. Таким образом, мы можем разработать первую про- грамму, сканирующую комплексную плоскость в сетке 192x192 точек (для исполь- зования максимального разрешения экрана согласно определениям в пашем видео- модуле), которая пересекается с таким диском:_ 7*..................... ~ .................... ** Графическая демопрограмма визуализации множества Мандельброта */ #include <p24fj128ga010.h> #include "../graphic/graphic.h" #define SIZE VRES #define MAXIT 64 void mandelbrot(float xxO, float yyO, float w) ( float x, y, d, xO, yO, x2, y2; int i, j, k; // Рассчитываем инкременты d = w/SIZE; // Повторяем для каждого экранного пикселя уО = ууО; for (i=0; i<SIZE; i++) { xO = xxO; for (j=0; j<SIZE; j++) { // Инициализация x = xO; У = yO; k = 0; // Основная итерация do {
Полет 223 х2 - х*х; у2 = у*у; у = 2*х*у + уО; х = х2 - у2 + хО; к++; } while ((х2 + у2 < 4) && (к < MAXIT)); // Проверяем принадлежность точки множеству Мандельброта if (к == MAXIT) plotfj, i); // Рассчитываем следующую точку хО хО += d; } // for j // Рассчитываем следующую точку уО уО += d; } // for i } // mandelbrot main () { float x, y, w; // Инициализация initVideo(); // Активизируем конечный автомат // Начальные координаты - левый нижний угол сетки х = -2.0; у = -2.0; // Исходная сторона сетки w = 4 . О; while(1) { clearScreen (); // Очищаем экран mandelbrot(х, у, w); while (1); } // Главный цикл } // main При максимальном числе итераций 64 микро- контроллер PIC24 формирует полное изображение, показанное на рис. 12.24 (так называемая кардиоида Мандельброта), примерно за 30 секунд. Признаюсь, что я экспериментировал с програм- мами визуализации фракталов еще с момента поку- пки своего первого персонального компьютера (это был Sinclair ZX Spectrum, который в те времена на- зывали “домашним компьютером”), когда учился в школе. У меня до сих пор живы в памяти долгие часы, проведенные в созерцании прорисовки изо- бражения па экране дисплея старым добрым про- Рис.12.24. Множество Мандельброта цессором Z80 (он работал на “заоблачной” частоте 3,5 МГц). Когда через несколько лет я приобрел IBM PC XT с процессором 8088,
224 Глава 12. Черный экран тактовая частота которого была лишь немного выше (4 МГц), улучшилось разве что экранное разрешение. В отношении же скорости вычислений, я все так же загружал программу вечером, а результаты получал только угром. Очевидно, что объем вы- числений, необходимых для прорисовки фрактала, зависит от выбранной области и допустимого максимального количества итераций. Честно говоря, когда я впервые запустил рассмотренную выше программу, то был просто поражен скоростью, с которой микроконтроллер PIC24 прорисовал перед моими глазами кардиоиду. Но на этом веселье только начинается. Наибольший интерес в множестве Ман- дельброта представляют краевые области, которые можно отмасштабировать для визуализации бесконечного комплекса деталей. Результирующее изображение мож- но еще больше улучшить, визуализировав не только точки, принадлежащие множе- ству, но также и краевые точки отклонений. Им назначаются цвета в зависимости от их фактической скорости отклонения. Поскольку в пашем распоряжении — только монохромный дисплей, мы воспользуемся обычными черными и белыми полосами, назначенными каждой точке согласно номеру итераций до достижения им макси- мального модуля или максимального количества итераций. Для этого нам понадо- бится откорректировать только одну строку кода из рассмотренного выше примера: // Проверяем принадлежность точки множеству Мандельброта if (к & 1) plot(j, i); Поскольку наилучший способ исследовать мно- жество Мандельброта — выбрать и отмасштабиро- вать новые области, чтобы увидеть их детали, мы мо- жем трансформировать главный программный цикл, реализовав простой пользовательский интерфейс с помощью четырех кнопок на плате Explorer 16. Ус- ловно разобьем наше изображение па четыре квад- ранта, с каждым из которых сопоставим отдельную кнопку. Нажатие этой кнопки должно приводить к приближению изображения (двойному увеличению разрешения и двойному уменьшению размерности сетки w) (рис. 12.25). main () { float х, у, w; Рис. 12.25. Разбиение экрана на четыре квадранта // Инициализация initVideo() ; // Активизируем конечный автомат // Исходные координаты левого нижнего угла сетки х = -2.0; У = ”2.0; // Исходный размер сетки w = 4.0; while(1) { clearScreen (); // Очищаем экран mandelbrot(х, у, w) ; // Рисуем изображение // Ожидаем нажатия кнопки
Полет 225 while (1) { if (!_RD6) { // Первый квадрант w /= 2; у += w; break; } if (!_RD7) { // Второй квадрант w /= 2; у += w; x += w; break; } if (!_RA7) { // Третий квадрант w /= 2; x += w; break; } if (!_RD13) { // Четвертый квадрант w /= 2; break; } } // Ожидание нажатия клавиши } // Главный цикл } // main Проявив немного терпения, мы можем увидеть детали краевых областей фрак- тала, показанные на рис. 12.26. Рис. 12.26. Детали фрактала: а — (+0.25 +] 0.5), w=0.25;6 — (+0.37500 -j 0.57813), w-0.01563; в - (-1.28125 +j 0.3125), w = 0.3125; a - (+0.34375 +j 0.56250), w= 0.03125; д - (+0.34375 +j 0.56250), w = 0.03125
226 Глава 12. Черный экран Текст До сих пор мы рассматривали графическую визуализацию, однако ни для кого не секрет, что информацию на экране обычно снабжают каким-то текстом. Разме- щение текста в видеопамяти — это ничто иное, как рисование точек и линий, что можно реализовать с помощью уже знакомых нам функций. Тем не менее, наиболее эффективный способ отображения текста па графическом дисплее, требующий ме- нее всего кода, — воспользоваться массивом шрифта 8^8. Каждый символ можно изобразить в виде набора пикселей размерами 8x8. На кодирование одной стро- ки в таком наборе требуется один, а для целого сим- вола — восемь байт (рис. 12.27). Таким образом формируется совокупность из 96 буквенных, числовых и пунктуационных символов в порядке следования, соответствующему таблице ASCII. Эта совокупность должна быть представлена в виде одного массива и сохранена во включаемом файле. Для экономии места мы не будем определять первые 32 кода таблицы ASCII, соответствующие раз- личным командам и специальным кодам синхронизации, которые в прошлом ис- пользовались телетайпами и модемами. // // Определение шрифта 8x8 // #define F_OFFS 0x20 // Начальное смещение ^define F_SIZE 0x60 // До сих пор определены только первые 64 символа const char Font8x8[] = { // 20 - Пробел ОЬОООООООО, ОЬОООООООО, ОЬОООООООО, ОЬОООООООО, ОЬОООООООО, ОЬОООООООО, ОЬОООООООО, ОЬОООООООО, // 1 - ! ОЬОООИООО, ОЬОООИООО, ObOOOllOOO, ObOOOllOOO, ObOOOllOOO, ОЬОООООООО, ObOOOllOOO, ОЬОООООООО, e Полный файл Font.h находится на прилагаемом к книге компакт-диске в папке Проекты\12 - Видео\ГопЪ. Обратите внимание, что массив Font8x8 определен с атрибутом const, по- скольку его содержимое во время выполнения программы должно оставаться неиз-
Полет 227 менным. Для экономии драгоценного пространства ОЗУ его лучше всего разместить в области памяти программ (флэш-память микроконтроллера PIC24). Разумеется, читатель может модифицировать содержимое массива Font8x8 в соответствии со своими предпочтениями. Теперь вывод символа на экран заключается в простом копировании одного байта из массива шрифта в требуемую экранную позицию. В простейшем случае символы можно выровнять по словам, составляющим массив VMap (видеопамять), определенный в графическом модуле. Таким образом позиции будут ограничены до 32 символов на строку (256/8), а на экране может быть отображено максимум 24 строки (192/8). Более сложное решение подразумевает абсолютную свободу в позиционирова- нии каждого символа в любой заданной пиксельной координате. Это потребует применения методики, которую часто называют BitBLT (сокращение от “Bit BLock Transfer” — “передача битовыми блоками”). Она очень распространена в мире ком- пьютерной графики и особенно — в разработке видеоигр. Впрочем, мы воспользу- емся более простым подходом, поскольку для нас предпочтительней решение, тре- бующее минимум ресурсов. Создайте новый проект TextOnGPage и файл исходного кода TextOnGPage . с, который будет содержать все функции, необходимые для вывода текста на гра- фической видеостранице. Далее определим две целочисленные переменные для от- слеживания позиции курсора: int сх, су; Теперь можно создать простую функцию вывода на экран одного символа ASCII в текущей позиции курсора: void putcVfint а) ( int i, *р; const char *pf; // 1. Проверка, находится ли символ в требуемом диапазоне а -= F_OFFS; if (а < 0) а = 0; ’ if (а >= F_SIZE) а = F_SIZE-1; // 2. Проверка границ страницы if (сх >= HRES/8) // Циклический перебор для х ( сх = 0; су++; } if (су >= VRES/8) // Циклический перебор для у су = 0; // 3. Устанавливаем указатель на слово в видеокарте р = &VMap[cy * 8 * HRES/16 + сх/2]; // Устанавливаем указатель на первую строку символа в массиве шрифта pf = &Font8x8[a « 3]; // 4. Копируем посимвольно каждую строку на экран for (i=0; i<8; i++) { if (ex & 1) {
228 Глава 12. Черный экран * р &= OxffOO; * р |= *pf++; } else { * р &= Oxff; * р |= (*pf++)<<8; } // Указатель на следующую строку р += HRES/16; } // for // Увеличиваем позицию курсора сх++ ; } // putcV В первых строках функции (пункт 1) мы проверяем, относится ли переданный символ к подмножеству символов ASCII, определенных в нашем шрифте. Если не относится, то он преобразуется в первый или последний из определенных символов. Альтернативный метод — вообще проигнорировать символ и сразу же выйти из подпрограммы. В пункте 2 функции реализован контроль за позицией курсора (с у,с у), чтобы при достижении правого края экрана был выполнен переход к следующей строке. При достижении правого нижнего угла курсор переносится в левый верхний угол. Альтернативный вариант — реализовать пролистывание содержимого экрана на од- ну строку вверх, чтобы освободить место для ввода новой строки. В пункте 3 на основании координат курсора вычисляется указатель на карту эк- ранной памяти, а также указатель на массив шрифта на основании кода символа ASCII. Наконец, в пункте 4 изображение шрифта в цикле построчно копируется в видеомассив (VMap). Поскольку видеомассив организован в виде набора слов (первым отображается старший байт), для правильного позиционирования внутри 16-разрядного слова необходимо уделить немного внимания перед0каждого бай- та. Если позиция курсора — четная, то данными шрифта заменяется старший байт выбранного слова; в противном случае — младший байт. При каждой итерации цикла указатель внутри видеокарты (р) увеличивается на 16 слов (HRES/16), чтобы указывать па ту же позицию в следующей строке. Указатель внутри массива шриф- та (pf) увеличивается на единицу для получения следующего байта, составляющего изображение символа. Для удобства напишем функцию, выводящую на экран всю ASCII-строку с за- вершающим нулевым символом: void putsV(unsigned char *s) { while (*s) putcV( *s++); } // putsV ©Полный файл TextOnGPage. с находится на прилагаемом к книге компакт-диске в папке Проек- ты\12 - BmieoXTextG. Не забудьте включить в модуль все необходимые файлы: tfinclude <p24fj128ga010.h> ^include "../font/font.h" #include "../graphic/graphic.h"
Полет 229 Наконец, создайте новый заголовочный файл, чтобы экспортировать только что определенные функции и добавить пару полезных макросов: 7*........................................................................... ** Текст на графической странице */ extern int сх, су; • void putcV( int a); void putsV( unsigned char *s); #define Home() (cx=0; cy=0;} #define Clrscr() {clearScreen (); Home();} ^define AT(x, y) {ex = (x); cy = (y) ; } ®Файл TextOnGPage.h также находится на прилагаемом к книге компакт-диске в папке Проек- ты\12 - ВидеоХТехГС. Макрос Поте () просто позиционирует курсор в левом верхнем углу экрана. Макрос Clrscr () очищает экран вызовом функции, определенной в графическом модуле. Макрос АТ () позиционирует курсор по требованию следующей команды putcV и/или putsV. Обратите внимание, что в отличие от графической системы координат начало системы координат текстового курсора находится в позиции левого верхнего угла экрана. При этом увеличивается вертикальная координата в соответствии с номера- ми строк вниз по странице. Тестирование модуля TextOnGPage Для быстрого тестирования эффективности нового текстового модуля можно разработать небольшую программу, которая после вывода заголовка в первой стро- ке экрана выведет' каждый символ, определенный в шрифте 8^8: 7*.......................................... ** Тестирование текстовой страницы ★ ★ */ #include <p24fj128ga010.h> ^include "../graphic/graphic.h" #include "../textg/TextOnGPage.h" main( void) { int i; // Инициализация initVideo(); // Активизируем конечный автомат Clrscr(); AT(0, 0); putsV("FLYING THE PIC24’"); AT(0, 2); for(i=32; i<128; i++) putcV( i); while (1); } // main Сохраните этот файл под именем TextOnGTest. с и добавьте его в проект. Удостоверьтесь, что в проект добавлен все остальные необходимые модули, вклю-
230 Глава 12. Черный экран чая graphic . с, graphic . h, font. h, textongpage . с и textongpage. h, по- сле чего скомпонуйте и запустите программу (рис. 12.28). Рис. 12.28. Вывод текста на графической странице Разработка текстовой видеостраницы Рис. 12.29. Показатели потребления памяти для проекта TextOnGTes t Благодаря только что разработанному моду- лю TextOnGPage.с, у нас появилась возмож- ность отображать на видеоэкране текст и графи- ку. Система в целом требует для видеокарты 6 080 байт ОЗУ, что составляет значительную часть от доступного объема оперативной памяти микроконтроллера PIC24fjl28ga010, однако со- всем незначительную долю памяти программ (рис. 12.29). Если бы наше приложение выводило только текст, то такое решение было бы крайне неэф- фективным. По сути, с помощью шрифта 8^8 мы можем отобразить только 32 символа на каждую из 24 строк, что в сумме дает 768 символов. Другими словами, если приложение использует видео исключительно для вывода текста, то мы впустую тратим 5 244 байт драгоценной оперативной памяти. На заре компьютерной техники (включая первые IBM PC) это было серьезной эко- номической проблемой, для решения которой требовалось специальное аппаратное обеспечение. Все ранние персональные компьютеры использовали так называемую “текстовую страницу” — особый видеорежим, в котором дисплей мог визуализиро- вать только текст и ничего, кроме текста. Это значительно снижало требования к расходу оперативной памяти и повышало скорость взаимодействия с экраном. В случае с текстовой страницей коды ASCII-символов сохраняются непосредст- венно в видеопамяти и преобразуются “на лету” в графическое шрифтовое пред- ставление специальным аппаратным устройством (генератором шрифта), тесно свя- занным со схемой видеоразвертки и синхронизации. Таким образом, объем памяти, требуемой для обработки 768 символов (как в нашем проекте), составляет ровно 768 байт, т.е. всего лишь около 10% от объема памяти, потребляемой нашим графиче- ским решением.
Полот 231 Здесь перед нами открываются новые перспективы, и в следующем проекте мы разработаем более эффективный видеомодуль, рассчитанный исключительно па вы- вод текста. Для этого нам придется вернуться к начальному определению конечного автомата. Впрочем, его структура в основном останется нетроиугой, а оптимизации подвергнутся только некоторые ключевые области. Так, мы не будем менять ника- ких элементов, составляющих сигналы горизонтальной и вертикальной синхрониза- ции. Кроме того, останется незатронутой структура горизонтальных строк вплоть до момента начала передачи данных в модуль SPI1. В то время как в графическом дисплее из карты памяти извлекается слово, ко- торое затем помещается в буфер SPI, в случае с текстовой видеостраницей требует- ся побайтная обработка и добавляется этап преобразования. Массив Font8x8 будет выступать в качестве таблицы соответствия, используемой для преобразования кода ASCII (теперь карта VMap определяется как массив байтов) в изображение, переда- ваемое в буфер SPI. В общем случае это преобразование можно описать следующим выражением: lookup = Font8x8[*VPtr * 8 + RCount]; где VPtr — указатель на текущий символ в массиве текстовой страницы, a RCo- unt — счетчик от 0 до 7, который задает’ номер видеостроки, формирующей одну строку текста (восемь видеострок на каждую текстовую строку). На практике все немного сложнее. Поскольку модуль SPI воспринимает' 16-раз- рядные пакеты данных, два символа после двух последовательных выборок из таб- лицы соответствия необходимо объединить в одно слово: lookupl = Font8x8[*VPtr++ * 8 + RCount]; lookup? = Font8x8[*VPtr++ * 8 + RCount]; SPI1BUF = (256 * lookupl + lookup2); Для заполнения буфера SPI эту последовательность необходимо повторить во- семь раз. Теперь нам предстоит выполнить массу работы за те несколько микросекунд, которые предоставляет' подпрограмма обслуживания прерывания от модуля ОС4. Даже если бы в нашем распоряжении был наивысший уровень оптимизации компи- лятора (напоминаю, что в этой книге мы решили никогда не использовать оптими- зацию), вероятность того, что нам удалось бы втиснуться в доступный промежуток (менее 25 мкс), очень невысока. Дело в том, что при обработке таблицы соответст- вия требуется выполнить слишком много операций умножения и сложения. К счастью, мы можем изменить эту ситуацию, реорганизовав способ формирования массива Font8x8. Хотя массив удобнее инициализировать, последовательно заполняя все восемь строк каждого символа, для упрощения выражения соответствия мы применим дру- гой метод. Заполнение массива будет начинаться с первого байта каждого символа шрифта, после чего последует второй байт каждого символа и т.д. В результате представленные выше выражения можно переписать следующим образом для ново- го массива шрифта RFont: lookupl = RFont[(RCount * F_SIZE) + *VPtr++]; lookup2 = RFont[(RCount * F_SIZE) + *VPtr++]; SPI1BUF = (256 * lookupl + lookup2); Большое преимущество данного метода заключается в том, что выражение RCount*F_SIZE дает постоянное смещение, и для его обработки мы даже можем получить указатель внутри шрифта с помощью следующего выражения:
232 Глава 12. Черный экран FPtr = &RFont[RCount * F_SIZE]; Значение указателя можно получать внутри подпрограммы обслуживания пре- рывания от таймера Timer3 в начале каждой строки, что значительно повышает эф- фективность программы. lookupl = FPtr[*VPtr++]; lookup? = FPtr[*VPtr++]; SPI1BUF = (lookupl « 8 + lookup2); Теперь, по крайней мере, у нас появилась возможность уместить выражение для таблицы соответствия в те несколько микросекунд, которые у нас есть. Но этого ма- ло. В такой важной и часто вызываемой подпрограмме, как при обслуживании пре- рывания от модуля ОС4, па счету каждая наносекунда, поэтому для достижения паилучшей оптимизации мы реализуем выборочное ручное кодирование ключевых моментов на ассемблере. Если указатель шрифта FPtr поместить в рабочий регистр W2, а указатель ви- деопамяти VPtr — в рабочий регистр W1, то всю последовательность обращения к таблице соответствий можно закодировать с помощью всего лишь трех ассемб- лерных команд: mov.b [wl++], wO // wO = *VPtr++ (8 bit) ze wO, wO // Расширяем wO до 16-разрядного целого mov.b [w2+w0], w3 // w3 = FPtr[wO] = FPtr[*VPtr++] = lookupl Повторив эти же команды для lookup2, мы можем объединить два значения в одном слове с помощью одной операции сдвига и последующего сложения: si w3, #8, w3 // Сдвигаем 8 разрядов W3 влево (*256) add wO, w3, wO // Складываем (lookupl*256) и lookup2 Все это можно объединить в одном макросе (назовем его DECODE): #define DECODE(sfr) \ asm volatile ("mov.b [wl++], wO"); \ asm volatile ("ze wO, wO"); \ asm volatile ("mov.b [w2+w0], w3"); \ asm volatile ("si w3,#8,w3"); \ asm volatile ("mov.b [wl++], wO"); \ asm volatile ("ze wO, wO"); \ asm volatile ("mov.b [w2+w0], wO") ; \ asm volatile ("ze wO, wO"); \ asm volatile ("add wO, w3, wO"); \ asm volatile ("mov wO, %0" : "=U"((sfr))); Атрибут volatile здесь гарантирует, что компилятор не изменит порядок следования и позицию встроенного ассемблерного кода, если в будущем будет ак- тивизирован оптимизатор. Остановимся также на последней строке макроса, кото- рая для непосвященных может показаться “китайской грамотой”. Дело в том, что здесь использовано расширение синтаксиса встроенного ассемблера, предоставляе- мое компилятором СЗО: возможность смешивать в С-переменных имен, передавае- мых в качестве параметров в функцию asm (). Специальная запись : ”=и” () озна- чает, что операнд, указанный в скобках, является приемником выходных данных. Теперь можно модифицировать подпрограмму обслуживания прерывания от модуля ОС4 для извлечения всех преимуществ из нашего оптимизированного мето- да обработки таблицы соответствий:
Полет 233 void _ISRFAST _0C4Interrupt(void) { // Готовим указатели volatile asm ("mov %0, w2" ::"U" (FPtr)); // w2 = FPtr volatile asm ("mov %0, wl" ::"U" (VPtr)); // wl = VPtr // Преобразование шрифта для восьми слов DECODE(SPI1BUF); DECODE(SPI1BUF); DECODE(SPI1BUF); DECODE(SPI1BUF); DECODE(SPI1BUF); DECODE(SPI1BUF); DECODE(SPI1BUF); DECODE(SPI1BUF); __asm__("mov wl, %0" :"=U" (VPtr)); // Обновляем VPtr if (—HCount > 0) { // Опять активизируем для следующей загрузки по SPI OC4R += (Р1Х_Т * 8 * 16); OC4CON = 0x0009; // Отдельное событие } // Сбрасываем флаг прерывания _OC4IF = 0; } // Прерывание от ОС4 Как было отмечено ранее, модификации в подпрограмме обслуживания преры- вания от таймера ТппегЗ — минимальны, поскольку для обеспечения правильной последовательности текстовых строк и расчета смещения шрифта требуется только пара указателей: void _ISRFAST _T3Interrupt(void) { // Начало импульса синхронизации SYNC = 0; // Декремент счетчика по вертикали VCount—; // Конечный автомат для вертикали switch (VState) { case SV-PREEQ: // Импульс горизонтальной синхронизации OC3R = HSYNC_T; OC3CON = 0x0009; // Отдельное событие break; case SV_SYNC: // Импульс вертикальной синхронизации OC3R = H_NTSC - HSYNC_T; OC3CON = 0x0009; // Отдельное событие break; case SV_POSTEQ: // Импульс горизонтальной синхронизации OC3R = HSYNC_T; OC3CON = 0x0009; // Отдельное событие // При последнем posteq готовим новый кадр
234 Глава 12. Чорный экран if (VCount == 0) { LPtr = VMap; RCount = 0; } break; default: case SV_LINE: // Импульс горизонтальной синхронизации OC3R = HSYNC_T; OC3CON = 0x0009; // Отдельное событие // Активизируем модуль ОС4 для загрузки по SPI OC4R = HSYNC-T + BPORCH_T; OC4CON = 0x0009; // Отдельное событие HCount =3; // Повторно инициализируем счетчик // Готовим указатель шрифта FPtr = &RFont[RCount * F_SIZE]; // Готовим указатель строки VPtr = LPtr; // Приращение RCount if (++RCount == 8) { RCount = 0; LPtr += COLS; } } //switch // Расширение конечного автомата if (VCount == 0) { VCount = VC[VState); VState = VS[VState]; } // Сбрасываем флаг прерывания _T3IF = 0; } // Прерывание от таймера ТЗ Также необходимо внести одно дополнение в подпрограмму инициализации видео, поскольку изменился способ организации массива шрифта: // Подготовка prepare a reversed font table for (i=0; i<8;' i++) { p = Font8x8 + i; for (j=0; j<F_SIZE; j++) { *r++ = *p; p+=8; } // for j } // for i Хотя для простоты мы реализовали второй массив в ОЗУ, конечное решение за- ключается в перестроении определений в файле font.h, чтобы массив Font8x8
Полот 235 был изначально оптимизирован во избежание напрасных трат оперативной памяти, а во время инициализации видео процессорное время не расходовалось на преобра- зования. Когда ранее мы разрабатывали графический интерфейс, то выяснили, что экран размерами 256x192 пикселей — это приемлемый компромисс между разрешением и потреблением памяти, поскольку он оставляет 2 Кбайт ОЗУ для потребностей приложения. Теперь этот баланс сильно изменился. Поскольку дисплей состоит из 24 строк и 32 столбцов, то видеомодулем используется всего лишь 768 байт, а зна- чит мы можем себе позволить немного увеличить разрешение (в первую очередь — горизонтальное). Большинство видеотерминалов используют формат 25x80, в то время как печатный документ содержит в среднем не менее 60 символов в каждой строке. Хотя мы могли бы использовать ОЗУ (25 строк на 80 столбцов - 2 000 симво- лов), на этот раз ограничения накладывают спецификации стандарта NTSC. Как бы- ло отмечено в начале главы, максимальная полоса пропускания для полного видео- сигнала NTSC фиксирована и составляет 4,2 МГц, а период формирования видимой строки изображения равен 52 мкс. Это определяет' максимальное теоретическое раз- решение ио горизонтали в 436 пикселя что в случае шрифта 8x8 дает максимум 54 столбца. На практике обычно выбирают значение меньше 54. Для оптимального исполь- зования механизма FIFO модуля SPI, который мы с успехом применяли до сих пор, лучше всего взять число, кратное 16. В то время как в графическом модуле для за- полнения FIFO-буферов мы использовали два последовательных блока из 128 пик- селей каждый, для текстовой страницы необходимо добавить третий блок, приведя общее горизонтальное разрешение к 48 символам. При этом предделитель SPI дол- жен быть переведен в высокочастотный режим (Р1Х_Т= 2). Для вертикального разрешения стандарт NTSC предоставляет' относительную свободу действий, поскольку определяет 262 строки, из которых для фактического изображения теоретически можно использовать до 253. Таким образом, не составит труда реализовать 25 текстовых строк в виде 200 строк экрана. Итак, наш модуль текстовой страницы будет выдавать изображение из 25 строк и 48 столбцов, что составляет 1 200 байт. Таким образом, для завершения работы над ним необходимо объявить несколько новых констант: /* ** TextPage.c ** Видеомодулъ текстовой страницы ★ ★ */ #include <p24fj128ga010.h> ^include "../Text/TextPage.h" #include ”../font/font.h” // Определения портов ввода-вывода #define SYNC _LATG0 // Вывод #define SDO RF8 // SPI1 SDO // Расчет видеопараметров #define V_NTSC 262 #define VRES (ROWS*8) #define VSYNC N 3 NTSC для конечного автомата для вертикали // Общее число строк, составляющих кадр // Требуемое вертикальное разрешение (<242) // Строки вертикальной синхронизации
236 Глава 12. Чорный экран // Подсчитываем количество оставшихся черных строк вверху и внизу #define VBLANK_N (V_NTSC - VRES - VSYNC_N) #define PREEQ_N VBLANK_N /2 #define POSTEQ_N VBLANK_N - PREEQ_N // Переднее уравнивание + пустые // строки внизу // Заднее уравнивание + пустые // строки вверху // Определения для конечного автомата вертикальной синхронизации ♦define SV_PREEQ 0 ♦define SV_SYNC 1 ♦define SV_POSTEQ ! 2 ♦define SV_LINE 3 // Видеопараметры [ NTSC для конечного автомата для горизонтали ♦define H_NTSC 1018 // Общее количество Тсу в строке // (63,5 мкс) ♦define HRES (COLS*8) // Требуемое горизонтальное // разрешение (кратно 16) ♦define HSYNC_T 72 // Тсу в импульсе горизонтальной // синхронизации (4,7 мкс) ♦define BPORCH_T 90 // Тсу в задней площадке строчного // интервала гашения (4,7 мкс) ♦define PIX_T 2 // Тсу в каждом пикселе ♦define LINE_T HRES * Р1Х_Т // Тсу в каждой горизонтальной // строке изображения // Массив текстовой страницы unsigned char VMap[COLS * ROWS]; unsigned char *VPtr, *LPtr; // Упорядоченный по-новому шрифт unsigned char RFont[F_SIZE*8]; unsigned char *FPtr; volatile int HCount, VCount, RCount, VState, HState; // Таблица "Следующее состояние" int VS[4] = (SV-SYNC, SV_POSTEQ, SV_LINE, SV_PREEQ}; // Таблица "Следующий счетчик" int VC[4] = {VSYNC_N, POSTEQ_N, VRES, PREEQ_N}; Теперь в текущий проект можно добавить те же подпрограммы, которые были разработаны для проекта TextOnGPage. void haltvideo() { T3CONbits.TON =0; // Отключаем конечный автомат для вертикали } //haltvideo void initScreen(void) ( int i, j; char *v; v = VMap; // Очищаем экран for (i=0; i < (ROWS); i++) for (j=0; j < (COLS); j++)
Полот 237 *v++ = 0; } //initScreen int ex, cy; void putcV( int a) ( // Проверяем, находится ли символ в границах шрифта а -= F_OFFS; if (а < 0) а = 0; if (а >= F_SIZE) а = F_SIZE-1; // Проверяем границы страницы if (сх >= COLS) // Циклический возврат х { сх = 0; су++; } су %= ROWS; // Циклический возврат у // Находим первую строку в видеокарте VMap[су * COLS + сх] = а; // Увеличиваем позицию курсора сх++; } // putcV void putsV(unsigned char *s) { while (*s) putcV( *s++); } // putsV void per(void) { ex = 0 ; cy++; cy %= ROWS; } // per Теперь можно сохранить полученный файл под именем Text Раде . с, а также создать новый заголовочный файл TextPage. h. 7*................................. ** TextPage.h ** Видеомодуль текстовой страницы */ #define ROWS 25 // Строк текста #define COLS 48 // Столбцов текста // Массив текстовой страницы extern unsigned char VMap[COLS * ROWS]; // Инициализация видеовывода void initVideo( void);
238 Глава 12. Черный экран // Останов видеовывода void haltvideo(); // Очистка видеокарты void initScreen(void); // Курсор extern int ex, су; void putV(int a) ; void putsV(unsigned char *s) ; void per(void); #define home() {cx=0; cy=0;} #define clrscr() {initScreen (); home();} #define AT(x, y) (ex = (x); cy = (y);} e Файлы TextPage.c и TextPage.h также находятся на прилагаемом к книге компакг-диске В папке Проекты\12 - Видео\ТехГ. Тестирование производительности текстовой страницы Для того чтобы протестировать новый видеомодуль текстовой страницы, мы модифицируем пример “Матрица”, рассмотренный в главе 8, “Асинхронный обмен данными”. Тогда с помощью модуля UART1 было организовано взаимодействие с компьютерным терминалом VT100 (или точнее — с ПК, па котором работает’ про- грамма HyperTerminal, сконфигурированная в режиме имитации протокола VT100 терминалов DEC). Заменим вызовы функции putcU, используемой для передачи одного символа в последовательный порт, вызовами функции putcV, направляю- щей данные в видеоинтерфейс. Создайте новый проект Matrix2 и добавьте в него все необходимые модули, включая rand, с, rand.h, textpage.c, textpage.h и новый модуль, кото- рый мы назовем matrix2 . с. 7*................................................................ ** Новый вариант "Матрицы" */ #include <p24fj128ga010.h> #include "../random/rand.h" ^include "../Text/TextPage.h" Me fine COL 40 #define ROW 24 Mefine DELAY 12000 ((define pcr() (ex = 0; cy++;} main () { . int v[40]; // Вектор с длинами каждой строки int i,j,k; // 1. Инициализация T1CON = 0x8030; // Включаем TMR1, предделитель 256, Tcy/2 initVideo(); clrscr(); // Очищаем экран
Полет 239 randomize(12); // Активизируем генератор псевдослучайных чисел // 2. Инициализируем длину каждого столбца for(j =0; j<COL; j++) v[j] = rand()%ROW; // 3. Главный цикл while(1) { home(); // 3.1. Обновляем экран случайными столбцами for(i=0; i<ROW; i++) { // Обновляем по одной строке for(j=0; j<COL; j++) { // Выводим случайный символ вниз на всю длину столбца if (i < v[j]) putcV('A’ + (rand()%32)); else putcV(' '); } // for j per () ; } // for i // 3.1.1. Задержка для замедления обновления экрана TMR1 = 0; while (TMRKDELAY) ; // 3.2. Хаотически увеличиваем или уменьшаем длину каждого столбца for(j=0; j<COL; j++) { switch (rand()%3) { case 0: // Увеличиваем длину v[ j]++; if (v[j]>ROW) v[j]=ROW; break; case 1: // Уменьшаем длину v[ j] —; if (v[j]<l) v[j]=l; break; default: // Без изменений break; } // switch } // for } // Главный цикл } // main Файл Matrix2. с также находится на прилагаемом к книге компакт-диске в папке Проекты\12 - Видео.
240 Глава 12. Черный экран После сохранения и компоновки проекта запустите его на плате Explorer 16, подключенной к какому-либо видеоустройству. Обратите внимание, насколько бы- стрее теперь обновляется экран, поскольку программа напрямую обращается к ви- деопамяти без ограничений последовательного обмена данными. Кроме того, по- скольку теперь каждый символ в видеопамяти можно извлечь и обработать “на мес- те”, мы можем применить пару новых приемов для приближения изображения к ха- рактеристиками кинофильма. Но, кроме визуального впечатления, нас также интересует фактическое потреб- ление новыми видеофункциями, выполняющими преобразование шрифта, ресурсов процессора. Для этих расчетов мы опять воспользуемся программным имитатором MPLAB SIM. Как и в предыдущих главах, выберем один из выводов порта А (на- пример, RA0) для сигнализации о выполнении кода внутри одной из трех подпро- грамм обслуживания прерывания: void _ISRFAST _T3Interrupt(void) { _RA0=l; _RA0=0; } // Прерывание от ТЗ void _ISRFAST _OC3lnterrupt(void) { _RA0=l; _RA0=0; } // Прерывание от ОСЗ void —ISRFAST _OC4Interrupt(void) { _RA0=l; _RA0=0; } // Прерывание от OC4 He забудьте добавить в функцию initVideo () или главную программу код инициализации регистра TRISA, чтобы активизировать вывод RA0 в качестве вы- хода. Затем добавьте выводы RG0 (отвечает за формирование синхроимпульса) и RA0 в каналы окна Logic Analyzer. Перекомпонуйте проект и запустите программу на короткое время, достаточное для получения первых нескольких сггрок изображения, когда подпрограммы обслу- живания прерываний выполняют больше всего работы (рис. 12.30). Теперь с помощью курсоров окна Logic Analyzer можно измерить количество циклов, необходимое для каждой из четырех подпрограмм обслуживания прерыва- ний во время периодов формирования горизонтальной строки. Хотя точное число циклов можно определить только с помощью средств Stopwatch, логический анали- затор дает хорошее приближение при гораздо меньших трудозатратах. Так, соглас- но моим измерениям на выполнение подпрограмм обслуживания прерываний ви- деомодуля в каждом из периодов в 1 018 циклов ушло 384 цикла, т.е. около 38% от доступных вычислительных ресурсов процессора. Это значение почти в два раза больше, чем в случае с графическим видеомодулем, однако 20% разницы мы с радо- стью жертвуем ради значительного снижения потребления оперативной памяти и увеличения разрешения для приложений с исключительно текстовым выводом.
Разбор полета 241 Разбор полета В этом уроке мы изучили возможности видеовывода при минимальных аппа- ратных затратах на построение интерфейса (всего лишь три резистора). Было рас- смотрено совместное использование четырех периферийных модулей для построе- ния комплексного механизма формирования полного видеосигнала NTSC. Базовый период горизонтальной синхронизации был получен с помощью 16- разрядного таймера, а промежуточные эталонные периоды — с помощью двух мо- дулей сравнения на выходе. Для последовательного вывода видеоданных мы ис- пользовали модуль SPI в расширенном режиме с восьмиуровневым 16-разрядным FIFO-буфером. После разработки базовых графических функций рисования отдельных пиксе- лей и линий были рассмотрены некоторые возможности графического видеовывода, включая визуализацию двух- и трехмерных функций. Кратко исследовав мир фрак- талов, мы перешли к вопросу отображения на экране текста. Вначале были разрабо- таны подпрограммы для добавления текста на графическую страницу, а затем мы создали видеомодуль, оптимизированный под отображение исключительно текста. Советы и хитрости В качестве завершающего штриха в нашем коротком экскурсе в мир графики мы добавим в наши библиотеки немного анимации. Для того чтобы движение было плавным, без раздражающего мерцания изображения на экране применим методику двойной буферизации, согласно которой в каждый момент времени используются два буфера изображения. Один из них — активный (т.е. его содержимое отобража- ется па экране), в то время как в другом (скрытом) прорисовывается новый кадр. По завершении прорисовки во втором буфере он становится активным, а первый буфер скрывается, очищается и используется для формирования следующего изображения. Затем буферы опять меняются местами и т.д. Единственное ограничение на реализацию данной методики в нашем случае на- кладывается объемом ОЗУ. Для того чтобы два буфера поместились в 8 Кбайт памя- ти микроконтроллера PIC24fjl28ga010, оставив достаточно места для переменных
242 Глава 12. Черный экран и стека, придется уменьшить разрешение изображения. Например, можно использо- вать два буфера размерами 160x160, что дает 3 200 байт для каждого: int _FAR VIMap[VRES * (HRES/16)]; int _FAR V2Map[VRES * (HRES/16)]; Другие изменения, которые необходимо внести в проект: • заменить прямые ссылки па массив VMap ссылками па указатели; • реализовать конечный автомат с управлением по прерываниям, который обнов- ляет экран с помощью указателя па активный буфер: int *VA; . • модифицировать функции рисования под использование указателя на скрытый буфер: int *VH; В таком случае переключение между буферами заключается в перестановке местами двух указателей: void swapV(void) { int * V; while (VCount != 1); // Ожидаем окончания кадра V = VA; VA = VH; VH = V; // Переключаем буферы на следующем // импульсе вертикальной синхронизации } //swapV Будьте внимательны, чтобы переключение не произошло посреди кадра, но бы- ло синхронизировано с окончанием текущего и началом следующего кадра. Упражнения 1. Создайте функцию write, с для замены стандартной библиотечной функции stdio. h с целью перенаправления вывода на текстовый/графический экран. 2. Добавьте поддержку ввода с клавиатуры PS/2 для создания завершенной консо- ли. Ссылки • http://en.wikipedia.org/wiki/Zx_spectrum. Компьютер Sinclair ZX Spectrum, выпущенный в начале 1980-х годов, был одной из первых в мире “персоналок”. Его графические возможности во многом напоминали возможно- сти библиотек, разработанных в данной главе. Хотя этот ПК использовал для видеовывода несколько аппаратных устройств, его вычислительная мощь со- ставляет менее 25% от процессорных ресурсов микроконтроллера PIC24. Тем не менее, даже ограниченные возможности формирования цвета (всего 16 цве- тов при разрешении шрифта 8x8) не помешали множеству программистов соз- дать тысячи интересных видеоигр.
ГЛАВА 1 3 Запоминающие устройства большой емкости В этой главе: > Физический интерфейс с картами SD™/MMC Взаимодействие с платой Explorer16 Новый проект > Выбор рабочего режима SPI Передача команд в режиме SPI Завершение инициализации карты SD/MMC Чтение данных из карты SD/MMC > Запись данных в карту SD/MMC > Применение интерфейсного модуля SD/MMC Взаимосвязь между весом (массой) и рабочими характеристиками самолета по- нятна как пилотам, так и людям, далеким от авиации. Если крылья перегрузить, то разбег станет настолько долгим, что не хватит взлетно-посадочной полосы, а значит самолет вообще не взлетит. Но вот с весом багажа, который вы или ваша “вторая половина” проносите с со- бой на борт, понимания гораздо меньше. Когда ты готовишь самолет к прогулке с друзьями или семьей, это напоминает упаковку рюкзака для турпохода: все помес- тилось, но поднять невозможно. Но у тебя, как пилота, нет выбора: общий вес дол- жен соответствовать норме. Поэтому приходится использовать весы и решать, чем пожертвовать: частью багажа или горючего. Единственное, что я вам настоятельно не рекомендую делать, — просить стать па весы вашу “вторую половину”. План полета Во многих встроенных системах возникает необходимость в большом энергоне- зависимом хранилище данных, емкость которого превосходит возможности обыч- ных последовательных EEPROM-устройств, рассмотренных в предыдущих главах, не говоря уже о флэш-памяти программ самого микроконтроллера. Речь идет о сот- нях мегабайт или даже гигабайтах. Владельцы цифровых фотокамер, МРЗ-плейеров и мобильных телефонов, наверное, знакомы с существующими технологиями хра- пения больших объемов данных при работе с потребительскими мультимедиа-при- ложениями: миниатюрные жесткие диски с малым энергопотреблением, а также множество полупроводниковых решений, основанных на все той же технологии Flash (CompactFlash®, SmartMedia™, Secure Digital (SD), Memory Stick® и др.). Учи-
244 Глава 13. Запоминающие устройства большой емкости тывая широкое распространение подобных устройств на рынке, их цена за послед- ние годы упала настолько, что появилась возможность (и даже необходимость) их внедрения во встроенные системы. В этом уроке мы рассмотрим взаимодействие с микроконтроллером PIC24 од- ного из наиболее популярного и дешевого типа устройств большой емкости при ми- нимальных затратах ресурсов процессора. Полет Каждая из представленных на рынке технологий хранения больших объемов данных обладает своими достоинствами и недостатками, поскольку ориентирована на какую-то отдельную область применения. Мы будем выбирать устройство, руко- водствуясь следующими критериями: • широкое распространение памяти и необходимых соединителей; • небольшое количество выводов, требуемых физическому (последовательному) интерфейсу; • большой объем памяти; • наличие открытых спецификаций; • простота реализации; • низкая стоимость памяти и необходимых соединителей. Все эти аспекты характерны для стандарта Secure Digital (SD) — наиболее рас- пространенного сегодня в мире цифровых фотокамер и многих других мультиме- диа-устройствах. Карты SD — это продолжение технологии, известной под назва- нием Multi Media Card (ММС). Между ними до сих пор существует частичная (пря- мая) совместимость по электрическим и механическим характеристикам. Технические спецификации стандартов для памяти SD регулируются асеоциаг цией SDCA (Secure Digital Card Association). Согласно ее требованиям все компа- нии, которые планируют активно применять в своих разработках, производстве или продажах изделия, использующие технологию SD, обязаны вступить в ассоциацию. В общем случае членские взносы для SDCA составляют $2 000 в год. В то же время ассоциация ММСА (Multi Media Card Association) не требует обязательного членст- ва от реализаторов технологии ММС, однако копирование их спецификаций стоит от $500 и выше. Таким образом, обе технологии далеко не бесплатные и к тому же — закрытые. К счастью, существует подмножество спецификаций SD, разрешенные ассоциацией SDCA для общего пользования в форме “упрощенных физических спецификаций”. Этой информации вполне достаточно для того, чтобы получить базовые понятия о технологии SD/ММС и приступить к разработке интерфейса между микроконтро- ллером PIC24 и запоминающим устройством большой емкости. Физический интерфейс с картами SD/MMC Для карт SD требуется всего лишь девять электрических контактов и SD/MMC- совместимый соединитель, который можно заказать почти по любому Web-каталогу за каких-то пару долларов (рис. 13.1). Кроме того, необходимы два дополнительных вывода для обнаружения подключения и опроса состояния переключателя защиты от записи. Два основных режима обмена данными: • шина SD — исходный режим стандарта SD/ММС, требующий использование четырехразрядного шинного интерфейса;
Полот 245 • последовательный обмен — основан на популярном стандарте SPI. Именно благодаря режиму последовательного обмена, устройства SD/MMC особенно привлекательны для разработки встроенных систем, поскольку большин- ство микроконтроллеров или оснащены аппаратным интерфейсом SPI, или позво- ляют без труда его имитировать, задействовав небольшое число выводов. Наконец, физические спецификации карт SD/MMC определяют диапазон рабочего напряже- ния 2,0..3,6 В, который идеально подходит для всех устройств с современными мик- роконтроллерами, включая семейство PIC24. SD ММС 8. DAT1 7. DATO/DO 6.VSS2 5.CLK 4. Vcc 3.VS51 2.CMD/DI 1.DAT3/CS 9. DAT2 7. DATO/DO 6.VSS2 5.CLK 4. Vcc 3.VSS1 2.CMD/DI 1.DAT3/CS Рис. 13.1. Распределение выводов в соединителях карт SD и ММС Взаимодействие с платой Explored6 Хотя количество электрических линий, необходимых для интерфейса SPI, неве- лико, все присутствующие на рынке соединители карт SD/MMC, к сожалению, рас- считаны исключительно на поверхностный монтаж, из-за чего их практически не- возможно задействовать в области макетирования демонстрационной платы Ex- plorer 16. Для прохождения этого и последующих уроков, связанных с использова- нием запоминающих устройств большой емкости, на сопроводительном Web-сайте www. flyingthePIC24 .com опубликованы все схемы и макеты специальной пе- чатной платы расширения. Поскольку в предыдущей главе для формирования видеосигнала был задейст- вован периферийный модуль SPI1, а приложение не допускает совместное исполь- зование этого ресурса, для организации взаимодействия между интерфейсом карты SD и памятью EEPROM мы воспользуемся модулем SPI2 с раздельными сигналами Chip Select (CS). В дополнение к обычным выводам SCK, SDI и SDO будут обеспе- чены подтягивающие резисторы для пезадействованных выводов соединителя SD/MMC (зарезервированных под четырехразрядный шинный интерфейс SD), а так- же — для двух дополнительных выводов, соответствующих сигналам обнаружения карты и защиты от записи (рис. 13.2). Новый проект После создания нового проекта с помощью типичного контрольного списка, разработаем базовые подпрограммы инициализации для всех необходимых портов ввода-вывода и конфигурирования модуля SPI2: /* ** Интерфейс карты SD
246 Глава 13. Запоминающие устройства большой емкости #include <p24fj128ga010.h> // Определения выводов #define SDWD _RG1 #define SDCD _RF1 #define SDCS RFO // Вход защиты от записи // Вход обнаружения карты // Выход выбора карты void initSD(void) // Инициализация портов ввода-вывода и периферийных модулей (SPI2) { SDCS =1; //По умолчанию карта не выбрана (высокий уровень) —TRISF0 =0; // Выход - только вывод выбора карты // Инициализация SPI2CON1 = 0x013с; SPI2STAT = 0x8000; } // initSD модуля SPI для медленного (надежного) тактирования // СКЕ=1, SMP=0, СКР=0, предделитель 1:64 // Активизация периферии .SPI2 Рис. 13.2. Интерфейс карты SD/ММС для подключения к демонстрационной плате Explorer16 В частности, с помощью регистра SPI2CON1 модуль SPI конфигурируется для работы в режиме ведущего устройства с надлежащей полярностью тактового сигна- ла, активным фронтом, точкой опроса входа и начальной частотой синхронизации. Тактовый выход SCK должен быть активен, а в ждущем режиме переведен в со- стояние низкого уровня. Точка опроса для входа SDI установлена по центру. Часто- та контролируется с помощью двух предделителей (первичный и вторичный), де- лящих тактовую частоту главного процессора Тсу для формирования синхросигнала SPI. После подачи питания и до инициализации карты SD необходимо уменьшить тактовую частоту до надежного значения (ниже 400 кГц), поэтому мы используем настройку первичного предделителя 1:64 для получения сигнала с частотой 250 кГц. Это — лишь временное соглашение. После передачи нескольких первых команд мы сможем значительно повысить скорость обмена данными. ПРИМЕЧАНИЕ____________ Обратите внимание, что конфигурировать вручную необходимо только вывод RFO; соответствующий сигналу выбора карты, в то время как выводы RG6 и RG8 (SCK2 и SDO2 соответственно) конфигурируются автоматически как выходы при активиза- ции периферии SPI2.
Полот 247 Выбор рабочего режима SPI Когда карта SD/MMC подключается в разъем, и па нее подается питание, опа переходит в рабочий режим шипы SD, выбранный по умолчанию. Для того чтобы активизировать альтернативный режим SPI, необходимо установить низкий уровень сигнала па выводе SDCS (т.е. выбрать карту) и передать первую команду RESET. Как только карта окажется в режиме SPI, опа не сможет вернуться в режим шипы SD до отключения и повторной подачи питания. Это означает, что нам необходимо реализовать вызов подпрограммы инициализации (или хотя бы команды сброса) на случай несанкционированного извлечения и обратного подключения карты, чтобы она гарантированно переходила в режим SPI. Наличие карты можно проверить в любой момент, проверив состояние вывода RF1, соединенного с линией CD. Передача команд в режиме SPI В режиме SPI команды передаются карте SD/MMC в виде пакетов по шесть байт, а все ответы карты сопровождаются блоками данных переменной длины. Та- ким образом, все, что нам необходимо, для организации взаимодействия с картой памяти, — обычная базовая подпрофамма побайтного приема и передачи данных но интерфейсу SPI: // Одновременный прием и передача одного байта данных unsigned char writeSPI(unsigned char b) { SPI2BUF = b; // Запись в буфер для передачи while(’SPI2STATbits.SPIRBF); // Ожидаем завершения передачи return SPI2BUF; // Считываем принятое значение }// writeSPI Для повышения удобочитаемости кода мы также определим два макроса, мас- кирующих функцию writeSPI () для чтения (readSPI ()) или выдачи тактового сигнала (clockSPI ()). Оба макроса передают фиктивный байт данных OxFF: #define readSPI() writeSPI(OxFF) Adeline clockSPI () writeSPI(OxFF) Для передачи команды вначале необходимо выбрать карту (низкий уровень сигнала SDCS) и выдать через порт SPI пакет', состоящий из трех частей (рис. 13.3). Байт 1 Байт 2 БайтЗ Байт 4 Байт 5 Байт 6 7 6 5 4 3 2 10 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 КОМАНДА АДРЕС CRC-код Рис. 13.3. Формат команды в режиме SPI карты SD/MMC Первая часть содержит байт с кодом команды. Представленные ниже определе- ния задают все команды, используемые в нашем проекте: // Команды карты SD Adeline RESET 0 // Переход в ждущий режим (CMD0) Adeline INIT 1 // Передача кода операции (CMD1) Adeline READ_SINGLE 17 // Чтение блока данных Adeline WRITE_SINGLE 24 // Запись блока данных После кода команды следует 32-разрядный адрес памяти: беззнаковое длинное целое число, у которого первым передается старший байт. Для удобства обработки подобных длинных полей адреса мы определим новый тип LBA, позаимствовав об-
248 Глава 13. Запоминающие устройства большой емкости щеизвестный термин, применяемый в сфере запоминающих устройств большой ем- кости для обозначения очень большого адреса некоторого блока данных: typedef unsigned long LBA; // 32-разрядный адрес логического блока Командный пакет завершается одним байтом CRC-кода контроля с помощью циклического избыточного кода. Этот код всегда используется в режиме шины SD для гарантированно безошибочной передачи каждого блока данных. При переклю- чении в режим SPI CRC-контроль автоматически отключается, поскольку карта предполагает наличие прямого и надежного подключения к хосту (в нашем слу- чае — микроконтроллеру PIC24). Это значительно упрощает программный код, по- скольку мы можем заменить CRC-расчеты фиксированным значением: кодом, соот- ветствующими команде RESET, который будет игнорироваться всеми последую- щими командами. Рассмотрим первую часть функции sendSDCmd (): int sendSDCmd (unsigned char- c, LBA a) // Передача карте 6-байтного командного блока, линия SDCS - активна { int i, г; // Активизация карты SD SDCS = 0; // Передача командного пакета (6 байт) writeSPI(с | 0x40); // Команда + бит кадра writeSPI((unsigned char) а>>24); // Старший байт адреса writeSPI (а>>16); writeSPI (а»8) ; writeSPI(а); // Младший байт адреса // CRC-код требуется только для команды CMD0-RESET, поскольку //в режиме SPI он не используется writeSPI(0x95); // CRC-код команды RESET После передачи карте всех шести байтов необходимо реализовать цикл ожида- ния ответного байта (по сути, мы постоянно передаем фиктивные данные, такти- рующие порт SPI port). Ответ должен содержать значение OxFF (в основном, в ли- нии SDI сохраняется высокий уровень сигнала) до тех пор, пока карта не будет го- това выдать надлежащий код. Спецификацией определено, что для получения фак- тического отклика может потребоваться до 64 тактовых импульсов (т.е. восемь байт). В случае превышения этого предела можно предположить, что карта вышла из строя, и прервать обмен данными. // Ожидаем отклика с задержкой до 8 байт i = 9; do { г = readSPIO; // Проверяем готовность if (г != OxFF) break; } while (--i > 0) ; return ( r) ; /* Возвращаемый ответ FF - истечение времени ожидания, ответ отсутствует 00 - команда воспринята 01 - команда принята, карта - в состоянии ожидания (после RESET) Другие ошибки */ } // sendSDCmd
Полет 249 В коде отклика каждый разряд, установленный в 1, указывает на какую-либо проблему: • 0 — состояние ожидания; • 1 — отмена сброса; • 2 — некорректная команда; • 3 — ошибка CRC; • 4 — ошибка удаления последовательности; • 5 — ошибка адреса; • 6 — ошибка параметра; • 7 — всегда 0. Обратите внимание, что после выхода из функции sendSDCmd () карта SD по- прежнему выбрана (низкий уровень сигнала в линии SDCS), что позволяет восполь- зоваться командами, которые требуют передачу или приема дополнительных дан- ных (например, Block Write и Block Read). В случае со всеми остальными команда- ми, не требующими передачи дополнительных данных, сразу же после вызова функции sendSDCmd () следует обязательно переводить линию SDCS в состояние высокого уровня. Более того, поскольку порт SPI2 используется совместно с други- ми периферийными устройствами (например, последовательной памятью EEPROM, смонтированной на плате Explored 6), необходимо удостовериться, что карта SD/ ММС сразу же после нарастающего фронта сигнала выбора кристалла (SDCS) при- нимает еще несколько тактовых импульсов (достаточно восьми). Согласно специ- фикации SD/ММС это позволит карте завершить некоторые важные внутренние операции, включая освобождение линии SDO, что крайне важно для корректной ра- боты других устройств, подключенных к той же шине. Во всем этом нам поможет еще два макроса: #define disableSDO SDCS = 1; clockSPIO #define enableSDO SDCS = 0 Завершение инициализации карты SD/ММС Прежде, чем карту можно эффективно использовать во встроенной системе, ей необходимо передать дополнительную, четко определенную последовательность команд. Она определена в оригинальной спецификации карты ММС и лишь немно- го модифицирована в спецификации карты SD. Поскольку мы не планируем ис- пользовать какие-либо возможности, характерные для стандарта SD, мы воспользу- емся базовой последовательностью, определенной для карт ММС. Она состоит из пяти пунктов (рис. 13.4). 1. Карта подключена в разъем, и на нее подано питание. 2. В линии CS поддерживается высокий уровень сигнала (карта не выбрана). 3. До того момента, когда карта сможет принимать команды, ей должно быть пе- редано 74 тактовых импульса. 4. Выбор карты и передача команды RESET(CMDO). Карта должна ответить пере- ходом в состояние ожидания и активизацией режима SPI. 5. Передача команды INIT(CMDl) до тех пор, пока карта не выйдет из состояния ожидания. Эти пять шагов инициализации реализованы в следующем фрагменте функции initMedia():
250 Глава 13. Запоминающие устройства большой емкости int initMedia(void) { int i, r; // 1. До тех пор, пока карта не выбрана SDCS = 1; // 2. Передача 80 начальных тактовых импульсов for (i=0; i<10; i++) clockSPI(); // 3. Выбираем карту SDCS = 0; // 4. Передаем команду RESET для перехода в режим SPI г = sendSDCmd(RESET, 0); SDCS = 1; if (г != 1) return 0x84; // 5. Постоянно передаем команду INIT i = 10000; // Допускается до 0,3 с ожидания do ( г = sendSDCmd(INIT, 0); SDCS = 1; if (r) break; } while(—i > 0) ; if (( i==0) |I ( r!=l)) return 0x85; // Период ожидания истек Для выполнения команды инициализации может' потребоваться определенное время, зависящее от размера и типа карты памяти (обычно несколько десятков се- кунд). Поскольку мы работаем па скорости 250 Кбит/с, для передачи каждого байта требуется 32 мкс. С учетом П1ести байтов для каждого повтора команды и счета до 10 000 для учета периода ожидания получаем приличный предел около двух секунд. Только после успешного прохождения рассмотренной последовательности мы можем, наконец, “переключить передачу” и повысить тактовую частоту до макси- мально значения, поддерживаемого нашим аппаратным обеспечением. Достаточно немного поэкспериментировать, чтобы убедиться, что плата Explorer 16 с корректно настроенной платой расширения, предоставляющей разъем SD/MMC, может без
Полет 251 труда обеспечить тактовую частоту до 8 МГц. Это значение получают путем рекой- фигурировапия первичного предделителя SPI па коэффициент 1:1, а вторичного предделителя — па коэффициент 1:2. Теперь мы можем завершить функцию init- Media(): // 6. Увеличиваем скорость SPI2STAT =0; //На мгновение отключаем модуль SPI2 SPI2CON1 = 0x013b; // Устанавливаем коэффициент предделителя 1:2 SPI2STAT = 0x8000; // Опять активизируем модуль SPI2 return 0; } // initMedia Чтение данных из карты SD/MMC Карты SD/MMC — это полупроводниковые устройства, которые обычно содер- жат большие массивы флэш-памяти, а значит мы можем считывать и записывать любые объемы данных (в пределах емкости карты) по любому адресу. В действи- тельности из-за необходимости совместимости со множеством старых технологий храпения больших массивов данных существует достаточно ограничений на спосо- бы доступа к памяти. По сути, все операции определены в блоках фиксированного размера (по умолчанию 512 байт). Такое значение выбрано не случайно, поскольку 512 байт — это стандартный размер сектора данных типичного жесткого диска ПК. Хотя размер блока можно изменить с помощью соответствующей команды, из со- ображений совместимости мы сохраним настройку по умолчанию. Для реализации в следующей главе завершенной файловой системы, совместимой с большинством популярных операционных систем ПК, мы разработаем специальный набор подпро- грамм. Это позволит нам получить доступ к файлам, записанным па карту ПК и на- оборот, ПК сможет получить доступ к файлам, записанным нашим приложением. Для инициализации передачи одного “сектора” по заданному адресу памяти достаточно команды READ_SINGLE (CMD17). Впрочем, опа принимает в качестве аргумента 32-разрядный адрес “байта”, поэтому во избежание путаницы в после- дующих разделах мы будем неизменно использовать только адреса блоков (LBA), а фактический адрес байта получать путем умножения значения LBA па 512 непо- средственно перед передачей параметра команде READ_SINGLE. Для инициализации последовательности считывания мы воспользуемся разра- ботанной ранее функцией sendSDCmd () (она будет выбирать карту и оставлять ее в выбранном состоянии). После проверки полученного в ответ кода на предмет ошибок (таковые должны отсутствовать) будет реализовано ожидание от карты па- мяти особого маркера DATA_START, который уникальным образом идентифициру- ет начало блока данных. И опять, как и па этапе инициализации, важно учесть до- пустимое время ожидания, хотя в данном случае мы можем быть более щедрыми. Поскольку во время ожидания маркера данных постоянно вызывается только функ- ция readsPI () (передача/прием одного байта), эффективное лимитирование вре- мени обеспечивает счет до 10 000, что соответствует примерно 0,32 с. Как только будет обнаружен маркер, можно уверенно и быстро считать все 512 байт, составляющих запрошенный блок данных. После них следует 16-разрядное значение кода CRC, которое также необходимо считать, хотя оно и не будет исполь- зоваться (рис. 13.5). Только теперь мы можем отменить выбор карты памяти и пре- рвать последовательность команд чтения.
252 Глава 13. Запоминающие устройства большой емкости Вывод данных ОТВЕТ МАРКЕР НАЧАЛА 16-разрядный код CRC Рис. 13.5. Передача данных в течение команды read_single Подпрограмма readSECTOR () реализует всю последовательность в несколь- ких строках кода. // Ответ карты SD ^define DATA_START OxFE int readSECTOR(LBA a, char *p) //a - запрашиваемый LBA // p - указатель на буфер данных // В случае успеха возвращается значение TRUE { int г, i; READ_LED = 1; г = sendSDCmd(READ—SINGLE, (a « 9)); if (r == 0) // Проверка, воспринята ли команда { // Ожидаем ответ i = 10000; do { г = readSPI(); if (г == DATA_START) break; } while(--i>0); // Если не было превышения времени ожидания, то считываем сектор // данных размером 512 байт if (i) { for(i-0; i<512; i++) *p++ = readSPI(); // Игнорируем CRC readSPI(); readSPI() ; } // Поступление данных } // Воспринята команда // Не забываем отключить карту disableSD(); READ_LED = 0; return (г == DATA—START); //В случае успеха возвращаем TRUE } // readSECTOR Для обеспечения визуальной индикации взаимодействия с картой памяти, по- добно тому, как это происходит в случае с приводами жестких и гибких дисков, вы-
Полет 253 делим один из светодиодов платы Explorer 16. Это позволит пользователю увидеть, когда карта занята, чтобы случайно не извлечь ее из разъема. Светодиод будет включаться перед каждой командой чтения и отключаться по ее окончании. Впро- чем, можно применять и другие подходы. Например, подобно тому, как это реали- зовано во флэш-памяги USB, светодиод может включаться при инициализации кар- ты независимо от фактически выполняемой команды. При таком варианте светоди- од отключается только в результате вызова подпрограммы деинициализации. Запись данных в карту SD/MMC Руководствуясь теми же соображениями, что и в отношении функций чтения, разработаем функцию записи, которая аналогичным образом будет оперировать “секторами”, т.е. блоками данных размером 512 байт. Само собой, последователь- ность записи построена на команде WRITE_SIl.GLE, однако на этот раз данные пе- редаются в обратном направлении. Убедившись, что команда воспринята, мы сразу же начинаем передачу маркера DATA_START, а сразу после него — всех 512 байт данных, после которых следуют два байта 16-разрядного когда CRC (любое фик- тивное значение). Затем последует пауза в ожидании приема от карты маркера под- тверждения приема блока данных DATA_ACCEPT. После этого начинается операция записи. До тех пор, пока карта выполняет запись, она поддерживает на линии SDO сигнал низкого уровня. Ожидание завершения команды записи потребует нового цикла, в котором ли- ния SDO постоянно опрашивается на предмет появления сигнала высокого уровня. При этом, опять таки, следует учесть лимит времени ожидания, выделенного на за- вершение операции. Поскольку все устройства SD/MMC основаны па технологии памяти Flash, время записи обычно значительно превышает время чтения. Все той же предельной задержки в 10 000 отсчетов, что соответствует 0,3 с, опять будет бо- лее, чем достаточно для учета самой медленнодействующей карты памяти из при- сутствующих на рынке (рис. 13.6). Вывод данных (SDO) МАРКЕР НАЧАЛА 16-разрядный код CRC Ввод данных (SDI) ОТВЕТ ДАННЫЕ ПРИНЯТЫ ЗАВЕРШЕНИЕЗАПИСИ Рис. 13.6. Передача данных во время выполнения команды writers ingle #define DATA_ACCEPT 0x05 int writeSECTOR (LBA a, char *p) //a - LBA запрашиваемого сектора // p - указатель на буфер сектора //В случае успеха возвращается значение TRUE { unsigned г, i; WRITE_LED = 1; г = sendSDCmd(WRITE_SINGLE, (a « 9)); if (r == 0) // Проверка, была ли воспринята команда
254 Глава 13. Запоминающие устройства большой емкости { writeSPI(DATA_START); for(i=0; i<512; i++) writeSPI(*p++); // Передаем фиктивный код CRC clockSPI () ; clockSPI(); // Проверяем, восприняты ли данные if ((г = readSPI () & Oxf) == DATA_ACCEPT) { for(i=10000; i>0; i--) { // Ожидаем окончания операции записи if (г = readSPI()) break; } } // Восприняты else г = FAIL; } // Команда воспринята // Отключение карты и выход disableSD(); WRITE_LED - 0; return (г); //В случае успеха возвращается значение TRUE } // writeSECTOR Подобно подпрограмме чтения, для индикации операции записи будет задейст- вован еще один светодиод. Если карту извлечь во время последовательности записи, то данные скорее всего будут потеряны. Сохраните разработанный файл исходного кода под именем sdmmc.c, после чего добавьте в него две функции для проверки наличия карты и состояния пере- ключателя защиты от записи: int detectSD(void) { return (!SDCD); } // delectSD int detectWP(void) { return (!SDWP); } // detectWP ©Полный файл sdmmc.c находится на прилагаемом к книге компакт-диске в папке Проекты\13 - SDMMC. ПРИМЕЧАНИЕ Переключатель WP обеспечивает только индикацию Он не соединен ни с каким ап- паратным механизмом, который мог бы предотвратить от фактической операции за- писи в карту. Ответственность за подобные проверки полностью возлагается на про- граммиста. Наконец, создайте заголовочный файл sdmmc.h для размещения в нем прото- типов и базовых определений, используемых интерфейсным модулем SD/MMC.
Полет 255 /* ★ ★ Низкоуровневый интерфейс карты SD/MMC * / # define TRUE 1 # define FALSE О ttdefine FAIL 0 // Определения портов ввода-вывода # define READDLED _RA1 ^define WRITE_LED _RA2 typedef unsigned long LBA; // 32-разрядный адрес логического блока void initSD(void); int initMedia(void); int detectSD(void); int detectWP(void); int readSECTOR(LBA, char *); int writeSECTOR(LBA, char *); Файл sdmmc.h находится также на прилагаемом к книге компакт-диске в папке ПроектыМЗ - Применение интерфейсного модуля SD/MMC Хотите верьте, хотите нет, по шесть рассмотренных выше небольших подпро- грамм — это все, что нам нужно для получения доступа к практически безгранич- ным просторам энергонезависимой памяти, предоставляемой картами SD/MMC. Например, карта па 512 Мбайт обеспечивает нас приблизительно миллионом (!) блоков (секторов) но 512 байт с независимой адресацией. И при этом карты подоб- ного объема можно купить менее, чем за 20 долларов! Разработаем небольшую тестовую программу, демонстрирующую использова- ние модуля SD/MMC. Ее идея заключается в имитации некоторого типичного при- ложения, сохраняющего большой объем данных на карту. Ио предопределенному диапазону адресов будет записано, а затем считано обратно фиксированное количе- ство блоков. Создайте новый файл исходного кода и добавьте в него обычные ссылки па за- головочные файлы с определениями процессора и модуля sdmmc. /* ★ ★ Тестирование чтения/записи для карты SDMMC * * * / tfinclude <p24fj128ga010.h> # include "SDMMC.h" Далее определите два массива па 512 байт (установленный по умолчанию раз- мер блока памяти SD/MMC). #define B_SIZE 512 // Размер сектора/блока данных char data[B_SIZEj; char buffer[B_SIZE];
256 Глава 13. Запоминающие устройства большой емкости Первый массив будет заполнен специальным, легким для распознавания шаб- лоном, после чего тестовая программа перепишет его содержимое в карту памяти. Диапазон адресов определим с помощью двух констант: #define START_ADDRESS 10000 // Начальный адрес блока #define N_BLOCKS 1000 // Количество блоков Визуальную индикацию корректности выполнения программы и/или возни- кающих ошибок реализуем с помощью светодиодов, подключенных к порту А де- монстрационной платы Explorer 16. Теперь можно написать первые строки главной программы, в которых происхо- дит инициализация портов ввода-вывода, необходимых для модуля SD/MMC и под- ключения линейки светодиодов. main( void) { LBA addr; int i, r; // Инициализация портов ввода-вывода TRISA = Oxff00; // Инициализация выводов порта А как выходов initSD(); // Инициализация всех портов для модуля SD/MMC // Заполняем буфер "данными" for(i=0; i<B_SIZE; i++) data[i]= i; Следующий фрагмент кода проверяет наличие карты SD в разъеме/соединителе. Здесь в цикле проверяется состояние ключа обнаружение карты, а также реализова- на дополнительная задержка для исчезновения дребезга контактов. // Ожидание подключения карты while(!detectSD()); // Предполагается, что вывод SDCD - вход Delayms(100); // Пауза исчезновения дребезка контактов и // подачи питания На паузе в ожидании исчезновения дребезга контактов не стоит экономить, по- скольку подключение карты должно быть гарантированно устойчивым. В против- ном случае выполнение команд записи может повредить сторонние данные на кар- те. Для этой цели вполне достаточно задержки в 100 мс, а функцию Delayms () можно без труда реализовать с помощью любого таймера микроконтроллера PIC24 или даже модуля RTCC. В представленном ниже примере используется модуль тай- мера Timerl, исходя из предположения, что тактовая частота процессора составляет 32 МГц (как в случае с платой Explorerl6). void Delayms(unsigned t) { T1CON = 0x8000; // Активизируем tmrl, Тсу, 1:1 while (t—) { TMR1 = 0; while (TMRK16000) ; } } // Delayms Крайне важно, чтобы функция задержки была реализована отдельно от функции detectSD () и модуля SD/MMC, поскольку это позволит разным приложениям
Полет 257 выбирать оптимальную временную стратегию и оптимизировать распределение ре- сурсов. ©Файлы Delay, с и Delay, h находятся на прилагаемом к книге компакт-диске в папке ПроектыХ Delay. Убедившись в наличии карты, мы можем продолжить инициализацию, вызвав функцию initMedia (). // Инициализация карты памяти (в случае успеха возвращает 0) г = initMedia(); if (г) // Невозможно инициализировать карту { PORTA = г; // Выводим код ошибки на светодиоды while(1); // Останов } Эта функция возвращает целое число: ноль для успешного завершения после- довательности инициализации или некоторый код ошибки. В нашем примере код ошибки просто отображается с помощью светодиодов, и выполнение программы останавливается с помощью бесконечного цикла. Так, кодам 0x84 и 0X85 соответ- ствует сбой па этапе 4 и 5 функции initMedia (), что соответствует некорректно- му выполнению команд карты RESEThINIT. Если все прошло успешно, то можно переходить непосредственно к этапу запи- си данных. else { // Заполняем N_BLOCK блоков/секторов содержимым буфера данных addr = START_ADDRESS; for(i=0; i<N_BLOCKS; i++) if (!writeSECTOR(addr+i, data)) { // Ошибка записи PORTA = 0x0 f; whiled); // Останов } В простом цикле for вызывается функция writeSECTOR () для диапазона ад- ресов от блока 10 000 до блока 10 999, копируя один и тот же блок данных и прове- ряя на каждом шаге успешность выполнения команды. Если команда блочной запи- си возвращает код ошибки, то на светодиоды выводится уникальный код 0x0f, и выполнение программы останавливается. На практике это эквивалентно записи файла размером 512 000 байт. // Проверяем содержимое каждого записанного блока/сектора addr = START_ADDRESS; for(i=0; i<N_BLOCKS; i++) { // Считываем no одному блоку if (!readSECTOR(addr+i, buffer)) { // Ошибка чтения PORTA = OxfO; while (1); // Останов } // Проверяем содержимое каждого блока if (Jmemcmp(data, buffer, B_SIZE)) { // He совпадает PORTA = Oxff; while(1); // Останов
258 Глава 13. Запоминающие устройства большой емкости } } // Для каждого блока Далее начинается новый цикл для чтения каждого блока данных во второй бу- фер с последующим сравнением его содержимого с исходным шаблоном в первом буфере. Если функция readSECTOR () дает ошибку чтения, то па светодиоды вы- водится код ошибки OxfO, и выполнение тестовой программы останавливается. В противном случае с помощью стандартной библиотечной С-функции memcmp () мы сравниваем содержимое двух буферов. Если возвращается значение 0, то два буфера идентичны (чего мы и ожидаем), в противном случае считанные данные со- держат некорректные блоки, и для индикации ошибки мы выводим па светодиоды код ошибки 0x55. Для получения доступа к функции memcmp (), реализованной в стандартной библиотеке string, необходимо добавить ссылку па соответствую- щий заголовочный файл: ^include <string.h> В завершение главной программы реализуем индикацию успешного окончания операций, включив все свет одиода, подключенные к порту Л: } // else - инициализация карты // Индикация успешного окончания операций PORTA = OxFF; // Главный цикл while (1) ; } // main Полный файл SDMMCtest.c находится на прилагаемом к книге компакт-диске в папке Проекты\ 13 - SDMMC. Добавьте в проект все необходимые исходные файлы (sdmmc.h, sdmmc.c и sdmmctest. с), скомпонуйте его и запрограммируйте демонстрационную плату Explorer 16. Как было отмечено в начале урока, для выполнения теста потребуется специальная плата расширения с разъемами SD/MMC, однако усилия, потраченные па ее разработку (или деньги, ушедшие па покупку), не будут выброшены на ветер. Все расходы с лихвой компенсируются чувством глубокого удовлетворения при ви- де того, как микроконтроллер PIC24 справляется с задачей за доли секунды. Объем требуемого кода также чрезвычайно мал (рис. 13.7). Рис. 13.7. Содержимое окна Memory Gauges среды MPLAB
Разбор полета 259 Тестовая программа вместе с модулем доступа к карте SD/MMC занимают все- го лишь 803 слова (2 409 байт) флэш-намяти программ процессора, что составляет менее 2% от общего объема доступной памяти. Как и в предыдущих уроках, этот результат был получен без какой-либо оптимизации со стороны компилятора. Разбор полета На мой взгляд, не существует более дешевой и простой технологии хранения больших объемов данных, чем карты SD/MMC. Посудите сами: для огромного рас- ширения памяти нам достаточно лишь нескольких подтягивающих резисторов, де- шевого соединителя и нескольких выводов микроконтроллера, а с точки зрения ре- сурсов микроконтроллера PIC24 — только периферийного модуля SPI, который к тому же можно использовать совместно с другими приложениями. Впрочем, противовесом простоте данного подхода являются некоторые явные ограничения. Так, данные можно записывать только блоками фиксированного раз- мера, а из позиция внутри массива памяти определяется исключительно приложе- нием. Другими словами, данные невозможно использовать совместно с ПК или дру- гим устройством, поддерживающим карты SD/MMC, без разработки специальной программы. Что еще хуже, попытка воспользоваться картой, на которую уже были записаны данные с ПК, скорее всего, приведет к повреждению таких данных, а кар- ту придется полностью переформатировать. Эти проблемы будут решены в следующем уроке, в котором мы разработаем библиотеку завершенной файловой системы. Советы и хитрости Решение работать с блоками размером 512 байт обусловлено, главным образом, историческими причинами. Благодаря тому, что подпрограммы низкоуровневого доступа в этом уроке согласуются со стандартом, принятым в большинстве запоми- нающих устройств большой емкости (включая жесткие диски), разработка следую- щего уровня (файловой системы) значительно упрощается. Впрочем, если стоит за- дача достичь максимальной производительности, то такой вариант неприемлем. Так, для повышения скорости записи (“тонкое место” всех Flash-носителей) лучше использовать блоки данных гораздо большего размера. Процесс записи во флэш-память состоит из двух этапов. Вначале должен быть очищен большой блок данных (его часто называют “страницей”), после чего собст- венно происходит запись блоками меньшего размера. Чем больше массив памяти, тем крупнее страница, и тем больше времени гребуется на ее очистку. Например, для карты памяти на 512 Мбайт размер страницы может превышать 2 Кбайт. Все эти подробности скрыты от пользователя, поскольку все операции по очистке, запи- си и буферизации берез' на себя встроенный контроллер карты, однако они влияют на общую производительность приложения. Так, если предположить, что размер страницы в карте SD составляет 2 Кбайт, то запись любого объема данных менее 2 Кбайт погребует от внутреннего контроллера выполнить следующие операции. 1. Считать содержимое всего блока 2 Кбайт во внутренний буфер. 2. Очистить буфер и выждать время, необходимое для завершения очистки. 3. Заменить часть буфера новыми данными. 4. Записать целый блок и выждать время, необходимое для завершения записи.
260 Глава 13. Запоминающие устройства большой емкости Если использовать блоки по 512 байт, то для записи 2 Кбайт данных пашей библиотеке придется потребовать от контроллера карты SD выполнить всю после- довательность четыре раза, хотя все можно было бы сделать за один шаг. Впрочем, к увеличению размера блока данных следует подходить с осторожностью. Хотя в пашем примере это и повысило бы скорость записи на 400%, следует учитывать ряд факторов, чтобы не пришлось потом расплачиваться за ошибку: • фактический размер страницы может быть неизвестен или не гарантироваться производителем; • увеличение блока данных приводит к росту буфера, который необходимо выде- лить в оперативной памяти (драгоценный ресурс любого встроенного приложе- ния) микроконтроллера PIC24; • более высокий уровень программного обеспечения (об этом мы поговорим в следующем уроке) может привести к затруднениям с интеграцией, если раз- мер блока данных будет варьироваться; • чем больше буфер, тем больше данных будет потеряно, если карту извлечь из разъема до очистки буфера. Упражнения 1. Поэкспериментируйте с различными размерами блока данных, чтобы выяснить, какой из них обеспечивает наивысшую скорость записи для вашей карты SD. Это даст косвенное представление о размере страницы флэш-памяти, использо- ванной производителем карты. 2. Поэкспериментируйте с командами записи одновременно нескольких блоков, изменив длину блока. Это позволит выяснить, каким образом контроллер карты SD выполняет внутреннюю буферизацию, и сравнить производительность двух методов записи. Ссылки • www. mmca . org/home — официальный Web-сайт ассоциации MultiMedia Card Association (MMCA). • www.sdcard.org — официальный Web-сайт ассоциации Secure Digital Card Association (SDCA). • www.sdcard.org/sdio/Simplified%20SDIO%20Card%20Specificat ion.pdf — упрощенная спецификация карты SDIO, согласно которой интер- фейс SD может использоваться не только для хранения данных, но также и для другой периферии, наподобие GPS-приемников, цифровых фотокамер и др.
ГЛАВА *1 4 Файловый ввод-вывод В этой главе: >• Секторы и кластеры & Таблица размещения файлов FAT Корневой каталог Поиск клада > Открытие файла > Чтение данных из файла Закрытие файла > Создание модуля файлового ввода-вывода & Тестирование функций ТорепМ () uf readMO Запись данных в файл > Еще раз о закрытии файла > Вспомогательные функции > Тестирование завершенного модуля файлового ввода-вывода Размер кода Каждый тренировочный полет должен в точности следовать плану, назначен- ному инструктором, или программе учебного курса летной школы. В каждом уроке мы указывали его цель в разделе, озаглавленном “План полета”, однако в авиации настоящий план полога выглядит совсем иначе. Это очень подробный список, со- держащий время, высоту, курс, расход топлива и т.п. для все сегментов (отрезков) полога. Для дальних рейсов такой план крайне важен, поскольку позволяет пилоту отслеживать текущее положение самолета и принимать решения в случае возникно- вения непредвиденной ситуации. Настоятельно рекомендуется передавать план в службу обеспечения полетов (СОП) в виде файла (например, через Internet) или, надиктовав его по телефону дис- петчеру. Если СОП знает где вы находитесь и куда и по какому маршруту направ- ляетесь, то это поможет им отслеживать местоположение самолета на радаре (так называемое сопровождение полета). Когда высота полета слишком мала для того, чтобы организовать сопровождение, диспетчер может проверить прохождение оп- ределенных точек в установленные моменты времени по радиосвязи. Если на связь выйти не удалось или самолет не прибыл вовремя в аэропорт назначения, то сразу же начинается поисковая операция. Такая незамедлительная реакция особенно важ- на для спасения экипажа и пассажиров в экстремальных климатических условиях, а также в горной и пустынной местности. При формировании планов полета большинство пилотов испытывают смешан- ное чувство подростка, который должен отчитаться перед мамой о своих планах на
262 Глава 14. Файловый ввод-вывод вечер (чего ему ужасно не хочется делать), и ответственного лица, понимающего, что такая мера предосторожности просто необходима. На то, чтобы рассказать маме (т.е. СОП), куда и как вы летите, много времени не потребуется, но зато в будущем это принесет большую пользу. В мире встроенных систем предоставление файлов (информации) ПК также может быть чрезвычайно полезным, однако для этого приходится следовать опреде- ленным правилам. Другими словами, необходимо знать, каким образом работаег файловая система ПК. План полета В предыдущем уроке мы разработали базовый интерфейсный модуль (как про- граммный, так и аппаратный), обеспечивающий доступ к карте SD™/MMC для хра- пения больших объемов данных. Аналогичный интерфейс можно было бы разрабо- тать и для некоторых других типов носителей, однако в этом уроке речь пойдет об алгоритмах и структурах данных, необходимых для корректного использования ин- формации между наиболее популярными операционными системами ПК (DOS, Windows® и некоторые версии Linux). Другими словами, мы разработаем модуль для доступа к стандартной файловой системе FAT 16. Первая файловая система FAT была создана Билом Гейтсом (Bill Gates) и Мар- ком Макдональдом (Marc McDonald) в 1977 году для управления дисками в Micro- soft Disk BASIC. Опа была основана па методиках, применявшихся в уже существо- вавших па то время файловых системах, и постоянно развивалась. За следующие двадцать лет появилось несколько новых версий этой файловой системы, поддер- живающих запоминающие устройства все большего объема и реализующих все но- вые возможности. Среди них по сей день используются FAT12, FAT16 и FAT32, причем системы FAT 16 и FAT32 распознаются практически любой современной операционной системой ПК. Выбор одной из них обусловлен, главным образом, со- ображениями производительности и емкости носителя. В большинстве Flash-уст- ройств, популярных па потребительском рынке, используется файловая система FAT16. Полет Аббревиатура FAT означает “File Allocation Table”, т.е. “таблица размещения файлов”. Этот термин определяет одну из наиболее важных в файловой системе структуру данных. По большому счету, файловая система — это всего лишь метод хранения и организации компьютерных файлов и содержащихся в них данных для упрощения их поиска и доступа к ним. К сожалению, в истории ПК часто случается, что стандарты и технологии являются плодом постоянного эволюционного разви- тия, а не первоначального замысла. По этой причине многие детали файловой сис- темы FAT, рассмотренные в последующих подразделах, можно объяснить только в контексте борьбы за сохранение совместимости с несметным множеством уста- ревших технологий и программных средств. Секторы и кластеры Базовые принципы, положенные в основу файловой системы FAT, весьма про- сты. Как было показано в предыдущем уроке, большинство запоминающих уст- ройств большой емкости следуют “традиции”, позаимствованной в мире жестких
Полет 263 дисков: вся работа с памятью организована через блоки фиксированного размера в 521 байт, называемые “секторами”. В файловой системе FAT небольшая часть этих секторов зарезервирована и используется в качестве своеобразного индекса. Это и есть таблица размещения файлов. Остальные (почти все) секторы предназна- чены для хранения данных, однако работа с ними организована не по отдельности, а небольшими группами смежных секторов, которые называют “кластерами”. Раз- мер кластера может составлять от одного до 64 секторов. Именно использование и положение каждого кластера отслеживается таблицей размещения файлов. Таким образом, наименьшей единицей памяти, распознаваемой файловой системой FAT, является кластер. Упрощенная схема, показанная на рис. 14.1, иллюстрирует гипотетический пример файловой системы FAT, отформатированной под 1 022 кластера, каждый из которых состоит из 16 секторов (обратите внимание, что область данных начинается со второго кластера). В данном случае каждый кластер содержит 8 Кбайт данных, а общая емкость памяти составляет около 8 Мбайт. Зарезервировано Зарезервировано Область данных (кластеры) Сектор О Рис. 14.1.Упрощенный пример структуры файловой системы FAT Обратите внимание, что чем больше кластеры, тем меньше их требуется для об- работки полного пространства памяти, и тем меньше размеры таблицы размещения (т.е. тем выше эффективность файловой системы). Однако в случае записи множе- ства маленьких файлов увеличение размера кластеров приводит к напрасной трате памяти. Обычно выбор идеального размера кластеров для поддержания оптималь- ного баланса возлагается на операционную систему в процессе форматирования под файловую систему FAT. Таблица размещения файлов FAT В файловой системе FAT 16 таблица размещения файлов содержит по одному 16-разрядному целочисленному значению для каждого кластера. Если кластер пуст и доступен для использования, то соответствующая ячейка таблицы содержит число 0x0000. Если кластер уже задействован и содержит целый файл данных, то в ячей- ке FAT будет значение OxFFFF. Для файлов, размер которых превышает размер од- ного кластера, формируется кластерная цепочка. В таком случае соответствующие ячейки таблицы FAT содержат помер следующего кластера в цепи, а завершаю- щая— значение OxFFFF. Кроме того, для пометки зарезервированных кластеров используется значение 0x0 001, а порченных кластеров — значение 0xFFF7.
264 Глава 14. Файловый ввод-вывод Именно особое значение чисел 0x0000 и 0x0001 стало основной причиной для соглашении о начале области данных со второго кластера. Соответствующим обра- зом, первые два целых 16-разрядных числа в FAT зарезервированы. На рис. 14.2 показан пример содержимого FAT для системы из предыдущего примера. Кластер 0x0000 | Л Кластер 0x0001 Кластер 0x0002 OxFFFF Кластер 0x0003 0x0004 Кластер 0x0004 0x0005 Кластер 0x0005 OxFFFF Кластер 0x0006 0x0000 I 1 1 1 Кластер 0x1023 0x0000 Зарезервировано Задействован, отдельный кластер Задействован, указывает на следующий кластер в цели Задействован, указывает на следующий кластер в цели Задействован, последний кластер в цепи Пустой и доступен для использования Рис. 14.2. Кластерные цепочки в таблице FAT Кластеры 0 и 1 зарезервированы. Все или некоторые из 16 секторов кластера 2 заполнены данными из файла, размер которого не превышает 8 Кбайт. Кластер 3 — первый в цепи из трех кластеров. Все секторы кластеров 3 и 4 и некоторые или все секторы кластера 5 заполнены данными из файла, размер которого превосходит 16 Кбайт, но меньше 24 Кбайт. Все последующие кластеры — пустые и доступные для использования. Размер самой таблицы FAT получают как общее количество кластеров, умно- женное на два (по два байта на кластер), а значит она может занимать несколько секторов. В нашем примере для FAT из 1 024 кластеров требуется 2 048 байт, т.е. четыре сектора по 512 байт каждый. Кроме того, учитывая ключевую роль таблицы размещения файлов для всей файловой системы FAT, перед областью данных по- следовательно размещается и сопровождается несколько (обычно две) копий этой таблицы. Корневой каталог Назначение таблицы FAT — отслеживать, как и где размещаются данные. Она не содержит никакой информации о природе файла, к которому принадлежат дан- ные. Для этой цели существует другая структура, называемая корневым каталогом. Она хранит имена файлов, информацию об их размере, дате и времени создании и некоторые другие атрибуты. В файловой системе FAT 16 корневой каталог (далее будем называть его просто “корень”) находится в фиксированной позиции и зани- мает фиксированный объем между второй копией FAT и первым кластером данных (рис. 14.3). Поскольку и позиция, и размер (количество секторов) — фиксированы, макси- мальное количество файлов (элементов каталога) в корне ограничено и определяет- ся при форматировании носителя. Каждый сектор, размещенный в корне, допускает документирование до 16 файловых элементов, где под каждый элемент требуется блок из 32 байт (рис. 14.4).
Полот 265 FAT1 и FAT2 Зарезервировано Область данных (кластеры) Сектор О Рис. 14.3. Размещение корневого каталога файловой системы FAT Смещение: 0 Имя файла Восемь ASCII-символов Смещение: 8 Расширение Три ASCII-символа Смещение: 11 Атрибуты Зарезервировано Один байт Смещение: 22 Время Одно слово (16 бит) Смещение: 24 Дата Одно слово (16 бит) Смещение: 26 Первый кластер Одно слово (16 бит) Смещение: 28 Размер файла Одно длинное слово (32 бига) Рис. 14.4. Базовая структура элемента корневого каталога Суть полей, содержащих имя и расширение файла, вполне очевидна. При этом под имя отведено восемь, а под расширение — три символа (разделяющая точка не указывается). Поле атрибутов содержит группу флагов, смысл которых указан в табл. 14.1. Таблица 14.1. Атрибуты файла в элементе каталога Разряд Маска Описание 0 0x01 Только для чтения 1 0x02 Скрытый 2 0x04 Системный 3 0x08 Метка тома 4 0x10 Подкаталог 5 0x20 Архивный Поля времени и даты (табл. 14.2 и табл. 14.3) содержат информацию о моменте последней модификации файла и кодируются в специальном сжатом формате, кото- рый помещается всего лишь в двух 16-разрядных словах.
266 Глава 14. Файловый ввод-вывод Таблица 14.2. Кодирование времени в соответствующем поло элемента каталога Разряды Описание 15-11 Часы (0-23) 10-5 Минуты (0-59) 4-0 Сокунды/2 (0-29) Таблица 14.3. Кодирование даты в соответствующем поле элемента каталога Разряды Описание 15-9 Год (0 = 1980, 127 = 2107) 8-5 Месяц (1 = январь, 12 = декабрь) 4-0 День (1-31) Обратите внимание, что в поле даты код 0x0000 воспринимается как ошибоч- ный, что помогает файловой системе в поиске неиспользованных и поврежденных полей. Поле “Первый кластер” содержит базовую ссылку на таблицу FAT. Это 16-раз- рядное слово — ничто иное, как номер кластера (единственного или первого в цепи) с данными файла. Наконец, поле “Размер” содержит длинное целое (32-разрядное) число байтов, составляющих файл. По первому символу в имени файла можно также определить, занят элемент ка- талога или свободен. Если элемент задействован, то символ представляет' собой фактический печатаемый символ ASCII, если же элемент пуст, то первый байт структуры — нулевой. В последнем случае мы можем также судить о завершении списка файлов, поскольку файловая система обрабатывает все элементы каталога последовательно. Существует еще и третий вариант: когда файл удаляется из ката- лога, первый символ соответствующего элемента просто заменяется специальным кодом 0хЕ5. Это означает, что содержимое элемента больше не актуально и может быть использовано под новый файл при первой же возможности. Впрочем, про- сматривая список в поисках какого-либо файла, мы можем опять встретить актив- ные элементы. Поиск клада Можно было бы еще многое рассказать о структуре файловой системы FAT16, однако представленного выше описания ключевых механизмов вполне достаточно и мы готовы приступить к написанию программного кода. До сих пор мы придерживались определенного уровня упрощения, игнорируя ряд фундаментальных вопросов: • Как выяснить емкость запоминающего устройства? • Как определить позицию размещения таблицы FAT? • Как выяснить количество секторов в каждом кластере? • Как определить начальную точку области данных? Чуть ниже мы дадим ответ па все эти вопросы, рассмотрев последовательность операций, немного напоминающую поиск сокровища. Для начала мы с помощью функций модуля sdmmc.c, разработанного в предыдущем уроке, инициализируем порты ввода-вывода и убедимся в наличии карты в разъеме. // 0. Инициализация портов ввода-вывода initSD(); // 1. Проверяем наличие карты в разъеме
Полот 267 if (!detectSD()) ( FError = FE_NOT_PRESENT; return NULL; } Затем следует инициализация запоминающего устройства с помощью функции initMedia(). // 2. Инициализация карты if (initMedia()) { FError = FE_CANNOT_INIT; return NULL; } Мы также воспользуемся стандартной С-библиотекой stdlib.h для динами- ческого размещения в памяти двух структур данных: // 3. Распределение памяти под структуру MEDIA D = (MEDIA *) malloc(sizeof(MEDIA)); if (D == NULL) // Указывает на ошибку { FError = FE_MALLOC_FAILED; return NULL; } // 4. Распределение памяти под временный буфер сектора buffer = (unsigned char *) malloc(512); if (buffer == NULL) // Указывает на ошибку { FError = FE_MALLOC_FAILED; free(D); return NULL; } Первая структура (подробнее будет рассмотрена позже) называется MEDIA и предназначена для накопления ответов па все вышеперечисленные вопросы (эда- кая сокровищница). Вторая структура — это обычный массив па 512 байт, в кото- рый будут записываться извлеченные секторы данных в ходе поисковых работ. Помните, что для успешного распределения памяти функцией malloc () необ- ходимо зарезервировать определенную область ОЗУ для кучи (процедура модифи- кации настроек компоновщика описана в контрольном списке “Сборка проекта”). По историческим причинам первый сектор (адрес 0) каждого запоминающего устройства большой емкости содержит так называемую главную загрузочную за- пись (рис. 14.5). Для доступа к ней можно воспользоваться функцией readSEC- TOR(): // 5. Доступ к главной загрузочной записи if (!readSECTOR(0, buffer)) { FError = FE_CANNOT_READ_MBR; free(D); free(buffer); return NULL; } Корректность считанных данных подтверждает' сигнатура: особое значение Ох55ДА в последнем слове сектора главной загрузочной записи.
268 Глава 14. Файловый ввод-вывод #define FO_SIGN OxlFE // Адрес сигнатуры главной загрузочной записи // 6. Проверяем корректность сектора главной загрузочной записи // Проверяем слово сигнатуры if ((buffer[FO_SIGN] != 0x55) || (buffer[FO_SIGN+1] != ОхАА)) { FError = FE_INVALID_MBR; free(D); free(buffer); return NULL; Offset 0 1 2 3 4 5 6 7 8 9 А в с D Е F 00000000 во 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00000010 00 00 00 00 00 00 00 00 00 00 00 00 по 00 00 00 00000020 00 00 00 00 00 0 0 00 00 00 00 00 00 00 0 0 00 00 00000030 00 00 00 00 00 00 0 0 00 00 00 00 00 00 00 00 00 00000040 00 00 00 00 00 0 0 00 00 00 00 00 сю 00 00 00 00 00000050 00 00 00 00 00 00 00 00 00 00 00 00 сю 00 00 00 ; 00000060 00 00 00 00 00 00 0 0 00 00 00 00 00 00 0 0 00 00 00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 оо ; 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 0 0 00 00 00000050 00 00 00 00 00 00 00 00 00 00 00 сю 00 00 0 0 00 000000А0 00 00 00 00 00 00 00 00 00 00 00 00 сю 00 00 00 оооооово 00 00 00 00 00 00 00 00 00 00 00 сю 00 00 00 00 оооооосо 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000000D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 000000Е0 00 00 00 00 00 00 0 0 00 00 00 00 сю 00 сю 0 0 00 OOOOOOFO 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00000130 00 00 00 00 00 0 0 00 00 00 по 00 00 00 00 00 00 00000140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ! 00000150 00 00 00 00 00 0 0 00 00 00 0 0 00 00 сю 00 00 00 ; 00000160 00 00 00 00 00 00 0 0 00 00 00 00 00 00 00 0 0 00 | 00000170 00 00 ею 00 00 00 0 0 00 00 00 00 00 гю 00 сю 00 i 00000180 00 00 00 00 00 00 00 00 00 00 00 сю 00 00 00 00 00000190 00 00 00 00 00 00 0 0 00 00 00 00 00 00 00 00 00 i 000001А0 00 00 00 00 00 00 0 0 00 00 00 00 00 00 00 00 00 i 000001В0 00 00 00 00 00 00 00 00 00 00 00 сю 00 сю 0 0 03 i oooooico 35 00 сю 08 D8 С1 F1 00 00 00 0F С9 0Е 00 00 00 > 000001D0 00 00 00 00 00 00 0 0 00 00 00 00 00 по 00 00 00 i 000001ЕО 00 00 00 00 00 00 00 00 00 00 00 00 сю 00 00 00 j 000001F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA : 5 Ассёи V j Ц» .0An Puc. 14.5. Шестнадцатеричный дамп сектора главной загрузочной записи Когда-то эту запись использовали для сохранения фактического кода, который должен быть выполнен ПК при подаче питания. Тем не менее, современные ПК это- го уже не делают, а для микроконтроллера PIC24 этот код процессора 8086 и подав- но не нужен. Большую часть времени главная загрузочная запись — пустая (обычно заполнена нулями) за исключением одной фиксированной позиции по смещению OxlBE. Здесь находится так называемая таблица разделов (состоит всего лишь из четырех элементов по 16 байт каждый), которая не имеет смысла для относительно небольших по объему карт памяти, наподобие нашей SD/MMC. Опа сохранена только из соображений совместимости и идентична таблицам разделов жестких дисков, применяемых в ПК (см. рис. 14.5). Для нашего приложения имеет смысл исходить из предположения, что вся кар- та была отформатирована как один раздел, и таблица разделов содержит единствен- ный элемент (16-байтный блок). Из этих 16 байт нам понадобятся только некоторые для выяснения размера раздела (должен включать в себя всю карту), начального сектора и, что важнее всего, — типа файловой системы. В извлечении данных из буфера в слова нам помогу!' два макроса:
Полет 269 #define ReadW(a, f) *(unsigned *)(a+f) tfdefine ReadLfa, f) *(unsigned long *)(a+f) Кроме того, для получения корректного смещения в главной загрузочной запи- си пригодятся следующие определения: #define FO_FIRST_P OxlBE // Смещение таблицы первого раздела #define FO_FIRST_TYPE 0xlC2 // Смещение типа первого раздела ^define FO_FIRST_SECT 0xlC6 // Смещение первого сектора 1-го раздела #define FO_FIRST_SIZE OxlCA // Количество секторов в разделе // 7. Считываем количество секторов в разделе psize = ReadL(buffer, FO_FIRST_SIZE); // 8. Проверяем, приемлем ли тип раздела i = buffer[F0_FIRST_TYPE]; switch (i) ( case 0x04: case 0x06: case OxOE: // Корректные варианты FAT16 break; default: FError = FE_PARTITION—TYPE; free(D); free(buffer) ; return NULL; } // switch По историческим причинам файловой системе FAT 16 соответствует несколько кодов, которые могут' быть корректно дешифрованы: 0x04, 0x0 6 и ОхОЕ. Для продолжения поиска клада необходимо извлечь длинное слово (32 разряда), обнаруженное в первом элементе таблицы разделов: FO_FIRST_SECT (0х1С6). // 9. Извлекаем первый сектор первого раздела -> Загрузочная запись firsts = ReadL(buffer, FO_FIRST_SECT); Мы получили адрес следующего сектора, который необходимо считать из уст- ройства. // 10. Получаем загруженный сектор (загрузочная запись) if (!readSECTOR( firsts, buffer)) { free(D); free (buffer); return NULL; } Аналогично главной загрузочной записи он содержит сигнатуру, расположен- ную в последнем слове сектора, которую необходимо проверить. // 11. Проверяем корректность загрузочной записи // Проверяем слово сигнатуры if ((buffer[FO_SIGN] != 0x55) || (buffer[FO_SIGN +1] != OxAA)) { FError = FE_INVALID—BR; free(D); free(buffer); return NULL; }
270 Глава 14. Файловый ввод-вывод Этот сектор называют загрузочной записью (первого раздела), и он содержит фактический выполняемый код, который пас, опять-таки, не интересует (рис. 14.6). Offset 0 1 3 4 5 6 7 8 9 A в c D E F j- . Асеева V j 0001E200 EB 00 90 20 20 20 20 20 20 20 20 co 02 20 01 00 S.l 0001E210 02 00 02 00 00 FS 77 00 3F 00 10 00 Fl 00 00 00 . й. . . 0001E220 OF C9 0E 00 У LI 00 2У 13 18 FD EU 20 20 20 20 20 E.|).ya 0001E230 20 20 20 20 20 20 46 41 54 31 36 20 20 20 00 00 FAT16 0001E240 00 00 00 00 00 00 00 00 00 00 00 co 00 00 0 0 00 0001E250 00 00 00 0 0 00 00 0 0 00 00 00 co GO 00 00 00 00 0001E260 00 00 00 0 0 00 00 OU 00 00 uo 00 00 00 00 00 00 0001E270 00 LIO 00 00 00 00 00 00 00 00 co 00 00 00 00 00 0001E280 00 OU 00 00 00 00 00 00 00 00 00 00 00 DO 00 00 0001E280 00 00 00 00 00 00 00 00 00 00 00 co uo 00 00 00 ... 0001E2A0 00 00 co 00 00 00 00 00 00 uo 00 GO 00 00 00 00 0001E2B0 00 00 00 00 00 00 00 00 00 00 GO 00 00 00 00 00 0001E2C0 00 00 00 00 00 00 00 00 00 00 co 00 DO 00 00 00 0001E2D0 OC01E2E0 00 00 00 00 co co 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 co 00 00 00 00 00 00 00 00 0001E2F0 00 00 co 00 00 00 00 00 00 00 00 00 00 00 00 00 0001E300 00 00 00 ULI 00 00 00 00 00 00 00 uo 00 00 ou 00 0001E310 00 00 00 00 00 00 0 0 00 00 00 00 co 00 00 00 00 0001E320 00 0 0 co 00 00 00 00 00 00 00 co 00 00 00 00 00 0001E330 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0001E340 00 00 co 00 00 00 00 00 00 bo co 00 00 00 00 00 0001E350 00 OU co 00 00 00 00 00 00 00 00 ou 00 00 00 00 0001E360 00 LIO 00 00 00 00 00 00 00 00 co 00 00 00 00 00 0001E370 00 00 co 0 0 00 00 00 00 00 00 co 00 00 00 00 00 0001E380 00 OU GO co 00 00 00 00 00 00 00 00 00 00 00 00 0001E390 00 00 00 00 00 UU 00 00 00 00 UO co 00 00 00 00 0001E3A0 00 ou co 00 00 00 00 00 00 00 00 00 00 00 ou 00 0Q01E3B0 00 00 00 00 00 00 00 00 00 LIO 00 00 00 00 00 00 Q001E3C0 00 00 co 00 00 00 00 00 00 00 co 00 00 00 00 00 0001E3D0 00 0 0 co 00 00 00 00 00 00 00 co co 00 00 00 00 0001E3E0 00 00 co 00 00 00 00 00 00 00 00 00 00 00 00 00 0001E3F0 00 00 co 00 00 00 OU 00 00 00 00 00 00 uo 55 AA Ui Рис. 14.6. Шестнадцатеричный дамп загрузочной записи К счастью, в той же записи в фиксированных, известных позициях находятся другие ответы на паши вопросы и элементы, с помощью которых мы рассчитаем ос- тавшуюся часть карты полной файловой системы FAT 16. Определим ключевые смещения в буфере загрузочной записи: // Смещения ключевых полей #define BR sxc Oxd #define brI ’res Oxe #define BR_ _FAT_ SIZE 0x16 #define BR~ ’fat' 'CPY 0x10 #define BR~ "max’ "root 0x11 загрузочной записи раздела // Секторов на кластер (байт) // Зарезервированные секторы для // загрузочной записи (слово) // Размер FAT в секторах (слово) // Количество копий FAT (байт) // Максимальное количество элементов // в корневом каталоге (нечетное слово) Размер кластера можно вычислить с помощью следующего кода: // 12. Определяем размер кластера D->sxc = buffer[BR_SXC]; // Это же будет флагом, указывающим на то, что носитель смонтирован Определяем позицию, размер и количество копий таблицы FAT: // 13. Определяем FAT, корень и LBA данных // FAT - первый сектор в разделе (загрузочная запись) г // зарезервированные записи D->fat = firsts + ReadW(buffer, BR_RES); D->fatsize = ReadW(buffer, BR_FAT_SIZE); D->fatcopy = buffer[BR_FAT_CPY];
Полот 271 Находим также позицию корневого каталога: // 14. Корень = FAT + (количество секторов в FAT * число копий FAT) D->root = D->fat + (D->fatsize * D->fatcopy); А теперь внимание! Мы готовы откопать последние слитки золота, и пас под- стерегает ловушка! // 15. Максимальное количество элементов в корневом каталоге D->maxroot = ReadW(buffer, BR_MAX_ROOT) ; Заметили ловушку? Her? Тогда подсказываю: взгляните на значение смещения BR MAX ROOT, определенного несколькими строками выше. Обратите внимание на нечетность адреса (0x11). Когда макрос ReadW () попытается использовать его в качестве адреса слова, процессор попадет в ловушку и микроконтроллер PIC24 перезагрузится! Необходим специальный макрос (пусть даже и менее эффектив- ный), который сможет собрать слово из байтов, не угодив в ловушку. // Безопасная версия ReadW для полей с нечетным адресом ttdefine ReadOddW(a, f) (*(a + f) + (*(a+f+l) << 8)) // 15. Максимальное количество элементов в корневом каталоге D->maxroot = ReadOddW( buffer, BR_MAX_ROOT) ; Получить два последних фрагмента данных теперь не составит труда. С их по- мощью мы выясним, где начинается область данных (разбитая на кластеры), и сколько кластеров доступно для нашего приложения: // 16. Данные = корень + (MAXIMUM ROOT *32 / 512) D->data = D->root + ( D->maxroot » 4); // Предполагая, что // maxroot % 16 == 0!!! // 17. Максимум кластеров в разделе = // = (всего секторов - системных секторов )/sxc D->maxcls = (psize - (D->data - firsts)) / D->sxc; Итак, для того чтобы добраться до клада, от пас потребовалось сделать 17 осто- рожных шагов. Теперь у нас есть вся необходимая информация для составления полной картины о структуре файловой системы FAT 16 карты памяти SD/MMC (или почти любого другого запоминающего устройства большой емкости). В таком слу- чае наш клад — это просто еще одна карта, с помощью которой мы будем искать файлы в памяти (рис. 14.7). maxcls кластера Рис. 14.7. Клад найден: полная структура FAT16 maxcls
272 Глава 14. Файловый ввод-вывод Наконец, мы можем увидеть полное определение структуры MEDIA, которую мы, разместив в куче в начале урока, с таким терпением заполняли. Вот как выгля- дит наш сундук с золотом: typeclef struct { LBA fat; // LBA root; // LBA data; // unsigned maxroot; // unsigned maxcls; // unsigned fatsize; // unsigned char fatcopy; unsigned char sxc; // } MEDIA; LBA таблицы FAT LBA корневого каталога LBA области данных Максимальное количество элементов в корне Максимальное количество кластеров в разделе Количество секторов // Количество копий FAT Количество секторов на кластер Теперь мы можем объединить все рассмотренные этапы в одной функции (на- зовем ее mount () по аналогии с функцией операционных систем семейства Unix). Для того чтобы запоминающее устройство большой емкости могло быть использо- вано в Unix, его файловая система должна быть “смонтирована” (mounted), т.е. при- креплена в качестве новой ветви главной (системной) файловой системы. Пользова- телям Windows эта концепция может быть незнакома, поскольку им не предостав- ляется возможность выбирать, где и когда будет смонтирована файловая система нового устройства. Все запоминающие устройства монтируются Windows автома- тически и без каких-либо условий при загрузке компьютера или после подключения носителя в корне файловой системы Windows. При этом им назначаются идентифи- каторы в виде отдельной буквы (“С:”, “D:”, “Е:” и т.д.). //--------------------------------------------------------------------- // mount инициализирует структуру MEDIA для файлового доступа // MEDIA * mount(void) { LBA psize; // Количество секторов в разделе LBA firsts; // LBA первого сектора внутри первого раздела int i; unsigned char *buffer; ... Здесь находятся все 17 этапов операции по поиску клада // 18. Освобождаем временный буфер free(buffer); return D; } // mount Мы также определим глобальный указатель D на структуру MEDIA, который будет хранить результат, возвращаемый функцией mount (). Он послужит в каче- стве отправной точки для всей файловой системы. Изначально предположим, что в каждый момент времени присутствует только одно запоминающее устройство (одна карта). // Глобальные определения MEDIA *D; Также определим функцию unmount (), отвечающую исключительно за осво- бождение области памяти, выделенной под структуру MEDIA. //------------------------------------------------------------------- // unmount освобождает память, выделенную под структуру MEDIA
Полет 273 // void unmount(void) { free(D); D = NULL; } // unmount Открытие файла Теперь, когда перед нам разложена “карта” запоминающего устройства, мы мо- жем приступать к достижению исходной цели: реализации доступа к отдельным файлам. В последующих подразделах этого урока будег разработан набор высоко- уровневых функций, аналогичных тем, которые можно найти в большинстве опера- ционных систем для манипуляции файлов. Нам потребуется функция поиска файла в запоминающем устройстве, функция последовательного чтения данных и еще од- на — для записи и создания новых файлов. Следуя логике, мы начнем с разработки функции fopenMO для поиска всей доступной информации о файле (если он существует) с ее последующим сохранени- ем в повой структуре (назовем ее MFILE во избежание конфликтов с аналогичными структурами и функциями, определенным в стандартной С-библиотеке st di о. h). typedef struct { MEDIA * mda; // Структура MEDIA unsigned char * buffer; // Буфер сектора unsigned cluster; // Первый кластер unsigned eels; // Текущий кластер в файле unsigned sec; // Сектор в текущем кластере unsigned pos; // Позиция в текущем секторе unsigned top; // Количество байт данных в буфере long seek; // Позиция в файле long size; // Размер файла unsigned time; // Время последнего обновления unsigned date; // Дата последнего обновления char name[11]; // Имя файла char chk; // Контрольная сумма структуры MFILE = // ~ (entry + name[0]) unsigned entry; // Позиция элемента в текущем каталоге char mode; // режим ’г’ или 'w' } MFILE; Согласен, что поначалу эта структура кажется слишком длинной (целых 40 байт), однако, как будет показано в оставшейся части урока, нам понадобятся все определенные здесь поля. Подражая реализации стандартной С-библиотеки (общей для множества опера- ционных систем), функция fopenMO будет принимать два строковых (ASCII) па- раметра: имя файла и обозначение режима (“г” для чтения или “w” для записи). MFILE *fopenM(const char *filename, const char *mode) { char c; int i, r; unsigned char *b; // Только что размещенный в памяти буфер MFILE *fp; // Указатель на структуру MFILE MEDIA *mda=D; // Указатель на структуру MEDIA
274 Глава 14. Файловый ввод-вывод Для оптимизации использования памяти структура MFILE размещается только ио необходимости. По сути, в этом и заключается одна из первых задач функции fopenMO, которая возвращает указатель на данную структуру. Если функция fopenMO не срабатывает', то в качестве признака ошибки опа возвращаег указа- тель NULL. Разумеется, для того чтобы файл мог быть открыт, файловая система запоми- нающего устройства должна быть предварительно смонтирована с помощью функ- ции mount (). Указатель на структуру MEDIA уже должен быть размещен в гло- бальном указателе D. // 1. Проверяем, смонтировано ли запоминающее устройство if (D == NULL) // Несмонтировано { FError = FE_MEDIA_NOT_MNTD; return NULL; } Поскольку все операции с запоминающим устройством выполняются блоками по 512 байт, нам понадобится распределить соответствующую область памяти в ка- честве буфера чтепия/записи. // 2. Выделяем буфер для файла b = (unsigned char*)malloc(512) ; if (b == NULL) { FError = FE_MALLOC_FAILED; return NULL; } Дальше можно двигаться только в том случае, если такой объем памяти досту- пен. Затем потребуется выделить еще немного места под структуру MFILE. // 3. Размещаем структуру MFILE в куче fp = (MFILE *) malloc(sizeof( MFILE)); if (fp == NULL) // Документируем ошибку { FError = FE_MALLOC_FAILED; free (b); return NULL; } Теперь указатели на буфер и па структуру MEDIA можно сохранить внутри структуры данных MFILE. // 4. Устанавливаем указатели на структуру MEDIA и на буфер fp->mda = D; fp->buffer = b; Далее необходимо извлечь имя файла с последующим переводом каждого сим- вола в верхний регистр с помощью стандартной функции из С-библиотеки с type, h и дополнением в случае необходимости пробелами до восьми символов. // 5. Форматируем имя файла for(i=0; i<8; i++) { с = toupper(*filename-ы-); // Считываем символ и переводим // его в верхний регистр if (( с == ’.’) || ( с == •\0')) // Расширение break;
Полет 275 else fp->name[i] = с; } // for // Если имя короче 8 символов, дополняем его пробелами while (i<8) fp->name[i++] = ’ Аналогичным образом после отбрасывания точки должно быть отформатирова- но и дополнено до трех символов расширение файла. // 6. При наличии расширения if (с != ’\0’) { for(i=8; i<ll; i++) { с = toupper(*filename++); // Считываем символ, переводим // в верхний регистр if (с == '.') с = toupper(*filename++); if (с == ’ \0 ’) // Короткое расширение break; else fp->name[i] = с; } // for // Если расширение короче 3 символов, дополняем его пробелами while (i<ll) fp->name[i++] = ’ } H if Хотя большинство С-библиотек предоставляют обширную поддержку разнооб- разных “режимов” доступа к файлам (например, различение текстовых и бинарных файлов или возможность дополнения данными), мы примем на вооружение (по крайней мере, для начала) подмножество из двух базовых вариантов: “г” и “w”. // 7. Копируем символ файлового режима (г, w) if ((*mode == ’г') I I (*mode == 'w’)) fp->mode = *mode; else { FError = FE_INVALID_MODE; goto ExitOpen; } Отформатировав имя файла, мы можем приступать к поиску соответствующего элемента в корневом каталоге запоминающего устройства. // 8. Поиск заголовка в текущем каталоге if ((г = findDIR(fp)) == FAIL) { FError = FE_FIND_ERROR; goto ExitOpen; I Оставим подробности механизма поиска без внимания, просто доверившись функции findDIRO, возвращающей одно из трех значений: FAIL (ошибка), NOTJFOUND (не найден) и FOUND (найден). Возможность ошибки всегда следует учитывать во избежание аварийного прерывания процесса доступа. В такой ситуа- ции лучше сразу же освободить ранее выделенную память и вернуть указатель NULL, предварительно сохранив код ошибки в специальной переменной FError, как это было в процессе монтирования. Если поиск файла завершился без ошибок
276 Глава 14. Файловый ввод-вывод (независимо от результата поиска), то мы можем продолжить инициализацию структуры MFILE. // 9. Инициализируем все счетчики на начало файла fp->seek =0; // Первый байт в файле fp->sec =0; // Первый сектор в кластере fp->pos = 0; // Первый байт в секторе/кластере Счетчик seek служит для отслеживания позиции внутри файла при последова- тельном доступе. Он содержит длинное целое число без знака в диапазоне от 0 до размера файла в байтах. Поле sec хранит номер активного сектора внутри текуще- го кластера: целочисленное значение в диапазоне от 0 до sxc-1 (количества секто- ров, из которых состоит каждый кластер данных). Наконец, ноле pos содержит по- мер байта внутри текущего буфера, к которому будет осуществлен следующий дос- туп (целое число от 0 до 511). // 10. depending on the mode (read or write) if (fp->mode == ’r') { В этой точке необходимо выполнить ряд операций в зависимости от режима ра- боты с файлом: открытие для чтения или создание для записи. Для начала займемся чтением. В этом случае файл обязательно должен существовать. // 10.1. ’г' открыт для чтения if (г == NOT_FOUND) { FError = FE_FILE_NOT_FOUND; goto ExitOpen; } Если файл существует, то функция findDIRO заполнила несколько полей структуры MFILE: • entry — позиция файла в корневом каталоге; • cluster — помер первого кластера, используемого для сохранения данных файла; • size — количество байт, составляющих файл; • время (time) и дата (date) создания; • атрибуты файла. Номер первого кластера станет нашим текущим кластером eels: else { // Найден // 10.2. Устанавливаем текущий указатель кластера на первый // кластер файла fp->ccls = fp->cluster; Теперь у пас есть вся информация, необходимая для идентификации первого сектора данных в буфере. Функция readDATA () (будет рассмотрена чуть позже) выполняет' простые расчеты, необходимые для преобразования значений cels и sec в абсолютный помер сектора внутри области данных, а также с помощью низкоуровневой функции readSECTOR () извлекает данные из запоминающего устройства. // 10.3. Чтение сектора данных из файла
Полет 277 if (!readDATA(fp)) { goto ExitOpen; } Обратите внимание, что длина файла не обязательно кратна размеру сектора, из-за чего вполне вероятна ситуация, когда только часть извлеченных в буфер дан- ных будет действительно принадлежать файлу. Фактическую границу данных файла можно отследить с помощью ноля top структуры MFILE. // 10.4. Определяем, сколько данных внутри буфера if (fp->size-fp->seek < 512) fp->top = fp->size - fp->seek; else fp->top = 512; } // Найден } // ’r’ Поскольку это, но сути, — все, что нам требуется для завершения функции fopenM () (при открытии файла на чтение), мы можем вернуть указатель на струк- туру MFILE. Впрочем, в качестве дополнительной меры предосторожности против возможных ошибок, связанных с использованием указателей, мы вычислим про- стую контрольную сумму. // 12. Вычисляем контрольную сумму для структуры MFILE fp->chk = ^(fp->entry + fp->name[0]); return fp; ПРИМЕЧАНИЕ Скоро мы поместим перед этим пунктом еще один фрагмент кода, поэтому не ду- майте, что я допустил ошибку в нумерации. В том случае, если на каком-либо из рассмотренных выше этанов произойдет сбой, функция вернег указатель NULL, предварительно освободив память, выделен- ную под буфер сектора и структуру MFILE. // 13. Выход с ошибкой ExitOpen: free(fp->buffer) ; free(fp); return NULL; } // fopenM Теперь реализуем две вспомогательных функции, использованные внутри функции fopenM (). Начнем с readDATA () : unsigned readDATA(MFILE *fp) { LBA 1; // Расчитываем LBA кластера/сектора 1 = fp->mda->data + (LBA)(fp->ccls-2) * fp->mda->sxc + fp->sec; return(readSECTOR(1, fp->buffer)) } // readDATA Обратите внимание, что для вычисления корректного номера сектора мы ис- пользуем поля data и sxc структуры MEDIA. Как видим, все очень просто!
278 Глава 14. Файловый ввод-вывод Аналогичным образом можно создать функцию чтения из корневого каталога блока данных, содержащего заданный элемент. unsigned readDIR(MFILE *fp, unsigned e) // Загружает в файловый буфер сектор текущего элемента // Возвращает FAIL или TRUE { LBA 1; // Загружаем корневой сектор, содержащий элемент каталога ”е” 1 = fp->mda->root + (е » 4) ; return (readSECTOR(1, fp->buffer)); } // readDIR Мы знаем, что длина каждого элемента каталога составляет 32 байта, а значит каждый сектор содержит 16 элементов. Теперь можно создать функцию f indDIR () в виде короткой последовательно- сти операций в цикле просмотра всех доступных элементов корневого каталога. unsigned findDIR(MFILE *fp) // fp - файловая структура // Возвращается FOUND, NOT_FOUND или FAIL { unsigned eCount; // Счетчик текущего элемента unsigned e; // Смещение текущего элемента в буфере int i, а, с, d; MEDIA *mda = fp->mda; // 1. Начинаем с первого элемента eCount = 0; // Загружаем первый сектор корня if (!readDIR(fp, eCount)) return FAIL; Для начала мы загружаем в буфер первый корневой сектор, содержащий первые 16 элементов. Для каждого элемента вычисляется его смещение внутри буфера: // 2. Цикл до достижения конца или нахождения файла while (1) { // 2.0. Определяем смещение в текущем буфере е = (eCount & Oxf) * DIR_ESIZE; Анализируем первый символ имени файла: // 2.1. Считываем первый символ имени файла а = fp->buffer[e + DIR_NAME]; Если он содержит 0, что указывает па пустой элемент и окончание списка, то мы сразу же выходим из функции, указав, что файл с таким именем не найден. // 2.2. Выходим, если элемент пуст (конец списка) if (а == DIR_EMPTY) { return NOT_FOUND; } // Пустой элемент Другой вариант — элемент, помеченный как удаленный. В таком случае мы пропускаем его. // 2.3. Пропускаем удаленные элементы
Полет 279 if (а != DIR_DEL) { Если элемент не удален, то необходимо проверить атрибуты файла, чтобы оп- ределить, соответствуют ли они требуемому типу объекта (подкаталог, метка тома или длинное имя файла). Поскольку мы все упрощаем и держимся в стороне от наи- более сложных (и иногда запатентованных) свойств последних версий стандарта FAT, пас данный вопрос не интересует. // 2.3.1. Если не метка тома и не каталог, то сравниваем имена а = fp->buffer[e + DIR_ATTRIB] ; if (!((а & ATT_DIR) |I (а & ATT_VOL))) ( Имена файлов сравниваем посимвольно, проверяя их на полное совпадение. // Сравниваем имя и расширение файла for (i=DIR_NAME; i<DIR_ATTRIB; i++) ( if (( fp->buffer[e + i]) != (fp->name[i])) break; // Обнаружено различие } Только если символы совпадают, ключевые фрагменты информации извлека- ются из элемента и копируются в структуру MFILE, а функция возвращает значение FOUND. if (i == DIR_ATTRIB) { // Элемент найден, заполняем структуру MFILE fp->entry = eCount; // Сохраняем индекс элемента fp->time = ReadW(fp~>buffer, e + DIR_TIME); fp->date = ReadW(fp->buffer, e + DIR_DATE); fp->size = ReadL(fp->buffer, e + DIR_SIZE); fp->cluster = ReadL(fp->buffer, e + DIR_CLST); return FOUND; } } // He метка тома и не каталог } // Не удален Если имя файла и/или расширение различны, то мы просто продолжаем поиск, переходя к следующему элементу. При этом после каждой группы из 16 элементов не забываем загружать следующий сектор из корневого каталога. // 2.4. Извлекаем следующий элемент eCount++; if (eCount & Oxf == 0) { // Загружаем новый сектор из каталога if (!readDIR(fp, eCount)) return FAIL; } Мы знаем максимальное число элементов в корневом каталоге (maxroot). Ес- ли достигнут конец каталога, а совпадения так и не было найдено, то функция воз- вращает значение NOT_FOUND. // 2.5. Выходим из цикла при достижении конца или ошибке if (eCount >= mda->maxroot) return NOT—FOUND; // Достигнут последний элемент }// while
280 Глава 14. Файловый ввод-вывод } // findDIR Чтение данных из файла Вот и настал долгожданный момент, когда файловая система смонтирована, файл найден и открыт для чтения, и мы можем разработать функцию ffreadMQ для считывания блоков данных. unsigned freadMfvoid * dest, unsigned size, MFILE *fp) // fp - указатель на структуру MFILE // dest - указатель на буфер назначения // count - количество передаваемых байтов // Возвращает количество фактически переданных байтов { MEDIA * mda = fp->mda; // Структура MEDIA unsigned count=size; // Счетчик передаваемых байтов unsigned len; Имя этой функции, а также количество и порядок следования ее параметров, опять-таки, подобны аналогичным функциям из стандартных С-библиотек. Буфер назначения предназначен для копирования данных, считанных из файла, а количе- ство байтов необходимо при передаче указателя па открытую структуру MFILE. Функция freadMO старается считать из файла как можно больше байтов, и возвращает фактически считанное их количество. В нашей простой реализации, если возвращенное число не совпадает с запрошенным вызывающим приложением, то это говорит о том, что произошло нечто заслуживающее внимания. Скорее всего, был достигну!' конец файла, однако мог и произойти какой-нибудь сбой (например, извлечение карты в процессе чтения). Как обычно, мы проявим недоверие к переданному в качестве параметра указа- телю и проверим, указывает ли он на корректную структуру MFILE. Для этого пе- ресчитаем контрольную сумму и сравним ее со значением, полученным в результате успешного открытия файла. // 1. Проверяем, указывает ли fp на корректную структуру открытого // файла if ((fp->entry + fp->name[0] != ~fp->chk ) || (fp->mode != ’г')) { // Ошибка контрольной суммы или не открыт в режиме чтения FError = FE_INVALID_FILE; return size-count; } Только теперь можно начать цикл передачи данных из буфера сектора. // 2. Цикл передачи данных while (count>0) { Внутри цикла в первую очередь проверяется текущая позиция по отношению к общему размеру файла. // 2.1. Проверяем достижение конца файла if (fp->seek >= fp->size) { FError = FE_EOF; break; } // Достигнут конец
Полет 281 Обратите внимание, что эта ошибка возникнет только в том случае, если при- ложение, вызывающее функцию freadMO, проигнорирует предыдущий симптом: последний вызов функции f readM () вернул количество байт данных меньше, чем требуется, или приложение затребовало точное число байтов, доступных в файле. В противном случае мы проверяем, заполнен ли текущий буфер данных. // 2.2. Если необходимо, загружаем новый сектор if (fp->pos == fp->top) { В случае необходимости сбрасываем указатели па буферы и пытаемся загрузить из файла следующий сектор: fp->pos = 0; fp->sec++; Если мы уже использовали все секторы в текущем кластере, то придется перей- ти к следующему кластеру, заглянув внутрь таблицы FAT и проследовав по кла- стерной цепочке: // 2.2.1. Если необходимо, переходим к следующему кластеру if ( fp->sec == mda->sxc) { fp->sec = 0; if (!nextFAT(fp, 1)) { break; } } В любом случае новый сектор данных загружается в буфер. Попутно мы прове- ряем, не последний ли он в файле и не заполнен ли только частично: // 2.2.2. Загружаем сектор данных if (!readDATA(fp)) { break; } // 2.2.3. Определяем, сколько данных на самом деле в буфере if (fp->size-fp->seek < 512) fp->top = fp->size - fp->seek; else fp->top = 512; } // Загрузка нового сектора Итак, в буфере находятся готовые к передаче данные, и мы можем определить, какое их количество доступно для передачи одним пакетом: // 2.3. Копируем максимальное число байтов одним пакетом // Берем столько, сколько помещается в текущем секторе if (fp->pos+count < fp->top) len = count; // Все помещается в текущем секторе else len = fp->top - fp->pos; // Берем первый пакет. Есть еще memcpy(dest, fp->buffer 4- fp->pos, len); С помощью функции memcpy () из стандартной С-библиотеки st ring, h мы перемещаем блок данных из файлового буфера в буфер назначения. При этом дос- тигается наивысшая производительность, поскольку подобные подпрограммы он-
282 Глава 14. Файловый ввод-вывод химизированы. Указатели и счетчики обновляются, и цикл повторяется до тех пор, пока не будут' переданы все запрошенные данные. // 2.4. Обновляем все счетчики и указатели count-= len; // Вычисляем, что осталось dest += len; // Увеличиваем указатель на буфер назначения fp->pos += len; // Увеличиваем указатель на текущий сектор fp->seek 1 += len; // Увеличиваем указатель поиска } // while count Наконец, мы можем выйти из функции и возвратить количество фактически пе- реданных в цикле байтов: // 3. Возвращаем количество фактически прочитанных байтов return size-count; } // freadM Закрытие файла Поскольку мы можем открыть файл только для чтения (с помощью функции f орепМ ()), для его закрытия не потребуется больших усилий. Вначале необходимо сделать недействительной контрольную сумму, созданную функцией fopenMO, а затем освободить всю память, выделенную под структуру MFILE и буфер сектора. unsigned fcloseM(MFILE *fp) { // 1. Делаем недействительной файловую структуру fp->chk = fp->entry + fp->name[0]; // Искажаем контрольную сумму! // 2. Освобождаем буфер и структуру MFILE free(fp->buffer); free(fp); } // fcloseM Создание модуля файлового ввода-вывода Мы можем создать небольшой библиотечный модуль, сохранив все созданные до этого момента функции в файле f ileio. с. Добавим в него типичный заголовок со ссылками на несколько включаемых файлов: /* * * Интерфейс файлового ввода-вывода ★ * * * Модуль: fileio.с * / // Задействованы стандартные С-библиотеки tfinclude <stdlib.h> // NULL, malloc, free... #include <ctype.h> // toupper... ttinclude <string.h> // memcpy... #include ’’sdmmc.h” // Интерфейс карты SD/MMC #include ’’fileio.h” // Подпрограммы файлового ввода-вывода И конечно же, нам необходимо создать файл f ileio .h со всеми определения- мии прототипами, общедоступными для использования в будущих приложениях. 7 * .......................... ~....... .................... ** Интерфейс файлового ввода-вывода
Полет 283 ** Поддержка FAT16 ★ ★ ** Модуль: fileio.h */ extern char FError; // Переменная для сохранения кода ошибки // Коды ошибок файлового ввода-вывода #define FE_IDE_ERROR 1 // ttdefine FE_NOT_PRESENT 2 // #define FE_PARTITION_TYPE 3 // ttdefine FE_INVALID_MBR 4 // // ^define FE_INVALID_BR 5 // fldefine FE_MEDIA_NOT_MNTD 6 // #define FE_FILE_NOT_FOUND 7 // ^define FE_INVALID_FILE 8 // frdefine FE_FAT_EOF 9 // // # define FE_EOF 10 // tfdefine FE_INVALID_CLUSTER 11 // ^define FE_DIR_FULL 12 // ttdefine FE_MEDIA_FULL 13 // # define FE_FILE_OVERWRITE 14 // # define FE_CANNOT_INIT 15 // # define FE_CANNOT_READ_MBR 16 // // # define FE_MALLOC_FAILED 17 // // # define FE_INVALID_MODE 18 // tfdefine FE_FIND_ERROR 19 // typedef struct { LBA fat; // LBA root; // LBA data; // unsigned maxroot; // // unsigned maxcls; // // unsigned fatsize; // unsigned char fatcopy; // unsigned char sxc; // // (’ } MEDIA; Ошибка выполнения команды IDE Карта отсутствует Неверный тип раздела, не FAT12 Неверная сигнатура сектора главной загрузочной записи Неверная сигнатура загрузочной записи Носитель не смонтирован Файл не найден открытым для чтения Файл не открыт FAT пытается прочитать за пределами конца файла Достигнут конец файла Неверное значение кластера > maxcls Взяты все элементы корневого каталога Взяты все кластеры в разделе Уже существует файл с тем же именем Невозможно инициализировать карту Невозможно прочитать главную загрузочную запись Функции malloc не удается разместить в памяти структуру MFILE Режим не r.w. Сбой в ходе поиска файла LBA таблицы FAT LBA корневого каталога LBA области данных Максимальное количество элементов в корневом каталоге Максимальное количество кластеров в разделе Количество секторов Количество копий Количество секторов на кластер =0 означает, что носитель смонтирован) typedef struct { MEDIA * mda; // unsigned char * buffer; // unsigned cluster; // unsigned eels; // unsigned sec; // unsigned pos; // unsigned top; // long seek; // long size; // unsigned time; // Указатель на структуру MEDIA Буфер сектора Первый кластер Текущий кластер в файле Сектор в текущем кластере Позиция в текущем секторе Количество байт данных в буфере Позиция в файле Размер файла Время последнего обновления
284 Глава 14. Файловый ввод-вывод unsigned date; // char name[11]; // char chk; // unsigned entry; // char mode; // Дата последнего обновления Имя файла Контрольная сумма = -(entry+name[0]) Позиция элемента в текущем каталоге Режим 'г', ’w', 'а' } MFILE; // Атрибуты файла #define ATT_RO 1 // Только для чтения #define ATT_HIDE 2 // Скрытый #define att_sys 4 // Системный файл #define ATT_VOL 8 // Метка тома if def ine ATT_DIR 0x10 // Подкаталог #define ATT_ARC 0x20 // Архив #define ATT_LFN OxOf // Маска для записей с длинными именами файлов #define FOUND 2 // Совпадение с элементом каталога #define NOT_FOUND 1 // Элемент каталога не найден // Макрос для извлечения слов и длинных целых из массива байтов // Внимание! Если адрес не выровнять по слову, процессор перезагрузится #define ReadW(a, f) *(unsigned *)(a+f) #define ReadL(a, f) *(unsigned long *)(a+f) // Это "безопасная" версия ReadW для применения к нечетным адресным полям #define ReadOddW(a, f) (*(a+f) + (*(a+f+l) « 8)) // Прототипы unsigned nextFAT(MFILE * fp, unsigned n); unsigned newFAT(MFILE * fp); unsigned readDIR(MFILE *fp, unsigned entry); unsigned findDIR(MFILE *fp); unsigned newDIR (MFILE *fp); MEDIA * mount(void); void unmount(void); MFILE * fopenM (const char *name, const char *mode); unsigned freadM (void * dest, unsigned count, MFILE *); unsigned fwriteM (void * src, unsigned count, MFILE *); unsigned fcloseM (MFILE *fp); Файлы fileio.c и fileio.h находятся также на прилагаемом к книге компакт-диске в папке Проекты\14 - Файлы. Некоторые из функций, перечисленных в файле fileio.h, мы еще не рас- сматривали. В следующих подразделах мы наверстаем упущенное. Тестирование функций fopenMO и freadM() Что-то давненько мы не компоновали проект. Можно сказать, что уже достиг- нута “критическая масса”: минимальное ядро подпрограмм, без которых приложе- ние просто не будет работать. Теперь, обладая этой базовой функциональностью, мы можем разработать первую тестовую программу чтения файла из карты SD/MMC, отформатированной в файловой системе FAT 16.
Полет 285 Идея заключается в копировании любого текстового файла па карту SD/MMC с ПК, после чего микроконтроллер PIC24 с модулем file io. с прочитает этот файл и передаст его содержимое через последовательный порт обратно на ПК (дан- ные можно увидеть с помощью программы HyperTerminal или любого другого тер- минала). Тестовый модуль ReadTest. с выглядит следующим образом. 7*..................................................... ** ReadTest.с */ ^include <р24fj128ga010.h> #include "SDMMC.h" ^include "fileio.h" ttinclude "../delay/delay.h" #include "../3 comm/conu2.h" ttdefine B_SIZE 10 char data[B_SIZE]; main(void) ( MFILE *fs; unsigned i, r; //Инициализация initU2();. //115 200 бод 8,n, 1 putsU2("Инициализация"); while(!detectSD()); // Предполагаем, что вывод SDCD по умолчанию - вход Delayms(100); // Ожидаем подачи питания на карту putsU2("Носитель обнаружен"); if (mount()) { putsU2("Смонтирован"); if (fs = fopenM("name.txt", "r")) { putsU2("Файл открыт"); do { r = freadM(data, B_SIZE, fs) ; for(i=0; i<r; i++) putU2(data[i]); } while(r==B_SIZE); fcloseM(fs); putsU2("Файл закрыт"); } else putsU2("He могу открыть файл"); unmount(); putsU2("Носитель размонтирован"); ) // Главный цикл while(1); } // main Файл ReadTest.с находится также на прилагаемом к книге компакт-диске в папке Проекты\ 14 - Файлы.
286 Глава 14. Файловый ввод-вывод Мы воспользуемся модулем последовательного обмена данными conu2. с, разработанным в одном из первых уроков, а также модулем delay, с, содержащим функцию delayms () (см. предыдущий урок). Последовательность операций ана- логична тесту модуля sdmmc.c из предыдущей главы, однако на этот раз вместо вызова функции initMediaO и чтепия/заниси непосредственно секторов карты SD/MMC мы вызовет функцию mount () для получения доступа к файловой систе- ме FAT 16 карты. Мы откроем файл по его имени и считаем из него данные блоками произвольной длины B_SIZE, после чего выведем его содержимое в последова- тельный порт платы Explorer 16. Когда все содержимое файла будет исчерпано, мы закроем его, а затем освободим всю задействованную память. Создайте новый проект и добавьте в него все необходимые модули: • sdmmc.c; • fileio.c; • conu2.c; • delay, с; • readtest.с; • все необходимые файлы . h. С помощью соответствующего контрольного списка настройте отладчик ICD2, не забыв настроить компоновщик. В том же конфигурационном диалоговом окне немного увеличьте кучу, чтобы память под структуры файловой системы и буферы можно было распределять динамически (даже если достаточно 580 байт, дайте куче вдоволь места для маневра). После компоновки проекта и программирования выполняемого файла в плату Explorer 16 можно запустить тест. Если все было сделано правильно, то содержимое текстового файла будет' выведено на экран терминала. Помните о возможности перекомпилировать проект и запустить тест с другими размерами буфера данных (от одного байта до всего наличного пространства памяти микроконтроллера PIC24). Функция freadM () позаботится о чтении такого коли- чества секторов, которое необходимо для выполнения запроса, до тех нор, пока бу- дут доступны данные файла. Запись данных в файл Нам еще предстоит многое разработать. Модуль filed.о. с можно считать за- вершенным только тогда, когда он поддерживает возможность создания новых фай- лов. Для этого необходимо разработать функцию fwriteM (), а также откорректи- ровать функцию fopenM (). До сих пор опа возвращала код ошибки, если файл не был найден в корневом каталоге или не был задан режим “г”, однако при открытии файла па запись именно эти условия являются необходимыми. Таким образом, по- сле проверки режима требуется добавить новую функциональность. На этот раз нас интересует' вариант, когда файл при первом просмотре каталога не обнаружен: else // 11. Открываем для записи { if (г == NOT_FOUND) { Под данные нового файла в памяти требуется выделить новый кластер. Для по- иска свободной ячейки в таблице FAT (кластера, помеченного маркером 0x0000) мы воспользуемся функцией newFAT (). Если поиск завершается неудачно, то это
Полет 287 среди прочего может указывать па то, что устройство заполнено, и все кластеры данных использованы. Если же свободный кластер найден, функция должна сделать его первым для файла и обновить структуру MFILE. // 11.1. Размещаем первый класт fp->ccls = 0; // Указатель на новый файл if (newFAT(fp) == FAIL) { // Носитель, наверное, заполнен FError = FE_MEDIA_FULL; goto ExitOpen; } fp->cluster = fp->ccls; Далее нам необходимо найти доступное пространство в каталоге для нового файла. Это потребует второго просмотра корневого каталога — па этот раз в поис- ках первого элемента, помеченного как удаленный (код 0хЕ5), или окончания спи- ска, где будет найден пустой элемент (помечен кодом 0x00). // 11.2. Создание нового элемента // Повторный поиск. На этот раз - пустого элемента if ( (г = newDIR(fp)) == FAIL) // Документируем ошибку { FError = FE_IDE_ERROR; goto ExitOpen; } Поиск доступного элемента реализован в функции newDIR (), которая, подоб- но рассмотренной ранее функции f indDIR (), возвращает один из трех возможных кодов: • FAIL — общий сбой (или карта отключена); • NOT FOUND — корневой каталог, скорее всего, заполнен; • FOUND — найден пустой элемент. // 11.3. Новый элемент не найден if (г == NOT_FOUND) { FError = FE_DIR_FULL; goto ExitOpen; } В первых двух случаях должна быть документирована ошибка, однако, если элемент найден, его необходимо инициализировать. После вычисления смещения элемента в текущем буфере часть его полей должна быть заполнена данными из структуры MFILE. Вначале следует размер файла: else // 11.4. Заполнение нового элемента fp->entry { // 11.4.1. Инициализируем размер файла fp->size = 0; // 11.4.2. Определяем смещение в секторе каталога е = (fp->entry & Oxf) * DIR_ESIZE; // 16 элементов на сектор // 11.4.3. Устанавливаем исходный размер файла в 0 fp->buffer[e + DIR_SIZE] = 0; fp->buffer[e + DIR_SIZE4-1 ] = 0; fp->buffer[e 4- DIR_SIZE4-2] = 0; fp->buffer[e 4- DIR_SIZE4-3] = 0;
288 Глава 14. Файловый ввод-вывод Содержимое для полей времени и даты можно получить из регистров модуля RTCC или с помощью любого другого механизма отслеживания времени. Мы вос- пользуемся просто некоторым значением по умолчанию, чего будег достаточно для демонстрационных целей. fp->date = 0x34FE; // 30 июля 2006 года fp->buffer[e + DIR_DATE] = fp->date; fp->buffer[e + DIR_DATE+1]= fp->date»8; fp->buffer[e + DIR_TIME] = fp->time; fp->buffer[e + DIR—TIME +1 ] = fp->time»8; В завершение определим для элемента каталога помер первого кластера, имя файла и атрибуты. // 11.4.5. Устанавливаем первый кластер fp->buf£er[e + DIR_CLSTJ = fp->cluster; fp->buffer[e т DIR_CLS'J4-1 J = (fp->cluster»8) ; // 11.4.6. Устанавливаем имя for (i = 0; i<DIR_ATTRIB; i+i) fp->buffer[e + i] = fp->name[i); // 11.4.7. Устанавливаем атрибуты fp->buffer[e т DIR_ATTRIB] = ATT_ARC; // 11.4.8. Обновляем сектор каталога if (!writeDIR(fp, fp->entry)) { FError = FE_IDE_ERROR; goto ExitOpen; } } // Новый элемент } //He найден Возвращаемся к результатам нашего первого просмотра корневого каталога. Если файл с тем же именем действительно найден, то необходимо документировать ошибку. else // Файл уже существует, документируем ошибку ( FError = FE-FILE_OVERWRITE; goto ExitOpen } В качестве альтернативного варианта мы могли бы вначале удалить текущий элемент, освободить все задействованные кластеры, а затем начать все сначала. Тем не менее, документирование ошибки — более простой метод. На этом — все, что касается изменений в функции f орепМ (). Теперь можно создать функцию fwriteMO, прототип которой, опять-таки, позаимствован в стандартной С-библиотеке. unsigned fwriteM(void *src, unsigned count, MFILE * fp) // src - указывает на исходные данные (буфер) // count - количество записываемых байт // Возвращает количество фактически записанных байт { MEDIA *mda = fp->mda; unsigned len, size = count;
Полет 289 // 1. Проверяем, открыт ли файл if (fp->entry + fp->name[0] ’= ~fp->chk) { // Неверная контрольная сумма FError = FE_INVALID_FILE; return FAIL; } Переданные в функцию параметры идентичны параметрам, использованным в функции freadM (). То же относится и к первой проверке целостности структуры MFILE. Она поможет определить, можно ли доверять содержимому структуры MFILE, подготовленной вызовом функции fopenM (). Сердцевиной функции fwriteM () также будет цикл: // 2. Цикл записи, подсчитывающий байты while (count>0) ( Наша цель — передать за один раз как можно больше байт данных с помощью скоростной функции memcpy () из библиотеки string . h. // 2.1. Копируем за один раз как можно больше байтов if (fp->pos+count < 512) len = count; else len = 512- fp->pos ; memcpy(fp->buffer + fp->pos, src, len); Для отслеживания позиции в процессе добавления данных в буфер и увеличе- ния размера файла необходимо обновлять несколько указателей и счетчиков. // 2.2. Обновляем все указатели и счетчики fp->pos+=len; fp->seek+=len; count-=len; src+=len; // / / // // Увеличиваем позицию в буфере Подсчитываем добавленные байты Обновляем счетчик Увеличиваем указатель на источник // 2.3. Обновляем также размер файла if (fp->seek > fp- >si ze) fp->size = fp->seek; Как только буфер заполнен, данные необходимо передать носителю в сектор выделенного кластера. // 2.4. Если буфер заполнен, записываем в текущий сектор if (fp->pos == 512) { // 2.4.1. Буфер записи заполнен данными if (!writeDATA(fp)) return FAIL; Обратите внимание, что ошибка в этой точке — фатальная. Мы возвращаем код FAIL (значение 0), указывающий, что не было передано ни одного байта. По сути, все данные, записанные до этого момента в запоминающее устройство, будут поте- ряны. Если же все прошло без ошибок, то мы можем прирастать указатели сектора. На тот случай, когда все секторы в текущем кластере исчерпаны, необходимо раз- местить новый сектор с помощью функции newFAT (). // 2.4.2. Переходим к следующему сектору в кластере fp—>pos = 0; fp->sec++;
290 Глава 14. Файловый ввод-вывод // 2.4.3. Если необходимо, получаем новый кластер if (fp->sec == mda->sxc) { fp->sec = 0; if (newFAT(fp)== FAIL) return FAIL; } } // Сохранение сектора } // while count При разработке функции newFAT () необходимо удостовериться, что опа над- лежащим образом поддерживает цепочки кластеров в FAT при их добавлении в файл. // 3. Количество фактически записанных байтов return size-count; } // fwriteM На этом функция fwriteM () завершена, и после выхода из цикла мы можем выдать количество записанных байтов. Еще раз о закрытии файла В то время как закрытие файла, открытого для чтения, было сущей формально- стью и заключалось в освобождении определенной области памяти в куче, при за- крытии файла, открытого для записи, следует выполнить множество операций. Нам понадобится доработать функцию fcloseM (), которая будет начинаться с провер- ки поля режима. unsigned fcloseM(MFILE *fp) ( unsigned e, r; // Смещение элемента каталога в текущем буфере г = FAIL; // 1. Проверяем, был ли файл открыт для записи if (fp->mode == 'w') { По сути, при закрытии файла в буфере все еще находятся данные, которые не- обходимо записать в запоминающее устройство, хотя они и не заполнят целый сек- тор. // 1.1. Если текущий буфер содержит данные, очищаем его if (fp->pos >0) { if (!writeDATA(fp) ) goto Exitclose; } И опять-таки, любая ошибка в этот момент — фатальная, т.е. все данные файла ввиду некорректного завершения работы функции fcloseM () будут' утеряны. Необходимо извлечь корректный сектор корневого каталога и вычислить сме- щение для элемента каталога внутри буфера. // 1.2. Обновляем элемент каталога // 1.2.1. Извлекаем сектор каталога if (!readDIR(fp, fp->entry)) goto Exitclose;
Полет 291 // 1.2.2. Определяем позицию в секторе каталога е = (fp->entry & Oxf) * DIR_ESIZE; // 16 элементов на сектор Далее необходимо обновить файловый элемент в корневом каталоге фактиче- ским размером файла (изначально было равен нулю). // 1.2.3. Обновляем размер файла fp->buffer[e + DIR_SIZE] = fp->size; fp->buffer[е + DIR_SIZE+1] = fp->size>>8; fp->buffer[e + DIR_SIZE+2] = fp->size>>l6; fp->buffer[e + DIR_SIZE+3] = fp->size>>24; Наконец, обратно на носитель записывается целый сектор корневого каталога, содержащий файловый элемент. // 1.2.4. Обновление сектора каталога if (!writeDIR(fp, fp->entry)) goto Exitclose; } // Запись В завершение функции fcloseM() мы аннулируем ноле контрольной суммы для предотвращения случайного использования структуры MFILE и высвобождения выделенной под нее памяти и буфера. //2. В случае успеха, выходим г = TRUE; ExitClose: // 3. Аннулируем файловую структуру fp->chk = fp->entry + fp->name[0]; // Искажаем контрольную сумму // 4. Освобождаем буфер и структуру MFILE free(fp->buffer); free( fp); return(r); } // fcloseM Вспомогательные функции В функциях fopenMO, fcloseM() и fwriteMO для решения важных по- вторяющихся задач мы использовали несколько низкоуровневых функций. Для на- чала рассмотрим функцию newDIRO, выполняющую поиск свободного места в корневом каталоге для создания нового файла. На первый взгляд опа похожа на функцию f indDIR (), однако решаемая ею задача существенно отличается . unsigned newDIR(MFILE *fp) // fp - файловая структура // Возвращает FOUND/FAIL, заполненное поле fp->entry { unsigned eCount; // Счетчик текущего элемента unsigned е; // Смещение текущего элемента в буфере int а; MEDIA *mda = fp->mda; // 1. Начинаем с первого элемента eCount = 0; Загружаем первый сектор корня
292 Глава 14. Файловый ввод-вывод if ( ! r-eadDIR (fp, eCount)) return FAIL; // 2. Цикл до достижения конца или нахождения файла while (1) { // 2.0. Определяем смещение в текущем буфере е = (eCount&Oxf) * DIR_ESIZE; // 2.1. Считываем первый символ имени файла а = fp->buffer[е + DIR_NAME]; // 2.2. Прерываем, если пустой (конец списка) или удаленный if ((а == DIR_EMPTY) ||(а == DIR_DEL)) { fp->entry = eCount; return FOUND; } // Найден пустой или удаленный элемент // 2.3. Получаем следующий элемент eCount++; if ( (eCount & Oxf) == 0) { // Загружаем новый сектор из корня if (!readDIR(fp, eCount)) return FAIL; } // 2.4. Выходим из цикла при достижении конца или ошибке if (eCount > mda->maxroot) return NOT_FOUND; // Достигнут последний элемент }// while return FAIL; } // newDIR Для поиска свободного кластера мы использовали функцию new г‘АТ (), чтобы разместить новый блок данных (новый файл). Unsigned newFAT(MFILE * fp) // fp - файловая структура // fp->ccls == 0, если должен быть размещен первый кластер, или // != 0 в случае дополнительного кластера // Возвращает TRUE/FAIL // fp->ccls - номер нового кластера { unsigned i, с = fp->ccls; // Последовательно просматриваем FAT в поисках пустого кластера do { с++; // Проверяем следующий кластер в FAT // Проверяем, достигнут ли последний кластер в FAT, // и начинаем просмотр сначала if (с >= fp->mda->maxcls) с = 0; // Проверяем, пройден ли полный круг (носитель заполнен) if (с == fp->ccls) { FError = FE_MEDIA_FULL; return FAIL;
Полет 293 } // Извлекаем значение i = readFAT(fp, с); } while (i!=0);. // Поиск пустого кластера // Помечаем кластер, как задействованный и последний в цепи writeFAT(fp, с, FATJEOF) ; // Если не первый кластер, то соединяем текущий кластер с новым if (fp->ccls >0) writeFAT(fp, fp->ccls, с); // Обновляем структуру MFILE fp->ccls = с; return TRUE; } // Размещение нового кластера При размещении нового кластера за первым, функция new FAT () соединяет’ кластеры в цепь и помечает каждый из них как задействованный. При этом исполь- зуются две вспомогательные функции: readFAT-() и writeFAT (). unsigned readFAT(MFILE *fp, unsigned eels) // fp - структура MFILE // eels - текущий кластер // Возвращает значение следующего кластера или // Oxffff, если ошибка или последний кластер { unsigned р, с; LBA 1; // Получаем адрес текущего кластера в FAT р = eels; // Кластер = Oxabcd // Упакован как: 0 I 1 | 2 | 3 | // word р 0 1 | 2 3 | 4 5 | 6 7 | . . // cd ab| cd ab| cd ab| cd ab| // Загрузка сектора FAT, содержащего кластер 1 = fp->mda->fat + (р >> 8 ) ; // 256 кластеров на сектор if (!readSECTOR(1, fp->buffer)) return Oxffff; // Сбой // Получаем значение следующего кластера с = ReadOddW (fp->buf fer, ( (р & 0xFF)«l)); return с; } // readFAT Функция writeFAT () обновляет содержимое таблицы FAT, поддерживая в ак- туальном состоянии все ее копии. unsigned writeFAT(MFILE *fp, unsigned cis, unsigned v) // fp - структура MFILE // cis - текущий кластер // v - следующее значение // Возвращает TRUE в случае успеха или FAIL в противном случае { unsigned р; LBA 1;
294 Глава 14. Файловый ввод-вывод // Получаем адрес текущего кластера в FAT р : = cis * 2; // Всегда четный // Кластер = Oxabcd // Упакован как: 0 | 1 | 2 | 3 | // word р 0 1 | 2 3 | 4 5 | 6 7 | // cd ab I cd ab | cd ab | cd ab | // Загружаем сектор FAT, содержащий кластер 1 = fp->mda->fat 4- (р » 9 ) ; р &= Oxlfe; if ( ’readSECTOR(1, fp->buffer) ) return FAIL; // Получаем значение следующего кластера fp->buffer[p] = v; // Младший байт fp->buffer[p+lJ = (v>>8); // Старший байт // Обновляем все копии FAT for (i=0; i<fp->mda->fatcopy; i++, 1 += fp->mda->fatsize) if (!writeSECTOR(1, fp->buffer)) return FAIL; return TRUE; } // writeFAT Наконец, функция writeDATAO была использована в функциях fwriteM () и fcloseM () для записи в запоминающее устройство фактических секторов дан- ных. Она вычисляет адрес сектора на основе номера текущего сектора. unsigned writeDAТА(MFILE *fp) { LBA 1; // Вычисляем LBA кластера/сектора 1 = fp->mda->data + (LBA)(fp->ccls-2) * fp->mda->sxc + fp->sec; return (writeSECTOR(1, fp->buffer)); } // writeDATA Тестирование завершенного модуля файлового ввода-вывода Настал момент протестировать функциональность целого модуля. Как и в пре- дыдущем тесте, мы воспользуемся модулем последовательного обмена данными conu2 . с, разработанным в главе 8, “Асинхронный обмен данными”, а также мо- дулем задержки, содержащим функцию delayms (). е Вышеупомянутые модули находятся на прилагаемом к книге компакт-диске в папках ПроектыХ 08 - СОМ иПроекты\Ве1ау. На этот раз после монтирования файловой системы мы откроем некоторый ис-. ходпый файл source. txt (это может быть также любой другой файл) и скопиру- ем его содержимое в новый “целевой” файл dest3. txt, созданный в незанятой области носителя. Ниже представлен код главного модуля тестовой программы WriteTest. с (не забудьте откорректировать в исходно коде ссылки на нуги в со- ответствии со своим размещением заголовочных файлов).
Полет 295 /* ** WriteTest.c */ #include <p24fj128ga010.h> tfinclude "SDMMC.h" ^include "fileio.h" #include "../delay/delay.h" tfinclude "../8 comm/conu2.h" #define B_S1ZE 1024 char data[B_SIZE]; int main(void) { MFILE *fs, *fd; unsigned r; // Инициализация initU2(); //115 200 бод 8,n,l putsU2("init"); while(!detectSD()); // Считаем, что вывод SDCD пол умолчанию - вход Delayms(100); // Ожидаем подачи питания на карту if (mount()) { putsU2("mount"); // "Смонтировано" if ((fs = fopenM("source.txt", "r"))) ( putsU2("source file opened for reading"); // "Исходный файл открыт // для чтения" if ((fd = fopenM("dest.txt", "w"))) { putsU2("destination file opened for writing"); // "Целевой файл // открыт для записи" do { г = freadM(data, B_SIZE, fs) ; г = fwriteM(data, r, fd); putU2 (’.’); } while(r==B_SIZE); fcloseM(fd); putsU2("destination file closed"); // "Целевой файл закрыт" } else putsU2("could not open the destination file"); // "Невозможно // открыть целевой файл" fcloseM(fs); putsU2("source file closed"); // "Исходный файл закрыт" } else putsU2("could not open the source file"); // "Невозможно открыть // исходный файл" unmount(); putsU2("unmount"); // "Размонтировано" } else putsU2("mount failed"); // "Ошибка монтирования"
296 Глава 14. Файловый ввод-вывод // Главный цикл while(1) ; } // main в Файл writeTest.c находится также на прилагаемом к книге компакт-диске в папке Проекты\ 14 - Файлы. Создайте новый проект WriteTest и добавьте в него все необходимые файлы: • sdmmc.c; • fileio.c; • conu2. с; • delay, с; • writetest.с; • все необходимые заголовочные файлы (. h). В очередной раз с помощью соответствующих контрольных списков настройте новый проект и отладчик ICD2, выделив немного больше места в куче для возмож- ности динамического размещения по крайней мере двух буферов и двух структур MFILE. ПРИМЕЧАНИЕ ________________ Если для глобальных переменных и стека выделено достаточно места, то нет смыс- ла экономить на размере кучи. Выделите под нее как можно больше пространства, чтобы функции mallocO и free() могли оптимизировать использование доступ- ной памяти. ..................................................................... После компоновки проекта и программирования выполняемого кода в плату Explorer 16 можно запустить тест. Если все было сделано верно, но спустя доли се- кунды (фактическое время зависит от размера выбранного исходного файла) на эк- ране терминала появятся следующие сообщения: init mount source file opened for reading destination file opened for writing destination file closed source file closed unmount Количество точек будет' пропорционально размеру файла. Поскольку для пашей демонстрационной программы мы выбрали размер буфера 1 024 байт, то каждой точке соответствует ровно 1 Кбайт переданных данных. Перенеся карту SD/MMC обратно на ПК, можно убедиться, что новый файл действительно создан (рис. 14.8). Его размер и содержимое идентичны исходному файлу, в то время как дата и время отражают значения, установленные в функции f closeM (). Попытка запустить тестовую программу во второй раз завершится ничем: init mount source file opened for reading could not open the destination file source file closed unmount
Разбор полота 297 Рис. 14.8. Содержимое карты в Проводнике Windows Причина в том, что во время разработки функции f орепМ () мы решили, что попытка открыть для записи уже существующий файл (в нашем случае dest.txt) должна приводить к ошибке. Можете перекомпилировать проект и запустить тест с другими размерами бу- фера данных (от одного байта до свободного пространства памяти микроконтролле- ра PIC24). Для выполнения запроса функции f readM () и fwri teM () позаботятся о чтении и записи требуемого количества секторов. Если что-то и будет меняться, то это — время, необходимое для завершения операции. Размер кода Размер кода, полученного для проекта Writ eTest, значительно больше, чем для простого модуля sdmmc.с, протестирован- ного в предыдущем уроке (рис. 14.9). И все же в отсутствие какой-либо опти- мизации код занимает всего-лишь 8 442 байт (2 814 слов * 3), что составляет только 6% от доступного пространства памяти программ микроконтроллера PIC24FJ128GA010. Счи- таю, что для такого объема функционально- сти это совсем немного! Разбор полета Рис. 14.9. Использование памяти модулем Wri teTest В этом уроке мы изучили основы файловой системы FAT 16 и разработали не- большой интерфейсный модуль, позволяющий микроконтроллеру PIC24 считывать и записывать данные при работе с запоминающими устройствами большой емкости. С помощью модуля sdmmc. с, разработанного в предыдущем уроке для низко- уровневого интерфейса, был создан базовый интерфейс файлового ввода-вывода для карт памяти SD/MMC. Теперь микроконтроллер PIC24 может использовать данные совместно с боль- шинством компьютерных систем, поддерживающих карты SD/MMC: от карманных компьютеров и ноутбуков до настольных ПК, работающих иод управлением опера- ционных систем DOS, Windows и Linux, и машин Apple под управлением OS-X.
298 Глава 14. Файловый ввод-вывод Советы и хитрости От разработчиков встроенных систем я нередко слышу вопрос: “Как организо- вать взаимодействие с USB-флэшкой для переноса данных между приложением и ПК?”. Ответ на этот вопрос очень прост: “Лучше этого не делать”. В подобных случаях предпочтительней использовать карты SD, поскольку взаимодействие с ни- ми гребует немного кода и только один порт SPI. В то же время интерфейс USB выглядит привлекательным и простым с точки зрения пользователя, однако процесс записи/чтения при работе с ним чрезвычайно сложен и требует разработки слишком объемного (по меркам встроенных систем) кода. Прежде всего, шипа USB гораздо сложнее интерфейса SPI, поскольку для нее требуется реализовать не просто периферийный интерфейс, а целый протокол Host USB со всеми вытекающими отсюда последствиями. Кроме того, придется значи- тельно расширить и аппаратную часть за счет специальных приемопередатчиков и больших модулей Serial Interface Engine (SIE). О том же, сколько для поддержки всего этого потребуется кода и оперативной памяти, не стоит даже и говорить. От- метим только, что чмсла получаются на несколько порядков больше, чем в случае с рассмотренным выше примером для карты SD/MMC. Упражнения 1. Добавьте следующую функциональность: • управление подкаталогами; • удаление файлов; • поддержка длинных имен. 2. Обеспечьте использование текущего времени и даты с помощью модуля RTCC. 3. Реализуйте кэширование (и/или использование отдельного буфера) для содер- жимого текущей записи FAT с целью повышения производительности операций записи/чтения. 4. Внесите модификации, необходимые для буферизации больших блоков и/или целых кластеров и выполнения многоблочных операций чтепия/записи с целью оптимизации производительности низкоуровневого взаимодействия с картой SD. Оцените достоинства и недостатки этого подхода. Ссылки • http: / /www. tldp. огд/LDP/ tlk/1Ik-title. html — интерактивная книга “The Linux Kernel” (“Ядро Linux”) Дэйвида Раслипга (David A. Rusling), описывающая внутреннее устройство операционной системы Linux и ее файло- вой системы. • http://en.wikipedia.org/wiki/File_Allocation_Table — пре- восходное описание истории и ответвлений технологии FAT. • http : //en. wikipedia. org/wiki/List_of_f ile__systems -— попытка классифицировать все основные используемые сегодня компьютерные системы.
ГЛАВА 1 5 Проигрыватель В этой главе: Использование модулей Output Compare в режиме ШИМ > Применение ШИМ для цифро-аналогового преобразования Формирование аналоговой волны & Воссоздание голосовых сообщений > Проигрыватель Файловый формат WAVE Функция play () > Низкоуровневые аудио-подпрограммы Тестирование проигрывателя файлов WAVE & Оптимизация файлового ввода-вывода Профилирование светодиодов Чтобы узнать больше, заглянем под капот Последний полет — это экзамен с представителем Федерального авиационного агентства, время огромного напряжения и переживаний. Ты должен показать все, чему научился в летной школе. По па самом деле особо переживать не нужно. Все упражнения еще свежи в памяти, и экзамен закапчивается настолько быстро, что ты даже не успеваешь как следует осознать происходящее. Подобным же образом последняя глава построена на множестве фрагментов, разработанных в предыдущих уроках, и сообща они формируют нечто практичное и интересное. Если же говорить конкретно, то речь пойдет о проигрывателе звуко- вых файлов. План полета В этом уроке мы исследуем возможность получения звуковых сигналов с по- мощью уже знакомых нам модулей Output Compare микроконтроллера PIC24. В ре- жиме широтно-импульсной модуляции (ШИМ) в комбинации с фильтрами нижних частот (ФНЧ) эти модули можно эффективно использовать в качестве цифро-анало- говых преобразователей (ЦАП) для получения аналогового выходного сигнала. Ес- ли нам удастся модулировать аналоговый сигнал частотами в диапазоне, восприни- маемом человеческим ухом (примерно от 20 Гц до 20 кГц), то мы получим звук!
300 Глава 15. Проигрыватель Полет Принцип ШИМ довольно прост. Импульс формируется через регулярные ин- тервалы времени (7), которые обычно получают с помощью таймера и его регистра периода. При этом ширина импульса (?„„) не фиксированная, а программируемая и может варьироваться от 0 до 100% периода таймера. Соотношение между шири- ной импульса Топ и периодом сигнала Т называют коэффициентом заполнения (рис. 15.1). 50% заполнения Т0ПД- 1/2 10% заполнения ТОПД=1/10 Рис. 15.1. Пример ШИМ-сигналов с разными коэффициентами заполнения Два предельных случая для коэффициента заполнения — это 0% и 100%. Пер- вый из них соответствует постоянно отсутствующему, а второй — постоянно при- сутствующему сигналу. Количество промежуточных вариантов обычно относитель- но невелико и представляет собой логарифм по основанию 2: значение, о котором обычно говорят как о разрешении ШИМ. Например, для 256 вариантов ширины им- пульсов можно сказать, что используется ШИМ-сигнал с разрешение 8 бит. Если идеальный ШИМ-сигпал с фиксировашшм коэффициентом заполнения подать на спектральный анализатор для изучения его компонентов, то окажется, что он состоит из трех частей (рис. 15.2): • компонент постоянного тока с амплитудой, прямо пропорциональной коэффи- циенту заполнения; • синусоида с базовой частотой (f = 1/7); • бесконечное число гармоник, частота которых кратна базовой (2/, 3/ 4/ 5/, 6/..). Рис. 15.2. Частотный спектр ШИМ-сигнала
Полет 301 Таким образом, если нам удастся прикрепить “идеальный” фильтр нижних час- тот к выходу генератора ШИМ-сигнала для удаления всех частот от базовой и вы- ше, то мы получим чистый аналоговый сигнал постоянного тока с амплитудой, пря- мо пропорциональной коэффициенту заполнения (рис. 15.3). 50% заполнения Топ/Т=1/2 Аналоговый выход = 0.5 Рис. 15.3. Аналоговый сигнал на выходе схемы ШИМ с идеальным ФНЧ Конечно же, подобных идеальных фильтров не существует, однако мы можем воспользоваться его более-менее точной аппроксимацией для устранения достаточ- ной доли нежелательных частотных компонентов. В качестве такого ФНЧ (первого порядка) послужит простая пассивная RC-цепь или несколько (7V) активных каска- дов фильтрации (порядок 2 х N). Если нам необходимо получить звуковой сигнал, то, аккуратно подобрав часто- ту ШИМ, мы можем извлечь выгоду из естественных ограничений человеческого уха, дополнительно фильтрующего любой сигнал с частой за пределами диапазона 20..20 000 Гц. Кроме того, большинство аудиоусилителей, на которые мы подадим выходной сигнал, также реализуют подобную фильтрацию на входных каскадах. Другими словами, если мы обеспечим частоту ШИМ-сигнала не ниже 20 кГц, то оба вышеупомянутых явления позволят нам использовать более простую и менее доро- гостоящую схему фильтрации. Самой собой, поскольку мы можем изменять коэффициент заполнения только один раз в течение каждого периода ШИМ (Г), то с повышением частоты ШИМ увеличивается скорость изменения выходного аналогового сигнала, а значит и до- пустимая частота формируемого звукового сигнала. С практической точки зрения это означает, что самый высокий звук, который может выдать ШИМ, составляет только половину частоты ШИМ. Например, ШИМ-схема с частотой 20 кГц сможет воспроизвести только звук частотой 10 кГц, а для охвата полного слышимого спек- тра нам потребуется базовая частота минимум 40 кГц. Не случайно музыкальные компакт-диски кодируют па частоте 44 100 отсчетов в секунду. Использование модулей Output Compare в режиме ШИМ В предыдущем уроке мы уже использовали модули Output Compare микрокон- троллера PIC24 (схему см. на рис. 12.8) для получения точных времепньгх иптерва-
302 Глава 15. Проигрыватель лов (для формирования видеосигнала). На этот раз мы применим их в режиме ШИМ для создания потока регулярных импульсов с требуемым коэффициентом заполне- ния. Все что необходимо для инициализации модуля ОС на формирование ШИМ- сигнала, — это записать значение 0x110 в зри разряда ОСМ управляющего регистра OCxCON (см. рис. 12.9). Существует еще и второй режим ШИМ (0x111), однако мы не сможем воспользоваться входами OCFA и OCFB, которые обычно требуются различным приложениям в качестве защитного механизма (управление двигателем, преобразование мощности). Далее необходимо выбрать таймер для формирования периода ШИМ. Выбор ограничен модулями Timer2 и Timer3, что в данном случае для нас равнозначно. Самое главное — каким образом мы сконфигурируем выбранный таймер. Помня о том, что нам необходимо получить частоту ШИМ хотя бы 40 кГц, и предполагая, что частота тактирования периферии составляет 16 МГц (в случае платы Explorer 16), мы можем рассчитать оптимальные значения для нредделителя и регистра периода PRx. Если коэффициент нредделителя составляет’ 1:1, то мы по- лучаем период в 400 циклов, что составляет ровно 40 кГц. Это значение также дик- тует разрешение заполнения для модуля Output Compare. Поскольку мы получили 400 возможных значений коэффициента заполнения, то разрешение лежит в проме- жутке 8-9 бит, поскольку у нас есть более 256 (28), но менее 512 (29) шагов. Пони- жение частоты до 20 кГц даст еще один бит разрешения, но также ограничит мак- симальную выходную частоту значением 10 кГц. Хотя разница для человеческого уха будет не так и велика, но все же заметна. Как только выбранный таймер сконфигурирован, непосредственно перед запи- сью в регистр OCxCON необходимо сохранить значение первого рабочего цикла в регистры OCxR и OCxRS. В режиме ШИМ эти два регистра работают в конфигу- рации “главный/подчипепный”. Как только модуль ШИМ активизируется (путем установки соответствующих разрядов в регистре OCxCON), мы сможем изменять коэффициент заполнения, записывая данные только в регистр OCxRS. Регистр OCxR будет обновляться копией нового значения из регисгра OCxRS только в начале каж- дого периода. Это позволит избежать различных проблем и даст целый период Т па подготовку значения следующего коэффициента заполнения. Рассмотрим пример простой подпрограммы инициализации для модуля ОС1: void initAudio(void) { // Инициализируем TMR3 для получения временного базиса T3CON = 0x8000; // Активизируем TMR3, предделитель 1:1, // внутреннее тактирование PR3 = 400-1; // Устанавливаем период для заданной частоты // дискретизации _T3IF = 0; // Сбрасываем флаг прерывания _T3IE = 1; // Разрешаем прерывание от TMR3 // Инициализация ШИМ // Устанавливаем исходные коэффициенты заполнения (главный // и подчиненный) OC1R = OC1RS = 200; // На 50% // Активизируем модуль ШИМ OC1CON = ОхОООЕ; } // initAudio
Полет 303 Обратите внимание, что мы разрешили прерывание от таймера Timer3. Это по- зволит нам отслеживать начало каждого нового периода, и мы сможем решить, ка- ким образом обновлять значение коэффициента заполнения в регистре OC1RS. Применение ШИМ для цифро-аналогового преобразования Для того чтобы приступить к экспери- ментам на плате Explorer 16, па область ма- кетирования необходимо добавить пару дискрегных компонентов: простейший ФНЧ, состоящий из резистора номиналом 1 кОм и конденсатора емкостью 100 нФ (первый порядок с граничной частотой 1,5 кГц). Со- единим их последовательно и подключим к выходу модуля ОС1 (вывод 0 порта D) (рис. 15.4). Иаш короткий тестовый проект завер- шается несколькими строками кода: R1 1k IRD0 PCI >—\ЛЛг—----<Test | Cl 100nF “ GND Puc. 15.4. Использование ШИМ-сигнала для формирования аналогового сигнала void -ISRFAST _T3Interrupt(void) I // Сбрасываем флаг прерывания и выходим _T3IF = 0; } // Прерывание от ТЗ main(void) ( initAudio(); // Главный цикл while(1); }// main .Добавив обычный заголовок и ссылку на включаемый файл, сохраните код в новом файле ТеsIDA. с. Затем создайте и скомпонуйте тестовый проект с этим файлом и с помощью отладчика ICD2 запрограммируйте плату Explorer 16. ©Файл TestDA.c находится также на прилагаемом к книге компакт-диске в папке проекты\15 - Проигрыватель. Подключив к контрольной точке измерительный прибор или осциллоскоп, за- пустите программу и проверьте средний выходной сигнал постоянного тока. Стрел- ка измерителя или осциллофамма покажет уровень около 1,5 В, что составляет 50% обычного выходного напряжения цифрового вывода платы Explorer 16. Эхо значение соответствует коэффициенту 200 (для периода в 400 циклов, заданного подпро- граммой инициализации). При наличии осциллоскопа пробник можно перенести на другую сторону резистора R1 (непосредственно на выход модуля ОС1) и удостове- риться в наличии прямоугольных колебаний с частотой ровно 40 кГц и коэффици- ентом заполнения 50%. Теперь можете изменить подпрограмму инициализации, чтобы поэксперимен- тировать с другими значениями коэффициента от 0 до 399 и проверить реакцию схемы и пропорции выходного сигнала в диапазоне 0..3 В.
304 Глава 15. Проигрыватель Формирование аналоговой волны С помощью модуля ОС1 мы только что пересекли границу между цифровым миром, сотканным из нулей и единиц, и аналоговым миром, в котором можно полу- чить множество значений в диапазоне от 0 до 3 В. Поэкспериментируем с коэффи- циентом заполнения, изменяя его от периода к периоду для получения разнообраз- ных волновых форм. Начнем с того, что немного модифицируем проект, добавив немного кода в подпрограмму обслуживания прерывания, которая до этого момента была пустой: OC1RS = (count < 20) 400 : 0; count++; if (count >= 40) count = 0; Целочисленную переменную count необходимо объявить как глобальную, не забыв инициализировать ее нулем. Сохраните и скомпонуйте проект, чтобы протес- тировать новый код на плате Explorer 16. еФайл TestDA2 .с находится также на прилагаемом к книге компакт-диске в папке Проекты\15 - Проигрыватель. Каждые 20 периодов ШИМ па выходе фильтра сигнал будет изменяться от уровня 3 В (100%) до уровня 0 В (0%), формируя прямоугольную волну частотой 1 кГц (40 кГц/40). Более интересный сигнал можно получить с помощью следующего алгоритма: OC1RS = count*10; count++; if (count >= 40) count = 0; Этот код дает треугольную (пилообразную) волну с пиком амплитуды пример- но 3 В и постепенным нарастанием коэффициента заполнения от 0 до 100% за 40 тагов (2,5% каждый), после чего следует резкий спад по заднему фронту до 0 В, и все повторяется сначала. Частота этого сигнала также составляет 1 кГц. Если любой из этих двух сигналов подать на аудиоусилитель, ни один из них не даст нормального звука, хотя они и создадут различимый высокий тон. Из-за мно- жества гармоник в звуковом спектре получится неприятное жужжание. Для получе- ния чистого тона необходима правильная синусоида. Для этого послужит представ- ленная ниже подпрограмма обслуживания прерывания. Опа дает идеальную сину- соиду с частотой 400 Гц (с точки зрения музыки, это примерно соответствует ноте “ля”). void _ISRFAST _T3lnterrupt (void) { OC1RS = 200 + (200 * sin (count * 0.0628)); count++; if (count >= 40) count = 0; // Сбрасываем флаг прерывания и выходим _T3IF = 0; } // Прерывание от ТЗ К сожалению, несмотря на всю производительность микроконтроллеров PIC24 и математических библиотек компилятора СЗО, мы никак не сможем использовать функцию sin (), выполнить необходимые умножения и сложения и получить новое
Полет 305 значение коэффициента заполнения на требуемой частоте 400 Гц. Прерывание от таймера Timer3 возникает каждые 25 мкс, что слишком мало для сложных вычисле- ний с вещественными числами. В результате подпрограмма будет “пропускать” прерывания, что даст синусоиду с частотой в два раза ниже ожидаемой (т.е. звук на октаву ниже). И все же, если полученный сигнал подать на аудиоусилитель, то ста- нет очевидно, что звук теперь гораздо чище. Для получения более высоких частот значения синусоиды потребуется свести в таблицу, чтобы во время выполнения программы получить минимально возмож- ное количество вычислений (желательно только над целыми числами). Ниже пока- зан пример, использующий такую таблицу, хранимую во флэш-памяти программ микроконтроллера PIC24. Табличные значения были получены с помощью отдель- ной электронной таблицы и следующей формулы: = Смещение 4- I NT (Амплитуда * SIN (ROW * 6.28/ Период)) Для периода в 100 выборок (400 Гц), а также смещения и амплитуды, равных 200, получаем: = 200 4 INT(200*SIN(Al *6.28/100)) Я заполнил первый столбец (А) электронной таблицы счетчиком, скопировал формулу в первые 100 строк второго столбца (В) и отформатировал результат без десятичных знаков (рис. 15.5). Рис. 15.5. Электронная таблица для расчета синусоиды частотой 400 Гц Затем я через буфер обмена вставил целый столбец в исходный код и добавил в конце каждой строки запятые, чтобы согласовать набор значений с синтаксисом языка С. void _ISRFAST _T3Interrupt(void) { OC1RS = Table[count]; count 4-4-; if (count >= 40)
306 Глава 15. Проигрыватель count = 0; // Сбрасываем флаг прерывания и выходим _T3IF = 0; } // Прерывание от ТЗ const int Table[100] = { 200, 212, ’ 225, 237, 249, 149, 161, 174, 186, 199 }; Иа этот раз получить требуемую выходную частоту 400 Гц не составит труда, а между вызовами прерывания от таймера Timer3 будет достаточно времени для решения других задач. Воссоздание голосовых сообщений Итак, мы знаем, как получить звук, и нас уже ничто не остановит. Эти знания можно применить в бесчисленном количестве встроенных приложений. Звуковая обратная связь будет нелишней в любом “человеческом” интерфейсе. С ее помощью можно привлекать внимание пользователя к каким-либо предупреждениям и сооб- щениям об ошибках или же просто расширить возможности системы. При этом обычные тоны и простейшие мелодии — далеко не предел. Мы можем воспроизве- сти фактически любой звук, если для него существует описание волновой формы. Как и в предыдущем примере с синусоидой, ничто не мешает воспользоваться большой таблицей, содержащей точные значения амплитуды, соответствующие ка- кому-либо инструменту или даже человеческому голосу. Единственно, что нас ог- раничивает, — это свободное пространство во флэш-памяти программ микрокон- троллера PIC24 для храпения таблицы. Если нам необходимо сохранять голосовые сообщения, то требования к выход- ной частоте значительно упрощаются, а частоту дискретизации ШИМ можно уменьшить всего лишь до 8 000 выборок в секунду, поскольку известно, что энергия человеческого голоса сосредоточена преимущественно в диапазоне 400..4 000 Гц. При этом мы по-прежнему можем поддерживать высокую частоту ШИМ, что по- зволяет удерживать гармоники ШИМ-сигнала вне звукового диапазона частот, а ФНЧ остается простым и недорогим. Потребуется уменьшить только частоту из- менения коэффициента заполнения и считывания новых данных из таблицы (в на- шем случае — один раз каждые пять прерываний (40 000 / 8 000 = 5). Имея в распо- ряжении 8 000 выборок в секунду, мы могли бы теоретически воспроизвести целых 16 секунд голосовых сообщений, хранимых во флэш-памяти микроконтроллера. Для решения, реализованного на одном кристалле, это — немало. Для увеличения вре- мени звучания (приблизительно удвоения) можно воспользоваться простой методи- кой сжатия, применяемой в голосовых приложениях, наподобие ADPCM (Adaptive Differential Pulse Code Modulation — адаптивная дифференциальная импульсно- кодовая модуляция). Эта методика основана на предположении, что разница между двумя последовательными выборками меньше абсолютного значения каждой вы-
Полет 307 борки, а значит ее можно закодировать с помощью меньшего количества бит. Фак- тическое количество битов изменяется динамически таким образом, чтобы избежать искажения сигнала и в то же время получить требуемый коэффициент сжатия (от- сюда и термин “адаптивная”). Проигрыватель Оставшаяся часть этого урока посвящена амбициозному проекту, в котором по- требуются все библиотеки и умения, приобретенные за время изучения последних нескольких глав. Мы попытаемся создать базовое мультимедиа-приложения, спо- собное воспроизводить музыкальные стереофайлы с карты памяти SD™/MMC. В качестве формата будет использован WAVE без сжатия, совместимый почти с лю- бой аудиопрограммой. Кроме того, данный формат выбран по умолчанию для из- влечения файлов с музыкальных компакт-дисков. Для начала, воспользовавшись привычным контрольным списком, создайте но- вый проект и сразу же добавьте в него низкоуровневый интерфейс SD/MMC и биб- лиотеку файлового ввода-вывода для доступа к файловой системе FAT 16. На этот раз после открытия файла на чтение нам потребуется каким-то образом распознать кодировку содержащихся в нем данных. Файловый формат WAVE Файлы с расширением . wav, кодированные в формате WAVE, — одни из наи- более простых и хорошо документированных, но все равно требуют тщательного изучения. Формат WAVE — это вариант файлового формата RIFF, который являет- ся стандартом для некоторых операционных систем, использующих методику со- хранения нескольких фрагментов данных путем их разбиения на “порции” (блоки с заголовком, состоящим из двух 32-разрядпых элементов: идентификатора и раз- мера порции) (табл. 15.1). Таблица 15.1. Формат “порции” данных Смещение Размер Значение Описание 0x00 4 ASCII Идентификатор порции 0x04 4 Размер Размер содержимого порции 0x08 Размер Данные 0х08+Размер 1 0x00 Необязательное дополнение пробелами Учтите, что общий размер порции должен быть кратен двум, чтобы все данные в файле RIFF были выровнены в точности по словам. Если это условие не выполня- ется, то к порции добавляется дополнительный байт. Файл . wav всегда начинается с порции с идентификатором “RIFF”, блок дан- ных которой начинается с четырехбайтпого поля “типа”. Это поле должно содер- жать строку “WAVE”. Порции можно вкладывать друг в друга, как матрешки, и внузри порции заданного типа может находиться несколько подчиненных порций. Структуру порции RIFF файла . wav иллюстрирует табл. 15.2. Таблица 15.2.Порция “RIFF”типа “WAVE” Смещение Размер Значение Описание 0x00 4 “RIFF" Идентификатор порции 0x04 4 Размер Размер блока данных + 4 0x08 4 “WAVE” Идентификатор типа 0x10 Размер Блок данных (подчиненные порции)
308 Глава 15. Проигрыватель Блок данных в свою очередь содержит пор- цию “fmt”, после которой следует порция “data”. Как часто бывает, лучше один раз увидеть, чем сто раз услышать (рис. 15.6). Порция “fmt” содержит определенную по- следовательность параметров, полностью опи- сывающих поток выборок в следующее далее порции “data” (табл. 15.3). В промежутке между порциями “lint” и “da- ta” могуг находиться другие порции, содержа- щие дополнительную информацию о файле. Та- ким образом, в поисках требуемой порции мо- жет потребоваться последовательно просмотреть список идентификаторов порций. Идентификатор порции “BIFF" Размер данных порции Тип порции “WAVE" Идентификатор порции "бпГ Размер данных порции Информация о выборке Идентификатор порции "data" Размер данных порции Звуковые выборкиё Рис. 15.6. Базовая структура файла И/Д VE Таблица 15.3. Содержимое порции “fmt” Смещение Размер Описание Значение 0x00 4 Идентификатор порции "fmt” 0x04 4 Размер данных порции 16 + дополнительные байты форматирования 0x08 2 Код сжатия Беззнаковое целое 0x0a 2 Количество каналов Беззнаковое целое 0x0c 4 Частота дискретизации Беззнаковое длинное целое 0x10 4 В среднем бай” в секунду Беззнаковое длинное целое 0x14 2 Выравнивание блока Беззнаковое целое 0x16 2 Значащих разрядов на выборку Беззнаковое целое (>1) 0x18 2 Дополнительные байты форматирования Беззнаковое целое Функция play () Создадим новый программный модуль, который будет отвечать за открытие за- данного файла .wav и после фиксации и декодирования информации в порции “fmt” — инициализировать звуковой вывод. Назовем этот модуль wave . с. /*---------------------------------------------------------------------- * * Wave.С * * * * Проигрыватель файлов Wave * * Использует два восьмиразрядных канала ШИМ */ #include <stdlib.h> #include "../Audio/Audio PWM.h" flinclude "../sdmmc/sdmmc.h" ^include "../sdmmc/fileio.h" // Определения идентификаторов порций fldefine RIFF_DWORD 0x46464952UL fldefine WAVE_DWORD 0x45564157UL
Полет 309 ildefine DATA_DWORD 0x61746164UL #define FMT_DWORD 0x20746d66UL typedef struct { // Порция "data" unsigned long dlength; // Фактический объем данных char data[ 4 J ; // "data" // Порция "fmt" unsigned bitpsample; unsigned bpsample; // Байт на выборку (4 = 16-бит, стерео) unsigned long bps; // Байт в секунду unsigned long srate; // Частота дискретизации в Гц unsigned channels; // Число каналов (1 = моно, 2 = стерео) unsigned subtype; // Всегда 01 unsigned long flength; // Размер вложенного блока (16) char fmt_[4]; // "fmt_" char type[4]; // Название типа файла "WAVE" unsigned long tlength; // Размер вложенного блока char riff[4]; // Оболочка "RIFF" } WAVE; Структура WAVE понадобится для сбора всех параметров “fmt” С помощью макросов различные уникальные идентификаторы порций распознаются как длин- ные целые, что позволяет выполнять эффективное сравнение. Переходим к разработке функции play(), которой требуется только один пара- метр: имя файла. unsigned play(const char *name) { int i; WAVE wav; MFILE *f; unsigned wi; unsigned long 1c, r, d; int skip, size, stereo, fix, pos; // 1. Открываем файл if ((f = fopenM(name, "r")) == NULL) { // Сбой при открытии return FALSE; } Попытавшись открыть файл, мы в случае успеха сразу же приступаем к поиску внутри буфера данных идентификатора порции “RIFF” и идентификатора типа “WAVE”, указывающих на корректность формата файла: // 2. Проверяем, соответствует ли файл формату RIFF if (ReadL(f->buffer, 0) != RIFF_DWORD) { fclose(f); return FALSE; // 3. Ищем метку типа WAVE if ((d = ReadL(f->buffer, 8)) != WAVE_DWORD) { fclose ( f); return FALSE;
310 Глава 15. Проигрыватель В случае успеха следует удостовериться, что порция “fint” — первая внутри блока данных. Затем извлекается вся информация, необходимая для обработки бло- ка данных в процессе воспроизведения: // 4. Ищем порцию, содержащую данные в формате WAVE if (ReadL(f->buffer, 12) != FMT_DWORD) return FALSE; wav.channels = ReadW(f->buffer, 22); wav.bitpsample = ReadW(f->buffer, 34); wav.srate = ReadL(f->buffer, 24); wav.bps = ReadL(f->buffer, 28); wav.bpsample = ReadW(f->buffer, 32); Далее приступаем к поиску порции “data”. Для этого мы проверяем ноля иден- тификаторов в блоке данных, следующем после окончания порции “fint”, и в случае несоответствия пропускаем целый блок: // 5. Поиск порции данных wi = 20 + ReadW(f->buffer, 16); while (wi < 512) { if (ReadL(f->buffer, wi) == DATA_DWORD) break; wi += 8 + ReadW(f->buffer, wi+4); } if (wi >= 512) // He можем найти порцию данных в текущем секторе { fclose (f); return FALSE; } Если в процессе поиска мы исчерпаем содержимое буфера данных, то это ука- зывает па наличие какой-то проблемы. Обычно файлы . wav, полученные путем из- влечения данных с музыкального компакт-диска, содержат только порцию “data”, за которой сразу же следует порция “fint”. Другие приложения (например, MIDI- интерфейсы) могут создавать файлы .wav с более сложной структурой, включая несколько порций “data”, “playlists.” (списки воспроизведения), “cues.” (ключи), “labels” (метки) и т.д., по пас интересует только воспроизведение “чистых” файлов . wav. Как только порция “data” обнаружена, ее размер укажет нам на фактическое ко- личество содержащихся в файле выборок: // 5.1. Находим размер данных (фактической информации о волне) wav.dlength = ReadL(f->buffer, wi+4); Теперь следует определить частоту дискретизации, чтобы выяснить, возможно ли воспроизведение с такой скоростью. Если опа превосходит наши возможности, то придется пропускать выборки для снижения частоты чтения данных. В качестве предельной мы воспользуемся частотой 48 Квыборок/соиг, поэтому сможем читать данные достаточно быстро для реализации как минимум восьмиразрядного разре- шения. // 6. Вычисляем период и корректируем частоту дискретизации г = wav.bps / wav.bpsample; // г = выборки в секунду skip = wav.bpsample; // Шаг для сужения // полосы пропускания (стерео) while (г > 48000)
Полот 311 I г »= 1; skip <<= 1; } // Поскольку мы делим частоту на два, // Умножаем шаг на два Более высокой скорости модно достичь постепенным делением частоты на два и удвоения шага. Далее можно вычислить требуемый период ШИМ (записывается в регистр PRx). Проблема возникает в том случае, если это значение превышает доступную разрядность регистра (16 бит), т.е. число 65 536. // 6.1. Проверяем, совместима ли частота дискретизации // с коэффициентом 1:1 предделителя таймера TMR3 d = (16000000L/r)-1; if (d > (65536L)) // Максимальный период TMR3 (16 бит) ( fclose (f); return FALSE; } В ходе воспроизведения мы будем отслеживать количество извлеченных из файла выборок (по целочисленной переменной 1с) для определения окончания файла. // 7. Начинаем загрузку буферов // Определяем количество байтов, составляющих порцию данных WAVE 1с = wav.dlength; Обратите внимание, что до сих не задействована функция freadM (). Мы за- глядываем внутрь файлового буфера обходным путем, зная, что функция fopenMO уже загружена. Во избежание рывков в воспроизведении мы воспользуемся схемой двойной буферизации (рис. 15.7). Когда подпрограмма обслуживания прерывания извлекает данные из одного буфера, второй буфер заполняется новыми данными из файла. MFILE Г Рис. 15.7. Двойная буферизация Массив ABuf fer определен как два блока по B_SIZE байт каждый. Значение B SIZE должно быть кратно 512, чтобы вызовы функции freadM () могли пере- давать за раз целые секторы данных (это максимизируег производительность). Не- обходимо удостовериться, что время, требуемое функции freadM () для заполне- ния одного буфера, меньше, чем время воспроизведения (использования) подпро- граммой обслуживания прерывания от модуля ШИМ всех данных из второго буфе- ра. При запуске схемы с двойной буферизацией заполняются оба буфера:
312 Глава 15. Проигрыватель // 8. Предварительная загрузка обоих буферов г = fread(ABuffer[0], B_SIZE*2, f); AEmptyFlag = FALSE; lc-= B_SIZE*2 ; // Предполагаем, что 1c >= B_SIZE*2!!! Здесь мы предполагаем, что файл . wav содержит достаточно данных для за- полнения двух буферов. Если планируется использовать очень короткие файлы раз- мером менее нескольких килобайт, то представленный выше код можно модифици- ровать. Выясните количество байтов, возвращаемых функцией freadMQ, и до- полните буферы корректными заполняющими данными. Теперь мы готовы инициализировать “машину” воспроизведения звука, пред- ставляющую собой обычную функцию T3Interrupt (), модифицированную под два стереоканала. Также необходимо реализовать возможность пропуска выборок для уменьшения частоты дискретизации, а также — возможность обработки как 16- (со знаком), так и восьмиразрядных (без знака) выборок. Вся эта информация пере- дается в подпрограмму звукового модуля in it Audi о () в виде короткого списка параметров: // 9. Начало воспроизведения, разрешение прерываний initAudio(wav.srate, skip, size, stereo, fix, pos); При активизации прерываний от таймера подпрограмму сразу же начинает вы- бирать данные из первого буфера. Как только содержимое этого буфера будет ис- черпано, она установит флаг AEmptyFlag, чтобы известить о необходимости из- влечь из файла .wav новые данные, а активным станет второй буфер. Таким обра- зом, реализация плавного воспроизведения заключается в цикличной проверке фла- га AEmptyFlag в готовности заполнить буфер новыми данными и подсчете счи- танных из файла байтов. // 10. Заполняем буферы в цикле while (1с >= B_SIZE) { if (AEmptyFlag) { г = fread(ABuffer[1-CurBuf], B_SIZE, f); AEmptyFlag = FALSE; 1c -= B—SIZE; } } // Пока доступны данные wav В действительно мы должны остановиться немного раньше, кода оставшихся в файле данных уже недостаточно для полного заполнения буфера (если только размер блока данных не был в точности кратен размеру буфера). В таком случае в буфер загружается и дополняется последний фрагмент. // 11. Заполняем буферы остаточными данными if (1с>0) { // Загружаем последний сектор г = fread(ABuffer[1-CurBuf], 1c, f); last = ABuffer [ 1-CurBuf] [r-1]; while((r<B_SIZE) && (last>0)) ABuffer[1-CurBuf][r++] = last; // Ожидаем опустошения текущего буфера AEmptyFlag = 0; while (!AEmptyFlag);
Полет 313 Затем мы ожидаем опустошения самого последнего буфера, после чего сразу же прерываем воспроизведение звукового файла. // 12. Опустошаем последний буфер AEmptyFlag = 0; while (!AEmptyFlag); // 13. Останавливаем воспроизведение haltAudio(); Напоследок мы закрываем файл, высвобождая при этом память, и возвращаемся в вызывающее приложение: // 14. Закрываем файл fclose (f) ; // 15. Успешное завершение операции return TRUE; } // play Для окончания разработки модуля необходимо создать небольшой заголовоч- ный файл wave . h для публикации прототипа функции play (): ............................................................................ ★ ★ Wave.Н ★ * * ★ Проигрыватель файлов Wave ** Использует два восьмибитных канала ШИМ ★ * */ unsigned play(const char *name); e Файлы wave. с и Wave .h находятся также на прилагаемом к книге компакт-диске в папке Проек - ты\15 - Проигрыватель. Низкоуровневые аудио-подпрограммы Только что созданная функция play () для фактической инициализации пери- ферии таймера и модуля Output Compare, а также — периодического обновления коэффициента заполнения ШИМ в значительной мере опирается па низкоуровне- вый аудиомодуль, который мы назовем Audio PWM.c. Этот модуль, в основном, использует' код, разработанный в начале главы, и реализует два стереоканала и ряд дополнительных возможностей. еФайл Audio pwm.c находится на прилагаемом к книге компакт-диске в папке проекты\15 - Проигрыватель\Audio. Модули ОС1 и ОС2 используются одновременно для обработки левого и право- го каналов. Настоящее ядро функциональности воспроизведения — это подпро- грамма обслуживания прерывания от таймера. Позицию внутри каждого буфера от- слеживает указатель BPtr. void _ISRFAST _T3Interrupt(void) // 1. Загружаем новые выборки для следующего цикла OC1RS = 30 + (*BPtr А Fix); if (Stereo)
314 Глава 15. Проигрыватель OC2RS = 30 + (*(BPtr + Size) Л Fix); else // Моно OC2RS = OC1RS; Указатель смещается па количество байт, зависящее от размера выборок (16 или 8 бит каждая), а также — от необходимости пропускать выборки для уменьше- ния частоты дискретизации: // 2. Пропускаем выборки для уменьшения частоты дискретизации BPtr += Skip; Как только использованы все данные буфера, необходимо сменить активный буфер: // 3. Проверяем, пуст ли буфер if (--BCount == 0) { // 3.1. Меняем буферы местами CurBuf = 1 - CurBuf; BPtr = ABuffer[CurBuf]; Далее мы инициализируем счетчик выборок и устанавливаем флаг, чтобы из- вестить функцию play () о необходимости подготовить новый буфер: // 3.2. Инициализируем счетчик BCount = B_SIZE/Size; // 3.3. Извещаем о необходимости заполнения нового буфера AEmptyFlag = 1; } Только после этого можно выйти из подпрограммы, сбросив перед этим флаг прерывания: // 4. Сбрасываем флаг прерывания и выходим _T3IF = 0; } // ТЗ Interrupt Подпрограмма инициализации также довольно проста. В нее из вызывающего приложения предается несколько перемешшх, которые копируются во внутренние (закрытые) переменные модуля: void initAudio(long bitrate, int skip, int size, int stereo, int fix, int pos) { // 1. Инициализируем указатели CurBuf =0; // Вначале активизируем буфер 0 BPtr = ABuffer[CurBuf]+pos; BCount = (B_SIZE-pos)/size; // Количество воспроизводимых выборок AEmptyFlag = 0; Skip = skip; Fix = fix; Stereo = stereo; Size = size; Здесь один из буферов выбирается в качестве “текущего”, а также инициализи- руются все указатели и счетчики. Далее необходимо инициализировать таймер и его механизм прерываний: // 2. Инициализируем таймер T3CON = 0x8000; // Таймер TMR3, предделитель 1:1, // внутреннее тактирование
Полет 315 PR3 = FCY / bitrate; // Задаем период для заданной частоты Offset = PR3/2; // дискретизации _T3IF = 0; // Сбрасываем флаг прерывания _T3IE = 1; // Разрешаем прерывания от таймера TMR3 Далее инициализируются коэффициенты заполнения но начальному смещению, составляющему половину периода, чтобы обеспечить выходной уровень 50%. // 3. Устанавливаем исходные коэффициенты заполнения OC1R = OC1RS = Offset; // Левый канал OC2R = OC2RS = Offset; // Правый канал В завершение активизируем модули Output Compare: // 4. Активизируем модули ШИМ OC1CON = ОхОООЕ; // Каналы СН1 и СН2 в режиме ШИМ, // на основе таймера TMR3 OC2CON = ОхОООЕ; } // initAudio Функция haltAudio (), вызываемая в конце воспроизведения, пожалуй, — самая простая из всех. Ее единственная задача — отключить таймер Timer3, что приведет к останову модулей Output Compare, а значит — и всего механизма преры- ваний: void haltAudio(void) { T3IE = 0; // Отключаем прерывания от TMR3 } // Останов звучания Для завершения модуля необходим обычный заголовок, ссылки на включаемые файлы и определения глобальных переменных, включая аудиобуферы. /* * * Audio PWM demo ★ * * / # include <p24fj128ga010.h> # include "AudioPWM.h" # define _FAR _attribute__((far)) // Глобальные определения unsigned Offset; // Заполнение 50% char _FAR ABuffer[2][B_SIZE]; // Двойной буфер данных int CurBuf; // Индекс активного буфера volatile int AEmptyFlag; // Внутренние переменные int Stereo; int Fix; int Skip; int Size; // Локальные определения unsigned char *BPtr; int BCount; // Флаг, указывающий на то, что буфер пуст // Извещает о стерео-воспроизвеДении // Фиксатор знака для 16-битных выборок // Шаг для уменьшения частоты дискретизации // Размер выборки (8 или 16 бит) // Указатель внутри активного буфера
316 Глава 15. Проигрыватель Обратите внимание, что, как и в предыдущих уроках, при размещении больших буферов для видеоприложений можно использовать атрибут far, чтобы память бы- ла выделена за пределами ближнего адресного пространства. Все определения и прототипы, необходимые для модуля Wave . с и других при- ложений, разместим в заголовочном файле Audio PWM. h. 7*....................................................................... ** Audio PWM.h #define FCY 16000000L #define TCYxUS 16 fdefine B_SIZE 2048 // Частота командного цикла // Количество Тс в одной микросекунде // Размер аудиобуфера extern char ABuffer[2][B_SIZE]; extern int CurBuf; extern volatile int AEmptyFlag; // Двойной буфер данных // Индекс активного буфера // Флаг, указывающий, что буфер пуст void initAudio(long bitrate, int skip, int size, int stereo, int fix, int pos); void haltAudio(void); Файл Audio PWM.h находится также на прилагаемом к книге компакт-диске в папке Проек- ты\15 - Проигрыватель\Audio. Тестирование проигрывателя файлов WAVE Итак, мы создали низкоуровневый аудиомодуль и завершили модуль воспроиз- ведения. Настал момент их протестировать на каком-либо музыкальном файле. Соз- дайте новый проект под названием WaveTest и добавьте в него все необходимые модули и их заголовочные файлы: • sdmmc.c; • fileio.c; • audiopwm.c; • wave, с; • sdmmc.h; • file io. h; • audiopwm.h; • wave.h. Затем создайте новый главный модуль wave ties t. с, который будет содержать всего лишь несколько строк кода: вызов функции play () с указанием имени зву- кового файла, предварительно скопированного на карту SD/MMC (назовем его trackOO . wav). 7*......................................................................... * * WaveTest ^include <p24fj128ga010.h> ^include "SDMMC.h" tfinclude "fileio.h" #include " . . /Audio/Audio PWM.h" ^include "../Wave/Wave.h"
Полет 317 main(void) ( TRISA = Oxff00; if (Imount()) PORTA = FError + 0x80; else { if (play("TRACK00.WAV")) PORTA = 0; else PORTA = OxFF; } // Смонтировано while(1) { } // Главный цикл } //main вФайл WaveTest.c находится также на прилагаемом к книге компакт-диске в папке Проек- ты\15 - Проигрыватель. Линейка светодиодов, подключенных к выводам порта А, послужит для ото- бражения кода ошибки в случае сбоя функции mount () или отсутствия файла в карте памяти. Скомпонуйте проект и запрограммируйте код в плату Explorer 16. Не забудьте зарезервировать немного места для кучи, поскольку модуль file io. с использует ее для размещения буферов и структур данных. Для поэтапной проверки рекомендую тестировать программу с помощью фай- лов . wav с нарастающими размерами и частотами дискретизации. Например, пер- вый тест можно выполнить для мопозаписи с восьмибитными выборками и часто- той 8 Квыборок/с. Далее выберите более сложный формат и увеличьте скорость воспроизведения, достигнув па последнем тесте стереозвучания с 16-битными вы- борками и частотой 44 100 выборок/с. Такой поэтапный подход обусловлен необхо- димостью проверить производительность нашего модуля fileio.c. Дело в том, что вместе с ростом частоты дискретизации, количества каналов и размера выборок увеличивается и полоса пропускания, требуемая файловой системой. Это позволяет быстро вычислить уровни производительности, необходимые для различных ком- бинаций перечисленных параметров (табл. 15.4). Таблица 15.4. Уровни производительности для различных аудиопарамотров Файл Размер выборки Кана- лов Частота дискретизации Байтовая скорость Период пере- загрузки (мс) Голос, моно 1 1 8 000 8 000 64,0 Голос, стерео 1 2 8 000 16 000 32,0 Аудио, 8 биг, моно 1 1 22 050 22 050 23,2 Аудио, 8 бит, стерео 1 2 22 050 44 100 11,6 Аудио, 8 бит, скоростное моно 1 1 44 100 44 100 11,6 Аудио, 8 бит, скоростное стерео 1 2 44 100 88 200 5,8 Аудио, 16 бит, моно 2 1 44 100 88 200 5,8 Аудио, 16 бит, стерео 2 2 44 100 176 400 2,9
318 Глава 15. Проигрыватель Табл. 15.4 отображает байтовую скорость, требуемую для каждого файлового формата, т.е. количество байтов, потребляемых функцией воспроизведения за одну секунду (размер выборки х количество каналов х частота дискретизации). Послед- ний столбец указывает на промежуток между моментами заполнения буфера дан- ными (512 / байтовую скорость), т.е. время, доступное для функции play () для чтения следующего сектора из файла . wav. Поскольку модули ШИМ микроконтроллера PIC24 для частоты 44 100 Гц могуг выдавать разрешение менее девяти бит, аудиомодуль предусматривает только ис- пользование старшего байта 16-разрядной выборки. По этой причине попытка вос- произвести файл . wav в одном из двух форматов, указанных в табл. 15.4 последни- ми, не даст никакого повышения качества звучания. Это приведет только к напрас- ной трате памяти в карте SD/MMC. Если необходимо максимизировать доступное пространство запоминающего устройства, то убедитесь, что при копировании файла на карту размер выборки уменьшен до восьми бит. Это позволит записать вдвое больше музыки при том же звуковом разрешении. Если применить описанное выше поэтапное тестирование, то по мере продви- жения вниз по табл. 15.4 окажегся, что за определенной точкой (скорее всего, “Ау- дио, 8 бит, скоростное моно”) в звучании появляются искажения: пропуски, повто- рения и рывки. Это означает, что функция freadM () достигла пределов своих воз- можностей и не в состоянии выполнить требования воспроизведения. Среднее вре- мя загрузки нового буфера данных превышает’ время, необходимое на опустошения буфера, в результате чего функция р!ау() начинает отставать, и подпрограмма воспроизведения повторяет фрагмент файла или воспроизводит буферы, которые еще не до конца заполнены. Оптимизация файлового ввода-вывода При написании библиотеки файлового ввода-вывода (и даже раньше: при соз- дании низкоуровневых функций доступа к карте SD/MMC) нас интересовал только конечный результат. Мы ни разу не попытались достичь доступного уровня произ- водительности, но теперь уделим этому вопросу немного внимания. Раньше мы принципиально не использовали предоставляемых компилятором средств оптими- зации, чтобы каждый пример можно было протестировать с помощью простой бес- платной версии компилятора MPLAB С. Не будем отступать от этого принципа и теперь, повысив производительность приложения за счет других факторов. Первое, что необходимо сделать, — выяснить, когда микроконтроллер PIC24 тратит больше всего времени на чтение данных с карты. Изучив функцию freadM (), что в ней присутствует только два вызова низкоуровневых подпро- грамм. Одна из них — функция readDATA (), предназначенная для захрузки ново- го сектора из текущего кластера, а другая — функция nextFATO, идентифици- рующая следующий кластер при исчерпании текущего кластера. Обе эти функции в свою очередь вызывают функцию readSECTOR () для фактического извлечения блока данных размером 512 байт. Наконец, для передачи блока данных в буфер вы- зывающего приложения используется стандартная С-функция memcpy (). Таким образом, общая производительность функции freadM () зависит от производи- тельности функций readSECTOR () и memcpy (). Профилирование светодиодов При наличии осциллоскопа определить, какая из этих двух подпрограмм несет большую нагрузку, — относительно просто. Как вы, наверное, помните, при разра-
Полет 319 ботке функции readSECTOR () мы использовали один из светодиодов па выводе порта А для сигнализации о завершении операции чтения с карты SD/MMC. Если во время цикла воспроизведения осциллоскоп подключить к аноду соответствующего светодиода, то можно увидеть периодические импульсы, длина которых соответст- вует точному времени, проводимому микроконтроллером PIC24 в ходе передачи данных внутри функции readSECTOR (). Пауза между импульсами пропорцио- нальна времени, проводимому внутри функции memcpy О и в большинстве других подпрограммах, вызываемых из функции freadM (). Теперь корень проблемы можно различить с одного взгляда (рис. 15.8). readSECTORO fread() kJ Рис. 15.8. Осциллоскоп подключен к выводу READ_LED Теперь пег сомнения, что самого пристального нашего внимания требует функ- ция readSECTOR (), поскольку она иснользуег почти весь период длительностью более 10 мс! int readSECTOR(LBA a, char *р) //а - LBA запрашиваемого сектора // р - указатель на буфер сектора //В случае успеха возвращает TRUE ( int г, tout; READ_LED = 1; г = sendSDCmd(READ_SINGLE, (a « 9)); if (r == 0) // Проверяем, была ли принята команда { // Ожидаем ответ tout = 10000; do { г = readSPI(); if (г == DATA_START) break; }while(--tout>0); 11 Если лимит ожидания не истек, считываем сектор данных 512 байт if (tout) { for(i-0; i<512; i++) *p++ = readSPI (); // Игнорируем код CRC readSPI(); readSPI(); } // Поступили данные } // Команда принята // Не забудем отключить карту disableSD();
320 Глава 15. Проигрыватель READ_LED = 0; return (г == DATA—START) ; } // readSECTOR Если внимательно взглянуть на этот листинг, то станет очевидно, что микро- контроллер PIC24 может тратить столько времени только в трех пунктах: • функция sendSDCmd (); • цикл ожидания от карты маркера DATA_START (может быть у нас просто медлкпнодействующая карта SD/MMC?); • цикл чтения из карты всех 512 байт. Для того чтобы определить, какой из трех источников приводит к проблеме, можно просто изменить момент включения и отключения линии READLED. Если перекомпилировать проект и пару раз запустить тест, то окажется, что функции sendSDCmd () соответствует' очень короткие импульсы на контрольном выводе. READ_LED = 1; г = sendSDCmd(READ_SINGLEZ (а « 9) ) ; READ_LED = 0; Это означает, что карта отвечает на команду очень быстро, и угечку времени следует искать в другом месте. Аналогичный результат будет' получен и для цикла ожидания маркера DATA START: READ_LED = 1; // Ожидаем ответ tout = 10000; do { г = readSPI(); if (г == DATA_START) break; } while(—tout>0); READ_LED = 0; Значит проблема в третьем цикле, который внешне совершенно безобидный, и лишь повторяется 512 раз. READ_LED = 1; for(i=0; i<512; i++) * p++ = readSPI(); READ_LED = 0; Именно здесь нам понадобится приложить наши усилия по оптимизации. Пер- вое, что приходит на ум, — попытаться избавиться от вызова функции readSPI (), заменив его просто несколькими строками кода: READ_LED = 1; for(i=0; i<512; i++) ( SPI2BUF = OxFF; // Запись в буфер для передачи while(!(SPl2STATbits.SPIRBF)); // Ожидаем завершения передачи * р++ = SPI2BUF; // Считываем принятое значение } READ_LED = 0; Если теперь откомпилировать проект, то выяснится, что длина контрольного импульса уменьшилась, однако не достаточно, чтобы существенно повысить произ- водительность приложения.
Полет 321 Чтобы узнать больше, заглянем под капот Естественный шаг — выяснить, каким образом компилятор обрабатывает эти три строки кода. Нас интересует конкретный фрагмент листинга дизассемблера. 139: for( i=0; i<512; i++) 011А4 EB0000 clr.w 0x0000 011А6 980750 mov.w 0x0000,[0x001c+10] 011А8 9000DE mov.w [ 0x001c+10],0x0002 011АА 201FF0 mov.w #0xlff,0x0000 011АС 508F80 sub.w 0x0002,0x0000,[OxOOle] 011АЕ 3C0013 bra gts, 0x0011d6 011СЕ 90005Е mov.w [OxOOlc-ьЮ] , 0x0000 011D0 Е80000 inc.w 0x0000,0x0000 011D2 980750 mov.w 0x0000,[0x001c+10] 011D4 37FFE9 bra 0x0011a8 142: { 144: SPI2BUF = OxFF; 011В0 200FF0 mov.w #0xff,0x0000 011В2 881340 mov.w 0x0000,0x0268 146: while(!SP!2STATbits.SPIRBF) 011В4 BFC260 mov.b 0x0260,0x0000 011В6 FB8000 ze.b 0x0000,0x0000 011В8 600061 and.w 0x0000,#1,0x0000 011ВА Е00000 cpO.w 0x0000 011ВС 32FFFB bra z, 0x0011b4 147: *p++ = SPI2BUF; 011ВЕ 4701Е4 add.w OxOOle,#4,0x0006 011С0 780093 mov.w [0x0006],0x0002 011С2 801340 mov.w 0x0268,0x0000 011С4 784100 mov.b 0x0000,0x0004 011С6 780001 mov.w 0x0002,0x0000 011С8 784802 mov.b 0x0004,[0x0000] 011СА Е80081 inc.w 0x0002,0x0002 011СС 780981 mov.w 0x0002,[0x0006] 14 8: } ... <<3десь находится код завершения цикла for>> 011D6 Для выполнения цикла, который на первый взгляд кажется таким простым, по- требовалось более 25 команд. Необходимо каким-то образом упростить самый внутренний цикл while, реализующий ожидание завершения передачи данных пе- риферией SPI. Явно лишней в нем является команда ze.b, которая выглядит по- бочным продуктом поразрядной арифметики, используемой компилятором для про- верки флага SPI2STATbits . SPIRBF flag. Переформатировав код с применением прямого маскирования содержимого ре- гистра SPI2STAT, мы значительно улучшаем ситуацию: for(i=0; i<512; i++) { SPI2BUF = OxFF; while(! (SPI2STAT & 1) ) ; * p++ = SPI2BUF; // Запись в буфер для передачи // Ожидаем завершения передачи // Считываем принятое значение Полученный в результате код всего лишь па одну команду короче, однако не будем забывать, что вышеупомянутая команда повторяется по крайней мере дважды для каждой из 512 итераций.
322 Глава 15. Проигрыватель 139: for( i=0; i<512; i++) 011А4 EB0000 clr.w 0x0000 011А6 980750 mov.w 0x0000,[0x001c+10] 011А8 9000DE mov.w [0x001c+10],0x0002 011АА 201FF0 mov.w #0xlff,0x0000 011АС 508F80 sub.w 0x0002,0x0000,[OxOOle] 011АЕ ЗС0012 bra gts, 0x0011d4 011СС 90005Е mov.w [0x001c+10],0x0000 011СЕ Е80000 inc.w 0x0000,0x0000 011D0 980750 mov.w 0x0000,[0x001c+10] 011D2 37FFEA bra 0x0011a8 142: { 144: SPI2BUF = OxFF; 011В0 200FF0 mov.w #0xff,0x0000 011В2 881340 mov.w 0x0000,0x0268 1 —— "' " " ” ” ’ " •while(‘ (SPI2STAT&1) ) ; 011В4 801300 mov.w 0x0260,0x0000 011В6 600061 • and.w 0x0000,#1,0x0000 011В8 Е00000 cpO.w 0x0000 011ВА 3.2FFFC bra z, 0x001lb4 146: *p++’= SPI2BUF; 011ВС 4701Е4 add.w OxOOle,#4,0x0006 011ВЕ 780093 mov.w [0x0006],0x0002 011С0 801340 mov.w 0x0268,0x0000 011С2 784100 mov.b 0x0000,0x0004 011С4 780001 mov.w 0x0002,0x0000 011С6 784802 mov.b 0x0004,[0x0000] 011С8 Е80081 inc.w 0x0002,0x0002 011СА 780981 mov.w 0x0002, [0x0006] 147: } ... <<3десь находится код завершения цикла for>> 011D4 Следующее, что мы можем сделать, — сократить объем передачи данных по программному стеку, выделив отдельные регистры под часто используемые пере- менные. Один из таких кандидатов — переменная i, выполняющая роль индекса в цикле for, а другой — указатель р. Компилятор СЗО позволяет связать перемен- ную с регистром с помощью следующего синтаксиса: register unsigned i asm(’’w5”); Однако результат не гарантирован, если заданный регистр не будет доступен. Обычно в качестве сверхоперативной памяти компилятор использует' первые четыре регистра W0...W3, не позволяя нам задействовать их под наши задачи. Кроме того, регистр не может быть параметром функции, что, к сожалению, относится к указа- телю р. Впрочем, это ограничение не составит труда обойти, скопировав содержи- мое р в новый указатель (назовем его q): register unsigned i asm("w5"); register char * q asm("w6"); q = p; for( i=0; i<512; i++) { SPI2BUF = OxFF; while(! (SPI2STAT&1) ) ; * q++ = SPI2BUF; // Ожидаем завершения передачи // Считываем принятое значение
Полет 323 На этот раз, перекомпилировав код, можно заметить значительное сокращение размеров внешнего цикла и упрощение кодирования цикла for: 139: for(i=0; i<512; i++) 011А6 ЕВ0280 clr.w 0x000a 011А8 201FF0 mov.w #0xlff,0x0000 011АА 528F80 sub.w 0x000a,0x0000,[OxOOle] 011АС 3E000D bra gtu, 0x0011c8 011С4 Е80285 inc.w 0x000a,0x000a 011С6 37FFF0 bra 0x0011a8 142: { 144: SPI2BUF = OxFF; 011АЕ 200FF0 mov.w #0xff,0x0000 011В0 881340 mov.w 0x0000,0x0268 145: while(’(SPI2STAT&1)) 011В2 801300 mov.w 0x0260,0x0000 011В4 600061 and.w 0x0000,#1,0x0000 011В6 Е00000 cpO.w 0x0000 011В8 32FFFC bra z, 0x0011b2 146: *q++ = SPI2BUF; 011ВА 801340 mov.w 0x0268,0x0000 011ВС 784080 mov.b 0x0000,0x0002 011ВЕ 780006 mov.w 0x000c,0x0000 011С0 784801 mov.b 0x0002,[0x0000] 011С2 Е80306 inc.w 0x000c,0x000c ... <<3десь находится код завершения цикла for>> 011С8 Код сократился на 17 команд! Последний шаг — попытаться изменить тип цик- ла для подсчета 512 байт данных. На этот раз мы воспользуемся простым циклом do-while: register unsigned i asm(”w5”); register char * q asm(”w6”); q = p; i = 512; do { SPI2BUF = OxFF; while(!(SPI2STAT&1)); *q++ = SPI2BUF; } while (--i>0) ; // Ожидаем завершения передачи // Считываем принятое значение Это дает нам паи лучшие результаты: всего 15 инсгрукций! 011A6 202005 mov. w #0x200,0x000a 141: 144: 011A8 200FF0 mov.w do{ SPI2BUF = OxFF; #0xff,0x0000 011AA 881340 mov.w 0x0000,0x0268 145: 011AC 801300 mov.w while(!(SPI2STAT&1)); 0x0260,0x0000 011AE 600061 and.w 0x0000,#1,0x0000 011B0 E00000 cpO. w 0x0000 011B2 32FFFC bra z, OxOOllac 146: *q+4 011B4 = SPI2BUF; 801340 mov.w 0x0268,0x0000 011B6 784080 mov.b 0x0000,0x0002 011B8 780006 mov.w 0x000c,0x0000
324 Глава 15. Проигрыватель 011ВА 784801 mov. b 0x0002,[0x0000] 011ВС Е80306 inc. w 0x000c,0x000c 148: } while (--i>0) 0-11 BE Е90285 dec. w 0x000a,0x000a 011С0 Е00005 cpO . w 0x000a 011С2 3AFFF2 bra nz 0x0011a8 011С4 Настал момент запрограммировать новый код в плату Explorer 16 и еще раз про- верить, сколько времени затрачивает функция readSECTOR () па чтение полного сектора данных. Вы будете приятно удивлены, что это время уменьшилось менее, чем до 1,5 мс. Этого будет достаточно для воспроизведения даже наиболее требова- тельных файлов . wav. Разбор полета Этот последний урок стал, пожалуй, идеальным завершением нашего учебного курса, поскольку в нем смешались многие программные и аппаратные возможности как цифрового, так и аналогового мира. В начале мы использовали периферийный модуль Output Compare для получения аналоговых сигналов в спектре звуковых частот. С помощью программного модуля fileio.c, разработанного в предыду- щем уроке, мы реализовали воспроизведение музыкальных файлов в формате WAVE, хранимых на карте SD/MMC. Но разработанный проигрыватель — это только отправная точка. Существует бесчисленное множество вариантов для расширения проекта. Все заключается толь- ко в силе вашего воображения. Советы и хитрости Для модуля ШИМ критические точки — начало и конец воспроизведения. В со- стоянии покоя конденсатор выходного фильтра разряжен, и выходное напряжение составляет' О В. Однако, как только начинается воспроизведение, коэффициент за- полнения 50% быстро поднимет выходной уровень примерно до 1,5 В, что дает громкий и неприятный па слух щелчок. Противоположное явление могло бы про- изойти в конце воспроизведения, если бы вместо запрета прерываний мы отключи- ли модули ШИМ. Все это — характерные проявления при включении и выключе- нии схем с аналоговым усилителем, и простой способ обойти их — добавить пару строк кода. Перед разрешением прерываний от таймера и началом воспроизведения необходимо вставить простой цикл для постепенного увеличения выходного коэф- фициента заполнения от нуля до значения первой извлеченной из буфера выборки. Упражнения 1. Изучите методику декодирования сигналов ADPCM, используемую в голосо- вых сообщениях (см. руководство по применению AN643). 2. Реализуйте поиск всех файлов . wav на карте и сформируйте “список воспроиз- ведения”. 3. С помощью генератора псевдослучайных чисел реализуйте режим случайной выборки дорожек с постепенной очисткой списка воспроизведения.
Ссылки 325 4. Поэкспериментируйте с базовыми методиками цифровой фильтрации для уст- ранения одних частот и усиления других или же простого искажения звуков и голосов. Ссылки • http://en.wikipedia.org/wiki/RIFF — описание файлового формата RIFF ; • http://en.wikipedia.org/wiki/WAV — описание файлового формата WAVE; • http://ccrma.stantord.edu/courses/422/projects/WaveFormat — еще одно превосходное описание формата WAVE.
Контрольные списки Это приложение содержит конспективные описания различных операций, вы- полняемых в интегрированной среде разработки MPLAB. Настройка нового проекта 1. Выберите команду меню Project ► Project Wizard. 2. Шаг 1 — выбор устройства (например, PIC24FJ128GA010). 3. Шаг 2 — выбор набора инструментов. Выберите элемент MPLAB СЗО Compiler. 4. Шаг 3 — диалоговое окно New Project. Нажмите кнопку Browse. 5. Выберите или создайте папку. 6. Введите имя проекта в поле Project Name. 7. Шаг 4 — копирование файлов (при желании этот этап можно пропустить). 8. Шаг 5 — последнее окно мастера. Нажмите кнопку Готово. 9. Переходите к контрольному списку “Добавление к проекту сценария компо- новщика”. Добавление к проекту сценария компоновщика 1. Удостоверьтесь, что установлен флажок возле пункта меню View ► Project. 2. Выберите команду меню Project ► Add Files to Project. 3. Выберите каталог \Program Files\Microchip\MPLAB C30\support\ gid. 4. Выберите в раскрывающемся списке Тип файлов элемент Linker Scripts (*.gld). 5. Выберите в списке файл p24FJ128GA010. gid. 6. Выберите команду меню Project ► Save Project. Создание и добавление к проекту нового файла 1. Выберите команду меню Project ► Add Files to Project и загрузите интересующий файл . с или . h. 2. Дважды щелкните мышью на файле в окне проекта, чтобы открыть его текст в редакторе, после чего введите заголовок в виде комментария или вставьте его из буфера обмена. 3. С помощью директивы ttinclude включите в файл ссылку на заголовочный файл p24FJ128GA010.h. 4. В случае необходимости добавьте новый код. 5. Выберите команду меню File ► Save. 6. Выберите команду меню Project ► Save Project. Добавление файлов в проект (метод А) 1. Удостоверьтесь, что установлен флажок возле пункта меню View ► Project.
Добавление файлов в проект (метод Б) 327 2. Выберите команду меню Project ► Add Files to Project и зафузите интересующий файл . с, . о или . h. 3. Выберите команду меню Project ► Save Project. Добавление файлов в проект (метод Б) 1. Удостоверьтесь, что установлен флажок возле пункта меню View ► Project. 2. Щелкните правой кнопкой мыши па элементе па вкладке Files окна проекта. 3. Выберите в контекстном меню команду Add Files и загрузите интересующий файл . с, . о или . h. 4. Выберите команду меню Project ► Save Project. Добавление текстовых файлов в проект 1. С помощью команды меню File ► Open откройте существующий файл. 2. Когда курсор находится внутри редактора, щелкните правой кнопкой мыши. 3. Выберите в контекстном меню команду Add to Project. 4. Выберите команду меню Project ► Save Project. Настройка отладки в MPLAB SIM 1. Выберите пункт меню Debugger ► Select Tool ► MPLAB SIM. 2. Выберите команду меню Debugger ► Settings. 3. На вкладке Osc/Trace укажите в ноле Processor Frequency значение 32 MHz (для имитации с помощью платы Explorer 16). 4. На вкладке Osc/Trace установите флажок Trace All. 5. На вкладке Animation укажите в поле Animate Step Time значение 500 для боль- шой скорости или 10 для малой скорости. 6. На вкладке UART1 Ю установите флажок Enable UART1 Ю. 7. В группе Output выберите переключатель Window. 8. Нажмите кнопку Применить или ОК. Характеристики семейства PIC24FJ • Диапазон напряжений Vdd — 2,0..3,6 В. • цифровые входы/выходы — поддерживают напряжение 5 В; • аналоговые входы — Vdd максимум +0,3 В. Сборка проекта 1. Выберите команду меню Project ► Build Options ► Project. 2. На вкладке General в поле Library Path введите значение C:\Program Files\ Microchip\MPLAB C30\Iib. 3. На вкладке MPLAB СЗО выберите в раскрывающемся списке Categories элемент General. 4. Установите флажок Generate debugging information. 5. Выберите в раскрывающемся списке Categories элемент Optimization.
328 Контрольные списки 6. В поле Optimization Level выберите на время отладки элемент 0 или 1. 7. На время отладки сбросьте все флажки в группе Specific Optimizations. 8. На вкладке MPLAB LINK30 укажите размер кучи в поле Heap size, если в про- грамме используется функция malloc (). 9. Если используется отладчик ICD2, установите флажок Link for ICD2. 10. Нажмите кнопку OK. 11. С помощью контрольного списка “Добавление файлов в проект” добавьте в проект все необходимые файлы .с, .Ии .о. 12. Последуйте контрольному списку “Добавление к проекту сценария компонов- щика”. 13. Выберите команду меню Project ► Build АН или нажмите комбинацию клавиш <CtrHF10>. Если модифицировались только некоторые модули, то можно вос- пользоваться командой меню Project Make или клавишей <F10>. Настройка логического анализатора 1. Выберите команду меню View ► Simulator Logic Analyzer. 2. Выберите команду меню Debugger Settings и перейдите на вкладку Osc/Trace. 3. Установите в диалоговом окне Simulator Settings флажок Trace АП и нажмите кнопку ОК. 4. В диалоговом окне Logic Analyzer нажмите кнопку Channels. 5. Выберите все требуемые сигналы в списке Available Signals и нажмите кнопку Add. 6. С помощью кнопок Move Up и Move Down настройте порядок сигналов в списке Selected Signals. 7. Нажмите кнопку ОК. Характеристики микроконтроллера PIC24FJ128GA010 • Максимальная рабочая частота — 32 МГц. • Объем ОЗУ общего назначения — 8 192 байт. • Объем флэш-памяти программ — 128 Кбайт. Настройка отладчика ICD2 1. Подайте питание па целевую плату. 2. Подключите отладчик ICD2 к плате. 3. Подключите отладчик ICD2 к ПК и дождитесь тройного звукового сигнала. 4. Выберите команду меню Debugger ► Select Tool ► MPLAB ICD2 и закройте поя- вившийся на экране мастер, нажав кнопку Отмена. 5. Выберите команду меню Debugger ► Settings. 6. На вкладке Status (Состояние) установите в случае необходимости флажок Automatically connect at startup (Автоматически подключать при запуске) (не ре- комендуется). 7. Удостоверьтесь, что на вкладке Power (Питание) сброшен флажок Power target circuit from MPLAB ICD 2 (Запитывать целевую схему от MPLAB ICD 2).
Программирование в MPLAB ICD2 329 8. На вкладке Program (Программирование) выберите переключатель Allow ICD 2 to select memories and ranges (Позволить ICD2 выбирать память и диапазоны). 9. Установите в случае необходимости флажок Program after successful build (Про- граммировать после успешной сборки) (не рекомендуется). 10. Установите в случае необходимости флажок Run after successful program (Запус- кать после успешного программирования) (не рекомендуется). 11. Нажмите кнопку ОК. 12. Выберите команду меню Debugger ► Connect, если не был установлен флажок из пункта 6. 13. Удостоверьтесь, что установлен флажок Link for ICD2 (см. пункт 9 контрольного списка “Сборка проекта”). Программирование в MPLAB ICD2 1. Подайте питание па целевую плату. 2. Подключите отладчик ICD2 к плате. 3. Подключите отладчик ICD2 к ПК и дождитесь тройного звукового сигнала. 4. Выберите команду меню Debugger ► Select Tool ► MPLAB ICD2 и закройте поя- вившийся па экране мастер, нажав кнопку Отмена. 5. Выберите команду меню Debugger ► Connect, если не был установлен флажок из пункта 6 контрольного списка “Настройка отладчика ICD2”. 6. Последуйте контрольному списку “Конфигурация для демонстрационной платы Explorer 16”. 7. Выберите команду меню Debugger ► Program Target Device. Конфигурация для демонстрационной платы Explorer! 6 1. Выберите команду меню Configure ► Configuration Bits. 2. Выберите элемент Primary Oscillator. 3. В столбце Setting выберите элемент HS Oscillator Enabled. 4. Для элемента Primary Oscillator Output Function выберите в столбце Setting эле- мент OSCO pin has clock out function. 5. Для элемента Clock Switching and Monitor выберите в столбце Setting элемент Sw Disabled, Mon Disabled. 6. Для элемента Oscillator Select выберите в столбце Setting элемент Primary Oscilla- tor with PLL module. 7. Для элемента Watchdog Timer Postscaler выберите в столбце Setting элемент 1:32,768. 8. Для элемента WDT Prescaler выберите в столбце Setting элемент 1:128. 9. Для элемента Watchdog Timer Enable выберите в столбце Setting элемент Disable. 10. Для элемента Comm Channel Select выберите в столбце Setting элемент ЕМ1С2/ EMUD2 shared with PCG2/PGD2. 11. Для элемента Set Clip On Emulation Mode выберите в столбце Setting элемент Re- set Into Operational Mode. 12. Для оставшихся грех параметров конфигурации выберите в столбце Setting эле- мент Disabled.
330 Контрольные списки Аварийные ситуации Перезапуск драйверов USB (неудачное подключение ICD2) 1. Выберите команду меню Debugger ► Select Tool ► None. 2. Выберите команду меню Project ► Close, после чего сохраните и закройте про- ект. 3. Выберите команду меню File ► Exit. 4. Отсоедините кабель USB. 5. Выключите и включите питание целевой платы. 6. Запустите MPLAB. 7. Подсоедините кабель USB. 8. Выберите команду меню Debugger ► Select Tool ► MPLAB ICD2. 9. Выберите команду меню Debugger ► Connect. Невозможно установить точку прерывания (при работе с ICD2) • Удостоверьтесь, что строка исходного кода на С не закомментирована. • Удостоверьтесь, что не установлено более четырех точек прерывания (для про- смотра списка точек прерывания нажмите клавишу <F2>). • Удостоверьтесь, что строка исходного кода па С не содержит только объявле- ние переменной. • Удостоверьтесь, что файл с исходным кодом на С включен в список файлов проекта. • Удостоверьтесь, что перед установкой точки прерывания проект был собран. Потерян курсор при пошаговой отладке в MPLAB SIM 1. Проверьте значение счетчика команд, отображаемое в строке состояния MPLAB. 2. Если PC < 300, то, скорее всего, произошел сброс и вы находитесь внутри кода СО. Для решения проблемы выполните одно из следующих действий: • установите курсор на следующем операторе языка С и выберите команду Run То Cursor; • продолжайте пошаговую отладку до тех пор, пока курсор не появится в главной программе; • найдите положение счетчика команд в окне Program Memory. 3. Если PC >= 300, то, скорее всего, вы оказались внутри библиотечной функции. Для решения проблемы выполните одно из следующих действий: • установите курсор на следующем операторе языка С и выберите команду Run То Cursor; • если уже установлены точки прерывания, выберите команду Run. 4. Если курсор все равно не появился, выберите команду Reset и выполните про- грамму сначала.
Демонстрационная плата Explorer16 331 После выбора команды Halt MPLAB “зависает” (при работе с ICD2) Подождите, потому что MPLAB, возможно: • загружает в окно Watch содержимое большого массива; • обновляет содержимое окна Special Function Register (если оно открыто); • обновляет содержимое окна File Registers (если оно открыто); • обновляет содержимое окна Variables (если оно открыто и содержит большой объект). Получив контроль над MPLAB, закройте все окна с данными или удалите из них ссылки на большие объекты, после чего можете продолжать работу. Демонстрационная плата Explorer16 • Напряжение питания — 9.. 15 В (защита от обратной полярности). • Основной осциллятор — кристалл частоты 8 МГц (для получения 32 МГц ис- пользуются четыре PLL). • Дополнительный осциллятор — 32 768 Гц (соединен с тактовым генератором таймера Timerl).
Содержимое прилагаемого к книге компакт-диска Прилагаемый к книге компакт-диск содержит следующие папки: MPLAB — интегрированная среда разработки MPLAB 7.40, компилятор MPLAB СЗО и документацию по использованию этих средств; Документация — описания различных программных и аппаратных средств от компании Microchip; Проекты — исходные файлы всех рассмотренных в книге проектов.
Издательство “МК-Пресс” представляет Тим Уилмсхерст Разработка встроенных систем с помощью микроконтроллеров PIC. Принципы и практические примеры (+CD) ISBN 978-5-903383-61-0 544 стр., мягкая обложка Благодаря полезным примерам и иллюстрациям, эта книга дает глубокие познания в сфере проецирования систем с помощью микроконтроллеров PIC, а также — программирова- ния этих устройств на ассемблере и С. Подробно рассмотрены микроконтроллеры 16F84A, 16F873A и 18F242. Даны примеры реальных проектов, включая модель робота, выполненною в виде транспортного средства с автономным управлением. Дополнительно рассматриваются такие вопросы повышенной сложности, как применение устройств в сетевой среде и построение операционных систем реального времени. Барри Брэй Применение микроконтроллеров PIC18. Архитектура, программирование и построение интерфейсов с применением С и ассемблера (+CD) ISBN 978-5-7931-0516-3 576 стр., мягкая обложка Сегодня микроконтроллеры используются повсеместно в автомобилях, бытовой технике, промышленном и медицинском оборудовании и т.п. Этот учебник дает всестороннее представ- ление об архитектуре, программировании и построении интерфейсов этого современного чуда. На примере семейства микроконтроллеров PIC18 производства Microchip в книге объясняется архитектура, программирование и построение интерфейсов. Семейство PIC18 выбрано не слу- чайно, поскольку оно относится к самым современным восьмиразрядным микроконтроллерам. Изложенный в книге материал также применим как к более ранним версиям микроконтроллеров Microchip, так и к аналогичным устройствам других производителей. Он рассчитан на опытных практиков и радиолюбителей, интересующихся микроконтроллерами. Книги издательства “МК-Пресс” можно заказать: по адресу: 02002, г.Киев, а/я 294, по телефону/факсу: (044) 517-73-77, no e-mail: mfo@mk-press.com или приобрести в магазине “Микроника” по адресу: г.Киев, ул. М.Расковой, 13 Посетите наш Internet-магазин: http://www.mk-press.com
Издательство “МК-Пресс” представляет Кравченко А.В. 10 практических устройств на AVR-микроконтроллерах. Книга 1 (+CD) ISBN 978-966-8806-41-4 224 стр., мягкая обложка Данная книга открывает серию сборников с пракгическими примерами применения мик- роконтроллеров. В ней рассмотрены десять завершенных устройств на базе микрокон- троллеров AVR, которые можно легко собрать в домашних условиях и применять в быту или профессионалы юй деятельности: генератор световых эффектов; счетчик событий; музыкаль- ный звонок; индикатор уровня звука; повышающий преобразователь, схема управления шаго- вым двигателем; цифровой термометр и др. Благодаря подробному анализу аппаратной и программной части устройств, книга будет интересна и полезна как начинающим, так и опытным радиолюбителям, желающим изучить ме- тоды эффективного применения микроконтроллеров. Заец Н.И. Радиолюбительские конструкции на PIC-микроконтроллерах (+CD). Книга 4 ISBN 966-8806-42-1 336 стр., мягкая обложка Данная книга — практическое пособие по освоению микроконтроллеров PICmicro компа- нии Microchip и другой современной элементной базы, наподобие индикаторов, выполненных по COG-технологии. Рассмотрены алгоритмы работы, схемы и программы для различных полезных устройств: многофункциональных часов, отображающих текущее время и температуру воздуха; автомобильных часов, фиксирующих время в пути и сообщающих о поломке реле-регулятора; автомата включения освещения; цифрового устройства для блока питания с установкой защиты по току и напряжению; специализированных термометров и др. Для начинающих дана глава о наладке устройств на микроконтроллерах. Книга предназначена для широкого круга радиолюби- телей, а также может быть полезна студентам, изучающим программирование микроконтролле- ров. Книги издательства “МК-Пресс” можно заказать: по адресу: 02002, г.Киев, а/я 294, по телефону/факсу: (044) 517-73-77, по e-mail: info@mk-press.com или приобрести в магазине “Микроника” по адресу: г.Киев, ул. М.Расковой, 13 Посетите наш Internet-магазин: http://www.mk-press.com
ПРОГРАММИРОВАНИЕ на микроконтроллеров PIC24 Новые 16-разрядные микроконтроллеры PIC24 предоставляют разработчикам встроенных систем больше быстродействия, памяти и периферии, чем любые их предшественники семейства PIC, и данная книга раскрывает все, что необходимо знать об этой прогрессивной архитектуре от компании Microchip. В увлекательной, легкой для восприятия манере автор рассматривает следующие темы: - Базовые операции ввода-вывода - Многозадачность с помощью прерываний PIC24 - Новая аппаратная периферия - Управление ЖК-дисплеями - Формирование звуковых и видеосигналов - Доступ к запоминающим устройствам большой емкости - Совместное использование с ПК файлов на носителях большой емкости - Эксперименты с демонстрационной платой Explorer 16 - Методы отладки с помощью инструментария MPLAB-SIM и ICD2 Лусио ди Джасио, эксперт из компании Microchip, предлагает свой уникальный взгляд на революционную технологию PIC24, проводя читателя от основ 16-разрядной архитектуры до самых сложных программных разработок средствами языка С. Книга содержит множество интересных примеров с подробным описанием кода и будет полезна как опытным PIC-разработчикам, так и новичкам в мире встроенных систем. Автор показывает, как избегать распространенных "ловушек", эффективно решать реальные задачи и оптимизировать код под новые возможности микроконтроллеров PIC24. Компакт-диск содержит: - Интегрированную среду разработки MPLAB 7.40 - Компилятор MPLAB СЗО - Документацию по использованию различных программных и аппаратных средств от компании Microchip - Исходные файлы всех рассмотренных в книге проектов. Корона-Век ozon.ru • • • • • выбирайте ИНН IIII ЦНИИ 1033960635